d3 zoom and drag with SVG axes and canvas chart - d3.js

I've got a chart with lots of points. That's why I'm using canvas to draw the lines. For the x and y axes I'd like to use SVG since it's sharper and drawing text with canvas isn't super fast.
Here is the code (TypeScript)
import { min, max } from "d3-array";
import { scaleLinear, ScaleLinear } from "d3-scale";
import { select, event, Selection } from "d3-selection";
import { line, Line } from "d3-shape";
import { ZoomBehavior, zoom } from "d3-zoom";
import { axisBottom, Axis, axisLeft } from "d3-axis";
interface Margin {
left: number;
right: number;
top: number;
bottom: number;
}
interface Config {
margin: Margin;
target: HTMLCanvasElement;
svg: SVGSVGElement;
}
export default class ScopeChart {
private canvas: Selection<HTMLCanvasElement, unknown, null, undefined>;
private svg: Selection<SVGGElement, unknown, null, undefined>;
private xAxis: Axis<number>;
private xAxisGroup: Selection<SVGGElement, unknown, null, undefined>;
private yAxis: Axis<number>;
private yAxisGroup: Selection<SVGGElement, unknown, null, undefined>;
private context: CanvasRenderingContext2D;
private raw: number[];
private filtered: number[];
private xScale: ScaleLinear<number, number>;
private yScale: ScaleLinear<number, number>;
private line: Line<number>;
public constructor(config: Config) {
this.raw = [];
this.filtered = [];
const behavior = zoom() as ZoomBehavior<SVGGElement, unknown>;
const width = 640;
const height = 480;
const w = width - config.margin.left - config.margin.right;
const h = height - config.margin.top - config.margin.bottom;
this.canvas = select(config.target)
.attr("width", w)
.attr("height", h)
.style(
"transform",
`translate(${config.margin.left}px, ${config.margin.top}px)`
);
this.svg = select(config.svg)
.attr("width", width)
.attr("height", height)
.append("g")
.attr(
"transform",
`translate(${config.margin.left}, ${config.margin.top})`
);
this.svg
.append("rect")
.attr("width", w)
.attr("height", h)
.style("fill", "none")
.style("pointer-events", "all")
.call(behavior);
behavior
// set min to 1 to prevent zooming out of data
.scaleExtent([1, Infinity])
// prevent dragging data out of view
.translateExtent([[0, 0], [width, height]])
.on("zoom", this.zoom);
this.context = (this.canvas.node() as HTMLCanvasElement).getContext(
"2d"
) as CanvasRenderingContext2D;
this.xScale = scaleLinear().range([0, w]);
this.xAxis = axisBottom(this.xScale) as Axis<number>;
this.xAxisGroup = this.svg
.append("g")
.attr("class", "x axis")
.attr("transform", `translate(0, ${h})`)
.call(this.xAxis);
this.yScale = scaleLinear().range([h, 0]);
this.yAxis = axisLeft(this.yScale) as Axis<number>;
this.yAxisGroup = this.svg
.append("g")
.attr("class", "y axis")
.call(this.yAxis);
this.line = line<number>()
.x((_, i): number => this.xScale(i))
.y((d): number => this.yScale(d))
.context(this.context);
}
private drawRaw(context: CanvasRenderingContext2D): void {
context.beginPath();
this.line(this.raw);
context.lineWidth = 1;
context.strokeStyle = "steelblue";
context.stroke();
}
private drawFiltered(context: CanvasRenderingContext2D): void {
context.beginPath();
this.line(this.filtered);
context.lineWidth = 1;
context.strokeStyle = "orange";
context.stroke();
}
private clear(context: CanvasRenderingContext2D): void {
const width = this.canvas.property("width");
const height = this.canvas.property("height");
context.clearRect(0, 0, width, height);
}
public render(raw: number[], filtered: number[]): void {
this.raw = raw;
this.filtered = filtered;
this.xScale.domain([0, raw.length - 1]);
this.yScale.domain([min(raw) as number, max(raw) as number]);
this.clear(this.context);
this.drawRaw(this.context);
this.drawFiltered(this.context);
this.xAxisGroup.call(this.xAxis);
this.yAxisGroup.call(this.yAxis);
}
public zoom = (): void => {
const newXScale = event.transform.rescaleX(this.xScale);
const newYScale = event.transform.rescaleY(this.yScale);
this.line.x((_, i): number => newXScale(i));
this.line.y((d): number => newYScale(d));
this.clear(this.context);
this.drawRaw(this.context);
this.drawFiltered(this.context);
this.xAxisGroup.call(this.xAxis.scale(newXScale));
this.yAxisGroup.call(this.yAxis.scale(newYScale));
};
}
And here is the live example
https://codesandbox.io/s/1pprq
The problem is translateExtent. I'd like to restrict dragging when zoomed in to my available data, i.e. [0, 20000] on the x axis and [-1.2, 1.2] on the y axis.
Somehow I'm currently able to zoom in further. You can see the effect when zooming in and dragging all the way to the bottom. You will see a gap between the lowest value and the x axis.
I think it has something to do with using canvas and svg. The svg is on top of the canvas and the ZoomBehavior is on the svg. Somehow the zoom isn't properly translated to the canvas.
I'd like to keep the svg on top because I need more interface elements later one which are added to the svg.
Any ideas? Thank you!

If I understand the question correctly:
The issue you are running into is that your translate extent is not correct
behavior
// set min to 1 to prevent zooming out of data
.scaleExtent([1, Infinity])
// prevent dragging data out of view
.translateExtent([[0, 0], [width, height]])
.on("zoom", this.zoom);
In the above, width and height refer to the width and height of the SVG, not the canvas. Also, zoom extent is not often specified explicitly, but if zoom extent is not specified with zoom.extent(), the zoom extent defaults to the dimensions of the container it was called on.
If your translate extent is equal in size to your zoom extent - by default the extent of the container (the SVG) - which it is, you can zoom and pan anywhere within that container's coordinate space, but not to coordinates beyond it. Consequently, when zoom scale is 1, we cannot pan anywhere as we would by definition pan beyond the translate extent.
Note: This logically means translate extent must contain and not be smaller than the zoom extent.
But, in this scenario, if we zoom in, we can pan and remain within the translate extent.
We can see if you zoom in you cannot pan up beyond the intended limits. This is because the top of the canvas is at y==0, this is the bounds of the translate extent.
As you note if you zoom in you can pan down beyond the intended limits. The bottom of the canvas is h, which is smaller than height which is the translate extent limit, so as we zoom in, we can pan further and further down as the gap between h and height increases each time we zoom (and as noted above, cannot be panned when k==1).
We could try to change the translate extent to reflect the bounds of the canvas. But, as the canvas is smaller than the SVG this won't work as the translate extent would be smaller than the zoom extent. As noted above and noted here by Mike: "The problem is that the translateExtent you’ve specified is smaller than the zoom extent. So there’s no way to satisfy the requested constraint."
We can modify the translateExtent and the zoom's extent, however:
behavior
// set min to 1 to prevent zooming out of data
.scaleExtent([1, Infinity])
// set the zoom extent to the canvas size:
.extent([[0,0],[w,h]])
// prevent dragging data out of view
.translateExtent([[0, 0], [w, h]])
.on("zoom", this.zoom);
The above creates a zoom behavior that constrains the canvas to its original extent - we would be providing the same parameters if we were calling the zoom on the canvas and wanted to not be able to pan beyond it (except we could rely on the default zoom extent to provide the appropriate values rather than specifying the zoom extent manually).
Here's an updated sandbox.

Related

Updating D3 zoom behavior from v3

I am upgrading my d3 from version 3 to version 4 and having trouble updating the zoom behavior.
The following returns an error that y is not a function. Any Idea how to write this in version 4?
d3.zoom()
.y(yAxis)
.scaleExtent([1, 20])
.on('zoom', myFunctionHandler);
D3v3
In d3v3 the zoom behavior optionally modified x and y scales' domain and range using the zoom.x and zoom.y methods. This resulted in the following pattern:
let svg = d3.select("svg");
let g = svg.append("g").attr("transfrom","translate(0,50)");
let scale = d3.scale.linear()
.range([10,290])
let axis = d3.svg.axis()
.scale(scale)
.ticks(4)
.orient("bottom");
g.call(axis);
let zoom = d3.behavior.zoom()
.y(scale)
.scaleExtent([1, 20])
.on('zoom', zoomed);
svg.call(zoom);
function zoomed() {
g.call(axis);
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.17/d3.min.js"></script>
<svg width="300"></svg>
D3v4 and D3v5
D3v4 saw a substantial reworking of D3 as compared with v3, but the zoom behavior saw extensive changes relative to rest of the library. Instead of the zoom behavior modifying scales, the zoom event's transform have rescale methods that can be passed a scale. The rescale methods return a new scale with the appropriate updated domain for any given zoom (for either a horizontal or vertical scale), eg:
d3.event.transform.rescaleX(xScale);
Of course we need to update d3.svg.axis to d3.axis and d3.scale.type to d3.scaleType as well.
This leads to the following pattern:
let svg = d3.select("svg");
let g = svg.append("g").attr("transform","translate(0,50)");
let scale = d3.scaleLinear()
.range([10,290])
let axis = d3.axisBottom()
.scale(scale)
.ticks(4);
g.call(axis);
let zoom = d3.zoom()
.on("zoom", zoomed);
function zoomed() {
let zoomedScale = d3.event.transform.rescaleX(scale);
axis.scale(zoomedScale);
g.call(axis);
}
svg.call(zoom);
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<svg width="300"></svg>
Why not just rescale the original scale and use this with the axis? Because the zoom state is relative to its starting state (k: 1, x:0, y:0) we want to always use the original scale when rescaling, so we need to preserve the original scale. See here for more on that.
That the zoom no longer tracks/modifies x or y scales has resulted in the removal of the x and y methods of the zoom behavior: which explains your error that y is not a function.
D3v6 and v7
Starting with d3v6 the d3.event global is removed. Instead the event is passed to the zoom event function directly:
.on("zoom", function(event, datum) { ...
Which means we only need tweak the above:
let svg = d3.select("svg");
let g = svg.append("g").attr("transform","translate(0,50)");
let scale = d3.scaleLinear()
.range([10,290])
let axis = d3.axisBottom()
.scale(scale)
.ticks(4);
g.call(axis);
let zoom = d3.zoom()
.on("zoom", zoomed);
function zoomed({transform}) {
let zoomedScale = transform.rescaleX(scale);
axis.scale(zoomedScale);
g.call(axis);
}
svg.call(zoom);
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/7.0.0/d3.min.js"></script>
<svg width="300"></svg>
TL;DR
For all versions of d3 after v4 the zoom behavior does not modify scales. Instead the zoom event transform property provides methods to rescale x and y scales, these methods return the new rescaled scales to provide to axis generators.

d3 world map with country click and zoom almost working not quite

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.

Semantic zoom on map with circle showing capital

I wanted to implement semantic zoom on map of d3.js. I have developed a example of map and Major cities of particular country, which is working fine but sometime circle got overlap due to near places in maps so if i implement semantic zoom which will solve my circle overlapping problem.
I don't understand how to transform only graph not circle in my map.
My zooming function code is :
var zoom = d3.behavior.zoom()
.on("zoom",function() {
g.attr("transform","translate("+
d3.event.translate.join(",")+")scale("+d3.event.scale+")");
g.selectAll("circle")
.attr("d", path.projection(projection));
g.selectAll("path")
.attr("d", path.projection(projection));
});
My jsfiddle link
Anybody help me please!
Are you asking how to not scale the circles according to the zoom? The way you have it you are scaling the g element and the circles are in it. The way I'd do it is to "shrink" the circles when zoomed.
// zoom and pan
var zoom = d3.behavior.zoom()
.on("zoom",function() {
g.attr("transform","translate("+
d3.event.translate.join(",")+")scale("+d3.event.scale+")");
g.selectAll("circle")
.attr("r", function(){
var self = d3.select(this);
var r = 8 / d3.event.scale; // set radius according to scale
self.style("stroke-width", r < 4 ? (r < 2 ? 0.5 : 1) : 2); // scale stroke-width
return r;
});
});
Update fiddle.
I know this is an old post and it already has an accepted answer but as the original post suggests, d3's Semantic Zoom is a cleaner way of doing this. I implemented the same thing you did (circles on cities on a map) by following this demo https://bl.ocks.org/mbostock/368095. The only thing I had to change was I had to subtract the initial position of the circles in the transform function in order to correct their initial position.
function transform(t) {
return function(d) {
const point = [d.x, d.y] // this is the initial point
const tPoint = t.apply(point) // after translation
const finalPoint = [tPoint[0] - d.x, tPoint[1] - d.y]// subtract initial x & y to correct
return `translate(${finalPoint})`;
};
}

Making an button-activated animated tour in d3

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.

D3js cartography: auto-focus on geographic area ? (svg canvas, zoom scale, coordinate translation)

I processed SRTM raster data to generate shapefiles -> geojson -> topojson so I may feed D3js with a suitable format.
The result look like this (the blue area is my oversized svg canvas):
Given geographic area of interest geo-borders (west border, North, East, South) and center:
// India geo-frame borders in decimal ⁰
var WNES = { "W": 67.0, "N":37.5, "E": 99.0, "S": 5.0 };
// India geo-center :
var latCenter = (WNES.S + WNES.N)/2,
lonCenter = (WNES.W + WNES.E)/2;
// HTML expected frame dimensions
var width = 713,
height = 724;
// Projection: projection, center coord, scale(?), translate:
var projection = d3.geo.mercator()
.center([lonCenter, latCenter])
.scale(width)
.translate([width/2, height/2]); // this push into the center of the html frame
// SVG injection:
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
What is/are the relations between geo-dimensions, svg dimensions, and the scale ?
How to do an 'auto-focus' simplified as much as possible ?
I reused some code from Bostock & al, with some edits so you input your focus geo-area bounds (decimal coordinates):
var WNES = { "W": 67.0, "N":37.5, "E": 99.0, "S": 5.0 };
and the target svg canvas' width (px) such :
var width = 600,
to automatically set the svg canvas' height, the zoom scale, and the translation in order to focus the display only on and fully on the target geo area.
// 1. -------------- SETTINGS ------------- //
// India geo-frame borders (decimal ⁰)
var WNES = { "W": 67.0, "N":37.5, "E": 99.0, "S": 5.0 };
// Geo values of interest :
var latCenter = (WNES.S + WNES.N)/2, // will not be used
lonCenter = (WNES.W + WNES.E)/2, // will not be used
geo_width = (WNES.E - WNES.W),
geo_height= (WNES.N - WNES.S);
// HTML expected frame dimensions
var width = 600,
height = (geo_height / geo_width) * width ; // height function of width with ratio of geo-frame (later requires equirectangular projection!)
// Projection: projection, reset scale and reset translate
var projection = d3.geo.equirectangular()
.scale(1)
.translate([0, 0]);
// Normal stuff: SVG injection:
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
// Normal stuff: Path
var path = d3.geo.path()
.projection(projection)
.pointRadius(4);
// Data (getJSON: TopoJSON)
d3.json("final.json", showData);
// 2. ---------- FUNCTION ------------- //
function showData(error, fra) {
var Levels = topojson.feature(fra, fra.objects.levels);
// Focus area box compute to derive scale & translate.
var b = path.bounds(Levels), // get data's bounds as [​[left, bottom], [right, top]​] [[W, S], [E, N]]
s = 1 / 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];
// Projection update
projection
.scale(s)
.translate(t);
//Normal stuff: Append my topojson objects => svg layers
svg.append("path")
.datum(Levels)
.attr("d", path)
svg.selectAll(".levels")
.data(topojson.feature(fra, fra.objects.levels).features)
.enter().append("path")
.attr("class", function(d) { return "Topo_" + d.properties.name; })
.attr("data-elev", function(d) { return d.properties.name; })
.attr("d", path)
}
Result is perfect:
See:
path.bounds(feature) -- Computes the projected bounding box (in pixels) for the specified feature.
India relief map with auto-focus (Hugo Lopez) -- Working example (with custom canvas height)
Center a map in d3 given a geoJSON object (Bostock & al.) -- critical help from there
Project to Bounding Box (Mike Bostock) -- Working example (with prefixed canvas dimensions)
D3js: How to design topographic maps? (Hugo Lopez) -- tutorial with makefile and html-d3js code.

Resources