Skip to content

fix(plugin-charts): a two-dimension pie, donut, funnel or treemap draws every group or refuses, never the first group alone - #27736

Merged
objectstack-fleet[bot] merged 7 commits into
mainfrom
claude/project-thread-osge5f
Oct 29, 2026
Merged

objectstack-fleet[bot] merged 7 commits into
mainfrom
claude/project-thread-osge5f

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

domain:ui#2 · os-bill · session_01Utp6CY2n1JmZZKrHgtp9jC

Requested by Bill · project thread

Fixes #27563

Clause-②: no
Metadata-protocol: none

维护者速读

  • 改了什么:仪表板的矩形树图拿到两个维度(例如 阶段 × 负责人,金额求和)时,按「阶段 → 负责人」两级嵌套画出每一格;饼图、环形图、漏斗图拿到两个维度时不再只画第一个负责人的数,而是在磁贴上显示拒绝说明,并点名每一个负责人分组。
  • 为什么改:第二个维度被透视成多条序列,这四个图表分支只读第一条,其余分组静默丢掉,看起来像一张完整的图。裁决是「每组都画,或者拒绝,绝不出半张图」:树图原生支持嵌套,所以画全;饼图一圈只能表达一个整体的份额、漏斗每段只能是一个总数,没有放第二条序列的位置,所以拒绝。
  • 风险与回滚:只动这四个图表分支和仪表板组件给这几类图传的序列;单维度图表画法不变,对比叠加(compareTo)不算分组,没有数据时仍是原来的空图。仪表板外带多个度量的饼图/漏斗(例如手写多条 yAxis)现在也会拒绝、树图会按度量嵌套,见 Acceptance notes 第一条,请席位确认;回滚即 revert 本 PR。
  • 先看:ui/packages/plugin-charts/src/AdvancedChartImpl.tsx 的 buildTreemapGroups、OneSeriesFamilyRefusal 和 pie/funnel/treemap 分支开头;ui/packages/plugin-dashboard/src/DatasetWidget.tsx 的 chartSeries。
  • 两分钟验证:pnpm --dir ui exec vitest run packages/plugin-charts/src/AdvancedChartImpl.twoDimensionSingleSeries-27563.test.tsx packages/plugin-dashboard/src/__tests__/DatasetWidget.twoDimensionSingleSeries-27563.test.tsx,预期 32 条全绿。
  • 需要你做的:无,席位落地。

What changes

A dashboard widget (or dataset-bound chart) of type pie, donut, funnel or treemap with dimensions: ['stage', 'owner'] and one measure reaches the renderer pivoted by buildChartSeries: one row per stage, one series per owner. These four arms read series[0] alone, so they drew the first owner's amounts as if they were the whole chart. Triage's rule (the same as #27460): draw every group, or refuse a shape the chart cannot draw; never a partial chart. Per family:

  • Treemap: draws every group. Nesting is native to Recharts' Treemap, so two or more groups are drawn as two levels (buildTreemapGroups, the treemap counterpart of fix(plugin-charts): a two-dimension sankey draws every flow, not only the first group's #27560's buildSankeyCellFlows): each stage is a parent, each owner it has records for a leaf inside it, sized by the measure. A leaf is named stage / owner (the title the dashboard's drill gives the same cell; a parent's own name is painted over by its leaves) and takes its stage's colour. A combination the dataset did not answer is no leaf and no omission. A carried cell that cannot be sized is left out and the existing unsized-rows note is printed once per owner that lost one; no sizable cell at all is the existing no-positive-magnitude refusal, now naming every owner it tested. Clicking a leaf drills to its cell ({ category, series }, as the two-level sankey's flow click does).
  • Pie, donut, funnel: refuse on the tile. A pie's angle is one share of one whole and a funnel stage one total, so a second series has no place on either; a second ring or a stacked funnel would be a new visual with its own legend and colour contract, which this card does not decide. Handed two or more groups with rows to draw, these arms now return OneSeriesFamilyRefusal in the existing ChartRefusal shell (role="status", data-chart-error="one-series-family"): "A pie draws one series, and this chart carries 3: Ann, Bo, Cy. Drawing only the first would hide the others, so none is drawn." Every group is named, and the sentence stays true if the series are measures rather than groups.
  • Which series are groups. groupSeries (the sankey's sankeyFlowSeries, renamed and shared) drops a comparison overlay, so a compareTo treemap stays one level.
  • The dashboard hands these families one measure. DatasetWidget already handed a sankey only the measure it renders (fix(plugin-charts): a two-dimension sankey draws every flow, not only the first group's #27560); the same filter now covers every SINGLE_SERIES_CHART_FAMILIES family. A stored widget of these types that declares several measures with one dimension (refused at publish by the spec's single-series arm since 17.7.0) therefore still draws its first measure, and the dropped-measure console warning stays true, instead of its second measure being read as a group. A pivot carries one measure, so nothing is dropped from it.

Unchanged: every one-dimension chart of the four types (regression pins below), the empty-rows path, the sankey.

Tests

  • ui/packages/plugin-charts/src/AdvancedChartImpl.twoDimensionSingleSeries-27563.test.tsx (20): the fixture is a real buildChartSeries pivot (stage × owner, sum amount). Treemap: one leaf per answered cell with its size, shared stage colour, the per-owner note for a cell at -5, the magnitude refusal naming every owner, the leaf click's drill event; pie/donut/funnel: no Pie/Funnel element rendered and the refusal names every owner; regression pins per family for one dimension, the compared treemap, and no rows. Marks are read from the props each Recharts element was rendered with, not jsdom pixels.
  • ui/packages/plugin-charts/src/ChartRenderer.yAxisUndrawn-10691.test.tsx: draw settles on the plot or the renderer's refusal, and the pie control asserts the one-series refusal (see Acceptance notes).
  • ui/packages/plugin-dashboard/src/__tests__/DatasetWidget.twoDimensionSingleSeries-27563.test.tsx (12): the card's widgets through DatasetWidget → SchemaRenderer → AdvancedChartImpl: the treemap draws 5 depth-2 leaves, pie/donut/funnel refuse naming every owner, a one-dimension widget of each type draws, and a stored one-dimension widget with two measures still draws (no group refusal, no nested treemap).

Acceptance notes

  • Mechanism measured, premise holds. On origin/main (525ea3632) the renderer file swapped in: the treemap drew 3 one-level leaves with no children, and pie, donut and funnel each rendered one Pie/Funnel element bound to the first owner's column (15 of the 32 new tests red, every control green).
  • Refusal on the tile, not at publish. Triage's text says "refuse at publish". A publish-time refusal of a second dimension on these types is a spec door (packages/spec, outside this card); the tile refusal meets "never a partial chart" today and names every group. Whether the spec should also refuse the shape at publish is the seat's call.
  • ⚠ Seat to confirm: charts outside the dashboard that carry several MEASURES. AdvancedChartImpl cannot tell a pivoted group from a second measure; it reads two or more non-comparison series as groups, as fix(plugin-charts): a two-dimension sankey draws every flow, not only the first group's #27560 made the sankey do. The dashboard hands these families one measure (above). Two other shapes reach the arms with several measures and change too: a chart block that authors a yAxis list on a pie, donut or funnel, and a page chart bound to a dataset with ONE dimension and TWO measures (checkDashboardWidgetChartMeasureArity is attached to the dashboard widget schema only, so no spec door refuses either there). Both used to draw their first measure silently, a partial chart; a pie, donut or funnel now shows the one-series refusal naming every measure, and a treemap nests the measures under each category. Measured: ChartRenderer.yAxisUndrawn-10691.test.tsx's control "(control) pie, three entries: no note" settled on a drawn svg, which the pie no longer draws; its subject (no y-axis note on a pie) still holds, so the helper now settles on the plot or the refusal and the control also asserts the refusal. If the seat wants those multi-measure charts kept drawing their first measure, the alternative is to mark pivot series in buildChartSeries (@object-ui/core, a published type: Clause-②: yes (widening)) and refuse only on that marker.
  • Copy is English, matching every sibling refusal in this file (no-positive-magnitude, no-positive-flow, missing-series-key); no locale keys added.
  • A third dimension still collapses into the (d0, d1) cell, as the card notes; untouched.
  • File surface: DatasetWidget.tsx is touched for the measure filter (the line fix(plugin-charts): a two-dimension sankey draws every flow, not only the first group's #27560 added for the sankey, widened) and two comments, not for a refusal note; the Claim lists the file conditionally.

Verification

Local subset at head 4fb5e46c (after merging origin/main); the two commits after it touch only the changeset, and the changeset-reading gates were re-run at d12c72c8. CI runs the full farm.

Premise and ablation (base `525ea3632`)

Each leg swapped files from the base into the tree with git restore --source=525ea3632, checked the blob hash, ran, and restored from HEAD under a trap (restore proven by git hash-object equal to the HEAD blob and git diff HEAD empty).

  • Renderer and widget at base: the two new files, 15 failed / 17 passed (every subject red, every control green). The treemap drew three one-level leaves (Prospect, Proposal, Won) with no children; pie, donut and funnel each rendered one Pie / Funnel element (bound to the first owner's column).
  • Widget measure filter alone at base (renderer fixed): 8 failed: the four "stored one-dimension TYPE with two measures still draws its first measure" controls, and the four fires iff the second measure's values leave the drawn chart unchanged rows of DatasetWidget.singleSeriesMeasures-11417.test.tsx for pie, donut, funnel and treemap.
Tests
  • pnpm --dir ui exec vitest run --maxWorkers=2 packages/plugin-charts/ packages/plugin-dashboard/ (under the verify lock, at 5d26a453): Test Files 297 passed, 1 failed (298); the failure was the ChartRenderer.yAxisUndrawn-10691 pie control described in Acceptance notes, updated in the next commit.
  • The updated control, the 27563 renderer file and every ui test file that names a pie, donut, funnel or treemap outside the two packages (console unfulfilled-chart-stubs-8760, app-shell listViewLegacyChartRetired-6152 and ObjectView.chartRelay-7823, plugin-chatbot ChatbotEnhanced.chartOrder-13492 and useObjectChat.chartOrderRehydrate-13492, plugin-list ListView.chart-capability-7544, the whole plugin-report/src/__tests__/): Test Files 38 passed (38), Tests 390 passed, 7 skipped.
  • At 4fb5e46c: the two 27563 files, the 10691 file, DatasetWidget.singleSeriesMeasures-11417, and both 27460 sankey files: Test Files 6 passed (6), Tests 81 passed.
  • pnpm --filter @object-ui/plugin-charts run type-check && pnpm --filter @object-ui/plugin-dashboard run type-check: exit 0 (both programs include the new tests via tsconfig.test.json).
  • eslint on the touched files: 0 errors; the warnings on new lines are no-explicit-any in the renderer test's recharts mocks, the shape the 27460 test uses.
Derived gates (dispatch-gates --commands --repo objectstack-ai/objectstack: 35; all run; reconciled with --ran)
node scripts/check-adr-0087-registration.mjs --self-test :: exit 0
node scripts/check-closing-keyword-parity.mjs --self-test :: exit 0
node scripts/check-empty-changeset.mjs --self-test :: exit 0
node scripts/check-sdui-manifest.mjs :: exit 0
node scripts/check-sdui-manifest.mjs --self-test :: exit 0
node scripts/pm/release-rehearsal-clone.mjs --self-test :: exit 0
node scripts/release-pending-publish.mjs --self-test :: exit 0
pnpm --dir ui exec vitest run scripts/ :: exit 0
pnpm --filter @objectstack/spec run check:duration-unit-keys :: exit 0
pnpm check:changeset-gate-self-tests :: exit 0
pnpm check:closed-ceremony-text :: exit 0
pnpm check:cross-package-test-inputs :: exit 0
pnpm check:dispatcher-error-vocabulary :: exit 0
pnpm check:driver-memory-census :: exit 0
pnpm check:engine-double-contract :: exit 0
pnpm check:error-status-conformance :: exit 0
pnpm check:gitlink-declared :: exit 0
pnpm check:nul-bytes :: exit 0
pnpm check:published-files :: exit 0
pnpm check:query-options-erasure :: exit 0
pnpm check:refd-timer-probe :: exit 0
pnpm check:skill-identifier-liveness :: exit 0
pnpm check:test-source-alias :: exit 0
pnpm check:tier-file-adoption :: exit 0
pnpm check:type-check-coverage :: exit 0
pnpm check:type-check-debt :: exit 0
pnpm check:undeclared-dep-imports :: exit 0
pnpm check:watch-hint-literal :: exit 0
pnpm check:where-matcher :: exit 0
node scripts/check-adr-0087-registration.mjs --base origin/main :: exit 0
node scripts/check-changeset-no-major.mjs --base origin/main :: exit 0
node scripts/check-empty-changeset.mjs --base origin/main :: exit 0
node scripts/check-closing-keyword-parity.mjs :: exit 0
pnpm check:pm-changeset-deadline-census :: exit 0
node scripts/check-comment-mask-corpus.mjs :: exit 0

dispatch-gates --ran: 35 derived, 35 run, 0 NOT-MEASURED, 0 UNRUN.

Human Minimum

  • 1. Schemas start as Zod; packages/spec holds no runtime logic. (packages/spec untouched.)
  • 2. Every name I added uses the casing for its kind.
  • 3. No formatter ran; pnpm lint passes. (eslint on the touched files: 0 errors; the repo-wide run is CI's.)
  • 4. One issue, one branch off an up-to-date main, one pull request.
  • 5. A change that publishes carries a changeset, and both declaration lines are filled in here and in the changeset.
  • 6. packages/spec touched: n/a.
  • 7. No hand edits to content/docs/references/, packages/*/CHANGELOG.md or content/docs/releases/.
  • 8. Title, body and comments are in English.
  • The Before Submitting list in CONTRIBUTING.md is done.

Generated by Claude Code

…ery group or refuses (WIP)

Claude-Session: https://claude.ai/code/session_01Utp6CanXq6PY9nH1pZ5E5m
Co-authored-by: Claude <noreply@anthropic.com>
…lick as unknown, not any

Claude-Session: https://claude.ai/code/session_01Utp6CanXq6PY9nH1pZ5E5m
Co-authored-by: Claude <noreply@anthropic.com>
…ontrol settles on the one-series refusal

Claude-Session: https://claude.ai/code/session_01Utp6CanXq6PY9nH1pZ5E5m
Co-authored-by: Claude <noreply@anthropic.com>
…at now refuse or nest

Claude-Session: https://claude.ai/code/session_01Utp6CanXq6PY9nH1pZ5E5m
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/l documentation Improvements or additions to documentation tooling labels Oct 29, 2026
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:ui#2 · os-bill · session_01Utp6CY2n1JmZZKrHgtp9jC

席位核对

ACCEPT · head d12c72c818c69ea25169f241868f87998e3ad941 · 2026-10-29T22:14Z

  1. Shape: draft → main, Fixes #27563, attribution block on top. Six files: plugin-charts/src/AdvancedChartImpl.tsx, plugin-dashboard/src/DatasetWidget.tsx, two new tests, one edited control test (ChartRenderer.yAxisUndrawn-10691) and one changeset. No assignee, no reviewer.
  2. Premise measured:
    • buildChartSeries pivots dimensions[1] into one series per value, and the pie/donut, funnel and treemap arms bound series[0] only. On the base, 15 of the 32 new tests were red and the controls were green.
    • The fix follows the triage rule ("draw every group, or refuse … Never a partial chart"):
      • treemap draws every cell as two levels (first dimension → leaf per carried cell), with the existing unsized note per group and drill per leaf;
      • pie / donut / funnel refuse on the tile (data-chart-error="one-series-family") and name every group.
    • DatasetWidget hands every single-series family the one measure it renders (the dashboards: a sankey widget with two dimensions silently draws only the first category's flows #27460 sankey filter, widened), so a stored multi-measure widget's other measures are not read as groups. Its dropped-measure warning stays true.
    • The seat read the diff. The 10691 control keeps its subject (no note on a pie) and adds the refusal assertion. The refusal copy is English-only, like this file's existing refusals (MagnitudeRefusal).
  3. Gates (check-runs on this head): 44 success, 11 skipped, 0 red.
    • Named runs: Lint & Repo Gates 123756420508, TypeScript Type Check 123755129364, Test UI 123758216086 (16/16 shards green), Lint · PM dispatch-gates 123753272107.
    • The dev's derived gates: 35/35 run, 0 NOT-MEASURED. The plugin-charts and plugin-dashboard suites pass, as do 38 chart-touching files across console, app-shell, chatbot, list and report.
    • Ablations went red and were restored byte-identical.
  4. Contract review: not owed. Clause-②: no: no exported shape changes, and the refusal is an internal component. Metadata-protocol: none.
  5. Accepted deviations / out of scope:
    • Seat call (A): the renderer cannot tell a pivoted group from a second measure. So a page chart block of type pie, donut or funnel that carries several measures (an authored yAxis list, or one dimension + two measures) now refuses, naming every measure, where it used to draw the first one silently. Such a treemap now nests the measures.
      • The seat accepts this as the same rule: a silent first-measure chart is a partial chart (triage "Never a partial chart"; AGENTS Prime Directive 12), and the merged sankey arm (dashboards: a sankey widget with two dimensions silently draws only the first category's flows #27460) reads the same way.
      • The alternative, marking pivot series in @object-ui/core buildChartSeries (Clause-②: yes (widening)), was declined: it buys no user-visible gain.
      • The changeset states the change in its "Several measures outside a dashboard" bullet.
    • Not filed: the publish-time half of triage's rule (a spec door refusing two dimensions on these families) belongs to the spec lane.
  6. Landing: not governed (check-governed-merges --pr 27736: NOT governed, 0 of 6 paths), 594 lines < 3000. The seat lands it: ready + auto-merge now.

Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 29, 2026 22:15
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 29, 2026
Merged via the queue into main with commit d7625ea Oct 29, 2026
56 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/project-thread-osge5f branch October 29, 2026 22:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/l tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

dashboard pie/donut/funnel/treemap chart with two dimensions draws only the first second-dimension group, silently

1 participant