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. 🙂