4
\$\begingroup\$

In this example (will post all code below) I'm drawing two different charts using the same csv data.

What I basically want to know is if there's anything I'm doing "wrong" here or if there's a more efficient way of drawing the two charts in this way.

Here's all the code:

<!DOCTYPE html>
<html>
<head>
<meta charset ="utf-8">
<script src="//d3js.org/d3.v4.min.js"></script>

<style>

body {
  margin:auto;
  width:850px;
  font:10px arial;
  padding:25px;
}
select {
  -moz-appearance: none;
  -webkit-appearance:none;
  border:none;
  border-bottom: 1px solid #333;
  padding:3px;
  font:10px arial;
  font-weight: bold;
}

</style>
</head>

<body>

Select Category: 
<select id="category">
  <option value=" 1">Category 1</option>
  <option value=" 2">Category 2</option>
</select> 

<!-- Chart -->
<div id="chart"></div>

<script>

let formatValue = d3.format(",.0f");

let margin = {top: 35, right: 35, bottom: 35, left: 35},
    width = 550 - margin.left - margin.right,
    height = 400 - margin.top - margin.bottom;

let durations = 0;

let afterLoad = () => durations = 750;

// ===== Monthly bars init values =====

let x = d3.scaleBand().rangeRound([0, width]).padding(0.1),
    y = d3.scaleLinear().rangeRound([height, 0]);

let xAxis = d3.axisBottom(x),
    yAxis = d3.axisLeft(y)

let g = d3.select("#chart").append("svg")
    .attr("width", width + margin.left + margin.right)
    .attr("height", height + margin.top + margin.bottom)
  .append("g")
    .attr("transform","translate(" + margin.left + "," + margin.top + ")");

g.append("g")
    .attr("class", "axis axis--x")
    .attr("transform", "translate(0," + height + ")");
g.append("g")
    .attr("class", "axis axis--y");

// ===== Total bar init values =====

let totalG = d3.select("#chart").append("svg")
    .attr("width", (width/4) + margin.left + margin.right)
    .attr("height", height + margin.top + margin.bottom)
  .append("g")
    .attr("transform","translate(" + margin.left + "," + margin.top + ")");

let xTotal = d3.scaleBand().rangeRound([0, (width/4)]).padding(0.1),
    yTotal = d3.scaleLinear().rangeRound([height, 0]);

let xAxisTotal = d3.axisBottom(xTotal),
    yAxisTotal = d3.axisLeft(yTotal);

totalG.append("g")
    .attr("class", "axis axis--xTotal")
    .attr("transform", "translate(0," + height + ")");
totalG.append("g")
    .attr("class", "axis axis--yTotal");

d3.queue()
    .defer(d3.csv, "data.csv", d => d)
    .await(ready);

function ready(error, data) {

  if (error) throw error;

  var INT;

  // Event handlers
  d3.select("#category").on('change', update);

  update();

  function update() {

    INT = d3.select('#category').property('value');

    var totalSum = d3.sum(data, d => d["Category"  + INT])

    // ========= Monthly bars =========

    y.domain([0, d3.max(data, d => d["Category" + INT]) ]).nice();

    x.domain(data.map( d => d.month ));

    g.selectAll(".axis.axis--y").transition()
      .duration(durations)
      .call(yAxis);

    g.selectAll(".axis.axis--x").transition()
      .duration(durations)
      .call(xAxis);

    let bars = g.selectAll(".barEnter")
      .data(data, d => d.month);

    bars = bars
      .enter()
    .append("rect")
      .attr("class", "barEnter")
      .attr("fill","steelblue")
      .attr("x", d => x(d.month))
      .attr("width", x.bandwidth())
      .merge(bars);

    bars.transition()
      .duration(durations)
      .attr("y", d => y(d["Category" + INT]))
      .attr("height", d => height - y(d["Category" + INT]))

    bars.exit().remove();

    // ========= Total bar =========

    yTotal.domain([0, totalSum]).nice();

    xTotal.domain(["Total"]);

    totalG.selectAll(".axis.axis--yTotal").transition()
      .duration(durations)
      .call(yAxisTotal);

    totalG.selectAll(".axis.axis--xTotal").transition()
      .duration(durations)
      .call(xAxisTotal);

    let totalBar = totalG.selectAll(".totalBar")
      .data(["empty"]); 

    totalBar = totalBar
      .enter()
    .append("rect")
      .attr("class", "totalBar")
      .attr("fill","#ccc")
      .attr("x", xTotal(["Total"]) )
      .attr("width", xTotal.bandwidth())
      .merge(totalBar)

    totalBar.transition()
      .duration(durations)
      .attr("y", yTotal(totalSum))
      .attr("height", height - yTotal(totalSum))

  } // End of update

  afterLoad()

} // End of ready

</script>

</body>
</html> 
\$\endgroup\$
2
\$\begingroup\$

Overall your code seems good, it's an idiomatic D3 and it doesn't have any major problem (like appending elements in a loop). I would put the two charts in a single SVG with different <g> elements, but that's a matter of personal choice (having two SVGs can be an advantage if you want, for instance, rearrange them in the HTML).

I'd suggest 3 changes, though:

  1. You have just 1 CSV file. Therefore, you can drop the d3.queue, which is normally used to deal with several (that is, more than one) asynchronous tasks. Use a simple d3.csv instead:

    d3.csv("data.csv", function (error, data) {
        //etc...
    

    Edit: in the new D3 version 5, d3.csv uses promises. Therefore, the above snippet would be:

    d3.csv("data.csv").then(function (data) {
        //etc...
    
  2. Don't use variables external to the function to define the behaviour of that function: it's hard to understand where the value comes from and harder to debug.

    Instead of that, pass those variables as arguments. In the case of your update function, pass the value of the dropdown:

    var category = d3.select('#category').property('value');
    
    d3.select("#category").on('change', function(){
        update(this.value)
    });
    
    update(category);
    
    function update(INT) {
        //etc...
    }
    
  3. You don't need that afterLoad function... just change durations in the event handler!

    d3.select("#category").on('change', function(){
        durations = 750;
        update(this.value)
    });
    

Here is the updated Plunker with those changes: https://plnkr.co/edit/P5r3D7JRbhB9yv8dmwzg?p=preview

\$\endgroup\$
  • \$\begingroup\$ Thank you so much! One small question though, in most of Mike Bostocks examples using csv he uses a type function and returns d, and there is no such function in your answer, in what scenarios should you use this function? \$\endgroup\$ – Robert Andersson Jan 29 '18 at 8:47
  • 1
    \$\begingroup\$ d3.csv always loads the values as strings, even if they are numbers. The row function is normally used to coercion (but not only). \$\endgroup\$ – Gerardo Furtado Jan 29 '18 at 9:09

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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