@wick-charts/react 0.3.5 → 0.4.0

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.
@@ -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,
@@ -22,10 +22,12 @@ export type {
22
22
  ChartOptions,
23
23
  ChartTheme,
24
24
  CrosshairPosition,
25
+ EdgeReachedInfo,
26
+ EdgeSide,
27
+ EdgeState,
25
28
  HoverInfo,
26
29
  LegendItem,
27
30
  /** @deprecated Use {@link TimePoint} instead. */
28
- LineData,
29
31
  LineSeriesOptions,
30
32
  NavigatorCandlePoint,
31
33
  NavigatorControllerParams,
@@ -51,6 +53,9 @@ export type {
51
53
  TooltipFormatter,
52
54
  TooltipPosition,
53
55
  TooltipPositionArgs,
56
+ Transition,
57
+ TransitionContext,
58
+ TransitionFactory,
54
59
  Typography,
55
60
  ValueFormatter,
56
61
  VisibleRange,
@@ -69,7 +74,6 @@ export {
69
74
  catppuccin,
70
75
  computeTooltipPosition,
71
76
  createTheme,
72
- darkTheme,
73
77
  detectInterval,
74
78
  dracula,
75
79
  formatCompact,
@@ -79,10 +83,11 @@ export {
79
83
  githubLight,
80
84
  gruvbox,
81
85
  handwritten,
86
+ hermite,
82
87
  highContrast,
88
+ isDarkBg,
83
89
  lavenderMist,
84
90
  lightPink,
85
- lightTheme,
86
91
  materialPalenight,
87
92
  minimalLight,
88
93
  mintBreeze,
@@ -91,6 +96,7 @@ export {
91
96
  normalizeTime,
92
97
  oneDarkPro,
93
98
  panda,
99
+ parseAnimationTime,
94
100
  peachCream,
95
101
  quietLight,
96
102
  resolveAxisFontSize,
@@ -98,7 +104,9 @@ export {
98
104
  resolveCandlestickBodyColor,
99
105
  rosePineDawn,
100
106
  sandDune,
107
+ snap,
101
108
  solarizedLight,
109
+ spring,
102
110
  } from '@wick-charts/core';
103
111
 
104
112
  export { BarSeries } from './BarSeries';
@@ -107,6 +115,8 @@ export { CandlestickSeries } from './CandlestickSeries';
107
115
  export { ChartContainer } from './ChartContainer';
108
116
  // React hooks
109
117
  export { useChartInstance } from './context';
118
+ export type { EdgeLoaderProps, EdgeLoaderRenderArgs } from './EdgeLoader';
119
+ export { EdgeLoader } from './EdgeLoader';
110
120
  export { LineSeries } from './LineSeries';
111
121
  export { PieSeries } from './PieSeries';
112
122
  export {
@@ -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
  );
@@ -97,7 +97,41 @@ export function InfoBar({ sort = 'none', format = defaultInfoBarFormat, children
97
97
  }
98
98
  }
99
99
 
100
- if (snapshots.length === 0) return null;
100
+ // No data yet (initial mount, before the first series tick) render a
101
+ // zero-content placeholder *for the default UI only* so the header reserves
102
+ // its real height from the very first paint. Without this the header grows
103
+ // when InfoBar pops in, ChartContainer's header-measure observer fires
104
+ // `setPadding`, and the canvas + axes visibly shift down on the next RAF.
105
+ //
106
+ // The render-prop variant (`children`) keeps the legacy "return null"
107
+ // behavior — its layout depends on user-supplied JSX, so we can't
108
+ // synthesise a meaningful placeholder.
109
+ if (snapshots.length === 0) {
110
+ if (children) return null;
111
+
112
+ return (
113
+ <div
114
+ data-tooltip-legend=""
115
+ aria-hidden="true"
116
+ style={{
117
+ display: 'flex',
118
+ alignItems: 'center',
119
+ gap: 4,
120
+ padding: '4px 8px',
121
+ flexShrink: 0,
122
+ fontSize: theme.typography.fontSize,
123
+ fontFamily: theme.typography.fontFamily,
124
+ fontVariantNumeric: 'tabular-nums',
125
+ visibility: 'hidden',
126
+ pointerEvents: 'none',
127
+ }}
128
+ >
129
+ {/* Non-breaking space keeps line-height intact so the div claims its
130
+ real rendered height instead of collapsing to padding-only. */}
131
+ <span>&nbsp;</span>
132
+ </div>
133
+ );
134
+ }
101
135
 
102
136
  if (children) {
103
137
  return (
@@ -1,6 +1,12 @@
1
1
  import { type CSSProperties, useMemo } from 'react';
2
2
 
3
- import { type ChartTheme, type TimePoint, formatCompact, resolveCandlestickBodyColor } from '@wick-charts/core';
3
+ import {
4
+ type AxisBound,
5
+ type ChartTheme,
6
+ type TimePoint,
7
+ formatCompact,
8
+ resolveCandlestickBodyColor,
9
+ } from '@wick-charts/core';
4
10
 
5
11
  import { BarSeries } from '../BarSeries';
6
12
  import { ChartContainer } from '../ChartContainer';
@@ -12,6 +18,23 @@ export type SparklineValuePosition = 'left' | 'right' | 'none';
12
18
  export interface SparklineProps {
13
19
  /** Data points plotted by the sparkline. A flat `TimePoint[]` — the sparkline only ever shows one tiny line/bar. */
14
20
  data: TimePoint[];
21
+ /**
22
+ * Streaming-window mode: viewport is fixed at `capacity` bars wide. Pass
23
+ * at least two seed points in `data` so the initial window can infer the
24
+ * tick interval.
25
+ *
26
+ * `align` controls where the seed sits at mount:
27
+ * - `'right'` *(default)* — seed flush with the right edge; each tick
28
+ * shifts the viewport left by one interval and the new tick lands at
29
+ * the right edge.
30
+ * - `'left'` — seed flush with the left edge; the viewport is held in
31
+ * place until empty bars on the right are consumed, then normal
32
+ * tail-scroll resumes.
33
+ * - `'offscreen'` — seed starts one interval past the right edge so the
34
+ * first tick's tail-scroll animates it onto canvas (a brief "drive-in"
35
+ * effect).
36
+ */
37
+ flow?: { capacity: number; align?: 'left' | 'right' | 'offscreen' };
15
38
  /** Visual theme. Drives series colour, background gradient, and the change-direction colours used in the value block. */
16
39
  theme: ChartTheme;
17
40
  /** 'line' (default) or 'bar' */
@@ -35,6 +58,15 @@ export interface SparklineProps {
35
58
  };
36
59
  /** @deprecated Use {@link area} instead. */
37
60
  areaFill?: boolean;
61
+ /**
62
+ * Fixed Y-axis bounds. Omit a side (or pass `'auto'`) to keep that edge
63
+ * auto-scaled. Useful to pin a baseline (`{ min: 0 }`) or hold a stable
64
+ * window so streaming ticks don't rescale the line.
65
+ *
66
+ * Each bound is an {@link AxisBound}: a number, `'auto'`, a percentage
67
+ * offset string (`'+10%'`), or a `(values) => number` reducer.
68
+ */
69
+ yRange?: { min?: AxisBound; max?: AxisBound };
38
70
  /** Chart width (default: 140) */
39
71
  width?: number;
40
72
  /** Overall height (default: 48) */
@@ -77,6 +109,8 @@ export function Sparkline({
77
109
  negativeColor,
78
110
  area,
79
111
  areaFill,
112
+ yRange,
113
+ flow,
80
114
  width = 140,
81
115
  height = 48,
82
116
  strokeWidth = 1.5,
@@ -95,6 +129,58 @@ export function Sparkline({
95
129
  change.positive ? theme.candlestick.up.body : theme.candlestick.down.body,
96
130
  );
97
131
 
132
+ // Previously Sparkline kept its own running min/max in a useRef and handed
133
+ // a padded Y range to ChartContainer via `axis.y.{min,max}`. That worked
134
+ // around the chart's default auto-Y "jumps" on streamed wild values, but
135
+ // it had a hidden cost: every new data prop made the memo emit a fresh
136
+ // `{min, max}` object, which ChartContainer fed into `chart.setAxis`, and
137
+ // setAxis SNAPS Y (sets `#yInited = false` and calls `updateYRange(true)`).
138
+ // Result: every streaming tick snapped Y without animation, which is the
139
+ // jerky behaviour you saw. The chart core now has sticky-Y bounds + a
140
+ // `viewportChange` emit on Y advance, so the chart handles streaming
141
+ // stability itself — Sparkline can drop its local fix.
142
+
143
+ // Captured-at-mount viewport for flow mode. Three layouts, see the
144
+ // `flow.align` docstring on SparklineProps for the user-facing summary.
145
+ //
146
+ // - 'left' uses the `{ from, bars }` form, which arms the viewport's
147
+ // warm-up hold (#holdUntilFilled) so it stays put while empty bars on
148
+ // the right are consumed, then releases to normal tail-scroll.
149
+ // - 'right' and 'offscreen' use `{ from, to }`, which leaves the hold off
150
+ // so tail-scroll kicks in on the first tick. The only difference is
151
+ // `to`: at `last` the seed sits flush right; at `last - interval` the
152
+ // seed sits one interval past the right edge and the first tick's scroll
153
+ // animates it into view.
154
+ //
155
+ // Requires at least 2 seed points so `interval` can be inferred; falls
156
+ // back to undefined otherwise (chart fits to data normally). Subsequent
157
+ // renders don't recompute because ChartContainer ignores viewport prop
158
+ // changes after mount.
159
+ const viewport = useMemo(() => {
160
+ if (!flow || data.length < 2) return undefined;
161
+
162
+ const interval = data[1].time - data[0].time;
163
+ if (interval <= 0) return undefined;
164
+
165
+ const align = flow.align ?? 'right';
166
+
167
+ if (align === 'left') {
168
+ return {
169
+ maxVisibleBars: flow.capacity,
170
+ initialRange: { from: data[0].time, bars: flow.capacity } as const,
171
+ };
172
+ }
173
+
174
+ const last = data[data.length - 1].time;
175
+ const to = align === 'offscreen' ? last - interval : last;
176
+ const from = to - flow.capacity * interval;
177
+
178
+ return {
179
+ maxVisibleBars: flow.capacity,
180
+ initialRange: { from, to } as const,
181
+ };
182
+ }, []);
183
+
98
184
  const valueBlock = valuePosition !== 'none' && (
99
185
  <div
100
186
  style={{
@@ -166,13 +252,19 @@ export function Sparkline({
166
252
  <ChartContainer
167
253
  theme={theme}
168
254
  axis={{
169
- y: { visible: false, width: 0 },
255
+ // `min`/`max` are stable user props (not recomputed per tick), so
256
+ // ChartContainer's setAxis effect — keyed on the primitive bound
257
+ // values — only re-applies on an actual change, never per stream
258
+ // tick. This is why a fixed `yRange` is safe where the old
259
+ // recompute-every-update min/max was not (see note above).
260
+ y: { visible: false, width: 0, min: yRange?.min, max: yRange?.max },
170
261
  x: { visible: false, height: 0 },
171
262
  }}
172
263
  padding={{ top: 5, right: 0, bottom: 0, left: 0 }}
173
264
  gradient={gradient}
174
265
  interactive={false}
175
266
  grid={{ visible: false }}
267
+ viewport={viewport}
176
268
  >
177
269
  {variant === 'line' ? (
178
270
  <LineSeries
@@ -192,6 +284,7 @@ export function Sparkline({
192
284
  colors: [resolvedColor, resolvedNegColor],
193
285
  barWidthRatio: 0.7,
194
286
  stacking: 'off',
287
+ anchor: 'right',
195
288
  }}
196
289
  />
197
290
  )}
@@ -1,16 +1,9 @@
1
1
  import { useLayoutEffect, useRef } from 'react';
2
2
 
3
- import { formatTime, resolveAxisFontSize, resolveAxisTextColor } from '@wick-charts/core';
3
+ import { mountAxisLabels } from '@wick-charts/core';
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';
8
-
9
- interface TrackedTick {
10
- opacity: number;
11
- addedAt: number;
12
- fadedAt?: number;
13
- }
14
7
 
15
8
  export interface TimeAxisProps {
16
9
  /** Desired number of labels (≥ 2). Overrides chart-level `axis.x.labelCount`. */
@@ -21,7 +14,11 @@ export interface TimeAxisProps {
21
14
 
22
15
  export function TimeAxis({ labelCount, minLabelSpacing }: TimeAxisProps = {}) {
23
16
  const chart = useChartInstance();
24
- useVisibleRange(chart); // subscribe to viewport changes so ticks re-render
17
+ // Subscribe so the container re-renders when chart geometry shifts
18
+ // (yAxisWidth / xAxisHeight can change on resize, legend mount, etc.).
19
+ useVisibleRange(chart);
20
+
21
+ const containerRef = useRef<HTMLDivElement | null>(null);
25
22
 
26
23
  useLayoutEffect(() => {
27
24
  chart.setTimeAxisLabelDensity({
@@ -33,49 +30,17 @@ export function TimeAxis({ labelCount, minLabelSpacing }: TimeAxisProps = {}) {
33
30
  chart.setTimeAxisLabelDensity({ labelCount: null, minLabelSpacing: null });
34
31
  };
35
32
  }, [chart, labelCount, minLabelSpacing]);
36
- const theme = chart.getTheme();
37
- const dataInterval = chart.getDataInterval();
38
- const { ticks: currentTicks, tickInterval } = chart.timeScale.niceTickValues(dataInterval);
39
- const currentSet = new Set(currentTicks);
40
-
41
- // Persistent map: tick value → tracked state
42
- const mapRef = useRef<Map<number, TrackedTick>>(new Map());
43
- const map = mapRef.current;
44
- const now = performance.now();
45
33
 
46
- // Mark current ticks as visible
47
- for (const t of currentTicks) {
48
- if (!map.has(t)) {
49
- map.set(t, { opacity: 1, addedAt: now });
50
- } else {
51
- map.get(t)!.opacity = 1;
52
- }
53
- }
54
-
55
- // Mark missing ticks for fade-out
56
- for (const [t, entry] of map) {
57
- if (!currentSet.has(t)) {
58
- if (entry.opacity !== 0) {
59
- entry.opacity = 0;
60
- entry.fadedAt = now;
61
- }
62
- }
63
- }
64
-
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.
68
- for (const [t, entry] of map) {
69
- if (entry.opacity === 0 && entry.fadedAt !== undefined && now - entry.fadedAt > AXIS_LABEL_CLEANUP_MS) {
70
- map.delete(t);
71
- }
72
- }
34
+ useLayoutEffect(() => {
35
+ const container = containerRef.current;
36
+ if (container === null) return;
73
37
 
74
- // Collect all ticks to render (current + fading out)
75
- const allTicks = Array.from(map.entries());
38
+ return mountAxisLabels({ chart, container, axis: 'x' });
39
+ }, [chart]);
76
40
 
77
41
  return (
78
42
  <div
43
+ ref={containerRef}
79
44
  style={{
80
45
  position: 'absolute',
81
46
  left: 0,
@@ -86,30 +51,6 @@ export function TimeAxis({ labelCount, minLabelSpacing }: TimeAxisProps = {}) {
86
51
  display: 'flex',
87
52
  alignItems: 'center',
88
53
  }}
89
- >
90
- {allTicks.map(([time, entry]) => {
91
- const x = chart.timeScale.timeToX(time);
92
- return (
93
- <span
94
- key={time}
95
- style={{
96
- position: 'absolute',
97
- left: x,
98
- transform: 'translateX(-50%)',
99
- color: resolveAxisTextColor(theme, 'x'),
100
- fontSize: resolveAxisFontSize(theme, 'x'),
101
- fontFamily: theme.typography.fontFamily,
102
- userSelect: 'none',
103
- whiteSpace: 'nowrap',
104
- opacity: entry.opacity,
105
- transition: AXIS_LABEL_FADE_CSS,
106
- willChange: 'opacity',
107
- }}
108
- >
109
- {formatTime(time, tickInterval)}
110
- </span>
111
- );
112
- })}
113
- </div>
54
+ />
114
55
  );
115
56
  }
package/src/ui/Title.tsx CHANGED
@@ -1,4 +1,4 @@
1
- import type { CSSProperties, ReactNode } from 'react';
1
+ import { type CSSProperties, type ReactNode, memo } from 'react';
2
2
 
3
3
  import { useTheme } from '../ThemeContext';
4
4
 
@@ -32,31 +32,35 @@ export interface TitleProps {
32
32
  * </ChartContainer>
33
33
  * ```
34
34
  */
35
- export function Title({ children, sub, style }: TitleProps) {
36
- const theme = useTheme();
37
- return (
38
- <div
39
- data-chart-title=""
40
- style={{
41
- display: 'flex',
42
- alignItems: 'baseline',
43
- gap: 6,
44
- padding: '6px 8px 4px',
45
- flexShrink: 0,
46
- fontFamily: theme.typography.fontFamily,
47
- fontSize: theme.typography.fontSize,
48
- fontWeight: 600,
49
- color: theme.tooltip.textColor,
50
- pointerEvents: 'none',
51
- ...style,
52
- }}
53
- >
54
- {children != null && children !== false && <span>{children}</span>}
55
- {sub != null && sub !== false && (
56
- <span style={{ fontWeight: 400, color: theme.axis.textColor, fontSize: theme.axis.fontSize }}>
57
- {sub}
58
- </span>
59
- )}
60
- </div>
61
- );
62
- }
35
+ export const Title = memo(
36
+ function Title({ children, sub, style }: TitleProps) {
37
+ const theme = useTheme();
38
+ return (
39
+ <div
40
+ data-chart-title=""
41
+ style={{
42
+ display: 'flex',
43
+ alignItems: 'baseline',
44
+ gap: 6,
45
+ padding: '6px 8px 0',
46
+ flexShrink: 0,
47
+ fontFamily: theme.typography.fontFamily,
48
+ fontSize: theme.typography.fontSize,
49
+ fontWeight: 600,
50
+ color: theme.tooltip.textColor,
51
+ pointerEvents: 'none',
52
+ ...style,
53
+ }}
54
+ >
55
+ {children != null && children !== false && <span>{children}</span>}
56
+ {sub != null && sub !== false && (
57
+ <span style={{ fontWeight: 400, color: theme.axis.textColor, fontSize: theme.axis.fontSize }}>{sub}</span>
58
+ )}
59
+ </div>
60
+ );
61
+ },
62
+ // Explicit comparator: equivalent to default shallow-compare, but avoids the
63
+ // dev-only Profiler/highlight noise observed with bare `memo` (see
64
+ // facebook/react#19778).
65
+ (prev, next) => prev.children === next.children && prev.sub === next.sub && prev.style === next.style,
66
+ );