Gantt: a plan shorter than the pane stretches to fill it end to end - #122
Merged
Conversation
The chart width was totalDays x the grain's fixed density, with no relation to the viewport — a four-week plan at Week zoom drew ~235px of chart and left the rest of the frame dead white, and every coarser grain shrank it further (32px at Quarter). The grain's density is now a floor, not the width: the canvas measures its timeline pane with a ResizeObserver and widens the day so a short plan spans the pane exactly, while a plan longer than the pane keeps the spec density and scrolls — which is what the grain switch is for. The effective density is threaded into the axis and the milestone layer so ticks, shading, bars and markers can never disagree, and the "T" (scroll to today) shortcut uses the same value. Under jsdom (no ResizeObserver) and before first measure the behaviour is unchanged, which keeps every existing width assertion valid; three new scale tests pin the stretch, the floor, and the fallback. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TArsz4CrMDAKmeALMozkR5
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
ib823
marked this pull request as ready for review
August 9, 2026 00:18
ib823
pushed a commit
that referenced
this pull request
Aug 9, 2026
… plan The timeline sized itself to the plan and the zoom control only relabelled the header, so a five-week phase filled the screen at every grain — "Quarter" showed two quarters — and there was nowhere to put work that had not been scheduled yet. Stretching short plans to fill the pane (#122) treated the symptom; this replaces the model. Each grain now carries its own planning horizon, independent of the plan: a quarter of days, a year of weeks, three years of months, five years of quarters, anchored on a whole unit just before the plan and scrolled horizontally. A day is the grain's width, full stop — 26px at Day on every screen — so two plans at one grain are directly comparable. The horizon extends rather than compresses when the plan or the viewport is longer, so a wide monitor shows more time, never wider time. - Hierarchical headers, as spanning bands rather than point labels, so a label always describes exactly the columns beneath it: Day = month/year over days; Week = month/year over weeks with daily gridlines; Month = year over months with weekly gridlines; Quarter = year over quarters with monthly gridlines. - Changing scale preserves the date in the middle of the screen, held as a date rather than a scroll offset because the window's origin moves with the grain. - Fitting the view to the plan is now the explicit "Fit project" command, which picks the finest grain the span fits in and centres it. - One working calendar (`lib/gantt-tool/working-calendar.ts`) feeds the weekend shading, the holiday markers AND the duration columns. They disagreed before: the axis shaded the region's public holidays while every working-day count had only ever been passed the project's own, so a bar over a public holiday was shaded and counted as a working day. - Holidays render at all four grains at their own proportional date, floored to 3px so they survive Quarter zoom — a marked day, never the week, month or quarter containing it — and carry name and date in a tooltip. Weekends stay at Day and Week only, where a day is wide enough to read as a day rather than as stripes. - ISO dates are parsed as local midnight throughout. Mixing `new Date("2026-06-17")` (midnight UTC) with a local-midnight origin was a day of error east of Greenwich, putting bars and the shading under them on different columns. Validated on the specified plan — a phase from 15 June 2026 to 13 July 2026 with a holiday on 17 June — asserting at all four grains that the phase stays 29 inclusive days from the 15th to the 13th, the holiday stays on the 17th, two days into the phase, at the same fraction along the calendar, and that the header bands tile the window with no gap or overlap. 1,790 unit tests, 23 bundle budgets, typecheck, lint and build all pass. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TArsz4CrMDAKmeALMozkR5
ib823
pushed a commit
that referenced
this pull request
Aug 9, 2026
… plan The timeline sized itself to the plan and the zoom control only relabelled the header, so a five-week phase filled the screen at every grain — "Quarter" showed two quarters — and there was nowhere to put work that had not been scheduled yet. Stretching short plans to fill the pane (#122) treated the symptom; this replaces the model. Each grain now carries its own planning horizon, independent of the plan: a quarter of days, a year of weeks, three years of months, five years of quarters, anchored on a whole unit just before the plan and scrolled horizontally. A day is the grain's width, full stop — 26px at Day on every screen — so two plans at one grain are directly comparable. The horizon extends rather than compresses when the plan or the viewport is longer, so a wide monitor shows more time, never wider time. - Hierarchical headers, as spanning bands rather than point labels, so a label always describes exactly the columns beneath it: Day = month/year over days; Week = month/year over weeks with daily gridlines; Month = year over months with weekly gridlines; Quarter = year over quarters with monthly gridlines. - Changing scale preserves the date in the middle of the screen, held as a date rather than a scroll offset because the window's origin moves with the grain. - Fitting the view to the plan is now the explicit "Fit project" command, which picks the finest grain the span fits in and centres it. - One working calendar (`lib/gantt-tool/working-calendar.ts`) feeds the weekend shading, the holiday markers AND the duration columns. They disagreed before: the axis shaded the region's public holidays while every working-day count had only ever been passed the project's own, so a bar over a public holiday was shaded and counted as a working day. - Holidays render at all four grains at their own proportional date, floored to 3px so they survive Quarter zoom — a marked day, never the week, month or quarter containing it — and carry name and date in a tooltip. Weekends stay at Day and Week only, where a day is wide enough to read as a day rather than as stripes. - ISO dates are parsed as local midnight throughout. Mixing `new Date("2026-06-17")` (midnight UTC) with a local-midnight origin was a day of error east of Greenwich, putting bars and the shading under them on different columns. Validated on the specified plan — a phase from 15 June 2026 to 13 July 2026 with a holiday on 17 June — asserting at all four grains that the phase stays 29 inclusive days from the 15th to the 13th, the holiday stays on the 17th, two days into the phase, at the same fraction along the calendar, and that the header bands tile the window with no gap or overlap. 1,790 unit tests, 23 bundle budgets, typecheck, lint and build all pass. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TArsz4CrMDAKmeALMozkR5
ib823
added a commit
that referenced
this pull request
Aug 9, 2026
* Gantt: true semantic zoom on a planning horizon, not a picture of the plan The timeline sized itself to the plan and the zoom control only relabelled the header, so a five-week phase filled the screen at every grain — "Quarter" showed two quarters — and there was nowhere to put work that had not been scheduled yet. Stretching short plans to fill the pane (#122) treated the symptom; this replaces the model. Each grain now carries its own planning horizon, independent of the plan: a quarter of days, a year of weeks, three years of months, five years of quarters, anchored on a whole unit just before the plan and scrolled horizontally. A day is the grain's width, full stop — 26px at Day on every screen — so two plans at one grain are directly comparable. The horizon extends rather than compresses when the plan or the viewport is longer, so a wide monitor shows more time, never wider time. - Hierarchical headers, as spanning bands rather than point labels, so a label always describes exactly the columns beneath it: Day = month/year over days; Week = month/year over weeks with daily gridlines; Month = year over months with weekly gridlines; Quarter = year over quarters with monthly gridlines. - Changing scale preserves the date in the middle of the screen, held as a date rather than a scroll offset because the window's origin moves with the grain. - Fitting the view to the plan is now the explicit "Fit project" command, which picks the finest grain the span fits in and centres it. - One working calendar (`lib/gantt-tool/working-calendar.ts`) feeds the weekend shading, the holiday markers AND the duration columns. They disagreed before: the axis shaded the region's public holidays while every working-day count had only ever been passed the project's own, so a bar over a public holiday was shaded and counted as a working day. - Holidays render at all four grains at their own proportional date, floored to 3px so they survive Quarter zoom — a marked day, never the week, month or quarter containing it — and carry name and date in a tooltip. Weekends stay at Day and Week only, where a day is wide enough to read as a day rather than as stripes. - ISO dates are parsed as local midnight throughout. Mixing `new Date("2026-06-17")` (midnight UTC) with a local-midnight origin was a day of error east of Greenwich, putting bars and the shading under them on different columns. Validated on the specified plan — a phase from 15 June 2026 to 13 July 2026 with a holiday on 17 June — asserting at all four grains that the phase stays 29 inclusive days from the 15th to the 13th, the holiday stays on the 17th, two days into the phase, at the same fraction along the calendar, and that the header bands tile the window with no gap or overlap. 1,790 unit tests, 23 bundle budgets, typecheck, lint and build all pass. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TArsz4CrMDAKmeALMozkR5 * Allow CI to be re-run manually; hoist the canvas scroll constant PR #123 opened with no validate or e2e run at all — the pull_request event did not spawn one, and reopening the PR did not either. A PR carrying no CI signal looks exactly like one whose CI has not finished, so the state was unrecoverable without pushing a commit. workflow_dispatch makes a re-run possible directly. MS_PER_DAY moves to module scope alongside GRAINS: it is a constant, not per-render state, and reads as one there. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TArsz4CrMDAKmeALMozkR5 --------- Co-authored-by: Claude <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
On the Timeline view, a short plan renders as a narrow sliver of chart with the rest of the frame dead white — and switching to a coarser grain shrinks it further (a 4-week plan is ~235px at Week zoom, 32px at Quarter). Expected: the timeline always covers the frame end to end, and the zoom control only changes date density.
Root cause
The chart width was
totalDays × PX_PER_DAY[grain]— a fixed density with no relation to the viewport, applied independently in the canvas, the axis, and the milestone layer.Fix
effectivePxPerDay(grain, totalDays, viewportPx)inscale.ts: the grain's density becomes a floor. A plan shorter than the pane widens its day so the chart spans the pane exactly; a longer plan keeps the spec density and scrolls (which is what the grain switch is for).GanttCanvasmeasures its timeline pane with aResizeObserverand threads the effective density intoTimelineAxisandGanttMilestones, so ticks, shading, bars, today rule and markers can never disagree. The "T" (scroll to today) shortcut uses the same value.ResizeObserverdoesn't exist — behaviour is unchanged, keeping every existing width assertion valid.Verification
tsc --noEmitclean,next lint --max-warnings=0clean, production build succeeds, 1,779 unit tests pass.🤖 Generated with Claude Code
https://claude.ai/code/session_01TArsz4CrMDAKmeALMozkR5
Generated by Claude Code