d3.js zooming mouse coordinates - javascript

Im using d3.js zoom function to zoom in on an image inside an svg. Im using a mask to reveal an underlying image beneath. If i dont zoom the mask and mouse cursor coordinates match up perfectly. However, when i start to zoom the mouse coordinates are not translating to the zoom level, thus the map reveal is not lining up with the cursor anymore.
here is what im using so far...Im assuming there needs to be some sort of coordinate translation when zooming?
var lightMap = d3.select("#lightMap").call(d3.behavior.zoom().scaleExtent([1, 3]).on("zoom", zoom));
var darkMap = d3.select("#darkMap").call(d3.behavior.zoom().scaleExtent([1, 3]).on("zoom", zoom));
function zoom() {
lightMap.attr("transform", "translate(" + d3.event.translate + ")scale(" + d3.event.scale + ")");
darkMap.attr("transform", "translate(" + d3.event.translate + ")scale(" + d3.event.scale + ")");
}
var svg = d3.select("svg");
svg.on('mousemove', function () {
var coordinates = [0, 0];
coordinates = d3.mouse(this);
var x = coordinates[0];
var y = coordinates[1];
primaryCircle.setAttribute("cy", y + 'px');
primaryCircle.setAttribute("cx", x + 'px');
});

(I know this is a late answer but I had the same problem and thought I'd share how I fixed it for future people who see this)
Fix: Use coordinates=mouse(lightMap.node()) / darkMap.node() instead of mouse(this). Alternatively, and probably more correctly, call the zoom behavior on svg and keep using mouse(this).
Explanation: You call the mousemove function on svg, so mouse(this) gets the coordinates within the svg element. However you don't apply the zoom behavior to svg, so you get wrong coordinates. Call mouse(_) on an element that zooms.

Related

Preventing d3 translation value in zoom event moving beyond bounds

I have a zoom event handler on my tree graph like so:
d3.select("#"+canvasId+" svg")
.call(d3.behavior.zoom()
.scaleExtent([0.05, 5])
.on("zoom", zoom));
Which calls the zoom function which handles the translation bounding logic:
function zoom() {
console.log(d3.event.translate[0]);
var wcanvas = $("#"+canvasId+" svg").width();
var hcanvas = $("#"+canvasId+" svg").height();
var displayedWidth = w*scale;
var scale = d3.event.scale;
var h = d3.select("#"+canvasId+" svg g").node().getBBox().height*scale;
var w = d3.select("#"+canvasId+" svg g").node().getBBox().width*scale;
var padding = 100;
var translation = d3.event.translate;
var tbound = -(h-hcanvas)-padding;
var bbound = padding;
var lbound = -(w-wcanvas)-padding;
var rbound = padding;
// limit translation to thresholds
translation = [
Math.max(Math.min(translation[0], rbound), lbound),
Math.max(Math.min(translation[1], bbound), tbound)
];
console.log("Width: "+w*scale+" || Height: "+h*scale+" /// "+"Left: "+translation[0]+" || Top: "+translation[1]);
d3.select("#"+canvasId+" svg g")
.attr("transform", "translate(" + translation + ")" +" scale(" + scale + ")");
console.log(d3.select("#"+canvasId+" svg g")[0]);
}
However, translations beyond the bounds cause the d3.event.translate values to increase. The result is that even if the translation is not causing the graph to move as it has reached its limit for translation, the value for the translation within successive events can continue to increase.
The result is that say I drag the graph far to the left, even though it will stop moving past a certain point, because the value within the events continues to increase, I would then have to drag it a long way back right before it actually begins to move right again.
Is there a good way to prevent this behaviour?
Okay I worked it out. The trick is to set the translation for the d3.behaviour.zoom so that successive zoom pans start at the bounded translation rather than with the additional panning that didn't actually give any movement.
To do this, we declare the zoom behaviour as a separate variable and add it to our zoomable element:
var zoomBehaviour = d3.behavior.zoom()
.scaleExtent([0.05, 5])
.on("zoom", zoom)
d3.select("#"+canvasId+" svg")
.call(zoomBehaviour);
Then we set the translation of this zoomBehaviour to our bounded translation in the zoom function:
function zoom() {
...
translation = [
Math.max(Math.min(translation[0], rbound), lbound),
Math.max(Math.min(translation[1], bbound), tbound)
];
zoomBehaviour.translate(translation);
d3.select("#"+canvasId+" svg g")
.attr("transform", "translate(" + translation + ")" +" scale(" + scale + ")");
}

D3.js map zoom behaviors in canvas and svg

Thanks for reading.
The Goal
I would like to compare a working SVG click-to-zoom from Mike Bostock's blocks to a canvas-based system. I've placed the working SVG on the top, and a canvas on the bottom. When a user clicks on a state in the upper SVG, I would like the lower canvas element to "follow", or mimic, the zooming and panning. For example, clicking Minnesota in the upper SVG will also cause the lower canvas to zoom and pan to Minnesota.
The Problem
My canvas element draws fine after loading the topojson, but it does not animate. I would like it to animate. I believe this is because I do not fully understand zoom behaviors and path-based projections.
http://jsfiddle.net/30w8nv4t/2/
function zoomed(d) {
g.style("stroke-width", 1.5 / d3.event.scale + "px");
g.attr("transform", "translate(" + d3.event.translate + ")scale(" + d3.event.scale + ")");
// the zTranslate and zScale variables appear to be ok,
// but `d` is null. I'm not sure how to redraw.
var zTranslate = zoom.translate();
var zScale = zoom.scale();
console.log(zTranslate, zScale, d);
context.clearRect(0, 0, width, height);
context.beginPath();
canvasPath(d);
context.stroke();
}
In this case, d is clearly null, and I'm not able to redraw anything. I assume my problem could be in either the zoomed method or in the clicked function.
The Reason
I am using this side-by-side approach because I would like to learn how paths, projections, and zoom behaviors work together. I admire how well canvas performs against SVG, but the lack of interactivity is daunting. Fortunately, being able to zoom and pan to arbitrary geometry cuts my problem in half.
Thank you for reading. The link to JSFiddle is at the top of this post.
The canvas drawing function uses the projected values of the lat/long co-ordinates, but you're not updating the scale and translate of your projection in your zoom handler.
One way to get the behavior that you're after, is to switch from a transform on the svg in the zoom handler, to a transform on the projection.
I have done just that in this updated fiddle: http://jsfiddle.net/30w8nv4t/7/
The differences are:
Update the zoom behavior to use the projection translate and scale as the default values, and to set the scaleExtent values to be based on the projection as well.
var zoom = d3.behavior.zoom()
.translate(projection.translate())
.scale(projection.scale())
.scaleExtent([projection.scale()/5, projection.scale()*5])
.on("zoom", zoomed);
Update your zoomed function to translate and scale the projection and then redraw the svg based paths.
function zoomed(d) {
//g.style("stroke-width", 1.5 / d3.event.scale + "px");
//g.attr("transform", "translate(" + d3.event.translate + ")scale(" + d3.event.scale + ")");
projection.translate(d3.event.translate).scale(d3.event.scale);
g.selectAll("path").attr("d", path);
var zTranslate = zoom.translate();
var zScale = zoom.scale();
console.log(zTranslate, zScale, d);
context.clearRect(0, 0, width, height);
context.beginPath();
canvasPath(states);
context.stroke();
}
Update your clicked function accordingly.
function clicked(d) {
if (active.node() === this) {
zoom.scale(500).translate([width/2, height/2]);
active.classed("active", false);
active = d3.select(null);
} else {
var centroid = path.centroid(d),
translate = zoom.translate(),
bounds = path.bounds(d),
dx = bounds[1][0] - bounds[0][0],
dy = bounds[1][1] - bounds[0][1],
scale = .9/ Math.max(dx / width, dy / height);
zoom.scale(scale * zoom.scale())
.translate([
translate[0] - centroid[0] * scale + width * scale / 2,
translate[1] - centroid[1] * scale + height * scale / 2]);
active.classed("active", false);
active = d3.select(this).classed("active", true);
}
zoom.event(svg);
}
This is probably one of the major components of the change, as the scale and translate are applied to the zoom behavior, and when they are, they have to be scaled by the current zoom scale. The clicked function then fires the zoomed function to redraw the svg and canvas elements.
As you can see, your canvas drawing code was correct. It was just that the drawing code of was using the projection to determine the x and y positions of the points to draw based on the projection which wasn't being updated by the zoom handler.
It would also be possible to have a separate projection for the canvas, and update that in the zoom handler before calling the canvas redraw functions. I'll leave that as an exercise for the reader!

How to calculate (SVG) X/Y coordinates out of translation and rotation (in JS/D3.js)?

The following function draws small circles in the middle of some arcs. I want to draw lines from these circles to other elements.
Thats why I need to get the cx/cy values of the circles (after they have been rotated).
var drawSmallCircles = function(arcs){
var d=arcs;
var arcRadius=d[0].outer;
var svg = d3.select("svg").append("g")
.attr("transform", "translate(" + width / 4 + "," + height / 2 + ")");
var smallCircles = svg.selectAll("circle").data(d).enter().append("circle")
.attr("fill","black")
.attr("cx",0)
.attr("cy",-arcRadius)
.attr("r",4)
.attr("transform", function(d) {
return "rotate(" + (((d.startAngle+d.endAngle)/2) * (180/Math.PI)) + ")";
});
}
Best would be if someone could show me a function which gets the Arc-Radius and an Angle and returns (cx/cy). I would pre-calculate and store (cx/cy) in the "arcs-objects" and draw the circles and the lines out of those values.
The "translate" transformation is not really my problem.
Thank you!
A friend of mine helped me with the following calculations:
new_x= ( width/4
+Math.cos((d.startAngle+d.endAngle)/2 ) * (0 )
-Math.sin((d.startAngle+d.endAngle)/2 ) * (-arcRadius) );
new_y= ( height/2
+Math.sin((d.startAngle+d.endAngle)/2 ) * (0 )
+Math.cos((d.startAngle+d.endAngle)/2 ) * (-arcRadius) );
In this case, your first transform is applied to the g element, which contains a circle element that has a cy attribute and a rotation transform.
The first transform sets the origin of your group, the second rotates the circle around the origin at a distance of cy. Since your cy attribute is negative, the y-position relative to the origin will be given by -r*Math.cos(theta), while the x-position will be given by r*Math.sin(theta), where theta is the rotation angle in radians and r is the radial distance from the transformed origin to the center of the circle (arcRadius in your code).

simple zoom in d3.js

I am trying to implement a simple zoom in d3.js, simpler than all the examples I have gone through (I suppose) but it just doesn't wanna work. So, the functionality that I want to implement is: the user clicks on a section of the graph and that section zooms at a predefined fixed size in the centre of the chart; the user cannot zoom it any further, no panning either. And when the user clicks at any other section of the chart, the zoomed section translates back to its normal/original position.
var container = svg.append("g").classed("container-group", true);
container.attr({transform: "translate(" + 40*test_data.row + "," + 40*test_data.col + ")"});
container.call(d3.behavior.zoom().scaleExtent([1,5]).on("zoom", zoom));
function zoom() {
container.attr("transform","translate(" + d3.event.translate + ")scale(" + d3.event.scale + ")");
}
I have tried zoom.translate and zoom.size but couldn't get them right. And don't know how to reset the zoomed section either.
Any help would be much appreciated !
I´ll give an example of zooming some circles. Clicking on the red rectangle will zoom out to 50%, clicking on the blue one will return to a 100% scale. The exact functions you are looking for are zoomOut() and initialZoom()
var zoomListener = d3.behavior.zoom().scaleExtent([0.1, 3]);
width = 200 ;
height = 200 ;
//svg
var svg = d3.select("body").append("svg").attr("id","vis")
.attr("width", width )
.attr("height", height );
//transition listener group
var svgGroup = svg.append("g").call(zoomListener);
//zoom in and zoom out buttons
svg.append("rect").attr("x",0).attr("y",0).attr("width",50).attr("height",50).style("fill","red").on("click",zoomOut);
svg.append("rect").attr("x",0).attr("y",50).attr("width",50).attr("height",50).style("fill","blue").on("click",initialZoom);
var i,k;
for(i=90;i<width-20;i+=20){
for( k=20;k<height-20;k+=20){
svgGroup.append("circle").attr("cx", i).attr("cy", k).attr("r", 10);
}
}
function zoomOut(){
//fix transition to center of canvas
x = (width/2) * 0.5;
y = (height/2) * 0.5;
//zoom transition- scale value 150%
svgGroup.transition().duration(500).attr("transform", "translate("+x+","+y+")scale(0.5)" );
}
function initialZoom(){
//fix transition to center of canvas
x = (width/2) ;
y = (height/2) ;
//zoom transition- scale value 100%
svgGroup.transition().duration(500).attr("transform", "scale(1)" );
}

Getting the Correct Viewport Coordinates via getBoundingClientRect()

Applying the finishing touches to an interactive infographic, I changed the alignment of the SVG object on the HTML page from left to centered. That change broke the placement of the little pop-ups that appear over each state. Here is the left-aligned version, which works correctly:
http://www.50laboratories.com/demographicclout/demographicclout-left.html
And the centered version, which places the pop-ups incorrectly:
http://www.50laboratories.com/demographicclout/demographicclout-centered.html
Here's the code that determines the pop-up location using getBoundingClientRect():
targetbackground = document.getElementById(selectedstate + mapyear);
targetwidth=targetbackground.getBoundingClientRect().width;
targetex = targetbackground.getBoundingClientRect().left + (targetwidth/2)+excorrection;
targetwye = targetbackground.getBoundingClientRect().top + wyecorrection;
d3.select("#datapopup").attr("transform", "translate(" + targetex + "," + targetwye + ")");
Apparently getBoundingClientRect() is returning the distance from the top-left corner of the browser window, not the top-left corner of the SVG viewport. How do I consistently get the correct coordinate values, that is, from the point of origin of the viewport?
Use SVG's getBBox() method:
targetbackground = document.getElementById(selectedstate + mapyear);
targetwidth = targetbackground.getBBox().width;
targetex = targetbackground.getBBox().x + (targetwidth/2) + excorrection;
targetwye = targetbackground.getBBox().y + wyecorrection;
d3.select("#datapopup").attr("transform", "translate(" + targetex + "," + targetwye + ")");

Categories