"""Surface plotting functions.
NB: Code in this module is a copied subset from:
https://github.com/MICA-MNI/BrainSpace/blob/master/brainspace/plotting/surface_plotting.py
Code has been modified (lines 30-31) just to accommodate extra orientations
('anterior', 'posterior').
"""
# Author: Oualid Benkarim <oualid.benkarim@mcgill.ca>
# License: BSD 3 clause
from itertools import product as iter_prod
from matplotlib.colors import LinearSegmentedColormap, ListedColormap
import matplotlib.pyplot as plt
import numpy as np
from brainspace.plotting.base import Plotter
from brainspace.plotting.colormaps import colormaps
import brainspace.plotting.defaults_plotting as dp
from brainspace.plotting.utils import (_broadcast, _expand_arg, _grep_args,
_gen_grid, _get_ranges)
orientations = {'medial': (0, -90, -90),
'lateral': (0, 90, 90),
'ventral': (0, 180, 0),
'dorsal': (0, 0, 0),
'anterior': (90, -90, -90),
'posterior': (-90, -90, -90)}
def _add_colorbar(ren, lut, location, **cb_kwds):
kwds = dp.scalarBarActor_kwds.copy()
kwds = {k.lower(): v for k, v in kwds.items()}
orientation = 'vertical'
if location in {'top', 'bottom'}:
orientation = 'horizontal'
kwds['width'], kwds['height'] = kwds['height'], kwds['width']
if lut.GetIndexedLookup():
if location == 'left':
kwds['position'] = (.32, 0.25)
elif location == 'right':
kwds['position'] = (-.32, 0.25)
elif location == 'bottom':
kwds['position'] = (0.25, 0.73)
else:
kwds['position'] = (0.25, -.43)
elif location in {'top', 'bottom'}:
kwds['position'] = kwds['position'][::-1]
text_pos = 'precedeScalarBar'
if lut.GetIndexedLookup():
if location in {'left', 'bottom'}:
text_pos = 'succeedScalarBar'
elif location in {'right', 'top'}:
text_pos = 'succeedScalarBar'
for k, v in cb_kwds.items():
if isinstance(kwds.get(k, None), dict):
kwds[k].update(v)
else:
kwds[k] = v
kwds.update({'lookuptable': lut, 'orientation': orientation,
'textPosition': text_pos})
return ren.AddScalarBarActor(**kwds)
def _add_text(ren, text, location, **lt_kwds):
orientation = 0
if location == 'left':
orientation = 90
elif location == 'right':
orientation = -90
kwds = dp.textActor_kwds.copy()
kwds = {k.lower(): v for k, v in kwds.items()}
for k, v in lt_kwds.items():
if isinstance(kwds.get(k, None), dict):
kwds[k].update(v)
else:
kwds[k] = v
kwds.update({'input': text, 'orientation': orientation})
return ren.AddTextActor(**kwds)
def _set_table(cm, lut):
cm = plt.get_cmap(cm)
nvals = lut['numberOfTableValues']
table = cm(np.linspace(0, 1, nvals)) * 255
return table.astype(np.uint8)
[docs]def build_plotter(surfs, layout, array_name=None, view=None, color_bar=None,
color_range=None, share=False, label_text=None,
cmap='viridis', nan_color=(0, 0, 0, 1), zoom=1,
background=(1, 1, 1), size=(400, 400), **kwargs):
"""Build plotter arranged according to the `layout`.
Parameters
----------
surfs : dict[str, BSPolyData]
Dictionary of surfaces.
layout : array-like, shape = (n_rows, n_cols)
Array of surface keys in `surfs`. Specifies how window is arranged.
array_name : array-like, optional
Names of point data array to plot for each layout entry.
Use a tuple with multiple array names to plot multiple arrays
(overlays) per layout entry. Default is None.
view : array-like, optional
View for each each layout entry. Possible views are {'lateral',
'medial', 'ventral', 'dorsal'}. If None, use default view.
Default is None.
color_bar : {'left', 'right', 'top', 'bottom'} or None, optional
Location where color bars are rendered. If None, color bars are not
included. Default is None.
color_range : {'sym'}, tuple or sequence.
Range for each array name. If 'sym', uses a symmetric range. Only used
if array has positive and negative values. Default is None.
share : {'row', 'col', 'both'} or bool, optional
If ``share == 'row'``, point data for surfaces in the same row share
same data range. If ``share == 'col'``, the same but for columns.
If ``share == 'both'``, all data shares same range. If True, similar
to ``share == 'both'``. Default is False.
label_text : dict[str, array-like], optional
Label text for column/row. Possible keys are {'left', 'right',
'top', 'bottom'}, which indicate the location. Default is None.
cmap : str or sequence of str, optional
Color map name (from matplotlib) for each array name.
Default is 'viridis'.
nan_color : tuple
Color for nan values. Default is (0, 0, 0, 1).
zoom : float or sequence of float, optional
Zoom applied to the surfaces in each layout entry.
background : tuple
Background color. Default is (1, 1, 1).
size : tuple, optional
Window size. Default is (400, 400).
kwargs : keyword-valued args
Additional arguments passed to the renderers, actors, mapper, color_bar
or plotter. Keywords starting with:
- 'renderer__' are passed to the renderers.
- 'actor__' are passed to the actors.
- 'mapper__' are passed to the mappers.
- 'cb__' are passed to color bar actors.
- 'text__' are passed to color text actors.
The rest of keywords are passed to the plotter.
Returns
-------
plotter : Plotter
An instance of Plotter.
See Also
--------
:func:`plot_surf`
:func:`plot_hemispheres`
Notes
-----
If sequences, shapes of `array_name`, `view` and `zoom` must be equal
or broadcastable to the shape of `layout`. Renderer keywords must also
be broadcastable to the shape of `layout`.
If sequences, shapes of `cmap` and `cbar_range` must be equal or
broadcastable to the shape of `array_name`, including the number of array
names per entry. Actor and mapper keywords must also be broadcastable to
the shape of `array_name`.
"""
# Layout
for k in np.unique(layout):
if k not in surfs and k is not None:
raise ValueError("Key '%s' is not in 'surfs'" % k)
# Share
if share is True:
share = 'b'
elif share is None or share is False:
share = None
elif share in {'row', 'r', 'col', 'c', 'both', 'b'}:
share = share[0]
else:
raise ValueError("Unknown share=%s" % share)
# Color bar
if color_bar is True:
color_bar = 'right'
elif color_bar is None or color_bar is False:
color_bar = None
elif color_bar not in {'left', 'right', 'top', 'bottom'}:
raise ValueError("Unknown color_bar=%s" % color_bar)
if share == 'c' and color_bar in {'left', 'right'}:
raise ValueError("Incompatible color_bar=%s and "
"share=%s" % (color_bar, share))
if share == 'r' and color_bar in {'top', 'bottom'}:
raise ValueError("Incompatible color_bar=%s and "
"share=%s" % (color_bar, share))
layout = np.atleast_2d(layout)
nrow, ncol = shape = layout.shape
view = _broadcast(view, 'view', shape)
zoom = _broadcast(zoom, 'zoom', shape)
array_name = _expand_arg(array_name, 'array_name', shape)
cmap = _expand_arg(cmap, 'cmap', shape, ref=array_name)
color_range = _expand_arg(color_range, 'cbar_range', shape, ref=array_name)
ren_kwds = _grep_args('renderer', kwargs, shape=shape)
actor_kwds = _grep_args('actor', kwargs, shape=shape, ref=array_name)
mapper_kwds = _grep_args('mapper', kwargs, shape=shape, ref=array_name)
cb_kwds = _grep_args('cb', kwargs)
text_kwds = _grep_args('text', kwargs)
# lut_kwds = _grep_args('lut', kwargs)
# Label text
if label_text is None:
label_text = {}
elif isinstance(label_text, (list, np.ndarray)):
label_text = {'left': label_text}
# Array ranges
specs = _get_ranges(layout, surfs, array_name, share, color_range)
# Grid
grid_row, grid_col, ridx, cidx, entries = \
_gen_grid(nrow, ncol, label_text, color_bar, share)
kwargs.update({'nrow': grid_row, 'ncol': grid_col, 'size': size})
p = Plotter(**kwargs)
for iren, jren in iter_prod(range(len(ridx)), range(len(cidx))):
i, j = ridx[iren], cidx[jren]
kwds = dp.renderer_kwds.copy()
kwds.update({'row': iren, 'col': jren, 'background': background})
# Renderers for empty entries
if isinstance(i, str) or isinstance(j, str):
if isinstance(i, str) and isinstance(j, str):
p.AddRenderer(**kwds)
continue
kwds.update({k: v[i, j] for k, v in ren_kwds.items()})
kwds['background'] = background # just in case
ren = p.AddRenderer(**kwds)
if layout[i, j] is None:
continue
s = surfs[layout[i, j]]
for ia, name in enumerate(array_name[i, j]):
if name is False or name is None:
continue
sp = specs[ia, i, j]
# Actor
actor = dp.actor_kwds.copy()
actor.update({k: v[i, j][ia] for k, v in actor_kwds.items()})
if view[i, j] is not None:
actor['orientation'] = orientations[view[i, j]]
# Mapper
mapper = dp.mapper_kwds.copy()
mapper['scalarVisibility'] = name is not True
mapper['interpolateScalarsBeforeMapping'] = not sp['disc']
mapper.update({k: v[i, j][ia] for k, v in mapper_kwds.items()})
mapper['inputDataObject'] = s
if name is not True:
mapper['arrayName'] = name
# Lut
lut = dp.lookuptable_kwds.copy()
lut['numberOfTableValues'] = sp['nval']
lut['range'] = (sp['min'], sp['max'])
cm = cmap[i, j][ia]
if cm is not None:
if (isinstance(cm, (LinearSegmentedColormap, ListedColormap)) or
cm not in colormaps):
cm = plt.get_cmap(cm)
nvals = lut['numberOfTableValues']
table = cm(np.linspace(0, 1, nvals)) * 255
table = table.astype(np.uint8)
else:
table = _set_table(cm, lut)
lut['table'] = table
if nan_color:
lut['nanColor'] = nan_color
# Do not support indexed lut for now
# if sp['disc']:
# lut['IndexedLookup'] = True
# color_idx = sp['val']
# lut['annotations'] = (color_idx, color_idx.astype(str))
# cb_kwds['labelFormat'] = '%-4.0f'
mapper['lookuptable'] = lut
ren.AddActor(**actor, mapper=mapper)
ren.ResetCamera()
ren.activeCamera.parallelProjection = True
ren.activeCamera.Zoom(zoom[i, j])
# Plot renderers for color bar, text
for e in entries:
kwds = dp.renderer_kwds.copy()
kwds.update({'row': e.row, 'col': e.col, 'background': background})
ren1 = p.AddRenderer(**kwds)
if isinstance(e.label, str):
_add_text(ren1, e.label, e.loc, **text_kwds)
else: # color bar
ren_lut = p.renderers[p.populated[e.label]][-1]
lut = ren_lut.actors.lastActor.mapper.lookupTable
_add_colorbar(ren1, lut.VTKObject, e.loc, **cb_kwds)
return p
[docs]def plot_surf(surfs, layout, array_name=None, view=None, color_bar=None,
color_range=None, share=False, label_text=None, cmap='viridis',
nan_color=(0, 0, 0, 1), zoom=1, background=(1, 1, 1),
size=(400, 400), embed_nb=False, interactive=True, scale=(1, 1),
transparent_bg=True, screenshot=False, filename=None,
return_plotter=False, **kwargs):
"""Plot surfaces arranged according to the `layout`.
Parameters
----------
surfs : dict[str, BSPolyData]
Dictionary of surfaces.
layout : array-like, shape = (n_rows, n_cols)
Array of surface keys in `surfs`. Specifies how window is arranged.
array_name : array-like, optional
Names of point data array to plot for each layout entry.
Use a tuple with multiple array names to plot multiple arrays
(overlays) per layout entry. Default is None.
view : array-like, optional
View for each each layout entry. Possible views are {'lateral',
'medial', 'ventral', 'dorsal'}. If None, use default view.
Default is None.
color_bar : {'left', 'right', 'top', 'bottom'} or None, optional
Location where color bars are rendered. If None, color bars are not
included. Default is None.
color_range : {'sym'}, tuple or sequence.
Range for each array name. If 'sym', uses a symmetric range. Only used
if array has positive and negative values. Default is None.
share : {'row', 'col', 'both'} or bool, optional
If ``share == 'row'``, point data for surfaces in the same row share
same data range. If ``share == 'col'``, the same but for columns.
If ``share == 'both'``, all data shares same range. If True, similar
to ``share == 'both'``. Default is False.
label_text : dict[str, array-like], optional
Label text for column/row. Possible keys are {'left', 'right',
'top', 'bottom'}, which indicate the location. Default is None.
cmap : str or sequence of str, optional
Color map name (from matplotlib) for each array name.
Default is 'viridis'.
nan_color : tuple
Color for nan values. Default is (0, 0, 0, 1).
zoom : float or sequence of float, optional
Zoom applied to the surfaces in each layout entry.
background : tuple
Background color. Default is (1, 1, 1).
size : tuple, optional
Window size. Default is (400, 400).
interactive : bool, optional
Whether to enable interaction. Default is True.
embed_nb : bool, optional
Whether to embed figure in notebook. Only used if running in a
notebook. Default is False.
screenshot : bool, optional
Take a screenshot instead of rendering. Default is False.
filename : str, optional
Filename to save the screenshot. Default is None.
transparent_bg : bool, optional
Whether to us a transparent background. Only used if
``screenshot==True``. Default is False.
scale : tuple, optional
Scale (magnification). Only used if ``screenshot==True``.
Default is None.
kwargs : keyword-valued args
Additional arguments passed to the renderers, actors, mapper or
plotter. Keywords starting with:
- 'renderer__' are passed to the renderers.
- 'actor__' are passed to the actors.
- 'mapper__' are passed to the mappers.
The rest of keywords are passed to the plotter.
Returns
-------
figure : Ipython Image or panel or None
Figure to plot. None if using vtk for rendering (i.e.,
``embed_nb == False``).
See Also
--------
:func:`build_plotter`
:func:`plot_hemispheres`
Notes
-----
If sequences, shapes of `array_name`, `view` and `zoom` must be equal
or broadcastable to the shape of `layout`. Renderer keywords must also
be broadcastable to the shape of `layout`.
If sequences, shapes of `cmap` and `cbar_range` must be equal or
broadcastable to the shape of `array_name`, including the number of array
names per entry. Actor and mapper keywords must also be broadcastable to
the shape of `array_name`.
"""
if screenshot and filename is None:
raise ValueError('Filename is required.')
if screenshot or embed_nb:
kwargs.update({'offscreen': True})
p = build_plotter(surfs, layout, array_name=array_name, view=view,
color_bar=color_bar, color_range=color_range,
share=share, label_text=label_text, cmap=cmap,
nan_color=nan_color, zoom=zoom, background=background,
size=size, **kwargs)
if return_plotter:
return p
if screenshot:
return p.screenshot(filename, transparent_bg=transparent_bg,
scale=scale)
return p.show(embed_nb=embed_nb, interactive=interactive, scale=scale,
transparent_bg=transparent_bg)