htmld3.jsvisualizationgrouped-bar-chart

D3.js - Grouped Bar Chart - Error Updating Input Data


I'm trying to use a grouped bar chart to show the performance of responses to test questions. I have created two .svg files with results which my visualisation should be able to switch between. I can load and visualise the first set of data without issue but when I select the second set, new bars are created overtop of the old bars which are not removed.

Here's what I've got so far:

<script>
    // There's a lot of this script here: https://d3-graph-gallery.com/graph/barplot_grouped_basicWide.html

// set the dimensions and margins of the graph
const margin = {top: 10, right: 30, bottom: 20, left: 50},
    width = 460 - margin.left - margin.right,
    height = 400 - margin.top - margin.bottom;

// append the svg object to the body of the page
let svg = d3.select("#my_dataviz")
  .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})`);

    function update (test_no){

// Reading Data from CSV
d3.csv(`bardisect_${test_no}.csv`).then( function(data) {

  // Creates the subgroups using group names
  let subgroups = data.columns.slice(1)

  // Making a map of the subgroups for the x axis
  let groups = data.map(d => d.groups)

  // Add X axis
  let x = d3.scaleBand()
      .domain(groups)
      .range([0, width])
      .padding([0.2])
  svg.append("g")
    .attr("transform", `translate(0, ${height})`)
    .call(d3.axisBottom(x).tickSize(0));

  // Add Y axis
  let y = d3.scaleLinear()
    .domain([0, 100])
    .range([ height, 0 ]);
  svg.append("g")
    .call(d3.axisLeft(y));

  // creating separate x axis for subgroups 
  let xSubgroup = d3.scaleBand()
    .domain(subgroups)
    .range([0, x.bandwidth()])
    .padding([0.05])

  // colours for the subgroups
  let color = d3.scaleOrdinal()
    .domain(subgroups)
    .range(['green','red','grey'])


  // Show the bars
  svg.append("g")
    .selectAll("g")

    // Looping through each group to shoow the right numbers
    .data(data)
    .join(
    enter => {
        enter
        let sel = enter
            .append("g")
            .attr("transform", d => `translate(${x(d.groups)}, 0)`)
      return sel;
    })
    .selectAll("rect")
    .data((d) => { return subgroups.map(function(key) { return {key: key, value: d[key]}; }); })
    .join(
      (enter) => {
         enter
            .append("rect")
            .attr('fill', 'white')
            .attr("x", d => xSubgroup(d.key))
            .attr("y", d => y(d.value))
            .attr("width", xSubgroup.bandwidth())
            .attr('height', 0)
            .transition()
            .duration(1000)
            .attr("height", d => height - y(d.value))
            .attr("fill", d => color(d.key))
          },
        (update) => {
          update
            .transition()
            .duration(1000)
            .attr("fill", d => color(d.key))
            .attr("x", d => xSubgroup(d.key))
            .attr("width", xSubgroup.bandwidth())
            .attr("height", d => height - y(d.value))
            .attr("y", d => y(d.value))
        },
        (exit) => {
          exit
          .transition()
          .duration(1000)
          .attr('height', 0)
          .remove();
        }
  )
    });

}


//updating the test selection 
let select = d3.select('#test_no');
select.on('change', function() {
    console.log(this.value);
    update(this.value);
})

update('02');

</script>

I'm pretty new to D3 and have been playing about with update, exit and .remove(), etc. but with no luck! Any advice would be greatly appreciated!


Solution

  • Normally, if you have a function that gets called often, seeing append should set off red flags (unless the append is only on the enter selection).

    In your code, every time update is run, the svg appends a new group element for the x axis. Instead, it should select the old x-axis, and update it.

      svg.selectAll("g.my-x-axis").data([null]).join('g')
        .attr("class", "my-x-axis")
        .attr("transform", `translate(0, ${height})`)
        .call(d3.axisBottom(x).tickSize(0));
    

    This will select the old x-axis group (if it exists), or create one if none exists. Our empty data array [null] only contains one null element, so only one x-axis will be created.

    The same can be said for your code that adds the rectangles. You do join the data with the enter/update/enter pattern, but only enter is ever called. This is because before enter/update/exit, you add an empty group to your svg with svg.append("g").selectAll("g"). Because you are always appending an empty group, it will never have anything to update. Therefore, it is creating new groups, entering the data, and these groups are stacking on top of each other. Instead, you should create a group once, and select it the next time (like I showed above for the x-axis).