用JS寫的一個好看的折線圖
阿新 • • 發佈:2019-01-26
之前做移動端專案的時候,有要顯示圖表的需求, 但是由於設計師設計的太漂亮, 一般的第三方控制元件不加修改的話都不太滿足, 如果引進了的話, 要修改的東西太多了, 也不好修改,廢話不多說, 先上圖:
至於怎麼實現了,其實也蠻簡單的,canvas自己一個個畫的。
以下是原始碼:
/** * Created by freeson on 2017/10/24. */ export const lineChart = { circleDotOuterRadius: 8, circleDotInnerRadius: 6, dotX: [], dotY: [], config: null, selectedIndex: 0, screenWidth: 0, charMaxHeight: 0, onDestroyed: function () { this.dotX = []; this.dotY = []; this.config = null; }, renderLineChart: function () { var context = this.config.context; context.clearRect(0, 0, this.config.width, this.config.height); this.drawLine(context); this.drawPath(context); var linearGradient = context.createLinearGradient(0, this.config.height, 0, 0);//圖表的漸變顏色,從透明到設定值 linearGradient.addColorStop(0.0, "transparent"); linearGradient.addColorStop(0.5, 'rgba(53,130,226,0.1)'); linearGradient.addColorStop(1, 'rgba(53,130,226,0.3)'); context.fillStyle = linearGradient; context.fill(); this.drawCircleDot(context); this.drawText(context); }, //這個方法是畫圖表的路徑 drawPath(context) { context.beginPath(); for (let i = 0; i < 7; ++i) { if (i === 0) { context.moveTo(this.dotX[i], this.dotY[i]); } else { context.lineTo(this.dotX[i], this.dotY[i]); } } context.closePath(); }, //這個方法是畫最外面那條曲線 drawLine(context) { context.strokeStyle = "#ebebeb"; context.lineWidth = 1; for (let i = 1; i < 6; ++i) { if (i === 1) { context.moveTo(this.dotX[i] + 0.5, this.dotY[i] + 0.5); } else { context.lineTo(this.dotX[i] + 0.5, this.dotY[i] + 0.5); context.stroke(); } } }, //這個方法是畫一個月份的圓點 drawCircleDot(context) { let x = 0, y = 0; for (let i = 1; i < 6; ++i) { x = this.dotX[i]; y = this.dotY[i]; if (i === 1) { x = this.circleDotOuterRadius; } else if (i === 5) { x = x - this.circleDotOuterRadius; } context.fillStyle = "#ffffff"; context.beginPath(); context.arc(x, y, this.circleDotOuterRadius, 0, 2 * Math.PI, true); context.closePath(); context.fill(); if (this.config.selectedIndex + 1 == i) { context.fillStyle = "#1874e6"; } else { context.fillStyle = "#ebebeb"; } context.beginPath(); context.arc(x, y, this.circleDotInnerRadius, 0, 2 * Math.PI, true); context.closePath(); context.fill(); } }, //這個方法是畫金額,就是如果月份金額是屬於上升,金額畫在圓點上方, 如果是下降的畫, 畫在圓點下方 drawText(context) { if (!this.screenWidth) { let app = document.getElementById('app'); if (app) { this.screenWidth = app.clientWidth; } else { this.screenWidth = window.screen.width; } } let font = this.screenWidth * 24 / 750; context.font = 'bold ' + font + 'px sans-serif'; let data = this.config.data; let x = 0, y = 0; let beforeTextIsBottom = false; for (let i = 0; i < data.length; ++i) { x = this.dotX[i + 1]; y = this.dotY[i + 1]; let tempBefore = data[i - 1] * 100; let temp = data[i] * 100; if (i != 0 && temp < tempBefore) { y = y + Math.floor(font) + 8; beforeTextIsBottom = true; if (y > this.charMaxHeight) { y = y - 8 - Math.floor(font) - 8; // beforeTextIsBottom = false; } } else { if (i > 1 && temp == tempBefore) { if (beforeTextIsBottom) { y = y + Math.floor(font) + 8; beforeTextIsBottom = true; } else { beforeTextIsBottom = false; y = y - 8; } } else { beforeTextIsBottom = false; y = y - 8; } } let len = String(data[i]).length; if (i != 0) { if (i === data.length - 1) { x -= (font / 2) * len + font / 2; } else { if (len == 1) { x -= 3.5; } else { x -= 7 * (len - 1) / 2; } } } else { if (len == 1) { x += 3.5; } } if (this.config.selectedIndex == i) { context.fillStyle = "#1874e6"; } else { context.fillStyle = "#cccccc"; } context.fillText(String(data[i]), x, y); } }, //這個方法是構造資料 buildXYData(config) { let canvas = config.canvas; canvas.width = canvas.clientWidth; canvas.height = canvas.clientHeight; let context = canvas.getContext("2d"); let width = canvas.width, height = canvas.height; config.width = width; config.height = height; if (window.devicePixelRatio) { canvas.height = canvas.height * window.devicePixelRatio; canvas.width = canvas.width * window.devicePixelRatio; context.scale(window.devicePixelRatio, window.devicePixelRatio); } config.context = context; this.config = config; this.dotX[0] = 0; this.dotX[1] = 0; this.dotX[2] = width * 0.25; this.dotX[3] = width * 0.5; this.dotX[4] = width * 0.75; this.dotX[5] = width; this.dotX[6] = width; this.dotY = []; this.dotY.push(height);//起點 this.charMaxHeight = height; for (let i = 1; i < 6; ++i) {//基於底線(資料全為0)初始化資料 this.dotY.push(height - 8); } this.dotY.push(height);//結束點 let data = config.data; let max = 0, maxIndex = 0; let hasZero = false, zeroIndex = 0; for (let j = 0; j < data.length; ++j) { let temp = data[j] * 100; if (j != 0 && temp == 0) { hasZero = true; zeroIndex = j; } if (temp > max) { max = temp; maxIndex = j; } } if (max == 0) { return; } max /= 100; let maxPercentHeight = height * 0.84 - 8; if (hasZero) { let allZeroBefore = true; for (let k = 0; k < zeroIndex; ++k) { if (data[k] != 0) { allZeroBefore = false; break; } } if (!allZeroBefore) { maxPercentHeight = height * 0.68; } } else { for (let l = 1; l < data.length; ++l) { if (data[l] < data[l - 1]) { maxPercentHeight = height * 0.68; break; } } } let topDotY = height * 0.16; for (let n = 0; n < data.length; ++n) { if (n == maxIndex) { this.dotY[maxIndex + 1] = topDotY;//最高值的頂部,預留圓圈和文字(16%) } else { let percentHeight = ((data[n] / max) * maxPercentHeight).toFixed(2); this.dotY[n + 1] = parseInt((maxPercentHeight - percentHeight + topDotY).toFixed(2)); } } }, render: function (config) { this.buildXYData(config); this.renderLineChart(); }, //點選月份,更新選中狀態 updateSelected(selectedIndex) { this.config.selectedIndex = selectedIndex; this.renderLineChart(); } }
以下是xml佈局(用的是vue框架):
<div class="line-container"> <div class="line-chart-container"> <canvas id="line-chart" class="line-chart"></canvas> </div> <div class="bottom-month-container" v-if="list.length!=0"> <span @click="monthClick(0)" :class="{blue:selectedMonth==0}">{{list[0].bar}}</span> <div class="month-other-container"> <div><span @click="monthClick(1)" :class="{blue:selectedMonth==1}">{{list[1].bar}}</span> </div> <div><span @click="monthClick(2)" :class="{blue:selectedMonth==2}">{{list[2].bar}}</span> </div> <div><span @click="monthClick(3)" :class="{blue:selectedMonth==3}">{{list[3].bar}}</span> </div> <div><span @click="monthClick(4)" :class="{blue:selectedMonth==4}">{{list[4].bar}}</span> </div> </div> </div> </div>
以下是用到到css:
.line-container { height: pxToRem(300); position: relative; .line-chart-container { height: pxToRem(260); /*padding-bottom: pxToRem(30);*/ .line-chart { width: 100%; height: 100%; } } .bottom-month-container { font-size: pxToRem(28); color: $grey; position: relative; display: flex; .month-other-container { flex: 1; div { float: left; width: 25%; text-align: right; } } .blue { color: $dark-blue; } } }
以下在vue裡面呼叫:
drawLineChart() {
let canvas = document.getElementById("line-chart");
let data = [];
for (let i = 0; i < this.list.length; ++i) {
data.push(this.list[i].income);
}
lineChart.render({
canvas: canvas,
data: data,
selectedIndex: this.selectedMonth,//預設選中哪個月份
});
},
程式碼就是這麼簡單了, 試過很多中情況了,資料還能顯示正確,當然可能還有我未發現的bug,大家看著修改就好, 很簡單。
這個是用在移動端的, 在pc端大小可能要自己去調整。