react-native-livechart 3.4.0 → 3.5.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 +1 -0
- package/dist/components/LiveChart.d.ts.map +1 -1
- package/dist/components/MarkerOverlay.d.ts.map +1 -1
- package/dist/components/ThresholdLineOverlay.d.ts +34 -0
- package/dist/components/ThresholdLineOverlay.d.ts.map +1 -0
- package/dist/core/resolveConfig.d.ts +33 -1
- package/dist/core/resolveConfig.d.ts.map +1 -1
- package/dist/draw/markerAtlas.d.ts +16 -1
- package/dist/draw/markerAtlas.d.ts.map +1 -1
- package/dist/hooks/useChartPaths.d.ts +5 -1
- package/dist/hooks/useChartPaths.d.ts.map +1 -1
- package/dist/hooks/useThreshold.d.ts +22 -0
- package/dist/hooks/useThreshold.d.ts.map +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/math/threshold.d.ts +25 -0
- package/dist/math/threshold.d.ts.map +1 -0
- package/dist/types.d.ts +59 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/components/LiveChart.tsx +197 -40
- package/src/components/MarkerOverlay.tsx +13 -5
- package/src/components/ThresholdLineOverlay.tsx +172 -0
- package/src/core/resolveConfig.ts +62 -0
- package/src/draw/markerAtlas.ts +31 -5
- package/src/hooks/useChartPaths.ts +25 -1
- package/src/hooks/useThreshold.ts +63 -0
- package/src/index.ts +2 -0
- package/src/math/threshold.ts +59 -0
- package/src/types.ts +61 -0
package/src/draw/markerAtlas.ts
CHANGED
|
@@ -74,7 +74,9 @@ export function markerAppearanceSig(m: Marker): string {
|
|
|
74
74
|
|
|
75
75
|
/** One packed glyph in the atlas image: its source rect + box size. */
|
|
76
76
|
export interface AtlasCell {
|
|
77
|
+
/** Source rect into the (hi-res) atlas texture, in **device** pixels. */
|
|
77
78
|
rect: SkRect;
|
|
79
|
+
/** Logical (DPR-independent) cell width/height, used to center the blit. */
|
|
78
80
|
w: number;
|
|
79
81
|
h: number;
|
|
80
82
|
}
|
|
@@ -84,9 +86,16 @@ export interface MarkerAtlas {
|
|
|
84
86
|
image: SkImage | null;
|
|
85
87
|
/** appearance-signature → cell. */
|
|
86
88
|
cells: Record<string, AtlasCell>;
|
|
89
|
+
/**
|
|
90
|
+
* Device-pixel scale the texture was rasterized at. Cell `rect`s are in
|
|
91
|
+
* texture (device) pixels, while `w`/`h` stay in logical pixels — so the
|
|
92
|
+
* per-frame blit must apply an `RSXform` scale of `1 / scale` to map the
|
|
93
|
+
* hi-res cell back to its logical on-canvas size. See `buildMarkerAtlas`.
|
|
94
|
+
*/
|
|
95
|
+
scale: number;
|
|
87
96
|
}
|
|
88
97
|
|
|
89
|
-
const EMPTY_ATLAS: MarkerAtlas = { image: null, cells: {} };
|
|
98
|
+
const EMPTY_ATLAS: MarkerAtlas = { image: null, cells: {}, scale: 1 };
|
|
90
99
|
|
|
91
100
|
function fillPaint(color: string): SkPaint {
|
|
92
101
|
const p = Skia.Paint();
|
|
@@ -258,11 +267,18 @@ function cellSpec(m: Marker, palette: LiveChartPalette, font: SkFont): CellSpec
|
|
|
258
267
|
*
|
|
259
268
|
* Connector kinds (see `isConnectorMarker`) are skipped — they render via a
|
|
260
269
|
* self-projecting fallback component.
|
|
270
|
+
*
|
|
271
|
+
* The texture is rasterized at `scale` (the screen's device-pixel ratio) so the
|
|
272
|
+
* cells carry enough resolution to stay crisp when blitted onto a retina canvas
|
|
273
|
+
* — every glyph is recorded in logical coords then magnified by `scale`, and the
|
|
274
|
+
* per-frame `drawAtlas` shrinks it back with an `RSXform` scale of `1 / scale`.
|
|
275
|
+
* Without this the logical-sized texture is upscaled ~3× on iOS and looks blurry.
|
|
261
276
|
*/
|
|
262
277
|
export function buildMarkerAtlas(
|
|
263
278
|
markers: Marker[],
|
|
264
279
|
palette: LiveChartPalette,
|
|
265
280
|
font: SkFont,
|
|
281
|
+
scale = 1,
|
|
266
282
|
): MarkerAtlas {
|
|
267
283
|
const seen = new Set<string>();
|
|
268
284
|
const specs: { sig: string; spec: CellSpec }[] = [];
|
|
@@ -282,11 +298,16 @@ export function buildMarkerAtlas(
|
|
|
282
298
|
totalW += specs[i].spec.w;
|
|
283
299
|
if (specs[i].spec.h > maxH) maxH = specs[i].spec.h;
|
|
284
300
|
}
|
|
301
|
+
// Logical packed size; the texture itself is `scale`× larger in each axis.
|
|
285
302
|
const W = Math.max(1, Math.ceil(totalW));
|
|
286
303
|
const H = Math.max(1, Math.ceil(maxH));
|
|
304
|
+
const texW = Math.max(1, Math.ceil(W * scale));
|
|
305
|
+
const texH = Math.max(1, Math.ceil(H * scale));
|
|
287
306
|
|
|
288
307
|
const recorder = Skia.PictureRecorder();
|
|
289
|
-
const canvas = recorder.beginRecording(Skia.XYWHRect(0, 0,
|
|
308
|
+
const canvas = recorder.beginRecording(Skia.XYWHRect(0, 0, texW, texH));
|
|
309
|
+
// Magnify so glyphs drawn in logical coords fill the hi-res texture.
|
|
310
|
+
canvas.scale(scale, scale);
|
|
290
311
|
const cells: Record<string, AtlasCell> = {};
|
|
291
312
|
let x = 0;
|
|
292
313
|
for (let i = 0; i < specs.length; i++) {
|
|
@@ -295,10 +316,15 @@ export function buildMarkerAtlas(
|
|
|
295
316
|
canvas.translate(x, 0);
|
|
296
317
|
spec.draw(canvas, spec.w / 2, H / 2);
|
|
297
318
|
canvas.restore();
|
|
298
|
-
|
|
319
|
+
// Source rect indexes the device-pixel texture; w/h stay logical.
|
|
320
|
+
cells[sig] = {
|
|
321
|
+
rect: Skia.XYWHRect(x * scale, 0, spec.w * scale, texH),
|
|
322
|
+
w: spec.w,
|
|
323
|
+
h: H,
|
|
324
|
+
};
|
|
299
325
|
x += spec.w;
|
|
300
326
|
}
|
|
301
327
|
const picture = recorder.finishRecordingAsPicture();
|
|
302
|
-
const image = drawAsImageFromPicture(picture, { width:
|
|
303
|
-
return { image, cells };
|
|
328
|
+
const image = drawAsImageFromPicture(picture, { width: texW, height: texH });
|
|
329
|
+
return { image, cells, scale };
|
|
304
330
|
}
|
|
@@ -22,9 +22,13 @@ export function useChartPaths(
|
|
|
22
22
|
engine: SingleEngineState,
|
|
23
23
|
padding: ChartPadding,
|
|
24
24
|
morphT?: SharedValue<number>,
|
|
25
|
+
/** When set, also build `thresholdFillPath` — the band between the line and this
|
|
26
|
+
* pixel-Y, closed at the threshold instead of the chart baseline. */
|
|
27
|
+
thresholdY?: SharedValue<number>,
|
|
25
28
|
) {
|
|
26
29
|
const lineBuilder = usePathBuilder();
|
|
27
30
|
const fillBuilder = usePathBuilder();
|
|
31
|
+
const thresholdFillBuilder = usePathBuilder();
|
|
28
32
|
|
|
29
33
|
const cacheRef = useRef<{
|
|
30
34
|
emptyPath: SkPath;
|
|
@@ -105,5 +109,25 @@ export function useChartPaths(
|
|
|
105
109
|
return b.detach();
|
|
106
110
|
});
|
|
107
111
|
|
|
108
|
-
|
|
112
|
+
// Threshold-anchored fill: the same spline, closed at `thresholdY` instead of the
|
|
113
|
+
// baseline, so the band lies between the line and the threshold (the profit/loss
|
|
114
|
+
// area). Painted with the hard-split vertical gradient, the part above the split
|
|
115
|
+
// shows the above-color and the part below shows the below-color.
|
|
116
|
+
const thresholdFillPath = useDerivedValue(() => {
|
|
117
|
+
const cache = cacheRef.current!;
|
|
118
|
+
if (!thresholdY) return cache.emptyPath;
|
|
119
|
+
const yT = thresholdY.get();
|
|
120
|
+
const pts = flatPts.get();
|
|
121
|
+
const n = pts.length >> 1;
|
|
122
|
+
if (n < 2 || !Number.isFinite(yT)) return cache.emptyPath;
|
|
123
|
+
const b = thresholdFillBuilder.value;
|
|
124
|
+
b.moveTo(pts[0], pts[1]);
|
|
125
|
+
drawSpline(b, pts, cache.scratch);
|
|
126
|
+
b.lineTo(pts[(n - 1) * 2], yT);
|
|
127
|
+
b.lineTo(pts[0], yT);
|
|
128
|
+
b.close();
|
|
129
|
+
return b.detach();
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
return { linePath, fillPath, thresholdFillPath } as const;
|
|
109
133
|
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { vec } from "@shopify/react-native-skia";
|
|
2
|
+
import { useDerivedValue, type SharedValue } from "react-native-reanimated";
|
|
3
|
+
|
|
4
|
+
import type { ChartEngineLayout } from "../core/useLiveChartEngine";
|
|
5
|
+
import type { ChartPadding } from "../draw/line";
|
|
6
|
+
import {
|
|
7
|
+
thresholdLineY,
|
|
8
|
+
thresholdSplitPositions,
|
|
9
|
+
thresholdVisible,
|
|
10
|
+
} from "../math/threshold";
|
|
11
|
+
|
|
12
|
+
export interface ThresholdGeometry {
|
|
13
|
+
/** Threshold pixel-Y within the canvas, or NaN before layout / degenerate range. */
|
|
14
|
+
lineY: SharedValue<number>;
|
|
15
|
+
/** Whether the threshold sits within the plot area (drives marker-line opacity). */
|
|
16
|
+
visible: SharedValue<boolean>;
|
|
17
|
+
/** Vertical gradient end vector, `vec(0, canvasHeight)` — shared by stroke + fill. */
|
|
18
|
+
gradientEnd: SharedValue<ReturnType<typeof vec>>;
|
|
19
|
+
/** `[0, t, t, 1]` stop positions for the hard-split gradient — shared by stroke + fill. */
|
|
20
|
+
splitPositions: SharedValue<number[]>;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Per-frame screen geometry for the threshold split, shared by the stroke
|
|
25
|
+
* gradient, the profit/loss fill band, and the dashed marker line. Reads the live
|
|
26
|
+
* threshold value + engine Y-range on the UI thread; the math lives in
|
|
27
|
+
* `math/threshold` so it stays unit-testable without Reanimated.
|
|
28
|
+
*/
|
|
29
|
+
export function useThreshold(
|
|
30
|
+
engine: ChartEngineLayout,
|
|
31
|
+
padding: ChartPadding,
|
|
32
|
+
value: SharedValue<number>,
|
|
33
|
+
): ThresholdGeometry {
|
|
34
|
+
const lineY = useDerivedValue(() =>
|
|
35
|
+
thresholdLineY(
|
|
36
|
+
value.get(),
|
|
37
|
+
engine.displayMin.get(),
|
|
38
|
+
engine.displayMax.get(),
|
|
39
|
+
engine.canvasHeight.get(),
|
|
40
|
+
padding.top,
|
|
41
|
+
padding.bottom,
|
|
42
|
+
),
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
const visible = useDerivedValue(() =>
|
|
46
|
+
thresholdVisible(
|
|
47
|
+
lineY.get(),
|
|
48
|
+
engine.canvasHeight.get(),
|
|
49
|
+
padding.top,
|
|
50
|
+
padding.bottom,
|
|
51
|
+
),
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
const splitPositions = useDerivedValue(() =>
|
|
55
|
+
thresholdSplitPositions(lineY.get(), engine.canvasHeight.get()),
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
const gradientEnd = useDerivedValue(() =>
|
|
59
|
+
vec(0, Math.max(1, engine.canvasHeight.get())),
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
return { lineY, visible, gradientEnd, splitPositions };
|
|
63
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure, worklet-safe geometry for the threshold split ({@link ThresholdConfig}).
|
|
3
|
+
* No SharedValues and no hooks — these are unit-tested directly; `useThreshold`
|
|
4
|
+
* wires the live SharedValues into them.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Pixel Y of a threshold value within the plot. Mirrors the line path's value→Y
|
|
9
|
+
* mapping in `buildLinePoints` (`top + (max - v) / range * chartH`). Returns NaN
|
|
10
|
+
* when the canvas hasn't laid out yet or the value range is degenerate, so the
|
|
11
|
+
* line/fill/marker callers can cull instead of drawing at a bogus position.
|
|
12
|
+
*/
|
|
13
|
+
export function thresholdLineY(
|
|
14
|
+
value: number,
|
|
15
|
+
displayMin: number,
|
|
16
|
+
displayMax: number,
|
|
17
|
+
canvasHeight: number,
|
|
18
|
+
paddingTop: number,
|
|
19
|
+
paddingBottom: number,
|
|
20
|
+
): number {
|
|
21
|
+
"worklet";
|
|
22
|
+
const range = displayMax - displayMin;
|
|
23
|
+
const chartH = canvasHeight - paddingTop - paddingBottom;
|
|
24
|
+
if (canvasHeight <= 0 || range <= 0 || chartH <= 0) return NaN;
|
|
25
|
+
return paddingTop + ((displayMax - value) / range) * chartH;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** True when the threshold's Y sits within the plot area (i.e. on-screen). */
|
|
29
|
+
export function thresholdVisible(
|
|
30
|
+
lineY: number,
|
|
31
|
+
canvasHeight: number,
|
|
32
|
+
paddingTop: number,
|
|
33
|
+
paddingBottom: number,
|
|
34
|
+
): boolean {
|
|
35
|
+
"worklet";
|
|
36
|
+
if (!Number.isFinite(lineY) || canvasHeight <= 0) return false;
|
|
37
|
+
return lineY >= paddingTop && lineY <= canvasHeight - paddingBottom;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Stop positions for the vertical hard-split gradient, paired with the 4-stop
|
|
42
|
+
* color array `[above, above, below, below]`: `[0, t, t, 1]`, where `t` is the
|
|
43
|
+
* threshold's fraction down the full canvas (the gradient vector spans
|
|
44
|
+
* `0 → canvasHeight`), clamped to `[0, 1]`.
|
|
45
|
+
*
|
|
46
|
+
* - `t ≤ 0` (threshold above everything) → `[0, 0, 0, 1]` → solid below-color.
|
|
47
|
+
* - `t ≥ 1` (threshold below everything) → `[0, 1, 1, 1]` → solid above-color.
|
|
48
|
+
*/
|
|
49
|
+
export function thresholdSplitPositions(
|
|
50
|
+
lineY: number,
|
|
51
|
+
canvasHeight: number,
|
|
52
|
+
): number[] {
|
|
53
|
+
"worklet";
|
|
54
|
+
if (canvasHeight <= 0 || !Number.isFinite(lineY)) return [0, 1, 1, 1];
|
|
55
|
+
let t = lineY / canvasHeight;
|
|
56
|
+
if (t < 0) t = 0;
|
|
57
|
+
else if (t > 1) t = 1;
|
|
58
|
+
return [0, t, t, 1];
|
|
59
|
+
}
|
package/src/types.ts
CHANGED
|
@@ -241,6 +241,61 @@ export interface LineConfig {
|
|
|
241
241
|
colors?: string[];
|
|
242
242
|
}
|
|
243
243
|
|
|
244
|
+
/**
|
|
245
|
+
* Color the line above vs. below a live threshold value — green above, red below
|
|
246
|
+
* by default. The threshold is **always** a `SharedValue` so it can track a live
|
|
247
|
+
* benchmark (break-even / average cost, VWAP, previous close, a peg) on the UI
|
|
248
|
+
* thread without re-rendering. Drives a hard-split line stroke and, optionally, a
|
|
249
|
+
* tinted profit/loss fill band and a dashed marker line at the threshold.
|
|
250
|
+
*
|
|
251
|
+
* The split stroke supersedes `LineConfig.color`/`colors` and segment recoloring
|
|
252
|
+
* for the main line while a threshold is set.
|
|
253
|
+
*/
|
|
254
|
+
export interface ThresholdConfig {
|
|
255
|
+
/**
|
|
256
|
+
* The split value, in Y-axis (price) units. A `SharedValue` — update it with
|
|
257
|
+
* `.set()` and the split tracks live on the UI thread (break-even, VWAP, the
|
|
258
|
+
* previous close, a peg, …).
|
|
259
|
+
*/
|
|
260
|
+
value: SharedValue<number>;
|
|
261
|
+
/** Stroke color where the line is at/above `value`. Default: palette up-green (`candleUp`). */
|
|
262
|
+
aboveColor?: string;
|
|
263
|
+
/** Stroke color where the line is below `value`. Default: palette down-red (`candleDown`). */
|
|
264
|
+
belowColor?: string;
|
|
265
|
+
/**
|
|
266
|
+
* Tint the area between the line and `value` (the profit/loss band) toward the
|
|
267
|
+
* above/below colors. Independent of the baseline `gradient` fill — set
|
|
268
|
+
* `gradient={false}` for the threshold band alone. Default `false`.
|
|
269
|
+
*/
|
|
270
|
+
fill?: boolean;
|
|
271
|
+
/**
|
|
272
|
+
* Dashed marker line + optional gutter label at the threshold. `true` → a dashed
|
|
273
|
+
* line in the palette reference color; object → styled; omit/`false` → none.
|
|
274
|
+
* Default off.
|
|
275
|
+
*/
|
|
276
|
+
line?: boolean | ThresholdLineConfig;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/** Dashed marker line drawn at a {@link ThresholdConfig} value. */
|
|
280
|
+
export interface ThresholdLineConfig {
|
|
281
|
+
/** Label text, e.g. `"Break-even"`. */
|
|
282
|
+
label?: string;
|
|
283
|
+
/**
|
|
284
|
+
* Label side. `"left"` sits just inside the plot at the line's left edge —
|
|
285
|
+
* clear of the y-axis labels and the live badge; `"right"` uses the right
|
|
286
|
+
* gutter like a legacy reference line (may overlap y-axis labels). Default `"left"`.
|
|
287
|
+
*/
|
|
288
|
+
labelPosition?: "left" | "right";
|
|
289
|
+
/** Line + label color. Defaults to palette `refLine` / `refLabel`. */
|
|
290
|
+
color?: string;
|
|
291
|
+
/** Dash pattern `[dashLength, gapLength]` in pixels. Default `[4, 4]`. */
|
|
292
|
+
intervals?: [number, number];
|
|
293
|
+
/** Line thickness in pixels. Default `1`. */
|
|
294
|
+
strokeWidth?: number;
|
|
295
|
+
/** Append the formatted threshold value to the label. Default `false`. */
|
|
296
|
+
showValue?: boolean;
|
|
297
|
+
}
|
|
298
|
+
|
|
244
299
|
/** Area fill gradient beneath the chart line. */
|
|
245
300
|
export interface GradientConfig {
|
|
246
301
|
/** Opacity at the top of the gradient (near the line). Default `0.35`. */
|
|
@@ -1034,6 +1089,12 @@ export interface LiveChartProps extends LiveChartCoreProps {
|
|
|
1034
1089
|
* `mutedColors`). Optional dashed `divider` + `label` mark a segment's edge.
|
|
1035
1090
|
*/
|
|
1036
1091
|
segments?: ChartSegment[];
|
|
1092
|
+
/**
|
|
1093
|
+
* Color the line above vs. below a live threshold value (break-even / average
|
|
1094
|
+
* cost, VWAP, previous close, a peg). Always a `SharedValue` so the split tracks
|
|
1095
|
+
* live on the UI thread. See {@link ThresholdConfig}.
|
|
1096
|
+
*/
|
|
1097
|
+
threshold?: ThresholdConfig;
|
|
1037
1098
|
/** Render the live value as a large text overlay in the top-left. Default `false`. */
|
|
1038
1099
|
showValue?: boolean;
|
|
1039
1100
|
/** Tint the `showValue` text by momentum (green up / red down). Default `false`. */
|