Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[ENH] Allow setting vmin in plot_glass_brain and plot_stat_map #3993

Merged
merged 25 commits into from
Oct 11, 2023
Merged
Show file tree
Hide file tree
Changes from 23 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
9b4ba9e
add vmin to plot_glass_brain
michellewang Sep 12, 2023
943033a
update thresholding
michellewang Sep 12, 2023
e26c79e
add test for plot_glass_brain
michellewang Sep 12, 2023
df374fb
update _get_colorbar_and_data_ranges() and tests
michellewang Sep 12, 2023
bf597fa
use half of colormap if plot_abs is True
michellewang Sep 12, 2023
6d121f6
update docstrings
michellewang Sep 18, 2023
65a518f
fix plot_abs and nan vmin/vmax
michellewang Sep 19, 2023
0f49ef7
minor refactoring
michellewang Sep 19, 2023
7da66f1
add example with vmin
michellewang Sep 19, 2023
070d724
fix flake8 and do thresholding after transform_to_2d
michellewang Sep 20, 2023
174366e
update example
michellewang Sep 20, 2023
751d054
attempt to increase test coverage
michellewang Sep 20, 2023
1c3bb48
Merge branch 'main' into 3084/more_vmin
michellewang Sep 20, 2023
35f73d4
add changelog entry
michellewang Sep 20, 2023
8dff80f
Merge branch 'main' into 3084/more_vmin
michellewang Sep 26, 2023
d420580
move default="auto" for symmetric_cbar to substitution string
michellewang Sep 26, 2023
10c1ff0
remove redundant absolute value transformation
michellewang Sep 26, 2023
fdfbb9f
Merge remote-tracking branch 'upstream/main' into 3084/more_vmin
michellewang Oct 2, 2023
a082c03
attempt to increase test coverage
michellewang Oct 2, 2023
3a9b38e
update docs for symmetric_cbar
michellewang Oct 5, 2023
a49efe8
replace error with warning for plot_abs/vmin case
michellewang Oct 5, 2023
56ab1f9
Merge remote-tracking branch 'upstream/main' into 3084/more_vmin
michellewang Oct 5, 2023
b04548e
allow arbitrary vmin even if plot_abs is True
michellewang Oct 6, 2023
c45e8cd
Apply suggestions from code review
Oct 10, 2023
58f717d
Merge branch 'main' into HEAD
Oct 10, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 2 additions & 0 deletions doc/changes/latest.rst
Original file line number Diff line number Diff line change
Expand Up @@ -19,5 +19,7 @@ Fixes
Enhancements
------------

- Allow setting ``vmin`` in :func:`~nilearn.plotting.plot_glass_brain` and :func:`~nilearn.plotting.plot_stat_map` (:gh:`3993` by `Michelle Wang`_).

Changes
-------
30 changes: 30 additions & 0 deletions examples/01_plotting/plot_demo_glass_brain_extensive.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,36 @@
stat_img, threshold=3, colorbar=True, plot_abs=True, display_mode="yx"
)

###############################################################################
michellewang marked this conversation as resolved.
Show resolved Hide resolved
# We can control the limits of the colormap and colorbar by setting ``vmin``
# and ``vmax``. Note that we use a non-diverging colormap here since the
# colorbar will not be centered around zero.

# only plot positive values
plotting.plot_glass_brain(
stat_img,
colorbar=True,
plot_abs=False,
display_mode="yz",
vmin=0,
threshold=2,
symmetric_cbar=False,
cmap="viridis",
)

###############################################################################
michellewang marked this conversation as resolved.
Show resolved Hide resolved
# Here we set ``vmin`` to the threshold to use the full color range instead of
# losing colours due to the thresholding.
plotting.plot_glass_brain(
stat_img,
colorbar=True,
plot_abs=False,
display_mode="yz",
vmin=2,
threshold=2,
symmetric_cbar=False,
cmap="viridis",
)

# %%
# Different projections for the left and right hemispheres
Expand Down
17 changes: 7 additions & 10 deletions nilearn/_utils/docs.py
Original file line number Diff line number Diff line change
Expand Up @@ -871,16 +871,13 @@ def custom_function(vertices):
docdict[
"symmetric_cbar"
] = """
symmetric_cbar : :obj:`bool`, or "auto", optional
Specifies whether the colorbar should range from `-vmax` to `vmax`
or from `vmin` to `vmax`.
Setting to `"auto"` will select the latter
if the range of the whole image is either positive or negative.

.. note::

The colormap will always range from `-vmax` to `vmax`.

symmetric_cbar : :obj:`bool`, or "auto", default="auto"
Specifies whether the colorbar and colormap should range from `-vmax` to
`vmax` (or from `vmin` to `-vmin` if `-vmin` is greater than `vmax`) or
from `vmin` to `vmax`.
Setting to `"auto"` (the default) will select the former if either
`vmin` or `vmax` is `None` and the image has both positive and negative
values.
"""

# t_r
Expand Down
36 changes: 24 additions & 12 deletions nilearn/plotting/displays/_slicers.py
Original file line number Diff line number Diff line change
Expand Up @@ -398,16 +398,6 @@
img = reorder_img(img, resample=resampling_interpolation)
threshold = float(threshold) if threshold is not None else None

if threshold is not None:
data = _safe_get_data(img, ensure_finite=True)
if threshold == 0:
data = np.ma.masked_equal(data, 0, copy=False)
else:
data = np.ma.masked_inside(
data, -threshold, threshold, copy=False
)
img = new_img_like(img, data, img.affine)

Comment on lines -401 to -410
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

affine = img.affine
data = _safe_get_data(img, ensure_finite=True)
data_bounds = get_bounds(data.shape, affine)
Expand Down Expand Up @@ -462,15 +452,37 @@
ims = []
to_iterate_over = zip(self.axes.values(), data_2d_list)
for display_ax, data_2d in to_iterate_over:
# If data_2d is completely masked, then there is nothing to
# plot. Hence, no point to do imshow().
if data_2d is not None:
# If data_2d is completely masked, then there is nothing to
# plot. Hence, no point to do imshow().
data_2d = self._threshold(

Check warning on line 458 in nilearn/plotting/displays/_slicers.py

View check run for this annotation

Codecov / codecov/patch

nilearn/plotting/displays/_slicers.py#L458

Added line #L458 was not covered by tests
data_2d,
threshold,
vmin=kwargs.get("vmin"),
vmax=kwargs.get("vmax"),
)

im = display_ax.draw_2d(
data_2d, data_bounds, bounding_box, type=type, **kwargs
)
ims.append(im)
return ims

@classmethod
def _threshold(cls, data, threshold=None, vmin=None, vmax=None):

Check warning on line 472 in nilearn/plotting/displays/_slicers.py

View check run for this annotation

Codecov / codecov/patch

nilearn/plotting/displays/_slicers.py#L471-L472

Added lines #L471 - L472 were not covered by tests
"""Threshold the data."""
if threshold is not None:
data = np.ma.masked_where(

Check warning on line 475 in nilearn/plotting/displays/_slicers.py

View check run for this annotation

Codecov / codecov/patch

nilearn/plotting/displays/_slicers.py#L475

Added line #L475 was not covered by tests
np.abs(data) <= threshold,
data,
copy=False,
)
if (vmin is not None) and (vmin >= -threshold):
data = np.ma.masked_where(data < vmin, data, copy=False)

Check warning on line 481 in nilearn/plotting/displays/_slicers.py

View check run for this annotation

Codecov / codecov/patch

nilearn/plotting/displays/_slicers.py#L481

Added line #L481 was not covered by tests
if (vmax is not None) and (vmax <= threshold):
data = np.ma.masked_where(data > vmax, data, copy=False)
return data

Check warning on line 484 in nilearn/plotting/displays/_slicers.py

View check run for this annotation

Codecov / codecov/patch

nilearn/plotting/displays/_slicers.py#L483-L484

Added lines #L483 - L484 were not covered by tests

@fill_doc
def _show_colorbar(
self, cmap, norm, cbar_vmin=None, cbar_vmax=None, threshold=None
Expand Down
91 changes: 53 additions & 38 deletions nilearn/plotting/img_plotting.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
# delayed, so that the part module can be used without them).
import numpy as np
from matplotlib import gridspec as mgs
from matplotlib.colors import LinearSegmentedColormap

Check warning on line 25 in nilearn/plotting/img_plotting.py

View check run for this annotation

Codecov / codecov/patch

nilearn/plotting/img_plotting.py#L25

Added line #L25 was not covered by tests
from nibabel.spatialimages import SpatialImage
from scipy import stats
from scipy.ndimage import binary_fill_holes
Expand Down Expand Up @@ -62,22 +63,20 @@

def _get_colorbar_and_data_ranges(
stat_map_data, vmin=None, vmax=None, symmetric_cbar=True,
force_min_stat_map_value=None, symmetric_data_range=True,
force_min_stat_map_value=None,
):
"""Set colormap and colorbar limits.

Used by plot_stat_map, plot_glass_brain and plot_img_on_surf.

If symmetric_data_range is True, the limits for the colormap will
always be set to range from -vmax to vmax. The limits for the colorbar
depend on the symmetric_cbar argument, please refer to docstring of
plot_stat_map.
The limits for the colorbar depend on the symmetric_cbar argument. Please
refer to docstring of plot_stat_map.
"""
if symmetric_data_range and (vmin is not None):
raise ValueError('this function does not accept a "vmin" '
'argument, as it uses a symmetrical range '
'defined via the vmax argument. To threshold '
'the plotted map, use the "threshold" argument')
# handle invalid vmin/vmax inputs
if (not isinstance(vmin, numbers.Number)) or (not np.isfinite(vmin)):
vmin = None

Check warning on line 77 in nilearn/plotting/img_plotting.py

View check run for this annotation

Codecov / codecov/patch

nilearn/plotting/img_plotting.py#L77

Added line #L77 was not covered by tests
if (not isinstance(vmax, numbers.Number)) or (not np.isfinite(vmax)):
vmax = None

Check warning on line 79 in nilearn/plotting/img_plotting.py

View check run for this annotation

Codecov / codecov/patch

nilearn/plotting/img_plotting.py#L79

Added line #L79 was not covered by tests

# avoid dealing with masked_array:
if hasattr(stat_map_data, '_mask'):
Expand All @@ -91,13 +90,13 @@
stat_map_max = np.nanmax(stat_map_data)

if symmetric_cbar == "auto":
if symmetric_data_range or (vmin is None) or (vmax is None):
if (vmin is None) or (vmax is None):
symmetric_cbar = stat_map_min < 0 and stat_map_max > 0
else:
symmetric_cbar = np.isclose(vmin, -vmax)

# check compatibility between vmin, vmax and symmetric_cbar
if symmetric_cbar or symmetric_data_range:
if symmetric_cbar:
if vmin is None and vmax is None:
vmax = max(-stat_map_min, stat_map_max)
vmin = -vmax
Expand All @@ -109,28 +108,35 @@
raise ValueError(
"vmin must be equal to -vmax unless symmetric_cbar is False."
)

# set vmin/vmax based on data if they are not already set
if vmin is None:
vmin = stat_map_min
if vmax is None:
vmax = stat_map_max
cbar_vmin = vmin
cbar_vmax = vmax

Check warning on line 112 in nilearn/plotting/img_plotting.py

View check run for this annotation

Codecov / codecov/patch

nilearn/plotting/img_plotting.py#L111-L112

Added lines #L111 - L112 were not covered by tests

# set colorbar limits
if not symmetric_cbar:
else:
negative_range = stat_map_max <= 0
positive_range = stat_map_min >= 0
if positive_range:
cbar_vmin = 0
cbar_vmax = None
if vmin is None:
cbar_vmin = 0

Check warning on line 120 in nilearn/plotting/img_plotting.py

View check run for this annotation

Codecov / codecov/patch

nilearn/plotting/img_plotting.py#L120

Added line #L120 was not covered by tests
else:
cbar_vmin = vmin
cbar_vmax = vmax

Check warning on line 123 in nilearn/plotting/img_plotting.py

View check run for this annotation

Codecov / codecov/patch

nilearn/plotting/img_plotting.py#L122-L123

Added lines #L122 - L123 were not covered by tests
elif negative_range:
cbar_vmax = 0
cbar_vmin = None
if vmax is None:
cbar_vmax = 0

Check warning on line 126 in nilearn/plotting/img_plotting.py

View check run for this annotation

Codecov / codecov/patch

nilearn/plotting/img_plotting.py#L126

Added line #L126 was not covered by tests
else:
cbar_vmax = vmax
cbar_vmin = vmin

Check warning on line 129 in nilearn/plotting/img_plotting.py

View check run for this annotation

Codecov / codecov/patch

nilearn/plotting/img_plotting.py#L128-L129

Added lines #L128 - L129 were not covered by tests
else:
cbar_vmin = stat_map_min
cbar_vmax = stat_map_max
else:
cbar_vmin, cbar_vmax = None, None
# limit colorbar to plotted values
cbar_vmin = vmin
cbar_vmax = vmax

Check warning on line 133 in nilearn/plotting/img_plotting.py

View check run for this annotation

Codecov / codecov/patch

nilearn/plotting/img_plotting.py#L132-L133

Added lines #L132 - L133 were not covered by tests

# set vmin/vmax based on data if they are not already set
if vmin is None:
vmin = stat_map_min

Check warning on line 137 in nilearn/plotting/img_plotting.py

View check run for this annotation

Codecov / codecov/patch

nilearn/plotting/img_plotting.py#L137

Added line #L137 was not covered by tests
if vmax is None:
vmax = stat_map_max

Check warning on line 139 in nilearn/plotting/img_plotting.py

View check run for this annotation

Codecov / codecov/patch

nilearn/plotting/img_plotting.py#L139

Added line #L139 was not covered by tests

return cbar_vmin, cbar_vmax, vmin, vmax

Expand Down Expand Up @@ -945,7 +951,6 @@

Default=`plt.cm.cold_hot`.
%(symmetric_cbar)s
Default='auto'.
%(dim)s
Default='auto'.
%(vmin)s
Expand Down Expand Up @@ -1061,7 +1066,6 @@
:ref:`sphx_glr_auto_examples_01_plotting_plot_demo_glass_brain_extensive.py` # noqa
for examples. Default=True.
%(symmetric_cbar)s
Default='auto'.
%(resampling_interpolation)s
Default='continuous'.
%(radiological)s
Expand All @@ -1073,20 +1077,31 @@
"""
if cmap is None:
cmap = cm.cold_hot if black_bg else cm.cold_white_hot
# use only positive half of colormap if plotting absolute values
if plot_abs:
cmap = LinearSegmentedColormap.from_list(

Check warning on line 1082 in nilearn/plotting/img_plotting.py

View check run for this annotation

Codecov / codecov/patch

nilearn/plotting/img_plotting.py#L1082

Added line #L1082 was not covered by tests
'cmap_pos', cmap(np.linspace(0.5, 1, 256)),
)

if stat_map_img:
stat_map_img = _utils.check_niimg_3d(stat_map_img, dtype='auto')
if plot_abs:
cbar_vmin, cbar_vmax, vmin, vmax = _get_colorbar_and_data_ranges(
_safe_get_data(stat_map_img, ensure_finite=True),
vmax=vmax,
symmetric_cbar=symmetric_cbar,
force_min_stat_map_value=0)
if vmin is not None and vmin < 0:
warnings.warn(

Check warning on line 1090 in nilearn/plotting/img_plotting.py

View check run for this annotation

Codecov / codecov/patch

nilearn/plotting/img_plotting.py#L1090

Added line #L1090 was not covered by tests
'vmin is negative but plot_abs is True',
category=UserWarning,
)
force_min_stat_map_value = 0

Check warning on line 1094 in nilearn/plotting/img_plotting.py

View check run for this annotation

Codecov / codecov/patch

nilearn/plotting/img_plotting.py#L1094

Added line #L1094 was not covered by tests
else:
cbar_vmin, cbar_vmax, vmin, vmax = _get_colorbar_and_data_ranges(
_safe_get_data(stat_map_img, ensure_finite=True),
vmax=vmax,
symmetric_cbar=symmetric_cbar)
force_min_stat_map_value = None

Check warning on line 1096 in nilearn/plotting/img_plotting.py

View check run for this annotation

Codecov / codecov/patch

nilearn/plotting/img_plotting.py#L1096

Added line #L1096 was not covered by tests

cbar_vmin, cbar_vmax, vmin, vmax = _get_colorbar_and_data_ranges(

Check warning on line 1098 in nilearn/plotting/img_plotting.py

View check run for this annotation

Codecov / codecov/patch

nilearn/plotting/img_plotting.py#L1098

Added line #L1098 was not covered by tests
_safe_get_data(stat_map_img, ensure_finite=True),
vmin=vmin,
vmax=vmax,
symmetric_cbar=symmetric_cbar,
force_min_stat_map_value=force_min_stat_map_value,
)
else:
cbar_vmin, cbar_vmax = None, None

Expand Down
10 changes: 1 addition & 9 deletions nilearn/plotting/surf_plotting.py
Original file line number Diff line number Diff line change
Expand Up @@ -1086,7 +1086,6 @@ def plot_surf_stat_map(surf_mesh, stat_map, bg_map=None,
%(vmin)s
%(vmax)s
%(symmetric_cbar)s
Default='auto'.
%(bg_on_data)s

%(darkness)s
Expand Down Expand Up @@ -1142,7 +1141,6 @@ def plot_surf_stat_map(surf_mesh, stat_map, bg_map=None,
vmin=vmin,
vmax=vmax,
symmetric_cbar=symmetric_cbar,
symmetric_data_range=False,
)

display = plot_surf(
Expand Down Expand Up @@ -1264,7 +1262,6 @@ def _colorbar_from_array(array, vmin, vmax, threshold, symmetric_cbar=True,
vmin=vmin,
vmax=vmax,
symmetric_cbar=symmetric_cbar,
symmetric_data_range=False,
)
norm = Normalize(vmin=vmin, vmax=vmax)
cmaplist = [cmap(i) for i in range(cmap.N)]
Expand Down Expand Up @@ -1350,11 +1347,7 @@ def plot_img_on_surf(stat_map, surf_mesh='fsaverage5', mask_img=None,
%(vmin)s
%(vmax)s
%(threshold)s
symmetric_cbar : :obj:`bool`, or "auto", optional
Specifies whether the colorbar should range from `-vmax` to `vmax`
(or from `vmin` to `-vmin` if `-vmin` is greater than `vmax`) or
from `vmin` to `vmax`.
Default=True.
%(symmetric_cbar)s
%(cmap)s
Default='cold_hot'.
kwargs : dict, optional
Expand Down Expand Up @@ -1412,7 +1405,6 @@ def plot_img_on_surf(stat_map, surf_mesh='fsaverage5', mask_img=None,
vmin=vmin,
vmax=vmax,
symmetric_cbar=symmetric_cbar,
symmetric_data_range=False,
)

for i, (mode, hemi) in enumerate(itertools.product(modes, hemis)):
Expand Down
19 changes: 19 additions & 0 deletions nilearn/plotting/tests/test_displays.py
Original file line number Diff line number Diff line change
Expand Up @@ -375,3 +375,22 @@ def test_add_graph_with_node_color_as_string(node_color):
node_coords = [[-53.60, -62.80, 36.64], [23.87, 0.31, 69.42]]
lzry_projector.add_graph(matrix, node_coords, node_color=node_color)
lzry_projector.close()


@pytest.mark.parametrize(
"threshold,vmin,vmax,expected_results",
[
(None, None, None, [[-2, -1, 0], [0, 1, 2]]),
(0.5, None, None, [[-2, -1, np.nan], [np.nan, 1, 2]]),
(1, 0, None, [[np.nan, np.nan, np.nan], [np.nan, np.nan, 2]]),
(1, None, 1, [[-2, np.nan, np.nan], [np.nan, np.nan, np.nan]]),
(0, 0, 0, [[np.nan, np.nan, np.nan], [np.nan, np.nan, np.nan]]),
],
)
def test_threshold(threshold, vmin, vmax, expected_results):
"""Tests for ``OrthoSlicer._threshold``."""
data = np.array([[-2, -1, 0], [0, 1, 2]], dtype=float)
assert np.ma.allequal(
OrthoSlicer._threshold(data, threshold, vmin, vmax),
np.ma.masked_invalid(expected_results),
)