Skip to content

fix(plugin-charts): place each y axis by the spec yAxis[].position, and lay out and bind horizontal-bar's axes (objectui#10654) - #10686

Merged
objectstack-fleet[bot] merged 5 commits into
mainfrom
claude/issue-10654-yaxis-position-honoured
Sep 25, 2026
Merged

objectstack-fleet[bot] merged 5 commits into
mainfrom
claude/issue-10654-yaxis-position-honoured

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #10654
Clause-②: no

The renderer begins honouring a key the spec already declares (yAxis[].position, on @objectstack/spec's ChartAxisSchema, which each yAxis entry takes); no export, schema or accept set moves. objectui#10655, closed as a duplicate and folded into this card, is rows 5 and 6 below.

What changed

One resolver. placeYAxes (in packages/plugin-charts/src/normalizeChartSchema.ts) is the one place a y side is resolved. normalizeChartSchema binds each series it derives from the entries (no series) with it, and AdvancedChartImpl places the value axes and writes the notes from the same answer. The x-side mechanism of PR objectui#10647 is reused: the ChartFootnote note channel (joined with joinNotes), and the default side. No second note mechanism.

Family Open sides for a yAxis entry Default
bar (column), line, area, combo left / right first entry left, second right
scatter left / right, first entry only (scatter draws one y axis) left
horizontal-bar bottom / top (the value axes run across the plot) first entry bottom, second top

A series binds to a slot by its spec yAxis: 'left' | 'right'. On horizontal-bar the chart is the vertical one transposed, so 'left' binds the bottom axis and 'right' the top one (the same swap PR objectui#10647 reads for xAxis there).

A side a value axis cannot take is refused: the entry is placed as if it named none, and the chart shows p role="note" data-chart-note="y-axis-position" under the plot, naming yAxis[N].position, the value, and the side it was drawn on. Not a ChartRefusal, no console warning, and no schema narrowing, for the reasons the x note gives.

horizontal-bar. Two entries now draw two value axes (bottom and top, each a recharts XAxis with its own xAxisId), and the bars bind to those ids. They were bound to yAxisId left / right, which the branch never rendered. xAxis.title is drawn on the category axis, laid out for an axis running down the plot, and the yAxis title under (or over) the value axis, laid out for an axis running across it.

JSDoc. The yAxes prop JSDoc and NormalizedChartSchema.yAxes no longer say a lone position: 'right' entry turns on a secondary axis, or that index 0 is the left axis.

Docs. The yAxis sentence of the position row in content/docs/plugins/plugin-charts.mdx now states what the head does (it pointed at this card). Changeset .changeset/10654-yaxis-position-honoured.md, '@object-ui/plugin-charts': patch.

The rule when both entries name a side, on the four axes

Rule: each entry takes the open side it names; when both name the same side, the first keeps it and the second is drawn on the other side, with a note naming yAxis[1].position; an entry naming no side takes the side the other left free.

  • Real business need. series[].yAxis binds by side name, and the docs example binds margin to the right-hand axis. Only a per-entry side makes a declared position mean the side the docs promise. yAxis[0] is already the primary axis the docs name (its showGridLines governs the horizontal grid), so keeping it primary changes no reading an author relies on.
  • Long-term fit. One resolver, shared by the normalizer and the renderer, so a derived series can never be bound by one rule and drawn by another (the base defect in row 4).
  • Hard for an AI author to get wrong. Declaration order is the only tie-break an author can read off the metadata, and the conflict is named on screen instead of resolved silently. A value the axis cannot take is refused visibly, never mapped to a "nearest" side.
  • No scope growth. No key, no schema change, no new channel: the x note's channel and wording shape.

Rejected: the second entry wins a conflict (it overrides the entry the docs call primary); both refused to defaults (more notes, and neither entry honoured); index-only placement (the base behaviour, rows 1 to 4).

Measured on the base (4758b33; packages/plugin-charts byte-identical to 02e6d36)

A throwaway jsdom probe through ChartRenderer (probe file deleted) confirmed every H1 row:

  • a lone position: 'right' entry: every value tick orientation="left" on bar, line, area and scatter, and no note;
  • top on a y entry: drawn on the left, no note;
  • [{ revenue, position: 'right', min: 0, max: 100 }, { cost, position: 'left' }] with no series: the left axis carried the first entry's 0..100 domain and no series, and both series were measured on the right axis (0..20);
  • horizontal-bar with two entries, with or without authored series: 4 bar rectangles where 6 are owed (bar control: 6);
  • horizontal-bar with xAxis.title: 'Month' and a yAxis title Revenue: one label, Revenue, rotated -90 at x=70; Month not drawn (bar control: Month under the category axis, Revenue rotated at x=10);
  • a combo with a lone right entry titled Revenue: the title drawn on both sides.

Pin: packages/plugin-charts/src/ChartRenderer.yAxisPosition-10654.test.tsx

One enumeration over the rows, through ChartRenderer with the axes in spec shape, each row with its bar control. Readings: tick orientation, tick text per side, text.recharts-label position and rotation, .recharts-bar-rectangle count, data-chart-note="y-axis-position" notes, grid lines and reference lines.

  • head 2fd09e7: Tests 52 passed (52), exit 0.
  • base implementation swapped in (AdvancedChartImpl.tsx blob d97fcee0 and normalizeChartSchema.ts blob 6b418617, the base blobs; restored to the head blobs 4356fc08 / ab7945cb after, git diff HEAD empty): Tests 38 failed | 14 passed (52), exit 1. The 14 green are exactly the controls; every non-control pin is red.

Per-hunk ablation (ablation-replace.mjs, anchor hit as declared, blob changed, restored to the head blob each time)

Run at f531ed9, whose two implementation blobs are byte-identical at 2fd09e7.

Hunk ablated Pins red
derived-series binding in the normalizer (back to "index above 0, or right") 5: row 4 (bar, line, area, combo render + the normalizer reading)
horizontal-bar bars bound by xAxisId (back to yAxisId) 4: row 5 (both), row 7 horizontal-bar, row 11 horizontal-bar
valueAxisIdFor forced to no id 5: row 11, every non-control case
right-hand title layout (forced to the left layout) 3: row 10, every non-control case
yPositionNote forced null 10: rows 2 (5), 7 (3), 8 (2)
category-axis title on horizontal-bar (deleted) 1: row 6 horizontal-bar
lone-axis orientation: 'right' (both sites) 5: row 1 (4), row 10 lone right

The lone-axis ablation's first attempt was refused by the tool (its replacement already occurred once in the file, so the count check failed and nothing ran); it was re-run with a distinct replacement, above.

In-place fixes under the bounded exemption (same defect class, same file, same gates)

  • Grid and annotations bind to a rendered value axis. recharts binds a grid or an annotation to axis id 0 by default, and a branch whose value axes carry ids renders none. Measured at base: a two-entry bar chart drew 2 horizontal grid lines instead of one per tick and dropped its axis: 'x' annotation; a combo with fewer than two entries drew no annotation (0 of 2); a two-entry horizontal-bar dropped its axis: 'y' annotation. They now bind to the 'left' slot's axis (valueAxisIdFor); row 11 pins it. Without it this change would also have regressed a two-entry horizontal-bar's vertical grid.
  • A right-hand value axis lays its title out on the far side of its tick labels, the left layout mirrored (angle: 90, insideRight), as PR objectui#10647 mirrors a top x title. Measured with the base layout: the right-hand title at x=462, its tick labels starting at x=465. Row 10 pins it. This also moves the title of every existing second-entry (right-hand) axis.

Gates (local, head 2fd09e7)

  • pnpm exec vitest run packages/plugin-charts/ --maxWorkers=2 (via os-verify-lock, VERDICT command-exit 0): Test Files 79 passed (79) · Tests 937 passed (937).
  • pnpm --filter @object-ui/plugin-charts type-check (tsc --noEmit && tsc -p tsconfig.test.json): exit 0, 0 error TS. The test program lists the new pin file (--listFiles). Dependency closure built first: pnpm --workspace-concurrency=2 --filter '@object-ui/plugin-charts^...' build exit 0.
  • check:control-bytes, check:test-path-roots, check:vi-mock-specifiers, check:vi-mock-inherit, check:vi-mock-override-shape: all exit 0 and OK.
  • check:new-line-citations: VERDICT new-cross-file-line-citations: 0 new citation(s), enforcement report-only -> exit 0.
  • check-changeset-presence: ✅ 3 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s): .changeset/10654-yaxis-position-honoured.md. check-changeset-no-major exit 0. check-changeset-overwrite: ✅ No pre-existing changeset was modified or deleted. check:pending-changeset-literals exit 0.
  • check:changeset-claims (report-only, exit 0) asks for a read of .changeset/7546-chart-series-keys-declared.md ("yAxis binds the secondary axis") and .changeset/7694-chart-series-chart-type-alias-refusal.md: both read, both still true.
  • Docs: check-doc-links (Links are valid across 17 scan roots.), check:doc-types, check:prompt-keys, check:doc-example-ids, check:doc-fences, check:shell-escape-residue, check:doc-example-readers, check-doc-expression-carriage: all exit 0.
  • Lint, narrowed (the repo-wide run is CI's): eslint --format json over the three changed TS files: 3 files, 0 errors, 49 warnings. The two source files carry the same rule tally as their base blobs (AdvancedChartImpl.tsx 39 no-explicit-any, 4 exhaustive-deps, 1 preserve-manual-memoization; normalizeChartSchema.ts 5 no-explicit-any); the pin file has none. The resolved config has empty parserOptions (no type-aware linting) and no rule in eslint-rules/ reads another file, so this diff cannot move any untouched file's verdict.
  • Merged origin/main (7baede3) before opening; it touched ObjectChart.tsx in this package, and the suite above ran after the merge.
  • CI: not awaited.

Acceptance notes

  • NOT MEASURED: check:doc-snippets and check:doc-examples exit 2 (prerequisite: the scoped build of the packages the covered docs import). The docs edit changes one table cell and no fenced block.
  • NOT MEASURED: a real-browser render. Geometry (title and tick positions) is read from the jsdom render, whose text metrics are zero. The category-axis title on horizontal-bar sits at the axis's outer edge as a vertical chart's left title does; its clearance from long category labels was not measured.
  • Default-case prose left as is, true for entries that name no side and outside this card's surface: the Axis options intro in plugin-charts.mdx ("a second entry declares the right-hand axis"), content/docs/api/schema-reference.md, the @object-ui/types JSDoc and Zod describe strings, and .changeset/7690-chart-axis-config-declared.md.
  • Entries after the second are placed on no axis, as before: they draw no axis and no note, and a series derived from one still binds to the right. Reported to the seat, not addressed here.

Implemented by the os-dev run for the domain:ui seat 5, session session_01KUxVUa7e39aNjhkKi1gsoy.


Generated by Claude Code

…nd lay out and bind horizontal-bar's axes

`placeYAxes` (normalizeChartSchema) is the one place a y side is resolved:
the normalizer binds each series it derives from the entries with it, and
the renderer places the value axes and writes the `y-axis-position` note
from the same answer. A lone right-hand entry now draws on the right, a side
a value axis cannot take is refused with a note naming `yAxis[N].position`,
and when both entries name one side the first keeps it.

horizontal-bar draws its value axes across the plot (bottom / top), binds its
bars to the ids it renders, draws `xAxis.title` on the category axis and the
`yAxis` title under the value axis.

Claude-Session: https://claude.ai/code/session_01KUxVUa7e39aNjhkKi1gsoy
Co-authored-by: Claude <noreply@anthropic.com>
…t value axis, and mirror a right-hand y title

A branch that renders its value axes with ids (a combo always; bar, line,
area and horizontal-bar with two yAxis entries) renders no axis 0, so the
grid and the annotations bound to it by default collapsed or were dropped.
They now bind to the left slot's axis. A right-hand y axis lays its title
out on the far side of its tick labels, as a top x axis does.

Claude-Session: https://claude.ai/code/session_01KUxVUa7e39aNjhkKi1gsoy
Co-authored-by: Claude <noreply@anthropic.com>
…t-hand title in the objectui#10654 enumeration

Claude-Session: https://claude.ai/code/session_01KUxVUa7e39aNjhkKi1gsoy
Co-authored-by: Claude <noreply@anthropic.com>
…e plugin-charts patch

The yAxis sentence of the axis `position` row now reads what the head does,
the changeset states the placement rule, and the enumeration pins the
free-side rule on bar and column.

Claude-Session: https://claude.ai/code/session_01KUxVUa7e39aNjhkKi1gsoy
Co-authored-by: Claude <noreply@anthropic.com>
…xis-position-honoured

Claude-Session: https://claude.ai/code/session_01KUxVUa7e39aNjhkKi1gsoy
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

changeset-claim-re-read

⚠️ 2 pending changeset(s) describe a file this change touches

Their bodies publish verbatim into the CHANGELOG at the next release, so this is a request to re-read them against your diff — addressed here because you are the one seat that can answer it without re-deriving anything.

⛔ Nothing here blocks, and nothing here is a verdict on your change. This gate exits 0, is not a required context, and judges name resolution, never meaning: it asked whether a pending body names a file you touched. "Is this sentence still true?" is the one question it will not answer, and the one you are being asked to answer.

.changeset/7546-chart-series-keys-declared.md

  • names AdvancedChartImpl.tsx → packages/plugin-charts/src/AdvancedChartImpl.tsx — edited by this change

    Every one of the six is read by normalizeSeries (@object-ui/plugin-charts, normalizeChartSchema.ts:242-255) and does real work in AdvancedChartImpl.tsx — label names the legend entry, variant === 'comparison' selects the muted overlay, opacity / dashArray set stroke and fill, stack becomes Recharts' stackId, yAxis binds the secondary axis. Any consumer of the parse output — objectui check / objectui validate via safeValidateSchema, a JSON schema derived from the mirror, or any pipeline that keeps parse()'s result — lost them outright.

.changeset/7694-chart-series-chart-type-alias-refusal.md

  • names normalizeChartSchema.ts → packages/plugin-charts/src/normalizeChartSchema.ts — edited by this change

    chartType is the renderer's INTERNAL spelling of type: the first limb of normalizeSeries' str(raw.chartType) ?? str(raw.type) (@object-ui/plugin-charts, normalizeChartSchema.ts), written by the internal-shape producers that hand dataKey-shaped arrays straight to ChartRenderer (ObjectChart, DatasetWidget; core/utils/chart-presentation translates authored type into it) and by nothing an author writes. Re-measured at implementation time, series-level, with lit controls (dataKey / name / type / color): docs 0, fixtures 0, designer inputs 0 (the chart registration's series is one code input), src literals 0, tests 9 — every one an internal-shape array that never meets this mirror. Limb ablation over 304 files / 5817 tests: deleting str(raw.chartType) ?? left all green; deleting the ?? str(raw.type) sibling went 2 red.

Read the paragraph, not the line: both false halves of the objectui#8617 claim sat in one paragraph, and correcting either alone would have left it asserting the same wrong thing.

If a claim did go false, correct the body. That is precedented and prose-only, frontmatter untouched; check-changeset-overwrite.mjs will report the correction as its own case 2 ("correcting a declaration on purpose … legitimate"), which is the intended shape — one gate asks for the read, the other records the write.

Not covered, stated so nobody reads this as more: a born-false claim that spells no line address at all (objectui#9495 coordinated one by ORDINAL — "a grep finds that member first" — and deciding that means reading what the sentence means), a claim spelled as a symbol or a package rather than a backticked file name, and a file named ambiguously.

Compared the checked-out tree with 7baede374 (merge-base with origin/main): 4 file(s) changed outside .changeset/, read against 1516 pending declaration(s) that publish a body (2102 pending in total). · run

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 329 chunks) 3055.8 KB 3104.5 KB
Main entry chunk (gzip) 147.9 KB 350 KB
Entry file index-THSNNhUC.js —
Status PASS —

The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it.


📦 Bundle Size Report

Package Size Gzipped
app-shell (consoleActionDispatch.js) 0.20KB 0.19KB
app-shell (index.js) 16.57KB 6.15KB
app-shell (runtime-config.js) 20.68KB 7.36KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.06KB 3.86KB
auth (ActiveOrganizationStorage.js) 27.95KB 10.04KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 40.17KB 10.58KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.15KB 5.39KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.65KB 2.22KB
auth (SocialSignInButtons.js) 9.61KB 3.89KB
auth (UserMenu.js) 3.39KB 1.21KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 40.21KB 10.80KB
auth (createAuthenticatedFetch.js) 8.52KB 3.45KB
auth (index.js) 3.63KB 1.64KB
auth (invitation-status.js) 1.22KB 0.70KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 5.30KB 1.02KB
auth (useWorkspaceAdminStatus.js) 11.08KB 4.58KB
collaboration (CommentThread.js) 27.13KB 7.95KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.49KB 2.64KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.68KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.05KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 547.22KB 130.96KB
core (index.js) 9.52KB 3.79KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 223.91KB 62.28KB
fields (index.js) 259.19KB 65.61KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (builtinAggregateLabels.js) 0.86KB 0.49KB
i18n (currency.js) 2.59KB 1.22KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 8.87KB 3.64KB
i18n (index.js) 5.24KB 2.27KB
i18n (pickLocalized.js) 9.86KB 3.95KB
i18n (provider.js) 39.40KB 12.91KB
i18n (useDisplayLocale.js) 3.52KB 1.76KB
i18n (useObjectLabel.js) 34.34KB 9.17KB
i18n (useSafeTranslation.js) 5.60KB 2.33KB
layout (index.js) 39.28KB 11.09KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.75KB
mobile (index.js) 1.99KB 0.87KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 6.62KB 2.45KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useSpecGesture.js) 5.52KB 2.10KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 13.52KB 4.88KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.24KB 2.16KB
permissions (discardProofCache.js) 1.04KB 0.55KB
permissions (evaluator.js) 8.33KB 3.07KB
permissions (index.js) 0.93KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.53KB
permissions (usePermissions.js) 4.83KB 2.27KB
plugin-ai (index.js) 16.01KB 3.93KB
plugin-calendar (index.js) 51.49KB 14.64KB
plugin-charts (index.js) 80.37KB 22.20KB
plugin-chatbot (index.js) 198.36KB 47.20KB
plugin-dashboard (index.js) 133.60KB 35.41KB
plugin-designer (index.js) 216.25KB 44.39KB
plugin-detail (index.js) 232.85KB 61.58KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 152.55KB 39.16KB
plugin-gantt (index.js) 169.71KB 41.93KB
plugin-grid (index.js) 216.65KB 59.28KB
plugin-kanban (index.js) 48.35KB 15.08KB
plugin-list (index.js) 115.77KB 28.75KB
plugin-map (index.js) 23.01KB 7.60KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 43.55KB 11.99KB
plugin-timeline (index.js) 30.75KB 8.98KB
plugin-tree (index.js) 10.52KB 3.69KB
plugin-view (index.js) 87.77KB 21.99KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 11.66KB 3.50KB
providers (index.js) 0.45KB 0.23KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.62KB 2.34KB
react (LazyPluginLoader.js) 4.47KB 1.63KB
react (SchemaRenderer.js) 116.21KB 38.14KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.03KB 1.86KB
react (schema-input.js) 4.25KB 2.04KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (body-dialect.js) 4.78KB 2.09KB
sdui-parser (codegen.js) 6.58KB 2.74KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 5.78KB 2.56KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (kanban-quick-add.js) 3.89KB 1.87KB
sdui-parser (parse.js) 25.28KB 7.80KB
sdui-parser (provenance.js) 3.66KB 1.82KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 18.27KB 6.20KB
types (ai.js) 4.11KB 2.06KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 2.87KB 1.00KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 2.93KB 1.49KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (data-display.js) 3.75KB 1.85KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.85KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (expression.js) 0.20KB 0.18KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 0.20KB 0.18KB
types (form.js) 0.20KB 0.18KB
types (http-inflight.js) 8.87KB 3.73KB
types (http-retry.js) 4.32KB 2.02KB
types (icon-key-migration.js) 4.26KB 1.63KB
types (index.js) 4.74KB 2.25KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 5.00KB 2.39KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 2.52KB 1.31KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (select-option.js) 0.20KB 0.19KB
types (spec-report.js) 5.05KB 1.93KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (strict-authoring-face.js) 17.15KB 6.32KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 8.11KB 3.32KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 2fd09e70848da932f28bbc43d689f78da5d0c40a

Read: card objectui#10654 (body, comments 5834536782 · 5835132719 · 5835682793; the os-dev-report 5836589916 was not used as evidence), the folded card objectui#10655 and its fold comment 5835169621, PR #10686 body and file list, the diff at merge-base 7baede3745904d929d4afd80bcd5e2314b466072 against the head, the installed @objectstack/spec@17.4.0 (ChartAxisSchema.position: z.enum(['left','right','top','bottom']).optional(), ChartSeries.yAxis: z.enum(['left','right']).default('left'), ChartConfig.yAxis: z.array(ChartAxisSchema).optional()), objectui AGENTS.md (版本号策略 and the changeset rule at :168-169) and .changeset/config.json. Five files: .changeset/10654-yaxis-position-honoured.md (+48), content/docs/plugins/plugin-charts.mdx (+1/-1), packages/plugin-charts/src/AdvancedChartImpl.tsx (+252/-70), packages/plugin-charts/src/ChartRenderer.yAxisPosition-10654.test.tsx (+411, new), packages/plugin-charts/src/normalizeChartSchema.ts (+122/-4). No existing test file is edited or deleted. I did not run the suite (read-only via git; the shared checkout carries no node_modules); CI at the head is the instrument for the pins.

① Derived judgments

Triage 5835132719 binds: 「follow PR objectui#10647's shape for xAxis.position. Honour the sides a y axis can take, refuse any other side with a visible note naming yAxis[N].position, and settle which entry is primary when both name a side. On horizontal-bar, bind the bars to the axes the branch renders, draw xAxis.title on the category axis and the yAxis title under the number axis. One enumeration pin over the rows, each with the bar control.」 Rows checked: the card's four (lone right drawn left; top/bottom silent; second entry always right; hasDualAxis by count), the thread's two (5834536782: horizontal-bar two entries draws 4 bars; no-series first-entry right binds both derived series right, combo lone-right title on both sides), and objectui#10655's two (horizontal-bar never draws xAxis.title; yAxis title rotated beside the category ticks).

  1. The resolver, placeYAxes — normalizeChartSchema.ts:487-522. Takes the first two entries; openSlot maps left/right to a slot on a chart whose value axes run up its side and bottom/top (as left/right slots) on horizontal-bar (:493-496); anything else is undefined and gets a reason: 'side' note (:512); both naming the same open side gives the first its side and the second the other with a reason: 'taken' note (:505, :514); an entry naming no side takes the free one (:506-508); neither naming one gives left, right (:508). RIGHT against the triage: sides honoured, other sides refused with a note naming yAxis[N].position, primary settled. The primary-entry rule (first keeps a contested side; the unnamed entry takes the free side) is the one declaration-order tie-break an author can read off the metadata, matches yAxis[0] already governing the horizontal grid (AdvancedChartImpl.tsx showYGrid = primaryY?.showGridLines !== false), and never maps a refused side to a "nearest" one. Note text (AdvancedChartImpl.tsx:656-684): a side note says which two sides are open and where the axis was drawn; a taken note says yAxis[0] already sits there and where this one was drawn; on horizontal-bar the sides read "at the bottom / at the top" via valueAxisSide (:606) and onSide (:612). RIGHT.

  2. Note channel reused, no second mechanism — yAxisPositionNotes returns a p role="note" data-chart-note="y-axis-position" (:665), the exact shape of xAxisPositionNote's data-chart-note="x-axis-position" (:586), and both go through the same joinNotes(...) into the same ChartFootnote on scatter (:2406), combo (:2529) and the plain branch (:2627). null when no note, which keeps every other chart's DOM unchanged. RIGHT (the x channel of PR objectui#10647 is the only mechanism).

  3. Lone right entry draws on the right (card row 1) — plain branch single YAxis element gets orientation: 'right' when soleYSlot === 'right' (:2718) and its spec props measured for that slot (:1794, axisValues now takes every series on a one-axis chart, :1774); scatter's y axis the same (:2454); horizontal-bar's lone value axis takes orientation: 'top' (:2667). hasDualAxis is now "both slots filled" (:1720), so a lone right entry moves the axis and no longer implies a second one. RIGHT.

  4. top / bottom on a y entry no longer silent (row 2) — refused with the note, placed as if unnamed (item 1). On horizontal-bar it is left / right that are refused (:493). RIGHT.

  5. Second entry no longer always the right axis (row 3) — the left and right axis elements carry leftY / rightY, the entries placeYAxes put in each slot (:1714-1715), on the plain branch (:2700, :2709), combo (:2549-2550) and horizontal-bar (:2658-2659); tick formatters and value labels follow the slot (leftYTickFormatter / rightYTickFormatter, axisOfSeries :1727). RIGHT.

  6. Derived series bind to their own entry's slot (thread row 2) — normalizeChartSchema.ts:637-644: the no-series branch calls the same placeYAxes and writes yAxis: slot for a right slot or any entry after the first (so an explicit left reaches an authored combo, which otherwise binds by index). Indices are the authored ones: the normaliser keeps every object entry with or without field (:311-333, :596-598), and the field filter runs after the slot is read by index. Entries past the second keep binding right, as before (disclosed in the PR body's acceptance notes; out of the card's rows). RIGHT.

  7. Combo lone right entry (thread row 2, combo) — both combo axes now take the slot's entry or nothing (:2549-2550), so a lone right entry configures the right axis only and its title draws once. RIGHT.

  8. horizontal-bar two entries: bars bound to rendered axes (thread row 1) — two value XAxis elements with xAxisId="left" (bottom) and xAxisId="right" (orientation="top") (:2658-2659), and every mark in the plain branch binds { xAxisId: slot } when horizontal, { yAxisId: slot } otherwise, nothing when not dual (:2751; spread on Bar, Line and Area). At base the bars carried yAxisId while the branch rendered no YAxis with either id. RIGHT.

  9. horizontal-bar titles (objectui#10655) — xAxis.title is drawn as the category YAxis element's label, laid out for an axis running down the plot on the side placeXAxis put it (yAxisTitleLayoutFor(xAxisDown), :2683); the yAxis title takes the bottom x layout on the bottom value axis and the top x layout on the top one (:2658-2659, :2668). RIGHT: "draw xAxis.title on the category axis and the yAxis title under the number axis".

  10. JSDoc — yAxes prop JSDoc (:322-327) no longer says a lone position: 'right' turns on the secondary axis; NormalizedChartSchema.yAxes (normalizeChartSchema.ts:247-249) no longer says index 0 is the left axis. Asked by the card's Direction. RIGHT.

  11. hasNoNumericSeriesValue (:2997-3002) reads the same placement; with two entries the left/right entries are the placed ones and dual gates the s.yAxis !== 'left' term; with one entry every series counts against it. Still over-approximated toward silence. The existing noNumericValue tests use [left, right] entries, which place index-identically. RIGHT (consistency change the resolver requires). Minor: this helper places the full list on scatter while the render path slices to one entry (:1713); the difference can only add a declared scale, i.e. more silence, never a refusal.

  12. In-place fix (a): grid and annotations bound via valueAxisIdFor (:641-645; grid :2542, :2644; annotations :1831, :1844, :1857). Judgment: PARTLY REQUIRED, remainder in-place. The new horizontal-bar dual branch renders two value XAxis elements with ids and none at id 0, so a CartesianGrid without xAxisId would have lost its vertical lines: the horizontal-bar row cannot land without this binding. Its extension to combo and two-entry bar/line/area (a { yAxisId: 'left' } for a grid or an annotation bound to id 0 that those branches never render) is a visible change the card did not enumerate, same file, same class (a mark bound to an axis the branch does not render). Disclosed in the changeset ("Grid and annotations bind to the rendered left value axis…" and the user-visible sentence). Breaks no existing pin: no test at main reads grid-line counts on a dual-axis or combo chart (the only grid-count test is scatterGridLines-9792, and scatter's grid line :2414 is untouched); the reference-line/area tests in plugin-charts and plugin-dashboard assert only "greater than 0" or 0, and the new ids always name an axis the branch renders, so a count cannot drop. Pinned by row 11 with the one-entry bar control (test :384-410). Breaks no documented behaviour. Judged acceptable under the bounded exemption.

  13. In-place fix (b): right-hand title mirrored — Y_AXIS_RIGHT_TITLE_LAYOUT = { angle: 90, position: 'insideRight' } (:517), chosen by yAxisTitleLayoutFor (:520) for the right slot (:1794). Judgment: NOT REQUIRED by any enumerated row (row 3's onRight(title.x) would already hold with the base insideLeft layout at x≈462 of 510), so it is a visible change the card did not ask for, and it moves the title of every existing second-entry axis on charts that never set position. It follows the shape of PR objectui#10647, which mirrored the top x title for the same reason (X_AXIS_TOP_TITLE_LAYOUT, :505). Disclosed in the changeset (own bullet, plus "the title of every right-hand value axis" in the user-visible sentence) and in the PR body. Breaks no existing pin: no test at main reads insideLeft, insideRight, an angle, or a right-hand title's geometry outside the two position pins. Breaks no documented behaviour: no docs sentence places a right-hand title. Pinned by row 10 with the left-axis bar control (test :355-371). Not a defect under the FAIL criteria; recorded as scope beyond the ask, disclosed.

  14. Missing asks — none binding. Non-binding: objectui#10655's Direction said the docs title row "may need a sentence on which axis each title names on horizontal-bar"; the title row (mdx :236) is unchanged, while the position row already states that on horizontal-bar xAxis configures the category axis. Not a breach.

  15. Done but not asked — items 12 (its non-horizontal-bar part) and 13 above; axisValues measuring every series on a one-axis chart whichever slot it sits in (:1774), which the lone-right axis needs for stepSize. Nothing else: no key, schema, export or accept set added.

  16. Existing pins edited or deleted — none. The diff touches no existing test; every pin at the merge-base is byte-identical at the head, including ChartRenderer.xAxisPosition-10587.test.tsx. No guard weakened.

  17. Can the new pins fail — yes. 52 cases in one enumeration (rows 1-11), each row with a bar control (14 controls: rows 1 two, 8 three, every other row one). Lit controls: e.g. row 4's control reads left 0..100 and right 0..8 ticks (test :226-230), row 11's control reads one grid line per tick and both reference lines (:406-410). Negative legs: refused sides must produce exactly one note naming yAxis[0].position, the value and the drawn side (rows 2, 8); a contested side must produce one note naming yAxis[1].position (row 7); open sides and unnamed entries must produce zero notes (rows 1, 3, 7 control, 8 controls). Readings are DOM facts (tick orientation, tick text per side, text.recharts-label x/y/rotation, bar-rectangle count, note count, grid and reference-line counts), not implementation calls. From the base code each non-control row reads red: the base single YAxis carries no orientation, emits no y note, binds bars by yAxisId on horizontal-bar, and draws no category-axis title there.

  18. Serial constraint — PR objectui#10647 merged as 33b324c, an ancestor of the merge-base. Its pin file is untouched and green in every CI shard at the head. Reasoned regression check: every case in it declares one yAxis entry with no position, so hasDualAxis is false, soleYSlot is left, no y note is emitted, the plain-branch title still comes from the untouched xAxisCommonProps (:1935), and the horizontal-bar category axis gains a label only when xAxis.title is set, which those cases never set. No regression.

② Semver level

.changeset/10654-yaxis-position-honoured.md declares '@object-ui/plugin-charts': patch. @object-ui/plugin-charts is in the fixed group of .changeset/config.json; the diff changes two files under its src/, so AGENTS.md :168 owes a bump changeset (not empty frontmatter, :169 forbids riding a user-visible fix). 版本号策略: no major (none declared; Changeset Bump Policy green), minor reserved for objectui's own breaking changes. This change honours a declared key, moves no export, type, schema or accept set, and the barrel (src/index.tsx:32-33) re-exports only normalizeChartSchema and the NormalizedChartSchema type, whose shape is unchanged; placeYAxes, ValueAxisSlot, YAxisPositionNote and YAxisPlacement stay module-internal. patch is the right level. The docs edit is not docs-only (source changed), so no empty-frontmatter changeset is owed.

Changeset prose, sentence by sentence against the diff:

  • "A chart's yAxis[].position now places each value axis, and horizontal-bar lays out and binds its axes" — TRUE (items 1, 8, 9).
  • "position is declared on @objectstack/spec's ChartAxisSchema, which each yAxis entry takes, and the normalizer keeps it." — TRUE (spec; normalizeAxis :326-329).
  • "No axis was placed by it: it only chose which entry configured the right-hand axis a combo always draws (and the value format of a right-bound series), and how the normalizer bound the series it derives from the entries." — TRUE at base (secondaryY = ... primaryY?.position === 'right' ? primaryY : undefined, used by combo's right axis and the right-bound valueFormatter; normaliser bound a derived series right when its index was above 0 or its entry named right).
  • "So a lone position: 'right' entry drew its axis on the left, top / bottom passed in silence, a second entry was always the right-hand axis, and with no series a first entry at right took both derived series to the right-hand axis." — TRUE at base.
  • "placeYAxes (in normalizeChartSchema) is the one place a y side is resolved; the normalizer binds each series it derives from the entries with the same call, so a derived series plots against its own entry's axis." — TRUE (:637, :1713, :2997).
  • "bar (column), line, area, combo and scatter: left / right. Scatter draws one y axis, the first entry's." — TRUE (:1713 slices scatter to one entry).
  • "horizontal-bar: the value axes run across the plot, so bottom / top; a series' yAxis: 'left' binds the bottom axis and 'right' the top one." — TRUE (:493, :2658-2659, :2751).
  • "A lone entry's axis is drawn on the side it names. With two entries each is drawn on the side it names, and an entry that names no side takes the side the other left free." — TRUE (:501-508).
  • "Both entries naming the same side: the first keeps it and the second is drawn on the other side, with a note naming yAxis[1].position. yAxis[0] is already the primary axis (its showGridLines governs the horizontal grid), and declaration order is the one tie-break an author can read off the metadata." — TRUE (:505, :514; showYGrid).
  • "A side a value axis cannot take is refused: the entry is placed as if it named none, and the chart shows a note under the plot (data-chart-note="y-axis-position") naming yAxis[N].position, the value and the side the axis was drawn on. ChartAxisSchema is shared by both axes and is not narrowed." — TRUE (:512, :665-680; no schema edit).
  • "horizontal-bar. Two entries draw two value axes, along the bottom and the top, and the bars bind to them; they were bound to yAxisIds the branch never rendered, and bars were dropped." — TRUE (item 8).
  • "xAxis.title is drawn on the category axis, and the yAxis title is laid out for an axis running across the plot, under it at the bottom and over it at the top." — TRUE (item 9).
  • "Grid and annotations bind to the rendered left value axis whenever the value axes carry ids (a combo always; bar, line, area and horizontal-bar with two entries)." — TRUE (:641-645, :1723). "They were bound to an axis id those charts do not render: a two-entry chart's horizontal grid lost its per-tick lines and its axis: 'x' annotations, a combo with fewer than two entries drew no annotation, and a two-entry horizontal-bar dropped its axis: 'y' annotations." — the binding facts are TRUE from the base code (hasDualAxis && !onX ? { yAxisId: 'left' } : {} left an onX annotation and every combo-under-two-entries annotation at id 0, and put a horizontal-bar y annotation on a yAxisId its category axis lacks); the grid-line count is a base measurement I could not re-run, consistent with those semantics and pinned by row 11's control.
  • "A right-hand value axis lays its title out on the far side of its tick labels, the left layout mirrored, as a top x axis mirrors the bottom one." — TRUE (:517).
  • "User-visible render change for a chart whose yAxis entries declare position, a horizontal-bar with an axis title or two yAxis entries, the grid and annotations of two-entry and combo charts, and the title of every right-hand value axis." — TRUE and complete.
  • "ChartRenderer.yAxisPosition-10654.test.tsx pins each row against its bar control." — TRUE.

Docs prose (plugin-charts.mdx:241, the yAxis sentences of the position row), sentence by sentence:

  • "On yAxis it is the side of that entry's value axis." — TRUE.
  • "The open sides are left and right on bar (column), line, area, combo and scatter charts (a scatter chart draws one y axis, the first entry's), and bottom and top on a horizontal-bar chart, whose value axes run across the plot; there a series' yAxis: 'left' binds the bottom axis and 'right' the top one." — TRUE.
  • "A lone entry's axis is drawn on the side it names." — TRUE (on combo the named axis carries the entry's config; combo always draws both axes, as the Axis-options intro already says).
  • "With two entries each is drawn on the side it names, and an entry that names no side takes the side the other left free; when both name the same side, the first keeps it and the second is drawn on the other side, with a note under the plot naming yAxis[1].position." — TRUE.
  • "Any other side places the entry as if it named none, and the chart shows a note under the plot naming yAxis[N].position (objectui#10654)." — TRUE for every placed entry. Observation, not a falsehood: a second scatter entry is sliced away before placement (:1713), so a position on it draws nothing and notes nothing; the same sentence's parenthetical already says scatter draws only the first entry's axis. The x-side half of the row is unchanged and still true.

Other yAxis / right-axis prose at the head, verified as default-case and still true: the Axis-options intro (mdx :231 "a second entry declares the right-hand axis") and the dual-axis example comment (mdx :259), content/docs/api/schema-reference.md:513, packages/types/src/data-display.ts:2059-2062 and :2184-2187, packages/types/src/objectql.ts:4530-4533, packages/types/src/zod/data-display.zod.ts:731, :841 and :993-995, .changeset/7690-chart-axis-config-declared.md:13-14, .changeset/7546-chart-series-keys-declared.md:37 ("yAxis binds the secondary axis" is the series key, still true). None mentions position; with neither entry naming a side the second still draws on the right (:508), so each holds as the default case. .changeset/7694-… carries no y-axis sentence. Nothing becomes false.

③ Boundary flags

  • PR body flags and open questions. "Not a ChartRefusal, no console warning, and no schema narrowing" — confirmed (note channel only, no schema edit). "Rejected: the second entry wins; both refused to defaults; index-only placement" — the chosen rule is judged right in item 1. Acceptance notes: check:doc-snippets / check:doc-examples not measured locally — CI's Doc Snippet Type Check and Doc Component Type Check are green at the head; no real-browser render — the pins read jsdom geometry with zero text metrics, so the category-axis title's clearance from long labels on horizontal-bar is unmeasured (noted, not a contract breach); default-case prose left as is — verified above; third-and-later entries unplaced — pre-existing, out of the rows, reported.
  • CI at the head 2fd09e7. First read: 42 runs, 29 success, 3 skipped, 10 in progress. Re-poll 1: 43 runs, 38 success, 3 skipped, 2 in progress (Test aggregate, Spec Main Shape Gate). Re-poll 2 (final): 43 runs, 40 success, 3 skipped (dependabot, the two coverage matrix entries), 0 in progress, 0 failed. All eight Test (shard N/8), Test (dist pins), Type Check, Lint, Changeset Declaration, Changeset Bump Policy, Changeset Claim Re-read, Changeset Overwrite Report, Changeset Fixed Group Check, Build Docs, Internal Docs Link Check, Governed Surface Queue Guard are success.
  • Draft. Yes, draft: true (the dispatched-dev terminal state).
  • Clause-②: no — holds. normalizeAxis still keeps exactly the four enum sides (normalizeChartSchema.ts:326-329); no zod or spec schema is edited; the published barrel's exports are unchanged (normalizeChartSchema and the NormalizedChartSchema type, whose shape is unchanged); the four new symbols in normalizeChartSchema.ts are not re-exported. The behaviour of normalizeChartSchema's output (derived series bindings) changes, which is the fix, not a surface move.
  • Main moved since the merge-base? One commit, bb6db27 (app-shell Studio publish, objectui#8219): git log 7baede3..refs/review/main -- packages/plugin-charts content/docs/plugins is empty, so main did not move under any of the PR's five files. The head already merges 7baede3; the PR reads mergeable_state: behind, which the queue rebuilds.

Implemented-by: claude/issue-10654-yaxis-position-honoured
Reviewed-by: session_01KUxVUa7e39aNjhkKi1gsoy

VERDICT: PASS

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review September 25, 2026 17:45
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Sep 25, 2026
Merged via the queue into main with commit 4df0f3d Sep 25, 2026
45 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-10654-yaxis-position-honoured branch September 25, 2026 17:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

2 participants