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=" 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的朋友可以移步原文

原文地址 http://www.bettersmile.cn

原文地址:https://www.cnblogs.com/vadim-web/p/11463072.html