# Charts Family

Canonical: https://socra.design/charts/charts-family

The charts.* namespace family — TimeSeries, TimeScatter, StatTile, StatRow, Legend, ToneDot, StackedBar — quantitative display on semantic tones and closed scales; the rendering engine is a module secret.

Use charts.* for every quantitative display: charts.TimeSeries for daily levels (area wash) and flows (thin line), charts.TimeScatter for event distributions (when × magnitude, one dot per event, optional benchmark line and click-to-select), charts.StatTile / charts.StatRow for the headline-figure strip, charts.Legend for swatch+label identity — static, or the series toggle itself, charts.ToneDot for the themed 8px status swatch beside a label, charts.StackedBar for the slim segments-carrying-tone bar (a part-to-whole stack, or against a max as a meter). A series carries a semantic TONE and a MARK, never a color, a pixel, or an engine prop; every axis, tooltip, grid, and duration recipe is standardized under the hood.

Status: ready. The charts.* namespace family (TimeSeries, TimeScatter, StatTile, StatRow, Legend, ToneDot, StackedBar) ships from @socra/ui-web-chart with recharts as a module secret: semantic tones resolved from @socra/theme, closed count/duration scales with standardized axis and tooltip recipes, keep-at-least-one legend toggling, and its executable public-API contract and behavior tests.

Library: @socra/ui-web-chart

## Anatomy

- charts.TimeSeries — the time-series chart: data is buckets on local-calendar keys — a day (`YYYY-MM-DD`) or, for a window of hours, a sub-day bucket (`YYYY-MM-DDTHH:mm`) the axis and tooltip read by the clock; each series declares key/label/tone/mark; level series wear an area wash + thick stroke, flow series a thin line; `active` pairs with the Legend toggle.
- charts.TimeScatter — every event as one dot (when × magnitude): points carry id/at/value/tone/title/caption; the scale (count | duration) decides formatting and axis ticks; `reference` draws the benchmark line; `onSelect` makes dots targets.
- charts.StatTile — one headline figure on the standard glass tile: label, formatted value, grounding caption; `—` when a figure cannot be derived.
- charts.StatRow — the responsive tile strip; the row owns all sizing, a tile never chooses its own width.
- charts.Legend — swatch + label per series so identity is never color alone; give it onToggle and it becomes the multi-select series control with keep-at-least-one enforced.
- charts.ToneDot — the 8px themed swatch keying a status or a series to its label; product code renders it directly beside a status word or a legend row.
- charts.StackedBar — one slim toned bar of segments; without `max` the segments fill 100% part-to-whole, with `max` they render proportional to that scale and the unfilled remainder shows on a recessive track (the meter reading).

## States

- **Rest:** Shows the stable, enabled component without implied activity.
- **Hover:** Adds pointer affordance without moving content or changing meaning.
- **Focus:** Shows the shared visible focus treatment without depending on hover.
- **Pressed:** Acknowledges active input immediately and returns cleanly on release.
- **Selected:** Uses a neutral surface plus native selected semantics when selection applies.
- **Disabled:** Remains legible, unavailable, and absent from misleading interaction feedback.
- **Loading:** Preserves context while honestly identifying work that has not completed.
- **Error:** Places the failure and recovery path beside the action or content that failed.

## Motion

Stable state changes respond immediately. Appearance, expansion, and morphing begin at the triggering origin and use an interruptible spring that can retarget from its live position and velocity. When spatial motion is not meaningful, the component changes without decorative travel.

## Usage

- Say the tone, never the color. primary/info/success/warning/error/neutral resolve from the active theme, so charts re-theme with the fleet; a hex dies at the type level.
- Pick the mark by what the series IS. A level (how much is standing) wears an area wash; a flow (how much moved) draws thin — the mark class is a second identity channel beside the tone.
- Let the scale format the values. count and duration own their formatting and axis ticks (42s / 18m / 3.2h / 2.4d on round steps) — call sites never carry format functions for axes.
- Show the distribution, not the average. TimeScatter exists because a daily average hides batch sweeps and outliers; every event is one dot and the reference line carries the benchmark.

## Avoid

- Do not import a chart engine in product code. recharts is a module secret behind the family boundary — a product importing it forfeits fleet-wide improvement (the fab-web dashboard was the violation that demanded this family).
- Do not pass geometry or engine props. The family owns heights, margins, axes, grids, and tooltips; height/margin/sx are rejected at the type level.
- Do not encode identity in color alone. Every toned mark also carries its label through charts.Legend and the tooltip — priority and status stay readable without color vision.

## Tokens

- `designTokens.color[mode].color.brand.primary`: Primary action role.
- `designTokens.color[mode].color.surface.secondary`: Working surface role.
- `designTokens.color[mode].color.content.primary`: Primary content role.
- `designTokens.spacing.role.contentGap`: Spacing relationship.
- `designTokens.shape.role.control`: Shape relationship.

## Example

```tsx
<charts.StatRow>
  <charts.StatTile label="Cycle time" value="3.2h" caption="median of 14 cycles · last 7 days" />
  <charts.StatTile label="Open" value="12" caption="waiting on the line now" />
  <charts.StatTile label="Closed" value="41" caption="done last 7 days" />
</charts.StatRow>;

<charts.Legend items={flowSeries} active={active} onToggle={toggleSeries} />;
<charts.TimeSeries
  data={flowDays}
  series={[
    { key: 'open', label: 'Open', tone: 'warning', mark: 'level' },
    { key: 'inWork', label: 'In work', tone: 'primary', mark: 'level' },
    { key: 'closed', label: 'Closed', tone: 'success', mark: 'flow' },
  ]}
  active={active}
/>;

<charts.TimeScatter
  points={cycles.map(cycle => ({
    id: cycle.issueId,
    at: cycle.doneAt,
    value: cycle.ms,
    tone: priorityTone(cycle.priority),
    title: cycle.title,
    caption: cycle.module,
  }))}
  since={since}
  until={now}
  scale="duration"
  reference={median === null ? undefined : { value: median, label: `median ${formatMedian(median)}` }}
  onSelect={openIssue}
/>;

<charts.ToneDot tone={STATUS_META[issue.status].tone} />;

<charts.StackedBar
  ariaLabel={`${row.module} · ${fmtCount(row.total)} issues`}
  max={fleetMax}
  segments={STATUS_ORDER.map(status => ({
    key: status,
    tone: STATUS_META[status].tone,
    value: row.counts[status],
    label: `${STATUS_META[status].label} ${fmtCount(row.counts[status])}`,
  }))}
/>;
```
