Scale path to fit SVG height without blind trial and error - dictionary
This is the Feature
var singaporeJSON = {"type":"FeatureCollection","geocoding":{"creation_date":"2016-01-09","generator":{"author":{"name":"Mapzen"},"package":"fences-builder","version":"0.1.2"},"license":"ODbL (see http://www.openstreetmap.org/copyright)"},"features":[{"properties":{"name:display":"Singapore","name":"singapore"},"geometry":{"type":"MultiPolygon","coordinates":[[[[104.5706735,1.4419380999999996],[104.55258289999996,1.4106225999999995],[104.3891305,1.3172011999999995],[104.3488465,1.3331733],[104.35531379999998,1.3563739000000001],[104.37827119999996,1.4102812999999996],[104.4633395,1.4991501],[104.48455620000003,1.5130449],[104.486389,1.5123502],[104.4986883,1.5064215999999995],[104.52192920000003,1.4921171000000004],[104.5327622,1.4838079999999998],[104.5430018,1.4747782999999997],[104.5526002,1.4650701999999998],[104.5706735,1.4419380999999996]]],[[[104.12611109999997,1.28925],[104.12517559999996,1.2758195999999995],[104.11490400000002,1.2765354],[104.0927257,1.2733869],[104.0333333,1.2694999999999996],[103.9184908,1.2226474],[103.88074999999998,1.20725],[103.85983329999998,1.1959722],[103.80499999999999,1.1714444],[103.74069440000004,1.1303611],[103.67072219999996,1.1794444],[103.66069440000003,1.1881667],[103.57233329999997,1.1987500000000004],[103.56666670000003,1.1955],[103.60286109999997,1.2641666999999996],[103.6170833,1.3154166999999997],[103.6178333,1.3216111],[103.6300556,1.3410556],[103.64919440000003,1.3799166999999997],[103.65297219999995,1.3870833],[103.6535,1.3912778000000001],[103.6571667,1.4004166999999996],[103.6639167,1.4104721999999998],[103.6694444,1.4157499999999996],[103.67388889999995,1.4281389],[103.68333329999997,1.43725],[103.69405560000003,1.4398889],[103.69886109999997,1.4433055999999995],[103.70375,1.4507778],[103.71411110000003,1.4574721999999998],[103.72833329999997,1.4601389],[103.74677779999998,1.4503889],[103.76069440000003,1.4483889],[103.7711111,1.4527778],[103.7904722,1.4651389],[103.80366670000002,1.4765],[103.81266670000002,1.4784722],[103.8221667,1.4766943999999993],[103.83416670000003,1.4729999999999996],[103.85199999999999,1.4661666999999996],[103.8586667,1.4629443999999996],[103.8649722,1.4588610999999996],[103.8681667,1.4565278],[103.88613889999996,1.4351388999999997],[103.8978611,1.4279443999999992],[103.91275,1.4275277999999996],[103.91683330000002,1.4266666999999993],[103.93341670000002,1.4304166999999997],[103.93769440000005,1.4304721999999999],[103.94252780000002,1.4278332999999992],[103.96066670000003,1.42475],[103.96680560000003,1.4221666999999996],[103.9835833,1.4242778],[103.98616670000003,1.4249166999999996],[103.99525000000003,1.4236943999999991],[104.00286109999998,1.4206111],[104.0233056,1.4391388999999997],[104.0408333,1.4438889],[104.05747219999996,1.4398610999999992],[104.07119440000002,1.4346389],[104.07647219999996,1.4309166999999996],[104.08847219999996,1.4175832999999995],[104.09075,1.4124443999999996],[104.09266670000002,1.4059443999999992],[104.0930278,1.3998055999999999],[104.09247219999996,1.3942500000000004],[104.0835833,1.3687500000000001],[104.07972219999996,1.3575],[104.08527779999997,1.3466666999999997],[104.12611109999997,1.28925]]]]},"type":"Feature"}]}
This is the Code
var w = $('#map').parent().width();
var h = $(document).height() - 100;
var projection = d3.geoEquirectangular()
.scale(w)//scale it down to h / (2*Math.PI)
.translate([-w , h]);//translate as per your choice
//Create SVG element
var svg = d3.select("#map")
.append("svg")
.attr("width", w)
.attr("height", h);
svg.selectAll("path")
.data(singaporeJSON.features)
.enter()
.append("path")
.attr("d", d3.geoPath().projection(projection))
.style("fill", "steelblue");
And the result is, (ref attachment, highlighted by red circle)
How do I scale this without any trial and error, so that it fits the SVG height and width
The answer is in the link
Center a map in d3 given a geoJSON object
referred by Gerado Futado.
This would be the final code
// Create a unit projection.
var projection = d3.geo.albers()
.scale(1)
.translate([0, 0]);
// Create a path generator.
var path = d3.geo.path()
.projection(projection);
// Compute the bounds of a feature of interest, then derive scale & translate.
var b = path.bounds(feature), // if u have features, use features[0]
s = .95 / Math.max((b[1][0] - b[0][0]) / width, (b[1][1] - b[0][1]) / height),
t = [(width - s * (b[1][0] + b[0][0])) / 2, (height - s * (b[1][1] + b[0][1])) / 2];
// Update the projection to use computed scale & translate.
projection
.scale(s)
.translate(t);
svg.selectAll("path")
.data(singaporeJSON.features)
.enter()
.append("path")
.attr("d", d3.geoPath().projection(projection)) .style("fill", "steelblue");
Related
D3.geo : responsive frame given a geojson object?
I use Mike Bostock's code to Center a map in d3 given a geoJSON object. The important part of the code is this: var width = 960, height = 500; var svg = d3.select("body").append("svg") .attr("width", width) .attr("height", height); d3.json("/d/4090846/us.json", function(error, us) { var states = topojson.feature(us, us.objects.states), state = states.features.filter(function(d) { return d.id === 34; })[0]; /* ******************* AUTOCENTERING ************************* */ // Create a unit projection. var projection = d3.geo.albers() .scale(1) .translate([0, 0]); // Create a path generator. var path = d3.geo.path() .projection(projection); // Compute the bounds of a feature of interest, then derive scale & translate. var b = path.bounds(state), s = .95 / Math.max((b[1][0] - b[0][0]) / width, (b[1][1] - b[0][1]) / height), t = [(width - s * (b[1][0] + b[0][0])) / 2, (height - s * (b[1][1] + b[0][1])) / 2]; // Update the projection to use computed scale & translate. projection .scale(s) .translate(t); /* ******************* END *********************************** */ // Landmass svg.append("path") .datum(states) .attr("class", "feature") .attr("d", path); // Focus svg.append("path") .datum(state) .attr("class", "outline") .attr("d", path); }); For example, bl.ocks.org/4707858 zoom in such: How to center and zoom on the target topo/geo.json AND adjust the svg frame dimensions so it fit a 5% margin on each size ?
Mike's explained Basically, Mike's code states the frame dimensions via var width = 960, height = 500; var svg = d3.select("body").append("svg") .attr("width", width) .attr("height", height); Once the frame is hardly set, then you check out the largest limiting ratio so your geojson shape fill your svg frame on its largest dimension relative to the svg frame dimensions widht & height. Aka, if the shape's width VS frame width or shape height VS frame height is the highest. This, in turn, help to recalculate the scale via 1/highest ratio so the shape is as small as required. It's all done via: var b = path.bounds(state), s = .95 / Math.max((b[1][0] - b[0][0]) / width, (b[1][1] - b[0][1]) / height); // b as [[left, bottom], [right, top]] // (b[1][0] - b[0][0]) = b.left - b.right = shape's width // (b[1][3] - b[0][4]) = b.top - b.bottom = shape's height Then, refreshing your scale and transition you get Mike Bostock's zoom: New framing To frame up around the geojson shape is actually a simplification of Mike's code. First, set temporary svg dimensions: var width = 200; var svg = d3.select("body").append("svg") .attr("width", width); Then, get the dimensions of the shapes and compute around it : var b = path.bounds(state); // b.s = b[0][1]; b.n = b[1][1]; b.w = b[0][0]; b.e = b[1][0]; b.height = Math.abs(b[1][1] - b[0][1]); b.width = Math.abs(b[1][0] - b[0][0]); var r = ( b.height / b.width ); var s = 0.9 / (b.width / width); // dimension of reference: `width` (constant) //var s = 1 / Math.max(b.width / width, b.height / height ); // dimension of reference: largest side. var t = [(width - s * (b[1][0] + b[0][0])) / 2, (width*r - s * (b[1][1] + b[0][1])) / 2]; //translation Refresh projection and svg's height: var proj = projection .scale(s) .translate(t); svg.attr("height", width*r); It's done and fit the pre-allocated width=150px, find the needed height, and zoom properly. See http://bl.ocks.org/hugolpz/9643738d5f79c7b594d0
D3 rotating globe and text
Needing some help... i was able to find an example of a rotating globe, that works great, i even found a way to put red circles at a point. Even better to setup a timer and everything rotates with the globe great. But if i put text on the map at the same point as the red circles it shows up at the starting point that i placed it, but as the world turns the red circle moves with the globe, but the text is frozen at the points that it was written. i am trying to get the text to rotate with the world and the red circles. think in the country of united states i want to put a number, brazil would have number when the globe rotates to china the values would still be on the countries i put it and when it rotates US and Brazil back to the front the numbers are there showing. This is what i have in code, bear with me I am still a noob when working with D3. thanks for any input... // Initialize some variables: var element = '#home1', width = $("#home1").width(), height = $("#home1").height(); var diameter = 460, radius = diameter/2, velocity = .001, then = Date.now(); var features, circles; var projection = d3.geo.orthographic() .scale(radius - 2) .translate([radius, radius]) .clipAngle(90); // Save the path generator for the current projection: var path = d3.geo.path() .projection(projection) .pointRadius( function(d,i) { return radius; }); // Define the longitude and latitude scales, which allow us to map lon/lat coordinates to pixel values: var lambda = d3.scale.linear() .domain([0, width]) .range([-180, 180]); var phi = d3.scale.linear() .domain([0, height]) .range([90, -90]); // Create the drawing canvas: var svg = d3.select("#home1").append("svg:svg") .attr("width", diameter) .attr("height", diameter); //Create a base circle: (could use this to color oceans) var backgroundCircle = svg.append("svg:circle") .attr('cx', diameter / 2) .attr('cy', diameter / 2) .attr('r', 0) .attr('class', 'geo-globe'); // Make a tag to group all our countries, which is useful for zoom purposes. (child elements belong to a 'group', which we can zoom all-at-once) var world = svg.append('svg:g'); var zoomScale = 1; // default // Create the element group to mark individual locations: var locations = svg.append('svg:g').attr('id', 'locations'); // Having defined the projection, update the backgroundCircle radius: backgroundCircle.attr('r', projection.scale() ); // Construct our world map based on the projection: d3.json('world-countries.json', function(collection) { features = world.selectAll('path') .data(collection.features) .enter() .append('svg:path') .attr('class', 'geo-path') .attr('d', path); // features.append('svg:title') // .text( function(d) { return d.properties.name; }); }); // end FUNCTION d3.json() d3.json("data.geojson", function(collection) { console.log("2"); cs = locations.selectAll('path') .data(collection.features) .enter().append('svg:path') .datum(function(d) {return {type: "Point", coordinates: [d.geometry.coordinates[0], d.geometry.coordinates[1]]}; }) .attr('class', 'geo-node') .attr("d", path.pointRadius(5)) .attr('d', path); cs1 = locations.selectAll('text') .data(collection.features) .enter().append('svg:text') .attr("transform", function(d) {return "translate(" + projection(d.geometry.coordinates) + ")"; }) .attr("dy", ".35em") .attr('d', path) .text(function(d) { return d.properties.name; }); }); // end FUNCTION d3.json() d3.timer(function() { if(offpage === 0) { var angle = velocity * (Date.now() - then); projection.rotate([angle,0,0]) svg.selectAll("path").attr("d", path.projection(projection)); } }); d3.select(window) .on("touchmove", mousemove) .on("touchstart", mousedown); function mousemove() { offpage = 0; } function mousedown() { offpage=1 }
In your code, features(the world map) is a path, and cs(the city points) is a path, but cs1(the city names) is a text. In your timer you rotate the paths, which doesn't rotate the text. My solution uses rotation degrees, instead of angle, so you'll have to adapt the formula. d3.timer(function() { tcounter++ rotation++ if (rotation>=360) rotation = 0 projection.rotate([rotation,0,0]) www.attr("d", path.projection(projection)); citydot.attr("d", path.projection(projection)); ctext.attr("transform", function(d) { return "translate(" + projection(d.geometry.coordinates) + ")"; }) .text(function(d) { if (((rotation + d.geometry.coordinates[0] > -90) && (rotation + d.geometry.coordinates[0] <90)) || ((rotation + d.geometry.coordinates[0] > 270) && (rotation + d.geometry.coordinates[0] <450))) return d.properties.city; else return "" }); if (tcounter > 360) return true else return false })
d3 map - After using blur filter, zoom does not work properly
I am using the blur effect on the d3 map as given here: http://geoexamples.blogspot.in/2014/01/d3-map-styling-tutorial-ii-giving-style.html? But after using this method (because of how the data is loaded..using datum) my zoom functionality behaves randomly. Irrespective of where I click it zooms to the same point. Also, the animations have become very slow after using the filter. Is there any other way to achieve blur? Or a solution to this problem? Any help? Thanks. This is the code for the world creation in case when filtering is required (use of datum as per the code on the above site). d3.json("world-110m2.json", function(error, world) { g.insert("path") .datum(topojson.feature(world, world.objects.land)) .attr("d", path); g.insert("path") .datum(topojson.mesh(world, world.objects.countries, function(a, b) { return a !== b; })) .attr("d", path) .append("path"); g.selectAll("path") .on("click", click);}) This is the code used in case filtering is not required (No use of datum - maybe the datum is causing the issue) d3.json("world-110m2.json", function(error,topology) { g.selectAll("path") .data(topojson.object(topology, topology.objects.countries) .geometries) .enter() .append("path") .attr("d",path) .on("click", click);)} This is the zoom function: got the code from here: http://bl.ocks.org/mbostock/2206590 function click(d) { var x, y, k; var centered; if (d && centered !== d) { var centroid = path.centroid(d); x = centroid[0]; y = centroid[1]; k = 4; centered = d; } else { x = width / 2; y = height / 2; k = 1; centered = null; } if (active === d) return reset(); g.selectAll(".active").classed("active", false); d3.select(this).classed("active", active = d); var b = path.bounds(d); g.selectAll("path") .classed("active", centered && function(d) { return d === centered; }); g.transition() .duration(750) .attr("transform", "translate(" + width / 2 + "," + height / 2 + ")scale(" + k + ")translate(" + -x + "," + -y + ")") .style("stroke-width", 1.5 / k + "px"); }
The blur filter consumes lots of resources, as indicated in the post. Speciallly if you combine it with other filters. One solution would be using Canvas instead of SVG. Here you have some filters using the Canvas element. It should be possible to achieve the same result. I can't find why the zoom stops working, but the performance is slower because you use all the data, so you are applying the filter to all the data instead of using only the part of the word you are showing, so you are using a much bigger image when you zoom.
Radius of projected sphere in screen space
I'm trying to find the visible size of a sphere in pixels, after projection to screen space. The sphere is centered at the origin with the camera looking right at it. Thus the projected sphere should be a perfect circle in two dimensions. I am aware of this 1 existing question. However, the formula given there doesn't seem to produce the result I want. It is too small by a few percent. I assume this is because it is not correctly taking perspective into account. After projecting to screen space you do not see half the sphere but significantly less, due to perspective foreshortening (you see just a cap of the sphere instead of the full hemisphere 2). How can I derive an exact 2D bounding circle?
Indeed, with a perspective projection you need to compute the height of the sphere "horizon" from the eye / center of the camera (this "horizon" is determined by rays from the eye tangent to the sphere). Notations: d: distance between the eye and the center of the sphere r: radius of the sphere l: distance between the eye and a point on the sphere "horizon", l = sqrt(d^2 - r^2) h: height / radius of the sphere "horizon" theta: (half-)angle of the "horizon" cone from the eye phi: complementary angle of theta h / l = cos(phi) but: r / d = cos(phi) so, in the end: h = l * r / d = sqrt(d^2 - r^2) * r / d Then once you have h, simply apply the standard formula (the one from the question you linked) to get the projected radius pr in the normalized viewport: pr = cot(fovy / 2) * h / z with z the distance from the eye to the plane of the sphere "horizon": z = l * cos(theta) = sqrt(d^2 - r^2) * h / r so: pr = cot(fovy / 2) * r / sqrt(d^2 - r^2) And finally, multiply pr by height / 2 to get the actual screen radius in pixels. What follows is a small demo done with three.js. The sphere distance, radius and the vertical field of view of the camera can be changed by using respectively the n / f, m / p and s / w pairs of keys. A yellow line segment rendered in screen-space shows the result of the computation of the radius of the sphere in screen-space. This computation is done in the function computeProjectedRadius(). projected-sphere.js: "use strict"; function computeProjectedRadius(fovy, d, r) { var fov; fov = fovy / 2 * Math.PI / 180.0; //return 1.0 / Math.tan(fov) * r / d; // Wrong return 1.0 / Math.tan(fov) * r / Math.sqrt(d * d - r * r); // Right } function Demo() { this.width = 0; this.height = 0; this.scene = null; this.mesh = null; this.camera = null; this.screenLine = null; this.screenScene = null; this.screenCamera = null; this.renderer = null; this.fovy = 60.0; this.d = 10.0; this.r = 1.0; this.pr = computeProjectedRadius(this.fovy, this.d, this.r); } Demo.prototype.init = function() { var aspect; var light; var container; this.width = window.innerWidth; this.height = window.innerHeight; // World scene aspect = this.width / this.height; this.camera = new THREE.PerspectiveCamera(this.fovy, aspect, 0.1, 100.0); this.scene = new THREE.Scene(); this.scene.add(THREE.AmbientLight(0x1F1F1F)); light = new THREE.DirectionalLight(0xFFFFFF); light.position.set(1.0, 1.0, 1.0).normalize(); this.scene.add(light); // Screen scene this.screenCamera = new THREE.OrthographicCamera(-aspect, aspect, -1.0, 1.0, 0.1, 100.0); this.screenScene = new THREE.Scene(); this.updateScenes(); this.renderer = new THREE.WebGLRenderer({ antialias: true }); this.renderer.setSize(this.width, this.height); this.renderer.domElement.style.position = "relative"; this.renderer.autoClear = false; container = document.createElement('div'); container.appendChild(this.renderer.domElement); document.body.appendChild(container); } Demo.prototype.render = function() { this.renderer.clear(); this.renderer.setViewport(0, 0, this.width, this.height); this.renderer.render(this.scene, this.camera); this.renderer.render(this.screenScene, this.screenCamera); } Demo.prototype.updateScenes = function() { var geometry; this.camera.fov = this.fovy; this.camera.updateProjectionMatrix(); if (this.mesh) { this.scene.remove(this.mesh); } this.mesh = new THREE.Mesh( new THREE.SphereGeometry(this.r, 16, 16), new THREE.MeshLambertMaterial({ color: 0xFF0000 }) ); this.mesh.position.z = -this.d; this.scene.add(this.mesh); this.pr = computeProjectedRadius(this.fovy, this.d, this.r); if (this.screenLine) { this.screenScene.remove(this.screenLine); } geometry = new THREE.Geometry(); geometry.vertices.push(new THREE.Vector3(0.0, 0.0, -1.0)); geometry.vertices.push(new THREE.Vector3(0.0, -this.pr, -1.0)); this.screenLine = new THREE.Line( geometry, new THREE.LineBasicMaterial({ color: 0xFFFF00 }) ); this.screenScene = new THREE.Scene(); this.screenScene.add(this.screenLine); } Demo.prototype.onKeyDown = function(event) { console.log(event.keyCode) switch (event.keyCode) { case 78: // 'n' this.d /= 1.1; this.updateScenes(); break; case 70: // 'f' this.d *= 1.1; this.updateScenes(); break; case 77: // 'm' this.r /= 1.1; this.updateScenes(); break; case 80: // 'p' this.r *= 1.1; this.updateScenes(); break; case 83: // 's' this.fovy /= 1.1; this.updateScenes(); break; case 87: // 'w' this.fovy *= 1.1; this.updateScenes(); break; } } Demo.prototype.onResize = function(event) { var aspect; this.width = window.innerWidth; this.height = window.innerHeight; this.renderer.setSize(this.width, this.height); aspect = this.width / this.height; this.camera.aspect = aspect; this.camera.updateProjectionMatrix(); this.screenCamera.left = -aspect; this.screenCamera.right = aspect; this.screenCamera.updateProjectionMatrix(); } function onLoad() { var demo; demo = new Demo(); demo.init(); function animationLoop() { demo.render(); window.requestAnimationFrame(animationLoop); } function onResizeHandler(event) { demo.onResize(event); } function onKeyDownHandler(event) { demo.onKeyDown(event); } window.addEventListener('resize', onResizeHandler, false); window.addEventListener('keydown', onKeyDownHandler, false); window.requestAnimationFrame(animationLoop); } index.html: <!DOCTYPE html> <html> <head> <title>Projected sphere</title> <style> body { background-color: #000000; } </style> <script src="http://cdnjs.cloudflare.com/ajax/libs/three.js/r61/three.min.js"></script> <script src="projected-sphere.js"></script> </head> <body onLoad="onLoad()"> <div id="container"></div> </body> </html>
Let the sphere have radius r and be seen at a distance d from the observer. The projection plane is at distance f from the observer. The sphere is seen under the half angle asin(r/d), so the apparent radius is f.tan(asin(r/d)), which can be written as f . r / sqrt(d^2 - r^2). [The wrong formula being f . r / d.]
The illustrated accepted answer above is excellent, but I needed a solution without knowing the field of view, just a matrix to transform between world and screen space, so I had to adapt the solution. Reusing some variable names from the other answer, calculate the start point of the spherical cap (the point where line h meets line d): capOffset = cos(asin(l / d)) * r capCenter = sphereCenter + ( sphereNormal * capOffset ) where capCenter and sphereCenter are points in world space, and sphereNormal is a normalized vector pointing along d, from the sphere center towards the camera. Transform the point to screen space: capCenter2 = matrix.transform(capCenter) Add 1 (or any amount) to the x pixel coordinate: capCenter2.x += 1 Transform it back to world space: capCenter2 = matrix.inverse().transform(capCenter2) Measure the distance between the original and new points in world space, and divide into the amount you added to get a scale factor: scaleFactor = 1 / capCenter.distance(capCenter2) Multiply that scale factor by the cap radius h to get the visible screen radius in pixels: screenRadius = h * scaleFactor
Why is my D3 zoom transform not centering properly?
I'm upgrading my map to v3 of D3 and using the click to zoom transformation outlined in the D3 example code found here. My code is nearly identical except that my map has slightly smaller dimensions (564 x 300 instead of 960 x 500). In addition, I have my map nested within a div and off to the top left of my page (though I don't think this matters) The initial load of my map loads is fine (using black background for distinction currently) // Clear existing map in case there is remnant data. $("#map").html(null); var mapWidth = 564; var mapHeight = 300; var projection = d3.geo.albersUsa() .scale(mapWidth) .translate([0, 0]); var path = d3.geo.path() .projection(projection); var svg = d3.select("#map") .append("svg") .attr("id", "map-svg") .attr("width", mapWidth) .attr("height", mapHeight); svg.append("rect") .attr("id", "map-background") .attr("class", "background") .attr("width", mapWidth) .attr("height", mapHeight) .on("click", click); // Create placeholders for shapes and labels var states = svg.append("g") .attr("transform", "translate(" + mapWidth / 2 + "," + mapHeight / 2 + ")") .attr("id", "states"); However, when a state is clicked, and the click function runs, my transform seems to be off. In my case, the state of Arkansas was clicked (indicated with blue shading) function click(d) { var x = 0, y = 0, k = 1; if (d && centered !== d) { var centroid = path.centroid(d); x = -centroid[0]; y = -centroid[1]; k = 4; centered = d; } else { centered = null; } d3.select("#states").selectAll("path") .classed("active", centered && function (d) { return d === centered; }); d3.select("#states").transition() .duration(1000) .attr("transform", "scale(" + k + ")translate(" + x + "," + y + ")") .style("stroke-width", 1.5 / k + "px"); } My only thought is that my centroid calculations need to be adjusted slightly for the smaller size or different position of the map, but this doesn't seem right either. How do I make the proper adjustments? EDIT: I found that if I add a "negative translation" at the end of the transform (translate(" + -x + "," + -y + ")) that it gets closer to properly centering on the zoom, but not perfectly
You're missing one of two transforms from the example; but read on for a simpler solution. The example you're using has two nested transforms. First, a static transform on the outer G element: var g = svg.append("g") .attr("transform", "translate(" + width / 2 + "," + height / 2 + ")") .append("g") .attr("id", "states"); This transform serves the same purpose as projection.translate normally does, but the example is using the translate([0, 0]). (As I said, there will be a simpler solution…) The second transform is set dynamically on the inner G (with id "states") to zoom in: g.transition() .attr("transform", "scale(" + k + ")translate(" + x + "," + y + ")"); Note that the var g here refers to the inner G element, because when g was defined, there was an append("g") that was chained with a second append("g"); the variable is thus defined as the second, inner G, rather than the first, outer G. The resulting SVG looks like this: <g transform="translate(480,250)"> <g id="states" transform="translate(75.746,-439.514)scale(4,4)"> … </g> </g> Your derivation is missing the nested, inner G. So when you set the "transform" attribute on your #states G element, you're overwriting the outer transform, giving you this: <g id="states" transform="scale(4)translate(18.936679862557288,-109.8787070159044)"> … </g> So, you're missing the static transform, "translate(480,250)". I'd recommend combining these transforms together. Then you don't need the outer G, and you can : g.transition() .attr("transform", "translate(" + width / 2 + "," + height / 2 + ")" + "scale(" + k + ")" + "translate(" + x + "," + y + ")"); This also eliminates the need to set the projection’s translate to [0, 0], so you can use the standard translate [width / 2, height / 2] instead. I've updated the example to do just that!