react-native-livechart 4.20.0 → 4.21.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.
Files changed (75) hide show
  1. package/dist/components/CustomThresholdBadgeOverlay.d.ts +18 -0
  2. package/dist/components/CustomThresholdBadgeOverlay.d.ts.map +1 -0
  3. package/dist/components/CustomTooltipOverlay.d.ts +4 -2
  4. package/dist/components/CustomTooltipOverlay.d.ts.map +1 -1
  5. package/dist/components/LiveChart.d.ts.map +1 -1
  6. package/dist/components/ReferenceLineOverlay.d.ts +9 -4
  7. package/dist/components/ReferenceLineOverlay.d.ts.map +1 -1
  8. package/dist/components/ReferenceLineSeriesOverlay.d.ts +19 -0
  9. package/dist/components/ReferenceLineSeriesOverlay.d.ts.map +1 -0
  10. package/dist/core/liveChartEngineTick.d.ts +6 -1
  11. package/dist/core/liveChartEngineTick.d.ts.map +1 -1
  12. package/dist/core/resolveConfig.d.ts +32 -1
  13. package/dist/core/resolveConfig.d.ts.map +1 -1
  14. package/dist/core/useLiveChartEngine.d.ts +9 -1
  15. package/dist/core/useLiveChartEngine.d.ts.map +1 -1
  16. package/dist/draw/candle.d.ts +3 -1
  17. package/dist/draw/candle.d.ts.map +1 -1
  18. package/dist/draw/candleGap.d.ts +14 -0
  19. package/dist/draw/candleGap.d.ts.map +1 -0
  20. package/dist/draw/lineGap.d.ts +25 -0
  21. package/dist/draw/lineGap.d.ts.map +1 -0
  22. package/dist/hooks/crosshairShared.d.ts +7 -3
  23. package/dist/hooks/crosshairShared.d.ts.map +1 -1
  24. package/dist/hooks/useCandleGapPaths.d.ts +12 -0
  25. package/dist/hooks/useCandleGapPaths.d.ts.map +1 -0
  26. package/dist/hooks/useChartPaths.d.ts +4 -1
  27. package/dist/hooks/useChartPaths.d.ts.map +1 -1
  28. package/dist/hooks/useCrosshair.d.ts +9 -5
  29. package/dist/hooks/useCrosshair.d.ts.map +1 -1
  30. package/dist/hooks/useLineGapPaths.d.ts +10 -0
  31. package/dist/hooks/useLineGapPaths.d.ts.map +1 -0
  32. package/dist/hooks/useReferenceLine.d.ts +4 -3
  33. package/dist/hooks/useReferenceLine.d.ts.map +1 -1
  34. package/dist/hooks/useReferenceLineSeries.d.ts +23 -0
  35. package/dist/hooks/useReferenceLineSeries.d.ts.map +1 -0
  36. package/dist/hooks/useThreshold.d.ts +13 -13
  37. package/dist/hooks/useThreshold.d.ts.map +1 -1
  38. package/dist/index.d.ts +1 -1
  39. package/dist/index.d.ts.map +1 -1
  40. package/dist/math/candleGaps.d.ts +18 -0
  41. package/dist/math/candleGaps.d.ts.map +1 -0
  42. package/dist/math/referenceLineSeries.d.ts +12 -0
  43. package/dist/math/referenceLineSeries.d.ts.map +1 -0
  44. package/dist/math/referenceLines.d.ts +5 -5
  45. package/dist/math/referenceLines.d.ts.map +1 -1
  46. package/dist/math/spline.d.ts +5 -1
  47. package/dist/math/spline.d.ts.map +1 -1
  48. package/dist/types.d.ts +188 -14
  49. package/dist/types.d.ts.map +1 -1
  50. package/package.json +1 -1
  51. package/src/components/CustomThresholdBadgeOverlay.tsx +74 -0
  52. package/src/components/CustomTooltipOverlay.tsx +7 -3
  53. package/src/components/LiveChart.tsx +320 -43
  54. package/src/components/ReferenceLineOverlay.tsx +38 -7
  55. package/src/components/ReferenceLineSeriesOverlay.tsx +141 -0
  56. package/src/core/liveChartEngineTick.ts +34 -1
  57. package/src/core/resolveConfig.ts +186 -0
  58. package/src/core/useLiveChartEngine.ts +37 -5
  59. package/src/draw/candle.ts +1 -1
  60. package/src/draw/candleGap.ts +132 -0
  61. package/src/draw/lineGap.ts +180 -0
  62. package/src/hooks/crosshairShared.ts +92 -11
  63. package/src/hooks/useCandleGapPaths.ts +88 -0
  64. package/src/hooks/useChartPaths.ts +81 -22
  65. package/src/hooks/useCrosshair.ts +126 -40
  66. package/src/hooks/useLineGapPaths.ts +76 -0
  67. package/src/hooks/useReferenceLine.ts +6 -5
  68. package/src/hooks/useReferenceLineSeries.ts +101 -0
  69. package/src/hooks/useThreshold.ts +31 -22
  70. package/src/index.ts +15 -0
  71. package/src/math/candleGaps.ts +120 -0
  72. package/src/math/referenceLineSeries.ts +83 -0
  73. package/src/math/referenceLines.ts +34 -6
  74. package/src/math/spline.ts +15 -5
  75. package/src/types.ts +199 -14
@@ -95,15 +95,15 @@ export interface ThresholdSeriesGeometry {
95
95
  samples: SharedValue<number[]>;
96
96
  /** Whether any of the polyline is on-screen (drives marker-line opacity). */
97
97
  visible: SharedValue<boolean>;
98
- /** Threshold value at `now` (flat-extended past the last point) — the badge label. */
99
- currentValue: SharedValue<number>;
100
- /** Pixel-Y of `currentValue` — anchors the badge. */
101
- currentLineY: SharedValue<number>;
102
- /** Whether the badge should show: `currentLineY` on-plot AND, with
103
- * `extendToNow` off, "now" not past the series' last point. (`visible` can be
104
- * true for the polyline while the value-at-now sits outside the plot; the
105
- * badge must not draw into the gutters then.) */
106
- currentVisible: SharedValue<boolean>;
98
+ /** Threshold value at the configured badge anchor: the visible window's
99
+ * left edge (`"first"`) or its live/right edge (`"last"`). */
100
+ badgeValue: SharedValue<number>;
101
+ /** Pixel-Y of `badgeValue` — anchors the badge. */
102
+ badgeLineY: SharedValue<number>;
103
+ /** Whether the configured badge anchor is on-plot and belongs to a visible
104
+ * part of the threshold. (`visible` can still be true when that one endpoint
105
+ * is off-plot.) */
106
+ badgeVisible: SharedValue<boolean>;
107
107
  /** Pixel-X where the threshold ends: the last point's X with `extendToNow`
108
108
  * off, else {@link THRESHOLD_NO_CLIP}. The shader paints its plain
109
109
  * `restColor` right of it; the marker polyline stops there. */
@@ -118,9 +118,9 @@ const EMPTY_SAMPLES: number[] = new Array(THRESHOLD_SAMPLE_COUNT).fill(0);
118
118
  * Per-frame screen geometry for a **time-varying** threshold — a plain
119
119
  * `LiveChartPoint[]` `value` or a live `SharedValue<LiveChartPoint[]>` `series`
120
120
  * (which wins when both are given): the screen polyline (marker line +
121
- * fill-band bottom), the shader's pixel-Y `samples[]`, the current value/anchor
122
- * for the badge, and the `extendToNow` cutoff X. The array buffers ping-pong
123
- * (Reanimated only re-notifies subscribers when the returned reference
121
+ * fill-band bottom), the shader's pixel-Y `samples[]`, the configured endpoint
122
+ * value/anchor for the badge, and the `extendToNow` cutoff X. The array buffers
123
+ * ping-pong (Reanimated only re-notifies subscribers when the returned reference
124
124
  * changes). When the threshold is a constant `SharedValue<number>` every
125
125
  * worklet short-circuits cheaply and {@link useThreshold} drives the render
126
126
  * instead.
@@ -131,6 +131,7 @@ export function useThresholdSeries(
131
131
  value: ThresholdValue,
132
132
  series: SharedValue<LiveChartPoint[]> | null = null,
133
133
  extendToNow = true,
134
+ labelAnchor: "first" | "last" = "last",
134
135
  ): ThresholdSeriesGeometry {
135
136
  const cacheRef = useRef<{
136
137
  ptsA: number[];
@@ -238,15 +239,19 @@ export function useThresholdSeries(
238
239
  );
239
240
  });
240
241
 
241
- const currentValue = useDerivedValue(() => {
242
+ const badgeValue = useDerivedValue(() => {
242
243
  const pts = series ? series.get() : Array.isArray(value) ? value : null;
243
244
  if (pts === null) return NaN;
244
- return interpolateAtTime(pts, engine.timestamp.get()) ?? NaN;
245
+ const time =
246
+ labelAnchor === "first"
247
+ ? engine.timestamp.get() - engine.displayWindow.get()
248
+ : engine.timestamp.get();
249
+ return interpolateAtTime(pts, time) ?? NaN;
245
250
  });
246
251
 
247
- const currentLineY = useDerivedValue(() =>
252
+ const badgeLineY = useDerivedValue(() =>
248
253
  thresholdLineY(
249
- currentValue.get(),
254
+ badgeValue.get(),
250
255
  engine.displayMin.get(),
251
256
  engine.displayMax.get(),
252
257
  engine.canvasHeight.get(),
@@ -255,15 +260,19 @@ export function useThresholdSeries(
255
260
  ),
256
261
  );
257
262
 
258
- const currentVisible = useDerivedValue(() => {
259
- if (!extendToNow) {
263
+ const badgeVisible = useDerivedValue(() => {
264
+ if (labelAnchor === "first") {
265
+ // If the non-extended series ended before this window, there is no left
266
+ // endpoint to label even though interpolation can still clamp a value.
267
+ if (screenPts.get().length < 4) return false;
268
+ } else if (!extendToNow) {
260
269
  // The threshold ends at its last point — no badge past it.
261
270
  const pts = series ? series.get() : Array.isArray(value) ? value : null;
262
271
  if (pts === null || pts.length === 0) return false;
263
272
  if (pts[pts.length - 1].time < engine.timestamp.get()) return false;
264
273
  }
265
274
  return thresholdVisible(
266
- currentLineY.get(),
275
+ badgeLineY.get(),
267
276
  engine.canvasHeight.get(),
268
277
  padding.top,
269
278
  padding.bottom,
@@ -274,9 +283,9 @@ export function useThresholdSeries(
274
283
  screenPts,
275
284
  samples,
276
285
  visible,
277
- currentValue,
278
- currentLineY,
279
- currentVisible,
286
+ badgeValue,
287
+ badgeLineY,
288
+ badgeVisible,
280
289
  clipRightX,
281
290
  };
282
291
  }
package/src/index.ts CHANGED
@@ -39,9 +39,23 @@ export type {
39
39
  BadgeMetrics,
40
40
  BadgeStyleConfig,
41
41
  BadgeVariant,
42
+ CandleGap,
43
+ CandleGapBandStyle,
44
+ CandleGapBridgeStyle,
45
+ CandleGapKind,
46
+ CandleGapLabelStyle,
47
+ CandleGapStyle,
48
+ CandleGapsConfig,
42
49
  CandleMetrics,
43
50
  CandlePoint,
44
51
  CanvasMode,
52
+ ChartGap,
53
+ ChartGapBandStyle,
54
+ ChartGapBridgeStyle,
55
+ ChartGapKind,
56
+ ChartGapLabelStyle,
57
+ ChartGapsConfig,
58
+ ChartGapStyle,
45
59
  ChartInsets,
46
60
  ChartOverlayContext,
47
61
  ChartPlotRect,
@@ -100,6 +114,7 @@ export type {
100
114
  SeriesConfig,
101
115
  ThemeMode,
102
116
  ThresholdConfig,
117
+ ThresholdBadgeRenderProps,
103
118
  ThresholdLineConfig,
104
119
  TooltipRenderProps,
105
120
  TradeEvent,
@@ -0,0 +1,120 @@
1
+ import type { CandleGap, CandleGapKind, CandlePoint } from "../types";
2
+
3
+ /** Built-in, overridable copy for gap bands and scrub tooltips. */
4
+ export function candleGapDefaultLabel(kind: CandleGapKind): string {
5
+ "worklet";
6
+ if (kind === "no-trades") return "No trades";
7
+ if (kind === "unavailable") return "Trading unavailable";
8
+ return "Data unavailable";
9
+ }
10
+
11
+ /** Find the explicit half-open gap containing `time`. Input must be start-sorted. */
12
+ export function pickCandleGapAtTime(
13
+ gaps: CandleGap[],
14
+ time: number,
15
+ ): CandleGap | null {
16
+ "worklet";
17
+ let lo = 0;
18
+ let hi = gaps.length - 1;
19
+ while (lo <= hi) {
20
+ const mid = (lo + hi) >> 1;
21
+ if (gaps[mid].from <= time) lo = mid + 1;
22
+ else hi = mid - 1;
23
+ }
24
+ if (hi < 0) return null;
25
+ const gap = gaps[hi];
26
+ return time >= gap.from && time < gap.to ? gap : null;
27
+ }
28
+
29
+ /** Latest real candle strictly before `time`, preferring a newer live candle. */
30
+ export function previousCandleAtTime(
31
+ candles: CandlePoint[],
32
+ liveCandle: CandlePoint | null,
33
+ time: number,
34
+ ): CandlePoint | null {
35
+ "worklet";
36
+ let lo = 0;
37
+ let hi = candles.length;
38
+ while (lo < hi) {
39
+ const mid = (lo + hi) >> 1;
40
+ if (candles[mid].time < time) lo = mid + 1;
41
+ else hi = mid;
42
+ }
43
+ let result = lo > 0 ? candles[lo - 1] : null;
44
+ if (
45
+ liveCandle &&
46
+ liveCandle.time < time &&
47
+ (!result || liveCandle.time > result.time)
48
+ ) {
49
+ result = liveCandle;
50
+ }
51
+ return result;
52
+ }
53
+
54
+ /** Previous observed close for a bridged gap; `null` when no anchor exists. */
55
+ export function previousCandleCloseAtTime(
56
+ candles: CandlePoint[],
57
+ liveCandle: CandlePoint | null,
58
+ time: number,
59
+ ): number | null {
60
+ "worklet";
61
+ return previousCandleAtTime(candles, liveCandle, time)?.close ?? null;
62
+ }
63
+
64
+ /** Whether a real candle starts in the bucket beginning at `bucketTime`. */
65
+ export function candleAtBucketTime(
66
+ candles: CandlePoint[],
67
+ liveCandle: CandlePoint | null,
68
+ bucketTime: number,
69
+ candleWidthSecs: number,
70
+ ): CandlePoint | null {
71
+ "worklet";
72
+ let lo = 0;
73
+ let hi = candles.length - 1;
74
+ while (lo <= hi) {
75
+ const mid = (lo + hi) >> 1;
76
+ const t = candles[mid].time;
77
+ if (t < bucketTime) lo = mid + 1;
78
+ else if (t > bucketTime) hi = mid - 1;
79
+ else return candles[mid];
80
+ }
81
+ if (
82
+ liveCandle &&
83
+ Math.abs(liveCandle.time - bucketTime) < candleWidthSecs * 1e-6
84
+ ) {
85
+ return liveCandle;
86
+ }
87
+ return null;
88
+ }
89
+
90
+ /**
91
+ * Bucket start under `time`, aligned to the nearest preceding real candle.
92
+ * Returns `null` for a partial edge bucket, a bucket with real OHLC, or a gap
93
+ * without a previous-close anchor.
94
+ */
95
+ export function candleGapBucketStartAtTime(
96
+ gap: CandleGap,
97
+ time: number,
98
+ candles: CandlePoint[],
99
+ liveCandle: CandlePoint | null,
100
+ candleWidthSecs: number,
101
+ ): number | null {
102
+ "worklet";
103
+ if (
104
+ candleWidthSecs <= 0 ||
105
+ time < gap.from ||
106
+ time >= gap.to ||
107
+ gap.kind !== "no-trades"
108
+ ) {
109
+ return null;
110
+ }
111
+ const previous = previousCandleAtTime(candles, liveCandle, time);
112
+ if (!previous) return null;
113
+ const steps = Math.floor((time - previous.time) / candleWidthSecs);
114
+ const bucket = previous.time + steps * candleWidthSecs;
115
+ if (bucket < gap.from || bucket + candleWidthSecs > gap.to) return null;
116
+ if (candleAtBucketTime(candles, liveCandle, bucket, candleWidthSecs)) {
117
+ return null;
118
+ }
119
+ return bucket;
120
+ }
@@ -0,0 +1,83 @@
1
+ import type { ChartPadding } from "../draw/line";
2
+ import type { LiveChartPoint } from "../types";
3
+ import { interpolateAtTime } from "./interpolate";
4
+ import { thresholdLineY } from "./threshold";
5
+
6
+ /**
7
+ * Project a historical reference-line series into screen-space `[x, y, …]`
8
+ * points for the visible time window. Window-edge values are interpolated so
9
+ * the path enters and leaves the plot cleanly. The series clamps to its first
10
+ * value on the left and, by default, to its last value at the live edge.
11
+ *
12
+ * Worklet-safe. When `out` is supplied it is cleared and reused.
13
+ */
14
+ export function buildReferenceLineSeriesPoints(
15
+ points: LiveChartPoint[],
16
+ now: number,
17
+ windowSecs: number,
18
+ displayMin: number,
19
+ displayMax: number,
20
+ canvasWidth: number,
21
+ canvasHeight: number,
22
+ padding: ChartPadding,
23
+ extendToNow: boolean,
24
+ out?: number[],
25
+ ): number[] {
26
+ "worklet";
27
+ const result = out ?? [];
28
+ result.length = 0;
29
+
30
+ const plotLeft = padding.left;
31
+ const plotRight = canvasWidth - padding.right;
32
+ const plotWidth = plotRight - plotLeft;
33
+ const plotHeight = canvasHeight - padding.top - padding.bottom;
34
+ const valueRange = displayMax - displayMin;
35
+ if (
36
+ points.length === 0 ||
37
+ !(windowSecs > 0) ||
38
+ plotWidth <= 0 ||
39
+ plotHeight <= 0 ||
40
+ !(valueRange > 0)
41
+ ) {
42
+ return result;
43
+ }
44
+
45
+ const windowStart = now - windowSecs;
46
+ const lastTime = points[points.length - 1].time;
47
+ const endTime = extendToNow ? now : Math.min(now, lastTime);
48
+ if (endTime < windowStart) return result;
49
+
50
+ const startValue = interpolateAtTime(points, windowStart);
51
+ const endValue = interpolateAtTime(points, endTime);
52
+ if (
53
+ startValue === null ||
54
+ endValue === null ||
55
+ !Number.isFinite(startValue) ||
56
+ !Number.isFinite(endValue)
57
+ ) {
58
+ return result;
59
+ }
60
+
61
+ const toX = (time: number) =>
62
+ plotLeft + ((time - windowStart) / windowSecs) * plotWidth;
63
+ const toY = (value: number) =>
64
+ thresholdLineY(
65
+ value,
66
+ displayMin,
67
+ displayMax,
68
+ canvasHeight,
69
+ padding.top,
70
+ padding.bottom,
71
+ );
72
+
73
+ result.push(plotLeft, toY(startValue));
74
+ for (let i = 0; i < points.length; i++) {
75
+ const point = points[i];
76
+ if (point.time <= windowStart) continue;
77
+ if (point.time >= endTime) break;
78
+ if (!Number.isFinite(point.time) || !Number.isFinite(point.value)) continue;
79
+ result.push(toX(point.time), toY(point.value));
80
+ }
81
+ result.push(toX(endTime), toY(endValue));
82
+ return result;
83
+ }
@@ -4,16 +4,22 @@ import type {
4
4
  ReferenceLineBadgeConfig,
5
5
  } from "../types";
6
6
 
7
- /** Which of the three reference-line forms a `ReferenceLine` resolves to. */
8
- export type ReferenceLineForm = "line" | "value-band" | "time-band" | "none";
7
+ /** Which of the four reference-line forms a `ReferenceLine` resolves to. */
8
+ export type ReferenceLineForm =
9
+ | "line"
10
+ | "series"
11
+ | "value-band"
12
+ | "time-band"
13
+ | "none";
9
14
 
10
15
  /**
11
- * Classify a `ReferenceLine` into one of its three forms, applying the
12
- * documented precedence A (line) > B (value band) > C (time band).
16
+ * Classify a `ReferenceLine` into one of its four forms, applying the
17
+ * documented precedence A (line) > B (series) > C (value band) > D (time band).
13
18
  */
14
19
  export function referenceLineForm(rl: ReferenceLine): ReferenceLineForm {
15
20
  "worklet";
16
21
  if (rl.value !== undefined) return "line";
22
+ if (rl.series !== undefined) return "series";
17
23
  if (rl.valueFrom !== undefined && rl.valueTo !== undefined) {
18
24
  return "value-band";
19
25
  }
@@ -24,7 +30,7 @@ export function referenceLineForm(rl: ReferenceLine): ReferenceLineForm {
24
30
  /**
25
31
  * Gather every Y value a set of reference lines should contribute to the
26
32
  * axis-range computation. Lines flagged `excludeFromRange` are skipped, as are
27
- * time bands (Form C) which constrain time, not value.
33
+ * time bands (Form D) which constrain time, not value.
28
34
  */
29
35
  export function collectReferenceValues(lines: ReferenceLine[]): number[] {
30
36
  const out: number[] = [];
@@ -35,6 +41,22 @@ export function collectReferenceValues(lines: ReferenceLine[]): number[] {
35
41
  case "line":
36
42
  out.push(rl.value as number);
37
43
  break;
44
+ case "series": {
45
+ const points = rl.series;
46
+ if (!points || points.length === 0) break;
47
+ let min = Infinity;
48
+ let max = -Infinity;
49
+ for (let j = 0; j < points.length; j++) {
50
+ const value = points[j].value;
51
+ if (!Number.isFinite(value)) continue;
52
+ if (value < min) min = value;
53
+ if (value > max) max = value;
54
+ }
55
+ // Only the extrema are needed by the engine, avoiding a per-frame scan
56
+ // through the full historical annotation series.
57
+ if (min !== Infinity) out.push(min, max);
58
+ break;
59
+ }
38
60
  case "value-band":
39
61
  out.push(rl.valueFrom as number, rl.valueTo as number);
40
62
  break;
@@ -56,7 +78,13 @@ export function referenceLineReactKeys(
56
78
  const occurrences = new Map<string, number>();
57
79
  const keys: string[] = [];
58
80
  for (const line of lines) {
59
- const base = line.id ?? stableReferenceLineSignature(line);
81
+ // A series' samples are render data, not identity. Omitting their contents
82
+ // keeps a growing historical series from remounting its overlay every render.
83
+ const base =
84
+ line.id ??
85
+ stableReferenceLineSignature(
86
+ line.series === undefined ? line : { ...line, series: "series" },
87
+ );
60
88
  const occurrence = occurrences.get(base) ?? 0;
61
89
  occurrences.set(base, occurrence + 1);
62
90
  keys.push(`${base}:${occurrence}`);
@@ -56,16 +56,26 @@ export function drawSpline(
56
56
  /** Straight polyline (`lineTo` per point) instead of the monotone cubic — an
57
57
  * angular, hard-edged line. The caller has already `moveTo`'d point 0. */
58
58
  linear = false,
59
+ /** First point index to draw (inclusive). The caller must moveTo this point. */
60
+ startPoint = 0,
61
+ /** Last point index to draw (exclusive). Defaults to the full array. */
62
+ endPoint = pts.length >> 1,
59
63
  ) {
60
64
  "worklet";
61
- const n = pts.length >> 1;
65
+ const start = Math.max(0, startPoint);
66
+ const end = Math.min(pts.length >> 1, endPoint);
67
+ const n = end - start;
62
68
  if (n < 2) return;
63
69
  if (linear) {
64
- for (let i = 1; i < n; i++) path.lineTo(pts[i * 2], pts[i * 2 + 1]);
70
+ for (let i = 1; i < n; i++) {
71
+ const point = (start + i) * 2;
72
+ path.lineTo(pts[point], pts[point + 1]);
73
+ }
65
74
  return;
66
75
  }
67
76
  if (n === 2) {
68
- path.lineTo(pts[2], pts[3]);
77
+ const point = (start + 1) * 2;
78
+ path.lineTo(pts[point], pts[point + 1]);
69
79
  return;
70
80
  }
71
81
 
@@ -73,7 +83,7 @@ export function drawSpline(
73
83
  const delta: number[] = scratch ? scratch.delta : new Array(n - 1);
74
84
  const h: number[] = scratch ? scratch.h : new Array(n - 1);
75
85
  for (let i = 0; i < n - 1; i++) {
76
- const i2 = i * 2;
86
+ const i2 = (start + i) * 2;
77
87
  const j2 = i2 + 2;
78
88
  h[i] = pts[j2] - pts[i2];
79
89
  delta[i] = h[i] === 0 ? 0 : (pts[j2 + 1] - pts[i2 + 1]) / h[i];
@@ -110,7 +120,7 @@ export function drawSpline(
110
120
 
111
121
  // 4. Draw bezier curves
112
122
  for (let i = 0; i < n - 1; i++) {
113
- const i2 = i * 2;
123
+ const i2 = (start + i) * 2;
114
124
  const j2 = i2 + 2;
115
125
  const hi = h[i];
116
126
  path.cubicTo(