Skip to content

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: These patterns detect calendar quarters (Q1 = Jan–Mar, Q2 = Apr–Jun, Q3 = Jul–Sep, Q4 = Oct–Dec) and always anchor FY strings' year grain at Jan 1. To author a fiscal year/quarter that starts in a different month, set style.axis_x.fiscal_year_start_month — see Fiscal-quarter offset below.

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.

Year-shape path (bare year INTEGER or YYYY string)

dbt marts routinely store the year as a plain INTEGER (launch_year) or a display VARCHAR ("2024") rather than a date. Dataface detects a year-shaped x column — every non-null value is an integer in 1900–2100, or a string matching ^\d{4}$ in that range — and treats it as the year grain, exactly as if you had cast it to a DATE. No make_date(...) cast and no axis_x.time_unit are needed:

  • On a bar chart the years render as evenly-spaced vertical bands with %Y ("2024") tick labels — no ~s SI suffix ("2.02k") and no auto-flip to horizontal that a plain VARCHAR category would trigger.
  • On a line/area chart they render on a continuous temporal scale (see the scale-type section below).

The detection is value-only — the column name is never consulted, so a revenue_bucket column holding 2010, 2011, … is treated the same as one named year. Non-year integers (counts, ids, ratings) stay quantitative. An authored axis_x.type or axis_x.scale.type always overrides the inference.

Emitted scale type

Each detected bucket becomes an evenly-spaced band on an ordinal x-axis for bar/column charts; line/area charts emit a continuous temporal scale (a trend line reads over real time, and has no per-bucket gridline problem). Bar width, line-dot alignment, and data-table column spacing all follow band geometry automatically. Either default is overridden by an authored axis_x.type.

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.

Fiscal-quarter offset

year and yearquarter bucketing default to the calendar convention (year starts January, Q1 = Jan–Mar). Businesses whose fiscal year starts in a different month set style.axis_x.fiscal_year_start_month (1=Jan..12=Dec):

charts:
  - id: fiscal_quarterly_revenue
    type: bar
    x: quarter
    y: revenue
    style:
      axis_x:
        time_unit: yearquarter
        fiscal_year_start_month: 4   # fiscal Q1 = Apr-Jun

The offset shifts bucket boundaries, Q1..Q4 numbering, and the axis label's year-boundary line break together — there is no separate "fiscal" mode to keep in sync. It applies on both ordinal (bar, or line/area with curve: step-band) and continuous-temporal (line/area) scales: at a non-default offset, Dataface always buckets fiscally in Python rather than handing the field to Vega-Lite's native timeUnit, since Vega-Lite's own timeUnit transform has no fiscal-offset concept and would silently re-bucket to calendar-aligned boundaries.

Scale type: default depends on mark

For a bucketed-calendar grain the default scale type depends on the mark: bar/column default to ordinal (evenly-spaced bands, one per bucket); line/area default to temporal (a continuous time scale so a trend reads over real calendar time and irregular gaps are preserved). Override either with axis_x.type:

style:
  axis_x:
    type: temporal   # force a continuous time scale (e.g. on a bar chart)
axis_x.type Scale When to use
auto (default) Ordinal for bar/column; temporal for line/area The common case
ordinal Ordinal (explicit) Force bands on a line/area, or document bar intent
temporal Continuous temporal Force a time scale on a bar; irregular sampling

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."

Tick cadence: step-anchored interval + step

style.axis_x.ticks.count (see Axis Labels) asks Vega-Lite for a target number of ticks — a plausible-sounding integer that VL treats as advisory, and that has no relationship to your domain's actual calendar width. Reaching a clean "every 5 years" cadence with count means guessing which integer happens to produce 5-year spacing on your particular date range.

style.axis_x.ticks.interval (with an optional step) names the cadence directly instead: "ticks at every 5 years, anchored on multiples of 5" — the same way an author says "quarterly" or "every other month".

style:
  axis_x:
    ticks:
      interval: year
      step: 5   # every 5 years — 1900, 1905, 1910, ...

interval alone (no step) is equivalent to step: 1 — one tick per unit. This is a thin passthrough to Vega-Lite's native axis.tickCount: {interval, step}; d3 (VL's tick engine) anchors ticks on multiples of step, including odd steps, with no date materialization on Dataface's side.

Constraints:

  • interval accepts year, quarter, or month only — the units where d3's anchoring is provably clean. Day/week cadences give irregular spacing (d3's own documented caveat) and are out of scope.
  • Continuous temporal axis_x only. The axis must resolve to a continuous temporal scale (see Scale type above) — an ordinal bucketed axis (e.g. a low-density monthly bar chart) has no VL tick-count concept to pass this through to, and raises rather than silently doing nothing.
  • step requires interval. step alone doesn't name a unit, so it raises at compile time.
  • Mutually exclusive with ticks.count. They are two different ways to express tick cadence; author one or the other, not both.
  • axis_y is rejected. The measure axis is never temporal in Dataface's cartesian model.
  • No anchor override. Ticks always anchor on multiples of step (d3's own default) — there is no field to shift the anchor off that grid.
  • Endpoint control belongs to label.values (see Axis Labels) — this surface only controls tick/grid density, not which labels render.
queries:
  index_series:
    columns: [year, index]
    values:
      - ["2006-01-01", 245.0]
      - ["2007-01-01", 251.0]
      - ["2008-01-01", 260.4]
      - ["2009-01-01", 265.9]
      - ["2010-01-01", 272.0]
      - ["2011-01-01", 279.0]
      - ["2012-01-01", 282.2]
      - ["2013-01-01", 288.6]
      - ["2014-01-01", 296.4]
      - ["2015-01-01", 306.1]
      - ["2016-01-01", 308.7]
      - ["2017-01-01", 313.5]
      - ["2018-01-01", 316.1]
      - ["2019-01-01", 326.6]
      - ["2020-01-01", 336.2]
      - ["2021-01-01", 338.3]
      - ["2022-01-01", 351.6]
      - ["2023-01-01", 365.3]
      - ["2024-01-01", 375.5]
      - ["2025-01-01", 385.4]

charts:
  index_line:
    query: index_series
    type: line
    title: Index
    x: year
    y: index
    style:
      axis_x:
        ticks:
          interval: year
          step: 5
rows:
  - index_line
2010201520202025250300350Index Data as of 16:49 UTC on 5 Aug 2026 made with dataface

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 %YJan 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: Measured. On a bucket-aligned column chart with room for them, every week is labeled with the day of month it starts on. The three-letter month appears on a second row under the first week of each month:

 6  13  20  27   3  10  17  24   3  10  17  24  31
Jan'25          Feb             Mar

The first visible label always carries the year. On continuous line and area axes, Vega's weekly scale positions ticks on Sunday; Dataface labels each tick with the represented ISO Monday bucket without moving the tick. When the day numbers no longer fit, labels promote to month text and default ticks follow the month openers. Further thinning can show fewer month labels without removing ticks. The weekly encoding grain remains unchanged throughout.

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)

Default label cadence: A short bucket-aligned column chart labels every day with its day of month and puts month context on a second row. As the labels stop fitting, the cadence steps first to Mondays—still shown as day numbers—and then to month text. Default ticks follow a format promotion, but later visibility-only thinning leaves them alone.

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 are 2024-01-15 (ISO dates), or week labels alongside quarter labels. Set style.axis_x.time_unit explicitly 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. style.axis_x.label.time_unit controls label vocabulary when authored. Automatic label vocabulary can adapt to the domain and chart width; the encoding time unit never changes.

style:
  axis_x:
    time_unit: yearmonth
    label:
      time_unit: yearquarter

When label.time_unit is omitted, Dataface measures bucket-aligned daily and weekly labels against the available width. Daily labels try every day, then Mondays, then month text. Weekly labels try every week, then month text. The encoding grain and chart data remain unchanged, while default ticks follow a promoted display grain at the first source bucket that opens each period. Continuous line and area axes remain continuous; weekly ticks still use the compact Monday-bucket label described above. An authored label time unit pins its vocabulary regardless of width.

An authored label.time_unit: yearquarter uses Q1Q4, including fiscal quarter numbering, and makes quarter boundaries the default tick cadence. On a continuous temporal axis, an authored axis_x.ticks.count or axis_x.ticks.interval overrides that default.

Label density after display-grain selection is a separate render-time decision. When labels do not fit, Dataface skips labels while preserving the resolved vocabulary. Monthly labels therefore thin to fiscal-quarter openers: Jan/Apr/Jul/Oct for a January fiscal year, or Mar/Jun/Sep/Dec for a March fiscal year. Width-driven visibility thinning does not change ticks or gridlines.

If the thinned labels still do not fit, Dataface tries the configured label.tilt_increments. The order is always skip, then tilt, and the existing label.overlap.skip and label.overlap.tilt switches control those steps. The fiscal opener is the phase anchor. The first visible label always includes the year, even when that label is not itself a fiscal-year opener.

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.