Search code examples
javascriptsortingd3.jsnvd3.js

Preview d3 bar chart such that it sorted by values for each x value


I created a nvd3 group bar chart with following data.

var data = [{
  key: "JUN",
  values: [
    { x: 2013, y: 200 },
    { x: 2014, y: 352 },
    { x: 2015, y: 1100 },
    { x: 2016, y: 120 }
  ]
}, {
  key: "JUL",
  values: [
    { x: 2013, y: 200 },
    { x: 2014, y: 862 },
    { x: 2015, y: 124 },
    { x: 2016, y: 500 }
  ]
}, {
  key: "MAR",
  values: [
    { x: 2013, y: 578 },
    { x: 2014, y: 964 },
    { x: 2015, y: 788 },
    { x: 2016, y: 152 }
  ]
}, {
  key: "JAN",
  values: [
    { x: 2013, y: 20 },
    { x: 2014, y: 485 },
    { x: 2015, y: 258 },
    { x: 2016, y: 900 }
  ]
}];

nv.addGraph(function() {
  var chart = nv.models.multiBarChart();

  chart.xAxis.tickFormat(d3.format(',f'));
  chart.yAxis.tickFormat(d3.format(',.1f'));

  d3.select('#chart svg')
      .datum(data)
      .transition().duration(500)
      .call(chart);

  nv.utils.windowResize(chart.update);

  return chart;
});
#chart svg {
  height: 400px;
}
<link href="https://cdnjs.cloudflare.com/ajax/libs/nvd3/1.8.6/nv.d3.min.css" rel="stylesheet"/>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.17/d3.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/nvd3/1.8.6/nv.d3.min.js"></script>

<div id="chart"><svg></svg></div>


I got the result as follows. enter image description here

My question is, can I rearrange the d3 chart such that it reorder the series axis such that it view in sorted way as follows. In this way bars of each series swap for each x values. Can we achieve this in d3?

enter image description here


Solution

  • You would want to override the nv.models.multiBar class. That is where the bars are being drawn.

    It looks appears that each month has its own group and they are being stacked on top of each other. Each grouping is offset from one another by a margin.

    You would need to sort each group by its y-value and alter the x-position (horizontal margin).

    The group offset is determined by: x(getX(d, i)).

    bars
      .attr('class', function(d, i) {
        return getY(d, i) < 0 ? 'nv-bar negative' : 'nv-bar positive'
      })
      .attr('transform',
        function(d, i) { return 'translate(' + x(getX(d, i)) + ',0)';
      })
    

    The bar position within the group is determined by:

    barSelection
      .attr('x', function(d, i) {
        return d.series * x.rangeBand() / data.length;
      })
    

    If you can sort the data, you can alter these values.


    You can transform the data with the following method. This can be used inside the bar layout function to reference the index position.

    function sortGroupedData(data) {
      var groups = {};
      for (var m = 0; m < data.length; m++) {
        var subset = data[m];
        for (var y = 0; y < subset.values.length; y++) {
          var point = subset.values[y],
              tuples = groups[point.x] || [];
          tuples.push([subset.key, point.y, m]);
          groups[point.x] = tuples;
        }
      }
      Object.keys(groups).forEach(function(key) {
        groups[key].sort(function(a, b) {
          return a[1] - b[1];
        });
      });
      return groups;
    }
    

    The resulting data is a map (by year) with each month sorted by its y-value in ascending order. I included the original index (position) of the bar within its grouping.

    {
      "2013" : [
        ["JAN", 20,  3],
        ["JUN", 200, 0],
        ["JUL", 200, 1],
        ["MAR", 578, 2]
      ],
      "2014" : [
        ["JUN", 352, 0],
        ["JAN", 485, 3],
        ["JUL", 862, 1],
        ["MAR", 964, 2]
      ],
      "2015" : [
        ["JUL", 124,  1],
        ["JAN", 258,  3],
        ["MAR", 788,  2],
        ["JUN", 1100, 0]
      ],
      "2016" : [
        ["JUN", 120, 0],
        ["MAR", 152, 2],
        ["JUL", 500, 1],
        ["JAN", 900, 3]
      ]
    }
    

    Required code changes

    nv.models.multiBar = function() {
      // ...
      function chart(selection) {
        // ...
        selection.each(function(data) {
          var dataMap = sortGroupedData(data);
          // ...
          else {
            barSelection
              .attr('x', function(d,i) {
                var pos = dataMap[d.x].map((val, idx) => val[0] === d.key ? idx : -1).filter(x => x > -1)[0];
                return pos * x.rangeBand() / data.length;
              })
            // ...
    

    Final working demo

    Disclaimer: This does not work with stacked, I will fix that when I get the chance. This only fixes the user's immediate problem.

    https://jsfiddle.net/MrPolywhirl/xrj4eyph/


    Stacked

    The d.y1 values are pre-determined before writing out the y-position for the SVG. We need to re-calculate the y1 (bottom) positions for each item in the data.

    if (stacked) {
      barSelection
        .attr('y', function(d, i, j) {
          var yVal = 0;
          if (!data[j].nonStackable) {
            yVal = y(d.y1); // Already determined...