Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added

- **Time-varying reference lines.** `ReferenceLine.series` draws a historical
`LiveChartPoint[]` benchmark over line or candle charts, with the existing
color, dash, width, label, and range-fitting behavior. The final value extends
flat to the live edge by default; `extendToNow: false` stops at the last point.
- **Custom threshold badges.** `LiveChart.renderThresholdBadge` replaces the
built-in Skia threshold pill with any React Native element while the chart
continues to position it from the live threshold value on the UI thread. Its
Expand Down
64 changes: 61 additions & 3 deletions app/demo/candlestick.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import { useState } from "react";
import type { CandlePoint } from "react-native-livechart";
import { LiveChart } from "react-native-livechart";
import {
LiveChart,
type CandlePoint,
type LiveChartPoint,
type ReferenceLine,
} from "react-native-livechart";
import { useSharedValue } from "react-native-reanimated";

import { DemoScreen } from "../../demo-lib/DemoScreen";
Expand Down Expand Up @@ -75,6 +79,24 @@ const VOLUME_ROUND_OPTIONS: { value: number; label: string }[] = [
/** Distinct violet bars so the volume color override reads as a feature. */
const CUSTOM_VOLUME_COLORS = { upColor: "#8b5cf6", downColor: "#4c1d95" };

/**
* Historical average-cost changes from a realistic averaging-in flow. Adjacent
* points around each fill make the cost basis step at that timestamp instead of
* drifting diagonally between fills.
*/
function buildAverageCostSeries(anchor: number): LiveChartPoint[] {
return [
{ time: anchor - 3_600, value: 100.8 },
{ time: anchor - 780.01, value: 100.8 },
{ time: anchor - 780, value: 99.6 },
{ time: anchor - 360.01, value: 99.6 },
{ time: anchor - 360, value: 101.2 },
{ time: anchor - 90.01, value: 101.2 },
{ time: anchor - 90, value: 100.4 },
{ time: anchor - 30, value: 100.4 },
];
}

export default function CandlestickScreen() {
const [tfLabel, setTfLabel] = useState<TimeframeLabel>("15m · 1m");
const [stripCandles, setStripCandles] = useState(false);
Expand All @@ -86,6 +108,9 @@ export default function CandlestickScreen() {
const [volumeRound, setVolumeRound] = useState(2);
const [customVolumeColors, setCustomVolumeColors] = useState(false);
const [snapToCandles, setSnapToCandles] = useState(false);
const [averageCost, setAverageCost] = useState(true);
const [extendAverageCost, setExtendAverageCost] = useState(true);
const [referenceAnchor] = useState(() => Date.now() / 1000);

const tf = TIMEFRAMES.find((t) => t.label === tfLabel) ?? TIMEFRAMES[1];
const candleWidthSecs = tf.candleWidthSecs;
Expand All @@ -111,11 +136,28 @@ export default function CandlestickScreen() {
maxPoints: 10000,
});

const referenceLines: ReferenceLine[] = averageCost
? [
{
id: "average-cost",
series: buildAverageCostSeries(referenceAnchor),
extendToNow: extendAverageCost,
label: "Avg cost",
showValue: true,
labelPosition: "right",
color: "#f59e0b",
labelColor: "#b45309",
strokeWidth: 2,
intervals: [6, 4],
},
]
: [];

return (
<DemoScreen
title="Candlestick"
docs="guides/candlestick"
description={`mode="candle" with ${candleWidthSecs}s OHLC buckets. Each candle aggregates many ticks, so it shows a real body + wick. Needs ≥2 committed candles before it draws.`}
description={`mode="candle" with ${candleWidthSecs}s OHLC buckets plus a historical average-cost reference series. Each candle aggregates many ticks, so it shows a real body + wick. Needs ≥2 committed candles before it draws.`}
chart={
<LiveChart
data={data}
Expand All @@ -139,9 +181,25 @@ export default function CandlestickScreen() {
: false
}
scrub={{ tooltip: true, snapToCandles }}
referenceLines={referenceLines}
/>
}
>
<ControlRow label="Reference series">
<ToggleChip
label="Historical average cost"
value={averageCost}
onChange={setAverageCost}
/>
{averageCost ? (
<ToggleChip
label="Extend to live edge"
value={extendAverageCost}
onChange={setExtendAverageCost}
/>
) : null}
</ControlRow>

<ChipRow
label="Timeframe (window · candle)"
options={TIMEFRAME_OPTIONS}
Expand Down
6 changes: 4 additions & 2 deletions docs/api-reference/livechart.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -258,8 +258,10 @@ provided; everything else has a default.
</ParamField>

<ParamField body="referenceLines" type="ReferenceLine[]">
Reference lines / bands (horizontal line, value band, or time band). Form-A lines
can be `draggable` (drag to set a price, with `snap` / `bounds` and `onChange` /
Reference lines / bands (horizontal line, time-varying `series`, value band, or
time band). Series lines work in both line and candle mode and extend their last
value to the live edge unless `extendToNow` is `false`. Form-A lines can be
`draggable` (drag to set a price, with `snap` / `bounds` and `onChange` /
`onCommit` / `onDragIn` / `onDragOut` callbacks). See
[Reference lines](/guides/reference-lines-and-bands).
</ParamField>
Expand Down
11 changes: 7 additions & 4 deletions docs/api-reference/types.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -179,10 +179,13 @@ interface ReferenceLine {
id?: string; // stable identity; supply when lines may reorder
// Form A — horizontal line:
value?: number;
// Form B — horizontal band:
// Form B — time-varying line (sorted unix-second points):
series?: LiveChartPoint[];
extendToNow?: boolean; // flat-extend the last value to live; default true
// Form C — horizontal band:
valueFrom?: number;
valueTo?: number;
// Form C — vertical time band (unix seconds):
// Form D — vertical time band (unix seconds):
from?: number;
to?: number;
// Shared:
Expand All @@ -192,8 +195,8 @@ interface ReferenceLine {
color?: string;
fillColor?: string; // band fill; defaults to color, then palette refLine
fillOpacity?: number; // default 0.16
strokeOpacity?: number; // line / band-border opacity; default 1
fullWidth?: boolean; // span edge-to-edge through the Y-axis gutter (Form A/B);
strokeOpacity?: number; // line / series / band-border opacity; default 1
fullWidth?: boolean; // span edge-to-edge through the Y-axis gutter (Form A/C);
// label/badge stay in the plot. default false
labelColor?: string;
labelPosition?: "left" | "center" | "right";
Expand Down
25 changes: 25 additions & 0 deletions docs/guides/candlestick.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,31 @@ Configure colors, band height, rounding, and opacity with a
by candle direction (up/down) unless you override `upColor` / `downColor`.
</Note>

## Historical reference series

Overlay a changing average cost, VWAP, peg, or other historical benchmark with the
[`referenceLines.series`](/guides/reference-lines-and-bands#time-varying-reference-line-series)
form. It draws a Skia polyline over the candles using the same time and Y scales, supports the
normal reference-line color, dash, width, and label fields, and extends its last value flat to
the live edge by default.

```tsx
<LiveChart
mode="candle"
candles={candles}
liveCandle={liveCandle}
referenceLines={[
{
series: averageCostHistory,
label: "Avg cost",
showValue: true,
color: "#f59e0b",
strokeWidth: 2,
},
]}
/>
```

## Scrubbing & the OHLC tooltip

Pass `scrub` to enable the crosshair: the built-in tooltip shows the scrubbed candle's O/H/L/C
Expand Down
50 changes: 48 additions & 2 deletions docs/guides/reference-lines-and-bands.mdx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
title: Reference lines & bands
icon: "ruler-horizontal"
description: "Draw horizontal lines, value/time bands, pill badges, and draggable working orders."
description: "Draw horizontal or time-varying lines, value/time bands, pill badges, and draggable working orders."
---

<Note>
Expand All @@ -16,9 +16,10 @@ description: "Draw horizontal lines, value/time bands, pill badges, and draggabl
<video autoPlay loop muted playsInline controls poster="/media/reference-lines-and-bands.jpg" src="/media/reference-lines-and-bands.mp4" />
</Frame>

`referenceLines` accepts an array of `ReferenceLine`s in three mutually-exclusive forms:
`referenceLines` accepts an array of `ReferenceLine`s in four mutually-exclusive forms:

- **Horizontal line** at a `value`
- **Time-varying line** following a `series`
- **Horizontal band** between `valueFrom` and `valueTo`
- **Vertical time band** between `from` and `to` (unix seconds)

Expand Down Expand Up @@ -48,6 +49,51 @@ range (`excludeFromRange`). Bands can use separate `fillColor` / `fillOpacity` a
When the array can reorder (for example, a dynamic order book), give each line a unique `id`
so its rendered identity follows that line instead of its array position.

## Time-varying reference line (`series`)

Use the `series` form for a historical benchmark that changes over time: average cost after
multiple fills, VWAP, a moving target, or a peg. It is especially useful over candlesticks, where
threshold split coloring does not apply.

```tsx
import type { LiveChartPoint, ReferenceLine } from "react-native-livechart";

const averageCost: LiveChartPoint[] = [
{ time: 1_700_000_000, value: 101.2 },
{ time: 1_700_000_900, value: 99.8 },
{ time: 1_700_001_800, value: 100.4 },
];

const referenceLines: ReferenceLine[] = [
{
series: averageCost,
label: "Avg cost",
showValue: true,
color: "#f59e0b",
strokeWidth: 2,
intervals: [6, 4],
},
];

<LiveChart
mode="candle"
candles={candles}
liveCandle={liveCandle}
referenceLines={referenceLines}
/>
```

Points use unix-second timestamps and must be sorted oldest to newest. The first value extends
to the window's left edge, and the last value extends flat to the live edge. Set
`extendToNow: false` to stop the path and its label at the final point. A series contributes its
minimum and maximum to automatic Y-range fitting unless `excludeFromRange` is set.

The series form supports the shared line presentation fields: `color`, `strokeOpacity`,
`strokeWidth`, `intervals`, `label`, `labelColor`, `labelPosition`, `showValue`, and
`excludeFromRange`.
Working-order fields (`badge`, `draggable`, drag callbacks) and custom tag renderers remain for
single-value Form-A lines.

## Full-width lines (`fullWidth`)

By default a line/band stops at the plot's right edge. Set `fullWidth: true` to span it
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
import { MONO_FONT_FAMILY } from "../lib/monoFontFamily";
import { referenceLineForm, resolveReferenceBadge } from "../math/referenceLines";
import type { FontConfig, LiveChartPalette, ReferenceLine } from "../types";
import { ReferenceLineSeriesOverlay } from "./ReferenceLineSeriesOverlay";

/** Translucent fill alpha for value / time bands. */
const BAND_FILL_OPACITY = 0.16;
Expand All @@ -33,9 +34,10 @@ const CONNECTOR_GAP = 4;
const BADGE_EDGE_INSET = 2;

/**
* Renders one reference line or band into the chart canvas. Handles all three
* `ReferenceLine` forms (horizontal line, horizontal value band, vertical time
* band) plus the Form-A pill badge (in-range tag + off-screen chevron pin).
* Renders one reference line or band into the chart canvas. Handles all four
* `ReferenceLine` forms (horizontal line, time-varying line, horizontal value
* band, vertical time band) plus the Form-A pill badge (in-range tag +
* off-screen chevron pin).
* Self-contained so callers can `.map()` over a variable-length array.
*/
type ReferenceLineOverlayProps = {
Expand Down Expand Up @@ -94,7 +96,28 @@ type ReferenceLineOverlayProps = {
gridEndGap?: number;
};

export function ReferenceLineOverlay({
/**
* Dispatch to the series or scalar/band renderer without mixing their hook
* lifecycles. A stable `id` may therefore switch forms safely across renders.
*/
export function ReferenceLineOverlay(props: ReferenceLineOverlayProps) {
if (referenceLineForm(props.line) === "series") {
return (
<ReferenceLineSeriesOverlay
engine={props.engine}
padding={props.padding}
line={props.line}
palette={props.palette}
formatValue={props.formatValue}
font={props.font}
badgeLayer={props.badgeLayer ?? false}
/>
);
}
return <ReferenceLineStaticOverlay {...props} />;
}

function ReferenceLineStaticOverlay({
engine,
padding,
line,
Expand Down
Loading
Loading