@wick-charts/react
Advanced tools
| /** | ||
| * Axis-label fade timing — shared between {@link TimeAxis} and {@link YAxis}. | ||
| * | ||
| * The fade is a pure CSS opacity transition, not Animator-driven, because the | ||
| * label set itself is rebuilt on every render: a tick that "leaves" the | ||
| * range becomes a separate DOM node fading out while a new node fades in, | ||
| * and inline `transition` is the cheapest way to crossfade them without a | ||
| * per-tick Animator instance. | ||
| * | ||
| * The duration matches the chart-level `DEFAULT_ENTER_MS` / `streamTick` so | ||
| * label transitions land in lockstep with the X re-fit, Y range chase, and | ||
| * series live-track. Cleanup buffer leaves the node mounted past the | ||
| * visible fade so React doesn't unmount it mid-transition. | ||
| */ | ||
| const AXIS_LABEL_FADE_MS = 250; | ||
| /** Inline `style.transition` value the axis label spans use. */ | ||
| export const AXIS_LABEL_FADE_CSS = `opacity ${AXIS_LABEL_FADE_MS / 1000}s ease`; | ||
| /** Time after which a faded-out tick can be dropped from the persistent map. | ||
| * `2 * AXIS_LABEL_FADE_MS` — one transition plus a frame margin. */ | ||
| export const AXIS_LABEL_CLEANUP_MS = AXIS_LABEL_FADE_MS * 2; |
+2
-2
| { | ||
| "name": "@wick-charts/react", | ||
| "version": "0.3.3", | ||
| "version": "0.3.4", | ||
| "description": "High-performance canvas timeseries charts for React — candlestick, line, bar, pie. Tree-shakeable, zero runtime deps.", | ||
@@ -52,3 +52,3 @@ "license": "MIT", | ||
| "devDependencies": { | ||
| "@wick-charts/core": "^0.3.3" | ||
| "@wick-charts/core": "^0.3.4" | ||
| }, | ||
@@ -55,0 +55,0 @@ "scripts": { |
+18
-164
@@ -5,16 +5,16 @@ # Wick Charts | ||
| High-performance timeseries charts for **React**, **Vue**, and **Svelte**. Canvas-rendered, tree-shakeable, ~36KB gzipped when tree-shaken. | ||
| High-performance timeseries charts for **React**, **Vue**, and **Svelte**. Canvas-rendered, tree-shakeable, zero runtime dependencies. | ||
| [Live Demo](https://mo4islona.github.io/wick-charts/) | ||
| [Live Demo](https://mo4islona.github.io/wick-charts/) · [Docs](https://mo4islona.github.io/wick-charts/#/api/chart-container) | ||
| ## Features | ||
| - **Candlestick, Line, Bar, Pie** — all from one package | ||
| - **Real-time streaming** — append/update data at 60fps | ||
| - **22 built-in themes** — dark, light, and custom | ||
| - **Candlestick, Line, Bar, Pie, Sparkline** — all from one package | ||
| - **Real-time streaming** — append/update at 60fps with coordinated animations | ||
| - **22 built-in themes** plus `createTheme()` for custom palettes | ||
| - **Interactive** — zoom, pan, crosshair, tooltips | ||
| - **Stacking** — normal and percent modes for line/bar | ||
| - **Custom-render helpers** — `buildHoverSnapshots` / `buildLastSnapshots` / `computeTooltipPosition` for overlays that need to escape the built-in UI (structural-equality cache included) | ||
| - **Custom-render slots** — keep the built-in positioning, replace the contents | ||
| - **Tree-shakeable** — import only what you use | ||
| - **Zero dependencies** — just your framework | ||
| - **Zero runtime dependencies** — just your framework | ||
@@ -48,166 +48,20 @@ ## Install | ||
| ## Series Types | ||
| ## API | ||
| | Component | Data Format | Description | | ||
| |---|---|---| | ||
| | `CandlestickSeries` | `{ time, open, high, low, close, volume? }[]` | OHLC candlesticks with volume bars | | ||
| | `LineSeries` | `{ time, value }[][]` | Line/area charts, multi-layer, stacking | | ||
| | `BarSeries` | `{ time, value }[][]` | Histogram/bar charts, stacking | | ||
| | `PieSeries` | `{ label, value, color? }[]` | Pie and donut charts | | ||
| Every component, prop, type, and slot context lives in the docs site: | ||
| ## UI Overlays | ||
| [mo4islona.github.io/wick-charts/#/api/chart-container](https://mo4islona.github.io/wick-charts/#/api/chart-container) | ||
| Every DOM overlay ships a default UI **and** a scoped slot / render-prop so you can replace the contents with your own layout. Positioning, crosshair wiring, and data computation stay in the library — the slot just hands you the already-computed data. | ||
| Start there for [ChartContainer](https://mo4islona.github.io/wick-charts/#/api/chart-container), then drill into the series ([Candlestick](https://mo4islona.github.io/wick-charts/#/api/candlestick-series), [Line](https://mo4islona.github.io/wick-charts/#/api/line-series), [Bar](https://mo4islona.github.io/wick-charts/#/api/bar-series), [Pie](https://mo4islona.github.io/wick-charts/#/api/pie-series), [Sparkline](https://mo4islona.github.io/wick-charts/#/api/sparkline)) and overlays ([Tooltip](https://mo4islona.github.io/wick-charts/#/api/tooltip), [InfoBar](https://mo4islona.github.io/wick-charts/#/api/info-bar), [Crosshair](https://mo4islona.github.io/wick-charts/#/api/crosshair), [Legend](https://mo4islona.github.io/wick-charts/#/api/legend), [YAxis](https://mo4islona.github.io/wick-charts/#/api/y-axis), [XAxis](https://mo4islona.github.io/wick-charts/#/api/x-axis), [Navigator](https://mo4islona.github.io/wick-charts/#/api/navigator), …). | ||
| | Component | Description | Slot ctx | | ||
| |---|---|---| | ||
| | `Tooltip` | Floating glass tooltip near cursor on hover | `{ snapshots, time }` | | ||
| | `InfoBar` | Compact OHLC / values info bar hoisted above the canvas | `{ snapshots, time, isHover }` | | ||
| | `Title` | Chart title / subtitle bar hoisted above the canvas | — | | ||
| | `Crosshair` | Axis labels at cursor position | — | | ||
| | `YAxis` | Vertical price/value axis with animated ticks | — | | ||
| | `TimeAxis` | Horizontal time axis with animated ticks | — | | ||
| | `YLabel` | Floating price badge with dashed line | `{ value, y, bgColor, isLive, direction, format }` | | ||
| | `Legend` | Clickable legend with toggle/isolate modes | `{ items: LegendItem[] }` | | ||
| | `PieTooltip` | Tooltip for pie/donut hover | `{ info, format }` | | ||
| | `PieLegend` | Slice labels with values or percentages | `{ slices, mode, format }` | | ||
| ## Bundle size | ||
| ## Custom render (slots / render-props) | ||
| Tree-shaken React scenarios via `pnpm size` (esbuild, minified, browser target, React/ReactDOM external): | ||
| ```tsx | ||
| // React — filter two of five series with your own layout | ||
| <Tooltip> | ||
| {({ snapshots, time }) => | ||
| snapshots | ||
| .filter((s) => s.seriesId === 'btc' || s.seriesId === 'eth') | ||
| .map((s) => ( | ||
| <div key={s.id} style={{ color: s.color }}> | ||
| {s.label}: {s.data.close ?? s.data.value} | ||
| </div> | ||
| )) | ||
| } | ||
| </Tooltip> | ||
| ``` | ||
| | Scenario | Raw | Gzip | Brotli | | ||
| |---|---:|---:|---:| | ||
| | Candlestick only | 147 KB | 44 KB | 38 KB | | ||
| | Line only | 147 KB | 44 KB | 38 KB | | ||
| | Full React | 164 KB | 49 KB | 41 KB | | ||
| Each overlay has its own slot context (see the Slot ctx column above); the shape is consistent across frameworks for the same overlay. | ||
| ### Public helpers (re-exported from each framework package) | ||
| - `buildHoverSnapshots(chart, { time, sort?, cacheKey })` / `buildLastSnapshots(chart, { sort?, cacheKey })` — structural-equality-cached snapshot arrays for building your own floating widgets. Calls with the same args return the **same reference** while the chart's overlay version is unchanged, so `React.memo` / Vue `computed` / Svelte `$:` skip renders on no-op mousemoves. | ||
| - `computeTooltipPosition({ x, y, chartWidth, chartHeight, tooltipWidth, tooltipHeight, offsetX?, offsetY? })` — flip + clamp positioning for a tooltip container you own. | ||
| - Types: `SeriesSnapshot`, `LegendItem`, `SliceInfo`, `HoverInfo`. | ||
| ## Custom number formatting | ||
| Every numeric overlay accepts a `format` prop so you can override the default label rendering. Two shared helpers ship in each framework package (`@wick-charts/react`, `@wick-charts/vue`, `@wick-charts/svelte`): | ||
| - `formatCompact(v)` — K/M/B/T suffixes with adaptive precision. Default for `YAxis` (at ranges ≥ 1e6), `PieLegend`, `PieTooltip`, `Sparkline`. | ||
| - `formatPriceAdaptive(v)` — full-precision display that scales decimals to the value's magnitude. Default for `Tooltip` / `InfoBar` OHLC and line-value cells. Handles sub-cent prices (`0.00001234` → `"0.00001234"`, not `"0.00"`). | ||
| ```tsx | ||
| import { Tooltip, YAxis, formatCompact } from '@wick-charts/react'; | ||
| <YAxis format={(v) => `$${formatCompact(v)}`} /> | ||
| <Tooltip format={(v, field) => field === 'volume' ? formatCompact(v) : v.toFixed(4)} /> | ||
| ``` | ||
| Tooltip / InfoBar pass a `field` arg (`'open' | 'high' | 'low' | 'close' | 'volume' | 'value'`) so you can branch on which cell you're formatting. All other overlays receive a single `value: number`. | ||
| ## Themes | ||
| 22 built-in themes. Import only the ones you need (tree-shakable) and pass them to `ChartContainer` or `ThemeProvider` for global theming. | ||
| ```tsx | ||
| import { catppuccin } from '@wick-charts/react'; | ||
| // Dark: andromeda, ayuMirage, catppuccin, dracula, gruvbox, highContrast, | ||
| // materialPalenight, monokaiPro, nightOwl, oneDarkPro, panda | ||
| // Light: githubLight, handwritten, lavenderMist, lightPink, minimalLight, mintBreeze, | ||
| // peachCream, quietLight, rosePineDawn, sandDune, solarizedLight | ||
| <ChartContainer theme={catppuccin.theme}> | ||
| ``` | ||
| Create custom themes with `createTheme()`: | ||
| ```tsx | ||
| import { createTheme } from '@wick-charts/react'; | ||
| const myTheme = createTheme({ | ||
| background: '#1a1b2e', | ||
| candlestick: { | ||
| up: { body: '#00d4aa' }, | ||
| down: { body: '#ff5577' }, | ||
| }, | ||
| axis: { textColor: '#8888aa' }, | ||
| }); | ||
| ``` | ||
| ## Real-Time Data | ||
| ```tsx | ||
| // Full replace (initial load) | ||
| <CandlestickSeries data={allCandles} /> | ||
| // The component auto-detects changes: | ||
| // - data.length grew by 1-5 → append | ||
| // - data.length same → update last point | ||
| // - data.length shrunk or grew by >5 → full replace | ||
| ``` | ||
| ## Batch Updates | ||
| ```tsx | ||
| const chart = useChartInstance(); | ||
| chart.batch(() => { | ||
| chart.setSeriesData(id, layer0, 0); | ||
| chart.setSeriesData(id, layer1, 1); | ||
| // Y-range and render happen once after batch | ||
| }); | ||
| ``` | ||
| ## Configuration | ||
| ```tsx | ||
| <ChartContainer | ||
| theme={theme} | ||
| axis={{ | ||
| y: { visible: true, width: 55, min: 0, max: 'auto' }, | ||
| x: { visible: true, height: 30 }, | ||
| }} | ||
| padding={{ top: 20, bottom: 20, right: { intervals: 3 }, left: { intervals: 0 } }} | ||
| gradient={true} | ||
| interactive={true} | ||
| grid={true} | ||
| > | ||
| ``` | ||
| ## Hooks | ||
| | Hook | Description | | ||
| |---|---| | ||
| | `useChartInstance()` | Access the `ChartInstance` from context | | ||
| | `useVisibleRange(chart)` | Current visible time range | | ||
| | `useYRange(chart)` | Current Y-axis min/max | | ||
| | `useLastYValue(chart, id)` | Last value + live status for a series | | ||
| | `usePreviousClose(chart, id)` | Previous close price | | ||
| | `useCrosshairPosition(chart)` | Crosshair coordinates and snapped time | | ||
| ## Bundle Size | ||
| Full `dist/index.js` (minified + gzipped): | ||
| | Package | Raw | Gzip | | ||
| |---|---|---| | ||
| | `@wick-charts/react` | 224 KB | 59.3 KB | | ||
| Tree-shaking on the consumer side cuts this down further — `pnpm size` builds representative React scenarios through esbuild with production settings: | ||
| | Scenario | Raw | Gzip | | ||
| |---|---|---| | ||
| | Candlestick only | 122.1 KB | 36.3 KB | | ||
| | Line only | 122.4 KB | 36.4 KB | | ||
| | Full React (all overlays) | 140.6 KB | 41.3 KB | | ||
| ## Migration | ||
@@ -214,0 +68,0 @@ |
@@ -14,3 +14,9 @@ import { | ||
| import { type AxisConfig, ChartInstance, type ChartOptions, type ChartTheme } from '@wick-charts/core'; | ||
| import { | ||
| type AnimationsConfig, | ||
| type AxisConfig, | ||
| ChartInstance, | ||
| type ChartOptions, | ||
| type ChartTheme, | ||
| } from '@wick-charts/core'; | ||
@@ -79,2 +85,32 @@ type PerfOption = NonNullable<ChartOptions['perf']>; | ||
| /** | ||
| * Chart-level animation configuration. See {@link AnimationsConfig} for the | ||
| * full shape. | ||
| * | ||
| * Two layers — remember which is which: | ||
| * | ||
| * - **Chart-level (this prop)** — `animations.points.{enterMs, smoothMs, | ||
| * pulseMs}` and `animations.viewport.{reboundMs, yAxisMs, | ||
| * inputResponseMs}`. Acts as the default for every series. | ||
| * - **Per-series** — `<LineSeries options={{ entryMs, smoothMs, pulseMs }}>` | ||
| * (and the analogous CandlestickSeries / BarSeries options). Overrides | ||
| * the chart-level default for that one series. Note the spelling: | ||
| * `entryMs` per-series, `enterMs` chart-level — historical artefact, | ||
| * both refer to the same animation. | ||
| * | ||
| * Resolution: per-series option wins over chart-level numeric value. | ||
| * Chart-level wins only when its category is explicitly `false` — that's | ||
| * a hard disable that overrides per-series too. | ||
| * | ||
| * Shorthands: | ||
| * - `true` / omitted — built-in defaults (every settling animation 250 ms, | ||
| * pulse cycle 600 ms, input ease 0 / off). | ||
| * - `false` — disables every animation category. | ||
| * - `{ points: false }` / `{ viewport: false }` — disables a category. | ||
| * | ||
| * Runtime updates: changing this prop after mount calls | ||
| * `chart.setAnimations(...)` so the new durations take effect on the next | ||
| * animation / render. | ||
| */ | ||
| animations?: boolean | AnimationsConfig; | ||
| /** | ||
| * Enable runtime performance instrumentation. Off by default. | ||
@@ -188,2 +224,3 @@ * | ||
| perf, | ||
| animations, | ||
| style, | ||
@@ -214,2 +251,3 @@ className, | ||
| if (perfRef.current !== undefined) options.perf = perfRef.current; | ||
| if (animations !== undefined) options.animations = animations; | ||
| chartRef.current = new ChartInstance(containerRef.current, options); | ||
@@ -248,2 +286,12 @@ | ||
| useEffect(() => { | ||
| if (chartRef.current && animations !== undefined) { | ||
| chartRef.current.setAnimations(animations); | ||
| } | ||
| // Dep array is the JSON shape of the config — covers both the boolean | ||
| // shorthand and the full object. Cheap to stringify (the object is tiny) | ||
| // and lets callers pass a fresh reference each render without thrashing | ||
| // animator state when nothing has actually changed. | ||
| }, [JSON.stringify(animations)]); | ||
| // Top-overlay height (title + info bar) — measured below. Declared here so | ||
@@ -250,0 +298,0 @@ // the padding effect can fold it into `padding.top`. |
+1
-0
@@ -12,2 +12,3 @@ /** | ||
| export type { | ||
| AnimationsConfig, | ||
| AxisBound, | ||
@@ -14,0 +15,0 @@ AxisConfig, |
@@ -7,2 +7,3 @@ import { useLayoutEffect, useRef } from 'react'; | ||
| import { useVisibleRange } from '../store-bridge'; | ||
| import { AXIS_LABEL_CLEANUP_MS, AXIS_LABEL_FADE_CSS } from './axisFade'; | ||
@@ -65,5 +66,7 @@ interface TrackedTick { | ||
| // Clean up ticks that have finished fading (400ms CSS transition + buffer) | ||
| // Clean up ticks that have finished fading. Buffer = AXIS_LABEL_FADE_MS + 250 | ||
| // (one transition + a frame margin) so the DOM node sticks around past the | ||
| // visible fade. | ||
| for (const [t, entry] of map) { | ||
| if (entry.opacity === 0 && entry.fadedAt !== undefined && now - entry.fadedAt > 600) { | ||
| if (entry.opacity === 0 && entry.fadedAt !== undefined && now - entry.fadedAt > AXIS_LABEL_CLEANUP_MS) { | ||
| map.delete(t); | ||
@@ -104,3 +107,3 @@ } | ||
| opacity: entry.opacity, | ||
| transition: 'opacity 0.3s ease', | ||
| transition: AXIS_LABEL_FADE_CSS, | ||
| willChange: 'opacity', | ||
@@ -107,0 +110,0 @@ }} |
+4
-2
@@ -7,2 +7,3 @@ import { useLayoutEffect, useRef } from 'react'; | ||
| import { useYRange } from '../store-bridge'; | ||
| import { AXIS_LABEL_CLEANUP_MS, AXIS_LABEL_FADE_CSS } from './axisFade'; | ||
@@ -78,4 +79,5 @@ interface TrackedTick { | ||
| // Cleanup buffer matches the shared AXIS_LABEL_CLEANUP_MS — see axisFade.ts. | ||
| for (const [p, entry] of map) { | ||
| if (entry.opacity === 0 && entry.fadedAt !== undefined && now - entry.fadedAt > 600) { | ||
| if (entry.opacity === 0 && entry.fadedAt !== undefined && now - entry.fadedAt > AXIS_LABEL_CLEANUP_MS) { | ||
| map.delete(p); | ||
@@ -114,3 +116,3 @@ } | ||
| opacity: entry.opacity, | ||
| transition: 'opacity 0.3s ease', | ||
| transition: AXIS_LABEL_FADE_CSS, | ||
| willChange: 'opacity', | ||
@@ -117,0 +119,0 @@ }} |
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
666244
3.95%29
3.57%13014
4.61%71
-67.28%