Skip to main content

Axis indicators & live values

An indicator is a value pinned to an axis edge as a pill — the ChartIQ / Yahoo-Finance live price tag. Unlike an annotation, it always reads the axis coordinate (never a custom label): an indicator is a tick you placed. This page covers YAxisIndicator, the createLiveValue high-frequency path, and the x-axis pills produced by <Marker indicator>.

Crosshair the chart to read any sample; meanwhile the pill at the right jiggles live, driven by createLiveValue and coloured to match the line — only the pill repaints on each tick, not the chart:

src/examples/charts-indicator-live.tsx
import { useEffect, useRef } from 'react';
import {
ChartContainer,
ChartRow,
Layers,
LineChart,
YAxis,
YAxisIndicator,
createLiveValue,
} from '@pond-ts/charts';
import { useSiteChartTheme } from '@site/src/theme/useSiteChartTheme';
import { singleHostSeries } from './lib/server-metrics';

/** A tiny deterministic PRNG (mulberry32) — no external dependency. */
function mulberry32(seed: number): () => number {
let a = seed;
return () => {
a |= 0;
a = (a + 0x6d2b79f5) | 0;
let t = Math.imul(a ^ (a >>> 15), 1 | a);
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
};
}

/** Crosshair the chart to read any sample; meanwhile a live `YAxisIndicator`
* jiggles at the curve's leading edge — a mean-reverting random walk pushed
* through `createLiveValue.set()`, so only the pill repaints, not the chart.
* The pill takes the **line's colour** so it reads as that series' live value. */
export default function ChartsIndicatorLive() {
const theme = useSiteChartTheme();
const series = singleHostSeries();
const last = series.at(series.length - 1)?.get('cpu') ?? 0.4;

const live = useRef(createLiveValue(last)).current;
const rand = useRef(mulberry32(23)).current;
const valueRef = useRef(last);

useEffect(() => {
const id = setInterval(() => {
// Mean-revert toward `last` with a small per-tick jiggle — it wanders
// (live) but stays near the curve's end (doesn't drift off-screen).
const next =
valueRef.current +
(last - valueRef.current) * 0.1 +
(rand() - 0.5) * 0.05;
valueRef.current = Math.max(0.05, Math.min(0.95, next));
live.set(valueRef.current);
}, 150);
return () => clearInterval(id);
}, [live, rand, last]);

return (
<ChartContainer
range={series.timeRange()}
width={560}
theme={theme}
cursor="crosshair"
>
<ChartRow height={220}>
<YAxis id="pct" side="right" format=".0%" />
<Layers>
<LineChart series={series} column="cpu" axis="pct" />
<YAxisIndicator
source={live}
axis="pct"
color={theme.line.default.color}
format=".1%"
line
pointer
/>
</Layers>
</ChartRow>
</ChartContainer>
);
}

YAxisIndicator

A value pill pinned to a y-axis edge, rendered as a child of <Layers> so it shares the plot's coordinate space:

<Layers>
<LineChart series={price} axis="usd" />
<YAxisIndicator
source={liveLast}
axis="usd"
color="#4af"
format=",.2f"
line
/>
</Layers>
PropTypeDefaultPurpose
valuenumberA static value to pin. Pass this or source.
sourceLiveValueA LiveValue to subscribe to — the high-frequency path. Takes precedence over value.
axisstringrow defaultWhich <YAxis> (by id) to position against.
side'left' | 'right''right'Which edge the pill hugs (independent of the axis's side).
colorstringtheme.axis.labelPill hue — the colour of the series it tracks.
formatAxisFormataxis formatterd3 specifier or (value) => string. Omit to read exactly like a tick; set for finer precision (a live price wants ',.2f').
linebooleanfalseDraw a thin dashed guide line across the plot (the "price line").
pointerbooleanfalseAdd a small triangle on the plot-facing edge, pointing at the value.

There is no label prop — by the indicator law, an indicator always shows the axis value. A name belongs on a Baseline's near-line chip, not on the pill.

Live values — createLiveValue

YAxisIndicator's value re-renders with its parent — fine for an occasional change. For a high-frequency tick (a streaming price), use source with a LiveValue so only the pill repaints, not the chart:

const liveLast = createLiveValue(0);
// on each tick:
liveLast.set(nextPrice); // moves + relabels the pill, no chart re-render
interface LiveValue {
set(value: number): void; // no-op if unchanged
subscribe(onStoreChange: () => void): () => void;
getSnapshot(): number;
}

createLiveValue(initial) returns a handle backed by useSyncExternalStore on the subscribing indicator: .set(v) moves and relabels the pill without re-rendering the chart. This is the isolated-repaint path — the same motivation as the live-render model in Learn charts, chapter 8 and the @pond-ts/react hooks; this page doesn't restate throttling/snapshot cadence, only the indicator hook-up.

X-axis indicators

There is no standalone XAxisIndicator component. An on-axis pill on the x axis comes from <Marker indicator> — a marker at an at with its value pinned to the x-axis edge as a pill. The y-axis near-line counterpart is <Baseline indicator>. So the full picture:

Want a pill on…Use
the y axis edgeYAxisIndicator (pure pill)
the y axis, tied to a labelled line<Baseline indicator>
the x axis edge<Marker indicator>

All three obey the indicator law: the pill shows the formatted axis coordinate, and any custom label stays on the near-line chip.

Sharp edges

  • source beats value. Pass one; if you pass both, source wins.
  • No label on a pill — indicators show the axis value only, by design.
  • The live path repaints the pill, not the chart — that's the point of createLiveValue; don't route a high-frequency price through value (it re-renders the whole container each tick).

See also