Related
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 have a simple visual of many rects, over 100 I'd say. For aesthetic purposes I want to create a high light effect on mouse click. I also wanted to make this effect somewhat intuitive by removing that effect once the user clicks on a new rect. However I couldn't get this to work without resorting to a d3.selectAll() call, so I'm thinking this approach might not be ideal if this project gets any bigger. Here is the code:
.on('click.highlight', function() {
//set any previously highlighted rects back to normal color/brightness
d3.selectAll('.highlight').transition().duration(250)
.style('fill', function(d) { return d3.rgb(d.color)})
d3.select(this).classed('highlight',true);
//now it's safe to assign the current highlighted rect a brighter hue... i think
d3.select(this).transition().duration(250)
.style('fill', function(d) { return d3.rgb(d.color).brighter(.5)})
})
Though this code does what I wanted it to do, but presumably there could only ever be 1 other highlight rect to worry about at any give time. So again, I'm not sure that using d3.selectAll() is warranted here.
So anyway, is there a more efficient way? I'd like to keep it all within one .on('click') function if possible.
If you are looking to avoid use of .selectAll, you could create a selection of one rect that contains the last clicked rectangle. Each time you click on a rectangle:
unhighlight the previously selected highlighted rect
update that selection to reflect the most recently clicked rectangle
highlight the newly selected rect
I use the variable highlightedRect to hold the selection that will allow the above workflow:
var svg = d3.select("body").append("svg")
.attr("width",600)
.attr("height",400);
var highlightedRect = d3.select(null);
var rects = svg.selectAll("rect")
.data(d3.range(1600))
.enter()
.append("rect")
.attr("y",function(d) { return Math.floor(d/50)*12; })
.attr("x",function(d) { return d%50 * 12 })
.attr("width",11)
.attr("height",11)
.attr("stroke","white")
.on("click",function(d) {
// Recolor the last clicked rect.
highlightedRect.attr("fill","black");
// Color the new one:
highlightedRect = d3.select(this);
highlightedRect.attr("fill","steelblue");
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.10.0/d3.min.js"></script>
Please note - there is a solution for part of this problem by Gordon Woodhull in Disable brush on range chart before selecting a scale from the dropdown/on page load(dc.js,d3.js)
In addition,there is a partial solution at the end this question.
Furthermore there are two fiddles:
1) https://jsfiddle.net/dani2011/4dsjjdan/1/
2) https://jsfiddle.net/dani2011/uzg48yk7/1/ (with partial solution)
Need to disable resize of the brush on range chart (timeSlider) by dragging the line within the focus charts (bitChart,bitChart2). As Gordon Woodhull suggested (Disable brush resize (DC.js, D3.js) try to enable only pan without zoom.
Current behavior:
1)
Dragging the line on bitChart2 (focus chart) pans the brush until the borders of the timeChart. Once reaching the borders,the brush shrinks. The other focus chart (bitChart) resizes the brush of the range chart during drag of its line.
2)
When selecting a span for the brush from the dropdown only the .on('zoomed', function (chart, filter) { of bitChart2 is loaded and not the .on("zoomed"... of bitChart.
Print screens from the console:
a) Selecting scale from the dropdown
b) Dragging line on bitChart:
c) Dragging line on bitChart2:
3)
For both bitChart and bitChart2 the value of scale is 1 and the position
is 0,0:
.on('zoomed', function (chart, filter) {
//var zoom = d3.behavior.zoom()
// .translate([0, 0])
//.scale(1).scaleExtent([1, 1])
var zoom = d3.behavior.zoom()
var scale = zoom.scale(); var position = zoom.translate();
js file
The following implementations did not solve the issue:
**option 1**
bitChart.on('zoomed', function (chart, filter) {
d3.behavior.zoom().on("zoom", null);//doesn't stop zoom
//event needs svg element(tried different options),doesn't work
d3.behavior.zoom().scale(1).scaleExtent([1,1]).translate([0,0]).event(chart.select('g.stack_0')))
**option 2**
//Applied on timeslider,bitChart,bitChart2 to eliminate zoom and
//maintain pan
.zoomScale([1, 1])//dc.js
//And also
.on('zoomed', function (chart, filter) {
bitChart.zoomScale([1, 1]);
//Nothing pans with
chart.focus(chart.xOriginalDomain())
**option 3**
bitChart.on('zoomed', function (chart, filter) {
var svg = d3.select("body")
.append("svg")
.call(d3.behavior.zoom().on("zoom", function () {
svg.attr("transform", "translate(" + d3.event.translate + ")" +"
scale(" + 1 + ")")
}))
//.append("g")
**option 4**
.on('zoomed', function (chart, filter) {
var chartBody = chart.select('g.stack _0');
var path = chartBody.selectAll('path.line').data([extra_data]);
path.enter().append('path').attr({
class: 'line',
});
path.attr('transform', 'translate(0,50)');
**option 5**
bitChart.on('zoomed', function (chart, filter) {
var zoom = d3.behavior.zoom()
.scaleExtent([1, 1])
chart.select('g.stack _0').call(zoom);
zoom.scale(1);
zoom.event(chart.select('g.stack _0'));
**option 6**
bitChart.on('zoomed', function (chart, filter) {
svg.call(d3.behavior.zoom().scale(1));
**option 7**
var min_zoom = 1;
var max_zoom = 1;
var svg = d3.select("body").append("svg");
var zoom = d3.behavior.zoom().scaleExtent([min_zoom, max_zoom])
bitChart.on('zoomed', function (chart, filter) {
svg.call(zoom);
My fiddle:
https://jsfiddle.net/dani2011/4dsjjdan/1/ was forked from https://jsfiddle.net/gordonwoodhull/272xrsat/9/.
When selecting span from the dropdown and clicking on the range chart,The range chart (timeSlider) acts strange on the fiddle, but behaves as expected when run it in my environment. Please note in this fiddle that bitChart2 pans the brush as expected.The resize of the brush when reaching the edge happens in my enviroment. bitChart still resizes the brush.
A partial solution:
To enable multi focus charts on a single range chart as in https://github.com/dc-js/dc.js/blob/master/web/examples/multi-focus.html written by Gordon Woodhull.Used the focus chart which worked properly in my code (bitChart2) as the main reference chart:
bitChart2.focusCharts = function (chartlist) {
if (!arguments.length) {
return this._focusCharts;
}
this._focusCharts = chartlist; // only needed to support the getter above
this.on('filtered', function (range_chart) {
if (!range_chart.filter()) {
dc.events.trigger(function () {
chartlist.forEach(function(focus_chart) {
focus_chart.x().domain(focus_chart.xOriginalDomain());
});
});
} else chartlist.forEach(function(focus_chart) {
if (!rangesEqual(range_chart.filter(), focus_chart.filter())) {
dc.events.trigger(function () {
focus_chart.focus(range_chart.filter());
});
}
});
});
return this;
};
bitChart2.focusCharts([bitChart]);
My second fiddle:
https://jsfiddle.net/dani2011/uzg48yk7/1/ was forked from https://jsfiddle.net/gordonwoodhull/272xrsat/9/.
1) When clicking on the range chart in the fiddle it does not function properly, but works in my environment.
2) The brush does not resize at the edges of the range chart in the fiddle as it does in my environment
3) It does show in the fiddle that the whole range chart is selected when panning/clicking on the lines in the focus charts and when clicking in the range chart
4) It does show in the fiddle that after selecting the brush span from the dropdown, panning the lines in the focus charts moves the brush properly on the range chart.
5) It does show in the fiddle that dragging the brush on the range chart is possible again in no span is selected from the dropdown
Still needs to solve:
1) When reaching the ends of the range chart (timeSlider) the brush resizes
solved by updating versions to be the same as the version of the external resources in the fiddle https://jsfiddle.net/gordonwoodhull/272xrsat/9/. Thank you Gordon!
2) Before selecting a scale from the dropdown:
a) When panning /translating the line of the focus charts(bitChart,bitChart2) the brush resizes
b) It is possible again to drag the brush on the range chart
Any help would be appreciated !
Unfortunately, by 'all of a sudden', I mean between versions 3.1.1.10 and 3.2.0. I have a scatterplot with about 3000 points in it, and we are trying to make a mouse click based tooltip work since tooltips based on mouseover events don't really work for us.
So, while testing our initial attempt, we saw that it took a full 2 or 3 seconds before anything happened. I thought that was a little odd. I mean sure, we're doing a nearest-neighbor calculation, but it's only 3000 points, so what gives? I profiled it and found that once we got the event trigger, our calculations were only taking about 22 msec, so that's good. So what is causing d3 to take so much time in getting us active on mouse clicks? I started dropping in different versions of d3 to see if it was the culprit and sure enough, things were slow until I got back to version 3.1.0. That one have us the mouse event pretty much immediately and our tooltip popups were now working well.
Only problem is I can't use 3.1.10 because I need the zoom().center() functionality that was added more recently.
So any ideas? Anybody know why the slowdown occurred there? Can it be patched? Can someone suggest a clever workaround? I can't be the only one who has seen this problem.
I brought it up in the d3 forum, topic number qfz11UYgMC8
I understand why I cannot post codepen demos here without code, but the 2 demos I have demonstrate my point above by differing only in what version of d3 is included in them. I will include the code here if people really want to see it, but it's pretty much standard d3.js where I create my scatterplot from a json object, and it's thousands of lines of code.
And the problem is definitely based on the size of the scatterplot. A small plot of a dozen or so points pops up the tooltip right away, and the one with 3000 points is slow (on all d3.js libraries 3.2 and later).
Edit: adding code snippets, and codepen links
The code itself is thousands of lines long because the data itself gets embedded in the html. Here is what I hope is the relevant code...
<html>
<head>
<title>Rolling D3 speed test</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.2.0/d3.min.js"></script>
<!-- <script src="file://C:/d3.v3.5.16.min.js"></script> -->
<script type="text/javascript" charset="utf8" src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.3/jquery.min.js"></script>
<script type="text/javascript" language="javascript" class="init">
// variable init left out for brevity
...
var svg = d3.select("body").append("svg")
.attr("width",viewportWidth)
.attr("height",viewportHeight)
.call(zoom)
.append("g")
.attr("class", "main")
.attr("transform","translate("+(left_shift)+","+(top_shift)+")");
...
//mouse click on svg (to display tooltip on nearest point)
d3.select('svg').on("click", function() {
var startTime = new Date();
var coords = [0, 0];
coords = d3.mouse(this);
var isClicked = isPointClicked(point_clicked);
if(!isClicked) clicked(coords[0], coords[1]);
point_clicked = false;
var finalEndTime = new Date();
var finalTimeDiff = finalEndTime - startTime;
console.log('all click actions completed in '+finalTimeDiff+' milliseconds.');
});
//mouse-right click event to remove tooltip
d3.select('body').on("contextmenu", function() {
d3.event.preventDefault();
var ele = d3.select('div.tooltip');
//if(ele!=="undefined" && ele!==null)
ele.style("opacity",0);
});
...
// x and y axis rendering removed for brevity
...
//draw all data
//var radius = 2.5; // 2.5 normally, 8 is good for testing small sets
var side = 2; // takes place of radius
// shiftrect needs to be half the square size to center square on data point
var shiftrect = 1;
for(var k=0;k<plot_data.series.length;++k) {
if(plot_data.series[k].visible === true) {
var y_list = plot_data.series[k].ydata;
var x_list = plot_data.series[k].xdata;
svg.selectAll("dot")
.data(x_list)
.enter().append("rect")
.attr("id", function(d, i) { return "rect"+k+i;})
.attr("x", function(d) {return x(parseFloat(d))-shiftrect;})
.attr("y", function(d,i) {return y(parseFloat(y_list[i]))-shiftrect;})
.attr("height", side)
.attr("width", side)
.attr("fill",function() { return color(k);})
.attr("clip-path", "url(#plot-clip)");
}
}
Codepen that is slow
http://codepen.io/pkrouse/details/ONwQmz/
Codepen that is fast. Only diff is version of D3 used
http://codepen.io/pkrouse/details/JXBpKo/
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>