Axis Labels — Smart Layout¶
Dataface picks readable axis-label layout by leaning on Vega-Lite's scale-type-aware defaults and adding one targeted improvement (automatic horizontal orientation for bar charts with long category labels). Authors override anything they want via the standard style cascade.
The decision tree¶
flowchart TD
A[Axis with labels to render] --> B{Is this a bar chart with<br/>discrete x and style.orientation: auto?}
B -- yes --> C{Would labels be too long<br/>for horizontal display?}
C -- yes --> D[Flip to horizontal bars<br/>category labels go on y-axis<br/>render horizontal naturally]
C -- no --> E[Keep vertical bars<br/>VL defaults apply to x labels]
B -- no --> E
E --> F{Vega-Lite defaults<br/>by axis scale type}
F -- nominal / ordinal --> G[labels rotated -90°<br/>no auto-drop<br/>author overrides via label.angle]
F -- temporal / quantitative<br/>non-log --> H[labels horizontal at 0°<br/>parity-drop on overlap]
F -- log-scale quantitative --> I[labels horizontal at 0°<br/>greedy-drop on overlap]
The same tree in words:
- Bar chart with discrete x and
style.orientation: auto? If yes, check whether category labels would crowd. If so, flip the chart to horizontal — category labels move to the y-axis and render horizontal without tilting. - Otherwise: Vega-Lite's scale-type-aware label defaults apply unchanged. Nominal / ordinal labels rotate to
-90°(vertical), no drops. Continuous (temporal / quantitative) labels stay horizontal at0°, parity-drop on overlap (or greedy-drop for log scales). These defaults are sensible for the most common cases; for everything else, the override knobs below cover it.
Strategy reference¶
Overlap values¶
label.overlap is a four-value string enum. The theme defaults to
"smart".
| Value | What it does |
|---|---|
"smart" (theme default) |
Lets Vega-Lite pick per scale type — parity-drop for non-log continuous (numbers, non-log time), greedy-drop for log, none for nominal/ordinal. The "right thing per chart" default. |
"parity" |
Drop every other label until no overlaps remain, regardless of scale type. Use when you want to force thinning on a nominal axis (where the smart default would keep every label). |
"greedy" |
Linear scan, drop any label that overlaps the last visible one. Use on log scales or irregular cadence (fiscal years with gaps), where the leftmost label anchors. |
"allow" |
No overlap reduction — labels can collide. Use when every tick must be visible and you'll handle readability with label.angle or chart width instead. |
How "smart" actually works. Vega-Lite has two overlapping
mechanisms here. The literal value labelOverlap: true is a
synonym for "parity" — both drop alternates regardless of scale.
The per-scale adaptive dispatch only kicks in when the field is
omitted entirely. So "smart" instructs Dataface to omit
labelOverlap from the compiled VL spec, which is the only way
to get adaptive behavior. (Authoring the bool true directly is
rejected by Dataface's schema with an error pointing at "smart"
if you want adaptive, or "parity" if you wanted the literal
every-other-label drop. Same for false → "allow".)
Continuous axes (temporal, quantitative)¶
VL's defaults are already correct:
| Scale | VL default labelAngle |
Effective overlap strategy |
|---|---|---|
| Linear quantitative | 0° |
parity (drop every other) |
| Log quantitative | 0° |
greedy (drop overlapping with last visible) |
| Temporal | 0° |
parity |
Continuous labels (numbers, dates) are short and readable
horizontally, and dropping alternates is safe because gaps are
interpolable (May between Apr and Jun). Dataface emits no
overrides — VL's defaults stand.
You can override per-axis if you want a non-default strategy:
charts: monthly_signups: style: axis_x: label: overlap: greedy # force greedy-drop instead of VL's parity default
Continuous bar charts (histograms, time-series bars) follow this same path — they never flip to horizontal orientation, even when crowded. Time runs left-to-right by convention; flipping a monthly-revenue bar chart 90° would be confusing. Crowded continuous bars rely on parity-drop to thin labels.
Discrete axes (nominal, ordinal)¶
VL's default for nominal/ordinal labels is labelAngle: -90° —
labels rotate vertical. This is heavy-handed for short labels
(a 4-category bar with North / South / East / West renders labels
vertical by default), but it's a defensible static default. For the
common painful case — long category labels in a bar chart — the
horizontal-orientation flip (Lane C, below) catches it. For
everything else, set label.angle explicitly.
charts: flat_labels: type: line x: state_code style: axis_x: label: angle: 0 # override VL's -90° default
Themes can set the default angle once for all discrete x-axes:
style: charts: axis_x: label: angle: 0 # this theme renders nominal x labels horizontal everywhere
Horizontal flip (discrete bar charts)¶
When a bar chart has a discrete x-axis and style.orientation: auto
(the default), the engine measures the label widths. If they're too
long to fit horizontally — meaning VL's -90° default would
otherwise make them unreadable — the chart flips:
| Field | 4 short labels | 10 medium labels | 20 long labels |
|---|---|---|---|
North / South / East / West |
vertical (VL's -90°) |
n/a | n/a |
| Product names (avg 12 chars) | vertical | horizontal | horizontal |
| Error messages (avg 40 chars) | horizontal | horizontal | horizontal |
After the flip, category labels live on the y-axis where they render horizontally; the bars extend right.
Bar charts where the author explicitly sets style.orientation:
vertical or : horizontal skip the auto-pick.
This decision only applies to bar charts with a discrete x. Bar charts with a continuous x (histograms, time-series bars) always stay vertical, even when crowded — see Continuous axes above.
The fit check is intentionally simple — one continuous-x guard, one font-measured comparison against the per-label slot width. It does not account for axis-area padding, the y-axis label inset, or per-label gutter; the threshold is a coarse "would labels even come close to fitting" rather than a precise model. If real authoring pain shows up around the borderline cases, the threshold becomes a theme-tunable parameter — see D-019.
Why no fit-based tilt selection?¶
Fit-based smart-tilt — engine measures labels, picks the smallest tilt from a configured menu that makes labels fit — was the original plan to replace Dataface's "posture" system. After triple-checking VL's docs and evaluating the marginal improvement, we punted. Two reasons:
- VL already does the dispatch shape we want — tilt nominal,
drop continuous. It just does both crudely (static
-90°for nominal regardless of fit). Inventing a Dataface-extendedtiltenum value to do "what VL does, but with measurement" was added complexity for a small improvement. - The horizontal-orientation flip catches the painful case — long nominal labels in bar charts, where vertical labels are genuinely unreadable. That's a chart-shape problem, not a tilt problem. We solve it at the right layer.
If fit-based tilt turns out to be real pain that the orientation flip doesn't cover — line/area charts with crowded short nominal labels, dashboards needing the full editorial gradient of tilt angles — we'll revisit. Re-evaluation conditions are in the deferral decision.
Time axes¶
Bucketed time axes (monthly, quarterly, yearly) are detected
automatically and rendered with VL's timeUnit — see
Time Axes for the full grain-detection rules.
Once the time axis is bucketed, it's a temporal scale and falls
through the continuous branch of the decision tree above:
horizontal labels at 0°, parity-drop on overlap. Authors who want
long-form month labels (January / February / ...) that crowd at
typical chart widths should either format them short (Jan / Feb)
or set an explicit label.angle: -45 per chart.
Manual overrides¶
Every smart decision is overridable.
Force a specific orientation (bar charts)¶
charts: always_vertical: type: bar x: state_name style: orientation: vertical # skip auto-pick; force column chart
Force a specific tilt¶
charts: forced_horizontal_labels: type: bar x: state_name style: axis_x: label: angle: 0 # override VL's -90° default
Pick a different overlap strategy¶
charts: drop_alternates: type: line x: many_categories style: axis_x: label: overlap: parity # explicit drop-alternates
Control truncation¶
charts: long_category_names: style: axis_x: label: max_width: 240 # widen the truncation cap (default: 180px, matching Vega-Lite)
How does Vega-Lite handle this natively?¶
Dataface's behavior is mostly Vega-Lite's behavior — the only Dataface-added smart is the horizontal-orientation flip for bar charts. Knowing what VL does by itself explains everything else:
| Field type | VL default labelAngle |
Effective overlap strategy |
|---|---|---|
nominal / ordinal (no timeUnit) |
-90° |
none (no drops) |
temporal / quantitative (non-log) |
0° |
parity |
| Log-scale quantitative | 0° |
greedy |
These defaults are sensible for the most common cases. The one
exception is "4 short nominal labels render vertical by default" —
which is suboptimal but uncommon enough that we don't add machinery
to detect-and-fix. Authors override label.angle directly when it
matters.
See also¶
- Time Axes — grain detection and
timeUnitbehavior - Bar Charts — orientation and grouping
- Styling — full theme/style cascade and per-element styling
- Vega-Lite Axis docs — the underlying behavior we lean on