Skip to content

Chart System Architecture

Cindy Zhang edited this page Jun 23, 2026 · 1 revision

Chart System Architecture

How we think about data visualization in Astryx — the problems charts solve, the correctness guarantees the architecture enforces, and the design choices left to the developer.

For general Astryx conventions, see API Conventions and Astryx Philosophy.


The Problem

Charts lie. Not usually on purpose — but the default behavior of most charting libraries makes it easy to produce misleading visualizations without realizing it.

The common failure modes:

Overlaid datasets use different scales. Two lines on the same chart look comparable but are actually mapped through different y-domains. The visual relationship is arbitrary. This happens when chart components compute their own ranges independently.

Axes and marks disagree. The y-axis says 0–100, but the bars are actually mapped to a different range because the bar component recomputed the domain from its own data subset. The grid lines don't match either.

Streaming data jitters. Real-time charts auto-scale on every frame, so the axis jumps around constantly. A value that was at the top of the chart is suddenly in the middle because a new point stretched the domain.

Silent data reduction hides outliers. Large datasets get downsampled for rendering, and the naive approach (skip every Nth point) drops the spikes that are the whole reason you're looking at the chart.

Color encodes data but excludes people. Sequential palettes that aren't perceptually ordered mislead about magnitude. Categorical palettes that aren't colorblind-safe exclude 8% of male users.

These aren't edge cases. They're the default behavior of most charting approaches, and they produce charts that are subtly wrong in ways that are hard to notice and easy to act on.


Two Tiers

Some of these problems have objective answers — a chart where the axes don't match the data is wrong, full stop. Others are judgment calls where the right answer depends on context.

We split them into two tiers.

Tier 1: Correctness Guarantees

These are enforced by architecture, not documentation. A mark component physically cannot render at a different scale than its siblings because the only scale available is the one in context.

Guarantee Mechanism
All marks share one coordinate space Single yScale/xScale in React context. Components read it; none create their own.
Axes match the data Axes read from the same context scales as marks. They can't disagree.
Overlaid datasets are comparable Same scale, same pixel mapping. A point at value 50 on series A and value 50 on series B land at the same y-pixel.
SVG and WebGL agree Both call yScale(value) from context. Rendering backend is a detail, not a coordinate system.
Domain expands, never clips yDomain sets a floor. Data that exceeds it widens the range — nothing gets silently truncated.
Streaming uses the chart's scale ChartStreamGL reads yScale from context, not its own internal mapping.

These aren't props. You can't opt out. They're the shape of the system.

Tier 2: Rational Defaults

These are design choices where the right answer depends on what you're building. We pick defaults that produce honest charts and let you override them.

The pattern: rational default, explicit override, documented tradeoff.

A bar chart that doesn't start at zero is sometimes the right call — we default to including zero, the docs explain why, and yBaseline='data' is one prop away. No warnings, no errors. You read the docs, you make the call.


How It Works

Chart Owns the Coordinate Space

Chart computes the scales from data and provides them to children via React context. Everything else reads from it.

const colors = useChartColors();

<Chart data={data} xKey="month" yKeys={['revenue', 'trend']} height={300}>
  <ChartGrid horizontal />
  <ChartAxis position="bottom" />
  <ChartAxis position="left" />
  <ChartBar dataKey="revenue" color={colors.categorical(2)[0]} />
  <ChartLine dataKey="trend" color={colors.categorical(2)[1]} dots />
  <ChartTooltip />
</Chart>

The grid, axes, bars, line, and tooltip all read from the same context. There is no way for the bar to end up on a different scale than the axis next to it.

Domain control:

  • yDomain={[0, 100]} — sets a stable range. Data can push beyond it; it can't shrink below.
  • yBaseline='auto' — includes zero when all values are positive (the rational default for bars).
  • yBaseline='zero' — symmetric around zero (for profit/loss, seismograph, sentiment).
  • yBaseline='data' — tight fit to data extent (stock prices, temperatures).

Composition

Charts are composed from independent components. Bars, lines, dots, areas, error bars, candlesticks — all siblings, all reading from the same context. You mix SVG and WebGL marks on the same chart. You add or remove interaction layers without touching the data mapping.

This also means each component is testable on its own. You can verify a bar renders at the correct pixel position without the rest of the chart.

Color

Colors come from useChartColors() (React) or getChartColors(theme, mode) (non-React). Both return the same API, both resolve from theme tokens.

const colors = useChartColors();

colors.categorical(5)                        // 5 distinct series colors
colors.sequential.blue(3)                    // 3-stop blue ramp
colors.diverging.positiveNegative(7)         // shamrock → gray-1 → red
colors.alpha('#0171E3', 0.5)                 // any color with opacity

Sequential ramps are perceptually ordered (dark = high). Diverging palettes have a neutral midpoint (gray-1, per design spec). Legends accept palette output directly: gradient={colors.sequential.blue(5)}.

WebGL

For large datasets (1k+ points), WebGL marks render to a canvas overlay aligned pixel-for-pixel with the SVG chart space. The y-mapping still comes from the chart's yScale — the only difference is what draws the pixels.

  • ChartDotGL — scatter in a single draw call
  • ChartDotGLInteractive — GPU color-picking for O(1) hover detection (encodes each point's index as a unique color in an offscreen framebuffer, reads back the pixel under the cursor)
  • ChartHeatmapGL — 2D grid with color ramps
  • ChartStreamGL — ring-buffer streaming line, zero React re-renders per frame

Data Reduction

m4Reduce() implements the M4 algorithm — for each pixel column, keep the first, last, min, and max values. A million-point time series becomes ~4×width points with no visible loss. Outliers are preserved because min/max are always kept.

Data reduction is explicit (you call m4Reduce yourself). If we add auto-reduction later, it'll come with a visual disclosure so the user knows the data was aggregated.


Defaults Table

Default Why Override
yBaseline='auto' Includes zero for bars — prevents visual exaggeration 'data' or 'zero'
height=300 Reasonable aspect ratio for most data Any number
Horizontal grid only Horizontal lines aid y-value reading without clutter Add vertical prop
curveMonotoneX for lines Preserves data monotonicity — no false peaks between points Future: curve prop
Sequential palettes dark→light Perceptual convention: dark = high intensity Custom colorRange
No dual y-axes Dual axes create arbitrary implied relationships between scales Not implemented; if demanded, would be an explicit component with documented risks

Anti-Patterns

Can't happen (Tier 1)

Anti-Pattern Why it's structurally impossible
Overlaid data on different scales Single yScale in context. Marks can't create their own.
Axes disagree with marks Both read from same context.
SVG and WebGL positions diverge Both call yScale() from context.
Data silently clipped yDomain is a floor, not a ceiling.

Discouraged by defaults (Tier 2)

Anti-Pattern Risk Default Override
Truncated bar axis Exaggerates differences yBaseline='auto' includes 0 yBaseline='data'
Dual y-axes False correlation Not built Explicit component if added
3D effects Distorts area perception Not built
Color-only encoding Excludes colorblind users Documented guidance Shapes/patterns (future)
Silent data reduction Hides outliers M4 is opt-in Auto with disclosure (future)

References

  • Tufte, The Visual Display of Quantitative Information (1983)
  • Cleveland & McGill, Graphical Perception (1984)
  • Jugel et al., M4: Visualization-Oriented Time Series Data Aggregation (VLDB 2014)
  • WCAG 2.2 — 1.4.1, 1.4.3, 1.4.11

Clone this wiki locally