d3.js limit panning in force layout - javascript

I am using d3.js with a force layout to visualize a large number of nodes. I would like to implement a limitation to the panning option of the zoom.
JSFiddle : https://jsfiddle.net/40z5tw8h/24/
The above fiddle contains a simple version of what I am working on.
Because I would potentially have to visualize a very large dataset, I use a function to scale down the group holding element ('g') after forces are done. In that way i always have the full visualization visible afterwards.
I would like to limit the panning - when the graph is fully visible, to only be able to move it within the viewport.
In case the layout is zoomed, I would like to limit the panning as follows:
The group holding element should not be able to go:
down more than 20 px from the top of the svg.
right more than 20 px from the left side of the svg.
up more than 20 px from the bottom of the svg.
left more than 20 px from the right side of the svg.
I think all the implementation should be within the zoom function, which for now is:
function zoomed(){
if (d3.event.sourceEvent == null){ //when fitFullGraph uses the zoom
g.attr("transform", "translate(" + d3.event.translate + ")scale(" + d3.event.scale + ")");
}
else{
var gElementBounds = g.node().getBoundingClientRect();
var g_bottom = gElementBounds.bottom;
var g_top = gElementBounds.top;
var g_left = gElementBounds.left;
var g_right = gElementBounds.right;
var g_height = gElementBounds.height;
var g_width = gElementBounds.width;
var svg = g.node().parentElement;
var svgElementBounds = svg.getBoundingClientRect();
var svg_bottom = svgElementBounds.bottom;
var svg_top = svgElementBounds.top;
var svg_left = svgElementBounds.left;
var svg_right = svgElementBounds.right;
var svg_height = svgElementBounds.height;
var svg_width = svgElementBounds.width;
var t = d3.event.translate;
var margin = 20;
if(d3.event.sourceEvent.type == 'wheel'){//event is zoom
g.attr("transform", "translate(" + d3.event.translate + ")scale(" + d3.event.scale + ")");
}
else{//event is pan
// if(t[0] < svg_left + margin) t[0]= svg_left + margin;
//else if(t[0] > svg_width-g_width - margin) t[0] = svg_width-g_width - margin;
// if(t[1] < g_height +margin) t[1] = g_height + margin;
//else if (t[1] > svg_height - margin) t[1] = svg_height - margin;
//.attr("transform", "translate(" + t+ ")scale(" + d3.event.scale + ")");
//3.event.translate = t;
g.attr("transform", "translate(" + d3.event.translate + ")scale(" + d3.event.scale + ")");
}
}
}
The limitations I tried to implement are commented out, because they do not work properly.
Does anyone have a solution?

This is not the complete answer to your question.
I used for block panning to left side translate X scale
var translate = d3.event.translate;
var translateX = translate[0];
var translateY = translate[1];
var scale = d3.event.scale;
var tX = translateX * scale;
var tY = translateY * scale;
console.log('tx', tX, 'ty', tY);
// Do not pan more to left
if (tX> 0) {
g.attr("transform", "translate(" + d3.event.translate + ") scale(" + d3.event.scale + ")");
} else {
translate[0] = 0;
g.attr("transform", "translate(" + translate + ") scale(" + d3.event.scale + ")");
}
Which cancels the translation to left but internally it continues. Your user probably stops dragging to the left. Panning to the right gets weird when starting to pan as internally the event has panned far to the left.

Related

D3: keeping position of SVG's relative while panning and zooming

I'm trying to do a proof of concept of a SVG floorplan that pans and zooms and also have the ability to place markers on top. When zooming/panning happens the marker doesn't stay in position. I understand why this happens but not sure about the best way to keep the marker in position when panning/zooming.
Heres the code:
var svg = d3.select(".floorplan")
.attr("width", "100%")
.attr("height", "100%")
.call(d3.zoom().on("zoom", zoomed))
.select("g")
var marker = d3.selectAll(".marker")
.call(d3.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended)
)
function zoomed() {
svg.attr("transform", d3.event.transform);
}
function dragstarted(d) {
console.log('dragstarted');
}
function dragged(d) {
var x = d3.event.x;
var y = d3.event.y;
d3.select(this).attr("transform", "translate(" + x + "," + y + ")");
}
function dragended(d) {
console.log('drag ended: marker:'+ d3.select(this).attr('data-id') + ' position: ' + d3.event.x +', ' + d3.event.y);
}
Theres also a codepen to visually see here: https://codepen.io/danielhoff/pen/WzQbRr
I have an additional constraints that the marker element shouldn't be contained inside the floorplan svg.
Here is a modified version of your codepen which fixes the movements of the marker during drag events while keeping the marker outside the floorplan svg container:
https://codepen.io/xavierguihot/pen/OvyRPY?editors=0010
To bring back into context, an easy solution would have been to include the marker element inside the floorplan container (in order for the marker to get the same zoom events as the floorplan), but here we want the marker to be in its own svg container.
And it is not trivial!
Appart from including ids in html tags (in order to select these elements from the html), only the javascript part has ben modified.
Let's dig a little bit on the steps necessary to get to this point:
First: Let's modify the zoomed function to apply to the marker as well:
Initially this was the zoom function:
function zoomed() {
svg.attr("transform", d3.event.transform);
}
And the modified version:
function zoomed() {
// Before zooming the floor, let's find the previous scale of the floor:
var curFloor = document.getElementById('floorplan');
var curFloorScale = 1;
if (curFloor.getAttribute("transform")) {
var curFloorTransf = getTransformation(curFloor.getAttribute("transform"));
curFloorScale = curFloorTransf.scaleX;
}
// Let's apply the zoom
svg.attr("transform", d3.event.transform);
// And let's now find the new scale of the floor:
var newFloorTransf = getTransformation(curFloor.getAttribute("transform"));
var newFloorScale = newFloorTransf.scaleX;
// This way we get the diff of scale applied to the floor, which we'll apply to the marker:
var dscale = newFloorScale - curFloorScale;
// Then let's find the current x, y coordinates of the marker:
var marker = document.getElementById('Layer_1');
var currentTransf = getTransformation(marker.getAttribute("transform"));
var currentx = currentTransf.translateX;
var currenty = currentTransf.translateY;
// And the position of the mouse:
var center = d3.mouse(marker);
// In order to find out the distance between the mouse and the marker:
// (43 is based on the size of the marker)
var dx = currentx - center[0] + 43;
var dy = currenty - center[1];
// Which allows us to find out the exact place of the new x, y coordinates of the marker after the zoom:
// 38.5 and 39.8 comes from the ratio between the size of the floor container and the marker container.
// "/2" comes (I think) from the fact that the floor container is initially translated at the center of the screen:
var newx = currentx + dx * dscale / (38.5/2);
var newy = currenty + dy * dscale / (39.8/2);
// And we can finally apply the translation/scale of the marker!:
d3.selectAll(".marker").attr("transform", "translate(" + newx + "," + newy + ") scale(" + d3.event.transform.k + ")");
}
This heavily uses the getTransformation function which allows to retrieve the current transform details of an element.
Then: But now, after having zoomed, when we drag the marker, it takes back its original size:
This means we have to tweak the marker's dragg function to keep its current scale when applying the drag transform:
Here was the initial drag function:
function dragged(d) {
var x = d3.event.x;
var y = d3.event.y;
d3.select(this).attr("transform", "translate(" + x + "," + y + ")");
}
And its modified version:
function draggedMarker(d) {
var x = d3.event.x;
var y = d3.event.y;
// As we want to keep the same current scale of the marker during the transform, let's find out the current scale of the marker:
var marker = document.getElementById('Layer_1');
var curScale = 1;
if (marker.getAttribute("transform")) {
curScale = getTransformation(marker.getAttribute("transform")).scaleX;
}
// We can thus apply the translate And keep the current scale:
d3.select(this).attr("transform", "translate(" + x + "," + y + "), scale(" + curScale + ")");
}
Finally: When dragging the floor we also have to drag the marker accordingly:
We thus have to override the default dragging of the floor in order to include the same dragg event to the marker.
Here is the drag function applied to the floor:
function draggedFloor(d) {
// Overriding the floor drag to do the exact same thing as the default drag behaviour^^:
var dx = d3.event.dx;
var dy = d3.event.dy;
var curFloor = document.getElementById('svg-floor');
var curScale = 1;
var curx = 0;
var cury = 0;
if (curFloor.getAttribute("transform")) {
curScale = getTransformation(curFloor.getAttribute("transform")).scaleX;
curx = getTransformation(curFloor.getAttribute("transform")).translateX;
cury = getTransformation(curFloor.getAttribute("transform")).translateY;
}
d3.select(this).attr("transform", "translate(" + (curx + dx) + "," + (cury + dy) + ")");
// We had to override the floor drag in order to include in the same method the drag of the marker:
var marker = document.getElementById('Layer_1');
var currentTransf = getTransformation(marker.getAttribute("transform"));
var currentx = currentTransf.translateX;
var currenty = currentTransf.translateY;
var currentScale = currentTransf.scaleX;
d3.selectAll(".marker").attr("transform", "translate(" + (currentx + dx) + "," + (currenty + dy) + ") scale(" + currentScale + ")");
}

Dynamically Centering Filtered Node within SVG Element in D3

I am working to add filter functionality to my d3 graph. When the user searches for a specific node based on label or id, I want to re-render the graph and show the entire graph again but I want the filtered node to sit in the center of the svg element.
here is what I have the helped it to be centered:
// I get the width and height of the SVG element:
var svgWidth = parseInt(svg.style("width").replace(/px/, ""), 10);
var svgHeight = parseInt(svg.style("height").replace(/px/, ""), 10);
// I get the center of the svg:
var centerX = svgWidth / 2;
var centerY = svgHeight / 2;
_.forEach(nodes, function(e) {
// get the full node (with x and y coordinates) based on the id
var nodeObject = g.node(nodeId);
// I look for matches between the nodeId or label and search word
if (searchInput) {
if (nodeObject.id === parseInt(searchInput, 10) || nodeObject.label.toUpperCase().indexOf(searchInput.toUpperCase()) > -1) {
searchedNodes.push(nodeObject);
console.log(searchedNodes);
}
}
}
// after looping through all the nodes rendered
if (searchedNodes.length > 0) {
//var width = searchedNodes[0].elem.getBBox().width;
//var height = searchedNodes[0].elem.getBBox().height;
ctrl.selectedNode = searchedNodes[0];
var offsetX = centerX - searchedNodes[0].x;
var offsetY = centerY - searchedNodes[0].y;
svgGroup.attr("transform", "translate(" + offsetX + "," + offsetY + ")" + "scale(" + 3 + ")");
// this line here is incorrect syntax and breaks the build, essentially stopping the script from running
// the graph renders correctly when this line is here
svgGroup.attr("transform", "translate(" + offsetX + "," + offsetY + ")").scale(2).event;
}
This is what the graph looks like with the line above that breaks the script included.
When I removed that line, it doesn't center, almost looking like over-renders the graph. Obviously I will need to remove the line of code above that is incorrect but does anybody no why the graph doesn't render correctly in this case?:
// get the user input and re-render the graph
elem.find(".search").bind("keyup", function (e:any) {
var searchInput;
if (e["keyCode"] === 13) {
searchedNodes = [];
searchInput = scope["searchInput"];
currentFilteredNode = null;
enterKeyPressed = true;
renderGraph(searchInput);
}
if (e["keyCode"] === 8) {
searchedNodes = [];
searchInput = scope["searchInput"];
currentFilteredNode = null;
renderGraph(searchInput);
}
});
// if there is searchInput and at least one matching node sort the nodes
// by id and then select and center the first matching one
if (searchInput && searchedNodes.length > 0) {
searchedNodes.sort(function (node1:any, node2:any) {
return node1.id - node2.id;
});
// make sure the noResultsMessage does not get shown on the screen if there are matching results
scope.$apply(function() {
scope["noResultsMessage"] = false;
});
ctrl.selectedNode = searchedNodes[0];
offsetX = centerX - searchedNodes[0].x;
offsetY = centerY - searchedNodes[0].y;
svgGroup.attr("transform", "translate(" + offsetX + "," + offsetY + ")" + "scale(" + 3 + ")");
}
// the only other zoom and this runs just on page load
zoom = d3.behavior.zoom();
zoom.on("zoom", function() {
svgGroup.attr("transform", "translate(" + (<any>d3.event).translate + ")" + "scale(" + (<any>d3.event).scale + ")");
// this scales the graph - it runs on page load and whenever the user enters a search input, which re-renders the whole graph
var scaleGraph = function(useAnimation:any) {
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 === "disabled") {
zoomScale = 1;
} else {
// always scale to canvas if set to fill or if auto (when larger than canvas)
if (ctrl.autoResizeGraph === "fill" || (graphWidth > width || graphHeight > height)) {
zoomScale = Math.min(width / graphWidth, height / graphHeight);
}
}
var translate;
if (direction.toUpperCase() === "TB") {
// Center horizontal + align top (offset 1px)
translate = [(width / 2) - ((graphWidth * zoomScale) / 2) + 2, 1];
} else if (direction.toUpperCase() === "BT") {
// Center horizontal + align top (offset 1px)
translate = [(width / 2) - ((graphWidth * zoomScale) / 4) + 2, 1];
} else if (direction.toUpperCase() === "LR") {
// Center vertical (offset 1px)
translate = [1, (height / 2) - ((graphHeight * zoomScale) / 2)];
} else if (direction.toUpperCase() === "RL") {
// Center vertical (offset 1px)
translate = [1, (height / 2) - ((graphHeight * zoomScale) / 4)];
} else {
// Center horizontal and vertical
translate = [(width / 2) - ((graphWidth * zoomScale) / 2), (height / 2) - ((graphHeight * zoomScale) / 2)];
}
zoom.center([width / 2, height / 2]);
zoom.size([width, height]);
zoom.translate(translate);
zoom.scale(zoomScale);
// If rendering the first time, then don't use animation
zoom.event(useAnimation ? svg.transition().duration(500) : svg);
};
CODE FOR FILTERING THE NODES:
// move to the left of the searchedNodes array when the left arrow is clicked
scope["filterNodesLeft"] = function () {
filterNodesIndex--;
if (filterNodesIndex < 0) {
filterNodesIndex = searchedNodes.length - 1;
}
currentFilteredNode = searchedNodes[filterNodesIndex];
runScaleGraph = true;
number = 1;
renderGraph();
};
// move to the right of the searchNodes array when the right arrow is clicked
scope["filterNodesRight"] = function () {
filterNodesIndex++;
if (filterNodesIndex > searchedNodes.length - 1) {
filterNodesIndex = 0;
}
currentFilteredNode = searchedNodes[filterNodesIndex];
runScaleGraph = true;
number = 1;
renderGraph();
};
// get the current filteredNode in the searchNodes array and center it
// when the graph is re-rendered
if (currentFilteredNode) {
ctrl.selectedNode = currentFilteredNode;
offsetX = centerX - currentFilteredNode.x;
offsetY = centerY - currentFilteredNode.y;
svgGroup.attr("transform", "translate(" + offsetX + "," + offsetY + ")");
runScaleGraph = false;
}
You will want to find the x and y coordinates of your target node, and set the transform attribute of your group with class 'output' accordingly. You will also need to know the width and height of 'output' in order to position it such that your target node is in the center.
//when diagram is initially displayed
var output = d3.select('.output');
var bbox = output.getBBox();
var centerX = bbox.width * .5;
var centerY = bbox.height * .5;
//in your block where you find a node matches the filter
if (node.label.toUpperCase().indexOf(searchString.toUpperCase()) > -1) {
var offsetX = centerX - node.x;
var offsetY = centerY - node.y;
output.attr('transform', 'translate(' + offsetX + ',' + offsetY + ')');
}
Depending on the node's registration point, you may also need to take in to account the node's width and height to make sure we are directly centered on the node. For example, if the registration point is the top left of the node, you would want to add half the nodes width and half the nodes height to the offset.
-- Edit --
In the following line:
svgGroup.attr("transform", "translate(" + offsetX + "," + offsetY + ")" + "scale(" + 3 + ")");
by including "scale(" + 3 + ")" so you are scaling your entire graph - you are not 'zooming in' on the place you have centered, rather the content itself is bigger and so offsetX and offsetY are not the correct cordinates to center on.
The reason things look better when you add that other line, is that you are removing the scale.
svgGroup.attr("transform", "translate(" + offsetX + "," + offsetY + ")");
So, we are back to the default scale, immediately prior to your error being thrown.
If you want to scale, you'll need to multiply offsetX and offsetY by whatever you want to scale by.
If you do not want to scale, just remove
"scale(" + 3 + ")"
Here's how I solved it:
// zoom in on the searched or filtered node
function zoomOnNode (node:any) {
// get the width and height of the svg
var svgWidth = parseInt(svg.style("width").replace(/px/, ""), 10);
var svgHeight = parseInt(svg.style("height").replace(/px/, ""), 10);
// loop through all the rendered nodes (these nodes have x and y coordinates)
for (var i = 0; i < renderedNodes.length; i++) {
// if the first matching node passed into the function
// and the renderedNode's id match get the
// x and y coordinates from that rendered node and use it to calculate the svg transition
if (node.id === renderedNodes[i].id) {
var translate = [svgWidth / 2 - renderedNodes[i].x, svgHeight / 2 - renderedNodes[i].y];
var scale = 1;
svg.transition().duration(750).call(zoom.translate(translate).scale(scale).event);
}
}
}
// listen for the enter key press, get all matching nodes and pass in the first matching node in the array to the zoomOnNode function
elem.find(".search").bind("keyup", function (e:any) {
var searchInput;
if (e["keyCode"] === 13) {
searchedNodes = [];
searchInput = scope["searchInput"];
enterKeyPressed = true;
if (searchInput) {
// recursively get all matching nodes based on search input
getMatchingNodes(ctrl.nodes, searchInput);
scope.$apply(function() {
// show the toggle icons if searchedNodes.length is greater then 1
scope["matchingNodes"] = searchedNodes.length;
scope["noResultsMessage"] = false;
if (searchedNodes.length > 0) {
var firstNode = searchedNodes[0];
ctrl.selectedNode = firstNode;
zoomOnNode(firstNode);
} else if (searchedNodes.length === 0) {
ctrl.selectedNode = null;
// add the noResultsMessage to the screen
scope["noResultsMessage"] = true;
}
});
}
}
}

D3.js zoom and touch device smoothness

I am using d3.behaviour.zoom on my graph and all is well on the desktop. However, when i do it on an ipad the zooming is very choppy. Is there a way I can smooth it out? It seems like i need to cancel translate calls when zooming.
I have tried a couple of things but none to any great success.
an example is in the zoom handler i get the 2 touch point x and y then get the distance between then and depending if it shrinks or grows from the last stored distance i added or subtracted 0.05 to the d3.event.scale which is used in the transform.
Is this right or am i way off and need to take something else into consideration
Help much appreciated
Cheers
Mark
EDIT:
Thought i would include some code here seeing as i cannot use another service via works network
attaching the zoom handler
RadarDraw.ZoomListener = d3.behavior.zoom().scaleExtent([1, 5]).on("zoom", zoom);
// Create the SVG element, transforming the coordinates so (0,0) is at the centre
svg = d3.select("#radarContainer").append("svg")
.attr("viewBox", "0 0 " + _config.Width + " " + _config.Height + "")
.attr("id", "chartsvg")
.attr("width", _config.Width)
.attr("height", _config.Height)
//.call(zoom)
.append("g")
.attr("id", "svgGElm")
.attr("transform", "translate(" + _config.Width / 2 + "," + ((_config.Height - _config.Voffset) / 2 + _config.Voffset) + ")")
.call(_config.RadarType != "dash" ? RadarDraw.ZoomListener : function () { });
This is the zoom handler
function zoom(){
if (d3.event.scale <= 1 || d3.event.scale >= 5) {
if (!tools.IsArcFocused) {
svg.attr("transform", "translate("
+ (d3.event.translate[0] + (_config.Width / 2)) + "," + (d3.event.translate[1] + (_config.Height / 2))
+ ")scale(" + d3.event.scale + ")");
}
//handle the zoom of arc that occurs from dblclick. this moves to arc and centers it and zooms in into it.
if (tools.IsArcFocused) {
svg.attr("transform", "translate(" + _config.Width / 2 + "," + _config.Height / 2 + ")scale(" + 5 + ")translate("
+ -RadarDraw.transPosX + "," + -RadarDraw.transPosY + ")");
}
debugInfoBar(" scale: " + d3.event.scale
+ " last distance: " + lastDistance
+ " current distance: " + currentDistance);
//detect mouse wheel
if(d3.event.sourceEvent != null)
{
if (d3.event.sourceEvent.type=='mousewheel' || d3.event.sourceEvent.type=='wheel' || d3.event.sourceEvent.type=='DOMMouseScroll')
{
//if we are zoom in on an arc only listen to zoom out command to exit the zoom
//if wheeldelta is forward and in focus mode then ignore it
if (d3.event.sourceEvent.wheelDelta > 0 && tools.IsArcFocused)
{
//make sure dots stay smallest zoomed in size
scaleDots(5);
return;
}
if (d3.event.sourceEvent.wheelDelta < 0) {
if ((tools.IsArcFocused && d3.event.scale >= 5) || (tools.IsArcFocused && d3.event.scale <= 1)) {
tools.ExitZoom(false);
}
//reset to 0,0 scale 1 as we want to zoom out fully
if (d3.event.scale <= 1 && tools.IsArcFocused)
{
svg.attr("transform", "translate(" + _config.Width / 2 + "," + _config.Height / 2 + ")scale(" + 1 + ")");
}
}
}
if (d3.event.sourceEvent.type == "mousemove" && tools.IsArcFocused)
{
d3.event.scale = 5;
scaleDots(5);
return;
}
if (d3.event.sourceEvent.type == "touchmove")
{
//need to handle zoom out via touch (like whats done with mouse wheel)
return;
}
//deal with stopping double tap and double click events via the zoom
if (d3.event.sourceEvent.type == 'dblclick' || d3.event.sourceEvent.type == 'touchstart')
{
if (typeof (d3.event.preventDefault) == "function") {
d3.event.preventDefault();
d3.event.stopPropagation();
}
return;
}
}
scaleDots(d3.event.scale);
if(tools.IsArcFocused)
previousZoomLevel = 5;
else
previousZoomLevel = 1;
return;
} else {
if (tools.IsArcFocused) {
svg.attr("transform", "translate(" + _config.Width / 2 + "," + _config.Height / 2 + ")scale(" + 5 + ")translate("
+ -RadarDraw.transPosX + "," + -RadarDraw.transPosY + ")");
}
inTouchZoom = false;
//detect forward scroll when zoomed in so it exits zoom
if(d3.event.sourceEvent != null)
{
if (d3.event.sourceEvent.type == 'mousewheel' || d3.event.sourceEvent.type == 'wheel' || d3.event.sourceEvent.type == 'DOMMouseScroll') {
if (d3.event.sourceEvent.wheelDelta == 120)
{
d3.event.scale += 0.05;
if (d3.event.scale > 4.9)
d3.event.scale = 4.9;
}
else if (d3.event.sourceEvent.wheelDelta -= 120)
{
d3.event.scale -= 0.05;
if (d3.event.scale < 1)
d3.event.scale = 1;
}
//previousZoomLevel -gets confused sometimes and is 1 when it should be >=5
if (tools.IsArcFocused && previousZoomLevel >= 5) {
tools.ExitZoom(false);
previousZoomLevel = d3.event.scale;
}
}
if (d3.event.sourceEvent.type == "touchmove") {
//if only one touch point then do translation for pan otherwise leave as is
if (d3.event.sourceEvent.touches.length > 1) {
inTouchZoom = true;
//dont update translate use what was take before
d3.event.translate = touchZoomTranslate;
//we have atleast 2 points so use the first 2
var currentDistance = PointDistance(d3.event.sourceEvent.touches[0].pageX, d3.event.sourceEvent.touches[0].pageY, d3.event.sourceEvent.touches[1].pageX, d3.event.sourceEvent.touches[1].pageY);
debugInfoBar(" scale: " + d3.event.scale
+ " tp1 X: " + d3.event.sourceEvent.touches[0].pageX + " tp1Y: " + d3.event.sourceEvent.touches[0].pageY
+ " tp2 X: " + d3.event.sourceEvent.touches[1].pageX + " tp2Y: " + d3.event.sourceEvent.touches[1].pageY
+ " last distance: " + lastDistance
+ " current distance: " + currentDistance);
if (currentDistance > lastDistance) {
d3.event.scale += 0.05;
if(d3.event.scale > 4.9)
d3.event.scale = 4.9;
lastDistance = currentDistance;
}
else {
d3.event.scale -= 0.05;
if (d3.event.scale < 1)
d3.event.scale = 1;
lastDistance = currentDistance;
}
svg.attr("transform", "translate(" + _config.Width / 2 + "," + _config.Height / 2 + ")scale(" + d3.event.scale + ")");
return;
}
}
if (d3.event.sourceEvent.type == "touchend") {
//wipe last distance and we have finished touch
lastDistance = 0;
}
//deal with stopping double tap and double click events via the zoom
if (d3.event.sourceEvent.type == 'dblclick' || (d3.event.sourceEvent.type == 'touchstart' && !inTouchZoom))
{
if (typeof (d3.event.preventDefault) == "function") {
d3.event.preventDefault();
d3.event.stopPropagation();
}
return;
}
}
if (!tools.IsArcFocused) {
svg.attr("transform", "translate("
+ (d3.event.translate[0] + (_config.Width / 2)) + "," + (d3.event.translate[1] + (_config.Height / 2))
+ ")scale(" + d3.event.scale + ")");
}
if (d3.event.scale > 1)
previousZoom = true;
//if we are zoomed in and our previous zoom level is 5 or above then exit zoom (we are either scrolling out or pinching out of zoom)
if (tools.IsArcFocused && previousZoomLevel >= 5) {
tools.ExitZoom(false);
}
scaleDots(d3.event.scale);
}
if (d3.event.scale > 4.9)
d3.event.scale = 4.9;
if(!inTouchZoom)
touchZoomTranslate = d3.event.translate;
}
I corrected this issue by rewriting the zoom handler so it had specific touch input update, where I deal with zooming and not relying on d3. I also listened for a touchend event in zoom but it did not fire I had to attach another handler for zoomend and detect it that way
The zooming can be achieved without all the calculations.
var onZoom = function() {
var scale = d3.event.scale,
translate = d3.event.translate;
elemToZoom.attr('transform', 'scale(' + scale + ')translate(' + translate + ')');
};
var zoom = d3.behavior.zoom().on('zoom', onZoom);
var elemToZoom = d3.select('#zoomable')
.call(zoom);
When you are zooming in the graph, you may have to redraw the axes and the vertical and horizontal lines so that they don't get zoomed too.

Incorrect Positioning of Voronoi on Leaflet Map

i'm struggling with positioning and scaling of a voronoi diagram on a leaflet map.
the voronoi polygons are correctly displayed after they're appended on the map, but
after resizing they don't scale and translate correctly. i tried to reset the path after
panning and zooming to the feature element. but it looks like the new values are passed to their parent's element. If i set the path on feature.selectAll('path).attr('d',path) the scaling and translation is absolutly correct, but it show's the voronoi means instead the voronoi polygons. Any Idea?
Best regards,
Flo
this.id = p_id;
this.data = p_data;
d3.select('#'+this.id).remove();
var svg = d3.select(map.getPanes().overlayPane).append("svg").attr('id', this.id);
var g = svg.append("g").attr("class", "leaflet-zoom-hide").attr("id", "cells");
var pointdata = this.data.features;
var positions = [];
pointdata.forEach(function(d){
positions.push(project(d.geometry.coordinates));
});
var polygons = d3.geom.voronoi(positions);
var bounds = d3.geo.bounds(this.data),
path = d3.geo.path().projection(project);
var feature = g.selectAll('g').data(pointdata).enter().append('g');
feature.append('path')
.attr('class', 'cell')
.attr({
"d":function(d, i) { return "M" + polygons[i].join("L") + "Z"},
stroke:"#43676b",
fill: "rgba(255,140,10,0.3)"
});
map.on("viewreset", reset);
reset();
function reset() {
scale = Math.pow(2, map.getZoom() - map.options.zoom);
var padding = 25;
var bottomLeft = project(bounds[0]),
topRight = project(bounds[1]);
bottomLeft = [bottomLeft[0]-padding, bottomLeft[1]+padding]
topRight = [topRight[0]+padding, topRight[1]-padding]
console.log(polygons);
svg.attr("width", topRight[0] - bottomLeft[0]).attr("height", bottomLeft[1] - topRight[1]).style("margin-left", bottomLeft[0] + "px").style("margin-top", topRight[1] + "px");
g.attr("transform", "translate(" + -bottomLeft[0] + "," + -topRight[1] + ")");
feature.attr('d', path);
}
function project(x) {
var point = map.latLngToLayerPoint(new L.LatLng(x[1], x[0]));
return [point.x, point.y];
}

D3 selective Zoom

I am working on a force directed graph layout with some added features: selectable links/nodes, tooltips, fisheye effect, and -- important for my question -- zoom and pan.
Now, the zooming works very well like this:
d3 ... .append('svg:g').call(d3.behavior.zoom().on("zoom", redraw))...
Where the redraw function looks like this...
function redraw() {
trans = d3.event.translate;
scale = d3.event.scale;
vis.attr("transform", "translate(" + trans + ")" + " scale(" + scale + ")");
}
However, this method zooms the entire SVG graphic, including font sizes, graph edges, the line stroke-widths surrounding the nodes, etc.
Is it somehow possible not to zoom certain elements? The only solution I have seen so far is to put a line like this (took it from here http://jsfiddle.net/56RDx/2/)
node.attr("font-size", (nodeFontSize / d3.event.scale) + "px");
in the redraw method, to basically invert the zooming on certain elements on the fly. My problem is however (apart from this being an ugly hack), that my edge-widths are dynamically generated on graph-drawing (according to some graph properties...), so this 'invertion' method does not work...
you can add a class to the element you want to trigger the zoom on:
d3 ... .append('svg:g').classed("some_classname", true).call(d3.behavior.zoom().on("zoom", redraw))...
then do:
function redraw() {
trans = d3.event.translate;
scale = d3.event.scale;
vis.selectAll("some_classname").attr("transform", "translate(" + trans + ")" + " scale(" + scale + ")");
}
or you can add a class to all elements you don't want to trigger the zoom on then use the CSS3 :not pseudo-class:
function redraw() {
trans = d3.event.translate;
scale = d3.event.scale;
vis.selectAll("*:not(.some_classname)").attr("transform", "translate(" + trans + ")" + " scale(" + scale + ")");
}
The only solution I could find is an "ugly hack", if (I assume you are) you're trying to not zoom lines for example, the you should try the below, it works for both zooming in and out:
Demo: http://jsfiddle.net/SO_AMK/gJMTb/
JavaScript:
function redraw() {
vis.attr("transform", "translate(" + d3.event.translate + ")" + " scale(" + d3.event.scale + ")");
vis.attr("font-size", (nodeFontSize / d3.event.scale) + "px");
vis.selectAll("line.link").style("stroke-width", getStrokeWidth); // Function so it runs for each element individually
}
function getStrokeWidth(){
if (!this.__data__.stroke) { // Doesn't exist, so set it to the original stroke-width
this.__data__.stroke = parseFloat(d3.select(this).style("stroke-width"));
// I found __data__ to be easier than d3's .data()
}
return this.__data__.stroke / d3.event.scale + "px";
}
Please see the documentation for details on using a function with style()

Categories

Resources