├── glue_jupyter ├── bqplot │ ├── __init__.py │ ├── tests │ │ ├── __init__.py │ │ └── data │ │ │ └── bqplot.ipynb │ ├── image │ │ ├── tests │ │ │ ├── __init__.py │ │ │ ├── test_visual.py │ │ │ └── test_viewer.py │ │ ├── __init__.py │ │ └── frb_mark.py │ ├── histogram │ │ ├── tests │ │ │ ├── __init__.py │ │ │ └── test_viewer.py │ │ ├── __init__.py │ │ └── viewer.py │ ├── profile │ │ ├── tests │ │ │ └── __init__.py │ │ ├── __init__.py │ │ └── viewer.py │ ├── scatter │ │ ├── tests │ │ │ ├── __init__.py │ │ │ └── test_viewer.py │ │ ├── __init__.py │ │ └── viewer.py │ ├── common │ │ └── __init__.py │ └── compatibility.py ├── common │ ├── __init__.py │ ├── tests │ │ ├── __init__.py │ │ └── test_state3d.py │ ├── basic_jupyter_toolbar.vue │ ├── state_widgets │ │ ├── __init__.py │ │ ├── layer_profile.vue │ │ ├── viewer_scatter.vue │ │ ├── layer_histogram.py │ │ ├── viewer_histogram.py │ │ ├── layer_profile.py │ │ ├── viewer_scatter.py │ │ ├── tests │ │ │ └── test_viewer_image.py │ │ ├── viewer_profile.py │ │ ├── viewer_profile.vue │ │ ├── viewer_histogram.vue │ │ ├── viewer_image.vue │ │ ├── layer_scatter.py │ │ ├── viewer_image.py │ │ └── layer_image.py │ ├── toolbar_vuetify.py │ └── slice_helpers.py ├── icons │ └── __init__.py ├── tests │ ├── __init__.py │ ├── data_viewer_test.py │ ├── images │ │ └── py311-test-visual.json │ ├── helpers.py │ ├── test_viewer_registry.py │ └── test_state_traitlets_helpers.py ├── matplotlib │ ├── __init__.py │ ├── tests │ │ ├── __init__.py │ │ ├── test_matplotlib.py │ │ └── data │ │ │ └── matplotlib.ipynb │ ├── profile.py │ ├── histogram.py │ ├── scatter.py │ ├── image.py │ └── base.py ├── table │ ├── tests │ │ ├── __init__.py │ │ └── test_table.py │ ├── __init__.py │ └── table.vue ├── widgets │ ├── tests │ │ ├── __init__.py │ │ └── test_linked_dropdown.py │ ├── __init__.py │ ├── glue_throttled_slider.vue │ ├── subset_mode_test.py │ ├── size.py │ ├── glue_float_field.vue │ ├── color.py │ ├── temp.vue │ ├── subset_select.vue │ ├── layeroptions.vue │ ├── subset_mode_vuetify.py │ ├── subset_select_test.py │ ├── linked_dropdown.py │ └── subset_select_vuetify.py ├── ipyvolume │ ├── tests │ │ ├── __init__.py │ │ └── data │ │ │ └── ipyvolume.ipynb │ ├── scatter │ │ ├── tests │ │ │ ├── __init__.py │ │ │ └── test_viewer.py │ │ ├── __init__.py │ │ ├── viewer.py │ │ └── layer_style_widget.py │ ├── volume │ │ ├── tests │ │ │ ├── __init__.py │ │ │ └── test_viewer.py │ │ ├── __init__.py │ │ ├── viewer.py │ │ └── layer_style_widget.py │ ├── __init__.py │ └── common │ │ ├── __init__.py │ │ ├── viewer_options_widget.py │ │ ├── tools.py │ │ └── viewer.py ├── data.py ├── registries.py ├── ipywidgets_layout.py ├── vuetify_layout.py ├── vuetify_helpers.py ├── layout_widget.vue ├── conftest.py ├── __init__.py └── link.py ├── MANIFEST.in ├── tests └── ui │ └── snapshots │ └── tests │ └── ui │ └── test_selection.py │ ├── test_elliptical_selection-chromium-png-linux-reference.png │ ├── test_rectangular_selection-chromium-png-linux-reference.png │ ├── test_elliptical_selection_rotate-chromium-png-linux-reference.png │ └── test_rectangular_selection_rotate-chromium-png-linux-reference.png ├── .readthedocs.yml ├── binder ├── setup.sh └── Dockerfile ├── .github ├── release.yml └── workflows │ ├── update-changelog.yaml │ └── ci_workflows.yml ├── RELEASE.rst ├── .validate-notebooks.py ├── .gitignore ├── docs ├── api.rst ├── index.rst ├── installing.rst └── conf.py ├── LICENSE ├── tox.ini ├── notebooks ├── Generic │ └── demo_gaussian.ipynb ├── Astronomy │ └── L1448 │ │ └── L1448 in 3D.ipynb ├── Experimental │ └── bqplot_scatter_density.ipynb └── Planes │ └── Boston Planes.ipynb ├── .pre-commit-config.yaml ├── README.rst └── .circleci └── config.yml /glue_jupyter/bqplot/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /glue_jupyter/common/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /glue_jupyter/icons/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /glue_jupyter/tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /glue_jupyter/bqplot/tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /glue_jupyter/common/tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /glue_jupyter/matplotlib/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /glue_jupyter/table/tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /glue_jupyter/widgets/tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /glue_jupyter/bqplot/image/tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /glue_jupyter/ipyvolume/tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /glue_jupyter/matplotlib/tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /glue_jupyter/bqplot/histogram/tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /glue_jupyter/bqplot/profile/tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /glue_jupyter/bqplot/scatter/tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /glue_jupyter/ipyvolume/scatter/tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /glue_jupyter/ipyvolume/volume/tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /glue_jupyter/table/__init__.py: -------------------------------------------------------------------------------- 1 | from .viewer import TableViewer # noqa 2 | -------------------------------------------------------------------------------- /glue_jupyter/bqplot/common/__init__.py: -------------------------------------------------------------------------------- 1 | from .viewer import * # noqa 2 | from .tools import * # noqa 3 | -------------------------------------------------------------------------------- /glue_jupyter/bqplot/image/__init__.py: -------------------------------------------------------------------------------- 1 | from .layer_artist import * # noqa 2 | from .viewer import * # noqa 3 | -------------------------------------------------------------------------------- /glue_jupyter/bqplot/profile/__init__.py: -------------------------------------------------------------------------------- 1 | from .layer_artist import * # noqa 2 | from .viewer import * # noqa 3 | -------------------------------------------------------------------------------- /glue_jupyter/bqplot/scatter/__init__.py: -------------------------------------------------------------------------------- 1 | from .layer_artist import * # noqa 2 | from .viewer import * # noqa 3 | -------------------------------------------------------------------------------- /glue_jupyter/bqplot/histogram/__init__.py: -------------------------------------------------------------------------------- 1 | from .layer_artist import * # noqa 2 | from .viewer import * # noqa 3 | -------------------------------------------------------------------------------- /glue_jupyter/ipyvolume/__init__.py: -------------------------------------------------------------------------------- 1 | from .volume.viewer import IpyvolumeVolumeView # noqa 2 | from .scatter.viewer import IpyvolumeScatterView # noqa 3 | -------------------------------------------------------------------------------- /glue_jupyter/ipyvolume/common/__init__.py: -------------------------------------------------------------------------------- 1 | from .viewer_options_widget import * # noqa 2 | from .viewer import * # noqa 3 | from .tools import * # noqa 4 | -------------------------------------------------------------------------------- /glue_jupyter/ipyvolume/scatter/__init__.py: -------------------------------------------------------------------------------- 1 | from .layer_artist import * # noqa 2 | from .layer_style_widget import * # noqa 3 | from .viewer import * # noqa 4 | -------------------------------------------------------------------------------- /glue_jupyter/ipyvolume/volume/__init__.py: -------------------------------------------------------------------------------- 1 | from .layer_artist import * # noqa 2 | from .layer_style_widget import * # noqa 3 | from .viewer import * # noqa 4 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include CHANGES.md 2 | include LICENSE 3 | include README.rst 4 | include pyproject.toml 5 | 6 | recursive-include docs * 7 | recursive-include notebooks *.ipynb 8 | -------------------------------------------------------------------------------- /glue_jupyter/widgets/__init__.py: -------------------------------------------------------------------------------- 1 | from .color import Color 2 | from .size import Size 3 | from .linked_dropdown import LinkedDropdown 4 | 5 | __all__ = ['Color', 'Size', 'LinkedDropdown'] 6 | -------------------------------------------------------------------------------- /glue_jupyter/tests/data_viewer_test.py: -------------------------------------------------------------------------------- 1 | from glue_jupyter.registries import viewer_registry 2 | from glue.viewers.common.viewer import Viewer 3 | 4 | 5 | @viewer_registry("externalviewer") 6 | class ExternalViewerTest(Viewer): 7 | pass 8 | -------------------------------------------------------------------------------- /tests/ui/snapshots/tests/ui/test_selection.py/test_elliptical_selection-chromium-png-linux-reference.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glue-viz/glue-jupyter/HEAD/tests/ui/snapshots/tests/ui/test_selection.py/test_elliptical_selection-chromium-png-linux-reference.png -------------------------------------------------------------------------------- /tests/ui/snapshots/tests/ui/test_selection.py/test_rectangular_selection-chromium-png-linux-reference.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glue-viz/glue-jupyter/HEAD/tests/ui/snapshots/tests/ui/test_selection.py/test_rectangular_selection-chromium-png-linux-reference.png -------------------------------------------------------------------------------- /tests/ui/snapshots/tests/ui/test_selection.py/test_elliptical_selection_rotate-chromium-png-linux-reference.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glue-viz/glue-jupyter/HEAD/tests/ui/snapshots/tests/ui/test_selection.py/test_elliptical_selection_rotate-chromium-png-linux-reference.png -------------------------------------------------------------------------------- /tests/ui/snapshots/tests/ui/test_selection.py/test_rectangular_selection_rotate-chromium-png-linux-reference.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/glue-viz/glue-jupyter/HEAD/tests/ui/snapshots/tests/ui/test_selection.py/test_rectangular_selection_rotate-chromium-png-linux-reference.png -------------------------------------------------------------------------------- /.readthedocs.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | build: 4 | os: ubuntu-22.04 5 | tools: 6 | python: "3" 7 | 8 | sphinx: 9 | builder: html 10 | configuration: docs/conf.py 11 | fail_on_warning: true 12 | 13 | python: 14 | install: 15 | - method: pip 16 | path: . 17 | extra_requirements: 18 | - docs 19 | 20 | formats: [] 21 | -------------------------------------------------------------------------------- /binder/setup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -ex 4 | 5 | # Keep git happy 6 | 7 | git config --global user.email "binder@binder.com" 8 | git config --global user.name "Binder" 9 | 10 | # Install glue-jupyter and all requirements as well as Jupyter Lab. Also 11 | # install astroquery for the GAIA notebook. 12 | 13 | pip install . jupyterlab astroquery --user 14 | -------------------------------------------------------------------------------- /glue_jupyter/bqplot/compatibility.py: -------------------------------------------------------------------------------- 1 | from bqplot_image_gl import ImageGL 2 | import bqplot 3 | 4 | ScatterGL = None 5 | if hasattr(bqplot, "ScatterGL"): 6 | ScatterGL = bqplot.ScatterGL 7 | else: 8 | import bqplot_gl 9 | 10 | ScatterGL = bqplot_gl.marks.ScatterGL 11 | 12 | try: 13 | from bqplot_gl import LinesGL 14 | except ImportError: 15 | from bqplot_image_gl import LinesGL 16 | 17 | __all__ = ["ScatterGL", "ImageGL", "LinesGL"] 18 | -------------------------------------------------------------------------------- /.github/release.yml: -------------------------------------------------------------------------------- 1 | changelog: 2 | exclude: 3 | authors: 4 | - pre-commit-ci 5 | labels: 6 | - no-changelog-entry-needed 7 | - skip-changelog 8 | 9 | categories: 10 | - title: Bug Fixes 11 | labels: 12 | - bug 13 | - title: New Features 14 | labels: 15 | - enhancement 16 | - title: Documentation 17 | labels: 18 | - documentation 19 | - title: Other Changes 20 | labels: 21 | - "*" 22 | -------------------------------------------------------------------------------- /glue_jupyter/matplotlib/tests/test_matplotlib.py: -------------------------------------------------------------------------------- 1 | import os 2 | import nbformat 3 | from nbconvert.preprocessors import ExecutePreprocessor 4 | 5 | DATA = os.path.join(os.path.dirname(__file__), 'data') 6 | 7 | 8 | def test_notebook(): 9 | 10 | # Run an actual notebook 11 | 12 | with open(os.path.join(DATA, 'matplotlib.ipynb')) as f: 13 | nb = nbformat.read(f, as_version=4) 14 | 15 | ep = ExecutePreprocessor(timeout=600, kernel_name='python3') 16 | ep.preprocess(nb, {'metadata': {'path': DATA}}) 17 | -------------------------------------------------------------------------------- /glue_jupyter/common/basic_jupyter_toolbar.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | {{ tooltip }} 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /RELEASE.rst: -------------------------------------------------------------------------------- 1 | How to release a new version of glue-jupyter 2 | ============================================ 3 | 4 | #. Follow the instructions in the `Glue documentation 5 | `_ 6 | to create a release using the `GitHub menu 7 | `_. 8 | 9 | #. Have a beverage of your choosing while you can check the build progress 10 | `here `_. 11 | (The wheels may take a little while to build). 12 | -------------------------------------------------------------------------------- /glue_jupyter/ipyvolume/volume/tests/test_viewer.py: -------------------------------------------------------------------------------- 1 | def test_non_hex_colors(app, data_volume): 2 | 3 | # Make sure non-hex colors such as '0.4' and 'red', which are valid 4 | # matplotlib colors, work as expected. 5 | 6 | viewer = app.volshow(data=data_volume) 7 | data_volume.style.color = '0.3' 8 | data_volume.style.color = 'indigo' 9 | 10 | app.subset('test', data_volume.main_components[0] > 1) 11 | viewer.layer_options.selected = 1 12 | data_volume.subsets[0].style.color = '0.5' 13 | data_volume.subsets[0].style.color = 'purple' 14 | -------------------------------------------------------------------------------- /glue_jupyter/widgets/glue_throttled_slider.vue: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 10 | 20 | -------------------------------------------------------------------------------- /.validate-notebooks.py: -------------------------------------------------------------------------------- 1 | import os 2 | import glob 3 | 4 | import nbformat 5 | from nbconvert.preprocessors import ExecutePreprocessor 6 | 7 | for notebook in glob.glob('notebooks/**/*.ipynb', recursive=True): 8 | 9 | if 'Gaia' in notebook: 10 | print('Skipping {0}'.format(notebook)) 11 | continue 12 | 13 | print("Running {0}".format(notebook)) 14 | 15 | with open(notebook) as f: 16 | nb = nbformat.read(f, as_version=4) 17 | 18 | ep = ExecutePreprocessor(timeout=600, kernel_name='python3') 19 | ep.preprocess(nb, {'metadata': {'path': os.path.dirname(notebook)}}) 20 | -------------------------------------------------------------------------------- /glue_jupyter/common/state_widgets/__init__.py: -------------------------------------------------------------------------------- 1 | # State widgets are widgets that are UI views on the viewer and layer state 2 | # classes. Since we use the same state classes for different front-ends 3 | # (e.g. bqplot or matplotlib) we keep these state widgets in glue_jupyter.common 4 | 5 | from .layer_histogram import * # noqa 6 | from .viewer_histogram import * # noqa 7 | 8 | from .layer_image import * # noqa 9 | from .viewer_image import * # noqa 10 | 11 | from .layer_profile import * # noqa 12 | from .viewer_profile import * # noqa 13 | 14 | from .layer_scatter import * # noqa 15 | from .viewer_scatter import * # noqa 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Sphinx & coverage 2 | build 3 | docs/_build 4 | docs/api 5 | glue/tests/htmlcov 6 | *.coverage 7 | *htmlcov* 8 | 9 | # Packages/installer info 10 | doc/.eggs 11 | *.egg-info 12 | dist 13 | 14 | # Compiled files 15 | *.pyc 16 | 17 | # Other generated files 18 | glue/_githash.py 19 | 20 | # Other 21 | .pylintrc 22 | *.ropeproject 23 | glue/qt/glue_qt_resources.py 24 | *.__junk* 25 | *.orig 26 | *~ 27 | .cache 28 | 29 | # Mac OSX 30 | .DS_Store 31 | 32 | # PyCharm 33 | .idea 34 | 35 | # Eclipse editor project files 36 | .project 37 | .pydevproject 38 | .settings 39 | 40 | .eggs 41 | .hypothesis 42 | .pytest_cache 43 | 44 | .ipynb_checkpoints 45 | .tox 46 | -------------------------------------------------------------------------------- /glue_jupyter/common/state_widgets/layer_profile.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | opacity 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /glue_jupyter/data.py: -------------------------------------------------------------------------------- 1 | from urllib.request import urlopen 2 | 3 | __all__ = ['require_data'] 4 | 5 | DATA_REPO = "https://raw.githubusercontent.com/glue-viz/glue-example-data/master/" 6 | 7 | 8 | def require_data(file_path): 9 | """ 10 | Download the specified file to the current folder, preserving the directory 11 | structure. 12 | 13 | Note that this should include forward slashes for paths even on Windows. 14 | """ 15 | 16 | # We use urlopen instead of urlretrieve to have control over the timeout 17 | 18 | local_path = file_path.split('/')[-1] 19 | 20 | request = urlopen(DATA_REPO + file_path, timeout=60) 21 | with open(local_path, 'wb') as f: 22 | f.write(request.read()) 23 | 24 | print("Successfully downloaded data file to {0}".format(local_path)) 25 | -------------------------------------------------------------------------------- /glue_jupyter/tests/images/py311-test-visual.json: -------------------------------------------------------------------------------- 1 | { 2 | "glue_jupyter.bqplot.image.tests.test_visual.test_contour_units[chromium]": "68d848c66bc98053f3b61a1cba97666dffbf22e9f860a32a14d6e6455ac52ee6", 3 | "glue_jupyter.bqplot.scatter.tests.test_visual.test_visual_scatter2d[chromium]": "fbdd9fe2649a0d72813c03e77af6233909df64207cb834f28da479f50b9e7a1d", 4 | "glue_jupyter.bqplot.scatter.tests.test_visual.test_visual_scatter2d_density[chromium]": "d843a816a91e37cb0212c7caae913d7563f6c2eb42b49fa18345a5952e093b2f", 5 | "glue_jupyter.bqplot.scatter.tests.test_visual.test_visual_linestyle[chromium]": "a40792d0eda64923cff310987c729f69627ed583f01ed62bda96d7d6e80cdb66", 6 | "glue_jupyter.bqplot.scatter.tests.test_visual.test_visual_vector[chromium]": "be8e5ff91d4c7793928cda6896ddbf1b449ba120093e0f920850a53d9fcb0755" 7 | } 8 | -------------------------------------------------------------------------------- /glue_jupyter/widgets/subset_mode_test.py: -------------------------------------------------------------------------------- 1 | from glue.core.edit_subset_mode import OrMode, ReplaceMode 2 | 3 | 4 | def test_subset_mode(app, datax, dataxyz, dataxz): 5 | subset_mode = app.widget_subset_mode 6 | 7 | # glue -> ui sync 8 | assert subset_mode.main.children[0] is subset_mode.modes[0][1] 9 | app.set_subset_mode('and') 10 | assert subset_mode.main.children[0] is subset_mode.modes[2][1] 11 | app.set_subset_mode('replace') 12 | assert subset_mode.main.children[0] is subset_mode.modes[0][1] 13 | 14 | # ui -> glue sync 15 | subset_mode.children[0].children[1].fire_event('click', {}) 16 | assert app.session.edit_subset_mode.mode == OrMode 17 | subset_mode.children[0].children[0].fire_event('click', {}) 18 | assert app.session.edit_subset_mode.mode == ReplaceMode 19 | -------------------------------------------------------------------------------- /glue_jupyter/common/state_widgets/viewer_scatter.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | show axes 11 | 12 | 13 | 14 | 15 | 16 | 23 | -------------------------------------------------------------------------------- /glue_jupyter/common/state_widgets/layer_histogram.py: -------------------------------------------------------------------------------- 1 | from ipywidgets import FloatSlider, IntText, VBox 2 | 3 | from ...link import link 4 | 5 | __all__ = ['HistogramLayerStateWidget'] 6 | 7 | 8 | class HistogramLayerStateWidget(VBox): 9 | 10 | def __init__(self, layer_state): 11 | 12 | self.state = layer_state 13 | 14 | self.widget_opacity = FloatSlider(min=0, max=1, step=0.01, value=self.state.alpha, 15 | description='opacity') 16 | link((self.state, 'alpha'), (self.widget_opacity, 'value')) 17 | 18 | self.widget_zorder = IntText(description='z-order') 19 | link((self.state, 'zorder'), (self.widget_zorder, 'value')) 20 | 21 | super().__init__([self.widget_opacity, self.widget_zorder]) 22 | 23 | def cleanup(self): 24 | pass 25 | -------------------------------------------------------------------------------- /glue_jupyter/common/state_widgets/viewer_histogram.py: -------------------------------------------------------------------------------- 1 | import ipyvuetify as v 2 | import traitlets 3 | from ...state_traitlets_helpers import GlueState 4 | from ...vuetify_helpers import link_glue_choices 5 | 6 | 7 | __all__ = ['HistogramViewerStateWidget'] 8 | 9 | 10 | class HistogramViewerStateWidget(v.VuetifyTemplate): 11 | template_file = (__file__, 'viewer_histogram.vue') 12 | x_att_items = traitlets.List().tag(sync=True) 13 | x_att_selected = traitlets.Int(allow_none=True).tag(sync=True) 14 | glue_state = GlueState().tag(sync=True) 15 | 16 | def __init__(self, viewer_state): 17 | super().__init__() 18 | 19 | self.glue_state = viewer_state 20 | 21 | link_glue_choices(self, viewer_state, 'x_att') 22 | 23 | def vue_bins_to_axis(self, *args): 24 | self.glue_state.update_bins_to_view() 25 | -------------------------------------------------------------------------------- /glue_jupyter/common/state_widgets/layer_profile.py: -------------------------------------------------------------------------------- 1 | from ipyvuetify import VuetifyTemplate 2 | import traitlets 3 | from ...state_traitlets_helpers import GlueState 4 | from ...vuetify_helpers import link_glue_choices, link_glue 5 | 6 | __all__ = ['ProfileLayerStateWidget'] 7 | 8 | 9 | class ProfileLayerStateWidget(VuetifyTemplate): 10 | template_file = (__file__, 'layer_profile.vue') 11 | 12 | glue_state = GlueState().tag(sync=True) 13 | 14 | attribute_items = traitlets.List().tag(sync=True) 15 | attribute_selected = traitlets.Int().tag(sync=True) 16 | as_steps = traitlets.Bool(False).tag(sync=True) 17 | 18 | def __init__(self, layer_state): 19 | super().__init__() 20 | 21 | self.glue_state = layer_state 22 | 23 | link_glue_choices(self, layer_state, 'attribute') 24 | link_glue(self, 'as_steps', layer_state) 25 | -------------------------------------------------------------------------------- /docs/api.rst: -------------------------------------------------------------------------------- 1 | API Documentation 2 | ================= 3 | General 4 | ------- 5 | .. automodapi:: glue_jupyter 6 | 7 | .. automodapi:: glue_jupyter.app 8 | 9 | .. automodapi:: glue_jupyter.link 10 | 11 | .. automodapi:: glue_jupyter.view 12 | 13 | .. automodapi:: glue_jupyter.common.state3d 14 | 15 | .. automodapi:: glue_jupyter.common.state_widgets 16 | 17 | bqplot viewers 18 | -------------- 19 | 20 | .. automodapi:: glue_jupyter.bqplot.common 21 | 22 | .. automodapi:: glue_jupyter.bqplot.histogram 23 | 24 | .. automodapi:: glue_jupyter.bqplot.image 25 | 26 | .. automodapi:: glue_jupyter.bqplot.profile 27 | 28 | .. automodapi:: glue_jupyter.bqplot.scatter 29 | 30 | ipyvolume viewers 31 | ----------------- 32 | 33 | .. automodapi:: glue_jupyter.ipyvolume.common 34 | 35 | .. automodapi:: glue_jupyter.ipyvolume.scatter 36 | 37 | .. automodapi:: glue_jupyter.ipyvolume.volume 38 | 39 | Other viewers 40 | -------------- 41 | 42 | .. automodapi:: glue_jupyter.table 43 | -------------------------------------------------------------------------------- /glue_jupyter/common/state_widgets/viewer_scatter.py: -------------------------------------------------------------------------------- 1 | import ipyvuetify as v 2 | import traitlets 3 | 4 | from ...state_traitlets_helpers import GlueState 5 | from ...vuetify_helpers import link_glue_choices 6 | 7 | __all__ = ["ScatterViewerStateWidget"] 8 | 9 | 10 | class ScatterViewerStateWidget(v.VuetifyTemplate): 11 | 12 | template_file = (__file__, "viewer_scatter.vue") 13 | 14 | glue_state = GlueState().tag(sync=True) 15 | 16 | x_att_items = traitlets.List().tag(sync=True) 17 | x_att_selected = traitlets.Int(allow_none=True).tag(sync=True) 18 | 19 | y_att_items = traitlets.List().tag(sync=True) 20 | y_att_selected = traitlets.Int(allow_none=True).tag(sync=True) 21 | 22 | def __init__(self, viewer_state): 23 | 24 | super().__init__() 25 | 26 | self.viewer_state = viewer_state 27 | self.glue_state = viewer_state 28 | 29 | link_glue_choices(self, viewer_state, "x_att") 30 | link_glue_choices(self, viewer_state, "y_att") 31 | -------------------------------------------------------------------------------- /.github/workflows/update-changelog.yaml: -------------------------------------------------------------------------------- 1 | # This workflow takes the GitHub release notes and updates the changelog on the 2 | # main branch with the body of the release notes, thereby keeping a log in 3 | # the git repo of the changes. 4 | 5 | name: "Update Changelog" 6 | 7 | on: 8 | release: 9 | types: [released] 10 | 11 | jobs: 12 | update: 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - name: Checkout code 17 | uses: actions/checkout@v2 18 | with: 19 | ref: main 20 | 21 | - name: Update Changelog 22 | uses: stefanzweifel/changelog-updater-action@v1 23 | with: 24 | release-notes: ${{ github.event.release.body }} 25 | latest-version: ${{ github.event.release.name }} 26 | path-to-changelog: CHANGES.md 27 | 28 | - name: Commit updated Changelog 29 | uses: stefanzweifel/git-auto-commit-action@v4 30 | with: 31 | branch: main 32 | commit_message: Update CHANGELOG 33 | file_pattern: CHANGES.md 34 | -------------------------------------------------------------------------------- /glue_jupyter/ipyvolume/scatter/viewer.py: -------------------------------------------------------------------------------- 1 | from glue_jupyter.common.state3d import Scatter3DViewerState 2 | from .layer_artist import IpyvolumeScatterLayerArtist 3 | from .layer_style_widget import Scatter3DLayerStateWidget 4 | from ..common.viewer_options_widget import Viewer3DStateWidget 5 | from ..common.viewer import IpyvolumeBaseView 6 | 7 | __all__ = ['IpyvolumeScatterView'] 8 | 9 | 10 | class IpyvolumeScatterView(IpyvolumeBaseView): 11 | 12 | allow_duplicate_data = False 13 | allow_duplicate_subset = False 14 | 15 | _state_cls = Scatter3DViewerState 16 | _options_cls = Viewer3DStateWidget 17 | _data_artist_cls = IpyvolumeScatterLayerArtist 18 | _subset_artist_cls = IpyvolumeScatterLayerArtist 19 | _layer_style_widget_cls = Scatter3DLayerStateWidget 20 | 21 | def get_data_layer_artist(self, layer=None, layer_state=None): 22 | return self.get_layer_artist(self._data_artist_cls, layer=layer, layer_state=layer_state) 23 | 24 | def get_subset_layer_artist(self, layer=None, layer_state=None): 25 | return self.get_layer_artist(self._subset_artist_cls, layer=layer, layer_state=layer_state) 26 | -------------------------------------------------------------------------------- /glue_jupyter/registries.py: -------------------------------------------------------------------------------- 1 | from glue.config import DictRegistry 2 | 3 | __all__ = ['viewer_registry', 'ViewerRegistry'] 4 | 5 | 6 | class ViewerRegistry(DictRegistry): 7 | """ 8 | Registry containing references to custom viewers. 9 | """ 10 | 11 | def __call__(self, name=None): 12 | def decorator(cls): 13 | self.add(name, cls) 14 | return cls 15 | return decorator 16 | 17 | def add(self, name, cls): 18 | """ 19 | Add an item to the registry. 20 | 21 | Parameters 22 | ---------- 23 | name : str 24 | The key referencing the associated class in the registry 25 | dictionary. 26 | cls : type 27 | The class definition (not instance) associated with the name given 28 | in the first parameter. 29 | """ 30 | if name in self.members: 31 | raise ValueError(f"Viewer with the name {name} already exists, " 32 | f"please choose a different name.") 33 | else: 34 | self.members[name] = {'cls': cls} 35 | 36 | 37 | viewer_registry = ViewerRegistry() 38 | -------------------------------------------------------------------------------- /glue_jupyter/common/tests/test_state3d.py: -------------------------------------------------------------------------------- 1 | import traitlets 2 | 3 | from glue_jupyter.state_traitlets_helpers import GlueState 4 | from glue_jupyter.common.state3d import ViewerState3D 5 | 6 | 7 | class Widget(traitlets.HasTraits): 8 | 9 | state = GlueState() 10 | 11 | latest_json = None 12 | 13 | # The following two methods mimic the behavior of ipywidgets 14 | 15 | @traitlets.observe('state') 16 | def on_state_change(self, change): 17 | to_json = self.trait_metadata('state', 'to_json') 18 | self.latest_json = to_json(self.state, self) 19 | 20 | def set_state_from_json(self, json): 21 | from_json = self.trait_metadata('state', 'from_json') 22 | from_json(json, self) 23 | 24 | 25 | def test_json_serializable(): 26 | widget = Widget() 27 | assert widget.latest_json is None 28 | widget.state = ViewerState3D() 29 | assert widget.latest_json == { 30 | "x_att": None, "x_min": 0, "x_max": 1, 31 | "y_att": None, "y_min": 0, "y_max": 1, 32 | "z_att": None, "z_min": 0, "z_max": 1, 33 | "visible_axes": True, "native_aspect": False, 34 | "layers": [], "title": None, 35 | } 36 | -------------------------------------------------------------------------------- /glue_jupyter/ipywidgets_layout.py: -------------------------------------------------------------------------------- 1 | # A vuetify layout for the glue data viewers. For now we keep this isolated to 2 | # a single file, but once we are happy with it we can just replace the original 3 | # default layout. 4 | 5 | from ipywidgets import HBox, Tab, VBox 6 | 7 | 8 | __all__ = ['ipywidgets_layout_factory'] 9 | 10 | 11 | def ipywidgets_layout_factory(viewer): 12 | 13 | # Take all the different widgets and construct a standard layout 14 | # for the viewers, based on ipywidgets HBox and VBox. This can be 15 | # overridden in sub-classes to create alternate layouts. 16 | 17 | layout_toolbar = HBox([viewer.toolbar_selection_tools, 18 | viewer.toolbar_active_subset, 19 | viewer.toolbar_selection_mode]) 20 | 21 | layout_tab = Tab([viewer.viewer_options, 22 | viewer.layer_options]) 23 | layout_tab.set_title(0, "General") 24 | layout_tab.set_title(1, "Layers") 25 | 26 | layout = VBox([layout_toolbar, 27 | HBox([viewer.figure_widget, 28 | layout_tab]), 29 | viewer.output_widget]) 30 | 31 | return layout 32 | -------------------------------------------------------------------------------- /glue_jupyter/common/state_widgets/tests/test_viewer_image.py: -------------------------------------------------------------------------------- 1 | def test_contour_levels(app, data_image): 2 | viewer = app.imshow(data=data_image) 3 | widget_state = viewer._layout_layer_options.layers[0]['layer_panel'] 4 | layer_state = widget_state.layer_state 5 | layer_state.level_mode = "Custom" 6 | 7 | widget_state.c_levels_txt = '1e2, 1e3, 0.1, .1' 8 | assert layer_state.levels == [100, 1000, 0.1, 0.1] 9 | assert widget_state.c_levels_txt == '1e2, 1e3, 0.1, .1' 10 | 11 | layer_state.levels = [10, 100] 12 | assert widget_state.c_levels_txt == '10, 100' 13 | 14 | 15 | def test_no_slider_if_flat(app, data_flat): 16 | viewer = app.imshow(data=data_flat) 17 | widget = viewer.viewer_options 18 | 19 | viewer.state.reference_data = data_flat 20 | viewer.state.x_att = data_flat.pixel_component_ids[0] 21 | viewer.state.y_att = data_flat.pixel_component_ids[1] 22 | 23 | assert len(widget.sliders) == 1 24 | assert {slider['index'] for slider in widget.sliders} == {3} 25 | 26 | viewer.state.x_att = data_flat.pixel_component_ids[2] 27 | 28 | assert len(widget.sliders) == 2 29 | assert {slider['index'] for slider in widget.sliders} == {0, 3} 30 | -------------------------------------------------------------------------------- /binder/Dockerfile: -------------------------------------------------------------------------------- 1 | # This dockerfile is used to set up a plain Linux environment into which we can 2 | # install all dependencies with pip, and avoid conda. The documentation about 3 | # using Dockerfiles with mybinder can be found here: 4 | 5 | # https://mybinder.readthedocs.io/en/latest/tutorials/dockerfile.html#preparing-your-dockerfile 6 | 7 | FROM ubuntu:20.04 8 | 9 | # Set up Python 3.6 10 | 11 | RUN apt update 12 | RUN apt install -y python3 python3-pip git 13 | RUN pip3 install --upgrade pip 14 | 15 | # Set up user as required by mybinder docs: 16 | 17 | ENV NB_USER jovyan 18 | ENV NB_UID 1000 19 | ENV HOME /home/${NB_USER} 20 | 21 | RUN adduser --disabled-password \ 22 | --gecos "Default user" \ 23 | --uid ${NB_UID} \ 24 | ${NB_USER} 25 | 26 | # Copy the contents of notebooks and the postBuild script into the root of 27 | # the binder environment. 28 | 29 | COPY . ${HOME}/ 30 | USER root 31 | RUN chown -R ${NB_UID} ${HOME} 32 | USER ${NB_USER} 33 | 34 | # Change working directory 35 | 36 | WORKDIR ${HOME} 37 | 38 | # Update variables to point to local install 39 | 40 | ENV PATH="${HOME}/.local/bin:${PATH}" 41 | ENV JUPYTER_CONFIG_DIR="${HOME}/.local/etc/jupyter/" 42 | 43 | # Run post-build script 44 | 45 | RUN binder/setup.sh 46 | -------------------------------------------------------------------------------- /glue_jupyter/matplotlib/profile.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, division, print_function 2 | 3 | from glue.utils import defer_draw, decorate_all_methods 4 | from glue.viewers.profile.layer_artist import ProfileLayerArtist 5 | from glue.viewers.profile.state import ProfileViewerState 6 | from glue.viewers.profile.viewer import MatplotlibProfileMixin 7 | 8 | from .base import MatplotlibJupyterViewer 9 | 10 | from glue_jupyter.common.state_widgets.layer_profile import ProfileLayerStateWidget 11 | from glue_jupyter.common.state_widgets.viewer_profile import ProfileViewerStateWidget 12 | 13 | __all__ = ['ProfileJupyterViewer'] 14 | 15 | 16 | @decorate_all_methods(defer_draw) 17 | class ProfileJupyterViewer(MatplotlibProfileMixin, MatplotlibJupyterViewer): 18 | 19 | LABEL = '1D Profile' 20 | 21 | _state_cls = ProfileViewerState 22 | _data_artist_cls = ProfileLayerArtist 23 | _subset_artist_cls = ProfileLayerArtist 24 | _options_cls = ProfileViewerStateWidget 25 | _layer_style_widget_cls = ProfileLayerStateWidget 26 | 27 | def __init__(self, session, parent=None, state=None): 28 | super(ProfileJupyterViewer, self).__init__(session, parent=parent, state=state) 29 | MatplotlibProfileMixin.setup_callbacks(self) 30 | -------------------------------------------------------------------------------- /glue_jupyter/matplotlib/histogram.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, division, print_function 2 | 3 | from glue.utils import defer_draw, decorate_all_methods 4 | from glue.viewers.histogram.layer_artist import HistogramLayerArtist 5 | from glue.viewers.histogram.state import HistogramViewerState 6 | from glue.viewers.histogram.viewer import MatplotlibHistogramMixin 7 | 8 | from .base import MatplotlibJupyterViewer 9 | 10 | from glue_jupyter.common.state_widgets.layer_histogram import HistogramLayerStateWidget 11 | from glue_jupyter.common.state_widgets.viewer_histogram import HistogramViewerStateWidget 12 | 13 | __all__ = ['HistogramJupyterViewer'] 14 | 15 | 16 | @decorate_all_methods(defer_draw) 17 | class HistogramJupyterViewer(MatplotlibHistogramMixin, MatplotlibJupyterViewer): 18 | 19 | LABEL = '1D Histogram' 20 | 21 | _state_cls = HistogramViewerState 22 | _data_artist_cls = HistogramLayerArtist 23 | _subset_artist_cls = HistogramLayerArtist 24 | _options_cls = HistogramViewerStateWidget 25 | _layer_style_widget_cls = HistogramLayerStateWidget 26 | 27 | tools = ['select:xrange'] 28 | 29 | def __init__(self, session, parent=None, state=None): 30 | super(HistogramJupyterViewer, self).__init__(session, parent=parent, state=state) 31 | MatplotlibHistogramMixin.setup_callbacks(self) 32 | -------------------------------------------------------------------------------- /glue_jupyter/vuetify_layout.py: -------------------------------------------------------------------------------- 1 | # A vuetify layout for the glue data viewers. For now we keep this isolated to 2 | # a single file, but once we are happy with it we can just replace the original 3 | # default layout. 4 | 5 | import ipyvuetify as v 6 | import traitlets 7 | from ipywidgets import widget_serialization 8 | 9 | __all__ = ['vuetify_layout_factory'] 10 | 11 | 12 | class LayoutWidget(v.VuetifyTemplate): 13 | template_file = (__file__, 'layout_widget.vue') 14 | 15 | controls = traitlets.Dict().tag(sync=True, **widget_serialization) 16 | drawer_open = traitlets.Bool(False).tag(sync=True) 17 | open_panels = traitlets.List(default_value=[0, 1]).tag(sync=True) 18 | 19 | def __init__(self, viewer, *args, **kwargs): 20 | super().__init__(*args, **kwargs) 21 | self.controls = dict( 22 | toolbar_selection_tools=viewer.toolbar_selection_tools, 23 | toolbar_selection_mode=viewer.toolbar_selection_mode, 24 | toolbar_active_subset=viewer.toolbar_active_subset, 25 | figure_widget=viewer.figure_widget, 26 | output_widget=viewer.output_widget, 27 | viewer_options=viewer.viewer_options, 28 | layer_options=viewer.layer_options, 29 | ) 30 | 31 | 32 | def vuetify_layout_factory(viewer): 33 | return LayoutWidget(viewer) 34 | -------------------------------------------------------------------------------- /glue_jupyter/matplotlib/scatter.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, division, print_function 2 | 3 | from glue.utils import defer_draw, decorate_all_methods 4 | from glue.viewers.scatter.layer_artist import ScatterLayerArtist 5 | from glue.viewers.scatter.state import ScatterViewerState 6 | from glue.viewers.scatter.viewer import MatplotlibScatterMixin 7 | 8 | from .base import MatplotlibJupyterViewer 9 | 10 | from glue_jupyter.common.state_widgets.layer_scatter import ScatterLayerStateWidget 11 | from glue_jupyter.common.state_widgets.viewer_scatter import ScatterViewerStateWidget 12 | 13 | __all__ = ['ScatterJupyterViewer'] 14 | 15 | 16 | @decorate_all_methods(defer_draw) 17 | class ScatterJupyterViewer(MatplotlibScatterMixin, MatplotlibJupyterViewer): 18 | 19 | LABEL = '2D Scatter' 20 | 21 | _state_cls = ScatterViewerState 22 | _data_artist_cls = ScatterLayerArtist 23 | _subset_artist_cls = ScatterLayerArtist 24 | _options_cls = ScatterViewerStateWidget 25 | _layer_style_widget_cls = ScatterLayerStateWidget 26 | 27 | tools = ['select:rectangle', 'select:xrange', 28 | 'select:yrange', 'select:circle', 29 | 'select:polygon'] 30 | 31 | def __init__(self, session, parent=None, state=None): 32 | super(ScatterJupyterViewer, self).__init__(session, parent=parent, state=state) 33 | MatplotlibScatterMixin.setup_callbacks(self) 34 | -------------------------------------------------------------------------------- /glue_jupyter/bqplot/image/tests/test_visual.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from numpy.testing import assert_allclose 3 | 4 | from glue_jupyter import jglue 5 | from glue_jupyter.tests.helpers import visual_widget_test 6 | 7 | 8 | @visual_widget_test 9 | def test_contour_units( 10 | tmp_path, 11 | page_session, 12 | solara_test, 13 | ): 14 | 15 | x = np.linspace(-7, 7, 88) 16 | y = np.linspace(-6, 6, 69) 17 | X, Y = np.meshgrid(x, y) 18 | Z = np.exp(-(X * X + Y * Y) / 4) 19 | 20 | app = jglue() 21 | data = app.add_data(data={"x": X, "y": Y, "z": Z})[0] 22 | data.get_component("z").units = 'km' 23 | image = app.imshow(show=False) 24 | image.state.layers[0].attribute = data.id['z'] 25 | image.state.layers[0].contour_visible = True 26 | image.state.layers[0].c_min = 0.1 27 | image.state.layers[0].c_max = 0.9 28 | image.state.layers[0].n_levels = 5 29 | 30 | assert_allclose(image.state.layers[0].levels, [0.1, 0.3, 0.5, 0.7, 0.9]) 31 | 32 | image.state.layers[0].attribute_display_unit = 'mm' 33 | image.state.layers[0].attribute_display_unit = 'km' 34 | image.state.layers[0].attribute_display_unit = 'm' 35 | 36 | assert_allclose(image.state.layers[0].levels, [100, 300, 500, 700, 900]) 37 | assert image.state.layers[0].labels == ['100', '300', '500', '700', '900'] 38 | 39 | figure = image.figure_widget 40 | figure.layout = {"width": "400px", "height": "250px"} 41 | return figure 42 | -------------------------------------------------------------------------------- /glue_jupyter/ipyvolume/volume/viewer.py: -------------------------------------------------------------------------------- 1 | from glue_jupyter.common.state3d import VolumeViewerState 2 | from glue_jupyter.registries import viewer_registry 3 | 4 | from .layer_artist import IpyvolumeVolumeLayerArtist 5 | from .layer_style_widget import Volume3DLayerStateWidget 6 | 7 | from ..scatter.layer_artist import IpyvolumeScatterLayerArtist 8 | from ..scatter.layer_style_widget import Scatter3DLayerStateWidget 9 | 10 | from ..common.viewer_options_widget import Viewer3DStateWidget 11 | from ..common.viewer import IpyvolumeBaseView 12 | 13 | __all__ = ['IpyvolumeVolumeView'] 14 | 15 | 16 | @viewer_registry("volume") 17 | class IpyvolumeVolumeView(IpyvolumeBaseView): 18 | 19 | _state_cls = VolumeViewerState 20 | _options_cls = Viewer3DStateWidget 21 | _data_artist_cls = IpyvolumeVolumeLayerArtist 22 | _subset_artist_cls = IpyvolumeVolumeLayerArtist 23 | _layer_style_widget_cls = {IpyvolumeVolumeLayerArtist: Volume3DLayerStateWidget, 24 | IpyvolumeScatterLayerArtist: Scatter3DLayerStateWidget} 25 | 26 | def get_data_layer_artist(self, layer=None, layer_state=None): 27 | if layer.ndim == 1: 28 | cls = IpyvolumeScatterLayerArtist 29 | else: 30 | cls = IpyvolumeVolumeLayerArtist 31 | return self.get_layer_artist(cls, layer=layer, layer_state=layer_state) 32 | 33 | def get_subset_layer_artist(self, layer=None, layer_state=None): 34 | return self.get_data_layer_artist(layer, layer_state) 35 | -------------------------------------------------------------------------------- /glue_jupyter/common/state_widgets/viewer_profile.py: -------------------------------------------------------------------------------- 1 | import ipyvuetify as v 2 | import traitlets 3 | from ...state_traitlets_helpers import GlueState 4 | from ...vuetify_helpers import link_glue_choices 5 | 6 | __all__ = ['ProfileViewerStateWidget'] 7 | 8 | 9 | class ProfileViewerStateWidget(v.VuetifyTemplate): 10 | template_file = (__file__, 'viewer_profile.vue') 11 | 12 | glue_state = GlueState().tag(sync=True) 13 | 14 | reference_data_items = traitlets.List().tag(sync=True) 15 | reference_data_selected = traitlets.Int(allow_none=True).tag(sync=True) 16 | 17 | x_att_items = traitlets.List().tag(sync=True) 18 | x_att_selected = traitlets.Int(allow_none=True).tag(sync=True) 19 | 20 | function_items = traitlets.List().tag(sync=True) 21 | function_selected = traitlets.Int(allow_none=True).tag(sync=True) 22 | 23 | x_display_unit_items = traitlets.List().tag(sync=True) 24 | x_display_unit_selected = traitlets.Int(allow_none=True).tag(sync=True) 25 | 26 | y_display_unit_items = traitlets.List().tag(sync=True) 27 | y_display_unit_selected = traitlets.Int(allow_none=True).tag(sync=True) 28 | 29 | def __init__(self, viewer_state): 30 | super().__init__() 31 | 32 | self.glue_state = viewer_state 33 | 34 | link_glue_choices(self, viewer_state, 'reference_data') 35 | link_glue_choices(self, viewer_state, 'x_att') 36 | link_glue_choices(self, viewer_state, 'function') 37 | link_glue_choices(self, viewer_state, 'x_display_unit') 38 | link_glue_choices(self, viewer_state, 'y_display_unit') 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Glue - multidimensional data exploration 2 | 3 | Copyright (c) 2018-2021, Glue developers 4 | 5 | All rights reserved. 6 | 7 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 8 | 9 | * Redistributions of source code must retain the above copyright 10 | notice, this list of conditions and the following disclaimer. 11 | * Redistributions in binary form must reproduce the above copyright 12 | notice, this list of conditions and the following disclaimer in the 13 | documentation and/or other materials provided with the 14 | distribution. 15 | * Neither the name of the Glue project nor the names of its 16 | contributors may be used to endorse or promote products derived 17 | from this software without specific prior written permission. 18 | 19 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 20 | -------------------------------------------------------------------------------- /glue_jupyter/bqplot/scatter/viewer.py: -------------------------------------------------------------------------------- 1 | from glue.viewers.scatter.state import ScatterViewerState 2 | 3 | from ..common.viewer import BqplotBaseView 4 | 5 | from .layer_artist import BqplotScatterLayerArtist 6 | 7 | from glue_jupyter.common.state_widgets.layer_scatter import ScatterLayerStateWidget 8 | from glue_jupyter.common.state_widgets.viewer_scatter import ScatterViewerStateWidget 9 | 10 | from glue_jupyter.registries import viewer_registry 11 | 12 | __all__ = ['BqplotScatterView'] 13 | 14 | 15 | @viewer_registry("scatter") 16 | class BqplotScatterView(BqplotBaseView): 17 | 18 | allow_duplicate_data = False 19 | allow_duplicate_subset = False 20 | 21 | _state_cls = ScatterViewerState 22 | _options_cls = ScatterViewerStateWidget 23 | _data_artist_cls = BqplotScatterLayerArtist 24 | _subset_artist_cls = BqplotScatterLayerArtist 25 | _layer_style_widget_cls = ScatterLayerStateWidget 26 | 27 | tools = ['bqplot:home', 'bqplot:panzoom', 'bqplot:rectangle', 'bqplot:circle', 28 | 'bqplot:ellipse', 'bqplot:xrange', 'bqplot:yrange', 'bqplot:polygon', 'bqplot:lasso'] 29 | 30 | def __init__(self, *args, **kwargs): 31 | super().__init__(*args, **kwargs) 32 | self.state.add_callback('x_att', self._update_axes) 33 | self.state.add_callback('y_att', self._update_axes) 34 | self._update_axes() 35 | 36 | def _update_axes(self, *args): 37 | 38 | if self.state.x_att is not None: 39 | self.state.x_axislabel = str(self.state.x_att) 40 | 41 | if self.state.y_att is not None: 42 | self.state.y_axislabel = str(self.state.y_att) 43 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = 3 | py{310,311,312,313,314}-{test,notebooks,docs,dev} 4 | requires = pip >= 18.0 5 | setuptools >= 30.3.0 6 | 7 | [testenv] 8 | passenv = 9 | DISPLAY 10 | HOME 11 | setenv = 12 | JUPYTER_PLATFORM_DIRS=1 13 | HOST=0.0.0.0 14 | whitelist_externals = 15 | find 16 | rm 17 | sed 18 | make 19 | changedir = 20 | test: .tmp/{envname} 21 | docs: docs 22 | deps = 23 | notebooks: astroquery 24 | notebooks: pyyaml 25 | dev: git+https://github.com/glue-viz/glue 26 | extras = 27 | test: test 28 | notebooks: test 29 | docs: docs 30 | visual: visualtest 31 | install_command = 32 | !dev: python -I -m pip install 33 | dev: python -I -m pip install -v 34 | commands = 35 | test: pip freeze 36 | test-!visual: pytest --pyargs glue_jupyter --cov glue_jupyter {posargs} 37 | test-visual: playwright install chromium 38 | test-visual: pytest --show-capture=no --pyargs glue_jupyter {posargs} --mpl -m mpl_image_compare --mpl --mpl-generate-summary=html --mpl-results-path={toxinidir}/results --mpl-hash-library={toxinidir}/glue_jupyter/tests/images/{envname}.json --mpl-baseline-path=https://raw.githubusercontent.com/glue-viz/glue-jupyter-visual-tests/main/images/{envname}/ 39 | notebooks: python .validate-notebooks.py 40 | docs: sphinx-build -W -n -b html -d _build/doctrees . _build/html 41 | 42 | [testenv:codestyle] 43 | skipsdist = true 44 | skip_install = true 45 | description = Run all style and file checks with pre-commit 46 | deps = 47 | pre-commit 48 | commands = 49 | pre-commit install-hooks 50 | pre-commit run --color always --all-files --show-diff-on-failure 51 | -------------------------------------------------------------------------------- /glue_jupyter/ipyvolume/common/viewer_options_widget.py: -------------------------------------------------------------------------------- 1 | from ipywidgets import Checkbox, VBox, ToggleButton 2 | 3 | import ipyvolume as ipv 4 | 5 | from ...link import link, dlink 6 | from ...widgets import LinkedDropdown 7 | 8 | 9 | __all__ = ['Viewer3DStateWidget'] 10 | 11 | 12 | class Viewer3DStateWidget(VBox): 13 | 14 | def __init__(self, viewer_state): 15 | 16 | self.state = viewer_state 17 | 18 | self.widget_show_axes = Checkbox(value=False, description="Show axes") 19 | link((self.state, 'visible_axes'), (self.widget_show_axes, 'value')) 20 | 21 | self.widget_native_aspect = Checkbox(value=False, description="Native aspect ratio") 22 | link((self.state, 'native_aspect'), (self.widget_native_aspect, 'value')) 23 | 24 | self.widgets_axis = [] 25 | for i, axis_name in enumerate('xyz'): 26 | widget_axis = LinkedDropdown(self.state, axis_name + '_att', 27 | label=axis_name + ' axis') 28 | self.widgets_axis.append(widget_axis) 29 | 30 | super().__init__([self.widget_show_axes, self.widget_native_aspect] + self.widgets_axis) 31 | 32 | if hasattr(self.state, 'figure'): 33 | self.widget_show_movie_maker = ToggleButton(value=False, description="Show movie maker") 34 | self.movie_maker = ipv.moviemaker.MovieMaker(self.state.figure, 35 | self.state.figure.camera) 36 | dlink((self.widget_show_movie_maker, 'value'), 37 | (self.movie_maker.widget_main.layout, 'display'), 38 | lambda value: None if value else 'none') 39 | self.children += (self.widget_show_movie_maker, self.movie_maker.widget_main) 40 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | Glue in Jupyter 2 | =============== 3 | 4 | About 5 | ----- 6 | 7 | The **glue-jupyter** package provides a way to use the `glue 8 | `_ package for multi-dimensional linked-data exploration in 9 | the `Jupyter `_ notebook and Jupyter Lab. This package is 10 | still in early development, but it is already possible to try out some of the 11 | functionality. If you run into any issues or would like to request features, 12 | please head over to our `issue tracker 13 | `_. 14 | 15 | User guide 16 | ---------- 17 | 18 | .. toctree:: 19 | :maxdepth: 1 20 | 21 | installing 22 | getting_started 23 | developer_notes 24 | api 25 | 26 | Example notebooks 27 | ----------------- 28 | 29 | In addition to the user guide above, we provide several example notebooks that 30 | can be run online without having to install glue-jupyter on your computer (these 31 | run on https://mybinder.org/): 32 | 33 | * `Investigating star formation in the W5 region `__ (example with linking a table and an image) 34 | * `Exploring the L1448 data in 3D `__ (example of 3D volume rendering) 35 | * `Visualizing flight paths in the Boston area `__ (example with a single tabular dataset) 36 | * `Distance to the Pleiades with GAIA data `__ 37 | -------------------------------------------------------------------------------- /glue_jupyter/matplotlib/image.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, division, print_function 2 | 3 | from glue.utils import defer_draw, decorate_all_methods 4 | from glue.viewers.image.layer_artist import ImageLayerArtist, ImageSubsetLayerArtist 5 | from glue.viewers.scatter.layer_artist import ScatterLayerArtist 6 | from glue.viewers.image.state import ImageViewerState 7 | from glue.viewers.image.viewer import MatplotlibImageMixin 8 | 9 | from .base import MatplotlibJupyterViewer 10 | 11 | from glue_jupyter.common.state_widgets.layer_scatter import ScatterLayerStateWidget 12 | from glue_jupyter.common.state_widgets.layer_image import (ImageLayerStateWidget, 13 | ImageSubsetLayerStateWidget) 14 | from glue_jupyter.common.state_widgets.viewer_image import ImageViewerStateWidget 15 | 16 | __all__ = ['ImageJupyterViewer'] 17 | 18 | 19 | @decorate_all_methods(defer_draw) 20 | class ImageJupyterViewer(MatplotlibImageMixin, MatplotlibJupyterViewer): 21 | 22 | LABEL = '2D Image' 23 | 24 | _state_cls = ImageViewerState 25 | _data_artist_cls = ImageLayerArtist 26 | _options_cls = ImageViewerStateWidget 27 | _subset_artist_cls = ImageLayerArtist 28 | _layer_style_widget_cls = {ImageLayerArtist: ImageLayerStateWidget, 29 | ImageSubsetLayerArtist: ImageSubsetLayerStateWidget, 30 | ScatterLayerArtist: ScatterLayerStateWidget} 31 | 32 | tools = ['select:rectangle', 'select:xrange', 33 | 'select:yrange', 'select:circle', 34 | 'select:polygon', 'image:point_selection'] 35 | 36 | def __init__(self, session, parent=None, state=None): 37 | super(ImageJupyterViewer, self).__init__(session, wcs=True, parent=parent, state=state) 38 | MatplotlibImageMixin.setup_callbacks(self) 39 | -------------------------------------------------------------------------------- /docs/installing.rst: -------------------------------------------------------------------------------- 1 | Installation 2 | ============ 3 | 4 | Basic instructions 5 | ------------------ 6 | 7 | The **glue-jupyter** package is currently under active development, and depends 8 | on the latest developer version of several packages. Therefore, we recommend 9 | installing it in an isolated Python environment for now. If you are using conda, 10 | you can create an environment and install the latest version of glue-jupyter in 11 | it using:: 12 | 13 | conda create -n glue-jupyter -c glueviz/label/dev python=3.11 glue-jupyter 14 | 15 | To switch to the environment, use:: 16 | 17 | conda activate glue-jupyter 18 | 19 | If you have already installed glue-jupyter as above and want to update it and 20 | all its dependencies, switch to the environment then use:: 21 | 22 | conda update -c glueviz/label/dev --all 23 | 24 | The above will use conda packages built every day, but may not always include 25 | changes made within the last day. If you want to make sure you have the very 26 | latest version of glue-jupyter, or if you find conda too slow to install all the 27 | dependencies, you can also create the environment with conda (or another Python 28 | environment manager):: 29 | 30 | conda create -n glue-jupyter python=3.11 31 | 32 | then switch to the environment as above and install glue-jupyter and all its 33 | dependencies with:: 34 | 35 | pip install git+https://github.com/glue-viz/glue-jupyter.git 36 | 37 | Jupyter Lab 38 | ----------- 39 | 40 | If you are interested in using glue-jupyter in Jupyter Lab, you will need to 41 | first install Jupyter Lab (if not already installed):: 42 | 43 | pip install jupyterlab 44 | 45 | then install the following extensions manually:: 46 | 47 | jupyter labextension install @jupyter-widgets/jupyterlab-manager \ 48 | ipyvolume jupyter-threejs \ 49 | bqplot bqplot-image-gl jupyter-vuetify 50 | -------------------------------------------------------------------------------- /glue_jupyter/tests/helpers.py: -------------------------------------------------------------------------------- 1 | from functools import wraps 2 | 3 | import pytest 4 | from IPython.display import display 5 | 6 | try: 7 | import solara # noqa 8 | import playwright # noqa 9 | import pytest_mpl # noqa 10 | import pytest_playwright # noqa 11 | except ImportError: 12 | HAS_VISUAL_TEST_DEPS = False 13 | else: 14 | HAS_VISUAL_TEST_DEPS = True 15 | 16 | __all__ = ['visual_widget_test'] 17 | 18 | 19 | class DummyFigure: 20 | 21 | def __init__(self, png_bytes): 22 | self._png_bytes = png_bytes 23 | 24 | def savefig(self, filename_or_fileobj, *args, **kwargs): 25 | if isinstance(filename_or_fileobj, str): 26 | with open(filename_or_fileobj, 'wb') as f: 27 | f.write(self._png_bytes) 28 | else: 29 | filename_or_fileobj.write(self._png_bytes) 30 | 31 | 32 | def visual_widget_test(*args, **kwargs): 33 | 34 | tolerance = kwargs.pop("tolerance", 0) 35 | 36 | def decorator(test_function): 37 | @pytest.mark.skipif("not HAS_VISUAL_TEST_DEPS") 38 | @pytest.mark.mpl_image_compare( 39 | tolerance=tolerance, **kwargs 40 | ) 41 | @wraps(test_function) 42 | def test_wrapper(tmp_path, page_session, *args, **kwargs): 43 | layout = test_function(tmp_path, page_session, *args, **kwargs) 44 | 45 | layout.add_class("test-viewer") 46 | 47 | display(layout) 48 | 49 | viewer = page_session.locator(".test-viewer") 50 | viewer.wait_for() 51 | 52 | screenshot = viewer.screenshot() 53 | 54 | return DummyFigure(screenshot) 55 | 56 | return test_wrapper 57 | 58 | # If the decorator was used without any arguments, the only positional 59 | # argument will be the test to decorate so we do the following: 60 | if len(args) == 1: 61 | return decorator(*args) 62 | 63 | return decorator 64 | -------------------------------------------------------------------------------- /notebooks/Generic/demo_gaussian.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "import glue_jupyter as gj" 10 | ] 11 | }, 12 | { 13 | "cell_type": "code", 14 | "execution_count": null, 15 | "metadata": {}, 16 | "outputs": [], 17 | "source": [ 18 | "points = gj.example_data_xyz(loc=60, scale=30, N=10*1000)\n", 19 | "app = gj.jglue(points=points)" 20 | ] 21 | }, 22 | { 23 | "cell_type": "code", 24 | "execution_count": null, 25 | "metadata": {}, 26 | "outputs": [], 27 | "source": [] 28 | }, 29 | { 30 | "cell_type": "code", 31 | "execution_count": null, 32 | "metadata": {}, 33 | "outputs": [], 34 | "source": [ 35 | "s = app.scatter2d(x='x', y='y')" 36 | ] 37 | }, 38 | { 39 | "cell_type": "code", 40 | "execution_count": null, 41 | "metadata": {}, 42 | "outputs": [], 43 | "source": [ 44 | "s = app.scatter3d(x='x', y='y', z='z')" 45 | ] 46 | }, 47 | { 48 | "cell_type": "code", 49 | "execution_count": null, 50 | "metadata": {}, 51 | "outputs": [], 52 | "source": [ 53 | "s = app.scatter3d(x='vx', y='vy', z='vz')" 54 | ] 55 | }, 56 | { 57 | "cell_type": "code", 58 | "execution_count": null, 59 | "metadata": {}, 60 | "outputs": [], 61 | "source": [ 62 | "s = app.histogram1d(x='speed')" 63 | ] 64 | } 65 | ], 66 | "metadata": { 67 | "kernelspec": { 68 | "display_name": "Python 3", 69 | "language": "python", 70 | "name": "python3" 71 | }, 72 | "language_info": { 73 | "codemirror_mode": { 74 | "name": "ipython", 75 | "version": 3 76 | }, 77 | "file_extension": ".py", 78 | "mimetype": "text/x-python", 79 | "name": "python", 80 | "nbconvert_exporter": "python", 81 | "pygments_lexer": "ipython3", 82 | "version": "3.7.1" 83 | } 84 | }, 85 | "nbformat": 4, 86 | "nbformat_minor": 2 87 | } 88 | -------------------------------------------------------------------------------- /glue_jupyter/vuetify_helpers.py: -------------------------------------------------------------------------------- 1 | from .widgets.linked_dropdown import get_choices 2 | 3 | 4 | def link_glue(widget, widget_prop, state, glue_prop=None, from_glue_fn=lambda x: x, 5 | to_glue_fn=lambda x: x): 6 | 7 | if not glue_prop: 8 | glue_prop = widget_prop 9 | 10 | def from_glue_state(*args): 11 | setattr(widget, widget_prop, from_glue_fn(getattr(state, glue_prop))) 12 | 13 | state.add_callback(glue_prop, from_glue_state) 14 | from_glue_state() 15 | 16 | def to_glue_state(change): 17 | setattr(state, glue_prop, to_glue_fn(change['new'])) 18 | 19 | widget.observe(to_glue_state, names=[widget_prop]) 20 | 21 | 22 | def link_glue_choices(widget, state, prop): 23 | """ 24 | Links the choices of state.prop to the traitlet widget.{prop}_items, the 25 | selected value to the traitlet widget.{prop}_selected. 26 | """ 27 | 28 | def update_choices(*args): 29 | labels = get_choices(state, prop)[1] 30 | items = [dict(text=label, value=index) for index, label in enumerate(labels)] 31 | setattr(widget, f'{prop}_items', items) 32 | 33 | state.add_callback(prop, update_choices) 34 | update_choices() 35 | 36 | def choice_to_index(choice): 37 | if choice is None: 38 | return None 39 | return get_choices(state, prop)[0].index(choice) 40 | 41 | def index_to_choice(index): 42 | if index is None: 43 | return None 44 | return get_choices(state, prop)[0][index] 45 | 46 | link_glue(widget, f'{prop}_selected', state, prop, 47 | from_glue_fn=choice_to_index, 48 | to_glue_fn=index_to_choice) 49 | 50 | 51 | class WidgetCache(): 52 | 53 | def __init__(self): 54 | self.cache = {} 55 | 56 | def get_or_create(self, key, create_fn): 57 | # TODO: use weakref for object-keys 58 | if key not in self.cache.keys(): 59 | self.cache[key] = create_fn() 60 | return self.cache[key] 61 | -------------------------------------------------------------------------------- /glue_jupyter/common/state_widgets/viewer_profile.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | unfold_more 26 | 27 | 28 | normalize 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 52 | 53 | 58 | -------------------------------------------------------------------------------- /glue_jupyter/matplotlib/base.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, division, print_function 2 | 3 | from ipywidgets import HTML, VBox 4 | 5 | try: 6 | from ipympl.backend_nbagg import Canvas, FigureManager 7 | except ImportError: # Prior to June 2019 8 | from ipympl.backend_nbagg import (FigureCanvasNbAgg as Canvas, 9 | FigureManagerNbAgg as FigureManager) 10 | 11 | from matplotlib.figure import Figure 12 | 13 | from glue.viewers.matplotlib.mpl_axes import init_mpl 14 | from glue.viewers.matplotlib.state import MatplotlibDataViewerState 15 | from glue.viewers.matplotlib.viewer import MatplotlibViewerMixin 16 | 17 | from glue.utils.matplotlib import DEFER_DRAW_BACKENDS 18 | 19 | from glue_jupyter.view import IPyWidgetView 20 | 21 | __all__ = ['MatplotlibJupyterViewer'] 22 | 23 | # Register the Qt backend with defer_draw 24 | DEFER_DRAW_BACKENDS.append(Canvas) 25 | 26 | # By default, the Jupyter Matplotlib widget has a big clunky title bar, so 27 | # we apply custom CSS to remove it. 28 | REMOVE_TITLE_CSS = "" 29 | 30 | 31 | class MatplotlibJupyterViewer(MatplotlibViewerMixin, IPyWidgetView): 32 | 33 | _state_cls = MatplotlibDataViewerState 34 | 35 | large_data_size = None 36 | 37 | def __init__(self, session, parent=None, wcs=None, state=None): 38 | 39 | self.figure = Figure() 40 | self.canvas = Canvas(self.figure) 41 | self.canvas.manager = FigureManager(self.canvas, 0) 42 | self.figure, self.axes = init_mpl(self.figure, wcs=wcs) 43 | 44 | # FIXME: The following is required for now for the tools to work 45 | self.central_widget = self.figure 46 | self._axes = self.axes 47 | 48 | super(MatplotlibJupyterViewer, self).__init__(session, state=state) 49 | 50 | MatplotlibViewerMixin.setup_callbacks(self) 51 | 52 | self.css_widget = HTML(REMOVE_TITLE_CSS) 53 | 54 | self.create_layout() 55 | 56 | @property 57 | def figure_widget(self): 58 | return VBox([self.css_widget, self.canvas]) 59 | -------------------------------------------------------------------------------- /glue_jupyter/ipyvolume/scatter/tests/test_viewer.py: -------------------------------------------------------------------------------- 1 | def test_scatter3d_nd(app, data_4d): 2 | # Make sure that things work correctly with arrays that have more than 3 | # one dimension. 4 | app.add_data(data_4d) 5 | scatter = app.scatter3d(x='x', y='x', z='x', data=data_4d) 6 | scatter.state.layers[0].vector_visible = True 7 | scatter.state.layers[0].size_mode = 'Linear' 8 | scatter.state.layers[0].cmap_mode = 'Linear' 9 | 10 | 11 | def test_scatter3d_categorical(app, datacat): 12 | # Make sure that things work correctly with arrays that have categorical 13 | # components - for now these are skipped, until we figure out how to 14 | # show the correct categorical labels on the axes. 15 | app.add_data(datacat) 16 | scatter = app.scatter3d(data=datacat) 17 | assert str(scatter.state.x_att) == 'a' 18 | assert str(scatter.state.y_att) == 'b' 19 | assert str(scatter.state.z_att) == 'b' 20 | 21 | 22 | def test_non_hex_colors(app, dataxyz): 23 | 24 | # Make sure non-hex colors such as '0.4' and 'red', which are valid 25 | # matplotlib colors, work as expected. 26 | 27 | viewer = app.scatter3d(data=dataxyz) 28 | dataxyz.style.color = '0.3' 29 | dataxyz.style.color = 'indigo' 30 | 31 | app.subset('test', dataxyz.id['x'] > 1) 32 | viewer.layer_options.selected = 1 33 | dataxyz.subsets[0].style.color = '0.5' 34 | dataxyz.subsets[0].style.color = 'purple' 35 | 36 | 37 | def test_labels(app, dataxyz): 38 | # test the syncing of attributes to labels 39 | app.add_data(dataxyz) 40 | scatter = app.scatter3d(data=dataxyz) 41 | assert str(scatter.state.x_att) == 'x' 42 | assert scatter.figure.zlabel == 'x' 43 | assert str(scatter.state.y_att) == 'y' 44 | assert scatter.figure.xlabel == 'y' 45 | assert str(scatter.state.z_att) == 'z' 46 | assert scatter.figure.ylabel == 'z' 47 | 48 | scatter.state.x_att = dataxyz.id['y'] 49 | assert scatter.figure.zlabel == 'y' 50 | scatter.state.y_att = dataxyz.id['z'] 51 | assert scatter.figure.xlabel == 'z' 52 | scatter.state.z_att = dataxyz.id['x'] 53 | assert scatter.figure.ylabel == 'x' 54 | -------------------------------------------------------------------------------- /glue_jupyter/layout_widget.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | Viewer Options 25 | 26 | 27 | 28 | 29 | 30 | Layer Options 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /glue_jupyter/ipyvolume/scatter/layer_style_widget.py: -------------------------------------------------------------------------------- 1 | from ipywidgets import Checkbox, VBox, ToggleButtons 2 | from glue_jupyter.widgets import Color, Size 3 | 4 | from ...link import link, dlink 5 | from ...widgets import LinkedDropdown 6 | 7 | __all__ = ['Scatter3DLayerStateWidget'] 8 | 9 | 10 | class Scatter3DLayerStateWidget(VBox): 11 | 12 | def __init__(self, layer_state): 13 | 14 | self.state = layer_state 15 | 16 | self.widget_visible = Checkbox(description='visible', value=self.state.visible) 17 | link((self.state, 'visible'), (self.widget_visible, 'value')) 18 | 19 | self.widget_marker = ToggleButtons(options=['sphere', 'box', 'diamond', 'circle_2d']) 20 | link((self.state, 'geo'), (self.widget_marker, 'value')) 21 | 22 | self.widget_size = Size(state=self.state) 23 | self.widget_color = Color(state=self.state) 24 | 25 | # vector/quivers 26 | self.widget_vector = Checkbox(description='show vectors', value=self.state.vector_visible) 27 | 28 | self.widget_vector_x = LinkedDropdown(self.state, 'vx_att', label='vx') 29 | self.widget_vector_y = LinkedDropdown(self.state, 'vy_att', label='vy') 30 | self.widget_vector_z = LinkedDropdown(self.state, 'vz_att', label='vz') 31 | 32 | link((self.state, 'vector_visible'), (self.widget_vector, 'value')) 33 | dlink((self.widget_vector, 'value'), (self.widget_vector_x.layout, 'display'), 34 | lambda value: None if value else 'none') 35 | dlink((self.widget_vector, 'value'), (self.widget_vector_y.layout, 'display'), 36 | lambda value: None if value else 'none') 37 | dlink((self.widget_vector, 'value'), (self.widget_vector_z.layout, 'display'), 38 | lambda value: None if value else 'none') 39 | 40 | link((self.state, 'vector_visible'), (self.widget_vector, 'value')) 41 | 42 | super().__init__([self.widget_visible, self.widget_marker, 43 | self.widget_size, self.widget_color, 44 | self.widget_vector, self.widget_vector_x, 45 | self.widget_vector_y, self.widget_vector_z]) 46 | -------------------------------------------------------------------------------- /glue_jupyter/table/tests/test_table.py: -------------------------------------------------------------------------------- 1 | from glue_jupyter.table import TableViewer 2 | 3 | 4 | def test_table_filter(app, dataxyz): 5 | table = app.table(data=dataxyz) 6 | assert len(table.layers) == 1 7 | assert table.widget_table is not None 8 | table.widget_table.checked = [1] 9 | table.apply_filter() 10 | assert len(table.layers) == 2 11 | subset = table.layers[1].layer 12 | assert table.widget_table.selections == [subset.label] 13 | assert [k['text'] for k in table.widget_table.headers_selections] == [subset.label] 14 | assert table.widget_table.selection_colors == [subset.style.color] 15 | 16 | app.subset('test', dataxyz.id['x'] > 1) 17 | assert len(table.layers) == 3 18 | assert len(table.widget_table.selections) == 2 19 | 20 | 21 | def test_table_add_remove_data(app, dataxyz, dataxz, data_empty): 22 | table = app.new_data_viewer(TableViewer, data=None, show=True) 23 | assert len(table.layers) == 0 24 | assert table.widget_table.total_length == 0 25 | 26 | app.add_data(data_empty) 27 | table.add_data(data_empty) 28 | assert len(table.layers) == 1 29 | assert table.widget_table.total_length == 0 30 | table.remove_data(data_empty) 31 | 32 | table.add_data(dataxyz) 33 | assert table.widget_table.data is dataxyz 34 | assert table.widget_table.total_length == 3 35 | 36 | assert table.widget_table.items, "table should fill automatically" 37 | assert table.widget_table.items[0]['z'] == dataxyz['z'][0] 38 | assert table.widget_table.total_length, "total length should grow" 39 | 40 | assert dataxz['z'][0] != dataxyz['z'][0], "we assume this to check data changes in the table" 41 | 42 | table.add_data(dataxz) 43 | assert table.widget_table.data is dataxz 44 | assert table.widget_table.items[0]['z'] == dataxz['z'][0] 45 | assert len(table.layers) == 2 46 | 47 | table.remove_data(dataxz) 48 | assert table.widget_table.data is dataxyz 49 | assert table.widget_table.items[0]['z'] == dataxyz['z'][0] 50 | assert len(table.layers) == 1 51 | 52 | table.remove_data(dataxyz) 53 | assert table.widget_table.data is None 54 | assert table.widget_table.items == [] 55 | assert len(table.layers) == 0 56 | -------------------------------------------------------------------------------- /glue_jupyter/widgets/tests/test_linked_dropdown.py: -------------------------------------------------------------------------------- 1 | from glue.core.state_objects import State 2 | from glue.core.data_combo_helper import ComponentIDComboHelper 3 | from echo import SelectionCallbackProperty 4 | 5 | from ..linked_dropdown import LinkedDropdown 6 | 7 | 8 | class DummyState(State): 9 | """Mock state class for testing only.""" 10 | 11 | x_att = SelectionCallbackProperty(docstring='x test attribute') 12 | y_att = SelectionCallbackProperty(docstring='y test attribute', default_index=-1) 13 | 14 | 15 | def test_component(app, dataxz, dataxyz): 16 | # setup 17 | state = DummyState() 18 | helper = ComponentIDComboHelper(state, 'x_att', app.data_collection) 19 | helper.append_data(dataxz) 20 | state.helper = helper 21 | 22 | # main object we test 23 | dropdown = LinkedDropdown(state, 'x_att', 'x test attribute') 24 | 25 | # simple sanity tests 26 | assert dropdown.description == 'x test attribute' 27 | assert [item[0] for item in dropdown.options] == ['x', 'z'] 28 | 29 | # initial state 30 | assert state.x_att is dataxz.id['x'] 31 | assert dropdown.value is dataxz.id['x'] 32 | 33 | # glue state -> ui 34 | state.x_att = dataxz.id['z'] 35 | assert dropdown.value is dataxz.id['z'] 36 | 37 | # ui -> glue state 38 | dropdown.value = dataxz.id['x'] 39 | assert state.x_att is dataxz.id['x'] 40 | 41 | # same, but now be ok with strings 42 | state.x_att = 'z' 43 | assert dropdown.value is dataxz.id['z'] 44 | 45 | state.x_att = 'x' 46 | assert dropdown.value is dataxz.id['x'] 47 | 48 | 49 | def test_component_default_index(app, dataxz, dataxyz): 50 | 51 | # Regression test for a bug that caused the incorrect element to be selected 52 | # when default_index is involved. 53 | 54 | # setup 55 | state = DummyState() 56 | helper = ComponentIDComboHelper(state, 'y_att', app.data_collection) 57 | state.helper = helper 58 | dropdown = LinkedDropdown(state, 'y_att', 'y test attribute') 59 | assert [item[0] for item in dropdown.options] == [] 60 | 61 | helper.append_data(dataxz) 62 | assert [item[0] for item in dropdown.options] == ['x', 'z'] 63 | 64 | assert state.y_att is dataxz.id['z'] 65 | assert dropdown.value is dataxz.id['z'] 66 | -------------------------------------------------------------------------------- /glue_jupyter/common/state_widgets/viewer_histogram.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | unfold_more 21 | 22 | 23 | normalize 24 | 25 | 26 | 27 | 28 | trending_up 29 | 30 | 31 | cumulative 32 | 33 | 34 | 35 | Fit Bins to Axes 36 | 37 | 38 | 39 | 40 | 41 | 42 | 57 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | ci: 2 | autofix_prs: false 3 | autoupdate_schedule: 'monthly' 4 | 5 | repos: 6 | - repo: https://github.com/pre-commit/pre-commit-hooks 7 | rev: v6.0.0 8 | hooks: 9 | - id: check-added-large-files 10 | args: ["--enforce-all", "--maxkb=300"] 11 | exclude: "^(\ 12 | CHANGES.md|\ 13 | )$" 14 | # Prevent giant files from being committed. 15 | - id: check-case-conflict 16 | # Check for files with names that would conflict on a case-insensitive 17 | # filesystem like MacOS HFS+ or Windows FAT. 18 | - id: check-json 19 | # Attempts to load all json files to verify syntax. 20 | - id: check-merge-conflict 21 | # Check for files that contain merge conflict strings. 22 | - id: check-symlinks 23 | # Checks for symlinks which do not point to anything. 24 | - id: check-toml 25 | # Attempts to load all TOML files to verify syntax. 26 | - id: check-xml 27 | # Attempts to load all xml files to verify syntax. 28 | - id: check-yaml 29 | # Attempts to load all yaml files to verify syntax. 30 | exclude: ".*(.github.*)$" 31 | - id: detect-private-key 32 | # Checks for the existence of private keys. 33 | - id: end-of-file-fixer 34 | # Makes sure files end in a newline and only a newline. 35 | exclude: ".*(data.*|extern.*|icons.*|licenses.*|_static.*|_parsetab.py)$" 36 | # - id: fix-encoding-pragma # covered by pyupgrade 37 | - id: trailing-whitespace 38 | # Trims trailing whitespace. 39 | exclude_types: [python] # Covered by Ruff W291. 40 | exclude: ".*(CHANGES.md|extern.*|licenses.*|_static.*)$" 41 | 42 | - repo: https://github.com/pre-commit/pygrep-hooks 43 | rev: v1.10.0 44 | hooks: 45 | - id: rst-directive-colons 46 | # Detect mistake of rst directive not ending with double colon. 47 | - id: rst-inline-touching-normal 48 | # Detect mistake of inline code touching normal text in rst. 49 | - id: text-unicode-replacement-char 50 | # Forbid files which have a UTF-8 Unicode replacement character. 51 | 52 | - repo: https://github.com/astral-sh/ruff-pre-commit 53 | rev: "v0.14.7" 54 | hooks: 55 | - id: ruff 56 | args: ["--fix", "--show-fixes"] 57 | -------------------------------------------------------------------------------- /glue_jupyter/bqplot/histogram/viewer.py: -------------------------------------------------------------------------------- 1 | from glue.core.subset import roi_to_subset_state 2 | from glue.core.roi import RangeROI 3 | from glue.viewers.histogram.state import HistogramViewerState 4 | 5 | from ..common.viewer import BqplotBaseView 6 | 7 | from .layer_artist import BqplotHistogramLayerArtist 8 | from glue_jupyter.common.state_widgets.layer_histogram import HistogramLayerStateWidget 9 | from glue_jupyter.common.state_widgets.viewer_histogram import HistogramViewerStateWidget 10 | from glue_jupyter.registries import viewer_registry 11 | 12 | __all__ = ['BqplotHistogramView'] 13 | 14 | 15 | @viewer_registry("histogram") 16 | class BqplotHistogramView(BqplotBaseView): 17 | 18 | allow_duplicate_data = False 19 | allow_duplicate_subset = False 20 | is2d = False 21 | 22 | _state_cls = HistogramViewerState 23 | _options_cls = HistogramViewerStateWidget 24 | _data_artist_cls = BqplotHistogramLayerArtist 25 | _subset_artist_cls = BqplotHistogramLayerArtist 26 | _layer_style_widget_cls = HistogramLayerStateWidget 27 | 28 | tools = ['bqplot:home', 'bqplot:panzoom', 'bqplot:xrange'] 29 | 30 | def __init__(self, *args, **kwargs): 31 | super().__init__(*args, **kwargs) 32 | self.state.add_callback('x_att', self._update_axes) 33 | self.state.add_callback('x_log', self._update_axes) 34 | self.state.add_callback('normalize', self._update_axes) 35 | self._update_axes() 36 | 37 | def _update_axes(self, *args): 38 | 39 | if self.state.x_att is not None: 40 | self.state.x_axislabel = str(self.state.x_att) 41 | 42 | if self.state.normalize: 43 | self.state.y_axislabel = 'Normalized number' 44 | else: 45 | self.state.y_axislabel = 'Number' 46 | 47 | def _roi_to_subset_state(self, roi): 48 | # TODO: copy paste from glue/viewers/histogram/qt/data_viewer.py 49 | # TODO Does subset get applied to all data or just visible data? 50 | 51 | bins = self.state.bins 52 | 53 | x = roi.to_polygon()[0] 54 | lo, hi = min(x), max(x) 55 | 56 | if lo >= bins.min(): 57 | lo = bins[bins <= lo].max() 58 | if hi <= bins.max(): 59 | hi = bins[bins >= hi].min() 60 | 61 | roi_new = RangeROI(min=lo, max=hi, orientation='x') 62 | 63 | return roi_to_subset_state(roi_new, x_att=self.state.x_att) 64 | -------------------------------------------------------------------------------- /glue_jupyter/tests/test_viewer_registry.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from glue_jupyter.registries import viewer_registry 3 | from glue.viewers.common.viewer import Viewer 4 | import glue_jupyter as gj 5 | 6 | from .data_viewer_test import ExternalViewerTest # noqa 7 | 8 | 9 | def test_add_client(): 10 | @viewer_registry("test") 11 | class ClientTest(object): 12 | pass 13 | 14 | assert viewer_registry.members['test']['cls'] == ClientTest 15 | 16 | 17 | def test_access_missing_malformed_viewer(): 18 | app = gj.jglue() 19 | with pytest.raises(ValueError, match='No registered viewer found with name missing'): 20 | app.new_data_viewer('missing', data=None, show=False) 21 | 22 | @viewer_registry("malformed") 23 | class MalformedViewer(Viewer): 24 | pass 25 | 26 | del viewer_registry.members['malformed']['cls'] 27 | 28 | with pytest.raises(ValueError, match='Registry does not define a Viewer class for malformed'): 29 | app.new_data_viewer('malformed', data=None, show=False) 30 | 31 | 32 | def test_adding_viewers(): 33 | 34 | @viewer_registry("test2") 35 | class ViewerTest(Viewer): 36 | pass 37 | 38 | app = gj.jglue() 39 | from glue_jupyter.table import TableViewer # noqa 40 | viewer_cls = TableViewer 41 | s1 = app.new_data_viewer(viewer_cls, data=None) 42 | assert len(app.viewers) == 1 43 | assert app.viewers[0] is s1 44 | 45 | s2 = app.new_data_viewer('test2', data=None, show=False) 46 | assert len(app.viewers) == 2 47 | assert app.viewers[1] is s2 48 | assert isinstance(s2, ViewerTest) 49 | 50 | 51 | def test_external_viewer(): 52 | app = gj.jglue() 53 | s = app.new_data_viewer('externalviewer', data=None, show=False) 54 | assert len(app.viewers) == 1 55 | assert app.viewers[0] is s 56 | 57 | 58 | def test_builtin_table_viewer(app, dataxyz): 59 | from glue_jupyter.table import TableViewer # noqa 60 | 61 | s = app.new_data_viewer('table', data=dataxyz) 62 | assert len(app.viewers) == 1 63 | assert app.viewers[0] is s 64 | assert len(s.layers) == 1 65 | assert s.widget_table is not None 66 | 67 | 68 | def test_builtin_scatter_viewer(app, dataxyz): 69 | from glue_jupyter.bqplot.scatter import BqplotScatterView # noqa 70 | 71 | s = app.new_data_viewer('scatter', data=dataxyz) 72 | assert len(app.viewers) == 1 73 | assert app.viewers[0] is s 74 | -------------------------------------------------------------------------------- /glue_jupyter/widgets/size.py: -------------------------------------------------------------------------------- 1 | import ipywidgets as widgets 2 | 3 | from ..link import link, dlink 4 | from .linked_dropdown import LinkedDropdown 5 | 6 | 7 | class Size(widgets.VBox): 8 | 9 | def __init__(self, state, **kwargs): 10 | super(Size, self).__init__(**kwargs) 11 | self.state = state 12 | 13 | self.widget_size = widgets.FloatSlider(description='size', min=0, max=10, 14 | value=self.state.size) 15 | link((self.state, 'size'), (self.widget_size, 'value')) 16 | self.widget_scaling = widgets.FloatSlider(description='scale', min=0, max=2, 17 | value=self.state.size_scaling) 18 | link((self.state, 'size_scaling'), (self.widget_scaling, 'value')) 19 | 20 | options = type(self.state).size_mode.get_choice_labels(self.state) 21 | self.widget_size_mode = widgets.RadioButtons(options=options, description='size mode') 22 | link((self.state, 'size_mode'), (self.widget_size_mode, 'value')) 23 | 24 | self.widget_size_att = LinkedDropdown(self.state, 'size_att', 25 | ui_name='size attribute', 26 | label='size attribute') 27 | 28 | self.widget_size_vmin = widgets.FloatText(description='size min') 29 | self.widget_size_vmax = widgets.FloatText(description='size min') 30 | self.widget_size_v = widgets.VBox([self.widget_size_vmin, self.widget_size_vmax]) 31 | link((self.state, 'size_vmin'), (self.widget_size_vmin, 'value'), lambda value: value or 0) 32 | link((self.state, 'size_vmax'), (self.widget_size_vmax, 'value'), lambda value: value or 1) 33 | 34 | dlink((self.widget_size_mode, 'value'), (self.widget_size.layout, 'display'), 35 | lambda value: None if value == options[0] else 'none') 36 | dlink((self.widget_size_mode, 'value'), (self.widget_size_att.layout, 'display'), 37 | lambda value: None if value == options[1] else 'none') 38 | dlink((self.widget_size_mode, 'value'), (self.widget_size_v.layout, 'display'), 39 | lambda value: None if value == options[1] else 'none') 40 | 41 | self.children = (self.widget_size_mode, self.widget_size, 42 | self.widget_scaling, self.widget_size_att, 43 | self.widget_size_v) 44 | -------------------------------------------------------------------------------- /glue_jupyter/widgets/glue_float_field.vue: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 57 | -------------------------------------------------------------------------------- /glue_jupyter/common/state_widgets/viewer_image.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | equal aspect ratio 12 | 13 | 14 | 15 | show axes 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | {{ slider.label }}: {{ glue_state.slices[slider.index] }} ({{ slider.world_value }} {{ slider.unit }}) 27 | 30 | 31 | 32 | 33 | 46 | 61 | -------------------------------------------------------------------------------- /glue_jupyter/bqplot/histogram/tests/test_viewer.py: -------------------------------------------------------------------------------- 1 | from itertools import permutations, product 2 | from glue.core.subset import SubsetState 3 | 4 | 5 | def test_non_hex_colors(app, dataxyz): 6 | 7 | # Make sure non-hex colors such as '0.4' and 'red', which are valid 8 | # matplotlib colors, work as expected. 9 | 10 | viewer = app.histogram1d(data=dataxyz) 11 | dataxyz.style.color = '0.3' 12 | dataxyz.style.color = 'indigo' 13 | 14 | app.subset('test', dataxyz.id['x'] > 1) 15 | viewer.layer_options.selected = 1 16 | dataxyz.subsets[0].style.color = '0.5' 17 | dataxyz.subsets[0].style.color = 'purple' 18 | 19 | 20 | def test_remove_from_viewer(app, dataxz, dataxyz): 21 | s = app.histogram1d(data=dataxyz) 22 | s.add_data(dataxz) 23 | app.data_collection.new_subset_group(subset_state=dataxz.id['x'] > 1, label='test') 24 | assert len(s.figure.marks) == 4 25 | s.remove_data(dataxyz) 26 | assert len(s.figure.marks) == 2 27 | s.remove_data(dataxz) 28 | assert len(s.figure.marks) == 0 29 | 30 | 31 | def test_remove_from_data_collection(app, dataxz, dataxyz): 32 | s = app.histogram1d(data=dataxyz) 33 | s.add_data(dataxz) 34 | app.data_collection.new_subset_group(subset_state=dataxz.id['x'] > 1, label='test') 35 | assert len(s.figure.marks) == 4 36 | s.state.hist_n_bin = 30 37 | app.data_collection.remove(dataxyz) 38 | assert len(s.figure.marks) == 2 39 | s.state.hist_n_bin = 20 40 | app.data_collection.remove(dataxz) 41 | assert len(s.figure.marks) == 0 42 | s.state.hist_n_bin = 10 43 | 44 | 45 | def test_redraw_empty_subset(app, dataxz): 46 | s = app.histogram1d(data=dataxz) 47 | s.add_data(dataxz) 48 | app.data_collection.new_subset_group(subset_state=dataxz.id['x'] > 1, label='empty_test') 49 | layer_artist = s.layers[-1] 50 | subset = layer_artist.layer 51 | subset.subset_state = SubsetState() 52 | 53 | # Test each combination of cumulative, normalize, and y_log 54 | for flags in product([True, False], repeat=3): 55 | s.state.cumulative, s.state.normalize, s.state.y_log = flags 56 | assert all(layer_artist.bars.y == 0) 57 | 58 | 59 | def test_zorder(app, data_volume, dataxz, dataxyz): 60 | s = app.histogram1d(data=dataxyz) 61 | s.add_data(dataxz) 62 | s.add_data(data_volume) 63 | xyz, xz, vol = s.layers 64 | 65 | for p in permutations([1, 2, 3]): 66 | vol.state.zorder, xz.state.zorder, xyz.state.zorder = p 67 | it = iter(s.figure.marks) 68 | assert all(layer.bars in it for layer in s.layers) 69 | -------------------------------------------------------------------------------- /glue_jupyter/widgets/color.py: -------------------------------------------------------------------------------- 1 | import ipywidgets as widgets 2 | 3 | from glue.config import colormaps 4 | from glue.utils import color2hex 5 | 6 | from ..link import link, dlink 7 | from .linked_dropdown import LinkedDropdown 8 | 9 | 10 | class Color(widgets.VBox): 11 | 12 | def __init__(self, state, **kwargs): 13 | super(Color, self).__init__(**kwargs) 14 | self.state = state 15 | 16 | self.widget_color = widgets.ColorPicker(description='color') 17 | link((self.state, 'color'), (self.widget_color, 'value'), color2hex) 18 | 19 | cmap_mode_options = type(self.state).cmap_mode.get_choice_labels(self.state) 20 | self.widget_cmap_mode = widgets.RadioButtons(options=cmap_mode_options, 21 | description='cmap mode') 22 | link((self.state, 'cmap_mode'), (self.widget_cmap_mode, 'value')) 23 | 24 | self.widget_cmap_att = LinkedDropdown(self.state, 'cmap_att', 25 | ui_name='color attribute', 26 | label='color attribute') 27 | 28 | self.widget_cmap_vmin = widgets.FloatText(description='color min') 29 | self.widget_cmap_vmax = widgets.FloatText(description='color max') 30 | self.widget_cmap_v = widgets.VBox([self.widget_cmap_vmin, self.widget_cmap_vmax]) 31 | link((self.state, 'cmap_vmin'), (self.widget_cmap_vmin, 'value'), lambda value: value or 0) 32 | link((self.state, 'cmap_vmax'), (self.widget_cmap_vmax, 'value'), lambda value: value or 1) 33 | 34 | self.widget_cmap = widgets.Dropdown(options=colormaps, description='colormap') 35 | link((self.state, 'cmap'), (self.widget_cmap, 'label'), 36 | lambda cmap: colormaps.name_from_cmap(cmap), lambda name: colormaps[name]) 37 | 38 | dlink((self.widget_cmap_mode, 'value'), (self.widget_color.layout, 'display'), 39 | lambda value: None if value == cmap_mode_options[0] else 'none') 40 | dlink((self.widget_cmap_mode, 'value'), (self.widget_cmap.layout, 'display'), 41 | lambda value: None if value == cmap_mode_options[1] else 'none') 42 | dlink((self.widget_cmap_mode, 'value'), (self.widget_cmap_att.layout, 'display'), 43 | lambda value: None if value == cmap_mode_options[1] else 'none') 44 | dlink((self.widget_cmap_mode, 'value'), (self.widget_cmap_v.layout, 'display'), 45 | lambda value: None if value == cmap_mode_options[1] else 'none') 46 | self.children = (self.widget_cmap_mode, self.widget_color, 47 | self.widget_cmap_att, self.widget_cmap_v, 48 | self.widget_cmap) 49 | -------------------------------------------------------------------------------- /glue_jupyter/widgets/temp.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | add 9 | No selection (create new) 10 | 11 | 12 | 13 | 14 | signal_cellular_4_bar 15 | 16 | {{ available[index].label }} 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 29 | 30 | add 31 | 32 | 33 | {{ no_selection_text }} 34 | 35 | 36 | 37 | 42 | 43 | signal_cellular_4_bar 44 | 45 | 46 | 47 | {{ item.label }} 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /glue_jupyter/widgets/subset_select.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | add 8 | {{ no_selection_text }} 9 | 10 | 11 | 12 | 13 | 14 | signal_cellular_4_bar 15 | 16 | {{ (selected.length <= nr_of_full_names) ? subset.label : '' }} 17 | 18 | 19 | {{ subset.label }} 20 | 21 | 22 | 23 | 24 | 25 | 28 | 29 | add 30 | 31 | 32 | {{ no_selection_text }} 33 | 34 | 35 | 36 | 41 | 42 | signal_cellular_4_bar 43 | 44 | 45 | 46 | {{ subset.label }} 47 | 48 | mdi-delete 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /.github/workflows/ci_workflows.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | tags: 8 | - '*' 9 | pull_request: 10 | 11 | jobs: 12 | initial_checks: 13 | # Mandatory checks before CI tests 14 | uses: OpenAstronomy/github-actions-workflows/.github/workflows/tox.yml@v1 15 | with: 16 | coverage: false 17 | envs: | 18 | # Code style 19 | - linux: codestyle 20 | 21 | tests: 22 | needs: initial_checks 23 | uses: OpenAstronomy/github-actions-workflows/.github/workflows/tox.yml@v1 24 | with: 25 | coverage: codecov 26 | libraries: | 27 | apt: 28 | - libhdf5-dev 29 | 30 | envs: | 31 | - linux: py310-test 32 | - linux: py311-test 33 | - linux: py312-test 34 | - linux: py313-test 35 | 36 | - macos: py310-test 37 | - macos: py311-test 38 | - macos: py313-test 39 | - macos: py314-test 40 | 41 | - windows: py310-test 42 | - windows: py311-test 43 | - windows: py312-test 44 | - windows: py314-test 45 | 46 | # test against some dev versions of some packages 47 | - linux: py314-test-dev 48 | - macos: py312-test-dev 49 | - windows: py313-test-dev 50 | 51 | no_coverage: 52 | needs: initial_checks 53 | uses: OpenAstronomy/github-actions-workflows/.github/workflows/tox.yml@v1 54 | with: 55 | coverage: false 56 | pytest: false 57 | 58 | envs: | 59 | - windows: py310-notebooks 60 | - macos: py314-notebooks 61 | - linux: py313-notebooks 62 | 63 | - linux: py314-docs 64 | - macos: py310-docs 65 | - windows: py312-docs 66 | 67 | ui-tests: 68 | runs-on: ubuntu-latest 69 | steps: 70 | - uses: actions/checkout@v3 71 | - name: Install Python 72 | uses: actions/setup-python@v2 73 | with: 74 | python-version: "3.11" 75 | - name: Install 76 | run: pip install . "pytest-ipywidgets[all]" "pytest-playwright==0.5.2" "jupyterlab<4" 77 | - name: Install playwright 78 | run: | 79 | playwright install chromium 80 | - name: Run visual regression tests 81 | run: | 82 | pytest tests/ui 83 | - name: Upload UI Test artifacts 84 | if: always() 85 | uses: actions/upload-artifact@v4 86 | with: 87 | name: glue-jupyter-ui-test-output 88 | path: | 89 | test-results 90 | 91 | publish: 92 | needs: tests 93 | uses: OpenAstronomy/github-actions-workflows/.github/workflows/publish_pure_python.yml@v1 94 | with: 95 | libraries: libhdf5-dev 96 | test_extras: test 97 | test_command: pytest --pyargs glue_jupyter 98 | secrets: 99 | pypi_token: ${{ secrets.pypi_token }} 100 | -------------------------------------------------------------------------------- /notebooks/Astronomy/L1448/L1448 in 3D.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "## Exploring the L1448 data in 3D\n", 8 | "\n", 9 | "L1448 is a region within the [Perseus Molecular Cloud](https://en.wikipedia.org/wiki/Perseus_molecular_cloud), a star-formation region located at a distance of 600ly from the Sun. This notebook takes a look at a *spectral cube*, a 3D dataset where two dimensions are the spatial positions on the sky, and the third dimension is e.g. wavelength.\n", 10 | "\n", 11 | "### About the data\n", 12 | "\n", 13 | "The data is a 13CO spectral cube of the L1448 region. The data can be found in https://github.com/glue-viz/glue-example-data/tree/master/Astronomy/L1448/. For convenience we can use the ``require_data`` function to\n", 14 | "automatically download it here:" 15 | ] 16 | }, 17 | { 18 | "cell_type": "code", 19 | "execution_count": null, 20 | "metadata": {}, 21 | "outputs": [], 22 | "source": [ 23 | "from glue_jupyter.data import require_data\n", 24 | "require_data('Astronomy/L1448/l1448_13co.fits')" 25 | ] 26 | }, 27 | { 28 | "cell_type": "markdown", 29 | "metadata": {}, 30 | "source": [ 31 | "### Starting up the glue Jupyter application" 32 | ] 33 | }, 34 | { 35 | "cell_type": "markdown", 36 | "metadata": {}, 37 | "source": [ 38 | "Let's start up glue and load in the main data file:" 39 | ] 40 | }, 41 | { 42 | "cell_type": "code", 43 | "execution_count": null, 44 | "metadata": {}, 45 | "outputs": [], 46 | "source": [ 47 | "import glue_jupyter as gj\n", 48 | "app = gj.jglue()" 49 | ] 50 | }, 51 | { 52 | "cell_type": "code", 53 | "execution_count": null, 54 | "metadata": {}, 55 | "outputs": [], 56 | "source": [ 57 | "cube = app.load_data('l1448_13co.fits')" 58 | ] 59 | }, 60 | { 61 | "cell_type": "markdown", 62 | "metadata": {}, 63 | "source": [ 64 | "### 3D volume rendering viewer\n", 65 | "\n", 66 | "We can start off by taking a look at the data in the volume viewer:" 67 | ] 68 | }, 69 | { 70 | "cell_type": "code", 71 | "execution_count": null, 72 | "metadata": { 73 | "scrolled": false 74 | }, 75 | "outputs": [], 76 | "source": [ 77 | "app.volshow(data=cube)" 78 | ] 79 | } 80 | ], 81 | "metadata": { 82 | "kernelspec": { 83 | "display_name": "Python 3", 84 | "language": "python", 85 | "name": "python3" 86 | }, 87 | "language_info": { 88 | "codemirror_mode": { 89 | "name": "ipython", 90 | "version": 3 91 | }, 92 | "file_extension": ".py", 93 | "mimetype": "text/x-python", 94 | "name": "python", 95 | "nbconvert_exporter": "python", 96 | "pygments_lexer": "ipython3", 97 | "version": "3.7.1" 98 | } 99 | }, 100 | "nbformat": 4, 101 | "nbformat_minor": 2 102 | } 103 | -------------------------------------------------------------------------------- /glue_jupyter/widgets/layeroptions.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 12 | 13 | 14 | 15 | 16 | 20 | 21 | 22 | 23 | mdi-close 24 | 25 | 26 | 27 | 28 | 29 | mdi-eye{{ data.item.visible ? '' : '-off' }} 30 | 31 | {{ data.item.label }} 32 | 33 | 34 | 35 | 36 | 39 | 40 | mdi-eye{{ data.item.visible ? '' : '-off' }} 41 | 42 | {{ data.item.label }} 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 71 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | Jupyter interface for Glue 2 | ========================== 3 | 4 | Sometimes known as “Glupyter” 5 | 6 | |Build Status| |Coverage Status| |Documentation Status| 7 | 8 | About 9 | ~~~~~ 10 | 11 | `Glue `__ is a Python library to explore 12 | relationships within and among datasets. The main interface until now 13 | has been based on `Qt `__, but the **glue-jupyter** 14 | package aims to provide a way to use Glue in Jupyter notebooks and 15 | Jupyter lab instead. This is currently a work in progress and highly 16 | experimental. 17 | 18 | For some notebooks with examples of usage of glue-jupyter, see the 19 | ``notebooks`` directory. 20 | 21 | You can try out glue-jupyter online at mybinder: 22 | 23 | |Binder| 24 | 25 | Notebooks with real data: 26 | 27 | - `Investigating star formation in the W5 28 | region `__ 29 | (example with linking a table and an image) 30 | - `Exploring the L1448 data in 31 | 3D `__ 32 | (example of 3D volume rendering) 33 | - `Visualizing flight paths in the Boston 34 | area `__ 35 | (example with a single tabular dataset) 36 | - `Distance to the Pleiades with GAIA 37 | data `__ 38 | 39 | Installing 40 | ~~~~~~~~~~ 41 | 42 | For now, installing should be done using pip:: 43 | 44 | pip install git+https://github.com/glue-viz/glue-jupyter.git 45 | 46 | Or from source:: 47 | 48 | git clone https://github.com/glue-viz/glue-jupyter.git 49 | cd glue-jupyter 50 | pip install -e . 51 | 52 | Testing 53 | ~~~~~~~ 54 | 55 | The test suite can be run using:: 56 | 57 | pytest glue_jupyter 58 | 59 | .. |Build Status| image:: https://github.com/glue-viz/glue-jupyter/actions/workflows/ci_workflows.yml/badge.svg 60 | :target: https://github.com/glue-viz/glue-jupyter/actions/ 61 | :alt: Glue Jupyter's GitHub Actions CI Status 62 | .. |Coverage Status| image:: https://codecov.io/gh/glue-viz/glue-jupyter/branch/master/graph/badge.svg 63 | :target: https://codecov.io/gh/glue-viz/glue-jupyter 64 | :alt: Glue Jupyter's Coverage Status 65 | .. |Documentation Status| image:: https://img.shields.io/readthedocs/glue-jupyter/latest.svg?logo=read%20the%20docs&logoColor=white&label=Docs&version=stable 66 | :target: https://glue-jupyter.readthedocs.io/en/stable/?badge=stable 67 | :alt: Glue Jupyter's Documentation Status 68 | .. |Binder| image:: https://mybinder.org/badge_logo.svg 69 | :target: https://mybinder.org/v2/gh/glue-viz/glue-jupyter/main?urlpath=lab/tree/notebooks 70 | :alt: Launch Glue Jupyter on mybinder 71 | -------------------------------------------------------------------------------- /glue_jupyter/bqplot/profile/viewer.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | from glue.core.units import UnitConverter 4 | from glue.core.subset import roi_to_subset_state 5 | from glue.core.roi import RangeROI 6 | from glue.viewers.profile.state import ProfileViewerState 7 | 8 | from ..common.viewer import BqplotBaseView 9 | 10 | from .layer_artist import BqplotProfileLayerArtist 11 | 12 | from glue_jupyter.common.state_widgets.layer_profile import ProfileLayerStateWidget 13 | from glue_jupyter.common.state_widgets.viewer_profile import ProfileViewerStateWidget 14 | from glue_jupyter.registries import viewer_registry 15 | 16 | __all__ = ['BqplotProfileView'] 17 | 18 | 19 | @viewer_registry("profile") 20 | class BqplotProfileView(BqplotBaseView): 21 | 22 | allow_duplicate_data = False 23 | allow_duplicate_subset = False 24 | is2d = False 25 | 26 | _state_cls = ProfileViewerState 27 | _options_cls = ProfileViewerStateWidget 28 | _data_artist_cls = BqplotProfileLayerArtist 29 | _subset_artist_cls = BqplotProfileLayerArtist 30 | _layer_style_widget_cls = ProfileLayerStateWidget 31 | 32 | tools = ['bqplot:home', 'bqplot:panzoom', 'bqplot:panzoom_x', 'bqplot:panzoom_y', 33 | 'bqplot:xrange', 'bqplot:yrange'] 34 | 35 | def __init__(self, *args, **kwargs): 36 | super().__init__(*args, **kwargs) 37 | self.state.add_callback('x_att', self._update_axes) 38 | self.state.add_callback('normalize', self._update_axes) 39 | self.state.add_callback('x_display_unit', self._update_axes) 40 | self.state.add_callback('y_display_unit', self._update_axes) 41 | self._update_axes() 42 | 43 | def _update_axes(self, *args): 44 | 45 | if self.state.x_att is not None: 46 | if self.state.x_display_unit: 47 | self.state.x_axislabel = str(self.state.x_att) + f' [{self.state.x_display_unit}]' 48 | else: 49 | self.state.x_axislabel = str(self.state.x_att) 50 | 51 | if self.state.normalize: 52 | self.state.y_axislabel = 'Normalized data values' 53 | else: 54 | if self.state.y_display_unit: 55 | self.state.y_axislabel = f'Data values [{self.state.y_display_unit}]' 56 | else: 57 | self.state.y_axislabel = 'Data values' 58 | 59 | def _roi_to_subset_state(self, roi): 60 | 61 | x = roi.to_polygon()[0] 62 | lo, hi = min(x), max(x) 63 | 64 | # Apply inverse unit conversion, converting from display to native units 65 | converter = UnitConverter() 66 | lo, hi = converter.to_native(self.state.reference_data, 67 | self.state.x_att, np.array([lo, hi]), 68 | self.state.x_display_unit) 69 | 70 | # Sometimes unit conversions can cause the min/max to be swapped 71 | if lo > hi: 72 | lo, hi = hi, lo 73 | 74 | roi_new = RangeROI(min=lo, max=hi, orientation='x') 75 | 76 | return roi_to_subset_state(roi_new, x_att=self.state.x_att) 77 | -------------------------------------------------------------------------------- /glue_jupyter/widgets/subset_mode_vuetify.py: -------------------------------------------------------------------------------- 1 | from contextlib import nullcontext 2 | 3 | import glue.core.message as msg 4 | import ipyvuetify as v 5 | import ipywidgets as widgets 6 | from glue.core.edit_subset_mode import AndMode, AndNotMode, OrMode, ReplaceMode, XorMode 7 | from glue.core.hub import HubListener 8 | from glue.icons import icon_path 9 | from glue.utils.decorators import avoid_circular 10 | 11 | __all__ = ['SelectionModeMenu'] 12 | 13 | ICON_WIDTH = 20 14 | 15 | MODES = [ 16 | ('replace', 'glue_replace', ReplaceMode), 17 | ('add', 'glue_or', OrMode), 18 | ('and', 'glue_and', AndMode), 19 | ('xor', 'glue_xor', XorMode), 20 | ('remove', 'glue_andnot', AndNotMode), 21 | ] 22 | 23 | 24 | class SelectionModeMenu(v.Menu, HubListener): 25 | 26 | def __init__(self, session=None, output_widget=None): 27 | 28 | self.output = output_widget 29 | self.session = session 30 | 31 | self.modes = [] 32 | items = [] 33 | 34 | for name, icon_name, mode in MODES: 35 | icon = widgets.Image.from_file(icon_path(icon_name, icon_format="svg"), 36 | width=ICON_WIDTH) 37 | self.modes.append((name, icon, mode)) 38 | 39 | item = v.ListItem(children=[v.ListItemAction(children=[icon]), 40 | v.ListItemTitle(children=[name])]) 41 | item.on_event('click', self._sync_state_from_ui) 42 | items.append(item) 43 | 44 | mylist = v.List(children=items) 45 | 46 | self.main = v.Btn(icon=True, 47 | children=[self.modes[0][1]], v_on="menu.on") 48 | 49 | super().__init__( 50 | v_slots=[{ 51 | 'name': 'activator', 52 | 'variable': 'menu', 53 | 'children': self.main 54 | }], 55 | children=[mylist]) 56 | 57 | self.session.hub.subscribe(self, msg.EditSubsetMessage, 58 | handler=self._on_edit_subset_msg) 59 | 60 | self._sync_ui_from_state(self.session.edit_subset_mode.mode) 61 | 62 | @avoid_circular 63 | def _sync_state_from_ui(self, widget, event, data): 64 | with self.output or nullcontext(): 65 | icon = widget.children[0].children[0] 66 | self.main.children = [icon] 67 | for mode in self.modes: 68 | if icon is mode[1]: 69 | break 70 | else: 71 | mode = self.modes[0] 72 | self.session.edit_subset_mode.mode = mode[2] 73 | 74 | def _on_edit_subset_msg(self, msg): 75 | self._sync_ui_from_state(msg.mode) 76 | 77 | @avoid_circular 78 | def _sync_ui_from_state(self, mode): 79 | with self.output or nullcontext(): 80 | if self.session.edit_subset_mode.mode != mode: 81 | self.session.edit_subset_mode.mode = mode 82 | for m in self.modes: 83 | if mode is m[2]: 84 | icon = m[1] 85 | break 86 | else: 87 | icon = self.modes[0][1] 88 | self.main.children = [icon] 89 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | 3 | # -- Project information ----------------------------------------------------- 4 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information 5 | 6 | project = "glue-jupyter" 7 | copyright = "2018-2023, Maarten A. Breddels and Thomas Robitaille" 8 | author = "Maarten A. Breddels and Thomas Robitaille" 9 | 10 | # -- General configuration --------------------------------------------------- 11 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration 12 | 13 | extensions = ['sphinx.ext.autodoc', 14 | 'sphinx.ext.intersphinx', 15 | 'sphinx.ext.todo', 16 | 'sphinx.ext.mathjax', 17 | 'sphinx.ext.viewcode', 18 | 'nbsphinx', 19 | 'numpydoc', 20 | 'sphinx_automodapi.automodapi', 21 | 'sphinx_automodapi.smart_resolver'] 22 | 23 | templates_path = ["_templates"] 24 | exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] 25 | 26 | autoclass_content = "both" 27 | 28 | default_role = 'obj' 29 | nitpicky = True 30 | nitpick_ignore = [('py:class', 'ipywidgets.widgets.widget_box.Box'), 31 | ('py:class', 'ipywidgets.widgets.widget_box.VBox'), 32 | ('py:class', 'ipywidgets.widgets.widget.Widget'), 33 | ('py:class', 'ipywidgets.widgets.widget.LoggingHasTraits'), 34 | ('py:class', 'ipywidgets.widgets.domwidget.DOMWidget'), 35 | ('py:class', 'ipywidgets.widgets.widget_core.CoreWidget'), 36 | ('py:class', 'ipyvuetify.VuetifyTemplate.VuetifyTemplate'), 37 | ('py:class', 'traitlets.traitlets.HasTraits'), 38 | ('py:class', 'traitlets.traitlets.HasDescriptors'), 39 | ('py:class', 'echo.core.HasCallbackProperties'), 40 | ('py:class', 'glue.viewers.image.layer_artist.ImageLayerArtist'), 41 | ('py:class', 'glue.viewers.image.layer_artist.BaseImageLayerArtist'), 42 | ('py:class', 'glue_vispy_viewers.volume.layer_state.VolumeLayerState'), 43 | ('py:class', 'glue_vispy_viewers.common.layer_state.VispyLayerState')] 44 | 45 | automodapi_inheritance_diagram = False 46 | 47 | viewcode_follow_imported_members = False 48 | 49 | numpydoc_show_class_members = False 50 | autosummary_generate = True 51 | automodapi_toctreedirnm = "api" 52 | 53 | intersphinx_mapping = { 54 | 'python': ('https://docs.python.org/3.11', None), 55 | 'echo': ('https://echo.readthedocs.io/en/latest/', None), 56 | 'ipywidgets': ('https://ipywidgets.readthedocs.io/en/stable/', None), 57 | 'traitlets': ('https://traitlets.readthedocs.io/en/stable/', None), 58 | 'glue': ('https://glue-core.readthedocs.io/en/latest/', None), 59 | 'glueviz': ('https://docs.glueviz.org/en/latest/', None), 60 | } 61 | 62 | # -- Options for HTML output ------------------------------------------------- 63 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output 64 | 65 | html_theme = "sphinx_book_theme" 66 | html_theme_options = {'navigation_with_keys': False} 67 | -------------------------------------------------------------------------------- /glue_jupyter/widgets/subset_select_test.py: -------------------------------------------------------------------------------- 1 | def test_subset_select(app, datax, dataxyz, dataxz): 2 | subset_select = app.widget_subset_select 3 | 4 | assert len(subset_select.available) == 0 5 | assert len(app.data_collection.subset_groups) == 0 6 | assert subset_select.selected == [] 7 | 8 | # if there are no subsets, you cannot select multiple 9 | assert subset_select.available == [] 10 | assert not subset_select.multiple 11 | assert subset_select.selected == [] 12 | 13 | # now we make a selection 14 | app.subset_lasso2d(dataxyz.id['x'], dataxyz.id['y'], [0.5, 2.5, 2.5, 0.5], [1, 1, 3.5, 3.5]) 15 | 16 | assert len(subset_select.available) == 1 17 | assert subset_select.available[0]['label'] == 'Subset 1' 18 | 19 | assert subset_select.selected[0] == 0 20 | 21 | app.session.edit_subset_mode.edit_subset = [app.data_collection.subset_groups[0]] 22 | assert subset_select.selected[0] == 0 23 | 24 | # glue -> ui 25 | # we select no subsets, should go back to new subset 26 | app.session.edit_subset_mode.edit_subset = [] 27 | assert subset_select.selected == [] 28 | 29 | # ui -> glue 30 | subset_select.selected = [0] 31 | assert app.session.edit_subset_mode.edit_subset == [app.data_collection.subset_groups[0]] 32 | 33 | # glue -> ui (reset again) 34 | app.session.edit_subset_mode.edit_subset = [] 35 | assert subset_select.selected == [] 36 | assert dataxyz.subsets[0]['x'].tolist() == [1, 2] 37 | assert dataxyz.subsets[0]['y'].tolist() == [2, 3] 38 | assert dataxyz.subsets[0]['z'].tolist() == [5, 6] 39 | 40 | # now do a second selection 41 | app.session.edit_subset_mode.edit_subset = [app.data_collection.subset_groups[0]] 42 | assert subset_select.selected == [0] 43 | 44 | app.session.edit_subset_mode.edit_subset = [] 45 | assert subset_select.selected == [] 46 | 47 | app.subset_lasso2d(dataxyz.id['x'], dataxyz.id['y'], [0.5, 2.5, 2.5, 0.5], [1, 1, 3.5, 3.5]) 48 | assert len(subset_select.available) == 2 49 | assert subset_select.available[1]['label'] == 'Subset 2' 50 | assert subset_select.selected == [1] 51 | 52 | # we do not have multiple subsets enabled 53 | subset_select.selected = [0] 54 | 55 | # do multiple 56 | subset_select.multiple = True 57 | # now nothing should have changed in the selected subsets 58 | subset_select.selected = [0, 1] 59 | assert len(app.session.edit_subset_mode.edit_subset) == 2 60 | subset_select.selected = [0] 61 | assert len(app.session.edit_subset_mode.edit_subset) == 1 62 | 63 | # select multiple, then set multiple to false 64 | subset_select.selected = [0, 1] 65 | assert len(app.session.edit_subset_mode.edit_subset) == 2 66 | subset_select.multiple = False 67 | assert subset_select.selected == [0] 68 | assert len(app.session.edit_subset_mode.edit_subset) == 1 69 | 70 | # switch multiple on again 71 | subset_select.multiple = True 72 | assert subset_select.selected == [0] 73 | 74 | # and check again, now second selected 75 | subset_select.selected = [1] 76 | subset_select.multiple = False 77 | 78 | # if we deselect all, we should go to the 'new' state 79 | subset_select.selected = [] 80 | -------------------------------------------------------------------------------- /glue_jupyter/conftest.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import IPython.display as idisp 3 | import numpy as np 4 | from glue.core import Data 5 | import glue_jupyter as gj 6 | 7 | 8 | @pytest.fixture 9 | def dataxyz(): 10 | return Data(x=[1, 2, 3], y=[2, 3, 4], z=[5, 6, 7], label="xyz data") 11 | 12 | 13 | @pytest.fixture 14 | def datacat(): 15 | return Data(a=['a', 'b', 'c'], b=['d', 'e', 'f'], label="categorical data") 16 | 17 | 18 | @pytest.fixture 19 | def dataxz(): 20 | ox = 0 21 | oy = 1 22 | return Data(x=[1 + ox, 2 + ox, 3 + ox], z=[2 + oy, 3 + oy, 4 + oy], label="xy data") 23 | 24 | 25 | @pytest.fixture 26 | def datax(): 27 | ox = 0 28 | return Data(x=[1 + ox, 2 + ox, 3 + ox], label="x data") 29 | 30 | 31 | @pytest.fixture 32 | def data_unlinked(): 33 | return Data(a=[1, 2], label="unlinked data") 34 | 35 | 36 | @pytest.fixture 37 | def data_empty(): 38 | return Data(label="empty data") 39 | 40 | 41 | @pytest.fixture 42 | def data_4d(): 43 | return Data(x=np.arange(120).reshape((4, 2, 3, 5)), label='Data 4D') 44 | 45 | 46 | @pytest.fixture 47 | def data_volume(): 48 | return gj.example_volume() 49 | 50 | 51 | @pytest.fixture 52 | def data_image(): 53 | return gj.example_image() 54 | 55 | 56 | @pytest.fixture 57 | def data_flat(): 58 | return Data(x=np.arange(72).reshape((6, 4, 1, 3)), label='Flat 4D') 59 | 60 | 61 | @pytest.fixture 62 | def app(dataxyz, datax, dataxz, data_volume, data_image, data_flat): 63 | app = gj.jglue(dataxyz=dataxyz, dataxz=dataxz, datax=datax) 64 | app.add_link(dataxyz, 'x', dataxz, 'x') 65 | app.add_link(dataxyz, 'y', dataxz, 'z') 66 | app.add_link(dataxyz, 'x', datax, 'x') 67 | app.add_data(data_volume=data_volume) 68 | app.add_data(data_image=data_image) 69 | app.add_data(data_flat=data_flat) 70 | app.add_link(data_image, 'Pixel Axis 0 [y]', dataxyz, 'y') 71 | app.add_link(data_image, 'Pixel Axis 1 [x]', dataxyz, 'x') 72 | app.add_link(data_volume, 'Pixel Axis 0 [z]', dataxyz, 'z') 73 | app.add_link(data_volume, 'Pixel Axis 1 [y]', dataxyz, 'y') 74 | app.add_link(data_volume, 'Pixel Axis 2 [x]', dataxyz, 'x') 75 | app.add_link(data_flat, 'Pixel Axis 0', dataxyz, 'z') 76 | app.add_link(data_flat, 'Pixel Axis 1', dataxyz, 'y') 77 | app.add_link(data_flat, 'Pixel Axis 2', dataxyz, 'x') 78 | return app 79 | 80 | 81 | try: 82 | import solara # noqa: F401 83 | except ImportError: 84 | SOLARA_INSTALLED = False 85 | else: 86 | SOLARA_INSTALLED = True 87 | 88 | 89 | import vispy # noqa 90 | vispy.use('jupyter_rfb') 91 | 92 | # Tweak IPython's display to not print out lots of __repr__s for widgets to 93 | # standard output. However, if we are using solara, we shouldn't do this as 94 | # it seems to cause issues. 95 | 96 | if not SOLARA_INSTALLED: 97 | 98 | ORIGINAL_DISPLAY = None 99 | 100 | def noop(*args, **kwargs): 101 | pass 102 | 103 | def pytest_configure(config): 104 | global ORIGINAL_DISPLAY 105 | ORIGINAL_DISPLAY = idisp.display 106 | idisp.display = noop 107 | 108 | def pytest_unconfigure(config): 109 | idisp.display = ORIGINAL_DISPLAY 110 | -------------------------------------------------------------------------------- /glue_jupyter/ipyvolume/tests/data/ipyvolume.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "### Test suite for ipyvolume Jupyter viewers\n", 8 | "\n", 9 | "This is a test suite for the ipyvolume Jupyter viewers." 10 | ] 11 | }, 12 | { 13 | "cell_type": "code", 14 | "execution_count": null, 15 | "metadata": {}, 16 | "outputs": [], 17 | "source": [ 18 | "import numpy as np\n", 19 | "from glue.core.data import Data\n", 20 | "import glue_jupyter as gj" 21 | ] 22 | }, 23 | { 24 | "cell_type": "markdown", 25 | "metadata": {}, 26 | "source": [ 27 | "Start off by creating test data:" 28 | ] 29 | }, 30 | { 31 | "cell_type": "code", 32 | "execution_count": null, 33 | "metadata": {}, 34 | "outputs": [], 35 | "source": [ 36 | "data1d = Data(x=np.random.normal(0, 1, 1000),\n", 37 | " y=np.random.normal(0, 1, 1000),\n", 38 | " z=np.random.normal(0, 1, 1000))\n", 39 | "data3d = Data(x=np.random.random((50, 60, 70)))" 40 | ] 41 | }, 42 | { 43 | "cell_type": "markdown", 44 | "metadata": {}, 45 | "source": [ 46 | "Creat glue-jupyter application:" 47 | ] 48 | }, 49 | { 50 | "cell_type": "code", 51 | "execution_count": null, 52 | "metadata": {}, 53 | "outputs": [], 54 | "source": [ 55 | "app = gj.jglue(data1d=data1d, data3d=data3d)" 56 | ] 57 | }, 58 | { 59 | "cell_type": "markdown", 60 | "metadata": {}, 61 | "source": [ 62 | "### Create 3D scatter viewer" 63 | ] 64 | }, 65 | { 66 | "cell_type": "code", 67 | "execution_count": null, 68 | "metadata": {}, 69 | "outputs": [], 70 | "source": [ 71 | "scatter = app.scatter3d(x='x', y='y', z='z', data=data1d)" 72 | ] 73 | }, 74 | { 75 | "cell_type": "markdown", 76 | "metadata": {}, 77 | "source": [ 78 | "### Create 3D volume rendering viewer" 79 | ] 80 | }, 81 | { 82 | "cell_type": "code", 83 | "execution_count": null, 84 | "metadata": {}, 85 | "outputs": [], 86 | "source": [ 87 | "volume = app.volshow(data=data3d)" 88 | ] 89 | }, 90 | { 91 | "cell_type": "markdown", 92 | "metadata": {}, 93 | "source": [ 94 | "### Create subsets" 95 | ] 96 | }, 97 | { 98 | "cell_type": "code", 99 | "execution_count": null, 100 | "metadata": {}, 101 | "outputs": [], 102 | "source": [ 103 | "app.data_collection.new_subset_group(subset_state=data1d.id['x'] > 0.5, label='Subset 1')" 104 | ] 105 | }, 106 | { 107 | "cell_type": "code", 108 | "execution_count": null, 109 | "metadata": {}, 110 | "outputs": [], 111 | "source": [ 112 | "app.data_collection.new_subset_group(subset_state=data3d.id['x'] > 0.5, label='Subset 2')" 113 | ] 114 | } 115 | ], 116 | "metadata": { 117 | "kernelspec": { 118 | "display_name": "Python 3", 119 | "language": "python", 120 | "name": "python3" 121 | }, 122 | "language_info": { 123 | "codemirror_mode": { 124 | "name": "ipython", 125 | "version": 3 126 | }, 127 | "file_extension": ".py", 128 | "mimetype": "text/x-python", 129 | "name": "python", 130 | "nbconvert_exporter": "python", 131 | "pygments_lexer": "ipython3", 132 | "version": "3.7.1" 133 | } 134 | }, 135 | "nbformat": 4, 136 | "nbformat_minor": 2 137 | } 138 | -------------------------------------------------------------------------------- /glue_jupyter/common/state_widgets/layer_scatter.py: -------------------------------------------------------------------------------- 1 | import ipyvuetify as v 2 | import traitlets 3 | 4 | from glue.config import colormaps 5 | 6 | from ...state_traitlets_helpers import GlueState 7 | from ...vuetify_helpers import link_glue, link_glue_choices 8 | 9 | __all__ = ["ScatterLayerStateWidget"] 10 | 11 | 12 | class ScatterLayerStateWidget(v.VuetifyTemplate): 13 | 14 | template_file = (__file__, "layer_scatter.vue") 15 | 16 | glue_state = GlueState().tag(sync=True) 17 | 18 | # Color 19 | 20 | cmap_mode_items = traitlets.List().tag(sync=True) 21 | cmap_mode_selected = traitlets.Int(allow_none=True).tag(sync=True) 22 | 23 | cmap_att_items = traitlets.List().tag(sync=True) 24 | cmap_att_selected = traitlets.Int(allow_none=True).tag(sync=True) 25 | 26 | cmap_items = traitlets.List().tag(sync=True) 27 | 28 | # Points 29 | 30 | points_mode_items = traitlets.List().tag(sync=True) 31 | points_mode_selected = traitlets.Int(allow_none=True).tag(sync=True) 32 | 33 | size_mode_items = traitlets.List().tag(sync=True) 34 | size_mode_selected = traitlets.Int(allow_none=True).tag(sync=True) 35 | 36 | size_att_items = traitlets.List().tag(sync=True) 37 | size_att_selected = traitlets.Int(allow_none=True).tag(sync=True) 38 | 39 | dpi = traitlets.Float().tag(sync=True) 40 | 41 | # Line 42 | 43 | linestyle_items = traitlets.List().tag(sync=True) 44 | linestyle_selected = traitlets.Int(allow_none=True).tag(sync=True) 45 | 46 | # Vectors 47 | 48 | vx_att_items = traitlets.List().tag(sync=True) 49 | vx_att_selected = traitlets.Int(allow_none=True).tag(sync=True) 50 | 51 | vy_att_items = traitlets.List().tag(sync=True) 52 | vy_att_selected = traitlets.Int(allow_none=True).tag(sync=True) 53 | 54 | vector_origin_items = traitlets.List().tag(sync=True) 55 | vector_origin_selected = traitlets.Int(allow_none=True).tag(sync=True) 56 | 57 | def __init__(self, layer_state): 58 | super().__init__() 59 | 60 | self.layer_state = layer_state 61 | self.glue_state = layer_state 62 | 63 | # Color 64 | 65 | link_glue_choices(self, layer_state, "cmap_mode") 66 | link_glue_choices(self, layer_state, "cmap_att") 67 | 68 | self.cmap_items = [ 69 | {"text": cmap[0], "value": cmap[1].name} for cmap in colormaps.members 70 | ] 71 | 72 | # Points 73 | 74 | link_glue_choices(self, layer_state, "points_mode") 75 | link_glue_choices(self, layer_state, "size_mode") 76 | link_glue_choices(self, layer_state, "size_att") 77 | 78 | link_glue(self, "dpi", layer_state.viewer_state) 79 | 80 | # TODO: make sliders for dpi and size scaling logarithmic 81 | 82 | # FIXME: moving any sliders causes a change in the colormap 83 | 84 | # Line 85 | 86 | link_glue_choices(self, layer_state, "linestyle") 87 | 88 | # Vectors 89 | 90 | link_glue_choices(self, layer_state, "vx_att") 91 | link_glue_choices(self, layer_state, "vy_att") 92 | link_glue_choices(self, layer_state, "vector_origin") 93 | 94 | def vue_set_colormap(self, data): 95 | cmap = None 96 | for member in colormaps.members: 97 | if member[1].name == data: 98 | cmap = member[1] 99 | break 100 | self.layer_state.cmap = cmap 101 | -------------------------------------------------------------------------------- /glue_jupyter/common/toolbar_vuetify.py: -------------------------------------------------------------------------------- 1 | from mimetypes import guess_type 2 | import os 3 | import ipyvuetify as v 4 | import traitlets 5 | import base64 6 | 7 | from glue.icons import icon_path 8 | import glue.viewers.common.tool 9 | from glue.viewers.common.tool import CheckableTool 10 | 11 | __all__ = ['BasicJupyterToolbar'] 12 | 13 | _icons = {} 14 | 15 | 16 | def read_icon(file_name, format): 17 | if file_name not in _icons: 18 | with open(file_name, "rb") as f: 19 | _icons[file_name] =\ 20 | f'data:image/{format};base64,{base64.b64encode(f.read()).decode("ascii")}' 21 | 22 | return _icons[file_name] 23 | 24 | 25 | class BasicJupyterToolbar(v.VuetifyTemplate): 26 | template_file = (__file__, 'basic_jupyter_toolbar.vue') 27 | 28 | active_tool = traitlets.Instance(glue.viewers.common.tool.Tool, allow_none=True, 29 | default_value=None) 30 | tools_data = traitlets.Dict(default_value={}).tag(sync=True) 31 | active_tool_id = traitlets.Any().tag(sync=True) 32 | 33 | def __init__(self, viewer): 34 | self.output = viewer.output_widget 35 | self.tools = {} 36 | if viewer._default_mouse_mode_cls is not None: 37 | self._default_mouse_mode = viewer._default_mouse_mode_cls(viewer) 38 | self._default_mouse_mode.activate() 39 | else: 40 | self._default_mouse_mode = None 41 | super().__init__() 42 | 43 | @traitlets.observe('active_tool_id') 44 | def _on_change_v_model(self, change): 45 | if change.new is not None: 46 | if isinstance(self.tools[change.new], CheckableTool): 47 | self.active_tool = self.tools[change.new] 48 | else: 49 | # In this case it is a non-checkable tool and we should 50 | # activate it but not keep the tool checked in the toolbar 51 | self.tools[change.new].activate() 52 | self.active_tool_id = None 53 | else: 54 | self.active_tool = None 55 | 56 | @traitlets.observe('active_tool') 57 | def _on_change_active_tool(self, change): 58 | if change.old: 59 | change.old.deactivate() 60 | else: 61 | if self._default_mouse_mode: 62 | self._default_mouse_mode.deactivate() 63 | if change.new: 64 | change.new.activate() 65 | self.active_tool_id = change.new.tool_id 66 | else: 67 | self.active_tool_id = None 68 | if self._default_mouse_mode is not None: 69 | self._default_mouse_mode.activate() 70 | 71 | def add_tool(self, tool): 72 | self.tools[tool.tool_id] = tool 73 | # TODO: we should ideally just incorporate this check into icon_path directly. 74 | ext = os.path.splitext(tool.icon)[1][1:] or "svg" 75 | if os.path.exists(tool.icon): 76 | path = tool.icon 77 | else: 78 | path = icon_path(tool.icon, icon_format=ext) 79 | 80 | format = guess_type(path)[0] 81 | image_prefix = "image/" 82 | if format is None or not format.startswith(image_prefix): 83 | raise ValueError(f"Invalid or unknown image MIME type for: {path}") 84 | format = format[len(image_prefix):] 85 | self.tools_data = { 86 | **self.tools_data, 87 | tool.tool_id: { 88 | 'tooltip': tool.tool_tip, 89 | 'img': read_icon(path, format) 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /notebooks/Experimental/bqplot_scatter_density.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "id": "6f69d56f", 7 | "metadata": {}, 8 | "outputs": [], 9 | "source": [ 10 | "import numpy as np\n", 11 | "from bqplot import Figure, LinearScale, Axis, ColorScale\n", 12 | "from glue_jupyter.bqplot.scatter.scatter_density_mark import GenericDensityMark" 13 | ] 14 | }, 15 | { 16 | "cell_type": "code", 17 | "execution_count": null, 18 | "id": "4b03805a", 19 | "metadata": {}, 20 | "outputs": [], 21 | "source": [ 22 | "N = 1_000_000\n", 23 | "x = np.random.normal(1, 0.1, N)\n", 24 | "y = np.random.normal(0, 0.2, N)" 25 | ] 26 | }, 27 | { 28 | "cell_type": "code", 29 | "execution_count": null, 30 | "id": "77bb3c5e", 31 | "metadata": {}, 32 | "outputs": [], 33 | "source": [ 34 | "from fast_histogram import histogram2d\n", 35 | "\n", 36 | "def density1(bins=None, range=None):\n", 37 | " return histogram2d(y, x, bins=bins, range=range)\n", 38 | "\n", 39 | "def density2(bins=None, range=None):\n", 40 | " return histogram2d(y, x - 0.5, bins=bins, range=range)" 41 | ] 42 | }, 43 | { 44 | "cell_type": "code", 45 | "execution_count": null, 46 | "id": "ae814ef8", 47 | "metadata": {}, 48 | "outputs": [], 49 | "source": [ 50 | "scale_x = LinearScale(min=0, max=1)\n", 51 | "scale_y = LinearScale(min=0, max=1)\n", 52 | "scales = {'x': scale_x,\n", 53 | " 'y': scale_y}\n", 54 | "axis_x = Axis(scale=scale_x, label='x')\n", 55 | "axis_y = Axis(scale=scale_y, label='y', orientation='vertical')\n", 56 | "\n", 57 | "figure = Figure(scales=scales, axes=[axis_x, axis_y])\n", 58 | "\n", 59 | "scales_image = {'x': scale_x,\n", 60 | " 'y': scale_y,\n", 61 | " 'image': ColorScale(min=0, max=1)}\n", 62 | "\n", 63 | "image1 = GenericDensityMark(figure=figure, histogram2d_func=density1, dpi=20, color='red')\n", 64 | "image2 = GenericDensityMark(figure=figure, histogram2d_func=density2, dpi=20, color='blue')\n", 65 | "\n", 66 | "figure.marks = (image1, image2)\n", 67 | "\n", 68 | "figure" 69 | ] 70 | }, 71 | { 72 | "cell_type": "code", 73 | "execution_count": null, 74 | "id": "e080da1c", 75 | "metadata": {}, 76 | "outputs": [], 77 | "source": [ 78 | "from bqplot.interacts import PanZoom" 79 | ] 80 | }, 81 | { 82 | "cell_type": "code", 83 | "execution_count": null, 84 | "id": "2e64271a", 85 | "metadata": {}, 86 | "outputs": [], 87 | "source": [ 88 | "pz = PanZoom(scales={'x': [figure.axes[0].scale], 'y': [figure.axes[1].scale]})" 89 | ] 90 | }, 91 | { 92 | "cell_type": "code", 93 | "execution_count": null, 94 | "id": "c69c1efb", 95 | "metadata": {}, 96 | "outputs": [], 97 | "source": [ 98 | "figure.interaction = pz" 99 | ] 100 | } 101 | ], 102 | "metadata": { 103 | "kernelspec": { 104 | "display_name": "Python 3 (ipykernel)", 105 | "language": "python", 106 | "name": "python3" 107 | }, 108 | "language_info": { 109 | "codemirror_mode": { 110 | "name": "ipython", 111 | "version": 3 112 | }, 113 | "file_extension": ".py", 114 | "mimetype": "text/x-python", 115 | "name": "python", 116 | "nbconvert_exporter": "python", 117 | "pygments_lexer": "ipython3", 118 | "version": "3.11.4" 119 | } 120 | }, 121 | "nbformat": 4, 122 | "nbformat_minor": 5 123 | } 124 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2.1 2 | 3 | jobs: 4 | 5 | # The following job is to run any visual comparison test, and runs on any branch 6 | # or in any pull request. It will generate a summary page for each tox environment 7 | # being run which is accessible through the CircleCI artifacts. 8 | 9 | visual: 10 | parameters: 11 | jobname: 12 | type: string 13 | docker: 14 | - image: cimg/python:3.11 15 | environment: 16 | TOXENV: << parameters.jobname >> 17 | steps: 18 | - checkout 19 | - run: 20 | name: Install dependencies 21 | command: | 22 | sudo apt update 23 | sudo apt install libatk1.0-0 libatk-bridge2.0-0 libcups2 libxkbcommon0 libatspi2.0-0 libxdamage1 libgbm1 libpango-1.0-0 libcairo2 libasound2 24 | pip install pip tox --upgrade 25 | - run: 26 | name: Run tests 27 | command: tox -v 28 | - store_artifacts: 29 | path: results 30 | - run: 31 | name: "Image comparison page is available at: " 32 | command: echo "${CIRCLE_BUILD_URL}/artifacts/${CIRCLE_NODE_INDEX}/results/fig_comparison.html" 33 | 34 | # The following job runs only on main - and its main purpose is to update the 35 | # reference images in the glue-jupyter-visual-tests repository. This job needs 36 | # a deploy key. To produce this, go to the glue-jupyter-visual-tests 37 | # repository settings and go to SSH keys, then add your public SSH key. 38 | deploy-reference-images: 39 | parameters: 40 | jobname: 41 | type: string 42 | docker: 43 | - image: cimg/python:3.11 44 | environment: 45 | TOXENV: << parameters.jobname >> 46 | steps: 47 | - checkout 48 | - run: 49 | name: Install dependencies 50 | command: | 51 | sudo apt update 52 | sudo apt install libatk1.0-0 libatk-bridge2.0-0 libcups2 libxkbcommon0 libatspi2.0-0 libxdamage1 libgbm1 libpango-1.0-0 libcairo2 libasound2 53 | pip install pip tox --upgrade 54 | - run: ssh-add -D 55 | - add_ssh_keys: 56 | fingerprints: "be:23:bb:43:77:fd:bc:2d:38:82:3e:38:06:27:0f:fe" 57 | - run: ssh-keyscan github.com >> ~/.ssh/known_hosts 58 | - run: git config --global user.email "glue@circleci" && git config --global user.name "Glue Circle CI" 59 | - run: git clone git@github.com:glue-viz/glue-jupyter-visual-tests.git --depth 1 ~/glue-jupyter-visual-tests/ 60 | - run: 61 | name: Generate reference images 62 | command: tox -v -- --mpl-generate-path=/home/circleci/glue-jupyter-visual-tests/images/$TOXENV 63 | - run: | 64 | cd ~/glue-jupyter-visual-tests/ 65 | git pull 66 | git status 67 | git add . 68 | git commit -m "Update reference images from ${CIRCLE_BRANCH}" || echo "No changes to reference images to deploy" 69 | git push 70 | 71 | workflows: 72 | version: 2 73 | 74 | visual-tests: 75 | jobs: 76 | - visual: 77 | name: << matrix.jobname >> 78 | matrix: 79 | parameters: 80 | jobname: 81 | - "py311-test-visual" 82 | 83 | - deploy-reference-images: 84 | name: baseline-<< matrix.jobname >> 85 | matrix: 86 | parameters: 87 | jobname: 88 | - "py311-test-visual" 89 | requires: 90 | - << matrix.jobname >> 91 | filters: 92 | branches: 93 | only: 94 | - main 95 | 96 | notify: 97 | webhooks: 98 | - url: https://giles.cadair.dev/circleci 99 | -------------------------------------------------------------------------------- /glue_jupyter/common/slice_helpers.py: -------------------------------------------------------------------------------- 1 | # NOTE: The following MultiSliceHelper is adapted from the Qt version and there 2 | # is enough overlap that we could consider having a base class for the two. 3 | 4 | from ipywidgets import IntSlider 5 | 6 | from glue.viewers.image.state import AggregateSlice 7 | from glue.utils.decorators import avoid_circular 8 | 9 | __all__ = ['MultiSliceWidgetHelper'] 10 | 11 | 12 | class MultiSliceWidgetHelper(object): 13 | 14 | def __init__(self, viewer_state=None, layout=None): 15 | 16 | self.viewer_state = viewer_state 17 | 18 | self.layout = layout 19 | 20 | self.viewer_state.add_callback('x_att', self.sync_sliders_from_state) 21 | self.viewer_state.add_callback('y_att', self.sync_sliders_from_state) 22 | self.viewer_state.add_callback('slices', self.sync_sliders_from_state) 23 | self.viewer_state.add_callback('reference_data', self.sync_sliders_from_state) 24 | 25 | self._sliders = [] 26 | 27 | self._reference_data = None 28 | self._x_att = None 29 | self._y_att = None 30 | 31 | self.sync_sliders_from_state() 32 | 33 | @property 34 | def data(self): 35 | return self.viewer_state.reference_data 36 | 37 | def _clear(self): 38 | self.layout.children = [] 39 | self._sliders = [] 40 | 41 | @avoid_circular 42 | def sync_state_from_sliders(self, *args): 43 | slices = [] 44 | for i, slider in enumerate(self._sliders): 45 | if slider is not None: 46 | slices.append(slider.value) 47 | else: 48 | slices.append(self.viewer_state.slices[i]) 49 | self.viewer_state.slices = tuple(slices) 50 | 51 | @avoid_circular 52 | def sync_sliders_from_state(self, *args): 53 | 54 | if self.data is None or self.viewer_state.x_att is None or self.viewer_state.y_att is None: 55 | return 56 | 57 | if self.viewer_state.x_att is self.viewer_state.y_att: 58 | return 59 | 60 | # Update sliders if needed 61 | 62 | if (self.viewer_state.reference_data is not self._reference_data or 63 | self.viewer_state.x_att is not self._x_att or 64 | self.viewer_state.y_att is not self._y_att): 65 | 66 | self._reference_data = self.viewer_state.reference_data 67 | self._x_att = self.viewer_state.x_att 68 | self._y_att = self.viewer_state.y_att 69 | 70 | self._clear() 71 | 72 | for i in range(self.data.ndim): 73 | 74 | if i == self.viewer_state.x_att.axis or i == self.viewer_state.y_att.axis: 75 | self._sliders.append(None) 76 | continue 77 | 78 | if self.viewer_state.reference_data.coords is None: 79 | label = self.viewer_state.reference_data.pixel_component_ids[i].label 80 | else: 81 | label = self.viewer_state.reference_data.world_component_ids[i].label 82 | slider = IntSlider(min=0, max=self.data.shape[i]-1, description=label) 83 | 84 | slider.observe(self.sync_state_from_sliders, 'value') 85 | self._sliders.append(slider) 86 | self.layout.children += (slider,) 87 | 88 | for i in range(self.data.ndim): 89 | if self._sliders[i] is not None: 90 | if isinstance(self.viewer_state.slices[i], AggregateSlice): 91 | self._sliders[i].value = self.viewer_state.slices[i].center 92 | else: 93 | self._sliders[i].value = self.viewer_state.slices[i] 94 | -------------------------------------------------------------------------------- /glue_jupyter/common/state_widgets/viewer_image.py: -------------------------------------------------------------------------------- 1 | from glue.viewers.image.state import AggregateSlice 2 | from glue.core.coordinate_helpers import world_axis 3 | import ipyvuetify as v 4 | import traitlets 5 | from ...state_traitlets_helpers import GlueState 6 | from ...vuetify_helpers import link_glue_choices 7 | 8 | 9 | __all__ = ['ImageViewerStateWidget'] 10 | 11 | 12 | class ImageViewerStateWidget(v.VuetifyTemplate): 13 | template_file = (__file__, 'viewer_image.vue') 14 | 15 | glue_state = GlueState().tag(sync=True) 16 | 17 | color_mode_items = traitlets.List().tag(sync=True) 18 | color_mode_selected = traitlets.Int(allow_none=True).tag(sync=True) 19 | 20 | reference_data_items = traitlets.List().tag(sync=True) 21 | reference_data_selected = traitlets.Int(allow_none=True).tag(sync=True) 22 | 23 | x_att_world_items = traitlets.List().tag(sync=True) 24 | x_att_world_selected = traitlets.Int(allow_none=True).tag(sync=True) 25 | 26 | y_att_world_items = traitlets.List().tag(sync=True) 27 | y_att_world_selected = traitlets.Int(allow_none=True).tag(sync=True) 28 | 29 | sliders = traitlets.List().tag(sync=True) 30 | 31 | def __init__(self, viewer_state): 32 | super().__init__() 33 | 34 | self.viewer_state = viewer_state 35 | self.glue_state = viewer_state 36 | 37 | # Set up dropdown for color mode 38 | 39 | link_glue_choices(self, viewer_state, 'color_mode') 40 | 41 | # Set up dropdown for reference data 42 | 43 | link_glue_choices(self, viewer_state, 'reference_data') 44 | 45 | # Set up dropdowns for main attributes 46 | 47 | link_glue_choices(self, viewer_state, 'x_att_world') 48 | link_glue_choices(self, viewer_state, 'y_att_world') 49 | 50 | # Set up sliders for remaining dimensions 51 | 52 | for prop in ['x_att', 'y_att', 'slices', 'reference_data']: 53 | viewer_state.add_callback(prop, self._sync_sliders_from_state) 54 | 55 | self._sync_sliders_from_state() 56 | 57 | def _sync_sliders_from_state(self, *not_used): 58 | 59 | if self.viewer_state.reference_data is None or self.viewer_state.slices is None: 60 | return 61 | 62 | data = self.viewer_state.reference_data 63 | 64 | def used_on_axis(i): 65 | return i in [self.viewer_state.x_att.axis, self.viewer_state.y_att.axis] 66 | 67 | new_slices = [] 68 | for i in range(data.ndim): 69 | if not used_on_axis(i) and isinstance(self.viewer_state.slices[i], AggregateSlice): 70 | new_slices.append(self.viewer_state.slices[i].center) 71 | else: 72 | new_slices.append(self.viewer_state.slices[i]) 73 | self.viewer_state.slices = tuple(new_slices) 74 | 75 | self.sliders = [{ 76 | 'index': i, 77 | 'label': (data.world_component_ids[i].label if data.coords 78 | else data.pixel_component_ids[i].label), 79 | 'max': data.shape[i]-1, 80 | 'unit': (data.get_component(data.world_component_ids[i]).units if data.coords 81 | else ''), 82 | 'world_value': ("%0.4E" % world_axis(data.coords, 83 | data, 84 | pixel_axis=data.ndim - 1 - i, 85 | world_axis=data.ndim - 1 - i 86 | )[self.glue_state.slices[i]] if data.coords 87 | else '') 88 | } for i in range(data.ndim) if (not used_on_axis(i) and data.shape[i] > 1)] 89 | -------------------------------------------------------------------------------- /glue_jupyter/widgets/linked_dropdown.py: -------------------------------------------------------------------------------- 1 | from ipywidgets import Dropdown 2 | from echo.selection import ChoiceSeparator 3 | from glue.utils import avoid_circular 4 | 5 | __all__ = ['LinkedDropdown'] 6 | 7 | 8 | def get_choices(state, attribute_name): 9 | """ 10 | Return a list of the choices (excluding separators) in the 11 | SelectionCallbackProperty. 12 | """ 13 | choices = [] 14 | labels = [] 15 | display_func = getattr(type(state), attribute_name).get_display_func(state) 16 | if display_func is None: 17 | display_func = str 18 | for choice in getattr(type(state), attribute_name).get_choices(state): 19 | if not isinstance(choice, ChoiceSeparator): 20 | choices.append(choice) 21 | labels.append(display_func(choice)) 22 | return choices, labels 23 | 24 | 25 | class LinkedDropdown(Dropdown): 26 | """ 27 | A dropdown widget that is automatically linked to a SelectionCallbackProperty 28 | and syncs changes both ways. 29 | 30 | * On glue's side the state is in state.. 31 | * On the UI the state is in widget_select.value which holds the index of the selected item. 32 | * Indices are (for the moment) calculated from a list of choices (ignoring separators) 33 | """ 34 | 35 | def __init__(self, state, attribute_name, ui_name=None, label=None): 36 | 37 | if label is None: 38 | label = ui_name 39 | 40 | super(LinkedDropdown, self).__init__(description=label) 41 | 42 | self.state = state 43 | self.attribute_name = attribute_name 44 | 45 | self._update_options() 46 | 47 | self.state.add_callback(self.attribute_name, self._update_ui_from_glue_state) 48 | self.observe(self._update_glue_state_from_ui, 'value') 49 | 50 | # Set initial UI state to match SelectionCallbackProperty 51 | self._update_ui_from_glue_state() 52 | 53 | def _update_options(self): 54 | self._choices, self._labels = get_choices(self.state, self.attribute_name) 55 | value = self.value 56 | self.options = list(zip(self._labels, self._choices)) 57 | if value is not None and any(value is choice for choice in self._choices): 58 | self.value = value 59 | else: 60 | self.value = None 61 | 62 | @avoid_circular 63 | def _update_glue_state_from_ui(self, change): 64 | """ 65 | Update the SelectionCallbackProperty based on the UI. 66 | """ 67 | setattr(self.state, self.attribute_name, self.value) 68 | 69 | @avoid_circular 70 | def _update_ui_from_glue_state(self, *ignore_args): 71 | """ 72 | Update the UI based on the SelectionCallbackProperty. 73 | """ 74 | value = getattr(self.state, self.attribute_name) 75 | 76 | # If we are here, the SelectionCallbackProperty has been changed, and 77 | # this can be due to the options or the selection changing so we need 78 | # to update the options. 79 | self._update_options() 80 | 81 | if len(self._choices) > 0: 82 | value = getattr(self.state, self.attribute_name) 83 | for choice in self._choices: 84 | if choice is value: 85 | self.value = value 86 | break 87 | else: 88 | if isinstance(value, str): 89 | for i, label in enumerate(self._labels): 90 | if label == value: 91 | self.value = self._choices[i] 92 | break 93 | else: 94 | self.value = None 95 | else: 96 | self.value = None 97 | -------------------------------------------------------------------------------- /glue_jupyter/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | 3 | import importlib.metadata 4 | 5 | import numpy as np 6 | import ipyvolume as ipv 7 | from IPython.display import display 8 | 9 | from glue.core import Data 10 | from glue.core.parsers import parse_data, parse_links 11 | from glue.core.data_factories import load_data 12 | 13 | from .app import JupyterApplication # noqa 14 | 15 | __all__ = ['jglue', 'example_data_xyz', 'example_image', 'example_volume', 16 | 'JupyterApplication', 'set_layout_factory', 'get_layout_factory', 17 | '__version__'] 18 | 19 | __version__ = importlib.metadata.version(__name__) 20 | 21 | LAYOUT_FACTORY = None 22 | 23 | 24 | def set_layout_factory(func): 25 | """ 26 | Set the function to use to generate the viewer layout. This should take a 27 | viewer class and return a widget containing the viewer widgets laid out 28 | in the desired way. 29 | """ 30 | global LAYOUT_FACTORY 31 | LAYOUT_FACTORY = func 32 | 33 | 34 | def get_layout_factory(): 35 | """ 36 | Get the current layout factory. Returns `None` if using the default. 37 | """ 38 | if LAYOUT_FACTORY is None: 39 | from .vuetify_layout import vuetify_layout_factory 40 | return vuetify_layout_factory 41 | else: 42 | return LAYOUT_FACTORY 43 | 44 | 45 | def jglue(*args, settings=None, show=False, links=None, **kwargs): 46 | """ 47 | Create a new Jupyter-based glue application. 48 | 49 | It is typically easiest to call this function without arguments and load 50 | data and add links separately in subsequent calls. However, this function 51 | can also take the same inputs as the ``qglue`` function in Qt glue. 52 | 53 | Once this function is called, it will return a 54 | `~glue_jupyter.JupyterApplication` object, which can then be used to 55 | load data, set up links, and create visualizations. See the documentation 56 | for that class for more details. 57 | """ 58 | 59 | japp = JupyterApplication(settings=settings) 60 | 61 | dc = japp.data_collection 62 | for label, data in kwargs.items(): 63 | if isinstance(data, str): 64 | data = load_data(data) 65 | dc.extend(parse_data(data, label)) 66 | for data in args: 67 | dc.append(data) 68 | 69 | if links is not None: 70 | dc.add_link(parse_links(dc, links)) 71 | 72 | if show: 73 | display(japp) 74 | return japp 75 | 76 | 77 | def example_data_xyz(seed=42, N=500, loc=0, scale=1, label='xyz'): 78 | """ 79 | Create an example dataset with three attributes x, y, and z set to random 80 | values. 81 | """ 82 | rng = np.random.RandomState(seed) 83 | x, y, z = rng.normal(loc, scale, size=(3, N)) 84 | vx = x - x.mean() 85 | vy = y - y.mean() 86 | vz = z - z.mean() 87 | speed = np.sqrt(vx**2 + vy**2 + vz**2) 88 | data_xyz = Data(x=x, y=y, z=z, vx=vx, vy=vy, vz=vz, speed=speed, label=label) 89 | return data_xyz 90 | 91 | 92 | def example_volume(shape=64, limits=[-4, 4]): 93 | """ 94 | Creates a test 3-d dataset containing a ball. 95 | """ 96 | ball_data = ipv.examples.ball(shape=shape, limits=limits, show=False, draw=False) 97 | data = Data() 98 | data.add_component(ball_data, label='intensity') 99 | return data 100 | 101 | 102 | def example_image(shape=64, limits=[-4, 4]): 103 | """ 104 | Creates a test 2-d dataset containing an image. 105 | """ 106 | x = np.linspace(-3, 3, num=shape) 107 | X, Y = np.meshgrid(x, x) 108 | rho = 0.8 109 | intensity = np.exp(-X**2-Y**2-2*X*Y*rho) 110 | data = Data() 111 | data.add_component(intensity, label='intensity') 112 | return data 113 | -------------------------------------------------------------------------------- /glue_jupyter/bqplot/scatter/tests/test_viewer.py: -------------------------------------------------------------------------------- 1 | from itertools import permutations 2 | 3 | 4 | def test_scatter2d_nd(app, data_4d): 5 | # Regression test for a bug that meant that arrays with more than one 6 | # dimension did not work correctly. 7 | app.add_data(data_4d) 8 | scatter = app.scatter2d(x='x', y='x', data=data_4d) 9 | scatter.state.layers[0].vector_visible = True 10 | scatter.state.layers[0].size_mode = 'Linear' 11 | scatter.state.layers[0].cmap_mode = 'Linear' 12 | 13 | 14 | def test_scatter2d_categorical(app, datacat): 15 | # Make sure that things work correctly with arrays that have categorical 16 | # components - we use the numerical codes for these. In future we should 17 | # make sure that we show the correct labels on the axes. 18 | app.add_data(datacat) 19 | scatter = app.scatter2d(data=datacat) 20 | scatter.state.layers[0].vector_visible = True 21 | scatter.state.layers[0].size_mode = 'Linear' 22 | scatter.state.layers[0].cmap_mode = 'Linear' 23 | assert str(scatter.state.x_att) == 'a' 24 | assert str(scatter.state.y_att) == 'b' 25 | 26 | 27 | def test_non_hex_colors(app, dataxyz): 28 | 29 | # Make sure non-hex colors such as '0.4' and 'red', which are valid 30 | # matplotlib colors, work as expected. 31 | 32 | viewer = app.scatter2d(data=dataxyz) 33 | dataxyz.style.color = '0.3' 34 | dataxyz.style.color = 'indigo' 35 | 36 | app.subset('test', dataxyz.id['x'] > 1) 37 | viewer.layer_options.selected = 1 38 | dataxyz.subsets[0].style.color = '0.5' 39 | dataxyz.subsets[0].style.color = 'purple' 40 | 41 | 42 | def test_remove(app, dataxz, dataxyz): 43 | s = app.scatter2d(data=dataxyz) 44 | s.add_data(dataxz) 45 | app.data_collection.new_subset_group(subset_state=dataxz.id['x'] > 1, label='test') 46 | assert len(s.figure.marks) == 24 47 | s.remove_data(dataxyz) 48 | assert len(s.figure.marks) == 12 49 | s.remove_data(dataxz) 50 | assert len(s.figure.marks) == 0 51 | 52 | 53 | def test_zorder(app, data_volume, dataxz, dataxyz): 54 | s = app.scatter2d(data=dataxyz) 55 | s.add_data(dataxz) 56 | s.add_data(data_volume) 57 | xyz, xz, vol = s.layers 58 | 59 | for p in permutations([1, 2, 3]): 60 | vol.state.zorder, xz.state.zorder, xyz.state.zorder = p 61 | it = iter(s.figure.marks) 62 | assert all(layer.scatter_mark in it for layer in s.layers) 63 | 64 | 65 | def test_limits_init(app, dataxz, dataxyz): 66 | 67 | # Regression test for a bug that caused the bqplot limits 68 | # to not match the glue state straight after initialization 69 | 70 | s = app.scatter2d(data=dataxyz) 71 | 72 | assert s.state.x_min == 0.92 73 | assert s.state.x_max == 3.08 74 | assert s.state.y_min == 1.92 75 | assert s.state.y_max == 4.08 76 | 77 | assert s.scale_x.min == 0.92 78 | assert s.scale_x.max == 3.08 79 | assert s.scale_y.min == 1.92 80 | assert s.scale_y.max == 4.08 81 | 82 | 83 | def test_incompatible_data(app): 84 | 85 | # Regression test for a bug that caused the scatter viewer to raise an 86 | # exception if an incompatible dataset was present, and also for a bug 87 | # that occurred when trying to remove the original dataset 88 | 89 | d1 = app.add_data(data={'x': [1, 2, 3], 'y': [1, 2, 1]})[0] 90 | d2 = app.add_data(data={'x': [2, 3, 4], 'y': [2, 3, 2]})[0] 91 | 92 | s = app.scatter2d(data=d1) 93 | s.add_data(d2) 94 | 95 | assert s.state.x_att is d1.id['x'] 96 | assert s.state.y_att is d1.id['y'] 97 | 98 | assert len(s.layers) == 2 99 | assert s.layers[0].enabled 100 | assert not s.layers[1].enabled 101 | 102 | app.data_collection.remove(d1) 103 | 104 | assert s.state.x_att is d2.id['x'] 105 | assert s.state.y_att is d2.id['y'] 106 | 107 | assert len(s.layers) == 1 108 | assert s.layers[0].enabled 109 | -------------------------------------------------------------------------------- /glue_jupyter/tests/test_state_traitlets_helpers.py: -------------------------------------------------------------------------------- 1 | import traitlets 2 | from glue.core import Data 3 | from glue.core.state_objects import State, CallbackProperty 4 | from echo import ListCallbackProperty 5 | from glue_jupyter.state_traitlets_helpers import GlueState 6 | 7 | 8 | class Widget1(traitlets.HasTraits): 9 | 10 | state = GlueState() 11 | 12 | latest_json = None 13 | 14 | # The following two methods mimic the behavior of ipywidgets 15 | 16 | @traitlets.observe('state') 17 | def on_state_change(self, change): 18 | to_json = self.trait_metadata('state', 'to_json') 19 | self.latest_json = to_json(self.state, self) 20 | 21 | def set_state_from_json(self, json): 22 | from_json = self.trait_metadata('state', 'from_json') 23 | from_json(json, self) 24 | 25 | 26 | class CustomSubState(State): 27 | c = CallbackProperty(3) 28 | 29 | 30 | class CustomState(State): 31 | a = CallbackProperty(1) 32 | b = CallbackProperty(2) 33 | sub = ListCallbackProperty() 34 | 35 | 36 | def test_to_json(): 37 | widget = Widget1() 38 | assert widget.latest_json is None 39 | widget.state = CustomState() 40 | assert widget.latest_json == {"a": 1, "b": 2, "sub": []} 41 | widget.state.sub.append(CustomSubState()) 42 | assert widget.latest_json == {"a": 1, "b": 2, "sub": [{"c": 3}]} 43 | widget.state.sub[0].c = 4 44 | assert widget.latest_json == {"a": 1, "b": 2, "sub": [{"c": 4}]} 45 | widget.state.b = 5 46 | assert widget.latest_json == {"a": 1, "b": 5, "sub": [{"c": 4}]} 47 | widget.state.sub.pop(0) 48 | assert widget.latest_json == {"a": 1, "b": 5, "sub": []} 49 | 50 | 51 | def test_from_json(): 52 | widget = Widget1() 53 | widget.state = CustomState() 54 | widget.state.sub.append(CustomSubState()) 55 | assert widget.latest_json == {"a": 1, "b": 2, "sub": [{"c": 3}]} 56 | widget.set_state_from_json({"a": 3}) 57 | assert widget.state.a == 3 58 | assert widget.latest_json == {"a": 3, "b": 2, "sub": [{"c": 3}]} 59 | widget.set_state_from_json({"sub": [{"c": 2}]}) 60 | assert widget.state.sub[0].c == 2 61 | assert widget.latest_json == {"a": 3, "b": 2, "sub": [{"c": 2}]} 62 | # Giving an empty list does not clear the list - it just means that no 63 | # items will be updated. 64 | widget.set_state_from_json({"sub": []}) 65 | assert widget.latest_json == {"a": 3, "b": 2, "sub": [{"c": 2}]} 66 | # We can also update lists by passing a dict with index: value pairs in 67 | # cases where we just want to update some values 68 | widget.set_state_from_json({"sub": {0: {'c': 9}}}) 69 | assert widget.latest_json == {"a": 3, "b": 2, "sub": [{"c": 9}]} 70 | 71 | 72 | def test_to_json_data(): 73 | # Make sure we just convert the dataset to its label 74 | widget = Widget1() 75 | widget.state = CustomState() 76 | widget.state.a = Data(label='test') 77 | assert widget.latest_json == {"a": "611cfa3b-ebb5-42d2-b5c7-ba9bce8b51a4", 78 | "b": 2, 79 | "sub": []} 80 | 81 | 82 | def test_from_json_nested_ignore(): 83 | # Regression test for a bug that cause the MAGIC_IGNORE value to be set on 84 | # the glue state if it existed in a nested structure. 85 | widget = Widget1() 86 | widget.state = CustomState() 87 | widget.state.sub.append(CustomSubState()) 88 | widget.state.sub[0].c = Data(label='test') 89 | widget.state.sub.append(Data(label='test')) 90 | assert widget.latest_json == {"a": 1, 91 | "b": 2, 92 | "sub": [{'c': '611cfa3b-ebb5-42d2-b5c7-ba9bce8b51a4'}, 93 | '611cfa3b-ebb5-42d2-b5c7-ba9bce8b51a4']} 94 | widget.set_state_from_json(widget.latest_json) 95 | assert widget.state.a == 1 96 | assert widget.state.b == 2 97 | assert isinstance(widget.state.sub[0].c, Data) 98 | assert isinstance(widget.state.sub[1], Data) 99 | -------------------------------------------------------------------------------- /glue_jupyter/ipyvolume/common/tools.py: -------------------------------------------------------------------------------- 1 | from contextlib import nullcontext 2 | 3 | import numpy as np 4 | 5 | from glue.core.roi import PolygonalROI, CircularROI, RectangularROI, Projected3dROI 6 | 7 | from glue.config import viewer_tool 8 | from glue.viewers.common.tool import CheckableTool 9 | 10 | __all__ = [] 11 | 12 | 13 | class IPyVolumeCheckableTool(CheckableTool): 14 | 15 | def __init__(self, viewer): 16 | self.viewer = viewer 17 | self.viewer.figure.on_selection(self.on_selection) 18 | 19 | def activate(self): 20 | self.viewer.figure.selector = self.selector 21 | 22 | def deactivate(self): 23 | self.viewer.figure.selector = '' 24 | 25 | @property 26 | def projection_matrix(self): 27 | W = np.array(self.viewer.figure.matrix_world).reshape((4, 4)) .T # noqa: N806 28 | P = np.array(self.viewer.figure.matrix_projection).reshape((4, 4)).T # noqa: N806 29 | M = np.dot(P, W) # noqa: N806 30 | return M 31 | 32 | 33 | @viewer_tool 34 | class IpyvolumePolygonMode(IPyVolumeCheckableTool): 35 | 36 | icon = 'glue_polygon' 37 | tool_id = 'ipyvolume:polygon' 38 | action_text = 'Polygon ROI' 39 | tool_tip = 'Define a polygonal region of interest' 40 | 41 | selector = 'polygon' 42 | 43 | def on_selection(self, data, other=None): 44 | 45 | if data['type'] != self.selector: 46 | return 47 | 48 | if data['device']: 49 | with self.viewer._output_widget or nullcontext(): 50 | region = data['device'] 51 | vx, vy = zip(*region) 52 | roi_2d = PolygonalROI(vx=vx, vy=vy) 53 | roi = Projected3dROI(roi_2d, self.projection_matrix) 54 | self.viewer.apply_roi(roi) 55 | 56 | 57 | @viewer_tool 58 | class IpyvolumeLassoMode(IpyvolumePolygonMode): 59 | 60 | icon = 'glue_lasso' 61 | tool_id = 'ipyvolume:lasso' 62 | action_text = 'Lasso ROI' 63 | tool_tip = 'Lasso a region of interest' 64 | 65 | selector = 'lasso' 66 | 67 | 68 | @viewer_tool 69 | class IpyvolumeCircleMode(IPyVolumeCheckableTool): 70 | 71 | icon = 'glue_circle' 72 | tool_id = 'ipyvolume:circle' 73 | action_text = 'Circular ROI' 74 | tool_tip = 'Define a circular region of interest' 75 | 76 | selector = 'circle' 77 | 78 | def on_selection(self, data, other=None): 79 | 80 | if data['type'] != self.selector: 81 | return 82 | 83 | if data['device']: 84 | with self.viewer._output_widget or nullcontext(): 85 | x1, y1 = data['device']['begin'] 86 | x2, y2 = data['device']['end'] 87 | dx = x2 - x1 88 | dy = y2 - y1 89 | r = (dx**2 + dy**2)**0.5 90 | roi_2d = CircularROI(xc=x1, yc=y1, radius=r) 91 | roi = Projected3dROI(roi_2d, self.projection_matrix) 92 | self.viewer.apply_roi(roi) 93 | 94 | 95 | @viewer_tool 96 | class IpyvolumeRectanglewMode(IPyVolumeCheckableTool): 97 | 98 | icon = 'glue_square' 99 | tool_id = 'ipyvolume:rectangle' 100 | action_text = 'Rectangular ROI' 101 | tool_tip = 'Define a rectangular region of interest' 102 | 103 | selector = 'rectangle' 104 | 105 | def on_selection(self, data, other=None): 106 | 107 | if data['type'] != self.selector: 108 | return 109 | 110 | if data['device']: 111 | with self.viewer._output_widget or nullcontext(): 112 | x1, y1 = data['device']['begin'] 113 | x2, y2 = data['device']['end'] 114 | x = [x1, x2] 115 | y = [y1, y2] 116 | roi_2d = RectangularROI( 117 | xmin=min(x), xmax=max(x), ymin=min(y), ymax=max(y)) 118 | roi = Projected3dROI(roi_2d, self.projection_matrix) 119 | self.viewer.apply_roi(roi) 120 | -------------------------------------------------------------------------------- /glue_jupyter/ipyvolume/common/viewer.py: -------------------------------------------------------------------------------- 1 | import ipyvolume as ipv 2 | import ipyvolume.moviemaker 3 | 4 | from glue.core.subset import RoiSubsetState3d 5 | from glue.core.command import ApplySubsetState 6 | 7 | from ...view import IPyWidgetView 8 | from ...link import dlink 9 | 10 | from .viewer_options_widget import Viewer3DStateWidget 11 | 12 | __all__ = ['IpyvolumeBaseView'] 13 | 14 | 15 | class IpyvolumeBaseView(IPyWidgetView): 16 | 17 | allow_duplicate_data = False 18 | allow_duplicate_subset = False 19 | 20 | _options_cls = Viewer3DStateWidget 21 | 22 | tools = ['ipyvolume:lasso', 'ipyvolume:circle', 'ipyvolume:rectangle'] 23 | 24 | def __init__(self, *args, **kwargs): 25 | 26 | self.figure = ipv.figure(animation_exponent=1.) 27 | self.figure.selector = '' 28 | self.figure.width = 600 29 | self._figure_widget = self.figure 30 | 31 | super(IpyvolumeBaseView, self).__init__(*args, **kwargs) 32 | 33 | # FIXME: hack for the movie maker to have access to the figure 34 | self.state.figure = self.figure 35 | 36 | # note that for ipyvolume, we use axis in the order z, x, y in order to have 37 | # the z axis of glue pointing up 38 | def attribute_to_label(attribute): 39 | return 'null' if attribute is None else attribute.label 40 | 41 | dlink((self.state, 'x_att'), (self.figure, 'zlabel'), attribute_to_label) 42 | dlink((self.state, 'y_att'), (self.figure, 'xlabel'), attribute_to_label) 43 | dlink((self.state, 'z_att'), (self.figure, 'ylabel'), attribute_to_label) 44 | 45 | self.state.add_callback('x_min', self.limits_to_scales) 46 | self.state.add_callback('x_max', self.limits_to_scales) 47 | self.state.add_callback('y_min', self.limits_to_scales) 48 | self.state.add_callback('y_max', self.limits_to_scales) 49 | if hasattr(self.state, 'z_min'): 50 | self.state.add_callback('z_min', self.limits_to_scales) 51 | self.state.add_callback('z_max', self.limits_to_scales) 52 | 53 | self.state.add_callback('visible_axes', self._update_axes_visibility) 54 | self.state.add_callback('native_aspect', self._update_aspect) 55 | 56 | self.create_layout() 57 | 58 | def _update_axes_visibility(self, *args): 59 | with self.figure: 60 | if self.state.visible_axes: 61 | ipv.style.axes_on() 62 | ipv.style.box_on() 63 | else: 64 | ipv.style.axes_off() 65 | ipv.style.box_off() 66 | 67 | def _update_aspect(self, *args): 68 | self.figure.box_size = self.state.aspect.tolist() 69 | 70 | @property 71 | def figure_widget(self): 72 | return self._figure_widget 73 | 74 | def apply_roi(self, roi, use_current=False): 75 | if len(self.layers) > 0: 76 | # self.state.x_att.parent.get_component(self.state.x_att) 77 | x = self.state.x_att 78 | # self.state.y_att.parent.get_component(self.state.y_att) 79 | y = self.state.y_att 80 | # self.state.z_att.parent.get_component(self.state.z_att) 81 | z = self.state.z_att 82 | subset_state = RoiSubsetState3d(x, y, z, roi) 83 | cmd = ApplySubsetState(data_collection=self._data, 84 | subset_state=subset_state, 85 | override_mode=use_current) 86 | self._session.command_stack.do(cmd) 87 | 88 | def limits_to_scales(self, *args): 89 | if self.state.y_min is not None and self.state.y_max is not None: 90 | self.figure.xlim = self.state.y_min, self.state.y_max 91 | if self.state.x_min is not None and self.state.x_max is not None: 92 | self.figure.zlim = self.state.x_min, self.state.x_max 93 | if hasattr(self.state, 'z_min'): 94 | if self.state.z_min is not None and self.state.z_max is not None: 95 | self.figure.ylim = self.state.z_min, self.state.z_max 96 | 97 | def redraw(self): 98 | pass 99 | -------------------------------------------------------------------------------- /glue_jupyter/ipyvolume/volume/layer_style_widget.py: -------------------------------------------------------------------------------- 1 | from ipywidgets import (Checkbox, VBox, ColorPicker, Dropdown, FloatSlider, 2 | FloatLogSlider) 3 | 4 | from glue.utils import color2hex 5 | 6 | from ...link import link, dlink 7 | 8 | __all__ = ['Volume3DLayerStateWidget'] 9 | 10 | 11 | class Volume3DLayerStateWidget(VBox): 12 | 13 | def __init__(self, layer_state): 14 | 15 | self.state = layer_state 16 | 17 | self.widget_lighting = Checkbox(description='lighting', value=self.state.lighting) 18 | link((self.state, 'lighting'), (self.widget_lighting, 'value')) 19 | 20 | render_methods = 'NORMAL MAX_INTENSITY'.split() 21 | self.widget_render_method = Dropdown(options=render_methods, 22 | value=self.state.render_method, 23 | description='method') 24 | link((self.state, 'render_method'), (self.widget_render_method, 'value')) 25 | 26 | self.size_options = [32, 64, 128, 128+64, 256, 256+128, 512] 27 | options = [(str(k), k) for k in self.size_options] 28 | self.widget_max_resolution = Dropdown(options=options, value=128, 29 | description='max resolution') 30 | link((self.state, 'max_resolution'), (self.widget_max_resolution, 'value')) 31 | 32 | if self.state.vmin is None: 33 | self.state.vmin = 0 34 | 35 | self.widget_data_min = FloatSlider(description='min', min=0, max=1, 36 | value=self.state.vmin, step=0.001) 37 | link((self.state, 'vmin'), (self.widget_data_min, 'value')) 38 | dlink((self.state, 'data_min'), (self.widget_data_min, 'min')) 39 | dlink((self.state, 'data_max'), (self.widget_data_min, 'max')) 40 | 41 | if self.state.vmax is None: 42 | self.state.vmax = 1 43 | 44 | self.widget_data_max = FloatSlider(description='max', min=0, max=1, 45 | value=self.state.vmax, step=0.001) 46 | link((self.state, 'vmax'), (self.widget_data_max, 'value')) 47 | dlink((self.state, 'data_min'), (self.widget_data_max, 'min')) 48 | dlink((self.state, 'data_max'), (self.widget_data_max, 'max')) 49 | 50 | self.widget_clamp_min = Checkbox(description='clamp minimum', value=self.state.clamp_min) 51 | link((self.state, 'clamp_min'), (self.widget_clamp_min, 'value')) 52 | 53 | self.widget_clamp_max = Checkbox(description='clamp maximum', value=self.state.clamp_max) 54 | link((self.state, 'clamp_max'), (self.widget_clamp_max, 'value')) 55 | 56 | self.widget_color = ColorPicker(value=color2hex(self.state.color), description='color') 57 | link((self.state, 'color'), (self.widget_color, 'value'), color2hex) 58 | 59 | if self.state.alpha is None: 60 | self.state.alpha = 1 61 | 62 | self.widget_opacity = FloatSlider(description='opacity', min=0, max=1, 63 | value=self.state.alpha, step=0.001) 64 | link((self.state, 'alpha'), (self.widget_opacity, 'value')) 65 | 66 | self.widget_opacity_scale = FloatLogSlider(description='opacity scale', base=10, 67 | min=-3, max=3, step=0.01, 68 | value=self.state.opacity_scale) 69 | link((self.state, 'opacity_scale'), (self.widget_opacity_scale, 'value')) 70 | 71 | # FIXME: this should be fixed 72 | # self.widget_reset_zoom = Button(description="Reset zoom") 73 | # self.widget_reset_zoom.on_click(self.state.viewer_state.reset_limits) 74 | 75 | super().__init__([self.widget_render_method, self.widget_lighting, 76 | self.widget_data_min, self.widget_data_max, 77 | self.widget_clamp_min, self.widget_clamp_max, 78 | self.widget_max_resolution, # self.widget_reset_zoom, 79 | self.widget_color, self.widget_opacity, 80 | self.widget_opacity_scale]) 81 | -------------------------------------------------------------------------------- /glue_jupyter/table/table.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 15 | 16 | 17 | 18 | # 19 | 20 | 21 | filter_list 22 | 23 | 24 | 25 | brightness_1 26 | 27 | 28 | {{ header.text }} 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | {{ props.item.__row__ }} 37 | 38 | 39 | select({checked: value, row: props.item.__row__})" 44 | /> 45 | 46 | 47 | 48 | brightness_1 53 | 54 | 55 | 60 | 61 | {{ props.item[header.value] }} 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 104 | -------------------------------------------------------------------------------- /glue_jupyter/bqplot/tests/data/bqplot.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "### Test suite for bqplot Jupyter viewers\n", 8 | "\n", 9 | "This is a test suite for the bqplot Jupyter viewers." 10 | ] 11 | }, 12 | { 13 | "cell_type": "code", 14 | "execution_count": null, 15 | "metadata": {}, 16 | "outputs": [], 17 | "source": [ 18 | "import numpy as np\n", 19 | "from glue.core.data import Data\n", 20 | "import glue_jupyter as gj" 21 | ] 22 | }, 23 | { 24 | "cell_type": "markdown", 25 | "metadata": {}, 26 | "source": [ 27 | "Start off by creating test data:" 28 | ] 29 | }, 30 | { 31 | "cell_type": "code", 32 | "execution_count": null, 33 | "metadata": {}, 34 | "outputs": [], 35 | "source": [ 36 | "data1d = Data(x=np.random.normal(0, 1, 1000), y=np.random.normal(0, 1, 1000))\n", 37 | "data2d = Data(x=np.random.random((100, 100)))" 38 | ] 39 | }, 40 | { 41 | "cell_type": "markdown", 42 | "metadata": {}, 43 | "source": [ 44 | "Creat glue-jupyter application:" 45 | ] 46 | }, 47 | { 48 | "cell_type": "code", 49 | "execution_count": null, 50 | "metadata": {}, 51 | "outputs": [], 52 | "source": [ 53 | "app = gj.jglue(data1d=data1d, data2d=data2d)" 54 | ] 55 | }, 56 | { 57 | "cell_type": "markdown", 58 | "metadata": {}, 59 | "source": [ 60 | "### Create histogram viewer" 61 | ] 62 | }, 63 | { 64 | "cell_type": "code", 65 | "execution_count": null, 66 | "metadata": {}, 67 | "outputs": [], 68 | "source": [ 69 | "histogram = app.histogram1d(x='x', data=data1d, widget='bqplot')" 70 | ] 71 | }, 72 | { 73 | "cell_type": "markdown", 74 | "metadata": {}, 75 | "source": [ 76 | "### Create profile viewer" 77 | ] 78 | }, 79 | { 80 | "cell_type": "code", 81 | "execution_count": null, 82 | "metadata": { 83 | "scrolled": false 84 | }, 85 | "outputs": [], 86 | "source": [ 87 | "profile = app.profile1d(data=data2d, widget='bqplot')" 88 | ] 89 | }, 90 | { 91 | "cell_type": "markdown", 92 | "metadata": {}, 93 | "source": [ 94 | "### Create 2D scatter viewer" 95 | ] 96 | }, 97 | { 98 | "cell_type": "code", 99 | "execution_count": null, 100 | "metadata": {}, 101 | "outputs": [], 102 | "source": [ 103 | "scatter = app.scatter2d(x='x', y='y', data=data1d, widget='bqplot')" 104 | ] 105 | }, 106 | { 107 | "cell_type": "markdown", 108 | "metadata": {}, 109 | "source": [ 110 | "### Create image viewer" 111 | ] 112 | }, 113 | { 114 | "cell_type": "code", 115 | "execution_count": null, 116 | "metadata": { 117 | "scrolled": false 118 | }, 119 | "outputs": [], 120 | "source": [ 121 | "image = app.imshow(data=data2d, widget='bqplot')" 122 | ] 123 | }, 124 | { 125 | "cell_type": "markdown", 126 | "metadata": {}, 127 | "source": [ 128 | "### Create subsets" 129 | ] 130 | }, 131 | { 132 | "cell_type": "code", 133 | "execution_count": null, 134 | "metadata": {}, 135 | "outputs": [], 136 | "source": [ 137 | "app.data_collection.new_subset_group(subset_state=data1d.id['x'] > 0.5, label='Subset 1')" 138 | ] 139 | }, 140 | { 141 | "cell_type": "code", 142 | "execution_count": null, 143 | "metadata": {}, 144 | "outputs": [], 145 | "source": [ 146 | "app.data_collection.new_subset_group(subset_state=data2d.id['x'] > 0.5, label='Subset 2')" 147 | ] 148 | } 149 | ], 150 | "metadata": { 151 | "kernelspec": { 152 | "display_name": "Python 3", 153 | "language": "python", 154 | "name": "python3" 155 | }, 156 | "language_info": { 157 | "codemirror_mode": { 158 | "name": "ipython", 159 | "version": 3 160 | }, 161 | "file_extension": ".py", 162 | "mimetype": "text/x-python", 163 | "name": "python", 164 | "nbconvert_exporter": "python", 165 | "pygments_lexer": "ipython3", 166 | "version": "3.7.1" 167 | } 168 | }, 169 | "nbformat": 4, 170 | "nbformat_minor": 2 171 | } 172 | -------------------------------------------------------------------------------- /notebooks/Planes/Boston Planes.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "## Snakes on a Plane (well, Planes in Python)\n", 8 | "\n", 9 | "This notebook explores a CSV files that contains the position, speeds and other\n", 10 | "related information for planes in the Boston area over a period of 6 hours.\n", 11 | "\n", 12 | "### About the data\n", 13 | "\n", 14 | "These data were collected by directly recording publicly available [Automatic\n", 15 | "dependent surveillance — broadcast\n", 16 | "(ADS–B)](https://en.wikipedia.org/wiki/Automatic_dependent_surveillance_%E2%80%93_broadcast)\n", 17 | "transmissions from planes from a single location (hence the data should not be considered complete). The data can be found in https://github.com/glue-viz/glue-example-data/tree/master/Planes/.\n", 18 | "\n", 19 | "For convenience we can use the ``require_data`` function to\n", 20 | "automatically download them here:" 21 | ] 22 | }, 23 | { 24 | "cell_type": "code", 25 | "execution_count": null, 26 | "metadata": {}, 27 | "outputs": [], 28 | "source": [ 29 | "from glue_jupyter.data import require_data\n", 30 | "require_data('Planes/boston_planes_6h.csv')" 31 | ] 32 | }, 33 | { 34 | "cell_type": "markdown", 35 | "metadata": {}, 36 | "source": [ 37 | "### Starting up the glue Jupyter application" 38 | ] 39 | }, 40 | { 41 | "cell_type": "markdown", 42 | "metadata": {}, 43 | "source": [ 44 | "Let's start up glue:" 45 | ] 46 | }, 47 | { 48 | "cell_type": "code", 49 | "execution_count": null, 50 | "metadata": {}, 51 | "outputs": [], 52 | "source": [ 53 | "import glue_jupyter as gj\n", 54 | "app = gj.jglue()" 55 | ] 56 | }, 57 | { 58 | "cell_type": "markdown", 59 | "metadata": {}, 60 | "source": [ 61 | "and load in the data:" 62 | ] 63 | }, 64 | { 65 | "cell_type": "code", 66 | "execution_count": null, 67 | "metadata": {}, 68 | "outputs": [], 69 | "source": [ 70 | "planes = app.load_data('boston_planes_6h.csv')" 71 | ] 72 | }, 73 | { 74 | "cell_type": "markdown", 75 | "metadata": {}, 76 | "source": [ 77 | "We can start off by making a 2D plot of the positions of the plane (the x/y values are in km offset):" 78 | ] 79 | }, 80 | { 81 | "cell_type": "code", 82 | "execution_count": null, 83 | "metadata": {}, 84 | "outputs": [], 85 | "source": [ 86 | "scatter_viewer = app.scatter2d(x='x', y='y', data=planes)" 87 | ] 88 | }, 89 | { 90 | "cell_type": "markdown", 91 | "metadata": {}, 92 | "source": [ 93 | "Let's now make a histogram:" 94 | ] 95 | }, 96 | { 97 | "cell_type": "code", 98 | "execution_count": null, 99 | "metadata": {}, 100 | "outputs": [], 101 | "source": [ 102 | "histogram_viewer = app.histogram1d(x='vertical_rate', data=planes)" 103 | ] 104 | }, 105 | { 106 | "cell_type": "markdown", 107 | "metadata": {}, 108 | "source": [ 109 | "Try clicking on the **brush** tool and selecting some of the high values of vertical rate. These are planes taking off. Now check where these are in the 2D scatter plot.\n", 110 | "\n", 111 | "Let's have fun and take a look at what this looks like in 3D:" 112 | ] 113 | }, 114 | { 115 | "cell_type": "code", 116 | "execution_count": null, 117 | "metadata": {}, 118 | "outputs": [], 119 | "source": [ 120 | "scatter_3d_viewer = app.scatter3d(x='x', y='y', z='altitude', data=planes)" 121 | ] 122 | } 123 | ], 124 | "metadata": { 125 | "kernelspec": { 126 | "display_name": "Python 3", 127 | "language": "python", 128 | "name": "python3" 129 | }, 130 | "language_info": { 131 | "codemirror_mode": { 132 | "name": "ipython", 133 | "version": 3 134 | }, 135 | "file_extension": ".py", 136 | "mimetype": "text/x-python", 137 | "name": "python", 138 | "nbconvert_exporter": "python", 139 | "pygments_lexer": "ipython3", 140 | "version": "3.7.1" 141 | } 142 | }, 143 | "nbformat": 4, 144 | "nbformat_minor": 2 145 | } 146 | -------------------------------------------------------------------------------- /glue_jupyter/matplotlib/tests/data/matplotlib.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "### Test suite for Matplotlib Jupyter viewers\n", 8 | "\n", 9 | "This is a test suite for the Matplotlib Jupyter viewers." 10 | ] 11 | }, 12 | { 13 | "cell_type": "code", 14 | "execution_count": null, 15 | "metadata": {}, 16 | "outputs": [], 17 | "source": [ 18 | "import numpy as np\n", 19 | "from glue.core.data import Data\n", 20 | "import glue_jupyter as gj" 21 | ] 22 | }, 23 | { 24 | "cell_type": "markdown", 25 | "metadata": {}, 26 | "source": [ 27 | "Start off by creating test data:" 28 | ] 29 | }, 30 | { 31 | "cell_type": "code", 32 | "execution_count": null, 33 | "metadata": {}, 34 | "outputs": [], 35 | "source": [ 36 | "data1d = Data(x=np.random.normal(0, 1, 1000), y=np.random.normal(0, 1, 1000))\n", 37 | "data2d = Data(x=np.random.random((100, 100)))" 38 | ] 39 | }, 40 | { 41 | "cell_type": "markdown", 42 | "metadata": {}, 43 | "source": [ 44 | "Creat glue-jupyter application:" 45 | ] 46 | }, 47 | { 48 | "cell_type": "code", 49 | "execution_count": null, 50 | "metadata": {}, 51 | "outputs": [], 52 | "source": [ 53 | "app = gj.jglue(data1d=data1d, data2d=data2d)" 54 | ] 55 | }, 56 | { 57 | "cell_type": "markdown", 58 | "metadata": {}, 59 | "source": [ 60 | "### Create histogram viewer" 61 | ] 62 | }, 63 | { 64 | "cell_type": "code", 65 | "execution_count": null, 66 | "metadata": {}, 67 | "outputs": [], 68 | "source": [ 69 | "histogram = app.histogram1d(x='x', data=data1d, widget='matplotlib')" 70 | ] 71 | }, 72 | { 73 | "cell_type": "markdown", 74 | "metadata": {}, 75 | "source": [ 76 | "### Create 2D scatter viewer" 77 | ] 78 | }, 79 | { 80 | "cell_type": "code", 81 | "execution_count": null, 82 | "metadata": {}, 83 | "outputs": [], 84 | "source": [ 85 | "scatter = app.scatter2d(x='x', y='y', data=data1d, widget='matplotlib')" 86 | ] 87 | }, 88 | { 89 | "cell_type": "markdown", 90 | "metadata": {}, 91 | "source": [ 92 | "### Create profile viewer" 93 | ] 94 | }, 95 | { 96 | "cell_type": "code", 97 | "execution_count": null, 98 | "metadata": { 99 | "scrolled": false 100 | }, 101 | "outputs": [], 102 | "source": [ 103 | "profile = app.profile1d(x='Pixel Axis 1 [x]', data=data2d, widget='matplotlib')" 104 | ] 105 | }, 106 | { 107 | "cell_type": "markdown", 108 | "metadata": {}, 109 | "source": [ 110 | "### Create image viewer" 111 | ] 112 | }, 113 | { 114 | "cell_type": "code", 115 | "execution_count": null, 116 | "metadata": { 117 | "scrolled": false 118 | }, 119 | "outputs": [], 120 | "source": [ 121 | "image = app.imshow(data=data2d, widget='matplotlib')" 122 | ] 123 | }, 124 | { 125 | "cell_type": "markdown", 126 | "metadata": {}, 127 | "source": [ 128 | "### Create subsets" 129 | ] 130 | }, 131 | { 132 | "cell_type": "code", 133 | "execution_count": null, 134 | "metadata": {}, 135 | "outputs": [], 136 | "source": [ 137 | "app.data_collection.new_subset_group(subset_state=data1d.id['x'] > 0.5, label='Subset 1')" 138 | ] 139 | }, 140 | { 141 | "cell_type": "code", 142 | "execution_count": null, 143 | "metadata": {}, 144 | "outputs": [], 145 | "source": [ 146 | "app.data_collection.new_subset_group(subset_state=data2d.id['x'] > 0.5, label='Subset 2')" 147 | ] 148 | }, 149 | { 150 | "cell_type": "code", 151 | "execution_count": null, 152 | "metadata": {}, 153 | "outputs": [], 154 | "source": [] 155 | } 156 | ], 157 | "metadata": { 158 | "kernelspec": { 159 | "display_name": "Python 3", 160 | "language": "python", 161 | "name": "python3" 162 | }, 163 | "language_info": { 164 | "codemirror_mode": { 165 | "name": "ipython", 166 | "version": 3 167 | }, 168 | "file_extension": ".py", 169 | "mimetype": "text/x-python", 170 | "name": "python", 171 | "nbconvert_exporter": "python", 172 | "pygments_lexer": "ipython3", 173 | "version": "3.7.1" 174 | } 175 | }, 176 | "nbformat": 4, 177 | "nbformat_minor": 2 178 | } 179 | -------------------------------------------------------------------------------- /glue_jupyter/common/state_widgets/layer_image.py: -------------------------------------------------------------------------------- 1 | from ipywidgets import Checkbox, FloatSlider, ColorPicker, VBox 2 | from glue.config import colormaps 3 | from glue.utils import color2hex 4 | 5 | from ...link import link 6 | 7 | import ipyvuetify as v 8 | import traitlets 9 | from ...state_traitlets_helpers import GlueState 10 | from ...vuetify_helpers import link_glue_choices, link_glue 11 | 12 | __all__ = ['ImageLayerStateWidget', 'ImageSubsetLayerStateWidget'] 13 | 14 | 15 | class ImageLayerStateWidget(v.VuetifyTemplate): 16 | template_file = (__file__, 'layer_image.vue') 17 | 18 | glue_state = GlueState().tag(sync=True) 19 | 20 | # TODO: expose toggle to turn on image and/or contour 21 | 22 | attribute_items = traitlets.List().tag(sync=True) 23 | attribute_selected = traitlets.Int(allow_none=True).tag(sync=True) 24 | 25 | stretch_items = traitlets.List().tag(sync=True) 26 | stretch_selected = traitlets.Int(allow_none=True).tag(sync=True) 27 | 28 | percentile_items = traitlets.List().tag(sync=True) 29 | percentile_selected = traitlets.Int(allow_none=True).tag(sync=True) 30 | 31 | colormap_items = traitlets.List().tag(sync=True) 32 | color_mode = traitlets.Unicode().tag(sync=True) 33 | 34 | c_levels_txt = traitlets.Unicode().tag(sync=True) 35 | c_levels_txt_editing = False 36 | c_levels_error = traitlets.Unicode().tag(sync=True) 37 | 38 | has_contour = traitlets.Bool().tag(sync=True) 39 | 40 | def __init__(self, layer_state): 41 | super().__init__() 42 | 43 | self.layer_state = layer_state 44 | self.glue_state = layer_state 45 | 46 | self.has_contour = hasattr(layer_state, "contour_visible") 47 | 48 | link_glue_choices(self, layer_state, 'attribute') 49 | link_glue_choices(self, layer_state, 'stretch') 50 | link_glue_choices(self, layer_state, 'percentile') 51 | 52 | self.colormap_items = [dict( 53 | text=cmap[0], 54 | value=cmap[1].name 55 | ) for cmap in colormaps.members] 56 | 57 | link_glue(self, 'color_mode', layer_state.viewer_state) 58 | 59 | # we only go from glue state to the text version of the level list 60 | # the other way around is handled in _on_change_c_levels_txt 61 | if self.has_contour: 62 | def levels_to_text(*_ignore): 63 | if not self.c_levels_txt_editing: 64 | text = ", ".join('%g' % v for v in self.glue_state.levels) 65 | self.c_levels_txt = text 66 | 67 | self.glue_state.add_callback('levels', levels_to_text) 68 | 69 | @traitlets.observe('c_levels_txt') 70 | def _on_change_c_levels_txt(self, change): 71 | try: 72 | self.c_levels_txt_editing = True 73 | try: 74 | parts = change['new'].split(',') 75 | float_list_str = [float(v.strip()) for v in parts] 76 | except Exception as e: 77 | self.c_levels_error = str(e) 78 | return 79 | 80 | if self.glue_state.level_mode == "Custom": 81 | self.glue_state.levels = float_list_str 82 | self.c_levels_error = '' 83 | finally: 84 | self.c_levels_txt_editing = False 85 | 86 | def vue_set_colormap(self, data): 87 | cmap = None 88 | for member in colormaps.members: 89 | if member[1].name == data: 90 | cmap = member[1] 91 | break 92 | 93 | self.layer_state.cmap = cmap 94 | 95 | 96 | class ImageSubsetLayerStateWidget(VBox): 97 | 98 | def __init__(self, layer_state): 99 | 100 | self.state = layer_state 101 | 102 | self.widget_visible = Checkbox(description='visible', value=self.state.visible) 103 | link((self.state, 'visible'), (self.widget_visible, 'value')) 104 | 105 | self.widget_opacity = FloatSlider(min=0, max=1, step=0.01, value=self.state.alpha, 106 | description='opacity') 107 | link((self.state, 'alpha'), (self.widget_opacity, 'value')) 108 | 109 | self.widget_color = ColorPicker(description='color') 110 | link((self.state, 'color'), (self.widget_color, 'value'), color2hex) 111 | 112 | super().__init__([self.widget_visible, self.widget_opacity, self.widget_color]) 113 | -------------------------------------------------------------------------------- /glue_jupyter/widgets/subset_select_vuetify.py: -------------------------------------------------------------------------------- 1 | import ipyvuetify as v 2 | 3 | import traitlets 4 | from glue.core import message as msg 5 | from glue.core.hub import HubListener 6 | 7 | __all__ = ['SubsetSelect'] 8 | 9 | 10 | def subset_to_dict(subset): 11 | return { 12 | 'label': subset.label, 13 | 'color': subset.style.color 14 | } 15 | 16 | 17 | class SubsetSelect(v.VuetifyTemplate, HubListener): 18 | """ 19 | Widget responsible for selecting which subsets are active, sync state between UI and glue. 20 | """ 21 | 22 | selected = traitlets.List().tag(sync=True) 23 | available = traitlets.List().tag(sync=True) 24 | no_selection_text = traitlets.Unicode('No selection (create new)').tag(sync=True) 25 | multiple = traitlets.Bool(False).tag(sync=True) 26 | nr_of_full_names = traitlets.Int(2).tag(sync=True) 27 | show_allow_multiple_subsets = traitlets.Bool(False).tag(sync=True) 28 | 29 | methods = traitlets.Unicode('''{ 30 | toSubsets(indices) { 31 | return indices.map(i => this.available[i]); 32 | }, 33 | deselect() { 34 | this.multiple = false; 35 | this.selected = []; 36 | }, 37 | toggleSubset(index) { 38 | this.selected = this.selected.includes(index) 39 | ? this.selected.filter(x => x != index) 40 | : this.selected.concat(index); 41 | this.handleMultiple(); 42 | }, 43 | handleMultiple() { 44 | if (!this.multiple && this.selected.length > 1) { 45 | this.selected = [this.selected.pop()]; 46 | } 47 | } 48 | }''').tag(sync=True) 49 | 50 | template_file = (__file__, 'subset_select.vue') 51 | 52 | def __init__(self, session=None): 53 | super().__init__() 54 | 55 | self.edit_subset_mode = session.edit_subset_mode 56 | self.data_collection = session.data_collection 57 | 58 | # state change events from glue come in from the hub 59 | session.hub.subscribe(self, msg.EditSubsetMessage, 60 | handler=self._sync_selected_from_state) 61 | session.hub.subscribe(self, msg.SubsetCreateMessage, 62 | handler=self._sync_available_from_state) 63 | session.hub.subscribe(self, msg.SubsetUpdateMessage, 64 | handler=self._on_subset_update) 65 | session.hub.subscribe(self, msg.SubsetDeleteMessage, 66 | handler=self._sync_available_from_state) 67 | 68 | # manually trigger to set up the initial state 69 | self._sync_selected_from_state() 70 | self._sync_available_from_state() 71 | 72 | def _sync_selected_from_state(self, *args): 73 | self.selected = [self.data_collection.subset_groups.index(subset) for subset 74 | in self.edit_subset_mode.edit_subset] 75 | 76 | def _on_subset_update(self, msg): 77 | if msg.attribute in ('label', 'style'): 78 | self._sync_available_from_state() 79 | 80 | def _sync_available_from_state(self, *args): 81 | self.available = [subset_to_dict(subset) for subset in 82 | self.data_collection.subset_groups] 83 | if len(self.available) == 0: 84 | self.selected = [] 85 | 86 | @traitlets.observe('selected') 87 | def _sync_selected_from_ui(self, change): 88 | try: 89 | self.edit_subset_mode.edit_subset = [self.data_collection.subset_groups[index] for index 90 | in change['new']] 91 | # https://github.com/spacetelescope/jdaviz/issues/928 92 | except IndexError: 93 | if len(self.data_collection.subset_groups) == 0: 94 | self.edit_subset_mode.edit_subset = None 95 | else: 96 | self.edit_subset_mode.edit_subset = [self.data_collection.subset_groups[-1]] 97 | 98 | @traitlets.observe('multiple') 99 | def _switch_multiple(self, change): 100 | if not self.multiple: 101 | with self.hold_sync(): 102 | if len(self.selected) > 1: 103 | # take the first item of the selected items as the single selected item 104 | self.selected = self.selected[:1] 105 | 106 | def vue_remove_subset(self, index): 107 | self.data_collection.remove_subset_group(self.data_collection.subset_groups[index]) 108 | -------------------------------------------------------------------------------- /glue_jupyter/link.py: -------------------------------------------------------------------------------- 1 | import ipywidgets as widgets 2 | import echo.selection 3 | 4 | 5 | def _is_traitlet(obj): 6 | return hasattr(obj, 'observe') 7 | 8 | 9 | def _is_echo(link): 10 | return hasattr(getattr(type(link[0]), link[1]), 'add_callback') 11 | 12 | 13 | class link(object): 14 | def __init__(self, source, target, f1=lambda x: x, f2=lambda x: x): 15 | self.source = source 16 | self.target = target 17 | 18 | self._link(source, target, 'source', f1, True) 19 | self._link(target, source, 'target', f2) 20 | 21 | def _link(self, source, target, name, f, sync_directly=False): 22 | def sync(*ignore): 23 | old_value = getattr(target[0], target[1]) 24 | new_value = f(getattr(source[0], source[1])) 25 | if new_value != old_value: 26 | setattr(target[0], target[1], new_value) 27 | 28 | if _is_traitlet(source[0]): 29 | source[0].observe(sync, source[1]) 30 | elif _is_echo(source): 31 | callback_property = getattr(type(source[0]), source[1]) 32 | callback_property.add_callback(source[0], sync) 33 | else: 34 | raise ValueError('{} is unknown object'.format(name)) 35 | if sync_directly: 36 | sync() 37 | 38 | 39 | class dlink(link): 40 | def __init__(self, source, target, f1=lambda x: x): 41 | self.source = source 42 | self.target = target 43 | 44 | self._link(source, target, 'source', f1, True) 45 | 46 | 47 | def _assign(object, value): 48 | if isinstance(object, widgets.Widget): 49 | object, trait = object, 'value' 50 | else: 51 | object, trait = object 52 | setattr(object, trait, value) 53 | 54 | 55 | def calculation(inputs, output=None, initial_calculation=True): 56 | def decorator(f): 57 | def calculate(*ignore_args): 58 | values = [getattr(input, 'value') for input in inputs] 59 | result = f(*values) 60 | if output: 61 | _assign(output, result) 62 | for input in inputs: 63 | input.observe(calculate, 'value') 64 | if initial_calculation: 65 | calculate() 66 | return decorator 67 | 68 | 69 | def on_change(inputs, initial_call=False, once=False): 70 | def decorator(f): 71 | for input in inputs: 72 | # input can be (obj, 'x', 'y'), or just obj, where 'value' is assumed for default 73 | # this is only support for widgets/HasTraits 74 | if _is_traitlet(input): 75 | obj, attrnames = input, ['value'] 76 | else: 77 | obj, attrnames = input[0], input[1:] 78 | if _is_traitlet(input): 79 | obj.observe(lambda *ignore: f(), attrnames) 80 | elif _is_echo(input): 81 | for attrname in attrnames: 82 | callback_property = getattr(type(obj), attrname) 83 | callback_property.add_callback(obj, lambda *ignore: f()) 84 | else: 85 | raise ValueError('{} is unknown object'.format(obj)) 86 | if initial_call: 87 | f() 88 | return decorator 89 | 90 | 91 | def link_component_id_to_select_widget(state, state_attr, widget, widget_attr='value'): 92 | 93 | def update(*ignore): 94 | options = [k for k in getattr(type(state), state_attr).get_choices(state) 95 | if not isinstance(k, echo.selection.ChoiceSeparator)] 96 | display_func = getattr(type(state), state_attr).get_display_func(state) 97 | value = getattr(state, state_attr) 98 | widget.options = [(display_func(options[k]), k) for k in range(len(options))] 99 | # componentId's don't hash or compare well, use 'is' instead of == 100 | # ISSUE: value can be a string, and then it will never match 101 | matches = [k for k in range(len(options)) if value is options[k]] 102 | if len(matches): 103 | widget.index = matches[0] 104 | 105 | getattr(type(state), state_attr).add_callback(state, update) 106 | 107 | def update_state(change): 108 | options = [k for k in getattr(type(state), state_attr).get_choices(state) 109 | if not isinstance(k, echo.selection.ChoiceSeparator)] 110 | if change.new is not None: 111 | setattr(state, state_attr, options[change.new]) 112 | 113 | widget.observe(update_state, 'index') 114 | 115 | update() 116 | -------------------------------------------------------------------------------- /glue_jupyter/bqplot/image/tests/test_viewer.py: -------------------------------------------------------------------------------- 1 | import glue_jupyter.state_traitlets_helpers 2 | 3 | 4 | def test_non_hex_colors(app, data_image): 5 | 6 | # Make sure non-hex colors such as '0.4' and 'red', which are valid 7 | # matplotlib colors, work as expected. 8 | 9 | viewer = app.imshow(data=data_image) 10 | data_image.style.color = '0.3' 11 | data_image.style.color = 'indigo' 12 | 13 | app.subset('test', data_image.main_components[0] > 1) 14 | viewer.layer_options.selected = 1 15 | data_image.subsets[0].style.color = '0.5' 16 | data_image.subsets[0].style.color = 'purple' 17 | 18 | 19 | def test_remove(app, data_image, data_volume): 20 | s = app.imshow(data=data_image) 21 | s.add_data(data_volume) 22 | app.data_collection.new_subset_group(subset_state=data_image.id['intensity'] > 1, label='test') 23 | assert len(s.figure.marks) == 5 # 1 for composite, 2 for 2 subsets, 2 contours 24 | s.remove_data(data_image) 25 | assert len(s.figure.marks) == 3 # 1 composite, 1 contour, 1 subset 26 | s.remove_data(data_volume) 27 | assert len(s.figure.marks) == 1 # 1 composite 28 | 29 | 30 | def test_change_reference(app, data_image, data_volume): 31 | im = app.imshow(data=data_volume) 32 | im.add_data(data_image) 33 | im.state.reference_data = data_image 34 | 35 | 36 | def test_contour_levels(app, data_image, data_volume): 37 | s = app.imshow(data=data_image) 38 | layer = s.layers[0] 39 | assert layer.state.levels 40 | layer.state.c_min = 0 41 | layer.state.c_max = 10 42 | layer.state.n_levels = 3 43 | assert layer.state.levels == [0, 5, 10] 44 | # since we start invisible, we don't compute the contour lines 45 | assert len(layer.contour_artist.contour_lines) == 0 46 | # make the visible, so we trigger a compute 47 | layer.state.contour_visible = True 48 | assert len(layer.contour_artist.contour_lines) == 3 49 | layer.state.level_mode = 'Custom' 50 | layer.state.n_levels = 1 51 | assert layer.state.levels == [0, 5, 10] 52 | layer.state.level_mode = 'Linear' 53 | assert layer.state.levels == [0] 54 | assert len(layer.contour_artist.contour_lines) == 1 55 | 56 | # test the visual attributes 57 | layer.state.contour_visible = False 58 | assert layer.contour_artist.visible is False 59 | 60 | # since it's invisible, we should leave the contour lines alone 61 | layer.state.n_levels = 2 62 | assert len(layer.contour_artist.contour_lines) == 1 63 | # and update them again 64 | layer.state.contour_visible = True 65 | assert len(layer.contour_artist.contour_lines) == 2 66 | 67 | 68 | def test_contour_state(app, data_image): 69 | s = app.imshow(data=data_image) 70 | layer = s.layers[0] 71 | layer.state.c_min = 0 72 | layer.state.c_max = 10 73 | layer.state.n_levels = 3 74 | glue_jupyter.state_traitlets_helpers.update_state_from_dict( 75 | layer.state, 76 | {'level_mode': 'Custom', 'levels': [1, 2]} 77 | ) 78 | assert layer.state.levels == [1, 2] 79 | glue_jupyter.state_traitlets_helpers.update_state_from_dict( 80 | layer.state, 81 | {'level_mode': 'Linear', 'levels': [2, 3]} 82 | ) 83 | # Without priority of levels, this gets set to [2, 3] 84 | assert layer.state.levels == [0, 5, 10] 85 | 86 | 87 | def test_add_markers_zoom(app, data_image, data_volume, dataxyz): 88 | 89 | # Regression test for a bug that caused the zoom to be 90 | # reset when adding markers to an image 91 | 92 | im = app.imshow(data=data_image) 93 | 94 | im.state.x_min = 0.2 95 | im.state.x_max = 0.4 96 | im.state.y_min = 0.3 97 | im.state.y_max = 0.5 98 | 99 | app.add_link(data_image, data_image.pixel_component_ids[0], dataxyz, dataxyz.id['y']) 100 | app.add_link(data_image, data_image.pixel_component_ids[1], dataxyz, dataxyz.id['x']) 101 | im.add_data(dataxyz) 102 | 103 | assert im.state.x_min == 0.2 104 | assert im.state.x_max == 0.4 105 | assert im.state.y_min == 0.3 106 | assert im.state.y_max == 0.5 107 | 108 | im.add_data(data_volume) 109 | 110 | assert im.state.x_min == 0.2 111 | assert im.state.x_max == 0.4 112 | assert im.state.y_min == 0.3 113 | assert im.state.y_max == 0.5 114 | 115 | im.state.reference_data = data_volume 116 | 117 | assert im.state.x_min == -0.5 118 | assert im.state.x_max == 63.5 119 | assert im.state.y_min == -0.5 120 | assert im.state.y_max == 63.5 121 | -------------------------------------------------------------------------------- /glue_jupyter/bqplot/image/frb_mark.py: -------------------------------------------------------------------------------- 1 | # This is an image sub-class that automatically gets its values from an object 2 | # that implements __call__ with a bounds= argument and returns a fixed 3 | # resolution buffer (FRB). It is the equivalent of FRBArtist in glue-core. 4 | 5 | import math 6 | import numpy as np 7 | 8 | from bqplot import ColorScale 9 | from bqplot_image_gl import ImageGL 10 | 11 | from ...utils import debounced 12 | 13 | __all__ = ['FRBImage'] 14 | 15 | EMPTY_IMAGE = np.zeros((10, 10, 4), dtype=np.uint8) 16 | 17 | 18 | class FRBImage(ImageGL): 19 | 20 | def __init__(self, viewer, array_maker, compression='png'): 21 | 22 | # FIXME: need to use weakref to avoid circular references 23 | self.viewer = viewer 24 | 25 | self._external_padding = 0 26 | 27 | self.scale_image = ColorScale() 28 | self.scales = {'x': self.viewer.scale_x, 29 | 'y': self.viewer.scale_y, 30 | 'image': self.scale_image} 31 | 32 | super().__init__(image=EMPTY_IMAGE, scales=self.scales, compression=compression) 33 | 34 | self.array_maker = array_maker 35 | 36 | self.viewer.figure.axes[0].scale.observe(self.debounced_update, 'min') 37 | self.viewer.figure.axes[0].scale.observe(self.debounced_update, 'max') 38 | self.viewer.figure.axes[1].scale.observe(self.debounced_update, 'min') 39 | self.viewer.figure.axes[1].scale.observe(self.debounced_update, 'max') 40 | 41 | self._latest_hash = None 42 | 43 | # NOTE: we deliberately don't call .update() here because when FRBImage 44 | # is created for the main composite image layer the composite arrays 45 | # haven't been set up yet, and for subset layers the layer gets force 46 | # updated anyway when the layers are added to the viewer. 47 | 48 | @debounced(method=True) 49 | def debounced_update(self, *args, **kwargs): 50 | return self.update(self, *args, **kwargs) 51 | 52 | @property 53 | def shape(self): 54 | return self.viewer.shape 55 | 56 | @property 57 | def external_padding(self): 58 | return self._external_padding 59 | 60 | @external_padding.setter 61 | def external_padding(self, value): 62 | previous_value = self._external_padding 63 | self._external_padding = value 64 | if value > previous_value: # no point updating if the value is smaller than before 65 | self.debounced_update() 66 | 67 | def update(self, *args, force=False, **kwargs): 68 | 69 | # Shape can be (0, 0) when viewer was created and then destroyed. 70 | if self.shape is None or np.allclose(self.shape, 0): 71 | return 72 | 73 | # Get current limits from the plot 74 | xmin = self.viewer.figure.axes[0].scale.min 75 | xmax = self.viewer.figure.axes[0].scale.max 76 | ymin = self.viewer.figure.axes[1].scale.min 77 | ymax = self.viewer.figure.axes[1].scale.max 78 | 79 | if xmin is None or xmax is None or ymin is None or ymax is None: 80 | return 81 | 82 | current_hash = (xmin, xmax, ymin, ymax, self.external_padding) 83 | 84 | if not force and current_hash == self._latest_hash: 85 | return 86 | 87 | ny, nx = self.shape 88 | 89 | # Expand beyond the boundary 90 | if self.external_padding != 0: 91 | dx = (xmax - xmin) 92 | dy = (ymax - ymin) 93 | xmin, xmax = xmin - dx * self.external_padding, xmax + dx * self.external_padding 94 | ymin, ymax = ymin - dy * self.external_padding, ymax + dy * self.external_padding 95 | nx *= math.ceil(1 + 2 * self.external_padding) 96 | ny *= math.ceil(1 + 2 * self.external_padding) 97 | 98 | # Set up bounds 99 | bounds = [(ymin, ymax, ny), (xmin, xmax, nx)] 100 | 101 | # Get the array and assign it to the artist 102 | image = self.array_maker(bounds=bounds) 103 | if image is not None: 104 | if image.dtype == np.dtype("float64"): 105 | image = image.astype(np.float32) 106 | with self.hold_sync(): 107 | self.image = image 108 | self.x = (xmin, xmax) 109 | self.y = (ymin, ymax) 110 | else: 111 | self.image = EMPTY_IMAGE 112 | 113 | self._latest_hash = current_hash 114 | 115 | def invalidate_cache(self): 116 | self.update(force=True) 117 | --------------------------------------------------------------------------------