D3 - How to get correct scale and translate origin after manual zoom and pan to country path - d3.js

I've got a topology map with pan and zoom functionality.
Clicking on the country, I'm zoom/panning into the country, using this:
if (this.active === d) return
var g = this.vis.select('g')
g.selectAll(".active").classed("active", false)
d3.select(path).classed('active', active = d)
var b = this.path.bounds(d);
g.transition().duration(750).attr("transform","translate(" +
this.proj.translate() + ")" +
"scale(" + .95 / Math.max((b[1][0] - b[0][0]) / this.options.width, (b[1][1] - b[0][1]) / this.options.height) + ")" +
"translate(" + -(b[1][0] + b[0][0]) / 2 + "," + -(b[1][1] + b[0][1]) / 2 + ")");
g.selectAll('path')
.style("stroke-width", 1 / this.zoom.scale())
However, if I continue to drag pan, the map jerks back into the initial position before the click happens, before panning. Code to pan/zoom is here:
this.zoom = d3.behavior.zoom().on('zoom', redraw)
function redraw() {
console.log('redraw')
_this.vis.select('g').attr("transform","translate(" +
d3.event.translate.join(",") + ")scale(" + d3.event.scale + ")")
_this.vis.select('g').selectAll('path')
.style("stroke-width", 1 / _this.zoom.scale())
}
this.vis.call(this.zoom)
In another words, after zooming into a point by clicking, and then dragging by the redraw function, the redraw does not pick up the correct translate+scale to continue from.

To continue at the right 'zoom' after the transition, I had to set the zoom to the new translate and scale.
Example of reset which is applied similarly to my click and zoom event, the set new zoom point is the critical bit:
_this.vis.select('g').transition().duration(1000)
.attr('transform', "translate(0,0)scale(1)")
/* SET NEW ZOOM POINT */
_this.zoom.scale(1);
_this.zoom.translate([0, 0]);

Related

D3.js: How to Center svg map after Drag, Zoom, or MouseMove Events

My map projection and display goes awry when users drag, move the map (see: https://realtimeceap.brc.tamus.edu).
For example (duplicate event):
1. Select a field condition from dropdownlist.
2. Select a State from dropdownlist.
3. Select a County from dropdownlist.
4. Drag the map or move the mousewheel.
5. Then, select another State from dropdownlist. The map is not centered at the middle of the svg element and the scale is off, instead of scale 1.
Appreciate any help.
I reset the map on selecting a state as follows:
function resetMap() {
svg = d3.select("#svgMap2");
var w = 728;
var h = 500;
var project = d3.geoAlbersUsa()
.scale(1000)
.translate([w / 2, h / 2]);
var t = project.translate(); // the projection's default translation
var scale = project.scale;
//reset all features to original scale
d3.select("#svgMap2").select("#counties").selectAll(".county")
.transition()
.duration(750)
.style("stroke-width", "0.5px")
.style("stroke", "#808080")
.attr("transform", "translate(" + t[0] + "," + t[1] + ")scale(" + scale + ")");
}
Been working on this for a while and found the solution is so simple. I needed to reset the g element's transform attribute to null to redefine it. The id of my svg = svgMap2 and the id of my g element = counties. BTW, I'm using D3.js version 4.
function resetMap() {
var k = 1;
var t = [0, 0];
svg = d3.select("#svgMap2")
d3.select("#svgMap2").select("#counties").selectAll(".county").classed("active", false);
d3.select("#counties").attr("transform", null);
svg.select("#counties").selectAll(".county") //must use svg or it disables pan, zoom
.attr("width", width)
.attr("height", height)
.style("stroke-width", "0.5px")
.style("stroke", "#808080")
.attr("transform", "translate(" + t[0] + "," + t[1] + ")scale(" + k + ")");
}

d3js display only path elements contained in bounding box

I have two topojson files obtained from shapefiles which I append to the same g node of an svg element in a nested loop.
// Layer 1
d3.json("grid.topojson", function(error, grid) {
// Layer 2 (continents)
d3.json("continents.topojson", function(error, continent) {
...
It is possible to click on the continents to zoom in on a particular region (from https://bl.ocks.org/mbostock/4699541).
The grid layer is very dense so it slows down the zoom when clicking on the continents in the continent layer. To get around this, I would like to display the grid layer only after zooming in on a particular continent. I have an .on("click") event that triggers the zoom function:
function clicked(path, d, m_width, m_height, _this, grid) {
var this_class = get_classFromPath(_this);
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 = .2 / Math.max(dx / m_width, dy / m_height),
translate = [m_width / 2 - scale * x, m_height / 2 - scale * y];
g.transition()
.duration(750)
.style("stroke-width", 1.5 / scale + "px")
.attr("transform", "translate(" + translate + ")scale(" + scale + ")");
// .selectAll("path.model-grid")
// .style("display", "inline");
}
In the clicked function, I want to make the grid visible after the zoom, but it seems unnecessary (and is very slow) to useselectAll since only a small part of the total grid is visible in the display. Unfortunately, the shapefile of the grid does not contain any ids I can use to correlate with the id of the continents, so I cannot select grid path elements by id.
Is there a way to find out which path elements of the grid layer are contained in the bounds box after the zoom?
Any help is much appreciated, thanks!

D3 Map disable zoom on city label

i'm stuck in this problem for a few days, and i hope someone can help me.
Basically this is the problem:
I've a UK map , with city label and marker. I've enabled zoom and pan on this, but when i zoom in even marker and label grow in dimension.
I'd like to preserve marker poisiton during zoom and pan (London is always in its place) but don't scale the dimension of marker and label.
This is my zoom function
var zoomBehav= function(){
var t = d3.event.translate,
s = d3.event.scale;
t[0] = Math.min(width / 2 * (s - 1), Math.max(width / 2 * (1 - s), t[0]));
t[1] = Math.min(height / 2 * (s - 1) + 230 * s, Math.max(height / 2 * (1 - s) - 230 * s, t[1]));
zoom.translate(t);
g.style("stroke-width", 1 / s).attr("transform", "translate(" + t + ") scale(" + s + ")");
};
Anyone can help me?
ADDITIONAL INFO
I think i should write more info.
My svg is defined as
this.svg = d3.select("#"+this.config.mapContainer).append("svg")
.attr("id","svgMap")
.attr("width",width)
.attr("height",height)
.style("background-color", "lightblue")
.append("g").attr('id','first_g')
.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")")
//Adding Zoom Rect to svg
this.svg.append("rect")
.attr("id", "d3e_zoomArea")
.attr("class", "d3e_zoomArea")
.attr("x", -width / 2)
.attr("y", -height / 2)
.attr("width", width)
.attr("height", height)
.attr('onmousedown', 'return false');
//Insert g elementi in svg
g = this.svg.append("g").attr('id','g_pathContainer');
//Set Actual Map Right Projection
/ create a first guess for the projection
subunits = topojson.feature(map, map.objects[level]);
center = d3.geo.centroid(subunits);
projection = d3.geo.mercator()
.scale(width / 2 / Math.PI) //Crea la scala di visualizzazione
.center(center) //Centra la posizione sulla mappa
.translate([0, 0]);
path = d3.geo.path().projection(projection);
// using the path determine the bounds of the current map and use
// these to determine better values for the scale and translation
var bounds = path.bounds(subunits);
var hscale = scale*width / (bounds[1][0] - bounds[0][0]);
var vscale = scale*height / (bounds[1][1] - bounds[0][1]);
scale = (hscale < vscale) ? hscale : vscale;
// new projection
projection = d3.geo.mercator().center(center)
.scale(scale).translate([0, 0]);
path = path.projection(projection);
zoom = d3.behavior.zoom()
.translate(projection.translate())
.scale(projection.scale())
.scaleExtent([this.config.scaleRange.scaleMin , this.config.scaleRange.scaleMax])
.on("zoom", zoomed);
d3.select("#first_g").call(zoom);
JSFIDDLE added
I've created a jsfiddle (http://jsfiddle.net/z9S7y/) to reproduce my problem. I've applied some initial transformation to base projection in order to center and scale the map in relation to actual country.

Zoom on tree layout preserve root node location

I am quite new to d3 and am having trouble with zooming and dragging on a tree layout.
EDIT: Updated JSFiddle with information from comments
I have created a sample, JSFiddle here.
My issue is in the zoom function:
function zoom() {
var scale = d3.event.scale,
translation = d3.event.translate,
tbound = -height * scale * 100,
bbound = height * scale,
lbound = (-width + margin.right) * scale,
rbound = (width - margin.bottom) * scale;
console.log("pre min/max" + translation);
// limit translation to thresholds
translation = [
Math.max(Math.min(translation[0], rbound), lbound),
Math.max(Math.min(translation[1], bbound), tbound)];
console.log("scale" + scale);
console.log("translation" + translation);
d3.select("g")
.attr("transform", "translate(" + translation + ")" +
" scale(" + scale + ")");
}
If you expand and collapse nodes and then try to drag, the root node always goes back to the top left corner. I added some logging that shows that the value of translation in this case is -1,-1
Is there a way I can preserve the current root node position?
The problem is that the g element you're translating with the zoom behaviour is initialised with a non-zero translation. The zoom behaviour is not aware of this, resulting in the "jump" you see. To fix, initialise the zoom behaviour with that translation.
var zb = d3.behavior.zoom().scaleExtent([0.5, 5]).on("zoom", function () {
zoom();
});
zb.translate([margin.left, margin.top]);
Complete example here.

Using the zoom and pan functionality of d3

I'm trying to use the pan/zoom ability of d3 to draw boxes on the screen so that when you click on a box a new box appears and shifts the rest of the boxes to the right so that the new box is on the center of the canvas. The panning would allow me to scroll through all the boxes I've drawn.
Here is my jsfiddle: http://jsfiddle.net/uUTBE/1/
And here is my code for initializing the zoom/pan:
svg.call(d3.behavior.zoom().on("zoom", redraw));
function redraw() {
d3.select(".canvas").attr("transform",
"translate(" + d3.event.translate + ")"
+ " scale(" + d3.event.scale + ")");
}
And here is my code for drawing the boxes:
function drawBox(x, y) {
var boxGroup = canvas.append("g");
boxGroup.append("rect")
.attr("x", x)
.attr("y", y)
.attr("height", 100)
.attr("width", 100)
.attr("fill", function () {
var i = Math.floor(Math.random() * 4);
if (i === 1) return "red";
else if (i === 2) return "blue";
else if (i === 3) return "yellow";
else return "green";
})
.on("click", function () {
counter++;
d3.select(".canvas")
.transition()
.duration(1000)
.attr('transform', "translate(300,0)");
drawBox(x - counter * 120, y);
});
}
I have multiple problems with this fiddle, but two of my main concerns is:
1) How do I make it so that when I click on a new box a second time the boxes move accordingly (i.e. when I click on the box initially the old box shifts to the right and a new box appears, but when I click on the new box, the older boxes doesnn't shift to the right).
2)Why is it that when I click on the new box, the newer box has a big spacing between it? (only happens after trying to put 3 boxes on the screen).
Thanks any hints are appreciated!
I think there's some confusion here around transform. The transform attribute is static, not cumulative, for a single element - so setting .attr('transform', "translate(300,0)") more than once will have no effect after the first time. It also looks like your placement logic for the new boxes is off.
The positioning logic required here is pretty straightforward if you take a step back (assuming I understand what you're trying to do):
Every time a new box is added, the frame all boxes are in moves right 120px, so it needs a x-translation of 120 * counter.
New boxes need to be offset from the new frame position, so they need an x setting of -120 * counter.
Zoom needs to take the current canvas offset into account.
(1) above can be done in your click handler:
canvas
.transition()
.duration(1000)
.attr('transform', "translate(" + (offset * counter) + ",0)");
(2) is pretty easily applied to the g element you're wrapping boxes in:
var boxGroup = canvas.append("g")
.attr('transform', 'translate(' + (-offset * counter) + ',0)');
(3) can be added to your redraw handler:
function redraw() {
var translation = d3.event.translate,
newx = translation[0] + offset * counter,
newy = translation[1];
canvas.attr("transform",
"translate(" + newx + "," + newy + ")" + " scale(" + d3.event.scale + ")");
}
See the working fiddle here: http://jsfiddle.net/nrabinowitz/p3m8A/

Resources