All,
I know there are many examples for D3.js collision detection for circles and rectangles.
I'm trying to do force simulation of ellipse nodes.
I tried following snippet which is originally for rectangles, but it's not perfect.
var width = 960,
height = 500,
minSize = 10,
maxSize = 30;
var n = 20,
m = 10;
var color = d3.scaleOrdinal(d3.schemeCategory10)
.domain(d3.range(m));
var nodes = d3.range(n).map(function() {
var c = Math.floor(Math.random() * m),
rx = Math.sqrt((c + 1) / m * -Math.log(Math.random())) * (maxSize - minSize) + minSize,
ry = Math.sqrt((c + 1) / m * -Math.log(Math.random())) * (maxSize - minSize) + minSize,
d = {color: c, rx: rx, ry: ry};
return d;
});
var collide = function(alpha) {
var quadtree = d3.quadtree()
.x((d) => d.x)
.y((d) => d.y)
.addAll(nodes);
nodes.forEach((d) => {
quadtree.visit((quad, x0, y0, x1, y1) => {
let updated = false;
if (quad.data && (quad.data !== d)) {
let x = d.x - quad.data.x,
y = d.y - quad.data.y,
xSpacing = (quad.data.rx + d.rx),
ySpacing = (quad.data.ry + d.ry),
absX = Math.abs(x),
absY = Math.abs(y),
l, lx, ly;
if (absX < xSpacing && absY < ySpacing) {
l = Math.sqrt(x * x + y * y);
lx = (absX - xSpacing) / l * alpha;
ly = (absY - ySpacing) / l * alpha;
if (Math.abs(lx) > Math.abs(ly)) {
lx = 0;
} else {
ly = 0;
}
d.x -= x *= lx;
d.y -= y *= ly;
quad.data.x += x;
quad.data.y += y;
updated = true;
}
}
return updated;
});
});
};
var force = d3.forceSimulation()
.nodes(nodes)
.force("center", d3.forceCenter())
.force("collide", (alpha) => collide(alpha))
.force("x", d3.forceX().strength(.01))
.force("y", d3.forceY().strength(.01))
.on("tick", tick);
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height)
.append('g')
.attr('transform', 'translate(' + width / 2 + ',' + height / 2 + ')');
var ellipse = svg.selectAll("ellipse")
.data(nodes)
.enter().append("ellipse")
.attr("rx", function(d) { return d.rx; })
.attr("ry", function(d) { return d.ry; })
.style("fill", function(d) { return color(d.color); })
.call(d3.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended));
function tick() {
ellipse
.attr("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; });
}
function dragstarted(d) {
if (!d3.event.active) force.alphaTarget(0.3).restart();
d.x = d.x;
d.y = d.y;
}
function dragged(d) {
d.x = d3.event.x;
d.y = d3.event.y;
}
function dragended(d) {
if (!d3.event.active) force.alphaTarget(0);
d.x = d3.event.x;
d.y = d3.event.y;
}
<script src="https://d3js.org/d3.v4.min.js"></script>
There are too many gaps between nodes, and I know it's because ellipses are treated as rectangles in the collision detection.
Anybody who's got a good solution for this?
Thanks, in advance.
I've figured this out by myself.
Here's the collision detection library for d3.
ellipse-collision-detection
I've attached working example in the above repository.
Thanks!
Related
I have two questions, one about the icons and other about code duplication.
I'm using this example:
http://bl.ocks.org/rveciana/6184054/bd294b921ebf2180eccc3aca548c895367fca2d2
(Thanks Roger Veciana)
I want to add an icon to each of those cirlces but I need to put the url inside a json instead of using the script to generate those cirlces (that is inside the code). Not sure what I'm doing wrong but is not working. Any tip on how can I add the icon to each circle ?
I've also added some text, and it is working. Basically I do this:
var circlemaint = svgmaint.selectAll("circle")
.data(nodes.maint)
.enter().append("g").append("circle")
.attr("r", function(d) { return d.radius; })
.attr("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; })
.style("fill", function(d) { return d.color; })
.call(d3.drag);
var txtmaint = svgmaint.selectAll("circle")
.select('text')
.data(nodes.maint)
.enter().append('text')
.text(node => node.id)
.attr('font-size', 18)
.attr('dx', -25)
.attr('dy',20)
The issue here, is that I have 9 different svg's, and I don't want to duplicate this per svg. How can avoid code duplication?
I finally call it within the tick function and I do something like this:
function tick(e) {
simulation.alpha(0.2)
circlemaint
.each(gravity(this.alpha()))
.each(collide(.5))
.attr("cx", function(d) { return d.x; })
.attr("cy", function(d) { return d.y; });
txtmaint
.each(gravity(this.alpha()))
.each(collide(.5))
.attr("x", function(d) { return d.x; })
.attr("y", function(d) { return d.y; });
circleXXXX
......
txtXXXXXX
......
}
And here is the code of the example:
//Based in
///http://bl.ocks.org/mbostock/1804919
var margin = {
top: 0,
right: 0,
bottom: 0,
left: 0
},
width = 960 - margin.left - margin.right,
height = 500 - margin.top - margin.bottom;
var rect = [50, 50, width - 50, height - 50];
var n = 20,
m = 4,
padding = 6,
maxSpeed = 3,
radius = d3.scale.sqrt().range([0, 8]),
color = d3.scale.category10().domain(d3.range(m));
var nodes = [];
for (i in d3.range(n)) {
nodes.push({
radius: radius(1 + Math.floor(Math.random() * 4)),
color: color(Math.floor(Math.random() * m)),
x: rect[0] + (Math.random() * (rect[2] - rect[0])),
y: rect[1] + (Math.random() * (rect[3] - rect[1])),
speedX: (Math.random() - 0.5) * 2 * maxSpeed,
speedY: (Math.random() - 0.5) * 2 * maxSpeed
});
}
var force = d3.layout.force()
.nodes(nodes)
.size([width, height])
.gravity(0)
.charge(0)
.on("tick", tick)
.start();
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("svg:rect")
.attr("width", rect[2] - rect[0])
.attr("height", rect[3] - rect[1])
.attr("x", rect[0])
.attr("y", rect[1])
.style("fill", "None")
.style("stroke", "#222222");
var circle = svg.selectAll("circle")
.data(nodes)
.enter().append("circle")
.attr("r", function(d) {
return d.radius;
})
.attr("cx", function(d) {
return d.x;
})
.attr("cy", function(d) {
return d.y;
})
.style("fill", function(d) {
return d.color;
})
.call(force.drag);
var flag = false;
function tick(e) {
force.alpha(0.1)
circle
.each(gravity(e.alpha))
.each(collide(.5))
.attr("cx", function(d) {
return d.x;
})
.attr("cy", function(d) {
return d.y;
});
}
// Move nodes toward cluster focus.
function gravity(alpha) {
return function(d) {
if ((d.x - d.radius - 2) < rect[0]) d.speedX = Math.abs(d.speedX);
if ((d.x + d.radius + 2) > rect[2]) d.speedX = -1 * Math.abs(d.speedX);
if ((d.y - d.radius - 2) < rect[1]) d.speedY = -1 * Math.abs(d.speedY);
if ((d.y + d.radius + 2) > rect[3]) d.speedY = Math.abs(d.speedY);
d.x = d.x + (d.speedX * alpha);
d.y = d.y + (-1 * d.speedY * alpha);
};
}
// Resolve collisions between nodes.
function collide(alpha) {
var quadtree = d3.geom.quadtree(nodes);
return function(d) {
var r = d.radius + radius.domain()[1] + padding,
nx1 = d.x - r,
nx2 = d.x + r,
ny1 = d.y - r,
ny2 = d.y + r;
quadtree.visit(function(quad, x1, y1, x2, y2) {
if (quad.point && (quad.point !== d)) {
var x = d.x - quad.point.x,
y = d.y - quad.point.y,
l = Math.sqrt(x * x + y * y),
r = d.radius + quad.point.radius + (d.color !== quad.point.color) * padding;
if (l < r) {
l = (l - r) / l * alpha;
d.x -= x *= l;
d.y -= y *= l;
quad.point.x += x;
quad.point.y += y;
}
}
return x1 > nx2 ||
x2 < nx1 ||
y1 > ny2 ||
y2 < ny1;
});
};
}
<script src="https://d3js.org/d3.v3.js"></script>
Thank you so much!!
I have a D3 visualization with multiple clusters and I use a gravity function to get all the circles to the center of the focus. However, with this, the circles are overlapping.
I tried this block https://bl.ocks.org/mbostock/3231298 by converting it into V5 but I cant get it to work.
I am using a negative charge to repel the nodes, obviously when I call the gravity function in tick, it gets the codes to the center of cluster focus
force = d3.forceSimulation(nodes)
.force('charge', d3.forceManyBody(-100))
.force('collision', d3.forceCollide().radius(function (d) {
return 1.1*d.radius;
}))
.on('tick', tick);
force.alpha(0.01);
force.alphaDecay = 0.1;
force.alphaTarget(.001);
force.force('x', d3.forceX().x(function (d) {
return foci[d.choice].x;
}))
force.force('y', d3.forceY().y(function (d) {
return foci[d.choice].y;
}))
console.log(JSON.stringify(foci));
// Draw circle for each node.
circle = svg.selectAll("circle")
.data(nodes)
.enter().append("circle")
.attr("id", function (d) {
return d.id;
})
.attr("class", "node")
.style("stroke", "black");
;
// For smoother initial transition to settling spots.
// Need to research more on this
circle.transition()
.duration(100)
.delay(function (d, i) {
return i * 5;
})
.attrTween("r", function (d) {
var i = d3.interpolate(0, d.radius);
return function (t) {
return d.radius = i(t);
};
});
function tick(e) {
circle
.each(collide(0.5))
.each(gravity(.051 * .8))
.style("fill", function (d) {
//category is either 0 or 1
//so it's either 0+the choice or 6+the choice
//d.choice is between 0 and 5
multiplier = d.category
return colors[foci_count * multiplier + d.choice];
})
.attr("cx", function (d) {
return d.x;
})
.attr("cy", function (d) {
return d.y;
})
.attr("r", function(d) {
return d.radius;
})
;
}
function collide(alpha) {
var quadtree = d3.quadtree().addAll(nodes);
return function (d) {
// var r = d.radius + maxNodeRadius + Math.max(padding, clusterPadding),
var r = d.radius + 10,
nx1 = d.x - r,
nx2 = d.x + r,
ny1 = d.y - r,
ny2 = d.y + r;
quadtree.visit(function (quad, x1, y1, x2, y2) {
console.log("visit")
if (quad.point && (quad.point !== d)) {
var x = d.x - quad.point.x,
y = d.y - quad.point.y,
l = Math.sqrt(x * x + y * y),
r = d.radius + quad.point.radius + 10;
console.log(d.cluster);
if (l < r) {
l = (l - r) / l * alpha;
d.x -= x *= l;
d.y -= y *= l;
quad.point.x += x;
quad.point.y += y;
}
}
return x1 > nx2 || x2 < nx1 || y1 > ny2 || y2 < ny1;
});
};
}
// Move nodes toward cluster focus.
function gravity(alpha) {
return function (d) {
d.y += (foci[d.choice].y - d.y) * alpha;
d.x += (foci[d.choice].x - d.x) * alpha;
};
}
As #rioV8 has mentioned, all I needed to do was re-initialize the collide force with the updated nodes. So I updated my tick function to the following
function tick(e) {
circle
.each(gravity(.051 * .8))
.style("fill", function (d) {
//category is either 0 or 1
//so it's either 0+the choice or 6+the choice
//d.choice is between 0 and 5
multiplier = d.category
return colors[foci_count * multiplier + d.choice];
})
.attr("cx", function (d) {
return d.x;
})
.attr("cy", function (d) {
return d.y;
})
.attr("r", function(d) {
return d.radius;
});
force.force('collision', d3.forceCollide().radius(function (d) {
return 1.3 * d.radius;
}));
}
I am working on a D3 chart, but can't get the node labeling right (I would like to put the "size" on a second line, and change all the text to white. For instance, the "Toyota" node would say "Toyota Motor" and on a new line just below, "61.84").
Here is my starting point. I tried to add a pre block with the CSV data so it could run on JSfiddle, but I got stuck.
I know I need to change this code in order to get the data from the pre block instead of the external CSV:
d3.text("./car_companies.csv", function(error, text) {
After that, I need to add a new "node append", something like this:
node.append("revenue")
.style("text-anchor", "middle")
.attr("dy", "1.5em")
.text(function(d) { return d.revenue.substring(0, d.radius / 3); });
http://jsfiddle.net/nick2ny/pqo1x670/4/
Thank you for any ideas.
Not directly related to the question, but to use the <pre> element to hold your data, you have to use:
var text = d3.select("pre").text();
Instead of d3.text().
Back to the question:
For printing those values, you just need:
node.append("text")
.attr("dy", "1.3em")
.style("text-anchor", "middle")
.style("fill", "white")
.text(function(d) {
return d.size;
});
Adjusting dy the way you want. However, there is an additional problem: you're not populating size in the data array. Therefore, add this in the create_nodes function:
size: data[node_counter].size,
Here is the code with those changes:
<!DOCTYPE html>
<meta charset="utf-8">
<style type="text/css">
text {
font: 10px sans-serif;
}
pre {
display: none;
}
circle {
stroke: #565352;
stroke-width: 1;
}
</style>
<body>
<pre id="data">
Toyota Motor,61.84,Asia,239
Volkswagen,44.54,Europe,124
Daimler,40.79,Europe,104
BMW,35.78,Europe,80
Ford Motor,31.75,America,63
General Motors,30.98,America,60
</pre>
<script src="https://d3js.org/d3.v3.min.js"></script>
<script>
Array.prototype.contains = function(v) {
for (var i = 0; i < this.length; i++) {
if (this[i] === v) return true;
}
return false;
};
var width = 500,
height = 500,
padding = 1.5, // separation between same-color nodes
clusterPadding = 6, // separation between different-color nodes
maxRadius = 12;
var color = d3.scale.ordinal()
.range(["#0033cc", "#33cc66", "#990033"]);
var text = d3.select("pre").text();
var colNames = "text,size,group,revenue\n" + text;
var data = d3.csv.parse(colNames);
data.forEach(function(d) {
d.size = +d.size;
});
//unique cluster/group id's
var cs = [];
data.forEach(function(d) {
if (!cs.contains(d.group)) {
cs.push(d.group);
}
});
var n = data.length, // total number of nodes
m = cs.length; // number of distinct clusters
//create clusters and nodes
var clusters = new Array(m);
var nodes = [];
for (var i = 0; i < n; i++) {
nodes.push(create_nodes(data, i));
}
var force = d3.layout.force()
.nodes(nodes)
.size([width, height])
.gravity(.02)
.charge(0)
.on("tick", tick)
.start();
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
var node = svg.selectAll("circle")
.data(nodes)
.enter().append("g").call(force.drag);
node.append("circle")
.style("fill", function(d) {
return color(d.cluster);
})
.attr("r", function(d) {
return d.radius
})
node.append("text")
.attr("dy", ".3em")
.style("text-anchor", "middle")
.style("fill", "white")
.text(function(d) {
return d.text;
});
node.append("text")
.attr("dy", "1.3em")
.style("text-anchor", "middle")
.style("fill", "white")
.text(function(d) {
return d.size;
});
function create_nodes(data, node_counter) {
var i = cs.indexOf(data[node_counter].group),
r = Math.sqrt((i + 1) / m * -Math.log(Math.random())) * maxRadius,
d = {
cluster: i,
radius: data[node_counter].size * 1.5,
text: data[node_counter].text,
size: data[node_counter].size,
revenue: data[node_counter].revenue,
x: Math.cos(i / m * 2 * Math.PI) * 200 + width / 2 + Math.random(),
y: Math.sin(i / m * 2 * Math.PI) * 200 + height / 2 + Math.random()
};
if (!clusters[i] || (r > clusters[i].radius)) clusters[i] = d;
return d;
};
function tick(e) {
node.each(cluster(10 * e.alpha * e.alpha))
.each(collide(.5))
.attr("transform", function(d) {
var k = "translate(" + d.x + "," + d.y + ")";
return k;
})
}
// Move d to be adjacent to the cluster node.
function cluster(alpha) {
return function(d) {
var cluster = clusters[d.cluster];
if (cluster === d) return;
var x = d.x - cluster.x,
y = d.y - cluster.y,
l = Math.sqrt(x * x + y * y),
r = d.radius + cluster.radius;
if (l != r) {
l = (l - r) / l * alpha;
d.x -= x *= l;
d.y -= y *= l;
cluster.x += x;
cluster.y += y;
}
};
}
// Resolves collisions between d and all other circles.
function collide(alpha) {
var quadtree = d3.geom.quadtree(nodes);
return function(d) {
var r = d.radius + maxRadius + Math.max(padding, clusterPadding),
nx1 = d.x - r,
nx2 = d.x + r,
ny1 = d.y - r,
ny2 = d.y + r;
quadtree.visit(function(quad, x1, y1, x2, y2) {
if (quad.point && (quad.point !== d)) {
var x = d.x - quad.point.x,
y = d.y - quad.point.y,
l = Math.sqrt(x * x + y * y),
r = d.radius + quad.point.radius + (d.cluster === quad.point.cluster ? padding : clusterPadding);
if (l < r) {
l = (l - r) / l * alpha;
d.x -= x *= l;
d.y -= y *= l;
quad.point.x += x;
quad.point.y += y;
}
}
return x1 > nx2 || x2 < nx1 || y1 > ny2 || y2 < ny1;
});
};
}
</script>
This is the code of the bubble chart i created. I have used force layout to create the chart.
var margin = {
top: 10,
right: 10,
bottom: 10,
left: 10
},
width = 1000 - margin.left - margin.right,
height = 600 - margin.top - margin.bottom;
d3.select('#' + divId).append('div').attr('id', 'chart').attr('class', 'chart');
var n = data.vistaJson.length;
m = 1,
padding = 5,
radius = d3.scale.sqrt().range([10, 50]),
color = d3.scale.category10().domain(d3.range(m)),
x = d3.scale.ordinal().domain(d3.range(m)).rangePoints([0, width], 1);
var xscale = d3.scale.linear()
.domain([0, 500])
.range([20, 500]);
var nodes = [];
for(var i=0; i< n; i++){
var coordinates = data.vistaJson[i].SLAB.split('_');
v = data.vistaJson[i].COUNT
nodes.push({
radius: radius(v),
color: color(i),
count: v,
cx: xscale(x(i)),
cy: xscale(height / 2),
xAxis: coordinates[0],
yAxis: coordinates[1]
});
}
var svg = d3.select("#chart").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 + ")");
var force = d3.layout.force()
.nodes(nodes)
.size([width, height])
.gravity(0.5)
.charge(0.5)
.on("tick", tick)
.start();
var circle = svg.selectAll("circle")
.data(nodes)
.enter().append("circle")
.attr("r", function (d) {
return d.radius;
})
.style("fill", function (d) {
return d.color;
})
.call(force.drag);
var labels = svg.selectAll("text")
.data(nodes)
.enter()
.append("text")
.attr({"x":function(d){return d.x;},
"y":function(d){return d.y;}})
.text(function(d){return d.count;})
.call(force.drag);
circle.each(gravity(.2 * e.alpha))
.each(collide(.5))
.attr("cx", function (d) {
return d.x;
})
.attr("cy", function (d) {
return d.y;
});
labels.attr("x", function(d) { return d.x; })
.attr("y", function(d) { return d.y; });
}
// Move nodes toward cluster focus.
function gravity(alpha) {
return function (d) {
d.y += (d.cy - d.y) * alpha;
d.x += (d.cx - d.x) * alpha;
};
}
// Resolve collisions between nodes.
function collide(alpha) {
var quadtree = d3.geom.quadtree(nodes);
return function (d) {
var r = d.radius + radius.domain()[1] + padding,
nx1 = d.x - r,
nx2 = d.x + r,
ny1 = d.y - r,
ny2 = d.y + r;
quadtree.visit(function (quad, x1, y1, x2, y2) {
if (quad.point && (quad.point !== d)) {
var x = d.x - quad.point.x,
y = d.y - quad.point.y,
l = Math.sqrt(x * x + y * y),
r = d.radius + quad.point.radius + (d.color !== quad.point.color) * padding;
if (l < r) {
l = (l - r) / l * alpha;
d.x -= x *= l;
d.y -= y *= l;
quad.point.x += x;
quad.point.y += y;
}
}
return x1 > nx2 || x2 < nx1 || y1 > ny2 || y2 < ny1;
});
};
}
I want to add title to the node which is displayed when mouse is hovered on the node .
Earlier I used pack layout and I gave title like this :
var node = vis.selectAll("g.node")
.data(bubble.nodes(classes(json), function(d) { return d.name; })
.filter(function(d) { return !d.children; }))
.enter()
.append("svg:g")
.attr("class", "node")
.attr("transform", function(d) { return "translate(" + xscale(d.x) + "," + xscale(d.y) + ")"; });
node.append("svg:title")
.text(function(d) { return d.xAxis + ": " + d.yAxis; });
How can we show title when using forceLayout . Please help .
I think it should look something like this in the force layout:
var node = svg.selectAll(".node")
.data(graph.nodes)
.enter().append("circle")
.attr("class", function(d) { return d.nodes ? "nonleaf" : "leaf"; })
.attr("r", 5)
.call(force.drag);
node.append("title")
.text(function(d) { return d.count; });
I hope this helps somehow. For me it works like that..
I've got a fairly simple reusable chart built in D3.js -- some circles and some text.
I'm struggling to figure out how to update the chart with new data, without redrawing the entire chart.
With the current script, I can see that the new data is bound to the svg element, but none of the data-driven text or attributes is updating. Why isn't the chart updating?
Here's a fiddle: http://jsfiddle.net/rolfsf/em5kL/1/
I'm calling the chart like this:
d3.select('#clusters')
.datum({
Name: 'Total Widgets',
Value: 224,
Clusters: [
['Other', 45],
['FooBars', 30],
['Foos', 50],
['Bars', 124],
['BarFoos', 0]
]
})
.call( clusterChart() );
When the button is clicked, I'm simply calling the chart again, with different data:
$("#doSomething").on("click", function(){
d3.select('#clusters')
.datum({
Name: 'Total Widgets',
Value: 122,
Clusters: [
['Other', 14],
['FooBars', 60],
['Foos', 22],
['Bars', 100],
['BarFoos', 5]
]
})
.call( clusterChart() );
});
The chart script:
function clusterChart() {
var width = 450,
margin = 0,
radiusAll = 72,
maxRadius = radiusAll - 5,
r = d3.scale.linear(),
padding = 1,
height = 3 * (radiusAll*2 + padding),
startAngle = Math.PI / 2,
onTotalMouseOver = null,
onTotalClick = null,
onClusterMouseOver = null,
onClusterClick = null;
val = function(d){return d};
function chart(selection) {
selection.each(function(data) {
var cx = width / 2,
cy = height / 2,
stepAngle = 2 * Math.PI / data.Clusters.length,
outerRadius = 2*radiusAll + padding;
r = d3.scale.linear()
.domain([0, d3.max(data.Clusters, function(d){return d[1];})])
.range([50, maxRadius]);
var svg = d3.select(this).selectAll("svg")
.data([data])
.enter().append("svg");
//enter
var totalCircle = svg.append("circle")
.attr("class", "total-cluster")
.attr('cx', cx)
.attr('cy', cy)
.attr('r', radiusAll)
.on('mouseover', onTotalMouseOver)
.on('click', onTotalClick);
var totalName = svg.append("text")
.attr("class", "total-name")
.attr('x', cx)
.attr('y', cy + 16);
var totalValue = svg.append("text")
.attr("class", "total-value")
.attr('x', cx)
.attr('y', cy + 4);
var clusters = svg.selectAll('circle.cluster')
.data(data.Clusters)
.enter().append('circle')
.attr("class", "cluster");
var clusterValues = svg.selectAll("text.cluster-value")
.data(data.Clusters)
.enter().append('text')
.attr('class', 'cluster-value');
var clusterNames = svg.selectAll("text.cluster-name")
.data(data.Clusters)
.enter().append('text')
.attr('class', 'cluster-name');
clusters .attr('cx', function(d, i) { return cx + Math.cos(startAngle + stepAngle * i) * outerRadius; })
.attr('cy', function(d, i) { return cy + Math.sin(startAngle + stepAngle * i) * outerRadius; })
.attr("r", "10")
.on('mouseover', function(d, i, j) {
if (onClusterMouseOver != null) onClusterMouseOver(d, i, j);
})
.on('mouseout', function() { /*do something*/ })
.on('click', function(d, i){ onClusterClick(d); });
clusterNames
.attr('x', function(d, i) { return cx + Math.cos(startAngle + stepAngle * i) * outerRadius; })
.attr('y', function(d, i) { return cy + Math.sin(startAngle + stepAngle * i) * outerRadius + 16; });
clusterValues
.attr('x', function(d, i) { return cx + Math.cos(startAngle + stepAngle * i) * outerRadius; })
.attr('y', function(d, i) { return cy + Math.sin(startAngle + stepAngle * i) * outerRadius - 4; });
//update with data
svg .selectAll('text.total-value')
.text(val(data.Value));
svg .selectAll('text.total-name')
.text(val(data.Name));
clusters
.attr('class', function(d, i) {
if(d[1] === 0){ return 'cluster empty'}
else {return 'cluster'}
})
.attr("r", function (d, i) { return r(d[1]); });
clusterValues
.text(function(d) { return d[1] });
clusterNames
.text(function(d, i) { return d[0] });
$(window).resize(function() {
var w = $('.cluster-chart').width(); //make this more generic
svg.attr("width", w);
svg.attr("height", w * height / width);
});
});
}
chart.width = function(_) {
if (!arguments.length) return width;
width = _;
return chart;
};
chart.onClusterClick = function(_) {
if (!arguments.length) return onClusterClick;
onClusterClick = _;
return chart;
};
return chart;
}
I have applied the enter/update/exit pattern across all relevant svg elements (including the svg itself). Here is an example segment:
var clusterValues = svg.selectAll("text.cluster-value")
.data(data.Clusters,function(d){ return d[1];});
clusterValues.exit().remove();
clusterValues
.enter().append('text')
.attr('class', 'cluster-value');
...
Here is a complete FIDDLE with all parts working.
NOTE: I tried to touch your code as little as possible since you have carefully gone about applying a re-usable approach. This the reason why the enter/update/exit pattern is a bit different between the total circle (and text), and the other circles (and text). I might have gone about this using a svg:g element to group each circle and associated text.