Flutter docs
Default skin

ProgressCurve Composition

An S-curve: what a plan said would be complete by each day, against what was reported. It answers one question — how far behind are we, today, in points — and says the answer in words before it draws anything.

Usage

Ruko Depok — structurePlannedActual

73% actual · 96% planned · 23 points behind plan ()

Last report ·

Progress by day
DayPlannedActual
Jun 1, 20260%0%
Jun 15, 20266%7%
Jul 1, 202618%19%
Jul 15, 202634%30%
Aug 1, 202652%41%
Aug 15, 202670%52%
Sep 1, 202686%64%
Sep 15, 202696%73%
Sep 30, 2026100%73%

Reading, then picture

The sentence above the chart is not a caption. It is the reading on the day the reader is standing on: the actual, the planned, and the gap between them in points. Somebody checking a job on a phone between sites gets their answer without interpreting a line, and the picture is there for the shape of the slip — when it started, whether it is still widening.

The gap retains the sign of the supplied readings before formatting. A nonzero difference must not be classified as on plan merely because a rounded summary displays zero. The default gap reading says less than 0.1 for such differences. formatGapNumber lets a caller localize that magnitude separately from progress percentages; it receives the nonnegative, unrounded magnitude without a unit. If only formatNumber is supplied, it continues to format the gap too, so that formatter must keep small directional differences distinguishable from zero.

A day nobody reported is not a day of no progress. Both series carry the last report forward, so a gap in reporting reads as a flat stretch rather than a collapse to zero, which would say the work went backwards.

The vertical scale is pinned to 0–100%. Autoscaling would stretch a job that has reached 30% to the full height of the plot, which reads as nearly there — the opposite of the truth.

Interaction contract

Read-only. A crosshair follows the pointer; there is no pan, no zoom, no selection and no dismissal. A dashboard chart is read, not traded, and panning would let a reader scroll the plan off the screen with no way back.

What is marked on the plot is the last report, not today. Its label and date are also repeated below the plot so they remain readable when the plotted point is near an edge. A marker attaches to a data point, and today is usually not one — a "today" mark snapped to whichever reported day sat nearest it and then carried a label that was false, an arrow reading Today pointing four days off it. The last reported day is a point that always exists, so the arrow lands exactly on it, and it tells a reader the one thing the line cannot: how stale the actual is. Today's reading is stated in words above the plot, where it needs no pixel to land on.

The date in that reading is written for a person. formatDay lets a product choose its language; the underlying <time dateTime> retains the calendar day for machines. The plot's axis, crosshair, report marker and accessible table use the document's language. Axis labels omit the year to stay compact; the crosshair shows the complete calendar date.

Accessibility

The plot is a <canvas>, so it is not in the accessibility tree at all. This component therefore ships a second rendering rather than a description of the first: a table of every day either series reports, with both values and the gap. It is always in the DOM, visually hidden, and built from the same numbers as the line.

The table keeps the detail values at the precision supplied by formatDetailNumber, so a value such as 0.04 remains visible as 0.04%. It defaults to formatNumber; give it a separate formatter when the spoken summary should stay rounded while the detailed values retain more precision. Both formatters return the number without the percent sign.

Under forced-colors: active the canvas is not rendered at all and the table takes its place. A bitmap is the one thing a forced-colors user cannot have recoloured for them, and an empty labelled region holding a chart's worth of blank space is a worse answer than the readings themselves.

Colour is never the only difference between the two lines: the plan is dashed and thin, the actual is solid and thick, in the plot and in the legend.

Do

Use it when a plan and its delivery share one scale over time, and the gap between them is the point.

Don't

Use it for counts by category, or for a series with no dates — the substrate's scales are time scales.

Loading

Substrate

The canvas, the Token reading, the forced-colors path and the accessible table belong to TimeSeriesChart, the primitive every dated chart here is built from. This Composition adds the part that is about progress: the scale pinned to 0–100%, the two series with their dash treatment, and the reading in words.

That split happened when the second dated chart arrived (BalanceCurve). The shared half is ~200 lines of canvas and Token handling in which three defects had already been found — a theme applied during the substrate's async import being lost for good, an invisible table-sized box lying across the plot, and a mark labelled "Today" pointing four days off it. A second copy would have carried whichever two were fixed last.

Underneath, TradingView's Lightweight Charts 5.2.1 (Apache-2.0), imported dynamically inside the mount effect. A server render never evaluates it, and a consumer who shows no chart never downloads it — in a real consumer's production build it lands as its own 57 KB gzipped chunk. Attribution lives in THIRD_PARTY_NOTICES.md rather than on the canvas.

Tokens are read from the DOM with getComputedStyle and re-read whenever the theme, the Skin or the system colour scheme changes — a canvas cannot inherit CSS. Where they do not resolve, nothing is drawn: the component will not invent a colour, and the table stands as the rendering until they do.

Props

PropTypeDefaultNotes
plannedCurvePoint[]—What the plan said would be complete, by day. `{ day: "YYYY-MM-DD", percent: 0-100 }`.
actualCurvePoint[]—What was reported complete, by day. A repeated day takes the later value as the correction it is.
labelstring—Accessible name for the plot. Name the job.
todaystring—The day the reading above the plot is taken on; both series carry their last value at or before it forward. Not drawn on the plot. Without it, the reading is taken on the last reported day.
heightnumber260Plot height in pixels. The width is the container's.
plannedLabelstringPlannedLegend and column name for the plan.
actualLabelstringActualLegend and column name for delivery.
lastReportLabelstringLast reportLabel of the last-reported-day marker, repeated with its date below the plot.
emptyLabelstringNothing has been reported yet.Shown instead of the chart when neither series has a usable day.
tableLabelstringProgress by dayCaption of the accessible table.
dayLabelstringDayHeader of the table's day column.
formatDay(day: string) => stringEnglish day, short month, and yearFormats the visible reading day; the calendar day remains in the time element's dateTime.
formatNumber(value: number) => stringone decimal, dotHow actual and planned progress are written in the reading, without the `%`. A supplied formatter also formats the gap unless formatGapNumber is supplied.
formatGapNumber(magnitude: number) => stringsupplied formatNumber, otherwise one decimal with less than 0.1 for small nonzero gapsFormats the unrounded nonnegative gap magnitude without its unit. Supply separately when small directional gaps need different wording from rounded progress.
formatDetailNumber(value: number) => string`formatNumber`How values in the chart detail are written, without the `%`. Choose this separately when detail values need more precision than the reading.
gapUnitLabelstringpointsUnit after the plan–actual gap. The value is percentage points, not a relative percent; products may supply their own language.
aheadLabel / behindLabel / onTrackLabelstring—The words the gap is stated in.
loadingbooleanfalseRenders `ProgressCurve.Skeleton` in place of the chart.

Tokens

--cb-tone-brand (actual), --cb-fg-subtle (planned, crosshair), --cb-fg-muted, --cb-fg, --cb-border (grid and axes), --cb-surface (plot ground), --cb-font-sans, --cb-tone-danger and --cb-tone-success for the reading, plus --cb-space-*, --cb-text-* and --cb-radius-*.