Time Axes¶
Dataface auto-detects the chronological grain of date columns and renders bucketed-time axes as ordinal (evenly-spaced category bands) by default. Bar, line, and area charts all follow the same rule: the band scale gives bars their full width, aligns line dots with bar centers, and keeps data-table column spacing uniform — without any per-chart-type patches.
You can override the detected grain, use explicit time-part units, disable bucketing entirely, or opt back into a continuous temporal scale for the rare cases that need it.
For how time-axis labels themselves are laid out (when they tilt, when they drop, when a continuous time axis bar chart stays vertical even when crowded), see Axis Labels — Smart Layout.
Auto-detection¶
When x is a date/datetime column on a bar, line, or area chart,
Dataface inspects the distinct values and picks a grain via two coexisting
detection paths: calendar-date predicates (for ISO date/datetime values) and
labeled string patterns (for period key strings). Both paths produce a VL
timeUnit; the column must use one path exclusively — mixed shapes error.
Calendar-date predicate path¶
| Detected grain | Predicate |
|---|---|
year |
All distinct values fall on January 1 |
yearquarter |
All fall on the 1st of Jan / Apr / Jul / Oct |
yearmonth |
All fall on the 1st of any month |
yearweek |
All fall on a Monday (ISO week-start) |
yearmonthdate |
All values are at midnight (daily) |
| (none — continuous) | Any value has nonzero hours/minutes/seconds |
Labeled period-key path¶
Warehouse exports often return period keys as strings rather than dates. Dataface recognizes these stable shapes directly:
| Pattern | Example | Detected grain |
|---|---|---|
YYYY-Www (ISO week-year) |
2024-W32 |
yearweek |
W[eek ]N YYYY |
W32 2024, Week 32 2024 |
yearweek |
YYYY-Qn (canonical quarter) |
2024-Q3 |
yearquarter |
Qn YYYY |
Q3 2024 |
yearquarter |
YYYYQn |
2024Q3 |
yearquarter |
YYYY-MM |
2024-01 |
yearmonth |
Mon YYYY (English month) |
Jan 2024 |
yearmonth |
MM/YYYY (US month/year) |
01/2024 |
yearmonth |
FYnnnn (fiscal year key) |
FY2024 |
year (mapped to Jan 1) |
MM/DD/YYYY (US month/day/year) |
01/15/2024 |
yearmonthdate |
Mon DD[,] YYYY |
Jan 15, 2024 |
yearmonthdate |
Scope: Calendar quarters only (Q1 = Jan–Mar, Q2 = Apr–Jun, Q3 = Jul–Sep,
Q4 = Oct–Dec). Fiscal-quarter offsets are not supported; set
style.axis_x.time_unit explicitly for non-calendar fiscal periods.
FY strings map to the year grain with Jan 1 as anchor — no fiscal calendar
offset is applied.
US-only formats: MM/YYYY and MM/DD/YYYY are parsed as US month/day order.
European day-first ambiguity is not detected; set time_unit explicitly if your
warehouse returns day-first slash-delimited dates.
Not supported: H1 YYYY / YYYY-H1 half-year strings (no Vega-Lite
timeUnit for half-year buckets). These are treated as unparseable; use
time_unit: year explicitly.
Detection produces encoding.x.type: ordinal in the emitted VL spec (the
default since D-002). Each detected bucket becomes an evenly-spaced band on the
x-axis. Bar width, line-dot alignment, and data-table column spacing all follow
band geometry automatically.
Authoring¶
Override the detected grain or disable bucketing via style.axis_x.time_unit:
charts: - id: monthly_revenue type: bar x: month y: revenue style: axis_x: time_unit: yearmonth # explicit override
Valid values:
| Value | Meaning |
|---|---|
auto |
Default — auto-detect from data |
year |
Force year bucketing |
yearquarter |
Force quarter bucketing |
yearmonth |
Force month bucketing |
yearweek |
Force ISO-week bucketing |
yearmonthdate |
Force daily bucketing |
monthofyear |
Extract month-of-year (Jan ... Dec) |
dayofweek |
Extract day-of-week |
dayofmonth |
Extract day-of-month |
dayofyear |
Extract day-of-year |
hourofday |
Extract hour-of-day |
none |
Disable bucketing — continuous temporal scale |
Dataface uses long-form names for time-part units, then maps them to Vega-Lite
primitives at emission time: monthofyear -> month, dayofweek -> day,
dayofmonth -> date, dayofyear -> dayofyear, and hourofday -> hours.
Scale type: ordinal (default) vs temporal (escape hatch)¶
Bucketed-time axes default to ordinal. To opt into a continuous temporal scale
— for irregular sampling where visual gaps must be preserved — set axis_x.type:
style: axis_x: type: temporal # escape hatch: continuous time scale
axis_x.type |
Scale | When to use |
|---|---|---|
auto (default) |
Ordinal for all bucketed grains | The common case: regular calendar cadence |
ordinal |
Ordinal (explicit) | Same as auto; useful to document intent |
temporal |
Continuous temporal | Irregular sampling; visual gaps required |
time_unit: none always produces a continuous temporal scale regardless of
axis_x.type.
Extending the visible range past the data — scale.type + scale.domain¶
axis_x.scale.type: temporal is a second spelling of the same escape hatch,
for authors who reach for the scale block rather than the axis-level type
field. It's required before an authored scale.domain of ISO dates can pin
the visible range past the data extent — a domain only means anything on a
continuous scale:
style: axis_x: scale: type: temporal domain: ["1955-01-01", "2026-01-01"] # pads the axis before/after the data
Authoring scale.domain on a date axis without scale.type: temporal
(or axis_x.type: temporal) raises a compile error rather than silently
collapsing every mark: Vega-Lite reads a 2-element domain on the default
ordinal (band) scale as exactly two category values, not a [low, high]
range, so every mark lands on the domain's first "category."
Gap handling — synthesized rows for missing buckets¶
Ordinal scales only render bands for values that exist in the data. If your query
returns months Jan, Mar, and Apr (February skipped because there were no
events), Vega-Lite silently drops the February slot and the resulting chart
looks like Jan → Mar → Apr with equal spacing — indistinguishable from a
chart where February genuinely follows January.
When the engine detects a bucketed-calendar grain (year, yearquarter,
yearmonth, yearweek, yearmonthdate) on an ordinal x-axis, it
automatically synthesizes rows for every missing bucket between the minimum
and maximum date in the data window, ensuring every slot is rendered.
The fill field on axis_x controls what value is inserted for
synthesized rows:
| Value | Effect |
|---|---|
"null" |
Missing measure columns receive null. Lines and areas break at null. Theme default. Use a quoted string in chart-local YAML to pin this mode; bare fill: null means cascade-inherit. |
zero |
Missing measure columns receive 0. Event-count charts where a missing bucket means zero events. |
linear |
Straight-line values between observed neighbors (interior gaps only). |
step-after |
Looker Step (after): forward-fill from the last observed bucket. |
step-before |
Looker Step (before): each gap takes the next observed bucket’s value. |
step-center |
Looker Step (center): hold left value until the gap midpoint, then the right value. |
curve |
Smooth S-curve (smoothstep) between neighbors — softer than linear, same endpoints. |
linear, step-*, and curve fill interior synthetic buckets only; each color
/ stack series is filled independently.
Scatter exception: style.charts.axis_x.fill is the global default for
bar/line/area. Scatter pins style.charts.scatter.axis_x.fill: null at
cascade layer 3.5 so a global change to zero or linear does not fill gaps on
point charts.
charts: - id: weekly_signups type: line x: week y: signups style: axis_x: fill: "null" # explicit null-fill override (bare null = cascade-inherit; quote to pin) - id: weekly_orders type: bar x: week y: order_count style: axis_x: fill: zero # missing weeks show as zero-height bars - id: monthly_revenue_on_weekly_grid type: area x: week y: revenue style: axis_x: fill: linear # smooth bridge across sparse samples
Multi-series charts: The engine cross-joins every bucket in the window
with every dimension value observed in the data. "Observed" means only
values that appear in the query result — if a new dimension value appears
partway through the window, synthetic rows before its first appearance
carry null/0 measures for that value only.
Escape hatches: Gap-fill is skipped entirely when:
axis_x.type: temporal(continuous scale — VL handles irregular gaps visually)time_unit: none(non-bucketed continuous axis)- A non-bucketed grain is detected (
monthofyear,dayofweek, etc.)
Supported source formats per grain¶
year¶
| Source format | Example |
|---|---|
| ISO date at Jan 1 | 2024-01-01 |
Python date object |
date(2024, 1, 1) |
| Fiscal-year key | FY2024 (mapped to Jan 1) |
Recommended SQL: date_trunc('year', date) AS year (produces ISO dates for
auto-detection). EXTRACT(YEAR FROM date) returns an integer column which is
typed quantitative, not temporal.
yearquarter¶
| Source format | Example |
|---|---|
| ISO date at quarter start | 2024-01-01, 2024-04-01, 2024-07-01, 2024-10-01 |
| Canonical quarter label | 2024-Q1, 2024-Q2, 2024-Q3, 2024-Q4 |
| Quarter-first label | Q1 2024, Q2 2024 |
| Compact quarter label | 2024Q1, 2024Q2 |
Recommended SQL: date_trunc('quarter', date) AS quarter (produces ISO
dates), or CONCAT(YEAR, '-Q', QUARTER) AS quarter for labeled exports.
yearmonth¶
| Source format | Example |
|---|---|
| ISO date at month start | 2024-01-01, 2024-02-01, … |
Python date at month start |
date(2024, 1, 1) |
| Year-month string | 2024-01, 2024-12 |
| English month-name string | Jan 2024, February 2024 (3-letter abbreviation only) |
| US month/year slash | 01/2024, 12/2024 |
Recommended SQL: date_trunc('month', date) AS month
Display: Smart default applies %b %Y → Jan 2024 on the ordinal axis via
axis.formatType: "time". Override with style.axis_x.format.
yearweek¶
| Source format | Example |
|---|---|
| ISO date at week start (Monday) | 2024-01-01, 2024-01-08, … |
| ISO week-year label | 2024-W01, 2024-W32 |
| Spelled week label | W32 2024, Week 32 2024 |
Recommended SQL: date_trunc('week', date) AS week (produces ISO dates), or
CONCAT(ISO_YEAR, '-W', LPAD(ISO_WEEK, 2, '0')) AS week for labeled exports.
Note: ISO week-start is Monday. US Sunday-start weeks are out of scope.
Default label cadence: Monthly (Jan, Feb, …) — shows month openers so
dense weekly axes remain readable. Set label.time_unit: yearweek to restore
per-week labels.
yearmonthdate (daily)¶
| Source format | Example |
|---|---|
| ISO date | 2024-01-15 |
| ISO datetime at midnight | 2024-01-15T00:00:00 |
Python date / datetime (midnight) |
date(2024, 1, 15) |
| US month/day/year slash | 01/15/2024 |
| English day-name string | Jan 15, 2024, Jan 15 2024 |
Recommended SQL: date::DATE or date_trunc('day', date)
Time-Part Units¶
Use time-part units when the question compares a recurring calendar part rather than elapsed time. The x column should still be a real date or timestamp; Vega- Lite extracts the requested part. Time-part units remain on a temporal scale (they represent cyclic comparison, not ordered buckets).
charts: - id: revenue_by_month_of_year type: bar x: order_date y: revenue style: axis_x: time_unit: monthofyear format: "%b" - id: orders_by_day_of_week type: bar x: order_ts y: order_count style: axis_x: time_unit: dayofweek format: "%a"
time_unit: auto does not infer time-part intent from field names. If you want
seasonality, day-of-week, or hour-of-day behavior, author the time part
explicitly.
Failures¶
Dataface fails loud (raises an error) when:
- ≥10% of distinct values are unparseable as dates or recognized label patterns. Fix the query or cast the column.
- A column contains mixed shapes — e.g. some values are
2024-W32(week labels) and others are2024-01-15(ISO dates), or week labels alongside quarter labels. Setstyle.axis_x.time_unitexplicitly for ambiguous columns.
Use style.axis_x.time_unit: none for raw event data with sub-daily
timestamps — detection returns continuous temporal automatically, but the
explicit override documents intent.
Axis labels¶
style.axis_x.time_unit controls the data bucket sent to Vega-Lite. Use
style.axis_x.label.time_unit only when the label cadence should differ from
the encoded bucket.
style: axis_x: time_unit: yearmonth label: time_unit: yearquarter
When label.time_unit is omitted, it inherits the axis time unit except for
dense sub-month cadences:
| Encoding time unit | Default label cadence | Rationale |
|---|---|---|
yearweek |
yearmonth |
Weekly charts read Jan/Feb/Mar, not W01/W02/W03 |
yearmonthdate |
yearmonth |
Daily data defaults to monthly ticks; 400 daily points would otherwise produce a solid grey band |
Set label.time_unit: yearweek or label.time_unit: yearmonthdate explicitly
to restore per-week or per-day labels on short domains. Set label.time_unit:
none to disable Dataface's smart label expression and let Vega-Lite format the
axis.
For simple formatting, use style.axis_x.format:
style: axis_x: time_unit: yearmonth format: "%b %Y"
For label-density tuning — when monthly labels feel too cramped even without bounding-box overlap — see Label density.