Brushable and zoomable timeline with d3

1.3k views Asked by At

I want to make a timeline where the user can choose between scrolling to zoom or select a region to zoom. There are some examples of the first like: https://bl.ocks.org/mbostock/4015254 Or zooming in on a area with brush: https://bl.ocks.org/mbostock/f48fcdb929a620ed97877e4678ab15e6
But I cannot find an example that does both. How can I do both? Or are there any examples that I missed?

1

There are 1 answers

0
HamsterHuey On

This is not the most straightforward thing to implement. As you will notice, the brush based zooming does not rely on d3.zoom but instead performs the zooming via listeners that fire on events to do whatever is needed to scale the axes and move the plot elements accordingly.

In comparison, all the scrolling based zoom examples typically rely on d3.zoom which utilizes a d3.zoom() behavior that keeps track of all the transforms performed on the plot while panning/zooming and is solely responsible for updating the various chart elements. The difficulty lies in the fact that the 2 approaches are quite different and if you manually change the chart view via brushing, you need to figure out a way to update the internal zoom transform that d3.zoom references so that it is aware of the changes made via the brush based zoom events.

This is not at all easy to do because d3.zoom was not designed to be fed information from elsewhere and the internal record of transforms that were performed are not meant to be updateable/mutable. You can update the transform via selection.call(zoom.transform, d3.zoomIdentity); but that unfortunately also fires a whole bunch of events related to the actual zoom behavior, which is not something you want since you already handled all the zoom behavior with your brush based zoom. An ugly, but effective workaround that I was able to use to reset the zoom transform was to mutate the actual .__zoom field of the DOM node that is bound to the d3.zoom behavior as follows:

// WARNING: Ugly mutation of __zoom property of pan/scroll-zoom rect to
// reset the transform without having to fire events associated with zoom
// d3.select(".zoom").node().__zoom = {k: 1, x: 0, y: 0}; <-- Fails since __zoom contains other hidden objects
scrollZoom.node().__zoom["k"] = 1;
scrollZoom.node().__zoom["x"] = 0;
scrollZoom.node().__zoom["y"] = 0;

So for example: If you want a 2D brush for rectangle zooming, but also d3.zoom based zooming for panning and mouse-scrolling, then anytime you use the 2D brush to zoom, you will want to reset the d3.zoom transform back to the identity transform as above. This prevents and ugly and jarring jitter in panning/scrolling response when chaining 2D brush based zooming actions with panning/mouse-scrolling actions due to the transform on record with d3.zoom not being in-sync with the view on display (due to the 2D brush based zoom changing the view without d3.zoom's knowledge).

Here is something else that is important to note:

d3.zoom has a limitation in that it currently only supports a common zoom scale for both X and Y axes (Source). This unfortunately means that there is no way to map a 2-D brush based zoom to a d3.zoom based approach since 2D brush based zooming produces different zoom scaling in X and Y. If you want to do things with minimal issues, using a consistent approach, I'd recommend looking into using d3.xyzoom. This is a fork of d3.zoom that implements support for different scales for X and Y axes. This would enable you to calculate the corresponding X and Y zoom scaling and translation values for any 2D brush selection, which you could then feed into d3.zoom, thus enabling you to perform all the zooming using a common approach (which also results in the least amount of code duplication).

That being said, if you are solely interested in a 1-D brush based zoom, you should be able to map that to a d3.zoom approach so that you don't have to deal with 2 different paths for handling the view and scaling of all the axes and other graphical elements in your chart. Here is a good example of this:

https://bl.ocks.org/mbostock/34f08d5e11952a80609169b7917d4172

I apologize for the length of this post and if it is a bit rambling. I am working on putting together a block on my work in a couple of days and I'll try to circle back here and post a link when I get around to doing so. I only started learning D3 a week ago, so I'm learning along the way.