Making Data Viz Without SVG Using D3 & Flexbox
Bye bye tricky alignments.
July 2018
A few months ago, my co-worker Matt and I were collaborating on a project. I had been building a stacked bar chart in D3.js, and was seriously struggling to get the bars to stack on top of each other nicely and to animate from the bottom of the graphic instead of the top. For some reason, the fact that an SVG space places its (0,0) coordinates at the top left corner instead of the bottom left corner will always cause some weird mental disconnect in my brain.
Amid all of my frustration, Matt suggested ditching the SVG altogether, making the bars out of div
elements with a background-color
and aligning them using Flexbox. To be totally honest, I was super confused by the suggestion because it involved a few things that I honestly didn't know were possible. I should have asked: You can make D3 graphics without SVG? Divs can be used to make bars? I can use flexbox to do all the alignment math for me?
It turns out the answer to all of those questions is yes. In that moment, I was being stubborn and eventually got the SVG to work the way I wanted, but Matt’s suggestion stuck with me. And when I actually saw it in action in one of his more recent projects, I was mind-blown. I ended up loving it so much that one of the most recent data-viz stories that I programmed contains no SVG. I’m so in love with this method (for very specific circumstances) that I wanted to shout it from the rooftops...or at least from this post.
In this post, I’ll give some background information that I wish I knew, and an example of how to use this method to make a waffle chart, a modified histogram, and a stacked bar chart.
Some Important Context
If you’re already super-familiar with the inner-workings of D3.js, DOM elements, and Flexbox, you can skip this section.
Although I’d been using D3 to make graphics for over a year, I had some major misconceptions about what it can do that stopped me from fully grasping this concept early on. So here’s some things I wish I knew:
- D3 manipulates DOM elements, not just SVG: In hindsight, I feel like this one should have been obvious, but I clearly missed it. It even says it right on the D3 homepage: “D3 allows you to bind arbitrary data to a Document Object Model (DOM), and then apply data-driven transformations to the document.” meaning that while D3 can create and manipulate DOM elements that are on an SVG plane (think
path
,rect
,circle
etc. elements), it can also manipulate non-SVG elements (likep
,div
, andspan
elements). - Flexbox arranges DOM elements in rows or columns: Unlike CSS grid, Flexbox arranges items in a one-dimensional manner in either a row or column format. While you can use CSS-grid for the methods described in this post, I’m going to primarily focus on using Flexbox. More info on the differences can be found here.
- Flexbox can organize non-SVG DOM elements: You can’t use Flexbox to arrange elements within SVG. That is, if you have a bunch of
rect
elements within your SVG, Flexbox can’t move them. But, if you have several, separate SVG elements (think small multiples), Flexbox can arrange them for you.
Making a Waffle Chart
Let’s start with a waffle chart. In this kind of graphic, we’ll use small blocks to represent a percentage of something (much like a square pie chart). For this example, we’ll say that 5% of our readers resize their browser.
To make this kind of graphic, we’ll need a single figure
element with 100 smaller div
elements inside of it. Like this:
HTML
In your HTML file, you’ll only need a single, empty figure
element. Remember, no need to add an SVG element.
JavaScript
Since we’re going to need 100 blocks (in our case, div
elements) inside of our figure
element, we can add them with JavaScript. In this example, we’re creating an array of 100 numbers and creating a div for each one. To make our graphic represent 5% of something, we need 5 of our 100 blocks to be stylistically different from the rest. Here we set the style background-color
to be red if the number is less than 5 and gray if it’s greater than 5.
CSS
The magic of this method shows up here, in the CSS. At this point, we have an outer-most figure
element called waffle
and 100 divs inside that are each called block
. Using CSS, we can define the size of our block
and waffle
elements, and use Flexbox to arrange them. Be aware that in this example, we aren’t including any vendor prefixes. You can automatically add them by running this CSS code through an autoprefixer.
Here, the designation display: flex
indicates that the waffle
figure
element should be considered a flex container. The line flex-wrap: wrap
indicates that the items inside of waffle
should line themselves up in a row (starting in the upper left-hand corner) until they reach the end of the container (in this case, width: 140px
), and then form a new row. If you want this process to start in the lower right-hand corner instead, use flex-wrap: wrap-reverse
. Last, give your block
elements a width
and height
to make them visible. That’s it!
The Results
Here’s what our final graphic looks like. Feel free to inspect it to double-check that it’s only made up of div
elements.
Making a Modified Histogram
Histograms are generally used to show a distribution in the data. While a typical histogram uses a single bar of varying height to signify the count of items in a bin, at The Pudding, we often use a type of modified histogram, where small shapes of identical size are stacked on top of one another. So, instead of a 100px tall rectangle representing a count of 10, you could have 10, 10px tall rectangles (spaced 1px apart) representing a count of 10.
To make a modified histogram using this method, you need to plan for the overall structure of your graphic. You’ll need a figure
element to contain the entire graphic, a div
for each stack of blocks, and a div
for each block. Like this:
HTML
Again, we’ll start with a single figure
element to contain everything.
JavaScript
For this example, we’ll make a histogram of the state where each of The Pudding team members lives. We’ll manually add the state data for each of our 6 members, but you could have imported the data using D3.csv or D3.json. Next, we need to nest the data by state, add a div
to contain all the blocks we’ll add for each state. Then, to each group div
we need to add the appropriate number of block div
elements. Last, we’ll add a state label to each group.
CSS
Here we have two elements that need to act as flexbox containers. To align the block div
elements and the label in a column, we assign the group div
display: flex
and flex-direction: column
. Then, we want all 3 groups to sit next to one another in a row, so we assign the hist figure
element display: flex
and flex-direction: row
. This automatically aligns the groups in a row, but they are also aligned at the tops of their groups (so the stacks look like they are hanging from the top of the graphic instead of growing from the bottom). To fix this, we assign align-items: flex-end
.
The Results
Here’s what our final graphic looks like. Once again, feel free to inspect it to double-check that it’s only made up of div
elements.
Making a Stacked Bar Chart
Stacked bar charts are only slightly more complex than waffle charts or histograms because they require 2 additional elements: different sized block div
elements and a way to quantify the overall height of the bars. Structurally, it needs to be set up like this:
In this example, we’ll look at a small part of our site’s traffic. Specifically, the traffic that has come from Facebook, Twitter, YouTube, and Instagram over the past few months.
HTML
Just like our first two examples, you start with a single figure
element.
JavaScript
Our process of preparing our elements in JavaScript is similar to how you would write the JavaScript code for an SVG-rendered stacked bar chart, with one-notable example. You don’t need d3.stack()
. That specific function calculates the bottom and top positions of each rectangle in your stack, and since we’ll use Flexbox to arrange our shapes, that becomes unnecessary. Instead, we need to append a div
element for each month, and then append additional div
elements for each social media type. Last, we have to set a scale (here we use d3’s built in d3.scaleLinear()
) and use the scale to define the height of our div
elements (.style(‘height’, function(d){return `${scaleY(d.value)}px`)
). Instead of a full y-axis, we’re going to just add a value label to sit on the top of our bars.
CSS
The styling for this stacked bar chart is very similar to our modified histogram in that we want our block
elements to be aligned in a column and our group
elements to be aligned in a row. I’ve added a few other pieces of styling to arrange the items in the way that I want. Originally, the bars for Facebook were at the top of the stack because they are the first item in my array for each month. Since I want to draw my reader’s attention to these bars, I want them to have the same baseline, and so I want them to be on the bottom. Using Flexbox, I can reverse the order of my bars using flex-direction: column-reverse
.
Then, I want the month label
to be at the bottom of my bars, so I can assign it an order: 0
(if our bars were still organized using flex-direction: column
in the default order, order: 0
would put this label at the top of the stack). And I want the number count
label to be at the top of the bars, so I assign it an order: 2
. My block elements are assigned an order: 1
to keep them between both labels.
The Results
Here’s our final image (with a little bit of extra styling).
That's it!
Basically, this method takes away the need to calculate the exact positioning of all of our blocks and gives that responsibility to Flexbox to handle automatically. It works especially well when all of your elements can line up in a grid of rows and columns. If you’re interested in other ways to use D3 without SVG, check out this oldie but a goodie post from Jim Vallandingham about how to make an HTML bubble chart or this simple example from the D3 documentation that I totally missed 🤦🏻♀️.
This method doesn’t work as well if you have elements that would need to reach across your rows or columns. For instance, you can add axes (check out some examples of that here) but if you want gridlines to extend from your axes all the way behind your graphic...this method won’t work for you. Also, if you want annotations that aren’t going to line up in a grid...stick with SVG. Boxes arranged in this way also don’t smoothly animate from one position to another. So, you’ll lose object constancy if your graphic includes any animation.
But otherwise, give it a try! And hit us up with any questions at amber@pudding.cool.