/**
* Plot — a chart you assemble, for the chart that is not in the box.
*
* The other charts in this library each answer one question and answer it
* completely: a line chart knows it is drawing a series over time, and every
* decision inside it follows from that. This one knows nothing. It measures a
* box, resolves a scale, and hands both to whatever marks you put in it — so a
* combination nothing here ships as its own component is still a chart you can
* build rather than a chart you have to go without.
*
* ```tsx
*
*
*
*
*
*
*
*
*
* ```
*
* ## One scale, however many marks
*
* Every mark reads the same plot box and the same y-domain, and the domain is
* derived from all of them together. That is the whole reason to compose rather
* than to stack two charts on top of each other: two scales drawn over each
* other look like a comparison and are not one.
*
* ## Where your own marks go
*
* `Plot.Layer` drops its children into the SVG tree, and `usePlot()` gives them
* the resolved geometry — the box, the tweening domain, the reveal, the palette.
* The scale functions are worklets exported alongside this component, so a mark
* of your own is rebuilt on the UI thread on the same frames these are:
*
* ```tsx
* function Threshold({ value }: { value: number }) {
* const { plot, domainMin, domainMax } = usePlot();
* const props = useAnimatedProps(() => {
* const y = yOf(value, plot, domainMin.value, domainMax.value);
* return { d: `M${plot.left},${y}H${plot.left + plot.width}` };
* });
* return ;
* }
*
*
*
*
* ```
*
* Anything that is text or takes a touch goes in `Plot.Overlay` instead, which
* is a React Native view over the drawing. That split is not a preference: SVG
* text ignores the platform's text scaling and the theme's font, and a gesture
* handler cannot be attached to an SVG node at all.
*
* ## What it will not do for you
*
* There is no `type` prop and no set of defaults that guess at one. A chart
* assembled here is exactly the marks you wrote, in the order you wrote them —
* which is also the order they are drawn in, so a line over bars is a line
* written after them.
*/
import { type ReactNode } from 'react';
import { type ViewProps } from 'react-native';
import { type SharedValue } from 'react-native-reanimated';
import { type ChartAccessibilityProps } from '../../primitives/chart-accessibility.js';
import { type Plot as PlotBox } from '../../utils/chart.js';
export { areaPath, bandOf, barPath, compactNumber, linePath, segment, xAt, xOf, yOf, } from '../../utils/chart.js';
export type { Plot as PlotBox, ChartPoint } from '../../utils/chart.js';
/**
* Which layer a part belongs to. Read off the component itself, so composition
* stays a flat list of children instead of three nested slots whose order the
* caller has to remember.
*/
type Layer = 'svg' | 'series' | 'overlay' | 'header';
export type PlotStatus = 'loading' | 'ready';
export type PlotCurve = 'monotone' | 'linear';
export type PlotDatum = Record;
/**
* How the index of a row becomes an x.
*
* `point` puts the first and last rows on the plot's own edges, which is what a
* series wants — the line should reach the frame. `band` gives every row an
* equal slice and centres it in the middle of that, which is what anything with
* width wants: a bar sitting on the edge of the plot is a bar half of which is
* outside it.
*
* Resolved from the marks by default, so a chart with bars in it is banded
* without being told.
*/
export type PlotScale = 'point' | 'band';
/** One end of the y-domain: a number to pin it at, or `auto` to derive it. */
export type PlotBound = number | 'auto';
/** Everything a mark needs to draw itself. Read it with `usePlot()`. */
export interface PlotGeometry {
/** The rows, in order. */
data: PlotDatum[];
/** Key holding the x label. */
xDataKey: string;
/** The drawable box, after the padding and any axis gutter are taken off. */
plot: PlotBox;
/** How an index becomes an x. */
xScale: PlotScale;
/** `loading` draws nothing but the frame. */
status: PlotStatus;
/**
* The y-domain, tweened. Read these inside worklets — they are what makes a
* chart whose numbers changed redraw against a moving axis rather than jump.
*/
domainMin: SharedValue;
domainMax: SharedValue;
/**
* The domain the tween is heading for. Labels read this rather than the shared
* values: a number re-rendered on every frame of a tween is thirty renders of
* a label that lands on the string it started on.
*/
extent: [number, number];
/** `0` to `1` as the plot is uncovered on mount. */
reveal: SharedValue;
/** The five theme series colours, in order of prominence. */
palette: string[];
/** Every mark that registered itself, as `[dataKey, colour]`. */
series: [string, string][];
registerSeries: (key: string, color: string) => void;
unregisterSeries: (key: string, color?: string) => void;
/** Row under the cursor, or `-1`. On the UI thread. */
activeIndex: SharedValue;
/** The same index on the JS thread, for anything that has to re-render. */
activeIndexJS: number;
setActiveIndexJS: (index: number) => void;
}
/**
* The resolved geometry, for a mark of your own.
*
* Everything in it is either a plain number that changes on layout or a shared
* value that changes every frame, so a mark built from it animates with the
* rest of the chart rather than beside it.
*/
export declare function usePlot(): PlotGeometry;
/** The row under the cursor, for a readout drawn inside the plot. */
export declare function usePlotCursor(): {
activeIndex: number;
activePoint: PlotDatum | null;
};
export interface PlotProps extends ViewProps, ChartAccessibilityProps {
className?: string;
/** The rows. Each one is a position along the x-axis. */
data: PlotDatum[];
/** Key holding the x label. Used by the axis and the readout. */
xDataKey?: string;
/**
* `loading` draws the frame and nothing in it, and reveals the marks when it
* turns `ready`. One component throughout rather than a spinner swapped for a
* chart — swapping loses the transition.
*/
status?: PlotStatus;
/** Width ÷ height. `2` is the wide card shape; `1.6` suits a narrow column. */
aspectRatio?: number;
/** Milliseconds for the plot to be uncovered on mount. */
animationDuration?: number;
/** Milliseconds for the y-axis to settle after the data changes. */
domainDuration?: number;
/**
* The y-domain, as `[low, high]`. Either end may be a number to pin it there
* or `auto` to take it from the data.
*
* Pinning one end is the case this exists for: `[0, 'auto']` keeps the
* baseline at zero, which a chart of lengths needs — a bar cropped at the
* bottom is a length that lies — while still letting the top follow whatever
* arrives.
*/
yDomain?: [PlotBound, PlotBound];
/**
* Round the derived ends of the y-domain out to whole numbers.
*
* Left off, an axis ends a tenth of the span past the largest value, so it
* gets labelled 34,650 — true, and not a number anybody was looking for. On,
* the ends move out to a step of 1, 2 or 5 times a power of ten, and the
* labels become values a reader can measure against.
*
* It only ever widens the axis, and a pinned end is left alone.
*/
nice?: boolean;
/**
* How an index becomes an x. Derived from the marks when left out: a plot
* with bars in it is banded, and anything else is on points.
*/
xScale?: PlotScale;
/** How series are joined between points, unless a mark overrides it. */
curve?: PlotCurve;
/**
* The row under the cursor as it moves, and `-1`/`null` when the finger
* lifts. This is how a readout *outside* the plot gets its value — that
* header is not inside this provider, so it cannot use `usePlotCursor`.
*/
onActiveIndexChange?: (index: number, datum: PlotDatum | null) => void;
/**
* Drop the padding so the marks reach the edges — for a plot with no axis,
* grid or cursor, where the shape is the whole point.
*/
compact?: boolean;
children?: ReactNode;
}
/** Imperative handle: re-run the reveal, for a "replay" control. */
export interface PlotHandle {
replay: () => void;
}
export interface PlotGridProps {
/** Horizontal rules across the plot. */
rows?: number;
color?: string;
/** Dash pattern, e.g. `"4,6"`. Omit for a solid rule. */
dashArray?: string;
opacity?: number;
}
/**
* Horizontal reference lines.
*
* Outside the reveal clip on purpose: the grid is the frame the chart arrives
* into, so it is already there when the marks start being uncovered.
*/
declare function PlotGrid({ rows, color, dashArray, opacity }: PlotGridProps): import("react").JSX.Element;
declare namespace PlotGrid {
var displayName: string;
var layer: Layer;
}
export interface PlotSeriesProps {
/** Column of `data` this mark draws. */
dataKey: string;
/** Overrides the theme token. */
color?: string;
/** Which `--color-chart-*` token to take, `1` to `5`. */
colorIndex?: number;
}
export interface PlotLineProps extends PlotSeriesProps {
strokeWidth?: number;
/** `monotone` never overshoots between points; `linear` joins them straight. */
curve?: PlotCurve;
/** Dash pattern, e.g. `"6,4"` — for a forecast, or a series that is not real. */
dashArray?: string;
}
/** A series as a stroked line. */
declare function PlotLine({ dataKey, color, colorIndex, strokeWidth, curve, dashArray, }: PlotLineProps): import("react").JSX.Element | null;
declare namespace PlotLine {
var displayName: string;
var layer: Layer;
}
export interface PlotAreaProps extends PlotSeriesProps {
opacity?: number;
curve?: PlotCurve;
}
/**
* A series as a fill down to the baseline.
*
* Written before the line it belongs under, since the order the marks are
* written is the order they are drawn.
*/
declare function PlotArea({ dataKey, color, colorIndex, opacity, curve, }: PlotAreaProps): import("react").JSX.Element | null;
declare namespace PlotArea {
var displayName: string;
var layer: Layer;
}
export interface PlotBarsProps extends PlotSeriesProps {
/** Fraction of each slice left empty, `0` to `1`. */
gap?: number;
/** Rounds the end the bar grows towards, in points. */
radius?: number;
opacity?: number;
/**
* The value the columns grow from. Zero by default, and zero is nearly always
* right — a bar is a length, and a length has to start where the quantity
* does.
*
* Set it for the case where the reader is being shown movement rather than
* size: temperatures against a seasonal average, a score against a pass mark.
* Columns then run up and down from that line instead of all standing on the
* floor. It is clamped into the axis, so a baseline the domain does not cover
* falls back to the nearer edge.
*/
baseline?: number;
}
/**
* A series as columns.
*
* One path for all of them rather than one node per bar: a plot of two hundred
* periods is the same single animated prop a frame as a plot of twenty.
*
* A bar has width, so its presence puts the whole plot on a band scale unless
* the root was told otherwise — see `xScale`.
*/
declare function PlotBars({ dataKey, color, colorIndex, gap, radius, opacity, baseline, }: PlotBarsProps): import("react").JSX.Element | null;
declare namespace PlotBars {
var displayName: string;
var layer: Layer;
var mark: "band";
}
export interface PlotDotsProps extends PlotSeriesProps {
/** Radius, in points. */
size?: number;
/** Ring around each dot, so it reads on top of the line rather than in it. */
ringWidth?: number;
}
/** A dot per row — for a series short enough that its points are worth marking. */
declare function PlotDots({ dataKey, color, colorIndex, size, ringWidth, }: PlotDotsProps): import("react").JSX.Element;
declare namespace PlotDots {
var displayName: string;
var layer: Layer;
}
export interface PlotLayerProps {
children?: ReactNode;
}
/**
* Marks of your own, in the SVG tree.
*
* Whatever is inside is drawn where it is written — before the marks written
* after it, after the ones before it — and reaches the geometry through
* `usePlot()`. It is not given the geometry as an argument, because a mark that
* animates has to hold hooks of its own and a render prop is not a component.
*/
declare function PlotLayer({ children }: PlotLayerProps): import("react").JSX.Element;
declare namespace PlotLayer {
var displayName: string;
var layer: Layer;
}
export interface PlotOverlayProps {
className?: string;
children?: ReactNode;
}
/**
* Anything of your own that is text or takes a touch, laid over the drawing.
*
* SVG text ignores the platform's text scaling and the theme's font, and a
* gesture handler cannot be attached to an SVG node at all — so those two go
* here rather than in `Plot.Layer`.
*/
declare function PlotOverlay({ className, children }: PlotOverlayProps): import("react").JSX.Element;
declare namespace PlotOverlay {
var displayName: string;
var layer: Layer;
}
export interface PlotRuleProps {
/**
* Where to draw it, in the data's own units. Omit it and pass `x` instead for
* a rule down the plot rather than across it.
*/
y?: number;
/**
* A row to draw a vertical rule at, by index — the release the numbers are
* read against, the day a change landed.
*
* The x axis here carries positions rather than quantities, so this is which
* row rather than what value. Exactly one of `y` and `x` is drawn; `y` wins
* if both are given.
*/
x?: number;
/** A name for what the line means. Nothing is drawn without one. */
label?: string;
/** Overrides the line *and* its caption, so the two cannot drift apart. */
color?: string;
/** Thickness in points. */
strokeWidth?: number;
/**
* Break the line into dashes, for a rule that should read as an annotation
* rather than as a series the chart is drawing.
*/
dashed?: boolean;
/** Fades the line and its caption together. */
opacity?: number;
/** Which end of the rule the caption sits at. */
labelPlacement?: 'start' | 'end';
labelClassName?: string;
className?: string;
}
/**
* A reference line across the plot — a target, a limit, an average.
*
* A view rather than an SVG line, so its label is real text and follows the
* theme. Its thickness is a border and its position is a transform, so it costs
* no more than the SVG line would while gaining a legible caption.
*
* It is drawn at full strength in the foreground colour, and it is meant to be.
* A reference line is the number the series is being judged against — a target
* nobody can read is a target the chart is not actually stating — so what keeps
* it from being mistaken for a series is that it is neutral and optionally
* dashed, not that it is faint.
*/
declare function PlotRule({ y, x, label, color, strokeWidth, dashed, opacity, labelPlacement, labelClassName, className, }: PlotRuleProps): import("react").JSX.Element | null;
declare namespace PlotRule {
var displayName: string;
var layer: Layer;
}
export interface PlotXAxisProps {
/** How many labels to show. The rest are dropped, evenly. */
ticks?: number;
/** Turn a row into its label. Defaults to the value at `xDataKey`. */
format?: (datum: PlotDatum, index: number) => string;
className?: string;
}
/**
* The x labels. Real text rather than SVG text, so they follow the theme's font
* and the platform's text scaling — SVG text does neither.
*/
declare function PlotXAxis({ ticks, format, className }: PlotXAxisProps): import("react").JSX.Element;
declare namespace PlotXAxis {
var displayName: string;
var layer: Layer;
}
export interface PlotYAxisProps {
/** How many intervals to divide the axis into. Yields `ticks + 1` labels. */
ticks?: number;
/** Turn a value into its label. Defaults to a compact number. */
format?: (value: number) => string;
className?: string;
}
/**
* Value labels down the side, one per grid line.
*
* Give it the same `ticks` as the grid, or the numbers name lines that are not
* there. Four is the default on both for that reason.
*
* The labels are the domain the data settles at, not the tweening one — a
* number counting through every intermediate value while the axis animates is
* noise, and the axis is the part of a chart that has to hold still enough to
* be read.
*/
declare function PlotYAxis({ ticks, format, className }: PlotYAxisProps): import("react").JSX.Element;
declare namespace PlotYAxis {
var displayName: string;
var layer: Layer;
var axis: "y";
}
export interface PlotCursorProps {
color?: string;
/** Hide the vertical line and keep only the touch handling. */
showLine?: boolean;
}
/**
* The touch handling, and the line that follows it.
*
* Split from the readout next door because they are separately useful: a plot
* whose value is shown in its own header wants this and no label, and a plot
* that highlights a bar wants this and nothing else at all. Both read the same
* index.
*
* The hit area is the whole plot. A cursor you have to land on the line to
* summon is a cursor nobody finds.
*/
declare function PlotCursor({ color, showLine }: PlotCursorProps): import("react").JSX.Element | null;
declare namespace PlotCursor {
var displayName: string;
var layer: Layer;
}
export interface PlotTooltipProps {
/** Format one series' value. Defaults to a compact number. */
formatValue?: (value: number, key: string) => string;
/** Format the heading from the row. Defaults to the value at `xDataKey`. */
formatX?: (datum: PlotDatum) => string;
/** Draw the readout yourself, given the row under the cursor. */
children?: (datum: PlotDatum, index: number) => ReactNode;
className?: string;
}
/**
* The readout that rides the cursor.
*
* Needs a `Plot.Cursor` beside it — the cursor owns the gesture and this only
* reads the index it resolves. On its own it never appears, which is the right
* failure: a label with no way to move is worse than no label.
*/
declare function PlotTooltip({ formatValue, formatX, children, className }: PlotTooltipProps): import("react").JSX.Element | null;
declare namespace PlotTooltip {
var displayName: string;
var layer: Layer;
}
export interface PlotHeaderProps extends ViewProps {
className?: string;
/** A word for what the plot is of. */
title?: string;
/** The figure, large. Usually the total, or the row under the cursor. */
value?: string;
/** A line under the value. */
caption?: string;
/** Replaces the whole header, keeping only its place above the drawing. */
children?: ReactNode;
}
/** The row above the drawing: what it is, and the one number worth reading. */
declare function PlotHeader({ className, title, value, caption, children, ...props }: PlotHeaderProps): import("react").JSX.Element;
declare namespace PlotHeader {
var displayName: string;
var layer: Layer;
}
export interface PlotLegendProps extends ViewProps {
className?: string;
/** Names for the columns, keyed by `dataKey`. Falls back to the key itself. */
labels?: Record;
}
/**
* A swatch and a name per mark, taken from the marks that registered.
*
* In the header rather than over the drawing, because a key laid inside the
* plot either covers a mark or is squeezed to one word a line.
*/
declare function PlotLegend({ className, labels, ...props }: PlotLegendProps): import("react").JSX.Element | null;
declare namespace PlotLegend {
var displayName: string;
var layer: Layer;
}
export declare const Plot: import("react").ForwardRefExoticComponent> & {
Header: typeof PlotHeader;
Legend: typeof PlotLegend;
Grid: typeof PlotGrid;
Area: typeof PlotArea;
Bars: typeof PlotBars;
Line: typeof PlotLine;
Dots: typeof PlotDots;
Rule: typeof PlotRule;
Layer: typeof PlotLayer;
Overlay: typeof PlotOverlay;
XAxis: typeof PlotXAxis;
YAxis: typeof PlotYAxis;
Cursor: typeof PlotCursor;
Tooltip: typeof PlotTooltip;
};
//# sourceMappingURL=index.d.ts.map