4
\$\begingroup\$

Could I get some advice on the following D3 code that transitions a stacked bar chart into a bar chart with two different datasets? I would like to know how to write cleaner and more efficient code. And could you also check if the enter, update, exit pattern is being used correctly?

var value = 'stack';

var data = [{
  name: "Shihuahuaco",
  value: 1067,
  china: 772
}, {
  name: "Cachimbo",
  value: 283,
  china: 1
}, {
  name: "Estoraque",
  value: 204,
  china: 150
}, {
  name: "Cumala",
  value: 154,
  china: 0
}, {
  name: "Ishpingo",
  value: 108,
  china: 3
}, {
  name: "Huayruro",
  value: 108,
  china: 1
}, {
  name: "Tornillo",
  value: 61,
  china: 4
}, {
  name: "Congona",
  value: 54,
  china: 0
}, {
  name: "Capirona",
  value: 37,
  china: 5
}, {
  name: "Tahuari",
  value: 33,
  china: 14
}, {
  name: "Marupa",
  value: 33,
  china: 1
}, {
  name: "Quinilla",
  value: 28,
  china: 4
}, {
  name: "Azucar huayo",
  value: 22,
  china: 15
}, {
  name: "Protium sp.",
  value: 19,
  china: 0
}, {
  name: "Nogal",
  value: 15,
  china: 6
}, {
  name: "Ana Caspi",
  value: 14,
  china: 2
}, {
  name: "Cedro",
  value: 14,
  china: 0
}, {
  name: "Carapa guianensis",
  value: 12,
  china: 0
}];

var points = [{
  'lon': 105.3,
  'lat': 33.5,
  'name': 'China',
  'GTF': 1024,
  "ID": "CHN"
}, {
  'lon': -70.9,
  'lat': 18.8,
  'name': 'Dominican Republic',
  'GTF': 470,
  "ID": "DOM"
}, {
  'lon': -101,
  'lat': 38,
  'name': 'USA',
  'GTF': 248,
  "ID": "USA"
}, {
  'lon': -102.5,
  'lat': 22.7,
  'name': 'Mexico',
  'GTF': 220,
  "ID": "MEX"
}, {
  'lon': 2.98,
  'lat': 46,
  'name': 'France',
  'GTF': 85,
  "ID": "FRA"
}];

var margin = {
    top: 20,
    right: 30,
    bottom: 150,
    left: 60
  },
  widthB = 700 - margin.left - margin.right,
  heightB = 500 - margin.top - margin.bottom;

var dataIntermediate = ['value', 'china'].map(function(key, i) {
  return data.map(function(d, j) {
    return {
      x: d['name'],
      y: d[key]
    };
  })
})

var dataStackLayout = d3.layout.stack()(dataIntermediate);

var svgBar = d3.select("#chart").append("svg")
  .attr("width", widthB + margin.left + margin.right)
  .attr("height", heightB + margin.top + margin.bottom)

var gBar = svgBar.append("g")
  .attr("transform", "translate(" + margin.left + "," + margin.top + ")")
  .attr('class', 'gBar');

var x = d3.scale.ordinal()
  .rangeRoundBands([0, widthB], .2);

var y = d3.scale.linear()
  .range([heightB, 0]);

var xAxis = d3.svg.axis()
  .scale(x)
  .orient("bottom");

var yAxis = d3.svg.axis()
  .scale(y)
  .orient("left")
  .ticks(8)
  .tickFormat(function(d) {
    return y.tickFormat(4, d3.format(",d"))(d)
  });

data.forEach(function(d) {
  d.value = +d.value; // coerce to number
  d.china = +d.china;
});

x.domain(dataStackLayout[0].map(function(d) {
  return d.x;
}));

y.domain([0, d3.max(dataStackLayout[dataStackLayout.length - 1],
  function(d) {
    return d.y0 + d.y;
  })]).nice();

var layer;

// this part 
var bars;

var gX = gBar.append("g")
  .attr("class", "axis")
  .attr("transform", "translate(0," + (heightB + 10) + ")");

var gY = gBar.append("g")
              .attr("class", "y axis");

function draw() {
  if (value == 'stack') {

    x.domain(dataStackLayout[0].map(function(d) {
      return d.x;
    }));

    y.domain([0, d3.max(dataStackLayout[dataStackLayout.length - 1],
      function(d) {
        return d.y0 + d.y;
      })]).nice();

    layer = gBar.selectAll(".stack")
      .data(dataStackLayout);

    layer.exit()
      .transition()
      .delay(function(d, i) {
        return 30 * i;
      })
      .duration(1500)
      .style("fill", "none")
      .remove();

    layer.enter().append("g")
      .attr("class", "stack")
      .style("fill", function(d, i) {
        return i == 0 ? '#b4d5c3' : '#ecaeb3';
      });

    bars = layer.selectAll("rect")
      .data(function(d) {
        return d;
      });

    bars.exit()
      .transition()
      .delay(function(d, i) {
        return 30 * i;
      })
      .duration(1500)
      .attr("y", y(0))
      .attr("height", heightB - y(0))
      .style('fill-opacity', 1e-6)
      .remove();

    bars.enter().append("rect")
      .transition()
      .delay(function(d, i) {
        return 30 * i;
      })
      .duration(3000)
      .attr("x", function(d) {
        return x(d.x);
      })
      .attr("y", function(d) {
        return y(d.y + d.y0);
      })
      .attr("height", function(d) {
        return y(d.y0) - y(d.y + d.y0);
      })
      .attr("width", x.rangeBand());


    // the "UPDATE" set:
    bars.transition().delay(function(d, i) {
        return 30 * i;
      }).duration(1500).attr("x", function(d) {
        return x(d.x);
      }) // (d) is one item from the data array, x is the scale object from above
      .attr("width", x.rangeBand()) // constant, so no callback function(d) here
      .attr("y", function(d) {
        return y(d.y + d.y0);
      })
      .attr("height", function(d) {
        return y(d.y0) - y(d.y + d.y0);
      })
      .style("fill-opacity", 1);

    gX.transition().duration(300).call(xAxis)
      .selectAll("text")
      .style('font-size', '14px')
      .style('font-family', 'Alegreya')
      .style("text-anchor", "end")
      .attr("dx", "-0.40em")
      .attr("dy", ".10em")
      .attr("transform", function(d) {
        return "rotate(-65)"
      });

    gY.transition().duration(300).call(yAxis)
        .selectAll("text")  
        .style('font-size','16px')
        .style('font-family','Alegreya');

  } else {

    x.domain(points.map(function(d) {
      return d.name;
    }));

    y.domain([0, 1024]).nice();

    // this part 
    bars = layer.selectAll("rect")
      .data(points);

    bars.exit()
      .transition()
      .delay(function(d, i) {
        return 30 * i;
      })
      .duration(1500)
      .attr("y", y(0))
      .attr("height", heightB - y(0))
      .style('fill-opacity', 1e-6)
      .remove();

    bars.enter().append("rect")
      .transition()
      .delay(function(d, i) {
        return 30 * i;
      })
      .duration(3000)
      .attr("x", function(d) {
        return x(d.name);
      })
      .attr("y", function(d) {
        return y(d.GTF);

      })
      .attr("height", function(d) {
        return heightB - y(d.GTF);;

      })
      .attr("width", x.rangeBand());

    // the "UPDATE" set:
    bars.transition().delay(function(d, i) {
        return 30 * i;
      }).duration(1500).attr("x", function(d) {
        return x(d.name);
      }) // (d) is one item from the data array, x is the scale object from above
      .attr("width", x.rangeBand()) // constant, so no callback function(d) here
      .attr("y", function(d) {
        return y(d.GTF);
      })
      .attr("height", function(d) {
        return heightB - y(d.GTF);
      });

    gX.transition().duration(300).call(xAxis);

    gY.transition().duration(300).call(yAxis);

  }

}

window.onload = draw();


$("#click").on('click', function() {

  if (value == 'stack') {
    value = 'bar';
  } else {
    value = 'stack';
  }

  draw();

});
\$\endgroup\$

1 Answer 1

6
\$\begingroup\$

Before dealing with the "enter", "update" and "exit" pattern, which is specific to D3, I'd like to address some general issues first.

  1. Do not mix jQuery and D3: this mix is normally unnecessary. Not only that, it can make things fail silently. So, instead of:

    $("#click").on('click', function() {
        if (value == 'stack') {
            value = 'bar';
        } else {
            value = 'stack';
        }
        draw();
    });
    

    You could simply do:

    d3.select("#click").on('click', function() {
        if (value == 'stack') {
            value = 'bar';
        } else {
            value = 'stack';
        }
        draw();
    });
    

    By the way, this can be shortened:

    d3.select("#click").on('click', function() {
        value = value === "stack" ? "bar" : "stack"
        draw();
    });
    

    And this brings us to the next point:

  2. Don't rely on variables external to the function to set the function's behaviour. Other part of your code or other codes can change that variable, which will be very hard to debug. Instead of that, use arguments:

    function draw(layout){
        //etc...
    }
    
    var layout = "stack";
    
    d3.select("#click").on('click', function() {
        layout = layout === "stack" ? "bar" : "stack"
        draw(layout);
    });
    

    That way, you can be sure about the value of layout inside the draw function.

  3. The properties value and china in data are already numbers. This...

    data.forEach(function(d) {
        d.value = +d.value; // coerce to number
        d.china = +d.china;
    });
    

    ... is probably a remnant of d3.csv or other dsv function, which always return a string, even if the original value is a number. Just get rid of it.

  4. You cannot transition to...

    .style("fill", "none")
    

    ... since "none" is just a string. Instead of that, use something like:

    .style('fill-opacity', 1e-6)
    

    Which you actually use in other parts of the code.

  5. Since you don't use bars and layer outside draw, you don't need to declare them outside draw.

The enter, update and exit selections

The biggest problem with the enter/update/exit pattern in your code is that, when you create the stacked chart, you append <g> elements, but you don't remove them when creating the bar chart. So, you actually don't need the exit selection for them.

The same way, since this is D3 v3, you don't need the update selection for the bars. It's a good idea keeping this selection, though: this is an incorrect pattern in v3, which was corrected in v4.

This use of <g> elements in the stack chart and just <rect> elements in the bar chart is not the best option. You should deal with groups in both of them, or just rects in both of them. That way, you could create more constant transitions, which would have the same behaviour in bot layouts. However, this would be a major refactor, which is out of the scope of this answer.

Also, since your bar chart right now has less bars than the stacked chart, the bars naturally expand to the right hand side. So, to give the transition more fluidity, I'd set the exit to the right hand side...

bars.exit().attr("x", widthB)

.. and I'd set the enter from that right hand side:

bars.enter().append("rect")
    .attr("x", firstTime ? null : widthB);

Here, firstTime is just a boolean to check if the function is running for the first time.

Finally, I'd remove the transitions from the x axis, they are quite distracting (because of the rotate).

Here is your code with those changes:

var layout = "stack",
  firstTime = true;

var data = [{
  name: "Shihuahuaco",
  value: 1067,
  china: 772
}, {
  name: "Cachimbo",
  value: 283,
  china: 1
}, {
  name: "Estoraque",
  value: 204,
  china: 150
}, {
  name: "Cumala",
  value: 154,
  china: 0
}, {
  name: "Ishpingo",
  value: 108,
  china: 3
}, {
  name: "Huayruro",
  value: 108,
  china: 1
}, {
  name: "Tornillo",
  value: 61,
  china: 4
}, {
  name: "Congona",
  value: 54,
  china: 0
}, {
  name: "Capirona",
  value: 37,
  china: 5
}, {
  name: "Tahuari",
  value: 33,
  china: 14
}, {
  name: "Marupa",
  value: 33,
  china: 1
}, {
  name: "Quinilla",
  value: 28,
  china: 4
}, {
  name: "Azucar huayo",
  value: 22,
  china: 15
}, {
  name: "Protium sp.",
  value: 19,
  china: 0
}, {
  name: "Nogal",
  value: 15,
  china: 6
}, {
  name: "Ana Caspi",
  value: 14,
  china: 2
}, {
  name: "Cedro",
  value: 14,
  china: 0
}, {
  name: "Carapa guianensis",
  value: 12,
  china: 0
}];

var points = [{
  'lon': 105.3,
  'lat': 33.5,
  'name': 'China',
  'GTF': 1024,
  "ID": "CHN"
}, {
  'lon': -70.9,
  'lat': 18.8,
  'name': 'Dominican Republic',
  'GTF': 470,
  "ID": "DOM"
}, {
  'lon': -101,
  'lat': 38,
  'name': 'USA',
  'GTF': 248,
  "ID": "USA"
}, {
  'lon': -102.5,
  'lat': 22.7,
  'name': 'Mexico',
  'GTF': 220,
  "ID": "MEX"
}, {
  'lon': 2.98,
  'lat': 46,
  'name': 'France',
  'GTF': 85,
  "ID": "FRA"
}];

var margin = {
    top: 20,
    right: 30,
    bottom: 150,
    left: 60
  },
  widthB = 700 - margin.left - margin.right,
  heightB = 500 - margin.top - margin.bottom;

var dataIntermediate = ['value', 'china'].map(function(key, i) {
  return data.map(function(d, j) {
    return {
      x: d['name'],
      y: d[key]
    };
  })
})

var dataStackLayout = d3.layout.stack()(dataIntermediate);

var svgBar = d3.select("#chart").append("svg")
  .attr("width", widthB + margin.left + margin.right)
  .attr("height", heightB + margin.top + margin.bottom)

var gBar = svgBar.append("g")
  .attr("transform", "translate(" + margin.left + "," + margin.top + ")")
  .attr('class', 'gBar');

var x = d3.scale.ordinal()
  .rangeRoundBands([0, widthB], .2);

var y = d3.scale.linear()
  .range([heightB, 0]);

var xAxis = d3.svg.axis()
  .scale(x)
  .orient("bottom");

var yAxis = d3.svg.axis()
  .scale(y)
  .orient("left")
  .ticks(8)
  .tickFormat(function(d) {
    return y.tickFormat(4, d3.format(",d"))(d)
  });

x.domain(dataStackLayout[0].map(function(d) {
  return d.x;
}));

y.domain([0, d3.max(dataStackLayout[dataStackLayout.length - 1],
  function(d) {
    return d.y0 + d.y;
  })]).nice();

var gX = gBar.append("g")
  .attr("class", "axis")
  .attr("transform", "translate(0," + (heightB + 10) + ")");

var gY = gBar.append("g")
  .attr("class", "y axis");

function draw(layout) {
  if (layout == 'stack') {

    x.domain(dataStackLayout[0].map(function(d) {
      return d.x;
    }));

    y.domain([0, d3.max(dataStackLayout[dataStackLayout.length - 1],
      function(d) {
        return d.y0 + d.y;
      })]).nice();

    var layer = gBar.selectAll(".stack")
      .data(dataStackLayout);

    layer.enter().append("g")
      .attr("class", "stack")
      .style("fill", function(d, i) {
        return i == 0 ? '#b4d5c3' : '#ecaeb3';
      });

    var bars = layer.selectAll("rect")
      .data(function(d) {
        return d;
      });

    bars.exit()
      .transition()
      .delay(function(d, i) {
        return 30 * i;
      })
      .duration(1500)
      .attr("y", y(0))
      .attr("height", heightB - y(0))
      .style('fill-opacity', 1e-6)
      .remove();

    bars.enter().append("rect")
      .attr("x", firstTime ? null : widthB);

    bars.transition()
      .delay(function(d, i) {
        return 30 * i;
      })
      .duration(1500)
      .attr("x", function(d) {
        return x(d.x);
      })
      .attr("y", function(d) {
        return y(d.y + d.y0);
      })
      .attr("height", function(d) {
        return y(d.y0) - y(d.y + d.y0);
      })
      .attr("width", x.rangeBand())
      .style("fill-opacity", 1);

    gX.call(xAxis)
      .selectAll("text")
      .style('font-size', '14px')
      .style('font-family', 'Alegreya')
      .style("text-anchor", "end")
      .attr("dx", "-0.40em")
      .attr("dy", ".10em")
      .attr("transform", function(d) {
        return "rotate(-65)"
      });

    gY.transition().duration(300).call(yAxis)
      .selectAll("text")
      .style('font-size', '16px')
      .style('font-family', 'Alegreya');

  } else {

    x.domain(points.map(function(d) {
      return d.name;
    }));

    y.domain([0, 1024]).nice();

    // this part 
    var bars = gBar.selectAll(".stack").selectAll("rect")
      .data(points);

    bars.exit()
      .transition()
      .delay(function(d, i) {
        return 30 * i;
      })
      .duration(1400)
      .attr("y", y(0))
      .attr("x", widthB)
      .attr("height", heightB - y(0))
      .style('fill-opacity', 1e-6)
      .remove();

    bars.enter().append("rect")
      .transition()
      .delay(function(d, i) {
        return 30 * i;
      })
      .duration(3000)
      .attr("x", function(d) {
        return x(d.name);
      })
      .attr("y", function(d) {
        return y(d.GTF);

      })
      .attr("height", function(d) {
        return heightB - y(d.GTF);;

      })
      .attr("width", x.rangeBand());

    // the "UPDATE" set:
    bars.transition().delay(function(d, i) {
        return 30 * i;
      }).duration(1500).attr("x", function(d) {
        return x(d.name);
      }) // (d) is one item from the data array, x is the scale object from above
      .attr("width", x.rangeBand()) // constant, so no callback function(d) here
      .attr("y", function(d) {
        return y(d.GTF);
      })
      .attr("height", function(d) {
        return heightB - y(d.GTF);
      });

    gX.call(xAxis);

    gY.transition().duration(300).call(yAxis);

  }

}

draw(layout);

d3.select("#click").on('click', function() {
  firstTime = false;
  layout = layout === "stack" ? "bar" : "stack"
  draw(layout);
});
body {
  font-family: 'Alegreya', serif;
}

.axis text {
  font: 10px sans-serif;
}

.axis path {
  fill: none;
  stroke: #000;
  stroke-width: 0px;
  shape-rendering: crispEdges;
}

.axis line {
  fill: none;
  stroke: #000;
  stroke-width: 0.5px;
  shape-rendering: crispEdges;
}

.x.axis path {
  display: none;
}
<script src="https://d3js.org/d3.v3.min.js"></script>
<button id="click"> click here to change </button>
<div id="chart"></div>

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.