diff --git a/.gitignore b/.gitignore index 4694164..a3835c7 100644 --- a/.gitignore +++ b/.gitignore @@ -33,3 +33,10 @@ coverage *.swp *.swo *~ + + +# Claude +.claude +.agents +CLAUDE.md +skills-lock.json \ No newline at end of file diff --git a/docs/API.md b/docs/API.md index e27419d..d83292e 100644 --- a/docs/API.md +++ b/docs/API.md @@ -4,7 +4,7 @@ This package is a **React wrapper** around the `@chartgpu/chartgpu` core library - A React component (`ChartGPU`) with lifecycle + resize management - A small imperative ref API (`ChartGPUHandle`) -- Two hooks (`useChartGPU`, `useConnectCharts`) +- Three hooks (`useChartGPU`, `useGPUContext`, `useConnectCharts`) - Convenience re-exports of a few core helpers + types For an LLM-oriented navigation entrypoint, see [`docs/api/llm-context.md`](./api/llm-context.md). @@ -19,6 +19,7 @@ For an LLM-oriented navigation entrypoint, see [`docs/api/llm-context.md`](./api ### Hooks - **`useChartGPU(containerRef, options, gpuContext?)`** — create/manage an instance imperatively (3rd param optional shared context; init-only) +- **`useGPUContext()`** — create a shared `GPUAdapter` + `GPUDevice` + `PipelineCache` for multi-chart dashboards; see [`docs/api/hooks.md#usegpucontext`](./api/hooks.md#usegpucontext) - **`useConnectCharts([chartA, chartB, ...], syncOptions?)`** — keep crosshair/interaction-x in sync (optionally sync zoom) See [`docs/api/hooks.md`](./api/hooks.md). diff --git a/docs/api/hooks.md b/docs/api/hooks.md index 01c99cc..7964552 100644 --- a/docs/api/hooks.md +++ b/docs/api/hooks.md @@ -1,8 +1,9 @@ # Hooks -This package provides two hooks: +This package provides three hooks: - `useChartGPU(containerRef, options, gpuContext?)` — create/manage a `ChartGPUInstance` (3rd param optional shared context; init-only) +- `useGPUContext()` — create a shared `GPUAdapter` + `GPUDevice` + `PipelineCache` for multi-chart dashboards - `useConnectCharts(charts, syncOptions?)` — connect instances for synced crosshair/interaction-x (and optionally zoom) Related: @@ -84,6 +85,100 @@ export function HookChart() { } ``` +## `useGPUContext()` + +Creates a shared `GPUAdapter`, `GPUDevice`, and `PipelineCache` for multi-chart dashboards. Call this once in a parent component and pass the result to each `` (or to `useChartGPU`'s third argument). This avoids each chart requesting its own GPU device and compiling duplicate shader pipelines. + +### Import + +```ts +import { useGPUContext } from 'chartgpu-react'; +import type { UseGPUContextResult } from 'chartgpu-react'; +``` + +### Signature + +```ts +function useGPUContext(): { + adapter: GPUAdapter | null; + device: GPUDevice | null; + pipelineCache: PipelineCache | null; + isReady: boolean; + error: Error | null; +} +``` + +### Behavior + +- On mount, requests a `GPUAdapter` (high-performance preference) and `GPUDevice`, then creates a `PipelineCache`. +- All fields are `null` until initialization completes. `isReady` becomes `true` once both `adapter` and `device` are available. +- If WebGPU is not supported or adapter/device acquisition fails, `error` is set and other fields remain `null`. +- Safe in React 18 StrictMode dev (uses a ref guard to prevent double-initialization). +- Initialization runs once on mount and cannot be re-triggered. + +### Usage with `` + +The `gpuContext` prop on `` accepts `{ adapter, device, pipelineCache }` which maps to the `ChartGPUCreateContext` type. This prop is **init-only** -- it is captured in a `useRef` at mount and only read during `ChartGPU.create(...)`. Changing it after mount has no effect. + +### Example + +```tsx +import { useMemo, useState } from 'react'; +import { ChartGPU, useGPUContext } from 'chartgpu-react'; +import type { ChartGPUInstance, ChartGPUOptions } from 'chartgpu-react'; + +export function Dashboard() { + const { adapter, device, pipelineCache, isReady, error } = useGPUContext(); + + const [chartA, setChartA] = useState(null); + const [chartB, setChartB] = useState(null); + + const optionsA: ChartGPUOptions = useMemo( + () => ({ + series: [{ type: 'line', data: [{ x: 0, y: 1 }, { x: 1, y: 3 }] }], + xAxis: { type: 'value' }, + yAxis: { type: 'value' }, + }), + [] + ); + + const optionsB: ChartGPUOptions = useMemo( + () => ({ + series: [{ type: 'bar', data: [{ x: 0, y: 5 }, { x: 1, y: 2 }] }], + xAxis: { type: 'value' }, + yAxis: { type: 'value' }, + }), + [] + ); + + if (error) return
WebGPU not supported: {error.message}
; + if (!isReady) return
Initializing GPU...
; + + // After the isReady check, adapter/device/pipelineCache are non-null. + const gpuContext = { adapter: adapter!, device: device!, pipelineCache: pipelineCache! }; + + return ( + <> + +
+ + + ); +} +``` + ## `useConnectCharts(charts, syncOptions?)` Connects multiple `ChartGPUInstance`s so they share interaction state (crosshair/tooltip x). Optionally syncs zoom/pan across charts. diff --git a/docs/api/llm-context.md b/docs/api/llm-context.md index 379edfb..41a48dc 100644 --- a/docs/api/llm-context.md +++ b/docs/api/llm-context.md @@ -48,6 +48,7 @@ The goal of this `llm-context.md` file is to give Context7 (and other LLM toolin - `docs/api/chartgpu-handle.md` - Source: `src/types.ts`, `src/ChartGPU.tsx` - **Multi-chart dashboards (shared GPU device + pipeline cache)** + - [`docs/api/hooks.md#usegpucontext`](./hooks.md#usegpucontext) - Source: `src/useGPUContext.ts`, `src/types.ts`, `src/ChartGPU.tsx`, `src/useChartGPU.ts` - **Hooks (`useChartGPU`, `useConnectCharts`)** - `docs/api/hooks.md` diff --git a/docs/recipes/chart-sync.md b/docs/recipes/chart-sync.md index 342de60..dab0058 100644 --- a/docs/recipes/chart-sync.md +++ b/docs/recipes/chart-sync.md @@ -131,6 +131,83 @@ type ChartSyncOptions = Readonly<{ - **`syncCrosshair`** (default `true`): sync crosshair + tooltip x across charts. - **`syncZoom`** (default `false`): sync zoom/pan range across charts. +## Complete dashboard: shared GPU + synced interaction + +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](../api/hooks.md#usegpucontext) + +```tsx +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(null); + const [chartB, setChartB] = useState(null); + const [chartC, setChartC] = useState(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
WebGPU not supported: {error.message}
; + if (!isReady) return
Initializing GPU...
; + + const gpuContext = { adapter: adapter!, device: device!, pipelineCache: pipelineCache! }; + + return ( + <> + +
+ +
+ + + ); +} +``` + +Key points: + +- `useGPUContext()` runs once in the parent and shares a single `GPUDevice` + `PipelineCache` across all charts, reducing shader compilation overhead. +- `useConnectCharts` keeps crosshair and zoom in sync. It only activates once all chart instances are ready. +- The `gpuContext` prop is **init-only** on each `` -- it is read once during chart creation and cannot be changed after mount. + ## Notes - Always disconnect on cleanup to avoid leaking listeners. diff --git a/examples/main.tsx b/examples/main.tsx index ee8af00..9be2a56 100644 --- a/examples/main.tsx +++ b/examples/main.tsx @@ -1,6 +1,6 @@ import React, { useEffect, useMemo, useRef, useState } from 'react'; import { createRoot } from 'react-dom/client'; -import { ChartGPU, connectCharts, createAnnotationAuthoring, useGPUContext } from '../src'; +import { ChartGPU, connectCharts, createAnnotationAuthoring, useGPUContext, useConnectCharts } from '../src'; import type { ChartGPUCrosshairMovePayload, ChartGPUCreateContext, @@ -167,6 +167,281 @@ function ConnectedChartsExample() { ); } +function SyncedDashboardExample() { + const { adapter, device, pipelineCache, isReady, error } = useGPUContext(); + + const [chartA, setChartA] = useState(null); + const [chartB, setChartB] = useState(null); + const [chartC, setChartC] = useState(null); + + // Sync crosshair and zoom across all three charts + useConnectCharts([chartA, chartB, chartC], { syncZoom: true }); + + const optionsA: ChartGPUOptions = useMemo( + () => ({ + series: [ + { + type: 'line', + name: 'Series A', + data: generateLineData(500, 0.5), + lineStyle: { width: 2, color: '#4facfe' }, + }, + ], + xAxis: { type: 'value' }, + yAxis: { type: 'value' }, + tooltip: { show: true, trigger: 'axis' }, + dataZoom: [{ type: 'inside' }], + grid: { left: 60, right: 40, top: 40, bottom: 30 }, + }), + [] + ); + + const optionsB: ChartGPUOptions = useMemo( + () => ({ + series: [ + { + type: 'line', + name: 'Series B', + data: generateLineData(500, 2.1), + lineStyle: { width: 2, color: '#f093fb' }, + areaStyle: { color: 'rgba(240, 147, 251, 0.12)' }, + }, + ], + xAxis: { type: 'value' }, + yAxis: { type: 'value' }, + tooltip: { show: true, trigger: 'axis' }, + dataZoom: [{ type: 'inside' }], + grid: { left: 60, right: 40, top: 40, bottom: 30 }, + }), + [] + ); + + const optionsC: ChartGPUOptions = useMemo( + () => ({ + series: [ + { + type: 'line', + name: 'Series C', + data: generateLineData(500, 4.0), + lineStyle: { width: 2, color: '#40d17c' }, + }, + ], + xAxis: { type: 'value' }, + yAxis: { type: 'value' }, + tooltip: { show: true, trigger: 'axis' }, + dataZoom: [{ type: 'inside' }], + grid: { left: 60, right: 40, top: 40, bottom: 30 }, + }), + [] + ); + + if (error) return ( +
+

Synced dashboard (shared GPU + useConnectCharts)

+
+ WebGPU not supported: {error.message} +
+
+ ); + + if (!isReady) return ( +
+

Synced dashboard (shared GPU + useConnectCharts)

+
Initializing GPU...
+
+ ); + + const gpuContext = { adapter: adapter!, device: device!, pipelineCache: pipelineCache! }; + + return ( +
+

Synced dashboard (shared GPU + useConnectCharts)

+ +
+ Features: useGPUContext() + useConnectCharts() +{' '} + gpuContext prop +
+ Try it: Move crosshair or zoom on any chart — all three stay in sync +
+ +
+ +
+ +
+ +
+
+ ); +} + +function StreamingSyncedDashboardExample() { + const { adapter, device, pipelineCache, isReady, error } = useGPUContext(); + + const [chartA, setChartA] = useState(null); + const [chartB, setChartB] = useState(null); + const [chartC, setChartC] = useState(null); + + const refA = useRef(null); + const refB = useRef(null); + const refC = useRef(null); + + // Sync crosshair and zoom across all three charts + useConnectCharts([chartA, chartB, chartC], { syncZoom: true }); + + const optionsA: ChartGPUOptions = useMemo( + () => ({ + autoScroll: true, + series: [ + { + type: 'line', + name: 'latency(ms)', + data: generateLineData(200, 0.1), + lineStyle: { width: 2, color: '#4facfe' }, + }, + ], + xAxis: { type: 'value' }, + yAxis: { type: 'value' }, + tooltip: { show: true, trigger: 'axis' }, + dataZoom: [{ type: 'inside', start: 70, end: 100 }], + grid: { left: 60, right: 40, top: 40, bottom: 30 }, + }), + [] + ); + + const optionsB: ChartGPUOptions = useMemo( + () => ({ + autoScroll: true, + series: [ + { + type: 'line', + name: 'throughput', + data: generateLineData(200, 1.7), + lineStyle: { width: 2, color: '#f093fb' }, + areaStyle: { color: 'rgba(240, 147, 251, 0.12)' }, + }, + ], + xAxis: { type: 'value' }, + yAxis: { type: 'value' }, + tooltip: { show: true, trigger: 'axis' }, + dataZoom: [{ type: 'inside', start: 70, end: 100 }], + grid: { left: 60, right: 40, top: 40, bottom: 30 }, + }), + [] + ); + + const optionsC: ChartGPUOptions = useMemo( + () => ({ + autoScroll: true, + series: [ + { + type: 'line', + name: 'errors', + data: generateLineData(200, 3.2), + lineStyle: { width: 2, color: '#ff6b6b' }, + }, + ], + xAxis: { type: 'value' }, + yAxis: { type: 'value' }, + tooltip: { show: true, trigger: 'axis' }, + dataZoom: [{ type: 'inside', start: 70, end: 100 }], + grid: { left: 60, right: 40, top: 40, bottom: 30 }, + }), + [] + ); + + // Stream data into all three charts via refs + const xRef = useRef(200); + useEffect(() => { + const timer = window.setInterval(() => { + const x = xRef.current++; + refA.current?.appendData(0, [{ x, y: 40 + Math.sin(x * 0.03) * 25 + Math.random() * 5 }]); + refB.current?.appendData(0, [{ x, y: 55 + Math.cos(x * 0.02) * 22 + Math.random() * 6 }]); + refC.current?.appendData(0, [{ x, y: 10 + Math.abs(Math.sin(x * 0.05)) * 8 + Math.random() * 3 }]); + }, 120); + + return () => window.clearInterval(timer); + }, []); + + if (error) return ( +
+

Streaming synced dashboard (shared GPU + useConnectCharts + appendData)

+
+ WebGPU not supported: {error.message} +
+
+ ); + + if (!isReady) return ( +
+

Streaming synced dashboard (shared GPU + useConnectCharts + appendData)

+
Initializing GPU...
+
+ ); + + const gpuContext = { adapter: adapter!, device: device!, pipelineCache: pipelineCache! }; + + return ( +
+

Streaming synced dashboard (shared GPU + useConnectCharts + appendData)

+ +
+ Features: useGPUContext() + useConnectCharts() +{' '} + appendData streaming +
+ Try it: Move crosshair or zoom on any chart while data streams — all three stay in sync +
+ +
+ +
+ +
+ +
+
+ ); +} + function ExternalRenderModeExample() { const ref = useRef(null); const [mode, setMode] = useState<'auto' | 'external'>('external'); @@ -672,6 +947,8 @@ function App() { <> + + diff --git a/package.json b/package.json index 9462935..80b99e0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { - "name": "chartgpu-react", - "version": "0.1.3", + "name": "@chartgpu/chartgpu-react", + "version": "0.1.4", "description": "React bindings for ChartGPU - WebGPU-powered charting library", "license": "MIT", "author": "",