feat(analytics): order the time axis by default, give reports a sort declaration (#3916) - #3921
Merged
Merged
Conversation
…declaration (#3916) A matrix report with a date dimension across rendered its columns in arbitrary order. Declaring `dateGranularity` made the bucket keys sortable without making anything sort them, and nothing in the chain supplied an order: `resolveOrdering` returned undefined unless the selection carried one, the ObjectQL aggregate path has no ordering grammar so buckets came back in Map-insertion order, and the pivot builds its column headers in row-arrival order. The report author could not ask either — `DatasetSelection.order` existed on the wire but `ReportSchema` had no ordering field at all. Server default: when a selection states no `order` (and no `limit`, whose own fallback already ordered by every dimension), each selected dimension the cube types as `time` defaults to ascending, in selection order. Bucket keys are minted sort-stable precisely so this works. Lands on both strategy paths — a real ORDER BY where native SQL serves the query, the executor's post-pass where a bucketed query goes to the ObjectQL path. Deliberately narrow: only time dimensions get a default, so grids with nothing wrong with them are not reordered. Author surface: `ReportSchema.order` / `blocks[].order` is a list of `{ by, direction }` keys, most significant first — an array because key order is the contract. `by` must name a `rows`/`columns` dimension or a `values` measure the report selects; anything else, or a duplicate, fails at authoring time. A `joined` report orders per block. `reportSelectionOrder()` lowers the list into `DatasetSelection.order`, returning undefined for an empty list so the runtime defaults still apply. Ships as `planned` + authorWarn in the liveness ledger: the framework half is complete and live, but objectui's DatasetReportRenderer does not yet carry `report.order` into the selection it posts. The time-axis default needs no renderer change and is live now. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CYJ5WSiCgd522s1zCWtmFT
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
Contributor
📓 Docs Drift CheckThis PR changes 3 package(s): 107 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
…` field drifted `check:i18n` failed on the PR: the `order` field added to `reportForm` carries a label + helpText, and the platform-objects metadata-form bundles are GENERATED from the form declarations, so all four locales drifted from a fresh extract. Regenerated via `scripts/check-i18n-bundles.mjs --write`; the diff is exactly the one new `report.order` entry per locale, nothing else. The zh-CN / ja-JP / es-ES entries are hand-translated rather than left as the merge-mode English filler. Not cosmetic: platform-objects sits at 0 in scripts/i18n-coverage-baseline.json, so `check:i18n-coverage` is already the strict gate for this package and a new untranslated declared label would have turned the next run red. Verified both ways — `check:i18n` reports 8 bundles in sync (merge mode preserves the hand translations), and `os lint --json` reports 0 i18n issues for the config. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CYJ5WSiCgd522s1zCWtmFT
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #3916.
The problem
A matrix report with a date dimension across rendered its columns in arbitrary order (
2026-07-01, 2026-07-05, …, 2026-07-02). DeclaringdateGranularitymade the bucket keys sortable (2026-07,2026-Q3) without making anything sort them.Nothing in the chain supplied an order:
resolveOrderingreturnedundefinedunless the selection carriedorderexplicitly (dataset-executor.ts:318-344).in-memory-aggregation.ts:84), and native SQL declines every granularity query (native-sql-strategy.ts:30) — so bucketed queries always land there.And the author could not ask:
DatasetSelection.orderexisted on the wire, butReportSchemahad no ordering field at all. Dashboard widgets had their ownoptions.sortBychannel; reports had nothing.One correction to the issue's diagnosis:
ObjectQLStrategydoes renderORDER BYin its echoed SQL, but that string is documentation — execution goes throughengine.aggregate(), which never receives an ordering. The practical gap is exactly as reported.The fix
Server default — the symptom fix. When a selection states no
order(and nolimit, whose existing fallback already ordered by every dimension), each selected dimension the cube types astimenow defaults to ascending, in selection order. Bucket keys are minted sort-stable (2026-07,2026-Q3,2026-W31) precisely so ascending is chronological. This lands on both strategy paths: a realORDER BYwhere native SQL serves the query, the executor's post-pass where a bucketed query goes to the ObjectQL path. Null / empty buckets stay last, as everywhere else.Deliberately narrow — only time dimensions get a default, so grids with nothing wrong with them are not reordered. For the reported matrix (
rows: [owner],columns: [closed_month]) that makes the column dimension the sole sort key, so the pivot's arrival-order headers come out strictly chronological. Sorting the row dimension too would have put one owner's months first and appended everyone else's out of order.Author surface.
ReportSchema.order/blocks[].order— a list of{ by, direction }sort keys, most significant first. An array rather than aRecordbecause key order is the contract and JSON object key order should not have to be.bymust name a dimension the report groups by (rows/columns) or a measure it displays (values); anything unknown, or a duplicate, fails at authoring time rather than becoming an ordering that silently does nothing. Ajoinedreport orders per block — declaringorderon the container is an error.reportSelectionOrder()lowers the list into theDatasetSelection.ordera renderer posts, returningundefinedfor an empty list so the runtime's own defaults still apply.An explicit
orderstill wins outright — the chronological default is a default, not a policy, so "newest month first" is one declaration away.Liveness classification — please read
report.orderships asplanned+authorWarn, notlive. The framework half is complete and live (schema,reportSelectionOrder, executor), but objectui'sDatasetReportRendererbuilds the selection it posts and does not yet carryreport.orderinto it. Marking itlivewould be the exact failure the gate exists to catch. This needs a follow-up objectui PR to flip.The time-axis default needs no renderer change and is live now — it is what actually closes the reported bug.
Out of scope here
The console pivot (
packages/console) ships as a prebuilt bundle with no source in this repo, and the report→selection lowering lives in objectui. Neither is reachable from this change.Also
Corrected
docs/audits/2026-06-reportschema-property-liveness.md:21— the note claiming the retiredsortOrder"now lives on the dataset" was wrong, as the issue observed. No dataset-level sort field ever existed.Testing
dataset-time-axis-order.test.ts) pinning the default and its boundaries: time-only, selection order, explicit-order precedence, the bare-limit fallback, native-SQLORDER BYemission, quarter/year keys, nulls last.order, its validation, andreportSelectionOrder.json-schema.manifest.json,authorable-surface.json,api-surface.json,references/ui/report.mdx);check:liveness,check:spec-changes,check:upgrade-guide,check:skill-docs,check:skill-refs,check:skill-examples,check:docs,check:api-surface,check:react-blocks,check:doc-authoringall pass.🤖 Generated with Claude Code
https://claude.ai/code/session_01CYJ5WSiCgd522s1zCWtmFT
Generated by Claude Code