Animating Line Charts With D3.js

Written by maksymmostovyi | Published 2022/09/28
Tech Story Tags: data-visualization | front-end-development | javascript | d3js | frontend | charts | charting-tool | animating-line-charts

TLDRCharts are widely used in web applications and they are used to make complex data easy to understand and interact with. In this article, I would like to show how to enhance basic chart implementation and make it look better and fancier. I've created a simple project with the basic structure and dummy data set. It does not look too simple at first sight and it takes almost 100 lines of code to achieve that representation. So, I wrote comments on the main parts of the code to make it more clear.via the TL;DR App

Charts are widely used in web applications and they are used to make complex data easy to understand and interact with. In this article, I would like to show how to enhance basic chart implementation and make it look better and fancier.

I picked a line chart for my showcase, which is one of the most popular. It is widely used in different fintech apps or other data analytical software. The purpose of that particular chart is to display trends and analyze how the data has changed over time. So, I would like to show how to make a basic line chart more dynamic.

I've created a simple project with the basic structure and dummy data set. Let's take a look at that:

Index

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Line chart animation</title>
    <link rel="stylesheet" href="styles.css">
    <script src="https://d3js.org/d3.v7.min.js"></script>
    <script type="module" src="main.js" defer></script>
</head>
<body>
    <div class="chart-wrapper">
        <div id="line-chart" class="chart"></div>
    </div>
</body>
</html>

Chart data

const lineData = [{
    values: [
        {date: "2020/01/01", total: 230},
        {date: "2020/02/01", total: 290},
        {date: "2020/03/01", total: 230},
        {date: "2020/04/01", total: 270},
        {date: "2020/05/01", total: 244},
        {date: "2020/06/01", total: 270},
        {date: "2020/07/01", total: 320},
        {date: "2020/08/01", total: 320},
        {date: "2020/09/01", total: 250},
        {date: "2020/10/01", total: 272},
    ]
}]

export default lineData;

Styles

.chart-wrapper {
    width: 100vw;
    height: 100vh;
    display: flex;
    justify-content: center;
    align-items: center;
}

All is set, time to create a static line chart with the area. It does not look too simple at first sight and it takes almost 100 lines of code to achieve that representation. So, I wrote comments on the main parts of the code to make it more clear.

import lineData from '../data/line-data.js'

// Selecting the element
const element = document.getElementById('line-chart');

// Setting dimensions
const margin = {top: 40, right: 30, bottom: 7, left: 50},
    width = 900 - margin.left - margin.right,
    height = 300 - margin.top - margin.bottom;

// Parsing timestamps
const parseTime = d3.timeParse('%Y/%m/%d');

const parsedData = lineData.map(item => (
    {
        values: item.values.map((val) => ({
            total: val.total,
            date: parseTime(val.date)
        }))
    }));

// Appending svg to a selected element
const svg = d3.select(element)
    .append('svg')
    .attr('width', width + margin.left + margin.right)
    .attr('height', 300 + margin.top + margin.bottom)
    .attr("viewBox", `0 40 ${width + 80} ${height}`)
    .append('g')
    .attr('transform', `translate(${margin.left}, ${margin.top})`);

// Setting X,Y scale ranges
const xScale = d3.scaleTime()
    .domain([
        d3.min(parsedData, (d) => d3.min(d.values, (v) => v.date)),
        d3.max(parsedData, (d) => d3.max(d.values, (v) => v.date))
    ])
    .range([0, width]);

const yScale = d3.scaleLinear()
    .domain([
        d3.min(parsedData, (d) => d3.min(d.values, (v) => v.total)),
        d3.max(parsedData, (d) => d3.max(d.values, (v) => v.total))
    ])
    .range([height, 0]);

const chartSvg = svg.selectAll('.line')
    .data(parsedData)
    .enter();

// Drawing line with inner gradient and area
// Adding functionality to make line and area curved
const line = d3.line()
    .x(function(d) {
        return xScale(d.date);
    })
    .y(function(d) {
        return yScale(d.total);
    })
    .curve(d3.curveCatmullRom.alpha(0.5));

// Defining the area, which appear after animation ends
const area = d3.area()
    .x(function(d) { return xScale(d.date); })
    .y0(height)
    .y1(function(d) { return yScale(d.total); })
    .curve(d3.curveCatmullRom.alpha(0.5));

// Defining the line path and adding some styles
const path = chartSvg.append('path')
    .attr('d', function(d) {
        return line(d.values)
    })
    .attr('stroke-width', '2')
    .style('fill', 'none')
    .attr('stroke', '#ff6f3c');

// Drawing animated area
chartSvg.append("path")
    .attr("d", function(d) {
        return area(d.values)
    })
    .style('fill', 'rgba(255,111,60,0.15)')

// Adding the x Axis
svg.append("g")
    .attr("transform", "translate(0," + height + ")")
    .call(d3.axisBottom(xScale));

// Adding the y Axis
svg.append("g")
    .call(d3.axisLeft(yScale));

Here is how the chart looks:

Here is the point when we can add animation. I decided to animate the area and line separately. I would like the area to appear first with smooth animation and then the line to be slowly drawn over the area. To achieve that, I used D3 methods, such as transition and duration. Besides that, I used the delay method to avoid line and area animation overlapping.

To make that animation we need to define the initial state of the element. So we need to define starting value of the area. I declared a separate var and called it zeroArea and set Y scale value to 0.

const zeroArea = d3.area()
    .x(function(d) { return xScale(d.date); })
    .y0(height)
    .y1(function() { return 0; })
    .curve(d3.curveCatmullRom.alpha(0.5));

After that, I updated the part where we draw an area by tweaking the zeroArea to the area, which is expected to be after the transition.

chartSvg.append("path")
    .attr("d", function(d) {
        return zeroArea(d.values)
    })
    .style('fill', 'rgba(255,111,60,0.15)')
    .transition()
    .duration(1500)
    .attr("d", function(d) {
        return area(d.values)
    })
    .style('fill', 'rgba(255,111,60,0.15)');

Let's get back to the line. The idea here is similar to the way we animate the area. But their transition depends on line length, and to be able to get it, we should do the following:

const length = path.node().getTotalLength(); // Get line length

Then, we should set a transition of that line by using its length

path.attr("stroke-dasharray", length + " " + length)
    .attr("stroke-dashoffset", length)
    .transition()
    .ease(d3.easeLinear)
    .attr("stroke-dashoffset", 0)
    .delay(1500)
    .duration(3000)

Here is the result of that improvements:

Wrapping up

I believe that the chart looks much better now and the animation can be easily adjusted with small changes to the initial and transition values to X or Y scales. Similar animation can be also applied to pie and bar charts and it requires just several lines of code to be added to the implementation of the static chart. If you would like to explore and investigate all things implemented altogether, you can find the source code on my GitHub.

Thank you for reading, I hope it was helpful!


Written by maksymmostovyi | Software engineer with expertise in JavaScript, Angular, and React. One of my key skills is Data Visualisation.
Published by HackerNoon on 2022/09/28