I have setup this fiddle to try to figure out why the scale that I pass to d3.behavior.zoom() is being ignored. I am setting the scale to 10 but the effective scale is 1. Then if I try to pan the grid the scale jumps to 10. Same thing happens when I try to zoom the grid. The scale jumps to 10 and then the additional scale is applied. What am I doing wrong? How can I work this out in order to have an effective initial scale value?
You're initializing a var scale = 10 to represent that initial zoom, and you're applying that to zoomer via .scale(10). So zoomer knows the initial scale, but nothing in your code is doing the work of applying the initial scale to the d3 selection vis. The first time the scale is actually applied is once the user interacts, which in turn calls function zoom() {...}, where the scale is finally applied.
The time/place to apply the initial scale, is when you create vis:
//create a group that will hold all the content to be zoomed
var vis = graph.append("svg:g")
.attr("class", "plotting-area")
.attr("transform", "scale(" + scale + ")");// <-- THIS
Here's an updated fiddle
Related
I'm trying to zoom in on an SVG element after having translated it's Matrix, but when I start the initial zoom, the position of the SVG resets to 0,0. I'd like the zoom and pan to start from the moved position on page load.
<svg id="#svg">
<g id="#mainGrid>
....a bunch of SVG
</g>
<svg>
<script>
var winCenterV = $(window).height()/2;
var winCenterH = $(window).width()/2;
//set mainGrid to the center of window using snap.svg
var mainGrid = Snap.select("#mainGrid");
var myMatrix = new Snap.Matrix();
myMatrix.scale(1,1);
myMatrix.translate(winCenterH,winCenterV);
myMatrix.rotate(0);
mainGrid.transform(myMatrix);
//d3 zoom
var svgElement = d3.select("svg");
var gridELement = d3.select("#mainGrid");
svgElement.call(d3.zoom()
.scaleExtent([1 / 2, 4])
.on("zoom", zoomed));
function zoomed() {
gridELement.attr("transform", d3.event.transform);
}
</script>
the "mainGrid" SVG element zooms and pans fine, but it snaps back to it's 0,0 position (top left of browser) during the first zoom mouse click or wheel scroll instead of zooming and panning from the transformed location as set by myMatrix. How can I get the d3.event.transform to start zooming from this offset?
Problem:
The d3 zoom behavior does not track or know about what transforms you've applied to an element. It is initialized with scale equal to 1 and translate equal to 0,0. So, when you use
gridELement.attr("transform", d3.event.transform);
in your zoomed function, you are zooming relative to this initial scale and translate.
Rationale
Not every d3 zoom manipulates the transform of the SVG element it is called on: you may want to have zoom interaction everywhere on a plot, but only zoom the plot area and not the margins. Many d3 zooms don't manipulate any SVG transform: they may use semantic zooming, or the zoom may be altering a canvas rather than SVG, etc. As such, a d3-zoom behavior is independent of any SVG transorm attribute on the element(s) it is called on.
Solution
If your zooming by manipulating the transform attribute of an SVG element, let D3 zoom do all of the manipulation - no need to manipulate it manually and have a zoom, this is how the zoom gets out of step.
So,
You can programmatically trigger a zoom event prior to rendering anything. The easiest way to do so is to use:
selection.call(zoom.transform, d3.zoomIdentity.translate(x,y).scale(k));
This line triggers a zoom event. We can use this prior to rendering anything and rely on the zoomed function to set the initial value of the SVG transform attribute. All subsequent zooms will be relative to this and we can proceed as normal:
var svg = d3.select("svg");
var g = svg.append("g");
// define a zoom behavior:
var zoom = d3.zoom()
.on("zoom", zoomed);
// call the zoom:
svg.call(zoom)
// trigger tha initial zoom with an initial transform.
svg.call(zoom.transform,d3.zoomIdentity.scale(20).translate(-100,-100));
// draw the visualization:
var rect = g.append("rect")
.attr("width", 5)
.attr("height", 5)
.attr("x", 100)
.attr("y", 100);
// zoomed function:
// zoom as normal.
function zoomed() {
g.attr("transform",d3.event.transform);
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<svg width="500" height="300"></svg>
The above snippet applies an initial zoom (scale 20, translate -100,-100) and uses this as the starting point for the zoom interaction. The comment should help explain the process.
// It appears that you're moving #mainGrid and not #svg. So when you call zoom from svgElement it is zooming from it's 0,0 position.
// Or it may because you don't have pound sign in d3.select
var svgElement = d3.select("#svg");
// Try changing svgElement to gridElement
gridELement.call(d3.zoom()
.scaleExtent([1 / 2, 4])
.on("zoom", zoomed));
please see the following D3 example:
https://bl.ocks.org/mbostock/f48fcdb929a620ed97877e4678ab15e6
In this example, the user is able to draw a brush on the screen and then the graph zooms to the region encapsulated by the brush. The way this example works is by updating the x and y scale domains based on the brush extent. You can see that in the brushended() function.
I am looking to achieve this same functionality. However, I do not want to update the x and y scale domains but want to update the zoom scale and translate. The following example achieves this for a map:
https://bl.ocks.org/mmazanec22/586ee6674f52840492c16cc194aacb1f
You can see this is the brushend() function. Here the writer calculates a scale and translation based on the brush extent and then uses them to set a new scale and translate for the zoom behaviour. I am looking to achieve this however for a scatter plot.
I have a written an example of what I have tried in this jsfiddle: https://jsfiddle.net/SSS123/uxdd760k/1/ - see the part where I do my scale and translate calculations below:
var extent = brush.extent();
// Get box coordinates
var lowerX = d3.min([extent[0][0], extent[1][0]]);
var upperX = d3.max([extent[0][0], extent[1][0]]);
var lowerY = d3.min([extent[0][1], extent[1][1]]);
var upperY = d3.max([extent[0][1], extent[1][1]]);
var scale = Math.max( ( width / (x(upperX) - x(lowerX)) ), ( height / (y(upperY) - y(lowerY)) ) );
graphContainer.transition()
.duration(750)
.call(zoom.scale(scale).translate([-x(lowerX), -y(upperY)]).event);
I believe this now works fine for the case when you draw a brush with a width the same size as the height i.e. if you try drawing a box that stretches from 0 to 5 on the xAxis and 0 to 5 on the yAxis then this will zoom in quite well.
However, if you draw a box with 0 - 5 on the xAxis but 0 - 30 on the yAxis then this will no longer calculate a sensible zoom scale.
Has anyone any advise on how to solve this?
I'm having an issue specific to scale behavior with a default mercator projection scale value, in my case a pretty ridiculous value (75000), since somehow it was the only way I was able to get my TOPOJson map to be project as anything other than a dot.
I created a projection of a map and want to add zoom and pan. My functions work, all except for the default zoom value. As soon as you scroll to zoom, the value jumps back to the default projection scale value, rather than scaling based on the mouse wheel. I have a feeling that the reason is because in order to get the map to display I needed to make my projection scale value really high (75000).
So when the page loads, the zoom looks fine, like this... but when you try to pan or zoom in or out one mouse wheel click, it jumps to this
I tried to limit the code to just the projection and zoom logic bellow.
<body>
<script src="//d3js.org/d3.v3.min.js" charset="utf-8"></script></script>
<script src="//d3js.org/topojson.v1.min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.1.1/jquery.min.js"></script>
<div id = "map" style = "float : left; min-width:320px;width :50%;height : 600px;"></div>
<script>
var width = 900,
height = 800;
var projection = d3.geo.mercator()
.center([-73.94, 40.70])
.scale(75000)
.translate([(width/2), (height/2)]);
var svg = d3.select("#map").append("svg")
.attr("viewBox", "0 0 900 800")
.attr("preserveAspectRatio", "xMidYMid meet");
var path = d3.geo.path()
.projection(projection);
var g = svg.append("g")
.attr("transform", "translate(-793.0489643729818,-630.1796382734433)scale(3.01487286910262)");
d3.json("ny.json", function(error, ny) {
g.append("path")
.attr("id", "boroughs")
.datum(topojson.feature(ny, ny.objects.boroughs))
.attr("d", path);
// zoom and pan
var zoom = d3.behavior.zoom()
.on("zoom",function() {
g.attr("transform","translate("+
d3.event.translate.join(",")+")scale("+d3.event.scale+")");
g.selectAll("path")
.attr("d", path.projection(projection));
});
svg.call(zoom)
});
I believe your issue is that you set initial scaling and translate values for your g and then reset them on zoom. For scale, for example:
You set a scale for the g to 3.01 to start:
var g = svg.append("g")
.attr("transform", "translate(-793.0489643729818,-630.1796382734433)scale(3.01487286910262)");
However, the first time you zoom with d3.event, it'll return 0.607 or 1.647, (zoom out, zoom in) both are zoomed out relative to 3.01:
g.attr("transform","translate("+
d3.event.translate.join(",")+")scale("+d3.event.scale+")");
This creates the jump you are seeing, as both scale and translate are reset and thus independent of your initial values. I think the translate value would be ok but you call it on the svg and not the g on which you specify the initial translate.
In any event, it is probably easier to not set an initial transform on your g, instead using the projection to do this (instead of a two step center and zoom with both a projection and a transform). That way you can leave all your code unchanged, save for removing the line setting the initial g transform and a modification to your projection.
There is a question here
d3.js: pan with limits
Answering the question to limiting the pan movement. But this uses axis, which I do not have.
What I have is a force directed graph which has the ability to pan and zoom.
Now I have put a limit on the zoom using :
.call(d3.behavior.zoom().on("zoom", redraw).scaleExtent([0.8, 2]))
I am wondering how do I go about limiting the pan movement of this graph so i dont drag the network/graph outside of the viewport.
(The network may change size depending on the JSON file imported, so I can't use exact figures)
inner.attr("transform","translate(" + d3.event.translate + ")" + " scale(" + d3.event.scale + ")");
There is no equivalent zoom.extent. You can override zoom's translate with mins and maxes inside your redraw function, though. For instance, if you wanted to limit zoom to stay within 500px of its original position:
function redraw() {
var oldT = yourZoom.translate()
var newT = [0,0]
newT[0] = Math.max(-500,oldT[0]);
newT[0] = Math.min(500,oldT[0]);
newT[1] = Math.max(-500,oldT[0]);
newT[1] = Math.min(500,oldT[0]);
yourZoom.translate(newT);
//The rest of your redraw function, now using the updated translate on your zoom
}
http://jsfiddle.net/HF57g/ is an example of a single event(blue rect) placed on the timeline. If i zoom into the timeline, rect's placement on the x axis changes in harmony with the ticks on axis. but at the same time, rect is scaled horizontally.
if i modify the following section of the code
svg.selectAll(".item").attr("transform", "translate(" + d3.event.translate[0]+", 0)scale(" + d3.event.scale+", 1)");
to
svg.selectAll(".item").attr("transform", "translate(" + d3.event.translate[0]+", 0)scale(1, 1)");
as in http://jsfiddle.net/HF57g/1/ to disable horizontal scaling, then the rect's position changes much more than the axis' during zoom in/out.
How can i zoom in/out time.scale without scaling other related shapes?
I ended up adding an update function to rearrange the rects' places depending on the new positions returned by scale(x).
function update_events(){
return svg.selectAll("rect.item")
.attr("x", function(d){return scale(d);})
}
Here's the final version: http://jsfiddle.net/HF57g/2/