What is the best way to create small multiples in d3.js v6?

I am trying to create small multiple bar charts that have different y-axis scales using d3 v6. There are a few examples out there (https://flowingdata.com/2014/10/15/linked-small-multiples/) of small multiples for previous versions of d3, but not v6, which seems to have a good number of changes implemented.

I don’t have much experience with d3, so I am probably missing something obvious, but I can’t get the bars to properly generate, the axes are generating (though I think I am generating them too many times on top of each other).

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8">
    <title>Small multiple bar charts</title>    
    <script src="https://d3js.org/d3.v6.min.js"></script>
</head>
<body>
    <div id='vis'></div>
    <script type="text/javascript">
    
    // Set the sizing metrics
    var width = 150;
    var height = 120;
    var margin = {top: 15, right: 10, bottom: 40, left: 35};

    // Create the axes
    var xScale = d3.scaleBand()
                    .range([0, width])
                    .padding(0.1);
                
    var yScale = d3.scaleLinear()
                    .range([height, 0]);

    var xAxis = d3.axisBottom()
                    .scale(xScale);

    // Load the data
    d3.csv('data.csv').then(function(data) {
        data.forEach(function(d) {
            d.segment = d.segment;
            d.parameter = d.parameter;
            d.the_value = +d.the_value;
        });
        
        // set the x domain
        xScale.domain(data.map(function(d) { return d.segment; }));

        // group the data
        metrics = Array.from(
            d3.group(data, d => d.parameter), ([key, value]) => ({key, value})
        );

        // create a separate SVG object for each group
        var svg = d3.select('#vis').selectAll('svg')
                    .data(metrics)
                    .enter()
                    .append('svg');

        // loop over the data and create the bars
        metrics.forEach(function(d, i) {
            console.log(d);
            console.log(metrics);
            yScale.domain([0, d3.max(metrics, function(c) { return c.the_value; })]);

            svg.selectAll('.bar')
                .data(d)
                .enter().append('rect')
                .attr('class', 'bar')
                .attr('x', function(c) { return xScale(c.segment); })
                .attr('width', xScale.bandwidth())
                .attr('y', function(c) { return yScale(c.the_value); })
                .attr('height', function(c) { return height - yScale(c.the_value); })
                .attr('fill', 'teal');

            svg.append('g')
                .attr('transform', 'translate(0,' + height + ')')
                .call(xAxis)
        });
    });
    </script>
</body>
</html>

Here is the data file:

segment,parameter,the_value
A,one,33
A,two,537723
A,three,14
A,four,5
A,five,0.093430759
B,one,76
B,two,137110
B,three,16
B,four,20
B,five,0.893868331
C,one,74
C,two,62020
C,three,25
C,four,14
C,five,0.862952872

Eventually I would also like to get the charts linked so that when series A is hovered on the first graph the value will display for each series on all of the graphs, but the first step is to get the visuals properly working.

1 thought on “What is the best way to create small multiples in d3.js v6?”

  1. There’s a few small changes to get it working:

    • When you set the domain on the x scale, you just need the unique segments e.g. A, B, C and not the full list of segments from the data.

    • When you create the 5 SVGs you can class them so that you can refer to each separately when you loop through the values of the metrics. So the first small multiple has a class of one, the second small multiple has a class of two etc

    • Reset the y domain using the set of the_values from the metrics you’re charting – i.e. use d not metrics

    • When you loop metrics first select the small multiple for that metric and then selectAll('.bar')

    • Pass d.value to data as this makes the references to c.the_value etc work properly

    • To prevent adding the x axis multiple times, again select the SVG for the specific small multiple before call(xAxis) otherwise you add as many axes as there are parameters to each small multiple.

    I faked up your data to include random data.

    See the example below – maybe there’s a smarter way to do it:

    // fake data
    var data = ["A", "B", "C"].map(seg => {
      return ["one", "two", "three", "four", "five"].map((par, ix) => {
        return {
          "segment": seg, 
          "parameter": par, 
          "the_value": (Math.floor(Math.random() * 10) + 1) * (Math.floor(Math.random() * 10 * ix) + 1)
        }
      });
    }).flat();
    
    // Set the sizing metrics
    var width = 150;
    var height = 120;
    var margin = {top: 15, right: 10, bottom: 40, left: 35};
    
    // Create the axes
    var xScale = d3.scaleBand()
      .range([0, width])
      .padding(0.1);
    
    var yScale = d3.scaleLinear()
      .range([height, 0]);
    
    var xAxis = d3.axisBottom()
      .scale(xScale);
      
    // set the x domain
    // put unique segments into the domain e.g. A, B, C
    var uniqueSegments = Array.from(new Set(data.map(function(d) {return d.segment;} )));
    xScale.domain(uniqueSegments);
    
    // group the data
    var metrics = Array.from(
      d3.group(data, d => d.parameter), ([key, value]) => ({key, value})
    );
    
    // create a separate SVG object for each group
    // class each SVG with parameter from metrics
    var svg = d3.select('#vis').selectAll('svg')
      .data(metrics)
      .enter()
      .append('svg')
      .attr("class", function(d) { return d.value[0].parameter;});
    
    // loop over the data and create the bars
    metrics.forEach(function(d, i) {
      //console.log(d);
      //console.log(metrics);
      // reset yScale domain based on the set of the_value's for these metrics
      yScale.domain([0, d3.max(d.value, function(c) { return c.the_value; })]);
    
      // select the right svg for this set of metrics
      d3.select("svg." + d.value[0].parameter)
        .selectAll('.bar')
        .data(d.value) // use d.value to get to the the_value
        .enter()
        .append('rect')
        .attr('class', 'bar')
        .attr('x', function(c) { return xScale(c.segment); })
        .attr('width', xScale.bandwidth())
        .attr('y', function(c) { return yScale(c.the_value); })
        .attr('height', function(c) { return height - yScale(c.the_value); })
        .attr('fill', 'teal');
    
      // call axis just on this SVG
      // otherwise calling it 5 times for 5 metrics...
      d3.select("svg." + d.value[0].parameter)
        .append('g')
        .attr('transform', 'translate(0,' + height + ')')
        .call(xAxis)
        
    });
    <script src="https://cdnjs.cloudflare.com/ajax/libs/d3/6.5.0/d3.min.js"></script>
    <div id='vis'></div>
    Reply

Leave a Comment