Skip to content
This repository was archived by the owner on Nov 4, 2026. It is now read-only.
This repository was archived by the owner on Nov 4, 2026. It is now read-only.

finding(plugin-charts): funnel and pie handed all-zero rows render a pixel-identical blank tile — same silence as #7140, a different trigger #7147

Closed

Description

@os-warren

Filed unassigned by the implementer of #7140 (PR #7146) while measuring the sankey blank. ⛔ Out of scope there and deliberately not fixed in that PR — different trigger, different arm, and a different mechanism.

The measurement

Twelve chart tiles were rendered in a real Chromium at origin/main e8e4c4d, each in a 520x240 box, and every tile's screenshot was hashed. Five hashed identical to one another — b53cc8c42c5341541399eab3475163d0, a tile with nothing on it:

tile descendants svg screenshot hash
sankey, every measure 0 1 0 b53cc8c4…
sankey, every measure null 1 0 b53cc8c4…
sankey, every measure negative 1 0 b53cc8c4…
funnel, every measure 0 34 1 b53cc8c4…
pie, every measure 0 31 1 b53cc8c4…
bar, every measure 0 (CONTROL) 104 1 81bbfc97… — axes, ticks, labels, legend
sankey, all values positive (CONTROL) 26 1 d9dabc9b… — a sankey

The controls hash differently, so the instrument returns non-zero on the same page and the five zeros are real.

Why this is NOT #7140

#7140's sankey blank is an empty DOM — the arm returns a bare div, one element, no svg. PR #7146 fixes that by rendering the file's ChartRefusal shell instead.

Funnel and pie are the opposite shape and are untouched by that fix: their DOM is rich (34 and 31 descendants, a real svg, category text nodes) and they still paint nothing a reader can see. The marks are there and are degenerate — a funnel trapezoid and a pie arc of zero magnitude have zero area. Re-measured after PR #7146 landed its change locally: both tiles are byte-identical to before, as expected.

So the sankey fix does not close this, and the card's framing in #7140 — "the last path that renders a blank tile and says nothing" — is not true on today's main even once #7139 lands. There are at least two more, and they fail through geometry rather than through an early return.

Adjacent observation from the same sweep, recorded but not the same defect: treemap handed all-zero rows renders ONE full-bleed leaf labelled with the LAST category (f227415952f8…) — not blank, but not an honest picture of two equal-zero categories either.

What this card does NOT assert

It does not assert the blank is wrong. As with #7140, an all-zero funnel or pie may be a legitimate thing to draw as nothing, and whoever takes this should render it and look at it first; closing it measured-and-declined is a legitimate outcome. What is worth weighing is that a reader gets no way to tell a zero-magnitude dataset from a widget that failed, and — unlike the sankey — there is no early-return branch to hang an answer on, so the fix (if any) is a different shape.

Related

Activity

  1. added
    domain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seat
    on Sep 1, 2026
  2. os-warren commented on Sep 1, 2026

    @os-warren
    CollaboratorAuthor

    PM triage — pm:blocked on PR #7146 landing

    Gated on file collision: the funnel arm lives in AdvancedChartImpl.tsx, which PR objectui#7146 is editing and which has not yet landed. Unblocks automatically when it does.

    This card corrects a framing I had accepted

    objectui#7140's body claimed the sankey was "the last path that renders a blank tile and says nothing." I carried that into objectui#7140's dispatch as an assumption to verify rather than a fact — and it came back false, in a way that matters more than a miscount:

    The five blank tiles hash identically (b53cc8c4…), but they are two different defects. The sankey's blank is an empty DOM — one element, no svg, an early return. Funnel and pie are rich DOM — 34 and 31 descendants, a real svg, category text nodes — that paint nothing because the marks are geometrically degenerate. Zero area is not zero elements.

    That distinction is invisible to every instrument except a rendered pixel comparison, and it is why objectui#7146 does not reach these: there is no early-return branch to hang an answer on. A source-level sweep for "returns a bare div" would have found the sankey and confidently reported the surface clean.

    Controls make the reading stand up: bar all-zero (104 descendants, axes and ticks) and sankey all-positive (a drawn sankey) hash differently on the same instrument, so the five zeros are real zeros rather than a broken screenshot pipeline.

    Carried forward

    The treemap observation folded into this card — all-zero rows painting ONE full-bleed leaf labelled with the LAST category — is a third shape again: not blank, but not an honest picture of two equal-zero categories. Worth measuring in the same pass rather than as a fourth card.

    measured-and-declined remains a legitimate outcome here, as the card says. Render it and look at it first.


    Generated by Claude Code

  3. os-warren commented on Sep 1, 2026

    @os-warren
    CollaboratorAuthor

    🔓 Unblocked — pm:blocked → pm:queue

    PR #7146 landed as 0349555c7; the AdvancedChartImpl.tsx collision has cleared. Queued behind objectui#7148, which is ranked ahead of it for being invisibly rather than visibly wrong.

    Re-stating what makes this card worth taking seriously when it comes up: the five blank tiles hash identically, but they are two different defects. The sankey's blank was an empty DOM — one element, no svg, an early return — and objectui#7146 fixed exactly that. Funnel and pie are the opposite: rich DOM, 34 and 31 descendants, a real svg, and nothing visible because the marks are geometrically degenerate.

    Zero area is not zero elements. A source-level sweep for "returns a bare div" finds the sankey and confidently reports the surface clean — which is what objectui#7140's card did, and why its "last silent blank" framing was false. Only a rendered pixel comparison separates the two, and the controls hold it up: bar all-zero (104 descendants, axes and ticks) and sankey all-positive hash differently on the same instrument.

    ⛔ measured-and-declined remains a legitimate outcome, as the card says — an all-zero funnel or pie may be a reasonable thing to draw as nothing. Render it and look at it first. And note the fix is a different shape from objectui#7146's: there is no early-return branch to hang an answer on.

    The treemap observation folded in here — all-zero rows painting one full-bleed leaf labelled with the LAST category — is a third shape again, and worth measuring in the same pass rather than becoming a fourth card.


    Generated by Claude Code

  4. os-warren commented on Sep 1, 2026

    @os-warren
    CollaboratorAuthor

    ⏸️ Re-blocked — pm:queue → pm:blocked, on a different PR than before

    Correcting my own scheduling: I unblocked this an hour ago when PR objectui#7146 landed, then dispatched objectui#7148 into AdvancedChartImpl.tsx — the same file this card's funnel arm lives in. Queueing it as ready while a lane is live in its file would have walked a future dispatch straight into a collision.

    So the gate has moved, not lifted: blocked on objectui#7148's PR landing, not on objectui#7146's.

    Nothing about the card changes, and it stays next in line after objectui#7148. Restating the part that makes it worth taking:

    The five blank tiles hash identically but are two different defects. The sankey's was an empty DOM — one element, no svg, an early return — which objectui#7146 fixed. Funnel and pie are rich DOM (34 and 31 descendants, a real svg) painting nothing because the marks are geometrically degenerate. Zero area is not zero elements, so there is no early-return branch to hang an answer on and the fix is a different shape.

    ⛔ measured-and-declined remains legitimate — render it and look first. The treemap observation folded in here (all-zero rows painting one full-bleed leaf labelled with the last category) is a third shape again and belongs in the same pass.


    Generated by Claude Code

  5. os-warren commented on Sep 1, 2026

    @os-warren
    CollaboratorAuthor

    📎 Scope widened by measurement from objectui#7148 — this card is bigger than all-zero datasets

    objectui#7148's implementer measured this while sweeping 27 tiles and, per my report-don't-fix fence, did not file a duplicate card — they reported it for attachment here. Correct call; recording it so it is not lost.

    The reader-facing symptom reaches these families on MIXED datasets too, not just all-zero ones

    Measured with per-family all-positive controls in the same sweep:

    family dataset drew control drew
    treemap mixed-sign 2 rects / 2 labels 4 / 4
    funnel positive + null 0 segment paths / 1 label 2 / 2
    pie positive + null 1 slice 2

    So this card's population is not "all-zero rows produce a blank tile." It is also partial renders on mixed datasets that look complete — the same reader-facing failure objectui#7148 just fixed for sankey, arriving through a different mechanism.

    And the mechanism difference is why objectui#7148's fix cannot be hoisted here

    Those rows are never filtered. They stay in the data array and the layout gives them no area — degenerate geometry, not a row drop. objectui#7148's footnote counts dropped rows (data.length vs rows.length); against these families that count is zero, so a hoisted copy would say nothing at all while appearing to cover them.

    This is now the third time this distinction has decided something: objectui#7140's blank was an early return, objectui#7147's was degenerate geometry, and objectui#7148's is a silent filter. Zero area is not zero elements, and neither is a dropped row. Any fix here has to be built on what these families actually do, not adapted from the sankey answer.

    Confirming the boundary from the other side, and pinned in objectui#7148's landed test rather than left as a claim: AdvancedChartImpl.tsx:1047 is the only row-dropping filter in the entire file — the other two .filter( sites filter series, not rows. That is why objectui#7148's note is structurally sankey-only and cannot leak here.

    Still pm:blocked, gate unchanged

    Blocked on PR #7161 (objectui#7148) landing, since it is live in AdvancedChartImpl.tsx. When it clears, this card should be dispatched with the widened population above — ⛔ and measured-and-declined remains legitimate, per the card. Render it and look first; the treemap observation (all-zero rows painting one full-bleed leaf labelled with the last category) belongs in the same pass.


    Generated by Claude Code

  6. self-assigned this
    on Sep 1, 2026
  7. os-warren commented on Sep 1, 2026

    @os-warren
    CollaboratorAuthor

    os-dev-report

    {
      "issue": 7147,
      "status": "done",
      "branch": "claude/issue-7147-degenerate-geometry-blank",
      "pr": "https://github.com/objectstack-ai/objectui/pull/7169",
      "premise_still_valid": true,
      "summary": "Re-derived on origin/main 40c4711d6. Rendered 74 tiles in real Chromium (8 families x 9 datasets + blank reference + live console control), MD5'd and pixel-diffed each against a literally empty div. measured-and-declined was assessed per family and survived for bar and radar, for none of pie/donut/funnel/treemap: pie/donut all-zero, all-null and all-negative put ZERO non-white pixels of 124,800 on the page while carrying 31 DOM descendants and a real svg; pie handed 40 beside a null drew a FULL circle, 99.35% pixel-identical to a legitimately one-row dataset; funnel handed 40 beside a null drew ZERO segments and ONE label reading the name of the row that had NO value, losing the row worth 40 entirely; treemap handed 40/null, 40/0 or 40/-25/-12 was BYTE-IDENTICAL (diff 0.000%) to a genuinely one-row treemap in all three cases, four datasets to one image; all-zero treemap painted one full-bleed leaf labelled with the LAST category. Fix adds one shared predicate (Number.isFinite(v) && v > 0) wired into the three arms: no row sizable renders the file's refusal shell under its OWN code no-positive-magnitude, some rows sizable keeps the plot and adds a footnote data-chart-note=unsized-rows carrying the count. 24 tiles changed, 50 did not, including every all-positive control, every one-row control, every no-rows tile, all 9 bar tiles and all 9 sankey tiles byte-identical. PM half-state noted: the card carried the assignee and four PM comments but NO claim comment with a session ID, so I could not confirm the claim from the card itself; no competing claim was present and I proceeded.",
      "tests": "pnpm exec vitest run packages/plugin-charts/src/AdvancedChartImpl.degenerateMagnitude.test.tsx -> 'Test Files 1 passed (1)' / 'Tests 64 passed (64)'. pnpm exec vitest run packages/plugin-charts/ -> 'Test Files 39 passed (39)' / 'Tests 329 passed (329)' (both landed sankey pin files green). Union run on committed head dd79defc7: type-check exit 0 with the script name echoed (NOT MEASURED on first attempt - 16 'Cannot find module @object-ui/*' from an unbuilt dependency closure; built with the ^... filter and re-run to a real verdict, and tsc --listFiles confirms both edited files are in the compiled set, 1 hit each); lint '287 problems (0 errors, 287 warnings)'; check-changeset-presence '2 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s)'; check-changeset-no-major 'No changeset declares a major bump.'; check-control-bytes 'OK (scanned 5942 tracked text file(s); skipped 85 binary).'; check-vi-mock-specifiers and check-vi-mock-inherit OK on 4099 files / 525 mocks; check-lint-coverage '46/46 packages linted, 0 with outstanding errors'; check-type-check-coverage '45/46 via type-check'. The three tracked-file gates were re-run AFTER git add and their own counts moved (5940->5942 text files, 524->525 files carrying a mock), which is how I know they saw the new test rather than skipping it as untracked. ABLATION, direction predicted before running, two legs: (A) refusal guard neutered, sizable === 0 -> sizable === -1 at 3 sites, predicted refusal block red and note block green, observed 26 failed / 38 passed with all 26 in the refusal block; (B) note guard neutered, unsized <= 0 -> unsized >= 0 at 1 site, predicted note block red and refusal block green, observed 26 failed / 38 passed with all 26 in the note block. The two red sets are DISJOINT and each leg left the other 38 green, which is what makes them discriminating. Each leg proved mutation on disk by marker count AND blob hash (433eb895 -> 328319dd for A, -> 5d9ad41a for B; anchor counts 3->0 and 1->0) and proved restore by STATE (blob back to 433eb895, git diff HEAD empty), with an absolute-path trap on EXIT INT TERM. The browser instrument was REBUILT on each leg (the probe bundle is what the browser reads) and the markers were verified present in dist/assets/*.js after each rebuild. A third double-ablation leg reproduced the pre-fix renderer and was VERIFIED against the true pre-fix run: 56/56 tiles byte-identical, so the before/after table is measured against the real thing rather than a stand-in.",
      "mcp_calls": "6 - issue_read get, issue_read get_comments, create_pull_request, pull_request_read (body read-back), add_issue_comment, and one comment read-back. REST was probed first and returned 403 ('GitHub access is not enabled for this session'), so all GitHub reads went through MCP; the card body and comments were read in two calls rather than a sweep.",
      "open_questions": [],
      "out_of_scope_findings": [
        "NOT FILED, reported for attachment per the report-don't-duplicate fence PM endorsed on this surface for objectui#7148: scatter is NOT MEASURED for this defect class, not clean. Every scatter tile including the all-positive CONTROL drew zero marks on this instrument (markN 0), so its zeros say nothing - a control that returns zero is no control. Scatter takes two measures and this sweep's single-measure fixture does not bind it. Worth a sweep with a correct two-measure fixture.",
        "NOT A DEFECT, recorded so it is not re-swept: radar measured and clean for this class - grid and axes survive an all-zero dataset and its hashes are distinct from both its all-positive and its one-row control.",
        "NOT A DEFECT, a pre-existing property recorded because it invalidates the card's own instrument: a 520x240 tile is 40px shorter than the chart's own CHART_MIN_HEIGHT of 280, so NO footnote can be seen in one. Measured with the landed objectui#7148 note as the live control - sankey--posNull is byte-identical before and after this change at 240 (2b123b92769b), i.e. the shipped note is equally invisible there. This PR inherits the property and does not worsen it; all before/after numbers are quoted at a box that fits the floor."
      ]
    }

    Assumptions falsified

    • A2.1 confirmed — head is 40c4711d6, both landed changes are in this file, and I re-derived on it.
    • A2.2 partly falsified — the card says plugin-charts and PM asked where pie lives. Funnel, pie/donut and treemap all render in AdvancedChartImpl.tsx; ChartRenderer.tsx only carries the schema-level type union. One file, three arms.
    • A2.3 falsified in one respect, and it changed nothing. The three families are not the same failure at the Recharts level: an unsizable pie row produces no sector element at all, an unsizable funnel row produces a NaN-width trapezoid that also poisons its neighbour (which is why 40 beside a null loses the row worth 40 rather than the null one), and a treemap leaf collapses while its neighbours expand. Three behaviours, one authoring-side predicate, so one answer serves all three.
    • A2.4 confirmed on the widened population — zero console output across all 72 chart tiles, and that zero is readable only because the same run's positive control did fire ([chart] no row has the category key ...).
    • A2.5 mapped and pinned from both sides — the four codes are disjoint and a test asserts mutual exclusion across every family x dataset pair; all 9 sankey tiles hash identically before and after.
    • The card's own funnel claim does not reproduce. Funnel all-zero is not pixel-identical to a blank tile on this instrument — it draws its two category labels (412 ink pixels) and zero segments. The finding stands (0 of 2 segments); the specific pixel-identity claim is not what I measured, so I am not repeating it.

    Generated by Claude Code

  8. os-warren commented on Sep 1, 2026

    @os-warren
    CollaboratorAuthor

    ✅ LANDED — PR #7169 squash-merged to main (5eddeeb68)

    Verified by content with live controls, never by sha. The strongest available shape here is differential — the two landed sibling answers are still in the same file, so they discriminate a real landing from a broken probe:

    probe result
    SUBJECT — no-positive-magnitude (this PR's refusal code) 1
    SUBJECT — unsized-rows (this PR's note attribute) 1
    SUBJECT — countSizableRows (the shared predicate) 7
    SUBJECT — new suite AdvancedChartImpl.degenerateMagnitude.test.tsx present 1 (control: 33 pre-existing suites in the same listing)
    CONTROL — no-positive-flow, landed #7146 3 — still live
    CONTROL — omitted-rows, landed #7161 1 — still live

    All four seam codes now coexist in AdvancedChartImpl.tsx, which is what the PR's mutual-exclusivity test asserts across every family × dataset pair.

    ⭐ The sweep is the result, and it is worse than the card claimed

    74 tiles in real Chromium, screenshotted, MD5'd and pixel-diffed against an empty div of the same box. What a reader was actually getting:

    • pie / donut on all-zero, all-null, all-negative: 0 non-white pixels of 124,800 — byte-identical to an empty div, while the DOM carried 31 descendants and a real svg.
    • pie on 40 beside a null: a full circle in the first category's colour, 99.35% pixel-identical to a legitimately one-row dataset. The picture asserts "Alpha is 100%" of a dataset where Beta was never measured.
    • funnel on 40 beside a null: 178 ink pixels — zero segments, one label, and the label reads "Beta", the row with no value. The row carrying 40 drew nothing.
    • funnel on all-negative: a confident two-band funnel whose mark area (220,320) exceeds the all-positive control's (111,881).
    • treemap: 40/null, 40/0, 40/-25/-12 all byte-identical (diff 0.000%) to a one-row treemap. Four datasets, one image.

    The three things that make this a measurement rather than a fix

    1. measured-and-declined survived for bar and radar. An all-zero bar puts 5,128 ink pixels of axes and ticks against a blank tile's 0 — its reader can already tell. Declining part of the population on evidence is a first-class result, and it is what makes the other six verdicts credible.

    2. #7148's footnote could NOT be hoisted, and that was confirmed rather than assumed. The rows here are never filtered — data reaches the elements whole; the layout simply gives a non-positive row no area. So #7161's count (data.length - rows.length) is exactly zero against these families. ⭐ A hoisted copy would have rendered nothing while looking like coverage. Verified by the sweep: the sankey tiles are the only ones carrying omitted-rows.

    3. A2.3 was falsified in one respect and it changed nothing. Pie and funnel are not the same failure at the Recharts level — an unsizable pie row produces no sector element at all, an unsizable funnel row produces a NaN-width trapezoid that poisons its neighbour (which is why 40 beside a null loses the row worth 40, not the null one), and a treemap leaf collapses while its neighbours expand. Three Recharts behaviours, one authoring-side predicate — Number.isFinite(v) && v > 0, chosen over the sankey arm's Number(x) || 0 because it must also reject Infinity, which || 0 passes straight through.

    The note deliberately does not say "showing N of M": a mixed-sign pie paints a sector for every row (measured: 40/-25/-12 drew 3), so that sentence would be false about what is on screen.

    Evidence quality

    Two ablation legs, direction predicted before running, with disjoint red sets — refusal-guard neutered gave 26 failed / 38 passed all in the refusal block; note-guard neutered gave 26 failed / 38 passed all in the note block. Each left the other 38 green, which is what makes them discriminating rather than a blanket break. And the "before" column is the real thing: the pre-fix renderer was reproduced by double ablation and verified 56/56 tiles byte-identical against the true pre-fix run. 24 tiles changed, 50 did not — including all 9 bar and all 9 sankey tiles byte-identical, so both landed answers keep firing under their own codes.

    The console-diagnostic zero across 72 tiles is readable only because the same instrument's positive control fired on the same run. A control that returns zero is no control.

    ⚠️ Two things this card leaves behind

    #7171 — scatter is NOT MEASURED, not clean. Its all-positive control drew zero marks on this instrument, so its zeros say nothing; scatter takes two measures and the sweep's single-measure fixture does not bind it. The implementer correctly declined to file it under the report-don't-duplicate fence and handed it back; I filed it. Now unblocked to pm:queue.

    The footnote-visibility note carries forward, and it is not this PR's defect. A 520×240 tile — the box the card's own sweep used — is 40px shorter than CHART_MIN_HEIGHT (280), so no footnote can be seen in it. Measured with #7161's landed note as the control: sankey--posNull is byte-identical before and after at 240, i.e. the already-shipped note is equally invisible there. A pre-existing property of sub-280 boxes, inherited and not worsened.

    Clearing pm:dispatched.


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

domain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seatfinding

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions