I was trying to recreate the cluster force layout using D3 v4 like this: https://bl.ocks.org/mbostock/1747543. I reused the cluster function from Mike's code, but the result was not good (http://codepen.io/aizizhang/pen/OXzJdK). Also, if I passed in an alpha parameter larger than 1, the cx and cy will not be calculated properly. Can someone give me a hand?
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Clustered Bubble Chart</title>
<script src="https://d3js.org/d3.v4.js"></script>
</head>
<body>
<script>
let margin = {
top: 100,
right: 100,
bottom: 100,
left: 100
},
height = window.innerHeight,
width = window.innerWidth,
padding = 1.5, // separation between same-color nodes
clusterPadding = 6, // separation between different-color nodes
maxRadius = 12,
n = 200, // total number of nodes
m = 10, // number of distinct clusters
z = d3.scaleOrdinal(d3.schemeCategory20),
clusters = new Array(m);
let svg = d3.select('body')
.append('svg')
.attr('height', height)
.attr('width', width)
.append('g').attr('transform', `translate(${margin.right}, ${margin.top})`);
let nodes = d3.range(200).map(() => {
let i = Math.floor(Math.random() * m),
radius = Math.sqrt((i + 1) / m * -Math.log(Math.random())) * maxRadius,
d = {
cluster: i,
r: radius,
x: Math.random() * 200,
y: Math.random() * 200
};
if (!clusters[i] || (radius > clusters[i].r)) clusters[i] = d;
return d;
});
let circles = svg.append('g')
.datum(nodes)
.selectAll('.circle')
.data(d => d)
.enter().append('circle')
.attr('r', (d) => d.r)
.attr('cx', (d) => d.x)
.attr('cy', (d) => d.y)
.attr('fill', (d) => z(d.cluster))
.attr('stroke', 'black')
.attr('stroke-width', 1);
let simulation = d3.forceSimulation(nodes)
.velocityDecay(0.2)
.force("x", d3.forceX().x(200).strength(.5))
.force("y", d3.forceY().y(200).strength(.5))
.force("collide", d3.forceCollide().radius(function(d) {
return d.r + 0.5;
}).strength(0.5).iterations(2))
.on("tick", ticked)
// .force("charge", d3.forceManyBody(100))
function ticked() {
// let alpha = simulation.alpha();
circles
.each(clustering(0.5))
.attr('cx', (d) => d.x)
.attr('cy', (d) => d.y);
}
// Move d to be adjacent to the cluster node.
function clustering(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.r + cluster.r;
if (l !== r) {
l = (l - r) / l * alpha;
d.x -= x *= l;
d.y -= y *= l;
cluster.x += x;
cluster.y += y;
}
};
}
</script>
</body>
</html>
I think part of the problem is you are using the new 4.0 collide force but you need to use the collide calculations from the original block you were copying. Here's a port of the original example:
http://bl.ocks.org/shancarter/f621ac5d93498aa1223d8d20e5d3a0f4
Hope that helps!
Related
When creating a lineRadial, how can I get the x and y coordinates of the individual points of the line?
lineGenerator.angle() returns the angle of each point
lineGenerator.radius() returns the radius of each point
let width = 700;
let height = 500;
let svg = d3.select('svg')
.attr('width', width)
.attr('height', height);
let g = svg.append('g')
.attr('transform', `translate(${width/2}, ${height/2})`);
let randomY = d3.randomNormal(0,4);
let data = d3.range(20).map( (el,i) => { return {x:i, y:randomY()}; });
let r = 200;
let startAngle = -90 * (Math.PI / 180); // convert deg to rad
let endAngle = 90 * (Math.PI / 180); // convert deg to rad
let x = d3.scaleLinear()
.domain([ 0, 20 ])
.range([ startAngle, endAngle ]);
let y = d3.scaleLinear()
.domain([ 0,4 ])
.range([r - 10, r]);
let line = d3.lineRadial()
.angle( (d) => { return x(d.x); })
.radius( (d) => { return y(d.y); });
g.append('path')
.data([data])
.attr('class', 'line')
.attr('fill', 'none')
.attr('stroke', 'blue')
.attr('d', line);
g.selectAll('points')
.data(data)
.enter().append('circle')
.attr('fill', 'none')
.attr('stroke', 'black')
.attr('cx', line.angle() )
.attr('cy', line.radius() )
.attr('r', 2);
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<svg></svg>
A little trigonometry...
let width = 700;
let height = 500;
let svg = d3.select('svg')
.attr('width', width)
.attr('height', height);
let g = svg.append('g')
.attr('transform', `translate(${width/2}, ${height/2})`);
let randomY = d3.randomNormal(0,4);
let data = d3.range(20).map( (el,i) => { return {x:i, y:randomY()}; });
let r = 200;
let startAngle = -90 * (Math.PI / 180); // convert deg to rad
let endAngle = 90 * (Math.PI / 180); // convert deg to rad
let x = d3.scaleLinear()
.domain([ 0, 20 ])
.range([ startAngle, endAngle ]);
let y = d3.scaleLinear()
.domain([ 0,4 ])
.range([r - 10, r]);
let line = d3.lineRadial()
// stash the angle and radius, while they are calculated
// angle must be rotated...
.angle( (d) => { d.a = x(d.x) - Math.PI/2; return x(d.x); })
.radius( (d) => { d.r = y(d.y); return y(d.y); });
g.append('path')
.data([data])
.attr('class', 'line')
.attr('fill', 'none')
.attr('stroke', 'blue')
.attr('d', line);
g.selectAll('points')
.data(data)
.enter().append('circle')
.attr('fill', 'none')
.attr('stroke', 'black')
.attr('transform', function(d){
// convert to cartesian
let p = {
x: (d.r * Math.cos(d.a)),
y: (d.r * Math.sin(d.a))
};
return 'translate(' + p.x + ',' + p.y + ')';
})
.attr('r', 2);
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<svg></svg>
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>
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!
I am trying to modify the d3 force layout from SVG to DIV's. It seems the collision detection doesnt work as well with DIV's. You can see the working examples below.
(Another quick question, anyone know why css transform:translate isnt used for hardware acceleration)
DIV Version
<!DOCTYPE html>
<meta charset="utf-8">
<style>
body,
html {
margin: 0;
width: 100%;
height: 100%
}
.divs div {
border-radius: 50%;
background: red;
position: absolute;
}
</style>
<body>
<script src="https://d3js.org/d3.v3.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/1.11.3/jquery.js"></script>
<script>
var width = $('body').width(),
height = $('body').height(),
padding = 10, // separation between nodes
maxRadius = 30;
var n = 20, // total number of nodes
m = 1; // number of distinct clusters
var color = d3.scale.category10()
.domain(d3.range(m));
var xPos = d3.scale.ordinal()
.domain(d3.range(m))
.rangePoints([width, width], 1);
var x =
d3.scale.linear()
.domain([0, width])
.range([0, width]);
var y =
d3.scale.linear()
.domain([0, height])
.range([0, height]);
var nodes = d3.range(n).map(function() {
var i = Math.floor(Math.random() * m),
v = (i + 1) / m * -Math.log(Math.random());
return {
radius: Math.random() * maxRadius + 20,
color: color(i),
cx: xPos(i),
cy: height
};
});
var force = d3.layout.force()
.nodes(nodes)
.size([width, height])
.gravity(0)
.charge(0)
.on("tick", tick)
.start();
var $body = d3.select("body")
.append("div").attr('class', 'divs')
.attr('style', function(d) {
return 'width: ' + width + 'px; height: ' + height + 'px;';
});
var $div = $body.selectAll("div")
.data(nodes)
.enter()
.append("div")
.attr('style', function(d) {
return 'width: ' + (d.radius * 2) + 'px; height: ' + (d.radius * 2) + 'px; margin-left: -' + d.radius + 'px; margin-top: -' + d.radius + 'px;';
})
.call(force.drag);
function tick(e) {
$div
.each(gravity(.2 * e.alpha))
.each(collide(.5))
.style('left', function(d) {
return x(Math.max(d.radius, Math.min(width - d.radius, d.x))) + 'px';
})
.style('top', function(d) {
return y(Math.max(d.radius, Math.min(height - d.radius, d.y))) + 'px';
});
}
// 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 + maxRadius + 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>
<!DOCTYPE html>
<meta charset="utf-8">
<style>
body, html { margin: 0;width: 100%; height: 100%}
circle {
}
</style>
<body>
<script src="https://d3js.org/d3.v3.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/1.11.3/jquery.js"></script>
<script>
var width = $('body').width(),
height = $('body').height(),
padding = 10, // separation between nodes
maxRadius = 40;
var n = 10, // total number of nodes
m = 1; // number of distinct clusters
var color = d3.scale.category10()
.domain(d3.range(m));
var x = d3.scale.ordinal()
.domain(d3.range(m))
.rangePoints([width - 200, width], 1);
var nodes = d3.range(n).map(function() {
var i = Math.floor(Math.random() * m),
v = (i + 1) / m * -Math.log(Math.random());
return {
radius: Math.random() * maxRadius + 30,
color: color(i),
cx: x(i),
cy: height
};
});
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)
.attr("height", height);
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);
function tick(e) {
circle
.each(gravity(.2 * e.alpha))
.each(collide(.5))
//.attr("cx", function(d) { return d.x; })
//.attr("cy", function(d) { return d.y; });
.attr("cx", function(d) { return d.x = Math.max(d.radius, Math.min(width - d.radius, d.x)); })
.attr("cy", function(d) { return d.y = Math.max(d.radius, Math.min(height - d.radius, 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 + maxRadius + 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>
The reason is you are not updating the d.x and d.y in the tick so the layout will never come to know if they are colliding or not.
.style('left', function(d) {
//update the d.x
d.x = x(Math.max(d.radius, Math.min(width - d.radius, d.x)))
return d.x + 'px';
})
.style('top', function(d) {
//update the d.y
d.y=y(Math.max(d.radius, Math.min(height - d.radius, d.y)));
return d.y + "px"
});
Working code:
<!DOCTYPE html>
<meta charset="utf-8">
<style>
body,
html {
margin: 0;
width: 100%;
height: 100%
}
.divs div {
border-radius: 50%;
background: red;
position: absolute;
}
</style>
<body>
<script src="https://d3js.org/d3.v3.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/1.11.3/jquery.js"></script>
<script>
var width = $('body').width(),
height = $('body').height(),
padding = 10, // separation between nodes
maxRadius = 30;
var n = 20, // total number of nodes
m = 1; // number of distinct clusters
var color = d3.scale.category10()
.domain(d3.range(m));
var xPos = d3.scale.ordinal()
.domain(d3.range(m))
.rangePoints([width, width], 1);
var x =
d3.scale.linear()
.domain([0, width])
.range([0, width]);
var y =
d3.scale.linear()
.domain([0, height])
.range([0, height]);
var nodes = d3.range(n).map(function() {
var i = Math.floor(Math.random() * m),
v = (i + 1) / m * -Math.log(Math.random());
return {
radius: Math.random() * maxRadius + 20,
color: color(i),
cx: xPos(i),
cy: height
};
});
var force = d3.layout.force()
.nodes(nodes)
.size([width, height])
.gravity(0)
.charge(0)
.on("tick", tick)
.start();
var $body = d3.select("body")
.append("div").attr('class', 'divs')
.attr('style', function(d) {
return 'width: ' + width + 'px; height: ' + height + 'px;';
});
var $div = $body.selectAll("div")
.data(nodes)
.enter()
.append("div")
.attr('style', function(d) {
return 'width: ' + (d.radius * 2) + 'px; height: ' + (d.radius * 2) + 'px; margin-left: -' + d.radius + 'px; margin-top: -' + d.radius + 'px;';
})
.call(force.drag);
function tick(e) {
$div
.each(gravity(.2 * e.alpha))
.each(collide(.5))
.style('left', function(d) {
d.x = x(Math.max(d.radius, Math.min(width - d.radius, d.x)))
return d.x + 'px';
})
.style('top', function(d) {
d.y=y(Math.max(d.radius, Math.min(height - d.radius, d.y)));
return d.y + "px"
});
}
// 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 + maxRadius + 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>
Hope this helps!
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.