16

Our desk needed an intraday candlestick view that stays at 60fps when someone scrolls a full year of 1-minute bars. The obvious SVG version from the model died around 8k nodes, and the first canvas attempt redrew every frame on mousemove.

What fixed it was forcing a separation between a static price layer and a cheap overlay layer for the crosshair, plus an explicit rule that the y-scale is log for equities and linear for spreads. I also made it justify the wick vs body rendering order so thin bodies don't disappear under wicks.

How are people handling the gap between sessions? A continuous time axis leaves ugly overnight gaps, but an ordinal index axis makes the time ticks lie.

THE PROMPT
Build a candlestick chart on HTML canvas (no charting library) for arrays of {t, o, h, l, c}. Requirements:

Rendering: two stacked <canvas> elements sharing one coordinate space. Layer 1 (price) redraws ONLY on data change, zoom, or pan. Layer 2 (crosshair + hovered OHLC readout) redraws on pointer move. Never redraw layer 1 on mousemove.

Scales: x is a band scale over the bar index (not wall-clock time) so overnight gaps collapse; render time tick labels from the underlying t of the bars at those indices. y is configurable: log for prices, linear for spreads (opts.scale). devicePixelRatio-aware so it's crisp on retina.

Candle rules: draw the wick first (1px, then the body on top) so narrow bodies stay visible; up bars var --up #16a34a, down var --down #dc2626, doji as a 1px horizontal tick. Body width = 70% of band, min 1px.

Perf budget: must hold 60fps panning 50,000 bars; only draw candles whose x is within the viewport plus a 20-bar margin. Include a fps meter you can toggle. No per-frame allocations in the draw loop.

Deliver the code plus a note on which parts are the hot path.
'no per-frame allocations in the draw loop' should be pinned to the wall of every canvas dev. GC pauses are the silent fps killer.async_annie 2 months ago
add a comment

2 Answers

7

The two-layer split is exactly right. To push it further I had the model precompute a typed Float32Array of x/o/h/l/c pixel positions on zoom/pan and just iterate that in the draw loop, so there's zero object access per bar. That took my worst-case frame from 22ms to 6ms on the same 50k set.

THE PROMPT
On every zoom/pan, project all visible bars into a single Float32Array laid out as [x, yOpen, yHigh, yLow, yClose] * n. The per-frame draw loop reads only from that array; forbid touching the source objects inside requestAnimationFrame.
10

On the session-gap question: band scale over index is the standard answer, but add a thin vertical rule and a subtle background tint at each session boundary so users still perceive 'a day passed here'. Without it people misread a 3-day weekend as one flat stretch.

Your Answer