Skip to content

feat(profile-dist): datetime cell — classifier consolidation + intra-day tooltips (DRC-3669 + DRC-3670)#1451

Merged
danyelf merged 3 commits into
mainfrom
explore/drc-3670-intraday-tooltips
Jul 2, 2026
Merged

feat(profile-dist): datetime cell — classifier consolidation + intra-day tooltips (DRC-3669 + DRC-3670)#1451
danyelf merged 3 commits into
mainfrom
explore/drc-3670-intraday-tooltips

Conversation

@danyelf

@danyelf danyelf commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

What this PR does

Fixes two small, related problems with the datetime column display in the profile-distribution cell (the inline mini-histogram you see for a column). They're bundled together because they touch the same file. Both came out of the review of PR #1381/#1411 and were picked up during the June backlog-cleanup pass (Linear DRC-3669 and DRC-3670).


Part A — One source of truth for "is this column a date/time?" (DRC-3669)

The problem. The histogram cell had its own little helper functions for guessing whether a column is a date, a time, or a timestamp — separate from the app's main type classifier (classifyType). Two copies of the same logic is a maintenance trap: they drift apart, and a fix in one doesn't reach the other.

Why two copies existed. The main classifier didn't recognize DuckDB's sub-second timestamp types (TIMESTAMP_S, TIMESTAMP_MS, TIMESTAMP_NS) — it returned "unknown" for them. To work around that, someone gave the cell its own substring-based guesser. So the duplication was a symptom of a gap in the real classifier.

The fix. Close the gap, then delete the duplicate:

  • Teach classifyType about the three DuckDB timestamp types so they classify as datetime (this also fixes the type icon for those columns as a bonus).
  • Delete the cell's private helpers and have it ask classifyType instead — one classifier for the whole app.
  • One important guard: classifyType assumes it's handed a real string, but the cell's column type can be undefined while a cell is still loading. So the call is guarded (columnType ? classifyType(columnType) : "unknown") — without it, the cell would crash.

Part B — Timestamp tooltips that actually tell you something (DRC-3670)

The problem. Histogram tooltips for timestamp columns only showed the date, never the time. For data that all lands on a single day — which is normal for event logs loaded in one batch — every bar's tooltip read the same useless string: "Jun 5, 2026 – Jun 5, 2026."

The fix. Show the time of day in the tooltip, but only when it helps. The formatter looks at how wide the histogram's overall time span is and adapts:

  • spans a day or more → just the date (unchanged — keeps multi-day charts clean)
  • spans minutes-to-hours → date + HH:mm
  • spans under a minute → date + HH:mm:ss

Times stay in UTC throughout (the backend emits UTC seconds; rendering in local time would shove day-boundary values onto the wrong calendar day).

The two parts meet cleanly: once Part A routes datetime columns through the shared classifier, that path feeds Part B's span-aware formatter.


Files changed

  • classifyType.ts — add TIMESTAMP_S/_MS/_NS → datetime.
  • InlineProfileDistributionCell.tsx — drop the two local helpers; route through classifyType (guarded); build the span-aware time formatter.
  • formatTime.tsformatEpochSeconds gains an optional includeTime flag (UTC preserved).
  • Tests in classifyType.test.ts, InlineProfileDistributionCell.test.tsx, formatTime.test.ts.

How to see it

PairedHistogramDatetime.stories.tsx (Storybook) renders the tooltip at three scales — seconds, minutes, days — so you can eyeball the labels across spans.

Tests

Full js/ suite green on push: 1939 passed, 5 skipped. tsc --noEmit clean, Biome lint clean.

Known limitation (separate decision, not fixed here)

The paired histogram is percentile-based: each environment is binned on its own quantile edges (intentional — DRC-3389 / PR #1398 — because the backend returns approximate quantiles, not bin counts, so base and current genuinely can't share bin edges without giving up the fast approximate-percentile path). The two staircases share a value axis but not edge positions, so where a base edge and a current edge sit close together you get a thin "filler" segment whose two ends round to the same label (e.g. 14:30–14:30). The new precise time labels just make this pre-existing behavior visible. Suppressing those sub-precision segments (cosmetic) is a separate call — flagging, not fixing, here.

Checklist

  • Tests pass (pnpm test, pnpm type:check, pnpm lint)
  • DCO sign-off

🤖 Generated with Claude Code

danyelf and others added 2 commits June 30, 2026 00:49
formatEpochSeconds rendered DATE-ONLY (UTC), so a paired histogram over an
intra-day timestamp column produced information-free tooltips like
"Jun 5, 2026 - Jun 5, 2026". Add an optional `includeTime` param appending a
UTC HH:mm (true) or HH:mm:ss ("seconds") clock time, and have the cell's
datetime branch pick the precision from the bin-edge span:
  - span >= 1 day  -> date only (unchanged)
  - span <  1 day  -> date + HH:mm
  - span <  1 min  -> date + HH:mm:ss

Histogram components are untouched: the cell still passes a (v)=>string
formatValue closure. Flips the formatTime.test collapse assertion to
intra-day-distinctness + multi-day-day-only cases, and adds a
PairedHistogramDatetime story with seconds / minutes / days scales (each
surfaces the real bars' tooltips for visual review).

Draft exploration branch for DRC-3670 visual review.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Danyel Fisher <danyel@gmail.com>
… (DRC-3669)

Part A of the DRC-3669/3670 datetime-cell work. Replaces the two local
substring heuristics (isDatetimeType/isTimeOfDayType) in
InlineProfileDistributionCell with the canonical classifyType classifier,
so the cell and the DataTypeIcon glyph share one source of truth.

- Add DuckDB TIMESTAMP_S/_MS/_NS to CATEGORY_MAP (previously fell through
  to 'unknown', the gap that forced the local substring helpers).
- Route the histogram branch via classifyType with a null-guard
  (classifyType has no null-guard but columnType is optional):
  time -> formatTimeOfDay; date|datetime -> the span-aware makeEpochFormatter
  (DRC-3670 intra-day path); else -> no special formatting.
- Tests: TIMESTAMP_S/_MS/_NS -> datetime; TIME/TIMETZ stay time.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Danyel Fisher <danyel@gmail.com>
@danyelf

danyelf commented Jun 30, 2026

Copy link
Copy Markdown
Contributor Author

Code Review: PR #1451 — DRC-3669 + DRC-3670

SHA 81bca22 · Verdict: GO-WITH-NITS · self-review

Targeted suite green from js/: formatTime.test.ts + classifyType.test.ts + InlineProfileDistributionCell.test.tsx139 passed, 0 failed. The classifier consolidation (DRC-3669) is clean and matches its spec (drc-3669-usage-and-edge-cases.md): undefined-guard present, array narrowing analyzed as unreachable, DuckDB TIMESTAMP_S/_MS/_NS gap closed. No blockers. Two should-fix items below, both on the DRC-3670 side.

Should-fix

  1. Tooltip precision is keyed off total span, not bin/segment granularity — so adjacent edges still collapse for a band of inputs, the exact failure DRC-3670 fixes, one scale down.
    makeEpochFormatter (InlineProfileDistributionCell.tsx:152-155) picks precision from Math.max(edges) - Math.min(edges). But the tooltip renders a range per merged segment: ${fmt(lo)}–${fmt(hi)} over consecutive pairs of the base∪current edge union (PairedHistogramContinuous.tsx:316-331, :365). Minute precision is used for any 60s ≤ span < 1 day; whenever a segment is narrower than 60s, both ends round to the same HH:mm → an identical "14:30–14:30" range. This is not only the deferred skinny-segment artifact — it hits normal quantile bins too whenever span / binCount < 60s (≈ spans up to 11–50 min for 11–50 bins).
    Evidence in the PR's own showcase: the Minutes fixture (fixtures.ts, datetimeEdges(120, 25)) interleaves base and current edges 25s apart, so merged segments alternate 25s / 95s; the 25s segments render "…14:30 – …14:30". The play assertion new Set(titles).size > 1 (PairedHistogramDatetime.stories.tsx:155) still passes because other segments differ, so the collapse ships unflagged inside the very story meant to demonstrate distinctness. Same mechanism applies to the seconds scale when span < binCount seconds.
    Fix: derive precision from the minimum adjacent merged-edge gap (or span / binCount), not the total span. Acceptable to ship with a follow-up since this is tooltip fidelity, not data correctness — but the "distinct intra-day edges" claim is only partially delivered as written.

  2. The new core selection logic (makeEpochFormatter intra-day branches) is not covered by the CI-gating suite.
    pnpm test runs root vitest, which excludes packages/storybook/** (vitest.config.mts:317), and CI runs only pnpm test (tests-js.yaml:87) — no job runs storybook's separate browser vitest. So the only executable assertions on the HH:mm / HH:mm:ss selection, the empty-edges fallback (:151), and the base+current edge combination live in storybook play functions that never gate a merge. The jsdom cell test covers only the span ≥ 1 day (date-only) branch (InlineProfileDistributionCell.test.tsx:191-215, 86400s steps). It already has the render-cell + read-<title> harness; mirror it with an intra-day payload asserting HH:mm appears (and a sub-minute one for HH:mm:ss). formatEpochSeconds(v, true|'seconds') itself is unit-tested in formatTime.test.ts — it's the span→precision selection that isn't.

Notes

  1. classifyType narrowing leaves a ClickHouse-shaped gap analogous to the DuckDB one just fixed. Exact-match CATEGORY_MAP returns unknown for datetime spellings the deleted substring heuristic caught — e.g. ClickHouse DATE32 / DATETIME64(3) (old isDatetimeType matched substring date/datetime → epoch; now unknown → no formatting, raw epoch integers in the tooltip). Arrays (TIMESTAMP[]) also narrow but are unreachable (don't emit continuous histograms — spec Note A); ClickHouse datetime columns do. Community adapter, low severity, but it's the same class of gap as the TIMESTAMP_S/_MS/_NS keys this PR adds — a couple of map entries close it.

  2. Flipped test is fine, not tautological-in-the-bad-way. formatTime.test.ts:58 only renamed/recommented; the endOfDay === startOfDay assertion is unchanged (date-only obviously ignores time). The real UTC invariant did not get lost — it's now locked by the new toContain("23:00") AC6 test (:94) and the new distinct-HH:mm/HH:mm:ss cases. No action.

What I verified clean

  • Undefined-guard columnType ? classifyType(columnType) : "unknown" fully covers the optional prop (and empty-string → unknown); TS prevents non-string callers. .filter(Number.isFinite) correctly drops NaN/Infinity edges (extra index/array args are harmless to Number.isFinite). UTC integrity intact: the appended time uses getUTCHours/Minutes/Seconds (formatTime.ts:152-156), no local-tz Date path. Combined base+current span (:148) is the right choice for a shared formatter across the two paired axes — a wider multi-day side correctly collapses an intra-day side to date-only.

… gap; ClickHouse types; vitest coverage (DRC-3670)

Addresses PR #1451 self-review findings:

- should-fix #1: makeEpochFormatter now derives time precision from the
  minimum gap between adjacent merged (base∪current, sorted, de-duped) edges
  instead of the total span, so individual narrow segments no longer collapse
  to identical labels (e.g. 14:30-14:30). Empty/single-edge and NaN-edge cases
  still no-op gracefully via the existing Number.isFinite filter.

- should-fix #2: add real vitest coverage (jsdom render + read bar <title>s)
  for the intra-day HH:mm branch, sub-minute HH:mm:ss branch, the min-gap
  distinctness behavior, the multi-day date-only branch, and the empty-edges
  fallback. These run under 'pnpm test' (root vitest), not just Storybook.

- review note #3: classifyType CATEGORY_MAP gains ClickHouse DATE32 -> date
  and DATETIME64 -> datetime (incl. DATETIME64(3, 'UTC') via paren-strip),
  with tests.

Also DRYs the DRC-3670 Storybook datetime fixtures/stories onto a single
makeContinuousDatetimeFixture factory and a single makeDatetimeStory factory,
and updates them to demonstrate the new min-gap precision behavior.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Danyel Fisher <danyel@gmail.com>
@gcko gcko self-requested a review July 2, 2026 00:53

@gcko gcko left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

  • LOW: Sub-second timestamp bins can still collapse to identical tooltip labels even though this PR now classifies DuckDB TIMESTAMP_MS and TIMESTAMP_NS as datetime types. The formatter selects only HH:mm:ss when the merged edge gap is below 60 seconds, and formatEpochSeconds has no millisecond or fractional-second branch, so edges such as 14:30:00.100 and 14:30:00.900 both render as 14:30:00. This is a tooltip precision gap, not data corruption, but it leaves a visible edge case in the newly supported sub-second timestamp path; the new tests cover 10-second gaps but not fractional-second gaps. Citations: js/packages/ui/src/components/ui/DataTypeIcon/classifyType.ts:82, js/packages/ui/src/components/data/InlineProfileDistributionCell.tsx:167, js/packages/ui/src/utils/formatTime.ts:161, js/packages/ui/src/components/data/__tests__/InlineProfileDistributionCell.test.tsx:326.

@danyelf danyelf merged commit 7eca12a into main Jul 2, 2026
8 checks passed
@danyelf danyelf deleted the explore/drc-3670-intraday-tooltips branch July 2, 2026 20:22
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