D3实现折线绘制动画的三种方法

引言

想要用d3实现常见的折线绘制动画,找了找没有现成的库可以用。于是,自己结合svg的一些资料和d3的transition想了三种实现方法。总体是两类思路一类是是遮盖折线如用rect遮盖,通过clip-path修改可视区域遮盖,另外一类是通过修改折线的样式实现,如用stroke-dasharray和stroke-dashoffset。完整的demo代码在本文最后的gitHub链接中给出。

效果图如下:

一、用rect遮蔽

最朴素的思路是利用svg 后一个元素显示位于前一个元素之上的性质,用一个和背景色相同的矩形先遮盖住绘制好的折线,用transition 慢慢移动矩形到坐标轴外。实现视觉上的折线绘制动画
绘制过程:

  1. 先完全遮住(黑色矩形代表程序中的rect元素)
  2. 移动遮盖矩形

实现代码:

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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
let num = 20
let dataset = []
let w = 500
let h = 200
let padding = 30
//随机生成初始值
for (let i = 0; i < num; i++) {
let tevalue = Math.random() * 1000
let temp = {
id: i,
value: tevalue
}
dataset.push(temp)
}
var key = function (d) {
return d.id;
};

//x轴就是每个点等宽
let xScale = d3.scaleBand()
.domain(d3.range(num))
.range([padding, w - padding])
.paddingInner(0.05)

//y将实际值转化到整个画布上
let yScale = d3.scaleLinear()
.domain([0, d3.max(dataset, d => d.value)])
.rangeRound([h - padding, padding])

let svg = d3.select('body')
.append('svg')
.attr('width', w)
.attr('height', h);

let polyLine = svg.append("path")
//在path后面创建遮蔽用的与背景同色的白色矩形
let rect = svg.append('rect')
.attr("class", "shield_rect")
.attr("x", padding)
.attr("y", 0)
.attr("width", w - padding * 2)
.attr("height", h - padding)
.style("fill", "white")

let line = d3.line()
.x((d, i) => { return (xScale(i) + xScale.bandwidth() / 2) })
.y(d => yScale(d.value) - 1);

//axis
polyLine
.datum(dataset)
.attr("class", "Polyline")
.attr("stroke", "red")
.attr("fill", "none")
.attr("d", line);

//移动矩形把绘制的line暴露出来
rect.transition()
.duration(4000)
.attr("x", w - padding)
.remove()
let xAxis = d3.axisBottom(xScale)
.tickSize(5)

svg.append("g")
.attr("id", "x-axis")
.attr("transform", "translate(0, " + (h - padding) + ")")
.call(xAxis)

let yAxis = d3.axisLeft(yScale)
.tickSize(5)

svg.append("g")
.attr("id", "y-axis")
.attr("transform", "translate(" + padding + ",0)")
.call(yAxis);

二、用clip-path调整可视区域

第二个思路和第一个思路类似,都是用东西去遮盖折线,然后让它慢慢显示出来。
使用clip-path通过改变可视区域的大小来实现绘制动画的显示。
Clip-path简单介绍:
clip-path是使用裁剪的方式创建元素的可显示区域。区域内显示,区域外隐藏。具体的用法和描述可以看clip-path - CSS(层叠样式表) | MDN (mozilla.org)

绘制过程:

实现代码

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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
//clip-path 实现折现绘制动画
let num = 20
let dataset = []
let w = 500
let h = 200
let padding = 30

//随机生成初始值
for (let i = 0; i < num; i++) {
let tevalue = Math.random() * 1000
let temp = {
id: i,
value: tevalue
}
dataset.push(temp)
}
var key = function (d) {
return d.id;
};

//x轴就是每个点等宽
let xScale = d3.scaleBand()
.domain(d3.range(num))
.range([padding, w - padding])
.paddingInner(0.05)

//y将实际值转化到整个画布上
let yScale = d3.scaleLinear()
.domain([0, d3.max(dataset, d => d.value)])
.rangeRound([h - padding, padding])

let svg = d3.select('body')
.append('svg')
.attr('width', w)
.attr('height', h);

//操作dom创建clipPath
svg.append("clipPath")
.attr("id", "myClip")
.append("rect")
.attr("x", padding)
.attr("y", 0)
.attr("width", w - padding * 2)
.attr("height", h - padding)

let line = d3.line()
.x((d, i) => { return (xScale(i) + xScale.bandwidth() / 2) })
.y(d => yScale(d.value) - 1);


let polyLine = svg.append("path")
.datum(dataset)
.attr("class", "Polyline")
.attr("clip-path", "url(#myClip)")
.attr("stroke", "red")
.attr("fill", "none")
.attr("d", line);

//放大可视区域到整个坐标轴右上方全部
let rec = svg.select("#myClip rect")
rec
.attr("x", padding)
.attr("y", padding)
.attr("width", 0)
.attr("height", h - padding * 2)
.transition()
.duration(4000)
.ease(d3.easeLinear)
.attr("width", w - padding * 2)
.attr("height", h - padding * 2)

let xAxis = d3.axisBottom(xScale)
.tickSize(5)

svg.append("g")
.attr("id", "x-axis")
.attr("transform", "translate(0, " + (h - padding) + ")")
.call(xAxis)

let yAxis = d3.axisLeft(yScale)
.tickSize(5)

svg.append("g")
.attr("id", "y-axis")
.attr("transform", "translate(" + padding + ",0)")
.call(yAxis);

三、stroke-dasharray和stroke-dashoffset

第三个思路和前两个不同的是通过改变样式来实现效果。可以不断的去改变要绘制折线的样式,先是全白,然后前一小部分红色,逐渐扩大红色部分。这个过程相当于,你有一个透明的弯曲的水管,然后你往这个水管里面注入红色的水。管子的形状不会发生改变。但是,管子的样子因为水流入呈现了变化。在具体的svg中就是 path 的d 命令序列已经定了,也就是说绘制的形状不变,而它的样式随着 dashoffset在发生改变。

Stroke-dasharray和stroke-dashoffset简单介绍:

stroke-dasharray是svg中用来控制描边的点划线的图案范式。是一个外观属性,类似css属性。(stroke-dasharray - SVG | MDN (mozilla.org))一般是拿来做虚线样式的。下面是它的简单用法。
(这里的例子参考了(11条消息) SVG学习之stroke-dasharray 和 stroke-dashoffset 详解_huzhenv5的博客-CSDN博客

1
2
3
stroke-dasharray = '10'
stroke-dasharray = '10, 5'
stroke-dasharray = '20, 10, 5'

stroke-dasharray如果提供了奇数个数的值,则重复该值列表以产生偶数个数的值。奇数位置为实线长度,偶数位置的数表示空隙的长度。

  如:stroke-dasharray = ‘10’ 表示:虚线长10,间距10,然后重复 虚线长10,间距10

如果提供的是偶数个数的值就是直接重复,位置对应表示的含义和提供奇数个数的值相同。  

如:stroke-dasharray = ’10 20’ 表示:虚线长10,间距20,然后重复 虚线长10,间距20。
stroke-dashoffset是对stroke-dasharray中的虚线进行偏移,正数是向左偏,负数是向右偏。

绘制过程:

实现代码:

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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
let num = 20
let dataset = []
let w = 500
let h = 200
let padding = 30
//随机生成初始值
for (let i = 0; i < num; i++) {
let tevalue = Math.random() * 1000
let temp = {
id: i,
value: tevalue
}
dataset.push(temp)
}
var key = function (d) {
return d.id;
};

//x轴就是每个点等宽
let xScale = d3.scaleBand()
.domain(d3.range(num))
.range([padding, w - padding])
.paddingInner(0.05)

//y将实际值转化到整个画布上
let yScale = d3.scaleLinear()
.domain([0, d3.max(dataset, d => d.value)])
.rangeRound([h - padding, padding])

let svg = d3.select('body')
.append('svg')
.attr('width', w)
.attr('height', h);

let line = d3.line()
.x((d, i) => { return (xScale(i) + xScale.bandwidth() / 2) })
.y(d => yScale(d.value) - 1);
//先设置dasharray的样式为露出部分全为虚线,实线部分隐藏到屏幕左边
//接着设置 dashoffset移过去
let polyLine = svg.append("path")
.datum(dataset)
.attr("class", "Polyline")
.attr("stroke", "red")
.attr("fill", "none")
.attr("d", line)
.attr("stroke-dasharray", function () {
let totalLength = d3.select(this).node().getTotalLength();//返回路径的总长度
return totalLength;

})
.attr("stroke-dashoffset", function () {
return d3.select(this).node().getTotalLength();
})
.style("fill", "none")
.style("stroke", "red")
.style("stroke-width", "0.8")
.transition()
.duration(4000)
.ease(d3.easeLinear)
.attr("stroke-dashoffset", 50);

let xAxis = d3.axisBottom(xScale)

svg.append("g")
.attr("id", "x-axis")
.attr("transform", "translate(0, " + (h - padding) + ")")
.call(xAxis)

let yAxis = d3.axisLeft(yScale)
//fell so good
svg.append("g")
.attr("id", "y-axis")
.attr("transform", "translate(" + padding + ",0)")
.call(yAxis);

Demo的代码:

D3Learning/line_animation at main · betterMan001/D3Learning (github.com)

参考资料

  1. clip-path - CSS(层叠样式表) | MDN (mozilla.org)
  2. stroke-dasharray - SVG | MDN (mozilla.org)
  3. (11条消息) SVG学习之stroke-dasharray 和 stroke-dashoffset 详解_huzhenv5的博客-CSDN博客

本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!