diff --git a/glue_vispy_viewers/common/qt/viewer_options.py b/glue_vispy_viewers/common/qt/viewer_options.py index a1d362fb..9591a41b 100644 --- a/glue_vispy_viewers/common/qt/viewer_options.py +++ b/glue_vispy_viewers/common/qt/viewer_options.py @@ -5,6 +5,9 @@ from echo.qt import autoconnect_callbacks_to_qt from glue_qt.utils import load_ui +from glue_qt.viewers.common.slice_widget import MultiSliceWidgetHelper + +from glue_vispy_viewers.volume.viewer_state import Vispy3DVolumeViewerState from glue_vispy_viewers.scatter.viewer_state import Vispy3DScatterViewerState @@ -43,6 +46,10 @@ def __init__(self, viewer_state=None, session=None, parent=None): self.ui.label_reference_data.hide() self.ui.combosel_reference_data.hide() + if isinstance(viewer_state, Vispy3DVolumeViewerState): + self.slice_helper = MultiSliceWidgetHelper(viewer_state=viewer_state, + layout=self.ui.layout_slices) + self.ui.label_line_width.hide() self.ui.value_line_width.hide() diff --git a/glue_vispy_viewers/common/qt/viewer_options.ui b/glue_vispy_viewers/common/qt/viewer_options.ui index ed225538..84aecf8f 100644 --- a/glue_vispy_viewers/common/qt/viewer_options.ui +++ b/glue_vispy_viewers/common/qt/viewer_options.ui @@ -7,7 +7,7 @@ 0 0 276 - 415 + 443 @@ -67,9 +67,6 @@ - - - @@ -82,80 +79,54 @@ - - + + - - + + - true + 14 - - z axis + + padding: 0px - - - - - min/max: + - - - - Qt::Vertical - - - - 20 - 40 - + + + + + true + - - - - - stretch: + resolution: - + stretch: - - - - QComboBox::AdjustToMinimumContentsLengthWithIcon - - - - - - - QComboBox::AdjustToMinimumContentsLengthWithIcon - - - - - + + - - + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - + @@ -177,16 +148,6 @@ - - - - Show axes - - - true - - - @@ -234,8 +195,32 @@ + + + + Show axes + + + true + + + + + + + min/max: + + + + + + + min/max: + + + @@ -243,21 +228,43 @@ - - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + QComboBox::AdjustToMinimumContentsLengthWithIcon - - - - min/max: + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + -10000 + + + 10000 + + + 0 + + + Qt::Horizontal - + @@ -265,14 +272,14 @@ - x axis + y axis - - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + stretch: @@ -291,14 +298,42 @@ + + + + stretch: + + + - - + + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + - - + + + + QComboBox::AdjustToMinimumContentsLengthWithIcon + + + + + + + + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + 14 @@ -312,11 +347,8 @@ - - - - - + + -10000 @@ -331,25 +363,31 @@ - - - - - - - -10000 - - - 10000 + + + + + true + - - 0 + + x axis - - Qt::Horizontal + + + + + + min/max: + + + + + + @@ -366,23 +404,26 @@ - - + + - 14 + true - - padding: 0px - - + z axis - - + + + + + + + + diff --git a/glue_vispy_viewers/volume/jupyter/viewer_state_widget.py b/glue_vispy_viewers/volume/jupyter/viewer_state_widget.py index 2aa3541c..7f3f091d 100644 --- a/glue_vispy_viewers/volume/jupyter/viewer_state_widget.py +++ b/glue_vispy_viewers/volume/jupyter/viewer_state_widget.py @@ -1,6 +1,8 @@ import ipyvuetify as v import traitlets +from glue.core.coordinate_helpers import world_axis +from glue.viewers.image.state import AggregateSlice from glue_jupyter.state_traitlets_helpers import GlueState from glue_jupyter.vuetify_helpers import link_glue_choices @@ -28,6 +30,8 @@ class Volume3DViewerStateWidget(v.VuetifyTemplate): resolution_items = traitlets.List().tag(sync=True) resolution_selected = traitlets.Int(allow_none=True).tag(sync=True) + sliders = traitlets.List().tag(sync=True) + def __init__(self, viewer_state): super().__init__() @@ -40,3 +44,49 @@ def __init__(self, viewer_state): link_glue_choices(self, viewer_state, "y_att") link_glue_choices(self, viewer_state, "z_att") link_glue_choices(self, viewer_state, "resolution") + + for prop in ['x_att', 'y_att', 'z_att', 'slices', 'reference_data']: + viewer_state.add_callback(prop, self._sync_sliders_from_state) + + self._sync_sliders_from_state() + + def _sync_sliders_from_state(self, *not_used): + + if self.viewer_state.reference_data is None or self.viewer_state.slices is None: + return + + if self.viewer_state.x_att is None or \ + self.viewer_state.y_att is None or \ + self.viewer_state.z_att is None: + return + + data = self.viewer_state.reference_data + + def used_on_axis(index): + return index in [self.viewer_state.x_att.axis, + self.viewer_state.y_att.axis, + self.viewer_state.z_att.axis] + + new_slices = [] + for i in range(data.ndim): + slice = self.viewer_state.slices[i] + if not used_on_axis(i) and isinstance(slice, AggregateSlice): + new_slices.append(slice.center) + else: + new_slices.append(slice) + self.viewer_state.slices = tuple(new_slices) + + self.sliders = [{ + 'index': i, + 'label': (data.world_component_ids[i].label if data.coords + else data.pixel_component_ids[i].label), + 'max': data.shape[i] - 1, + 'unit': (data.get_component(data.world_component_ids[i]).units if data.coords + else ''), + 'world_value': ("%0.4E" % world_axis(data.coords, + data, + pixel_axis=data.ndim - 1 - i, + world_axis=data.ndim - 1 - i + )[self.glue_state.slices[i]] if data.coords + else '') + } for i in range(data.ndim) if not used_on_axis(i)] diff --git a/glue_vispy_viewers/volume/jupyter/viewer_state_widget.vue b/glue_vispy_viewers/volume/jupyter/viewer_state_widget.vue index 482bf280..e0c5ecc9 100644 --- a/glue_vispy_viewers/volume/jupyter/viewer_state_widget.vue +++ b/glue_vispy_viewers/volume/jupyter/viewer_state_widget.vue @@ -31,6 +31,16 @@
+
+ {{ slider.label }}: {{ glue_state.slices[slider.index] }} {{ slider.world_value }} + +
diff --git a/glue_vispy_viewers/volume/layer_artist.py b/glue_vispy_viewers/volume/layer_artist.py index 4ffb9e21..f2872688 100644 --- a/glue_vispy_viewers/volume/layer_artist.py +++ b/glue_vispy_viewers/volume/layer_artist.py @@ -6,6 +6,7 @@ from matplotlib.colors import ColorConverter from glue.core.data import Subset, Data +from glue.core.link_manager import equivalent_pixel_cids from glue.core.exceptions import IncompatibleAttribute from glue.core.fixed_resolution_buffer import ARRAY_CACHE, PIXEL_CACHE from .colors import get_mpl_cmap, get_translucent_cmap @@ -33,9 +34,15 @@ def viewer_state(self): @property def shape(self): - x_axis = self.viewer_state.x_att.axis - y_axis = self.viewer_state.y_att.axis - z_axis = self.viewer_state.z_att.axis + order = equivalent_pixel_cids(self.viewer_state.reference_data, + self.layer_artist.layer) + + try: + x_axis = order.index(self.viewer_state.x_att.axis) + y_axis = order.index(self.viewer_state.y_att.axis) + z_axis = order.index(self.viewer_state.z_att.axis) + except (AttributeError, ValueError): + return 0, 0, 0 if isinstance(self.layer_artist.layer, Subset): full_shape = self.layer_artist.layer.data.shape @@ -51,12 +58,46 @@ def compute_fixed_resolution_buffer(self, bounds=None): if self.layer_artist is None or self.viewer_state is None: return np.broadcast_to(0, shape) - if isinstance(self.layer_artist.layer, Subset): + order = equivalent_pixel_cids(self.viewer_state.reference_data, + self.layer_artist.layer) + reference_axes = [self.viewer_state.x_att.axis, + self.viewer_state.y_att.axis, + self.viewer_state.z_att.axis] + if order is not None and not set(reference_axes) <= set(order): + self.layer_artist.disable('Layer data is not fully linked to x/y/z attributes') + return np.broadcast_to(0, shape) + + # For this method, we make use of Data.compute_fixed_resolution_buffer, + # which requires us to specify bounds in the form (min, max, nsteps). + # We also allow view to be passed here (which is a normal Numpy view) + # and, if given, translate it to bounds. If neither are specified, + # we behave as if view was [slice(None), slice(None), slice(None)]. + + def slice_to_bound(slc, size): + min, max, step = slc.indices(size) + n = (max - min - 1) // step + max = min + step * n + return (min, max, n + 1) + + full_view, permutation = self.viewer_state.numpy_slice_permutation + + full_view[reference_axes[0]] = bounds[2] + full_view[reference_axes[1]] = bounds[1] + full_view[reference_axes[2]] = bounds[0] + + layer = self.layer_artist.layer + for i in range(self.viewer_state.reference_data.ndim): + if isinstance(full_view[i], slice): + full_view[i] = slice_to_bound(full_view[i], + self.viewer_state.reference_data.shape[i]) + + if isinstance(layer, Subset): try: - subset_state = self.layer_artist.layer.subset_state - result = self.layer_artist.layer.data.compute_fixed_resolution_buffer( - target_data=self.layer_artist._viewer_state.reference_data, - bounds=bounds, subset_state=subset_state, + subset_state = layer.subset_state + result = layer.data.compute_fixed_resolution_buffer( + full_view, + target_data=self.viewer_state.reference_data, + subset_state=subset_state, cache_id=self.layer_artist.id) except IncompatibleAttribute: self.layer_artist.disable_incompatible_subset() @@ -65,9 +106,10 @@ def compute_fixed_resolution_buffer(self, bounds=None): self.layer_artist.enable() else: try: - result = self.layer_artist.layer.compute_fixed_resolution_buffer( - target_data=self.layer_artist._viewer_state.reference_data, - bounds=bounds, target_cid=self.layer_artist.state.attribute, + result = layer.compute_fixed_resolution_buffer( + full_view, + target_data=self.viewer_state.reference_data, + target_cid=self.layer_artist.state.attribute, cache_id=self.layer_artist.id) except IncompatibleAttribute: self.layer_artist.disable('Layer data is not fully linked to reference data') @@ -75,6 +117,12 @@ def compute_fixed_resolution_buffer(self, bounds=None): else: self.layer_artist.enable() + if permutation: + try: + result = result.transpose(permutation) + except ValueError: + return np.broadcast_to(0, shape) + return result @@ -128,9 +176,9 @@ def visual(self): @property def bbox(self): - return (-0.5, self.layer.shape[2] - 0.5, - -0.5, self.layer.shape[1] - 0.5, - -0.5, self.layer.shape[0] - 0.5) + return (-0.5, self.layer.shape[self._viewer_state.x_att.axis] - 0.5, + -0.5, self.layer.shape[self._viewer_state.y_att.axis] - 0.5, + -0.5, self.layer.shape[self._viewer_state.z_att.axis] - 0.5) @property def shape(self): @@ -243,7 +291,9 @@ def _update_volume(self, force=False, **kwargs): if force or 'alpha' in changed: self._update_alpha() - if force or 'layer' in changed or 'attribute' in changed: + # TODO: Feel like we shouldn't need the axis atts here + if force or any(att in changed for att in + ('layer', 'attribute', 'slices', 'x_att', 'y_att', 'z_att')): self._update_data() if force or 'subset_mode' in changed: diff --git a/glue_vispy_viewers/volume/qt/tests/test_volume_viewer.py b/glue_vispy_viewers/volume/qt/tests/test_volume_viewer.py index 18e0849f..e2a7b8c3 100644 --- a/glue_vispy_viewers/volume/qt/tests/test_volume_viewer.py +++ b/glue_vispy_viewers/volume/qt/tests/test_volume_viewer.py @@ -1,6 +1,7 @@ import sys import pytest import numpy as np +from string import ascii_lowercase from glue.config import colormaps from glue.core import DataCollection, Data @@ -8,6 +9,7 @@ from glue.core.component import Component from glue.core.link_helpers import LinkSame +from ...layer_artist import DataProxy from ..volume_viewer import VispyVolumeViewer IS_WIN = sys.platform == 'win32' @@ -19,7 +21,7 @@ def make_test_data(dimensions=(10, 10, 10)): np.random.seed(12345) - for letter in 'abc': + for letter in ascii_lowercase[:len(dimensions)]: comp = Component(np.random.random(dimensions)) data.add_component(comp, letter) @@ -255,3 +257,175 @@ def test_add_data_with_incompatible_subsets(tmpdir): volume.add_data(data1) ga.close() + + +def test_add_higher_dimensional_layers(): + + # Check that we can load layers with > 3 dimensions + + shape_4d = (10, 10, 10, 5) + data_4d = make_test_data(shape_4d) + + shape_5d = (5, 5, 4, 4, 2) + data_5d = make_test_data(shape_5d) + + dc = DataCollection([data_4d, data_5d]) + + ga = GlueApplication(dc) + ga.show() + + # First add a 4D layer + volume = ga.new_data_viewer(VispyVolumeViewer) + volume.add_data(data_4d) + + assert len(volume.layers) == 1 + + volume.state.x_att = data_4d.pixel_component_ids[0] + volume.state.y_att = data_4d.pixel_component_ids[2] + volume.state.z_att = data_4d.pixel_component_ids[3] + + # Next add a 5D layer + volume2 = ga.new_data_viewer(VispyVolumeViewer) + volume2.add_data(data_5d) + + assert len(volume2.layers) == 1 + + volume2.state.x_att = data_5d.pixel_component_ids[0] + volume2.state.y_att = data_5d.pixel_component_ids[4] + volume2.state.z_att = data_5d.pixel_component_ids[2] + + ga.close() + + +def test_3d_4d_layers(): + shape_4d = (10, 10, 10, 5) + data_4d = make_test_data(shape_4d) + + shape_3d = (15, 20, 25) + data_3d = make_test_data(shape_3d) + + dc = DataCollection([data_4d, data_3d]) + + dc.add_link(LinkSame(data_4d.pixel_component_ids[0], data_3d.pixel_component_ids[2])) + dc.add_link(LinkSame(data_4d.pixel_component_ids[1], data_3d.pixel_component_ids[0])) + dc.add_link(LinkSame(data_4d.pixel_component_ids[2], data_3d.pixel_component_ids[1])) + + ga = GlueApplication(dc) + ga.show() + + volume = ga.new_data_viewer(VispyVolumeViewer) + volume.add_data(data_4d) + volume.add_data(data_3d) + layer_3d = volume.layers[1] + + volume.state.x_att = data_4d.pixel_component_ids[0] + volume.state.y_att = data_4d.pixel_component_ids[1] + volume.state.z_att = data_4d.pixel_component_ids[2] + + assert layer_3d.enabled + + volume.state.y_att = data_4d.pixel_component_ids[3] + + assert not layer_3d.enabled + + ga.close() + + +def test_scatter_on_4d(): + shape_4d = (10, 10, 10, 5) + data_4d = make_test_data(shape_4d) + + data_scatter = Data(label="Scatter", x=[1, 2, 3], y=[2, 3, 4], z=[3, 4, 5]) + + dc = DataCollection([data_4d, data_scatter]) + + dc.add_link(LinkSame(data_4d.pixel_component_ids[1], data_scatter.id['x'])) + dc.add_link(LinkSame(data_4d.pixel_component_ids[2], data_scatter.id['y'])) + dc.add_link(LinkSame(data_4d.pixel_component_ids[3], data_scatter.id['z'])) + + ga = GlueApplication(dc) + ga.show() + + volume = ga.new_data_viewer(VispyVolumeViewer) + volume.add_data(data_4d) + + volume.state.x_att = data_4d.pixel_component_ids[1] + volume.state.y_att = data_4d.pixel_component_ids[2] + volume.state.z_att = data_4d.pixel_component_ids[3] + + volume.add_data(data_scatter) + + layer_scatter = volume.layers[-1] + + assert layer_scatter.enabled + + volume.state.x_att = data_4d.pixel_component_ids[0] + + assert not layer_scatter.enabled + + volume.state.x_att = data_4d.pixel_component_ids[3] + volume.state.z_att = data_4d.pixel_component_ids[1] + + assert layer_scatter.enabled + + ga.close() + + +def test_data_proxy_shape(): + shape_4d = (5, 4, 2, 7) + data_4d = make_test_data(shape_4d) + + shape_4d_2 = (6, 3, 7, 11) + data_4d_2 = make_test_data(shape_4d_2) + + dc = DataCollection([data_4d, data_4d_2]) + + dc.add_link(LinkSame(data_4d.pixel_component_ids[0], data_4d_2.pixel_component_ids[3])) + dc.add_link(LinkSame(data_4d.pixel_component_ids[1], data_4d_2.pixel_component_ids[0])) + dc.add_link(LinkSame(data_4d.pixel_component_ids[2], data_4d_2.pixel_component_ids[1])) + + ga = GlueApplication(dc) + ga.show() + + volume = ga.new_data_viewer(VispyVolumeViewer) + volume.add_data(data_4d) + layer = volume.layers[0] + + volume.state.x_att = data_4d.pixel_component_ids[0] + volume.state.y_att = data_4d.pixel_component_ids[1] + volume.state.z_att = data_4d.pixel_component_ids[2] + + proxy = DataProxy(volume.state, layer.state) + assert proxy.shape == (2, 4, 5) + + volume.state.x_att = data_4d.pixel_component_ids[3] + assert proxy.shape == (2, 4, 7) + + volume.state.x_att = data_4d.pixel_component_ids[2] + volume.state.y_att = data_4d.pixel_component_ids[1] + volume.state.z_att = data_4d.pixel_component_ids[3] + assert proxy.shape == (7, 4, 2) + + volume.add_data(data_4d_2) + layer2 = volume.layers[-1] + + proxy2 = DataProxy(volume.state, layer2.state) + assert proxy2.shape == (0, 0, 0) + + volume.state.x_att = data_4d.pixel_component_ids[0] + volume.state.y_att = data_4d.pixel_component_ids[1] + volume.state.z_att = data_4d.pixel_component_ids[2] + assert proxy2.shape == (0, 0, 0) + + dc.add_link(LinkSame(data_4d.pixel_component_ids[3], data_4d_2.pixel_component_ids[2])) + assert proxy2.shape == (3, 6, 11) + + volume.state.x_att = data_4d.pixel_component_ids[2] + volume.state.y_att = data_4d.pixel_component_ids[0] + volume.state.z_att = data_4d.pixel_component_ids[1] + assert proxy2.shape == (6, 11, 3) + + volume.state.y_att = data_4d.pixel_component_ids[3] + assert proxy2.shape == (6, 7, 3) + + ga.close() diff --git a/glue_vispy_viewers/volume/viewer_state.py b/glue_vispy_viewers/volume/viewer_state.py index 34365e97..d208c807 100644 --- a/glue_vispy_viewers/volume/viewer_state.py +++ b/glue_vispy_viewers/volume/viewer_state.py @@ -1,8 +1,11 @@ from glue.core.data import BaseData -from echo import CallbackProperty, SelectionCallbackProperty +from echo import CallbackProperty, SelectionCallbackProperty, delay_callback +from numpy import argsort from glue_vispy_viewers.common.viewer_state import Vispy3DViewerState from glue.core.data_combo_helper import ManualDataComboHelper +from glue.viewers.image.state import AggregateSlice + __all__ = ['Vispy3DVolumeViewerState'] @@ -10,6 +13,7 @@ class Vispy3DVolumeViewerState(Vispy3DViewerState): downsample = CallbackProperty(True) resolution = SelectionCallbackProperty(4) + slices = CallbackProperty(docstring='The current slice along all dimensions') reference_data = SelectionCallbackProperty(docstring='The dataset that is used to define the ' 'available pixel/world components, and ' 'which defines the coordinate frame in ' @@ -20,8 +24,14 @@ def __init__(self, **kwargs): super(Vispy3DVolumeViewerState, self).__init__() self.ref_data_helper = ManualDataComboHelper(self, 'reference_data') + self.slices = () + + self.add_callback('layers', self._layers_changed, echo_old=True) + self.add_callback('x_att', self._on_xatt_changed, echo_old=True) + self.add_callback('y_att', self._on_yatt_changed, echo_old=True) + self.add_callback('z_att', self._on_zatt_changed, echo_old=True) - self.add_callback('layers', self._layers_changed) + self._layers_changed(None, self.layers) Vispy3DVolumeViewerState.resolution.set_choices(self, [2**i for i in range(4, 12)]) @@ -29,10 +39,12 @@ def __init__(self, **kwargs): def _first_3d_data(self): for layer_state in self.layers: - if getattr(layer_state.layer, 'ndim', None) == 3: + if getattr(layer_state.layer, 'ndim', None) >= 3: return layer_state.layer - def _layers_changed(self, *args): + def _layers_changed(self, old_layers, new_layers): + if self.reference_data is not None and old_layers == new_layers: + return self._update_combo_ref_data() self._set_reference_data() self._update_attributes() @@ -42,10 +54,13 @@ def _update_combo_ref_data(self, *args): def _set_reference_data(self, *args): if self.reference_data is None: + self.slices = () for layer in self.layers: if isinstance(layer.layer, BaseData): self.reference_data = layer.layer return + else: + self.slices = (0,) * self.reference_data.ndim def _update_attributes(self, *args): @@ -58,12 +73,42 @@ def _update_attributes(self, *args): type(self).z_att.set_choices(self, []) else: + pixel_ids = data.pixel_component_ids + with delay_callback(self, "x_att", "y_att", "z_att"): + type(self).x_att.set_choices(self, pixel_ids) + type(self).y_att.set_choices(self, pixel_ids) + type(self).z_att.set_choices(self, pixel_ids) - z_cid, y_cid, x_cid = data.pixel_component_ids + def _set_up_attributes(self, *args): + data = self._first_3d_data() + if data is not None: + pixel_ids = data.pixel_component_ids + self.x_att = pixel_ids[2] + self.y_att = pixel_ids[1] + self.z_att = pixel_ids[0] - type(self).x_att.set_choices(self, [x_cid]) - type(self).y_att.set_choices(self, [y_cid]) - type(self).z_att.set_choices(self, [z_cid]) + @property + def numpy_slice_permutation(self): + if self.reference_data is None: + return None, None + + slices = [] + coord_att_axes = [self.x_att.axis, self.y_att.axis, self.z_att.axis] + for i in range(self.reference_data.ndim): + if i in coord_att_axes: + slices.append(slice(None)) + else: + if isinstance(self.slices[i], AggregateSlice): + slices.append(self.slices[i].slice) + else: + slices.append(self.slices[i]) + + axes_order = argsort(coord_att_axes) + perm = [0] * len(axes_order) + for i, t in enumerate(axes_order): + perm[t] = i + perm = [perm[2], perm[1], perm[0]] + return slices, perm @property def clip_limits_relative(self): @@ -73,10 +118,30 @@ def clip_limits_relative(self): if data is None: return [0., 1., 0., 1., 0., 1.] else: - nz, ny, nx = data.shape + nx = data.shape[self.x_att.axis] + ny = data.shape[self.y_att.axis] + nz = data.shape[self.z_att.axis] return (self.x_min / nx, self.x_max / nx, self.y_min / ny, self.y_max / ny, self.z_min / nz, self.z_max / nz) + + def _on_xatt_changed(self, prev_att, new_att): + if self.y_att == new_att: + self.y_att = prev_att + elif self.z_att == new_att: + self.z_att = prev_att + + def _on_yatt_changed(self, prev_att, new_att): + if self.x_att == new_att: + self.x_att = prev_att + elif self.z_att == new_att: + self.z_att = prev_att + + def _on_zatt_changed(self, prev_att, new_att): + if self.x_att == new_att: + self.x_att = prev_att + elif self.y_att == new_att: + self.y_att = prev_att diff --git a/glue_vispy_viewers/volume/volume_viewer.py b/glue_vispy_viewers/volume/volume_viewer.py index f297f414..5f32042c 100644 --- a/glue_vispy_viewers/volume/volume_viewer.py +++ b/glue_vispy_viewers/volume/volume_viewer.py @@ -42,6 +42,9 @@ def setup_widget_and_callbacks(self): self._vispy_widget.add_data_visual(multivol) self._vispy_widget._multivol = multivol + self.state.add_callback('x_att', self._update_slice_transform) + self.state.add_callback('y_att', self._update_slice_transform) + self.state.add_callback('z_att', self._update_slice_transform) self.state.add_callback('resolution', self._update_resolution) self._update_resolution() @@ -58,7 +61,7 @@ def _update_clip(self, force=False): else: self._vispy_widget._multivol.set_clip(False, [0, 0, 0, 1, 1, 1]) - def _update_slice_transform(self): + def _update_slice_transform(self, *args): self._vispy_widget._multivol._update_slice_transform(self.state.x_min, self.state.x_max, self.state.y_min, self.state.y_max, self.state.z_min, self.state.z_max) @@ -90,12 +93,12 @@ def add_data(self, data): if first_layer_artist: raise Exception("Can only add a scatter plot overlay once " "a volume is present") - elif data.ndim == 3: + elif data.ndim >= 3: if not self._has_free_volume_layers: self._warn_no_free_volume_layers() return False else: - raise Exception("Data should be 1- or 3-dimensional ({0} dimensions " + raise Exception("Data should be 1- or >3-dimensional ({0} dimensions " "found)".format(data.ndim)) added = super().add_data(data) diff --git a/setup.cfg b/setup.cfg index 611731d6..00761a92 100644 --- a/setup.cfg +++ b/setup.cfg @@ -38,11 +38,11 @@ test = mock pyqt = qtpy - glue-qt>=0.1.0 + glue-qt>=0.4.0 PyQt6 pyside = qtpy - glue-qt>=0.1.0 + glue-qt>=0.4.0 PySide6 jupyter = glue-jupyter