D3.js Bar Chart

I’ve been playing A LOT with D3.js now and you always feel like it’s so hard, and you are right, it’s kinda tricky, but once you put everything in place, it starts making sense.

I will try to walk you through creating a bar chart with D3 and will try to explain every line in it, and basically how to convert it to an Angular Component.

First steps for D3.js Bar Chart

Let’s dump some code and then go through it step by step

<!DOCTYPE html>
<html lang="en">

  <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Bar Chart with D3.js</title>
    <script src="https://d3js.org/d3.v6.min.js"></script>
    <style>
      .bar {
        fill: steelblue;
      }

      .bar:hover {
        fill: orange;
      }

      .axis-label {
        font-size: 12px;
      }

    </style>
  </head>

  <body>
    <h1>Bar Chart Example</h1>
    <svg width="600" height="400"></svg>

    <script>
      // Data for the bar chart
      const data = [{
          name: 'A',
          value: 30
        },
        {
          name: 'B',
          value: 80
        },
        {
          name: 'C',
          value: 45
        },
        {
          name: 'D',
          value: 60
        },
        {
          name: 'E',
          value: 20
        },
        {
          name: 'F',
          value: 90
        },
        {
          name: 'G',
          value: 55
        }
      ];

      // Set the dimensions and margins of the graph
      const margin = {
          top: 20,
          right: 30,
          bottom: 40,
          left: 40
        },
        width = 600 - margin.left - margin.right,
        height = 400 - margin.top - margin.bottom;

      // Append the svg object to the body of the page
      const svg = d3.select("svg")
        .attr("width", width + margin.left + margin.right)
        .attr("height", height + margin.top + margin.bottom)
        .append("g")
        .attr("transform", `translate(${margin.left},${margin.top})`);

      // X axis
      const x = d3.scaleBand()
        .range([0, width])
        .domain(data.map(d => d.name))
        .padding(0.2);
      svg.append("g")
        .attr("transform", `translate(0,${height})`)
        .call(d3.axisBottom(x))
        .selectAll("text")
        .attr("transform", "translate(-10,0)rotate(-45)")
        .style("text-anchor", "end")
        .attr("class", "axis-label");

      // Add Y axis
      const y = d3.scaleLinear()
        .domain([0, 100])
        .range([height, 0]);
      svg.append("g")
        .call(d3.axisLeft(y))
        .selectAll("text")
        .attr("class", "axis-label");

      // Bars
      svg.selectAll("mybar")
        .data(data)
        .join("rect")
        .attr("x", d => x(d.name))
        .attr("y", d => y(d.value))
        .attr("width", x.bandwidth())
        .attr("height", d => height - y(d.value))
        .attr("class", "bar");

    </script>
  </body>

</html>

114 lines, not so much right?

HTML

First, let’s start with importing d3.js script

<script src="https://d3js.org/d3.v6.min.js"></script>

It’s using D3 version 6, but by the time of writing this article, D3 has version 7.

Then some CSS and styles, that basically will tell the bar fill color, which here will be blue and will turn to orange on mouse hover.

<svg width="600" height="400"></svg>

Here we are creating an SVG which will contain our bar chart

JavaScript

const data = [{
          name: 'A',
          value: 30
        },
        {
          name: 'B',
          value: 80
        },
....

Here we are defining the data, the value property will deal as the Y axis value and the name property will deal as the X axis tick or label.

      const margin = {
          top: 20,
          right: 30,
          bottom: 40,
          left: 40
        },
        width = 600 - margin.left - margin.right,
        height = 400 - margin.top - margin.bottom;

Here we are doing two things, the first thing is defining our margins and then the width and height of the chart.

Ok, why are we defining the margins? because basically we need to leave some space for the X axis and Y axis to be rendered, because the width and height are the are when the chart itself (the bars) should be drawn, without leaving a space for the axis, so that’s why here we are subtracting vertical margines from width and horizontal margins from the height.

      const svg = d3.select("svg")
        .attr("width", width + margin.left + margin.right)
        .attr("height", height + margin.top + margin.bottom)
        .append("g")
        .attr("transform", `translate(${margin.left},${margin.top})`);

Here, we are using d3 object to select the svg in our dome, it’s much like document.querySelector or document.getElementsByTagName or jQuery’s $(‘svg’). Then, we are adding our width and height, also appending a ‘g’.

What’s a ‘g’? a g is short for group, and we can think of it as a container that will hold some svg elements like rectangles and circles. Think of it as ‘div’ in HTML. It’s just a container.

We are translating/moving the ‘g’ element by the left and top margins, why? to leave a space for the X and Y axis to be drawn.

      // X axis
      const x = d3.scaleBand()
        .range([0, width])
        .domain(data.map(d => d.name))
        .padding(0.2);

Let’s get to the tricky part, defining the axis.

What are we doing here? to define an axis you have to know two things.

  • The width of the chart (or Height in case of Y Axis)
  • The domain of your data, and that’s basically your axis ticks, what are you drawing?

Why? because we will use this to place our bars on the Axis, so every value on the domain will map to the exact pixel in the chart. So, if:

We have a width of 5 pixels. and our X Axis values are A, B, C, D, E

Then C will be placed on the 3rd pixel of the chart and E will be on the 5th pixel.

If our with is 10 pixels, then, C will be on the 6th pixel and E will be on the 10th pixel, and so on.

svg.append("g")
        .attr("transform", `translate(0,${height})`)
        .call(d3.axisBottom(x))
        .selectAll("text")
        .attr("transform", "translate(-10,0)rotate(-45)")
        .style("text-anchor", "end")
        .attr("class", "axis-label");

Now we are adding the X axis itself to the chart. Basically wrapping it inside a group.

Translating/moving it by the height value. Why the height value? because we want it to be at the bottom of chart, because X axis is always at the bottom of the chart, right?

We are using the x value, which is our scale to create a scale for our axis.

And then, moving and rotating the x axis labels by -10 and -45 degrees, to enhance readability of the labels/ticks.

      // Add Y axis
      const y = d3.scaleLinear()
        .domain([0, 100])
        .range([height, 0]);
      svg.append("g")
        .call(d3.axisLeft(y))
        .selectAll("text")
        .attr("class", "axis-label");

We are doing pretty much the same here with Y axis, like we did with X axis.

Defining the domain from 0 to 100, and basically, here the 100 should’ve been the maximum value of our values, but we can leave it 100 here if it’s a percentage chart maybe.

And defining the height, 0 as our range, why height, 0 not 0, height? because we want the 100 to be our height bar right? I mean, the chart is drawn from pixel 0,0 to pixel 0,100 right? assuming (x,y).

So, if we want the height value to be at the top of the chart, then it should map to pixel 0, and the lowest value will map to the most far pixel in the chart, which will be the height value.

Then, we are adding the left axis to the chart as well by a group using the y scale.

Bars

      // Bars
      svg.selectAll("mybar")
        .data(data)
        .join("rect")
        .attr("x", d => x(d.name))
        .attr("y", d => y(d.value))
        .attr("width", x.bandwidth())
        .attr("height", d => height - y(d.value))
        .attr("class", "bar");

Finally let’s draw our chart bars.

We are assigning a temp value ‘mybar’ to our bars just to to read the data, and then reading our data from our data array.

The join method here creates a rect element for each entry of the data, and then moving it by the name, which is the x axis value, it will use our defined x scale that we’ve defined above to place the bar on the exact place on the x axis, and the same for the y value, it will use the y scale we’ve defined above to place the bar on the exact place on the y axis using the d.value, or the value property.

Then we are defining the width of the bar which here we r using the bandwidth method from the x scale, and it basically tells us what’s the free space that the bar can fill on the chart, an easy formula for that would be chartWidth / dataLength. but yeah, let bandwidth deal with that.

Then we assign the height property which will basically be the height of the bar. And, again, because we start from pixel 0,0 we have to subtract the yscale value from the height, because everything is upside down.

Then finally adding class ‘bar’ for each rec element of the chart for styling.

Finally

I hope you got what’s written in here and stay tuned for another post that will use this chart in an Angular component and also making our chart responsive.