I am upgrading my d3 from version 3 to version 4 and having trouble updating the zoom behavior.
The following returns an error that y is not a function. Any Idea how to write this in version 4?
d3.zoom()
.y(yAxis)
.scaleExtent([1, 20])
.on('zoom', myFunctionHandler);
D3v3
In d3v3 the zoom behavior optionally modified x and y scales' domain and range using the zoom.x and zoom.y methods. This resulted in the following pattern:
let svg = d3.select("svg");
let g = svg.append("g").attr("transfrom","translate(0,50)");
let scale = d3.scale.linear()
.range([10,290])
let axis = d3.svg.axis()
.scale(scale)
.ticks(4)
.orient("bottom");
g.call(axis);
let zoom = d3.behavior.zoom()
.y(scale)
.scaleExtent([1, 20])
.on('zoom', zoomed);
svg.call(zoom);
function zoomed() {
g.call(axis);
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.17/d3.min.js"></script>
<svg width="300"></svg>
D3v4 and D3v5
D3v4 saw a substantial reworking of D3 as compared with v3, but the zoom behavior saw extensive changes relative to rest of the library. Instead of the zoom behavior modifying scales, the zoom event's transform have rescale methods that can be passed a scale. The rescale methods return a new scale with the appropriate updated domain for any given zoom (for either a horizontal or vertical scale), eg:
d3.event.transform.rescaleX(xScale);
Of course we need to update d3.svg.axis to d3.axis and d3.scale.type to d3.scaleType as well.
This leads to the following pattern:
let svg = d3.select("svg");
let g = svg.append("g").attr("transform","translate(0,50)");
let scale = d3.scaleLinear()
.range([10,290])
let axis = d3.axisBottom()
.scale(scale)
.ticks(4);
g.call(axis);
let zoom = d3.zoom()
.on("zoom", zoomed);
function zoomed() {
let zoomedScale = d3.event.transform.rescaleX(scale);
axis.scale(zoomedScale);
g.call(axis);
}
svg.call(zoom);
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<svg width="300"></svg>
Why not just rescale the original scale and use this with the axis? Because the zoom state is relative to its starting state (k: 1, x:0, y:0) we want to always use the original scale when rescaling, so we need to preserve the original scale. See here for more on that.
That the zoom no longer tracks/modifies x or y scales has resulted in the removal of the x and y methods of the zoom behavior: which explains your error that y is not a function.
D3v6 and v7
Starting with d3v6 the d3.event global is removed. Instead the event is passed to the zoom event function directly:
.on("zoom", function(event, datum) { ...
Which means we only need tweak the above:
let svg = d3.select("svg");
let g = svg.append("g").attr("transform","translate(0,50)");
let scale = d3.scaleLinear()
.range([10,290])
let axis = d3.axisBottom()
.scale(scale)
.ticks(4);
g.call(axis);
let zoom = d3.zoom()
.on("zoom", zoomed);
function zoomed({transform}) {
let zoomedScale = transform.rescaleX(scale);
axis.scale(zoomedScale);
g.call(axis);
}
svg.call(zoom);
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/7.0.0/d3.min.js"></script>
<svg width="300"></svg>
TL;DR
For all versions of d3 after v4 the zoom behavior does not modify scales. Instead the zoom event transform property provides methods to rescale x and y scales, these methods return the new rescaled scales to provide to axis generators.
Related
I've got a chart with lots of points. That's why I'm using canvas to draw the lines. For the x and y axes I'd like to use SVG since it's sharper and drawing text with canvas isn't super fast.
Here is the code (TypeScript)
import { min, max } from "d3-array";
import { scaleLinear, ScaleLinear } from "d3-scale";
import { select, event, Selection } from "d3-selection";
import { line, Line } from "d3-shape";
import { ZoomBehavior, zoom } from "d3-zoom";
import { axisBottom, Axis, axisLeft } from "d3-axis";
interface Margin {
left: number;
right: number;
top: number;
bottom: number;
}
interface Config {
margin: Margin;
target: HTMLCanvasElement;
svg: SVGSVGElement;
}
export default class ScopeChart {
private canvas: Selection<HTMLCanvasElement, unknown, null, undefined>;
private svg: Selection<SVGGElement, unknown, null, undefined>;
private xAxis: Axis<number>;
private xAxisGroup: Selection<SVGGElement, unknown, null, undefined>;
private yAxis: Axis<number>;
private yAxisGroup: Selection<SVGGElement, unknown, null, undefined>;
private context: CanvasRenderingContext2D;
private raw: number[];
private filtered: number[];
private xScale: ScaleLinear<number, number>;
private yScale: ScaleLinear<number, number>;
private line: Line<number>;
public constructor(config: Config) {
this.raw = [];
this.filtered = [];
const behavior = zoom() as ZoomBehavior<SVGGElement, unknown>;
const width = 640;
const height = 480;
const w = width - config.margin.left - config.margin.right;
const h = height - config.margin.top - config.margin.bottom;
this.canvas = select(config.target)
.attr("width", w)
.attr("height", h)
.style(
"transform",
`translate(${config.margin.left}px, ${config.margin.top}px)`
);
this.svg = select(config.svg)
.attr("width", width)
.attr("height", height)
.append("g")
.attr(
"transform",
`translate(${config.margin.left}, ${config.margin.top})`
);
this.svg
.append("rect")
.attr("width", w)
.attr("height", h)
.style("fill", "none")
.style("pointer-events", "all")
.call(behavior);
behavior
// set min to 1 to prevent zooming out of data
.scaleExtent([1, Infinity])
// prevent dragging data out of view
.translateExtent([[0, 0], [width, height]])
.on("zoom", this.zoom);
this.context = (this.canvas.node() as HTMLCanvasElement).getContext(
"2d"
) as CanvasRenderingContext2D;
this.xScale = scaleLinear().range([0, w]);
this.xAxis = axisBottom(this.xScale) as Axis<number>;
this.xAxisGroup = this.svg
.append("g")
.attr("class", "x axis")
.attr("transform", `translate(0, ${h})`)
.call(this.xAxis);
this.yScale = scaleLinear().range([h, 0]);
this.yAxis = axisLeft(this.yScale) as Axis<number>;
this.yAxisGroup = this.svg
.append("g")
.attr("class", "y axis")
.call(this.yAxis);
this.line = line<number>()
.x((_, i): number => this.xScale(i))
.y((d): number => this.yScale(d))
.context(this.context);
}
private drawRaw(context: CanvasRenderingContext2D): void {
context.beginPath();
this.line(this.raw);
context.lineWidth = 1;
context.strokeStyle = "steelblue";
context.stroke();
}
private drawFiltered(context: CanvasRenderingContext2D): void {
context.beginPath();
this.line(this.filtered);
context.lineWidth = 1;
context.strokeStyle = "orange";
context.stroke();
}
private clear(context: CanvasRenderingContext2D): void {
const width = this.canvas.property("width");
const height = this.canvas.property("height");
context.clearRect(0, 0, width, height);
}
public render(raw: number[], filtered: number[]): void {
this.raw = raw;
this.filtered = filtered;
this.xScale.domain([0, raw.length - 1]);
this.yScale.domain([min(raw) as number, max(raw) as number]);
this.clear(this.context);
this.drawRaw(this.context);
this.drawFiltered(this.context);
this.xAxisGroup.call(this.xAxis);
this.yAxisGroup.call(this.yAxis);
}
public zoom = (): void => {
const newXScale = event.transform.rescaleX(this.xScale);
const newYScale = event.transform.rescaleY(this.yScale);
this.line.x((_, i): number => newXScale(i));
this.line.y((d): number => newYScale(d));
this.clear(this.context);
this.drawRaw(this.context);
this.drawFiltered(this.context);
this.xAxisGroup.call(this.xAxis.scale(newXScale));
this.yAxisGroup.call(this.yAxis.scale(newYScale));
};
}
And here is the live example
https://codesandbox.io/s/1pprq
The problem is translateExtent. I'd like to restrict dragging when zoomed in to my available data, i.e. [0, 20000] on the x axis and [-1.2, 1.2] on the y axis.
Somehow I'm currently able to zoom in further. You can see the effect when zooming in and dragging all the way to the bottom. You will see a gap between the lowest value and the x axis.
I think it has something to do with using canvas and svg. The svg is on top of the canvas and the ZoomBehavior is on the svg. Somehow the zoom isn't properly translated to the canvas.
I'd like to keep the svg on top because I need more interface elements later one which are added to the svg.
Any ideas? Thank you!
If I understand the question correctly:
The issue you are running into is that your translate extent is not correct
behavior
// set min to 1 to prevent zooming out of data
.scaleExtent([1, Infinity])
// prevent dragging data out of view
.translateExtent([[0, 0], [width, height]])
.on("zoom", this.zoom);
In the above, width and height refer to the width and height of the SVG, not the canvas. Also, zoom extent is not often specified explicitly, but if zoom extent is not specified with zoom.extent(), the zoom extent defaults to the dimensions of the container it was called on.
If your translate extent is equal in size to your zoom extent - by default the extent of the container (the SVG) - which it is, you can zoom and pan anywhere within that container's coordinate space, but not to coordinates beyond it. Consequently, when zoom scale is 1, we cannot pan anywhere as we would by definition pan beyond the translate extent.
Note: This logically means translate extent must contain and not be smaller than the zoom extent.
But, in this scenario, if we zoom in, we can pan and remain within the translate extent.
We can see if you zoom in you cannot pan up beyond the intended limits. This is because the top of the canvas is at y==0, this is the bounds of the translate extent.
As you note if you zoom in you can pan down beyond the intended limits. The bottom of the canvas is h, which is smaller than height which is the translate extent limit, so as we zoom in, we can pan further and further down as the gap between h and height increases each time we zoom (and as noted above, cannot be panned when k==1).
We could try to change the translate extent to reflect the bounds of the canvas. But, as the canvas is smaller than the SVG this won't work as the translate extent would be smaller than the zoom extent. As noted above and noted here by Mike: "The problem is that the translateExtent you’ve specified is smaller than the zoom extent. So there’s no way to satisfy the requested constraint."
We can modify the translateExtent and the zoom's extent, however:
behavior
// set min to 1 to prevent zooming out of data
.scaleExtent([1, Infinity])
// set the zoom extent to the canvas size:
.extent([[0,0],[w,h]])
// prevent dragging data out of view
.translateExtent([[0, 0], [w, h]])
.on("zoom", this.zoom);
The above creates a zoom behavior that constrains the canvas to its original extent - we would be providing the same parameters if we were calling the zoom on the canvas and wanted to not be able to pan beyond it (except we could rely on the default zoom extent to provide the appropriate values rather than specifying the zoom extent manually).
Here's an updated sandbox.
I am working on a world map that features a click to zoom feature. When clicking a country the map zooms in but the country is not always centered -- the same happens when you click out and repeat, it never seems to deliver the same result.
Note: If you disable the transition function, the zoom and centering does work, only when rotation is added it displays incorrectly.
What is wrong with my code?
I created a plunker for convenience http://plnkr.co/edit/tgIHG76bM3cbBLktjTX0?p=preview
<!DOCTYPE html>
<meta charset="utf-8">
<style>
.background {
fill: none;
pointer-events: all;
stroke:grey;
}
.feature, {
fill: #ccc;
cursor: pointer;
}
.feature.active {
fill: orange;
}
.mesh,.land {
fill: black;
stroke: #ddd;
stroke-linecap: round;
stroke-linejoin: round;
}
.water {
fill: #00248F;
}
</style>
<body>
<script src="//d3js.org/d3.v3.min.js"></script>
<script src="//d3js.org/topojson.v1.min.js"></script>
<script src="//d3js.org/queue.v1.min.js"></script>
<script>
var width = 960,
height = 600,
active = d3.select(null);
var projection = d3.geo.orthographic()
.scale(250)
.translate([width / 2, height / 2])
.clipAngle(90);
var path = d3.geo.path()
.projection(projection);
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
svg.append("rect")
.attr("class", "background")
.attr("width", width)
.attr("height", height)
.on("click", reset);
var g = svg.append("g")
.style("stroke-width", "1.5px");
var countries;
var countryIDs;
queue()
.defer(d3.json, "js/world-110m.json")
.defer(d3.tsv, "js/world-110m-country-names.tsv")
.await(ready)
function ready(error, world, countryData) {
if (error) throw error;
countries = topojson.feature(world, world.objects.countries).features;
countryIDs = countryData;
//Adding water
g.append("path")
.datum({type: "Sphere"})
.attr("class", "water")
.attr("d", path);
var world = g.selectAll("path.land")
.data(countries)
.enter().append("path")
.attr("class", "land")
.attr("d", path)
.on("click", clicked)
};
function clicked(d) {
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 = 0.5 / Math.max(dx / width, dy / height),
translate = [width / 2 - scale * x, height / 2 - scale * y];
g.transition()
.duration(750)
.style("stroke-width", 1.5 / scale + "px")
.attr("transform", "translate(" + translate + ")scale(" + scale + ")");
var countryCode;
for (i=0;i<countryIDs.length;i++) {
if(countryIDs[i].id==d.id) {
countryCode = countryIDs[i];
}
}
var rotate = projection.rotate();
var focusedCountry = country(countries, countryCode);
var p = d3.geo.centroid(focusedCountry);
(function transition() {
d3.transition()
.duration(2500)
.tween("rotate", function() {
var r = d3.interpolate(projection.rotate(), [-p[0], -p[1]]);
return function(t) {
projection.rotate(r(t));
g.selectAll("path").attr("d", path)
//.classed("focused", function(d, i) { return d.id == focusedCountry.id ? focused = d : false; });
};
})
})();
function country(cnt, sel) {
for(var i = 0, l = cnt.length; i < l; i++) {
console.log(sel.id)
if(cnt[i].id == sel.id) {
return cnt[i];
}
}
};
}
function reset() {
active.classed("active", false);
active = d3.select(null);
g.transition()
.duration(750)
.style("stroke-width", "1.5px")
.attr("transform", "");
}
</script>
This is a difficult question - I was surprised to see that there are not good examples of this (and the issue may have been raised previously without resolution). Based on the problem and what you are trying to achieve, I think you are overly complicating your transitions (and the tween functionality can be made clearer, perhaps). Instead of using both a transform on the g and a modification of the projection, you can achieve this with just a modification of the projection.
Current Approach
Currently you pan and zoom the g, this pans and zooms the g to the intended destination. After the click, the g is positioned so that the feature is in the middle and then scaled to showcase the feature. Consequently, the g is no longer centered in the svg (as it has been scaled and translated), in other words, the globe is moved and stretched so that the feature is centered. No paths are altered.
At this point, you rotate the projection, which recalculates the paths based on the new rotation. This moves the selected features to the center of the g, which is no longer centered within the svg - as the feature was already centered within the svg, any movement will decenter it. For example, if you remove the code that rescales and translates the g, you'll notice your feature is centered on click.
Potential solution
You appear to be after two transformations:
rotation
scale
Panning(/translating) is not something you probably want to do here, as this moves the globe when you simply want to rotate it.
Rotation can only be done with a d3 projection and scale can be done with either manipulation to the g or within the d3 projection. Therefore, it is probably simpler to just use a d3 projection to handle your map transformations.
Also, an issue with the current approach is that by using path.bounds to get a bbox, to derive both scale and translate, you are calculating values which may change as the projection is updated (the type of projection will vary the variance too).
For example, if only a portion of a feature is rendered (because it is partly over the horizon), the bounding box will be different than it should, this will cause problems in scaling and translating. To overcome this limitation in my proposed solution, rotate the globe first, calculate the bounds, and scale to that factor. You can calculate the scale without actually updating the rotation of the paths on the globe, just update path and transition the drawn paths later.
Solution Implementation
I've modified your code slightly, and I think it is cleaner ultimately, to implement the code:
I store the current rotation and scale (so we can transition from this to the new values) here:
// Store the current rotation and scale:
var currentRotate = projection.rotate();
var currentScale = projection.scale();
Using your variable p to get the feature centroid we are zooming to, I figure out the bounding box of the feature with the applied rotation (but I don't actually rotate the map yet). With the bbox, I get the scale needed to zoom to the selected feature:
projection.rotate([-p[0], -p[1]]);
path.projection(projection);
// calculate the scale and translate required:
var b = path.bounds(d);
var nextScale = currentScale * 1 / Math.max((b[1][0] - b[0][0]) / (width/2), (b[1][1] - b[0][1]) / (height/2));
var nextRotate = projection.rotate(); // as projection has already been updated.
For more information on the calculation of the parameters here, see this answer.
Then I tween between the current scale and rotation and the target (next) scale and rotation:
// Update the map:
d3.selectAll("path")
.transition()
.attrTween("d", function(d) {
var r = d3.interpolate(currentRotate, nextRotate);
var s = d3.interpolate(currentScale, nextScale);
return function(t) {
projection
.rotate(r(t))
.scale(s(t));
path.projection(projection);
return path(d);
}
})
.duration(1000);
Now we are transitioning both properties simultaneously:
Plunker
Not only that, since we are redrawing the paths only, we don't need to modify the stroke to account for scaling the g.
Other refinements
You can get the centroid of the country/feature with just this:
// Clicked on feature:
var p = d3.geo.centroid(d);
Updated Plunker
or Bl.ock
You can also toy with the easing - rather than just using a linear interpolation - such as in this plunker or bl.ock. This might help with keeping features in view during the transition.
Alternative Implementation
If you really want to keep the zoom as a manipulation of the g, rather than the projection, then you can achieve this, but the zoom has to be after the rotation - as the feature will then be centered in the g which will be centered in the svg. See this plunker. You could calculate the bbox prior to the rotation, but then the zoom will temporarily move the globe off center if making both transitions simultaneously (rotation and scale).
Why do I need to use tweening functions to rotate and scale?
Because portions of the paths are hidden, the actual paths can gain or loose points, completely appear or disappear. The transition to its final state might not represent the transition as one rotates beyond the horizon of the globe (in fact it surely won't), a plain transition of paths like this can cause artifacts, see this plunker for a visual demonstration using a modification of your code. To address this, we use the tween method .attrTween.
Since the .attrTween method is setting the transition from one path to another, we need to scale at the same time. We cannot use:
path.transition()
.attrTween("d", function()...) // set rotation
.attr("d", path) // set scale
Scaling SVG vs Scaling Projection
Many cylindrical projections can be panned and zoomed by manipulating the paths/svg directly, without updating the projection. As this doesn't recalculate the paths with a geoPath, it should be less demanding.
This is not a luxury afforded by the orthographic or conical projections, depending on the circumstances involved. Since you are recalculating the paths anyways when updating the rotation, an update of the scale likely won't lead to extra delay - the geographic path generator needs to re-calculate and re-draw the paths considering both scale and rotation anyways.
I'm trying to make a button-activated d3 zoom tour through three Northeastern US points, but am having a hard time getting the data to show up visually (it shows in the console, though). I'm a beginning user and can usually solve things, but this is over my head.
Here are the an example that comes close to what I'm trying to do:
Zooms between different spots in US:
http://bl.ocks.org/mbostock/6242308
The example doesn't style the data, uses TopoJSON and also uses canvas to do the zooming calls. I'm trying to do the zoom with GeoJSON(so I can link to a CartoDB table), and style it.
I've gone through a lot to make both of those things happen and am running out of successes. Right now it comes up blank and has been. I can see the data live, but can't change the styling.
What am I doing wrong here? I'm sure it's something simple, but need a nudge.
<!DOCTYPE html>
<html lang ="en">
<head>
<meta charset="utf-8">
<body>
<script src="http://d3js.org/d3.v3.min.js"></script>
<style type="text/css">
canvas{
color: 'blue';
}
path.state {
color: 'red';
}
</style>
</head>
<body>
<script type="text/javascript">
var width = 960,
height = 500,
stateMap;
var sf = [-122.417, 37.775],
ny = [-74.0064, 40.7142];
var scale,
translate,
visibleArea, // minimum area threshold for points inside viewport
invisibleArea; // minimum area threshold for points outside viewport
var zoom = d3.behavior.zoom()
.size([width, height])
.on("zoom", zoomed);
var projection = d3.geo.mercator()
.translate([width/2, height/2])
.scale(500);
var canvas = d3.select("body").append("canvas")
.attr("width", width)
.attr("height", height);
var svg = d3.select("body")
.append("svg")
.attr("width", width)
.attr("height", height);
var context = canvas.node().getContext("2d");
var path = d3.geo.path()
.projection(simplify)
.context(context);
stateMap = d3.json("http://linepointpath.cartodb.com/api/v2/sql?format=GeoJSON&q=SELECT * FROM GRAPHstates", function(error, stateMap) {
console.log(stateMap)
canvas
svg.selectAll("path")
.data(stateMap.feature)
.enter().append("path")
.attr("class", "state")
.attr("d", path)
.call(zoomTo(sf, 4).event)
.transition()
.duration(60 * 1000 / 89 * 2)
.each(jump);
});
var simplify = d3.geo.transform({
point: function(x, y, z) {
if (z < visibleArea) return;
x = x * scale + translate[0];
y = y * scale + translate[1];
if (x >= -10 && x <= width + 10 && y >= -10 && y <= height + 10 || z >= invisibleArea) this.stream.point(x, y);
}
});
function zoomTo(location, scale) {
var point = projection(location);
return zoom
.translate([width / 2 - point[0] * scale, height / 2 - point[1] * scale])
.scale(scale);
}
function zoomed(d) {
translate = zoom.translate();
scale = zoom.scale();
visibleArea = 1 / scale / scale;
invisibleArea = 200 * visibleArea;
context.clearRect(0, 0, width, height);
context.beginPath();
path(d);
context.stroke();
}
function jump() {
var t = d3.select(this);
(function repeat() {
t = t.transition()
.call(zoomTo(ny, 6).event)
.transition()
.call(zoomTo(sf, 4).event)
.each("end", repeat);
})();
}
</script>
</body>
</html>
I suspect the larger issue is that you're using a GeoJSON when the example you're going by is using a TopoJSON. The differences between the two are likely causing problems with how the paths are being rendered.
Another problem you're running into is that with a canvas, the function calls are different. You're using the normal svg function calls to append an SVG, bind the data, and they style it. With canvas, you interact with the elements through the canvas context object. This has a different syntax and usage than the standard SVG object you're trying to use in your code. If you follow this tutorial you'll notice she doesn't call any of the SVG functions but instead uses the context API to draw and style the canvas elements. Be sure to look at the working example of the code.
In your case, this means your code here:
svg.selectAll("path")
.data(stateMap.feature)
.enter().append("path")
.attr("class", "state")
.attr("d", path)
.call(zoomTo(sf, 4).event)
.transition()
.duration(60 * 1000 / 89 * 2)
Is unnecessary and not doing anything for you. The code that generates the path is embedded in the zoomed() function using the context object:
context.clearRect(0, 0, width, height);
context.beginPath();
path(d);
context.stroke();
It's using the context functions to create the objects you're trying to show. There are a chain of function calls that generate this behavior and you'll need to break down the chain to make sure you're getting what you want at each step.
If you want to use the GeoJSON start with just getting the map to display and then applying the zoom functionality. It'll probably make your life a lot easier in the end to iteratively build the visualization you want.
For more information on the difference between canvas and svg with D3, including examples of doing the same operation with each, checkout this blogpost and good luck with the project.
The squares in the example below are part of an SVG group that has an initial translate and scale set.
Clicking on a square initiates a zoom transition. But the intial values set by the transition are different from my defaults, as made obvious by the jarring start to this transition.
How can I set initial values for translate and scale on a zoom transition that I initiate programatically?
var svg = d3.select("#main");
svg.append("rect").attr({"x":0,"y":0,"height":100,"width":100,"fill":"red"})
svg.append("rect").attr({"x":100,"y":100,"height":100,"width":100,"fill":"blue"})
svg.append("rect").attr({"x":0,"y":100,"height":100,"width":100,"fill":"green"})
svg.append("rect").attr({"x":100,"y":0,"height":100,"width":100,"fill":"yellow"})
var zoom = d3.behavior.zoom().on("zoom",function(){
var t = d3.event.translate;
var s = d3.event.scale;
console.log(s)
svg.attr("transform","translate("+t[0]+","+t[1]+") scale("+s+")")
}).scaleExtent([1,10]).scale(1).translate([0,0])
d3.select("svg").call(zoom)
d3.selectAll("rect").on("mousedown",function(){
var scale = Math.random()*3;
var translate = [Math.random()*200,Math.random()*200]
zoom.scale(scale);
zoom.translate(translate);
//new transition
var T = svg.transition().duration(5000)
zoom.event(T);
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
<p style="font-weight:bold">When a zoom is triggered programatically, how do you set inital values for translate and scale?</p>
<p>Click on one of the squares</p>
<svg height="600px" width="600px">
<g id="main" transform="translate(25,25) scale(0.25)"></g>
</svg>
That is a problem with the zoom function itself. I would suggest zooming the children as opposed to the parent if that would work
var zoom = d3.behavior.zoom().on("zoom",function(){
var t = d3.event.translate;
var s = d3.event.scale;
svg.selectAll("rect").attr("transform","translate("+t[0]+","+t[1]+") scale("+s+")")
}).scaleExtent([1,10]);
EDIT
The problem with the above code is that d3.js does not register the transformation or initial state of the SVG. This problem runs deeper. As d3 does not keep track of the SVG transformations and just executes them. It only keeps track of transformations you've run on the library in a variable called __chart__.
So when the zoom function is run it just interpolates the variables and gives the output. As no functions have been run on this yet the __chart__ variable has not been set and causing the jerky start from (x=0, y=0, k=1).
Solution:
Run this code before the zoom transformation to set the initial chart manually
svg.transition().each(function(){
this.__chart__={x:25,y:25,k:0.25}; //or you can pick those values using attr
});
Zoom the svg programmatically to 25,25,0.25 first before any other function. (this is why your workaround works as the __chart__ variable gets set)
To set the initial value of the zoom, try something like this:
// Init zoom
var zoom = d3.behavior.zoom().on("zoom", function () {
svg.attr("transform", "translate(" + d3.event.translate + ")" + " scale(" + d3.event.scale + ")");
});
// Get SVG element
var svg = d3.select("svg")
.call(zoom)
.append("g");
// Create circle
svg.append("circle")
.attr("cx",0)
.attr("cy",0)
.attr("r", 5);
// Create init value
var scale = 5;
var translate = [50, 50];
// Set init value
zoom.scale(scale);
zoom.translate(translate);
// Call zoom event
svg.call(zoom.event);
// or svg.transition().call(zoom.event);
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
<svg height="100px" width="100px"></svg>
I was looking for the answer to this, but it seems D3 has already evolved a couple of versions.
Although Majkl and cjds's answers helped me solve my problem, I thought it would help to leave more up to date information, since it was hard finding v5.4 examples out there, until I found Observable at least.
// Applies event transformation to the Group element's attribute
const zoom_action = () => g.attr("transform", d3.event.transform)
// Create the zoom handler
const zoom = d3
.zoom()
.on("zoom", zoom_action)
// Get SVG element and apply zoom behaviour
var svg = d3
.select("svg")
.call(zoom)
// Create Group that will be zoomed
var g = svg.append("g")
// Create circle
g.append("circle")
.attr("cx",0)
.attr("cy",0)
.attr("r", 5)
// Set initial scale and translation
zoom.scaleBy(svg, 5)
zoom.translateBy(svg, 50, 50)
<script src="https://d3js.org/d3.v5.js"></script>
<svg height="100px" width="100px"></svg>
Apologies if this is a simple case of me being blind to the obvious, but I am trying to put together a page that shows a map of the world (data sourced from a TopoJSON file) in Mercator projection centered on the Pacific. I.e. Europe on the left, America on the right and Australia in the middle. A bit like this...
From this point I want to be able to zoom and pan the map to my hearts desire, but when I pan east or west, I want the map to scroll 'around' and not come to the end of the World (I hope that makes sense).
The code I am currently working on is here (or at the following Gist (https://gist.github.com/d3noob/4966228) or block (http://bl.ocks.org/d3noob/4966228));
<!DOCTYPE html>
<meta charset="utf-8">
<style>
body {font-size:11px;}
path {
stroke: black;
stroke-width: 0.25px;
}
</style>
<body>
<script src="http://d3js.org/d3.v3.min.js"></script>
<script src="http://d3js.org/topojson.v0.min.js"></script>
<script>
var width = 960,
velocity = .005,
then = Date.now()
height = 475;
var projection = d3.geo.mercator()
.center([0, 0 ])
.scale(1000);
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
var path = d3.geo.path()
.projection(projection);
var g = svg.append("g");
d3.json("world-110m.json", function(error, topology) {
g.selectAll("path")
.data(topojson.object(topology, topology.objects.countries).geometries)
.enter()
.append("path")
.attr("d", path)
.style("fill","black")
d3.timer(function() {
var angle = velocity * (Date.now() - then);
projection.rotate([angle,0,0]);
svg.selectAll("path")
.attr("d", path.projection(projection));
});
var zoom = d3.behavior.zoom()
.on("zoom",function() {
g.attr("transform","translate("+d3.event.translate.join(",")+")scale("+d3.event.scale+")")
});
svg.call(zoom)
});
</script>
</body>
</html>
The code is an amalgam of examples and as a result I can see a map that can rotate west to east automatically, and I can pan and zoom using the mouse, but when panning and zooming, from what I can tell, I am affecting the internal "g" element and not the map within the "svg" element.
There are plenty of good examples of being able to pan and zoom a map centered on the meridian. But none on the anti-meridian that I have discovered.
Any help would be greatly appreciated.
I ended up working on the same problem. Here's an example (see code) where you pan left/right to rotate the projection (with wraparound), and up/down to translate (clamped by max absolute latitude), with zoom as well. Ensures that projection always fits within viewbox.
I learned a lot about zoom behavior, and projection center() and rotate() interaction.
hope this code can solve your problem
var projection = d3.geo.equirectangular()
.center([0, 5])
.scale(90)
.translate([width / 2, height / 2])
.rotate([0, 0])
.precision(9);
Google maps on apple products work like this. Scrol left, and you will leave one Australia, then find another and another and another