paint-brush
Data Visualization for Dummies: 3 Simple Charts with <70 Lines of Codeby@maksymmostovyi
353 reads
353 reads

Data Visualization for Dummies: 3 Simple Charts with <70 Lines of Code

by Maksym Mostovyi October 21st, 2021
Read on Terminal Reader
Read this story w/o Javascript
tldt arrow

Too Long; Didn't Read

The most commonly used charts - line, bar, and pie charts - are used in data analysis and representation applications. I pick these charts for the most common use in most of the applications made for data analysis or representation. I did not use any framework, just HTML, CSS, and JavaScript. I recommend inspecting charts in the DOM to understand how D3 renders the elements. It takes around 70 lines of code to implement it.

Company Mentioned

Mention Thumbnail
featured image - Data Visualization for Dummies: 3 Simple Charts with <70 Lines of Code
Maksym Mostovyi  HackerNoon profile picture

Here I would like to talk about converting large, complex data into an easy visual representation format. For that, I pick the most commonly used charts - line, bar, and pie.

Actually, there are plenty of charts, but you can find these three in most of the applications made for data analysis or representation. Next, I will explain how to build them by using the popular library D3.js.

I’ve created a small demo application. It is a simple dashboard with those charts. I did not use any framework, just HTML, CSS, and JavaScript.

Also, I added 3 separate files with data set for each of the charts. I think it is more than enough to begin drawing the charts. But first, let’s take a look at the HTML structure:

<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8"/>
    <title>D3 charts</title>
    <link rel="stylesheet" href="styles.css">
    <link href="https://fonts.googleapis.com/css2?family=Roboto&display=swap" rel="stylesheet">
    <script src="https://d3js.org/d3.v7.min.js"></script>
    <script type="module" src="charts/bar-chart.js" defer></script>
    <script type="module" src="charts/line-chart.js" defer></script>
    <script type="module" src="charts/pie-chart.js" defer></script>
</head>
<body>
<header>
    <span>D3 Dashboard</span>
</header>
<section>
    <div class="bar-line-charts">
        <div class="chart-widget">
            <div class="widget-header">
                <span>Line chart</span>
            </div>
            <div id="line-chart" class="chart"></div>
        </div>
        <div class="chart-widget">
            <div class="widget-header">
                <span>Bar chart</span>
            </div>
            <div id="bar-chart" class="chart"></div>
        </div>
    </div>
    <div class="pie-chart">
        <div class="chart-widget">
            <div class="widget-header">
                <span>Pie chart</span>
            </div>
            <div id="d3-chart" class="chart" style="padding: 90px"></div>
        </div>
    </div>
</section>
</body>
</html>

Let’s draw the first chart, it is going to be a pie, then I will continue with the line and bar chart.

Pie Chart

import data from '../data/pie-data.js'

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

// Setting dimensions
const margin = 10,
    width = 400,
    height = 400;

// Setting the radius of the pie
const radius = Math.min(width, height) / 2 - margin;

const svg = d3.select(element)
    .append("svg")
    .attr("width", width)
    .attr("height", height)
    .attr("style", "margin-top: -32px !important")
    .append("g")
    .attr("transform", "translate(" + width / 2 + "," + height / 2 + ")");

// Setting the color scale
const color = d3.scaleOrdinal()
    .domain(data)
    .range(["#43ab92", "#f75f00", "#512c62"]);

// Setting the position of each group on the pie
const pie = d3.pie()
    .value(function (d) {
        return d[1].value;
    });

const data_ready = pie(Object.entries(data));

// Building arcs
const arcGenerator = d3.arc()
    .innerRadius(0)
    .outerRadius(radius);

// Building the pie chart
svg.selectAll('slices')
    .data(data_ready)
    .enter()
    .append('path')
    .attr('d', arcGenerator)
    .attr('fill', function (d) {
        return (color(d.data[1].name))
    });

// Adding titles to pie slices
svg.selectAll('slices')
    .data(data_ready)
    .enter()
    .append('text')
    .text(function (d) {
        return d.data[1].name
    })
    .attr("transform", function (d) {
        return "translate(" + arcGenerator.centroid(d) + ")";
    })
    .style("text-anchor", "middle")
    .style("font-size", 20);

Line Chart

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;

// Adding helper functions to make the line look nice
// The <defs> element is used to store graphical objects that will be used at a later time.
const createGradient = function(select) {
    const gradient = select
        .select('defs')
        .append('linearGradient')
        .attr('id', 'gradient')
        .attr('x1', '0%')
        .attr('y1', '100%')
        .attr('x2', '0%')
        .attr('y2', '0%');

    gradient.append('stop')
        .attr('offset', '0%')
        .attr('style', 'stop-color:#da6e2a;stop-opacity:0.05');

    gradient.append('stop')
        .attr('offset', '100%')
        .attr('style', 'stop-color:#da6e2a;stop-opacity:.5');
}

const createGlowFilter = function(select) {
    const filter = select
        .select('defs')
        .append('filter')
        .attr('id', 'glow')

    filter.append('feGaussianBlur')
        .attr('stdDeviation', '4')
        .attr('result', 'coloredBlur');

    const femerge = filter
        .append('feMerge');

    femerge.append('feMergeNode')
        .attr('in', 'coloredBlur');

    femerge.append('feMergeNode')
        .attr('in', 'SourceGraphic');
}

// 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]);

// Appending <defs>
svg.append('defs');
svg.call(createGradient);
svg.call(createGlowFilter);

// Drawing line with inner gradient
// Adding functionality to make line 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));

// Drawing inner part of a line
svg.selectAll('.line')
    .data(parsedData)
    .enter()
    .append('path')
    .attr('d', function(d) {
        const lineValues = line(d.values).slice(1);
        const splitedValues = lineValues.split(',');

        return `M0,${height},${lineValues},l0,${height - splitedValues[splitedValues.length - 1]}`
    })
    .style('fill', 'url(#gradient)');

// Drawing a line
svg.selectAll('.line')
    .data(parsedData)
    .enter()
    .append('path')
    .attr('d', function(d) {
        return line(d.values)
    })
    .attr('stroke-width', '2')
    .style('fill', 'none')
    .style('filter', 'url(#glow)')
    .attr('stroke', '#ff6f3c');

// 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));

Bar Chart

import barData from '../data/bar-data.js'

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

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

// Setting X,Y scale ranges
const xScale = d3.scaleBand()
    .range([0, width])
    .padding(0.1);

const yScale = d3.scaleLinear()
    .range([height, 0]);

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

// Formatting the data
barData.forEach(function (d) {
    d.value = +d.value;
});

// Scaling the range of the data in the domains
xScale.domain(barData.map(function (d) {
    return d.name;
}));
yScale.domain([0, d3.max(barData, function (d) {
    return d.value;
})]);

// Appending the rectangles for the bar chart
svg.selectAll(".bar")
    .data(barData)
    .enter().append("rect")
    .attr("x", function (d) {
        return xScale(d.name);
    })
    .attr("width", xScale.bandwidth())
    .attr("y", function (d) {
        return yScale(d.value);
    })
    .attr("height", function (d) {
        return height - yScale(d.value);
    })
    .style("fill", "#ff6f3c");

// 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));

Wrapping up

As you can see, all these charts do not have difficult logic. It takes around 70 lines of code to implement it. That is how we can make complex data to be converted in easy visual representation format.

Try it yourself! The source code can be found on GitHub. I recommend inspecting charts in the DOM to understand how D3 renders the elements.