用JavaScript無canvas來完成一個柱狀圖表

前言

提起數據可視化技術,都難免會讓人想到echarts,而最簡單的入門圖表就是柱狀圖了。它是基於canvas來實現的,而我在想,若是不用canvas,我是否能不用canvas實現柱狀圖,通過個人探索,終於實現了一款柱狀圖表。css

讓咱們查看一個已經完成的在線示例,以下圖所示:html

分析實現思路

首先,咱們須要肯定柱狀圖表有哪些部分,第一右上角頭部有legend部分,第二有xy軸部分,第三就是柱狀圖部分了。好了肯定了有哪些部分,咱們就能夠很好的實現了,好了,讓咱們進入正題吧。git

實現靜態頁面結構

編寫html

目前咱們完成的成品是已經封裝好的,而後頁面就只有一個容器元素。但咱們最開始確定不能這樣寫,咱們先寫一個寫死的結構以下所示:github

<div id="weekCost" class="ew-charts">
        <ew-charts-body>
            <ew-charts-legend>
                <i class="leg-1"></i>
                <span>直接訪問</span>
                <i class="leg-2"></i>
                <span>郵件營銷</span>
                <i class="leg-3"></i>
                <span>聯盟廣告</span>
                <i class="leg-4"></i>
                <span>視頻廣告</span>
                <i class="leg-5"></i>
                <span>搜索引擎</span>
            </ew-charts-legend>
            <ew-charts-x>
                <div class="x-1" style="letter-spacing:2px;">一月</div>
                <div class="x-2" style="letter-spacing:2px;">二月</div>
                <div class="x-3" style="letter-spacing:2px;">三月</div>
                <div class="x-4" style="letter-spacing:2px;">四月</div>
                <div class="x-5" style="letter-spacing:2px;">五月</div>
                <div class="x-6" style="letter-spacing:2px;">六月</div>
                <div class="x-7" style="letter-spacing:2px;">七月</div>
            </ew-charts-x>
            <ew-charts-y>
                <div class="y-1">500</div>
                <div class="y-2">1000</div>
                <div class="y-3">1500</div>
                <div class="y-4">2000</div>
            </ew-charts-y>
            <ew-charts-zone>
                <div class="zone-1">
                    <bar class="bar-1 dataId-1-1" data-value="320"></bar>
                    <bar class="bar-2 dataId-1-2" data-value="120"></bar>
                    <bar class="bar-3 dataId-1-3" data-value="220"></bar>
                    <bar class="bar-4 dataId-1-4" data-value="150"></bar>
                    <bar class="bar-5 dataId-1-5" data-value="862"></bar>
                </div>
                <div class="zone-2">
                    <bar class="bar-1 dataId-2-1" data-value="332"></bar>
                    <bar class="bar-2 dataId-2-2" data-value="132"></bar>
                    <bar class="bar-3 dataId-2-3" data-value="182"></bar>
                    <bar class="bar-4 dataId-2-4" data-value="232"></bar>
                    <bar class="bar-5 dataId-2-5" data-value="1018"></bar>
                </div>
                <div class="zone-3">
                    <bar class="bar-1 dataId-3-1" data-value="301"></bar>
                    <bar class="bar-2 dataId-3-2" data-value="101"></bar>
                    <bar class="bar-3 dataId-3-3" data-value="191"></bar>
                    <bar class="bar-4 dataId-3-4" data-value="201"></bar>
                    <bar class="bar-5 dataId-3-5" data-value="964"></bar>
                </div>
                <div class="zone-4">
                    <bar class="bar-1 dataId-4-1" data-value="334"></bar>
                    <bar class="bar-2 dataId-4-2" data-value="134"></bar>
                    <bar class="bar-3 dataId-4-3" data-value="234"></bar>
                    <bar class="bar-4 dataId-4-4" data-value="154"></bar>
                    <bar class="bar-5 dataId-4-5" data-value="1026"></bar>
                </div>
                <div class="zone-5">
                    <bar class="bar-1 dataId-5-1" data-value="390"></bar>
                    <bar class="bar-2 dataId-5-2" data-value="90"></bar>
                    <bar class="bar-3 dataId-5-3" data-value="290"></bar>
                    <bar class="bar-4 dataId-5-4" data-value="190"></bar>
                    <bar class="bar-5 dataId-5-5" data-value="1679"></bar>
                </div>
                <div class="zone-6">
                    <bar class="bar-1 dataId-6-1" data-value="330"></bar>
                    <bar class="bar-2 dataId-6-2" data-value="230"></bar>
                    <bar class="bar-3 dataId-6-3" data-value="330"></bar>
                    <bar class="bar-4 dataId-6-4" data-value="330"></bar>
                    <bar class="bar-5 dataId-6-5" data-value="1600"></bar>
                </div>
                <div class="zone-7">
                    <bar class="bar-1 dataId-7-1" data-value="320"></bar>
                    <bar class="bar-2 dataId-7-2" data-value="210"></bar>
                    <bar class="bar-3 dataId-7-3" data-value="310"></bar>
                    <bar class="bar-4 dataId-7-4" data-value="410"></bar>
                    <bar class="bar-5 dataId-7-5" data-value="1570"></bar>
                </div>
            </ew-charts-zone>
        </ew-charts-body>
    </div>

編寫css

接下來就須要根據頁面元素,一個一個的添加樣式了,這是一個慢工細活的過程,須要慢慢來。web

/**
* 功能:普通頁面樣式設置
**/
/*********************************************/
/* 樣式初始化部分 */
/*********************************************/
* {
    margin: 0;
    padding: 0;
}
body,html {
    height: 100%;
    font: 20px "微軟雅黑";
    -webkit-user-select: none;
    -moz-user-select: none;
    -ms-user-select: none;
    user-select: none;
    overflow: hidden;
}
/* 轉換爲IE盒子模型 */
*,*::before,*::after {
    box-sizing: border-box;
}

/* 手型按鈕 */
button,
input[type="button"],
input[type="submit"],
input[type="reset"],
input[type="radio"],
input[type="checkbox"],
a {
    cursor: pointer;
}

button,
input,
textarea,
select {
    outline: none;
}
@charset "utf-8";

/**
* 功能:統計圖表樣式
**/
/**** 圖表自定義標籤初始化部分 ****/
.ew-charts,
ew-charts-body,
ew-charts-x,
ew-charts-y,
ew-charts-zone,
ew-charts-legend {
    display: block;
}

ew-charts-x,
ew-charts-x>div,
ew-charts-y,
ew-charts-y>div {
    box-sizing: border-box;
    position: absolute;
    overflow: hidden;
}

ew-charts-zone,
ew-charts-zone>div,
ew-charts-zone>div bar {
    box-sizing: border-box;
}

ew-charts-body,
ew-charts-zone>div,
ew-charts-zone>div bar {
    position: relative;
}

ew-charts-zone,
ew-charts-zone>div bar,
ew-charts-legend,
ew-charts-zone>div bar>span {
    position: absolute;
}

/* 圖表容器 */
.ew-charts {
    width: 100%;
    height: 100%;
    color: #f8f5fa;
    background: linear-gradient(to right, #234, #789);
    margin: auto;
    color: #b3b3b3;
}

/*表體*/
ew-charts-body {
    width: 100%;
    height: 100%;
    font-size: 16px;
}

/*X軸*/
ew-charts-x {
    width: 90%;
    height: 8%;
    border-top: 1px solid #fefefe;
    left: 6%;
    bottom: 0;
}

ew-charts-x>div {
    height: 100%;
    text-align: center;
    line-height: 30px;
    top: 0;
}

/*Y軸*/
ew-charts-y {
    width: 6%;
    height: 80%;
    border-right: 1px solid #fefefe;
    overflow: visible;
    left: 0;
    top: 12%;
}

ew-charts-y>div {
    width: 100%;
    height: 24px;
    text-align: right;
    padding-right: 6px;
    left: 0;
}

/*表格數據區間*/
ew-charts-zone {
    width: 90%;
    height: 80%;
    left: 6%;
    top: 12%;
}

ew-charts-zone>div {
    height: 100%;
    float: right;
}

ew-charts-zone>div bar {
    height: 0;
    bottom: 0;
    border-top-left-radius: 3px;
    border-top-right-radius: 3px;
    text-shadow: 0 0 5px rgba(255, 255, 255, 0.8);
    transition: 0.6s cubic-bezier(.19, .55, .58, 1.3);
    /*默認值設置*/
    background-color: #606060;
    border: 1px solid #cdcdcd;
    box-shadow: 0 0 5px #606060;
}

ew-charts-zone>div bar:hover {
    z-index: 10;
}

ew-charts-zone>div bar>span {
    left: 50%;
    top: -40px;
    transform: translateX(-50%);
    font: 32px "方正姚體", "arial";
    opacity: 0;
}

ew-charts-zone>div bar>span.animation {
    animation: data-value-show 0.6s forwards;
}

/*圖注*/
ew-charts-legend {
    top: 10px;
    right: 4%;
}

ew-charts-legend i,
ew-charts-legend span {
    display: inline-block;
    vertical-align: middle;
}

ew-charts-legend i {
    width: 34px;
    height: 20px;
    border-radius: 3px;
    margin-left: 12px;
    margin-right: 6px;
    background-color: #606060;
    border: 1px solid #cdcdcd;
}
ew-charts-legend span {
    letter-spacing: 2px;
}
/*圖表動畫部分*/
@keyframes data-value-show {
    0% {
        opacity: 0;
    }

    100% {
        opacity: 1;
    }
}

編寫js

首先,咱們須要定義一個函數,用於封裝。apache

function ewCharts(options) {
    //這裏是判斷傳入的參數中是否含有color屬性,從而給予color屬性值
    if (!Array.isArray(options.color) || options.color.length !== options.data.Y.length) {
        let len = options.data.Y.length - options.color.length;
        for (let i = 0; i < len; i++) {
            options.color.push('#ffffff');
        }
    }
    //爲後期的擴展作準備,type類型爲bar就是默認的柱狀圖
    options.type = options.type === "bar" ? options.type : "bar";
    //將參數賦值到實例上
    this.options = options;
    //開始初始化
    this.init(options);
}

接着,咱們能夠看到頁面效果顏色有點高亮,接下來就是完成顏色的高亮效果工具函數,以下所示:canvas

/**
 * 顏色高亮
 */
ewCharts.prototype.lightColor = function (color) {
    // 傳入的顏色爲16進制顏色模式,如:#ffffff
    let everyColorLight = function (lightColor) {
        // 將傳入的顏色轉換成16進制數字,而後再乘以1.6至關於將顏色變亮1.6倍
        const value = Math.round(parseInt(lightColor, 16) * 1.6);
        // 值有一個最小值與最大值,當超過255則等於255,最小值不能小於16
        return (value >= 255 ? 255 : value <= 16 ? 16 : value).toString(16);
    }
    // 至關於處理每一區間的顏色代碼,除了#以外的,每2位表明一種顏色,如#fef2f3,則f2表明紅色區間,f2表明綠色區間,f3表明藍色區間
    return '#' + everyColorLight(color.slice(1, 3)) + everyColorLight(color.slice(3, 5)) + everyColorLight(color.slice(5, 7));
}

而後,咱們須要建立一個設置樣式的函數,以下所示:api

/**
 * 樣式規則設置
 */
ewCharts.prototype.setStyle = function () {
    //這裏的操做無非就是判斷頁面中是否含有link標籤,若是含有,就將樣式規則插入到該標籤所包含的樣式表中
    let link = this.$('link', false), linkIndex = 0;
    for (let i = 0, len = link.length; i < len; i++) {
        if (/\w+\.css/.test(link[i].getAttribute('href'))) {
            linkIndex = i;
        }
    }
    //api文檔https://www.w3school.com.cn/xmldom/met_cssstylesheet_insertrule.asp
    return link[linkIndex].sheet.insertRule.bind(link[linkIndex].sheet);
}

而後,咱們再來完成一個獲取DOM元素的函數封裝,以下所示:數組

/**,
 * 獲取DOM元素
 */
ewCharts.prototype.$ = function (selector, isSingle) {
    // 若是傳入的包含#,則是惟一的元素執行querySelector方法,不然根據傳入的布爾值來判斷執行哪一個方法查詢DOM
    isSingle = selector.indexOf('#') > -1 ? true : typeof isSingle === 'boolean' ? isSingle : true;
    return isSingle ? document.querySelector(selector) : document.querySelectorAll(selector);
}

而後,咱們就來完成初始化函數,以下所示:app

/**
 * 初始化
 */
ewCharts.prototype.init = function (options) {
    // 設置樣式規則
    let setStyle = this.setStyle();
    //圖表類型判斷,爲後期作擴展
    switch (options.type) {
        case "bar":
            //初始化頁面圖表全部部分
            this.resetAllCharts(this.$(options.el));
            //初始化X軸部分
            this.resetChartsX(options.data.X, setStyle);
            //初始化Y軸部分
            this.resetChartsY(options.data.Y, setStyle);
            //初始化圖注部分
            this.resetChartsLegend(options.data, setStyle);
            break;
    }
}

而後,完成初始化頁面圖表的結構,前面頁面結構和css寫好只有,頁面應該只保留一個容器元素,以下所示:

<div id="weekCost"></div>

接下來,咱們就往該元素添加結構,以下所示:

/**
 * 初始化圖表結構
 */
ewCharts.prototype.resetAllCharts = function (el) {
    el.innerHTML = "<ew-charts-body>" +
        "<ew-charts-legend></ew-charts-legend>" +
        "<ew-charts-x></ew-charts-x>" +
        "<ew-charts-y></ew-charts-y>" +
        "<ew-charts-zone></ew-charts-zone>" +
        "</ew-charts-body>";
    //爲容器元素添加一個類名
    el.classList.add('ew-charts');
    return el;
}

繼續初始化X軸,以下所示:

/**
 * 設置X軸
 * x軸的數據
 * 設置樣式的方法
 */
ewCharts.prototype.resetChartsX = function (dataX, setStyle) {
    let chartsX = this.$('ew-charts-x'), chartsXHTML = '';
    let dataXLen = dataX.length;
    // 添加x軸的文本元素
    for (let i = 0; i < dataXLen; i++) {
        chartsXHTML += "<div class=x-" + (i + 1) + " style='letter-spacing:2px;'>" + dataX[i] + "</div>";
    }
    chartsX.innerHTML = chartsXHTML;
    let chartsXContent = this.$('ew-charts-x > div', false), chartsXContentWidthArr = [];
    // 獲取元素的寬度數組,並找到最大寬度,從而設置每一個元素的寬度爲最大寬度
    for (let j = 0; j < dataXLen; j++) {
        chartsXContentWidthArr.push(chartsXContent[j].offsetWidth);
    }
    //最大寬度與單位寬度以及單位寬度的一半
    let maxWidth = Math.max.apply(null, chartsXContentWidthArr), unitWidth = parseInt(100 / dataXLen), half = unitWidth / 2;
    for (let k = 0; k < dataXLen; k++) {
        //循環分別設置x軸上的座標數據的元素寬度與left偏移量
        setStyle('ew-charts-x > div.x-' + (k + 1) + '{width:' + maxWidth + 'px;' + 'left:calc(' + (unitWidth * (k + 1) - half) + '% - ' + half + 'px)}', k);
    }
}

x軸部分已經完成,繼續完成y軸部分:

/**
 * 設置Y軸
 */
ewCharts.prototype.resetChartsY = function (dataY, setStyle) {
    let newDataValue = [], chartsY = this.$('ew-charts-y'), chartsYHTML = '';
    let keyNameArr = this.options.data.keyName;
    let keyValue = Array.isArray(keyNameArr) && keyNameArr.length === 2 ? keyNameArr[1] : 'value';
    for (let i = 0, len = dataY.length; i < len; i++) {
        // 將多個value值數組合併成一個數組
        newDataValue = newDataValue.concat(dataY[i][keyValue]);
    }
    // 求value數組的最大值
    let maxValue = Math.max.apply(null, newDataValue);
    if (/\./.test(String(maxValue))) {
        // 若是最大值有小數,則向上取整
        maxValue = Math.ceil(maxValue);
    }
    // 定義分段數與當前Y軸的最大值
    let subSections = null, currentMaxValue = null;
    // 按照每段爲1,5,50,500,5000,50000基準值來分段的
    // 當前做爲基準值判斷的依據數組
    let judgeMaxArr = [1000000, 100000, 10000, 1000, 100, 10];
    let currentJudgeValue = null;
    for (let l = 0, length = judgeMaxArr.length; l < length; l++) {
        // 若是知足條件就跳出循環
        if (maxValue >= judgeMaxArr[l]) {
            currentJudgeValue = judgeMaxArr[l];
            break;
        }
    }
    // 若是currentValue的值爲null,則默認分段值設爲1
    if (!currentJudgeValue) currentJudgeValue = 1;
    // 計算分段數
    subSections = currentJudgeValue > 1 ? Math.ceil(maxValue / (currentJudgeValue / 2)) : Math.ceil(maxValue / currentJudgeValue);
    // 計算當前Y軸最大值
    currentMaxValue = currentJudgeValue > 1 ? subSections * (currentJudgeValue / 2) : subSections * currentJudgeValue;
    // 根據分段數來生成Y軸元素
    for (let j = 0; j < subSections; j++) {
        chartsYHTML += "<div class='y-" + (j + 1) + "'>" + (currentMaxValue / subSections) * (j + 1) + "</div>";
    }
    chartsY.innerHTML = chartsYHTML;
    // 設置CSS規則
    for (let k = 0; k < subSections; k++) {
        setStyle('ew-charts-y > div.y-' + (k + 1) + '{ bottom:calc(' + parseInt((100 / subSections) * (k + 1)) + '% - 16px);}');
    }
    // 設置區域
    this.resetChartsZone(subSections, keyValue, currentMaxValue, setStyle);
}

y軸部分也已經完成,接下來是完成柱狀圖部分,也就是區域部分,以下:

/**
 * 設置區域
 */
ewCharts.prototype.resetChartsZone = function (subSections, keyValue, currentMaxValue, setStyle) {
    // 區域總體背景
    setStyle("ew-charts-zone { background:repeating-linear-gradient(180deg,#535456 0%,#724109 " + 100 / subSections + "%,#334455 calc(" + 100 / subSections + "% + 1px),#e0e1e5 " + 100 / subSections * 2 + "%)}", subSections + 1);
    let zoneLen = this.options.data.X.length;
    let chartsZone = this.$('ew-charts-zone'), chartsZoneHTML = '';
    // 由於設置了margin-left與margin-right各1%,因此要減去2
    let series_unit = parseInt(100 / zoneLen) - 2;
    // 設置剩餘空間
    let freeSpace = 0;
    // 系列數
    let series_count = this.options.data.Y.length;
    // 每一條數據的寬度
    let series_width = 0;
    // 每一條數據的left值
    let series_left = null;
    // 根據系列數來調整樣式
    if (series_count < 3) {
        series_width = 28;
        freeSpace = (100 - (series_count * 30)) / 2;
        series_left = 30;
    } else if (series_count >= 3 && series_count < 6) {
        series_width = 18;
        freeSpace = (100 - (series_count * 20)) / 2;
        series_left = 20;
    } else {
        series_width = 100 / (series_count - 1);
        freeSpace = 100 / series_count;
        series_left = 0;
    }
    let seriesHTML = '';
    for (let j = 0; j < series_count; j++) {
        // 邊框顏色高亮
        let borderColor = this.lightColor(this.options.color[j]);
        let left = null;
        if (series_left > 0) {
            left = series_left * j + freeSpace;
        } else {
            left = freeSpace * j;
        }
        // 設置初始樣式
        setStyle('ew-charts-zone > div bar.bar-' + (j + 1) + "{width:" + series_width + '%;background-color:' + this.options.color[j] + ';border-color:' + borderColor + ';left:' + left + '%;box-shadow:0 0 5px ' + this.options.color[j] + ';}', j);
        // 設置懸浮樣式
        setStyle('ew-charts-zone > div bar.bar-' + (j + 1) + ':hover{box-shadow:0 0 15px ' + this.options.color[j] + ';}');
        seriesHTML += '<bar class="bar-' + (j + 1) + '"></bar>'
    }
    setStyle("ew-charts-zone > div[class*='zone-']{ width:" + series_unit + "%;margin-left:1%;margin-right:1%;}");
    for (let i = 0; i < zoneLen; i++) {
        chartsZoneHTML += "<div class='zone-" + (i + 1) + "'>" + seriesHTML + "</div>";
    }
    chartsZone.innerHTML = chartsZoneHTML;
    let dataY = this.options.data.Y;
    // 延遲設置高度
    setTimeout(() => {
        for (let k = 0; k < zoneLen; k++) {
            for (let l = 0; l < series_count; l++) {
                // 獲取bar元素
                const bar = chartsZone.children[k].children[l];
                // 設置class類名,方便設置樣式規則
                bar.classList.add('dataId-' + (k + 1) + '-' + (l + 1));
                // 設置值,方便後續的懸浮操做顯示值
                bar.setAttribute('data-value', dataY[l][keyValue][k]);
                // 設置高度
                setStyle('ew-charts-zone > div bar.dataId-' + (k + 1) + '-' + (l + 1) + '{height:' + (dataY[l][keyValue][k]) / currentMaxValue * 100 + '%;}', l);
            }
        }
        // 綁定懸浮事件
        let bar = this.$('ew-charts-zone div bar', false);
        [].slice.call(bar).forEach((item) => {
            item.onmouseenter = function () {
                let value = this.getAttribute('data-value');
                this.innerHTML = "<span class='animation'>" + value + '</span>';
            }
            item.onmouseleave = function () {
                this.innerHTML = '';
            }
        })
    }, 0);

}

最後就是完成圖注部分了,以下所示:

/**
 * 設置圖注
 */
ewCharts.prototype.resetChartsLegend = function (dataLegend, setStyle) {
    let legendHTML = "";
    //圖注數據的屬性名
    let keyName = Array.isArray(dataLegend.keyName) && dataLegend.keyName.length === 2 ? dataLegend.keyName[0] : 'label';
    for (let i = 0, len = dataLegend.Y.length; i < len; i++) {
        let borderColor = this.lightColor(this.options.color[i]);
        setStyle("ew-charts-legend > i.leg-" + (i + 1) + "{ background:" + this.options.color[i] + ";border-color:" + borderColor + ";}", i);
        legendHTML += "<i class='leg-" + (i + 1) + "'></i><span>" + dataLegend.Y[i][keyName] + "</span>";
    }
    this.$('ew-charts-legend').innerHTML = legendHTML;
}

接下來,調用這個封裝好的函數,以下所示:

/**
* 功能:調用統計圖表功能
**/
/************************************************/
/* DOM加載完畢後執行(多媒體資源還沒有開始加載) */
/************************************************/
document.onreadystatechange = function(){
    if(document.readyState == "interactive"){
        let ewChart = new ewCharts({
            el:"#weekCost",
            color:["#07bc85","dd2345","#346578","#ff8654","#998213"],
            data:{
                X:['一月', '二月', '三月', '四月', '五月', '六月', '七月'],
                Y:[
                    {
                        name: '直接訪問',
                        data: [320, 332, 301, 334, 390, 330, 320]
                    },
                    {
                        name: '郵件營銷',
                        data: [120, 132, 101, 134, 90, 230, 210]
                    },
                    {
                        name: '聯盟廣告',
                        data: [220, 182, 191, 234, 290, 330, 310]
                    },
                    {
                        name: '視頻廣告',
                        data: [150, 232, 201, 154, 190, 330, 410]
                    },
                    {
                        name: '搜索引擎',
                        data: [862, 1018, 964, 1026, 1679, 1600, 1570]
                    },
                ],
                keyName:['name','data']
            }
        });
        console.log(ewChart);
    }
}

嗯,一款柱狀圖表就大功告成了,因爲每一部分的功能我都作了註釋,因此不須要作詳解,若有問題歡迎聯繫我,若是發現bug,也歡迎提issue,項目地址爲:my-web-projects,若有幫助,望不吝嗇star

相關文章
相關標籤/搜索