Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions .github/workflows/publish-gpr.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
name: Publish to GitHub Packages

on:
release:
types: [published]

jobs:
publish:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write

steps:
- name: Checkout
uses: actions/checkout@v4

- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: "20.x"
cache: "npm"

- name: Install dependencies
run: npm ci

- name: Verify release tag matches package version
env:
RELEASE_TAG: ${{ github.event.release.tag_name }}
run: |
node -e "const fs=require('fs');const v=JSON.parse(fs.readFileSync('package.json','utf8')).version;const t=(process.env.RELEASE_TAG||'').replace(/^v/,'');if(!t){console.error('Missing release tag');process.exit(1)};if(t!==v){console.error(`Release tag (${process.env.RELEASE_TAG}) does not match package.json version (${v})`);process.exit(1)};console.log(`Version OK: ${v}`);"

- name: Build
run: npm run build

# GitHub Packages npm registry requires a scoped package name that matches the owner/org.
# We scope it only at publish-time so the source package name can stay unscoped for npmjs.org if desired.
- name: Scope package name for GitHub Packages
run: |
node -e "const fs=require('fs');const p=JSON.parse(fs.readFileSync('package.json','utf8'));p.name='@chartgpu/chartgpu-react';fs.writeFileSync('package.json',JSON.stringify(p,null,2)+'\n');"

- name: Configure npm for GitHub Packages
uses: actions/setup-node@v4
with:
node-version: "20.x"
registry-url: "https://npm.pkg.github.com"

- name: Publish
run: npm publish --registry=https://npm.pkg.github.com
env:
NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
68 changes: 60 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,10 @@
## Highlights

- **`ChartGPU` component (recommended)**: async create/dispose lifecycle + debounced `ResizeObserver` sizing
- **Event props**: `onClick`, `onCrosshairMove`, `onZoomChange`, etc.
- **Imperative `ref` API**: `ChartGPUHandle` (`getChart`, `getContainer`, `appendData`, `setOption`, `setZoomRange`, `setInteractionX`, `getInteractionX`, `hitTest`)
- **Hooks**: `useChartGPU(...)`, `useConnectCharts(..., syncOptions?)`
- **Helper re-exports (from `@chartgpu/chartgpu`)**: `createChart`, `connectCharts`, `createAnnotationAuthoring`
- **Event props**: `onClick`, `onCrosshairMove`, `onZoomChange`, `onDataAppend`, `onDeviceLost`, etc.
- **Imperative `ref` API**: `ChartGPUHandle` (`getChart`, `getContainer`, `appendData`, `setOption`, `setZoomRange`, `setInteractionX`, `getInteractionX`, `hitTest`, `needsRender`, `renderFrame`, `getRenderMode`, `setRenderMode`)
- **Hooks**: `useChartGPU(...)`, `useGPUContext()`, `useConnectCharts(..., syncOptions?)`
- **Helper re-exports (from `@chartgpu/chartgpu`)**: `createChart`, `connectCharts`, `createPipelineCache`, `getPipelineCacheStats`, `destroyPipelineCache`, `createAnnotationAuthoring`

## Quick start

Expand Down Expand Up @@ -85,15 +85,17 @@ Check browser compatibility at [caniuse.com/webgpu](https://caniuse.com/webgpu).
- **`ChartGPU`** React component (recommended)
- lifecycle management (async create + dispose)
- `ResizeObserver` resize (debounced)
- event props: `onClick`, `onCrosshairMove`, `onZoomChange`, etc.
- imperative `ref` API: `ChartGPUHandle` (`getChart`, `getContainer`, `appendData`, `setOption`, `setZoomRange`, `setInteractionX`, `getInteractionX`, `hitTest`)
- event props: `onClick`, `onCrosshairMove`, `onZoomChange`, `onDataAppend`, `onDeviceLost`, etc.
- multi-chart dashboards: `gpuContext` prop (share a `GPUDevice` across charts)
- imperative `ref` API: `ChartGPUHandle` (`getChart`, `getContainer`, `appendData`, `setOption`, `setZoomRange`, `setInteractionX`, `getInteractionX`, `hitTest`, `needsRender`, `renderFrame`, `getRenderMode`, `setRenderMode`)
- **Hooks**
- `useChartGPU(containerRef, options)` — create/manage a chart instance
- `useChartGPU(containerRef, options, gpuContext?)` — create/manage a chart instance (optionally share GPU resources)
- `useGPUContext()` — create a shared `GPUAdapter` + `GPUDevice` + `PipelineCache` for multi-chart dashboards
- `useConnectCharts([chartA, chartB, ...], syncOptions?)` — sync crosshair/interaction-x (and optionally zoom) across charts
- **Deprecated**
- `ChartGPUChart` (legacy adapter; use `ChartGPU` instead)
- **Helper re-exports** (from peer dependency `@chartgpu/chartgpu`)
- `createChart`, `connectCharts`, `createAnnotationAuthoring`
- `createChart`, `connectCharts`, `createAnnotationAuthoring`, `createPipelineCache`, `getPipelineCacheStats`, `destroyPipelineCache`

For details, start with the [API reference](./docs/API.md).

Expand Down Expand Up @@ -133,6 +135,56 @@ disconnect();

If you prefer a hook-driven approach, you can use `onReady` (or `useChartGPU`) to capture instances, then call `useConnectCharts(...)` once both are available.

### External render mode (app-owned render loop)

```tsx
import { useEffect, useRef } from 'react';
import { ChartGPU } from 'chartgpu-react';
import type { ChartGPUHandle } from 'chartgpu-react';

function ExternalLoop() {
const ref = useRef<ChartGPUHandle>(null);

useEffect(() => {
let raf = 0;
const loop = () => {
if (ref.current?.needsRender()) {
ref.current.renderFrame();
}
raf = requestAnimationFrame(loop);
};
raf = requestAnimationFrame(loop);
return () => cancelAnimationFrame(raf);
}, []);

return <ChartGPU ref={ref} options={{ ...options, renderMode: 'external' }} />;
}
```

### Multi-chart dashboards (shared GPU device + pipeline cache)

```tsx
import { ChartGPU, useGPUContext } from 'chartgpu-react';

function Dashboard() {
const { adapter, device, pipelineCache, isReady, error } = useGPUContext();

if (error) return <div>{error.message}</div>;
if (!isReady || !adapter || !device) return <div>Loading…</div>;

const gpuContext = pipelineCache
? { adapter, device, pipelineCache }
: { adapter, device };

return (
<>
<ChartGPU options={optionsA} gpuContext={gpuContext} />
<ChartGPU options={optionsB} gpuContext={gpuContext} />
</>
);
}
```

### Annotation authoring UI (`createAnnotationAuthoring`)

```tsx
Expand Down
9 changes: 5 additions & 4 deletions docs/API.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ For an LLM-oriented navigation entrypoint, see [`docs/api/llm-context.md`](./api

### Hooks

- **`useChartGPU(containerRef, options)`** — create/manage an instance imperatively
- **`useChartGPU(containerRef, options, gpuContext?)`** — create/manage an instance imperatively (3rd param optional shared context; init-only)
- **`useConnectCharts([chartA, chartB, ...], syncOptions?)`** — keep crosshair/interaction-x in sync (optionally sync zoom)

See [`docs/api/hooks.md`](./api/hooks.md).
Expand Down Expand Up @@ -54,8 +54,8 @@ More recipes:

From `src/types.ts`:

- `ChartGPUProps` — props for the `ChartGPU` component
- `ChartGPUHandle` — imperative ref API
- `ChartGPUProps` — props for the `ChartGPU` component. Includes `gpuContext?` (init-only), `onDataAppend?` ↔ `'dataAppend'`, `onDeviceLost?` ↔ `'deviceLost'`
- `ChartGPUHandle` — imperative ref API (`renderFrame`, `needsRender`, `getRenderMode`, `setRenderMode`, `appendData`, etc.)
- `ChartInstance` — alias for `@chartgpu/chartgpu`'s `ChartGPUInstance`
- `ClickParams`, `MouseOverParams` — aliases for event payloads
- `ZoomRange` — derived from `ChartGPUInstance['getZoomRange']` (non-null range)
Expand Down Expand Up @@ -102,9 +102,10 @@ Start here: [`ChartGPU` component docs](./api/chartgpu-component.md).

Use the ref API when you need:

- `appendData` streaming
- `appendData` streaming (Cartesian series data superset + OHLC array)
- access to the underlying `ChartGPUInstance`
- access to the container element (e.g. for annotation authoring UI overlays)
- external render mode: `renderFrame()`, `needsRender()`, `getRenderMode()`, `setRenderMode()`

See [`ChartGPUHandle` docs](./api/chartgpu-handle.md).

Expand Down
4 changes: 2 additions & 2 deletions docs/GETTING_STARTED.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
# Getting started

`chartgpu-react` is a **thin React + TypeScript wrapper** around the [`chartgpu`](https://www.npmjs.com/package/chartgpu) WebGPU charting library.
`chartgpu-react` is a **thin React + TypeScript wrapper** around the [`@chartgpu/chartgpu`](https://www.npmjs.com/package/@chartgpu/chartgpu) WebGPU charting library.

## Install

```bash
npm install chartgpu-react chartgpu react react-dom
npm install chartgpu-react @chartgpu/chartgpu react react-dom
```

## Requirements
Expand Down
12 changes: 9 additions & 3 deletions docs/api/chartgpu-component.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ The props type is `ChartGPUProps` (defined in `src/types.ts`).
| Prop | Type | Required | Notes |
|---|---|---:|---|
| `options` | `ChartGPUOptions` | ✅ | Full chart configuration object. Updates call `setOption(...)` with a full replacement. |
| `gpuContext` | `ChartGPUCreateContext` | | **Init-only.** Shared GPU context for multi-chart dashboards. Changing after mount has no effect. |
| `theme` | `ChartGPUOptions['theme']` | | Theme override merged into `options` (`'dark' \| 'light' \| ThemeConfig`). |
| `style` | `React.CSSProperties` | | Applied to the container `<div>`. Provide an explicit `height`. |
| `className` | `string` | | Applied to the container `<div>`. |
Expand All @@ -31,6 +32,8 @@ The props type is `ChartGPUProps` (defined in `src/types.ts`).
| `onMouseOver` | `(payload: ChartGPUEventPayload) => void` | | Wires to `chart.on('mouseover', ...)`. |
| `onMouseOut` | `(payload: ChartGPUEventPayload) => void` | | Wires to `chart.on('mouseout', ...)`. |
| `onCrosshairMove` | `(payload: ChartGPUCrosshairMovePayload) => void` | | Wires to `chart.on('crosshairMove', ...)`. |
| `onDataAppend` | `(payload: ChartGPUDataAppendPayload) => void` | | Wires to `chart.on('dataAppend', ...)`. Fires when data is appended via `appendData(...)`. |
| `onDeviceLost` | `(payload: ChartGPUDeviceLostPayload) => void` | | Wires to `chart.on('deviceLost', ...)`. Most relevant when using shared `gpuContext`. |
| `onZoomChange` | `(range: ZoomRange) => void` | | Fires on `zoomRangeChange` event. Also emits the current range once on subscribe (initial hydration). |

## Imperative ref (`ChartGPUHandle`)
Expand All @@ -39,7 +42,10 @@ The props type is `ChartGPUProps` (defined in `src/types.ts`).

- `getChart()`
- `getContainer()`
- `appendData(seriesIndex, newPoints)`
- `appendData(seriesIndex, newPoints)` — accepts Cartesian series data superset or `OHLCDataPoint[]`
- `renderFrame(): boolean` — render one frame (external mode)
- `needsRender(): boolean` — whether the chart has pending changes
- `getRenderMode()`, `setRenderMode(mode)` — external render mode control
- `setOption(options)`
- `setZoomRange(start, end)`
- `setInteractionX(x, source?)`
Expand All @@ -55,10 +61,10 @@ See [`ChartGPUHandle`](./chartgpu-handle.md).
On mount, the component calls:

```ts
ChartGPU.create(containerDiv, effectiveOptions)
ChartGPU.create(containerDiv, effectiveOptions, gpuContext?)
```

Where `effectiveOptions` is `options` with `theme` merged in when provided.
Where `effectiveOptions` is `options` with `theme` merged in when provided. If `gpuContext` is passed, it is used for shared GPU resources (init-only; changing the prop after mount has no effect).

If the component unmounts before async creation completes (common in React 18 StrictMode dev), the newly-created chart instance is disposed immediately to avoid leaks.

Expand Down
35 changes: 33 additions & 2 deletions docs/api/chartgpu-handle.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
- accessing the underlying `ChartGPUInstance`
- accessing the container element
- streaming/append updates (`appendData`)
- external render mode (`renderFrame`, `needsRender`, `getRenderMode`, `setRenderMode`)
- replacing options (`setOption`)
- programmatic zoom control (`setZoomRange`)
- programmatic crosshair/tooltip (`setInteractionX`, `getInteractionX`)
Expand Down Expand Up @@ -42,15 +43,33 @@ Returns the container `<div>` used to mount the chart.

Common use case: pass it to helpers like `createAnnotationAuthoring(container, chart, ...)`.

### `appendData(seriesIndex: number, newPoints: DataPoint[] | OHLCDataPoint[]): void`
### `appendData(seriesIndex: number, newPoints: CartesianSeriesData | OHLCDataPoint[]): void`

Appends points to an existing series, delegating to `ChartGPUInstance.appendData(...)`.

- **`seriesIndex`**: zero-based index into `options.series`.
- **`newPoints`**: array of new points. For candlesticks, use `OHLCDataPoint[]`.
- **`newPoints`**: Cartesian series data superset (`DataPoint[]`, `XYArraysData`, `InterleavedXYData`) or `OHLCDataPoint[]` for candlesticks.

This is typically more efficient than replacing the entire `options.series[n].data` array.

### `renderFrame(): boolean`

Renders a single frame. Intended for external render mode.

- **Returns**: `true` if a frame was rendered, `false` if already clean.

### `needsRender(): boolean`

Returns whether the chart has pending changes that require rendering.

### `getRenderMode(): RenderMode`

Returns the current render mode (`'auto'` or `'external'`).

### `setRenderMode(mode: RenderMode): void`

Sets the render mode. Use `'external'` to drive frames manually via `renderFrame()` based on `needsRender()`.

### `setOption(options: ChartGPUOptions): void`

Replaces the chart options, delegating to `ChartGPUInstance.setOption(options)`.
Expand Down Expand Up @@ -100,6 +119,18 @@ Import the result type if needed:
import type { ChartGPUHitTestResult } from 'chartgpu-react';
```

## External render mode

Set `options.renderMode = 'external'` or call `setRenderMode('external')`, then drive frames yourself:

```ts
if (handle.current?.needsRender()) {
handle.current.renderFrame();
}
```

Useful when you need precise control over when the chart renders (e.g. coordinating with other animations or a custom render loop).

## Example: streaming with `appendData`

```tsx
Expand Down
13 changes: 8 additions & 5 deletions docs/api/hooks.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

This package provides two hooks:

- `useChartGPU(containerRef, options)` — create/manage a `ChartGPUInstance`
- `useChartGPU(containerRef, options, gpuContext?)` — create/manage a `ChartGPUInstance` (3rd param optional shared context; init-only)
- `useConnectCharts(charts, syncOptions?)` — connect instances for synced crosshair/interaction-x (and optionally zoom)

Related:
Expand All @@ -11,35 +11,38 @@ Related:
- [Chart sync recipe](../recipes/chart-sync.md)
- LLM entrypoint: [`llm-context.md`](./llm-context.md)

## `useChartGPU(containerRef, options)`
## `useChartGPU(containerRef, options, gpuContext?)`

Creates a `@chartgpu/chartgpu` chart instance inside a DOM element that you control.

### Import

```ts
import { useChartGPU } from 'chartgpu-react';
import type { ChartGPUOptions } from 'chartgpu-react';
import type { ChartGPUOptions, ChartGPUCreateContext } from 'chartgpu-react';
```

### Signature

```ts
function useChartGPU(
containerRef: React.RefObject<HTMLElement>,
options: ChartGPUOptions
options: ChartGPUOptions,
gpuContext?: ChartGPUCreateContext
): {
chart: ChartGPUInstance | null;
isReady: boolean;
error: Error | null;
}
```

The 3rd parameter `gpuContext` is **init-only**: it is only read during chart creation. Changing it after mount has no effect.

### Behavior

- On mount:
- checks WebGPU support (`'gpu' in navigator`)
- calls `ChartGPU.create(containerRef.current, options)`
- calls `ChartGPU.create(containerRef.current, options, gpuContext?)`
- sets `chart`, `isReady`, and `error` accordingly
- On `options` change:
- calls `chart.setOption(options)` (full replacement)
Expand Down
Loading