> ## Documentation Index
> Fetch the complete documentation index at: https://react-native-livechart.brandtnewlabs.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Threshold split

> Color the line above vs. below a live benchmark — green above, red below — with an optional P/L fill band.

<Note>
  **Live example:**
  [`app/demo/threshold.tsx`](https://github.com/brandtnewlabs/react-native-livechart/blob/main/app/demo/threshold.tsx)
  in the example app.
</Note>

Where a [reference line](/guides/reference-lines-and-bands) is a *static* annotation, `threshold`
colors the line itself **above vs. below a value** — green above, red below by default. The value
is either a single live benchmark (a `SharedValue<number>` that tracks on the UI thread without
re-rendering — break-even / average cost, VWAP, the previous close, a stablecoin peg) or a
[time-varying series](#time-varying-threshold-a-series) (a `LiveChartPoint[]` the split follows
point-for-point). Single-series `LiveChart`, line mode.

<Frame caption="Green above, red below a live break-even — with an optional P/L fill band">
  <video autoPlay loop muted playsInline controls poster="/media/threshold.jpg" src="https://mintcdn.com/brandtnewlabs/3aK_iydbC8Ff9g3s/media/threshold.mp4?fit=max&auto=format&n=3aK_iydbC8Ff9g3s&q=85&s=23b1c9f4fc9e5796eb593af5fe93615c" data-path="media/threshold.mp4" />
</Frame>

```tsx theme={null}
import { useSharedValue } from "react-native-reanimated";
import { LiveChart } from "react-native-livechart";

const breakEven = useSharedValue(100); // move it live with breakEven.set(...)

<LiveChart data={data} value={value} threshold={{ value: breakEven }} />;
```

A bare `{ value }` splits the **stroke** at the threshold (palette green above, red
below; override with `aboveColor` / `belowColor`). Two opt-in extras share the same
`SharedValue`:

* **`fill`** — tints the area *between the line and the threshold* (the profit/loss
  band) toward the above/below colors. It's independent of the baseline `gradient`
  fill, so set `gradient={false}` for the band alone. `true` uses the default band
  opacity (`0.16`, matching reference-line bands); pass `{ opacity: 0.35 }` to tune it.
* **`line`** — a dashed marker line at the threshold. `true` draws just the line;
  pass a `ThresholdLineConfig` to add a label badge (`labelColor` overrides the
  label's text color, like `ReferenceLine.labelColor`).
* **`includeInRange`** — fold the threshold into the Y-axis range fit, like
  reference lines do. Without it the range fits the data alone, so a benchmark
  outside the price range renders invisibly (marker off-plot, whole line one
  color). Default `false`.

```tsx theme={null}
<LiveChart
  data={data}
  value={value}
  gradient={false}
  threshold={{
    value: breakEven,
    aboveColor: "#16a34a",
    belowColor: "#dc2626",
    fill: true,
    // object → labelled badge; `line: true` → marker line only (no text/badge)
    line: { label: "Break-even", showValue: true },
  }}
/>;
```

The label renders as an opaque **badge** anchored flush to the plot's left edge
(`labelPosition: "left"`, the default — clear of the y-axis labels) or the right
gutter, and is drawn on top of the line so it's never painted over. For a
time-varying series, `labelAnchor` independently selects the threshold value and
Y position: `"first"` samples the visible window's left edge and `"last"` (the
default) samples the live/right edge. This lets a left-positioned badge line up
with the threshold where it enters the plot:

```tsx theme={null}
threshold={{
  series: vwap,
  line: {
    label: "VWAP",
    showValue: true,
    labelPosition: "left",
    labelAnchor: "first",
  },
}}
```

While a threshold is set it supersedes `line.color` / `line.colors` and segment
recoloring for the main stroke.

## Custom threshold badge

Use `renderThresholdBadge` when the badge needs React Native content the built-in
Skia pill cannot provide: a branded average-cost tag, icon, status chip, blur, or
an interactive child. The chart still owns the position and follows the live
threshold on the UI thread; your component owns only the badge's contents and
chrome. The dashed marker line remains built in.

```tsx theme={null}
import { TextInput, View, Text } from "react-native";
import Animated, { useAnimatedProps } from "react-native-reanimated";
import {
  LiveChart,
  type ThresholdBadgeRenderProps,
} from "react-native-livechart";

const AnimatedTextInput = Animated.createAnimatedComponent(TextInput);

function AverageCostBadge({ valueStr }: ThresholdBadgeRenderProps) {
  const animatedProps = useAnimatedProps(() => {
    const text = valueStr.get();
    return { text, defaultValue: text };
  });

  return (
    <View style={{ flexDirection: "row", backgroundColor: "#0f172a" }}>
      <Text>B/E</Text>
      <AnimatedTextInput editable={false} animatedProps={animatedProps} />
    </View>
  );
}

<LiveChart
  data={data}
  value={value}
  threshold={{
    value: breakEven,
    line: { label: "Break-even", showValue: true },
  }}
  renderThresholdBadge={(ctx) => <AverageCostBadge {...ctx} />}
/>
```

`ThresholdBadgeRenderProps` contains `value`, `valueStr`, `y`, and `visible`
SharedValues plus the resolved `line` config. Bind `valueStr` to animated text as
above so the badge value updates without a React render. Returning `null` or
`undefined` keeps the built-in badge. `renderThresholdBadge` has no effect unless
`threshold.line` is enabled.

## Time-varying threshold (a series)

Pass a `LiveChartPoint[]` as `value` instead of a `SharedValue<number>` and the
threshold becomes a **line**, not a level — the stroke split, fill band and marker
all follow it point-for-point. This is the natural fit for a benchmark that
*changes over time*: a break-even / average cost that steps up as you DCA in, a
historical VWAP, a moving peg. For a series that updates **live**, pass a
`SharedValue<LiveChartPoint[]>` as `series` instead (below).

```tsx theme={null}
import { LiveChart, type LiveChartPoint } from "react-native-livechart";

// A stepped average-cost (two points per step → a clean vertical riser).
const breakEven: LiveChartPoint[] = [
  { time: t0, value: 96.5 },
  { time: t1, value: 96.5 },
  { time: t1, value: 99.0 }, // ← step up
  { time: t2, value: 99.0 },
  { time: t2, value: 101.2 },
  { time: t3, value: 101.2 },
];

<LiveChart
  data={data}
  value={value}
  threshold={{ value: breakEven, fill: true, extendToStart: false }}
/>;
```

* By default, the series is **clamped to its first/last value** outside its own time range. So
  a threshold whose last point sits behind the live edge **extends as a flat line
  to "now"** — carrying the last known break-even forward — and a window that
  starts before the first point holds the first value.
* **`extendToStart: false`** opts out of the backward extension when the benchmark
  did not exist yet (for example, break-even before the first trade). Left of the
  first point the stroke keeps its plain line color; the fill and marker are
  absent, the threshold does not affect range fitting, and a left-positioned
  badge follows the marker start while remaining inside the plot.
* Points are connected with **straight segments**. For a step function, emit two
  points at each boundary (same time, old then new value) as above; for a smooth
  benchmark, emit it as you'd sample it.
* The split renders from **64 samples** across the plot (a GPU shader), so a hard
  step becomes a riser about 1/63 of the plot wide — visually a clean vertical at
  phone sizes. The stroke split, band and marker share the same samples, so they
  always agree at the riser.
* `aboveColor` / `belowColor` accept **hex (`#rgb`/`#rrggbb`), `rgb()` and
  `rgba()`** in series mode (an `rgba()` alpha carries into the stroke and scales
  the band). Named CSS colors and 8-digit hex are only supported by the constant
  form — in series mode they fall back to grey.
* The marker `line`'s badge shows the threshold's live-edge value by default.
  Set `labelAnchor: "first"` to show and align to the value at the visible
  window's left edge instead; `labelPosition` still controls the badge's X side.
* An **empty series** (`[]`) renders as "no threshold yet": plain line color, no
  band, no marker — handy while the threshold history is still loading.
* **`extendToNow: false`** opts out of the flat extension for a benchmark that
  must not project into the future (a closed session's VWAP): right of the last
  point the stroke keeps its plain line color and the band / marker / badge end.

Unlike the `SharedValue<number>` form, a `value` series flows in on **re-render**
(it's a plain array). Pass a stable, memoized array and update it when the
benchmark actually changes. For a threshold series that updates **live** — a VWAP
recomputed every tick — use the `series` form, a `SharedValue<LiveChartPoint[]>`
that works like the chart's own `data` prop: update it with `.set()` /
`.modify()` and the split tracks on the UI thread without re-rendering.

```tsx theme={null}
const vwap = useSharedValue<LiveChartPoint[]>([]);
// on each tick: vwap.set([...vwap.get(), { time, value }])

<LiveChart data={data} value={value} threshold={{ series: vwap, fill: true }} />;
```

<Note>
  Candle mode and `LiveChartSeries` (multi-series) are unaffected — threshold is a
  single-series line-mode feature in both forms.
</Note>

See [`ThresholdConfig`](/api-reference/types#thresholdconfig) for every field.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.