DEV Community

Jeff Palmer
Jeff Palmer

Posted on • Originally published at jpalmer.dev on

The 2013 Felton Histogram - Variant 2

Nicholas Felton’s 2013 Annual Report contained two distinct histogram variants. This post covers how I created the second variant, which looks like this:

Histogram (Variant 2) detail from 2013 Annual Report
Histogram (Variant 2) detail from 2013 Annual Report(Nicholas Felton)

This is similar to the first variant that I covered in my last post, with the following changes:

  • Two measures are plotted on the same x-axis, with their y-axes facing in opposite directions
  • Irregular stippling is used to differentiate the lower measure
  • Measure series labels are added, while value labels are removed

Plotting with Multiple Axes

This histogram variant adds a second measure that has its y-axis pointing downward, in opposition to the y-axis of the top measure. Since the y-axis in SVG increases from top to bottom, the range of the top scale will be from the y midpoint to the top margin (from x-axis upwards), and the range of the bottom scale will be from the y midpoint to the height minus the bottom margin (from x-axis downwards).

$: xScale = d3
  .scaleTime()
  .domain([dateParser("2020-01-01"), dateParser("2021-01-01")])
  .range([margins.left, width - margins.right]);

// The top scale goes from midpoint to top margin
// Also add 10% of the max top value to the end of the domain
// to ensure there's headroom on the chart
$: topScale = d3
  .scaleLinear()
  .domain([0, d3.max(data, topAccessor) * 1.1])
  .range([
    (height - margins.bottom - margins.top) / 2 + margins.top,
    margins.top,
  ])
  .nice();

// The bottom scale goes from midpoint to bottom margin
// Also add 10% of the max bottom value to the end of the domain
// to ensure there's footroom (?) on the chart
$: bottomScale = d3
  .scaleLinear()
  .domain([0, d3.max(data, bottomAccessor) * 1.1])
  .range([
    (height - margins.bottom - margins.top) / 2 + margins.top,
    height - margins.bottom,
  ])
  .nice();
Enter fullscreen mode Exit fullscreen mode

With these scales, the same line drawing techniques from the last post can be used to create the two measure lines.

First, the line paths were created using the generateFeltonLine method:

$: topLine = d3.line()(
  generateFeltonLine(
    augmentedData,
    xScale,
    xAccessor,
    topScale,
    topAccessor
  )
);

$: bottomLine = d3.line()(
  generateFeltonLine(
    augmentedData,
    xScale,
    xAccessor,
    bottomScale,
    bottomAccessor
  )
);
Enter fullscreen mode Exit fullscreen mode

Then they were placed within the SVG along with some CSS classes for styling:

<svg viewBox="0 0 {width} {height}" ...>
<path class="top-line" d={topLine} />
<path class="bottom-line" d={bottomLine} />
...
</svg>
Enter fullscreen mode Exit fullscreen mode

Note: Much of the boilerplate in the code has been omitted to keep things short. These omissions are represented by ... in the source snippets. See the GitHub repository for the working implementation.

Generating Irregular Stippling

The original histogram used a form of stippling to differentiate the bottom measure from the top measure. My goal was to find an algorithm to generate realistic-looking stippling, generate a polygon that represents the area to be shaded, and then use the polygon as a mask to determine which points should be shown.

Aesthetically pleasing random point distribution

It turns out that generating stippling that looks good is actually somewhat difficult. Luckily I was able to lean on the community to find an existing implementation of Bridson’s Algorithm, which can be used to approximate human stippling. Thanks (again) Mike Bostock!

I added a version of this generator function to my code and was able to use it to generate a set of points like so:

import { poissonDiscSampler } from "../utils.ts";

// Use the generator to generate all of the points
// so that they can be iterated over by svelte
function generatePoissonPoints(width, height, radius) {
  const sampler = poissonDiscSampler(width, height, radius);
  let points = [];
  let a: [float, float] = sampler();
  while (a) {
    points.push(a);
    a = sampler();
  }
  return points;
}

// generate points covering the entire bottom of the SVG
// they'll be filtered later
$: points = generatePoissonPoints(
  width,
  (height - margins.top - margins.bottom) / 2,
  10
);
Enter fullscreen mode Exit fullscreen mode

(The actual radius value of 10 that I used was the result of some visual trial and error.)

If you were to plot these points without any filtering, you get this:

Stippling without polygon containment checks
Stippling without polygon containment checks

The stippling looks great! Now we just need to get rid of everything outside of the measure area.

Creating the bounding polygon

Unsurprisingly, D3 has the ability to perform polygon containment checks out of the box via polygonContains. Since I had already created a function to generate the set of points that constitute the measure line, creating a polygon from those points was straightforward.

function generateClosedFeltonPolygon(data, xScale, xAccessor,
yScale, yAccessor) {
  const lineSegments = generateFeltonLine(data, xScale, xAccessor,
    yScale, yAccessor);
  return [
    // First point - on the origin
    [xScale(xAccessor(data[0])), yScale(0)],
    // Now the generated points for the measure lines
    ...lineSegments,
    // Bring the line back to the x-axis
    [xScale(xAccessor(data[data.length - 1])), yScale(0)],
    // Now close the polygon by going back to the origin
    [xScale(xAccessor(data[0])), yScale(0)],
  ];
}

$: closedPoly = generateClosedFeltonPolygon(
  augmentedData,
  xScale,
  xAccessor,
  bottomScale,
  bottomAccessor
);
Enter fullscreen mode Exit fullscreen mode

Once the polygon was created, it could be used to filter the point array.

<svg ...>
  ...
  {#each points as point}
    {#if d3.polygonContains(closedPoly, [point[0], height / 2 + point[1]])}
      <circle
        class="stipple"
        cx={point[0]}
        cy={height / 2 + point[1]}
        r={2.5}
      />
    {/if}
  {/each}
</svg>
Enter fullscreen mode Exit fullscreen mode

At this point, the filtered histogram looked like this:

Stippling after implementing polygon containment checks
Stippling after implementing polygon containment checks

Adding Labels

The final step was to add measure labels above and below the x-axis:

<text
  class="top-label"
  text-anchor="end"
  x={width - margins.right}
  y={(height - margins.top - margins.bottom) / 2 +
     margins.top - textHeight / 2}>Top</text
>
<text
  class="bottom-label"
  text-anchor="end"
  x={width - margins.right}
  y={(height - margins.top - margins.bottom) / 2 +
     margins.top + textHeight}>Bottom</text
>
Enter fullscreen mode Exit fullscreen mode

Unfortunately the lower label was somewhat obscured by the stippling:

Measure labels - detail
Measure labels - detail

I experimented with CSS-based text shadows in an attempt to create an outline that would mask the nearby points, but that didn’t work as well as I hoped it would. I did a little more research and eventually came across an article describing the feMorphology filters available in SVGs. (It’s still amazing to me what’s built into SVG!)

These filters can be used to define a series of image transformations, which in this case were used to implement a very clean text outline (see the linked article for a detailed description of how this actually works).

<svg ...>
  <filter id="outline">
    <feMorphology
      in="SourceAlpha"
      result="DILATED"
      operator="dilate"
      radius="5"
    />
    <feFlood
      flood-color="rgba(55, 65, 81, 1)"
      flood-opacity="1"
      result="BACKGROUND"
    />
    <feComposite
      in="BACKGROUND"
      in2="DILATED"
      operator="in"
      result="OUTLINE"
    />
    <feMerge>
      <feMergeNode in="OUTLINE" />
      <feMergeNode in="SourceGraphic" />
    </feMerge>
  </filter>

  <text
    class="top-label"
    filter="url(#outline)"
    text-anchor="end"
    x={width - margins.right}
    y={(height - margins.top - margins.bottom) / 2 +
       margins.top - textHeight / 2}>Top</text
  >
  <text
    class="bottom-label"
    filter="url(#outline)"
    text-anchor="end"
    x={width - margins.right}
    y={(height - margins.top - margins.bottom) / 2 +
       margins.top + textHeight}>Bottom</text
  >
</svg>
Enter fullscreen mode Exit fullscreen mode

Once the SVG text elements were altered to include a reference to the filter via filter="url(#outline)", the label text in the stippled area became significantly more legible:

SVG text outline via feMorphology filters - detail
SVG text outline via feMorphology filters - detail

Final Result

At this point, the final version of the histogram was complete:

Final histogram version - Variant 2
Final histogram version - Variant 2

The code I used to write this post is available on GitHub.

If you have any feedback, please let me know on Twitter or feel free to contact me here.

Top comments (0)