Transition of triangle svg path in D3.js - javascript

I would like to create 30 equilateral triangles of different dimensions and colors, in a random position inside the body, that are moving away from the current mouse position, stopping at a short distance from the border of the body.
Here is the code:
var body = d3.select("body");
var mouse = [];
var width = 1000;
var height = 600;
var numberOfTriangles = 30;
var isMouseMoving = false;
var triangle = d3.symbol()
.type(d3.symbolTriangle);
function drawTriangles(number) {
for(var i=0;i<number;i++){
var dim = Math.random()*400;
svg.append("path")
.attr("d", triangle.size(dim))
.attr("transform", function(d) { return "translate(" + Math.random()*width + "," + Math.random()*height + ")"; })
.attr("fill", "rgb("+parseInt(Math.random()*255)+","+parseInt(Math.random()*255)+","+parseInt(Math.random()*255)+")")
.attr("opacity", 2)
.attr("class", "path"+i);
}
}
function moveMouse(number) {
if (isMouseMoving) {
console.log("posix: x="+mouse[0]+",y="+mouse[1]+"\n");
for(var i=0;i<number;i++){
svg.select('.path'+i).transition().duration(5)
.attr({'x':-mouse[0],'y':-mouse[1]})
}
}
}
var svg = body.append("svg")
.attr("width", width)
.attr("height", height)
.style("border", "1px solid black")
.on("mousemove", function() {
mouse = d3.mouse(this);
isMouseMoving=true;
});
drawTriangles(numberOfTriangles);
d3.timer(function(){moveMouse(numberOfTriangles)});
I have few questions:
1) How to position the triangles inside the body? In this way that I have done, a little part of some triangles is outside the body. Math.random()*weight and Math.random()*height are not enough?
2) I saw some examples that implement the transition like I have done, but in my case doesn't work. How can I implement the transition of the triangles so that they move away from the current position of the mouse and they stop at a short distance from the border of the body?
Thank you in advice.

1) It depends on how the triangles are drawn.
If d3.symbolTriangle draws lineTo into negative x territory (i think it does), then you have the potential that the left half of your triangle will be cut off by the bounds of your body.
Try adding 1/2 of the triangle's width when you translate it on the x direction.
2) At the moment, your code is attempting to move the triangles to the position of the mouse, not away from it. Here is that working:
var body = d3.select("body");
var mouse = [];
var width = 1000;
var height = 600;
var numberOfTriangles = 30;
var isMouseMoving = false;
var triangle = d3.symbol()
.type(d3.symbolTriangle);
function drawTriangles(number) {
for (var i = 0; i < number; i++) {
var dim = Math.random() * 400;
svg.append("path")
.attr("d", triangle.size(dim))
.attr("transform", function(d) {
return "translate(" + Math.random() * width + "," + Math.random() * height + ")";
})
.attr("fill", "rgb(" + parseInt(Math.random() * 255) + "," + parseInt(Math.random() * 255) + "," + parseInt(Math.random() * 255) + ")")
.attr("opacity", 2)
.attr("class", "path" + i);
}
}
function moveMouse() {
if (isMouseMoving) {
//console.log("posix: x="+mouse[0]+",y="+mouse[1]+"\n");
svg.selectAll('path').each(function(d, i) {
var self = d3.select(this);
self.attr('transform', function() {
return "translate(" + mouse[0] + "," + mouse[1] + ")";
})
})
}
}
var svg = body.append("svg")
.attr("width", width)
.attr("height", height)
.style("border", "1px solid black")
.on("mousemove", function() {
mouse = d3.mouse(this);
isMouseMoving = true;
});
drawTriangles(numberOfTriangles);
d3.timer(function() {
moveMouse()
});
Fiddle: https://jsfiddle.net/hLcvdpjk/
D3 triangle: https://github.com/d3/d3-shape/blob/master/src/symbol/triangle.js

Related

Iterative/chained transitions along line graph with discrete points and delay

I created a jsfiddle here.
I do have a graph - in this case a sine wave - and want to move a circle along this line (triggered by a click event), stop at certain x and y value pairs that are on this graph and then move on to the last point of the graph from where it jumps to the first again (ideally this should go on until I press a stop button).
My current problem is that the circle only moves horizontally but not in the ordinate direction and also the delay is visible only once (in the very beginning).
The relevant code is this one (the entire running example can be found in the link above):
Creation of the circle:
// the circle I want to move along the graph
var circle = svg.append("circle")
.attr("id", "concindi")
.attr("cx", x_scale(xval[0]))
.attr("cy", y_scale(yval[0]))
.attr("transform", "translate(" + (0) + "," + (-1 * padding + 15) + ")")
.attr("r", 6)
.style("fill", 'red');
The moving process:
var coordinates = d3.zip(xval, yval);
svg.select("#concindi").on("click", function() {
coordinates.forEach(function(ci, indi){
//console.log(ci[1] + ": " + indi);
//console.log(coordinates[indi+1][1] + ": " + indi);
if (indi < (coordinates.length - 1)){
//console.log(coordinates[indi+1][1] + ": " + indi);
console.log(coordinates[indi + 1][0]);
console.log(coordinates[indi + 1][1]);
d3.select("#concindi")
.transition()
.delay(2000)
.duration(5000)
.ease("linear")
.attr("cx", x_scale(coordinates[indi + 1][0]))
.attr("cy", y_scale(coordinates[indi + 1][1]));
}
});
I am pretty sure that I use the loop in a wrong manner. The idea is to start at the first x/y pair, then move to the next one (which takes 5s), wait there for 2s and move on to the next and so on. Currently, the delay is only visible initially and then it just moves horizontally.
How would this be done correctly?
Why don't you use Bostock's translateAlong function?
function translateAlong(path) {
var l = path.getTotalLength();
return function(d, i, a) {
return function(t) {
var p = path.getPointAtLength(t * l);
return "translate(" + p.x + "," + p.y + ")";
};
};
}
Here is the demo:
// function to generate some data
function get_sin_val(value) {
return 30 * Math.sin(value * 0.25) + 35;
}
var width = 400;
var height = 200;
var padding = 50;
var svg = d3.select("body")
.append("svg")
.attr("width", width)
.attr("height", height);
var xrange_min = 0;
var xrange_max = 50;
var yrange_min = 0;
var yrange_max = 100;
var x_scale = d3.scale.linear()
.domain([xrange_min, xrange_max])
.range([padding, width - padding * 2]);
var y_scale = d3.scale.linear()
.domain([yrange_min, yrange_max])
.range([height - padding, padding]);
// create the data
var xval = d3.range(xrange_min, xrange_max, 1);
var yval = xval.map(get_sin_val);
// just for convenience
var coordinates = d3.zip(xval, yval);
//defining line graph
var lines = d3.svg.line()
.x(function(d) {
return x_scale(d[0]);
})
.y(function(d) {
return y_scale(d[1]);
})
.interpolate("linear");
//draw graph
var sin_graph = svg.append("path")
.attr("d", lines(coordinates))
.attr("stroke", "blue")
.attr("stroke-width", 2)
.attr("fill", "none");
// the circle I want to move along the graph
var circle = svg.append("circle")
.attr("id", "concindi")
.attr("transform", "translate(" + (x_scale(xval[0])) + "," + (y_scale(yval[0])) + ")")
.attr("r", 6)
.style("fill", 'red');
svg.select("#concindi").on("click", function() {
d3.select(this).transition()
.duration(5000)
.attrTween("transform", translateAlong(sin_graph.node()));
});
// Returns an attrTween for translating along the specified path element.
function translateAlong(path) {
var l = path.getTotalLength();
return function(d, i, a) {
return function(t) {
var p = path.getPointAtLength(t * l);
return "translate(" + p.x + "," + p.y + ")";
};
};
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
You have to understand that forEach will loop to the end of the array almost instantaneously. Thus, you cannot make the circle jumping to one coordinate to the other with your approach right now (thus, unfortunately, you are correct here:"I am pretty sure that I use the loop in a wrong manner").
If you want to add the 2s waiting period between one point and another, the best idea is chaining the transitions. Something like this (I'm reducing the delay and the duration times in the demo, so we can better see the effect):
var counter = 0;
transit();
function transit() {
counter++;
d3.select(that).transition()
.delay(500)
.duration(500)
.attr("transform", "translate(" + (x_scale(coordinates[counter][0]))
+ "," + (y_scale(coordinates[counter][1])) + ")")
.each("end", transit);
}
Here is the demo:
// function to generate some data
function get_sin_val(value) {
return 30 * Math.sin(value * 0.25) + 35;
}
var width = 400;
var height = 200;
var padding = 50;
var svg = d3.select("body")
.append("svg")
.attr("width", width)
.attr("height", height);
var xrange_min = 0;
var xrange_max = 50;
var yrange_min = 0;
var yrange_max = 100;
var x_scale = d3.scale.linear()
.domain([xrange_min, xrange_max])
.range([padding, width - padding * 2]);
var y_scale = d3.scale.linear()
.domain([yrange_min, yrange_max])
.range([height - padding, padding]);
// create the data
var xval = d3.range(xrange_min, xrange_max, 1);
var yval = xval.map(get_sin_val);
// just for convenience
var coordinates = d3.zip(xval, yval);
//defining line graph
var lines = d3.svg.line()
.x(function(d) {
return x_scale(d[0]);
})
.y(function(d) {
return y_scale(d[1]);
})
.interpolate("linear");
//draw graph
var sin_graph = svg.append("path")
.attr("d", lines(coordinates))
.attr("stroke", "blue")
.attr("stroke-width", 2)
.attr("fill", "none");
// the circle I want to move along the graph
var circle = svg.append("circle")
.attr("id", "concindi")
.attr("transform", "translate(" + (x_scale(xval[0])) + "," + (y_scale(yval[0])) + ")")
.attr("r", 6)
.style("fill", 'red');
svg.select("#concindi").on("click", function() {
var counter = 0;
var that = this;
transit();
function transit() {
counter++;
d3.select(that).transition()
.delay(500)
.duration(500)
.attr("transform", "translate(" + (x_scale(coordinates[counter][0])) + "," + (y_scale(coordinates[counter][1])) + ")")
.each("end", transit);
}
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>

Specify startpoint to interpolate a circle on an arc when clicked on by the user

How do I provide the starting point of an arc for moving a circle along the path of the arc. I have a world map with several arcs displayed on it. I wish to interpolate the movement of a circle on the arc that has been selected by the user using the .on('click') event. I wish to know how I can identify the startPoint of the arc in question.
Specifically, I am not able to understand what parameters to provide in .attr("transform", "translate(" + startPoint + ")") attribute of the circle to enable the circle to start from the starting position of the arc.
At present, it passes the entire path and I receive the following error
d3.v3.min.js:1 Error: attribute transform: Expected number, "translate(M1051.5549785289…".
Although, surprisingly, the circle marker appears on the screen and interpolates along the first arc that has been drawn. However, I wish to change this interpolation to an arc that has been clicked by the user. In other words, how do I feed a new startPoint to the circle marker every time the user clicks on a different arc and to have a subsequent interpolation of the same.
var path3 = arcGroup.selectAll(".arc"),
startPoint = pathStartPoint(path3)
var marker = arcGroup.append("circle")
marker.attr("r", 7)
.attr("transform", "translate(" + startPoint + ")")
transition();
function pathStartPoint(path) {
var d = path.attr("d")
console.log(path)
dsplitted = d.split(" ");
return dsplitted[0].split(",");
}
function transition() {
marker.transition()
.duration(7500)
.attrTween("transform", translateAlong(path3.node()))
.each("end", transition);// infinite loop
}
function translateAlong(path) {
var l = path.getTotalLength();
return function(i) {
return function(t) {
var p = path.getPointAtLength(t * l);
return "translate(" + p.x + "," + p.y + ")";//Move marker
}
}
}
As #altocumulus states in the their comment, getPointAtLength doesn't need a starting point. It takes as an argument a distance from 0 to path length where 0 is the starting point. Here's a quick example, click on any path below:
<!DOCTYPE html>
<html>
<head>
<script data-require="d3#4.0.0" data-semver="4.0.0" src="https://d3js.org/d3.v4.min.js"></script>
</head>
<body>
<script>
var w = 400,
h = 400;
var svg = d3.select('body')
.append('svg')
.attr('width', w)
.attr('height', h);
var data = []
for (var i = 0; i < 5; i++) {
data.push({
x0: Math.random() * w,
y0: Math.random() * h,
x1: Math.random() * w,
y1: Math.random() * h
});
}
var marker = svg.append("circle")
.attr("r", 20)
.style("fill", "steelblue")
.style("opacity", 0);
svg.selectAll("path")
.data(data)
.enter()
.append("path")
.attr("d", function(d) {
var dx = d.x1 - d.x0,
dy = d.y1 - d.y0,
dr = Math.sqrt(dx * dx + dy * dy);
return "M" + d.x0 + "," + d.y0 + "A" + dr + "," + dr +
" 0 0,1 " + d.x1 + "," + d.y1;
})
.style("stroke", function(d, i) {
return d3.schemeCategory10[i];
})
.style("stroke-width", "10px")
.style("fill", "none")
.on("click", function(d){
marker
.style("opacity", 1)
.transition()
.duration(1000)
.attrTween("transform", translateAlong(this))
.on("end", function(d) {
marker.style("opacity", 0);
});
});
function translateAlong(path) {
var l = path.getTotalLength();
return function(i) {
return function(t) {
var p = path.getPointAtLength(t * l);
return "translate(" + p.x + "," + p.y + ")"; //Move marker
}
}
}
</script>
</body>
</html>

d3.js - how to arrange the `squre` box around the `circle` properly

I am trying to arrange the squares around the circle but i am unable to get the correct output.
Can any one help me?
// largely based on http://bl.ocks.org/4063550
// some made-up data
var data = [2,2,2,2,2,2];
// tree-ify our fake data
var dataTree = {
children: data.map(function(d) { return { size: d }; })
};
// basic settings
var w = 300,
h = 300,
maxRadius = 75;
// size scale for data
var radiusScale = d3.scale.sqrt().domain([0, d3.max(data)]).range([0, maxRadius]);
// determine the appropriate radius for the circle
var roughCircumference = d3.sum(data.map(radiusScale)) * 2,
radius = roughCircumference / (Math.PI * 2);
// make a radial tree layout
var tree = d3.layout.tree()
.size([360, radius])
.separation(function(a, b) {
return radiusScale(a.size) + radiusScale(b.size);
});
// make the svg
var svg = d3.select("body").append("svg")
.attr("width", w )
.attr("height", h )
.append("g")
.attr("transform", "translate(" + (w / 2 ) + "," + (h /2) + ")");
var c = svg.append('circle').attr({r:75})
// apply the layout to the data
var nodes = tree.nodes(dataTree);
// create dom elements for the node
var node = svg.selectAll(".node")
.data(nodes.slice(1)) // cut out the root node, we don't need it
.enter().append("g")
.attr("class", "node")
.attr("transform", function(d) {
console.log(d.x);
return "rotate(" + (d.x - 90) + ") translate(" + d.y + ")";
})
node.append("rect")
.attr({
width: 25,
height:25,
fill : 'red',
"transform":function(d) {
return "rotate(" + (-1 * d.x + 90) + ") translate(" +0+ ")";
}
});
node.append("text")
.attr({"transform":function(d) {
return "rotate(" + (-1 * d.x + 90) + ")";
},
"text-anchor": "middle"
})
.text("testing a word");
svg {
border:1px solid gray;
}
circle {
fill: steelblue;
stroke: black;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
I am looking the output like this:
A sample code to work with.
I have assumed 8 nodes to be plotted so that the circle can be divided into 8 segments. Each square to placed at the distance of Pi/4 radians. You can compute the x,y as xSin , y Cos. Then you will need to transform the rectangle to centre at x,y rather than the top left corner.
// largely based on http://bl.ocks.org/4063550
// some made-up data
var data = [2,2,2,2,2,2,2,2];
// tree-ify our fake data
var dataTree = {
children: data.map(function(d) { return { size: d }; })
};
// basic settings
var w = 300,
h = 300,
maxRadius = 75;
// size scale for data
var radiusScale = d3.scale.sqrt().domain([0, d3.max(data)]).range([0, maxRadius]);
// determine the appropriate radius for the circle
var roughCircumference = d3.sum(data.map(radiusScale)) * 2,
radius = roughCircumference / (Math.PI * 2);
// make a radial tree layout
var tree = d3.layout.tree()
.size([360, radius])
.separation(function(a, b) {
return radiusScale(a.size) + radiusScale(b.size);
});
// make the svg
var svg = d3.select("body").append("svg")
.attr("width", w )
.attr("height", h )
.append("g")
.attr("transform", "translate(" + (w / 2 ) + "," + (h /2) + ")");
var c = svg.append('circle').attr({r:75})
var r = 75;
// apply the layout to the data
var nodes = tree.nodes(dataTree);
// create dom elements for the node
var node = svg.selectAll(".node")
.data(nodes.slice(1)) // cut out the root node, we don't need it
.enter().append("g")
.attr("class", "node")
.attr("transform", function(d,i) {
return "translate(" + (r * Math.sin(Math.PI * i * 0.25)) + "," + (r * Math.cos(Math.PI * i * 0.25)) + ")";
})
node.append("rect")
.attr({
width: 25,
height:25,
fill : 'red',
"transform":function(d) {
return "translate(" +(-12.5)+ ","+ (-12.5) + ")";
}
});
node.append("text")
.attr({"transform":function(d) {
return "rotate(" + (-1 * d.x + 90) + ")";
},
"text-anchor": "middle"
})
.text("testing a word");
svg {
border:1px solid gray;
}
circle {
fill: steelblue;
stroke: black;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
Here's a fiddle.
Okay, so this was basically some pixel level trial-and-error translate manipulation. This is for node.
.attr("transform", function(d) {
console.log(d.x);
return "rotate(" + (d.x - 90) + ") translate(" + (d.y - 65 ) + ")";
})
and this for rect:
.attr({
width: 25,
height:25,
fill : 'red',
"transform":function(d) {
return "rotate(" + -(d.x - 90) + ") translate(" +(-10)+ ","+ (-10) + ")";
}
});

adding text to circles in a rectangle

I am trying to draw circles in a rectangular div. I have followed the advice from Question 13339615(the answer I used is also made available in this fiddle, and this works perfectly.
However, being completely new to d3, I cannot work out how to label the circles. What I would basically like to recreate is similar to the visualisation in this article.
I have tried the following modifications to the fiddle:
var bubbles = bubbleGroup.selectAll("circle")
.data(data)
.enter()
.append("circle")
.append("text").attr("dy", ".3em")
.style("text-anchor", "middle").text("test");
but this breaks the visualisation.
Following question 13615381 I have also tried things like:
var bubbles = bubbleGroup.selectAll("circle")
.data(data)
.enter()
.append("circle");
bubbleGroup.append("text")
.attr("dx", function(d){return -20})
.text(function(d){return "test"})
but the text does not display. I'm imaging the code should be some variation of these, but I cannot figure it out.
Thank you!
Fixed by putting the circle and text inside a g and adjusting the g css-transform.
JSFiddle
var bubbles = bubbleGroup.selectAll("g")
.data(data)
.enter().append("g").attr("class","gBubble");
bubbles.append("circle")
.on("mouseover",function(){
$(this).attr("cursor","pointer")
})
.on("click",function(){alert("clicked")});
bubbles.append("text").text(function(d){return d.name;}).style("opacity","1");
(function() {
//D3 program to fit circles of different sizes
//in a rectangle of fixed aspect ratio
//as tightly as reasonable.
//
//By Amelia Bellamy-Royds, in response to
//http://stackoverflow.com/questions/13339615/packing-different-sized-circles-into-rectangle-d3-js
//Based on Mike Bostock's
//"http://bl.ocks.org/mbostock/7882658" example:
//http://bl.ocks.org/mbostock/7882658
//parameters//
var N = 25; //number of nodes
var sortOrder = -1;
//>0 for ascending, <0 for descending, 0 for no sort
//create data array//
var data = [], i = N;
var randNorm = d3.random.normal(1,0.6);
while(i--) data.push({
"size": Math.max(randNorm(), 0.1) });
//circle area will be proportional to size
var dataMax = d3.max(data, function(d){return d.size;});
var totalSize = d3.sum(data, function(d){return d.size;});
//________________//
//Set up SVG and rectangle//
var svg = d3.select("svg");
var digits = /(\d*)/;
var margin = 50; //space in pixels from edges of SVG
var padding = 4; //space in pixels between circles
var svgStyles = window.getComputedStyle(svg.node());
var width = parseFloat(svgStyles["width"]) - 2*margin;
var height = parseFloat(svgStyles["height"]) - 2*margin;
var usableArea = Math.PI*
Math.pow( Math.min(width,height)/2 ,2)*0.667;
var scaleFactor = Math.sqrt(usableArea)/
Math.sqrt(totalSize)/Math.PI;
var rScale = d3.scale.sqrt()
//make radius proportional to square root of data r
.domain([0, dataMax]) //data range
.range([0, Math.sqrt(dataMax)*scaleFactor]);
//The rScale range will be adjusted as necessary
//during packing.
//The initial value is based on scaling such that the total
//area of the circles is 2/3 the area of the largest circle
//you can draw within the box.
/*
console.log("Dimensions: ", [height, width]);
console.log("area", width*height);
console.log("Usable area: ", usableArea);
console.log("TotalSize: ", totalSize);
console.log("Initial Scale: ", scaleFactor);
console.log("RScale: ",rScale.domain(), rScale.range());
console.log("r(1)", rScale(1) );
// */
var box = svg.append("rect")
.attr({ "height": height, "width":width,
"x":margin, "y":margin,
"class":"box"
});
var bubbleGroup = svg.append("g")
.attr("class", "bubbles")
.attr("transform",
"translate(" + [margin,margin] + ")");
//__Initialize layout objects__//
// Use the pack layout to initialize node positions:
d3.layout.pack()
.sort((
sortOrder?
( (sortOrder<0)?
function(a,b){return b.size - a.size;} : //descending
function(a,b){return a.size - b.size;} ) : //ascending
function(a,b){return 0;} //no sort
))
.size([width/scaleFactor, height/scaleFactor])
.value(function(d) { return d.size; })
.nodes({children:data});
//Use the force layout to optimize:
var force = d3.layout.force()
.nodes(data)
.size([width/scaleFactor, height/scaleFactor])
.gravity(.5)
.charge(0) //don't repel
.on("tick", updateBubbles);
//Create circles!//
var bubbles = bubbleGroup.selectAll("circle")
.data(data)
.enter()
.append("circle");
//Create text
var text = bubbleGroup.selectAll("text")
.data(data).enter().append("text")
.attr("dy", function(d){
return d.y;
})
.attr("dx", function(d){
return d.x;
}).style("text-anchor", "middle").text("test");
// Create a function for this tick round,
// with a new quadtree to detect collisions
// between a given data element and all
// others in the layout, or the walls of the box.
//keep track of max and min positions from the quadtree
var bubbleExtent;
function collide(alpha) {
var quadtree = d3.geom.quadtree(data);
var maxRadius = Math.sqrt(dataMax);
var scaledPadding = padding/scaleFactor;
var boxWidth = width/scaleFactor;
var boxHeight = height/scaleFactor;
//re-set max/min values to min=+infinity, max=-infinity:
bubbleExtent = [[Infinity, Infinity],[-Infinity, -Infinity]];
return function(d) {
//check if it is pushing out of box:
var r = Math.sqrt(d.size) + scaledPadding,
nx1 = d.x - r,
nx2 = d.x + r,
ny1 = d.y - r,
ny2 = d.y + r;
if (nx1 < 0) {
d.x = r;
}
if (nx2 > boxWidth) {
d.x = boxWidth - r;
}
if (ny1 < 0) {
d.y = r;
}
if (ny2 > boxHeight) {
d.y = boxHeight - r;
}
//check for collisions
r = r + maxRadius,
//radius to center of any possible conflicting nodes
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 = Math.sqrt(d.size) + Math.sqrt(quad.point.size)
+ scaledPadding;
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;
});
//update max and min
r = r-maxRadius; //return to radius for just this node
bubbleExtent[0][0] = Math.min(bubbleExtent[0][0],
d.x - r);
bubbleExtent[0][1] = Math.min(bubbleExtent[0][1],
d.y - r);
bubbleExtent[1][0] = Math.max(bubbleExtent[1][0],
d.x + r);
bubbleExtent[1][1] = Math.max(bubbleExtent[1][1],
d.y + r);
};
}
function updateBubbles() {
bubbles
.each( collide(0.5) ); //check for collisions
text.each(collide(0.5));//check for text collisions
//update the scale to squeeze in the box
//to match the current extent of the bubbles
var bubbleWidth = bubbleExtent[1][0] - bubbleExtent[0][0];
var bubbleHeight = bubbleExtent[1][1] - bubbleExtent[0][1];
scaleFactor = (height/bubbleHeight +
width/bubbleWidth)/2; //average
/*
console.log("Box dimensions:", [height, width]);
console.log("Bubble dimensions:", [bubbleHeight, bubbleWidth]);
console.log("ScaledBubble:", [scaleFactor*bubbleHeight,
scaleFactor*bubbleWidth]);
//*/
rScale
.range([0, Math.sqrt(dataMax)*scaleFactor]);
//shift the bubble cluster to the top left of the box
bubbles
.each( function(d){
d.x -= bubbleExtent[0][0];
d.y -= bubbleExtent[0][1];
});
//update positions and size according to current scale:
bubbles
.attr("r", function(d){return rScale(d.size);} )
.attr("cx", function(d){return scaleFactor*d.x;})
.attr("cy", function(d){return scaleFactor*d.y;});
text
.attr("dy", function(d){
return (scaleFactor*d.y)+4;
})
.attr("dx", function(d){
return scaleFactor*d.x*2;
});
}
force.start();
})();
rect.box {
fill:none;
stroke:royalblue;
stroke-width:5;
shape-rendering: crispEdges;
}
g.bubbles circle {
fill:rgba(255,0,64,0.5);
stroke:rgb(255,0,64);
stroke-width:3;
}
g.bubbles text {
fill:royalblue;
font-family:sans-serif;
text-anchor:middle;
alignment-baseline:middle;
opacity:1;
pointer-events:all;
transition:1s;
}
g.bubbles text:hover {
opacity:1;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
<svg height=500 width=500></svg>
I've added text to circles, and also collision behavior too.
Initially the text is invisible because in the CSS they mentioned like below
g.bubbles text {
fill:royalblue;
font-family:sans-serif;
text-anchor:middle;
alignment-baseline:middle;
opacity:0;//See this value, this makes text to invisible
pointer-events:all;
transition:1s;
}
g.bubbles text:hover {
opacity:1;
}
In my snippet I changed it visible by making it's opacity to 1.
And updated fiddle

How to avoid labels overlapping in a D3.js pie chart?

I'm drawing a pie chart using D3.js with a quite simple script. The problem is that when slices are small, their labels overlap.
What options do I have to prevent them from overlapping? Does D3.js have built-in mechanisms I could exploit?
Demo: http://jsfiddle.net/roxeteer/JTuej/
var container = d3.select("#piechart");
var data = [
{ name: "Group 1", value: 1500 },
{ name: "Group 2", value: 500 },
{ name: "Group 3", value: 100 },
{ name: "Group 4", value: 50 },
{ name: "Group 5", value: 20 }
];
var width = 500;
var height = 500;
var radius = 150;
var textOffset = 14;
var color = d3.scale.category20();
var svg = container.append("svg:svg")
.attr("width", width)
.attr("height", height);
var pie = d3.layout.pie().value(function(d) {
return d.value;
});
var arc = d3.svg.arc()
.outerRadius(function(d) { return radius; });
var arc_group = svg.append("svg:g")
.attr("class", "arc")
.attr("transform", "translate(" + (width/2) + "," + (height/2) + ")");
var label_group = svg.append("svg:g")
.attr("class", "arc")
.attr("transform", "translate(" + (width/2) + "," + (height/2) + ")");
var pieData = pie(data);
var paths = arc_group.selectAll("path")
.data(pieData)
.enter()
.append("svg:path")
.attr("stroke", "white")
.attr("stroke-width", 0.5)
.attr("fill", function(d, i) { return color(i); })
.attr("d", function(d) {
return arc({startAngle: d.startAngle, endAngle: d.endAngle});
});
var labels = label_group.selectAll("path")
.data(pieData)
.enter()
.append("svg:text")
.attr("transform", function(d) {
return "translate(" + Math.cos(((d.startAngle + d.endAngle - Math.PI) / 2)) * (radius + textOffset) + "," + Math.sin((d.startAngle + d.endAngle - Math.PI) / 2) * (radius + textOffset) + ")";
})
.attr("text-anchor", function(d){
if ((d.startAngle +d.endAngle) / 2 < Math.PI) {
return "beginning";
} else {
return "end";
}
})
.text(function(d) {
return d.data.name;
});
D3 doesn't offer anything built-in that does this, but you can do it by, after having added the labels, iterating over them and checking if they overlap. If they do, move one of them.
var prev;
labels.each(function(d, i) {
if(i > 0) {
var thisbb = this.getBoundingClientRect(),
prevbb = prev.getBoundingClientRect();
// move if they overlap
if(!(thisbb.right < prevbb.left ||
thisbb.left > prevbb.right ||
thisbb.bottom < prevbb.top ||
thisbb.top > prevbb.bottom)) {
var ctx = thisbb.left + (thisbb.right - thisbb.left)/2,
cty = thisbb.top + (thisbb.bottom - thisbb.top)/2,
cpx = prevbb.left + (prevbb.right - prevbb.left)/2,
cpy = prevbb.top + (prevbb.bottom - prevbb.top)/2,
off = Math.sqrt(Math.pow(ctx - cpx, 2) + Math.pow(cty - cpy, 2))/2;
d3.select(this).attr("transform",
"translate(" + Math.cos(((d.startAngle + d.endAngle - Math.PI) / 2)) *
(radius + textOffset + off) + "," +
Math.sin((d.startAngle + d.endAngle - Math.PI) / 2) *
(radius + textOffset + off) + ")");
}
}
prev = this;
});
This checks, for each label, if it overlaps with the previous label. If this is the case, a radius offset is computed (off). This offset is determined by half the distance between the centers of the text boxes (this is just a heuristic, there's no specific reason for it to be this) and added to the radius + text offset when recomputing the position of the label as originally.
The maths is a bit involved because everything needs to be checked in two dimensions, but it's farily straightforward. The net result is that if a label overlaps a previous label, it is pushed further out. Complete example here.
#LarsKotthoff
Finally I have solved the problem. I have used stack approach to display the labels. I made a virtual stack on both left and right side. Based the angle of the slice, I allocated the stack-row. If stack row is already filled then I find the nearest empty row on both top and bottom of desired row. If no row found then the value (on the current side) with least share angle is removed from the stack and labels are adjust accordingly.
See the working example here:
http://manicharts.com/#/demosheet/3d-donut-chart-smart-labels
The actual problem here is one of label clutter.
So, you could try not displaying labels for very narrow arcs:
.text(function(d) {
if(d.endAngle - d.startAngle<4*Math.PI/180){return ""}
return d.data.key; });
This is not as elegant as the alternate solution, or codesnooker's resolution to that issue, but might help reduce the number of labels for those who have too many. If you need labels to be able to be shown, a mouseover might do the trick.
For small angles(less than 5% of the Pie Chart), I have changed the centroid value for the respective labels. I have used this code:
arcs.append("text")
.attr("transform", function(d,i) {
var centroid_value = arc.centroid(d);
var pieValue = ((d.endAngle - d.startAngle)*100)/(2*Math.PI);
var accuratePieValue = pieValue.toFixed(0);
if(accuratePieValue <= 5){
var pieLableArc = d3.svg.arc().innerRadius(i*20).outerRadius(outer_radius + i*20);
centroid_value = pieLableArc.centroid(d);
}
return "translate(" + centroid_value + ")";
})
.text(function(d, i) { ..... });

Categories