feat: improve consistent labels, figure-wide axes, and navigation cues at the multi-panel figure overview#639
Conversation
At the figure lobby (SUBPLOT scope of a multi-panel/facet figure), the title was reachable only via a bare 't', while an individual subplot reaches its labels through the label scope (l t / l x / l y / l s / l c) because a bare 't' there toggles Text mode. The lobby's label scope was also missing X/Y, so l x and l y did nothing. Make the lobby consistent with a subplot: - Add ANNOUNCE_X / ANNOUNCE_Y to FIGURE_LABEL so l x / l y announce the active subplot's (shared) axis labels. - Remove the direct 't' title binding from SUBPLOT; the title is now reached via l t. This also makes the auto-generated help render l t / l x / l y / l s / l c uniformly instead of a lone direct 't'. - Resolve axis labels from the active subplot's trace when announcing at figure/subplot level, so l x / l y work outside a single trace. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HFJwss8TaoKhFFQRZNbdxU
…s source Complete the figure-lobby label scope so it mirrors an individual subplot in full: add ANNOUNCE_Z (l z) alongside l x / l y / l t / l s / l c. AnnounceZCommand now resolves its level/group/trend label from the active subplot's trace via the shared resolver (renamed resolveActiveTraceState), degrading to "not available" for chart types with no z (box, single-line). The resolver reads the *currently focused* subplot's trace, so in a facet figure (shared axes) l x / l y / l z give the figure-wide labels, while in a general multi-panel figure with differing axes they track whichever subplot the cursor is on and update as the user navigates between subplots. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HFJwss8TaoKhFFQRZNbdxU
…lobby In a multi-panel figure each panel can carry different axes, so at the figure lobby an axis/level announcement now prefixes the focused subplot's position (e.g. "Subplot 2, X label is Month") via labelSourcePrefix(). The prefix is dropped once inside a subplot, in single-panel figures, and in terse mode, matching the existing minimal phrasing there. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HFJwss8TaoKhFFQRZNbdxU
Code ReviewReviewed the diff ( SummaryThis is a clean, well-scoped change that follows MVVC (all changes stay in the Command/Service layer, no model or view changes needed) and KISS. I traced through the logic and it holds up:
Minor / non-blocking notes
Security / PerformanceNo concerns — this is pure text-formatting/keymap logic, no new I/O, no user-controlled strings beyond existing authored JSON labels (already trusted elsewhere in the codebase), and no perf-sensitive paths touched. Nice work — the layered docstrings explaining why (facet vs. general multi-panel, terse mode, single-panel) make this easy to review and will help future readers. |
There was a problem hiding this comment.
Pull request overview
This PR aligns multi-panel “figure lobby” label access with subplot behavior by expanding the lobby’s label chord (l …) to include axis labels and by ensuring axis/level announce commands resolve labels from the currently focused subplot’s active trace.
Changes:
- Extended
FIGURE_LABELkeybindings sol x / l y / l zwork in the figure lobby (mirroringTRACE_LABEL). - Updated axis/level announce commands to resolve labels from the focused subplot’s trace and optionally prefix announcements with the subplot index in non-terse mode.
- Added unit tests covering lobby behavior for
l x / l y / l z, including terse-mode behavior and fallbacks.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
src/service/keybinding.ts |
Adds lobby label-scope bindings for X/Y/Z and adjusts subplot-scope description bindings/comments. |
src/command/describe.ts |
Adds shared trace-resolution logic and subplot-prefixing for axis/level announcements. |
test/command/announce-axis.test.ts |
Introduces tests validating axis/level announcements at the multi-panel figure lobby. |
| protected resolveActiveTraceState(): NonEmptyTraceState | null { | ||
| const state = this.context.state; | ||
| if (state.empty) { | ||
| return null; | ||
| } | ||
| if (state.type === 'trace') { | ||
| return state; | ||
| } | ||
| if (state.type === 'subplot') { | ||
| return state.trace.empty ? null : state.trace; | ||
| } | ||
| // Figure lobby: read the active subplot's active trace. | ||
| if (state.subplot.empty || state.subplot.trace.empty) { | ||
| return null; | ||
| } | ||
| return state.subplot.trace; | ||
| } |
| // Title/subtitle/caption/axis labels are reached through the label scope | ||
| // (l t / l x / l y / l s / l c), matching an individual subplot where a bare | ||
| // 't' toggles Text mode rather than announcing the title. |
| const command = new AnnounceXCommand( | ||
| createMockContext(state), | ||
| textViewModel, | ||
| createMockAudioService(), | ||
| createMockTextService(), | ||
| createMockDisplayService(), | ||
| ); |
🔎 MAIDR example preview is readyOpen the preview: https://pr-639.maidr-preview.pages.dev
Built from |
Add an optional figure-level axes concept so a facet grid can declare one
shared X/Y label at the figure level instead of repeating it on every panel,
and announce labels with a consistent "figure-global first, else focused
subplot" precedence at the lobby.
Schema:
- Maidr.axes?: { x?: AxisConfig; y?: AxisConfig } — optional figure-wide axis
labels, same per-axis shape as a layer's axes (only label is read here).
- Figure parses them; FigureState carries xAxis/yAxis; Context exposes
figureXAxis/figureYAxis + isAuthoredAxisLabel, mirroring subtitle/caption.
Announcements at the multi-panel lobby:
- l x / l y: authored figure-wide label wins ("Figure X label is ..."),
otherwise fall back to the focused subplot ("Subplot N, X label is ...").
- l t: figure title wins, otherwise fall back to the focused subplot's own
title ("Subplot N title is ...") instead of reporting "No title available".
Backward compatible: absent axes -> empty sentinel -> not authored -> falls
back to the focused subplot, identical to prior behavior. Inside a subplot and
in single-panel figures the trace's own axis/title are used unchanged. Docs
updated in SCHEMA.md.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HFJwss8TaoKhFFQRZNbdxU
Code ReviewReviewed the diff (docs/SCHEMA.md, src/command/describe.ts, src/model/context.ts, src/model/plot.ts, src/service/keybinding.ts, src/type/grammar.ts, src/type/state.ts, plus new/updated tests). Overall this is a clean, well-scoped change that follows MVVC boundaries — I traced through the FindingsTest coverage gap for the new figure-wide axes feature (most significant item). Minor: Minor/behavioral note (by design, not a bug): figure-wide label always wins over per-subplot truth. Keybinding removal ( Things I verified directly
I could not execute the Jest suite / 🤖 Generated with Claude Code |
…ption The 'd' chart-description modal previously left the axes section blank at the multi-panel figure lobby. Now it surfaces the authored figure-wide X/Y labels (a facet grid's shared axes) via DescriptionService.getFigureAxes(), reusing the same Context accessors as the l x / l y announcements. Per-subplot axes stay out of the figure overview (they belong to each subplot's own summary), and figures without authored figure-wide axes still render an empty axes object, so the modal hides the section exactly as before. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HFJwss8TaoKhFFQRZNbdxU
Review: feat(labels) — full L-chord + figure-wide axis labels at the multi-panel figure lobbyI read through the full diff (keybinding, model, command, service, schema, and tests) and traced the logic through Strengths
Minor nits (non-blocking)
Bugs / correctnessNone found. I traced the figure-wins-else-subplot-fallback logic for Security / PerformanceNo concerns — this is pure string formatting from typed model state, no user-controlled strings reach the DOM unsanitized ( Test coverageGood: new/updated tests cover figure-wide-wins vs. subplot-fallback vs. terse mode vs. empty state for X/Y/Z, the title fallback chain at the lobby, and the description-modal figure-wide axes (including the "only one axis authored" case). I'd call coverage solid for this change. Nice work — small, well-tested, and the precedence rules are easy to follow. The two nits above are optional cleanup, not blockers. 🤖 Automated review by Claude |
- resolveActiveTraceState: narrow the focused subplot to its non-empty variant via a local before reading trace, and document that the 'subplot' branch is a defensive, runtime-unreachable path. - keybinding: reword the SUBPLOT_KEYMAP comment to include l z and avoid implying a bare 't' binding exists at the lobby. - tests: invoke the trace-level announce case under Scope.TRACE_LABEL (not FIGURE_LABEL) so it reflects real trace-level usage, and add isAuthoredAxisLabel / figure-axis-getter coverage in context-authored. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HFJwss8TaoKhFFQRZNbdxU
Code ReviewReviewed the full diff ( Correctness
Minor / non-blocking
Architecture / style
Overall: solid implementation, good precedent-following, and the test additions (888 total per the PR description) give reasonable confidence. The two items above are polish suggestions, not blockers. |
…omment - Add terse-mode and trace-level (single-panel) cases for AnnounceYCommand and AnnounceZCommand so their duplicated execute() branching is guarded to the same degree as AnnounceXCommand, protecting a future refactor of the shared base-class helpers. - Reword the SUBPLOT_KEYMAP comment to reference "trace scope" (where a bare 't' toggles Text mode) instead of "an individual subplot", which read ambiguously. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HFJwss8TaoKhFFQRZNbdxU
ReviewWent through the diff ( Code quality / architecture
Potential issues
Test coverage
Security / performanceNo concerns — this is pure client-side state/announcement logic with no new I/O, no user-controlled strings reaching innerHTML/eval, and no perf-sensitive hot paths touched. Docs
Nice work — this is a solid, incremental accessibility improvement with real test discipline behind it. |
Rounds out AnnounceTitleCommand coverage with the terse-mode path of the figure→focused-subplot title fallback (previously only the verbose case was tested), guarding the announce() prefix-stripping behavior for titles. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HFJwss8TaoKhFFQRZNbdxU
Code ReviewReviewed the diff (docs/SCHEMA.md, src/command/describe.ts, src/model/{context,plot}.ts, src/service/{description,keybinding}.ts, src/type/{grammar,state}.ts, plus the new/updated tests). Overall this is clean, well-scoped work that follows the MVVC architecture and the existing Strengths
Minor / nit-level notes
Bugs / correctnessNone found. Traced the Security / performanceNo concerns — this is pure read-path label resolution and formatting, no new I/O, no user-controlled strings beyond what's already trusted from the authored MAIDR JSON (same trust boundary as existing title/subtitle/caption). Nice, focused PR — the incremental commit history (adding X/Y, then Z, then the subplot-prefix, then figure-wide precedence, then addressing self-review nits) made this easy to follow. |
Collapse the near-identical AnnounceXCommand/AnnounceYCommand execute() bodies into a single AnnounceCommand.announceAxisLabel(axis) helper, so the figure-wide precedence, subplot fallback, terse handling, and restoreScope wiring live in one place instead of being copy-pasted. Behavior is unchanged (guarded by the existing X/Y figure-wide / fallback / terse / trace-level / empty tests). Also clarify two comments flagged in review: the SUBPLOT_KEYMAP note now states plainly that a bare 't' does nothing at the lobby (mirroring trace scope where 't' is TOGGLE_TEXT), and getFigureAxes notes why there is no figure-level z. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HFJwss8TaoKhFFQRZNbdxU
Review: Full L-chord + figure-wide axis labels at the multi-panel lobbyWent through the diff ( Correctness
Minor nits (non-blocking)
Test coverageThe new Note on verificationI reviewed this statically (couldn't execute Nice work overall — the schema addition is minimal and additive, MVVC boundaries are respected (all new logic lives in |
With the bare 't' binding removed from SUBPLOT_KEYMAP, every restoreScope() caller (announce x/y/z/title/subtitle/caption) is now bound only inside a label scope. Reword the comment so it describes the guard as defensive against a future non-label-scope binding rather than an existing caller. The guard itself is kept (scopeRestore tests exercise it and it is a cheap safeguard). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HFJwss8TaoKhFFQRZNbdxU
Code ReviewReviewed the diff ( OverallWell-scoped and consistently applied. The new figure-wide axis concept follows the same sentinel + Architecture (MVVC)
Correctness
Minor observations (non-blocking)
Test coverageNew tests ( Security / PerformanceNo concerns — this is pure string/state resolution logic with no new I/O, no Nice work — the commit history itself shows good incremental self-review (dedup pass, comment corrections, added terse/trace-level guards for Y/Z to match X). Nothing here blocks merging. |
…xit cues Make the multi-panel figure lobby (Scope.SUBPLOT) behave like a real navigation level instead of a dead area: - t / s now toggle Text and Sonification modes at the lobby (global modes), and b announces a "Braille is not available here" warning + warning tone instead of doing nothing — mirroring how `l t` warns when text mode is off. - Entering a subplot (Enter) plays a rising three-note "enter" cue and announces "Entered subplot N of M, <type> plot."; the transition was previously silent. - Returning to the lobby (Esc) plays a falling "exit" cue and announces "Returned to figure overview, subplot N of M.". exitSubplot() already re-announces the figure via the observer chain, so the exit notify() is issued after it: the message overrides the nav value in the alert and React batches both dispatches into one clean announcement (no double read). AudioService gains playSubplotEnterTone/playSubplotExitTone (rising/falling C5-E5-G5 arpeggios) reusing the existing menu-tone engine, so they respect audio mode and node disposal. ToggleBrailleCommand and the two move-context commands take the notification/audio services they need; CommandFactory wires them. Verified end-to-end in Chromium on examples/multipanel.html: t/s/b, Enter, and Esc all announce as intended and the exit produces a single message. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HFJwss8TaoKhFFQRZNbdxU
Review: feat(labels) full L-chord + figure-wide axis labels at the multi-panel figure lobbyI reviewed the full diff (docs, schema/types, model/context, commands, services, and the new/updated tests) against Code quality & architecture
Potential issues / things to verify
Test coverage
Security / performance
Backward compatibility
Nice work overall — the commit history shows good iterative response to self-review (nits, dedup, comment corrections across the later commits), and the final state is consistent and well-tested. |
AnnounceZCommand announced "Z-axis is not available" while AnnounceXCommand / AnnounceYCommand say "X label is not available" / "Y label is not available". Now that Z is announced with the same "Z label is ..." phrasing on success, the unavailable path was the last inconsistency — reword it to "Z label is not available" so all three axis announcements match. Terse mode already shared the "unavailable" cue. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HFJwss8TaoKhFFQRZNbdxU
Review: feat(lobby) — consistent labels, figure-wide axes, and navigation cues at the multi-panel figure lobbyI read through the full diff (describe.ts, move.ts, toggle.ts, context.ts, plot.ts, audio.ts, description.ts, keybinding.ts, grammar.ts/state.ts, and the new/updated tests) and traced the runtime behavior rather than just the code shape. Overall this is a clean, well-scoped change that follows the MVVC architecture closely — commands stay thin, the new figure-wide-axis concept is threaded consistently through A few things worth double-checking, none of which look like blocking bugs: 1. Stale architectural-note comment on
|
…verage - Update the MoveToTraceContextCommand architectural note: it now also injects AudioService/NotificationService, and injecting feedback services into commands is the norm here (ToggleBrailleCommand, MoveToSubplotContextCommand), so drop the overstated "do not copy this pattern" framing. - Suppress the spoken "Entered subplot" alert when entry moves focus into the braille textarea (braille was enabled): a simultaneous role="alert" update can be clipped/reordered by the focus change, and the braille display + enter tone are the feedback there. The enter cue tone still plays in both paths. - Tests: assert the braille-enabled entry plays the tone but skips the alert, and that a reused AnnounceXCommand re-reads the focused subplot (so a general multi-panel figure with different per-panel axes announces the current panel, guarding against a future Context.state memoization regression). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HFJwss8TaoKhFFQRZNbdxU
Clarify in the Top-Level Figure Properties section that only figure-wide x/y axis labels are supported; the Z axis is inherently per-trace, so l z in the multi-panel lobby always reports the focused subplot's own Z label. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HFJwss8TaoKhFFQRZNbdxU
ReviewI read through the full diff (schema, commands, services, keybindings, and the new test suites) and traced several of the trickier interactions by hand: the label/announce precedence chain, the enter/exit cue + notification ordering, the Note: I wasn't able to run What looks solid
Minor / non-blocking observations
Test coverageCoverage looks thorough for the new behavior: label precedence matrix (figure-wide vs. subplot vs. absent, × terse/verbose), the braille "not available" warning, enter/exit cues across text modes (including the OFF-plays-tone-only and braille-suppresses-alert cases), the figure-lobby boundary cue, and real No blocking issues found. Nice work on this one — the iterative self-review commits (fixing the stale architectural comment, adding missing terse/off cases, fixing the braille-exit cue silence) suggest the diff already had a solid pass before this review. |
At the multi-panel figure lobby, terse text mode is now title-first: - Arrow navigation between subplots announces just the focused subplot's own title (e.g. a facet label), dropping the "Subplot N of M" position framing, the trace types, and the "Press ENTER" prompt. When a subplot has no authored title it falls back to a concise "Subplot N, <types>". - Pressing Enter to activate a subplot announces "Subplot N, <title>" (or "Subplot N" when untitled) instead of "Subplot N of M". Verbose mode is unchanged and keeps the full framing. Placeholder title defaults substituted by the model are treated as "no title" so terse navigation never reads back a bare "unavailable". Verified end-to-end against examples/multipanel.html: terse lobby navigation reads back the three panel titles, and Enter announces "Subplot 3, Bar Plot 2: Random Values". Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HFJwss8TaoKhFFQRZNbdxU
…obby
Two figure-lobby refinements:
- The Esc exit cue now mirrors the terse entry cue. In terse mode it names
the returned-to panel and, when authored, its title
("Figure, subplot 2, Sales" / "Figure, subplot 2" when untitled),
dropping the "of N" framing. Verbose is unchanged. A shared
focusedSubplotTitle() helper resolves the focused subplot's authored
title (rejecting placeholder defaults) for both exit paths (trace-scope
Esc and braille-mode Esc).
- Backspace now also returns from a subplot to the figure lobby, alongside
Esc, by binding it to MOVE_TO_SUBPLOT_CONTEXT in the trace keymap.
Verified end-to-end against examples/multipanel.html: terse Esc announces
"Figure, subplot 2, Bar Plot: Random Values", and Backspace performs the
same exit.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HFJwss8TaoKhFFQRZNbdxU
Review of PR #639I reviewed the diff, checked the new precedence/fallback logic, keybinding wiring, and audio cue timing, and spot-verified a couple of claims against the source. Summary Strengths
Issues
Test coverage gaps
Nitpicks / style
Nice work overall -- the precedence and mode-awareness logic is well thought out and well tested; the two flagged issues are minor/edge-case rather than blocking. |
Addresses reviewer feedback on PR #639 about duplicated placeholder-title logic. Extracts two pure helpers into the model as the single source of truth: - isAuthoredTitle(title): the placeholder-rejection rule, previously copied in Context.isAuthoredTitle and TextService. Context now delegates to it and TextService imports it, so the rule can no longer drift across copies. - focusedSubplotTitle(state): the 'read the focused subplot's authored title from a figure state' traversal, previously duplicated as subplotCue's focusedSubplotTitle and TextService's lobbySubplotTitle. Both the lobby navigation text and the exit cue now call the one model helper. Pure refactor: no behavior change (937 tests pass with no assertion edits; re-verified end-to-end against examples/multipanel.html). Also closes a test gap flagged in review: figureAxes.test.ts now covers an authored-but-blank figure axis label collapsing to the empty sentinel. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HFJwss8TaoKhFFQRZNbdxU
Review: Multi-panel figure lobby — labels, figure-wide axes, and navigation cuesSolid, well-scoped feature PR. It extends the A few things worth a look: 1. Verbose enter/exit cues never speak the subplot title (possible inconsistency)
if (text.isTerse()) {
return title ? `Subplot ${index}, ${title}` : `Subplot ${index}`;
}
const suffix = plotType ? `, ${plotType} plot` : '';
return `Entered subplot ${index} of ${size}${suffix}.`; // title droppedSo a facet panel titled "Sales in North" is announced by name in terse mode, but in verbose mode (arguably the mode that should carry the most information) the user only hears "Entered subplot 2 of 4, bar plot." — never the title, even though it was authored and is available at the call site. Since 2.
|
Addresses reviewer feedback on PR #639: the figure-wide `axes` field reused the full per-layer AxisConfig, but only `label` is honored at the figure level. A chart author setting `maidr.axes.x.min` would have it silently ignored. Narrowing the type to `Pick<AxisConfig, 'label'>` makes the schema self-documenting and surfaces unsupported figure-level axis properties as a compile error instead of a silent no-op. Docs updated to match. No runtime change: Figure only ever read `.label`, and all existing usages author only `label`. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HFJwss8TaoKhFFQRZNbdxU
Code ReviewI read through the full diff (schema, model, commands, services, keybindings, and the new tests) and traced the key control-flow paths ( A few things worth a look: Minor / worth confirming
Things that look correct (verified, not just read)
Test coverageCoverage is strong — new/updated tests exercise: figure-wide axis precedence and fallback, terse/verbose wording variants, entry/exit cue tone + message pairing (including the OFF-mode silent case and the braille-focus-suppresses-alert case), out-of-bounds lobby navigation cues, and No security concerns (no new user input parsing beyond typed JSON schema fields, no DOM injection, no new external calls). No obvious performance concerns — the new logic is all O(1) state reads on user-triggered key events. I wasn't able to run |
…tack invariant Addresses non-blocking reviewer feedback on PR #639: - Add a comment on the esc,backspace binding for MOVE_TO_SUBPLOT_CONTEXT explaining that Backspace is bound only in TRACE scope (not the braille/ review text areas), so it never collides with Backspace's text-delete meaning in editable fields — a value judgment worth documenting for screen-reader/braille users. - Add contextStackInvariant.test.ts pinning the 'no bare Subplot on the context stack' invariant that AnnounceCommand.resolveActiveTraceState / AnnounceTitleCommand's defensively-unreachable 'subplot' branch depends on. A future enterSubplot/exitSubplot refactor that broke it would now fail loudly instead of silently degrading label announcements. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HFJwss8TaoKhFFQRZNbdxU
ReviewI read through the full diff plus the surrounding source ( SummaryThis turns the multi-panel figure lobby into a mode-aware navigation level (full Traced end-to-end and confirmed correct:
Minor findings (non-blocking)
Test coverageStrong — new tests cover the figure-lobby precedence for Security / performanceNo concerns — this is UI/navigation/announcement logic with no new external input parsing beyond the already-validated Nice work — the comments throughout (especially the "why not use |
Addresses non-blocking reviewer feedback on PR #639: AnnounceTitleCommand's figure branch hand-rolled the same 'focused subplot title' traversal + placeholder rejection that the shared focusedSubplotTitle() helper (used by move.ts and text.ts) already provides. Migrate it to the single call site so the traversal and placeholder-rejection rule live in one place. No behavior change: focusedSubplotTitle(state) returns the authored subplot title or '' (already filtered via isAuthoredTitle), which the truthiness check consumes identically to the previous inline logic. All 939 tests pass. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HFJwss8TaoKhFFQRZNbdxU
Review: Multi-panel figure lobby as a first-class navigation levelI reviewed this PR by reading the full diff plus the surrounding files in context ( OverallThis is a well-scoped, well-tested change. It follows the MVVC architecture correctly (Model → Observer → Service → ViewModel → Redux → View), the schema addition ( A few things I specifically checked and confirmed are not bugs, since they looked suspicious at first glance:
Minor suggestions (non-blocking)
HousekeepingI wasn't able to run No blocking issues found — nice work on keeping the announcement logic mode-aware and on documenting the trickier invariants inline. |
… collaborator
Addresses the reviewer feedback that MoveToTraceContextCommand,
MoveToSubplotContextCommand, and ExitBrailleAndSubplotCommand had each grown
to 6-7 constructor args, all three gaining the same AudioService +
NotificationService + TextService trio to play a navigation tone and speak a
transition announcement.
Introduce a small stateless command-layer collaborator, SubplotCue (in
subplotCue.ts, alongside the pure message builders it wraps), that bundles the
trio and centralizes the repeated "play tone / build mode-aware message /
notify when non-null" sequence:
- playEnterTone() tone only (so the braille path can cue the tone while
suppressing the spoken alert)
- announceEntry(...) mode-aware entry alert
- announceExit(state) exit tone + mode-aware alert (derives the title via
focusedSubplotTitle internally)
The three commands now take a single "cue: SubplotCue" param instead of the
trio. All gating and ordering stay in the commands (lobby check, braille alert
suppression, exitSubplot-before-announce, plotType/title computed via
context.isAuthoredTitle); SubplotCue is deliberately unaware of Context,
braille, and lobby/scope. CommandFactory constructs one shared SubplotCue and
injects it into all three. Test mock helpers wrap the same audio/notification/
text mocks in a thin real SubplotCue, so every tone/notify assertion is
unchanged.
Pure refactor: no announced string, tone, ordering, or gating changed.
type-check + lint clean; 939/939 tests pass; verified end-to-end against
examples/multipanel.html that the enter/exit 3-note cues and messages still
fire (including via Backspace).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HFJwss8TaoKhFFQRZNbdxU
|
Review Summary This is a well-scoped, thoroughly-tested feature PR that promotes the multi-panel figure lobby ( Architecture / design
Correctness
Minor nits (non-blocking)
Test coverage I didn't run the build/test suite in this environment (no node_modules/network available), so please confirm CI is green - but nothing in the diff itself raised correctness or security concerns. |
…lot title
Per maintainer direction, rebalance the multi-panel lobby announcements:
- TERSE now identifies a panel by its authored title ALONE, dropping the
"Subplot N" framing entirely. Arrow navigation and the enter cue read back
just the title (e.g. "Bar Plot: Random Values"); the exit cue keeps the
"Figure," return marker then the title (e.g. "Figure, Bar Plot: Random
Values"). The bare "Subplot N" position is used only as a fallback for a
subplot that has no authored title.
- VERBOSE now includes the authored title alongside the existing structural
framing:
arrow -> "Subplot 2 of 4, Sales in North: This is a bar plot. Press 'ENTER'..."
enter -> "Entered subplot 2 of 4, Sales in North, bar plot."
exit -> "Returned to figure overview, subplot 2 of 4, Sales in North."
When a subplot has no title, verbose is unchanged.
OFF mode is unchanged (tone only). Behavior verified end-to-end against
examples/multipanel.html across both modes; type-check + lint clean; 942/942
tests pass (added verbose-with-title coverage for the enter, exit, and
arrow-navigation paths).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HFJwss8TaoKhFFQRZNbdxU
Review: Figure lobby as a first-class navigation levelSolid, well-scoped PR. The precedence logic (figure-wide → focused-subplot → "not available"), terse/verbose/off handling, and backward compatibility for figures that omit Verified non-issue (worth calling out since it looked suspicious at first glance)
Architecture: message-building duplicated across Service and Command layers
Minor: intentionally-dead branch
Test coverageCoverage is strong: figure-wide vs. subplot-level precedence, terse/verbose/off across all the new announce paths, single-panel (no figure-wide code path triggered) and empty-trace fallbacks, the braille-in-lobby warning, enter/exit tones, and the stack invariant are all exercised. One gap I didn't see covered: a test asserting the description modal ( Automated checksI wasn't able to run Nice attention to detail on the accessibility framing (terse vs. verbose wording, distinct enter/exit tones, mode-aware fallbacks) — this is a thoughtful extension of the existing lobby behavior. |
Closes a coverage gap flagged in PR #639 review: DescriptionService.getFigureAxes already filters an authored-but-blank figure axis label via isAuthoredAxisLabel, mirroring the l x / l y blank-label handling, but the d-modal path was not directly tested. Adds a case asserting a whitespace-only figure X label is omitted while an authored Y label is surfaced. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HFJwss8TaoKhFFQRZNbdxU
Review: Multi-panel figure lobby navigation (#639)Went through the full diff plus the actual PR source (checked out via Strengths
Minor observations (non-blocking)
Not verifiedI was unable to run Nice work overall — the lobby now behaves as a proper first-class navigation level rather than a second-class stub, and the precedence/fallback rules (figure-wide → subplot → "not available") are implemented consistently across text, audio, description, and braille. |
… guard Addresses two non-blocking notes from PR #639 review: - Add test/service/review.test.ts. ReviewService had no test at all, and this PR widened it from Observer<TraceState> to Observer<PlotState> for review mode at the figure lobby. The new tests exercise the figure-state path (accepted + formatted), the disabled/empty-state early returns, and the on/off announcements. - Document why ExitBrailleAndSubplotCommand's multi-panel branch needs no scope===SUBPLOT guard (unlike MoveToSubplotContextCommand): braille is only reachable from inside a trace, so the enclosing isMultiPanel branch always has a [Figure, Subplot, Trace] stack and exitSubplot() always pops a real subplot, so the announced exit always corresponds to a real transition. No behavior change. 948/948 tests pass; type-check + lint clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HFJwss8TaoKhFFQRZNbdxU
Review: Multi-panel figure lobby as a first-class navigation levelNice, well-scoped feature with strong test coverage (9 new/updated test files) and clean adherence to the MVVC/Observer patterns described in Things worth double-checking
Nits
Architecture / conventions
Overall this looks solid and ready to merge, modulo confirming points 1 and 2 above are intentional (or fixing them), and possibly tightening the test assertion in point 3. |
PR #639 review correctly flagged that the 'authored-but-blank axis' test had a trivially-true assertion (state.yAxis.trim() === '') and a misleading name: the model stores a whitespace label verbatim and does NOT collapse it to the empty sentinel at parse time. Rewrite it to document the real behavior — raw storage (xAxis === '', yAxis === ' ') — and to verify the meaningful downstream claim via Context.isAuthoredAxisLabel (both read as not-authored, so l x / l y fall back to the focused subplot's own axis). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HFJwss8TaoKhFFQRZNbdxU
ReviewI read through the full diff (schema, commands, model, services, keybindings, tests) and traced the trickier runtime paths (state stack invariants, audio observer chain, notification/text dispatch ordering) against the actual source rather than just the diff context. Overall this is a well-scoped, carefully-reasoned change that follows the MVVC architecture closely. A few notes below; nothing blocking. Strengths
Minor suggestions (non-blocking)
Things I specifically checked and found correct (no action needed, noting for reviewers' confidence)
No security, performance, or architecture-boundary concerns — this stays within Model/Service/Command layers appropriately, doesn't touch anything user-input-sensitive, and the added audio/DOM work (three-oscillator arpeggios) is bounded and consistent with the existing menu-tone pattern. |
Consolidates the multi-panel lobby text into one layer, per PR #639 review: the terse/verbose "title-or-position" wording rules were duplicated across the Service layer (TextService.formatFigureText, the steady-state lobby description) and the Command layer (subplotCue.ts's subplotEntryMessage / subplotExitMessage, the one-shot enter/exit cues), so a future wording tweak had to touch both. - Move the two message builders into TextService as subplotEntryText() / subplotExitText(), next to formatFigureText, and extract the shared terse panel identity into a private terseSubplotLabel() helper. All lobby wording and its mode-branching now live in the Service layer. - SubplotCue no longer builds strings: announceEntry() / announceExit() call the new TextService methods and only orchestrate tone + notify. The pure subplotEntryMessage / subplotExitMessage functions are removed. - Command tests now construct a real TextService (toggled to the desired mode) instead of an isOff/isTerse stub, so they exercise the actual wording via the collaborator rather than a reimplementation. Pure refactor: no announced string, tone, ordering, or gating changed. Verified end-to-end against examples/multipanel.html across VERBOSE, TERSE, and OFF modes (strings byte-identical). type-check + lint clean; 948/948 tests pass. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HFJwss8TaoKhFFQRZNbdxU
|
🎉 This PR is included in version 3.73.0 🎉 The release is available on GitHub release Your semantic-release bot 📦🚀 |
Pull Request
Description
Turns the multi-panel / facet figure lobby (the
Scope.SUBPLOTarea you land in before entering a subplot) into a first-class, mode-aware navigation level consistent with an individual subplot.L-chord at the lobby —l x / l y / l z / l t / l s / l call work (previously onlyt/s/cplus a lone baretfor the title).axeslets a facet grid declare one shared X/Y label; announcements prefer the figure-wide value when authored, else fall back to the focused subplot. Same figure→subplot fallback for the title.dat the lobby surfaces authored figure-wide axes.t(Text),s(Sonification),r(Review) work at the lobby;b(Braille) warns that it is unavailable there.Changes Made
Keybindings (
src/service/keybinding.ts)FIGURE_LABEL_KEYMAP: addedANNOUNCE_X/Y/Z.SUBPLOT_KEYMAP: removed the directttitle binding (title isl t); addedTOGGLE_TEXT(t),TOGGLE_AUDIO(s),TOGGLE_BRAILLE(b),TOGGLE_REVIEW(r).Schema (
src/type/grammar.ts,src/type/state.ts,src/model/plot.ts,src/model/context.ts)Maidr.axes?: { x?: AxisConfig; y?: AxisConfig }.Figureparses it;FigureStatecarriesxAxis/yAxis;ContextexposesfigureXAxis/figureYAxis+isAuthoredAxisLabel, mirroring thefigureSubtitle/isAuthoredSubtitlepattern.Announcements (
src/command/describe.ts)resolveActiveTraceState()/labelSourcePrefix()/announceAxisLabel()helpers. Lobby precedence: authored figure-wide label wins (Figure X label is Year), else the focused subplot (Subplot 2, X label is …).AnnounceTitleCommandgains the same figure→subplot title fallback.Description modal (
src/service/description.ts):getFigureDescriptionpopulates the axes section from authored figure-wide labels.Lobby interactions, cues & mode-awareness (
src/command/move.ts,src/command/toggle.ts,src/command/subplotCue.ts,src/command/factory.ts,src/service/audio.ts,src/service/text.ts,src/service/review.ts,src/state/viewModel/reviewViewModel.ts,src/controller.ts)ToggleBrailleCommandwarns ("Braille is not available here…" + tone) above the trace level.MoveToTraceContextCommand) / exit (MoveToSubplotContextCommand) play a rising/falling C5-E5-G5 arpeggio (AudioService.playSubplotEnterTone/ExitTone) and announce a mode-aware message via sharedsubplotCue.tsbuilders (OFF = tone only, terse = concise, verbose = full). The braille-mode Esc exit (ExitBrailleAndSubplotCommand) now plays the same cue + announcement — previously it dropped back to the lobby silently.TextService.formatFigureTextis terse-aware (drops "Press ENTER" framing); a new figure out-of-bounds branch announces "No more subplots" (terse) / "No more subplots to display" (verbose) / silence (off).ReviewService/ReviewViewModel/ToggleReviewCommandacceptPlotState, andReviewServiceobserves the figure/subplots so the review text updates as the user navigates panels.Docs (
docs/SCHEMA.md): documents the top-level figuretitle/subtitle/caption/axesand lobby precedence.Backward compatible: absent
axes→ empty sentinel → falls back to the focused subplot, identical to prior behavior. Inside a subplot / single-panel figures the trace's own axis/title are used unchanged.Screenshots (if applicable)
Checklist
Additional Notes
announce-axis,announce-title,subplot-transitions,context-authored,service/description,service/lobbyBoundary,service/audio.subplotCue,model/figureAxes. Cover the label precedence matrix (X/Y/Z/title × figure-wide/subplot/neither × terse/verbose), the braille warning + braille-exit cue, terse/verbose/off enter/exit + boundary cues, the "no double sound on exit" invariant, and realmaidr.axes → Figure.stateparsing.examples/multipanel.htmlacross verbose / terse / off, plus review-at-lobby. Full Jest suite green (921); type-check, ESLint, and production build clean.🤖 Generated with Claude Code
https://claude.ai/code/session_01HFJwss8TaoKhFFQRZNbdxU