Skip to content

fix(layout): widen straight-style fan row pitch for stacked branch labels - #1269

Open
pinin4fjords wants to merge 1638 commits into
mainfrom
fix/1266-straight-fan-row-pitch
Open

fix(layout): widen straight-style fan row pitch for stacked branch labels#1269
pinin4fjords wants to merge 1638 commits into
mainfrom
fix/1266-straight-fan-row-pitch

Conversation

@pinin4fjords

@pinin4fjords pinin4fjords commented Jul 2, 2026

Copy link
Copy Markdown
Member

Summary

  • In diamond_style: straight, a 3+-way fork/join fan stacks every branch
    label on the same side (layer-column alternation picks a side once per
    column), so a branch's label lands in the gap toward the next branch row.
    A thick multi-line bundle's marker grows past the default label offset,
    and the label reads as cramped among the bundle lines.
  • compute_min_y_spacing now widens the global row pitch for a straight-style
    3+-way fan whose branches share a column, are fed by a thick bundle, and
    carry a single-line label. Symmetric-style fans are unaffected.
  • Adds _guard_wide_fan_branch_label_clears_next_row, a validate=True
    runtime check for the same invariant, plus a shared
    _column_grouped_branch_rows helper (in off_track.py) used by the guard,
    the pre-layout pitch calculator, and the new test.
  • Adds test_wide_fan_branch_label_clears_next_row_marker, parametrized over
    three gallery fixtures and both diamond styles.

Fixes #1266

Test plan

  • pytest passes (27348 passed), including the new invariant test
  • ruff check + ruff format clean on whole repo
  • mypy clean
  • Runtime validator (_guard_wide_fan_branch_label_clears_next_row) added and exercised against the full fixture corpus
  • Guard golden baseline regenerated (two expected diffs: rnaseq_sections.mmd's bbox height grew 140→146px; funcprofiler_upstream.mmd's guard trace shortened by 6 calls — both are direct, correct consequences of the wider pitch)
  • Visual review of render preview: 5 of 214 gallery renders changed
    • funcprofiler_upstream: improvement — its 7-way HUMAnN/RGI/mifaser/DIAMOND/eggNOG fan opens up, each branch label now clears the bundle instead of sitting inside it
    • rnaseq_sections, rnaseq_sections_manual, sarek_metro: neutral — pixel-level shifts only (a few px of canvas growth), visually flat; these already had enough incidental row pitch from surrounding content
  • Render-preview verdict: no detrimental deltas, no narrowing needed

This is a corpus-wide vertical change: any straight-style 3+-way fan with a
thick bundle gets a wider row pitch, so all 5 deltas were reviewed above.

Generated with Claude Code

pinin4fjords and others added 30 commits June 29, 2026 14:24
chore: update owner references after repo transfer to seqeralabs
…utton

- Add `b64urlEncodeGz`/`b64urlDecodeGz` using CompressionStream/DecompressionStream
- Share link button now encodes as `#mmd-gz=<b64>` (gzip + base64url), typically 60-70% shorter than the raw `#mmd=` format for realistic maps
- Old `#mmd=` fragment URLs continue to load correctly (backward compat read path)
- Bug-report reproduce links keep the uncompressed `#mmd=` format (URL embedded in GitHub issue body, length is not a constraint there)
- Add "Copy source" button that writes the raw .mmd text to the clipboard

Closes #982

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Extract _bytesToB64url/_b64urlToBytes to eliminate copy-paste between sync
and async encode/decode pairs. Add _hashParam for fragment extraction (shared
by loadFromHash and loadFromHashGz). Add _pageUrl for URL assembly. Remove
redundant hasGz guard in initEditor. Fix stack-overflow risk in b64urlEncodeGz
by replacing spread with forEach (mirrors existing b64urlEncode). Remove
comments that restate function names.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Keeping a geometric-bypass helper re-feeds the bypassed section's height
into the next pass's downstream-section placement, which lags by one
pass, so a kept fix needs one more settling pass to reach its fixed
point. That pass was gated on `validate`, so `compute_layout(validate=False)`
(the render path) shipped a stale, one-pass-short layout while
`validate=True` ran the extra pass and converged -- breaking
validate-flag idempotency for a fold + bypass-V + downstream-section map.

Run the settling pass whenever a bypass was kept, regardless of
`validate`; the flag now only layers checks onto the pass it shares with
the render path. Geometry no longer depends on the flag, and the folded
downstream section seats at its converged Y in both modes.

Adds examples/topologies/fold_bypass_creep.mmd (gallery + README +
guard-golden baseline) as the regression lock and a focused
validate-idempotency / fixed-point test.

Fixes #1171

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…or mmd-gz

- Run Prettier to reformat app.js (line-length wraps)
- Update share-link round-trip test: wait for hash to settle after async
  gzip completes before asserting URL, and check for #mmd-gz= not #mmd=

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…press

feat(playground): compress share links via gzip; add copy-source button
)

A cross-row feed into a RIGHT entry whose source is stacked directly
above the target (same column) descended its shared corridor via a
mid-descent lateral jog: the wrap led in to a source-side corner, dropped
to an inter-row staging channel, then stepped sideways onto the corridor
X partway down before continuing the descent.

When the corridor admits a clean straight drop from the source row to the
entry, _route_right_entry_wrap now leads straight to it at the top corner
and drops in (reusing _route_right_entry_drop_in), so the descent reads as
one vertical run. Gated on same-column and corridor clearance so the
adjacent-column wrap (whose band traverse runs the natural inter-row
channel between the boxes) and multi-column bypasses are untouched; only
fold_bypass_creep changes.

Adds check_right_entry_corridor_descent_no_jog + its final-phase guard so
the kink cannot silently return, plus a parametrised invariant test.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…e-idempotent

fix: run the post-bypass settling pass in both validate modes (#1171)
The Copy source button and its click wiring were added to the standalone
harness and to app.js, but not to the deployed Astro playground page. On
that page el("btn-copy-source") is null, so the unguarded addEventListener
throws during init and aborts app.js before boot() runs - the playground
hangs forever on "Starting Python runtime…".

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…rce-button

fix(playground): add missing Copy source button to Astro page
The playground "Report a bug" link builds its GitHub issue URL from the
REPO constant in app.js, which still referenced the pre-transfer owner, so
reports opened against the old repository. Update it (and the matching e2e
assertion) to seqeralabs/nf-metro.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
fix(playground): point bug-report link at seqeralabs/nf-metro
On a folded vertical-flow (TB/BT) section whose file terminus sits one
row below the forking station, the bypass V peeling around that terminus
seats on the trailing row, so its run-out flat is exactly the
trailing-station-to-exit-corridor gap. That gap was MIN_PORT_STATION_GAP
(16px), below MIN_STATION_FLAT_LENGTH (20px), so the V read as a curve
apex rather than sitting on a visible flat run.

position_ports now widens the perpendicular exit gap to a full station
flat when a bypass V seats on the trailing row. The runtime guard
_guard_bypass_v_flat_visible, previously LR/RL-only, gains a vertical-flow
run-out check so the same property is enforced at render time on TB/BT
sections; the horizontal grid-column gaps still feed the strike-clearance
loop alone.

Adds fold_bypass_creep_tight.mmd as a gallery + topology regression
fixture; test_bypass_v_has_horizontal_segment auto-covers it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Rename _tb_bypass_v_short_runouts to _vertical_bypass_v_short_runouts to
match its lanes_run_along_x (TB and BT) scope, and align the horizontal
sibling _bypass_v_flat_gaps_from onto the same lanes_run_along_y axis
vocabulary instead of a raw ("LR", "RL") direction tuple.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Reword the fold_bypass_creep_tight gallery description to drop the
mid-scalar colon that broke gallery.yaml parsing (and cascaded into the
links / e2e / render-diff content builds), and re-align the topology
README table to Prettier's column widths after the new row.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…reep

fix(layout): clear bypass-V run-out on a tight 1-row fold (#1177)
…ng sections (#1167)

A lowered fold_threshold relocates sections onto a return row, where a
left/right entry/exit authored for the unfolded grid can face away from the
column its connecting sections occupy; the connecting leg then wraps back
across the section's own box and the routing self-check aborts.

Add a parse-time grid pass that, for fold-relocated horizontal sections, turns
a flow-axis entry toward its producer sections and an exit toward its consumers
whenever those all sit strictly to one side. It runs before the intra-section
re-anchor so that pass no longer mistakes the relocated geometry for an internal
fold. A Tier-B validator (_guard_fold_relocated_flow_ports_face_connections)
pins the invariant.

On genomeassembly under fold this faces the convergence sink inward; the
residual same-side hairpin fold-back is tracked as #1182 with a strict-xfail
lock.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Disconnected components are placed in separate local column grids then
stacked into disjoint vertical bands, but each component's grid rows
start at its own local minimum, so two components could share a grid_row
even though they occupy different vertical bands. The inter-row cascade
(Stage 6.13/6.14) reasons by grid_row assuming it rises monotonically
with Y; a folded component spanning several rows let an un-renormalised
peer get shuffled into the band, colliding with the section above.

Renormalise each component's grid rows onto a contiguous global band
matching the physical stack order. Adds a fixture, a parametrised
grid-row monotonicity invariant test, and a gallery entry for the CI
render-diff.

Fixes #1164

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Hoist the next-band computation out of the shift loop into a single
max-comprehension and trim the explanatory comment to its load-bearing
invariant.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…st after fix for #1167

Extract _expected_flow_side(cols, col) in resolve.py: returns the horizontal
side a flow-axis port should face when its connecting sections all sit strictly
to one side, else None. The parse-time re-siding, the runtime guard, and the
invariant test now consume it instead of each hand-rolling the all-strictly-one-
side rule. The test reads the typed Port.side rather than splitting the port id.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
fix(layout): keep disconnected components clear under folding (#1164)
…main

Records the _guard_fold_relocated_flow_ports_face_connections call in the
trace for fold_bypass_creep_tight and disconnected_components_fold (added on
main); the guard returns early on both, so their layout is unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…rt-side

fix: face fold-relocated convergence-sink ports toward their connecting sections (#1167)
…bottom [skip ci]

A folded TB section's LEFT/RIGHT exit runs straight into a relocated
target's entry. Stage 6.5 aligned the TB section's bbox bottom to the
target's live bbox bottom, but that read happened before the bbox-shrink
phase collapsed the target's padding band, so the TB section was left
stranded below the target once it tightened. The straight inter-section
run then cleared the two sections by different distances (62px vs 50px on
fold_left_exit_right_entry / epitopeprediction at fold 7).

Align to the target's settled content bottom (_predict_section_content_bottom)
instead, so both sections tighten to the same edge. Adds a runtime guard
(_guard_fold_lr_exit_sections_share_bbox_bottom) and corpus + fixture
invariant tests, sharing scope via iter_fold_lr_exit_straight_runs.

Fixes #1162

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
pinin4fjords and others added 7 commits July 2, 2026 21:20
The first pass lifted every titled map's topmost drawn section to a single
clearance, which over-padded maps that already cleared the title: a map
whose box top sat just above the clearance was pushed a full grid pitch
down (e.g. differentialabundance, 05f_banner_labels moved ~58px).

Split the rule into a deadband: TITLE_BAND_OVERLAP_FLOOR is the hard
no-overlap floor (header badge level with the title's lowest glyph); only
a map breaching it is lifted, and it lands at TITLE_BAND_CLEARANCE a small
band clear. A map already clearing the title keeps its exact position.
_canvas_top_shortfall (lift-to-target) and _canvas_top_preserved
(hard-floor feasibility for the grid snap) replace the single deficit
helper; the guard and tests assert no-overlap rather than the lift target.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
fix(layout): clear a title band above top grid-row section headers
…and fix

Merging main brought the #1273 title-band-clearance guards into the trace, so
the new fixture's guard-call sequence grew (416->418).  Regenerate the golden
baseline to record them.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
fix(layout): top-align side-entered vertical section to a grown feeder row-mate
Same JSON auto-merge gap as before: the newly-merged rowmate fixtures'
guard traces didn't pick up this branch's added guard call.
pinin4fjords and others added 20 commits July 2, 2026 23:10
…track

A station where 3+ metro lines on distinct tracks converge, with no single
predecessor carrying the full bundle, hit the convergence branch in
_place_single_node and snapped to the first-declared line's base track -- an
extreme of the feeder spread -- forcing every other feeder into a longer detour.

In diamond_style: symmetric the merge now anchors on the median feeder track
(median_low, an actual feeder track, so it stays on-grid for even feeder
counts). Straight mode is unchanged.

Fixes #1277

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
fix(layout): anchor symmetric multi-track merge on the median feeder track
The title-band clearance in canvas.py's _canvas_top_shortfall fired
whenever a titled map's header would overlap it, regardless of whether
render actually draws anything there. render.svg only draws a title or
standalone logo when the map isn't --bare and the logo (if any) doesn't
fold into the legend; a titled map combining a legend-embedded logo
with a header that would otherwise overlap the band paid for clearance
nothing occupies.

prepare_graph now resolves graph.reserve_title_band from --bare,
%%metro legend:, and whether a logo is configured (conservatively, via
the new render.legend.logo_certainly_shows helper) before layout runs,
and canvas.py gates the lift on it. HTML output always reserves, since
its embedded SVG forces the legend off and ignores --bare regardless.

Fixes #1278
A pre-migration check for moving off the legacy GitHub Pages builder,
which has been flaking repeatedly (see #1281's stalled/errored preview
build). Packages the live gh-pages branch through
actions/upload-pages-artifact - the step an Actions-based deployment
would use - without calling actions/deploy-pages, so it never touches
the live site or its Pages Source setting. workflow_dispatch only.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
v3 (the tag used in the initial dry-run workflow) invokes
actions/upload-artifact@v4 internally without a SHA, which this repo's
pinned-actions policy rejects even though the unpinned reference is
inside a third-party composite action rather than our own workflow
file. v5.0.0 pins that internal call to a commit SHA, so it passes.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Publishes the gh-pages branch via actions/configure-pages +
upload-pages-artifact + deploy-pages whenever that branch changes.
Replaces the dry-run-only workflow from #1282/#1283, now that the
packaging step is proven out - this one actually deploys.

Inert until Settings > Pages > Source is switched to "GitHub Actions":
until then, deploy-pages rejects the deployment and the legacy
branch-build publisher keeps serving the site unaffected.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Publishes the gh-pages branch via actions/configure-pages +
upload-pages-artifact + deploy-pages whenever that branch changes.
Supersedes the dry-run-only workflow from #1282/#1283, now that the
packaging step is proven out - this one actually deploys.

Inert until Settings > Pages > Source is switched to "GitHub Actions":
until then, deploy-pages rejects the deployment and the legacy
branch-build publisher keeps serving the site unaffected.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The gh-pages branch is a content-only orphan branch with no
.github/workflows directory, so a `push` trigger scoped to it can
never match a workflow definition and never fires. Listen for
completion of the three workflows that publish into gh-pages instead,
guarding on a successful upstream conclusion.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
ci: fire Pages deploy via workflow_run, not gh-pages push
Actions-source deployment does not fit this repo: the docs, render-preview
and cleanup workflows accumulate content into the gh-pages branch, but
actions/deploy-pages keys each deployment on the triggering commit SHA.
Under workflow_run that is always the default-branch head, so repeated
same-SHA deploys do not resurface newly added content and previews
silently fail to publish. Pages reverts to the branch builder
(build_type: legacy), which redeploys the branch on every push.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
ci: remove the Actions-based Pages deploy workflow
The publish step pushes the preview to the gh-pages branch, but GitHub
Pages builds and serves it asynchronously afterwards. Announcing the
link immediately produced a comment pointing at a URL that 404s until
the build finishes. Poll the preview URL after pushing and only post
the ready comment once it serves; otherwise post a still-publishing
notice.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
ci: gate render-preview comment on the URL being live
The publish job announced the preview as soon as it pushed to gh-pages,
but GitHub Pages serves the branch asynchronously. For a fresh preview
the old wait-for-200 check sufficed, but for an update the URL already
returned 200 from the previous render, so the comment could point at
stale content while the new render was still building.

Embed the render run id in the diff page (build_render_diff.py --marker,
passed as github.run_id) and poll until the served page carries that
exact marker before posting the ready comment; otherwise post a
still-publishing notice.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
ci: gate render-preview comment on a per-run content marker
Encodes the recurring legacy-builder stall and its re-trigger fix, the
pr-renders -> pr-render-publish -> build chain, the render content
marker, and the CDN caching gotchas that make preview verification
misleading.
skill: add pages-ci-doctor for Pages publishing issues
@pinin4fjords
pinin4fjords force-pushed the main branch 2 times, most recently from a90d751 to d134b08 Compare July 28, 2026 11:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Straight-style fan rows too tight for stacked single-line branch labels

3 participants