DEV Community

Benjamin G. for Camptocamp Geospatial Solutions

Posted on

D3-Helper: Mini-library to quickly set up a D3 chart.

I'm not a native English speaker

To draw charts in a web-page, my favorite library is D3. Even if that's not the simplest library to draw charts, it's quite stable and I was always able to reach my aim with it.
But after some implementations of charts, and as D3 is not only made to draw charts, my project team has seen that some part of the implementation was quite repetitive:

  • We always have to draw or clear the SVG, with a fixed ratio.
  • We always have to manage a margins system.
  • In Cartesian charts, we always have to set an axis system.
  • We often have a title, a color, a font-size...

Also, I don't know why, I really don't know why, but it looks like that for D3, all examples are always a one-function-code. That's not handy to use nor to test it. Even if you don't mind about d3-helper, please dispatch your D3 code in multiple functions 😀

So we have decided to make this micro-library d3-helper.
The aim is to set up a base for charts easily, so then the final app can focus on the drawing relative to the data. It currently contains two classes:

  • A BaseD3ChartSVG class, to select, draw and clear the SVG, with a "chart" group inside depending on a margins system.
  • A CartesianChart class (that extends the BaseD3ChartSVG class), to (mainly) draw and manage axes (set scale, draw, clean) for a Cartesian chart. Supported data must be numbers, texts or dates.

Both classes define small methods that can be called independently and "summary" methods that call multiple methods (for standard uses).
Some methods were "restricted", but we finally decided to have almost everything public to maximize the flexibility. And we leave the encapsulation to the final app (and never mind for the 0.1k more of compile code).

In the examples below, I use node to install the d3-helper library and some additional D3 functions to play with my data.

I also use a static dataset "data" (an Array of Objects).


Draw a pie-chart

Directly taken from the d3-helper pie-chart example.

I start by creating a chart class that extends BaseD3ChartSVG, so this will be the BaseD3ChartSVG. That's more handy than to attach an instance of it to a variable. In the constructor, I set the path to the chart element to render the PieChart in. Also, I set a d3Pie basic function.

class PieChart extends BaseD3ChartSVG {
  constructor() {
    super('.chart');
    this.pie_ = d3Pie()
      .sort(null)
      .value(d => d.elevation);
  }
  ...

Enter fullscreen mode Exit fullscreen mode

Then I want a draw function to draw my PieChart. I'll use the BaseD3ChartSVG to render the "generic part" of the chart: draw the SVG and set the margins.

  draw() {
    // Use BaseD3ChartSVG to set available space.
    this.updateSize();

    // Move pie-chart to the center of the svg.
    this.setMargins({ top: this.height / 2, left: this.width / 2 });

    // Draw the SVG.
    this.drawSVG();
    ...
Enter fullscreen mode Exit fullscreen mode

The generated SVG looks like that:

<svg viewBox="0 0 439 397" preserveAspectRatio="xMinYMin" class="svg">
  <g transform="translate(80, 60)" class="chart">...</g>
</svg>
Enter fullscreen mode Exit fullscreen mode

The SVG is now available with this.svg. The chart zone inside is available with this.chart.
The chart is the SVG minus the margins. It's the zone to render our chart in.

Then, I can render a pie-chart (or other kind of charts) on data in the chart group with custom code:

    ...
    // Draw a custom pie chart.
    const outerRadius = Math.min(this.width, this.height) / 2;
    const arc = d3Arc().innerRadius(outerRadius / 2).outerRadius(outerRadius)
    const pie = this.chart.selectAll()
      .data(this.pie_(data))
      .enter()
      .append("g")
      .attr('class', 'arc');
    // Draw pie slices
    pie.append('path')
      .attr('d', arc)
      .attr('fill', (d, i) => HEX_COLORS[i])
      .attr('stroke', 'white')
      .attr('stroke-width', '2px');
    // Draw text in slices
    pie.append("text")
      .attr("transform", d => `translate(${(arc.centroid(d)[0] - 12)} ${arc.centroid(d)[1]})`)
      .attr("dy", ".35em")
      .text(d => d.data.id);
  }
Enter fullscreen mode Exit fullscreen mode

Then if you do:

const chart = new PieChart();
chart.draw();
Enter fullscreen mode Exit fullscreen mode

It renders:
Alt Text

And then to refresh, you can add this function:

  Refresh() {
    this.removeSVG();
    this.updateSize();
    this.draw();
  }
Enter fullscreen mode Exit fullscreen mode

Full JS here.


Draw a Cartesian chart

Directly taken from the d3-helper pie-chart example.

For Cartesian charts, it's more or less the same principle. I use the CartesianChart class that extends from BaseD3ChartSVG class to render the axes in a SVG. Then, I write custom code to draw my data in a chart.

The CartesianChart is based on the type of the data (number, text or date), and on a configuration object. The minimal configuration is the keys of the data to use to set the axes (expected data are Objects in an Array):

    const config = {
      xAxis: {
        axisColumn: 'distance',
      },
     yAxis: {
        axisColumn: 'elevation',
      },
    };

Enter fullscreen mode Exit fullscreen mode

To draw the SVG with the axes, you can write:

    // Set the config for CartesianChart.
    this.setConfig(config);
    // Use BaseD3ChartSVG to draw the SVG.
    this.removeUpdateDrawSVG();
    // Already optional, use CartesianChart to get label for axis from the data (as we have no label in the config).
    this.useDataLabelAsDefaultForAxis('xAxis');
    this.useDataLabelAsDefaultForAxis('yAxis');
    // Set and draw axis using CartesianChart.
    this.setXAxis(data);
    this.setYAxis(data);
Enter fullscreen mode Exit fullscreen mode

That's it. Then we can draw a line chart for instance:

    // Draw a custom line chart.
    const lineFunction = d3Line()
      .curve(d3CurveMonotoneX)
      .x((d, i) => this.xScale(this.xData[i]))
      .y(d => this.yScale(d));

    this.chart
      .append('path')
      .attr('class', 'line')
      .attr('d', lineFunction(this.yData))
      .attr('stroke', `rgb(${this.color.join(',')})`) // use the default color.
      .attr('stroke-width', '1')
      .attr('fill', 'none');
Enter fullscreen mode Exit fullscreen mode

It renders:
Alt Text

Full JS here

Supplementary notes

  • In the Cartesian chart of d3-helper, data can't cross the axis (but negative values are possible). But if it's needed it's possible to override the drawXAxis method to change this behavior.
  • It is possible to have two y axes: one y axis, and one opposite y axis (look at the chart below). But it can have only one x axis.
  • Take a look at the Cartesian configuration and the project examples for all possibilities.

Another Cartesian chart result with more configuration full JS here:

It renders:
Alt Text


More - Implementation in a project

If you have only one simple chart to draw, you can use the same implementation as in the previous examples.

In one of our projects, we had to display multiple Cartesian charts (bars, scatter plot and lines). We made a component (We use Angular in this project, but any "view-level class" can do the same) that extends an adapter, that extends this CartesianChart. This component can set an HTML class via the configuration (to set the correct chart using a unique DOM path). That allows us to have some of these components on one page, and so multiple charts on the same page.

To have cleaner code, we separate line, point, vertical bars and horizontal bars charts in four classes. These classes implement an interface (we use Typescript) ChartDrawer. Each ChartDrawer has a ctx object that is a CartesianChart class, and a draw method. Then we have a configuration to say which data must be drawn by which ChartDrawer. That allows us to have lines, points and bars on the same chart, but with clean code. Each class is responsible for its own rendering and interaction possibilities.

The final implementation looks like this:

Alt Text

The code is quite well organized, flexible and testable.


Notes

  • It's a "half-public" library, made within a private project. We sadly don't have time to look at new issues or PR, except if we experience them directly. But don't hesitate to fork or take a look at examples to implement your charts.
  • It's only tested on modern browsers with small datasets.

Discussion (0)