Building stacked charts with flexbox

When the idea of building graphs in HTML and CSS instead of D3 and SVG comes up, some people are horrified. These aren't precision technologies, they say. How can you give up the option of non-linear scales? Why throw away easy access to grid lines and tick marks?

These people aren't entirely wrong. HTML and CSS are relatively low-fidelity, compared to SVG. They're historically buggier. And it can be difficult to add decorative elements. If a graphic is well-suited to SVG, it obviously makes sense to use the most appropriate technology.

But HTML and CSS also have advantages over vector-based graphics:

Bar charts--and especially stacked bar charts--are a natural match for flexbox, a layout tool added to CSS now shipping in every modern browser. And building a chart is also a great way to learn to use it for regular layout tasks, where it excels (want to vertically center something, or lay out side-by-side rows of cards? Flexbox is your best option).

Assembling our chart

In the last chapter, we covered flexbox. Mostly what we care about are two abilities: using justify-content and align-items to set the "gravity" of the containers, and flex-basis to force the scale of each bar.

In this particular graphic, we're going to arrange our chart so that each bar forms a row, since this is often easier to read with many items on mobile: our labels can have plenty of room, and the vertical space extends as far as we can scroll.

First, the markup for a row: there's a label inside, as well as a container for our stacked bars, each of has its flex-basis set to its percentage width.

<div class="row">
  <div class="label">
    Category Label
  </div>
  <div class="bar-container">
    <div class="bar val-a" style="flex-basis: 20%">20%</div>
    <div class="bar val-b" style="flex-basis: 70%">70%</div>
    <div class="bar val-c" style="flex-basis: 10%">10%</div>
  </div>
</div>

Next, we'll turn on styles for the bar layout:

.row {
  display: flex;
  align-items: stretch;
}

.row .label {
  flex: 0 0 120px;
}

.row .bar-container {
  flex: 1;
  display: flex;
  align-items: stretch;
  justify-content: flex-start;
}

.row .bar {
  display: flex;
  justify-content: center;
  align-items: center;
}

.val-a { background: salmon }
.val-b { background: wheat }
.val-c { background: honeydew }

This layout actually uses nested flexboxes: one on the outside, with the labels set to a reasonable width for the text, and one inside that contains the stacked bars. Using align-items: stretch for both containers means that they'll automatically fill to the tallest item--if the text wraps to a new line, the bars will expand vertically to match. If you want them to stay uniform in height, you can set a manual height on .bar-container instead. The bars themselves are also flexed, in order to center the labels inside.

Here it is in action:

Category Label
20%
70%
10%

When building a chart like this, consider adding a readout of value details in a hidden list under the bar-container, give it a flex basis of 100%, and turn on flex-wrap: wrap for the row. A small JavaScript click listener can turn it off and on, and the flexbox will automatically put it under the label and bars. On mobile, you can even tweak the label flex-basis to be 100%, creating label/bars/value sub-rows on small screens.

Rotating to vertical

What if we wanted to create traditional column charts or histograms? Flex can make that possible as well, by switching the flex-direction for each bar to "column" and justifying content to "flex-end" (the bottom). We'll need to give the chart a manual height, since there's no content to create an implicit size, but that's easily doable using height units:

<style>
/* horizontal row of bars */
.columns {
  display: flex;
  height: 50vh;
  max-height: 400px;
  align-items: stretch;
  max-width: 300px;
  border: 1px solid #CCC;
}

.column-container {
  flex: 1;
  display: flex;
  flex-direction: column;
  align-items: stretch;
  justify-content: flex-end;
}

.column-bar {
  border-right: 1px solid white;
}

.series-a { background: salmon }
.series-b { background: wheat; }
</style>
<div class="columns">
  <div class="column-container">
    <div class="column-bar series-b" style="flex-basis: 25%"></div>
    <div class="column-bar series-a" style="flex-basis: 20%"></div>
  </div>
  <div class="column-container">
    <div class="column-bar series-b" style="flex-basis: 45%"></div>
    <div class="column-bar series-a" style="flex-basis: 40%"></div>
  </div>
  <div class="column-container">
    <div class="column-bar series-b" style="flex-basis: 15%"></div>
    <div class="column-bar series-a" style="flex-basis: 50%"></div>
  </div>
  <div class="column-container">
    <div class="column-bar series-b" style="flex-basis: 5%"></div>
    <div class="column-bar series-a" style="flex-basis: 60%"></div>
  </div>
  <div class="column-container">
    <div class="column-bar series-b" style="flex-basis: 5%"></div>
    <div class="column-bar series-a" style="flex-basis: 92%"></div>
  </div>
  <div class="column-container">
    <div class="column-bar series-b" style="flex-basis: 15%"></div>
    <div class="column-bar series-a" style="flex-basis: 30%"></div>
  </div>
  <div class="column-container">
    <div class="column-bar series-b" style="flex-basis: 45%"></div>
    <div class="column-bar series-a" style="flex-basis: 10%"></div>
  </div>
</div>

Nested flex layouts like this can be extremely powerful, not just for charts. If you're building an app-like set of panels, mixing columns and rows will fill space in a series of blocks--indeed, Firefox's XML-based UI toolkit has been built on a similar collection of "hbox" and "vbox" elements for years. If the layout becomes sufficiently complex, however, you may want to look at grid layout instead.

Decoration

The more elaborate our chart becomes, the more we should probably be thinking about SVG instead. But for the sake of argument, what if we wanted to add grid lines or labels to our graphic? There's a couple of ways we could make that happen. First, by using a repeating linear background to "draw" grid lines across the chart area:

.background-grid {
  height: 200px;
  margin: 20px auto;
  border: 1px solid #CCC;
  border-bottom: none;
  background-color: #E8E8E8;
  background-image: repeating-linear-gradient(to top,
    #CCC,
    #CCC 1px,
    white 1px,
    white 10%,
    #CCC 10%
  )
}

This is a neat hack, as far as these things go. But it's prone to odd rendering bugs, and it feels fragile. It probably makes more sense to position grid lines as HTML elements, using absolute positioning within the container.

.background-grid {
  position: relative;
}

.grid-line {
  position: absolute;
  width: 100%;
  border-bottom: 1px solid #CCC;
  text-align: right;
  /* set border-bottom to the percentage for the line */
}

Either way, I find that the best use of flexbox is for stacked charts where the message is the proportions of each bar segment, not the absolute length of the bar. They're great for budget graphics and poll results. They're also useful for the odd table layout, as long as you can keep accessibility in mind.