2
\$\begingroup\$

I've created 3 different charts all drawn from the same dataset, my reasoning for building it this way is so that it'll be easier to add and remove additional rows of charts.

What I'm mainly interested in knowing is if the code is well structured and/or if it's idiomatic d3 code.

Here's a link to all the code: Plunker

And here's the same code but with hardcoded data:

var durations = 0;

var z = d3.scaleOrdinal()
  .range(["orange", "steelblue"]);

var margin = {
  top: 35, right: 35, bottom: 35, left: 35, pad: 25
};

var csvData = 
`AgeRange,female,male,Woman_one,Man_one,Woman_two,Man_two,Woman_three,Man_three
"17 - 19",50,36,23,22,3,0,5,5
"20 - 24",145,99,80,72,22,3,27,14
"25 - 29",123,109,40,80,28,3,42,22
"30 - 34",121,52,54,35,21,2,32,13
"35 - 39",88,65,23,30,15,4,44,28
"40 - 44",79,52,28,22,8,4,40,23
"45 - 49",89,51,21,27,14,1,47,20
"50 - 54",67,31,15,12,10,1,38,15
"55 - 59",55,25,7,3,7,3,39,17
"60 - 64",40,21,5,5,4,2,30,14
"65 - 69",26,11,1,0,1,0,22,11
"70 - 74",10,6,0,0,0,0,9,5
"75 +",9,1,0,0,0,0,9,1`;

var dataSet = d3.csvParse(csvData)

row1(dataSet);

function row1(data) {

  var check = false;

  update(check);

  function update(check) {

    var team = d3.selectAll(".select1").property("value")

    data.forEach(function(d, i, columns) {
      d.male = +(d.male);
      d.female = +(d.female);
      d["Woman" + team] = +d["Woman" + team];
      d["Man" + team] = +d["Man" + team];
      return d;
    })

    if (check) {
      pyramid.update(data, team)
      percent.update(data, team)
      totals.update(data, team)
    } else {
      pyramid(data, team);
      percent(data, team);
      totals(data, team);
    }
  }

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

}

function pyramid(data, team) {

  var width  = 550 - (margin.left + margin.right);
  var height = 420 - (margin.top + margin.bottom);

  var x = d3.scaleLinear()
        .rangeRound([(width/2) + margin.pad, width]),
      x2 = d3.scaleLinear()
        .rangeRound([(width/2) - margin.pad, 0]),
      y = d3.scaleBand()
        .rangeRound([height, 0]).padding(0.1);

  var xAxis = d3.axisBottom(x).ticks(6)
        .tickSize(-height),
      xAxis2 = d3.axisBottom(x2).ticks(6)
        .tickSize(-height),
      yAxis = d3.axisLeft(y);

  var svg = d3.select("#row1").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 + ")");

  svg.append("g")
      .attr("class", "axis axis--x hide")
      .attr("transform", "translate(0," + height + ")")
  svg.append("g")
      .attr("class", "axis axis--x2 hide")
      .attr("transform", "translate(0," + height + ")")
  svg.append("g")
      .attr("class", "axis axis--y")
      .attr("transform", "translate(" + (width/2 + 10) + ",0)")

  svg.append("text")
      .attr("x", width/2)
      .attr("y",  0)
      .attr("text-anchor", "middle")
      .text("Age");

  update(data, team);

  function update(data, team) {

    x.domain([0, d3.max(data, 
      d => Math.max(d.male, d.female))
    ]).nice();

    x2.domain(x.domain())

    y.domain(data.map(d => d.AgeRange));
    
    svg.selectAll(".axis.axis--x")
      .call(xAxis);

    svg.selectAll(".axis.axis--x2")
      .call(xAxis2);

    svg.selectAll(".axis.axis--y")
      .call(customYAxis);

    function customYAxis(g) {
      g.call(yAxis);
      g.selectAll("text").style("text-anchor", "middle")
      g.select(".domain").remove();
      g.selectAll("line").remove();
    }

    // ==== Men bar ====

    var menTtl = svg.selectAll(".menTtl")
      .data(data).enter()
    .insert("g", ".axis--x")
    .append("rect")
      .attr("class", "M menTtl")
      .attr("opacity",.6)
      .attr("x", x(0))
      .attr("y", d => y(d.AgeRange))
      .attr("height", y.bandwidth())
      .attr("width", d => Math.abs(x(d.male) - x(0)));
    
    var menTeam = svg.selectAll(".menTeam")
      .data(data);

    menTeam = menTeam
        .enter()
      .insert("g", ".axis--x")
      .append("rect")
        .attr("class", "M menTeam")
        .attr("x", x(0))
        .attr("y", d => y(d.AgeRange))
        .attr("height", y.bandwidth())
        .merge(menTeam)
    
    menTeam.transition().duration(durations)
        .attr("width", d => Math.abs(x(d["Man" + team]) - x(0)));

    // ==== Women bar ====
    
    var womenTtl = svg.selectAll(".womenTtl")
      .data(data).enter()
    .append("rect")
      .attr("class", "W womenTtl")
      .attr("opacity",.6)
      .attr("y", d => y(d.AgeRange))
      .attr("x", d => x2(d.female))
      .attr("height", y.bandwidth())
      .attr("width", d => Math.abs(x2(d.female) - x2(0)));
    
    var womenTeam = svg.selectAll(".womenTeam")
      .data(data);

    womenTeam = womenTeam
      .enter()
    .append("rect")
      .attr("class", "W womenTeam")
      .attr("y", d => y(d.AgeRange))
      .attr("height", y.bandwidth())
      .merge(womenTeam)
    
    womenTeam.transition().duration(durations)
      .attr("x", d => x2(d["Woman" + team]))
      .attr("width", d => Math.abs(x2(d["Woman" + team]) - x2(0)));
  }

  pyramid.update = update;

}

function percent(data, team) {

  var width  = 150 - (margin.left + margin.right),
      height = 420 - (margin.top + margin.bottom);

  var x0 = d3.scaleBand().rangeRound([0, width]),
      x1 = d3.scaleBand().paddingOuter(0.2),
      x2 = d3.scaleBand().paddingOuter(0.2),
      y = d3.scaleLinear().rangeRound([height, 0]);
    
  var xAxis = d3.axisBottom(x0),
      yAxis = d3.axisLeft(y);
    
  var svg = d3.select("#row1").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 + ")");

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

  update(data, team);

  function update(data, team) {

    var ttlKey = data.columns.slice(1, 3);

    var teamKey = ["Woman" + team, "Man" + team]

    x0.domain(["Women & Men"]);
    x1.domain(ttlKey).rangeRound([0, x0.bandwidth()]);
    x2.domain(teamKey).rangeRound([0, x0.bandwidth()]);

    y.domain([0, d3.sum(data, function(d) {
      return d3.max(ttlKey, key => d[key]); 
    })]).nice();
    
    svg.selectAll(".axis.axis--y")
      .call(yAxis);
    
    svg.selectAll(".axis.axis--x")
      .call(xAxis);

    var barGroups = svg.selectAll("g.layer")
      .data(["empty"]);

    barGroups.exit().remove();

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

    var bars = svg.selectAll("g.layer").selectAll(".bars")
      .data(function() { 
        return ttlKey.map(function(key) {
          var sum = d3.sum(data, e => e[key] )
          return {key: key, value: sum};
        }); 
      }).enter()
    .append("rect")
      .attr("class", "bars")
      .attr("fill", d => z(d.key))
      .attr("opacity", .6)
      .attr("x", d => x1(d.key))
      .attr("y", d => y(d.value))
      .attr("width", x1.bandwidth())
      .attr("height", d => height - y(d.value));

    var barsTeam = svg.selectAll("g.layer").selectAll(".barsTeam")
      .data(function() { 
        return teamKey.map(function(key) {
          var sum = d3.sum(data, e => e[key] )
          return {key: key, value: sum};
        }); 
      });

    barsTeam.exit().remove();

    barsTeam = barsTeam
      .enter()
    .append("rect") 
      .attr("class", "barsTeam")
      .attr("fill", d => z(d.key))
      .attr("x", d => x2(d.key))
      .attr("width", x1.bandwidth())
      .merge(barsTeam);

    barsTeam.transition().duration(durations)
      .attr("y", d => y(d.value))
      .attr("height", d => height - y(d.value));
  }

  percent.update = update;

}

function totals(data, team) {

  var width = 110 - (margin.left + margin.right),
      height = 420 - (margin.top + margin.bottom);
  
  var x = d3.scaleBand().rangeRound([0, width]).padding(0.1),
      y = d3.scaleLinear().rangeRound([height, 0]);
  
  var xAxis = d3.axisBottom(x),
      yAxis = d3.axisLeft(y);
  
  var svg = d3.select("#row1").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 + ")");
    
  svg.append("g")
      .attr("class", "axis axis--x")
      .attr("transform", "translate(0," + height + ")");
  
  svg.append("g")
      .attr("class", "axis axis--y");

  update(data, team);

  function update(data, team) {

    var sum = d3.sum(data, d => d3.sum([d.male, d.female]));

    var sumTeam = d3.sum(data, function(d) {
      return d3.sum([d["Woman" + team], d["Man" + team]])
    });

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

    x.domain(["Total"]);
    
    svg.selectAll(".axis.axis--y")
      .call(yAxis);
    
    svg.selectAll(".axis.axis--x")
      .call(xAxis);

    var ttlBar = svg.selectAll(".ttlBar")
      .data(["empty"]).enter()
    .append("rect")
      .attr("class", "ttlBar")
      .attr("fill", "#ccc")
      .attr("x", x(["Total"]))
      .attr("y", y(sum))
      .attr("width", x.bandwidth())
      .attr("height", height - y(sum));

    var ttlTeam = svg.selectAll(".ttlTeam")
      .data(["empty"]);
      
    ttlTeam = ttlTeam
      .enter()
    .append("rect")
      .attr("class", "ttlTeam")
      .attr("fill", "#999")
      .attr("x", x(["Total"]))
      .attr("width", x.bandwidth())
      .merge(ttlTeam);

    ttlTeam.transition().duration(durations)
      .attr("height", height - y(sumTeam))
      .attr("y", y(sumTeam))

  }

  totals.update = update

}
body {
  font: 12px arial;
  margin: auto;
  width: 850px;
  padding-top:25px;
}
select {
  border: 1px solid #fff;
  border-bottom: 1px solid #ccc;
  cursor: pointer;
}
.M { fill: steelblue; }
.W { fill: orange; }
.hide path { 
  display: none; 
}
.hide .tick:not(:first-of-type) line {
  opacity: .25;
}
<meta charset="utf-8">
<script src="https://d3js.org/d3.v5.min.js"></script>
<div style="margin-left: 25px;">
  <b>Choose Team:</b>
  <select class="select1">
    <option value="_one">Team 1</option>
    <option value="_two">Team 2</option>
    <option value="_three">Team 3</option>
  </select>
</div>

<div id="row1"></div>

\$\endgroup\$
1
\$\begingroup\$

My answer here will focus on refactoring the row1 function. Right now that's an awkward function, with some unnecessary logic.

Let's examine the it:

function row1(data) {

  var check = false;

  update(check);

  function update(check) {

    var team = d3.selectAll(".select1").property("value")

    data.forEach(function(d, i, columns) {
      d.male = +(d.male);
      d.female = +(d.female);
      d["Woman" + team] = +d["Woman" + team];
      d["Man" + team] = +d["Man" + team];
      return d;
    })

    if (check) {
      pyramid.update(data, team)
      percent.update(data, team)
      totals.update(data, team)
    } else {
      pyramid(data, team);
      percent(data, team);
      totals(data, team);
    }
  }

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

}

In the linked Plunker, which uses a real CSV, you get the result of the promise and pass it to the row1 function. So far, so good.

However, inside that function, you're calling another function, named update, which gets the selected value of a <select> element. Then, based on that option, you're coercing only some values of the data array to numbers, but keeping the whole structure of the data array (you're not filtering or anything like that).

The forEach inside update is also strange: you don't need to return the object row. Also, the third argument, columns, is probably a reminiscent of the third parameter in the d3.csv: in a forEach method the third argument is not the array of headers.

Continuing, you're using a check variable (which is always true after the user changes the select) to choose what functions to call, and finally setting a listener to the select that never listens to the value of the select! It just calls update again...

This is my proposition to simplify this row1 function:

function row1(data) {

    data.forEach(function(d) {
        for (var key in d) {
            if (key !== "AgeRange") {
                d[key] = +d[key]
            }
        }
    });

    pyramid(data, "_one");
    percent(data, "_two");
    totals(data, "_three");

    d3.selectAll(".select1").on("change", function() {
        durations = 750;
        pyramid.update(data, this.value)
        percent.update(data, this.value)
        totals.update(data, this.value)
    });

}

These are the changes:

  1. The forEach coerces all the values, except for AgeRange;
  2. You call the drawing functions with the data and the teams;
  3. Since you know what values you're using for the first time, I just hardcoded them. If you don't like that just use another selection to get the selected option (again, hardcoded!)
  4. You use the listener to get the value of the select with this.value, and call the update functions.

Also, if you put the drawing functions inside row1, you don't even need to pass the data to them (since the data never changes).

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

\$\endgroup\$

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.