A reusable function for creating charts with axes

Preview image

The formula for creating a Cartesian chart (rectangular, with X and Y coordinates) is pretty straightforward. Whether you're creating a bar chart, a scatter plot, a line or area chart, you'll almost always follow the same steps and create the same pieces.

This is of course the type of scenario where a reusable bit of code is an easy win.

While building examples for my upcoming book on D3 + SVG, I also wanted to hide these "plumbing" steps to keep the code focused on the skill being demonstrated. To that end, I created the function I'm sharing with you today.

(Click here to be notified as soon as the D3 + SVG book becomes available!)

Using the function

Before we walk through the function's code to understand it, let's look at how to use it.

initChart('#someDiv')

That's it.

Well, that's it if you're fine with the defaults, which produces a chart that looks like this.

The default configuration is 600 pixels wide and 400 pixels tall, with both axes running from 0 to 100. The only thing you have to pass to initChart is a selector string so it knows where to create the chart.

The optional second argument to initChart is a configuration object which will be merged into a default config. The default configuration takes this shape.

config = {
  width: 600,
  height: 400,
  margin: {
    top: 10,
    right: 10,
    bottom: 20,
    left: 25
  },
  xScale: d3.scaleLinear().domain([0, 100]),
  yScale: d3.scaleLinear().domain([0, 100])
}

All of the following options would work.

initChart('#someDiv', { height: 200 })
 
initChart('#someDiv', {
  margin: {
    bottom: 50
  }
})
 
initChart('#someDiv', {
  width: 800,
  xScale: d3.scaleTime().domain([
    new Date('1/1/2016'),
    new Date()
  ])
})

Understanding the function

The function itself is fairly straightforward. Rather than break it into pieces with explanations above partial code blocks, I want to try something different. I have heavily annotated the function below in hopes it will be easier to understand.

Is this format helpful? Hit reply and let me know 👍 or 👎!

function initChart(selector, config) {
  // default config definition
  // is merged with partial or full config param
  // largely thanks to ES6 rest params
  // see https://mzl.la/2AIh0Sk for more
  config = {
    width: 600,
    height: 400,
    margin: {
      top: 10,
      right: 10,
      bottom: 20,
      left: 25,
      // merge any margin props provided
      ...(config || {}).margin,
    },
    xScale: d3.scaleLinear().domain([0, 100]),
    yScale: d3.scaleLinear().domain([0, 100]),
    ...config, // merge all top level props provided
  };
  // store refs to important props
  const { width, height, margin } = config;
 
  // store chart's usable area in props
  const w = width - margin.left - margin.right;
  const h = height - margin.top - margin.bottom;
 
  const svg = d3
    .select(selector) // use the provided selector
    .append('svg') // create SVG of specified size
    .attr('width', width)
    .attr('height', height)
    // create root group that respects margins
    .append('g')
    .attr('transform', `translate(
      ${margin.left}${margin.top}
    )`);
 
  // create Y axis that fills vertical space
  svg.call(d3.axisLeft(config.yScale.range([h, 0])));
 
  // create X axis that fills horizontal space
  // and sits at the bottom of the chart
  svg
    .append('g')
    .attr('transform', `translate(0, ${h})`)
    .call(d3.axisBottom(config.xScale.range([0, w])));
 
  // return the root container so commands can be chained
  return svg;
}

That's it! Make sense? If not, please let me know.

As always, you can play with the real thing here.

Happy hacking!