D3 scale and translate is off target - javascript

I'm using my mouse click position to zoom and pan to a specific part of a map. Without scale the translate moves to the correct spot. However, if I scale and translate together it misses the click spot. I understand why but I don't understand how to compensate for it:
//get mouse interaction in svg path
let clickPoint = d3.mouse(this);
if (!d) {
//reset center and zoom level back to sart
projection.translate([width/2,height/2]).scale(scaleDefault);
centered = null;
scaleStartLevel = 1;
} else {
let translate = projection.translate();
//calc scale if the user hasn't zoomed to max zoom more
let getScale = scaleStartLevel < scaleMaxLevels ? projection.scale() + (scaleInc*scaleStartLevel) : projection.scale();
scaleStartLevel++;
//scale the map to the zoom level and translate to click point
projection.scale(getScale)
.translate([
translate[0] - clickPoint[0] + width / 2,
translate[1] - clickPoint[1] + height / 2
]);
centered = d;
}
//move the map
map.selectAll("path").transition()
.duration(750)
.attr("d", path);

Related

panning and zooming with scrollbars in d3 v3

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);
}

Combining a selection-rectangle with zoom + pan in a D3.js 4.x Force Simulation

I've built a Force Simulation in D3.js 4.4 that uses the built in zoom + pan functions (d3.zoom()), combined with a selection-rectangle for selecting nodes (like here or here).
Dragging/panning the viewport around is the default behaviour, but is filtered out when the Shift-key is held. Then another drag-handler takes over and draws a rectangle from the starting point. On mouseup the code intersects the rectangle with the nodes on screen and selects them.
This all works fine, but the whole thing goes sideways when I apply zoom behavior to the equation. I've pinpointed the problem to the scale values d3.event.transform applies to the g-container that contains the nodes.
Due to the nature of this project (and the internet-less dev environment we're working in) I can't copy/paste the working code on the web. So here are the abridged parts that are causing the problem (I had to re-type these from another monitor).
var currentZoom = null;
var zoom = d3.zoom()
.scaleExtent([0.5, 10])
.filter(function() {
return !d3.event.shiftKey // Don't zoom/pan when shift is pressed
})
.on("zoom", zoomed);
function zoomed() {
currentZoom = d3.event.transform;
d3.select("g.links").attr("transform", d3.event.transform);
d3.select("g.nodes").attr("transform", d3.event.transform);
}
// This fires on mouseup after dragging the rectangle (svg rect)
function intersectSelectionRect() {
var rect = svg.select("rect.selection-rect");
// Intersect rectangle with nodes
var lx = parseInt(rect.attr("x"));
var ly = parseInt(rect.attr("y"));
var lw = parseInt(rect.attr("width"));
var lh = parseInt(rect.attr("height"));
// Account for zoom
if (!!currentZoom) {
// Recalculate for pan
lx -= currentZoom.x;
ly -= currentZoom.y;
// Recalculate for zoom (scale)
// currentZoom.k ???????????????
}
// Rectangle corners
var upperRight = [lx + lw, ly];
var lowerLeft = [lx + ly + lh];
nodes.forEach(function(item, index) {
if ((item.x < upperRight[0] && item.x > lowerLeft[0]) &&
(item.y > upperRight[1] && item.y < lowerLeft[1])) {
item.selected = true;
}
});
}
As you can (hopefully) make out, the problem occurs when recalculating the rect x and y with the zoom k (scale). If the scale is 1 (starting value) the recalculation is correct. If it's anything other than that the actual selection rectangle no longer matches the one drawn on screen.
If everything is setup properly you should be able to apply the transform to the points,
var upperRight = [lx, ly];
var lowerLeft = [lx + lw, ly + lh];
if (!!currentZoom) {
upperRight = currentZoom.apply(upperRight);
lowerLeft = currentZoom.apply(lowerLeft);
}

d3.js version 4 - rect zoom - cannot get the transformation right

d3.js verison 4:
I have a line chart, which should have a rectangle zoom.
I used this example: http://bl.ocks.org/jasondavies/3689931
I don't want to apply the rectangle data to the scales, like in the example
Instead I want to apply this to my normal zoom Element.
For that I have the math:
.on("mouseup.zoomRect", function() {
d3.select(window).on("mousemove.zoomRect", null).on("mouseup.zoomRect", null);
d3.select("body").classed("noselect", false);
var m = d3.mouse(e);
m[0] = Math.max(0, Math.min(width, m[0]));
m[1] = Math.max(0, Math.min(height, m[1]));
if (m[0] !== origin[0] && m[1] !== origin[1]) {
//different code here
//I have the scale factor
var zoomRectWidth = Math.abs(m[0] - origin[0]);
scaleFactor = width / zoomRectWidth;
//Getting the translation
var translateX = Math.min(m[0], origin[0]);
//Apply to __zoom Element
var t = d3.zoomTransform(e.node());
e.transition()
.duration(that.chart.animationDuration)
.call(that.chart.zoomX.transform, t
.translate(translateX, 0)
.scale(scaleFactor)
.translate(-translateX, 0)
);
}
rect.remove();
refresh();
}, true);
So I actually get the scaleFactor right and it zooms in smoothly.
Only problem is, that I don't seem to get the translation correct.
So it zooms in to the wrong position.
So, now I got it right:
All transformations by earlier zooms need to be undone.
so that k = 1, x = 0, y = 0;
This is the d3.zoomIdentity.
From that point the current zoom needs to be applied and afterwards the translation.
After that the old transform needs to be applied, first translate and then scale
var t = d3.zoomTransform(e.node());
//store x translation
var x = t.x;
//store scaling factor
var k = t.k;
//apply first rect zoom scale and then translation
//then old translate and old zoom scale
e.transition()
.call(that.chart.zoomX.transform, d3.zoomIdentity
.scale(scaleFactor)
.translate(-translateX, 0)
.translate(x)
.scale(k)
);
Working Fiddle only for X-Axis here: https://jsfiddle.net/9j4kqq1v/3/
Working fiddle for X and Y-axis here: https://jsfiddle.net/9j4kqq1v/5/

How to keep current zoom scale and translate level when re-rendering graph

I have a d3 graph that allows for pan and zoom behavior. It also has subGraph functionality that allows a user to expand and collapse the subGraph. When the user clicks on the icon to open and close the subGraph this code runs:
btn.on("click", function(d:any, i:any) {
parentNode = nodeObject; // gives me he x and y coordinates of the parent node
d3.event["stopPropagation"]();
e["subGraph"].expand = !expand; // add or remove subGraph
window.resetGraph = !window.resetGraph;
console.log(window.resetGraph);
if (window.resetGraph) {
window.scale = window.zoom.scale();
window.translate = window.zoom.translate();
window.zoom.scale(1).translate([0 , 0]);
} else {
window.zoom.scale(window.scale).translate(window.translate);
}
console.log(window.zoom.scale());
console.log(translate);
renderGraph();
});
I'm essentially adding and removing the subGraph property of the node on click of the icon. Then redrawing the graph completely.
I can get the x and y coordinates of the parent container, but if I am re-rendering the graph, how can I go about rendering the graph so that the graph stays at the same scale and translation as it was when I clicked on the toggle subGraph icon. I am trying to redraw the graph at the same position as it was before just with the subGraph either expanded or collapsed. Below is the code that seems to be fighting me when the graph renders:
var scaleGraph = function() {
var graphWidth = g.graph().width + 4;
var graphHeight = g.graph().height + 4;
var width = parseInt(svg.style("width").replace(/px/, ""), 10);
var height = parseInt(svg.style("height").replace(/px/, ""), 10);
var zoomScale = originalZoomScale;
// Zoom and scale to fit
if (ctrl.autoResizeGraph === "original") {
zoomScale = initialZoomScale;
}
translate = [(width / 2) - ((graphWidth * zoomScale) / 2) + 2, 1];
zoom.center([width / 2, height / 2]);
zoom.size([width, height]);
zoom.translate(translate);
zoom.scale(zoomScale);
zoom.event(svg);
};
If I check if the toggle Icon has been clicked, can I redraw the graph with the same scale and translation as before it was redrawn?
Thanks
I just solved it by keeping track of the current scale and translation values:
zoom.on("zoom", function() {
svgGroup.attr("transform", "translate(" + (<any>d3.event).translate + ")" + "scale(" + (<any>d3.event).scale + ")");
// keep track of the currentZoomScale and currentPosition at all times
currentZoomScale = (<any>d3.event).scale;
currentPosition = (<any>d3.event).translate;
});
Then when re-rendering the graph in my scaleGraph function above I just checked to see if the graph was re-rendered because of the subGraph being toggled, if it has then I use the current scale and translation values:
if (ctrl.autoResizeGraph !== "original" && togglingSubGraph) {
zoomScale = currentZoomScale; // render the graph at its current scale
translate = currentPosition; // render the graph at its current position
}
I have created one fiddle for you.
fiddle
Please check the function refreshGraph
var refreshGraph =function(){
window.resetGraph = !window.resetGraph;
console.log(window.resetGraph);
if(window.resetGraph){
window.scale = window.zoom.scale();
window.translate = window.zoom.translate();
window.zoom.scale(1).translate([0,0]);
}else{
window.zoom.scale(window.scale)
.translate(window.translate);
}
console.log(window.zoom.scale());
console.log(translate);
draw();
}
I have used window.zoom (used window to let you know that this is shared)
Button reset toggles the zoom level to 0 scale and toggles back to where it was if clicked again.
Hope it helps.

simple zoom in d3.js

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)" );
}

Categories