Search code examples
javascriptjqueryd3.jschartsdimple.js

update series order in dimple multi-series chart with interactivity


I've created a version of the dimple interactive chart (see dimplejs.org/advanced_examples_viewer.html?id=advanced_interactive_legends) with two series: one pie series and one line series based on the same dataset.

jsfiddle: http://jsfiddle.net/jose_jimenez/1fvjvyvh/3/

 var svg = dimple.newSvg("#chartContainer", 700, 500);
 var mainSlicer = "classification";
 var pies;
 var lines;

//data here look for ------END OF DATA --------
 var data = [{
   "classification": "Undergraduate",
   "residency": "Resident",
   "year": 2006,
   "head count": 14011
 }, {
   "classification": "Undergraduate",
   "residency": "Domestic",
   "year": 2006,
   "head count": 6347
 }, {
   "classification": "Undergraduate",
   "residency": "International",
   "year": 2006,
   "head count": 380
 }, {
   "classification": "Graduate",
   "residency": "Resident",
   "year": 2006,
   "head count": 2693
 }, {
   "classification": "Graduate",
   "residency": "Domestic",
   "year": 2006,
   "head count": 2075
 }, {
   "classification": "Graduate",
   "residency": "International",
   "year": 2006,
   "head count": 1309
 }, {
   "classification": "Professional",
   "residency": "Resident",
   "year": 2006,
   "head count": 1374
 }, {
   "classification": "Professional",
   "residency": "Domestic",
   "year": 2006,
   "head count": 612
 }, {
   "classification": "Professional",
   "residency": "International",
   "year": 2006,
   "head count": 14
 }, {
   "classification": "Postgraduate",
   "residency": "Resident",
   "year": 2006,
   "head count": 825
 }, {
   "classification": "Postgraduate",
   "residency": "Domestic",
   "year": 2006,
   "head count": 38
 }, {
   "classification": "Postgraduate",
   "residency": "International",
   "year": 2006,
   "head count": 301
 }, {
   "classification": "Undergraduate",
   "residency": "Resident",
   "year": 2007,
   "head count": 13808
 }, {
   "classification": "Undergraduate",
   "residency": "Domestic",
   "year": 2007,
   "head count": 6695
 }, {
   "classification": "Undergraduate",
   "residency": "International",
   "year": 2007,
   "head count": 404
 }, {
   "classification": "Graduate",
   "residency": "Resident",
   "year": 2007,
   "head count": 2848
 }, {
   "classification": "Graduate",
   "residency": "Domestic",
   "year": 2007,
   "head count": 2127
 }, {
   "classification": "Graduate",
   "residency": "International",
   "year": 2007,
   "head count": 1246
 }, {
   "classification": "Professional",
   "residency": "Resident",
   "year": 2007,
   "head count": 1338
 }, {
   "classification": "Professional",
   "residency": "Domestic",
   "year": 2007,
   "head count": 642
 }, {
   "classification": "Professional",
   "residency": "International",
   "year": 2007,
   "head count": 16
 }, {
   "classification": "Postgraduate",
   "residency": "Resident",
   "year": 2007,
   "head count": 930
 }, {
   "classification": "Postgraduate",
   "residency": "Domestic",
   "year": 2007,
   "head count": 53
 }, {
   "classification": "Postgraduate",
   "residency": "International",
   "year": 2007,
   "head count": 302
 }, {
   "classification": "Undergraduate",
   "residency": "Resident",
   "year": 2008,
   "head count": 13192
 }, {
   "classification": "Undergraduate",
   "residency": "Domestic",
   "year": 2008,
   "head count": 7055
 }, {
   "classification": "Undergraduate",
   "residency": "International",
   "year": 2008,
   "head count": 576
 }, {
   "classification": "Graduate",
   "residency": "Resident",
   "year": 2008,
   "head count": 2932
 }, {
   "classification": "Graduate",
   "residency": "Domestic",
   "year": 2008,
   "head count": 2164
 }, {
   "classification": "Graduate",
   "residency": "International",
   "year": 2008,
   "head count": 1247
 }, {
   "classification": "Professional",
   "residency": "Resident",
   "year": 2008,
   "head count": 1288
 }, {
   "classification": "Professional",
   "residency": "Domestic",
   "year": 2008,
   "head count": 687
 }, {
   "classification": "Professional",
   "residency": "International",
   "year": 2008,
   "head count": 22
 }, {
   "classification": "Postgraduate",
   "residency": "Resident",
   "year": 2008,
   "head count": 994
 }, {
   "classification": "Postgraduate",
   "residency": "Domestic",
   "year": 2008,
   "head count": 58
 }, {
   "classification": "Postgraduate",
   "residency": "International",
   "year": 2008,
   "head count": 346
 }];
 // ------END OF DATA --------

 function addSeries(chart) {
   pies = chart.addSeries(slicer, dimple.plot.pie);
   lines = chart.addSeries("classification", dimple.plot.line);
   lines.lineMarkers = true;
   lines.lineWeight = 5;
   pies.radius = 20;
 }

 function createChart() {

   data_attributes = Object.getOwnPropertyNames(data[0]);
   ['year', 'head count', mainSlicer].forEach(function(f) {
     data_attributes.splice(data_attributes.indexOf(f), 1);
   });
   slicer = data_attributes[0];
   var myChart = new dimple.chart(svg, data);
   myChart.setBounds(60, 30, 500, 400)
   var x = myChart.addCategoryAxis("x", ["year", mainSlicer]);
   x.addOrderRule("year");
   myChart.addMeasureAxis("y", "head count");
   myChart.addMeasureAxis("p", "head count");
   addSeries(myChart);
   var myClassificationLegend = myChart.addLegend(550, 200, 150, 400, "left", lines);
   var mySlicerLegend = myChart.addLegend(550, 300, 150, 400, "left", pies);

   myChart.draw();

   myChart.legends = [];

   svg.selectAll("title_text")
     .data(["Click legend to", "show/hide segments:"])
     .enter()
     .append("text")
     .attr("x", 550)
     .attr("y", function(d, i) {
       return 160 + i * 14;
     })
     .style("font-family", "sans-serif")
     .style("font-size", "10px")
     .style('font-weight', 'bold')
     .style("color", "Black")
     .text(function(d) {
       return d;
     });


   var classFilterValues = dimple.getUniqueValues(data, mainSlicer);
   var slicerFilterValues = dimple.getUniqueValues(data, slicer);
   var hiddenValues = [];

   legendBits = myClassificationLegend.shapes;
   legendBits[0] = legendBits[0]
     .concat(mySlicerLegend.shapes[0]);

   legendBits.selectAll('rect')
     // Add a click event to each rectangle
     .on("click", function(e) {
       // This indicates whether the item is already visible or not
       var hide = false;
       var newClassificationFilters = [];
       var newSlicerFilters = [];
       var currentValue = e.aggField.slice(-1)[0];

       // If the filters contain the clicked shape hide it
       var whereIsIt = hiddenValues.indexOf(currentValue);
       if (whereIsIt > -1) {
         //it is hidden and needs to be shown.
         hide = false;
         hiddenValues.splice(whereIsIt, 1);
       } else {
         //it needs to be hidden
         hide = true;
         hiddenValues.push(currentValue);
       }


       classFilterValues.forEach(function(f) {
         if (hiddenValues.indexOf(f) < 0) {
           newClassificationFilters.push(f);
         }
       });
       slicerFilterValues.forEach(function(f) {
         if (hiddenValues.indexOf(f) < 0) {
           newSlicerFilters.push(f);
         }
       });
       // Hide the shape or show it
       if (hide) {
         d3.select(this).style("opacity", 0.2);
       } else {
         d3.select(this).style("opacity", 0.8);
       }

       // Filter the data
       myChart.data = dimple.filterData(dimple.filterData(data, 'classification', newClassificationFilters), slicer, newSlicerFilters);
       // Passing a duration parameter makes the chart animate. Without
       // it there is no transition

       myChart.draw(800, false);

     });

 }

 function moveLegend(legend, offset) {
   $(legend).find('text,rect').attr('y', parseInt($(f).find('text').attr('y')) + offset);
 }

 createChart();

I've rendered the line series after the pies so the lines end up on top of the pie segments. This allows for the mouseover interactivity of the line series to be accessed.

When the pie segments are toggled off and then on, they end up rendering on top of the line segment when they come back. This is likely because the paths are added to the end of the chart object and not interjected before the line series. Eventually they block the line series interactivity.

I've tried several solutions including:

  • attempting to move the pie.shapes.selectAll('path') to back using the d3.prototype.moveToBack code (see bl.ocks.org/eesur/4e0a69d57d3bfc8a82c2)
    • The paths don't have parentNodes or the parentNodes don't have childNodes
  • attempting to remove the line series and add it back again using chart.series.slice and or just removing both using chart.series = []
    • The chart was a mess afterward because the objects were not all removed.
    • removing pie.shapes and or line.shapes didn't help much.

I can bring them back by hiding all the lines and bringing them back (the order of addition makes the lines come back at the top).

I feel like jumping into the paths and using jquery to remove them and add them back which would feel like a total hack. Anyone have a more elegant way to handle this?


Solution

  • What you need to do inside chart.addSeries is add each series to a separate 'g' element within the svg. At the moment your circles and lines are all mixed up in the same g element, so the last appended elements are always drawn on top.

    e.g. so

    <g>
    <path>
    <circle>
    <path>
    <newly appended circle>
    </g>
    

    becomes

    <g>
    <circle>
    <newly appended circle>
    </g>
    <g>
    <path>
    <path>
    </g> 
    

    All paths will then always be drawn after all the circles as svg just draws the elements in the order above