Commit 4e2314c
Visualization overhaul: stateful fmridisplay, controller, unified colour pipeline (#82)
* fmridisplay: stateful handle class with live layers + controller widget
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>
* fmridisplay: graceful, informative errors for addblobs misuse
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>
* fmridisplay: unify surfaces as managed views; silence per-slice readout
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>
* fmridisplay: fix surface re-paint/erase and route opacity to surfaces
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>
* fmridisplay: robust to closed windows, layer-aware rethreshold, live 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>
* docs: plan deferred bypass-method -> managed object conversion (surface/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>
* docs: refine deferred surface-keyword plan (addbrain already covers cutaways)
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>
* fmridisplay: surface legend fix + remove_legend + controller upgrades
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>
* fmridisplay: hide internal helper methods from the public surface
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>
* fmridisplay: surface colormaps match montage; foursurfaces gets its own 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>
* fmridisplay: foursurfaces layout — white figure, axes off
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>
* fmridisplay: foursurfaces — turn off all axes in the layout figure
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>
* fmridisplay: fix legend after set_colormap; add 'mango' colormap; plan 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>
* fmridisplay controller: redesign + mango/seafire; legend single-range 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>
* fmridisplay controller: narrower single-column redesign, log p-slider, 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>
* fmridisplay: add squeeze_figure to remove montage figure white space
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>
* fmridisplay: mango default, toggle-legend, remove-layer, swatch picker, 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>
* viz: add central canlab_colormap value->RGB component (step 1 of colour 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>
* viz: render surfaces in true-colour RGB via central canlab_colormap (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>
* viz: fix black uncoloured surface areas on first true-colour render
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>
* viz: multi-layer surface compositing in true-colour RGB (step 3)
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>
* viz: per-layer surface visibility + opacity blending + sign-aware default 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>
* fmridisplay/surface: addbrain keyword pass-through + centralized composites
@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>
* fmridisplay/surface: informative error for unknown addbrain keyword
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>
* fmridisplay: host the legend in the controller; figure legends off by 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>
* fmridisplay/surface: fix mixed isosurface+isocaps (coronal_slabs_4) error
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: route montage colours through central canlab_colormap
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>
* canlab_colormap: add colorbar_ramp; route controller legend stripe through 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>
* fmridisplay colour: single-range legend fix + perceptual colormaps
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>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>1 parent 34c8415 commit 4e2314c
28 files changed
Lines changed: 4008 additions & 568 deletions
File tree
- CanlabCore
- @fmridisplay
- @image_vector
- Unit_tests
- image_vector
- visualization
- Visualization_functions
- fmridisplay_helper_functions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
2 | 2 | | |
3 | 3 | | |
4 | 4 | | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
5 | 24 | | |
6 | 25 | | |
7 | 26 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
157 | 157 | | |
158 | 158 | | |
159 | 159 | | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
| 164 | + | |
| 165 | + | |
| 166 | + | |
| 167 | + | |
| 168 | + | |
| 169 | + | |
| 170 | + | |
| 171 | + | |
| 172 | + | |
| 173 | + | |
| 174 | + | |
| 175 | + | |
| 176 | + | |
| 177 | + | |
| 178 | + | |
| 179 | + | |
| 180 | + | |
| 181 | + | |
| 182 | + | |
| 183 | + | |
| 184 | + | |
| 185 | + | |
| 186 | + | |
| 187 | + | |
| 188 | + | |
| 189 | + | |
| 190 | + | |
| 191 | + | |
| 192 | + | |
| 193 | + | |
| 194 | + | |
| 195 | + | |
160 | 196 | | |
161 | 197 | | |
162 | 198 | | |
163 | 199 | | |
164 | 200 | | |
165 | 201 | | |
166 | 202 | | |
167 | | - | |
| 203 | + | |
| 204 | + | |
| 205 | + | |
| 206 | + | |
| 207 | + | |
| 208 | + | |
168 | 209 | | |
169 | 210 | | |
170 | 211 | | |
| |||
185 | 226 | | |
186 | 227 | | |
187 | 228 | | |
188 | | - | |
| 229 | + | |
| 230 | + | |
| 231 | + | |
| 232 | + | |
| 233 | + | |
| 234 | + | |
| 235 | + | |
| 236 | + | |
| 237 | + | |
| 238 | + | |
189 | 239 | | |
190 | 240 | | |
191 | 241 | | |
| |||
218 | 268 | | |
219 | 269 | | |
220 | 270 | | |
221 | | - | |
222 | | - | |
223 | | - | |
224 | | - | |
| 271 | + | |
| 272 | + | |
| 273 | + | |
| 274 | + | |
| 275 | + | |
| 276 | + | |
225 | 277 | | |
226 | 278 | | |
227 | 279 | | |
| |||
300 | 352 | | |
301 | 353 | | |
302 | 354 | | |
| 355 | + | |
| 356 | + | |
| 357 | + | |
| 358 | + | |
| 359 | + | |
| 360 | + | |
| 361 | + | |
| 362 | + | |
| 363 | + | |
| 364 | + | |
| 365 | + | |
| 366 | + | |
| 367 | + | |
| 368 | + | |
| 369 | + | |
| 370 | + | |
| 371 | + | |
| 372 | + | |
| 373 | + | |
| 374 | + | |
| 375 | + | |
| 376 | + | |
| 377 | + | |
| 378 | + | |
| 379 | + | |
| 380 | + | |
| 381 | + | |
| 382 | + | |
| 383 | + | |
| 384 | + | |
| 385 | + | |
| 386 | + | |
| 387 | + | |
| 388 | + | |
| 389 | + | |
| 390 | + | |
| 391 | + | |
| 392 | + | |
| 393 | + | |
| 394 | + | |
| 395 | + | |
303 | 396 | | |
304 | 397 | | |
305 | 398 | | |
306 | 399 | | |
307 | 400 | | |
308 | | - | |
| 401 | + | |
309 | 402 | | |
310 | 403 | | |
311 | 404 | | |
| |||
351 | 444 | | |
352 | 445 | | |
353 | 446 | | |
| 447 | + | |
| 448 | + | |
| 449 | + | |
| 450 | + | |
| 451 | + | |
| 452 | + | |
| 453 | + | |
| 454 | + | |
354 | 455 | | |
355 | 456 | | |
356 | 457 | | |
| |||
407 | 508 | | |
408 | 509 | | |
409 | 510 | | |
| 511 | + | |
| 512 | + | |
| 513 | + | |
| 514 | + | |
410 | 515 | | |
411 | | - | |
412 | | - | |
413 | | - | |
414 | | - | |
415 | | - | |
416 | | - | |
417 | | - | |
418 | | - | |
419 | | - | |
420 | | - | |
421 | | - | |
422 | | - | |
423 | | - | |
424 | | - | |
425 | | - | |
426 | | - | |
427 | | - | |
428 | | - | |
429 | | - | |
430 | | - | |
431 | | - | |
432 | | - | |
433 | | - | |
434 | | - | |
435 | | - | |
436 | | - | |
437 | | - | |
438 | | - | |
439 | | - | |
440 | | - | |
441 | | - | |
442 | | - | |
443 | | - | |
444 | | - | |
445 | | - | |
446 | | - | |
447 | | - | |
448 | | - | |
449 | | - | |
450 | | - | |
451 | | - | |
452 | | - | |
453 | | - | |
454 | | - | |
455 | | - | |
456 | | - | |
457 | | - | |
458 | | - | |
459 | | - | |
460 | | - | |
461 | | - | |
462 | | - | |
| 516 | + | |
| 517 | + | |
463 | 518 | | |
464 | 519 | | |
| 520 | + | |
| 521 | + | |
| 522 | + | |
465 | 523 | | |
466 | 524 | | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
0 commit comments