fix(plugin-charts): name the scatter legend's series so its swatch stops reading as a stray point - #7398

Merged
hotlong merged 2 commits into
mainfrom
claude/issue-7248-scatter-y-domain
Sep 2, 2026
Merged

fix(plugin-charts): name the scatter legend's series so its swatch stops reading as a stray point#7398
hotlong merged 2 commits into
mainfrom
claude/issue-7248-scatter-y-domain

Conversation

@hotlong

Copy link
Copy Markdown
Contributor

Fixes#7248

The reported point is not a point — it is the legend swatch

The card reported the Chart Gallery scatter ("Estimate vs Progress") drawing a seventh
point below the x-axis at "x≈40, y≈-10", and asked for the y domain to include every
plotted value. Measured on the running showcase in real Chromium, that diagnosis is
false and the requested fix would have been in the wrong place.

ChartLegendContent resolves a label as config[nameKey || item.dataKey || 'value'].
A Scatter carries no dataKey — scatter's keys live on the XAxis/YAxis, not on
the mark — so the key collapsed to the literal string 'value', missed a config keyed
by measure name, and the legend entry rendered its colour swatch with no text beside
it. An 8x8 square in --chart-1, the same colour as the marks, sitting under the
x-axis.

Captured from recharts 3.10.1 rather than assumed, the scatter's legend payload item is:

{ color: '#3b82f6', type: 'circle', value: 'Avg Estimate',
payload: { name: 'Avg Estimate', ... } } // no `dataKey`

The series name was in value the whole time; the legend never read it.

Browser measurement — before and after

Showcase Chart Gallery, /apps/com.example.showcase/dashboard/showcase_chart_gallery,
real Chromium (Playwright 1.62.1), viewport 1440x1000, widget svg 510x350.

Before — plot area y 5..295, six marks, all inside it:

progress (x)avg_estimate (y)cxcyinside plot?
016.553215.3yes
4540256.4101.7yes
5560301.65yes
8024414.6179yes
9030459.8150yes
10012505237yes

The seventh "point" was the legend swatch: cx 255, cy 341, innerText === "". The
arithmetic closes the identification — the plot bottom (y=0) is cy 295 and the y scale
is 4.835 px per unit, so cy 341 is y = -9.5, at x ≈ 45. That is the card's
"x≈40, y≈-10", to the pixel.

Swept viewport widths 1440, 1100, 980, 900, 820, 760, 700, 640, 560, 480: zero marks
outside the plot area at any width
, so the symptom was never width-dependent either.

After — plot area y 5..287, same six marks all inside (cy 209.5, 99, 5, 174.2, 146,
230.6), and the legend now reads Avg Estimate beside its swatch instead of painting an
anonymous dot.

The y domain was not the defect and is unchanged

Clamping the y domain — the fix the card asked for — would have created the bug it
described. Two adversarial fixtures are pinned in this PR: mixed-sign data
(ym: 40, -15, 25) draws 3 of 3 marks and all-negative data (ym: -40, -15) draws 2 of
2, because recharts already extends the domain to cover negative values. Forcing
[0, 'auto'] is exactly what would push a negative point below the baseline.

The live data was read from the running showcase API and rules out the "bad row"
hypothesis independently: 10 seeded tasks, every progress a finite non-negative
number, no null / negative / string values, aggregating to the six buckets above.

Two changes

  1. AdvancedChartImpl.tsx, scatter branch — pass nameKey={scatterYKey} so the
    legend resolves the measure's config entry, the same way the pie branch already
    passes nameKey={xAxisKey}.

  2. ChartContainerImpl.tsx, ChartLegendContent — fall back to item.value, the
    series name recharts itself put on the legend item, when the config lookup misses.

The second is a deliberate bounded in-place fix rather than a drive-by: it is the same
defect class, at the layer that decides whether a swatch gets a name, and it closes the
class instead of this one instance. The swatch renders unconditionally, so a config
miss must never leave it anonymous. The helper directly below it already documents the
pie form of this same bug ("legend swatches with NO text at all"); that one was closed
by making one family's key resolve, this closes the general case. Charts whose config
already resolves are unaffected — only a currently-empty label can change.

Reverse verification (ablation)

Each half was ablated independently from the committed state, with a restore trap on
absolute paths, the mutation proved on disk by HEAD-blob-hash comparison plus an anchor
count, and the tree confirmed clean (git diff HEAD empty) after every leg. No dist
is involved — the test imports ./AdvancedChartImpl, which imports
./ChartContainerImpl, both relative source paths — so no rebuild leg applies.

ablationmutation landedresult
baseline (both halves)7 passed
remove nameKey only640e2290 → 52a84b18, anchor count 01 red — the config-resolution pin
remove ?? item.value only117306ba → 525ac855, anchor count 02 red
remove both (origin/main behaviour)both, anchor counts 04 red, incl. the headline assertion

The first run of this ablation found a real problem and changed the PR: removing
nameKey alone left all assertions green, because the item.value fallback masks
it on the label. Rather than ship an unenforced change, a pin was added for the one
branch the fallback cannot stand in for — itemConfig.icon, which only resolves when
the config lookup hits, so a config-declared legend icon is silently ignored for scatter
without nameKey while every other family honours it. That pin is the single red in
ablation 1 above.

Per-facet answers to the dispatch

Gates

Run on the final commit d2537532b:

  • pnpm exec vitest run packages/plugin-charts/ --maxWorkers=242 files, 390 tests
    passed
    (run from the repo root; the package-scoped pnpm --filter ... test form is
    the silent-green trap AGENTS.md bans)
  • pnpm --filter @object-ui/plugin-charts type-check — exit 0. tsc --listFiles
    confirms both edited files and the new test are inside the program, so this green
    actually covers the diff rather than excluding the test file
  • pnpm exec eslint . in packages/plugin-charts — exit 0, 0 errors, 318 pre-existing
    warnings, 55 files linted (count read from --format json), all three touched
    files present in the linted set
  • changeset: .changeset/7248-scatter-legend-names-its-series.md, @object-ui/plugin-charts patch

Declared narrowing. Lint was run at package scope rather than repo-wide. The
population comes from eslint's own config resolution, the count from --format json
(55 files), and the config is not type-aware (no parserOptions.project /
projectService), so this diff cannot move the verdict on any untouched file. CI runs
the full farm regardless.

Verify-lock declaration, verbatim from the tool on every heavy run:

os-verify-lock: VERDICT command-exit 0 · UNLOCKED (declared) · no usable `flock` on this
host, so the shared verify lock was NEVER taken and NOTHING was serialized

Out-of-scope findings filed

  • objectui#7396 — a scatter's extreme points are half-clipped by the plot edge (the
    numeric axes get no domain padding). Measured here: plot x 53..505 with marks at
    cx 53 and cx 505, so about half of each extreme symbol paints outside the plot area.
    Real, different mechanism, and changing it would alter every scatter's appearance.
  • objectui#7397 — packages/components/src/ui/chart.tsx is an unreferenced duplicate of
    the plugin-charts chart primitives carrying this same legend hole. Imported by
    nothing and absent from the barrel, so it is currently unreachable; left untouched
    here rather than widening this PR across a second published package.

Generated by Claude Code

hotlongand others added 2 commits September 3, 2026 01:20
…ops reading as a stray point (#7248)
The Chart Gallery scatter appeared to draw a seventh point below the x-axis.
It was the legend swatch: ChartLegendContent resolves a label as
config[nameKey || item.dataKey || 'value'], and a <Scatter> carries no
dataKey, so the key collapsed to 'value', missed the config and rendered a
colour swatch with no text.
Measured in real Chromium: swatch at cy 341 against a plot area ending at
cy 295, scale 4.835px/unit -> y = -9.5 at x ~ 45, matching the report's
'x~40, y~-10'. All six real marks were inside the plot area at every
viewport width from 1440 down to 480, so the y domain is left untouched.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Ablation showed removing nameKey alone left every assertion green: the
item.value fallback masks it on the label. The config lookup is still
load-bearing for itemConfig.icon, which the fallback cannot stand in for,
so pin that branch instead of shipping an unenforced change.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 48 chunks)3177.2 KB3191.4 KB
Main entry chunk (gzip)143.2 KB350 KB
Entry fileindex-CDQS-v_Q.js
StatusPASS

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

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)15.67KB5.75KB
app-shell (runtime-config.js)20.68KB7.36KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)514.87KB117.50KB
core (index.js)5.80KB2.32KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)178.20KB49.60KB
fields (index.js)244.08KB61.71KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.98KB10.98KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)47.00KB12.97KB
plugin-charts (index.js)70.31KB19.55KB
plugin-chatbot (index.js)196.19KB46.43KB
plugin-dashboard (index.js)132.63KB34.56KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)251.20KB64.18KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)132.87KB32.66KB
plugin-gantt (index.js)166.93KB40.82KB
plugin-grid (index.js)209.10KB56.65KB
plugin-kanban (index.js)53.21KB14.66KB
plugin-list (index.js)113.51KB27.67KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)30.21KB8.66KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.90KB21.12KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)81.07KB26.86KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.74KB2.25KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)8.11KB3.32KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

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

@hotlong
hotlong marked this pull request as ready for review September 2, 2026 17:43
@hotlong
hotlong added this pull request to the merge queueSep 2, 2026
Merged via the queue into main with commit bb459eaSep 2, 2026
32 checks passed
@hotlong
hotlong deleted the claude/issue-7248-scatter-y-domain branch September 2, 2026 17:58
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

plugin-charts: the Chart Gallery scatter ("Estimate vs Progress") draws one point below the x-axis, outside the plot area

1 participant

@hotlong
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

fix(plugin-charts): name the scatter legend's series so its swatch stops reading as a stray point - #7398

Merged
hotlong merged 2 commits into
mainfrom
claude/issue-7248-scatter-y-domain
Sep 2, 2026
Merged

fix(plugin-charts): name the scatter legend's series so its swatch stops reading as a stray point#7398
hotlong merged 2 commits into
mainfrom
claude/issue-7248-scatter-y-domain

Conversation

@hotlong

Copy link
Copy Markdown
Contributor

Fixes#7248

The reported point is not a point — it is the legend swatch

The card reported the Chart Gallery scatter ("Estimate vs Progress") drawing a seventh
point below the x-axis at "x≈40, y≈-10", and asked for the y domain to include every
plotted value. Measured on the running showcase in real Chromium, that diagnosis is
false and the requested fix would have been in the wrong place.

ChartLegendContent resolves a label as config[nameKey || item.dataKey || 'value'].
A Scatter carries no dataKey — scatter's keys live on the XAxis/YAxis, not on
the mark — so the key collapsed to the literal string 'value', missed a config keyed
by measure name, and the legend entry rendered its colour swatch with no text beside
it. An 8x8 square in --chart-1, the same colour as the marks, sitting under the
x-axis.

Captured from recharts 3.10.1 rather than assumed, the scatter's legend payload item is:

{ color: '#3b82f6', type: 'circle', value: 'Avg Estimate',
payload: { name: 'Avg Estimate', ... } } // no `dataKey`

The series name was in value the whole time; the legend never read it.

Browser measurement — before and after

Showcase Chart Gallery, /apps/com.example.showcase/dashboard/showcase_chart_gallery,
real Chromium (Playwright 1.62.1), viewport 1440x1000, widget svg 510x350.

Before — plot area y 5..295, six marks, all inside it:

progress (x)avg_estimate (y)cxcyinside plot?
016.553215.3yes
4540256.4101.7yes
5560301.65yes
8024414.6179yes
9030459.8150yes
10012505237yes

The seventh "point" was the legend swatch: cx 255, cy 341, innerText === "". The
arithmetic closes the identification — the plot bottom (y=0) is cy 295 and the y scale
is 4.835 px per unit, so cy 341 is y = -9.5, at x ≈ 45. That is the card's
"x≈40, y≈-10", to the pixel.

Swept viewport widths 1440, 1100, 980, 900, 820, 760, 700, 640, 560, 480: zero marks
outside the plot area at any width
, so the symptom was never width-dependent either.

After — plot area y 5..287, same six marks all inside (cy 209.5, 99, 5, 174.2, 146,
230.6), and the legend now reads Avg Estimate beside its swatch instead of painting an
anonymous dot.

The y domain was not the defect and is unchanged

Clamping the y domain — the fix the card asked for — would have created the bug it
described. Two adversarial fixtures are pinned in this PR: mixed-sign data
(ym: 40, -15, 25) draws 3 of 3 marks and all-negative data (ym: -40, -15) draws 2 of
2, because recharts already extends the domain to cover negative values. Forcing
[0, 'auto'] is exactly what would push a negative point below the baseline.

The live data was read from the running showcase API and rules out the "bad row"
hypothesis independently: 10 seeded tasks, every progress a finite non-negative
number, no null / negative / string values, aggregating to the six buckets above.

Two changes

  1. AdvancedChartImpl.tsx, scatter branch — pass nameKey={scatterYKey} so the
    legend resolves the measure's config entry, the same way the pie branch already
    passes nameKey={xAxisKey}.

  2. ChartContainerImpl.tsx, ChartLegendContent — fall back to item.value, the
    series name recharts itself put on the legend item, when the config lookup misses.

The second is a deliberate bounded in-place fix rather than a drive-by: it is the same
defect class, at the layer that decides whether a swatch gets a name, and it closes the
class instead of this one instance. The swatch renders unconditionally, so a config
miss must never leave it anonymous. The helper directly below it already documents the
pie form of this same bug ("legend swatches with NO text at all"); that one was closed
by making one family's key resolve, this closes the general case. Charts whose config
already resolves are unaffected — only a currently-empty label can change.

Reverse verification (ablation)

Each half was ablated independently from the committed state, with a restore trap on
absolute paths, the mutation proved on disk by HEAD-blob-hash comparison plus an anchor
count, and the tree confirmed clean (git diff HEAD empty) after every leg. No dist
is involved — the test imports ./AdvancedChartImpl, which imports
./ChartContainerImpl, both relative source paths — so no rebuild leg applies.

ablationmutation landedresult
baseline (both halves)7 passed
remove nameKey only640e2290 → 52a84b18, anchor count 01 red — the config-resolution pin
remove ?? item.value only117306ba → 525ac855, anchor count 02 red
remove both (origin/main behaviour)both, anchor counts 04 red, incl. the headline assertion

The first run of this ablation found a real problem and changed the PR: removing
nameKey alone left all assertions green, because the item.value fallback masks
it on the label. Rather than ship an unenforced change, a pin was added for the one
branch the fallback cannot stand in for — itemConfig.icon, which only resolves when
the config lookup hits, so a config-declared legend icon is silently ignored for scatter
without nameKey while every other family honours it. That pin is the single red in
ablation 1 above.

Per-facet answers to the dispatch

Gates

Run on the final commit d2537532b:

  • pnpm exec vitest run packages/plugin-charts/ --maxWorkers=242 files, 390 tests
    passed
    (run from the repo root; the package-scoped pnpm --filter ... test form is
    the silent-green trap AGENTS.md bans)
  • pnpm --filter @object-ui/plugin-charts type-check — exit 0. tsc --listFiles
    confirms both edited files and the new test are inside the program, so this green
    actually covers the diff rather than excluding the test file
  • pnpm exec eslint . in packages/plugin-charts — exit 0, 0 errors, 318 pre-existing
    warnings, 55 files linted (count read from --format json), all three touched
    files present in the linted set
  • changeset: .changeset/7248-scatter-legend-names-its-series.md, @object-ui/plugin-charts patch

Declared narrowing. Lint was run at package scope rather than repo-wide. The
population comes from eslint's own config resolution, the count from --format json
(55 files), and the config is not type-aware (no parserOptions.project /
projectService), so this diff cannot move the verdict on any untouched file. CI runs
the full farm regardless.

Verify-lock declaration, verbatim from the tool on every heavy run:

os-verify-lock: VERDICT command-exit 0 · UNLOCKED (declared) · no usable `flock` on this
host, so the shared verify lock was NEVER taken and NOTHING was serialized

Out-of-scope findings filed

  • objectui#7396 — a scatter's extreme points are half-clipped by the plot edge (the
    numeric axes get no domain padding). Measured here: plot x 53..505 with marks at
    cx 53 and cx 505, so about half of each extreme symbol paints outside the plot area.
    Real, different mechanism, and changing it would alter every scatter's appearance.
  • objectui#7397 — packages/components/src/ui/chart.tsx is an unreferenced duplicate of
    the plugin-charts chart primitives carrying this same legend hole. Imported by
    nothing and absent from the barrel, so it is currently unreachable; left untouched
    here rather than widening this PR across a second published package.

Generated by Claude Code

hotlongand others added 2 commits September 3, 2026 01:20
…ops reading as a stray point (#7248)
The Chart Gallery scatter appeared to draw a seventh point below the x-axis.
It was the legend swatch: ChartLegendContent resolves a label as
config[nameKey || item.dataKey || 'value'], and a <Scatter> carries no
dataKey, so the key collapsed to 'value', missed the config and rendered a
colour swatch with no text.
Measured in real Chromium: swatch at cy 341 against a plot area ending at
cy 295, scale 4.835px/unit -> y = -9.5 at x ~ 45, matching the report's
'x~40, y~-10'. All six real marks were inside the plot area at every
viewport width from 1440 down to 480, so the y domain is left untouched.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Ablation showed removing nameKey alone left every assertion green: the
item.value fallback masks it on the label. The config lookup is still
load-bearing for itemConfig.icon, which the fallback cannot stand in for,
so pin that branch instead of shipping an unenforced change.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 48 chunks)3177.2 KB3191.4 KB
Main entry chunk (gzip)143.2 KB350 KB
Entry fileindex-CDQS-v_Q.js
StatusPASS

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

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)15.67KB5.75KB
app-shell (runtime-config.js)20.68KB7.36KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)514.87KB117.50KB
core (index.js)5.80KB2.32KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)178.20KB49.60KB
fields (index.js)244.08KB61.71KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.98KB10.98KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)47.00KB12.97KB
plugin-charts (index.js)70.31KB19.55KB
plugin-chatbot (index.js)196.19KB46.43KB
plugin-dashboard (index.js)132.63KB34.56KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)251.20KB64.18KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)132.87KB32.66KB
plugin-gantt (index.js)166.93KB40.82KB
plugin-grid (index.js)209.10KB56.65KB
plugin-kanban (index.js)53.21KB14.66KB
plugin-list (index.js)113.51KB27.67KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)30.21KB8.66KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.90KB21.12KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)81.07KB26.86KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.74KB2.25KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)8.11KB3.32KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

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

@hotlong
hotlong marked this pull request as ready for review September 2, 2026 17:43
@hotlong
hotlong added this pull request to the merge queueSep 2, 2026
Merged via the queue into main with commit bb459eaSep 2, 2026
32 checks passed
@hotlong
hotlong deleted the claude/issue-7248-scatter-y-domain branch September 2, 2026 17:58
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

plugin-charts: the Chart Gallery scatter ("Estimate vs Progress") draws one point below the x-axis, outside the plot area

1 participant

@hotlong
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(plugin-charts): name the scatter legend's series so its swatch stops reading as a stray point - #7398

Merged
hotlong merged 2 commits into
mainfrom
claude/issue-7248-scatter-y-domain
Sep 2, 2026
Merged

fix(plugin-charts): name the scatter legend's series so its swatch stops reading as a stray point#7398
hotlong merged 2 commits into
mainfrom
claude/issue-7248-scatter-y-domain

Conversation

@hotlong

Copy link
Copy Markdown
Contributor

Fixes#7248

The reported point is not a point — it is the legend swatch

The card reported the Chart Gallery scatter ("Estimate vs Progress") drawing a seventh
point below the x-axis at "x≈40, y≈-10", and asked for the y domain to include every
plotted value. Measured on the running showcase in real Chromium, that diagnosis is
false and the requested fix would have been in the wrong place.

ChartLegendContent resolves a label as config[nameKey || item.dataKey || 'value'].
A Scatter carries no dataKey — scatter's keys live on the XAxis/YAxis, not on
the mark — so the key collapsed to the literal string 'value', missed a config keyed
by measure name, and the legend entry rendered its colour swatch with no text beside
it. An 8x8 square in --chart-1, the same colour as the marks, sitting under the
x-axis.

Captured from recharts 3.10.1 rather than assumed, the scatter's legend payload item is:

{ color: '#3b82f6', type: 'circle', value: 'Avg Estimate',
payload: { name: 'Avg Estimate', ... } } // no `dataKey`

The series name was in value the whole time; the legend never read it.

Browser measurement — before and after

Showcase Chart Gallery, /apps/com.example.showcase/dashboard/showcase_chart_gallery,
real Chromium (Playwright 1.62.1), viewport 1440x1000, widget svg 510x350.

Before — plot area y 5..295, six marks, all inside it:

progress (x)avg_estimate (y)cxcyinside plot?
016.553215.3yes
4540256.4101.7yes
5560301.65yes
8024414.6179yes
9030459.8150yes
10012505237yes

The seventh "point" was the legend swatch: cx 255, cy 341, innerText === "". The
arithmetic closes the identification — the plot bottom (y=0) is cy 295 and the y scale
is 4.835 px per unit, so cy 341 is y = -9.5, at x ≈ 45. That is the card's
"x≈40, y≈-10", to the pixel.

Swept viewport widths 1440, 1100, 980, 900, 820, 760, 700, 640, 560, 480: zero marks
outside the plot area at any width
, so the symptom was never width-dependent either.

After — plot area y 5..287, same six marks all inside (cy 209.5, 99, 5, 174.2, 146,
230.6), and the legend now reads Avg Estimate beside its swatch instead of painting an
anonymous dot.

The y domain was not the defect and is unchanged

Clamping the y domain — the fix the card asked for — would have created the bug it
described. Two adversarial fixtures are pinned in this PR: mixed-sign data
(ym: 40, -15, 25) draws 3 of 3 marks and all-negative data (ym: -40, -15) draws 2 of
2, because recharts already extends the domain to cover negative values. Forcing
[0, 'auto'] is exactly what would push a negative point below the baseline.

The live data was read from the running showcase API and rules out the "bad row"
hypothesis independently: 10 seeded tasks, every progress a finite non-negative
number, no null / negative / string values, aggregating to the six buckets above.

Two changes

  1. AdvancedChartImpl.tsx, scatter branch — pass nameKey={scatterYKey} so the
    legend resolves the measure's config entry, the same way the pie branch already
    passes nameKey={xAxisKey}.

  2. ChartContainerImpl.tsx, ChartLegendContent — fall back to item.value, the
    series name recharts itself put on the legend item, when the config lookup misses.

The second is a deliberate bounded in-place fix rather than a drive-by: it is the same
defect class, at the layer that decides whether a swatch gets a name, and it closes the
class instead of this one instance. The swatch renders unconditionally, so a config
miss must never leave it anonymous. The helper directly below it already documents the
pie form of this same bug ("legend swatches with NO text at all"); that one was closed
by making one family's key resolve, this closes the general case. Charts whose config
already resolves are unaffected — only a currently-empty label can change.

Reverse verification (ablation)

Each half was ablated independently from the committed state, with a restore trap on
absolute paths, the mutation proved on disk by HEAD-blob-hash comparison plus an anchor
count, and the tree confirmed clean (git diff HEAD empty) after every leg. No dist
is involved — the test imports ./AdvancedChartImpl, which imports
./ChartContainerImpl, both relative source paths — so no rebuild leg applies.

ablationmutation landedresult
baseline (both halves)7 passed
remove nameKey only640e2290 → 52a84b18, anchor count 01 red — the config-resolution pin
remove ?? item.value only117306ba → 525ac855, anchor count 02 red
remove both (origin/main behaviour)both, anchor counts 04 red, incl. the headline assertion

The first run of this ablation found a real problem and changed the PR: removing
nameKey alone left all assertions green, because the item.value fallback masks
it on the label. Rather than ship an unenforced change, a pin was added for the one
branch the fallback cannot stand in for — itemConfig.icon, which only resolves when
the config lookup hits, so a config-declared legend icon is silently ignored for scatter
without nameKey while every other family honours it. That pin is the single red in
ablation 1 above.

Per-facet answers to the dispatch

Gates

Run on the final commit d2537532b:

  • pnpm exec vitest run packages/plugin-charts/ --maxWorkers=242 files, 390 tests
    passed
    (run from the repo root; the package-scoped pnpm --filter ... test form is
    the silent-green trap AGENTS.md bans)
  • pnpm --filter @object-ui/plugin-charts type-check — exit 0. tsc --listFiles
    confirms both edited files and the new test are inside the program, so this green
    actually covers the diff rather than excluding the test file
  • pnpm exec eslint . in packages/plugin-charts — exit 0, 0 errors, 318 pre-existing
    warnings, 55 files linted (count read from --format json), all three touched
    files present in the linted set
  • changeset: .changeset/7248-scatter-legend-names-its-series.md, @object-ui/plugin-charts patch

Declared narrowing. Lint was run at package scope rather than repo-wide. The
population comes from eslint's own config resolution, the count from --format json
(55 files), and the config is not type-aware (no parserOptions.project /
projectService), so this diff cannot move the verdict on any untouched file. CI runs
the full farm regardless.

Verify-lock declaration, verbatim from the tool on every heavy run:

os-verify-lock: VERDICT command-exit 0 · UNLOCKED (declared) · no usable `flock` on this
host, so the shared verify lock was NEVER taken and NOTHING was serialized

Out-of-scope findings filed

  • objectui#7396 — a scatter's extreme points are half-clipped by the plot edge (the
    numeric axes get no domain padding). Measured here: plot x 53..505 with marks at
    cx 53 and cx 505, so about half of each extreme symbol paints outside the plot area.
    Real, different mechanism, and changing it would alter every scatter's appearance.
  • objectui#7397 — packages/components/src/ui/chart.tsx is an unreferenced duplicate of
    the plugin-charts chart primitives carrying this same legend hole. Imported by
    nothing and absent from the barrel, so it is currently unreachable; left untouched
    here rather than widening this PR across a second published package.

Generated by Claude Code

hotlongand others added 2 commits September 3, 2026 01:20
…ops reading as a stray point (#7248)
The Chart Gallery scatter appeared to draw a seventh point below the x-axis.
It was the legend swatch: ChartLegendContent resolves a label as
config[nameKey || item.dataKey || 'value'], and a <Scatter> carries no
dataKey, so the key collapsed to 'value', missed the config and rendered a
colour swatch with no text.
Measured in real Chromium: swatch at cy 341 against a plot area ending at
cy 295, scale 4.835px/unit -> y = -9.5 at x ~ 45, matching the report's
'x~40, y~-10'. All six real marks were inside the plot area at every
viewport width from 1440 down to 480, so the y domain is left untouched.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Ablation showed removing nameKey alone left every assertion green: the
item.value fallback masks it on the label. The config lookup is still
load-bearing for itemConfig.icon, which the fallback cannot stand in for,
so pin that branch instead of shipping an unenforced change.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 48 chunks)3177.2 KB3191.4 KB
Main entry chunk (gzip)143.2 KB350 KB
Entry fileindex-CDQS-v_Q.js
StatusPASS

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

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)15.67KB5.75KB
app-shell (runtime-config.js)20.68KB7.36KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)514.87KB117.50KB
core (index.js)5.80KB2.32KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)178.20KB49.60KB
fields (index.js)244.08KB61.71KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.98KB10.98KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)47.00KB12.97KB
plugin-charts (index.js)70.31KB19.55KB
plugin-chatbot (index.js)196.19KB46.43KB
plugin-dashboard (index.js)132.63KB34.56KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)251.20KB64.18KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)132.87KB32.66KB
plugin-gantt (index.js)166.93KB40.82KB
plugin-grid (index.js)209.10KB56.65KB
plugin-kanban (index.js)53.21KB14.66KB
plugin-list (index.js)113.51KB27.67KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)30.21KB8.66KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.90KB21.12KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)81.07KB26.86KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.74KB2.25KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)8.11KB3.32KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

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

@hotlong
hotlong marked this pull request as ready for review September 2, 2026 17:43
@hotlong
hotlong added this pull request to the merge queueSep 2, 2026
Merged via the queue into main with commit bb459eaSep 2, 2026
32 checks passed
@hotlong
hotlong deleted the claude/issue-7248-scatter-y-domain branch September 2, 2026 17:58
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

plugin-charts: the Chart Gallery scatter ("Estimate vs Progress") draws one point below the x-axis, outside the plot area

1 participant

@hotlong
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(plugin-charts): name the scatter legend's series so its swatch stops reading as a stray point - #7398

Merged
hotlong merged 2 commits into
mainfrom
claude/issue-7248-scatter-y-domain
Sep 2, 2026
Merged

fix(plugin-charts): name the scatter legend's series so its swatch stops reading as a stray point#7398
hotlong merged 2 commits into
mainfrom
claude/issue-7248-scatter-y-domain

Conversation

@hotlong

Copy link
Copy Markdown
Contributor

Fixes#7248

The reported point is not a point — it is the legend swatch

The card reported the Chart Gallery scatter ("Estimate vs Progress") drawing a seventh
point below the x-axis at "x≈40, y≈-10", and asked for the y domain to include every
plotted value. Measured on the running showcase in real Chromium, that diagnosis is
false and the requested fix would have been in the wrong place.

ChartLegendContent resolves a label as config[nameKey || item.dataKey || 'value'].
A Scatter carries no dataKey — scatter's keys live on the XAxis/YAxis, not on
the mark — so the key collapsed to the literal string 'value', missed a config keyed
by measure name, and the legend entry rendered its colour swatch with no text beside
it. An 8x8 square in --chart-1, the same colour as the marks, sitting under the
x-axis.

Captured from recharts 3.10.1 rather than assumed, the scatter's legend payload item is:

{ color: '#3b82f6', type: 'circle', value: 'Avg Estimate',
payload: { name: 'Avg Estimate', ... } } // no `dataKey`

The series name was in value the whole time; the legend never read it.

Browser measurement — before and after

Showcase Chart Gallery, /apps/com.example.showcase/dashboard/showcase_chart_gallery,
real Chromium (Playwright 1.62.1), viewport 1440x1000, widget svg 510x350.

Before — plot area y 5..295, six marks, all inside it:

progress (x)avg_estimate (y)cxcyinside plot?
016.553215.3yes
4540256.4101.7yes
5560301.65yes
8024414.6179yes
9030459.8150yes
10012505237yes

The seventh "point" was the legend swatch: cx 255, cy 341, innerText === "". The
arithmetic closes the identification — the plot bottom (y=0) is cy 295 and the y scale
is 4.835 px per unit, so cy 341 is y = -9.5, at x ≈ 45. That is the card's
"x≈40, y≈-10", to the pixel.

Swept viewport widths 1440, 1100, 980, 900, 820, 760, 700, 640, 560, 480: zero marks
outside the plot area at any width
, so the symptom was never width-dependent either.

After — plot area y 5..287, same six marks all inside (cy 209.5, 99, 5, 174.2, 146,
230.6), and the legend now reads Avg Estimate beside its swatch instead of painting an
anonymous dot.

The y domain was not the defect and is unchanged

Clamping the y domain — the fix the card asked for — would have created the bug it
described. Two adversarial fixtures are pinned in this PR: mixed-sign data
(ym: 40, -15, 25) draws 3 of 3 marks and all-negative data (ym: -40, -15) draws 2 of
2, because recharts already extends the domain to cover negative values. Forcing
[0, 'auto'] is exactly what would push a negative point below the baseline.

The live data was read from the running showcase API and rules out the "bad row"
hypothesis independently: 10 seeded tasks, every progress a finite non-negative
number, no null / negative / string values, aggregating to the six buckets above.

Two changes

  1. AdvancedChartImpl.tsx, scatter branch — pass nameKey={scatterYKey} so the
    legend resolves the measure's config entry, the same way the pie branch already
    passes nameKey={xAxisKey}.

  2. ChartContainerImpl.tsx, ChartLegendContent — fall back to item.value, the
    series name recharts itself put on the legend item, when the config lookup misses.

The second is a deliberate bounded in-place fix rather than a drive-by: it is the same
defect class, at the layer that decides whether a swatch gets a name, and it closes the
class instead of this one instance. The swatch renders unconditionally, so a config
miss must never leave it anonymous. The helper directly below it already documents the
pie form of this same bug ("legend swatches with NO text at all"); that one was closed
by making one family's key resolve, this closes the general case. Charts whose config
already resolves are unaffected — only a currently-empty label can change.

Reverse verification (ablation)

Each half was ablated independently from the committed state, with a restore trap on
absolute paths, the mutation proved on disk by HEAD-blob-hash comparison plus an anchor
count, and the tree confirmed clean (git diff HEAD empty) after every leg. No dist
is involved — the test imports ./AdvancedChartImpl, which imports
./ChartContainerImpl, both relative source paths — so no rebuild leg applies.

ablationmutation landedresult
baseline (both halves)7 passed
remove nameKey only640e2290 → 52a84b18, anchor count 01 red — the config-resolution pin
remove ?? item.value only117306ba → 525ac855, anchor count 02 red
remove both (origin/main behaviour)both, anchor counts 04 red, incl. the headline assertion

The first run of this ablation found a real problem and changed the PR: removing
nameKey alone left all assertions green, because the item.value fallback masks
it on the label. Rather than ship an unenforced change, a pin was added for the one
branch the fallback cannot stand in for — itemConfig.icon, which only resolves when
the config lookup hits, so a config-declared legend icon is silently ignored for scatter
without nameKey while every other family honours it. That pin is the single red in
ablation 1 above.

Per-facet answers to the dispatch

Gates

Run on the final commit d2537532b:

  • pnpm exec vitest run packages/plugin-charts/ --maxWorkers=242 files, 390 tests
    passed
    (run from the repo root; the package-scoped pnpm --filter ... test form is
    the silent-green trap AGENTS.md bans)
  • pnpm --filter @object-ui/plugin-charts type-check — exit 0. tsc --listFiles
    confirms both edited files and the new test are inside the program, so this green
    actually covers the diff rather than excluding the test file
  • pnpm exec eslint . in packages/plugin-charts — exit 0, 0 errors, 318 pre-existing
    warnings, 55 files linted (count read from --format json), all three touched
    files present in the linted set
  • changeset: .changeset/7248-scatter-legend-names-its-series.md, @object-ui/plugin-charts patch

Declared narrowing. Lint was run at package scope rather than repo-wide. The
population comes from eslint's own config resolution, the count from --format json
(55 files), and the config is not type-aware (no parserOptions.project /
projectService), so this diff cannot move the verdict on any untouched file. CI runs
the full farm regardless.

Verify-lock declaration, verbatim from the tool on every heavy run:

os-verify-lock: VERDICT command-exit 0 · UNLOCKED (declared) · no usable `flock` on this
host, so the shared verify lock was NEVER taken and NOTHING was serialized

Out-of-scope findings filed

  • objectui#7396 — a scatter's extreme points are half-clipped by the plot edge (the
    numeric axes get no domain padding). Measured here: plot x 53..505 with marks at
    cx 53 and cx 505, so about half of each extreme symbol paints outside the plot area.
    Real, different mechanism, and changing it would alter every scatter's appearance.
  • objectui#7397 — packages/components/src/ui/chart.tsx is an unreferenced duplicate of
    the plugin-charts chart primitives carrying this same legend hole. Imported by
    nothing and absent from the barrel, so it is currently unreachable; left untouched
    here rather than widening this PR across a second published package.

Generated by Claude Code

hotlongand others added 2 commits September 3, 2026 01:20
…ops reading as a stray point (#7248)
The Chart Gallery scatter appeared to draw a seventh point below the x-axis.
It was the legend swatch: ChartLegendContent resolves a label as
config[nameKey || item.dataKey || 'value'], and a <Scatter> carries no
dataKey, so the key collapsed to 'value', missed the config and rendered a
colour swatch with no text.
Measured in real Chromium: swatch at cy 341 against a plot area ending at
cy 295, scale 4.835px/unit -> y = -9.5 at x ~ 45, matching the report's
'x~40, y~-10'. All six real marks were inside the plot area at every
viewport width from 1440 down to 480, so the y domain is left untouched.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Ablation showed removing nameKey alone left every assertion green: the
item.value fallback masks it on the label. The config lookup is still
load-bearing for itemConfig.icon, which the fallback cannot stand in for,
so pin that branch instead of shipping an unenforced change.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 48 chunks)3177.2 KB3191.4 KB
Main entry chunk (gzip)143.2 KB350 KB
Entry fileindex-CDQS-v_Q.js
StatusPASS

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

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)15.67KB5.75KB
app-shell (runtime-config.js)20.68KB7.36KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)514.87KB117.50KB
core (index.js)5.80KB2.32KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)178.20KB49.60KB
fields (index.js)244.08KB61.71KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.98KB10.98KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)47.00KB12.97KB
plugin-charts (index.js)70.31KB19.55KB
plugin-chatbot (index.js)196.19KB46.43KB
plugin-dashboard (index.js)132.63KB34.56KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)251.20KB64.18KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)132.87KB32.66KB
plugin-gantt (index.js)166.93KB40.82KB
plugin-grid (index.js)209.10KB56.65KB
plugin-kanban (index.js)53.21KB14.66KB
plugin-list (index.js)113.51KB27.67KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)30.21KB8.66KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.90KB21.12KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)81.07KB26.86KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.74KB2.25KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)8.11KB3.32KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

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

@hotlong
hotlong marked this pull request as ready for review September 2, 2026 17:43
@hotlong
hotlong added this pull request to the merge queueSep 2, 2026
Merged via the queue into main with commit bb459eaSep 2, 2026
32 checks passed
@hotlong
hotlong deleted the claude/issue-7248-scatter-y-domain branch September 2, 2026 17:58
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

plugin-charts: the Chart Gallery scatter ("Estimate vs Progress") draws one point below the x-axis, outside the plot area

1 participant

@hotlong
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

fix(plugin-charts): name the scatter legend's series so its swatch stops reading as a stray point - #7398

Merged
hotlong merged 2 commits into
mainfrom
claude/issue-7248-scatter-y-domain
Sep 2, 2026
Merged

fix(plugin-charts): name the scatter legend's series so its swatch stops reading as a stray point#7398
hotlong merged 2 commits into
mainfrom
claude/issue-7248-scatter-y-domain

Conversation

@hotlong

Copy link
Copy Markdown
Contributor

Fixes#7248

The reported point is not a point — it is the legend swatch

The card reported the Chart Gallery scatter ("Estimate vs Progress") drawing a seventh
point below the x-axis at "x≈40, y≈-10", and asked for the y domain to include every
plotted value. Measured on the running showcase in real Chromium, that diagnosis is
false and the requested fix would have been in the wrong place.

ChartLegendContent resolves a label as config[nameKey || item.dataKey || 'value'].
A Scatter carries no dataKey — scatter's keys live on the XAxis/YAxis, not on
the mark — so the key collapsed to the literal string 'value', missed a config keyed
by measure name, and the legend entry rendered its colour swatch with no text beside
it. An 8x8 square in --chart-1, the same colour as the marks, sitting under the
x-axis.

Captured from recharts 3.10.1 rather than assumed, the scatter's legend payload item is:

{ color: '#3b82f6', type: 'circle', value: 'Avg Estimate',
payload: { name: 'Avg Estimate', ... } } // no `dataKey`

The series name was in value the whole time; the legend never read it.

Browser measurement — before and after

Showcase Chart Gallery, /apps/com.example.showcase/dashboard/showcase_chart_gallery,
real Chromium (Playwright 1.62.1), viewport 1440x1000, widget svg 510x350.

Before — plot area y 5..295, six marks, all inside it:

progress (x)avg_estimate (y)cxcyinside plot?
016.553215.3yes
4540256.4101.7yes
5560301.65yes
8024414.6179yes
9030459.8150yes
10012505237yes

The seventh "point" was the legend swatch: cx 255, cy 341, innerText === "". The
arithmetic closes the identification — the plot bottom (y=0) is cy 295 and the y scale
is 4.835 px per unit, so cy 341 is y = -9.5, at x ≈ 45. That is the card's
"x≈40, y≈-10", to the pixel.

Swept viewport widths 1440, 1100, 980, 900, 820, 760, 700, 640, 560, 480: zero marks
outside the plot area at any width
, so the symptom was never width-dependent either.

After — plot area y 5..287, same six marks all inside (cy 209.5, 99, 5, 174.2, 146,
230.6), and the legend now reads Avg Estimate beside its swatch instead of painting an
anonymous dot.

The y domain was not the defect and is unchanged

Clamping the y domain — the fix the card asked for — would have created the bug it
described. Two adversarial fixtures are pinned in this PR: mixed-sign data
(ym: 40, -15, 25) draws 3 of 3 marks and all-negative data (ym: -40, -15) draws 2 of
2, because recharts already extends the domain to cover negative values. Forcing
[0, 'auto'] is exactly what would push a negative point below the baseline.

The live data was read from the running showcase API and rules out the "bad row"
hypothesis independently: 10 seeded tasks, every progress a finite non-negative
number, no null / negative / string values, aggregating to the six buckets above.

Two changes

  1. AdvancedChartImpl.tsx, scatter branch — pass nameKey={scatterYKey} so the
    legend resolves the measure's config entry, the same way the pie branch already
    passes nameKey={xAxisKey}.

  2. ChartContainerImpl.tsx, ChartLegendContent — fall back to item.value, the
    series name recharts itself put on the legend item, when the config lookup misses.

The second is a deliberate bounded in-place fix rather than a drive-by: it is the same
defect class, at the layer that decides whether a swatch gets a name, and it closes the
class instead of this one instance. The swatch renders unconditionally, so a config
miss must never leave it anonymous. The helper directly below it already documents the
pie form of this same bug ("legend swatches with NO text at all"); that one was closed
by making one family's key resolve, this closes the general case. Charts whose config
already resolves are unaffected — only a currently-empty label can change.

Reverse verification (ablation)

Each half was ablated independently from the committed state, with a restore trap on
absolute paths, the mutation proved on disk by HEAD-blob-hash comparison plus an anchor
count, and the tree confirmed clean (git diff HEAD empty) after every leg. No dist
is involved — the test imports ./AdvancedChartImpl, which imports
./ChartContainerImpl, both relative source paths — so no rebuild leg applies.

ablationmutation landedresult
baseline (both halves)7 passed
remove nameKey only640e2290 → 52a84b18, anchor count 01 red — the config-resolution pin
remove ?? item.value only117306ba → 525ac855, anchor count 02 red
remove both (origin/main behaviour)both, anchor counts 04 red, incl. the headline assertion

The first run of this ablation found a real problem and changed the PR: removing
nameKey alone left all assertions green, because the item.value fallback masks
it on the label. Rather than ship an unenforced change, a pin was added for the one
branch the fallback cannot stand in for — itemConfig.icon, which only resolves when
the config lookup hits, so a config-declared legend icon is silently ignored for scatter
without nameKey while every other family honours it. That pin is the single red in
ablation 1 above.

Per-facet answers to the dispatch

Gates

Run on the final commit d2537532b:

  • pnpm exec vitest run packages/plugin-charts/ --maxWorkers=242 files, 390 tests
    passed
    (run from the repo root; the package-scoped pnpm --filter ... test form is
    the silent-green trap AGENTS.md bans)
  • pnpm --filter @object-ui/plugin-charts type-check — exit 0. tsc --listFiles
    confirms both edited files and the new test are inside the program, so this green
    actually covers the diff rather than excluding the test file
  • pnpm exec eslint . in packages/plugin-charts — exit 0, 0 errors, 318 pre-existing
    warnings, 55 files linted (count read from --format json), all three touched
    files present in the linted set
  • changeset: .changeset/7248-scatter-legend-names-its-series.md, @object-ui/plugin-charts patch

Declared narrowing. Lint was run at package scope rather than repo-wide. The
population comes from eslint's own config resolution, the count from --format json
(55 files), and the config is not type-aware (no parserOptions.project /
projectService), so this diff cannot move the verdict on any untouched file. CI runs
the full farm regardless.

Verify-lock declaration, verbatim from the tool on every heavy run:

os-verify-lock: VERDICT command-exit 0 · UNLOCKED (declared) · no usable `flock` on this
host, so the shared verify lock was NEVER taken and NOTHING was serialized

Out-of-scope findings filed

  • objectui#7396 — a scatter's extreme points are half-clipped by the plot edge (the
    numeric axes get no domain padding). Measured here: plot x 53..505 with marks at
    cx 53 and cx 505, so about half of each extreme symbol paints outside the plot area.
    Real, different mechanism, and changing it would alter every scatter's appearance.
  • objectui#7397 — packages/components/src/ui/chart.tsx is an unreferenced duplicate of
    the plugin-charts chart primitives carrying this same legend hole. Imported by
    nothing and absent from the barrel, so it is currently unreachable; left untouched
    here rather than widening this PR across a second published package.

Generated by Claude Code

hotlongand others added 2 commits September 3, 2026 01:20
…ops reading as a stray point (#7248)
The Chart Gallery scatter appeared to draw a seventh point below the x-axis.
It was the legend swatch: ChartLegendContent resolves a label as
config[nameKey || item.dataKey || 'value'], and a <Scatter> carries no
dataKey, so the key collapsed to 'value', missed the config and rendered a
colour swatch with no text.
Measured in real Chromium: swatch at cy 341 against a plot area ending at
cy 295, scale 4.835px/unit -> y = -9.5 at x ~ 45, matching the report's
'x~40, y~-10'. All six real marks were inside the plot area at every
viewport width from 1440 down to 480, so the y domain is left untouched.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Ablation showed removing nameKey alone left every assertion green: the
item.value fallback masks it on the label. The config lookup is still
load-bearing for itemConfig.icon, which the fallback cannot stand in for,
so pin that branch instead of shipping an unenforced change.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 48 chunks)3177.2 KB3191.4 KB
Main entry chunk (gzip)143.2 KB350 KB
Entry fileindex-CDQS-v_Q.js
StatusPASS

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

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)15.67KB5.75KB
app-shell (runtime-config.js)20.68KB7.36KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)514.87KB117.50KB
core (index.js)5.80KB2.32KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)178.20KB49.60KB
fields (index.js)244.08KB61.71KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.98KB10.98KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)47.00KB12.97KB
plugin-charts (index.js)70.31KB19.55KB
plugin-chatbot (index.js)196.19KB46.43KB
plugin-dashboard (index.js)132.63KB34.56KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)251.20KB64.18KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)132.87KB32.66KB
plugin-gantt (index.js)166.93KB40.82KB
plugin-grid (index.js)209.10KB56.65KB
plugin-kanban (index.js)53.21KB14.66KB
plugin-list (index.js)113.51KB27.67KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)30.21KB8.66KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.90KB21.12KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)81.07KB26.86KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.74KB2.25KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)8.11KB3.32KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

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

@hotlong
hotlong marked this pull request as ready for review September 2, 2026 17:43
@hotlong
hotlong added this pull request to the merge queueSep 2, 2026
Merged via the queue into main with commit bb459eaSep 2, 2026
32 checks passed
@hotlong
hotlong deleted the claude/issue-7248-scatter-y-domain branch September 2, 2026 17:58
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

plugin-charts: the Chart Gallery scatter ("Estimate vs Progress") draws one point below the x-axis, outside the plot area

1 participant

@hotlong
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(plugin-charts): name the scatter legend's series so its swatch stops reading as a stray point - #7398

Merged
hotlong merged 2 commits into
mainfrom
claude/issue-7248-scatter-y-domain
Sep 2, 2026
Merged

fix(plugin-charts): name the scatter legend's series so its swatch stops reading as a stray point#7398
hotlong merged 2 commits into
mainfrom
claude/issue-7248-scatter-y-domain

Conversation

@hotlong

Copy link
Copy Markdown
Contributor

Fixes#7248

The reported point is not a point — it is the legend swatch

The card reported the Chart Gallery scatter ("Estimate vs Progress") drawing a seventh
point below the x-axis at "x≈40, y≈-10", and asked for the y domain to include every
plotted value. Measured on the running showcase in real Chromium, that diagnosis is
false and the requested fix would have been in the wrong place.

ChartLegendContent resolves a label as config[nameKey || item.dataKey || 'value'].
A Scatter carries no dataKey — scatter's keys live on the XAxis/YAxis, not on
the mark — so the key collapsed to the literal string 'value', missed a config keyed
by measure name, and the legend entry rendered its colour swatch with no text beside
it. An 8x8 square in --chart-1, the same colour as the marks, sitting under the
x-axis.

Captured from recharts 3.10.1 rather than assumed, the scatter's legend payload item is:

{ color: '#3b82f6', type: 'circle', value: 'Avg Estimate',
payload: { name: 'Avg Estimate', ... } } // no `dataKey`

The series name was in value the whole time; the legend never read it.

Browser measurement — before and after

Showcase Chart Gallery, /apps/com.example.showcase/dashboard/showcase_chart_gallery,
real Chromium (Playwright 1.62.1), viewport 1440x1000, widget svg 510x350.

Before — plot area y 5..295, six marks, all inside it:

progress (x)avg_estimate (y)cxcyinside plot?
016.553215.3yes
4540256.4101.7yes
5560301.65yes
8024414.6179yes
9030459.8150yes
10012505237yes

The seventh "point" was the legend swatch: cx 255, cy 341, innerText === "". The
arithmetic closes the identification — the plot bottom (y=0) is cy 295 and the y scale
is 4.835 px per unit, so cy 341 is y = -9.5, at x ≈ 45. That is the card's
"x≈40, y≈-10", to the pixel.

Swept viewport widths 1440, 1100, 980, 900, 820, 760, 700, 640, 560, 480: zero marks
outside the plot area at any width
, so the symptom was never width-dependent either.

After — plot area y 5..287, same six marks all inside (cy 209.5, 99, 5, 174.2, 146,
230.6), and the legend now reads Avg Estimate beside its swatch instead of painting an
anonymous dot.

The y domain was not the defect and is unchanged

Clamping the y domain — the fix the card asked for — would have created the bug it
described. Two adversarial fixtures are pinned in this PR: mixed-sign data
(ym: 40, -15, 25) draws 3 of 3 marks and all-negative data (ym: -40, -15) draws 2 of
2, because recharts already extends the domain to cover negative values. Forcing
[0, 'auto'] is exactly what would push a negative point below the baseline.

The live data was read from the running showcase API and rules out the "bad row"
hypothesis independently: 10 seeded tasks, every progress a finite non-negative
number, no null / negative / string values, aggregating to the six buckets above.

Two changes

  1. AdvancedChartImpl.tsx, scatter branch — pass nameKey={scatterYKey} so the
    legend resolves the measure's config entry, the same way the pie branch already
    passes nameKey={xAxisKey}.

  2. ChartContainerImpl.tsx, ChartLegendContent — fall back to item.value, the
    series name recharts itself put on the legend item, when the config lookup misses.

The second is a deliberate bounded in-place fix rather than a drive-by: it is the same
defect class, at the layer that decides whether a swatch gets a name, and it closes the
class instead of this one instance. The swatch renders unconditionally, so a config
miss must never leave it anonymous. The helper directly below it already documents the
pie form of this same bug ("legend swatches with NO text at all"); that one was closed
by making one family's key resolve, this closes the general case. Charts whose config
already resolves are unaffected — only a currently-empty label can change.

Reverse verification (ablation)

Each half was ablated independently from the committed state, with a restore trap on
absolute paths, the mutation proved on disk by HEAD-blob-hash comparison plus an anchor
count, and the tree confirmed clean (git diff HEAD empty) after every leg. No dist
is involved — the test imports ./AdvancedChartImpl, which imports
./ChartContainerImpl, both relative source paths — so no rebuild leg applies.

ablationmutation landedresult
baseline (both halves)7 passed
remove nameKey only640e2290 → 52a84b18, anchor count 01 red — the config-resolution pin
remove ?? item.value only117306ba → 525ac855, anchor count 02 red
remove both (origin/main behaviour)both, anchor counts 04 red, incl. the headline assertion

The first run of this ablation found a real problem and changed the PR: removing
nameKey alone left all assertions green, because the item.value fallback masks
it on the label. Rather than ship an unenforced change, a pin was added for the one
branch the fallback cannot stand in for — itemConfig.icon, which only resolves when
the config lookup hits, so a config-declared legend icon is silently ignored for scatter
without nameKey while every other family honours it. That pin is the single red in
ablation 1 above.

Per-facet answers to the dispatch

Gates

Run on the final commit d2537532b:

  • pnpm exec vitest run packages/plugin-charts/ --maxWorkers=242 files, 390 tests
    passed
    (run from the repo root; the package-scoped pnpm --filter ... test form is
    the silent-green trap AGENTS.md bans)
  • pnpm --filter @object-ui/plugin-charts type-check — exit 0. tsc --listFiles
    confirms both edited files and the new test are inside the program, so this green
    actually covers the diff rather than excluding the test file
  • pnpm exec eslint . in packages/plugin-charts — exit 0, 0 errors, 318 pre-existing
    warnings, 55 files linted (count read from --format json), all three touched
    files present in the linted set
  • changeset: .changeset/7248-scatter-legend-names-its-series.md, @object-ui/plugin-charts patch

Declared narrowing. Lint was run at package scope rather than repo-wide. The
population comes from eslint's own config resolution, the count from --format json
(55 files), and the config is not type-aware (no parserOptions.project /
projectService), so this diff cannot move the verdict on any untouched file. CI runs
the full farm regardless.

Verify-lock declaration, verbatim from the tool on every heavy run:

os-verify-lock: VERDICT command-exit 0 · UNLOCKED (declared) · no usable `flock` on this
host, so the shared verify lock was NEVER taken and NOTHING was serialized

Out-of-scope findings filed

  • objectui#7396 — a scatter's extreme points are half-clipped by the plot edge (the
    numeric axes get no domain padding). Measured here: plot x 53..505 with marks at
    cx 53 and cx 505, so about half of each extreme symbol paints outside the plot area.
    Real, different mechanism, and changing it would alter every scatter's appearance.
  • objectui#7397 — packages/components/src/ui/chart.tsx is an unreferenced duplicate of
    the plugin-charts chart primitives carrying this same legend hole. Imported by
    nothing and absent from the barrel, so it is currently unreachable; left untouched
    here rather than widening this PR across a second published package.

Generated by Claude Code

hotlongand others added 2 commits September 3, 2026 01:20
…ops reading as a stray point (#7248)
The Chart Gallery scatter appeared to draw a seventh point below the x-axis.
It was the legend swatch: ChartLegendContent resolves a label as
config[nameKey || item.dataKey || 'value'], and a <Scatter> carries no
dataKey, so the key collapsed to 'value', missed the config and rendered a
colour swatch with no text.
Measured in real Chromium: swatch at cy 341 against a plot area ending at
cy 295, scale 4.835px/unit -> y = -9.5 at x ~ 45, matching the report's
'x~40, y~-10'. All six real marks were inside the plot area at every
viewport width from 1440 down to 480, so the y domain is left untouched.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Ablation showed removing nameKey alone left every assertion green: the
item.value fallback masks it on the label. The config lookup is still
load-bearing for itemConfig.icon, which the fallback cannot stand in for,
so pin that branch instead of shipping an unenforced change.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 48 chunks)3177.2 KB3191.4 KB
Main entry chunk (gzip)143.2 KB350 KB
Entry fileindex-CDQS-v_Q.js
StatusPASS

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

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)15.67KB5.75KB
app-shell (runtime-config.js)20.68KB7.36KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)514.87KB117.50KB
core (index.js)5.80KB2.32KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)178.20KB49.60KB
fields (index.js)244.08KB61.71KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.98KB10.98KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)47.00KB12.97KB
plugin-charts (index.js)70.31KB19.55KB
plugin-chatbot (index.js)196.19KB46.43KB
plugin-dashboard (index.js)132.63KB34.56KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)251.20KB64.18KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)132.87KB32.66KB
plugin-gantt (index.js)166.93KB40.82KB
plugin-grid (index.js)209.10KB56.65KB
plugin-kanban (index.js)53.21KB14.66KB
plugin-list (index.js)113.51KB27.67KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)30.21KB8.66KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.90KB21.12KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)81.07KB26.86KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.74KB2.25KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)8.11KB3.32KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

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

@hotlong
hotlong marked this pull request as ready for review September 2, 2026 17:43
@hotlong
hotlong added this pull request to the merge queueSep 2, 2026
Merged via the queue into main with commit bb459eaSep 2, 2026
32 checks passed
@hotlong
hotlong deleted the claude/issue-7248-scatter-y-domain branch September 2, 2026 17:58
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

plugin-charts: the Chart Gallery scatter ("Estimate vs Progress") draws one point below the x-axis, outside the plot area

1 participant

@hotlong
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(plugin-charts): name the scatter legend's series so its swatch stops reading as a stray point - #7398

Merged
hotlong merged 2 commits into
mainfrom
claude/issue-7248-scatter-y-domain
Sep 2, 2026
Merged

fix(plugin-charts): name the scatter legend's series so its swatch stops reading as a stray point#7398
hotlong merged 2 commits into
mainfrom
claude/issue-7248-scatter-y-domain

Conversation

@hotlong

Copy link
Copy Markdown
Contributor

Fixes#7248

The reported point is not a point — it is the legend swatch

The card reported the Chart Gallery scatter ("Estimate vs Progress") drawing a seventh
point below the x-axis at "x≈40, y≈-10", and asked for the y domain to include every
plotted value. Measured on the running showcase in real Chromium, that diagnosis is
false and the requested fix would have been in the wrong place.

ChartLegendContent resolves a label as config[nameKey || item.dataKey || 'value'].
A Scatter carries no dataKey — scatter's keys live on the XAxis/YAxis, not on
the mark — so the key collapsed to the literal string 'value', missed a config keyed
by measure name, and the legend entry rendered its colour swatch with no text beside
it. An 8x8 square in --chart-1, the same colour as the marks, sitting under the
x-axis.

Captured from recharts 3.10.1 rather than assumed, the scatter's legend payload item is:

{ color: '#3b82f6', type: 'circle', value: 'Avg Estimate',
payload: { name: 'Avg Estimate', ... } } // no `dataKey`

The series name was in value the whole time; the legend never read it.

Browser measurement — before and after

Showcase Chart Gallery, /apps/com.example.showcase/dashboard/showcase_chart_gallery,
real Chromium (Playwright 1.62.1), viewport 1440x1000, widget svg 510x350.

Before — plot area y 5..295, six marks, all inside it:

progress (x)avg_estimate (y)cxcyinside plot?
016.553215.3yes
4540256.4101.7yes
5560301.65yes
8024414.6179yes
9030459.8150yes
10012505237yes

The seventh "point" was the legend swatch: cx 255, cy 341, innerText === "". The
arithmetic closes the identification — the plot bottom (y=0) is cy 295 and the y scale
is 4.835 px per unit, so cy 341 is y = -9.5, at x ≈ 45. That is the card's
"x≈40, y≈-10", to the pixel.

Swept viewport widths 1440, 1100, 980, 900, 820, 760, 700, 640, 560, 480: zero marks
outside the plot area at any width
, so the symptom was never width-dependent either.

After — plot area y 5..287, same six marks all inside (cy 209.5, 99, 5, 174.2, 146,
230.6), and the legend now reads Avg Estimate beside its swatch instead of painting an
anonymous dot.

The y domain was not the defect and is unchanged

Clamping the y domain — the fix the card asked for — would have created the bug it
described. Two adversarial fixtures are pinned in this PR: mixed-sign data
(ym: 40, -15, 25) draws 3 of 3 marks and all-negative data (ym: -40, -15) draws 2 of
2, because recharts already extends the domain to cover negative values. Forcing
[0, 'auto'] is exactly what would push a negative point below the baseline.

The live data was read from the running showcase API and rules out the "bad row"
hypothesis independently: 10 seeded tasks, every progress a finite non-negative
number, no null / negative / string values, aggregating to the six buckets above.

Two changes

  1. AdvancedChartImpl.tsx, scatter branch — pass nameKey={scatterYKey} so the
    legend resolves the measure's config entry, the same way the pie branch already
    passes nameKey={xAxisKey}.

  2. ChartContainerImpl.tsx, ChartLegendContent — fall back to item.value, the
    series name recharts itself put on the legend item, when the config lookup misses.

The second is a deliberate bounded in-place fix rather than a drive-by: it is the same
defect class, at the layer that decides whether a swatch gets a name, and it closes the
class instead of this one instance. The swatch renders unconditionally, so a config
miss must never leave it anonymous. The helper directly below it already documents the
pie form of this same bug ("legend swatches with NO text at all"); that one was closed
by making one family's key resolve, this closes the general case. Charts whose config
already resolves are unaffected — only a currently-empty label can change.

Reverse verification (ablation)

Each half was ablated independently from the committed state, with a restore trap on
absolute paths, the mutation proved on disk by HEAD-blob-hash comparison plus an anchor
count, and the tree confirmed clean (git diff HEAD empty) after every leg. No dist
is involved — the test imports ./AdvancedChartImpl, which imports
./ChartContainerImpl, both relative source paths — so no rebuild leg applies.

ablationmutation landedresult
baseline (both halves)7 passed
remove nameKey only640e2290 → 52a84b18, anchor count 01 red — the config-resolution pin
remove ?? item.value only117306ba → 525ac855, anchor count 02 red
remove both (origin/main behaviour)both, anchor counts 04 red, incl. the headline assertion

The first run of this ablation found a real problem and changed the PR: removing
nameKey alone left all assertions green, because the item.value fallback masks
it on the label. Rather than ship an unenforced change, a pin was added for the one
branch the fallback cannot stand in for — itemConfig.icon, which only resolves when
the config lookup hits, so a config-declared legend icon is silently ignored for scatter
without nameKey while every other family honours it. That pin is the single red in
ablation 1 above.

Per-facet answers to the dispatch

Gates

Run on the final commit d2537532b:

  • pnpm exec vitest run packages/plugin-charts/ --maxWorkers=242 files, 390 tests
    passed
    (run from the repo root; the package-scoped pnpm --filter ... test form is
    the silent-green trap AGENTS.md bans)
  • pnpm --filter @object-ui/plugin-charts type-check — exit 0. tsc --listFiles
    confirms both edited files and the new test are inside the program, so this green
    actually covers the diff rather than excluding the test file
  • pnpm exec eslint . in packages/plugin-charts — exit 0, 0 errors, 318 pre-existing
    warnings, 55 files linted (count read from --format json), all three touched
    files present in the linted set
  • changeset: .changeset/7248-scatter-legend-names-its-series.md, @object-ui/plugin-charts patch

Declared narrowing. Lint was run at package scope rather than repo-wide. The
population comes from eslint's own config resolution, the count from --format json
(55 files), and the config is not type-aware (no parserOptions.project /
projectService), so this diff cannot move the verdict on any untouched file. CI runs
the full farm regardless.

Verify-lock declaration, verbatim from the tool on every heavy run:

os-verify-lock: VERDICT command-exit 0 · UNLOCKED (declared) · no usable `flock` on this
host, so the shared verify lock was NEVER taken and NOTHING was serialized

Out-of-scope findings filed

  • objectui#7396 — a scatter's extreme points are half-clipped by the plot edge (the
    numeric axes get no domain padding). Measured here: plot x 53..505 with marks at
    cx 53 and cx 505, so about half of each extreme symbol paints outside the plot area.
    Real, different mechanism, and changing it would alter every scatter's appearance.
  • objectui#7397 — packages/components/src/ui/chart.tsx is an unreferenced duplicate of
    the plugin-charts chart primitives carrying this same legend hole. Imported by
    nothing and absent from the barrel, so it is currently unreachable; left untouched
    here rather than widening this PR across a second published package.

Generated by Claude Code

hotlongand others added 2 commits September 3, 2026 01:20
…ops reading as a stray point (#7248)
The Chart Gallery scatter appeared to draw a seventh point below the x-axis.
It was the legend swatch: ChartLegendContent resolves a label as
config[nameKey || item.dataKey || 'value'], and a <Scatter> carries no
dataKey, so the key collapsed to 'value', missed the config and rendered a
colour swatch with no text.
Measured in real Chromium: swatch at cy 341 against a plot area ending at
cy 295, scale 4.835px/unit -> y = -9.5 at x ~ 45, matching the report's
'x~40, y~-10'. All six real marks were inside the plot area at every
viewport width from 1440 down to 480, so the y domain is left untouched.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Ablation showed removing nameKey alone left every assertion green: the
item.value fallback masks it on the label. The config lookup is still
load-bearing for itemConfig.icon, which the fallback cannot stand in for,
so pin that branch instead of shipping an unenforced change.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 48 chunks)3177.2 KB3191.4 KB
Main entry chunk (gzip)143.2 KB350 KB
Entry fileindex-CDQS-v_Q.js
StatusPASS

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

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)15.67KB5.75KB
app-shell (runtime-config.js)20.68KB7.36KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)514.87KB117.50KB
core (index.js)5.80KB2.32KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)178.20KB49.60KB
fields (index.js)244.08KB61.71KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.98KB10.98KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)47.00KB12.97KB
plugin-charts (index.js)70.31KB19.55KB
plugin-chatbot (index.js)196.19KB46.43KB
plugin-dashboard (index.js)132.63KB34.56KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)251.20KB64.18KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)132.87KB32.66KB
plugin-gantt (index.js)166.93KB40.82KB
plugin-grid (index.js)209.10KB56.65KB
plugin-kanban (index.js)53.21KB14.66KB
plugin-list (index.js)113.51KB27.67KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)30.21KB8.66KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.90KB21.12KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)81.07KB26.86KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.74KB2.25KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)8.11KB3.32KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

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

@hotlong
hotlong marked this pull request as ready for review September 2, 2026 17:43
@hotlong
hotlong added this pull request to the merge queueSep 2, 2026
Merged via the queue into main with commit bb459eaSep 2, 2026
32 checks passed
@hotlong
hotlong deleted the claude/issue-7248-scatter-y-domain branch September 2, 2026 17:58
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

plugin-charts: the Chart Gallery scatter ("Estimate vs Progress") draws one point below the x-axis, outside the plot area

1 participant

@hotlong
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

fix(plugin-charts): name the scatter legend's series so its swatch stops reading as a stray point - #7398

Merged
hotlong merged 2 commits into
mainfrom
claude/issue-7248-scatter-y-domain
Sep 2, 2026
Merged

fix(plugin-charts): name the scatter legend's series so its swatch stops reading as a stray point#7398
hotlong merged 2 commits into
mainfrom
claude/issue-7248-scatter-y-domain

Conversation

@hotlong

Copy link
Copy Markdown
Contributor

Fixes#7248

The reported point is not a point — it is the legend swatch

The card reported the Chart Gallery scatter ("Estimate vs Progress") drawing a seventh
point below the x-axis at "x≈40, y≈-10", and asked for the y domain to include every
plotted value. Measured on the running showcase in real Chromium, that diagnosis is
false and the requested fix would have been in the wrong place.

ChartLegendContent resolves a label as config[nameKey || item.dataKey || 'value'].
A Scatter carries no dataKey — scatter's keys live on the XAxis/YAxis, not on
the mark — so the key collapsed to the literal string 'value', missed a config keyed
by measure name, and the legend entry rendered its colour swatch with no text beside
it. An 8x8 square in --chart-1, the same colour as the marks, sitting under the
x-axis.

Captured from recharts 3.10.1 rather than assumed, the scatter's legend payload item is:

{ color: '#3b82f6', type: 'circle', value: 'Avg Estimate',
payload: { name: 'Avg Estimate', ... } } // no `dataKey`

The series name was in value the whole time; the legend never read it.

Browser measurement — before and after

Showcase Chart Gallery, /apps/com.example.showcase/dashboard/showcase_chart_gallery,
real Chromium (Playwright 1.62.1), viewport 1440x1000, widget svg 510x350.

Before — plot area y 5..295, six marks, all inside it:

progress (x)avg_estimate (y)cxcyinside plot?
016.553215.3yes
4540256.4101.7yes
5560301.65yes
8024414.6179yes
9030459.8150yes
10012505237yes

The seventh "point" was the legend swatch: cx 255, cy 341, innerText === "". The
arithmetic closes the identification — the plot bottom (y=0) is cy 295 and the y scale
is 4.835 px per unit, so cy 341 is y = -9.5, at x ≈ 45. That is the card's
"x≈40, y≈-10", to the pixel.

Swept viewport widths 1440, 1100, 980, 900, 820, 760, 700, 640, 560, 480: zero marks
outside the plot area at any width
, so the symptom was never width-dependent either.

After — plot area y 5..287, same six marks all inside (cy 209.5, 99, 5, 174.2, 146,
230.6), and the legend now reads Avg Estimate beside its swatch instead of painting an
anonymous dot.

The y domain was not the defect and is unchanged

Clamping the y domain — the fix the card asked for — would have created the bug it
described. Two adversarial fixtures are pinned in this PR: mixed-sign data
(ym: 40, -15, 25) draws 3 of 3 marks and all-negative data (ym: -40, -15) draws 2 of
2, because recharts already extends the domain to cover negative values. Forcing
[0, 'auto'] is exactly what would push a negative point below the baseline.

The live data was read from the running showcase API and rules out the "bad row"
hypothesis independently: 10 seeded tasks, every progress a finite non-negative
number, no null / negative / string values, aggregating to the six buckets above.

Two changes

  1. AdvancedChartImpl.tsx, scatter branch — pass nameKey={scatterYKey} so the
    legend resolves the measure's config entry, the same way the pie branch already
    passes nameKey={xAxisKey}.

  2. ChartContainerImpl.tsx, ChartLegendContent — fall back to item.value, the
    series name recharts itself put on the legend item, when the config lookup misses.

The second is a deliberate bounded in-place fix rather than a drive-by: it is the same
defect class, at the layer that decides whether a swatch gets a name, and it closes the
class instead of this one instance. The swatch renders unconditionally, so a config
miss must never leave it anonymous. The helper directly below it already documents the
pie form of this same bug ("legend swatches with NO text at all"); that one was closed
by making one family's key resolve, this closes the general case. Charts whose config
already resolves are unaffected — only a currently-empty label can change.

Reverse verification (ablation)

Each half was ablated independently from the committed state, with a restore trap on
absolute paths, the mutation proved on disk by HEAD-blob-hash comparison plus an anchor
count, and the tree confirmed clean (git diff HEAD empty) after every leg. No dist
is involved — the test imports ./AdvancedChartImpl, which imports
./ChartContainerImpl, both relative source paths — so no rebuild leg applies.

ablationmutation landedresult
baseline (both halves)7 passed
remove nameKey only640e2290 → 52a84b18, anchor count 01 red — the config-resolution pin
remove ?? item.value only117306ba → 525ac855, anchor count 02 red
remove both (origin/main behaviour)both, anchor counts 04 red, incl. the headline assertion

The first run of this ablation found a real problem and changed the PR: removing
nameKey alone left all assertions green, because the item.value fallback masks
it on the label. Rather than ship an unenforced change, a pin was added for the one
branch the fallback cannot stand in for — itemConfig.icon, which only resolves when
the config lookup hits, so a config-declared legend icon is silently ignored for scatter
without nameKey while every other family honours it. That pin is the single red in
ablation 1 above.

Per-facet answers to the dispatch

Gates

Run on the final commit d2537532b:

  • pnpm exec vitest run packages/plugin-charts/ --maxWorkers=242 files, 390 tests
    passed
    (run from the repo root; the package-scoped pnpm --filter ... test form is
    the silent-green trap AGENTS.md bans)
  • pnpm --filter @object-ui/plugin-charts type-check — exit 0. tsc --listFiles
    confirms both edited files and the new test are inside the program, so this green
    actually covers the diff rather than excluding the test file
  • pnpm exec eslint . in packages/plugin-charts — exit 0, 0 errors, 318 pre-existing
    warnings, 55 files linted (count read from --format json), all three touched
    files present in the linted set
  • changeset: .changeset/7248-scatter-legend-names-its-series.md, @object-ui/plugin-charts patch

Declared narrowing. Lint was run at package scope rather than repo-wide. The
population comes from eslint's own config resolution, the count from --format json
(55 files), and the config is not type-aware (no parserOptions.project /
projectService), so this diff cannot move the verdict on any untouched file. CI runs
the full farm regardless.

Verify-lock declaration, verbatim from the tool on every heavy run:

os-verify-lock: VERDICT command-exit 0 · UNLOCKED (declared) · no usable `flock` on this
host, so the shared verify lock was NEVER taken and NOTHING was serialized

Out-of-scope findings filed

  • objectui#7396 — a scatter's extreme points are half-clipped by the plot edge (the
    numeric axes get no domain padding). Measured here: plot x 53..505 with marks at
    cx 53 and cx 505, so about half of each extreme symbol paints outside the plot area.
    Real, different mechanism, and changing it would alter every scatter's appearance.
  • objectui#7397 — packages/components/src/ui/chart.tsx is an unreferenced duplicate of
    the plugin-charts chart primitives carrying this same legend hole. Imported by
    nothing and absent from the barrel, so it is currently unreachable; left untouched
    here rather than widening this PR across a second published package.

Generated by Claude Code

hotlongand others added 2 commits September 3, 2026 01:20
…ops reading as a stray point (#7248)
The Chart Gallery scatter appeared to draw a seventh point below the x-axis.
It was the legend swatch: ChartLegendContent resolves a label as
config[nameKey || item.dataKey || 'value'], and a <Scatter> carries no
dataKey, so the key collapsed to 'value', missed the config and rendered a
colour swatch with no text.
Measured in real Chromium: swatch at cy 341 against a plot area ending at
cy 295, scale 4.835px/unit -> y = -9.5 at x ~ 45, matching the report's
'x~40, y~-10'. All six real marks were inside the plot area at every
viewport width from 1440 down to 480, so the y domain is left untouched.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Ablation showed removing nameKey alone left every assertion green: the
item.value fallback masks it on the label. The config lookup is still
load-bearing for itemConfig.icon, which the fallback cannot stand in for,
so pin that branch instead of shipping an unenforced change.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 48 chunks)3177.2 KB3191.4 KB
Main entry chunk (gzip)143.2 KB350 KB
Entry fileindex-CDQS-v_Q.js
StatusPASS

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

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)15.67KB5.75KB
app-shell (runtime-config.js)20.68KB7.36KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)514.87KB117.50KB
core (index.js)5.80KB2.32KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)178.20KB49.60KB
fields (index.js)244.08KB61.71KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.98KB10.98KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)47.00KB12.97KB
plugin-charts (index.js)70.31KB19.55KB
plugin-chatbot (index.js)196.19KB46.43KB
plugin-dashboard (index.js)132.63KB34.56KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)251.20KB64.18KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)132.87KB32.66KB
plugin-gantt (index.js)166.93KB40.82KB
plugin-grid (index.js)209.10KB56.65KB
plugin-kanban (index.js)53.21KB14.66KB
plugin-list (index.js)113.51KB27.67KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)30.21KB8.66KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.90KB21.12KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)81.07KB26.86KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.74KB2.25KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)8.11KB3.32KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

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

@hotlong
hotlong marked this pull request as ready for review September 2, 2026 17:43
@hotlong
hotlong added this pull request to the merge queueSep 2, 2026
Merged via the queue into main with commit bb459eaSep 2, 2026
32 checks passed
@hotlong
hotlong deleted the claude/issue-7248-scatter-y-domain branch September 2, 2026 17:58
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

plugin-charts: the Chart Gallery scatter ("Estimate vs Progress") draws one point below the x-axis, outside the plot area

1 participant

@hotlong