I am quite new to d3 and am having trouble with zooming and dragging on a tree layout.
EDIT: Updated JSFiddle with information from comments
I have created a sample, JSFiddle here.
My issue is in the zoom function:
function zoom() {
var scale = d3.event.scale,
translation = d3.event.translate,
tbound = -height * scale * 100,
bbound = height * scale,
lbound = (-width + margin.right) * scale,
rbound = (width - margin.bottom) * scale;
console.log("pre min/max" + translation);
// limit translation to thresholds
translation = [
Math.max(Math.min(translation[0], rbound), lbound),
Math.max(Math.min(translation[1], bbound), tbound)];
console.log("scale" + scale);
console.log("translation" + translation);
d3.select("g")
.attr("transform", "translate(" + translation + ")" +
" scale(" + scale + ")");
}
If you expand and collapse nodes and then try to drag, the root node always goes back to the top left corner. I added some logging that shows that the value of translation in this case is -1,-1
Is there a way I can preserve the current root node position?
The problem is that the g element you're translating with the zoom behaviour is initialised with a non-zero translation. The zoom behaviour is not aware of this, resulting in the "jump" you see. To fix, initialise the zoom behaviour with that translation.
var zb = d3.behavior.zoom().scaleExtent([0.5, 5]).on("zoom", function () {
zoom();
});
zb.translate([margin.left, margin.top]);
Complete example here.
Related
I want to achieve panning the svg objects along both x and y axis and semantic zooming only along x axis.
As long as the d3.event.transform object holds the computed values for both zooming and panning, anytime I call panning and zooming one after each other, the newly translated y values are wrong because they ignore\do not ignore previous opposite actions. (zoom ignores the d3.event.transform.y of pan actions and pan does not ignore d3.event.transform.y of zoom actions)
If you zoom anywhere in the svg area, the circles should only translate the x coordinate and the y coordinate should stay the same as before (taking into account previous pannings)
Now the circles are "jumping" due to wrong y values.
if (isZoom)
{//zoom
return "translate(" + t.apply(d)[0] + "," + (d[1]) +")"; //ignores previous panning-only values and positions to static initial value
}
//panning
return "translate(" + t.apply(d)[0] + "," + (t.y + d[1]) +")"; //how to ignore portion of t.y belonging to previous zooming?
You can uncomment this line of code to prevent circles from jumping but the y is changing while zooming (which should not)
//ignore isZoom and apply both for panning and zooming
return "translate(" + t.apply(d)[0] + "," + (t.y + d[1]) +")";
https://jsfiddle.net/197cz2vj/
Thanks!
UPDATE
Finally I came up with a hack-like solution. I do not like it and I am still looking for a proper solution(I don't like deciding the isZoom action and also the deltaPanY solution. It is all succeptible for future changes in d3 libray). Here it is:
Every time the transformation changes and the change is triggered by the mousemove(panning), update the deltaPanY variable comparing the new value with the old remembered value of the transformation. (I make a copy also of the t.x and t.k but for my purposes only t.y and deltaPanY is necessary).
function copyLastTransform(t)
{
lastTransform =
{
x: t.x,
y: t.y,
k: t.k
};
};
Every time the transform occurs, set the delta variables and store the last transform:
if (isZoom)
{
deltaZoomY += t.y - lastTransform.y;
}
else
{
deltaPanY += t.y - lastTransform.y;
}
copyLastTransform(t);
Translate function looks like this now:
return "translate(" + t.apply(d)[0] + "," + (deltaPanY + d[1]) +")";
Forked fiddle:
https://jsfiddle.net/xpr364uo/
I am trying to replicate the "super group" functionality like in an example I found here. Even though my start and end angles are correct according to my main matrix, they Arc is not aligning correctly. I have a feeling it is the InnerRadius and OuterRadius on Arc2 but I can't narrow down what needs to change. My example code can be found here. You can see the brown arc does not align to the start angle of Index 0 or endAngle of Index 2.
//define grouping with colors
var groups = [
{sIndex: 0, eIndex: 2, title: 'SuperCategory 1', color: '#c69c6d'}
];
var cD = chord(matrix).groups;
console.log(cD);
//draw arcs
for(var i=0;i<groups.length;i++) {
var __g = groups[i];
console.log(cD[__g.sIndex].startAngle);
var arc1 = d3.arc()
.innerRadius(innerRadius)
.outerRadius(outerRadius)
.startAngle(cD[__g.sIndex].startAngle)
.endAngle(cD[__g.eIndex].endAngle)
svg.append("path").attr("d", arc1).attr('fill', __g.color).attr('id', 'SuperCategory' + i);
Looks like my path for my original groups (Test1, Test2, etc) had a translation applied to them. I had to apply the same translation to the SuperCategory ptah and then add to Inner/Outer radius in order to move it as an outer arc. I have updated the jsfiddle to reflect the change.
// Add to Radii to move arc flush against inner arc
.innerRadius(innerRadius + 20)
.outerRadius(outerRadius + 20)
.startAngle(cD[__g.sIndex].startAngle)
.endAngle(cD[__g.eIndex].endAngle)
// add the translation
svg.append("path").attr("d", arc1).attr('fill', __g.color).attr('id', 'SuperCategory' + i).attr("transform", "translate(" + width / 2 + "," + height / 2 + ")");
I have two topojson files obtained from shapefiles which I append to the same g node of an svg element in a nested loop.
// Layer 1
d3.json("grid.topojson", function(error, grid) {
// Layer 2 (continents)
d3.json("continents.topojson", function(error, continent) {
...
It is possible to click on the continents to zoom in on a particular region (from https://bl.ocks.org/mbostock/4699541).
The grid layer is very dense so it slows down the zoom when clicking on the continents in the continent layer. To get around this, I would like to display the grid layer only after zooming in on a particular continent. I have an .on("click") event that triggers the zoom function:
function clicked(path, d, m_width, m_height, _this, grid) {
var this_class = get_classFromPath(_this);
var bounds = path.bounds(d),
dx = bounds[1][0] - bounds[0][0],
dy = bounds[1][1] - bounds[0][1],
x = (bounds[0][0] + bounds[1][0]) / 2,
y = (bounds[0][1] + bounds[1][1]) / 2,
scale = .2 / Math.max(dx / m_width, dy / m_height),
translate = [m_width / 2 - scale * x, m_height / 2 - scale * y];
g.transition()
.duration(750)
.style("stroke-width", 1.5 / scale + "px")
.attr("transform", "translate(" + translate + ")scale(" + scale + ")");
// .selectAll("path.model-grid")
// .style("display", "inline");
}
In the clicked function, I want to make the grid visible after the zoom, but it seems unnecessary (and is very slow) to useselectAll since only a small part of the total grid is visible in the display. Unfortunately, the shapefile of the grid does not contain any ids I can use to correlate with the id of the continents, so I cannot select grid path elements by id.
Is there a way to find out which path elements of the grid layer are contained in the bounds box after the zoom?
Any help is much appreciated, thanks!
I've got a topology map with pan and zoom functionality.
Clicking on the country, I'm zoom/panning into the country, using this:
if (this.active === d) return
var g = this.vis.select('g')
g.selectAll(".active").classed("active", false)
d3.select(path).classed('active', active = d)
var b = this.path.bounds(d);
g.transition().duration(750).attr("transform","translate(" +
this.proj.translate() + ")" +
"scale(" + .95 / Math.max((b[1][0] - b[0][0]) / this.options.width, (b[1][1] - b[0][1]) / this.options.height) + ")" +
"translate(" + -(b[1][0] + b[0][0]) / 2 + "," + -(b[1][1] + b[0][1]) / 2 + ")");
g.selectAll('path')
.style("stroke-width", 1 / this.zoom.scale())
However, if I continue to drag pan, the map jerks back into the initial position before the click happens, before panning. Code to pan/zoom is here:
this.zoom = d3.behavior.zoom().on('zoom', redraw)
function redraw() {
console.log('redraw')
_this.vis.select('g').attr("transform","translate(" +
d3.event.translate.join(",") + ")scale(" + d3.event.scale + ")")
_this.vis.select('g').selectAll('path')
.style("stroke-width", 1 / _this.zoom.scale())
}
this.vis.call(this.zoom)
In another words, after zooming into a point by clicking, and then dragging by the redraw function, the redraw does not pick up the correct translate+scale to continue from.
To continue at the right 'zoom' after the transition, I had to set the zoom to the new translate and scale.
Example of reset which is applied similarly to my click and zoom event, the set new zoom point is the critical bit:
_this.vis.select('g').transition().duration(1000)
.attr('transform', "translate(0,0)scale(1)")
/* SET NEW ZOOM POINT */
_this.zoom.scale(1);
_this.zoom.translate([0, 0]);
I'm trying to use the pan/zoom ability of d3 to draw boxes on the screen so that when you click on a box a new box appears and shifts the rest of the boxes to the right so that the new box is on the center of the canvas. The panning would allow me to scroll through all the boxes I've drawn.
Here is my jsfiddle: http://jsfiddle.net/uUTBE/1/
And here is my code for initializing the zoom/pan:
svg.call(d3.behavior.zoom().on("zoom", redraw));
function redraw() {
d3.select(".canvas").attr("transform",
"translate(" + d3.event.translate + ")"
+ " scale(" + d3.event.scale + ")");
}
And here is my code for drawing the boxes:
function drawBox(x, y) {
var boxGroup = canvas.append("g");
boxGroup.append("rect")
.attr("x", x)
.attr("y", y)
.attr("height", 100)
.attr("width", 100)
.attr("fill", function () {
var i = Math.floor(Math.random() * 4);
if (i === 1) return "red";
else if (i === 2) return "blue";
else if (i === 3) return "yellow";
else return "green";
})
.on("click", function () {
counter++;
d3.select(".canvas")
.transition()
.duration(1000)
.attr('transform', "translate(300,0)");
drawBox(x - counter * 120, y);
});
}
I have multiple problems with this fiddle, but two of my main concerns is:
1) How do I make it so that when I click on a new box a second time the boxes move accordingly (i.e. when I click on the box initially the old box shifts to the right and a new box appears, but when I click on the new box, the older boxes doesnn't shift to the right).
2)Why is it that when I click on the new box, the newer box has a big spacing between it? (only happens after trying to put 3 boxes on the screen).
Thanks any hints are appreciated!
I think there's some confusion here around transform. The transform attribute is static, not cumulative, for a single element - so setting .attr('transform', "translate(300,0)") more than once will have no effect after the first time. It also looks like your placement logic for the new boxes is off.
The positioning logic required here is pretty straightforward if you take a step back (assuming I understand what you're trying to do):
Every time a new box is added, the frame all boxes are in moves right 120px, so it needs a x-translation of 120 * counter.
New boxes need to be offset from the new frame position, so they need an x setting of -120 * counter.
Zoom needs to take the current canvas offset into account.
(1) above can be done in your click handler:
canvas
.transition()
.duration(1000)
.attr('transform', "translate(" + (offset * counter) + ",0)");
(2) is pretty easily applied to the g element you're wrapping boxes in:
var boxGroup = canvas.append("g")
.attr('transform', 'translate(' + (-offset * counter) + ',0)');
(3) can be added to your redraw handler:
function redraw() {
var translation = d3.event.translate,
newx = translation[0] + offset * counter,
newy = translation[1];
canvas.attr("transform",
"translate(" + newx + "," + newy + ")" + " scale(" + d3.event.scale + ")");
}
See the working fiddle here: http://jsfiddle.net/nrabinowitz/p3m8A/