diff --git a/glue_plotly/__init__.py b/glue_plotly/__init__.py index c1316ca..3360579 100644 --- a/glue_plotly/__init__.py +++ b/glue_plotly/__init__.py @@ -90,6 +90,16 @@ def setup_jupyter(): from glue_jupyter.bqplot.image import BqplotImageView from glue_jupyter.bqplot.profile import BqplotProfileView from glue_jupyter.bqplot.scatter import BqplotScatterView + from glue_jupyter.ipyvolume import IpyvolumeScatterView, IpyvolumeVolumeView + + from glue_jupyter.ipyvolume.common.viewer import IpyvolumeBaseView + print(IpyvolumeBaseView.tools) + BqplotHistogramView.tools += ['save:bqplot_plotlyhist'] + BqplotImageView.tools += ['save:bqplot_plotlyimage2d'] + BqplotProfileView.tools += ['save:bqplot_plotlyprofile'] + BqplotScatterView.tools += ['save:bqplot_plotly2d'] + IpyvolumeScatterView.tools = [tool for tool in IpyvolumeScatterView.tools] + ['save:jupyter_plotly3dscatter'] + IpyvolumeVolumeView.tools = [tool for tool in IpyvolumeVolumeView.tools] + ['save:jupyter_plotlyvolume'] try: from glue_vispy_viewers.scatter.jupyter import JupyterVispyScatterViewer @@ -99,8 +109,3 @@ def setup_jupyter(): else: JupyterVispyScatterViewer.tools += ['save:jupyter_plotly3dscatter'] JupyterVispyVolumeViewer.tools += ['save:jupyter_plotlyvolume'] - - BqplotHistogramView.tools += ['save:bqplot_plotlyhist'] - BqplotImageView.tools += ['save:bqplot_plotlyimage2d'] - BqplotProfileView.tools += ['save:bqplot_plotlyprofile'] - BqplotScatterView.tools += ['save:bqplot_plotly2d'] diff --git a/glue_plotly/common/base_3d.py b/glue_plotly/common/base_3d.py index cff3ea6..26c15c4 100644 --- a/glue_plotly/common/base_3d.py +++ b/glue_plotly/common/base_3d.py @@ -21,7 +21,38 @@ def dimensions(viewer_state): def projection_type(viewer_state): - return "perspective" if viewer_state.perspective_view else "orthographic" + return "perspective" if getattr(viewer_state, "perspective_view", True) else "orthographic" + + +def get_resolution(viewer_state): + try: + from glue_vispy_viewers.volume.viewer_state import Vispy3DVolumeViewerState + if isinstance(viewer_state, Vispy3DVolumeViewerState): + return viewer_state.resolution + except ImportError: + pass + + try: + from glue_jupyter.common.state3d import VolumeViewerState + if isinstance(viewer_state, VolumeViewerState): + resolutions = tuple(getattr(state, 'max_resolution', None) for state in viewer_state.layers) + return max((res for res in resolutions if res is not None), default=256) + except ImportError: + pass + + return 256 + + +# TODO: Update other methods to not rely on these being reversed +def bounds(viewer_state, with_resolution=False): + bds = [(viewer_state.z_min, viewer_state.z_max), + (viewer_state.y_min, viewer_state.y_max), + (viewer_state.x_min, viewer_state.x_max)] + if with_resolution: + resolution = get_resolution(viewer_state) + return [(*b, resolution) for b in bds] + + return bds def axis(viewer_state, ax): @@ -84,6 +115,9 @@ def plotly_up_from_vispy(vispy_up): def layout_config(viewer_state): width, height, depth = dimensions(viewer_state) + x_stretch = getattr(viewer_state, "x_stretch", 1.) + y_stretch = getattr(viewer_state, "y_stretch", 1.) + z_stretch = getattr(viewer_state, "z_stretch", 1.) return dict( margin=dict(r=50, l=50, b=50, t=50), # noqa width=1200, @@ -99,9 +133,9 @@ def layout_config(viewer_state): # Currently there's no way to change this in glue up=plotly_up_from_vispy("+z") ), - aspectratio=dict(x=1 * viewer_state.x_stretch, - y=height / width * viewer_state.y_stretch, - z=depth / width * viewer_state.z_stretch), + aspectratio=dict(x=1 * x_stretch, + y=height / width * y_stretch, + z=depth / width * z_stretch), aspectmode='manual' ) ) diff --git a/glue_plotly/common/scatter3d.py b/glue_plotly/common/scatter3d.py index e0d1747..d51cc16 100644 --- a/glue_plotly/common/scatter3d.py +++ b/glue_plotly/common/scatter3d.py @@ -93,6 +93,21 @@ def error_bar_info(layer_state, mask): return errs +_IPYVOLUME_GEOMETRY_SYMBOLS = { + "sphere": "circle", + "box": "square", + "diamond": "diamond", + "circle2d": "circle", +} + + +def symbol_for_geometry(geometry: str) -> str: + symbol = _IPYVOLUME_GEOMETRY_SYMBOLS.get(geometry) + if symbol is not None: + return symbol + raise ValueError(f"Invalid geometry: {geometry}") + + def traces_for_layer(viewer_state, layer_state, hover_data=None, add_data_label=True): x, y, z, mask = clipped_data(viewer_state, layer_state) @@ -103,6 +118,10 @@ def traces_for_layer(viewer_state, layer_state, hover_data=None, add_data_label= opacity=layer_state.alpha, line=dict(width=0)) + if hasattr(layer_state, "geo"): + symbol = symbol_for_geometry(layer_state.geo) + marker["symbol"] = symbol + if hover_data is None or np.sum(hover_data) == 0: hoverinfo = 'skip' hovertext = None diff --git a/glue_plotly/common/volume.py b/glue_plotly/common/volume.py index 2e501c0..0ded721 100644 --- a/glue_plotly/common/volume.py +++ b/glue_plotly/common/volume.py @@ -1,4 +1,4 @@ -from glue_plotly.utils import rgba_components +from glue_plotly.utils import frb_for_layer, rgba_components from numpy import linspace, meshgrid, nan_to_num, nanmin from glue.core import BaseData @@ -30,23 +30,9 @@ def values(viewer_state, layer_state, bounds, precomputed=None): parent = layer_state.layer.data if subset_layer else layer_state.layer parent_label = parent.label if precomputed is not None and parent_label in precomputed: - data = precomputed[parent_label] + values = precomputed[parent_label] else: - data = parent.compute_fixed_resolution_buffer( - target_data=viewer_state.reference_data, - bounds=bounds, - target_cid=layer_state.attribute - ) - - if subset_layer: - subcube = parent.compute_fixed_resolution_buffer( - target_data=viewer_state.reference_data, - bounds=bounds, - subset_state=layer_state.layer.subset_state - ) - values = subcube * data - else: - values = data + values = frb_for_layer(viewer_state, layer_state, bounds) # This accounts for two transformations: the fact that the viewer bounds are in reverse order, # plus a need to change R -> L handedness for Plotly diff --git a/glue_plotly/html_exporters/jupyter/__init__.py b/glue_plotly/html_exporters/jupyter/__init__.py index f2336fe..e007c49 100644 --- a/glue_plotly/html_exporters/jupyter/__init__.py +++ b/glue_plotly/html_exporters/jupyter/__init__.py @@ -2,5 +2,5 @@ from . import image # noqa from . import profile # noqa from . import scatter2d # noqa -from . import vispy_scatter # noqa -from . import vispy_volume # noqa +from . import scatter3d # noqa +from . import volume # noqa diff --git a/glue_plotly/html_exporters/jupyter/vispy_scatter.py b/glue_plotly/html_exporters/jupyter/scatter3d.py similarity index 100% rename from glue_plotly/html_exporters/jupyter/vispy_scatter.py rename to glue_plotly/html_exporters/jupyter/scatter3d.py diff --git a/glue_plotly/html_exporters/jupyter/tests/test_base.py b/glue_plotly/html_exporters/jupyter/tests/test_base.py index 87400d1..bf01188 100644 --- a/glue_plotly/html_exporters/jupyter/tests/test_base.py +++ b/glue_plotly/html_exporters/jupyter/tests/test_base.py @@ -1,7 +1,7 @@ from glue_jupyter import jglue -class TestBqplotExporter: +class BaseTestJupyterExporter: viewer_type = None tool_id = None diff --git a/glue_plotly/html_exporters/jupyter/tests/test_histogram.py b/glue_plotly/html_exporters/jupyter/tests/test_histogram.py index 163c4db..4ffd0a7 100644 --- a/glue_plotly/html_exporters/jupyter/tests/test_histogram.py +++ b/glue_plotly/html_exporters/jupyter/tests/test_histogram.py @@ -6,12 +6,12 @@ importorskip('glue_jupyter') -from glue_jupyter.bqplot.histogram import BqplotHistogramView # noqa +from glue_jupyter.bqplot.histogram import BqplotHistogramView # noqa: E402 -from .test_base import TestBqplotExporter # noqa +from .test_base import BaseTestJupyterExporter # noqa: E402 -class TestHistogram(TestBqplotExporter): +class TestHistogram(BaseTestJupyterExporter): viewer_type = BqplotHistogramView tool_id = 'save:bqplot_plotlyhist' diff --git a/glue_plotly/html_exporters/jupyter/tests/test_image.py b/glue_plotly/html_exporters/jupyter/tests/test_image.py index ebc1175..77e799a 100644 --- a/glue_plotly/html_exporters/jupyter/tests/test_image.py +++ b/glue_plotly/html_exporters/jupyter/tests/test_image.py @@ -6,14 +6,14 @@ importorskip('glue_jupyter') -from glue_jupyter.bqplot.image import BqplotImageView # noqa +from glue_jupyter.bqplot.image import BqplotImageView # noqa: E402 -from numpy import arange, ones # noqa +from numpy import arange, ones # noqa: E402 -from .test_base import TestBqplotExporter # noqa +from .test_base import BaseTestJupyterExporter # noqa: E402 -class TestImage(TestBqplotExporter): +class TestImage(BaseTestJupyterExporter): viewer_type = BqplotImageView tool_id = 'save:bqplot_plotlyimage2d' diff --git a/glue_plotly/html_exporters/jupyter/tests/test_ipyvolume_scatter3d.py b/glue_plotly/html_exporters/jupyter/tests/test_ipyvolume_scatter3d.py new file mode 100644 index 0000000..b7fb2cb --- /dev/null +++ b/glue_plotly/html_exporters/jupyter/tests/test_ipyvolume_scatter3d.py @@ -0,0 +1,23 @@ +import os + +from glue.core import Data +from glue_plotly.html_exporters.jupyter.tests.test_base import BaseTestJupyterExporter + +from pytest import importorskip + +importorskip('glue_jupyter') + +from glue_jupyter.ipyvolume import IpyvolumeScatterView # noqa: E402 + + +class TestScatter3D(BaseTestJupyterExporter): + + viewer_type = IpyvolumeScatterView + tool_id = 'save:jupyter_plotly3dscatter' + + def make_data(self): + return Data(x=[1, 2, 3], y=[4, 5, 6], z=[7, 8, 9], label='d1') + + def test_default(self, tmpdir): + output_path = self.export_figure(tmpdir, 'test_default.html') + assert os.path.exists(output_path) diff --git a/glue_plotly/html_exporters/jupyter/tests/test_ipyvolume_volume.py b/glue_plotly/html_exporters/jupyter/tests/test_ipyvolume_volume.py new file mode 100644 index 0000000..6cf6665 --- /dev/null +++ b/glue_plotly/html_exporters/jupyter/tests/test_ipyvolume_volume.py @@ -0,0 +1,28 @@ +import os + +from glue.core import Data +from glue_plotly.html_exporters.jupyter.tests.test_base import BaseTestJupyterExporter + +from pytest import importorskip + +importorskip('glue_jupyter') + +from glue_jupyter.ipyvolume import IpyvolumeVolumeView # noqa: E402 + +from numpy import arange, ones # noqa: E402 + + +class TestVolume(BaseTestJupyterExporter): + + viewer_type = IpyvolumeVolumeView + tool_id = 'save:jupyter_plotlyvolume' + + def make_data(self): + return Data(label='d1', + x=arange(24).reshape((2, 3, 4)), + y=ones((2, 3, 4)), + z=arange(100, 124).reshape((2, 3, 4))) + + def test_default(self, tmpdir): + output_path = self.export_figure(tmpdir, 'test_default.html') + assert os.path.exists(output_path) diff --git a/glue_plotly/html_exporters/jupyter/tests/test_profile.py b/glue_plotly/html_exporters/jupyter/tests/test_profile.py index ac4cbfa..35309f4 100644 --- a/glue_plotly/html_exporters/jupyter/tests/test_profile.py +++ b/glue_plotly/html_exporters/jupyter/tests/test_profile.py @@ -6,12 +6,12 @@ importorskip('glue_jupyter') -from glue_jupyter.bqplot.profile import BqplotProfileView # noqa +from glue_jupyter.bqplot.profile import BqplotProfileView # noqa: E402 -from .test_base import TestBqplotExporter # noqa +from .test_base import BaseTestJupyterExporter # noqa: E402 -class TestProfile(TestBqplotExporter): +class TestProfile(BaseTestJupyterExporter): viewer_type = BqplotProfileView tool_id = 'save:bqplot_plotlyprofile' diff --git a/glue_plotly/html_exporters/jupyter/tests/test_scatter2d.py b/glue_plotly/html_exporters/jupyter/tests/test_scatter2d.py index 70c81a9..e4ed1e5 100644 --- a/glue_plotly/html_exporters/jupyter/tests/test_scatter2d.py +++ b/glue_plotly/html_exporters/jupyter/tests/test_scatter2d.py @@ -6,12 +6,12 @@ importorskip('glue_jupyter') -from glue_jupyter.bqplot.scatter import BqplotScatterView # noqa +from glue_jupyter.bqplot.scatter import BqplotScatterView # noqa: E402 -from .test_base import TestBqplotExporter # noqa +from .test_base import BaseTestJupyterExporter # noqa: E402 -class TestScatter2D(TestBqplotExporter): +class TestScatter2D(BaseTestJupyterExporter): viewer_type = BqplotScatterView tool_id = 'save:bqplot_plotly2d' diff --git a/glue_plotly/html_exporters/jupyter/tests/test_scatter3d.py b/glue_plotly/html_exporters/jupyter/tests/test_vispy_scatter3d.py similarity index 80% rename from glue_plotly/html_exporters/jupyter/tests/test_scatter3d.py rename to glue_plotly/html_exporters/jupyter/tests/test_vispy_scatter3d.py index a25e999..14db825 100644 --- a/glue_plotly/html_exporters/jupyter/tests/test_scatter3d.py +++ b/glue_plotly/html_exporters/jupyter/tests/test_vispy_scatter3d.py @@ -1,7 +1,7 @@ import os from glue.core import Data -from glue_plotly.html_exporters.jupyter.tests.test_base import TestBqplotExporter +from glue_plotly.html_exporters.jupyter.tests.test_base import BaseTestJupyterExporter from pytest import importorskip @@ -11,7 +11,7 @@ from glue_vispy_viewers.scatter.jupyter import JupyterVispyScatterViewer # noqa: E402 -class TestScatter3D(TestBqplotExporter): +class TestScatter3D(BaseTestJupyterExporter): viewer_type = JupyterVispyScatterViewer tool_id = 'save:jupyter_plotly3dscatter' diff --git a/glue_plotly/html_exporters/jupyter/tests/test_volume.py b/glue_plotly/html_exporters/jupyter/tests/test_vispy_volume.py similarity index 84% rename from glue_plotly/html_exporters/jupyter/tests/test_volume.py rename to glue_plotly/html_exporters/jupyter/tests/test_vispy_volume.py index b29cee1..a03056a 100644 --- a/glue_plotly/html_exporters/jupyter/tests/test_volume.py +++ b/glue_plotly/html_exporters/jupyter/tests/test_vispy_volume.py @@ -1,7 +1,7 @@ import os from glue.core import Data -from glue_plotly.html_exporters.jupyter.tests.test_base import TestBqplotExporter +from glue_plotly.html_exporters.jupyter.tests.test_base import BaseTestJupyterExporter from pytest import importorskip @@ -13,7 +13,7 @@ from numpy import arange, ones # noqa: E402 -class TestVolume(TestBqplotExporter): +class TestVolume(BaseTestJupyterExporter): viewer_type = JupyterVispyVolumeViewer tool_id = 'save:jupyter_plotlyvolume' diff --git a/glue_plotly/html_exporters/jupyter/vispy_volume.py b/glue_plotly/html_exporters/jupyter/volume.py similarity index 91% rename from glue_plotly/html_exporters/jupyter/vispy_volume.py rename to glue_plotly/html_exporters/jupyter/volume.py index 10e7999..4da93ec 100644 --- a/glue_plotly/html_exporters/jupyter/vispy_volume.py +++ b/glue_plotly/html_exporters/jupyter/volume.py @@ -1,7 +1,7 @@ from glue.config import viewer_tool from glue_vispy_viewers.scatter.layer_artist import ScatterLayerArtist -from glue_plotly.common.base_3d import layout_config +from glue_plotly.common.base_3d import bounds, layout_config from glue_plotly.common.common import data_count, layers_to_export from glue_plotly.common.scatter3d import traces_for_layer as scatter3d_traces_for_layer from glue_plotly.common.volume import traces_for_layer as volume_traces_for_layer @@ -26,14 +26,14 @@ def save_figure(self, filepath): layers = layers_to_export(self.viewer) add_data_label = data_count(layers) > 1 - bounds = self.viewer._vispy_widget._multivol._data_bounds + bds = bounds(self.viewer.state, with_resolution=True) count = 5 for layer in layers: if isinstance(layer, ScatterLayerArtist): traces = scatter3d_traces_for_layer(self.viewer.state, layer.state, add_data_label=add_data_label) else: - traces = volume_traces_for_layer(self.viewer.state, layer.state, bounds, + traces = volume_traces_for_layer(self.viewer.state, layer.state, bds, isosurface_count=count, add_data_label=add_data_label) diff --git a/glue_plotly/utils.py b/glue_plotly/utils.py index c98b997..22ea948 100644 --- a/glue_plotly/utils.py +++ b/glue_plotly/utils.py @@ -1,5 +1,8 @@ from re import match, sub +from glue.core import BaseData +from glue.viewers.common.state import LayerState + __all__ = [ 'cleaned_labels', 'mpl_ticks_values', @@ -93,3 +96,35 @@ def rgba_components(color): def components_to_hex(r, g, b, a=None): components = [hex_string(t) for t in (r, g, b, a) if t is not None] return f"#{''.join(components)}" + + +def data_for_layer(layer_or_state): + if isinstance(layer_or_state.layer, BaseData): + return layer_or_state.layer + else: + return layer_or_state.layer.data + + +def frb_for_layer(viewer_state, + layer_or_state, + bounds): + + data = data_for_layer(layer_or_state) + layer_state = layer_or_state if isinstance(layer_or_state, LayerState) else layer_or_state.state + is_data_layer = data is layer_or_state.layer + target_data = getattr(viewer_state, 'reference_data', data) + data_frb = data.compute_fixed_resolution_buffer( + target_data=target_data, + bounds=bounds, + target_cid=layer_state.attribute + ) + + if is_data_layer: + return data_frb + else: + subcube = data.compute_fixed_resolution_buffer( + target_data=target_data, + bounds=bounds, + subset_state=layer_state.layer.subset_state + ) + return subcube * data_frb