Reputation: 1183
I have a project where I try to create a stacked bar chart. One of the requirements of this is that the chart should be able to update. I have used the general update pattern to accomplish this. But I cannot get the code to work.
When the chart updates the new generated chart enters from the bottom. So I guess this means that the code never executes update part of the pattern and always thinks the updated data is new data.
I have added 3 different arrays of data to the snippet to give a good example of the problem I am facing. The first two arrays contain the same key and should update the chart. The last array contains completely different keys and here the chart should enter in stead of updating.
I have also noticed that when the new data is added to the chart. The old data is never deleted. So the exit state of the update pattern also does not get triggered.
I know there should be a key defined in the data function of the update pattern.data(stackData, d => {return d.key;})
. But I cannot seem to grasp what key I should enter here.
I have deleted as much code as needed from my original code in order to get it to work for the snippet. All the code that is included inside the snippet is needed to get it to work.
this.width = 400;
this.height = 200;
var margin = {
top: 20,
right: 20,
bottom: 30,
left: 40
}
this.index = 0;
this.svg = d3
.select(".canvas")
.classed("svg-container", true)
.append("svg")
.attr("class", "chart")
.attr(
"viewBox",
`0 0 ${this.width} ${this.height}`
)
.attr("preserveAspectRatio", "xMinYMin meet")
.classed("svg-content-responsive", true)
.append("g");
const scale = [0, 1200];
// set the scales
this.xScale = d3
.scaleBand()
.range([0, width])
.padding(0.3);
this.yScale = d3.scaleLinear().range([this.height, 0]);
this.svg.append("g").attr("class", "bars");
const update = data => {
const scale = [0, 1200];
// Update scales.
this.xScale.domain(data.map(d => d.key));
this.yScale.domain([scale[0], scale[1]]);
const subgroups = ["home", "work", "public"];
var color = d3
.scaleOrdinal()
.domain(subgroups)
.range(["#206BF3", "#171D2C", "#8B0000"]);
var stackData = d3.stack().keys(subgroups)(data);
// Set up transition.
const dur = 1000;
const t = d3.transition().duration(dur);
this.svg
.append("g")
.selectAll("g")
.data(stackData, d => {
return d.key;
})
.join(
enter => {
enter
.append("g")
.attr("fill", d => color(d.key))
.selectAll("rect")
.data(d => {
return d;
})
.join(
enter => {
enter
.append("rect")
.attr("class", "bar")
.attr("x", d => {
return this.xScale(d.data.key);
})
.attr("y", () => {
return this.yScale(0);
})
.attr("height", () => {
return this.height - this.yScale(0);
})
.attr("width", this.xScale.bandwidth())
.transition(t)
.delay((d, i) => i * 20)
.attr("y", d => {
return this.yScale(d[1]);
})
.attr("height", d => {
return this.yScale(d[0]) - this.yScale(d[1]);
});
},
update => {
update
.transition(t)
.delay((d, i) => i * 20)
.attr("x", d => this.xScale(d.key))
.attr("y", d => {
return this.yScale(d[1]);
})
.attr("height", d => {
return this.yScale(d[0]) - this.yScale(d[1]);
});
},
exit => {
exit
.transition()
.duration(dur / 2)
.style("fill-opacity", 0)
.remove();
}
);
},
update => {
update
.transition(t)
.delay((d, i) => i * 20)
.attr("x", d => this.xScale(d.key))
.attr("y", d => {
return this.yScale(d[1]);
})
.attr("height", d => {
return this.yScale(d[0]) - this.yScale(d[1]);
});
},
exit => {
exit
.transition()
.duration(dur / 2)
.style("fill-opacity", 0)
.remove();
}
);
};
const data = [
[{
key: "Jan",
home: 371,
work: 335,
public: 300
},
{
key: "Feb",
home: 343,
work: 437,
public: 228
},
{
key: "Mrt",
home: 359,
work: 261,
public: 202
},
{
key: "Apr",
home: 274,
work: 217,
public: 482
},
{
key: "Mei",
home: 442,
work: 314,
public: 477
},
{
key: "Jun",
home: 464,
work: 261,
public: 278
},
{
key: "Jul",
home: 343,
work: 244,
public: 396
},
{
key: "Aug",
home: 231,
work: 406,
public: 338
},
{
key: "Sep",
home: 380,
work: 382,
public: 366
},
{
key: "Okt",
home: 391,
work: 408,
public: 455
},
{
key: "Nov",
home: 419,
work: 261,
public: 226
},
{
key: "Dec",
home: 217,
work: 453,
public: 335
}
],
[{
key: "Jan",
home: 282,
work: 363,
public: 379
},
{
key: "Feb",
home: 428,
work: 355,
public: 216
},
{
key: "Mrt",
home: 217,
work: 493,
public: 280
},
{
key: "Apr",
home: 304,
work: 283,
public: 454
},
{
key: "Mei",
home: 397,
work: 406,
public: 289
},
{
key: "Jun",
home: 242,
work: 239,
public: 232
},
{
key: "Jul",
home: 327,
work: 453,
public: 264
},
{
key: "Aug",
home: 242,
work: 240,
public: 414
},
{
key: "Sep",
home: 495,
work: 382,
public: 368
},
{
key: "Okt",
home: 285,
work: 471,
public: 364
},
{
key: "Nov",
home: 315,
work: 421,
public: 482
},
{
key: "Dec",
home: 214,
work: 284,
public: 297
}
],
[{
key: "1",
home: 282,
work: 363,
public: 379
},
{
key: "2",
home: 232,
work: 432,
public: 324
},
{
key: "3",
home: 324,
work: 124,
public: 432
},
{
key: "4",
home: 425,
work: 353,
public: 532
}
]
];
update(data[this.index]);
const swap = document.querySelector(".swap");
swap.addEventListener("click", () => {
if (this.index < 2) this.index += 1;
else this.index = 0;
update(data[this.index]);
});
<button class="swap">swap</button>
<div class="canvas"></div>
<script src="https://d3js.org/d3.v6.js"></script>
Upvotes: 2
Views: 814
Reputation: 10886
There are at least a few things going on here. As pointed out by Michael Rovinsky, you're appending a new g
group for every update that houses all of your bars. Since it's a new group, it has no data bound to it, which will make new bars for every call.
Instead, you should make the group only once, and maybe assign it to a variable to make it a little clearer to work with. I'll use the this.svg.append("g").attr("class", "bars");
group since it doesn't seem to be used for anything else:
var bars = this.svg.append("g").attr("class", "bars");
Then in your update function, change:
this.svg
.append("g")
.selectAll("g")
// ...
to
bars
.selectAll("g")
Your invocations of join
aren't quite standard when you don't return the enter
, update
, and exit
selections. When you have an arrow function with a body like:
enter => {
enter.append('g') //...
}
it doesn't actually return the enter selection, it should be either
enter => {
return enter.append('g') //...
}
or
enter => enter.append('g') //...
While you don't technically need to return the enter
or update
selections, it's the intended way to use them, and does cause problems when you try to use the combined enter
and update
selections later, which we will later do.
The way you implement your nested joining suggests that you're not quite understanding how joining works. When you enter
and append new elements, those elements don't have any data bound to them, so when you use a subsequent join
, the nested update
and exit
functions won't ever run.
You'll also notice that you're repeating a lot of the same code in both enter
and update
.
The more standard way of doing what I think you intend to do, is to add new bars if necessary, and then to use the new bars and old bars (which is returned by join
and update them with the bar segments.
It'll look something like this:
bars.selectAll('g').data(data).join(
enter => enter.append('g'),
update => update, // don't do anything with only the update selection
exit => exit.remove()
) // now we're selecting all of the enter and update g's
.selectAll('rect')
.data(d => d).join(
enter => enter.append('rect'),
update => update,
exit => exit.remove()
) // now we're selecting all of the enter and update rects
.transition(t)
// ...
If you look at the format of the data you can see that stackData is always an array of length 3 that has an array for each entry. Those nested arrays vary based on the number of columns and it's at that level that the keys should be defined:
//... group selection
.selectAll('rect')
.data(d => d, d => d.data.key)
//...
Finally there's bit of code when you're updating the bar location .attr("x", d => this.xScale(d.key))
which should use d.data.key
instead.
I consolidated most of the redundant enter and update code, and removed some extraneous code. All together, it looks like:
this.width = 400;
this.height = 200;
var margin = {
top: 20,
right: 20,
bottom: 30,
left: 40
}
this.index = 0;
this.svg = d3
.select(".canvas")
.classed("svg-container", true)
.append("svg")
.attr("class", "chart")
.attr(
"viewBox",
`0 0 ${this.width} ${this.height}`
)
.attr("preserveAspectRatio", "xMinYMin meet")
.classed("svg-content-responsive", true)
.append("g");
const scale = [0, 1200];
// set the scales
this.xScale = d3
.scaleBand()
.range([0, width])
.padding(0.3);
this.yScale = d3.scaleLinear().range([this.height, 0]);
var bars = this.svg.append("g").attr("class", "bars");
const update = data => {
const scale = [0, 1200];
// Update scales.
this.xScale.domain(data.map(d => d.key));
this.yScale.domain([scale[0], scale[1]]);
const subgroups = ["home", "work", "public"];
var color = d3
.scaleOrdinal()
.domain(subgroups)
.range(["#206BF3", "#171D2C", "#8B0000"]);
var stackData = d3.stack().keys(subgroups)(data);
// Set up transition.
const dur = 1000;
const t = d3.transition().duration(dur);
bars
.selectAll("g")
.data(stackData)
.join(
enter => enter
.append("g")
.attr("fill", d => color(d.key)),
null, // no update function
exit => {
exit
.transition()
.duration(dur / 2)
.style("fill-opacity", 0)
.remove();
}
).selectAll("rect")
.data(d => d, d => d.data.key)
.join(
enter => enter
.append("rect")
.attr("class", "bar")
.attr("x", d => {
return this.xScale(d.data.key);
})
.attr("y", () => {
return this.yScale(0);
})
.attr("height", () => {
return this.height - this.yScale(0);
})
.attr("width", this.xScale.bandwidth())
,
null,
exit => {
exit
.transition()
.duration(dur / 2)
.style("fill-opacity", 0)
.remove();
}
)
.transition(t)
.delay((d, i) => i * 20)
.attr("x", d => this.xScale(d.data.key))
.attr("y", d => {
return this.yScale(d[1]);
})
.attr("width", this.xScale.bandwidth())
.attr("height", d => {
return this.yScale(d[0]) - this.yScale(d[1]);
});
};
const data = [
[{
key: "Jan",
home: 371,
work: 335,
public: 300
},
{
key: "Feb",
home: 343,
work: 437,
public: 228
},
{
key: "Mrt",
home: 359,
work: 261,
public: 202
},
{
key: "Apr",
home: 274,
work: 217,
public: 482
},
{
key: "Mei",
home: 442,
work: 314,
public: 477
},
{
key: "Jun",
home: 464,
work: 261,
public: 278
},
{
key: "Jul",
home: 343,
work: 244,
public: 396
},
{
key: "Aug",
home: 231,
work: 406,
public: 338
},
{
key: "Sep",
home: 380,
work: 382,
public: 366
},
{
key: "Okt",
home: 391,
work: 408,
public: 455
},
{
key: "Nov",
home: 419,
work: 261,
public: 226
},
{
key: "Dec",
home: 217,
work: 453,
public: 335
}
],
[{
key: "Jan",
home: 282,
work: 363,
public: 379
},
{
key: "Feb",
home: 428,
work: 355,
public: 216
},
{
key: "Mrt",
home: 217,
work: 493,
public: 280
},
{
key: "Apr",
home: 304,
work: 283,
public: 454
},
{
key: "Mei",
home: 397,
work: 406,
public: 289
},
{
key: "Jun",
home: 242,
work: 239,
public: 232
},
{
key: "Jul",
home: 327,
work: 453,
public: 264
},
{
key: "Aug",
home: 242,
work: 240,
public: 414
},
{
key: "Sep",
home: 495,
work: 382,
public: 368
},
{
key: "Okt",
home: 285,
work: 471,
public: 364
},
{
key: "Nov",
home: 315,
work: 421,
public: 482
},
{
key: "Dec",
home: 214,
work: 284,
public: 297
}
],
[{
key: "1",
home: 282,
work: 363,
public: 379
},
{
key: "2",
home: 232,
work: 432,
public: 324
},
{
key: "3",
home: 324,
work: 124,
public: 432
},
{
key: "4",
home: 425,
work: 353,
public: 532
}
]
];
update(data[this.index]);
const swap = document.querySelector(".swap");
swap.addEventListener("click", () => {
if (this.index < 2) this.index += 1;
else this.index = 0;
update(data[this.index]);
});
<button class="swap">swap</button>
<div class="canvas"></div>
<script src="https://d3js.org/d3.v6.js"></script>
Upvotes: 6
Reputation: 7210
Exit will not work because you add a "g" container on each update. Just add the following line:
this.svg.selectAll("g").transition().duration(dur).style("opacity",0).remove();
before you call
this.svg.append("g").selectAll("g").data(stackData...
You can see the solution working in a fiddle
Upvotes: 0
Reputation: 7210
The stateData you pass to .selectAll('g').data(stackData) is an array of arrays and does not contain keys. You need to enclose every array into an object and specify unique key:
correctStackData = stackData.map((item, index) => ({data: item, key: index}));
Upvotes: 0