To sync crosshair/tooltip x across multiple charts, connect their ChartGPUInstances.
You can do this:
- manually with
connectCharts(...), or - with the React hook
useConnectCharts(...)(recommended in React apps)
Both accept an optional ChartSyncOptions parameter to control what is synced.
Related:
import { useMemo, useState } from 'react';
import { ChartGPU, useConnectCharts } from 'chartgpu-react';
import type { ChartGPUInstance, ChartGPUOptions } from 'chartgpu-react';
export function SyncedCharts() {
const [a, setA] = useState<ChartGPUInstance | null>(null);
const [b, setB] = useState<ChartGPUInstance | null>(null);
useConnectCharts([a, b]);
const optionsA: ChartGPUOptions = useMemo(
() => ({
series: [{ type: 'line', data: [{ x: 0, y: 1 }, { x: 1, y: 2 }] }],
xAxis: { type: 'value' },
yAxis: { type: 'value' },
tooltip: { show: true, trigger: 'axis' },
}),
[]
);
const optionsB: ChartGPUOptions = useMemo(
() => ({
series: [{ type: 'line', data: [{ x: 0, y: 3 }, { x: 1, y: 1 }] }],
xAxis: { type: 'value' },
yAxis: { type: 'value' },
tooltip: { show: true, trigger: 'axis' },
}),
[]
);
return (
<>
<ChartGPU options={optionsA} onReady={setA} style={{ height: 220 }} theme="dark" />
<div style={{ height: 12 }} />
<ChartGPU options={optionsB} onReady={setB} style={{ height: 220 }} theme="dark" />
</>
);
}To also sync zoom/pan across charts, pass syncOptions:
useConnectCharts([a, b], { syncZoom: true });This keeps both crosshair and zoom range in sync. To sync only zoom:
useConnectCharts([a, b], { syncCrosshair: false, syncZoom: true });connectCharts is a helper from the peer dependency @chartgpu/chartgpu. chartgpu-react re-exports it for convenience:
import { connectCharts } from 'chartgpu-react';Manual wiring example:
import { useEffect, useMemo, useState } from 'react';
import { ChartGPU, connectCharts } from 'chartgpu-react';
import type { ChartGPUInstance, ChartGPUOptions } from 'chartgpu-react';
export function ManualSync() {
const [a, setA] = useState<ChartGPUInstance | null>(null);
const [b, setB] = useState<ChartGPUInstance | null>(null);
useEffect(() => {
if (!a || a.disposed) return;
if (!b || b.disposed) return;
// Pass syncOptions as the second argument (optional)
const disconnect = connectCharts([a, b], { syncZoom: true });
return () => disconnect();
}, [a, b]);
const options: ChartGPUOptions = useMemo(
() => ({
series: [{ type: 'line', data: [{ x: 0, y: 0 }, { x: 1, y: 1 }] }],
xAxis: { type: 'value' },
yAxis: { type: 'value' },
tooltip: { show: true, trigger: 'axis' },
}),
[]
);
return (
<>
<ChartGPU options={options} onReady={setA} style={{ height: 220 }} theme="dark" />
<div style={{ height: 12 }} />
<ChartGPU options={options} onReady={setB} style={{ height: 220 }} theme="dark" />
</>
);
}type ChartSyncOptions = Readonly<{
syncCrosshair?: boolean; // default true
syncZoom?: boolean; // default false
}>;syncCrosshair(defaulttrue): sync crosshair + tooltip x across charts.syncZoom(defaultfalse): sync zoom/pan range across charts.
For multi-chart dashboards, combine useGPUContext (shared GPU resources) with useConnectCharts (synced interaction). This avoids duplicate GPU device allocation and keeps crosshair/zoom in sync across all charts.
See also: useGPUContext hook docs
import { useMemo, useState } from 'react';
import { ChartGPU, useGPUContext, useConnectCharts } from 'chartgpu-react';
import type { ChartGPUInstance, ChartGPUOptions } from 'chartgpu-react';
export function SyncedDashboard() {
const { adapter, device, pipelineCache, isReady, error } = useGPUContext();
const [chartA, setChartA] = useState<ChartGPUInstance | null>(null);
const [chartB, setChartB] = useState<ChartGPUInstance | null>(null);
const [chartC, setChartC] = useState<ChartGPUInstance | null>(null);
// Sync crosshair and zoom across all three charts
useConnectCharts([chartA, chartB, chartC], { syncZoom: true });
const optionsA: ChartGPUOptions = useMemo(
() => ({
series: [{ type: 'line', data: [{ x: 0, y: 1 }, { x: 1, y: 3 }] }],
xAxis: { type: 'value' },
yAxis: { type: 'value' },
tooltip: { show: true, trigger: 'axis' },
dataZoom: { enabled: true },
}),
[]
);
const optionsB: ChartGPUOptions = useMemo(
() => ({
series: [{ type: 'bar', data: [{ x: 0, y: 5 }, { x: 1, y: 2 }] }],
xAxis: { type: 'value' },
yAxis: { type: 'value' },
tooltip: { show: true, trigger: 'axis' },
dataZoom: { enabled: true },
}),
[]
);
const optionsC: ChartGPUOptions = useMemo(
() => ({
series: [{ type: 'line', data: [{ x: 0, y: 4 }, { x: 1, y: 1 }] }],
xAxis: { type: 'value' },
yAxis: { type: 'value' },
tooltip: { show: true, trigger: 'axis' },
dataZoom: { enabled: true },
}),
[]
);
if (error) return <div>WebGPU not supported: {error.message}</div>;
if (!isReady) return <div>Initializing GPU...</div>;
const gpuContext = { adapter: adapter!, device: device!, pipelineCache: pipelineCache! };
return (
<>
<ChartGPU options={optionsA} gpuContext={gpuContext} onReady={setChartA} style={{ height: 200 }} theme="dark" />
<div style={{ height: 8 }} />
<ChartGPU options={optionsB} gpuContext={gpuContext} onReady={setChartB} style={{ height: 200 }} theme="dark" />
<div style={{ height: 8 }} />
<ChartGPU options={optionsC} gpuContext={gpuContext} onReady={setChartC} style={{ height: 200 }} theme="dark" />
</>
);
}Key points:
useGPUContext()runs once in the parent and shares a singleGPUDevice+PipelineCacheacross all charts, reducing shader compilation overhead.useConnectChartskeeps crosshair and zoom in sync. It only activates once all chart instances are ready.- The
gpuContextprop is init-only on each<ChartGPU>-- it is read once during chart creation and cannot be changed after mount.
- Always disconnect on cleanup to avoid leaking listeners.
- Only connect charts that are initialized and not disposed.
- When
syncZoomis enabled, all connected charts should have compatibledataZoomconfigs for best results.