Skip to content

fix(#111): cap chart X categories, not raw rows; honest transform note#274

Merged
BorisTyshkevich merged 1 commit into
mainfrom
fix/chart-category-cap-111
Jul 17, 2026
Merged

fix(#111): cap chart X categories, not raw rows; honest transform note#274
BorisTyshkevich merged 1 commit into
mainfrom
fix/chart-category-cap-111

Conversation

@BorisTyshkevich

Copy link
Copy Markdown
Collaborator

What & why

Fixes #111 (follow-up to #109). buildChartData() applied the chart-type
row cap to the raw rows before grouping, so the chart could disagree with
the result already in memory: a category was short-totaled when another row for
the same cell appeared after the cap, and a category could vanish only because
its first row appeared after the cap. The old note also said first N of M rows
while the chart displays categories, and — because the note lived inside the
controls block — Dashboard and read-only/detached tiles (controls:false)
capped categories silently.

Fix

  • Cap X categories, not raw rows. Two-pass buildChartData(): pass 1
    discovers unique raw X keys across all fetched rows in first-seen order and
    retains the first chartRowCap(type); pass 2 aggregates every fetched row
    whose X is a retained category. No value-ranking, sorting, or Other bucket.
  • Typed metadata. ChartDataResult.meta: ChartDataMeta
    (totalRows/totalCategories/shownCategories/categoriesTruncated/
    duplicateCellsSummed/groupKey).
  • Honest note. New pure chartDataNote(meta)
    first N of M categories and/or duplicate X[/series] rows summed in the browser. It describes only the browser-side transform, never whether the SQL
    pre-aggregated. Duplicate detection keys on X (or (X, series)), so a normal
    multi-series result with repeated X values is not reported as summing, and
    a duplicate confined to an omitted category never flags the visible chart.
  • Every surface discloses. Note renders inline in the config bar on the
    workbench, and as a standalone block above the canvas on controls:false
    surfaces (Dashboard / read-only / detached).
  • Aggregated once per renderchartJsConfig accepts the precomputed
    ChartDataResult via a new data option.

No panels.ts change was needed (the note now renders regardless of controls);
no new runtime dependency and no new network request.

Tests

  • chart-data.test.ts: late-row contribution, first-seen order, post-cap
    omission, all metadata states (truncation-only, duplicate-only, both,
    unique multi-series, duplicate (X, series), duplicate in an omitted
    category, series only in omitted categories), chartDataNote formatter, and
    chartJsConfig honoring precomputed data.
  • chart-render.test.ts: category-cap note text, type-switch recompute,
    duplicate-X / duplicate-X-series / combined text, standalone note on
    controls:false, no-note when neither condition holds, and single
    aggregation per render.
  • panels.test.ts: a Dashboard chart panel discloses the cap despite hidden
    controls.

npm test (2891 tests, per-file 100/100/100/100 gate) and npm run build pass.

Closes #111.

Checklist

  • npm test passes (the per-file coverage gate is non-negotiable)
  • Tests added/updated in the same change as the code
  • npm run build succeeds (single-file dist/sql.html)
  • Layers kept honest: pure logic in src/core/, network in src/net/ (injected fetch), DOM in src/ui/
  • No new runtime dependency (or it's a deliberate, justified addition — see CONTRIBUTING)
  • README / CHANGELOG.md ([Unreleased]) updated if behavior or the deployed surface changed
  • Reconciled affected tracked work (roadmap Roadmap to 1.0.0 #68, the issue body, ADR/CHANGELOG) if this change reshaped it

🤖 Generated with Claude Code

https://claude.ai/code/session_01XP1KN94N969SVMNFPdVtGg

buildChartData() previously sliced the raw rows to chartRowCap(type)
BEFORE grouping, so a row past the cap was dropped from its category's
sum and a category could vanish only because its first row arrived late.

Now it is two-pass: pass 1 discovers unique raw X keys across ALL fetched
rows in first-seen order and retains the first `cap`; pass 2 aggregates
every fetched row whose X is a retained category. Categories are never
value-ranked, sorted, or folded into an Other bucket.

The result carries typed ChartDataMeta (totalRows/totalCategories/
shownCategories/categoriesTruncated/duplicateCellsSummed/groupKey) and a
new pure chartDataNote(meta) renders "first N of M categories" and/or
"duplicate X[/series] rows summed in the browser" — describing only the
browser-side transform, never whether the SQL pre-aggregated. Duplicate
detection keys on X (or (X, series)), so a normal multi-series result is
not reported as summing, and a duplicate confined to an omitted category
never flags the visible chart.

renderChart aggregates once (chartJsConfig accepts the precomputed
result) and renders the note on every surface: inline in the config bar
on the workbench, and as a standalone block above the canvas on
Dashboard / read-only / detached tiles (controls:false), which
previously capped categories silently.

Tests: buildChartData category-cap + metadata cases, chartDataNote
formatter, renderChart note text/placement across control states + the
single-aggregation contract, and a Dashboard-surface panels regression.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XP1KN94N969SVMNFPdVtGg
@BorisTyshkevich
BorisTyshkevich merged commit ac08b55 into main Jul 17, 2026
7 checks passed
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.

Chart aggregation sums only a truncated prefix of fetched rows, and the note overstates what the chart represents

1 participant