Getting started
One chart API. Native everywhere.
High-performance, natively rendered graphs for web, iOS, and Android, designed for developers and coding agents.
Getting started
High-performance, natively rendered graphs for web, iOS, and Android, designed for developers and coding agents.
One package covers every platform. There is nothing else to add for iOS or Android — the native modules ship inside it.
npm install @hzblj/zyplotA plain @hzblj/zyplot import resolves to the renderer the current target needs, and gives you the chart forms that exist everywhere. The three subpaths name a platform outright, and are how you reach forms only one renderer has.
| Prop | Type | Default | Description |
|---|---|---|---|
@hzblj/zyplot | any target | — | Resolves per target: ECharts and uPlot on the web, the native module under React Native. Exposes the twenty-one shared forms. |
@hzblj/zyplot/web | web | — | The DOM renderer, and the only entry with Chart.Frame, Chart.Legend and a .Skeleton on every form. |
@hzblj/zyplot/ios | iOS | — | Shared forms plus Chart.Range and Chart.Rule, widened with the Swift Charts axis options. |
@hzblj/zyplot/android | Android | — | Shared forms plus Chart.Lollipop and Chart.Waterfall, widened with the Compose axis options. |
Web
@hzblj/zyplot/web draws in the DOM. A plain @hzblj/zyplot import already resolves here on every target except React Native, so naming the subpath only matters in a project that has both. It is also the only entry point that carries Chart.Frame, Chart.Legend and a .Skeleton on every form — all three are DOM composition with no native counterpart. Chart.Provider exists on every entry point, but only here does it take colorMode and a className.
Component names are the form, not the engine. Chart.Line and Chart.TimeSeries both draw a line — you pick between them on point count, never on renderer.
| Prop | Type | Default | Description |
|---|---|---|---|
ECharts | canvas | — | Eighteen of the twenty-one forms, Line through Treemap. Series types are registered per chart file, so a page with one line chart does not ship the sankey, sunburst and boxplot code. |
uPlot | canvas | — | Chart.TimeSeries and Chart.Sparkline. Tens of thousands of points, or forty sparklines in a table, where one scene graph per row would drop frames. |
Plain DOM | no engine | — | Chart.Meter is a filled track — two elements and no engine, so it is also the only form that needs no client boundary. |
Only marks and axis ticks are painted into the canvas. The legend is React: every chart with two or more series renders one on its own, a single series never does, and the labels stay selectable, translatable and reachable by a screen reader.
Chart props stay serializable — no formatter callbacks, no render props — so a server component can render a chart without a client boundary of its own. Anything that would have been a callback is expressed as data instead, which is what ChartNumberFormat is for.
--zyplot-* values from the DOM after mounting and repaints when they change. Until that first read there is nothing honest to paint, so a chart shows its skeleton for a frame even with isLoading false.Native
Zyplot ships an Expo module that draws with the platform's own graphics stack — Swift Charts on iOS, Jetpack Compose Canvas on Android — behind the same Chart namespace the web renderer exposes. There is no WebView.
The same single package you installed for the web carries the native code; autolinking picks it up. Rebuild the native project after adding it. These are native modules, so Expo Go cannot load them: use a development build.
yarn add @hzblj/zyplot
npx expo prebuild
npx expo run:ios
npx expo run:androidexpo-build-properties if your app targets something lower.Import from @hzblj/zyplot and the correct renderer is picked per platform — the same source builds for web, iOS and Android.
import { Chart } from '@hzblj/zyplot'
export function Revenue() {
return (
<Chart.Line
categories={['Jan', 'Feb', 'Mar']}
format={{ prefix: '$' }}
series={[{ id: 'revenue', label: 'Revenue', values: [42, 56, 51] }]}
/>
)
}A few forms exist on one platform only, because the underlying renderer has a mark the other has no honest equivalent for. They are not on the shared namespace — reaching for one would type-check and then render nothing on the platform that lacks it.
Instead, give the component one file per platform and let Metro choose. Write the shared parts once, import the platform entry point in the file that is already committed to that platform, and drop the Platform.OS branches entirely.
forecast.ios.tsx imports @hzblj/zyplot/ios
forecast.android.tsx imports @hzblj/zyplot/android
forecast.tsx optional web or fallback version// forecast.ios.tsx
import { Chart } from '@hzblj/zyplot/ios'
export const Forecast = ({ bands }: ForecastProps) => (
<Chart.Range data={bands} height={300} />
)// forecast.android.tsx
import { Chart } from '@hzblj/zyplot/android'
export const Forecast = ({ bands }: ForecastProps) => (
<Chart.Lollipop data={bands.map(toPoint)} height={300} />
)The call site imports ./forecast with no extension and never learns which one it got. This is the same convention Expo's own UI packages use.
tsc does not know about platform extensions, so ./forecast needs a plain forecast.tsx to resolve to. It doubles as the web version, or returns null if the component is native-only.All twenty-one shared chart kinds render on both native platforms, and each platform adds two of its own. The props are the same ones documented under Charts.
| Prop | Type | Default | Description |
|---|---|---|---|
Cartesian | web · iOS · Android | — | Line, Area, Bar, StackedBar, TimeSeries, Sparkline, Scatter, Histogram. |
Radial and flow | web · iOS · Android | — | Pie, Gauge, Meter, Radar, Sunburst, Treemap, Funnel, Sankey. |
Statistical and finance | web · iOS · Android | — | Candlestick, Boxplot, DivergingBar, Dumbbell, Heatmap. |
iOS extensions | iOS only | — | Chart.Range and Chart.Rule, built on Swift Charts marks with no web equivalent. |
Android extensions | Android only | — | Chart.Waterfall and Chart.Lollipop, with no web equivalent. |
| Prop | Type | Default | Description |
|---|---|---|---|
className | web only | — | Native charts have no DOM node to style. Use height and plot instead. |
texture | web only | — | Pattern fills answer forced-colors and print, which a native chart never meets. Not part of the native props — the compiler rejects it rather than the renderer ignoring it. |
skeleton | web only | — | Native has no custom skeleton slot; isLoading draws the platform spinner. |
colorMode | shared | "system" | Per chart on native: there is no cascade to inherit through, so Chart.Provider scopes surface and theme but not the color mode. |
interaction | shared | — | Delivered through onInteraction on both platforms, with haptics available natively. |
labels | shared | — | Boxplot terminology is honoured natively too — the tooltip shows the five-number summary in your wording. |
Native
@hzblj/zyplot/ios renders with SwiftUI and Swift Charts. Chart configuration crosses the bridge as JSON and is decoded into native models, so every prop stays serializable. Importing it commits a file to iOS, so name that file *.ios.tsx.
// price.ios.tsx
import { Chart } from '@hzblj/zyplot/ios'
export function Price() {
return (
<Chart.Candlestick
data={candles}
format={{ prefix: '$' }}
onInteraction={(event) => console.log(event.category, event.value)}
showVolume
/>
)
}Two marks Swift Charts provides that neither the web nor the Compose renderer has a counterpart for. They are absent from the shared @hzblj/zyplot namespace, so TypeScript rejects them unless the file imports @hzblj/zyplot/ios.
| Prop | Type | Default | Description |
|---|---|---|---|
Chart.Range* | ChartRangePropsIos | — | Shaded band between a low and a high per category — forecast ranges, confidence intervals. |
Chart.Rule* | ChartRulePropsIos | — | Reference rules at a value, optionally spanning a start and end. |
All of it is readable — the module is a few hundred lines of SwiftUI over Swift Charts, and every documented form links to the exact file that draws it from its own page.
| Prop | Type | Default | Description |
|---|---|---|---|
ios/Bridge/ZyplotModule.swift | bridge | — | The Expo module: the ExpoView, its single prop, and the SwiftUI hosting controller. |
ios/Core/ZyplotModels.swift | model | — | Codable models the JSON configuration decodes into. Adding a prop starts here. |
ios/Charts/Marks/ZyplotMarksChart.swift | marks | — | Every Swift Charts mark — line, bar, pie, scatter, boxplot, rule, range. |
ios/Charts/Specialized/ZyplotSpecializedCharts.swift | canvas | — | The forms Swift Charts has no mark for, drawn on a SwiftUI Canvas. |
xAxis and yAxis accept everything ChartAxisOptions defines, plus these Swift Charts scrolling controls.
| Prop | Type | Default | Description |
|---|---|---|---|
xAxis.visibleDomain | number | — | Length of the visible x domain. Setting it makes the plot horizontally scrollable. |
xAxis.scrollPosition | number | string | — | Initial scroll offset along the x axis. |
yAxis.plotDimensionStartPadding | number | — | Extra padding before the first mark. |
yAxis.plotDimensionEndPadding | number | — | Extra padding after the last mark. |
Native
@hzblj/zyplot/android draws on a Jetpack Compose Canvas. Marks, axis text, grid, annotations and the tooltip are all drawn by the module, so a chart is one view rather than a tree of them. Importing it commits a file to Android, so name that file *.android.tsx.
// spend.android.tsx
import { Chart } from '@hzblj/zyplot/android'
export function Spend() {
return (
<Chart.Waterfall
data={movements}
format={{ prefix: '$', decimals: 0 }}
interaction={{ haptics: true, tooltip: true }}
/>
)
}Two forms the Compose Canvas draws that Swift Charts has no equivalent for. They are absent from the shared @hzblj/zyplot namespace, so TypeScript rejects them unless the file imports @hzblj/zyplot/android.
| Prop | Type | Default | Description |
|---|---|---|---|
Chart.Waterfall* | ChartWaterfallPropsAndroid | — | Running total across signed movements, colored by direction. |
Chart.Lollipop* | ChartLollipopPropsAndroid | — | A stem and a dot per category — a bar chart with the ink of a dot plot. |
One Canvas draws the whole chart — marks, axis text, grid, annotations and the tooltip — so a chart is a single view rather than a tree of them. Every documented form links to the draw function behind it from its own page.
| Prop | Type | Default | Description |
|---|---|---|---|
android/.../bridge/ZyplotModule.kt | bridge | — | The Expo module: the ExpoComposeView, its single prop, and the composable root. |
android/.../core/ChartConfiguration.kt | model | — | Parses the JSON configuration and resolves the palette, axes and theme. |
android/.../charts/ZyplotChart.kt | host | — | Animation, gestures, the surface container and dispatch to the draw functions. |
android/.../charts/{cartesian,radial,specialized} | canvas | — | The draw functions themselves, split into cartesian, radial and specialized. |
xAxis and yAxis accept everything ChartAxisOptions defines, plus overflow handling for long tick labels.
| Prop | Type | Default | Description |
|---|---|---|---|
xAxis.labelOverflow | "clip" | "ellipsis" | "visible" | "ellipsis" | How a tick label that exceeds its band is cut. |
yAxis.labelOverflow | "clip" | "ellipsis" | "visible" | "ellipsis" | How a tick label that exceeds the gutter is cut. |
Chart.Provider scopes a palette to a subtree. It writes what you pass as --zyplot-* custom properties on its own wrapper, and the charts inside read the resolved values back off the DOM. A key you leave out keeps the stylesheet's value, including that value's dark variant.
<Chart.Provider
theme={{
colors: {
categorical: ['#7c3aed', '#0284c7', '#ea580c'],
grid: '#e5e7eb',
},
typography: {
fontFamily: 'Geist, sans-serif',
},
}}
>
<Dashboard />
</Chart.Provider>Every key is optional and takes any color the browser can resolve. There is no background here — the box a chart sits in is surface, below.
ChartTheme of their own — no sequential, diverging, muted or border, and a background instead — and they accept it per chart as well as from the provider, because a native chart has no cascade to read a variable out of.type ChartTheme = {
colors?: {
/** Slots 1…7, in order. A series takes one by index or by its own slot. */
categorical?: string[]
/** Low → high, five steps. Heatmap, treemap, sunburst. */
sequential?: string[]
/** Signed scales: diverging bars, and any positive/negative encoding. */
diverging?: {
negative?: string
negativeSoft?: string
neutral?: string
positive?: string
positiveSoft?: string
}
/** The de-emphasis grey — every series that is context rather than subject. */
muted?: string
axis?: string
grid?: string
/** Axis and data labels. */
label?: string
/** Tooltip fill. */
surface?: string
/** Tooltip hairline. */
border?: string
/** The unfilled part of a gauge or a meter. */
track?: string
}
typography?: {
/** A resolved family name. A canvas cannot read var(--font-sans). */
fontFamily?: string
}
}categorical entries and the first five sequential steps are ever read. An eighth series color is one no color-blind reader can separate from a slot that already exists, so the eighth series is an "other" bucket, a small multiple, or a second encoding through texture — not a longer palette.theme answers "what colour is this series"; surface answers "what does the container look like". Keeping them apart is what lets a design system set one card treatment for every chart while each chart keeps its own palette.
<Chart.Provider surface={{ background: '#fff', cornerRadius: 16, padding: 12 }}>
<Chart.Line categories={categories} series={series} />
<Chart.Bar surface={{ cornerRadius: 24 }} categories={categories} series={series} />
</Chart.Provider>A chart's own surface merges over the provider's key by key, so the bar above rounds its corners without restating the background it inherits.
| Prop | Type | Default | Description |
|---|---|---|---|
background | string | — | Fill behind the plot. Any CSS or hex colour. |
border | { color?: string; width?: number } | — | Outline around the container. |
cornerRadius | number | 0 | Corner rounding, in px on web and points on native. |
padding | number | ChartSurfacePadding | — | A number applies to all four sides; the object form takes horizontal, vertical and the individual sides, most specific winning. |
div, a SwiftUI view and a Compose Canvas live here. Anything that would have to be approximated on one of the three is deliberately absent.| Prop | Type | Default | Description |
|---|---|---|---|
children* | ReactNode | — | Charts rendered inside the scope. |
theme | ChartTheme | — | Scoped color and typography overrides. |
surface | ChartSurface | — | Container treatment every chart in the subtree inherits, merged key by key with the chart's own. |
colorMode | "inherit" | "light" | "dark" | "system" | "inherit" | How the light/dark palette is resolved for the subtree. |
className | string | — | CSS class on the wrapper element the provider renders. |
div in your layout rather than context alone. It carries className for that reason.Both palettes ship in the stylesheet. The dark one is keyed off .dark or data-theme="dark" on the document root — the two conventions Tailwind and next-themes already write — and a root that pins neither falls back to prefers-color-scheme. A project doing either needs no chart-specific wiring.
Chart.Provider pins a subtree instead. Its colorMode lands as data-zyplot-color-mode on the wrapper, and the stylesheet resolves the palette from there.
| Prop | Type | Default | Description |
|---|---|---|---|
"inherit" | document root | default | Takes whatever the document root resolved to, including the OS fallback. Charts outside a provider behave this way too. |
"light" | pinned | — | The light palette regardless of the root, plus color-scheme: light. |
"dark" | pinned | — | The dark palette regardless of the root, plus color-scheme: dark. |
"system" | media query | — | Follows the OS through prefers-color-scheme, ignoring the document root. |
class, data-theme, data-zyplot-color-mode and style on the root — plus the prefers-color-scheme query — and repaints from the new values. No remount, and no chart left painting light-mode series on a dark canvas.These names are the public API; the Tailwind tokens behind them are not. Override them wherever you set the rest of your theme — the values below are the light defaults, and every color among them has a dark counterpart in the stylesheet.
:root {
/* Categorical: slots 1…7, in the order series take them. */
--zyplot-color-categorical-1: #4400fc;
--zyplot-color-categorical-2: #0092de;
--zyplot-color-categorical-3: #ff5700;
--zyplot-color-categorical-4: #9c74ff;
--zyplot-color-categorical-5: #00a546;
--zyplot-color-categorical-6: #006fac;
--zyplot-color-categorical-7: #ff133c;
/* Sequential: low → high. Heatmap, treemap, sunburst. */
--zyplot-color-sequential-1: #b89bff;
--zyplot-color-sequential-2: #9c74ff;
--zyplot-color-sequential-3: #7135ff;
--zyplot-color-sequential-4: #4400fc;
--zyplot-color-sequential-5: #2f00ae;
/* Diverging: signed scales. */
--zyplot-color-diverging-negative: #d23100;
--zyplot-color-diverging-negative-soft: #ff7d4f;
--zyplot-color-diverging-neutral: #d9d9d9;
--zyplot-color-diverging-positive-soft: #59c4fd;
--zyplot-color-diverging-positive: #006fac;
/* Chrome. */
--zyplot-color-axis: #a6a6a6;
--zyplot-color-grid: #f5f5f5;
--zyplot-color-label: #666666;
--zyplot-color-muted: #808080;
--zyplot-color-surface: #fcfcfc;
--zyplot-color-border: #f5f5f5;
--zyplot-color-track: #ebebeb;
--zyplot-font-family: inherit;
}
/* Only the keys you actually change need restating per mode. */
[data-theme='dark'] {
--zyplot-color-categorical-1: #7135ff;
--zyplot-color-grid: #212121;
}The font is inherited from the page. Set --zyplot-font-family only when charts should use a different stack, and give it a resolved family name — a canvas cannot read another variable.
color(display-p3 …), which ECharts' own parser rejects. Every color is normalized to sRGB on the way to the canvas, so the variable can hold whatever your design tokens hold.Chart data is plain serializable objects — no formatter callbacks and no render props, which is what lets a server component render a chart. Labels are already translated: this package never resolves an i18n key. Type names in the props tables link here.
Used by line, area, bar, stacked bar, radar and every other multi-series form. Each values entry aligns with the category at the same index, so the array is as long as categories.
type ChartSeries = {
/** Stable identity: the React key, and how hover correlates across charts. */
id: string
/** Already-translated display name, used by the legend and the tooltip. */
label: string
/** One value per category. null is a genuine gap, drawn as one, never as zero. */
values: (number | null)[]
/** Palette slot, 1-based. Pin it when the caller can hide series. */
slot?: number
/** Overrides the active palette for this series. */
color?: string
}An explicit color wins over the provider palette and the CSS variables. Omit slot and a series takes its index — correct for a fixed list, wrong the moment the list can be filtered, because the survivors get repainted and the reader has to re-learn the chart.
const series: ChartSeries[] = [
{
id: 'revenue',
label: 'Revenue',
values: [42, 56, null, 72],
slot: 1,
color: '#16a34a',
},
]A labelled scalar — the shape part-to-whole and ranked forms consume: pie, funnel, gauge segments, diverging bars.
type ChartDatum = {
id: string
label: string
value: number
slot?: number
color?: string
}axis is the on/off switch both cartesian axes share; format is one description of a number, applied to axis ticks, tooltips and direct labels alike, so they can never disagree.
type ChartAxes = {
x?: boolean
y?: boolean
}
type ChartNumberFormat = {
/** Fraction digits. Defaults to 0. */
decimals?: number
/** BCP 47 tag for grouping and decimal separators. Defaults to the runtime locale. */
locale?: string
/** Rendered before the number — a currency symbol, typically. */
prefix?: string
/** Rendered after the number — a unit or a percent sign. */
suffix?: string
}What Chart.Legend takes when you place identity yourself. The color is already resolved — a swatch is a color, not a slot to look up.
type ChartLegendItem = {
id: string
label: string
color: string
}Some forms take a shape-specific contract instead of ChartSeries, because their encoding is not "one value per category". The field names describe the marks directly.
/** Chart.Radar — one axis per row. Axes are scaled independently. */
type ChartRadarAxis = {
label: string
max: number
}
/** Chart.Heatmap — addressed by axis index; null renders empty, not as the ramp's low end. */
type ChartHeatmapCell = {
columnIndex: number
rowIndex: number
value: number | null
}
/** Chart.Dumbbell — a before → after pair per row. */
type ChartDumbbellRow = {
id: string
label: string
before: number
after: number
}
/** Chart.Boxplot — the five-number summary, plus outliers you have already picked. */
type ChartBoxplotGroup = {
id: string
label: string
min: number
q1: number
median: number
q3: number
max: number
outliers?: number[]
}
/** Required, because "Q1" is not a word every reader of your product knows. */
type BoxplotLabels = {
min: string
q1: string
median: string
q3: string
max: string
}
/** Chart.Sankey — nodes, and the weighted edges that address them by id. */
type ChartFlowNode = {
id: string
label: string
slot?: number
color?: string
}
type ChartFlowLink = {
source: string
target: string
value: number
}
/** Chart.Treemap and Chart.Sunburst — leaves carry a value, parents sum their children. */
type ChartHierarchyNode = {
id: string
label: string
value?: number
children?: ChartHierarchyNode[]
slot?: number
color?: string
}
/** Chart.Scatter — an unordered two-measure space. size turns points into bubbles. */
type ChartScatterSeries = {
id: string
label: string
points: Array<{
x: number
y: number
size?: number
label?: string
}>
slot?: number
color?: string
}
/** Chart.TimeSeries — parallel arrays, because that is what uPlot consumes. */
type ChartTimePoints = {
/** Unix seconds, strictly ascending. */
timestamps: number[]
/** One entry per series, each as long as timestamps. */
values: (number | null)[][]
}One entry per session. volume is what showVolume draws, and timestamp is only needed when something outside the chart has to line up with the session.
type ChartCandlestickDatum = {
id: string
category: string
open: number
high: number
low: number
close: number
volume?: number
/** Unix seconds. */
timestamp?: number
}
type ChartCandlestickStyle = {
upColor?: string
downColor?: string
neutralColor?: string
/** Draws rising candles as outlines — the convention on most trading desks. */
hollowUp?: boolean
candleWidth?: number
wickWidth?: number
volumeUpColor?: string
volumeDownColor?: string
/** Share of the plot height the volume histogram takes. */
volumeHeightRatio?: number
}axis switches an axis off; xAxis and yAxis describe one. Both are read by line, area, bar, stacked bar and candlestick — the forms whose readers pin a domain, change the scale or annotate a value. The other forms take the visibility switch only, which is why their props tables list axis alone.
type ChartAxisOptions = {
visible?: boolean
label?: string
/** "auto" | "category" | "linear" | "log" | "time" */
scale?: ChartAxisScale
/** Pins the extent. Omit either end to keep it computed. */
domain?: { min?: number; max?: number }
format?: ChartNumberFormat
grid?: boolean
gridDash?: number[]
labelRotation?: number
/** Which side the axis is drawn on: "start" | "end". */
position?: ChartAxisPosition
reversed?: boolean
/** A hint, not a guarantee — the engine still picks readable ticks. */
tickCount?: number
/** Exact ticks, when the reader is looking for specific ones. */
tickValues?: (number | string)[]
}A union discriminated on type. Coordinates are number | string: a category name on a category axis, a value on a linear or time one.
type ChartAnnotation =
| ChartLineAnnotation
| ChartRangeAnnotation
| ChartPointAnnotation
| ChartTextAnnotation
/** A target, a threshold, a launch date. */
type ChartLineAnnotation = {
type: 'line'
id: string
axis: 'x' | 'y'
value: number | string
label?: string
color?: string
dash?: number[]
}
/** A shaded span — a quarter, an incident window, a tolerance band. */
type ChartRangeAnnotation = {
type: 'range'
id: string
axis: 'x' | 'y'
start: number | string
end: number | string
label?: string
color?: string
opacity?: number
}
type ChartPointAnnotation = {
type: 'point'
id: string
x: number | string
y: number
label?: string
color?: string
symbol?: ChartSymbol
}
type ChartTextAnnotation = {
type: 'text'
id: string
text: string
x?: number | string
y?: number
color?: string
}interaction is what the chart does on its own; onInteraction is how your code hears about it. The event is one flat serializable shape for every form, so a handler written for a bar chart works on a line chart.
type ChartInteraction = {
/** "axis" | "nearest" | "series" | "none" */
hover?: ChartHoverMode
/** "both" | "x" | "y" | "none" */
crosshair?: ChartCrosshairMode
tooltip?: boolean
/** "single" | "multiple" | "none" */
selection?: ChartSelectionMode
pan?: boolean
zoom?: boolean
/** How far a hovered mark grows. */
highlightScale?: number
/** How far the rest fades while one mark is hovered. */
dimOpacity?: number
/** Native only — the web has no honest equivalent. */
haptics?: boolean
}
type ChartInteractionEvent = {
seriesId?: string
category?: string
value?: number
x?: number
y?: number
/** Unix seconds, on the time-based forms. */
timestamp?: number
/** Pointer position in the native view's coordinate space. */
nativeX?: number
nativeY?: number
}onInteraction is the one prop that cannot cross, and the file that passes it needs "use client".surface is the box the chart sits in; plot is the drawing area inside it. seriesStyles is keyed by ChartSeries.id, so a style survives reordering and filtering the way slot does for color.
type ChartPlotStyle = {
backgroundColor?: string
borderColor?: string
borderWidth?: number
borderRadius?: number
/** Clips marks to the plot area — the honest choice when a domain is pinned. */
clip?: boolean
padding?: number | { top?: number; right?: number; bottom?: number; left?: number }
}
type ChartSeriesStyle = {
color?: string
strokeWidth?: number
strokeDash?: number[]
fillOpacity?: number
opacity?: number
/** "circle" | "diamond" | "square" | "triangle" | "none" */
symbol?: ChartSymbol
symbolSize?: number
}
type ChartAnimation = {
enabled?: boolean
/** The entrance. Turn it off for a chart that re-renders on every keystroke. */
initial?: boolean
/** The transition when data changes under a mounted chart. */
updates?: boolean
duration?: number
delay?: number
/** "linear" | "ease-in" | "ease-out" | "ease-in-out" | "spring" */
easing?: ChartAnimationEasing
}Hold isLoading true while the data is in flight. The chart shows the shape it is about to be, at the height it will occupy, and cross-fades into the plot when the flag drops — same grid cell, same size, so nothing on the page moves when the marks land.
<Chart.Line
categories={categories}
height={320}
isLoading={revenue.isPending}
series={series}
/>The placeholder is derived from the props the chart already has: one legend row per series, and axis rows only where an axis is visible. There is nothing to configure and nothing to keep in sync when the chart changes.
isLoading false and data in hand. The wrapper carries aria-busy while either is true, and the placeholder itself is aria-hidden.isLoading means the same thing on iOS and Android, but there it draws the platform's own spinner: no .Skeleton component and no skeleton slot, because there is no DOM to build one out of.Every form also exposes its placeholder on its own, as Chart.Line.Skeleton — for when the chart is not mounted yet at all: a Suspense fallback, a route placeholder, a dashboard slot whose query has not started. These props apply only there; a chart driven by isLoading fills them in itself.
<Suspense fallback={<Chart.Line.Skeleton height={320} legendCount={2} />}>
<Revenue />
</Suspense>| Prop | Type | Default | Description |
|---|---|---|---|
height | number | 240 | Reserved height. Match the chart it stands in for. |
legendCount | number | 0 | Legend rows to reserve. Drawn from two up — a single series gets no legend, so reserving a row for one would leave a gap the chart never fills. |
xAxis | boolean | true | Reserves the horizontal-axis label row. |
yAxis | boolean | true | Reserves the vertical-axis label column. |
className | string | — | CSS class applied to the skeleton root. |
skeleton takes a rendered element, not a component, and replaces the built-in one while isLoading is true. Keep its height equal to the chart's so the swap still costs no layout shift.
function RevenueSkeleton({ height = 320 }) {
return (
<div
aria-label="Loading revenue chart"
aria-busy="true"
role="status"
style={{ height }}
>
<div className="skeleton-title" />
<div className="skeleton-plot" />
</div>
)
}
<Chart.Line
isLoading
skeleton={<RevenueSkeleton height={320} />}
height={320}
axis={{ x: false, y: true }}
categories={categories}
series={series}
/>isLoading only. The frame before the first paint still uses the built-in placeholder, because that one is derived from the chart and always matches it. Legend rows and axis gutters are yours to mirror here — axis shapes the built-in placeholder, not this one.Chart.Frame is the card a chart can sit in: a title, a description, one row for filters, and a caption underneath for the source or the caveat. It is optional — a chart dropped straight into a dashboard grid needs no card — and when it is used it is the standard card recipe, so a chart never invents its own container.
<Chart.Frame
title="Revenue"
description="Monthly recurring revenue"
caption="Source: billing ledger"
>
<Chart.Line categories={categories} series={series} />
</Chart.Frame>The header only exists when at least one of title, description and actions is set, so a frame with none of them is a plain card around the plot.
surface. The frame is a card with type in it, rendered around the chart; surface is the box the plot itself is painted on, and it exists on native too. Use the frame for a titled dashboard card, surface when the chart needs its own background or padding.| Prop | Type | Default | Description |
|---|---|---|---|
children* | ReactNode | — | Chart or composed visualization content. |
title | string | — | Heading rendered above the chart. |
description | string | — | Supporting text below the title. |
actions | ReactNode | — | Filters and controls aligned with the heading. |
caption | string | — | Source, method or caveat below the chart. |
className | string | — | CSS class applied to the frame. |
A chart renders its own legend from two series up, and none for a single one, so Chart.Legend is for the surface that places identity itself — one legend above a row of small multiples, or a legend that doubles as a series filter. Colors come in already resolved, so pin slot or color on the series and both agree by construction.
| Prop | Type | Default | Description |
|---|---|---|---|
items* | ChartLegendItem[] | — | Stable IDs, labels and resolved swatch colors. |
className | string | — | CSS class applied to the legend. |
Chart
Show open, high, low and close for each session, with optional volume.
Use for OHLC price data. For a single measure over time reach for Line or Time series instead — a candlestick spends four values of ink on one.
The chart's own props first, then the shared ones this form reads.
| Prop | Type | Default | Description |
|---|---|---|---|
data* | ChartCandlestickDatum[] | — | One entry per session, in chronological order. |
format | ChartNumberFormat | — | Number formatting shared by axes, labels and tooltips. |
showVolume | boolean | false | Adds a volume histogram beneath the price plot. Needs `volume` on each datum. |
style | ChartCandlestickStyle | — | Candle body, wick and volume colors, plus hollow-up rendering. |
axis | ChartAxes | { x: true, y: true } | Horizontal and vertical axis visibility. |
animation | ChartAnimation | — | Mark entrance and data-update animation. |
annotations | ChartAnnotation[] | — | Reference lines, highlighted ranges, points and text anchored to the plot. |
interaction | ChartInteraction | — | Hover, crosshair, tooltip, selection, pan and zoom behaviour. |
onInteraction | (event: ChartInteractionEvent) => void | — | Receives normalized pointer and selection data. Needs a client component. |
plot | ChartPlotStyle | — | The plot area alone — its own background, border, clipping and padding, inside the surface. |
xAxis | ChartAxisOptions | — | Scale, domain, ticks, grid and label for the horizontal axis — everything axis cannot say. |
yAxis | ChartAxisOptions | — | The same for the vertical axis. |
texture | boolean | false | Draws decal patterns over fills — a second encoding on top of hue, for full colour-vision deficiency, print and forced-colors. |
className | string | — | CSS class applied to the chart root. |
height | number | 240 | Plot height in px. A chart never measures its own content, so this is what reserves the space. |
isLoading | boolean | false | Held true while the data is in flight. Shows the matching skeleton, then cross-fades into the plot. |
skeleton | ReactNode | — | Replaces the built-in placeholder while isLoading is true. Takes a rendered element. |
surface | ChartSurface | — | The container the plot sits in — background, border, corner radius, padding. Merges over Chart.Provider, key by key. |