@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/README.md +6 -6
- package/dist/index.cjs +2 -2
- package/dist/index.d.ts +782 -344
- package/dist/index.js +4926 -4157
- package/package.json +3 -3
- package/src/BarSeries.tsx +18 -56
- package/src/CandlestickSeries.tsx +11 -57
- package/src/ChartContainer.tsx +80 -51
- package/src/LineSeries.tsx +18 -56
- package/src/PieSeries.tsx +1 -2
- package/src/index.ts +8 -4
- package/src/ui/Crosshair.tsx +5 -1
- package/src/ui/InfoBar.tsx +35 -1
- package/src/ui/Sparkline.tsx +95 -2
- package/src/ui/TimeAxis.tsx +13 -72
- package/src/ui/Title.tsx +33 -29
- package/src/ui/YAxis.tsx +11 -67
- package/src/ui/axisFade.ts +0 -23
package/src/ui/InfoBar.tsx
CHANGED
|
@@ -97,7 +97,41 @@ export function InfoBar({ sort = 'none', format = defaultInfoBarFormat, children
|
|
|
97
97
|
}
|
|
98
98
|
}
|
|
99
99
|
|
|
100
|
-
|
|
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> </span>
|
|
132
|
+
</div>
|
|
133
|
+
);
|
|
134
|
+
}
|
|
101
135
|
|
|
102
136
|
if (children) {
|
|
103
137
|
return (
|
package/src/ui/Sparkline.tsx
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import { type CSSProperties, useMemo } from 'react';
|
|
2
2
|
|
|
3
|
-
import {
|
|
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
|
-
|
|
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
|
)}
|
package/src/ui/TimeAxis.tsx
CHANGED
|
@@ -1,16 +1,9 @@
|
|
|
1
1
|
import { useLayoutEffect, useRef } from 'react';
|
|
2
2
|
|
|
3
|
-
import {
|
|
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
|
-
|
|
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
|
-
|
|
47
|
-
|
|
48
|
-
if (
|
|
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
|
-
|
|
75
|
-
|
|
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
|
|
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
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
{sub}
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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
|
+
);
|
package/src/ui/YAxis.tsx
CHANGED
|
@@ -1,16 +1,9 @@
|
|
|
1
1
|
import { useLayoutEffect, useRef } from 'react';
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { type ValueFormatter, mountAxisLabels } from '@wick-charts/core';
|
|
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';
|
|
8
|
-
|
|
9
|
-
interface TrackedTick {
|
|
10
|
-
opacity: number;
|
|
11
|
-
addedAt: number;
|
|
12
|
-
fadedAt?: number;
|
|
13
|
-
}
|
|
14
7
|
|
|
15
8
|
export interface YAxisProps {
|
|
16
9
|
/**
|
|
@@ -29,7 +22,9 @@ export interface YAxisProps {
|
|
|
29
22
|
|
|
30
23
|
export function YAxis({ format, labelCount, minLabelSpacing }: YAxisProps = {}) {
|
|
31
24
|
const chart = useChartInstance();
|
|
32
|
-
useYRange(chart);
|
|
25
|
+
useYRange(chart);
|
|
26
|
+
|
|
27
|
+
const containerRef = useRef<HTMLDivElement | null>(null);
|
|
33
28
|
|
|
34
29
|
// Route the prop through yScale so the *same* formatter drives every
|
|
35
30
|
// surface that reads `yScale.formatY()` (Crosshair, YLabel fallback).
|
|
@@ -50,42 +45,16 @@ export function YAxis({ format, labelCount, minLabelSpacing }: YAxisProps = {})
|
|
|
50
45
|
};
|
|
51
46
|
}, [chart, labelCount, minLabelSpacing]);
|
|
52
47
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
const mapRef = useRef<Map<number, TrackedTick>>(new Map());
|
|
58
|
-
const map = mapRef.current;
|
|
59
|
-
const now = performance.now();
|
|
60
|
-
|
|
61
|
-
for (const p of currentTicks) {
|
|
62
|
-
if (!map.has(p)) {
|
|
63
|
-
map.set(p, { opacity: 1, addedAt: now });
|
|
64
|
-
} else {
|
|
65
|
-
map.get(p)!.opacity = 1;
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
for (const [p, entry] of map) {
|
|
70
|
-
if (!currentSet.has(p)) {
|
|
71
|
-
if (entry.opacity !== 0) {
|
|
72
|
-
entry.opacity = 0;
|
|
73
|
-
entry.fadedAt = now;
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
// Cleanup buffer matches the shared AXIS_LABEL_CLEANUP_MS — see axisFade.ts.
|
|
79
|
-
for (const [p, entry] of map) {
|
|
80
|
-
if (entry.opacity === 0 && entry.fadedAt !== undefined && now - entry.fadedAt > AXIS_LABEL_CLEANUP_MS) {
|
|
81
|
-
map.delete(p);
|
|
82
|
-
}
|
|
83
|
-
}
|
|
48
|
+
useLayoutEffect(() => {
|
|
49
|
+
const container = containerRef.current;
|
|
50
|
+
if (container === null) return;
|
|
84
51
|
|
|
85
|
-
|
|
52
|
+
return mountAxisLabels({ chart, container, axis: 'y' });
|
|
53
|
+
}, [chart]);
|
|
86
54
|
|
|
87
55
|
return (
|
|
88
56
|
<div
|
|
57
|
+
ref={containerRef}
|
|
89
58
|
style={{
|
|
90
59
|
position: 'absolute',
|
|
91
60
|
right: 0,
|
|
@@ -94,31 +63,6 @@ export function YAxis({ format, labelCount, minLabelSpacing }: YAxisProps = {})
|
|
|
94
63
|
width: chart.yAxisWidth,
|
|
95
64
|
pointerEvents: 'none',
|
|
96
65
|
}}
|
|
97
|
-
|
|
98
|
-
{allTicks.map(([price, entry]) => {
|
|
99
|
-
const y = chart.yScale.valueToY(price);
|
|
100
|
-
return (
|
|
101
|
-
<span
|
|
102
|
-
key={price}
|
|
103
|
-
style={{
|
|
104
|
-
position: 'absolute',
|
|
105
|
-
right: 8,
|
|
106
|
-
top: y,
|
|
107
|
-
transform: 'translateY(-50%)',
|
|
108
|
-
color: resolveAxisTextColor(theme, 'y'),
|
|
109
|
-
fontSize: resolveAxisFontSize(theme, 'y'),
|
|
110
|
-
fontFamily: theme.typography.fontFamily,
|
|
111
|
-
fontVariantNumeric: 'tabular-nums',
|
|
112
|
-
userSelect: 'none',
|
|
113
|
-
opacity: entry.opacity,
|
|
114
|
-
transition: AXIS_LABEL_FADE_CSS,
|
|
115
|
-
willChange: 'opacity',
|
|
116
|
-
}}
|
|
117
|
-
>
|
|
118
|
-
{chart.yScale.formatY(price)}
|
|
119
|
-
</span>
|
|
120
|
-
);
|
|
121
|
-
})}
|
|
122
|
-
</div>
|
|
66
|
+
/>
|
|
123
67
|
);
|
|
124
68
|
}
|
package/src/ui/axisFade.ts
DELETED
|
@@ -1,23 +0,0 @@
|
|
|
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;
|