2026-07-14·2 min read

One file, two renderers

field-notesfolium

I wanted a chart card. Bar, line, pie, donut, the kind of thing every React app reaches for a library to draw. Recharts was the obvious pick, a few props and you have an SVG. Then I remembered what Export does: it writes the whole board into one .html file, a single document someone can double-click with no server and no install. Recharts lives inside the React tree. The exported file has no React tree. The chart would render perfectly in the app and show nothing at all in the one artifact Folium promises people — a whole board, one file.

That’s the constraint in its plainest form. The export in src/export/html.ts is a second renderer: a plain-JS viewer with no framework and no build step, and it gets a vote on every card type before that card ships. A React-only library fails that vote instantly.

So the chart renderer in src/charts/renderChart.ts is hand-rolled: pure functions that walk rows of numbers and emit <svg> markup directly. The comment on it says the function “MUST stay self-contained — embedded into the HTML export via .toString()”, and that is the actual mechanism. The decision I’d defend hardest sits on one line of html.ts:

var renderChartSvg = ${renderChartSvg.toString()};

The app’s own function is stringified and dropped into the exported file as live JavaScript. The app and the export don’t run similar code; they run the same function, serialized twice, so they cannot drift apart. The whiteboard ink outlines and the elbow line routing get the same treatment, pulled in from src/model/inkOutline.ts and src/model/lineRoute.ts. The chart renderer has its own test file, src/charts/renderChart.test.ts, checking negative values, NaN, a single data point, label text that needs escaping, because once the function is serialized into someone else’s file I can’t patch it after the fact.

Parity is a standing bill

The viewer keeps needing the same polish the app gets. When boards gained drag-to-pan, the export’s script had to learn it separately, since there is no bundler to share a module through. The export CSS carries a comment that it mirrors .app-canvas[data-board-bg] in src/styles/global.css, a note-to-self that two stylesheets have to agree by hand. None of this is glamorous. It’s the price of a board that can leave my machine as one file and still look like the board I built, and I keep paying it because the alternative is an export that quietly falls behind the app until the day someone opens a file and the chart just isn’t there.