Sorting a stacked bar chart d3js - javascript

In this plunker I have the same problem as I had in my previous question where my data.sort function is technically sorting the bars as they should but not in the desired way.
Just like last time, I'd like em to be sorting like this rather than the way they're sorting now.
I've tried putting the data.sort alongside the x.domain and axis--x in various ways without any success and I'm all out of ideas.
Any help is as always appreciated!

updated plunker
You need to use key functions .data(d=>d,d=>d.data.State);, so that d3 knows which data is new, old, or existing. It's recommended to always use key functions, unless you want the data to be keyed by array index.
You also need to apply the x transformation to the existing bars.
Even though your example doesn't have new data after the initial data, it's still good practice to merge the enter selection with the update selection, and then run the transition.
let bars = g.selectAll("g.layer").selectAll("rect")
.data(d=>d,d=>d.data.State);
bars = bars.enter().append("rect")
.attr("width", x.bandwidth()).merge(bars)
.transition().duration(Globalvar.durations)
.attr("x", function(d) { return x(d.data.State); })
.attr("y", function(d) { return y(d[1]); })
.attr("height", function(d) { return y(d[0]) - y(d[1]); });
notice how the merge code is more concise than writing the transition twice for both the enter and update selections.
let bars = g.selectAll("g.layer").selectAll("rect")
.data(function(d) { return d; });
bars.enter().append("rect")
.attr("width", x.bandwidth())
.transition().duration(Globalvar.durations)
.attr("x", function(d) { return x(d.data.State); })
.attr("y", function(d) { return y(d[1]); })
.attr("height", function(d) { return y(d[0]) - y(d[1]); });
bars.transition().duration(Globalvar.durations)
.attr("y", function(d) { return y(d[1]); })
.attr("height", function(d) { return y(d[0]) - y(d[1]); });

Related

d3 y axis label position

I have a grouped bar chart design that has each bar amount immediately above the bar instead of the y axis bar on the far left.
I have added the text like so...
svg.selectAll(".text")
.data(data)
.enter()
.append("text")
.attr("class","label")
.attr("x", function(d) { return x1(d.rate) })
//.attr("y", function(d) { return y(d.value) + 1; })
.attr("dy", ".75em")
.text(function(d) {return d.value; });
But i cant seem to obtain the x and y values according to the bar position and the actual text of the d.value isn't getting inserted.
Sample here:
https://jsfiddle.net/6p7hmef1/7/
That's because the data bound to the texts that you're adding isn't right.
If you add a console.log as follows, you'll be able to see why the labels aren't being inserted. Check for the outputs in console. Console log fiddle
.text(function(d) {console.log(d); return d.value; });
One approach would be to append the texts to the bar groups ("slice" in your case). Just like you do for the bars. Here's a fiddle doing this:
JS Fiddle Demo
slice.selectAll(".text")
.data(function(d) { return d.values; })
.enter()
.append("text")
.attr("class","label");
slice.selectAll('text.label')
.attr("x", function(d) { return x1(d.rate)+ x1.rangeBand()/3 })
.attr("y", function(d) { return y(d.value) + 1; }).attr('dy', '-0.4em')
.text(function(d) {return d.value; });
This might look weird as the texts are shown before the transition of the bars. So changing the value of texts once the bars are shown seems like the right approach to me. (dx and dy attributes can be adjusted as per the requirement)
Here's the final fiddle:
JS FIDDLE
I'm using the "end" callback for every transition and changing the text values accordingly.
.each('end', function () {
d3.select(this.parentNode).selectAll('text.label')
.attr("x", function(d) { return x1(d.rate)+ x1.rangeBand()/3 })
.attr("y", function(d) { return y(d.value) + 1; }).attr('dy', '-0.4em')
.text(function(d) {return d.value; });
})
Hope this helps. :)
Let me know if any part of the code isn't understandable.

Circles on top of bar chart d3.js

I am making grouped bar chart based on Mike Bostock's tutorial.
I can't figure out how to put circles on top of my bars to act as tooltip when hovering, just like in this tutorial except it's on bars and not on a line.
I tried appending the circles like this :
svg.selectAll("dot")
.data(data)
.enter().append("circle")
.attr("r", 5)
.attr("cx", function(d) { return x1(d.name); })
.attr("cy", function(d) { return y(d.value); })
});
But I get NaN values. I am very confused about which variable I should use to get the right cx and cy.
Here is my code.
Any ideas ?
Thank you
You will get NaN values since your data join is not correct, you are trying to get values that are not currently present in your data. In order to get those values you would need to make a reference to data.years.
Here is my approach:
// Inheriting data from parent node and setting it up,
// add year to each object so we can make use for our
// mouse interactions.
year.selectAll('.gender-circles')
.data(function(data) {
return data.years.map(function(d) {
d.year = data.year;
return d;
})
})
.enter().append('circle')
.attr("class", function(d) {
return "gender-circles gender-circles-" + d.year;
})
.attr("r", 10)
.attr('cx', function(d) {
console.log(d)
return x1(d.name) + 6.5;
})
.attr('cy', function(d) {
return y(d.value) - 15;
})
.style('display', 'none'); // default display
// ....
// Using an invisible rect for mouseover interactions
year.selectAll('.gender-rect-interaction')
.data(function(d) { // Inheriting data from parent node and setting it up
return [d];
})
.enter().append('rect')
.attr("width", x0.rangeBand()) // full width of x0 rangeband
.attr("x", function(d) {
return 0;
})
.attr("y", function(d) {
return 0;
})
.attr("height", function(d) { // full height
return height;
})
.style('opacity', 0) // invisible!
.on('mousemove', function(d) { // show all our circles by class
d3.selectAll('.gender-circles-' + d.year)
.style('display', 'block');
})
.on('mouseout', function(d) { // hide all our circles by class
d3.selectAll('.gender-circles-' + d.year)
.style('display', 'none');
});
Working plnkr: https://plnkr.co/edit/oH4KXdxdIW82nLGv46NI?p=preview

D3JS refreshing graph for new Data

I'm trying to update the graph with a new csv file (data2.csv) by calling update but the graph isnt changing. The code as below is the function that would be called when I click a button.
Plnkr is the sample code..
Do advice!
http://plnkr.co/edit/pOYqmaOxy1lmY82jlhfA
<script>
function update(){
d3.csv("data2.csv", function(error, data) {
if (error) throw error;
var ageNames = d3.keys(data[0]).filter(function(key) { return key !== "State"; });
data.forEach(function(d) {
d.ages = ageNames.map(function(name) { return {name: name, value: +d[name]}; });
});
x0.domain(data.map(function(d) { return d.State; }));
x1.domain(ageNames).rangeRoundBands([0, x0.rangeBand()]);
y.domain([0, d3.max(data, function(d) { return d3.max(d.ages, function(d) { return d.value; }); })]);
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + height + ")")
.call(xAxis);
svg.append("g")
.attr("class", "y axis")
.call(yAxis)
.append("text")
.attr("transform", "rotate(-90)")
.attr("y", 6)
.attr("dy", ".71em")
.style("text-anchor", "end")
.text("Population");
var state = svg.selectAll(".state")
.data(data)
.enter().append("g")
.attr("class", "g")
.attr("transform", function(d) { return "translate(" + x0(d.State) + ",0)"; });
state.selectAll("rect")
.data(function(d) { return d.ages; })
.enter().append("rect")
.attr("width", x1.rangeBand())
.attr("x", function(d) { return x1(d.name); })
.attr("y", function(d) { return y(d.value); })
.attr("height", function(d) { return height - y(d.value); })
.style("fill", function(d) { return color(d.name); });
var legend = svg.selectAll(".legend")
.data(ageNames.slice().reverse())
.enter().append("g")
.attr("class", "legend")
.attr("transform", function(d, i) { return "translate(0," + i * 20 + ")"; });
legend.append("rect")
.attr("x", width - 18)
.attr("width", 18)
.attr("height", 18)
.style("fill", color);
legend.append("text")
.attr("x", width - 24)
.attr("y", 9)
.attr("dy", ".35em")
.style("text-anchor", "end")
.text(function(d) { return d; });
});
}
</script>
You say you want to update an existing graph with your update function and new data coming from an csv after some event occurs, correct?
D3 stands for Data Driven Documents, so your data is very important when drawing graphs. D3 works with selections (or collections if that works better for you) based on the data you want to display.
Say you want a barchart displaying the following array: [10,20,30]. The height of the bars is in function with the data in the array.
creating new elements
If you dont have bar elements on the page already, that means you will need to 'append' them to the graph. This is usually done with a code pattern resembling like:
svg.selectAll("rect")
.data(function(d) { return d.ages; })
.enter().append("rect")
.attr("width", x1.rangeBand())
.attr("x", function(d) { return x1(d.name); })
.attr("y", function(d) { return y(d.value); })
.attr("height", function(d) { return height - y(d.value); })
.style("fill", function(d) { return color(d.name); });
With this code, you take the svg variable (which is basically a d3 selection containing one element, the svg) and you select all "rect" elements on the page but inside the svg element. There are none at this very moment, remember, you are going to create them. After the selectAll, you see the data function which specifies the data that will be bound to the elements. Your array contains 3 pieces of data, that means that you expect ot see 3 bars. How will D3 know? It will because of the .enter() (meaning: which elements are not on the graph yet?) and the .append(element) functions. The enter function basically means: In my current d3 selection (being selecAll('rect') ), how many of the specified elements do i need to append? Since you current selection is empty (you dont have 'rect' elements yet), and d3 spots 3 pieces of data in your data function, using .append() it will create and append 3 elements for you. With the attr fuctions you can specify how the elements will look like.
updating elements
Suppose my array of [10,20,30] suddenly changes to [40,50,60]. notice something very important here, my array still contains 3 pieces of data! It is just their value that changed!
I would really want to see my bars reflecting this update! (and i think your case matches this one).
But if I use this pattern again, what will happen?
state.selectAll("rect")
.data(function(d) { return d.ages; })
.enter().append("rect")
.attr("width", x1.rangeBand())
...
The state.selectAll("rect") selection contains 3 elements, d3 checks how many pieces of data you have (still 3) and it sees that it doesnt need to append anything!
Does that mean you cannot update with D3? Absolutely not! It is just much simpler then you would think :-).
If i would want to update my bars so that their height reflects the new values of my data, I should do it like this:
state.selectAll("rect")
.data(function(d) { return d.ages; })
.attr("height", function(d) { return height - y(d.value); });
Basically, I select all my rects, I tell d3 what data i am working on and then I simply tell d3 to alter the height of of my rect. You can even do it with a transition! (more info on transitions here ). I think this is what you need to do, instead of appending the "rect" elements again.
Updating elements, part 2
continuing with my example, what do to if my array all of a sudden wouuld be like this: [100,200,300, 400]? Note that my values changed again BUT there is an extra element there!!
Well, when handling the event (for example a click on a button, or a submit of data) that changes the data, you need to tell D3 that it will need to append something and update the existing bars. This can simply be done by doing coding both patterns:
state.selectAll("rect")
.data(function(d) { return d.ages; })
.enter().append("rect")
.attr("width", x1.rangeBand())
.attr("x", function(d) { return x1(d.name); })
.attr("y", function(d) { return y(d.value); })
.attr("height", function(d) { return height - y(d.value); })
.style("fill", function(d) { return color(d.name); });
state.selectAll("rect")
.data(function(d) { return d.ages; })
.attr("height", function(d) { return height - y(d.value); });
Removing elements
What if my data array would suddenly only consist of only 2 pieces of data: [10,20] ?
Just like there is a function for telling d3 that it needs to append something, you can tell it that it what to do with elements that dont seem to have data to be bound on anymore:
svg.selectAll("rect")
.data(function(d) { return d.ages; })
.exit().remove();
The exit function matches the amount of pieces of data you have vs the amount of selected elements. The exit function then tells d3 to drop those elements.
I hope this was helpfull. It is a bit of a basic explanation (its a little more complicated then that) but I had to hurry, so if there should be questions or errors, please tell me.
d3.csv("data2.csv", function(error, data) {
if your server is caching this reference - try a Math.random() :D
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/random
This will force(ish) a refresh of data - could be costly so triage according to your needs via a serverside process
edit:
d3.csv("data2.csv?=" + (Math.random() * (100 - 1) + 1), function(error, data) {
would be the alteration. Its sloppy but illustrates how to suggestively force a cache refresh
You can do it like this:
Make a buildMe() function which makes the graph.
function buildMe(file) {//the file name to display
d3.csv(file, function(error, data) {
if (error) throw error;
var ageNames = d3.keys(data[0]).filter(function(key) {
return key !== "State";
});
data.forEach(function(d) {
d.ages = ageNames.map(function(name) {
return {
name: name,
value: +d[name]
};
});
});
x0.domain(data.map(function(d) {
return d.State;
}));
x1.domain(ageNames).rangeRoundBands([0, x0.rangeBand()]);
y.domain([0, d3.max(data, function(d) {
return d3.max(d.ages, function(d) {
return d.value;
});
})]);
svg.selectAll("g").remove();//remove all the gs within svg
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + height + ")")
.call(xAxis);
svg.append("g")
.attr("class", "y axis")
.call(yAxis)
.append("text")
.attr("transform", "rotate(-90)")
.attr("y", 6)
.attr("dy", ".71em")
.style("text-anchor", "end")
.text("Population");
var state = svg.selectAll(".state")
.data(data)
.enter().append("g")
.attr("class", "g")
.attr("transform", function(d) {
return "translate(" + x0(d.State) + ",0)";
});
state.selectAll("rect")
.data(function(d) {
return d.ages;
})
.enter().append("rect")
.attr("width", x1.rangeBand())
.attr("x", function(d) {
return x1(d.name);
})
.attr("y", function(d) {
return y(d.value);
})
.attr("height", function(d) {
return height - y(d.value);
})
.style("fill", function(d) {
return color(d.name);
});
var legend = svg.selectAll(".legend")
.data(ageNames.slice().reverse())
.enter().append("g")
.attr("class", "legend")
.attr("transform", function(d, i) {
return "translate(0," + i * 20 + ")";
});
legend.append("rect")
.attr("x", width - 18)
.attr("width", 18)
.attr("height", 18)
.style("fill", color);
legend.append("text")
.attr("x", width - 24)
.attr("y", 9)
.attr("dy", ".35em")
.style("text-anchor", "end")
.text(function(d) {
return d;
});
});
}
Then on Load do this buildMe("data.csv");
On button click do this
function updateMe() {
console.log("Hi");
buildMe("data2.csv");//load second set of data
}
Working code here
Hope this helps!

Bug in d3.js Stacked chart morphing

I've created a stacked chart animation/update app. However there appears to be NaN values being passed into the y and height variables. I am unsure as to what is wrong. If you toggle the data the charts eventually fill up.
jsFiddle
but the problem may occur first in setting the yaxis
svg.select("g.y")
.transition()
.duration(500)
.call(methods.yAxis);
It looks like something goes wrong in the bar rect enter/exit code.
//_morph bars
var bar = stacks.selectAll("rect")
.data(function(d) {
return d.blocks;
});
// Enter
bar.enter()
.append("rect")
.attr("class", "bar")
.attr("y", function(d) { return methods.y(d.y1); })
.attr("width", methods.x.rangeBand())
.style("fill", function(d) { return methods.color(d.name); });
// Update
bar
.attr("y", methods.height)
.attr("height", initialHeight)
.attr("width", methods.x.rangeBand())
.transition()
.duration(500)
.attr("x", function(d) { return methods.x(d.Label); })
.attr("width", methods.x.rangeBand())
.attr("y", function(d) { return methods.y(d.y1); })
.attr("height", function(d) { return methods.y(d.y0) - methods.y(d.y1); })
// Exit
bar.exit()
.transition()
.duration(250)
.attr("y", function(d) { return methods.y(d.y1); })
.attr("height", function(d) { methods.y(d.y0) - methods.y(d.y1); })
.remove();
//__morph bars
I've managed to narrow down the problem to the setDBlock function.
It appears if another chart has the same set of data, it takes on additional object parameters inside the dblock obj.
http://jsfiddle.net/XnngU/44/
I'm not sure at this stage as to how to clean it up. But I have isolated this via a legend and a function.
setDBlocks: function(incomingdata){
var data = incomingdata.slice(0);
methods.color.domain(d3.keys(data[0]).filter(function(key) { return key !== "Label"; }));
data.forEach(function(d) {
console.log("D", d);
var y0 = 0;
if(d["blocks"] == undefined){
d.blocks = methods.color.domain().map(function(name) {
var val = d[name];
if(isNaN(val)){
val = 0;
}
return {name: name, values: val, y0: y0, y1: y0 += +val};
});
}
d.total = d.blocks[d.blocks.length - 1].y1;
});
}
I've fixed the anomaly by deleting data in the update function. I'm not sure why though the data is not unique - it looks like if the data is the same - as the last chart - it gets modified accordingly and used again for its next chart. Is there a better way of cleaning this up - I've tried to keep objects unique and clean by cloning/splicing but maybe that is contributing towards the problem.
delete d.blocks;
delete d.total;
http://jsfiddle.net/XnngU/53/
update: function(data){
methods.el = this;
var selector = methods.el["selector"];
data.forEach(function(d) {
delete d.blocks;
delete d.total;
});
methods.animateBars(selector, data);
}

Stacked bar chart transition

I'm trying to get the bar transition one by one in the horizontal stacked bar chart. But each bar is starting at the same time.
rects = groups.selectAll('stackedBar')
.data(function(d,i) {
console.log("data", d, i);
return d;
})
.enter()
.append('rect')
.attr('class','stackedBar')
.attr('x', function(d) { return xScale(d.x0); })
.attr('y', function(d, i) {return yScale(d.y); })
.attr('height', function(d) { return yScale.rangeBand(); })
.attr('width', 0)
.transition()
.delay(function(d, i){
console.log('hi', d, i);
return i * 500;
})
.attr("width", function(d) { return xScale(d.x); })
.attr("x", function(d) { return xScale(d.x0); })
.duration(1000);
How can i make it animate one by one? Thanks!
jsFiddle
You're almost there -- you need to use .delay() to achieve this, as you're doing already. The only problem is that you're using a nested selection (i.e. rects within gs) and the index you get is that of the inner selection. This is always 0 because there's only one rect per g.
To make it work, reference the secret third argument in a nested selection, which is the index within the data passed to the parent:
.delay(function(d,i,j){console.log('hi',d,j); return j*500;})
This will give you the index of the g element. Complete example here.

Categories