← Back to projects

StoryGraph — matplotlib storytelling toolkit

open source

matplotlib can draw anything — and its defaults show data without telling stories: rainbow colors, framed legends, rotated labels, and a title that names the axes instead of the finding. The reader has to do all the thinking.

The problem

After reading Cole Knaflic's Storytelling with Data, I wanted to apply its principles in the library I actually work in — fast and repeatably. But default matplotlib fights that at every step: each chart is a from-scratch battle against clutter, and nothing carries over to the next one.

Why it mattered

In analyst work a chart that only shows data is a finding nobody acts on — the point is buried under rainbow bars and a framed legend, and the reader moves on in two seconds. Without a reusable system, every chart re-fights the same styling battle, so good analysis keeps dying at the last step: the picture.

What I built

I built StoryGraph — a matplotlib toolkit that encodes one habit: gray everything, color the story, title the insight, annotate the why. It ships a drop-in style kit (two .mplstyle presets), a six-role palette, annotation and source-note helpers, and a custom diverging colormap, so a new chart inherits the whole system in a few lines. To prove it, I rebuilt 10 real charts — from default matplotlib to story-first — across 10 different chart types, all on live public datasets.

  1. Step 1
    Gray everything

    Push all context to neutral gray so nothing competes for attention.

  2. Step 2
    Color the story

    One color, on the single series that actually carries the point.

  3. Step 3
    Title the insight

    The title states the finding — not the names of the axes.

  4. Step 4
    Annotate the why

    An arrow or a short note delivers the one thing to remember.

The four-move method that turns a default chart into a two-second argument.

Key decisions

Reusable toolkit over one-off styling. A drop-in style kit plus annotation helpers means every future chart inherits the system in a few lines — instead of re-styling from scratch and drifting out of sync each time.

Code-native matplotlib over Tableau / Power BI / Plotly. matplotlib gives full control, version-controlled and reproducible output, no license, and it's the library the analysis already lives in — so storytelling isn't quarantined in a separate BI tool.

Three hard constraints over flexibility. ≤3 colors, exactly one insight per chart, and the accent color reserved for genuinely urgent data. The limits are the point: they force the clarity that unlimited options quietly destroy.

Outcome

10 default matplotlib charts 10 story-first arguments
  • 10 default matplotlib charts → 10 story-first redesigns across 10 chart types (big number, slope, dumbbell, heatmap, and more).
  • A reusable kit — six-role palette, two style presets, annotation + source-note helpers, a custom diverging colormap. New charts inherit the system in a few lines.
  • Built on real data — 11 Our World in Data datasets, not toy numbers.
  • One rulebook, enforced — ≤3 colors, exactly 1 insight per chart, accent reserved.

What a production version needs

This is a toolkit and gallery, not a published package. To go further it would need:

  • A pip-installable package with a stable public API, instead of a sys.path import.
  • Tests around the style API and colormaps so redesigns can't silently regress.
  • More chart types and an interactive (web / Plotly) path for dashboards.
  • A published gallery site so the before/after pairs are browsable without the repo.

Three redesigns, before → after

Same data, same library — only the storytelling changes.

Default matplotlib bar chart of extreme poverty over timeDefault matplotlib StoryGraph redesign: the single number, 130,000 people per dayStoryGraph
Big number: a bar chart of poverty data becomes the one stat that sticks — 130,000 people escaped extreme poverty every day.
Default matplotlib grouped bar chart of renewable energy shareDefault matplotlib StoryGraph slope chart of renewable energy shareStoryGraph
Slope chart: grouped bars force you to compare heights; the slope makes direction and magnitude the story — Denmark rockets up, others flatline.
Default matplotlib viridis heatmap of life-expectancy factorsDefault matplotlib StoryGraph diverging-colormap heatmap of life-expectancy factorsStoryGraph
Heatmap: a default viridis grid becomes a diverging colormap with only the strong correlations labeled — schooling (r=0.75) beats GDP (r=0.46).
Stack
Pythonmatplotlibpandasnumpy