Bill White's Blog

HTML5 / Javascript / d3 / Data Visualizations / Flex

d3 Minimap v4 Update

by Bill on September 12, 2017, no comments

Understanding d3 v4 ZoomBehavior in the Pan and Zoom Minimap

See the Pen d3 Minimap Pan and Zoom Demo by Bill White (@billdwhite) on CodePen.

It has been a while since I posted any new articles.  I’ve been working  non-stop with Angular 4 (formerly Angular 2), Typescript, RxJS and d3 v4.   I recently needed to add a minimap for a visualization and I decided to update my old minimap demo to make us of d3 version 4.  While researching the updates to the zooming behavior, I came across this post on GitHub where another developer user was trying to do the same thing (funny enough, also using my previous example).  After reading Mike’s response about cross-linking the zoom listeners, I decided to give it a try.   While implementing his suggestion, I learned quite a bit about how the new and improved zoom functionality works and I wanted to share that in this post.

My previous example did not make good use of the capabilities of d3’s ZoomBehavior.  I was manually updating the transforms for the elements based on zoom events as well as directly inspecting the attributes of related elements.  With the latest updates to d3 v4, creating this type of minimap functionality ended being really simple and I was able to remove a lot of code from the old demo.  I found that, while the release notes and the docs for the zoom feature are helpful, actually reading through the source is also enlightening.  I was eventually able to boil it down to some basic bullet points.

 

Design

  1. The is a canvas component (the visualization) and a minimap component.  I will refer to these as counterparts, with each making updates to the other.  The canvas has a viewport (the innerwrapper element in my example) and a visualization (the pancanvas in my example).  The minimap also has a viewport (the frame) and a miniature version of the visualization (the container).  As the visualization is scaled and translated, the relative size and scale is portrayed in the minimap.
  2. The d3 v4 updated demo has 2 zoomHandler methods that each react separately to ‘local’ zoom events.  One makes changes to the main visualization’s transform and scale.  The other does the same to the minimap’s frame.
  3. There are also 2 ‘update’ methods.  These are called by the zoomHandler in the counterpart component.  Each update method will then call the local zoomHandler on behalf of the counterpart.  This effectively convey’s the ZoomBehavior changes made in one counterpart over to the other component.
  4. There will continue to be separate logic for updating the miniature version of the base visualization over to the minimap. (found in the minimap’s render() method)
  5. There will continue to be separate logic to sync the size of the visualization with the size of the frame. (also found in the minimap’s render() method)

 

Zoom Behavior notes

  1. The ZoomBehavior is applied to an element, but that does not mean it will zoom THAT element.  It simply hooks up listeners to that element upon which it is called/applied and gives you a place to react to those events (the zoomHandler that is listening for the “zoom” event).
  2. The actual manipulation of the elements on the screen will take place in the zoomHandler which receives a d3.event.transform value (which is of type ZoomEvent for those of you using Typescript).  That event provides you with the details about what just happened (i.e. the transformation and scale that the user just performed on the ZoomBehavior’s target).  At this point, you have to decide what to do with that information, such as applying that transformation/scaling to an element.  Again, that element does not have to be the same element that the ZoomBehavior was originally applied to (as is the case here).
  3. We have to add a filtering if() check within each zoom handler to avoid creating an infinite loop.  More on that in a in the next section…..

 

Logic Flow

  1. We apply the ZoomBehavior on two elements (using <element>.call(zoom)).  The InnerWrapper of the visualization’s canvas and the Container of the minimap.  They will listen for user actions and report back using the zoomHandler.
  2. Once the zoomHandler is called, we will take the d3.event.transform information it received and update some other element.  In this demo, the InnerWrapper’s zoom events are applied to the PanCanvas, while the minimap’s Container events are used to transform the minimap’s Frame element.
  3. Once each zoom handler has transformed it’s own local target, it then examines the event to see if it originated from its own local ZoomBehavior.  If it did, then the logic executes an ‘update()’ call over on its counterpart so that it can also be modified.  So we get a sequence like this:  “InnerWrapper(ZoomBehavior) –> zoomHandler(in Canvas component) –> updates PanCanvas element –> did zoom event occur locally? –> if so, update minimap”.  And from the other side we have this: “minimap Container(ZoomBehavior) -> zoomHandler(in minimap) -> updates minimap Frame element -> did zoom event occur locally? -> if so, update visualization”.  You can see how this could lead to an infinite loop so the check to see if the event originated locally is vital.

This diagram shows the general structure I’m describing:

So the end result is a “push me/pull you” type of action.  Each side makes its own updates and, if necessary, tells the counterpart about those updates.  A few other things to point out:

  1. Because the minimap’s frame (representing the visualization’s viewport) needs to move and resize inverse to the viewport, I modify the transform event within the minimap when they are received and before they are sent out.  The encapsulates the inversion logic in one place and puts that burden solely on the minimap component.
  2. When modifying a transform, ordering matters.  Mike Bostock mentions this in the docs, but I still got tripped up by this when my minimap was not quite in sync with the visualization.  I had to scale first, then apply the transform.
  3. Rather than using the old getXYFromTranslate() method that parses the .attr(“transform”) string property off an element, it is much easier to use the method d3.zoomTransform(elementNode) to get this information. (remember, that method works with nodes, not selections)

At this point, the design works.  However, there’s another problem waiting for us:

When the user moves an element on one side, the counterpart on the other gets updated.  However, when the user goes to move the counterpart element, the element will “jump back” to the last position that it’s own ZoomBehavior triggered.  This is because, when the ZoomBehavior on an element contacts its zoomHandler, it stashes the transform data for future reference so it can pick up where it left off on future zoom actions.  This ‘stashing’ only happens when the ZoomBehavior is triggered from UI events (user zooming/dragging etc).  So when we manually update the PanCanvas in response to something other than the ZoomBehavior’s ‘zoom’ event, the stashing does not occur and the state is lost.  To fix this, we must manually stash the latest transform information ourselves when updating the element outside of the ZoomBehavior’s knowledge.  There’s another subtle point here that briefly tripped me up:  the ZoomBehavior stashes the zoom transform on the element to which it was applied, NOT the element upon which we are acting.  So when the ZoomBehavior hears zoom events on the InnerWrapper, it updates the __zoom property on the InnerWrapper.  Later on, when the minimap makes an update call back to the visualization, we have to manually update that property on the InnerWrapper, even though we are using that data to transform the PanCanvas in the zoomHandler.

 

So here is the final interaction:

  1. User moves the mouse over the PanCanvas
  2. The ZoomBehavior on the InnerWrapper hears those events and saves that transform data in the __zoom property on the InnerWrapper.
  3. The ZoomBehavior then emits the ‘zoom’ event which is handled by the local zoomHandler in the visualization (canvas component in the demo)
  4. The zoomHandler will apply the transform to the PanCanvas to visibly update its appearance in response to the mouse actions from step #1
  5. The zoomHandler looks at the event and if it determines that it originated locally, it makes an update call over to the minimap so it can be updated
  6. The minimap’s update handler inverts the transform event data and applies it to the Frame element
  7. Because the minimap’s updates to the Frame element occurred outside of the minimap’s own ZoomBehavior,  we stash the latest transform data for future state reference. Note: we stash that state, not on the Frame element, but on the minimap’s Container element because that is the element to which the minimap’s ZoomBehavior was applied and that is where it will look for previous state when subsequent ZoomBehavior events are fired when the user mouses over the minimap.
  8. The minimap’s zoomHandler is called by the update method which applies the matching minimap appearance update to the Frame element.
  9. The minimap’s zoomHandler determines the update event did not come from the local ZoomBehavior and therefore it does not call the update method on the visualization, thus preventing an infinite loop.

Hopefully this will save you some time and help you understand how the d3 v4 ZoomBehavior can be used for functionality such as this demo.  🙂

Book Review: Learning D3.js Mapping

by Bill on February 24, 2015, one comment

I was recently asked to review a copy of PacktPub’s “Learning D3.js Mapping” by Thomas Newton and Oscar Villarreal. I’ve done a good deal of d3 development, but the mapping portion of the library has not received much of my attention until now. A fellow d3 developer and leader of our local d3 Meetup group, Andrew Thornton, has done considerable work with d3-driven mapping applications and it is something I’ve always wanted to delve into but never really had the chance. This book served as that motivation and while it is relatively short, it provides as a great introduction to generating maps in d3 and demonstrates just how powerful the library truly is for creating geographic visualizations.

The book starts by laying out the basic setup for running the provided code samples. They suggest running with the code using a local nodejs http-server, but you can just as easily run the examples directly from the downloaded code samples folder, or using an online IDE such as CodePen or JSBin, although you will need to have access to the hosted sample JSON data files used in the examples. A basic foundation of SVG fundamentals is also provided, which can be useful for those just getting started with SVG-based data visualizations. While the more seasoned d3 developers will be inclined to skip this section, the authors focus a good deal on SVG elements such as paths, which foreshadows their importance in creating maps later in the book. The third chapter is a d3 primer, providing some history behind the library itself. The enter/update/exit lifecycle is described although novice readers will want to read further about this before attempting the more advanced d3 code samples.

The meat of the book is found starting with chapter 4 where you create your first map. The reader is introduced to projections which handle the conversion of coordinates from one space to another. The authors do a good job of covering supporting topics such as scaling and translations which may be new to some readers. Later on in the chapter, an approach to creating choropleths (a fancy way of saying “color-filled maps”) is discussed which introduces the beginners to the use of d3 scales for mapping colors to values, handling click events and adding transitions to spruce up the user interaction. Again, nothing new for most d3 experts, but the authors are clearly trying not to leave anyone out with respect to programming backgrounds. Below is an example of one of the code samples discussed in Chapter 4:

JS Bin

Chapter 5 is where the book really hits its stride. Starting with the addition of tooltips and then diving into the use of pan and zoom (one my personal favorites), this chapter takes a basic d3-generated map and turns it into a functional data visualization. The reader learns about orthographic projections (representing three-dimensions in a two-dimensional format) such as this:

JS Bin

Of course, maps are cool to display using d3, but a big piece of the puzzle is obtaining the mapping data to drive the visualization. Chapter 6 discusses the difference between shapefiles, GeoJSON and TopoJSON file types and even shows you where to get free shapefiles, albeit they are the largest and most inefficient of the three types. I found this chapter to be very useful since understanding the mapping source data is key to creating solid mapping visualizations. Up to this point, I had never really understood the difference between these types, nor was I aware that Mike Bostock, the creator of d3, was also behind the TopoJSON project. (It is amazing how much code developers rely upon every day without knowing the names of the key contributors or understanding the motivation for it’s initial creation. This is yet further proof that open source software makes the world go ’round).

The final chapter review unit testing techniques, which seems a bit out of place, but is helpful and informative nonetheless. Unit testing is clearly an important aspect of writing good code. However I cannot help but think that, if what we want is more scalable, maintainable and testable code, then getting away from a weakly-typed, inconsistently-implemented, functional language such as Javascript would be a good first step. But since Javascript is really the only viable option we have these days, we have to make the best of it and the techniques the authors discuss here are certainly worth considering.

Overall, I enjoyed the book and would certainly recommend it to anyone looking to create maps using d3. The d3 library itself is quite challenging in its own right, but this book gives you enough background that you can get some geographic visualizations up and running in very little time.

D3 in 3D: Combining d3.js and three.js

by Bill on January 12, 2015, 6 comments

Along with d3, the three.js library is one of the most powerful javascript libraries available for creating slick visualizations. I wondered if it would be possible to create data visualizations in threejs as easily as I had done using d3. One thing that d3 does very well is to take your data and apply a layout algorithm to it for use in drawing visualizations such as treemaps and piecharts. While I’m sure there are other ways to generate this layout data, the d3 approach is the one that I’m most comfortable with and I wondered if it would be possible to use this layout data to render 3D versions of those same visualizations.

This first demo shows a WebGL (or Canvas if you’ve got a crappy browser like IE) rendered treemap that was created using the d3 treemap layout: (I had to use JSBin instead of CodePen for the demos because the embedded CodePen snippets kept locking a few seconds after loading; no idea why)

JS Bin

The results work pretty well. The Tweening is handled using TweenJS which is what the threejs folks use. I wanted to do the updates using d3 transitions but using the Tween code from the examples I had seen worked straight away so maybe I can swithc over to d3 transitions in the future. Also, when you set the zscale to 0, you see a bit of color bleeding and I have no idea why. (Let me know if you have a solution.)

First, I must give credit to Steve Hall who has also investigated this concept. Last year I explored ways to create 3D visualizations with threejs using layout data from d3, but I hit a wall when I was unable to get the coordinates to translate between the two worlds correctly. Steve also worked on this idea and wrote some fantastic blog entries about it. His work focused more on created mini-visualizations, attaching them to DOM nodes and then having threejs render those nodes in 3D. After reading his work, I contacted him to ask if he knew how to correctly translate the coordinates and to my relief, he gladly provided the solution. So in the interests of full disclosure, my work here was greatly facilitated by his assistance. Thanks Steve!

The solution I landed on is relatively straightforward: Take a dataset, run it through d3 to apply layout positioning information to each data item and then render those items in threejs rather than the normal DOM-oriented CSS styling that we use with d3. In a typical d3 visualization, the data is bound to the DOM nodes and you use the enter/update/exit lifecycle to add, modify and remove those nodes. With threejs, we are no longer adding stuff to the DOM tree (unless we use the CSS3DRenderer, but more on that in a second) because the 3D world is rendered using WebGL (or Canvas when WebGL is unsupported).

So I am taking the dataset that has been processed by d3 and then adding each item to the threejs scene. I add, update and remove items from the scene in reaction to the add/update/remove calls from the d3 general update lifecycle. Interestingly, I’m actually adding DOM nodes to the tree using d3 but they are empty and unstyled so they end up only serving as placeholders for the current set of rendered data in the threejs scene. If the data changes, the d3-generated empty nodes are added/updated/removed and then, for each change, a corresponding change is made to the objects in the threejs scene.

Steve’s demos are implemented using the CSS3DRenderer which actually displays the 3D objects using only CSS3 transforms. Because of this, you are actually still in the d3 data-bound-to-the-DOM-node situation. It is really the best of both d3 and threejs because you can take full advantage of d3 selections and still have threejs display the 3D results using those same nodes. However, if you want your 3D visualization to look, well, three-dimensional, you have to bridge the gap between the d3 generated DOM nodes and the corresponding objects in the threejs scene yourself.

This second demo uses the CSS3DRenderer and uses d3 to create the DOM elements which are then manipulated by threejs. You can see that while it is nice, it is not quite as fancy as the first demo:

JS Bin

Responsive d3 Charts in Web Applications

by Bill on November 11, 2014, 3 comments

Question: Is it possible to make a single page web application that uses CSS to layout the page and still have your d3 charts know when to update their size/appearance?

One thing that always catches my attention with d3 examples is the lack of code that makes the chart fill the viewport when it is resized. Granted, the focus of these examples is usually on the design of the visualization itself, but in the real world these graphics will usually run inside a larger application and it should fill the space available. And by resizing, I do not mean scaling the chart to fit. To me, making the content bigger or smaller based on the viewport size is no more useful than if your word processor made the font bigger when you viewed it in full-screen mode. When the visualization resizes, it should simply make better use of the available space. The trick is knowing when to resize the chart in response to the layout change. So how do you know when to resize the d3 chart? Well, that depends on what is controlling the layout of the DOM elements. Is it a JavaScript widget library? Maybe an event from a container created by jQuery? Or are you using CSS3 to layout the page?

Approach 1: JavaScript widget library

If the visualization is running inside a web application created with something like Sencha ExtJS or Dojo, you can utilize the event mechanisms that these libraries provide in their widget infrastructures. For example, in Dojo you can layout the interface using a BorderContainer which holds multiple ContentPanes. You can then listen for the ContentPane’s resize event to know when to update the d3 chart it contains. In the example below, you see two resizable pie charts in a Dojo split container. Then you move the splitter, each pie chart hears the resize event fired by its parent ContentPane and it updates its own size accordingly:

See the Pen d3 CSS Layout – Dojo PieChart by Bill White (@billdwhite) on CodePen.

The downside is that widget libraries create a fairly heavyweight DOM structure in order to provide all the fancy features and handle backward compatibility with older browsers. There is also a good amount of JavaScript execution happening here, particularly when you start nesting BorderContainers within BorderContainers within BorderContainers, etc. Each widget executes a series of lifecycle methods that compute measurements, calculate space requirements and then dispatch events to notify the other containers that they might need to resize in response. It would be great if we could get these powerful layouts without the extra overhead and then have the d3 charts know when the layout has made size changes.

Approach 2: Listen to window.resize or use jQuery to dispatch events

If you have CSS handling the layout of your DOM elements, you could update your d3 charts by listening for resize events from the browser window. You can find this approach suggested in a few posts as a standard way to make the chart “responsive”. This is not a bad solution if it suits your need, but I personally find it has some undesirable side effects. Ideally, your d3 chart would only resize when the parent DOM element does. If you are using dynamic components (i.e. accordion containers, etc) that resize without changing the size of the browser itself, the window’s resize event may not fire unless someone has gone through the trouble of triggering that event manually. Another problem with only listening for the browser resize event is that if you have several visualizations in your application, they could all be resizing in response to the window event whether it is truly necessary or not.

Take the following example. Using jQuery and a reusable d3 pie chart, we have a visualization set inside a resizable container. The pie chart listens to the window.resize event to know when it should update itself. Try resizing each container and you will notice that neither pie chart adjusts its size. Only when the window is resized do the charts perform an update: (click on the “Edit on CodePen” link in the right corner of the demo to launch it separately to see the pie charts resize when the browser window is adjusted. The embedded version in this post cannot see these events)

See the Pen xhEKk by Bill White (@billdwhite) on CodePen.

My point is that your d3 chart will not resize if you only listen to window events and the DOM resizes for some other reason. In this second example, I instead listen for the resize event on the container created by jQuery.

See the Pen xhEKk by Bill White (@billdwhite) on CodePen.


This is better but we still are not using a CSS-only approach to handle the layout of the DOM. The goal here is to use CSS for the layout and still have the d3 chart know when a resize is required.

Approach 3: CSS Layouts

Rather than have JavaScript executing in response to an event provided by a widget library (Dojo/Sencha, etc), why not use CSS to handle the layouts? In particular, we can use the flexbox layout to position everything for us. The catch is that you still have know that a resize action has occurred. Again, you could resort to listening to the window’s resize events, but we’ve already mentioned the problems with that approach. Is there a way to know when CSS has made changes to the size of a container? Maybe so…..

I came across a project by Marc Schmidt that might offer a solution. Using the ResizeSensor I was able to setup a relatively lightweight listener that fires when the containing DIV is resized. This in turn notifies the d3 chart that it needs to update. The following demo shows 2 reusable treemaps displayed within a CSS flowbox container. For the sake of the demo I added a jQuery resizable container to allow you to see what happens when the flexbox layout adjusts the size of the containers holding the treemaps: (doesn’t work cleanly in IE yet)

See the Pen vLgpf by Bill White (@billdwhite) on CodePen.

The ResizeSensor does add a DOM element to the container along with a bit of JavaScript to track size changes. But this overhead is small compared with a traditional JavaScript widget library and the script watches for any size changes to that particular element rather than the entire browser window. Additionally, this approach is flexible enough that it works with responsive layout managers such as Twitter Bootstrap. The following example shows a treemap that is contained inside a Bootstrap 12 column layout:

See the Pen vLgpf by Bill White (@billdwhite) on CodePen.

Using a ResizeSensor, we respond to the parent DOM element being changed by the Bootstrap layout. jQuery’s resizable container is only used to let you trigger that Bootstrap layout update for the sake of the demo.

So what are the flaws with this approach? The primary concern is that if you rely on CSS to layout your application, you may run into issues if you have to support older browsers. The ResizeSensor seems to work great in many older browsers, but the immensely useful flexbox layout is not supported by older versions of IE so you have to work around that. Still, if you can make CSS handle your application’s layout, you can get your d3 charts to be responsive without the overhead of a JavaScript widget library or a bunch of listeners tied to the browser window’s resize event.

If anyone can add to this discussion or find oversights in my approaches, I would love to get your feedback. 🙂

d3 Scalability – Virtual Scrolling for Large Visualizations

by Bill on May 17, 2014, 5 comments

The visualizations that I work on have large datasets that need to be rendered quickly with as little DOM overhead as possible. One of the cool things about d3 is that is renders the data present at any given moment along with changes to that data that are coming or going. However, if you have a large dataset, d3 can kill the browser under the weight of all the DOM nodes that it is attempting to create. Here is a demo of what I came up with to solve this problem:

Virtual Scrolling Demo

You can explore the GIST code here.

Virtual rendering has been around for a long long time and is used to varying degrees on almost every known development platform to provide access to large datasets without slowing down the UI. For web developers, showing millions of rows of data is easily achieved by using a Javascript data grid that can pull in those rows as the user scrolls through the grid.

I’m definitely not the first one to attempt the implementation of virtual rendering in d3. I came across a d3 Google Group post where using d3 for virtually rendered grids was being discussed and found that Jason Davies had toyed with this a couple of years back. There were many other experiments as well, but I needed one that could render more than just a grid or simple chart.

My solution began with Jason’s longscroll.js class, which I heavily modified to allow the user to specify enter/update/exit methods that the plugin uses for rendering rows. The general idea is that you determine how much data can be shown on the screen at any given time and then slice out that amount of data from the master dataset and render it. As the user drags the scrollbar along, you continue to slice out corresponding ranges of data from the dataset and render them on screen. Ideally, you re-use the components already created on the screen and simply change out the data that backs each node with a new node from the sliced range to avoid the expense of adding and removing DOM elements.

For this plugin, here is what you provide:

  1. a viewport reference – this is the DOM element that will contain the SVG. I write everything with variable resizing in mind so this viewport’s dimensions can change and the plugin will adapt if the resize boolean is set to true when the virtual scrolling action is requested
  2. an svg reference – this is sized based on the rowheight * the total data size, thus creating an accurately sized scroll bar
  3. an svg child group reference – this will be used to render the content and will be transformed within the svg to keep the contents visible.
  4. a total data size – you may wonder why the plugin cannot just look a the size of the data being provided, but if you plan to only page in a subset of the total amount of data, this would not size the scroll bar correctly; therefore the code sets this value independently from the size of the data the plugin knows about
  5. an enter/update/exit method – this will be used to render the rows on the screen

There are several aspects to this solution that should be pointed out:

First, your visualization needs consistently sized rows so you can divide the visible height of the viewport by that size and calculate the range of visible rows. You also use the row height multiplied by the length of the entire dataset to size the scrolling DOM node container so that the scrollbar is sized correctly. I imagine you can implement a version with nodes of different heights, but you will need to know the height of the un-rendered rows above and below the visible range if you want to size the scroll bar correctly.

Second, there is no need to load the entire dataset into memory if you have the ability to get the total size of the dataset from the server and then page in subsets of data as they are needed.

Third, you can also use this solution to render tree structures but things get more complicated if you plan to page in that data on an as-needed basis. More on that in a future post once I get it all figured out. 🙂

d3 Multiple Flow Container Demo

by Bill on March 31, 2014, no comments

I received a comment on the Flow Tree Layout post asking if it was possible to use that same layout multiple times within a single SVG. The code in that example created an SVG inside of a dom node passed in by the caller, but it is possible to modify that sample to accept any valid SVG container for executing the layout. I created a quick and dirty sample GIST showing how it might be done.

I also put together another d3 demo showing how the flow layout could be used within multiple containers within the same SVG. This is a much more component-ized implementation which lets you create any number of containers, each with its own layout (all are flow layout in this case). You can resize the containers below to see their individual flow layouts operate. This demo also includes z-ordering and selection logic (drag on the background to do a lasso selection of multiple containers) all implemented using d3. Gist is here and bl.ocks version is here. Enjoy 🙂

Multiple Flow Containers