Repository navigation
fix(llc/frontend): export the Gantt at display resolution and cull its axis to the viewport (#14767) - #14796
Conversation
✅ SSOT Configuration Compliance: Passing🎉 No hardcoded values detected that have SSOT config equivalents! |
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
CHANGES REQUESTED — one real bug, one unverified headline claimThe export implementation itself is sound: 1. Blank axis after a zoom change — filed as #14799
The docstring at :353-356 already names this outcome — "an unmeasurable viewport 2. The export path has no test coverage at allBoth new PNG-export tests call There is a second trap waiting for whoever writes that test: 3. "Restores culling after export" cannot failIt compares tick counts before and after the export and never asserts during 4. No boundary case is exercisedEvery culling assertion uses Non-blocking
Not touching the branch; it has a live worktree. |
…me fill, culling suspension (#14767)
…port teardown branches (#14767)
Closes #14767, #14769
PR 3 of 5 from the canvas review. Both issues are in
GanttTimelineView.vueand they interact, which is the main thing to look at.
Thinking Path
They arrived as separate findings — one a bug in the PNG export, one a
performance gap in the axis — and they would have been separate PRs if they
did not collide. They do:
#14769 culls axis ticks to the visible window. #14767 serialises the SVG to
produce a PNG. Implemented independently, the export would silently ship a
chart missing every gridline and date label outside the viewport — a worse
defect than the 1x resolution being fixed, and one nothing would have caught,
because a soft-but-complete PNG and a sharp-but-gutted PNG both look like "an
export happened". So culling is suspended for the duration of an export, with
a
nextTick()so Vue repaints before serialisation, and restored in thefinally. There is a test for the restore specifically: ifexportingwereonly reset on the happy path, one failed export would leave the axis unculled
for the rest of the session.
The culling fallback matters more than the fast path. When the viewport
cannot be measured — before mount, or in any headless environment where
clientWidthis 0 — the axis renders its FULL range rather than nothing. Anunmeasurable viewport must not read as "nothing is visible": a chart with no
axis is indistinguishable from a chart with no data, which is the same
conflation #14064 records for this view's empty state.
Three smaller decisions:
msForX()is written as the explicit inverse ofxForMs()and keptadjacent to it, so the pair cannot drift about where a millisecond lands.
same date whether or not culling is active. Without that, scrolling would
slide the whole axis onto different dates.
v-forwas keyed by array index. Under culling, index 0 is nolonger always
rangeStart, so ticks now carry and are keyed by theirms..gantt-scrollsits behindv-elseand does not exist until a timeline hasloaded, so the
ResizeObserveris attached from awatchon the ref ratherthan
onMounted— anonMounted-only hookup would observe nothing, leaveviewWidthat 0, and permanently take the no-culling fallback while appearingto work.
What Changed
Export (#14767) — three defects in one function:
devicePixelRatio x 2, floored at 1x, capped so neither dimension exceeds 8192revokeObjectURLafter theawait— leaked wheneverimg.onerrorfiredfinallyctx.fillStyle = '#ffffff'--bg-surfaceread from the live element, so it follows the active themeThe scale never drops below 1x: a very wide timeline should export whole and
soft rather than sharp and cropped. Unlike a raster pipeline there is no
source-resolution ceiling to respect — the source is vector SVG — so the only
limit is the canvas cap.
Axis (#14769) — viewport state (
scrollEl,viewLeft,viewWidth), aResizeObserverattached viawatch,msForX(), and culling with a 400pxoverscan either side. Observer released on unmount as well as on element
change.
Docs —
docs/research/canvas-grid-rendering-review.mdplus its_indexrow. This is the review all ten issues in this series came from, written as an
AutoBot-facing techniques audit. It carries no external product, repository or
author name, per the repo convention.
Verification
New
GanttTimelineView.rendering.test.ts:unmeasured viewport renders the full range, not an empty axis.
check.
Imageto reject rather than by asserting the happy path.The tests defeat two ways they could pass for the wrong reason: jsdom reports
clientWidthas 0 and itsscrollLeftsetter does not stick, so both aredefined outright — otherwise a culling assertion would pass because the
fallback rendered everything, which is the opposite of what it claims to check.
Not run locally, per the project rule that CI verifies correctness.
Model Used
Claude Opus 5 (1M context)
Not in scope
#12707 (no empty state) and #14064 (a load failure rendering as a legitimately
empty board) also live in this file, but they are error/empty-state concerns
spanning board and timeline, and #12707 proposes a shared component. They
pair with each other, not with rendering geometry.