Common Pitfalls
The recurring bugs we see in production support tickets — symptom, root cause, and the one-line fix. Skim before you ship.
This is the catalogue of bugs that account for ~80 % of support tickets we get from teams shipping the chart for the first time. Each entry is two lines: the symptom you'd describe in chat, and the line of code that fixes it.
If you're hitting a smoothness problem (frame drops, hover lag, slow pan) check the Performance guide instead — that page is structured as do-this / don't-do-this rather than symptom-first.
Lifecycle
"The chart mounts but nothing renders"
The chart has data but the render loop never started.
const chart = await createChartBridge(canvas, { licenseKey })
chart.setKlines(t, o, h, l, c, v)
chart.start() // ← easy to forget
start() boots the loop. Call it once, after the first data load.
"The chart renders, then freezes after a route change"
The previous instance leaked because destroy() wasn't called on unmount.
// Vue
onBeforeUnmount(() => chart.destroy())
// React
useEffect(() => () => chart.destroy(), [])
Without destroy, the render loop and event listeners stay attached to the (now-detached) canvas. After 2-3 route changes, three render loops compete for the main thread and everything stutters.
"Two charts on the same page double-fire hover events"
You called createChartBridge(sameCanvas) twice. The canvas can host one chart at a time. To swap symbol or timeframe, reuse the same chart instance and call setKlines again — don't re-init.
// ❌
const chart = await createChartBridge(canvas)
// ... later, on symbol change:
const chart = await createChartBridge(canvas) // leaked old instance
// ✓
chart.setKlines(/* new symbol data */)
"After the first render, the chart never updates"
You're holding a stale reference. createChartBridge is async — if your component re-renders while the promise is in flight, the resolved chart may bind to an unmounted canvas.
let cancelled = false
createChartBridge(canvas).then((c) => {
if (cancelled) { c.destroy(); return }
chart = c
chart.setKlines(...)
})
onBeforeUnmount(() => { cancelled = true })
Data
"Candles are 1000× wider than expected"
Timestamps are in milliseconds; the engine expects seconds.
// ❌
chart.appendKline(Date.now(), o, h, l, c, v)
// ✓
chart.appendKline(Date.now() / 1000, o, h, l, c, v)
Every public exchange API returns milliseconds. Divide by 1000 once at parse time.
"Candles overlap or render in the wrong order"
The timestamp array isn't strictly increasing. Some exchange websockets emit duplicates during reconnect, or stream historic + live frames interleaved.
function dedup(rows) {
const seen = new Set()
return rows.filter((r) => seen.has(r.ts) ? false : (seen.add(r.ts), true))
.sort((a, b) => a.ts - b.ts)
}
chart.setKlines(...toArrays(dedup(raw)))
"The viewport jumps to the right whenever I reload"
You called setKlines to reload the same symbol+timeframe. That method resets the viewport intentionally (fresh-load look). For reconnect / re-fetch flows, use the viewport-preserving variant:
chart.setKlinesPreserveViewport(t, o, h, l, c, v)
"Real-time updates don't show up"
Most likely cause #1: you're calling setKlines on every tick. That's the bulk-load method and it resets the dataset.
Most likely cause #2: your last bar is closed but you keep calling updateLastKline for a NEW tick that belongs to a NEW bar. Cross the boundary explicitly with appendKline.
if (tick.ts >= currentBarEnd) {
chart.appendKline(/* opening tick of the new bar */)
currentBarEnd += intervalSeconds
} else {
chart.updateLastKline(/* updated last-bar OHLCV */)
}
"Infinite scroll keeps fetching the same page"
You're firing the fetch on every pan event without rate-limiting. Wrap the fetch in a re-entrancy guard and key it off the oldest loaded timestamp so a stalled request can't trigger a duplicate:
let loading = false
let lastFetchedCursor = null
async function onNearLeftEdge(oldestTs) {
if (loading) return
if (lastFetchedCursor === oldestTs) return // already fetched this page
loading = true
lastFetchedCursor = oldestTs
try {
const older = await fetchOlder(oldestTs)
// Forward via the prepend method documented on /docs/charting-library/data-loading.
prependOlder(older)
} finally { loading = false }
}
See Data Loading for the prepend-older-bars method matching your build.
Orderbook heatmap
"The heatmap is blank but the candles render fine"
The depth matrix wasn't loaded. The heatmap needs its own setHeatmap call — enabling the layer with enableHeatmap(true) only flips the visibility flag.
chart.setHeatmap(matrix, rows, cols, priceMin, priceMax, timeStart, timeEnd)
chart.enableHeatmap(true)
"The heatmap renders but realtime columns don't append"
Either the timestamp of the appended column equals the last column's timestamp (engine dedupes), or the price range you passed to setHeatmap doesn't include the current mid-price (column rendered, just off-canvas vertically).
Quick sanity check: log the price range on init and confirm the latest mid is inside it.
"Heatmap colours look washed out"
The colormap range is calibrated for the densest cell in the loaded snapshot. If you loaded a quiet 1-minute window, then the market got active, the busy cells exceed the calibration max and clip to the brightest colour. Reload the matrix periodically (every few minutes) or call the recalibration helper documented on the Orderbook Heatmap page.
Footprint
"Footprint chart shows empty cells"
Footprint needs trade-level data, not candle-level. Aggregated OHLCV doesn't carry the bid-vs-ask breakdown. Feed the raw trade stream ({ ts, price, qty, isBuyerMaker }) via pushTrades — the engine aggregates into bid/ask cells internally.
"Footprint cells flicker on every tick"
You're feeding trades one at a time inside a tight loop. Batch the trades per bar on your side before forwarding them to the chart (the Footprint Chart page documents the exact intake method for your build):
let buffer = []
ws.on('aggTrade', (t) => buffer.push(t))
setInterval(() => {
if (buffer.length === 0) return
// Forward the batch through the footprint-intake method
// documented on /docs/charting-library/footprint-chart.
pushFootprintBatch(buffer)
buffer = []
}, 100) // 10 Hz is plenty for footprint
Indicators
"RSI line is shifted N bars to the left"
You enabled the indicator before setKlines finished. The indicator computed against the previous (or empty) dataset.
await chart.setKlines(...) // ← await if your wrapper makes it async
chart.enableRsi(true, 14)
setKlines itself is synchronous on the engine side; the await pattern is for your own data-fetch wrapper. Just make sure data is set before indicators turn on.
"Indicator settings dialog doesn't reflect my updates"
updateIndicatorParams(id, partial) writes to local UI state only. If you have a settings dialog that reads from your own framework store, make sure that store is the one driving the dialog — not a snapshot from chart init.
Drawings
"User-drawn trendlines disappear on the next data tick"
The drawings layer is independent of the data layer, but only persists in memory for the lifetime of the chart instance. If you destroy and recreate the chart on a tick (anti-pattern, see Lifecycle above), the drawings go with it.
Persist drawings yourself if the user needs them to survive a reload. Subscribe to the drawing-commit event on the chart instance (documented on Drawing) and round-trip the shape through your own backend:
subscribeDrawingCommit(chart, (d) => saveToYourBackend(d))
// On chart init:
const saved = await fetchYourBackend()
saved.forEach((d) => addDrawingToChart(chart, d))
"Drawings appear at wrong positions after a symbol change"
Drawings are tied to chart coordinates (price × time). Switching symbols invalidates them. Clear them before swapping data — see Drawing for the bulk-clear method, then proceed with the data swap:
function onSymbolChange(newSymbol) {
clearDrawings(chart)
chart.setKlines(/* new symbol data */)
}
Theming
"Dark / light theme doesn't sync with my app"
setTheme('dark' | 'light') is one-shot. Bind it to your framework's theme store:
// Vue
watch(themeStore.mode, (m) => chart.setTheme(m), { immediate: true })
// React
useEffect(() => chart.setTheme(mode), [mode])
"Custom indicator colours don't show in the legend"
Legend colours come from the indicator's params.colour (or equivalent). Update the param, don't recolour the legend DOM:
chart.updateIndicatorParams(rsiId, { lineColour: '#D4A60A' })
Tooltips
"Tooltip never hides"
You didn't handle the null payload. The chart emits null once when the cursor leaves the canvas — that's your signal to hide.
chart.onTooltip((raw, sx, sy) => {
if (raw === null) { hide(); return }
...
})
"Tooltip is clipped at the chart edge"
Render it on <body>, not inside the chart's container.
<!-- Vue -->
<Teleport to="body"><div class="tooltip">...</div></Teleport>
// React
createPortal(<div className="tooltip">...</div>, document.body)
See Events & Tooltips for the full positioning recipe with edge-flipping.
Licensing
"Chart renders fine in dev, shows a watermark in production"
The trial license is fingerprinted to localhost. Production needs a real key — set it via licenseKey on createChartBridge. See the licensing section for the flow.
"Trial expired badge persists after I added a paid key"
Hard-refresh the page or clear localStorage once. The license check runs at chart-init time, not every frame.
Next
- Performance & Smoothness — the do/don't list specifically for frame timing.
- Getting Started — back to basics if anything above felt out of context.