D3 v4 - d3.zoom() prevents binding some events except on the Window - javascript

Unhelpful Github discussion led me to ask this question here:
After changing to the new v4.0 D3, seems like using d3.zoom() blocks some events, like:
Calling this:
svg.call(d3.zoom()
.scaleExtent([1, 40])
.translateExtent([[-100, -100], [width + 90, height + 100]])
.on("zoom", zoomed));
And then this:
document.querySelector('svg').addEventListener('mouseup', function(){ alert(1) })
Won't alert anything when clicking anywhere on the SVG.
I really don't want to do:
window.addEventListener("mouseup", function(e){
if( [target or target parent is the specific SVG element] ){
// do something
}
}
Because this will introduce a global non-namespaced event mouseup on the window (this technique seems to only work on the window object) and this event might be removed somewhere else, or happen multiple times. (It isn't easy to encapsulate it, or I simply don't know how).
Any better ideas on how to capture mouseup events on the SVG?
Related to another question of mine: Event capturing namespace
Demo page
Update:
With the help of Mark's answer, here's the working demo

Still not sure I understand your intent, but you might be approaching this from the wrong angle. Instead of trying to add your own events, just work within the events provided by the zoom behavior. The zoom functionality provides for panning where mousedown starts a pan (i.e. zoom start event) and mouseup ends a pan (i.e. zoom end event). So, the real question becomes, how can you differentiate a pan vs a "click" mouseup? To do that, just check if the user moved the mouse:
svg.call(d3.zoom()
.scaleExtent([1, 40])
.translateExtent([[-100, -100], [width + 90, height + 100]])
.on("start", zoomstart)
.on("end", zoomend)
.on("zoom", zoomed));
var mousePos = null;
function zoomstart(){
mousePos = d3.mouse(this);
}
function zoomend(){
var m = d3.mouse(this);
if (mousePos[0] === m[0] && mousePos[1] === m[1]){
alert('mouseup');
}
}
function zoomed() {
svg.attr("transform", d3.event.transform);
gX.call(xAxis.scale(d3.event.transform.rescaleX(x)));
gY.call(yAxis.scale(d3.event.transform.rescaleY(y)));
}
Update fiddle.

Related

D3.js line that follows cursor regardless of zoom

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);
}
})

d3: unable to send mouse events to another svg element (event.target.__data__ is null)

I'm trying to add a brush to a d3 line-chart I wrote, which already supported hovering through a rect to which i've attached mousemove. However, in order to avoid breaking the hover functionality, I need to initialize the d3 brush behind this rect, so that it's not capturing all the mouse events.
For the brush to work, I therefore need to send mouse events from the hover rect to the svg element behind it, on which I've initialized the brush, exactly like in this example, which captures the events from elements in front of the brush:
.on('mousedown', function(){
brush_elm = svg.select(".brush").node();
new_click_event = new Event('mousedown');
new_click_event.pageX = d3.event.pageX;
new_click_event.clientX = d3.event.clientX;
new_click_event.pageY = d3.event.pageY;
new_click_event.clientY = d3.event.clientY;
brush_elm.dispatchEvent(new_click_event);
});
However, using the example, which is from 2013 and uses d3 v3, brush_elm.dispatchEvent(new_click_event); throws an exception:
It appears that d3's brush.js is looking for an event.target.__data__.type, which doesn't exist because it's not a "real" event.
How can I avoid this? Is it no longer possible to "fake" d3 mouse events?
I found this newer example, which actually seems to work: https://bl.ocks.org/mthh/99dc420cd7e276ecafe4ef4bf12c6927
It replaces the problematic code I mentioned with the following:
const brush_elm = vis.select('.brush > .overlay').node();
const new_click_event = new MouseEvent('mousedown', {
pageX: d3.event.pageX,
pageY: d3.event.pageY,
clientX: d3.event.clientX,
clientY: d3.event.clientY,
layerX: d3.event.layerX,
layerY: d3.event.layerY,
bubbles: true,
cancelable: true,
view: window });
brush_elm.dispatchEvent(new_click_event);

Programmatically triggering a D3 brush event

I have seen examples here and here were a brush is triggered in JavaScript. I want to understand the implementation of the first one.
Background
The first example bundles two D3 line charts in a single svg container; classes focus and context, respectively:
The context chart (marked in light blue, above) is the one containing the brush, which can be triggered by a mouse click:
When we look inside its group container, we find the designated brush parameters; under the extent class:
Question 1.
I don't understand what happens in the last two lines, in particular the last line:
function drawBrush(a, b) {
// define our brush extent
// note that x0 and x1 refer to the lower and upper bound of the brush extent
// while x2 refers to the scale for the second x-axis, for context or brush area.
// unfortunate variable naming :-/
var x0 = x2.invert(a*width)
var x1 = x2.invert(b*width)
console.log("x0", x0)
console.log("x1", x1)
brush.extent([x0, x1])
// now draw the brush to match our extent
// use transition to slow it down so we can see what is happening
// set transition duration to 0 to draw right away
brush(d3.select(".brush").transition().duration(500));
// now fire the brushstart, brushmove, and brushend events
// set transition the delay and duration to 0 to draw right away
brush.event(d3.select(".brush").transition().delay(10duration(500))
}
In brush(d3.select(".brush").transition().duration(500));, the current brush parameters are selected with a transition precondition; which is passed to brush, so it can draw the new brush according to the changed brush.extend values.
In brush.event(d3.select(".brush").transition().delay(10duration(500)), it seems that the previous line sets the parameters, after which brush.event executes with the new brush parameters. Can someone make sense of this? How do the brush events apply to this case?
Question 2.
I also don't see how exactly, this event action gets linked back to the focused chart. If find the mechanisms via callbacks quite cryptic:
var brush = d3.svg.brush()
.x(x2)
.on("brush", brushed);
This snippet seems crystal-clear: the brush is made and linked to the brush event listener. On a brush event, brushed will act as the event handler. Furthermore, the scale of context's x-axis x2 is passed to the brush, as it sits on the context chart.
But I'm not quite sure how brushed works:
function brushed() {
x.domain(brush.empty() ? x2.domain() : brush.extent());
focus.select(".area").attr("d", area);
focus.select(".x.axis").call(xAxis);
}
Just to be sure, is it correct that a new axis is generated in focus.select(".x.axis").call(xAxis); with the brush parameters set in x.domain(brush.empty() ? x2.domain() : brush.extent());?
First, there is a typo in the last line. In the code it actually is:
brush.event(d3.select(".brush").transition().delay(1000).duration(500))
Back to your question, the confusion you're facing trying to understand what the brush events have to do with it is quite simple: you're reading the D3 v4 docs, while that code uses D3 v3.
This is brush.event in D3 v3:
brush.event(selection)
If selection is a selection, it dispatches a brush gesture to registered listeners as a three event sequence: brushstart, brush and brushend. This can be useful in triggering listeners after setting the brush extent programatically. (emphasis mine)
As you can clearly see, the first line changes the brush itself (the context), while the second one changes the big area chart (the focus).

How to set zoom step in D3JS v4?

I am trying to achieve consistent behavior between zooming in the view by buttons and mouse scroll. While I am able to specify the zoom step (0.5) using buttons (by calling zoom.scaleTo), I am wondering how I can do the same when using the mouse scroll. Should I somehow do this in "zoom" event callback, and there try to modify the event transform? Or should I capture wheel event's and there try to manually invoke d3.zoom functions? I'd like the mouse scroll to change scale every 0.5 step and maitain the center point of zoom to be the mouse location. Do you have any suggestions what is the best way to achieve this?
var svg = d3.select("body").append("svg")
.attr("width", 600)
.attr("height", 600)
.call(zoom);
var mainContainer = svg.append("g");
please check with JSFiddle example i thinks you can help with this
click here

D3 disable pan & zoom when scrolling

I've rendered a d3 map that has pan and zoom enabled, but when scrolling down the viewport either on desktop or mobile, the window gets stuck zooming in the map.
Is there a way to temporarily disable d3.zoom, while the window is scrolling?
I've seen ways of toggling the zoom/pan using a button as seen here: http://jsfiddle.net/0xncswrk/, but I wanted to know if it's possible without having to add a button. Here's my current zoom logic.
Thanks!
this.zoom = d3.zoom()
.scaleExtent([1, 8])
.on('zoom', () => {
this.svg.attr('transform', d3.event.transform);
});
this.svg = d3.select(this.el).append('svg')
.attr('width', '100%')
.attr('height', this.height)
.attr('class', 'bubble-map__svg-us')
.call(this.zoom)
.append('g');
EDIT: Wow old answer but never saw your comment. Sorry about that. Yeah sorry I forgot to consider mobile zooming.
In the documentation, perhaps this is new, but they recommend having more granular control of what zoom you allow by using zoom.filter. For touch related events, they also support zoom.touchable.
As specified in the d3-zoom documentation
To disable just wheel-driven zooming (say to not interfere with native scrolling), you can remove the zoom behavior’s wheel event listener after applying the zoom behavior to the selection:
selection
.call(zoom)
.on("wheel.zoom", null);
You can also consider just setting the scaleExtent to be [1,1] especially if it's just temporary so it locks the zoom to only one possible scale but preferably you opt for what the documentation says :P
Got here because I was dealing with a similar problem. Perhaps for anyone coming after this, a simple way to deal with this might be to use the filter() method that a zoom() instance provides. That will allow you to toggle between applying or ignoring zoom events altogether. It works a little better than temporarily setting null to watchers because - at least in my experience - the events then still get recorded and stacked. As a consequence, you would zoom in or out in unexpected leaps once you re-enabled the handler. The filter actually really ignores what's going on, it seems. To implement:
let isZooming = true; // Use controls to set to true or false
zoom()
// Make sure we only apply zoom events if zooming is enabled by the user
.filter(() => isZooming)
.on('zoom', event => {
// Specify whatever you want to do with the event
});
Doc: https://github.com/d3/d3-zoom#zoom_filter

Categories

Resources