accepted

Default axis-padding to a 6px pixel gutter on every cartesian chart

Markers at data extremes no longer clip against axis frames. Scale range insets inward by 6px; axis lines stay at the frame edge. Opt out with `axisPadding: 0` or `false`.

LineChartScatterPlotCometChartBumpChartDistributionChartXGTimeline default-behaviourchart-apivisual-polish

Context

Markers at the data domain extremes clip against the axis frame today. A first-matchweek marker at x=1 sits exactly on the y-axis line; a top value at y=1.8 sits on the upper plot-area edge. Zero-config output is not publishable because the frame bisects markers.

Two distinct fixes exist in the wild:

  • Domain padding (matplotlib margins, ggplot expand, Highcharts minPadding): widen the axis domain by a fraction of the range. Changes tick positions. Needs clamp semantics so padding doesn’t force an axis below 0 for count-like data.
  • Axis gutter / range inset (Observable Plot inset, classic d3 pattern): leave the domain untouched; inset the scale range inside the frame by a few pixels. No clamp needed. No tick re-compute.

Decision

Adopt axis-gutter with axisPadding prop defaulting to 6 pixels (both axes). Shape: number | [x, y] | false. Scale range insets by the gutter; axis lines stay at the frame edge; data markers at domain extremes sit inside the frame by exactly axisPadding px.

Per-chart overrides: XGTimeline defaults to [0, 6] (no x-gutter — the timeline is bound by match-clock compression bands; shifting x-axis breaks alignment). All other in-scope charts take the symmetric 6.

Domain-padding / clamp is deferred to a separate packet — most users only need the cosmetic fix, and layering domain-pad on top preserves zero-config simplicity.

Why 6

Six pixels = default marker diameter (radius 3 × 2). Smallest value that guarantees marker-edge clearance for default styling. Observable Plot’s default inset is also 6px. Highcharts’ default is minPadding: 0.05 (proportional) — translates to ~15px on a typical chart, which reads as deliberately generous whitespace. We want tight editorial framing by default, not Excel-style breathing room.

Consequences

  • Every cartesian chart’s rendered output shifts by a few pixels. Golden SVG fixtures regenerated.
  • ChartCartesianAxes primitive gained an optional frame prop so the axis line can render at frame bounds while tick positions follow the (inset) plotArea. Default falls back to plotArea when no frame — backwards-compatible for any direct-primitive consumer.
  • Every affected model exposes layout.frame alongside layout.plotArea so renderers can draw background + clipPath at outer bounds, ticks + markers at inset bounds.
  • Three charts (Beeswarm, DistributionComparison, SmallMultiples) have non-standard layouts without a single layout.plotArea. Deferred to a follow-up.
← All decisions