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 + ")");
Related
I am making a website with a multipage google-doc-like user interface and want to allow the user to "zoom" or change the scale of everything on the page with "zoom in" and "zoom out" buttons. At first I tried to achieve this by styling everything using rem units and using this js:
$(document).ready(function() {
var fontSize = parseInt($("html").css("font-size"), 10);
$("#in").on("click", function() {
fontSize += 0.5;
$("html").css("font-size", fontSize + "px");
});
$("#out").on("click", function() {
fontSize -= 0.5;
$("html").css("font-size", fontSize + "px");
});
});
This works, but the problem is that there are many divs on the page (laid out in a single vertical column), so if you are scrolled in the middle of the page and then click the zoom function, causing them all to resize, it produces a scrolling effect as the divs get smaller or bigger and therefore get pushed further up or down on the page. This is disorienting if the content you were viewing before zooming is no longer on the page after the zoom. Here is a codepen demonstrating this.
Next, I tried zooming using the transform scale() css property and adjusting the transform-origin to be centered on the user's scroll position:
var zoom = 1;
$("#in").on("click", function () {
var x = window.innerWidth / 2;
var y = $(window).scrollTop() + $(window).height() / 2;
zoom += 0.2;
$(".container").css({
transformOrigin: x + "px " + y + "px",
transform: "scale(" + zoom + ")",
});
});
$("#out").on("click", function () {
var x = window.innerWidth / 2;
var y = $(window).scrollTop() + $(window).height() / 2;
zoom -= 0.2;
$(".container").css({
transformOrigin: x + "px " + y + "px",
transform: "scale(" + zoom + ")",
});
});
The problem with this is that portions of the pages get cut off when you zoom as the viewport doesn't expand to accommodate the scaled divs. Here is a codepen to demonstrate this approach.
I've searched extensively and can't seem to find a good solution to the problem. I have also considered using the "zoom" css function but from what I understand this is not supported and behaves differently in different browsers.
Any ideas would be much appreciated! Thanks
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.
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)" );
}
When I zoom with the mouse, the following function attached to myZoom will be executed:
myZoom.on('zoom', function() {
someElement.attr('transform', 'translate(' + d3.event.translate[0] + ',' + d3.event.translate[1] + ') scale(' + d3.event.scale + ')');
....
// redraw axes, which should stay where they are at.
....
}
To simulate zoom without mouse or some other pointing device, I can just change the value of the attribute 'transform' above. Easy.
But problem is in this function I actually redraw axes, whose scale is automatically recalculated. Refer to this official documentation from d3:
zoom.y([y])
Specifies an y-scale whose domain should be automatically adjusted
when zooming. If not specified, returns the current y-scale, which
defaults to null. If the scale's domain is modified programmatically,
it should be reassigned to the zoom behaviour.
I need to zoom programmatically (maybe with zoom button). How can I fire zoom event, so that scale of my axes is automatically recalculated?
Programmatic zoom seems to be a daunting task in the D3 library because the D3 zooming is closely tied to the mouse events. A common instance of programmatic zooming is zooming in or out with a slider control. Surprisingly, I couldn't find a single working example of how to make D3 zooming work with a slider control. After investing some time and effort I developed this working demo which can be found here D3SliderZoom. The key point is to change the transform attribute of a "<g>" SVGElement embedded in an "<svg>" element using the scale value thrown by the slider.
function zoomWithSlider(scale) {
var svg = d3.select("body").select("svg");
var container = svg.select("g");
var h = svg.attr("height"), w = svg.attr("width");
// Note: works only on the <g> element and not on the <svg> element
// which is a common mistake
container.attr("transform",
"translate(" + w/2 + ", " + h/2 + ") " +
"scale(" + scale + ") " +
"translate(" + (-w/2) + ", " + (-h/2) + ")");
}
This method then has to be invoked from the change event of the slider as shown below.
$(function() {
$( "#slider-vertical" ).slider({
orientation: "vertical",
range: "min",
min: 1000,
max: 10000,
value: 1000,
slide: function( event, ui ) {
zoomWithSlider(ui.value/1000);
}
});
});
This solution is much more elegant than generating pseudo-mouse scroll event.
I ended up calculating new domain for a new zoom level by myself. With this new domain I could redraw two y-axes. For someone, who has same problem, I post my code. It's very specific to my project, so it might be hard to understand. Just for your interest.
wr.zoomSim = function(sNew) {
var s = wr.zoom.scale(),
tx = wr.zoom.translate()[0],
ty = wr.zoom.translate()[1],
sReal = sNew / s,
dtx = wr.width / 2 * (1 - sReal),
dty = wr.height / 2 * (1 - sReal),
txNew = sReal * tx + dtx,
tyNew = sReal * ty + dty,
a = wr.scaleYBZoom.domain()[0],
b = wr.scaleYBZoom.domain()[1],
c = wr.scaleYBZoom.range()[0],
d = wr.scaleYBZoom.range()[1],
r = (b-a)/(d-c);
wr.scaleYBZoom.domain([a + r * ( (c - dty) / sReal - c), a + r * ( (d - dty) / sReal - c)]);
wr.zoom.scale(sNew);
wr.zoom.translate([txNew, tyNew]);
wr.svg2.select('g#bar')
.attr('transform', 'translate(' + txNew + ',' + tyNew + ') scale(' + sNew + ')');
wr.svg2.select('g#axisl')
.call(d3.svg.axis().scale(wr.scaleYBZoom).orient('left'))
.selectAll('line.tick')
.attr('x2', wr.width - wr.bar.left - wr.bar.right + 2 * wr.padding);
wr.svg2.select('g#axisr')
.call(d3.svg.axis().scale(wr.scaleYBZoom).orient('right'))
.selectAll('line')
.remove();
};
Unfortunately #Debasis's answer didn't work for me, because I wanted to achieve this with a zoom behavior which I was already using with my force layout. After two days of desperation I finally found the solution in this thread:
https://groups.google.com/forum/#!msg/d3-js/qu4lX5mpvWY/MnnRMLz_cnUJ
function programmaticZoom ($svg, $zoomContainer, zoomBehavior, factor) {
var width = $svg.attr('width');
var height = $svg.attr('height');
var newScale = zoomBehavior.scale() * factor;
var newX = (zoomBehavior.translate()[0] - width / 2) * factor + width / 2;
var newY = (zoomBehavior.translate()[1] - height / 2) * factor + height / 2;
zoomBehavior
.scale(newScale)
.translate([newX,newY])
.event($zoomContainer);
}
I did this by making use of zoomListener. Worked well in simple steps for me:
Define zoomListener:
var zoomListener = d3.behavior.zoom();
Call the zoom listener
d3.select(the-element-that-you-need-zoom-on).call(zoomListener);
Decide your zoom step. I took steps of 0.1. (Use 1.1 for zoom-in and 0.9 for zoom-out)
Multiply with the current zoom scale
var newScale = zoomListener.scale() * step;
Set the new scale value
zoomListener.scale(newScale);
I am using javascript Gesture events to detect multitouch pan/scale/rotation applied to an element in a HTML document.
Visit this URL with an iPad:
http://www.merkwelt.com/people/stan/rotate_test/
You can touch the element with two finger and rotate it, but sometimes the rotation property goes go astray and my element flips around many full rotations.
Here is part of my code, I am really only taking the value directly from the event object:
...bind("gesturechange",function(e){
e.preventDefault();
curX = e.originalEvent.pageX - startX;
curY = e.originalEvent.pageY - startY;
node.style.webkitTransform = "rotate(" + (e.originalEvent.rotation) + "deg)" +
" scale(" + e.originalEvent.scale + ") translate3D(" + curX + "px, " + curY + "px, 0px)";
}...
What happens is that the value gets either 360 degrees added or subtracted, so I could monitor the value and react to sudden large changes, but this feels like a last resort.
Am I missing something obvious?
I found a solution.
In order to avoid sudden changes in the rotation that don't reflect real finger moves you need to test for that. I do that testing if the rotation changed more then 300 degrees in either direction, if it does then you need to add or subtract 360 depending on the direction. Not really intuitive, but it works.
Fixed page is here:
http://www.merkwelt.com/people/stan/rotate_test/index2.html
Here is the code
<script type="text/javascript">
var node;
var node_rotation=0;
var node_last_rotation=0;
$('.frame').bind("gesturestart",function(e){
e.preventDefault();
node=e.currentTarget;
startX=e.originalEvent.pageX;
startY=e.originalEvent.pageY;
node_rotation=e.originalEvent.rotation;
node_last_rotation=node_rotation;
}).bind("gesturechange",function(e){
e.preventDefault();
//whats the difference to the last given rotation?
var diff=(e.originalEvent.rotation-node_last_rotation)%360;
//test for the outliers and correct if needed
if( diff<-300)
{
diff+=360;
}
else if(diff>300)
{
diff-=360;
}
node_rotation+=diff;
node_last_rotation=e.originalEvent.rotation;
node.style.webkitTransform = "rotate(" + (node_rotation) + "deg)" +
" scale(" + (e.originalEvent.scale) +
") translate3D(" + (e.originalEvent.pageX - startX) + "px, " + (e.originalEvent.pageY - startY) + "px, 0px)";
}).bind("gestureend",function(e){
e.preventDefault();
});
</script>