Visualization overhaul: stateful fmridisplay, controller, unified colour pipeline#82
Conversation
Converts fmridisplay from a value class storing baked graphics into a
handle class whose blob layers retain their source data and render
options, so they can be re-rendered in place. Implements Phases 1-2, 5,
and 6 of VISUALIZATION_OVERHAUL_NOTES.md (the deep render-pipeline
unification and engine consolidation remain deferred, per doc section 11).
- classdef fmridisplay < handle. A full call-site audit found no
value->handle aliasing hazard; every site uses the safe
`o2 = method(o2, ...)` self-reassign idiom, so back-compat is preserved.
- addblobs: each activation_maps{} layer now retains source_region,
source_object (e.g. a statistic_image, enabling re-threshold up and
down), render_args, wh_montage/wh_surface, applied_threshold.
- New @fmridisplay methods: refresh, rethreshold, set_colormap,
set_opacity (live in-place re-render on montage views), and
controller(obj) (uifigure control panel bound to the instance).
- Fix: a bare 'colormap' flag (montage(r, o2, 'colormap')) was a
pre-existing crash in render_blobs (which requires an n x 3 matrix);
addblobs now strips it before forwarding. Un-breaks
canlab_help_5_regression_walkthrough.
- New tests: Unit_tests/image_vector/canlab_test_fmridisplay_handle.m.
Verified: full unit tier 157 pass / 0 fail / 2 skip; walkthroughs 10/10.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replaces cryptic MATLAB errors when addblobs is called incorrectly: - addblobs(o2) with no region -> instead of "Not enough input arguments" (MATLAB:minrhs), explain that a region/clusters second argument is required and show the montage(t) + addblobs(o2, region(t)) usage. - addblobs(o2, badtype) -> name the class received and how to convert, with a stable error id (fmridisplay:addblobs:badRegion). - addblobs(img) on image data (forgot to build a display) -> a new @image_vector/addblobs stub replaces "Undefined function 'addblobs' for input arguments of type 'statistic_image'" with a redirect to canlab_results_fmridisplay/montage then addblobs(o2, region(img)). (Note: surface(o2) on an fmridisplay is valid — it adds a surface view — so it is intentionally left working.) Verified: viz unit + help_examples 42/0, walkthroughs 10/10. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Surfaces are now first-class managed views (design notes 4.2 / 7.4), so blobs act across montages and surfaces together: - Factor surface drawing out of addblobs into a shared render_layer_surfaces method that derives colors from each layer's stored render_args. addblobs, surface (pull-in), and refresh all route through it. - surface(o2) registers on the SAME handle and pulls in any blob layers that already exist, so a surface added AFTER addblobs still shows the blobs. - refresh (hence rethreshold / set_colormap / set_opacity) re-renders surfaces too, not just montages. - Multi-surface keywords 'foursurfaces' / 'foursurfaces_hcp' / 'foursurfaces_freesurfer' add the four canonical views (L/R lateral + medial) as separate registered views on the same object, instead of being ignored. - Silence the per-slice "N voxels displayed, M not displayed on these slices" readout in render_blobs (commented out, with a re-enable note). - Tests: +4 surface-integration cases in canlab_test_fmridisplay_handle (13/13 pass). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Fixes two issues with surfaces as managed views: - Re-painting an already-colored surface (addblobs twice, addblobs after registering a new surface, or refresh) used to poison the saved restore data: render_on_surface stashes the *current* FaceVertexCData as the "gray" to restore, so a second paint saved the blob colors. removeblobs then could not clear the surface. render_layer_surfaces now restores the saved gray before re-painting a previously-painted surface, so removeblobs reliably returns every registered surface to anatomy. - set_opacity (and the controller opacity slider) now reaches surfaces: render_layer_surfaces applies a constant 'transvalue' as the surface patch FaceAlpha. set_colormap already propagated via render_args. Both are consequences of surfaces being unified views: add/remove/recolor/ opacity now act on montages and every registered surface together. Tests: +2 regression cases (repeat-paint clears, opacity dims surface); canlab_test_fmridisplay_handle 15/15. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…controller Addresses several interactive-use issues: - Closed figure windows no longer crash addblobs/removeblobs/refresh. New prune_dead_views drops montage/surface views whose figures were closed (with a short note); surface erase/render also filter to live handles. - rethreshold is now layer-aware and the montage path keeps the real source: image_vector.montage passes the original object via a new addblobs 'source_object' override, so a montage(t) layer retains its statistic_image and re-thresholds by p-value. rethreshold dispatches by source class (statistic_image -> p-value, fmri_data/mask -> raw value, region -> magnitude cutoff) and honors 'layers'. - Controller is now live: rebuilt in place on repeat calls, auto-refreshed by addblobs/removeblobs (update_controller via a Transient controller_handle property), each control initialized from the layer's current state, and the threshold field is type-aware (p-value vs raw). HandleVisibility='on' so `close all` closes it. Tests: +4 cases (closed-figure prune, montage source class, raw rethreshold, controller auto-update + state); canlab_test_fmridisplay_handle 19/19. checkcode clean; canlab_test_display 4/4. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ce/orthviews) Record in VISUALIZATION_OVERHAUL_NOTES.md section 11 that image_vector.surface and image_vector.orthviews still return raw handles (unlike montage), and the plan to make them return a managed fmridisplay: opt-in flag first (~24 call sites use the handle return), mirroring image_vector.montage with 'source_object' retention. Captures T. Wager's keyword-parity idea: pass the direction keyword + remaining varargin through to addbrain (as the hcp surfaces already do) to expose addbrain's full surface set with minimal new code, with a caveat that the parametric surface_cutaway types (coronal_slabs, ycut_mm) live in region.surface and would need a second step. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…utaways) Correct an earlier caveat: addbrain.m:844 already dispatches the cutaway/slab keywords (coronal_slabs_4, *_cutaway, slabs) to canlab_canonical_brain_surface_cutaways, so the planned addbrain pass-through covers cutaways in one mechanism (no separate step). Record that supporting all addbrain keywords in @fmridisplay/surface needs bare-keyword parsing + control- opt stripping + post-render view/lighting handling (moderate, not piecemeal), and that making the default surface 'left_cutaway' (in @fmridisplay/surface and image_vector.surface) is a back-compat-affecting decision for the §7.4 phase. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Legend / colorbar: - render_on_surface now parents the colorbar legend to the SURFACE's figure instead of gcf, so it no longer lands on the montage window. - New remove_legend(obj) method deletes surface colorbar legends (keeps blobs). Controller: - Command-line rethreshold / set_colormap / set_opacity now sync an open controller: controls update IN PLACE when the layer count is unchanged (rebuild only on addblobs/removeblobs), so the threshold/colormap/opacity fields reflect command-line changes without destroying controls mid-callback. - Each GUI action echoes the equivalent code line to the command window (e.g. `han = set_colormap(han, 'color', [1 0 0]);`), using the bound variable name (inputname, preserved across rebuilds). - More colormaps (split / warm / cool / winter) plus a 'solid colour…' entry that opens the built-in uisetcolor palette. - Cosmetic: light-green figure, bound-variable name in the title bar, opacity slider tick marks removed. Deferred (documented in section 11): per-layer surface visibility (Visible toggle hides montage blobs only) — needs a visible flag respected across all surface-draw paths; do it with the multi-layer compositing work. True perceptual colormaps (inferno/viridis) also deferred. Tests: +3 (legend on surface figure, remove_legend, rethreshold->controller sync); canlab_test_fmridisplay_handle 22/22; checkcode clean; display 4/4. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Declare the internal helpers activate_figures, prune_dead_views, refresh, render_layer_surfaces, and update_controller as Hidden (signature-only block in fmridisplay.m; bodies stay in their @fmridisplay/*.m files), so they no longer appear in methods(obj) / tab-completion. Hidden rather than Access=private so existing external callers keep working: activate_figures is used by help-example scripts (c_univariate_contrast_maps*.m) and refresh by the unit tests. All user-facing methods remain public. Test: +1 (public vs internal method surface, and internal methods still callable); canlab_test_fmridisplay_handle 23/23. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…wn figure Colormap fix (render_layer_surfaces): - render_on_surface does not understand maxcolor/mincolor, so warm/cool/winter (max/min colour ramps) and solid colours were silently dropped on surfaces and fell back to the default split hot/cool (blue negatives under "warm"; cool/winter not updating). Now the layer's colour spec is translated into pos_colormap/neg_colormap ramps (surface_colormaps_from_args) and the tokens render_on_surface can't read are stripped. Solid colours map the same colour on both signs (no stray default-cool negatives). Verified: warm surface map B=0.0, cool/winter produce distinct surface colormaps. (Residual: the split hot/cool map still differs slightly montage-vs-surface due to the two engines' colormap construction; full match is the deferred render-pipeline unification.) Figure fix (surface / add_multiple_surfaces): - surface(o2, 'foursurfaces*') opened the 2x2 layout in the current figure, so without a preceding `figure;` it drew over the montage and showed nothing. It now reuses an empty current figure or opens a new one. Tests: +2 (surface colormap follows max/min colour; foursurfaces uses its own figure); canlab_test_fmridisplay_handle 25/25; checkcode clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
In add_multiple_surfaces, set the layout figure Color to white and turn the four surface axes Visible/Color off, so no axes box or grey background shows behind the surfaces. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The 2x2 layout figure also contained a stray default-position axes (created during rendering) that showed a visible frame behind the surfaces. Turn off Visible on every axes in the layout figure (surfaces, colorbar axes, and the stray one); surface patches and colorbars are unaffected. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…n update
- legend() no longer errors after set_colormap switches a layer to a single
colormap. Root cause: refresh re-rendered the montage but did not update the
layer's legend-defining .mincolor/.maxcolor, so a stale 2-row split mincolor
made legend take the split path and index past the 2-element cmaprange.
refresh now keeps .mincolor/.maxcolor in sync (as addblobs does), and legend
has a defensive guard: a split-shaped mincolor without 4 cmaprange anchors
falls back to a single map instead of erroring.
- Controller: new 'mango' colormap option ->
set_colormap(obj, 'splitcolor', {[.5 0 1] [0 .8 .3] [1 .2 1] [1 1 .3]}); the
dropdown reflects it and the action echoes the code line.
- Plan: record the agreed long-term colour architecture (one central
value->colour mapping module feeding slice and surface renderers; surfaces in
true-colour RGB; subsumes the unify-pipeline / compositing / RGB-surface items).
Tests: +1 (legend after set_colormap); canlab_test_fmridisplay_handle 26/26.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… to plan
Controller:
- ~2x wider, two compact rows per layer: Opacity slider + Visible on row 1;
a type-aware threshold slider + Colors dropdown on row 2. Bigger text,
shorter re-render button, fixed-width opacity/colors controls.
- Threshold is now a slider: p-value slider with ticks at .001/.005/.01/.05/.1
for statistic_image layers; raw |x| slider anchored at 0 and the 99.9th
percentile of |data| for fmri_data/region layers.
- Colormap menu: rename 'mango' -> 'split (mango)' and add 'seafire'
(set_colormap splitcolor {[0 0 1] [.2 .8 .6] [1 .4 .1] [1 1 0]}); dropdown
reflects both and echoes the code line.
Plan: recorded that the single-range colorbar legend (warm/cool should be ONE
colorbar spanning zero, not split) is part of the central value->colour mapping
work — render_blobs' positive-percentile cmaprange and render_on_surface's
pos+neg colorbars both stem from the duplicated range/colorbar logic.
Tests: +1 (threshold slider + mango/seafire); canlab_test_fmridisplay_handle 27/27.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…, swatches
- Revert to a narrower (460px) single-column layout (the wide 2-col version cut
off the colormap dropdown). Each layer panel: colormap title stripe, opacity
slider, threshold slider, Colors dropdown + live preview swatch, Visible.
- Threshold p-value slider is now LOG scale (value = log10 p), ticks at
.001/.005/.01/.05/.1 so .05-.1 sit close together and .001-.005 spread out.
Raw |x| slider unchanged (linear, 0..99.9th pct).
- Colormap preview swatch next to the dropdown and a full-width title stripe,
both updating live with the layer's colormap.
- Footer groups Re-render / Remove legend / Close buttons.
- Fonts +4pt. Window colour set by FIG_COLOR at the top of controller.m
(now [1 .5 0]).
- seafire colours changed to {[.4 0 1] [.2 .9 .5] [1 .3 .2] [1 .8 .4]}.
Tests updated for the log slider; canlab_test_fmridisplay_handle 27/27.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A montage figure is typically much taller than the slices it contains (they occupy a ~40% horizontal band, leaving big top/bottom white margins). squeeze_figure(obj) shrinks each montage figure's height and remaps the slice axes to fill it, scaled so each slice keeps its pixel size, aspect ratio, and horizontal placement — only the empty vertical margins are removed. Opt-in (doesn't change montage defaults); skips figures that also contain surfaces. Verified: 670 -> 258 px, slice pixel size preserved to 99.9%. Test: +1 (squeeze_figure preserves slice size); canlab_test_fmridisplay_handle 28/28. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…r, p->0 - mango is now the DEFAULT blob colormap (addblobs default split colours). - Controller footer: 'Remove legend' -> 'Toggle legend' (on/off); dropped the Close button. - Per-layer 'Remove layer' button + new remove_layer(obj, k) method (removes a single layer and re-renders remaining layers on surfaces; removeblobs still removes all). - Clicking the colormap swatch opens the colour picker (set/re-pick a solid colour; works around uidropdown not re-firing on the already-selected item). - p-value threshold slider extends below .001 down to ~0 (1e-6 floor, '~0' tick). - Confirmed rethreshold already supports cluster-extent 'k' (e.g. 'k', 50), passed through to threshold(); verified it shrinks blobs as k grows. Tests: +2 (default mango, remove_layer); slider-range test updated; canlab_test_fmridisplay_handle 30/30. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ur pipeline) First step of the central value->colour mapping (VISUALIZATION_OVERHAUL_NOTES.md): a single source of truth that turns data values into RGB for one colormap type, independent of the renderer, so montage/surface/legend can agree exactly. canlab_colormap (fmridisplay_helper_functions/canlab_colormap.m): - static factories single / split / solid / indexed, plus from_render_args(args, cmaprange) bridging the existing layer render_args. - map(values) -> Nx3 RGB, matching render_blobs (split: minpos->maxpos for positives, maxneg->minneg for negatives; single: mincolor->maxcolor; solid: constant; indexed: round+index; uncoloured -> NaN). - legend_samples(n) -> one colorbar's samples (split spans zero in a single bar, the basis for fixing the single-range legend); lut(n) -> nx3 table. Tests: Unit_tests/visualization/canlab_test_canlab_colormap.m (14 cases) all pass; no regression in the fmridisplay handle suite (44/44 total). Not yet wired into the renderers — surfaces -> true-colour RGB is the next step. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…step 2) render_on_surface gains an opt-in 'truecolor', <canlab_colormap> option: after it samples per-vertex values, it colours each vertex via map() as N x 3 RGB, composites over the anatomy gray (anatomy_to_rgb preserves curvature shading) for uncoloured vertices, and sets FaceVertexCData=RGB with CDataMapping direct. Default (option absent) is unchanged, so the @image_vector/surface bypass path still uses indexed colour. render_layer_surfaces builds canlab_colormap.from_render_args(args, cmaprange) and passes it, so the managed display's surfaces now match the montage colours exactly (warm = red->yellow, no blue negatives; verified) and removeblobs/erase still restore gray. Surfaces are now N x 3 true colour, which is the basis for multi-layer compositing and per-layer surface visibility. Tests: +1 (surface true-colour via central map); canlab_test_fmridisplay_handle 31/31; canlab_colormap 14/14; display 4/4 — no regression. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A fresh surface is displayed via a solid FaceColor ([.5 .5 .5]) but its FaceVertexCData can hold a stale dark value (~0.13). The true-colour path used that stale CData as the anatomy gray, so uncoloured areas looked near-black on the first render (re-render worked because eraseblobs had rewritten the CData to a proper gray). Fix: anatomy_to_rgb now prefers the surface's solid FaceColor (the gray actually on screen) when FaceColor is a 1x3 RGB triplet, and render_on_surface saves that proper gray to UserData so eraseblobs restores it too. Test: +1 (first-render uncoloured areas are gray, not black); canlab_test_fmridisplay_handle 32/32; display 4/4. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Now that surfaces are true-colour RGB, layers composite instead of overwriting. - render_layer_surfaces paints a layer onto the CURRENT surface colours (the erase-before-repaint is gone), so a new layer's coloured vertices win and its uncoloured vertices keep whatever is underneath (lower layers / gray). The anatomy gray is saved once (save-once UserData) so erase/removeblobs still restore gray, not a composite. - New composite_surfaces(obj[, wh_surface]) does a full recompute: reset each surface to gray, then repaint every layer bottom->top. refresh (hence rethreshold/set_colormap/set_opacity) and remove_layer use it; addblobs composites a single new layer on top incrementally. Verified: a broad green mask under narrower split stats shows green where the stats don't cover and split on top; remove_layer recomposites the remainder; removeblobs restores gray. Tests: +1 (multi-layer compositing + remove_layer recomposite); canlab_test_fmridisplay_handle 33/33; display 4/4 — no regression. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ault colormap - Surfaces now BLEND layers by opacity (render_on_surface 'truecolor_alpha'): a layer's coloured vertices = a*colour + (1-a)*below, so a semi-transparent layer lets the one below show through, matching montages. render_layer_surfaces passes the layer's transvalue as the alpha (no more whole-patch FaceAlpha). - Per-layer surface visibility: a layer with visible==false is skipped in render_layer_surfaces/composite_surfaces; the controller's Visible toggle sets the flag, hides the montage blobs, and recomposites the surfaces. - Sign-aware default colormap (addblobs, only when no explicit colour given): binary mask -> solid; positive-only -> warm; negative-only -> cool; mixed +/- -> mango split. Propagates to montage and surface via the central map. Tests: +2 (surface visibility, sign-aware default), opacity test updated for blending; canlab_test_fmridisplay_handle 35/35; display 4/4; colormap 14/14. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…osites
@fmridisplay/surface no longer hard-codes a per-direction if/elseif switch.
It parses the reserved pairs (direction/orientation/axes), accepts a bare
direction token (surface(o2,'thalamus')), and passes remaining args through to
addbrain(dir, extra{:}) — so any eligible addbrain surface/region keyword now
works in the managed display automatically.
addbrain owns the default (lateral) view + lighting per keyword; surface only
special-cases bigbrain left/right (different addbrain surface) and mirrors the
azimuth 180 deg for a medial view. Managed multi-surface layouts keep identical
view angles; a couple of standalone lateral azimuths move to addbrain's
surface-specific convention.
Per T. Wager's suggestion, the brainstem left/right and caudate left/right
composite handle-sets move from surface.m into addbrain (alongside limbic/bg/
*_group), with their oblique 3D views and help/region entries.
help fmridisplay/surface now lists the common directions, the bare-token form,
and points to help addbrain for the full keyword list.
Tests: +test_surface_addbrain_passthrough, +test_surface_medial_flips_azimuth
(handle suite 37/37; display 4/4 unchanged).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
When a bare direction token is not a recognized addbrain keyword, addbrain errors with a terse 'Unknown method.'. surface() now catches that and raises fmridisplay:surface:unknownDirection naming the offending token and pointing to `help addbrain` (e.g. clarifies cutaways are 'left_cutaway'/'right_cutaway', not 'cutaway_left'). All other addbrain errors pass through unchanged. Test: +test_surface_unknown_keyword_informative_error. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… default The colour stripe in each controller layer panel is now the legend: taller, with numeric end labels underneath (from the layer's cmaprange, 1 sig fig). Split (+/-) maps label both extremes with 0 in the centre; single ramps label the two ends. Labels refresh on rethreshold/recolor. Opacity/threshold/colors/ visible rows shifted down to make room. Figure colorbars are now OFF by default: @image_vector/montage no longer draws the montage-figure legend unless 'legend' is passed; render_layer_surfaces / composite_surfaces take a show_legend flag (default false -> 'nolegend' to render_on_surface) so surfaces have no colorbar by default. The fmridisplay constructor and montage now accept 'legend'/'nolegend' without warning. Toggle legend now toggles colorbars on the montage/surface FIGURES (for export) and works both ways: ON targets the montage figure explicitly (fixing the bug where legend() drew into the controller uifigure via gcf and never reappeared) and re-renders surfaces with colorbars; OFF removes them. State in controller appdata; montage legend axes tagged for clean removal. render_layer_surfaces gains a 4th arg (show_legend); its methods(Hidden) declaration in fmridisplay.m is updated to match (requires a fresh class load). Tests: +test_controller_shows_legend_labels, +test_montage_figure_legend_off_by_default; surface-legend tests force legends on; pull-in test checks vertex colouring. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…rror
surface() did `if strcmp(h(1).FaceColor,'interp')`, which errors ("Dot indexing
is not supported for variables of this type") when addbrain returns OLD-STYLE
numeric handles (doubles) instead of graphics objects -- e.g. coronal_slabs_4
returns 8 patch handles. It would also have blanket-grayed the whole set,
destroying the isocaps that use 'interp' to show the anatomy cross-section.
Replace with a per-object loop using get/set (works on numeric handles too):
keep any 'interp' colouring, set the rest to flat gray, make all opaque.
Test: +test_surface_mixed_patch_handles. QA: handle 41/41, display 4/4,
colormap 14/14 (interactive).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
render_blobs no longer re-derives split/single colours inline. It builds one canlab_colormap (split or single) from the parsed colours + cmaprange before the slice loop and colours each slice via a shared central_map_slice helper -- the same mapping render_on_surface already uses -- so montage and surface colours come from a single implementation. Verified pixel-identical to the old renderer on split/single/solid sample montages (displayed-colour maxdiff: single 0, split 4e-12 float-eps, solid 0). The only raw-CData differences were out-of-range values the old code stored at invisible alpha=0 voxels, which the central map correctly clamps. Retired the now-dead duplicated colour logic: splitcolor_Z_to_slicecdat and the cdat2/cdatminneg/cdatmaxneg colour matrices. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…rough it Adds canlab_colormap.colorbar_ramp(n) -- the continuous legend-bar ramp with no threshold gap (unlike map/legend_samples): for split it is the negative ramp (minneg->maxneg) then the positive ramp (minpos->maxpos), reading extreme-neg ... 0 ... extreme-pos. The controller's per-layer colour stripe + preview swatch (the in-app legend) now come from from_render_args(args).colorbar_ramp(64) instead of bespoke colormap_tor calls, so the stripe, the rendered blobs, and surfaces share one colour source. +2 colorbar_ramp tests (colormap suite 14->16). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Single-range colorbar legend: - get_default_cmaprange: a single-ramp (warm/cool) map on SIGNED data now spans the full robust range THROUGH ZERO ([prctile2 prctile98]) instead of a positive-only [p10 p90] that hid negatives. Positive/negative-only data and split maps are unchanged. - render_on_surface gains a 'single_colorbar' flag (set by render_layer_surfaces from the central-map type) so single-ramp/solid/continuous maps draw ONE colorbar over the full range, not a misleading pos+neg pair. Split maps still get two. Verified: warm range [2.82 4.86] -> [-2.98 6.07]; surface colorbars 2 -> 1; split/solid montage colours unchanged. Perceptual colormaps: - canlab_colormap gains a 'continuous' type (value range mapped continuously through an n x 3 LUT, distinct from atlas-style 'indexed'). - new canlab_perceptual_colormap(name, n): viridis/inferno/magma/plasma (embedded matplotlib anchors, interpolated) + turbo/parula (built-ins). - controller dropdown offers all six; selecting one routes through set_colormap -> render_args 'colormap' -> central continuous map, so montage, surface, and the controller stripe all render it (one colorbar). Tests: +single-range legend (2), +continuous/perceptual (3, colormap suite ->19). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Deferred work / future developmentEverything below is optional follow-up — the overhaul is complete and self-consistent without it. Captured here so it can be picked up later. Pointers reference 1. Bypass methods → managed object (§7.4) — biggest remaining item
2. Figure
|
Overview
Reworks the CANlab display layer (
@fmridisplay+ the slice/surface renderers) into a stateful, source-retaining system with a live controller and one central colour pipeline. Montages, surfaces, and the legend now derive colours from a single source, layers can be re-thresholded/recoloured/toggled in place, and anyaddbrainsurface works in the managed display. Design + per-step log:VISUALIZATION_OVERHAUL_NOTES.md(§11).29 commits;
master's recent niivue/PR-81 work is preserved (merge is conflict-free).Highlights
Stateful handle class + controller
fmridisplayis now ahandleclass; the value-style call contract (o2 = addblobs(o2, …)) is preserved.0centre for split), opacity, type-aware threshold (p-values on a log slider), colormap dropdown + preview swatch, and a per-layer visibility/remove. Command-linerethreshold/set_colormap/set_opacityupdate it live; it echoes the equivalent code.Surfaces unified as managed views
@fmridisplay/surfaceis a thin pass-through toaddbrain: a bare token works (surface(o2,'thalamus')) and any eligible addbrain surface/region keyword is usable; unknown keywords get an informative error.addbrainowns the camera view;brainstem/caudatecomposites were centralized there.One central colour pipeline (
canlab_colormap)render_blobsrouted through it — verified pixel-identical, retired the duplicated split/single math), and the controller legend stripe.canlab_perceptual_colormap(viridis/inferno/magma/plasma + built-in turbo/parula), selectable in the controller, flowing through the whole pipeline.Default legends off on new montages/surfaces (the controller carries the legend);
'legend'opts the figure colorbar back in for export; Toggle legend fixed.Bug fixes
coronal_slabs_4) no longer error (numeric-handle-safe, per-object face colouring).addblobs/surfacemisuse; robust to closed figure windows.Tests
Unit_tests/image_vector/canlab_test_fmridisplay_handle.m(43),Unit_tests/visualization/canlab_test_canlab_colormap.m(19),canlab_test_display.m.-batch— a closed-figureishandletiming issue and offscreen-GL contention — both pass interactively / when run alone.)Notes for review
fmridisplayis now ahandleclass — the main behavioural change to be aware of. The value-style return contract is kept, and a back-compat audit found no aliasing call sites.legend.mcolorbar keeps its own split-positioning helper (its colours already match the central map) — noted in §11.🤖 Generated with Claude Code