Prevent panning outside of map bounds in d3v5 - javascript

in d3 v3, I used this example to prevent panning outside of an SVG. Here's the relevant code:
.on("zoom", function() {
// the "zoom" event populates d3.event with an object that has
// a "translate" property (a 2-element Array in the form [x, y])
// and a numeric "scale" property
var e = d3.event,
// now, constrain the x and y components of the translation by the
// dimensions of the viewport
tx = Math.min(0, Math.max(e.translate[0], width - width * e.scale)),
ty = Math.min(0, Math.max(e.translate[1], height - height * e.scale));
// then, update the zoom behavior's internal translation, so that
// it knows how to properly manipulate it on the next movement
zoom.translate([tx, ty]);
// and finally, update the <g> element's transform attribute with the
// correct translation and scale (in reverse order)
g.attr("transform", ["translate(" + [tx, ty] + ")","scale(" + e.scale + ")"].join(" "));
}
In d3 v5, it doesn't work anymore. All the examples allow the map to pan ridiculous amounts off screen. My goal is that the rightmost edge of the map never goes further left than the rightmost edge of the div, etc. How can I accomplish this? Are there any more recent examples?

For the example you linked, these are the necessary changes inside the zoom function for that to work with D3 v5:
var e = d3.event.transform,
tx = Math.min(0, Math.max(e.x, width - width * e.k)),
ty = Math.min(0, Math.max(e.y, height - height * e.k));
Besides that, change the group translate function and remove zoom.translate([tx, ty]);.
Here is the original code with those changes:
<html>
<head>
<title>Restricted zoom behavior in d3</title>
<meta charset="utf-8">
<script src="https://d3js.org/d3.v5.min.js"></script>
<style>
</style>
</head>
<body>
<script>
// first, define your viewport dimensions
var width = 960,
height = 500;
// then, create your svg element and a <g> container
// for all of the transformed content
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height)
.style("background-color", randomColor),
g = svg.append("g");
// then, create the zoom behvavior
var zoom = d3.zoom()
// only scale up, e.g. between 1x and 50x
.scaleExtent([1, 50])
.on("zoom", function() {
// the "zoom" event populates d3.event with an object that has
// a "translate" property (a 2-element Array in the form [x, y])
// and a numeric "scale" property
var e = d3.event.transform,
// now, constrain the x and y components of the translation by the
// dimensions of the viewport
tx = Math.min(0, Math.max(e.x, width - width * e.k)),
ty = Math.min(0, Math.max(e.y, height - height * e.k));
// then, update the zoom behavior's internal translation, so that
// it knows how to properly manipulate it on the next movement
// and finally, update the <g> element's transform attribute with the
// correct translation and scale (in reverse order)
g.attr("transform", [
"translate(" + [tx, ty] + ")",
"scale(" + e.k + ")"
].join(" "));
});
// then, call the zoom behavior on the svg element, which will add
// all of the necessary mouse and touch event handlers.
// remember that if you call this on the <g> element, the even handlers
// will only trigger when the mouse or touch cursor intersects with the
// <g> elements' children!
svg.call(zoom);
// then, let's add some circles
var circle = g.selectAll("circle")
.data(d3.range(300).map(function(i) {
return {
x: Math.random() * width,
y: Math.random() * height,
r: .01 + Math.random() * 50,
color: randomColor()
};
}).sort(function(a, b) {
return d3.descending(a.r, b.r);
}))
.enter()
.append("circle")
.attr("fill", function(d) {
return d.color;
})
.attr("cx", function(d) {
return d.x;
})
.attr("cy", function(d) {
return d.y;
})
.attr("r", function(d) {
return d.r;
});
function randomColor() {
return "hsl(" + ~~(60 + Math.random() * 180) + ",80%,60%)";
}
</script>
</body>
</html>

Related

d3 arc transition anticlockwise not clockwise

I am looking at having two animated donut graphs and need to animate them a certain way.
If you look at the jsfiddle:
https://jsfiddle.net/5uc1xfxm/
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="content-type" content="text/html; charset=UTF-8">
<meta name="robots" content="noindex, nofollow">
<meta name="googlebot" content="noindex, nofollow">
<script type="text/javascript" src="/js/lib/dummy.js"></script>
<link rel="stylesheet" type="text/css" href="/css/result-light.css">
<script type="text/javascript" src="https://d3js.org/d3.v4.min.js"></script>
<style type="text/css">
.animated-ring {
margin-left: auto;
margin-right: auto;
//margin-top: 50px;
width: 200px;
background-color: #fff;
}
</style>
<script type='text/javascript'>//<![CDATA[
window.onload=function(){
var tau = 2 * Math.PI; // http://tauday.com/tau-manifesto
// An arc function with all values bound except the endAngle. So, to compute an
// SVG path string for a given angle, we pass an object with an endAngle
// property to the `arc` function, and it will return the corresponding string.
var arc1 = d3.arc()
.innerRadius(45)
.outerRadius(90)
.startAngle(0.75 * tau);
var arc2 = d3.arc()
.innerRadius(45)
.outerRadius(90)
.startAngle(0.25 * tau);
// Get the SVG container, and apply a transform such that the origin is the
// center of the canvas. This way, we don’t need to position arcs individually.
var svg1 = d3.select("#anim1"),
width = +svg1.attr("width"),
height = +svg1.attr("height"),
g = svg1.append("g").attr("transform", "translate(" + width / 2 + "," + height / 2 + ")");
var svg2 = d3.select("#anim2"),
width = +svg2.attr("width"),
height = +svg2.attr("height"),
h = svg2.append("g").attr("transform", "translate(" + width / 2 + "," + height / 2 + ")");
// Add the background arc, from 0 to 100% (tau).
var background1 = g.append("path")
.datum({
endAngle: tau
})
.style("fill", "transparent")
.attr("d", arc1);
var background2 = h.append("path")
.datum({
endAngle: tau
})
.style("fill", "transparent")
.attr("d", arc2);
// Add the foreground arc in orange, currently showing 12.7%.
var foreground1 = g.append("path")
.datum({
endAngle: 0.75 * tau
})
.style("fill", "#EF4939")
.attr("d", arc1);
var foreground2 = h.append("path")
.datum({
endAngle: 0.25 * tau
})
.style("fill", "blue")
.attr("d", arc2);
// Every so often, start a transition to a new random angle. The attrTween
// definition is encapsulated in a separate function (a closure) below.
d3.timer(function() {
foreground1.transition()
.duration(75)
.attrTween("d", arcTween(0 * tau));
foreground2.transition()
.duration(75)
.attrTween("d", arcTween2(0.5 * tau));
});
// Returns a tween for a transition’s "d" attribute, transitioning any selected
// arcs from their current angle to the specified new angle.
function arcTween(newAngle) {
// The function passed to attrTween is invoked for each selected element when
// the transition starts, and for each element returns the interpolator to use
// over the course of transition. This function is thus responsible for
// determining the starting angle of the transition (which is pulled from the
// element’s bound datum, d.endAngle), and the ending angle (simply the
// newAngle argument to the enclosing function).
return function(d) {
// To interpolate between the two angles, we use the default d3.interpolate.
// (Internally, this maps to d3.interpolateNumber, since both of the
// arguments to d3.interpolate are numbers.) The returned function takes a
// single argument t and returns a number between the starting angle and the
// ending angle. When t = 0, it returns d.endAngle; when t = 1, it returns
// newAngle; and for 0 < t < 1 it returns an angle in-between.
var interpolate = d3.interpolate(d.endAngle, newAngle);
// The return value of the attrTween is also a function: the function that
// we want to run for each tick of the transition. Because we used
// attrTween("d"), the return value of this last function will be set to the
// "d" attribute at every tick. (It’s also possible to use transition.tween
// to run arbitrary code for every tick, say if you want to set multiple
// attributes from a single function.) The argument t ranges from 0, at the
// start of the transition, to 1, at the end.
return function(t) {
// Calculate the current arc angle based on the transition time, t. Since
// the t for the transition and the t for the interpolate both range from
// 0 to 1, we can pass t directly to the interpolator.
//
// Note that the interpolated angle is written into the element’s bound
// data object! This is important: it means that if the transition were
// interrupted, the data bound to the element would still be consistent
// with its appearance. Whenever we start a new arc transition, the
// correct starting angle can be inferred from the data.
d.endAngle = interpolate(t);
// Lastly, compute the arc path given the updated data! In effect, this
// transition uses data-space interpolation: the data is interpolated
// (that is, the end angle) rather than the path string itself.
// Interpolating the angles in polar coordinates, rather than the raw path
// string, produces valid intermediate arcs during the transition.
return arc1(d);
};
};
}
// Returns a tween for a transition’s "d" attribute, transitioning any selected
// arcs from their current angle to the specified new angle.
function arcTween2(newAngle) {
return function(d) {
var interpolate = d3.interpolate(d.endAngle, newAngle);
return function(t) {
d.endAngle = interpolate(t);
return arc2(d);
};
};
}
}//]]>
</script>
</head>
<body>
<div class="animated-ring">
<svg width="200" height="200" id="anim1"></svg>
<svg width="200" height="200" id="anim2"></svg>
</div>
<script>
// tell the embed parent frame the height of the content
if (window.parent && window.parent.parent){
window.parent.parent.postMessage(["resultsFrame", {
height: document.body.getBoundingClientRect().height,
slug: "5uc1xfxm"
}], "*")
}
</script>
</body>
</html>
You will see the two animations. The orange graph is working as required but the blue graph isn't.
The start and end positions are correct but instead of it moving clockwise and filling only a quarter of the circle. I need it to move anti-clockwise and fill three quarters of the circle.
Any ideas?
Assuming I understand the problem,
Instead of having the arc transition from 0.25 tau to 0.5 tau, try -0.5 tau:
.attrTween("d", arcTween2(-0.5 * tau));
This will move the arc backwards and cause it to fill 3/4 of the complete donut. This fits the pattern from the first donut which behaves properly, where the ultimate end angle is less than the start angle.
Updated fiddle here
And a snippet using your code for good measure (I've changed the order of the donuts so you don't need to quickly scroll):
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="content-type" content="text/html; charset=UTF-8">
<meta name="robots" content="noindex, nofollow">
<meta name="googlebot" content="noindex, nofollow">
<script type="text/javascript" src="/js/lib/dummy.js"></script>
<link rel="stylesheet" type="text/css" href="/css/result-light.css">
<script type="text/javascript" src="https://d3js.org/d3.v4.min.js"></script>
<style type="text/css">
.animated-ring {
margin-left: auto;
margin-right: auto;
//margin-top: 50px;
width: 200px;
background-color: #fff;
}
</style>
<script type='text/javascript'>//<![CDATA[
window.onload=function(){
var tau = 2 * Math.PI; // http://tauday.com/tau-manifesto
// An arc function with all values bound except the endAngle. So, to compute an
// SVG path string for a given angle, we pass an object with an endAngle
// property to the `arc` function, and it will return the corresponding string.
var arc1 = d3.arc()
.innerRadius(45)
.outerRadius(90)
.startAngle(0.75 * tau);
var arc2 = d3.arc()
.innerRadius(45)
.outerRadius(90)
.startAngle(0.25 * tau);
// Get the SVG container, and apply a transform such that the origin is the
// center of the canvas. This way, we don’t need to position arcs individually.
var svg1 = d3.select("#anim1"),
width = +svg1.attr("width"),
height = +svg1.attr("height"),
g = svg1.append("g").attr("transform", "translate(" + width / 2 + "," + height / 2 + ")");
var svg2 = d3.select("#anim2"),
width = +svg2.attr("width"),
height = +svg2.attr("height"),
h = svg2.append("g").attr("transform", "translate(" + width / 2 + "," + height / 2 + ")");
// Add the background arc, from 0 to 100% (tau).
var background1 = g.append("path")
.datum({
endAngle: tau
})
.style("fill", "transparent")
.attr("d", arc1);
var background2 = h.append("path")
.datum({
endAngle: tau
})
.style("fill", "transparent")
.attr("d", arc2);
// Add the foreground arc in orange, currently showing 12.7%.
var foreground1 = g.append("path")
.datum({
endAngle: 0.75 * tau
})
.style("fill", "#EF4939")
.attr("d", arc1);
var foreground2 = h.append("path")
.datum({
endAngle: 0.25 * tau
})
.style("fill", "blue")
.attr("d", arc2);
// Every so often, start a transition to a new random angle. The attrTween
// definition is encapsulated in a separate function (a closure) below.
d3.timer(function() {
foreground1.transition()
.duration(75)
.attrTween("d", arcTween(0 * tau));
foreground2.transition()
.duration(75)
.attrTween("d", arcTween2(-0.5 * tau));
});
// Returns a tween for a transition’s "d" attribute, transitioning any selected
// arcs from their current angle to the specified new angle.
function arcTween(newAngle) {
// The function passed to attrTween is invoked for each selected element when
// the transition starts, and for each element returns the interpolator to use
// over the course of transition. This function is thus responsible for
// determining the starting angle of the transition (which is pulled from the
// element’s bound datum, d.endAngle), and the ending angle (simply the
// newAngle argument to the enclosing function).
return function(d) {
// To interpolate between the two angles, we use the default d3.interpolate.
// (Internally, this maps to d3.interpolateNumber, since both of the
// arguments to d3.interpolate are numbers.) The returned function takes a
// single argument t and returns a number between the starting angle and the
// ending angle. When t = 0, it returns d.endAngle; when t = 1, it returns
// newAngle; and for 0 < t < 1 it returns an angle in-between.
var interpolate = d3.interpolate(d.endAngle, newAngle);
// The return value of the attrTween is also a function: the function that
// we want to run for each tick of the transition. Because we used
// attrTween("d"), the return value of this last function will be set to the
// "d" attribute at every tick. (It’s also possible to use transition.tween
// to run arbitrary code for every tick, say if you want to set multiple
// attributes from a single function.) The argument t ranges from 0, at the
// start of the transition, to 1, at the end.
return function(t) {
// Calculate the current arc angle based on the transition time, t. Since
// the t for the transition and the t for the interpolate both range from
// 0 to 1, we can pass t directly to the interpolator.
//
// Note that the interpolated angle is written into the element’s bound
// data object! This is important: it means that if the transition were
// interrupted, the data bound to the element would still be consistent
// with its appearance. Whenever we start a new arc transition, the
// correct starting angle can be inferred from the data.
d.endAngle = interpolate(t);
// Lastly, compute the arc path given the updated data! In effect, this
// transition uses data-space interpolation: the data is interpolated
// (that is, the end angle) rather than the path string itself.
// Interpolating the angles in polar coordinates, rather than the raw path
// string, produces valid intermediate arcs during the transition.
return arc1(d);
};
};
}
// Returns a tween for a transition’s "d" attribute, transitioning any selected
// arcs from their current angle to the specified new angle.
function arcTween2(newAngle) {
return function(d) {
var interpolate = d3.interpolate(d.endAngle, newAngle);
return function(t) {
d.endAngle = interpolate(t);
return arc2(d);
};
};
}
}//]]>
</script>
</head>
<body>
<div class="animated-ring">
<svg width="200" height="200" id="anim2"></svg>
<svg width="200" height="200" id="anim1"></svg>
</div>
<script>
// tell the embed parent frame the height of the content
if (window.parent && window.parent.parent){
window.parent.parent.postMessage(["resultsFrame", {
height: document.body.getBoundingClientRect().height,
slug: "5uc1xfxm"
}], "*")
}
</script>
</body>
</html>

d3.js rewriting zoom example in version4

Drag and Drop Example
I am trying to rewrite part of this example above to use in my code, specifically this piece:
function centerNode(source) {
scale = zoomListener.scale();
x = -source.y0;
y = -source.x0;
x = x * scale + viewerWidth / 2;
y = y * scale + viewerHeight / 2;
d3.select('g').transition()
.duration(duration)
.attr("transform", "translate(" + x + "," + y + ")scale(" + scale + ")");
zoomListener.scale(scale);
zoomListener.translate([x, y]);
}
However I am getting stuck since the v4 package has changed quite a bit. I wrote my zoomListener function to be
var zoomListener = d3.zoom()
.scaleExtent([0.3,2])
.on("zoom", zoomed);
function zoomed() {
transform = d3.event.transform;
console.log(d3.event);
svg.attr("transform", transform);
}
function centerNode(source){
t = transform;
console.log(t);
x = t.x*t.k; //I only want things to be centered vertically
y = (t.y + -source.x0)*t.k + (viewerHeight)/2 ;
svg.transition()
.duration(duration)
.attr("transform","translate(" + x + "," + y +")scale(" + t.k + ")");
transform.scale(t.k); //DOES NOT WORK
transform.translate([x, y]); //DOES NOT WORK
}
and I know that according to the doc things have changed and info are no longer are stored on what would be my zoomListener
D3 V4 release note on zoom I guess I am just confused on how I am suppose to do it with the new version. The last few lines of my centerNode function don't work which has for effect that when I center the node the zooming and panning reset...
Any suggestion?
So after much digging and trial and error I cam up with an answer that works pretty well for my purposes. Note that this code below is only the relevant part of my code not the whole code, certain variable were self explanatory so did not include them. ALSO THIS IS IN VERSION 4 of d3.js.
var zoom = d3.zoom()
.scaleExtent([0.3,2])
.on("zoom", zoomed);
var svg = d3.select("body")
.append("svg")
.attr("width", viewerWidth)
.attr("height", viewerHeight);
var zoomer = svg.append("rect")
.attr("width", viewerWidth)
.attr("height", viewerHeight)
.style("fill", "none")
.style("pointer-events", "all")
.call(zoom);
var g = svg.append("g");
zoomer.call(zoom.transform, d3.zoomIdentity.translate(150,0)); //This is to pad my svg by a 150px on the left hand side
function zoomed() {
g.attr("transform", d3.event.transform);//The zoom and panning is affecting my G element which is a child of SVG
}
function centerNode(source){
t = d3.zoomTransform(zoomer.node());
console.log(t);
x = t.x;
y = source.x0;
y = -y *t.k + viewerHeight / 2;
g.transition()
.duration(duration)
.attr("transform", "translate(" + x + "," + y + ")scale(" + t.k + ")")
.on("end", function(){ zoomer.call(zoom.transform, d3.zoomIdentity.translate(x,y).scale(t.k))});
}
As per the examples for v4 on the d3.js page, I used a rectangle to apply the zoom to
The zoom behavior is applied to an invisible rect overlaying the SVG
element; this ensures that it receives input, and that the pointer
coordinates are not affected by the zoom behavior’s transform. Pan & Zoom Example
In the Center node function I am using d3.zoomTransform(zoomer.node()); to get the current transform applied to the page.
The purpose of this function is only to center the collapsible tree vertically not horizontally, so I am keeping the current transform.x (here t.x) the same.
The coordinate in my svg are flip hence why y= source.x0, source is a what node was clicked in my collapsible tree. ("Look to the example referenced to the top of this thread to understand what I am trying to convert to version 4)
I am apply the transformation to my G element and then I want to commit those changes to the zoom transform, to do so I use the .on("end", function(){}) otherwise it was doing weird behavior with the transition, by doing that all it does is setting the current state of the transform.
zoomer.call(zoom.transform, d3.zoomIdentity.translate(x,y).scale(t.k))
This line above is applying a translation of x and y and a scale -- that is equal to what the current state -- to the identiy matrix has to get a new transform for G, i then apply it to zoomer which is the element I called zoom on earlier.
This worked like a charm for me!
Calling transform.scale and transform.translate returns a new transform, and modifies nothing. Therefore:
transform = transform.translate([x, y]).scale(k)
svg.call(zoomListener.transform, newTransform)
(At this point zoomListener is a pretty inaccurate name for this, but regardless...)
k, x, and y can be derived from source, maybe as you show, but I'm not sure, because I don't know what source is. But to me, t.x*t.k looks suspicious, because it's multiplying the existing transforms x by its scale. Seems like it would cause a feedback loop.
For more into about the zoom in v4, check out this related StackOverflow post, or this example by mbostock demonstrating programmatic control over the zoom transform of an element (canvas in this case) and includes transitions.

Issues with preserveAspectRatio, viewBox and layout.size()

I have written the following code designed to display a d3js tree layout but encountered some difficulty when trying to get the generated svg to resize according to its aspect ratio. I was able (in the attached demo) to get the svg to scale the way I had desired but the code I have written is constrained by const ASPECT_RATIO, as seen here:
canvas.attr("viewBox", " 0 0 " + window.innerWidth + " " + (window.innerWidth * ASPECT_RATIO));
and again, further down, here:
layout.size([(window.innerWidth * ASPECT_RATIO), window.innerWidth - 128]);
Is there a way to circumvent this? I would prefer not having to change this value by hand every time the aspect ratio of the svg changes (that is, any time new content is added).
Regards,
Brian
tl;dr: Help me eliminate const ASPECT_RATIO.
Code:
/// <reference path="d3.d.ts" />
"use strict";
/* (c) brianjenkins94 | brianjenkins94.me | MIT licensed */
// Get JSON, impose synchronicity
d3.json("js/data.json", function(error, treeData) {
if (!error) {
// Instantiate canvas
var canvas = d3.select("#canvas");
// Aspect ratio nonsense
const ASPECT_RATIO = 1.89260808926;
canvas.attr("viewBox", " 0 0 " + window.innerWidth + " " + (window.innerWidth * ASPECT_RATIO));
canvas.attr("preserveAspectRatio", "xMinYMin slice");
// Update
update();
function update() {
// Add an SVG group element
canvas.append("g");
// Instantiate group
var group = canvas.select("g");
// Translate group right
group.attr("transform", "translate(64, 0)");
// Instantiate layout tree
var layout = d3.layout.tree();
// Initialize layout dimensions
layout.size([(window.innerWidth * ASPECT_RATIO), window.innerWidth - 128]);
// Instantiate rotation diagonal
var diagonal = d3.svg.diagonal();
// Rotate projection 90 degrees about the diagonal
diagonal.projection(function(d) { return [d.y, d.x]; });
// Initialize node array
var nodes = layout.nodes(treeData);
// Initialize link array
var links = layout.links(nodes);
// Select all paths in group
group.selectAll("path")
// For each link, create a path
.data(links).enter().append("path")
// Provide the specific diagonal
.attr("d", diagonal);
// Select all groups in group
var node = group.selectAll("g")
// For each node, create a group
.data(nodes).enter().append("g")
// Translate accordingly
.attr("transform", function(d) { return "translate(" + d.y + "," + d.x + ")"; });
// Add a circle at every node
node.append("circle")
.attr("r", 3);
// Add label
node.append("text")
// To the left if the node has children, otherwise right
.attr("dx", function(d) { return d.children ? -8 : 8; })
.attr("dy", 0)
// Branch if the node has children
.attr("text-anchor", function(d) { return d.children ? "end" : "start"; })
.text(function(d) { return d.name; });
}
} else {
console.log("There was a connection error of some sort.");
}
});
Demo:
https://rawgit.com/brianjenkins94/html5tree/master/index.html
Here's what I learned:
While an svg can be "dimensionless" it can't be "aspect-ratio-less." There must be some core aspect ratio to govern the resize.
The tree.size() and tree.nodeSize() functions create an "arbitrary coordinate system" that "is not limited screen coordinates."
What the aspect ratio is, is up to the developer and can be expressed as "arbitrary coordinates" or, in accordance with responsive design, can be expressed as relativistic measurements.
My solution is as follows:
// Viewbox & preserveAspectRatio
canvas.attr("viewBox", " 0 0 " + window.innerWidth + " " + (2 * window.innerWidth));
canvas.attr("preserveAspectRatio", "xMinYMin slice");
...
// Initialize layout dimensions
layout.size([(2 * window.innerWidth), (window.innerWidth - 128)]);
Thus eliminating the dependence on const ASPECT_RATIO in favor of relative measurements based off of browser dimensions.
This can potentially (and almost certainly will) cause rendering inconsistency across multiple viewports, but can be handled accordingly by querying the viewport prior to rendering and employing cosmetic adjustments.

How can I fix the coordinate-type change that occurs from dragging in d3?

Right now in my program I have both a drag function and a zoom function.
The problem can best be illustrated in this minimal test case. When the zoom is initiated at first, you may notice that the points in the graph will transition just fine. But, if you drag a point before initiating a zoom, that point will usually travel outside the scope of the graph.
This problem seems to be happening because my coordinates are defined in scaled space centered around the upper left corner. However, after an object is dragged, its coordinates are switched to pixel space, centered around its original coordinates. When a zoom occurs, it will then treat the pixel coordinates as scaled space coordinates relative to the upper left corner, which causes problems.
I would appreciate any tips or pointers and thanks in advance
Drag function:
function dragmove(d) {
var barz = document.querySelector("#visual");
var point = d3.mouse(barz),
tempP = {
x: point[0],
y: point[1]
};
if (this.nodeName === "circle") {
d3.event.sourceEvent.stopPropagation();
var useZoom = $('#zoom').is(":checked");
if (useZoom == false) {
d.usePixels = 1;
d3.select(this).attr("transform", "translate(" + (d.x = tempP.x - xRange(d.initx)) + "," + (d.y = tempP.y - yRange(d.inity)) + ")");
//events to update line to fit dots
updateXs();
redoLine();
updateBars(canvas);
}
} }
Zoom function:
function zoomOut() {
//update axis
yRange.domain([d3.min(lineData, function (d) {
return d.y - 10;
}), d3.max(lineData, function (d) {
return d.y + 10;
})])
yAxisGroup.transition().call(yAxis);
xAxisGroup.transition().attr("transform", "translate(0," + yRange(0) + ")");
//update line
d3.select(".myLine").transition()
.attr("d", lineFunc(lineData));
var c = vis.selectAll("circle")
c.transition()
.attr(circleAttrs)
}

Adjusting scale of a group to ensure shapes inside are as big as possible in d3 js

I'm using d3 tree layout similar to this example: http://bl.ocks.org/mbostock/4339083
I implemented a search box that when typing, centers your screen on a virtual "average" position of all the appropriate nodes.
I want to adjust the scale, so that selected nodes will be
All Visible
As zoomed in as possible.
If the search match is exactly 1, simulate the clicking on the node, else center to this virtual position.
if (matches[0].length === 1) {
click(matches.datum(), 0, 0, false);
}
else {
var position = GetAveragePosition(matches);
centerToPosition(position.x, position.y, 1);
}
This is what the centerToPosition function looks like:
function centerToPosition(x0, y0, newScale) {
if (typeof newScale == "undefined") {
scale = zoomListener.scale();
}
else {
scale = newScale;
}
var x = y0 * -1; //not sure why this is.. but it is
var y = x0 * -1;
x = x * scale + viewerWidth / 2;
y = y * scale + viewerHeight / 2;
d3.select('g').transition()
.duration(duration)
.attr("transform", "translate(" + x + "," + y + ")scale(" + scale + ")");
zoomListener.scale(scale);
zoomListener.translate([x, y]);
}
So how can I calculate the new scale? I tried different variations by taking the extents of the data points
var xExtent = d3.extent(matches.data(), function (d) {
return d.x0;
});
var yExtent = d3.extent(matches.data(), function (d) {
return d.y0;
});
Also tried looking at the transform properties of the group before centering the screen.
var components = d3.transform(svgGroup.attr("transform"));
I'll try to add a js fiddle soon!
EDIT: Here it is: http://jsfiddle.net/7SJqC/
Interesting project.
The method of determining the appropriate scale to fit a collection of points is fairly straightforward, although it took me quite a while to figure out why it wasn't working for me -- I hadn't clued in to the fact that (since you were drawing the tree horizontally) "x" from the tree layout represented vertical position, and "y" represented horizontal position, so I was getting apparently arbitrary results.
With that cleared up, to figure out the zoom you simply need to find the height and width (in data-coordinates) of the area you want to display, and compare that with the height and width of the viewport (or whatever your original max and min dimensions are).
ScaleFactor = oldDomain / newDomain
Generally, you don't want to distort the image with different horizontal and vertical scales, so you figure out the scale factor separately for width and height and take the minimum (so the entire area will fit in the viewport).
You can use the d3 array functions to figure out the extent of positions in each direction, and then find the middle of the extent adding max and min and dividing by two.
var matches = d3.selectAll(".selected");
/*...*/
if ( matches.empty() ) {
centerToPosition(0, 0, 1); //reset
}
else if (matches.size() === 1) {
click(matches.datum(), 0, 0, false);
}
else {
var xExtent = d3.extent(matches.data(), function (d) {
return d.x0;
});
var yExtent = d3.extent(matches.data(), function (d) {
return d.y0;
});
//note: the "x" values are used to set VERTICAL position,
//while the "y" values are setting the HORIZONTAL position
var potentialXZoom = viewerHeight/(xExtent[1] - xExtent[0] + 20);
var potentialYZoom = viewerWidth/(yExtent[1] - yExtent[0] + 150);
//The "20" and "150" are for height and width of the labels
//You could (should) replace with calculated values
//or values stored in variables
centerToPosition( (xExtent[0] + xExtent[1])/2,
(yExtent[0] + yExtent[1])/2,
Math.min(potentialXZoom, potentialYZoom)
);
}
http://jsfiddle.net/7SJqC/2/

Categories

Resources