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 pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy