D3 circle pack - dynamic label update - javascript

I'm pretty new to coding in D3. I'm working on a near real-time circle pack chart that gets its underlying data from an ajax call and resizes the nodes based on changes in data values. The challenge I'm facing is likely to be dreadfully simple, but I've not yet found a similar-enough example online to leverage as a solution.
When I run this code, I know that the text values are actually being passed properly as the data changes. However, what's happening is that the code keeps appending text tags to the svg "g" nodes (with the updated values) rather than changing the existing element to reflect the updated value. The result is a layered text mess in the middle of an otherwise attractive bubble.
I have tried using d3.exit().remove() to no avail - it's possible that I misused it and that it's actually the appropriate technique to apply.
Would someone be willing to provide some guidance on how I should accomplish 2 specific things:
1) I'd like to re-use existing "text" elements rather than remove + append unless it's not practical.
2) I'd like to update the values of an existing "text" element with new data without refreshing the page.
The full code for the .js file is here below. I'm aware that I can use "svg" instead of "svg:svg", etc. but I haven't gotten to the tidying-up stage on this file yet.
var Devices = {
setup_devices : function() {
var r = 500,
format = d3.format(",d"),
fill = d3.scale.category10();
var bubble = d3.layout.pack()
.sort(null)
.size([r, r])
.padding(1.5);
var chart = d3.select("#device_info").append("svg:svg")
.attr("width", r)
.attr("height", r)
.attr("class", "bubble")
.append("svg:g")
.attr("transform", "translate(2, 2)");
var tip = d3.tip()
.attr('class', 'd3-tip')
.offset([-10, 0])
.html(function(d) {
return "<strong>Device:</strong> <span style='color:red'>" + d.name + "</span>";
});
chart.call(tip);
setInterval(function() {
console.log("Devices Refreshing");
$.ajax({
type: "GET",
url: "/devices",
dataType: "json",
beforeSend: function() {
},
error: function( jqXHR, textStatus, thrownError ) {
return true;
},
success: function(data) {
update(data);
return true;
}
});
d3.timer.flush();
}, 2000);
function update(data) {
var updated = chart.data([data]).selectAll("g.node")
.data(bubble.nodes);
updated.enter().append("svg:g")
.attr("class", "node")
.attr("transform", function(d) {
return "translate(" + d.x + "," + d.y + ")";
})
.attr("data-name", function(d) {
return d.name;
})
.attr("data-device", function(d) {
return d.device_id;
})
.on('mouseover', tip.show)
.on('mouseout', tip.hide)
.append("svg:circle")
.attr("r", function(d) { return d.r; })
.style("fill", function(d) { return fill(d.name); })
.attr("text-anchor", "middle")
.attr("dy", ".3em")
.text(function(d) { return d.value + "%" });
updated.append("svg:text")
.attr("text-anchor", "middle")
.attr("dy", ".3em")
.text(function(d) { return d.value + "%" });
updated.transition()
.duration(1000)
.attr("class", "node")
.attr("transform", function(d) {
return "translate(" + d.x + "," + d.y + ")";
});
updated.select("circle").transition()
.duration(1000)
.attr("r", function(d) { return d.r; })
.text(function(d) { return d.value + "%" });
}
}
}

You just need to handle the enter and update selections separately -- to the enter selection you append, for the update selection you reuse existing elements.
var enterGs = updated.enter().append("svg:g")
.attr("class", "node")
.attr("transform", function(d) {
return "translate(" + d.x + "," + d.y + ")";
})
.attr("data-name", function(d) {
return d.name;
})
.attr("data-device", function(d) {
return d.device_id;
})
.on('mouseover', tip.show)
.on('mouseout', tip.hide);
enterGs.append("circle");
enterGs.append("text")
.attr("text-anchor", "middle")
.attr("dy", ".3em");
updated.select("circle")
.attr("r", function(d) { return d.r; })
.style("fill", function(d) { return fill(d.name); });
updated.select("text")
.text(function(d) { return d.value + "%" });

Related

D3js labels on grouped bar chart

I'm trying to put labels on my d3js grouped bar chart, which is supposed to be easy, but I haven't been able to do it correctly. I know it must be similar to the way I'm adding the rects but no.. I tried to follow this example but it didn't work.
This is how I add my rectangles:
var rectG = pp.selectAll('rect')
.data(dataFilter);
rectG.exit().remove();
var rectGEnter= rectG.enter().append("g")
.append("g")
.attr("transform", function(d) {return "translate(" + x(d.group) + ",0)"; })
.selectAll("rect")
.data(function(d) { return subgroups.map(function(key) {return {key: key, value: d[key]}; }); })
.enter().append("rect")
.attr("x", function(d) { return xSubgroup(d.key); })
.attr("y", function(d) { return y(d.value? d.value : 0); })
.attr("width", xSubgroup.bandwidth())
.attr("height", function(d) { return height - y(d.value? d.value : 0); })
.attr("fill", function(d) { return getColor(d.key); })
so I tried to do the same with the lables but didn't work, except for this:
rectGEnter= rectG.enter().append("g")
.append("g")
.attr("transform", function(d) {return "translate(" + x(d.group) + ",0)"; })
.selectAll("text")
.data(function(d) { return subgroups.map(function(key) {return {key: key, value: d[key]}; }); })
.enter().append("text")
.attr("x", function(d) { return xSubgroup(d.key)+1; })
.attr("y", function(d) { return y(d.value? d.value : 0); })
.attr("text-anchor", "start")
.style("alignment-baseline", "middle")
.text(function(d) { return (d.value? d.value : 0); });
}
and if ok for the first time, but if I update my data the labels don't update well, I haven an example here, any help will be apreciate
If I understand right you only want to remove the old labels. I tried it in you plunker. The following line of code does the trick.
function updates(selectedGroup) {
pp.selectAll("rect").remove();
pp.selectAll("text").remove(); // <-- remove the old text elements
...
EDIT:
To prevent the removal of the labels we can simply add a class to specify which text we want to remove.
rectGEnter= rectG.enter().append("g")
.append("g")
.attr("transform", function(d) {return "translate(" + x(d.group) + ",0)"; })
.selectAll("text")
.data(function(d) { return subgroups.map(function(key) {return {key: key, value: d[key]}; }); })
.enter().append("text")
.attr('class', 'valueLabels') // <-- Here we add a class
.attr("x", function(d) { return xSubgroup(d.key)+1; })
.attr("y", function(d) { return y(d.value? d.value : 0); })
.attr("text-anchor", "start")
.style("alignment-baseline", "middle")
.text(function(d) { return (d.value? d.value : 0); });
Now we can specify the class when removing the elements.
function updates(selectedGroup) {
pp.selectAll("rect").remove();
pp.selectAll('g').selectAll('g').selectAll("text.valueLabels").remove(); // <-- remove the old text elements
...

Word cloud unable to use onClick

I'm building a wordcloud using Jason Davies d3.layout.cloud() https://github.com/jasondavies/d3-cloud.git
I decided to add a transition and duration attributes to make the wordcloud look better when it arrives on the screen . However, I can't use on("click") attribute anymore since it sends me an error :
Uncaught TypeError: undefined is not a function
It refers to the line .on("click", function (d) {alert('ok');})
When I remove the transition and duration, the on click works correctly .
Here is the js code :
var fill = d3.scale.category20();
var layout = d3.layout.cloud().size([1500, 800])
.words(frequency_list)
.padding(5)
.rotate(function() {return ~~(Math.random() -0.5) * 120;})
.font("Impact")
.fontSize(function(d) { return d.size; })
.on("end", draw);
layout.start();
function draw(words) {
d3.select("svg").remove();
d3.select("body").append("svg")
.attr("width", layout.size()[0])
.attr("height", layout.size()[1])
.append("g")
.attr("transform", "translate(" + layout.size()[0] / 2 + "," + layout.size()[1] / 2 + ")")
.selectAll("text")
.data(words)
.enter().append("text")
.transition()
.duration(function(d) { return d.time} )
.attr('opacity', 1)
.style("font-size", function(d) { return d.size + "px"; })
.style("font-family", "Impact")
.style("fill", function(d, i) { return fill(i); })
.attr("text-anchor", "middle")
.attr("transform", function(d) {
return "translate(" + [d.x, d.y] + ")rotate(" + d.rotate + ")";
})
.style("cursor", "pointer")
.text(function(d) { return d.text; })
.on("click", function (d) {alert('ok');});
frequency_list is a list whose elements contain the attributes "text", "size", and "time" .
I don't know how to solve this .
Any help would be appraciated :)
Taken from my answer to a similar question: Since transitions are a special kind of selections, you cannot use all methods available to a selection on a transition. In your case this means, you are not allowed to use on() to register event handlers on a transition. Instead, use transition.each() to bind your handler to the elements in the transition.
.transition()
// rest of your code
.each(function () {
d3.select(this).on("click", function (d) {alert('ok');});
});

How to keep circles inside the svg when updating data in bubble chart

I am trying to add interaction in the bubble chart and update data when clicking on the according button. But sth goes wrong when I click the button, the circles go out of the bound of the svg. I can't figure out how to fix it. Please help!
Here is the working Plunk.(Try 2006,2007 or 2008)
function changebubble(i) {
d3.csv("count_s.csv", function(csvData) {
pack.value(function(d){var valuedata=[d.count2006, d.count2007, d.count2008];
return valuedata[i];});
var data = { name: "city", children: csvData };
var node = svg.data([data]).selectAll("circle")
.data(pack.nodes);
var nodeEnter=node.enter().append("g")
.attr("class", "node").attr("cx",0).attr("cy",0)
.attr("transform", function(d) { return "translate(" + d.x + "," + d.y + ")"; });
nodeEnter.append("title")
.text(function(d) { return d.city+ " : " +format(d.value); });
nodeEnter.append("circle")
.attr("r", function(d) { return d.r; })
.style("fill", function(d) { return color(d.city); });
nodeEnter.append("text")
.attr("dy", ".3em")
.style("text-anchor", "middle")
.text(function(d) { return d.city });
node.select("circle")
.transition().duration(1000)
.attr("r", function(d) { return d.r; })
.style("fill", function(d) { return color(d.city); });
node.transition().attr("class", "node")
.attr("transform", function (d) {
return "translate(" + d.x + "," + d.y + ")";
});
node.select("text")
.transition().duration(1000)
.attr("dy", ".3em")
.style("text-anchor", "middle")
.text(function(d) { return d.city });
node.select("title")
.transition().duration(1000)
.text(function(d) { return d.city+ " : " +format(d.value); });
node.exit().remove();
});
}
function updateBubble1() {changebubble(0);}
function updateBubble2() {changebubble(1);}
function updateBubble3() {changebubble(2);}
d3.select("#count2006").on("click",updateBubble1);
d3.select("#count2007").on("click",updateBubble2);
d3.select("#count2008").on("click",updateBubble3);
Thanks a lot!
There are some problems with your update function, to name a couple of big ones:
The elements you are selecting (var node = svg2.selectAll("circle")) do not match the elements you are 'entering' (var nodeEnter=node.enter().append("g")). This leads to problems when defining key functions and performing data joins
You seem to be trying to rebind the data when transitioning existing elements (node.select("circle").data(pack.nodes,function(d) {return d.city})) This will cause problems -- the data is already bound to these elements and re-binding is un-necessary at this point.
I've made updates to your code here: http://plnkr.co/edit/pYQTCOKWXoRM3ZE0HEt3?p=preview

d3 pack layout circles overlapping after movement/size transitions

I have circles arranged with the pack layout, from a dataset which periodically updates the radii.
The code I started out with is this standard example for a bubble chart: http://bl.ocks.org/mbostock/4063269
Whenever the circle sizes change, they transition. Often when circles grow, they move to overlap other circles. I don't want them to overlap each other.
I'm still pretty new to d3, have moved the code around a lot and tried everything I can think of, but no luck.
The function makeBubbles is passed raw Json (see below).
function makeBubbles(root){
var diameter = $(window).width(),
diameterh = $(window).height(),
format = d3.format(",d"),
color = d3.scale.category20();
var bubble = d3.layout.pack()
.sort(null)
.size([diameter, diameterh])
.value(function(d){return d.value; })
.padding(1.5);
var svg = d3.select("svg")
.attr("width", diameter)
.attr("height", diameterh)
.attr("class", "bubble");
var node = svg.selectAll(".node")
.data(bubble.nodes(classes(root)).filter(function(d) { return !d.children; }), function(d){ console.log(d); return d.className; });
node.append("title")
.text(function(d) { return d.className + ": " + format(d.value); });
node.append("circle")
.style("fill", function(d) { return color(d.packageName); })
.on("click", function(d) { window.location = d.url; })
.attr("r", 0)
.transition()
.duration(1000)
.attr("r", function(d) { return d.r; });
node.transition().duration(1000).attr("transform", function(d) { return "translate(" + d.x + "," + d.y + ")"; });
node.enter().append("g")
.attr("class", "node")
.attr("transform", function(d) { return "translate(" + d.x + "," + d.y + ")"; });
node.exit().transition().duration(200).attr("transform", "scale(0.001)").remove();
node.append("text")
.attr("dy", ".3em")
.style("text-anchor", "middle")
.text(function(d) { return d.className.substring(0, d.r / 6); })
.attr("opacity",0)
.transition().duration(1000)
.attr("opacity",1);
// Returns a flattened hierarchy containing all leaf nodes under the root.
function classes(root) {
var classes = [];
function recurse(name, node) {
if (node.children) node.children.forEach(function(child) { recurse(node.name, child); });
else classes.push({packageName: name, className: node.name, value: node.size, url: node.url});
}
recurse(null, root);
return {children: classes};
}
d3.select(self.frameElement).style("height", diameterh + "px");
}
Data passed looks something like this (varying as the dataset is updated):
{"name":"bubbles","children":[{"name":"tourism","children":[{"name":"tourism","children":[{"name":"practical","children":[{"name":"ACCOMM","size":13,"url":"#"},{"name":"HIRE","size":2,"url":"#"}]},{"name":"activity","children":[{"name":"EVENT","size":6,"url":"#"},{"name":"TOUR","size":3,"url":"#"}]},{"name":"leisure","children":[{"name":"RESTAURANT","size":168,"url":"#"},{"name":"ATTRACTION","size":8,"url":"#"}]}]}]}]}
I had a similar problem.
I slightly modified (mostly simplified) your code, and here you can find working example.
My approach is not to use transformations. Without them, the code looks more readable and maintainable. So, I propose a simple solution, I hope you can use it in your case.
Label. transition is maybe not the best, but you can change it.
On jsfiddle, its impossible to integrate json files, so the data is inside javascript. In your code, you would need to handle loading json, but the core idea from my example can be applied without change.
The key function is:
function updateVis() {
if (dataSource == 0)
pack.value(function(d) { return d.size; });
if (dataSource == 1)
pack.value(function(d) { return 100; });
if (dataSource == 2)
pack.value(function(d) { return 1 +
Math.floor(Math.random()*501); });
var data1 = pack.nodes(data);
titles.attr("x", function(d) { return d.x; })
.attr("y", function(d) { return d.y; })
.text(function(d) {
return (d.children ? "" : d.name + ": " + format(d.value));
});
circles.transition()
.duration(5000)
.attr("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; })
.attr("r", function(d) { return d.r; });
labels.transition()
.duration(5000)
.attr("opacity", 0)
.attr("x", function(d) { return d.x; })
.attr("y", function(d) { return d.y; })
.each("end", function(d){
d3.select(this).text(function(d) {
return d.children ? "" : d.name.substring(0, d.r / 4);
});
d3.select(this).transition()
.duration(1000)
.attr("opacity", 1);
});
};

D3js filter selection

I am creating an interactive bubble chart of fortune 500 data. I am trying to reduce a selection but can't figure out why it's not working as I expect.
var bubble = d3.layout.pack()
...
d3.json("js/data/fortune2009.json", function(json) {
var node = svg.data([json]).selectAll("g.node")
.data(bubble.nodes); // filter here?
node.enter()
.append("g")
.attr("class", "node")
.attr("transform", function(d) { return "translate(" + d.x + "," + d.y + ")"; });
node.append("circle")
.attr("r", function(d) { return d.r; })
.attr("class", function(d) { return "q" + color(d.Profit) + "-9"; });
node.filter(function(d) { return !d.children; })
.append("text")
.attr("dy", ".3em")
.style("text-anchor", "middle")
.text(function(d) { return d.Company.substring(0, d.r / 3); });
});
The JSON data looks like this:
{
"children":[
{
"Year" : "2009",
"Rank" : "1",
"Revenue" : "442",
"Profit" : "851.00",
"Company" : "Exxon Mobil"
},
....
}
I want to remove the parent node. I am trying to use a filter function like described here.
.filter(function(d) { return !d.children; })
But if i add my filter function where I initialize the node selection, either nothing happens or I get an error.
(The filter works when I apply it before appending text)
How do I solve this?
UPDATE: Here is a JSFIDDLE http://jsfiddle.net/B9w4N/8/ showing my problem.
You had the right idea. You just needed to apply the filter where the circles are created (in addition to where the text is created):
node
.filter(function(d) { return !d.children; })
.append("circle")
.attr("r", function(d) { return d.r; });
Forked version on JSFiddle
You can filter the array that the layout outputs:
var node = svg.data([fortune2009]).selectAll("g.node")
.data(function(d) {
return bubble.nodes(d).filter(function(v) {
return v.depth > 0;
});
});
See updated fiddle: http://jsfiddle.net/B9w4N/10/

Categories