1
\$\begingroup\$

In this Plunker (Will post all code below) I'm using d3.dispatch to handle all updates of the charts.

What I want to know is if I'm using the dispatch events properly or as it was "intended" to be used, I could not find many examples using this particular method which is why I'm unsure of whether I'm using it correctly. Or just general code review.

<!DOCTYPE html>
<head>
<meta charset="utf-8">
<script src="//d3js.org/d3.v5.min.js"></script>
<style>
/* --- Global --- */
body {
  margin:auto;
  width: 850px;
  font: 10px arial;
  padding: 25px;
  color:#333;
}
/* --- Chart --- */
.grid--y path,
.grid--y text {
  display: none;
}
.grid--y line {
  opacity:.15;
}
.axis--xS path {
  display: none;
}
.grid--yS path,
.grid--yS text {
  display: none;
}
.grid--yS line {
  opacity:.15;
}

</style>
</head>
<body>

<!-- Selection Category -->
<b>Välj Uteblivande</b>

<select id="category" class="options">
  <option value="AA">All</option>
  <option value="1U">1 Unit</option>
</select> 

<!-- Selection New dataset -->
<span style="margin-left:30px;">
<b>Choose Year</b>

<select id="year" class="options">
  <option value="2017">2017</option>
  <option value="2016">2016</option>
</select>
</span>

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

<script>

var durations = 0,
    formatValue = d3.format(".2s"),
    formatPercent = d3.format(",.0f");

var teamColor = d3.scaleOrdinal()
  .range(["steelblue","darkorange", "lightblue"]);

var dispatch = d3.dispatch("load", "update");

var files = ["data1.csv", "data2.csv"];

Promise.all(files.map(url => d3.csv(url))).then(function(dataSet) {

  // === Load all data ===

  var data1 = dataSet[0];
  var data2 = dataSet[1];

  dispatch.call("load", this);

  // === Update data ===

  update();

  function update() {

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

    var data = d3.select('#year')
      .property('value') == '2017' ? data1 : data2;  

    data.forEach(function(d, i, columns) {
      for (var i = 1, ttl = 0, n = columns.length; i < n; ++i) 
        ttl += d[columns[i]] = +d[columns[i]];
        d.total = ttl;
        d.sliceTotal = d3.sum([
          d["Team 1 " + input], 
          d["Team 2 " + input],
          d["Team 3 " + input]
        ]);
      return d;
    })

    dispatch.call("update", this, data, input);

  }

  // === Event handler ===

  d3.selectAll(".options").on("change", function() {
    durations = 750;
    update();
  })

});

dispatch.on("load.occupation", function() {

  let margin = {top: 35, right: 45, bottom: 35, left: 45},
      width = 540 - margin.left - margin.right,
      height = 420 - margin.top - margin.bottom;

  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 + ")");

  let x0 = d3.scaleBand()
    .rangeRound([0, width])
    .paddingInner(0.1)
    .padding(0.1);

  let x1 = d3.scaleBand();

  let y = d3.scaleLinear()
    .rangeRound([height, 0]);

  let xAxis = d3.axisBottom(x0),
      yAxis = d3.axisLeft(y).ticks(null, "s");
      yGrid = d3.axisLeft(y).tickSize(-width);

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

  g.append("g")
      .attr("class", "axis axis--y");

  g.append("g")
      .attr("class", "axis grid--y");

  dispatch.on("update.occupation", function(dataInit, input) {

    var keys = dataInit.columns.slice(1, 4);

    let copy = [];

      keys.forEach(function(t) {
        t = t.slice(0, -2) 
        copy.push(t) 
      })

    var data = dataInit.filter(function(d) {
      return d.State !== "ALL"
    })

    let sumOfTeam = d3.sum(data, d=> d3.sum(keys, key=> d[key]));

    y.domain([0, d3.max(data, function(d) {
      return d3.max(keys, function(key) {
        return d.sliceTotal;
        }); 
      })
    ]).nice();

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

    // bars
    let barGroups = g.selectAll("g.layer").data(data);
    barGroups.enter().append("g")
      .classed('layer', true);

    barGroups.exit().remove();

    // bars
    let barGroups2 = g.selectAll("g.layer2").data(data);
    barGroups2.enter().append("g")
      .classed('layer2', true);

    barGroups2.exit().remove();

    // xDomains
    x0.domain(data.map(function(d) { return d.State; }));
    x1.domain(keys).rangeRound([0, x0.bandwidth()]);

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

    g.selectAll("g.layer").transition().duration(durations)
      .attr("transform", function(d, i) {
        return "translate(" + x0(d.State) + ",0)";
      });

    let bars = g.selectAll("g.layer").selectAll(".bars")
      .data(function(d) { 
        return copy.map(function(key) { // Return copy 
          return { key: key+input, value: d[key+input] }; // Add input
        }); 
      });

    bars = bars
      .enter()
    .append("rect")
      .attr("class", "bars")
      .attr("fill", function(d) { return teamColor(d.key); })
      .attr("width", x1.bandwidth())
      .attr("x", function(d) { return x1(d.key); })
      .merge(bars)

    bars.transition().duration(durations)
      .attr("y", function(d) { return y(d["value"]); })
      .attr("height", function(d) { return height - y(d["value"]); });

    bars.exit().remove();

    g.selectAll("g.layer2").transition().duration(durations)
      .attr("transform", function(d, i) {
        return "translate(" + x0(d.State) + ",0)";
      });

    let barsTotal = g.selectAll(".test")
      .data(data, function(d){
        return d.State;
      });

    barsTotal = barsTotal
      .enter()
    .append("rect")
      .attr("fill", "none")
      .attr("stroke","#999")
      .attr("stroke-width","1px")
      .attr("class","test")
      .attr("width", x0.bandwidth())
      .merge(barsTotal);

    barsTotal.transition().duration(durations)
      .attr("x", function(d) { 
        return x0(d.State); 
      })
      .attr("y", function(d) { 
        return y(d.sliceTotal); 
      })
      .attr("height", function(d) { 
        return height - y(d.sliceTotal); 
      });

    barsTotal.exit().remove();

  });

});

dispatch.on("load.all", function() { 

  let margin = {top: 35, right: 45, bottom: 35, left: 45},
      width = 150 - margin.left - margin.right,
      height = 420 - margin.top - margin.bottom;

  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 + ")");

  var x = d3.scaleBand()
    .rangeRound([0, width])
    .paddingInner(0.1)
    .padding(0.1);

  var y = d3.scaleLinear()
    .rangeRound([height, 0]);

  let xAxis = d3.axisBottom(x),
      yAxis = d3.axisLeft(y)
        .ticks(null, "s");

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

  g.append("g")
      .attr("class", "axis axis--y");

  dispatch.on("update.all", function(dataInit, input) {

    var keys = dataInit.columns.slice(1, 4);

    var copy = []

      keys.forEach(function(s){
        s = s.slice(0, -2)
        copy.push(s)
      })

    let combined = [];

      copy.forEach(function(t){
        t = t + input
        combined.push(t)
      })

    var data = dataInit.filter(function(d) {
      return d.State == "ALL"
    })

    y.domain([0, d3.max(data, function(d) {
      return d3.sum(copy, function(key) {
        return d[key + input];
        }); 
      })
    ]).nice();

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

    teamColor.domain(combined);

    x.domain(data.map(function(d) { return d.State; }));

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

    var stacking = d3.stack().keys(combined)(data)

    let barGroups = g.selectAll("g.layer")
      .data(stacking,function(d){
        return d.key.split(' ')[0] + d.key.split(' ')[1]
      });

    barGroups
      .enter()
    .append("g")
      .classed('layer', true);

    barGroups.exit().remove();

    g.selectAll("g.layer").transition().duration(durations)
      .attr("fill", function(d) { return teamColor(d.key); });

    let bars = g.selectAll("g.layer").selectAll("rect")
      .data(function(d) { return d; }, d => d.data.State);

    bars = bars
      .enter()
    .append("rect")
      .attr("width", x.bandwidth())
      .merge(bars);

    bars.transition().duration(durations)
      .attr("x", function(d) { return x(d.data.State); })
      .attr("y", function(d) { return y(d[1]); })
      .attr("height", function(d) { return y(d[0]) - y(d[1]); });

  });

});

</script>
</body>
\$\endgroup\$
1
\$\begingroup\$

While you can do it using d3.dispatch, as you just found out, it doesn't seem to be a good practice:

  1. It's not necessary, you can do the same using regular functions (see below). The main reason why it is not needed is that, despite the fact that you have two events, load and update, you just need one, since you already have the necessary enter, update and exit selections, and their proper transitions.

  2. It's not idiomatic and adds needless complications to your code. Because it's not idiomatic, it will make harder to other D3 programmers trying to understand/maintain your code in the future.

  3. It creates a lot of duplications (in my solution below I saved you 100 lines of code, going from 379 to 279 lines).

That being said, my proposed solution is: use regular functions.

So, in your code, I put all the drawing parts inside a draw function...

function draw(data, input) {

... which is called by the update:

function update() {

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

    var data = d3.select('#year')
        .property('value') == '2017' ? data1 : data2;

    data.forEach(function(d, i, columns) {
        for (var i = 1, ttl = 0, n = columns.length; i < n; ++i)
            ttl += d[columns[i]] = +d[columns[i]];
        d.total = ttl;
        d.sliceTotal = d3.sum([
            d["Team 1 " + input],
            d["Team 2 " + input],
            d["Team 3 " + input]
        ]);
        return d;
    })

    draw(data, input)
}

After that, I removed all the parts that don't change and that you need to paint just once from draw, like the SVG and the axes selections.

Last but not least, good on you for using the brand new D3 v5 and Promise.all!

Here is the updated Plunker: https://plnkr.co/edit/t17PoC4dl2aSOhn2Dxz6?p=preview

\$\endgroup\$
  • \$\begingroup\$ Thanks a lot! Although I like your solution, the reason I had my code setup like that (having margin, width etc not being global) is so that if I wanted to add another chart I wouldn't have to worry as much about naming variables, that might be a bad reason though. \$\endgroup\$ – Robert Andersson Apr 8 '18 at 8:49
  • \$\begingroup\$ In that case nest two functions, and outer drawn and an inner update or whatever name you want. So the variables are confined inside draw. \$\endgroup\$ – Gerardo Furtado Apr 8 '18 at 8:52
  • \$\begingroup\$ You don't happen to have an example of this? Because I tried doing it with functions at first but was unable to call the nested function from my event handler without also calling the function surrounding the update function \$\endgroup\$ – Robert Andersson Apr 8 '18 at 14:21
  • \$\begingroup\$ Well, it's complicated to say something without seeing the real code and what you plan to do. You can always post a new question when you have a more complete version of it. \$\endgroup\$ – Gerardo Furtado Apr 8 '18 at 16:05
  • 1
    \$\begingroup\$ This is what I meant, found a solution Plunker seems like everything works as intended, thanks for your help \$\endgroup\$ – Robert Andersson Apr 8 '18 at 18:47

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.