diff --git a/README.md b/README.md index 1d960ef..c6ae73f 100644 --- a/README.md +++ b/README.md @@ -1,30 +1,37 @@ -# chartgpu-react +

+ ChartGPU +

- React bindings for ChartGPU — WebGPU-powered charting for high-performance data visualization. + React bindings for ChartGPU — The fastest open-source charting library — 50M points at 60 FPS.

+[Powered by WebGPU](https://forthebadge.com) [![Documentation](https://img.shields.io/badge/Documentation-Getting%20Started-blue?style=for-the-badge)](https://github.com/chartgpu/chartgpu-react/blob/main/docs/GETTING_STARTED.md) [![API Reference](https://img.shields.io/badge/API-Reference-blue?style=for-the-badge)](https://github.com/chartgpu/chartgpu-react/blob/main/docs/API.md) [![Examples](https://img.shields.io/badge/Examples-Run%20Locally-blue?style=for-the-badge)](https://github.com/chartgpu/chartgpu-react/tree/main/examples) - [![npm version](https://img.shields.io/npm/v/chartgpu-react?style=for-the-badge&color=blue)](https://www.npmjs.com/package/chartgpu-react) [![NPM Downloads](https://img.shields.io/npm/dm/chartgpu-react?style=for-the-badge&color=%2368cc49)](https://www.npmjs.com/package/chartgpu-react) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg?style=for-the-badge)](https://www.npmjs.com/package/chartgpu-react) +[Featured on Hacker News](https://news.ycombinator.com/item?id=46706528) + +[Featured in Awesome WebGPU](https://github.com/mikbry/awesome-webgpu) + +
-`chartgpu-react` is a **thin React + TypeScript wrapper** around the [`chartgpu`](https://www.npmjs.com/package/chartgpu) core library. +`chartgpu-react` is a **thin React + TypeScript wrapper** around the [`@chartgpu/chartgpu`](https://www.npmjs.com/package/@chartgpu/chartgpu) core library. ## 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`) -- **Hooks**: `useChartGPU(...)`, `useConnectCharts(...)` -- **Helper re-exports (from `chartgpu`)**: `createChart`, `connectCharts`, `createAnnotationAuthoring` +- **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` ## Quick start @@ -60,7 +67,7 @@ function MyChart() { ## Installation ```bash -npm install chartgpu-react chartgpu react react-dom +npm install chartgpu-react @chartgpu/chartgpu react react-dom ``` ### Requirements @@ -79,20 +86,20 @@ Check browser compatibility at [caniuse.com/webgpu](https://caniuse.com/webgpu). - lifecycle management (async create + dispose) - `ResizeObserver` resize (debounced) - event props: `onClick`, `onCrosshairMove`, `onZoomChange`, etc. - - imperative `ref` API: `ChartGPUHandle` (`getChart`, `getContainer`, `appendData`, `setOption`) + - imperative `ref` API: `ChartGPUHandle` (`getChart`, `getContainer`, `appendData`, `setOption`, `setZoomRange`, `setInteractionX`, `getInteractionX`, `hitTest`) - **Hooks** - `useChartGPU(containerRef, options)` — create/manage a chart instance - - `useConnectCharts([chartA, chartB, ...])` — sync crosshair/interaction-x across charts + - `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`) +- **Helper re-exports** (from peer dependency `@chartgpu/chartgpu`) - `createChart`, `connectCharts`, `createAnnotationAuthoring` For details, start with the [API reference](./docs/API.md). ## Feature snippets (ChartGPU core) -These snippets use helpers and events from the `chartgpu` core library (peer dependency of `chartgpu-react`). +These snippets use helpers and events from the `@chartgpu/chartgpu` core library (peer dependency of `chartgpu-react`). ### Crosshair / interaction X (`'crosshairMove'`) @@ -112,16 +119,19 @@ import type { ChartGPUCrosshairMovePayload } from 'chartgpu-react'; ### Connect charts (sync crosshair/tooltip) ```tsx -import { connectCharts } from 'chartgpu'; +import { connectCharts } from 'chartgpu-react'; // When you have two ChartGPUInstance objects: const disconnect = connectCharts([chartA, chartB]); +// With zoom sync: +// const disconnect = connectCharts([chartA, chartB], { syncZoom: true }); + // Later: disconnect(); ``` -If you prefer a hook-driven approach, you can use `onReady` (or `useChartGPU`) to capture instances, then call `connectCharts(...)` once both are available. +If you prefer a hook-driven approach, you can use `onReady` (or `useChartGPU`) to capture instances, then call `useConnectCharts(...)` once both are available. ### Annotation authoring UI (`createAnnotationAuthoring`) @@ -158,7 +168,7 @@ function AnnotationAuthoringExample() { import { useEffect, useRef } from 'react'; import { ChartGPU } from 'chartgpu-react'; import type { ChartGPUHandle, ChartGPUOptions } from 'chartgpu-react'; -import type { OHLCDataPoint } from 'chartgpu'; +import type { OHLCDataPoint } from 'chartgpu-react'; function CandlestickStreaming() { const ref = useRef(null); @@ -232,28 +242,28 @@ The dev server will start at `http://localhost:3000` and open the examples page ### Local development with linked ChartGPU -To develop `chartgpu-react` against a local version of the `chartgpu` package (useful for testing changes across both repositories): +To develop `chartgpu-react` against a local version of the `@chartgpu/chartgpu` package (useful for testing changes across both repositories): ```bash -# 1. Link the chartgpu package from the sibling repo +# 1. Link the @chartgpu/chartgpu package from the sibling repo cd ../chart-gpu npm link -# 2. Link chartgpu into this project +# 2. Link @chartgpu/chartgpu into this project cd ../chartgpu-react -npm link chartgpu +npm link @chartgpu/chartgpu # 3. Build and run - will use the linked local package npm run build npm run dev ``` -**Note:** After linking, `npm run build` and `npm run dev` will resolve imports to your local `chartgpu` package instead of the published version. This allows you to test changes in both repos simultaneously. +**Note:** After linking, `npm run build` and `npm run dev` will resolve imports to your local `@chartgpu/chartgpu` package instead of the published version. This allows you to test changes in both repos simultaneously. To unlink and return to the published package: ```bash -npm unlink chartgpu +npm unlink @chartgpu/chartgpu npm install ``` @@ -267,13 +277,25 @@ import type { ChartGPUOptions, ChartGPUEventPayload, ChartGPUCrosshairMovePayload, + ChartGPUZoomRangeChangePayload, + ChartGPUHitTestResult, + ChartGPUHitTestMatch, + ChartSyncOptions, AreaSeriesConfig, LineSeriesConfig, BarSeriesConfig, PieSeriesConfig, ScatterSeriesConfig, SeriesConfig, + LineStyleConfig, + AreaStyleConfig, DataPoint, + LegendConfig, + LegendPosition, + AnimationConfig, + TooltipConfig, + TooltipParams, + PerformanceMetrics, } from 'chartgpu-react'; ``` @@ -293,7 +315,7 @@ const checkSupport = async () => { ## Contributing -Issues and pull requests are welcome. If you’re planning a larger change, open an issue first so we can discuss direction. +Issues and pull requests are welcome. If you're planning a larger change, open an issue first so we can discuss direction. ## License diff --git a/docs/API.md b/docs/API.md index cf24e83..0be44cc 100644 --- a/docs/API.md +++ b/docs/API.md @@ -1,6 +1,6 @@ # API reference (chartgpu-react) -This package is a **React wrapper** around the `chartgpu` core library. Most runtime behavior lives in `chartgpu`; this repo primarily provides: +This package is a **React wrapper** around the `@chartgpu/chartgpu` core library. Most runtime behavior lives in `@chartgpu/chartgpu`; this repo primarily provides: - A React component (`ChartGPU`) with lifecycle + resize management - A small imperative ref API (`ChartGPUHandle`) @@ -19,11 +19,11 @@ For an LLM-oriented navigation entrypoint, see [`docs/api/llm-context.md`](./api ### Hooks - **`useChartGPU(containerRef, options)`** — create/manage an instance imperatively -- **`useConnectCharts([chartA, chartB, ...])`** — keep crosshair/interaction-x in sync +- **`useConnectCharts([chartA, chartB, ...], syncOptions?)`** — keep crosshair/interaction-x in sync (optionally sync zoom) See [`docs/api/hooks.md`](./api/hooks.md). -### Helper re-exports (from peer dependency `chartgpu`) +### Helper re-exports (from peer dependency `@chartgpu/chartgpu`) `chartgpu-react` exposes these helpers so you can often import everything from one package: @@ -31,8 +31,8 @@ See [`docs/api/hooks.md`](./api/hooks.md). - `connectCharts` - `createAnnotationAuthoring` -- `createChart` / `connectCharts` are re-exported directly from `chartgpu`. -- `createAnnotationAuthoring` is a thin wrapper around `chartgpu`’s helper that includes a small fix for `chartgpu@0.2.3`: the upstream authoring context menu hit-testing does not recognize `type: "text"` annotations, so **Edit** may not appear for text notes. +- `createChart` / `connectCharts` are re-exported directly from `@chartgpu/chartgpu`. +- `createAnnotationAuthoring` is a thin wrapper around `@chartgpu/chartgpu`'s helper that patches text-annotation context-menu hit-testing (broken in `chartgpu@0.2.3`). Upstream v0.2.5 fixes this natively; the wrapper is kept for API stability — the patch is harmless when running against v0.2.5+. ```ts import { connectCharts, createAnnotationAuthoring } from 'chartgpu-react'; @@ -56,13 +56,13 @@ From `src/types.ts`: - `ChartGPUProps` — props for the `ChartGPU` component - `ChartGPUHandle` — imperative ref API -- `ChartInstance` — alias for `chartgpu`’s `ChartGPUInstance` +- `ChartInstance` — alias for `@chartgpu/chartgpu`'s `ChartGPUInstance` - `ClickParams`, `MouseOverParams` — aliases for event payloads - `ZoomRange` — derived from `ChartGPUInstance['getZoomRange']` (non-null range) -### Re-exported core types (from peer dependency `chartgpu`) +### Re-exported core types (from peer dependency `@chartgpu/chartgpu`) -From `src/index.ts`, this package re-exports a curated set of `chartgpu` types so consumers can do: +From `src/index.ts`, this package re-exports a curated set of `@chartgpu/chartgpu` types so consumers can do: ```ts import type { ChartGPUOptions, ChartGPUInstance, DataPoint } from 'chartgpu-react'; @@ -71,11 +71,18 @@ import type { ChartGPUOptions, ChartGPUInstance, DataPoint } from 'chartgpu-reac Currently re-exported: - **Core**: `ChartGPUInstance`, `ChartGPUOptions` -- **Events**: `ChartGPUEventPayload`, `ChartGPUCrosshairMovePayload` +- **Events**: `ChartGPUEventPayload`, `ChartGPUCrosshairMovePayload`, `ChartGPUZoomRangeChangePayload` +- **Hit testing**: `ChartGPUHitTestResult`, `ChartGPUHitTestMatch` +- **Chart sync**: `ChartSyncOptions` - **Annotation authoring**: `AnnotationAuthoringInstance`, `AnnotationAuthoringOptions`, `AnnotationConfig` - **Series config**: `AreaSeriesConfig`, `LineSeriesConfig`, `BarSeriesConfig`, `PieSeriesConfig`, `ScatterSeriesConfig`, `CandlestickSeriesConfig`, `SeriesConfig` +- **Style config**: `LineStyleConfig`, `AreaStyleConfig` - **Data**: `DataPoint`, `OHLCDataPoint` - **Interaction/zoom**: `DataZoomConfig` +- **Legend**: `LegendConfig`, `LegendPosition` +- **Animation**: `AnimationConfig` +- **Tooltip**: `TooltipConfig`, `TooltipParams` +- **Performance**: `PerformanceMetrics` - **Themes**: `ThemeConfig`, `ThemeName` - **Layout**: `AxisConfig`, `GridConfig` @@ -109,10 +116,10 @@ See [`useChartGPU` docs](./api/hooks.md#usechartgpu). ### 4) Connecting charts (`useConnectCharts` / `connectCharts`) -To sync crosshair / interaction-x across multiple charts, use: +To sync crosshair / interaction-x (and optionally zoom) across multiple charts, use: -- `useConnectCharts([chartA, chartB, ...])` (React-friendly) -- or `connectCharts([chartA, chartB, ...])` (manual) +- `useConnectCharts([chartA, chartB, ...], syncOptions?)` (React-friendly) +- or `connectCharts([chartA, chartB, ...], syncOptions?)` (manual) See [Chart sync recipe](./recipes/chart-sync.md). @@ -126,4 +133,3 @@ Step-by-step guides for common use cases: - [Streaming](./recipes/streaming.md) — realtime data updates with `appendData` - [dataZoom basics](./recipes/datazoom-basics.md) — zoom and pan with `dataZoom` + `onZoomChange` - [Scatter density](./recipes/scatter-density.md) — density heatmaps for large scatter datasets - diff --git a/docs/api/chartgpu-component.md b/docs/api/chartgpu-component.md index b855da7..db388a9 100644 --- a/docs/api/chartgpu-component.md +++ b/docs/api/chartgpu-component.md @@ -31,7 +31,7 @@ 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', ...)`. | -| `onZoomChange` | `(range: ZoomRange) => void` | | Fires when `chart.getZoomRange()` changes (polled every 100ms). | +| `onZoomChange` | `(range: ZoomRange) => void` | | Fires on `zoomRangeChange` event. Also emits the current range once on subscribe (initial hydration). | ## Imperative ref (`ChartGPUHandle`) @@ -41,6 +41,10 @@ The props type is `ChartGPUProps` (defined in `src/types.ts`). - `getContainer()` - `appendData(seriesIndex, newPoints)` - `setOption(options)` +- `setZoomRange(start, end)` +- `setInteractionX(x, source?)` +- `getInteractionX()` +- `hitTest(e)` — note: React synthetic events require `e.nativeEvent` See [`ChartGPUHandle`](./chartgpu-handle.md). @@ -80,10 +84,9 @@ Resize calls are debounced (100ms). ### Zoom change events -If you provide `onZoomChange`, the component polls `chart.getZoomRange()` every 100ms and fires the callback when: +If you provide `onZoomChange`, the component subscribes to the upstream `zoomRangeChange` event and fires the callback whenever the zoom range changes. -- the zoom range transitions from `null` → non-null, or -- `start`/`end` values change. +On subscribe, the component also reads the current zoom range via `getZoomRange()` and fires `onZoomChange` once if the range is non-null. This ensures consumers can hydrate UI state without waiting for user interaction. If zoom is disabled (`null`), no callback is fired. diff --git a/docs/api/chartgpu-handle.md b/docs/api/chartgpu-handle.md index b54a2e6..f1ee9fa 100644 --- a/docs/api/chartgpu-handle.md +++ b/docs/api/chartgpu-handle.md @@ -6,12 +6,16 @@ - accessing the container element - streaming/append updates (`appendData`) - replacing options (`setOption`) +- programmatic zoom control (`setZoomRange`) +- programmatic crosshair/tooltip (`setInteractionX`, `getInteractionX`) +- hit testing (`hitTest`) Related: - [`ChartGPU` component](./chartgpu-component.md) - [Streaming recipe](../recipes/streaming.md) - [Annotation authoring recipe](../recipes/annotation-authoring.md) +- [dataZoom basics](../recipes/datazoom-basics.md) - LLM entrypoint: [`llm-context.md`](./llm-context.md) ## Import @@ -25,7 +29,7 @@ import type { ChartGPUHandle } from 'chartgpu-react'; ### `getChart(): ChartGPUInstance | null` -Returns the underlying `chartgpu` instance once initialized, otherwise `null`. +Returns the underlying `@chartgpu/chartgpu` instance once initialized, otherwise `null`. Notes: @@ -53,6 +57,49 @@ Replaces the chart options, delegating to `ChartGPUInstance.setOption(options)`. Important: `setOption` is treated as a **full replacement**, not a partial merge. +### `setZoomRange(start: number, end: number): void` + +Programmatically sets the zoom range in percent-space. + +- **`start`**: start of zoom range (0–100). +- **`end`**: end of zoom range (0–100). + +No-op when zoom is disabled on the chart (i.e. no `dataZoom` configured). + +### `setInteractionX(x: number | null, source?: unknown): void` + +Programmatically drives the crosshair / tooltip to a domain-space x value. Pass `null` to clear the crosshair. + +- **`x`**: domain-space x value, or `null` to clear. +- **`source`** (optional): source identifier, useful for sync disambiguation (e.g. avoiding echo loops with `connectCharts`). + +### `getInteractionX(): number | null` + +Reads the current crosshair / interaction x in domain units. Returns `null` when the crosshair is inactive. + +### `hitTest(e: PointerEvent | MouseEvent): ChartGPUHitTestResult` + +Performs hit-testing on a pointer or mouse event. Returns coordinates and any matched chart element. + +- **`e`**: a native `PointerEvent` or `MouseEvent` from the chart container. +- **Returns**: `ChartGPUHitTestResult` with pixel coordinates, domain-space coordinates, and an optional match object. +- If the chart is not initialized or disposed, returns a sentinel with `isInGrid: false` and `NaN` coordinates. + +**React ergonomics note**: React synthetic events (`React.PointerEvent`, `React.MouseEvent`) are not the same as native DOM events. Pass `e.nativeEvent` instead: + +```tsx +
{ + const result = chartRef.current?.hitTest(e.nativeEvent); + // ... +}} /> +``` + +Import the result type if needed: + +```ts +import type { ChartGPUHitTestResult } from 'chartgpu-react'; +``` + ## Example: streaming with `appendData` ```tsx @@ -98,4 +145,3 @@ export function StreamingCandles() { return ; } ``` - diff --git a/docs/api/hooks.md b/docs/api/hooks.md index a168259..cb8cf3e 100644 --- a/docs/api/hooks.md +++ b/docs/api/hooks.md @@ -3,7 +3,7 @@ This package provides two hooks: - `useChartGPU(containerRef, options)` — create/manage a `ChartGPUInstance` -- `useConnectCharts(charts)` — connect instances for synced crosshair/interaction-x +- `useConnectCharts(charts, syncOptions?)` — connect instances for synced crosshair/interaction-x (and optionally zoom) Related: @@ -13,7 +13,7 @@ Related: ## `useChartGPU(containerRef, options)` -Creates a `chartgpu` chart instance inside a DOM element that you control. +Creates a `@chartgpu/chartgpu` chart instance inside a DOM element that you control. ### Import @@ -81,9 +81,9 @@ export function HookChart() { } ``` -## `useConnectCharts(charts)` +## `useConnectCharts(charts, syncOptions?)` -Connects multiple `ChartGPUInstance`s so they share interaction state (crosshair/tooltip x). +Connects multiple `ChartGPUInstance`s so they share interaction state (crosshair/tooltip x). Optionally syncs zoom/pan across charts. This hook is a React-friendly wrapper around the upstream helper `connectCharts(...)`. @@ -91,26 +91,41 @@ This hook is a React-friendly wrapper around the upstream helper `connectCharts( ```ts import { useConnectCharts } from 'chartgpu-react'; -import type { ChartGPUInstance } from 'chartgpu-react'; +import type { ChartGPUInstance, ChartSyncOptions } from 'chartgpu-react'; ``` ### Signature ```ts function useConnectCharts( - charts: ReadonlyArray + charts: ReadonlyArray, + syncOptions?: ChartSyncOptions ): void; ``` +### `ChartSyncOptions` + +```ts +type ChartSyncOptions = Readonly<{ + syncCrosshair?: boolean; // default true + syncZoom?: boolean; // default false +}>; +``` + +- **`syncCrosshair`** (default `true`): sync crosshair + tooltip x across charts. +- **`syncZoom`** (default `false`): sync zoom/pan range across charts. + ### Behavior - Does nothing until all provided instances exist and are **not disposed** - Automatically disconnects when: - the hook unmounts, or - - the identity/disposed state of instances changes -- If `connectCharts(...)` throws, the hook logs an error and avoids crashing your component tree + - the identity/disposed state of instances changes, or + - `syncOptions` changes +- Reconnection is based on **option values**, not object identity — you can pass a new `syncOptions` object each render and the hook will only reconnect when the actual `syncCrosshair`/`syncZoom` values change +- If `connectCharts(...)` throws, the hook logs an error (dev builds only) and avoids crashing your component tree -### Example +### Example: synced crosshair (default) ```tsx import { useMemo, useState } from 'react'; @@ -151,3 +166,14 @@ export function SyncedCharts() { } ``` +### Example: synced zoom + +```tsx +useConnectCharts([a, b], { syncZoom: true }); +``` + +This syncs both crosshair and zoom across the connected charts. To sync only zoom without crosshair: + +```tsx +useConnectCharts([a, b], { syncCrosshair: false, syncZoom: true }); +``` diff --git a/docs/api/llm-context.md b/docs/api/llm-context.md index 90b13ba..9ea4c3c 100644 --- a/docs/api/llm-context.md +++ b/docs/api/llm-context.md @@ -1,6 +1,6 @@ # chartgpu-react — LLM / Context7 documentation entrypoint -This repository provides **React bindings** for the [`chartgpu`](https://www.npmjs.com/package/chartgpu) WebGPU charting library. +This repository provides **React bindings** for the [`@chartgpu/chartgpu`](https://www.npmjs.com/package/@chartgpu/chartgpu) WebGPU charting library. The goal of this `llm-context.md` file is to give Context7 (and other LLM tooling) a **single, stable entrypoint** that maps common tasks to the right documentation pages and source files. @@ -16,15 +16,15 @@ The goal of this `llm-context.md` file is to give Context7 (and other LLM toolin - `ChartGPUChart` (legacy / deprecated, thin adapter) - **Hooks** - `useChartGPU(containerRef, options)` — imperative hook for creating and managing a ChartGPU instance - - `useConnectCharts([chartA, chartB, ...])` — connect multiple charts for synced crosshair/tooltip + - `useConnectCharts([chartA, chartB, ...], syncOptions?)` — connect multiple charts for synced crosshair/tooltip (optionally sync zoom) - **Core helpers** - - `createChart` (re-exported from `chartgpu`) - - `connectCharts` (re-exported from `chartgpu`) - - `createAnnotationAuthoring` (wrapper around `chartgpu`’s helper; includes a fix for `chartgpu@0.2.3` text annotation context-menu hit-testing) + - `createChart` (re-exported from `@chartgpu/chartgpu`) + - `connectCharts` (re-exported from `@chartgpu/chartgpu`) + - `createAnnotationAuthoring` (wrapper around `@chartgpu/chartgpu`'s helper; includes a legacy fix for text annotation context-menu hit-testing, now fixed upstream in v0.2.5) - **Types** - Wrapper types: `ChartGPUProps`, `ChartGPUHandle`, `ZoomRange` - Re-exported core types: `ChartGPUOptions`, `ChartGPUInstance`, `ChartGPUEventPayload`, `ChartGPUCrosshairMovePayload`, - `DataPoint`, `OHLCDataPoint`, `AnnotationConfig`, `DataZoomConfig`, etc. + `ChartGPUZoomRangeChangePayload`, `ChartSyncOptions`, `ChartGPUHitTestResult`, `DataPoint`, `OHLCDataPoint`, `AnnotationConfig`, `DataZoomConfig`, etc. ## Task → doc page mapping @@ -58,4 +58,3 @@ The goal of this `llm-context.md` file is to give Context7 (and other LLM toolin - Legacy component: `src/ChartGPUChart.tsx` - Hooks: `src/useChartGPU.ts`, `src/useConnectCharts.ts` - Public wrapper types: `src/types.ts` - diff --git a/docs/assets/chartgpu.png b/docs/assets/chartgpu.png new file mode 100644 index 0000000..5fa0e5b Binary files /dev/null and b/docs/assets/chartgpu.png differ diff --git a/docs/assets/powered-by-webgpu.svg b/docs/assets/powered-by-webgpu.svg new file mode 100644 index 0000000..044bf6d --- /dev/null +++ b/docs/assets/powered-by-webgpu.svg @@ -0,0 +1 @@ +POWERED BYWEBGPU diff --git a/docs/recipes/chart-sync.md b/docs/recipes/chart-sync.md index 887caa0..342de60 100644 --- a/docs/recipes/chart-sync.md +++ b/docs/recipes/chart-sync.md @@ -7,10 +7,13 @@ 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: -- [`useConnectCharts`](../api/hooks.md#useconnectchartscharts) +- [`useConnectCharts`](../api/hooks.md#useconnectchartscharts-syncoptions) - [Crosshair move recipe](./crosshair-move.md) +- [dataZoom basics](./datazoom-basics.md) ## Option A: `useConnectCharts` (recommended) @@ -55,9 +58,23 @@ export function SyncedCharts() { } ``` +### Zoom sync + +To also sync zoom/pan across charts, pass `syncOptions`: + +```tsx +useConnectCharts([a, b], { syncZoom: true }); +``` + +This keeps both crosshair and zoom range in sync. To sync only zoom: + +```tsx +useConnectCharts([a, b], { syncCrosshair: false, syncZoom: true }); +``` + ## Option B: manual `connectCharts(...)` -`connectCharts` is a helper from the peer dependency `chartgpu`. `chartgpu-react` re-exports it for convenience: +`connectCharts` is a helper from the peer dependency `@chartgpu/chartgpu`. `chartgpu-react` re-exports it for convenience: ```ts import { connectCharts } from 'chartgpu-react'; @@ -77,7 +94,8 @@ export function ManualSync() { useEffect(() => { if (!a || a.disposed) return; if (!b || b.disposed) return; - const disconnect = connectCharts([a, b]); + // Pass syncOptions as the second argument (optional) + const disconnect = connectCharts([a, b], { syncZoom: true }); return () => disconnect(); }, [a, b]); @@ -101,8 +119,20 @@ export function ManualSync() { } ``` +## `ChartSyncOptions` + +```ts +type ChartSyncOptions = Readonly<{ + syncCrosshair?: boolean; // default true + syncZoom?: boolean; // default false +}>; +``` + +- **`syncCrosshair`** (default `true`): sync crosshair + tooltip x across charts. +- **`syncZoom`** (default `false`): sync zoom/pan range across charts. + ## Notes - Always disconnect on cleanup to avoid leaking listeners. - Only connect charts that are initialized and not disposed. - +- When `syncZoom` is enabled, all connected charts should have compatible `dataZoom` configs for best results. diff --git a/docs/recipes/datazoom-basics.md b/docs/recipes/datazoom-basics.md index a334fa5..47f2da5 100644 --- a/docs/recipes/datazoom-basics.md +++ b/docs/recipes/datazoom-basics.md @@ -5,6 +5,7 @@ Enable `dataZoom` to allow users to zoom/pan through data. The `ChartGPU` React Related: - [`ChartGPU` props](../api/chartgpu-component.md#props) +- [`ChartGPUHandle`](../api/chartgpu-handle.md) — programmatic zoom via `setZoomRange` - [Crosshair move recipe](./crosshair-move.md) ## Example @@ -54,6 +55,6 @@ export function DataZoomExample() { ## Notes -- `onZoomChange` is implemented by polling `chart.getZoomRange()` every 100ms and detecting changes. +- `onZoomChange` subscribes to the native `zoomRangeChange` event on the chart instance (introduced in ChartGPU v0.2.5). No polling is involved. - If zoom is disabled, `getZoomRange()` returns `null` and the callback will not fire. - +- You can programmatically control the zoom range via the imperative ref: `chartRef.current?.setZoomRange(start, end)` (percent-space, 0–100). See [`ChartGPUHandle.setZoomRange`](../api/chartgpu-handle.md#setzoomrangestart-number-end-number-void). diff --git a/examples/main.tsx b/examples/main.tsx index 32bb13b..43a6be0 100644 --- a/examples/main.tsx +++ b/examples/main.tsx @@ -8,7 +8,7 @@ import type { ChartGPUOptions, ScatterPointTuple, } from '../src'; -import type { OHLCDataPoint } from 'chartgpu'; +import type { OHLCDataPoint } from '@chartgpu/chartgpu'; type Candle = Extract< OHLCDataPoint, diff --git a/package-lock.json b/package-lock.json index b33c81d..313b76c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,18 +1,18 @@ { "name": "chartgpu-react", - "version": "0.1.0", + "version": "0.1.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "chartgpu-react", - "version": "0.1.0", + "version": "0.1.1", "license": "MIT", "devDependencies": { + "@chartgpu/chartgpu": "^0.2.5", "@types/react": "^18.2.66", "@types/react-dom": "^18.2.22", "@vitejs/plugin-react": "^4.2.1", - "chartgpu": "^0.2.3", "react": "^18.2.0", "react-dom": "^18.2.0", "typescript": "^5.3.3", @@ -20,7 +20,7 @@ "vite-plugin-dts": "^3.7.3" }, "peerDependencies": { - "chartgpu": ">=0.2.3 <0.3.0", + "@chartgpu/chartgpu": "^0.2.5", "react": ">=18.0.0", "react-dom": ">=18.0.0" } @@ -270,6 +270,13 @@ "node": ">=6.9.0" } }, + "node_modules/@chartgpu/chartgpu": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/@chartgpu/chartgpu/-/chartgpu-0.2.5.tgz", + "integrity": "sha512-PEe3m/NeJWtDyYVu81ozFTAmGo6JMYMWI1fmgCuDvenYBTdX832CT0+Ngs1jYBI2+cNhcfA0qRA00hu6N/F/ZA==", + "dev": true, + "license": "MIT" + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.21.5", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", @@ -1547,11 +1554,6 @@ ], "license": "CC-BY-4.0" }, - "node_modules/chartgpu": { - "version": "0.2.3", - "dev": true, - "license": "MIT" - }, "node_modules/commander": { "version": "9.5.0", "dev": true, diff --git a/package.json b/package.json index c1f17a9..988891f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "chartgpu-react", - "version": "0.1.1", + "version": "0.1.2", "description": "React bindings for ChartGPU - WebGPU-powered charting library", "license": "MIT", "author": "", @@ -30,7 +30,7 @@ "preview": "vite preview" }, "peerDependencies": { - "chartgpu": ">=0.2.3 <0.3.0", + "@chartgpu/chartgpu": "^0.2.5", "react": ">=18.0.0", "react-dom": ">=18.0.0" }, @@ -38,7 +38,7 @@ "@types/react": "^18.2.66", "@types/react-dom": "^18.2.22", "@vitejs/plugin-react": "^4.2.1", - "chartgpu": "^0.2.3", + "@chartgpu/chartgpu": "^0.2.5", "react": "^18.2.0", "react-dom": "^18.2.0", "typescript": "^5.3.3", diff --git a/src/ChartGPU.tsx b/src/ChartGPU.tsx index e206edc..096a338 100644 --- a/src/ChartGPU.tsx +++ b/src/ChartGPU.tsx @@ -6,7 +6,7 @@ import { forwardRef, useCallback, } from 'react'; -import { ChartGPU as ChartGPULib } from 'chartgpu'; +import { ChartGPU as ChartGPULib } from '@chartgpu/chartgpu'; import type { ChartGPUProps, ChartGPUHandle, @@ -14,7 +14,7 @@ import type { ClickParams, MouseOverParams, } from './types'; -import type { ChartGPUOptions } from 'chartgpu'; +import type { ChartGPUOptions, ChartGPUZoomRangeChangePayload } from '@chartgpu/chartgpu'; /** * Debounce utility for throttling frequent calls. @@ -45,7 +45,7 @@ function debounce void>( * - Automatic resize handling via ResizeObserver * - Theme support with options override * - Declarative event handlers - * - Zoom change detection via polling + * - Zoom change detection via event subscription * - Imperative methods via forwardRef * * Example usage: @@ -82,12 +82,6 @@ export const ChartGPU = forwardRef( const [chart, setChart] = useState(null); const mountedRef = useRef(false); const resizeObserverRef = useRef(null); - const zoomPollIntervalRef = useRef | null>( - null - ); - const lastZoomRangeRef = useRef>( - null - ); // Expose imperative handle useImperativeHandle( @@ -107,6 +101,39 @@ export const ChartGPU = forwardRef( instance.setOption(newOptions); } }, + setZoomRange: (start: number, end: number) => { + const instance = instanceRef.current; + if (instance && !instance.disposed) { + instance.setZoomRange(start, end); + } + }, + setInteractionX: (x: number | null, source?: unknown) => { + const instance = instanceRef.current; + if (instance && !instance.disposed) { + instance.setInteractionX(x, source); + } + }, + getInteractionX: () => { + const instance = instanceRef.current; + if (instance && !instance.disposed) { + return instance.getInteractionX(); + } + return null; + }, + hitTest: (e: PointerEvent | MouseEvent) => { + const instance = instanceRef.current; + if (!instance || instance.disposed) { + return { + isInGrid: false, + canvasX: NaN, + canvasY: NaN, + gridX: NaN, + gridY: NaN, + match: null, + }; + } + return instance.hitTest(e); + }, }), [] ); @@ -190,8 +217,10 @@ export const ChartGPU = forwardRef( instance.on('click', handler); return () => { - if (instance && !instance.disposed) { + try { instance.off('click', handler); + } catch { + // instance may already be disposed; swallow } }; }, [chart, onClick]); @@ -208,8 +237,10 @@ export const ChartGPU = forwardRef( instance.on('mouseover', handler); return () => { - if (instance && !instance.disposed) { + try { instance.off('mouseover', handler); + } catch { + // instance may already be disposed; swallow } }; }, [chart, onMouseOver]); @@ -226,8 +257,10 @@ export const ChartGPU = forwardRef( instance.on('mouseout', handler); return () => { - if (instance && !instance.disposed) { + try { instance.off('mouseout', handler); + } catch { + // instance may already be disposed; swallow } }; }, [chart, onMouseOut]); @@ -244,8 +277,10 @@ export const ChartGPU = forwardRef( instance.on('crosshairMove', handler); return () => { - if (instance && !instance.disposed) { + try { instance.off('crosshairMove', handler); + } catch { + // instance may already be disposed; swallow } }; }, [chart, onCrosshairMove]); @@ -277,49 +312,34 @@ export const ChartGPU = forwardRef( // eslint-disable-next-line react-hooks/exhaustive-deps }, [chart]); // Re-run when instance changes - // Set up zoom change polling (100ms interval) + // Register/unregister zoomRangeChange event handler. + // Also emits the current zoom range once on subscribe (initial hydration) + // so consumers don't need to wait for user interaction to receive the first value. useEffect(() => { const instance = chart; if (!instance || instance.disposed || !onZoomChange) return; - const checkZoomChange = () => { - if (!instance || instance.disposed) return; - - const currentRange = instance.getZoomRange(); - const lastRange = lastZoomRangeRef.current; - - // Check if zoom range changed - if (currentRange !== null) { - if ( - lastRange === null || - lastRange.start !== currentRange.start || - lastRange.end !== currentRange.end - ) { - lastZoomRangeRef.current = currentRange; - onZoomChange(currentRange); - } - } else { - // Range is null (no zoom), reset last range - if (lastRange !== null) { - lastZoomRangeRef.current = null; - } - } + const handler = (payload: ChartGPUZoomRangeChangePayload) => { + // Map upstream payload to ZoomRange (strip `source`) + onZoomChange({ start: payload.start, end: payload.end }); }; - const intervalId = setInterval(checkZoomChange, 100); - zoomPollIntervalRef.current = intervalId; + instance.on('zoomRangeChange', handler); - // Initial check - checkZoomChange(); + // Hydrate: fire once with the current zoom range (if non-null) + const current = instance.getZoomRange(); + if (current) { + onZoomChange({ start: current.start, end: current.end }); + } return () => { - if (zoomPollIntervalRef.current) { - clearInterval(zoomPollIntervalRef.current); - zoomPollIntervalRef.current = null; + try { + instance.off('zoomRangeChange', handler); + } catch { + // instance may already be disposed; swallow } - lastZoomRangeRef.current = null; }; - }, [chart, onZoomChange]); // Re-run when instance or callback changes + }, [chart, onZoomChange]); return (
; diff --git a/src/index.ts b/src/index.ts index b34e708..7a30b1b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -36,20 +36,28 @@ export { ChartGPUChart } from './ChartGPUChart'; export type { ChartGPUChartProps } from './ChartGPUChart'; // Re-export chartgpu helpers (avoid colliding with our `ChartGPU` React component) -export { createChart, connectCharts } from 'chartgpu'; +export { createChart, connectCharts } from '@chartgpu/chartgpu'; export { createAnnotationAuthoring } from './createAnnotationAuthoring'; -// Re-export types from chartgpu for convenience +// Re-export types from @chartgpu/chartgpu for convenience // This provides a single import point for all ChartGPU types export type { // Core instance and options ChartGPUInstance, ChartGPUOptions, - + // Event payloads ChartGPUEventPayload, ChartGPUCrosshairMovePayload, - + ChartGPUZoomRangeChangePayload, + + // Hit testing + ChartGPUHitTestResult, + ChartGPUHitTestMatch, + + // Chart sync + ChartSyncOptions, + // Annotation authoring AnnotationAuthoringInstance, AnnotationAuthoringOptions, @@ -63,20 +71,38 @@ export type { ScatterSeriesConfig, CandlestickSeriesConfig, SeriesConfig, - + + // Style configurations + LineStyleConfig, + AreaStyleConfig, + // Data types DataPoint, OHLCDataPoint, ScatterPointTuple, - + // Zoom / interaction DataZoomConfig, + // Legend + LegendConfig, + LegendPosition, + + // Animation + AnimationConfig, + + // Tooltip + TooltipConfig, + TooltipParams, + + // Performance + PerformanceMetrics, + // Theme configuration ThemeConfig, ThemeName, - + // Axis and grid configurations AxisConfig, GridConfig, -} from 'chartgpu'; +} from '@chartgpu/chartgpu'; diff --git a/src/types.ts b/src/types.ts index 745b77a..8bea7b6 100644 --- a/src/types.ts +++ b/src/types.ts @@ -8,10 +8,11 @@ import type { ChartGPUOptions, ChartGPUInstance, ChartGPUEventPayload, + ChartGPUHitTestResult, DataPoint, OHLCDataPoint, ChartGPUCrosshairMovePayload, -} from 'chartgpu'; +} from '@chartgpu/chartgpu'; /** * Bivariant callback helper (matches React's event handler variance behavior). @@ -163,6 +164,38 @@ export interface ChartGPUHandle { * @param options - New complete chart configuration */ setOption(options: ChartGPUOptions): void; + + /** + * Programmatically set the zoom range (percent-space). + * No-op when zoom is disabled on the chart. + * + * @param start - Start of zoom range (0-100) + * @param end - End of zoom range (0-100) + */ + setZoomRange(start: number, end: number): void; + + /** + * Programmatically drive the crosshair / tooltip to a domain-space x value. + * Passing `null` clears the crosshair. + * + * @param x - Domain-space x value, or null to clear + * @param source - Optional source identifier (useful for sync disambiguation) + */ + setInteractionX(x: number | null, source?: unknown): void; + + /** + * Read the current interaction x (domain units), or `null` when inactive. + */ + getInteractionX(): number | null; + + /** + * Perform hit-testing on a pointer or mouse event. + * Returns coordinates and matched chart element (if any). + * + * @param e - Pointer or mouse event to test + * @returns Hit-test result with coordinates and optional match + */ + hitTest(e: PointerEvent | MouseEvent): ChartGPUHitTestResult; } /** @@ -176,4 +209,4 @@ export type { ChartGPUCrosshairMovePayload, DataPoint, OHLCDataPoint, -} from 'chartgpu'; +} from '@chartgpu/chartgpu'; diff --git a/src/useChartGPU.ts b/src/useChartGPU.ts index d7934bb..5ed43a4 100644 --- a/src/useChartGPU.ts +++ b/src/useChartGPU.ts @@ -1,6 +1,6 @@ import { useEffect, useRef, useState } from 'react'; -import { ChartGPU as ChartGPULib } from 'chartgpu'; -import type { ChartGPUOptions } from 'chartgpu'; +import { ChartGPU as ChartGPULib } from '@chartgpu/chartgpu'; +import type { ChartGPUOptions } from '@chartgpu/chartgpu'; import type { ChartInstance } from './types'; /** diff --git a/src/useConnectCharts.ts b/src/useConnectCharts.ts index 4153611..37c9c97 100644 --- a/src/useConnectCharts.ts +++ b/src/useConnectCharts.ts @@ -1,18 +1,23 @@ import { useEffect, useRef } from 'react'; -import { connectCharts } from 'chartgpu'; -import type { ChartGPUInstance } from 'chartgpu'; +import { connectCharts } from '@chartgpu/chartgpu'; +import type { ChartGPUInstance, ChartSyncOptions } from '@chartgpu/chartgpu'; type DisconnectCharts = ReturnType; /** - * React hook to connect multiple ChartGPU instances for synced crosshair/tooltip x. + * React hook to connect multiple ChartGPU instances for synced interactions. + * + * Supports optional `syncOptions` to control which interactions are synced: + * - `syncCrosshair` (default `true`): sync crosshair + tooltip x across charts + * - `syncZoom` (default `false`): sync zoom/pan across charts * * Safety: * - Will not connect until all instances exist and are not disposed. - * - Automatically disconnects on unmount and when the set of instances changes. + * - Automatically disconnects on unmount and when the set of instances or options change. */ export function useConnectCharts( - charts: ReadonlyArray + charts: ReadonlyArray, + syncOptions?: ChartSyncOptions ): void { const disconnectRef = useRef(null); const idsRef = useRef>(new WeakMap()); @@ -27,13 +32,18 @@ export function useConnectCharts( }; // Build a stable signature so callers can pass new arrays without forcing reconnect. - const signature = charts - .map((c) => { - if (!c) return 'null'; - const id = getId(c); - return `${id}:${c.disposed ? 1 : 0}`; - }) - .join('|'); + // Include syncOptions so changes to them trigger reconnection. + const optionsSig = syncOptions + ? `cr=${syncOptions.syncCrosshair ?? ''},zm=${syncOptions.syncZoom ?? ''}` + : ''; + const signature = + charts + .map((c) => { + if (!c) return 'null'; + const id = getId(c); + return `${id}:${c.disposed ? 1 : 0}`; + }) + .join('|') + `|${optionsSig}`; useEffect(() => { // Always tear down any previous connection first. @@ -51,7 +61,7 @@ export function useConnectCharts( } try { - const disconnect = connectCharts(resolved); + const disconnect = connectCharts(resolved, syncOptions); disconnectRef.current = disconnect; return () => { disconnect(); @@ -60,10 +70,23 @@ export function useConnectCharts( } catch (err) { // Avoid crashing render trees if upstream throws (e.g. mismatched chart state). // Consumers can still manually call connectCharts if they need error handling. - console.error('useConnectCharts: failed to connect charts', err); + // Only log in development; tree-shaken in production builds + try { + if ( + // @ts-expect-error -- process may not exist in browser environments + typeof process === 'undefined' || process.env?.NODE_ENV !== 'production' + ) { + // eslint-disable-next-line no-console + console.error('useConnectCharts: failed to connect charts', err); + } + } catch { + // process access threw; assume dev + // eslint-disable-next-line no-console + console.error('useConnectCharts: failed to connect charts', err); + } return; } - // `charts` is intentionally not a dependency; `signature` captures identity + disposed state. + // `charts` is intentionally not a dependency; `signature` captures identity + disposed state + options. // eslint-disable-next-line react-hooks/exhaustive-deps }, [signature]); } diff --git a/vite.config.ts b/vite.config.ts index 47cd046..c1d090a 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -34,7 +34,7 @@ export default defineConfig(({ command }) => { fileName: () => 'index.js', }, rollupOptions: { - external: ['react', 'react-dom', 'react/jsx-runtime', 'chartgpu'], + external: ['react', 'react-dom', 'react/jsx-runtime', '@chartgpu/chartgpu'], output: { preserveModules: false, },