@wick-charts/react 0.3.2 → 0.3.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wick-charts/react",
3
- "version": "0.3.2",
3
+ "version": "0.3.4",
4
4
  "description": "High-performance canvas timeseries charts for React — candlestick, line, bar, pie. Tree-shakeable, zero runtime deps.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -49,7 +49,7 @@
49
49
  "react-dom": ">=18.0.0"
50
50
  },
51
51
  "devDependencies": {
52
- "@wick-charts/core": "^0.3.2"
52
+ "@wick-charts/core": "^0.3.4"
53
53
  },
54
54
  "scripts": {
55
55
  "build": "vite build"
@@ -11,7 +11,13 @@ import {
11
11
  useState,
12
12
  } from 'react';
13
13
 
14
- import { type AxisConfig, ChartInstance, type ChartOptions, type ChartTheme } from '@wick-charts/core';
14
+ import {
15
+ type AnimationsConfig,
16
+ type AxisConfig,
17
+ ChartInstance,
18
+ type ChartOptions,
19
+ type ChartTheme,
20
+ } from '@wick-charts/core';
15
21
 
16
22
  type PerfOption = NonNullable<ChartOptions['perf']>;
17
23
 
@@ -74,6 +80,36 @@ export interface ChartContainerProps {
74
80
  * the title. The chart background still spans the full container.
75
81
  */
76
82
  headerLayout?: 'overlay' | 'inline';
83
+ /**
84
+ * Chart-level animation configuration. See {@link AnimationsConfig} for the
85
+ * full shape.
86
+ *
87
+ * Two layers — remember which is which:
88
+ *
89
+ * - **Chart-level (this prop)** — `animations.points.{enterMs, smoothMs,
90
+ * pulseMs}` and `animations.viewport.{reboundMs, yAxisMs,
91
+ * inputResponseMs}`. Acts as the default for every series.
92
+ * - **Per-series** — `<LineSeries options={{ entryMs, smoothMs, pulseMs }}>`
93
+ * (and the analogous CandlestickSeries / BarSeries options). Overrides
94
+ * the chart-level default for that one series. Note the spelling:
95
+ * `entryMs` per-series, `enterMs` chart-level — historical artefact,
96
+ * both refer to the same animation.
97
+ *
98
+ * Resolution: per-series option wins over chart-level numeric value.
99
+ * Chart-level wins only when its category is explicitly `false` — that's
100
+ * a hard disable that overrides per-series too.
101
+ *
102
+ * Shorthands:
103
+ * - `true` / omitted — built-in defaults (every settling animation 250 ms,
104
+ * pulse cycle 600 ms, input ease 0 / off).
105
+ * - `false` — disables every animation category.
106
+ * - `{ points: false }` / `{ viewport: false }` — disables a category.
107
+ *
108
+ * Runtime updates: changing this prop after mount calls
109
+ * `chart.setAnimations(...)` so the new durations take effect on the next
110
+ * animation / render.
111
+ */
112
+ animations?: boolean | AnimationsConfig;
77
113
  /**
78
114
  * Enable runtime performance instrumentation. Off by default.
79
115
  *
@@ -183,6 +219,7 @@ export function ChartContainer({
183
219
  grid,
184
220
  headerLayout = 'overlay',
185
221
  perf,
222
+ animations,
186
223
  style,
187
224
  className,
188
225
  }: ChartContainerProps) {
@@ -208,6 +245,7 @@ export function ChartContainer({
208
245
  if (interactive !== undefined) options.interactive = interactive;
209
246
  if (grid !== undefined) options.grid = grid;
210
247
  if (perfRef.current !== undefined) options.perf = perfRef.current;
248
+ if (animations !== undefined) options.animations = animations;
211
249
  chartRef.current = new ChartInstance(containerRef.current, options);
212
250
 
213
251
  // Note: the init path above already propagated `grid` into the chart. The
@@ -241,6 +279,16 @@ export function ChartContainer({
241
279
  }
242
280
  }, [axis?.y?.width, axis?.y?.min, axis?.y?.max, axis?.y?.visible, axis?.x?.height, axis?.x?.visible]);
243
281
 
282
+ useEffect(() => {
283
+ if (chartRef.current && animations !== undefined) {
284
+ chartRef.current.setAnimations(animations);
285
+ }
286
+ // Dep array is the JSON shape of the config — covers both the boolean
287
+ // shorthand and the full object. Cheap to stringify (the object is tiny)
288
+ // and lets callers pass a fresh reference each render without thrashing
289
+ // animator state when nothing has actually changed.
290
+ }, [JSON.stringify(animations)]);
291
+
244
292
  // Top-overlay height (title + info bar) — measured below. Declared here so
245
293
  // the padding effect can fold it into `padding.top`.
246
294
  const topOverlayRef = useRef<HTMLDivElement>(null);
package/src/index.ts CHANGED
@@ -9,6 +9,7 @@
9
9
  */
10
10
 
11
11
  export type {
12
+ AnimationsConfig,
12
13
  AxisBound,
13
14
  AxisConfig,
14
15
  BarSeriesOptions,
@@ -4,6 +4,7 @@ import { formatTime, resolveAxisFontSize, resolveAxisTextColor } from '@wick-cha
4
4
 
5
5
  import { useChartInstance } from '../context';
6
6
  import { useVisibleRange } from '../store-bridge';
7
+ import { AXIS_LABEL_CLEANUP_MS, AXIS_LABEL_FADE_CSS } from './axisFade';
7
8
 
8
9
  interface TrackedTick {
9
10
  opacity: number;
@@ -61,9 +62,11 @@ export function TimeAxis({ labelCount, minLabelSpacing }: TimeAxisProps = {}) {
61
62
  }
62
63
  }
63
64
 
64
- // Clean up ticks that have finished fading (400ms CSS transition + buffer)
65
+ // Clean up ticks that have finished fading. Buffer = AXIS_LABEL_FADE_MS + 250
66
+ // (one transition + a frame margin) so the DOM node sticks around past the
67
+ // visible fade.
65
68
  for (const [t, entry] of map) {
66
- if (entry.opacity === 0 && entry.fadedAt !== undefined && now - entry.fadedAt > 600) {
69
+ if (entry.opacity === 0 && entry.fadedAt !== undefined && now - entry.fadedAt > AXIS_LABEL_CLEANUP_MS) {
67
70
  map.delete(t);
68
71
  }
69
72
  }
@@ -99,7 +102,7 @@ export function TimeAxis({ labelCount, minLabelSpacing }: TimeAxisProps = {}) {
99
102
  userSelect: 'none',
100
103
  whiteSpace: 'nowrap',
101
104
  opacity: entry.opacity,
102
- transition: 'opacity 0.3s ease',
105
+ transition: AXIS_LABEL_FADE_CSS,
103
106
  willChange: 'opacity',
104
107
  }}
105
108
  >
package/src/ui/YAxis.tsx CHANGED
@@ -4,6 +4,7 @@ import { resolveAxisFontSize, resolveAxisTextColor, type ValueFormatter } from '
4
4
 
5
5
  import { useChartInstance } from '../context';
6
6
  import { useYRange } from '../store-bridge';
7
+ import { AXIS_LABEL_CLEANUP_MS, AXIS_LABEL_FADE_CSS } from './axisFade';
7
8
 
8
9
  interface TrackedTick {
9
10
  opacity: number;
@@ -74,8 +75,9 @@ export function YAxis({ format, labelCount, minLabelSpacing }: YAxisProps = {})
74
75
  }
75
76
  }
76
77
 
78
+ // Cleanup buffer matches the shared AXIS_LABEL_CLEANUP_MS — see axisFade.ts.
77
79
  for (const [p, entry] of map) {
78
- if (entry.opacity === 0 && entry.fadedAt !== undefined && now - entry.fadedAt > 600) {
80
+ if (entry.opacity === 0 && entry.fadedAt !== undefined && now - entry.fadedAt > AXIS_LABEL_CLEANUP_MS) {
79
81
  map.delete(p);
80
82
  }
81
83
  }
@@ -109,7 +111,7 @@ export function YAxis({ format, labelCount, minLabelSpacing }: YAxisProps = {})
109
111
  fontVariantNumeric: 'tabular-nums',
110
112
  userSelect: 'none',
111
113
  opacity: entry.opacity,
112
- transition: 'opacity 0.3s ease',
114
+ transition: AXIS_LABEL_FADE_CSS,
113
115
  willChange: 'opacity',
114
116
  }}
115
117
  >
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Axis-label fade timing — shared between {@link TimeAxis} and {@link YAxis}.
3
+ *
4
+ * The fade is a pure CSS opacity transition, not Animator-driven, because the
5
+ * label set itself is rebuilt on every render: a tick that "leaves" the
6
+ * range becomes a separate DOM node fading out while a new node fades in,
7
+ * and inline `transition` is the cheapest way to crossfade them without a
8
+ * per-tick Animator instance.
9
+ *
10
+ * The duration matches the chart-level `DEFAULT_ENTER_MS` / `streamTick` so
11
+ * label transitions land in lockstep with the X re-fit, Y range chase, and
12
+ * series live-track. Cleanup buffer leaves the node mounted past the
13
+ * visible fade so React doesn't unmount it mid-transition.
14
+ */
15
+
16
+ const AXIS_LABEL_FADE_MS = 250;
17
+
18
+ /** Inline `style.transition` value the axis label spans use. */
19
+ export const AXIS_LABEL_FADE_CSS = `opacity ${AXIS_LABEL_FADE_MS / 1000}s ease`;
20
+
21
+ /** Time after which a faded-out tick can be dropped from the persistent map.
22
+ * `2 * AXIS_LABEL_FADE_MS` — one transition plus a frame margin. */
23
+ export const AXIS_LABEL_CLEANUP_MS = AXIS_LABEL_FADE_MS * 2;