# Sizing & scaling (/docs/sizing)

Every chart is a single `<svg>` with a `viewBox` and `preserveAspectRatio`. Give it a size in whatever way suits the
layout and the geometry follows. There's no `ResizeObserver`, no layout effect, and no client JavaScript: a static chart
scales through SVG and CSS alone.

## The three ways to size a chart

| You want…                    | Do this                                                  |
| ---------------------------- | -------------------------------------------------------- |
| A sensible default           | Pass nothing — every chart has an intrinsic box.         |
| An exact pixel size          | Set `width` / `height` props (where the chart has them). |
| To fill whatever contains it | Give it a width in CSS — the height follows.             |

> Not every chart has `width`/`height`. Grid charts derive their box from the cell: `ActivityGrid`, `CalendarStrip`, `GardenGrid` and `CohortTriangle` take `cell` and `gap`, `Honeycomb` takes `cell`, and `ConfusionGrid` takes `size`. The box then follows from the number of cells, so you tune the cell rather than the frame. Glyph marks (`StatusDot`, `TrendArrow`, `MoonPhase`, `DicePips`, and the rest) have one fixed intrinsic box and are sized with CSS. And `TokenConfidence` isn't an `<svg>` at all: it renders text, so it inherits the font size of whatever it sits in. Everything below about `viewBox` scaling applies to the SVG charts.

## Default: data alone

`data` alone renders at the chart's own intrinsic size. Each chart type ships a default box tuned to its shape: a
Sparkline defaults to a wide, short 80 × 20, while a RubricStrip has no fixed default height and grows a row per
criterion. Use it to drop a mark into a table cell or a sentence without picking dimensions.

```tsx
// no width/height → each chart's own default box
<Sparkline data={[3, 5, 4, 8, 6, 9]} />
```

## Fixed size: width & height

`width` and `height` are **viewBox units**. A static chart renders `<svg width={…} height={…}>`, so they also set the
rendered pixel box: one pair of numbers controls both the coordinate space the geometry is drawn in and the size on
screen.

```tsx
// width & height are viewBox units — they also set the pixel box
<Sparkline data={[3, 5, 4, 8, 6, 9]} width={200} height={48} />
```

> Because `width`/`height` set the viewBox, they change how much room the geometry has: a taller RubricStrip gives each criterion a thicker row, a wider Sparkline spreads its points further apart. They are layout dimensions, not a zoom level.

## How big a chart usefully gets

Each chart's geometry is authored for a box in the word-sized range, and past that box it stops scaling. Internal caps
hold the marks at their authored size and the extra room becomes whitespace.
`<EventTimeline width={823} height={658} />` draws a 6-unit bar in 658 units of nothing, because the span bar caps at 6
viewBox units, the point diamond at 2.5, and the track inset at 2.

Every chart publishes its own ceiling as `maxWidth` and `maxHeight` in [`/catalog.json`](/catalog.json), the machine
catalog an agent reads to pick a chart. Sparkline is authored to 340 × 100, EventTimeline to 320 × 50, Progress to 200
× 40.

To render larger, keep the props inside that box and scale with CSS. The viewBox handles the rest and the marks keep
their proportions:

```tsx
<Sparkline data={data} width={320} height={80} style={{ width: "100%", height: "auto" }} />
```

Charts sized by `cell`, by their own content, or by CSS alone publish no maximum. Each one says which knob sizes it in
its catalog `gotchas` list.

## Fill the container

Set a width in CSS. The height follows on its own, because the `viewBox` keeps the aspect ratio and `.mc-root` ships
`height: auto`. This is the one-liner for a KPI card, a responsive dashboard tile, or a full-width hero:

```tsx
<Sparkline data={data} className="w-full" />
```

A class and a `style` do the same thing here, and either one is enough:

```tsx
<Sparkline data={data} style={{ width: "100%" }} />
```

Spelling `height: auto` yourself is still fine. It changes nothing, because that is the shipped default.

The chart now grows and shrinks with its parent. To constrain it, size the container or give it a `max-width`:

```tsx
<div style={{ width: "100%", maxWidth: 320 }}>
  <Sparkline data={data} className="w-full" />
</div>
```

A chart also never overflows a container narrower than itself: `.mc-root` carries `max-width: 100%`, so a 80-unit Bullet
in a 60px cell shrinks to 60px instead of spilling.

You can still pass `width`/`height` alongside fluid CSS. They set the chart's aspect ratio and its size before CSS takes
over the width, and geometry still resolves in those units, so a wider `width` prop spreads a series further apart
rather than magnifying it.

To give a whole row of charts one identical box rather than sizing each by hand, put `width`/`height` on `SparkGroup`:
it enforces them on every series child that didn't set its own, and shares one scale at the same time. See
[Formatting & scale](/docs/formatting#scale-the-domain-decision).

## Measure the container with useFluidWidth

CSS stretches a drawing the geometry already laid out. When you want the geometry to lay out again at the new size,
measure the box and pass the number:

```tsx
"use client";

import { useFluidWidth } from "@microcharts/react/fluid";
import { Sparkline } from "@microcharts/react/sparkline";

export function Card({ data }: { data: number[] }) {
  const { ref, width } = useFluidWidth(240);
  return (
    <div ref={ref}>
      <Sparkline data={data} width={width} />
    </div>
  );
}
```

`width` is a number on every chart, with no string form anywhere in the catalog, and that is why measuring is the
answer. A CSS `width: 100%` over a rendered viewBox scales the drawing rather than re-laying it out, so the spacing
between ticks stops meaning what it meant at the authored size.

The hook is opt-in, and nothing in the library imports it. Static entries are hook-free, listener-free, and
observer-free by architecture: they render on the server, where there is nothing to measure, so a chart can never do
this for you. The subpath measures 403 B gzip and adds nothing to any chart subpath.

It answers the four questions a hand-written version has to answer:

- **`initial`** is what renders on the server, on the first client paint, and where `ResizeObserver` doesn't exist. It
  defaults to 80, the width most charts fall back to on their own. Pass the width you expect so the layout is reserved
  and the measurement causes no reflow.
- **A measured 0 never reaches `width`.** A collapsed disclosure, an inactive tab, and a `display: none` ancestor all
  measure 0, and a chart 0 units wide draws nothing, so the last real width holds until the box comes back.
- **Widths round to whole pixels**, matching the integer viewBox coordinates the charts draw in.
- **Commits land once per animation frame**, so dragging a window edge doesn't write state inside the layout pass it was
  measured in.

Attach `ref` to one element and keep it for the life of the component. The observer binds on mount, so swapping the node
out leaves it watching the old one.

`useFluidWidth` answers "my chart lives in a flexible box"; the [authored maximum](#how-big-a-chart-usefully-gets)
answers "how big is worth asking for". Clamp the measured number when the container can outgrow the chart:

```tsx
<Sparkline data={data} width={Math.min(width, 340)} />
```

> `.mc-root` is `display: inline-block` by default, so a chart flows next to text. Filling a block-level container works anyway; if you ever see one refuse to stretch, that's the reason. Give it `display: block` or a flex/grid parent.

## Interactive charts size the same way

Everything above applies **unchanged** to the `…/interactive` entries. An interactive chart wraps its SVG in a focusable
`<span>` that owns the pointer, keyboard, and touch gestures for the whole chart, and that wrapper and the SVG are
always the **same box**: the wrapper is an `inline-block` that hugs its child, and the composed SVG inside is pinned to
`width: 100%; height: auto`. Your `style` merges over the wrapper's base style and your `className` composes after its
base class, so the same recipe fills it:

```tsx
import { Sparkline } from "@microcharts/react/sparkline/interactive";

// identical to the static entry — fills its container
<Sparkline data={data} style={{ width: "100%", height: "auto" }} />;
```

> Because the wrapper and the SVG share one box, hit-testing is exact at any size: the crosshair, highlight, and readout land under the cursor, a touch drag scrubs the unit beneath the finger, and a pinned selection stays on the right unit when the box reflows. An interactive chart is its static twin plus interaction, at the same size.

## The readout in a tight container

Put an interactive chart anywhere: a scrolling side rail, a table cell, a sticky header, a panel with
`overflow: hidden`. The hover readout renders in the
[top layer](https://developer.mozilla.org/en-US/docs/Glossary/Top_layer) and is placed by CSS anchor positioning, so it
doesn't need room inside your container and no ancestor can clip it.

Three behaviors follow, and none of them costs you a prop:

- It **flips below** the chart when there isn't room above.
- It **stops at the window edge** and grows the other way instead of running off-screen. A chart flush against the right
  edge opens its readout to the left.
- It **hides** when the chart scrolls out of view, rather than floating over unrelated UI.

Chrome, Edge, and Safari place it this way. Firefox has no anchor positioning yet, so it keeps the older placement:
above the chart, positioned against the chart's own box. That degrades the placement, never the reading — the value and
the announcement are identical either way.

You don't need to reserve headroom, add padding, raise a `z-index`, or set `readout={false}` to stop a clipped chip. If
you set `readout={false}` for that reason, you can drop it.

## Inline in a sentence

A chart in running prose should scale with the **font** rather than a pixel box. Wrap it in `className="mc-inline"`
(shipped in `styles.css`) and size it in `em` so it rides the text:

```tsx
<p>
  Revenue is up this week{" "}
  <span className="mc-inline">
    <Sparkline data={[3, 5, 4, 8, 6, 9]} style={{ height: "1em", width: "auto" }} />
  </span>{" "}
  and holding.
</p>
```

`mc-inline` handles vertical alignment; the `em` height ties the mark to the surrounding type size, so it stays
proportional wherever the text lands. The alignment is a **baseline seat** rather than a fixed nudge: the wrapper is an
`inline-flex` box whose only child is the SVG, so it takes its baseline from the SVG's bottom edge and the mark stands
on the text baseline like a glyph. That holds in any typeface.

Symmetric glyph marks have no bottom edge to stand on, so `mc-inline` centers those on the cap band instead:
`TrendArrow`, `StatusDot`, `MoonPhase`, `DicePips`, the radial dials, and the other glyph shapes read like an icon set
in running prose. You don't opt in.

Which seat a mark gets is never guessed from its class name. Each chart reports its own plot box, measured in the same
viewBox units it draws in, and the stylesheet seats the mark from that. So a chart whose padding changes can't drift out
of alignment, and a chart that changes shape with its props carries the right seat for each: `SparkBar` stands on the
baseline in bar mode and centers in win-loss, because only bar mode has a floor. Two tokens nudge the result if a
particular typeface needs it: `--mc-inline-nudge` for every mark, `--mc-glyph-nudge` for centered ones only.

> Leave `<Delta>` bare, with no `mc-inline` wrapper. It renders as inline text and already owns its own baseline.

## Strokes stay crisp at any scale

Scaling an SVG normally thickens or thins its lines. microcharts sets `vector-effect: non-scaling-stroke` on data marks,
so a line drawn at 200 px and the same line at 800 px keep the **same visual stroke weight**: the geometry scales, the
ink doesn't. Square-cornered marks also use `shape-rendering: crispEdges`, so bars and grids stay sharp at any size.
Rounded corners keep their anti-aliasing, and a mark under one viewBox unit opts back out, because snapping its edges to
the pixel grid can erase it.

Two marks scale their ink on purpose. A MicroDonut wedge and a ProgressRing arc are drawn as a stroke, so the band you
see is their stroke weight, and it grows with the box like any other geometry.

Stroke weight is a token: set `--mc-stroke-width` (see [Theming](/docs/theming)) to make it heavier or lighter,
independent of how big the chart is.

For a whole dense context, a table packed with sparklines, use `--mc-density`: one scalar that scales stroke weight,
label size, and small-multiple gap together (`< 1` tighter, `> 1` airier). It tunes the ink and type, never the box, so
`width`/`height` still own the geometry.
