今天我们来仿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:;
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:;
}
.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

d3.js 教程 模仿echarts折线图的更多相关文章

  1. d3.js 教程 模仿echarts柱状图

    由于最近工作不是很忙,隧由把之前的charts项目用d3.js重写的一下,其实d3.js文档很多,但是入门不是很难,可是想真的能做一个完成的,交互良好的图还是要下一番功夫的.今天在echarts找到了 ...

  2. d3.js 教程 模仿echarts legend功能

    上一节记录没有加上echarts的legend功能,这一小节补一下. 1. 数据 我们可以从echarts中看出,折线数据并不是我们传进入的原始数据(多数情况下我们也不会修改原始数据),而是原始数组的 ...

  3. 【D3.js】Focus + Context 折线图

    利用D3.js库实现Focus+Context的折线图.读取data.tsv文件数据 index.html <!DOCTYPE html> <meta charset="u ...

  4. vue使用axios读取本地json文件来显示echarts折线图

    编辑器:HBuilderx axios文档:http://www.axios-js.com/zh-cn/docs/ echarts实例:https://echarts.apache.org/examp ...

  5. 实现Echarts折线图的虚实转换

    需求:医院的体温单,在统计体温时,对于正常情况下统计的体温数据,需要显示实线:对于进行物理降温后统计的体温数据,需要显示虚线. 现有的体温单是运用 Echarts 折线图,统一用实线显示.因此在这基础 ...

  6. echarts折线图动态改变数据时的一个bug

    echarts折线图中当增加dataZoom,修改start大于0的时候,会出现折线混乱,变成竖直的线,绘制有问题. 解决方法,在dataZoom中增加filterMode: 'empty' http ...

  7. ECharts折线图堆叠设置为不堆叠的方法

    下图是ECharts折线图堆叠的官方源码,设置折线图不堆叠只需要将每一个stack的值设置为不一样的名称或者将stack属性删除即可. option = { title: { text: '折线图堆叠 ...

  8. echarts折线图,数据切换时(最近七天)绘图不合理现象

    echarts折线图,当进行数据切换时存在绘制不合理的问题,数据没错,但绘制不对. 两个0之间的连线应该是平滑直线,如图: 正确的显示: 解决: 在myCharts.setOption(option) ...

  9. echarts折线图--数据交互

    <!DOCTYPE html> <html> <head lang="en"> <meta charset="UTF-8&quo ...

随机推荐

  1. VueRouter爬坑第二篇-动态路由

    VueRouter系列的文章示例编写时,项目是使用vue-cli脚手架搭建. 项目搭建的步骤和项目目录专门写了一篇文章:点击这里进行传送 后续VueRouter系列的文章的示例编写均基于该项目环境. ...

  2. htm&css 颜色的浮动

    目标样式 目标分析: 由图可知,整个色板可以分为三块 可以理解为一个大的盒子包含了三个小的盒子,定义如下 LEFT的定义方式 TOP的定义方式 class为bottom的块最为复杂,但分析方法也是异曲 ...

  3. 高性能消息队列(MQ)Kafka 简单由来介绍(1)

    Kafka是由Apache软件基金会开发的一个开源流处理平台,由Scala和Java编写.Kafka是一种高吞吐量的分布式发布订阅消息系统,它可以处理消费者在网站中的所有动作流数据. 这种动作(网页浏 ...

  4. 关于Go defer的详细使用

    先抛砖引玉defer的延迟调用:defer特性: . 关键字 defer 用于注册延迟调用. . 这些调用直到 return 前才被执.因此,可以用来做资源清理. . 多个defer语句,按先进后出的 ...

  5. 【Android - 控件】之V - DrawerLayout的使用

    DrawerLayout是Android V4包中的一个布局控件,用来实现一个抽屉样式的布局. DrawerLayout通过设置子视图的layout_gravity来决定子视图停靠在屏幕的哪个边缘外侧 ...

  6. Xtrabackup 全备和还原以及增量备份和还原

    目录 MySQL环境介绍 全备和还原 准备备份目录 创建测试数据 全量备份 模拟删除数据 还原数据操作 第一步 备份备份文件 第二步 关闭数据库 第三步 移除数据库的data目录 第四步 恢复前准备 ...

  7. CCNA 之 十三 广域网概述

    广域网概述 为什么需要WAN ? 分区或分支机构的员工需要与总部通信并共享数据: 组织经常需要与其他组织远距离共享信息: 经常出差的员工需要访问公司网络信息: 什么事广域网链路? 用于连接LAN的.跨 ...

  8. Git基础用法

    从远程仓库拉取代码: git clone https://xxxx.xxx.xx 进入拉取到代码的路径下,(文件夹中含有 .git 隐藏文件夹) 查看当前是否关联到远git代码管理 git statu ...

  9. 如何彻底理解Java抽象类 为什么要用抽象类 什么情况下用抽象类

    如何彻底理解Java抽象类 为什么要用抽象类 什么情况下用抽象类 呐,到底什么是抽象类,有时明明一个普通类就可以解决了,为啥非得整个抽象类,装逼吗 我曾带着这样的疑惑,查了很多资料,看了很多源码,写了 ...

  10. Sql增加,删除,修改列

    1. 查看约束条件 - MySQL: SELECT * FROM information_schema.`TABLE_CONSTRAINTS` where table_name = 'book'; - ...