diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0274add7d..528b62772 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -52,7 +52,7 @@ jobs: run: | python -m pip install --upgrade pip setuptools # remove pygfx from install_requires, we install using pygfx@main - sed -i "/pygfx/d" ./setup.py + sed -i "/pygfx/d" ./pyproject.toml pip install git+https://github.com/pygfx/pygfx.git@main - name: Install fastplotlib run: | diff --git a/.github/workflows/docs-deploy.yml b/.github/workflows/docs-deploy.yml index a0cb54357..470e2e5a5 100644 --- a/.github/workflows/docs-deploy.yml +++ b/.github/workflows/docs-deploy.yml @@ -40,7 +40,7 @@ jobs: run: | python -m pip install --upgrade pip setuptools # remove pygfx from install_requires, we install using pygfx@main - sed -i "/pygfx/d" ./setup.py + sed -i "/pygfx/d" ./pyproject.toml pip install git+https://github.com/pygfx/pygfx.git@main pip install -e ".[docs,notebook,imgui]" - name: Show wgpu backend @@ -68,7 +68,7 @@ jobs: if: ${{ github.ref == 'refs/heads/main' }} # any push to main goes to fastplotlib.org/ver/dev run: echo "DOCS_VERSION_DIR=dev" >> "$GITHUB_ENV" - + # upload docs via SCP - name: Deploy docs uses: appleboy/scp-action@v0.1.7 @@ -90,7 +90,7 @@ jobs: with: message: | 📚 Docs preview built and uploaded! https://www.fastplotlib.org/ver/${{ env.DOCS_VERSION_DIR }} - + # upload docs via SCP - name: Deploy docs release if: ${{ github.ref_type == 'tag' }} diff --git a/.github/workflows/screenshots.yml b/.github/workflows/screenshots.yml index 0985fc179..cfaf419b8 100644 --- a/.github/workflows/screenshots.yml +++ b/.github/workflows/screenshots.yml @@ -36,7 +36,7 @@ jobs: run: | python -m pip install --upgrade pip setuptools # remove pygfx from install_requires, we install using pygfx@main - sed -i "/pygfx/d" ./setup.py + sed -i "/pygfx/d" ./pyproject.toml pip install git+https://github.com/pygfx/pygfx.git@main - name: Install fastplotlib run: | diff --git a/MANIFEST.in b/MANIFEST.in deleted file mode 100644 index b8debd28d..000000000 --- a/MANIFEST.in +++ /dev/null @@ -1,4 +0,0 @@ -recursive-include fastplotlib/utils/colormaps/ * -include fastplotlib/VERSION -recursive-include fastplotlib/assets/ * - diff --git a/docs/source/_static/guide_ipywidgets.webp b/docs/source/_static/guide_ipywidgets.webp new file mode 100644 index 000000000..9a7963381 Binary files /dev/null and b/docs/source/_static/guide_ipywidgets.webp differ diff --git a/docs/source/_static/switcher.json b/docs/source/_static/switcher.json index 67f723e2f..9f792b252 100644 --- a/docs/source/_static/switcher.json +++ b/docs/source/_static/switcher.json @@ -1,7 +1,22 @@ [ + { + "name": "release", + "version": "v0.4.0", + "url": "http://www.fastplotlib.org/" + }, { "name": "dev/main", "version": "dev", - "url": "http://www.fastplotlib.org/versions/dev" + "url": "http://www.fastplotlib.org/ver/dev" + }, + { + "name": "v0.3.0", + "version": "v0.3.0", + "url": "http://www.fastplotlib.org/ver/0.3.0" + }, + { + "name": "v0.4.0", + "version": "v0.4.0", + "url": "http://www.fastplotlib.org/ver/0.4.0" } ] diff --git a/docs/source/api/graphic_features/Deleted.rst b/docs/source/api/graphic_features/Deleted.rst index 09131c4a7..ffc704917 100644 --- a/docs/source/api/graphic_features/Deleted.rst +++ b/docs/source/api/graphic_features/Deleted.rst @@ -6,7 +6,7 @@ Deleted ======= Deleted ======= -.. currentmodule:: fastplotlib.graphics._features +.. currentmodule:: fastplotlib.graphics.features Constructor ~~~~~~~~~~~ diff --git a/docs/source/api/graphic_features/FontSize.rst b/docs/source/api/graphic_features/FontSize.rst index 4b8df9826..5e34c6038 100644 --- a/docs/source/api/graphic_features/FontSize.rst +++ b/docs/source/api/graphic_features/FontSize.rst @@ -6,7 +6,7 @@ FontSize ======== FontSize ======== -.. currentmodule:: fastplotlib.graphics._features +.. currentmodule:: fastplotlib.graphics.features Constructor ~~~~~~~~~~~ diff --git a/docs/source/api/graphic_features/GraphicFeatureEvent.rst b/docs/source/api/graphic_features/GraphicFeatureEvent.rst new file mode 100644 index 000000000..233462052 --- /dev/null +++ b/docs/source/api/graphic_features/GraphicFeatureEvent.rst @@ -0,0 +1,38 @@ +.. _api.GraphicFeatureEvent: + +GraphicFeatureEvent +******************* + +=================== +GraphicFeatureEvent +=================== +.. currentmodule:: fastplotlib.graphics.features + +Constructor +~~~~~~~~~~~ +.. autosummary:: + :toctree: GraphicFeatureEvent_api + + GraphicFeatureEvent + +Properties +~~~~~~~~~~ +.. autosummary:: + :toctree: GraphicFeatureEvent_api + + GraphicFeatureEvent.bubbles + GraphicFeatureEvent.cancelled + GraphicFeatureEvent.current_target + GraphicFeatureEvent.root + GraphicFeatureEvent.target + GraphicFeatureEvent.time_stamp + GraphicFeatureEvent.type + +Methods +~~~~~~~ +.. autosummary:: + :toctree: GraphicFeatureEvent_api + + GraphicFeatureEvent.cancel + GraphicFeatureEvent.stop_propagation + diff --git a/docs/source/api/graphic_features/ImageCmap.rst b/docs/source/api/graphic_features/ImageCmap.rst index 23d16a4a2..2c23a3406 100644 --- a/docs/source/api/graphic_features/ImageCmap.rst +++ b/docs/source/api/graphic_features/ImageCmap.rst @@ -6,7 +6,7 @@ ImageCmap ========= ImageCmap ========= -.. currentmodule:: fastplotlib.graphics._features +.. currentmodule:: fastplotlib.graphics.features Constructor ~~~~~~~~~~~ diff --git a/docs/source/api/graphic_features/ImageCmapInterpolation.rst b/docs/source/api/graphic_features/ImageCmapInterpolation.rst index 7e04ec788..0577f2d70 100644 --- a/docs/source/api/graphic_features/ImageCmapInterpolation.rst +++ b/docs/source/api/graphic_features/ImageCmapInterpolation.rst @@ -6,7 +6,7 @@ ImageCmapInterpolation ====================== ImageCmapInterpolation ====================== -.. currentmodule:: fastplotlib.graphics._features +.. currentmodule:: fastplotlib.graphics.features Constructor ~~~~~~~~~~~ diff --git a/docs/source/api/graphic_features/ImageInterpolation.rst b/docs/source/api/graphic_features/ImageInterpolation.rst index 866e76333..ebf69c279 100644 --- a/docs/source/api/graphic_features/ImageInterpolation.rst +++ b/docs/source/api/graphic_features/ImageInterpolation.rst @@ -6,7 +6,7 @@ ImageInterpolation ================== ImageInterpolation ================== -.. currentmodule:: fastplotlib.graphics._features +.. currentmodule:: fastplotlib.graphics.features Constructor ~~~~~~~~~~~ diff --git a/docs/source/api/graphic_features/ImageVmax.rst b/docs/source/api/graphic_features/ImageVmax.rst index b7dfe7e2d..aa8d6526a 100644 --- a/docs/source/api/graphic_features/ImageVmax.rst +++ b/docs/source/api/graphic_features/ImageVmax.rst @@ -6,7 +6,7 @@ ImageVmax ========= ImageVmax ========= -.. currentmodule:: fastplotlib.graphics._features +.. currentmodule:: fastplotlib.graphics.features Constructor ~~~~~~~~~~~ diff --git a/docs/source/api/graphic_features/ImageVmin.rst b/docs/source/api/graphic_features/ImageVmin.rst index 0d4634894..361cc5838 100644 --- a/docs/source/api/graphic_features/ImageVmin.rst +++ b/docs/source/api/graphic_features/ImageVmin.rst @@ -6,7 +6,7 @@ ImageVmin ========= ImageVmin ========= -.. currentmodule:: fastplotlib.graphics._features +.. currentmodule:: fastplotlib.graphics.features Constructor ~~~~~~~~~~~ diff --git a/docs/source/api/graphic_features/LinearRegionSelectionFeature.rst b/docs/source/api/graphic_features/LinearRegionSelectionFeature.rst index b8958c86b..9f06f2682 100644 --- a/docs/source/api/graphic_features/LinearRegionSelectionFeature.rst +++ b/docs/source/api/graphic_features/LinearRegionSelectionFeature.rst @@ -6,7 +6,7 @@ LinearRegionSelectionFeature ============================ LinearRegionSelectionFeature ============================ -.. currentmodule:: fastplotlib.graphics._features +.. currentmodule:: fastplotlib.graphics.features Constructor ~~~~~~~~~~~ diff --git a/docs/source/api/graphic_features/LinearSelectionFeature.rst b/docs/source/api/graphic_features/LinearSelectionFeature.rst index ad7b8645a..b9e71cd7b 100644 --- a/docs/source/api/graphic_features/LinearSelectionFeature.rst +++ b/docs/source/api/graphic_features/LinearSelectionFeature.rst @@ -6,7 +6,7 @@ LinearSelectionFeature ====================== LinearSelectionFeature ====================== -.. currentmodule:: fastplotlib.graphics._features +.. currentmodule:: fastplotlib.graphics.features Constructor ~~~~~~~~~~~ diff --git a/docs/source/api/graphic_features/Name.rst b/docs/source/api/graphic_features/Name.rst index 288fcfc22..f5a5235d8 100644 --- a/docs/source/api/graphic_features/Name.rst +++ b/docs/source/api/graphic_features/Name.rst @@ -6,7 +6,7 @@ Name ==== Name ==== -.. currentmodule:: fastplotlib.graphics._features +.. currentmodule:: fastplotlib.graphics.features Constructor ~~~~~~~~~~~ diff --git a/docs/source/api/graphic_features/Offset.rst b/docs/source/api/graphic_features/Offset.rst index 683aaf763..fdb2af66a 100644 --- a/docs/source/api/graphic_features/Offset.rst +++ b/docs/source/api/graphic_features/Offset.rst @@ -6,7 +6,7 @@ Offset ====== Offset ====== -.. currentmodule:: fastplotlib.graphics._features +.. currentmodule:: fastplotlib.graphics.features Constructor ~~~~~~~~~~~ diff --git a/docs/source/api/graphic_features/PointsSizesFeature.rst b/docs/source/api/graphic_features/PointsSizesFeature.rst index 3dcc4eeb2..f3f78b74b 100644 --- a/docs/source/api/graphic_features/PointsSizesFeature.rst +++ b/docs/source/api/graphic_features/PointsSizesFeature.rst @@ -6,7 +6,7 @@ PointsSizesFeature ================== PointsSizesFeature ================== -.. currentmodule:: fastplotlib.graphics._features +.. currentmodule:: fastplotlib.graphics.features Constructor ~~~~~~~~~~~ diff --git a/docs/source/api/graphic_features/RectangleSelectionFeature.rst b/docs/source/api/graphic_features/RectangleSelectionFeature.rst index d35752a24..cdfd1ad3f 100644 --- a/docs/source/api/graphic_features/RectangleSelectionFeature.rst +++ b/docs/source/api/graphic_features/RectangleSelectionFeature.rst @@ -6,7 +6,7 @@ RectangleSelectionFeature ========================= RectangleSelectionFeature ========================= -.. currentmodule:: fastplotlib.graphics._features +.. currentmodule:: fastplotlib.graphics.features Constructor ~~~~~~~~~~~ diff --git a/docs/source/api/graphic_features/Rotation.rst b/docs/source/api/graphic_features/Rotation.rst index f8963b0fd..b7729c7a4 100644 --- a/docs/source/api/graphic_features/Rotation.rst +++ b/docs/source/api/graphic_features/Rotation.rst @@ -6,7 +6,7 @@ Rotation ======== Rotation ======== -.. currentmodule:: fastplotlib.graphics._features +.. currentmodule:: fastplotlib.graphics.features Constructor ~~~~~~~~~~~ diff --git a/docs/source/api/graphic_features/SizeSpace.rst b/docs/source/api/graphic_features/SizeSpace.rst index 0bca1ecc8..e7c8e30be 100644 --- a/docs/source/api/graphic_features/SizeSpace.rst +++ b/docs/source/api/graphic_features/SizeSpace.rst @@ -6,7 +6,7 @@ SizeSpace ========= SizeSpace ========= -.. currentmodule:: fastplotlib.graphics._features +.. currentmodule:: fastplotlib.graphics.features Constructor ~~~~~~~~~~~ diff --git a/docs/source/api/graphic_features/TextData.rst b/docs/source/api/graphic_features/TextData.rst index 1c27b6e48..bf08b08d6 100644 --- a/docs/source/api/graphic_features/TextData.rst +++ b/docs/source/api/graphic_features/TextData.rst @@ -6,7 +6,7 @@ TextData ======== TextData ======== -.. currentmodule:: fastplotlib.graphics._features +.. currentmodule:: fastplotlib.graphics.features Constructor ~~~~~~~~~~~ diff --git a/docs/source/api/graphic_features/TextFaceColor.rst b/docs/source/api/graphic_features/TextFaceColor.rst index 5dae54192..5ab01b04b 100644 --- a/docs/source/api/graphic_features/TextFaceColor.rst +++ b/docs/source/api/graphic_features/TextFaceColor.rst @@ -6,7 +6,7 @@ TextFaceColor ============= TextFaceColor ============= -.. currentmodule:: fastplotlib.graphics._features +.. currentmodule:: fastplotlib.graphics.features Constructor ~~~~~~~~~~~ diff --git a/docs/source/api/graphic_features/TextOutlineColor.rst b/docs/source/api/graphic_features/TextOutlineColor.rst index f7831b0df..571261625 100644 --- a/docs/source/api/graphic_features/TextOutlineColor.rst +++ b/docs/source/api/graphic_features/TextOutlineColor.rst @@ -6,7 +6,7 @@ TextOutlineColor ================ TextOutlineColor ================ -.. currentmodule:: fastplotlib.graphics._features +.. currentmodule:: fastplotlib.graphics.features Constructor ~~~~~~~~~~~ diff --git a/docs/source/api/graphic_features/TextOutlineThickness.rst b/docs/source/api/graphic_features/TextOutlineThickness.rst index 75d485781..450ae54c9 100644 --- a/docs/source/api/graphic_features/TextOutlineThickness.rst +++ b/docs/source/api/graphic_features/TextOutlineThickness.rst @@ -6,7 +6,7 @@ TextOutlineThickness ==================== TextOutlineThickness ==================== -.. currentmodule:: fastplotlib.graphics._features +.. currentmodule:: fastplotlib.graphics.features Constructor ~~~~~~~~~~~ diff --git a/docs/source/api/graphic_features/TextureArray.rst b/docs/source/api/graphic_features/TextureArray.rst index 79707c453..73facc5bf 100644 --- a/docs/source/api/graphic_features/TextureArray.rst +++ b/docs/source/api/graphic_features/TextureArray.rst @@ -6,7 +6,7 @@ TextureArray ============ TextureArray ============ -.. currentmodule:: fastplotlib.graphics._features +.. currentmodule:: fastplotlib.graphics.features Constructor ~~~~~~~~~~~ diff --git a/docs/source/api/graphic_features/Thickness.rst b/docs/source/api/graphic_features/Thickness.rst index 061f96fe8..dc4c5888f 100644 --- a/docs/source/api/graphic_features/Thickness.rst +++ b/docs/source/api/graphic_features/Thickness.rst @@ -6,7 +6,7 @@ Thickness ========= Thickness ========= -.. currentmodule:: fastplotlib.graphics._features +.. currentmodule:: fastplotlib.graphics.features Constructor ~~~~~~~~~~~ diff --git a/docs/source/api/graphic_features/UniformColor.rst b/docs/source/api/graphic_features/UniformColor.rst index 7370589b7..8e9d56eae 100644 --- a/docs/source/api/graphic_features/UniformColor.rst +++ b/docs/source/api/graphic_features/UniformColor.rst @@ -6,7 +6,7 @@ UniformColor ============ UniformColor ============ -.. currentmodule:: fastplotlib.graphics._features +.. currentmodule:: fastplotlib.graphics.features Constructor ~~~~~~~~~~~ diff --git a/docs/source/api/graphic_features/UniformSize.rst b/docs/source/api/graphic_features/UniformSize.rst index e342d6a70..e4727dcb9 100644 --- a/docs/source/api/graphic_features/UniformSize.rst +++ b/docs/source/api/graphic_features/UniformSize.rst @@ -6,7 +6,7 @@ UniformSize =========== UniformSize =========== -.. currentmodule:: fastplotlib.graphics._features +.. currentmodule:: fastplotlib.graphics.features Constructor ~~~~~~~~~~~ diff --git a/docs/source/api/graphic_features/VertexCmap.rst b/docs/source/api/graphic_features/VertexCmap.rst index a3311d6e6..77d96aaf6 100644 --- a/docs/source/api/graphic_features/VertexCmap.rst +++ b/docs/source/api/graphic_features/VertexCmap.rst @@ -6,7 +6,7 @@ VertexCmap ========== VertexCmap ========== -.. currentmodule:: fastplotlib.graphics._features +.. currentmodule:: fastplotlib.graphics.features Constructor ~~~~~~~~~~~ diff --git a/docs/source/api/graphic_features/VertexColors.rst b/docs/source/api/graphic_features/VertexColors.rst index 3c2089a78..d09da7a18 100644 --- a/docs/source/api/graphic_features/VertexColors.rst +++ b/docs/source/api/graphic_features/VertexColors.rst @@ -6,7 +6,7 @@ VertexColors ============ VertexColors ============ -.. currentmodule:: fastplotlib.graphics._features +.. currentmodule:: fastplotlib.graphics.features Constructor ~~~~~~~~~~~ diff --git a/docs/source/api/graphic_features/VertexPositions.rst b/docs/source/api/graphic_features/VertexPositions.rst index 9669ab6d5..d181f07b9 100644 --- a/docs/source/api/graphic_features/VertexPositions.rst +++ b/docs/source/api/graphic_features/VertexPositions.rst @@ -6,7 +6,7 @@ VertexPositions =============== VertexPositions =============== -.. currentmodule:: fastplotlib.graphics._features +.. currentmodule:: fastplotlib.graphics.features Constructor ~~~~~~~~~~~ diff --git a/docs/source/api/graphic_features/Visible.rst b/docs/source/api/graphic_features/Visible.rst index 957b4433a..06bfd2278 100644 --- a/docs/source/api/graphic_features/Visible.rst +++ b/docs/source/api/graphic_features/Visible.rst @@ -6,7 +6,7 @@ Visible ======= Visible ======= -.. currentmodule:: fastplotlib.graphics._features +.. currentmodule:: fastplotlib.graphics.features Constructor ~~~~~~~~~~~ diff --git a/docs/source/api/graphic_features/index.rst b/docs/source/api/graphic_features/index.rst index dc88e97d6..90a58fe8e 100644 --- a/docs/source/api/graphic_features/index.rst +++ b/docs/source/api/graphic_features/index.rst @@ -31,3 +31,4 @@ Graphic Features Rotation Visible Deleted + GraphicFeatureEvent diff --git a/docs/source/api/graphics/Graphic.rst b/docs/source/api/graphics/Graphic.rst new file mode 100644 index 000000000..08ab0404b --- /dev/null +++ b/docs/source/api/graphics/Graphic.rst @@ -0,0 +1,47 @@ +.. _api.Graphic: + +Graphic +******* + +======= +Graphic +======= +.. currentmodule:: fastplotlib + +Constructor +~~~~~~~~~~~ +.. autosummary:: + :toctree: Graphic_api + + Graphic + +Properties +~~~~~~~~~~ +.. autosummary:: + :toctree: Graphic_api + + Graphic.axes + Graphic.block_events + Graphic.deleted + Graphic.event_handlers + Graphic.name + Graphic.offset + Graphic.right_click_menu + Graphic.rotation + Graphic.supported_events + Graphic.visible + Graphic.world_object + +Methods +~~~~~~~ +.. autosummary:: + :toctree: Graphic_api + + Graphic.add_axes + Graphic.add_event_handler + Graphic.clear_event_handlers + Graphic.remove_event_handler + Graphic.rotate + Graphic.share_property + Graphic.unshare_property + diff --git a/docs/source/api/graphics/index.rst b/docs/source/api/graphics/index.rst index b64ac53c0..491013dff 100644 --- a/docs/source/api/graphics/index.rst +++ b/docs/source/api/graphics/index.rst @@ -4,9 +4,10 @@ Graphics .. toctree:: :maxdepth: 1 + Graphic LineGraphic - ImageGraphic ScatterGraphic + ImageGraphic TextGraphic LineCollection LineStack diff --git a/docs/source/api/index.rst b/docs/source/api/index.rst index 87c134782..3a1184e6c 100644 --- a/docs/source/api/index.rst +++ b/docs/source/api/index.rst @@ -9,6 +9,7 @@ API Reference graphics/index graphic_features/index selectors/index + tools/index ui/index widgets/index fastplotlib diff --git a/docs/source/api/layouts/figure.rst b/docs/source/api/layouts/figure.rst index b5cbbd2bb..d191fe8ce 100644 --- a/docs/source/api/layouts/figure.rst +++ b/docs/source/api/layouts/figure.rst @@ -27,6 +27,8 @@ Properties Figure.names Figure.renderer Figure.shape + Figure.show_tooltips + Figure.tooltip_manager Methods ~~~~~~~ diff --git a/docs/source/api/layouts/imgui_figure.rst b/docs/source/api/layouts/imgui_figure.rst index a338afe96..0abfcc067 100644 --- a/docs/source/api/layouts/imgui_figure.rst +++ b/docs/source/api/layouts/imgui_figure.rst @@ -29,6 +29,8 @@ Properties ImguiFigure.names ImguiFigure.renderer ImguiFigure.shape + ImguiFigure.show_tooltips + ImguiFigure.tooltip_manager Methods ~~~~~~~ diff --git a/docs/source/api/tools/HistogramLUTTool.rst b/docs/source/api/tools/HistogramLUTTool.rst new file mode 100644 index 000000000..d134eb1ce --- /dev/null +++ b/docs/source/api/tools/HistogramLUTTool.rst @@ -0,0 +1,53 @@ +.. _api.HistogramLUTTool: + +HistogramLUTTool +**************** + +================ +HistogramLUTTool +================ +.. currentmodule:: fastplotlib + +Constructor +~~~~~~~~~~~ +.. autosummary:: + :toctree: HistogramLUTTool_api + + HistogramLUTTool + +Properties +~~~~~~~~~~ +.. autosummary:: + :toctree: HistogramLUTTool_api + + HistogramLUTTool.axes + HistogramLUTTool.block_events + HistogramLUTTool.cmap + HistogramLUTTool.deleted + HistogramLUTTool.event_handlers + HistogramLUTTool.image_graphic + HistogramLUTTool.name + HistogramLUTTool.offset + HistogramLUTTool.right_click_menu + HistogramLUTTool.rotation + HistogramLUTTool.supported_events + HistogramLUTTool.visible + HistogramLUTTool.vmax + HistogramLUTTool.vmin + HistogramLUTTool.world_object + +Methods +~~~~~~~ +.. autosummary:: + :toctree: HistogramLUTTool_api + + HistogramLUTTool.add_axes + HistogramLUTTool.add_event_handler + HistogramLUTTool.clear_event_handlers + HistogramLUTTool.disconnect_image_graphic + HistogramLUTTool.remove_event_handler + HistogramLUTTool.rotate + HistogramLUTTool.set_data + HistogramLUTTool.share_property + HistogramLUTTool.unshare_property + diff --git a/docs/source/api/tools/Tooltip.rst b/docs/source/api/tools/Tooltip.rst new file mode 100644 index 000000000..71607bf20 --- /dev/null +++ b/docs/source/api/tools/Tooltip.rst @@ -0,0 +1,38 @@ +.. _api.Tooltip: + +Tooltip +******* + +======= +Tooltip +======= +.. currentmodule:: fastplotlib + +Constructor +~~~~~~~~~~~ +.. autosummary:: + :toctree: Tooltip_api + + Tooltip + +Properties +~~~~~~~~~~ +.. autosummary:: + :toctree: Tooltip_api + + Tooltip.background_color + Tooltip.font_size + Tooltip.outline_color + Tooltip.padding + Tooltip.text_color + Tooltip.world_object + +Methods +~~~~~~~ +.. autosummary:: + :toctree: Tooltip_api + + Tooltip.register + Tooltip.unregister + Tooltip.unregister_all + diff --git a/docs/source/api/tools/index.rst b/docs/source/api/tools/index.rst new file mode 100644 index 000000000..c2666ed28 --- /dev/null +++ b/docs/source/api/tools/index.rst @@ -0,0 +1,8 @@ +Tools +***** + +.. toctree:: + :maxdepth: 1 + + HistogramLUTTool + Tooltip diff --git a/docs/source/api/utils.rst b/docs/source/api/utils.rst index 6222e22c6..be7b1a049 100644 --- a/docs/source/api/utils.rst +++ b/docs/source/api/utils.rst @@ -4,3 +4,7 @@ fastplotlib.utils .. currentmodule:: fastplotlib.utils .. automodule:: fastplotlib.utils.functions :members: + +.. currentmodule:: fastplotlib.utils +.. automodule:: fastplotlib.utils._plot_helpers + :members: diff --git a/docs/source/conf.py b/docs/source/conf.py index 865c462a6..8d17c97ae 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -60,12 +60,16 @@ "../../examples/image_widget", "../../examples/gridplot", "../../examples/window_layouts", + "../../examples/controllers", "../../examples/line", "../../examples/line_collection", "../../examples/scatter", + "../../examples/text", + "../../examples/events", "../../examples/selection_tools", "../../examples/machine_learning", "../../examples/guis", + "../../examples/ipywidgets", "../../examples/misc", "../../examples/qt", ] diff --git a/docs/source/developer_notes/layouts.rst b/docs/source/developer_notes/layouts.rst index 4aacd38da..daf197c44 100644 --- a/docs/source/developer_notes/layouts.rst +++ b/docs/source/developer_notes/layouts.rst @@ -4,8 +4,8 @@ Layouts PlotArea -------- -This is the main base class within layouts. A ``Figure`` and ``Dock`` are areas within a ``Subplot`` that -inherit from ``PlotArea``. +This is the main base class within layouts. A ``Subplot`` and ``Dock`` are areas within a ``Figure``. +``Subplot`` and ``Dock`` 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: @@ -81,4 +81,4 @@ Now that we have understood ``PlotArea`` and ``Subplot`` we need a way for the u 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. \ No newline at end of file +the subplots. All subplots within a ``Figure`` share the same canvas and use different viewports to create the subplots. diff --git a/docs/source/generate_api.py b/docs/source/generate_api.py index 6887566cb..0be967a36 100644 --- a/docs/source/generate_api.py +++ b/docs/source/generate_api.py @@ -1,12 +1,15 @@ -from typing import * +from collections import defaultdict import inspect -from pathlib import Path +from io import StringIO import os +from pathlib import Path +from typing import * import fastplotlib -from fastplotlib.layouts._subplot import Subplot +from fastplotlib.layouts import Subplot from fastplotlib import graphics -from fastplotlib.graphics import _features, selectors +from fastplotlib.graphics import features, selectors +from fastplotlib import tools from fastplotlib import widgets from fastplotlib import utils from fastplotlib import ui @@ -19,8 +22,10 @@ GRAPHICS_DIR = API_DIR.joinpath("graphics") GRAPHIC_FEATURES_DIR = API_DIR.joinpath("graphic_features") SELECTORS_DIR = API_DIR.joinpath("selectors") +TOOLS_DIR = API_DIR.joinpath("tools") WIDGETS_DIR = API_DIR.joinpath("widgets") UI_DIR = API_DIR.joinpath("ui") +GUIDE_DIR = current_dir.joinpath("user_guide") doc_sources = [ API_DIR, @@ -28,6 +33,7 @@ GRAPHICS_DIR, GRAPHIC_FEATURES_DIR, SELECTORS_DIR, + TOOLS_DIR, WIDGETS_DIR, UI_DIR, ] @@ -56,16 +62,6 @@ "See the rendercanvas docs: https://rendercanvas.readthedocs.io/stable/api.html#rendercanvas.BaseLoop " ) -with open(API_DIR.joinpath("utils.rst"), "w") as f: - f.write( - "fastplotlib.utils\n" - "*****************\n\n" - - "..currentmodule:: fastplotlib.utils\n" - "..automodule:: fastplotlib.utils.functions\n" - " : members:\n" - ) - def get_public_members(cls) -> Tuple[List[str], List[str]]: """ @@ -139,12 +135,18 @@ def generate_class( return out -def generate_functions_module(module, name: str): +def generate_functions_module(module, name: str, generate_header: bool = True): underline = "*" * len(name) + if generate_header: + header = ( + f"{name}\n" + f"{underline}\n" + f"\n" + ) + else: + header = "\n" out = ( - f"{name}\n" - f"{underline}\n" - f"\n" + f"{header}" f".. currentmodule:: {name}\n" f".. automodule:: {module.__name__}\n" f" :members:\n" @@ -173,6 +175,60 @@ def generate_page( to_write = generate_class(cls, module) f.write(to_write) +####################################################### +# Used for GraphicFeature class event table +# copy-pasted from https://pablofernandez.tech/2019/03/21/turning-a-list-of-dicts-into-a-restructured-text-table/ + +def _generate_header(field_names, column_widths): + with StringIO() as output: + for field_name in field_names: + output.write(f"+-{'-' * column_widths[field_name]}-") + output.write("+\n") + for field_name in field_names: + output.write(f"| {field_name} {' ' * (column_widths[field_name] - len(field_name))}") + output.write("|\n") + for field_name in field_names: + output.write(f"+={'=' * column_widths[field_name]}=") + output.write("+\n") + return output.getvalue() + + +def _generate_row(row, field_names, column_widths): + with StringIO() as output: + for field_name in field_names: + output.write(f"| {row[field_name]}{' ' * (column_widths[field_name] - len(str(row[field_name])))} ") + output.write("|\n") + for field_name in field_names: + output.write(f"+-{'-' * column_widths[field_name]}-") + output.write("+\n") + return output.getvalue() + + +def _get_fields(data): + field_names = [] + column_widths = defaultdict(lambda: 0) + for row in data: + for field_name in row: + if field_name not in field_names: + field_names.append(field_name) + column_widths[field_name] = max(column_widths[field_name], len(field_name), len(str(row[field_name]))) + return field_names, column_widths + + +def dict_to_rst_table(data): + """convert a list of dicts to an RST table""" + field_names, column_widths = _get_fields(data) + with StringIO() as output: + output.write(_generate_header(field_names, column_widths)) + for row in data: + output.write(_generate_row(row, field_names, column_widths)) + + output.write("\n") + + return output.getvalue() + +####################################################### + def main(): generate_page( @@ -211,7 +267,8 @@ def main(): ) # the rest of this is a mess and can be refactored later - + ############################################################################## + # ** Graphic classes ** # graphic_classes = [getattr(graphics, g) for g in graphics.__all__] graphic_class_names = [g.__name__ for g in graphic_classes] @@ -237,8 +294,8 @@ def main(): source_path=GRAPHICS_DIR.joinpath(f"{graphic_cls.__name__}.rst"), ) ############################################################################## - - feature_classes = [getattr(_features, f) for f in _features.__all__] + # ** GraphicFeature classes ** # + feature_classes = [getattr(features, f) for f in features.__all__] feature_class_names = [f.__name__ for f in feature_classes] @@ -258,11 +315,11 @@ def main(): generate_page( page_name=feature_cls.__name__, classes=[feature_cls], - modules=["fastplotlib.graphics._features"], + modules=["fastplotlib.graphics.features"], source_path=GRAPHIC_FEATURES_DIR.joinpath(f"{feature_cls.__name__}.rst"), ) ############################################################################## - + # ** Selector classes ** # selector_classes = [getattr(selectors, s) for s in selectors.__all__] selector_class_names = [s.__name__ for s in selector_classes] @@ -286,8 +343,35 @@ def main(): modules=["fastplotlib"], source_path=SELECTORS_DIR.joinpath(f"{selector_cls.__name__}.rst"), ) + ############################################################################## + # ** Tools classes ** # + tools_classes = [getattr(tools, t) for t in tools.__all__] + + tools_class_names = [t.__name__ for t in tools_classes] + + tools_class_names_str = "\n ".join([""] + tools_class_names) + + with open(TOOLS_DIR.joinpath("index.rst"), "w") as f: + f.write( + f"Tools\n" + f"*****\n" + f"\n" + f".. toctree::\n" + f" :maxdepth: 1\n" + f"{tools_class_names_str}\n" + ) + for tool_cls in tools_classes: + generate_page( + page_name=tool_cls.__name__, + classes=[tool_cls], + modules=["fastplotlib"], + source_path=TOOLS_DIR.joinpath(f"{tool_cls.__name__}.rst"), + ) + + ############################################################################## + # ** Widget classes ** # widget_classes = [getattr(widgets, w) for w in widgets.__all__] widget_class_names = [w.__name__ for w in widget_classes] @@ -312,7 +396,7 @@ def main(): source_path=WIDGETS_DIR.joinpath(f"{widget_cls.__name__}.rst"), ) ############################################################################## - + # ** UI classes ** # ui_classes = [ui.BaseGUI, ui.Window, ui.EdgeWindow, ui.Popup] ui_class_names = [cls.__name__ for cls in ui_classes] @@ -340,11 +424,12 @@ def main(): ############################################################################## utils_str = generate_functions_module(utils.functions, "fastplotlib.utils") + utils_str += generate_functions_module(utils._plot_helpers, "fastplotlib.utils", generate_header=False) with open(API_DIR.joinpath("utils.rst"), "w") as f: f.write(utils_str) - # nake API index file + # make API index file with open(API_DIR.joinpath("index.rst"), "w") as f: f.write( "API Reference\n" @@ -356,11 +441,49 @@ def main(): " graphics/index\n" " graphic_features/index\n" " selectors/index\n" + " tools/index\n" " ui/index\n" " widgets/index\n" " fastplotlib\n" " utils\n" ) + ############################################################################## + # graphic feature event tables + + def write_table(name, feature_cls): + s = f"{name}\n" + s += "^" * len(name) + "\n\n" + + if hasattr(feature_cls, "event_extra_attrs"): + s += "**extra attributes**\n\n" + s += dict_to_rst_table(feature_cls.event_extra_attrs) + + s += "**event info dict**\n\n" + s += dict_to_rst_table(feature_cls.event_info_spec) + + return s + + with open(GUIDE_DIR.joinpath("event_tables.rst"), "w") as f: + f.write(".. _event_tables:\n\n") + f.write("Event Tables\n") + f.write("============\n\n") + + for graphic_cls in [*graphic_classes, *selector_classes]: + if graphic_cls is graphics.Graphic: + # skip Graphic base class + continue + f.write(f"{graphic_cls.__name__}\n") + f.write("-" * len(graphic_cls.__name__) + "\n\n") + for name, type_ in graphic_cls._features.items(): + if isinstance(type_, tuple): + for t in type_: + if t is None: + continue + f.write(write_table(name, t)) + else: + f.write(write_table(name, type_)) + + if __name__ == "__main__": main() diff --git a/docs/source/user_guide/event_tables.rst b/docs/source/user_guide/event_tables.rst new file mode 100644 index 000000000..1b9b2f7ec --- /dev/null +++ b/docs/source/user_guide/event_tables.rst @@ -0,0 +1,1020 @@ +.. _event_tables: + +Event Tables +============ + +LineGraphic +----------- + +data +^^^^ + +**event info dict** + ++----------+----------------------------------------------+--------------------------------------------------------+ +| dict key | type | description | ++==========+==============================================+========================================================+ +| key | slice, index (int) or numpy-like fancy index | key at which vertex positions data were indexed/sliced | ++----------+----------------------------------------------+--------------------------------------------------------+ +| value | int | float | array-like | new data values for points that were changed | ++----------+----------------------------------------------+--------------------------------------------------------+ + +colors +^^^^^^ + +**event info dict** + ++------------+--------------------------------------+------------------------------------------------------+ +| dict key | type | description | ++============+======================================+======================================================+ +| key | slice, index, numpy-like fancy index | index/slice at which colors were indexed/sliced | ++------------+--------------------------------------+------------------------------------------------------+ +| value | np.ndarray [n_points_changed, RGBA] | new color values for points that were changed | ++------------+--------------------------------------+------------------------------------------------------+ +| user_value | str or array-like | user input value that was parsed into the RGBA array | ++------------+--------------------------------------+------------------------------------------------------+ + +colors +^^^^^^ + +**event info dict** + ++----------+-------------------+-----------------+ +| dict key | type | description | ++==========+===================+=================+ +| value | np.ndarray [RGBA] | new color value | ++----------+-------------------+-----------------+ + +cmap +^^^^ + +**event info dict** + ++----------+-------+--------------------------------+ +| dict key | type | description | ++==========+=======+================================+ +| key | slice | key at cmap colors were sliced | ++----------+-------+--------------------------------+ +| value | str | new cmap to set at given slice | ++----------+-------+--------------------------------+ + +thickness +^^^^^^^^^ + +**event info dict** + ++----------+-------+---------------------+ +| dict key | type | description | ++==========+=======+=====================+ +| value | float | new thickness value | ++----------+-------+---------------------+ + +size_space +^^^^^^^^^^ + +**event info dict** + ++----------+------+------------------------------+ +| dict key | type | description | ++==========+======+==============================+ +| value | str | 'screen' | 'world' | 'model' | ++----------+------+------------------------------+ + +name +^^^^ + +**event info dict** + ++----------+------+--------------------+ +| dict key | type | description | ++==========+======+====================+ +| value | str | user provided name | ++----------+------+--------------------+ + +offset +^^^^^^ + +**event info dict** + ++----------+---------------------------------+----------------------+ +| dict key | type | description | ++==========+=================================+======================+ +| value | np.ndarray[float, float, float] | new offset (x, y, z) | ++----------+---------------------------------+----------------------+ + +rotation +^^^^^^^^ + +**event info dict** + ++----------+----------------------------------------+-------------------------+ +| dict key | type | description | ++==========+========================================+=========================+ +| value | np.ndarray[float, float, float, float] | new rotation quaternion | ++----------+----------------------------------------+-------------------------+ + +visible +^^^^^^^ + +**event info dict** + ++----------+------+---------------------+ +| dict key | type | description | ++==========+======+=====================+ +| value | bool | new visibility bool | ++----------+------+---------------------+ + +deleted +^^^^^^^ + +**event info dict** + ++----------+------+-------------------------------+ +| dict key | type | description | ++==========+======+===============================+ +| value | bool | True when graphic was deleted | ++----------+------+-------------------------------+ + +ScatterGraphic +-------------- + +data +^^^^ + +**event info dict** + ++----------+----------------------------------------------+--------------------------------------------------------+ +| dict key | type | description | ++==========+==============================================+========================================================+ +| key | slice, index (int) or numpy-like fancy index | key at which vertex positions data were indexed/sliced | ++----------+----------------------------------------------+--------------------------------------------------------+ +| value | int | float | array-like | new data values for points that were changed | ++----------+----------------------------------------------+--------------------------------------------------------+ + +sizes +^^^^^ + +**event info dict** + ++----------+----------------------------------------------+----------------------------------------------+ +| dict key | type | description | ++==========+==============================================+==============================================+ +| key | slice, index (int) or numpy-like fancy index | key at which point sizes were indexed/sliced | ++----------+----------------------------------------------+----------------------------------------------+ +| value | int | float | array-like | new size values for points that were changed | ++----------+----------------------------------------------+----------------------------------------------+ + +sizes +^^^^^ + +**event info dict** + ++----------+-------+----------------+ +| dict key | type | description | ++==========+=======+================+ +| value | float | new size value | ++----------+-------+----------------+ + +colors +^^^^^^ + +**event info dict** + ++------------+--------------------------------------+------------------------------------------------------+ +| dict key | type | description | ++============+======================================+======================================================+ +| key | slice, index, numpy-like fancy index | index/slice at which colors were indexed/sliced | ++------------+--------------------------------------+------------------------------------------------------+ +| value | np.ndarray [n_points_changed, RGBA] | new color values for points that were changed | ++------------+--------------------------------------+------------------------------------------------------+ +| user_value | str or array-like | user input value that was parsed into the RGBA array | ++------------+--------------------------------------+------------------------------------------------------+ + +colors +^^^^^^ + +**event info dict** + ++----------+-------------------+-----------------+ +| dict key | type | description | ++==========+===================+=================+ +| value | np.ndarray [RGBA] | new color value | ++----------+-------------------+-----------------+ + +cmap +^^^^ + +**event info dict** + ++----------+-------+--------------------------------+ +| dict key | type | description | ++==========+=======+================================+ +| key | slice | key at cmap colors were sliced | ++----------+-------+--------------------------------+ +| value | str | new cmap to set at given slice | ++----------+-------+--------------------------------+ + +size_space +^^^^^^^^^^ + +**event info dict** + ++----------+------+------------------------------+ +| dict key | type | description | ++==========+======+==============================+ +| value | str | 'screen' | 'world' | 'model' | ++----------+------+------------------------------+ + +name +^^^^ + +**event info dict** + ++----------+------+--------------------+ +| dict key | type | description | ++==========+======+====================+ +| value | str | user provided name | ++----------+------+--------------------+ + +offset +^^^^^^ + +**event info dict** + ++----------+---------------------------------+----------------------+ +| dict key | type | description | ++==========+=================================+======================+ +| value | np.ndarray[float, float, float] | new offset (x, y, z) | ++----------+---------------------------------+----------------------+ + +rotation +^^^^^^^^ + +**event info dict** + ++----------+----------------------------------------+-------------------------+ +| dict key | type | description | ++==========+========================================+=========================+ +| value | np.ndarray[float, float, float, float] | new rotation quaternion | ++----------+----------------------------------------+-------------------------+ + +visible +^^^^^^^ + +**event info dict** + ++----------+------+---------------------+ +| dict key | type | description | ++==========+======+=====================+ +| value | bool | new visibility bool | ++----------+------+---------------------+ + +deleted +^^^^^^^ + +**event info dict** + ++----------+------+-------------------------------+ +| dict key | type | description | ++==========+======+===============================+ +| value | bool | True when graphic was deleted | ++----------+------+-------------------------------+ + +ImageGraphic +------------ + +data +^^^^ + +**event info dict** + ++----------+--------------------------------------+--------------------------------------------------+ +| dict key | type | description | ++==========+======================================+==================================================+ +| key | slice, index, numpy-like fancy index | key at which image data was sliced/fancy indexed | ++----------+--------------------------------------+--------------------------------------------------+ +| value | np.ndarray | float | new data values | ++----------+--------------------------------------+--------------------------------------------------+ + +cmap +^^^^ + +**event info dict** + ++----------+------+---------------+ +| dict key | type | description | ++==========+======+===============+ +| value | str | new cmap name | ++----------+------+---------------+ + +vmin +^^^^ + +**event info dict** + ++----------+-------+----------------+ +| dict key | type | description | ++==========+=======+================+ +| value | float | new vmin value | ++----------+-------+----------------+ + +vmax +^^^^ + +**event info dict** + ++----------+-------+----------------+ +| dict key | type | description | ++==========+=======+================+ +| value | float | new vmax value | ++----------+-------+----------------+ + +interpolation +^^^^^^^^^^^^^ + +**event info dict** + ++----------+------+--------------------------------------------+ +| dict key | type | description | ++==========+======+============================================+ +| value | str | new interpolation method, nearest | linear | ++----------+------+--------------------------------------------+ + +cmap_interpolation +^^^^^^^^^^^^^^^^^^ + +**event info dict** + ++----------+------+------------------------------------------------+ +| dict key | type | description | ++==========+======+================================================+ +| value | str | new cmap interpolatio method, nearest | linear | ++----------+------+------------------------------------------------+ + +name +^^^^ + +**event info dict** + ++----------+------+--------------------+ +| dict key | type | description | ++==========+======+====================+ +| value | str | user provided name | ++----------+------+--------------------+ + +offset +^^^^^^ + +**event info dict** + ++----------+---------------------------------+----------------------+ +| dict key | type | description | ++==========+=================================+======================+ +| value | np.ndarray[float, float, float] | new offset (x, y, z) | ++----------+---------------------------------+----------------------+ + +rotation +^^^^^^^^ + +**event info dict** + ++----------+----------------------------------------+-------------------------+ +| dict key | type | description | ++==========+========================================+=========================+ +| value | np.ndarray[float, float, float, float] | new rotation quaternion | ++----------+----------------------------------------+-------------------------+ + +visible +^^^^^^^ + +**event info dict** + ++----------+------+---------------------+ +| dict key | type | description | ++==========+======+=====================+ +| value | bool | new visibility bool | ++----------+------+---------------------+ + +deleted +^^^^^^^ + +**event info dict** + ++----------+------+-------------------------------+ +| dict key | type | description | ++==========+======+===============================+ +| value | bool | True when graphic was deleted | ++----------+------+-------------------------------+ + +TextGraphic +----------- + +text +^^^^ + +**event info dict** + ++----------+------+---------------+ +| dict key | type | description | ++==========+======+===============+ +| value | str | new text data | ++----------+------+---------------+ + +font_size +^^^^^^^^^ + +**event info dict** + ++----------+-------------+---------------+ +| dict key | type | description | ++==========+=============+===============+ +| value | float | int | new font size | ++----------+-------------+---------------+ + +face_color +^^^^^^^^^^ + +**event info dict** + ++----------+------------------+----------------+ +| dict key | type | description | ++==========+==================+================+ +| value | str | np.ndarray | new text color | ++----------+------------------+----------------+ + +outline_color +^^^^^^^^^^^^^ + +**event info dict** + ++----------+------------------+-------------------+ +| dict key | type | description | ++==========+==================+===================+ +| value | str | np.ndarray | new outline color | ++----------+------------------+-------------------+ + +outline_thickness +^^^^^^^^^^^^^^^^^ + +**event info dict** + ++----------+-------+----------------------------+ +| dict key | type | description | ++==========+=======+============================+ +| value | float | new text outline thickness | ++----------+-------+----------------------------+ + +name +^^^^ + +**event info dict** + ++----------+------+--------------------+ +| dict key | type | description | ++==========+======+====================+ +| value | str | user provided name | ++----------+------+--------------------+ + +offset +^^^^^^ + +**event info dict** + ++----------+---------------------------------+----------------------+ +| dict key | type | description | ++==========+=================================+======================+ +| value | np.ndarray[float, float, float] | new offset (x, y, z) | ++----------+---------------------------------+----------------------+ + +rotation +^^^^^^^^ + +**event info dict** + ++----------+----------------------------------------+-------------------------+ +| dict key | type | description | ++==========+========================================+=========================+ +| value | np.ndarray[float, float, float, float] | new rotation quaternion | ++----------+----------------------------------------+-------------------------+ + +visible +^^^^^^^ + +**event info dict** + ++----------+------+---------------------+ +| dict key | type | description | ++==========+======+=====================+ +| value | bool | new visibility bool | ++----------+------+---------------------+ + +deleted +^^^^^^^ + +**event info dict** + ++----------+------+-------------------------------+ +| dict key | type | description | ++==========+======+===============================+ +| value | bool | True when graphic was deleted | ++----------+------+-------------------------------+ + +LineCollection +-------------- + +data +^^^^ + +**event info dict** + ++----------+----------------------------------------------+--------------------------------------------------------+ +| dict key | type | description | ++==========+==============================================+========================================================+ +| key | slice, index (int) or numpy-like fancy index | key at which vertex positions data were indexed/sliced | ++----------+----------------------------------------------+--------------------------------------------------------+ +| value | int | float | array-like | new data values for points that were changed | ++----------+----------------------------------------------+--------------------------------------------------------+ + +colors +^^^^^^ + +**event info dict** + ++------------+--------------------------------------+------------------------------------------------------+ +| dict key | type | description | ++============+======================================+======================================================+ +| key | slice, index, numpy-like fancy index | index/slice at which colors were indexed/sliced | ++------------+--------------------------------------+------------------------------------------------------+ +| value | np.ndarray [n_points_changed, RGBA] | new color values for points that were changed | ++------------+--------------------------------------+------------------------------------------------------+ +| user_value | str or array-like | user input value that was parsed into the RGBA array | ++------------+--------------------------------------+------------------------------------------------------+ + +colors +^^^^^^ + +**event info dict** + ++----------+-------------------+-----------------+ +| dict key | type | description | ++==========+===================+=================+ +| value | np.ndarray [RGBA] | new color value | ++----------+-------------------+-----------------+ + +cmap +^^^^ + +**event info dict** + ++----------+-------+--------------------------------+ +| dict key | type | description | ++==========+=======+================================+ +| key | slice | key at cmap colors were sliced | ++----------+-------+--------------------------------+ +| value | str | new cmap to set at given slice | ++----------+-------+--------------------------------+ + +thickness +^^^^^^^^^ + +**event info dict** + ++----------+-------+---------------------+ +| dict key | type | description | ++==========+=======+=====================+ +| value | float | new thickness value | ++----------+-------+---------------------+ + +size_space +^^^^^^^^^^ + +**event info dict** + ++----------+------+------------------------------+ +| dict key | type | description | ++==========+======+==============================+ +| value | str | 'screen' | 'world' | 'model' | ++----------+------+------------------------------+ + +name +^^^^ + +**event info dict** + ++----------+------+--------------------+ +| dict key | type | description | ++==========+======+====================+ +| value | str | user provided name | ++----------+------+--------------------+ + +offset +^^^^^^ + +**event info dict** + ++----------+---------------------------------+----------------------+ +| dict key | type | description | ++==========+=================================+======================+ +| value | np.ndarray[float, float, float] | new offset (x, y, z) | ++----------+---------------------------------+----------------------+ + +rotation +^^^^^^^^ + +**event info dict** + ++----------+----------------------------------------+-------------------------+ +| dict key | type | description | ++==========+========================================+=========================+ +| value | np.ndarray[float, float, float, float] | new rotation quaternion | ++----------+----------------------------------------+-------------------------+ + +visible +^^^^^^^ + +**event info dict** + ++----------+------+---------------------+ +| dict key | type | description | ++==========+======+=====================+ +| value | bool | new visibility bool | ++----------+------+---------------------+ + +deleted +^^^^^^^ + +**event info dict** + ++----------+------+-------------------------------+ +| dict key | type | description | ++==========+======+===============================+ +| value | bool | True when graphic was deleted | ++----------+------+-------------------------------+ + +LineStack +--------- + +data +^^^^ + +**event info dict** + ++----------+----------------------------------------------+--------------------------------------------------------+ +| dict key | type | description | ++==========+==============================================+========================================================+ +| key | slice, index (int) or numpy-like fancy index | key at which vertex positions data were indexed/sliced | ++----------+----------------------------------------------+--------------------------------------------------------+ +| value | int | float | array-like | new data values for points that were changed | ++----------+----------------------------------------------+--------------------------------------------------------+ + +colors +^^^^^^ + +**event info dict** + ++------------+--------------------------------------+------------------------------------------------------+ +| dict key | type | description | ++============+======================================+======================================================+ +| key | slice, index, numpy-like fancy index | index/slice at which colors were indexed/sliced | ++------------+--------------------------------------+------------------------------------------------------+ +| value | np.ndarray [n_points_changed, RGBA] | new color values for points that were changed | ++------------+--------------------------------------+------------------------------------------------------+ +| user_value | str or array-like | user input value that was parsed into the RGBA array | ++------------+--------------------------------------+------------------------------------------------------+ + +colors +^^^^^^ + +**event info dict** + ++----------+-------------------+-----------------+ +| dict key | type | description | ++==========+===================+=================+ +| value | np.ndarray [RGBA] | new color value | ++----------+-------------------+-----------------+ + +cmap +^^^^ + +**event info dict** + ++----------+-------+--------------------------------+ +| dict key | type | description | ++==========+=======+================================+ +| key | slice | key at cmap colors were sliced | ++----------+-------+--------------------------------+ +| value | str | new cmap to set at given slice | ++----------+-------+--------------------------------+ + +thickness +^^^^^^^^^ + +**event info dict** + ++----------+-------+---------------------+ +| dict key | type | description | ++==========+=======+=====================+ +| value | float | new thickness value | ++----------+-------+---------------------+ + +size_space +^^^^^^^^^^ + +**event info dict** + ++----------+------+------------------------------+ +| dict key | type | description | ++==========+======+==============================+ +| value | str | 'screen' | 'world' | 'model' | ++----------+------+------------------------------+ + +name +^^^^ + +**event info dict** + ++----------+------+--------------------+ +| dict key | type | description | ++==========+======+====================+ +| value | str | user provided name | ++----------+------+--------------------+ + +offset +^^^^^^ + +**event info dict** + ++----------+---------------------------------+----------------------+ +| dict key | type | description | ++==========+=================================+======================+ +| value | np.ndarray[float, float, float] | new offset (x, y, z) | ++----------+---------------------------------+----------------------+ + +rotation +^^^^^^^^ + +**event info dict** + ++----------+----------------------------------------+-------------------------+ +| dict key | type | description | ++==========+========================================+=========================+ +| value | np.ndarray[float, float, float, float] | new rotation quaternion | ++----------+----------------------------------------+-------------------------+ + +visible +^^^^^^^ + +**event info dict** + ++----------+------+---------------------+ +| dict key | type | description | ++==========+======+=====================+ +| value | bool | new visibility bool | ++----------+------+---------------------+ + +deleted +^^^^^^^ + +**event info dict** + ++----------+------+-------------------------------+ +| dict key | type | description | ++==========+======+===============================+ +| value | bool | True when graphic was deleted | ++----------+------+-------------------------------+ + +LinearSelector +-------------- + +selection +^^^^^^^^^ + +**extra attributes** + ++--------------------+----------+----------------------------------+ +| attribute | type | description | ++====================+==========+==================================+ +| get_selected_index | callable | returns index under the selector | ++--------------------+----------+----------------------------------+ + +**event info dict** + ++----------+-------+-------------------------------+ +| dict key | type | description | ++==========+=======+===============================+ +| value | float | new x or y value of selection | ++----------+-------+-------------------------------+ + +name +^^^^ + +**event info dict** + ++----------+------+--------------------+ +| dict key | type | description | ++==========+======+====================+ +| value | str | user provided name | ++----------+------+--------------------+ + +offset +^^^^^^ + +**event info dict** + ++----------+---------------------------------+----------------------+ +| dict key | type | description | ++==========+=================================+======================+ +| value | np.ndarray[float, float, float] | new offset (x, y, z) | ++----------+---------------------------------+----------------------+ + +rotation +^^^^^^^^ + +**event info dict** + ++----------+----------------------------------------+-------------------------+ +| dict key | type | description | ++==========+========================================+=========================+ +| value | np.ndarray[float, float, float, float] | new rotation quaternion | ++----------+----------------------------------------+-------------------------+ + +visible +^^^^^^^ + +**event info dict** + ++----------+------+---------------------+ +| dict key | type | description | ++==========+======+=====================+ +| value | bool | new visibility bool | ++----------+------+---------------------+ + +deleted +^^^^^^^ + +**event info dict** + ++----------+------+-------------------------------+ +| dict key | type | description | ++==========+======+===============================+ +| value | bool | True when graphic was deleted | ++----------+------+-------------------------------+ + +LinearRegionSelector +-------------------- + +selection +^^^^^^^^^ + +**extra attributes** + ++----------------------+----------+------------------------------------+ +| attribute | type | description | ++======================+==========+====================================+ +| get_selected_indices | callable | returns indices under the selector | ++----------------------+----------+------------------------------------+ +| get_selected_data | callable | returns data under the selector | ++----------------------+----------+------------------------------------+ + +**event info dict** + ++----------+------------+-----------------------------+ +| dict key | type | description | ++==========+============+=============================+ +| value | np.ndarray | new [min, max] of selection | ++----------+------------+-----------------------------+ + +name +^^^^ + +**event info dict** + ++----------+------+--------------------+ +| dict key | type | description | ++==========+======+====================+ +| value | str | user provided name | ++----------+------+--------------------+ + +offset +^^^^^^ + +**event info dict** + ++----------+---------------------------------+----------------------+ +| dict key | type | description | ++==========+=================================+======================+ +| value | np.ndarray[float, float, float] | new offset (x, y, z) | ++----------+---------------------------------+----------------------+ + +rotation +^^^^^^^^ + +**event info dict** + ++----------+----------------------------------------+-------------------------+ +| dict key | type | description | ++==========+========================================+=========================+ +| value | np.ndarray[float, float, float, float] | new rotation quaternion | ++----------+----------------------------------------+-------------------------+ + +visible +^^^^^^^ + +**event info dict** + ++----------+------+---------------------+ +| dict key | type | description | ++==========+======+=====================+ +| value | bool | new visibility bool | ++----------+------+---------------------+ + +deleted +^^^^^^^ + +**event info dict** + ++----------+------+-------------------------------+ +| dict key | type | description | ++==========+======+===============================+ +| value | bool | True when graphic was deleted | ++----------+------+-------------------------------+ + +RectangleSelector +----------------- + +selection +^^^^^^^^^ + +**extra attributes** + ++----------------------+----------+------------------------------------+ +| attribute | type | description | ++======================+==========+====================================+ +| get_selected_indices | callable | returns indices under the selector | ++----------------------+----------+------------------------------------+ +| get_selected_data | callable | returns data under the selector | ++----------------------+----------+------------------------------------+ + +**event info dict** + ++----------+------------+-------------------------------------------+ +| dict key | type | description | ++==========+============+===========================================+ +| value | np.ndarray | new [xmin, xmax, ymin, ymax] of selection | ++----------+------------+-------------------------------------------+ + +name +^^^^ + +**event info dict** + ++----------+------+--------------------+ +| dict key | type | description | ++==========+======+====================+ +| value | str | user provided name | ++----------+------+--------------------+ + +offset +^^^^^^ + +**event info dict** + ++----------+---------------------------------+----------------------+ +| dict key | type | description | ++==========+=================================+======================+ +| value | np.ndarray[float, float, float] | new offset (x, y, z) | ++----------+---------------------------------+----------------------+ + +rotation +^^^^^^^^ + +**event info dict** + ++----------+----------------------------------------+-------------------------+ +| dict key | type | description | ++==========+========================================+=========================+ +| value | np.ndarray[float, float, float, float] | new rotation quaternion | ++----------+----------------------------------------+-------------------------+ + +visible +^^^^^^^ + +**event info dict** + ++----------+------+---------------------+ +| dict key | type | description | ++==========+======+=====================+ +| value | bool | new visibility bool | ++----------+------+---------------------+ + +deleted +^^^^^^^ + +**event info dict** + ++----------+------+-------------------------------+ +| dict key | type | description | ++==========+======+===============================+ +| value | bool | True when graphic was deleted | ++----------+------+-------------------------------+ + diff --git a/docs/source/user_guide/faq.rst b/docs/source/user_guide/faq.rst index 0061a04d4..5985efae1 100644 --- a/docs/source/user_guide/faq.rst +++ b/docs/source/user_guide/faq.rst @@ -56,6 +56,8 @@ Should I use ``fastplotlib`` for making publication figures? While `fastplotlib` figures can be exported to PNG using ``figure.export()``, `fastplotlib` is not intended for creating *static* publication figures. There are many other libraries that are well-suited for this task. + The rendering engine pygfx has a starting point for an svg renderer, you may try and expand upon it: https://github.com/pygfx/pygfx/tree/main/pygfx/renderers/svg + How does ``fastplotlib`` handle data loading? --------------------------------------------- diff --git a/docs/source/user_guide/guide.rst b/docs/source/user_guide/guide.rst index d544c42a3..073fa806c 100644 --- a/docs/source/user_guide/guide.rst +++ b/docs/source/user_guide/guide.rst @@ -41,8 +41,8 @@ The fundamental goal of ``fastplotlib`` is to provide a high-level, expressive A make it easy and intuitive to produce interactive visualizations that are as performant and vibrant as a modern video game 😄 -How to use ``fastplotlib`` --------------------------- +``fastplotlib`` basics +---------------------- Before giving a detailed overview of the library, here is a minimal example:: @@ -71,16 +71,22 @@ This is just a simple example of how the ``fastplotlib`` API works to create a p However, we are just scratching the surface of what is possible with ``fastplotlib``. Next, let's take a look at the building blocks of ``fastplotlib`` and how they can be used to create more complex visualizations. +Aside from this user guide, the Examples Gallery is the best place to learn specific things in fastplotlib. +If you still need help don't hesitate to post an issue or discussion post! + Figure ------ -The starting point for creating any visualization in ``fastplotlib`` is a ``Figure`` object. This can be a single plot or a grid of subplots. +The starting point for creating any visualization in ``fastplotlib`` is a ``Figure`` object. This can be a single subplot or many subplots. The ``Figure`` object houses and takes care of the underlying rendering components such as the camera, controller, renderer, and canvas. Most users won't need to use these directly; however, the ability to directly interact with the rendering engine is still available if needed. -By default, if no ``shape`` argument is provided when creating a ``Figure``, there will be a single subplot. All subplots in a ``Figure`` can be accessed using -indexing (i.e. ``fig_object[i ,j]``). +By default, if no ``shape`` argument is provided when creating a ``Figure``, there will be a single ``Subplot``. + +If a shape argument is provided, all subplots in a ``Figure`` can be accessed by indexing (i.e. ``fig_object[i ,j]``). A "window layout" +with customizable subplot positions and sizes can also be set by providing a ``rects`` or ``extents`` argument. The Examples Gallery +has a few examples that show how to create a "Window Layout". After defining a ``Figure``, we can begin to add ``Graphic`` objects. @@ -99,18 +105,22 @@ to be easily accessed from figures:: add image graphic image_graphic = fig[0, 0].add_image(data=data, name="astronaut") - # show plot + # show figure fig.show() - # index plot to get graphic + # index subplot to get graphic fig[0, 0]["astronaut"] + # another way to index graphics in a subplot + fig[0, 0].graphics[0] is fig[0, 0]["astronaut"] # will return `True` + .. See the examples gallery for examples on how to create and interactive with all the various types of graphics. -Graphics also have mutable properties that can be linked to events. Some of these properties, such as the ``data`` or ``colors`` of a line can even be indexed, -allowing for the creation of very powerful visualizations. +Graphics also have mutable properties. Some of these properties, such as the ``data`` or ``colors`` of a line can even be sliced, +allowing for the creation of very powerful visualizations. Event handlers can be added to a graphic to capture changes to +any of these properties. (1) Common properties that all graphics have @@ -132,17 +142,17 @@ allowing for the creation of very powerful visualizations. (a) ``ImageGraphic`` - +------------------------+------------------------------------+ - | Feature Name | Description | - +========================+====================================+ - | data | Underlying image data | - +------------------------+------------------------------------+ - | vmin | Lower contrast limit of an image | - +------------------------+------------------------------------+ - | vmax | Upper contrast limit of an image | - +------------------------+------------------------------------+ - | cmap | Colormap of an image | - +------------------------+------------------------------------+ + +------------------------+---------------------------------------------------+ + | Feature Name | Description | + +========================+===================================================+ + | data | Underlying image data | + +------------------------+---------------------------------------------------+ + | vmin | Lower contrast limit of an image | + +------------------------+---------------------------------------------------+ + | vmax | Upper contrast limit of an image | + +------------------------+---------------------------------------------------+ + | cmap | Colormap for a grayscale image, ignored if RGB(A) | + +------------------------+---------------------------------------------------+ (b) ``LineGraphic``, ``LineCollection``, ``LineStack`` @@ -244,14 +254,13 @@ your data, you are able to select an entire region. See the examples gallery for more in-depth examples with selector tools. Now we have the basics of creating a ``Figure``, adding ``Graphics`` to a ``Figure``, and working with ``Graphic`` properties to dynamically change or alter them. -Let's take a look at how we can define events to link ``Graphics`` and their properties together. Events ------ -Events can be a multitude of things: traditional events such as mouse or keyboard events, or events related to ``Graphic`` properties. +Events can be a multitude of things: canvas events such as mouse or keyboard events, or events related to ``Graphic`` properties. -There are two ways to add events in ``fastplotlib``. +There are two ways to add events to a graphic: 1) Use the method `add_event_handler()` :: @@ -272,24 +281,24 @@ There are two ways to add events in ``fastplotlib``. .. -The ``event_handler`` is a user-defined function that accepts an event instance as the first and only positional argument. +The ``event_handler`` is a user-defined callback function that accepts an event instance as the first and only positional argument. Information about the structure of event instances are described below. The ``"event_type"`` -is a string that identifies the type of event; this can be either a ``pygfx.Event`` or a ``Graphic`` property event. +is a string that identifies the type of event. ``graphic.supported_events`` will return a tuple of all ``event_type`` strings that this graphic supports. When an event occurs, the user-defined event handler will receive an event object. Depending on the type of event, the -event object will have relevant information that can be used in the callback. See below for event tables. +event object will have relevant information that can be used in the callback. See the next section for details. Graphic property events ^^^^^^^^^^^^^^^^^^^^^^^ -All ``Graphic`` events have the following attributes: +All ``Graphic`` events are instances of ``fastplotlib.GraphicFeatureEvent`` and have the following attributes: +------------+-------------+-----------------------------------------------+ | attribute | type | description | +============+=============+===============================================+ - | type | str | "colors" - name of the event | + | type | str | name of the event type | +------------+-------------+-----------------------------------------------+ | graphic | Graphic | graphic instance that the event is from | +------------+-------------+-----------------------------------------------+ @@ -300,144 +309,80 @@ All ``Graphic`` events have the following attributes: | time_stamp | float | time when the event occurred, in ms | +------------+-------------+-----------------------------------------------+ -The ``info`` attribute will house additional information for different ``Graphic`` property events: - -event_type: "colors" - - Vertex Colors - - **info dict** - - +------------+-----------------------------------------------------------+----------------------------------------------------------------------------------+ - | dict key | value type | value description | - +============+===========================================================+==================================================================================+ - | key | int | slice | np.ndarray[int | bool] | tuple[slice, ...] | key at which colors were indexed/sliced | - +------------+-----------------------------------------------------------+----------------------------------------------------------------------------------+ - | value | np.ndarray | new color values for points that were changed, shape is [n_points_changed, RGBA] | - +------------+-----------------------------------------------------------+----------------------------------------------------------------------------------+ - | user_value | str | np.ndarray | tuple[float] | list[float] | list[str] | user input value that was parsed into the RGBA array | - +------------+-----------------------------------------------------------+----------------------------------------------------------------------------------+ - - Uniform Colors - - **info dict** - - +------------+-----------------------------------------------------------+----------------------------------------------------------------------------------+ - | dict key | value type | value description | - +============+===========================================================+==================================================================================+ - | value | np.ndarray | new color values for points that were changed, shape is [n_points_changed, RGBA] | - +------------+-----------------------------------------------------------+----------------------------------------------------------------------------------+ +Selectors have one event called ``selection`` which has extra attributes in addition to those listed in the table above. +The selection event section covers these. -event_type: "sizes" +The ``info`` attribute for most graphic property events will have one key, ``"value"``, which is the new value +of the graphic property. Events for graphic properties that represent arrays, such the ``data`` properties for +images, lines, and scatters will contain more entries. Here are a list of all graphic properties that have such +additional entries: - **info dict** +* ``ImageGraphic`` + * data - +----------+----------------------------------------------------------+------------------------------------------------------------------------------------------+ - | dict key | value type | value description | - +==========+==========================================================+==========================================================================================+ - | key | int | slice | np.ndarray[int | bool] | tuple[slice, ...] | key at which vertex positions data were indexed/sliced | - +----------+----------------------------------------------------------+------------------------------------------------------------------------------------------+ - | value | np.ndarray | float | list[float] | new data values for points that were changed, shape depends on the indices that were set | - +----------+----------------------------------------------------------+------------------------------------------------------------------------------------------+ +* ``LineGraphic`` + * data, colors, cmap -event_type: "data" +* ``ScatterGraphic`` + * data, colors, cmap, sizes - **info dict** +You can understand an event's attributes by adding a simple event handler:: - +----------+----------------------------------------------------------+------------------------------------------------------------------------------------------+ - | dict key | value type | value description | - +==========+==========================================================+==========================================================================================+ - | key | int | slice | np.ndarray[int | bool] | tuple[slice, ...] | key at which vertex positions data were indexed/sliced | - +----------+----------------------------------------------------------+------------------------------------------------------------------------------------------+ - | value | np.ndarray | float | list[float] | new data values for points that were changed, shape depends on the indices that were set | - +----------+----------------------------------------------------------+------------------------------------------------------------------------------------------+ - -event_type: "thickness" - - **info dict** - - +------------+-----------------------------------------------------------+----------------------------------------------------------------------------------+ - | dict key | value type | value description | - +============+===========================================================+==================================================================================+ - | value | float | new thickness value | - +------------+-----------------------------------------------------------+----------------------------------------------------------------------------------+ - -event_type: "cmap" - - **info dict** - - +------------+-----------------------------------------------------------+----------------------------------------------------------------------------------+ - | dict key | value type | value description | - +============+===========================================================+==================================================================================+ - | value | string | new colormap value | - +------------+-----------------------------------------------------------+----------------------------------------------------------------------------------+ + @graphic.add_event_handler("event_type") + def handler(ev): + print(ev.type) + print(ev.graphic) + print(ev.info) -event_type: "selection" + # trigger the event + graphic.event_type = - ``LinearSelector`` + # direct example + @image_graphic.add_event_handler("cmap") + def cmap_changed(ev): + print(ev.type) + print(ev.info) - **additional event attributes:** + image_graphic.cmap = "viridis" + # this will trigger the cmap event and print the following: + # 'cmap' + # {"value": "viridis"} - +--------------------+----------+------------------------------------+ - | attribute | type | description | - +====================+==========+====================================+ - | get_selected_index | callable | returns indices under the selector | - +--------------------+----------+------------------------------------+ +.. - **info dict:** +The :ref:`event_tables` provide a description of the event info dicts for all Graphic Feature Events. - +----------+------------+-------------------------------+ - | dict key | value type | value description | - +==========+============+===============================+ - | value | np.ndarray | new x or y value of selection | - +----------+------------+-------------------------------+ +Selection event +~~~~~~~~~~~~~~~ - ``LinearRegionSelector`` +The ``selection`` event for selectors has additional attributes, mostly ``callable`` methods, that aid in using the +selector tool, such as getting the indices or data under the selection. The ``info`` dict will contain one entry ``value`` +which is the new selection value. - **additional event attributes:** +The :ref:`event_tables` provide a description of the additional attributes as well as the event info dicts for selector events. - +----------------------+----------+------------------------------------+ - | attribute | type | description | - +======================+==========+====================================+ - | get_selected_indices | callable | returns indices under the selector | - +----------------------+----------+------------------------------------+ - | get_selected_data | callable | returns data under the selector | - +----------------------+----------+------------------------------------+ +Canvas Events +^^^^^^^^^^^^^ - **info dict:** +Canvas events can be added to a graphic or to a Figure (see next section). +Here is a description of all canvas events and their attributes. - +----------+------------+-----------------------------+ - | dict key | value type | value description | - +==========+============+=============================+ - | value | np.ndarray | new [min, max] of selection | - +----------+------------+-----------------------------+ +The examples gallery provides several examples using pointer and key events. -Rendering engine events from a Graphic -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +Pointer events +~~~~~~~~~~~~~~ -Rendering engine event handlers can be added to a graphic or to a Figure (see next section). -Here is a description of all rendering engine events and their attributes. +**List of pointer events:** * **pointer_down**: emitted when the user interacts with mouse, - touch or other pointer devices, by pressing it down. - - * *x*: horizontal position of the pointer within the widget. - * *y*: vertical position of the pointer within the widget. - * *button*: the button to which this event applies. See "Mouse buttons" section below for details. - * *buttons*: a tuple of buttons being pressed down. - * *modifiers*: a tuple of modifier keys being pressed down. See section below for details. - * *ntouches*: the number of simultaneous pointers being down. - * *touches*: a dict with int keys (pointer id's), and values that are dicts - that contain "x", "y", and "pressure". - * *time_stamp*: a timestamp in seconds. * **pointer_up**: emitted when the user releases a pointer. - This event has the same keys as the pointer down event. * **pointer_move**: emitted when the user moves a pointer. - This event has the same keys as the pointer down event. This event is throttled. +* **click**: emmitted when a mouse button is clicked. + * **double_click**: emitted on a double-click. This event looks like a pointer event, but without the touches. @@ -465,25 +410,19 @@ Here is a description of all rendering engine events and their attributes. * *modifiers*: a tuple of modifier keys being pressed down. * *time_stamp*: a timestamp in seconds. -* **key_down**: emitted when a key is pressed down. - - * *key*: the key being pressed as a string. See section below for details. - * *modifiers*: a tuple of modifier keys being pressed down. - * *time_stamp*: a timestamp in seconds. - -* **key_up**: emitted when a key is released. - This event has the same keys as the key down event. - - -Time stamps -~~~~~~~~~~~ - -Since the time origin of ``time_stamp`` values is undefined, -time stamp values only make sense in relation to other time stamps. +All pointer events have the following attributes: +* *x*: horizontal position of the pointer within the widget. +* *y*: vertical position of the pointer within the widget. +* *button*: the button to which this event applies. See "Mouse buttons" section below for details. +* *buttons*: a tuple of buttons being pressed down (see below) +* *modifiers*: a tuple of modifier keys being pressed down. See section below for details. +* *ntouches*: the number of simultaneous pointers being down. +* *touches*: a dict with int keys (pointer id's), and values that are dicts + that contain "x", "y", and "pressure". +* *time_stamp*: a timestamp in seconds. -Mouse buttons -~~~~~~~~~~~~~ +**Mouse buttons:** * 0: No button. * 1: Left button. @@ -491,9 +430,20 @@ Mouse buttons * 3: Middle button * 4-9: etc. +Key events +~~~~~~~~~~ + +**List of key (keyboard keys) events:** + +* **key_down**: emitted when a key is pressed down. + +* **key_up**: emitted when a key is released. -Keys -~~~~ +Key events have the following attributes: + +* *key*: the key being pressed as a string. See section below for details. +* *modifiers*: a tuple of modifier keys being pressed down. +* *time_stamp*: a timestamp in seconds. The key names follow the `browser spec `_. @@ -504,13 +454,18 @@ The key names follow the `browser spec `_ library is great for rapidly building UIs for prototyping +in jupyter. It is particularly useful for scientific and engineering applications since we can rapidly create a UI to +interact with our ``fastplotlib`` visualization. The main downside is that it only works in jupyter. + +.. image:: ../_static/guide_ipywidgets.webp + +For examples please see the examples gallery. + +Qt +^^ + +Qt is a very popular UI library written in C++, ``PyQt6`` and ``PySide6`` provide python bindings. There are countless +tutorials on how to build a UI using Qt which you can easily find if you google ``PyQt``. You can embed a ``Figure`` as +a Qt widget within a Qt application. + +For examples please see the examples gallery. + +imgui +^^^^^ + +`Imgui `_ is also a very popular library used for building UIs. The difference +between imgui and ipywidgets, Qt, and wx is the imgui UI can be rendered directly on the same canvas as a fastplotlib +``Figure``. This is hugely advantageous, it means that you can write an imgui UI and it will run on any GUI backend, +i.e. it will work in jupyter, Qt, glfw and wx windows! The programming model is different from Qt and ipywidgets, there +are no callbacks, but it is easy to learn if you see a few examples. + +.. image:: ../_static/guide_imgui.png + +We specifically use `imgui-bundle `_ for the python bindings in fastplotlib. +There is large community and many resources out there on building UIs using imgui. + +To install ``fastplotlib`` with ``imgui`` use the ``imgui`` extras option, i.e. ``pip install fastplotlib[imgui]``, or ``pip install imgui_bundle`` if you've already installed fastplotlib. + +Fastplotlib comes built-in with imgui UIs for subplot toolbars and a standard right-click menu with a number of options. +You can also make custom GUIs and embed them within the canvas, see the examples gallery for detailed examples. + +**Some tips:** + +The ``imgui-bundle`` docs as of March 2025 don't have a nice API list (as far as I know), here is how we go about developing UIs with imgui: + +1. Use the ``pyimgui`` API docs to locate the type of UI element we want, for example if we want a ``slider_int``: https://pyimgui.readthedocs.io/en/latest/reference/imgui.core.html#imgui.core.slider_int + +2. Look at the function signature in the ``imgui-bundle`` sources. You can usually access this easily with your IDE: https://github.com/pthom/imgui_bundle/blob/a5e7d46555832c40e9be277d4747eac5a303dbfc/bindings/imgui_bundle/imgui/__init__.pyi#L1693-L1696 + +3. ``pyimgui`` and ``imgui-bundle`` sometimes don't have the same function signature, so we use a combination of the pyimgui docs and +imgui-bundle function signature to understand and implement the UI element. + ImageWidget ----------- @@ -572,12 +584,9 @@ Let's look at an example: :: movie = iio.imread("imageio:cockatoo.mp4") - # convert RGB movie to grayscale - gray_movie = np.dot(movie[..., :3], [0.299, 0.587, 0.114]) - iw_movie = ImageWidget( - data=gray_movie, - cmap="gray" + data=movie, + rgb=True ) iw_movie.show() @@ -658,21 +667,6 @@ There are several spaces to consider when using ``fastplotlib``: For more information on the various spaces used by rendering engines please see this `article `_ -Imgui ------ - -Fastplotlib uses `imgui_bundle `_ to provide within-canvas UI elemenents if you -installed ``fastplotlib`` using the ``imgui`` toggle, i.e. ``fastplotlib[imgui]``, or installed ``imgui_bundle`` afterwards. - -Fastplotlib comes built-in with imgui UIs for subplot toolbars and a standard right-click menu with a number of options. -You can also make custom GUIs and embed them within the canvas, see the examples gallery for detailed examples. - -.. note:: - Imgui is optional, you can use other GUI frameworks such at Qt or ipywidgets with fastplotlib. You can also of course - use imgui and Qt or ipywidgets. - -.. image:: ../_static/guide_imgui.png - Using ``fastplotlib`` in an interactive shell --------------------------------------------- diff --git a/docs/source/user_guide/index.rst b/docs/source/user_guide/index.rst index 59189be22..92f0da98c 100644 --- a/docs/source/user_guide/index.rst +++ b/docs/source/user_guide/index.rst @@ -6,5 +6,6 @@ User Guide :maxdepth: 2 guide + event_tables gpu faq diff --git a/examples/controllers/README.rst b/examples/controllers/README.rst new file mode 100644 index 000000000..824087ce3 --- /dev/null +++ b/examples/controllers/README.rst @@ -0,0 +1,2 @@ +Controller examples +=================== diff --git a/examples/controllers/specify_integers.py b/examples/controllers/specify_integers.py new file mode 100644 index 000000000..14b09b015 --- /dev/null +++ b/examples/controllers/specify_integers.py @@ -0,0 +1,50 @@ +""" +Specify IDs with integers +========================= + +Specify controllers to sync subplots using integer IDs +""" + +# test_example = false +# sphinx_gallery_pygfx_docs = 'screenshot' + +import numpy as np +import fastplotlib as fpl + + +xs = np.linspace(0, 2 * np.pi, 100) +sine = np.sin(xs) +cosine = np.cos(xs) + +# controller IDs +# one controller is created for each unique ID +# if the IDs are the same, those subplots will be synced +ids = [ + [0, 1], + [2, 0], +] + +figure = fpl.Figure( + shape=(2, 2), + controller_ids=ids, + size=(700, 560), +) + +for subplot, controller_id in zip(figure, np.asarray(ids).ravel()): + subplot.title = f"contr. id: {controller_id}" + +figure[0, 0].add_line(np.column_stack([xs, sine])) + +figure[0, 1].add_line(np.random.rand(100)) +figure[1, 0].add_line(np.random.rand(100)) + +figure[1, 1].add_line(np.column_stack([xs, cosine])) + +figure.show(maintain_aspect=False) + + +# NOTE: `if __name__ == "__main__"` is NOT how to use fastplotlib interactively +# please see our docs for using fastplotlib interactively in ipython and jupyter +if __name__ == "__main__": + print(__doc__) + fpl.loop.run() diff --git a/examples/controllers/specify_names.py b/examples/controllers/specify_names.py new file mode 100644 index 000000000..fb0539c4a --- /dev/null +++ b/examples/controllers/specify_names.py @@ -0,0 +1,47 @@ +""" +Specify IDs with subplot names +============================== + +Provide a list of tuples where each tuple has subplot names. The same controller will be used for the subplots +indicated by each of these tuples +""" + +# test_example = false +# sphinx_gallery_pygfx_docs = 'screenshot' + +import numpy as np +import fastplotlib as fpl + + +xs = np.linspace(0, 2 * np.pi, 100) +ys = np.sin(xs) + +# create some subplots names +names = ["subplot_0", "subplot_1", "subplot_2", "subplot_3", "subplot_4", "subplot_5"] + +# list of tuples of subplot names +# subplots within each tuple will use the same controller. +ids = [ + ("subplot_0", "subplot_3"), + ("subplot_1", "subplot_2", "subplot_4"), +] + + +figure = fpl.Figure( + shape=(2, 3), + controller_ids=ids, + names=names, + size=(700, 560), +) + +for subplot in figure: + subplot.add_line(np.column_stack([xs, ys + np.random.normal(scale=0.1, size=100)])) + +figure.show(maintain_aspect=False) + + +# NOTE: `if __name__ == "__main__"` is NOT how to use fastplotlib interactively +# please see our docs for using fastplotlib interactively in ipython and jupyter +if __name__ == "__main__": + print(__doc__) + fpl.loop.run() diff --git a/examples/controllers/sync_all.py b/examples/controllers/sync_all.py new file mode 100644 index 000000000..0683a8827 --- /dev/null +++ b/examples/controllers/sync_all.py @@ -0,0 +1,30 @@ +""" +Sync subplots +============= + +Use one controller for all subplots. +""" + +# test_example = false +# sphinx_gallery_pygfx_docs = 'screenshot' + +import numpy as np +import fastplotlib as fpl + + +xs = np.linspace(0, 2 * np.pi, 100) +ys = np.sin(xs) + +figure = fpl.Figure(shape=(2, 2), controller_ids="sync", size=(700, 560)) + +for subplot in figure: + subplot.add_line(np.column_stack([xs, ys + np.random.normal(scale=0.5, size=100)])) + +figure.show(maintain_aspect=False) + + +# NOTE: `if __name__ == "__main__"` is NOT how to use fastplotlib interactively +# please see our docs for using fastplotlib interactively in ipython and jupyter +if __name__ == "__main__": + print(__doc__) + fpl.loop.run() diff --git a/examples/events/README.rst b/examples/events/README.rst new file mode 100644 index 000000000..8e2deca4b --- /dev/null +++ b/examples/events/README.rst @@ -0,0 +1,4 @@ +Events +====== + +Several examples using events \ No newline at end of file diff --git a/examples/events/cmap_event.py b/examples/events/cmap_event.py new file mode 100644 index 000000000..6cd68f333 --- /dev/null +++ b/examples/events/cmap_event.py @@ -0,0 +1,75 @@ +""" +cmap event +========== + +Add a cmap event handler to multiple graphics. When any one graphic changes the cmap, the cmap of all other graphics +is also changed. + +This also shows how bidirectional events are supported. +""" + +# test_example = false +# sphinx_gallery_pygfx_docs = 'screenshot' + +import numpy as np +import fastplotlib as fpl +import imageio.v3 as iio + +# load images +img1 = iio.imread("imageio:camera.png") +img2 = iio.imread("imageio:moon.png") + +# Create a figure +figure = fpl.Figure( + shape=(2, 2), + size=(700, 560), + names=["camera", "moon", "sine", "cloud"], +) + +# create graphics +figure["camera"].add_image(img1) +figure["moon"].add_image(img2) + +# sine wave +xs = np.linspace(0, 4 * np.pi, 100) +ys = np.sin(xs) + +figure["sine"].add_line(np.column_stack([xs, ys])) + +# make a 2D gaussian cloud +cloud_data = np.random.normal(0, scale=3, size=1000).reshape(500, 2) +figure["cloud"].add_scatter( + cloud_data, + sizes=3, + cmap="plasma", + cmap_transform=np.linalg.norm(cloud_data, axis=1) # cmap transform using distance from origin +) +figure["cloud"].axes.intersection = (0, 0, 0) + +# show the plot +figure.show() + + +# event handler to change the cmap of all graphics when the cmap of any one graphic changes +def cmap_changed(ev: fpl.GraphicFeatureEvent): + # get the new cmap + new_cmap = ev.info["value"] + + # set cmap of the graphics in the other subplots + for subplot in figure: + subplot.graphics[0].cmap = new_cmap + + +for subplot in figure: + # add event handler to the graphic added to each subplot + subplot.graphics[0].add_event_handler(cmap_changed, "cmap") + + +# change the cmap of graphic image, triggers all other graphics to set the cmap +figure["camera"].graphics[0].cmap = "jet" + +# NOTE: `if __name__ == "__main__"` is NOT how to use fastplotlib interactively +# please see our docs for using fastplotlib interactively in ipython and jupyter +if __name__ == "__main__": + print(__doc__) + fpl.loop.run() diff --git a/examples/events/drag_points.py b/examples/events/drag_points.py new file mode 100644 index 000000000..9a91779d4 --- /dev/null +++ b/examples/events/drag_points.py @@ -0,0 +1,99 @@ +""" +Drag points +=========== + +Example where you can drag scatter points on a line. This example also demonstrates how you can use a shared buffer +between two graphics to represent the same data using different graphics. When you update the data of one graphic the +data of the other graphic is also changed simultaneously since they use the same underlying buffer on the GPU. + +""" + +# test_example = false +# sphinx_gallery_pygfx_docs = 'screenshot' + +import numpy as np +import fastplotlib as fpl +import pygfx + +xs = np.linspace(0, 2 * np.pi, 10) +ys = np.sin(xs) + +data = np.column_stack([xs, ys]) + +figure = fpl.Figure(size=(700, 560)) + +# add a line +line_graphic = figure[0, 0].add_line(data) + +# add a scatter, share the line graphic buffer! +scatter_graphic = figure[0, 0].add_scatter(data=line_graphic.data, sizes=25, colors="r") + +is_moving = False +vertex_index = None + + +@scatter_graphic.add_event_handler("pointer_down") +def start_drag(ev: pygfx.PointerEvent): + global is_moving + global vertex_index + + if ev.button != 1: + return + + is_moving = True + vertex_index = ev.pick_info["vertex_index"] + scatter_graphic.colors[vertex_index] = "cyan" + + +@figure.renderer.add_event_handler("pointer_move") +def move_point(ev): + global is_moving + global vertex_index + + # if not moving, return + if not is_moving: + return + + # disable controller + figure[0, 0].controller.enabled = False + + # map x, y from screen space to world space + pos = figure[0, 0].map_screen_to_world(ev) + + if pos is None: + # end movement + is_moving = False + scatter_graphic.colors[vertex_index] = "r" # reset color + vertex_index = None + return + + # change scatter data + # since we are sharing the buffer, the line data will also change + scatter_graphic.data[vertex_index, :-1] = pos[:-1] + + # re-enable controller + figure[0, 0].controller.enabled = True + + +@figure.renderer.add_event_handler("pointer_up") +def end_drag(ev: pygfx.PointerEvent): + global is_moving + global vertex_index + + # end movement + if is_moving: + # reset color + scatter_graphic.colors[vertex_index] = "r" + + is_moving = False + vertex_index = None + + +figure.show() + + +# NOTE: `if __name__ == "__main__"` is NOT how to use fastplotlib interactively +# please see our docs for using fastplotlib interactively in ipython and jupyter +if __name__ == "__main__": + print(__doc__) + fpl.loop.run() diff --git a/examples/misc/simple_event.py b/examples/events/image_click.py similarity index 58% rename from examples/misc/simple_event.py rename to examples/events/image_click.py index e382f04b5..acb6cde37 100644 --- a/examples/misc/simple_event.py +++ b/examples/events/image_click.py @@ -1,14 +1,15 @@ """ -Simple Event -============ +Image click event +================= -Example showing how to add a simple callback event. +Example showing how to use a click event on an image. """ # test_example = false # sphinx_gallery_pygfx_docs = 'screenshot' import fastplotlib as fpl +import pygfx import imageio.v3 as iio data = iio.imread("imageio:camera.png") @@ -16,32 +17,21 @@ # Create a figure figure = fpl.Figure(size=(700, 560)) -# plot sine wave, use a single color -image_graphic = figure[0,0].add_image(data=data) +# create image graphic +image_graphic = figure[0, 0].add_image(data=data) # show the plot figure.show() -# define callback function to print the event data -def callback_func(event_data): - print(event_data.info) - - -# Will print event data when the color changes -image_graphic.add_event_handler(callback_func, "cmap") - -image_graphic.cmap = "viridis" - - # adding a click event, we can also use decorators to add event handlers @image_graphic.add_event_handler("click") -def click_event(event_data): +def click_event(ev: pygfx.PointerEvent): # get the click location in screen coordinates - xy = (event_data.x, event_data.y) + xy = (ev.x, ev.y) # map the screen coordinates to world coordinates - xy = figure[0,0].map_screen_to_world(xy)[:-1] + xy = figure[0, 0].map_screen_to_world(xy)[:-1] # print the click location print(xy) diff --git a/examples/events/image_data_event.py b/examples/events/image_data_event.py new file mode 100644 index 000000000..32f78996c --- /dev/null +++ b/examples/events/image_data_event.py @@ -0,0 +1,56 @@ +""" +Image data event +================ + +Example showing how to add an event handler to an ImageGraphic to capture when the data changes. +""" + +# test_example = false +# sphinx_gallery_pygfx_docs = 'screenshot' + +import fastplotlib as fpl +import imageio.v3 as iio +from scipy.ndimage import gaussian_filter + +rgb_weights = [0.299, 0.587, 0.114] + +# load images, convert to grayscale +img1 = iio.imread("imageio:wikkie.png") @ rgb_weights +img2 = iio.imread("imageio:astronaut.png") @ rgb_weights + +# Create a figure +figure = fpl.Figure( + shape=(1, 2), + size=(700, 560), + names=["image", "gaussian filtered image"] +) + +# create image graphics +image_raw = figure[0, 0].add_image(img1) +image_filt = figure[0, 1].add_image(gaussian_filter(img1, sigma=5)) + +# show the plot +figure.show() + + +# add event handler +@image_raw.add_event_handler("data") +def data_changed(ev: fpl.GraphicFeatureEvent): + # get the new image data + new_img = ev.info["value"] + + # set the filtered image graphic + image_filt.data = gaussian_filter(new_img, sigma=5) + + +# set the data on the first image graphic +# this will trigger the `data_changed()` handler to be called +image_raw.data = img2 + + +# NOTE: `if __name__ == "__main__"` is NOT how to use fastplotlib interactively +# please see our docs for using fastplotlib interactively in ipython and jupyter +if __name__ == "__main__": + print(__doc__) + fpl.loop.run() + diff --git a/examples/events/key_events.py b/examples/events/key_events.py new file mode 100644 index 000000000..6979d44d7 --- /dev/null +++ b/examples/events/key_events.py @@ -0,0 +1,85 @@ +""" +Key Events +========== + +Move an image around using and change some of its properties using keyboard events. + +- Use the arrows keys to move the image by changing its offset + +- Press "v", "g", "p" to change the colormaps (viridis, grey, plasma). + +- Press "r" to rotate the image +18 degrees (pi / 10 radians) +- Press "Shift + R" to rotate the image -18 degrees +- Axis of rotation is the origin + +- Press "-", "=" to decrease/increase the vmin +- Press "_", "+" to decrease/increase the vmax + +We use the ImageWidget here because the histogram LUT tool makes it easy to see the changes in vmin and vmax. +""" + +# test_example = false +# sphinx_gallery_pygfx_docs = 'screenshot' + +import numpy as np +import fastplotlib as fpl +import pygfx +import imageio.v3 as iio + +data = iio.imread("imageio:camera.png") + +iw = fpl.ImageWidget(data, figure_kwargs={"size": (700, 560)}) + +image = iw.managed_graphics[0] + + +@iw.figure.renderer.add_event_handler("key_down") +def handle_event(ev: pygfx.KeyboardEvent): + match ev.key: + # change the cmap + case "v": + image.cmap = "viridis" + case "g": + image.cmap = "grey" + case "p": + image.cmap = "plasma" + + # keys to change vmin/vmax + case "-": + image.vmin -= 1 + case "=": + image.vmin += 1 + case "_": + image.vmax -= 1 + case "+": + image.vmax += 1 + + # rotate + case "r": + image.rotate(np.pi / 10, axis="z") + case "R": + image.rotate(-np.pi / 10, axis="z") + + # arrow key events to move the image + case "ArrowUp": + image.offset = image.offset + [0, -10, 0] # remember y-axis is flipped for images + case "ArrowDown": + image.offset = image.offset + [0, 10, 0] + case "ArrowLeft": + image.offset = image.offset + [-10, 0, 0] + case "ArrowRight": + image.offset = image.offset + [10, 0, 0] + + +iw.show() + + +figure = iw.figure # ignore, this is just so the docs gallery scraper picks up the figure + + +# NOTE: `if __name__ == "__main__"` is NOT how to use fastplotlib interactively +# please see our docs for using fastplotlib interactively in ipython and jupyter +if __name__ == "__main__": + print(__doc__) + fpl.loop.run() + diff --git a/examples/events/line_data_thickness_event.py b/examples/events/line_data_thickness_event.py new file mode 100644 index 000000000..4baaba42c --- /dev/null +++ b/examples/events/line_data_thickness_event.py @@ -0,0 +1,79 @@ +""" +Events line data thickness +========================== + +Simple example of adding event handlers for line data and thickness. +""" + +# test_example = false +# sphinx_gallery_pygfx_docs = 'screenshot' + +import fastplotlib as fpl +import numpy as np + +figure = fpl.Figure(size=(700, 560)) + +xs = np.linspace(0, 4 * np.pi, 100) +# sine wave +ys = np.sin(xs) +sine = np.column_stack([xs, ys]) + +# cosine wave +ys = np.cos(xs) +cosine = np.column_stack([xs, ys]) + +# create line graphics +sine_graphic = figure[0, 0].add_line(data=sine) +cosine_graphic = figure[0, 0].add_line(data=cosine, offset=(0, 4, 0)) + +# make a list of the line graphics for convenience +lines = [sine_graphic, cosine_graphic] + + +def change_thickness(ev: fpl.GraphicFeatureEvent): + # sets thickness of all the lines + new_value = ev.info["value"] + + for g in lines: + g.thickness = new_value + + +def change_data(ev: fpl.GraphicFeatureEvent): + # sets data of all the lines using the given event and value from the event + + # the user's slice/index + # This can be a single int index, a slice, + # or even a numpy array of int or bool for fancy indexing! + indices = ev.info["key"] + + # the new values to set at the given indices + new_values = ev.info["value"] + + # set the data for all the lines + for g in lines: + g.data[indices] = new_values + + +# add the event handlers to the line graphics +for g in lines: + g.add_event_handler(change_thickness, "thickness") + g.add_event_handler(change_data, "data") + + +figure.show() +figure[0, 0].axes.intersection = (0, 0, 0) + +# set the y-value of the middle 40 points of the sine graphic to 1 +# after the sine_graphic sets its data, the event handlers will be called +# and therefore the cosine graphic will also set its data using the event data +sine_graphic.data[30:70, 1] = np.ones(40) + +# set the thickness of the cosine graphic, this will trigger an event +# that causes the sine graphic's thickness to also be set from this value +cosine_graphic.thickness = 10 + +# NOTE: `if __name__ == "__main__"` is NOT how to use fastplotlib interactively +# please see our docs for using fastplotlib interactively in ipython and jupyter +if __name__ == "__main__": + print(__doc__) + fpl.loop.run() diff --git a/examples/events/lines_mouse_nearest.py b/examples/events/lines_mouse_nearest.py new file mode 100644 index 000000000..8c9601de6 --- /dev/null +++ b/examples/events/lines_mouse_nearest.py @@ -0,0 +1,62 @@ +""" +Highlight nearest circle +======================== + +Shows how to use the "pointer_move" event to get the nearest circle and highlight it. + +""" + +# test_example = false +# sphinx_gallery_pygfx_docs = 'screenshot' + +from itertools import product +import numpy as np +import fastplotlib as fpl +import pygfx + + +def make_circle(center, radius: float, n_points: int) -> np.ndarray: + theta = np.linspace(0, 2 * np.pi, n_points) + xs = radius * np.cos(theta) + ys = radius * np.sin(theta) + + return np.column_stack([xs, ys]) + center + +spatial_dims = (100, 100) + +circles = list() +for center in product(range(0, spatial_dims[0], 15), range(0, spatial_dims[1], 15)): + circles.append(make_circle(center, 5, n_points=75)) + +pos_xy = np.vstack(circles) + +figure = fpl.Figure(size=(700, 560)) + +line_collection = figure[0, 0].add_line_collection(circles, colors="w", thickness=5) + + +@figure.renderer.add_event_handler("pointer_move") +def highlight_nearest(ev: pygfx.PointerEvent): + line_collection.colors = "w" + + pos = figure[0, 0].map_screen_to_world(ev) + if pos is None: + return + + # get_nearest_graphics() is a helper function + # sorted the passed array or collection of graphics from nearest to furthest from the passed `pos` + nearest = fpl.utils.get_nearest_graphics(pos, line_collection)[0] + + nearest.colors = "r" + + +# remove clutter +figure[0, 0].axes.visible = False + +figure.show() + +# NOTE: `if __name__ == "__main__"` is NOT how to use fastplotlib interactively +# please see our docs for using fastplotlib interactively in ipython and jupyter +if __name__ == "__main__": + print(__doc__) + fpl.loop.run() diff --git a/examples/events/paint_image.py b/examples/events/paint_image.py new file mode 100644 index 000000000..cfc2eda11 --- /dev/null +++ b/examples/events/paint_image.py @@ -0,0 +1,71 @@ +""" +Paint an Image +============== + +Click and drag the mouse to paint in the image +""" + +# test_example = false +# sphinx_gallery_pygfx_docs = 'screenshot' + +import numpy as np +import fastplotlib as fpl +import pygfx + +figure = fpl.Figure(size=(700, 560)) + +# add a blank image +image = figure[0, 0].add_image(np.zeros((100, 100)), vmin=0, vmax=255) + +painting = False # use to toggle painting state + + +@image.add_event_handler("pointer_down") +def on_pointer_down(ev: pygfx.PointerEvent): + # start painting when mouse button is down + global painting + + # get image element index, (x, y) pos corresponds to array (column, row) + col, row = ev.pick_info["index"] + + # increase value of this image element + image.data[row, col] = np.clip(image.data[row, col] + 50, 0, 255) + + # toggle on painting state + painting = True + + # disable controller until painting stops when mouse button is un-clicked + figure[0, 0].controller.enabled = False + + +@image.add_event_handler("pointer_move") +def on_pointer_move(ev: pygfx.PointerEvent): + # continue painting when mouse pointer is moved + global painting + + if not painting: + return + + # get image element index, (x, y) pos corresponds to array (column, row) + col, row = ev.pick_info["index"] + + image.data[row, col] = np.clip(image.data[row, col] + 50, 0, 255) + + +@figure.renderer.add_event_handler("pointer_up") +def on_pointer_up(ev: pygfx.PointerEvent): + # toggle off painting state + global painting + painting = False + + # re-enable controller + figure[0, 0].controller.enabled = True + + +figure.show() + +# NOTE: `if __name__ == "__main__"` is NOT how to use fastplotlib interactively +# please see our docs for using fastplotlib interactively in ipython and jupyter +if __name__ == "__main__": + print(__doc__) + fpl.loop.run() diff --git a/examples/events/scatter_click.py b/examples/events/scatter_click.py new file mode 100644 index 000000000..e56dca743 --- /dev/null +++ b/examples/events/scatter_click.py @@ -0,0 +1,66 @@ +""" +Scatter click +============= + +Add an event handler to click on scatter points and highlight them, i.e. change the color and size of the clicked point. +Fly around the 3D scatter using WASD keys and click on points to highlight them +""" + +# test_example = false +# sphinx_gallery_pygfx_docs = 'screenshot' + +import numpy as np +import fastplotlib as fpl +import pygfx + +# make a gaussian cloud +data = np.random.normal(loc=0, scale=3, size=1500).reshape(500, 3) + +figure = fpl.Figure(cameras="3d", size=(700, 560)) + +scatter = figure[0, 0].add_scatter( + data, # the gaussian cloud + sizes=10, # some big points that are easy to click + cmap="viridis", + cmap_transform=np.linalg.norm(data, axis=1) # color points using distance from origin +) + +# simple dict to restore the original scatter color and size +# of the previously clicked point upon clicking a new point +old_props = {"index": None, "size": None, "color": None} + + +@scatter.add_event_handler("click") +def highlight_point(ev: pygfx.PointerEvent): + global old_props + + # the index of the point that was just clicked + new_index = ev.pick_info["vertex_index"] + + # restore old point's properties + if old_props["index"] is not None: + old_index = old_props["index"] + if new_index == old_index: + # same point was clicked, ignore + return + scatter.colors[old_index] = old_props["color"] + scatter.sizes[old_index] = old_props["size"] + + # store the current property values of this new point + old_props["index"] = new_index + old_props["color"] = scatter.colors[new_index].copy() # if you do not copy you will just get a view of the array! + old_props["size"] = scatter.sizes[new_index] + + # highlight this new point + scatter.colors[new_index] = "magenta" + scatter.sizes[new_index] = 20 + + +figure.show() + + +# NOTE: `if __name__ == "__main__"` is NOT how to use fastplotlib interactively +# please see our docs for using fastplotlib interactively in ipython and jupyter +if __name__ == "__main__": + print(__doc__) + fpl.loop.run() diff --git a/examples/events/scatter_hover.py b/examples/events/scatter_hover.py new file mode 100644 index 000000000..9d69dc24c --- /dev/null +++ b/examples/events/scatter_hover.py @@ -0,0 +1,69 @@ +""" +Scatter hover +============= + +Add an event handler to hover on scatter points and highlight them, i.e. change the color and size of the clicked point. +Fly around the 3D scatter using WASD keys and click on points to highlight them. + +There is no "hover" event, you can create a hover effect by using "pointer_move" events. +""" + +# test_example = false +# sphinx_gallery_pygfx_docs = 'screenshot' + +import numpy as np +import fastplotlib as fpl +import pygfx + +# make a gaussian cloud +data = np.random.normal(loc=0, scale=3, size=1500).reshape(500, 3) + +figure = fpl.Figure(cameras="3d", size=(700, 560)) + +scatter = figure[0, 0].add_scatter( + data, # the gaussian cloud + sizes=10, # some big points that are easy to click + cmap="viridis", + cmap_transform=np.linalg.norm(data, axis=1) # color points using distance from origin +) + +# simple dict to restore the original scatter color and size +# of the previously clicked point upon clicking a new point +old_props = {"index": None, "size": None, "color": None} + + +@scatter.add_event_handler("pointer_move") +def highlight_point(ev: pygfx.PointerEvent): + global old_props + + # the index of the point that was just entered + new_index = ev.pick_info["vertex_index"] + + # if a new point has been entered, but we have not yet had + # a leave event for the previous point, then reset this old point + if old_props["index"] is not None: + old_index = old_props["index"] + if new_index == old_index: + # same point, ignore + return + scatter.colors[old_index] = old_props["color"] + scatter.sizes[old_index] = old_props["size"] + + # store the current property values of this new point + old_props["index"] = new_index + old_props["color"] = scatter.colors[new_index].copy() # if you do not copy you will just get a view of the array! + old_props["size"] = scatter.sizes[new_index] + + # highlight this new point + scatter.colors[new_index] = "magenta" + scatter.sizes[new_index] = 20 + + +figure.show() + + +# NOTE: `if __name__ == "__main__"` is NOT how to use fastplotlib interactively +# please see our docs for using fastplotlib interactively in ipython and jupyter +if __name__ == "__main__": + print(__doc__) + fpl.loop.run() diff --git a/examples/events/scatter_hover_transforms.py b/examples/events/scatter_hover_transforms.py new file mode 100644 index 000000000..7f9fbb9ff --- /dev/null +++ b/examples/events/scatter_hover_transforms.py @@ -0,0 +1,126 @@ +""" +Scatter data explore scalers +============================ + +Based on the sklearn preprocessing scalers example. Hover points to highlight the corresponding point of the dataset +transformed by the various scalers. + +See: https://scikit-learn.org/stable/auto_examples/preprocessing/plot_all_scaling.html + +This is another example that uses bi-directional events. +""" + +# test_example = false +# sphinx_gallery_pygfx_docs = 'screenshot' + +from sklearn.datasets import fetch_california_housing +from sklearn.preprocessing import ( + Normalizer, + QuantileTransformer, + PowerTransformer, +) + +import fastplotlib as fpl +import pygfx + +# get the dataset +dataset = fetch_california_housing() +X_full, y = dataset.data, dataset.target +feature_names = dataset.feature_names + +feature_mapping = { + "MedInc": "Median income in block", + "HouseAge": "Median house age in block", + "AveRooms": "Average number of rooms", + "AveBedrms": "Average number of bedrooms", + "Population": "Block population", + "AveOccup": "Average house occupancy", + "Latitude": "House block latitude", + "Longitude": "House block longitude", +} + +# Take only 2 features to make visualization easier +# Feature MedInc has a long tail distribution. +# Feature AveOccup has a few but very large outliers. +features = ["MedInc", "AveOccup"] +features_idx = [feature_names.index(feature) for feature in features] +X = X_full[:, features_idx] + +# list of our scalers and their names as strings +scalers = [PowerTransformer, QuantileTransformer, Normalizer] +names = ["Original Data", *[s.__name__ for s in scalers]] + +# fastplotlib code starts here, make a figure +figure = fpl.Figure( + shape=(2, 2), + names=names, + size=(700, 780), +) + +scatters = list() # list to store our 4 scatter graphics for convenience + +# add a scatter of the original data +s = figure["Original Data"].add_scatter( + data=X, + cmap="viridis", + cmap_transform=y, + sizes=3, +) + +# append to list of scatters +scatters.append(s) + +# add the scaled data as scatter graphics +for scaler in scalers: + name = scaler.__name__ + s = figure[name].add_scatter(scaler().fit_transform(X), cmap="viridis", cmap_transform=y, sizes=3) + scatters.append(s) + + +# simple dict to restore the original scatter color and size +# of the previously clicked point upon clicking a new point +old_props = {"index": None, "size": None, "color": None} + + +def highlight_point(ev: pygfx.PointerEvent): + # event handler to highlight the point when the mouse moves over it + global old_props + + # the index of the point that was just clicked + new_index = ev.pick_info["vertex_index"] + + # restore old point's properties + if old_props["index"] is not None: + old_index = old_props["index"] + if new_index == old_index: + # same point was clicked, ignore + return + for s in scatters: + s.colors[old_index] = old_props["color"] + s.sizes[old_index] = old_props["size"] + + # store the current property values of this new point + old_props["index"] = new_index + # all the scatters have the same colors and size for the corresponding index + # so we can just use the first scatter's original color and size + old_props["color"] = scatters[0].colors[new_index].copy() # if you do not copy you will just get a view of the array! + old_props["size"] = scatters[0].sizes[new_index] + + # highlight this new point + for s in scatters: + s.colors[new_index] = "magenta" + s.sizes[new_index] = 15 + + +# add the event handler to all the scatter graphics +for s in scatters: + s.add_event_handler(highlight_point, "pointer_move") + + +figure.show(maintain_aspect=False) + +# NOTE: `if __name__ == "__main__"` is NOT how to use fastplotlib interactively +# please see our docs for using fastplotlib interactively in ipython and jupyter +if __name__ == "__main__": + print(__doc__) + fpl.loop.run() diff --git a/examples/gridplot/README.rst b/examples/gridplot/README.rst index 486e708e7..0a2cc1828 100644 --- a/examples/gridplot/README.rst +++ b/examples/gridplot/README.rst @@ -1,2 +1,2 @@ -GridPlot Examples -================= +Grid layout Examples +==================== diff --git a/examples/gridplot/gridplot.py b/examples/gridplot/gridplot.py index 5c38d6d43..af4d82408 100644 --- a/examples/gridplot/gridplot.py +++ b/examples/gridplot/gridplot.py @@ -1,8 +1,8 @@ """ -GridPlot Simple -=============== +Grid layout Simple +================== -Example showing simple 2x2 GridPlot with Standard images from imageio. +Example showing simple 2x2 grid layout with standard images from imageio. """ # test_example = true diff --git a/examples/gridplot/gridplot_non_square.py b/examples/gridplot/gridplot_non_square.py index 0277bcccd..e8ce15b7b 100644 --- a/examples/gridplot/gridplot_non_square.py +++ b/examples/gridplot/gridplot_non_square.py @@ -1,8 +1,8 @@ """ -GridPlot Non-Square Example -=========================== +Grid Layout 2 +============= -Example showing simple 2x2 GridPlot with Standard images from imageio. +Simple 2x2 grid layout Figure with standard images from imageio, one subplot is left empty """ # test_example = true diff --git a/examples/gridplot/gridplot_viewports_check.py b/examples/gridplot/gridplot_viewports_check.py index 99584b411..496204b98 100644 --- a/examples/gridplot/gridplot_viewports_check.py +++ b/examples/gridplot/gridplot_viewports_check.py @@ -1,6 +1,6 @@ """ -GridPlot test viewport rects -============================ +Grid layout test viewport rects +=============================== Test figure to test that viewport rects are positioned correctly """ diff --git a/examples/gridplot/multigraphic_gridplot.py b/examples/gridplot/multigraphic_gridplot.py index 1bed60b31..8408f4f23 100644 --- a/examples/gridplot/multigraphic_gridplot.py +++ b/examples/gridplot/multigraphic_gridplot.py @@ -1,8 +1,8 @@ """ -Multi-Graphic GridPlot -====================== +Multi-Graphic Grid layout +========================= -Example showing a Figure with multiple subplots and multiple graphic types. +A Figure with multiple subplots and multiple graphic types. """ # test_example = false diff --git a/examples/guis/sine_cosine_funcs.py b/examples/guis/sine_cosine_funcs.py new file mode 100644 index 000000000..c91a3b2e8 --- /dev/null +++ b/examples/guis/sine_cosine_funcs.py @@ -0,0 +1,186 @@ +""" +Sine and Cosine functions +========================= + +Identical to the Unit Circle example but you can change the angular frequencies using a UI + +""" + +# test_example = false +# sphinx_gallery_pygfx_docs = 'screenshot' + +import glfw +import numpy as np +import fastplotlib as fpl +from fastplotlib.ui import EdgeWindow +from imgui_bundle import imgui + + +# initial frequency coefficients for sine and cosine functions +P = 1 +Q = 1 + + +# helper function to make a circle +def make_circle(center, radius: float, p, q, n_points: int) -> np.ndarray: + theta = np.linspace(0, 2 * np.pi, n_points) + xs = radius * np.cos(theta * p) + ys = radius * np.sin(theta * q) + + return np.column_stack([xs, ys]) + center + + +# we can define this layout using "extents", i.e. min and max ranges on the canvas +# (x_min, x_max, y_min, y_max) +# extents can be defined as fractions as shown here +extents = [ + (0, 0.5, 0, 1), # circle subplot + (0.5, 1, 0, 0.5), # sine subplot + (0.5, 1, 0.5, 1), # cosine subplot +] + +# create a figure with 3 subplots +figure = fpl.Figure( + extents=extents, + names=["circle", "sin", "cos"], + size=(700, 560) +) + +# set more descriptive figure titles +figure["circle"].title = "sin(x*p) over cos(x*q)" +figure["sin"].title = "sin(x * p)" +figure["cos"].title = "cos(x * q)" + +# set the axes to intersect at (0, 0, 0) to better illustrate the unit circle +for subplot in figure: + subplot.axes.intersection = (0, 0, 0) + subplot.toolbar = False # reduce clutter + +figure["sin"].camera.maintain_aspect = False +figure["cos"].camera.maintain_aspect = False + +# create sine and cosine data +xs = np.linspace(0, 2 * np.pi, 360) +sine = np.sin(xs * P) +cosine = np.cos(xs * Q) + +# circle data +circle_data = make_circle(center=(0, 0), p=P, q=Q, radius=1, n_points=360) + +# make the circle line graphic, set the cmap transform using the sine function +circle_graphic = figure["circle"].add_line( + circle_data, thickness=4, cmap="bwr", cmap_transform=sine +) + +# line to show the circle radius +# use it to indicate the current position of the sine and cosine selctors (below) +radius_data = np.array([[0, 0, 0], [*circle_data[0], 0]]) +circle_radius_graphic = figure["circle"].add_line( + radius_data, thickness=6, colors="magenta" +) + +# sine line graphic, cmap transform set from the sine function +sine_graphic = figure["sin"].add_line( + sine, thickness=10, cmap="bwr", cmap_transform=sine +) + +# cosine line graphic, cmap transform set from the sine function +# illustrates the sine function values on the cosine graphic +cosine_graphic = figure["cos"].add_line( + cosine, thickness=10, cmap="bwr", cmap_transform=sine +) + +# add linear selectors to the sine and cosine line graphics +sine_selector = sine_graphic.add_linear_selector() +cosine_selector = cosine_graphic.add_linear_selector() + + +def set_circle_cmap(ev): + # sets the cmap transforms + + cmap_transform = ev.graphic.data[:, 1] # y-val data of the sine or cosine graphic + for g in [sine_graphic, cosine_graphic]: + g.cmap.transform = cmap_transform + + # set circle cmap transform + circle_graphic.cmap.transform = cmap_transform + +# when the sine or cosine graphic is clicked, the cmap_transform +# of the sine, cosine and circle line graphics are all set from +# the y-values of the clicked line +sine_graphic.add_event_handler(set_circle_cmap, "click") +cosine_graphic.add_event_handler(set_circle_cmap, "click") + + +def set_x_val(ev): + # used to sync the two selectors + value = ev.info["value"] + index = ev.get_selected_index() + + sine_selector.selection = value + cosine_selector.selection = value + + circle_radius_graphic.data[1, :-1] = circle_data[index] + +# add same event handler to both graphics +sine_selector.add_event_handler(set_x_val, "selection") +cosine_selector.add_event_handler(set_x_val, "selection") + +# initial selection value +sine_selector.selection = 50 + + +class GUIWindow(EdgeWindow): + def __init__(self, figure, size, location, title): + super().__init__(figure=figure, size=size, location=location, title=title) + + self._p = 1 + self._q = 1 + + def _set_data(self): + global sine_graphic, cosine_graphic, circle_graphic, circle_radius_graphic, circle_data + + # make new data + sine = np.sin(xs * self._p) + cosine = np.cos(xs * self._q) + circle_data = make_circle(center=(0, 0), p=self._p, q=self._q, radius=1, n_points=360) + + + # set the graphics + sine_graphic.data[:, 1] = sine + cosine_graphic.data[:, 1] = cosine + circle_graphic.data[:, :2] = circle_data + circle_radius_graphic.data[1, :-1] = circle_data[sine_selector.get_selected_index()] + + def update(self): + flag_set_data = False + + changed, self._p = imgui.input_int("P", v=self._p, step_fast=2) + if changed: + flag_set_data = True + + changed, self._q = imgui.input_int("Q", v=self._q, step_fast=2) + if changed: + flag_set_data = True + + if flag_set_data: + self._set_data() + + +gui = GUIWindow( + figure=figure, + size=100, + location="right", + title="Freq. coeffs" +) + +figure.add_gui(gui) + +figure.show() + + +# NOTE: `if __name__ == "__main__"` is NOT how to use fastplotlib interactively +# please see our docs for using fastplotlib interactively in ipython and jupyter +if __name__ == "__main__": + print(__doc__) + fpl.loop.run() diff --git a/examples/image_widget/image_widget_single_video.py b/examples/image_widget/image_widget_single_video.py index 3a0e94fca..aa601d3c1 100644 --- a/examples/image_widget/image_widget_single_video.py +++ b/examples/image_widget/image_widget_single_video.py @@ -20,7 +20,7 @@ movie_sub = movie[:15, ::12, ::12].copy() del movie -iw = fpl.ImageWidget(movie_sub, rgb=[True], figure_kwargs={"size": (700, 560)}) +iw = fpl.ImageWidget(movie_sub, rgb=True, figure_kwargs={"size": (700, 560)}) # ImageWidget supports setting window functions the `time` "t" or `volume` "z" dimension # These can also be given as kwargs to `ImageWidget` during instantiation diff --git a/examples/ipywidgets/README.rst b/examples/ipywidgets/README.rst new file mode 100644 index 000000000..3f6ae9d5f --- /dev/null +++ b/examples/ipywidgets/README.rst @@ -0,0 +1,2 @@ +Using with ipywidgets +===================== diff --git a/examples/ipywidgets/ipywidgets_modify_image.py b/examples/ipywidgets/ipywidgets_modify_image.py new file mode 100644 index 000000000..c0206e945 --- /dev/null +++ b/examples/ipywidgets/ipywidgets_modify_image.py @@ -0,0 +1,69 @@ +""" +ipwidgets modify an ImageGraphic +================================ + +Use ipywidgets to modify some features of an ImageGraphic. Run in jupyterlab. +""" + +# test_example = false +# sphinx_gallery_pygfx_docs = 'code' + +import fastplotlib as fpl +from scipy.ndimage import gaussian_filter +import imageio.v3 as iio +from ipywidgets import FloatRangeSlider, FloatSlider, Select, VBox + +data = iio.imread("imageio:moon.png") + +iw = fpl.ImageWidget(data, figure_kwargs={"size": (700, 560)}) + +# get the ImageGraphic from the image widget +image = iw.managed_graphics[0] + +min_v, max_v = fpl.utils.quick_min_max(data) + +# slider to adjust vmin, vmax of the image +vmin_vmax_slider = FloatRangeSlider(value=(image.vmin, image.vmax), min=min_v, max=max_v, description="vmin, vmax:") + +# slider to adjust sigma of a gaussian kernel used to filter the image (i.e. gaussian blur) +slider_sigma = FloatSlider(min=0.0, max=10.0, value=0.0, description="σ: ") + +# select box to choose the sample image shown in the ImageWidget +select_image = Select(options=["moon.png", "camera.png", "checkerboard.png"], description="image: ") + + +def update_vmin_vmax(change): + vmin, vmax = change["new"] + + image = iw.managed_graphics[0] + image.vmin, image.vmax = vmin, vmax + + +def update_sigma(change): + sigma = change["new"] + + # set a "frame apply" dict onto the ImageWidget + # this maps {image_index: function} + # the function is applied to the image data at the image index given by the key + iw.frame_apply = {0: lambda image_data: gaussian_filter(image_data, sigma=sigma)} + + +def update_image(change): + filename = change["new"] + data = iio.imread(f"imageio:{filename}") + + iw.set_data(data) + + # set vmin, vmax sliders w.r.t. this new image + image = iw.managed_graphics[0] + vmin_vmax_slider.value = image.vmin, image.vmax + vmin_vmax_slider.min, vmin_vmax_slider.max = fpl.utils.quick_min_max(data) + + +# connect the ipywidgets to the handler functions +vmin_vmax_slider.observe(update_vmin_vmax, "value") +slider_sigma.observe(update_sigma, "value") +select_image.observe(update_image, "value") + +# display in a vbox +VBox([iw.show(), vmin_vmax_slider, slider_sigma, select_image]) diff --git a/examples/ipywidgets/ipywidgets_sliders_line.py b/examples/ipywidgets/ipywidgets_sliders_line.py new file mode 100644 index 000000000..8288e5719 --- /dev/null +++ b/examples/ipywidgets/ipywidgets_sliders_line.py @@ -0,0 +1,91 @@ +""" +ipywidget sliders to modify a sine wave +======================================= + +Example with ipywidgets sliders to change a sine wave and view the frequency spectra. You can run this in jupyterlab +""" + +# test_example = false +# sphinx_gallery_pygfx_docs = 'code' + +import numpy as np +import fastplotlib as fpl +from ipywidgets import FloatSlider, Checkbox, VBox + + +def generate_data(freq, duration, sampling_rate, ampl, noise_sigma): + # generate a sine wave using given params + xs = np.linspace(0, duration, sampling_rate * duration) + ys = np.sin((2 * np.pi) * freq * xs) * ampl + + noise = np.random.normal(scale=noise_sigma, size=sampling_rate * duration) + + signal = np.column_stack([xs, ys + noise]) + fft_mag = np.abs(np.fft.rfft(signal[:, 1])) + fft_freqs = np.linspace(0, sampling_rate / 2, num=fft_mag.shape[0]) + + return np.column_stack([xs, ys + noise]), np.column_stack([fft_freqs, fft_mag]) + + +signal, fft = generate_data( + freq=1, + duration=10, + sampling_rate=50, + ampl=1, + noise_sigma=0.05 +) + +# create a figure +figure = fpl.Figure(shape=(2, 1), names=["signal", "fft"], size=(700, 560)) + +# line graphic for the signal +signal_line = figure[0, 0].add_line(signal, thickness=1) + +# easier to understand the frequency of the sine wave if the +# axes go through the middle of the sine wave +figure[0, 0].axes.intersection = (0, 0, 0) + +# line graphic for fft +fft_line = figure[1, 0].add_line(fft) + +# do not maintain the aspect ratio of the fft subplot +figure[1, 0].camera.maintain_aspect = False + +# create ipywidget sliders +slider_freq = FloatSlider(min=0.1, max=10, value=1.0, step=0.1, description="freq: ") +slider_ampl = FloatSlider(min=0.0, max=10, value=1.0, step=0.5, description="ampl: ") +slider_noise = FloatSlider(min=0, max=1, value=0.05, step=0.05, description="noise: ") + +# checkbox +checkbox_autoscale = Checkbox(value=False, description="autoscale: ") + + +def update(*args): + # update whenever a slider changes + freq = slider_freq.value + ampl = slider_ampl.value + noise = slider_noise.value + + signal, fft = generate_data( + freq=freq, + duration=10, + sampling_rate=50, + ampl=ampl, + noise_sigma=noise, + ) + + signal_line.data[:, :-1] = signal + fft_line.data[:, :-1] = fft + + if checkbox_autoscale.value: + for subplot in figure: + subplot.auto_scale(maintain_aspect=False) + + +# when any one slider changes, it calls update +for slider in [slider_freq, slider_ampl, slider_noise]: + slider.observe(update, "value") + +# display the fastplotlib figure and ipywidgets in a VBox (vertically stacked) +# figure.show() just returns an ipywidget object +VBox([figure.show(), slider_freq, slider_ampl, slider_noise, checkbox_autoscale]) diff --git a/examples/line_collection/line_stack.py b/examples/line_collection/line_stack.py index 95b681b76..4f0c6037d 100644 --- a/examples/line_collection/line_stack.py +++ b/examples/line_collection/line_stack.py @@ -19,7 +19,10 @@ data = np.column_stack([xs, ys]) multi_data = np.stack([data] * 10) -figure = fpl.Figure(size=(700, 560)) +figure = fpl.Figure( + size=(700, 560), + show_tooltips=True +) line_stack = figure[0, 0].add_line_stack( multi_data, # shape: (10, 100, 2), i.e. [n_lines, n_points, xy] @@ -28,6 +31,26 @@ separation=1, # spacing between lines along the separation axis, default separation along "y" axis ) + +def tooltip_info(ev): + """A custom function to display the index of the graphic within the collection.""" + index = ev.pick_info["vertex_index"] # index of the line datapoint being hovered + + # get index of the hovered line within the line stack + line_index = np.where(line_stack.graphics == ev.graphic)[0].item() + info = f"line index: {line_index}\n" + + # append data value info + info += "\n".join(f"{dim}: {val}" for dim, val in zip("xyz", ev.graphic.data[index])) + + # return str to display in tooltip + return info + +# register the line stack with the custom tooltip function +figure.tooltip_manager.register( + line_stack, custom_info=tooltip_info +) + figure.show(maintain_aspect=False) diff --git a/examples/misc/multiplot_animation.py b/examples/misc/multiplot_animation.py index 18512add1..4eb9399f8 100644 --- a/examples/misc/multiplot_animation.py +++ b/examples/misc/multiplot_animation.py @@ -2,7 +2,7 @@ Multi-Subplot Image Update ========================== -Example showing updating a multiple subplots with new random 512x512 data. +Multiple subplots with an image that updates with new data on every render. """ # test_example = false @@ -27,7 +27,7 @@ figure[1,1]["rand-img"].cmap = "spring" # Define a function to update the image graphics with new data -# add_animations will pass the gridplot to the animation function +# add_animations will pass the figure to the animation function def update_data(f): for subplot in f: new_data = np.random.rand(512, 512) @@ -37,7 +37,7 @@ def update_data(f): # add the animation function figure.add_animations(update_data) -# show the gridplot +# show the figure figure.show() diff --git a/examples/misc/tooltips.py b/examples/misc/tooltips.py new file mode 100644 index 000000000..4fdae1482 --- /dev/null +++ b/examples/misc/tooltips.py @@ -0,0 +1,54 @@ +""" +Tooltips +======== + +Show tooltips on all graphics +""" + +# test_example = false +# sphinx_gallery_pygfx_docs = 'screenshot' + +import numpy as np +import imageio.v3 as iio +import fastplotlib as fpl + + +# get some data +scatter_data = np.random.rand(1_000, 3) + +xs = np.linspace(0, 2 * np.pi, 100) +ys = np.sin(xs) + +gray = iio.imread("imageio:camera.png") +rgb = iio.imread("imageio:astronaut.png") + +# create a figure +figure = fpl.Figure( + cameras=["3d", "2d", "2d", "2d"], + controller_types=["orbit", "panzoom", "panzoom", "panzoom"], + size=(700, 560), + shape=(2, 2), + show_tooltips=True, # tooltip will display data value info for all graphics +) + +# create graphics +scatter = figure[0, 0].add_scatter(scatter_data, sizes=3, colors="r") +line = figure[0, 1].add_line(np.column_stack([xs, ys])) +image = figure[1, 0].add_image(gray) +image_rgb = figure[1, 1].add_image(rgb) + + +figure.show() + +# to hide tooltips for all graphics in an existing Figure +# figure.show_tooltips = False + +# to show tooltips for all graphics in an existing Figure +# figure.show_tooltips = True + + +# NOTE: `if __name__ == "__main__"` is NOT how to use fastplotlib interactively +# please see our docs for using fastplotlib interactively in ipython and jupyter +if __name__ == "__main__": + print(__doc__) + fpl.loop.run() diff --git a/examples/misc/tooltips_custom.py b/examples/misc/tooltips_custom.py new file mode 100644 index 000000000..a62190906 --- /dev/null +++ b/examples/misc/tooltips_custom.py @@ -0,0 +1,54 @@ +""" +Tooltips Customization +====================== + +Customize the information displayed in a tooltip. This example uses the Iris dataset and sets the tooltip to display +the species and cluster label of the point that is being hovered by the mouse pointer. +""" + +# test_example = false +# sphinx_gallery_pygfx_docs = 'screenshot' + + +import fastplotlib as fpl +from sklearn.cluster import AgglomerativeClustering +from sklearn import datasets + + +figure = fpl.Figure(size=(700, 560)) + +dataset = datasets.load_iris() +data = dataset["data"] + +agg = AgglomerativeClustering(n_clusters=3) +agg.fit_predict(data) + +scatter_graphic = figure[0, 0].add_scatter( + data=data[:, :-1], # use only xy data + sizes=15, + cmap="Set1", + cmap_transform=agg.labels_ # use the labels as a transform to map colors from the colormap +) + + +def tooltip_info(ev) -> str: + # get index of the scatter point that is being hovered + index = ev.pick_info["vertex_index"] + + # get the species name + target = dataset["target"][index] + cluster = agg.labels_[index] + info = f"species: {dataset['target_names'][target]}\ncluster: {cluster}" + + # return this string to display it in the tooltip + return info + + +figure.tooltip_manager.register(scatter_graphic, custom_info=tooltip_info) + +figure.show() + + +if __name__ == "__main__": + print(__doc__) + fpl.loop.run() diff --git a/examples/notebooks/quickstart.ipynb b/examples/notebooks/quickstart.ipynb index 737aee3e7..0d8fc3c31 100644 --- a/examples/notebooks/quickstart.ipynb +++ b/examples/notebooks/quickstart.ipynb @@ -463,7 +463,7 @@ "id": "5694dca1-1041-4e09-a1da-85b293c5af47", "metadata": {}, "source": [ - "### RGB images are also supported\n", + "### RGB(A) images are supported\n", "\n", "`cmap` arguments are ignored for rgb images, but vmin vmax still works" ] @@ -538,7 +538,7 @@ "source": [ "### Image updates\n", "\n", - "This examples show how you can define animation functions that run on every render cycle." + "This example shows how you can define animation functions that run on every render cycle." ] }, { @@ -620,7 +620,7 @@ "id": "f226c9c2-8d0e-41ab-9ab9-1ae31fd91de5", "metadata": {}, "source": [ - "#### Keeping a reference to the Graphic instance, as shown above `image_graphic_instance`, is useful if you're creating something where you need flexibility in the naming of the graphics" + "#### Keeping a reference to the Graphic instance, as shown above `image_graphic_instance`, is useful if you're creating something where it is convenient to keep your own reference to a `Graphic`" ] }, { @@ -628,7 +628,7 @@ "id": "d11fabb7-7c76-4e94-893d-80ed9ee3be3d", "metadata": {}, "source": [ - "### You can also use `ipywidgets.VBox` and `HBox` to stack plots. See the `subplot` notebooks for more automated subplotting" + "### You can also use `ipywidgets.VBox` and `HBox` to stack plots." ] }, { @@ -664,7 +664,7 @@ "\n", "## 2D line plots\n", "\n", - "This example plots a sine wave, cosine wave, and ricker wavelet and demonstrates how **Graphic Features** can be modified by slicing!" + "This example plots a sine wave, cosine wave, and ricker wavelet and demonstrates how **Graphic Properties** can be modified by slicing!" ] }, { @@ -755,7 +755,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 `subplot.camera.maintain_aspect`" + "You can also click the **`⛶`** button to toggle this, or use `subplot.camera.maintain_aspect`" ] }, { @@ -763,7 +763,7 @@ "id": "1651e965-f750-47ac-bf53-c23dae84cc98", "metadata": {}, "source": [ - "### reset the plot area" + "### reset the plot area camera" ] }, { @@ -783,7 +783,9 @@ "id": "dcd68796-c190-4c3f-8519-d73b98ff6367", "metadata": {}, "source": [ - "## Graphic features support slicing! :D " + "## Graphic properties support slicing! :D\n", + "\n", + "Data, colors, and cmaps can often be sliced just like arrays to set or get values!" ] }, { @@ -811,7 +813,7 @@ "id": "c9689887-cdf3-4a4d-948f-7efdb09bde4e", "metadata": {}, "source": [ - "## You can capture changes to a graphic feature as events" + "## Graphic properties are _evented_, so you can capture when they change" ] }, { @@ -1551,7 +1553,7 @@ " 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", + "# add_animations will pass the figure to the animation function\n", "def update_data(f):\n", " for subplot in f:\n", " new_data = np.random.rand(512, 512)\n", @@ -1561,7 +1563,7 @@ "# add the animation function\n", "figure_grid.add_animations(update_data)\n", "\n", - "# show the gridplot\n", + "# show the figure\n", "figure_grid.show()" ] }, @@ -1575,7 +1577,7 @@ } }, "source": [ - "### Slicing GridPlot" + "### Slicing a grid layout to get subplots" ] }, { @@ -1605,7 +1607,7 @@ } }, "source": [ - "You can get the graphics within a subplot, just like with simple `Plot`" + "You can get the graphics within a subplot" ] }, { @@ -1661,7 +1663,7 @@ } }, "source": [ - "more slicing with `GridPlot`" + "more slicing with a `Figure` that has a grid layout" ] }, { @@ -1707,7 +1709,7 @@ }, "outputs": [], "source": [ - "# these are really the same\n", + "# these are the same\n", "figure_grid[\"top-right-plot\"] is figure_grid[0, 2]" ] }, @@ -1749,7 +1751,7 @@ } }, "source": [ - "## Figure subplot customization" + "## Figure subplot customization in a grid layout" ] }, { @@ -1776,13 +1778,13 @@ "]\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", + "# Create the figure\n", "figure_grid = fpl.Figure(\n", " shape=shape,\n", " controller_ids=controller_ids,\n", @@ -1819,7 +1821,7 @@ } }, "source": [ - "Indexing the gridplot to access subplots" + "Slicing/indexing the figure to get subplots" ] }, { @@ -1834,7 +1836,7 @@ }, "outputs": [], "source": [ - "# can access subplot by name\n", + "# get subplot by name\n", "figure_grid[\"subplot0\"]" ] }, @@ -1850,7 +1852,7 @@ }, "outputs": [], "source": [ - "# can access subplot by index\n", + "# or get subplot by index\n", "figure_grid[0, 0]" ] }, @@ -1864,7 +1866,7 @@ } }, "source": [ - "**subplots also support indexing!**\n", + "**from before, remember subplots themselves also support slicing to get graphics within them!**\n", "\n", "this can be used to get graphics if they are named" ] @@ -1885,6 +1887,17 @@ "figure_grid[\"subplot0\"][\"rand-image\"]" ] }, + { + "cell_type": "code", + "execution_count": null, + "id": "87905450bdc0ec0a", + "metadata": {}, + "outputs": [], + "source": [ + "# or by their numerical index\n", + "figure_grid[\"subplot0\"].graphics[0]" + ] + }, { "cell_type": "code", "execution_count": null, @@ -1911,7 +1924,7 @@ } }, "source": [ - "positional indexing also works event if subplots have string names" + "positional indexing also works even if subplots have string names" ] }, { diff --git a/examples/notebooks/screenshots/nb-image-widget-movie-single-0-reset.png b/examples/notebooks/screenshots/nb-image-widget-movie-single-0-reset.png index bb2e1ee37..0129cb423 100644 --- a/examples/notebooks/screenshots/nb-image-widget-movie-single-0-reset.png +++ b/examples/notebooks/screenshots/nb-image-widget-movie-single-0-reset.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6c9b898259fc965452ef0b6ff53ac7fa41196826c6e27b6b5d417d33fb352051 -size 112399 +oid sha256:d3f5a721456b5a54e819fc987b8fa1f61d638f578339a7332ad46a22e7aa8fc0 +size 112674 diff --git a/examples/notebooks/screenshots/nb-image-widget-movie-single-0.png b/examples/notebooks/screenshots/nb-image-widget-movie-single-0.png index bb2e1ee37..0129cb423 100644 --- a/examples/notebooks/screenshots/nb-image-widget-movie-single-0.png +++ b/examples/notebooks/screenshots/nb-image-widget-movie-single-0.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6c9b898259fc965452ef0b6ff53ac7fa41196826c6e27b6b5d417d33fb352051 -size 112399 +oid sha256:d3f5a721456b5a54e819fc987b8fa1f61d638f578339a7332ad46a22e7aa8fc0 +size 112674 diff --git a/examples/notebooks/screenshots/nb-image-widget-movie-single-279.png b/examples/notebooks/screenshots/nb-image-widget-movie-single-279.png index 1841cd237..4908c8b59 100644 --- a/examples/notebooks/screenshots/nb-image-widget-movie-single-279.png +++ b/examples/notebooks/screenshots/nb-image-widget-movie-single-279.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b9cbc2a6916c7518d40812a13276270eb1acfc596f3e6e02e98a6a5185da03a4 -size 132971 +oid sha256:4511a28e728af412f5006bb456f133aea1fdc9c1922c3174f127c79d9878401d +size 133635 diff --git a/examples/notebooks/screenshots/nb-image-widget-movie-single-50-window-max-33.png b/examples/notebooks/screenshots/nb-image-widget-movie-single-50-window-max-33.png index 6cc1821fa..cfdc3c8a9 100644 --- a/examples/notebooks/screenshots/nb-image-widget-movie-single-50-window-max-33.png +++ b/examples/notebooks/screenshots/nb-image-widget-movie-single-50-window-max-33.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:070748e90bd230a01d3ae7c6d6487815926b0158888a52db272356dc8b0a89d7 -size 119453 +oid sha256:c6910106cd799a4327a6650edbc956ddb9b6a489760b86b279c593575ae805b8 +size 120114 diff --git a/examples/notebooks/screenshots/nb-image-widget-movie-single-50-window-mean-13.png b/examples/notebooks/screenshots/nb-image-widget-movie-single-50-window-mean-13.png index 3865aef93..92513cf5b 100644 --- a/examples/notebooks/screenshots/nb-image-widget-movie-single-50-window-mean-13.png +++ b/examples/notebooks/screenshots/nb-image-widget-movie-single-50-window-mean-13.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b24450ccf1f8cf902b8e37e73907186f37a6495f227dcbd5ec53f75c52125f56 -size 105213 +oid sha256:8233dfc429a7fefe96f0fdb89eb2c57188b7963c16db5d1d08f7faefb45d8cb7 +size 105755 diff --git a/examples/notebooks/screenshots/nb-image-widget-movie-single-50-window-mean-33.png b/examples/notebooks/screenshots/nb-image-widget-movie-single-50-window-mean-33.png index 025086930..8bce59baf 100644 --- a/examples/notebooks/screenshots/nb-image-widget-movie-single-50-window-mean-33.png +++ b/examples/notebooks/screenshots/nb-image-widget-movie-single-50-window-mean-33.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3dfc8e978eddf08d1ed32e16fbf93c037ccdf5f7349180dcda54578a8c9e1a18 -size 97359 +oid sha256:a4af684cdaec8f98081862eb8a377cd419efec64cdf08b662a456276b78f1fb5 +size 98091 diff --git a/examples/notebooks/screenshots/nb-image-widget-movie-single-50-window-reset.png b/examples/notebooks/screenshots/nb-image-widget-movie-single-50-window-reset.png index 5ff5052b0..61c3c4f6c 100644 --- a/examples/notebooks/screenshots/nb-image-widget-movie-single-50-window-reset.png +++ b/examples/notebooks/screenshots/nb-image-widget-movie-single-50-window-reset.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:00130242d3f199926604df16dda70a062071f002566a8056e4794805f29adfde -size 118044 +oid sha256:133dfe6b0028dda6248df1afde1288c57625be99b25c8224673597de4d4f70fc +size 118588 diff --git a/examples/notebooks/screenshots/nb-image-widget-movie-single-50.png b/examples/notebooks/screenshots/nb-image-widget-movie-single-50.png index 5ff5052b0..61c3c4f6c 100644 --- a/examples/notebooks/screenshots/nb-image-widget-movie-single-50.png +++ b/examples/notebooks/screenshots/nb-image-widget-movie-single-50.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:00130242d3f199926604df16dda70a062071f002566a8056e4794805f29adfde -size 118044 +oid sha256:133dfe6b0028dda6248df1afde1288c57625be99b25c8224673597de4d4f70fc +size 118588 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50-frame-apply-gaussian.png b/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50-frame-apply-gaussian.png index 13297e09f..29fe20f44 100644 --- a/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50-frame-apply-gaussian.png +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50-frame-apply-gaussian.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:70c7738ed303f5a3e19271e8dfc12ab857a6f3aff767bdbecb485b763a09913e -size 55584 +oid sha256:87a3947d6c59c7f67acca25911e0ab93ddc9231a8c3060d2fffe3c53f39055f2 +size 62263 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50-frame-apply-reset.png b/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50-frame-apply-reset.png index b8307bc44..c7944f591 100644 --- a/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50-frame-apply-reset.png +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50-frame-apply-reset.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:66a435e45dc4643135633115af2eeaf70761e408a94d70d94d80c14141574528 -size 69343 +oid sha256:b57c65974362d258ec7be8de391c41d7909ed260b92411f4b0ed8ed03b886a29 +size 73040 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50-max-window-13.png b/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50-max-window-13.png index d6237dc9f..eb9c9059d 100644 --- a/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50-max-window-13.png +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50-max-window-13.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:731f225fa2de3457956b2095d1cc539734983d041b13d6ad1a1f9d8e7ebfa4bc -size 115239 +oid sha256:008381b267ae26e8693ae51e7a4fabc464288ec8aa911ff3a1deb37543cc4fbe +size 115543 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50-mean-window-13.png b/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50-mean-window-13.png index ecf63a369..8b887f5fd 100644 --- a/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50-mean-window-13.png +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50-mean-window-13.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7e2d70159ac47c004acb022b3a669e7bd307299ddd590b83c08854b0dba27b70 -size 93885 +oid sha256:fedfec781724d4731f8cc34ffc39388d14dc60dad4a9fae9ff56625edf11f87a +size 94178 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50-mean-window-5.png b/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50-mean-window-5.png index e7106fae9..ef3aa7a92 100644 --- a/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50-mean-window-5.png +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50-mean-window-5.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1756783ab90435b46ded650033cf29ac36d2b4380744bf312caa2813267f7f38 -size 89813 +oid sha256:08e8379187754fa14f360ed54f2ed8cf61b3df71a8b6f2e95ff1ed27aa435d60 +size 90105 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50.png b/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50.png index ddd4f85ca..c7944f591 100644 --- a/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50.png +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a35e2e4b892b55f5d2500f895951f6a0289a2df3b69cf12f59409bbc091d1caf -size 72810 +oid sha256:b57c65974362d258ec7be8de391c41d7909ed260b92411f4b0ed8ed03b886a29 +size 73040 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-frame-99.png b/examples/notebooks/screenshots/nb-image-widget-zfish-frame-99.png index d9971c3fd..0d19a35ce 100644 --- a/examples/notebooks/screenshots/nb-image-widget-zfish-frame-99.png +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-frame-99.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3bdb0ed864c8a6f2118cfe0d29476f61c54576f7b8e041f3c3a895ba0a440c05 -size 65039 +oid sha256:848e89e38b9b5ef97d6bb4b301c0ae10cc29f438518721663ae52fa42f492408 +size 65267 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50-frame-apply-gaussian.png b/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50-frame-apply-gaussian.png index 6736e108c..96a3b12c8 100644 --- a/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50-frame-apply-gaussian.png +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50-frame-apply-gaussian.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7ae7c86bee3a30bde6cfa44e1e583e6dfd8de6bb29e7c86cea9141ae30637b4a -size 80627 +oid sha256:17cd05ae14cacdef6aa1eca3544246b814ef21762a33f6e785f6d621ea30ff96 +size 80570 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50-frame-apply-reset.png b/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50-frame-apply-reset.png index dce99223b..1df19c904 100644 --- a/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50-frame-apply-reset.png +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50-frame-apply-reset.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b51a5d26f2408748e59e3ee481735694f8f376539b50deb2b5c5a864b7de1079 -size 105581 +oid sha256:a673fa1ffa6f746ab9f462b4d592492ec02bfdd3fb53bdf1f71fb9427f8d6d23 +size 105798 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50-max-window-13.png b/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50-max-window-13.png index cdea3673d..43230f8be 100644 --- a/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50-max-window-13.png +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50-max-window-13.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e854f7f2fdaeeac6c8358f94a33698b5794c0f6c55b240d384e8c6d51fbfb0ff -size 143301 +oid sha256:446d54cea3d54b0fd92b70abcc090cfee30b19454dce118d9875fbeb8b40b4a8 +size 141294 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50-mean-window-13.png b/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50-mean-window-13.png index 25a2fa53e..0841a8e08 100644 --- a/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50-mean-window-13.png +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50-mean-window-13.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c8c8d3c59c145a4096deceabc71775a03e5e121e82509590787c768944d155bd -size 110744 +oid sha256:99d3706d5574a1236264f556eb3ce6d71e81b65bd8dcce1c1415e5f139316c23 +size 107894 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50-mean-window-5.png b/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50-mean-window-5.png index 00a4a1fd2..28bab9f02 100644 --- a/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50-mean-window-5.png +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50-mean-window-5.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c4b4af7b99cad95ea3f688af8633de24b6602bd700cb244f93c28718af2e1e85 -size 114982 +oid sha256:ffa17fc1b71c5146cae88493ed40c606dd0a99f3e10f3827ac349d5a5d6f6108 +size 112702 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50.png b/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50.png index 3b5594c64..1df19c904 100644 --- a/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50.png +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6d28a4be4c76d5c0da5f5767b169acf7048a268b010f33f96829a5de7f06fd7d -size 107477 +oid sha256:a673fa1ffa6f746ab9f462b4d592492ec02bfdd3fb53bdf1f71fb9427f8d6d23 +size 105798 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-99.png b/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-99.png index 239237b45..06ed02628 100644 --- a/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-99.png +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-99.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:30dba982c9a605a7a3c0f2fa6d8cdf0df4160b2913a95b26ffdb6b04ead12add -size 104603 +oid sha256:4d3e88eee05bc68dd17918197602fb5c0a959ad74a4f592aea4514e570d29232 +size 103431 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-grid-init-mean-window-5.png b/examples/notebooks/screenshots/nb-image-widget-zfish-grid-init-mean-window-5.png index 0745a4d4a..61702a6d9 100644 --- a/examples/notebooks/screenshots/nb-image-widget-zfish-grid-init-mean-window-5.png +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-grid-init-mean-window-5.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e431229806ee32a78fb9313a09af20829c27799798232193feab1723b66b1bca -size 112646 +oid sha256:272156c4261bba40eba92f953a0f5078ad8ff2aa80f06a53f73a3572eb537dd5 +size 111155 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-grid-set_data-reset-indices-false.png b/examples/notebooks/screenshots/nb-image-widget-zfish-grid-set_data-reset-indices-false.png index 498b19cb7..412822a40 100644 --- a/examples/notebooks/screenshots/nb-image-widget-zfish-grid-set_data-reset-indices-false.png +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-grid-set_data-reset-indices-false.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a8e899b48881e3eb9200cc4e776db1f865b0911c340c06d4009b3ae12aa1fc85 -size 105421 +oid sha256:8203f859fe54e2b59a143a9a569c2854640b1501b9ab4f8512520bbf73dae3c6 +size 105658 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-grid-set_data-reset-indices-true.png b/examples/notebooks/screenshots/nb-image-widget-zfish-grid-set_data-reset-indices-true.png index 369168141..234924487 100644 --- a/examples/notebooks/screenshots/nb-image-widget-zfish-grid-set_data-reset-indices-true.png +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-grid-set_data-reset-indices-true.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:93933e7ba5f791072df2934c94a782e39ed97f7db5b55c5d71c8c5bbfc69d800 -size 106360 +oid sha256:8ca187ba67e7928c8f96b1f9a0a18bec65f81352701e60c33d47aaadb2756d5c +size 106446 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-init-mean-window-5.png b/examples/notebooks/screenshots/nb-image-widget-zfish-init-mean-window-5.png index b62721be2..870945ce7 100644 --- a/examples/notebooks/screenshots/nb-image-widget-zfish-init-mean-window-5.png +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-init-mean-window-5.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bf38b2af1ceb372cd0949d42c027acb5fcc4c6b9a8f38c5aacdce1cd14e290fe -size 78533 +oid sha256:f42367c833a23d3fe10c6fb0d754338c12a30288d9769ad3f8b1159505abf8ff +size 78796 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 index 76ed01a7c..7880fc1d8 100644 --- 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 @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ff462d24820f0bdd509e58267071fa956b5c863b8b8d66fea061c5253b7557cf -size 113926 +oid sha256:cb99cd81a18fa2f8986c5f00071c45dc778c8aa177f4b02dca6bc5fab122b054 +size 114825 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 index d9a593ee7..82f3d0a9b 100644 --- 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 @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2b8fd14f8e8a90c3cd3fbb84a00d50b1b826b596d64dfae4a5ea1bab0687d906 -size 110829 +oid sha256:31b2b92b9d983950b58b90a09f16199740e35a0737fc1b18904f507ea322d8f2 +size 111118 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 index cf10c6d42..1446c8941 100644 --- 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 @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d88c64b716d19a3978bd60f8d75ffe09e022183381898fa1c48b77598be8fb7c -size 111193 +oid sha256:0fb724e005c6e081ae3bf235e155f3f526c3480facac7479d9b9452aae81baf0 +size 111437 diff --git a/examples/scatter/scatter_size.py b/examples/scatter/scatter_size.py index 73be31f62..c982e0220 100644 --- a/examples/scatter/scatter_size.py +++ b/examples/scatter/scatter_size.py @@ -2,7 +2,10 @@ Scatter Plot Size ================= -Example showing point size change for scatter plot. +Example that shows how to set scatter sizes in two different ways. + +One subplot uses a single scaler value for every point, and another subplot uses an array that defines the size for +each individual scatter point. """ # test_example = true @@ -14,10 +17,10 @@ # figure with 2 rows and 3 columns shape = (2, 1) -# you can give string names for each subplot within the gridplot +# you can give string names for each subplot within the figure names = [["scalar_size"], ["array_size"]] -# Create the grid plot +# Create the figure figure = fpl.Figure(shape=shape, names=names, size=(700, 560)) # get y_values using sin function diff --git a/examples/screenshots/image_widget_grid.png b/examples/screenshots/image_widget_grid.png index 45bc70726..a6ccd144a 100644 --- a/examples/screenshots/image_widget_grid.png +++ b/examples/screenshots/image_widget_grid.png @@ -1,3 +1,4 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:306977f7eebdb652828ba425d73b6018e97c100f3cf8f3cbaa0244ffb6c040a3 -size 249103 + +oid sha256:430cd0ee5c05221c42073345480acbeee672c299311f239dc0790a9495d0d758 +size 248046 diff --git a/examples/screenshots/linear_region_selectors_match_offsets.png b/examples/screenshots/linear_region_selectors_match_offsets.png index 327f14e72..e6fab4c4d 100644 --- a/examples/screenshots/linear_region_selectors_match_offsets.png +++ b/examples/screenshots/linear_region_selectors_match_offsets.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8fac4f439b34a5464792588b77856f08c127c0ee06fa77722818f8d6b48dd64c -size 95433 +oid sha256:f2eac8ffeb8cd35a0c65d51b0952defea61928abb53c865e681fa72af4ac4347 +size 95750 diff --git a/examples/screenshots/linear_selector.png b/examples/screenshots/linear_selector.png index 2db42319d..8571d664b 100644 --- a/examples/screenshots/linear_selector.png +++ b/examples/screenshots/linear_selector.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:09f60f24e702dd6b17ba525604c1a04f23682eb08c8c2100d45a34b2626bebc6 -size 153115 +oid sha256:62ded18658bc5cb41129d27eb21f47f029cf7c75bb6388b5d72af6fe9c5cada9 +size 130919 diff --git a/examples/screenshots/no-imgui-linear_region_selectors_match_offsets.png b/examples/screenshots/no-imgui-linear_region_selectors_match_offsets.png index 809908432..d82efa849 100644 --- a/examples/screenshots/no-imgui-linear_region_selectors_match_offsets.png +++ b/examples/screenshots/no-imgui-linear_region_selectors_match_offsets.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:303d562f1a16f6a704415072d43ca08a51e12a702292b522e0f17f397b1aee60 -size 96668 +oid sha256:1b22ee4506bc532344cfcbd5daa0c4e90d9a831d59f1d916bd28534786947771 +size 97036 diff --git a/examples/screenshots/no-imgui-linear_selector.png b/examples/screenshots/no-imgui-linear_selector.png new file mode 100644 index 000000000..4416cb4d5 --- /dev/null +++ b/examples/screenshots/no-imgui-linear_selector.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1f1a323dec6d50d1c701632aadbd17d87ee3b3b42171046ca9b1284f93576a3b +size 131922 diff --git a/examples/selection_tools/linear_region_line_collection.py b/examples/selection_tools/linear_region_line_collection.py index 76739d784..4b85b34dc 100644 --- a/examples/selection_tools/linear_region_line_collection.py +++ b/examples/selection_tools/linear_region_line_collection.py @@ -59,8 +59,11 @@ def update_zoomed_subplots(ev): for i in range(len(zoomed_data)): # interpolate y-vals - data = interpolate(zoomed_data[i], axis=1) - figure[i + 1, 0]["zoomed"].data[:, 1] = data + if zoomed_data[i].size == 0: + figure[i + 1, 0]["zoomed"].data[:, 1] = 0 + else: + data = interpolate(zoomed_data[i], axis=1) + figure[i + 1, 0]["zoomed"].data[:, 1] = data figure[i + 1, 0].auto_scale() diff --git a/examples/selection_tools/linear_region_selector.py b/examples/selection_tools/linear_region_selector.py index 6fa17db38..272623370 100644 --- a/examples/selection_tools/linear_region_selector.py +++ b/examples/selection_tools/linear_region_selector.py @@ -29,15 +29,15 @@ names=names, ) -# preallocated size for zoomed data -zoomed_prealloc = 1_000 +# preallocated number of datapoints for zoomed data +zoomed_prealloc = 5_000 # data to plot -xs = np.linspace(0, 10 * np.pi, 1_000) -ys = np.sin(xs) # y = sine(x) +xs = np.linspace(0, 200 * np.pi, 10_000) +ys = np.sin(xs) + np.random.normal(scale=0.2, size=10000) # make sine along x axis -sine_graphic_x = figure[0, 0].add_line(np.column_stack([xs, ys])) +sine_graphic_x = figure[0, 0].add_line(np.column_stack([xs, ys]), thickness=1) # x = sine(y), sine(y) > 0 = 0 sine_y = ys @@ -51,7 +51,7 @@ sine_graphic_y.position_y = 50 # add linear selectors -selector_x = sine_graphic_x.add_linear_region_selector() # default axis is "x" +selector_x = sine_graphic_x.add_linear_region_selector((0, 100)) # default axis is "x" selector_y = sine_graphic_y.add_linear_region_selector(axis="y") # preallocate array for storing zoomed in data @@ -79,9 +79,9 @@ def set_zoom_x(ev): if selected_data.size == 0: # no data selected zoomed_x.data[:, 1] = 0 - - # interpolate the y-values since y = f(x) - zoomed_x.data[:, 1] = interpolate(selected_data, axis=1) + else: + # interpolate the y-values since y = f(x) + zoomed_x.data[:, 1] = interpolate(selected_data, axis=1) figure[1, 0].auto_scale() @@ -92,9 +92,9 @@ def set_zoom_y(ev): if selected_data.size == 0: # no data selected zoomed_y.data[:, 1] = 0 - - # interpolate the x values since this x = f(y) - zoomed_y.data[:, 1] = -interpolate(selected_data, axis=0) + else: + # interpolate the x values since this x = f(y) + zoomed_y.data[:, 1] = -interpolate(selected_data, axis=0) figure[1, 1].auto_scale() @@ -102,8 +102,8 @@ def set_zoom_y(ev): selector_y.add_event_handler(set_zoom_y, "selection") # set initial selection -selector_x.selection = selector_y.selection = (0, 4 * np.pi) - +selector_x.selection = (0, 150) +selector_y.selection = (0, 150) figure.show(maintain_aspect=False) diff --git a/examples/selection_tools/linear_region_selectors_match_offsets.py b/examples/selection_tools/linear_region_selectors_match_offsets.py index b48e30f28..a803a5e75 100644 --- a/examples/selection_tools/linear_region_selectors_match_offsets.py +++ b/examples/selection_tools/linear_region_selectors_match_offsets.py @@ -74,9 +74,9 @@ def set_zoom_x(ev): if selected_data.size == 0: # no data selected zoomed_x.data[:, 1] = 0 - - # interpolate the y-values since y = f(x) - zoomed_x.data[:, 1] = interpolate(selected_data, axis=1) + else: + # interpolate the y-values since y = f(x) + zoomed_x.data[:, 1] = interpolate(selected_data, axis=1) figure[1, 0].auto_scale() @@ -87,9 +87,9 @@ def set_zoom_y(ev): if selected_data.size == 0: # no data selected zoomed_y.data[:, 1] = 0 - - # interpolate the x values since this x = f(y) - zoomed_y.data[:, 1] = -interpolate(selected_data, axis=0) + else: + # interpolate the x values since this x = f(y) + zoomed_y.data[:, 1] = -interpolate(selected_data, axis=0) figure[1, 1].auto_scale() diff --git a/examples/selection_tools/linear_selector.py b/examples/selection_tools/linear_selector.py index 1edf6345c..d7a8e6739 100644 --- a/examples/selection_tools/linear_selector.py +++ b/examples/selection_tools/linear_selector.py @@ -5,7 +5,7 @@ Example showing how to use a `LinearSelector` with lines and line collections. """ -# test_example = false +# test_example = true # sphinx_gallery_pygfx_docs = 'screenshot' import fastplotlib as fpl diff --git a/examples/tests/testutils.py b/examples/tests/testutils.py index d6fce52fe..4c23b3481 100644 --- a/examples/tests/testutils.py +++ b/examples/tests/testutils.py @@ -25,8 +25,9 @@ "line_collection/*.py", "gridplot/*.py", "window_layouts/*.py", - "misc/*.py", + "events/*.py", "selection_tools/*.py", + "misc/*.py", "guis/*.py", ] diff --git a/examples/text/README.rst b/examples/text/README.rst new file mode 100644 index 000000000..01466a39f --- /dev/null +++ b/examples/text/README.rst @@ -0,0 +1,2 @@ +Text Examples +============= diff --git a/examples/text/moving_label.py b/examples/text/moving_label.py new file mode 100644 index 000000000..45d2439ee --- /dev/null +++ b/examples/text/moving_label.py @@ -0,0 +1,84 @@ +""" +Moving TextGraphic label +======================== + +A TextGraphic that labels a point on a line and another TextGraphic that moves along the line on every draw. +""" + +# test_example = false +# sphinx_gallery_pygfx_docs = 'animate 10s' + +import numpy as np +import fastplotlib as fpl + +# create a sinc wave +xs = np.linspace(-2 * np.pi, 2 * np.pi, 200) +ys = np.sinc(xs) + +data = np.column_stack([xs, ys]) + +# create a figure +figure = fpl.Figure(size=(700, 450)) + +# sinc wave +line = figure[0, 0].add_line(data, thickness=2) + +# position for the text label on the peak +pos = (0, max(ys), 0) + +# create label for the peak +text_peak = figure[0, 0].add_text( + f"peak ", + font_size=20, + anchor="bottom-right", + offset=pos +) + +# add a point on the peak +point_peak = figure[0, 0].add_scatter(np.asarray([pos]), sizes=10, colors="r") + +# create a text that will move along the line +text_moving = figure[0, 0].add_text( + f"({xs[0]:.2f}, {ys[0]:.2f}) ", + font_size=16, + outline_color="k", + outline_thickness=1, + anchor="top-center", + offset=(*data[0], 0) +) +# a point that will move on the line +point_moving = figure[0, 0].add_scatter(np.asarray([data[0]]), sizes=10, colors="magenta") + + +index = 0 +def update(): + # moves the text and point before every draw + global index + # get the new position + new_pos = (*data[index], 0) + + # move the text and point to the new position + text_moving.offset = new_pos + point_moving.data[0] = new_pos + + # set the text to the new position + text_moving.text = f"({new_pos[0]:.2f}, {new_pos[1]:.2f})" + + # increment index + index += 1 + if index == data.shape[0]: + index = 0 + + +# add update as an animation functions +figure.add_animations(update) + +figure[0, 0].axes.visible = False +figure.show(maintain_aspect=False) + + +# NOTE: `if __name__ == "__main__"` is NOT how to use fastplotlib interactively +# please see our docs for using fastplotlib interactively in ipython and jupyter +if __name__ == "__main__": + print(__doc__) + fpl.loop.run() diff --git a/fastplotlib/VERSION b/fastplotlib/VERSION deleted file mode 100644 index 1d0ba9ea1..000000000 --- a/fastplotlib/VERSION +++ /dev/null @@ -1 +0,0 @@ -0.4.0 diff --git a/fastplotlib/__init__.py b/fastplotlib/__init__.py index 7eb9554e8..6dab91605 100644 --- a/fastplotlib/__init__.py +++ b/fastplotlib/__init__.py @@ -1,8 +1,11 @@ from pathlib import Path +from ._version import __version__, version_info + # this must be the first import for auto-canvas detection from .utils import loop # noqa from .graphics import * +from .graphics.features import GraphicFeatureEvent from .graphics.selectors import * from .graphics.utils import pause_events from .legends import * @@ -20,9 +23,6 @@ from .utils import config, enumerate_adapters, select_adapter, print_wgpu_report -with open(Path(__file__).parent.joinpath("VERSION"), "r") as f: - __version__ = f.read().split("\n")[0] - if len(enumerate_adapters()) < 1: from warnings import warn diff --git a/fastplotlib/_version.py b/fastplotlib/_version.py new file mode 100644 index 000000000..ddeeb3d84 --- /dev/null +++ b/fastplotlib/_version.py @@ -0,0 +1,113 @@ +""" +Versioning: we use a hard-coded version number, because it's simple and always +works. For dev installs we add extra version info from Git. +""" + +import logging +import subprocess +from pathlib import Path + + +# This is the reference version number, to be bumped before each release. +# The build system detects this definition when building a distribution. +__version__ = "0.5.0" + +# Allow using nearly the same code in different projects +project_name = "fastplotlib" + + +logger = logging.getLogger(project_name.lower()) + +# Get whether this is a repo. If so, repo_dir is the path, otherwise repo_dir is None. +repo_dir = Path(__file__).parents[1] +repo_dir = repo_dir if repo_dir.joinpath(".git").is_dir() else None + + +def get_version(): + """Get the version string.""" + if repo_dir: + return get_extended_version() + else: + return __version__ + + +def get_extended_version(): + """Get an extended version string with information from git.""" + + release, post, labels = get_version_info_from_git() + + # Sample first 3 parts of __version__ + base_release = ".".join(__version__.split(".")[:3]) + + # Check release + if not release: + release = base_release + elif release != base_release: + logger.warning( + f"{project_name} version from git ({release}) and __version__ ({base_release}) don't match." + ) + + # Build the total version + version = release + if post and post != "0": + version += f".post{post}" + if labels: + version += "+" + ".".join(labels) + + return version + + +def get_version_info_from_git(): + """Get (release, post, labels) from Git. + + With `release` the version number from the latest tag, `post` the + number of commits since that tag, and `labels` a tuple with the + git-hash and optionally a dirty flag. + """ + + # Call out to Git + command = [ + "git", + "describe", + "--long", + "--always", + "--tags", + "--dirty", + "--first-parent", + ] + try: + p = subprocess.run(command, cwd=repo_dir, capture_output=True) + except Exception as e: + logger.warning(f"Could not get {project_name} version: {e}") + p = None + + # Parse the result into parts + if p is None: + parts = (None, None, "unknown") + else: + output = p.stdout.decode(errors="ignore") + if p.returncode: + stderr = p.stderr.decode(errors="ignore") + logger.warning( + f"Could not get {project_name} version.\n\nstdout: " + + output + + "\n\nstderr: " + + stderr + ) + parts = (None, None, "unknown") + else: + parts = output.strip().lstrip("v").split("-") + if len(parts) <= 2: + # No tags (and thus also no post). Only git hash and maybe 'dirty' + parts = (None, None, *parts) + + # Return unpacked parts + release, post, *labels = parts + return release, post, labels + + +__version__ = get_version() + +version_info = tuple( + int(i) if i.isnumeric() else i for i in __version__.split("+")[0].split(".") +) diff --git a/fastplotlib/graphics/__init__.py b/fastplotlib/graphics/__init__.py index ff96baa4c..b458a8c48 100644 --- a/fastplotlib/graphics/__init__.py +++ b/fastplotlib/graphics/__init__.py @@ -1,3 +1,4 @@ +from ._base import Graphic from .line import LineGraphic from .scatter import ScatterGraphic from .image import ImageGraphic @@ -6,9 +7,10 @@ __all__ = [ + "Graphic", "LineGraphic", - "ImageGraphic", "ScatterGraphic", + "ImageGraphic", "TextGraphic", "LineCollection", "LineStack", diff --git a/fastplotlib/graphics/_base.py b/fastplotlib/graphics/_base.py index 61ad291ee..e115107b0 100644 --- a/fastplotlib/graphics/_base.py +++ b/fastplotlib/graphics/_base.py @@ -16,7 +16,7 @@ import pygfx -from ._features import ( +from .features import ( BufferManager, Deleted, Name, @@ -50,7 +50,7 @@ class Graphic: - _features: set[str] = {} + _features: dict[str, type] = dict() def __init_subclass__(cls, **kwargs): # set the type of the graphic in lower case like "image", "line_collection", etc. @@ -63,12 +63,12 @@ def __init_subclass__(cls, **kwargs): # set of all features cls._features = { - *cls._features, - "name", - "offset", - "rotation", - "visible", - "deleted", + **cls._features, + "name": Name, + "offset": Offset, + "rotation": Rotation, + "visible": Visible, + "deleted": Deleted, } super().__init_subclass__(**kwargs) @@ -129,7 +129,7 @@ def __init__( @property def supported_events(self) -> tuple[str]: """events supported by this graphic""" - return (*tuple(self._features), *PYGFX_EVENTS) + return (*tuple(self._features.keys()), *PYGFX_EVENTS) @property def name(self) -> str | None: @@ -273,7 +273,7 @@ def decorator(_callback): # add to our record self._event_handlers[t].add(_callback) - if t in self._features: + if t in self._features.keys(): # fpl feature event feature = getattr(self, f"_{t}") feature.add_event_handler(_callback_wrapper) diff --git a/fastplotlib/graphics/_positions_base.py b/fastplotlib/graphics/_positions_base.py index 565a4cd98..8b127aa19 100644 --- a/fastplotlib/graphics/_positions_base.py +++ b/fastplotlib/graphics/_positions_base.py @@ -4,7 +4,7 @@ import pygfx from ._base import Graphic -from ._features import ( +from .features import ( VertexPositions, VertexColors, UniformColor, @@ -19,7 +19,7 @@ class PositionsGraphic(Graphic): @property def data(self) -> VertexPositions: - """Get or set the vertex positions data""" + """Get or set the graphic's data""" return self._data @data.setter @@ -28,7 +28,7 @@ def data(self, value): @property def colors(self) -> VertexColors | pygfx.Color: - """Get or set the colors data""" + """Get or set the colors""" if isinstance(self._colors, VertexColors): return self._colors @@ -45,7 +45,11 @@ def colors(self, value: str | np.ndarray | tuple[float] | list[float] | list[str @property def cmap(self) -> VertexCmap: - """Control the cmap, cmap transform, or cmap alpha""" + """ + Control the cmap, cmap transform, or cmap alpha + + For supported colormaps see the ``cmap`` library catalogue: https://cmap-docs.readthedocs.io/en/stable/catalog/ + """ return self._cmap @cmap.setter @@ -58,7 +62,7 @@ def cmap(self, name: str): @property def size_space(self): """ - The coordinate space in which the size is expressed (‘screen’, ‘world’, ‘model’) + The coordinate space in which the size is expressed ('screen', 'world', 'model') See https://docs.pygfx.org/stable/_autosummary/utils/utils/enums/pygfx.utils.enums.CoordSpace.html#pygfx.utils.enums.CoordSpace for available options. """ diff --git a/fastplotlib/graphics/_features/__init__.py b/fastplotlib/graphics/features/__init__.py similarity index 96% rename from fastplotlib/graphics/_features/__init__.py rename to fastplotlib/graphics/features/__init__.py index a1915bbe9..18bcf5187 100644 --- a/fastplotlib/graphics/_features/__init__.py +++ b/fastplotlib/graphics/features/__init__.py @@ -19,7 +19,7 @@ from ._base import ( GraphicFeature, BufferManager, - FeatureEvent, + GraphicFeatureEvent, to_gpu_supported_dtype, ) @@ -67,4 +67,5 @@ "Rotation", "Visible", "Deleted", + "GraphicFeatureEvent", ] diff --git a/fastplotlib/graphics/_features/_base.py b/fastplotlib/graphics/features/_base.py similarity index 96% rename from fastplotlib/graphics/_features/_base.py rename to fastplotlib/graphics/features/_base.py index 1088dc005..d32904ae5 100644 --- a/fastplotlib/graphics/_features/_base.py +++ b/fastplotlib/graphics/features/_base.py @@ -1,5 +1,5 @@ from warnings import warn -from typing import Any, Literal +from typing import Literal import numpy as np from numpy.typing import NDArray @@ -23,7 +23,7 @@ def to_gpu_supported_dtype(array): return np.asarray(array).astype(np.float32) -class FeatureEvent(pygfx.Event): +class GraphicFeatureEvent(pygfx.Event): """ **All event instances have the following attributes** @@ -34,11 +34,11 @@ class FeatureEvent(pygfx.Event): +------------+-------------+-----------------------------------------------+ | graphic | Graphic | graphic instance that the event is from | +------------+-------------+-----------------------------------------------+ - | info | dict | event info dictionary (see below) | + | info | dict | event info dictionary | +------------+-------------+-----------------------------------------------+ | target | WorldObject | pygfx rendering engine object for the graphic | +------------+-------------+-----------------------------------------------+ - | time_stamp | float | time when the event occured, in ms | + | time_stamp | float | time when the event occurred, in ms | +------------+-------------+-----------------------------------------------+ """ @@ -57,7 +57,7 @@ def __init__(self, **kwargs): self._reentrant_block: bool = False @property - def value(self) -> Any: + def value(self): """Graphic Feature value, must be implemented in subclass""" raise NotImplemented @@ -120,7 +120,7 @@ def clear_event_handlers(self): """Clear all event handlers""" self._event_handlers.clear() - def _call_event_handlers(self, event_data: FeatureEvent): + def _call_event_handlers(self, event_data: GraphicFeatureEvent): if self._block_events: return @@ -310,7 +310,7 @@ def _emit_event(self, type: str, key, value): "key": key, "value": value, } - event = FeatureEvent(type, info=event_info) + event = GraphicFeatureEvent(type, info=event_info) self._call_event_handlers(event) diff --git a/fastplotlib/graphics/_features/_common.py b/fastplotlib/graphics/features/_common.py similarity index 53% rename from fastplotlib/graphics/_features/_common.py rename to fastplotlib/graphics/features/_common.py index e9c49a475..71e979f77 100644 --- a/fastplotlib/graphics/_features/_common.py +++ b/fastplotlib/graphics/features/_common.py @@ -1,12 +1,17 @@ import numpy as np -from ._base import GraphicFeature, FeatureEvent, block_reentrance +from ._base import GraphicFeature, GraphicFeatureEvent, block_reentrance class Name(GraphicFeature): - """Graphic name""" + property_name = "name" + event_info_spec = [ + {"dict key": "value", "type": "str", "description": "user provided name"}, + ] def __init__(self, value: str): + """Graphic name""" + self._value = value super().__init__() @@ -24,17 +29,29 @@ def set_value(self, graphic, value: str): self._value = value - event = FeatureEvent(type="name", info={"value": value}) + event = GraphicFeatureEvent(type="name", info={"value": value}) self._call_event_handlers(event) class Offset(GraphicFeature): - """Offset position of the graphic, [x, y, z]""" + property_name = "offset" + event_info_spec = [ + { + "dict key": "value", + "type": "np.ndarray[float, float, float]", + "description": "new offset (x, y, z)", + }, + ] def __init__(self, value: np.ndarray | list | tuple): + """Offset position of the graphic, [x, y, z]""" + self._validate(value) - self._value = np.array(value) - self._value.flags.writeable = False + # initialize zeros array + self._value = np.zeros(3) + + # set values + self._value[:] = value super().__init__() def _validate(self, value): @@ -48,22 +65,38 @@ def value(self) -> np.ndarray: @block_reentrance def set_value(self, graphic, value: np.ndarray | list | tuple): self._validate(value) + value = np.asarray(value) graphic.world_object.world.position = value - self._value = graphic.world_object.world.position.copy() - self._value.flags.writeable = False - event = FeatureEvent(type="offset", info={"value": value}) + # sometimes there are transforms so get the final position value like this + value = graphic.world_object.world.position.copy() + + # set value of existing feature value array + self._value[:] = value + + event = GraphicFeatureEvent(type="offset", info={"value": value}) self._call_event_handlers(event) class Rotation(GraphicFeature): - """Graphic rotation quaternion""" + property_name = "offset" + event_info_spec = [ + { + "dict key": "value", + "type": "np.ndarray[float, float, float, float]", + "description": "new rotation quaternion", + }, + ] def __init__(self, value: np.ndarray | list | tuple): + """Graphic rotation quaternion""" + self._validate(value) - self._value = np.array(value) - self._value.flags.writeable = False + # create zeros array + self._value = np.zeros(4) + + self._value[:] = value super().__init__() def _validate(self, value): @@ -79,18 +112,29 @@ def value(self) -> np.ndarray: @block_reentrance def set_value(self, graphic, value: np.ndarray | list | tuple): self._validate(value) + value = np.asarray(value) graphic.world_object.world.rotation = value - self._value = graphic.world_object.world.rotation.copy() - self._value.flags.writeable = False - event = FeatureEvent(type="rotation", info={"value": value}) + # get the actual final quaternion value, pygfx adjusts to make sure || q ||_2 == 1 + # i.e. pygfx checks to make sure norm 1 and other transforms + value = graphic.world_object.world.rotation.copy() + + # set value of existing feature value array + self._value[:] = value + + event = GraphicFeatureEvent(type="rotation", info={"value": value}) self._call_event_handlers(event) class Visible(GraphicFeature): """Access or change the visibility.""" + property_name = "offset" + event_info_spec = [ + {"dict key": "value", "type": "bool", "description": "new visibility bool"}, + ] + def __init__(self, value: bool): self._value = value super().__init__() @@ -104,7 +148,7 @@ def set_value(self, graphic, value: bool): graphic.world_object.visible = value self._value = value - event = FeatureEvent(type="visible", info={"value": value}) + event = GraphicFeatureEvent(type="visible", info={"value": value}) self._call_event_handlers(event) @@ -113,6 +157,15 @@ class Deleted(GraphicFeature): Used when a graphic is deleted, triggers events that can be useful to indicate this graphic has been deleted """ + property_name = "deleted" + event_info_spec = [ + { + "dict key": "value", + "type": "bool", + "description": "True when graphic was deleted", + }, + ] + def __init__(self, value: bool): self._value = value super().__init__() @@ -124,5 +177,5 @@ def value(self) -> bool: @block_reentrance def set_value(self, graphic, value: bool): self._value = value - event = FeatureEvent(type="deleted", info={"value": value}) + event = GraphicFeatureEvent(type="deleted", info={"value": value}) self._call_event_handlers(event) diff --git a/fastplotlib/graphics/_features/_image.py b/fastplotlib/graphics/features/_image.py similarity index 81% rename from fastplotlib/graphics/_features/_image.py rename to fastplotlib/graphics/features/_image.py index c0e2b28d2..c47a26e6a 100644 --- a/fastplotlib/graphics/_features/_image.py +++ b/fastplotlib/graphics/features/_image.py @@ -5,7 +5,7 @@ import numpy as np import pygfx -from ._base import GraphicFeature, FeatureEvent, block_reentrance +from ._base import GraphicFeature, GraphicFeatureEvent, block_reentrance from ...utils import ( make_colors, @@ -15,6 +15,19 @@ # manages an array of 8192x8192 Textures representing chunks of an image class TextureArray(GraphicFeature): + event_info_spec = [ + { + "dict key": "key", + "type": "slice, index, numpy-like fancy index", + "description": "key at which image data was sliced/fancy indexed", + }, + { + "dict key": "value", + "type": "np.ndarray | float", + "description": "new data values", + }, + ] + def __init__(self, data, isolated_buffer: bool = True): super().__init__() @@ -142,7 +155,7 @@ def __setitem__(self, key, value): for texture in self.buffer.ravel(): texture.update_range((0, 0, 0), texture.size) - event = FeatureEvent("data", info={"key": key, "value": value}) + event = GraphicFeatureEvent("data", info={"key": key, "value": value}) self._call_event_handlers(event) def __len__(self): @@ -152,6 +165,14 @@ def __len__(self): class ImageVmin(GraphicFeature): """lower contrast limit""" + event_info_spec = [ + { + "dict key": "value", + "type": "float", + "description": "new vmin value", + }, + ] + def __init__(self, value: float): self._value = value super().__init__() @@ -166,13 +187,21 @@ def set_value(self, graphic, value: float): graphic._material.clim = (value, vmax) self._value = value - event = FeatureEvent(type="vmin", info={"value": value}) + event = GraphicFeatureEvent(type="vmin", info={"value": value}) self._call_event_handlers(event) class ImageVmax(GraphicFeature): """upper contrast limit""" + event_info_spec = [ + { + "dict key": "value", + "type": "float", + "description": "new vmax value", + }, + ] + def __init__(self, value: float): self._value = value super().__init__() @@ -187,13 +216,21 @@ def set_value(self, graphic, value: float): graphic._material.clim = (vmin, value) self._value = value - event = FeatureEvent(type="vmax", info={"value": value}) + event = GraphicFeatureEvent(type="vmax", info={"value": value}) self._call_event_handlers(event) class ImageCmap(GraphicFeature): """colormap for texture""" + event_info_spec = [ + { + "dict key": "value", + "type": "str", + "description": "new cmap name", + }, + ] + def __init__(self, value: str): self._value = value self.texture = get_cmap_texture(value) @@ -210,13 +247,21 @@ def set_value(self, graphic, value: str): graphic._material.map.texture.update_range((0, 0, 0), size=(256, 1, 1)) self._value = value - event = FeatureEvent(type="cmap", info={"value": value}) + event = GraphicFeatureEvent(type="cmap", info={"value": value}) self._call_event_handlers(event) class ImageInterpolation(GraphicFeature): """Image interpolation method""" + event_info_spec = [ + { + "dict key": "value", + "type": "str", + "description": "new interpolation method, nearest | linear", + }, + ] + def __init__(self, value: str): self._validate(value) self._value = value @@ -237,13 +282,21 @@ def set_value(self, graphic, value: str): graphic._material.interpolation = value self._value = value - event = FeatureEvent(type="interpolation", info={"value": value}) + event = GraphicFeatureEvent(type="interpolation", info={"value": value}) self._call_event_handlers(event) class ImageCmapInterpolation(GraphicFeature): """Image cmap interpolation method""" + event_info_spec = [ + { + "dict key": "value", + "type": "str", + "description": "new cmap interpolatio method, nearest | linear", + }, + ] + def __init__(self, value: str): self._validate(value) self._value = value @@ -268,5 +321,5 @@ def set_value(self, graphic, value: str): graphic._material.map.mag_filter = value self._value = value - event = FeatureEvent(type="cmap_interpolation", info={"value": value}) + event = GraphicFeatureEvent(type="cmap_interpolation", info={"value": value}) self._call_event_handlers(event) diff --git a/fastplotlib/graphics/_features/_positions_graphics.py b/fastplotlib/graphics/features/_positions_graphics.py similarity index 75% rename from fastplotlib/graphics/_features/_positions_graphics.py rename to fastplotlib/graphics/features/_positions_graphics.py index 78e53f545..868701079 100644 --- a/fastplotlib/graphics/_features/_positions_graphics.py +++ b/fastplotlib/graphics/features/_positions_graphics.py @@ -9,7 +9,7 @@ from ._base import ( GraphicFeature, BufferManager, - FeatureEvent, + GraphicFeatureEvent, to_gpu_supported_dtype, block_reentrance, ) @@ -17,20 +17,24 @@ class VertexColors(BufferManager): - """ - - **info dict** - +------------+-----------------------------------------------------------+----------------------------------------------------------------------------------+ - | dict key | value type | value description | - +============+===========================================================+==================================================================================+ - | key | int | slice | np.ndarray[int | bool] | tuple[slice, ...] | key at which colors were indexed/sliced | - +------------+-----------------------------------------------------------+----------------------------------------------------------------------------------+ - | value | np.ndarray | new color values for points that were changed, shape is [n_points_changed, RGBA] | - +------------+-----------------------------------------------------------+----------------------------------------------------------------------------------+ - | user_value | str | np.ndarray | tuple[float] | list[float] | list[str] | user input value that was parsed into the RGBA array | - +------------+-----------------------------------------------------------+----------------------------------------------------------------------------------+ - - """ + property_name = "colors" + event_info_spec = [ + { + "dict key": "key", + "type": "slice, index, numpy-like fancy index", + "description": "index/slice at which colors were indexed/sliced", + }, + { + "dict key": "value", + "type": "np.ndarray [n_points_changed, RGBA]", + "description": "new color values for points that were changed", + }, + { + "dict key": "user_value", + "type": "str or array-like", + "description": "user input value that was parsed into the RGBA array", + }, + ] def __init__( self, @@ -137,18 +141,28 @@ def __setitem__( "user_value": user_value, } - event = FeatureEvent("colors", info=event_info) + event = GraphicFeatureEvent("colors", info=event_info) self._call_event_handlers(event) def __len__(self): return len(self.buffer.data) -# Manages uniform color for line or scatter material class UniformColor(GraphicFeature): + property_name = "colors" + event_info_spec = [ + { + "dict key": "value", + "type": "np.ndarray [RGBA]", + "description": "new color value", + }, + ] + def __init__( self, value: str | np.ndarray | tuple | list | pygfx.Color, alpha: float = 1.0 ): + """Manages uniform color for line or scatter material""" + v = (*tuple(pygfx.Color(value))[:-1], alpha) # apply alpha self._value = pygfx.Color(v) super().__init__() @@ -163,13 +177,19 @@ def set_value(self, graphic, value: str | np.ndarray | tuple | list | pygfx.Colo graphic.world_object.material.color = value self._value = value - event = FeatureEvent(type="colors", info={"value": value}) + event = GraphicFeatureEvent(type="colors", info={"value": value}) self._call_event_handlers(event) -# manages uniform size for scatter material class UniformSize(GraphicFeature): + property_name = "sizes" + event_info_spec = [ + {"dict key": "value", "type": "float", "description": "new size value"}, + ] + def __init__(self, value: int | float): + """Manages uniform size for scatter material""" + self._value = float(value) super().__init__() @@ -179,16 +199,27 @@ def value(self) -> float: @block_reentrance def set_value(self, graphic, value: float | int): - graphic.world_object.material.size = float(value) + value = float(value) + graphic.world_object.material.size = value self._value = value - event = FeatureEvent(type="sizes", info={"value": value}) + event = GraphicFeatureEvent(type="sizes", info={"value": value}) self._call_event_handlers(event) -# manages the coordinate space for scatter/line class SizeSpace(GraphicFeature): + property_name = "size_space" + event_info_spec = [ + { + "dict key": "value", + "type": "str", + "description": "'screen' | 'world' | 'model'", + }, + ] + def __init__(self, value: str): + """Manages the coordinate space for scatter/line graphic""" + self._value = value super().__init__() @@ -198,27 +229,35 @@ def value(self) -> str: @block_reentrance def set_value(self, graphic, value: str): + if value not in ["screen", "world", "model"]: + raise ValueError( + f"`size_space` must be one of: {['screen', 'world', 'model']}" + ) + if "Line" in graphic.world_object.material.__class__.__name__: graphic.world_object.material.thickness_space = value else: graphic.world_object.material.size_space = value self._value = value - event = FeatureEvent(type="size_space", info={"value": value}) + event = GraphicFeatureEvent(type="size_space", info={"value": value}) self._call_event_handlers(event) class VertexPositions(BufferManager): - """ - +----------+----------------------------------------------------------+------------------------------------------------------------------------------------------+ - | dict key | value type | value description | - +==========+==========================================================+==========================================================================================+ - | key | int | slice | np.ndarray[int | bool] | tuple[slice, ...] | key at which vertex positions data were indexed/sliced | - +----------+----------------------------------------------------------+------------------------------------------------------------------------------------------+ - | value | np.ndarray | float | list[float] | new data values for points that were changed, shape depends on the indices that were set | - +----------+----------------------------------------------------------+------------------------------------------------------------------------------------------+ - - """ + property_name = "data" + event_info_spec = [ + { + "dict key": "key", + "type": "slice, index (int) or numpy-like fancy index", + "description": "key at which vertex positions data were indexed/sliced", + }, + { + "dict key": "value", + "type": "int | float | array-like", + "description": "new data values for points that were changed", + }, + ] def __init__(self, data: Any, isolated_buffer: bool = True): """ @@ -268,15 +307,19 @@ def __len__(self): class PointsSizesFeature(BufferManager): - """ - +----------+-------------------------------------------------------------------+----------------------------------------------+ - | dict key | value type | value description | - +==========+===================================================================+==============================================+ - | key | int | slice | np.ndarray[int | bool] | list[int | bool] | key at which point sizes indexed/sliced | - +----------+-------------------------------------------------------------------+----------------------------------------------+ - | value | int | float | np.ndarray | list[int | float] | tuple[int | float] | new size values for points that were changed | - +----------+-------------------------------------------------------------------+----------------------------------------------+ - """ + property_name = "sizes" + event_info_spec = [ + { + "dict key": "key", + "type": "slice, index (int) or numpy-like fancy index", + "description": "key at which point sizes were indexed/sliced", + }, + { + "dict key": "value", + "type": "int | float | array-like", + "description": "new size values for points that were changed", + }, + ] def __init__( self, @@ -341,7 +384,10 @@ def __len__(self): class Thickness(GraphicFeature): - """line thickness""" + property_name = "thickness" + event_info_spec = [ + {"dict key": "value", "type": "float", "description": "new thickness value"}, + ] def __init__(self, value: float): self._value = value @@ -353,18 +399,28 @@ def value(self) -> float: @block_reentrance def set_value(self, graphic, value: float): + value = float(value) graphic.world_object.material.thickness = value self._value = value - event = FeatureEvent(type="thickness", info={"value": value}) + event = GraphicFeatureEvent(type="thickness", info={"value": value}) self._call_event_handlers(event) class VertexCmap(BufferManager): - """ - Sliceable colormap feature, manages a VertexColors instance and - provides a way to set colormaps with arbitrary transforms - """ + property_name = "cmap" + event_info_spec = [ + { + "dict key": "key", + "type": "slice", + "description": "key at cmap colors were sliced", + }, + { + "dict key": "value", + "type": "str", + "description": "new cmap to set at given slice", + }, + ] def __init__( self, @@ -373,6 +429,11 @@ def __init__( transform: np.ndarray | None, alpha: float = 1.0, ): + """ + Sliceable colormap feature, manages a VertexColors instance and + provides a way to set colormaps with arbitrary transforms + """ + super().__init__(data=vertex_colors.buffer) self._vertex_colors = vertex_colors @@ -405,12 +466,12 @@ def __setitem__(self, key: slice, cmap_name): if not isinstance(key, slice): raise TypeError( "fancy indexing not supported for VertexCmap, only slices " - "of a continuous are supported for apply a cmap" + "of a continuous range are supported for applying a cmap" ) if key.step is not None: raise TypeError( "step sized indexing not currently supported for setting VertexCmap, " - "slices must be a continuous region" + "slices must be a continuous range" ) # parse slice diff --git a/fastplotlib/graphics/_features/_selection_features.py b/fastplotlib/graphics/features/_selection_features.py similarity index 74% rename from fastplotlib/graphics/_features/_selection_features.py rename to fastplotlib/graphics/features/_selection_features.py index c157023b4..233353401 100644 --- a/fastplotlib/graphics/_features/_selection_features.py +++ b/fastplotlib/graphics/features/_selection_features.py @@ -3,28 +3,25 @@ import numpy as np from ...utils import mesh_masks -from ._base import GraphicFeature, FeatureEvent, block_reentrance +from ._base import GraphicFeature, GraphicFeatureEvent, block_reentrance class LinearSelectionFeature(GraphicFeature): - """ - **additional event attributes:** - - +--------------------+----------+------------------------------------+ - | attribute | type | description | - +====================+==========+====================================+ - | get_selected_index | callable | returns indices under the selector | - +--------------------+----------+------------------------------------+ - - **info dict:** - - +----------+------------+-------------------------------+ - | dict key | value type | value description | - +==========+============+===============================+ - | value | np.ndarray | new x or y value of selection | - +----------+------------+-------------------------------+ - - """ + event_info_spec = [ + { + "dict key": "value", + "type": "float", + "description": "new x or y value of selection", + }, + ] + + event_extra_attrs = [ + { + "attribute": "get_selected_index", + "type": "callable", + "description": "returns index under the selector", + } + ] def __init__(self, axis: str, value: float, limits: tuple[float, float]): """ @@ -71,33 +68,33 @@ def set_value(self, selector, value: float): self._value = value - event = FeatureEvent("selection", {"value": value}) + event = GraphicFeatureEvent("selection", {"value": value}) event.get_selected_index = selector.get_selected_index self._call_event_handlers(event) class LinearRegionSelectionFeature(GraphicFeature): - """ - **additional event attributes:** - - +----------------------+----------+------------------------------------+ - | attribute | type | description | - +======================+==========+====================================+ - | get_selected_indices | callable | returns indices under the selector | - +----------------------+----------+------------------------------------+ - | get_selected_data | callable | returns data under the selector | - +----------------------+----------+------------------------------------+ - - **info dict:** - - +----------+------------+-----------------------------+ - | dict key | value type | value description | - +==========+============+=============================+ - | value | np.ndarray | new [min, max] of selection | - +----------+------------+-----------------------------+ - - """ + event_info_spec = [ + { + "dict key": "value", + "type": "np.ndarray", + "description": "new [min, max] of selection", + }, + ] + + event_extra_attrs = [ + { + "attribute": "get_selected_indices", + "type": "callable", + "description": "returns indices under the selector", + }, + { + "attribute": "get_selected_data", + "type": "callable", + "description": "returns data under the selector", + }, + ] def __init__(self, value: tuple[int, int], axis: str, limits: tuple[float, float]): super().__init__() @@ -183,7 +180,7 @@ def set_value(self, selector, value: Sequence[float]): if len(self._event_handlers) < 1: return - event = FeatureEvent("selection", {"value": self.value}) + event = GraphicFeatureEvent("selection", {"value": self.value}) event.get_selected_indices = selector.get_selected_indices event.get_selected_data = selector.get_selected_data @@ -195,26 +192,26 @@ def set_value(self, selector, value: Sequence[float]): class RectangleSelectionFeature(GraphicFeature): - """ - **additional event attributes:** - - +----------------------+----------+------------------------------------+ - | attribute | type | description | - +======================+==========+====================================+ - | get_selected_indices | callable | returns indices under the selector | - +----------------------+----------+------------------------------------+ - | get_selected_data | callable | returns data under the selector | - +----------------------+----------+------------------------------------+ - - **info dict:** - - +----------+------------+-------------------------------------------+ - | dict key | value type | value description | - +==========+============+===========================================+ - | value | np.ndarray | new [xmin, xmax, ymin, ymax] of selection | - +----------+------------+-------------------------------------------+ - - """ + event_info_spec = [ + { + "dict key": "value", + "type": "np.ndarray", + "description": "new [xmin, xmax, ymin, ymax] of selection", + }, + ] + + event_extra_attrs = [ + { + "attribute": "get_selected_indices", + "type": "callable", + "description": "returns indices under the selector", + }, + { + "attribute": "get_selected_data", + "type": "callable", + "description": "returns data under the selector", + }, + ] def __init__( self, @@ -336,7 +333,7 @@ def set_value(self, selector, value: Sequence[float]): if len(self._event_handlers) < 1: return - event = FeatureEvent("selection", {"value": self.value}) + event = GraphicFeatureEvent("selection", {"value": self.value}) event.get_selected_indices = selector.get_selected_indices event.get_selected_data = selector.get_selected_data diff --git a/fastplotlib/graphics/_features/_text.py b/fastplotlib/graphics/features/_text.py similarity index 65% rename from fastplotlib/graphics/_features/_text.py rename to fastplotlib/graphics/features/_text.py index a95fe256c..d8e5e95e8 100644 --- a/fastplotlib/graphics/_features/_text.py +++ b/fastplotlib/graphics/features/_text.py @@ -2,10 +2,18 @@ import pygfx -from ._base import GraphicFeature, FeatureEvent, block_reentrance +from ._base import GraphicFeature, GraphicFeatureEvent, block_reentrance class TextData(GraphicFeature): + event_info_spec = [ + { + "dict key": "value", + "type": "str", + "description": "new text data", + }, + ] + def __init__(self, value: str): self._value = value super().__init__() @@ -19,11 +27,19 @@ def set_value(self, graphic, value: str): graphic.world_object.set_text(value) self._value = value - event = FeatureEvent(type="text", info={"value": value}) + event = GraphicFeatureEvent(type="text", info={"value": value}) self._call_event_handlers(event) class FontSize(GraphicFeature): + event_info_spec = [ + { + "dict key": "value", + "type": "float | int", + "description": "new font size", + }, + ] + def __init__(self, value: float | int): self._value = value super().__init__() @@ -37,11 +53,19 @@ def set_value(self, graphic, value: float | int): graphic.world_object.font_size = value self._value = graphic.world_object.font_size - event = FeatureEvent(type="font_size", info={"value": value}) + event = GraphicFeatureEvent(type="font_size", info={"value": value}) self._call_event_handlers(event) class TextFaceColor(GraphicFeature): + event_info_spec = [ + { + "dict key": "value", + "type": "str | np.ndarray", + "description": "new text color", + }, + ] + def __init__(self, value: str | np.ndarray | list[float] | tuple[float]): self._value = pygfx.Color(value) super().__init__() @@ -56,11 +80,19 @@ def set_value(self, graphic, value: str | np.ndarray | list[float] | tuple[float graphic.world_object.material.color = value self._value = graphic.world_object.material.color - event = FeatureEvent(type="face_color", info={"value": value}) + event = GraphicFeatureEvent(type="face_color", info={"value": value}) self._call_event_handlers(event) class TextOutlineColor(GraphicFeature): + event_info_spec = [ + { + "dict key": "value", + "type": "str | np.ndarray", + "description": "new outline color", + }, + ] + def __init__(self, value: str | np.ndarray | list[float] | tuple[float]): self._value = pygfx.Color(value) super().__init__() @@ -75,11 +107,19 @@ def set_value(self, graphic, value: str | np.ndarray | list[float] | tuple[float graphic.world_object.material.outline_color = value self._value = graphic.world_object.material.outline_color - event = FeatureEvent(type="outline_color", info={"value": value}) + event = GraphicFeatureEvent(type="outline_color", info={"value": value}) self._call_event_handlers(event) class TextOutlineThickness(GraphicFeature): + event_info_spec = [ + { + "dict key": "value", + "type": "float", + "description": "new text outline thickness", + }, + ] + def __init__(self, value: float): self._value = value super().__init__() @@ -93,5 +133,5 @@ def set_value(self, graphic, value: float): graphic.world_object.material.outline_thickness = value self._value = graphic.world_object.material.outline_thickness - event = FeatureEvent(type="outline_thickness", info={"value": value}) + event = GraphicFeatureEvent(type="outline_thickness", info={"value": value}) self._call_event_handlers(event) diff --git a/fastplotlib/graphics/_features/utils.py b/fastplotlib/graphics/features/utils.py similarity index 100% rename from fastplotlib/graphics/_features/utils.py rename to fastplotlib/graphics/features/utils.py diff --git a/fastplotlib/graphics/image.py b/fastplotlib/graphics/image.py index 8b937023b..b2a8048b3 100644 --- a/fastplotlib/graphics/image.py +++ b/fastplotlib/graphics/image.py @@ -6,7 +6,7 @@ from ..utils import quick_min_max from ._base import Graphic from .selectors import LinearSelector, LinearRegionSelector, RectangleSelector -from ._features import ( +from .features import ( TextureArray, ImageCmap, ImageVmin, @@ -71,7 +71,14 @@ def chunk_index(self) -> tuple[int, int]: class ImageGraphic(Graphic): - _features = {"data", "cmap", "vmin", "vmax", "interpolation", "cmap_interpolation"} + _features = { + "data": TextureArray, + "cmap": ImageCmap, + "vmin": ImageVmin, + "vmax": ImageVmax, + "interpolation": ImageInterpolation, + "cmap_interpolation": ImageCmapInterpolation, + } def __init__( self, @@ -100,7 +107,8 @@ def __init__( maximum value for color scaling, calculated from data if not provided cmap: str, optional, default "plasma" - colormap to use to display the data + colormap to use to display the data. For supported colormaps see the + ``cmap`` library catalogue: https://cmap-docs.readthedocs.io/en/stable/catalog/ interpolation: str, optional, default "nearest" interpolation filter, one of "nearest" or "linear" @@ -111,7 +119,8 @@ def __init__( isolated_buffer: bool, default True If True, initialize a buffer with the same shape as the input data and then set the data, useful if the data arrays are ready-only such as memmaps. - If False, the input array is itself used as the buffer. + If False, the input array is itself used as the buffer - useful if the + array is large. kwargs: additional keyword arguments passed to Graphic @@ -193,7 +202,11 @@ def data(self, data): @property def cmap(self) -> str: - """colormap name""" + """ + Get or set the colormap + + For supported colormaps see the ``cmap`` library catalogue: https://cmap-docs.readthedocs.io/en/stable/catalog/ + """ if self.data.value.ndim > 2: raise AttributeError("RGB(A) images do not have a colormap property") return self._cmap.value @@ -224,7 +237,7 @@ def vmax(self, value: float): @property def interpolation(self) -> str: - """image data interpolation method""" + """Data interpolation method""" return self._interpolation.value @interpolation.setter @@ -242,12 +255,7 @@ def cmap_interpolation(self, value: str): def reset_vmin_vmax(self): """ - Reset the vmin, vmax by estimating it from the data - - Returns - ------- - None - + Reset the vmin, vmax by estimating it from the data by subsampling. """ vmin, vmax = quick_min_max(self._data.value) @@ -255,19 +263,19 @@ def reset_vmin_vmax(self): self.vmax = vmax def add_linear_selector( - self, selection: int = None, axis: str = "x", padding: float = None, **kwargs + self, selection: int = None, axis: str = "x", **kwargs ) -> LinearSelector: """ Adds a :class:`.LinearSelector`. + Selectors are just ``Graphic`` objects, so you can manage, remove, or delete them + from a plot area just like any other ``Graphic``. + Parameters ---------- selection: int, optional initial position of the selector - padding: float, optional - pad the length of the selector - kwargs: passed to :class:`.LinearSelector` @@ -278,22 +286,12 @@ def add_linear_selector( """ if axis == "x": - size = self._data.value.shape[0] - center = size / 2 limits = (0, self._data.value.shape[1]) elif axis == "y": - size = self._data.value.shape[1] - center = size / 2 limits = (0, self._data.value.shape[0]) else: raise ValueError("`axis` must be one of 'x' | 'y'") - # default padding is 25% the height or width of the image - if padding is None: - size *= 1.25 - else: - size += padding - if selection is None: selection = limits[0] @@ -305,8 +303,6 @@ def add_linear_selector( selector = LinearSelector( selection=selection, limits=limits, - size=size, - center=center, axis=axis, parent=self, **kwargs, @@ -328,8 +324,10 @@ def add_linear_region_selector( **kwargs, ) -> LinearRegionSelector: """ - Add a :class:`.LinearRegionSelector`. Selectors are just ``Graphic`` objects, so you can manage, - remove, or delete them from a plot area just like any other ``Graphic``. + Add a :class:`.LinearRegionSelector`. + + Selectors are just ``Graphic`` objects, so you can manage, remove, or delete them + from a plot area just like any other ``Graphic``. Parameters ---------- @@ -348,7 +346,6 @@ def add_linear_region_selector( Returns ------- LinearRegionSelector - linear selection graphic """ @@ -403,13 +400,16 @@ def add_rectangle_selector( **kwargs, ) -> RectangleSelector: """ - Add a :class:`.RectangleSelector`. Selectors are just ``Graphic`` objects, so you can manage, - remove, or delete them from a plot area just like any other ``Graphic``. + Add a :class:`.RectangleSelector`. + + Selectors are just ``Graphic`` objects, so you can manage, remove, or delete them + from a plot area just like any other ``Graphic``. Parameters ---------- selection: (float, float, float, float), optional initial (xmin, xmax, ymin, ymax) of the selection + """ # default selection is 25% of the diagonal if selection is None: diff --git a/fastplotlib/graphics/line.py b/fastplotlib/graphics/line.py index 489c64930..ab5b94146 100644 --- a/fastplotlib/graphics/line.py +++ b/fastplotlib/graphics/line.py @@ -6,21 +6,35 @@ from ._positions_base import PositionsGraphic from .selectors import LinearRegionSelector, LinearSelector, RectangleSelector -from ._features import Thickness, SizeSpace +from .features import ( + Thickness, + VertexPositions, + VertexColors, + UniformColor, + VertexCmap, + SizeSpace, +) +from ..utils import quick_min_max class LineGraphic(PositionsGraphic): - _features = {"data", "colors", "cmap", "thickness", "size_space"} + _features = { + "data": VertexPositions, + "colors": (VertexColors, UniformColor), + "cmap": (VertexCmap, None), # none if UniformColor + "thickness": Thickness, + "size_space": SizeSpace, + } def __init__( self, data: Any, thickness: float = 2.0, - colors: str | np.ndarray | Iterable = "w", + colors: str | np.ndarray | Sequence = "w", uniform_color: bool = False, alpha: float = 1.0, cmap: str = None, - cmap_transform: np.ndarray | Iterable = None, + cmap_transform: np.ndarray | Sequence = None, isolated_buffer: bool = True, size_space: str = "screen", **kwargs, @@ -31,14 +45,17 @@ def __init__( Parameters ---------- data: array-like - Line data to plot, 2D must be of shape [n_points, 2], 3D must be of shape [n_points, 3] + Line data to plot. Can provide 1D, 2D, or a 3D data. + | If passing a 1D array, it is used to set the y-values and the x-values are generated as an integer range + from [0, data.size] + | 2D data must be of shape [n_points, 2]. 3D data must be of shape [n_points, 3] thickness: float, optional, default 2.0 thickness of the line colors: str, array, or iterable, default "w" specify colors as a single human-readable string, a single RGBA array, - or an iterable of strings or RGBA arrays + or a Sequence (array, tuple, or list) of strings or RGBA arrays uniform_color: bool, default ``False`` if True, uses a uniform buffer for the line color, @@ -48,14 +65,15 @@ def __init__( alpha value for the colors cmap: str, optional - apply a colormap to the line instead of assigning colors manually, this - overrides any argument passed to "colors" + Apply a colormap to the line instead of assigning colors manually, this + overrides any argument passed to "colors". For supported colormaps see the + ``cmap`` library catalogue: https://cmap-docs.readthedocs.io/en/stable/catalog/ cmap_transform: 1D array-like of numerical values, optional if provided, these values are used to map the colors from the cmap size_space: str, default "screen" - coordinate space in which the size is expressed ("screen", "world", "model") + coordinate space in which the thickness is expressed ("screen", "world", "model") **kwargs passed to Graphic @@ -107,7 +125,7 @@ def __init__( @property def thickness(self) -> float: - """line thickness""" + """Get or set the line thickness""" return self._thickness.value @thickness.setter @@ -115,24 +133,22 @@ def thickness(self, value: float): self._thickness.set_value(self, value) def add_linear_selector( - self, selection: float = None, padding: float = 0.0, axis: str = "x", **kwargs + self, selection: float = None, axis: str = "x", **kwargs ) -> LinearSelector: """ - Adds a linear selector. + Adds a :class:`.LinearSelector`. + + Selectors are just ``Graphic`` objects, so you can manage, remove, or delete them from a + plot area just like any other ``Graphic``. - Parameters - ---------- Parameters ---------- selection: float, optional - selected point on the linear selector, computed from data if not provided + selected point on the linear selector, by default the first datapoint on the line. axis: str, default "x" axis that the selector resides on - padding: float, default 0.0 - Extra padding to extend the linear selector along the orthogonal axis to make it easier to interact with. - kwargs passed to :class:`.LinearSelector` @@ -143,7 +159,7 @@ def add_linear_selector( """ bounds_init, limits, size, center = self._get_linear_selector_init_args( - axis, padding + axis, padding=0 ) if selection is None: @@ -152,8 +168,6 @@ def add_linear_selector( selector = LinearSelector( selection=selection, limits=limits, - size=size, - center=center, axis=axis, parent=self, **kwargs, @@ -174,8 +188,10 @@ def add_linear_region_selector( **kwargs, ) -> LinearRegionSelector: """ - Add a :class:`.LinearRegionSelector`. Selectors are just ``Graphic`` objects, so you can manage, - remove, or delete them from a plot area just like any other ``Graphic``. + Add a :class:`.LinearRegionSelector`. + + Selectors are just ``Graphic`` objects, so you can manage, remove, or delete them from a + plot area just like any other ``Graphic``. Parameters ---------- @@ -231,8 +247,10 @@ def add_rectangle_selector( **kwargs, ) -> RectangleSelector: """ - Add a :class:`.RectangleSelector`. Selectors are just ``Graphic`` objects, so you can manage, - remove, or delete them from a plot area just like any other ``Graphic``. + Add a :class:`.RectangleSelector`. + + Selectors are just ``Graphic`` objects, so you can manage, remove, or delete them from a + plot area just like any other ``Graphic``. Parameters ---------- @@ -298,6 +316,6 @@ def _get_linear_selector_init_args( size = int(np.ptp(magn_vals) * 1.5 + padding) # center of selector along the other axis - center = np.nanmean(magn_vals) + center = sum(quick_min_max(magn_vals)) / 2 return bounds_init, limits, size, center diff --git a/fastplotlib/graphics/line_collection.py b/fastplotlib/graphics/line_collection.py index c4af5dddc..de4139679 100644 --- a/fastplotlib/graphics/line_collection.py +++ b/fastplotlib/graphics/line_collection.py @@ -374,8 +374,6 @@ def add_linear_selector( selector = LinearSelector( selection=selection, limits=limits, - size=size, - center=center, axis=axis, parent=self, **kwargs, diff --git a/fastplotlib/graphics/scatter.py b/fastplotlib/graphics/scatter.py index 189af4844..7fd09ffca 100644 --- a/fastplotlib/graphics/scatter.py +++ b/fastplotlib/graphics/scatter.py @@ -4,11 +4,25 @@ import pygfx from ._positions_base import PositionsGraphic -from ._features import PointsSizesFeature, UniformSize, SizeSpace +from .features import ( + PointsSizesFeature, + UniformSize, + SizeSpace, + VertexPositions, + VertexColors, + UniformColor, + VertexCmap, +) class ScatterGraphic(PositionsGraphic): - _features = {"data", "sizes", "colors", "cmap", "size_space"} + _features = { + "data": VertexPositions, + "sizes": (PointsSizesFeature, UniformSize), + "colors": (VertexColors, UniformColor), + "cmap": (VertexCmap, None), + "size_space": SizeSpace, + } def __init__( self, @@ -19,7 +33,7 @@ def __init__( cmap: str = None, cmap_transform: np.ndarray = None, isolated_buffer: bool = True, - sizes: float | np.ndarray | Iterable[float] = 1, + sizes: float | np.ndarray | Sequence[float] = 1, uniform_size: bool = False, size_space: str = "screen", **kwargs, @@ -30,36 +44,38 @@ def __init__( Parameters ---------- data: array-like - Scatter data to plot, 2D must be of shape [n_points, 2], 3D must be of shape [n_points, 3] + Scatter data to plot, Can provide 2D, or a 3D data. 2D data must be of shape [n_points, 2]. + 3D data must be of shape [n_points, 3] - colors: str, array, or iterable, default "w" - specify colors as a single human readable string, a single RGBA array, - or an iterable of strings or RGBA arrays + colors: str, array, tuple, list, Sequence, default "w" + specify colors as a single human-readable string, a single RGBA array, + or a Sequence (array, tuple, or list) of strings or RGBA arrays uniform_color: bool, default False - if True, uses a uniform buffer for the scatter point colors, - basically saves GPU VRAM when the entire line has a single color + if True, uses a uniform buffer for the scatter point colors. Useful if you need to + save GPU VRAM when all points have the same color. alpha: float, optional, default 1.0 alpha value for the colors cmap: str, optional apply a colormap to the scatter instead of assigning colors manually, this - overrides any argument passed to "colors" + overrides any argument passed to "colors". For supported colormaps see the + ``cmap`` library catalogue: https://cmap-docs.readthedocs.io/en/stable/catalog/ cmap_transform: 1D array-like or list of numerical values, optional if provided, these values are used to map the colors from the cmap isolated_buffer: bool, default True whether the buffers should be isolated from the user input array. - Generally always ``True``, ``False`` is for rare advanced use. + Generally always ``True``, ``False`` is for rare advanced use if you have large arrays. sizes: float or iterable of float, optional, default 1.0 - size of the scatter points + sizes of the scatter points uniform_size: bool, default False - if True, uses a uniform buffer for the scatter point sizes, - basically saves GPU VRAM when all scatter points are the same size + if True, uses a uniform buffer for the scatter point sizes. Useful if you need to + save GPU VRAM when all points have the same size. size_space: str, default "screen" coordinate space in which the size is expressed ("screen", "world", "model") diff --git a/fastplotlib/graphics/selectors/_base_selector.py b/fastplotlib/graphics/selectors/_base_selector.py index 5158a9239..b74bcf759 100644 --- a/fastplotlib/graphics/selectors/_base_selector.py +++ b/fastplotlib/graphics/selectors/_base_selector.py @@ -16,12 +16,17 @@ class MoveInfo: stores move info for a WorldObject """ - # last position for an edge, fill, or vertex in world coordinates - # can be None, such as key events - last_position: Union[np.ndarray, None] + # The initial selection. Differs per type of selector + start_selection: Any + + # The initial world position of the cursor + start_position: np.ndarray | None + + # Delta position in world coordinates + delta: np.ndarray # WorldObject or "key" event - source: Union[WorldObject, str] + source: WorldObject | str # key bindings used to move the selector @@ -35,8 +40,6 @@ class MoveInfo: # Selector base class class BaseSelector(Graphic): - _features = {"selection"} - @property def axis(self) -> str: return self._axis @@ -138,16 +141,18 @@ def __init__( self._hover_responsive: Tuple[WorldObject, ...] = hover_responsive + # Original color of object that we change the colors of + self._original_colors = {} + + # Colors as they are changed by the hover events, so they can be restored after a move action + self._hover_colors = {} + if hover_responsive is not None: - self._original_colors = dict() for wo in self._hover_responsive: self._original_colors[wo] = wo.material.color self._axis = axis - # current delta in world coordinates - self.delta: np.ndarray = None - self.arrow_keys_modifier = arrow_keys_modifier # if not False, moves the slider on every render cycle self._key_move_value = False @@ -275,9 +280,14 @@ def _move_start(self, event_source: WorldObject, ev): pygfx ``Event`` """ - last_position = self._plot_area.map_screen_to_world(ev) + position = self._plot_area.map_screen_to_world(ev) - self._move_info = MoveInfo(last_position=last_position, source=event_source) + self._move_info = MoveInfo( + start_selection=None, + start_position=position, + delta=np.zeros_like(position), + source=event_source, + ) self._moving = True self._initial_controller_state = self._plot_area.controller.enabled @@ -300,21 +310,14 @@ def _move(self, ev): # disable controller during moves self._plot_area.controller.enabled = False - # get pointer current world position - world_pos = self._plot_area.map_screen_to_world(ev) + # get pointer current world position, in 'mouse capute mode' + world_pos = self._plot_area.map_screen_to_world(ev, allow_outside=True) - # outside this viewport - if world_pos is None: - return - - # compute the delta - self.delta = world_pos - self._move_info.last_position + # update the delta + self._move_info.delta = world_pos - self._move_info.start_position self._pygfx_event = ev - self._move_graphic(self.delta) - - # update last position - self._move_info.last_position = world_pos + self._move_graphic(self._move_info) # restore the initial controller state # if it was disabled, keep it disabled @@ -327,6 +330,11 @@ def _move_end(self, ev): self._move_info = None self._moving = False + # Reset hover state + for wo, color in self._hover_colors.items(): + wo.material.color = color + self._hover_colors.clear() + # restore the initial controller state # if it was disabled, keep it disabled if self._initial_controller_state is not None: @@ -362,24 +370,29 @@ def _move_to_pointer(self, ev): if world_pos is None: return - self.delta = world_pos - current_pos_world + delta = world_pos - current_pos_world self._pygfx_event = ev # use fill by default as the source, such as in region selectors if len(self._fill) > 0: - self._move_info = MoveInfo( - last_position=current_pos_world, source=self._fill[0] + move_info = MoveInfo( + start_selection=None, + start_position=None, + delta=delta, + source=self._fill[0], ) # else use an edge, such as for linear selector else: - self._move_info = MoveInfo( - last_position=current_pos_world, source=self._edges[0] + move_info = MoveInfo( + start_position=current_pos_world, + last_position=current_pos_world, + source=self._edges[0], ) - self._move_graphic(self.delta) - self._move_info = None + self._move_graphic(move_info) def _pointer_enter(self, ev): + if self._hover_responsive is None: return @@ -390,17 +403,23 @@ def _pointer_enter(self, ev): if wo in self._edges: self._edge_hovered = True - wo.material.color = "magenta" + if self._moving: + self._hover_colors[wo] = "magenta" + else: + wo.material.color = "magenta" def _pointer_leave(self, ev): if self._hover_responsive is None: return + self._edge_hovered = False + # reset colors for wo in self._hover_responsive: - wo.material.color = self._original_colors[wo] - - self._edge_hovered = False + if self._moving: + self._hover_colors[wo] = self._original_colors[wo] + else: + wo.material.color = self._original_colors[wo] def _toggle_arrow_key_moveable(self, ev): self.arrow_key_events_enabled = not self.arrow_key_events_enabled @@ -413,15 +432,23 @@ def _key_hold(self): # set event source # use fill by default as the source if len(self._fill) > 0: - self._move_info = MoveInfo(last_position=None, source=self._fill[0]) + move_info = MoveInfo( + start_selection=None, + start_position=None, + delta=delta, + source=self._fill[0], + ) # else use an edge else: - self._move_info = MoveInfo(last_position=None, source=self._edges[0]) + move_info = MoveInfo( + start_selection=None, + start_position=None, + delta=delta, + source=self._edges[0], + ) # move the graphic - self._move_graphic(delta=delta) - - self._move_info = None + self._move_graphic(move_info) def _key_down(self, ev): # key bind modifier must be set and must be used for the event @@ -443,8 +470,6 @@ def _key_up(self, ev): if ev.key in key_bind_direction.keys(): self._key_move_value = False - self._move_info = None - def _fpl_prepare_del(self): if hasattr(self, "_pfunc_fill"): self._plot_area.renderer.remove_event_handler( diff --git a/fastplotlib/graphics/selectors/_linear.py b/fastplotlib/graphics/selectors/_linear.py index fe57036a3..64a673768 100644 --- a/fastplotlib/graphics/selectors/_linear.py +++ b/fastplotlib/graphics/selectors/_linear.py @@ -7,11 +7,13 @@ from .._base import Graphic from .._collection_base import GraphicCollection -from .._features._selection_features import LinearSelectionFeature -from ._base_selector import BaseSelector +from ..features._selection_features import LinearSelectionFeature +from ._base_selector import BaseSelector, MoveInfo class LinearSelector(BaseSelector): + _features = {"selection": LinearSelectionFeature} + @property def parent(self) -> Graphic: return self._parent @@ -73,8 +75,6 @@ def __init__( self, selection: float, limits: Sequence[float], - size: float, - center: float, axis: str = "x", parent: Graphic = None, edge_color: str | Sequence[float] | np.ndarray = "w", @@ -93,12 +93,6 @@ def __init__( limits: (int, int) (min, max) limits along the x or y-axis for the selector, in data space - size: float - size of the selector, usually the range of the data - - center: float - center offset of the selector on the orthogonal axis, usually the data mean - axis: str, default "x" "x" | "y", the axis along which the selector can move @@ -129,29 +123,22 @@ def __init__( self._limits = np.asarray(limits) - end_points = [-size / 2, size / 2] - if axis == "x": - xs = np.array([selection, selection]) - ys = np.array(end_points) - zs = np.zeros(2) + xs = np.array([selection, selection], dtype=np.float32) + ys = np.array([0, 1], dtype=np.float32) + zs = np.zeros(2, dtype=np.float32) - line_data = np.column_stack([xs, ys, zs]) elif axis == "y": - xs = np.array(end_points) - ys = np.array([selection, selection]) - zs = np.zeros(2) + xs = np.array([0, 1], dtype=np.float32) + ys = np.array([selection, selection], dtype=np.float32) + zs = np.zeros(2, dtype=np.float32) - line_data = np.column_stack([xs, ys, zs]) else: - raise ValueError("`axis` must be one of 'x' or 'y'") + raise ValueError("`axis` must be one of 'x' | 'y'") - line_data = line_data.astype(np.float32) + line_data = np.column_stack([xs, ys, zs]) - if thickness < 1.1: - material = pygfx.LineThinMaterial - else: - material = pygfx.LineMaterial + material = pygfx.LineInfiniteSegmentMaterial self.colors_outer = pygfx.Color([0.3, 0.3, 0.3, 1.0]) @@ -175,12 +162,10 @@ def __init__( world_object.add(self.line_outer) world_object.add(line_inner) - self._move_info: dict = None - if axis == "x": - offset = (parent.offset[0], center + parent.offset[1], 0) + offset = (parent.offset[0], 0, 0) elif axis == "y": - offset = (center + parent.offset[0], parent.offset[1], 0) + offset = (0, parent.offset[1], 0) # init base selector BaseSelector.__init__( @@ -274,7 +259,7 @@ def _get_selected_index(self, graphic): return min(round(index), upper_bound) - def _move_graphic(self, delta: np.ndarray): + def _move_graphic(self, move_info: MoveInfo): """ Moves the graphic @@ -285,7 +270,9 @@ def _move_graphic(self, delta: np.ndarray): """ - if self.axis == "x": - self.selection = self.selection + delta[0] - else: - self.selection = self.selection + delta[1] + # If this the first move in this drag, store initial selection + if move_info.start_selection is None: + move_info.start_selection = self.selection + + delta = move_info.delta[0] if self.axis == "x" else move_info.delta[1] + self.selection = move_info.start_selection + delta diff --git a/fastplotlib/graphics/selectors/_linear_region.py b/fastplotlib/graphics/selectors/_linear_region.py index c1e6095f8..14160b10c 100644 --- a/fastplotlib/graphics/selectors/_linear_region.py +++ b/fastplotlib/graphics/selectors/_linear_region.py @@ -6,11 +6,13 @@ from .._base import Graphic from .._collection_base import GraphicCollection -from .._features._selection_features import LinearRegionSelectionFeature -from ._base_selector import BaseSelector +from ..features._selection_features import LinearRegionSelectionFeature +from ._base_selector import BaseSelector, MoveInfo class LinearRegionSelector(BaseSelector): + _features = {"selection": LinearRegionSelectionFeature} + @property def parent(self) -> Graphic | None: """graphic that the selector is associated with""" @@ -286,7 +288,7 @@ def get_selected_data( # slices n_datapoints dim data_selections.append(g.data[s]) - return source.data[s] + return data_selections else: if ixs.size == 0: # empty selection @@ -366,31 +368,29 @@ def get_selected_indices( # indices map directly to grid geometry for image data buffer return np.arange(*bounds, dtype=int) - def _move_graphic(self, delta: np.ndarray): + def _move_graphic(self, move_info: MoveInfo): + + # If this the first move in this drag, store initial selection + if move_info.start_selection is None: + move_info.start_selection = self.selection + # add delta to current min, max to get new positions - if self.axis == "x": - # add x value - new_min, new_max = self.selection + delta[0] + delta = move_info.delta[0] if self.axis == "x" else move_info.delta[1] - elif self.axis == "y": - # add y value - new_min, new_max = self.selection + delta[1] + # Get original selection + cur_min, cur_max = move_info.start_selection # move entire selector if event source was fill if self._move_info.source == self.fill: - # prevent weird shrinkage of selector if one edge is already at the limit - if self.selection[0] == self.limits[0] and new_min < self.limits[0]: - # self._move_end(None) # TODO: cancel further movement to prevent weird asynchronization with pointer - return - if self.selection[1] == self.limits[1] and new_max > self.limits[1]: - # self._move_end(None) - return - - # move entire selector - self._selection.set_value(self, (new_min, new_max)) + # Limit the delta to avoid weird resizine behavior + min_delta = self.limits[0] - cur_min + max_delta = self.limits[1] - cur_max + delta = np.clip(delta, min_delta, max_delta) + # Update both bounds with equal amount + self._selection.set_value(self, (cur_min + delta, cur_max + delta)) return - # if selector is not resizable return + # if selector not resizable return if not self._resizable: return @@ -398,8 +398,10 @@ def _move_graphic(self, delta: np.ndarray): # move the edge that caused the event if self._move_info.source == self.edges[0]: # change only left or bottom bound - self._selection.set_value(self, (new_min, self._selection.value[1])) + new_min = min(cur_min + delta, cur_max) + self._selection.set_value(self, (new_min, cur_max)) elif self._move_info.source == self.edges[1]: # change only right or top bound - self._selection.set_value(self, (self.selection[0], new_max)) + new_max = max(cur_max + delta, cur_min) + self._selection.set_value(self, (cur_min, new_max)) diff --git a/fastplotlib/graphics/selectors/_polygon.py b/fastplotlib/graphics/selectors/_polygon.py index a4ecd440c..22e42e63e 100644 --- a/fastplotlib/graphics/selectors/_polygon.py +++ b/fastplotlib/graphics/selectors/_polygon.py @@ -62,11 +62,16 @@ def _add_segment(self, ev): """After click event, adds a new line segment""" self._current_mode = "add" - last_position = self._plot_area.map_screen_to_world(ev) - self._move_info = MoveInfo(last_position=last_position, source=None) + position = self._plot_area.map_screen_to_world(ev) + self._move_info = MoveInfo( + start_selection=None, + start_position=position, + delta=np.zeros_like(position), + source=None, + ) # line with same position for start and end until mouse moves - data = np.array([last_position, last_position]) + data = np.array([position, position]) new_line = pygfx.Line( geometry=pygfx.Geometry(positions=data.astype(np.float32)), diff --git a/fastplotlib/graphics/selectors/_rectangle.py b/fastplotlib/graphics/selectors/_rectangle.py index 51c3209b1..e3dd3887e 100644 --- a/fastplotlib/graphics/selectors/_rectangle.py +++ b/fastplotlib/graphics/selectors/_rectangle.py @@ -7,11 +7,13 @@ from .._collection_base import GraphicCollection from .._base import Graphic -from .._features import RectangleSelectionFeature -from ._base_selector import BaseSelector +from ..features import RectangleSelectionFeature +from ._base_selector import BaseSelector, MoveInfo class RectangleSelector(BaseSelector): + _features = {"selection": RectangleSelectionFeature} + @property def parent(self) -> Graphic | None: """Graphic that selector is associated with.""" @@ -22,7 +24,7 @@ def selection(self) -> np.ndarray[float]: """ (xmin, xmax, ymin, ymax) of the rectangle selection """ - return self._selection.value + return self._selection.value.copy() @selection.setter def selection(self, selection: Sequence[float]): @@ -58,7 +60,7 @@ def __init__( edge_color=(0.8, 0.6, 0), edge_thickness: float = 8, vertex_color=(0.7, 0.4, 0), - vertex_thickness: float = 8, + vertex_size: float = 8, arrow_keys_modifier: str = "Shift", name: str = None, ): @@ -81,14 +83,17 @@ def __init__( if ``True``, the edges can be dragged to resize the selection fill_color: str, array, or tuple - fill color for the selector, passed to pygfx.Color + fill color for the selector as a str or RGBA array edge_color: str, array, or tuple - edge color for the selector, passed to pygfx.Color + edge color for the selector as a str or RGBA array edge_thickness: float, default 8 edge thickness + vertex_color: str, array, or tuple + vertex color for the selector as a str or RGBA array + arrow_keys_modifier: str modifier key that must be pressed to initiate movement using arrow keys, must be one of: "Control", "Shift", "Alt" or ``None`` @@ -209,10 +214,10 @@ def __init__( bottom_right_vertex_data = (xmax, ymin, 1) top_left_vertex = pygfx.Points( - pygfx.Geometry(positions=[top_left_vertex_data], sizes=[vertex_thickness]), + pygfx.Geometry(positions=[top_left_vertex_data], sizes=[vertex_size]), pygfx.PointsMarkerMaterial( marker="square", - size=vertex_thickness, + size=vertex_size, color=self.vertex_color, size_mode="vertex", edge_color=self.vertex_color, @@ -220,10 +225,10 @@ def __init__( ) top_right_vertex = pygfx.Points( - pygfx.Geometry(positions=[top_right_vertex_data], sizes=[vertex_thickness]), + pygfx.Geometry(positions=[top_right_vertex_data], sizes=[vertex_size]), pygfx.PointsMarkerMaterial( marker="square", - size=vertex_thickness, + size=vertex_size, color=self.vertex_color, size_mode="vertex", edge_color=self.vertex_color, @@ -231,12 +236,10 @@ def __init__( ) bottom_left_vertex = pygfx.Points( - pygfx.Geometry( - positions=[bottom_left_vertex_data], sizes=[vertex_thickness] - ), + pygfx.Geometry(positions=[bottom_left_vertex_data], sizes=[vertex_size]), pygfx.PointsMarkerMaterial( marker="square", - size=vertex_thickness, + size=vertex_size, color=self.vertex_color, size_mode="vertex", edge_color=self.vertex_color, @@ -244,12 +247,10 @@ def __init__( ) bottom_right_vertex = pygfx.Points( - pygfx.Geometry( - positions=[bottom_right_vertex_data], sizes=[vertex_thickness] - ), + pygfx.Geometry(positions=[bottom_right_vertex_data], sizes=[vertex_size]), pygfx.PointsMarkerMaterial( marker="square", - size=vertex_thickness, + size=vertex_size, color=self.vertex_color, size_mode="vertex", edge_color=self.vertex_color, @@ -477,33 +478,41 @@ def get_selected_indices( return ixs - def _move_graphic(self, delta: np.ndarray): + def _move_graphic(self, move_info: MoveInfo): - # new selection positions - xmin_new = self.selection[0] + delta[0] - xmax_new = self.selection[1] + delta[0] - ymin_new = self.selection[2] + delta[1] - ymax_new = self.selection[3] + delta[1] + # If this the first move in this drag, store initial selection + if move_info.start_selection is None: + move_info.start_selection = self.selection + + # add delta to current min, max to get new positions + deltax, deltay = move_info.delta[0], move_info.delta[1] + + # Get original selection + xmin, xmax, ymin, ymax = move_info.start_selection # move entire selector if source is fill if self._move_info.source == self.fill: - if self.selection[0] == self.limits[0] and xmin_new < self.limits[0]: - return - if self.selection[1] == self.limits[1] and xmax_new > self.limits[1]: - return - if self.selection[2] == self.limits[2] and ymin_new < self.limits[2]: - return - if self.selection[3] == self.limits[3] and ymax_new > self.limits[3]: - return - # set thew new bounds - self._selection.set_value(self, (xmin_new, xmax_new, ymin_new, ymax_new)) + # Limit the delta to avoid weird resizine behavior + min_deltax = self.limits[0] - xmin + max_deltax = self.limits[1] - xmax + min_deltay = self.limits[2] - ymin + max_deltay = self.limits[3] - ymax + deltax = np.clip(deltax, min_deltax, max_deltax) + deltay = np.clip(deltay, min_deltay, max_deltay) + # Update all bounds with equal amount + self._selection.set_value( + self, (xmin + deltax, xmax + deltax, ymin + deltay, ymax + deltay) + ) return # if selector not resizable return if not self._resizable: return - xmin, xmax, ymin, ymax = self.selection + xmin_new = min(xmin + deltax, xmax) + xmax_new = max(xmax + deltax, xmin) + ymin_new = min(ymin + deltay, ymax) + ymax_new = max(ymax + deltay, ymin) if self._move_info.source == self.vertices[0]: # bottom left self._selection.set_value(self, (xmin_new, xmax, ymin_new, ymax)) diff --git a/fastplotlib/graphics/text.py b/fastplotlib/graphics/text.py index e3794743a..fba3962ad 100644 --- a/fastplotlib/graphics/text.py +++ b/fastplotlib/graphics/text.py @@ -2,7 +2,7 @@ import numpy as np from ._base import Graphic -from ._features import ( +from .features import ( TextData, FontSize, TextFaceColor, @@ -13,11 +13,11 @@ class TextGraphic(Graphic): _features = { - "text", - "font_size", - "face_color", - "outline_color", - "outline_thickness", + "text": TextData, + "font_size": FontSize, + "face_color": TextFaceColor, + "outline_color": TextOutlineColor, + "outline_thickness": TextOutlineThickness, } def __init__( @@ -43,10 +43,10 @@ def __init__( font_size: float | int, default 10 font size - face_color: str or array, default "w" + face_color: str, array, list, tuple, default "w" str or RGBA array to set the color of the text - outline_color: str or array, default "w" + outline_color: str, array, list, tuple, default "w" str or RGBA array to set the outline color of the text outline_thickness: float, default 0 @@ -102,7 +102,7 @@ def world_object(self) -> pygfx.Text: @property def text(self) -> str: - """the text displayed""" + """Get or set the text""" return self._text.value @text.setter @@ -111,7 +111,7 @@ def text(self, text: str): @property def font_size(self) -> float | int: - """ "text font size""" + """Get or set the font size""" return self._font_size.value @font_size.setter @@ -120,7 +120,7 @@ def font_size(self, size: float | int): @property def face_color(self) -> pygfx.Color: - """text face color""" + """Get or set the face color""" return self._face_color.value @face_color.setter @@ -129,7 +129,7 @@ def face_color(self, color: str | np.ndarray | list[float] | tuple[float]): @property def outline_thickness(self) -> float: - """text outline thickness""" + """Get or set the outline thickness""" return self._outline_thickness.value @outline_thickness.setter @@ -138,7 +138,7 @@ def outline_thickness(self, thickness: float): @property def outline_color(self) -> pygfx.Color: - """text outline color""" + """Get or set the outline color""" return self._outline_color.value @outline_color.setter diff --git a/fastplotlib/layouts/__init__.py b/fastplotlib/layouts/__init__.py index 8fb1d54d8..23839586c 100644 --- a/fastplotlib/layouts/__init__.py +++ b/fastplotlib/layouts/__init__.py @@ -1,4 +1,5 @@ from ._figure import Figure +from ._subplot import Subplot from ._utils import IMGUI if IMGUI: diff --git a/fastplotlib/layouts/_engine.py b/fastplotlib/layouts/_engine.py index 877a7fbab..bf73d5f0d 100644 --- a/fastplotlib/layouts/_engine.py +++ b/fastplotlib/layouts/_engine.py @@ -7,7 +7,7 @@ from ._rect import RectManager -class UnderlayCamera(pygfx.Camera): +class ScreenSpaceCamera(pygfx.Camera): """ Same as pygfx.ScreenCoordsCamera but y-axis is inverted. diff --git a/fastplotlib/layouts/_figure.py b/fastplotlib/layouts/_figure.py index a1bae965e..bfd97000b 100644 --- a/fastplotlib/layouts/_figure.py +++ b/fastplotlib/layouts/_figure.py @@ -19,8 +19,9 @@ ) from ._utils import controller_types as valid_controller_types from ._subplot import Subplot -from ._engine import GridLayout, WindowLayout, UnderlayCamera +from ._engine import GridLayout, WindowLayout, ScreenSpaceCamera from .. import ImageGraphic +from ..tools import Tooltip class Figure: @@ -51,6 +52,7 @@ def __init__( canvas_kwargs: dict = None, size: tuple[int, int] = (500, 300), names: list | np.ndarray = None, + show_tooltips: bool = False, ): """ Create a Figure containing Subplots. @@ -102,9 +104,10 @@ def __init__( | this syncs subplot_a, subplot_b and subplot_e together; syncs subplot_c and subplot_d together 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 - ``controllers`` are provided. + Directly provide pygfx.Controller instances(s). Useful if you want to use a ``Controller`` from an existing + subplot or a ``Controller`` you have already instantiated. Also useful if you want to provide a custom + ``Controller`` subclass. Other controller kwargs, i.e. ``controller_types`` and ``controller_ids`` + are ignored if `controllers` are provided. canvas: str, BaseRenderCanvas, pygfx.Texture Canvas to draw the figure onto, usually auto-selected based on running environment. @@ -121,6 +124,9 @@ def __init__( names: list or array of str, optional subplot names + show_tooltips: bool, default False + show tooltips on graphics + """ if rects is not None: @@ -144,7 +150,9 @@ def __init__( else: if not all(isinstance(v, (int, np.integer)) for v in shape): - raise TypeError("shape argument must be a tuple[n_rows, n_cols]") + raise TypeError( + f"shape argument must be a tuple[n_rows, n_cols], you have passed: {shape}" + ) n_subplots = shape[0] * shape[1] layout_mode = "grid" @@ -154,13 +162,40 @@ def __init__( rects = [None] * n_subplots if names is not None: + # user has specified subplot names subplot_names = np.asarray(names).flatten() - if subplot_names.size != n_subplots: + # make an array without nones for sanity checks + subplot_names_without_nones = subplot_names[subplot_names != np.array(None)] + + # make sure all names are unique + if ( + subplot_names_without_nones.size + != np.unique(subplot_names_without_nones).size + ): + raise ValueError( + f"subplot `names` must be unique, you have provided: {names}" + ) + + # check that there are enough subplots given the number of names + if subplot_names.size > n_subplots: raise ValueError( - f"must provide same number of subplot `names` as specified by shape, extents, or rects: {n_subplots}" + f"must provide same number or fewer subplot `names` than number of supblots specified by shape, " + f"extents, or rects." + f"You have specified {n_subplots} subplots, but {subplot_names.size} subplot names." + ) + + if subplot_names.size < n_subplots: + # pad the subplot names with nones + subplot_names = np.concatenate( + [ + subplot_names, + np.asarray([None] * (n_subplots - subplot_names.size)), + ] ) else: + # no user specified subplot names if layout_mode == "grid": + # make names that show the [row index, col index] subplot_names = np.asarray( list(map(str, product(range(shape[0]), range(shape[1])))) ) @@ -188,7 +223,7 @@ def __init__( if cameras.size != n_subplots: raise ValueError( - f"Number of cameras: {cameras.size} does not match the number of subplots: {n_subplots}" + f"Number of cameras: {cameras.size} does not match the number of specified subplots: {n_subplots}" ) # create the cameras @@ -213,8 +248,8 @@ def __init__( pass else: raise TypeError( - "controllers argument must be a single pygfx.Controller instance, or a Iterable of " - "pygfx.Controller instances" + f"controllers argument must be a single pygfx.Controller instance, or a Iterable of " + f"pygfx.Controller instances. You have passed: {controllers}" ) subplot_controllers: np.ndarray[pygfx.Controller] = np.asarray( @@ -242,7 +277,8 @@ def __init__( else: raise ValueError( f"`controller_ids` must be one of 'sync', an array/list of subplot names, or an array/list of " - f"integer ids. See the docstring for more details." + f"integer ids. You have passed: {controller_ids}.\n" + f"See the docstring for more details." ) # list controller_ids @@ -259,12 +295,14 @@ def __init__( # make sure each controller_id str is a subplot name if not all([n in subplot_names for n in ids_flat]): raise KeyError( - f"all `controller_ids` strings must be one of the subplot names" + f"all `controller_ids` strings must be one of the subplot names. You have passed " + f"the following `controller_ids`:\n{controller_ids}\n\n" + f"and the following subplot names:\n{subplot_names}" ) if len(ids_flat) > len(set(ids_flat)): raise ValueError( - "id strings must not appear twice in `controller_ids`" + f"id strings must not appear twice in `controller_ids`: \n{controller_ids}" ) # initialize controller_ids array @@ -284,7 +322,8 @@ def __init__( controller_ids = np.asarray(controller_ids).flatten() if controller_ids.max() < 0: raise ValueError( - "if passing an integer array of `controller_ids`, all the integers must be positive." + f"if passing an integer array of `controller_ids`, " + f"all the integers must be positive:{controller_ids}" ) else: @@ -295,7 +334,8 @@ def __init__( if controller_ids.size != n_subplots: raise ValueError( - f"Number of controller_ids does not match the number of subplots: {n_subplots}" + f"Number of controller_ids: {controller_ids.size} " + f"does not match the number of subplots: {n_subplots}" ) if controller_types is None: @@ -409,13 +449,23 @@ def __init__( canvas_rect=self.get_pygfx_render_area(), ) - self._underlay_camera = UnderlayCamera() - + # underlay render pass + self._underlay_camera = ScreenSpaceCamera() self._underlay_scene = pygfx.Scene() for subplot in self._subplots.ravel(): self._underlay_scene.add(subplot.frame._world_object) + # overlay render pass + self._overlay_camera = ScreenSpaceCamera() + self._overlay_scene = pygfx.Scene() + + # tooltip in overlay render pass + self._tooltip_manager = Tooltip() + self._overlay_scene.add(self._tooltip_manager.world_object) + + self._show_tooltips = show_tooltips + self._animate_funcs_pre: list[callable] = list() self._animate_funcs_post: list[callable] = list() @@ -429,7 +479,7 @@ def __init__( @property def shape(self) -> list[tuple[int, int, int, int]] | tuple[int, int]: - """[n_rows, n_cols]""" + """Only for grid layouts of subplots: [n_rows, n_cols]""" if isinstance(self.layout, GridLayout): return self.layout.shape @@ -483,6 +533,29 @@ def names(self) -> np.ndarray[str]: names.flags.writeable = False return names + @property + def tooltip_manager(self) -> Tooltip: + """manage tooltips""" + return self._tooltip_manager + + @property + def show_tooltips(self) -> bool: + """show/hide tooltips for all graphics""" + return self._show_tooltips + + @show_tooltips.setter + def show_tooltips(self, val: bool): + self._show_tooltips = val + + if val: + # register all graphics + for subplot in self: + for graphic in subplot.graphics: + self._tooltip_manager.register(graphic) + + elif not val: + self._tooltip_manager.unregister_all() + def _render(self, draw=True): # draw the underlay planes self.renderer.render(self._underlay_scene, self._underlay_camera, flush=False) @@ -492,6 +565,9 @@ def _render(self, draw=True): for subplot in self: subplot._render() + # overlay render pass + self.renderer.render(self._overlay_scene, self._overlay_camera, flush=False) + self.renderer.flush() # call post-render animate functions @@ -711,7 +787,7 @@ def export_numpy(self, rgb: bool = False) -> np.ndarray: def export(self, uri: str | Path | bytes, **kwargs): """ - Use ``imageio`` for writing the current Figure to a file, or return a byte string. + Use ``imageio`` to export the current Figure to a file, or return a byte string. Must have ``imageio`` installed. Parameters diff --git a/fastplotlib/layouts/_graphic_methods_mixin.py b/fastplotlib/layouts/_graphic_methods_mixin.py index a04b681f5..f2595923f 100644 --- a/fastplotlib/layouts/_graphic_methods_mixin.py +++ b/fastplotlib/layouts/_graphic_methods_mixin.py @@ -32,7 +32,7 @@ def add_image( interpolation: str = "nearest", cmap_interpolation: str = "linear", isolated_buffer: bool = True, - **kwargs + **kwargs, ) -> ImageGraphic: """ @@ -51,7 +51,8 @@ def add_image( maximum value for color scaling, calculated from data if not provided cmap: str, optional, default "plasma" - colormap to use to display the data + colormap to use to display the data. For supported colormaps see the + ``cmap`` library catalogue: https://cmap-docs.readthedocs.io/en/stable/catalog/ interpolation: str, optional, default "nearest" interpolation filter, one of "nearest" or "linear" @@ -62,7 +63,8 @@ def add_image( isolated_buffer: bool, default True If True, initialize a buffer with the same shape as the input data and then set the data, useful if the data arrays are ready-only such as memmaps. - If False, the input array is itself used as the buffer. + If False, the input array is itself used as the buffer - useful if the + array is large. kwargs: additional keyword arguments passed to Graphic @@ -78,7 +80,7 @@ def add_image( interpolation, cmap_interpolation, isolated_buffer, - **kwargs + **kwargs, ) def add_line_collection( @@ -96,7 +98,7 @@ def add_line_collection( metadatas: Union[Sequence[Any], numpy.ndarray] = None, isolated_buffer: bool = True, kwargs_lines: list[dict] = None, - **kwargs + **kwargs, ) -> LineCollection: """ @@ -169,21 +171,21 @@ def add_line_collection( metadatas, isolated_buffer, kwargs_lines, - **kwargs + **kwargs, ) def add_line( self, data: Any, thickness: float = 2.0, - colors: Union[str, numpy.ndarray, Iterable] = "w", + colors: Union[str, numpy.ndarray, Sequence] = "w", uniform_color: bool = False, alpha: float = 1.0, cmap: str = None, - cmap_transform: Union[numpy.ndarray, Iterable] = None, + cmap_transform: Union[numpy.ndarray, Sequence] = None, isolated_buffer: bool = True, size_space: str = "screen", - **kwargs + **kwargs, ) -> LineGraphic: """ @@ -192,14 +194,17 @@ def add_line( Parameters ---------- data: array-like - Line data to plot, 2D must be of shape [n_points, 2], 3D must be of shape [n_points, 3] + Line data to plot. Can provide 1D, 2D, or a 3D data. + | If passing a 1D array, it is used to set the y-values and the x-values are generated as an integer range + from [0, data.size] + | 2D data must be of shape [n_points, 2]. 3D data must be of shape [n_points, 3] thickness: float, optional, default 2.0 thickness of the line colors: str, array, or iterable, default "w" specify colors as a single human-readable string, a single RGBA array, - or an iterable of strings or RGBA arrays + or a Sequence (array, tuple, or list) of strings or RGBA arrays uniform_color: bool, default ``False`` if True, uses a uniform buffer for the line color, @@ -209,14 +214,15 @@ def add_line( alpha value for the colors cmap: str, optional - apply a colormap to the line instead of assigning colors manually, this - overrides any argument passed to "colors" + Apply a colormap to the line instead of assigning colors manually, this + overrides any argument passed to "colors". For supported colormaps see the + ``cmap`` library catalogue: https://cmap-docs.readthedocs.io/en/stable/catalog/ cmap_transform: 1D array-like of numerical values, optional if provided, these values are used to map the colors from the cmap size_space: str, default "screen" - coordinate space in which the size is expressed ("screen", "world", "model") + coordinate space in which the thickness is expressed ("screen", "world", "model") **kwargs passed to Graphic @@ -234,7 +240,7 @@ def add_line( cmap_transform, isolated_buffer, size_space, - **kwargs + **kwargs, ) def add_line_stack( @@ -253,7 +259,7 @@ def add_line_stack( separation: float = 10.0, separation_axis: str = "y", kwargs_lines: list[dict] = None, - **kwargs + **kwargs, ) -> LineStack: """ @@ -334,7 +340,7 @@ def add_line_stack( separation, separation_axis, kwargs_lines, - **kwargs + **kwargs, ) def add_scatter( @@ -346,10 +352,10 @@ def add_scatter( cmap: str = None, cmap_transform: numpy.ndarray = None, isolated_buffer: bool = True, - sizes: Union[float, numpy.ndarray, Iterable[float]] = 1, + sizes: Union[float, numpy.ndarray, Sequence[float]] = 1, uniform_size: bool = False, size_space: str = "screen", - **kwargs + **kwargs, ) -> ScatterGraphic: """ @@ -358,36 +364,38 @@ def add_scatter( Parameters ---------- data: array-like - Scatter data to plot, 2D must be of shape [n_points, 2], 3D must be of shape [n_points, 3] + Scatter data to plot, Can provide 2D, or a 3D data. 2D data must be of shape [n_points, 2]. + 3D data must be of shape [n_points, 3] - colors: str, array, or iterable, default "w" - specify colors as a single human readable string, a single RGBA array, - or an iterable of strings or RGBA arrays + colors: str, array, tuple, list, Sequence, default "w" + specify colors as a single human-readable string, a single RGBA array, + or a Sequence (array, tuple, or list) of strings or RGBA arrays uniform_color: bool, default False - if True, uses a uniform buffer for the scatter point colors, - basically saves GPU VRAM when the entire line has a single color + if True, uses a uniform buffer for the scatter point colors. Useful if you need to + save GPU VRAM when all points have the same color. alpha: float, optional, default 1.0 alpha value for the colors cmap: str, optional apply a colormap to the scatter instead of assigning colors manually, this - overrides any argument passed to "colors" + overrides any argument passed to "colors". For supported colormaps see the + ``cmap`` library catalogue: https://cmap-docs.readthedocs.io/en/stable/catalog/ cmap_transform: 1D array-like or list of numerical values, optional if provided, these values are used to map the colors from the cmap isolated_buffer: bool, default True whether the buffers should be isolated from the user input array. - Generally always ``True``, ``False`` is for rare advanced use. + Generally always ``True``, ``False`` is for rare advanced use if you have large arrays. sizes: float or iterable of float, optional, default 1.0 - size of the scatter points + sizes of the scatter points uniform_size: bool, default False - if True, uses a uniform buffer for the scatter point sizes, - basically saves GPU VRAM when all scatter points are the same size + if True, uses a uniform buffer for the scatter point sizes. Useful if you need to + save GPU VRAM when all points have the same size. size_space: str, default "screen" coordinate space in which the size is expressed ("screen", "world", "model") @@ -409,7 +417,7 @@ def add_scatter( sizes, uniform_size, size_space, - **kwargs + **kwargs, ) def add_text( @@ -422,7 +430,7 @@ def add_text( screen_space: bool = True, offset: tuple[float] = (0, 0, 0), anchor: str = "middle-center", - **kwargs + **kwargs, ) -> TextGraphic: """ @@ -436,10 +444,10 @@ def add_text( font_size: float | int, default 10 font size - face_color: str or array, default "w" + face_color: str, array, list, tuple, default "w" str or RGBA array to set the color of the text - outline_color: str or array, default "w" + outline_color: str, array, list, tuple, default "w" str or RGBA array to set the outline color of the text outline_thickness: float, default 0 @@ -473,5 +481,5 @@ def add_text( screen_space, offset, anchor, - **kwargs + **kwargs, ) diff --git a/fastplotlib/layouts/_imgui_figure.py b/fastplotlib/layouts/_imgui_figure.py index 40145fe50..c54890239 100644 --- a/fastplotlib/layouts/_imgui_figure.py +++ b/fastplotlib/layouts/_imgui_figure.py @@ -44,6 +44,7 @@ def __init__( canvas_kwargs: dict = None, size: tuple[int, int] = (500, 300), names: list | np.ndarray = None, + show_tooltips: bool = False, ): self._guis: dict[str, EdgeWindow] = {k: None for k in GUI_EDGES} @@ -60,6 +61,7 @@ def __init__( canvas_kwargs=canvas_kwargs, size=size, names=names, + show_tooltips=show_tooltips, ) self._imgui_renderer = ImguiRenderer(self.renderer.device, self.canvas) @@ -150,7 +152,7 @@ def _draw_imgui(self) -> imgui.ImDrawData: def add_gui(self, gui: EdgeWindow): """ - Add a GUI to the Figure. GUIs can be added to the top, bottom, left or right edge. + Add a GUI to the Figure. GUIs can be added to the left or bottom edge. Parameters ---------- @@ -191,25 +193,15 @@ def get_pygfx_render_area(self, *args) -> tuple[int, int, int, int]: width, height = self.canvas.get_logical_size() - for edge in ["left", "right"]: + for edge in ["right"]: if self.guis[edge]: width -= self._guis[edge].size - for edge in ["top", "bottom"]: + for edge in ["bottom"]: if self.guis[edge]: height -= self._guis[edge].size - if self.guis["left"]: - xpos = self.guis["left"].size - else: - xpos = 0 - - if self.guis["top"]: - ypos = self.guis["top"].size - else: - ypos = 0 - - return xpos, ypos, max(1, width), max(1, height) + return 0, 0, max(1, width), max(1, height) def register_popup(self, popup: Popup.__class__): """ diff --git a/fastplotlib/layouts/_plot_area.py b/fastplotlib/layouts/_plot_area.py index e780607ce..2542fc215 100644 --- a/fastplotlib/layouts/_plot_area.py +++ b/fastplotlib/layouts/_plot_area.py @@ -273,7 +273,7 @@ def background_color(self, colors: str | tuple[float]): self._background_material.set_colors(*colors) def map_screen_to_world( - self, pos: tuple[float, float] | pygfx.PointerEvent + self, pos: tuple[float, float] | pygfx.PointerEvent, allow_outside: bool = False ) -> np.ndarray | None: """ Map screen position to world position @@ -287,7 +287,7 @@ def map_screen_to_world( if isinstance(pos, pygfx.PointerEvent): pos = pos.x, pos.y - if not self.viewport.is_inside(*pos): + if not allow_outside and not self.viewport.is_inside(*pos): return None vs = self.viewport.logical_size @@ -491,6 +491,10 @@ def _add_or_insert_graphic( obj_list = self._graphics self._fpl_graphics_scene.add(graphic.world_object) + # add to tooltip registry + if self.get_figure().show_tooltips: + self.get_figure().tooltip_manager.register(graphic) + else: raise TypeError("graphic must be of type Graphic | BaseSelector | Legend") @@ -504,7 +508,6 @@ def _add_or_insert_graphic( if center: self.center_graphic(graphic) - # if we don't use the weakref above, then the object lingers if a plot hook is used! graphic._fpl_add_plot_area_hook(self) def _check_graphic_name_exists(self, name): diff --git a/fastplotlib/legends/legend.py b/fastplotlib/legends/legend.py index df78d5662..69a556109 100644 --- a/fastplotlib/legends/legend.py +++ b/fastplotlib/legends/legend.py @@ -5,8 +5,8 @@ import numpy as np import pygfx -from ..graphics._base import Graphic -from ..graphics._features._base import FeatureEvent +from ..graphics import Graphic +from ..graphics.features import GraphicFeatureEvent from ..graphics import LineGraphic, ScatterGraphic, ImageGraphic from ..utils import mesh_masks @@ -116,7 +116,7 @@ def label(self, text: str): self._parent._check_label_unique(text) self._label_world_object.geometry.set_text(text) - def _update_color(self, ev: FeatureEvent): + def _update_color(self, ev: GraphicFeatureEvent): new_color = ev.info["value"] if np.unique(new_color, axis=0).shape[0] > 1: raise ValueError( diff --git a/fastplotlib/tools/__init__.py b/fastplotlib/tools/__init__.py index 80396c98d..df129a369 100644 --- a/fastplotlib/tools/__init__.py +++ b/fastplotlib/tools/__init__.py @@ -1 +1,7 @@ from ._histogram_lut import HistogramLUTTool +from ._tooltip import Tooltip + +__all__ = [ + "HistogramLUTTool", + "Tooltip", +] diff --git a/fastplotlib/tools/_histogram_lut.py b/fastplotlib/tools/_histogram_lut.py index b8c6633a8..aeb8dd996 100644 --- a/fastplotlib/tools/_histogram_lut.py +++ b/fastplotlib/tools/_histogram_lut.py @@ -5,6 +5,7 @@ import pygfx +from ..utils import subsample_array from ..graphics import LineGraphic, ImageGraphic, TextGraphic from ..graphics.utils import pause_events from ..graphics._base import Graphic @@ -193,28 +194,10 @@ def _fpl_add_plot_area_hook(self, plot_area): self._plot_area.controller.enabled = True def _calculate_histogram(self, data): - if data.ndim > 2: - # subsample to max of 500 x 100 x 100, - # np.histogram takes ~30ms with this size on a 8 core Ryzen laptop - # dim0 is usually time, allow max of 500 timepoints - ss0 = max(1, int(data.shape[0] / 500)) # max to prevent step = 0 - # allow max of 100 for x and y if ndim > 2 - ss1 = max(1, int(data.shape[1] / 100)) - ss2 = max(1, int(data.shape[2] / 100)) - data_ss = data[::ss0, ::ss1, ::ss2] - - hist, edges = np.histogram(data_ss, bins=self._nbins) - - else: - # allow max of 1000 x 1000 - # this takes ~4ms on a 8 core Ryzen laptop - ss0 = max(1, int(data.shape[0] / 1_000)) - ss1 = max(1, int(data.shape[1] / 1_000)) - - data_ss = data[::ss0, ::ss1] - - hist, edges = np.histogram(data_ss, bins=self._nbins) + # get a subsampled view of this array + data_ss = subsample_array(data, max_size=int(1e6)) # 1e6 is default + hist, edges = np.histogram(data_ss, bins=self._nbins) # used if data ptp <= 10 because event things get weird # with tiny world objects due to floating point error diff --git a/fastplotlib/tools/_tooltip.py b/fastplotlib/tools/_tooltip.py new file mode 100644 index 000000000..2fbdfcec2 --- /dev/null +++ b/fastplotlib/tools/_tooltip.py @@ -0,0 +1,297 @@ +from functools import partial + +import numpy as np +import pygfx + +from ..graphics import LineGraphic, ImageGraphic, ScatterGraphic, Graphic +from ..graphics.features import GraphicFeatureEvent + + +class MeshMasks: + """Used set the x0, x1, y0, y1 positions of the plane mesh""" + + x0 = np.array( + [ + [False, False, False], + [True, False, False], + [False, False, False], + [True, False, False], + ] + ) + + x1 = np.array( + [ + [True, False, False], + [False, False, False], + [True, False, False], + [False, False, False], + ] + ) + + y0 = np.array( + [ + [False, True, False], + [False, True, False], + [False, False, False], + [False, False, False], + ] + ) + + y1 = np.array( + [ + [False, False, False], + [False, False, False], + [False, True, False], + [False, True, False], + ] + ) + + +masks = MeshMasks + + +class Tooltip: + def __init__(self): + # text object + self._text = pygfx.Text( + text="", + font_size=12, + screen_space=False, + anchor="bottom-left", + material=pygfx.TextMaterial( + color="w", + outline_color="w", + outline_thickness=0.0, + pick_write=False, + ), + ) + + # plane for the background of the text object + geometry = pygfx.plane_geometry(1, 1) + material = pygfx.MeshBasicMaterial(color=(0.1, 0.1, 0.3, 0.95)) + self._plane = pygfx.Mesh(geometry, material) + # else text not visible + self._plane.world.z = 0.5 + + # line to outline the plane mesh + self._line = pygfx.Line( + geometry=pygfx.Geometry( + positions=np.array( + [ + [0, 0, 0], + [0, 0, 0], + [0, 0, 0], + [0, 0, 0], + [0, 0, 0], + ], + dtype=np.float32, + ) + ), + material=pygfx.LineThinMaterial(thickness=1.0, color=(0.8, 0.8, 1.0, 1.0)), + ) + + self._world_object = pygfx.Group() + self._world_object.add(self._plane, self._text, self._line) + + # padded to bbox so the background box behind the text extends a bit further + # making the text easier to read + self._padding = np.array([[5, 5, 0], [-5, -5, 0]], dtype=np.float32) + + self._registered_graphics = dict() + + @property + def world_object(self) -> pygfx.Group: + return self._world_object + + @property + def font_size(self): + """Get or set font size""" + return self._text.font_size + + @font_size.setter + def font_size(self, size: float): + self._text.font_size = size + + @property + def text_color(self): + """Get or set text color using a str or RGB(A) array""" + return self._text.material.color + + @text_color.setter + def text_color(self, color: str | tuple | list | np.ndarray): + self._text.material.color = color + + @property + def background_color(self): + """Get or set background color using a str or RGB(A) array""" + return self._plane.material.color + + @background_color.setter + def background_color(self, color: str | tuple | list | np.ndarray): + self._plane.material.color = color + + @property + def outline_color(self): + """Get or set outline color using a str or RGB(A) array""" + return self._line.material.color + + @outline_color.setter + def outline_color(self, color: str | tuple | list | np.ndarray): + self._line.material.color = color + + @property + def padding(self) -> np.ndarray: + """ + Get or set the background padding in number of pixels. + The padding defines the number of pixels around the tooltip text that the background is extended by. + """ + + return self.padding[0, :2].copy() + + @padding.setter + def padding(self, padding_xy: tuple[float, float]): + self._padding[0, :2] = padding_xy + self._padding[1, :2] = -np.asarray(padding_xy) + + def _set_position(self, pos: tuple[float, float]): + """ + Set the position of the tooltip + + Parameters + ---------- + pos: [float, float] + position in screen space + + """ + # need to flip due to inverted y + x, y = pos[0], pos[1] + + # put the tooltip slightly to the top right of the cursor positoin + x += 8 + y -= 8 + + self._text.world.position = (x, -y, 0) + + bbox = self._text.get_world_bounding_box() - self._padding + [[x0, y0, _], [x1, y1, _]] = bbox + + self._plane.geometry.positions.data[masks.x0] = x0 + self._plane.geometry.positions.data[masks.x1] = x1 + self._plane.geometry.positions.data[masks.y0] = y0 + self._plane.geometry.positions.data[masks.y1] = y1 + + self._plane.geometry.positions.update_range() + + # line points + pts = [[x0, y0], [x0, y1], [x1, y1], [x1, y0], [x0, y0]] + + self._line.geometry.positions.data[:, :2] = pts + self._line.geometry.positions.update_range() + + def _event_handler(self, custom_tooltip: callable, ev: pygfx.PointerEvent): + """Handles the tooltip appear event, determines the text to be set in the tooltip""" + if custom_tooltip is not None: + info = custom_tooltip(ev) + + elif isinstance(ev.graphic, ImageGraphic): + col, row = ev.pick_info["index"] + if ev.graphic.data.value.ndim == 2: + info = str(ev.graphic.data[row, col]) + else: + info = "\n".join( + f"{channel}: {val}" + for channel, val in zip("rgba", ev.graphic.data[row, col]) + ) + + elif isinstance(ev.graphic, (LineGraphic, ScatterGraphic)): + index = ev.pick_info["vertex_index"] + info = "\n".join( + f"{dim}: {val}" for dim, val in zip("xyz", ev.graphic.data[index]) + ) + else: + raise TypeError("Unsupported graphic") + + # make the tooltip object visible + self.world_object.visible = True + + # set the text and top left position of the tooltip + self._text.set_text(info) + self._set_position((ev.x, ev.y)) + + def _clear(self, ev): + self._text.set_text("") + self.world_object.visible = False + + def register( + self, + graphic: Graphic, + appear_event: str = "pointer_move", + disappear_event: str = "pointer_leave", + custom_info: callable = None, + ): + """ + Register a Graphic to display tooltips. + + **Note:** if the passed graphic is already registered then it first unregistered + and then re-registered using the given arguments. + + Parameters + ---------- + graphic: Graphic + Graphic to register + + appear_event: str, default "pointer_move" + the pointer that triggers the tooltip to appear. Usually one of "pointer_move" | "click" | "double_click" + + disappear_event: str, default "pointer_leave" + the event that triggers the tooltip to disappear, does not have to be a pointer event. + + custom_info: callable, default None + a custom function that takes the pointer event defined as the `appear_event` and returns the text + to display in the tooltip + + """ + if graphic in list(self._registered_graphics.keys()): + # unregister first and then re-register + self.unregister(graphic) + + pfunc = partial(self._event_handler, custom_info) + graphic.add_event_handler(pfunc, appear_event) + graphic.add_event_handler(self._clear, disappear_event) + + self._registered_graphics[graphic] = (pfunc, appear_event, disappear_event) + + # automatically unregister when graphic is deleted + graphic.add_event_handler(self.unregister, "deleted") + + def unregister(self, graphic: Graphic): + """ + Unregister a Graphic to no longer display tooltips for this graphic. + + **Note:** if the passed graphic is not registered then it is just ignored without raising any exception. + + Parameters + ---------- + graphic: Graphic + Graphic to unregister + + """ + + if isinstance(graphic, GraphicFeatureEvent): + # this happens when the deleted event is triggered + graphic = graphic.graphic + + if graphic not in self._registered_graphics: + return + + # get pfunc and event names + pfunc, appear_event, disappear_event = self._registered_graphics.pop(graphic) + + # remove handlers from graphic + graphic.remove_event_handler(pfunc, appear_event) + graphic.remove_event_handler(self._clear, disappear_event) + + def unregister_all(self): + """unregister all graphics""" + for graphic in self._registered_graphics.keys(): + self.unregister(graphic) diff --git a/fastplotlib/ui/_base.py b/fastplotlib/ui/_base.py index 6c134d415..e31dd8d4a 100644 --- a/fastplotlib/ui/_base.py +++ b/fastplotlib/ui/_base.py @@ -6,7 +6,7 @@ from ..layouts._figure import Figure -GUI_EDGES = ["top", "right", "bottom", "left"] +GUI_EDGES = ["right", "bottom"] class BaseGUI: @@ -40,7 +40,7 @@ def __init__( self, figure: Figure, size: int, - location: Literal["top", "bottom", "left", "right"], + location: Literal["bottom", "right"], title: str, window_flags: int = imgui.WindowFlags_.no_collapse | imgui.WindowFlags_.no_resize, @@ -48,7 +48,7 @@ def __init__( **kwargs, ): """ - A base class for imgui windows displayed at one of the four edges of a Figure + A base class for imgui windows displayed at the bottom or top edge of a Figure Parameters ---------- @@ -58,7 +58,7 @@ def __init__( size: int width or height of the window, depending on its location - location: str, "top" | "bottom" | "left" | "right" + location: str, "bottom" | "right" location of the window title: str @@ -168,10 +168,6 @@ def get_rect(self) -> tuple[int, int, int, int]: width_canvas, height_canvas = self._figure.canvas.get_logical_size() match self._location: - case "top": - x_pos, y_pos = (0, 0) - width, height = (width_canvas, self.size) - case "bottom": x_pos = 0 y_pos = height_canvas - self.size @@ -179,22 +175,8 @@ def get_rect(self) -> tuple[int, int, int, int]: case "right": x_pos, y_pos = (width_canvas - self.size, 0) - - if self._figure.guis["top"]: - # if there is a GUI in the top edge, make this one below - y_pos += self._figure.guis["top"].size - width, height = (self.size, height_canvas) - if self._figure.guis["bottom"] is not None: - height -= self._figure.guis["bottom"].size - case "left": - x_pos, y_pos = (0, 0) - if self._figure.guis["top"]: - # if there is a GUI in the top edge, make this one below - y_pos += self._figure.guis["top"].size - - width, height = (self.size, height_canvas) if self._figure.guis["bottom"] is not None: height -= self._figure.guis["bottom"].size @@ -203,8 +185,11 @@ def get_rect(self) -> tuple[int, int, int, int]: def draw_window(self): """helps simplify using imgui by managing window creation & position, and pushing/popping the ID""" # window position & size + x, y, w, h = self.get_rect() imgui.set_next_window_size((self.width, self.height)) imgui.set_next_window_pos((self.x, self.y)) + # imgui.set_next_window_pos((x, y)) + # imgui.set_next_window_size((w, h)) flags = self._window_flags # begin window diff --git a/fastplotlib/ui/right_click_menus/_standard_menu.py b/fastplotlib/ui/right_click_menus/_standard_menu.py index 1937df858..4bb59c51d 100644 --- a/fastplotlib/ui/right_click_menus/_standard_menu.py +++ b/fastplotlib/ui/right_click_menus/_standard_menu.py @@ -31,7 +31,7 @@ def __init__(self, figure, fa_icons): # whether the right click menu is currently open or not self.is_open: bool = False - def get_subplot(self) -> PlotArea | bool: + def get_subplot(self) -> PlotArea | bool | None: """get the subplot that a click occurred in""" if self._last_right_click_pos is None: return False @@ -40,6 +40,9 @@ def get_subplot(self) -> PlotArea | bool: if subplot.viewport.is_inside(*self._last_right_click_pos): return subplot + # not inside a subplot + return False + def cleanup(self): """called when the popup disappears""" self.is_open = False @@ -80,6 +83,11 @@ def update(self): imgui.text(f"subplot: {name}") imgui.separator() + _, show_fps = imgui.menu_item( + "Show fps", "", self.get_subplot().get_figure().imgui_show_fps + ) + self.get_subplot().get_figure().imgui_show_fps = show_fps + # autoscale, center, maintain aspect if imgui.menu_item(f"Autoscale", "", False)[0]: self.get_subplot().auto_scale() @@ -174,4 +182,19 @@ def update(self): imgui.end_menu() + # renderer blend modes + if imgui.begin_menu("Blend mode"): + for blend_mode in sorted( + self.get_subplot().renderer._blenders_available.keys() + ): + clicked, _ = imgui.menu_item( + label=blend_mode, + shortcut="", + p_selected=self.get_subplot().renderer.blend_mode == blend_mode, + ) + + if clicked: + self.get_subplot().renderer.blend_mode = blend_mode + imgui.end_menu() + imgui.end_popup() diff --git a/fastplotlib/utils/_plot_helpers.py b/fastplotlib/utils/_plot_helpers.py index 5a39b76d0..12afe1cb2 100644 --- a/fastplotlib/utils/_plot_helpers.py +++ b/fastplotlib/utils/_plot_helpers.py @@ -36,10 +36,12 @@ def get_nearest_graphics_indices( if not all(isinstance(g, Graphic) for g in graphics): raise TypeError("all elements of `graphics` must be Graphic objects") - pos = np.asarray(pos) + pos = np.asarray(pos).ravel() - if pos.shape != (2,) or not pos.shape != (3,): - raise TypeError + if pos.shape != (2,) and pos.shape != (3,): + raise TypeError( + f"pos.shape must be (2,) or (3,), the shape of pos you have passed is: {pos.shape}" + ) # get centers centers = np.empty(shape=(len(graphics), len(pos))) diff --git a/fastplotlib/utils/functions.py b/fastplotlib/utils/functions.py index 6ad365e40..a1d6d476a 100644 --- a/fastplotlib/utils/functions.py +++ b/fastplotlib/utils/functions.py @@ -267,15 +267,17 @@ def make_colors_dict(labels: Sequence, cmap: str, **kwargs) -> OrderedDict: return OrderedDict(zip(labels, colors)) -def quick_min_max(data: np.ndarray) -> tuple[float, float]: +def quick_min_max(data: np.ndarray, max_size=1e6) -> tuple[float, float]: """ - Adapted from pyqtgraph.ImageView. - Estimate the min/max values of *data* by subsampling. + Estimate the min/max values of *data* by subsampling relative to the size of each dimension in the array. Parameters ---------- data: np.ndarray or array-like with `min` and `max` attributes + max_size : int, optional + largest array size allowed in the subsampled array. Default is 1e6. + Returns ------- (float, float) @@ -289,11 +291,7 @@ def quick_min_max(data: np.ndarray) -> tuple[float, float]: ): return data.min, data.max - while np.prod(data.shape) > 1e6: - ax = np.argmax(data.shape) - sl = [slice(None)] * data.ndim - sl[ax] = slice(None, None, 2) - data = data[tuple(sl)] + data = subsample_array(data, max_size=max_size) return float(np.nanmin(data)), float(np.nanmax(data)) @@ -405,3 +403,77 @@ def parse_cmap_values( colors = np.vstack([colormap[val] for val in norm_cmap_values]) return colors + + +def subsample_array( + arr: np.ndarray, max_size: int = 1e6, ignore_dims: Sequence[int] | None = None +): + """ + Subsamples an input array while preserving its relative dimensional proportions. + + The dimensions (shape) of the array can be represented as: + + .. math:: + + [d_1, d_2, \\dots d_n] + + The product of the dimensions can be represented as: + + .. math:: + + \\prod_{i=1}^{n} d_i + + To find the factor ``f`` by which to divide the size of each dimension in order to + get max_size ``s`` we must solve for ``f`` in the following expression: + + .. math:: + + \\prod_{i=1}^{n} \\frac{d_i}{\\mathbf{f}} = \\mathbf{s} + + The solution for ``f`` is is simply the nth root of the product of the dims divided by the max_size + where n is the number of dimensions + + .. math:: + + \\mathbf{f} = \\sqrt[n]{\\frac{\\prod_{i=1}^{n} d_i}{\\mathbf{s}}} + + Parameters + ---------- + arr: np.ndarray + input array of any dimensionality to be subsampled. + + max_size: int, default 1e6 + maximum number of elements in subsampled array + + ignore_dims: Sequence[int], optional + List of dimension indices to exclude from subsampling (i.e. retain full resolution). + For example, `ignore_dims=[0]` will avoid subsampling along the first axis. + + Returns + ------- + np.ndarray + subsample of the input array + """ + full_shape = np.array(arr.shape, dtype=np.uint64) + if np.prod(full_shape) <= max_size: + return arr[:] # no need to subsample if already below the threshold + + # get factor by which to divide all dims + f = np.power((np.prod(full_shape) / max_size), 1.0 / arr.ndim) + + # new shape for subsampled array + ns = np.floor(np.array(full_shape) / f).clip(min=1) + + # get the step size for the slices + slices = list( + slice(None, None, int(s)) for s in np.floor(full_shape / ns).astype(int) + ) + + # ignore dims e.g. RGB, which we don't want to downsample + if ignore_dims is not None: + for dim in ignore_dims: + slices[dim] = slice(None) + + slices = tuple(slices) + + return np.asarray(arr[slices]) diff --git a/fastplotlib/widgets/image_widget/_widget.py b/fastplotlib/widgets/image_widget/_widget.py index 0fbc02be3..b3fe1d05d 100644 --- a/fastplotlib/widgets/image_widget/_widget.py +++ b/fastplotlib/widgets/image_widget/_widget.py @@ -195,33 +195,46 @@ def current_index(self, index: dict[str, int]): if not self._initialized: return - if not set(index.keys()).issubset(set(self._current_index.keys())): - raise KeyError( - f"All dimension keys for setting `current_index` must be present in the widget sliders. " - f"The dimensions currently used for sliders are: {list(self.current_index.keys())}" - ) + if self._reentrant_block: + return - for k, val in index.items(): - if not isinstance(val, int): - raise TypeError("Indices for all dimensions must be int") - if val < 0: - raise IndexError("negative indexing is not supported for ImageWidget") - if val > self._dims_max_bounds[k]: - raise IndexError( - f"index {val} is out of bounds for dimension '{k}' " - f"which has a max bound of: {self._dims_max_bounds[k]}" + try: + self._reentrant_block = True # block re-execution until current_index has *fully* completed execution + if not set(index.keys()).issubset(set(self._current_index.keys())): + raise KeyError( + f"All dimension keys for setting `current_index` must be present in the widget sliders. " + f"The dimensions currently used for sliders are: {list(self.current_index.keys())}" ) - self._current_index.update(index) + for k, val in index.items(): + if not isinstance(val, int): + raise TypeError("Indices for all dimensions must be int") + if val < 0: + raise IndexError( + "negative indexing is not supported for ImageWidget" + ) + if val > self._dims_max_bounds[k]: + raise IndexError( + f"index {val} is out of bounds for dimension '{k}' " + f"which has a max bound of: {self._dims_max_bounds[k]}" + ) - for i, (ig, data) in enumerate(zip(self.managed_graphics, self.data)): - frame = self._process_indices(data, self._current_index) - frame = self._process_frame_apply(frame, i) - ig.data = frame + self._current_index.update(index) - # call any event handlers - for handler in self._current_index_changed_handlers: - handler(self.current_index) + for i, (ig, data) in enumerate(zip(self.managed_graphics, self.data)): + frame = self._process_indices(data, self._current_index) + frame = self._process_frame_apply(frame, i) + ig.data = frame + + # call any event handlers + for handler in self._current_index_changed_handlers: + handler(self.current_index) + except Exception as exc: + # raise original exception + raise exc # current_index setter has raised. The lines above below are probably more relevant! + finally: + # set_value has finished executing, now allow future executions + self._reentrant_block = False @property def n_img_dims(self) -> list[int]: @@ -329,7 +342,7 @@ def __init__( manually provide the shape for the Figure, otherwise the number of rows and columns is estimated figure_kwargs: dict, optional - passed to `GridPlot` + passed to ``Figure`` names: Optional[str] gives names to the subplots @@ -574,10 +587,12 @@ def __init__( self.figure.add_gui(self._image_widget_sliders) - self._initialized = True - self._current_index_changed_handlers = set() + self._reentrant_block = False + + self._initialized = True + @property def frame_apply(self) -> dict | None: return self._frame_apply diff --git a/pyproject.toml b/pyproject.toml index 4d957aee3..216b4ab46 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,3 +1,84 @@ +# ===== Project info + +[project] +dynamic = ["version"] +name = "fastplotlib" +description = "Next-gen fast plotting library running on WGPU using the Pygfx rendering engine " +readme = "README.md" +license = { file = "LICENSE" } +authors = [{ name = "Kushal Kolar" }, { name = "Caitlin Lewis" }] +keywords = [ + "visualization", + "science", + "interactive", + "pygfx", + "webgpu", + "wgpu", + "vulkan", + "gpu", +] +requires-python = ">= 3.10" +dependencies = [ + "numpy>=1.23.0", + "pygfx==0.10.0", + "wgpu>=0.20.0", + "cmap>=0.1.3", + # (this comment keeps this list multiline in VSCode) +] + +[project.optional-dependencies] +docs = [ + "sphinx", + "sphinx-gallery", + "pydata-sphinx-theme", + "glfw", + "ipywidgets>=8.0.0,<9", + "sphinx-copybutton", + "sphinx-design", + "pandoc", + "imageio[ffmpeg]", + "matplotlib", + "scikit-learn", +] +notebook = [ + "jupyterlab", + "jupyter-rfb>=0.5.1", + "ipywidgets>=8.0.0,<9", + "sidecar", +] +tests = [ + "pytest", + "nbmake", + "black", + "scipy", + "imageio[ffmpeg]", + "scikit-learn", + "tqdm", +] +imgui = ["imgui-bundle"] +dev = ["fastplotlib[docs,notebook,tests,imgui]"] + +[project.urls] +Homepage = "https://www.fastplotlib.org/" +Documentation = "https://www.fastplotlib.org/" +Repository = "https://github.com/fastplotlib/fastplotlib" + +# ===== Building + [build-system] -requires = ["setuptools", "wheel"] +requires = ["flit_core >=3.2,<4"] +build-backend = "flit_core.buildapi" + +# ===== Tooling + +# [tool.ruff] +# line-length = 88 +# [tool.ruff.lint] +# select = ["F", "E", "W", "N", "B", "RUF", "TC"] +# ignore = [ +# "E501", # Line too long +# "E731", # Do not assign a `lambda` expression, use a `def` +# "B019", # Use of `functools.lru_cache` or `functools.cache` on methods can lead to memory leaks +# "RUF012", # Mutable class attributes should be annotated with `typing.ClassVar`" +# ] diff --git a/scripts/generate_add_graphic_methods.py b/scripts/generate_add_graphic_methods.py index 533ae77c6..85e0be669 100644 --- a/scripts/generate_add_graphic_methods.py +++ b/scripts/generate_add_graphic_methods.py @@ -49,23 +49,26 @@ def generate_add_graphics_methods(): f.write(" return graphic\n\n") for m in modules: - class_name = m - method_name = class_name.type + cls = m + if cls.__name__ == "Graphic": + # skip base class + continue + method_name = cls.type - class_args = inspect.getfullargspec(class_name)[0][1:] + class_args = inspect.getfullargspec(cls)[0][1:] class_args = [arg + ", " for arg in class_args] s = "" for a in class_args: s += a f.write( - f" def add_{method_name}{inspect.signature(class_name.__init__)} -> {class_name.__name__}:\n" + f" def add_{method_name}{inspect.signature(cls.__init__)} -> {cls.__name__}:\n" ) f.write(' """\n') - f.write(f" {class_name.__init__.__doc__}\n") + f.write(f" {cls.__init__.__doc__}\n") f.write(' """\n') f.write( - f" return self._create_graphic({class_name.__name__}, {s} **kwargs)\n\n" + f" return self._create_graphic({cls.__name__}, {s} **kwargs)\n\n" ) f.close() diff --git a/setup.py b/setup.py deleted file mode 100644 index 3ca95de0f..000000000 --- a/setup.py +++ /dev/null @@ -1,76 +0,0 @@ -from setuptools import setup, find_packages -from pathlib import Path - - -install_requires = [ - "numpy>=1.23.0", - "pygfx~=0.9.0", - "wgpu>=0.20.0", - "cmap>=0.1.3", -] - - -extras_require = { - "docs": [ - "sphinx", - "sphinx-gallery", - "pydata-sphinx-theme", - "glfw", - "ipywidgets>=8.0.0,<9", - "sphinx-copybutton", - "sphinx-design", - "pandoc", - "imageio[ffmpeg]", - "matplotlib", - "scikit-learn", - ], - "notebook": [ - "jupyterlab", - "jupyter-rfb>=0.5.1", - "ipywidgets>=8.0.0,<9", - "sidecar", - ], - "tests": [ - "pytest", - "nbmake", - "black", - "scipy", - "imageio[ffmpeg]", - "scikit-learn", - "tqdm", - ], - "imgui": ["imgui-bundle"], -} - - -with open(Path(__file__).parent.joinpath("README.md")) as f: - readme = f.read() - -with open(Path(__file__).parent.joinpath("fastplotlib", "VERSION"), "r") as f: - ver = f.read().split("\n")[0] - - -classifiers = [ - "Programming Language :: Python :: 3", - "Topic :: Scientific/Engineering :: Visualization", - "License :: OSI Approved :: Apache Software License", - "Intended Audience :: Science/Research", -] - - -setup( - name="fastplotlib", - version=ver, - long_description=readme, - long_description_content_type="text/markdown", - packages=find_packages(), - url="https://github.com/fastplotlib/fastplotlib", - license="Apache 2.0", - author="Kushal Kolar, Caitlin Lewis", - author_email="", - python_requires=">=3.10", - install_requires=install_requires, - extras_require=extras_require, - include_package_data=True, - description="A fast plotting library built using the pygfx render engine", -) diff --git a/tests/events.py b/tests/events.py index ea160dec3..e9b212adb 100644 --- a/tests/events.py +++ b/tests/events.py @@ -5,7 +5,7 @@ import pygfx import fastplotlib as fpl -from fastplotlib.graphics._features import FeatureEvent +from fastplotlib.graphics.features import GraphicFeatureEvent def make_positions_data() -> np.ndarray: @@ -22,7 +22,7 @@ def make_scatter_graphic() -> fpl.ScatterGraphic: return fpl.ScatterGraphic(make_positions_data()) -event_instance: FeatureEvent = None +event_instance: GraphicFeatureEvent = None def event_handler(event): @@ -30,7 +30,7 @@ def event_handler(event): event_instance = event -decorated_event_instance: FeatureEvent = None +decorated_event_instance: GraphicFeatureEvent = None @pytest.mark.parametrize("graphic", [make_line_graphic(), make_scatter_graphic()]) @@ -42,7 +42,7 @@ def test_positions_data_event(graphic: fpl.LineGraphic | fpl.ScatterGraphic): info = {"key": (slice(3, 8, None), 1), "value": value} - expected = FeatureEvent(type="data", info=info) + expected = GraphicFeatureEvent(type="data", info=info) def validate(graphic, handler, expected_feature_event, event_to_test): assert expected_feature_event.type == event_to_test.type diff --git a/tests/test_colors_buffer_manager.py b/tests/test_colors_buffer_manager.py index 8a6c5700f..7b1aef16a 100644 --- a/tests/test_colors_buffer_manager.py +++ b/tests/test_colors_buffer_manager.py @@ -5,7 +5,7 @@ import pygfx import fastplotlib as fpl -from fastplotlib.graphics._features import VertexColors, FeatureEvent +from fastplotlib.graphics.features import VertexColors, GraphicFeatureEvent from .utils import ( generate_slice_indices, generate_color_inputs, @@ -18,7 +18,7 @@ def make_colors_buffer() -> VertexColors: return colors -EVENT_RETURN_VALUE: FeatureEvent = None +EVENT_RETURN_VALUE: GraphicFeatureEvent = None def event_handler(ev): @@ -65,7 +65,7 @@ def test_int(test_graphic): if test_graphic: # test event - assert isinstance(EVENT_RETURN_VALUE, FeatureEvent) + assert isinstance(EVENT_RETURN_VALUE, GraphicFeatureEvent) assert EVENT_RETURN_VALUE.graphic == graphic assert EVENT_RETURN_VALUE.target is graphic.world_object assert EVENT_RETURN_VALUE.info["key"] == 3 @@ -120,7 +120,7 @@ def test_tuple(test_graphic, slice_method): if test_graphic: # test event - assert isinstance(EVENT_RETURN_VALUE, FeatureEvent) + assert isinstance(EVENT_RETURN_VALUE, GraphicFeatureEvent) assert EVENT_RETURN_VALUE.graphic == graphic assert EVENT_RETURN_VALUE.target is graphic.world_object assert EVENT_RETURN_VALUE.info["key"] == (s, slice(None)) @@ -142,7 +142,7 @@ def test_tuple(test_graphic, slice_method): if test_graphic: # test event - assert isinstance(EVENT_RETURN_VALUE, FeatureEvent) + assert isinstance(EVENT_RETURN_VALUE, GraphicFeatureEvent) assert EVENT_RETURN_VALUE.graphic == graphic assert EVENT_RETURN_VALUE.target is graphic.world_object assert EVENT_RETURN_VALUE.info["key"] == slice(None) @@ -218,7 +218,7 @@ def test_slice(color_input, slice_method: dict, test_graphic: bool): if test_graphic: global EVENT_RETURN_VALUE - assert isinstance(EVENT_RETURN_VALUE, FeatureEvent) + assert isinstance(EVENT_RETURN_VALUE, GraphicFeatureEvent) assert EVENT_RETURN_VALUE.graphic == graphic assert EVENT_RETURN_VALUE.target is graphic.world_object if isinstance(s, slice): diff --git a/tests/test_common_features.py b/tests/test_common_features.py index 332ac71ae..5671478a7 100644 --- a/tests/test_common_features.py +++ b/tests/test_common_features.py @@ -4,7 +4,7 @@ import pytest import fastplotlib as fpl -from fastplotlib.graphics._features import FeatureEvent, Name, Offset, Rotation, Visible +from fastplotlib.graphics.features import GraphicFeatureEvent, Name, Offset, Rotation, Visible def make_graphic(kind: str, **kwargs): @@ -29,11 +29,11 @@ def make_graphic(kind: str, **kwargs): ] -RETURN_EVENT_VALUE: FeatureEvent = None -DECORATED_EVENT_VALUE: FeatureEvent = None +RETURN_EVENT_VALUE: GraphicFeatureEvent = None +DECORATED_EVENT_VALUE: GraphicFeatureEvent = None -def return_event(ev: FeatureEvent): +def return_event(ev: GraphicFeatureEvent): global RETURN_EVENT_VALUE RETURN_EVENT_VALUE = ev @@ -138,7 +138,7 @@ def decorated_handler(ev): assert DECORATED_EVENT_VALUE.type == "offset" assert DECORATED_EVENT_VALUE.graphic is graphic assert DECORATED_EVENT_VALUE.target is graphic.world_object - assert DECORATED_EVENT_VALUE.info["value"] == (7.0, 8.0, 9.0) + npt.assert_almost_equal(DECORATED_EVENT_VALUE.info["value"], (7.0, 8.0, 9.0)) @pytest.mark.parametrize( @@ -202,7 +202,7 @@ def decorated_handler(ev): assert DECORATED_EVENT_VALUE.type == "rotation" assert DECORATED_EVENT_VALUE.graphic is graphic assert DECORATED_EVENT_VALUE.target is graphic.world_object - assert DECORATED_EVENT_VALUE.info["value"] == (0, 0, 0.6, 0.8) + npt.assert_almost_equal(DECORATED_EVENT_VALUE.info["value"], (0, 0, 0.6, 0.8)) @pytest.mark.parametrize( diff --git a/tests/test_figure.py b/tests/test_figure.py index 757b1eeae..520091009 100644 --- a/tests/test_figure.py +++ b/tests/test_figure.py @@ -170,3 +170,93 @@ def test_set_controllers_from_existing_controllers(): assert fig[0, 0].camera is cameras[0][0] assert fig[0, 1].camera.fov == 50 + + +def test_subplot_names(): + # names must be unique + with pytest.raises(ValueError): + fpl.Figure( + shape=(2, 3), + names=["1", "2", "3", "4", "4", "5"] + ) + + with pytest.raises(ValueError): + fpl.Figure( + shape=(2, 3), + names=["1", "2", None, "4", "4", "5"] + ) + + with pytest.raises(ValueError): + fpl.Figure( + shape=(2, 3), + names=[None, "2", None, "4", "4", "5"] + ) + + # len(names) <= n_subplots + fig = fpl.Figure( + shape=(2, 3), + names=["1", "2", "3", "4", "5", "6"] + ) + + assert fig[0, 0].name == "1" + assert fig[0, 1].name == "2" + assert fig[0, 2].name == "3" + assert fig[1, 0].name == "4" + assert fig[1, 1].name == "5" + assert fig[1, 2].name == "6" + + fig = fpl.Figure( + shape=(2, 3), + names=["1", "2", "3", None, "5", "6"] + ) + + assert fig[0, 0].name == "1" + assert fig[0, 1].name == "2" + assert fig[0, 2].name == "3" + assert fig[1, 0].name is None + assert fig[1, 1].name == "5" + assert fig[1, 2].name == "6" + + fig = fpl.Figure( + shape=(2, 3), + names=["1", "2", "3", None, "5", None] + ) + + assert fig[0, 0].name == "1" + assert fig[0, 1].name == "2" + assert fig[0, 2].name == "3" + assert fig[1, 0].name is None + assert fig[1, 1].name == "5" + assert fig[1, 2].name is None + + # if fewer subplot names are given than n_sublots, pad with Nones + fig = fpl.Figure( + shape=(2, 3), + names=["1", "2", "3", "4"] + ) + + assert fig[0, 0].name == "1" + assert fig[0, 1].name == "2" + assert fig[0, 2].name == "3" + assert fig[1, 0].name == "4" + assert fig[1, 1].name is None + assert fig[1, 2].name is None + + # raise if len(names) > n_subplots + with pytest.raises(ValueError): + fpl.Figure( + shape=(2, 3), + names=["1", "2", "3", "4", "5", "6", "7"] + ) + + with pytest.raises(ValueError): + fpl.Figure( + shape=(2, 3), + names=["1", "2", "3", "4", None, "6", "7"] + ) + + with pytest.raises(ValueError): + fpl.Figure( + shape=(2, 3), + names=["1", None, "3", "4", None, "6", "7"] + ) diff --git a/tests/test_image_graphic.py b/tests/test_image_graphic.py index 02b982d80..f2d87860b 100644 --- a/tests/test_image_graphic.py +++ b/tests/test_image_graphic.py @@ -5,7 +5,7 @@ import pygfx import fastplotlib as fpl -from fastplotlib.graphics._features import FeatureEvent +from fastplotlib.graphics.features import GraphicFeatureEvent from fastplotlib.utils import make_colors GRAY_IMAGE = iio.imread("imageio:camera.png") @@ -18,7 +18,7 @@ # new screenshot tests too for these when in graphics -EVENT_RETURN_VALUE: FeatureEvent = None +EVENT_RETURN_VALUE: GraphicFeatureEvent = None def event_handler(ev): @@ -28,7 +28,7 @@ def event_handler(ev): def check_event(graphic, feature, value): global EVENT_RETURN_VALUE - assert isinstance(EVENT_RETURN_VALUE, FeatureEvent) + assert isinstance(EVENT_RETURN_VALUE, GraphicFeatureEvent) assert EVENT_RETURN_VALUE.type == feature assert EVENT_RETURN_VALUE.graphic == graphic assert EVENT_RETURN_VALUE.target == graphic.world_object @@ -58,7 +58,7 @@ def check_set_slice( npt.assert_almost_equal(data_values[:, col_slice.stop :], data[:, col_slice.stop :]) global EVENT_RETURN_VALUE - assert isinstance(EVENT_RETURN_VALUE, FeatureEvent) + assert isinstance(EVENT_RETURN_VALUE, GraphicFeatureEvent) assert EVENT_RETURN_VALUE.type == "data" assert EVENT_RETURN_VALUE.graphic == image_graphic assert EVENT_RETURN_VALUE.target == image_graphic.world_object diff --git a/tests/test_positions_data_buffer_manager.py b/tests/test_positions_data_buffer_manager.py index 77d049ab5..18a7b36e8 100644 --- a/tests/test_positions_data_buffer_manager.py +++ b/tests/test_positions_data_buffer_manager.py @@ -3,14 +3,14 @@ import pytest import fastplotlib as fpl -from fastplotlib.graphics._features import VertexPositions, FeatureEvent +from fastplotlib.graphics.features import VertexPositions, GraphicFeatureEvent from .utils import ( generate_slice_indices, generate_positions_spiral_data, ) -EVENT_RETURN_VALUE: FeatureEvent = None +EVENT_RETURN_VALUE: GraphicFeatureEvent = None def event_handler(ev): @@ -72,7 +72,7 @@ def test_int(test_graphic): # check event if test_graphic: - assert isinstance(EVENT_RETURN_VALUE, FeatureEvent) + assert isinstance(EVENT_RETURN_VALUE, GraphicFeatureEvent) assert EVENT_RETURN_VALUE.graphic == graphic assert EVENT_RETURN_VALUE.target is graphic.world_object assert EVENT_RETURN_VALUE.info["key"] == 2 @@ -87,7 +87,7 @@ def test_int(test_graphic): # check event if test_graphic: - assert isinstance(EVENT_RETURN_VALUE, FeatureEvent) + assert isinstance(EVENT_RETURN_VALUE, GraphicFeatureEvent) assert EVENT_RETURN_VALUE.graphic == graphic assert EVENT_RETURN_VALUE.target is graphic.world_object assert EVENT_RETURN_VALUE.info["key"] == slice(None) @@ -148,7 +148,7 @@ def test_slice(test_graphic, slice_method: dict, test_axis: str): # check event if test_graphic: - assert isinstance(EVENT_RETURN_VALUE, FeatureEvent) + assert isinstance(EVENT_RETURN_VALUE, GraphicFeatureEvent) assert EVENT_RETURN_VALUE.graphic == graphic assert EVENT_RETURN_VALUE.target is graphic.world_object if isinstance(s, slice): @@ -172,7 +172,7 @@ def test_slice(test_graphic, slice_method: dict, test_axis: str): # check event if test_graphic: - assert isinstance(EVENT_RETURN_VALUE, FeatureEvent) + assert isinstance(EVENT_RETURN_VALUE, GraphicFeatureEvent) assert EVENT_RETURN_VALUE.graphic == graphic assert EVENT_RETURN_VALUE.target is graphic.world_object if isinstance(s, slice): @@ -191,7 +191,7 @@ def test_slice(test_graphic, slice_method: dict, test_axis: str): # check event if test_graphic: - assert isinstance(EVENT_RETURN_VALUE, FeatureEvent) + assert isinstance(EVENT_RETURN_VALUE, GraphicFeatureEvent) assert EVENT_RETURN_VALUE.graphic == graphic assert EVENT_RETURN_VALUE.target is graphic.world_object if isinstance(s, slice): diff --git a/tests/test_positions_graphics.py b/tests/test_positions_graphics.py index b76ece2ca..ed791b6fa 100644 --- a/tests/test_positions_graphics.py +++ b/tests/test_positions_graphics.py @@ -5,7 +5,7 @@ import pygfx import fastplotlib as fpl -from fastplotlib.graphics._features import ( +from fastplotlib.graphics.features import ( VertexPositions, VertexColors, VertexCmap, @@ -13,7 +13,7 @@ UniformSize, PointsSizesFeature, Thickness, - FeatureEvent, + GraphicFeatureEvent, ) from .utils import ( @@ -58,7 +58,7 @@ } -EVENT_RETURN_VALUE: FeatureEvent = None +EVENT_RETURN_VALUE: GraphicFeatureEvent = None def event_handler(ev): diff --git a/tests/test_sizes_buffer_manager.py b/tests/test_sizes_buffer_manager.py index 1d0a17f3d..2f55eab27 100644 --- a/tests/test_sizes_buffer_manager.py +++ b/tests/test_sizes_buffer_manager.py @@ -2,7 +2,7 @@ from numpy import testing as npt import pytest -from fastplotlib.graphics._features import PointsSizesFeature +from fastplotlib.graphics.features import PointsSizesFeature from .utils import generate_slice_indices diff --git a/tests/test_text_graphic.py b/tests/test_text_graphic.py index deb25ca6b..ec3d0be54 100644 --- a/tests/test_text_graphic.py +++ b/tests/test_text_graphic.py @@ -1,8 +1,8 @@ from numpy import testing as npt import fastplotlib as fpl -from fastplotlib.graphics._features import ( - FeatureEvent, +from fastplotlib.graphics.features import ( + GraphicFeatureEvent, TextData, FontSize, TextFaceColor, @@ -40,7 +40,7 @@ def test_create_graphic(): assert text.world_object.material.outline_thickness == 0 -EVENT_RETURN_VALUE: FeatureEvent = None +EVENT_RETURN_VALUE: GraphicFeatureEvent = None def event_handler(ev): @@ -50,7 +50,7 @@ def event_handler(ev): def check_event(graphic, feature, value): global EVENT_RETURN_VALUE - assert isinstance(EVENT_RETURN_VALUE, FeatureEvent) + assert isinstance(EVENT_RETURN_VALUE, GraphicFeatureEvent) assert EVENT_RETURN_VALUE.type == feature assert EVENT_RETURN_VALUE.graphic == graphic assert EVENT_RETURN_VALUE.target == graphic.world_object diff --git a/tests/test_texture_array.py b/tests/test_texture_array.py index c85fc7652..6220f2fe5 100644 --- a/tests/test_texture_array.py +++ b/tests/test_texture_array.py @@ -5,7 +5,7 @@ import pygfx import fastplotlib as fpl -from fastplotlib.graphics._features import TextureArray +from fastplotlib.graphics.features import TextureArray from fastplotlib.graphics.image import _ImageTile pFad - Phonifier reborn

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

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


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy