Semiotic 3.5.4
Asymmetric bands and percentile fans on LineChart/AreaChart, edge-anchored ticks, theme-driven CSS font-size variables, per-axis class names, loadingContent on every HOC, and one unified ribbon primitive for bounds and band.
AI-Generated·
3.5.4 lands a real envelope encoding on LineChart andAreaChart, sharpens the axis surface (edge-anchored ticks, CSS-variable font sizes, per-axis class names), and gives every HOC a sibling toemptyContent with the new loadingContent slot. Under the hood,boundsAccessor and band now share a single ribbon primitive — one scene builder, one y-extent pass, one style cascade. Full release notes are onGitHub.
Asymmetric bands and percentile fans
The new band prop on LineChart and AreaChart draws an asymmetric min/max envelope under the line/area, driven by independent y0Accessor andy1Accessor. That's distinct from the existing boundsAccessor(which is symmetric ±offset) and from AreaChart.y0Accessor (which replaces the area baseline). Pass a single BandConfig for one envelope or an array for percentile fans — p25/p75 stacked on top of p10/p90 is the canonical shape.
Per-series by default: one ribbon per lineBy / colorBy group, colored from the parent line at 0.2 fillOpacity. PassperSeries: false for an aggregate min/max envelope across all series. Bands are non-interactive by default (hovers pass through to the line on top); setinteractive: true if the band should participate in hit testing. Band y0/y1 values feed yExtent auto-derivation so a tall envelope can never clip; explicityExtent still wins. Live demo at/charts/line-chart#band.
Tooltip enrichment covers every interaction surface: the hovered datum carriesband: { y0, y1 } (first band) and bands: [...] (all bands) on the pointer hover path, each allSeries[i].datum in multi-mode, and the keyboard-navigation datum. The default tooltip auto-surfaces band rows whenband is configured without a custom tooltip — string accessors become labels; function accessors fall back to low / high.
Axis surface: edge anchors, CSS vars, per-axis targeting
tickAnchor: "edges" on frameProps.axes[i]— flips the leftmost tick's text-anchor to start and the rightmost to end on horizontal axes (and dominant-baseline tohanging / auto on vertical axes) so edge labels can't overflow the plot. Pairs naturally with axisExtent: "exact": exact pins the domain to the literal data min/max; edges keeps the labels readable at those bounds. Edge detection is pixel-based, so inverted y scales and reversed-x streaming charts anchor correctly.--semiotic-tick-font-size and --semiotic-axis-label-font-sizeCSS variables— emitted from the canonical theme typography fields (tickSize,labelSize) alongside the existing tick/title font-family/size variables. BoththemeToCSS and ThemeProvider write them;themeToTokens exports them as DTCG dimension tokens. SVG axes consume the vars via inline style, so an override on any ancestor (<div style={{ "--semiotic-tick-font-size": "14px" }}>) flows down without consumers needing !important.data-orient and per-axis class names— each axis now renders as its own<g class="semiotic-axis semiotic-axis-{bottom|left|right|top}" data-orient="…">inside .stream-axes. Style one axis at a time from external CSS:[data-orient='left'] text { font-size: 14px }. Tick text carriessemiotic-axis-tick, axis labels semiotic-axis-label, and chart titles semiotic-chart-title for class-based targeting.
loadingContent on every HOC
Sibling to emptyContent. When loading is true andloadingContent is set, it renders in place of the default shimmer-bar skeleton (wrapped in the same sized container so the chart slot stays reserved). PassloadingContent={false} to suppress the loading UI entirely — the early-return becomes null and a consumer's outer loading state takes over. Threaded through useChartSetup, useNetworkChartSetup, anduseCustomChartSetup; all 47 HOCs accept it via BaseChartProps.
One ribbon primitive for bounds and band
Both public envelope APIs (boundsAccessor and band) now normalize to a single resolvedRibbons: ResolvedRibbon[] array at the PipelineStore layer, then flow through xySceneBuilders/ribbonScene.ts — one scene builder, one y-extent expansion pass, one style cascade. The dedicated boundsScene.ts andbandScene.ts modules are gone. Public prop surfaces stay distinct (asymmetric pairs read better as band than as a boundsAccessor union return type), but the implementation is no longer duplicated.
Two correctness wins fell out of the unification: bounds ribbons now skip datums with null/NaN y (the coerced +null === 0 previously rendered a ribbon around the implicit-zero "value" of a missing row), and akind: "bounds" | "band" discriminator on each ribbon restrictsdatum.band / datum.bands tooltip enrichment to band-sourced envelopes — bounds stays decorative-only, matching its prior contract.
Upgrade notes
This release is additive. Consumers already using boundsAccessor get the null/NaN-row fix for free; anything that relied on the implicit-zero ribbon behavior should switch to filtering at the data layer. The website build now injects the Atom feed<link rel="alternate"> via the prerender step instead of source HTML, which closes a parcel resolution failure on nested prerendered routes.