How to set initial position and scale when using d3.zoom() - d3.js

I have the following code working;
const svg = d3
.select("svg")
.call(
d3
.zoom()
.wheelDelta(this.delta)
.on("zoom", function() {
svg.attr("transform", d3.event.transform);
})
)
.append("g")
.attr("id", "tree-container");
I later add a d3 tree layout insode the g.tree-container. The initial tree node is drawn in top left. Ideally I would like the initial node somewhere near the center of the svg
I have look here;
D3.js Set initial zoom level
but can't get it to work with my code and I don't really understand what is going on.
(i believe i am using d3 version 5.7)

Related

D3 Transition Path Left And Shift Up/Down Simultaneously

Following the tutorial of Mike Bostock on Path transitions here, I am trying to create an interpolated line chart that not only shifts through time but also transitions the y-scale / y-axis, such that is always fits to the lower and upper bounds of the data.
Some background information: The line is clipped by a clipPath and is shifted to the left whenever a new data point is added. Each new data point is added by the tick function, which also transitions the path to slide to the left.
Now the problem is, when I update the y-axis domain, it jumps to the new position. However, I would like it to smoothly transition up or down, similar to how it shifts along the x-axis. The solution probably lies in transforming the scaling of the path based on the new maximum of the data. Is there any way to achieve this or would it require a different approach by building a custom d3.interpolator() for interpolating the path?
function tick() {
// Push a new data point onto the back.
data.push(random());
// Redraw the line.
d3.select(this)
.attr("d", line)
.attr("transform", null);
// Slide it to the left.
d3.active(this)
.attr("transform", "translate(" + x(0) + ",0)")
.transition()
.on("start", tick);
// Pop the old data point off the front.
data.shift();
let max = d3.max(data, (d) => {
return d;
});
y = d3.scaleLinear()
.domain([-1, max])
.range([height, 0]);
d3.select('g .axis.axis--y').transition().duration(500).call(d3.axisLeft(y))
d3.select('g .axis.axis--x').transition().duration(500)
.attr("transform", "translate(0," + y(0) + ")")
}
I have created a jsfiddle which demonstrates the problem here.
Found a solution by using the external d3-interpolate-path library from here. Instead of transitioning the path using a transform, I interpolate the path with the old data and the path with the new data using d3.interpolatePath. Where previous is the old path and current is the new path with the newly added data point.
d3.select(this)
.attr("d", lineOld)
.attr("transform", null)
.transition().duration(500).ease(d3.easeLinear).attrTween('d', (d) => {
let previous = d3.select(this).attr('d');
let current = line(d);
return d3.interpolatePath(previous, current)
}).on("end", tick);
The Jsfiddle with my solution can be found here

d3 v6 pointer function not adjusting for scale and translate

I am upgrading my app from d3 v5 to v6 and am having an issue migrating the d3.mouse functionality. In my app I apply a transform to the top level svg group and use the zoom functionality to zoom and pan (scale and translate). When I double click on the screen I take the mouse position and draw a square.
Now I am replacing the d3.mouse function with d3.pointer. In my double click event I get the mouse position by calling d3.pointer(event). However this function is not producing a position that is relative to where my top level svg group is positioned and scaled. When I remove the translate and scale from the top level group, the position matches up.
In the older version of d3 I could call d3.mouse(this.state.svg.node()) and it would produce the exact position I clicked corrected for pan and scale. Is this available in version 6? If not, is there a clean way I can adjust for this? The new event object is coming through with a host of different position properties: pagex, offsetx, screenx, x. None of these is producing the position I clicked on. Is there a clean way to acheive this?
You could specify a container element which would factor in a zoom transform in v5 and earlier:
d3.mouse(container)
Returns the x and y coordinates of the current event relative to the specified container. The container may be an HTML or SVG container element, such as a G element or an SVG element. The coordinates are returned as a two-element array of numbers [x, y]. (source)
In d3v6 you can specify this by using the second parameter of d3.pointer:
d3.pointer(event[, target])
Returns a two-element array of numbers [x, y] representing the coordinates of the specified event relative to the specified target. event can be a MouseEvent, a PointerEvent, a Touch, or a custom event holding a UIEvent as event.sourceEvent.
...
If the target is an SVG element, the event’s coordinates are transformed using the inverse of the screen coordinate transformation matrix. If the target is an HTML element, the event’s coordinates are translated relative to the top-left corner of the target’s bounding client rectangle. (source)
So as far as I'm aware, you should be use:
d3.pointer(event,this.state.svg.node());
Instead of
d3.mouse(this.state.svg.node());
Here's a d3v6 example:
var svg = d3.select("body")
.append("svg")
.attr("width", 500)
.attr("height", 200);
var rect = svg.append("rect")
.attr("width",500)
.attr("height",200)
.attr("fill", "#eee")
var g = svg.append("g");
var zoomed = function(event) {
g.attr("transform", event.transform);
}
rect.call(d3.zoom().on("zoom",zoomed))
.on("click", function(event) {
var xy = d3.pointer(event,g.node());
g.append("circle")
.attr("r", 5)
.attr("cx", xy[0])
.attr("cy", xy[1])
.attr("fill","crimson");
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/6.0.0/d3.min.js"></script>
Adapting this v5 example:
var svg = d3.select("body")
.append("svg")
.attr("width", 500)
.attr("height", 200);
var rect = svg.append("rect")
.attr("width",500)
.attr("height",200)
.attr("fill", "#eee")
var g = svg.append("g");
var zoomed = function() {
g.attr("transform", d3.event.transform);
}
rect.call(d3.zoom().on("zoom",zoomed))
.on("click", function() {
var xy = d3.mouse(g.node());
g.append("circle")
.attr("r", 5)
.attr("cx", xy[0])
.attr("cy", xy[1])
.attr("fill","crimson");
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>

Map zoom and pan in d3 js v4 + scale limit

I've got this d3 js map. I've tried to make such a simple thing as zoom
and pan and just stalled. Now only dots zooms (I use v4). How to 'synchronize' zoom and pan of dots and map svg?
How to set limits of zoom and pan in d3 v4? I want it to be like this
var svg = d3.select("body")
.append("svg")
.attr("width", width)
.attr("height", height)
.style("border","none")
.style("background-color", "none")
.call(d3.zoom().on("zoom", function () {
svg.attr("transform", d3.event.transform)
}))
.append("g");
The circles scale but the paths do not because of how you append them. First, let's see how you apply the zoom:
var svg = d3.select("body")
.append("svg")
...
.call(d3.zoom().on("zoom", function () {
svg.attr("transform", d3.event.transform)
}))
.append("g"); // returns a selection of a newly appended g element
So the selection svg is actually a g element. While zoom is called on the svg element (not svg selection) it modifies the svg selection (which is actually holds a g): svg.attr(("transform"....
When you append your paths you use var map = d3.select("svg").insert(... and create a new g to hold the paths. But - this g is not in or a child of the selection svg - so it is not updated: d3.select("svg") != svg in this case. Instead, use:
var map = svg.insert(... // we insert into the svg selection which holds a g
This way we are inserting elements into the parent g that is updated each zoom.
While this really is a second question, the solution is simple enough. A d3.zoom() behavior can be constrained by both scale and translate:
d3.zoom().scaleExtent([1,4]) // limit scale to full size or 4x size.
.translateExtent([[0,0],[width,height]]) // limit pan to original SVG dimensions
zoom.translateExtent([p1,p2]) takes two points, upper left and lower right. We can constrain based on these dimensions if your features don't extend past the SVG bounds when initially loaded with a scale of 1.
Here's an updated bin.

How to get the result of zoom.translate() without altering the sensibility of panning?

I have a D3.js draw in a svg and I want to be able to pan and zoom on it. I also want a rectangle to be drawn around the initial window. I want the rectangle to become smaller as I zoom out, larger as I zoom in and to move normally as I pan around. Usual stuff.
I also want to be able to redraw the rectangle to make it fit again the actual window (so it will be smaller than the first if I zoomed in between the redraw for example). I didn't included this part in the code example but it explains why I need a way to get the zoom properties.
To properly trace the rectangle I found the zoom.translate() and zoom.scale() property, supposed to give me the parameters I need to calculate the coordinates of the rectangle's parts. However since I added this part to my code the panning sensibility became to shift as I zoom in and out: The more I zoom in, the less sensible is the panning, and the more I zoom out the more sensible it becomes.
In my mind, zoom.translate() and zoom.scale() were only supposed to fetch the parameters, not to change the way zooming and panning work, how can I fix that?
I also have inexplicably a rectangle that doesn't fit the window: it is a bit larger and shorter.
Here my piece of code:
var svg;
var map;
var zoom;
var graph = d3.select("#graph");
zoom = d3.behavior.zoom()
.translate([0, 0])
.scale(1)
.on("zoom", function () {
svg.attr("transform", "translate(" + d3.event.translate + ")" + " scale(" + d3.event.scale + ")");
});
svg = graph.append("svg")
.attr("width",window.innerWidth)
.attr("height",window.innerHeight)
.call(zoom);
map = svg.append("g");
Here is the acquisition of the rectangle's coordinates:
var w_b = {
x_min: (0 - zoom.translate()[0])/zoom.scale(),
x_max: (svg.attr("width") - zoom.translate()[0])/zoom.scale(),
y_min: (0 - zoom.translate()[1])/zoom.scale(),
y_max: (svg.attr("height") - zoom.translate()[1])/zoom.scale()
And here is the drawing of the rectangle:
map.selectAll("line").remove();
map.append("line").attr("x1", w_b['x_min']).attr("x2", w_b['x_max']).attr("y1", w_b['y_min']).attr("y2", w_b['y_min'])
.attr("stroke-width", 1).attr("stroke", "black");
map.append("line").attr("x1", w_b['x_min']).attr("x2", w_b['x_max']).attr("y1", w_b['y_max']).attr("y2", w_b['y_max'])
.attr("stroke-width", 1).attr("stroke", "black");
map.append("line").attr("x1", w_b['x_min']).attr("x2", w_b['x_min']).attr("y1", w_b['y_min']).attr("y2", w_b['y_max'])
.attr("stroke-width", 1).attr("stroke", "black");
map.append("line").attr("x1", w_b['x_max']).attr("x2", w_b['x_max']).attr("y1", w_b['y_min']).attr("y2", w_b['y_max'])
.attr("stroke-width", 1).attr("stroke", "black");
As a bonus question, this doesn't work at all on Chrome, any idea about what it could be ?
To work correctly, the zoom must be defined like this:
zoom = d3.behavior.zoom()
.translate([0, 0])
.scale(1)
.on("zoom", function () {
map.attr("transform", "translate(" + d3.event.translate + ")" + " scale(" + d3.event.scale + ")");
});
Notice the "map.attr" instead of "svg.attr".
However it doesn't solve the issues about the size of the window and the fact it's not working on Chrome.

D3JS scaling and transition performance

I have some code to scale and translate a map in D3 but the performance is quite terrible. When zooming and panning, it's taking nearly 3 seconds for a refresh. I thought the map would look nicer including line boundaries for all the counties, but at 6MB+ I suspect this may be where the bottleneck is coming from. Is there another way I should be handling the transforms or maybe a way to optimize the map data? Is D3 really not suited to this level of detail? Very new to D3.
I'm using shape files from here, converted from DBF to Geojson using QGIS:
https://www.census.gov/cgi-bin/geo/shapefiles2010/main
<!doctype html>
<html>
<head>
<title>d3 map</title>
<script src="http://d3js.org/d3.v3.min.js">
</script>
</head>
<body>
<script>
var width = 800;
var height = 600;
var projection = d3.geo.mercator();
var path = d3.geo.path().projection (projection);
var canvas = d3.select ("body")
.append ("svg")
.attr ("width", width)
.attr ("height", height)
var zoomVar = d3.behavior.zoom()
.translate(projection.translate())
.scale(projection.scale())
.scaleExtent([height, 60 * height])
.on("zoom", onPostZoom);
var hotbox = canvas.append("g").call(zoomVar);
hotbox.append("rect")
.attr("class", "background")
.attr("width", width)
.attr("fill", "white")
.attr("height", height);
d3.json ("cali.geojson", function (data)
{
hotbox.append("g")
.attr("id", "geometry")
.selectAll("path")
.data(data.features)
.enter()
.append("path")
.attr("d", path)
.attr("fill", "steelblue")
.on("click", onClick);
})
function onClick (d)
{
var centroid = path.centroid(d), translate = projection.translate();
projection.translate(
[translate[0] - centroid[0] + width / 2,
translate[1] - centroid[1] + height / 2 ]);
zoomVar.translate(projection.translate());
hotbox.selectAll("path").transition()
.duration(700)
.attr("d", path);
}
function onPostZoom()
{
projection.translate(d3.event.translate).scale(d3.event.scale);
hotbox.selectAll("path").attr("d", path);
}
</script>
</body>
</html>
As Lars said, you should definitely simplify your data to the appropriate resolution. Choose the maximum resolution based on how far you want to zoom in. I recommend topojson -s to simplify, as you’ll also get the benefits of the smaller TopoJSON format.
The other big thing is to avoid reprojection if you’re just panning and zooming. Reprojection is a comparatively expensive trigonometric operation, and so is serializing very large path strings in SVG. You can avoid this for panning and zooming (translating and scaling) by simply setting the transform attribute on the path elements or a containing G element. See these examples:
Zoom to Bounding Box
Raster & Vector Zoom
You should also consider using projected TopoJSON (alternate example) which bakes the projection into the TopoJSON file. This makes the client is even faster: it never has to project!
The problem you're experiencing isn't really because of D3, but because of the browser. The main bottleneck is rendering all the visual elements, not computing their positions etc.
The only way to avoid this is to have less data. One way to start would be to simplify the boundaries in QGIS, using e.g. the dpsimplify plugin.

Resources