D3 zoom breaks mouse wheel zooming after using zoomIdentity - d3.js

When I use d3-zoom and programatically call the scaleTo function using zoomIdentity I cannot zoom using the mouse wheel anymore.
How do I fix this issue?
https://observablehq.com/d/8a5dfbc7d858a16b
// mouse wheel zoom not working because use of zoomIdentity
chart = {
const svg = d3.create("svg")
.attr("viewBox", [0, 0, width, height])
.style("display", "block");
const zoom = d3Zoom.zoom()
svg.call(zoom);
const zoomArea = svg.append('g');
zoom.on('zoom', (e) => {
zoomArea.attr("transform", e.transform)
})
zoomArea.append('circle')
.attr("cx", width/2)
.attr("cy", height/2)
.attr("r", 20)
zoom.scaleTo(svg, d3Zoom.zoomIdentity)
return svg.node();
}

The second parameter of zoom.scaleTo(svg, d3Zoom.zoomIdentity) accepts a k scaling factor (e.g., 2 for 2x zoom). The method zoom.scaleTo is intended to be used when you want to set the zoom level, but not the translation (x and y positions).
If you want to set the whole transform to the zoom identity (which resets both the zoom level and the x and y positions), the method is zoom.transform(svg, d3Zoom.zoomIdentity).
If you indeed just want to reset the scale, you can use zoom.scaleTo(svg, d3Zoom.zoomIdentity.k), or simply zoom.scaleTo(svg, 1).

Related

d3 v6 pointer function not adjusting for scale and translate

I am upgrading my app from d3 v5 to v6 and am having an issue migrating the d3.mouse functionality. In my app I apply a transform to the top level svg group and use the zoom functionality to zoom and pan (scale and translate). When I double click on the screen I take the mouse position and draw a square.
Now I am replacing the d3.mouse function with d3.pointer. In my double click event I get the mouse position by calling d3.pointer(event). However this function is not producing a position that is relative to where my top level svg group is positioned and scaled. When I remove the translate and scale from the top level group, the position matches up.
In the older version of d3 I could call d3.mouse(this.state.svg.node()) and it would produce the exact position I clicked corrected for pan and scale. Is this available in version 6? If not, is there a clean way I can adjust for this? The new event object is coming through with a host of different position properties: pagex, offsetx, screenx, x. None of these is producing the position I clicked on. Is there a clean way to acheive this?
You could specify a container element which would factor in a zoom transform in v5 and earlier:
d3.mouse(container)
Returns the x and y coordinates of the current event relative to the specified container. The container may be an HTML or SVG container element, such as a G element or an SVG element. The coordinates are returned as a two-element array of numbers [x, y]. (source)
In d3v6 you can specify this by using the second parameter of d3.pointer:
d3.pointer(event[, target])
Returns a two-element array of numbers [x, y] representing the coordinates of the specified event relative to the specified target. event can be a MouseEvent, a PointerEvent, a Touch, or a custom event holding a UIEvent as event.sourceEvent.
...
If the target is an SVG element, the event’s coordinates are transformed using the inverse of the screen coordinate transformation matrix. If the target is an HTML element, the event’s coordinates are translated relative to the top-left corner of the target’s bounding client rectangle. (source)
So as far as I'm aware, you should be use:
d3.pointer(event,this.state.svg.node());
Instead of
d3.mouse(this.state.svg.node());
Here's a d3v6 example:
var svg = d3.select("body")
.append("svg")
.attr("width", 500)
.attr("height", 200);
var rect = svg.append("rect")
.attr("width",500)
.attr("height",200)
.attr("fill", "#eee")
var g = svg.append("g");
var zoomed = function(event) {
g.attr("transform", event.transform);
}
rect.call(d3.zoom().on("zoom",zoomed))
.on("click", function(event) {
var xy = d3.pointer(event,g.node());
g.append("circle")
.attr("r", 5)
.attr("cx", xy[0])
.attr("cy", xy[1])
.attr("fill","crimson");
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/6.0.0/d3.min.js"></script>
Adapting this v5 example:
var svg = d3.select("body")
.append("svg")
.attr("width", 500)
.attr("height", 200);
var rect = svg.append("rect")
.attr("width",500)
.attr("height",200)
.attr("fill", "#eee")
var g = svg.append("g");
var zoomed = function() {
g.attr("transform", d3.event.transform);
}
rect.call(d3.zoom().on("zoom",zoomed))
.on("click", function() {
var xy = d3.mouse(g.node());
g.append("circle")
.attr("r", 5)
.attr("cx", xy[0])
.attr("cy", xy[1])
.attr("fill","crimson");
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>

create a circle on a link between nodes on click event in d3

Want to add a circle on the links between nodes on click and I should be able to attach a drag event to the circle so that when I drag a circle, the link should move to . where I am going wrong in this?
var dragCircle = d3.behavior.drag()
.on('dragstart', function(){
d3.event.sourceEvent.stopPropagation();
})
.on('drag', function(d,i){
var x = d3.event.x;
var y = d3.event.y;
d3.select(this).attr("transform", "translate(" + x + "," + y + ")");
});
//I want to attach circle to the link so that when I drag circle, line should move too.
function drawCircle(x, y, size) {
svg.selectAll(".edge").append("circle")
.attr('class', 'linkcirc')
.attr("cx", x)
.attr("cy", y)
.attr("r", size)
.style("cursor", "pointer")
.call(dragCircle);
}
//catching the mouse position to decide to place the circle
edge.on("click",function() {
var coords = d3.mouse(this);
drawCircle(coords[0], coords[1],3);
});
SVG will not allow you to create a circle as a child of a line (and your code is creating one circle for every link on every click). Instead of this:
svg.selectAll(".edge").append("circle") # appends one circle to each edge
Try this:
svg.append("circle") # appends a single circle to the SVG image
After changing your fiddle accordingly I was able to fire the drag event, but it still needs work. Using the drag behaviour you probably want to look at the event.dx and event.dy values rather than the absolute values, and you can simply change the circle's cx and cy instead of applying a translation (if that's easier). See https://jsfiddle.net/pzej8tkq/3/ for ideas.

set d3 zoom scaleExtent to positive values

Well, first of all, here is a jsfiddle to illustrate what I would like to ask
So I am trying to implement a zoom behaviour. I managed to limit the zooming by setting a translate to the zoom function, (which I don't really quite understand, Just got it from another SO question), and a scaleExtent([1,10]) on the zoom behaviour
So this is the zoom behavior:
zoom = d3.behavior.zoom()
.x(xScale)
.y(yScale)
.scaleExtent([1, 10])
.on("zoom", zoomed)
and this is the zoomed function:
zoomed = ->
t = zoom.translate()
s = zoom.scale()
tx = t[0]
ty = t[1]
tx = Math.min(0, Math.max(w * (1-s), t[0]))
ty = Math.min(0, Math.max((h-padding) * (1-s), t[1]))
zoom.translate([tx, ty])
svgContainer.select("g.x.axis").call xAxis
svgContainer.select("g.y.axis").call yAxis
svgContainer.selectAll("line.matched_peak")
.attr("x1", (d) -> return xScale(d.m_mz) )
.attr("y1", h - padding)
.attr("x2", (d) -> return xScale(d.m_mz) )
.attr("y2", (d) -> return yScale(d.m_intensity) )
However, setting the scaleExtent to
.scaleExtent([1, 10]) ,
makes that one particular point with a value of 1 in the y scale is never going to be displayed (first one on the left in the jsfiddle).
But setting
.scaleExtent([0, 10])
disables the limit to zoom, and the user may zoom and pan below 0 in the y axis.
Also tried .scaleExtent([0.1, 10]) but this also allows zoom and pan below 0.
So How could I allow zoom only to positive values from 0 (including a value of 1) ?
And what function can be used to avoid the lines show beyond the axis? Is not that implicit when I use the translate function in the zoom?
The scale proceeds in a geometric series -- that is, the scale step before 1 is not 0, but 0.5. Limiting your scale extent to
.scaleExtent([0.5, 10])
should do what you want. I've also added .nice() to your scale definitions to get round numbers at the ends.
As for the clipping, no, this is not implicit in the translation. You need to clip explicitly using a clip path, which in your case can be defined as follows.
cp = svgContainer.append("defs").append("clipPath").attr("id", "cp")
.append("rect")
.attr("x", padding)
.attr("y", padding)
.attr("width", w - 2 * padding)
.attr("height", h - 2 * padding)
All you then need to do is use it when adding the elements:
msBars = svgContainer.selectAll('line.matched_peak')
.data(jsonFragmentIons)
.enter()
.append("line")
.attr("clip-path", "url(#cp)")
Complete example here.

Getting Screen Positions of D3 Nodes After Transform

I'm trying to get the screen position of a node after the layout has been transformed by d3.behavior.zoom() but I'm not having much luck. How might I go about getting a node's actual position in the window after translating and scaling the layout?
mouseOver = function(node) {
screenX = magic(node.x); // Need a magic function to transform node
screenY = magic(node.y); // positions into screen coordinates.
};
Any guidance would be appreciated.
EDIT: 'node' above is a force layout node, so it's x and y properties are set by the simulation and remain constant after the simulation comes to rest, regardless of what type of transform is applied.
EDIT: The strategy I'm using to transform the SVG comes from d3's zoom behavior, which is outlined here: SVG Geometric Zooming.
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.call(d3.behavior.zoom().scaleExtent([1, 8]).on("zoom", zoom))
.append("g");
svg.append("rect")
.attr("class", "overlay")
.attr("width", width)
.attr("height", height);
svg.selectAll("circle")
.data(data)
.enter().append("circle")
.attr("r", 2.5)
.attr("transform", function(d) { return "translate(" + d + ")"; });
function zoom() {
svg.attr("transform", "translate(" + d3.event.translate + ")scale(" + d3.event.scale + ")");
}
It's pretty straightforward. d3's zoom behavior delivers pan and zoom events to a handler, which applies the transforms to the container element by way of the transform attribute.
EDIT: I'm working around the issue by using mouse coordinates instead of node coordinates, since I'm interested in the node position when the node is hovered over with the mouse pointer. It's not exactly the behavior I'm after, but it works for the most part, and is better than nothing.
EDIT: The solution was to get the current transformation matrix of the svg element with element.getCTM() and then use it to offset the x and y coordinates to a screen-relative state. See below.
It appears the solution to my original question looks something like this:
(Updated to support rotation transforms.)
// The magic function.
function getScreenCoords(x, y, ctm) {
var xn = ctm.e + x*ctm.a + y*ctm.c;
var yn = ctm.f + x*ctm.b + y*ctm.d;
return { x: xn, y: yn };
}
var circle = document.getElementById('svgCircle'),
cx = +circle.getAttribute('cx'),
cy = +circle.getAttribute('cy'),
ctm = circle.getCTM(),
coords = getScreenCoords(cx, cy, ctm);
console.log(coords.x, coords.y); // shows coords relative to my svg container
Alternately, this can also be done using the translate and scale properties from d3.event (if rotation transforms are not needed):
// This function is called by d3's zoom event.
function zoom() {
// The magic function - converts node positions into positions on screen.
function getScreenCoords(x, y, translate, scale) {
var xn = translate[0] + x*scale;
var yn = translate[1] + y*scale;
return { x: xn, y: yn };
}
// Get element coordinates and transform them to screen coordinates.
var circle = document.getElementById('svgCircle');
cx = +circle.getAttribute('cx'),
cy = +circle.getAttribute('cy'),
coords = getScreenCoords(cx, cy, d3.event.translate, d3.event.scale);
console.log(coords.x, coords.y); // shows coords relative to my svg container
// ...
}
EDIT: I found the below form of the function to be the most useful and generic, and it seems to stand up where getBoundingClientRect falls down. More specifically, when I was trying to get accurate SVG node positions in a D3 force layout project, getBoundingClientRect produced inaccurate results while the below method returned the circle element's exact center coordinates across multiple browsers.
(Updated to support rotation transforms.)
// Pass in the element and its pre-transform coords
function getElementCoords(element, coords) {
var ctm = element.getCTM(),
x = ctm.e + coords.x*ctm.a + coords.y*ctm.c,
y = ctm.f + coords.x*ctm.b + coords.y*ctm.d;
return {x: x, y: y};
};
// Get post-transform coords from the element.
var circle = document.getElementById('svgCircle'),
x = +circle.getAttribute('cx'),
y = +circle.getAttribute('cy'),
coords = getElementCoords(circle, {x:x, y:y});
// Get post-transform coords using a 'node' object.
// Any object with x,y properties will do.
var node = ..., // some D3 node or object with x,y properties.
circle = document.getElementById('svgCircle'),
coords = getElementCoords(circle, node);
The function works by getting the transform matrix of the DOM element, and then using the matrix rotation, scale, and translate information to return the post-transform coordinates of the given node object.
You can try node.getBBox() to get the pixel positions of a tight bounding box around the node shapes after any transform has been applied. See here for more: link.
EDIT:
getBBox doesn't work quite the way I thought. Since the rectangle is defined in terms of the transformed coordinate space it is always relative to the parent <g> and will therefore always be the same for contained shapes.
There is another function called element.getBoundingClientRect that appears to be quite widely supported and it returns its rectangle in pixel position relative to the top left of the browser view port. That might get you closer to what you want without needing to mess with the transform matrix directly.

D3 log scale displaying wrong numbers

I'm trying to wrap my head around the log scales provided by D3.js. It should be noted that as of yesterday, I had no idea what a logarithmic scale was.
For practice, I made a column chart displaying a dataset with four values: [100, 200, 300, 500]. I used a log scale to determine their height.
var y = d3.scale.log()
.domain([1, 500])
.range([height, 1]);
This scale doesn't work (at least not when applied to the y-axis as well). The bar representing the value 500 does not reach the top of the svg container as it should. If I change the domain to [100, 500] that bar does reach the top but the axis ticks does not correspond to the proper values of the bars. Because 4e+2 is 4*10^2, right?
What am I not getting here? Here is a fiddle.
Your scale already reverses the range to account for the SVG y-coordinates starting at the top of the screen -- ie, you have domain([min, max]) and range([max, min]). This means your calcs for the y position and height should be reversed because your scale already calculated y directly:
bars.append("rect")
.attr("x", function (d, i) { return i * 20 + 20; })
.attr("y", function (d) { return y(d); })
.attr("width", 15)
.attr("height", function (d) { return height - y(d); });
Here's an updated Fiddle: http://jsfiddle.net/findango/VeNYj/2/

Resources