Brushable and zoomable timeline with d3 - javascript

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?

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.

Related

Performant GL Triangles Mapbox GL JS

I am working on trying to create a basic, grid-based, but performant weather-arrow visualization system.
EDIT 2:
Up-to-date version here: ( Mapbox Tracker ) of the system using the workflow which is described below
Usage Instructions:
- Click on Wind icon (on the left)
- Wait for triangles to occupy screen
- Pan time-slider (at the bottom)
As you will observe (especially on larger resolutions or when panning time slider quickly) there is quite a performance hit when drawing the triangles.
I would greatly appreciate any advice on where to start with either using something in the current API which would help, or any ideas on how to tap into the current graphics pipeline with some type of custom buffer where I would only need to rotate, scale, change color of triangles already populated in screen space.
I feel as though my specific use-case would greatly benefit from something like this, I really just don't know how to approach it.
I have a naive implementation running using this workflow:
Create a geojson FeatureCollection source
Create a fill layer
Using Data Driven property: fill-color
Data function:
Get map bounds
Project sw & ne into screen points (map.project(LatLng))
Divide height and width into portions
Loop through width and height portions
Lookup data
Access data rotation property
Create vertices based on center point + size
Rotate vertices
Create Point objects for vertices
Unproject Point Object and wrap map.unproject(Point).wrap()
Create Feature Object, assign Data driven Color
Assign unprojected LatLng as Coordinates to Polygon geometry
Add to Feature Array for Collection
Call setData on layer
So while this works, I'm looking for advice for a more performance friendly approach.
What I'm thinking here is whether I can somehow create a custom layer, one where I only need to draw to screen co-ordinates to represent the data relative to its LatLng point. So that I can draw colored, scaled, rotated triangles in screen space, and then have them update to relevant data from the new relative LatLng position.
E.g. Update some type of Mesh on screen instead of having to: unproject, then update feature collection source using map.getSource('arrows').setData(d), requestAnimationFrame(function) etc.
I've done similar in three.js in other projects but I would much rather use something that is more mapbox native. Does this sound feasible? Am I going to see a decent performance boost if so?
I've not dealt with raw gl calls before etc so I might need a pointer or two in the right direction if its going to need to get as low level as that.
EDIT:
Previous Implementation using gmaps / three.js : volvooceanrace
(wait for button on left to go from grey to black) click on top button which shows a 'wind' label when hovered over, slide red time bar underneath to change data.
Added screenshot of current working implementation
Mapbox GL Arrows
Not sure what was available in 2016, but a reasonable approach these days might be to use symbol layers, and the icon-rotate data-driven property to rotate each icon based on the property of its data point.

How do I use collision detection to fix overlapping images with d3.js?

I'm trying to display a large number of images on a d3 display using T-SNE. The x and y coordinates are pre-calculated, the location on the svg area is adjusted using using translate/zoom.
At the moment they all display using the precalculated coordinates.
and they remain in place as zooming/panning.
I'm looking to use collision detection (like this example) to adjust the images locations slightly so that they don't overlap, but as much as possible maintain the rough global structure.
Here's my attempt so far
https://gist.github.com/GerHarte/329af8ee5ffd8a1f87c5
With this it loads as in the image above, but as soon as I pan or zoom, all the points expand out hugely to a completely different location on the canvas and look like this, they don't seem to overlap, but they're extremely far apart.
Is there something wrong in my code or is there a better way to approach this?
Update:
I followed Lar's answer here, with the slight addition of setting the raw data points to where Lar's code settles since the points are translated when zooming or panning. The results look great (see below), but for a larger number of points (5000+) it seems to crash before converging on a final result.
Are there any suggestions to improve the efficiency with this approach? Going to try the Multi-Foci Forced Directed approach.

D3, combine FishEye with brushing and updated scales

I am working on a project where I have to create a scatterplot with genes denoted by dots. Since there are a lot of genes (20k), they get clustered and are stacked often, even after brushing and zooming in on the brush.
To allow the user to separate them a bit better, I want to implement D3's FishEye plugin, preferably the Cartesian. I implemented the example, but the points are flying outside the screen. My guess is that this happens because if you brush (and zoom in), the scales and axes are updated.
How can I make my FishEye adhere to the current domain of the axes in terms of the distortion range and how can I make the points translate correctly (and not fly outside the screen) ? I have already tried something with the min and maximum x-values, but that doesn't work either.

D3.js scattergraph with large (>500,000) points? Clustering?

I'm looking at plotting a scatterplot with a large number of points (500,000 and upwards).
Currently, we're doing this in Python with Matplotlib. It plots the points, and it provides controls to pan and zoom. I don't believe it provides any clustering or points, it just plots them all - doesn't make much sense at the zoomed out view, I suppose, but you can zoom in and they're all there.
I was looking at doing the chart in JavaScript, to make it a bit easier to distribute. I was looking at D3.js, to see if something similar is feasible there. I did find this example of a basic scatterplot:
http://bl.ocks.org/mbostock/3887118
Firstly, would you be able to plot that number of points? (500,000 and upwards) I was under the impression you couldn't due to the overhead of all the DOM objects? Are there ways around this?
Secondly, is there any kind of clustering available, either a library or even just an example of this being done in D3.js?
Thirdly, if anybody knows any good examples of pan/zoom functionality and clustering, or even just a packaged JS library that handles it, that would be awesome.
Fourth, it would be also nice to have click handlers for each point - and to display some text either in a overlay, or even just in a separate window. Any thoughts on this?
Can you draw half a million points with D3? Sure, but not with SVG. You'll have to use canvas (here's a simple example with 10,000 points that includes brush-based selection: http://bl.ocks.org/emeeks/306e64e0d687a4374bcd) and that means that you no longer have individual elements to assign click handlers to. You will not be able to render half a million points with SVG, because all those DOM elements will choke your interface, as you mentioned.
D3 does include quadtree support that can be leveraged for clustering. It's in use in the above example to speed up search but you could use it to nest elements in proximity at certain scales.
Ultimately, your choices are:
1) Some other library/custom implementation that renders in canvas and polls the mouse position to give you the data element rendered at that point.
2) A sophisticated custom D3 approach that nests elements in proximity and only renders SVG elements appropriate at the zoom level and canvas position (pan) you're at.
Yes, D3.js can be made to work with million scale data with two things:
pre-rendering on the server side. For more see here: https://mango-is.com/blog/engineering/pre-render-d3-js-charts-at-server-side/
By aggregating (or clustering) part of the data so that user can interact and expand the graph if need be. For this use collapsible nodes if you can (http://bl.ocks.org/mbostock/1062288).
Also avoid using force layout. It takes time to settle and converge to a stable positioning.
For clustering libraries, I would pick one up off the shelf. I would choose the scikits library from python, there are many in JavaScript but they are not very robust as they mostly cover k-means or hierarchical clustering. I would precalculate the coordinates using scikits by clustering and then render it using D3.
D3 handles Pan and zoom. Again click handlers and text display are available in D3. (http://bl.ocks.org/robschmuecker/7880033)

D3 Pan+Zoom - Add item when zoomed results in wrong position

I have a regular grid with Pan+Zoom where I draw squares in subsequent orders, when a user click on a button, e.g.
However, if I zoom-in before adding a new item, it gets added to wrong position, i.e.
Demo of the issue http://jsbin.com/dewiq/2/edit
Any ideas?
You're mixing up two different ways of zooming in d3 -- zooming with scales ("semantic" zooming) and zooming with transforms ("geometric" zooming). See this answer for a detailed breakdown of the differences.
You've attached your scales to the zoom behaviour, so when you zoom or pan your scales get modified to reflect the change. However, you're only using that modification to redraw the axes, for the plot you're creating the zoom effect with a geometric transformation. That works until you try to redraw any of your plot (or, draw a new shape). When you do that, the modified scales and the transformed plot add together to double the zoom and pan effects, resulting in shapes that are out of position with the axes.
The solution is to simply do one or the other. Either connect your scales to the zoom behaviour, and then use those scales to redraw all your shapes after every zoom event; or use transformations -- but then your axes won't automatically change the tick spacing as you zoom in and out.

Categories