diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index facca91db..fe9c90242 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -58,9 +58,9 @@ Fastplotlib uses the [`pygfx`](https://github.com/pygfx/pygfx) rendering engine plotting library. Some degree of familiarity with [`pygfx`](https://github.com/pygfx/pygfx) or rendering engines may be useful depending on the type of contribution you're working on. -There are currently 2 major subpackages within `fastplotlib`, `layouts` and `graphics`. The two user-facing public -classes within `layouts` are `Plot` and `GridPlot`. A user is intended to create either a `Plot` or `GridPlot`, and -then add *Graphics* to that layout, such as an `ImageGraphic`, `LineGraphic`, etc. +There are currently 2 major subpackages within `fastplotlib`, `layouts` and `graphics`. The user-facing public +class within `layouts` is `Figure`. A user is intended to create a `Figure`, and +then add *Graphics* to subplots within that `Figure`. ### Graphics @@ -70,7 +70,7 @@ fastplotlib graphics, such as `ImageGraphic`, `ScatterGraphic`, etc. inherit fro These might change in the future (ex. `Graphic.position_x` etc.). All graphics can be given a string name for the user's convenience. This allows graphics to be easily accessed from -plots, ex: `plot["some_image"]`. +plots, ex: `subplot["some_image"]`. All graphics contain a `world_object` property which is just the `pygfx.WorldObject` that this graphic uses. Fastplotlib keeps a *private* global dictionary of all `WorldObject` instances and users are only given a weakref proxy to this world object. @@ -119,8 +119,8 @@ after the aforementioned `Input` class PR in `pygfx` and after https://github.co #### PlotArea -This is the main base class within layouts. Every kind of "plot area", whether it's a single `Plot`, subplots within a -`GridPlot`, or `Dock` area, use `PlotArea` in some way. +This is the main base class within layouts. Subplots within a `Figure` and `Dock` areas within a `Subplot`, +inherit from `PlotArea`. `PlotArea` has the following key properties that allow it to be a "plot area" that can be used to view graphical objects: @@ -135,10 +135,10 @@ Abstract method that must be implemented in subclasses: * get_rect - musut return [x, y, width, height] that defines the viewport rect for this `PlotArea` -Properties specifically used by subplots in a gridplot: +Properties specifically used by subplots in a Figure: -* parent - A parent if relevant, used by individual `Subplots` in `GridPlot`, and by `Dock` which are "docked" subplots at the edges of a subplot. -* position - if a subplot within a gridplot, it is the position of this subplot within the `GridPlot` +* parent - A parent if relevant, used by individual `Subplots` in `Figure`, and by `Dock` which are "docked" subplots at the edges of a subplot. +* position - if a subplot within a Figure, it is the position of this subplot within the `Figure` Other important properties: @@ -182,18 +182,13 @@ Subplot has one property that is not in `PlotArea`: The key method in `Subplot` is an implementation of `get_rect` that returns the viewport rect for this subplot. -#### Plot, GridPlot, and Frame +#### Figure -Now that we have understood `PlotArea` and `Subplot` we need a way for the user to create either single plots or gridplots -and display them! +Now that we have understood `PlotArea` and `Subplot` we need a way for the user to create them! -There's one more class to talk about, `Frame`. This is a class that "frames" a `Plot` or `GridPlot`. Depending on -whether the plot's `Canvas` is a Qt or jupyter canvas, `Frame.show()` will create a plot toolbar and place this toolbar -below the `Canvas`. If using a glfw canvas it just returns the canvas. - -`Plot` and `GridPlot` both inherit from `Frame` which gives them `show()`. `Plot` is just a single `Subplot` with the -addition of `Frame`. `GridPlot.__init__` basically does a lot of parsing of user arguments to determine how to create -the subplots. All subplots within a `GridPlot` share the same canvas and use different viewports to create the subplots. +A `Figure` contains a grid of subplot and has methods such as `show()` to output the figure. +`Figure.__init__` basically does a lot of parsing of user arguments to determine how to create +the subplots. All subplots within a `Figure` share the same canvas and use different viewports to create the subplots. ## Tests in detail @@ -216,6 +211,9 @@ ground-truth image are within that tolerance the test will pass. To run tests: ```bash +# tests basic backend functionality +WGPU_FORCE_OFFSCREEN=1 pytest -v -s tests/ + # desktop examples pytest -v examples diff --git a/README.md b/README.md index 37a2d0779..64e1649e8 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,6 @@ [![CI](https://github.com/kushalkolar/fastplotlib/actions/workflows/ci.yml/badge.svg)](https://github.com/kushalkolar/fastplotlib/actions/workflows/ci.yml) [![PyPI version](https://badge.fury.io/py/fastplotlib.svg)](https://badge.fury.io/py/fastplotlib) [![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) | @@ -15,7 +14,7 @@ [**Examples**](https://github.com/kushalkolar/fastplotlib#examples) | [**Contributing**](https://github.com/kushalkolar/fastplotlib#heart-contributing) -Next-gen plotting library built using the [`pygfx`](https://github.com/pygfx/pygfx) rendering engine that can utilize [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. +Next-gen plotting library built using the [`pygfx`](https://github.com/pygfx/pygfx) rendering engine that can utilize [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! `fastplotlib` also aims to be an expressive plotting library that enables rapid prototyping for large scale explorative scientific visualization. ![scipy-fpl](https://github.com/fastplotlib/fastplotlib/assets/9403332/b981a54c-05f9-443f-a8e4-52cd01cd802a) @@ -23,8 +22,8 @@ Next-gen plotting library built using the [`pygfx`](https://github.com/pygfx/pyg [![fpl_thumbnail](http://i3.ytimg.com/vi/Q-UJpAqljsU/hqdefault.jpg)](https://www.youtube.com/watch?v=Q-UJpAqljsU) -Notebooks from talk: https://github.com/fastplotlib/fastplotlib-scipy2023 - +Note that the API is currently evolving quickly. We recommend using the latest notebooks from the repo but the general +concepts are similar to those from the API shown in the video. # Supported frameworks @@ -36,9 +35,9 @@ Notebooks from talk: https://github.com/fastplotlib/fastplotlib-scipy2023 :heavy_check_mark: `wxPython` **Notes:**\ -:heavy_check_mark: Non-blocking Qt output is supported in ipython and notebooks by using [`%gui qt`](https://ipython.readthedocs.io/en/stable/interactive/magics.html#magic-gui) before creating plots. This hook only supports pyqt6 at the moment.\ +:heavy_check_mark: Non-blocking Qt/PySide output is supported in ipython and notebooks by using [`%gui qt`](https://ipython.readthedocs.io/en/stable/interactive/magics.html#magic-gui). This **must** be called *before* importing `fastplotlib`! :grey_exclamation: We do not officially support `jupyter notebook` through `jupyter_rfb`, this may change with notebook v7\ -:disappointed: [`jupyter_rfb`](https://github.com/vispy/jupyter_rfb) does not work in collab yet, see https://github.com/vispy/jupyter_rfb/pull/77 +:disappointed: [`jupyter_rfb`](https://github.com/vispy/jupyter_rfb) does not work in collab, see https://github.com/vispy/jupyter_rfb/pull/77 > **Note** > @@ -169,4 +168,4 @@ WGPU uses Metal instead of Vulkan on Mac. You will need at least Mac OSX 10.13. We welcome contributions! See the contributing guide: https://github.com/kushalkolar/fastplotlib/blob/main/CONTRIBUTING.md -You can also take a look at our [**Roadmap for 2024**](https://github.com/kushalkolar/fastplotlib/issues/55) and [**Issues**](https://github.com/kushalkolar/fastplotlib/issues) for ideas on how to contribute! +You can also take a look at our [**Roadmap for 2025**](https://github.com/kushalkolar/fastplotlib/issues/55) and [**Issues**](https://github.com/kushalkolar/fastplotlib/issues) for ideas on how to contribute! diff --git a/docs/source/api/graphic_features/Deleted.rst b/docs/source/api/graphic_features/Deleted.rst new file mode 100644 index 000000000..998e94588 --- /dev/null +++ b/docs/source/api/graphic_features/Deleted.rst @@ -0,0 +1,33 @@ +.. _api.Deleted: + +Deleted +******* + +======= +Deleted +======= +.. currentmodule:: fastplotlib.graphics._features + +Constructor +~~~~~~~~~~~ +.. autosummary:: + :toctree: Deleted_api + + Deleted + +Properties +~~~~~~~~~~ +.. autosummary:: + :toctree: Deleted_api + + +Methods +~~~~~~~ +.. autosummary:: + :toctree: Deleted_api + + Deleted.add_event_handler + Deleted.block_events + Deleted.clear_event_handlers + Deleted.remove_event_handler + diff --git a/docs/source/api/graphic_features/index.rst b/docs/source/api/graphic_features/index.rst index 1c4b33392..06e3119e5 100644 --- a/docs/source/api/graphic_features/index.rst +++ b/docs/source/api/graphic_features/index.rst @@ -20,3 +20,4 @@ Graphic Features to_gpu_supported_dtype LinearSelectionFeature LinearRegionSelectionFeature + Deleted diff --git a/docs/source/api/graphics/HeatmapGraphic.rst b/docs/source/api/graphics/HeatmapGraphic.rst index 3bd2f2baa..ffa86eb16 100644 --- a/docs/source/api/graphics/HeatmapGraphic.rst +++ b/docs/source/api/graphics/HeatmapGraphic.rst @@ -21,10 +21,12 @@ Properties :toctree: HeatmapGraphic_api HeatmapGraphic.children + HeatmapGraphic.name HeatmapGraphic.position HeatmapGraphic.position_x HeatmapGraphic.position_y HeatmapGraphic.position_z + HeatmapGraphic.rotation HeatmapGraphic.visible HeatmapGraphic.world_object @@ -37,5 +39,6 @@ Methods HeatmapGraphic.add_linear_selector HeatmapGraphic.link HeatmapGraphic.reset_feature + HeatmapGraphic.rotate HeatmapGraphic.set_feature diff --git a/docs/source/api/graphics/ImageGraphic.rst b/docs/source/api/graphics/ImageGraphic.rst index 871462701..00b27340d 100644 --- a/docs/source/api/graphics/ImageGraphic.rst +++ b/docs/source/api/graphics/ImageGraphic.rst @@ -21,10 +21,12 @@ Properties :toctree: ImageGraphic_api ImageGraphic.children + ImageGraphic.name ImageGraphic.position ImageGraphic.position_x ImageGraphic.position_y ImageGraphic.position_z + ImageGraphic.rotation ImageGraphic.visible ImageGraphic.world_object @@ -37,5 +39,6 @@ Methods ImageGraphic.add_linear_selector ImageGraphic.link ImageGraphic.reset_feature + ImageGraphic.rotate ImageGraphic.set_feature diff --git a/docs/source/api/graphics/LineCollection.rst b/docs/source/api/graphics/LineCollection.rst index 3f67feed9..8d10d8376 100644 --- a/docs/source/api/graphics/LineCollection.rst +++ b/docs/source/api/graphics/LineCollection.rst @@ -24,10 +24,12 @@ Properties LineCollection.cmap LineCollection.cmap_values LineCollection.graphics + LineCollection.name LineCollection.position LineCollection.position_x LineCollection.position_y LineCollection.position_z + LineCollection.rotation LineCollection.visible LineCollection.world_object @@ -42,5 +44,6 @@ Methods LineCollection.link LineCollection.remove_graphic LineCollection.reset_feature + LineCollection.rotate LineCollection.set_feature diff --git a/docs/source/api/graphics/LineGraphic.rst b/docs/source/api/graphics/LineGraphic.rst index 4aae4bbee..8b6fedf22 100644 --- a/docs/source/api/graphics/LineGraphic.rst +++ b/docs/source/api/graphics/LineGraphic.rst @@ -21,10 +21,12 @@ Properties :toctree: LineGraphic_api LineGraphic.children + LineGraphic.name LineGraphic.position LineGraphic.position_x LineGraphic.position_y LineGraphic.position_z + LineGraphic.rotation LineGraphic.visible LineGraphic.world_object @@ -37,5 +39,6 @@ Methods LineGraphic.add_linear_selector LineGraphic.link LineGraphic.reset_feature + LineGraphic.rotate LineGraphic.set_feature diff --git a/docs/source/api/graphics/LineStack.rst b/docs/source/api/graphics/LineStack.rst index 36ae6808e..a39db46f8 100644 --- a/docs/source/api/graphics/LineStack.rst +++ b/docs/source/api/graphics/LineStack.rst @@ -24,10 +24,12 @@ Properties LineStack.cmap LineStack.cmap_values LineStack.graphics + LineStack.name LineStack.position LineStack.position_x LineStack.position_y LineStack.position_z + LineStack.rotation LineStack.visible LineStack.world_object @@ -42,5 +44,6 @@ Methods LineStack.link LineStack.remove_graphic LineStack.reset_feature + LineStack.rotate LineStack.set_feature diff --git a/docs/source/api/graphics/ScatterGraphic.rst b/docs/source/api/graphics/ScatterGraphic.rst index 3c4bf3909..44d87d008 100644 --- a/docs/source/api/graphics/ScatterGraphic.rst +++ b/docs/source/api/graphics/ScatterGraphic.rst @@ -21,10 +21,12 @@ Properties :toctree: ScatterGraphic_api ScatterGraphic.children + ScatterGraphic.name ScatterGraphic.position ScatterGraphic.position_x ScatterGraphic.position_y ScatterGraphic.position_z + ScatterGraphic.rotation ScatterGraphic.visible ScatterGraphic.world_object @@ -33,4 +35,5 @@ Methods .. autosummary:: :toctree: ScatterGraphic_api + ScatterGraphic.rotate diff --git a/docs/source/api/graphics/TextGraphic.rst b/docs/source/api/graphics/TextGraphic.rst index 6290dcc2e..23425cf41 100644 --- a/docs/source/api/graphics/TextGraphic.rst +++ b/docs/source/api/graphics/TextGraphic.rst @@ -22,12 +22,14 @@ Properties TextGraphic.children TextGraphic.face_color + TextGraphic.name TextGraphic.outline_color TextGraphic.outline_size TextGraphic.position TextGraphic.position_x TextGraphic.position_y TextGraphic.position_z + TextGraphic.rotation TextGraphic.text TextGraphic.text_size TextGraphic.visible @@ -38,4 +40,5 @@ Methods .. autosummary:: :toctree: TextGraphic_api + TextGraphic.rotate diff --git a/docs/source/api/layouts/figure.rst b/docs/source/api/layouts/figure.rst new file mode 100644 index 000000000..a2d5e5758 --- /dev/null +++ b/docs/source/api/layouts/figure.rst @@ -0,0 +1,44 @@ +.. _api.Figure: + +Figure +****** + +====== +Figure +====== +.. currentmodule:: fastplotlib + +Constructor +~~~~~~~~~~~ +.. autosummary:: + :toctree: Figure_api + + Figure + +Properties +~~~~~~~~~~ +.. autosummary:: + :toctree: Figure_api + + Figure.cameras + Figure.canvas + Figure.controllers + Figure.names + Figure.output + Figure.renderer + Figure.shape + Figure.toolbar + +Methods +~~~~~~~ +.. autosummary:: + :toctree: Figure_api + + Figure.add_animations + Figure.clear + Figure.close + Figure.remove_animation + Figure.render + Figure.show + Figure.start_render + diff --git a/docs/source/api/layouts/gridplot.rst b/docs/source/api/layouts/gridplot.rst deleted file mode 100644 index b5b03bfa4..000000000 --- a/docs/source/api/layouts/gridplot.rst +++ /dev/null @@ -1,42 +0,0 @@ -.. _api.GridPlot: - -GridPlot -******** - -======== -GridPlot -======== -.. currentmodule:: fastplotlib - -Constructor -~~~~~~~~~~~ -.. autosummary:: - :toctree: GridPlot_api - - GridPlot - -Properties -~~~~~~~~~~ -.. autosummary:: - :toctree: GridPlot_api - - GridPlot.canvas - GridPlot.renderer - GridPlot.toolbar - GridPlot.widget - -Methods -~~~~~~~ -.. autosummary:: - :toctree: GridPlot_api - - GridPlot.add_animations - GridPlot.clear - GridPlot.close - GridPlot.record_start - GridPlot.record_stop - GridPlot.remove_animation - GridPlot.render - GridPlot.show - GridPlot.start_render - diff --git a/docs/source/api/layouts/plot.rst b/docs/source/api/layouts/plot.rst deleted file mode 100644 index bd38720b4..000000000 --- a/docs/source/api/layouts/plot.rst +++ /dev/null @@ -1,73 +0,0 @@ -.. _api.Plot: - -Plot -**** - -==== -Plot -==== -.. currentmodule:: fastplotlib - -Constructor -~~~~~~~~~~~ -.. autosummary:: - :toctree: Plot_api - - Plot - -Properties -~~~~~~~~~~ -.. autosummary:: - :toctree: Plot_api - - Plot.camera - Plot.canvas - Plot.controller - Plot.docks - Plot.graphics - Plot.name - Plot.parent - Plot.position - Plot.renderer - Plot.scene - Plot.selectors - Plot.toolbar - Plot.viewport - Plot.widget - -Methods -~~~~~~~ -.. autosummary:: - :toctree: Plot_api - - Plot.add_animations - Plot.add_graphic - Plot.add_heatmap - Plot.add_image - Plot.add_line - Plot.add_line_collection - Plot.add_line_stack - Plot.add_scatter - Plot.add_text - Plot.auto_scale - Plot.center_graphic - Plot.center_scene - Plot.center_title - Plot.clear - Plot.close - Plot.delete_graphic - Plot.get_rect - Plot.insert_graphic - Plot.map_screen_to_world - Plot.record_start - Plot.record_stop - Plot.remove_animation - Plot.remove_graphic - Plot.render - Plot.set_axes_visibility - Plot.set_grid_visibility - Plot.set_title - Plot.set_viewport_rect - Plot.show - Plot.start_render - diff --git a/docs/source/api/layouts/subplot.rst b/docs/source/api/layouts/subplot.rst index c61c46e05..91884557a 100644 --- a/docs/source/api/layouts/subplot.rst +++ b/docs/source/api/layouts/subplot.rst @@ -25,7 +25,9 @@ Properties Subplot.controller Subplot.docks Subplot.graphics + Subplot.legends Subplot.name + Subplot.objects Subplot.parent Subplot.position Subplot.renderer @@ -54,6 +56,7 @@ Methods Subplot.clear Subplot.delete_graphic Subplot.get_rect + Subplot.get_refcounts Subplot.insert_graphic Subplot.map_screen_to_world Subplot.remove_animation diff --git a/docs/source/api/selectors/LinearRegionSelector.rst b/docs/source/api/selectors/LinearRegionSelector.rst index ce0d8d9b6..1b59e80c9 100644 --- a/docs/source/api/selectors/LinearRegionSelector.rst +++ b/docs/source/api/selectors/LinearRegionSelector.rst @@ -22,10 +22,12 @@ Properties LinearRegionSelector.children LinearRegionSelector.limits + LinearRegionSelector.name LinearRegionSelector.position LinearRegionSelector.position_x LinearRegionSelector.position_y LinearRegionSelector.position_z + LinearRegionSelector.rotation LinearRegionSelector.visible LinearRegionSelector.world_object @@ -39,4 +41,5 @@ Methods LinearRegionSelector.get_selected_index LinearRegionSelector.get_selected_indices LinearRegionSelector.make_ipywidget_slider + LinearRegionSelector.rotate diff --git a/docs/source/api/selectors/LinearSelector.rst b/docs/source/api/selectors/LinearSelector.rst index 4056bcc46..3278559d0 100644 --- a/docs/source/api/selectors/LinearSelector.rst +++ b/docs/source/api/selectors/LinearSelector.rst @@ -22,10 +22,12 @@ Properties LinearSelector.children LinearSelector.limits + LinearSelector.name LinearSelector.position LinearSelector.position_x LinearSelector.position_y LinearSelector.position_z + LinearSelector.rotation LinearSelector.visible LinearSelector.world_object @@ -39,4 +41,5 @@ Methods LinearSelector.get_selected_index LinearSelector.get_selected_indices LinearSelector.make_ipywidget_slider + LinearSelector.rotate diff --git a/docs/source/api/selectors/PolygonSelector.rst b/docs/source/api/selectors/PolygonSelector.rst index aaa434dbf..8de87ec74 100644 --- a/docs/source/api/selectors/PolygonSelector.rst +++ b/docs/source/api/selectors/PolygonSelector.rst @@ -21,10 +21,12 @@ Properties :toctree: PolygonSelector_api PolygonSelector.children + PolygonSelector.name PolygonSelector.position PolygonSelector.position_x PolygonSelector.position_y PolygonSelector.position_z + PolygonSelector.rotation PolygonSelector.visible PolygonSelector.world_object @@ -37,4 +39,5 @@ Methods PolygonSelector.get_selected_index PolygonSelector.get_selected_indices PolygonSelector.get_vertices + PolygonSelector.rotate diff --git a/docs/source/api/widgets/ImageWidget.rst b/docs/source/api/widgets/ImageWidget.rst index 08bce8d7a..3ca384968 100644 --- a/docs/source/api/widgets/ImageWidget.rst +++ b/docs/source/api/widgets/ImageWidget.rst @@ -23,9 +23,11 @@ Properties ImageWidget.cmap ImageWidget.current_index ImageWidget.data - ImageWidget.dims_order - ImageWidget.gridplot + ImageWidget.figure + ImageWidget.frame_apply ImageWidget.managed_graphics + ImageWidget.n_img_dims + ImageWidget.n_scrollable_dims ImageWidget.ndim ImageWidget.slider_dims ImageWidget.sliders diff --git a/docs/source/generate_api.py b/docs/source/generate_api.py index 19b739d1b..a5f668130 100644 --- a/docs/source/generate_api.py +++ b/docs/source/generate_api.py @@ -142,17 +142,10 @@ def generate_page( def main(): generate_page( - page_name="Plot", - classes=[fastplotlib.Plot], + page_name="Figure", + classes=[fastplotlib.Figure], modules=["fastplotlib"], - source_path=LAYOUTS_DIR.joinpath("plot.rst"), - ) - - generate_page( - page_name="GridPlot", - classes=[fastplotlib.GridPlot], - modules=["fastplotlib"], - source_path=LAYOUTS_DIR.joinpath("gridplot.rst"), + source_path=LAYOUTS_DIR.joinpath("figure.rst"), ) generate_page( diff --git a/docs/source/index.rst b/docs/source/index.rst index 9dbd30783..0ceb146e4 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -22,8 +22,7 @@ Welcome to fastplotlib's documentation! :maxdepth: 1 :caption: API - Plot - Gridplot + Figure Subplot Graphics Graphic Features diff --git a/docs/source/quickstart.ipynb b/docs/source/quickstart.ipynb index 0de4667bf..6a892399e 100644 --- a/docs/source/quickstart.ipynb +++ b/docs/source/quickstart.ipynb @@ -79,17 +79,17 @@ }, "outputs": [], "source": [ - "# create a `Plot` instance\n", - "plot = fpl.Plot()\n", + "# create a `Figure` instance\n", + "fig = fpl.Figure()\n", "\n", "# get a grayscale image\n", "data = iio.imread(\"imageio:camera.png\")\n", "\n", "# plot the image data\n", - "image_graphic = plot.add_image(data=data, name=\"sample-image\")\n", + "image_graphic = fig[0, 0].add_image(data=data, name=\"sample-image\")\n", "\n", "# show the plot\n", - "plot.show()" + "fig.show()" ] }, { @@ -117,7 +117,7 @@ }, "outputs": [], "source": [ - "plot.canvas.snapshot()" + "fig.canvas.snapshot()" ] }, { @@ -149,7 +149,7 @@ }, "outputs": [], "source": [ - "plot.canvas.snapshot()" + "fig.canvas.snapshot()" ] }, { @@ -198,7 +198,7 @@ }, "outputs": [], "source": [ - "plot.canvas.snapshot()" + "fig.canvas.snapshot()" ] }, { @@ -230,7 +230,7 @@ }, "outputs": [], "source": [ - "plot.canvas.snapshot()" + "fig.canvas.snapshot()" ] }, { @@ -263,7 +263,7 @@ }, "outputs": [], "source": [ - "plot.canvas.snapshot()" + "fig.canvas.snapshot()" ] }, { @@ -331,7 +331,7 @@ }, "outputs": [], "source": [ - "plot.canvas.snapshot()" + "fig.canvas.snapshot()" ] }, { @@ -363,7 +363,7 @@ }, "outputs": [], "source": [ - "plot.canvas.snapshot()" + "fig.canvas.snapshot()" ] }, { @@ -391,7 +391,7 @@ }, "outputs": [], "source": [ - "plot" + "fig" ] }, { @@ -403,7 +403,7 @@ }, "outputs": [], "source": [ - "plot[\"sample-image\"]" + "fig[0, 0][\"sample-image\"]" ] }, { @@ -423,7 +423,7 @@ }, "outputs": [], "source": [ - "plot.graphics" + "fig[0, 0].graphics" ] }, { @@ -435,7 +435,7 @@ }, "outputs": [], "source": [ - "plot.graphics[0]" + "fig[0, 0].graphics[0]" ] }, { @@ -467,7 +467,7 @@ }, "outputs": [], "source": [ - "image_graphic == plot[\"sample-image\"]" + "image_graphic is fig[0, 0][\"sample-image\"]" ] }, { @@ -489,11 +489,11 @@ }, "outputs": [], "source": [ - "plot_rgb = fpl.Plot()\n", + "fig_rgb = fpl.Figure()\n", "\n", - "plot_rgb.add_image(new_data, name=\"rgb-image\")\n", + "fig_rgb[0, 0].add_image(new_data, name=\"rgb-image\")\n", "\n", - "plot_rgb.show()" + "fig_rgb.show()" ] }, { @@ -505,7 +505,7 @@ }, "outputs": [], "source": [ - "plot_rgb.canvas.snapshot()" + "fig_rgb.canvas.snapshot()" ] }, { @@ -525,7 +525,7 @@ }, "outputs": [], "source": [ - "plot_rgb[\"rgb-image\"].cmap.vmin = 100" + "fig_rgb[0, 0][\"rgb-image\"].cmap.vmin = 100" ] }, { @@ -537,7 +537,7 @@ }, "outputs": [], "source": [ - "plot_rgb.canvas.snapshot()" + "fig_rgb.canvas.snapshot()" ] }, { @@ -561,28 +561,26 @@ }, "outputs": [], "source": [ - "# create another `Plot` instance\n", - "plot_v = fpl.Plot()\n", - "\n", - "plot.canvas.max_buffered_frames = 1\n", + "# create another `Figure` instance\n", + "fig_vid = fpl.Figure()\n", "\n", "# make some random data again\n", "data = np.random.rand(512, 512)\n", "\n", "# plot the data\n", - "plot_v.add_image(data=data, name=\"random-image\")\n", + "fig_vid[0, 0].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 argument\n", - "def update_data(plot_instance):\n", + "# a subplot will pass its instance to the animation function as an argument\n", + "def update_data(subplot):\n", " new_data = np.random.rand(512, 512)\n", - " plot_instance[\"random-image\"].data = new_data\n", + " subplot[\"random-image\"].data = new_data\n", "\n", - "#add this as an animation function\n", - "plot_v.add_animations(update_data)\n", + "#add this as an animation function to the subplot\n", + "fig_vid[0, 0].add_animations(update_data)\n", "\n", "# show the plot\n", - "plot_v.show()" + "fig_vid.show()" ] }, { @@ -602,11 +600,11 @@ "metadata": {}, "outputs": [], "source": [ - "plot_sync = fpl.Plot(controller=plot_v.controller)\n", + "fig_sync = fpl.Figure(controllers=fig_vid.controllers)\n", "\n", "data = np.random.rand(512, 512)\n", "\n", - "image_graphic_instance = plot_sync.add_image(data=data, cmap=\"viridis\")\n", + "image_graphic_instance = fig_sync[0, 0].add_image(data=data, cmap=\"viridis\")\n", "\n", "# you will need to define a new animation function for this graphic\n", "def update_data_2():\n", @@ -614,9 +612,10 @@ " # alternatively, you can use the stored reference to the graphic as well instead of indexing the Plot\n", " image_graphic_instance.data = new_data\n", "\n", - "plot_sync.add_animations(update_data_2)\n", + "# add the animation function to the figure instead of the subplot\n", + "fig_sync.add_animations(update_data_2)\n", "\n", - "plot_sync.show()" + "fig_sync.show()" ] }, { @@ -718,19 +717,19 @@ "outputs": [], "source": [ "# Create a plot instance\n", - "plot_l = fpl.Plot()\n", + "fig_line = fpl.Figure()\n", "\n", "# plot sine wave, use a single color\n", - "sine_graphic = plot_l.add_line(data=sine, thickness=5, colors=\"magenta\")\n", + "sine_graphic = fig_line[0, 0].add_line(data=sine, thickness=5, colors=\"magenta\")\n", "\n", "# you can also use colormaps for lines!\n", - "cosine_graphic = plot_l.add_line(data=cosine, thickness=12, cmap=\"autumn\")\n", + "cosine_graphic = fig_line[0, 0].add_line(data=cosine, thickness=12, cmap=\"autumn\")\n", "\n", "# or a list of colors for each datapoint\n", "colors = [\"r\"] * 25 + [\"purple\"] * 25 + [\"y\"] * 25 + [\"b\"] * 25\n", - "sinc_graphic = plot_l.add_line(data=sinc, thickness=5, colors = colors)\n", + "sinc_graphic = fig_line[0, 0].add_line(data=sinc, thickness=5, colors = colors)\n", "\n", - "plot_l.show()" + "fig_line.show()" ] }, { @@ -754,7 +753,7 @@ }, "outputs": [], "source": [ - "plot_l.camera.maintain_aspect = False" + "fig_line[0, 0].camera.maintain_aspect = False" ] }, { @@ -774,7 +773,7 @@ }, "outputs": [], "source": [ - "plot_l.auto_scale(maintain_aspect=True)" + "fig_line[0, 0].auto_scale(maintain_aspect=True)" ] }, { @@ -822,7 +821,7 @@ }, "outputs": [], "source": [ - "plot_l.canvas.snapshot()" + "fig_line.canvas.snapshot()" ] }, { @@ -868,7 +867,7 @@ }, "outputs": [], "source": [ - "plot_l.canvas.snapshot()" + "fig_line.canvas.snapshot()" ] }, { @@ -909,7 +908,7 @@ }, "outputs": [], "source": [ - "plot_l.canvas.snapshot()" + "fig_line.canvas.snapshot()" ] }, { @@ -939,7 +938,7 @@ }, "outputs": [], "source": [ - "plot_l.canvas.snapshot()" + "fig_line.canvas.snapshot()" ] }, { @@ -961,7 +960,7 @@ }, "outputs": [], "source": [ - "plot_l.canvas.snapshot()" + "fig_line.canvas.snapshot()" ] }, { @@ -979,7 +978,7 @@ "metadata": {}, "outputs": [], "source": [ - "sinc_graphic.present.add_event_handler(plot_l.auto_scale)" + "sinc_graphic.present.add_event_handler(fig_line[0, 0].auto_scale)" ] }, { @@ -1001,7 +1000,7 @@ }, "outputs": [], "source": [ - "plot_l.canvas.snapshot()" + "fig_line.canvas.snapshot()" ] }, { @@ -1023,7 +1022,7 @@ }, "outputs": [], "source": [ - "plot_l.canvas.snapshot()" + "fig_line.canvas.snapshot()" ] }, { @@ -1045,11 +1044,11 @@ "source": [ "img = np.random.rand(20, 100)\n", "\n", - "plot_l.add_image(img, name=\"image\", cmap=\"gray\")\n", + "fig_line[0, 0].add_image(img, name=\"image\", cmap=\"gray\")\n", "\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" + "fig_line[0, 0][\"image\"].position_z = -1\n", + "fig_line[0, 0][\"image\"].position_x = -50" ] }, { @@ -1061,7 +1060,7 @@ }, "outputs": [], "source": [ - "plot_l.canvas.snapshot()" + "fig_line.canvas.snapshot()" ] }, { @@ -1080,7 +1079,7 @@ "outputs": [], "source": [ "# just set the camera as \"3d\", the rest is basically the same :D \n", - "plot_l3d = fpl.Plot(camera='3d')\n", + "fig_line_3d = fpl.Figure(cameras='3d')\n", "\n", "# create a spiral\n", "phi = np.linspace(0, 30, 200)\n", @@ -1093,9 +1092,9 @@ "# note: you usually mix 3D and 2D graphics on the same plot\n", "spiral = np.dstack([xs, ys, zs])[0]\n", "\n", - "plot_l3d.add_line(data=spiral, thickness=2, cmap='winter')\n", + "fig_line_3d[0, 0].add_line(data=spiral, thickness=2, cmap='winter')\n", "\n", - "plot_l3d.show()" + "fig_line_3d.show()" ] }, { @@ -1107,7 +1106,7 @@ }, "outputs": [], "source": [ - "plot_l3d.auto_scale(maintain_aspect=True)" + "fig_line_3d[0, 0].auto_scale(maintain_aspect=True)" ] }, { @@ -1157,12 +1156,12 @@ "colors = [\"yellow\"] * n_points + [\"cyan\"] * n_points + [\"magenta\"] * n_points\n", "\n", "# create plot\n", - "plot_s = fpl.Plot()\n", + "fig_scatter = fpl.Figure()\n", "\n", "# use an alpha value since this will be a lot of points\n", - "scatter_graphic = plot_s.add_scatter(data=cloud, sizes=3, colors=colors, alpha=0.7)\n", + "scatter_graphic = fig_scatter[0, 0].add_scatter(data=cloud, sizes=3, colors=colors, alpha=0.7)\n", "\n", - "plot_s.show()" + "fig_scatter.show()" ] }, { @@ -1193,7 +1192,7 @@ }, "outputs": [], "source": [ - "plot_s.canvas.snapshot()" + "fig_scatter.canvas.snapshot()" ] }, { @@ -1216,7 +1215,7 @@ }, "outputs": [], "source": [ - "plot_s.canvas.snapshot()" + "fig_scatter.canvas.snapshot()" ] }, { @@ -1239,7 +1238,7 @@ }, "outputs": [], "source": [ - "plot_s.canvas.snapshot()" + "fig_scatter.canvas.snapshot()" ] }, { @@ -1262,7 +1261,7 @@ }, "outputs": [], "source": [ - "plot_s.canvas.snapshot()" + "fig_scatter.canvas.snapshot()" ] }, { @@ -1285,7 +1284,7 @@ }, "outputs": [], "source": [ - "plot_s.canvas.snapshot()" + "fig_scatter.canvas.snapshot()" ] }, { @@ -1318,11 +1317,7 @@ "id": "a26c0063-b7e0-4f36-bb14-db06bafa31aa", "metadata": {}, "source": [ - "## Gridplot\n", - "\n", - "Subplots within a `GridPlot` behave the same as simple `Plot` instances! \n", - "\n", - "💡 `Plot` is actually a subclass of `Subplot`!" + "## More subplots" ] }, { @@ -1334,11 +1329,11 @@ }, "outputs": [], "source": [ - "# GridPlot of shape 2 x 3 with all controllers synced\n", - "grid_plot = fpl.GridPlot(shape=(2, 3), controller_ids=\"sync\")\n", + "# Figure of shape 2 x 3 with all controllers synced\n", + "figure_grid = fpl.Figure(shape=(2, 3), controller_ids=\"sync\")\n", "\n", "# Make a random image graphic for each subplot\n", - "for subplot in grid_plot:\n", + "for subplot in figure_grid:\n", " # create image data\n", " data = np.random.rand(512, 512)\n", " # add an image to the subplot\n", @@ -1346,17 +1341,17 @@ "\n", "# Define a function to update the image graphics with new data\n", "# add_animations will pass the gridplot to the animation function\n", - "def update_data(gp):\n", - " for sp in gp:\n", + "def update_data(f):\n", + " for subplot in f:\n", " new_data = np.random.rand(512, 512)\n", " # index the image graphic by name and set the data\n", - " sp[\"rand-img\"].data = new_data\n", + " subplot[\"rand-img\"].data = new_data\n", " \n", "# add the animation function\n", - "grid_plot.add_animations(update_data)\n", + "figure_grid.add_animations(update_data)\n", "\n", "# show the gridplot \n", - "grid_plot.show()" + "figure_grid.show()" ] }, { @@ -1378,7 +1373,7 @@ "source": [ "# positional indexing\n", "# row 0 and col 0\n", - "grid_plot[0, 0]" + "figure_grid[0, 0]" ] }, { @@ -1398,7 +1393,7 @@ }, "outputs": [], "source": [ - "grid_plot[0, 1].graphics" + "figure_grid[0, 1].graphics" ] }, { @@ -1418,7 +1413,7 @@ }, "outputs": [], "source": [ - "grid_plot[0, 1].graphics[0].vmax = 0.5" + "figure_grid[0, 1].graphics[0].vmax = 0.5" ] }, { @@ -1439,7 +1434,7 @@ "outputs": [], "source": [ "# you can give subplots human-readable string names\n", - "grid_plot[0, 2].name = \"top-right-plot\"" + "figure_grid[0, 2].name = \"top-right-plot\"" ] }, { @@ -1451,7 +1446,7 @@ }, "outputs": [], "source": [ - "grid_plot[\"top-right-plot\"]" + "figure_grid[\"top-right-plot\"]" ] }, { @@ -1464,7 +1459,7 @@ "outputs": [], "source": [ "# view its position\n", - "grid_plot[\"top-right-plot\"].position" + "figure_grid[\"top-right-plot\"].position" ] }, { @@ -1477,7 +1472,7 @@ "outputs": [], "source": [ "# these are really the same\n", - "grid_plot[\"top-right-plot\"] is grid_plot[0, 2]" + "figure_grid[\"top-right-plot\"] is figure_grid[0, 2]" ] }, { @@ -1497,7 +1492,7 @@ }, "outputs": [], "source": [ - "grid_plot[\"top-right-plot\"][\"rand-img\"].vmin = 0.5" + "figure_grid[\"top-right-plot\"][\"rand-img\"].vmin = 0.5" ] }, { @@ -1505,7 +1500,7 @@ "id": "6a5b4368-ae4d-442c-a11f-45c70267339b", "metadata": {}, "source": [ - "## GridPlot customization" + "## Figure subplot customization" ] }, { @@ -1517,8 +1512,8 @@ }, "outputs": [], "source": [ - "# grid with 2 rows and 3 columns\n", - "grid_shape = (2, 3)\n", + "# 2 rows and 3 columns\n", + "shape = (2, 3)\n", "\n", "# pan-zoom controllers for each view\n", "# views are synced if they have the \n", @@ -1536,15 +1531,15 @@ "]\n", "\n", "# Create the grid plot\n", - "grid_plot = fpl.GridPlot(\n", - " shape=grid_shape,\n", + "figure_grid = fpl.Figure(\n", + " shape=shape,\n", " controller_ids=controller_ids,\n", " names=names,\n", ")\n", "\n", "\n", "# Make a random image graphic for each subplot\n", - "for subplot in grid_plot:\n", + "for subplot in figure_grid:\n", " data = np.random.rand(512, 512)\n", " # create and add an ImageGraphic\n", " subplot.add_image(data=data, name=\"rand-image\")\n", @@ -1558,8 +1553,8 @@ " subplot[\"rand-image\"].data = new_data\n", "\n", "# add the animation\n", - "grid_plot.add_animations(set_random_frame)\n", - "grid_plot.show()" + "figure_grid.add_animations(set_random_frame)\n", + "figure_grid.show()" ] }, { @@ -1580,7 +1575,7 @@ "outputs": [], "source": [ "# can access subplot by name\n", - "grid_plot[\"subplot0\"]" + "figure_grid[\"subplot0\"]" ] }, { @@ -1593,7 +1588,7 @@ "outputs": [], "source": [ "# can access subplot by index\n", - "grid_plot[0, 0]" + "figure_grid[0, 0]" ] }, { @@ -1616,7 +1611,7 @@ "outputs": [], "source": [ "# can access graphic directly via name\n", - "grid_plot[\"subplot0\"][\"rand-image\"]" + "figure_grid[\"subplot0\"][\"rand-image\"]" ] }, { @@ -1628,8 +1623,8 @@ }, "outputs": [], "source": [ - "grid_plot[\"subplot0\"][\"rand-image\"].vmin = 0.6\n", - "grid_plot[\"subplot0\"][\"rand-image\"].vmax = 0.8" + "figure_grid[\"subplot0\"][\"rand-image\"].vmin = 0.6\n", + "figure_grid[\"subplot0\"][\"rand-image\"].vmax = 0.8" ] }, { @@ -1649,8 +1644,8 @@ }, "outputs": [], "source": [ - "grid_plot[1, 0][\"rand-image\"].vim = 0.1\n", - "grid_plot[1, 0][\"rand-image\"].vmax = 0.3" + "figure_grid[1, 0][\"rand-image\"].vim = 0.1\n", + "figure_grid[1, 0][\"rand-image\"].vmax = 0.3" ] } ], diff --git a/examples/desktop/gridplot/gridplot.py b/examples/desktop/gridplot/gridplot.py index 3acf6a8ba..2669dd49b 100644 --- a/examples/desktop/gridplot/gridplot.py +++ b/examples/desktop/gridplot/gridplot.py @@ -10,25 +10,23 @@ import imageio.v3 as iio -plot = fpl.GridPlot(shape=(2, 2)) -# to force a specific framework such as glfw: -# plot = fpl.GridPlot(canvas="glfw") +fig = fpl.Figure(shape=(2, 2)) im = iio.imread("imageio:clock.png") im2 = iio.imread("imageio:astronaut.png") im3 = iio.imread("imageio:coffee.png") im4 = iio.imread("imageio:hubble_deep_field.png") -plot[0, 0].add_image(data=im) -plot[0, 1].add_image(data=im2) -plot[1, 0].add_image(data=im3) -plot[1, 1].add_image(data=im4) +fig[0, 0].add_image(data=im) +fig[0, 1].add_image(data=im2) +fig[1, 0].add_image(data=im3) +fig[1, 1].add_image(data=im4) -plot.show() +fig.show() -plot.canvas.set_logical_size(800, 800) +fig.canvas.set_logical_size(800, 800) -for subplot in plot: +for subplot in fig: subplot.auto_scale() if __name__ == "__main__": diff --git a/examples/desktop/gridplot/gridplot_non_square.py b/examples/desktop/gridplot/gridplot_non_square.py index fe43a3c04..ea93096dc 100644 --- a/examples/desktop/gridplot/gridplot_non_square.py +++ b/examples/desktop/gridplot/gridplot_non_square.py @@ -10,23 +10,21 @@ import imageio.v3 as iio -plot = fpl.GridPlot(shape=(2, 2), controller_ids="sync") -# to force a specific framework such as glfw: -# plot = fpl.GridPlot(canvas="glfw") +fig = fpl.Figure(shape=(2, 2), controller_ids="sync") im = iio.imread("imageio:clock.png") im2 = iio.imread("imageio:astronaut.png") im3 = iio.imread("imageio:coffee.png") -plot[0, 0].add_image(data=im) -plot[0, 1].add_image(data=im2) -plot[1, 0].add_image(data=im3) +fig[0, 0].add_image(data=im) +fig[0, 1].add_image(data=im2) +fig[1, 0].add_image(data=im3) -plot.show() +fig.show() -plot.canvas.set_logical_size(800, 800) +fig.canvas.set_logical_size(800, 800) -for subplot in plot: +for subplot in fig: subplot.auto_scale() if __name__ == "__main__": diff --git a/examples/desktop/heatmap/heatmap.py b/examples/desktop/heatmap/heatmap.py index 45c340cbd..fa5ec6715 100644 --- a/examples/desktop/heatmap/heatmap.py +++ b/examples/desktop/heatmap/heatmap.py @@ -9,9 +9,8 @@ import fastplotlib as fpl import numpy as np -plot = fpl.Plot() -# to force a specific framework such as glfw: -# plot = fpl.Plot(canvas="glfw") + +fig = fpl.Figure() xs = np.linspace(0, 1_000, 10_000) @@ -24,13 +23,13 @@ data[1::2] = cosine # plot the image data -heatmap_graphic = plot.add_heatmap(data=data, name="heatmap") +heatmap_graphic = fig[0, 0].add_heatmap(data=data, name="heatmap") -plot.show() +fig.show() -plot.canvas.set_logical_size(1500, 1500) +fig.canvas.set_logical_size(1500, 1500) -plot.auto_scale() +fig[0, 0].auto_scale() if __name__ == "__main__": print(__doc__) diff --git a/examples/desktop/heatmap/heatmap_cmap.py b/examples/desktop/heatmap/heatmap_cmap.py index afc67f5b8..a1434bb0e 100644 --- a/examples/desktop/heatmap/heatmap_cmap.py +++ b/examples/desktop/heatmap/heatmap_cmap.py @@ -9,9 +9,8 @@ import fastplotlib as fpl import numpy as np -plot = fpl.Plot() -# to force a specific framework such as glfw: -# plot = fpl.Plot(canvas="glfw") + +fig = fpl.Figure() xs = np.linspace(0, 1_000, 10_000) @@ -24,13 +23,13 @@ data[1::2] = cosine # plot the image data -heatmap_graphic = plot.add_heatmap(data=data, name="heatmap") +heatmap_graphic = fig[0, 0].add_heatmap(data=data, name="heatmap") -plot.show() +fig.show() -plot.canvas.set_logical_size(1500, 1500) +fig.canvas.set_logical_size(1500, 1500) -plot.auto_scale() +fig[0, 0].auto_scale() heatmap_graphic.cmap = "viridis" diff --git a/examples/desktop/heatmap/heatmap_data.py b/examples/desktop/heatmap/heatmap_data.py index 78e819ab8..67aee1668 100644 --- a/examples/desktop/heatmap/heatmap_data.py +++ b/examples/desktop/heatmap/heatmap_data.py @@ -9,9 +9,8 @@ import fastplotlib as fpl import numpy as np -plot = fpl.Plot() -# to force a specific framework such as glfw: -# plot = fpl.Plot(canvas="glfw") + +fig = fpl.Figure() xs = np.linspace(0, 1_000, 10_000) @@ -24,13 +23,13 @@ data[1::2] = cosine # plot the image data -heatmap_graphic = plot.add_heatmap(data=data, name="heatmap") +heatmap_graphic = fig[0, 0].add_heatmap(data=data, name="heatmap") -plot.show() +fig.show() -plot.canvas.set_logical_size(1500, 1500) +fig.canvas.set_logical_size(1500, 1500) -plot.auto_scale() +fig[0, 0].auto_scale() heatmap_graphic.data[:5_000] = sine heatmap_graphic.data[5_000:] = cosine diff --git a/examples/desktop/heatmap/heatmap_vmin_vmax.py b/examples/desktop/heatmap/heatmap_vmin_vmax.py index 7aae1d6d3..6fe8a08b8 100644 --- a/examples/desktop/heatmap/heatmap_vmin_vmax.py +++ b/examples/desktop/heatmap/heatmap_vmin_vmax.py @@ -9,9 +9,8 @@ import fastplotlib as fpl import numpy as np -plot = fpl.Plot() -# to force a specific framework such as glfw: -# plot = fpl.Plot(canvas="glfw") + +fig = fpl.Figure() xs = np.linspace(0, 1_000, 10_000) @@ -24,13 +23,13 @@ data[1::2] = cosine # plot the image data -heatmap_graphic = plot.add_heatmap(data=data, name="heatmap") +heatmap_graphic = fig[0, 0].add_heatmap(data=data, name="heatmap") -plot.show() +fig.show() -plot.canvas.set_logical_size(1500, 1500) +fig.canvas.set_logical_size(1500, 1500) -plot.auto_scale() +fig[0, 0].auto_scale() heatmap_graphic.cmap.vmin = -0.5 heatmap_graphic.cmap.vmax = 0.5 diff --git a/examples/desktop/image/image_cmap.py b/examples/desktop/image/image_cmap.py index b7f7b39af..bb8e9f9d8 100644 --- a/examples/desktop/image/image_cmap.py +++ b/examples/desktop/image/image_cmap.py @@ -9,21 +9,18 @@ import fastplotlib as fpl import imageio.v3 as iio - -plot = fpl.Plot() -# to force a specific framework such as glfw: -# plot = fpl.Plot(canvas="glfw") - im = iio.imread("imageio:camera.png") +fig = fpl.Figure() + # plot the image data -image_graphic = plot.add_image(data=im, name="random-image") +image_graphic = fig[0, 0].add_image(data=im, name="random-image") -plot.show() +fig.show() -plot.canvas.set_logical_size(800, 800) +fig.canvas.set_logical_size(800, 800) -plot.auto_scale() +fig[0, 0].auto_scale() image_graphic.cmap = "viridis" diff --git a/examples/desktop/image/image_rgb.py b/examples/desktop/image/image_rgb.py index 2642962fd..ce7e151d0 100644 --- a/examples/desktop/image/image_rgb.py +++ b/examples/desktop/image/image_rgb.py @@ -10,20 +10,18 @@ import imageio.v3 as iio -plot = fpl.Plot() -# to force a specific framework such as glfw: -# plot = fpl.Plot(canvas="glfw") - im = iio.imread("imageio:astronaut.png") +fig = fpl.Figure() + # plot the image data -image_graphic = plot.add_image(data=im, name="iio astronaut") +image_graphic = fig[0, 0].add_image(data=im, name="iio astronaut") -plot.show() +fig.show() -plot.canvas.set_logical_size(800, 800) +fig.canvas.set_logical_size(800, 800) -plot.auto_scale() +fig[0, 0].auto_scale() if __name__ == "__main__": diff --git a/examples/desktop/image/image_rgbvminvmax.py b/examples/desktop/image/image_rgbvminvmax.py index e5c4af531..9725c038a 100644 --- a/examples/desktop/image/image_rgbvminvmax.py +++ b/examples/desktop/image/image_rgbvminvmax.py @@ -10,20 +10,18 @@ import imageio.v3 as iio -plot = fpl.Plot() -# to force a specific framework such as glfw: -# plot = fpl.Plot(canvas="glfw") - im = iio.imread("imageio:astronaut.png") +fig = fpl.Figure() + # plot the image data -image_graphic = plot.add_image(data=im, name="iio astronaut") +image_graphic = fig[0, 0].add_image(data=im, name="iio astronaut") -plot.show() +fig.show() -plot.canvas.set_logical_size(800, 800) +fig.canvas.set_logical_size(800, 800) -plot.auto_scale() +fig[0, 0].auto_scale() image_graphic.cmap.vmin = 0.5 image_graphic.cmap.vmax = 0.75 diff --git a/examples/desktop/image/image_simple.py b/examples/desktop/image/image_simple.py index 2d273ad68..a640974ed 100644 --- a/examples/desktop/image/image_simple.py +++ b/examples/desktop/image/image_simple.py @@ -10,20 +10,18 @@ import imageio.v3 as iio -plot = fpl.Plot() -# to force a specific framework such as glfw: -# plot = fpl.Plot(canvas="glfw") +fig = fpl.Figure() data = iio.imread("imageio:camera.png") # plot the image data -image_graphic = plot.add_image(data=data, name="iio camera") +image_graphic = fig[0, 0].add_image(data=data, name="iio camera") -plot.show() +fig.show() -plot.canvas.set_logical_size(800, 800) +fig.canvas.set_logical_size(800, 800) -plot.auto_scale() +fig[0, 0].auto_scale() if __name__ == "__main__": print(__doc__) diff --git a/examples/desktop/image/image_vminvmax.py b/examples/desktop/image/image_vminvmax.py index e764f6775..3c8607aef 100644 --- a/examples/desktop/image/image_vminvmax.py +++ b/examples/desktop/image/image_vminvmax.py @@ -10,20 +10,18 @@ import imageio.v3 as iio -plot = fpl.Plot() -# to force a specific framework such as glfw: -# plot = fpl.Plot(canvas="glfw") +fig = fpl.Figure() data = iio.imread("imageio:astronaut.png") # plot the image data -image_graphic = plot.add_image(data=data, name="iio astronaut") +image_graphic = fig[0, 0].add_image(data=data, name="iio astronaut") -plot.show() +fig.show() -plot.canvas.set_logical_size(800, 800) +fig.canvas.set_logical_size(800, 800) -plot.auto_scale() +fig[0, 0].auto_scale() image_graphic.cmap.vmin = 0.5 image_graphic.cmap.vmax = 0.75 diff --git a/examples/desktop/image/image_widget.py b/examples/desktop/image/image_widget.py index c50d914d3..80aafe0b1 100644 --- a/examples/desktop/image/image_widget.py +++ b/examples/desktop/image/image_widget.py @@ -5,13 +5,12 @@ When run in a notebook, or with the Qt GUI backend, sliders are also shown. """ -import numpy as np import fastplotlib as fpl import imageio.v3 as iio # not a fastplotlib dependency, only used for examples a = iio.imread("imageio:camera.png") -iw = fpl.widgets.ImageWidget(data=a, cmap="viridis") +iw = fpl.ImageWidget(data=a, cmap="viridis") iw.show() diff --git a/examples/desktop/line/line.py b/examples/desktop/line/line.py index 8cab1954f..56575a810 100644 --- a/examples/desktop/line/line.py +++ b/examples/desktop/line/line.py @@ -10,9 +10,7 @@ import numpy as np -plot = fpl.Plot() -# to force a specific framework such as glfw: -# plot = fpl.Plot(canvas="glfw") +fig = fpl.Figure() xs = np.linspace(-10, 10, 100) # sine wave @@ -28,20 +26,20 @@ ys = np.sinc(xs) * 3 + 8 sinc = np.dstack([xs, ys])[0] -sine_graphic = plot.add_line(data=sine, thickness=5, colors="magenta") +sine_graphic = fig[0, 0].add_line(data=sine, thickness=5, colors="magenta") # you can also use colormaps for lines! -cosine_graphic = plot.add_line(data=cosine, thickness=12, cmap="autumn") +cosine_graphic = fig[0, 0].add_line(data=cosine, thickness=12, cmap="autumn") # or a list of colors for each datapoint colors = ["r"] * 25 + ["purple"] * 25 + ["y"] * 25 + ["b"] * 25 -sinc_graphic = plot.add_line(data=sinc, thickness=5, colors=colors) +sinc_graphic = fig[0, 0].add_line(data=sinc, thickness=5, colors=colors) -plot.show() +fig.show() -plot.canvas.set_logical_size(800, 800) +fig.canvas.set_logical_size(800, 800) -plot.auto_scale() +fig[0, 0].auto_scale() if __name__ == "__main__": diff --git a/examples/desktop/line/line_cmap.py b/examples/desktop/line/line_cmap.py index b196132ed..7d8e1e7d6 100644 --- a/examples/desktop/line/line_cmap.py +++ b/examples/desktop/line/line_cmap.py @@ -10,9 +10,7 @@ import numpy as np -plot = fpl.Plot() -# to force a specific framework such as glfw: -# plot = fpl.Plot(canvas="glfw") +fig = fpl.Figure() xs = np.linspace(-10, 10, 100) # sine wave @@ -24,7 +22,7 @@ cosine = np.dstack([xs, ys])[0] # cmap_values from an array, so the colors on the sine line will be based on the sine y-values -sine_graphic = plot.add_line( +sine_graphic = fig[0, 0].add_line( data=sine, thickness=10, cmap="plasma", @@ -33,16 +31,16 @@ # qualitative colormaps, useful for cluster labels or other types of categorical labels cmap_values = [0] * 25 + [5] * 10 + [1] * 35 + [2] * 30 -cosine_graphic = plot.add_line( +cosine_graphic = fig[0, 0].add_line( data=cosine, thickness=10, cmap="tab10", cmap_values=cmap_values ) -plot.show() +fig.show() -plot.canvas.set_logical_size(800, 800) +fig.canvas.set_logical_size(800, 800) if __name__ == "__main__": print(__doc__) diff --git a/examples/desktop/line/line_colorslice.py b/examples/desktop/line/line_colorslice.py index f2aca8125..4df666531 100644 --- a/examples/desktop/line/line_colorslice.py +++ b/examples/desktop/line/line_colorslice.py @@ -10,9 +10,7 @@ import numpy as np -plot = fpl.Plot() -# to force a specific framework such as glfw: -# plot = fpl.Plot(canvas="glfw") +fig = fpl.Figure() xs = np.linspace(-10, 10, 100) # sine wave @@ -28,16 +26,16 @@ ys = np.sinc(xs) * 3 + 8 sinc = np.dstack([xs, ys])[0] -sine_graphic = plot.add_line(data=sine, thickness=5, colors="magenta") +sine_graphic = fig[0, 0].add_line(data=sine, thickness=5, colors="magenta") # you can also use colormaps for lines! -cosine_graphic = plot.add_line(data=cosine, thickness=12, cmap="autumn") +cosine_graphic = fig[0, 0].add_line(data=cosine, thickness=12, cmap="autumn") # or a list of colors for each datapoint colors = ["r"] * 25 + ["purple"] * 25 + ["y"] * 25 + ["b"] * 25 -sinc_graphic = plot.add_line(data=sinc, thickness=5, colors=colors) +sinc_graphic = fig[0, 0].add_line(data=sinc, thickness=5, colors=colors) -plot.show() +fig.show() # indexing of colors cosine_graphic.colors[:15] = "magenta" @@ -58,9 +56,9 @@ key2 = np.array([True, False, True, False, True, True, True, True]) cosine_graphic.colors[key2] = "Green" -plot.canvas.set_logical_size(800, 800) +fig.canvas.set_logical_size(800, 800) -plot.auto_scale() +fig[0, 0].auto_scale() if __name__ == "__main__": diff --git a/examples/desktop/line/line_dataslice.py b/examples/desktop/line/line_dataslice.py index ea87ba552..12a5f0f04 100644 --- a/examples/desktop/line/line_dataslice.py +++ b/examples/desktop/line/line_dataslice.py @@ -10,9 +10,7 @@ import numpy as np -plot = fpl.Plot() -# to force a specific framework such as glfw: -# plot = fpl.Plot(canvas="glfw") +fig = fpl.Figure() xs = np.linspace(-10, 10, 100) # sine wave @@ -28,16 +26,16 @@ ys = np.sinc(xs) * 3 + 8 sinc = np.dstack([xs, ys])[0] -sine_graphic = plot.add_line(data=sine, thickness=5, colors="magenta") +sine_graphic = fig[0, 0].add_line(data=sine, thickness=5, colors="magenta") # you can also use colormaps for lines! -cosine_graphic = plot.add_line(data=cosine, thickness=12, cmap="autumn") +cosine_graphic = fig[0, 0].add_line(data=cosine, thickness=12, cmap="autumn") # or a list of colors for each datapoint colors = ["r"] * 25 + ["purple"] * 25 + ["y"] * 25 + ["b"] * 25 -sinc_graphic = plot.add_line(data=sinc, thickness=5, colors=colors) +sinc_graphic = fig[0, 0].add_line(data=sinc, thickness=5, colors=colors) -plot.show() +fig.show() cosine_graphic.data[10:50:5, :2] = sine[10:50:5] cosine_graphic.data[90:, 1] = 7 @@ -47,9 +45,9 @@ key2 = np.array([True, False, True, False, True, True, True, True]) sinc_graphic.data[key2] = np.array([[5, 1, 2]]) -plot.canvas.set_logical_size(800, 800) +fig.canvas.set_logical_size(800, 800) -plot.auto_scale() +fig[0, 0].auto_scale() if __name__ == "__main__": diff --git a/examples/desktop/line/line_present_scaling.py b/examples/desktop/line/line_present_scaling.py index 327186c16..d334e6fbd 100644 --- a/examples/desktop/line/line_present_scaling.py +++ b/examples/desktop/line/line_present_scaling.py @@ -10,9 +10,7 @@ import numpy as np -plot = fpl.Plot() -# to force a specific framework such as glfw: -# plot = fpl.Plot(canvas="glfw") +fig = fpl.Figure() xs = np.linspace(-10, 10, 100) # sine wave @@ -28,22 +26,22 @@ ys = np.sinc(xs) * 3 + 8 sinc = np.dstack([xs, ys])[0] -sine_graphic = plot.add_line(data=sine, thickness=5, colors="magenta") +sine_graphic = fig[0, 0].add_line(data=sine, thickness=5, colors="magenta") # you can also use colormaps for lines! -cosine_graphic = plot.add_line(data=cosine, thickness=12, cmap="autumn") +cosine_graphic = fig[0, 0].add_line(data=cosine, thickness=12, cmap="autumn") # or a list of colors for each datapoint colors = ["r"] * 25 + ["purple"] * 25 + ["y"] * 25 + ["b"] * 25 -sinc_graphic = plot.add_line(data=sinc, thickness=5, colors=colors) +sinc_graphic = fig[0, 0].add_line(data=sinc, thickness=5, colors=colors) -plot.show() +fig.show() sinc_graphic.present = False -plot.canvas.set_logical_size(800, 800) +fig.canvas.set_logical_size(800, 800) -plot.auto_scale() +fig[0, 0].auto_scale() if __name__ == "__main__": diff --git a/examples/desktop/line_collection/line_collection.py b/examples/desktop/line_collection/line_collection.py index 071da2e2e..dd6f3ca33 100644 --- a/examples/desktop/line_collection/line_collection.py +++ b/examples/desktop/line_collection/line_collection.py @@ -27,15 +27,13 @@ def make_circle(center, radius: float, n_points: int = 75) -> np.ndarray: pos_xy = np.vstack(circles) -plot = fpl.Plot() -# to force a specific framework such as glfw: -# plot = fpl.Plot(canvas="glfw") +fig = fpl.Figure() -plot.add_line_collection(circles, cmap="jet", thickness=5) +fig[0, 0].add_line_collection(circles, cmap="jet", thickness=5) -plot.show() +fig.show() -plot.canvas.set_logical_size(800, 800) +fig.canvas.set_logical_size(800, 800) if __name__ == "__main__": print(__doc__) diff --git a/examples/desktop/line_collection/line_collection_cmap_values.py b/examples/desktop/line_collection/line_collection_cmap_values.py index 3623c20c3..9eeef40f8 100644 --- a/examples/desktop/line_collection/line_collection_cmap_values.py +++ b/examples/desktop/line_collection/line_collection_cmap_values.py @@ -33,20 +33,18 @@ def make_circle(center, radius: float, n_points: int = 75) -> np.ndarray: # highest values, lowest values, mid-high values, mid values cmap_values = [10] * 4 + [0] * 4 + [7] * 4 + [5] * 4 -plot = fpl.Plot() -# to force a specific framework such as glfw: -# plot = fpl.Plot(canvas="glfw") +fig = fpl.Figure() -plot.add_line_collection( +fig[0, 0].add_line_collection( circles, cmap="bwr", cmap_values=cmap_values, thickness=10 ) -plot.show() +fig.show() -plot.canvas.set_logical_size(800, 800) +fig.canvas.set_logical_size(800, 800) if __name__ == "__main__": print(__doc__) diff --git a/examples/desktop/line_collection/line_collection_cmap_values_qualitative.py b/examples/desktop/line_collection/line_collection_cmap_values_qualitative.py index f56d2ca02..85f0724d8 100644 --- a/examples/desktop/line_collection/line_collection_cmap_values_qualitative.py +++ b/examples/desktop/line_collection/line_collection_cmap_values_qualitative.py @@ -39,20 +39,18 @@ def make_circle(center, radius: float, n_points: int = 75) -> np.ndarray: 1, 1, 1, 5 ] -plot = fpl.Plot() -# to force a specific framework such as glfw: -# plot = fpl.Plot(canvas="glfw") +fig = fpl.Figure() -plot.add_line_collection( +fig[0, 0].add_line_collection( circles, cmap="tab10", cmap_values=cmap_values, thickness=10 ) -plot.show() +fig.show() -plot.canvas.set_logical_size(800, 800) +fig.canvas.set_logical_size(800, 800) if __name__ == "__main__": print(__doc__) diff --git a/examples/desktop/line_collection/line_collection_colors.py b/examples/desktop/line_collection/line_collection_colors.py index d74f65d82..d53afcd5b 100644 --- a/examples/desktop/line_collection/line_collection_colors.py +++ b/examples/desktop/line_collection/line_collection_colors.py @@ -31,15 +31,13 @@ def make_circle(center, radius: float, n_points: int = 75) -> np.ndarray: # this will produce 16 circles so we will define 16 colors colors = ["blue"] * 4 + ["red"] * 4 + ["yellow"] * 4 + ["w"] * 4 -plot = fpl.Plot() -# to force a specific framework such as glfw: -# plot = fpl.Plot(canvas="glfw") +fig = fpl.Figure() -plot.add_line_collection(circles, colors=colors, thickness=10) +fig[0, 0].add_line_collection(circles, colors=colors, thickness=10) -plot.show() +fig.show() -plot.canvas.set_logical_size(800, 800) +fig.canvas.set_logical_size(800, 800) if __name__ == "__main__": print(__doc__) diff --git a/examples/desktop/line_collection/line_stack.py b/examples/desktop/line_collection/line_stack.py index 5a94caee7..cf5d933e3 100644 --- a/examples/desktop/line_collection/line_stack.py +++ b/examples/desktop/line_collection/line_stack.py @@ -17,16 +17,14 @@ # make 25 lines data = np.vstack([ys] * 25) -plot = fpl.Plot() -# to force a specific framework such as glfw: -# plot = fpl.Plot(canvas="glfw") +fig = fpl.Figure() # line stack takes all the same arguments as line collection and behaves similarly -plot.add_line_stack(data, cmap="jet") +fig[0, 0].add_line_stack(data, cmap="jet") -plot.show(maintain_aspect=False) +fig.show(maintain_aspect=False) -plot.canvas.set_logical_size(900, 600) +fig.canvas.set_logical_size(900, 600) if __name__ == "__main__": print(__doc__) diff --git a/examples/desktop/scatter/scatter.py b/examples/desktop/scatter/scatter.py index 778f37deb..c47306722 100644 --- a/examples/desktop/scatter/scatter.py +++ b/examples/desktop/scatter/scatter.py @@ -10,9 +10,7 @@ import numpy as np from pathlib import Path -plot = fpl.Plot() -# to force a specific framework such as glfw: -# plot = fpl.Plot(canvas="glfw") +fig = fpl.Figure() data_path = Path(__file__).parent.parent.joinpath("data", "iris.npy") data = np.load(data_path) @@ -20,13 +18,13 @@ n_points = 50 colors = ["yellow"] * n_points + ["cyan"] * n_points + ["magenta"] * n_points -scatter_graphic = plot.add_scatter(data=data[:, :-1], sizes=6, alpha=0.7, colors=colors) +scatter_graphic = fig[0, 0].add_scatter(data=data[:, :-1], sizes=6, alpha=0.7, colors=colors) -plot.show() +fig.show() -plot.canvas.set_logical_size(800, 800) +fig.canvas.set_logical_size(800, 800) -plot.auto_scale() +fig[0, 0].auto_scale() if __name__ == "__main__": diff --git a/examples/desktop/scatter/scatter_cmap.py b/examples/desktop/scatter/scatter_cmap.py index 3e986d5d5..f1bba98c3 100644 --- a/examples/desktop/scatter/scatter_cmap.py +++ b/examples/desktop/scatter/scatter_cmap.py @@ -12,28 +12,23 @@ from sklearn.cluster import AgglomerativeClustering -plot = fpl.Plot() -# to force a specific framework such as glfw: -# plot = fpl.Plot(canvas="glfw") +fig = fpl.Figure() data_path = Path(__file__).parent.parent.joinpath("data", "iris.npy") data = np.load(data_path) - agg = AgglomerativeClustering(n_clusters=3) - agg.fit_predict(data) - -scatter_graphic = plot.add_scatter( +scatter_graphic = fig[0, 0].add_scatter( data=data[:, :-1], sizes=15, alpha=0.7, cmap="Set1", cmap_values=agg.labels_ ) -plot.show() +fig.show() -plot.canvas.set_logical_size(800, 800) +fig.canvas.set_logical_size(800, 800) -plot.auto_scale() +fig[0, 0].auto_scale() scatter_graphic.cmap = "tab10" diff --git a/examples/desktop/scatter/scatter_colorslice.py b/examples/desktop/scatter/scatter_colorslice.py index d752cacbd..43f405b06 100644 --- a/examples/desktop/scatter/scatter_colorslice.py +++ b/examples/desktop/scatter/scatter_colorslice.py @@ -11,9 +11,7 @@ from pathlib import Path -plot = fpl.Plot() -# to force a specific framework such as glfw: -# plot = fpl.Plot(canvas="glfw") +fig = fpl.Figure() data_path = Path(__file__).parent.parent.joinpath("data", "iris.npy") data = np.load(data_path) @@ -21,13 +19,13 @@ n_points = 50 colors = ["yellow"] * n_points + ["cyan"] * n_points + ["magenta"] * n_points -scatter_graphic = plot.add_scatter(data=data[:, :-1], sizes=6, alpha=0.7, colors=colors) +scatter_graphic = fig[0, 0].add_scatter(data=data[:, :-1], sizes=6, alpha=0.7, colors=colors) -plot.show() +fig.show() -plot.canvas.set_logical_size(800, 800) +fig.canvas.set_logical_size(800, 800) -plot.auto_scale() +fig[0, 0].auto_scale() scatter_graphic.colors[0:75] = "red" scatter_graphic.colors[75:150] = "white" diff --git a/examples/desktop/scatter/scatter_dataslice.py b/examples/desktop/scatter/scatter_dataslice.py index 3008aab61..989b7c21c 100644 --- a/examples/desktop/scatter/scatter_dataslice.py +++ b/examples/desktop/scatter/scatter_dataslice.py @@ -11,9 +11,7 @@ from pathlib import Path -plot = fpl.Plot() -# to force a specific framework such as glfw: -# plot = fpl.Plot(canvas="glfw") +fig = fpl.Figure() data_path = Path(__file__).parent.parent.joinpath("data", "iris.npy") data = np.load(data_path) @@ -21,13 +19,13 @@ n_points = 50 colors = ["yellow"] * n_points + ["cyan"] * n_points + ["magenta"] * n_points -scatter_graphic = plot.add_scatter(data=data[:, :-1], sizes=6, alpha=0.7, colors=colors) +scatter_graphic = fig[0, 0].add_scatter(data=data[:, :-1], sizes=6, alpha=0.7, colors=colors) -plot.show() +fig.show() -plot.canvas.set_logical_size(800, 800) +fig.canvas.set_logical_size(800, 800) -plot.auto_scale() +fig[0, 0].auto_scale() scatter_graphic.data[0] = np.array([[5, 3, 1.5]]) scatter_graphic.data[1] = np.array([[4.3, 3.2, 1.3]]) diff --git a/examples/desktop/scatter/scatter_present.py b/examples/desktop/scatter/scatter_present.py index ad4be837f..5da4610bd 100644 --- a/examples/desktop/scatter/scatter_present.py +++ b/examples/desktop/scatter/scatter_present.py @@ -11,7 +11,7 @@ from pathlib import Path -plot = fpl.Plot() +fig = fpl.Figure() data_path = Path(__file__).parent.parent.joinpath("data", "iris.npy") data = np.load(data_path) @@ -19,16 +19,16 @@ n_points = 50 colors = ["yellow"] * n_points + ["cyan"] * n_points + ["magenta"] * n_points -scatter_graphic = plot.add_scatter(data=data[:, :-1], sizes=6, alpha=0.7, colors=colors) +scatter_graphic = fig[0, 0].add_scatter(data=data[:, :-1], sizes=6, alpha=0.7, colors=colors) colors = ["red"] * n_points + ["white"] * n_points + ["blue"] * n_points -scatter_graphic2 = plot.add_scatter(data=data[:, 1:], sizes=6, alpha=0.7, colors=colors) +scatter_graphic2 = fig[0, 0].add_scatter(data=data[:, 1:], sizes=6, alpha=0.7, colors=colors) -plot.show() +fig.show() -plot.canvas.set_logical_size(800, 800) +fig.canvas.set_logical_size(800, 800) -plot.auto_scale() +fig[0, 0].auto_scale() scatter_graphic.present = False diff --git a/examples/desktop/scatter/scatter_size.py b/examples/desktop/scatter/scatter_size.py index 5c1f97703..41a97ad53 100644 --- a/examples/desktop/scatter/scatter_size.py +++ b/examples/desktop/scatter/scatter_size.py @@ -8,14 +8,14 @@ import numpy as np import fastplotlib as fpl -# grid with 2 rows and 3 columns -grid_shape = (2, 1) +# figure with 2 rows and 3 columns +shape = (2, 1) # 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, names=names, size=(1000, 1000)) +fig = fpl.Figure(shape=shape, names=names, size=(1000, 1000)) # get y_values using sin function angles = np.arange(0, 20 * np.pi + 0.001, np.pi / 20) @@ -24,17 +24,17 @@ data = np.column_stack([x_values, y_values]) -plot["scalar_size"].add_scatter( +fig["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") +fig["array_size"].add_scatter(data=data, sizes=non_scalar_sizes, colors="red") -for graph in plot: +for graph in fig: graph.auto_scale(maintain_aspect=True) -plot.show() +fig.show() if __name__ == "__main__": print(__doc__) diff --git a/examples/notebooks/heatmap.ipynb b/examples/notebooks/heatmap.ipynb index 82583b1df..90c07a3cb 100644 --- a/examples/notebooks/heatmap.ipynb +++ b/examples/notebooks/heatmap.ipynb @@ -70,11 +70,11 @@ }, "outputs": [], "source": [ - "plot = fpl.Plot()\n", + "fig = fpl.Figure()\n", "\n", - "plot.add_heatmap(data, cmap=\"viridis\")\n", + "fig[0, 0].add_heatmap(data, cmap=\"viridis\")\n", "\n", - "plot.show(maintain_aspect=False)" + "fig.show(maintain_aspect=False)" ] }, { diff --git a/examples/notebooks/image_widget.ipynb b/examples/notebooks/image_widget.ipynb index 56d5c8a81..5136ba028 100644 --- a/examples/notebooks/image_widget.ipynb +++ b/examples/notebooks/image_widget.ipynb @@ -78,7 +78,7 @@ }, "outputs": [], "source": [ - "iw.gridplot[0, 0][\"image_widget_managed\"].cmap = \"gnuplot2\"" + "iw.figure[0, 0][\"image_widget_managed\"].cmap = \"gnuplot2\"" ] }, { @@ -115,7 +115,6 @@ "source": [ "iw_movie = ImageWidget(\n", " data=gray_movie, \n", - " slider_dims=[\"t\"],\n", " cmap=\"gray\"\n", ")" ] @@ -232,7 +231,7 @@ "outputs": [], "source": [ "iw_movie.set_data(new_data=new_data)\n", - "iw_movie.gridplot[0, 0].auto_scale()# sidecar is optional" + "iw_movie.figure[0, 0].auto_scale()# sidecar is optional" ] }, { @@ -341,7 +340,7 @@ }, "outputs": [], "source": [ - "iw_zfish.gridplot[\"plane-2\"]" + "iw_zfish.figure[\"plane-2\"]" ] }, { diff --git a/examples/notebooks/image_widget_test.ipynb b/examples/notebooks/image_widget_test.ipynb index c236ce9b7..321f7b84f 100644 --- a/examples/notebooks/image_widget_test.ipynb +++ b/examples/notebooks/image_widget_test.ipynb @@ -2,7 +2,7 @@ "cells": [ { "cell_type": "code", - "execution_count": null, + "execution_count": 3, "id": "07019035-83f2-4753-9e7c-628ae439b441", "metadata": { "tags": [] @@ -18,7 +18,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 4, "id": "10b8ab40-944d-472c-9b7e-cae8a129e7ce", "metadata": {}, "outputs": [], @@ -60,7 +60,7 @@ "iw = ImageWidget(\n", " data=a,\n", " cmap=\"viridis\",\n", - " grid_plot_kwargs={\"size\": (900, 600)},\n", + " figure_kwargs={\"size\": (900, 600)},\n", ")" ] }, @@ -81,9 +81,9 @@ "metadata": {}, "outputs": [], "source": [ - "plot_test(\"image-widget-single\", iw.gridplot)\n", - "iw.gridplot[0, 0][\"image_widget_managed\"].cmap = \"gnuplot2\"\n", - "plot_test(\"image-widget-single-gnuplot2\", iw.gridplot)" + "plot_test(\"image-widget-single\", iw.figure)\n", + "iw.figure[0, 0][\"image_widget_managed\"].cmap = \"gnuplot2\"\n", + "plot_test(\"image-widget-single-gnuplot2\", iw.figure)" ] }, { @@ -130,9 +130,8 @@ "source": [ "iw_movie = ImageWidget(\n", " data=gray_movie, \n", - " slider_dims=[\"t\"],\n", " cmap=\"gray\",\n", - " grid_plot_kwargs={\"size\": (900, 600)},\n", + " figure_kwargs={\"size\": (900, 600)},\n", ")" ] }, @@ -158,23 +157,23 @@ "# testing cell ignore\n", "assert iw_movie.sliders[\"t\"].max == gray_movie.shape[0] - 1\n", "assert iw_movie.sliders[\"t\"].min == 0\n", - "plot_test(\"image-widget-movie-single-0\", iw_movie.gridplot)\n", + "plot_test(\"image-widget-movie-single-0\", iw_movie.figure)\n", "iw_movie.sliders[\"t\"].value = 50\n", - "plot_test(\"image-widget-movie-single-50\", iw_movie.gridplot)\n", + "plot_test(\"image-widget-movie-single-50\", iw_movie.figure)\n", "iw_movie.sliders[\"t\"].value = 279\n", - "plot_test(\"image-widget-movie-single-279\", iw_movie.gridplot)\n", + "plot_test(\"image-widget-movie-single-279\", iw_movie.figure)\n", "iw_movie.sliders[\"t\"].value = 0\n", - "plot_test(\"image-widget-movie-single-0-reset\", iw_movie.gridplot)\n", + "plot_test(\"image-widget-movie-single-0-reset\", iw_movie.figure)\n", "iw_movie.sliders[\"t\"].value = 50\n", "iw_movie.window_funcs = {\"t\": (np.mean, 13)}\n", "# testing cell ignore\n", - "plot_test(\"image-widget-movie-single-50-window-mean-13\", iw_movie.gridplot)\n", + "plot_test(\"image-widget-movie-single-50-window-mean-13\", iw_movie.figure)\n", "iw_movie.window_funcs[\"t\"].window_size = 33\n", - "plot_test(\"image-widget-movie-single-50-window-mean-33\", iw_movie.gridplot)\n", + "plot_test(\"image-widget-movie-single-50-window-mean-33\", iw_movie.figure)\n", "iw_movie.window_funcs[\"t\"].func = np.max\n", - "plot_test(\"image-widget-movie-single-50-window-max-33\", iw_movie.gridplot)\n", + "plot_test(\"image-widget-movie-single-50-window-max-33\", iw_movie.figure)\n", "iw_movie.window_funcs = None\n", - "plot_test(\"image-widget-movie-single-50-window-reset\", iw_movie.gridplot)\n", + "plot_test(\"image-widget-movie-single-50-window-reset\", iw_movie.figure)\n", "iw_movie.sliders[\"t\"].value = 0" ] }, @@ -206,7 +205,7 @@ "outputs": [], "source": [ "iw_movie.set_data(new_data=new_data)\n", - "iw_movie.gridplot[0, 0].auto_scale()" + "iw_movie.figure[0, 0].auto_scale()" ] }, { @@ -216,7 +215,7 @@ "metadata": {}, "outputs": [], "source": [ - "plot_test(\"image-widget-movie-set_data\", iw_movie.gridplot)" + "plot_test(\"image-widget-movie-set_data\", iw_movie.figure)" ] }, { @@ -275,9 +274,6 @@ "execution_count": null, "id": "76535d56-e514-4c16-aa48-a6359f8019d5", "metadata": { - "jupyter": { - "source_hidden": true - }, "tags": [] }, "outputs": [], @@ -287,7 +283,7 @@ " window_funcs={\"t\": (np.mean, 5)},\n", " names=[f\"plane-{i}\" for i in range(n_planes)],\n", " cmap=\"gnuplot2\", \n", - " grid_plot_kwargs={\"size\": (900, 600)},\n", + " figure_kwargs={\"size\": (900, 600)},\n", ")" ] }, @@ -313,25 +309,25 @@ "# testing cell ignore\n", "assert iw_zfish.sliders[\"t\"].max == zfish_data.shape[0] - 1\n", "assert iw_zfish.sliders[\"t\"].min == 0\n", - "plot_test(\"image-widget-zfish-grid-init-mean-window-5\", iw_zfish.gridplot)\n", + "plot_test(\"image-widget-zfish-grid-init-mean-window-5\", iw_zfish.figure)\n", "iw_zfish.sliders[\"t\"].value = 50\n", - "plot_test(\"image-widget-zfish-grid-frame-50-mean-window-5\", iw_zfish.gridplot)\n", + "plot_test(\"image-widget-zfish-grid-frame-50-mean-window-5\", iw_zfish.figure)\n", "iw_zfish.window_funcs[\"t\"].window_size = 13\n", - "plot_test(\"image-widget-zfish-grid-frame-50-mean-window-13\", iw_zfish.gridplot)\n", + "plot_test(\"image-widget-zfish-grid-frame-50-mean-window-13\", iw_zfish.figure)\n", "iw_zfish.window_funcs = None\n", - "plot_test(\"image-widget-zfish-grid-frame-50\", iw_zfish.gridplot)\n", + "plot_test(\"image-widget-zfish-grid-frame-50\", iw_zfish.figure)\n", "iw_zfish.sliders[\"t\"].value = 99\n", - "plot_test(\"image-widget-zfish-grid-frame-99\", iw_zfish.gridplot)\n", + "plot_test(\"image-widget-zfish-grid-frame-99\", iw_zfish.figure)\n", "iw_zfish.sliders[\"t\"].value = 50\n", "iw_zfish.window_funcs = {\"t\": (np.max, 13)}\n", - "plot_test(\"image-widget-zfish-grid-frame-50-max-window-13\", iw_zfish.gridplot)\n", + "plot_test(\"image-widget-zfish-grid-frame-50-max-window-13\", iw_zfish.figure)\n", "iw_zfish.window_funcs = None\n", "iw_zfish.frame_apply = lambda frame: gaussian_filter(frame.astype(np.float32), sigma=3)\n", "iw_zfish.reset_vmin_vmax()\n", - "plot_test(\"image-widget-zfish-grid-frame-50-frame-apply-gaussian\", iw_zfish.gridplot)\n", + "plot_test(\"image-widget-zfish-grid-frame-50-frame-apply-gaussian\", iw_zfish.figure)\n", "iw_zfish.frame_apply = None\n", "iw_zfish.reset_vmin_vmax()\n", - "plot_test(\"image-widget-zfish-grid-frame-50-frame-apply-reset\", iw_zfish.gridplot)" + "plot_test(\"image-widget-zfish-grid-frame-50-frame-apply-reset\", iw_zfish.figure)" ] }, { @@ -347,13 +343,13 @@ " reset_indices=False\n", ")\n", "\n", - "plot_test(\"image-widget-zfish-grid-set_data-reset-indices-false\", iw_zfish.gridplot)\n", + "plot_test(\"image-widget-zfish-grid-set_data-reset-indices-false\", iw_zfish.figure)\n", "\n", "iw_zfish.set_data(\n", " [zfish_data[:, i] for i in range(n_planes - 1, -1, -1)],\n", " reset_indices=True\n", ")\n", - "plot_test(\"image-widget-zfish-grid-set_data-reset-indices-true\", iw_zfish.gridplot)" + "plot_test(\"image-widget-zfish-grid-set_data-reset-indices-true\", iw_zfish.figure)" ] }, { @@ -387,7 +383,7 @@ " data=zfish_data, # you can also provide a list of tzxy arrays\n", " window_funcs={\"t\": (np.mean, 5)},\n", " cmap=\"gnuplot2\", \n", - " grid_plot_kwargs={\"size\": (900, 600)},\n", + " figure_kwargs={\"size\": (900, 600)},\n", ")" ] }, @@ -410,28 +406,28 @@ "metadata": {}, "outputs": [], "source": [ - "# same tests as with the gridplot\n", + "# same tests as with the figure\n", "assert iw_z.sliders[\"t\"].max == zfish_data.shape[0] - 1\n", "assert iw_z.sliders[\"t\"].min == 0\n", - "plot_test(\"image-widget-zfish-init-mean-window-5\", iw_z.gridplot)\n", + "plot_test(\"image-widget-zfish-init-mean-window-5\", iw_z.figure)\n", "iw_z.sliders[\"t\"].value = 50\n", - "plot_test(\"image-widget-zfish-frame-50-mean-window-5\", iw_z.gridplot)\n", + "plot_test(\"image-widget-zfish-frame-50-mean-window-5\", iw_z.figure)\n", "iw_z.window_funcs[\"t\"].window_size = 13\n", - "plot_test(\"image-widget-zfish-frame-50-mean-window-13\", iw_z.gridplot)\n", + "plot_test(\"image-widget-zfish-frame-50-mean-window-13\", iw_z.figure)\n", "iw_z.window_funcs = None\n", - "plot_test(\"image-widget-zfish-frame-50\", iw_z.gridplot)\n", + "plot_test(\"image-widget-zfish-frame-50\", iw_z.figure)\n", "iw_z.sliders[\"t\"].value = 99\n", - "plot_test(\"image-widget-zfish-frame-99\", iw_z.gridplot)\n", + "plot_test(\"image-widget-zfish-frame-99\", iw_z.figure)\n", "iw_z.sliders[\"t\"].value = 50\n", "iw_z.window_funcs = {\"t\": (np.max, 13)}\n", - "plot_test(\"image-widget-zfish-frame-50-max-window-13\", iw_z.gridplot)\n", + "plot_test(\"image-widget-zfish-frame-50-max-window-13\", iw_z.figure)\n", "iw_z.window_funcs = None\n", "iw_z.frame_apply = lambda frame: gaussian_filter(frame.astype(np.float32), sigma=3)\n", "iw_z.reset_vmin_vmax()\n", - "plot_test(\"image-widget-zfish-frame-50-frame-apply-gaussian\", iw_z.gridplot)\n", + "plot_test(\"image-widget-zfish-frame-50-frame-apply-gaussian\", iw_z.figure)\n", "iw_z.frame_apply = None\n", "iw_z.reset_vmin_vmax()\n", - "plot_test(\"image-widget-zfish-frame-50-frame-apply-reset\", iw_z.gridplot)" + "plot_test(\"image-widget-zfish-frame-50-frame-apply-reset\", iw_z.figure)" ] }, { @@ -444,23 +440,66 @@ "iw_z.close()" ] }, + { + "cell_type": "markdown", + "id": "6716f255-44c2-400d-a2bf-254683e4cd9d", + "metadata": {}, + "source": [ + "# Test Mixed Shapes, RGB (and set data)" + ] + }, { "cell_type": "code", - "execution_count": null, - "id": "870627ef-09d8-44e4-8952-aedb702d1526", + "execution_count": 30, + "id": "ed783360-992d-40f8-bb6f-152a59edff43", "metadata": {}, "outputs": [], "source": [ - "notebook_finished()" + "zfish_data = np.load(\"./zfish_test.npy\")\n", + "zfish_frame_1 = zfish_data[0, 0, :, :]\n", + "zfish_frame_2 = zfish_data[20, 3, :, :]\n", + "movie = iio.imread(\"imageio:cockatoo.mp4\")\n", + "\n", + "iw_mixed_shapes = ImageWidget(\n", + " data=[zfish_frame_1, movie], # you can also provide a list of tzxy arrays\n", + " rgb=[False, True],\n", + " histogram_widget=True,\n", + " cmap=\"gnuplot2\", \n", + " figure_kwargs = {\"controller_ids\": None},\n", + ")\n", + "\n", + "iw_mixed_shapes.show()" + ] + }, + { + "cell_type": "code", + "execution_count": 29, + "id": "274c67b4-aa07-4fcf-a094-1b1e70d0378a", + "metadata": {}, + "outputs": [], + "source": [ + "iw_mixed_shapes.sliders[\"t\"].value = 50\n", + "plot_test(\"image-widget-zfish-mixed-rgb-cockatoo-frame-50\", iw_mixed_shapes.figure)\n", + "\n", + "#Set the data, changing the first array and also the size of the \"T\" slider\n", + "iw_mixed_shapes.set_data([zfish_frame_2, movie[:200, :, :, :]], reset_indices=True)\n", + "plot_test(\"image-widget-zfish-mixed-rgb-cockatoo-set-data\", iw_mixed_shapes.figure)\n", + "\n", + "#Check how a window function might work on the RGB data\n", + "iw_mixed_shapes.window_funcs = {\"t\": (np.mean, 4)}\n", + "iw_mixed_shapes.sliders[\"t\"].value = 20\n", + "plot_test(\"image-widget-zfish-mixed-rgb-cockatoo-windowrgb\", iw_mixed_shapes.figure)" ] }, { "cell_type": "code", "execution_count": null, - "id": "b8fff1a6-119e-4f03-ba3a-4c7b9e8c212b", + "id": "870627ef-09d8-44e4-8952-aedb702d1526", "metadata": {}, "outputs": [], - "source": [] + "source": [ + "notebook_finished()" + ] } ], "metadata": { @@ -479,7 +518,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.11.3" + "version": "3.11.8" } }, "nbformat": 4, diff --git a/examples/notebooks/linear_region_selector.ipynb b/examples/notebooks/linear_region_selector.ipynb index 43cea4f81..2ba40ed54 100644 --- a/examples/notebooks/linear_region_selector.ipynb +++ b/examples/notebooks/linear_region_selector.ipynb @@ -19,7 +19,7 @@ "import numpy as np\n", "from ipywidgets import IntRangeSlider, FloatRangeSlider, VBox\n", "\n", - "gp = fpl.GridPlot((2, 2))\n", + "fig = fpl.Figure((2, 2))\n", "\n", "# preallocated size for zoomed data\n", "zoomed_prealloc = 1_000\n", @@ -29,14 +29,14 @@ "sine = np.sin(xs) * 20\n", "\n", "# make sine along x axis\n", - "sine_graphic_x = gp[0, 0].add_line(sine)\n", + "sine_graphic_x = fig[0, 0].add_line(sine)\n", "\n", "# just something that looks different for line along y-axis\n", "sine_y = sine\n", "sine_y[sine_y > 0] = 0\n", "\n", "# sine along y axis\n", - "sine_graphic_y = gp[0, 1].add_line(np.column_stack([sine_y, xs]))\n", + "sine_graphic_y = fig[0, 1].add_line(np.column_stack([sine_y, xs]))\n", "\n", "# offset the position of the graphic to demonstrate `get_selected_data()` later\n", "sine_graphic_y.position_x = 50\n", @@ -50,8 +50,8 @@ "zoomed_init = np.column_stack([np.arange(zoomed_prealloc), np.random.rand(zoomed_prealloc)])\n", "\n", "# make line graphics for displaying zoomed data\n", - "zoomed_x = gp[1, 0].add_line(zoomed_init)\n", - "zoomed_y = gp[1, 1].add_line(zoomed_init)\n", + "zoomed_x = fig[1, 0].add_line(zoomed_init)\n", + "zoomed_y = fig[1, 1].add_line(zoomed_init)\n", "\n", "\n", "def interpolate(subdata: np.ndarray, axis: int):\n", @@ -67,21 +67,21 @@ " \"\"\"sets zoomed x selector data\"\"\"\n", " selected_data = ev.pick_info[\"selected_data\"]\n", " zoomed_x.data = interpolate(selected_data, axis=1) # use the y-values\n", - " gp[1, 0].auto_scale()\n", + " fig[1, 0].auto_scale()\n", "\n", "\n", "def set_zoom_y(ev):\n", " \"\"\"sets zoomed y selector data\"\"\"\n", " selected_data = ev.pick_info[\"selected_data\"]\n", " zoomed_y.data = -interpolate(selected_data, axis=0) # use the x-values\n", - " gp[1, 1].auto_scale()\n", + " fig[1, 1].auto_scale()\n", "\n", "\n", "# update zoomed plots when bounds change\n", "ls_x.selection.add_event_handler(set_zoom_x)\n", "ls_y.selection.add_event_handler(set_zoom_y)\n", "\n", - "gp.show()" + "fig.show()" ] }, { @@ -147,28 +147,25 @@ "metadata": {}, "outputs": [], "source": [ - "import fastplotlib as fpl\n", - "import numpy as np\n", - "\n", "# data to plot\n", "xs = np.linspace(0, 100, 1_000)\n", "sine = np.sin(xs) * 20\n", "cosine = np.cos(xs) * 20\n", "\n", - "plot = fpl.GridPlot((5, 1))\n", + "fig_stack = fpl.Figure((5, 1))\n", "\n", "# sines and cosines\n", "sines = [sine] * 2\n", "cosines = [cosine] * 2\n", "\n", "# make line stack\n", - "line_stack = plot[0, 0].add_line_stack(sines + cosines, separation=50)\n", + "line_stack = fig_stack[0, 0].add_line_stack(sines + cosines, separation=50)\n", "\n", "# make selector\n", "selector = line_stack.add_linear_region_selector()\n", "\n", "# populate subplots with preallocated graphics\n", - "for i, subplot in enumerate(plot):\n", + "for i, subplot in enumerate(fig_stack):\n", " if i == 0:\n", " # skip the first one\n", " continue\n", @@ -182,12 +179,12 @@ " \n", " for i in range(len(zoomed_data)):\n", " data = interpolate(zoomed_data[i], axis=1)\n", - " plot[i + 1, 0][\"zoomed\"].data = data\n", - " plot[i + 1, 0].auto_scale()\n", + " fig_stack[i + 1, 0][\"zoomed\"].data = data\n", + " fig_stack[i + 1, 0].auto_scale()\n", "\n", "\n", "selector.selection.add_event_handler(update_zoomed_subplots)\n", - "plot.show()" + "fig_stack.show()" ] }, { @@ -213,19 +210,19 @@ "sine = np.sin(xs) * 20\n", "cosine = np.cos(xs) * 20\n", "\n", - "plot = fpl.GridPlot((1, 2))\n", + "fig_stack_large = fpl.Figure((1, 2))\n", "\n", "# sines and cosines\n", "sines = [sine] * 1_00\n", "cosines = [cosine] * 1_00\n", "\n", "# make line stack\n", - "line_stack = plot[0, 0].add_line_stack(sines + cosines, separation=50)\n", + "line_stack = fig_stack_large[0, 0].add_line_stack(sines + cosines, separation=50)\n", "\n", "# make selector\n", "stack_selector = line_stack.add_linear_region_selector(padding=200)\n", "\n", - "zoomed_line_stack = plot[0, 1].add_line_stack([zoomed_init] * 2_000, separation=50, name=\"zoomed\")\n", + "zoomed_line_stack = fig_stack_large[0, 1].add_line_stack([zoomed_init] * 2_000, separation=50, name=\"zoomed\")\n", " \n", "def update_zoomed_stack(ev):\n", " \"\"\"update the zoomed subplots\"\"\"\n", @@ -235,11 +232,11 @@ " data = interpolate(zoomed_data[i], axis=1)\n", " zoomed_line_stack.graphics[i].data = data\n", " \n", - " plot[0, 1].auto_scale()\n", + " fig_stack_large[0, 1].auto_scale()\n", "\n", "\n", "stack_selector.selection.add_event_handler(update_zoomed_stack)\n", - "plot.show()" + "fig_stack_large.show()" ] }, { diff --git a/examples/notebooks/linear_selector.ipynb b/examples/notebooks/linear_selector.ipynb index 0f81bc36b..e9c8e664a 100644 --- a/examples/notebooks/linear_selector.ipynb +++ b/examples/notebooks/linear_selector.ipynb @@ -21,14 +21,14 @@ "import numpy as np\n", "from ipywidgets import VBox, IntSlider, FloatSlider\n", "\n", - "plot = fpl.Plot()\n", + "fig = fpl.Figure()\n", "\n", "# data to plot\n", "xs = np.linspace(0, 100, 1000)\n", "sine = np.sin(xs) * 20\n", "\n", "# make sine along x axis\n", - "sine_graphic = plot.add_line(np.column_stack([xs, sine]).astype(np.float32))\n", + "sine_graphic = fig[0, 0].add_line(np.column_stack([xs, sine]).astype(np.float32))\n", "\n", "# make some selectors\n", "selector = sine_graphic.add_linear_selector()\n", @@ -56,8 +56,8 @@ "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(add_widgets=[ipywidget_slider])" + "fig[0, 0].auto_scale()\n", + "fig.show(add_widgets=[ipywidget_slider])" ] }, { @@ -95,9 +95,9 @@ "source": [ "sines = [sine] * 10\n", "\n", - "plot = fpl.Plot()\n", + "fig = fpl.Figure()\n", "\n", - "sine_stack = plot.add_line_stack(sines)\n", + "sine_stack = fig[0, 0].add_line_stack(sines)\n", "\n", "colors = \"y\", \"blue\", \"red\", \"green\"\n", "\n", @@ -108,7 +108,7 @@ " \n", "ss = Synchronizer(*selectors)\n", "\n", - "plot.show()" + "fig.show()" ] }, { diff --git a/examples/notebooks/lineplot.ipynb b/examples/notebooks/lineplot.ipynb index 667cae178..85ebb60f5 100644 --- a/examples/notebooks/lineplot.ipynb +++ b/examples/notebooks/lineplot.ipynb @@ -7,7 +7,7 @@ "tags": [] }, "source": [ - "# A more complex example combing different graphics, gridplot and multiple perspectives" + "# A more complex example combing different graphics, subplots and multiple perspectives" ] }, { @@ -18,7 +18,7 @@ "outputs": [], "source": [ "import numpy as np\n", - "from fastplotlib import GridPlot" + "import fastplotlib as fpl" ] }, { @@ -46,11 +46,11 @@ "metadata": {}, "outputs": [], "source": [ - "# grid with 2 rows and 2 columns\n", + "# figure with 2 rows and 2 columns\n", "shape = (2, 2)\n", "\n", - "# pan-zoom controllers for each view\n", - "# views are synced if they have the \n", + "# pan-zoom controllers for each subplot\n", + "# subplots are synced if they have the\n", "# same controller ID\n", "# in this example the first view has its own controller\n", "# and the last 3 views are synced\n", @@ -59,14 +59,14 @@ " [1, 1]\n", "]\n", "\n", - "# create the grid plot\n", - "grid_plot = GridPlot(\n", + "# create the figure\n", + "fig = fpl.Figure(\n", " shape=shape,\n", - " cameras='3d', # 3D view for all subplots within the grid\n", + " cameras='3d', # 3D view for all subplots within the figure\n", " controller_ids=controller_ids\n", ")\n", "\n", - "for i, subplot in enumerate(grid_plot):\n", + "for i, subplot in enumerate(fig):\n", " # create and add the LineGraphic\n", " line_graphic = subplot.add_line(data=spiral, thickness=3, cmap='jet')\n", " \n", @@ -87,13 +87,13 @@ " if marker_index == spiral.shape[0]:\n", " marker_index = 0\n", " \n", - " for subplot in grid_plot:\n", + " for subplot in fig:\n", " subplot[\"marker\"].data = spiral[marker_index]\n", " \n", "# add `move_marker` to the animations\n", - "grid_plot.add_animations(move_marker)\n", + "fig.add_animations(move_marker)\n", "\n", - "grid_plot.show()" + "fig.show()" ] }, { diff --git a/examples/notebooks/lines_cmap.ipynb b/examples/notebooks/lines_cmap.ipynb index c6dc604b4..dbcbb3e16 100644 --- a/examples/notebooks/lines_cmap.ipynb +++ b/examples/notebooks/lines_cmap.ipynb @@ -55,11 +55,11 @@ }, "outputs": [], "source": [ - "plot = fpl.Plot()\n", + "fig = fpl.Figure()\n", "\n", - "plot.add_line(sine, thickness=10)\n", + "fig[0, 0].add_line(sine, thickness=10)\n", "\n", - "plot.show()" + "fig.show()" ] }, { @@ -71,7 +71,7 @@ }, "outputs": [], "source": [ - "plot_test(\"lines-cmap-white\", plot)" + "plot_test(\"lines-cmap-white\", fig)" ] }, { @@ -91,7 +91,7 @@ }, "outputs": [], "source": [ - "plot.graphics[0].cmap = \"jet\"" + "fig[0, 0].graphics[0].cmap = \"jet\"" ] }, { @@ -104,7 +104,7 @@ "outputs": [], "source": [ "# for testing, ignore\n", - "plot_test(\"lines-cmap-jet\", plot)" + "plot_test(\"lines-cmap-jet\", fig)" ] }, { @@ -116,7 +116,7 @@ }, "outputs": [], "source": [ - "plot.graphics[0].cmap.values = sine[:, 1]" + "fig[0, 0].graphics[0].cmap.values = sine[:, 1]" ] }, { @@ -129,7 +129,7 @@ "outputs": [], "source": [ "# for testing, ignore\n", - "plot_test(\"lines-cmap-jet-values\", plot)" + "plot_test(\"lines-cmap-jet-values\", fig)" ] }, { @@ -141,7 +141,7 @@ }, "outputs": [], "source": [ - "plot.graphics[0].cmap.values = cosine[:, 1]" + "fig[0, 0].graphics[0].cmap.values = cosine[:, 1]" ] }, { @@ -154,7 +154,7 @@ "outputs": [], "source": [ "# for testing, ignore\n", - "plot_test(\"lines-cmap-jet-values-cosine\", plot)" + "plot_test(\"lines-cmap-jet-values-cosine\", fig)" ] }, { @@ -166,7 +166,7 @@ }, "outputs": [], "source": [ - "plot.graphics[0].cmap = \"viridis\"" + "fig[0, 0].graphics[0].cmap = \"viridis\"" ] }, { @@ -179,7 +179,7 @@ "outputs": [], "source": [ "# for testing, ignore\n", - "plot_test(\"lines-cmap-viridis\", plot)" + "plot_test(\"lines-cmap-viridis\", fig)" ] }, { @@ -203,7 +203,7 @@ }, "outputs": [], "source": [ - "plot.graphics[0].cmap.values = cmap_values" + "fig[0, 0].graphics[0].cmap.values = cmap_values" ] }, { @@ -216,7 +216,7 @@ "outputs": [], "source": [ "# for testing, ignore\n", - "plot_test(\"lines-cmap-viridis-values\", plot)" + "plot_test(\"lines-cmap-viridis-values\", fig)" ] }, { @@ -228,7 +228,7 @@ }, "outputs": [], "source": [ - "plot.graphics[0].cmap = \"tab10\"" + "fig[0, 0].graphics[0].cmap = \"tab10\"" ] }, { @@ -239,7 +239,7 @@ "outputs": [], "source": [ "# for testing, ignore\n", - "plot_test(\"lines-cmap-tab-10\", plot)" + "plot_test(\"lines-cmap-tab-10\", fig)" ] }, { diff --git a/examples/notebooks/multiprocessing_zmq/multiprocessing_zmq_plot.ipynb b/examples/notebooks/multiprocessing_zmq/multiprocessing_zmq_plot.ipynb index 2c6e93d8f..564512451 100644 --- a/examples/notebooks/multiprocessing_zmq/multiprocessing_zmq_plot.ipynb +++ b/examples/notebooks/multiprocessing_zmq/multiprocessing_zmq_plot.ipynb @@ -60,13 +60,13 @@ "metadata": {}, "outputs": [], "source": [ - "plot = fpl.Plot()\n", + "fig = fpl.Figure()\n", "\n", "# initialize some data, must be of same dtype and shape as data sent by publisher\n", "data = np.random.rand(512, 512).astype(np.float32)\n", - "plot.add_image(data, name=\"image\")\n", + "fig[0, 0].add_image(data, name=\"image\")\n", "\n", - "def update_frame(p):\n", + "def update_frame(subplot):\n", " # recieve bytes\n", " b = get_bytes()\n", " \n", @@ -75,10 +75,10 @@ " a = np.frombuffer(b, dtype=np.float32).reshape(512, 512)\n", " \n", " # set graphic data\n", - " p[\"image\"].data = a\n", + " subplot[\"image\"].data = a\n", "\n", - "plot.add_animations(update_frame)\n", - "plot.show()" + "fig[0, 0].add_animations(update_frame)\n", + "fig.show()" ] }, { diff --git a/examples/notebooks/nb_test_utils.py b/examples/notebooks/nb_test_utils.py index 90b7158ad..cb84a5271 100644 --- a/examples/notebooks/nb_test_utils.py +++ b/examples/notebooks/nb_test_utils.py @@ -5,7 +5,7 @@ import imageio.v3 as iio import numpy as np -from fastplotlib import Plot, GridPlot +import fastplotlib as fpl # make dirs for screenshots and diffs current_dir = Path(__file__).parent @@ -93,11 +93,11 @@ def _run_tests(): return False -def plot_test(name, plot: Union[Plot, GridPlot]): +def plot_test(name, fig: fpl.Figure): if not _run_tests(): return - snapshot = plot.canvas.snapshot() + snapshot = fig.canvas.snapshot() rgb_img = rgba_to_rgb(snapshot.data) if "REGENERATE_SCREENSHOTS" in os.environ.keys(): diff --git a/examples/notebooks/scatter.ipynb b/examples/notebooks/scatter.ipynb index 9d7ff099f..b78521064 100644 --- a/examples/notebooks/scatter.ipynb +++ b/examples/notebooks/scatter.ipynb @@ -20,7 +20,7 @@ "outputs": [], "source": [ "import numpy as np\n", - "from fastplotlib import GridPlot" + "import fastplotlib as fpl" ] }, { @@ -63,7 +63,7 @@ }, "outputs": [], "source": [ - "# grid with 2 rows and 2 columns\n", + "# figure with 2 rows and 2 columns\n", "shape = (2, 2)\n", "\n", "# define the camera\n", @@ -73,8 +73,8 @@ " ['3d', '2d']\n", "]\n", "\n", - "# pan-zoom controllers for each view\n", - "# views are synced if they have the \n", + "# pan-zoom controllers for each subplot\n", + "# subplots are synced if they have the\n", "# same controller ID\n", "# you can only sync controllers that use the same camera type\n", "# i.e. you cannot sync between 2d and 3d subplots\n", @@ -83,21 +83,21 @@ " [1, 0]\n", "]\n", "\n", - "# create the grid plot\n", - "grid_plot = GridPlot(\n", + "# create the figure\n", + "fig = fpl.Figure(\n", " shape=shape,\n", " cameras=cameras,\n", " controller_ids=controller_ids\n", ")\n", "\n", - "for subplot in grid_plot:\n", + "for subplot in fig:\n", " subplot.add_scatter(data=cloud, colors=colors, alpha=0.7, sizes=5)\n", " \n", " subplot.set_axes_visibility(True)\n", " subplot.set_grid_visibility(True)\n", "\n", "\n", - "grid_plot.show()" + "fig.show()" ] }, { @@ -109,7 +109,7 @@ }, "outputs": [], "source": [ - "grid_plot[0, 1].graphics[0].colors[n_points:int(n_points * 1.5)] = \"r\"" + "fig[0, 1].graphics[0].colors[n_points:int(n_points * 1.5)] = \"r\"" ] }, { @@ -121,7 +121,7 @@ }, "outputs": [], "source": [ - "grid_plot[0, 1].graphics[0].colors[:n_points:10] = \"blue\"" + "fig[0, 1].graphics[0].colors[:n_points:10] = \"blue\"" ] }, { @@ -133,7 +133,7 @@ }, "outputs": [], "source": [ - "grid_plot[0, 1].graphics[0].colors[n_points:] = \"green\"" + "fig[0, 1].graphics[0].colors[n_points:] = \"green\"" ] }, { @@ -145,7 +145,7 @@ }, "outputs": [], "source": [ - "grid_plot[0, 1].graphics[0].colors[n_points:, -1] = 0" + "fig[0, 1].graphics[0].colors[n_points:, -1] = 0" ] }, { @@ -157,7 +157,7 @@ }, "outputs": [], "source": [ - "grid_plot[0, 1].graphics[0].data[:n_points] = grid_plot[0, 1].graphics[0].data[n_points * 2:]" + "fig[0, 1].graphics[0].data[:n_points] = fig[0, 1].graphics[0].data[n_points * 2:]" ] }, { diff --git a/examples/notebooks/scatter_sizes_animation.ipynb b/examples/notebooks/scatter_sizes_animation.ipynb index 06a6b11a2..9ca067bee 100644 --- a/examples/notebooks/scatter_sizes_animation.ipynb +++ b/examples/notebooks/scatter_sizes_animation.ipynb @@ -2,67 +2,37 @@ "cells": [ { "cell_type": "code", - "execution_count": 1, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "5d9f9913391a42af95d4d43d07c17b19", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "RFBOutputContext()" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "9cd08c319b814934a09fd266a1b6322b", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "JupyterOutputContext(children=(JupyterWgpuCanvas(), IpywidgetToolBar(children=(Button(icon='expand-arrows-alt'…" - ] - }, - "execution_count": 1, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "from time import time\n", "\n", "import numpy as np\n", "import fastplotlib as fpl\n", "\n", - "plot = fpl.Plot()\n", + "fig = fpl.Figure()\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", + "def update_positions(subplot):\n", " current_time = time()\n", " newPositions = points + np.sin(((current_time / 4) % 1)*np.pi)\n", - " plot.graphics[0].data = newPositions\n", + " subplot.graphics[0].data = newPositions\n", "\n", - "def update_sizes():\n", + "def update_sizes(subplot):\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", + " subplot.graphics[0].sizes = min_sizes + size_delta\n", "\n", - "scatter = plot.add_scatter(points, colors=[\"red\", \"green\", \"blue\"], sizes=12)\n", - "plot.add_animations(update_positions, update_sizes)\n", + "scatter = fig[0, 0].add_scatter(points, colors=[\"red\", \"green\", \"blue\"], sizes=12)\n", + "fig[0, 0].add_animations(update_positions, update_sizes)\n", "\n", - "plot.camera.width = 12\n", - "plot.show(autoscale=False)" + "fig[0, 0].camera.width = 12\n", + "fig.show(autoscale=False)" ] }, { diff --git a/examples/notebooks/scatter_sizes_grid.ipynb b/examples/notebooks/scatter_sizes_grid.ipynb index e152056c9..21985f189 100644 --- a/examples/notebooks/scatter_sizes_grid.ipynb +++ b/examples/notebooks/scatter_sizes_grid.ipynb @@ -16,18 +16,18 @@ "import numpy as np\n", "import fastplotlib as fpl\n", "\n", - "# grid with 2 rows and 3 columns\n", - "grid_shape = (2,1)\n", + "# figure with 2 rows and 3 columns\n", + "shape = (2, 1)\n", "\n", - "# you can give string names for each subplot within the gridplot\n", + "# you can give string names for each subplot\n", "names = [\n", " [\"scalar_size\"],\n", " [\"array_size\"]\n", "]\n", "\n", - "# Create the grid plot\n", - "plot = fpl.GridPlot(\n", - " shape=grid_shape,\n", + "# Create the figure\n", + "fig = fpl.Figure(\n", + " shape=shape,\n", " names=names,\n", " size=(1000, 1000)\n", ")\n", @@ -39,15 +39,15 @@ "\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", + "fig[\"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", + "fig[\"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", + "for subplot in fig:\n", + " subplot.auto_scale(maintain_aspect=True)\n", "\n", - "plot.show()" + "fig.show()" ] }, { diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-mixed-rgb-cockatoo-frame-50.png b/examples/notebooks/screenshots/nb-image-widget-zfish-mixed-rgb-cockatoo-frame-50.png new file mode 100644 index 000000000..5e0750ac8 --- /dev/null +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-mixed-rgb-cockatoo-frame-50.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7f8f74a0a5fa24e10a88d3723836306913243fa5fc23f46f44bbdae4c0209075 +size 58878 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-mixed-rgb-cockatoo-set-data.png b/examples/notebooks/screenshots/nb-image-widget-zfish-mixed-rgb-cockatoo-set-data.png new file mode 100644 index 000000000..8df83fe33 --- /dev/null +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-mixed-rgb-cockatoo-set-data.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0809b2dda0e773b7f100386f97144c40d36d51cd935c86ef1dcd4a938fce3981 +size 56319 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-mixed-rgb-cockatoo-windowrgb.png b/examples/notebooks/screenshots/nb-image-widget-zfish-mixed-rgb-cockatoo-windowrgb.png new file mode 100644 index 000000000..5bbefc7ae --- /dev/null +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-mixed-rgb-cockatoo-windowrgb.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a2e2e2cf7ac6be1a4fccec54494c3fd48af673765653675438fa2469c549e90c +size 55055 diff --git a/examples/notebooks/simple.ipynb b/examples/notebooks/simple.ipynb index f5901080b..3b42385c8 100644 --- a/examples/notebooks/simple.ipynb +++ b/examples/notebooks/simple.ipynb @@ -53,7 +53,7 @@ }, "outputs": [], "source": [ - "from fastplotlib import Plot\n", + "import fastplotlib as fpl\n", "from ipywidgets import VBox, HBox, IntSlider\n", "import numpy as np" ] @@ -90,17 +90,18 @@ }, "outputs": [], "source": [ - "# create a `Plot` instance\n", - "plot = Plot()\n", + "# create a `Figure` instance\n", + "# by default the figure will have 1 subplot\n", + "fig = fpl.Figure()\n", "\n", "# get a grayscale image\n", "data = iio.imread(\"imageio:camera.png\")\n", "\n", "# plot the image data\n", - "image_graphic = plot.add_image(data=data, name=\"sample-image\")\n", + "image_graphic = fig[0, 0].add_image(data=data, name=\"sample-image\")\n", "\n", "# show the plot\n", - "plot.show(sidecar=True)" + "fig.show(sidecar=True)" ] }, { @@ -221,7 +222,7 @@ "outputs": [], "source": [ "# testing cell, ignore\n", - "plot_test(\"camera\", plot)" + "plot_test(\"camera\", fig)" ] }, { @@ -312,7 +313,7 @@ "outputs": [], "source": [ "# testing cell, ignore\n", - "plot_test(\"astronaut\", plot)" + "plot_test(\"astronaut\", fig)" ] }, { @@ -324,7 +325,7 @@ }, "outputs": [], "source": [ - "plot.canvas.get_logical_size()" + "fig.canvas.get_logical_size()" ] }, { @@ -332,7 +333,7 @@ "id": "b53bc11a-ddf1-4786-8dca-8f3d2eaf993d", "metadata": {}, "source": [ - "### Indexing plots" + "### Indexing subplots" ] }, { @@ -340,7 +341,7 @@ "id": "67b92ffd-40cc-43fe-9df9-0e0d94763d8e", "metadata": {}, "source": [ - "**Plots are indexable and give you their graphics by name**" + "**Subplots are indexable and give you their graphics by name**" ] }, { @@ -352,27 +353,37 @@ }, "outputs": [], "source": [ - "plot" + "fig[0, 0]" ] }, { - "cell_type": "code", - "execution_count": null, - "id": "5b18f4e3-e13b-46d5-af1f-285c5a7fdc12", + "cell_type": "markdown", + "id": "e6eccef1", "metadata": { - "tags": [] + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } }, - "outputs": [], "source": [ - "plot[\"sample-image\"]" + "Access graphics in a subplot" ] }, { - "cell_type": "markdown", - "id": "a64314bf-a737-4858-803b-ea2adbd3578c", - "metadata": {}, + "cell_type": "code", + "execution_count": null, + "id": "7d8eaaf3", + "metadata": { + "collapsed": false, + "is_executing": true, + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], "source": [ - "**You can also use numerical indexing on `plot.graphics`**" + "# by name\n", + "fig[0, 0][\"sample-image\"]" ] }, { @@ -384,7 +395,8 @@ }, "outputs": [], "source": [ - "plot.graphics" + "# or through the .graphics property of a subplot\n", + "fig[0, 0].graphics" ] }, { @@ -396,7 +408,8 @@ }, "outputs": [], "source": [ - "plot.graphics[0]" + "# these are the same!\n", + "fig[0, 0].graphics[0] is fig[0, 0][\"sample-image\"]" ] }, { @@ -428,7 +441,7 @@ }, "outputs": [], "source": [ - "image_graphic == plot[\"sample-image\"]" + "image_graphic == fig[0, 0][\"sample-image\"]" ] }, { @@ -438,8 +451,8 @@ "metadata": {}, "outputs": [], "source": [ - "# close the plot\n", - "plot.close()" + "# close the figure\n", + "fig.close()" ] }, { @@ -461,12 +474,12 @@ }, "outputs": [], "source": [ - "plot_rgb = Plot()\n", + "fig_rgb = fpl.Figure()\n", "\n", - "plot_rgb.add_image(new_data, name=\"rgb-image\")\n", + "fig_rgb[0, 0].add_image(new_data, name=\"rgb-image\")\n", "\n", - "# show the plot\n", - "plot_rgb.show()" + "# show the figure\n", + "fig_rgb.show()" ] }, { @@ -486,7 +499,7 @@ }, "outputs": [], "source": [ - "plot_rgb[\"rgb-image\"].cmap.vmin = 100" + "fig_rgb[0, 0][\"rgb-image\"].cmap.vmin = 100" ] }, { @@ -499,7 +512,7 @@ "outputs": [], "source": [ "# testing cell, ignore\n", - "plot_test(\"astronaut_RGB\", plot_rgb)" + "plot_test(\"astronaut_RGB\", fig_rgb)" ] }, { @@ -509,8 +522,8 @@ "metadata": {}, "outputs": [], "source": [ - "# close plot\n", - "plot_rgb.close()" + "# close figure\n", + "fig_rgb.close()" ] }, { @@ -534,28 +547,35 @@ }, "outputs": [], "source": [ - "# create another `Plot` instance\n", - "plot_v = Plot()\n", + "# create a figure\n", + "fig_v = fpl.Figure()\n", "\n", - "plot.canvas.max_buffered_frames = 1\n", + "fig.canvas.max_buffered_frames = 1\n", "\n", "# make some random data again\n", "data = np.random.rand(512, 512)\n", "\n", "# plot the data\n", - "plot_v.add_image(data=data, name=\"random-image\")\n", + "fig_v[0, 0].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 argument\n", - "def update_data(plot_instance):\n", + "# a figure-level animation function will optionally take the figure as an argument\n", + "def update_data(figure_instance):\n", " new_data = np.random.rand(512, 512)\n", - " plot_instance[\"random-image\"].data = new_data\n", + " figure_instance[0, 0][\"random-image\"].data = new_data\n", + "\n", + "# you can also add animation functions to individual subplots\n", + "def update_data_subplot(subplot_instance):\n", + " pass\n", "\n", - "#add this as an animation function\n", - "plot_v.add_animations(update_data)\n", + "# add this as an animation function\n", + "fig_v.add_animations(update_data)\n", + "\n", + "# similarly you can add animation function to a subplot\n", + "# fig_v[0, 0].add_animations(update_data_subplot)\n", "\n", "# show the plot\n", - "plot_v.show()" + "fig_v.show()" ] }, { @@ -565,7 +585,7 @@ "source": [ "### We can share controllers across plots\n", "\n", - "This example creates a new plot, but it synchronizes the pan-zoom controller" + "This example creates a new figure, but it share the pan-zoom controllers from the previous figure!" ] }, { @@ -575,21 +595,21 @@ "metadata": {}, "outputs": [], "source": [ - "plot_sync = Plot(controller=plot_v.controller)\n", + "fig_sync = fpl.Figure(controllers=fig_v.controllers)\n", "\n", "data = np.random.rand(512, 512)\n", "\n", - "image_graphic_instance = plot_sync.add_image(data=data, cmap=\"viridis\")\n", + "image_graphic_instance = fig_sync[0, 0].add_image(data=data, cmap=\"viridis\")\n", "\n", "# you will need to define a new animation function for this graphic\n", "def update_data_2():\n", " new_data = np.random.rand(512, 512)\n", - " # alternatively, you can use the stored reference to the graphic as well instead of indexing the Plot\n", + " # alternatively, you can use the stored reference to the graphic as well instead of indexing the subplot\n", " image_graphic_instance.data = new_data\n", "\n", - "plot_sync.add_animations(update_data_2)\n", + "fig_sync.add_animations(update_data_2)\n", "\n", - "plot_sync.show()" + "fig_sync.show()" ] }, { @@ -605,7 +625,7 @@ "id": "d11fabb7-7c76-4e94-893d-80ed9ee3be3d", "metadata": {}, "source": [ - "### You can also use `ipywidgets.VBox` and `HBox` to stack plots. See the `gridplot` notebooks for a proper gridplot interface for more automated subplotting" + "### You can also use `ipywidgets.VBox` and `HBox` to stack plots. See the `subplot` notebooks for more automated subplotting" ] }, { @@ -615,7 +635,7 @@ "metadata": {}, "outputs": [], "source": [ - "HBox([plot_v.show(), plot_sync.show()])" + "HBox([fig_v.show(), fig_sync.show()])" ] }, { @@ -625,9 +645,9 @@ "metadata": {}, "outputs": [], "source": [ - "# close plot\n", - "plot_v.close()\n", - "plot_sync.close()" + "# close figures\n", + "fig_v.close()\n", + "fig_sync.close()" ] }, { @@ -690,21 +710,24 @@ "metadata": {}, "outputs": [], "source": [ - "# Create a plot instance\n", - "plot_l = Plot()\n", + "# Create a figure\n", + "fig_lines = fpl.Figure()\n", + "\n", + "# we will add all the lines to the same subplot\n", + "subplot = fig_lines[0, 0]\n", "\n", "# plot sine wave, use a single color\n", - "sine_graphic = plot_l.add_line(data=sine, thickness=5, colors=\"magenta\")\n", + "sine_graphic = subplot.add_line(data=sine, thickness=5, colors=\"magenta\")\n", "\n", "# you can also use colormaps for lines!\n", - "cosine_graphic = plot_l.add_line(data=cosine, thickness=12, cmap=\"autumn\")\n", + "cosine_graphic = subplot.add_line(data=cosine, thickness=12, cmap=\"autumn\")\n", "\n", "# or a list of colors for each datapoint\n", "colors = [\"r\"] * 25 + [\"purple\"] * 25 + [\"y\"] * 25 + [\"b\"] * 25\n", - "sinc_graphic = plot_l.add_line(data=sinc, thickness=5, colors = colors)\n", + "sinc_graphic = subplot.add_line(data=sinc, thickness=5, colors = colors)\n", "\n", "# show the plot\n", - "plot_l.show(sidecar=True, sidecar_kwargs={\"title\": \"lines\"})" + "fig_lines.show(sidecar=True, sidecar_kwargs={\"title\": \"lines\"})" ] }, { @@ -717,7 +740,7 @@ "outputs": [], "source": [ "# testing cell, ignore\n", - "plot_test(\"lines\", plot_l)" + "plot_test(\"lines\", fig_lines)" ] }, { @@ -729,7 +752,7 @@ "\n", "Set `maintain_aspect = False` on a camera, and then use the right mouse button and move the mouse to stretch and squeeze the view!\n", "\n", - "You can also click the **`1:1`** button to toggle this, or use `plot.camera.maintain_aspect`" + "You can also click the **`1:1`** button to toggle this, or use `subplot.camera.maintain_aspect`" ] }, { @@ -749,7 +772,7 @@ }, "outputs": [], "source": [ - "plot_l.auto_scale(maintain_aspect=True)" + "subplot.auto_scale(maintain_aspect=True)" ] }, { @@ -824,7 +847,7 @@ "outputs": [], "source": [ "# testing cell, ignore\n", - "plot_test(\"lines-colors\", plot_l)" + "plot_test(\"lines-colors\", fig_lines)" ] }, { @@ -832,7 +855,7 @@ "id": "c29f81f9-601b-49f4-b20c-575c56e58026", "metadata": {}, "source": [ - "## Graphic _data_ is also indexable" + "## Graphic _data_ is also slicable and settable" ] }, { @@ -866,7 +889,7 @@ "outputs": [], "source": [ "# testing cell, ignore\n", - "plot_test(\"lines-data\", plot_l)" + "plot_test(\"lines-data\", fig_lines)" ] }, { @@ -912,7 +935,7 @@ "metadata": {}, "outputs": [], "source": [ - "sinc_graphic.present.add_event_handler(plot_l.auto_scale)" + "sinc_graphic.present.add_event_handler(subplot.auto_scale)" ] }, { @@ -954,12 +977,12 @@ "source": [ "img = iio.imread(\"imageio:camera.png\")\n", "\n", - "plot_l.add_image(img[::20, ::20], name=\"image\", cmap=\"gray\")\n", + "subplot.add_image(img[::20, ::20], name=\"image\", cmap=\"gray\")\n", "\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" + "subplot[\"image\"].position_z = -1\n", + "subplot[\"image\"].position_x = -8\n", + "subplot[\"image\"].position_y = -8" ] }, { @@ -972,7 +995,7 @@ "outputs": [], "source": [ "# testing cell, ignore\n", - "plot_test(\"lines-underlay\", plot_l)" + "plot_test(\"lines-underlay\", fig_lines)" ] }, { @@ -983,7 +1006,7 @@ "outputs": [], "source": [ "# close plot\n", - "plot_l.close()" + "fig_lines.close()" ] }, { @@ -1001,8 +1024,8 @@ "metadata": {}, "outputs": [], "source": [ - "# just set the camera as \"3d\", the rest is basically the same :D \n", - "plot_l3d = Plot(camera='3d')\n", + "# just set the camera as \"3d\", the rest is basically the same :D\n", + "fig_l3d = fpl.Figure(cameras=\"3d\")\n", "\n", "# create a spiral\n", "phi = np.linspace(0, 30, 200)\n", @@ -1015,9 +1038,9 @@ "# note: you usually mix 3D and 2D graphics on the same plot\n", "spiral = np.dstack([xs, ys, zs])[0]\n", "\n", - "plot_l3d.add_line(data=spiral, thickness=2, cmap='winter')\n", + "fig_l3d[0, 0].add_line(data=spiral, thickness=2, cmap='winter')\n", "\n", - "plot_l3d.show()" + "fig_l3d.show()" ] }, { @@ -1037,7 +1060,7 @@ }, "outputs": [], "source": [ - "plot_l3d.auto_scale(maintain_aspect=True)" + "fig_l3d[0, 0].auto_scale(maintain_aspect=True)" ] }, { @@ -1050,7 +1073,7 @@ "outputs": [], "source": [ "# testing cell, ignore\n", - "plot_test(\"lines-3d\", plot_l3d)" + "plot_test(\"lines-3d\", fig_l3d)" ] }, { @@ -1061,7 +1084,23 @@ "outputs": [], "source": [ "# change the FOV of the persepctive camera\n", - "plot_l3d.camera.fov = 70" + "fig_l3d[0, 0].camera.fov = 70" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9e126e6c", + "metadata": { + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "# change the controller, ex. from the current \"fly\" controller to a \"panzoom\" controller\n", + "fig_l3d[0, 0].controller = \"panzoom\"" ] }, { @@ -1074,7 +1113,7 @@ "outputs": [], "source": [ "# close plot\n", - "plot_l3d.close()" + "fig_l3d.close()" ] }, { @@ -1089,18 +1128,6 @@ "#### There might be a small delay for a few seconds before the plot shows, this is due to shaders being compiled and a few other things. The plot should be very fast and responsive once it is displayed and future modifications should also be fast!" ] }, - { - "cell_type": "code", - "execution_count": null, - "id": "2ecb2385-8fa4-4239-881c-b754c24aed9f", - "metadata": {}, - "outputs": [], - "source": [ - "from fastplotlib import Plot\n", - "from ipywidgets import VBox, HBox, IntSlider\n", - "import numpy as np" - ] - }, { "cell_type": "code", "execution_count": null, @@ -1135,12 +1162,12 @@ "colors = [\"yellow\"] * n_points + [\"cyan\"] * n_points + [\"magenta\"] * n_points\n", "\n", "# create plot\n", - "plot_s = Plot()\n", - "\n", + "fig_scatter = fpl.Figure()\n", + "subplot_scatter = fig_scatter[0, 0]\n", "# use an alpha value since this will be a lot of points\n", - "scatter_graphic = plot_s.add_scatter(data=cloud, sizes=3, colors=colors, alpha=0.7)\n", + "scatter_graphic = subplot_scatter.add_scatter(data=cloud, sizes=3, colors=colors, alpha=0.7)\n", "\n", - "plot_s.show()" + "fig_scatter.show()" ] }, { @@ -1221,8 +1248,8 @@ "metadata": {}, "outputs": [], "source": [ - "plot_s.camera = \"3d\"\n", - "plot_s.controller = \"fly\"" + "subplot_scatter.camera = \"3d\"\n", + "subplot_scatter.controller = \"fly\"" ] }, { @@ -1233,7 +1260,7 @@ "outputs": [], "source": [ "# close plot\n", - "plot_s.close()" + "fig_scatter.close()" ] }, { diff --git a/examples/notebooks/gridplot.ipynb b/examples/notebooks/subplots.ipynb similarity index 72% rename from examples/notebooks/gridplot.ipynb rename to examples/notebooks/subplots.ipynb index f1ceb2180..72b4b3007 100644 --- a/examples/notebooks/gridplot.ipynb +++ b/examples/notebooks/subplots.ipynb @@ -5,7 +5,7 @@ "id": "3cfc2d9f-6a09-42f4-a47c-3ba51f1a1801", "metadata": {}, "source": [ - "### More in-depth on `GridPlot`" + "### More in-depth on subplots with a Figure" ] }, { @@ -16,7 +16,7 @@ "outputs": [], "source": [ "import numpy as np\n", - "from fastplotlib import GridPlot" + "import fastplotlib as fpl" ] }, { @@ -27,10 +27,10 @@ "outputs": [], "source": [ "# grid with 2 rows and 3 columns\n", - "grid_shape = (2, 3)\n", + "shape = (2, 3)\n", "\n", - "# pan-zoom controllers for each view\n", - "# views are synced if they have the \n", + "# pan-zoom controllers for each subplot\n", + "# subplots are synced if they have the\n", "# same controller ID\n", "controller_ids = [\n", " [0, -3, 1], # id each controller with an integer\n", @@ -44,22 +44,22 @@ "]\n", "\n", "\n", - "# you can give string names for each subplot within the gridplot\n", + "# you can give string names for each subplot within the figure\n", "names = [\n", " [\"subplot0\", \"subplot1\", \"subplot2\"],\n", " [\"subplot3\", \"subplot4\", \"subplot5\"]\n", "]\n", "\n", - "# Create the grid plot\n", - "grid_plot = GridPlot(\n", - " shape=grid_shape,\n", + "# Create the figure\n", + "fig = fpl.Figure(\n", + " shape=shape,\n", " controller_ids=controller_ids,\n", " names=names,\n", ")\n", "\n", "\n", "# Make a random image graphic for each subplot\n", - "for subplot in grid_plot:\n", + "for subplot in fig:\n", " data = np.random.rand(512, 512)\n", " # create and add an ImageGraphic\n", " subplot.add_image(data=data, name=\"rand-image\")\n", @@ -73,8 +73,8 @@ " subplot[\"rand-image\"].data = new_data\n", "\n", "# add the animation\n", - "grid_plot.add_animations(set_random_frame)\n", - "grid_plot.show()" + "fig.add_animations(set_random_frame)\n", + "fig.show()" ] }, { @@ -82,18 +82,20 @@ "id": "2867bcd6-7691-4073-91d9-9c33e8fdb896", "metadata": {}, "source": [ - "### Indexing the gridplot to access subplots" + "### Accessing subplots" ] }, { "cell_type": "code", "execution_count": null, "id": "2a6f7eb5-776e-42a6-b6c2-c26009a26795", - "metadata": {}, + "metadata": { + "is_executing": true + }, "outputs": [], "source": [ - "# can access subplot by name\n", - "grid_plot[\"subplot0\"]" + "# by name\n", + "fig[\"subplot0\"]" ] }, { @@ -103,8 +105,8 @@ "metadata": {}, "outputs": [], "source": [ - "# can access subplot by index\n", - "grid_plot[0, 0]" + "# by index\n", + "fig[0, 0]" ] }, { @@ -112,7 +114,7 @@ "id": "3272b8b3-3063-47a4-94c8-15ceeeaecc69", "metadata": {}, "source": [ - "## subplots also support indexing!\n", + "## getting graphics within subplots!\n", "this can be used to get graphics if they are named" ] }, @@ -124,7 +126,7 @@ "outputs": [], "source": [ "# can access graphic directly via name\n", - "grid_plot[\"subplot0\"][\"rand-image\"]" + "fig[\"subplot0\"][\"rand-image\"]" ] }, { @@ -134,17 +136,39 @@ "metadata": {}, "outputs": [], "source": [ - "grid_plot[\"subplot0\"][\"rand-image\"].cmap.vmin = 0.6\n", - "grid_plot[\"subplot0\"][\"rand-image\"].cmap.vmax = 0.8" + "fig[\"subplot0\"][\"rand-image\"].cmap.vmin = 0.6\n", + "fig[\"subplot0\"][\"rand-image\"].cmap.vmax = 0.8" ] }, { "cell_type": "markdown", - "id": "516a46e1-cc53-4137-b49b-d5fb94e212d7", - "metadata": {}, "source": [ - "### positional indexing also works event if subplots have string names" - ] + "If they are not named use .graphics" + ], + "metadata": { + "collapsed": false + } + }, + { + "cell_type": "code", + "execution_count": null, + "outputs": [], + "source": [ + "fig[\"subplot0\"].graphics" + ], + "metadata": { + "collapsed": false, + "is_executing": true + } + }, + { + "cell_type": "markdown", + "source": [ + "### positional indexing also works" + ], + "metadata": { + "collapsed": false + } }, { "cell_type": "code", @@ -153,8 +177,8 @@ "metadata": {}, "outputs": [], "source": [ - "grid_plot[1, 0][\"rand-image\"].cmap.vim = 0.1\n", - "grid_plot[1, 0][\"rand-image\"].cmap.vmax = 0.3" + "fig[1, 0][\"rand-image\"].cmap.vim = 0.1\n", + "fig[1, 0][\"rand-image\"].cmap.vmax = 0.3" ] }, { diff --git a/examples/notebooks/gridplot_simple.ipynb b/examples/notebooks/subplots_simple.ipynb similarity index 69% rename from examples/notebooks/gridplot_simple.ipynb rename to examples/notebooks/subplots_simple.ipynb index 74807f55a..e519584d3 100644 --- a/examples/notebooks/gridplot_simple.ipynb +++ b/examples/notebooks/subplots_simple.ipynb @@ -5,12 +5,12 @@ "id": "0e42f03b-9cdf-484f-b158-78b07fdf524d", "metadata": {}, "source": [ - "## This notebook shows how you can use more of the `fastplotlib` API to create `Graphic` objects and add them to a `GridPlot`" + "## This notebook shows how you can use more of the `fastplotlib` API to create `Graphic` objects and add them to subplots" ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 1, "id": "5171a06e-1bdc-4908-9726-3c1fd45dbb9d", "metadata": { "ExecuteTime": { @@ -19,10 +19,43 @@ }, "tags": [] }, - "outputs": [], + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "301d76bd4c5c42c7912cdd28651e2899", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "Image(value=b'\\x89PNG\\r\\n\\x1a\\n\\x00\\x00\\x00\\rIHDR\\x00\\x00\\x01,\\x00\\x00\\x007\\x08\\x06\\x00\\x00\\x00\\xb6\\x1bw\\x99\\x…" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Unable to find extension: VK_EXT_swapchain_colorspace\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Available devices:\n", + "✅ (default) | AMD RADV POLARIS10 (ACO) | DiscreteGPU | Vulkan | Mesa 20.3.5 (ACO)\n", + "❗ | llvmpipe (LLVM 11.0.1, 256 bits) | CPU | Vulkan | Mesa 20.3.5 (LLVM 11.0.1)\n", + "✅ | NVIDIA GeForce RTX 3080 | DiscreteGPU | Vulkan | 530.30.02\n", + "❗ | Radeon RX 570 Series (POLARIS10, DRM 3.40.0, 5.10.0-21-amd64, LLVM 11.0.1) | Unknown | OpenGL | \n" + ] + } + ], "source": [ "import numpy as np\n", - "from fastplotlib import GridPlot" + "import fastplotlib as fpl" ] }, { @@ -38,29 +71,29 @@ }, "outputs": [], "source": [ - "# GridPlot of shape 2 x 3 with all controllers synced\n", - "grid_plot = GridPlot(shape=(2, 3), controller_ids=\"sync\")\n", + "# Figure of shape 2 x 3 with all controllers synced\n", + "fig = fpl.Figure(shape=(2, 3), controller_ids=\"sync\")\n", "\n", "# Make a random image graphic for each subplot\n", - "for subplot in grid_plot:\n", + "for subplot in fig:\n", " # create image data\n", " data = np.random.rand(512, 512)\n", " # add an image to the subplot\n", " subplot.add_image(data, name=\"rand-img\")\n", "\n", "# Define a function to update the image graphics with new data\n", - "# add_animations will pass the gridplot to the animation function\n", - "def update_data(gp):\n", - " for sp in gp:\n", + "# add_animations will pass the figure to the animation function\n", + "def update_data(f):\n", + " for sp in f:\n", " new_data = np.random.rand(512, 512)\n", " # index the image graphic by name and set the data\n", " sp[\"rand-img\"].data = new_data\n", " \n", "# add the animation function\n", - "grid_plot.add_animations(update_data)\n", + "fig.add_animations(update_data)\n", "\n", - "# show the gridplot \n", - "grid_plot.show()" + "# show the figure\n", + "fig.show()" ] }, { @@ -68,7 +101,7 @@ "id": "e7801781-c3e9-490f-ab12-1cd2f480d3e9", "metadata": {}, "source": [ - "## Accessing subplots within `GridPlot`" + "## Accessing subplots within `Figure`" ] }, { @@ -78,7 +111,7 @@ "metadata": {}, "outputs": [], "source": [ - "grid_plot" + "fig" ] }, { @@ -92,7 +125,7 @@ "source": [ "# positional indexing\n", "# row 0 and col 0\n", - "grid_plot[0, 0]" + "fig[0, 0]" ] }, { @@ -100,7 +133,7 @@ "id": "276dfede-e9bc-4488-b9e6-3ca5cf91e4dc", "metadata": {}, "source": [ - "### You can get the graphics within a subplot, just like with simple `Plot`" + "### You can get the graphics within a subplot" ] }, { @@ -112,7 +145,7 @@ }, "outputs": [], "source": [ - "grid_plot[0, 1].graphics" + "fig[0, 1].graphics" ] }, { @@ -132,7 +165,7 @@ }, "outputs": [], "source": [ - "grid_plot[0, 1].graphics[0].cmap.vmax = 0.5" + "fig[0, 1].graphics[0].cmap.vmax = 0.5" ] }, { @@ -140,7 +173,7 @@ "id": "00506fa1-2dc0-4435-96a0-e50667d3174f", "metadata": {}, "source": [ - "### more indexing with `GridPlot`" + "### more indexing" ] }, { @@ -153,7 +186,7 @@ "outputs": [], "source": [ "# you can give subplots human-readable string names\n", - "grid_plot[0, 2].name = \"top-right-plot\"" + "fig[0, 2].name = \"top-right-plot\"" ] }, { @@ -165,7 +198,7 @@ }, "outputs": [], "source": [ - "grid_plot[\"top-right-plot\"]" + "fig[\"top-right-plot\"]" ] }, { @@ -178,7 +211,7 @@ "outputs": [], "source": [ "# view its position\n", - "grid_plot[\"top-right-plot\"].position" + "fig[\"top-right-plot\"].position" ] }, { @@ -191,7 +224,7 @@ "outputs": [], "source": [ "# these are really the same\n", - "grid_plot[\"top-right-plot\"] is grid_plot[0, 2]" + "fig[\"top-right-plot\"] is fig[0, 2]" ] }, { @@ -211,7 +244,7 @@ }, "outputs": [], "source": [ - "grid_plot[\"top-right-plot\"][\"rand-img\"].cmap.vmin = 0.5" + "fig[\"top-right-plot\"][\"rand-img\"].cmap.vmin = 0.5" ] }, { @@ -223,7 +256,7 @@ }, "outputs": [], "source": [ - "grid_plot.close()" + "fig.close()" ] }, { diff --git a/examples/notebooks/test_gc.ipynb b/examples/notebooks/test_gc.ipynb index 6caf6a9e3..39f964cf7 100644 --- a/examples/notebooks/test_gc.ipynb +++ b/examples/notebooks/test_gc.ipynb @@ -59,12 +59,12 @@ "metadata": {}, "outputs": [], "source": [ - "gp = fpl.GridPlot((2, 2))\n", + "fig = fpl.Figure((2, 2))\n", "\n", - "line = gp[0, 0].add_line(points_data, name=\"line\")\n", - "scatter = gp[0, 1].add_scatter(points_data.copy(), name=\"scatter\")\n", - "line_stack = gp[1, 0].add_line_stack(line_collection_data, name=\"line-stack\")\n", - "image = gp[1, 1].add_image(img_data, name=\"image\")\n", + "line = fig[0, 0].add_line(points_data, name=\"line\")\n", + "scatter = fig[0, 1].add_scatter(points_data.copy(), name=\"scatter\")\n", + "line_stack = fig[1, 0].add_line_stack(line_collection_data, name=\"line-stack\")\n", + "image = fig[1, 1].add_image(img_data, name=\"image\")\n", "\n", "linear_sel = line.add_linear_selector(name=\"line_linear_sel\")\n", "linear_region_sel = line.add_linear_region_selector(name=\"line_region_sel\")\n", @@ -88,7 +88,7 @@ "\n", "\n", "objects = list()\n", - "for subplot in gp:\n", + "for subplot in fig:\n", " objects += subplot.objects\n", "\n", "\n", @@ -100,7 +100,7 @@ " f = getattr(g, feature)\n", " f.add_event_handler(feature_changed_handler)\n", "\n", - "gp.show()" + "fig.show()" ] }, { @@ -110,7 +110,7 @@ "metadata": {}, "outputs": [], "source": [ - "gp.clear()" + "fig.clear()" ] }, { diff --git a/examples/tests/test_examples.py b/examples/tests/test_examples.py index a570b4f36..b8369e368 100644 --- a/examples/tests/test_examples.py +++ b/examples/tests/test_examples.py @@ -69,7 +69,7 @@ def test_example_screenshots(module, force_offscreen): example = importlib.import_module(module_name) # render a frame - img = np.asarray(example.plot.renderer.target.draw()) + img = np.asarray(example.fig.renderer.target.draw()) # check if _something_ was rendered assert img is not None and img.size > 0 diff --git a/fastplotlib/__init__.py b/fastplotlib/__init__.py index 33db8c79d..8e6341156 100644 --- a/fastplotlib/__init__.py +++ b/fastplotlib/__init__.py @@ -1,12 +1,13 @@ from pathlib import Path -from .layouts import Plot, GridPlot +from .utils.gui import run from .graphics import * from .graphics.selectors import * from .legends import * +from .layouts import Figure + from .widgets import ImageWidget from .utils import config -from .utils.gui import run import wgpu @@ -21,9 +22,8 @@ __all__ = [ - "Plot", - "GridPlot", + "Figure", "run", - "ImageWidget", + # "ImageWidget", "Legend", ] diff --git a/fastplotlib/layouts/__init__.py b/fastplotlib/layouts/__init__.py index aaed4c5a4..60111cabc 100644 --- a/fastplotlib/layouts/__init__.py +++ b/fastplotlib/layouts/__init__.py @@ -1,4 +1,3 @@ -from ._gridplot import GridPlot -from ._plot import Plot +from ._figure import Figure -__all__ = ["Plot", "GridPlot"] +__all__ = ["Figure"] diff --git a/fastplotlib/layouts/_gridplot.py b/fastplotlib/layouts/_figure.py similarity index 67% rename from fastplotlib/layouts/_gridplot.py rename to fastplotlib/layouts/_figure.py index 472d3dd2e..7e83e103a 100644 --- a/fastplotlib/layouts/_gridplot.py +++ b/fastplotlib/layouts/_figure.py @@ -1,4 +1,9 @@ +import os from itertools import product, chain +from multiprocessing import Queue +from pathlib import Path +from time import time + import numpy as np from typing import Literal, Iterable from inspect import getfullargspec @@ -8,17 +13,17 @@ from wgpu.gui import WgpuCanvasBase -from ._frame import Frame +from ._video_writer import VideoWriterAV from ._utils import make_canvas_and_renderer, create_controller, create_camera from ._utils import controller_types as valid_controller_types from ._subplot import Subplot -from ._record_mixin import RecordMixin +from .. import ImageGraphic -class GridPlot(Frame, RecordMixin): +class Figure: def __init__( self, - shape: tuple[int, int], + shape: tuple[int, int] = (1, 1), cameras: ( Literal["2d", "3d"] | Iterable[Iterable[Literal["2d", "3d"]]] @@ -46,7 +51,7 @@ def __init__( Parameters ---------- - shape: (int, int) + shape: (int, int), default (1, 1) (n_rows, n_cols) cameras: "2d", "3", list of "2d" | "3d", Iterable of camera instances, or Iterable of "2d" | "3d", optional @@ -76,7 +81,7 @@ def __init__( controllers: pygfx.Controller | list[pygfx.Controller] | np.ndarray[pygfx.Controller], optional directly provide pygfx.Controller instances(s). Useful if you want to use a controller from an existing - plot/subplot. Other controller kwargs, i.e. ``controller_Types`` and ``controller_ids`` are ignored if + plot/subplot. Other controller kwargs, i.e. ``controller_types`` and ``controller_ids`` are ignored if ``controllers`` are provided. canvas: WgpuCanvas, optional @@ -97,7 +102,7 @@ def __init__( if names is not None: if len(list(chain(*names))) != len(self): raise ValueError( - "must provide same number of subplot `names` as specified by gridplot shape" + "must provide same number of subplot `names` as specified by Figure `shape`" ) subplot_names = np.asarray(names).reshape(self.shape) @@ -141,7 +146,7 @@ def __init__( pass else: raise TypeError( - "controllers argument must be a single pygfx.Controller instance of a Iterable of " + "controllers argument must be a single pygfx.Controller instance, or a Iterable of " "pygfx.Controller instances" ) @@ -326,8 +331,22 @@ def __init__( self._starting_size = size - RecordMixin.__init__(self) - Frame.__init__(self) + self._output = None + + if self.canvas.__class__.__name__ == "JupyterWgpuCanvas": + self.recorder = FigureRecorder(self) + else: + self.recorder = None + + @property + def toolbar(self): + """ipywidget or QToolbar instance""" + return self._output.toolbar + + @property + def output(self): + """ipywidget or QWidget that contains this plot""" + return self._output @property def shape(self) -> tuple[int, int]: @@ -336,12 +355,12 @@ def shape(self) -> tuple[int, int]: @property def canvas(self) -> WgpuCanvasBase: - """The canvas associated to this GridPlot""" + """The canvas associated to this Figure""" return self._canvas @property def renderer(self) -> pygfx.WgpuRenderer: - """The renderer associated to this GridPlot""" + """The renderer associated to this Figure""" return self._renderer @property @@ -391,6 +410,114 @@ def render(self): # call post-render animate functions self._call_animate_functions(self._animate_funcs_post) + def start_render(self): + """start render cycle""" + self.canvas.request_draw(self.render) + self.canvas.set_logical_size(*self._starting_size) + + def show( + self, + autoscale: bool = True, + maintain_aspect: bool = None, + toolbar: bool = True, + sidecar: bool = False, + sidecar_kwargs: dict = None, + add_widgets: list = None, + ): + """ + Begins the rendering event loop and shows the plot in the desired output context (jupyter, qt or glfw). + + Parameters + ---------- + autoscale: bool, default ``True`` + autoscale the Scene + + maintain_aspect: bool, default ``True`` + maintain aspect ratio + + toolbar: bool, default ``True`` + show toolbar + + sidecar: bool, default ``True`` + display plot in a ``jupyterlab-sidecar``, only for jupyter output context + + sidecar_kwargs: dict, default ``None`` + kwargs for sidecar instance to display plot + i.e. title, layout + + add_widgets: list of widgets + a list of ipywidgets or QWidget that are vertically stacked below the plot + + Returns + ------- + OutputContext + In jupyter, it will display the plot in the output cell or sidecar + + In Qt, it will display the Plot, toolbar, etc. as stacked widget, use `Plot.widget` to access it. + """ + + # show was already called, return existing output context + if self._output is not None: + return self._output + + self.start_render() + + if sidecar_kwargs is None: + sidecar_kwargs = dict() + + if add_widgets is None: + add_widgets = list() + + # flip y-axis if ImageGraphics are present + for subplot in self: + for g in subplot.graphics: + if isinstance(g, ImageGraphic): + subplot.camera.local.scale_y *= -1 + break + + if autoscale: + for subplot in self: + if maintain_aspect is None: + _maintain_aspect = subplot.camera.maintain_aspect + else: + _maintain_aspect = maintain_aspect + subplot.auto_scale(maintain_aspect=_maintain_aspect, zoom=0.95) + + # used for generating images in docs using nbsphinx + if "NB_SNAPSHOT" in os.environ.keys(): + if os.environ["NB_SNAPSHOT"] == "1": + return self.canvas.snapshot() + + # return the appropriate OutputContext based on the current canvas + if self.canvas.__class__.__name__ == "JupyterWgpuCanvas": + from .output.jupyter_output import ( + JupyterOutputContext, + ) # noqa - inline import + + self._output = JupyterOutputContext( + frame=self, + make_toolbar=toolbar, + use_sidecar=sidecar, + sidecar_kwargs=sidecar_kwargs, + add_widgets=add_widgets, + ) + + elif self.canvas.__class__.__name__ == "QWgpuCanvas": + from .output.qt_output import QOutputContext # noqa - inline import + + self._output = QOutputContext( + frame=self, make_toolbar=toolbar, add_widgets=add_widgets + ) + + else: # assume GLFW, the output context is just the canvas + self._output = self.canvas + + # return the output context, this call is required for jupyter but not for Qt + return self._output + + def close(self): + self.output.close() + def _call_animate_functions(self, funcs: list[callable]): for fn in funcs: try: @@ -413,7 +540,7 @@ def add_animations( ): """ Add function(s) that are called on every render cycle. - These are called at the GridPlot level. + These are called at the Figure level. Parameters ---------- @@ -492,3 +619,157 @@ def __repr__(self): f"\t{newline.join(subplot.__str__() for subplot in self)}" f"\n" ) + + +class FigureRecorder: + def __init__(self, figure: Figure): + self._figure = figure + self._video_writer: VideoWriterAV = None + self._video_writer_queue = Queue() + self._record_fps = 25 + self._record_timer = 0 + self._record_start_time = 0 + + def _record(self): + """ + Sends frame to VideoWriter through video writer queue + """ + # current time + t = time() + + # put frame in queue only if enough time as passed according to the desired framerate + # otherwise it tries to record EVERY frame on every rendering cycle, which just blocks the rendering + if t - self._record_timer < (1 / self._record_fps): + return + + # reset timer + self._record_timer = t + + if self._video_writer is not None: + ss = self._figure.canvas.snapshot() + # exclude alpha channel + self._video_writer_queue.put(ss.data[..., :-1]) + + def start( + self, + path: str | Path, + fps: int = 25, + codec: str = "mpeg4", + pixel_format: str = "yuv420p", + options: dict = None, + ): + """ + Start a recording, experimental. Call ``record_end()`` to end a recording. + Note: playback duration does not exactly match recording duration. + + Requires PyAV: https://github.com/PyAV-Org/PyAV + + **Do not resize canvas during a recording, the width and height must remain constant!** + + Parameters + ---------- + path: str or Path + path to save the recording + + fps: int, default ``25`` + framerate, do not use > 25 within jupyter + + codec: str, default "mpeg4" + codec to use, see ``ffmpeg`` list: https://www.ffmpeg.org/ffmpeg-codecs.html . + In general, ``"mpeg4"`` should work on most systems. ``"libx264"`` is a + better option if you have it installed. + + pixel_format: str, default "yuv420p" + pixel format + + options: dict, optional + Codec options. For example, if using ``"mpeg4"`` you can use ``{"q:v": "20"}`` to set the quality between + 1-31, where "1" is highest and "31" is lowest. If using ``"libx264"``` you can use ``{"crf": "30"}`` where + the "crf" value is between "0" (highest quality) and "50" (lowest quality). See ``ffmpeg`` docs for more + info on codec options + + Examples + -------- + + With ``"mpeg4"`` + + .. code-block:: python + + # start recording video + figure.recorder.start("./video.mp4", options={"q:v": "20"} + + # do stuff like interacting with the plot, change things, etc. + + # end recording + figure.recorder.stop() + + With ``"libx264"`` + + .. code-block:: python + + # start recording video + figure.recorder.start("./vid_x264.mp4", codec="libx264", options={"crf": "25"}) + + # do stuff like interacting with the plot, change things, etc. + + # end recording + figure.recorder.stop() + + """ + + if Path(path).exists(): + raise FileExistsError(f"File already exists at given path: {path}") + + # queue for sending frames to VideoWriterAV process + self._video_writer_queue = Queue() + + # snapshot to get canvas width height + ss = self._figure.canvas.snapshot() + + # writer process + self._video_writer = VideoWriterAV( + path=str(path), + queue=self._video_writer_queue, + fps=int(fps), + width=ss.width, + height=ss.height, + codec=codec, + pixel_format=pixel_format, + options=options, + ) + + # start writer process + self._video_writer.start() + + # 1.3 seems to work well to reduce that difference between playback time and recording time + # will properly investigate later + self._record_fps = fps * 1.3 + self._record_start_time = time() + + # record timer used to maintain desired framerate + self._record_timer = time() + + self._figure.add_animations(self._record) + + def stop(self) -> float: + """ + End a current recording. Returns the real duration of the recording + + Returns + ------- + float + recording duration + """ + + # tell video writer that recording has finished + self._video_writer_queue.put(None) + + # wait for writer to finish + self._video_writer.join(timeout=5) + + self._video_writer = None + + # so self._record() is no longer called on every render cycle + self._figure.remove_animation(self._record) + + return time() - self._record_start_time diff --git a/fastplotlib/layouts/_frame/__init__.py b/fastplotlib/layouts/_frame/__init__.py deleted file mode 100644 index c34884022..000000000 --- a/fastplotlib/layouts/_frame/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from ._frame import Frame diff --git a/fastplotlib/layouts/_frame/_frame.py b/fastplotlib/layouts/_frame/_frame.py deleted file mode 100644 index 219a59082..000000000 --- a/fastplotlib/layouts/_frame/_frame.py +++ /dev/null @@ -1,152 +0,0 @@ -import os - -from ...graphics import ImageGraphic -from ._toolbar import ToolBar - - -class Frame: - """ - Mixin class for Plot and GridPlot that "frames" the plot. - - Gives them their `show()` call that returns the appropriate output context. - """ - - def __init__(self): - self._output = None - - @property - def toolbar(self) -> ToolBar: - """ipywidget or QToolbar instance""" - return self._output.toolbar - - @property - def widget(self): - """ipywidget or QWidget that contains this plot""" - # @caitlin: this is the same as the output context, but I figure widget is a simpler public name - return self._output - - def render(self): - """render call implemented in subclass""" - raise NotImplemented - - def _autoscale_init(self, maintain_aspect: bool): - """autoscale function that is called only during show()""" - if hasattr(self, "_subplots"): - for subplot in self: - if maintain_aspect is None: - _maintain_aspect = subplot.camera.maintain_aspect - else: - _maintain_aspect = maintain_aspect - subplot.auto_scale(maintain_aspect=_maintain_aspect, zoom=0.95) - else: - if maintain_aspect is None: - maintain_aspect = self.camera.maintain_aspect - self.auto_scale(maintain_aspect=maintain_aspect, zoom=0.95) - - def start_render(self): - """start render cycle""" - self.canvas.request_draw(self.render) - self.canvas.set_logical_size(*self._starting_size) - - def show( - self, - autoscale: bool = True, - maintain_aspect: bool = None, - toolbar: bool = True, - sidecar: bool = False, - sidecar_kwargs: dict = None, - add_widgets: list = None, - ): - """ - Begins the rendering event loop and shows the plot in the desired output context (jupyter, qt or glfw). - - Parameters - ---------- - autoscale: bool, default ``True`` - autoscale the Scene - - maintain_aspect: bool, default ``True`` - maintain aspect ratio - - toolbar: bool, default ``True`` - show toolbar - - sidecar: bool, default ``True`` - display plot in a ``jupyterlab-sidecar``, only for jupyter output context - - sidecar_kwargs: dict, default ``None`` - kwargs for sidecar instance to display plot - i.e. title, layout - - add_widgets: list of widgets - a list of ipywidgets or QWidget that are vertically stacked below the plot - - Returns - ------- - OutputContext - In jupyter, it will display the plot in the output cell or sidecar - - In Qt, it will display the Plot, toolbar, etc. as stacked widget, use `Plot.widget` to access it. - """ - - # show was already called, return existing output context - if self._output is not None: - return self._output - - self.start_render() - - if sidecar_kwargs is None: - sidecar_kwargs = dict() - - if add_widgets is None: - add_widgets = list() - - # flip y axis if ImageGraphics are present - if hasattr(self, "_subplots"): - for subplot in self: - for g in subplot.graphics: - if isinstance(g, ImageGraphic): - subplot.camera.local.scale_y *= -1 - break - else: - for g in self.graphics: - if isinstance(g, ImageGraphic): - self.camera.local.scale_y *= -1 - break - - if autoscale: - self._autoscale_init(maintain_aspect) - - # used for generating images in docs using nbsphinx - if "NB_SNAPSHOT" in os.environ.keys(): - if os.environ["NB_SNAPSHOT"] == "1": - return self.canvas.snapshot() - - # return the appropriate OutputContext based on the current canvas - if self.canvas.__class__.__name__ == "JupyterWgpuCanvas": - from ._jupyter_output import JupyterOutputContext # noqa - inline import - - self._output = JupyterOutputContext( - frame=self, - make_toolbar=toolbar, - use_sidecar=sidecar, - sidecar_kwargs=sidecar_kwargs, - add_widgets=add_widgets, - ) - - elif self.canvas.__class__.__name__ == "QWgpuCanvas": - from ._qt_output import QOutputContext # noqa - inline import - - self._output = QOutputContext( - frame=self, make_toolbar=toolbar, add_widgets=add_widgets - ) - - else: # assume GLFW, the output context is just the canvas - self._output = self.canvas - - # return the output context, this call is required for jupyter but not for Qt - return self._output - - def close(self): - """Close the output context""" - self._output.close() diff --git a/fastplotlib/layouts/_frame/_qt_toolbar.py b/fastplotlib/layouts/_frame/_qt_toolbar.py deleted file mode 100644 index d62994c2d..000000000 --- a/fastplotlib/layouts/_frame/_qt_toolbar.py +++ /dev/null @@ -1,250 +0,0 @@ -from datetime import datetime -from functools import partial -from math import copysign -import traceback -from typing import * - -from ...utils.gui import QtCore, QtWidgets -from ...graphics.selectors import PolygonSelector -from ._toolbar import ToolBar -from ._qtoolbar_template import Ui_QToolbar - - -class QToolbar( - ToolBar, QtWidgets.QWidget -): # inheritance order MUST be Toolbar first, QWidget second! Else breaks - """Toolbar for Qt context""" - - def __init__(self, output_context, plot): - QtWidgets.QWidget.__init__(self, parent=output_context) - ToolBar.__init__(self, plot) - - # initialize UI - self.ui = Ui_QToolbar() - self.ui.setupUi(self) - - # connect button events - self.ui.auto_scale_button.clicked.connect(self.auto_scale_handler) - self.ui.center_button.clicked.connect(self.center_scene_handler) - self.ui.panzoom_button.toggled.connect(self.panzoom_handler) - self.ui.maintain_aspect_button.toggled.connect(self.maintain_aspect_handler) - self.ui.y_direction_button.clicked.connect(self.y_direction_handler) - - # the subplot labels that update when a user click on subplots - if hasattr(self.plot, "_subplots"): - subplot = self.plot[0, 0] - # set label from first subplot name - if subplot.name is not None: - name = subplot.name - else: - name = str(subplot.position) - - # here we will just use a simple label, not a dropdown like ipywidgets - # the dropdown implementation is tedious with Qt - self.ui.current_subplot = QtWidgets.QLabel(parent=self) - self.ui.current_subplot.setText(name) - self.ui.horizontalLayout.addWidget(self.ui.current_subplot) - - # update the subplot label when a subplot is clicked into - self.plot.renderer.add_event_handler(self.update_current_subplot, "click") - - self.setMaximumHeight(35) - - # set the initial values for buttons - self.ui.maintain_aspect_button.setChecked( - self.current_subplot.camera.maintain_aspect - ) - self.ui.panzoom_button.setChecked(self.current_subplot.controller.enabled) - - if copysign(1, self.current_subplot.camera.local.scale_y) == -1: - self.ui.y_direction_button.setText("v") - else: - self.ui.y_direction_button.setText("^") - - def update_current_subplot(self, ev): - """update the text label for the current subplot""" - for subplot in self.plot: - pos = subplot.map_screen_to_world((ev.x, ev.y)) - if pos is not None: - if subplot.name is not None: - name = subplot.name - else: - name = str(subplot.position) - self.ui.current_subplot.setText(name) - - # set buttons w.r.t. current subplot - self.ui.panzoom_button.setChecked(subplot.controller.enabled) - self.ui.maintain_aspect_button.setChecked( - subplot.camera.maintain_aspect - ) - - if copysign(1, subplot.camera.local.scale_y) == -1: - self.ui.y_direction_button.setText("v") - else: - self.ui.y_direction_button.setText("^") - - def _get_subplot_dropdown_value(self) -> str: - return self.ui.current_subplot.text() - - def auto_scale_handler(self, *args): - self.current_subplot.auto_scale( - maintain_aspect=self.current_subplot.camera.maintain_aspect - ) - - def center_scene_handler(self, *args): - self.current_subplot.center_scene() - - def panzoom_handler(self, value: bool): - self.current_subplot.controller.enabled = value - - def maintain_aspect_handler(self, value: bool): - for camera in self.current_subplot.controller.cameras: - camera.maintain_aspect = value - - def y_direction_handler(self, *args): - # flip every camera under the same controller - for camera in self.current_subplot.controller.cameras: - camera.local.scale_y *= -1 - - if copysign(1, self.current_subplot.camera.local.scale_y) == -1: - self.ui.y_direction_button.setText("v") - else: - self.ui.y_direction_button.setText("^") - - def record_handler(self, ev): - if self.ui.record_button.isChecked(): - try: - self.plot.record_start( - f"./{datetime.now().isoformat(timespec='seconds').replace(':', '_')}.mp4" - ) - except Exception: - traceback.print_exc() - self.ui.record_button.setChecked(False) - else: - self.plot.record_stop() - - def add_polygon(self, *args): - ps = PolygonSelector(edge_width=3, edge_color="mageneta") - self.current_subplot.add_graphic(ps, center=False) - - -# TODO: There must be a better way to do this -# TODO: Check if an interface exists between ipywidgets and Qt -class SliderInterface: - """ - This exists so that ImageWidget has a common interface for Sliders. - - This interface makes a QSlider behave somewhat like a ipywidget IntSlider, enough for ImageWidget to function. - """ - - def __init__(self, qslider): - self.qslider = qslider - - @property - def value(self) -> int: - return self.qslider.value() - - @value.setter - def value(self, value: int): - self.qslider.setValue(value) - - @property - def max(self) -> int: - return self.qslider.maximum() - - @max.setter - def max(self, value: int): - self.qslider.setMaximum(value) - - @property - def min(self): - return self.qslider.minimum() - - @min.setter - def min(self, value: int): - self.qslider.setMinimum(value) - - -class QToolbarImageWidget(QtWidgets.QWidget): - """Toolbar for ImageWidget""" - - def __init__(self, image_widget): - QtWidgets.QWidget.__init__(self) - - # vertical layout - self.vlayout = QtWidgets.QVBoxLayout(self) - - self.image_widget = image_widget - - hlayout_buttons = QtWidgets.QHBoxLayout() - - self.reset_vmin_vmax_button = QtWidgets.QPushButton(self) - self.reset_vmin_vmax_button.setText("auto-contrast") - self.reset_vmin_vmax_button.clicked.connect(self.image_widget.reset_vmin_vmax) - hlayout_buttons.addWidget(self.reset_vmin_vmax_button) - - self.reset_vmin_vmax_hlut_button = QtWidgets.QPushButton(self) - self.reset_vmin_vmax_hlut_button.setText("reset histogram-lut") - self.reset_vmin_vmax_hlut_button.clicked.connect( - self.image_widget.reset_vmin_vmax_frame - ) - hlayout_buttons.addWidget(self.reset_vmin_vmax_hlut_button) - - self.vlayout.addLayout(hlayout_buttons) - - self.sliders: Dict[str, SliderInterface] = dict() - - # has time and/or z-volume - if self.image_widget.ndim > 2: - # create a slider, spinbox and dimension label for each dimension in the ImageWidget - for dim in self.image_widget.slider_dims: - hlayout = ( - QtWidgets.QHBoxLayout() - ) # horizontal stack for label, slider, spinbox - - # max value for current dimension - max_val = self.image_widget._dims_max_bounds[dim] - 1 - - # make slider - slider = QtWidgets.QSlider(self) - slider.setOrientation(QtCore.Qt.Orientation.Horizontal) - slider.setMinimum(0) - slider.setMaximum(max_val) - slider.setValue(0) - slider.setSingleStep(1) - slider.setPageStep(10) - - # make spinbox - spinbox = QtWidgets.QSpinBox(self) - spinbox.setMinimum(0) - spinbox.setMaximum(max_val) - spinbox.setValue(0) - spinbox.setSingleStep(1) - - # link slider and spinbox - slider.valueChanged.connect(spinbox.setValue) - spinbox.valueChanged.connect(slider.setValue) - - # connect slider to change the index within the dimension - slider.valueChanged.connect( - partial(self.image_widget._slider_value_changed, dim) - ) - - # slider dimension label - slider_label = QtWidgets.QLabel(self) - slider_label.setText(dim) - - # add the widgets to the horizontal layout - hlayout.addWidget(slider_label) - hlayout.addWidget(slider) - hlayout.addWidget(spinbox) - - # add horizontal layout to the vertical layout - self.vlayout.addLayout(hlayout) - - # add to sliders dict for easier access to users - self.sliders[dim] = SliderInterface(slider) - - max_height = 35 + (35 * len(self.sliders.keys())) - - self.setMaximumHeight(max_height) diff --git a/fastplotlib/layouts/_plot.py b/fastplotlib/layouts/_plot.py deleted file mode 100644 index 4656649e6..000000000 --- a/fastplotlib/layouts/_plot.py +++ /dev/null @@ -1,68 +0,0 @@ -from typing import * - -import pygfx -from wgpu.gui import WgpuCanvasBase - -from ._subplot import Subplot -from ._frame import Frame -from ._record_mixin import RecordMixin - - -class Plot(Subplot, Frame, RecordMixin): - def __init__( - self, - canvas: Union[str, WgpuCanvasBase] = None, - renderer: pygfx.WgpuRenderer = None, - camera: Union[str, pygfx.PerspectiveCamera] = "2d", - controller: Union[str, pygfx.Controller] = None, - size: Tuple[int, int] = (500, 300), - **kwargs, - ): - """ - Simple Plot object. - - Parameters - ---------- - canvas: WgpuCanvas, optional - Canvas for drawing - - renderer: pygfx.Renderer, optional - pygfx renderer instance - - camera: str or pygfx.PerspectiveCamera, optional - | One of ``"2d"`` or ``"3d"`` indicating 2D or 3D camera - - controller: str or pygfx.Controller, optional - Usually ``None``, you can pass an existing controller from another - ``Plot`` or ``Subplot`` to synchronize them. - - You can also pass str arguments of valid controller names, see Subplot docstring for valid names - - size: (int, int) - starting size of canvas, default (500, 300) - - kwargs - passed to Subplot, for example ``name`` - - """ - super().__init__( - parent=None, - position=(0, 0), - parent_dims=(1, 1), - canvas=canvas, - renderer=renderer, - camera=camera, - controller=controller, - **kwargs, - ) - RecordMixin.__init__(self) - Frame.__init__(self) - - self._starting_size = size - - def render(self): - """performs a single render of the plot, not for the user""" - super().render() - - self.renderer.flush() - self.canvas.request_draw() diff --git a/fastplotlib/layouts/_plot_area.py b/fastplotlib/layouts/_plot_area.py index 37a25bbcc..bbc5b0e15 100644 --- a/fastplotlib/layouts/_plot_area.py +++ b/fastplotlib/layouts/_plot_area.py @@ -92,7 +92,7 @@ def get_refcounts(self): def __init__( self, - parent: Union["PlotArea", "GridPlot"], + parent: Union["PlotArea", "Figure"], position: tuple[int, int] | str, camera: pygfx.PerspectiveCamera, controller: pygfx.Controller, @@ -103,11 +103,11 @@ def __init__( ): """ Base class for plot creation and management. ``PlotArea`` is not intended to be instantiated by users - but rather to provide functionality for ``subplot`` in ``gridplot`` and single ``plot``. + but rather to provide functionality for ``subplots`` in a user ``Figure`` Parameters ---------- - parent: PlotArea or GridPlot + parent: PlotArea or Figure parent object position: Any @@ -181,7 +181,7 @@ def parent(self): @property def position(self) -> tuple[int, int] | str: - """Position of this plot area within a larger layout (such as GridPlot) if relevant""" + """Position of this plot area within a larger layout (such as a Figure) if relevant""" return self._position @property @@ -265,7 +265,7 @@ def controller(self, new_controller: str | pygfx.Controller): # TODO: monkeypatch until we figure out a better # pygfx plans on refactoring viewports anyways if self.parent is not None: - if self.parent.__class__.__name__ == "GridPlot": + if self.parent.__class__.__name__ == "Figure": for subplot in self.parent: if subplot.camera in cameras_list: new_controller.register_events(subplot.viewport) @@ -312,7 +312,7 @@ def get_rect(self) -> tuple[float, float, float, float]: Returns the viewport rect to define the rectangle occupied by the viewport w.r.t. the Canvas. - If this is a subplot within a GridPlot, it returns the rectangle + If this is a subplot within a Figure, it returns the rectangle for only this subplot w.r.t. the parent canvas. Must return: [x_pos, y_pos, width_viewport, height_viewport] diff --git a/fastplotlib/layouts/_record_mixin.py b/fastplotlib/layouts/_record_mixin.py deleted file mode 100644 index 59a8e92e4..000000000 --- a/fastplotlib/layouts/_record_mixin.py +++ /dev/null @@ -1,241 +0,0 @@ -from pathlib import Path -from multiprocessing import Queue, Process -from time import time - - -def _get_av(): - try: - import av - except ImportError: - raise ModuleNotFoundError( - "Recording to video file requires `av`:\n" - "https://github.com/PyAV-Org/PyAV" - ) from None - else: - return av - - -class VideoWriterAV(Process): - """Video writer, uses PyAV in an external process to write frames to disk""" - - def __init__( - self, - path: Path | str, - queue: Queue, - fps: int, - width: int, - height: int, - codec: str, - pixel_format: str, - options: dict = None, - ): - super().__init__() - self.queue = queue - - av = _get_av() - self.container = av.open(path, mode="w") - - self.stream = self.container.add_stream(codec, rate=fps, options=options) - - # in case libx264, trim last rows and/or column - # because libx264 doesn't like non-even number width or height - if width % 2 != 0: - width -= 1 - if height % 2 != 0: - height -= 1 - - self.stream.width = width - self.stream.height = height - - self.stream.pix_fmt = pixel_format - - def run(self): - av = _get_av() - while True: - if self.queue.empty(): # no frame to write - continue - - frame = self.queue.get() - - # recording has ended - if frame is None: - self.container.close() - break - - frame = av.VideoFrame.from_ndarray( - frame[ - : self.stream.height, : self.stream.width - ], # trim if necessary because of x264 - format="rgb24", - ) - - for packet in self.stream.encode(frame): - self.container.mux(packet) - - # I don't exactly know what this does, copied from pyav example - for packet in self.stream.encode(): - self.container.mux(packet) - - # close file - self.container.close() - - # close process, release resources - self.close() - - -# adds recording functionality to GridPlot and Plot -class RecordMixin: - def __init__(self): - self._video_writer: VideoWriterAV = None - self._video_writer_queue = Queue() - self._record_fps = 25 - self._record_timer = 0 - self._record_start_time = 0 - - def _record(self): - """ - Sends frame to VideoWriter through video writer queue - """ - # current time - t = time() - - # put frame in queue only if enough time as passed according to the desired framerate - # otherwise it tries to record EVERY frame on every rendering cycle, which just blocks the rendering - if t - self._record_timer < (1 / self._record_fps): - return - - # reset timer - self._record_timer = t - - if self._video_writer is not None: - ss = self.canvas.snapshot() - # exclude alpha channel - self._video_writer_queue.put(ss.data[..., :-1]) - - def record_start( - self, - path: str | Path, - fps: int = 25, - codec: str = "mpeg4", - pixel_format: str = "yuv420p", - options: dict = None, - ): - """ - Start a recording, experimental. Call ``record_end()`` to end a recording. - Note: playback duration does not exactly match recording duration. - - Requires PyAV: https://github.com/PyAV-Org/PyAV - - **Do not resize canvas during a recording, the width and height must remain constant!** - - Parameters - ---------- - path: str or Path - path to save the recording - - fps: int, default ``25`` - framerate, do not use > 25 within jupyter - - codec: str, default "mpeg4" - codec to use, see ``ffmpeg`` list: https://www.ffmpeg.org/ffmpeg-codecs.html . - In general, ``"mpeg4"`` should work on most systems. ``"libx264"`` is a - better option if you have it installed. - - pixel_format: str, default "yuv420p" - pixel format - - options: dict, optional - Codec options. For example, if using ``"mpeg4"`` you can use ``{"q:v": "20"}`` to set the quality between - 1-31, where "1" is highest and "31" is lowest. If using ``"libx264"``` you can use ``{"crf": "30"}`` where - the "crf" value is between "0" (highest quality) and "50" (lowest quality). See ``ffmpeg`` docs for more - info on codec options - - Examples - -------- - - With ``"mpeg4"`` - - .. code-block:: python - - # create a plot or gridplot etc - - # start recording video - plot.record_start("./video.mp4", options={"q:v": "20"} - - # do stuff like interacting with the plot, change things, etc. - - # end recording - plot.record_end() - - With ``"libx264"`` - - .. code-block:: python - - # create a plot or gridplot etc - - # start recording video - plot.record_start("./vid_x264.mp4", codec="libx264", options={"crf": "25"}) - - # do stuff like interacting with the plot, change things, etc. - - # end recording - plot.record_end() - - """ - - if Path(path).exists(): - raise FileExistsError(f"File already exists at given path: {path}") - - # queue for sending frames to VideoWriterAV process - self._video_writer_queue = Queue() - - # snapshot to get canvas width height - ss = self.canvas.snapshot() - - # writer process - self._video_writer = VideoWriterAV( - path=str(path), - queue=self._video_writer_queue, - fps=int(fps), - width=ss.width, - height=ss.height, - codec=codec, - pixel_format=pixel_format, - options=options, - ) - - # start writer process - self._video_writer.start() - - # 1.3 seems to work well to reduce that difference between playback time and recording time - # will properly investigate later - self._record_fps = fps * 1.3 - self._record_start_time = time() - - # record timer used to maintain desired framerate - self._record_timer = time() - - self.add_animations(self._record) - - def record_stop(self) -> float: - """ - End a current recording. Returns the real duration of the recording - - Returns - ------- - float - recording duration - """ - - # tell video writer that recording has finished - self._video_writer_queue.put(None) - - # wait for writer to finish - self._video_writer.join(timeout=5) - - self._video_writer = None - - # so self._record() is no longer called on every render cycle - self.remove_animation(self._record) - - return time() - self._record_start_time diff --git a/fastplotlib/layouts/_subplot.py b/fastplotlib/layouts/_subplot.py index 4b1e92c51..a541c9d78 100644 --- a/fastplotlib/layouts/_subplot.py +++ b/fastplotlib/layouts/_subplot.py @@ -15,7 +15,7 @@ class Subplot(PlotArea, GraphicMethodsMixin): def __init__( self, - parent: Union["GridPlot", None] = None, + parent: Union["Figure", None] = None, position: tuple[int, int] = None, parent_dims: tuple[int, int] = None, camera: Literal["2d", "3d"] | pygfx.PerspectiveCamera = "2d", @@ -29,22 +29,22 @@ def __init__( name: str = None, ): """ - General plot object that composes a ``Gridplot``. Each ``Gridplot`` instance will have [n rows, n columns] + General plot object is found within a ``Figure``. Each ``Figure`` instance will have [n rows, n columns] of subplots. .. important:: - ``Subplot`` is not meant to be constructed directly, it only exists as part of a ``GridPlot`` + ``Subplot`` is not meant to be constructed directly, it only exists as part of a ``Figure`` Parameters ---------- - parent: 'GridPlot' | None - parent GridPlot instance + parent: 'Figure' | None + parent Figure instance position: (int, int), optional - corresponds to the [row, column] position of the subplot within a ``Gridplot`` + corresponds to the [row, column] position of the subplot within a ``Figure`` parent_dims: (int, int), optional - dimensions of the parent ``GridPlot`` + dimensions of the parent ``Figure`` camera: str or pygfx.PerspectiveCamera, default '2d' indicates the FOV for the camera, '2d' sets ``fov = 0``, '3d' sets ``fov = 50``. diff --git a/fastplotlib/layouts/_video_writer.py b/fastplotlib/layouts/_video_writer.py new file mode 100644 index 000000000..b7e111b50 --- /dev/null +++ b/fastplotlib/layouts/_video_writer.py @@ -0,0 +1,82 @@ +from pathlib import Path +from multiprocessing import Queue, Process + + +def _get_av(): + try: + import av + except ImportError: + raise ModuleNotFoundError( + "Recording to video file requires `av`:\n" + "https://github.com/PyAV-Org/PyAV" + ) from None + else: + return av + + +class VideoWriterAV(Process): + """Video writer, uses PyAV in an external process to write frames to disk""" + + def __init__( + self, + path: Path | str, + queue: Queue, + fps: int, + width: int, + height: int, + codec: str, + pixel_format: str, + options: dict = None, + ): + super().__init__() + self.queue = queue + + av = _get_av() + self.container = av.open(path, mode="w") + + self.stream = self.container.add_stream(codec, rate=fps, options=options) + + # in case libx264, trim last rows and/or column + # because libx264 doesn't like non-even number width or height + if width % 2 != 0: + width -= 1 + if height % 2 != 0: + height -= 1 + + self.stream.width = width + self.stream.height = height + + self.stream.pix_fmt = pixel_format + + def run(self): + av = _get_av() + while True: + if self.queue.empty(): # no frame to write + continue + + frame = self.queue.get() + + # recording has ended + if frame is None: + self.container.close() + break + + frame = av.VideoFrame.from_ndarray( + frame[ + : self.stream.height, : self.stream.width + ], # trim if necessary because of x264 + format="rgb24", + ) + + for packet in self.stream.encode(frame): + self.container.mux(packet) + + # I don't exactly know what this does, copied from pyav example + for packet in self.stream.encode(): + self.container.mux(packet) + + # close file + self.container.close() + + # close process, release resources + self.close() diff --git a/fastplotlib/layouts/output/__init__.py b/fastplotlib/layouts/output/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/fastplotlib/layouts/_frame/_ipywidget_toolbar.py b/fastplotlib/layouts/output/_ipywidget_toolbar.py similarity index 54% rename from fastplotlib/layouts/_frame/_ipywidget_toolbar.py rename to fastplotlib/layouts/output/_ipywidget_toolbar.py index 5b42c8eab..787c8d442 100644 --- a/fastplotlib/layouts/_frame/_ipywidget_toolbar.py +++ b/fastplotlib/layouts/output/_ipywidget_toolbar.py @@ -2,20 +2,14 @@ from datetime import datetime from itertools import product from math import copysign -from functools import partial from pathlib import Path from ipywidgets.widgets import ( - IntSlider, - VBox, HBox, ToggleButton, Dropdown, Layout, Button, - BoundedIntText, - Play, - jslink, Image, ) @@ -27,8 +21,8 @@ class IpywidgetToolBar(HBox, ToolBar): """Basic toolbar using ipywidgets""" - def __init__(self, plot): - ToolBar.__init__(self, plot) + def __init__(self, figure): + ToolBar.__init__(self, figure) self._auto_scale_button = Button( value=False, @@ -107,27 +101,26 @@ def __init__(self, plot): ) widgets.append(image) - if hasattr(self.plot, "_subplots"): - positions = list( - product(range(self.plot.shape[0]), range(self.plot.shape[1])) - ) - values = list() - for pos in positions: - if self.plot[pos].name is not None: - values.append(self.plot[pos].name) - else: - values.append(str(pos)) - - self._dropdown = Dropdown( - options=values, - disabled=False, - description="Subplots:", - layout=Layout(width="200px"), - ) + positions = list( + product(range(self.figure.shape[0]), range(self.figure.shape[1])) + ) + values = list() + for pos in positions: + if self.figure[pos].name is not None: + values.append(self.figure[pos].name) + else: + values.append(str(pos)) + + self._dropdown = Dropdown( + options=values, + disabled=False, + description="Subplots:", + layout=Layout(width="200px"), + ) - self.plot.renderer.add_event_handler(self.update_current_subplot, "click") + self.figure.renderer.add_event_handler(self.update_current_subplot, "click") - widgets.append(self._dropdown) + widgets.append(self._dropdown) self._panzoom_controller_button.observe(self.panzoom_handler, "value") self._auto_scale_button.on_click(self.auto_scale_handler) @@ -176,7 +169,7 @@ def y_direction_handler(self, obj): self._y_direction_button.icon = "arrow-up" def update_current_subplot(self, ev): - for subplot in self.plot: + for subplot in self.figure: pos = subplot.map_screen_to_world((ev.x, ev.y)) if pos is not None: # update self.dropdown @@ -195,137 +188,15 @@ def update_current_subplot(self, ev): def record_plot(self, obj): if self._record_button.value: try: - self.plot.record_start( + self.figure.recorder.start( f"./{datetime.now().isoformat(timespec='seconds').replace(':', '_')}.mp4" ) except Exception: traceback.print_exc() self._record_button.value = False else: - self.plot.record_stop() + self.figure.recorder.stop() def add_polygon(self, obj): ps = PolygonSelector(edge_width=3, edge_color="magenta") self.current_subplot.add_graphic(ps, center=False) - - -class IpywidgetImageWidgetToolbar(VBox): - def __init__(self, iw): - """ - Basic toolbar for a ImageWidget instance. - - Parameters - ---------- - plot: - """ - self.iw = iw - - self.reset_vminvmax_button = Button( - value=False, - disabled=False, - icon="adjust", - layout=Layout(width="auto"), - tooltip="reset vmin/vmax", - ) - - self.reset_vminvmax_hlut_button = Button( - value=False, - icon="adjust", - description="reset", - layout=Layout(width="auto"), - tooltip="reset vmin/vmax and reset histogram using current frame", - ) - - self.sliders: dict[str, IntSlider] = dict() - - # only for xy data, no time point slider needed - if self.iw.ndim == 2: - widgets = [self.reset_vminvmax_button] - # for txy, tzxy, etc. data - else: - for dim in self.iw.slider_dims: - slider = IntSlider( - min=0, - max=self.iw._dims_max_bounds[dim] - 1, - step=1, - value=0, - description=f"dimension: {dim}", - orientation="horizontal", - ) - - slider.observe( - partial(self.iw._slider_value_changed, dim), names="value" - ) - - self.sliders[dim] = slider - - self.step_size_setter = BoundedIntText( - value=1, - min=1, - max=self.sliders["t"].max, - step=1, - description="Step Size:", - disabled=False, - description_tooltip="set slider step", - layout=Layout(width="150px"), - ) - self.speed_text = BoundedIntText( - value=100, - min=1, - max=1_000, - step=50, - description="Speed", - disabled=False, - description_tooltip="Playback speed, this is NOT framerate.\nArbitrary units between 1 - 1,000", - layout=Layout(width="150px"), - ) - self.play_button = Play( - value=0, - min=self.sliders["t"].min, - max=self.sliders["t"].max, - step=self.sliders["t"].step, - description="play/pause", - disabled=False, - ) - widgets = [ - self.reset_vminvmax_button, - self.reset_vminvmax_hlut_button, - self.play_button, - self.step_size_setter, - self.speed_text, - ] - - self.play_button.interval = 10 - - self.step_size_setter.observe(self._change_stepsize, "value") - self.speed_text.observe(self._change_framerate, "value") - jslink((self.play_button, "value"), (self.sliders["t"], "value")) - jslink((self.play_button, "max"), (self.sliders["t"], "max")) - - self.reset_vminvmax_button.on_click(self._reset_vminvmax) - self.reset_vminvmax_hlut_button.on_click(self._reset_vminvmax_frame) - - self.iw.gridplot.renderer.add_event_handler(self._set_slider_layout, "resize") - - # the buttons - self.hbox = HBox(widgets) - - super().__init__((self.hbox, *list(self.sliders.values()))) - - def _reset_vminvmax(self, obj): - self.iw.reset_vmin_vmax() - - def _reset_vminvmax_frame(self, obj): - self.iw.reset_vmin_vmax_frame() - - def _change_stepsize(self, obj): - self.sliders["t"].step = self.step_size_setter.value - - def _change_framerate(self, change): - interval = int(1000 / change["new"]) - self.play_button.interval = interval - - def _set_slider_layout(self, *args): - w, h = self.iw.gridplot.renderer.logical_size - for k, v in self.sliders.items(): - v.layout = Layout(width=f"{w}px") diff --git a/fastplotlib/layouts/output/_qt_toolbar.py b/fastplotlib/layouts/output/_qt_toolbar.py new file mode 100644 index 000000000..4334f1369 --- /dev/null +++ b/fastplotlib/layouts/output/_qt_toolbar.py @@ -0,0 +1,125 @@ +from datetime import datetime +from math import copysign +import traceback + +from ...utils.gui import QtWidgets +from ...graphics.selectors import PolygonSelector +from ._toolbar import ToolBar +from ._qtoolbar_template import Ui_QToolbar + + +class QToolbar( + ToolBar, QtWidgets.QWidget +): # inheritance order MUST be Toolbar first, QWidget second! Else breaks + """Toolbar for Qt context""" + + def __init__(self, output_context, figure): + QtWidgets.QWidget.__init__(self, parent=output_context) + ToolBar.__init__(self, figure) + + # initialize UI + self.ui = Ui_QToolbar() + self.ui.setupUi(self) + + # connect button events + self.ui.auto_scale_button.clicked.connect(self.auto_scale_handler) + self.ui.center_button.clicked.connect(self.center_scene_handler) + self.ui.panzoom_button.toggled.connect(self.panzoom_handler) + self.ui.maintain_aspect_button.toggled.connect(self.maintain_aspect_handler) + self.ui.y_direction_button.clicked.connect(self.y_direction_handler) + + # subplot labels update when a user click on subplots + subplot = self.figure[0, 0] + # set label from first subplot name + if subplot.name is not None: + name = subplot.name + else: + name = str(subplot.position) + + # here we will just use a simple label, not a dropdown like ipywidgets + # the dropdown implementation is tedious with Qt + self.ui.current_subplot = QtWidgets.QLabel(parent=self) + self.ui.current_subplot.setText(name) + self.ui.horizontalLayout.addWidget(self.ui.current_subplot) + + # update the subplot label when a subplot is clicked into + self.figure.renderer.add_event_handler(self.update_current_subplot, "click") + + self.setMaximumHeight(35) + + # set the initial values for buttons + self.ui.maintain_aspect_button.setChecked( + self.current_subplot.camera.maintain_aspect + ) + self.ui.panzoom_button.setChecked(self.current_subplot.controller.enabled) + + if copysign(1, self.current_subplot.camera.local.scale_y) == -1: + self.ui.y_direction_button.setText("v") + else: + self.ui.y_direction_button.setText("^") + + def update_current_subplot(self, ev): + """update the text label for the current subplot""" + for subplot in self.figure: + pos = subplot.map_screen_to_world((ev.x, ev.y)) + if pos is not None: + if subplot.name is not None: + name = subplot.name + else: + name = str(subplot.position) + self.ui.current_subplot.setText(name) + + # set buttons w.r.t. current subplot + self.ui.panzoom_button.setChecked(subplot.controller.enabled) + self.ui.maintain_aspect_button.setChecked( + subplot.camera.maintain_aspect + ) + + if copysign(1, subplot.camera.local.scale_y) == -1: + self.ui.y_direction_button.setText("v") + else: + self.ui.y_direction_button.setText("^") + + def _get_subplot_dropdown_value(self) -> str: + return self.ui.current_subplot.text() + + def auto_scale_handler(self, *args): + self.current_subplot.auto_scale( + maintain_aspect=self.current_subplot.camera.maintain_aspect + ) + + def center_scene_handler(self, *args): + self.current_subplot.center_scene() + + def panzoom_handler(self, value: bool): + self.current_subplot.controller.enabled = value + + def maintain_aspect_handler(self, value: bool): + for camera in self.current_subplot.controller.cameras: + camera.maintain_aspect = value + + def y_direction_handler(self, *args): + # flip every camera under the same controller + for camera in self.current_subplot.controller.cameras: + camera.local.scale_y *= -1 + + if copysign(1, self.current_subplot.camera.local.scale_y) == -1: + self.ui.y_direction_button.setText("v") + else: + self.ui.y_direction_button.setText("^") + + def record_handler(self, ev): + if self.ui.record_button.isChecked(): + try: + self.figure.record_start( + f"./{datetime.now().isoformat(timespec='seconds').replace(':', '_')}.mp4" + ) + except Exception: + traceback.print_exc() + self.ui.record_button.setChecked(False) + else: + self.figure.record_stop() + + def add_polygon(self, *args): + ps = PolygonSelector(edge_width=3, edge_color="mageneta") + self.current_subplot.add_graphic(ps, center=False) diff --git a/fastplotlib/layouts/_frame/_qtoolbar_template.py b/fastplotlib/layouts/output/_qtoolbar_template.py similarity index 100% rename from fastplotlib/layouts/_frame/_qtoolbar_template.py rename to fastplotlib/layouts/output/_qtoolbar_template.py diff --git a/fastplotlib/layouts/_frame/_toolbar.py b/fastplotlib/layouts/output/_toolbar.py similarity index 82% rename from fastplotlib/layouts/_frame/_toolbar.py rename to fastplotlib/layouts/output/_toolbar.py index 6a0485655..5edd201fa 100644 --- a/fastplotlib/layouts/_frame/_toolbar.py +++ b/fastplotlib/layouts/output/_toolbar.py @@ -2,8 +2,8 @@ class ToolBar: - def __init__(self, plot): - self.plot = plot + def __init__(self, figure): + self.figure = figure def _get_subplot_dropdown_value(self) -> str: raise NotImplemented @@ -11,17 +11,17 @@ def _get_subplot_dropdown_value(self) -> str: @property def current_subplot(self) -> Subplot: """Returns current subplot""" - if hasattr(self.plot, "_subplots"): + if hasattr(self.figure, "_subplots"): # parses dropdown or label value as plot name or position current = self._get_subplot_dropdown_value() if current[0] == "(": # str representation of int tuple to tuple of int current = tuple(int(i) for i in current.strip("()").split(",")) - return self.plot[current] + return self.figure[current] else: - return self.plot[current] + return self.figure[current] else: - return self.plot + return self.figure def panzoom_handler(self, ev): raise NotImplemented diff --git a/fastplotlib/layouts/_frame/_jupyter_output.py b/fastplotlib/layouts/output/jupyter_output.py similarity index 100% rename from fastplotlib/layouts/_frame/_jupyter_output.py rename to fastplotlib/layouts/output/jupyter_output.py diff --git a/fastplotlib/layouts/_frame/_qt_output.py b/fastplotlib/layouts/output/qt_output.py similarity index 95% rename from fastplotlib/layouts/_frame/_qt_output.py rename to fastplotlib/layouts/output/qt_output.py index d7e7f2612..20aaef2d1 100644 --- a/fastplotlib/layouts/_frame/_qt_output.py +++ b/fastplotlib/layouts/output/qt_output.py @@ -37,7 +37,7 @@ def __init__( self.vlayout.addWidget(self.frame.canvas) if make_toolbar: # make toolbar and add to layout - self.toolbar = QToolbar(output_context=self, plot=frame) + self.toolbar = QToolbar(output_context=self, figure=frame) self.vlayout.addWidget(self.toolbar) for w in add_widgets: # add any additional widgets to layout diff --git a/fastplotlib/layouts/_frame/qtoolbar.ui b/fastplotlib/layouts/output/qtoolbar.ui similarity index 100% rename from fastplotlib/layouts/_frame/qtoolbar.ui rename to fastplotlib/layouts/output/qtoolbar.ui diff --git a/fastplotlib/utils/functions.py b/fastplotlib/utils/functions.py index da781b521..561863b0c 100644 --- a/fastplotlib/utils/functions.py +++ b/fastplotlib/utils/functions.py @@ -219,7 +219,7 @@ def make_pygfx_colors(colors, n_colors): return colors_array -def calculate_gridshape(n_subplots: int) -> tuple[int, int]: +def calculate_figure_shape(n_subplots: int) -> tuple[int, int]: """ Returns ``(n_rows, n_cols)`` from given number of subplots ``n_subplots`` """ diff --git a/fastplotlib/widgets/_image_widget_ipywidget_toolbar.py b/fastplotlib/widgets/_image_widget_ipywidget_toolbar.py new file mode 100644 index 000000000..24f7a6279 --- /dev/null +++ b/fastplotlib/widgets/_image_widget_ipywidget_toolbar.py @@ -0,0 +1,135 @@ +from functools import partial + +from ipywidgets import ( + VBox, + Button, + Layout, + IntSlider, + BoundedIntText, + Play, + jslink, + HBox, +) + + +class IpywidgetImageWidgetToolbar(VBox): + def __init__(self, iw): + """ + Basic toolbar for a ImageWidget instance. + + Parameters + ---------- + plot: + """ + self.iw = iw + + self.reset_vminvmax_button = Button( + value=False, + disabled=False, + icon="adjust", + layout=Layout(width="auto"), + tooltip="reset vmin/vmax", + ) + + self.reset_vminvmax_hlut_button = Button( + value=False, + icon="adjust", + description="reset", + layout=Layout(width="auto"), + tooltip="reset vmin/vmax and reset histogram using current frame", + ) + + self.sliders: dict[str, IntSlider] = dict() + + # only for xy data, no time point slider needed + if self.iw.ndim == 2: + widgets = [self.reset_vminvmax_button] + # for txy, tzxy, etc. data + else: + for dim in self.iw.slider_dims: + slider = IntSlider( + min=0, + max=self.iw._dims_max_bounds[dim] - 1, + step=1, + value=0, + description=f"dimension: {dim}", + orientation="horizontal", + ) + + slider.observe( + partial(self.iw._slider_value_changed, dim), names="value" + ) + + self.sliders[dim] = slider + + self.step_size_setter = BoundedIntText( + value=1, + min=1, + max=self.sliders["t"].max, + step=1, + description="Step Size:", + disabled=False, + description_tooltip="set slider step", + layout=Layout(width="150px"), + ) + self.speed_text = BoundedIntText( + value=100, + min=1, + max=1_000, + step=50, + description="Speed", + disabled=False, + description_tooltip="Playback speed, this is NOT framerate.\nArbitrary units between 1 - 1,000", + layout=Layout(width="150px"), + ) + self.play_button = Play( + value=0, + min=self.sliders["t"].min, + max=self.sliders["t"].max, + step=self.sliders["t"].step, + description="play/pause", + disabled=False, + ) + widgets = [ + self.reset_vminvmax_button, + self.reset_vminvmax_hlut_button, + self.play_button, + self.step_size_setter, + self.speed_text, + ] + + self.play_button.interval = 10 + + self.step_size_setter.observe(self._change_stepsize, "value") + self.speed_text.observe(self._change_framerate, "value") + jslink((self.play_button, "value"), (self.sliders["t"], "value")) + jslink((self.play_button, "max"), (self.sliders["t"], "max")) + + self.reset_vminvmax_button.on_click(self._reset_vminvmax) + self.reset_vminvmax_hlut_button.on_click(self._reset_vminvmax_frame) + + self.iw.figure.renderer.add_event_handler(self._set_slider_layout, "resize") + + # the buttons + self.hbox = HBox(widgets) + + super().__init__((self.hbox, *list(self.sliders.values()))) + + def _reset_vminvmax(self, obj): + self.iw.reset_vmin_vmax() + + def _reset_vminvmax_frame(self, obj): + self.iw.reset_vmin_vmax_frame() + + def _change_stepsize(self, obj): + self.sliders["t"].step = self.step_size_setter.value + + def _change_framerate(self, change): + interval = int(1000 / change["new"]) + self.play_button.interval = interval + + def _set_slider_layout(self, *args): + w, h = self.iw.figure.renderer.logical_size + + for k, v in self.sliders.items(): + v.layout = Layout(width=f"{w}px") diff --git a/fastplotlib/widgets/_image_widget_qt_toolbar.py b/fastplotlib/widgets/_image_widget_qt_toolbar.py new file mode 100644 index 000000000..2117f95ab --- /dev/null +++ b/fastplotlib/widgets/_image_widget_qt_toolbar.py @@ -0,0 +1,127 @@ +from functools import partial +from typing import Dict + +from fastplotlib.utils.gui import QtWidgets, QtCore + + +# TODO: There must be a better way to do this +# TODO: Check if an interface exists between ipywidgets and Qt +# TODO: Or we won't need it anyways once we have UI in pygfx +class SliderInterface: + """ + This exists so that ImageWidget has a common interface for Sliders. + + This interface makes a QSlider behave somewhat like a ipywidget IntSlider, enough for ImageWidget to function. + """ + + def __init__(self, qslider): + self.qslider = qslider + + @property + def value(self) -> int: + return self.qslider.value() + + @value.setter + def value(self, value: int): + self.qslider.setValue(value) + + @property + def max(self) -> int: + return self.qslider.maximum() + + @max.setter + def max(self, value: int): + self.qslider.setMaximum(value) + + @property + def min(self): + return self.qslider.minimum() + + @min.setter + def min(self, value: int): + self.qslider.setMinimum(value) + + +class QToolbarImageWidget(QtWidgets.QWidget): + """Toolbar for ImageWidget""" + + def __init__(self, image_widget): + QtWidgets.QWidget.__init__(self) + + # vertical layout + self.vlayout = QtWidgets.QVBoxLayout(self) + + self.image_widget = image_widget + + hlayout_buttons = QtWidgets.QHBoxLayout() + + self.reset_vmin_vmax_button = QtWidgets.QPushButton(self) + self.reset_vmin_vmax_button.setText("auto-contrast") + self.reset_vmin_vmax_button.clicked.connect(self.image_widget.reset_vmin_vmax) + hlayout_buttons.addWidget(self.reset_vmin_vmax_button) + + self.reset_vmin_vmax_hlut_button = QtWidgets.QPushButton(self) + self.reset_vmin_vmax_hlut_button.setText("reset histogram-lut") + self.reset_vmin_vmax_hlut_button.clicked.connect( + self.image_widget.reset_vmin_vmax_frame + ) + hlayout_buttons.addWidget(self.reset_vmin_vmax_hlut_button) + + self.vlayout.addLayout(hlayout_buttons) + + self.sliders: Dict[str, SliderInterface] = dict() + + # has time and/or z-volume + if self.image_widget.ndim > 2: + # create a slider, spinbox and dimension label for each dimension in the ImageWidget + for dim in self.image_widget.slider_dims: + hlayout = ( + QtWidgets.QHBoxLayout() + ) # horizontal stack for label, slider, spinbox + + # max value for current dimension + max_val = self.image_widget._dims_max_bounds[dim] - 1 + + # make slider + slider = QtWidgets.QSlider(self) + slider.setOrientation(QtCore.Qt.Orientation.Horizontal) + slider.setMinimum(0) + slider.setMaximum(max_val) + slider.setValue(0) + slider.setSingleStep(1) + slider.setPageStep(10) + + # make spinbox + spinbox = QtWidgets.QSpinBox(self) + spinbox.setMinimum(0) + spinbox.setMaximum(max_val) + spinbox.setValue(0) + spinbox.setSingleStep(1) + + # link slider and spinbox + slider.valueChanged.connect(spinbox.setValue) + spinbox.valueChanged.connect(slider.setValue) + + # connect slider to change the index within the dimension + slider.valueChanged.connect( + partial(self.image_widget._slider_value_changed, dim) + ) + + # slider dimension label + slider_label = QtWidgets.QLabel(self) + slider_label.setText(dim) + + # add the widgets to the horizontal layout + hlayout.addWidget(slider_label) + hlayout.addWidget(slider) + hlayout.addWidget(spinbox) + + # add horizontal layout to the vertical layout + self.vlayout.addLayout(hlayout) + + # add to sliders dict for easier access to users + self.sliders[dim] = SliderInterface(slider) + + max_height = 35 + (35 * len(self.sliders.keys())) + + self.setMaximumHeight(max_height) diff --git a/fastplotlib/widgets/image.py b/fastplotlib/widgets/image.py index acef26a7d..2a4dc31b4 100644 --- a/fastplotlib/widgets/image.py +++ b/fastplotlib/widgets/image.py @@ -1,22 +1,32 @@ -from typing import * +from typing import Any, Literal, Callable from warnings import warn import numpy as np - -from ..layouts import GridPlot +from ..layouts import Figure from ..graphics import ImageGraphic -from ..utils import calculate_gridshape +from ..utils import calculate_figure_shape from .histogram_lut import HistogramLUT -DEFAULT_DIMS_ORDER = { - 2: "xy", - 3: "txy", - 4: "tzxy", - 5: "tzcxy", +# Number of dimensions that represent one image/one frame. For grayscale shape will be [x, y], i.e. 2 dims, for RGB(A) +# shape will be [x, y, c] where c is of size 3 (RGB) or 4 (RGBA) +IMAGE_DIM_COUNTS = {"gray": 2, "rgb": 3} + +# Map boolean (indicating whether we use RGB or grayscale) to the string. Used to index RGB_DIM_MAP +RGB_BOOL_MAP = {False: "gray", True: "rgb"} + +# Dimensions that can be scrolled from a given data array +SCROLLABLE_DIMS_ORDER = { + 0: "", + 1: "t", + 2: "tz", } +ALLOWED_SLIDER_DIMS = {0: "t", 1: "z"} + +ALLOWED_WINDOW_DIMS = {"t", "z"} + def _is_arraylike(obj) -> bool: """ @@ -89,11 +99,11 @@ def __repr__(self): class ImageWidget: @property - def gridplot(self) -> GridPlot: + def figure(self) -> Figure: """ - ``GridPlot`` instance within the `ImageWidget`. + ``Figure`` used by `ImageWidget`. """ - return self._gridplot + return self._figure @property def widget(self): @@ -103,17 +113,17 @@ def widget(self): return self._output @property - def managed_graphics(self) -> List[ImageGraphic]: + def managed_graphics(self) -> list[ImageGraphic]: """List of ``ImageWidget`` managed graphics.""" iw_managed = list() - for subplot in self.gridplot: + for subplot in self.figure: # empty subplots will not have any image widget data if len(subplot.graphics) > 0: iw_managed.append(subplot["image_widget_managed"]) return iw_managed @property - def cmap(self) -> List[str]: + def cmap(self) -> list[str]: cmaps = list() for g in self.managed_graphics: cmaps.append(g.cmap.name) @@ -121,7 +131,7 @@ def cmap(self) -> List[str]: return cmaps @cmap.setter - def cmap(self, names: Union[str, List[str]]): + def cmap(self, names: str | list[str]): if isinstance(names, list): if not all([isinstance(n, str) for n in names]): raise TypeError( @@ -143,32 +153,35 @@ def cmap(self, names: Union[str, List[str]]): g.cmap = names @property - def data(self) -> List[np.ndarray]: + def data(self) -> list[np.ndarray]: """data currently displayed in the widget""" return self._data @property def ndim(self) -> int: - """number of dimensions in the image data displayed in the widget""" + """Number of dimensions of grayscale data displayed in the widget (it will be 1 more for RGB(A) data)""" return self._ndim @property - def dims_order(self) -> List[str]: - """dimension order of the data displayed in the widget""" - return self._dims_order + def n_scrollable_dims(self) -> list[int]: + """ + list indicating the number of dimenensions that are scrollable for each data array + All other dimensions are frame/image data, i.e. [x, y] or [x, y, c] + """ + return self._n_scrollable_dims @property - def sliders(self) -> Dict[str, Any]: + def sliders(self) -> dict[str, Any]: """the ipywidget IntSlider or QSlider instances used by the widget for indexing the desired dimensions""" return self._image_widget_toolbar.sliders @property - def slider_dims(self) -> List[str]: + def slider_dims(self) -> list[str]: """the dimensions that the sliders index""" return self._slider_dims @property - def current_index(self) -> Dict[str, int]: + def current_index(self) -> dict[str, int]: """ Get or set the current index @@ -184,8 +197,58 @@ def current_index(self) -> Dict[str, int]: """ return self._current_index + @property + def n_img_dims(self) -> list[int]: + """ + list indicating the number of dimensions that contain image/single frame data for each data array. + if 2: data are grayscale, i.e. [x, y] dims, if 3: data are [x, y, c] where c is RGB or RGBA, + this is the complement of `n_scrollable_dims` + """ + return self._n_img_dims + + def _get_n_scrollable_dims(self, curr_arr: np.ndarray, rgb: bool) -> list[int]: + """ + For a given ``array`` displayed in the ImageWidget, this function infers how many of the dimensions are + supported by sliders (aka scrollable). Ex: "xy" data has 0 scrollable dims, "txy" has 1, "tzxy" has 2. + + Parameters + ---------- + curr_arr: np.ndarray + np.ndarray or a list of array-like + + rgb: bool + True if we view this as RGB(A) and False if grayscale + + Returns + ------- + int + Number of scrollable dimensions for each ``array`` in the dataset. + """ + + n_img_dims = IMAGE_DIM_COUNTS[RGB_BOOL_MAP[rgb]] + # Make sure each image stack at least ``n_img_dims`` dimensions + if len(curr_arr.shape) < n_img_dims: + raise ValueError( + f"Your array has shape {curr_arr.shape} " + f"but you specified that each image in your array is {n_img_dims}D " + ) + + # If RGB(A), last dim must be 3 or 4 + if n_img_dims == 3: + if not (curr_arr.shape[-1] == 3 or curr_arr.shape[-1] == 4): + raise ValueError( + f"Expected size 3 or 4 for last dimension of RGB(A) array, got: {curr_arr.shape[-1]}." + ) + + n_scrollable_dims = len(curr_arr.shape) - n_img_dims + + if n_scrollable_dims not in SCROLLABLE_DIMS_ORDER.keys(): + raise ValueError(f"Array had shape {curr_arr.shape} which is not supported") + + return n_scrollable_dims + @current_index.setter - def current_index(self, index: Dict[str, int]): + def current_index(self, index: dict[str, int]): # ignore if output context has not been created yet if self.widget is None: return @@ -222,31 +285,31 @@ def current_index(self, index: Dict[str, int]): def __init__( self, - data: Union[np.ndarray, List[np.ndarray]], - dims_order: Union[str, Dict[int, str]] = None, - slider_dims: Union[str, int, List[Union[str, int]]] = None, - window_funcs: Union[int, Dict[str, int]] = None, - frame_apply: Union[callable, Dict[int, callable]] = None, - grid_shape: Tuple[int, int] = None, - names: List[str] = None, - grid_plot_kwargs: dict = None, + data: np.ndarray | list[np.ndarray], + window_funcs: dict[str, tuple[Callable, int]] = None, + frame_apply: Callable | dict[int, Callable] = None, + figure_shape: tuple[int, int] = None, + names: list[str] = None, + figure_kwargs: dict = None, histogram_widget: bool = True, - **kwargs, + rgb: list[bool] = None, + cmap: str = "plasma", + graphic_kwargs: dict = None, ): """ - A high level widget for displaying n-dimensional image data in conjunction with automatically generated - sliders for navigating through 1-2 selected dimensions within image data. - - Can display a single n-dimensional image array or a grid of n-dimensional images. + This widget facilitates high-level navigation through image stacks, which are arrays containing one or more + images. It includes sliders for key dimensions such as "t" (time) and "z", enabling users to smoothly navigate + through one or multiple image stacks simultaneously. - Default dimension orders: + Allowed dimensions orders for each image stack: Note that each has a an optional (c) channel which refers to + RGB(A) a channel. So this channel should be either 3 or 4. ======= ========== n_dims dims order ======= ========== - 2 "xy" - 3 "txy" - 4 "tzxy" + 2 "xy(c)" + 3 "txy(c)" + 4 "tzxy(c)" ======= ========== Parameters @@ -254,41 +317,28 @@ def __init__( data: Union[np.ndarray, List[np.ndarray] array-like or a list of array-like - dims_order: Optional[Union[str, Dict[np.ndarray, str]]] - | ``str`` or a dict mapping to indicate dimension order - | a single ``str`` if ``data`` is a single array, or a list of arrays with the same dimension order - | examples: ``"xyt"``, ``"tzxy"`` - | ``dict`` mapping of ``{array_index: axis_order}`` if specific arrays have a non-default axes order. - | "array_index" is the position of the corresponding array in the data list. - | examples: ``{array_index: "tzxy", another_array_index: "xytz"}`` - - slider_dims: Optional[Union[str, int, List[Union[str, int]]]] - | The dimensions for which to create a slider - | can be a single ``str`` such as **"t"**, **"z"** or a numerical ``int`` that indexes the desired dimension - | can also be a list of ``str`` or ``int`` if multiple sliders are desired for multiple dimensions - | examples: ``"t"``, ``["t", "z"]`` - - window_funcs: Dict[Union[int, str], int] - | average one or more dimensions using a given window - | if a slider exists for only one dimension this can be an ``int``. - | if multiple sliders exist, then it must be a `dict`` mapping in the form of: ``{dimension: window_size}`` - | dimension/axes can be specified using ``str`` such as "t", "z" etc. or ``int`` that indexes the dimension - | if window_size is not an odd number, adds 1 - | use ``None`` to disable averaging for a dimension, example: ``{"t": 5, "z": None}`` + window_funcs: dict[str, tuple[Callable, int]], i.e. {"t" or "z": (callable, int)} + | Apply function(s) with rolling windows along "t" and/or "z" dimensions of the `data` arrays. + | Pass a dict in the form: {dimension: (func, window_size)}, `func` must take a slice of the data array as + | the first argument and must take `axis` as a kwarg. + | Ex: mean along "t" dimension: {"t": (np.mean, 11)}, if `current_index` of "t" is 50, it will pass frames + | 45 to 55 to `np.mean` with `axis=0`. + | Ex: max along z dim: {"z": (np.max, 3)}, passes current, previous & next frame to `np.max` with `axis=1` frame_apply: Union[callable, Dict[int, callable]] - | apply a function to slices of the array before displaying the frame - | pass a single function or a dict of functions to apply to each array individually + | Apply function(s) to `data` arrays before to generate final 2D image that is displayed. + | Ex: apply a spatial gaussian filter + | Pass a single function or a dict of functions to apply to each array individually | examples: ``{array_index: to_grayscale}``, ``{0: to_grayscale, 2: threshold_img}`` | "array_index" is the position of the corresponding array in the data list. | if `window_funcs` is used, then this function is applied after `window_funcs` | this function must be a callable that returns a 2D array | example use case: converting an RGB frame from video to a 2D grayscale frame - grid_shape: Optional[Tuple[int, int]] - manually provide the shape for a gridplot, otherwise a square gridplot is approximated. + figure_shape: Optional[Tuple[int, int]] + manually provide the shape for the Figure, otherwise the number of rows and columns is estimated - grid_plot_kwargs: dict, optional + figure_kwargs: dict, optional passed to `GridPlot` names: Optional[str] @@ -297,40 +347,75 @@ def __init__( histogram_widget: bool, default False make histogram LUT widget for each subplot - kwargs: Any - passed to fastplotlib.graphics.Image + rgb: bool | list[bool], default None + Includes a True or False for each ``array`` in the ImageWidget, indicating whether images are displayed as + grayscale or RGB(A). - """ + graphic_kwargs: Any + passed to each ImageGraphic in the ImageWidget figure subplots + """ self._names = None # output context self._output = None + if _is_arraylike(data): + data = [data] + if isinstance(data, list): # verify that it's a list of np.ndarray if all([_is_arraylike(d) for d in data]): - if grid_shape is None: - grid_shape = calculate_gridshape(len(data)) - # verify that user-specified grid shape is large enough for the number of image arrays passed - elif grid_shape[0] * grid_shape[1] < len(data): - grid_shape = calculate_gridshape(len(data)) + # Grid computations + if figure_shape is None: + figure_shape = calculate_figure_shape(len(data)) + + # verify that user-specified figure shape is large enough for the number of image arrays passed + elif figure_shape[0] * figure_shape[1] < len(data): + figure_shape = calculate_figure_shape(len(data)) warn( - f"Invalid `grid_shape` passed, setting grid shape to: {grid_shape}" + f"Invalid `figure_shape` passed, setting figure shape to: {figure_shape}" ) - _ndim = [d.ndim for d in data] + self._data: list[np.ndarray] = data - # verify that all image arrays have same number of dimensions - # sliders get messy otherwise - if not len(set(_ndim)) == 1: + # Establish number of image dimensions and number of scrollable dimensions for each array + if rgb is None: + rgb = [False] * len(self.data) + if rgb is bool: + rgb = [rgb] + if not isinstance(rgb, list): + raise TypeError( + f"rgb_disp parameter must be a list, a {type(rgb)} was provided" + ) + if not len(rgb) == len(self.data): raise ValueError( - f"Number of dimensions of all data arrays must match, your ndims are: {_ndim}" + f"rgb had length {len(rgb)} but there are {len(self.data)} data arrays; these must be equal" ) - self._data: List[np.ndarray] = data - self._ndim = self.data[0].ndim # all ndim must be same + self._rgb = rgb + + self._n_img_dims = [ + IMAGE_DIM_COUNTS[RGB_BOOL_MAP[self._rgb[i]]] + for i in range(len(self.data)) + ] + + self._n_scrollable_dims = [ + self._get_n_scrollable_dims(self.data[i], self._rgb[i]) + for i in range(len(self.data)) + ] + + # Define ndim of ImageWidget instance as largest number of scrollable dims + 2 (grayscale dimensions) + self._ndim = ( + max( + [ + self.n_scrollable_dims[i] + for i in range(len(self.n_scrollable_dims)) + ] + ) + + IMAGE_DIM_COUNTS[RGB_BOOL_MAP[False]] + ) if names is not None: if not all([isinstance(n, str) for n in names]): @@ -351,165 +436,29 @@ def __init__( f"You have passed the following types:\n" f"{[type(a) for a in data]}" ) - - elif _is_arraylike(data): - self._data = [data] - self._ndim = self.data[0].ndim - - grid_shape = calculate_gridshape(len(self._data)) else: raise TypeError( - f"`data` must be an array-like type representing an n-dimensional image " - f"or a list of array-like representing a grid of n-dimensional images. " + f"`data` must be an array-like type or a list of array-like." f"You have passed the following type {type(data)}" ) - # default dims order if not passed - # updated later if passed - self._dims_order: List[str] = [DEFAULT_DIMS_ORDER[self.ndim]] * len(self.data) - - if dims_order is not None: - if isinstance(dims_order, str): - dims_order = dims_order.lower() - if len(dims_order) != self.ndim: - raise ValueError( - f"number of dims '{len(dims_order)} passed to `dims_order` " - f"does not match ndim '{self.ndim}' of data" - ) - self._dims_order: List[str] = [dims_order] * len(self.data) - elif isinstance(dims_order, dict): - self._dims_order: List[str] = [DEFAULT_DIMS_ORDER[self.ndim]] * len( - self.data - ) - - # dict of {array_ix: dims_order_str} - for data_ix in list(dims_order.keys()): - if not isinstance(data_ix, int): - 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` " - f"does not match ndim '{self.ndim}' of data" - ) - _do = dims_order[data_ix].lower() - # make sure the same dims are present - if not set(_do) == set(DEFAULT_DIMS_ORDER[self.ndim]): - raise ValueError( - f"Invalid `dims_order` passed for one of your arrays, " - f"valid `dims_order` for given number of dimensions " - f"can only contain the following characters: " - f"{DEFAULT_DIMS_ORDER[self.ndim]}" - ) - try: - self.dims_order[data_ix] = _do - except Exception: - raise IndexError( - f"index {data_ix} out of bounds for `dims_order`, the bounds are 0 - {len(self.data)}" - ) - else: - raise TypeError( - f"`dims_order` must be a or , you have passed a: <{type(dims_order)}>" - ) - - if not len(self.dims_order[0]) == self.ndim: - raise ValueError( - f"Number of dims specified by `dims_order`: {len(self.dims_order[0])} does not" - f" match number of dimensions in the `data`: {self.ndim}" - ) - - ao = np.array([sorted(v) for v in self.dims_order]) - - if not np.all(ao == ao[0]): - raise ValueError( - f"`dims_order` for all arrays must contain the same combination of dimensions, your `dims_order` are: " - f"{self.dims_order}" - ) - - # if slider_dims not provided - if slider_dims is None: - # by default sliders are made for all dimensions except the last 2 - default_dim_names = {0: "t", 1: "z", 2: "c"} - slider_dims = list() - for dim in range(self.ndim - 2): - if dim in default_dim_names.keys(): - slider_dims.append(default_dim_names[dim]) - else: - slider_dims.append(f"{dim}") - - # slider for only one of the dimensions - if isinstance(slider_dims, (int, str)): - # if numerical dimension is specified - if isinstance(slider_dims, int): - ao = np.array([v for v in self.dims_order]) - if not np.all(ao == ao[0]): - raise ValueError( - f"`dims_order` for all arrays must be identical if passing in a `slider_dims` argument. " - f"Pass in a argument if the `dims_order` are different for each array." - ) - self._slider_dims: List[str] = [self.dims_order[0][slider_dims]] - - # if dimension specified by str - elif isinstance(slider_dims, str): - if slider_dims not in self.dims_order[0]: - raise ValueError( - f"if `slider_dims` is a , it must be a character found in `dims_order`. " - f"Your `dims_order` characters are: {set(self.dims_order[0])}." - ) - self._slider_dims: List[str] = [slider_dims] - - # multiple sliders, one for each dimension - elif isinstance(slider_dims, list): - self._slider_dims: List[str] = list() - - # make sure window_funcs and frame_apply are dicts if multiple sliders are desired - if (not isinstance(window_funcs, dict)) and (window_funcs is not None): - raise TypeError( - f"`window_funcs` must be a if multiple `slider_dims` are provided. You must specify the " - f"window for each dimension." - ) - if (not isinstance(frame_apply, dict)) and (frame_apply is not None): - raise TypeError( - f"`frame_apply` must be a if multiple `slider_dims` are provided. You must specify a " - f"function for each dimension." - ) - - for sdm in slider_dims: - if isinstance(sdm, int): - ao = np.array([v for v in self.dims_order]) - if not np.all(ao == ao[0]): - raise ValueError( - f"`dims_order` for all arrays must be identical if passing in a `slider_dims` argument. " - f"Pass in a argument if the `dims_order` are different for each array." - ) - # parse int to a str - self.slider_dims.append(self.dims_order[0][sdm]) - - elif isinstance(sdm, str): - if sdm not in self.dims_order[0]: - raise ValueError( - f"if `slider_dims` is a , it must be a character found in `dims_order`. " - f"Your `dims_order` characters are: {set(self.dims_order[0])}." - ) - self.slider_dims.append(sdm) - - else: - raise TypeError( - "If passing a list for `slider_dims` each element must be either an or " - ) - - else: - raise TypeError( - f"`slider_dims` must a , or , you have passed a: {type(slider_dims)}" - ) + # Sliders are made for all dimensions except the image dimensions + self._slider_dims = list() + max_scrollable = max( + [self.n_scrollable_dims[i] for i in range(len(self.n_scrollable_dims))] + ) + for dim in range(max_scrollable): + if dim in ALLOWED_SLIDER_DIMS.keys(): + self.slider_dims.append(ALLOWED_SLIDER_DIMS[dim]) - self._frame_apply: Dict[int, callable] = dict() + self._frame_apply: dict[int, callable] = dict() if frame_apply is not None: if callable(frame_apply): - self._frame_apply = {0: frame_apply} + self._frame_apply = frame_apply elif isinstance(frame_apply, dict): - self._frame_apply: Dict[int, callable] = dict.fromkeys( + self._frame_apply: dict[int, callable] = dict.fromkeys( list(range(len(self.data))) ) @@ -530,34 +479,44 @@ def __init__( ) # current_index stores {dimension_index: slice_index} for every dimension - self._current_index: Dict[str, int] = {sax: 0 for sax in self.slider_dims} + self._current_index: dict[str, int] = {sax: 0 for sax in self.slider_dims} self._window_funcs = None self.window_funcs = window_funcs - self._sliders: Dict[str, Any] = dict() + self._sliders: dict[str, Any] = dict() - # get max bound for all data arrays for all dimensions - self._dims_max_bounds: Dict[str, int] = {k: np.inf for k in self.slider_dims} - for _dim in list(self._dims_max_bounds.keys()): - for array, order in zip(self.data, self.dims_order): - self._dims_max_bounds[_dim] = min( - self._dims_max_bounds[_dim], array.shape[order.index(_dim)] - ) + # get max bound for all data arrays for all slider dimensions and ensure compatibility across slider dims + self._dims_max_bounds: dict[str, int] = {k: 0 for k in self.slider_dims} + for i, _dim in enumerate(list(self._dims_max_bounds.keys())): + for array, partition in zip(self.data, self.n_scrollable_dims): + if partition <= i: + continue + else: + if 0 < self._dims_max_bounds[_dim] != array.shape[i]: + raise ValueError(f"Two arrays differ along dimension {_dim}") + else: + self._dims_max_bounds[_dim] = max( + self._dims_max_bounds[_dim], array.shape[i] + ) - grid_plot_kwargs_default = {"controller_ids": "sync"} - if grid_plot_kwargs is None: - grid_plot_kwargs = dict() + figure_kwargs_default = {"controller_ids": "sync"} + if figure_kwargs is None: + figure_kwargs = dict() # update the default kwargs with any user-specified kwargs # user specified kwargs will overwrite the defaults - grid_plot_kwargs_default.update(grid_plot_kwargs) + figure_kwargs_default.update(figure_kwargs) - self._gridplot: GridPlot = GridPlot( - shape=grid_shape, **grid_plot_kwargs_default - ) + if graphic_kwargs is None: + graphic_kwargs = dict() + + graphic_kwargs.update({"cmap": cmap}) - for data_ix, (d, subplot) in enumerate(zip(self.data, self.gridplot)): + self._figure: Figure = Figure(shape=figure_shape, **figure_kwargs_default) + + self._histogram_widget = histogram_widget + for data_ix, (d, subplot) in enumerate(zip(self.data, self.figure)): if self._names is not None: name = self._names[data_ix] else: @@ -565,12 +524,12 @@ def __init__( frame = self._process_indices(d, slice_indices=self._current_index) frame = self._process_frame_apply(frame, data_ix) - ig = ImageGraphic(frame, name="image_widget_managed", **kwargs) + ig = ImageGraphic(frame, name="image_widget_managed", **graphic_kwargs) subplot.add_graphic(ig) subplot.name = name subplot.set_title(name) - if histogram_widget: + if self._histogram_widget: hlut = HistogramLUT(data=d, image_graphic=ig, name="histogram_lut") subplot.docks["right"].add_graphic(hlut) @@ -582,11 +541,11 @@ def __init__( self._image_widget_toolbar = None @property - def frame_apply(self) -> Union[dict, None]: + def frame_apply(self) -> dict | None: return self._frame_apply @frame_apply.setter - def frame_apply(self, frame_apply: Dict[int, callable]): + def frame_apply(self, frame_apply: dict[int, callable]): if frame_apply is None: frame_apply = dict() @@ -595,7 +554,7 @@ def frame_apply(self, frame_apply: Dict[int, callable]): self.current_index = self.current_index @property - def window_funcs(self) -> Dict[str, _WindowFunctions]: + def window_funcs(self) -> dict[str, _WindowFunctions]: """ Get or set the window functions @@ -607,69 +566,61 @@ def window_funcs(self) -> Dict[str, _WindowFunctions]: return self._window_funcs @window_funcs.setter - def window_funcs(self, sa: Union[int, Dict[str, int]]): - if sa is None: + def window_funcs(self, callable_dict: dict[str, int]): + if callable_dict is None: self._window_funcs = None # force frame to update self.current_index = self.current_index return - # for a single dim - elif isinstance(sa, tuple): - if len(self.slider_dims) > 1: - raise TypeError( - "Must pass dict argument to window_funcs if using multiple sliders. See the docstring." - ) - if not callable(sa[0]) or not isinstance(sa[1], int): - raise TypeError( - "Tuple argument to `window_funcs` must be in the form of (func, window_size). See the docstring." + elif isinstance(callable_dict, dict): + if not set(callable_dict.keys()).issubset(ALLOWED_WINDOW_DIMS): + raise ValueError( + f"The only allowed keys to window funcs are {list(ALLOWED_WINDOW_DIMS)} " + f"Your window func passed in these keys: {list(callable_dict.keys())}" ) - - dim_str = self.slider_dims[0] - self._window_funcs = dict() - self._window_funcs[dim_str] = _WindowFunctions(self, *sa) - - # for multiple dims - elif isinstance(sa, dict): if not all( - [isinstance(_sa, tuple) or (_sa is None) for _sa in sa.values()] + [ + isinstance(_callable_dict, tuple) + for _callable_dict in callable_dict.values() + ] ): raise TypeError( "dict argument to `window_funcs` must be in the form of: " "`{dimension: (func, window_size)}`. " "See the docstring." ) - for v in sa.values(): - if v is not None: - if not callable(v[0]) or not ( - isinstance(v[1], int) or v[1] is None - ): - raise TypeError( - "dict argument to `window_funcs` must be in the form of: " - "`{dimension: (func, window_size)}`. " - "See the docstring." - ) + for v in callable_dict.values(): + if not callable(v[0]): + raise TypeError( + "dict argument to `window_funcs` must be in the form of: " + "`{dimension: (func, window_size)}`. " + "See the docstring." + ) + if not isinstance(v[1], int): + raise TypeError( + f"dict argument to `window_funcs` must be in the form of: " + "`{dimension: (func, window_size)}`. " + f"where window_size is integer. you passed in {v[1]} for window_size" + ) if not isinstance(self._window_funcs, dict): self._window_funcs = dict() - for k in list(sa.keys()): - if sa[k] is None: - self._window_funcs[k] = None - else: - self._window_funcs[k] = _WindowFunctions(self, *sa[k]) + for k in list(callable_dict.keys()): + self._window_funcs[k] = _WindowFunctions(self, *callable_dict[k]) else: raise TypeError( - f"`window_funcs` must be of type `int` if using a single slider or a dict if using multiple sliders. " - f"You have passed a {type(sa)}. See the docstring." + f"`window_funcs` must be either Nonetype or dict." + f"You have passed a {type(callable_dict)}. See the docstring." ) # force frame to update self.current_index = self.current_index def _process_indices( - self, array: np.ndarray, slice_indices: Dict[Union[int, str], int] + self, array: np.ndarray, slice_indices: dict[str, int] ) -> np.ndarray: """ Get the 2D array from the given slice indices. If not returning a 2D slice (such as due to window_funcs) @@ -680,11 +631,11 @@ def _process_indices( array: np.ndarray array-like to get a 2D slice from - slice_indices: Dict[int, int] - dict in form of {dimension_index: slice_index} + slice_indices: Dict[str, int] + dict in form of {dimension_index: current_index} For example if an array has shape [1000, 30, 512, 512] corresponding to [t, z, x, y]: To get the 100th timepoint and 3rd z-plane pass: - {"t": 100, "z": 3}, or {0: 100, 1: 3} + {"t": 100, "z": 3} Returns ------- @@ -692,26 +643,32 @@ def _process_indices( array-like, 2D slice """ - indexer = [slice(None)] * self.ndim + + data_ix = None + for i in range(len(self.data)): + if self.data[i] is array: + data_ix = i + break numerical_dims = list() + + # Totally number of dimensions for this specific array + curr_ndim = self.data[data_ix].ndim + + # Initialize slices for each dimension of array + indexer = [slice(None)] * curr_ndim + + # Maps from n_scrollable_dims to one of "", "t", "tz", etc. + curr_scrollable_format = SCROLLABLE_DIMS_ORDER[self.n_scrollable_dims[data_ix]] for dim in list(slice_indices.keys()): - if isinstance(dim, str): - data_ix = None - for i in range(len(self.data)): - if self.data[i] is array: - data_ix = i - break - if data_ix is None: - raise ValueError(f"Given `array` not found in `self.data`") - # get axes order for that specific array - numerical_dim = self.dims_order[data_ix].index(dim) - else: - numerical_dim = dim + if dim not in curr_scrollable_format: + continue + # get axes order for that specific array + numerical_dim = curr_scrollable_format.index(dim) indices_dim = slice_indices[dim] - # takes care of averaging if it was specified + # takes care of index selection (window slicing) for this specific axis indices_dim = self._get_window_indices(data_ix, numerical_dim, indices_dim) # set the indices for this dimension @@ -724,9 +681,9 @@ def _process_indices( if self.window_funcs is not None: a = array for i, dim in enumerate(sorted(numerical_dims)): - dim_str = self.dims_order[data_ix][dim] + dim_str = curr_scrollable_format[dim] dim = dim - i # since we loose a dimension every iteration - _indexer = [slice(None)] * (self.ndim - i) + _indexer = [slice(None)] * (curr_ndim - i) _indexer[dim] = indexer[dim + i] # if the indexer is an int, this dim has no window func @@ -737,7 +694,6 @@ def _process_indices( func = self.window_funcs[dim_str].func window = a[tuple(_indexer)] a = func(window, axis=dim) - # a = np.mean(a[tuple(_indexer)], axis=dim) return a else: return array[tuple(indexer)] @@ -749,7 +705,7 @@ def _get_window_indices(self, data_ix, dim, indices_dim): else: ix = indices_dim - dim_str = self.dims_order[data_ix][dim] + dim_str = SCROLLABLE_DIMS_ORDER[self.n_scrollable_dims[data_ix]][dim] # if no window stuff specified for this dim if dim_str not in self.window_funcs.keys(): @@ -785,7 +741,7 @@ def _process_frame_apply(self, array, data_ix) -> np.ndarray: return array - def _slider_value_changed(self, dimension: str, change: Union[dict, int]): + def _slider_value_changed(self, dimension: str, change: dict | int): if self.block_sliders: return if isinstance(change, dict): @@ -811,7 +767,7 @@ def reset_vmin_vmax_frame(self): TODO: We could think of applying the frame_apply funcs to a subsample of the entire array to get a better estimate of vmin vmax? """ - for subplot in self.gridplot: + for subplot in self.figure: if "histogram_lut" not in subplot.docks["right"]: continue @@ -821,7 +777,7 @@ def reset_vmin_vmax_frame(self): def set_data( self, - new_data: Union[np.ndarray, List[np.ndarray]], + new_data: np.ndarray | list[np.ndarray], reset_vmin_vmax: bool = True, reset_indices: bool = True, ): @@ -848,9 +804,11 @@ def set_data( self.sliders[key].value = 0 # set slider max according to new data - max_lengths = {"t": np.inf, "z": np.inf} + max_lengths = dict() + for scroll_dim in self.slider_dims: + max_lengths[scroll_dim] = np.inf - if isinstance(new_data, np.ndarray): + if _is_arraylike(new_data): new_data = [new_data] if len(self._data) != len(new_data): @@ -866,16 +824,24 @@ def set_data( f"does not equal current data ndim {current_array.ndim}" ) + # Computes the number of scrollable dims and also validates new_array + new_scrollable_dims = self._get_n_scrollable_dims(new_array, self._rgb[i]) + + if self.n_scrollable_dims[i] != new_scrollable_dims: + raise ValueError( + f"number of dimensions of data arrays must match number of dimensions of " + f"existing data arrays" + ) + # if checks pass, update with new data for i, (new_array, current_array, subplot) in enumerate( - zip(new_data, self._data, self.gridplot) + zip(new_data, self._data, self.figure) ): # check last two dims (x and y) to see if data shape is changing - old_data_shape = self._data[i].shape[-2:] + old_data_shape = self._data[i].shape[-self.n_img_dims[i] :] self._data[i] = new_array - if old_data_shape != new_array.shape[-2:]: - # make a new graphic with the new xy dims + if old_data_shape != new_array.shape[-self.n_img_dims[i] :]: frame = self._process_indices( new_array, slice_indices=self._current_index ) @@ -886,23 +852,31 @@ def set_data( # set hlut tool to use new graphic subplot.docks["right"]["histogram_lut"].image_graphic = new_graphic - # delete old graphic after setting hlut tool to new graphic # this ensures gc subplot.delete_graphic(graphic=subplot["image_widget_managed"]) subplot.insert_graphic(graphic=new_graphic) - if new_array.ndim > 2: - # to set max of time slider, txy or tzxy - max_lengths["t"] = min(max_lengths["t"], new_array.shape[0] - 1) - - if new_array.ndim > 3: # tzxy - max_lengths["z"] = min(max_lengths["z"], new_array.shape[1] - 1) + # Returns "", "t", or "tz" + curr_scrollable_format = SCROLLABLE_DIMS_ORDER[self.n_scrollable_dims[i]] + + for scroll_dim in self.slider_dims: + if scroll_dim in curr_scrollable_format: + new_length = new_array.shape[ + curr_scrollable_format.index(scroll_dim) + ] + if max_lengths[scroll_dim] == np.inf: + max_lengths[scroll_dim] = new_length + elif max_lengths[scroll_dim] != new_length: + raise ValueError( + f"New arrays have differing values along dim {scroll_dim}" + ) # set histogram widget - subplot.docks["right"]["histogram_lut"].set_data( - new_array, reset_vmin_vmax=reset_vmin_vmax - ) + if self._histogram_widget: + subplot.docks["right"]["histogram_lut"].set_data( + new_array, reset_vmin_vmax=reset_vmin_vmax + ) # set slider maxes # TODO: maybe make this stuff a property, like ndims, n_frames etc. and have it set the sliders @@ -924,21 +898,17 @@ def show( OutputContext ImageWidget just uses the Gridplot output context """ - if self.gridplot.canvas.__class__.__name__ == "JupyterWgpuCanvas": - from ..layouts._frame._ipywidget_toolbar import ( - IpywidgetImageWidgetToolbar, - ) # noqa - inline import + if self.figure.canvas.__class__.__name__ == "JupyterWgpuCanvas": + from ._image_widget_ipywidget_toolbar import IpywidgetImageWidgetToolbar self._image_widget_toolbar = IpywidgetImageWidgetToolbar(self) - elif self.gridplot.canvas.__class__.__name__ == "QWgpuCanvas": - from ..layouts._frame._qt_toolbar import ( - QToolbarImageWidget, - ) # noqa - inline import + elif self.figure.canvas.__class__.__name__ == "QWgpuCanvas": + from ._image_widget_qt_toolbar import QToolbarImageWidget self._image_widget_toolbar = QToolbarImageWidget(self) - self._output = self.gridplot.show( + self._output = self.figure.show( toolbar=toolbar, sidecar=sidecar, sidecar_kwargs=sidecar_kwargs, @@ -949,4 +919,4 @@ def show( def close(self): """Close Widget""" - self.gridplot.close() + self.figure.close() diff --git a/tests/test_figure.py b/tests/test_figure.py new file mode 100644 index 000000000..27b74c0b6 --- /dev/null +++ b/tests/test_figure.py @@ -0,0 +1,164 @@ +import numpy as np +import pytest + +import fastplotlib as fpl +import pygfx + + +def test_cameras_controller_properties(): + cameras = [ + ["2d", "3d", "3d"], + ["3d", "3d", "3d"] + ] + + controller_types = [ + ["panzoom", "panzoom", "fly"], + ["orbit", "trackball", "panzoom"] + ] + + fig = fpl.Figure( + shape=(2, 3), + cameras=cameras, + controller_types=controller_types, + canvas="offscreen" + ) + + print(fig.canvas) + + subplot_cameras = [subplot.camera for subplot in fig] + subplot_controllers = [subplot.controller for subplot in fig] + + for c1, c2 in zip(subplot_cameras, fig.cameras.ravel()): + assert c1 is c2 + + for c1, c2 in zip(subplot_controllers, fig.controllers.ravel()): + assert c1 is c2 + + for camera_type, subplot_camera in zip(np.asarray(cameras).ravel(), fig.cameras.ravel()): + if camera_type == "2d": + assert subplot_camera.fov == 0 + else: + assert subplot_camera.fov == 50 + + for controller_type, subplot_controller in zip(np.asarray(controller_types).ravel(), fig.controllers.ravel()): + match controller_type: + case "panzoom": + assert isinstance(subplot_controller, pygfx.PanZoomController) + case "fly": + assert isinstance(subplot_controller, pygfx.FlyController) + case "orbit": + assert isinstance(subplot_controller, pygfx.OrbitController) + case "trackball": + assert isinstance(subplot_controller, pygfx.TrackballController) + + # check changing cameras + fig[0, 0].camera = "3d" + assert fig[0, 0].camera.fov == 50 + fig[1, 0].camera = "2d" + assert fig[1, 0].camera.fov == 0 + + # test changing controller + fig[1, 1].controller = "fly" + assert isinstance(fig[1, 1].controller, pygfx.FlyController) + assert fig[1, 1].controller is fig.controllers[1, 1] + fig[0, 2].controller = "panzoom" + assert isinstance(fig[0, 2].controller, pygfx.PanZoomController) + assert fig[0, 2].controller is fig.controllers[0, 2] + + +def test_controller_ids_int(): + ids = [ + [0, 1, 1], + [0, 2, 3], + [4, 1, 2] + ] + + fig = fpl.Figure(shape=(3, 3), controller_ids=ids, canvas="offscreen") + + assert fig[0, 0].controller is fig[1, 0].controller + assert fig[0, 1].controller is fig[0, 2].controller is fig[2, 1].controller + assert fig[1, 1].controller is fig[2, 2].controller + + +def test_controller_ids_int_change_controllers(): + ids = [ + [0, 1, 1], + [0, 2, 3], + [4, 1, 2] + ] + + cameras = [ + ["2d", "3d", "3d"], + ["2d", "3d", "2d"], + ["3d", "3d", "3d"] + ] + + fig = fpl.Figure(shape=(3, 3), cameras=cameras, controller_ids=ids, canvas="offscreen") + + assert isinstance(fig[0, 1].controller, pygfx.FlyController) + + # changing controller when id matches should change the others too + fig[0, 1].controller = "panzoom" + assert isinstance(fig[0, 1].controller, pygfx.PanZoomController) + assert fig[0, 1].controller is fig[0, 2].controller is fig[2, 1].controller + assert set(fig[0, 1].controller.cameras) == {fig[0, 1].camera, fig[0, 2].camera, fig[2, 1].camera} + + # change to orbit + fig[0, 1].controller = "orbit" + assert isinstance(fig[0, 1].controller, pygfx.OrbitController) + assert fig[0, 1].controller is fig[0, 2].controller is fig[2, 1].controller + assert set(fig[0, 1].controller.cameras) == {fig[0, 1].camera, fig[0, 2].camera, fig[2, 1].camera} + + +def test_controller_ids_str(): + names = [ + ["a", "b", "c"], + ["d", "e", "f"] + ] + + controller_ids = [ + ["a", "f"], + ["b", "d", "e"] + ] + + fig = fpl.Figure(shape=(2, 3), controller_ids=controller_ids, names=names, canvas="offscreen") + + assert fig[0, 0].controller is fig[1, 2].controller is fig["a"].controller is fig["f"].controller + assert fig[0, 1].controller is fig[1, 0].controller is fig[1, 1].controller is fig["b"].controller is fig["d"].controller is fig["e"].controller + + # make sure subplot c is unique + exclude_c = [fig[n].controller for n in ["a", "b", "d", "e", "f"]] + assert fig["c"] not in exclude_c + + +def test_set_controllers_from_existing_controllers(): + fig = fpl.Figure(shape=(3, 3), canvas="offscreen") + fig2 = fpl.Figure(shape=fig.shape, controllers=fig.controllers, canvas="offscreen") + + assert fig.controllers[:-1].size == 6 + with pytest.raises(ValueError): + fig3 = fpl.Figure(shape=fig.shape, controllers=fig.controllers[:-1], canvas="offscreen") + + for fig1_subplot, fig2_subplot in zip(fig, fig2): + assert fig1_subplot.controller is fig2_subplot.controller + + cameras = [ + [pygfx.PerspectiveCamera(), "3d"], + ["3d", "2d"] + ] + + controllers = [ + [pygfx.FlyController(cameras[0][0]), pygfx.TrackballController()], + [pygfx.OrbitController(), pygfx.PanZoomController()] + ] + + fig = fpl.Figure(shape=(2, 2), cameras=cameras, controllers=controllers, canvas="offscreen") + + assert fig[0, 0].controller is controllers[0][0] + assert fig[0, 1].controller is controllers[0][1] + assert fig[1, 0].controller is controllers[1][0] + assert fig[1, 1].controller is controllers[1][1] + + assert fig[0, 0].camera is cameras[0][0] + + assert fig[0, 1].camera.fov == 50 diff --git a/tests/test_gridplot.py b/tests/test_gridplot.py deleted file mode 100644 index 3814664d7..000000000 --- a/tests/test_gridplot.py +++ /dev/null @@ -1,164 +0,0 @@ -import numpy as np -import pytest - -import fastplotlib as fpl -import pygfx - - -def test_cameras_controller_properties(): - cameras = [ - ["2d", "3d", "3d"], - ["3d", "3d", "3d"] - ] - - controller_types = [ - ["panzoom", "panzoom", "fly"], - ["orbit", "trackball", "panzoom"] - ] - - gp = fpl.GridPlot( - shape=(2, 3), - cameras=cameras, - controller_types=controller_types, - canvas="offscreen" - ) - - print(gp.canvas) - - subplot_cameras = [subplot.camera for subplot in gp] - subplot_controllers = [subplot.controller for subplot in gp] - - for c1, c2 in zip(subplot_cameras, gp.cameras.ravel()): - assert c1 is c2 - - for c1, c2 in zip(subplot_controllers, gp.controllers.ravel()): - assert c1 is c2 - - for camera_type, subplot_camera in zip(np.asarray(cameras).ravel(), gp.cameras.ravel()): - if camera_type == "2d": - assert subplot_camera.fov == 0 - else: - assert subplot_camera.fov == 50 - - for controller_type, subplot_controller in zip(np.asarray(controller_types).ravel(), gp.controllers.ravel()): - match controller_type: - case "panzoom": - assert isinstance(subplot_controller, pygfx.PanZoomController) - case "fly": - assert isinstance(subplot_controller, pygfx.FlyController) - case "orbit": - assert isinstance(subplot_controller, pygfx.OrbitController) - case "trackball": - assert isinstance(subplot_controller, pygfx.TrackballController) - - # check changing cameras - gp[0, 0].camera = "3d" - assert gp[0, 0].camera.fov == 50 - gp[1, 0].camera = "2d" - assert gp[1, 0].camera.fov == 0 - - # test changing controller - gp[1, 1].controller = "fly" - assert isinstance(gp[1, 1].controller, pygfx.FlyController) - assert gp[1, 1].controller is gp.controllers[1, 1] - gp[0, 2].controller = "panzoom" - assert isinstance(gp[0, 2].controller, pygfx.PanZoomController) - assert gp[0, 2].controller is gp.controllers[0, 2] - - -def test_gridplot_controller_ids_int(): - ids = [ - [0, 1, 1], - [0, 2, 3], - [4, 1, 2] - ] - - gp = fpl.GridPlot(shape=(3, 3), controller_ids=ids, canvas="offscreen") - - assert gp[0, 0].controller is gp[1, 0].controller - assert gp[0, 1].controller is gp[0, 2].controller is gp[2, 1].controller - assert gp[1, 1].controller is gp[2, 2].controller - - -def test_gridplot_controller_ids_int_change_controllers(): - ids = [ - [0, 1, 1], - [0, 2, 3], - [4, 1, 2] - ] - - cameras = [ - ["2d", "3d", "3d"], - ["2d", "3d", "2d"], - ["3d", "3d", "3d"] - ] - - gp = fpl.GridPlot(shape=(3, 3), cameras=cameras, controller_ids=ids, canvas="offscreen") - - assert isinstance(gp[0, 1].controller, pygfx.FlyController) - - # changing controller when id matches should change the others too - gp[0, 1].controller = "panzoom" - assert isinstance(gp[0, 1].controller, pygfx.PanZoomController) - assert gp[0, 1].controller is gp[0, 2].controller is gp[2, 1].controller - assert set(gp[0, 1].controller.cameras) == {gp[0, 1].camera, gp[0, 2].camera, gp[2, 1].camera} - - # change to orbit - gp[0, 1].controller = "orbit" - assert isinstance(gp[0, 1].controller, pygfx.OrbitController) - assert gp[0, 1].controller is gp[0, 2].controller is gp[2, 1].controller - assert set(gp[0, 1].controller.cameras) == {gp[0, 1].camera, gp[0, 2].camera, gp[2, 1].camera} - - -def test_gridplot_controller_ids_str(): - names = [ - ["a", "b", "c"], - ["d", "e", "f"] - ] - - controller_ids = [ - ["a", "f"], - ["b", "d", "e"] - ] - - gp = fpl.GridPlot(shape=(2, 3), controller_ids=controller_ids, names=names, canvas="offscreen") - - assert gp[0, 0].controller is gp[1, 2].controller is gp["a"].controller is gp["f"].controller - assert gp[0, 1].controller is gp[1, 0].controller is gp[1, 1].controller is gp["b"].controller is gp["d"].controller is gp["e"].controller - - # make sure subplot c is unique - exclude_c = [gp[n].controller for n in ["a", "b", "d", "e", "f"]] - assert gp["c"] not in exclude_c - - -def test_set_gridplot_controllers_from_existing_controllers(): - gp = fpl.GridPlot(shape=(3, 3), canvas="offscreen") - gp2 = fpl.GridPlot(shape=gp.shape, controllers=gp.controllers, canvas="offscreen") - - assert gp.controllers[:-1].size == 6 - with pytest.raises(ValueError): - gp3 = fpl.GridPlot(shape=gp.shape, controllers=gp.controllers[:-1], canvas="offscreen") - - for sp_gp, sp_gp2 in zip(gp, gp2): - assert sp_gp.controller is sp_gp2.controller - - cameras = [ - [pygfx.PerspectiveCamera(), "3d"], - ["3d", "2d"] - ] - - controllers = [ - [pygfx.FlyController(cameras[0][0]), pygfx.TrackballController()], - [pygfx.OrbitController(), pygfx.PanZoomController()] - ] - - gp = fpl.GridPlot(shape=(2, 2), cameras=cameras, controllers=controllers, canvas="offscreen") - - assert gp[0, 0].controller is controllers[0][0] - assert gp[0, 1].controller is controllers[0][1] - assert gp[1, 0].controller is controllers[1][0] - assert gp[1, 1].controller is controllers[1][1] - - assert gp[0, 0].camera is cameras[0][0] - - assert gp[0, 1].camera.fov == 50