@wick-charts/react 0.3.5 → 0.3.6
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/dist/index.cjs +2 -2
- package/dist/index.d.ts +120 -4
- package/dist/index.js +2145 -2041
- package/package.json +2 -2
- package/src/ChartContainer.tsx +17 -0
- package/src/EdgeLoader.tsx +187 -0
- package/src/index.ts +6 -0
- package/src/ui/Title.tsx +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wick-charts/react",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.6",
|
|
4
4
|
"description": "High-performance canvas timeseries charts for React — candlestick, line, bar, pie. Tree-shakeable, zero runtime deps.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
"react-dom": ">=18.0.0"
|
|
50
50
|
},
|
|
51
51
|
"devDependencies": {
|
|
52
|
-
"@wick-charts/core": "^0.3.
|
|
52
|
+
"@wick-charts/core": "^0.3.6"
|
|
53
53
|
},
|
|
54
54
|
"scripts": {
|
|
55
55
|
"build": "vite build"
|
package/src/ChartContainer.tsx
CHANGED
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
ChartInstance,
|
|
18
18
|
type ChartOptions,
|
|
19
19
|
type ChartTheme,
|
|
20
|
+
type EdgeReachedInfo,
|
|
20
21
|
} from '@wick-charts/core';
|
|
21
22
|
|
|
22
23
|
type PerfOption = NonNullable<ChartOptions['perf']>;
|
|
@@ -120,6 +121,18 @@ export interface ChartContainerProps {
|
|
|
120
121
|
* Only read at mount; changing this prop after the chart is created is ignored.
|
|
121
122
|
*/
|
|
122
123
|
perf?: PerfOption;
|
|
124
|
+
/**
|
|
125
|
+
* Fired after the user releases a pan/zoom gesture that pulled the viewport
|
|
126
|
+
* past a data edge by more than ~10% of the visible range. Hosts typically
|
|
127
|
+
* respond by prefetching more history.
|
|
128
|
+
*
|
|
129
|
+
* For threshold-based prefetch (load *before* the user fully overshoots),
|
|
130
|
+
* use `<EdgeLoader>` instead — that component subscribes to `viewportChange`
|
|
131
|
+
* and arms when the visible range nears the data edge.
|
|
132
|
+
*
|
|
133
|
+
* Captured at mount only; changing the prop identity later is ignored.
|
|
134
|
+
*/
|
|
135
|
+
onEdgeReached?: (info: EdgeReachedInfo) => void;
|
|
123
136
|
/** Inline style for the chart's outer wrapper element. */
|
|
124
137
|
style?: CSSProperties;
|
|
125
138
|
/** Extra class for the chart's outer wrapper element. */
|
|
@@ -220,12 +233,15 @@ export function ChartContainer({
|
|
|
220
233
|
headerLayout = 'overlay',
|
|
221
234
|
perf,
|
|
222
235
|
animations,
|
|
236
|
+
onEdgeReached,
|
|
223
237
|
style,
|
|
224
238
|
className,
|
|
225
239
|
}: ChartContainerProps) {
|
|
226
240
|
// Mount-only: capture the initial perf option in a ref so later renders with
|
|
227
241
|
// a new object identity don't recreate the chart or remount the HUD.
|
|
228
242
|
const perfRef = useRef(perf);
|
|
243
|
+
// Same mount-only capture for the edge callback — the chart binds it once.
|
|
244
|
+
const onEdgeReachedRef = useRef(onEdgeReached);
|
|
229
245
|
const contextTheme = useThemeOptional();
|
|
230
246
|
const resolvedTheme = theme ?? contextTheme ?? undefined;
|
|
231
247
|
|
|
@@ -246,6 +262,7 @@ export function ChartContainer({
|
|
|
246
262
|
if (grid !== undefined) options.grid = grid;
|
|
247
263
|
if (perfRef.current !== undefined) options.perf = perfRef.current;
|
|
248
264
|
if (animations !== undefined) options.animations = animations;
|
|
265
|
+
if (onEdgeReachedRef.current) options.onEdgeReached = onEdgeReachedRef.current;
|
|
249
266
|
chartRef.current = new ChartInstance(containerRef.current, options);
|
|
250
267
|
|
|
251
268
|
// Note: the init path above already propagated `grid` into the chart. The
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import { type ReactNode, useEffect, useRef, useState } from 'react';
|
|
2
|
+
|
|
3
|
+
import type { EdgeSide } from '@wick-charts/core';
|
|
4
|
+
|
|
5
|
+
import { useChartInstance } from './context';
|
|
6
|
+
|
|
7
|
+
/** Argument shape passed to the {@link EdgeLoader} render-prop. */
|
|
8
|
+
export interface EdgeLoaderRenderArgs {
|
|
9
|
+
/**
|
|
10
|
+
* CSS pixels in the chart's overlay coordinate space, anchored at the data
|
|
11
|
+
* edge (`data.from` for `side='left'`, `data.to` for `side='right'`).
|
|
12
|
+
* The overlay div is positioned with `inset: 0`, so this value can be used
|
|
13
|
+
* directly as `style={{ left: x }}` or `transform: translateX(...)`.
|
|
14
|
+
*/
|
|
15
|
+
x: number;
|
|
16
|
+
side: EdgeSide;
|
|
17
|
+
/** True between {@link EdgeLoaderProps.onTrigger} firing and its Promise resolving. */
|
|
18
|
+
isLoading: boolean;
|
|
19
|
+
/** Time coordinate (ms) of the data edge — convenient for "fetch history before T" requests. */
|
|
20
|
+
boundaryTime: number;
|
|
21
|
+
/** Becomes `false` after `onTrigger` resolves with the literal value `false`. */
|
|
22
|
+
hasMore: boolean;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface EdgeLoaderProps {
|
|
26
|
+
/** Which edge to watch. */
|
|
27
|
+
side: EdgeSide;
|
|
28
|
+
/**
|
|
29
|
+
* Bars from the edge that arms the trigger. Multiplied by the chart's data
|
|
30
|
+
* interval. Default `5`.
|
|
31
|
+
*/
|
|
32
|
+
threshold?: number;
|
|
33
|
+
/**
|
|
34
|
+
* Called when the visible range moves within {@link EdgeLoaderProps.threshold}
|
|
35
|
+
* bars of the data edge. Returning a Promise toggles `isLoading` for its
|
|
36
|
+
* lifetime. **Resolve with `false`** to signal "no more data" — the loader
|
|
37
|
+
* stops watching and switches the optional canvas indicator to its
|
|
38
|
+
* `'no-data'` state. Any other resolve value (including `undefined`) means
|
|
39
|
+
* "keep watching for the next near-edge event".
|
|
40
|
+
*/
|
|
41
|
+
// biome-ignore lint/suspicious/noConfusingVoidType: void allows callers to write `() => fetch()` without an explicit return
|
|
42
|
+
onTrigger: () => void | Promise<unknown>;
|
|
43
|
+
/**
|
|
44
|
+
* - `'canvas'` (default): drive the chart's built-in canvas spinner via
|
|
45
|
+
* {@link ChartInstance.setEdgeState}. Renders inside the chart area at
|
|
46
|
+
* the data boundary.
|
|
47
|
+
* - `'custom'`: skip the canvas indicator. Use the render-prop `children`
|
|
48
|
+
* to draw your own DOM/SVG loader.
|
|
49
|
+
*/
|
|
50
|
+
indicator?: 'canvas' | 'custom';
|
|
51
|
+
/**
|
|
52
|
+
* Optional render-prop. Receives the live edge state — render whatever
|
|
53
|
+
* positioned overlay you want, or return `null`.
|
|
54
|
+
*/
|
|
55
|
+
children?: (args: EdgeLoaderRenderArgs) => ReactNode;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Subscribes to the chart's viewport and triggers a fetch when the visible
|
|
60
|
+
* range nears the chosen data edge. Handles the boilerplate every load-on-scroll
|
|
61
|
+
* site otherwise has to re-implement: arming after first user pan, deduping
|
|
62
|
+
* via Promise tracking, and exposing the boundary's pixel coordinate so a
|
|
63
|
+
* loader can be anchored to "the wall of available history".
|
|
64
|
+
*
|
|
65
|
+
* Place as a child of `<ChartContainer>`.
|
|
66
|
+
*/
|
|
67
|
+
export function EdgeLoader({ side, threshold = 5, onTrigger, indicator = 'canvas', children }: EdgeLoaderProps) {
|
|
68
|
+
const chart = useChartInstance();
|
|
69
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
70
|
+
const [hasMore, setHasMore] = useState(true);
|
|
71
|
+
// Bump on viewportChange / overlayChange so the render-prop re-runs with
|
|
72
|
+
// the latest pixel x. State, not ref, because we want the re-render.
|
|
73
|
+
const [, setTick] = useState(0);
|
|
74
|
+
|
|
75
|
+
const triggerRef = useRef(onTrigger);
|
|
76
|
+
triggerRef.current = onTrigger;
|
|
77
|
+
// Stash `children` in a ref so the effect doesn't have to rebind listeners
|
|
78
|
+
// on every render-prop identity change, and so the bump-on-change gate can
|
|
79
|
+
// read the latest value without putting `children` in deps.
|
|
80
|
+
const hasChildrenRef = useRef(children !== undefined);
|
|
81
|
+
hasChildrenRef.current = children !== undefined;
|
|
82
|
+
const inflight = useRef(false);
|
|
83
|
+
// Largest "distance from edge" (in time units) seen so far — gate the
|
|
84
|
+
// trigger on it crossing the threshold once, so the initial fit-to-data
|
|
85
|
+
// (where visible === data) doesn't fire the loader on mount.
|
|
86
|
+
const armed = useRef(false);
|
|
87
|
+
|
|
88
|
+
useEffect(() => {
|
|
89
|
+
if (!hasMore) return;
|
|
90
|
+
|
|
91
|
+
const distanceFromEdge = (): number | null => {
|
|
92
|
+
const visible = chart.getVisibleRange();
|
|
93
|
+
const data = chart.getDataRange();
|
|
94
|
+
if (!data) return null;
|
|
95
|
+
|
|
96
|
+
return side === 'left' ? visible.from - data.from : data.to - visible.to;
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const fire = () => {
|
|
100
|
+
if (inflight.current || !hasMore) return;
|
|
101
|
+
|
|
102
|
+
inflight.current = true;
|
|
103
|
+
setIsLoading(true);
|
|
104
|
+
if (indicator === 'canvas') chart.setEdgeState(side, 'loading');
|
|
105
|
+
|
|
106
|
+
// biome-ignore lint/suspicious/noConfusingVoidType: matches onTrigger's return type exactly
|
|
107
|
+
let result: void | Promise<unknown>;
|
|
108
|
+
try {
|
|
109
|
+
result = triggerRef.current();
|
|
110
|
+
} catch (err) {
|
|
111
|
+
inflight.current = false;
|
|
112
|
+
setIsLoading(false);
|
|
113
|
+
if (indicator === 'canvas') chart.setEdgeState(side, 'idle');
|
|
114
|
+
throw err;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const finish = (value: unknown) => {
|
|
118
|
+
inflight.current = false;
|
|
119
|
+
setIsLoading(false);
|
|
120
|
+
if (value === false) {
|
|
121
|
+
setHasMore(false);
|
|
122
|
+
if (indicator === 'canvas') chart.setEdgeState(side, 'no-data');
|
|
123
|
+
} else if (indicator === 'canvas') {
|
|
124
|
+
chart.setEdgeState(side, 'idle');
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
if (result && typeof (result as Promise<unknown>).then === 'function') {
|
|
129
|
+
(result as Promise<unknown>).then(finish, () => finish(undefined));
|
|
130
|
+
} else {
|
|
131
|
+
finish(undefined);
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
const onChange = () => {
|
|
136
|
+
const interval = chart.getDataInterval();
|
|
137
|
+
const distance = distanceFromEdge();
|
|
138
|
+
if (distance === null) return;
|
|
139
|
+
|
|
140
|
+
if (!armed.current) {
|
|
141
|
+
// Wait until the visible range has moved away from the edge once —
|
|
142
|
+
// then we know the chart isn't in its mount-time fit-to-data state.
|
|
143
|
+
if (distance > threshold * interval) {
|
|
144
|
+
armed.current = true;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (distance <= threshold * interval) {
|
|
151
|
+
fire();
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Bump only when a render-prop is consuming the live pixel-x — the
|
|
155
|
+
// canvas-indicator path is driven entirely by chart.setEdgeState and
|
|
156
|
+
// doesn't need a React re-render on every pan/zoom frame.
|
|
157
|
+
if (hasChildrenRef.current) setTick((n) => n + 1);
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
chart.on('viewportChange', onChange);
|
|
161
|
+
chart.on('overlayChange', onChange);
|
|
162
|
+
onChange();
|
|
163
|
+
|
|
164
|
+
return () => {
|
|
165
|
+
chart.off('viewportChange', onChange);
|
|
166
|
+
chart.off('overlayChange', onChange);
|
|
167
|
+
};
|
|
168
|
+
}, [chart, side, threshold, indicator, hasMore]);
|
|
169
|
+
|
|
170
|
+
// Reset the canvas indicator when this loader unmounts so a remount with a
|
|
171
|
+
// fresh side / threshold doesn't inherit stale state.
|
|
172
|
+
useEffect(() => {
|
|
173
|
+
return () => {
|
|
174
|
+
if (indicator === 'canvas') chart.setEdgeState(side, 'idle');
|
|
175
|
+
};
|
|
176
|
+
}, [chart, side, indicator]);
|
|
177
|
+
|
|
178
|
+
if (!children) return null;
|
|
179
|
+
|
|
180
|
+
const data = chart.getDataRange();
|
|
181
|
+
if (!data) return null;
|
|
182
|
+
|
|
183
|
+
const boundaryTime = side === 'left' ? data.from : data.to;
|
|
184
|
+
const x = chart.timeScale.timeToX(boundaryTime);
|
|
185
|
+
|
|
186
|
+
return <>{children({ x, side, isLoading, boundaryTime, hasMore })}</>;
|
|
187
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -22,6 +22,9 @@ 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. */
|
|
@@ -80,6 +83,7 @@ export {
|
|
|
80
83
|
gruvbox,
|
|
81
84
|
handwritten,
|
|
82
85
|
highContrast,
|
|
86
|
+
isDarkBg,
|
|
83
87
|
lavenderMist,
|
|
84
88
|
lightPink,
|
|
85
89
|
lightTheme,
|
|
@@ -107,6 +111,8 @@ export { CandlestickSeries } from './CandlestickSeries';
|
|
|
107
111
|
export { ChartContainer } from './ChartContainer';
|
|
108
112
|
// React hooks
|
|
109
113
|
export { useChartInstance } from './context';
|
|
114
|
+
export type { EdgeLoaderProps, EdgeLoaderRenderArgs } from './EdgeLoader';
|
|
115
|
+
export { EdgeLoader } from './EdgeLoader';
|
|
110
116
|
export { LineSeries } from './LineSeries';
|
|
111
117
|
export { PieSeries } from './PieSeries';
|
|
112
118
|
export {
|
package/src/ui/Title.tsx
CHANGED
|
@@ -41,7 +41,7 @@ export function Title({ children, sub, style }: TitleProps) {
|
|
|
41
41
|
display: 'flex',
|
|
42
42
|
alignItems: 'baseline',
|
|
43
43
|
gap: 6,
|
|
44
|
-
padding: '6px 8px
|
|
44
|
+
padding: '6px 8px 0',
|
|
45
45
|
flexShrink: 0,
|
|
46
46
|
fontFamily: theme.typography.fontFamily,
|
|
47
47
|
fontSize: theme.typography.fontSize,
|