D3: Animating bars “going down”
One confusing aspect of animating bars (and working with SVG in general) is that the coordinate system origin is at the top-left corner, which means that y
increases downwards.
One would expect that to animate a bar diminishing in size (“going down”) changing the height
attribute would be all that’s needed. However that’s not true. This is a visual explanation of why it’s necessary to animate both y
and height
attributes.
Another solution would be to surround the bar with a clipPath
element and then transition the bar down. This way, everything on the inside of the clipping area is allowed to show through but everything on the outside is masked out.
This other approach would only work for a simple bar chart though. For a more complex, animated stacked chart, animating both y
and height
attributes is the only solution.
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
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
<!DOCTYPE html>
<meta charset="utf-8">
<style>
html, body {
height: 100%;
margin: 0;
}
.chart-container {
box-sizing: border-box;
height: 100%;
padding: 1em;
}
.controls {
display: flex;
flex-direction: column;
position: absolute;
bottom: 1em;
left: 1em;
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
}
@media screen and (min-width: 310px) {
.controls {
display: block;
bottom: initial;
top: 1em;
}
}
.chart {
width: 100%;
height: 100%;
}
.chart path,
.chart line,
.chart rect {
shape-rendering: crispEdges;
}
.chart .bar {
fill: steelblue;
}
.chart .baseline {
fill: none;
stroke: black;
}
</style>
<body>
<form class="controls">
<label><button type="button" value="height">Animate height</button></label>
<label><button type="button" value="y">Animate y</button></label>
<label><button type="button" value="both">Animate y and height</button></label>
</form>
<div class="chart-container">
<svg class="chart"></svg>
</div>
<script src="https://d3js.org/d3.v5.min.js"></script>
<script type="text/javascript">
"use strict";
var container = { width: 1000, height: 500 },
margin = { top: 60, right: 0, bottom: container.height / 6, left: 0 },
width = container.width - margin.left - margin.right,
height = container.height - margin.top - margin.bottom,
animationDuration = 400,
barWidth = 40,
barX = (width - barWidth) / 2,
toggling = true;
var svg = d3.select(".chart")
.attr("viewBox", "0 0 " + container.width + " " + container.height);
var mainContainer = svg.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
var bar = mainContainer.append("rect")
.attr("class", "bar")
.attr("x", barX)
.attr("y", 0)
.attr("width", barWidth)
.attr("height", height);
mainContainer.append("line")
.attr("class", "baseline")
.attr("x1", 0)
.attr("x2", width)
.attr("y1", height)
.attr("y2", height);
var buttons = d3.selectAll(".controls button");
buttons.on("click", toggleBar);
function toggleBar() {
if (toggling) {
hideBar(this.value);
buttons.attr("disabled", "disabled");
d3.select(this).attr("disabled", null);
} else {
showBar(this.value);
buttons.attr("disabled", null);
}
toggling = !toggling;
}
function hideBar(togglingMode) {
var transition = bar.transition().duration(animationDuration);
switch (togglingMode) {
case "height":
transition.attr("height", 0);
break;
case "y":
transition.attr("y", height);
break;
case "both":
transition.attr("y", height).attr("height", 0);
break;
}
}
function showBar(togglingMode) {
var transition = bar.transition().duration(animationDuration);
switch (togglingMode) {
case "height":
transition.attr("height", height);
break;
case "y":
transition.attr("y", 0);
break;
case "both":
transition.attr("y", 0).attr("height", height);
break;
}
}
</script>
</body>