Troubleshooting
Copyable checks and fixes for common chart integration failures.
These failure modes are generated from the same versioned knowledge used by
diagnose_chart. Begin with the symptom that most closely matches the
application, apply the diagnostic check, and keep only fixes that preserve the
documented lifecycle and typed-array ownership contract.
If no entry matches, inspect the relevant symbol in the
nested API reference or ask the local MCP server to run a
focused diagnosis. Reduce the reproduction to renderMode: "main" and
animated: false only as a diagnostic step, not as an automatic production
configuration.
Symptom index
The canvas is blank or has zero size
Applies to: line, stock.
Symptom. The chart initializes without a visible error, but no plot appears or the canvas measures zero pixels high.
Likely cause. The canvas host did not have a concrete height when the renderer initialized.
Check
- Inspect the host and canvas with getBoundingClientRect(); both width and height must be greater than zero.
- Check whether a flex or grid ancestor collapsed the chart row.
Fix
- Give the host a concrete height or min-height before initialize() runs.
- Let the canvas fill that host instead of relying on intrinsic canvas dimensions.
Give the chart a measurable host
.chart-host {
position: relative;
width: 100%;
height: clamp(20rem, 55vh, 34rem);
}
.chart-host > canvas {
display: block;
width: 100%;
height: 100%;
}Data is installed before the chart is ready
Applies to: line, stock.
Symptom. The first dataset does not appear, while a later update or remount works.
Likely cause. A vanilla integration called a data method before initialize() completed, or initialized the same canvas more than once.
Check
- Verify that initialize() is awaited exactly once for each chart instance.
- If a framework owns the view, prefer the official adapter so mount and unmount ordering is handled for you.
Fix
- Await initialize() before calling setData() or setMultiSeriesData().
- Destroy the old chart before reusing its canvas for another instance.
Use the vanilla lifecycle in order
const chart = new LineChart(canvas, { renderMode: "auto" });
await chart.initialize();
chart.setData(data);
// Call this when the view is permanently removed.
export function disposeChart() {
chart.destroy();
}Worker rendering falls back to the main thread
Applies to: line, stock.
Symptom. A chart works but resolves to main-thread rendering, or worker construction fails only in production.
Likely cause. Worker, OffscreenCanvas, transferControlToOffscreen, or the deployed Content Security Policy prevented the worker renderer from starting.
Check
- Use setStatsCallback() temporarily and inspect stats.renderMode.
- Check the browser console and worker-src Content Security Policy directive.
Fix
- Keep renderMode set to auto unless forcing a mode is necessary for diagnostics.
- Allow the application-generated worker URL in worker-src and keep the main-thread fallback operational.
Confirm the resolved renderer
chart.setStatsCallback((stats) => {
console.info("Sixtyfold renderer:", stats.renderMode);
chart.setStatsCallback(null);
});Typed arrays become detached after setData
Applies to: line, stock.
Symptom. A source typed array has byteLength 0 or can no longer be read after data was passed to a worker-backed chart.
Likely cause. Bulk chart data is transferred to the renderer in worker mode; transferred ArrayBuffers leave the caller.
Check
- Inspect byteLength immediately before and after the bulk data call.
- Confirm whether another subsystem genuinely needs to retain a readable copy.
Fix
- Treat the bulk data call as an ownership transfer.
- Clone only the columns that another subsystem must retain; avoid duplicating multi-million-value datasets by default.
Retain an intentional application copy
const retainedX = sourceX.slice();
const retainedY = sourceY.slice();
chart.setData({
x: sourceX,
y: sourceY,
length: sourceX.length,
});
// retainedX and retainedY remain readable.The time axis is compressed, empty, or far in the future
Applies to: line, stock.
Symptom. Ticks show implausible dates, the visible range collapses, or panning behaves unpredictably.
Likely cause. The X column used seconds or strings instead of finite ordered epoch milliseconds, or observations were not sorted.
Check
- Inspect the first and last timestamps and convert them with new Date(value).
- Verify every timestamp is finite and non-decreasing.
Fix
- Convert epoch seconds to milliseconds before creating the Float64Array.
- Parse ISO dates once during ingestion and sort complete observations before splitting them into columns.
Normalize timestamps during ingestion
const timestamp = Float64Array.from(rows, (row) => {
const value = Date.parse(row.recordedAt);
if (!Number.isFinite(value)) throw new TypeError("Invalid recordedAt value");
return value;
});Columns have different lengths or invalid numeric values
Applies to: line, stock.
Symptom. The chart rejects a dataset, truncates unexpectedly, or renders values under the wrong timestamps.
Likely cause. Columnar data was created from separate filters or mappings, so the time and value columns no longer describe the same observations.
Check
- Compare every column length before calling the chart.
- Validate numeric conversion before transferring buffers.
Fix
- Filter and sort complete source observations first, then split them into aligned typed columns.
- Use explicit NaN only for a documented line-series gap; OHLCV values must remain valid candles.
Reject misaligned columns before transfer
const lengths = [timestamp.length, open.length, high.length, low.length, close.length, volume.length];
if (!lengths.every((length) => length === lengths[0])) {
throw new RangeError(`OHLCV columns are misaligned: ${lengths.join(", ")}`);
}SSR or hydration accesses browser-only chart APIs
Applies to: line, stock.
Symptom. A server render throws because window, document, Worker, or HTMLCanvasElement is unavailable, or hydration produces a different tree.
Likely cause. The interactive browser chart was constructed during server evaluation instead of after the client mounted.
Check
- Find chart construction at module scope or inside a server component.
- Separate server image generation through @sixtyfold/ssr from browser interaction.
Fix
- Use the official framework adapter inside a client-owned component.
- Render a stable host during SSR and initialize the interactive chart only after mount.
Keep a Next.js chart behind the client boundary
"use client";
import type { MultiSeriesData, TimeSeriesData } from "@sixtyfold/core";
import { SixtyfoldLineChart } from "@sixtyfold/react/line";
type LineData = TimeSeriesData | MultiSeriesData;
export function ChartPanel({ data }: { data: LineData }) {
return <SixtyfoldLineChart data={data} options={{ renderMode: "auto" }} />;
}Workers or memory remain after navigation
Applies to: line, stock.
Symptom. Memory grows after repeatedly opening the view, or old charts continue rendering after navigation.
Likely cause. A vanilla chart or temporary data worker was not destroyed when its owning view was removed.
Check
- Record a browser heap snapshot before and after several mount/unmount cycles.
- Look for retained chart instances, workers, observers, or decoded source arrays.
Fix
- Call destroy() exactly once for every vanilla chart instance.
- Terminate temporary fetch/decode workers after their final transfer; official framework adapters destroy chart workers automatically.
Release chart and ingestion workers
return () => {
dataWorker?.terminate();
chart.destroy();
};Interaction is slow or drops frames
Applies to: line, stock.
Symptom. Zooming, panning, initial decode, or streaming updates block visibly or miss the target frame rate.
Likely cause. The main thread is decoding or reshaping too much data, the chart resolved to the fallback renderer, or the visible workload includes more drawing passes than the target device can sustain.
Check
- Inspect stats.renderMode, frameTime, visible work, and LOD readiness during the actual interaction sequence.
- Profile fetch, decode, conversion, transfer, and rendering separately.
Fix
- Use renderMode auto, prepare columnar typed arrays in a dedicated data worker, transfer once, and terminate that ingestion worker.
- Benchmark representative browsers, DPRs, series counts, and gestures; 60 FPS is a design target rather than a device-independent guarantee.
Measure the active rendering path
chart.setStatsCallback((stats) => {
console.table({
mode: stats.renderMode,
fps: stats.fps,
frameTime: stats.frameTime,
ready: stats.lodReady,
});
}, { intervalMs: 250 });Line detail changes abruptly after zoom settles
Applies to: line.
Symptom. The viewport animates smoothly but the line texture changes again at the end of the gesture.
Likely cause. Presentation density or grid rebasing is too aggressive for the workload, or application code replaces data or appearance after animation completion.
Check
- Replay the same viewport sequence while observing presentation density, grid delta, and query visits.
- Check for application-side setData(), setAppearance(), or viewport restoration after the animation settles.
Fix
- Start with adaptive mode, density 0.75, rebaseRatio 1.25, and quantizationStep 0.25.
- Use setLODOptions() to tune one coefficient at a time; use density 0.25 for expensive range fills or exceptionally large multi-series work.
Tune presentation without recreating the chart
chart.setLODOptions({
mode: "adaptive",
density: 0.75,
rebaseRatio: 1.25,
quantizationStep: 0.25,
});A line connects across missing observations
Applies to: line.
Symptom. A line visually crosses an interval where no measurement exists.
Likely cause. Missing observations were removed or replaced with zero instead of being represented as an aligned NaN gap.
Check
- Confirm the shared X column includes the missing interval when a visible break is required.
- Inspect whether ingestion converted null values to zero.
Fix
- Keep aligned X positions and encode missing line values as Number.NaN.
- Do not synthesize OHLCV candles or connect market closures; stock time-scale behavior is driven by observed candles.
Represent a real line-series gap
const data = {
x: new Float64Array([0, 1, 2, 3]),
y: new Float64Array([12, Number.NaN, Number.NaN, 18]),
length: 4,
};