"""
Plot3d
======
Plotting routines based on qt.
Note
----
This module is based on the pyqtgraph package.
"""
__author__ = 'Christoph Kirst <christoph.kirst.ck@gmail.com>'
__license__ = 'GPLv3 - GNU General Public License v3 (see LICENSE)'
__copyright__ = 'Copyright © 2020 by Christoph Kirst'
__webpage__ = 'https://idisco.info'
__download__ = 'https://github.com/ClearAnatomics/ClearMap'
import itertools
from dataclasses import dataclass
from pathlib import Path
from typing import Optional, Tuple, List, Sequence, TypeAlias, Union, Any
import numpy as np
import pyqtgraph as pg
import functools as ft
from PyQt5.QtCore import QRect
from PyQt5.QtWidgets import QApplication
from ClearMap.Utils.utilities import runs_on_spyder
import ClearMap.Visualization.Qt.DataViewer as dv
import ClearMap.Visualization.Qt.utils as qtu
############################################################################################################
# Plotting
############################################################################################################
# TODO: figure / windows handler to update data in existing windows
SourceLike: TypeAlias = Union[str, Path, np.ndarray, "ClearMap.IO.source.Source"]
SourceType: TypeAlias = Union[SourceLike, Sequence[SourceLike], Sequence[Sequence[SourceLike]]]
Lut: TypeAlias = Optional[str]
# Per-panel min/max: either a single (low, high) or a list of these for overlays
MinMax: TypeAlias = Optional[Union[Tuple[float, float], List[Tuple[float, float]]]]
# Per-panel max-projection flags: either a single int or per-overlay list
MaxProj: TypeAlias = Optional[Union[int, List[int]]]
[docs]
@dataclass
class PlotPanel:
"""
One window in multi_plot.
images : Path | str | np.ndarray | Source | List[Any] | List[List[Any]] (overlays)
Typically, any that Plot3d already accepts
"""
images: SourceType
title: Optional[str] = None
lut: Lut = None
min_max: MinMax = None # potentially nested to match overlays
max_projection: MaxProj = None
[docs]
def multi_plot_from_panels(panels: Sequence[PlotPanel], *, axis: int | None = None,
scale: Tuple[float, float, float] | None = None,
invert_y: bool = True, arrange: bool = True, sync: bool = True,
screen: int | None = None, to_front: bool = True, parent=None):
"""
Thin wrapper that converts a list of Panels into a call to multi_plot().
(same Path->str normalization as Plot3d.plot()).
"""
sources: List[SourceType] = []
titles: List[Optional[str]] = []
luts: List[Lut] = []
min_maxes: List[MinMax] = []
max_projs: List[MaxProj] = []
def _normalize_paths(obj: Any):
if isinstance(obj, Path):
return str(obj)
if isinstance(obj, (list, tuple)):
return [ _normalize_paths(x) for x in obj ]
return obj
for p in panels:
sources.append(_normalize_paths(p.images))
titles.append(p.title)
luts.append(p.lut)
min_maxes.append(p.min_max)
max_projs.append(p.max_projection)
return multi_plot(sources, axis=axis, scale=scale, title=titles, invert_y=invert_y,
min_max=min_maxes, max_projection=max_projs, arrange=arrange, screen=screen,
lut=luts, parent=parent, sync=sync, to_front=to_front)
[docs]
def plot(source, axis=None, scale=None, title=None, invert_y=True, min_max=None, screen=None,
arrange=True, lut=None, max_projection=None, to_front=True, parent=None, sync=True):
"""
Plot a source as 2d slices.
Arguments
---------
source : Source, pathlib.Path, list or dict
The source to plot. If a list is given several synchronized windows are
generated. If an element in the list is a list of sources those are
overlayed in different colors in that window.
axis : int or None
The axis along which to slice the data.
scale : tuple of float
A spatial scale for each axis used for the spatial cursor position.
title : str or None
The title of the window.
invert_y : bool
If True invert the y axis (as typically done for images).
min_max : tuple or None
The minimal and maximal values for each source. If None, determine them from
the source.
screen : int or None
Specify on which screen to open the window.
Returns
-------
plot : DataViewer
A data viewer class.
"""
if not isinstance(source, (list, tuple)):
source = [source]
if isinstance(source, tuple):
source = list(source)
for i, src in enumerate(source):
if isinstance(src, Path):
source[i] = str(src)
data_viewers = multi_plot(source, axis=axis, scale=scale, title=title, invert_y=invert_y,
min_max=min_max, max_projection=max_projection, screen=screen,
arrange=arrange, lut=lut, to_front=to_front,
parent=parent, sync=sync)
if not runs_on_spyder():
inst = QApplication.instance()
# if inst is not None:
# inst.exec_()
return data_viewers
[docs]
def multi_plot(sources, axis=None, scale=None, title=None, invert_y=True, min_max=None,
max_projection=None, arrange=True, screen=None, lut='flame', screen_percent=90, parent=None, sync=True, to_front=True):
"""
Plot a source as 2d slices.
Arguments
---------
sources : list of sources
The sources to plot.If an element in the list is a list of sources
those are overlayed in different colors in that window.
axis : int or None
The axis along which to slice the data.
scale : tuple of float
A spatial scale for each axis used for the spatial cursor position.
title : str or None
The title of the window.
invert_y : bool
If True invert the y axis (as typically done for images).
min_max : tuple or None
The minimal and maximal values for each source. If None, determine them from
the source.
screen : int or None
Specify on which screen to open the window.
Returns
-------
plots : list of DataViewers
A list of viewer classes.
"""
if not isinstance(title, (tuple, list)):
title = [title] * len(sources)
if not isinstance(lut, (list, tuple)):
lut = [lut] * len(sources)
if min_max is None or np.isscalar(min_max[0]): # Because it is a list of lists
min_max = [min_max] * len(sources)
if not isinstance(max_projection, list):
max_projection = [max_projection] * len(sources)
dvs = [dv.DataViewer(source=src, axis=axis, scale=scale, title=title_, invertY=invert_y,
minMax=min_max_, max_projection=max_projection_, default_lut=lut_, parent=parent)
for src, title_, lut_, min_max_, max_projection_ in zip(sources, title, lut, min_max, max_projection)]
if arrange:
try:
geo = qtu.tiled_layout(len(dvs), percent=screen_percent, screen=screen)
for d, g in zip(dvs, geo):
# d.setFixedSize(int(0.95 * g[2]), int(0.9 * g[3]))
d.setGeometry(QRect(*g))
except: # FIXME: too broad
pass
if sync:
for d1, d2 in itertools.combinations(dvs, 2):
synchronize(d1, d2)
if to_front:
bring_to_front(dvs)
#for d in dvs:
# d.update();
return dvs
[docs]
def arrange_plots(plots, screen = None, screen_percent = 90):
try:
geo = qtu.tiled_layout(len(plots), percent=screen_percent, screen=screen)
for d, g in zip(plots, geo):
d.setGeometry(pg.QtCore.QRect(*g))
except:
pass
[docs]
def synchronize(viewer1, viewer2):
"""Synchronize scrolling between two data viewers"""
def sync_d1_d2_scroll():
"""sync dv1 -> dv2"""
viewer2.sliceLine.setValue(viewer1.sliceLine.value())
def sync_d1_d2_button(button, ax):
viewer2.axis_buttons[ax].setChecked(button.isChecked())
viewer1.sliceLine.sigPositionChanged.connect(sync_d1_d2_scroll)
for ax, button in enumerate(viewer1.axis_buttons):
button.clicked.connect(ft.partial(viewer2.setSliceAxis, ax))
button.clicked.connect(ft.partial(sync_d1_d2_button, button, ax))
def sync_d2_d1_scroll():
"""sync dv2 -> dv1"""
viewer1.sliceLine.setValue(viewer2.sliceLine.value())
def sync_d2_d1_button(button, ax):
viewer1.axis_buttons[ax].setChecked(button.isChecked())
viewer2.sliceLine.sigPositionChanged.connect(sync_d2_d1_scroll)
for ax, button in enumerate(viewer2.axis_buttons):
button.clicked.connect(ft.partial(viewer1.setSliceAxis, ax))
button.clicked.connect(ft.partial(sync_d2_d1_button, button, ax))
viewer1.view.setXLink(viewer2.view)
viewer1.view.setYLink(viewer2.view)
[docs]
def set_source(viewer, source):
"""Set the source data in a viewer.
Arguments
---------
viewer : DataViewer
The viewer to set a new source for.
source : Source
The source to use in the viewer.
Returns
-------
viewer : DataViewer
The viewer.
"""
viewer.setSource(source)
return viewer
[docs]
def bring_to_front(plots):
if not isinstance(plots, list):
plots = [plots]
for plot in plots:
plot.setWindowFlag(pg.Qt.QtCore.Qt.WindowStaysOnTopHint)
plot.raise_()
plot.activateWindow()
plot.show()
[docs]
def close(plots='all'):
if plots == 'all':
pg.Qt.App.closeAllWindows()
else:
if not isinstance(plots, list):
plots = [plots]
for plot in plots:
plot.close()
############################################################################################################
# ## Tests
############################################################################################################
def _test():
import numpy as np
import ClearMap.Visualization.Qt.Plot3d as p3d
img1 = np.random.rand(*(100, 80, 30))
img2 = np.random.rand(*(100, 80, 30)) > 0.5
p = p3d.plot([img1, img2]) # analysis:ignore