Builders

Every prop on this page is a plain object, and writing one by hand stays supported. The builders exist for the handful of shapes where hand-writing is genuinely error-prone: a union you have to remember the discriminant for, a record whose key repeats an id declared elsewhere, a type that permits two fields only one variant reads.

They return the same plain objects the props always were — no wrapper, no class, nothing to unwrap. So they compose, spread and serialize, and you can build your own presets on top of them.

tsx
import {
  // Builders with variants, one function per variant.
  annotation,
  axis,
  marker,
  reveal,
  // A series and its styling, declared together.
  series,
  seriesProps,
  // Typed passthroughs for the groups with nothing to get wrong.
  animation,
  format,
  glow,
  halo,
  interaction,
  plot,
  seriesStyle,
  surface,
  theme,
} from '@hzblj/zyplot'
They are on every entry point. The builders live in the shared contract, so the same import works from @hzblj/zyplot, /web, /ios and /android, and every renderer draws what they describe: glow, halo, badge, pulse and scrubOpacity all land in the DOM as well as on a native canvas.

series and seriesProps

seriesStyles is a record keyed by ChartSeries.id, which means styling a series normally spells its id twice — once on the series, once as the key — with nothing checking the two agree. A typo does not fail: it silently drops the styling. series declares both in one place and seriesProps splits the list into the two props the chart takes.

tsx
const lines = useMemo(
  () =>
    seriesProps([
      series({
        color: '#ff3b4a',
        id: 'price',
        label: 'Price',
        style: { glow: glow({ opacity: 0.16, radius: 7 }), strokeWidth: 2.3 },
        values: range.values,
      }),
    ]),
  [range]
)

<Chart.Line {...lines} categories={range.categories} />
PropTypeDefaultDescription
series(options)StyledChartSeriesOne series with its styling attached. Everything ChartSeries takes, plus style for the per-series overrides.
seriesProps(list){ series, seriesStyles }Splits a list of styled series into { series, seriesStyles }, keying each style by the id it was declared with.
Hold the result across renders. seriesProps rebuilds both props on every call, and a chart whose props change identity re-serializes its whole dataset — over the bridge, on native. Wrap it in useMemo keyed on the data, as above.

annotation

annotations is a union discriminated by type, so writing one by hand means remembering which fields belong to which variant: a range takes start/end, a point takes x/y, and a line takes one value plus the axis it sits on. These set the discriminant and let autocomplete offer only the fields that variant actually has.

PropTypeDefaultDescription
annotation.lineNativeChartLineAnnotationA reference line: a target, a threshold, a launch date. Takes axis and value, plus dash and width for how it is drawn — omit dash for a solid rule.
annotation.rangeChartRangeAnnotationA shaded span: a quarter, an incident window, a tolerance band. Takes start and end.
annotation.pointNativeChartPointAnnotationA single marked point, placed by coordinate. Takes x and y.
annotation.textChartTextAnnotationFree text on the plot. Omit x and y and the renderer places it.

Because they are functions returning objects, a project's own vocabulary is a one-line wrapper — and the place to put the decision once rather than at each call site.

tsx
const baseline = (value: number, label: string) =>
  annotation.line({
    axis: 'y',
    // Omit dash for a solid rule; width is in points on both platforms.
    dash: [1, 4],
    id: 'baseline',
    label,
    // A dark chip behind the digits, and the side that keeps them inside the plot.
    labelBackground: '#000000',
    labelPosition: 'auto',
    // Fade the rule out entirely while a price is being read.
    scrubOpacity: 0,
    value,
    width: 1.5,
  })

const marketOpen = (category: string) =>
  annotation.line({ axis: 'x', badge: '', dash: [2, 4], id: 'open', size: 18, value: category })

<Chart.Line annotations={[baseline(range.baseline, 'Prev close'), marketOpen('09:30')]} />

axis

One builder per axis position. start and end put the labels in a gutter on that side; overlay puts them inside the plot against its trailing edge and reserves no gutter, which keeps the full width for the marks.

labelInset — how far an overlaid label sits off that trailing edge — only means something when the labels are inside the plot, because a gutter axis has no edge to sit off. So it is overlay's alone, and the other two do not accept it.

tsx
const priceAxis = (domain: { max: number; min: number }) =>
  axis.overlay({
    domain,
    format: { decimals: 2 },
    grid: false,
    labelInset: 22,
    labelSize: 13,
    ticks: false,
    // Only the two prices a reader is actually looking for.
    tickValues: [domain.min, domain.max],
  })

<Chart.Line xAxis={axis.start({ grid: false })} yAxis={priceAxis(domain)} />
PropTypeDefaultDescription
axis.startNativeChartAxisOptionsLabels in a gutter on the leading side — the left of a y axis, the bottom of an x axis.
axis.endNativeChartAxisOptionsLabels in a gutter on the trailing side.
axis.overlayNativeChartAxisOptionsLabels inside the plot, no gutter reserved. The only position that takes labelInset.
They describe the wider axis, and it is not native-only. What these return is a NativeChartAxisOptions, which is what xAxis and yAxis take on every entry point — so the overlaid position, labelInset, labelSize and ticks all work on a web chart too. Plain ChartAxisOptions objects stay valid everywhere, because the wider type is a superset of them.

reveal

The first-render entrance, passed as animation.reveal. Only a traced entrance has a frontier to light, so flashColor, trackColor and startOpacity are draw's alone — a fade has nothing to flash, and therefore takes only its duration and easing.

tsx
const arrival = animation({
  reveal: reveal.draw({
    duration: 420,
    // A brief brightening as the trace lands.
    flashColor: '#ffe3e8',
    flashDuration: 480,
    // Hold the glow a beat, then let it leave in one piece.
    flashEasing: 'ease-in-out',
    flashHold: 220,
    // Draw the whole path faintly first, so the shape reads from frame one.
    trackColor: '#8a8a8a',
    trackOpacity: 0.4,
  }),
  updates: false,
})

<Chart.Line animation={arrival} />
PropTypeDefaultDescription
reveal.drawChartRevealAnimationTraces the marks along the x axis. Takes the flash, track and easing fields. Forms with no direction — radial and hierarchical ones — fall back to a fade.
reveal.fadeChartRevealAnimationFades the marks in. Takes duration and easing only.
reveal.noneChartRevealAnimationNo entrance. Takes nothing.

marker

How the mark under the finger is picked out, passed as interaction.marker. The two styles read different fields: span is how far a segment reaches along the line and means nothing to a dot, size is a dot's diameter and means nothing to a segment. The type permits both at once; these do not.

tsx
const scrubbing = interaction({
  crosshair: 'x',
  crosshairStyle: { color: '#ffffff', width: 1 },
  // Dim the rest of the line so the lit segment reads as the reading.
  dimOpacity: 0.38,
  haptics: true,
  hover: 'nearest',
  marker: marker.segment({
    color: '#ffffff',
    glow: glow({ color: '#ff3b4a', opacity: 0.32, radius: 42 }),
    span: 2,
  }),
})

<Chart.Line interaction={{ ...scrubbing, tooltip: false }} />
PropTypeDefaultDescription
marker.pointChartSelectionMarkerA dot on the mark. Takes size; rejects span.
marker.segmentChartSelectionMarkerBrightens the stretch of line around the mark and blooms behind it, which reads as light moving along the data. Takes span; rejects size.
A marker is not a tooltip. It says only which mark is being read, never what it says — so use it when the value is shown outside the plot, and pair it with useChartScrub to drive that readout.

The passthroughs

The option groups with no variants to get wrong get a plain identity function each. They add no safety a prop's own type does not already give. What they add is a name to import and hold, so a preset can be declared and reused away from the chart that consumes it, and read as one thing rather than as an anonymous object literal.

tsx
// chart-style.ts — declared once, imported by both the line and the candlestick chart.
export const chartTheme = theme({ colors: { label: '#8a8a8a' } })
export const priceFormat = format({ decimals: 2, locale: 'en-US' })
export const card = surface({ background: '#0b0b0b', cornerRadius: 16, padding: 12 })
export const priceLine = seriesStyle({ glow: glow({ opacity: 0.16, radius: 7 }), strokeWidth: 2.3 })
PropTypeDefaultDescription
animationNativeChartAnimationEntrance and data-change timing.
interactionNativeChartInteractionPointer and finger behaviour.
seriesStyleNativeChartSeriesStylePer-series overrides.
plotChartPlotStyleThe drawing area inside the surface.
surfaceChartSurfaceThe box the chart sits in.
themeChartThemeColours and fonts.
formatChartNumberFormatHow numbers are written.
glowChartGlowA bloom behind a mark, in the mark’s own colour unless told otherwise.
haloChartHaloA hard disc behind a point — unlike a glow it has an edge, so a small bright dot can sit in a larger ring.
There is no builder for a pulse. It is the one nested object that also takes a boolean: pulse: true is the whole of what most live points want, and annotation.point already types the object form for the ones that tune the rhythm.