提起數據可視化技術,都難免會讓人想到echarts,而最簡單的入門圖表就是柱狀圖了。它是基於canvas
來實現的,而我在想,若是不用canvas
,我是否能不用canvas
實現柱狀圖,通過個人探索,終於實現了一款柱狀圖表。css
讓咱們查看一個已經完成的在線示例,以下圖所示:html
首先,咱們須要肯定柱狀圖表有哪些部分,第一右上角頭部有legend
部分,第二有x
與y
軸部分,第三就是柱狀圖部分了。好了肯定了有哪些部分,咱們就能夠很好的實現了,好了,讓咱們進入正題吧。git
目前咱們完成的成品是已經封裝好的,而後頁面就只有一個容器元素。但咱們最開始確定不能這樣寫,咱們先寫一個寫死的結構以下所示: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>
接下來就須要根據頁面元素,一個一個的添加樣式了,這是一個慢工細活的過程,須要慢慢來。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; } }
首先,咱們須要定義一個函數,用於封裝。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
。