@wick-charts/react 0.3.6 → 0.4.1

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.6",
3
+ "version": "0.4.1",
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": {
@@ -8,7 +8,7 @@
8
8
  "url": "git+https://github.com/mo4islona/wick-charts.git",
9
9
  "directory": "packages/react"
10
10
  },
11
- "homepage": "https://mo4islona.github.io/wick-charts/",
11
+ "homepage": "https://wick-charts.eeff.io/",
12
12
  "bugs": "https://github.com/mo4islona/wick-charts/issues",
13
13
  "keywords": [
14
14
  "charts",
@@ -49,7 +49,7 @@
49
49
  "react-dom": ">=18.0.0"
50
50
  },
51
51
  "devDependencies": {
52
- "@wick-charts/core": "^0.3.6"
52
+ "@wick-charts/core": "^0.4.1"
53
53
  },
54
54
  "scripts": {
55
55
  "build": "vite build"
package/src/BarSeries.tsx CHANGED
@@ -1,6 +1,12 @@
1
1
  import { useEffect, useLayoutEffect, useRef } from 'react';
2
2
 
3
- import { type BarSeriesOptions, type TimePoint, normalizeTime } from '@wick-charts/core';
3
+ import {
4
+ type BarSeriesOptions,
5
+ EMPTY_SYNC_STATE,
6
+ type SeriesSyncState,
7
+ type TimePoint,
8
+ syncSeriesLayer,
9
+ } from '@wick-charts/core';
4
10
 
5
11
  import { useChartInstance } from './context';
6
12
 
@@ -13,30 +19,19 @@ export interface BarSeriesProps {
13
19
  id?: string;
14
20
  }
15
21
 
16
- /** Only fall back to a full `setSeriesData` replace when more than this many new
17
- * points appear in a single tick — otherwise streamed updates would always look
18
- * like bulk loads and the renderer would clear its entrance-animation entries. */
19
- const BULK_THRESHOLD = 20;
20
-
21
22
  export function BarSeries({ data, options, id: idProp }: BarSeriesProps) {
22
23
  const chart = useChartInstance();
23
24
  const seriesRef = useRef<string | null>(null);
24
- const prevLensRef = useRef<number[]>([]);
25
- const prevFirstTimesRef = useRef<(number | null)[]>([]);
26
- const prevLastTimesRef = useRef<(number | null)[]>([]);
25
+ const prevSyncRef = useRef<SeriesSyncState[]>([]);
27
26
 
28
27
  useLayoutEffect(() => {
29
- const id = chart.addBarSeries({ ...options, layers: data.length, id: idProp });
28
+ const id = chart.addSeries('bar', { ...options, layers: data.length, id: idProp });
30
29
  seriesRef.current = id;
31
- prevLensRef.current = new Array(data.length).fill(0);
32
- prevFirstTimesRef.current = new Array(data.length).fill(null);
33
- prevLastTimesRef.current = new Array(data.length).fill(null);
30
+ prevSyncRef.current = new Array(data.length).fill(EMPTY_SYNC_STATE);
34
31
  return () => {
35
32
  chart.removeSeries(id);
36
33
  seriesRef.current = null;
37
- prevLensRef.current = [];
38
- prevFirstTimesRef.current = [];
39
- prevLastTimesRef.current = [];
34
+ prevSyncRef.current = [];
40
35
  };
41
36
  }, [chart, data.length, idProp]);
42
37
 
@@ -46,44 +41,13 @@ export function BarSeries({ data, options, id: idProp }: BarSeriesProps) {
46
41
 
47
42
  chart.batch(() => {
48
43
  for (let i = 0; i < data.length; i++) {
49
- const layer = data[i];
50
- const prevLen = prevLensRef.current[i] ?? 0;
51
- const prevFirst = prevFirstTimesRef.current[i] ?? null;
52
-
53
- if (layer.length === 0) {
54
- chart.setSeriesData(id, [], i);
55
- prevLensRef.current[i] = 0;
56
- prevFirstTimesRef.current[i] = null;
57
- prevLastTimesRef.current[i] = null;
58
- continue;
59
- }
60
-
61
- const firstTime = normalizeTime(layer[0].time);
62
- const lastTime = normalizeTime(layer[layer.length - 1].time);
63
- const prevLast = prevLastTimesRef.current[i] ?? null;
64
- const shifted = prevFirst !== null && prevFirst !== firstTime;
65
- const added = layer.length - prevLen;
66
- const hasNewLast = prevLast !== null && prevLast !== lastTime;
67
-
68
- // Rolling-window slide (maxPoints cap): drop oldest, append newest,
69
- // length unchanged. Sync prefix then appendData the new tail so the
70
- // entrance animation fires instead of getting wiped by setSeriesData.
71
- if (shifted && added === 0 && hasNewLast) {
72
- chart.setSeriesData(id, layer.slice(0, -1), i);
73
- chart.appendData(id, layer[layer.length - 1], i);
74
- } else if (prevLen === 0 || layer.length < prevLen || added > BULK_THRESHOLD || shifted) {
75
- chart.setSeriesData(id, layer, i);
76
- } else if (layer.length === prevLen) {
77
- chart.updateData(id, layer[layer.length - 1], i);
78
- } else {
79
- for (let j = prevLen; j < layer.length; j++) {
80
- chart.appendData(id, layer[j], i);
81
- }
82
- }
83
-
84
- prevLensRef.current[i] = layer.length;
85
- prevFirstTimesRef.current[i] = firstTime;
86
- prevLastTimesRef.current[i] = lastTime;
44
+ prevSyncRef.current[i] = syncSeriesLayer({
45
+ chart,
46
+ id,
47
+ data: data[i],
48
+ prev: prevSyncRef.current[i] ?? EMPTY_SYNC_STATE,
49
+ layerIndex: i,
50
+ });
87
51
  }
88
52
  });
89
53
  }, [chart, data]);
@@ -98,9 +62,7 @@ export function BarSeries({ data, options, id: idProp }: BarSeriesProps) {
98
62
  options?.barWidthRatio,
99
63
  options?.stacking,
100
64
  options?.entryAnimation,
101
- options?.enterAnimation,
102
65
  options?.entryMs,
103
- options?.enterMs,
104
66
  options?.smoothMs,
105
67
  ]);
106
68
 
@@ -1,16 +1,15 @@
1
1
  import { useEffect, useLayoutEffect, useRef } from 'react';
2
2
 
3
- import type { CandlestickSeriesOptions, OHLCInput } from '@wick-charts/core';
4
- import { normalizeTime } from '@wick-charts/core';
3
+ import {
4
+ type CandlestickSeriesOptions,
5
+ EMPTY_SYNC_STATE,
6
+ type OHLCInput,
7
+ type SeriesSyncState,
8
+ syncSeriesLayer,
9
+ } from '@wick-charts/core';
5
10
 
6
11
  import { useChartInstance } from './context';
7
12
 
8
- /** Only fall back to a full `setSeriesData` replace when more than this many new
9
- * candles appear in a single tick. Streamed bursts (OHLCStream emits up to ~8
10
- * per 500ms) must stay under this so their appendData path still fires entrance
11
- * animations; history loads (50/batch) deliberately exceed it. */
12
- const BULK_THRESHOLD = 20;
13
-
14
13
  export interface CandlestickSeriesProps {
15
14
  /** OHLC candles to render. Each element carries `time/open/high/low/close` and an optional `volume`. */
16
15
  data: OHLCInput[];
@@ -23,19 +22,15 @@ export interface CandlestickSeriesProps {
23
22
  export function CandlestickSeries({ data, options, id: idProp }: CandlestickSeriesProps) {
24
23
  const chart = useChartInstance();
25
24
  const seriesRef = useRef<string | null>(null);
26
- const prevLenRef = useRef(0);
27
- const prevFirstTimeRef = useRef<number | null>(null);
28
- const prevLastTimeRef = useRef<number | null>(null);
25
+ const prevSyncRef = useRef<SeriesSyncState>(EMPTY_SYNC_STATE);
29
26
 
30
27
  useLayoutEffect(() => {
31
- const id = chart.addCandlestickSeries({ ...options, id: idProp });
28
+ const id = chart.addSeries('candlestick', { ...options, id: idProp });
32
29
  seriesRef.current = id;
33
30
  return () => {
34
31
  chart.removeSeries(id);
35
32
  seriesRef.current = null;
36
- prevLenRef.current = 0;
37
- prevFirstTimeRef.current = null;
38
- prevLastTimeRef.current = null;
33
+ prevSyncRef.current = EMPTY_SYNC_STATE;
39
34
  };
40
35
  }, [chart, idProp]);
41
36
 
@@ -43,46 +38,7 @@ export function CandlestickSeries({ data, options, id: idProp }: CandlestickSeri
43
38
  const id = seriesRef.current;
44
39
  if (!id) return;
45
40
 
46
- if (data.length === 0) {
47
- // Explicit clear
48
- chart.setSeriesData(id, []);
49
- prevLenRef.current = 0;
50
- prevFirstTimeRef.current = null;
51
- prevLastTimeRef.current = null;
52
- return;
53
- }
54
-
55
- const prevLen = prevLenRef.current;
56
- const prevFirst = prevFirstTimeRef.current;
57
- const prevLast = prevLastTimeRef.current;
58
- const firstTime = normalizeTime(data[0].time);
59
- const lastTime = normalizeTime(data[data.length - 1].time);
60
- const shifted = prevFirst !== null && prevFirst !== firstTime;
61
- const added = data.length - prevLen;
62
- const hasNewLast = prevLast !== null && prevLast !== lastTime;
63
-
64
- // Rolling-window slide: same array length but first AND last timestamps
65
- // advanced (old point dropped, new point appended). Must NOT fall through
66
- // to a full `setSeriesData` — that would wipe the entrance-animation
67
- // entries. Sync the stable prefix, then appendData the fresh tail so the
68
- // renderer registers an entry for just the new point.
69
- if (shifted && added === 0 && hasNewLast) {
70
- chart.setSeriesData(id, data.slice(0, -1));
71
- chart.appendData(id, data[data.length - 1]);
72
- } else if (prevLen === 0 || data.length < prevLen || added > BULK_THRESHOLD || shifted) {
73
- chart.setSeriesData(id, data);
74
- } else if (data.length === prevLen) {
75
- // Same length, same timestamps — last candle updated in place.
76
- chart.updateData(id, data[data.length - 1]);
77
- } else {
78
- for (let i = prevLen; i < data.length; i++) {
79
- chart.appendData(id, data[i]);
80
- }
81
- }
82
-
83
- prevLenRef.current = data.length;
84
- prevFirstTimeRef.current = firstTime;
85
- prevLastTimeRef.current = lastTime;
41
+ prevSyncRef.current = syncSeriesLayer({ chart, id, data, prev: prevSyncRef.current });
86
42
  }, [chart, data]);
87
43
 
88
44
  useEffect(() => {
@@ -100,9 +56,7 @@ export function CandlestickSeries({ data, options, id: idProp }: CandlestickSeri
100
56
  options?.down?.wick,
101
57
  options?.bodyWidthRatio,
102
58
  options?.entryAnimation,
103
- options?.enterAnimation,
104
59
  options?.entryMs,
105
- options?.enterMs,
106
60
  options?.smoothMs,
107
61
  ]);
108
62
 
@@ -18,6 +18,7 @@ import {
18
18
  type ChartOptions,
19
19
  type ChartTheme,
20
20
  type EdgeReachedInfo,
21
+ type VisibleRangeSpec,
21
22
  } from '@wick-charts/core';
22
23
 
23
24
  type PerfOption = NonNullable<ChartOptions['perf']>;
@@ -63,6 +64,30 @@ export interface ChartContainerProps {
63
64
  */
64
65
  left?: number | { intervals: number };
65
66
  };
67
+ /**
68
+ * Viewport-level streaming behavior. Captured at mount only — changing this
69
+ * prop after the chart is created is ignored.
70
+ */
71
+ viewport?: {
72
+ /**
73
+ * Width of the visible window in data bars, set on the first data load
74
+ * to `maxVisibleBars * dataInterval`. While the dataset is smaller than
75
+ * this width, streaming ticks render into the empty right-side gap and
76
+ * the viewport stays put; once the data reaches the right edge, the
77
+ * viewport pans forward to keep the latest bar pinned (tail-scroll).
78
+ * Default: 200.
79
+ */
80
+ maxVisibleBars?: number;
81
+ /**
82
+ * Initial visible range applied before the first paint with data. Same
83
+ * shape as the imperative `chart.setVisibleRange` — pass a bar count
84
+ * (e.g. `35`), an explicit `{from, to}` window, or `{from, bars}` for
85
+ * a warm-up pair. The standard alternative is calling
86
+ * `setVisibleRange` from a `useEffect`, but that runs post-paint and
87
+ * makes the chart visibly re-zoom on the next RAF. Captured at mount.
88
+ */
89
+ initialRange?: VisibleRangeSpec;
90
+ };
66
91
  /** Show the chart background gradient. Defaults to true. */
67
92
  gradient?: boolean;
68
93
  /** Enable zoom, pan, and crosshair interactions. Defaults to true. */
@@ -82,33 +107,16 @@ export interface ChartContainerProps {
82
107
  */
83
108
  headerLayout?: 'overlay' | 'inline';
84
109
  /**
85
- * Chart-level animation configuration. See {@link AnimationsConfig} for the
86
- * full shape.
87
- *
88
- * Two layers remember which is which:
89
- *
90
- * - **Chart-level (this prop)** — `animations.points.{enterMs, smoothMs,
91
- * pulseMs}` and `animations.viewport.{reboundMs, yAxisMs,
92
- * inputResponseMs}`. Acts as the default for every series.
93
- * - **Per-series** — `<LineSeries options={{ entryMs, smoothMs, pulseMs }}>`
94
- * (and the analogous CandlestickSeries / BarSeries options). Overrides
95
- * the chart-level default for that one series. Note the spelling:
96
- * `entryMs` per-series, `enterMs` chart-level — historical artefact,
97
- * both refer to the same animation.
98
- *
99
- * Resolution: per-series option wins over chart-level numeric value.
100
- * Chart-level wins only when its category is explicitly `false` — that's
101
- * a hard disable that overrides per-series too.
110
+ * Animation control. `true` / omitted uses built-in defaults; `false`
111
+ * disables every category. Per-series options on `<LineSeries>` /
112
+ * `<CandlestickSeries>` / `<BarSeries>` override these chart-level
113
+ * defaults unless the category here is explicitly `false`.
102
114
  *
103
- * Shorthands:
104
- * - `true` / omitted — built-in defaults (every settling animation 250 ms,
105
- * pulse cycle 600 ms, input ease 0 / off).
106
- * - `false` disables every animation category.
107
- * - `{ points: false }` / `{ viewport: false }` disables a category.
108
- *
109
- * Runtime updates: changing this prop after mount calls
110
- * `chart.setAnimations(...)` so the new durations take effect on the next
111
- * animation / render.
115
+ * **Init-only by reference identity.** A new `animations` object
116
+ * recreates the underlying `ChartInstance` (and its canvas). Wrap the
117
+ * value in `useMemo(() => ({...}), [deps])` so an unstable parent
118
+ * render doesn't tear down the chart every commit. In dev mode the
119
+ * container emits a console warning when it detects >3 recreates / s.
112
120
  */
113
121
  animations?: boolean | AnimationsConfig;
114
122
  /**
@@ -227,6 +235,7 @@ export function ChartContainer({
227
235
  theme,
228
236
  axis,
229
237
  padding,
238
+ viewport,
230
239
  gradient = true,
231
240
  interactive,
232
241
  grid,
@@ -249,15 +258,33 @@ export function ChartContainer({
249
258
  const chartRef = useRef<ChartInstance | null>(null);
250
259
  const [_, setRevision] = useState(0);
251
260
 
252
- // useLayoutEffect synchronous, runs before paint.
261
+ // Dev-only: warn when `animations` reference identity changes more than
262
+ // three times per second. The most common cause is a parent re-rendering
263
+ // with a fresh inline object; ChartInstance is no longer reconfigurable
264
+ // post-construction, so each new reference is a full canvas teardown.
265
+ const recreateStampsRef = useRef<number[]>([]);
266
+
267
+ // useLayoutEffect — synchronous, runs before paint. Re-runs when
268
+ // `animations` changes by reference: chart-level animation timings, the
269
+ // Y transition factory and per-series timings are init-only contracts
270
+ // in Phase 2, so a new `animations` object is a full rebuild.
271
+ //
272
+ // TODO(api): this identity-based init-only contract is fragile — any
273
+ // streaming caller that forgets to wrap `animations` in useMemo gets
274
+ // a destroy+recreate on every tick (see docs/pages/stress/streaming.tsx
275
+ // for the worked example: ~30 rebuilds/sec at 32ms intervals). Either
276
+ // diff structurally here, or split the init-only sub-fields (the Y
277
+ // transition factory and per-series timings) into a separate prop with
278
+ // a name that signals "stable identity required", so the common
279
+ // chart-level timings can stay live-updatable.
253
280
  useLayoutEffect(() => {
254
281
  if (!containerRef.current) return;
255
- if (chartRef.current) return;
256
282
 
257
283
  const options: ChartOptions = {};
258
284
  if (axis) options.axis = axis;
259
285
  if (resolvedTheme) options.theme = resolvedTheme;
260
286
  if (padding) options.padding = padding;
287
+ if (viewport) options.viewport = viewport;
261
288
  if (interactive !== undefined) options.interactive = interactive;
262
289
  if (grid !== undefined) options.grid = grid;
263
290
  if (perfRef.current !== undefined) options.perf = perfRef.current;
@@ -265,24 +292,31 @@ export function ChartContainer({
265
292
  if (onEdgeReachedRef.current) options.onEdgeReached = onEdgeReachedRef.current;
266
293
  chartRef.current = new ChartInstance(containerRef.current, options);
267
294
 
268
- // Note: the init path above already propagated `grid` into the chart. The
269
- // effect below handles live updates, but also needs to run on the same
270
- // commit so an initial `grid={{visible:false}}` isn't silently reset.
295
+ if (process.env.NODE_ENV !== 'production') {
296
+ const now = performance.now();
297
+ const stamps = recreateStampsRef.current;
298
+ stamps.push(now);
299
+ while (stamps.length > 0 && now - stamps[0] > 1000) stamps.shift();
300
+ if (stamps.length > 3) {
301
+ console.warn(
302
+ '[wick-charts] <ChartContainer> recreated the chart >3 times in the last second. ' +
303
+ 'The `animations` prop is init-only — wrap it in useMemo(() => ({...}), [deps]) ' +
304
+ 'so a stable reference identity prevents tear-down on every render.',
305
+ );
306
+ }
307
+ }
308
+
309
+ // The init path above already propagated `grid` into the chart. The
310
+ // effect below also writes it for live updates, but it needs to fire
311
+ // on the same commit so an initial `grid={{visible:false}}` isn't
312
+ // silently reset.
271
313
  setRevision((r) => r + 1);
272
314
 
273
315
  return () => {
274
- // Destroy synchronously. A previous revision deferred this through
275
- // `setTimeout(..., 0)` to "tolerate StrictMode" but the guard was
276
- // broken: in the StrictMode remount sequence (cleanup → second mount →
277
- // timeout), the check `if (!chartRef.current) instance.destroy()`
278
- // always saw the second instance and skipped the destroy — leaking
279
- // the first ChartInstance's canvases (hence 4 canvases per chart in
280
- // dev). StrictMode exists precisely to exercise cleanup; a correct
281
- // `destroy` is cheap enough to run on every cycle.
282
316
  chartRef.current?.destroy();
283
317
  chartRef.current = null;
284
318
  };
285
- }, []);
319
+ }, [animations]);
286
320
 
287
321
  useEffect(() => {
288
322
  if (chartRef.current && resolvedTheme) {
@@ -296,16 +330,6 @@ export function ChartContainer({
296
330
  }
297
331
  }, [axis?.y?.width, axis?.y?.min, axis?.y?.max, axis?.y?.visible, axis?.x?.height, axis?.x?.visible]);
298
332
 
299
- useEffect(() => {
300
- if (chartRef.current && animations !== undefined) {
301
- chartRef.current.setAnimations(animations);
302
- }
303
- // Dep array is the JSON shape of the config — covers both the boolean
304
- // shorthand and the full object. Cheap to stringify (the object is tiny)
305
- // and lets callers pass a fresh reference each render without thrashing
306
- // animator state when nothing has actually changed.
307
- }, [JSON.stringify(animations)]);
308
-
309
333
  // Top-overlay height (title + info bar) — measured below. Declared here so
310
334
  // the padding effect can fold it into `padding.top`.
311
335
  const topOverlayRef = useRef<HTMLDivElement>(null);
@@ -318,7 +342,12 @@ export function ChartContainer({
318
342
  // fire redundant `chart.setPadding(...)` calls (headerExtra stays 0).
319
343
  const headerExtra = headerLayout === 'overlay' ? topOverlayHeight : 0;
320
344
 
321
- useEffect(() => {
345
+ // useLayoutEffect (not useEffect) so the header-height fold-in lands
346
+ // before the browser paints the chart for the first time. With
347
+ // `useEffect` the padding update would fire AFTER paint, causing a
348
+ // visible "chart drawn, then everything shifts down by the header
349
+ // height on the next frame" jump on initial mount.
350
+ useLayoutEffect(() => {
322
351
  const current = chartRef.current;
323
352
  if (!current) return;
324
353
  const userTop = padding?.top ?? 20;
@@ -1,6 +1,12 @@
1
1
  import { useEffect, useLayoutEffect, useRef } from 'react';
2
2
 
3
- import { type LineSeriesOptions, type TimePoint, normalizeTime } from '@wick-charts/core';
3
+ import {
4
+ EMPTY_SYNC_STATE,
5
+ type LineSeriesOptions,
6
+ type SeriesSyncState,
7
+ type TimePoint,
8
+ syncSeriesLayer,
9
+ } from '@wick-charts/core';
4
10
 
5
11
  import { useChartInstance } from './context';
6
12
 
@@ -13,30 +19,19 @@ export interface LineSeriesProps {
13
19
  id?: string;
14
20
  }
15
21
 
16
- /** Only fall back to a full `setSeriesData` replace when more than this many new
17
- * points appear in a single tick — otherwise streamed updates would always look
18
- * like bulk loads and the renderer would clear its entrance-animation entries. */
19
- const BULK_THRESHOLD = 20;
20
-
21
22
  export function LineSeries({ data, options, id: idProp }: LineSeriesProps) {
22
23
  const chart = useChartInstance();
23
24
  const seriesRef = useRef<string | null>(null);
24
- const prevLensRef = useRef<number[]>([]);
25
- const prevFirstTimesRef = useRef<(number | null)[]>([]);
26
- const prevLastTimesRef = useRef<(number | null)[]>([]);
25
+ const prevSyncRef = useRef<SeriesSyncState[]>([]);
27
26
 
28
27
  useLayoutEffect(() => {
29
- const id = chart.addLineSeries({ ...options, layers: data.length, id: idProp });
28
+ const id = chart.addSeries('line', { ...options, layers: data.length, id: idProp });
30
29
  seriesRef.current = id;
31
- prevLensRef.current = new Array(data.length).fill(0);
32
- prevFirstTimesRef.current = new Array(data.length).fill(null);
33
- prevLastTimesRef.current = new Array(data.length).fill(null);
30
+ prevSyncRef.current = new Array(data.length).fill(EMPTY_SYNC_STATE);
34
31
  return () => {
35
32
  chart.removeSeries(id);
36
33
  seriesRef.current = null;
37
- prevLensRef.current = [];
38
- prevFirstTimesRef.current = [];
39
- prevLastTimesRef.current = [];
34
+ prevSyncRef.current = [];
40
35
  };
41
36
  }, [chart, data.length, idProp]);
42
37
 
@@ -46,44 +41,13 @@ export function LineSeries({ data, options, id: idProp }: LineSeriesProps) {
46
41
 
47
42
  chart.batch(() => {
48
43
  for (let i = 0; i < data.length; i++) {
49
- const layer = data[i];
50
- const prevLen = prevLensRef.current[i] ?? 0;
51
- const prevFirst = prevFirstTimesRef.current[i] ?? null;
52
-
53
- if (layer.length === 0) {
54
- chart.setSeriesData(id, [], i);
55
- prevLensRef.current[i] = 0;
56
- prevFirstTimesRef.current[i] = null;
57
- prevLastTimesRef.current[i] = null;
58
- continue;
59
- }
60
-
61
- const firstTime = normalizeTime(layer[0].time);
62
- const lastTime = normalizeTime(layer[layer.length - 1].time);
63
- const prevLast = prevLastTimesRef.current[i] ?? null;
64
- const shifted = prevFirst !== null && prevFirst !== firstTime;
65
- const added = layer.length - prevLen;
66
- const hasNewLast = prevLast !== null && prevLast !== lastTime;
67
-
68
- // Rolling-window slide (maxPoints cap): drop oldest, append newest,
69
- // length unchanged. Sync prefix then appendData the new tail so the
70
- // entrance animation fires instead of getting wiped by setSeriesData.
71
- if (shifted && added === 0 && hasNewLast) {
72
- chart.setSeriesData(id, layer.slice(0, -1), i);
73
- chart.appendData(id, layer[layer.length - 1], i);
74
- } else if (prevLen === 0 || layer.length < prevLen || added > BULK_THRESHOLD || shifted) {
75
- chart.setSeriesData(id, layer, i);
76
- } else if (layer.length === prevLen) {
77
- chart.updateData(id, layer[layer.length - 1], i);
78
- } else {
79
- for (let j = prevLen; j < layer.length; j++) {
80
- chart.appendData(id, layer[j], i);
81
- }
82
- }
83
-
84
- prevLensRef.current[i] = layer.length;
85
- prevFirstTimesRef.current[i] = firstTime;
86
- prevLastTimesRef.current[i] = lastTime;
44
+ prevSyncRef.current[i] = syncSeriesLayer({
45
+ chart,
46
+ id,
47
+ data: data[i],
48
+ prev: prevSyncRef.current[i] ?? EMPTY_SYNC_STATE,
49
+ layerIndex: i,
50
+ });
87
51
  }
88
52
  });
89
53
  }, [chart, data]);
@@ -101,9 +65,7 @@ export function LineSeries({ data, options, id: idProp }: LineSeriesProps) {
101
65
  options?.pulse,
102
66
  options?.stacking,
103
67
  options?.entryAnimation,
104
- options?.enterAnimation,
105
68
  options?.entryMs,
106
- options?.enterMs,
107
69
  options?.smoothMs,
108
70
  ]);
109
71
 
package/src/PieSeries.tsx CHANGED
@@ -19,7 +19,7 @@ export function PieSeries({ data, options, id: idProp }: PieSeriesProps) {
19
19
  const seriesRef = useRef<string | null>(null);
20
20
 
21
21
  useLayoutEffect(() => {
22
- const id = chart.addPieSeries({ ...options, id: idProp });
22
+ const id = chart.addSeries('pie', { ...options, id: idProp });
23
23
  seriesRef.current = id;
24
24
  return () => {
25
25
  chart.removeSeries(id);
@@ -48,7 +48,6 @@ export function PieSeries({ data, options, id: idProp }: PieSeriesProps) {
48
48
  options?.sliceLabels?.labelGap,
49
49
  options?.sliceLabels?.distance,
50
50
  options?.sliceLabels?.railWidth,
51
- options?.sliceLabels?.balanceSides,
52
51
  ]);
53
52
 
54
53
  useLayoutEffect(() => {
package/src/index.ts CHANGED
@@ -9,12 +9,12 @@
9
9
  */
10
10
 
11
11
  export type {
12
+ AnimationTime,
12
13
  AnimationsConfig,
13
14
  AxisBound,
14
15
  AxisConfig,
15
16
  BarSeriesOptions,
16
17
  /** @deprecated Use {@link StackingMode} instead. */
17
- BarStacking,
18
18
  BuildHoverSnapshotsArgs,
19
19
  BuildLastSnapshotsArgs,
20
20
  CandlestickSeriesOptions,
@@ -28,7 +28,6 @@ export type {
28
28
  HoverInfo,
29
29
  LegendItem,
30
30
  /** @deprecated Use {@link TimePoint} instead. */
31
- LineData,
32
31
  LineSeriesOptions,
33
32
  NavigatorCandlePoint,
34
33
  NavigatorControllerParams,
@@ -54,6 +53,9 @@ export type {
54
53
  TooltipFormatter,
55
54
  TooltipPosition,
56
55
  TooltipPositionArgs,
56
+ Transition,
57
+ TransitionContext,
58
+ TransitionFactory,
57
59
  Typography,
58
60
  ValueFormatter,
59
61
  VisibleRange,
@@ -72,7 +74,6 @@ export {
72
74
  catppuccin,
73
75
  computeTooltipPosition,
74
76
  createTheme,
75
- darkTheme,
76
77
  detectInterval,
77
78
  dracula,
78
79
  formatCompact,
@@ -82,11 +83,11 @@ export {
82
83
  githubLight,
83
84
  gruvbox,
84
85
  handwritten,
86
+ hermite,
85
87
  highContrast,
86
88
  isDarkBg,
87
89
  lavenderMist,
88
90
  lightPink,
89
- lightTheme,
90
91
  materialPalenight,
91
92
  minimalLight,
92
93
  mintBreeze,
@@ -95,6 +96,7 @@ export {
95
96
  normalizeTime,
96
97
  oneDarkPro,
97
98
  panda,
99
+ parseAnimationTime,
98
100
  peachCream,
99
101
  quietLight,
100
102
  resolveAxisFontSize,
@@ -102,7 +104,9 @@ export {
102
104
  resolveCandlestickBodyColor,
103
105
  rosePineDawn,
104
106
  sandDune,
107
+ snap,
105
108
  solarizedLight,
109
+ spring,
106
110
  } from '@wick-charts/core';
107
111
 
108
112
  export { BarSeries } from './BarSeries';
@@ -11,6 +11,10 @@ export function Crosshair() {
11
11
 
12
12
  const theme = chart.getTheme();
13
13
  const dataInterval = chart.getDataInterval();
14
+ // Format the time pill at the axis's *resolved* tick granularity, not the raw
15
+ // data interval — otherwise a time-of-day badge floats among date labels when
16
+ // zoomed out. Falls back to dataInterval on a degenerate range.
17
+ const tickInterval = chart.timeScale.niceTickValues(dataInterval).tickInterval || dataInterval;
14
18
 
15
19
  const labelStyle = {
16
20
  // Blend the theme's labelBackground at 80% opacity so the axis grid
@@ -54,7 +58,7 @@ export function Crosshair() {
54
58
  ...labelStyle,
55
59
  }}
56
60
  >
57
- {formatTime(position.time, dataInterval)}
61
+ {formatTime(position.time, tickInterval)}
58
62
  </div>
59
63
  </>
60
64
  );