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)
Related
I have a force layout with a zooming behavior.
var min_zoom = 0.3;
var max_zoom = 3;
var zoom = d3.behavior.zoom().scaleExtent([min_zoom,max_zoom]);
Before form submission, zooming (using the mouse wheel) and translation work just fine just as expected.
svg.attr("transform", "translate(" + d3.event.translate + ")" + " scale(" + d3.event.scale + ")" );
To this end, life is good.
When the form is submitted, the current translate and scale values are saved to the session. As an example, say that the current scale is 2. After the submission of the form and loading the page again, the saved scale is applied, restoring the zoom to its state before submission.
However, after this loading of the page, the value for d3.event.scale is set again to its initial value (I guess it is 1). Hence, trying to perform an in or out zoom again (using mouse wheel) applies the scale factor to the initial base value of the zoom (supposedly 1 for d3.event.scale), instead of using the values saved from the session 2, and will make a sudden, unexpected change of the zoom.
PS. Trying to manually assign a value to d3.event.scale doesn't work! I think the most straightforward workaround would be altering d3.event.scale in case that's possible. Otherwise, manually controlling the zoom seems an exhaustive and non-intuitive option to take.
At the creation of your SVG, you need to adjust the transform of your main group as well as set the scale of your zoom.
const previousScale = 2;
const min_scale = 0.3;
const max_scale = 3;
const width = 600;
const height = 200;
var zoom = d3.behavior.zoom()
.scale(previousScale)
.scaleExtent([min_scale, max_scale])
.on('zoom', redraw)
const svg = d3.select('svg')
const g = svg
.attr('width', width)
.attr('height', height)
.append('g')
.attr('transform', `scale(${previousScale}, ${previousScale})`)
.call(zoom)
function draw() {
g.append('circle')
.attr('r', 50)
.attr('cx', 50)
.attr('cy', 50)
const initialScale = zoom.scale();
console.log(`initialScale: ${initialScale}`)
}
draw()
function redraw() {
const newScale = zoom.scale();
g.attr('transform', `scale(${newScale}, ${newScale})`)
console.clear()
console.log(`newScale: ${newScale.toFixed(2)}`)
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
<svg></svg>
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 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/