I am looking for an example for to rotate a pie chart on mouse down event. On mouse down, I need to rotate the pie chart either clock wise or anti clock wise direction.
If there is any example how to do this in D3.js, that will help me a lot. I found an example using FusionChart and I want to achieve the same using D3.js
Pretty easy with d3:
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")");
var g = svg.selectAll(".arc")
.data(pie(data))
.enter().append("g")
.attr("class", "arc");
g.append("path")
.attr("d", arc)
.style("fill", function(d) {
return color(d.data.age);
});
var curAngle = 0;
var interval = null;
svg.on("mousedown", function(d) {
interval = setInterval(goRotate,10);
});
svg.on("mouseup", function(d){
clearInterval(interval);
})
function goRotate() {
curAngle += 1;
svg.attr("transform", "translate(" + width / 2 + "," + height / 2 + ") rotate(" + curAngle + "," + 0 + "," + 0 + ")");
}
Working example.
I did a similar thing with a compass instead of pie chart. You mainly need three methods - each bound to a different mouse event.
Bind this to the mousedown event on your compass circle:
function beginCompassRotate(el) {
var rect = compassCircle[0][0].getBBox(); //compassCircle would be your piechart d3 object
compassMoving = true;
compassCenter = {
x: (rect.width / 2),
y: (rect.height / 2)
}
}
Bind this to the mouse move on your canvas or whatever is holding your pie chart - you can bind it to the circle (your pie chart) but it makes the movement a little glitchy. Binding it to the circle's container keeps it smooth.
function rotateCompass() {
if (compassMoving) {
var mouse = d3.mouse(svg[0][0]);
var p2 = {
x: mouse[0],
y: mouse[1]
};
var newAngle = getAngle(compassCenter, p2) + 90;
//again this v is your pie chart instead of compass
compass.attr("transform", "translate(90,90) rotate(" + newAngle + "," + 0 + "," + 0 + ")");
}
}
Finally bind this to the mouseup on your canvas - again you can bind it to the circle but this way you can end the rotation without the mouse over the circle. If it is on the circle you will keep rotating the circle until you have a mouse up event over the circle.
function endCompassRotate(el) {
compassMoving = false;
}
Here is a jsfiddle showing it working: http://jsfiddle.net/4oy2ggdt/
Related
I am currently working on a D3 world map in which I have brought in a zoom functionality up-to the boundary level of any country or county based on its click.
I have Added Bubbles pointing the counties in Kenya,which gets enlarged on the zoom functionality that I have added.But I want to stop the zooming of bubbles,on zooming of the Map.
Here is a plunker for my current work.
https://plnkr.co/edit/nZIlJxvU74k8Nmtpduzc?p=preview
And below is the code for zooming and zoom out
function clicked(d) {
var conditionalChange = d;
if(d.properties.hasOwnProperty("Country")){
var country = d.properties.Country;
var obj = data.objects.countries.geometries;
$.each(obj, function(key, value ) {
if(countries[key].properties.name == "Kenya")
{
conditionalChange = countries[key].geometry;
}
});
}
d = conditionalChange;
if (active.node() === this) return reset();
active.classed("active", false);
active = d3.select(this).classed("active", true);
var bounds = path.bounds(d),
dx = bounds[1][0] - bounds[0][0],
dy = bounds[1][1] - bounds[0][1],
x = (bounds[0][0] + bounds[1][0]) / 2,
y = (bounds[0][1] + bounds[1][1]) / 2,
scale = 1.2/ Math.max(dx / width, dy / height),
translate = [width / 2 - scale * x, height / 2 - scale * y];
g.transition()
.duration(750)
.style("stroke-width", 1/ scale + "px")
.attr("transform", "translate(" + translate + ")scale(" + scale + ")");
}
function reset() {
active.classed("active", false);
active = d3.select(null);
g.transition()
.duration(750)
.style("stroke-width", "1px")
.attr("transform", "");
}
You are scaling the entire g element, this effectively zooms the map. Everything will increase in size; however, for the map lines you have adjusted the stroke to reflect the change in g scale factor:
g.transition()
.duration(750)
.style("stroke-width", 1/ scale + "px")
.attr("transform", "translate(" + translate + ")scale(" + scale + ")");
To keep the circles the same size, you have to do the same adjustment for your circles by modifying the r attribute for each circle according to the g scale factor:
g.selectAll(".city-circle")
.transition()
.attr("r", 5 / scale )
.duration(750);
Though, since you don't actually apply the class city-circle on your circles you'll need to do that too when you append them:
.attr("class","city-circle")
And, just as you reset the stroke width on reset, you need to reset the circles' r :
g.selectAll(".city-circle")
.transition()
.attr("r", 5)
.duration(750);
Together that gives us this.
I've already spent too much time trying to figure this out.
My goal is to create d3 collapsible tree but for some reason when you zoom it, it moves the tree on position 0,0. I've already seen a few questions with similar problem such as this one d3.behavior.zoom jitters, shakes, jumps, and bounces when dragging but can't figure it out how to apply it to my situation.
I think this part is making a problem but I'm not sure how to change it to have the proper zooming functionality.
d3.select('g').transition()
.duration(duration)
.attr("transform", "translate(" + x + "," + y + ")scale(" + scale + ")")
zoomListener.scale(scale);
Here is my code: https://jsfiddle.net/ramo2600/y79r5dyk/11/
You are translating your zoomable g to position [100,100] but not telling the zoom d3.behavior.zoom() about it. So it starts from [0,0] and you see the "jump".
Modify your centerNode function to:
function centerNode(source) {
scale = zoomListener.scale();
// x = -source.y0;
y = -source.x0;
// x = x * scale + viewerWidth / 2;
x = 100;
y = 100;
// y = y * scale + viewerHeight / 2;
d3.select('g').transition()
.duration(duration)
.attr("transform", "translate(" + x + "," + y + ")scale(" + scale + ")")
zoomListener.scale(scale);
zoomListener.translate([x,y]); //<-- tell zoom about position
}
I'm building a map plotting tool with D3. I'm using this example.
Everything works but I want to draw each point with a 10ms difference, like its drawing.
I tried to make an interval but didn't work. I also was thinking to make css animation and each point to have an animation-delay but that doesn't seem work well.
Can someone explain to me how to draw the data one by one?
function redrawSubset(subset) {
var radius = 2;
var bounds = path.bounds({ type: 'FeatureCollection', features: subset });
var topLeft = bounds[0];
var bottomRight = bounds[1];
var start = new Date();
var points = g.selectAll('path')
.data(subset, function(d) {
return d.id;
});
path.pointRadius(radius);
svg.attr('width', bottomRight[0] - topLeft[0] + radius * 2)
.attr('height', bottomRight[1] - topLeft[1] + radius * 2)
.style('left', topLeft[0] + 'px')
.style('top', topLeft[1] + 'px');
g.attr('transform', 'translate(' + (-topLeft[0] + radius) + ',' + (-topLeft[1] + radius) + ')');
points.enter().append('path');
points.exit().remove();
points.attr('d', path);
}
It is possible to render circle by circle, but it's a little complicated. Maybe a workaround is to draw all of them transparent, and setting the opacity to 1 with a delay of 10ms:
points.enter().append("path").attr("opacity", 0)
.transition()
.duration(10)
.delay(function(d,i){ return i*10})
.attr("opacity", 1);
Here is your plunker: http://plnkr.co/edit/ys4VofukQOvA4pY0TQX7?p=preview
I have a D3js map built with topojson.js.
var projection = d3.geo.mercator();
Everything works fine, but I am looking for an effect that I cannot manage to achieve. When, zooming the map I would like the pins over it to scale down, But if I scale it down, I need to recalculate their coordinates and I can't find the formula to do so.
Zoom handler
scale = d3.event.scale;
if (scale >= 1) {
main.attr("transform", "translate(" + d3.event.translate + ")
scale(" + d3.event.scale + ")");
}
else {
main.attr("transform", "translate(" + d3.event.translate + ")scale(1)");
}
//43x49 is the size initial pine size when scale = 1
var pins = main.select("#pins").selectAll("g").select("image")
.attr("width", function () {
return 43 - (43 * (scale - 1));
})
.attr("height", function () {
return 49 - (49 * (scale - 1));
})
.attr("x", function () {
//Calculate new image coordinates;
})
.attr("y", function () {
//Calculate new image coordinates;
});
My question is : how do I calculate x and y based on new scale?
I hope I am clear enough.
Thanks for your help
EDIT :
Calculation of initial pins coordinates :
"translate(" + (projection([d.lon, d.lat])[0] - 20) + ","
+ (projection([d.lon, d.lat])[1] - 45) + ")"
-20 and -45 to have the tip of the pin right on the target.
You need to "counter-scale" the pins, i.e. as main scales up/down you need to scale the pins down/up, in the opposite direction. The counter-scale factor is 1/scale, and you should apply it to each of the <g>s containing the pin image. This lets you remove your current width and height calculation from the <image> nodes (since the parent <g>'s scale will take care of it).
However, for this to work properly, you'll also need to remove the x and y attributes from the <image> and apply position via the counter-scaled parent <g> as well. This is necessary because if there's a local offset (which is the case when x and y are set on the <image>) that local offset gets scaled as the parent <g> is scaled, which makes the pin move to an incorrect location.
So:
var pinContainers = main.select("#pins").selectAll("g")
.attr("transform", function(d) {
var x = ... // don't know how you calculate x (since you didn't show it)
var y = ... // don't know how you calculate y
var unscale = 1/scale;
return "translate(" + x + " " + y + ") scale(" + unscale + ")";
})
pinContainers.select("image")
.attr("width", 43)
.attr("height", 49)
// you can use non-zero x and y to get the image to scale
// relative to some point other than the top-left of the
// image (such as the tip of the pin)
.attr("x", 0)
.attr("y", 0)
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) { ..... });