d3 elements within a group not being removed properly - d3.js

I have a table with filtered data that's working properly and now I'm trying to make a corresponding barchart. The barchart consists of a group for each bar, with two text elements and a rect inside of it. The exit selection successfully removes the g element but the internal rect and text somehow ends up in another g.
function updateSvg(data) {
parameters.svg = d3.select(".svg")
.attr("height", data.length * parameters.barHeight)
var life_expectancy = d3.extent(data.map(getter('life_expectancy')));
var min = life_expectancy[0];
var max = life_expectancy[1];
var x = d3.scale.linear()
.domain([0, max])
.range([0, parameters.svgWidth])
// Data join.
var groups = parameters.svg.selectAll("g")
.data(data)
// Enter.
var groupsEnter = groups.enter().append("g").attr("transform", function(d, i) { return "translate(0," + i * parameters.barHeight + ")"; })
// Update.
var bars = groups.append("rect")
.attr("width", function(d) { return x(d.life_expectancy)})
.attr("height", parameters.barHeight - 1)
var labels = groups.append("text")
.attr("x", 20)
.attr("y", parameters.barHeight / 2)
.attr("dy", ".35em")
.text(function(d) { return d.name; })
var values = groups.append("text")
.attr("x", function(d) { return x(d.life_expectancy) - 50; })
.attr("y", parameters.barHeight / 2)
.attr("dy", ".35em")
.text(function(d) { return d.life_expectancy})
// Exit.
groups.exit().remove()
}
Here's what I have working so far: http://chrisdaly.github.io/D3/World%20Countries%20Rank/table.html. If you untick all the continents except Oceania for example and inspect the bars, it shows a tonne of different rects etc hidden underneath the correct one. Any guidance is appreciated!

The problem is here
groups.exit().remove()
On slider motion the values with in the country array will change but none of the g group DOM will get removed because the array still has the same number of array elements. So on that g group you go on appending rect and text.
groups.append("rect")
.attr("width", function(d) { return x(d.life_expectancy)})
.attr("height", parameters.barHeight - 1)
Now when you tick off Americas the g tag for USA will go which is what exit function does. Reason: your array is filtered has no record for USA.
But the g for Asia countries and others you append the text and rect again thus it keeps growing.
Best way out is when you update do this to remove all rect and text:
groups.selectAll("text").remove();
groups.selectAll("rect").remove();

Related

How add legend to a bar-chart based on data in an object dynamically (d3)

I am really new to the realm of D3 and based on the book of 'Interactive Data visualization for the web', I managed to create a Bar chart which is mostly based on the code from the following link.
The problem is I don't manage to add a legend to my bar chart based on an object dynamically.
I have tried consulting youtube videos and other stackoverflow questions related to 'adding a legend to a bar-chart', however in my opinion I couldn't find the question concerning how one is able to retrieve keys from an array of objects and use the data to add as an legend to the bar-chart.
For now all my bars also have the same color, see the second code below.
See the code below for the formatting of my object which is embedded in an array.
The name 'key' and 'value' are fixed, while the amount of the objects and their corresponding name and value differ after an click event of the user ( which determines which variables will be included in the object).
The following example is able create a legend, however in this case the formatting of the object is somehow different than in my case and my current knowledge of D3 is limitd, so I have no idea in which ways I have to adapt the code.
2: {key: "bedrijfsvestigingen_Sbi2008_BedrijfsvestigingenTotaal", value: 490}
3: {key: "bedrijfsvestigingen_Sbi2008_BedrijfsvestigingenNaarActiviteit_M_nZakelijkeDienstverlening", value: 165}
4: {key: "bedrijfsvestigingen_Sbi2008_BedrijfsvestigingenNaarActiviteit_R_uCultuur_Recreatie_OverigeDiensten", value: 120}
5: {key: "bedrijfsvestigingen_Sbi2008_BedrijfsvestigingenNaarActiviteit_K_lFinancieleDiensten_OnroerendGoed", value: 15}
6: {key: "bedrijfsvestigingen_Sbi2008_BedrijfsvestigingenNaarActiviteit_ALandbouw_BosbouwEnVisserij", value: 0}
7: {key: "bedrijfsvestigingen_Sbi2008_BedrijfsvestigingenNaarActiviteit_H_p_JVervoer_InformatieEnCommunicatie", value: 85}];
Based on the code from the book and accounting for other variables, I have currently the following code for visualizing a bar chart, in which the values (see object above) are shown in the bar charts and the color of the bar are all blueish. However there is not yet an legend included in my current code. Therefore I am wondering how one is able to dynamically create a legend based on the 'keys' ( in my case)in the object and represent the corresponding color bound to the bars. I would like to achieve the lowest image which I have drawn a sketch of.
var svg = d3.select("#barchart")
.select("svg")
.remove("svg");
//Width and height
var w = 600;
var h = 250;
var padding=20;
var xScale = d3.scaleBand()
.domain(d3.range(dataset.length))
.rangeRound([w - padding,padding ])
.paddingInner(0.05);
var yScale = d3.scaleLinear()
.domain([0, d3.max(dataset, function (d) {
return d.value;
})])
.range([padding,h - padding]);
console.log("yscale",yScale);
//Define key function, to be used when binding data
var key = function (d) {
console.log("key", d);
return d.key;
};
// d3.select("svg").remove();
//Create SVG element
var svg = d3.select("#barchart")
.append("svg")
.attr("width", w)
.attr("height", h);
console.log("svg", svg);
//Create bars
svg.selectAll("rect")
.data(dataset, key) //Bind data with custom key function
.enter()
.append("rect")
.attr("x", function (d, i) {
return xScale(i);
})
.attr("y", function (d) {
return h - yScale(d.value);
})
.attr("width", xScale.bandwidth())
.attr("height", function (d) {
return yScale(d.value);
})
// .attr("data-legend", function (d) { return d.key })
.attr("fill", function (d) {
return "rgb(0, 0, " + (d.value * 10) + ")";
});
//Create labels
svg.selectAll("text")
.data(dataset, key) //Bind data with custom key function
.enter()
.append("text")
.text(function (d) {
return d.value;
})
.attr("text-anchor", "middle")
.attr("x", function (d, i) {
return xScale(i) + xScale.bandwidth() / 2;
})
.attr("y", function (d) {
return h - yScale(d.value) + 14;
})
.attr("font-family", "sans-serif")
.attr("font-size", "11px")
.attr("fill", "white");
If I understood correctly this is what you should need.
Plunker with working code.
First of all I would encourage to use an margin object which will allow better flexibility when dealing with charts
var margin = {
top: 20,
right: 20,
bottom: 20,
left: 20
};
We want to display the data with an odinal scale from the data and example you provided.
{key: "bedrijfsvestigingen_Sbi2008_BedrijfsvestigingenTotaal", value: 490}
{key: "bedrijfsvestigingen_Sbi2008_BedrijfsvestigingenNaarActiviteit_M_nZakelijkeDienstverlening", value: 165}
{key: "bedrijfsvestigingen_Sbi2008_BedrijfsvestigingenNaarActiviteit_R_uCultuur_Recreatie_OverigeDiensten", value: 120}
{key: "bedrijfsvestigingen_Sbi2008_BedrijfsvestigingenNaarActiviteit_K_lFinancieleDiensten_OnroerendGoed", value: 15}
{key: "bedrijfsvestigingen_Sbi2008_BedrijfsvestigingenNaarActiviteit_ALandbouw_BosbouwEnVisserij", value: 0}
{key: "bedrijfsvestigingen_Sbi2008_BedrijfsvestigingenNaarActiviteit_H_p_JVervoer_InformatieEnCommunicatie", value: 85}];
Taking into account that probably the first element is a sum of sorts of the dataset I think it shouldn't be included in the chart since it is an aggregation of the elements we want to display.
(In case you need to display it as an element you should be able to do it quickly after reviewing the answer)
The element structure in your dataset is the following:
{
key: "bedrijfsvestigingen_Sbi2008_BedrijfsvestigingenNaarActiviteit_H_p_JVervoer_InformatieEnCommunicatie",
value: 85
}
The domain of our xScale should be all the key values in our dataset, since the key is a huge string, I created a custom property in each element called label
{
key:
"bedrijfsvestigingen_Sbi2008_BedrijfsvestigingenNaarActiviteit_M_nZakelijkeDienstverlening",
label: "Business Services",
value: 165
}
Lets create our scale with the correct domain and range:
var xScale = d3
.scaleBand()
.domain(dataset.map(d => d.label)) // All our label properties
.rangeRound([0, w - margin.left - margin.right]) // This scale will map our values from [0, width - margin.left - margin.right]
.paddingInner(0.05);
The yScale was almost correct, we just need to change it a little to use our margin object and use the correct range
The range must start from 0, if we used padding as the starting point our values will have an offset, since our values would be mapped from [padding, h - padding]. If we wanted to display a zero the value would be mapped to the padding value, if this is way you want to show the information keep it that way. In this case we will modify the scale.
var yScale = d3
.scaleLinear()
.domain([
0,
d3.max(dataset, function(d) {
return d.value;
})
])
.range([0, h - margin.top - margin.bottom]);
Next we will create a function to get the desired value from our elements
var xKey = function(d) {
return d.label;
};
Add our svg with some visual cues to help visualizing the way the elements are layed out:
var svg = d3
.select("#barchart")
.append("svg")
.style("background", "rgb(243, 243, 243)")
.style("border", "1px dashed #b4b4b4")
.attr("width", w)
.attr("height", h);
We want to use a margin, so lets use a group tag to achieve this, we could individually set the margin in each group/element we desired but I find this way simpler and clearer
var g = svg
.append("g")
.attr("transform", `translate(${margin.left}, ${margin.top})`);
We will need the width and height of the chart with the margins taken into account, lets define them really quick:
const customWidth = w - margin.left - margin.right;
const customHeight = h - margin.top - margin.bottom;
Let us add a rect to show where will our rects will be displayed:
g.append("rect")
.attr("fill", "#e3e3e3")
.attr("width", customWidth)
.attr("height", customHeight);
Lets deal with the rect creation, in your code you had a custom fill function which modified the b value within the RGB color values. In this case since we are dealing with categorical data we will use an array of colors for the rects.
g.append("g")
.attr("class", "rect__container")
.selectAll("rect")
.data(dataset, xKey) //Bind data with custom key function
.enter()
.append("rect")
.attr("x", function(d, i) {
return xScale(xKey(d)); // use our key function
})
.attr("y", function(d) {
return customHeight - yScale(d.value); // use our custom size values
})
.attr("width", xScale.bandwidth())
.attr("height", function(d) {
return yScale(d.value);
})
.attr("fill", function(d, i) {
return d3.schemeCategory10[i]; // use an array of colors and use the index to decide which color to use
});
We have two options to show the labels of the chart:
We can create an x-axis or the desired legends. We will do both since it won't affect the outcome of the chart and either one of them can be removed.
var margin = {
top: 20,
right: 300, // modifiy our margin to have space to display the legends
bottom: 50,
left: 20
};
var legendElement = g
.append("g")
.attr("class", "legend__container")
.attr("transform", `translate(${customWidth}, ${margin.top})`) // set our group position to the end of the chart
.selectAll("g.legend__element")
.data(xScale.domain()) // use the scale domain as data
.enter()
.append("g")
.attr("transform", function(d, i) {
return `translate(${10}, ${i * 30})`; // provide an offset for each element found in the domain
});
legendElement
.append("text")
.attr("x", 30)
.attr("font-size", "14px")
.text(d => d);
legendElement
.append("rect")
.attr("x", 0)
.attr("y", -15)
.attr("width", 20)
.attr("height", 20)
.attr("fill", function(d, i) {
return d3.schemeCategory10[i]; // use the same category color that we previously used in rects
});
Now lets use the axis approach:
// create axis
var x_axis = d3.axisBottom().scale(xScale);
//Append group and insert axis
g.append("g")
.attr("transform", `translate(${0}, ${customHeight})`)
.call(x_axis);
g.append("g")
.attr("transform", `translate(${customWidth / 2}, ${customHeight + 40})`)
.append("text")
.text("Activities")
.attr("font-family", "sans-serif")
.attr("font-size", "14px")
.attr("font-weight", "bold")
.style("text-transform", "uppercase")
.attr("text-anchor", "middle");
And finally create the labels for the value in our data:
//Create labels
g.append("g")
.attr("class", "text__container")
.selectAll("text")
.data(dataset, xKey) //Bind data with custom key function
.enter()
.append("text")
.text(function(d) {
return d.value;
})
.attr("text-anchor", "middle")
.attr("x", function(d, i) {
return xScale(xKey(d)) + xScale.bandwidth() / 2;
})
.attr("y", function(d) {
return customHeight - yScale(d.value) + 14;
})
.attr("font-family", "sans-serif")
.attr("font-size", "11px")
.attr("fill", "white");

D3.js get data index of shape on mouseover event

I am attempting to access the data index of a shape on mouseover so that I can control the behavior of the shape based on the index.
Lets say that this block of code lays out 5 rect in a vertical line based on some data.
var g_box = svg
.selectAll("g")
.data(controls)
.enter()
.append("g")
.attr("transform", function (d,i){
return "translate("+(width - 100)+","+((controlBoxSize+5)+i*(controlBoxSize+ 5))+")"
})
.attr("class", "controls");
g_box
.append("rect")
.attr("class", "control")
.attr("width", 15)
.attr("height", 15)
.style("stroke", "black")
.style("fill", "#b8b9bc");
When we mouseover rect 3, it transitions to double size.
g_box.selectAll("rect")
.on("mouseover", function(d){
d3.select(this)
.transition()
.attr("width", controlBoxSize*2)
.attr("height", controlBoxSize*2);
var additionalOffset = controlBoxSize*2;
g_box
.attr("transform", function (d,i){
if( i > this.index) { // want to do something like this, what to use for "this.index"?
return "translate("+(width - 100)+","+((controlBoxSize+5)+i*(controlBoxSize+5)+additionalOffset)+")"
} else {
return "translate("+(width - 100)+","+((controlBoxSize+5)+i*(controlBoxSize+5))+")"
}
})
})
What I want to do is move rect 4 and 5 on mouseover so they slide out of the way and do not overlap rect 3 which is expanding.
So is there a way to detect the data index "i" of "this" rect in my mouseover event so that I could implement some logic to adjust the translate() of the other rect accordingly?
You can easily get the index of any selection with the second argument of the anonymous function.
The problem here, however, is that you're trying to get the index in an anonymous function which is itself inside the event handler, and this won't work.
Thus, get the index in the event handler...
selection.on("mouseover", function(d, i) {
//index here ---------------------^
... and, inside the inner anonymous function, get the index again, using different parameter name, comparing them:
innerSelection.attr("transform", function(e, j) {
//index here, with a different name -----^
This is a simple demo (full of magic numbers), just to show you how to do it:
var svg = d3.select("svg");
var data = d3.range(5);
var groups = svg.selectAll("foo")
.data(data)
.enter()
.append("g");
var rects = groups.append("rect")
.attr("y", 10)
.attr("x", function(d) {
return 10 + d * 20
})
.attr("width", 10)
.attr("height", 100)
.attr("fill", "teal");
groups.on("mouseover", function(d, i) {
d3.select(this).select("rect").transition()
.attr("width", 50);
groups.transition()
.attr("transform", function(e, j) {
if (i < j) {
return "translate(40,0)"
}
})
}).on("mouseout", function() {
groups.transition().attr("transform", "translate(0,0)");
rects.transition().attr("width", 10);
})
<script src="https://d3js.org/d3.v4.min.js"></script>
<svg></svg>
PS: don't do...
g_box.selectAll("rect").on("mouseover", function(d, i){
... because you won't get the correct index that way (which explain your comment). Instead of that, attach the event to the groups, and get the rectangle inside it.
I'm pretty sure d3 passes in the index as well as the data in the event listener.
So try
.on("mouseover", function(d,i)
where i is the index.
Also you can take a look at a fiddle i made a couple months ago, which is related to what you're asking.
https://jsfiddle.net/guanzo/h1hdet8d/1/
You can find the index usign indexOf(). The second argument in the event mouseover it doesn't show the index in numbers, it shows the data info you're working, well, you can pass this info inside indexOf() to find the number of the index that you need.
.on("mouseover", (event, i) => {
let index = data.indexOf(i);
console.log(index); // will show the index number
})

d3js update redraw stacked bar graph

I am trying to make a stacked bar graph through d3js and have it update when new data is passed through an update function. I call this update function to initially call the graph and it works fine. However, when I change the data and call it again, it erases all the "rect" elements from the graph (When I console log the data, it appears to be passing through). How can I make the graph be redrawn appropriately? I have tried experimenting with the .remove() statement at the beginning, but without it the data doesn't pass through when the bars are redrawn.
function update(my_data) {
svg.selectAll(".year").remove();
var year = svg.selectAll(".year")
.data(my_data)
.enter().append("g")
.attr("class", "year")
.attr("transform", function(d) { return "translate(" + x0(d.Year) + ",0)"; });
var bar = year.selectAll(".bar")
.data( function(d){ return d.locations; });
bar
.enter().append("rect")
.attr("class", "bar")
.attr("width", x0.rangeBand())
.attr("y", function(d) { return y(d.y1); })
.attr("height", function(d) { return y(d.y0) - y(d.y1); })
.style("fill", function(d) { return color(d.name); });
}
update(data);
It's hard to tell exactly what you're doing cause your question doesn't include the data or the DOM. It would help if you included a link to a work-in-progress jsFiddle or something.
If I had to guess what's going wrong, it looks like you're doing a nested join where each year gets bound to a g element and then each location gets bound to a rect inside each g element.
The issue is likely you are only specifying the enter behavior, but not the update behavior or the exit behavior. As a result, when you try to redraw, nothing updates and nothing exits - but new data elements will get added.
It would seem that is why you have to add the selectAll().remove() to get anything to redraw. By removing everything, all the data elements will trigger the enter condition and get added again.
Take a look at these tutorials to better understand how the enter/update/exit pattern works and how nested joins work.
General Update Pattern: https://bl.ocks.org/mbostock/3808218
Nested Selections: https://bost.ocks.org/mike/nest/
Also, here is a jsFiddle I wrote some time ago to demonstrate how to use nested selections and the general update pattern together:
https://jsfiddle.net/reblace/bWp8L/
var series = svg.selectAll("g.row").data(data, function(d) { return d.key; });
/*
* This section handles the "enter" for each row
*/
// Adding a g element to wrap the svg elements of each row
var seriesEnter = series.enter().append("g");
seriesEnter
.attr("class", "row")
.attr("transform", function(d, i){
return "translate(" + margin.left + "," + (margin.top + (span*i)) + ")";
})
.attr("opacity", 0).transition().duration(200).attr("opacity", 1);
// Adding a text label for each series
seriesEnter.append("text")
.style("text-anchor", "end")
.attr("x", -6)
.attr("y", boxMargin + (boxDim/2))
.attr("dy", ".32em")
.text(function(d){ return d.key; });
// nested selection for the rects associated with each row
var seriesEnterRect = seriesEnter.selectAll("rect").data(function(d){ return d.values; });
// rect enter. don't need to worry about updates/exit when a row is added
seriesEnterRect.enter().append("rect")
.attr("fill", function(d){ return colorScale(d)})
.attr("x", function(d, i){ return i*span + boxMargin; })
.attr("y", boxMargin)
.attr("height", boxDim)
.attr("width", boxDim);
/*
* This section handles updates to each row
*/
var seriesUpdateRect = series.selectAll("rect").data(function(d){ return d.values});
// rect update (Will handle updates after enter)
// rect enter
seriesUpdateRect.enter().append("rect")
.attr("x", function(d, i){ return i*span + boxMargin; })
.attr("y", boxMargin)
.attr("height", boxDim)
.attr("width", boxDim);
// rect enter + update
seriesUpdateRect
.attr("fill", function(d){ return colorScale(d)});
// Exit
seriesUpdateRect.exit();
/*
* This section handles row exit
*/
series.exit()
.attr("opacity", 1)
.transition().duration(200).attr("opacity", 0)
.remove();

d3.js Changing gridlines and blocks to be evenly spaced

I'm playing with the following d3 block http://bl.ocks.org/lakenen/8529857 which is a rendering of a candlestick-ish chart. Its output looks like this:
The data for each block is 1 day worth of financial stock numbers: the high, low, start, and close.
Typically, candlestick charts are different, though. Typically, the blocks are evenly spaced, and there is one gridline per day, and the x axis is labeled once per day. Here's an example of this on google finance:
And here's the code that renders the d3 chart from above:
var margin = 50;
var chart = d3.select("#chart")
.append("svg:svg")
.attr("class", "chart")
.attr("width", width)
.attr("height", height);
var y = d3.scale.linear()
.domain([d3.min(data.map(function(x) {return x["Low"];})), d3.max(data.map(function(x){return x["High"];}))])
.range([height-margin, margin]);
var x = d3.scale.linear()
.domain([d3.min(data.map(function(d){return d.timestamp;})), d3.max(data.map(function(d){ return d.timestamp;}))])
.range([margin,width-margin]);
chart.selectAll("line.x")
.data(x.ticks(10))
.enter().append("svg:line")
.attr("class", "x")
.attr("x1", x)
.attr("x2", x)
.attr("y1", margin)
.attr("y2", height - margin)
.attr("stroke", "#ccc");
chart.selectAll("line.y")
.data(y.ticks(10))
.enter().append("svg:line")
.attr("class", "y")
.attr("x1", margin)
.attr("x2", width - margin)
.attr("y1", y)
.attr("y2", y)
.attr("stroke", "#ccc");
chart.selectAll("text.xrule")
.data(x.ticks(10))
.enter().append("svg:text")
.attr("class", "xrule")
.attr("x", x)
.attr("y", height - margin)
.attr("dy", 20)
.attr("text-anchor", "middle")
.text(function(d){ var date = new Date(d * 1000); return (date.getMonth() + 1)+"/"+date.getDate(); });
chart.selectAll("text.yrule")
.data(y.ticks(10))
.enter().append("svg:text")
.attr("class", "yrule")
.attr("x", width - margin)
.attr("y", y)
.attr("dy", 0)
.attr("dx", 20)
.attr("text-anchor", "middle")
.text(String);
chart.selectAll("rect")
.data(data)
.enter().append("svg:rect")
.attr("x", function(d) { return x(d.timestamp); })
.attr("y", function(d) {return y(max(d.Open, d.Close));})
.attr("height", function(d) { return y(min(d.Open, d.Close))-y(max(d.Open, d.Close));})
.attr("width", function(d) { return 0.5 * (width - 2*margin)/data.length; })
.attr("fill",function(d) { return d.Open > d.Close ? "red" : "green" ;});
chart.selectAll("line.stem")
.data(data)
.enter().append("svg:line")
.attr("class", "stem")
.attr("x1", function(d) { return x(d.timestamp) + 0.25 * (width - 2 * margin)/ data.length;})
.attr("x2", function(d) { return x(d.timestamp) + 0.25 * (width - 2 * margin)/ data.length;})
.attr("y1", function(d) { return y(d.High);})
.attr("y2", function(d) { return y(d.Low); })
.attr("stroke", function(d){ return d.Open > d.Close ? "red" : "green"; })
}
I've tried tinkering with the .data(x.ticks(10)) values, which changes the number of ticks, but I'm not sure how to set that equal to the value of datapoints, and I'm also unsure of how exactly the d3.scale.linear().domain(...) stuff is changing the data before rendering begins.
So, how do I made the blocks evenly spaced so that I can make a gridline per block and a label per block?
The problem is that the graph you are trying to emulate doesn't have a linear x-axis based on time (it's missing days). You'll need to use a linear scale based on the number of data points and then custom set the label values.
I didn't really test this code so there may be bugs. However, this is how I would approach the problem.
// Create a formatter that given an index, will print the day number for the
// data at that index in data
var dayFormatter = d3.time.format('%d');
var dayAxisFormatter = function(d) {
return dayFormatter(new Date(data[d].timestamp));
}
// Create a formatter that given an index, will print the short month name
// along with the day number for the data at that index in data
var dayWithMonthFormatter = d3.time.format('%b %d');
var dayWithMonthAxisFormatter = function(d) {
return dayWithMonthFormatter(new Date(data[d].timestamp));
}
// Custom formatter to handle printing just the day number except for the first
// instance of the month, there we will print the short month and the day
// helper to create the formatter function that d3 accepts
function timeFormat(formats) {
return function(date) {
var i = formats.length - 1, f = formats[i];
while (!f[1](date)) f = formats[--i];
return f[0](date);
};
}
var firstDone = {}; // track the months so first instance gets month label
var tickFormatter = timeFormat([
[dayAxisFormatter, function(d) { return true; }],
[dayWithMonthFormatter, function(d) {
var month = (new Date(data[d].timestamp)).getMonth();
var result = !firstDone['m' + month];
firstDone['m' + month] = true;
return result;
}],
]);
// Set up a regular linear scale. This would normally just count up from
// 0 to d.length, but we'll use a custom formatter to print out our day
// numbers instead.
var x = d3.scale.linear()
.domain([0, d.length]) // set the domain to be from 0 to # of points
.range([margin,width-margin]);
// Set up the axis to use our customer formatter
var xAxis = d3.svg.axis()
.scale(x)
.tickSize(height)
.tickFormat(tickFormatter);
// Now when you go to draw your data, you need to remember that the
// underlying scale is based on the data index, not the data timestamp.
chart.selectAll("rect")
.data(data)
.enter().append("svg:rect")
.attr("x", function(d, i) { return x(i); })
...

d3.js - group 2 data values in a stacked bar chart

I have the following csv data,
date,scanned,unscanned,compid,sbu
01/2014,10,90,101,f&r
02/2014,55,40,101,f&r
03/2014,45,23,101,f&r
04/2014,65,35,101,f&r
05/2014,100,20,101,f&r
06/2014,50,30,101,f&r
07/2014,10,90,101,f&r
08/2014,22,48,101,f&r
09/2014,0,100,101,f&r
10/2014,3,97,101,f&r
11/2014,22,60,101,f&r
12/2014,57,37,101,f&r
01/2014,30,100,101,ip
02/2014,130,10,101,ip
Is there a way that we can combine the data for jan-2014 for both the f&r and ip sbu values and show the values in stacked bar. for e.g if i check a checkbox to group, i need to show scanned as 30+10=40 and unscanned as 100+90= 190 in a stack for jan 2014 in x-axis.
My code to build the stack bar is as follows:
var w = 960,
h = 500,
p = [20, 50, 30, 20],
x = d3.time.scale().range([1, 80]);
y = d3.scale.linear().range([0, h - p[0] - p[2]]),
z = d3.scale.ordinal().range(["#819FF7", "#CB491A"]),
parse = d3.time.format("%m/%Y").parse,
format = d3.time.format("%b-%y");
var xAxis=d3.svg.axis()
.scale(x)
.orient("bottom")
.ticks(d3.time.month, 1)
//.ticks(12)
xAxis.tickFormat(d3.time.format("%b-%y"));
/*var yAxis = d3.svg.axis()
.scale(y)
.ticks(12)
.orient("left");*/
var svg = d3.select("#container").append("svg:svg")
.attr("width", w)
.attr("height", h)
.append("svg:g")
.attr("transform", "translate(" + p[3] + "," + (h - p[2]) + ")");
d3.csv("scandata.csv", function(scan) {
// Transpose the data into layers by cause.
var scantypes = d3.layout.stack()(["scanned", "unscanned"].map(function(scans) {
return scan.map(function(d) {
return {x: parse(d.date), y: +d[scans],z:d.compid,typescan:scans};
});
}));
// Compute the x-domain (by date) and y-domain (by top).
x.domain(scantypes [0].map(function(d) { return d.x; }));
y.domain([0, d3.max(scantypes[scantypes .length - 1], function(d) { return d.y0 + d.y; })]);
// Add a group for each scan.
var cause = svg.selectAll("g.scan")
.data(scantypes)
.enter().append("svg:g")
.attr("class", "scan")
.style("fill", function(d, i) { return z(i); })
.style("stroke", function(d, i) { return d3.rgb(z(i)).darker(); });
// Add a rect for each date.
var rect = cause.selectAll("rect")
.data(Object)
.enter().append("svg:rect")
.attr("id", function(d,i) { return i + " comp " + d.z; })
.attr("x", function(d,i) {
if (i ==0)
{
return x(d.x) ;
}
else
{
return x(d.x);
}} )
.attr("y", function(d) { return -y(d.y0) - y(d.y); })
.attr("height", function(d) { return y(d.y); })
.attr("width", 30)//x.rangeBand()/2
.on("mouseover", function(d){
return tooltip.style("visibility", "visible")
.text((d.y))//d.typescan + " - " +
.style("left", (d3.event.pageX) + "px")
.style("top", (d3.event.pageY - 20) + "px"); ;})
.on("mousemove", function(d){
return tooltip.style("visibility", "visible")
.text((d.y)) //d.typescan + " - " +
.style("left", (d3.event.pageX) + "px")
.style("top", (d3.event.pageY - 20) + "px"); ;})
.on("mouseout", function(d){return tooltip.style("visibility", "hidden");})
.on("click", function(d){});
var tooltip = d3.select("#container")
.append("div")
.style("position", "absolute")
.style("z-index", "10")
.style("visibility", "visible")
.text("Scanned vs UnScanned")
.style("font", "Arial")
.style("color", "white")
.style("font-size", "14px");
//Add x-Axis
svg.append("g")
.attr("class", "x axis")
//.attr("transform", function(d) { return "translate(0,80)"; })
.call(xAxis)
// Add a label per date.
var label = svg.selectAll("text")
.data(x.domain())
.enter().append("svg:text")
.attr("x", function(d) { return x(d.x); })//x.rangeBand() / 4
.attr("y", 6)
.attr("text-anchor", "middle")
.attr("dy", ".71em")
.text(format);
// Add y-axis rules.
var rule = svg.selectAll("g.rule")
.data(y.ticks(5))
.enter().append("svg:g")
.attr("class", "rule")
.attr("transform", function(d) { return "translate(0," + -y(d) + ")"; });
rule.append("svg:line")
.attr("x2", w - p[1] - p[3])
.style("stroke", function(d) { return d ? "#fff" : "#000"; })
.style("stroke-opacity", function(d) { return d ? .7 : null; });
rule.append("svg:text")
.attr("x", -15)
.style("font-family","Arial 12px")
.attr("dy", ".25em")
.text(d3.format(",d"));
You seem to be confused about what the SVG should look like, and so don't know how to make it happen.
The bars in SVG are just rectangles. You need to tell them where they should be positioned (which is always defined by the top left corner of the bar) and how big they should be.
To get the bars to line up in a stacked graph, you need to figure out their position and size based on all the values for that stack.
I've created a very simplified example of a stacked bar chart using your data (just the scanned/unscanned data, I haven't separated things out by the sbu variable).
Here's the working example
Here's the code with comments:
var width = 400;
height = 500;
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
var xScale = d3.scale.ordinal()
.rangeRoundBands([0,width], 0.1);
var yScale = d3.scale.linear()
.range([height, 0]);
//note the inverted range, so that small values
//scale to the bottom of the SVG
var data = d3.csv.parse( d3.select("pre#data").text() );
//this just grabs the text from the preformatted block
//and parses it as if it was a csv file
//in your real code, you would use d3.csv(filename, callbackFunction)
//and the rest would be inside your callback function:
xScale.domain( data.map(function(d){return d.date;}) );
//the xScale domain is the list of all categorical values.
//The map function grabs all the date values from the data
//and returns them as a new array.
//I'm not worrying about parsing dates, since
//strings work fine with an ordinal scale
//(although you'd want to parse them if you wanted to reformat them).
yScale.domain( [0,
d3.max(data,
function(d){
return +d.scanned + +d.unscanned;
})
]);
//The yScale domain starts at 0 (since it's a bar chart)
//and goes to the maximum *total* value for each date.
//The d3.max function finds the maximum for an array
//based on the result returned by the function for each
//element of the array. This function just finds the sum
//of the scanned and unscanned values
//(after converting them from strings to numbers with "+").
var dateGroups = svg.selectAll("g")
//create an empty selection of groups
.data(data); //join to the data, each row will get a group
dateGroups.enter().append("g")
//create the actual <g> elements for each row of data
.attr("class", "dateGroup");
//give them a meaningful class
//Now, within each group create a rectangle
//for each category (scanned and unscanned).
//If you had lots of categories, you'd want to
//use a nested selection and a second data join.
//However, to do that you'd need to do a lot of
//data manipulation to create an array of
//separate data objects for each category.
//
//With only two categories, it's easier to just
//do each one separately, and let them inherit
//the data from the parent <g> element.
//For the bottom of the stack:
var bottom = dateGroups.append("rect")
.attr("class", "data scanned");
bottom.attr("y", function(d){
return yScale(+d.scanned);
} )
//y is the TOP of the rectangle
//i.e., the position of this data value
//on the scale
.attr("height", function(d){
return Math.abs( yScale(+d.scanned) - yScale(0) );
//The height of the rectangle is the difference between
//its data value and the zero line.
//Note that the yScale value of zero is
//bigger than the yScale value of the data
//because of the inverted scale, so we use
//absolute value to always get a positive height.
} );
//For the top of the stack:
var top = dateGroups.append("rect")
.attr("class", "data unscanned");
top.attr("y", function(d){
return yScale(+d.unscanned + +d.scanned);
} )
//y is the TOP of the rectangle
//i.e., the position on the scale of
//the *total* of the two data categories
.attr("height", function(d){
return Math.abs( yScale(+d.unscanned) - yScale(0) );
//The height of this bar is just based on
//its value. However, this could also be
//written as
//Math.abs(+yScale(+d.scanned + +d.unscanned)
// - yScale(+d.scanned) )
//i.e., as the difference between the total
//(top of the bar) and the other category's
//value (bottom of the bar)
} );
//The x value and width are the same for both bars
//so we can re-select all the rectangles and
//set these attributes at the same time:
dateGroups.selectAll("rect.data")
.attr("x", function(d){
return xScale(d.date);
})
.attr("width", xScale.rangeBand() );
//don't need a function for width,
//since it doesn't depend on the data
Once you are sure you understand what is happening at every step of that program, then you can start to add extra features like axes or tooltips. You will also be in a good position to adapt the code to work with many categories, although in that case you will probably want to create a sub-array of representing the data for each category, and use a nested selection to create the rectangles. That's the approach used by most stacked bar graph examples; they will hopefully be easier to understand after working with this very simplified version.
Edit
The above solution works if you know that you only have two values in each stack, with the data for both values from the same row of the data table. If you might have many bars in each stack, and/or if they come from multiple rows of the data table, you will want to use a nested selection to match the data to individual bars.
In order to use the nested selection approach, you first have to do some manipulation to your data. You need to get it into nested array format. The outer array has to represent each stack, and each stack data object has to include a sub-array representing each bar.
How you make the nested array depends on your original data format. When the values that you want to stack are in different rows, the d3.nest operator can group them together into sub-arrays. When the stacked values are different numbers from the same row of the data table, you have to use a forEach() function to loop through all the rows of your data and construct an array from each.
In your example, you want to do both, so we're going to combine a nesting operation with a forEach operation. At the same time, we're going to calculate the running totals for the stack: in order to position each bar correctly, we need to know not only its own count, but also the total count of all values under it in the stack.
Here's a working fiddle
The data manipulation code is
/*Nest data by date string */
var nestFunction = d3.nest().key( function(d){return d.date;} );
var nestedData = nestFunction.entries(data);
var maxTotal = 0; //maximum count per date,
//for setting the y domain
nestedData.forEach(function(dateGroup) {
//for each entry in the nested array,
//each of which contains all the rows for a given date,
//calculate the total count,
//and the before-and-after counts for each category.
dateGroup.date = dateGroup.key;
//just using the original strings here, but you could
//parse the string date value to create a date object
dateGroup.bars = [];
//create an array to hold one value for each bar
//(i.e., two values for each of the original rows)
var total = 0; //total count per date
dateGroup.values.forEach(function(row) {
//the values array created by the nest function
//contians all the original row data objects
//that match this date (i.e., the nesting key)
//create an object representing the bar for
//the scanned count, and add to the bars array
dateGroup.bars.push(
{date:dateGroup.date,
type: "scanned",
count: +row.scanned,
compid: row.compid,
sbu: row.sbu,
y0: total, //total value *before* this bar
y1: (total = total + +row.scanned) //new total
}
);
//create an object representing the bar for
//the UNscanned count, and add to the bars array
dateGroup.bars.push(
{date:dateGroup.date,
type: "unscanned",
count: +row.unscanned,
compid: row.compid,
sbu: row.sbu,
y0: total, //total value *before* this bar
y1: (total = total + +row.unscanned) //new total
}
);
});
maxTotal = Math.max(maxTotal, total); //update max
});
If you didn't want to stack certain types of bars together -- for example, if you wanted to keep the values from different compids in different stacks -- then you would include that parameter as a second key to the nesting function. Values are only nested together if they match on all the nesting keys. Of course, then you'd also have to amend your x-scale to separate out the stacks by both keys. Look up examples of grouped bar charts for how to do that.
Once you have the properly nested data, you join the outer array (the array of nest objects) to <g> elements representing each stack, and then create a nested selection of rectangles within each group and join the inner array (the bar data) to it:
var dateGroups = svg.selectAll("g")
//create an empty selection of groups
.data(nestedData); //join to the data,
//each nested object (i.e., date) will get a group
dateGroups.enter().append("g")
//create the actual <g> elements for each row of data
.attr("class", "dateGroup");
//give them a meaningful class
//Now, within each group create a rectangle
//for each category from the "bars" array created earlier.
//This uses a nested selection, since we don't know
//how many bars there will be for a given date.
var bars = dateGroups.selectAll("rect")
.data( function(d) {return d.bars;})
//the 'd' value passed in is the data for each
//dateGroup, each of which will now have a
//nested selection of bars
bars.enter().append("rect"); //create the rectangles
bars.attr("class", function(d){
//assign classes for all the categorical values
//(after stripping out non-word characters)
//so they can be styled with CSS
var specialchars = /\W+/g;
//regular expression to match all non-letter, non-digit characters
return ["data",
"type-" + d.type.replace(specialchars, ""),
"compid-" + d.compid.replace(specialchars, ""),
"sbu-" + d.sbu.replace(specialchars, "")
].join(" "); //class list is space-separated
})
.attr("y", function(d){
return yScale(d.y1);
//y is the TOP of the rectangle
//i.e., the position of the *total* value
//for this bar and all others under it in the stack
} )
.attr("height", function(d){
return Math.abs( yScale(d.y1) - yScale(d.y0) );
//the height of the rectangle is the difference
//between the total *after*
//this value is added to the stack
// (top of the bar, y1)
//and the total *before* it is added
// (bottom of the bar, y0)
//Since this is a linear scale, this could also
//be written as
//Math.abs( yScale(d.count) - yScale(0) )
//i.e., as the difference between
//its data value and zero line.
//Note the use of absolute value to
//compensate for a possibly inverted scale.
} )
.attr("x", function(d){
return xScale(d.date);
})
.attr("width", xScale.rangeBand() )
//don't need a function for width,
//since it doesn't depend on the data
.append("title") //add a tooltip title
.text(function(d) {
return d.sbu + ", " +d.type +":" + d.count;
});

Resources