I am quite new to D3 but have been working through some mbostocks examples but hitting an issue when trying to update multiple pie charts. I can generate these fine from my data array but when I want to update them I run into an issue.
The issue is quite simple but I am a little stuck on how to fix this. I have run up my code in js fiddle that can be found here. You will see that in my example I build three pies, then wait 3 seconds and update these to new data. The issue I have is that all pies always seem to get updated with the same data.
I believe this is due to the way I am making the path selection in order to update the pie. it looks like I am updating each all the paths each time with each data array so they all end up being updated with the last dataset in my array.
If anyone knows how I can update this in order to correctly build the pies I would be very grateful of any help, pointers or comments.
var data = [
[3, 4, 5, 9],
[1, 7, 3, 4],
[4, 3, 2, 1],
];
function getData() {
// Generate some random data to update the pie with
tdata = []
for(i in data) {
rdata = []
for(c in data[i]) {
rdata.push(Math.floor((Math.random() * 10) + 1) )
}
tdata.push(rdata)
}
return tdata
}
// ------------
var m = 10,
r = 100
var mycolors = ["red","#FF7F00","#F5CC11","#D61687","#1E93C1","#64B72D","#999999"]
var arc = d3.svg.arc()
.innerRadius(r / 2)
.outerRadius(r)
var pie = d3.layout.pie()
.value(function(d) { return d; })
.sort(null);
var svg = d3.select("body").selectAll("svg")
.data(data)
.enter()
.append("svg")
.attr("width", (r + m) * 2)
.attr("height", (r + m) * 2)
.attr("id", function(d,i) {return 'pie'+i;})
.append("svg:g")
.attr("transform", "translate(" + (r + m) + "," + (r + m) + ")");
var path = svg.selectAll("path")
.data(pie)
.enter()
.append("svg:path")
.attr("d", arc)
.style("fill", function(d, i) { return mycolors[i]; })
.each(function(d) { this._current = d; }); // store the initial angles
var titles = svg.append("svg:text")
.attr("class", "title")
.text(function(d,i) {return i;})
.attr("dy", "5px")
.attr("text-anchor", "middle")
// -- Do the updates
//------------------------
setInterval(function() {
change()
}, 3000);
function change() {
// Update the Pie charts with random data
piedata = getData()
svg.each(function(d,i) {
path = path.data(pie(piedata[i]))
path.transition().duration(1000).attrTween("d", arcTween);
})
// temp, print new array to screen
tdata = ""
for(x in piedata) {
tdata += "<strong>"+x+":</strong> "+piedata[x]+"<br>"
}
$('#pieData').html(tdata)
}
function arcTween(a) {
var i = d3.interpolate(this._current, a);
this._current = i(0);
return function(t) {
return arc(i(t));
};
}
Right, I finally got this working and am posting the working solution incase others are trying to do the same thing.
I expect this might not be the best nor most efficient way of doing it but this is going to be fine for what I need (at this point). But if anyone still has any better solutions it would be good to hear from you.
I ended up selecting the paths based on a unique id that I gave the individual SVG elements which I created, then just updated these paths only. Sounds simple now when I say it like this but did have me stumped for a while.
function change() {
// Update the Pie charts with random data
var newdata = getData()
for(x in newdata) {
var npath = d3.select("#pie"+x).selectAll("path").data(pie(newdata[x]))
npath.transition().duration(1000).attrTween("d", arcTween); // redraw the arcs
}
}
Full working copy can be found at http://jsfiddle.net/THT75/nskwwbnf/
Related
I'm a newbie to the d3 library and javascript in general.
I'm trying to achieve something like
this, where I have a sunburst partition but each node has a different height with respect to the radial center - but the padding to its parent/child stays the same.
I've tried looking around and couldn't come up with any solutions.
(trying to change the innerRadius/outerRadius parameters didn't seem to work :( ).
Here is my code:
var vis = d3.select("#chart").append("svg")
.style("margin", "auto")
.style("position", "relative")
.attr("width", width)
.attr("height", height)
.append("svg:g")
.attr("id", "container")
.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")");
var partition = d3.layout.partition()
.sort(function (a, b) { return d3.ascending(a.time, b.time); })
.size([2 * Math.PI, radius * radius])
.value(function(d) { return d.n_leaves+1; });
var arc = d3.svg.arc()
.startAngle(function(d) { return d.x; })
.endAngle(function(d) { return d.x + d.dx; })
.innerRadius(function(d) { return Math.sqrt(d.y); })
.outerRadius(function(d) { return Math.sqrt(d.y + d.dy); });
//read data from json file and visualize it
d3.text("5rrasx_out.json", function(text) {
var data = JSON.parse(text);
var json = buildHierarchy(data,'5rrasx');
createVisualization(json);
});
// Main function to draw and set up the visualization, once we have the data.
function createVisualization(json) {
// Bounding circle underneath the sunburst, to make it easier to detect
// when the mouse leaves the parent g.
vis.append("svg:circle")
.attr("r", radius)
.style("opacity", 0);
// For efficiency, filter nodes to keep only those large enough to see.
var nodes = partition.nodes(json);
var dataSummary = [{label: 'pos', count: totalPos}, {label: 'neg', count: totalNeg}];
//set title
$("#title").text(json.title.replace(/\[.*\]/g,""));
//set chart
var path = vis.data([json]).selectAll("path")
.data(nodes)
.enter().append("path")
.attr("class", "sunburst_node")
.attr("display", function(d) { return d.depth ? null : "none"; })
.attr("d", arc)
.attr("fill-rule", "evenodd")
.style("fill", function(d) { return (d.sentiment > 0) ? colors["pos"] : colors["neg"]; })
.style("opacity", 1)
.on("mouseover", mouseover)
.on("click", click);
};
Any help would be much appreciated.
Thanks!
I know this is not a proper answer to the question above, but in case someone needs a sunburst with different dimensions for each node, here I post how to do it in R using the ggsunburst package.
# install ggsunburst
if (!require("ggplot2")) install.packages("ggplot2")
if (!require("rPython")) install.packages("rPython")
install.packages("http://genome.crg.es/~didac/ggsunburst/ggsunburst_0.0.10.tar.gz", repos=NULL, type="source")
library(ggsunburst)
# one possible input for ggsunburst is newick format
# consider the following newick "(((A,B),C),D,E);"
# you can define the distance in node A with "A:0.5"
# you can define size in node E with "E[&&NHX:size=5]"
# adding both attributes to the newick
nw <- '(((A:0.5,B),C:3),D[&&NHX:size=5],E[&&NHX:size=5]);'
sb <- sunburst_data(nw)
sunburst(sb, rects.fill.aes = "name") + scale_fill_discrete(guide=F)
as you can see in the code, these attributes can be defined independently, and as you can see in the plot they affect the dimennsions of the correponding nodes:
node "A" is 0.5 times shorter than "B", which is defined by the attribute "distance"
E has an angle 5 times wider than C, which is defined by the attribute "size".
and here an attempt to resemble the example posted in the question with a newick tree
nw <- "(((.:0[&&NHX:support=1.0:dist=0.0:name=.:size=3],a3:1[&&NHX:color=2:support=1.0:dist=1.0:name=a3:size=1])1:1[&&NHX:color=-3:support=1.0:dist=1.0:name=a2])1:1[&&NHX:color=-1:support=1.0:dist=1.0:name=a1],b1:1.8[&&NHX:color=1:support=1.0:dist=1.8:name=b1:size=5],(((a4:1[&&NHX:color=1:support=1.0:dist=1.0:name=a4:size=1],b4:1.8[&&NHX:color=-1:support=1.0:dist=1.8:name=b4:size=1],c4:1.5[&&NHX:color=2:support=1.0:dist=1.5:name=c4:size=1],d4:0.8[&&NHX:color=-2:support=1.0:dist=0.8:name=d4:size=1])1:1[&&NHX:color=1:support=1.0:dist=1.0:name=b3:size=1])1:1[&&NHX:color=-3:support=1.0:dist=1.0:name=b2:size=1],(c3:1[&&NHX:color=1:support=1.0:dist=1.0:name=c3:size=1],(e4:1[&&NHX:color=-2:support=1.0:dist=1.0:name=e4:size=1])1:0.5[&&NHX:color=-1:support=1.0:dist=0.5:name=d3:size=1])1:0.5[&&NHX:color=1:support=1.0:dist=0.5:name=c2:size=1])1:1[&&NHX:color=-1:support=1.0:dist=1.0:name=c1:size=1],d1:0.8[&&NHX:color=3:support=1.0:dist=0.8:name=d1:size=20]);"
sb <- sunburst_data(nw, node_attributes = "color")
sunburst(sb, leaf_labels.size = 4, node_labels.size = 4, node_labels = T, node_labels.min = 1, rects.fill.aes = "color") +
scale_fill_gradient2(guide=F) + ylim(-8,NA)
The idea is to have a d3 vertical bar-chart that will be given live data.
I simulate the live data with a setInterval function that updates the the values of the elements in my dataset:
var updateData = function(){
a = parseInt(Math.random() * 100),
b = parseInt(Math.random() * 100),
c = parseInt(Math.random() * 100),
d = parseInt(Math.random() * 100);
dataset = [a, b, c, d];
console.log(dataset);
};
// simullate live data input
var update = setInterval(updateData, 1000);
I want to update the chart every 2 seconds.
For that I need a update function that gets the new dataset and then animates a transition to show the new results.
Like that:
var updateVis = function(){
..........
};
var updateLoop = setInterval(drawVis,2000);
I don't want to simply remove the chart and draw again. I want to animate the transition between the new and old bar height for each bar.
Checkout the fiddle
Since your not changing the number of bars, this can be as simple as:
var updateVis = function(){
svg.selectAll(".input")
.data(dataset)
.transition()
.attr("y", function(d) {
return y(d);
})
.attr("height", function(d) {
return h - y(d);
});
};
Updated fiddle.
But your next question becomes, what if I need a different number of bars? This is where you need to handle enter, update, exit a little better. You you can write one function for initial draw or updating.
function drawVis(){
// update selection
var uSel = svg.selectAll(".input")
.data(dataset);
// those exiting
uSel.exit().remove();
// new bars
uSel
.enter()
.append("rect")
.attr("class", "input")
.attr("fill", "rgb(250, 128, 114)");
// update all
uSel
.attr("x", function(d, i) {
return i * (w / dataset.length) + 2.5/100 * w;
})
.attr("width", w / dataset.length - barPadding)
.attr("height", y(0))
.transition().duration(750).ease("linear")
.attr("y", function(d) {
return y(d);
})
.attr("height", function(d) {
return h - y(d);
});
}
New fiddle.
That's the way to go.
Just think what you've done to get the initial chart:
1) Get data
2) Bind it to element (.enter())
3) Set element attributes to be function of the data.
Well, you do this again:
In the function updateData you get a new dataset that's the first step.
Then, rebind it:
d3.selectAll("rect").data(dataset);
And finally update the attributes:
d3.selectAll("rect").attr("y", function(d) {
return y(d);
})
.attr("height", function(d) {
return h - y(d);
});
(Want transitions? Go for it. It is easy to add in your code but you better read this tuto if you want to deeply understand it)
Check it on fiddle
I like dcjs, http://bl.ocks.org/d3noob/6584483 but the problem is I see no labels anywhere for the line chart (Events Per Hour). Is it possible to add a label that shows up just above the data point, or even better, within a circular dot at the tip of each data point?
I attempted to apply the concepts in the pull request and came up with:
function getLayers(chart){
var chartBody = chart.chartBodyG();
var layersList = chartBody.selectAll('g.label-list');
if (layersList.empty()) {
layersList = chartBody.append('g').attr('class', 'label-list');
}
var layers = layersList.data(chart.data());
return layers;
}
function addDataLabelToLineChart(chart){
var LABEL_FONTSIZE = 50;
var LABEL_PADDING = -19;
var layers = getLayers(chart);
layers.each(function (d, layerIndex) {
var layer = d3.select(this);
var labels = layer.selectAll('text.lineLabel')
.data(d.values, dc.pluck('x'));
labels.enter()
.append('text')
.attr('class', 'lineLabel')
.attr('text-anchor', 'middle')
.attr('x', function (d) {
return dc.utils.safeNumber(chart.x()(d.x));
})
.attr('y', function (d) {
var y = chart.y()(d.y + d.y0) - LABEL_PADDING;
return dc.utils.safeNumber(y);
})
.attr('fill', 'white')
.style('font-size', LABEL_FONTSIZE + "px")
.text(function (d) {
return chart.label()(d);
});
dc.transition(labels.exit(), chart.transitionDuration())
.attr('height', 0)
.remove();
});
}
I changed the "layers" to be a new group rather than using the existing "stack-list" group so that it would be added after the data points and therefore render on top of them.
Here is a fiddle of this hack: https://jsfiddle.net/bsx0vmok/
I'd like to show labels for tiny slices (chart.minAngleForLabel(0.05)) avoiding text overlap.
I added a renderlet that shifts labels toward outer edge:
.on('renderlet', function(chart) {
chart.selectAll('text').attr('transform', function(d, i) {
var old = this.getAttribute('transform');
if (d.endAngle-d.startAngle > 0.3) { return old; }
var xy = old.slice(10,-1).split(',');
var m = 1.25 + (i%3) * 0.25;
return 'translate(' + (+xy[0]*m) + ',' + (+xy[1]*m) + ')';
})
})
and i'm rather happy with it (the second image is after renderlet):
but it makes annoying transitions -- labels move toward centroid and then jump back. Is there a workaround for this?
My solution is a bit excessive, but I wanted to know if it's now possible to replaced transitioned positions, now that we have the pretransition event in dc.js 2.0 beta 11.
In fact, it is. The impractical part is that your code relies on already having the final positions, which we're not going to have if we replace the transitions. Instead, we have to calculate the positions from scratch, which means copying a bunch of code out of the pie chart.
I wasn't able to get your code to work, so I'm just testing this by offsetting all label positions by -25, -25. But it's the same idea, we use the original code to get the centroid, and then modify that position:
// copied from pieChart
function buildArcs(chart) {
return d3.svg.arc().outerRadius(chart.radius()).innerRadius(chart.innerRadius());
}
function labelPosition(d, arc) {
var centroid = arc.centroid(d);
if (isNaN(centroid[0]) || isNaN(centroid[1])) {
return [0,0];
} else {
return centroid;
}
}
//
.on('pretransition', function(chart) {
chart.selectAll('text.pie-slice').transition().duration(chart.transitionDuration())
.attr('transform', function(d, i) {
var arc = buildArcs(chart);
var xy = labelPosition(d, arc);
return 'translate(' + (+xy[0] - 25) + ',' + (+xy[1] - 25) + ')';
})
});
The big idea here is that if you specify a new transition for an element, it will replace the transition that was already active. So we are completely removing the original position and transition, and replacing it with our own. No "jump"!
Not really solving your problem, but might look better with a transition on the position?
chart.selectAll('text')
.transition()
.delay(800)
.attr("transform", ...
I have a solution for this problem. Try this once , this will works to avoid overlapping of label names in pie charts.
function buildArcs(chart) {
return
d3.svg.arc().outerRadius(chart.radius()).innerRadius(chart.innerRadius());
}
function labelPosition(d, arc) {
var centroid = arc.centroid(d);
if (isNaN(centroid[0]) || isNaN(centroid[1])) {
return [0,0];
} else {
return centroid;
}
}
.on('pretransition', function(chart) {
chart.selectAll('text.pieslice').transition()
.duration(chart.transitionDuration())
.attr('transform', function(d, i) {
var j = 0;
var arc = buildArcs(chart);
var xy = labelPosition(d, arc);
if (xy[1] < 0) {
j = -(10 * (i + 1));
}
else {
j = 10 * (i + 1);
}
return 'translate(' + (+xy[0] - 25) + ',' + (j) + ')';
})
});
I'm trying to create a reusable pie chart with dynamic transitions as a learning task. I'm working off of the d3.js resuable components e-book by Chris Viau.
The problem I'm having is basically its not updating, but creating multiple pie charts. I'm wondering if I'm not understanding how d3.dispatch works or whether I've messed something up in the way the pie char should work. It creates multiple circles instead of dynamically updating a single pie chart with random values.
here is my jsfiddle.
http://jsfiddle.net/seoulbrother/Upcr5/
thanks!
js code below:
d3.edge = {};
d3.edge.donut = function module() {
var width = 460,
height = 300,
radius = Math.min(width, height) / 2;
var color = d3.scale.category20();
var dispatch = d3.dispatch("customHover");
function graph(_selection) {
_selection.each(function(_data) {
var pie = d3.layout.pie()
.value(function(_data) { return _data; })
.sort(null);
var arc = d3.svg.arc()
.innerRadius(radius - 100)
.outerRadius(radius - 50);
if (!svg){
var svg = d3.select(this).append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")");
}
var path = svg.selectAll("path")
.data(pie)
.enter().append("path")
.attr("fill", function(d, i) { return color(i); })
.attr("d", arc)
.each(function(d) {this._current = d;} );
path.transition()
.ease("elastic")
.duration(750)
.attrTween("d", arcTween);
function arcTween(a) {
var i = d3.interpolate(this._current, a);
this._current = i(0);
return function(t) {
return arc(i(t));
};
}
});
}
d3.rebind(graph, dispatch, "on");
return graph;
}
donut = d3.edge.donut();
var data = [1, 2, 3, 4];
var container = d3.select("#viz").datum(data).call(donut);
function update(_data) {
data = d3.range(~~(Math.random() * 20)).map(function(d, i) {
return ~~(Math.random() * 100);
});
container.datum(data).transition().ease("linear").call(donut);
}
update();
setTimeout( update, 1000);
The main reason for multiple SVGs appearing is that you're not checking if there is one already correctly. You're relying on the variable svg being defined, but define it only after checking whether it is defined.
The better way is to select the element you're looking for and check whether that selection is empty:
var svg = d3.select(this).select("svg > g");
if (svg.empty()){ // etc
In addition, you need to handle the update and exit selections in your code in addition to the enter selection. Complete jsfiddle here.