d3.js - Allow brushing and zooming on same chart - d3.js

UPDATE
I finally managed to get a working solution to this challenge. Zoom incorporates event.stopPropagation(). This means that any events on elements contained within a parent element will not "bubble up" to the parent element, as per:
https://developer.mozilla.org/en-US/docs/Learn/JavaScript/Building_blocks/Events#Event_bubbling_and_capture
This is how I understand it working and what I have rationalised to get my solution functional:
For brushing, the event will propagate up to ancestor elements. In my case, I have appended a 'rect' element to the svg, as per the examples in the docs, and attached the zoom behaviour to the rect. Therefore, the element with the zoom action is not a parent of the element with the brush action.
I changed the code by instead attaching the zoom behaviour to the svg (rather than the rect) so that it is an ancestor of the group element with the brush action, this will allow the action on the g element to bubble up to the svg. Now zoom can also fire as the event will bubble up from the g element.
I then put a check to see if control was clicked or not, and depending on the outcome I ran the appropriate zoom or brush function. I guess another method may be to check the source event fired, and then run only if the event was the one that we want to trigger either the zoom or brush action.
Good day,
I am having an issue where the zoom action is consuming the events on a line chart. I have seen a number of examples using context and focus, and zoom and drag with discrete elements, but none with allowing zooming and brushing on the same line/area chart.
Objective: To allow a user to either hold down ctrlKey or shiftKey to enable panning, and without the key being held down the chart will pan (using d3-zoom).
Problem: Zooming is consuming the events, as per the d3.js documentation. This prevents the brushing from ever being triggered. This means that even if I use a filter and allow the ctrlKey to be used, and then use an if-block to determine if ctrlKey has been pressed or not, the event will still never get passed the zoom handler to the brush.
I noted in early versions of d3 (seems prior to v4) this co-existing of events was not an issue for my use case, but seemed to have caused problems with multiple events being fired simultaneously for other use-cases, which caused problems. To resolve, it seems that preventDefault and stopPropagation have been implemented on the d3-zoom behaviour. This prevents allowing the event to propagate to the brush handler and then using custom logic to either zoom (in my case just pan) or brush.
Question: Does anyone know any means to allow the events to propagate without changing the d3.js code itself? I would really like be able to pan or zoom depending on whether ctrlKey is pressed, without using a toggle button to disable pointer events.
Code:
const MARGIN = { TOP: 10, BOTTOM: 80, LEFT: 120, RIGHT: 10 }
const WIDTH = 1000 - MARGIN.LEFT - MARGIN.RIGHT;
const HEIGHT = 500 - MARGIN.TOP - MARGIN.BOTTOM;
svg = d3.select(element.current)
.append("svg")
.attr("width", WIDTH + MARGIN.LEFT + MARGIN.RIGHT)
.attr("height", HEIGHT + MARGIN.TOP + MARGIN.BOTTOM);
graph = svg.append("g")
.attr("transform", `translate(${MARGIN.LEFT-50}, ${MARGIN.TOP})`)
.attr("class", "graph")
svg.append("defs")
.append("svg:clipPath")
.attr("id", "clip")
.append("svg:rect")
.attr("width", WIDTH )
.attr("height", HEIGHT );
x = d3.scaleTime()
.range([0, WIDTH]);
y = d3.scaleLinear()
.range([HEIGHT , 0]);
panBehaviour = d3.zoom()
.extent([[0, 0], [WIDTH, HEIGHT]])
.translateExtent([[0, 0], [WIDTH, HEIGHT]])
.scaleExtent([1, 1])
.filter( ()=>!d3.event.button)
.on('zoom', panPlot)
pan = svg.append('rect')
.attr('class', 'pan')
.attr("transform", `translate(${MARGIN.LEFT-50}, ${MARGIN.TOP})`)
.attr("width", WIDTH )
.attr("height", HEIGHT )
.call(panBehaviour);
brush = d3.brushX()
.extent([ [0, 0], [WIDTH, HEIGHT] ])
.on("end", zoom)
graph.append('g')
.attr('class', 'brush')
.call(brush)
path = graph.append('path');
path.attr("clip-path", "url(#clip)");
// The panning function replaced with a log statement
panPlot() {
if (d3.event.sourceEvent.ctrlKey){
console.log("PANNING");
}
};
// The zoom function replaced with a log statement
zoom(){
if (!d3.event.sourceEvent.ctrlKey){
console.log("ZOOMING");
}
};
A very similar question was asked here, but with no solution. D3 v4 Brush and Zoom on the same element (without mouse events conflicting)

You can disable/enable pan and brush behaviors each time ctrl key is pressed/released.
These two behaviors can't coexist, so you have to disable one.
The answer is based on d3 v5.
const noZoom = d3.zoom();
const doc = d3.select(document);
doc.on('keydown', () => {
if (d3.event.key === "Control") {
pan.call(noZoom);
}
})
doc.on('keyup', () => {
if (d3.event.key === "Control") {
pan.call(panBehaviour);
}
})

Related

Capturing mouseover events on two overlapping elements

So I have a d3 chart with a rect overlay to hold crosshair elements on mouseover events. Under the overlay I have other rects displaying data that have mouseover event handlers also, but The overlay is blocking mouseover events form triggeron the children rects below.
let chartWindow = svg
.append("g");
/* this holds axis groups, and cadlestick group*/
let candleStickWindow = chartWindow.append("g")
//this event never fires
.on('mousemove', ()=>console.log('mouse move'));
let candlesCrosshairWindow = chartWindow
.append("rect")
.attr("class", "overlay")
.attr("height", innerHeight)
.attr("width", innerWidth)
.on("mouseover", function() {
crosshair.style("display", null);
})
.on("mouseout", function() {
crosshair.style("display", "none");
removeAllAxisAnnotations();
})
.on("mousemove", mousemove);
The CrosshairWindow has CSS property pointer-events: all. If I remove that, I get my events to fire on the candleStickWindow but not the CrosshairWindow. How can I get mouse events onto both of the elements??
Thanks for any help!
Update
I changed the crosshair rect element to be on the bottom and it kinda works, the candlestick bars mouseover event works but it blocks the crosshair from working.
One solution that comes to mind might use event bubbling which, however, only works if the events can bubble up along the same DOM sub-tree. If, in your DOM structure, the crosshairs rectangle and the other elements do not share a common ancestor to which you could reasonably attach such listener, you need to either rethink your DOM or resort to some other solution. For this answer I will lay out an alternative approach which is more generally applicable.
You can position your full-size rect at the very bottom of your SVG and have its pointer-events set to all. That way you can easily attach a mousemove handler to it to control your crosshairs' movements spanning the entire viewport. As you have noticed yourself, however, this does not work if there are elements above which have listeners for that particular event type attached to them. Because in that case, once the event has reached its target, there is no way propagating it further to the underlying rectangle for handling the crosshairs component. The work-around is easy, though, since you can clone the event and dispatch that new one directly to your rectangle.
Cloning the event is done by using the MouseEvent() constructor passing in the event's details from the d3.event reference:
new MouseEvent(d3.event.type, d3.event)
You can then dispatch the newly created event object to your crosshairs rect element by using the .dispatchEvent() method of the EventTarget interface which is implemented by SVGRectElement:
.dispatchEvent(new MouseEvent(d3.event.type, d3.event));
For lack of a full example in your question I set up a working demo myself illustrating the approach. You can drag around the blue circle which is a boiled down version of your crosshairs component. Notice, how the circle can be seamlessly moved around even when under the orange rectangles. To demonstrate the event handlers attached to those small rectangles they will transition to green and back to orange when entering or leaving them with the mouse pointer.
const width = 500;
const height = 500;
const radius = 10;
const orange = d3.hsl("orange");
const steelblue = d3.hsl("steelblue");
const limegreen = d3.hsl("limegreen");
const svg = d3.select("body")
.append("svg")
.attr("width", width)
.attr("height", height);
const target = svg.append("rect")
.attr("x", 0)
.attr("y", 0)
.attr("width", width)
.attr("height", height)
.attr("fill", "none")
.attr("pointer-events", "all")
.on("mousemove", () => {
circle.attr("cx", d3.event.clientX - radius);
circle.attr("cy", d3.event.clientY - radius);
});
const circle = svg.append("circle")
.attr("r", radius)
.attr("fill", steelblue)
.attr("pointer-events", "none");
const rect = svg.selectAll(null)
.data(d3.range(3).map(d => [Math.random() * width, Math.random() * height]))
.enter().append("rect")
.attr("x", d => d[0])
.attr("y", d => d[1])
.attr("width", 50)
.attr("height", 50)
.attr("fill", orange)
.attr("opacity", 0.5)
.on("mouseover", function() {
d3.select(this).transition().attr("fill", limegreen);
})
.on("mousemove", function() {
target.node().dispatchEvent(new MouseEvent(d3.event.type, d3.event));
})
.on("mouseout", function() {
d3.select(this).transition().attr("fill", orange);
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>

Is it possible to eliminate lag in rendering the mouse location for d3?

I have some code that displays an SVG item above the cursor location. I've noticed that the object lags behind the cursor by more than I'd expect if it were just a frame behind.
var width = 600,
height = 200;
var mouseLocation = [0,0];
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
var cursor = svg.append('circle')
.attr("r", 2)
.attr("fill", 'darkblue');
// update mouse location
d3.select(window).on("mousemove", () => {
mouseLocation = d3.mouse(svg.node());
});
// show mouse
d3.timer(function() {
cursor.attr("transform", 'translate(' + mouseLocation + ')');
});
<script src="http://d3js.org/d3.v4.min.js"></script>
You can see the delay by smoothly sliding the mouse from one side to the other. The blue circle lags behind the cursor by a bit. (At least on Chrome 57 on Windows)
I've tried a couple variations including calling circle.attr within the mousemove callback function. But there's always a bit of a delay.
Edit Things I've tried unsuccessfully:
setting the cursor transform in the mousemove event
Checking the elapsed time since the previous mousemove call to only update it every 16 ms.
svg.style("shape-rendering", "optimizeSpeed");
Using the mousemove event for the svg rather than the window
Using the d3.event.pageX and d3.event.pageY rather than d3.mouse(svg.node())
I get the same issue using <canvas> demo
In the first version of this answer (look at the edit history) I tried d3.interval and setInterval, with no difference.
However, I believe that, maybe, this question has no answer (in the sense of what OP wants), because the lag depends on D3 handling the mousemove event. It's also possible that the issue here is not even D3 related, but depending on how the user agent (browser) controls the mousemove event frequency.
This can be seen if we put the code for moving the circle inside the very mousemove function:
d3.select(window).on("mousemove", () => {
mouseLocation = d3.mouse(svg.node());
cursor.attr("transform", 'translate(' + mouseLocation + ')');
});
And the lag is still the same:
var width = 600,
height = 200;
var mouseLocation = [0,0];
var svg = d3.select("div").append("svg")
.attr("width", width)
.attr("height", height);
var cursor = svg.append('circle')
.attr("r", 2)
.attr("fill", 'darkblue');
// update mouse location
d3.select("div").on("mousemove", () => {
cursor.attr("transform", 'translate(' + (d3.event.pageX - 16) + "," + (d3.event.pageY - 16) + ')');
});
<script src="http://d3js.org/d3.v4.min.js"></script>
<div></div>

D3: How to refresh a chart with new data?

I have created a d3 donut chart. Here is my code:
var width = 480;
var height = 480;
var radius = Math.min(width, height) / 2;
var doughnutWidth = 30;
var color = d3.scale.category10();
var arc = d3.svg.arc()
.outerRadius(radius - 10)
.innerRadius(radius - 70);
var pie = d3.layout.pie()
.sort(null)
.value(function(d) { return d[1]; });
var dataset = settings.dataset;
console.log(dataset);
var svg = d3.select("body")
.append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")");
var path = svg.selectAll('path')
.data(pie(dataset))
.enter()
.append('path')
.attr('d', arc)
.attr('fill', function(d, i) {
return color(d.data[0]);
})
I have a simple form on my web page which displays a dropdown menu with several options. Every time a user changes a value on the form a new dataset is sent to my script (settings.dataset) and the donut is redrawn on the page.
Problem is, some of the values from the previous dataset remain in the DOM. In the console output below, you can see that the second dataset only has two elements. The third one is from the previous dataset. This is messing up the chart, as it is displaying a value that shouldn't be there.
My question: how do I clear the old values? I've read up on .exit() and .remove(), but I can't get my head around these methods.
Create one function that (re)draws the pie when it's created and when it's updated.
New data should be added to pie using enter() and old data should be removed using exit().remove()
It is as simple as this:
path.enter().append("path")
.attr("fill", function(d, i) { return color(i); })
.attr("d", arc)
.each(function(d) {this._current = d;} );
path.transition()
.attrTween("d", arcTween);
path.exit().remove()
Full working code -> JSFIDDLE
There are two steps to implement the 'redraw' effect you want:
First, I suppose you want the svg canvas to be drawn only once when the page is loaded for the first time, and then update the chart in the svg instead of remove and redraw the svg:
var svg = d3.select("body")
.selectAll("svg")
.data([settings.dataset]);
// put data in an array with only one element
// this ensures there is only one consistent svg on the page when data is updated(when the script gets executed)
svg.enter().append("svg")
Second, understanding enter(), exit(), there are many great tutorials about this. In your case, I would suggest to draw the donut something like this:
var path = svg.selectAll(".donut")
.data(settings.data)
// bind data to elements, now you have elements belong to one of those
// three 'states', enter, exit, or update
// for `enter` selection, append elements
path.enter().append("path").attr("d", arc).attr("fill", "teal")
// for `update` selection, which means they are already binded with data
path.transition().attrTween("d", someFunction) // apply transition here if you want some animation for data change
// for `exit` selection, they shouldn't be on the svg canvas since there is no corresponding data, you can then remove them
path.exit().remove()
//remove and create svg
d3.select("svg").remove();
var svg = d3.select("body").append("svg").attr("width","960").attr("height", "600"),
inner = svg.append("g");

D3 zoom pan stutter

I'm experiencing 'stutter' with the D3 drag behavior.
Seems to be a similar problem to "Stuttering" drag when using d3.behavior.drag() and transform
However the solution does not seem to work for the zoom behavior.
Here is an example of the issue: (try dragging the rectangle)
http://jsfiddle.net/EMNGq/109/
blocks = [
{ x: 0, y: 0 }
];
var translate_var = [0,0];
zoom_var = d3.behavior.zoom()
.on("zoom", function(d) {
d.x = d3.event.x;
d.y = d3.event.y;
draw();
});
svg = d3.select("body")
.append("svg:svg")
.attr("width", 600)
.attr("height", 600);
function draw() {
g = svg.selectAll("g")
.data(blocks);
gEnter = g.enter().append("g")
.call(zoom_var);
g.attr("transform", function(d) { return "translate("+translate_var[0]+","+translate_var[1]+")"; });
gEnter.append("rect")
.attr("height", 100)
.attr("width", 100);
}
draw()
You zoom in or drag the element, but then translate the same element. Because the translation is relative, it results in this stuttering.
As stated in the documentation for Zoom Behavior:
This behavior automatically creates event listeners to handle zooming and panning gestures on a container element. Both mouse and touch events are supported.
Contrast it to the documentation for Drag Behavior:
This behavior automatically creates event listeners to handle drag gestures on an element. Both mouse events and touch events are supported.
Your solution is inverse to the similar question. Call your zoom function on the container.
svg = d3.select("body")
.append("svg:svg")
.attr("width", 600)
.attr("height", 600)
.call(zoom_var);
Here's the demo.
You might also be interested in the actual zoom. To do that simply add the scale to your transform rule. Here's the demo with zoom enabled.

Disable d3 brush resize

Is it possible to have a brush that is a static width ie a brush that is not resizable? It would still need to be draggable. There doesn't seem to be anything indicating whether it's possible in the documentation.
Resize can be prevented by hiding the resize handles via css
g.brush>.resize {
display: none;
}
There's no explicit option for that, but all you need to do is reset the domain in the brush event handler, for example
var brush = d3.svg.brush().on("brush", brushed);
function brushed() {
brush.extent(desiredDomain);
}
In D3.js v4.0 you can disable the pointer events in CSS for the SVG elements (handle rectangle).
.handle {
pointer-events: none;
}
Actually, manually resetting the extent does not seem to work well.
The solution I've found, which is hidden in the slider example, is to remove the resize element altogether:
// Create the brush normally.
var brush = d3.svg.brush()
.x(xScale)
.on("brush", brushed);
// Add it to a SVG group
var slider = context.append("g")
.attr("class", "x brush")
.call(brush);
// Remove the resize control
slider.selectAll(".resize").remove();
I encountered exactly the same problem. I was using D3 4.0 and need to implement a fixed size brush along the x axis. I used drag.filter() function and modified elements inside .brush to achieve this:
var brush = d3.brushX()
.filter(function () {return d3.mouse(this)[0] > brushStart && d3.mouse(this)[0] < brushStart + brushWidth})
.extent([[0, 0], [width, height]])
.on("end", brushended)
.on('brush', beingBrushed);
svg.append('g')
.attr('class', 'brush')
.call(brush)
.call(brush.move, [0, brushWidth]); //initial position
d3.selectAll('.brush>.handle').remove();
function brushended() {
var s = d3.event.selection;
if (s!== null) {
brushStart = s[0];
}
}
This way the brush allows dragging but not resizing.
I just removed the .handle elements from graph with the brush:
contextGraph.selectAll('.handle').remove();

Resources