@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.
- package/README.md +6 -6
- package/dist/index.cjs +2 -2
- package/dist/index.d.ts +889 -335
- package/dist/index.js +4761 -3949
- package/package.json +3 -3
- package/src/BarSeries.tsx +18 -56
- package/src/CandlestickSeries.tsx +11 -57
- package/src/ChartContainer.tsx +97 -51
- package/src/EdgeLoader.tsx +187 -0
- package/src/LineSeries.tsx +18 -56
- package/src/PieSeries.tsx +1 -2
- package/src/index.ts +14 -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/LineSeries.tsx
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import { useEffect, useLayoutEffect, useRef } from 'react';
|
|
2
2
|
|
|
3
|
-
import {
|
|
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
|
|
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.
|
|
28
|
+
const id = chart.addSeries('line', { ...options, layers: data.length, id: idProp });
|
|
30
29
|
seriesRef.current = id;
|
|
31
|
-
|
|
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
|
-
|
|
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
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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.
|
|
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 {
|
package/src/ui/Crosshair.tsx
CHANGED
|
@@ -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,
|
|
61
|
+
{formatTime(position.time, tickInterval)}
|
|
58
62
|
</div>
|
|
59
63
|
</>
|
|
60
64
|
);
|
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
|
+
);
|