diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bc223ae27..23bfc8ae7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -16,7 +16,7 @@ on: jobs: docs-build: name: Docs - runs-on: bigmem + runs-on: ubuntu-latest timeout-minutes: 10 if: ${{ !github.event.pull_request.draft }} strategy: @@ -48,8 +48,8 @@ jobs: make html SPHINXOPTS="-W --keep-going" test-build-full: - name: Test Linux, notebook + glfw - runs-on: bigmem + name: Test Linux, notebook + offscreen + runs-on: ubuntu-latest timeout-minutes: 10 if: ${{ !github.event.pull_request.draft }} strategy: @@ -105,8 +105,8 @@ jobs: examples/notebooks/diffs test-build-desktop: - name: Test Linux, only glfw - runs-on: bigmem + name: Test Linux, only offscreen + runs-on: ubuntu-latest timeout-minutes: 10 if: ${{ !github.event.pull_request.draft }} strategy: diff --git a/.github/workflows/screenshots.yml b/.github/workflows/screenshots.yml index c1ed81644..0686b4445 100644 --- a/.github/workflows/screenshots.yml +++ b/.github/workflows/screenshots.yml @@ -13,7 +13,7 @@ on: jobs: screenshots: name: Regenerate - runs-on: bigmem + runs-on: ubuntu-latest timeout-minutes: 10 if: ${{ !github.event.pull_request.draft }} steps: diff --git a/docs/source/_static/click_event.gif b/docs/source/_static/click_event.gif deleted file mode 100644 index 81a334318..000000000 --- a/docs/source/_static/click_event.gif +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:b45c8aba01ba9557ee707e11952798df151d111a144cc09432323d98a3e2ee17 -size 110505 diff --git a/docs/source/_static/guide_animation.gif b/docs/source/_static/guide_animation.gif deleted file mode 100644 index 6328dbefc..000000000 --- a/docs/source/_static/guide_animation.gif +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:0b69ad02a527f4c7353cb94ce22deedd92134e84b2cc0776bf81f3d0083d0e37 -size 4095880 diff --git a/docs/source/_static/guide_animation.webp b/docs/source/_static/guide_animation.webp new file mode 100644 index 000000000..f204fa117 Binary files /dev/null and b/docs/source/_static/guide_animation.webp differ diff --git a/docs/source/_static/guide_click_event.webp b/docs/source/_static/guide_click_event.webp new file mode 100644 index 000000000..1f511396c Binary files /dev/null and b/docs/source/_static/guide_click_event.webp differ diff --git a/docs/source/_static/guide_hello_world.png b/docs/source/_static/guide_hello_world.png index dbdc7029a..ccffcbac5 100644 --- a/docs/source/_static/guide_hello_world.png +++ b/docs/source/_static/guide_hello_world.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b621c2034eb2998e602409ab41b5c574d24e265088787fd7331c8308eec6d7cb -size 178268 +oid sha256:97fda350fd73fc33792447114828884563862cae1f89530f242360d72f284ccc +size 106236 diff --git a/docs/source/_static/guide_hello_world_data.png b/docs/source/_static/guide_hello_world_data.png deleted file mode 100644 index 105a2292a..000000000 --- a/docs/source/_static/guide_hello_world_data.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:11652d0a0cfef535eb7e9fe1b796cbd2a6ef3f779dd5ce477273fcfda4cefb99 -size 207602 diff --git a/docs/source/_static/guide_hello_world_fancy_slicing.png b/docs/source/_static/guide_hello_world_fancy_slicing.png new file mode 100644 index 000000000..c5d0a1441 --- /dev/null +++ b/docs/source/_static/guide_hello_world_fancy_slicing.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:523160dd2a81b6788bef6a57392f194239252ad58cd64ec9e5408040bd7130e4 +size 138165 diff --git a/docs/source/_static/guide_hello_world_simple_slicing.png b/docs/source/_static/guide_hello_world_simple_slicing.png new file mode 100644 index 000000000..6d66bc7c7 --- /dev/null +++ b/docs/source/_static/guide_hello_world_simple_slicing.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fd3ee6a1de4ef244969014f0e8e2cb548f8c4ff8b865e4cc08f728412f9189bf +size 101339 diff --git a/docs/source/_static/guide_hello_world_vmax.png b/docs/source/_static/guide_hello_world_vmax.png index c90785b1b..a835c41ac 100644 --- a/docs/source/_static/guide_hello_world_vmax.png +++ b/docs/source/_static/guide_hello_world_vmax.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:14f07430628acef116b5e426fa274978127b2f8decb2655b8367edf5ca731501 -size 170500 +oid sha256:0085ffeddcf765a6902eea71659de40c9034648dee587d33068b7603ea08ad3a +size 93647 diff --git a/docs/source/_static/guide_image_widget.gif b/docs/source/_static/guide_image_widget.gif deleted file mode 100644 index 06f23c52d..000000000 --- a/docs/source/_static/guide_image_widget.gif +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:4a3c90078ab4ec8552c4eac199c726d413c89ec63143988e3ea0fca5114393d6 -size 2122357 diff --git a/docs/source/_static/guide_image_widget.webp b/docs/source/_static/guide_image_widget.webp new file mode 100644 index 000000000..2fc206041 Binary files /dev/null and b/docs/source/_static/guide_image_widget.webp differ diff --git a/docs/source/_static/guide_imgui.png b/docs/source/_static/guide_imgui.png new file mode 100644 index 000000000..6c17e36b3 --- /dev/null +++ b/docs/source/_static/guide_imgui.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:262dfd4e83abba504a3630c74ba873fbe6471fdb69b32f250cd372fa67c4a44c +size 63997 diff --git a/docs/source/_static/guide_linear_selector.gif b/docs/source/_static/guide_linear_selector.gif deleted file mode 100644 index 383c3ef43..000000000 --- a/docs/source/_static/guide_linear_selector.gif +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:acf1cab72eb4d268581b26295e47cd0ca9aebe08beaf80e1424b100086ed59ca -size 331446 diff --git a/docs/source/_static/guide_linear_selector.webp b/docs/source/_static/guide_linear_selector.webp new file mode 100644 index 000000000..c60ec6c03 Binary files /dev/null and b/docs/source/_static/guide_linear_selector.webp differ diff --git a/docs/source/api/graphics/ImageGraphic.rst b/docs/source/api/graphics/ImageGraphic.rst index d4fa2e7b9..dd5ff1ccc 100644 --- a/docs/source/api/graphics/ImageGraphic.rst +++ b/docs/source/api/graphics/ImageGraphic.rst @@ -30,6 +30,7 @@ Properties ImageGraphic.interpolation ImageGraphic.name ImageGraphic.offset + ImageGraphic.right_click_menu ImageGraphic.rotation ImageGraphic.supported_events ImageGraphic.visible diff --git a/docs/source/api/graphics/LineCollection.rst b/docs/source/api/graphics/LineCollection.rst index 459884fdd..ad4b7f929 100644 --- a/docs/source/api/graphics/LineCollection.rst +++ b/docs/source/api/graphics/LineCollection.rst @@ -33,6 +33,7 @@ Properties LineCollection.names LineCollection.offset LineCollection.offsets + LineCollection.right_click_menu LineCollection.rotation LineCollection.rotations LineCollection.supported_events diff --git a/docs/source/api/graphics/LineGraphic.rst b/docs/source/api/graphics/LineGraphic.rst index a3e1587f7..cb924b4dc 100644 --- a/docs/source/api/graphics/LineGraphic.rst +++ b/docs/source/api/graphics/LineGraphic.rst @@ -29,6 +29,7 @@ Properties LineGraphic.event_handlers LineGraphic.name LineGraphic.offset + LineGraphic.right_click_menu LineGraphic.rotation LineGraphic.supported_events LineGraphic.thickness diff --git a/docs/source/api/graphics/LineStack.rst b/docs/source/api/graphics/LineStack.rst index 3c14e708c..db060a4c2 100644 --- a/docs/source/api/graphics/LineStack.rst +++ b/docs/source/api/graphics/LineStack.rst @@ -33,6 +33,7 @@ Properties LineStack.names LineStack.offset LineStack.offsets + LineStack.right_click_menu LineStack.rotation LineStack.rotations LineStack.supported_events diff --git a/docs/source/api/graphics/ScatterGraphic.rst b/docs/source/api/graphics/ScatterGraphic.rst index 595346f07..8f15e827a 100644 --- a/docs/source/api/graphics/ScatterGraphic.rst +++ b/docs/source/api/graphics/ScatterGraphic.rst @@ -29,6 +29,7 @@ Properties ScatterGraphic.event_handlers ScatterGraphic.name ScatterGraphic.offset + ScatterGraphic.right_click_menu ScatterGraphic.rotation ScatterGraphic.sizes ScatterGraphic.supported_events diff --git a/docs/source/api/graphics/TextGraphic.rst b/docs/source/api/graphics/TextGraphic.rst index 107bc1c74..2a55d78ef 100644 --- a/docs/source/api/graphics/TextGraphic.rst +++ b/docs/source/api/graphics/TextGraphic.rst @@ -30,6 +30,7 @@ Properties TextGraphic.offset TextGraphic.outline_color TextGraphic.outline_thickness + TextGraphic.right_click_menu TextGraphic.rotation TextGraphic.supported_events TextGraphic.text diff --git a/docs/source/api/layouts/figure.rst b/docs/source/api/layouts/figure.rst index 817284e18..17ee965b6 100644 --- a/docs/source/api/layouts/figure.rst +++ b/docs/source/api/layouts/figure.rst @@ -6,7 +6,7 @@ Figure ====== Figure ====== -.. currentmodule:: fastplotlib +.. currentmodule:: fastplotlib.layouts Constructor ~~~~~~~~~~~ @@ -24,10 +24,8 @@ Properties Figure.canvas Figure.controllers Figure.names - Figure.output Figure.renderer Figure.shape - Figure.toolbar Methods ~~~~~~~ @@ -38,6 +36,8 @@ Methods Figure.clear Figure.close Figure.export + Figure.get_pygfx_render_area + Figure.open_popup Figure.remove_animation Figure.render Figure.show diff --git a/docs/source/api/layouts/imgui_figure.rst b/docs/source/api/layouts/imgui_figure.rst new file mode 100644 index 000000000..38a546ae9 --- /dev/null +++ b/docs/source/api/layouts/imgui_figure.rst @@ -0,0 +1,49 @@ +.. _api.ImguiFigure: + +ImguiFigure +*********** + +=========== +ImguiFigure +=========== +.. currentmodule:: fastplotlib.layouts + +Constructor +~~~~~~~~~~~ +.. autosummary:: + :toctree: ImguiFigure_api + + ImguiFigure + +Properties +~~~~~~~~~~ +.. autosummary:: + :toctree: ImguiFigure_api + + ImguiFigure.cameras + ImguiFigure.canvas + ImguiFigure.controllers + ImguiFigure.guis + ImguiFigure.imgui_renderer + ImguiFigure.names + ImguiFigure.renderer + ImguiFigure.shape + +Methods +~~~~~~~ +.. autosummary:: + :toctree: ImguiFigure_api + + ImguiFigure.add_animations + ImguiFigure.add_gui + ImguiFigure.clear + ImguiFigure.close + ImguiFigure.export + ImguiFigure.get_pygfx_render_area + ImguiFigure.open_popup + ImguiFigure.register_popup + ImguiFigure.remove_animation + ImguiFigure.render + ImguiFigure.show + ImguiFigure.start_render + diff --git a/docs/source/api/layouts/subplot.rst b/docs/source/api/layouts/subplot.rst index efe2fa4fc..3de44222d 100644 --- a/docs/source/api/layouts/subplot.rst +++ b/docs/source/api/layouts/subplot.rst @@ -35,6 +35,7 @@ Properties Subplot.renderer Subplot.scene Subplot.selectors + Subplot.toolbar Subplot.viewport Methods @@ -56,6 +57,7 @@ Methods Subplot.center_title Subplot.clear Subplot.delete_graphic + Subplot.get_figure Subplot.get_rect Subplot.insert_graphic Subplot.map_screen_to_world diff --git a/docs/source/api/selectors/LinearRegionSelector.rst b/docs/source/api/selectors/LinearRegionSelector.rst index 34df92b2a..bb406b7e2 100644 --- a/docs/source/api/selectors/LinearRegionSelector.rst +++ b/docs/source/api/selectors/LinearRegionSelector.rst @@ -29,6 +29,7 @@ Properties LinearRegionSelector.name LinearRegionSelector.offset LinearRegionSelector.parent + LinearRegionSelector.right_click_menu LinearRegionSelector.rotation LinearRegionSelector.selection LinearRegionSelector.supported_events diff --git a/docs/source/api/selectors/LinearSelector.rst b/docs/source/api/selectors/LinearSelector.rst index 31f546e2c..d434ef82f 100644 --- a/docs/source/api/selectors/LinearSelector.rst +++ b/docs/source/api/selectors/LinearSelector.rst @@ -29,6 +29,7 @@ Properties LinearSelector.name LinearSelector.offset LinearSelector.parent + LinearSelector.right_click_menu LinearSelector.rotation LinearSelector.selection LinearSelector.supported_events diff --git a/docs/source/api/selectors/RectangleSelector.rst b/docs/source/api/selectors/RectangleSelector.rst index b2dc40d2e..930e12c67 100644 --- a/docs/source/api/selectors/RectangleSelector.rst +++ b/docs/source/api/selectors/RectangleSelector.rst @@ -29,6 +29,7 @@ Properties RectangleSelector.name RectangleSelector.offset RectangleSelector.parent + RectangleSelector.right_click_menu RectangleSelector.rotation RectangleSelector.selection RectangleSelector.supported_events diff --git a/docs/source/api/ui/BaseGUI.rst b/docs/source/api/ui/BaseGUI.rst new file mode 100644 index 000000000..788e1414a --- /dev/null +++ b/docs/source/api/ui/BaseGUI.rst @@ -0,0 +1,30 @@ +.. _api.BaseGUI: + +BaseGUI +******* + +======= +BaseGUI +======= +.. currentmodule:: fastplotlib.ui + +Constructor +~~~~~~~~~~~ +.. autosummary:: + :toctree: BaseGUI_api + + BaseGUI + +Properties +~~~~~~~~~~ +.. autosummary:: + :toctree: BaseGUI_api + + +Methods +~~~~~~~ +.. autosummary:: + :toctree: BaseGUI_api + + BaseGUI.update + diff --git a/docs/source/api/ui/EdgeWindow.rst b/docs/source/api/ui/EdgeWindow.rst new file mode 100644 index 000000000..5835ab847 --- /dev/null +++ b/docs/source/api/ui/EdgeWindow.rst @@ -0,0 +1,38 @@ +.. _api.EdgeWindow: + +EdgeWindow +********** + +========== +EdgeWindow +========== +.. currentmodule:: fastplotlib.ui + +Constructor +~~~~~~~~~~~ +.. autosummary:: + :toctree: EdgeWindow_api + + EdgeWindow + +Properties +~~~~~~~~~~ +.. autosummary:: + :toctree: EdgeWindow_api + + EdgeWindow.height + EdgeWindow.location + EdgeWindow.size + EdgeWindow.width + EdgeWindow.x + EdgeWindow.y + +Methods +~~~~~~~ +.. autosummary:: + :toctree: EdgeWindow_api + + EdgeWindow.draw_window + EdgeWindow.get_rect + EdgeWindow.update + diff --git a/docs/source/api/ui/Popup.rst b/docs/source/api/ui/Popup.rst new file mode 100644 index 000000000..a154e9ce9 --- /dev/null +++ b/docs/source/api/ui/Popup.rst @@ -0,0 +1,33 @@ +.. _api.Popup: + +Popup +***** + +===== +Popup +===== +.. currentmodule:: fastplotlib.ui + +Constructor +~~~~~~~~~~~ +.. autosummary:: + :toctree: Popup_api + + Popup + +Properties +~~~~~~~~~~ +.. autosummary:: + :toctree: Popup_api + + +Methods +~~~~~~~ +.. autosummary:: + :toctree: Popup_api + + Popup.clear_event_filters + Popup.open + Popup.set_event_filter + Popup.update + diff --git a/docs/source/api/ui/Window.rst b/docs/source/api/ui/Window.rst new file mode 100644 index 000000000..63c384261 --- /dev/null +++ b/docs/source/api/ui/Window.rst @@ -0,0 +1,30 @@ +.. _api.Window: + +Window +****** + +====== +Window +====== +.. currentmodule:: fastplotlib.ui + +Constructor +~~~~~~~~~~~ +.. autosummary:: + :toctree: Window_api + + Window + +Properties +~~~~~~~~~~ +.. autosummary:: + :toctree: Window_api + + +Methods +~~~~~~~ +.. autosummary:: + :toctree: Window_api + + Window.update + diff --git a/docs/source/api/ui/index.rst b/docs/source/api/ui/index.rst new file mode 100644 index 000000000..4f31e651a --- /dev/null +++ b/docs/source/api/ui/index.rst @@ -0,0 +1,10 @@ +UI Bases +******** + +.. toctree:: + :maxdepth: 1 + + BaseGUI + Window + EdgeWindow + Popup diff --git a/docs/source/api/widgets/ImageWidget.rst b/docs/source/api/widgets/ImageWidget.rst index 3ca384968..fbafd4723 100644 --- a/docs/source/api/widgets/ImageWidget.rst +++ b/docs/source/api/widgets/ImageWidget.rst @@ -30,8 +30,6 @@ Properties ImageWidget.n_scrollable_dims ImageWidget.ndim ImageWidget.slider_dims - ImageWidget.sliders - ImageWidget.widget ImageWidget.window_funcs Methods @@ -39,7 +37,10 @@ Methods .. autosummary:: :toctree: ImageWidget_api + ImageWidget.add_event_handler + ImageWidget.clear_event_handlers ImageWidget.close + ImageWidget.remove_event_handler ImageWidget.reset_vmin_vmax ImageWidget.reset_vmin_vmax_frame ImageWidget.set_data diff --git a/docs/source/conf.py b/docs/source/conf.py index 1b296f533..913cfd50f 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -10,12 +10,16 @@ os.environ["WGPU_FORCE_OFFSCREEN"] = "1" import fastplotlib +import pygfx from pygfx.utils.gallery_scraper import find_examples_for_gallery from pathlib import Path import sys from sphinx_gallery.sorting import ExplicitOrder import imageio.v3 as iio +MAX_TEXTURE_SIZE = 2048 +pygfx.renderers.wgpu.set_wgpu_limits(**{"max-texture-dimension2d": MAX_TEXTURE_SIZE}) + ROOT_DIR = Path(__file__).parents[1].parents[0] # repo root EXAMPLES_DIR = Path.joinpath(ROOT_DIR, "examples", "desktop") @@ -52,6 +56,7 @@ "subsection_order": ExplicitOrder( [ "../../examples/desktop/image", + "../../examples/desktop/image_widget", "../../examples/desktop/gridplot", "../../examples/desktop/line", "../../examples/desktop/line_collection", @@ -59,6 +64,7 @@ "../../examples/desktop/heatmap", "../../examples/desktop/misc", "../../examples/desktop/selectors", + "../../examples/desktop/guis" ] ), "ignore_pattern": r'__init__\.py', diff --git a/docs/source/generate_api.py b/docs/source/generate_api.py index 0150836ec..dbe1d8005 100644 --- a/docs/source/generate_api.py +++ b/docs/source/generate_api.py @@ -9,6 +9,7 @@ from fastplotlib.graphics import _features, selectors from fastplotlib import widgets from fastplotlib import utils +from fastplotlib import ui current_dir = Path(__file__).parent.resolve() @@ -19,6 +20,7 @@ GRAPHIC_FEATURES_DIR = API_DIR.joinpath("graphic_features") SELECTORS_DIR = API_DIR.joinpath("selectors") WIDGETS_DIR = API_DIR.joinpath("widgets") +UI_DIR = API_DIR.joinpath("ui") doc_sources = [ API_DIR, @@ -27,6 +29,7 @@ GRAPHIC_FEATURES_DIR, SELECTORS_DIR, WIDGETS_DIR, + UI_DIR, ] for source_dir in doc_sources: @@ -143,11 +146,18 @@ def generate_page( def main(): generate_page( page_name="Figure", - classes=[fastplotlib.Figure], - modules=["fastplotlib"], + classes=[fastplotlib.layouts._figure.Figure], + modules=["fastplotlib.layouts"], source_path=LAYOUTS_DIR.joinpath("figure.rst"), ) + generate_page( + page_name="ImguiFigure", + classes=[fastplotlib.layouts.ImguiFigure], + modules=["fastplotlib.layouts"], + source_path=LAYOUTS_DIR.joinpath("imgui_figure.rst"), + ) + generate_page( page_name="Subplot", classes=[Subplot], @@ -258,17 +268,37 @@ def main(): ) ############################################################################## + ui_classes = [ui.BaseGUI, ui.Window, ui.EdgeWindow, ui.Popup] + + ui_class_names = [cls.__name__ for cls in ui_classes] + + ui_class_names_str = "\n ".join([""] + ui_class_names) + + with open(UI_DIR.joinpath("index.rst"), "w") as f: + f.write( + f"UI Bases\n" + f"********\n" + f"\n" + f".. toctree::\n" + f" :maxdepth: 1\n" + f"{ui_class_names_str}\n" + ) + + for ui_cls in ui_classes: + generate_page( + page_name=ui_cls.__name__, + classes=[ui_cls], + modules=["fastplotlib.ui"], + source_path=UI_DIR.joinpath(f"{ui_cls.__name__}.rst"), + ) + + ############################################################################## + utils_str = generate_functions_module(utils.functions, "fastplotlib.utils") with open(API_DIR.joinpath("utils.rst"), "w") as f: f.write(utils_str) - # gpu selection - fpl_functions = generate_functions_module(fastplotlib, "fastplotlib.utils.gpu") - - with open(API_DIR.joinpath("gpu.rst"), "w") as f: - f.write(fpl_functions) - if __name__ == "__main__": main() diff --git a/docs/source/index.rst b/docs/source/index.rst index f855569e3..4caa7fc7e 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -15,10 +15,12 @@ Welcome to fastplotlib's documentation! fastplotlib Figure + ImguiFigure Subplot Graphics Graphic Features Selectors + UI Widgets Utils @@ -31,8 +33,7 @@ Welcome to fastplotlib's documentation! Summary ======= -A fast plotting library built using the `pygfx `_ render engine utilizing `Vulkan `_, `DX12 `_, or `Metal `_ via `WGPU `_, so it is very fast! We also aim to be an expressive plotting library that enables rapid prototyping for large scale explorative scientific visualization. `fastplotlib` will run on any framework that ``pygfx`` runs on, this includes ``glfw``, ``Qt`` and ``jupyter lab`` - +Next-gen plotting library built using the `pygfx `_ render engine utilizing `Vulkan `_, `DX12 `_, or `Metal `_ via `WGPU `_, so it is very fast! ``fastplotlib`` is an expressive plotting library that enables rapid prototyping for large scale exploratory scientific visualization. ``fastplotlib`` will run on any framework that ``pygfx`` runs on, this includes ``glfw``, ``Qt`` and ``jupyter lab`` Installation ============ @@ -41,17 +42,6 @@ For installation please see the instructions on GitHub: https://github.com/kushalkolar/fastplotlib#installation -FAQ -=== - -1. Axes, axis, ticks, labels, legends - -A: They are on the `roadmap `_ and expected by summer 2024 :) - -2. Why the parrot logo? - -A: The logo is a `swift parrot `_, they are the fastest species of parrot and they are colorful like fastplotlib visualizations :D - Contributing ============ diff --git a/docs/source/user_guide/faq.rst b/docs/source/user_guide/faq.rst index aa4f3dc87..ebf17fcd6 100644 --- a/docs/source/user_guide/faq.rst +++ b/docs/source/user_guide/faq.rst @@ -122,4 +122,9 @@ How can I use `fastplotlib` interactively? 2. IPython - Users can select between using a Qt backend or glfw using the same methods as above. \ No newline at end of file + Users can select between using a Qt backend or glfw using the same methods as above. + +Why the parrot logo? +-------------------- + + The logo is a `swift parrot `_, they are the fastest species of parrot and they are colorful like fastplotlib visualizations :D \ No newline at end of file diff --git a/docs/source/user_guide/guide.rst b/docs/source/user_guide/guide.rst index df0c54c78..4c482e03a 100644 --- a/docs/source/user_guide/guide.rst +++ b/docs/source/user_guide/guide.rst @@ -175,12 +175,19 @@ Using our example from above: once we add a ``Graphic`` to the figure, we can th .. image:: ../_static/guide_hello_world_vmax.png -``Graphic`` properties also support slicing and indexing. For example :: +``Graphic`` properties also support numpy-like slicing for getting and setting data. For example :: - image_graphic.data[::8, :, :] = 1 - image_graphic.data[:, ::8, :] = 1 + # basic numpy-like slicing, set the top right corner + image_graphic.data[:150, -150:] = 0 -.. image:: ../_static/guide_hello_world_data.png +.. image:: ../_static/guide_hello_world_simple_slicing.png + +Fancy indexing is also supported! :: + + bool_array = np.random.choice([True, False], size=(512, 512), p=[0.1, 0.9]) + image_graphic.data[bool_array] = 254 + +.. image:: ../_static/guide_hello_world_fancy_slicing.png Selectors @@ -207,16 +214,15 @@ data. Let's look at an example: :: # add a linear selector the sine wave selector = sine_graphic.add_linear_selector() - fig[0, 0].auto_scale() - fig.show(maintain_aspect=False) -.. image:: ../_static/guide_linear_selector.gif +.. image:: ../_static/guide_linear_selector.webp A ``LinearRegionSelector`` is very similar to a ``LinearSelector`` but as opposed to selecting a singular point of 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. @@ -271,8 +277,10 @@ Rendering engine (``pygfx``) events: When an event occurs, the user-defined event handler will receive and 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 Attributes +^^^^^^^^^^^^^^^^ -**All ``Graphic`` events have the following attributes:** +All ``Graphic`` events have the following attributes: +------------+-------------+-----------------------------------------------+ | attribute | type | description | @@ -447,9 +455,9 @@ For example: :: # change the closest graphic color to white nearest.colors = "w" - fig.show() + fig.show() -.. image:: ../_static/click_event.gif +.. image:: ../_static/guide_click_event.webp ImageWidget ----------- @@ -473,7 +481,7 @@ to easily navigate through different dimensions of your data. Let's look at an e iw_movie.show() -.. image:: ../_static/guide_image_widget.gif +.. image:: ../_static/guide_image_widget.webp Animations ---------- @@ -483,24 +491,41 @@ An animation function is a user-defined function that gets called on every rende import fastplotlib as fpl import numpy as np - data = np.random.rand(512, 512) + # generate some data + start, stop = 0, 2 * np.pi + increment = (2 * np.pi) / 50 - fig = fpl.Figure() + # make a simple sine wave + xs = np.linspace(start, stop, 100) + ys = np.sin(xs) - fig[0,0].add_image(data=data, name="random-img") + figure = fpl.Figure(size=(700, 560)) - def update_data(plot_instance): - new_data = np.random.rand(512, 512) - plot_instance["random-img"].data = new_data + # plot the image data + sine = figure[0, 0].add_line(ys, name="sine", colors="r") - fig[0,0].add_animations(update_data) - fig.show() + # increment along the x-axis on each render loop :D + def update_line(subplot): + global increment, start, stop + xs = np.linspace(start + increment, stop + increment, 100) + ys = np.sin(xs) + + start += increment + stop += increment -.. image:: ../_static/guide_animation.gif + # change only the y-axis values of the line + subplot["sine"].data[:, 1] = ys -Here we are defining a function that updates the data of the ``ImageGraphic`` in the plot with new random data. When adding an animation function, the -user-defined function will receive a plot instance as an argument when it is called. + + figure[0, 0].add_animations(update_line) + + figure.show(maintain_aspect=False) + +.. image:: ../_static/guide_animation.webp + +Here we are defining a function that updates the data of the ``LineGraphic`` in the plot with new data. When adding an animation function, the +user-defined function will receive a subplot instance as an argument when it is called. Spaces ------ @@ -532,6 +557,20 @@ 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`` interactively ----------------------------------- diff --git a/examples/desktop/guis/README.rst b/examples/desktop/guis/README.rst new file mode 100644 index 000000000..9cbf4d424 --- /dev/null +++ b/examples/desktop/guis/README.rst @@ -0,0 +1,2 @@ +ImGUI for within-canvas GUIs +============================ diff --git a/examples/desktop/guis/image_widget_imgui.py b/examples/desktop/guis/image_widget_imgui.py new file mode 100644 index 000000000..38a5c72e1 --- /dev/null +++ b/examples/desktop/guis/image_widget_imgui.py @@ -0,0 +1,82 @@ +""" +ImGUI with ImageWidget +====================== + +Example showing how to write a custom GUI with imgui and use it with ImageWidget +""" + +# test_example = true +# sphinx_gallery_pygfx_docs = 'screenshot' + +# some simple image processing functions +from scipy.ndimage import gaussian_filter +import imageio.v3 as iio + +import fastplotlib as fpl + +# subclass from EdgeWindow to make a custom ImGUI Window to place inside the figure! +from fastplotlib.ui import EdgeWindow +from imgui_bundle import imgui + +a = iio.imread("imageio:camera.png") +iw = fpl.ImageWidget(data=a, cmap="viridis", figure_kwargs={"size": (700, 560)}) +iw.show() + + +# GUI for some basic image processing +class ImageProcessingWindow(EdgeWindow): + def __init__(self, figure, size, location, title): + super().__init__(figure=figure, size=size, location=location, title=title) + + self.sigma = 0.0 + self.order_x, self.order_y = 0, 0 + + def update(self): + # implement the GUI within the update function + # you do not need to call imgui.new_frame(), this is handled by Figure + + # window creation is handled by the base EdgeWindow.draw_window() + # if you want to customize the imgui window, you can override EdgeWindow.draw_window() + + something_changed = False + + # slider for gaussian filter sigma value + changed, value = imgui.slider_float(label="sigma", v=self.sigma, v_min=0.0, v_max=20.0) + if changed: + self.sigma = value + something_changed = True + + # int entries for gaussian filter order + for axis in ["x", "y"]: + changed, value = imgui.input_int(f"order {axis}", v=getattr(self, f"order_{axis}")) + if changed: + if value < 0: + value = 0 + setattr(self, f"order_{axis}", value) + something_changed = True + + if something_changed: + self.process_image() + + # imgui.end() is handled by EdgeWindow.draw_window() + + # do not call imgui.end_frame(), this is handled by Figure + + def process_image(self): + processed = gaussian_filter(a, sigma=self.sigma, order=(self.order_y, self.order_x)) + iw.set_data(processed) + + +gui = ImageProcessingWindow(iw.figure, size=200, location="right", title="Gaussian Filter") + + +iw.figure.add_gui(gui) + +figure = iw.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.run() diff --git a/examples/desktop/guis/imgui_basic.py b/examples/desktop/guis/imgui_basic.py new file mode 100644 index 000000000..456375950 --- /dev/null +++ b/examples/desktop/guis/imgui_basic.py @@ -0,0 +1,123 @@ +""" +ImGUI Basics +============ + +Basic examples demonstrating how to use imgui in fastplotlib. + +See the imgui docs for extensive examples on how to create all UI elements: https://pyimgui.readthedocs.io/en/latest/reference/imgui.core.html#imgui.core.begin_combo +""" + +# test_example = true +# sphinx_gallery_pygfx_docs = 'screenshot' + +import numpy as np +import fastplotlib as fpl + +# subclass from EdgeWindow to make a custom ImGUI Window to place inside the figure! +from fastplotlib.ui import EdgeWindow +from imgui_bundle import imgui + +# make some initial data +np.random.seed(0) + +xs = np.linspace(0, np.pi * 10, 100) +ys = np.sin(xs) + np.random.normal(scale=0.0, size=100) +data = np.column_stack([xs, ys]) + + +# make a figure +figure = fpl.Figure(size=(700, 560)) + +# make some scatter points at every 10th point +figure[0, 0].add_scatter(data[::10], colors="cyan", sizes=15, name="sine-scatter", uniform_color=True) + +# place a line above the scatter +figure[0, 0].add_line(data, thickness=3, colors="r", name="sine-wave", uniform_color=True) + + +class ImguiExample(EdgeWindow): + def __init__(self, figure, size, location, title): + super().__init__(figure=figure, size=size, location=location, title=title) + # this UI will modify the line + self._line = self._figure[0, 0]["sine-wave"] + + # set the default values + # wave amplitude + self._amplitude = 1 + + # sigma for gaussian noise + self._sigma = 0.0 + + def update(self): + # the UI will be used to modify the line + self._line = figure[0, 0]["sine-wave"] + + # get the current line RGB values + rgb_color = self._line.colors[:-1] + # make color picker + changed_color, rgb = imgui.color_picker3("color", col=rgb_color) + + # get current line color alpha value + alpha = self._line.colors[-1] + # make float slider + changed_alpha, new_alpha = imgui.slider_float("alpha", v=alpha, v_min=0.0, v_max=1.0) + + # if RGB or alpha changed + if changed_color | changed_alpha: + # set new color along with alpha + self._line.colors = [*rgb, new_alpha] + + # example of a slider, you can also use input_float + changed, amplitude = imgui.slider_float("amplitude", v=self._amplitude, v_max=10, v_min=0.1) + if changed: + # set y values + self._amplitude = amplitude + self._set_data() + + # slider for thickness + changed, thickness = imgui.slider_float("thickness", v=self._line.thickness, v_max=50.0, v_min=2.0) + if changed: + self._line.thickness = thickness + + # slider for gaussian noise + changed, sigma = imgui.slider_float("noise-sigma", v=self._sigma, v_max=1.0, v_min=0.0) + if changed: + self._sigma = sigma + self._set_data() + + # reset button + if imgui.button("reset"): + # reset line properties + self._line.colors = (1, 0, 0, 1) + self._line.thickness = 3 + + # reset the data params + self._amplitude = 1.0 + self._sigma = 0.0 + + # reset the data values for the line + self._set_data() + + def _set_data(self): + self._line.data[:, 1] = (np.sin(xs) * self._amplitude) + np.random.normal(scale=self._sigma, size=100) + + +# make GUI instance +gui = ImguiExample( + figure, # the figure this GUI instance should live inside + size=275, # width or height of the GUI window within the figure + location="right", # the edge to place this window at + title="Imgui Window", # window title +) + +# add it to the figure +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.run() diff --git a/examples/desktop/heatmap/heatmap.py b/examples/desktop/heatmap/heatmap.py index 008686464..11d5559c4 100644 --- a/examples/desktop/heatmap/heatmap.py +++ b/examples/desktop/heatmap/heatmap.py @@ -1,7 +1,8 @@ """ -Simple Heatmap -============== -Example showing how to plot a heatmap +Heatmap or large arrays +======================= +Example showing how ImageGraphics can be useful for viewing large arrays, these can be in the order of 10^4 x 10^4. +The performance and limitations will depend on your hardware. """ # test_example = true @@ -10,13 +11,14 @@ import fastplotlib as fpl import numpy as np + figure = fpl.Figure(size=(700, 560)) -xs = np.linspace(0, 1_000, 9_000, dtype=np.float32) +xs = np.linspace(0, 2300, 2300, dtype=np.float16) sine = np.sin(np.sqrt(xs)) -data = np.vstack([sine * i for i in range(15_000)]) +data = np.vstack([sine * i for i in range(2_300)]) # plot the image data img = figure[0, 0].add_image(data=data, name="heatmap") @@ -24,7 +26,6 @@ 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__": diff --git a/examples/desktop/heatmap/heatmap_cmap.py b/examples/desktop/heatmap/heatmap_cmap.py deleted file mode 100644 index 8791741a7..000000000 --- a/examples/desktop/heatmap/heatmap_cmap.py +++ /dev/null @@ -1,33 +0,0 @@ -""" -Heatmap change cmap -=================== -Change the cmap of a heatmap -""" - - -# test_example = false -# sphinx_gallery_pygfx_docs = 'hidden' - -import fastplotlib as fpl -import numpy as np - -figure = fpl.Figure(size=(700, 560)) - -xs = np.linspace(0, 1_000, 10_000, dtype=np.float32) - -sine = np.sin(np.sqrt(xs)) - -data = np.vstack([sine * i for i in range(20_000)]) - -# plot the image data -img = figure[0, 0].add_image(data=data, name="heatmap") - -figure.show() - -img.cmap = "viridis" - -# 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.run() diff --git a/examples/desktop/heatmap/heatmap_data.py b/examples/desktop/heatmap/heatmap_data.py deleted file mode 100644 index f524f5476..000000000 --- a/examples/desktop/heatmap/heatmap_data.py +++ /dev/null @@ -1,35 +0,0 @@ -""" -Heatmap change data -=================== -Change the data of a heatmap -""" - -# test_example = false -# sphinx_gallery_pygfx_docs = 'hidden' - -import fastplotlib as fpl -import numpy as np - -figure = fpl.Figure(size=(700, 560)) - -xs = np.linspace(0, 1_000, 9_000, dtype=np.float32) - -sine = np.sin(np.sqrt(xs)) - -data = np.vstack([sine * i for i in range(9_000)]) - -# plot the image data -img = figure[0, 0].add_image(data=data, name="heatmap") - -figure.show() - -cosine = np.cos(np.sqrt(xs)[:3000]) - -# change first 2,000 rows and 3,000 columns -img.data[:2_000, :3_000] = np.vstack([cosine * i * 4 for i in range(2_000)]) - -# 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.run() diff --git a/examples/desktop/heatmap/heatmap_square.py b/examples/desktop/heatmap/heatmap_square.py deleted file mode 100644 index aee4f7d44..000000000 --- a/examples/desktop/heatmap/heatmap_square.py +++ /dev/null @@ -1,31 +0,0 @@ -""" -Square Heatmap -============== -square heatmap test -""" - -# test_example = false -# sphinx_gallery_pygfx_docs = 'hidden' - -import fastplotlib as fpl -import numpy as np - - -figure = fpl.Figure(size=(700, 560)) - -xs = np.linspace(0, 1_000, 20_000, dtype=np.float32) - -sine = np.sin(np.sqrt(xs)) - -data = np.vstack([sine * i for i in range(20_000)]) - -# plot the image data -img = figure[0, 0].add_image(data=data, name="heatmap") - -del data # data no longer needed after given to graphic -figure.show() - - -if __name__ == "__main__": - print(__doc__) - fpl.run() diff --git a/examples/desktop/heatmap/heatmap_vmin_vmax.py b/examples/desktop/heatmap/heatmap_vmin_vmax.py deleted file mode 100644 index e7f9c758b..000000000 --- a/examples/desktop/heatmap/heatmap_vmin_vmax.py +++ /dev/null @@ -1,33 +0,0 @@ -""" -Heatmap change vmin vmax -======================== -Change the vmin vmax of a heatmap -""" - -# test_example = false -# sphinx_gallery_pygfx_docs = 'hidden' - -import fastplotlib as fpl -import numpy as np - -figure = fpl.Figure(size=(700, 560)) - -xs = np.linspace(0, 1_000, 10_000, dtype=np.float32) - -sine = np.sin(np.sqrt(xs)) - -data = np.vstack([sine * i for i in range(20_000)]) - -# plot the image data -img = figure[0, 0].add_image(data=data, name="heatmap") - -figure.show() - -img.vmin = -5_000 -img.vmax = 10_000 - -# 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.run() diff --git a/examples/desktop/heatmap/heatmap_wide.py b/examples/desktop/heatmap/heatmap_wide.py deleted file mode 100644 index 6bf3ff72d..000000000 --- a/examples/desktop/heatmap/heatmap_wide.py +++ /dev/null @@ -1,30 +0,0 @@ -""" -Wide Heatmap -============ -Wide example -""" - -# test_example = false -# sphinx_gallery_pygfx_docs = 'hidden' - -import fastplotlib as fpl -import numpy as np - - -figure = fpl.Figure(size=(700, 560)) - -xs = np.linspace(0, 1_000, 20_000, dtype=np.float32) - -sine = np.sin(np.sqrt(xs)) - -data = np.vstack([sine * i for i in range(10_000)]) - -# plot the image data -img = figure[0, 0].add_image(data=data, name="heatmap") - -figure.show() - - -if __name__ == "__main__": - print(__doc__) - fpl.run() diff --git a/examples/desktop/image_widget/README.rst b/examples/desktop/image_widget/README.rst new file mode 100644 index 000000000..f445f7390 --- /dev/null +++ b/examples/desktop/image_widget/README.rst @@ -0,0 +1,2 @@ +ImageWidget Examples +==================== diff --git a/examples/desktop/image/image_widget.py b/examples/desktop/image_widget/image_widget.py similarity index 57% rename from examples/desktop/image/image_widget.py rename to examples/desktop/image_widget/image_widget.py index 131e02bd7..78b54b8ef 100644 --- a/examples/desktop/image/image_widget.py +++ b/examples/desktop/image_widget/image_widget.py @@ -3,9 +3,12 @@ ============ Example showing the image widget in action. -When run in a notebook, or with the Qt GUI backend, sliders are also shown. + +Every image in an `ImageWidget` is associated with an interactive Histogram LUT tool and colorbar. Right-click the +colorbar to pick colormaps. """ +# test_example = true # sphinx_gallery_pygfx_docs = 'screenshot' import fastplotlib as fpl @@ -15,6 +18,13 @@ iw = fpl.ImageWidget(data=a, cmap="viridis", figure_kwargs={"size": (700, 560)}) iw.show() +# Access ImageGraphics managed by the image widget +iw.figure[0, 0]["image_widget_managed"].data[:50, :50] = 0 +iw.figure[0, 0]["image_widget_managed"].cmap = "gnuplot2" + +# another way to access the image widget managed ImageGraphics +iw.managed_graphics[0].data[450:, 450:] = 255 + figure = iw.figure # NOTE: `if __name__ == "__main__"` is NOT how to use fastplotlib interactively diff --git a/examples/desktop/image_widget/image_widget_grid.py b/examples/desktop/image_widget/image_widget_grid.py new file mode 100644 index 000000000..48b31caa7 --- /dev/null +++ b/examples/desktop/image_widget/image_widget_grid.py @@ -0,0 +1,41 @@ +""" +Image widget grid +================= + +Example showing how to view multiple images in an ImageWidget +""" + +import fastplotlib as fpl +import imageio.v3 as iio + +# test_example = true +# sphinx_gallery_pygfx_docs = 'screenshot' + +img1 = iio.imread("imageio:camera.png") +img2 = iio.imread("imageio:astronaut.png") +img3 = iio.imread("imageio:chelsea.png") +img4 = iio.imread("imageio:wikkie.png") + +iw = fpl.ImageWidget( + data=[img1, img2, img3, img4], + rgb=[False, True, True, True], # mix of grayscale and RGB images + names=["cameraman", "astronaut", "chelsea", "Almar's cat"], + # ImageWidget will sync controllers by default + # by setting `controller_ids=None` we can have independent controllers for each subplot + # this is useful when the images have different dimensions + figure_kwargs={"size": (700, 560), "controller_ids": None}, +) +iw.show() + +figure = iw.figure + +for subplot in figure: + # sometimes the toolbar adds clutter + subplot.toolbar = 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.run() diff --git a/examples/desktop/image_widget/image_widget_single_video.py b/examples/desktop/image_widget/image_widget_single_video.py new file mode 100644 index 000000000..30073a935 --- /dev/null +++ b/examples/desktop/image_widget/image_widget_single_video.py @@ -0,0 +1,47 @@ +""" +Image widget Video +================== + +Example showing how to scroll through one or more videos using the ImageWidget +""" + +# test_example = true +# sphinx_gallery_pygfx_docs = 'animate 6s 20fps' + +import fastplotlib as fpl +import imageio.v3 as iio +import numpy as np + + +movie = iio.imread("imageio:cockatoo.mp4") + +# Ignore and do not use the next 2 lines +# for the purposes of docs gallery generation we subsample and only use 15 frames +movie_sub = movie[:15, ::12, ::12].copy() +del movie + +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 +# to set a window function, give a dict in the form of {dim: (func, window_size)} +iw.window_funcs = {"t": (np.mean, 13)} + +# change the window size +iw.window_funcs["t"].window_size = 33 + +# change the function +iw.window_funcs["t"].func = np.max + +# or reset it +iw.window_funcs = None + +iw.show() + +figure = iw.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.run() diff --git a/examples/desktop/image_widget/image_widget_videos.py b/examples/desktop/image_widget/image_widget_videos.py new file mode 100644 index 000000000..6e5c35c50 --- /dev/null +++ b/examples/desktop/image_widget/image_widget_videos.py @@ -0,0 +1,43 @@ +""" +Image widget videos side by side +================================ + +Example showing how to scroll through one or more videos using the ImageWidget +""" + +# test_example = true +# sphinx_gallery_pygfx_docs = 'animate 6s 20fps' + +import fastplotlib as fpl +import imageio.v3 as iio +import numpy as np + + +# load the standard cockatoo video +cockatoo = iio.imread("imageio:cockatoo.mp4") + +# Ignore and do not use the next 2 lines +# for the purposes of docs gallery generation we subsample and only use 15 frames +cockatoo_sub = cockatoo[:15, ::12, ::12].copy() +del cockatoo + +# make a random grayscale video, shape is [t, rows, cols] +np.random.seed(0) +random_data = np.random.rand(*cockatoo_sub.shape[:-1]) + +iw = fpl.ImageWidget( + [random_data, cockatoo_sub], + rgb=[False, True], + figure_shape=(2, 1), # 2 rows, 1 column + figure_kwargs={"size": (700, 560)} +) + +iw.show() + +figure = iw.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.run() diff --git a/examples/desktop/misc/lorenz_animation.py b/examples/desktop/misc/lorenz_animation.py index cf7a77b38..af577d5a2 100644 --- a/examples/desktop/misc/lorenz_animation.py +++ b/examples/desktop/misc/lorenz_animation.py @@ -51,7 +51,8 @@ def lorenz(xyz, *, s=10, r=28, b=2.667): figure = fpl.Figure( cameras="3d", - controller_types="fly" + controller_types="fly", + size=(700, 560) ) lorenz_line = figure[0, 0].add_line_collection(data=lorenz_data, thickness=.1, cmap="tab10") @@ -59,14 +60,14 @@ def lorenz(xyz, *, s=10, r=28, b=2.667): scatter_markers = list() for graphic in lorenz_line: - marker = figure[0, 0].add_scatter(graphic.data.value[0], sizes=8, colors=graphic.colors[0]) + marker = figure[0, 0].add_scatter(graphic.data.value[0], sizes=16, colors=graphic.colors[0]) scatter_markers.append(marker) # initialize time time = 0 -def animate(supblot): +def animate(subplot): global time time += 2 @@ -83,7 +84,7 @@ def animate(supblot): figure.show() # set initial camera position to make animation in gallery render better -figure[0, 0].camera.world.z = 75 +figure[0, 0].camera.world.z = 80 # NOTE: `if __name__ == "__main__"` is NOT how to use fastplotlib interactively # please see our docs for using fastplotlib interactively in ipython and jupyter diff --git a/examples/desktop/screenshots/gridplot.png b/examples/desktop/screenshots/gridplot.png index 99ba70155..1a222affd 100644 --- a/examples/desktop/screenshots/gridplot.png +++ b/examples/desktop/screenshots/gridplot.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a0da6067ecd930fb0add52124dfd97f7d73b27ab7696df681c75e333c749975a -size 328971 +oid sha256:8de769538bb435b71b33e038998b2bafa340c635211c0dfc388c7a5bf55fd36d +size 286794 diff --git a/examples/desktop/screenshots/gridplot_non_square.png b/examples/desktop/screenshots/gridplot_non_square.png index 6db1c3f2a..45d71abb2 100644 --- a/examples/desktop/screenshots/gridplot_non_square.png +++ b/examples/desktop/screenshots/gridplot_non_square.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2763431048efa1642a276bc3e659ed93a2f787ff6db700bcd29acc619d542f3f -size 236206 +oid sha256:92f55da7e2912a68e69e212b31df760f27e72253ec234fe1dd5b5463b60061b3 +size 212647 diff --git a/examples/desktop/screenshots/heatmap.png b/examples/desktop/screenshots/heatmap.png index a8f91765e..a63eb5ec8 100644 --- a/examples/desktop/screenshots/heatmap.png +++ b/examples/desktop/screenshots/heatmap.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d40c5e47f686dc498f003684efeefc16e6962d6ce1e2edc4c2cd8537b3ff3387 -size 82267 +oid sha256:1f2f0699e01eb12c44a2dbefd1d8371b86b3b3456b28cb5f1850aed44c13f412 +size 94505 diff --git a/examples/desktop/screenshots/image_cmap.png b/examples/desktop/screenshots/image_cmap.png index 837d6765f..6f7081b03 100644 --- a/examples/desktop/screenshots/image_cmap.png +++ b/examples/desktop/screenshots/image_cmap.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:95ed35b1ab7d5e56ff81e883d5c56419ddede3481f1a0c77f5af01dba83d03ea -size 236774 +oid sha256:e1482ce72511bc4f815825c29fabac5dd0f2586ac4c827a220a5cecb1162be4d +size 210019 diff --git a/examples/desktop/screenshots/image_rgb.png b/examples/desktop/screenshots/image_rgb.png index 2ca946c15..88beb7df3 100644 --- a/examples/desktop/screenshots/image_rgb.png +++ b/examples/desktop/screenshots/image_rgb.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:86e421deb8e013f25737b9a752409890ba14f794a1a01fbed728d474490292bb -size 269316 +oid sha256:8210ad8d1755f7819814bdaaf236738cdf1e9a0c4f77120aca4968fcd8aa8a7a +size 239431 diff --git a/examples/desktop/screenshots/image_rgbvminvmax.png b/examples/desktop/screenshots/image_rgbvminvmax.png index c31263344..f3ef59d84 100644 --- a/examples/desktop/screenshots/image_rgbvminvmax.png +++ b/examples/desktop/screenshots/image_rgbvminvmax.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fc5983f07d840320bf6866896d221845f59eecedbc6d89a7a0bc5dd1f6472c7b -size 49999 +oid sha256:8ebbcc4a2e83e9733eb438fe2341f77c86579421f3fa96b6a49e94073c0ffd32 +size 48270 diff --git a/examples/desktop/screenshots/image_simple.png b/examples/desktop/screenshots/image_simple.png index 194e5afe4..0c7e011f4 100644 --- a/examples/desktop/screenshots/image_simple.png +++ b/examples/desktop/screenshots/image_simple.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ec0770ff5671a9f83f43f8ece18e45b74137244ff578b8035eace3fd98291595 -size 237699 +oid sha256:44bc2d1fd97921fef0be45424f21513d5d978b807db8cf148dfc59c07f6e292f +size 211333 diff --git a/examples/desktop/screenshots/image_small.png b/examples/desktop/screenshots/image_small.png index 5ed8f615d..41a4a240e 100644 --- a/examples/desktop/screenshots/image_small.png +++ b/examples/desktop/screenshots/image_small.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3818b137bcfce829ea6a8670ca52a20122b2489f536ca5ff38e0ed6288043113 -size 12824 +oid sha256:079ee6254dc995cc5fc8c20ff1c00cb0899f21ba2d5d1a4dc0d020c3a71902c4 +size 13022 diff --git a/examples/desktop/screenshots/image_vminvmax.png b/examples/desktop/screenshots/image_vminvmax.png index c31263344..f3ef59d84 100644 --- a/examples/desktop/screenshots/image_vminvmax.png +++ b/examples/desktop/screenshots/image_vminvmax.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fc5983f07d840320bf6866896d221845f59eecedbc6d89a7a0bc5dd1f6472c7b -size 49999 +oid sha256:8ebbcc4a2e83e9733eb438fe2341f77c86579421f3fa96b6a49e94073c0ffd32 +size 48270 diff --git a/examples/desktop/screenshots/image_widget.png b/examples/desktop/screenshots/image_widget.png new file mode 100644 index 000000000..af248dd3e --- /dev/null +++ b/examples/desktop/screenshots/image_widget.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e2ae1938c5e7b742fb2dac0336877028f6ece26cd80e84f309195a55601025cb +size 197495 diff --git a/examples/desktop/screenshots/image_widget_grid.png b/examples/desktop/screenshots/image_widget_grid.png new file mode 100644 index 000000000..e0f0ff5c8 --- /dev/null +++ b/examples/desktop/screenshots/image_widget_grid.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:eeb5b86e7c15dfe2e71267453426930200223026f72156f34ff1ccc2f9389b6e +size 253769 diff --git a/examples/desktop/screenshots/image_widget_imgui.png b/examples/desktop/screenshots/image_widget_imgui.png new file mode 100644 index 000000000..135a0d4c4 --- /dev/null +++ b/examples/desktop/screenshots/image_widget_imgui.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7e2cd0e3892377e6e2d552199391fc64aac6a02413168a5b4c5c4848f3390dec +size 166265 diff --git a/examples/desktop/screenshots/image_widget_single_video.png b/examples/desktop/screenshots/image_widget_single_video.png new file mode 100644 index 000000000..aa829125c --- /dev/null +++ b/examples/desktop/screenshots/image_widget_single_video.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:11ffeceb298c5b5d429da822e1764c11d862bf85630ce3390475c766366bceab +size 91299 diff --git a/examples/desktop/screenshots/image_widget_videos.png b/examples/desktop/screenshots/image_widget_videos.png new file mode 100644 index 000000000..70ad686c6 --- /dev/null +++ b/examples/desktop/screenshots/image_widget_videos.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2c17df678e54e9cddbd42869cfe7a32069b7ffa70e6227c95c333537e1efede6 +size 170218 diff --git a/examples/desktop/screenshots/imgui_basic.png b/examples/desktop/screenshots/imgui_basic.png new file mode 100644 index 000000000..27288e38f --- /dev/null +++ b/examples/desktop/screenshots/imgui_basic.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3391b7cf02fc7bd2c73dc57214b21ceaca9a1513556b3a4725639f21588824e4 +size 36261 diff --git a/examples/desktop/screenshots/line.png b/examples/desktop/screenshots/line.png index 3cf15db2d..492ea2ada 100644 --- a/examples/desktop/screenshots/line.png +++ b/examples/desktop/screenshots/line.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e0ea3004cc871f54d1f12f6e5a39afbda568748ca907468a0533268949c67916 -size 173435 +oid sha256:1458d472362f8d5bcef599fd64f931997a246f9e7649c80cc95f465cbd858850 +size 170243 diff --git a/examples/desktop/screenshots/line_cmap.png b/examples/desktop/screenshots/line_cmap.png index 6ec5a4998..10779fcd5 100644 --- a/examples/desktop/screenshots/line_cmap.png +++ b/examples/desktop/screenshots/line_cmap.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cbf54efd9999593043c48a53f189c675ef6544a962c44297ce76df4fbe75ad42 -size 47804 +oid sha256:66e64835f824d80dd7606d90530517dbc320bcc11a68393ab92c08fef3d23f5a +size 48828 diff --git a/examples/desktop/screenshots/line_collection.png b/examples/desktop/screenshots/line_collection.png index ffe8cc96e..d9124daf1 100644 --- a/examples/desktop/screenshots/line_collection.png +++ b/examples/desktop/screenshots/line_collection.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b373c63989b4d3d3c9b5ea1607ef1602fa7d45753cdc0895a6e6d1d4a2c5420b -size 106504 +oid sha256:50920f4bc21bb5beffe317777a20d8d09f90f3631a14df51c219814d3507c602 +size 100758 diff --git a/examples/desktop/screenshots/line_collection_cmap_values.png b/examples/desktop/screenshots/line_collection_cmap_values.png index 66d36dec3..e04289699 100644 --- a/examples/desktop/screenshots/line_collection_cmap_values.png +++ b/examples/desktop/screenshots/line_collection_cmap_values.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:dff530c128132f26aded7c2ad9e202cc98e7486fbad84146a9055b6514c99453 -size 67561 +oid sha256:850e3deb2220d44f01e6366ee7cffb83085cad933a137b9838ce8c2231e7786a +size 64152 diff --git a/examples/desktop/screenshots/line_collection_cmap_values_qualitative.png b/examples/desktop/screenshots/line_collection_cmap_values_qualitative.png index b144dbdcb..710cee119 100644 --- a/examples/desktop/screenshots/line_collection_cmap_values_qualitative.png +++ b/examples/desktop/screenshots/line_collection_cmap_values_qualitative.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ce6e25567214539b296248a4dc665552f47687cda03d412f715db7f72138c341 -size 69992 +oid sha256:ba5fefc8e1043fe0ebd926a6b8e6ab19e724205a4c13e4d7740122cfe464e38b +size 67017 diff --git a/examples/desktop/screenshots/line_collection_colors.png b/examples/desktop/screenshots/line_collection_colors.png index 90948c126..6c1d05f04 100644 --- a/examples/desktop/screenshots/line_collection_colors.png +++ b/examples/desktop/screenshots/line_collection_colors.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9aeb3ef27fd7a393b4884749e7988e8cde3906c9f19b573e51bd78bf31fc7a45 -size 60514 +oid sha256:17d48f07310090b835e5cd2e6fa9c178db9af8954f4b0a9d52d21997ec229abd +size 57778 diff --git a/examples/desktop/screenshots/line_collection_slicing.png b/examples/desktop/screenshots/line_collection_slicing.png index 26933c5cc..abb63760f 100644 --- a/examples/desktop/screenshots/line_collection_slicing.png +++ b/examples/desktop/screenshots/line_collection_slicing.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:beb5193965530c490324edeb253ed429237e44289c5239079743a71d2aece797 -size 132171 +oid sha256:ed0d4fdb729409d07ec9ec9e05d915a04ebb237087d266591e7f46b0838e05b3 +size 130192 diff --git a/examples/desktop/screenshots/line_colorslice.png b/examples/desktop/screenshots/line_colorslice.png index 34ff56c4f..1f100d89e 100644 --- a/examples/desktop/screenshots/line_colorslice.png +++ b/examples/desktop/screenshots/line_colorslice.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c8afbeb5a79192eb1805c7c8478b26f6aabc534f3ac58fc7190f108ebb8640fe -size 56462 +oid sha256:1b2c5562f4150ec69029a4a139469b0a2524a14078b78055df40d9b487946ce5 +size 57037 diff --git a/examples/desktop/screenshots/line_dataslice.png b/examples/desktop/screenshots/line_dataslice.png index c135997bb..b2f963195 100644 --- a/examples/desktop/screenshots/line_dataslice.png +++ b/examples/desktop/screenshots/line_dataslice.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e6c5c4ef3aaeca5597c11e5db3764599c8c41b191c692db5fda54f525d8079da -size 68033 +oid sha256:c31a12afa3e66c442e370e6157ad9a5aad225b21f0f95fb6a115066b1b4f2e73 +size 68811 diff --git a/examples/desktop/screenshots/line_stack.png b/examples/desktop/screenshots/line_stack.png index ea5a3a330..786f434be 100644 --- a/examples/desktop/screenshots/line_stack.png +++ b/examples/desktop/screenshots/line_stack.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cdb26c1460583f8f605ffe6751c926c0e84463b10d68343169660593b82a9078 -size 130495 +oid sha256:fcfa7c49d465ff9cfe472ee885bcc9d9a44106b82adfc151544847b95035d760 +size 121640 diff --git a/examples/desktop/screenshots/scatter_cmap_iris.png b/examples/desktop/screenshots/scatter_cmap_iris.png index 96acbec6c..a887d1f99 100644 --- a/examples/desktop/screenshots/scatter_cmap_iris.png +++ b/examples/desktop/screenshots/scatter_cmap_iris.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:79f7d22b575c3a68dfdcd4bf806f79f1896a784ecbb6a2d3ba01da5731fa78dd -size 59731 +oid sha256:6d6bfba80eb737099040eebce9b70e1b261720f26cc895ec4b81ca21af60471c +size 60550 diff --git a/examples/desktop/screenshots/scatter_colorslice_iris.png b/examples/desktop/screenshots/scatter_colorslice_iris.png index 73fcddebf..e260df642 100644 --- a/examples/desktop/screenshots/scatter_colorslice_iris.png +++ b/examples/desktop/screenshots/scatter_colorslice_iris.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3c778cf9c51c9636d4f4ff13e4a1c841795a4dba327eb7118de2a0fb60c7e3f3 -size 35810 +oid sha256:febd4aa7240eea70b2759337cf98be31cacc1b147859bf628e929ead0153ef9c +size 36791 diff --git a/examples/desktop/screenshots/scatter_dataslice_iris.png b/examples/desktop/screenshots/scatter_dataslice_iris.png index 32f797c67..e5f05bb74 100644 --- a/examples/desktop/screenshots/scatter_dataslice_iris.png +++ b/examples/desktop/screenshots/scatter_dataslice_iris.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:444f0bd81459a4977df2eb9aa5645c0f7745fce97baa0c9e39c254bd32cdb1e6 -size 38351 +oid sha256:6cfbc717281c15c6d1d8fe2989770bc9c46f42052c897c2270294ad1b4b40d66 +size 39296 diff --git a/examples/desktop/screenshots/scatter_iris.png b/examples/desktop/screenshots/scatter_iris.png index dc53d97b0..9c452d448 100644 --- a/examples/desktop/screenshots/scatter_iris.png +++ b/examples/desktop/screenshots/scatter_iris.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:153db7a803709978a1a997d7c94db37ebc0504ec9a7eebce80977d4c90d48f61 -size 37365 +oid sha256:98eab41312eb42cbffdf8add0651b55e63b5c2fb5f4523e32dc51ed28a1be369 +size 38452 diff --git a/examples/desktop/screenshots/scatter_size.png b/examples/desktop/screenshots/scatter_size.png index 74c1b6e56..f2f036ea4 100644 --- a/examples/desktop/screenshots/scatter_size.png +++ b/examples/desktop/screenshots/scatter_size.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:381877c06882f40a8b46bbe07e1e1ca41a74ff9cda84544cca4ee92a4b522cda -size 62476 +oid sha256:e3522468f99c030cb27c225f009ecb4c7aafbd97cfc743cf1d07fb8d7ff8e0d4 +size 71336 diff --git a/examples/desktop/selectors/README.rst b/examples/desktop/selectors/README.rst index 0f7e412a7..e0376d728 100644 --- a/examples/desktop/selectors/README.rst +++ b/examples/desktop/selectors/README.rst @@ -1,2 +1,2 @@ Selection Tools -=============== \ No newline at end of file +=============== diff --git a/examples/desktop/selectors/linear_selector.py b/examples/desktop/selectors/linear_selector.py index b224c197f..d724ccf5d 100644 --- a/examples/desktop/selectors/linear_selector.py +++ b/examples/desktop/selectors/linear_selector.py @@ -2,7 +2,7 @@ Linear Selectors ================ -Example showing how to use a `LinearSelector` with lines, line collections, and images +Example showing how to use a `LinearSelector` with lines and line collections. """ # test_example = false @@ -11,6 +11,7 @@ import fastplotlib as fpl import numpy as np + # create some data xs = np.linspace(0, 10 * np.pi, 100) sine = np.column_stack([xs, np.sin(xs)]) @@ -23,7 +24,7 @@ # create a figure figure = fpl.Figure( - shape=(2, 2), + shape=(1, 2), size=(700, 560) ) @@ -42,7 +43,7 @@ line_selector_text, offset=(0., 1.75, 0.), anchor="middle-left", - font_size=22, + font_size=32, face_color=line.colors[0], outline_color="w", outline_thickness=0.1, @@ -50,6 +51,8 @@ # add an event handler using a decorator, selectors are just like other graphics +# you can also use the .add_event_handler() method directly instead of a decorator +# see the line collection example below for a non-decorator example @line_selector.add_event_handler("selection") def line_selector_changed(ev): selection = ev.info["value"] @@ -79,13 +82,11 @@ def line_selector_changed(ev): line_stack_selector_text, offset=(0., 7.0, 0.), anchor="middle-left", - font_size=18, + font_size=24, face_color="w", ) -# add an event handler using a decorator -@line_stack_selector.add_event_handler("selection") def line_stack_selector_changed(ev): selection = ev.info["value"] @@ -101,57 +102,21 @@ def line_stack_selector_changed(ev): f"cosine y value: {line_stack[1].data[index, 1]:.2f}\n") -# create an image -image = figure[1, 0].add_image(image_data) - -# add a row selector -image_row_selector = image.add_linear_selector(axis="y") - -# add column selector -image_col_selector = image.add_linear_selector() - -# make a line to indicate row data -line_image_row = figure[1, 1].add_line(image.data[0]) - -# make a line to indicate column data -line_image_col_data = np.column_stack([image.data[:, 0], np.arange(100)]) -line_image_col = figure[1, 1].add_line(line_image_col_data) - - -# callbacks to change the line data in subplot [1, 1] -# to display selected row and selected column data -def image_row_selector_changed(ev): - ix = ev.get_selected_index() - new_data = image.data[ix] - # set y values of line - line_image_row.data[:, 1] = new_data - - -def image_col_selector_changed(ev): - ix = ev.get_selected_index() - new_data = image.data[:, ix] - # set x values of line - line_image_col.data[:, 0] = new_data - - -# add event handlers, you can also use a decorator -image_row_selector.add_event_handler(image_row_selector_changed, "selection") -image_col_selector.add_event_handler(image_col_selector_changed, "selection") - -figure.show(maintain_aspect=False) +# add an event handler, you can also use a decorator +line_stack_selector.add_event_handler(line_stack_selector_changed, "selection") # some axes and camera zoom settings for subplot in [figure[0, 0], figure[0, 1]]: + subplot.axes.grids.xy.visible = True subplot.axes.auto_grid = False subplot.axes.grids.xy.major_step = (np.pi, 1) subplot.axes.grids.xy.minor_step = (0, 0) - subplot.camera.zoom = 0.6 -figure[1, 1].camera.zoom = 0.5 +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.run() \ No newline at end of file + fpl.run() diff --git a/examples/desktop/selectors/linear_selector_image.py b/examples/desktop/selectors/linear_selector_image.py new file mode 100644 index 000000000..540c7645a --- /dev/null +++ b/examples/desktop/selectors/linear_selector_image.py @@ -0,0 +1,73 @@ +""" +Linear Selectors Image +====================== + +Example showing how to use a `LinearSelector` to selector rows or columns of an image. The subplot on the right +displays the data for the selector row and column. +""" + +# test_example = false +# sphinx_gallery_pygfx_docs = 'screenshot' + +import fastplotlib as fpl +from imageio import v3 as iio + +image_data = iio.imread("imageio:coins.png") + +figure = fpl.Figure( + (1, 3), + size=(700, 300), + names=[["image", "selected row data", "selected column data"]] +) + +# create an image +image = figure[0, 0].add_image(image_data) + +# add a row selector +image_row_selector = image.add_linear_selector(axis="y") + +# add column selector +image_col_selector = image.add_linear_selector() + +# make a line to indicate row data +line_image_row = figure[0, 1].add_line(image.data[0]) + +# make a line to indicate column data +line_image_col = figure[0, 2].add_line(image.data[:, 0]) + + +# callbacks to change the line data in subplot [0, 1] +# to display selected row and selected column data +def image_row_selector_changed(ev): + ix = ev.get_selected_index() + new_data = image.data[ix] + # set y values of line with the row data + line_image_row.data[:, 1] = new_data + + +def image_col_selector_changed(ev): + ix = ev.get_selected_index() + new_data = image.data[:, ix] + # set y values of line with the column data + line_image_col.data[:, 1] = new_data + + +# add event handlers, you can also use a decorator +image_row_selector.add_event_handler(image_row_selector_changed, "selection") +image_col_selector.add_event_handler(image_col_selector_changed, "selection") + +# programmatically set the selection or drag it with your mouse pointer +image_row_selector.selection = 200 +image_col_selector.selection = 180 + +figure.show() + +for subplot in figure: + subplot.camera.zoom = 0.5 + + +# 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.run() diff --git a/examples/notebooks/heatmap.ipynb b/examples/notebooks/heatmap.ipynb index 7de3af2a0..08fd72501 100644 --- a/examples/notebooks/heatmap.ipynb +++ b/examples/notebooks/heatmap.ipynb @@ -10,12 +10,65 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 1, "id": "49b2498d-56ae-4559-9282-c8484f3e6b6d", "metadata": { "tags": [] }, - "outputs": [], + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "WARNING: The name 'ylorbr' is an alias for 'colorbrewer:YlOrBr', but is also available as: 'tol:YlOrBr'.\n", + "To silence this warning, use a fully namespaced name.\n", + "WARNING: The name 'rdbu' is an alias for 'colorbrewer:RdBu', but is also available as: 'vispy:RdBu'.\n", + "To silence this warning, use a fully namespaced name.\n", + "WARNING: The name 'rainbow' is an alias for 'gnuplot:rainbow', but is also available as: 'yorick:rainbow'.\n", + "To silence this warning, use a fully namespaced name.\n", + "WARNING: The name 'ice' is an alias for 'cmocean:ice', but is also available as: 'imagej:ice, vispy:ice'.\n", + "To silence this warning, use a fully namespaced name.\n", + "WARNING: The name 'fire' is an alias for 'imagej:fire', but is also available as: 'vispy:fire'.\n", + "To silence this warning, use a fully namespaced name.\n", + "WARNING: The name 'prgn' is an alias for 'colorbrewer:PRGn', but is also available as: 'tol:PRGn'.\n", + "To silence this warning, use a fully namespaced name.\n" + ] + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "7e56de31fa0c41fa8ac48dc276c157b9", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "Image(value=b'\\x89PNG\\r\\n\\x1a\\n\\x00\\x00\\x00\\rIHDR\\x00\\x00\\x01,\\x00\\x00\\x007\\x08\\x06\\x00\\x00\\x00\\xb6\\x1bw\\x99\\x…" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "WGPU: enumerate_adapters() is deprecated, use enumerate_adapters_sync() instead.\n", + "Unable to find extension: VK_EXT_swapchain_colorspace\n", + "WGPU: request_adapter() is deprecated, use request_adapter_sync() instead.\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Available devices:\n", + "✅ (default) | AMD RADV POLARIS10 (ACO) | DiscreteGPU | Vulkan | Mesa 20.3.5 (ACO)\n", + "❗ | llvmpipe (LLVM 11.0.1, 256 bits) | CPU | Vulkan | Mesa 20.3.5 (LLVM 11.0.1)\n", + "✅ | NVIDIA GeForce RTX 3080 | DiscreteGPU | Vulkan | 530.30.02\n", + "❗ | Radeon RX 570 Series (POLARIS10, DRM 3.40.0, 5.10.0-21-amd64, LLVM 11.0.1) | Unknown | OpenGL | 4.6 (Core Profile) Mesa 20.3.5\n" + ] + } + ], "source": [ "import numpy as np\n", "import fastplotlib as fpl" @@ -31,7 +84,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 2, "id": "40718465-abf6-4727-8bd7-4acdd59843d5", "metadata": { "tags": [] @@ -47,24 +100,75 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 3, "id": "02b072eb-2909-40c8-8739-950f07efbbc2", "metadata": { "tags": [] }, - "outputs": [], + "outputs": [ + { + "data": { + "text/plain": [ + "(10000, 20000)" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "data.shape" ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 4, "id": "84deb31b-5464-4cce-a938-694371011021", "metadata": { "tags": [] }, - "outputs": [], + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "956570245b55414e9b89bca0dc227535", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "RFBOutputContext()" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "WGPU: request_device() is deprecated, use request_device_sync() instead.\n" + ] + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "ce013002bc6f4d1893c14a25bd3ae55b", + "version_major": 2, + "version_minor": 0 + }, + "text/html": [ + "
snapshot
" + ], + "text/plain": [ + "JupyterWgpuCanvas()" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "fig = fpl.Figure()\n", "\n", diff --git a/examples/notebooks/image_widget_test.ipynb b/examples/notebooks/image_widget_test.ipynb index aaf41f3e3..2c05db6b0 100644 --- a/examples/notebooks/image_widget_test.ipynb +++ b/examples/notebooks/image_widget_test.ipynb @@ -153,26 +153,33 @@ "outputs": [], "source": [ "# testing cell ignore\n", - "assert iw_movie.sliders[\"t\"].max == gray_movie.shape[0] - 1\n", - "assert iw_movie.sliders[\"t\"].min == 0\n", + "assert iw_movie._dims_max_bounds[\"t\"] == gray_movie.shape[0]\n", + "\n", "plot_test(\"image-widget-movie-single-0\", iw_movie.figure)\n", - "iw_movie.sliders[\"t\"].value = 50\n", + "\n", + "iw_movie.current_index = {\"t\": 50}\n", "plot_test(\"image-widget-movie-single-50\", iw_movie.figure)\n", - "iw_movie.sliders[\"t\"].value = 279\n", + "\n", + "iw_movie.current_index = {\"t\": 279}\n", "plot_test(\"image-widget-movie-single-279\", iw_movie.figure)\n", - "iw_movie.sliders[\"t\"].value = 0\n", + "\n", + "iw_movie.current_index = {\"t\": 0}\n", "plot_test(\"image-widget-movie-single-0-reset\", iw_movie.figure)\n", - "iw_movie.sliders[\"t\"].value = 50\n", + "\n", + "iw_movie.current_index = {\"t\": 50}\n", "iw_movie.window_funcs = {\"t\": (np.mean, 13)}\n", - "# testing cell ignore\n", + "\n", "plot_test(\"image-widget-movie-single-50-window-mean-13\", iw_movie.figure)\n", "iw_movie.window_funcs[\"t\"].window_size = 33\n", + "\n", "plot_test(\"image-widget-movie-single-50-window-mean-33\", iw_movie.figure)\n", "iw_movie.window_funcs[\"t\"].func = np.max\n", + "\n", "plot_test(\"image-widget-movie-single-50-window-max-33\", iw_movie.figure)\n", "iw_movie.window_funcs = None\n", + "\n", "plot_test(\"image-widget-movie-single-50-window-reset\", iw_movie.figure)\n", - "iw_movie.sliders[\"t\"].value = 0" + "iw_movie.current_index = {\"t\": 0}" ] }, { @@ -305,24 +312,31 @@ "outputs": [], "source": [ "# testing cell ignore\n", - "assert iw_zfish.sliders[\"t\"].max == zfish_data.shape[0] - 1\n", - "assert iw_zfish.sliders[\"t\"].min == 0\n", + "assert iw_zfish._dims_max_bounds[\"t\"] == zfish_data.shape[0]\n", + "\n", "plot_test(\"image-widget-zfish-grid-init-mean-window-5\", iw_zfish.figure)\n", - "iw_zfish.sliders[\"t\"].value = 50\n", + "\n", + "iw_zfish.current_index = {\"t\": 50}\n", "plot_test(\"image-widget-zfish-grid-frame-50-mean-window-5\", iw_zfish.figure)\n", + "\n", "iw_zfish.window_funcs[\"t\"].window_size = 13\n", "plot_test(\"image-widget-zfish-grid-frame-50-mean-window-13\", iw_zfish.figure)\n", + "\n", "iw_zfish.window_funcs = None\n", "plot_test(\"image-widget-zfish-grid-frame-50\", iw_zfish.figure)\n", - "iw_zfish.sliders[\"t\"].value = 99\n", + "\n", + "iw_zfish.current_index = {\"t\": 99}\n", "plot_test(\"image-widget-zfish-grid-frame-99\", iw_zfish.figure)\n", - "iw_zfish.sliders[\"t\"].value = 50\n", + "\n", + "iw_zfish.current_index = {\"t\": 50}\n", "iw_zfish.window_funcs = {\"t\": (np.max, 13)}\n", "plot_test(\"image-widget-zfish-grid-frame-50-max-window-13\", iw_zfish.figure)\n", + "\n", "iw_zfish.window_funcs = None\n", "iw_zfish.frame_apply = lambda frame: gaussian_filter(frame.astype(np.float32), sigma=3)\n", "iw_zfish.reset_vmin_vmax()\n", "plot_test(\"image-widget-zfish-grid-frame-50-frame-apply-gaussian\", iw_zfish.figure)\n", + "\n", "iw_zfish.frame_apply = None\n", "iw_zfish.reset_vmin_vmax()\n", "plot_test(\"image-widget-zfish-grid-frame-50-frame-apply-reset\", iw_zfish.figure)" @@ -405,24 +419,31 @@ "outputs": [], "source": [ "# same tests as with the figure\n", - "assert iw_z.sliders[\"t\"].max == zfish_data.shape[0] - 1\n", - "assert iw_z.sliders[\"t\"].min == 0\n", + "assert iw_z._dims_max_bounds[\"t\"] == zfish_data.shape[0]\n", + "\n", "plot_test(\"image-widget-zfish-init-mean-window-5\", iw_z.figure)\n", - "iw_z.sliders[\"t\"].value = 50\n", + "\n", + "iw_z.current_index = {\"t\": 50}\n", "plot_test(\"image-widget-zfish-frame-50-mean-window-5\", iw_z.figure)\n", + "\n", "iw_z.window_funcs[\"t\"].window_size = 13\n", "plot_test(\"image-widget-zfish-frame-50-mean-window-13\", iw_z.figure)\n", + "\n", "iw_z.window_funcs = None\n", "plot_test(\"image-widget-zfish-frame-50\", iw_z.figure)\n", - "iw_z.sliders[\"t\"].value = 99\n", + "\n", + "iw_z.current_index = {\"t\": 99}\n", "plot_test(\"image-widget-zfish-frame-99\", iw_z.figure)\n", - "iw_z.sliders[\"t\"].value = 50\n", + "\n", + "iw_z.current_index = {\"t\": 50}\n", "iw_z.window_funcs = {\"t\": (np.max, 13)}\n", "plot_test(\"image-widget-zfish-frame-50-max-window-13\", iw_z.figure)\n", + "\n", "iw_z.window_funcs = None\n", "iw_z.frame_apply = lambda frame: gaussian_filter(frame.astype(np.float32), sigma=3)\n", "iw_z.reset_vmin_vmax()\n", "plot_test(\"image-widget-zfish-frame-50-frame-apply-gaussian\", iw_z.figure)\n", + "\n", "iw_z.frame_apply = None\n", "iw_z.reset_vmin_vmax()\n", "plot_test(\"image-widget-zfish-frame-50-frame-apply-reset\", iw_z.figure)" @@ -476,7 +497,7 @@ "metadata": {}, "outputs": [], "source": [ - "iw_mixed_shapes.sliders[\"t\"].value = 50\n", + "iw_mixed_shapes.current_index = {\"t\": 50}\n", "plot_test(\"image-widget-zfish-mixed-rgb-cockatoo-frame-50\", iw_mixed_shapes.figure)\n", "\n", "# Set the data, changing the first array and also the size of the \"T\" slider\n", @@ -485,7 +506,7 @@ "\n", "# Check how a window function might work on the RGB data\n", "iw_mixed_shapes.window_funcs = {\"t\": (np.mean, 4)}\n", - "iw_mixed_shapes.sliders[\"t\"].value = 20\n", + "iw_mixed_shapes.current_index = {\"t\": 20}\n", "plot_test(\"image-widget-zfish-mixed-rgb-cockatoo-windowrgb\", iw_mixed_shapes.figure)" ] }, diff --git a/examples/notebooks/screenshots/nb-astronaut.png b/examples/notebooks/screenshots/nb-astronaut.png index 70c1a95a7..32b09caf9 100644 --- a/examples/notebooks/screenshots/nb-astronaut.png +++ b/examples/notebooks/screenshots/nb-astronaut.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c1491279a44125be3fc51678a2662b0632d8618a7425b7894677a7eba919eae9 -size 84735 +oid sha256:8d9e2b0479d3de1c12764b984679dba83a1876ea6a88c072789a0e06f957ca2a +size 70655 diff --git a/examples/notebooks/screenshots/nb-astronaut_RGB.png b/examples/notebooks/screenshots/nb-astronaut_RGB.png index 0443de1c4..be498bb6d 100644 --- a/examples/notebooks/screenshots/nb-astronaut_RGB.png +++ b/examples/notebooks/screenshots/nb-astronaut_RGB.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:716e19f1f9d16443602de327716daee8663731e1afccfa4a9b16c68ffd3b0c11 -size 76074 +oid sha256:e2d02877510191e951d38d03a6fe9d31f5c0c335913876c65b175c4bb1a9c0e1 +size 69942 diff --git a/examples/notebooks/screenshots/nb-camera.png b/examples/notebooks/screenshots/nb-camera.png index e71803ade..3e9a518f9 100644 --- a/examples/notebooks/screenshots/nb-camera.png +++ b/examples/notebooks/screenshots/nb-camera.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b84ffb87948cfd523941041a3c9c6827ccac51bb5648faddd810d15a4bd0912c -size 52034 +oid sha256:5271c2204a928185b287c73c852ffa06b708d8d6a33de09acda8d2ea734e78c5 +size 51445 diff --git a/examples/notebooks/screenshots/nb-image-widget-movie-set_data.png b/examples/notebooks/screenshots/nb-image-widget-movie-set_data.png index e8e74e817..8fdf2fd89 100644 --- a/examples/notebooks/screenshots/nb-image-widget-movie-set_data.png +++ b/examples/notebooks/screenshots/nb-image-widget-movie-set_data.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d9421323aac16e9e8d3489332b7db7b2381effc4b10a132e2c58dc86544720ae -size 45797 +oid sha256:1faa3db006aa7f9d41757564783cef67d1a906dc67bca045c2c30501a86306c2 +size 43947 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 4fce1c96a..f64393c89 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:949885c0eab52bbb5293aa74ded4d3dedfd5172d1217934fa8963b7c74f176e8 -size 118713 +oid sha256:8e3f53d21e99424f11a3a920346909dce42f2c344ae9b43af5965bc2302ae9ab +size 117732 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 ffb80c4ec..f64393c89 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:f05522f502bc848086c01ba86f17036b310617e8bfb239d83505ef31c2ad23a7 -size 106685 +oid sha256:8e3f53d21e99424f11a3a920346909dce42f2c344ae9b43af5965bc2302ae9ab +size 117732 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 0063b3fa2..812e0f60d 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:9cb358df1f9dcb67f26818cad619c0740f25602cdbb634b737112d6d43c89fc8 -size 142265 +oid sha256:b59639c87a6d02aaf8a14e8d681d763a795c15b7aa8d2d0a90dba3a5732e4fe5 +size 140917 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 9c48d5258..9907e1473 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:62c303c87a6fbc2f2874b817ca0d938b8a6f83042e81a659d9feb7d7fe7442a6 -size 127805 +oid sha256:5f9f5e1953aae367cca8add259c86d82fd5225f4cf4279c6504b1ecd9d5a0bd1 +size 125867 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 388a280e1..695964431 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:b22f9823bab849de025b256558f008efdfadcb181c66510f32293d2fea53c6f0 -size 110339 +oid sha256:3633ce4d8995ebdb224df9fcd8ebdf22ad9ffa72e3ef80692f4f691895faf903 +size 110162 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 1d0802226..039cdd25c 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:5baf57418ed6f36278715187250ac69307cd88eb4232df586a3c929ffbc40d4b -size 102774 +oid sha256:8ba9762d2d3fb7ddaa1628e40588b28780ac3e0185ec97187eb1975016aa32f1 +size 102404 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 6534b9907..a6aae44ba 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:e009147472683c8d207a23d7c64575465f936ee48250dfa9fe15654ed7d34403 -size 126018 +oid sha256:f9374ebf448c1692c63c3ca1c28b0d16125bb5ae021d9a7cc8a1beee3c25a183 +size 124817 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 6534b9907..a6aae44ba 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:e009147472683c8d207a23d7c64575465f936ee48250dfa9fe15654ed7d34403 -size 126018 +oid sha256:f9374ebf448c1692c63c3ca1c28b0d16125bb5ae021d9a7cc8a1beee3c25a183 +size 124817 diff --git a/examples/notebooks/screenshots/nb-image-widget-single-gnuplot2.png b/examples/notebooks/screenshots/nb-image-widget-single-gnuplot2.png index f157e63c2..48ab5d6fe 100644 --- a/examples/notebooks/screenshots/nb-image-widget-single-gnuplot2.png +++ b/examples/notebooks/screenshots/nb-image-widget-single-gnuplot2.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4d77e42683f74dbd311aa56e5c66b4bb90449e5e52a5a9d4ae3a04cf774ca4df -size 306329 +oid sha256:c65e2dc4276393278ab769706f22172fd71e38eeb3c9f4d70fa51de31820f1d1 +size 234012 diff --git a/examples/notebooks/screenshots/nb-image-widget-single.png b/examples/notebooks/screenshots/nb-image-widget-single.png index c262e74ce..5e1cb8cc1 100644 --- a/examples/notebooks/screenshots/nb-image-widget-single.png +++ b/examples/notebooks/screenshots/nb-image-widget-single.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a0f6a4eea4dcf0100b6cdd89892fb02bc2d2c5396445ef0694a0810b9d4465e8 -size 274170 +oid sha256:7d4e4edf1429a135bafb7c1c927ea87f78a93fb5f3e0cbee2fb5c156af88d5a0 +size 220490 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 a78761846..0f6223ab4 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:de65879e7ad15cd85740c989d62bd27e4e2fdbe61de3585238caaa52b554fa95 -size 129651 +oid sha256:ecba9807b765ea12ad1183dabc35c9b6a2ba45f95aa0126d772801c3a5aba6e1 +size 92089 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 f5989caa9..0c6b55201 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:828e3e104d21f0cc743f16b72152642aa276d5727258b4454a3f5dcc6514ac7e -size 81188 +oid sha256:433190c3f56075ca3e9a5486e5986424d31fb7b6f6145225a15bfafd5e00fa83 +size 74779 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 3e3cdc025..8321b60e9 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:e615d9dbcbc09d14eb1ab2aea14c609e996e0f96bafdf3c4513acd0613260509 -size 205824 +oid sha256:de11fd007bad064ccb6574ee682d7cd25c64738768d1dc9e42b53c88cb78c46c +size 155123 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 22fe4e54d..27c3af054 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:d81c351726c3eb1cbef662b28454747e6074374bdd2203d0e71835c6611cda11 -size 151657 +oid sha256:ec9e3a90abd029a5fb6e149ba721f3340e499c26a2dd4aab6ab07a185bbc4ff0 +size 103878 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 e6a877eec..72ee543e2 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:71676028d9a29fb4dbb4e3aaa3dd9173dff585fe08300a5d8884e6d8e413952e -size 131857 +oid sha256:f1c9b64c4c67a024a5dc839ad13f68ee60f3b3675144976c7e3b6ce989e0c822 +size 97746 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 023cb947c..572b1e590 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:dc841d0a37938319d177ecd74662de486c4fe2bc8be35de456ad7a3abd4ca329 -size 90997 +oid sha256:51865c6bfd62d9691c638d4bc3b62507a61e873851dc35bad66acb0d41e4fe3e +size 83536 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 c1fa94056..1ceebf476 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:42a51e116e1455fcea00dd9ea9f387633df31e185706cd0fd18e8101947718be -size 74817 +oid sha256:9f46a0e116fed8d474217d5a6ca6f9861647707e1abe049e796c67a417e197cd +size 72243 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 f79d956b0..8464eed64 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:f9900ac2df273c48d163538c6cbda2d4b43245bbcc55f759a50883aaf51cf876 -size 160362 +oid sha256:ac0fcaae315baf29a46f2c4151b988535934927441c13eef711b8567951b50cb +size 106873 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 572e1c2a7..c81b99e29 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:35d948baddc0c069e65d63d04fb3f22276dd936deee281fdf0bf31c2203c0e01 -size 156432 +oid sha256:a001606a953ea6b8a5c7f741532df4a006e7bf4dd31ff9e7b4f9f1025987153c +size 109591 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 8f083da9b..b7314938c 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:371727d51121efa1f6250f9aebdb1a3847e2ef79cf1a9137d5c07b8738114b9b -size 307668 +oid sha256:cb99c2827dea7e49d5ffcd4fe10e4052671548500f05001067077a8841d03cc8 +size 168987 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 e59f9020f..b09d8b4ca 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:d179a1279dcd12c10495e3022fdf8ae3b5ee8ed50ca0ba512f0dbd6b1fd325f8 -size 184162 +oid sha256:729b31d419eccf02eb617a10bb29b2f9295726f8167329ac0f7498e752688bb8 +size 113715 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 3d133063f..9daeb680f 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:dae80d58e60532eb151354de65574b70a73bc6ef8dcaba8c65d39da6cc388eda -size 184497 +oid sha256:7de21cfe1a21c1ef59bfb7121b2ef6f55dd9931d5885a3ee3c932dcf82389191 +size 116752 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 e79a20bbd..fff0cca94 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:ba9a962dfdc0bcfd033dff87e970554b3d480226c904a3fb2428c427312c7e42 -size 176697 +oid sha256:ca70505035a6b8cb8589a8219edd3d79eecdc93642e3d7f7db00516d27bac959 +size 122542 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 9f8791bcb..57154e0c0 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:b3cbdc194f63da1e33a7e367175e93e49bf5f69c988bb8ac03c137bd1505adc5 -size 166434 +oid sha256:a41f79b543ea852345ca5c5c3e401d39c43b9f5d4172805c48d7010d3e85e88a +size 118378 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 fcd0b1382..e5554d635 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:051f4e6dc5a6a9824da12165bf6475baf83886ca97c6ba10c0ea2f615dc4e0ee -size 162378 +oid sha256:d364f4c18516c282cd284c731122618bdf3418b6d536afa7b0556105f89c3607 +size 119048 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 9d45ca1aa..048078520 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:6adc3973283da26ad475351af58d91892251c53fe0e09f714cf21cfdab7502c6 -size 140885 +oid sha256:33ce1260b4715b3d28ba28d0ad4c5eb94c9997bdc1676ff6208121e789e168a5 +size 99287 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 190025d6d..ade8fb483 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:bab10f413eaac26d69848ada9528fa1e36e616eab37c5244d7f1c9c3ab85e7d6 -size 143505 +oid sha256:5e08f4e4cb3330fbbbf827af56c02039af3b293036c7676f2a87c309ad07f2f6 +size 99759 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 e97c2ffd0..94b39e8f9 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:10042f15d067049e05418f1e82eb3428f56f53a2c159ac4eaa135d42dfc3d057 -size 88268 +oid sha256:07bcc6ef243d9d3ffd7fae69facf655e7c02b1cb53ea96a38b40ed672655bf66 +size 86607 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 de9822952..dab2098fd 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:1e031e6712bb7a9601f627e32347c05ed2669363ee1ffe428d10797081c32ef0 -size 113064 +oid sha256:7270870881ac478f48a269b060c5bf7ca59e7abe8a42254162a0295f6165230b +size 117870 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 2e47302a8..7f530e554 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:0aaa7782c20f209e07a7259d676b4fc993d4f25ba1a52150d5512d8ef16b82bc -size 130999 +oid sha256:414ebe9a0b2bc4eb1caa4b4aeef070955c662bb691899c4b2046be3e2ca821e3 +size 113649 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 9104fb9ea..e2f6b8318 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:5b478e4cd25c96e2c08b3f595193d019a0cfcac69f8ea3e3a8330cf6c0ffabbf -size 131188 +oid sha256:ea6d0c4756db434af6e257b7cd809f1d49089eca6b9eae9e347801e20b175686 +size 113631 diff --git a/examples/notebooks/screenshots/nb-lines-3d.png b/examples/notebooks/screenshots/nb-lines-3d.png index 65310e7f1..2e26a8cd7 100644 --- a/examples/notebooks/screenshots/nb-lines-3d.png +++ b/examples/notebooks/screenshots/nb-lines-3d.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fc7a8caabb59ff2f2fd9811678b974542c6a3dddfd0d005109777264056d458a -size 23430 +oid sha256:857eb528b02fd7dd4b9f46ce1e65942066736f1bdf5271db141d73a0abab82b0 +size 19457 diff --git a/examples/notebooks/screenshots/nb-lines-cmap-jet-values-cosine.png b/examples/notebooks/screenshots/nb-lines-cmap-jet-values-cosine.png index 9f3a156b9..08b6e8ac1 100644 --- a/examples/notebooks/screenshots/nb-lines-cmap-jet-values-cosine.png +++ b/examples/notebooks/screenshots/nb-lines-cmap-jet-values-cosine.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:80d318cb1daf701e682e600c0058cf3e5c336062dd459618eac60f92ec2399ad -size 17362 +oid sha256:a499ecad892c779aa857e9074a5e157b02bc914007b28aa4958b2b231b5961a4 +size 18585 diff --git a/examples/notebooks/screenshots/nb-lines-cmap-jet-values.png b/examples/notebooks/screenshots/nb-lines-cmap-jet-values.png index 677906685..83b5c21d9 100644 --- a/examples/notebooks/screenshots/nb-lines-cmap-jet-values.png +++ b/examples/notebooks/screenshots/nb-lines-cmap-jet-values.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:996d29cdf767f3863615ebc0d5b91f4ec07af898350c07b2fd78426c239cb452 -size 18817 +oid sha256:9506e9838bd5bb1f79d41c8dfaa92c127d12852758bfcecfa37202d02b0ba325 +size 19914 diff --git a/examples/notebooks/screenshots/nb-lines-cmap-jet.png b/examples/notebooks/screenshots/nb-lines-cmap-jet.png index 5195c617d..34a6f8b6f 100644 --- a/examples/notebooks/screenshots/nb-lines-cmap-jet.png +++ b/examples/notebooks/screenshots/nb-lines-cmap-jet.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:74da9cc1bac480b5d72693ea7e074d5192e382a989816912757bd307a1e04faf -size 17367 +oid sha256:8e6a74dad6621df938517558adf89e19e975b39386d1a46d48991f0cffe725dd +size 18549 diff --git a/examples/notebooks/screenshots/nb-lines-cmap-tab-10.png b/examples/notebooks/screenshots/nb-lines-cmap-tab-10.png index d766bcda0..ca41e764d 100644 --- a/examples/notebooks/screenshots/nb-lines-cmap-tab-10.png +++ b/examples/notebooks/screenshots/nb-lines-cmap-tab-10.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:04052da5609011c7df22e4688bfe1100ad90fa757099d7b315d0b2bcaeb8c3d0 -size 15876 +oid sha256:5725b52f18e1be1f7bd358b489374c0f733cf9c3f1f127a4fbfbd1daec30a57f +size 17061 diff --git a/examples/notebooks/screenshots/nb-lines-cmap-viridis-values.png b/examples/notebooks/screenshots/nb-lines-cmap-viridis-values.png index 723beb580..dc7ec0c13 100644 --- a/examples/notebooks/screenshots/nb-lines-cmap-viridis-values.png +++ b/examples/notebooks/screenshots/nb-lines-cmap-viridis-values.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1ea8423ccba04a9a137640b28ff8f84e167d46735d87fd38c48c29373a9601ac -size 16223 +oid sha256:f1f08fea3f0f74ab7632725f544c03303334cfcf1e01b188d01113a8bcc84dd7 +size 17353 diff --git a/examples/notebooks/screenshots/nb-lines-cmap-viridis.png b/examples/notebooks/screenshots/nb-lines-cmap-viridis.png index e6493053c..912cae1e4 100644 --- a/examples/notebooks/screenshots/nb-lines-cmap-viridis.png +++ b/examples/notebooks/screenshots/nb-lines-cmap-viridis.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9fd9697d7df47491c6b9e73424dd07088c7e22758d5672a99ddbce56e4ff3b02 -size 19316 +oid sha256:a5d14cb03071ca2ad4c0c2145d68ab3555d30565f8f6d2be6fa7fc649213d748 +size 20499 diff --git a/examples/notebooks/screenshots/nb-lines-cmap-white.png b/examples/notebooks/screenshots/nb-lines-cmap-white.png index cbbbef0bc..2edc6903b 100644 --- a/examples/notebooks/screenshots/nb-lines-cmap-white.png +++ b/examples/notebooks/screenshots/nb-lines-cmap-white.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:54242cbcd3f196e5f39fc3a27a98b42f9515f04625d37d3b210afd37721078dc -size 8967 +oid sha256:3c7a2c8ccd70787e67800fb0d9625d7816fb6b004c0128b3b254fec7c7fd7c71 +size 16058 diff --git a/examples/notebooks/screenshots/nb-lines-colors.png b/examples/notebooks/screenshots/nb-lines-colors.png index 60792f453..1e13983f3 100644 --- a/examples/notebooks/screenshots/nb-lines-colors.png +++ b/examples/notebooks/screenshots/nb-lines-colors.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9ae3f1bae2ea0fe146c7096af3551e5e58704416bff49a6f0bdd5415cfc1533b -size 37095 +oid sha256:6681a1e5658c1f2214217dcb7321cad89c7a0a3fd7919296a1069f27f1a7ee92 +size 35381 diff --git a/examples/notebooks/screenshots/nb-lines-data.png b/examples/notebooks/screenshots/nb-lines-data.png index 86ce4362b..a7e8287ef 100644 --- a/examples/notebooks/screenshots/nb-lines-data.png +++ b/examples/notebooks/screenshots/nb-lines-data.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:17ec845345cb97088de4c18c8eebc54c9a27c5770724f8f582b4144f3e70d139 -size 46868 +oid sha256:043d8d9cd6dfc7627a6ccdb5810efd4b1a15e8880a4e30c0f558ae4d67c2f470 +size 42410 diff --git a/examples/notebooks/screenshots/nb-lines-underlay.png b/examples/notebooks/screenshots/nb-lines-underlay.png index 7d1280db4..c2908d479 100644 --- a/examples/notebooks/screenshots/nb-lines-underlay.png +++ b/examples/notebooks/screenshots/nb-lines-underlay.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:467788d8588fa1794c0cd705e03564537ff49f79762a5e8f092700516d503391 -size 52447 +oid sha256:c52ac60ffc08005d1f1fcad1b29339a89a0f31b58c9ca692f9d93400e7c8ac9e +size 48540 diff --git a/examples/notebooks/screenshots/nb-lines.png b/examples/notebooks/screenshots/nb-lines.png index 4fd64a56d..f4a4d58b1 100644 --- a/examples/notebooks/screenshots/nb-lines.png +++ b/examples/notebooks/screenshots/nb-lines.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:da1e28036caa8077885f52aa3a6ba4dbe1ee4f8cfa79a7b604614483150cd7b7 -size 24798 +oid sha256:2cef0e2fb84e985f8d9c18f77817fb3eba31bd30b8fa4c54bb71432587909458 +size 30075 diff --git a/examples/tests/test_examples.py b/examples/tests/test_examples.py index 9562a4357..0143c9bef 100644 --- a/examples/tests/test_examples.py +++ b/examples/tests/test_examples.py @@ -8,7 +8,10 @@ import os import numpy as np import imageio.v3 as iio +import pygfx +MAX_TEXTURE_SIZE = 2048 +pygfx.renderers.wgpu.set_wgpu_limits(**{"max-texture-dimension2d": MAX_TEXTURE_SIZE}) from .testutils import ( ROOT, @@ -64,14 +67,30 @@ def test_example_screenshots(module, force_offscreen): .as_posix() .replace("/", ".") ) + print(pygfx.renderers.wgpu.get_shared().device.limits["max-texture-dimension2d"]) # import the example module example = importlib.import_module(module_name) + # there doesn't seem to be a resize event for the manual offscreen canvas + example.figure.imgui_renderer._backend.io.display_size = example.figure.canvas.get_logical_size() + + # run this once so any edge widgets set their sizes and therefore the subplots get the correct rect + # hacky but it works for now + example.figure.imgui_renderer.render() + + # render each subplot for subplot in example.figure: subplot.viewport.render(subplot.scene, subplot.camera) + for dock in subplot.docks.values(): + dock.set_viewport_rect() + + # flush pygfx renderer example.figure.renderer.flush() + # render imgui + example.figure.imgui_renderer.render() + # render a frame img = np.asarray(example.figure.renderer.target.draw()) diff --git a/examples/tests/testutils.py b/examples/tests/testutils.py index 2ea4742ea..3db6901ef 100644 --- a/examples/tests/testutils.py +++ b/examples/tests/testutils.py @@ -18,13 +18,15 @@ # examples live in themed sub-folders example_globs = [ "image/*.py", + "image_widget/*.py", "heatmap/*.py", "scatter/*.py", "line/*.py", "line_collection/*.py", "gridplot/*.py", "misc/*.py", - "selectors/*.py" + "selectors/*.py", + "guis/*.py", ] diff --git a/fastplotlib/__init__.py b/fastplotlib/__init__.py index 19dfb1903..a85de93c2 100644 --- a/fastplotlib/__init__.py +++ b/fastplotlib/__init__.py @@ -5,7 +5,15 @@ from .graphics.selectors import * from .graphics.utils import pause_events from .legends import * -from .layouts import Figure +from .tools import * + +from .layouts import IMGUI + +if IMGUI: + # default to imgui figure if imgui_bundle is installed + from .layouts import ImguiFigure as Figure +else: + from .layouts import Figure from .widgets import ImageWidget from .utils import config, enumerate_adapters, select_adapter, print_wgpu_report diff --git a/fastplotlib/graphics/_base.py b/fastplotlib/graphics/_base.py index 27bfbc149..c3fc665e7 100644 --- a/fastplotlib/graphics/_base.py +++ b/fastplotlib/graphics/_base.py @@ -7,6 +7,13 @@ import pylinalg as la from wgpu.gui.base import log_exception +try: + from imgui_bundle import imgui +except ImportError: + IMGUI = False +else: + IMGUI = True + import pygfx from ._features import ( @@ -117,6 +124,8 @@ def __init__( self._axes: Axes = None + self._right_click_menu = None + @property def supported_events(self) -> tuple[str]: """events supported by this graphic""" @@ -303,17 +312,6 @@ def _handle_event(self, callback, event: pygfx.Event): # for feature events event._target = self.world_object - if isinstance(event, pygfx.PointerEvent): - # map from screen to world space and data space - world_xy = self._plot_area.map_screen_to_world(event) - - # subtract offset to map to data - data_xy = world_xy - self.offset - - # append attributes - event.x_world, event.y_world = world_xy[:2] - event.x_data, event.y_data = data_xy[:2] - with log_exception(f"Error during handling {event.type} event"): callback(event) @@ -334,8 +332,6 @@ def remove_event_handler(self, callback, *types): self._event_handlers[t].remove(callback) # remove callback wrapper from world object if pygfx event if t in PYGFX_EVENTS: - print("pygfx event") - print(wrapper) self.world_object.remove_event_handler(wrapper, t) else: feature = getattr(self, f"_{t}") @@ -437,3 +433,24 @@ def add_axes(self): self._plot_area.scene.add(self.axes.world_object) self._axes.update_using_bbox(self.world_object.get_world_bounding_box()) + + @property + def right_click_menu(self): + return self._right_click_menu + + @right_click_menu.setter + def right_click_menu(self, menu): + if not IMGUI: + raise ImportError( + "imgui is required to set right-click menus:\n" + "pip install imgui_bundle" + ) + + self._right_click_menu = menu + menu.owner = self + + def _fpl_request_right_click_menu(self): + pass + + def _fpl_close_right_click_menu(self): + pass diff --git a/fastplotlib/graphics/_features/__init__.py b/fastplotlib/graphics/_features/__init__.py index 1d2f6ca44..4f9013425 100644 --- a/fastplotlib/graphics/_features/__init__.py +++ b/fastplotlib/graphics/_features/__init__.py @@ -14,7 +14,6 @@ ImageVmax, ImageInterpolation, ImageCmapInterpolation, - WGPU_MAX_TEXTURE_SIZE, ) from ._base import ( GraphicFeature, diff --git a/fastplotlib/graphics/_features/_base.py b/fastplotlib/graphics/_features/_base.py index a57f8a453..1612414a1 100644 --- a/fastplotlib/graphics/_features/_base.py +++ b/fastplotlib/graphics/_features/_base.py @@ -9,9 +9,6 @@ import pygfx -WGPU_MAX_TEXTURE_SIZE = 8192 - - def to_gpu_supported_dtype(array): """ convert input array to float32 numpy array diff --git a/fastplotlib/graphics/_features/_image.py b/fastplotlib/graphics/_features/_image.py index 2d93745bf..513677e15 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, WGPU_MAX_TEXTURE_SIZE +from ._base import GraphicFeature, FeatureEvent from ...utils import ( make_colors, @@ -20,6 +20,9 @@ def __init__(self, data, isolated_buffer: bool = True): data = self._fix_data(data) + shared = pygfx.renderers.wgpu.get_shared() + self._texture_limit_2d = shared.device.limits["max-texture-dimension2d"] + if isolated_buffer: # useful if data is read-only, example: memmaps self._value = np.zeros(data.shape, dtype=data.dtype) @@ -31,13 +34,13 @@ def __init__(self, data, isolated_buffer: bool = True): # data start indices for each Texture self._row_indices = np.arange( 0, - ceil(self.value.shape[0] / WGPU_MAX_TEXTURE_SIZE) * WGPU_MAX_TEXTURE_SIZE, - WGPU_MAX_TEXTURE_SIZE, + ceil(self.value.shape[0] / self._texture_limit_2d) * self._texture_limit_2d, + self._texture_limit_2d, ) self._col_indices = np.arange( 0, - ceil(self.value.shape[1] / WGPU_MAX_TEXTURE_SIZE) * WGPU_MAX_TEXTURE_SIZE, - WGPU_MAX_TEXTURE_SIZE, + ceil(self.value.shape[1] / self._texture_limit_2d) * self._texture_limit_2d, + self._texture_limit_2d, ) # buffer will be an array of textures @@ -118,8 +121,8 @@ def __next__(self) -> tuple[pygfx.Texture, tuple[int, int], tuple[slice, slice]] chunk_index = (chunk_row, chunk_col) # stop indices of big data array for this chunk - row_stop = min(self.value.shape[0], data_row_start + WGPU_MAX_TEXTURE_SIZE) - col_stop = min(self.value.shape[1], data_col_start + WGPU_MAX_TEXTURE_SIZE) + row_stop = min(self.value.shape[0], data_row_start + self._texture_limit_2d) + col_stop = min(self.value.shape[1], data_col_start + self._texture_limit_2d) # row and column slices that slice the data for this chunk from the big data array data_slice = (slice(data_row_start, row_stop), slice(data_col_start, col_stop)) diff --git a/fastplotlib/layouts/__init__.py b/fastplotlib/layouts/__init__.py index 60111cabc..4a4f45174 100644 --- a/fastplotlib/layouts/__init__.py +++ b/fastplotlib/layouts/__init__.py @@ -1,3 +1,15 @@ from ._figure import Figure -__all__ = ["Figure"] +try: + import imgui_bundle +except ImportError: + IMGUI = False +else: + IMGUI = True + +if IMGUI: + from ._imgui_figure import ImguiFigure + + __all__ = ["Figure", "ImguiFigure"] +else: + __all__ = ["Figure"] diff --git a/fastplotlib/layouts/_figure.py b/fastplotlib/layouts/_figure.py index 3ad5231c7..bba5d4aab 100644 --- a/fastplotlib/layouts/_figure.py +++ b/fastplotlib/layouts/_figure.py @@ -109,7 +109,9 @@ def __init__( else: subplot_names = None - canvas, renderer = make_canvas_and_renderer(canvas, renderer) + canvas, renderer = make_canvas_and_renderer( + canvas, renderer, canvas_kwargs={"size": size} + ) if isinstance(cameras, str): # create the array representing the views for each subplot in the grid @@ -322,25 +324,10 @@ def __init__( self._current_iter = None - self._starting_size = size + self._sidecar = None self._output = None - if self.canvas.__class__.__name__ == "JupyterWgpuCanvas": - self.recorder = FigureRecorder(self) - else: - self.recorder = None - - @property - def toolbar(self): - """ipywidget or QToolbar instance""" - return self._output.toolbar - - @property - def output(self): - """ipywidget or QWidget that contains this plot""" - return self._output - @property def shape(self) -> tuple[int, int]: """[n_rows, n_cols]""" @@ -390,7 +377,7 @@ def __getitem__(self, index: tuple[int, int] | str) -> Subplot: else: return self._subplots[index[0], index[1]] - def render(self): + def render(self, draw=True): # call the animation functions before render self._call_animate_functions(self._animate_funcs_pre) @@ -398,7 +385,8 @@ def render(self): subplot.render() self.renderer.flush() - self.canvas.request_draw() + if draw: + self.canvas.request_draw() # call post-render animate functions self._call_animate_functions(self._animate_funcs_post) @@ -406,19 +394,16 @@ def render(self): def start_render(self): """start render cycle""" self.canvas.request_draw(self.render) - self.canvas.set_logical_size(*self._starting_size) def show( self, autoscale: bool = True, maintain_aspect: bool = None, - toolbar: bool = True, sidecar: bool = False, sidecar_kwargs: dict = None, - add_widgets: list = None, ): """ - Begins the rendering event loop and shows the plot in the desired output context (jupyter, qt or glfw). + Begins the rendering event loop and shows the Figure, returns the canvas Parameters ---------- @@ -428,29 +413,22 @@ def show( maintain_aspect: bool, default ``True`` maintain aspect ratio - toolbar: bool, default ``True`` - show toolbar - sidecar: bool, default ``True`` - display plot in a ``jupyterlab-sidecar``, only for jupyter output context + display plot in a ``jupyterlab-sidecar``, only in jupyter sidecar_kwargs: dict, default ``None`` kwargs for sidecar instance to display plot i.e. title, layout - add_widgets: list of widgets - a list of ipywidgets or QWidget that are vertically stacked below the plot - Returns ------- - OutputContext - In jupyter, it will display the plot in the output cell or sidecar - - In Qt, it will display the Plot, toolbar, etc. as stacked widget, use `Plot.widget` to access it. + WgpuCanvasBase + In Qt or GLFW, the canvas window containing the Figure will be shown. + In jupyter, it will display the plot in the output cell or sidecar. """ - # show was already called, return existing output context - if self._output is not None: + # show was already called, return canvas + if self._output: return self._output self.start_render() @@ -458,9 +436,6 @@ def show( if sidecar_kwargs is None: sidecar_kwargs = dict() - if add_widgets is None: - add_widgets = list() - # flip y-axis if ImageGraphics are present for subplot in self: for g in subplot.graphics: @@ -476,26 +451,23 @@ def show( _maintain_aspect = maintain_aspect subplot.auto_scale(maintain_aspect=maintain_aspect) - # return the appropriate OutputContext based on the current canvas + # parse based on canvas type if self.canvas.__class__.__name__ == "JupyterWgpuCanvas": - from .output.jupyter_output import ( - JupyterOutputContext, - ) # noqa - inline import - - self._output = JupyterOutputContext( - frame=self, - make_toolbar=toolbar, - use_sidecar=sidecar, - sidecar_kwargs=sidecar_kwargs, - add_widgets=add_widgets, - ) + if sidecar: + from sidecar import Sidecar + from IPython.display import display + + self._sidecar = Sidecar(**sidecar_kwargs) + self._output = self.canvas + with self._sidecar: + return display(self.canvas) + self._output = self.canvas + return self._output elif self.canvas.__class__.__name__ == "QWgpuCanvas": - from .output.qt_output import QOutputContext # noqa - inline import - - self._output = QOutputContext( - frame=self, make_toolbar=toolbar, add_widgets=add_widgets - ) + self._output = self.canvas + self._output.show() + return self.canvas elif self.canvas.__class__.__name__ == "WgpuManualOffscreenCanvas": # for test and docs gallery screenshots @@ -509,16 +481,30 @@ def show( # but it is necessary for the gallery images too so that's why this check is here if "RTD_BUILD" in os.environ.keys(): if os.environ["RTD_BUILD"] == "1": - subplot.viewport.render(subplot.scene, subplot.camera) + self.render() - else: # assume GLFW, the output context is just the canvas + else: # assume GLFW self._output = self.canvas - # return the output context, this call is required for jupyter but not for Qt + # return the canvas return self._output def close(self): - self.output.close() + self._output.close() + if self._sidecar: + self._sidecar.close() + + def get_pygfx_render_area(self, *args) -> tuple[int, int, int, int]: + """ + Get rect for the portion of the canvas that the pygfx renderer draws to + + Returns + ------- + tuple[int, int, int, int] + x_pos, y_pos, width, height + + """ + return 0, 0, *self.canvas.get_logical_size() def _call_animate_functions(self, funcs: list[callable]): for fn in funcs: @@ -640,6 +626,25 @@ def export(self, uri: str | Path | bytes, **kwargs): return iio.imwrite(uri, snapshot, **kwargs) + def open_popup(self, *args, **kwargs): + warn("popups only supported by ImguiFigure") + + def get_pygfx_render_area(self, *args) -> tuple[int, int, int, int]: + """ + Fet rect for the portion of the canvas that the pygfx renderer draws to, + i.e. non-imgui, part of canvas + + Returns + ------- + tuple[int, int, int, int] + x_pos, y_pos, width, height + + """ + + width, height = self.canvas.get_logical_size() + + return 0, 0, width, height + def _get_iterator(self): return product(range(self.shape[0]), range(self.shape[1])) diff --git a/fastplotlib/layouts/_imgui_figure.py b/fastplotlib/layouts/_imgui_figure.py new file mode 100644 index 000000000..3396c3d27 --- /dev/null +++ b/fastplotlib/layouts/_imgui_figure.py @@ -0,0 +1,243 @@ +from pathlib import Path +from typing import Literal, Iterable + +import numpy as np + +import imgui_bundle +from imgui_bundle import imgui, icons_fontawesome_6 as fa + +from wgpu.utils.imgui import ImguiRenderer +from wgpu.gui import WgpuCanvasBase + +import pygfx + +from ._figure import Figure +from ._utils import make_canvas_and_renderer +from ..ui import EdgeWindow, SubplotToolbar, StandardRightClickMenu, Popup, GUI_EDGES +from ..ui import ColormapPicker + + +class ImguiFigure(Figure): + def __init__( + self, + shape: tuple[int, int] = (1, 1), + cameras: ( + Literal["2d", "3d"] + | Iterable[Iterable[Literal["2d", "3d"]]] + | pygfx.PerspectiveCamera + | Iterable[Iterable[pygfx.PerspectiveCamera]] + ) = "2d", + controller_types: ( + Iterable[Iterable[Literal["panzoom", "fly", "trackball", "orbit"]]] + | Iterable[Literal["panzoom", "fly", "trackball", "orbit"]] + ) = None, + controller_ids: ( + Literal["sync"] + | Iterable[int] + | Iterable[Iterable[int]] + | Iterable[Iterable[str]] + ) = None, + controllers: pygfx.Controller | Iterable[Iterable[pygfx.Controller]] = None, + canvas: str | WgpuCanvasBase | pygfx.Texture = None, + renderer: pygfx.WgpuRenderer = None, + size: tuple[int, int] = (500, 300), + names: list | np.ndarray = None, + ): + self._guis: dict[str, EdgeWindow] = {k: None for k in GUI_EDGES} + + canvas, renderer = make_canvas_and_renderer( + canvas, renderer, canvas_kwargs={"size": size} + ) + self._imgui_renderer = ImguiRenderer(renderer.device, canvas) + + super().__init__( + shape=shape, + cameras=cameras, + controller_types=controller_types, + controller_ids=controller_ids, + controllers=controllers, + canvas=canvas, + renderer=renderer, + size=size, + names=names, + ) + + fronts_path = str( + Path(imgui_bundle.__file__).parent.joinpath( + "assets", "fonts", "Font_Awesome_6_Free-Solid-900.otf" + ) + ) + + io = imgui.get_io() + + self._fa_icons = io.fonts.add_font_from_file_ttf( + fronts_path, 16, glyph_ranges_as_int_list=[fa.ICON_MIN_FA, fa.ICON_MAX_FA] + ) + + io.fonts.build() + self.imgui_renderer.backend.create_fonts_texture() + + self.imgui_renderer.set_gui(self._draw_imgui) + + self._subplot_toolbars: np.ndarray[SubplotToolbar] = np.empty( + shape=self._subplots.shape, dtype=object + ) + + for subplot in self._subplots.ravel(): + toolbar = SubplotToolbar(subplot=subplot, fa_icons=self._fa_icons) + self._subplot_toolbars[subplot.position] = toolbar + + self._right_click_menu = StandardRightClickMenu( + figure=self, fa_icons=self._fa_icons + ) + + self._popups: dict[str, Popup] = {} + + self.register_popup(ColormapPicker) + + @property + def guis(self) -> dict[str, EdgeWindow]: + """GUI windows added to the Figure""" + return self._guis + + @property + def imgui_renderer(self) -> ImguiRenderer: + """imgui renderer""" + return self._imgui_renderer + + def render(self, draw=False): + super().render(draw) + + self.imgui_renderer.render() + self.canvas.request_draw() + + def _draw_imgui(self) -> imgui.ImDrawData: + imgui.new_frame() + + for subplot, toolbar in zip( + self._subplots.ravel(), self._subplot_toolbars.ravel() + ): + if not subplot.toolbar: + # if subplot.toolbar is False + continue + toolbar.update() + + for gui in self.guis.values(): + if gui is not None: + gui.draw_window() + + for popup in self._popups.values(): + popup.update() + + self._right_click_menu.update() + + imgui.end_frame() + + imgui.render() + + return imgui.get_draw_data() + + def add_gui(self, gui: EdgeWindow): + """ + Add a GUI to the Figure. GUIs can be added to the top, bottom, left or right edge. + + Parameters + ---------- + gui: EdgeWindow + A GUI EdgeWindow instance + + """ + if not isinstance(gui, EdgeWindow): + raise TypeError( + f"GUI must be of type: {EdgeWindow} you have passed a {type(gui)}" + ) + + location = gui.location + + if location not in GUI_EDGES: + raise ValueError( + f"GUI does not have a valid location, valid locations are: {GUI_EDGES}, you have passed: {location}" + ) + + if self.guis[location] is not None: + raise ValueError(f"GUI already exists in the desired location: {location}") + + self.guis[location] = gui + + self._reset_viewports() + + def get_pygfx_render_area(self, *args) -> tuple[int, int, int, int]: + """ + Fet rect for the portion of the canvas that the pygfx renderer draws to, + i.e. non-imgui, part of canvas + + Returns + ------- + tuple[int, int, int, int] + x_pos, y_pos, width, height + + """ + + width, height = self.canvas.get_logical_size() + + for edge in ["left", "right"]: + if self.guis[edge]: + width -= self._guis[edge].size + + for edge in ["top", "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, width, height + + def _reset_viewports(self): + # TODO: think about moving this to Figure later, + # maybe also refactor Subplot and PlotArea so that + # the resize event is handled at the Figure level instead + for subplot in self: + subplot.set_viewport_rect() + for dock in subplot.docks.values(): + dock.set_viewport_rect() + + def register_popup(self, popup: Popup.__class__): + """ + Register a popup class. Note that this takes the class, not an instance + + Parameters + ---------- + popup: Popup subclass + + """ + self._popups[popup.name] = popup(self) + + def open_popup(self, name: str, pos: tuple[int, int], **kwargs): + """ + Open a registered popup + + Parameters + ---------- + name: str + The registered name of the popup + + pos: int, int + x_pos, y_pos for the popup + + kwargs + any additional kwargs to pass to the Popup's open() method + + """ + + if self._popups[name].is_open: + return + + self._popups[name].open(pos, **kwargs) diff --git a/fastplotlib/layouts/_plot_area.py b/fastplotlib/layouts/_plot_area.py index 133526115..5dc415ad7 100644 --- a/fastplotlib/layouts/_plot_area.py +++ b/fastplotlib/layouts/_plot_area.py @@ -34,6 +34,7 @@ def __init__( scene: pygfx.Scene, canvas: WgpuCanvasBase, renderer: pygfx.WgpuRenderer, + extra_renderers: dict = None, name: str = None, ): """ @@ -122,6 +123,19 @@ def __init__( self.set_viewport_rect() + def get_figure(self, obj=None): + """Get Figure instance that contains this plot area""" + if obj is None: + obj = self + + if obj.parent.__class__.__name__.endswith("Figure"): + return obj.parent + else: + if obj.parent is None: + raise RecursionError + + return self.get_figure(obj=obj.parent) + # several read-only properties @property def parent(self): @@ -214,7 +228,7 @@ def controller(self, new_controller: str | pygfx.Controller): # TODO: monkeypatch until we figure out a better # pygfx plans on refactoring viewports anyways if self.parent is not None: - if self.parent.__class__.__name__ == "Figure": + if self.parent.__class__.__name__.endswith("Figure"): for subplot in self.parent: if subplot.camera in cameras_list: new_controller.register_events(subplot.viewport) diff --git a/fastplotlib/layouts/_subplot.py b/fastplotlib/layouts/_subplot.py index 293cea00c..9c3b174a9 100644 --- a/fastplotlib/layouts/_subplot.py +++ b/fastplotlib/layouts/_subplot.py @@ -7,25 +7,25 @@ from wgpu.gui import WgpuCanvasBase from ..graphics import TextGraphic -from ._utils import make_canvas_and_renderer, create_camera, create_controller +from ._utils import create_camera, create_controller from ._plot_area import PlotArea from ._graphic_methods_mixin import GraphicMethodsMixin from ..graphics._axes import Axes +# number of pixels taken by the imgui toolbar when present +IMGUI_TOOLBAR_HEIGHT = 39 + + class Subplot(PlotArea, GraphicMethodsMixin): def __init__( self, - parent: Union["Figure", None] = None, - position: tuple[int, int] = None, - parent_dims: tuple[int, int] = None, - camera: Literal["2d", "3d"] | pygfx.PerspectiveCamera = "2d", - controller: ( - Literal["panzoom", "fly", "trackball", "orbit"] | pygfx.Controller - ) = None, - canvas: ( - Literal["glfw", "jupyter", "qt", "wx"] | WgpuCanvasBase | pygfx.Texture - ) = None, + parent: Union["Figure"], + position: tuple[int, int], + parent_dims: tuple[int, int], + camera: Literal["2d", "3d"] | pygfx.PerspectiveCamera, + controller: pygfx.Controller, + canvas: WgpuCanvasBase | pygfx.Texture, renderer: pygfx.WgpuRenderer = None, name: str = None, ): @@ -56,12 +56,10 @@ def __init__( | if ``str``, must be one of: `"panzoom", "fly", "trackball", or "orbit"`. | also accepts a pygfx.Controller instance - canvas: one of "jupyter", "glfw", "qt", "ex, a WgpuCanvas, or a pygfx.Texture, optional - Provides surface on which a scene will be rendered. Can optionally provide a WgpuCanvas instance or a str - to force the PlotArea to use a specific canvas from one of the following options: "jupyter", "glfw", "qt". - Can also provide a pygfx Texture to render to. + canvas: WgpuCanvas, or a pygfx.Texture + Provides surface on which a scene will be rendered. - renderer: WgpuRenderer, optional + renderer: WgpuRenderer object used to render scenes using wgpu name: str, optional @@ -71,8 +69,6 @@ def __init__( super(GraphicMethodsMixin, self).__init__() - canvas, renderer = make_canvas_and_renderer(canvas, renderer) - if position is None: position = (0, 0) @@ -91,6 +87,8 @@ def __init__( self._title_graphic: TextGraphic = None + self._toolbar = True + super(Subplot, self).__init__( parent=parent, position=position, @@ -142,6 +140,16 @@ def docks(self) -> dict: """ return self._docks + @property + def toolbar(self) -> bool: + """show/hide toolbar""" + return self._toolbar + + @toolbar.setter + def toolbar(self, visible: bool): + self._toolbar = bool(visible) + self.set_viewport_rect() + def render(self): self.axes.update_using_camera() super().render() @@ -172,19 +180,44 @@ def center_title(self): self.docks["top"].center_graphic(self._title_graphic, zoom=1.5) self._title_graphic.world_object.position_y = -3.5 - def get_rect(self): - """Returns the bounding box that defines the Subplot within the canvas.""" + def get_rect(self) -> np.ndarray: + """ + Returns the bounding box that defines the Subplot within the canvas. + + Returns + ------- + np.ndarray + x_position, y_position, width, height + + """ row_ix, col_ix = self.position - width_canvas, height_canvas = self.canvas.get_logical_size() + + x_start_render, y_start_render, width_canvas_render, height_canvas_render = ( + self.parent.get_pygfx_render_area() + ) x_pos = ( - (width_canvas / self.ncols) + ((col_ix - 1) * (width_canvas / self.ncols)) - ) + self.spacing + ( + (width_canvas_render / self.ncols) + + ((col_ix - 1) * (width_canvas_render / self.ncols)) + ) + + self.spacing + + x_start_render + ) y_pos = ( - (height_canvas / self.nrows) + ((row_ix - 1) * (height_canvas / self.nrows)) - ) + self.spacing - width_subplot = (width_canvas / self.ncols) - self.spacing - height_subplot = (height_canvas / self.nrows) - self.spacing + ( + (height_canvas_render / self.nrows) + + ((row_ix - 1) * (height_canvas_render / self.nrows)) + ) + + self.spacing + + y_start_render + ) + width_subplot = (width_canvas_render / self.ncols) - self.spacing + height_subplot = (height_canvas_render / self.nrows) - self.spacing + + if self.parent.__class__.__name__ == "ImguiFigure" and self.toolbar: + # leave space for imgui toolbar + height_subplot -= IMGUI_TOOLBAR_HEIGHT rect = np.array([x_pos, y_pos, width_subplot, height_subplot]) @@ -232,73 +265,93 @@ def size(self, s: int): self.set_viewport_rect() def get_rect(self, *args): + """ + Returns the bounding box that defines this dock area within the canvas. + + Returns + ------- + np.ndarray + x_position, y_position, width, height + """ if self.size == 0: self.viewport.rect = None return row_ix_parent, col_ix_parent = self.parent.position - width_canvas, height_canvas = self.parent.renderer.logical_size + + x_start_render, y_start_render, width_render_canvas, height_render_canvas = ( + self.parent.parent.get_pygfx_render_area() + ) spacing = 2 # spacing in pixels if self.position == "right": x_pos = ( - (width_canvas / self.parent.ncols) - + ((col_ix_parent - 1) * (width_canvas / self.parent.ncols)) - + (width_canvas / self.parent.ncols) + (width_render_canvas / self.parent.ncols) + + ((col_ix_parent - 1) * (width_render_canvas / self.parent.ncols)) + + (width_render_canvas / self.parent.ncols) - self.size ) y_pos = ( - (height_canvas / self.parent.nrows) - + ((row_ix_parent - 1) * (height_canvas / self.parent.nrows)) + (height_render_canvas / self.parent.nrows) + + ((row_ix_parent - 1) * (height_render_canvas / self.parent.nrows)) ) + spacing width_viewport = self.size - height_viewport = (height_canvas / self.parent.nrows) - spacing + height_viewport = (height_render_canvas / self.parent.nrows) - spacing elif self.position == "left": - x_pos = (width_canvas / self.parent.ncols) + ( - (col_ix_parent - 1) * (width_canvas / self.parent.ncols) + x_pos = (width_render_canvas / self.parent.ncols) + ( + (col_ix_parent - 1) * (width_render_canvas / self.parent.ncols) ) y_pos = ( - (height_canvas / self.parent.nrows) - + ((row_ix_parent - 1) * (height_canvas / self.parent.nrows)) + (height_render_canvas / self.parent.nrows) + + ((row_ix_parent - 1) * (height_render_canvas / self.parent.nrows)) ) + spacing width_viewport = self.size - height_viewport = (height_canvas / self.parent.nrows) - spacing + height_viewport = (height_render_canvas / self.parent.nrows) - spacing elif self.position == "top": x_pos = ( - (width_canvas / self.parent.ncols) - + ((col_ix_parent - 1) * (width_canvas / self.parent.ncols)) + (width_render_canvas / self.parent.ncols) + + ((col_ix_parent - 1) * (width_render_canvas / self.parent.ncols)) + spacing ) y_pos = ( - (height_canvas / self.parent.nrows) - + ((row_ix_parent - 1) * (height_canvas / self.parent.nrows)) + (height_render_canvas / self.parent.nrows) + + ((row_ix_parent - 1) * (height_render_canvas / self.parent.nrows)) ) + spacing - width_viewport = (width_canvas / self.parent.ncols) - spacing + width_viewport = (width_render_canvas / self.parent.ncols) - spacing height_viewport = self.size elif self.position == "bottom": x_pos = ( - (width_canvas / self.parent.ncols) - + ((col_ix_parent - 1) * (width_canvas / self.parent.ncols)) + (width_render_canvas / self.parent.ncols) + + ((col_ix_parent - 1) * (width_render_canvas / self.parent.ncols)) + spacing ) y_pos = ( ( - (height_canvas / self.parent.nrows) - + ((row_ix_parent - 1) * (height_canvas / self.parent.nrows)) + (height_render_canvas / self.parent.nrows) + + ((row_ix_parent - 1) * (height_render_canvas / self.parent.nrows)) ) - + (height_canvas / self.parent.nrows) + + (height_render_canvas / self.parent.nrows) - self.size ) - width_viewport = (width_canvas / self.parent.ncols) - spacing + width_viewport = (width_render_canvas / self.parent.ncols) - spacing height_viewport = self.size else: raise ValueError("invalid position") - return [x_pos, y_pos, width_viewport, height_viewport] + if self.parent.__class__.__name__ == "ImguiFigure" and self.parent.toolbar: + # leave space for imgui toolbar + height_viewport -= IMGUI_TOOLBAR_HEIGHT + + return [ + x_pos + x_start_render, + y_pos + y_start_render, + width_viewport, + height_viewport, + ] def get_parent_rect_adjust(self): if self.position == "right": diff --git a/fastplotlib/layouts/_utils.py b/fastplotlib/layouts/_utils.py index 85c35532c..ea44f6950 100644 --- a/fastplotlib/layouts/_utils.py +++ b/fastplotlib/layouts/_utils.py @@ -2,13 +2,45 @@ import pygfx from pygfx import WgpuRenderer, Texture, Renderer +from pygfx.renderers.wgpu.engine.renderer import ( + EVENT_TYPE_MAP, + PointerEvent, + WheelEvent, +) + from wgpu.gui import WgpuCanvasBase from ..utils import gui +# temporary until https://github.com/pygfx/pygfx/issues/495 +class WgpuRendererWithEventFilters(WgpuRenderer): + def __init__(self, target, *args, **kwargs): + super().__init__(target, *args, **kwargs) + self._event_filters = {} + + def convert_event(self, event: dict): + event_type = event["event_type"] + + if EVENT_TYPE_MAP[event_type] in [PointerEvent, WheelEvent]: + for filt in self.event_filters.values(): + if ( + filt[0, 0] < event["x"] < filt[1, 0] + and filt[0, 1] < event["y"] < filt[1, 1] + ): + return + + super().convert_event(event) + + @property + def event_filters(self) -> dict: + return self._event_filters + + def make_canvas_and_renderer( - canvas: str | WgpuCanvasBase | Texture | None, renderer: Renderer | None + canvas: str | WgpuCanvasBase | Texture | None, + renderer: Renderer | None, + canvas_kwargs: dict, ): """ Parses arguments and returns the appropriate canvas and renderer instances @@ -16,10 +48,10 @@ def make_canvas_and_renderer( """ if canvas is None: - canvas = gui.WgpuCanvas(max_fps=60) + canvas = gui.WgpuCanvas(max_fps=60, **canvas_kwargs) elif isinstance(canvas, str): m = importlib.import_module("wgpu.gui." + canvas) - canvas = m.WgpuCanvas(max_fps=60) + canvas = m.WgpuCanvas(max_fps=60, **canvas_kwargs) elif not isinstance(canvas, (WgpuCanvasBase, Texture)): raise TypeError( f"canvas option must either be a valid WgpuCanvas implementation, a pygfx Texture" @@ -27,7 +59,7 @@ def make_canvas_and_renderer( ) if renderer is None: - renderer = WgpuRenderer(canvas) + renderer = WgpuRendererWithEventFilters(canvas) elif not isinstance(renderer, Renderer): raise TypeError( f"renderer option must be a pygfx.Renderer instance such as pygfx.WgpuRenderer" diff --git a/fastplotlib/layouts/output/__init__.py b/fastplotlib/layouts/output/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/fastplotlib/layouts/output/_ipywidget_toolbar.py b/fastplotlib/layouts/output/_ipywidget_toolbar.py deleted file mode 100644 index 787c8d442..000000000 --- a/fastplotlib/layouts/output/_ipywidget_toolbar.py +++ /dev/null @@ -1,202 +0,0 @@ -import traceback -from datetime import datetime -from itertools import product -from math import copysign -from pathlib import Path - -from ipywidgets.widgets import ( - HBox, - ToggleButton, - Dropdown, - Layout, - Button, - Image, -) - -from ...graphics.selectors import PolygonSelector -from ._toolbar import ToolBar -from ...utils import config - - -class IpywidgetToolBar(HBox, ToolBar): - """Basic toolbar using ipywidgets""" - - def __init__(self, figure): - ToolBar.__init__(self, figure) - - self._auto_scale_button = Button( - value=False, - disabled=False, - icon="expand-arrows-alt", - layout=Layout(width="auto"), - tooltip="auto-scale scene", - ) - self._center_scene_button = Button( - value=False, - disabled=False, - icon="align-center", - layout=Layout(width="auto"), - tooltip="auto-center scene", - ) - self._panzoom_controller_button = ToggleButton( - value=True, - disabled=False, - icon="hand-pointer", - layout=Layout(width="auto"), - tooltip="panzoom controller", - ) - self._maintain_aspect_button = ToggleButton( - value=True, - disabled=False, - description="1:1", - layout=Layout(width="auto"), - tooltip="maintain aspect", - ) - self._maintain_aspect_button.style.font_weight = "bold" - - self._y_direction_button = Button( - value=False, - disabled=False, - icon="arrow-up", - layout=Layout(width="auto"), - tooltip="y-axis direction", - ) - - self._record_button = ToggleButton( - value=False, - disabled=False, - icon="video", - layout=Layout(width="auto"), - tooltip="record", - ) - - self._add_polygon_button = Button( - value=False, - disabled=False, - icon="draw-polygon", - layout=Layout(width="auto"), - tooltip="add PolygonSelector", - ) - - widgets = [ - self._auto_scale_button, - self._center_scene_button, - self._panzoom_controller_button, - self._maintain_aspect_button, - self._y_direction_button, - self._add_polygon_button, - self._record_button, - ] - - if config.party_parrot: - gif_path = Path(__file__).parent.parent.parent.joinpath("assets", "egg.gif") - with open(gif_path, "rb") as f: - gif = f.read() - - image = Image( - value=gif, - format="png", - width=35, - height=25, - ) - widgets.append(image) - - positions = list( - product(range(self.figure.shape[0]), range(self.figure.shape[1])) - ) - values = list() - for pos in positions: - if self.figure[pos].name is not None: - values.append(self.figure[pos].name) - else: - values.append(str(pos)) - - self._dropdown = Dropdown( - options=values, - disabled=False, - description="Subplots:", - layout=Layout(width="200px"), - ) - - self.figure.renderer.add_event_handler(self.update_current_subplot, "click") - - widgets.append(self._dropdown) - - self._panzoom_controller_button.observe(self.panzoom_handler, "value") - self._auto_scale_button.on_click(self.auto_scale_handler) - self._center_scene_button.on_click(self.center_scene_handler) - self._maintain_aspect_button.observe(self.maintain_aspect_handler, "value") - self._y_direction_button.on_click(self.y_direction_handler) - self._add_polygon_button.on_click(self.add_polygon) - self._record_button.observe(self.record_plot, "value") - - # set initial values for some buttons - self._maintain_aspect_button.value = self.current_subplot.camera.maintain_aspect - - if copysign(1, self.current_subplot.camera.local.scale_y) == -1: - self._y_direction_button.icon = "arrow-down" - else: - self._y_direction_button.icon = "arrow-up" - - super().__init__(widgets) - - def _get_subplot_dropdown_value(self) -> str: - return self._dropdown.value - - def auto_scale_handler(self, obj): - self.current_subplot.auto_scale( - maintain_aspect=self.current_subplot.camera.maintain_aspect - ) - - def center_scene_handler(self, obj): - self.current_subplot.center_scene() - - def panzoom_handler(self, obj): - self.current_subplot.controller.enabled = self._panzoom_controller_button.value - - def maintain_aspect_handler(self, obj): - for camera in self.current_subplot.controller.cameras: - camera.maintain_aspect = self._maintain_aspect_button.value - - def y_direction_handler(self, obj): - # flip every camera under the same controller - for camera in self.current_subplot.controller.cameras: - camera.local.scale_y *= -1 - - if copysign(1, self.current_subplot.camera.local.scale_y) == -1: - self._y_direction_button.icon = "arrow-down" - else: - self._y_direction_button.icon = "arrow-up" - - def update_current_subplot(self, ev): - for subplot in self.figure: - pos = subplot.map_screen_to_world((ev.x, ev.y)) - if pos is not None: - # update self.dropdown - if subplot.name is None: - self._dropdown.value = str(subplot.position) - else: - self._dropdown.value = subplot.name - self._panzoom_controller_button.value = subplot.controller.enabled - self._maintain_aspect_button.value = subplot.camera.maintain_aspect - - if copysign(1, subplot.camera.local.scale_y) == -1: - self._y_direction_button.icon = "arrow-down" - else: - self._y_direction_button.icon = "arrow-up" - - def record_plot(self, obj): - if self._record_button.value: - try: - self.figure.recorder.start( - f"./{datetime.now().isoformat(timespec='seconds').replace(':', '_')}.mp4" - ) - except Exception: - traceback.print_exc() - self._record_button.value = False - else: - self.figure.recorder.stop() - - def add_polygon(self, obj): - ps = PolygonSelector(edge_width=3, edge_color="magenta") - self.current_subplot.add_graphic(ps, center=False) diff --git a/fastplotlib/layouts/output/_qt_toolbar.py b/fastplotlib/layouts/output/_qt_toolbar.py deleted file mode 100644 index 4334f1369..000000000 --- a/fastplotlib/layouts/output/_qt_toolbar.py +++ /dev/null @@ -1,125 +0,0 @@ -from datetime import datetime -from math import copysign -import traceback - -from ...utils.gui import QtWidgets -from ...graphics.selectors import PolygonSelector -from ._toolbar import ToolBar -from ._qtoolbar_template import Ui_QToolbar - - -class QToolbar( - ToolBar, QtWidgets.QWidget -): # inheritance order MUST be Toolbar first, QWidget second! Else breaks - """Toolbar for Qt context""" - - def __init__(self, output_context, figure): - QtWidgets.QWidget.__init__(self, parent=output_context) - ToolBar.__init__(self, figure) - - # initialize UI - self.ui = Ui_QToolbar() - self.ui.setupUi(self) - - # connect button events - self.ui.auto_scale_button.clicked.connect(self.auto_scale_handler) - self.ui.center_button.clicked.connect(self.center_scene_handler) - self.ui.panzoom_button.toggled.connect(self.panzoom_handler) - self.ui.maintain_aspect_button.toggled.connect(self.maintain_aspect_handler) - self.ui.y_direction_button.clicked.connect(self.y_direction_handler) - - # subplot labels update when a user click on subplots - subplot = self.figure[0, 0] - # set label from first subplot name - if subplot.name is not None: - name = subplot.name - else: - name = str(subplot.position) - - # here we will just use a simple label, not a dropdown like ipywidgets - # the dropdown implementation is tedious with Qt - self.ui.current_subplot = QtWidgets.QLabel(parent=self) - self.ui.current_subplot.setText(name) - self.ui.horizontalLayout.addWidget(self.ui.current_subplot) - - # update the subplot label when a subplot is clicked into - self.figure.renderer.add_event_handler(self.update_current_subplot, "click") - - self.setMaximumHeight(35) - - # set the initial values for buttons - self.ui.maintain_aspect_button.setChecked( - self.current_subplot.camera.maintain_aspect - ) - self.ui.panzoom_button.setChecked(self.current_subplot.controller.enabled) - - if copysign(1, self.current_subplot.camera.local.scale_y) == -1: - self.ui.y_direction_button.setText("v") - else: - self.ui.y_direction_button.setText("^") - - def update_current_subplot(self, ev): - """update the text label for the current subplot""" - for subplot in self.figure: - pos = subplot.map_screen_to_world((ev.x, ev.y)) - if pos is not None: - if subplot.name is not None: - name = subplot.name - else: - name = str(subplot.position) - self.ui.current_subplot.setText(name) - - # set buttons w.r.t. current subplot - self.ui.panzoom_button.setChecked(subplot.controller.enabled) - self.ui.maintain_aspect_button.setChecked( - subplot.camera.maintain_aspect - ) - - if copysign(1, subplot.camera.local.scale_y) == -1: - self.ui.y_direction_button.setText("v") - else: - self.ui.y_direction_button.setText("^") - - def _get_subplot_dropdown_value(self) -> str: - return self.ui.current_subplot.text() - - def auto_scale_handler(self, *args): - self.current_subplot.auto_scale( - maintain_aspect=self.current_subplot.camera.maintain_aspect - ) - - def center_scene_handler(self, *args): - self.current_subplot.center_scene() - - def panzoom_handler(self, value: bool): - self.current_subplot.controller.enabled = value - - def maintain_aspect_handler(self, value: bool): - for camera in self.current_subplot.controller.cameras: - camera.maintain_aspect = value - - def y_direction_handler(self, *args): - # flip every camera under the same controller - for camera in self.current_subplot.controller.cameras: - camera.local.scale_y *= -1 - - if copysign(1, self.current_subplot.camera.local.scale_y) == -1: - self.ui.y_direction_button.setText("v") - else: - self.ui.y_direction_button.setText("^") - - def record_handler(self, ev): - if self.ui.record_button.isChecked(): - try: - self.figure.record_start( - f"./{datetime.now().isoformat(timespec='seconds').replace(':', '_')}.mp4" - ) - except Exception: - traceback.print_exc() - self.ui.record_button.setChecked(False) - else: - self.figure.record_stop() - - def add_polygon(self, *args): - ps = PolygonSelector(edge_width=3, edge_color="mageneta") - self.current_subplot.add_graphic(ps, center=False) diff --git a/fastplotlib/layouts/output/_qtoolbar_template.py b/fastplotlib/layouts/output/_qtoolbar_template.py deleted file mode 100644 index d2311c595..000000000 --- a/fastplotlib/layouts/output/_qtoolbar_template.py +++ /dev/null @@ -1,61 +0,0 @@ -# Form implementation generated from reading ui file 'qtoolbar.ui' -# -# Created by: PyQt6 UI code generator 6.5.3 -# -# WARNING: Any manual changes made to this file will be lost when pyuic6 is -# run again. Do not edit this file unless you know what you are doing. - -from ...utils.gui import QtGui, QtCore, QtWidgets - - -class Ui_QToolbar(object): - def setupUi(self, QToolbar): - QToolbar.setObjectName("QToolbar") - QToolbar.resize(638, 48) - self.horizontalLayout_2 = QtWidgets.QHBoxLayout(QToolbar) - self.horizontalLayout_2.setObjectName("horizontalLayout_2") - self.horizontalLayout = QtWidgets.QHBoxLayout() - self.horizontalLayout.setObjectName("horizontalLayout") - self.auto_scale_button = QtWidgets.QPushButton(parent=QToolbar) - self.auto_scale_button.setObjectName("auto_scale_button") - self.horizontalLayout.addWidget(self.auto_scale_button) - self.center_button = QtWidgets.QPushButton(parent=QToolbar) - self.center_button.setObjectName("center_button") - self.horizontalLayout.addWidget(self.center_button) - self.panzoom_button = QtWidgets.QPushButton(parent=QToolbar) - self.panzoom_button.setCheckable(True) - self.panzoom_button.setObjectName("panzoom_button") - self.horizontalLayout.addWidget(self.panzoom_button) - self.maintain_aspect_button = QtWidgets.QPushButton(parent=QToolbar) - font = QtGui.QFont() - font.setBold(True) - font.setWeight(QtGui.QFont.Weight.Bold) - self.maintain_aspect_button.setFont(font) - self.maintain_aspect_button.setCheckable(True) - self.maintain_aspect_button.setObjectName("maintain_aspect_button") - self.horizontalLayout.addWidget(self.maintain_aspect_button) - self.y_direction_button = QtWidgets.QPushButton(parent=QToolbar) - self.y_direction_button.setObjectName("y_direction_button") - self.horizontalLayout.addWidget(self.y_direction_button) - self.add_polygon_button = QtWidgets.QPushButton(parent=QToolbar) - self.add_polygon_button.setObjectName("add_polygon_button") - self.horizontalLayout.addWidget(self.add_polygon_button) - self.record_button = QtWidgets.QPushButton(parent=QToolbar) - self.record_button.setCheckable(True) - self.record_button.setObjectName("record_button") - self.horizontalLayout.addWidget(self.record_button) - self.horizontalLayout_2.addLayout(self.horizontalLayout) - - self.retranslateUi(QToolbar) - QtCore.QMetaObject.connectSlotsByName(QToolbar) - - def retranslateUi(self, QToolbar): - _translate = QtCore.QCoreApplication.translate - QToolbar.setWindowTitle(_translate("QToolbar", "Form")) - self.auto_scale_button.setText(_translate("QToolbar", "autoscale")) - self.center_button.setText(_translate("QToolbar", "center")) - self.panzoom_button.setText(_translate("QToolbar", "panzoom")) - self.maintain_aspect_button.setText(_translate("QToolbar", "1:1")) - self.y_direction_button.setText(_translate("QToolbar", "^")) - self.add_polygon_button.setText(_translate("QToolbar", "polygon")) - self.record_button.setText(_translate("QToolbar", "record")) diff --git a/fastplotlib/layouts/output/_toolbar.py b/fastplotlib/layouts/output/_toolbar.py deleted file mode 100644 index 5edd201fa..000000000 --- a/fastplotlib/layouts/output/_toolbar.py +++ /dev/null @@ -1,45 +0,0 @@ -from .._subplot import Subplot - - -class ToolBar: - def __init__(self, figure): - self.figure = figure - - def _get_subplot_dropdown_value(self) -> str: - raise NotImplemented - - @property - def current_subplot(self) -> Subplot: - """Returns current subplot""" - if hasattr(self.figure, "_subplots"): - # parses dropdown or label value as plot name or position - current = self._get_subplot_dropdown_value() - if current[0] == "(": - # str representation of int tuple to tuple of int - current = tuple(int(i) for i in current.strip("()").split(",")) - return self.figure[current] - else: - return self.figure[current] - else: - return self.figure - - def panzoom_handler(self, ev): - raise NotImplemented - - def maintain_aspect_handler(self, ev): - raise NotImplemented - - def y_direction_handler(self, ev): - raise NotImplemented - - def auto_scale_handler(self, ev): - raise NotImplemented - - def center_scene_handler(self, ev): - raise NotImplemented - - def record_handler(self, ev): - raise NotImplemented - - def add_polygon(self, ev): - raise NotImplemented diff --git a/fastplotlib/layouts/output/jupyter_output.py b/fastplotlib/layouts/output/jupyter_output.py deleted file mode 100644 index 9ebf0941d..000000000 --- a/fastplotlib/layouts/output/jupyter_output.py +++ /dev/null @@ -1,83 +0,0 @@ -from ipywidgets import VBox, Widget -from sidecar import Sidecar -from IPython.display import display - -from ._ipywidget_toolbar import IpywidgetToolBar - - -class JupyterOutputContext(VBox): - """ - Output context to display plots in jupyter. Inherits from ipywidgets.VBox - - Basically vstacks plot canvas, toolbar, and other widgets. Uses sidecar if desired. - """ - - def __init__( - self, - frame, - make_toolbar: bool, - use_sidecar: bool, - sidecar_kwargs: dict, - add_widgets: list[Widget], - ): - """ - - Parameters - ---------- - frame: - Plot frame for which to generate the output context - - sidecar_kwargs: dict - optional kwargs passed to Sidecar - - add_widgets: List[Widget] - list of ipywidgets to stack below the plot and toolbar - """ - self.frame = frame - self.toolbar = None - self.sidecar = None - - # verify they are all valid ipywidgets - if False in [isinstance(w, Widget) for w in add_widgets]: - raise TypeError( - f"add_widgets must be list of ipywidgets, you have passed:\n{add_widgets}" - ) - - self.use_sidecar = use_sidecar - - if not make_toolbar: # just stack canvas and the additional widgets, if any - self.output = (frame.canvas, *add_widgets) - - if make_toolbar: # make toolbar and stack canvas, toolbar, add_widgets - self.toolbar = IpywidgetToolBar(frame) - self.output = (frame.canvas, self.toolbar, *add_widgets) - - if use_sidecar: # instantiate sidecar if desired - self.sidecar = Sidecar(**sidecar_kwargs) - - # stack all of these in the VBox - super().__init__(self.output) - - def _repr_mimebundle_(self, *args, **kwargs): - """ - This is what jupyter hook into when this output context instance is returned at the end of a cell. - """ - if self.use_sidecar: - with self.sidecar: - # TODO: prints all the child widgets in the cell output, will figure out later, sidecar output works - return display(VBox(self.output)) - else: - # just display VBox contents in cell output - return super()._repr_mimebundle_(*args, **kwargs) - - def close(self): - """Closes the output context, cleanup all the stuff""" - self.frame.canvas.close() - - if self.toolbar is not None: - self.toolbar.close() - - if self.sidecar is not None: - self.sidecar.close() - - super().close() # ipywidget VBox cleanup diff --git a/fastplotlib/layouts/output/qt_output.py b/fastplotlib/layouts/output/qt_output.py deleted file mode 100644 index 20aaef2d1..000000000 --- a/fastplotlib/layouts/output/qt_output.py +++ /dev/null @@ -1,57 +0,0 @@ -from ...utils.gui import QtWidgets -from ._qt_toolbar import QToolbar - - -class QOutputContext(QtWidgets.QWidget): - """ - Output context to display plots in Qt apps. Inherits from QtWidgets.QWidget - - Basically vstacks plot canvas, toolbar, and other widgets. - """ - - def __init__( - self, - frame, - make_toolbar, - add_widgets, - ): - """ - - Parameters - ---------- - frame: - Plot frame for which to generate the output context - - add_widgets: List[Widget] - list of QWidget to stack below the plot and toolbar - """ - # no parent, user can use Plot.widget.setParent(parent) if necessary to embed into other widgets - QtWidgets.QWidget.__init__(self, parent=None) - self.frame = frame - self.toolbar = None - - # vertical layout used to stack plot canvas, toolbar, and add_widgets - self.vlayout = QtWidgets.QVBoxLayout(self) - - # add canvas to layout - self.vlayout.addWidget(self.frame.canvas) - - if make_toolbar: # make toolbar and add to layout - self.toolbar = QToolbar(output_context=self, figure=frame) - self.vlayout.addWidget(self.toolbar) - - for w in add_widgets: # add any additional widgets to layout - w.setParent(self) - self.vlayout.addWidget(w) - - self.setLayout(self.vlayout) - - self.resize(*self.frame._starting_size) - - self.show() - - def close(self): - """Cleanup and close the output context""" - self.frame.canvas.close() - self.toolbar.close() - super().close() # QWidget cleanup diff --git a/fastplotlib/layouts/output/qtoolbar.ui b/fastplotlib/layouts/output/qtoolbar.ui deleted file mode 100644 index 6c9aadae8..000000000 --- a/fastplotlib/layouts/output/qtoolbar.ui +++ /dev/null @@ -1,89 +0,0 @@ - - - QToolbar - - - - 0 - 0 - 638 - 48 - - - - Form - - - - - - - - autoscale - - - - - - - center - - - - - - - panzoom - - - true - - - - - - - - 75 - true - - - - 1:1 - - - true - - - - - - - ^ - - - - - - - polygon - - - - - - - record - - - true - - - - - - - - - - diff --git a/fastplotlib/tools/__init__.py b/fastplotlib/tools/__init__.py new file mode 100644 index 000000000..80396c98d --- /dev/null +++ b/fastplotlib/tools/__init__.py @@ -0,0 +1 @@ +from ._histogram_lut import HistogramLUTTool diff --git a/fastplotlib/widgets/histogram_lut.py b/fastplotlib/tools/_histogram_lut.py similarity index 95% rename from fastplotlib/widgets/histogram_lut.py rename to fastplotlib/tools/_histogram_lut.py index 0f63eb8f4..b8c6633a8 100644 --- a/fastplotlib/widgets/histogram_lut.py +++ b/fastplotlib/tools/_histogram_lut.py @@ -24,7 +24,7 @@ def _get_image_graphic_events(image_graphic: ImageGraphic) -> list[str]: # TODO: This is a widget, we can think about a BaseWidget class later if necessary -class HistogramLUT(Graphic): +class HistogramLUTTool(Graphic): def __init__( self, data: np.ndarray, @@ -136,6 +136,7 @@ def __init__( # colorbar for grayscale images if self.image_graphic.data.value.ndim != 3: self._colorbar: ImageGraphic = self._make_colorbar(edges_flanked) + self._colorbar.add_event_handler(self._open_cmap_picker, "click") self.world_object.add(self._colorbar.world_object) else: @@ -349,10 +350,12 @@ def set_data(self, data, reset_vmin_vmax: bool = True): self._data = weakref.proxy(data) if self._colorbar is not None: + self._colorbar.clear_event_handlers() self.world_object.remove(self._colorbar.world_object) if self.image_graphic.data.value.ndim != 3: self._colorbar: ImageGraphic = self._make_colorbar(edges_flanked) + self._colorbar.add_event_handler(self._open_cmap_picker, "click") self.world_object.add(self._colorbar.world_object) else: @@ -370,7 +373,7 @@ def image_graphic(self) -> ImageGraphic: def image_graphic(self, graphic): if not isinstance(graphic, ImageGraphic): raise TypeError( - f"HistogramLUT can only use ImageGraphic types, you have passed: {type(graphic)}" + f"HistogramLUTTool can only use ImageGraphic types, you have passed: {type(graphic)}" ) if self._image_graphic is not None: @@ -392,6 +395,15 @@ def disconnect_image_graphic(self): del self._image_graphic # self._image_graphic = None + def _open_cmap_picker(self, ev): + # check if right click + if ev.button != 2: + return + + pos = ev.x, ev.y + + self._plot_area.get_figure().open_popup("colormap-picker", pos, lut_tool=self) + def _fpl_prepare_del(self): self._linear_region_selector._fpl_prepare_del() self._histogram_line._fpl_prepare_del() diff --git a/fastplotlib/ui/__init__.py b/fastplotlib/ui/__init__.py new file mode 100644 index 000000000..a1e57a9c5 --- /dev/null +++ b/fastplotlib/ui/__init__.py @@ -0,0 +1,3 @@ +from ._base import BaseGUI, Window, EdgeWindow, Popup, GUI_EDGES +from ._subplot_toolbar import SubplotToolbar +from .right_click_menus import StandardRightClickMenu, ColormapPicker diff --git a/fastplotlib/ui/_base.py b/fastplotlib/ui/_base.py new file mode 100644 index 000000000..4ca9fbeca --- /dev/null +++ b/fastplotlib/ui/_base.py @@ -0,0 +1,282 @@ +from typing import Literal +import numpy as np + +from imgui_bundle import imgui + +from ..layouts._figure import Figure + + +GUI_EDGES = ["top", "right", "bottom", "left"] + + +class BaseGUI: + """ + Base class for all ImGUI based GUIs, windows and popups + + The main purpose of this base is for setting a unique ID between multiple figs with identical UI elements + + This ID can be pushed in subclasses within the `update()` method + """ + + ID_COUNTER: int = 0 + + def __init__(self): + BaseGUI.ID_COUNTER += 1 + self._id_counter = BaseGUI.ID_COUNTER + + def update(self): + """must be implemented in subclass""" + raise NotImplementedError + + +class Window(BaseGUI): + """Base class for imgui windows drawn within Figures""" + + pass + + +class EdgeWindow(Window): + def __init__( + self, + figure: Figure, + size: int, + location: Literal["top", "bottom", "left", "right"], + title: str, + window_flags: int = imgui.WindowFlags_.no_collapse + | imgui.WindowFlags_.no_resize, + *args, + **kwargs, + ): + """ + A base class for imgui windows displayed at one of the four edges of a Figure + + Parameters + ---------- + figure: Figure + Figure instance that this window will be placed in + + size: int + width or height of the window, depending on its location + + location: str, "top" | "bottom" | "left" | "right" + location of the window + + title: str + window title + + window_flags: int + window flag enum, valid flags are: + + .. code-block:: py + + imgui.WindowFlags_.no_title_bar + imgui.WindowFlags_.no_resize + imgui.WindowFlags_.no_move + imgui.WindowFlags_.no_scrollbar + imgui.WindowFlags_.no_scroll_with_mouse + imgui.WindowFlags_.no_collapse + imgui.WindowFlags_.always_auto_resize + imgui.WindowFlags_.no_background + imgui.WindowFlags_.no_saved_settings + imgui.WindowFlags_.no_mouse_inputs + imgui.WindowFlags_.menu_bar + imgui.WindowFlags_.horizontal_scrollbar + imgui.WindowFlags_.no_focus_on_appearing + imgui.WindowFlags_.no_bring_to_front_on_focus + imgui.WindowFlags_.always_vertical_scrollbar + imgui.WindowFlags_.always_horizontal_scrollbar + imgui.WindowFlags_.no_nav_inputs + imgui.WindowFlags_.no_nav_focus + imgui.WindowFlags_.unsaved_document + imgui.WindowFlags_.no_docking + imgui.WindowFlags_.no_nav + imgui.WindowFlags_.no_decoration + imgui.WindowFlags_.no_inputs + + *args + additional args for the GUI + + **kwargs + additional kwargs for teh GUI + """ + super().__init__() + + if location not in GUI_EDGES: + f"GUI does not have a valid location, valid locations are: {GUI_EDGES}, you have passed: {location}" + + self._figure = figure + self._size = size + self._location = location + self._title = title + self._window_flags = window_flags + self._fa_icons = self._figure._fa_icons + + self._x, self._y, self._width, self._height = self.get_rect() + + self._figure.canvas.add_event_handler(self._set_rect, "resize") + + @property + def size(self) -> int | None: + """width or height of the edge window""" + return self._size + + @size.setter + def size(self, value): + if not isinstance(value, int): + raise TypeError + self._size = value + + @property + def location(self) -> str: + """location of the window""" + return self._location + + @property + def x(self) -> int: + """canvas x position of the window""" + return self._x + + @property + def y(self) -> int: + """canvas y position of the window""" + return self._y + + @property + def width(self) -> int: + """with the window""" + return self._width + + @property + def height(self) -> int: + """height of the window""" + return self._height + + def _set_rect(self, *args): + self._x, self._y, self._width, self._height = self.get_rect() + + def get_rect(self) -> tuple[int, int, int, int]: + """ + Compute the rect that defines the area this GUI is drawn to + + Returns + ------- + int, int, int, int + x_pos, y_pos, width, height + + """ + + 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 + width, height = (width_canvas, self.size) + + 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 + + return x_pos, y_pos, width, height + + def draw_window(self): + """helps simplify using imgui by managing window creation & position, and pushing/popping the ID""" + # window position & size + imgui.set_next_window_size((self.width, self.height)) + imgui.set_next_window_pos((self.x, self.y)) + flags = self._window_flags + + # begin window + imgui.begin(self._title, p_open=None, flags=flags) + + # push ID to prevent conflict between multiple figs with same UI + imgui.push_id(self._id_counter) + + # draw stuff from subclass into window + self.update() + + # pop ID + imgui.pop_id() + + # end the window + imgui.end() + + def update(self): + """Implement your GUI here and it will be drawn within the window. See the GUI examples""" + raise NotImplementedError + + +class Popup(BaseGUI): + def __init__(self, figure: Figure, *args, **kwargs): + """ + Base class for creating ImGUI popups within Figures + + Parameters + ---------- + figure: Figure + Figure instance + *args + any args to pass to subclass constructor + + **kwargs + any kwargs to pass to subclass constructor + """ + + super().__init__() + + self._figure = figure + self._fa_icons = self._figure._fa_icons + + self._event_filter_names = set() + + self.is_open = False + + def set_event_filter(self, name: str): + """Filter out events under the popup from being handled by pygfx renderer""" + # get popup window position & size + x1, y1 = imgui.get_window_pos() + width, height = imgui.get_window_size() + x2, y2 = x1 + width, y1 + height + + # add or modify event filter + if name not in self._figure.renderer.event_filters.keys(): + self._figure.renderer.event_filters[name] = np.array( + [[x1 - 1, y1 - 1], [x2 + 4, y2 + 4]] + ) + else: + self._figure.renderer.event_filters[name][:] = [x1 - 1, y1 - 1], [ + x2 + 4, + y2 + 4, + ] + + self._event_filter_names.add(name) + + def clear_event_filters(self): + """clear event filters when the popup is not shown""" + for name in self._event_filter_names: + self._figure.renderer.event_filters[name][:] = [-1, -1], [-1, -1] + + def open(self, pos: tuple[int, int], *args, **kwargs): + """implement in subclass""" + raise NotImplementedError diff --git a/fastplotlib/ui/_subplot_toolbar.py b/fastplotlib/ui/_subplot_toolbar.py new file mode 100644 index 000000000..6c1a81f73 --- /dev/null +++ b/fastplotlib/ui/_subplot_toolbar.py @@ -0,0 +1,73 @@ +from imgui_bundle import imgui, icons_fontawesome_6 as fa, imgui_ctx + +from ..layouts._subplot import Subplot +from ._base import Window + + +class SubplotToolbar(Window): + def __init__(self, subplot: Subplot, fa_icons: imgui.ImFont): + """ + Subplot toolbar shown below all subplots + """ + super().__init__() + + self._subplot = subplot + self._fa_icons = fa_icons + + def update(self): + # get subplot rect + x, y, width, height = self._subplot.get_rect() + + # place the toolbar window below the subplot + pos = (x, y + height) + + imgui.set_next_window_size((width, 0)) + imgui.set_next_window_pos(pos) + flags = imgui.WindowFlags_.no_collapse | imgui.WindowFlags_.no_title_bar + + imgui.begin(f"Toolbar-{self._subplot.position}", p_open=None, flags=flags) + + # icons for buttons + imgui.push_font(self._fa_icons) + + # push ID to prevent conflict between multiple figs with same UI + imgui.push_id(self._id_counter) + with imgui_ctx.begin_horizontal(f"toolbar-{self._subplot.position}"): + # autoscale button + if imgui.button(fa.ICON_FA_MAXIMIZE): + self._subplot.auto_scale() + imgui.pop_font() + if imgui.is_item_hovered(0): + imgui.set_tooltip("autoscale scene") + + # center scene + imgui.push_font(self._fa_icons) + if imgui.button(fa.ICON_FA_ALIGN_CENTER): + self._subplot.center_scene() + imgui.pop_font() + if imgui.is_item_hovered(0): + imgui.set_tooltip("center scene") + + imgui.push_font(self._fa_icons) + # checkbox controller + _, self._subplot.controller.enabled = imgui.checkbox( + fa.ICON_FA_COMPUTER_MOUSE, self._subplot.controller.enabled + ) + imgui.pop_font() + if imgui.is_item_hovered(0): + imgui.set_tooltip("enable/disable controller") + + imgui.push_font(self._fa_icons) + # checkbox maintain_apsect + _, self._subplot.camera.maintain_aspect = imgui.checkbox( + fa.ICON_FA_EXPAND, self._subplot.camera.maintain_aspect + ) + imgui.pop_font() + if imgui.is_item_hovered(0): + imgui.set_tooltip("maintain aspect") + + # pop id when all UI has been written to window + imgui.pop_id() + + # end window + imgui.end() diff --git a/fastplotlib/ui/right_click_menus/__init__.py b/fastplotlib/ui/right_click_menus/__init__.py new file mode 100644 index 000000000..6ccc50646 --- /dev/null +++ b/fastplotlib/ui/right_click_menus/__init__.py @@ -0,0 +1,2 @@ +from ._colormap_picker import ColormapPicker +from ._standard_menu import StandardRightClickMenu diff --git a/fastplotlib/ui/right_click_menus/_colormap_picker.py b/fastplotlib/ui/right_click_menus/_colormap_picker.py new file mode 100644 index 000000000..5a14705c7 --- /dev/null +++ b/fastplotlib/ui/right_click_menus/_colormap_picker.py @@ -0,0 +1,180 @@ +import ctypes + +import numpy as np +import cmap + +import wgpu +from imgui_bundle import imgui +from wgpu import GPUTexture + +from .. import Popup +from ...utils.functions import ( + COLORMAP_NAMES, + SEQUENTIAL_CMAPS, + CYCLIC_CMAPS, + DIVERGING_CMAPS, + MISC_CMAPS, +) + +all_cmaps = [*SEQUENTIAL_CMAPS, *CYCLIC_CMAPS, *DIVERGING_CMAPS, *MISC_CMAPS] + + +class ColormapPicker(Popup): + """Colormap picker menu popup tool""" + + # name used to trigger this popup after it has been registered with a Figure + name = "colormap-picker" + + def __init__(self, figure): + super().__init__(figure=figure, fa_icons=None) + + self.renderer = self._figure.renderer + self.imgui_renderer = self._figure.imgui_renderer + + # maps str cmap names -> int texture IDs + self._texture_ids: dict[str, int] = {} + self._textures = list() + + # make all colormaps and upload representative texture for each cmap to the GPU + for name in all_cmaps: + # get data that represents cmap + colormap = cmap.Colormap(name) + data = colormap(np.linspace(0, 1)) * 255 + + # needs to be 2D to create a texture + data = np.vstack([[data]] * 2).astype(np.uint8) + + # upload the texture to the GPU, get the texture ID and texture + self._texture_ids[name], texture = self._create_texture_and_upload(data) + self._textures.append(texture) + + # used to set the states of the UI + self._lut_tool = None + self._pos: tuple[int, int] = -1, -1 + self._open_new: bool = False + + self.is_open = False + + self._popup_state = "never-opened" + + self._texture_height = None + + def _create_texture_and_upload(self, data: np.ndarray) -> tuple[int, GPUTexture]: + """crates a GPUTexture from the 2D data and uploads it""" + + # create a GPUTexture + texture = self.renderer.device.create_texture( + size=(data.shape[1], data.shape[0], 4), + usage=wgpu.TextureUsage.COPY_DST | wgpu.TextureUsage.TEXTURE_BINDING, + dimension=wgpu.TextureDimension.d2, + format=wgpu.TextureFormat.rgba8unorm, + mip_level_count=1, + sample_count=1, + ) + + # upload to the GPU + self.renderer.device.queue.write_texture( + {"texture": texture, "mip_level": 0, "origin": (0, 0, 0)}, + data, + {"offset": 0, "bytes_per_row": data.shape[1] * 4}, + (data.shape[1], data.shape[0], 1), + ) + + # get a view + texture_view = texture.create_view() + + # get the id so that imgui can display it + id_texture = ctypes.c_int32(id(texture_view)).value + # add texture view to the backend so that it can be retrieved for rendering + self.imgui_renderer.backend._texture_views[id_texture] = texture_view + + return id_texture, texture + + def open(self, pos: tuple[int, int], lut_tool): + """ + Request that the popup be opened on the next render cycle + + Parameters + ---------- + pos: int, int + (x, y) position + + lut_tool: HistogramLUTTool + instance of the LUT tool + + Returns + ------- + + """ + self._lut_tool = lut_tool + + self._pos = pos + + self._open_new = True + + def close(self): + """cleanup after popup has closed""" + self._lut_tool = None + self._open_new = False + self._pos = -1, -1 + + self.is_open = False + + self.clear_event_filters() + + def _add_cmap_menu_item(self, cmap_name: str): + texture_id = self._texture_ids[cmap_name] + imgui.image( + texture_id, image_size=(50, self._texture_height), border_col=(1, 1, 1, 1) + ) + + imgui.same_line() + + clicked, selected = imgui.selectable( + label=cmap_name, + p_selected=cmap_name == self._lut_tool.cmap, + ) + + if clicked and selected: + self._lut_tool.cmap = cmap_name + + def update(self): + if self._open_new: + # new popup has been triggered by a LUT tool + self._open_new = False + + imgui.set_next_window_pos(self._pos) + imgui.open_popup("cmap-picker") + + if imgui.begin_popup("cmap-picker"): + self.is_open = True + + # event filter so click events in the menu aren't propagated down to pygfx + self.set_event_filter("cmap-picker-filter") + + # make the cmap image height the same as the text height + self._texture_height = ( + self.imgui_renderer.backend.io.font_global_scale + * imgui.get_font().font_size + ) - 2 + + if imgui.menu_item("Reset vmin-vmax", None, False)[0]: + self._lut_tool.image_graphic.reset_vmin_vmax() + + # add all the cmap options + for cmap_type in COLORMAP_NAMES.keys(): + if cmap_type == "qualitative": + continue + + imgui.separator() + imgui.text(cmap_type.capitalize()) + + for cmap_name in COLORMAP_NAMES[cmap_type]: + self._add_cmap_menu_item(cmap_name) + + imgui.end_popup() + + else: + # popup went from open to closed + if self.is_open == True: + self.close() diff --git a/fastplotlib/ui/right_click_menus/_standard_menu.py b/fastplotlib/ui/right_click_menus/_standard_menu.py new file mode 100644 index 000000000..71e8df632 --- /dev/null +++ b/fastplotlib/ui/right_click_menus/_standard_menu.py @@ -0,0 +1,181 @@ +from imgui_bundle import imgui + +from ...layouts._utils import controller_types +from ...layouts._plot_area import PlotArea +from ...ui import Popup + + +def flip_axis(subplot: PlotArea, axis: str, flip: bool): + camera = subplot.camera + axis_attr = f"scale_{axis}" + scale = getattr(camera.local, axis_attr) + + if flip and scale > 0: + # flip is checked and axis is not already flipped + setattr(camera.local, axis_attr, scale * -1) + + elif not flip and scale < 0: + # flip not checked and axis is flipped + setattr(camera.local, axis_attr, scale * -1) + + +class StandardRightClickMenu(Popup): + """Right click menu that is shown on subplots""" + + def __init__(self, figure, fa_icons): + super().__init__(figure=figure, fa_icons=fa_icons) + + self._last_right_click_pos = None + self._mouse_down: bool = False + + # whether the right click menu is currently open or not + self.is_open: bool = False + + def get_subplot(self) -> PlotArea | bool: + """get the subplot that a click occurred in""" + if self._last_right_click_pos is None: + return False + + for subplot in self._figure: + if subplot.viewport.is_inside(*self._last_right_click_pos): + return subplot + + def cleanup(self): + """called when the popup disappears""" + self.clear_event_filters() + self.is_open = False + + def update(self): + if imgui.is_mouse_down(1) and not self._mouse_down: + # mouse button was pressed down, store this position + self._mouse_down = True + self._last_right_click_pos = imgui.get_mouse_pos() + + if imgui.is_mouse_released(1) and self._mouse_down: + self._mouse_down = False + + # open popup only if mouse was not moved between mouse_down and mouse_up events + if self._last_right_click_pos == imgui.get_mouse_pos(): + if self.get_subplot(): + # open only if right click was inside a subplot + imgui.open_popup(f"right-click-menu") + + # TODO: call this just once when going from open -> closed state + if not imgui.is_popup_open("right-click-menu"): + self.cleanup() + + if imgui.begin_popup(f"right-click-menu"): + # set event filter so event in the popup region are not handled by pygfx.WgpuRenderer + self.set_event_filter("right-click-menu") + + if not self.get_subplot(): + # for some reason it will still trigger at certain locations + # despite open_popup() only being called when an actual + # subplot is returned + imgui.end_popup() + imgui.close_current_popup() + self.cleanup() + return + + name = self.get_subplot().name + if name is None: + name = self.get_subplot().position + + # text label at the top of the menu + imgui.text(f"subplot: {name}") + imgui.separator() + + # autoscale, center, maintain aspect + if imgui.menu_item(f"Autoscale", None, False)[0]: + self.get_subplot().auto_scale() + + if imgui.menu_item(f"Center", None, False)[0]: + self.get_subplot().center_scene() + + _, maintain_aspect = imgui.menu_item( + "Maintain Aspect", None, self.get_subplot().camera.maintain_aspect + ) + self.get_subplot().camera.maintain_aspect = maintain_aspect + + imgui.separator() + + # toggles to flip axes cameras + for axis in ["x", "y", "z"]: + scale = getattr(self.get_subplot().camera.local, f"scale_{axis}") + changed, flip = imgui.menu_item(f"Flip {axis} axis", None, scale < 0) + + if changed: + flip_axis(self.get_subplot(), axis, flip) + + imgui.separator() + + # toggles to show/hide the grid + for plane in ["xy", "xz", "yz"]: + grid = getattr(self.get_subplot().axes.grids, plane) + visible = grid.visible + changed, new_visible = imgui.menu_item(f"Grid {plane}", None, visible) + + if changed: + grid.visible = new_visible + + imgui.separator() + + # camera FOV + changed, fov = imgui.slider_float( + "FOV", v=self.get_subplot().camera.fov, v_min=0.0, v_max=180.0 + ) + + imgui.separator() + + if changed: + # FOV between 0 and 1 is numerically unstable + if 0 < fov < 1: + fov = 1 + + # need to update FOV via controller, if FOV is directly set + # on the camera the controller will immediately set it back + self.get_subplot().controller.update_fov( + fov - self.get_subplot().camera.fov, + animate=False, + ) + + imgui.separator() + + # controller options + if imgui.begin_menu("Controller"): + self.set_event_filter("controller-menu") + _, enabled = imgui.menu_item( + "Enabled", None, self.get_subplot().controller.enabled + ) + + self.get_subplot().controller.enabled = enabled + + changed, damping = imgui.slider_float( + "Damping", + v=self.get_subplot().controller.damping, + v_min=0.0, + v_max=10.0, + ) + + if changed: + self.get_subplot().controller.damping = damping + + imgui.separator() + imgui.text("Controller type:") + # switching between different controllers + for name, controller_type_iter in controller_types.items(): + current_type = type(self.get_subplot().controller) + + clicked, _ = imgui.menu_item( + label=name, + shortcut=None, + p_selected=current_type is controller_type_iter, + ) + + if clicked and (current_type is not controller_type_iter): + # menu item was clicked and the desired controller isn't the current one + self.get_subplot().controller = name + + imgui.end_menu() + + imgui.end_popup() diff --git a/fastplotlib/utils/functions.py b/fastplotlib/utils/functions.py index 64f9a94c3..d93f09da3 100644 --- a/fastplotlib/utils/functions.py +++ b/fastplotlib/utils/functions.py @@ -2,11 +2,54 @@ from typing import * import numpy as np -from cmap import Colormap +import cmap as cmap_lib from pygfx import Texture, Color +cmap_catalog = cmap_lib.Catalog() + +COLORMAPS = cmap_catalog.short_keys() + +SEQUENTIAL_CMAPS = list() +QUALITATIVE_CMAPS = list() +CYCLIC_CMAPS = list() +DIVERGING_CMAPS = list() +MISC_CMAPS = list() + + +for name in COLORMAPS: + _colormap = cmap_lib.Colormap(name) + match _colormap.category: + case "sequential": + if _colormap.interpolation == "nearest": + continue + SEQUENTIAL_CMAPS.append(name) + case "cyclic": + if _colormap.interpolation == "nearest": + continue + CYCLIC_CMAPS.append(name) + case "diverging": + if _colormap.interpolation == "nearest": + continue + DIVERGING_CMAPS.append(name) + case "qualitative": + QUALITATIVE_CMAPS.append(name) + case "miscellaneous": + if _colormap.interpolation == "nearest": + continue + MISC_CMAPS.append(name) + + +COLORMAP_NAMES = { + "sequential": sorted(SEQUENTIAL_CMAPS), + "cyclic": sorted(CYCLIC_CMAPS), + "diverging": sorted(DIVERGING_CMAPS), + "qualitative": sorted(QUALITATIVE_CMAPS), + "miscellaneous": sorted(MISC_CMAPS), +} + + def get_cmap(name: str, alpha: float = 1.0, gamma: float = 1.0) -> np.ndarray: """ Get a colormap as numpy array @@ -26,7 +69,8 @@ def get_cmap(name: str, alpha: float = 1.0, gamma: float = 1.0) -> np.ndarray: [n_colors, 4], i.e. [n_colors, RGBA] """ - cmap = Colormap(name).lut(256, gamma=gamma) + + cmap = cmap_lib.Colormap(name).lut(256, gamma=gamma) cmap[:, -1] = alpha return cmap.astype(np.float32) @@ -53,7 +97,8 @@ def make_colors(n_colors: int, cmap: str, alpha: float = 1.0) -> np.ndarray: shape is [n_colors, 4], where the last dimension is RGBA """ - cm = Colormap(cmap) + + cm = cmap_lib.Colormap(cmap) # can also use cm.category == "qualitative", but checking for non-interpolated # colormaps is a bit more general. (and not all "custom" colormaps will be @@ -63,7 +108,7 @@ def make_colors(n_colors: int, cmap: str, alpha: float = 1.0) -> np.ndarray: if n_colors > max_colors: raise ValueError( f"You have requested <{n_colors}> colors but only <{max_colors}> exist for the " - f"chosen cmap: <{name}>" + f"chosen cmap: <{cmap}>" ) return np.asarray(cm.color_stops, dtype=np.float32)[:n_colors, 1:] @@ -72,7 +117,7 @@ def make_colors(n_colors: int, cmap: str, alpha: float = 1.0) -> np.ndarray: def get_cmap_texture(name: str, alpha: float = 1.0) -> Texture: - return Colormap(name).to_pygfx() + return cmap_lib.Colormap(name).to_pygfx() def make_colors_dict(labels: Sequence, cmap: str, **kwargs) -> OrderedDict: @@ -247,7 +292,8 @@ def parse_cmap_values( n_colors = colormap.shape[0] - 1 # can also use cm.category == "qualitative" - if Colormap(cmap_name).interpolation == "nearest": + if cmap_lib.Colormap(cmap_name).interpolation == "nearest": + # check that cmap_values are and within the number of colors `n_colors` # do not scale, use directly diff --git a/fastplotlib/widgets/__init__.py b/fastplotlib/widgets/__init__.py index 30a68d672..766620ea6 100644 --- a/fastplotlib/widgets/__init__.py +++ b/fastplotlib/widgets/__init__.py @@ -1,3 +1,3 @@ -from .image import ImageWidget +from .image_widget import ImageWidget __all__ = ["ImageWidget"] diff --git a/fastplotlib/widgets/_image_widget_ipywidget_toolbar.py b/fastplotlib/widgets/_image_widget_ipywidget_toolbar.py deleted file mode 100644 index 24f7a6279..000000000 --- a/fastplotlib/widgets/_image_widget_ipywidget_toolbar.py +++ /dev/null @@ -1,135 +0,0 @@ -from functools import partial - -from ipywidgets import ( - VBox, - Button, - Layout, - IntSlider, - BoundedIntText, - Play, - jslink, - HBox, -) - - -class IpywidgetImageWidgetToolbar(VBox): - def __init__(self, iw): - """ - Basic toolbar for a ImageWidget instance. - - Parameters - ---------- - plot: - """ - self.iw = iw - - self.reset_vminvmax_button = Button( - value=False, - disabled=False, - icon="adjust", - layout=Layout(width="auto"), - tooltip="reset vmin/vmax", - ) - - self.reset_vminvmax_hlut_button = Button( - value=False, - icon="adjust", - description="reset", - layout=Layout(width="auto"), - tooltip="reset vmin/vmax and reset histogram using current frame", - ) - - self.sliders: dict[str, IntSlider] = dict() - - # only for xy data, no time point slider needed - if self.iw.ndim == 2: - widgets = [self.reset_vminvmax_button] - # for txy, tzxy, etc. data - else: - for dim in self.iw.slider_dims: - slider = IntSlider( - min=0, - max=self.iw._dims_max_bounds[dim] - 1, - step=1, - value=0, - description=f"dimension: {dim}", - orientation="horizontal", - ) - - slider.observe( - partial(self.iw._slider_value_changed, dim), names="value" - ) - - self.sliders[dim] = slider - - self.step_size_setter = BoundedIntText( - value=1, - min=1, - max=self.sliders["t"].max, - step=1, - description="Step Size:", - disabled=False, - description_tooltip="set slider step", - layout=Layout(width="150px"), - ) - self.speed_text = BoundedIntText( - value=100, - min=1, - max=1_000, - step=50, - description="Speed", - disabled=False, - description_tooltip="Playback speed, this is NOT framerate.\nArbitrary units between 1 - 1,000", - layout=Layout(width="150px"), - ) - self.play_button = Play( - value=0, - min=self.sliders["t"].min, - max=self.sliders["t"].max, - step=self.sliders["t"].step, - description="play/pause", - disabled=False, - ) - widgets = [ - self.reset_vminvmax_button, - self.reset_vminvmax_hlut_button, - self.play_button, - self.step_size_setter, - self.speed_text, - ] - - self.play_button.interval = 10 - - self.step_size_setter.observe(self._change_stepsize, "value") - self.speed_text.observe(self._change_framerate, "value") - jslink((self.play_button, "value"), (self.sliders["t"], "value")) - jslink((self.play_button, "max"), (self.sliders["t"], "max")) - - self.reset_vminvmax_button.on_click(self._reset_vminvmax) - self.reset_vminvmax_hlut_button.on_click(self._reset_vminvmax_frame) - - self.iw.figure.renderer.add_event_handler(self._set_slider_layout, "resize") - - # the buttons - self.hbox = HBox(widgets) - - super().__init__((self.hbox, *list(self.sliders.values()))) - - def _reset_vminvmax(self, obj): - self.iw.reset_vmin_vmax() - - def _reset_vminvmax_frame(self, obj): - self.iw.reset_vmin_vmax_frame() - - def _change_stepsize(self, obj): - self.sliders["t"].step = self.step_size_setter.value - - def _change_framerate(self, change): - interval = int(1000 / change["new"]) - self.play_button.interval = interval - - def _set_slider_layout(self, *args): - w, h = self.iw.figure.renderer.logical_size - - for k, v in self.sliders.items(): - v.layout = Layout(width=f"{w}px") diff --git a/fastplotlib/widgets/_image_widget_qt_toolbar.py b/fastplotlib/widgets/_image_widget_qt_toolbar.py deleted file mode 100644 index 2117f95ab..000000000 --- a/fastplotlib/widgets/_image_widget_qt_toolbar.py +++ /dev/null @@ -1,127 +0,0 @@ -from functools import partial -from typing import Dict - -from fastplotlib.utils.gui import QtWidgets, QtCore - - -# TODO: There must be a better way to do this -# TODO: Check if an interface exists between ipywidgets and Qt -# TODO: Or we won't need it anyways once we have UI in pygfx -class SliderInterface: - """ - This exists so that ImageWidget has a common interface for Sliders. - - This interface makes a QSlider behave somewhat like a ipywidget IntSlider, enough for ImageWidget to function. - """ - - def __init__(self, qslider): - self.qslider = qslider - - @property - def value(self) -> int: - return self.qslider.value() - - @value.setter - def value(self, value: int): - self.qslider.setValue(value) - - @property - def max(self) -> int: - return self.qslider.maximum() - - @max.setter - def max(self, value: int): - self.qslider.setMaximum(value) - - @property - def min(self): - return self.qslider.minimum() - - @min.setter - def min(self, value: int): - self.qslider.setMinimum(value) - - -class QToolbarImageWidget(QtWidgets.QWidget): - """Toolbar for ImageWidget""" - - def __init__(self, image_widget): - QtWidgets.QWidget.__init__(self) - - # vertical layout - self.vlayout = QtWidgets.QVBoxLayout(self) - - self.image_widget = image_widget - - hlayout_buttons = QtWidgets.QHBoxLayout() - - self.reset_vmin_vmax_button = QtWidgets.QPushButton(self) - self.reset_vmin_vmax_button.setText("auto-contrast") - self.reset_vmin_vmax_button.clicked.connect(self.image_widget.reset_vmin_vmax) - hlayout_buttons.addWidget(self.reset_vmin_vmax_button) - - self.reset_vmin_vmax_hlut_button = QtWidgets.QPushButton(self) - self.reset_vmin_vmax_hlut_button.setText("reset histogram-lut") - self.reset_vmin_vmax_hlut_button.clicked.connect( - self.image_widget.reset_vmin_vmax_frame - ) - hlayout_buttons.addWidget(self.reset_vmin_vmax_hlut_button) - - self.vlayout.addLayout(hlayout_buttons) - - self.sliders: Dict[str, SliderInterface] = dict() - - # has time and/or z-volume - if self.image_widget.ndim > 2: - # create a slider, spinbox and dimension label for each dimension in the ImageWidget - for dim in self.image_widget.slider_dims: - hlayout = ( - QtWidgets.QHBoxLayout() - ) # horizontal stack for label, slider, spinbox - - # max value for current dimension - max_val = self.image_widget._dims_max_bounds[dim] - 1 - - # make slider - slider = QtWidgets.QSlider(self) - slider.setOrientation(QtCore.Qt.Orientation.Horizontal) - slider.setMinimum(0) - slider.setMaximum(max_val) - slider.setValue(0) - slider.setSingleStep(1) - slider.setPageStep(10) - - # make spinbox - spinbox = QtWidgets.QSpinBox(self) - spinbox.setMinimum(0) - spinbox.setMaximum(max_val) - spinbox.setValue(0) - spinbox.setSingleStep(1) - - # link slider and spinbox - slider.valueChanged.connect(spinbox.setValue) - spinbox.valueChanged.connect(slider.setValue) - - # connect slider to change the index within the dimension - slider.valueChanged.connect( - partial(self.image_widget._slider_value_changed, dim) - ) - - # slider dimension label - slider_label = QtWidgets.QLabel(self) - slider_label.setText(dim) - - # add the widgets to the horizontal layout - hlayout.addWidget(slider_label) - hlayout.addWidget(slider) - hlayout.addWidget(spinbox) - - # add horizontal layout to the vertical layout - self.vlayout.addLayout(hlayout) - - # add to sliders dict for easier access to users - self.sliders[dim] = SliderInterface(slider) - - max_height = 35 + (35 * len(self.sliders.keys())) - - self.setMaximumHeight(max_height) diff --git a/fastplotlib/widgets/image_widget/__init__.py b/fastplotlib/widgets/image_widget/__init__.py new file mode 100644 index 000000000..93aaa4ce1 --- /dev/null +++ b/fastplotlib/widgets/image_widget/__init__.py @@ -0,0 +1 @@ +from ._widget import ImageWidget diff --git a/fastplotlib/widgets/image_widget/_sliders.py b/fastplotlib/widgets/image_widget/_sliders.py new file mode 100644 index 000000000..c8ad67f39 --- /dev/null +++ b/fastplotlib/widgets/image_widget/_sliders.py @@ -0,0 +1,177 @@ +import os +from time import perf_counter + +from imgui_bundle import imgui, icons_fontawesome_6 as fa + +from ...ui import EdgeWindow + + +class ImageWidgetSliders(EdgeWindow): + def __init__(self, figure, size, location, title, image_widget): + super().__init__(figure=figure, size=size, location=location, title=title) + self._image_widget = image_widget + + # whether or not a dimension is in play mode + self._playing: dict[str, bool] = {"t": False, "z": False} + + # approximate framerate for playing + self._fps: dict[str, int] = {"t": 20, "z": 20} + # framerate converted to frame time + self._frame_time: dict[str, float] = {"t": 1 / 20, "z": 1 / 20} + + # last timepoint that a frame was displayed from a given dimension + self._last_frame_time: dict[str, float] = {"t": 0, "z": 0} + + self._loop = False + + if "RTD_BUILD" in os.environ.keys(): + if os.environ["RTD_BUILD"] == "1": + self._playing["t"] = True + self._loop = True + + def set_index(self, dim: str, index: int): + """set the current_index of the ImageWidget""" + + # make sure the max index for this dim is not exceeded + max_index = self._image_widget._dims_max_bounds[dim] - 1 + if index > max_index: + if self._loop: + # loop back to index zero if looping is enabled + index = 0 + else: + # if looping not enabled, stop playing this dimension + self._playing[dim] = False + return + + # set current_index + self._image_widget.current_index = {dim: min(index, max_index)} + + def update(self): + """called on every render cycle to update the GUI elements""" + + # store the new index of the image widget ("t" and "z") + new_index = dict() + + # flag if the index changed + flag_index_changed = False + + # reset vmin-vmax using full orig data + imgui.push_font(self._fa_icons) + if imgui.button(label=fa.ICON_FA_CIRCLE_HALF_STROKE + fa.ICON_FA_FILM): + self._image_widget.reset_vmin_vmax() + imgui.pop_font() + if imgui.is_item_hovered(0): + imgui.set_tooltip("reset contrast limits using full movie/stack") + + # reset vmin-vmax using currently displayed ImageGraphic data + imgui.push_font(self._fa_icons) + imgui.same_line() + if imgui.button(label=fa.ICON_FA_CIRCLE_HALF_STROKE): + self._image_widget.reset_vmin_vmax_frame() + imgui.pop_font() + if imgui.is_item_hovered(0): + imgui.set_tooltip("reset contrast limits using current frame") + + # time now + now = perf_counter() + + # buttons and slider UI elements for each dim + for dim in self._image_widget.slider_dims: + imgui.push_id(f"{self._id_counter}_{dim}") + imgui.push_font(self._fa_icons) + + if self._playing[dim]: + # show pause button if playing + if imgui.button(label=fa.ICON_FA_PAUSE): + # if pause button clicked, then set playing to false + self._playing[dim] = False + + # if in play mode and enough time has elapsed w.r.t. the desired framerate, increment the index + if now - self._last_frame_time[dim] >= self._frame_time[dim]: + self.set_index(dim, self._image_widget.current_index[dim] + 1) + self._last_frame_time[dim] = now + + else: + # we are not playing, so display play button + if imgui.button(label=fa.ICON_FA_PLAY): + # if play button is clicked, set last frame time to 0 so that index increments on next render + self._last_frame_time[dim] = 0 + # set playing to True since play button was clicked + self._playing[dim] = True + + imgui.same_line() + # step back one frame button + if imgui.button(label=fa.ICON_FA_BACKWARD_STEP) and not self._playing[dim]: + self.set_index(dim, self._image_widget.current_index[dim] - 1) + + imgui.same_line() + # step forward one frame button + if imgui.button(label=fa.ICON_FA_FORWARD_STEP) and not self._playing[dim]: + self.set_index(dim, self._image_widget.current_index[dim] + 1) + + imgui.same_line() + # stop button + if imgui.button(label=fa.ICON_FA_STOP): + self._playing[dim] = False + self._last_frame_time[dim] = 0 + self.set_index(dim, 0) + + imgui.same_line() + # loop checkbox + _, self._loop = imgui.checkbox(label=fa.ICON_FA_ROTATE, v=self._loop) + imgui.pop_font() + if imgui.is_item_hovered(0): + imgui.set_tooltip("loop playback") + + imgui.same_line() + imgui.text("framerate :") + imgui.same_line() + imgui.set_next_item_width(100) + # framerate int entry + fps_changed, value = imgui.input_int( + label="fps", v=self._fps[dim], step_fast=5 + ) + if imgui.is_item_hovered(0): + imgui.set_tooltip( + "framerate is approximate and less reliable as it approaches your monitor refresh rate" + ) + if fps_changed: + if value < 1: + value = 1 + if value > 50: + value = 50 + self._fps[dim] = value + self._frame_time[dim] = 1 / value + + val = self._image_widget.current_index[dim] + vmax = self._image_widget._dims_max_bounds[dim] - 1 + + imgui.text(f"{dim}: ") + imgui.same_line() + # so that slider occupies full width + imgui.set_next_item_width(self.width * 0.85) + + if "Jupyter" in self._image_widget.figure.canvas.__class__.__name__: + # until https://github.com/pygfx/wgpu-py/issues/530 + flags = imgui.SliderFlags_.no_input + else: + # clamps to min, max if user inputs value outside these bounds + flags = imgui.SliderFlags_.always_clamp + + # slider for this dimension + changed, index = imgui.slider_int( + f"{dim}", v=val, v_min=0, v_max=vmax, flags=flags + ) + + new_index[dim] = index + + # if the slider value changed for this dimension + flag_index_changed |= changed + + imgui.pop_id() + + if flag_index_changed: + # if any slider dim changed set the new index of the image widget + self._image_widget.current_index = new_index + + self.size = int(imgui.get_window_height()) diff --git a/fastplotlib/widgets/image.py b/fastplotlib/widgets/image_widget/_widget.py similarity index 90% rename from fastplotlib/widgets/image.py rename to fastplotlib/widgets/image_widget/_widget.py index 1819f8742..e40495be5 100644 --- a/fastplotlib/widgets/image.py +++ b/fastplotlib/widgets/image_widget/_widget.py @@ -1,12 +1,15 @@ -from typing import Any, Literal, Callable +from typing import Callable from warnings import warn import numpy as np -from ..layouts import Figure -from ..graphics import ImageGraphic -from ..utils import calculate_figure_shape -from .histogram_lut import HistogramLUT +from wgpu.gui import WgpuCanvasBase + +from ... import Figure +from ...graphics import ImageGraphic +from ...utils import calculate_figure_shape +from ...tools import HistogramLUTTool +from ._sliders import ImageWidgetSliders # Number of dimensions that represent one image/one frame. For grayscale shape will be [x, y], i.e. 2 dims, for RGB(A) @@ -105,13 +108,6 @@ def figure(self) -> Figure: """ return self._figure - @property - def widget(self): - """ - Output context, either an ipywidget or QWidget - """ - return self._output - @property def managed_graphics(self) -> list[ImageGraphic]: """List of ``ImageWidget`` managed graphics.""" @@ -170,11 +166,6 @@ def n_scrollable_dims(self) -> list[int]: """ return self._n_scrollable_dims - @property - def sliders(self) -> dict[str, Any]: - """the ipywidget IntSlider or QSlider instances used by the widget for indexing the desired dimensions""" - return self._image_widget_toolbar.sliders - @property def slider_dims(self) -> list[str]: """the dimensions that the sliders index""" @@ -197,6 +188,39 @@ def current_index(self) -> dict[str, int]: """ return self._current_index + @current_index.setter + 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())}" + ) + + 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]}" + ) + + self._current_index.update(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) + @property def n_img_dims(self) -> list[int]: """ @@ -247,42 +271,6 @@ def _get_n_scrollable_dims(self, curr_arr: np.ndarray, rgb: bool) -> list[int]: return n_scrollable_dims - @current_index.setter - def current_index(self, index: dict[str, int]): - # ignore if output context has not been created yet - if self.widget is None: - 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())}" - ) - - 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]}" - ) - - self._current_index.update(index) - - # can make a callback_block decorator later - self.block_sliders = True - for k in index.keys(): - self.sliders[k].value = index[k] - self.block_sliders = False - - 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 - def __init__( self, data: np.ndarray | list[np.ndarray], @@ -355,14 +343,13 @@ def __init__( passed to each ImageGraphic in the ImageWidget figure subplots """ + self._initialized = False + self._names = None if figure_kwargs is None: figure_kwargs = dict() - # output context - self._output = None - if _is_arraylike(data): data = [data] @@ -493,8 +480,6 @@ def __init__( self._window_funcs = None self.window_funcs = window_funcs - self._sliders: dict[str, Any] = dict() - # get max bound for all data arrays for all slider dimensions and ensure compatibility across slider dims self._dims_max_bounds: dict[str, int] = {k: 0 for k in self.slider_dims} for i, _dim in enumerate(list(self._dims_max_bounds.keys())): @@ -538,15 +523,34 @@ def __init__( subplot.set_title(name) if self._histogram_widget: - hlut = HistogramLUT(data=d, image_graphic=ig, name="histogram_lut") + hlut = HistogramLUTTool(data=d, image_graphic=ig, name="histogram_lut") subplot.docks["right"].add_graphic(hlut) subplot.docks["right"].size = 80 subplot.docks["right"].auto_scale(maintain_aspect=False) subplot.docks["right"].controller.enabled = False - self.block_sliders = False - self._image_widget_toolbar = None + # hard code the expected height so that the first render looks right in tests, docs etc. + if len(self.slider_dims) == 0: + ui_size = 57 + if len(self.slider_dims) == 1: + ui_size = 106 + elif len(self.slider_dims) == 2: + ui_size = 155 + + self._image_widget_sliders = ImageWidgetSliders( + figure=self.figure, + size=ui_size, + location="bottom", + title="ImageWidget Controls", + image_widget=self, + ) + + self.figure.add_gui(self._image_widget_sliders) + + self._initialized = True + + self._current_index_changed_handlers = set() @property def frame_apply(self) -> dict | None: @@ -749,21 +753,66 @@ def _process_frame_apply(self, array, data_ix) -> np.ndarray: return array - def _slider_value_changed(self, dimension: str, change: dict | int): - if self.block_sliders: - return - if isinstance(change, dict): - value = change["new"] - else: - value = change - self.current_index = {dimension: value} + def add_event_handler(self, handler: callable, event: str = "current_index"): + """ + Register an event handler. + + Currently the only event that ImageWidget supports is "current_index". This event is + emitted whenever the index of the ImageWidget changes. + + Parameters + ---------- + handler: callable + callback function, must take a dict as the only argument. This dict will be the `current_index` + + event: str, "current_index" + the only supported event is "current_index" + + Example + ------- + + .. code-block:: py + + def my_handler(index): + print(index) + # example prints: {"t": 100} if data has only time dimension + # "z" index will be another key if present in the data, ex: {"t": 100, "z": 5} + + # create an image widget + iw = ImageWidget(...) + + # add event handler + iw.add_event_handler(my_handler) + + """ + if event != "current_index": + raise ValueError( + "`current_index` is the only event supported by `ImageWidget`" + ) + + self._current_index_changed_handlers.add(handler) + + def remove_event_handler(self, handler: callable): + """Remove a registered event handler""" + self._current_index_changed_handlers.remove(handler) + + def clear_event_handlers(self): + """Clear all registered event handlers""" + self._current_index_changed_handlers.clear() def reset_vmin_vmax(self): """ Reset the vmin and vmax w.r.t. the full data """ - for ig in self.managed_graphics: - ig.reset_vmin_vmax() + for data, subplot in zip(self.data, self.figure): + if "histogram_lut" not in subplot.docks["right"]: + continue + hlut = subplot.docks["right"]["histogram_lut"] + hlut.set_data(data, reset_vmin_vmax=True) + + else: + for ig in self.managed_graphics: + ig.reset_vmin_vmax() def reset_vmin_vmax_frame(self): """ @@ -808,8 +857,6 @@ def set_data( if reset_indices: for key in self.current_index: self.current_index[key] = 0 - for key in self.sliders: - self.sliders[key].value = 0 # set slider max according to new data max_lengths = dict() @@ -880,18 +927,14 @@ def set_data( f"New arrays have differing values along dim {scroll_dim}" ) + self._dims_max_bounds[scroll_dim] = max_lengths[scroll_dim] + # set histogram widget if self._histogram_widget: subplot.docks["right"]["histogram_lut"].set_data( new_array, reset_vmin_vmax=reset_vmin_vmax ) - # set slider maxes - # TODO: maybe make this stuff a property, like ndims, n_frames etc. and have it set the sliders - for key in self.sliders.keys(): - self.sliders[key].max = max_lengths[key] - self._dims_max_bounds[key] = max_lengths[key] - # force graphics to update self.current_index = self.current_index @@ -903,28 +946,16 @@ def show( Returns ------- - OutputContext - ImageWidget just uses the Gridplot output context - """ - if self.figure.canvas.__class__.__name__ == "JupyterWgpuCanvas": - from ._image_widget_ipywidget_toolbar import IpywidgetImageWidgetToolbar - - self._image_widget_toolbar = IpywidgetImageWidgetToolbar(self) + WgpuCanvasBase + canvas used by the Figure - elif self.figure.canvas.__class__.__name__ == "QWgpuCanvas": - from ._image_widget_qt_toolbar import QToolbarImageWidget - - self._image_widget_toolbar = QToolbarImageWidget(self) + """ - self._output = self.figure.show( - toolbar=toolbar, + return self.figure.show( sidecar=sidecar, sidecar_kwargs=sidecar_kwargs, - add_widgets=[self._image_widget_toolbar], ) - return self._output - def close(self): """Close Widget""" self.figure.close() diff --git a/setup.py b/setup.py index 6f7f64468..46b68fae6 100644 --- a/setup.py +++ b/setup.py @@ -4,7 +4,8 @@ install_requires = [ "numpy>=1.23.0", - "pygfx>=0.4.0", + "pygfx>=0.5.0", + "wgpu>=0.18.1", "cmap>=0.1.3", ] @@ -22,9 +23,10 @@ "pandoc", "jupyterlab", "sidecar", - "imageio", + "imageio[pyav]", "matplotlib", - "scikit-learn" + "scikit-learn", + "imgui-bundle", ], "notebook": [ "jupyterlab", @@ -44,14 +46,17 @@ "scikit-learn", "tqdm", "sidecar", + "imgui-bundle", ], "tests-desktop": [ "pytest<8.0.0", "scipy", - "imageio", + "imageio[pyav]", "scikit-learn", "tqdm", + "imgui-bundle", ], + "imgui": ["imgui-bundle"], } diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 000000000..ffc34d464 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,8 @@ +import pygfx + + +MAX_TEXTURE_SIZE = 1024 + + +def pytest_sessionstart(session): + pygfx.renderers.wgpu.set_wgpu_limits(**{"max-texture-dimension2d": MAX_TEXTURE_SIZE}) diff --git a/tests/test_texture_array.py b/tests/test_texture_array.py index e1a6a1753..c85fc7652 100644 --- a/tests/test_texture_array.py +++ b/tests/test_texture_array.py @@ -5,10 +5,13 @@ import pygfx import fastplotlib as fpl -from fastplotlib.graphics._features import TextureArray, WGPU_MAX_TEXTURE_SIZE +from fastplotlib.graphics._features import TextureArray from fastplotlib.graphics.image import _ImageTile +MAX_TEXTURE_SIZE = 1024 + + def make_data(n_rows: int, n_cols: int) -> np.ndarray: """ Makes a 2D array where the amplitude of the sine wave @@ -50,14 +53,14 @@ def check_texture_array( assert ta.buffer[chunk_index] is texture chunk_row, chunk_col = chunk_index - data_row_start_index = chunk_row * WGPU_MAX_TEXTURE_SIZE - data_col_start_index = chunk_col * WGPU_MAX_TEXTURE_SIZE + data_row_start_index = chunk_row * MAX_TEXTURE_SIZE + data_col_start_index = chunk_col * MAX_TEXTURE_SIZE data_row_stop_index = min( - data.shape[0], data_row_start_index + WGPU_MAX_TEXTURE_SIZE + data.shape[0], data_row_start_index + MAX_TEXTURE_SIZE ) data_col_stop_index = min( - data.shape[1], data_col_start_index + WGPU_MAX_TEXTURE_SIZE + data.shape[1], data_col_start_index + MAX_TEXTURE_SIZE ) row_slice = slice(data_row_start_index, data_row_stop_index) @@ -96,7 +99,7 @@ def check_image_graphic(texture_array, graphic): @pytest.mark.parametrize("test_graphic", [False, True]) def test_small_texture(test_graphic): # tests TextureArray with dims that requires only 1 texture - data = make_data(1_000, 1_000) + data = make_data(500, 500) if test_graphic: graphic = make_image_graphic(data) @@ -118,13 +121,13 @@ def test_small_texture(test_graphic): if test_graphic: check_image_graphic(ta, graphic) - check_set_slice(data, ta, slice(50, 200), slice(600, 800)) + check_set_slice(data, ta, slice(50, 200), slice(200, 400)) @pytest.mark.parametrize("test_graphic", [False, True]) def test_texture_at_limit(test_graphic): - # tests TextureArray with data that is 8192 x 8192 - data = make_data(WGPU_MAX_TEXTURE_SIZE, WGPU_MAX_TEXTURE_SIZE) + # tests TextureArray with data that is 1024 x 1024 + data = make_data(MAX_TEXTURE_SIZE, MAX_TEXTURE_SIZE) if test_graphic: graphic = make_image_graphic(data) @@ -146,12 +149,12 @@ def test_texture_at_limit(test_graphic): if test_graphic: check_image_graphic(ta, graphic) - check_set_slice(data, ta, slice(5000, 8000), slice(2000, 3000)) + check_set_slice(data, ta, slice(500, 800), slice(200, 300)) @pytest.mark.parametrize("test_graphic", [False, True]) def test_wide(test_graphic): - data = make_data(10_000, 20_000) + data = make_data(1_200, 2_200) if test_graphic: graphic = make_image_graphic(data) @@ -166,19 +169,19 @@ def test_wide(test_graphic): buffer_shape=(2, 3), row_indices_size=2, col_indices_size=3, - row_indices_values=np.array([0, 8192]), - col_indices_values=np.array([0, 8192, 16384]), + row_indices_values=np.array([0, MAX_TEXTURE_SIZE]), + col_indices_values=np.array([0, MAX_TEXTURE_SIZE, 2 * MAX_TEXTURE_SIZE]), ) if test_graphic: check_image_graphic(ta, graphic) - check_set_slice(data, ta, slice(6_000, 9_000), slice(12_000, 18_000)) + check_set_slice(data, ta, slice(600, 1_100), slice(100, 2_100)) @pytest.mark.parametrize("test_graphic", [False, True]) def test_tall(test_graphic): - data = make_data(20_000, 10_000) + data = make_data(2_200, 1_200) if test_graphic: graphic = make_image_graphic(data) @@ -193,19 +196,19 @@ def test_tall(test_graphic): buffer_shape=(3, 2), row_indices_size=3, col_indices_size=2, - row_indices_values=np.array([0, 8192, 16384]), - col_indices_values=np.array([0, 8192]), + row_indices_values=np.array([0, MAX_TEXTURE_SIZE, 2 * MAX_TEXTURE_SIZE]), + col_indices_values=np.array([0, MAX_TEXTURE_SIZE]), ) if test_graphic: check_image_graphic(ta, graphic) - check_set_slice(data, ta, slice(12_000, 18_000), slice(6_000, 9_000)) + check_set_slice(data, ta, slice(100, 2_100), slice(600, 1_100)) @pytest.mark.parametrize("test_graphic", [False, True]) def test_square(test_graphic): - data = make_data(20_000, 20_000) + data = make_data(2_200, 2_200) if test_graphic: graphic = make_image_graphic(data) @@ -220,11 +223,11 @@ def test_square(test_graphic): buffer_shape=(3, 3), row_indices_size=3, col_indices_size=3, - row_indices_values=np.array([0, 8192, 16384]), - col_indices_values=np.array([0, 8192, 16384]), + row_indices_values=np.array([0, MAX_TEXTURE_SIZE, 2 * MAX_TEXTURE_SIZE]), + col_indices_values=np.array([0, MAX_TEXTURE_SIZE, 2 * MAX_TEXTURE_SIZE]), ) if test_graphic: check_image_graphic(ta, graphic) - check_set_slice(data, ta, slice(12_000, 18_000), slice(16_000, 19_000)) + check_set_slice(data, ta, slice(100, 2_100), slice(100, 2_100))