Animated Bar Chart
In a previous tutorial, we created a simple bar chart. Let’s build on top of that and add an animation to the bars, so their height’s will grow from 0 to their final height like below.
Let’s look at the logic behind the animation. What we need to do here is to transition the y
and theheight
attributes of the rect element simultaneously, starting from where the height is 0 and the y attribute corresponds to the Bar at the lowest position to the positions in the final state.
What we are transitioning here is from this rect element:
<rect x="80" y="199" width="50" height="1" fill="#fcd34d"></rect>
to this one:
<rect x="80" y="50" width="50" height="150" fill="#fcd34d"></rect>
Let’s see now how we can implement this in our Bar chart example.
BarChart.svelte
It is possible to do this animation by using d3 transition
and select
functions but Svelte ships classes for adding motion to your user interfaces. The Tween
class helps smoothly transition between a starting and ending value, which we will use here to animate our bar chart. see here for more info: see here for more info.
Let’s create a component for the <rect>
elements that it is easier to set up our Tween variables.
<!-- Rect.svelte -->
<script>
import { Tween } from 'svelte/motion';
import { cubicOut } from 'svelte/easing';
import { interpolate } from 'd3-interpolate';
// Props declaration
let { x, value, yScale, width, i, fill } = $props();
// Animation parameters
const tweenParams = {
duration: 250,
easing: cubicOut,
interpolate
};
// Variables to be animated
let tY = new Tween(0, {
...tweenParams,
delay: 300 + i * 50
});
let tFill = new Tween(fill, {
...tweenParams,
delay: 800 + i * 50
});
// Run the animation via Tweens
$effect(() => {
// animate the bar height
tY.target = value;
// animate the bar fill color
tFill.target = fill;
});
</script>
<rect
{x}
{width}
y={yScale(tY.current)}
height={yScale(0) - yScale(tY.current)}
fill={tFill.current}
stroke="none" />
Let’s explaing to code above. We componentized our svg rect
element and defined attributes as props so that they can be set from the a parent component.
What does tweened do is that when data is changed in the parent they will transition the old values to the new values e.g y={yScale(tY.current)}
and height={yScale(0) - yScale(tY.current)}
over time by using the parameters defined in the tweenParams. We are also using the index i
value of the points array so bars will be shown sequentially one after the other and with a longer delay in tweenParamsFill
we make sure that the fill color will animate only after all the bars are visible.
There is not a big change in our Barchart.svelte compoment than the simple bar chart example. We only added a setTimeout function to change the data and replaced the rect elements with the rect component.
<!-- Barchart.svelte -->
<script>
import { scaleLinear } from 'd3-scale';
import Rect from './Rect.svelte';
// State variables
let points = $state([
{ year: 1990, birthrate: 0 },
{ year: 1995, birthrate: 0 },
{ year: 2000, birthrate: 0 },
{ year: 2005, birthrate: 0 },
{ year: 2010, birthrate: 0 },
{ year: 2015, birthrate: 0 },
{ year: 2020, birthrate: 0 },
{ year: 2025, birthrate: 0 },
{ year: 2030, birthrate: 0 },
{ year: 2035, birthrate: 0 }
]);
let fill = $state('#fffbeb');
let width = $state(500);
const height = 380;
setTimeout(() => {
fill = '#fcd34d';
points = [
{ year: 1990, birthrate: 6.7 },
{ year: 1995, birthrate: 4.6 },
{ year: 2000, birthrate: 14.4 },
{ year: 2005, birthrate: 18 },
{ year: 2010, birthrate: 7 },
{ year: 2015, birthrate: 12.4 },
{ year: 2020, birthrate: 17 },
{ year: 2025, birthrate: 10.9 },
{ year: 2030, birthrate: 8 },
{ year: 2035, birthrate: 12.9 }
];
}, 100);
// 2. Dimensions, Margins & Scales
// Constants
const yTicks = [0, 5, 10, 15, 20];
const padding = { top: 20, right: 15, bottom: 50, left: 25 };
// Computed values
let xScale = $derived(
scaleLinear()
.domain([0, points.length])
.range([padding.left, width - padding.right])
);
let yScale = scaleLinear()
.domain([0, Math.max.apply(null, yTicks)])
.range([height - padding.bottom, padding.top]);
let innerWidth = $derived(width - (padding.left + padding.right));
let barWidth = $derived(innerWidth / points.length);
// 3. Functions needed to create the Data elements
// or Helper functions, e.g d3.line d3.arc when needed
function formatMobile(tick) {
return "'" + tick.toString().slice(-2);
}
// 4. Create the main data elements (usually by iteration, using svelte each blocks)
// We will do this in the html markup
</script>
<div class="chart" bind:clientWidth={width}>
<svg {width} {height}>
<!-- Bars -->
<g class="bars">
{#each points as point, i}
<Rect
{i}
{yScale}
value={point.birthrate}
x={xScale(i) + 2}
width={barWidth * 0.9}
{fill} />
{/each}
</g>
<!-- Y axis -->
<g class="axis y-axis">
{#each yTicks as tick}
<g class="tick tick-{tick}" transform="translate(0, {yScale(tick)})">
<line x2="100%" />
<text y="-4"
>{tick} {tick === 20 ? ' per 1,000 population' : ''}</text>
</g>
{/each}
</g>
<!-- X axis -->
<g class="axis x-axis">
{#each points as point, i}
<g
class="tick"
transform="translate({xScale(i)}, {height -
padding.bottom +
padding.top})">
<text x={barWidth / 2} y="-4">
{width > 380 ? point.year : formatMobile(point.year)}</text>
</g>
{/each}
</g>
</svg>
</div>
<style>
.x-axis .tick text {
text-anchor: middle;
color: white;
margin: 40px;
}
.tick {
font-family: Poppins, sans-serif;
font-size: 0.725em;
font-weight: 200;
color: white;
}
.tick text {
fill: white;
text-anchor: start;
color: white;
}
.tick line {
stroke: #fcd34d;
stroke-dasharray: 2;
opacity: 1;
}
.tick.tick-0 line {
display: inline-block;
stroke-dasharray: 0;
}
</style>
Thanks for reading!
Join our Discord to share your charts, ask questions, or collaborate with fellow developers!