Related
I am trying to find the nearest neighbors in a scatterplot using the data attached below, with the help of this snippet -
const voronoiDiagram = d3.voronoi()
.x(d => d.x)
.y(d => d.y)(data);
data.forEach(function(d){
console.log(d, voronoiDiagram.find(d.x, d.y, 50));
});
Now the dataset I am using is the standard iris sepal, petal lengths data in
format -
{"sepalLength":7.7,"sepalWidth":3,"petalLength":"6.1","petalWidth":"2.3","species":"virginica","index":135,"x":374.99999999999994,"y":33.75,"vy":0,"vx":0},
{"sepalLength":6.3,"sepalWidth":3.4,"petalLength":"5.6","petalWidth":"2.4","species":"virginica","index":136,"x":524.9999999999999,"y":191.25,"vy":0,"vx":0},
{"sepalLength":6.4,"sepalWidth":3.1,"petalLength":"5.5","petalWidth":"1.8","species":"virginica","index":137,"x":412.5,"y":179.99999999999994,"vy":0,"vx":0},
{"sepalLength":6,"sepalWidth":3,"petalLength":"4.8","petalWidth":"1.8","species":"virginica","index":138,"x":374.99999999999994,"y":225,"vy":0,"vx":0},
....
So, essentially it is in the form of
{d: {x, y, sepal length, width, petal length, width}.
Now, I am trying to find the nearest neighbors with d3 voronoi from reference.
But, all I get is this in results -
Let point d in my dataset =
{"sepalLength":5.9,"sepalWidth":3,"petalLength":"5.1","petalWidth":"1.8","species":"virginica","index":149,"x":374.99999999999994,"y":236.24999999999997,"vy":0,"vx":0}
Now, the voronoiDiagram.find(d.x, d.y, 50) for this is resulting in -
"[375,236.25]"
That is, the same point with coordinates rounded off instead of another point.
So, how do I exclude current point being scanned in this case from the voronoi diagram.
Also, If I exclude that point & re-calculate everything would this be good from the performance perspective ?
Can anyone help me with finding nearest neighbors from a set of points
with d3 voronoi / quadtrees (I have tried a couple of examples already from Mike Bostock but couldn't get them to work in my case because of some errors,
so will post them if d3 voronoi does not help).
voronoiDiagram.find(y, x, r) will only ever return, at most, once cell. From the API documentation:
Returns the nearest site to point [x, y]. If radius is specified, only sites within radius distance are considered. (link)
I've previously read that as being plural, apparently I've never looked closely (and I think there is a large amount of utility in being able to find all points within a given radius).
What we can do instead is create a function fairly easily that will:
start with voronoiDiagram.find() to find the cell the point falls in
find the neighbors of the found cell
for each neighbor, see if its point is within the specified radius
if a neighbors point is within the specified radius:
add the neighbor to a list of cells with points within the specified radius,
use the neighbor to repeat steps 2 through 4
stop when no more neighbors have been found within the specified radius, (keep a list of already checked cells to ensure none are checked twice).
The snippet below uses the above process (in the function findAll(x,y,r)) to show points within the specified distance as orange, the closest point will be red (I've set the function to differentiate between the two).
var width = 500;
var height = 300;
var data = d3.range(200).map(function(d) {
var x = Math.random()*width;
var y = Math.random()*height;
var index = d;
return {x:x,y:y,index:index}
});
var svg = d3.select("body")
.append("svg")
.attr("width",width)
.attr("height",height);
var circles = svg.selectAll()
.data(data, function(d,i) { return d.index; });
circles = circles.enter()
.append("circle")
.attr("cx",function(d) { return d.x; })
.attr("cy",function(d) { return d.y; })
.attr("r",3)
.attr("fill","steelblue")
.merge(circles);
var voronoi = d3.voronoi()
.x(function(d) { return d.x; })
.y(function(d) { return d.y; })
.size([width,height])(data);
var results = findAll(width/2,height/2,30);
circles.data(results.nearest,function(d) { return d.index; })
.attr("fill","orange");
circles.data([results.center],function(d) { return d.index; })
.attr("fill","crimson");
var circle = svg.append("circle")
.attr("cx",width/2)
.attr("cy",height/2)
.attr("r",30)
.attr("fill","none")
.attr("stroke","black")
.attr("stroke-width",1);
circle.transition()
.attrTween("r", function() {
var node = this;
return function(t) {
var r = d3.interpolate(30,148)(t);
var results = findAll(width/2,height/2,r);
circles.data(results.nearest,function(d) { return d.index; })
.attr("fill","orange");
return r;
}
})
.duration(2000)
.delay(1000);
function findAll(x,y,r) {
var start = voronoi.find(x,y,r);
if(!start) return {center:[],nearest:[]} ; // no results.
var queue = [start];
var checked = [];
var results = [];
for(i = 0; i < queue.length; i++) {
checked.push(queue[i].index); // don't check cells twice
var edges = voronoi.cells[queue[i].index].halfedges;
// use edges to find neighbors
var neighbors = edges.map(function(e) {
if(voronoi.edges[e].left == queue[i]) return voronoi.edges[e].right;
else return voronoi.edges[e].left;
})
// for each neighbor, see if its point is within the radius:
neighbors.forEach(function(n) {
if (n && checked.indexOf(n.index) == -1) {
var dx = n[0] - x;
var dy = n[1] - y;
var d = Math.sqrt(dx*dx+dy*dy);
if(d>r) checked.push(n.index) // don't check cells twice
else {
queue.push(n); // add to queue
results.push(n); // add to results
}
}
})
}
// center: the point/cell that is closest/overlapping, and within the specified radius, of point x,y
// nearest: all other cells within the specified radius of point x,y
return {center:start,nearest:results};
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.10.0/d3.min.js"></script>
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've been keen on developing a choropleth map for Namibia.But found two interesting tools. leaflet and D3, though leaflet has clear instructions to implement which i did Its not so functionally in line with what i want to do. And that is where D3Geo came in. I've everything set but this function below to set my projection.
var projection = d3.geo.conicConformal()
.rotate([, 0])
.center([0, 0])
.parallels([ , ])
.scale(1000)
Is there just no function to just simply add the co-ordinates as how its done in the leaflet function below. for us who are not so geocentric.
var map = L.map('mapid').setView([-22.26,16.52], 5);
And if there isn't, can someone please guide me on how to convert the coordinates (-22.26,16.52 )to show Namibia using the d3.geo.conicConformal().
Correct me if it didn't address your issue (maybe you can provide a minimal example showing where you are stuck, using JSFiddle for example), but if I understand well you want to move/zoom/center the displayed image on the extend of your country. Here is an example doing this (I also added some code on how the layer was added for consistency):
// Define the projection you want to use,
// setting scale and translate to some starting values :
var projection = d3.geoConicConformal()
.translate([0, 0])
.scale(1)
var layer_name = "your_layer_name";
var geo_features = topojson.feature(topoObj, topoObj.objects[layer_name]).features;
// Define the path generator :
var path = d3.geoPath().projection(projection);
var width = 800,
height = 600;
// This is the main svg object on which you are drawing :
var map = d3.select("body").append("div")
.style("width", width + "px")
.style("height", height + "px")
.append("svg")
.attr("id", "svg_map")
.attr("width", width)
.attr("height", height);
// Add you layer to the map
map.append("g").attr("id", layer_name)
.attr("class", "layers")
.selectAll("path")
.data(geo_features)
.enter().append("path")
.attr("d", path)
.attr("id", (d,i)=> "feature_" + i)
.styles({"stroke": "rgb(0, 0, 0)", "fill": "beige")
// Where the job is done :
scale_to_layer(layer_name)
function scale_to_layer(name){
var bbox_layer = undefined;
// use all the paths of the layer (if there is many features)
// to compute the layer bbox :
map.select("#"+name).selectAll('path').each(function(d, i){
var bbox_path = path.bounds(d);
if(bbox_layer === undefined){
bbox_layer = bbox_path;
}
else {
bbox_layer[0][0] = bbox_path[0][0] < bbox_layer[0][0]
? bbox_path[0][0] : bbox_layer[0][0];
bbox_layer[0][1] = bbox_path[0][1] < bbox_layer[0][1]
? bbox_path[0][1] : bbox_layer[0][1];
bbox_layer[1][0] = bbox_path[1][0] > bbox_layer[1][0]
? bbox_path[1][0] : bbox_layer[1][0];
bbox_layer[1][1] = bbox_path[1][1] > bbox_layer[1][1]
? bbox_path[1][1] : bbox_layer[1][1];
}
});
// Compute the new scale param, with a little space (5%) around the outer border :
var s = 0.95 / Math.max((bbox_layer[1][0] - bbox_layer[0][0]) / width,
(bbox_layer[1][1] - bbox_layer[0][1]) / height);
// Compute the according translation :
var t = [(width - s * (bbox_layer[1][0] + bbox_layer[0][0])) / 2,
(height - s * (bbox_layer[1][1] + bbox_layer[0][1])) / 2];
// Apply the new projections parameters :
projection.scale(s)
.translate(t);
// And redraw your paths :
map.selectAll("g.layer").selectAll("path").attr("d", path);
};
Also, note that this example use d3 v4 (but in this case it doesn't change a lot apart from the naming of geoPath and geoConicConformal)
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>