Home > Software design >  Unable to supdate chart when switching dataset on button click
Unable to supdate chart when switching dataset on button click

Time:11-04

The first time I load data, the graph draws correctly, but when I load a different data set, the graph remains unchanged.

I switch between datasets using buttons. The first click always draws the graph correctly, no matter what button I click. But I can't update the graph after it is drawn by clicking on the other button. Any help is very much appreciated,thank you!

const dataA = [
  { population: 50, size: 100 },
  { population: 100, size: 100 },
];

const dataB = [
  { money: 4, currency: "usd" },
  { money: 10, currency: "eur" },
];

function drawChart(dataSet, prop) {
  let width = 900;
  let height = 200;
  let x = d3.scale.ordinal().rangeRoundBands([0, width], 0.9);
  let y = d3.scale
    .linear()
    .domain([dataSet[0][prop] - 39, dataSet[dataSet.length - 1][prop]])
    .range([height, 0]);
  let chart = d3.select("#chart").attr("width", width).attr("height", height);
  let barWidth = width / dataSet.length;
  let div = d3.select("body").append("div").attr("class", "tooltip");
  let bar = chart
    .selectAll("g")
    .data(dataSet)
    .enter()
    .append("g")
    .attr("transform", function (d, i) {
      return "translate("   i * barWidth   ",0)";
    });
  bar
    .append("rect")
    .attr("y", function (d) {
      return y(d[prop]);
    })
    .attr("height", function (d) {
      return height - y(d[prop]);
    })
    .attr("width", barWidth);
}

function drawDataA() {
  drawChart(dataA, "population");
}
function drawDataB() {
  drawChart(dataB, "money");
}

d3.select("#dataA").on("click", drawDataA);
d3.select("#dataB").on("click", drawDataB);
<!DOCTYPE html>
<html>
  <meta charset="utf-8" />
  <head>
    <script src="https://d3js.org/d3.v4.js"></script>
  </head>
  <body>
    <div id="app">
      <svg class="chart" id="chart"></svg>
      <button id="dataA">data1</button>
      <button id="dataB">data2</button>
    </div>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.17/d3.min.js"></script>
    <script src="./index.js"></script>
  </body>
</html>
<iframe name="sif1" sandbox="allow-forms allow-modals allow-scripts" frameborder="0"></iframe>

CodePen: https://codepen.io/rfripp2/pen/porpaLL

CodePudding user response:

This is the expected behavior. Let's look at your code:

let bar = chart
    .selectAll("g")   
    .data(dataSet)    
    .enter()         
    .append("g")

The select all statement selects all existing elements matching the selector that are children of elements in the selection chart.

The data method binds a new data array to this selection.

The enter method returns a new selection, containing a placeholder for every item in the data array which does not have a corresponding element in the selection.

The append method returns a newly appended child element for every element in the selection it is called on.

Running the code

The first time you call the draw function you have no g elements, so the selection is empty. You bind data to this empty selection. You then use the enter selection. Because there are two data items and no elements in the selection, enter contains two placeholders/elements. You then use append to add those elements.

The second time you call the draw function you have two g elements, so the selection has two elements in it. You bind data to this selection. You then use the enter selection. Because you already have two elements and you only have two data points, the enter selection is empty. As a consequence, append does not create any new elements.

You can see this by using selection.size():

Show code snippet

const dataA = [
  { population: 50, size: 100 },
  { population: 100, size: 100 },
];

const dataB = [
  { money: 4, currency: "usd" },
  { money: 10, currency: "eur" },
];

function drawChart(dataSet, prop) {
  let width = 900;
  let height = 200;
  let x = d3.scale.ordinal().rangeRoundBands([0, width], 0.9);
  let y = d3.scale
    .linear()
    .domain([dataSet[0][prop] - 39, dataSet[dataSet.length - 1][prop]])
    .range([height, 0]);
  let chart = d3.select("#chart").attr("width", width).attr("height", height);
  let barWidth = width / dataSet.length;
  let div = d3.select("body").append("div").attr("class", "tooltip");
  let bar = chart
    .selectAll("g")
    .data(dataSet)
    .enter()
    .append("g")
    .attr("transform", function (d, i) {
      return "translate("   i * barWidth   ",0)";
    });
    
    console.log("The enter selection contains: "   bar.size()   "elements")
    
  bar
    .append("rect")
    .attr("y", function (d) {
      return y(d[prop]);
    })
    .attr("height", function (d) {
      return height - y(d[prop]);
    })
    .attr("width", barWidth);
}

function drawDataA() {
  drawChart(dataA, "population");
}
function drawDataB() {
  drawChart(dataB, "money");
}

d3.select("#dataA").on("click", drawDataA);
d3.select("#dataB").on("click", drawDataB);
<!DOCTYPE html>
<html>
  <meta charset="utf-8" />
  <head>
    <script src="https://d3js.org/d3.v4.js"></script>
  </head>
  <body>
    <div id="app">
      <svg class="chart" id="chart"></svg>
      <button id="dataA">data1</button>
      <button id="dataB">data2</button>
    </div>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.17/d3.min.js"></script>
    <script src="./index.js"></script>
  </body>
</html>
<iframe name="sif2" sandbox="allow-forms allow-modals allow-scripts" frameborder="0"></iframe>

Solution

We want to use both the update and the enter selection (if the dataset ever changes in size, we'd likely want the exit selection too). We can use .join() to simplify this, join removes elements in the exit selection (surplus elements which don't have a corresponding data item), and returns the merged enter selection (new elements for surplus data items) and update selection (preexisting elements).

The nesting of your elements into a parent g and child rect is unnecessary here - and requires additional modifications. By positioning the bars directly we avoid the need for the parent g:

Show code snippet

const dataA = [
  { population: 50, size: 100 },
  { population: 100, size: 100 },
];

const dataB = [
  { money: 4, currency: "usd" },
  { money: 10, currency: "eur" },
];

function drawChart(dataSet, prop) {
  let width = 400;
  let height = 200;
  let x = d3.scaleBand().range([0, width], 0.9);
  let y = d3.scaleLinear()
    .domain([dataSet[0][prop] - 39, dataSet[dataSet.length - 1][prop]])
    .range([height, 0]);
  let chart = d3.select("#chart").attr("width", width).attr("height", height);
  let barWidth = width / dataSet.length;
  let div = d3.select("body").append("div").attr("class", "tooltip");
  let bar = chart
    .selectAll("rect")
    .data(dataSet)
    .join("rect")
    .attr("transform", function (d, i) {
      return "translate("   i * barWidth   ",0)";
    }).attr("y", function (d) {
      return y(d[prop]);
    })
    .attr("height", function (d) {
      return height - y(d[prop]);
    })
    .attr("width", barWidth);
}

function drawDataA() {
  drawChart(dataA, "population");
}
function drawDataB() {
  drawChart(dataB, "money");
}

d3.select("#dataA").on("click", drawDataA);
d3.select("#dataB").on("click", drawDataB);
<!DOCTYPE html>
<html>
  <meta charset="utf-8" />
  <head>
    
  </head>
  <body>
    <div id="app">
      <svg class="chart" id="chart"></svg>
      <br />
      <button id="dataA">data1</button>
      <button id="dataB">data2</button>
    </div>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/d3/7.0.0/d3.min.js"></script>
    <script src="./index.js"></script>
  </body>
</html>
<iframe name="sif3" sandbox="allow-forms allow-modals allow-scripts" frameborder="0"></iframe>

This requires updating your version of D3 from v3 (you are actually using two versions of D3, v3 and v4, both fairly outdated, and both with different method names, actually with different ways of handling the enter selection).

If you wish to use d3v4, then the join method is not available, but we can merge enter and update:

When I say update selection, I'm refering to the initial selection:

  // update selection:
  let bar = chart
    .selectAll("rect")
    .data(dataSet);
    
   // enter selection:   
  bar.enter().append("rect")

Show code snippet

const dataA = [
  { population: 50, size: 100 },
  { population: 100, size: 100 },
];

const dataB = [
  { money: 4, currency: "usd" },
  { money: 10, currency: "eur" },
];

function drawChart(dataSet, prop) {
  let width = 400;
  let height = 200;
  let x = d3.scaleBand().range([0, width], 0.9);
  let y = d3.scaleLinear()
    .domain([dataSet[0][prop] - 39, dataSet[dataSet.length - 1][prop]])
    .range([height, 0]);
  let chart = d3.select("#chart").attr("width", width).attr("height", height);
  let barWidth = width / dataSet.length;
  let div = d3.select("body").append("div").attr("class", "tooltip");
  let bar = chart
    .selectAll("rect")
    .data(dataSet);
    
  bar.enter().append("rect")
     .merge(bar)
    .attr("transform", function (d, i) {
      return "translate("   i * barWidth   ",0)";
    }).attr("y", function (d) {
      return y(d[prop]);
    })
    .attr("height", function (d) {
      return height - y(d[prop]);
    })
    .attr("width", barWidth);
}

function drawDataA() {
  drawChart(dataA, "population");
}
function drawDataB() {
  drawChart(dataB, "money");
}

d3.select("#dataA").on("click", drawDataA);
d3.select("#dataB").on("click", drawDataB);
<!DOCTYPE html>
<html>
  <meta charset="utf-8" />
  <head>
    
  </head>
  <body>
    <div id="app">
      <svg class="chart" id="chart"></svg>
      <br />
      <button id="dataA">data1</button>
      <button id="dataB">data2</button>
    </div>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.1.0/d3.min.js"></script>
    <script src="./index.js"></script>
  </body>
</html>
<iframe name="sif4" sandbox="allow-forms allow-modals allow-scripts" frameborder="0"></iframe>

And lastly, if you wish to keep d3v3 (we are on v7 already), we can rely on an implicit merging of update and enter on enter (modifying the update selection). This "magic" was removed in v4, partly because it was not explicit. To do so we need to break your method chaining so that bar contains the

Show code snippet

const dataA = [
  { population: 50, size: 100 },
  { population: 100, size: 100 },
];

const dataB = [
  { money: 4, currency: "usd" },
  { money: 10, currency: "eur" },
];

function drawChart(dataSet, prop) {
  let width = 400;
  let height = 200;
  let x = d3.scale.ordinal().rangeRoundBands([0, width], 0.9);
  let y = d3.scale.linear()
    .domain([dataSet[0][prop] - 39, dataSet[dataSet.length - 1][prop]])
    .range([height, 0]);
  let chart = d3.select("#chart").attr("width", width).attr("height", height);
  let barWidth = width / dataSet.length;
  let div = d3.select("body").append("div").attr("class", "tooltip");
  
  // update selection:
  let bar = chart
    .selectAll("rect")
    .data(dataSet);
    
   // enter selection:   
  bar.enter().append("rect")
     
     
    bar.attr("transform", function (d, i) {
      return "translate("   i * barWidth   ",0)";
    }).attr("y", function (d) {
      return y(d[prop]);
    })
    .attr("height", function (d) {
      return height - y(d[prop]);
    })
    .attr("width", barWidth);
}

function drawDataA() {
  drawChart(dataA, "population");
}
function drawDataB() {
  drawChart(dataB, "money");
}

d3.select("#dataA").on("click", drawDataA);
d3.select("#dataB").on("click", drawDataB);
<!DOCTYPE html>
<html>
  <meta charset="utf-8" />
  <head>
    
  </head>
  <body>
    <div id="app">
      <svg class="chart" id="chart"></svg>
      <br />
      <button id="dataA">data1</button>
      <button id="dataB">data2</button>
    </div>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.17/d3.min.js"></script>
    <script src="./index.js"></script>
  </body>
</html>
<iframe name="sif5" sandbox="allow-forms allow-modals allow-scripts" frameborder="0"></iframe>

Note: d3v4 made changes to method names that break code from v3. This required changes to d3.scale.linear / d3.scale.ordinal in the snippets using v4 and 7 (using merge and join respectively).

  • Related