Skip to content

fix: use utcFormat for temporal axis label comparisons#161

Merged
cpsievert merged 2 commits intoposit-dev:mainfrom
cpsievert:fix/utc-format-temporal-labels
Mar 2, 2026
Merged

fix: use utcFormat for temporal axis label comparisons#161
cpsievert merged 2 commits intoposit-dev:mainfrom
cpsievert:fix/utc-format-temporal-labels

Conversation

@cpsievert
Copy link
Collaborator

Problem

Date axis labels show nonsensical time-of-day values like "06 PM" / "07 PM" instead of formatted dates when using RENAMING on temporal scales (e.g., SCALE x VIA date RENAMING * => '{:time %b %Y}').

The x-axis should show month/year labels like "Jan 2024", "Feb 2024", etc. — instead it shows "06 PM" / "07 PM" alternating (the alternation is caused by DST shifts).

Root Cause

The Vega-Lite labelExpr generated for temporal scales used timeFormat(datum.value, '%Y-%m-%d') to compare axis tick values against ISO date keys from the RENAMING clause. The problem is that timeFormat formats timestamps in the browser's local timezone, while ggsql writes all temporal data as UTC ISO strings.

In any non-UTC timezone, every comparison silently fails:

Step UTC (correct) US Central (actual)
Data value "2024-01-01" → midnight UTC same
timeFormat(datum.value, '%Y-%m-%d') "2024-01-01" "2023-12-31" (6 PM local = previous day)
Comparison vs key "2024-01-01" ✅ match ❌ no match

When all comparisons fail, the labelExpr falls through to datum.label (Vega-Lite's default temporal formatter). Since the tick values are midnight UTC — which translates to ~6 PM local time — Vega-Lite auto-formats them as time-of-day labels. The "06 PM" / "07 PM" alternation comes from DST: winter months are UTC-6, summer months are UTC-5.

Fix

One-line change: timeFormatutcFormat in build_label_expr(). The utcFormat function formats in UTC, matching the ISO date keys that ggsql generates.

Test Plan

  • Added 4 unit tests for build_label_expr covering temporal (utcFormat), non-temporal (datum.label), empty mappings (fallback), and null suppression
  • All 944 existing tests continue to pass

The Vega-Lite labelExpr for temporal scales (date, datetime, time) was
using timeFormat() to compare axis tick values against ISO date keys
from the RENAMING clause. timeFormat formats in the browser's local
timezone, but ggsql writes all temporal data as UTC ISO strings (e.g.,
"2024-01-01" represents midnight UTC).

In non-UTC timezones, this causes every label comparison to fail. For
example, in US Central Time (UTC-6), timeFormat formats "2024-01-01"
(midnight UTC) as "2023-12-31" (6 PM local), so the comparison against
the key "2024-01-01" never matches. The labelExpr falls through to
datum.label (Vega-Lite's default formatter), which shows time-of-day
labels like "06 PM" / "07 PM" instead of the expected date labels.

The fix is to use utcFormat(), which formats in UTC and matches the
ISO date keys that ggsql generates.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@cpsievert cpsievert force-pushed the fix/utc-format-temporal-labels branch from aea16b6 to e994810 Compare February 27, 2026 20:29
Copy link
Collaborator

@thomasp85 thomasp85 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perfect - thanks

@thomasp85
Copy link
Collaborator

once you reformat I'm happy to merge

@cpsievert cpsievert merged commit d684fa3 into posit-dev:main Mar 2, 2026
4 checks passed
@cpsievert cpsievert deleted the fix/utc-format-temporal-labels branch March 2, 2026 16:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants