d3.js 教程 模仿echarts折线图
今天我们来仿echarts折线图,这个图在echarts是折线图堆叠,但是我用d3改造成了普通的折线图,只为了大家学习(其实在简单的写一个布局就可以)。废话不多说商行代码。
1 制作 Line 类
class Line { constructor() { this._width = 1100; this._height = 800; this._padding = 10; this._offset = 35; this._margins = {right: 50,bottom: 50,left: 70,top: 100}; this._scaleX = d3.scaleBand().range([0, this.quadrantWidth()]).paddingInner(1).align(0); this._scaleY = d3.scaleLinear().range([this.quadrantHeight(), 0]); this._color = d3.scaleOrdinal(d3.schemeCategory10); this._dataX = []; this._series = []; this._svg = null; this._body = null; this._tooltip = null; this._transLine = null; this._activeR = 5; this._ticks = 5; } render() { if(!this._tooltip) { this._tooltip = d3.select('body') .append('div') .style('left', '40px') .style('top', '30px') .attr('class', 'tooltip') .html(''); } if(!this._svg) { this._svg = d3.select('body') .append('svg') .attr('width', this._width) .attr('height', this._height) .style('background', '#f3f3f3') this.renderAxes(); this.renderClipPath(); } this.renderBody(); } renderAxes() { let axes = this._svg.append('g') .attr('class', 'axes'); this.renderXAxis(axes); this.renderYAxis(axes); } renderXAxis(axes) { let xAxis = d3.axisBottom().scale(this._scaleX).ticks(this._dataX.length); axes.append('g') .attr('class', 'x axis') .attr('transform', `translate(${this.xStart()}, ${this.yStart()})`) .call(xAxis) d3.selectAll('g.x .tick text') .data(this._dataX) .enter() } renderYAxis(axes) { let yAxis = d3.axisLeft().scale(this._scaleY).ticks(this._ticks); axes.append('g') .attr('class', 'y axis') .attr('transform', `translate(${this.xStart()}, ${this.yEnd()})`) .call(yAxis) d3.selectAll('.y .tick') .append('line') .attr('class', 'grid-line') .attr('x1', 0) .attr('y1', 0) .attr('x2', this.quadrantWidth()) .attr('y2', 0) } renderClipPath() { this._svg.append('defs') .append('clipPath') .attr('id', 'body-clip') .append('rect') .attr('x', 0 - this._activeR - 1) .attr('y', 0) .attr('width', this.quadrantWidth() + (this._activeR + 1) * 2) .attr('height', this.quadrantHeight()) } renderBody() { if(!this._body) { this._body = this._svg.append('g') .attr('class', 'body') .attr('transform', `translate(${this._margins.left},${this._margins.top})`) .attr('clip-path', 'url(#body-clip)') this.renderTransLine() } this.renderLines(); this.renderDots(); this.listenMousemove(); } renderTransLine() { this._transLine = this._body.append('line') .attr('class', 'trans-line') .attr('x1', 0) .attr('y1', 0) .attr('x2', 0) .attr('y2', this._scaleY(0)) .attr('stroke-opacity', 0) } renderLines() { let line = d3.line() .x((d,i) => this._scaleX(this._dataX[i])) .y(d => this._scaleY(d)) let lineElements = this._body .selectAll('path.line') .data(this._series); let lineEnter = lineElements .enter() .append('path') .attr('class', 'line') .attr('d', d => line(d.data.map(v => 0))) .attr('stroke', (d,i) => this._color(i)) let lineUpdate = lineEnter .merge(lineElements) .transition() .duration(100) .ease(d3.easeCubicOut) .attr('d', d => line(d.data)) let lineExit = lineElements .exit() .transition() .attr('d', d => line(d.data)) .remove(); } renderDots() { this._series.forEach((d,i) => { let dotElements = this._body .selectAll('circle._' + i) .data(d.data); let dotEnter = dotElements .enter() .append('circle') .attr('class', (v, index) => 'dot _' + i + ' index_' + index) .attr('cx', (d,i) => this._scaleX(this._dataX[i])) .attr('cy', d => this._scaleY(d)) .attr('r', 1e-6) .attr('stroke', (d,i) => this._color(i)) let dotUpdate = dotEnter .merge(dotElements) .transition() .duration(100) .ease(d3.easeCubicOut) .attr('cx', (d,i) => this._scaleX(this._dataX[i])) .attr('cy', d => this._scaleY(d)) .attr('r', 2) let dotExit = dotElements .exit() .transition() .attr('r', 0) .remove(); }) this._dataX.forEach((d,i) => { d3.selectAll('circle._' + i) .attr('stroke', this._color(i)) }) } listenMousemove() { this._svg.on('mousemove', () => { let px = d3.event.offsetX; let py = d3.event.offsetY; if(px < this.xEnd() && px > this.xStart() && py < this.yStart() && py > this.yEnd()) { this.renderTransLineAndTooltip(px, py, px - this.xStart()); } else { this.hideTransLineAndTooltip(); } }) } renderTransLineAndTooltip(x, y, bodyX) { //鼠标悬浮的index let cutIndex = Math.floor((bodyX + this.everyWidth() / 2) / this.everyWidth()); //提示线位置 this._transLine.transition().duration(50).ease(d3.easeLinear).attr('x1', cutIndex * this.everyWidth()).attr('x2', cutIndex * this.everyWidth()).attr('stroke-opacity', 1); // dot圆圈动画 d3.selectAll('circle.dot').transition().duration(100).ease(d3.easeCubicOut).attr('r', 2) d3.selectAll('circle.index_' + cutIndex).transition().duration(100).ease(d3.easeBounceOut).attr('r', this._activeR) //提示框位置和内容 if(x > this.quadrantWidth() - this._tooltip.style('width').slice(0,-2) - this._padding * 2) { x = x - this._tooltip.style('width').slice(0,-2) - this._padding * 2 - this._offset * 2; } if(y > this.quadrantHeight() - this._tooltip.style('height').slice(0,-2) - this._padding * 2) { y = y - this._tooltip.style('height').slice(0,-2) - this._padding * 2 - this._offset * 2; } let str = `<div style="text-align: center">${this._dataX[cutIndex]}</div>`; this._series.forEach((d, i) => { str = str + `<div style="width: 15px;height: 15px;vertical-align: middle;margin-right: 5px;border-radius: 50%;display: inline-block;background: ${this._color(i)};"></div>${d.name}<span style="display: inline-block;margin-left: 20px">${d['data'][cutIndex]}</span><br/>` }) this._tooltip.html(str).transition().duration(100).ease(d3.easeLinear).style('display', 'inline-block').style('opacity', .6).style('left', `${x + this._offset + this._padding}px`).style('top', `${y + this._offset + this._padding}px`); } hideTransLineAndTooltip() { this._transLine.transition().duration(50).ease(d3.easeLinear).attr('stroke-opacity', 0); d3.selectAll('circle.dot').transition().duration(100).ease(d3.easeCubicOut).attr('r', 2); this._tooltip.transition().duration(100).style('opacity', 0).on('end', function () {d3.select(this).style('display', 'none')}); } everyWidth() { return this.quadrantWidth() / (this._dataX.length - 1); } quadrantWidth() { return this._width - this._margins.left - this._margins.right; } quadrantHeight() { return this._height - this._margins.top - this._margins.bottom; } xStart() { return this._margins.left; } xEnd() { return this._width - this._margins.right; } yStart() { return this._height - this._margins.bottom; } yEnd() { return this._margins.top; } scaleX(a) { this._scaleX = this._scaleX.domain(a); } scaleY(a) { this._scaleY = this._scaleY.domain(a) } selectMaxYNumber(arr) { let temp = []; arr.forEach(item => temp.push(...item.data)); let max = d3.max(temp); let base = Math.pow(10, Math.floor(max / 4).toString().length - 1); //获取Y轴最大值 return Math.floor(max / 4 / base) * 5 * base; } dataX(data) { if(!arguments.length) return this._dataX; this._dataX = data; this.scaleX(this._dataX); return this; } series(series) { if(!arguments.length) return this._series; this._series = series; let maxY = this.selectMaxYNumber(this._series); this.scaleY([0, maxY]) return this; } }
2 css 文件
.domain { stroke-width: 2; fill: none; stroke: #888; shape-rendering: crispEdges; } .tick text { font-size: 14px; } .grid-line { fill: none; stroke: #888; opacity: .4; shape-rendering: crispEdges; } .trans-line { fill: none; stroke: #666; opacity: .4; } .line { fill: none; stroke-width: 2; } .dot { fill: #fff; } .tooltip{ font-size: 15px; width: auto; padding: 10px; height: auto; position: absolute; background-color: #000000; opacity: .6; border-radius:5px; color: #ffffff; display: none; }
3 HTML 文件
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>$Title$</title> <link rel="stylesheet" type="text/css" href="css/base.css"/> <script type="text/javascript" src="js/d3.v4.js"></script> <script type="text/javascript" src="js/line.js"></script> </head> <body> <script> var dataX = ['周一', '周二', '周三', '周四', '周五', '周六', '周日']; var series = [{name: '邮件营销', data:[120, 132, 101, 134, 90, 230, 210]}, {name: '联盟广告', data:[340, 314, 292, 368, 380, 560, 520]}, {name: '视频广告', data:[490, 546, 493, 522, 570, 890, 930]}, {name: '直接访问', data:[810, 878, 794, 856, 960, 1220, 1250]}, {name: '搜索引擎', data:[1640, 1864, 1802, 1868, 2580, 2660, 2640]}] var line = new Line(); line .dataX(dataX) .series(series) .render() setInterval(() => { series = series.map((d,i) => { return { name: d.name, data: new Array(7).fill(1).map((dd, ii) => { return Math.floor(Math.random() * 200) + i * 200 }) } }) console.log(series); line .dataX(dataX) .series(series) .render() }, 4000) </script> </body> </html>
想预览和下载demo的朋友可以移步原文