The 2013 Felton Bar Chart
Tags: d3js, visualization, typescript, svelte
Published: 2021-03-12 00:35:43

---
title: The 2013 Felton Bar Chart
published: true
date: 2021-02-18 23:51:42 UTC
tags: d3js,visualization,typescript,svelte
canonical_url: https://jpalmer.dev/2021/02/the-2013-felton-bar-chart/
---

This bar chart was one of the more straightforward visualizations to implement, and it was a good place for me to get my feet wet with Svelte and D3.

For reference, Nicholas Felton’s original bar chart looks like this:

![Bar chart detail from 2013 Annual Report](https://jpalmer.dev/2021/02/the-2013-felton-bar-chart/original-felton-bar-chart.png) <figcaption>Bar chart detail from 2013 Annual Report <a href="http://feltron.com/FAR13.html">(Nicholas Felton)</a>
</figcaption>

Simple and effective.

In order to recreate this, several things needed to be done:

1. Set up some sizing values to define the spacing of individual elements in each line.
2. Calculate the height of each line, which is then used to determine the height of the SVG.
3. Create a scale for the x-axis which will be used to draw both the solid and dashed lines.
4. Parameterize the highlight color so that the chart can be re-used with different colors.

I’ll cover how I did these things below.

### Caveats

First of all, I’m sort of assuming that you have some idea of how Svelte works. If you don’t and are curious, you can take 30 minutes to work through the [Svelte Tutorial](https://svelte.dev/tutorial). While there are a bunch of other great resources available, I’ve found that Svelte is simple enough that the tutorial is actually a really great starting point.

Also, I’m not going to cover the details of how Svelte and D3 interact. The key takeaway is that Svelte handles reactivity and DOM manipulation, and D3 is used for things like scales, etc. [Paul Butler’s YouTube series about combining D3 with Svelte](https://www.youtube.com/playlist?list=PLXiRqCwhpnehz5z0gpsmcb2l8vRBSi14M) is a great starting point, as are Matthias Stahl’s presentations on [Svelte and D3](https://youtu.be/bnd64ZrHC0U) and [How do you set up visualization with Svelte](https://youtu.be/xtTrcCp2aVU).

## SVG Sizing and Some CSS Gymnastics

In order to create an SVG that will maintain its aspect ratio when placed into containers of various sizes, you need to use a `viewBox` attribute to set the coordinate system of the SVG. This had to be one of the most confusing things I ran into when getting started. In fact, I’m still not sure that I have completely internalized the specifics of how SVG sizing/scaling works, so I’ll simply refer you to the resources I used to get started:

- [Understanding SVG Coordinate Systems and Transformations](https://www.sarasoueidan.com/blog/svg-coordinate-systems/) ( **amazing** )
- [How to Scale SVG](https://css-tricks.com/scale-svg/)
- [SVG: Font Size](http://xahlee.info/js/svg_font_size.html)

So, one of the first things I needed to do was calculate the height of the overall bar chart based on its contents. I ended up explicitly setting the font size in my TypeScript code and using that to perform calculations with the text height, like calculating offsets, line heights, etc.

```typescript
// Sizing in SVG user space
const width = 1200;
// Font size in user space
let textHeight = 30;
```

This needs to be made available to the CSS, but because Svelte doesn’t perform template substitution within the CSS `<style>` section, you need to [do a little hoop jumping](https://stackoverflow.com/questions/57174373/can-i-set-svelte-style-css-attribute-values-using-variables-passed-in-to-a-compo) to get things to work. You can add a `style` attribute on the SVG (or any container) that defines [CSS custom properties](https://developer.mozilla.org/en-US/docs/Web/CSS/Using_CSS_custom_properties), like so:

```html
<svg style="--label-size: {textHeight}px; --highlight-color: {highlightColor};">
  ...
</svg>
```
Once you’ve done this you can write CSS that uses those variables in your `<style>` section:

```css
<style>
svg {
  width: 100%;
  height: auto;
}
.label {
  font-size: var(--label-size);
}
.highlight {
  fill: var(--highlight-color);
  stroke: var(--highlight-color);
  font-weight: bold;
}
.measure {
  stroke-width: 5px;
}
.dash {
  stroke-dasharray: 4px 12px;
  stroke-width: 3px;
}
</style>
```

This makes it straightforward to change the font size if I’m using the chart in a different context. You can even make `textHeight` a [Svelte component property](https://svelte.dev/docs#Attributes_and_props) and pass it in as necessary. That said, ideally I’d be able to control all of this styling via CSS, so I’m still trying to figure out if you can do that. If you have suggestions on how to better manage consistent font sizes across both CSS and SVG text elements, please let me know.

### Calculating the SVG Height

Okay, so with the `textHeight` available, we can calculate the height of an individual line, and then the overall SVG height.

Each line in the visualization is comprised of two parts: the name and metric value on top, and the bar visualization of the metric value below. The spacing of a line is the height of the text line plus some padding below the text that contains the bar visualization. I used `linePadding` and `barOffset` to keep track of these spacing elements.

```typescript
// Margins within the SVG
let margins = {
  top: 50,
  bottom: 50,
  left: 50,
  right: 50,
};

// How many lines should be displayed?
const topN = 10;

// Space between the bottom of the text and the next line
const linePadding = 40;

// Space between the bottom of the text and the metric bar
// *must be less than linePadding*
const barOffset = 15;
const lineHeight = textHeight + linePadding;

// Compute the overall height
const height = topN * lineHeight + margins.top + margins.bottom;
```

Once we’ve calculated the height we can finally add the `viewBox` attribute to set the aspect ratio of the SVG.

```html
<svg viewBox="0 0 {width} {height}">
  ...
</svg>
```

💣 One thing to keep in mind with SVGs is that the x-axis runs from left to right, but the y-axis runs from top to bottom, opposite of the coordinate system that most of us were taught in geometry. That means that (0, 0) is in the upper-left part of the SVG, and (100, 100) will be in the lower right. While not really a problem for this simple chart, this definitely messed with my head on another chart that had multiple y-axes pointing in different directions.

## Drawing the Data

Now we can move on to drawing the elements of the bar chart. The structure of the data that I used in this example is as follows:

```typescript
const data = [
  {
    name: "Elias Vančo",
    total_actions: 260,
  },
  {
    name: "Sommer Wolfe",
    total_actions: 337,
  },
  ...
];
```

Many D3 functions take accessor functions for data arrays, so I like to declare helper functions to access individual data elements so that I can easily change them later, if necessary.<sup id="fnref:1"><a href="#fn:1" role="doc-noteref">1</a></sup>

```typescript
const metricAccessor = (d) => d.total_actions;
const titleAccessor = (d) => d.name;
```

### Creating the X-Axis Scale

D3 makes it easy to map values in your data _domain_ to the coordinate system of the SVG (the _range_) through the use of [scales](https://github.com/d3/d3-scale). There are a number of different scales from which to choose, and for this visual I used `scaleLinear` to perform a basic linear mapping from domain to range.

```typescript
import * as d3 from "d3";

// Grab the maximum value from the data for later use
const xMax = d3.max(data, metricAccessor);

// Create a D3 scale that maps domain values (from 0 to xMax)
// to SVG coordinates, respecting margins
const xScale = d3
  .scaleLinear()
  .domain([0, xMax])
  .range([margins.left, width - margins.right]);

// Calculate the index-based offset from the top of the SVG
// Note: Add textHeight to shift text below the starting point
// as text is placed according the the baseline
const yOffsetFn = (i) => lineHeight * i + margins.top + textHeight;
```

`xScale` is a function that calculates the absolute x-axis SVG positions for any data value in the domain. `yOffsetFn` serves the same purpose as `xScale` but calculates an index-based vertical offset for the y-axis to be used for line spacing.

### Creating the Bar Elements

Once I had a scale that mapped domain values to SVG coordinates, I could iterate through the data to draw the bar chart.

First, I sorted the data and pulled the `topN` values into a separate variable using [Ramda](https://ramdajs.com/).

```typescript
import * as R from "ramda";
// Sort and extract the first topN elements from the sorted collection
const diff = (a, b) => metricAccessor(b) - metricAccessor(a);
const topParticipants = R.take(topN, R.sort(diff, data));
```

I then used the Svelte [`#each`](https://svelte.dev/docs#each) construct to iterate through the `topParticipants` collection to generate SVG elements. In these elements I made use of the `xScale` and `yOffsetFn` functions to calculate absolute SVG positions.

```html
<svg ...>
{#each topParticipants as participant, i}
  <text
    class="label"
    text-anchor="start"
    x={xScale(0)}
    y={yOffsetFn(i)}
  >{titleAccessor(participant)}</text>
  <text
    class="label highlight"
    text-anchor="end"
    x={xScale(xMax)}
    y={yOffsetFn(i)}
  >{metricAccessor(participant)}</text>
  <line
    class="dash"
    x1={xScale(0)}
    x2={xScale(xMax)}
    y1={yOffsetFn(i) + barOffset}
    y2={yOffsetFn(i) + barOffset}
  />
  <line
    class="measure highlight"
    x1={xScale(0)}
    x2={xScale(metricAccessor(participant))}
    y1={yOffsetFn(i) + barOffset}
    y2={yOffsetFn(i) + barOffset}
  />
{/each}
</svg>
```

The first `text` element is the left-aligned participant name, using `xScale(0)` for the `x` position.

The second `text` element is the right-aligned count of total actions. To do this I needed to calculate the maximum `x` position via `xScale(xMax)`, and set the `text-anchor` attribute of the `text` element to `end`. That moved the element all the way to the right, but lined up the end of the text with the provided `x` coordinate.

The first `line` element is the dashed-line placeholder for the measure. The `y` position is offset by the additional `barOffset` to give the text above it a little breathing room. This line is drawn across the entire x-axis to produce visually consistent dashed lines across the entire chart. Since SVG elements are drawn in the order they are defined, with later elements overlapping earlier ones, this placeholder is hidden by the next element and there are no fancy line start/end calculations required.

The second `line` element is the bar representing the actual measure. It is drawn on the same `y` position as the previous line, but is slightly thicker to completely obscure the dashed line.

Once all of these elements were in place the entire visual was basically done. I did notice one detail that bothered me after a quick visual inspection:

![Right side dash mis-alignment](https://jpalmer.dev/2021/02/the-2013-felton-bar-chart/left-to-right-bar-chart-detail.png) <figcaption>Right side dash mis-alignment</figcaption>

The final dot at the end of the dashed placeholder lines didn’t match up to the right end of the scale. This was easily remedied by drawing the dashed line from right to left instead of left to right (by swapping the `x1` and `x2` values), which resulted in any left side alignment issues being hidden under the metric bars.

Here’s the final result:

![Final bar chart version](https://jpalmer.dev/2021/02/the-2013-felton-bar-chart/bar-chart-final.png) <figcaption>Final bar chart version</figcaption>

The code I used to write this post is [available on GitHub](https://github.com/JeffreyPalmer/svelte-d3-examples).

Please feel free to contact me if you have any questions or comments.

<section role="doc-endnotes">
<hr>
<ol>
<li id="fn:1" role="doc-endnote">
<p>If this were really a general purpose chart library, you’d need to pass these functions in to this component, in the same way that generic sorting algorithms take comparison function parameters. <a href="#fnref:1" role="doc-backlink">↩︎</a></p>
</li>
</ol>
</section>