Circle Pack Layout D3 - remove add nodes on new data - javascript

I have a situation where I want users to browse 3 different data sets using circle pack layout. I am using Bostock's Zoomable circle packing.
I have added 3 options additionally which loads data and creates new nodes. Here chartid is passed from these elements.
function changeDataSet(chartid)
{
console.log(chartid);
//console.log(nodes);
//add new chart data depending on the selected option
if(chartid === "plevels")
{
root = JSON.parse(newKmap_slevels);
focus = root;
nodes = pack.nodes(root);
}
else if (chartid === "pduration")
{
root = JSON.parse(newKmap_sduration);
focus = root;
nodes = pack.nodes(root);
}
else
{
root = JSON.parse(newKmap_stype);
focus = root;
nodes = pack.nodes(root);
}
refresh();
}
Then I have the refresh function, but I am not understnading how to remove existting nodes, and add new ones based on the new data set. Showing some transition animations would be nice also.
Currently I am trying to remove and recreate the initial elements, but the chart goes blank when I do that.
var refresh = function() {
//var nodes = pack.nodes(root);
var duration = 10;
console.log(nodes);
d3.select("#conf_knowledge_map").selectAll("g")
.remove();
svg
.attr("width", diameter)
.attr("height", diameter)
.append("g")
.attr("transform", "translate(" + diameter / 2 + "," + diameter / 2 + ")");
circle = svg.selectAll("circle")
.data(nodes)
.enter().append("circle")
.attr("class", function(d) { return d.parent ? d.children ? "node" : "node node--leaf" : "node node--root"; })
.style("fill", function(d) { return d.children ? color(d.depth) : null; })
.on("click", function(d) {
if (focus !== d) zoom(d), d3.event.stopPropagation();
});
text = svg.selectAll("text")
.data(nodes)
.enter().append("text")
.attr("class", "label")
.style("fill-opacity", function(d) { return d.parent === root ? 1 : 0; })
.style("display", function(d) { return d.parent === root ? "inline" : "none"; })
.text(function(d) {
//console.log(d);
if( d.size )
{
return d.name + ":" + d.size;
}
else
return d.name;
});
}
So my question is how can I remove and then create new nodes on click?
UPDATE
I was able to remove all nodes and add the new nodes based on the new data, but now on click to zoom the layout is all messed up. The transform function is not applied to the new nodes somehow.
var refresh = function() {
svg.selectAll(".node").remove();
svg.selectAll(".label").remove();
var nodes = pack.nodes(root);
focus = root;
circle = svg.selectAll("circle")
.data(nodes)
.enter()
.append("circle")
.attr("class", function(d) { return d.parent ? d.children ? "node" : "node node--leaf" : "node node--root"; })
.attr("r", function(d) { return d.r;})
.attr("cx", function(d) {
console.log(d);
// if(d.depth === 0)
// return 0;
// else
// return d.x - (diameter/2);
return d.x;
})
.attr("cy", function(d) {
return d.y;
})
// .attr("transform", "translate(" + "x" + "," + "y" + ")")
.style("fill", function(d) { return d.children ? color(d.depth) : null; })
.on("click", function(d) {
// d.x = d.x - (diameter/2);
// d.y = d.y - (diameter/2);
if (focus !== d) zoom(d), d3.event.stopPropagation();
});
text = svg.selectAll("text")
.data(nodes)
.enter().append("text")
.attr("class", "label")
.attr("x", function(d) {return d.x - (diameter/2);})
.attr("y", function(d) {return d.y - (diameter/2);})
.style("fill-opacity", function(d) { return d.parent === root ? 1 : 0; })
.style("display", function(d) { return d.parent === root ? "inline" : "none"; })
// .attr("transform", "translate(" + "x" + "," + "y" + ")")
.text(function(d) {
//console.log(d);
if( d.size )
{
return d.name + ":" + d.size;
}
else
return d.name;
});
}
How can I transform the new nodes to be in place and for the zoom to work properly?
UPDATE 2
Now the transform is working properly after attaching 'g' element to the circles, and showing all the nodes and text correctly. The only problem now is that the zoom does not work when I click on the circles!!
circle = svg.selectAll("circle")
.data(nodes)
.enter()
.append("circle")
.attr("class", function(d) { return d.parent ? d.children ? "node" : "node node--leaf" : "node node--root"; })
.attr("r", function(d) { return d.r;})
.attr("cx", function(d) {
if(d.depth === 0)
return 0;
else
return d.x - (diameter/2);
})
.attr("cy", function(d) {
if(d.depth === 0)
return 0;
else
return d.y - (diameter/2);
})
.style("fill", function(d) { return d.children ? color(d.depth) : null;})
.on("click", function(d) {
if (focus !== d) zoom(d); d3.event.stopPropagation();
})
.append("g")
.attr("transform", function(d) { return "translate(" + d.x + "," + d.y + ")"; })
;
How to make the zoom work??

It so happens that I was making the mistake of simultaneously creating three independent circle pack layouts, with mixed statements one after the other. This was problematic as when you have to select svg sections, different elements were all getting selected and wrongly attached with different events.
So I decided to separate the three implementations and create the three layouts one after the other, only after each one finished. This way I kept the section selection and attaching events etc, separate and then load them as and when required.

Related

Zoomable Circle Packing with Automatic Text Sizing in D3.js

I'm trying to merge two of Mike's examples: Zoomable Circle Packing + Automatic Text Sizing.
It works when initially displayed at the top-level. However, if you zoom in to the next level, the fonts are not sized correctly.
I'm not sure if I need to modify the transform, or modify the part which calculates the font size.
Here's my codepen: http://codepen.io/anon/pen/GJWqrL
var circleFill = function(d) {
if (d['color']) {
return d.color;
} else {
return d.children ? color(d.depth) : '#FFF';
}
}
var calculateTextFontSize = function(d) {
return Math.min(2 * d.r, (2 * d.r - 8) / this.getComputedTextLength() * 11) + "px";
}
var margin = 20,
diameter = 960;
var color = d3.scale.linear()
.domain([-1, 18])
.range(["hsl(0,0%,100%)", "hsl(228,30%,40%)"])
.interpolate(d3.interpolateHcl);
var pack = d3.layout.pack()
.padding(2)
.size([diameter - margin, diameter - margin])
.value(function(d) {
return d.size;
})
var svg = d3.select("body").append("svg")
.attr("width", window.innerWidth)
.attr("height", window.innerHeight)
.append("g")
.attr("transform", "translate(" + diameter / 2 + "," + diameter / 2 + ")");
var focus = root,
nodes = pack.nodes(root),
view;
var circle = svg.selectAll("circle")
.data(nodes)
.enter().append("circle")
.attr("class", function(d) {
return d.parent ? d.children ? "node" : "node node--leaf" : "node node--root";
})
.style("fill", circleFill)
.on("click", function(d) {
if (focus !== d) zoom(d), d3.event.stopPropagation();
});
circle.append("svg:title")
.text(function(d) {
return d.name;
})
var text = svg.selectAll("text")
.data(nodes)
.enter().append("text")
.attr("class", "label")
.style("fill-opacity", function(d) {
return d.parent === root ? 1 : 0;
})
.style("display", function(d) {
return d.parent === root ? null : "none";
})
.text(function(d) {
return d.name;
})
.style("font-size", calculateTextFontSize)
.attr("dy", ".35em");
var node = svg.selectAll("circle,text");
d3.select("body")
.style("background", color(-1))
.on("click", function() {
zoom(root);
});
zoomTo([root.x, root.y, root.r * 2 + margin]);
function zoom(d) {
var focus0 = focus;
focus = d;
var transition = d3.transition()
.duration(d3.event.altKey ? 7500 : 750)
.tween("zoom", function(d) {
var i = d3.interpolateZoom(view, [focus.x, focus.y, focus.r * 2 + margin]);
return function(t) {
zoomTo(i(t));
};
});
transition.selectAll("text")
.filter(function(d) {
return d.parent === focus || this.style.display === "inline";
})
.style("fill-opacity", function(d) {
return d.parent === focus ? 1 : 0;
})
.each("start", function(d) {
if (d.parent === focus) this.style.display = "inline";
})
.each("end", function(d) {
if (d.parent !== focus) this.style.display = "none";
});
}
function zoomTo(v) {
var k = diameter / v[2];
view = v;
node.attr("transform", function(d) {
return "translate(" + (d.x - v[0]) * k + "," + (d.y - v[1]) * k + ")";
});
circle.attr("r", function(d) {
return d.r * k;
});
}
d3.select(self.frameElement).style("height", diameter + "px");
Clicking the largest sub-circle in the "vis" circle illustrates the problem.
https://dl.dropboxusercontent.com/u/3040414/vis-circle.png
First give an id to the circle, here I am giving text name as the circle ID so that i can link the text and its circle via text name.
var circle = svg.selectAll("circle")
.data(nodes)
.enter().append("circle")
.attr("class", function(d) {
return d.parent ? d.children ? "node" : "node node--leaf" : "node node--root";
})
.style("fill", circleFill)
.attr("r", function(d) {
return d.r;
})
.attr("id", function(d) {
return d.name;//setting text name as the ID
})
.on("click", function(d) {
if (focus !== d) zoom(d), d3.event.stopPropagation();
});
On transition complete of zoom(d) function(i.e when you click on a circle and it zooms) add a timeout function which will recalculate the text font size based on the zoom.
setTimeout(function() {
d3.selectAll("text").filter(function(d) {
return d.parent === focus || this.style.display === "inline";
}).style("font-size", calculateTextFontSize);//calculate the font
}, 500)
Your calculateTextFontSize function will look like this(I am using the real DOM radius to calculate the font size):
var calculateTextFontSize = function(d) {
var id = d3.select(this).text();
var radius = 0;
if (d.fontsize){
//if fontsize is already calculated use that.
return d.fontsize;
}
if (!d.computed ) {
//if computed not present get & store the getComputedTextLength() of the text field
d.computed = this.getComputedTextLength();
if(d.computed != 0){
//if computed is not 0 then get the visual radius of DOM
var r = d3.selectAll("#" + id).attr("r");
//if radius present in DOM use that
if (r) {
radius = r;
}
//calculate the font size and store it in object for future
d.fontsize = (2 * radius - 8) / d.computed * 24 + "px";
return d.fontsize;
}
}
}
Working code here
I also had same problem as you and I tried this one and it works for me.
D3.js Auto font-sizing based on nodes individual radius/diameter

dynamically update d3.js treemap

I am trying to dynamically update my d3 treemap when the data updates. I have two functions, one that I call initially to build the treemap and another to redraw the treemap. When I redraw the treemap, I get a thin black bar on top of the treemap, and the first element in the array that I am graphing goes black. If I click on either of the two black elements. I get the error...
Uncaught TypeError: Cannot read property 'dx' of undefined
So the data being passed to the cell is undefined.
Additionally when I call regraph I have checked and the data has changed, but the graph is unchanged from when I initially built the treemap with the exception of the two black elements.
The code for building the treemap is below. Also the createObj function takes two arrays and creates a Json object.
function drawTreeMap(array1,array2){
console.log("got to drawing");
nestedJson=createObj(array1, array2);
w = 1880 - 80,
h = 900 - 180,
x = d3.scale.linear().range([0, w]),
y = d3.scale.linear().range([0, h]),
color = d3.scale.linear()
.range(['lightgreen', 'darkgreen']) // or use hex values
.domain([computeMin(array2), computeMaxNum(array2)]);
root,
node;
treemap = d3.layout.treemap()
.round(false)
.size([w, h])
.sticky(true)
.padding([10, 0, 0, 0])
.value(function(d) { return d.size; });
svg = d3.select("#body").append("div")
.attr("class", "chart")
.style("width", w + "px")
.style("height", h + "px")
.append("svg:svg")
.attr("width", w)
.attr("height", h)
.append("svg:g")
.attr("transform", "translate(.5,.5)");
node = root = nestedJson;
var nodes = treemap.nodes(root)
.filter(function(d) { return !d.children; });
var cell = svg.selectAll("g")
.data(nodes)
.enter().append("svg:g")
.attr("class", "cell")
.attr("transform", function(d) { return "translate(" + d.x + "," + d.y + ")"; })
.on("click", function(d) { return zoom(node == d.parent ? root : d.parent); });
cell.append("svg:rect")
.attr("width", function(d) { return d.dx - 1; })
.attr("height", function(d) { return d.dy ; })
.style("fill", function(d) { return color(d.size)});
cell.append("svg:text")
.attr("x", function(d) { return d.dx / 2; })
.attr("y", function(d) { return d.dy / 2; })
.attr("dy", ".35em")
.attr("text-anchor", "middle")
.text(function(d) { return (d.name+",-- "+d.size); })
.style("opacity", function(d) { d.w = this.getComputedTextLength(); return d.dx > d.w ? 1 : 0; });
d3.select(window).on("click", function() { zoom(root); });
d3.select("select").on("change", function() {
treemap.value(this.value == "size" ? size : count).nodes(root);
zoom(node);
});
}
The code for redrawing is below.
function redrawGraphFromJson(data) {
treemap = d3.layout.treemap()
.round(false)
.size([w,h])
.value(function(d) { return d.size; })
.sticky(true);
// Draw the graph
node = root = data;
var nodes = treemap.nodes(root)
.filter(function(d) { return !d.children; });
var cell = svg.selectAll("g")
.data(nodes)
.attr("class", "cell")
.attr("transform", function(d) { return "translate(" + d.x + "," + d.y + ")"; })
.on("click", function(d) { return zoom(node == d.parent ? root : d.parent); });
cell.append("svg:rect")
.attr("width", function(d) { return d.dx - 1; })
.attr("height", function(d) { return d.dy ; })
.style("fill", function(d) { return color(d.size);});
cell.append("svg:text")
.attr("x", function(d) { return d.dx / 2; })
.attr("y", function(d) { return d.dy / 2; })
.attr("dy", ".35em")
.attr("text-anchor", "middle")
.text(function(d) { return (d.name+",-- "+d.size); })
.style("opacity", function(d) { d.w = this.getComputedTextLength(); return d.dx > d.w ? 1 : 0; });
}
Thank you.
In your redrawing of the graph, you are going to want to .exit() on the initial graph and then .enter() to update the groups with the new data. This will replace the old map with the new.
http://bost.ocks.org/mike/join/ explains it perfectly and has a really good example to look at.

D3 - circle packing multiple data

I am a bit new to D3 and still have some problems with understanding it.
I am using this tutorial Zoomable Circle Packing:
However, I don't know how to load multiple data sets.
For example I need something like (you can see on jsfiddle) but when the button is pressed, a different .JSON file is loaded (the names in both files are the same, but values are different).
The solution might be "Thinking with Joins" by mbostock but I really dont know how to use it.
Any help would be appreciated.
You can use function to call loading of json file like this:
var callJson = function (json) {
d3.json(json, function(error, root) {
if (error) return console.error(error);
svg.selectAll("circle").remove();
svg.selectAll("text").remove();
var focus = root,
nodes = pack.nodes(root),
view;
var circle = svg.selectAll("circle")
.data(nodes)
.enter().append("circle")
.attr("class", function(d) { return d.parent ? d.children ? "node" : "node node--leaf" : "node node--root"; })
.style("fill", function(d) { return d.children ? color(d.depth) : null; })
.on("click", function(d) { if (focus !== d) zoom(d), d3.event.stopPropagation(); });
var text = svg.selectAll("text")
.data(nodes)
.enter().append("text")
.attr("class", "label")
.style("fill-opacity", function(d) { return d.parent === root ? 1 : 0; })
.style("display", function(d) { return d.parent === root ? null : "none"; })
.text(function(d) { return d.name; });
var node = svg.selectAll("circle,text");
d3.select("body")
.style("background", color(-1))
.on("click", function() { zoom(root); });
zoomTo([root.x, root.y, root.r * 2 + margin]);
function zoom(d) {
var focus0 = focus; focus = d;
var transition = d3.transition()
.duration(d3.event.altKey ? 7500 : 750)
.tween("zoom", function(d) {
var i = d3.interpolateZoom(view, [focus.x, focus.y, focus.r * 2 + margin]);
return function(t) { zoomTo(i(t)); };
});
transition.selectAll("text")
.filter(function(d) { return d.parent === focus || this.style.display === "inline"; })
.style("fill-opacity", function(d) { return d.parent === focus ? 1 : 0; })
.each("start", function(d) { if (d.parent === focus) this.style.display = "inline"; })
.each("end", function(d) { if (d.parent !== focus) this.style.display = "none"; });
}
function zoomTo(v) {
var k = diameter / v[2]; view = v;
node.attr("transform", function(d) { return "translate(" + (d.x - v[0]) * k + "," + (d.y - v[1]) * k + ")"; });
circle.attr("r", function(d) { return d.r * k; });
}
});
};
... and then you call it with callJson("flare.json");
Here is working example with multiple json files - http://bl.ocks.org/chule/74e95deeadd353e42034

D3 Treemap with fisheye

I have a large dataset being visualized by a treemap. There are 2 problems I am facing:
Load and display times on localhost are very slow. Expect worse performance over ajax calls. My dataset is 400 parents, each with approximately 10-15 children. A parent only have children (children do not have children)
Because there are 400 parents on the top level the initial treemap is very crowded. What I would like to implement is the fisheye functionality. However, my code below does not work, meaning when I move the mouse over the treemap I do not see the fisheye. I see the mouse x-y being correctly outputted to the console. but not the image.
var svg = d3.select("#body").append("div")
.attr("class", "chart")
.style("width", w + "px")
.style("height", h + "px")
.append("svg:svg")
.attr("width", w)
.attr("height", h)
.append("svg:g")
.attr("transform", "translate(.5,.5)");'
d3.json("../data/flare.json", function(data) {
node = root = data;
var nodes = treemap.nodes(root)
.filter(function(d) { return !d.children; });
var cell = svg.selectAll("g")
.data(nodes)
.enter()
.append("svg:g")
.attr("class", "cell")
.attr("transform", function(d) { return "translate(" + d.x + "," + d.y + ")"; })
.on("click", function(d) { return zoom(node == d.parent ? root : d.parent); });
cell.append("svg:rect")
.attr("width", function(d) { return d.dx - 1; })
.attr("height", function(d) { return d.dy - 1; })
.style("fill", function(d) { return color(d.parent.name); });
cell.append("svg:text")
.attr("x", function(d) { return d.dx / 2; })
.attr("y", function(d) { return d.dy / 2; })
.attr("dy", ".35em")
.attr("text-anchor", "middle")
.text(function(d) { return d.name; })
.style("opacity", function(d) { d.w = this.getComputedTextLength(); return d.dx > d.w ? 1 : 0; });
d3.select(window).on("click", function() { zoom(root); });
d3.select("select").on("change", function() {
treemap.value(this.value == "size" ? size : count).nodes(root);
zoom(node);
});
var xFisheye = d3.fisheye.scale(d3.scale.identity).domain([0, w]).focus(360),
yFisheye = d3.fisheye.scale(d3.scale.identity).domain([0, h]).focus(90);
svg.on("mousemove", function() {
var mouse = d3.mouse(this);
xFisheye.focus(mouse[0]);
yFisheye.focus(mouse[1]);
redraw();
});
function redraw() {
cell.attr("x1", xFisheye).attr("x2", xFisheye);
cell.attr("y1", xFisheye).attr("y2", xFisheye);
}
});

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);
});
};

Categories