Codementor Events

D3.js - three basic charts

Published Mar 11, 2020
D3.js - three basic charts

Getting to grips with D3.js can be a steep learning curve. I've already written two articles on it's most important feature - appending data.

Here's a new one to get you started on the three most basic charts:

  • vertical bar
  • scatter
  • line and area

It comes with a simple demo. All three chart types have the same core elements:

  • an svg
  • an x_scale and a y_scale
  • x and y axes
  • data

After drawing your svg, you need to set the scales:

  const years = d3.set(my_data, d => d.year).values();
  const y_max = d3.max(my_data, d => d.value);
  
  const x_scale = d3.scaleBand().domain(years).range([0,width]);
  const y_scale = d3.scaleLinear().domain([0,y_max]).range([height,0]);

In this instance:

  • x_scale = ordinal, based on the value of the year variable
  • y_scale = linear, starting at 0 and running to the maximum of the value variable

A separate article on d3-scale will follow!

Now the axes:

 svg.append("g")
    .call(d3.axisBottom(x_scale))
    .attr("transform","translate(" + margin + "," + (height-margin) + ")");
  
  svg.append("g")
    .call(d3.axisLeft(y_scale))
    .attr("transform","translate(" + margin + "," + margin + ")");
  

I haven't applied CSS or used any additional functionality here for simplicity. I'm planning another article on d3-axis as well.

Again for clarity, I've used the same dataset for each chart.

my_data = [{"year":1999,"value":55}, ... {"year":2008,"value":180}]

So the next step is to append the data - see my earlier post for more details on this.

You'll notice in my simple demo that there are two different data append code blocks. This is the difference in chart type:

  • bar and scatter - d3 appends a new svg element (rectangle or circle) for each row in the datasest
  • line and area - d3 appends a new element for each dataset (a path generated by d3.area and d3.line). This is why my_data is enclosed in [], otherwise it will attempt (and fail) to draw a path for each row in the dataset.

And finally a walk-through of the charts, one by one:

Bar Chart

    my_group.select(".chart_rect")
      .attr("x",d => x_scale(d.year))
      .attr("y", d => y_scale(d.value))
      .attr("width",x_scale.bandwidth())
      .attr("height",d => y_scale(0) - y_scale(d.value))
      .attr("fill","lightblue")
      .attr("transform","translate(" + margin + "," + margin + ")");
  • x and y positions are based on x_scale and y_scale
  • width is the bandwidth of x_scale.
  • height is less intuitive. You'll notice that the range of y_scale is in reverse [height,0] Why? The y position on an svg runs from the top to bottom and we want the scale in reverse. Therefore, when we calculate the height, we need to account for this, so it's y_scale(0) - y_scale(d.value)
  • fill - the fill colour
  • transform translates the rectangles for the top and left margin.

Scatter Chart

    my_group.select(".chart_dot")
      .attr("cx",d => x_scale(d.year) + (x_scale.bandwidth()/2))
      .attr("cy", d => y_scale(d.value))
      .attr("r", 10)
      .attr("fill","red")
      .attr("transform","translate(" + margin + "," + margin + ")");

This is very similar but with different attributes for the svg circle element:

  • cx and cy instead of x and y
  • cx also needs to translate half of the x_scale bandwidth so the dot is central
  • r - which is the radius in pixels

Line and Area Chart

     line_group.select(".chart_line")
       .attr("d", line)
       .attr("fill","none")
       .attr("stroke","green")
       .attr("stroke-width",1)
       .attr("transform","translate(" + (margin + (x_scale.bandwidth()/2)) + "," + margin + ")");
  
     line_group.select(".chart_area")
       .attr("d", area)
       .attr("stroke","none")
       .attr("fill","green")
       .attr("fill-opacity",0.2)
       .attr("transform","translate(" + (margin + (x_scale.bandwidth()/2)) + "," + margin + ")");

The d attributes refer to the d3 line and area generators defined earlier

   const line = d3.line()
            .x(d => x_scale(d.year))
            .y(d => y_scale(d.value));
  
   const area = d3.area()
            .x(d => x_scale(d.year))
            .y0(d => y_scale(0))
            .y1(d => y_scale(d.value));

They both draw a data-driven path using the x_scale and y_scale. For the area there is an additional y co-ordinate for the bottom of the area - in this case y_scale(0).

The rest is similar to the other charts:

  • fill and stroke - colour with a fill-opacity of 0.2 for the area for contrast and a fill of none for the lines - we're interested in the stroke.
  • transform includes half of the x_scale bandwidth as well. You cannot add an x attribute to an svg path so any translating needs to be done in a transform attribute.

Remember, you can see it working live on my simple demo.

Discover and read more posts from Bryony Miles
get started