I've got a D3 radial dendrogram tree that I've applied d3.zoom to, but it jitters when I drag it. The zoom behaviour itself is fine, but the drag is not. I think there might be some issue with the way the 'g' element is translated (width / 2, height / 2 + 20).
Any help would be appreciated!
Here's a codesandbox of my tree: https://codesandbox.io/s/4zr43po6l9
change 'svg' to 'g', so the zoom affects the g element directly below the svg the zoom behaviour is attached to, and it's now smooth
let zoom = d3.zoom().on("zoom", () => {
g.attr("transform", d3.event.transform);
});
I'm not sure why it works like this to be honest
Perhaps changing the svg transform freaks out the event mouse x y position a bit as it bases its values on the svg, so you get that juddering effect?
Related
Running D3 v6.
This is a multi part question as in trying to solve the original problem I have a question about D3 and mouseevents. A quick note while using my fiddle, if you press the ESC key it will clear the draw line behavior.
How to draw a line from one node to another, following the cursor, regardless of zoom level and pan position?
Why does the line I draw behave differently when the .on('mousemove') is applied to an svg versus a g element?
Problem 1. The problem I am facing is that when panning and zooming, the end point of the line does not follow the cursor properly because the container I'm zooming on had it's x and y translated. Zoom in and click on a node to see the issue.
Related fiddle
This works just fine in my demo, until zooming and panning are involved. I've managed to take care of the panning issues by using d3.zoomTransform() to get the current [x,y] and apply that to the end point of the line. I cannot figure out to accommodate the zoom level though. I have tried transform(scale(zoomLevel.k)) but this doesn't work great. To recreate this issue, click a node without panning/zooming and observe the line follows the cursor. Zoom the graph and then click a node and observe the line does not follow the cursor.
Problem 2. I thought that I could solve the above issue by having the cursor react to mouse events on the g element I use for zooming and positioning rather than my parent svg element. When the mousemove event is on the g the line follows the cursor regardless of zoom/pan but is very laggy and I don't understand why.
SVG mouseevent
G mouseevent
Brief code overview, view fiddles for full code
let sourceNode;
const svg = d3.select("#chart")
.attr("viewBox", [0, 0, width, height]);
const g = svg.append('g');
const drawLine = g.append('line').attr('stroke', 'red').attr('stroke-width', 5).attr('visibility', 'hidden')
const nodes = g.append(//do node stuff)
const links = g.append(//do link stuff)
svg.call(d3.zoom().on('zoom', (event) => {
g.attr('transform', `translate(${event.transform.x}, ${event.transform.y}) scale(${event.transform.k})`)
}))
node.on('click', (event, d) => {
sourceNode = d
})
svg.on('mousemove', (event) => {
if (sourceNode) {
const currentZoom = d3.zoomTransform(svg.node());
drawLine
.attr('visibility', 'visible')
.attr('x1', sourceNode.x)
.attr('y1', sourceNode.y)
// Remove the currentZoom offset and observe the line being jank
.attr('x2', d3.pointer(event)[0] - currentZoom.x)
.attr('y2', d3.pointer(event)[1] - currentZoom.y);
}
})
I am building a d3js chart that plots some dots on a gradient scale of agree to disagree. I need it to be able to zoom and pan and I have all of that working except for a rectangle holding a linearGradient. The gradient zooms just as I need it, but it scales up both horizontally AND vertically, expanding past the original 20px height:
Or shrinking excessively:
I tried to use a clip path which is clearly not working, it seems that the clip path scales with the gradient. How can I clamp the rectangle to the axis and keep it the same size?
Here is my block
Thank you!!
There are two primary challenges from what I can see:
the rectangle filled with the gradient scales on the y axis as you note.
the clip path isn't working as intended.
Let's drop the clip path, that'll make things a bit easier. Secondly, let's not scale the rectangle at all when we zoom (we can keep the translate that is applied though).
Now that I've destroyed what we had, let's build it back up.
Instead of scaling the rectangle with transform(scale()) let's modify its width directly. If d3.event.transform.k is the factor at which we were scaling (both x and y), let's just modify the width of the rectangle. Instead of:
gradientScale
.attr("transform","translate("+d3.event.transform.x+","+(height- spacer*3)+")scale("+d3.event.transform.k+")");
Let's use:
gradientScale
.attr("transform", "translate( " +d3.event.transform.x+ " , " + (height - spacer*3) + ")")
.attr("width", width * d3.event.transform.k);
By removing the scaling, the above won't warp any coordinates, which won't lead to stretching of the y axis. It doesn't modify any y coordiantes, so the bar stays where it is height wise with the same height.
It does modify the width - by the same amount we were scaling it before (the rectangle's width at k = 1 is width). This achieves the stretching of the scale as we zoom in. The x translate factor is unchanged.
Here's a bl.ock of the modified code.
My initial thought before looking closely was to try a completely different approach. So, for comparison, here's a completely different approach modifying the axis itself.
I'm trying to remove zoom completely from a svg.
zoom = d3.behavior.zoom()
.x(userNodesScaleX)
.y(userNodesScaleY)
.on("zoom", zoomed);
userMapSvg.call(zoom);
And this has added a 'rect.background' to the top of the SVG, which prevent the mouse event from reaching the other elements in the SVG.
So I decide to remove the zoom completely. remove the event, remove that rect. How can I do that?
Current code is
removeZoom = d3.behavior.zoom()
.on("zoom", null);
which doesn't work. It only toggles the event.
To stop any future zooming from transforming the page, remove the listener:
zoom.on("zoom", null)
To undo previous zoom transformations:
zoom.scale(1).translate([0,0]).event(userMapSvg)
http://bl.ocks.org/1wheel/6414125
The buttons at the top of the bl.ocks show both behaviors.
If neither work/are what you're looking for, posting a working example of the problem would be extremely helpful. You might also want to look through the zoom documentation.
Another way that I found to work is to set the zoom's extent and .extent and translateExtent to the width and height element ( thus disabling the zoom altogether ). And of course to set the scaleExtent to [1,1].
Try
userMapSvg.on(".zoom", null);
Transitions in combination with rotations have odd results.
Here is a fiddle with my problem: http://jsfiddle.net/emperorz/E3G3z/1/
Try clicking on each square to see the varying behaviour.
Please forgive the hacked code, but if I use transition with rotation (and x/y placement) then it loops about.
I have tried:
1) all in the transform (rotate then translate), and that seems mostly okay. A little wobbly.
2) just rotate in the transform, positioned using x/y attributes. Flies all over the place, but ends up at the correct spot. Very weird.
3) all in the transform (translate then rotate), flies away, and ends up in the (completely) wrong place.
Hmmm. Strange.
Is there a correct approach to rotating shapes with transitions?
Intuitively, it would be good if the second option worked.
Thanks
To rotate an SVG object on an arbitrary axis, you need two transformations: translate (to set the axis) and rotate. What you really want is to apply the translate fully first and then rotate the already moved element, but it appears that translate and rotate operate independently and simultaneously. This ends at the right place, but animating the translate is essentially moving the axis during rotation, creating the wobble. You can isolate the translate from the rotate by having them occur at separate places in the SVG element hierarchy. For example, take a look at the following:
<g class="outer">
<g class="rect-container">
<rect class="rotate-me" width=200 height=100 />
</g>
</g>
You can center the <rect> on (0,0) with translate (-100, -50). It will wobble if you apply your rotation to the <rect> element, but it will rotate cleanly if you rotate the g.rect-container element. If you want to reposition, scale, or otherwise transform the element further, do so on g.outer. That's it. You now have full control of your transforms.
Finding a <rect>'s center is easy, but finding the center of a <path>, <g>, etc. is much harder. Luckily, a simple solution is available in the .getBBox() method (code in CoffeeScript; see below for a JavaScript version*):
centerToOrigin = (el) ->
boundingBox = el.getBBox()
return {
x: -1 * Math.floor(boundingBox.width/2),
y: -1 * Math.floor(boundingBox.height/2)
}
You can now center your element/group by passing the non-wrapped element (using D3's .node() method)
group = d3.select("g.rotate-me")
center = centerToOrigin(group.node())
group.attr("transform", "translate(#{center.x}, #{center.y})")
For code that implements this on a both a single <rect> and <g> of of 2 rects with repositioning and scaling, see this fiddle.
*Javascript of the above code:
var center, centerToOrigin, group;
centerToOrigin = function(el) {
var boundingBox;
boundingBox = el.getBBox();
return {
x: -1 * Math.floor(boundingBox.width / 2),
y: -1 * Math.floor(boundingBox.height / 2)
};
};
group = d3.select("g.rotate-me");
center = centerToOrigin(group.node());
group.attr("transform", "translate(" + center.x + ", " + center.y + ")");
iirc translate is relative to 0,0 whereas rotate is around the center point of the object
As such, because your shapes are offset from 0,0 (e.g. 100,200, or 200,100) they end up migrating when translated. This can be seen by changing the offsets for Diamond3 to [50,50] - much smaller migration around the screen
The solution would be rebase the 0,0 point to the center of the diamond. There is a way to do this in D3 - but I can't remember what it is off the top of my head :(
When I use tipsy on my d3 force directed graph I have a problem: when I set the tipsy gravity to west, the tipsy begins at the upper left corner of my circle. How can I make it begin on the right side of my circle?
Here is the sample of the code I use in d3:
var node = vis.selectAll("g.node")
.data(json.nodes)
.enter().append("svg:g");
node.append("svg:circle")
.attr("r", function(d){return d.credits *5+"px";})
.style("fill", "orange");
$('svg circle').tipsy({
gravity: 'w',
html: true,
title: function() {
var d = this.__data__,
name = d.name;
return name;
}
});
Edit In this question: https://stackoverflow.com/a/10806220/1041692 they say the following:
You could try adding the tooltip to an svg:g that you overlay with the
actual circle, but give zero width and height. Currently it's taking
the bounding box and putting the tooltip at the edge. Playing around
with tipsy's options might help as well.
But either I do it wrong or it doesn't work, it didn't solve my problem.
EDIT 2 This problem also depends on the browser, in chrome the tipsy element is attached on the top left corner of the circle whereas I would like it to be attached on the middle of the right side of the circle. In Firefox, the tipsy appears on the top left of the whole webpage.
The D3 tipsy tutorial actually uses a modified version of tipsy:
http://bl.ocks.org/1373263
It is slightly tweaked to correctly calculate bounding boxes of SVG elements. So copy that source code, rather than using tipsy downloaded from the tipsy site.