I'm attempting to make an interactive pan/zoom SVG floorplan/map using the d3.behavior.zoom() functionality. I have based my code loosely on Zoom to Bounding Box II.
I am asynchronously loading a svg via $.get() then using a button.on('click') handler to get the .getBBox() of a specific <g> by element ID, #g-1101 (represented as a red circle on the svg). I then would like to center the viewport of the svg to the middle of #g-1101's bounding box.
As a cursory try I was just trying to translate the top-level svg > g by using g#1101's .getBBox().x && .getBBox().y. It seems to me my math is off.
I've tried incorporating the (svg.node().getBBox().width / 2) - scale * g.getBBox().x) to center the middle point of the bounding box to viewport but it's translation is not even in the ballpark.
Code
(function(){
var svg, g; $.get('http://layersofcomplexity.com/__fp.svg', function(svg){
$('body').append($(svg));
init();
},'text');
function init() {
console.log('init');
svg = d3.select('svg');
g = d3.select('svg > g');
var zoom = d3.behavior.zoom()
.translate([0, 0])
.scale(1)
.scaleExtent([1, 8])
.on("zoom", zoomed);
svg
.call(zoom)
.call(zoom.event);
$('.pan').on('click', function(){
// var id = '#g-1011';
var scale = 4;
var bbox = $('#g-1101')[0].getBBox();
// d3.select(id).node().getBBox();
var x = bbox.x;
var y = bbox.y;
// var scale = .9 / Math.max(dx / width, dy / height),
// var translate = [width / 2 - scale * x, height / 2 - scale * y];
var width = svg.node().getBBox().width;
var height = svg.node().getBBox().height;
var translate = [-scale*x,-scale*y];
g.transition().duration(750) .call(zoom.translate(translate).scale(scale).event);
});
}
function zoomed() {
g.attr("transform", "translate(" + d3.event.translate + ")scale(" + d3.event.scale + ")");
}
})();
-- EDIT JSBin was broken --
What am I missing? JSBin.
One small change in your code to center the marked in red g#g-1101
var bbox = $('#g-1101')[0].getBBox();
var x = bbox.x-((bbox.width));
var y = bbox.y-((bbox.height));
//scale should be 1 less
var translate = [-(x*(scale-1)),-(y*(scale-1))];
working code here
Hope this helps
Related
I am trying to implement panning, zooming and scrolling with scroll bars.
I have two problems with zoom,pan and scroll bars combination:
1.pan/drag the svg is working with moving scrollbars, when you pan/drag the chart->scroll with scrollbars->pan/drag with cursor on svg, It goes to old position where last pan/drag is performed.
2.Scrollbar with zoom is not working wonders, when you zoom->scroll->zoom it zooms at the old location where the first zoom happened.
how to implement this lines "wrapper.call(d3.zoom().translateTo, x / scale, y / scale);" in d3.v3. It would be great help if anyone shows the solution to implement in d3 version 3. below code is for zooming, panning and scrolling.
function zoomed() {
var scale = d3.event.scale;
const scaledWidth = (newWidth+1000) * scale;
const scaledHeight = height * scale;
// Change SVG dimensions.
d3.select('#hierarchyChart svg')
.attr('width', scaledWidth)
.attr('height', scaledHeight);
// Scale the image itself.
d3.select('#hierarchyChart svg g').attr('transform', "scale("+scale+")");
// Move scrollbars.
const wrapper = d3.select('#hierarchyChart')[0];
if(d3.event.translate >= [0,0] && d3.event.translate <=[scaledWidth,scaledHeight]){
wrapper[0].scrollLeft = -d3.event.translate[0];
wrapper[0].scrollTop = -d3.event.translate[1];
}
console.log(wrapper[0].scrollLeft);
console.log(wrapper[0].scrollTop);
// If the image is smaller than the wrapper, move the image towards the
// center of the wrapper.
const dx = d3.max([0, wrapper[0].clientWidth/ 2 - scaledWidth / 2]);
const dy = d3.max([0, wrapper[0].clientHeight / 2 - scaledHeight / 2]);
// d3.select('svg').attr('transform', "translate(" + dx + "," + dy + ")");
}
function scrolled() {
const x = wrapper[0].scrollLeft + wrapper[0].clientWidth / 2;
const y = wrapper[0].scrollTop + wrapper[0].clientHeight / 2;
const scale = d3.event.scale;
// Update zoom parameters based on scrollbar positions.
// wrapper.call(d3.zoom().translateTo, x / scale, y / scale);
}
I have a d3.js force layout with some tweaks to the force calculations. As I'm working with large datasets, sometimes the graph is partly or entirely outside of the viewport. I'd like to add a command to rescale and center the graph to be inside the viewport, but having some trouble with that.
what works:
I have a canvas and a viewport onto it:
this.svg_canvas = d3.select("#" + this.container_id)
.append("svg")
.attr("width", this.width)
.attr("height", this.height)
.call(this.zoom_behavior.bind(this))
;
this.viewport = this.svg_canvas.append("g")
.attr("id", "viewport")
;
I have a zoom behavior that scales and translates the viewport:
this.zoom_behavior = d3.behavior.zoom()
.scaleExtent([GraphView.MIN_ZOOM, GraphView.MAX_ZOOM])
.on('zoom', this._handleZoom.bind(this))
;
GraphView.prototype._handleZoom = function() {
var translate = d3.event.translate;
var scale = d3.event.scale;
this.viewport.attr("transform",
"translate(" + translate + ") " +
"scale(" + scale + ")");
};
All of that works as it should.
what doesn't work
I added a "recenter and scale" method that is supposed to perform the scaling and translation to bring the graph onto the viewport. The way it is supposed to work is that it first finds the extent of the graph (via my boundingBox() method), then call zoom.behavior with the appropriate scaling and translation arguments:
GraphView.prototype.recenterAndScale = function(nodes) {
var bounding_box = this._boundingBox(nodes || this.force.nodes());
var viewport = this.zoom_behavior.size(); // viewport [width, height]
var tx = viewport[0]/2 - bounding_box.x0;
var ty = viewport[1]/2 - bounding_box.y0;
var scale = Math.min(viewport[0]/bounding_box.dx, viewport[1]/bounding_box.dy);
this.zoom_behavior.translate([tx, ty])
.scale(scale)
.event(this.svg_canvas)
;
};
This doesn't work. It (usually) locates the graph the edge of my viewport. Perhaps I'm using the wrong reference for something. Is there an online example of how to do this "properly" (using the d3 idioms)?
For completeness, here's my definition of boundingBox() -- it returns the geometric center and extent of the nodes in the graph. As far as I can tell, this is working properly:
// Return {dx:, dy:, x0:, y0:} for the given nodes. If no nodes
// are given, return {dx: 0, dy: 0, x0: 0, y0: 0}
GraphView.prototype._boundingBox = function(nodes) {
if (nodes.length === 0) {
return {dx: 0, dy: 0, x0: 0, y0: 0};
} else {
var min_x = Number.MAX_VALUE;
var min_y = Number.MAX_VALUE;
var max_x = -Number.MAX_VALUE;
var max_y = -Number.MAX_VALUE;
nodes.forEach(function(node) {
if (node.x < min_x) min_x = node.x;
if (node.x > max_x) max_x = node.x;
if (node.y < min_y) min_y = node.y;
if (node.y > max_y) max_y = node.y;
});
return {dx: max_x - min_x,
dy: max_y - min_y,
x0: (max_x + min_x) / 2.0,
y0: (max_y + min_y) / 2.0
};
}
}
This was actually pretty easy: I needed to apply the effects of scaling when computing the translation. The corrected recenterAndScale() method is:
GraphView.prototype.recenterAndScale = function(nodes) {
var bbox = this._boundingBox(nodes || this.force.nodes());
var viewport = this.zoom_behavior.size(); // => [width, height]
var scale = Math.min(viewport[0]/bbox.dx, viewport[1]/bbox.dy);
var tx = viewport[0]/2 - bbox.x0 * scale; // <<< account for scale
var ty = viewport[1]/2 - bbox.y0 * scale; // <<< account for scale
this.zoom_behavior.translate([tx, ty])
.scale(scale)
.event(this.svg_canvas)
;
};
I have a zoom event handler on my tree graph like so:
d3.select("#"+canvasId+" svg")
.call(d3.behavior.zoom()
.scaleExtent([0.05, 5])
.on("zoom", zoom));
Which calls the zoom function which handles the translation bounding logic:
function zoom() {
console.log(d3.event.translate[0]);
var wcanvas = $("#"+canvasId+" svg").width();
var hcanvas = $("#"+canvasId+" svg").height();
var displayedWidth = w*scale;
var scale = d3.event.scale;
var h = d3.select("#"+canvasId+" svg g").node().getBBox().height*scale;
var w = d3.select("#"+canvasId+" svg g").node().getBBox().width*scale;
var padding = 100;
var translation = d3.event.translate;
var tbound = -(h-hcanvas)-padding;
var bbound = padding;
var lbound = -(w-wcanvas)-padding;
var rbound = padding;
// limit translation to thresholds
translation = [
Math.max(Math.min(translation[0], rbound), lbound),
Math.max(Math.min(translation[1], bbound), tbound)
];
console.log("Width: "+w*scale+" || Height: "+h*scale+" /// "+"Left: "+translation[0]+" || Top: "+translation[1]);
d3.select("#"+canvasId+" svg g")
.attr("transform", "translate(" + translation + ")" +" scale(" + scale + ")");
console.log(d3.select("#"+canvasId+" svg g")[0]);
}
However, translations beyond the bounds cause the d3.event.translate values to increase. The result is that even if the translation is not causing the graph to move as it has reached its limit for translation, the value for the translation within successive events can continue to increase.
The result is that say I drag the graph far to the left, even though it will stop moving past a certain point, because the value within the events continues to increase, I would then have to drag it a long way back right before it actually begins to move right again.
Is there a good way to prevent this behaviour?
Okay I worked it out. The trick is to set the translation for the d3.behaviour.zoom so that successive zoom pans start at the bounded translation rather than with the additional panning that didn't actually give any movement.
To do this, we declare the zoom behaviour as a separate variable and add it to our zoomable element:
var zoomBehaviour = d3.behavior.zoom()
.scaleExtent([0.05, 5])
.on("zoom", zoom)
d3.select("#"+canvasId+" svg")
.call(zoomBehaviour);
Then we set the translation of this zoomBehaviour to our bounded translation in the zoom function:
function zoom() {
...
translation = [
Math.max(Math.min(translation[0], rbound), lbound),
Math.max(Math.min(translation[1], bbound), tbound)
];
zoomBehaviour.translate(translation);
d3.select("#"+canvasId+" svg g")
.attr("transform", "translate(" + translation + ")" +" scale(" + scale + ")");
}
I am trying to implement a simple zoom in d3.js, simpler than all the examples I have gone through (I suppose) but it just doesn't wanna work. So, the functionality that I want to implement is: the user clicks on a section of the graph and that section zooms at a predefined fixed size in the centre of the chart; the user cannot zoom it any further, no panning either. And when the user clicks at any other section of the chart, the zoomed section translates back to its normal/original position.
var container = svg.append("g").classed("container-group", true);
container.attr({transform: "translate(" + 40*test_data.row + "," + 40*test_data.col + ")"});
container.call(d3.behavior.zoom().scaleExtent([1,5]).on("zoom", zoom));
function zoom() {
container.attr("transform","translate(" + d3.event.translate + ")scale(" + d3.event.scale + ")");
}
I have tried zoom.translate and zoom.size but couldn't get them right. And don't know how to reset the zoomed section either.
Any help would be much appreciated !
I´ll give an example of zooming some circles. Clicking on the red rectangle will zoom out to 50%, clicking on the blue one will return to a 100% scale. The exact functions you are looking for are zoomOut() and initialZoom()
var zoomListener = d3.behavior.zoom().scaleExtent([0.1, 3]);
width = 200 ;
height = 200 ;
//svg
var svg = d3.select("body").append("svg").attr("id","vis")
.attr("width", width )
.attr("height", height );
//transition listener group
var svgGroup = svg.append("g").call(zoomListener);
//zoom in and zoom out buttons
svg.append("rect").attr("x",0).attr("y",0).attr("width",50).attr("height",50).style("fill","red").on("click",zoomOut);
svg.append("rect").attr("x",0).attr("y",50).attr("width",50).attr("height",50).style("fill","blue").on("click",initialZoom);
var i,k;
for(i=90;i<width-20;i+=20){
for( k=20;k<height-20;k+=20){
svgGroup.append("circle").attr("cx", i).attr("cy", k).attr("r", 10);
}
}
function zoomOut(){
//fix transition to center of canvas
x = (width/2) * 0.5;
y = (height/2) * 0.5;
//zoom transition- scale value 150%
svgGroup.transition().duration(500).attr("transform", "translate("+x+","+y+")scale(0.5)" );
}
function initialZoom(){
//fix transition to center of canvas
x = (width/2) ;
y = (height/2) ;
//zoom transition- scale value 100%
svgGroup.transition().duration(500).attr("transform", "scale(1)" );
}
I have a map which has been translated to make it fit on the canvas properly.
I'm trying to implement a way to zoom it and it does work, but it moves away from center when you zoom in, rather than centering on the mouse or even the canvas.
This is my code:
function map(data, total_views) {
var xy = d3.geo.mercator().scale(4350),
path = d3.geo.path().projection(xy),
transX = -320,
transY = 648,
init = true;
var quantize = d3.scale.quantize()
.domain([0, total_views*2/Object.keys(data).length])
.range(d3.range(15).map(function(i) { return "map-colour-" + i; }));
var map = d3.select("#map")
.append("svg:g")
.attr("id", "gb-regions")
.attr("transform","translate("+transX+","+transY+")")
.call(d3.behavior.zoom().on("zoom", redraw));
d3.json(url_prefix + "map/regions.json", function(json) {
d3.select("#regions")
.selectAll("path")
.data(json.features)
.enter().append("svg:path")
.attr("d", path)
.attr("class", function(d) { return quantize(data[d.properties.fips]); });
});
function redraw() {
var trans = d3.event.translate;
var scale = d3.event.scale;
if (init) {
trans[0] += transX;
trans[1] += transY;
init = false;
}
console.log(trans);
map.attr("transform", "translate(" + trans + ")" + " scale(" + scale + ")");
}
}
I've found that adding the initial translation to the new translation (trans) works for the first zoom, but for all subsequent zooms it makes it worse. Any ideas?
Here's a comprehensive starting-point: semantic zooming of force directed graph in d3
And this example helped me specifically (just rip out all the minimap stuff to make it simpler): http://codepen.io/billdwhite/pen/lCAdi?editors=001
var zoomHandler = function(newScale) {
if (!zoomEnabled) { return; }
if (d3.event) {
scale = d3.event.scale;
} else {
scale = newScale;
}
if (dragEnabled) {
var tbound = -height * scale,
bbound = height * scale,
lbound = -width * scale,
rbound = width * scale;
// limit translation to thresholds
translation = d3.event ? d3.event.translate : [0, 0];
translation = [
Math.max(Math.min(translation[0], rbound), lbound),
Math.max(Math.min(translation[1], bbound), tbound)
];
}
d3.select(".panCanvas, .panCanvas .bg")
.attr("transform", "translate(" + translation + ")" + " scale(" + scale + ")");
minimap.scale(scale).render();
}; // startoff zoomed in a bit to show pan/zoom rectangle
Though I had to tweak that function a fair bit to get it working for my case, but the idea is there. Here's part of mine. (E.range(min,max,value) just limits value to be within the min/max. The changes are mostly because I'm treating 0,0 as the center of the screen in this case.
// limit translation to thresholds
var offw = width/2*scale;
var offh = height/2*scale;
var sw = width*scale/2 - zoomPadding;
var sh = height*scale/2- zoomPadding;
translate = d3.event ? d3.event.translate : [0, 0];
translate = [
E.range(-sw,(width+sw), translate[0]+offw),
E.range(-sh,(height+sh), translate[1]+offh)
];
}
var ts = [translate[0], translate[1]];
var msvg = [scale, 0, 0, scale, ts[0], ts[1]];