Smoothen transition for order of bars in d3 chart - javascript

I am allowing users to order the data in their bar chart either chronologically or in descending order from highest to lowest based on a metric of their choosing.
The height of the bars themselves are smoothly transitioning, but when descending order is chosen, the order of the array of data changes, meaning the order of the bars need to change as well. Right now, the new data in this case just flashes its update (without any smooth animation). Where would I build in this additional transition? Or even better, how would I target this?
scope.render = function(data){
if (scope.metric !== "") {
var maximumY = d3.max(data, function(d) {
return eval('d.' + scope.metric);
});
x.domain(data.map(function(d) { return d.id; }));
y.domain([-(maximumY * .01), d3.max(data, function(d) { return eval('d.' + scope.metric); })]);
chart.select(".x.axis").remove();
chart
.append("g")
.append("line")
.attr('x1',0)
.attr('x2',width)
.attr('y1',height )
.attr('y2',height)
.attr('stroke-width','2')
.attr("class", "domain");
chart.select(".y.axis").remove();
chart.append("g")
.attr("class", "y axis")
.call(yAxis)
.append("text")
.attr("transform", "rotate(-90)")
.attr("y", -40)
.attr('class','label')
.attr("x", -height)
.attr("dy", ".71em")
.style("text-anchor", "begin")
.text(scope.label);
var bar = chart.selectAll(".bar")
.data(data, function(d) { return d.id; });
// new data:
bar.enter()
.append("g")
.attr("class", "bar-container")
.append("rect")
.attr("class", "bar")
.attr('fill','#4EC7BD')
.attr("x", function(d) { return x(d.id); })
.attr("y", function(d) { return y(eval('d.'+scope.metric)); })
.attr("height", function(d) { return height - y(eval('d.'+scope.metric)); })
.attr("width", x.rangeBand())
.on('click', function(d){
scope.showDetails(d, eval('d.'+scope.metric))
})
// removed data:
bar.exit().remove();
// updated data:
bar
.transition()
.duration(750)
.attr("y", function(d) { return y(eval('d.'+scope.metric)); })
.attr("height", function(d) { return height - y(eval('d.'+scope.metric)); });
}
};
Let me know if there is anything additional I can provide.

Related

d3 v4 getting data for bar graph tooltip

I am able to get a tooltip to work by defining the text but for the life of me I cannot get the tooltip to print text from a data file. I feel like I am messing up defining d.value even though it is being graphed just fine but I also wonder if there is something with v4 that I don't know about. I've tried moving the var statement all over the place but nothing seems to help. It usually ends up being a "Cannot read property of undefined" error. Any thoughts on what I'm doing wrong? Thanks.
var tooltip = d3.select("body")
.append("div")
.style("position", "absolute")
.style("z-index", "10")
.style("visibility", "hidden")
.text("This works just fine");
/* .text(function(d) { return d.value; }); */
d3.tsv("15.tsv", type, function(error, data) {
if (error) throw error;
x.domain([0, d3.max(data, function(d) { return d.value; })]);
y.domain(data.map(function(d) { return (d.gamedate); }))
.paddingInner(0.1)
.paddingOuter(0.5);
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + height + ")")
.call(xAxis)
.append("text")
.attr("class", "label")
.attr("transform", "translate(" + width + ",0)")
.attr("y", 15)
.style("text-anchor", "end")
.text("Game Score");
svg.append("g")
.attr("class", "y axis")
.call(yAxis);
svg.selectAll(".bar")
.data(data)
.enter().append("rect")
.attr("class", "bar")
.attr("x", 0)
.attr("height", y.bandwidth())
.attr("y", function(d) { return y(d.gamedate); })
.attr("width", function(d) { return x(d.value); })
.on("mouseover", function(d){
return tooltip.style("visibility", "visible");})
.on("mousemove", function(d){return tooltip.style("top", (event.pageY-10)+"px").style("left",(event.pageX+400)+"px");})
.on("mouseout", function(d){return tooltip.style("visibility", "hidden");})
I think the main issue is that event variable is not defined in the scope of the mousemove function. You can get the mouse values by doing the following:
.on("mousemove", function(d) {
const [xMouse, yMouse] = d3.mouse(this);
tooltip.style("top", (yMouse) + "px")
.style("left", (xMouse) + "px")
.text(`Gamedate ${d.gamedate} with value ${d.value}`)
})
I made a JSbin in order to show how I got it to work, the data is mocked.

Data not being redrawn after being filtered on d3 bar chart

I've been trying to add a filter to a bar chart. The bar is removed when I click on the legend, but when I try to reactivate the bar is not being redrawn.
Here is a plnk;
http://plnkr.co/edit/GZtErHGdq8GbM2ZawQSD?p=preview
I can't seem to work out the issue - if anyone could lend a hand?
Thanks
JS code;
// load the data
d3.json("data.json", function(error, data) {
var group = [];
function updateData() {
group = [];
var organization = data.organizations.indexOf("Org1");
var dateS = $("#selectMonth").text()
for (var country = 0; country < data.countries.length; country++) {
var date = data.dates.indexOf(dateS);
group.push({
question: data.organizations[organization],
label: data.countries[country],
value: data.values[organization][country][date]
});
}
}
function draw(create) {
updateData();
x.domain(group.map(function(d) {
return d.label;
}));
y.domain([0, 100]);
// add axis
// Add bar chart
var bar = svg.selectAll("rect")
.data(group);
if (create) {
bar
.enter().append("rect")
.attr('x', function(d) {
return x(d.label);
})
.attr('y', function(d) {
return y(d.value);
})
.attr('width', x.rangeBand())
.attr('height', function(d) {
return height - y(d.value);
});
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + height + ")")
.call(xAxis)
.selectAll("text")
.style("text-anchor", "end")
.attr("dx", "-.8em")
.attr("dy", "-.55em")
.attr("transform", "rotate(-90)");
svg.append("g")
.attr("class", "y axis")
.call(yAxis)
.append("text")
.attr("transform", "rotate(-90)")
.attr("y", 5)
.attr("dy", ".71em")
.style("text-anchor", "end")
.text("Value");
}
bar
.transition()
.duration(750)
.attr("y", function(d) {
return y(d.value);
})
.attr("height", function(d) {
return height - y(d.value);
});
// Existing code to draw y-axis:
var legendGroups = d3.select("#legend")
.selectAll(".legendGroup")
.data(group, function(d) {
return d.label; // always try and use a key function to uniquely identify
});
var enterGroups = legendGroups
.enter()
.append("g")
.attr("class", "legendGroup");
legendGroups
.exit()
.remove();
legendGroups
.attr("transform", function(d, i) {
return "translate(10," + (10 + i * 15) + ")"; // position the whole group
});
enterGroups.append("text")
.text(function(d) {
return d.label;
})
.attr("x", 15)
.attr("y", 10);
enterGroups
.append("rect")
.attr("width", 10)
.attr("height", 10)
.attr("fill", function(d) {
return "#bfe9bc";
})
.attr("class", function(d, i) {
return "legendcheckbox " + d.label.replace(/\s|\(|\)|\'|\,|\.+/g, '')
})
.on("click", function(d) {
d.active = !d.active;
d3.select(this).attr("fill", function(d) {
if (d3.select(this).attr("fill") == "#cccccc") {
return "#bfe9bc";
} else {
return "#cccccc";
}
})
var result = group.filter(function(d) {
return $("." + d.label.replace(/\s|\(|\)|\'|\,+/g, '')).attr("fill") != "#cccccc"
})
x.domain(result.map(function(d) {
return d.label;
}));
bar
.select(".x.axis")
.transition()
.call(xAxis);
bar
.data(result, function(d) {
return d.label.replace(/\s|\(|\)|\'|\,|\.+/g, '')
})
.enter()
.append("rect")
.attr("class", "bar")
bar
.transition()
.attr("x", function(d) {
return x(d.label);
})
.attr("width", x.rangeBand())
.attr("y", function(d) {
return y(d.value);
})
.attr("height", function(d) {
return height - y(d.value);
});
bar
.data(result, function(d) {
return d.label.replace(/\s|\(|\)|\'|\,|\.+/g, '')
})
.exit()
.remove()
});
}
The bar disappear because you totally remove it with
.remove()
What you can do is hide the element when not selected like that :
d3.selectAll('.graph').selectAll("rect").attr("visibility", function(e) {
return e.active ? "hidden" : "";
})
See http://plnkr.co/edit/2UWaZOsffkw1vkdIJuSA?p=preview

Realtime d3 bar chart with custom bar width

I want to add new bars to existing d3 bar chart and make it real time graph.
I can see the bars are getting updated but labels are not aligning themselves when the bars rescales.
var margin = {
top: 20,
right: 20,
bottom: 30,
left: 40
},
width = 960 - margin.left - margin.right,
height = 500 - margin.top - margin.bottom;
var x = d3.scale.ordinal()
.rangeRoundBands([0, width], .1);
var y = d3.scale.linear()
.range([height, 0]);
var xAxis = d3.svg.axis()
.scale(x)
.orient("bottom");
var yAxis = d3.svg.axis()
.scale(y)
.orient("left")
.ticks(10, "%");
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 + ")");
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + height + ")")
svg.append("g")
.attr("class", "y axis")
.append("text") // just for the title (ticks are automatic)
.attr("transform", "rotate(-90)") // rotate the text!
.attr("y", 6)
.attr("dy", ".71em")
.style("text-anchor", "end")
.text("Frequency");
function draw(data) {
x.domain(data.map(function(d) {
return d.letter;
}));
y.domain([0, d3.max(data, function(d) {
return d.frequency;
})]);
var labels = svg
.selectAll(".bartext")
.data(data, function(d) {
return d.letter;
});
labels
.exit()
.remove();
labels
.enter()
.append("text")
.attr("class", "bartext")
.attr("text-anchor", "middle")
.attr("fill", "black")
.attr("x", function(d, i) {
return x(d.letter) + 7.5;
})
.attr("y", function(d, i) {
return height + 15;
})
.text(function(d, i) {
return d.letter;
});
svg.select(".y.axis").transition().duration(300).call(yAxis)
var bars = svg.selectAll(".bar").data(data, function(d) {
return d.letter;
})
bars.exit()
.transition()
.duration(300)
.remove();
bars.enter().append("rect")
.attr("class", "bar");
bars.transition().duration(300).attr("x", function(d) {
return x(d.letter);
})
.attr("width", 15)
.attr("y", function(d) {
return y(d.frequency);
})
.attr("height", function(d) {
return height - y(d.frequency);
});
}
var data1 = [{
"letter": 'A',
"frequency": .00167
}];
var data2 = [{
"letter": 'A',
"frequency": .01167
},{
"letter": 'I',
"frequency": .01477
}];
draw(data1);
setTimeout(function() {
draw(data2);
}, 2000);
https://jsfiddle.net/foh7cgst/
Here's the relevant part of the selection.enter() documentation:
var update_sel = svg.selectAll("circle").data(data)
update_sel.attr(/* operate on old elements only */)
update_sel.enter().append("circle").attr(/* operate on new elements
only */)
update_sel.attr(/* operate on old and new elements */)
update_sel.exit().remove() /* complete the enter-update-exit pattern
*/
As you can see, when you append to an enter selection, the operations that follow only target the new elements that were appended.
If you want to target both new and old elements, you should operate on the update selection after entering the nodes.
So, using your example code that is inside the draw function, this:
labels.enter().append("text")
.attr("class", "bartext")
.attr("text-anchor", "middle")
.attr("fill", "black")
.attr("x", function(d, i) {
return x(d.letter) + 7.5;
})
.attr("y", function(d, i) {
return height + 15;
})
.text(function(d, i) {
return d.letter;
});
Should be changed to this:
labels.enter().append("text")
.attr("class", "bartext")
.attr("text-anchor", "middle")
.attr("fill", "black")
.attr("y", height + 15);
labels
.attr("x", function(d) {
return x(d.letter) + 7.5;
})
.text(function(d) {
return d.letter;
});
Instead of this:
labels
.enter()
.append("text")
.attr("class", "bartext")
.attr("text-anchor", "middle")
.attr("fill", "black")
.attr("x", function(d, i) {
return x(d.letter) + 7.5;
})
.attr("y", function(d, i) {
return height + 15;
})
.text(function(d, i) {
return d.letter;
});
Do this:
labels
.enter()
.append("text")
.attr("class", "bartext");
//update all the bar text.
svg
.selectAll(".bartext").attr("text-anchor", "middle")
.transition().duration(300)
.attr("fill", "black")
.attr("x", function(d, i) {
return x(d.letter) + 7.5;
})
.attr("y", function(d, i) {
return height + 15;
})
.text(function(d, i) {
return d.letter;
});
In the first case it did not work, because the attributes will get updated only for new data, updated data will not get updated to the DOM.
working code here

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!

How do I get the index number from the array in d3?

In the following code how do I get the number for the index/row that is 'd'?
var docs = svg.selectAll("g")
.data(dataset)
.enter()
.append("g")
.attr("id", function(d) { return d["Identifier"]})
.attr("num", function(d) { return d}) // this
.attr("x", function(d) { return xScale(d3.time.format.iso.parse(d["UserDate"]))})
.attr("y", function(d) { return xScale(d3.time.format.iso.parse(d["UserDate"]))});
simply use :
function(d,i){ ... }
e.g.
.attr('id', function(d,i){return 'item'+i;})

Categories