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:
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>
| Prop | Type | Default | Purpose |
|---|---|---|---|
value | number | — | A static value to pin. Pass this or source. |
source | LiveValue | — | A LiveValue to subscribe to — the high-frequency path. Takes precedence over value. |
axis | string | row default | Which <YAxis> (by id) to position against. |
side | 'left' | 'right' | 'right' | Which edge the pill hugs (independent of the axis's side). |
color | string | theme.axis.label | Pill hue — the colour of the series it tracks. |
format | AxisFormat | axis formatter | d3 specifier or (value) => string. Omit to read exactly like a tick; set for finer precision (a live price wants ',.2f'). |
line | boolean | false | Draw a thin dashed guide line across the plot (the "price line"). |
pointer | boolean | false | Add 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 edge | YAxisIndicator (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
sourcebeatsvalue. Pass one; if you pass both,sourcewins.- 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 throughvalue(it re-renders the whole container each tick).
See also
- The annotation model — the indicator law and pills-vs-flags.
- Region · Baseline · Marker —
<Marker indicator>/<Baseline indicator>. - Learn charts, chapter 8 — the live-render model this builds on.
- Storybook:
Indicators/Y AxisandIndicators/X Axis.