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/
Related
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
...
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
I would like to reproduce the process from D3 Sankey chart using circle node instead of rectangle node, however, I would like to select only certain nodes to change from rectangles to circles.
For example, in this jsfiddle used in the example, how would you only select Node 4 and Node 7 to be converted to a circle?
I updated your fiddle.
Basically you just need some way to select the nodes that you want to make different. I used unique classname so that you can style them with CSS as well. I didn't feel like writing the code to select just 4 and 7 (I'm lazy) so I just selected all of the even nodes instead.
// add in the nodes
var node = svg.append("g").selectAll(".node")
.data(graph.nodes)
.enter().append("g")
.attr("class", function (d, i) { return i % 2 ? "node rect" : "node circle";
})
Then you can use that to select the nodes and add circles instead of rectangles.
svg.selectAll(".node.circle").append("circle")
.attr("r", sankey.nodeWidth() / 2)
.attr("cy", function(d) { return d.dy/2; })
.attr("cx", sankey.nodeWidth() / 2)
.style("fill", function (d) {
There is also another similar approach, illustrated in the following jsfiddle.
I started from this fiddle (from another SO question that you merntioned)), where all nodes had already been converted to circles:
Then I modified existing and added some new code that involves filtering during creation of circles:
// add the circles for "node4" and "node7"
node
.filter(function(d){ return (d.name == "node4") || (d.name == "node7"); })
.append("circle")
.attr("cx", sankey.nodeWidth()/2)
.attr("cy", function (d) {
return d.dy/2;
})
.attr("r", function (d) {
return Math.sqrt(d.dy);
})
.style("fill", function (d) {
return d.color = color(d.name.replace(/ .*/, ""));
})
.style("fill-opacity", ".9")
.style("shape-rendering", "crispEdges")
.style("stroke", function (d) {
return d3.rgb(d.color).darker(2);
})
.append("title")
.text(function (d) {
return d.name + "\n" + format(d.value);
});
// add the rectangles for the rest of the nodes
node
.filter(function(d){ return !((d.name == "node4") || (d.name == "node7")); })
.append("rect")
.attr("y", function (d) {
return d.dy/2 - Math.sqrt(d.dy)/2;
})
.attr("height", function (d) {
return Math.sqrt(d.dy);
})
.attr("width", sankey.nodeWidth())
.style("fill", function (d) {
return d.color = color(d.name.replace(/ .*/, ""));
})
.style("fill-opacity", ".9")
.style("shape-rendering", "crispEdges")
.style("stroke", function (d) {
return d3.rgb(d.color).darker(2);
})
.append("title")
.text(function (d) {
return d.name + "\n" + format(d.value);
});
Similar code had to be modified for accurate positioning text beside rectangles.
I believe the final result looks natural, even though it lost some of the qualities of the original Sankey (like wider connections).
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 + "%" });
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);
});
};