From 1cec7a4be5ce0569f2c7824f7b185b610249385d Mon Sep 17 00:00:00 2001 From: DavidVFiumano <40008486+DavidVFiumano@users.noreply.github.com> Date: Sat, 15 Jul 2023 18:46:45 -0400 Subject: [PATCH 01/13] Add sizes to scatter plots (#289) * Added a size feature. Not sure if it works yet, still need to add tests. * Added a few tests for the new sizes feature. * Forgot to add this file last commit. * Improved scatter_size.py example * Made some changes addressing Kushal's comments. * fixed an error caused by me forgetting to remove a cell in one of the notebooks * scattter plot added * updated to snake_case * updated to fixed some missing dependencies and remove unecessary code in the notebooks --- examples/desktop/scatter/scatter_size.py | 56 +++++++++ examples/desktop/screenshots/scatter_size.png | 3 + .../notebooks/scatter_sizes_animation.ipynb | 71 ++++++++++++ examples/notebooks/scatter_sizes_grid.ipynb | 86 ++++++++++++++ fastplotlib/graphics/_features/__init__.py | 2 + fastplotlib/graphics/_features/_sizes.py | 108 ++++++++++++++++++ fastplotlib/graphics/scatter.py | 23 +--- 7 files changed, 331 insertions(+), 18 deletions(-) create mode 100644 examples/desktop/scatter/scatter_size.py create mode 100644 examples/desktop/screenshots/scatter_size.png create mode 100644 examples/notebooks/scatter_sizes_animation.ipynb create mode 100644 examples/notebooks/scatter_sizes_grid.ipynb diff --git a/examples/desktop/scatter/scatter_size.py b/examples/desktop/scatter/scatter_size.py new file mode 100644 index 000000000..5b6987b7c --- /dev/null +++ b/examples/desktop/scatter/scatter_size.py @@ -0,0 +1,56 @@ +""" +Scatter Plot +============ +Example showing point size change for scatter plot. +""" + +# test_example = true +import numpy as np +import fastplotlib as fpl + +# grid with 2 rows and 3 columns +grid_shape = (2,1) + +# pan-zoom controllers for each view +# views are synced if they have the +# same controller ID +controllers = [ + [0], + [0] +] + + +# you can give string names for each subplot within the gridplot +names = [ + ["scalar_size"], + ["array_size"] +] + +# Create the grid plot +plot = fpl.GridPlot( + shape=grid_shape, + controllers=controllers, + names=names, + size=(1000, 1000) +) + +# get y_values using sin function +angles = np.arange(0, 20*np.pi+0.001, np.pi / 20) +y_values = 30*np.sin(angles) # 1 thousand points +x_values = np.array([x for x in range(len(y_values))], dtype=np.float32) + +data = np.column_stack([x_values, y_values]) + +plot["scalar_size"].add_scatter(data=data, sizes=5, colors="blue") # add a set of scalar sizes + +non_scalar_sizes = np.abs((y_values / np.pi)) # ensure minimum size of 5 +plot["array_size"].add_scatter(data=data, sizes=non_scalar_sizes, colors="red") + +for graph in plot: + graph.auto_scale(maintain_aspect=True) + +plot.show() + +if __name__ == "__main__": + print(__doc__) + fpl.run() \ No newline at end of file diff --git a/examples/desktop/screenshots/scatter_size.png b/examples/desktop/screenshots/scatter_size.png new file mode 100644 index 000000000..db637d270 --- /dev/null +++ b/examples/desktop/screenshots/scatter_size.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a4cefd4cf57e54e1ef7883edea54806dfde57939d0a395c5a7758124e41b8beb +size 63485 diff --git a/examples/notebooks/scatter_sizes_animation.ipynb b/examples/notebooks/scatter_sizes_animation.ipynb new file mode 100644 index 000000000..061f444d6 --- /dev/null +++ b/examples/notebooks/scatter_sizes_animation.ipynb @@ -0,0 +1,71 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from time import time\n", + "\n", + "import numpy as np\n", + "import fastplotlib as fpl\n", + "\n", + "plot = fpl.Plot()\n", + "\n", + "points = np.array([[-1,0,1],[-1,0,1]], dtype=np.float32).swapaxes(0,1)\n", + "size_delta_scales = np.array([10, 40, 100], dtype=np.float32)\n", + "min_sizes = 6\n", + "\n", + "def update_positions():\n", + " current_time = time()\n", + " newPositions = points + np.sin(((current_time / 4) % 1)*np.pi)\n", + " plot.graphics[0].data = newPositions\n", + " plot.camera.width = 4*np.max(newPositions[0,:])\n", + " plot.camera.height = 4*np.max(newPositions[1,:])\n", + "\n", + "def update_sizes():\n", + " current_time = time()\n", + " sin_sample = np.sin(((current_time / 4) % 1)*np.pi)\n", + " size_delta = sin_sample*size_delta_scales\n", + " plot.graphics[0].sizes = min_sizes + size_delta\n", + "\n", + "points = np.array([[0,0], \n", + " [1,1], \n", + " [2,2]])\n", + "scatter = plot.add_scatter(points, colors=[\"red\", \"green\", \"blue\"], sizes=12)\n", + "plot.add_animations(update_positions, update_sizes)\n", + "plot.show(autoscale=True)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "fastplotlib-dev", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.4" + }, + "orig_nbformat": 4 + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/examples/notebooks/scatter_sizes_grid.ipynb b/examples/notebooks/scatter_sizes_grid.ipynb new file mode 100644 index 000000000..ff64184f7 --- /dev/null +++ b/examples/notebooks/scatter_sizes_grid.ipynb @@ -0,0 +1,86 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "\"\"\"\n", + "Scatter Plot\n", + "============\n", + "Example showing point size change for scatter plot.\n", + "\"\"\"\n", + "\n", + "# test_example = true\n", + "import numpy as np\n", + "import fastplotlib as fpl\n", + "\n", + "# grid with 2 rows and 3 columns\n", + "grid_shape = (2,1)\n", + "\n", + "# pan-zoom controllers for each view\n", + "# views are synced if they have the \n", + "# same controller ID\n", + "controllers = [\n", + " [0],\n", + " [0]\n", + "]\n", + "\n", + "\n", + "# you can give string names for each subplot within the gridplot\n", + "names = [\n", + " [\"scalar_size\"],\n", + " [\"array_size\"]\n", + "]\n", + "\n", + "# Create the grid plot\n", + "plot = fpl.GridPlot(\n", + " shape=grid_shape,\n", + " controllers=controllers,\n", + " names=names,\n", + " size=(1000, 1000)\n", + ")\n", + "\n", + "# get y_values using sin function\n", + "angles = np.arange(0, 20*np.pi+0.001, np.pi / 20)\n", + "y_values = 30*np.sin(angles) # 1 thousand points\n", + "x_values = np.array([x for x in range(len(y_values))], dtype=np.float32)\n", + "\n", + "data = np.column_stack([x_values, y_values])\n", + "\n", + "plot[\"scalar_size\"].add_scatter(data=data, sizes=5, colors=\"blue\") # add a set of scalar sizes\n", + "\n", + "non_scalar_sizes = np.abs((y_values / np.pi)) # ensure minimum size of 5\n", + "plot[\"array_size\"].add_scatter(data=data, sizes=non_scalar_sizes, colors=\"red\")\n", + "\n", + "for graph in plot:\n", + " graph.auto_scale(maintain_aspect=True)\n", + "\n", + "plot.show()" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "fastplotlib-dev", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.4" + }, + "orig_nbformat": 4 + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/fastplotlib/graphics/_features/__init__.py b/fastplotlib/graphics/_features/__init__.py index 8e78a6260..a6ce9c3a3 100644 --- a/fastplotlib/graphics/_features/__init__.py +++ b/fastplotlib/graphics/_features/__init__.py @@ -1,5 +1,6 @@ from ._colors import ColorFeature, CmapFeature, ImageCmapFeature, HeatmapCmapFeature from ._data import PointsDataFeature, ImageDataFeature, HeatmapDataFeature +from ._sizes import PointsSizesFeature from ._present import PresentFeature from ._thickness import ThicknessFeature from ._base import GraphicFeature, GraphicFeatureIndexable, FeatureEvent, to_gpu_supported_dtype @@ -11,6 +12,7 @@ "ImageCmapFeature", "HeatmapCmapFeature", "PointsDataFeature", + "PointsSizesFeature", "ImageDataFeature", "HeatmapDataFeature", "PresentFeature", diff --git a/fastplotlib/graphics/_features/_sizes.py b/fastplotlib/graphics/_features/_sizes.py index e69de29bb..377052918 100644 --- a/fastplotlib/graphics/_features/_sizes.py +++ b/fastplotlib/graphics/_features/_sizes.py @@ -0,0 +1,108 @@ +from typing import Any + +import numpy as np + +import pygfx + +from ._base import ( + GraphicFeatureIndexable, + cleanup_slice, + FeatureEvent, + to_gpu_supported_dtype, + cleanup_array_slice, +) + + +class PointsSizesFeature(GraphicFeatureIndexable): + """ + Access to the vertex buffer data shown in the graphic. + Supports fancy indexing if the data array also supports it. + """ + + def __init__(self, parent, sizes: Any, collection_index: int = None): + sizes = self._fix_sizes(sizes, parent) + super(PointsSizesFeature, self).__init__( + parent, sizes, collection_index=collection_index + ) + + @property + def buffer(self) -> pygfx.Buffer: + return self._parent.world_object.geometry.sizes + + def __getitem__(self, item): + return self.buffer.data[item] + + def _fix_sizes(self, sizes, parent): + graphic_type = parent.__class__.__name__ + + n_datapoints = parent.data().shape[0] + if not isinstance(sizes, (list, tuple, np.ndarray)): + sizes = np.full(n_datapoints, sizes, dtype=np.float32) # force it into a float to avoid weird gpu errors + elif not isinstance(sizes, np.ndarray): # if it's not a ndarray already, make it one + sizes = np.array(sizes, dtype=np.float32) # read it in as a numpy.float32 + if (sizes.ndim != 1) or (sizes.size != parent.data().shape[0]): + raise ValueError( + f"sequence of `sizes` must be 1 dimensional with " + f"the same length as the number of datapoints" + ) + + sizes = to_gpu_supported_dtype(sizes) + + if any(s < 0 for s in sizes): + raise ValueError("All sizes must be positive numbers greater than or equal to 0.0.") + + if sizes.ndim == 1: + if graphic_type == "ScatterGraphic": + sizes = np.array(sizes) + else: + raise ValueError(f"Sizes must be an array of shape (n,) where n == the number of data points provided.\ + Received shape={sizes.shape}.") + + return np.array(sizes) + + def __setitem__(self, key, value): + if isinstance(key, np.ndarray): + # make sure 1D array of int or boolean + key = cleanup_array_slice(key, self._upper_bound) + + # put sizes into right shape if they're only indexing datapoints + if isinstance(key, (slice, int, np.ndarray, np.integer)): + value = self._fix_sizes(value, self._parent) + # otherwise assume that they have the right shape + # numpy will throw errors if it can't broadcast + + if value.size != self.buffer.data[key].size: + raise ValueError(f"{value.size} is not equal to buffer size {self.buffer.data[key].size}.\ + If you want to set size to a non-scalar value, make sure it's the right length!") + + self.buffer.data[key] = value + self._update_range(key) + # avoid creating dicts constantly if there are no events to handle + if len(self._event_handlers) > 0: + self._feature_changed(key, value) + + def _update_range(self, key): + self._update_range_indices(key) + + def _feature_changed(self, key, new_data): + if key is not None: + key = cleanup_slice(key, self._upper_bound) + if isinstance(key, (int, np.integer)): + indices = [key] + elif isinstance(key, slice): + indices = range(key.start, key.stop, key.step) + elif isinstance(key, np.ndarray): + indices = key + elif key is None: + indices = None + + pick_info = { + "index": indices, + "collection-index": self._collection_index, + "world_object": self._parent.world_object, + "new_data": new_data, + } + + event_data = FeatureEvent(type="sizes", pick_info=pick_info) + + self._call_event_handlers(event_data) \ No newline at end of file diff --git a/fastplotlib/graphics/scatter.py b/fastplotlib/graphics/scatter.py index 9e162c57a..141db2af3 100644 --- a/fastplotlib/graphics/scatter.py +++ b/fastplotlib/graphics/scatter.py @@ -5,16 +5,16 @@ from ..utils import parse_cmap_values from ._base import Graphic -from ._features import PointsDataFeature, ColorFeature, CmapFeature +from ._features import PointsDataFeature, ColorFeature, CmapFeature, PointsSizesFeature class ScatterGraphic(Graphic): - feature_events = ("data", "colors", "cmap", "present") + feature_events = ("data", "sizes", "colors", "cmap", "present") def __init__( self, data: np.ndarray, - sizes: Union[int, np.ndarray, list] = 1, + sizes: Union[int, float, np.ndarray, list] = 1, colors: np.ndarray = "w", alpha: float = 1.0, cmap: str = None, @@ -86,24 +86,11 @@ def __init__( self, self.colors(), cmap_name=cmap, cmap_values=cmap_values ) - if isinstance(sizes, int): - sizes = np.full(self.data().shape[0], sizes, dtype=np.float32) - elif isinstance(sizes, np.ndarray): - if (sizes.ndim != 1) or (sizes.size != self.data().shape[0]): - raise ValueError( - f"numpy array of `sizes` must be 1 dimensional with " - f"the same length as the number of datapoints" - ) - elif isinstance(sizes, list): - if len(sizes) != self.data().shape[0]: - raise ValueError( - "list of `sizes` must have the same length as the number of datapoints" - ) - + self.sizes = PointsSizesFeature(self, sizes) super(ScatterGraphic, self).__init__(*args, **kwargs) world_object = pygfx.Points( - pygfx.Geometry(positions=self.data(), sizes=sizes, colors=self.colors()), + pygfx.Geometry(positions=self.data(), sizes=self.sizes(), colors=self.colors()), material=pygfx.PointsMaterial(vertex_colors=True, vertex_sizes=True), ) From 82d463616ab2eef30bce936f965fd7b6af2bda79 Mon Sep 17 00:00:00 2001 From: Kushal Kolar Date: Mon, 17 Jul 2023 13:37:01 -0500 Subject: [PATCH 02/13] fix examples so they can be run locally without offscreen canvas (#294) --- examples/desktop/line/line_colorslice.py | 1 - examples/desktop/line/line_dataslice.py | 1 - examples/desktop/line/line_present_scaling.py | 1 - examples/desktop/scatter/scatter.py | 1 - examples/desktop/scatter/scatter_cmap.py | 1 - examples/desktop/scatter/scatter_colorslice.py | 1 - examples/desktop/scatter/scatter_dataslice.py | 1 - examples/desktop/scatter/scatter_present.py | 1 - 8 files changed, 8 deletions(-) diff --git a/examples/desktop/line/line_colorslice.py b/examples/desktop/line/line_colorslice.py index f757a7efe..f2aca8125 100644 --- a/examples/desktop/line/line_colorslice.py +++ b/examples/desktop/line/line_colorslice.py @@ -62,7 +62,6 @@ plot.auto_scale() -img = np.asarray(plot.renderer.target.draw()) if __name__ == "__main__": print(__doc__) diff --git a/examples/desktop/line/line_dataslice.py b/examples/desktop/line/line_dataslice.py index ef3cccfe8..ea87ba552 100644 --- a/examples/desktop/line/line_dataslice.py +++ b/examples/desktop/line/line_dataslice.py @@ -51,7 +51,6 @@ plot.auto_scale() -img = np.asarray(plot.renderer.target.draw()) if __name__ == "__main__": print(__doc__) diff --git a/examples/desktop/line/line_present_scaling.py b/examples/desktop/line/line_present_scaling.py index b8e9be63c..327186c16 100644 --- a/examples/desktop/line/line_present_scaling.py +++ b/examples/desktop/line/line_present_scaling.py @@ -45,7 +45,6 @@ plot.auto_scale() -img = np.asarray(plot.renderer.target.draw()) if __name__ == "__main__": print(__doc__) diff --git a/examples/desktop/scatter/scatter.py b/examples/desktop/scatter/scatter.py index 243924035..778f37deb 100644 --- a/examples/desktop/scatter/scatter.py +++ b/examples/desktop/scatter/scatter.py @@ -28,7 +28,6 @@ plot.auto_scale() -img = np.asarray(plot.renderer.target.draw()) if __name__ == "__main__": print(__doc__) diff --git a/examples/desktop/scatter/scatter_cmap.py b/examples/desktop/scatter/scatter_cmap.py index ae113537a..edc55a4b1 100644 --- a/examples/desktop/scatter/scatter_cmap.py +++ b/examples/desktop/scatter/scatter_cmap.py @@ -41,7 +41,6 @@ scatter_graphic.cmap = "tab10" -# img = np.asarray(plot.renderer.target.draw()) if __name__ == "__main__": print(__doc__) diff --git a/examples/desktop/scatter/scatter_colorslice.py b/examples/desktop/scatter/scatter_colorslice.py index f5f32f5be..d752cacbd 100644 --- a/examples/desktop/scatter/scatter_colorslice.py +++ b/examples/desktop/scatter/scatter_colorslice.py @@ -33,7 +33,6 @@ scatter_graphic.colors[75:150] = "white" scatter_graphic.colors[::2] = "blue" -img = np.asarray(plot.renderer.target.draw()) if __name__ == "__main__": print(__doc__) diff --git a/examples/desktop/scatter/scatter_dataslice.py b/examples/desktop/scatter/scatter_dataslice.py index 7b80d6c9e..22c495bff 100644 --- a/examples/desktop/scatter/scatter_dataslice.py +++ b/examples/desktop/scatter/scatter_dataslice.py @@ -36,7 +36,6 @@ scatter_graphic.data[10:15] = scatter_graphic.data[0:5] + np.array([1, 1, 1]) scatter_graphic.data[50:100:2] = scatter_graphic.data[100:150:2] + np.array([1,1,0]) -img = np.asarray(plot.renderer.target.draw()) if __name__ == "__main__": print(__doc__) diff --git a/examples/desktop/scatter/scatter_present.py b/examples/desktop/scatter/scatter_present.py index fe0a3bf4f..ad4be837f 100644 --- a/examples/desktop/scatter/scatter_present.py +++ b/examples/desktop/scatter/scatter_present.py @@ -32,7 +32,6 @@ scatter_graphic.present = False -img = np.asarray(plot.renderer.target.draw()) if __name__ == "__main__": print(__doc__) From 36ed10ce5aad2a3a1e7824b8bf93cc9901658f94 Mon Sep 17 00:00:00 2001 From: Kushal Kolar Date: Thu, 24 Aug 2023 12:04:19 -0400 Subject: [PATCH 03/13] Update README.md add new overview gif, add link to scipy talk --- README.md | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index dccd8196b..ae03ea13b 100644 --- a/README.md +++ b/README.md @@ -4,13 +4,19 @@ [![Documentation Status](https://readthedocs.org/projects/fastplotlib/badge/?version=latest)](https://fastplotlib.readthedocs.io/en/latest/?badge=latest) [![Gitter](https://badges.gitter.im/fastplotlib/community.svg)](https://gitter.im/fastplotlib/community?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) -[**Installation**](https://github.com/kushalkolar/fastplotlib#installation) | [**GPU Drivers**](https://github.com/kushalkolar/fastplotlib#graphics-drivers) | [**Examples**](https://github.com/kushalkolar/fastplotlib#examples) | [**Contributing**](https://github.com/kushalkolar/fastplotlib#heart-contributing) +[**Installation**](https://github.com/kushalkolar/fastplotlib#installation) | +[**GPU Drivers**](https://github.com/kushalkolar/fastplotlib#graphics-drivers) | +[**Examples**](https://github.com/kushalkolar/fastplotlib#examples) | +[**Contributing**](https://github.com/kushalkolar/fastplotlib#heart-contributing) A fast plotting library built using the [`pygfx`](https://github.com/pygfx/pygfx) render engine utilizing [Vulkan](https://en.wikipedia.org/wiki/Vulkan), [DX12](https://en.wikipedia.org/wiki/DirectX#DirectX_12), or [Metal](https://developer.apple.com/metal/) via WGPU, so it is very fast! We also aim to be an expressive plotting library that enables rapid prototyping for large scale explorative scientific visualization. -![fpl_neuro_demo](https://github.com/kushalkolar/fastplotlib/assets/9403332/0bebe2fe-3c45-4da4-a026-9505751a4087) +![scipy-fpl](https://github.com/fastplotlib/fastplotlib/assets/9403332/b981a54c-05f9-443f-a8e4-52cd01cd802a) + +### SciPy Talk + +[![fpl_thumbnail](http://i3.ytimg.com/vi/Q-UJpAqljsU/hqdefault.jpg)](https://www.youtube.com/watch?v=Q-UJpAqljsU) -Higher resolution demo: [https://github.com/kushalkolar/fastplotlib/assets/9403332/1df06d4d-9a7e-4f0d-aad8-8d2e9b387647](https://github.com/kushalkolar/fastplotlib/assets/9403332/1df06d4d-9a7e-4f0d-aad8-8d2e9b387647) # Supported frameworks @@ -139,12 +145,6 @@ plot.show() ![out](https://user-images.githubusercontent.com/9403332/209422871-6b2153f3-81ca-4f62-9200-8206a81eaf0d.gif) -### Image widget - -Interactive visualization of large imaging datasets in the notebook. - -![zfish](https://user-images.githubusercontent.com/9403332/209711810-abdb7d1d-81ce-4874-80f5-082efa2c421d.gif) - ## Graphics drivers You will need a relatively modern GPU (newer integrated GPUs in CPUs are usually fine). Generally if your GPU is from 2017 or later it should be fine. From 2b97c9d88714551977e78496f347ff9307dda289 Mon Sep 17 00:00:00 2001 From: Kushal Kolar Date: Mon, 11 Sep 2023 17:03:57 -0400 Subject: [PATCH 04/13] auto connect a ipywidget slider to a linear selector (#298) * auto connect a ipywidget slider to a linear selector, not yet tested * add _moving attr, smooth bidirectional link between linear selector and ipywidget sliders * update linear selector example nb * ipywidget connector for LinearRegionSelector, limits is a settable property for linear and linearregion * comments --- .../notebooks/linear_region_selector.ipynb | 26 ++- examples/notebooks/linear_selector.ipynb | 26 ++- .../graphics/_features/_selection_features.py | 12 +- .../graphics/selectors/_base_selector.py | 5 + fastplotlib/graphics/selectors/_linear.py | 129 ++++++++++---- .../graphics/selectors/_linear_region.py | 158 +++++++++++++++++- 6 files changed, 300 insertions(+), 56 deletions(-) diff --git a/examples/notebooks/linear_region_selector.ipynb b/examples/notebooks/linear_region_selector.ipynb index 11cd3a490..f252e6f6f 100644 --- a/examples/notebooks/linear_region_selector.ipynb +++ b/examples/notebooks/linear_region_selector.ipynb @@ -19,7 +19,7 @@ "source": [ "import fastplotlib as fpl\n", "import numpy as np\n", - "\n", + "from ipywidgets import IntRangeSlider, FloatRangeSlider, VBox\n", "\n", "gp = fpl.GridPlot((2, 2))\n", "\n", @@ -83,7 +83,27 @@ "ls_x.selection.add_event_handler(set_zoom_x)\n", "ls_y.selection.add_event_handler(set_zoom_y)\n", "\n", - "gp.show()" + "# make some ipywidget sliders too\n", + "# these are not necessary, it's just to show how they can be connected\n", + "x_range_slider = IntRangeSlider(\n", + " value=ls_x.selection(),\n", + " min=ls_x.limits[0],\n", + " max=ls_x.limits[1],\n", + " description=\"x\"\n", + ")\n", + "\n", + "y_range_slider = FloatRangeSlider(\n", + " value=ls_y.selection(),\n", + " min=ls_y.limits[0],\n", + " max=ls_y.limits[1],\n", + " description=\"x\"\n", + ")\n", + "\n", + "# connect the region selector to the ipywidget slider\n", + "ls_x.add_ipywidget_handler(x_range_slider, step=5)\n", + "ls_y.add_ipywidget_handler(y_range_slider, step=0.1)\n", + "\n", + "VBox([gp.show(), x_range_slider, y_range_slider])" ] }, { @@ -281,7 +301,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.11.3" + "version": "3.11.4" } }, "nbformat": 4, diff --git a/examples/notebooks/linear_selector.ipynb b/examples/notebooks/linear_selector.ipynb index a4d6b97ea..a67a30e98 100644 --- a/examples/notebooks/linear_selector.ipynb +++ b/examples/notebooks/linear_selector.ipynb @@ -21,7 +21,7 @@ "from fastplotlib.graphics.selectors import Synchronizer\n", "\n", "import numpy as np\n", - "from ipywidgets import VBox\n", + "from ipywidgets import VBox, IntSlider, FloatSlider\n", "\n", "plot = fpl.Plot()\n", "\n", @@ -49,22 +49,18 @@ "\n", "# fastplotlib LineSelector can make an ipywidget slider and return it :D \n", "ipywidget_slider = selector.make_ipywidget_slider()\n", + "ipywidget_slider.description = \"slider1\"\n", + "\n", + "# or you can make your own ipywidget sliders and connect them to the linear selector\n", + "ipywidget_slider2 = IntSlider(min=0, max=100, description=\"slider2\")\n", + "ipywidget_slider3 = FloatSlider(min=0, max=100, description=\"slider3\")\n", + "\n", + "selector2.add_ipywidget_handler(ipywidget_slider2, step=5)\n", + "selector3.add_ipywidget_handler(ipywidget_slider3, step=0.1)\n", "\n", "plot.auto_scale()\n", "plot.show()\n", - "VBox([plot.show(), ipywidget_slider])" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "a632c8ee-2d4c-44fc-9391-7b2880223fdb", - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "selector.step = 0.1" + "VBox([plot.show(), ipywidget_slider, ipywidget_slider2, ipywidget_slider3])" ] }, { @@ -135,7 +131,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.11.3" + "version": "3.11.4" } }, "nbformat": 4, diff --git a/fastplotlib/graphics/_features/_selection_features.py b/fastplotlib/graphics/_features/_selection_features.py index ae486026e..5f161562f 100644 --- a/fastplotlib/graphics/_features/_selection_features.py +++ b/fastplotlib/graphics/_features/_selection_features.py @@ -150,14 +150,14 @@ class LinearSelectionFeature(GraphicFeature): def __init__(self, parent, axis: str, value: float, limits: Tuple[int, int]): super(LinearSelectionFeature, self).__init__(parent, data=value) - self.axis = axis - self.limits = limits + self._axis = axis + self._limits = limits def _set(self, value: float): - if not (self.limits[0] <= value <= self.limits[1]): + if not (self._limits[0] <= value <= self._limits[1]): return - if self.axis == "x": + if self._axis == "x": self._parent.position_x = value else: self._parent.position_y = value @@ -219,7 +219,7 @@ def __init__( super(LinearRegionSelectionFeature, self).__init__(parent, data=selection) self._axis = axis - self.limits = limits + self._limits = limits self._set(selection) @@ -238,7 +238,7 @@ def _set(self, value: Tuple[float, float]): # make sure bounds not exceeded for v in value: - if not (self.limits[0] <= v <= self.limits[1]): + if not (self._limits[0] <= v <= self._limits[1]): return # make sure `selector width >= 2`, left edge must not move past right edge! diff --git a/fastplotlib/graphics/selectors/_base_selector.py b/fastplotlib/graphics/selectors/_base_selector.py index a4159c194..da7ba36ec 100644 --- a/fastplotlib/graphics/selectors/_base_selector.py +++ b/fastplotlib/graphics/selectors/_base_selector.py @@ -80,6 +80,9 @@ def __init__( self._move_info: MoveInfo = None + # sets to `True` on "pointer_down", sets to `False` on "pointer_up" + self._moving = False #: indicates if the selector is currently being moved + # used to disable fill area events if the edge is being actively hovered # otherwise annoying and requires too much accuracy to move just an edge self._edge_hovered: bool = False @@ -189,6 +192,7 @@ def _move_start(self, event_source: WorldObject, ev): last_position = self._plot_area.map_screen_to_world(ev) self._move_info = MoveInfo(last_position=last_position, source=event_source) + self._moving = True def _move(self, ev): """ @@ -231,6 +235,7 @@ def _move_graphic(self, delta: np.ndarray): def _move_end(self, ev): self._move_info = None + self._moving = False self._plot_area.controller.enabled = True def _move_to_pointer(self, ev): diff --git a/fastplotlib/graphics/selectors/_linear.py b/fastplotlib/graphics/selectors/_linear.py index 39710305d..951e353d3 100644 --- a/fastplotlib/graphics/selectors/_linear.py +++ b/fastplotlib/graphics/selectors/_linear.py @@ -1,5 +1,6 @@ from typing import * import math +from numbers import Real import numpy as np @@ -18,6 +19,21 @@ class LinearSelector(Graphic, BaseSelector): + @property + def limits(self) -> Tuple[float, float]: + return self._limits + + @limits.setter + def limits(self, values: Tuple[float, float]): + # check that `values` is an iterable of two real numbers + # using `Real` here allows it to work with builtin `int` and `float` types, and numpy scaler types + if len(values) != 2 or not all(map(lambda v: isinstance(v, Real), values)): + raise TypeError( + "limits must be an iterable of two numeric values" + ) + self._limits = tuple(map(round, values)) # if values are close to zero things get weird so round them + self.selection._limits = self._limits + # TODO: make `selection` arg in graphics data space not world space def __init__( self, @@ -27,7 +43,6 @@ def __init__( parent: Graphic = None, end_points: Tuple[int, int] = None, arrow_keys_modifier: str = "Shift", - ipywidget_slider=None, thickness: float = 2.5, color: Any = "w", name: str = None, @@ -57,9 +72,6 @@ def __init__( "Control", "Shift", "Alt" or ``None``. Double click on the selector first to enable the arrow key movements, or set the attribute ``arrow_key_events_enabled = True`` - ipywidget_slider: IntSlider, optional - ipywidget slider to associate with this graphic - thickness: float, default 2.5 thickness of the slider @@ -84,7 +96,8 @@ def __init__( if len(limits) != 2: raise ValueError("limits must be a tuple of 2 integers, i.e. (int, int)") - limits = tuple(map(round, limits)) + self._limits = tuple(map(round, limits)) + selection = round(selection) if axis == "x": @@ -141,14 +154,9 @@ def __init__( self.position_y = selection self.selection = LinearSelectionFeature( - self, axis=axis, value=selection, limits=limits + self, axis=axis, value=selection, limits=self._limits ) - self.ipywidget_slider = ipywidget_slider - - if self.ipywidget_slider is not None: - self._setup_ipywidget_slider(ipywidget_slider) - self._move_info: dict = None self._pygfx_event = None @@ -156,6 +164,8 @@ def __init__( self._block_ipywidget_call = False + self._handled_widgets = list() + # init base selector BaseSelector.__init__( self, @@ -166,21 +176,41 @@ def __init__( ) def _setup_ipywidget_slider(self, widget): - # setup ipywidget slider with callbacks to this LinearSelector - widget.value = int(self.selection()) + # setup an ipywidget slider with bidirectional callbacks to this LinearSelector + value = self.selection() + + if isinstance(widget, ipywidgets.IntSlider): + value = int(value) + + widget.value = value + + # user changes widget -> linear selection changes widget.observe(self._ipywidget_callback, "value") - self.selection.add_event_handler(self._update_ipywidget) + + # user changes linear selection -> widget changes + self.selection.add_event_handler(self._update_ipywidgets) + self._plot_area.renderer.add_event_handler(self._set_slider_layout, "resize") - def _update_ipywidget(self, ev): - # update the ipywidget slider value when LinearSelector value changes - self._block_ipywidget_call = True - self.ipywidget_slider.value = int(ev.pick_info["new_data"]) + self._handled_widgets.append(widget) + + def _update_ipywidgets(self, ev): + # update the ipywidget sliders when LinearSelector value changes + self._block_ipywidget_call = True # prevent infinite recursion + + value = ev.pick_info["new_data"] + # update all the handled slider widgets + for widget in self._handled_widgets: + if isinstance(widget, ipywidgets.IntSlider): + widget.value = int(value) + else: + widget.value = value + self._block_ipywidget_call = False def _ipywidget_callback(self, change): # update the LinearSelector if the ipywidget value changes - if self._block_ipywidget_call: + if self._block_ipywidget_call or self._moving: return self.selection = change["new"] @@ -188,7 +218,8 @@ def _ipywidget_callback(self, change): def _set_slider_layout(self, *args): w, h = self._plot_area.renderer.logical_size - self.ipywidget_slider.layout = ipywidgets.Layout(width=f"{w}px") + for widget in self._handled_widgets: + widget.layout = ipywidgets.Layout(width=f"{w}px") def make_ipywidget_slider(self, kind: str = "IntSlider", **kwargs): """ @@ -197,7 +228,7 @@ def make_ipywidget_slider(self, kind: str = "IntSlider", **kwargs): Parameters ---------- kind: str - "IntSlider" or "FloatSlider" + "IntSlider", "FloatSlider" or "FloatLogSlider" kwargs passed to the ipywidget slider constructor @@ -207,28 +238,68 @@ def make_ipywidget_slider(self, kind: str = "IntSlider", **kwargs): ipywidgets.Intslider or ipywidgets.FloatSlider """ - if self.ipywidget_slider is not None: - raise AttributeError("Already has ipywidget slider") if not HAS_IPYWIDGETS: raise ImportError( "Must installed `ipywidgets` to use `make_ipywidget_slider()`" ) + if kind not in ["IntSlider", "FloatSlider", "FloatLogSlider"]: + raise TypeError( + f"`kind` must be one of: 'IntSlider', 'FloatSlider' or 'FloatLogSlider'\n" + f"You have passed: '{kind}'" + ) + cls = getattr(ipywidgets, kind) + value = self.selection() + if "Int" in kind: + value = int(self.selection()) + slider = cls( - min=self.selection.limits[0], - max=self.selection.limits[1], - value=int(self.selection()), - step=1, + min=self.limits[0], + max=self.limits[1], + value=value, **kwargs, ) - self.ipywidget_slider = slider - self._setup_ipywidget_slider(slider) + self.add_ipywidget_handler(slider) return slider + def add_ipywidget_handler( + self, + widget, + step: Union[int, float] = None + ): + """ + Bidirectionally connect events with a ipywidget slider + + Parameters + ---------- + widget: ipywidgets.IntSlider, ipywidgets.FloatSlider, or ipywidgets.FloatLogSlider + ipywidget slider to connect to + + step: int or float, default ``None`` + step size, if ``None`` 100 steps are created + + """ + + if not isinstance(widget, (ipywidgets.IntSlider, ipywidgets.FloatSlider, ipywidgets.FloatLogSlider)): + raise TypeError( + f"`widget` must be one of: ipywidgets.IntSlider, ipywidgets.FloatSlider, or ipywidgets.FloatLogSlider\n" + f"You have passed a: <{type(widget)}" + ) + + if step is None: + step = (self.limits[1] - self.limits[0]) / 100 + + if isinstance(widget, ipywidgets.IntSlider): + step = int(step) + + widget.step = step + + self._setup_ipywidget_slider(widget) + def get_selected_index(self, graphic: Graphic = None) -> Union[int, List[int]]: """ Data index the slider is currently at w.r.t. the Graphic data. With LineGraphic data, the geometry x or y diff --git a/fastplotlib/graphics/selectors/_linear_region.py b/fastplotlib/graphics/selectors/_linear_region.py index 0759cd4fc..8579ad6d0 100644 --- a/fastplotlib/graphics/selectors/_linear_region.py +++ b/fastplotlib/graphics/selectors/_linear_region.py @@ -1,4 +1,13 @@ from typing import * +from numbers import Real + +try: + import ipywidgets + + HAS_IPYWIDGETS = True +except (ImportError, ModuleNotFoundError): + HAS_IPYWIDGETS = False + import numpy as np import pygfx @@ -9,6 +18,21 @@ class LinearRegionSelector(Graphic, BaseSelector): + @property + def limits(self) -> Tuple[float, float]: + return self._limits + + @limits.setter + def limits(self, values: Tuple[float, float]): + # check that `values` is an iterable of two real numbers + # using `Real` here allows it to work with builtin `int` and `float` types, and numpy scaler types + if len(values) != 2 or not all(map(lambda v: isinstance(v, Real), values)): + raise TypeError( + "limits must be an iterable of two numeric values" + ) + self._limits = tuple(map(round, values)) # if values are close to zero things get weird so round them + self.selection._limits = self._limits + def __init__( self, bounds: Tuple[int, int], @@ -81,9 +105,9 @@ def __init__( """ - # lots of very close to zero values etc. so round them + # lots of very close to zero values etc. so round them, otherwise things get weird bounds = tuple(map(round, bounds)) - limits = tuple(map(round, limits)) + self._limits = tuple(map(round, limits)) origin = tuple(map(round, origin)) # TODO: sanity checks, we recommend users to add LinearSelection using the add_linear_selector() methods @@ -203,9 +227,13 @@ def __init__( # set the initial bounds of the selector self.selection = LinearRegionSelectionFeature( - self, bounds, axis=axis, limits=limits + self, bounds, axis=axis, limits=self._limits ) + self._handled_widgets = list() + self._block_ipywidget_call = False + self._pygfx_event = None + BaseSelector.__init__( self, edges=self.edges, @@ -341,6 +369,130 @@ def get_selected_indices( ixs = np.arange(*self.selection(), dtype=int) return ixs + def make_ipywidget_slider(self, kind: str = "IntRangeSlider", **kwargs): + """ + Makes and returns an ipywidget slider that is associated to this LinearSelector + + Parameters + ---------- + kind: str + "IntRangeSlider" or "FloatRangeSlider" + + kwargs + passed to the ipywidget slider constructor + + Returns + ------- + ipywidgets.Intslider or ipywidgets.FloatSlider + + """ + + if not HAS_IPYWIDGETS: + raise ImportError( + "Must installed `ipywidgets` to use `make_ipywidget_slider()`" + ) + + if kind not in ["IntRangeSlider", "FloatRangeSlider"]: + raise TypeError( + f"`kind` must be one of: 'IntRangeSlider', or 'FloatRangeSlider'\n" + f"You have passed: '{kind}'" + ) + + cls = getattr(ipywidgets, kind) + + value = self.selection() + if "Int" in kind: + value = tuple(map(int, self.selection())) + + slider = cls( + min=self.limits[0], + max=self.limits[1], + value=value, + **kwargs, + ) + self.add_ipywidget_handler(slider) + + return slider + + def add_ipywidget_handler( + self, + widget, + step: Union[int, float] = None + ): + """ + Bidirectionally connect events with a ipywidget slider + + Parameters + ---------- + widget: ipywidgets.IntRangeSlider or ipywidgets.FloatRangeSlider + ipywidget slider to connect to + + step: int or float, default ``None`` + step size, if ``None`` 100 steps are created + + """ + if not isinstance(widget, (ipywidgets.IntRangeSlider, ipywidgets.FloatRangeSlider)): + raise TypeError( + f"`widget` must be one of: ipywidgets.IntRangeSlider or ipywidgets.FloatRangeSlider\n" + f"You have passed a: <{type(widget)}" + ) + + if step is None: + step = (self.limits[1] - self.limits[0]) / 100 + + if isinstance(widget, ipywidgets.IntSlider): + step = int(step) + + widget.step = step + + self._setup_ipywidget_slider(widget) + + def _setup_ipywidget_slider(self, widget): + # setup an ipywidget slider with bidirectional callbacks to this LinearSelector + value = self.selection() + + if isinstance(widget, ipywidgets.IntSlider): + value = tuple(map(int, value)) + + widget.value = value + + # user changes widget -> linear selection changes + widget.observe(self._ipywidget_callback, "value") + + # user changes linear selection -> widget changes + self.selection.add_event_handler(self._update_ipywidgets) + + self._plot_area.renderer.add_event_handler(self._set_slider_layout, "resize") + + self._handled_widgets.append(widget) + + def _update_ipywidgets(self, ev): + # update the ipywidget sliders when LinearSelector value changes + self._block_ipywidget_call = True # prevent infinite recursion + + value = ev.pick_info["new_data"] + # update all the handled slider widgets + for widget in self._handled_widgets: + if isinstance(widget, ipywidgets.IntSlider): + widget.value = tuple(map(int, value)) + else: + widget.value = value + + self._block_ipywidget_call = False + + def _ipywidget_callback(self, change): + # update the LinearSelector if the ipywidget value changes + if self._block_ipywidget_call or self._moving: + return + + self.selection = change["new"] + + def _set_slider_layout(self, *args): + w, h = self._plot_area.renderer.logical_size + + for widget in self._handled_widgets: + widget.layout = ipywidgets.Layout(width=f"{w}px") + def _move_graphic(self, delta: np.ndarray): # add delta to current bounds to get new positions if self.selection.axis == "x": From a363713abfa4b9b09209d15f892ca48f456badc9 Mon Sep 17 00:00:00 2001 From: Kushal Kolar Date: Mon, 18 Sep 2023 05:48:54 -0400 Subject: [PATCH 05/13] Selector fixes (#304) * bugfix for axis=y, use round() instead of int() in get_selected_index(), more accurate * add _pygfx_event = None to BaseSelector init * fix type annotation for Synchronizer --- fastplotlib/graphics/selectors/_base_selector.py | 2 ++ fastplotlib/graphics/selectors/_linear.py | 11 +++++------ fastplotlib/graphics/selectors/_sync.py | 3 ++- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/fastplotlib/graphics/selectors/_base_selector.py b/fastplotlib/graphics/selectors/_base_selector.py index da7ba36ec..2b1a2aa0d 100644 --- a/fastplotlib/graphics/selectors/_base_selector.py +++ b/fastplotlib/graphics/selectors/_base_selector.py @@ -87,6 +87,8 @@ def __init__( # otherwise annoying and requires too much accuracy to move just an edge self._edge_hovered: bool = False + self._pygfx_event = None + def get_selected_index(self): """Not implemented for this selector""" raise NotImplementedError diff --git a/fastplotlib/graphics/selectors/_linear.py b/fastplotlib/graphics/selectors/_linear.py index 951e353d3..c00bebcc7 100644 --- a/fastplotlib/graphics/selectors/_linear.py +++ b/fastplotlib/graphics/selectors/_linear.py @@ -107,8 +107,8 @@ def __init__( line_data = np.column_stack([xs, ys, zs]) elif axis == "y": - xs = np.zeros(end_points) - ys = np.array(2) + xs = np.array(end_points) + ys = np.zeros(2) zs = np.zeros(2) line_data = np.column_stack([xs, ys, zs]) @@ -158,7 +158,6 @@ def __init__( ) self._move_info: dict = None - self._pygfx_event = None self.parent = parent @@ -349,9 +348,9 @@ def _get_selected_index(self, graphic): or math.fabs(find_value - geo_positions[idx - 1]) < math.fabs(find_value - geo_positions[idx]) ): - return int(idx - 1) + return round(idx - 1) else: - return int(idx) + return round(idx) if ( "Heatmap" in graphic.__class__.__name__ @@ -359,7 +358,7 @@ def _get_selected_index(self, graphic): ): # indices map directly to grid geometry for image data buffer index = self.selection() - offset - return int(index) + return round(index) def _move_graphic(self, delta: np.ndarray): """ diff --git a/fastplotlib/graphics/selectors/_sync.py b/fastplotlib/graphics/selectors/_sync.py index b01823394..499f05449 100644 --- a/fastplotlib/graphics/selectors/_sync.py +++ b/fastplotlib/graphics/selectors/_sync.py @@ -1,8 +1,9 @@ from . import LinearSelector +from typing import * class Synchronizer: - def __init__(self, *selectors: LinearSelector, key_bind: str = "Shift"): + def __init__(self, *selectors: LinearSelector, key_bind: Union[str, None] = "Shift"): """ Synchronize the movement of `Selectors`. Selectors will move in sync only when the selected `"key_bind"` is used during the mouse movement event. Valid key binds are: ``"Control"``, ``"Shift"`` and ``"Alt"``. From f7fa6451620d5c0796bea205181066a7ae59924b Mon Sep 17 00:00:00 2001 From: Kian-Meng Ang Date: Tue, 19 Sep 2023 10:41:55 +0800 Subject: [PATCH 06/13] Fix typos (#303) * Fix typos Found via `codespell -L nwo,fo,te,ue,nd,ned,noo,bumb,bu,tbe,morg` * Update CODE_OF_CONDUCT.md --------- Co-authored-by: Kushal Kolar --- docs/source/quickstart.ipynb | 4 ++-- examples/notebooks/simple.ipynb | 4 ++-- fastplotlib/graphics/_features/_present.py | 2 +- fastplotlib/graphics/selectors/_sync.py | 2 +- fastplotlib/layouts/_base.py | 2 +- fastplotlib/widgets/image.py | 4 ++-- 6 files changed, 9 insertions(+), 9 deletions(-) diff --git a/docs/source/quickstart.ipynb b/docs/source/quickstart.ipynb index aebe04b25..6a3afec33 100644 --- a/docs/source/quickstart.ipynb +++ b/docs/source/quickstart.ipynb @@ -599,7 +599,7 @@ "plot_v.add_image(data=data, name=\"random-image\")\n", "\n", "# a function to update the image_graphic\n", - "# a plot will pass its plot instance to the animation function as an arugment\n", + "# a plot will pass its plot instance to the animation function as an argument\n", "def update_data(plot_instance):\n", " new_data = np.random.rand(512, 512)\n", " plot_instance[\"random-image\"].data = new_data\n", @@ -1073,7 +1073,7 @@ "\n", "plot_l.add_image(img, name=\"image\", cmap=\"gray\")\n", "\n", - "# z axix position -1 so it is below all the lines\n", + "# z axis position -1 so it is below all the lines\n", "plot_l[\"image\"].position_z = -1\n", "plot_l[\"image\"].position_x = -50" ] diff --git a/examples/notebooks/simple.ipynb b/examples/notebooks/simple.ipynb index e994bfba8..69c11d47c 100644 --- a/examples/notebooks/simple.ipynb +++ b/examples/notebooks/simple.ipynb @@ -533,7 +533,7 @@ "plot_v.add_image(data=data, name=\"random-image\")\n", "\n", "# a function to update the image_graphic\n", - "# a plot will pass its plot instance to the animation function as an arugment\n", + "# a plot will pass its plot instance to the animation function as an argument\n", "def update_data(plot_instance):\n", " new_data = np.random.rand(512, 512)\n", " plot_instance[\"random-image\"].data = new_data\n", @@ -952,7 +952,7 @@ "\n", "plot_l.add_image(img[::20, ::20], name=\"image\", cmap=\"gray\")\n", "\n", - "# z axix position -1 so it is below all the lines\n", + "# z axis position -1 so it is below all the lines\n", "plot_l[\"image\"].position_z = -1\n", "plot_l[\"image\"].position_x = -8\n", "plot_l[\"image\"].position_y = -8" diff --git a/fastplotlib/graphics/_features/_present.py b/fastplotlib/graphics/_features/_present.py index ba257e60b..b0bb627c5 100644 --- a/fastplotlib/graphics/_features/_present.py +++ b/fastplotlib/graphics/_features/_present.py @@ -38,7 +38,7 @@ def _set(self, present: bool): if i > 100: raise RecursionError( - "Exceded scene graph depth threshold, cannot find Scene associated with" + "Exceeded scene graph depth threshold, cannot find Scene associated with" "this graphic." ) diff --git a/fastplotlib/graphics/selectors/_sync.py b/fastplotlib/graphics/selectors/_sync.py index 499f05449..8ba7dfd97 100644 --- a/fastplotlib/graphics/selectors/_sync.py +++ b/fastplotlib/graphics/selectors/_sync.py @@ -75,7 +75,7 @@ def _move_selectors(self, source, delta): for s in self.selectors: # must use == and not is to compare Graphics because they are weakref proxies! if s == source: - # if it's the source, since it has already movied + # if it's the source, since it has already moved continue s._move_graphic(delta) diff --git a/fastplotlib/layouts/_base.py b/fastplotlib/layouts/_base.py index 69f50800e..c5dcb0581 100644 --- a/fastplotlib/layouts/_base.py +++ b/fastplotlib/layouts/_base.py @@ -40,7 +40,7 @@ def __init__( ): """ Base class for plot creation and management. ``PlotArea`` is not intended to be instantiated by users - but rather to provide functionallity for ``subplot`` in ``gridplot`` and single ``plot``. + but rather to provide functionality for ``subplot`` in ``gridplot`` and single ``plot``. Parameters ---------- diff --git a/fastplotlib/widgets/image.py b/fastplotlib/widgets/image.py index 962a94151..62cba0da8 100644 --- a/fastplotlib/widgets/image.py +++ b/fastplotlib/widgets/image.py @@ -300,7 +300,7 @@ def __init__( if names is not None: if not all([isinstance(n, str) for n in names]): raise TypeError( - "optinal argument `names` must be a list of str" + "optional argument `names` must be a list of str" ) if len(names) != len(self.data): @@ -350,7 +350,7 @@ def __init__( # dict of {array_ix: dims_order_str} for data_ix in list(dims_order.keys()): if not isinstance(data_ix, int): - raise TypeError("`dims_oder` dict keys must be ") + raise TypeError("`dims_order` dict keys must be ") if len(dims_order[data_ix]) != self.ndim: raise ValueError( f"number of dims '{len(dims_order)} passed to `dims_order` " From 4fbf6ac6338cd5217757a24e902cfc99abe2620e Mon Sep 17 00:00:00 2001 From: Kushal Kolar Date: Wed, 20 Sep 2023 20:37:43 -0400 Subject: [PATCH 07/13] make set_feature, reset_feature public (#308) --- fastplotlib/graphics/_base.py | 8 ++++---- fastplotlib/graphics/image.py | 8 ++++---- fastplotlib/graphics/line.py | 6 +++--- fastplotlib/graphics/line_collection.py | 6 +++--- 4 files changed, 14 insertions(+), 14 deletions(-) diff --git a/fastplotlib/graphics/_base.py b/fastplotlib/graphics/_base.py index d30f7175f..d145821e4 100644 --- a/fastplotlib/graphics/_base.py +++ b/fastplotlib/graphics/_base.py @@ -166,11 +166,11 @@ class Interaction(ABC): """Mixin class that makes graphics interactive""" @abstractmethod - def _set_feature(self, feature: str, new_data: Any, indices: Any): + def set_feature(self, feature: str, new_data: Any, indices: Any): pass @abstractmethod - def _reset_feature(self, feature: str): + def reset_feature(self, feature: str): pass def link( @@ -312,14 +312,14 @@ def _event_handler(self, event): # the real world object in the pick_info and not the proxy if wo is event.pick_info["world_object"]: indices = i - target_info.target._set_feature( + target_info.target.set_feature( feature=target_info.feature, new_data=target_info.new_data, indices=indices, ) else: # if target is a single graphic, then indices do not matter - target_info.target._set_feature( + target_info.target.set_feature( feature=target_info.feature, new_data=target_info.new_data, indices=None, diff --git a/fastplotlib/graphics/image.py b/fastplotlib/graphics/image.py index d60fa36b2..121134de5 100644 --- a/fastplotlib/graphics/image.py +++ b/fastplotlib/graphics/image.py @@ -304,10 +304,10 @@ def __init__( # set it with the actual data self.data = data - def _set_feature(self, feature: str, new_data: Any, indices: Any): + def set_feature(self, feature: str, new_data: Any, indices: Any): pass - def _reset_feature(self, feature: str): + def reset_feature(self, feature: str): pass @@ -500,8 +500,8 @@ def vmax(self, value: float): """Maximum contrast limit.""" self._material.clim = (self._material.clim[0], value) - def _set_feature(self, feature: str, new_data: Any, indices: Any): + def set_feature(self, feature: str, new_data: Any, indices: Any): pass - def _reset_feature(self, feature: str): + def reset_feature(self, feature: str): pass diff --git a/fastplotlib/graphics/line.py b/fastplotlib/graphics/line.py index aeeeea3b0..fb7e38e62 100644 --- a/fastplotlib/graphics/line.py +++ b/fastplotlib/graphics/line.py @@ -281,11 +281,11 @@ def _get_linear_selector_init_args(self, padding: float, **kwargs): def _add_plot_area_hook(self, plot_area): self._plot_area = plot_area - def _set_feature(self, feature: str, new_data: Any, indices: Any = None): + def set_feature(self, feature: str, new_data: Any, indices: Any = None): if not hasattr(self, "_previous_data"): self._previous_data = dict() elif hasattr(self, "_previous_data"): - self._reset_feature(feature) + self.reset_feature(feature) feature_instance = getattr(self, feature) if indices is not None: @@ -302,7 +302,7 @@ def _set_feature(self, feature: str, new_data: Any, indices: Any = None): data=previous, indices=indices ) - def _reset_feature(self, feature: str): + def reset_feature(self, feature: str): if feature not in self._previous_data.keys(): return diff --git a/fastplotlib/graphics/line_collection.py b/fastplotlib/graphics/line_collection.py index 06f260ee7..062c5ba91 100644 --- a/fastplotlib/graphics/line_collection.py +++ b/fastplotlib/graphics/line_collection.py @@ -415,7 +415,7 @@ def _get_linear_selector_init_args(self, padding, **kwargs): def _add_plot_area_hook(self, plot_area): self._plot_area = plot_area - def _set_feature(self, feature: str, new_data: Any, indices: Any): + def set_feature(self, feature: str, new_data: Any, indices: Any): # if single value force to be an array of size 1 if isinstance(indices, (np.integer, int)): indices = np.array([indices]) @@ -429,7 +429,7 @@ def _set_feature(self, feature: str, new_data: Any, indices: Any): if self._previous_data[feature].indices == indices: return # nothing to change, and this allows bidirectional linking without infinite recursion - self._reset_feature(feature) + self.reset_feature(feature) # coll_feature = getattr(self[indices], feature) @@ -455,7 +455,7 @@ def _set_feature(self, feature: str, new_data: Any, indices: Any): # since calling `feature._set()` triggers all the feature callbacks feature_instance._set(new_data) - def _reset_feature(self, feature: str): + def reset_feature(self, feature: str): if feature not in self._previous_data.keys(): return From 0f5655ce9fa27db3a70012d54d3e726369cd1af7 Mon Sep 17 00:00:00 2001 From: Caitlin Lewis <69729525+clewis7@users.noreply.github.com> Date: Thu, 21 Sep 2023 12:45:14 -0400 Subject: [PATCH 08/13] fix flip button (#307) * fix flip, update simple notebook * change flip icon to arrow in direction of y-axis * change tooltip --- examples/notebooks/simple.ipynb | 6 +++--- fastplotlib/layouts/_gridplot.py | 10 +++++++--- fastplotlib/layouts/_plot.py | 8 ++++++-- 3 files changed, 16 insertions(+), 8 deletions(-) diff --git a/examples/notebooks/simple.ipynb b/examples/notebooks/simple.ipynb index 69c11d47c..1aeeef560 100644 --- a/examples/notebooks/simple.ipynb +++ b/examples/notebooks/simple.ipynb @@ -108,7 +108,7 @@ "source": [ "**Use the handle on the bottom right corner of the _canvas_ to resize it. You can also pan and zoom using your mouse!**\n", "\n", - "By default the origin is on the bottom left, you can click the flip button to flip the y-axis, or use `plot.camera.world.scale_y *= -1`" + "By default the origin is on the bottom left, you can click the flip button to flip the y-axis, or use `plot.camera.local.scale_y *= -1`" ] }, { @@ -120,7 +120,7 @@ }, "outputs": [], "source": [ - "plot.camera.world.scale_y *= -1" + "plot.camera.local.scale_y *= -1" ] }, { @@ -464,7 +464,7 @@ }, "outputs": [], "source": [ - "plot_rgb.camera.world.scale_y *= -1" + "plot_rgb.camera.local.scale_y *= -1" ] }, { diff --git a/fastplotlib/layouts/_gridplot.py b/fastplotlib/layouts/_gridplot.py index b339e8659..f52c40d1b 100644 --- a/fastplotlib/layouts/_gridplot.py +++ b/fastplotlib/layouts/_gridplot.py @@ -415,9 +415,9 @@ def __init__(self, plot: GridPlot): self.flip_camera_button = Button( value=False, disabled=False, - icon="arrows-v", + icon="arrow-up", layout=Layout(width="auto"), - tooltip="flip", + tooltip="y-axis direction", ) self.record_button = ToggleButton( @@ -490,7 +490,11 @@ def maintain_aspect(self, obj): def flip_camera(self, obj): current = self.current_subplot - current.camera.world.scale_y *= -1 + current.camera.local.scale_y *= -1 + if current.camera.local.scale_y == -1: + self.flip_camera_button.icon = "arrow-down" + else: + self.flip_camera_button.icon = "arrow-up" def update_current_subplot(self, ev): for subplot in self.plot: diff --git a/fastplotlib/layouts/_plot.py b/fastplotlib/layouts/_plot.py index 1f91bb303..d529ef5f5 100644 --- a/fastplotlib/layouts/_plot.py +++ b/fastplotlib/layouts/_plot.py @@ -170,7 +170,7 @@ def __init__(self, plot: Plot): self.flip_camera_button = Button( value=False, disabled=False, - icon="arrows-v", + icon="arrow-up", layout=Layout(width="auto"), tooltip="flip", ) @@ -224,7 +224,11 @@ def maintain_aspect(self, obj): self.plot.camera.maintain_aspect = self.maintain_aspect_button.value def flip_camera(self, obj): - self.plot.camera.world.scale_y *= -1 + self.plot.camera.local.scale_y *= -1 + if self.plot.camera.local.scale_y == -1: + self.flip_camera_button.icon = "arrow-down" + else: + self.flip_camera_button.icon = "arrow-up" def add_polygon(self, obj): ps = PolygonSelector(edge_width=3, edge_color="magenta") From e910e141aeefb676afabd1e4330cca745d14d9c3 Mon Sep 17 00:00:00 2001 From: Caitlin Lewis <69729525+clewis7@users.noreply.github.com> Date: Sun, 24 Sep 2023 01:57:32 -0400 Subject: [PATCH 09/13] add jupyter-sidecar as a dependency, update simple notebook (#300) * add jupyter-sidecar as a dependency, update simple notebook * Update setup.py * add sidecar for plots * sidecar updates * add sidecar to gridplot * add close method to image widget, add sidecar to image widget * fix notebook errors * fix linear region selector * add vbox as kwarg for additional ipywidgets when showing plot and gridplot * fix notebooks --- examples/notebooks/gridplot_simple.ipynb | 119 ++++++++++-------- .../notebooks/linear_region_selector.ipynb | 76 ++++------- examples/notebooks/linear_selector.ipynb | 35 +++--- examples/notebooks/simple.ipynb | 98 +++++++++++++-- fastplotlib/layouts/_gridplot.py | 69 +++++++++- fastplotlib/layouts/_plot.py | 70 ++++++++++- fastplotlib/widgets/image.py | 57 +++++++-- setup.py | 6 +- 8 files changed, 383 insertions(+), 147 deletions(-) diff --git a/examples/notebooks/gridplot_simple.ipynb b/examples/notebooks/gridplot_simple.ipynb index f90c0b157..8b50b2701 100644 --- a/examples/notebooks/gridplot_simple.ipynb +++ b/examples/notebooks/gridplot_simple.ipynb @@ -12,7 +12,9 @@ "cell_type": "code", "execution_count": 1, "id": "5171a06e-1bdc-4908-9726-3c1fd45dbb9d", - "metadata": {}, + "metadata": { + "tags": [] + }, "outputs": [], "source": [ "import numpy as np\n", @@ -23,12 +25,14 @@ "cell_type": "code", "execution_count": 2, "id": "86a2488f-ae1c-4b98-a7c0-18eae8013af1", - "metadata": {}, + "metadata": { + "tags": [] + }, "outputs": [ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "5e4e0c5ca610425b8216db8e30cae997", + "model_id": "f9067cd724094b8c8dfecf60208acbfa", "version_major": 2, "version_minor": 0 }, @@ -40,31 +44,12 @@ "output_type": "display_data" }, { - "data": { - "text/html": [ - "
initial snapshot
" - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "1eeb8c42e1b24c4fb40e3b5daa63909a", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "JupyterWgpuCanvas()" - ] - }, - "execution_count": 2, - "metadata": {}, - "output_type": "execute_result" + "name": "stderr", + "output_type": "stream", + "text": [ + "/home/clewis7/repos/fastplotlib/fastplotlib/graphics/_features/_base.py:34: UserWarning: converting float64 array to float32\n", + " warn(f\"converting {array.dtype} array to float32\")\n" + ] } ], "source": [ @@ -105,15 +90,18 @@ "cell_type": "code", "execution_count": 3, "id": "17c6bc4a-5340-49f1-8597-f54528cfe915", - "metadata": {}, + "metadata": { + "tags": [] + }, "outputs": [ { "data": { "text/plain": [ - "unnamed: Subplot @ 0x7fd4cc9bf820\n", - " parent: None\n", + "unnamed: Subplot @ 0x7f15df4f5c50\n", + " parent: fastplotlib.GridPlot @ 0x7f15d3f27890\n", + "\n", " Graphics:\n", - "\t'rand-img': ImageGraphic @ 0x7fd4f675a350" + "\t'rand-img': ImageGraphic @ 0x7f15d3fb5390" ] }, "execution_count": 3, @@ -139,12 +127,14 @@ "cell_type": "code", "execution_count": 4, "id": "34130f12-9ef6-43b0-b929-931de8b7da25", - "metadata": {}, + "metadata": { + "tags": [] + }, "outputs": [ { "data": { "text/plain": [ - "('rand-img': ImageGraphic @ 0x7fd4a03295a0,)" + "(,)" ] }, "execution_count": 4, @@ -166,12 +156,14 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": 11, "id": "ef8a29a6-b19c-4e6b-a2ba-fb4823c01451", - "metadata": {}, + "metadata": { + "tags": [] + }, "outputs": [], "source": [ - "grid_plot[0, 1].graphics[0].vmax = 0.5" + "grid_plot[0, 1].graphics[0].cmap.vmax = 0.5" ] }, { @@ -186,7 +178,9 @@ "cell_type": "code", "execution_count": 6, "id": "d6c2fa4b-c634-4dcf-8b61-f1986f7c4918", - "metadata": {}, + "metadata": { + "tags": [] + }, "outputs": [], "source": [ "# you can give subplots human-readable string names\n", @@ -197,15 +191,18 @@ "cell_type": "code", "execution_count": 7, "id": "2f6b549c-3165-496d-98aa-45b96c3de674", - "metadata": {}, + "metadata": { + "tags": [] + }, "outputs": [ { "data": { "text/plain": [ - "top-right-plot: Subplot @ 0x7fd4cca0ffd0\n", - " parent: None\n", + "top-right-plot: Subplot @ 0x7f15d3f769d0\n", + " parent: fastplotlib.GridPlot @ 0x7f15d3f27890\n", + "\n", " Graphics:\n", - "\t'rand-img': ImageGraphic @ 0x7fd4a03716c0" + "\t'rand-img': ImageGraphic @ 0x7f15b83f7250" ] }, "execution_count": 7, @@ -219,9 +216,11 @@ }, { "cell_type": "code", - "execution_count": 8, + "execution_count": 12, "id": "be436e04-33a6-4597-8e6a-17e1e5225419", - "metadata": {}, + "metadata": { + "tags": [] + }, "outputs": [ { "data": { @@ -229,7 +228,7 @@ "(0, 2)" ] }, - "execution_count": 8, + "execution_count": 12, "metadata": {}, "output_type": "execute_result" } @@ -241,9 +240,11 @@ }, { "cell_type": "code", - "execution_count": 9, + "execution_count": 13, "id": "6699cda6-af86-4258-87f5-1832f989a564", - "metadata": {}, + "metadata": { + "tags": [] + }, "outputs": [ { "data": { @@ -251,7 +252,7 @@ "True" ] }, - "execution_count": 9, + "execution_count": 13, "metadata": {}, "output_type": "execute_result" } @@ -271,9 +272,11 @@ }, { "cell_type": "code", - "execution_count": 10, + "execution_count": 14, "id": "545b627b-d794-459a-a75a-3fde44f0ea95", - "metadata": {}, + "metadata": { + "tags": [] + }, "outputs": [], "source": [ "grid_plot[\"top-right-plot\"][\"rand-img\"].vmin = 0.5" @@ -281,8 +284,20 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 15, "id": "36432d5b-b76c-4a2a-a32c-097faf5ab269", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "grid_plot.close()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b507b723-1371-44e7-aa6d-6aeb3196b27d", "metadata": {}, "outputs": [], "source": [] @@ -304,7 +319,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.5" + "version": "3.11.3" } }, "nbformat": 4, diff --git a/examples/notebooks/linear_region_selector.ipynb b/examples/notebooks/linear_region_selector.ipynb index f252e6f6f..43cea4f81 100644 --- a/examples/notebooks/linear_region_selector.ipynb +++ b/examples/notebooks/linear_region_selector.ipynb @@ -2,7 +2,7 @@ "cells": [ { "cell_type": "markdown", - "id": "40bf515f-7ca3-4f16-8ec9-31076e8d4bde", + "id": "1db50ec4-8754-4421-9f5e-6ba8ca6b81e3", "metadata": {}, "source": [ "# `LinearRegionSelector` with single lines" @@ -11,10 +11,8 @@ { "cell_type": "code", "execution_count": null, - "id": "41f4e1d0-9ae9-4e59-9883-d9339d985afe", - "metadata": { - "tags": [] - }, + "id": "b7bbfeb4-1ad0-47db-9a82-3d3f642a1f63", + "metadata": {}, "outputs": [], "source": [ "import fastplotlib as fpl\n", @@ -83,32 +81,12 @@ "ls_x.selection.add_event_handler(set_zoom_x)\n", "ls_y.selection.add_event_handler(set_zoom_y)\n", "\n", - "# make some ipywidget sliders too\n", - "# these are not necessary, it's just to show how they can be connected\n", - "x_range_slider = IntRangeSlider(\n", - " value=ls_x.selection(),\n", - " min=ls_x.limits[0],\n", - " max=ls_x.limits[1],\n", - " description=\"x\"\n", - ")\n", - "\n", - "y_range_slider = FloatRangeSlider(\n", - " value=ls_y.selection(),\n", - " min=ls_y.limits[0],\n", - " max=ls_y.limits[1],\n", - " description=\"x\"\n", - ")\n", - "\n", - "# connect the region selector to the ipywidget slider\n", - "ls_x.add_ipywidget_handler(x_range_slider, step=5)\n", - "ls_y.add_ipywidget_handler(y_range_slider, step=0.1)\n", - "\n", - "VBox([gp.show(), x_range_slider, y_range_slider])" + "gp.show()" ] }, { "cell_type": "markdown", - "id": "66b1c599-42c0-4223-b33e-37c1ef077204", + "id": "0bad4a35-f860-4f85-9061-920154ab682b", "metadata": {}, "source": [ "### On the x-axis we have a 1-1 mapping from the data that we have passed and the line geometry positions. So the `bounds` min max corresponds directly to the data indices." @@ -117,10 +95,8 @@ { "cell_type": "code", "execution_count": null, - "id": "8b26a37d-aa1d-478e-ad77-99f68a2b7d0c", - "metadata": { - "tags": [] - }, + "id": "2c96a3ff-c2e7-4683-8097-8491e97dd6d3", + "metadata": {}, "outputs": [], "source": [ "ls_x.selection()" @@ -129,10 +105,8 @@ { "cell_type": "code", "execution_count": null, - "id": "c2be060c-8f87-4b5c-8262-619768f6e6af", - "metadata": { - "tags": [] - }, + "id": "3ec71e3f-291c-43c6-a954-0a082ba5981c", + "metadata": {}, "outputs": [], "source": [ "ls_x.get_selected_indices()" @@ -140,7 +114,7 @@ }, { "cell_type": "markdown", - "id": "d1bef432-d764-4841-bd6d-9b9e4c86ff62", + "id": "1588a89e-1da4-4ada-92e2-7437ba942065", "metadata": {}, "source": [ "### However, for the y-axis line we have passed a 2D array where we've used a linspace, so there is not a 1-1 mapping from the data to the line geometry positions. Use `get_selected_indices()` to get the indices of the data bounded by the current selection. In addition the position of the Graphic is not `(0, 0)`. You must use `get_selected_indices()` whenever you want the indices of the selected data." @@ -149,10 +123,8 @@ { "cell_type": "code", "execution_count": null, - "id": "c370d6d7-d92a-4680-8bf0-2f9d541028be", - "metadata": { - "tags": [] - }, + "id": "18e10277-6d5d-42fe-8715-1733efabefa0", + "metadata": {}, "outputs": [], "source": [ "ls_y.selection()" @@ -161,10 +133,8 @@ { "cell_type": "code", "execution_count": null, - "id": "cdf351e1-63a2-4f5a-8199-8ac3f70909c1", - "metadata": { - "tags": [] - }, + "id": "8e9c42b9-60d2-4544-96c5-c8c6832b79e3", + "metadata": {}, "outputs": [], "source": [ "ls_y.get_selected_indices()" @@ -173,10 +143,8 @@ { "cell_type": "code", "execution_count": null, - "id": "6fd608ad-9732-4f50-9d43-8630603c86d0", - "metadata": { - "tags": [] - }, + "id": "a9583d2e-ec52-405c-a875-f3fec5e3aa16", + "metadata": {}, "outputs": [], "source": [ "import fastplotlib as fpl\n", @@ -224,7 +192,7 @@ }, { "cell_type": "markdown", - "id": "63acd2b6-958e-458d-bf01-903037644cfe", + "id": "0fa051b5-d6bc-4e4e-8f12-44f638a00c88", "metadata": {}, "source": [ "# Large line stack with selector" @@ -233,10 +201,8 @@ { "cell_type": "code", "execution_count": null, - "id": "20e53223-6ccd-4145-bf67-32eb409d3b0a", - "metadata": { - "tags": [] - }, + "id": "d5ffb678-c989-49ee-85a9-4fd7822f033c", + "metadata": {}, "outputs": [], "source": [ "import fastplotlib as fpl\n", @@ -279,7 +245,7 @@ { "cell_type": "code", "execution_count": null, - "id": "80e276ba-23b3-43d0-9e0c-86acab79ac67", + "id": "cbcd6309-fb47-4941-9fd1-2b091feb3ae7", "metadata": {}, "outputs": [], "source": [] @@ -301,7 +267,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.11.4" + "version": "3.11.3" } }, "nbformat": 4, diff --git a/examples/notebooks/linear_selector.ipynb b/examples/notebooks/linear_selector.ipynb index a67a30e98..9382ffa63 100644 --- a/examples/notebooks/linear_selector.ipynb +++ b/examples/notebooks/linear_selector.ipynb @@ -2,7 +2,7 @@ "cells": [ { "cell_type": "markdown", - "id": "e0354810-f942-4e4a-b4b9-bb8c083a314e", + "id": "a06e1fd9-47df-42a3-a76c-19e23d7b89fd", "metadata": {}, "source": [ "## `LinearSelector`, draggable selector that can optionally associated with an ipywidget." @@ -11,10 +11,8 @@ { "cell_type": "code", "execution_count": null, - "id": "d79bb7e0-90af-4459-8dcb-a7a21a89ef64", - "metadata": { - "tags": [] - }, + "id": "eb95ba19-14b5-4bf4-93d9-05182fa500cb", + "metadata": {}, "outputs": [], "source": [ "import fastplotlib as fpl\n", @@ -59,13 +57,22 @@ "selector3.add_ipywidget_handler(ipywidget_slider3, step=0.1)\n", "\n", "plot.auto_scale()\n", - "plot.show()\n", - "VBox([plot.show(), ipywidget_slider, ipywidget_slider2, ipywidget_slider3])" + "plot.show(vbox=[ipywidget_slider])" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7ab9f141-f92f-4c4c-808b-97dafd64ca25", + "metadata": {}, + "outputs": [], + "source": [ + "selector.step = 0.1" ] }, { "cell_type": "markdown", - "id": "2c49cdc2-0555-410c-ae2e-da36c3bf3bf0", + "id": "3b0f448f-bbe4-4b87-98e3-093f561c216c", "metadata": {}, "source": [ "### Drag linear selectors with the mouse, hold \"Shift\" to synchronize movement of all the selectors" @@ -73,7 +80,7 @@ }, { "cell_type": "markdown", - "id": "69057edd-7e23-41e7-a284-ac55df1df5d9", + "id": "c6f041b7-8779-46f1-8454-13cec66f53fd", "metadata": {}, "source": [ "## Also works for line collections" @@ -82,10 +89,8 @@ { "cell_type": "code", "execution_count": null, - "id": "1a3b98bd-7139-48d9-bd70-66c500cd260d", - "metadata": { - "tags": [] - }, + "id": "e36da217-f82a-4dfa-9556-1f4a2c7c4f1c", + "metadata": {}, "outputs": [], "source": [ "sines = [sine] * 10\n", @@ -109,7 +114,7 @@ { "cell_type": "code", "execution_count": null, - "id": "b6c2d9d6-ffe0-484c-a550-cafb44fa8465", + "id": "71ae4fca-f644-4d4f-8f32-f9d069bbc2f1", "metadata": {}, "outputs": [], "source": [] @@ -131,7 +136,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.11.4" + "version": "3.11.3" } }, "nbformat": 4, diff --git a/examples/notebooks/simple.ipynb b/examples/notebooks/simple.ipynb index 1aeeef560..753de5a98 100644 --- a/examples/notebooks/simple.ipynb +++ b/examples/notebooks/simple.ipynb @@ -76,7 +76,9 @@ "id": "a9b386ac-9218-4f8f-97b3-f29b4201ef55", "metadata": {}, "source": [ - "## Simple image" + "## Simple image\n", + "\n", + "We are going to be using `jupyterlab-sidecar` to render some of the plots on the side. This makes it very easy to interact with your plots without having to constantly scroll up and down :D" ] }, { @@ -325,6 +327,18 @@ "plot_test(\"astronaut\", plot)" ] }, + { + "cell_type": "code", + "execution_count": null, + "id": "0bb1cfc7-1a06-4abb-a10a-a877a0d51c6b", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "plot.canvas.get_logical_size()" + ] + }, { "cell_type": "markdown", "id": "b53bc11a-ddf1-4786-8dca-8f3d2eaf993d", @@ -429,6 +443,17 @@ "image_graphic == plot[\"sample-image\"]" ] }, + { + "cell_type": "code", + "execution_count": null, + "id": "058d9785-a692-46f6-a062-cdec9c040afe", + "metadata": {}, + "outputs": [], + "source": [ + "# close the sidecar\n", + "plot.sidecar.close()" + ] + }, { "cell_type": "markdown", "id": "5694dca1-1041-4e09-a1da-85b293c5af47", @@ -452,6 +477,7 @@ "\n", "plot_rgb.add_image(new_data, name=\"rgb-image\")\n", "\n", + "# show the plot\n", "plot_rgb.show()" ] }, @@ -500,6 +526,17 @@ "plot_test(\"astronaut_RGB\", plot_rgb)" ] }, + { + "cell_type": "code", + "execution_count": null, + "id": "8316b4f2-3d6e-46b5-8776-c7c963a7aa99", + "metadata": {}, + "outputs": [], + "source": [ + "# close sidecar\n", + "plot_rgb.sidecar.close()" + ] + }, { "cell_type": "markdown", "id": "1cb03f42-1029-4b16-a16b-35447d9e2955", @@ -576,7 +613,7 @@ "\n", "plot_sync.add_animations(update_data_2)\n", "\n", - "plot_sync.show()" + "plot_sync.show(sidecar=False)" ] }, { @@ -602,7 +639,7 @@ "metadata": {}, "outputs": [], "source": [ - "VBox([plot_v.show(), plot_sync.show()])" + "VBox([plot_v.show(sidecar=False), plot_sync.show(sidecar=False)])" ] }, { @@ -612,7 +649,18 @@ "metadata": {}, "outputs": [], "source": [ - "HBox([plot_v.show(), plot_sync.show()])" + "HBox([plot_v.show(sidecar=False), plot_sync.show(sidecar=False)])" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f33f4cd9-02fc-41b7-961b-9dfeb455b63a", + "metadata": {}, + "outputs": [], + "source": [ + "# close sidecar\n", + "plot_v.sidecar.close()" ] }, { @@ -688,7 +736,8 @@ "colors = [\"r\"] * 25 + [\"purple\"] * 25 + [\"y\"] * 25 + [\"b\"] * 25\n", "sinc_graphic = plot_l.add_line(data=sinc, thickness=5, colors = colors)\n", "\n", - "plot_l.show()" + "# show the plot\n", + "plot_l.show(sidecar_kwargs={\"title\": \"lines\", \"layout\": {'width': '800px'}})" ] }, { @@ -971,6 +1020,17 @@ "plot_test(\"lines-underlay\", plot_l)" ] }, + { + "cell_type": "code", + "execution_count": null, + "id": "bef729ea-f524-4efd-a189-bfca23b39af5", + "metadata": {}, + "outputs": [], + "source": [ + "# close sidecar\n", + "plot_l.sidecar.close()" + ] + }, { "cell_type": "markdown", "id": "2c90862e-2f2a-451f-a468-0cf6b857e87a", @@ -1030,6 +1090,19 @@ "plot_test(\"lines-3d\", plot_l3d)" ] }, + { + "cell_type": "code", + "execution_count": null, + "id": "c2c70541-98fe-4e02-a718-ac2857cc25be", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "# close sidecar\n", + "plot_l3d.sidecar.close()" + ] + }, { "cell_type": "markdown", "id": "a202b3d0-2a0b-450a-93d4-76d0a1129d1d", @@ -1159,6 +1232,17 @@ "scatter_graphic.data[n_points:n_points * 2, 0] = np.linspace(-40, 0, n_points)" ] }, + { + "cell_type": "code", + "execution_count": null, + "id": "a9ffdde4-4b8e-4ff7-98b3-464cf5462d20", + "metadata": {}, + "outputs": [], + "source": [ + "# close sidecar\n", + "plot_s.sidecar.close()" + ] + }, { "cell_type": "markdown", "id": "d9e554de-c436-4684-a46a-ce8a33d409ac", @@ -1176,8 +1260,8 @@ "metadata": {}, "outputs": [], "source": [ - "row1 = HBox([plot.show(), plot_v.show(), plot_sync.show()])\n", - "row2 = HBox([plot_l.show(), plot_l3d.show(), plot_s.show()])\n", + "row1 = HBox([plot.show(sidecar=False), plot_v.show(sidecar=False), plot_sync.show(sidecar=False)])\n", + "row2 = HBox([plot_l.show(sidecar=False), plot_l3d.show(sidecar=False), plot_s.show(sidecar=False)])\n", "\n", "VBox([row1, row2])" ] diff --git a/fastplotlib/layouts/_gridplot.py b/fastplotlib/layouts/_gridplot.py index f52c40d1b..be268fa9a 100644 --- a/fastplotlib/layouts/_gridplot.py +++ b/fastplotlib/layouts/_gridplot.py @@ -12,7 +12,9 @@ from wgpu.gui.auto import WgpuCanvas, is_jupyter if is_jupyter(): - from ipywidgets import HBox, Layout, Button, ToggleButton, VBox, Dropdown + from ipywidgets import HBox, Layout, Button, ToggleButton, VBox, Dropdown, Widget + from sidecar import Sidecar + from IPython.display import display from ._utils import make_canvas_and_renderer from ._defaults import create_controller @@ -81,6 +83,9 @@ def __init__( self.shape = shape self.toolbar = None + self.sidecar = None + self.vbox = None + self.plot_open = False canvas, renderer = make_canvas_and_renderer(canvas, renderer) @@ -294,7 +299,13 @@ def remove_animation(self, func): self._animate_funcs_post.remove(func) def show( - self, autoscale: bool = True, maintain_aspect: bool = None, toolbar: bool = True + self, + autoscale: bool = True, + maintain_aspect: bool = None, + toolbar: bool = True, + sidecar: bool = True, + sidecar_kwargs: dict = None, + vbox: list = None ): """ Begins the rendering event loop and returns the canvas @@ -307,15 +318,26 @@ def show( maintain_aspect: bool, default ``True`` maintain aspect ratio - toolbar: bool, default True + toolbar: bool, default ``True`` show toolbar + sidecar: bool, default ``True`` + display plot in a ``jupyterlab-sidecar`` + + sidecar_kwargs: dict, default ``None`` + kwargs for sidecar instance to display plot + i.e. title, layout + + vbox: list, default ``None`` + list of ipywidgets to be displayed with plot + Returns ------- WgpuCanvas the canvas """ + self.canvas.request_draw(self.render) self.canvas.set_logical_size(*self._starting_size) @@ -343,7 +365,38 @@ def show( 0, 0 ].camera.maintain_aspect - return VBox([self.canvas, self.toolbar.widget]) + # validate vbox if not None + if vbox is not None: + for widget in vbox: + if not isinstance(widget, Widget): + raise ValueError(f"Items in vbox must be ipywidgets. Item: {widget} is of type: {type(widget)}") + self.vbox = VBox(vbox) + + if not sidecar: + if self.vbox is not None: + return VBox([self.canvas, self.toolbar.widget, self.vbox]) + else: + return VBox([self.canvas, self.toolbar.widget]) + + # used when plot.show() is being called again but sidecar has been closed via "x" button + # need to force new sidecar instance + # couldn't figure out how to get access to "close" button in order to add observe method on click + if self.plot_open: + self.sidecar = None + + if self.sidecar is None: + if sidecar_kwargs is not None: + self.sidecar = Sidecar(**sidecar_kwargs) + self.plot_open = True + else: + self.sidecar = Sidecar() + self.plot_open = True + + with self.sidecar: + if self.vbox is not None: + return display(VBox([self.canvas, self.toolbar.widget, self.vbox])) + else: + return display(VBox([self.canvas, self.toolbar.widget])) def close(self): """Close the GridPlot""" @@ -352,6 +405,14 @@ def close(self): if self.toolbar is not None: self.toolbar.widget.close() + if self.sidecar is not None: + self.sidecar.close() + + if self.vbox is not None: + self.vbox.close() + + self.plot_open = False + def clear(self): """Clear all Subplots""" for subplot in self: diff --git a/fastplotlib/layouts/_plot.py b/fastplotlib/layouts/_plot.py index d529ef5f5..bf15456bf 100644 --- a/fastplotlib/layouts/_plot.py +++ b/fastplotlib/layouts/_plot.py @@ -3,11 +3,14 @@ import traceback import os +import ipywidgets import pygfx from wgpu.gui.auto import WgpuCanvas, is_jupyter if is_jupyter(): - from ipywidgets import HBox, Layout, Button, ToggleButton, VBox + from ipywidgets import HBox, Layout, Button, ToggleButton, VBox, Widget + from sidecar import Sidecar + from IPython.display import display from ._subplot import Subplot from ._record_mixin import RecordMixin @@ -64,6 +67,9 @@ def __init__( self._starting_size = size self.toolbar = None + self.sidecar = None + self.vbox = None + self.plot_open = False def render(self): super(Plot, self).render() @@ -72,7 +78,13 @@ def render(self): self.canvas.request_draw() def show( - self, autoscale: bool = True, maintain_aspect: bool = None, toolbar: bool = True + self, + autoscale: bool = True, + maintain_aspect: bool = None, + toolbar: bool = True, + sidecar: bool = True, + sidecar_kwargs: dict = None, + vbox: list = None ): """ Begins the rendering event loop and returns the canvas @@ -85,15 +97,26 @@ def show( maintain_aspect: bool, default ``None`` maintain aspect ratio, uses ``camera.maintain_aspect`` if ``None`` - toolbar: bool, default True + toolbar: bool, default ``True`` show toolbar + sidecar: bool, default ``True`` + display the plot in a ``jupyterlab-sidecar`` + + sidecar_kwargs: dict, default ``None`` + kwargs for sidecar instance to display plot + i.e. title, layout + + vbox: list, default ``None`` + list of ipywidgets to be displayed with plot + Returns ------- WgpuCanvas the canvas """ + self.canvas.request_draw(self.render) self.canvas.set_logical_size(*self._starting_size) @@ -117,7 +140,38 @@ def show( self.toolbar = ToolBar(self) self.toolbar.maintain_aspect_button.value = maintain_aspect - return VBox([self.canvas, self.toolbar.widget]) + # validate vbox if not None + if vbox is not None: + for widget in vbox: + if not isinstance(widget, Widget): + raise ValueError(f"Items in vbox must be ipywidgets. Item: {widget} is of type: {type(widget)}") + self.vbox = VBox(vbox) + + if not sidecar: + if self.vbox is not None: + return VBox([self.canvas, self.toolbar.widget, self.vbox]) + else: + return VBox([self.canvas, self.toolbar.widget]) + + # used when plot.show() is being called again but sidecar has been closed via "x" button + # need to force new sidecar instance + # couldn't figure out how to get access to "close" button in order to add observe method on click + if self.plot_open: + self.sidecar = None + + if self.sidecar is None: + if sidecar_kwargs is not None: + self.sidecar = Sidecar(**sidecar_kwargs) + self.plot_open = True + else: + self.sidecar = Sidecar() + self.plot_open = True + + with self.sidecar: + if self.vbox is not None: + return display(VBox([self.canvas, self.toolbar.widget, self.vbox])) + else: + return display(VBox([self.canvas, self.toolbar.widget])) def close(self): """Close Plot""" @@ -126,6 +180,14 @@ def close(self): if self.toolbar is not None: self.toolbar.widget.close() + if self.sidecar is not None: + self.sidecar.close() + + if self.vbox is not None: + self.vbox.close() + + self.plot_open = False + class ToolBar: def __init__(self, plot: Plot): diff --git a/fastplotlib/widgets/image.py b/fastplotlib/widgets/image.py index 62cba0da8..9dbad277e 100644 --- a/fastplotlib/widgets/image.py +++ b/fastplotlib/widgets/image.py @@ -17,6 +17,8 @@ Play, jslink, ) +from sidecar import Sidecar +from IPython.display import display from ..layouts import GridPlot from ..graphics import ImageGraphic @@ -271,6 +273,8 @@ def __init__( self._names = None self.toolbar = None + self.sidecar = None + self.plot_open = False if isinstance(data, list): # verify that it's a list of np.ndarray @@ -913,7 +917,7 @@ def set_data( if reset_vmin_vmax: self.reset_vmin_vmax() - def show(self, toolbar: bool = True): + def show(self, toolbar: bool = True, sidecar: bool = True, sidecar_kwargs: dict = None): """ Show the widget @@ -930,13 +934,50 @@ def show(self, toolbar: bool = True): if self.toolbar is None: self.toolbar = ImageWidgetToolbar(self) - return VBox( - [ - self.gridplot.show(toolbar=True), - self.toolbar.widget, - self._vbox_sliders, - ] - ) + if not sidecar: + return VBox( + [ + self.gridplot.show(toolbar=True, sidecar=False, sidecar_kwargs=None), + self.toolbar.widget, + self._vbox_sliders, + ] + ) + + if self.plot_open: + self.sidecar = None + + if self.sidecar is None: + if sidecar_kwargs is not None: + self.sidecar = Sidecar(**sidecar_kwargs) + self.plot_open = True + else: + self.sidecar = Sidecar() + self.plot_open = True + + with self.sidecar: + return display(VBox( + [ + self.gridplot.show(toolbar=True, sidecar=False, sidecar_kwargs=None), + self.toolbar.widget, + self._vbox_sliders + ] + ) + ) + + def close(self): + """Close Widget""" + self.gridplot.canvas.close() + + self._vbox_sliders.close() + + if self.toolbar is not None: + self.toolbar.widget.close() + self.gridplot.toolbar.widget.close() + + if self.sidecar is not None: + self.sidecar.close() + + self.plot_open = False class ImageWidgetToolbar: diff --git a/setup.py b/setup.py index 2616093fc..1d1204a69 100644 --- a/setup.py +++ b/setup.py @@ -26,7 +26,8 @@ [ "jupyterlab", "jupyter-rfb>=0.4.1", - "ipywidgets>=8.0.0,<9" + "ipywidgets>=8.0.0,<9", + "sidecar" ], "tests": @@ -39,7 +40,8 @@ "jupyter-rfb>=0.4.1", "ipywidgets>=8.0.0,<9", "scikit-learn", - "tqdm" + "tqdm", + "sidecar" ] } From b770a8bafd6c39771d28fea2397242d81c44f783 Mon Sep 17 00:00:00 2001 From: Kushal Kolar Date: Sat, 30 Sep 2023 23:39:00 -0400 Subject: [PATCH 10/13] add sidecar to docs requirements in setup.py --- setup.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 1d1204a69..aa194aa3e 100644 --- a/setup.py +++ b/setup.py @@ -19,7 +19,8 @@ "sphinx-design", "nbsphinx", "pandoc", - "jupyterlab" + "jupyterlab", + "sidecar" ], "notebook": From c3fd72e05309e6853ad18f77a19e1a927522d6c5 Mon Sep 17 00:00:00 2001 From: Kushal Kolar Date: Mon, 2 Oct 2023 18:30:27 -0400 Subject: [PATCH 11/13] vertex_colors -> color_mode='vertex' (#312) * vertex_colors -> color_mode='vertex' * update ci to use pygfx@main instead of a specific commit --- .github/workflows/ci.yml | 4 ++-- .github/workflows/screenshots.yml | 2 +- fastplotlib/graphics/line.py | 2 +- fastplotlib/graphics/scatter.py | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3abcfaaf0..85731e381 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -39,7 +39,7 @@ jobs: python -m pip install --upgrade pip # remove pygfx from requirements, we install a specific commit of pygfx since both fpl and pygfx are fast evolving sed -i "/pygfx/d" ./setup.py - pip install git+https://github.com/pygfx/pygfx.git@cf1da6c32223ba3cf7256982ce9b89c81b593076 + pip install git+https://github.com/pygfx/pygfx.git@main pip install -e ".[notebook,docs,tests]" - name: Build docs run: | @@ -78,7 +78,7 @@ jobs: python -m pip install --upgrade pip # remove pygfx from requirements, we install a specific commit of pygfx since both fpl and pygfx are fast evolving sed -i "/pygfx/d" ./setup.py - pip install git+https://github.com/pygfx/pygfx.git@cf1da6c32223ba3cf7256982ce9b89c81b593076 + pip install git+https://github.com/pygfx/pygfx.git@main pip install -e ".["tests"]" - name: Show wgpu backend run: diff --git a/.github/workflows/screenshots.yml b/.github/workflows/screenshots.yml index 488ad108f..5e274da83 100644 --- a/.github/workflows/screenshots.yml +++ b/.github/workflows/screenshots.yml @@ -33,7 +33,7 @@ jobs: python -m pip install --upgrade pip # remove pygfx from requirements, we install a specific commit of pygfx since both fpl and pygfx are fast evolving sed -i "/pygfx/d" ./setup.py - pip install git+https://github.com/pygfx/pygfx.git@b63f22a1aa61993c32cd96895316cb8248a81e4d + pip install git+https://github.com/pygfx/pygfx.git@main pip install -e ".["tests"]" - name: Show wgpu backend run: diff --git a/fastplotlib/graphics/line.py b/fastplotlib/graphics/line.py index fb7e38e62..d6f061ab0 100644 --- a/fastplotlib/graphics/line.py +++ b/fastplotlib/graphics/line.py @@ -114,7 +114,7 @@ def __init__( world_object: pygfx.Line = pygfx.Line( # self.data.feature_data because data is a Buffer geometry=pygfx.Geometry(positions=self.data(), colors=self.colors()), - material=material(thickness=self.thickness(), vertex_colors=True), + material=material(thickness=self.thickness(), color_mode="vertex"), ) self._set_world_object(world_object) diff --git a/fastplotlib/graphics/scatter.py b/fastplotlib/graphics/scatter.py index 141db2af3..961324c23 100644 --- a/fastplotlib/graphics/scatter.py +++ b/fastplotlib/graphics/scatter.py @@ -91,7 +91,7 @@ def __init__( world_object = pygfx.Points( pygfx.Geometry(positions=self.data(), sizes=self.sizes(), colors=self.colors()), - material=pygfx.PointsMaterial(vertex_colors=True, vertex_sizes=True), + material=pygfx.PointsMaterial(color_mode="vertex", vertex_sizes=True), ) self._set_world_object(world_object) From 3965bb163b7ab8ea1606535c5317bea104c61189 Mon Sep 17 00:00:00 2001 From: Kushal Kolar Date: Tue, 3 Oct 2023 15:21:14 -0400 Subject: [PATCH 12/13] add desktop-only CI, add py312, remove `ipywidgets` import in `plot.py`, pin `pygfx>=0.1.14` (#314) * add desktop-only CI, add py312, remove ipywidget import, pin pygfx>=0.1.14 * add tests-desktop to extras_require * install setuptools explicitly, use py3.11 for docs test --- .github/workflows/ci.yml | 73 ++++++++++++++++++++++++++---- .github/workflows/pypi-publish.yml | 2 +- .github/workflows/screenshots.yml | 8 ++-- fastplotlib/layouts/_plot.py | 1 - setup.py | 11 ++++- 5 files changed, 80 insertions(+), 15 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 85731e381..5fe2f65fe 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -22,10 +22,10 @@ jobs: fail-fast: false steps: - uses: actions/checkout@v3 - - name: Set up Python 3.9 + - name: Set up Python 3.11 uses: actions/setup-python@v4 with: - python-version: 3.9 + python-version: 3.11 - name: Install llvmpipe and lavapipe for offscreen canvas, and git lfs run: | sudo apt-get update -y -qq @@ -36,8 +36,8 @@ jobs: sudo apt-get install ./pandoc-3.1.4-1-amd64.deb - name: Install dev dependencies run: | - python -m pip install --upgrade pip - # remove pygfx from requirements, we install a specific commit of pygfx since both fpl and pygfx are fast evolving + python -m pip install --upgrade pip setuptools + # remove pygfx from install_requires, we install using pygfx@main sed -i "/pygfx/d" ./setup.py pip install git+https://github.com/pygfx/pygfx.git@main pip install -e ".[notebook,docs,tests]" @@ -46,8 +46,8 @@ jobs: cd docs make html SPHINXOPTS="-W --keep-going" - test-build: - name: Test examples + test-build-full: + name: Test examples, env with notebook and glfw runs-on: ubuntu-latest if: ${{ !github.event.pull_request.draft }} strategy: @@ -60,6 +60,8 @@ jobs: pyversion: '3.10' - name: Test py311 pyversion: '3.11' + - name: Test py312 + pyversion: '3.12' steps: - name: Install git-lfs run: | @@ -75,8 +77,8 @@ jobs: sudo apt-get install --no-install-recommends -y libegl1-mesa libgl1-mesa-dri libxcb-xfixes0-dev mesa-vulkan-drivers git-lfs - name: Install dev dependencies run: | - python -m pip install --upgrade pip - # remove pygfx from requirements, we install a specific commit of pygfx since both fpl and pygfx are fast evolving + python -m pip install --upgrade pip setuptools + # remove pygfx from install_requires, we install using pygfx@main sed -i "/pygfx/d" ./setup.py pip install git+https://github.com/pygfx/pygfx.git@main pip install -e ".["tests"]" @@ -100,3 +102,58 @@ jobs: path: | examples/desktop/diffs examples/notebooks/diffs + + test-build-desktop: + name: Test examples, env with only glfw + runs-on: ubuntu-latest + if: ${{ !github.event.pull_request.draft }} + strategy: + fail-fast: false + matrix: + include: + - name: Test py39 + pyversion: '3.9' + - name: Test py310 + pyversion: '3.10' + - name: Test py311 + pyversion: '3.11' + - name: Test py312 + pyversion: '3.12' + steps: + - name: Install git-lfs + run: | + sudo apt install --no-install-recommends -y git-lfs + - uses: actions/checkout@v3 + - name: Set up Python + uses: actions/setup-python@v3 + with: + python-version: ${{ matrix.pyversion }} + - name: Install llvmpipe and lavapipe for offscreen canvas + run: | + sudo apt-get update -y -qq + sudo apt-get install --no-install-recommends -y libegl1-mesa libgl1-mesa-dri libxcb-xfixes0-dev mesa-vulkan-drivers git-lfs + - name: Install dev dependencies + run: | + python -m pip install --upgrade pip setuptools + # remove pygfx from install_requires, we install using pygfx@main + sed -i "/pygfx/d" ./setup.py + pip install git+https://github.com/pygfx/pygfx.git@main + pip install -e ".["tests-desktop"]" + - name: Show wgpu backend + run: + python -c "from examples.tests.testutils import wgpu_backend; print(wgpu_backend)" + - name: fetch git lfs files + run: | + git lfs fetch --all + git lfs pull + - name: Test examples + env: + PYGFX_EXPECT_LAVAPIPE: true + run: | + pytest -v examples + - uses: actions/upload-artifact@v3 + if: ${{ failure() }} + with: + name: screenshot-diffs + path: | + examples/desktop/diffs diff --git a/.github/workflows/pypi-publish.yml b/.github/workflows/pypi-publish.yml index ec703542b..207d92351 100644 --- a/.github/workflows/pypi-publish.yml +++ b/.github/workflows/pypi-publish.yml @@ -25,7 +25,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v3 with: - python-version: '3.x' + python-version: '3.10' - name: Install dependencies run: | python -m pip install --upgrade pip diff --git a/.github/workflows/screenshots.yml b/.github/workflows/screenshots.yml index 5e274da83..d4cfb94d3 100644 --- a/.github/workflows/screenshots.yml +++ b/.github/workflows/screenshots.yml @@ -20,18 +20,18 @@ jobs: run: | sudo apt install --no-install-recommends -y git-lfs - uses: actions/checkout@v3 - - name: Set up Python 3.10 + - name: Set up Python 3.11 uses: actions/setup-python@v4 with: - python-version: '3.10' + python-version: '3.11' - name: Install llvmpipe and lavapipe for offscreen canvas run: | sudo apt-get update -y -qq sudo apt-get install --no-install-recommends -y libegl1-mesa libgl1-mesa-dri libxcb-xfixes0-dev mesa-vulkan-drivers - name: Install dev dependencies run: | - python -m pip install --upgrade pip - # remove pygfx from requirements, we install a specific commit of pygfx since both fpl and pygfx are fast evolving + python -m pip install --upgrade pip setuptools + # remove pygfx from install_requires, we install using pygfx@main sed -i "/pygfx/d" ./setup.py pip install git+https://github.com/pygfx/pygfx.git@main pip install -e ".["tests"]" diff --git a/fastplotlib/layouts/_plot.py b/fastplotlib/layouts/_plot.py index bf15456bf..253b6296b 100644 --- a/fastplotlib/layouts/_plot.py +++ b/fastplotlib/layouts/_plot.py @@ -3,7 +3,6 @@ import traceback import os -import ipywidgets import pygfx from wgpu.gui.auto import WgpuCanvas, is_jupyter diff --git a/setup.py b/setup.py index aa194aa3e..6557994ef 100644 --- a/setup.py +++ b/setup.py @@ -4,7 +4,7 @@ install_requires = [ "numpy>=1.23.0", - "pygfx>=0.1.13", + "pygfx>=0.1.14", ] @@ -43,6 +43,15 @@ "scikit-learn", "tqdm", "sidecar" + ], + + "tests-desktop": + [ + "pytest", + "scipy", + "imageio", + "scikit-learn", + "tqdm", ] } From 097a7cd9ceebe6ef3953eee8693ac8f02a3f712d Mon Sep 17 00:00:00 2001 From: Kushal Kolar Date: Tue, 3 Oct 2023 16:37:32 -0400 Subject: [PATCH 13/13] bump version --- fastplotlib/VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fastplotlib/VERSION b/fastplotlib/VERSION index 99bed0205..9a1d5d93c 100644 --- a/fastplotlib/VERSION +++ b/fastplotlib/VERSION @@ -1 +1 @@ -0.1.0.a12 +0.1.0.a13