GODRICH
#css-animation#button#loader#transition#scroll#dashboard#ui#design-trends

9 Data Visualization Chart Types With Real Math

A data visualization chart types set built from real math, not screenshots. Three of the nine flow like ribbons, three break into bars, two clump into circles,

Auto-plays · click a tile to jump to its section · all nine in one zip

9 Data Visualization Chart Types: How One Value Reshapes the Drawing

Most roundups list data visualization charts and graphs in whatever order looks best in a thumbnail; this one is ordered by how much of the drawing a single input value actually reshapes. Item 01 turns one flow amount into a ribbon's thickness, items 02 through 04 turn a running total, a stage share, and a category size into a bar height, a trapezoid width, and a circle radius, and item 05 turns a sorted array into a box-and-whisker shape. From there, items 06 through 08 compare several values across axes, brushes, or ranked positions at once, and item 09 closes the set by packing four numbers — open, high, low, close — into a single candlestick mark. Reading them in this order shows the same idea scaling up: one JavaScript number in, one shape out.

Code by Chart Type

Nine index.html/style.scss pairs sit behind this set, one canvas per chart type, and each snippet below is pulled straight from the file the live demo runs — not a simplified rewrite. If you've been hunting for sankey diagram examples that show the ribbon math instead of just a finished screenshot, item 01 is the one to read first.

01Sankey Ribbon Diagram Whose Width Encodes Flow Value

Each ribbon's thickness is calculated from the amount it carries, so a wider ribbon reads at a glance as a bigger flow. It fits a budget-allocation or traffic-source report, where the reader needs to compare flow sizes without reading a single number.

Sankey Ribbon Diagram Whose Width Encodes Flow Value
Open in a new tab

01 · Sankey Ribbon Diagram Whose Width Encodes Flow Value — Measured area 31.643% · moving frames 19/23Original by GODRICH · SCSS and React ship in the zip · See the password

SANKEY_DRAW_DURATION_MSRIBBON_STAGGER_MSbuildLayout
  var rand = seedRand(425011);
  var LEFT = ['A', 'B', 'C'];
  var RIGHT = ['P', 'Q'];
  var FLOW = [
    [18 + rand() * 40, 0],
    [10 + rand() * 30, 14 + rand() * 36],
    [0, 22 + rand() * 44]
  ];

  // Time (ms) for a ribbon to draw from left to right. The variant changes this from 900 to 1800.
  var SANKEY_DRAW_DURATION_MS = 900;
  var RIBBON_STAGGER_MS = 90;

  function clamp(v, a, b) { return Math.max(a, Math.min(b, v)); }
  function lerp(a, b, t) { return a + (b - a) * t; }
  function easeOutCubic(x) { return 1 - Math.pow(1 - x, 3); }

  function buildLayout(cw, ch) {

Item 01 repainted 31.6% of the frame at an average intensity of 88.9 across 19/23 measured frames.

02Waterfall Bridge Chart Connecting Each Rise and Fall

Each bar starts exactly where the previous one's running total ended, so the rises and falls read as a connected bridge instead of a row of disconnected columns. It suits a monthly revenue bridge for a briefing slide, where a viewer needs to see which single change moved the total the most.

Waterfall Bridge Chart Connecting Each Rise and Fall
Open in a new tab

02 · Waterfall Bridge Chart Connecting Each Rise and Fall — Measured area 10.019% · moving frames 22/23Original by GODRICH · SCSS and React ship in the zip · See the password

BAR_GROW_MSBAR_STAGGER_MSCOLOR_DOWN
  var BAR_GROW_MS = 260;
  var BAR_STAGGER_MS = 210;

  function clamp(v, a, b) { return Math.max(a, Math.min(b, v)); }
  function lerp(a, b, t) { return a + (b - a) * t; }
  function easeOutCubic(x) { return 1 - Math.pow(1 - x, 3); }

  // bars[0] and bars[5] are totals, bars[1..4] are deltas. anchor is the point the bar's grow animation starts from.
  var bars = [
    { kind: 'total', value: startVal, anchor: 0, target: startVal },
    { kind: 'delta', value: deltas[0], anchor: running[0], target: running[1] },
    { kind: 'delta', value: deltas[1], anchor: running[1], target: running[2] },
    { kind: 'delta', value: deltas[2], anchor: running[2], target: running[3] },
    { kind: 'delta', value: deltas[3], anchor: running[3], target: running[4] },
    { kind: 'total', value: endVal, anchor: 0, target: endVal }
  ];

  var COLOR_TOTAL = '#0a4fe8';
  var COLOR_UP = '#2b9d74';
  var COLOR_DOWN = '#c2361a';

Item 02 repainted 10.0% of the frame at an average intensity of 171.7 across 22/23 measured frames — the sharpest per-pixel change of the nine, since a whole bar swaps color and height in one step.

03Funnel Chart That Narrows at Every Stage

Each stage's trapezoid width is calculated from its share of the value before it, so drop-off shows up as a visible narrowing rather than a number you have to subtract yourself. It's built for a signup-to-checkout conversion funnel report, where the point of biggest loss needs to jump out visually.

Funnel Chart That Narrows at Every Stage
Open in a new tab

03 · Funnel Chart That Narrows at Every Stage — Measured area 29.776% · moving frames 21/23Original by GODRICH · SCSS and React ship in the zip · See the password

makeFunnelDATASET_ASTAGE_COLORS
  var rand = seedRand(425031);
  function makeFunnel() {
    var v = 100;
    var out = [v];
    for (var i = 0; i < 4; i += 1) {
      v = v * (0.55 + rand() * 0.25);
      out.push(v);
    }
    return out;
  }
  var DATASET_A = makeFunnel();
  var DATASET_B = makeFunnel();

  var STAGE_COLORS = ['#2f6df6', '#c2361a', '#2b9d74', '#8a5cd6', '#0a4fe8'];

Item 03 repainted 29.8% of the frame at an average intensity of 40.7 across 21/23 measured frames.

04Bubble-Pack Chart Where Circles Push Apart by Value Size

Bigger values get a bigger radius, and positions are recalculated repeatedly until no two circles overlap, so size alone compares the values without any axis at all. It works as a category revenue-share comparison card, where a small dashboard tile has no room for labeled axes.

Bubble-Pack Chart Where Circles Push Apart by Value Size
Open in a new tab

04 · Bubble-Pack Chart Where Circles Push Apart by Value Size — Measured area 25.791% · moving frames 18/23Original by GODRICH · SCSS and React ship in the zip · See the password

BUBBLE_PACK_COLLISION_ITERATIONSsnapshotsradii
  var COUNT = 7;
  var K = 3.1;
  var BUBBLE_PACK_COLLISION_ITERATIONS = 30;

  function buildPack(seed) {
    var pr = seedRand(seed);
    var values = [];
    for (var vi = 0; vi < COUNT; vi += 1) values.push(30 + pr() * 220);
    var radii = values.map(function (v) { return Math.sqrt(v) * K; });
    var packCenterX = CALC_W * 0.5;
    var packCenterY = CALC_H * 0.5;
    var initialPositions = [];
    for (var pi = 0; pi < COUNT; pi += 1) {
      initialPositions.push({
        x: packCenterX + (pr() - 0.5) * 46,
        y: packCenterY + (pr() - 0.5) * 46
      });
    }
    var snapshots = [initialPositions.map(function (p) { return { x: p.x, y: p.y }; })];
    var relaxed = initialPositions.map(function (p) { return { x: p.x, y: p.y }; });
    for (var iter = 0; iter < BUBBLE_PACK_COLLISION_ITERATIONS; iter += 1) {

Item 04 repainted 25.8% of the frame at an average intensity of 117.6 across 18/23 measured frames.

05Boxplot Drawing Quartiles as a Box and Whiskers

Values are sorted to compute the quartile boundaries, then mapped into the box height and whisker length, so the median and spread live in one shape instead of a table of five numbers. It fits a response-time distribution comparison by group, where the spread matters as much as the average.

Boxplot Drawing Quartiles as a Box and Whiskers
Open in a new tab

05 · Boxplot Drawing Quartiles as a Box and Whiskers — Measured area 12.065% · moving frames 20/23Original by GODRICH · SCSS and React ship in the zip · See the password

GROUP_A_1quartileslerpStats
  var GROUP_A_1 = sample(9), GROUP_A_2 = sample(9);
  var GROUP_B_1 = sample(9), GROUP_B_2 = sample(9);

  function quartiles(arr) {
    var s = arr.slice().sort(function (a, b) { return a - b; });
    function pct(p) {
      var idx = (s.length - 1) * p, lo = Math.floor(idx), hi = Math.ceil(idx);
      return s[lo] + (s[hi] - s[lo]) * (idx - lo);
    }
    return { min: s[0], q1: pct(0.25), med: pct(0.5), q3: pct(0.75), max: s[s.length - 1] };
  }
  var qA1 = quartiles(GROUP_A_1), qA2 = quartiles(GROUP_A_2);
  var qB1 = quartiles(GROUP_B_1), qB2 = quartiles(GROUP_B_2);

  function lerp(x0, x1, f) { return x0 + (x1 - x0) * f; }
  function lerpStats(s1, s2, f) {
    return {
      min: lerp(s1.min, s2.min, f),
      q1: lerp(s1.q1, s2.q1, f),
      med: lerp(s1.med, s2.med, f),
      q3: lerp(s1.q3, s2.q3, f),
      max: lerp(s1.max, s2.max, f)
    };
  }

Item 05 repainted 12.1% of the frame at an average intensity of 128.2 across 20/23 measured frames.

06Parallel-Coordinates Chart Linking Values Across Axes With Crossing Lines

One item's several values are calculated as coordinates on each vertical axis and joined by line segments, and dragging across one axis highlights only the lines passing through it. It suits a multi-attribute product spec comparison, and it's one of only two items in this set — the other is 09 — that also responds to arrow-key input, not just a pointer.

Parallel-Coordinates Chart Linking Values Across Axes With Crossing Lines
Open in a new tab

06 · Parallel-Coordinates Chart Linking Values Across Axes With Crossing Lines — Measured area 46.867% · moving frames 21/23Original by GODRICH · SCSS and React ship in the zip · See the password

BRUSH_AXIS_INDEXsetBrushCenterautoDemoStep
  function setBrushCenter(v) {
    brushCenter = clamp(v, BRUSH_HALF, 1 - BRUSH_HALF);
    render();
  }

  canvas.addEventListener('pointerdown', function (e) {
    var rect = canvas.getBoundingClientRect();
    var x = (e.clientX - rect.left) * (cw / rect.width);
    var axisPx = axisX(BRUSH_AXIS_INDEX);
    if (Math.abs(x - axisPx) < cw * 0.14) {
      dragging = true;
      var y = (e.clientY - rect.top) * (ch / rect.height);
      lastUserInputAt = elapsed;
      setBrushCenter(yToValue(y));
    }
  });
  canvas.addEventListener('pointermove', function (e) {
    if (!dragging) return;
    var rect = canvas.getBoundingClientRect();
    var y = (e.clientY - rect.top) * (ch / rect.height);
    lastUserInputAt = elapsed;
    setBrushCenter(yToValue(y));
  });
  canvas.addEventListener('pointerup', function () { dragging = false; });
  canvas.addEventListener('pointerleave', function () { dragging = false; });

Item 06 repainted 46.9% of the frame at an average intensity of 33.6 across 21/23 measured frames — the widest area of the nine, since dragging the brush redraws every line on the canvas at once.

07Coxcomb Chart Fanning Out Petals With a Radius per Value

The circle is split into equal-angle wedges, and each value is converted into a radius, so a bigger value stretches its petal farther out even at the same width. It's built for a monthly recurring metric comparison dashboard card, where every category needs the same visual weight except for its length.

Coxcomb Chart Fanning Out Petals With a Radius per Value
Open in a new tab

07 · Coxcomb Chart Fanning Out Petals With a Radius per Value — Measured area 19.5% · moving frames 17/23Original by GODRICH · SCSS and React ship in the zip · See the password

COXCOMB_PETAL_GAP_DEGtargetRadiiradiusAt
  var COXCOMB_PETAL_GAP_DEG = 2;
  var anglePerPetal = (360 - COXCOMB_PETAL_GAP_DEG * COUNT) / COUNT;

  var MIN_R = 18, MAX_R = 130;
  var maxValue = Math.max.apply(null, VALUES);
  var targetRadii = VALUES.map(function (v) { return MIN_R + (v / maxValue) * (MAX_R - MIN_R); });

  function easeOutCubic(x) { return 1 - Math.pow(1 - x, 3); }
  function easeInCubic(x) { return x * x * x; }
  function clamp01(x) { return Math.max(0, Math.min(1, x)); }

  var CENTER_X = CALC_W * 0.5, CENTER_Y = CALC_H * 0.5;
  var FILL_COLORS = ['47,109,246', '194,54,26', '43,157,116', '138,92,214'];
  var INK_STROKE = '23,20,26';

  function radiusAt(i, t) {
    var target = targetRadii[i];
    if (t < 1400) {
      var growProgress = clamp01((t - i * 90) / 500);
      return target * easeOutCubic(growProgress);
    }
    if (t < 1750) return target;
    var shrinkProgress = clamp01((t - 1750) / 200);
    return target * (1 - easeInCubic(shrinkProgress));
  }

Item 07 repainted 19.5% of the frame at an average intensity of 137.9 across 17/23 measured frames.

08Bump Chart Where Lines Cross as Rankings Change

Values are re-ranked at every period and turned into a vertical coordinate, so lines cross exactly where two items swap places. It fits a quarterly product-ranking change chart, where the crossing points themselves are the story.

Bump Chart Where Lines Cross as Rankings Change
Open in a new tab

08 · Bump Chart Where Lines Cross as Rankings Change — Measured area 42.761% · moving frames 20/23Original by GODRICH · SCSS and React ship in the zip · See the password

ranksOfPERIOD_COUNTSEG_DUR
  var rand = seedRand(425081);
  var ITEM_COUNT = 5, PERIOD_COUNT = 5;
  var VALUES = [];
  for (var p = 0; p < PERIOD_COUNT; p += 1) {
    var row = [];
    for (var i = 0; i < ITEM_COUNT; i += 1) row.push(10 + rand() * 90);
    VALUES.push(row);
  }
  function ranksOf(row) {
    var idx = row.map(function (v, i) { return i; });
    idx.sort(function (a, b) { return row[b] - row[a]; });
    var rank = new Array(row.length);
    idx.forEach(function (itemI, pos) { rank[itemI] = pos; });
    return rank;
  }
  var RANKS = VALUES.map(ranksOf);
  var LINE_COLORS = ['#2f6df6', '#c2361a', '#2b9d74', '#8a5cd6', '#0a4fe8'];

  var LOOP_MS = 2000;
  var T0 = 200;
  var SEG_MS = 300;
  var SEG_DUR = 260;
  var SEG_COUNT = PERIOD_COUNT - 1;

Item 08 repainted 42.8% of the frame at an average intensity of 75.8 across 20/23 measured frames.

09Candlestick Chart Drawing Open-High-Low-Close With Body and Wick

The gap between open and close is drawn as a solid body, while the reach up to the high and down to the low becomes a thin wick, packing four values into one bar. Candlestick is one of the few chart types in trading that fits this much information into a single mark, which is why item 09 is built as a coded stock or exchange-rate history panel.

Candlestick Chart Drawing Open-High-Low-Close With Body and Wick
Open in a new tab

09 · Candlestick Chart Drawing Open-High-Low-Close With Body and Wick — Measured area 13.603% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

CANDLE_REVEAL_STAGGER_MSgrowPnearestCandle
  function clamp(v, lo, hi) { return Math.max(lo, Math.min(hi, v)); }
  function easeOutCubic(x) { return 1 - Math.pow(1 - x, 3); }

  function candleX(idx) {
    var padX = cw * 0.06;
    var slot = (cw - padX * 2) / CANDLE_COUNT;
    return padX + slot * (idx + 0.5);
  }
  function priceY(v) {
    var padY = ch * 0.12;
    return ch - padY - (ch - padY * 2) * (v - scaleMin) / (scaleMax - scaleMin);
  }
  function growP(idx, t) {
    return clamp((t - T0 - idx * CANDLE_REVEAL_STAGGER_MS) / CANDLE_REVEAL_DUR_MS, 0, 1);
  }
  function nearestCandle(x) {
    var best = 0, bestD = Infinity;
    for (var k = 0; k < CANDLE_COUNT; k += 1) {
      var d = Math.abs(candleX(k) - x);
      if (d < bestD) { bestD = d; best = k; }
    }
    return best;
  }

Item 09 repainted 13.6% of the frame at an average intensity of 121.0 across 23/23 measured frames — every one of the 23 measured deltas moved, more than any other item in this set.

Where the Value Changes the Drawing — Ribbon Width, Circle Radius, Petal Angle

None of these nine draw a fixed shape and swap a label — the shape itself is a function of the value. Item 01's ribbon, timed by SANKEY_DRAW_DURATION_MS, gets its width from FLOW; item 04's radii come from Math.sqrt(v) * K, so circle area, not radius, tracks the value linearly; and item 07's radiusAt(i, t) reads straight from targetRadii, which is VALUES rescaled between MIN_R and MAX_R. The requestAnimationFrame loop behind each canvas (see MDN's Window.requestAnimationFrame()) only decides how the shape gets there — the destination itself always comes from the data. The table below measures how much of each canvas actually repaints on the way:

Pattern Cumulative area Avg. intensity Frames moved
01 Sankey ribbon 31.6% 88.9 19/23
02 Waterfall bridge 10.0% 171.7 22/23
03 Funnel taper 29.8% 40.7 21/23
04 Bubble-pack collision 25.8% 117.6 18/23
05 Boxplot whiskers 12.1% 128.2 20/23
06 Parallel-coordinates brush 46.9% 33.6 21/23
07 Coxcomb petals 19.5% 137.9 17/23
08 Bump rank crossing 42.8% 75.8 20/23
09 Candlestick OHLC 13.6% 121.0 23/23

Where It Breaks — Reduced Motion, Narrow Screens, Zero or Negative Values

All nine items check window.matchMedia('(prefers-reduced-motion: reduce)') before they auto-play, so none of them force a moving canvas on a reader who has that setting on — only two of the nine, 06 and 09, also take arrow-key input as a pointer alternative, and the other seven stay pointer- or autoplay-only. On narrow screens, every item's fit() function reads wrap.getBoundingClientRect() and resizes the canvas to match, so the drawing always fills its wrapper — but item 09's OHLC label chip is still set with a fixed ctx.font = '11px sans-serif', so on a phone-width demo that label can crowd against the candle it's naming instead of shrinking with everything else — all nine items' below-canvas caption text does shrink to 10px under a shared @media (max-width: 340px) rule, but that CSS rule can't reach text drawn inside the canvas itself. Zero and negative values aren't all handled the same way, z7mev6vs, and that gap is worth knowing before you drop a real dataset in: item 02's waterfall explicitly branches on b.value >= 0 ? COLOR_UP : COLOR_DOWN, so a negative delta paints correctly on purpose, but item 03's dropRate divides by values[i - 1] with no zero guard, so a stage with a value of exactly zero produces a non-finite drop-rate label, and item 09's priceY() divides by (scaleMax - scaleMin), so a flat dataset where every candle shares the same high and low collapses that range to zero and breaks the vertical scale.

Three Variants and Accessibility

Variant Constant & values Effect
Sankey draw-in speed SANKEY_DRAW_DURATION_MS 900 → 1800 Lowering the value draws every ribbon in one quick sweep, while raising it slows the draw so each ribbon appears to trace its flow.
Bubble-pack collision iterations BUBBLE_PACK_COLLISION_ITERATIONS 30 → 80 Lowering the value settles the circles quickly while some still overlap, while raising it keeps nudging them longer until every overlap is fully resolved.
Coxcomb petal gap angle COXCOMB_PETAL_GAP_DEG 2 → 10 Lowering the value packs the petals almost edge to edge into one solid disc, while raising it opens gaps so each petal reads as a separate wedge.

Every canvas in this set is built with the CanvasRenderingContext2D API and nothing else — no chart library, no WebGL context — so the accessibility story is whatever the nine index.html files do themselves. Nine out of nine skip their autoplay loop under prefers-reduced-motion: reduce and settle on a static final frame instead of looping forever, which matters more here than on a CSS transition because a canvas redraw doesn't pause on its own. Only items 06 and 09 add a keydown handler (ArrowUp/ArrowDown for the brush, ArrowLeft/ArrowRight for the candle cursor) as a non-pointer way to reach the same interaction a mouse or touch drag already gives you; the other seven items are autoplay-only demos with no interactive state to reach by keyboard in the first place.

What Sets These Chart Types for Data Visualization Apart?

A charting library gives you all nine of these encodings behind one config object, which is exactly why it's hard to see what each one is actually doing with your numbers. Stripped down to plain canvas math, item 01's ribbon width, item 05's quartile box, and item 08's rank-swap crossing point are each doing one clear conversion from array to pixel, and reading that conversion is the fastest way to know which chart type actually fits a dataset instead of just looking impressive next to it. CSS Chart Design: 9 SVG Graphs, No Library draws a different five of these encodings in SVG path data instead of canvas pixels, which is the version to reach for when the chart needs to stay crisp at any zoom level, and Chart UI Design: 9 Interactions You Can Drag picks up right where this set stops — turning a finished chart into something a reader can drag, brush, or filter. If the flow-and-stage half of this set is closer to what you need, JavaScript Chart Drawing, 9 Ways — No Library covers a second, non-overlapping batch of canvas-drawn chart types built the same no-library way.

FAQ: What to Check Before Wiring These Into a Live Dashboard

Are these good sankey diagram examples for a real dataset, or just a demo with fake numbers?

The seeded random numbers in item 01 are only there so the demo replays the same layout every time; the buildLayout() function reads FLOW the same way regardless of where those numbers came from, so swapping in a real dataset only means changing what fills that array, not how the ribbon width is calculated.

Why does item 02 use anchor instead of just drawing each bar at its own value?

Because a waterfall bar has to start where the previous total ended, not at zero — anchor stores that running total so the bar's grow animation has a fixed floor to climb from, and the visual bridge between bars only works if that floor is correct.

Does the bubble-pack chart in item 04 ever fully stop overlapping?

Not always inside BUBBLE_PACK_COLLISION_ITERATIONS at its default of 30 — that's a fixed iteration count, not a check for "no overlap left," so a crowded dataset can still show a slight overlap when the loop ends; raising the constant toward 80, as the second variant does, gives the relaxation more passes to fully separate every circle.

Enter the archive password

The password is inside this article. You will find it as you read.