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.
Related
I want to rotate and zoom graphic around its center with D3.js. When I zoom graphic I want to zoom it with current aspect ratio and vice versa when I rotate graphic I want to zoom it to the current point that my mouse points. For zooming I use wheel of the mouse and for rotation I use the button of the mouse.
var svg = d3.select("svg"),
width = +svg.attr("width"),
height = +svg.attr("height"),
transform = d3.zoomIdentity;
var points = d3.range(2000).map(phyllotaxis(10));
var g = svg.append("g");
g.append("line")
.attr("x1", "20")
.attr("y1", "20")
.attr("x2", "60")
.attr("y2", "60")
.attr("stroke", "black")
.attr("stroke-width", "10");
svg.call(d3.drag()
.on("drag",onDrag)
)
// ##########################
var boxCenter = [100, 100];
// #############################
function onDrag(){
var x = d3.event.sourceEvent.pageX,
y = d3.event.sourceEvent.pageY;
var angle = Math.atan2(x - boxCenter[0],
- (y - boxCenter[1]) )*(180/Math.PI);
g.attr("transform", "rotate("+angle+")");
}
svg.call(d3.zoom()
.scaleExtent([1 / 2, 8])
.on("zoom", zoomed));
function zoomed() {
g.attr("transform", d3.event.transform);
}
function phyllotaxis(radius) {
var theta = Math.PI * (3 - Math.sqrt(5));
return function(i) {
var r = radius * Math.sqrt(i), a = theta * i;
return {
x: width / 2 + r * Math.cos(a),
y: height / 2 + r * Math.sin(a)
};
};
}
Here is my example:
https://jsfiddle.net/6Lyjz35L/
For the rotation around center to be correct at the initial zoom you need to add a 'transform-origin' attribute to 'g'.
g.attr("transform-origin", "50% 50%");
The other problems you're having stem from assigning the 'transform' attribute in two separate places. An element ('g') can only have one 'transform' attribute applied at a time, so you're overwriting one or the other each time you rotate or zoom. To fix this you can create a helper method which will append both of the transforms you want in a single string.
var currentAngle = 0;
var currentZoom = '';
function getTransform(p_angle, p_zoom) {
return `${p_zoom} rotate(${p_angle})`;
// return p_zoom + " rotate(" + p_angle + ")";
}
// In the rotate:
currentAngle = angle;
g.attr("transform", getTransform(currentAngle, currentZoom));
// In the zoom:
currentZoom = d3.event.transform;
g.attr("transform", getTransform(currentAngle, currentZoom));
There is one more issue which is introduced by the zoom, and that is that you'll have to calculate a new transform-origin at different zoom levels.
The issue I said was introduced by the zoom was actually the result of applying the operations in the incorrect order. Originally I applied the rotation and THEN then translation. It actually needs to be reversed, translation and THEN rotation. This will keep the correct transform-origin.
Here's a fiddle with those changes: https://jsfiddle.net/scmxcszz/1/
I am trying to create a rectangle where I can resize and rotate it using handlers (small circles) located on the top of the rectangle. Similar to the most of the drawing tools that allow us to resize and rotate the shapes.
I added three circles on the top of my rectangle. One circle is for resizing the width of the rectangle (circle on the right side). Another rectangle is for resizing the height of the bar (circle on the top). Resizing the rectangle works perfectly.
margin = {top: 40, right: 20, bottom: 30, left: 70},
width = 600 - margin.left - margin.right,
height = 400 - margin.top - margin.bottom;
var svg = d3.select('#canvas').attr("width",width).attr("height",height);
gContainer = svg.append('g')
.attr("class", "gContainer")
.attr("transform", function(d,i){
return "translate("+300+","+200+")"
})
gBars = gContainer.append('g').attr("class", "gBar");
gBars.append("rect")
.attr("class", "Bar")
.attr("fill","black")
.attr("width", 40)
.attr("height", function(d) { return height - 200})
.style("opacity", 0.5);
var handlerRadius = 3.5;
handlerPointsPosition=[];
elementWidth = Number(d3.select(".Bar").attr("width"));
elementHeight = Number(d3.select(".Bar").attr("height"));
x0 = 0 + (elementWidth/2) ;
y0 = 0 ;
x1 = 0 + (elementWidth);
y1 = 0 +(elementHeight/2) ;
x2= 0 + (elementWidth/2) ;
y2= -20;
handlerPointsPosition = [[x0,y0],[x1,y1],[x2,y2]];
var rectangleHandlers = d3.behavior.drag()
.origin(function(d) { return d; })
.on("dragstart", dragstarted)
.on("drag", dragged)
gHandlerPoints= gContainer.selectAll("g.HandlerPoint")
.data(handlerPointsPosition)
.enter().append('g')
.attr("class", "gHandlerPoint")
.attr("id", function(d,i){return "gHandlerPoint_id_"+i;})
.attr("transform", function(d,i){
//console.log(d);
return "translate("+d[0]+","+d[1]+")"
})
.call(rectangleHandlers);
gHandlerPoints.append("circle")
.attr("class", "handlerPoint")
.style("fill", "white")
.style("stroke", "blue")
.attr("stroke","")
.attr("r",function(d,i){return (i == 2 ? 4: 3.5);})
.attr("id", function(d,i){return "HandlerPointId_"+i;})
gContainer.append("line")
.attr("class","handlerLine")
.attr("x1", (elementWidth/2) )
.attr("y1", 0- handlerRadius)
.attr("x2", (elementWidth/2) )
.attr("y2", -20 + handlerRadius)
.attr("stroke-width", 1)
.attr("stroke", "blue");
function updateHandlerPosition(id, dX, dY)
{
d3.select(id).attr("transform", function(d,i){
return "translate(" + [ dX, dY] + ")"
})
}
function dragstarted(d,i) {
dragIconX = d3.transform(d3.select(this).attr("transform")).translate[0];
dragIconY = d3.transform(d3.select(this).attr("transform")).translate[1];
barStartWidth = d3.select(".Bar").attr("width");
}
function dragged(d,i) {
barHeight = d3.select(".Bar").attr("height");
if(i == 0) // circle on the top edge of the bar
{
dragIconY = dragIconY + d3.mouse(this)[1];
updateHandlerPosition("#gHandlerPoint_id_"+i, dragIconX, dragIconY );
updateHandlerPosition("#gHandlerPoint_id_1", (barStartWidth), (barHeight/2) );
var x = d3.transform(d3.select(".gContainer").attr("transform")).translate[0];
var y = d3.transform(d3.select(".gContainer").attr("transform")).translate[1];
d3.select(".gContainer").attr("transform", function(d,i){
y = y + dragIconY;
return "translate(" + [ x , y] + ")"
})
console.log(height, barHeight, barHeight - Number(dragIconY));
d3.select(".Bar").attr("height", barHeight - Number(dragIconY));
}
else if (i==1) // circle on the right side of the bar
{
oldMouseX = dragIconX;
dragIconX = d3.mouse(this)[0]+dragIconX;
barWidth = dragIconX;
updateHandlerPosition("#gHandlerPoint_id_"+i, dragIconX, dragIconY );
updateHandlerPosition("#gHandlerPoint_id_0", (barWidth/2), 0 );
updateHandlerPosition("#gHandlerPoint_id_2", (barWidth/2), -20);
d3.select(".handlerLine").attr("x1",(barWidth/2)).attr("x2", (barWidth/2));
d3.select(".Bar").attr("width", Math.abs(dragIconX));
}
else if(i==3) //circle on very top
{
// code for rotation should come here.
}
}
Link to jsFiddle: http://jsfiddle.net/Q5Jag/2103/
I put the third circle for rotation (the circle on the very top). However, I have no idea how to fix the rotation. I want the rectangle to rotate when I drag the circle on the very top. I also want to be able to resize the circle accordingly when it is rotated.
Any idea?
You can calculate the rotation with respect to the center of your rectangle. Consider v1 as vector from center of rectangle to the rotation handle before rotation and v2 as a vector from center of rectangle to the rotation handle after rotation. Read here to know the math behind it.
// old vector
v1 = {};
v1.x = oldMouseX-gCenterX;
v1.y = oldMouseY-gCenterY;
// new vector
v2 = {};
v2.x = dragHandleX-gCenterX;
v2.y = dragHandleY-gCenterY;
angle = Math.atan2(v2.y,v2.x) - Math.atan2(v1.y,v1.x);
where
gCenterX and gCenterY are coordinates of the center of rectangle
oldMouseX and oldMouseY are coordinates of mouse pointer prior to rotation
dragHandleX and dragHandleY are coordinates of mouse pointer after rotation
Here is the complete code:
http://jsfiddle.net/Q5Jag/2109/
(But it fails if you mix rotation and resizing)
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/
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)
Here is some example code and a fiddle of it:
var w = 400;
var h = 400;
var r = 20;
var factor = 5;
var svg = d3.select("body").append("svg")
.attr("width", w)
.attr("height", h)
.append("g")
.attr("transform", "translate(" + w/2 + "," + h/2 + ")");
svg.append("circle")
.attr("cx", 0)
.attr("cy", 0)
.attr("r", r)
.style("fill", "black");
svg.append("circle")
.attr("cx", 150)
.attr("cy", 150)
.attr("r", r)
.style("fill", "red");
svg.selectAll("circle")
.transition()
.duration(2000)
.attr("transform", "scale(" + 1/factor +")")
.attr("r", r*factor);
http://jsfiddle.net/o1wzfas7/2/
In the example, I am scaling two circles down by a factor of 5 (which also scales their positions and thus moves them "closer" to each other) and simultaneously scaling up the circles' radii by a factor of 5. The idea is that they'll appear to move closer to each other without changing size (as if I was changing their "cx" and "cy" attributes instead), but for some reason the scale transition and radius transition seem to go at different rates, so you see the circles get larger and then settle back to the initial size.
Does anybody know how I would do this using scale and radius transitions, but having the two cancel each other out so that the circles don't appear to change in size?
First, to explain what's going on:
The problem is that the changes you are making cancel out multiplicatively, but transitions proceed in an additive way.
So for your simple example, where radius (r) starts at 20, the scale (s) starts out (implicitly) as 1 and you are transitioning by a factor of 5, the effective radius of the circle is r*s:
At the start of transition:
r =20
s =1
r*s =20
At the end of transition:
r =4
s =5
r*s =20
Now, the way you're thinking of it in your head is that the factor should transition from 1 to 5, but that's not what is going to happen. The default transition functions don't see your factor, they just see that radius is transitioning from 20 to 4, and scale is transitioning from 1 to 5.
Therefore, at the midpoint of the transition, each attribute will be at the midpoint (average) of its start and end values:
r = (20+4)/2 = 12
s = (1+5)/2 = 3
r*s = 36
In order to do what you want, you're going to have to create a custom tween, which directly transitions the factor, and then calculates the radius and scale from there:
svg.selectAll("circle")
.transition()
.duration(2000)
.tween("factor", function(d,i){
/* create an interpolator for your factor */
var f = d3.interpolateNumber(1,factor);
/* store the selected element in a variable for easy modification */
var c = d3.select(this);
/* return the function which will do the updates at each tick */
return function(t) {
var f_t = f(t);
c.attr("transform", "scale(" + 1/f_t + ")" );
c.attr("r", r*f_t );
};
});
Note that in your real application, you'll need to store the "start" value for your factor transition in a global variable or each data object, since it won't automatically be 1 when you transition to a different scaling factor.
var w = 400;
var h = 400;
var r = 20;
var factor = 5;
var svg = d3.select("body").append("svg")
.attr("width", w)
.attr("height", h)
.append("g")
.attr("transform", "translate(" + w/2 + "," + h/2 + ")");
svg.append("circle")
.attr("cx", 0)
.attr("cy", 0)
.attr("r", r)
.style("fill", "black");
svg.append("circle")
.attr("cx", 150)
.attr("cy", 150)
.attr("r", r)
.style("fill", "red");
svg.selectAll("circle")
.transition()
.duration(2000)
.tween("factor", function(d,i){
/* create an interpolator for your factor */
var f = d3.interpolateNumber(1,factor);
/* store the selected element in a variable for easy modification */
var c = d3.select(this);
/* return the function which will do the updates at each tick */
return function(t) {
var f_t = f(t);
c.attr("transform", "scale(" + 1/f_t + ")" );
c.attr("r", r*f_t );
};
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>