For people who want to make data videos, not just read about them

A finished data reel, in two commands.

47.06s

2160×384060 fps7.9 MB

read out of ffprobe, not the render log

The project behind @reelgorithm.py — 17.4M views across 69 reels. The chart library, the motion layer, and one worked episode.

Regular price $79.99, now $39.99 onceSave $40 until December 1530-day refundLifetime updates

~/reel-starterthe two commands
$ npm install $ npm run render  remotion render src/index.ts Episode \    out/Episode.mp4 --scale=2 --crf=16   bundled   src/index.ts  rendered  Episode   2,820 frames   out/Episode.mp4  2160x3840  60fps  47.06s  7.9 MB

2,820 frames · 13 dependencies · Node 18+ · nothing else

17.4M

views on the channel

69

reels, all measured

75K

Instagram followers

98

charts, 15 families

51,134

words, and the code

measured · 17,399,231 views and 74,820 followers read out of analytics/data on 19 September 2026. What the work did — not a number you are promised.

The work

Eight reels, and what each one measured

Cut from the delivered masters — the files that were posted, not re-renders. The six most recent under the skip ceiling, plus the two that set it.

The team the flags cost most

29K
views
30%
skip@3s

Strongest in the final five

25K
views
38.6%
skip@3s

The best bank

30K
views
37.6%
skip@3s

What your team owes you

447K
views
33.7%
skip@3s

The market is right — that's why you lose

30K
views
36.8%
skip@3s

The hardest sport to go pro in

671K
views
36.4%
skip@3s

The player who shouldn't exist

5.4M
views
40.2%
skip@3s

Ticketmaster

247K
views
21.7%
skip@3s

Drag the ring, or pick a reel for the rest of its numbers.

source · codepop/out/*.mp4 · figures from analytics/data/history.csv, pulled 2026-09-19 · 17,399,231 views across 69 reels

Who wrote it

@reelgorithm.py

I run a data-storytelling channel. One named dataset per video, the data published with it, and the working shown in an article afterwards. 68.4K people follow it and 15.3M views have gone through it — and neither of those is a number I’m promising you.

Anatomy of a Data Reel is that process written down after 65 of them: what gets picked, what gets thrown out, where the data comes from, and the eight or nine times I got it wrong first.

You have no reason to take any of that on faith, so none of it is behind a login:

The channel is the evidence and the book is the method. If the videos don’t look like something you’d want to be able to make, that’s a good reason not to buy this.

The numbers

The hook doesn’t make a reel work. It decides whether it’s allowed to

Every reel on the channel, plotted on the one measurement that predicts the rest. The interesting part of this chart is the part with nothing in it.

1K10K100K1M10Mempty28 reels, none above 88,573most viewedlowest skip20%30%40%50%60%70%80%left within 3 seconds →views

61×

more views available below 45% skip

88,573

the best of the 28 above it

5.4M

the best of the 41 below

-0.73

correlation, against log views

The most-viewed reel on the channel is not the best hook. Messi sits at 40.2% — worse than eight reels that did a hundredth of its numbers. A hook cannot make anyone care. What it can do is decide whether anyone is still there to find out, and that is why there is no line fitted through this cloud: the shape is a ceiling, not a slope.

source · analytics/data/history.csv · 69 reels with a skip reading · pulled 19 September 2026 · best TicketDrop 21.7%, median 42.2%, worst Sorting 84.6%

And they keep going

36 reels, 67 days, nothing new posted

+17.3%

The same set of reels, frozen on 14 July 2026, picked up 1,878,219 views on their own. Nothing in this line is a new post or a reel the tracker found later.

0+1.0M+2.0M14 Jul19 Sep

source · analytics/data/history.csv · the 36 scenes already tracked on 2026-07-14, carried forward to 2026-09-19 · views gained, so the baseline is a true zero

In the box

A repo that runs, and about 51,000 words on why it runs that way

One zip, 0.27 MB, 72 files, Node 18 and nothing else — and the writing is a web page you open in a browser, not a PDF and not a video course.

reel-starter.zip0.27 MB · 72 files
reel-starter/
  src/
    viz/          98 charts, 15 families
      core/       scales, marks, morph
    episode/      the worked 6-beat reel
      beats/      one file per beat
      layout.ts   throws on collision
    motion.tsx    easing, springs, air
    grid.ts       the 8pt law
    Watermark.tsx ships as "yourchannel."
  data/
    pro-pipeline.csv   the episode's dataset
  qa/ql.mjs       the gate
  scripts/
    sheet.mjs     24 stills, one bundle
    make-data.mjs one real bug, on purpose
5 galleries13 dependenciesTypeScriptRemotionno API keys

Three families are held back. geo, three, iso3d are the only files pulling three.js, deck.gl and topojson — roughly 200 MB of install, and where every one of the four silent black-frame traps lives. Leaving them out is a decision, not an oversight.

ranking14

BarH · Dumbbell

time10

Line · Area

distribution9

Histogram · Violin

relation9

Scatter · ScatterFit

flow8

Sankey · Chord

partwhole7

Treemap · Waffle

stat7

ForestPlot · QQPlot

advanced6

ParallelCoordinates · RadialBar

matrix6

Heatmap · CorrelationMatrix

single6

BigNumber · Bullet

spatial5

Choropleth · PointMap

hierarchy4

Dendrogram · CirclePack

atlas3

SystemMap · DecisionTree

showcase3

Globe · IsoCity

sport1

SportHeatmap

One contract, 98 charts. Every one takes the same props, so any chart swaps for any other in a scene — and a bar can morph into a scatter, because the marks are matched by a stable key rather than by array position.

Twelve chapters, twelve questions

about 51,000 words7 format blueprints10 fill-in sheets

  1. 01

    Is this idea even worth making?

  2. 02

    Where do I get data that isn't already on everyone else's page?

  3. 03

    How do I know my numbers aren't quietly wrong?

  4. 04

    How long should each part of the video actually be?

  5. 05

    Why does my narration sound like a robot reading a list?

  6. 06

    Why does nobody watch my chart?

  7. 07

    What's about to quietly waste my entire day?

    free to read below
  8. 08

    Did it work, and how would I actually know?

  9. 09

    What does all of this look like on one real video?

  10. 10

    How do I get a project rendering on my own machine?

  11. 11

    How is one of these actually put together, beat by beat?

  12. 12

    What's between a rendered file and a posted video?

source · codepop/starter — tree from find, counts from the library’s own VizMeta registries, zip from scripts/pack-starter.mjs · chapter questions from content/copy.ts

FreeChapter 73,221 wordsNo email

Things that cost a day

The whole chapter, not an excerpt. It’s the one I’d read first if I were deciding whether to buy the rest — if it isn’t useful to you, nothing else here will be either.

This is the free chapter. It's the one I'd read first if I were deciding whether to buy the rest.

Every failure in this chapter has one property in common, and it's the property that makes them expensive: they exit zero. Nothing crashes. No error is printed. The file gets written, the render completes, the gate goes green, and the thing is wrong.

That's not a coincidence, it's the whole category. A crash costs you twenty minutes. A silent wrong answer costs you a day, or it ships. I've lost more time to tools that returned a plausible number without doing the work than to every tool that ever threw an exception.

Here are the ones that got me.


1 · Four ways a GPU scene renders pure black

A single-frame renderer is not a browser. Anything in a WebGL stack that assumes a continuous animation loop will produce a black rectangle and no error, because from the renderer's point of view a black rectangle is a perfectly valid frame.

The backend. You have to force ANGLE. The default backend renders GL panels black without erroring; EGL and SwiftShader crash outright, which is at least honest. Pass --gl=angle (or swangle) on every render that touches WebGL, and put it in the render script rather than in your memory, because you will forget it once and lose an afternoon deciding your shader is broken.

Instanced meshes from the convenience layer. The helper component for instancing syncs its instance matrix inside the per-frame animation hook. A one-shot renderer never ticks that hook, so the matrix stays at identity and nothing draws. Build the instanced mesh imperatively and write the matrices in a layout effect instead. Same output, and it survives a single-frame capture.

Map layers that render asynchronously. A GPU map library that draws on its own schedule needs an explicit handshake: turn off its internal animation loop and open a render-delay handle per frame, released by the library's own after-render callback. Skip it and stills still pass, which is the cruel part. The full render tears instead, because a still gives the library enough wall-clock slack to finish and a 3,000-frame render doesn't.

Colour strings. Palette helpers that blend or ramp colours usually return rgb(...) strings, not hex. A GPU layer that parses hex only gives you NaN, and then uploads the NaN to the GPU without complaining. Black.

Four traps, four days. And a fifth I'll throw in free: WebGL ignores line width on the basic line material. Raw line segments are always a one-pixel hairline no matter what you set. Use the camera-facing line implementation instead, which honours the width, and which, unlike the instancing helper, sets its geometry on mount rather than in the animation loop, so it survives the one-shot render.

Two more, if you're loading real 3D models. An async model loader will miss a single-frame capture entirely and hand you a black frame; bake whatever geometry you need to a static file offline and import it, which also makes the scene a pure function of the frame number. And a model file whose texture images are missing will abort the whole parse, not just the textures — if you only need the geometry, strip the image, texture and sampler blocks out of the model header before loading it.

How to survive this class permanently: keep two smoke-test scenes, one per GPU library, and re-render both after every dependency bump. Two stills is a cheap standing cost against four days you've already paid once.


2 · The 44 milliseconds that make every sound effect land late

Every sound effect in a video was landing 45–48 milliseconds after its intended frame. Uniformly. Not drifting, not per-cue — every single one, the same amount late.

It reads as "the sound design feels a bit off" and it's almost impossible to find by listening, because 45ms is right at the threshold where audio-late becomes perceptible, and it's worst exactly where it hurts most: sharp transient cues keyed to a visual cut.

It isn't a composition bug. Rendering the identical composition to ProRes with 16-bit PCM audio put the first transient at 5.4ms — exactly the sample's own peak offset, which is to say perfectly in sync.

The lag is AAC encoder priming. The encoder needs 2,112 samples of runway, which at 48kHz is 44 milliseconds, and the muxer writes them into the file without an edit list, so nothing downstream knows to compensate. Probe the file and both streams report start_time=0.000000, which is how you confirm it: the container is asserting they're aligned and they aren't.

The fix is two commands instead of one. Render a PCM master, then encode delivery separately:

# master: ProRes video, PCM audio, no AAC anywhere near it
<renderer> render <id> out/<id>_pcm.mov --codec=prores --audio-codec=pcm-16

# delivery: ffmpeg writes the edit list, so the offset is compensated
ffmpeg -y -i out/<id>_pcm.mov -c:v libx264 -crf 16 -pix_fmt yuv420p \
  -c:a aac -b:a 192k -movflags +faststart out/<id>.mp4

That measures 5.4ms. In sync.

How to verify any file, rather than trusting me: decode the audio to raw mono PCM at a low sample rate, walk the samples until one exceeds a noise floor, and divide by the sample rate to get the time of the first energy. Compare it to the cue time you intended. A uniform offset across every cue means priming. A growing offset means drift, which is a different bug.

What not to do: don't fix this by moving your cues earlier in the timeline. It breaks the editor preview, and it rots silently the day someone changes the encoder.


3 · Two fixes that were each correct, and collided

This one is my favourite, because both changes were right, the bug only exists in the file that gets uploaded, and it is invisible in the editor, invisible in the master, and invisible in every still.

Fix one. The renderer defaults to a BT.601 colour space, so every master was being tagged as standard-definition PAL while the content was sRGB. Decoded as-tagged versus as-BT.709, the brand pink moved about 30 levels out of 255 in two channels. The fix is --color-space=bt709. Correct.

Fix two. Masters were being rendered at twice the platform's ceiling on each axis (four times the pixels), so the platform's fast server-side scaler ran on every frame, and a cheap downscaler destroys thin rules and small type first. The fix is to downscale locally with a good filter to exactly the target resolution so their scaler never runs. Also correct.

The collision. The delivery step had in_range=full hard-coded, and that was right for every master made before fix one existed, because the renderer's default output is full-range. But --color-space=bt709 makes it emit limited-range instead. So fix one turned fix two into a bug: the scaler squeezed an already-limited master a second time.

luma 8  →  16 + 8 × (219/255)  ≈  23

Measured on one episode: the master's background sat at luma 8. The delivered file's background sat at 23. A uniform, washed-out grey where the design says near-black. The report I got was simply "the background isn't the right black" — from someone watching the uploaded version on a phone.

Why this look is maximally exposed to it: about 42% of a frame sits below luma 16 and about 12% above 235. Fifty-five percent of the picture lives at the extremes, because it's white type on near-black. Any stage that disagrees about range mangles over half the frame.

The fix is to detect rather than assume. The delivery step now probes the source's pixel format and tags, decides full or limited from that, and — this is the part worth stealing — deletes its own output file rather than ship one whose tags came out wrong. A tool that refuses to leave a bad artifact on disk is worth more than a tool that warns.

And one specific ffmpeg landmine inside the same fix: passing -x264-params at all overrides ffmpeg's own -color_primaries and -color_trc, and they come out unknown. The command exits zero. Set them inside the params string instead — colorprim=bt709:transfer=bt709:colormatrix=bt709.

How to check it without eyeballing: measure the always-empty margins of the frame — the bands the platform's UI covers, which are guaranteed to contain no content, and compare their luma between master and delivery. Two cleverer metrics were tried first and both returned confidently wrong answers: a "luminance haze band" scores antialiased type, and "smooth dark pixels" scores flat chart fills.


4 · A hockey trophy on a bank

Matching a named entity to an image from a catalogue by fuzzy search is a problem that looks solved and isn't. The rule in place was "the candidate shares at least one distinctive token with the subject". Every one of these matched confidently, downloaded cleanly, and rendered:

wantedgot
Citizens BankCitizens First Bank — a different bank
The Huntington National BankHuntington Heroes — its charity programme
Capital OneCapital One Hall — a concert hall
Glacier BankGlacier County, Montana
Centennial BankCentennial Cup — a junior hockey trophy
First National Bank of PennsylvaniaFirst Bank of the United States — closed 1811
Santander BankSantander Consumer Bank
First Horizon BankFirst Tennessee Bank — its own former name

Look at what they have in common. Every one shares a word and adds another — HALL, HEROES, CUP, COUNTY, CONSUMER, and the added word is precisely what makes it a different thing.

The rule that works: require the candidate's identifying words to be a subset of the subject's, after stripping generic vocabulary and bare numbers. Accept a strong substring match separately. Be deliberately stricter than you need to be, and recover the good ones through a hand-checked pin table.

A subject with no image can fall back to a name tile, which is honest and looks deliberate. A subject wearing another company's mark is a factual error on screen that no downstream check can catch. Never loosen the rule to rescue one asset.

The mirror failure, from the same build: over-aggressive generic-word stripping emptied "Bank of America" to a token-less string, so it matched nothing. And an empty normalised key isn't a failed match — it's a key that matches every other name that also emptied out.

The same class, in brand names. Sourcing airline liveries, a bare /united/i matched China United Airlines — a red tail that shipped into a render as United, and later a British operator, where the match was on the word Kingdom. /american/i matched American Trans Air, a carrier that no longer exists. Match the full name as a phrase, carry an explicit exclusion pattern per brand, and reject any title that names two of your brands at once. That last guard alone catches most of it.

And then look at the images. Every failure above is obvious in one glance and invisible to every automated check — the file downloads, the licence is clean, the aspect ratio is fine, and the plane is the wrong colour. Contact-sheet the whole asset folder before wiring any of it into a scene.


5 · The axis helper that rounds your range and lies about it

A charting library's linear scale usually "nices" the domain by default — rounding the ends outward to friendly numbers. That's a sensible default for a computed domain, and it is a catastrophe for an explicit one.

An explicit domain of [1, 9], for a nine-item rank chart, rendered an axis running 0 → 10. Ticks for rank 0 and rank 10, neither of which exists. No error. The chart looks plausible and the numbers are wrong. The same bug was sitting in an already-published episode.

Two halves are needed to fix it, and doing only one leaves it broken:

  • pass the flag that disables nicing when the caller supplied a domain, so the domain is honoured;
  • filter the generated ticks to those inside the domain, with a small float tolerance, because the tick generator will still emit ones outside it.

Why it matters more than it looks like it does: for an ordinal quantity — ranks, counts, positions — a niced domain isn't a cosmetic difference. It invents categories that don't exist. That's a lie-factor problem, not a layout one.

Fix it in the shared chart code, not in the scene. A scene-level workaround leaves the next video to rediscover it.

So the check is mechanical now. Hand a library an explicit domain, then pull a still and read the axis end labels back against what you asked for. An explicit domain is a request rather than a guarantee, and nothing tells you when the library declined it.


6 · Ranking photos by shape gets you paintings

Sourcing 100 US city photographs from a free image repository, the ranking function preferred portrait-shaped images, because the video is portrait. Three distinct failures, each of which shipped into a render before being caught:

  1. Preferring portrait → paintings and archives. Portrait-shaped "city" content in a free repository is art and documents. Columbus came back as a Childe Hassam oil painting. Austin came back as the cover of a 1976 Armed Forces Week magazine.
  2. Preferring portrait photographs → single buildings. Kansas City became a lobby entrance. Pittsburgh became one tower.
  3. Not filtering orbit → satellite imagery. Houston came back as a photo taken from the International Space Station.

Never rank candidates by aspect ratio. Hard-filter to modern photographs, score for whole-subject views, and use aspect only as a tie-breaker. Reject on title and category words for artwork, print media, maps, monuments and diagrams; reject orbit words; reject single-structure words unless the title also matches a whole-city word; reward skyline, cityscape, panorama, aerial, downtown; require the subject's own name in the title, because search returns neighbours.

Then the framing problem, which is separate and worse. object-fit: cover destroys a landscape photograph in a portrait frame. A 1.5-aspect skyline in a 1080×1920 frame scales to 2880×1920 — you see the middle 37% of the width. No amount of re-picking photos fixes it, because the crop is the problem.

Size the image off width — around 1.4 to 1.55 times the frame width — let the height fall out of the aspect ratio, and anchor it high so the subject sits in the top half. Feather both ends with a gradient mask, or a hard-edged band reads as a photo pasted onto black. Give type its own contrast with a shadow rather than turning the scrim up until the photo dies.


7 · The probe that can't detect the thing it's measuring

Four measurements in one session were wrong in a way that looked like a result. This is the most dangerous item in the chapter and it's the reason the rest of it exists.

  • A simulation of highlight/shadow crush returned 0.08%. The scaler used the frame's own metadata and silently ignored the range override that was the entire condition being tested, so the filter never applied it. Recomputed as plain arithmetic on the luma histogram: 42.4% at or below 16, 12.5% at or above 235. Off by three orders of magnitude, and 0.08% would have ended the investigation at the wrong conclusion.
  • A PSNR comparison emitted nothing. Twice. No error, no output.
  • "Distinct dark tones — master 1, delivery 1." The sampled patch is flat in the master too, so the probe couldn't have returned anything else. It demonstrated nothing while reading as a finding.
  • An encode that "succeeded" shipped untagged — the -x264-params override from §3, exit code zero.

The rule, and it's the one worth the price of the chapter: positive-control your instrument before you believe a null. Feed the probe an input where the effect is known to be enormous. If it doesn't light up, the probe is broken, not the hypothesis.

Three corollaries:

  • Prefer arithmetic on raw values over a filter chain that might reinterpret them. Histograms don't have opinions about colour metadata.
  • Assume your tooling may silently override a flag you passed. Probe the output and assert the properties you asked for. Deleting your own bad output is the pattern to copy.
  • When a measurement is unreliable, say "inconclusive, here's why" and move on. Don't keep chasing it, and never soften it into a weak finding.

8 · The render you're looking at is the old one

A short list, because each of these individually cost an hour and collectively cost more than any single bug above.

Check the output file's modification time after every render. Not the exit code — the mtime, and the duration. A stale mtime means the render failed silently and you are about to QA yesterday's file. I once spent many turns extracting frames from a stale video while two separate failures were being swallowed by a pipe into grep.

Don't pipe build output through a filter that hides errors. If you must filter, also grep for ERROR, Traceback and command not found.

A glob that matches nothing can kill an entire command chain. In zsh, rm -f out/Thing*.mkv && <render> with no matching file aborts at the glob, so the render never runs and the failure looks like an ordinary empty result. Use a literal path, or enable null-glob, and never let a possibly-empty glob gate a long command.

Caches keyed by scene name collide across projects. A text-to-speech cache keyed on scene name meant one video silently played another video's narration in two scenes, because both projects had a scene called ColdOpen. Diagnose by comparing the cached file's timestamp against the script's — stale audio predates the script it's supposed to be reading.

Some verbosity flags suppress the output you're asking for. Running a volume measurement at error-level verbosity hides its report entirely and looks exactly like silence, which is how a perfectly good mix gets diagnosed as a missing audio file.

Check every beat, not just the ones you changed. Two collisions shipped in an already-"verified" render — an annotation drawn over seven rows of a bar chart, and a line of text overprinting an axis, because the earlier pass sampled the beats that had been edited. A full render is minutes. A still is seconds. Every defect found by rendering is a defect a still would have found for about 3% of the cost, and a defect not found ships.


The pattern

Read back through the eight. Not one of them announced itself.

The GPU renders black and reports success. The audio is 44ms late and the container claims both streams start at zero. The delivered file is grey and the master is perfect. The logo is the wrong company and the licence check passes. The axis is wrong and the chart is beautiful. The photo is a painting and the download is clean. The probe returns 0.08% and exits zero. The render is stale and the exit code is fine.

The danger is the absence of an error, not the presence of one. Every gate you build should be designed to fail loudly on the things that otherwise fail quietly, and the single most valuable one you can write is the tool that deletes its own output rather than hand you something wrong.

There's a checklist version of this chapter in the artifact pack: the pre-ship trap list.

8 more failures · free · no email

That was one chapter of 12.

It stays free whether or not you buy anything — nothing on this page moves behind a paywall later. The other 11 are the same depth, and they’re $39.99 once with 30 days to change your mind. That’s the founding price — it goes to $79.99 on December 15.

Get the other 11 — $39.99

30-day refund · lifetime updates · no subscription

Price

One price, once

$39.99 until December 15, then $79.99 — the same book either way, and 30 days to change your mind. The chapter you just read stays free whatever you decide; nothing on this page moves behind a paywall later.

Regular price $79.99, now $39.99Save $40

One time · Lifetime updates · No subscription

Founding price. It goes to $79.99 on December 15 and stays there. Buy before then and you keep every future update at no extra cost — including the five formats still marked as hypotheses.

Anatomy of a Data Reel

  • reel-starter — the project — runs with two commands; renders a 47.0s reel at 2160×3840 · 60fps
  • The chart library — 15 families, one shared contract, staged reveals built in
  • One worked episode — 6 beats, its dataset, its script, its beat clock — and its bugs
  • 12 chapters — 33,013 words, every number measured. The last three build a reel
  • 7 format blueprints — 2 proven in full, 5 marked as hypotheses
  • 10 fill-in sheets — 386 tick-boxes, 279 blanks, all persisting
  • Lifetime updates — text and code, same link, no second purchase

30 days, no questions.

Not a form, not an interrogation. If the project doesn’t run on your machine, or a chapter isn’t delivering, that’s my problem to fix — tell me and take the refund anyway.

  1. Lemon Squeezy takes the payment — card details never touch this site.
  2. You land back here on a page with the link on it. No waiting for email.
  3. The same link is emailed to you. It doesn’t expire, and updates arrive there.

$39.99 once — the founding price until December 15, after which it is $79.99. Lemon Squeezy is the seller of record and handles any VAT or sales tax, so the price above is the price you pay. By buying you agree to the terms and privacy policy.

$0Already published

What’s free, and stays free

  • 41 datasets — 79,924 rows across 64 files
  • 14 articles — one per shipped video
  • Chapter 7 — complete, on this page, no email

The raw material behind every video, with its methodology note. Not a sample of the paid thing — it was free before this existed.

No bragging, just real mistakes and lessons and how to learn from them, which makes it feel trustworthy. The content is deep and detailed, like actual hard-earned knowledge.
Pre-release readerRead a free pre-release copy
Go to the kit

The code

Three decisions you can disagree with

Verbatim from the zip — not a simplified version for the page. The whole of the 98-chart library works the way the first one does.

the contractsrc/viz/core/contract.ts

One shape, 98 charts

Every chart in the library takes the same props, so any chart swaps for any other in a scene. The rule that makes it work is the one that is hard to retrofit: every datum carries a stable key. A scatter point for Kansas City and a bar for Kansas City are the same object in two arrangements — which is why the transition between them is a translation and not a cross-fade.

/** The universal datum. Every field except `key` is *  optional — a bar needs {key, value}, a scatter needs *  {key, x, y} — but both are Datum, so either can *  morph into the other. */export type Datum = {  /** stable identity. Survives across chart types. */  key: string;  x?: number | string;  y?: number;  value?: number;  group?: string;  /** THE subject, drawn in the accent colour. Normally   *  exactly one per chart: more than one accent and the   *  frame no longer has a point. */  accent?: boolean;};
the clocksrc/episode/beats.ts

Measure the read. Don't budget it

The runtime is an output, not an input. Pick 45 seconds first and divide by six, and the day you record the narration every beat is wrong at once — not by a little, and not fixably, because every animation inside a beat is timed against a start that just moved. The cap is a constraint on the script.

// The mistake almost everyone makes: you pick "45// seconds", divide it into six, then record the// narration and discover beat 4 needed nine seconds.// Now every beat is wrong.//// So it goes the other way round://   1. Write the script.//   2. Speak it, and MEASURE each line.//   3. Each beat is its own clip, plus a little air.//   4. The runtime is whatever that adds up to. export const BEAT_SECONDS = [  6.0,  // 1 · the question  7.5,  // 2 · what we measured  8.5,  // 3 · the field  8.0,  // 4 · the obvious answer, and why it's wrong  9.0,  // 5 · the answer  8.0,  // 6 · the finding, and the ask] as const;
the call sitesrc/episode/beats/Beat6Finding.tsx

What using it looks like

No axes to build, no reveal to stage by hand. plotBox() encodes the Reels safe band so the chart cannot land under the caption, and the six reveal stages — frame, scaffold, marks, structure, emphasis, annotation — are timed by the library from one start.

import {ScatterFit, plotBox, PALETTES} from '../../viz'; const BOX = plotBox(PLOT_SHORT); <ScatterFit  frame={frame}  start={start + 0.3}  box={BOX}  data={POINTS}  palette={PALETTES.pink}  xLabel="years of training"  yLabel="chance of going pro"/>

source · codepop/starter/src — verbatim, as shipped in the zip

The machine

An agent writes most of this repo. Nothing ships until a gate says so

Speed was never the hard part. The hard part is knowing, without watching a 47-second render, that the thing you just changed didn’t quietly break a frame three beats later.

CLAUDE.mdthe router
## 2 · viz library = main tool // hand-build an axis, or hand-stage a reveal⊥ hand-build axes. ⊥ hand-stage reveals. - V1: ∀ chart takes VizProps<D> = frame, start, out?, box,      data, labels, palette? ∴ any chart swaps for any other;      morph keyed on Datum.key.- V2: six stages, always this order — frame → scaffold →      marks → structure → emphasis → annotation.- V3: box ∀ chart with plotBox() / panelBox() / rowBox(),      ⊥ raw margins — they encode the Reels safe band.

A map, not a rulebook

One file at the root says where everything is and which single document to open for the task at hand. An agent that reads it doesn’t grep 80 files to find the chart library, and doesn’t hand-build an axis that already exists.

The gates

  • ql-sec83 passed

    webhook signatures, open redirect, CSP, consumer law

  • ql-starter52 passed

    what may leave the building in the paid zip

  • ql-chartsgreen

    safe band, edge clip, label collision

  • ql-layoutgreen

    the 8pt grid, statically

135assertions green

run 2026-09-18 · counted from the run, not from memory

in the boxstarter/src/episode/layout.ts

A comment saying “these must not overlap” is a comment

The worked episode’s vertical budget throws at import time. Make a chart taller without moving the caption and the build stops, rather than shipping a frame with two layers of text on top of each other. Four beats of six collided the first time this was built, and the studio showed none of it.

src/episode/layout.tsships in the zip
for (const [name, plot, caption] of [  ['PLOT_TALL', PLOT_TALL, CAPTION_TALL],  ['PLOT_SHORT', PLOT_SHORT, CAPTION_SHORT],] as const) {  if (bottomOf(plot) > caption) {    throw new Error(      `${name} ends at ${bottomOf(plot)} but captions start ` +      `at ${caption} — the source line would render on top.`,    );  }}

07:00, every day

Four desks. No model in the loop.

A launchd job wakes four desks — six keyless public sources, SEC Form 4 filings, House roll calls, federal contract awards — and each one ships a carousel. The scoring that picks the day’s angle is a σ calculation, not a prompt. Deliberately: a generator that can be surprising can also be wrong in a way no gate catches, and this one runs while nobody is watching.

  • daily

    6 keyless sources

  • insiders

    SEC Form 4

  • floor

    House roll calls

  • contracts

    USAspending

ASK → TEST → ANSWER · 4 carousels a day · zero LLM calls

To be exact about what that means for you: the $39.99 ships the repo and the book — the chart library, the motion layer, the worked episode and its gate. It does not ship the agent setup, the router, the daily engine or the analytics stack. Those are how this studio runs, not a product, and the full exclusion list is further down this page.

source · codepop/CLAUDE.md · qa/ql-sec.mjs and qa/ql-starter.mjs, run 2026-09-18 · codepop/daily/README.md · starter/src/episode/layout.ts

The anatomy

A ranking episode is six slots and a stopwatch.

Not a style. A spec — 2,850 frames at 60fps, six beats, 185 words of voiceover, locked so an episode becomes a spec file plus a dataset instead of a new build every time.

This is the real table out of the format spec, at true scale. One beat takes 38% of the runtime, and the chapter explaining why is the part you’re buying.

47.5s

2,850 frames · 185 words · 6 beats

  1. Hook4.0s16w
  2. The Ruler7.5s29w
  3. The Field6.5s25w
  4. The Examples18.0s70w
  5. The Podium8.0s31w
  6. CTA3.5s14w

Where the format spent its blood

Four episodes of rebuilding one four-second beat.

Measured skip at 3 seconds — lower is better

Twenty-seven points of improvement across four episodes, and views moved with it.

The direction of travel is the whole finding: wide, dense, everything at once, camera moving out. The metro episode showed a few rows racing. The bank episode showed the entire country extruded, every state carrying the mark of the bank it actually uses.

Read those skip numbers as roughly comparable: none of these four was ever pushed to a cold audience, and measured skip inflates when a reel breaks out. These are the least-contaminated readings on the channel.

Both tables above are printed from formats/01-the-ranking.md, one of the seven blueprints. What isn’t here is the analysis around them — why beat four absorbed two beats that were deleted, and the measurement showing every comma costs about half a second of voiceover.

Readers

Five people read it before it went on sale

Each was sent the whole thing free and asked to be honest. Their words are trimmed for length and nothing is reworded — including the two who said it reads long, which is answered underneath rather than left out.

  • It’s an honest, well-argued manual on making data reels that guides you through all the process and its iterations to get the best result, and it knows exactly what it is — failures costed, untested claims labeled as guesses. If you also do analysis and decks, there’s a solid chapter of verification discipline and a handful of chart rules worth lifting.

    Pre-release reader

    Read a free pre-release copy

  • No bragging, just real mistakes and lessons and how to learn from them, which makes it feel trustworthy. The content is deep and detailed, like actual hard-earned knowledge.

    Pre-release reader

    Read a free pre-release copy

  • There’s quite a few amazing concepts. I’ve always been captivated by the reels and I can see why… I’m getting loads of gems. It’s applicable to one of my side projects.

    Pre-release reader

    Read a free pre-release copy

  • The dataset article has great decision-making philosophy without the boringness of a textbook, but still carries a lot of the important information. Note that an immediate solution definitely does not come, as there's lots to unpack — but quite worthwhile.

    Pre-release reader

    Read a free pre-release copy

All four received a free copy in exchange for an honest review. None were paid, and none were shown a draft of what to say.

The most common criticism, before you buy it

Early readers said it reads long — a wall of text with the density turned all the way up. That is fair, and the rebuild into modules with diagrams and inline checklists is underway. Everyone who buys now gets it without paying again.

The line

What you get, and what you don't

Printed in the same words the book uses. The right-hand column is everything that is genuinely not in the box, and I'd rather you knew now than in thirty days.

  • A runnable Remotion project: the frame, the grid, the motion layer, the atmosphere
    My API keys, tokens and environment — you bring your own
  • The chart library — 15 families, one contract, staged reveals built in
    The three GPU families (geo, 3D, iso) — 200 MB of install and four ways to render black
  • One complete worked episode: source, dataset, script, beat clock, and the bugs in it
    The analytics pull, the funnel scripts, the posting automation and the scheduler
  • The build-along — empty folder to rendered file, then to a delivery encode
    The licensed voice model, and any audio at all. The chain is taught; the asset isn’t mine to give
  • The QA tooling: the chart gate, and the contact-sheet sweep that found four real bugs
    My channel’s own analytics data, and anything identifying me
  • How ideas get picked, and the two kill gates that reject most of them
    Photographs, logos and third-party imagery — every one is somebody’s licence
  • Data sourcing routes that are hard to find, and the traps in each
    A guarantee that any of it will work. See the no-guarantee section
There’s quite a few amazing concepts. I’ve always been captivated by the reels and I can see why… I’m getting loads of gems. It’s applicable to one of my side projects.
Pre-release readerRead a free pre-release copy

The rule it holds in every ambiguous case

If it’s a decision or a tool, it’s in; if it’s a credential, an account or somebody else’s licence, it’s out.

You get the code

You get the code. It’s a real project — `npm install`, `npm run render`, and a 47-second reel comes out. One thing that’s on you: Remotion, the renderer underneath it, has its own licence and needs a company licence above a certain size. Nothing you pay me covers that.

Fit

Who it’s for, and who should skip it

The right-hand column has stopped more wrong purchases than anything else written here. It is not modesty and it is not a disclaimer.

Buy it if

  • Someone who wants to make these — data-driven short video — and would rather start from a working project than from an empty folder.

  • Someone making them already who keeps ending up with a chart nobody watches.

  • Someone who wants the decisions as well as the code: what to measure, what to withhold, how long a beat runs, why the hook keeps failing.

  • Someone doing analysis for an audience who needs the honesty apparatus: sensitivity, residualisation, tie handling, the difference between a claim and a measurement.

  • Someone who has been burned by a tool that returned a plausible number without doing the work.

Don’t buy it if

  • Anyone who won’t open a terminal. The project is TypeScript and React, run from a command line. You don’t have to be good at either — the worked episode runs with two commands — but there is no visual editor here and there isn’t going to be one.

  • Anyone looking for growth tactics. There’s one chapter about measurement and it spends most of its length explaining why three things I believed turned out to be false. If you want a list of hooks that go viral, this will annoy you.

  • Anyone who doesn’t have or want a dataset. The whole method is built around one real, nameable dataset per video. Without that, most of this doesn’t apply.

  • Anyone who needs it to work on a platform I haven’t measured. All the retention numbers here come from one channel on one platform. The craft transfers. The specific thresholds might not.

  • Anyone in a hurry. The shortest chapter is about two thousand words and the useful parts are in the details.

The dataset article has great decision-making philosophy without the boringness of a textbook, but still carries a lot of the important information. Note that an immediate solution definitely does not come, as there's lots to unpack — but quite worthwhile.
Pre-release readerRead a free pre-release copy

The disclaimer that matters

This teaches a process. It guarantees no outcome.

Every performance figure here is from one channel, one audience, one format family, and a corpus in the dozens rather than the thousands. Several of the findings are explicitly labelled as association rather than causation, and one whole section is about a rule I believed, published, and then falsified with my own experiment.

Nothing here promises views, subscribers, revenue, or a working business. If you apply all of it perfectly, you’ll have a more rigorous process and a better argument for why your video is the way it is. What the algorithm does with that is not something I can sell you.

The receipts

Fourteen articles that used the method before the book wrote it down

Fourteen published articles, each attached to a shipped video and a public dataset. All free, none behind an email.

Objections

The questions people ask first

Every answer below is drawn from something already written in the book, not from a copywriting exercise.

Before you decide

Chapter 7 is free,
and it’s the honest sample.

It’s the chapter about the things that cost me a day each. If that one isn’t useful to you, nothing behind the price will be either — which is the most reliable way I know to stop someone buying something they’ll regret.

  • $39.99 once, no subscription
  • $79.99 from December 15 — you are buying at the founding price
  • 30 days to change your mind, no questions
  • Lifetime updates at the same address
  • The free chapter stays free either way