I am creating a stacked bar chart using d3 charts. I have button in the front-end. On clicking the button chart should display.On clicking a button I am getting following error.
Changes in D3v4
Changes were made in d3v4 that would break a d3v3 chart. Stacks in d3v3 had x accessors and used y0
and y
to represent start and end values for each segment of a stacked chart. The result looks something like this:
.append("rect")
.attr("x", function(d) { return x(d.x); })
.attr("y", function(d) { return y(d.y0 + d.y); })
.attr("height", function(d) { return y(d.y0) - y(d.y0 + d.y); })
This approach does not work with d3v4+, but it appears to be the approach you are using. At a glance, it appears your code may be based on this v3 code.
Stacks in d3v4+
Let's take a look at what d3.stack produces in d3v4+.
We have our input data:
var data = [
{ country:"India", sales:100, profit:100, loss:10 },
{ country:"US", sales:100, profit:80, loss:40 },
{ country:"aus", sales:100, profit:70, loss:30 }
];
And a stack generator:
var stack = d3.stack()
.keys(["sales","profit","loss"])
.order(d3.stackOrderNone) // default value, does not need to be specified.
.offset(d3.stackOffsetNone) // default value, does not need to be specified
When we pass the data to the stack generator we get:
[/* India US AUS */
[[0, 100],[0, 100],[0, 100]], // Sales
[[100,200],[100,180],[100,170]], // Profit
[[200,210],[180,220],[170,200]] // Loss
]
If arranging the array as a table, the columns represent each item in the original data array (a series representing a country), the rows represent each key. Each of the two element arrays represent a single stacked bar segment.
The arrays that contain all the values for a given key (for example [[0,100],[0,100],[0,100]]
for sales), also contain a property "key" that hold's that key name.
Each of the arrays that represent a single segment (eg: [0,100]
), have a property data
that is an item in the original data array (eg: {"country": "India","sales": 100,"profit": 100,"loss": 10}
).
The number of items in the stack array (the number of rows) is not equal to the number of series (countries in this case), but is equal to the number of keys.
Creating The Scales
The first issue you have is how you specify scales:
var x = d3.scaleOrdinal()
.domain(series[0].map(function(d) { return d.x; }))
.range([10, width-10], 0.02);
var y = d3.scaleLinear()
.domain([0, d3.max(series, function(d) { return d3.max(d, function(d) { return d.y0 + d.y; }); })])
.range([height, 0]);
Because the stack doesn't create any y0
, y
or x
properties, these won't work.
The x scale should be a band scale (bar charts generally use band scales with d3), and we can just use the original data array to create a domain:
var x = d3.scaleBand() // band scale
.domain(data.map(function(d) {
return d.country; })) // return the country.
.range([10, width-10]);
The y scale needs to access d[0] and d[1] rather than d.y0 and d.y:
var y = d3.scaleLinear()
.domain([0, d3.max(series, function(d) {
return d3.max(d, function(d) {
return d[0] + d[1]; }); })]) // access stack values (not d.y, d.y0)
.range([height, 0]);
Appending The Rectangles
Now let's move to where we append the rectangles:
.append("rect")
.attr("x", function(d) { return x(d.x); })
.attr("y", function(d) { return y(d.y0 + d.y); })
.attr("height", function(d) { return y(d.y0) - y(d.y0 + d.y); })
.attr("width", x.range())
As d.x is undefined, we can use x(d.data.country)
. Where d.y0
and d.y
appears we need to subsitute d[0]
and d[1]
. Lastly x.range()
is the width of the graph almost, we can use x.bandwidth()
instead, which is the width of a single bar.
Below is your code (starting where you define the margin) with these updates:
var margin = { top: 20, right: 160, bottom: 35, left: 30 };
var width = 500 - margin.left - margin.right,
height = 300 - margin.top - margin.bottom;
var svg = d3.select("body")
.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 data = [
{ country:"India", sales:100, profit:100, loss:10 },
{ country:"US", sales:100, profit:80, loss:40 },
{ country:"Aus", sales:100, profit:70, loss:30 },
{ country:"NZ", sales:100, profit:70, loss:30 }
];
var stack = d3.stack()
.keys(["sales","profit","loss"])
.order(d3.stackOrderNone)
.offset(d3.stackOffsetNone);
var series = stack(data);
var x = d3.scaleBand() // band scale
.domain(data.map(function(d) {
return d.country; })) // return the country.
.range([10, width-10])
.padding(0.1); // space between (value between 0 and 1).
var y = d3.scaleLinear()
.domain([0, d3.max(series, function(d) {
return d3.max(d, function(d) {
return d[0] + d[1]; }); })]) // access stack values (not d.y, d.y0)
.range([height, 0]);
// color scale:
var colors = d3.scaleOrdinal()
.range(["b33040", "#d25c4d", "#f2b447", "#d9d574"])
.domain(series.map(function(d) { return d.key; }));
var yAxis = d3.axisLeft(y)
.ticks(5)
.tickSize(-width, 0, 0)
.tickFormat( function(d) { return d } );
var xAxis = d3.axisBottom(x)
svg.append("g")
.attr("class", "y axis")
.call(yAxis);
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + height + ")")
.call(xAxis);
var groups = svg.selectAll("g.cost")
.data(series)
.enter().append("g")
.attr("class", "cost")
.style("fill", function(d) { return colors(d); });
// More changes:
var rect = groups.selectAll("rect")
.data(function(d) { return d; })
.enter()
.append("rect")
.attr("x", function(d) { return x(d.data.country); }) // Access country.
.attr("y", function(d) { return y(d[0] + d[1]); })
.attr("height", function(d) { return y(d[0]) - y(d[0] + d[1]); })
.attr("width", x.bandwidth())
// Minor changes:
var legend = svg.selectAll(".legend")
.data(series)
.enter().append("g")
.attr("class", "legend")
.attr("transform", function(d, i) { return "translate(30," + i * 19 + ")"; });
legend.append("rect")
.attr("x", width - 18)
.attr("width", 18)
.attr("height", 18)
.style("fill", function(d) {
return colors(d.key);
});
legend.append("text")
.attr("x", width + 5)
.attr("y", 9)
.attr("dy", ".35em")
.style("text-anchor", "start")
.text(function(d) { return d.key; })
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
To simplify the snippet I removed mouse interaction
Creating the Legend
This can be simplified a bit, it isn't part of the question, nonetheless, I've included it here, along with a color scale.