0%

使用D3.js 绘制折线图

在一个网站上绘制折线图使用了ant/g2,打包后的体积到了一兆多,这不行了,需要按需加载。但是它的支持不太友好,我尝试在官网上用它的方法来按需引入,不是缺这就是缺那,很不好用。

反正我这里只是画个折线图,其他的功能也用不上,那么我直接用D3自己手画不行么?

安装

可以查看官网选择直接使用<script>标签的形式直接链接进来:

1
<script src="https://d3js.org/d3.v5.min.js"></script>

也可以使用npm安装,(点击查看npm地址):

1
npm install d3

可以直接导入一个D3的命名空间:

1
import * as d3 from 'd3';

也可以导入一个模块:

1
import {scaleLinear} from 'd3-scacle';

我使用在angular项目中,自带ts,那么我希望d3也有ts声明,可以安装:

1
npm i --save @types/d3

开始画图

定义折线图的展示参数

我们需要定义折线图的高度、宽度,以及它的边距:

1
2
3
4
5
6
7
8
const width = 1000;
const height = 400;
const padding = {
top: 50,
right: 50,
bottom: 50,
left: 50,
};

在DOM中创建元素

为了方便,我们可以先在html中创建一个用id标记的dom节点来放置我们的折线图,当然,也可以以动态的方式在ts中创建,因为我们的焦点在使用D3,所以先简单点,手动创建dom:

1
<div id="container"></div>

然后我们在ts中加入生成画布的命令:

1
2
3
4
const svg = select('#container')
.append('svg')
.attr('width', width + 'px')
.attr('height', height + 'px');

目前我们在页面中是看不到啥的,如果审查页面,则可以看到svg元素已经创建了。

定义数据结构

我们这里的折线图的数据结构为:

1
2
3
4
interface ChartDataInterface {
date: string;
uv: number;
}

date代表日期,uv是个数据值,代表这天的访问量。

设置比例尺

比例尺是把一组输入域映射到输出域的函数。映射就是两个数据集之间元素相互对应的关系。好比地图上的距离比例尺,假设地图上1cm代表500米,那么5cm代表2500米。一个意思。

D3中有各种比例尺函数,有连续性的,有非连续性的。我们这里目前情况,对于y轴,因为它是一个数值,所以我们需要一个线性比例尺。对于x轴,是离散的点,需要一个序数比例尺。我们需要引入这两个比例尺:

1
import {scaleLinear, scaleBand} from 'd3-scale';

有了比例尺,我们需要设置比例尺的domainrange

  • domain 设置比例尺的域 是一个值数组。将第一个元素映射到第一个频段,第二个元素映射到第二个频段,依次类推。
  • range 设置比例尺的值范围。为指定的两个数字数组,默认范围是[0, 1]

为了构造y轴的值范围,我们需要从y轴的数据中获取最大的值:

1
2
3
import {max} from 'd3-array';
const maxData = max(this.chartData, (d: ChartDataInterface) => d.uv);

1
2
3
4
5
6
const xScale = scaleBand()
.domain(this.chartData.map(item => item.date))
.range([0, width - padding.left - padding.right]);
const yScale = scaleLinear()
.domain([0, maxData])
.range([height - padding.top - padding.bottom, 0]);

x轴的比例尺范围为从左到右,y轴的比例尺范围为从上到下,注意需要减去边距。

设置坐标轴

坐标轴我们需要使用axisBottomaxisLeft,即左轴和下轴,需要引入这两个符号:

1
import {axisBottom, axisLeft} from 'd3-axis';

然后直接用我们前面构造的比例尺来生成坐标轴:

1
2
const xAxis = axisBottom(xScale);
const yAxis = axisLeft(yScale);

接下来绘制坐标轴。

1
2
3
4
5
6
7
8
svg.append('g')
.attr('class', 'axis')
.attr('transform', 'translate(' + padding.left + ',' + (height - padding.bottom) + ')')
.call(xAxis);
svg.append('g')
.attr('class', 'axis')
.attr('transform', 'translate(' + padding.left + ',' + padding.top + ')')
.call(yAxis);

这样在页面中可以看到我们的坐标轴了:
d3 svg axis

绘制折线

首先我们需要一个折线的路径,需要line函数,需要引入:

1
import {line} from 'd3-shape';

line生成一个折线生成器,然后line.x设置x坐标访问器,line.y设置y坐标访问器。

1
2
3
const linePath = line<ChartDataInterface>()
.x((d: ChartDataInterface) => xScale(d.date))
.y((d: ChartDataInterface) => yScale(d.uv));

这里需要注意,line默认的的参数为[number, number][],我们这里需要提供一个类型给line,让它接受我们的数据结构,不然会ts报错很烦。

然后开始绘制折线:

1
2
3
4
5
6
7
8
svg.append('path')
.datum(this.chartData)
.attr('class', 'line-path')
.attr('transform', 'translate(' + padding.left + ',' + padding.top + ')')
.attr('d', linePath)
.attr('fill', 'none')
.attr('stroke-width', 3)
.attr('stroke', 'green');

看看绘制出来的线:
d3 svg line

嗯。。。。好像对,好像又不对,折线图的是绘制了出来了,但是为什么好像偏了???

自信点,不是好像,是确定偏了。😅

scaleBand比例尺会将范围划分为domain设置的n个带(n为domain数组中值的个数),然后一个单元内居中对齐。所以我们这个折线需要对应x轴做偏移。

为了辅助我们观察方便,我们需要添加一个辅助线:

1
2
3
4
svg.append('g')
.attr('id', 'grid')
.attr('transform', 'translate(' + (padding.left) + ',' + (height - padding.bottom) + ')')
.call(xAxis.ticks(this.chartData.length).tickSize(-height + padding.top + padding.bottom).tickFormat(() => null));

然后看我们的图:
d3 svg auxiliary

结合前面的scaleBand比例尺的规则,我们知道,比例尺把x轴划分为8个宽度(我们这里的数据是八组数据)。而一个宽度里面的刻度是居中的,所以我们需要让折线图便宜半个宽度单位即可。

但是又看了下图的位置,发现好像这个图是偏的:
d3 svg xoffset

看了下这个轴线的宽度是6.5px。所以可以知道,我们需要的偏移量是一半的宽度,还需要减掉这个轴的宽度:

1
const xOffset = xScale(this.chartData[1].date) - 6.5;

然后我们再修正绘制的地方:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
svg.append('path')
.datum(this.chartData)
.attr('class', 'line-path')
.attr('transform', 'translate(' + xOffset + ',' + padding.top + ')')
.attr('d', linePath)
.attr('fill', 'none')
.attr('stroke-width', 3)
.attr('stroke', 'green');

````
再给节点加上环:
``` ts
svg.append('g')
.selectAll('circle')
.data(this.chartData)
.enter()
.append('circle')
.attr('r', 5)
.attr('transform', (d: ChartDataInterface) => {
return 'translate(' + (xScale(d.date) + xOffset) + ',' + (yScale(d.uv) + padding.top) + ')';
})
attr('fill', 'green');
svg.append('g')
.selectAll('circle')
.data(this.chartData)
.enter()
.append('circle')

.attr('r', 2)
.attr('transform', (d: ChartDataInterface) => {
return 'translate(' + (xScale(d.date) + xOffset) + ',' + (yScale(d.uv) + padding.top) + ')';
})
.attr('fill', '#fff');

然后我们的折线图就完美了:
d3 svg ok


暂时到这里吧,基本的折线图绘制完毕,其他的美化和事件监听后续再完善。

动态获取y轴宽度

前面我们手动的量了y轴的宽度,这是治标不治本的方法。要想一劳永逸,我们需要动态获取y轴的宽度。

代码:

1
2
const yAxisWidth: number = (this.svg.select('.yAxis').node() as SVGSVGElement).getBBox().width;
const xOffset = xScale(this.chartData[1].date) / 2 + padding.left - yAxisWidth;

测试看看:

svg auto get y axis withd

(暂时先忽略x轴吧!😓)

这里的思想是:

  • 首先获得y轴的宽度,备用
  • 需要得到x轴的标尺第一个块的相对位置,然后取一半即为第一个点的x轴偏移量。
  • 得到的x轴偏移量+y轴的宽度+图的左边的padding=整个曲线的偏移量

然后在绘曲线的时候直接使用:

1
2
3
4
5
6
7
8
this.svg.append('path')
.datum(this.chartData)
.attr('class', 'line-path')
.attr('transform', 'translate(' + xOffset + ',' + padding.top + ')')
.attr('d', linePath)
.attr('fill', 'none')
.attr('stroke-width', 3)
.attr('stroke', lineColor[key]);
码字辛苦,打赏个咖啡☕️可好?💘