Skip to content
HDCharts

Pipeline and Stage Ownership

Every chart is the same fifteen stages. This page names them, says which stage belongs where, and records where each chart differs. Entry Seam describes the four stages that run in ChartEntry and how a chart declares them; this page is where a stage goes and why.

The pipeline splits in two. Stages 1 to 4 turn public data and a style into a render model and a drawable style, and run once before anything is drawn. Stages 8 to 15 work in pixels — a measured size, the clock, a pointer — and run in the chart's content composable. Stages 5 to 7 and 11 to 12 sit either side of that split and are the interesting ones.

The stages

#StageCanonical codeRuns in
1Selection lifecyclerememberSelectionLifecyclePublic composable
2ValidationChartPolicy.errorsForSeam
3Style clampChartSpec.clamp → style.clamp(density)Seam
4Model conversionChartSpec.convert → ChartRenderDataSeam
5Density decisionshouldUseScrollableDensity, maxBarsThatFitContent
6AggregationaggregateForCompactDensityContent
7PaletteresolvePaletteColorsSee Palette
8Chrome layoutBoxWithConstraints and the header, plot and legend slotsContent
9Axis planningplanAxisXLabelStride, buildNumericYAxisTicksContent
10Canvas limit guardchartCanvasFitsContent
11DomainresolveLineRange, resolveBarRange, minMax, resolveStackedTotalsRangeContent
12NormalizationnormalizeByMinMax, barValueYFraction, normalizeStackedValuesContent
13Animation stateupdate() → animateToContent
14Gesture mathselectedIndexForTouchX, nearestPointIndexForContentXContent
15DrawdrawLineChartSeries → drawChartPathContent

Rules

These four are the contract. Everything above is a description of them.

1. Stages 2 and 3 happen at the entry point, once. Validation logic lives in charts-core; which checks apply is the chart's ChartPolicy. The style is clamped at the ambient density before any content runs.

2. Stage 4 happens once, between the entry and the content composable. Public ChartData becomes the render model at exactly one seam. A chart does not convert at the entry and again below.

3. Stages 8 to 10 and 13 to 15 happen inside the content composable, because they work in pixels. Layout, axis planning, the canvas guard, animation, gesture maths and drawing all need a measured size, the clock, or a pointer. A stage that needs a measured size is never hoisted to the entry.

4. Formatting reads the caller's formatter and is never hardcoded in a content composable. A chart without a formatter parameter falls back to its Defaults.

Rules 1, 2 and 3 hold for every chart. Rule 4's exception is stacked bar, stacked area and radar, which format their readouts from ChartValueFormatters.Default because their public composables declare the formatter through ChartValueFormatters.Default.

The test for a stage that is not obviously placed

Is this stage a pure function of the public data, the style and the density — or of a measured size, the clock, or a pointer?

Pure in the first: decided once, cannot change while the chart is on screen, belongs in the seam. Pure in the second: belongs in the content composable, because hoisting it would recompute it on every size change or read a value that does not exist yet.

Two costs sit on top of purity. A stage that walks the data costs a pass per data change, so a hoist has to be justified against the passes already there; and ChartEntry must not walk the data twice for the same fact, which is why validation checks finiteness and the conversion does not check it again.

Which chart runs which stage

StageLineBarHistogramStacked barStacked areaRadarPie
1 Selection lifecycleyesyessharedyesyesyes, twiceyes
2 Validationyesyessharedyesyesyesown path
3 Clampingyesyessharedyesyesyesyes
4 Conversionpass-throughpass-throughpass-throughtransposespass-throughpass-throughnone
5 Density decisionyesyessharedyesyes——
6 Aggregationyesyesdisabledyesyes——
7 Paletteframeown coloursharedentryentryentryown file
8 Chrome layoutLineChartFrameBarChartContentsharedStackedBarChartFrameStackedAreaChartFrameinlinePieChartFrame
9 Axis planningyesyessharedyesyeslabels only—
10 Canvas guardyesyessharedyesyes——
11 DomainresolveLineRangeresolveBarRangesharedtotalstotalsminMaxshare-based
12 Normalizationlistscalar per barsharedlistlistlistshare-based
13 Animationyesyessharedyesyesyesyes
14 Gestureyesyessharedyesyesyesyes
15 Drawyesyessharedyesyesyesyes

Bar and histogram share one plot, so they share every stage but aggregation: stages 1 to 5 and 7 to 15. Their specs differ in the policy — histogram forbids negative bin heights — and only histogram passes aggregate = false, so histogram runs the density decision but never compacts.

Recorded divergences

Each of these is a decision, written down so the next change reads it.

Two pipeline shapes. Radar and pie run stages 1 to 4, 7, 8 and 11 to 15. Neither compacts, so they skip the density decision and aggregation, and neither runs the Cartesian canvas guard. Radar labels its own axes in place of stage 9; pie has no axis and skips it. Every other chart runs all fifteen. A Cartesian chart with a compact mode and a polar or share-based chart are presented as one pipeline, and this is the shape the library has. Pie runs the same stages 1 to 4 as every other chart, through PieChartEntry.

The domain has five shapes, and pie has none of them. Bar and both stacked charts fold zero into the domain, so an all-positive series starts at the axis. Line and radar take the data's own range. Line, bar and histogram accept a caller's fixed range through style.range. Stacked area's domain is a line inside its normalizer, so finding it means opening the normalizer. Pie's axis is the caller's own share, not a data range, which is why its matrix row reads share-based. A domain is a pair of doubles and reads no constraint, so by the test above it could sit in the seam; whether it should is open.

Flat data draws at a different height per chart. Line passes zeroRangeValue = 0f and draws a flat series at the bottom. Radar passes 1f and draws it at full radius. Both domains are the same shape.

Normalization has two shapes. Line's is a list per series, because it animates between two of them. Bar's is a per-bar scalar computed at draw time. Each is the shape its chart needs.

Palette

resolvePaletteColors is pure in the clamped style and an item count, so by the test above it belongs at the seam. Each style exposes a thin wrapper over it — resolveColors in line, stacked bar and stacked area, resolveLineColors in radar — and pie's wrapper takes a slice count instead of a series count. Stacked bar (StackedBarChartEntry), stacked area (StackedAreaChartEntry), radar (RadarChartEntry) and pie (PieChartEntry) resolve inside their entry. Line resolves it in rememberLineColors, which is declared in LineChartFrame and called from LineChartImpl and LiveLineChartImpl, one frame above where it is declared.

singleItemUsesBase is a boolean literal at each of the five call sites. A single-series chart draws its base colour on line, stacked area and radar, and a generated shade on pie and stacked bar.

Bar does not use the palette helper. It repeats one colour across its bars and keeps two values: the caller's colors when they set any, and bars.color as the fallback for every bar when they set none. Compact mode reads the caller's palette at the bucket centres in source index space, because a bucket is drawn where its centre was. Both values reach the draw site, which reads barColors.getOrNull(index) ?: defaultBarColor. BarBarsStyle.resolveColors is the shape the other charts use and has no production caller — BarStyleBlocksTest is its only one.

What deliberately stays in the content composable

  • The domain and normalization when a caller's fixed range can clip the data. Line resolves its domain from style.range, so the clamp at the end of normalizeValue is what keeps a line inside a plot whose range excludes its highest value. Validation Errors has the case.
  • Animation buffers. The morph's two buffers are deliberately different lengths mid-animation, so its lookup has to tolerate the shorter one.
  • Axis planning. AxisXPlanRequest is mostly measured input, and the plan is recomputed on every resize anyway.

Adding a stage or a chart

A new chart runs all fifteen stages in this order. A chart that skips one says so here; radar and pie skip stages 5, 6 and 10, and stage 9 is radar's own label layout and absent from pie.

A new stage goes where the test puts it, and a stage whose behaviour differs between charts is declared in the chart's ChartSpec, not branched inside a content composable. A policy field nothing reads is the drift the seam exists to remove, so a field lands with the change that reads it.