diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b0ddb4baf..3abcfaaf0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,6 +15,37 @@ on: jobs: + docs-build: + name: Docs + runs-on: ubuntu-latest + strategy: + fail-fast: false + steps: + - uses: actions/checkout@v3 + - name: Set up Python 3.9 + uses: actions/setup-python@v4 + with: + python-version: 3.9 + - name: Install llvmpipe and lavapipe for offscreen canvas, and git lfs + run: | + sudo apt-get update -y -qq + sudo apt-get install --no-install-recommends -y libegl1-mesa libgl1-mesa-dri libxcb-xfixes0-dev mesa-vulkan-drivers git-lfs + - name: Install pandoc v3.14, nbsphinx complains about older pandoc versions + run: | + wget https://github.com/jgm/pandoc/releases/download/3.1.4/pandoc-3.1.4-1-amd64.deb + sudo apt-get install ./pandoc-3.1.4-1-amd64.deb + - name: Install dev dependencies + run: | + python -m pip install --upgrade pip + # remove pygfx from requirements, we install a specific commit of pygfx since both fpl and pygfx are fast evolving + sed -i "/pygfx/d" ./setup.py + pip install git+https://github.com/pygfx/pygfx.git@cf1da6c32223ba3cf7256982ce9b89c81b593076 + pip install -e ".[notebook,docs,tests]" + - name: Build docs + run: | + cd docs + make html SPHINXOPTS="-W --keep-going" + test-build: name: Test examples runs-on: ubuntu-latest @@ -47,7 +78,7 @@ jobs: python -m pip install --upgrade pip # remove pygfx from requirements, we install a specific commit of pygfx since both fpl and pygfx are fast evolving sed -i "/pygfx/d" ./setup.py - pip install git+https://github.com/pygfx/pygfx.git@b63f22a1aa61993c32cd96895316cb8248a81e4d + pip install git+https://github.com/pygfx/pygfx.git@cf1da6c32223ba3cf7256982ce9b89c81b593076 pip install -e ".["tests"]" - name: Show wgpu backend run: @@ -61,9 +92,11 @@ jobs: PYGFX_EXPECT_LAVAPIPE: true run: | pytest -v examples - pytest --nbmake notebooks/ + pytest --nbmake examples/notebooks/ - uses: actions/upload-artifact@v3 if: ${{ failure() }} with: name: screenshot-diffs - path: examples/diffs + path: | + examples/desktop/diffs + examples/notebooks/diffs diff --git a/.github/workflows/screenshots.yml b/.github/workflows/screenshots.yml index 98d2ad86b..488ad108f 100644 --- a/.github/workflows/screenshots.yml +++ b/.github/workflows/screenshots.yml @@ -44,8 +44,11 @@ jobs: run: | # regenerate screenshots REGENERATE_SCREENSHOTS=1 pytest -v examples + REGENERATE_SCREENSHOTS=1 pytest --nbmake examples/notebooks/ - uses: actions/upload-artifact@v3 if: always() with: name: screenshots - path: examples/screenshots/ + path: | + examples/desktop/screenshots/ + examples/notebooks/screenshots/ diff --git a/.readthedocs.yaml b/.readthedocs.yaml index ae7598f41..ce6b214e4 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -10,7 +10,23 @@ build: - libxcb-xfixes0-dev - mesa-vulkan-drivers - libglfw3 + - pandoc # installs older version of pandoc which nbsphinx complains about, but works for now jobs: + post_checkout: + # Download and uncompress the binary + # https://git-lfs.github.com/ + - wget https://github.com/git-lfs/git-lfs/releases/download/v3.1.4/git-lfs-linux-amd64-v3.1.4.tar.gz + - tar xvfz git-lfs-linux-amd64-v3.1.4.tar.gz + # Modify LFS config paths to point where git-lfs binary was downloaded + - git config filter.lfs.process "`pwd`/git-lfs filter-process" + - git config filter.lfs.smudge "`pwd`/git-lfs smudge -- %f" + - git config filter.lfs.clean "`pwd`/git-lfs clean -- %f" + # Make LFS available in current repository + - ./git-lfs install + # Download content from remote + - ./git-lfs fetch + # Make local files to have the real content on them + - ./git-lfs checkout pre_install: - pip install git+https://github.com/pygfx/pygfx.git@main diff --git a/README.md b/README.md index 856df65d8..dccd8196b 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,9 @@ Higher resolution demo: [https://github.com/kushalkolar/fastplotlib/assets/94033 http://fastplotlib.readthedocs.io/ -The docs are not entirely thorough, we recommend the example notebooks to get started. +The Quickstart guide is not interactive. We recommend cloning/downloading the repo and trying out the `desktop` or `notebook` examples: https://github.com/kushalkolar/fastplotlib/tree/master/examples + +If someone wants to integrate `pyodide` with `pygfx` we would be able to have live interactive examples! :smiley: Questions, ideas? Post an issue or [chat on gitter](https://gitter.im/fastplotlib/community?utm_source=share-link&utm_medium=link&utm_campaign=share-link). @@ -77,18 +79,26 @@ pip install -e ".[notebook,docs,tests]" > > `fastplotlib` and `pygfx` are fast evolving, you may require the latest `pygfx` and `fastplotlib` from github to use the examples in the master branch. -Clone or download the repo to try the examples +First clone or download the repo to try the examples ```bash -# clone the repo git clone https://github.com/kushalkolar/fastplotlib.git +``` + +### Desktop examples using `glfw` or `Qt` + +```bash +# most dirs within examples contain example code +cd examples/desktop + +# simplest example +python image/image_simple.py +``` -# IMPORTANT: if you are using a specific version from pip, checkout that version to get the examples which work for that version -# example: -# git checkout git checkout v0.1.0.a9 # replace "v0.1.0.a9" with the version you have +### Notebook examples -# cd into notebooks and launch jupyter lab -cd fastplotlib/notebooks +```bash +cd examples/notebooks jupyter lab ``` @@ -96,10 +106,10 @@ jupyter lab ### Simple image plot ```python -from fastplotlib import Plot +import fastplotlib as fpl import numpy as np -plot = Plot() +plot = fpl.Plot() data = np.random.rand(512, 512) plot.add_image(data=data) @@ -110,10 +120,10 @@ plot.show() ### Fast animations ```python -from fastplotlib import Plot +import fastplotlib as fpl import numpy as np -plot = Plot() +plot = fpl.Plot() data = np.random.rand(512, 512) image = plot.image(data=data) diff --git a/docs/doc_build_instructions.md b/docs/doc_build_instructions.md new file mode 100644 index 000000000..9df7587e2 --- /dev/null +++ b/docs/doc_build_instructions.md @@ -0,0 +1,11 @@ +1. The API doc files are autogenerated using `source/generate_api.py` + +``` +python source/generate_api.py +``` + +2. make the docs + +make html -j24 + + diff --git a/docs/source/_static/logo.png b/docs/source/_static/logo.png new file mode 100644 index 000000000..304fc88bc --- /dev/null +++ b/docs/source/_static/logo.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7385f790683edc6c79fb6131e1649bd54d7fefd405cc8b6005ed86ea7dbb8fa6 +size 31759 diff --git a/examples/gridplot/__init__.py b/docs/source/_static/style.css similarity index 100% rename from examples/gridplot/__init__.py rename to docs/source/_static/style.css diff --git a/docs/source/_templates/autosummary/class.rst b/docs/source/_templates/autosummary/class.rst new file mode 100644 index 000000000..d4fd5208b --- /dev/null +++ b/docs/source/_templates/autosummary/class.rst @@ -0,0 +1,5 @@ +{{ fullname | escape | underline}} + +.. currentmodule:: {{ module }} + +.. autoclass:: {{ objname }} diff --git a/docs/source/_templates/autosummary/method.rst b/docs/source/_templates/autosummary/method.rst new file mode 100644 index 000000000..306d2aab5 --- /dev/null +++ b/docs/source/_templates/autosummary/method.rst @@ -0,0 +1,5 @@ +{{ name | escape | underline}} + +.. currentmodule:: {{ module }} + +.. automethod:: {{ objname }} diff --git a/docs/source/_templates/autosummary/property.rst b/docs/source/_templates/autosummary/property.rst new file mode 100644 index 000000000..c31bebe07 --- /dev/null +++ b/docs/source/_templates/autosummary/property.rst @@ -0,0 +1,5 @@ +{{ name | escape | underline}} + +.. currentmodule:: {{ module }} + +.. autoproperty:: {{ objname }} diff --git a/docs/source/api/graphic_features.rst b/docs/source/api/graphic_features.rst deleted file mode 100644 index 2fe60ce24..000000000 --- a/docs/source/api/graphic_features.rst +++ /dev/null @@ -1,81 +0,0 @@ -.. _api_graphic_features: - -Graphic Features -**************** - -Image -##### - -.. autoclass:: fastplotlib.graphics.features.ImageDataFeature - :members: - :inherited-members: - :exclude-members: __init__ - :no-undoc-members: - -.. autoclass:: fastplotlib.graphics.features.ImageCmapFeature - :members: - :inherited-members: - :exclude-members: __init__ - :no-undoc-members: - -Heatmap -####### - -.. autoclass:: fastplotlib.graphics.features.HeatmapDataFeature - :members: - :inherited-members: - :exclude-members: __init__ - :no-undoc-members: - -.. autoclass:: fastplotlib.graphics.features.HeatmapCmapFeature - :members: - :inherited-members: - :exclude-members: __init__ - :no-undoc-members: - -Line -#### - -.. autoclass:: fastplotlib.graphics.features.PositionsDataFeature - :members: - :inherited-members: - :exclude-members: __init__ - :no-undoc-members: - -.. autoclass:: fastplotlib.graphics.features.ColorsFeature - :members: - :inherited-members: - :exclude-members: __init__ - :no-undoc-members: - -.. autoclass:: fastplotlib.graphics.features.ThicknessFeature - :members: - :inherited-members: - :exclude-members: __init__ - :no-undoc-members: - -Scatter -####### - -.. autoclass:: fastplotlib.graphics.features.PositionsDataFeature - :members: - :inherited-members: - :exclude-members: __init__ - :no-undoc-members: - -.. autoclass:: fastplotlib.graphics.features.ColorsFeature - :members: - :inherited-members: - :exclude-members: __init__ - :no-undoc-members: - -Common -###### - -Features common to all graphics - -.. autoclass:: fastplotlib.graphics.features.PresentFeature - :members: - :inherited-members: - :exclude-members: __init__ - :no-undoc-members: diff --git a/docs/source/api/graphic_features/CmapFeature.rst b/docs/source/api/graphic_features/CmapFeature.rst new file mode 100644 index 000000000..03e3330b7 --- /dev/null +++ b/docs/source/api/graphic_features/CmapFeature.rst @@ -0,0 +1,35 @@ +.. _api.CmapFeature: + +CmapFeature +*********** + +=========== +CmapFeature +=========== +.. currentmodule:: fastplotlib.graphics._features + +Constructor +~~~~~~~~~~~ +.. autosummary:: + :toctree: CmapFeature_api + + CmapFeature + +Properties +~~~~~~~~~~ +.. autosummary:: + :toctree: CmapFeature_api + + CmapFeature.buffer + CmapFeature.values + +Methods +~~~~~~~ +.. autosummary:: + :toctree: CmapFeature_api + + CmapFeature.add_event_handler + CmapFeature.block_events + CmapFeature.clear_event_handlers + CmapFeature.remove_event_handler + diff --git a/docs/source/api/graphic_features/ColorFeature.rst b/docs/source/api/graphic_features/ColorFeature.rst new file mode 100644 index 000000000..3ed84cd70 --- /dev/null +++ b/docs/source/api/graphic_features/ColorFeature.rst @@ -0,0 +1,34 @@ +.. _api.ColorFeature: + +ColorFeature +************ + +============ +ColorFeature +============ +.. currentmodule:: fastplotlib.graphics._features + +Constructor +~~~~~~~~~~~ +.. autosummary:: + :toctree: ColorFeature_api + + ColorFeature + +Properties +~~~~~~~~~~ +.. autosummary:: + :toctree: ColorFeature_api + + ColorFeature.buffer + +Methods +~~~~~~~ +.. autosummary:: + :toctree: ColorFeature_api + + ColorFeature.add_event_handler + ColorFeature.block_events + ColorFeature.clear_event_handlers + ColorFeature.remove_event_handler + diff --git a/docs/source/api/graphic_features/FeatureEvent.rst b/docs/source/api/graphic_features/FeatureEvent.rst new file mode 100644 index 000000000..f22ee3ef4 --- /dev/null +++ b/docs/source/api/graphic_features/FeatureEvent.rst @@ -0,0 +1,29 @@ +.. _api.FeatureEvent: + +FeatureEvent +************ + +============ +FeatureEvent +============ +.. currentmodule:: fastplotlib.graphics._features + +Constructor +~~~~~~~~~~~ +.. autosummary:: + :toctree: FeatureEvent_api + + FeatureEvent + +Properties +~~~~~~~~~~ +.. autosummary:: + :toctree: FeatureEvent_api + + +Methods +~~~~~~~ +.. autosummary:: + :toctree: FeatureEvent_api + + diff --git a/docs/source/api/graphic_features/GraphicFeature.rst b/docs/source/api/graphic_features/GraphicFeature.rst new file mode 100644 index 000000000..7abc3e6b2 --- /dev/null +++ b/docs/source/api/graphic_features/GraphicFeature.rst @@ -0,0 +1,33 @@ +.. _api.GraphicFeature: + +GraphicFeature +************** + +============== +GraphicFeature +============== +.. currentmodule:: fastplotlib.graphics._features + +Constructor +~~~~~~~~~~~ +.. autosummary:: + :toctree: GraphicFeature_api + + GraphicFeature + +Properties +~~~~~~~~~~ +.. autosummary:: + :toctree: GraphicFeature_api + + +Methods +~~~~~~~ +.. autosummary:: + :toctree: GraphicFeature_api + + GraphicFeature.add_event_handler + GraphicFeature.block_events + GraphicFeature.clear_event_handlers + GraphicFeature.remove_event_handler + diff --git a/docs/source/api/graphic_features/GraphicFeatureIndexable.rst b/docs/source/api/graphic_features/GraphicFeatureIndexable.rst new file mode 100644 index 000000000..7bd1383bc --- /dev/null +++ b/docs/source/api/graphic_features/GraphicFeatureIndexable.rst @@ -0,0 +1,34 @@ +.. _api.GraphicFeatureIndexable: + +GraphicFeatureIndexable +*********************** + +======================= +GraphicFeatureIndexable +======================= +.. currentmodule:: fastplotlib.graphics._features + +Constructor +~~~~~~~~~~~ +.. autosummary:: + :toctree: GraphicFeatureIndexable_api + + GraphicFeatureIndexable + +Properties +~~~~~~~~~~ +.. autosummary:: + :toctree: GraphicFeatureIndexable_api + + GraphicFeatureIndexable.buffer + +Methods +~~~~~~~ +.. autosummary:: + :toctree: GraphicFeatureIndexable_api + + GraphicFeatureIndexable.add_event_handler + GraphicFeatureIndexable.block_events + GraphicFeatureIndexable.clear_event_handlers + GraphicFeatureIndexable.remove_event_handler + diff --git a/docs/source/api/graphic_features/HeatmapCmapFeature.rst b/docs/source/api/graphic_features/HeatmapCmapFeature.rst new file mode 100644 index 000000000..77df37ab0 --- /dev/null +++ b/docs/source/api/graphic_features/HeatmapCmapFeature.rst @@ -0,0 +1,35 @@ +.. _api.HeatmapCmapFeature: + +HeatmapCmapFeature +****************** + +================== +HeatmapCmapFeature +================== +.. currentmodule:: fastplotlib.graphics._features + +Constructor +~~~~~~~~~~~ +.. autosummary:: + :toctree: HeatmapCmapFeature_api + + HeatmapCmapFeature + +Properties +~~~~~~~~~~ +.. autosummary:: + :toctree: HeatmapCmapFeature_api + + HeatmapCmapFeature.vmax + HeatmapCmapFeature.vmin + +Methods +~~~~~~~ +.. autosummary:: + :toctree: HeatmapCmapFeature_api + + HeatmapCmapFeature.add_event_handler + HeatmapCmapFeature.block_events + HeatmapCmapFeature.clear_event_handlers + HeatmapCmapFeature.remove_event_handler + diff --git a/docs/source/api/graphic_features/HeatmapDataFeature.rst b/docs/source/api/graphic_features/HeatmapDataFeature.rst new file mode 100644 index 000000000..029f0e199 --- /dev/null +++ b/docs/source/api/graphic_features/HeatmapDataFeature.rst @@ -0,0 +1,35 @@ +.. _api.HeatmapDataFeature: + +HeatmapDataFeature +****************** + +================== +HeatmapDataFeature +================== +.. currentmodule:: fastplotlib.graphics._features + +Constructor +~~~~~~~~~~~ +.. autosummary:: + :toctree: HeatmapDataFeature_api + + HeatmapDataFeature + +Properties +~~~~~~~~~~ +.. autosummary:: + :toctree: HeatmapDataFeature_api + + HeatmapDataFeature.buffer + +Methods +~~~~~~~ +.. autosummary:: + :toctree: HeatmapDataFeature_api + + HeatmapDataFeature.add_event_handler + HeatmapDataFeature.block_events + HeatmapDataFeature.clear_event_handlers + HeatmapDataFeature.remove_event_handler + HeatmapDataFeature.update_gpu + diff --git a/docs/source/api/graphic_features/ImageCmapFeature.rst b/docs/source/api/graphic_features/ImageCmapFeature.rst new file mode 100644 index 000000000..d2174ff9a --- /dev/null +++ b/docs/source/api/graphic_features/ImageCmapFeature.rst @@ -0,0 +1,35 @@ +.. _api.ImageCmapFeature: + +ImageCmapFeature +**************** + +================ +ImageCmapFeature +================ +.. currentmodule:: fastplotlib.graphics._features + +Constructor +~~~~~~~~~~~ +.. autosummary:: + :toctree: ImageCmapFeature_api + + ImageCmapFeature + +Properties +~~~~~~~~~~ +.. autosummary:: + :toctree: ImageCmapFeature_api + + ImageCmapFeature.vmax + ImageCmapFeature.vmin + +Methods +~~~~~~~ +.. autosummary:: + :toctree: ImageCmapFeature_api + + ImageCmapFeature.add_event_handler + ImageCmapFeature.block_events + ImageCmapFeature.clear_event_handlers + ImageCmapFeature.remove_event_handler + diff --git a/docs/source/api/graphic_features/ImageDataFeature.rst b/docs/source/api/graphic_features/ImageDataFeature.rst new file mode 100644 index 000000000..35fe74cf7 --- /dev/null +++ b/docs/source/api/graphic_features/ImageDataFeature.rst @@ -0,0 +1,35 @@ +.. _api.ImageDataFeature: + +ImageDataFeature +**************** + +================ +ImageDataFeature +================ +.. currentmodule:: fastplotlib.graphics._features + +Constructor +~~~~~~~~~~~ +.. autosummary:: + :toctree: ImageDataFeature_api + + ImageDataFeature + +Properties +~~~~~~~~~~ +.. autosummary:: + :toctree: ImageDataFeature_api + + ImageDataFeature.buffer + +Methods +~~~~~~~ +.. autosummary:: + :toctree: ImageDataFeature_api + + ImageDataFeature.add_event_handler + ImageDataFeature.block_events + ImageDataFeature.clear_event_handlers + ImageDataFeature.remove_event_handler + ImageDataFeature.update_gpu + diff --git a/docs/source/api/graphic_features/LinearRegionSelectionFeature.rst b/docs/source/api/graphic_features/LinearRegionSelectionFeature.rst new file mode 100644 index 000000000..a15825530 --- /dev/null +++ b/docs/source/api/graphic_features/LinearRegionSelectionFeature.rst @@ -0,0 +1,34 @@ +.. _api.LinearRegionSelectionFeature: + +LinearRegionSelectionFeature +**************************** + +============================ +LinearRegionSelectionFeature +============================ +.. currentmodule:: fastplotlib.graphics._features + +Constructor +~~~~~~~~~~~ +.. autosummary:: + :toctree: LinearRegionSelectionFeature_api + + LinearRegionSelectionFeature + +Properties +~~~~~~~~~~ +.. autosummary:: + :toctree: LinearRegionSelectionFeature_api + + LinearRegionSelectionFeature.axis + +Methods +~~~~~~~ +.. autosummary:: + :toctree: LinearRegionSelectionFeature_api + + LinearRegionSelectionFeature.add_event_handler + LinearRegionSelectionFeature.block_events + LinearRegionSelectionFeature.clear_event_handlers + LinearRegionSelectionFeature.remove_event_handler + diff --git a/docs/source/api/graphic_features/LinearSelectionFeature.rst b/docs/source/api/graphic_features/LinearSelectionFeature.rst new file mode 100644 index 000000000..aeb1ca66b --- /dev/null +++ b/docs/source/api/graphic_features/LinearSelectionFeature.rst @@ -0,0 +1,33 @@ +.. _api.LinearSelectionFeature: + +LinearSelectionFeature +********************** + +====================== +LinearSelectionFeature +====================== +.. currentmodule:: fastplotlib.graphics._features + +Constructor +~~~~~~~~~~~ +.. autosummary:: + :toctree: LinearSelectionFeature_api + + LinearSelectionFeature + +Properties +~~~~~~~~~~ +.. autosummary:: + :toctree: LinearSelectionFeature_api + + +Methods +~~~~~~~ +.. autosummary:: + :toctree: LinearSelectionFeature_api + + LinearSelectionFeature.add_event_handler + LinearSelectionFeature.block_events + LinearSelectionFeature.clear_event_handlers + LinearSelectionFeature.remove_event_handler + diff --git a/docs/source/api/graphic_features/PointsDataFeature.rst b/docs/source/api/graphic_features/PointsDataFeature.rst new file mode 100644 index 000000000..078b1c535 --- /dev/null +++ b/docs/source/api/graphic_features/PointsDataFeature.rst @@ -0,0 +1,34 @@ +.. _api.PointsDataFeature: + +PointsDataFeature +***************** + +================= +PointsDataFeature +================= +.. currentmodule:: fastplotlib.graphics._features + +Constructor +~~~~~~~~~~~ +.. autosummary:: + :toctree: PointsDataFeature_api + + PointsDataFeature + +Properties +~~~~~~~~~~ +.. autosummary:: + :toctree: PointsDataFeature_api + + PointsDataFeature.buffer + +Methods +~~~~~~~ +.. autosummary:: + :toctree: PointsDataFeature_api + + PointsDataFeature.add_event_handler + PointsDataFeature.block_events + PointsDataFeature.clear_event_handlers + PointsDataFeature.remove_event_handler + diff --git a/docs/source/api/graphic_features/PresentFeature.rst b/docs/source/api/graphic_features/PresentFeature.rst new file mode 100644 index 000000000..1ddbf1ec4 --- /dev/null +++ b/docs/source/api/graphic_features/PresentFeature.rst @@ -0,0 +1,33 @@ +.. _api.PresentFeature: + +PresentFeature +************** + +============== +PresentFeature +============== +.. currentmodule:: fastplotlib.graphics._features + +Constructor +~~~~~~~~~~~ +.. autosummary:: + :toctree: PresentFeature_api + + PresentFeature + +Properties +~~~~~~~~~~ +.. autosummary:: + :toctree: PresentFeature_api + + +Methods +~~~~~~~ +.. autosummary:: + :toctree: PresentFeature_api + + PresentFeature.add_event_handler + PresentFeature.block_events + PresentFeature.clear_event_handlers + PresentFeature.remove_event_handler + diff --git a/docs/source/api/graphic_features/ThicknessFeature.rst b/docs/source/api/graphic_features/ThicknessFeature.rst new file mode 100644 index 000000000..80219a2cd --- /dev/null +++ b/docs/source/api/graphic_features/ThicknessFeature.rst @@ -0,0 +1,33 @@ +.. _api.ThicknessFeature: + +ThicknessFeature +**************** + +================ +ThicknessFeature +================ +.. currentmodule:: fastplotlib.graphics._features + +Constructor +~~~~~~~~~~~ +.. autosummary:: + :toctree: ThicknessFeature_api + + ThicknessFeature + +Properties +~~~~~~~~~~ +.. autosummary:: + :toctree: ThicknessFeature_api + + +Methods +~~~~~~~ +.. autosummary:: + :toctree: ThicknessFeature_api + + ThicknessFeature.add_event_handler + ThicknessFeature.block_events + ThicknessFeature.clear_event_handlers + ThicknessFeature.remove_event_handler + diff --git a/docs/source/api/graphic_features/index.rst b/docs/source/api/graphic_features/index.rst new file mode 100644 index 000000000..aff2aabda --- /dev/null +++ b/docs/source/api/graphic_features/index.rst @@ -0,0 +1,21 @@ +Graphic Features +**************** + +.. toctree:: + :maxdepth: 1 + + ColorFeature + CmapFeature + ImageCmapFeature + HeatmapCmapFeature + PointsDataFeature + ImageDataFeature + HeatmapDataFeature + PresentFeature + ThicknessFeature + GraphicFeature + GraphicFeatureIndexable + FeatureEvent + to_gpu_supported_dtype + LinearSelectionFeature + LinearRegionSelectionFeature diff --git a/docs/source/api/graphic_features/to_gpu_supported_dtype.rst b/docs/source/api/graphic_features/to_gpu_supported_dtype.rst new file mode 100644 index 000000000..984a76157 --- /dev/null +++ b/docs/source/api/graphic_features/to_gpu_supported_dtype.rst @@ -0,0 +1,29 @@ +.. _api.to_gpu_supported_dtype: + +to_gpu_supported_dtype +********************** + +====================== +to_gpu_supported_dtype +====================== +.. currentmodule:: fastplotlib.graphics._features + +Constructor +~~~~~~~~~~~ +.. autosummary:: + :toctree: to_gpu_supported_dtype_api + + to_gpu_supported_dtype + +Properties +~~~~~~~~~~ +.. autosummary:: + :toctree: to_gpu_supported_dtype_api + + +Methods +~~~~~~~ +.. autosummary:: + :toctree: to_gpu_supported_dtype_api + + diff --git a/docs/source/api/graphics.rst b/docs/source/api/graphics.rst deleted file mode 100644 index d38045dae..000000000 --- a/docs/source/api/graphics.rst +++ /dev/null @@ -1,61 +0,0 @@ -.. _api_graphics: - -Graphics -******** - -Image -##### - -.. autoclass:: fastplotlib.graphics.image.ImageGraphic - :members: - :inherited-members: - -Line -#### - -.. autoclass:: fastplotlib.graphics.line.LineGraphic - :members: - :inherited-members: - -Line Collection -############### - -.. autoclass:: fastplotlib.graphics.line_collection.LineCollection - :members: - :inherited-members: - -Line Stack -########## - -.. autoclass:: fastplotlib.graphics.line_collection.LineStack - :members: - :inherited-members: - -Heatmap -####### - -.. autoclass:: fastplotlib.graphics.image.HeatmapGraphic - :members: - :inherited-members: - -Histogram -######### - -.. autoclass:: fastplotlib.graphics.histogram.HistogramGraphic - :members: - :inherited-members: - -Scatter -####### - -.. autoclass:: fastplotlib.graphics.scatter.ScatterGraphic - :members: - :inherited-members: - -Text -#### - -.. autoclass:: fastplotlib.graphics.text.TextGraphic - :members: - :inherited-members: - diff --git a/docs/source/api/graphics/HeatmapGraphic.rst b/docs/source/api/graphics/HeatmapGraphic.rst new file mode 100644 index 000000000..57466698a --- /dev/null +++ b/docs/source/api/graphics/HeatmapGraphic.rst @@ -0,0 +1,41 @@ +.. _api.HeatmapGraphic: + +HeatmapGraphic +************** + +============== +HeatmapGraphic +============== +.. currentmodule:: fastplotlib + +Constructor +~~~~~~~~~~~ +.. autosummary:: + :toctree: HeatmapGraphic_api + + HeatmapGraphic + +Properties +~~~~~~~~~~ +.. autosummary:: + :toctree: HeatmapGraphic_api + + HeatmapGraphic.children + HeatmapGraphic.position + HeatmapGraphic.position_x + HeatmapGraphic.position_y + HeatmapGraphic.position_z + HeatmapGraphic.visible + HeatmapGraphic.vmax + HeatmapGraphic.vmin + HeatmapGraphic.world_object + +Methods +~~~~~~~ +.. autosummary:: + :toctree: HeatmapGraphic_api + + HeatmapGraphic.add_linear_region_selector + HeatmapGraphic.add_linear_selector + HeatmapGraphic.link + diff --git a/docs/source/api/graphics/ImageGraphic.rst b/docs/source/api/graphics/ImageGraphic.rst new file mode 100644 index 000000000..083c72abb --- /dev/null +++ b/docs/source/api/graphics/ImageGraphic.rst @@ -0,0 +1,39 @@ +.. _api.ImageGraphic: + +ImageGraphic +************ + +============ +ImageGraphic +============ +.. currentmodule:: fastplotlib + +Constructor +~~~~~~~~~~~ +.. autosummary:: + :toctree: ImageGraphic_api + + ImageGraphic + +Properties +~~~~~~~~~~ +.. autosummary:: + :toctree: ImageGraphic_api + + ImageGraphic.children + ImageGraphic.position + ImageGraphic.position_x + ImageGraphic.position_y + ImageGraphic.position_z + ImageGraphic.visible + ImageGraphic.world_object + +Methods +~~~~~~~ +.. autosummary:: + :toctree: ImageGraphic_api + + ImageGraphic.add_linear_region_selector + ImageGraphic.add_linear_selector + ImageGraphic.link + diff --git a/docs/source/api/graphics/LineCollection.rst b/docs/source/api/graphics/LineCollection.rst new file mode 100644 index 000000000..003ad2897 --- /dev/null +++ b/docs/source/api/graphics/LineCollection.rst @@ -0,0 +1,44 @@ +.. _api.LineCollection: + +LineCollection +************** + +============== +LineCollection +============== +.. currentmodule:: fastplotlib + +Constructor +~~~~~~~~~~~ +.. autosummary:: + :toctree: LineCollection_api + + LineCollection + +Properties +~~~~~~~~~~ +.. autosummary:: + :toctree: LineCollection_api + + LineCollection.children + LineCollection.cmap + LineCollection.cmap_values + LineCollection.graphics + LineCollection.position + LineCollection.position_x + LineCollection.position_y + LineCollection.position_z + LineCollection.visible + LineCollection.world_object + +Methods +~~~~~~~ +.. autosummary:: + :toctree: LineCollection_api + + LineCollection.add_graphic + LineCollection.add_linear_region_selector + LineCollection.add_linear_selector + LineCollection.link + LineCollection.remove_graphic + diff --git a/docs/source/api/graphics/LineGraphic.rst b/docs/source/api/graphics/LineGraphic.rst new file mode 100644 index 000000000..75af2c4fe --- /dev/null +++ b/docs/source/api/graphics/LineGraphic.rst @@ -0,0 +1,39 @@ +.. _api.LineGraphic: + +LineGraphic +*********** + +=========== +LineGraphic +=========== +.. currentmodule:: fastplotlib + +Constructor +~~~~~~~~~~~ +.. autosummary:: + :toctree: LineGraphic_api + + LineGraphic + +Properties +~~~~~~~~~~ +.. autosummary:: + :toctree: LineGraphic_api + + LineGraphic.children + LineGraphic.position + LineGraphic.position_x + LineGraphic.position_y + LineGraphic.position_z + LineGraphic.visible + LineGraphic.world_object + +Methods +~~~~~~~ +.. autosummary:: + :toctree: LineGraphic_api + + LineGraphic.add_linear_region_selector + LineGraphic.add_linear_selector + LineGraphic.link + diff --git a/docs/source/api/graphics/LineStack.rst b/docs/source/api/graphics/LineStack.rst new file mode 100644 index 000000000..6104d0f74 --- /dev/null +++ b/docs/source/api/graphics/LineStack.rst @@ -0,0 +1,44 @@ +.. _api.LineStack: + +LineStack +********* + +========= +LineStack +========= +.. currentmodule:: fastplotlib + +Constructor +~~~~~~~~~~~ +.. autosummary:: + :toctree: LineStack_api + + LineStack + +Properties +~~~~~~~~~~ +.. autosummary:: + :toctree: LineStack_api + + LineStack.children + LineStack.cmap + LineStack.cmap_values + LineStack.graphics + LineStack.position + LineStack.position_x + LineStack.position_y + LineStack.position_z + LineStack.visible + LineStack.world_object + +Methods +~~~~~~~ +.. autosummary:: + :toctree: LineStack_api + + LineStack.add_graphic + LineStack.add_linear_region_selector + LineStack.add_linear_selector + LineStack.link + LineStack.remove_graphic + diff --git a/docs/source/api/graphics/ScatterGraphic.rst b/docs/source/api/graphics/ScatterGraphic.rst new file mode 100644 index 000000000..3c4bf3909 --- /dev/null +++ b/docs/source/api/graphics/ScatterGraphic.rst @@ -0,0 +1,36 @@ +.. _api.ScatterGraphic: + +ScatterGraphic +************** + +============== +ScatterGraphic +============== +.. currentmodule:: fastplotlib + +Constructor +~~~~~~~~~~~ +.. autosummary:: + :toctree: ScatterGraphic_api + + ScatterGraphic + +Properties +~~~~~~~~~~ +.. autosummary:: + :toctree: ScatterGraphic_api + + ScatterGraphic.children + ScatterGraphic.position + ScatterGraphic.position_x + ScatterGraphic.position_y + ScatterGraphic.position_z + ScatterGraphic.visible + ScatterGraphic.world_object + +Methods +~~~~~~~ +.. autosummary:: + :toctree: ScatterGraphic_api + + diff --git a/docs/source/api/graphics/TextGraphic.rst b/docs/source/api/graphics/TextGraphic.rst new file mode 100644 index 000000000..c83c108f6 --- /dev/null +++ b/docs/source/api/graphics/TextGraphic.rst @@ -0,0 +1,42 @@ +.. _api.TextGraphic: + +TextGraphic +*********** + +=========== +TextGraphic +=========== +.. currentmodule:: fastplotlib + +Constructor +~~~~~~~~~~~ +.. autosummary:: + :toctree: TextGraphic_api + + TextGraphic + +Properties +~~~~~~~~~~ +.. autosummary:: + :toctree: TextGraphic_api + + TextGraphic.children + TextGraphic.position + TextGraphic.position_x + TextGraphic.position_y + TextGraphic.position_z + TextGraphic.visible + TextGraphic.world_object + +Methods +~~~~~~~ +.. autosummary:: + :toctree: TextGraphic_api + + TextGraphic.update_face_color + TextGraphic.update_outline_color + TextGraphic.update_outline_size + TextGraphic.update_position + TextGraphic.update_size + TextGraphic.update_text + diff --git a/docs/source/api/graphics/index.rst b/docs/source/api/graphics/index.rst new file mode 100644 index 000000000..611ee5833 --- /dev/null +++ b/docs/source/api/graphics/index.rst @@ -0,0 +1,13 @@ +Graphics +******** + +.. toctree:: + :maxdepth: 1 + + ImageGraphic + ScatterGraphic + LineGraphic + HeatmapGraphic + LineCollection + LineStack + TextGraphic diff --git a/docs/source/api/gridplot.rst b/docs/source/api/gridplot.rst deleted file mode 100644 index 7e0f877c1..000000000 --- a/docs/source/api/gridplot.rst +++ /dev/null @@ -1,8 +0,0 @@ -.. _api_gridplot: - -GridPlot -######## - -.. autoclass:: fastplotlib.GridPlot - :members: - :inherited-members: diff --git a/docs/source/api/layouts/gridplot.rst b/docs/source/api/layouts/gridplot.rst new file mode 100644 index 000000000..63f1516cf --- /dev/null +++ b/docs/source/api/layouts/gridplot.rst @@ -0,0 +1,39 @@ +.. _api.GridPlot: + +GridPlot +******** + +======== +GridPlot +======== +.. currentmodule:: fastplotlib + +Constructor +~~~~~~~~~~~ +.. autosummary:: + :toctree: GridPlot_api + + GridPlot + +Properties +~~~~~~~~~~ +.. autosummary:: + :toctree: GridPlot_api + + GridPlot.canvas + GridPlot.renderer + +Methods +~~~~~~~ +.. autosummary:: + :toctree: GridPlot_api + + GridPlot.add_animations + GridPlot.clear + GridPlot.close + GridPlot.record_start + GridPlot.record_stop + GridPlot.remove_animation + GridPlot.render + GridPlot.show + diff --git a/docs/source/api/layouts/plot.rst b/docs/source/api/layouts/plot.rst new file mode 100644 index 000000000..a0be9287b --- /dev/null +++ b/docs/source/api/layouts/plot.rst @@ -0,0 +1,70 @@ +.. _api.Plot: + +Plot +**** + +==== +Plot +==== +.. currentmodule:: fastplotlib + +Constructor +~~~~~~~~~~~ +.. autosummary:: + :toctree: Plot_api + + Plot + +Properties +~~~~~~~~~~ +.. autosummary:: + :toctree: Plot_api + + Plot.camera + Plot.canvas + Plot.controller + Plot.docks + Plot.graphics + Plot.name + Plot.parent + Plot.position + Plot.renderer + Plot.scene + Plot.selectors + Plot.viewport + +Methods +~~~~~~~ +.. autosummary:: + :toctree: Plot_api + + Plot.add_animations + Plot.add_graphic + Plot.add_heatmap + Plot.add_image + Plot.add_line + Plot.add_line_collection + Plot.add_line_stack + Plot.add_scatter + Plot.add_text + Plot.auto_scale + Plot.center_graphic + Plot.center_scene + Plot.center_title + Plot.clear + Plot.close + Plot.delete_graphic + Plot.get_rect + Plot.insert_graphic + Plot.map_screen_to_world + Plot.record_start + Plot.record_stop + Plot.remove_animation + Plot.remove_graphic + Plot.render + Plot.set_axes_visibility + Plot.set_grid_visibility + Plot.set_title + Plot.set_viewport_rect + Plot.show + diff --git a/docs/source/api/layouts/subplot.rst b/docs/source/api/layouts/subplot.rst new file mode 100644 index 000000000..c61c46e05 --- /dev/null +++ b/docs/source/api/layouts/subplot.rst @@ -0,0 +1,66 @@ +.. _api.Subplot: + +Subplot +******* + +======= +Subplot +======= +.. currentmodule:: fastplotlib.layouts._subplot + +Constructor +~~~~~~~~~~~ +.. autosummary:: + :toctree: Subplot_api + + Subplot + +Properties +~~~~~~~~~~ +.. autosummary:: + :toctree: Subplot_api + + Subplot.camera + Subplot.canvas + Subplot.controller + Subplot.docks + Subplot.graphics + Subplot.name + Subplot.parent + Subplot.position + Subplot.renderer + Subplot.scene + Subplot.selectors + Subplot.viewport + +Methods +~~~~~~~ +.. autosummary:: + :toctree: Subplot_api + + Subplot.add_animations + Subplot.add_graphic + Subplot.add_heatmap + Subplot.add_image + Subplot.add_line + Subplot.add_line_collection + Subplot.add_line_stack + Subplot.add_scatter + Subplot.add_text + Subplot.auto_scale + Subplot.center_graphic + Subplot.center_scene + Subplot.center_title + Subplot.clear + Subplot.delete_graphic + Subplot.get_rect + Subplot.insert_graphic + Subplot.map_screen_to_world + Subplot.remove_animation + Subplot.remove_graphic + Subplot.render + Subplot.set_axes_visibility + Subplot.set_grid_visibility + Subplot.set_title + Subplot.set_viewport_rect + diff --git a/docs/source/api/plot.rst b/docs/source/api/plot.rst deleted file mode 100644 index 6b3ecb188..000000000 --- a/docs/source/api/plot.rst +++ /dev/null @@ -1,9 +0,0 @@ -.. _api_plot: - -Plot -#### - -.. autoclass:: fastplotlib.Plot - :members: - :inherited-members: - diff --git a/docs/source/api/selectors.rst b/docs/source/api/selectors.rst deleted file mode 100644 index c43f936bd..000000000 --- a/docs/source/api/selectors.rst +++ /dev/null @@ -1,15 +0,0 @@ -.. _api_selectors: - -Selectors -********* - -Linear -###### - -.. autoclass:: fastplotlib.graphics.selectors.LinearSelector - :members: - :inherited-members: - -.. autoclass:: fastplotlib.graphics.selectors.LinearRegionSelector - :members: - :inherited-members: diff --git a/docs/source/api/selectors/LinearRegionSelector.rst b/docs/source/api/selectors/LinearRegionSelector.rst new file mode 100644 index 000000000..e1824cfc8 --- /dev/null +++ b/docs/source/api/selectors/LinearRegionSelector.rst @@ -0,0 +1,39 @@ +.. _api.LinearRegionSelector: + +LinearRegionSelector +******************** + +==================== +LinearRegionSelector +==================== +.. currentmodule:: fastplotlib + +Constructor +~~~~~~~~~~~ +.. autosummary:: + :toctree: LinearRegionSelector_api + + LinearRegionSelector + +Properties +~~~~~~~~~~ +.. autosummary:: + :toctree: LinearRegionSelector_api + + LinearRegionSelector.children + LinearRegionSelector.position + LinearRegionSelector.position_x + LinearRegionSelector.position_y + LinearRegionSelector.position_z + LinearRegionSelector.visible + LinearRegionSelector.world_object + +Methods +~~~~~~~ +.. autosummary:: + :toctree: LinearRegionSelector_api + + LinearRegionSelector.get_selected_data + LinearRegionSelector.get_selected_index + LinearRegionSelector.get_selected_indices + diff --git a/docs/source/api/selectors/LinearSelector.rst b/docs/source/api/selectors/LinearSelector.rst new file mode 100644 index 000000000..2c30579f1 --- /dev/null +++ b/docs/source/api/selectors/LinearSelector.rst @@ -0,0 +1,40 @@ +.. _api.LinearSelector: + +LinearSelector +************** + +============== +LinearSelector +============== +.. currentmodule:: fastplotlib + +Constructor +~~~~~~~~~~~ +.. autosummary:: + :toctree: LinearSelector_api + + LinearSelector + +Properties +~~~~~~~~~~ +.. autosummary:: + :toctree: LinearSelector_api + + LinearSelector.children + LinearSelector.position + LinearSelector.position_x + LinearSelector.position_y + LinearSelector.position_z + LinearSelector.visible + LinearSelector.world_object + +Methods +~~~~~~~ +.. autosummary:: + :toctree: LinearSelector_api + + LinearSelector.get_selected_data + LinearSelector.get_selected_index + LinearSelector.get_selected_indices + LinearSelector.make_ipywidget_slider + diff --git a/docs/source/api/selectors/Synchronizer.rst b/docs/source/api/selectors/Synchronizer.rst new file mode 100644 index 000000000..d0fa0c2a8 --- /dev/null +++ b/docs/source/api/selectors/Synchronizer.rst @@ -0,0 +1,32 @@ +.. _api.Synchronizer: + +Synchronizer +************ + +============ +Synchronizer +============ +.. currentmodule:: fastplotlib + +Constructor +~~~~~~~~~~~ +.. autosummary:: + :toctree: Synchronizer_api + + Synchronizer + +Properties +~~~~~~~~~~ +.. autosummary:: + :toctree: Synchronizer_api + + Synchronizer.selectors + +Methods +~~~~~~~ +.. autosummary:: + :toctree: Synchronizer_api + + Synchronizer.add + Synchronizer.remove + diff --git a/docs/source/api/selectors/index.rst b/docs/source/api/selectors/index.rst new file mode 100644 index 000000000..918944fd8 --- /dev/null +++ b/docs/source/api/selectors/index.rst @@ -0,0 +1,9 @@ +Selectors +********* + +.. toctree:: + :maxdepth: 1 + + LinearSelector + LinearRegionSelector + Synchronizer diff --git a/docs/source/api/subplot.rst b/docs/source/api/subplot.rst deleted file mode 100644 index b9f7a402b..000000000 --- a/docs/source/api/subplot.rst +++ /dev/null @@ -1,10 +0,0 @@ -.. _api_subplot: - -Subplot -####### - -.. note:: ``Subplot`` is NOT meant to be instantiated directly, it only exists as part of a GridPlot. - -.. autoclass:: fastplotlib.layouts._subplot.Subplot - :members: - :inherited-members: diff --git a/docs/source/api/utils.rst b/docs/source/api/utils.rst new file mode 100644 index 000000000..6222e22c6 --- /dev/null +++ b/docs/source/api/utils.rst @@ -0,0 +1,6 @@ +fastplotlib.utils +***************** + +.. currentmodule:: fastplotlib.utils +.. automodule:: fastplotlib.utils.functions + :members: diff --git a/docs/source/api/widgets.rst b/docs/source/api/widgets.rst deleted file mode 100644 index c7e621a37..000000000 --- a/docs/source/api/widgets.rst +++ /dev/null @@ -1,12 +0,0 @@ -.. _api_widgets: - -Widgets -******* - -ImageWidget -########### - -.. autoclass:: fastplotlib.widgets.image.ImageWidget - :members: - :inherited-members: - diff --git a/docs/source/api/widgets/ImageWidget.rst b/docs/source/api/widgets/ImageWidget.rst new file mode 100644 index 000000000..62ec176ce --- /dev/null +++ b/docs/source/api/widgets/ImageWidget.rst @@ -0,0 +1,41 @@ +.. _api.ImageWidget: + +ImageWidget +*********** + +=========== +ImageWidget +=========== +.. currentmodule:: fastplotlib + +Constructor +~~~~~~~~~~~ +.. autosummary:: + :toctree: ImageWidget_api + + ImageWidget + +Properties +~~~~~~~~~~ +.. autosummary:: + :toctree: ImageWidget_api + + ImageWidget.current_index + ImageWidget.data + ImageWidget.dims_order + ImageWidget.gridplot + ImageWidget.managed_graphics + ImageWidget.ndim + ImageWidget.slider_dims + ImageWidget.sliders + ImageWidget.window_funcs + +Methods +~~~~~~~ +.. autosummary:: + :toctree: ImageWidget_api + + ImageWidget.reset_vmin_vmax + ImageWidget.set_data + ImageWidget.show + diff --git a/docs/source/api/widgets/index.rst b/docs/source/api/widgets/index.rst new file mode 100644 index 000000000..5cb5299f6 --- /dev/null +++ b/docs/source/api/widgets/index.rst @@ -0,0 +1,7 @@ +Widgets +******* + +.. toctree:: + :maxdepth: 1 + + ImageWidget diff --git a/docs/source/conf.py b/docs/source/conf.py index 7be450060..77bd6be62 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -2,21 +2,31 @@ # # For the full list of built-in configuration values, see the documentation: # https://www.sphinx-doc.org/en/master/usage/configuration.html -from bs4 import BeautifulSoup -from typing import * +import fastplotlib # -- Project information ----------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information project = 'fastplotlib' -copyright = '2022, Kushal Kolar, Caitlin Lewis' +copyright = '2023, Kushal Kolar, Caitlin Lewis' author = 'Kushal Kolar, Caitlin Lewis' -release = 'v0.1.0.a6' +release = fastplotlib.__version__ # -- General configuration --------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration -extensions = ["sphinx.ext.napoleon", "sphinx.ext.autodoc"] +extensions = [ + "sphinx.ext.napoleon", + "sphinx.ext.autodoc", + "sphinx.ext.autosummary", + "sphinx.ext.intersphinx", + "sphinx.ext.viewcode", + "sphinx_copybutton", + "sphinx_design", + "nbsphinx", +] + +autosummary_generate = True templates_path = ['_templates'] exclude_patterns = [] @@ -26,58 +36,27 @@ # -- Options for HTML output ------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output -html_theme = 'pydata_sphinx_theme' -html_theme_options = {"page_sidebar_items": ["class_page_toc"]} +html_theme = "furo" html_static_path = ['_static'] +html_logo = "_static/logo.png" +html_title = f"v{release}" autodoc_member_order = 'groupwise' autoclass_content = "both" +add_module_names = False autodoc_typehints = "description" autodoc_typehints_description_target = "documented_params" -def _setup_navbar_side_toctree(app: Any): - - def add_class_toctree_function(app: Any, pagename: Any, templatename: Any, context: Any, doctree: Any): - def get_class_toc() -> Any: - soup = BeautifulSoup(context["body"], "html.parser") - - matches = soup.find_all('dl') - if matches is None or len(matches) == 0: - return "" - items = [] - deeper_depth = matches[0].find('dt').get('id').count(".") - for match in matches: - match_dt = match.find('dt') - if match_dt is not None and match_dt.get('id') is not None: - current_title = match_dt.get('id') - current_depth = match_dt.get('id').count(".") - current_link = match.find(class_="headerlink") - if current_link is not None: - if deeper_depth > current_depth: - deeper_depth = current_depth - if deeper_depth == current_depth: - items.append({ - "title": current_title.split('.')[-1], - "link": current_link["href"], - "attributes_and_methods": [] - }) - if deeper_depth < current_depth: - items[-1]["attributes_and_methods"].append( - { - "title": current_title.split('.')[-1], - "link": current_link["href"], - } - ) - return items - context["get_class_toc"] = get_class_toc - - app.connect("html-page-context", add_class_toctree_function) - - -def setup(app: Any): - for setup_function in [ - _setup_navbar_side_toctree, - ]: - setup_function(app) +intersphinx_mapping = { + 'python': ('https://docs.python.org/3', None), + 'numpy': ('https://numpy.org/doc/stable/', None), + 'pygfx': ('https://pygfx.readthedocs.io/en/latest', None) +} + +html_theme_options = { + "source_repository": "https://github.com/kushalkolar/fastplotlib", + "source_branch": "master", + "source_directory": "docs/", +} diff --git a/docs/source/fastplotlib_banner.xcf b/docs/source/fastplotlib_banner.xcf new file mode 100644 index 000000000..e632ed0d7 Binary files /dev/null and b/docs/source/fastplotlib_banner.xcf differ diff --git a/docs/source/fastplotlib_logo.xcf b/docs/source/fastplotlib_logo.xcf new file mode 100644 index 000000000..f80b3e1b5 Binary files /dev/null and b/docs/source/fastplotlib_logo.xcf differ diff --git a/docs/source/generate_api.py b/docs/source/generate_api.py new file mode 100644 index 000000000..05e8b0f1c --- /dev/null +++ b/docs/source/generate_api.py @@ -0,0 +1,295 @@ +from typing import * +import inspect +from pathlib import Path +import os + +import fastplotlib +from fastplotlib.layouts._subplot import Subplot +from fastplotlib import graphics +from fastplotlib.graphics import _features, selectors +from fastplotlib import widgets +from fastplotlib import utils + + +current_dir = Path(__file__).parent.resolve() + +API_DIR = current_dir.joinpath("api") +LAYOUTS_DIR = API_DIR.joinpath("layouts") +GRAPHICS_DIR = API_DIR.joinpath("graphics") +GRAPHIC_FEATURES_DIR = API_DIR.joinpath("graphic_features") +SELECTORS_DIR = API_DIR.joinpath("selectors") +WIDGETS_DIR = API_DIR.joinpath("widgets") + +doc_sources = [ + API_DIR, + LAYOUTS_DIR, + GRAPHICS_DIR, + GRAPHIC_FEATURES_DIR, + SELECTORS_DIR, + WIDGETS_DIR +] + +for source_dir in doc_sources: + os.makedirs(source_dir, exist_ok=True) + + +def get_public_members(cls) -> Tuple[List[str], List[str]]: + """ + Returns (public_methods, public_properties) + + Parameters + ---------- + cls + + Returns + ------- + + """ + methods = list() + properties = list() + for member in inspect.getmembers(cls): + # only document public methods + if member[0].startswith("_"): + continue + + if callable(member[1]): + methods.append(member[0]) + elif isinstance(member[1], property): + properties.append(member[0]) + + return methods, properties + + +def generate_class( + cls: type, + module: str, +): + name = cls.__name__ + methods, properties = get_public_members(cls) + methods = [ + f"{name}.{m}" for m in methods + ] + + properties = [ + f"{name}.{p}" for p in properties + ] + + underline = "=" * len(name) + + methods_str = "\n ".join([""] + methods) + properties_str = "\n ".join([""] + properties) + + out = ( + f"{underline}\n" + f"{name}\n" + f"{underline}\n" + f".. currentmodule:: {module}\n" + f"\n" + f"Constructor\n" + f"~~~~~~~~~~~\n" + f".. autosummary::\n" + f" :toctree: {name}_api\n" + f"\n" + f" {name}\n" + f"\n" + f"Properties\n" + f"~~~~~~~~~~\n" + f".. autosummary::\n" + f" :toctree: {name}_api\n" + f"{properties_str}\n" + f"\n" + f"Methods\n" + f"~~~~~~~\n" + f".. autosummary::\n" + f" :toctree: {name}_api\n" + f"{methods_str}\n" + f"\n" + ) + + return out + + +def generate_functions_module(module, name: str): + underline = "*" * len(name) + out = ( + f"{name}\n" + f"{underline}\n" + f"\n" + f".. currentmodule:: {name}\n" + f".. automodule:: {module.__name__}\n" + f" :members:\n" + ) + + return out + + +def generate_page( + page_name: str, + modules: List[str], + classes: List[type], + source_path: Path, +): + page_name_underline = "*" * len(page_name) + with open(source_path, "w") as f: + f.write( + f".. _api.{page_name}:\n" + f"\n" + f"{page_name}\n" + f"{page_name_underline}\n" + f"\n" + ) + + for cls, module in zip(classes, modules): + to_write = generate_class(cls, module) + f.write(to_write) + + +def main(): + generate_page( + page_name="Plot", + classes=[fastplotlib.Plot], + modules=["fastplotlib"], + source_path=LAYOUTS_DIR.joinpath("plot.rst") + ) + + generate_page( + page_name="GridPlot", + classes=[fastplotlib.GridPlot], + modules=["fastplotlib"], + source_path=LAYOUTS_DIR.joinpath("gridplot.rst") + ) + + generate_page( + page_name="Subplot", + classes=[Subplot], + modules=["fastplotlib.layouts._subplot"], + source_path=LAYOUTS_DIR.joinpath("subplot.rst") + ) + + # the rest of this is a mess and can be refactored later + + graphic_classes = [ + getattr(graphics, g) for g in graphics.__all__ + ] + + graphic_class_names = [ + g.__name__ for g in graphic_classes + ] + + graphic_class_names_str = "\n ".join([""] + graphic_class_names) + + # graphic classes index file + with open(GRAPHICS_DIR.joinpath("index.rst"), "w") as f: + f.write( + f"Graphics\n" + f"********\n" + f"\n" + f".. toctree::\n" + f" :maxdepth: 1\n" + f"{graphic_class_names_str}\n" + ) + + for graphic_cls in graphic_classes: + generate_page( + page_name=graphic_cls.__name__, + classes=[graphic_cls], + modules=["fastplotlib"], + source_path=GRAPHICS_DIR.joinpath(f"{graphic_cls.__name__}.rst") + ) + ############################################################################## + + feature_classes = [ + getattr(_features, f) for f in _features.__all__ + ] + + feature_class_names = [ + f.__name__ for f in feature_classes + ] + + feature_class_names_str = "\n ".join([""] + feature_class_names) + + with open(GRAPHIC_FEATURES_DIR.joinpath("index.rst"), "w") as f: + f.write( + f"Graphic Features\n" + f"****************\n" + f"\n" + f".. toctree::\n" + f" :maxdepth: 1\n" + f"{feature_class_names_str}\n" + ) + + for feature_cls in feature_classes: + generate_page( + page_name=feature_cls.__name__, + classes=[feature_cls], + modules=["fastplotlib.graphics._features"], + source_path=GRAPHIC_FEATURES_DIR.joinpath(f"{feature_cls.__name__}.rst") + ) + ############################################################################## + + selector_classes = [ + getattr(selectors, s) for s in selectors.__all__ + ] + + selector_class_names = [ + s.__name__ for s in selector_classes + ] + + selector_class_names_str = "\n ".join([""] + selector_class_names) + + with open(SELECTORS_DIR.joinpath("index.rst"), "w") as f: + f.write( + f"Selectors\n" + f"*********\n" + f"\n" + f".. toctree::\n" + f" :maxdepth: 1\n" + f"{selector_class_names_str}\n" + ) + + for selector_cls in selector_classes: + generate_page( + page_name=selector_cls.__name__, + classes=[selector_cls], + modules=["fastplotlib"], + source_path=SELECTORS_DIR.joinpath(f"{selector_cls.__name__}.rst") + ) + ############################################################################## + + widget_classes = [ + getattr(widgets, w) for w in widgets.__all__ + ] + + widget_class_names = [ + w.__name__ for w in widget_classes + ] + + widget_class_names_str = "\n ".join([""] + widget_class_names) + + with open(WIDGETS_DIR.joinpath("index.rst"), "w") as f: + f.write( + f"Widgets\n" + f"*******\n" + f"\n" + f".. toctree::\n" + f" :maxdepth: 1\n" + f"{widget_class_names_str}\n" + ) + + for widget_cls in widget_classes: + generate_page( + page_name=widget_cls.__name__, + classes=[widget_cls], + modules=["fastplotlib"], + source_path=WIDGETS_DIR.joinpath(f"{widget_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) + + +if __name__ == "__main__": + main() diff --git a/docs/source/index.rst b/docs/source/index.rst index 9a0723af7..7e1d3865a 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -6,34 +6,45 @@ Welcome to fastplotlib's documentation! ======================================= +.. toctree:: + :caption: Quick Start + :maxdepth: 2 + + quickstart + .. toctree:: :maxdepth: 1 - :caption: Contents: + :caption: API - Plot - Subplot - Gridplot - Graphics - Graphic Features - Selectors - Widgets + Plot + Gridplot + Subplot + Graphics + Graphic Features + Selectors + Widgets + Utils Summary ======= -``fastplotlib`` is a fast plotting library built using ``pygfx`` render engine utilizing `Vulkan `_ via WGPU. We are focused on fast interactive plotting in the notebook using an expressive API. It also works within desktop application using ``glfw`` or ``Qt``. +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`` + + Installation ============ -For installation please see the instruction on the README on GitHub: +For installation please see the instructions on GitHub: -https://github.com/kushalkolar/fastplotlib +https://github.com/kushalkolar/fastplotlib#installation Contributing ============ -We're open to contributions! If you think there is any useful functionality that can be added, post an issue on the repo with your idea. Also, take a look at the `Roadmap 2023 `_ for future plans or ways in which you could contribute. +Contributions are welcome! See the contributing guide on GitHub: https://github.com/kushalkolar/fastplotlib/blob/master/CONTRIBUTING.md. + +Also take a look at the `Roadmap 2023 `_ for future plans or ways in which you could contribute. Indices and tables ================== diff --git a/docs/source/quickstart.ipynb b/docs/source/quickstart.ipynb new file mode 100644 index 000000000..aebe04b25 --- /dev/null +++ b/docs/source/quickstart.ipynb @@ -0,0 +1,1716 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "93740a09-9111-4777-ad57-173e9b80a2f0", + "metadata": { + "tags": [] + }, + "source": [ + "# Quick Start Guide 🚀\n", + "\n", + "This notebook goes through the basic components of the `fastplotlib` API, images, image updates, line plots, scatter plots, and grid plots.\n", + "\n", + "**NOTE: This quick start guide in the docs is NOT interactive. Download the examples from the repo and try them on your own computer. You can run the desktop examples directly if you have `glfw` installed, or try the notebook demos:** https://github.com/kushalkolar/fastplotlib/tree/master/examples\n", + "\n", + "It will not be possible to have live demos on the docs until someone can figure out how to get [pygfx](https://github.com/pygfx/pygfx) to work with `wgpu` in the browser, perhaps through [pyodide](https://github.com/pyodide/pyodide) or something :D." + ] + }, + { + "cell_type": "markdown", + "id": "5d21c330-89cd-49ab-9069-4e3652d4286b", + "metadata": {}, + "source": [ + "**The example images are from `imageio` so you will need to install it for this example notebook. But `imageio` is not required to use `fasptlotlib`**" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "07f064bb-025a-4794-9b05-243810edaf60", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "!pip install imageio" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5f842366-bd39-47de-ad00-723b2be707e4", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "import imageio.v3 as iio" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "fb57c3d3-f20d-4d88-9e7a-04b9309bc637", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "import fastplotlib as fpl\n", + "from ipywidgets import VBox, HBox, IntSlider\n", + "import numpy as np" + ] + }, + { + "cell_type": "markdown", + "id": "a9b386ac-9218-4f8f-97b3-f29b4201ef55", + "metadata": {}, + "source": [ + "## Images" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "237823b7-e2c0-4e2f-9ee8-e3fc2b4453c4", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "# create a `Plot` instance\n", + "plot = fpl.Plot()\n", + "\n", + "# get a grayscale image\n", + "data = iio.imread(\"imageio:camera.png\")\n", + "\n", + "# plot the image data\n", + "image_graphic = plot.add_image(data=data, name=\"sample-image\")\n", + "\n", + "# show the plot\n", + "plot.show()" + ] + }, + { + "cell_type": "markdown", + "id": "be5b408f-dd91-4e36-807a-8c22c8d7d216", + "metadata": {}, + "source": [ + "**In live notebooks or desktop applications, you can use the handle on the bottom right corner of the _canvas_ to resize it. You can also pan and zoom using your mouse!**\n", + "\n", + "By default the origin is on the bottom left, you can click the flip button to flip the y-axis, or use `plot.camera.world.scale_y *= -1`" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9b4e977e-2a7d-4e2b-aee4-cfc36767b3c6", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "plot.camera.world.scale_y *= -1" + ] + }, + { + "cell_type": "markdown", + "id": "9ba07ec1-a0cb-4461-87c6-c7b64d4a882b", + "metadata": {}, + "source": [ + "This is how you can take a snapshot of the canvas. Snapshots are shown throughout this doc page for the purposes of documentation, they are NOT necessary for real interactive usage. Download the notebooks to run live demos." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b64ba135-e753-43a9-ad1f-adcc7310792d", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "plot.canvas.snapshot()" + ] + }, + { + "cell_type": "markdown", + "id": "ac5f5e75-9aa4-441f-9a41-66c22cd53de8", + "metadata": {}, + "source": [ + "Changing graphic **\"features\"**" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d3541d1d-0819-450e-814c-588ffc8e7ed5", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "image_graphic.cmap = \"viridis\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ab544719-9187-45bd-8127-aac79eea30e4", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "plot.canvas.snapshot()" + ] + }, + { + "cell_type": "markdown", + "id": "9693cf94-11e9-46a6-a5b7-b0fbed42ad81", + "metadata": {}, + "source": [ + "### Slicing data\n", + "\n", + "**Most features, such as `data` support slicing!**\n", + "\n", + "Out image data is of shape [n_rows, n_cols]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "330a47b5-50b1-4e6a-b8ab-d55d92af2042", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "image_graphic.data().shape" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "601f46d9-7f32-4a43-9090-4674218800ea", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "image_graphic.data[::15, :] = 1\n", + "image_graphic.data[:, ::15] = 1" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3443948f-9ac9-484a-a4bf-3a06c1ce5658", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "plot.canvas.snapshot()" + ] + }, + { + "cell_type": "markdown", + "id": "53125b3b-3ce2-43c5-b2e3-7cd37cec7d7d", + "metadata": {}, + "source": [ + "**Fancy indexing**" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7344cbbe-40c3-4d9e-ae75-7abe3ddaeeeb", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "image_graphic.data[data > 175] = 255" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ef113d79-5d86-4be0-868e-30f82f8ab528", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "plot.canvas.snapshot()" + ] + }, + { + "cell_type": "markdown", + "id": "4df5296e-2a18-403f-82f1-acb8eaf280e3", + "metadata": {}, + "source": [ + "Adjust vmin vmax" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "28af88d1-0518-47a4-ab73-431d6aaf9cb8", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "image_graphic.cmap.vmin = 50\n", + "image_graphic.cmap.vmax = 150" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e3dfb827-c812-447d-b413-dc15653160b1", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "plot.canvas.snapshot()" + ] + }, + { + "cell_type": "markdown", + "id": "19a1b56b-fdca-40c5-91c9-3c9486fd8a21", + "metadata": {}, + "source": [ + "**Set the entire data array again**\n", + "\n", + "Note: The shape of the new data array must match the current data shown in the Graphic." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4dc3d0e4-b128-42cd-a53e-76846fc9b8a8", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "new_data = iio.imread(\"imageio:astronaut.png\")\n", + "new_data.shape" + ] + }, + { + "cell_type": "markdown", + "id": "3bd06068-fe3f-404d-ba4a-a72a2105904f", + "metadata": {}, + "source": [ + "This is an RGB image, convert to grayscale to maintain the shape of (512, 512)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "150047a6-a6ac-442d-a468-3e0c224a2b7e", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "gray = new_data.dot([0.3, 0.6, 0.1])\n", + "gray.shape" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "bf24576b-d336-4754-9992-9649ccaa4d1e", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "image_graphic.data = gray" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "67d810c2-4020-4769-a5ba-0d4a972ee243", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "plot.canvas.snapshot()" + ] + }, + { + "cell_type": "markdown", + "id": "2fe82654-e554-4be6-92a0-ecdee0ef8519", + "metadata": {}, + "source": [ + "reset vmin vmax" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "0be6e4bb-cf9a-4155-9f6a-8106e66e6132", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "image_graphic.cmap.reset_vmin_vmax()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "bd51936c-ad80-4b33-b855-23565265a430", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "plot.canvas.snapshot()" + ] + }, + { + "cell_type": "markdown", + "id": "a6c1f3fb-a3a7-4175-bd8d-bb3203740771", + "metadata": {}, + "source": [ + "### Indexing plots" + ] + }, + { + "cell_type": "markdown", + "id": "3fc38694-aca6-4f56-97ac-3435059a6be7", + "metadata": {}, + "source": [ + "**Plots are indexable and give you their graphics by name**" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8a547138-0f7d-470b-9925-8df479c3979e", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "plot" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5551861f-9860-4515-8222-2f1c6d6a3220", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "plot[\"sample-image\"]" + ] + }, + { + "cell_type": "markdown", + "id": "0c29b36e-0eb4-4bb3-a8db-add58c303ee8", + "metadata": {}, + "source": [ + "**You can also use numerical indexing on `plot.graphics`**" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ce6adbb0-078a-4e74-b189-58f860ee5df5", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "plot.graphics" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "119bd6af-c486-4378-bc23-79b1759aa3a4", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "plot.graphics[0]" + ] + }, + { + "cell_type": "markdown", + "id": "6b8e3f0d-56f8-447f-bf26-b52629d06e95", + "metadata": {}, + "source": [ + "The `Graphic` instance is also returned when you call `plot.add_`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "967c0cbd-287c-4d99-9891-9baf18f7b56a", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "image_graphic" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5da72e26-3536-47b8-839c-53452dd94f7a", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "image_graphic == plot[\"sample-image\"]" + ] + }, + { + "cell_type": "markdown", + "id": "2b5ee18b-e61b-415d-902a-688b1c9c03b8", + "metadata": {}, + "source": [ + "### RGB images\n", + "\n", + "`cmap` arguments are ignored for rgb images, but vmin vmax still works" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1f7143ec-8ee1-47d2-b017-d0a8efc69fc6", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "plot_rgb = fpl.Plot()\n", + "\n", + "plot_rgb.add_image(new_data, name=\"rgb-image\")\n", + "\n", + "plot_rgb.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7e4b5a30-4293-4ae3-87dc-06a1355bb2c7", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "plot_rgb.camera.world.scale_y *= -1" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a47b1eaf-3638-470a-88a5-0026c81d7e2b", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "plot_rgb.canvas.snapshot()" + ] + }, + { + "cell_type": "markdown", + "id": "4848a929-4f3b-46d7-921b-ebfe8de0ebb5", + "metadata": {}, + "source": [ + "vmin and vmax are still applicable to rgb images" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ffe50132-8dd0-433c-b9c6-9ead8c3d48de", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "plot_rgb[\"rgb-image\"].cmap.vmin = 100" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "161468ba-b836-4021-8d11-8dfc140b94eb", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "plot_rgb.canvas.snapshot()" + ] + }, + { + "cell_type": "markdown", + "id": "1cb03f42-1029-4b16-a16b-35447d9e2955", + "metadata": { + "tags": [] + }, + "source": [ + "## Image updates\n", + "\n", + "This examples show how you can define animation functions that run on every render cycle." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "aadd757f-6379-4f52-a709-46aa57c56216", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "# create another `Plot` instance\n", + "plot_v = fpl.Plot()\n", + "\n", + "plot.canvas.max_buffered_frames = 1\n", + "\n", + "# make some random data again\n", + "data = np.random.rand(512, 512)\n", + "\n", + "# plot the data\n", + "plot_v.add_image(data=data, name=\"random-image\")\n", + "\n", + "# a function to update the image_graphic\n", + "# a plot will pass its plot instance to the animation function as an arugment\n", + "def update_data(plot_instance):\n", + " new_data = np.random.rand(512, 512)\n", + " plot_instance[\"random-image\"].data = new_data\n", + "\n", + "#add this as an animation function\n", + "plot_v.add_animations(update_data)\n", + "\n", + "# show the plot\n", + "plot_v.show()" + ] + }, + { + "cell_type": "markdown", + "id": "b313eda1-6e6c-466f-9fd5-8b70c1d3c110", + "metadata": {}, + "source": [ + "**Share controllers across plots**\n", + "\n", + "This example creates a new plot, but it synchronizes the pan-zoom controller" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "86e70b1e-4328-4035-b992-70dff16d2a69", + "metadata": {}, + "outputs": [], + "source": [ + "plot_sync = fpl.Plot(controller=plot_v.controller)\n", + "\n", + "data = np.random.rand(512, 512)\n", + "\n", + "image_graphic_instance = plot_sync.add_image(data=data, cmap=\"viridis\")\n", + "\n", + "# you will need to define a new animation function for this graphic\n", + "def update_data_2():\n", + " new_data = np.random.rand(512, 512)\n", + " # alternatively, you can use the stored reference to the graphic as well instead of indexing the Plot\n", + " image_graphic_instance.data = new_data\n", + "\n", + "plot_sync.add_animations(update_data_2)\n", + "\n", + "plot_sync.show()" + ] + }, + { + "cell_type": "markdown", + "id": "f226c9c2-8d0e-41ab-9ab9-1ae31fd91de5", + "metadata": {}, + "source": [ + "Keeping a reference to the Graphic instance, as shown above `image_graphic_instance`, is useful if you're creating something where you need flexibility in the naming of the graphics" + ] + }, + { + "cell_type": "markdown", + "id": "d11fabb7-7c76-4e94-893d-80ed9ee3be3d", + "metadata": {}, + "source": [ + "You can also use `ipywidgets.VBox` and `HBox` to stack plots. See the `gridplot` notebooks for a proper gridplot interface for more automated subplotting\n", + "\n", + "Not shown in the docs, try the live demo for this feature" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ef9743b3-5f81-4b79-9502-fa5fca08e56d", + "metadata": {}, + "outputs": [], + "source": [ + "#VBox([plot_v.canvas, plot_sync.show()])" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "11839d95-8ff7-444c-ae13-6b072c3112c5", + "metadata": {}, + "outputs": [], + "source": [ + "#HBox([plot_v.show(), plot_sync.show()])" + ] + }, + { + "cell_type": "markdown", + "id": "e7859338-8162-408b-ac72-37e606057045", + "metadata": { + "tags": [] + }, + "source": [ + "## Line plots\n", + "\n", + "2D line plots\n", + "\n", + "This example plots a sine wave, cosine wave, and ricker wavelet and demonstrates how **Graphic Features** can be modified by slicing!" + ] + }, + { + "cell_type": "markdown", + "id": "a6fee1c2-4a24-4325-bca2-26e5a4bf6338", + "metadata": {}, + "source": [ + "Generate some data." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8e8280da-b421-43a5-a1a6-2a196a408e9a", + "metadata": {}, + "outputs": [], + "source": [ + "# linspace, create 100 evenly spaced x values from -10 to 10\n", + "xs = np.linspace(-10, 10, 100)\n", + "# sine wave\n", + "ys = np.sin(xs)\n", + "sine = np.dstack([xs, ys])[0]\n", + "\n", + "# cosine wave\n", + "ys = np.cos(xs) + 5\n", + "cosine = np.dstack([xs, ys])[0]\n", + "\n", + "# sinc function\n", + "a = 0.5\n", + "ys = np.sinc(xs) * 3 + 8\n", + "sinc = np.dstack([xs, ys])[0]" + ] + }, + { + "cell_type": "markdown", + "id": "fbb806e5-1565-4189-936c-b7cf147a59ee", + "metadata": {}, + "source": [ + "Plot all of it on the same plot. Each line plot will be an individual Graphic, you can have any combination of graphics on a plot." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "93a5d1e6-d019-4dd0-a0d1-25d1704ab7a7", + "metadata": {}, + "outputs": [], + "source": [ + "# Create a plot instance\n", + "plot_l = fpl.Plot()\n", + "\n", + "# plot sine wave, use a single color\n", + "sine_graphic = plot_l.add_line(data=sine, thickness=5, colors=\"magenta\")\n", + "\n", + "# you can also use colormaps for lines!\n", + "cosine_graphic = plot_l.add_line(data=cosine, thickness=12, cmap=\"autumn\")\n", + "\n", + "# or a list of colors for each datapoint\n", + "colors = [\"r\"] * 25 + [\"purple\"] * 25 + [\"y\"] * 25 + [\"b\"] * 25\n", + "sinc_graphic = plot_l.add_line(data=sinc, thickness=5, colors = colors)\n", + "\n", + "plot_l.show()" + ] + }, + { + "cell_type": "markdown", + "id": "22dde600-0f56-4370-b017-c8f23a6c01aa", + "metadata": {}, + "source": [ + "\"stretching\" the camera, useful for large timeseries data\n", + "\n", + "Set `maintain_aspect = False` on a camera, and then use the right mouse button and move the mouse to stretch and squeeze the view!\n", + "\n", + "You can also click the **`1:1`** button to toggle this." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2695f023-f6ce-4e26-8f96-4fbed5510d1d", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "plot_l.camera.maintain_aspect = False" + ] + }, + { + "cell_type": "markdown", + "id": "1651e965-f750-47ac-bf53-c23dae84cc98", + "metadata": {}, + "source": [ + "reset the plot area" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ba50a6ed-0f1b-4795-91dd-a7c3e40b8e3c", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "plot_l.auto_scale(maintain_aspect=True)" + ] + }, + { + "cell_type": "markdown", + "id": "dcd68796-c190-4c3f-8519-d73b98ff6367", + "metadata": {}, + "source": [ + "Graphic features support slicing! :D " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "cb0d13ed-ef07-46ff-b19e-eeca4c831037", + "metadata": {}, + "outputs": [], + "source": [ + "# indexing of colors\n", + "cosine_graphic.colors[:15] = \"magenta\"\n", + "cosine_graphic.colors[90:] = \"red\"\n", + "cosine_graphic.colors[60] = \"w\"\n", + "\n", + "# indexing to assign colormaps to entire lines or segments\n", + "sinc_graphic.cmap[10:50] = \"gray\"\n", + "sine_graphic.cmap = \"seismic\"\n", + "\n", + "# more complex indexing, set the blue value directly from an array\n", + "cosine_graphic.colors[65:90, 0] = np.linspace(0, 1, 90-65)" + ] + }, + { + "cell_type": "markdown", + "id": "bfe14ed3-e81f-4058-96a7-e2720b6d2f45", + "metadata": {}, + "source": [ + "Make a snapshot of the canvas after slicing" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a061a888-d732-406e-a9c2-8cc632fbc368", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "plot_l.canvas.snapshot()" + ] + }, + { + "cell_type": "markdown", + "id": "c9689887-cdf3-4a4d-948f-7efdb09bde4e", + "metadata": {}, + "source": [ + "**You can capture changes to a graphic feature as events**" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "cfa001f6-c640-4f91-beb0-c19b030e503f", + "metadata": {}, + "outputs": [], + "source": [ + "def callback_func(event_data):\n", + " print(event_data)\n", + "\n", + "# Will print event data when the color changes\n", + "cosine_graphic.colors.add_event_handler(callback_func)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "bb8a0f95-0063-4cd4-a117-e3d62c6e120d", + "metadata": {}, + "outputs": [], + "source": [ + "# more complex indexing of colors\n", + "# from point 15 - 30, set every 3rd point as \"cyan\"\n", + "cosine_graphic.colors[15:50:3] = \"cyan\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3da9a43b-35bd-4b56-9cc7-967536aac967", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "plot_l.canvas.snapshot()" + ] + }, + { + "cell_type": "markdown", + "id": "c29f81f9-601b-49f4-b20c-575c56e58026", + "metadata": {}, + "source": [ + "Graphic `data` is also indexable" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d1a4314b-5723-43c7-94a0-b4cbb0e44d60", + "metadata": {}, + "outputs": [], + "source": [ + "cosine_graphic.data[10:50:5, :2] = sine[10:50:5]\n", + "cosine_graphic.data[90:, 1] = 7" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "682db47b-8c7a-4934-9be4-2067e9fb12d5", + "metadata": {}, + "outputs": [], + "source": [ + "cosine_graphic.data[0] = np.array([[-10, 0, 0]])" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f779cba0-7ee2-4795-8da8-9a9593d3893e", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "plot_l.canvas.snapshot()" + ] + }, + { + "cell_type": "markdown", + "id": "3f6d264b-1b03-407e-9d83-cd6cfb02e706", + "metadata": {}, + "source": [ + "Toggle the presence of a graphic within the scene" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "fcba75b7-9a1e-4aae-9dec-715f7f7456c3", + "metadata": {}, + "outputs": [], + "source": [ + "sinc_graphic.present = False" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a5e22d0f-a244-47e2-9a2d-1eaf79eda1d9", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "plot_l.canvas.snapshot()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "763b9943-53a4-4e2a-b47a-4e9e5c9d7be3", + "metadata": {}, + "outputs": [], + "source": [ + "sinc_graphic.present = True" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b22a8660-26b3-4c73-b87a-df9d7cb4353a", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "plot_l.canvas.snapshot()" + ] + }, + { + "cell_type": "markdown", + "id": "86f4e535-ce88-415a-b8d2-53612a2de7b9", + "metadata": {}, + "source": [ + "You can create callbacks to `present` too, for example to re-scale the plot w.r.t. graphics that are present in the scene" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "64a20a16-75a5-4772-a849-630ade9be4ff", + "metadata": {}, + "outputs": [], + "source": [ + "sinc_graphic.present.add_event_handler(plot_l.auto_scale)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "fb093046-c94c-4085-86b4-8cd85cb638ff", + "metadata": {}, + "outputs": [], + "source": [ + "sinc_graphic.present = False" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f9dd6a54-3460-4fb7-bffb-82fd9288902f", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "plot_l.canvas.snapshot()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f05981c3-c768-4631-ae62-6a8407b20c4c", + "metadata": {}, + "outputs": [], + "source": [ + "sinc_graphic.present = True" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "cb5bf73e-b015-4b4f-82a0-c3ae8cc39ef7", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "plot_l.canvas.snapshot()" + ] + }, + { + "cell_type": "markdown", + "id": "05f93e93-283b-45d8-ab31-8d15a7671dd2", + "metadata": {}, + "source": [ + "You can set the z-positions of graphics to have them appear under or over other graphics" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6bb33406-5bef-455b-86ea-358a7d3ffa94", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "img = np.random.rand(20, 100)\n", + "\n", + "plot_l.add_image(img, name=\"image\", cmap=\"gray\")\n", + "\n", + "# z axix position -1 so it is below all the lines\n", + "plot_l[\"image\"].position_z = -1\n", + "plot_l[\"image\"].position_x = -50" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5b586a89-ca3e-4e88-a801-bdd665384f59", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "plot_l.canvas.snapshot()" + ] + }, + { + "cell_type": "markdown", + "id": "2c90862e-2f2a-451f-a468-0cf6b857e87a", + "metadata": {}, + "source": [ + "### 3D line plot" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9c51229f-13a2-4653-bff3-15d43ddbca7b", + "metadata": {}, + "outputs": [], + "source": [ + "# just set the camera as \"3d\", the rest is basically the same :D \n", + "plot_l3d = fpl.Plot(camera='3d')\n", + "\n", + "# create a spiral\n", + "phi = np.linspace(0, 30, 200)\n", + "\n", + "xs = phi * np.cos(phi)\n", + "ys = phi * np.sin(phi)\n", + "zs = phi\n", + "\n", + "# use 3D data\n", + "# note: you usually mix 3D and 2D graphics on the same plot\n", + "spiral = np.dstack([xs, ys, zs])[0]\n", + "\n", + "plot_l3d.add_line(data=spiral, thickness=2, cmap='winter')\n", + "\n", + "plot_l3d.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "28eb7014-4773-4a34-8bfc-bd3a46429012", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "plot_l3d.auto_scale(maintain_aspect=True)" + ] + }, + { + "cell_type": "markdown", + "id": "a202b3d0-2a0b-450a-93d4-76d0a1129d1d", + "metadata": {}, + "source": [ + "## Scatter plots\n", + "\n", + "Plot tens of thousands or millions of points\n", + "\n", + "There might be a small delay for a few seconds before the plot shows, this is due to shaders being compiled and a few other things. The plot should be very fast and responsive once it is displayed and future modifications should also be fast!" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "39252df5-9ae5-4132-b97b-2785c5fa92ea", + "metadata": {}, + "outputs": [], + "source": [ + "# create a random distribution\n", + "# only 1,000 points shown here in the docs, but it can be millions\n", + "n_points = 1_000\n", + "\n", + "# if you have a good GPU go for 1.5 million points :D \n", + "# this is multiplied by 3\n", + "#n_points = 500_000\n", + "\n", + "# dimensions always have to be [n_points, xyz]\n", + "dims = (n_points, 3)\n", + "\n", + "clouds_offset = 15\n", + "\n", + "# create some random clouds\n", + "normal = np.random.normal(size=dims, scale=5)\n", + "# stack the data into a single array\n", + "cloud = np.vstack(\n", + " [\n", + " normal - clouds_offset,\n", + " normal,\n", + " normal + clouds_offset,\n", + " ]\n", + ")\n", + "\n", + "# color each of them separately\n", + "colors = [\"yellow\"] * n_points + [\"cyan\"] * n_points + [\"magenta\"] * n_points\n", + "\n", + "# create plot\n", + "plot_s = fpl.Plot()\n", + "\n", + "# use an alpha value since this will be a lot of points\n", + "scatter_graphic = plot_s.add_scatter(data=cloud, sizes=3, colors=colors, alpha=0.7)\n", + "\n", + "plot_s.show()" + ] + }, + { + "cell_type": "markdown", + "id": "b6e4a704-ee6b-4316-956e-acb4dcc1c6f2", + "metadata": {}, + "source": [ + "**Scatter graphic features work similarly to line graphic**" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8fa46ec0-8680-44f5-894c-559de3145932", + "metadata": {}, + "outputs": [], + "source": [ + "# half of the first cloud's points to red\n", + "scatter_graphic.colors[:n_points:2] = \"r\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "293a4793-44b9-4d18-ae6a-68e7c6f91acc", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "plot_s.canvas.snapshot()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e4dc71e4-5144-436f-a464-f2a29eee8f0b", + "metadata": {}, + "outputs": [], + "source": [ + "# set the green value directly\n", + "scatter_graphic.colors[n_points:n_points * 2, 1] = 0.3" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5ea7852d-fdae-401b-83b6-b6cfd975f64f", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "plot_s.canvas.snapshot()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5b637a29-cd5e-4011-ab81-3f91490d9ecd", + "metadata": {}, + "outputs": [], + "source": [ + "# set color values directly using an array\n", + "scatter_graphic.colors[n_points * 2:] = np.repeat([[1, 1, 0, 0.5]], n_points, axis=0)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "02c19f51-6436-4601-976e-04326df0de81", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "plot_s.canvas.snapshot()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a4084fce-78a2-48b3-9a0d-7b57c165c3c1", + "metadata": {}, + "outputs": [], + "source": [ + "# change the data, change y-values\n", + "scatter_graphic.data[n_points:n_points * 2, 1] += 15" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2ec43f58-4710-4603-9358-682c4af3f701", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "plot_s.canvas.snapshot()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f486083e-7c58-4255-ae1a-3fe5d9bfaeed", + "metadata": {}, + "outputs": [], + "source": [ + "# set x values directly but using an array\n", + "scatter_graphic.data[n_points:n_points * 2, 0] = np.linspace(-40, 0, n_points)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6bcb3bc3-4b75-4bbc-b8ca-f8a3219ec3d7", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "plot_s.canvas.snapshot()" + ] + }, + { + "cell_type": "markdown", + "id": "d9e554de-c436-4684-a46a-ce8a33d409ac", + "metadata": {}, + "source": [ + "## ipywidget layouts\n", + "\n", + "This just plots everything from these examples in a single output cell" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "01a6f70b-c81b-4ee5-8a6b-d979b87227eb", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "# row1 = HBox([plot.show(), plot_v.show(), plot_sync.show()])\n", + "# row2 = HBox([plot_l.show(), plot_l3d.show(), plot_s.show()])\n", + "\n", + "# VBox([row1, row2])" + ] + }, + { + "cell_type": "markdown", + "id": "a26c0063-b7e0-4f36-bb14-db06bafa31aa", + "metadata": {}, + "source": [ + "## Gridplot\n", + "\n", + "Subplots within a `GridPlot` behave the same as simple `Plot` instances! \n", + "\n", + "💡 `Plot` is actually a subclass of `Subplot`!" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6b7e1129-ae8e-4a0f-82dc-bd8fb65871fc", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "# GridPlot of shape 2 x 3 with all controllers synced\n", + "grid_plot = fpl.GridPlot(shape=(2, 3), controllers=\"sync\")\n", + "\n", + "# Make a random image graphic for each subplot\n", + "for subplot in grid_plot:\n", + " # create image data\n", + " data = np.random.rand(512, 512)\n", + " # add an image to the subplot\n", + " subplot.add_image(data, name=\"rand-img\")\n", + "\n", + "# Define a function to update the image graphics with new data\n", + "# add_animations will pass the gridplot to the animation function\n", + "def update_data(gp):\n", + " for sp in gp:\n", + " new_data = np.random.rand(512, 512)\n", + " # index the image graphic by name and set the data\n", + " sp[\"rand-img\"].data = new_data\n", + " \n", + "# add the animation function\n", + "grid_plot.add_animations(update_data)\n", + "\n", + "# show the gridplot \n", + "grid_plot.show()" + ] + }, + { + "cell_type": "markdown", + "id": "f4f71c34-3925-442f-bd76-60dd57d09f48", + "metadata": {}, + "source": [ + "### Slicing GridPlot" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d8194c9e-9a99-4d4a-8984-a4cfcab0c42c", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "# positional indexing\n", + "# row 0 and col 0\n", + "grid_plot[0, 0]" + ] + }, + { + "cell_type": "markdown", + "id": "d626640f-bc93-4883-9bf4-47b825bbc663", + "metadata": {}, + "source": [ + "You can get the graphics within a subplot, just like with simple `Plot`" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "bffec80c-e81b-4945-85a2-c2c5e8395677", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "grid_plot[0, 1].graphics" + ] + }, + { + "cell_type": "markdown", + "id": "a4e3184f-c86a-4a7e-b803-31632cc163b0", + "metadata": {}, + "source": [ + "and change their properties" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "04b616fb-6644-42ba-8683-0589ce7d165e", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "grid_plot[0, 1].graphics[0].vmax = 0.5" + ] + }, + { + "cell_type": "markdown", + "id": "28f7362c-d1b9-43ef-85c5-4d68f70f459c", + "metadata": {}, + "source": [ + "more slicing with `GridPlot`" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "920e6365-bb50-4882-9b0d-8367dc485360", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "# you can give subplots human-readable string names\n", + "grid_plot[0, 2].name = \"top-right-plot\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "73300d2c-3e70-43ad-b5a2-40341b701ac8", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "grid_plot[\"top-right-plot\"]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "834d9905-35e9-4711-9375-5b1828c80ee2", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "# view its position\n", + "grid_plot[\"top-right-plot\"].position" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9aa61efa-c6a5-4611-a03b-1b8da66b19f0", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "# these are really the same\n", + "grid_plot[\"top-right-plot\"] is grid_plot[0, 2]" + ] + }, + { + "cell_type": "markdown", + "id": "28c8b145-86cb-4445-92be-b7537a87f7ca", + "metadata": {}, + "source": [ + "Indexing with subplot name and graphic name" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2b7b73a3-5335-4bd5-bbef-c7d3cfbb3ca7", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "grid_plot[\"top-right-plot\"][\"rand-img\"].vmin = 0.5" + ] + }, + { + "cell_type": "markdown", + "id": "6a5b4368-ae4d-442c-a11f-45c70267339b", + "metadata": {}, + "source": [ + "## GridPlot customization" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "175d45a6-3351-4b75-8ff3-08797fe0a389", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "# grid with 2 rows and 3 columns\n", + "grid_shape = (2, 3)\n", + "\n", + "# pan-zoom controllers for each view\n", + "# views are synced if they have the \n", + "# same controller ID\n", + "controllers = [\n", + " [0, 3, 1], # id each controller with an integer\n", + " [2, 2, 3]\n", + "]\n", + "\n", + "\n", + "# you can give string names for each subplot within the gridplot\n", + "names = [\n", + " [\"subplot0\", \"subplot1\", \"subplot2\"],\n", + " [\"subplot3\", \"subplot4\", \"subplot5\"]\n", + "]\n", + "\n", + "# Create the grid plot\n", + "grid_plot = fpl.GridPlot(\n", + " shape=grid_shape,\n", + " controllers=controllers,\n", + " names=names,\n", + ")\n", + "\n", + "\n", + "# Make a random image graphic for each subplot\n", + "for subplot in grid_plot:\n", + " data = np.random.rand(512, 512)\n", + " # create and add an ImageGraphic\n", + " subplot.add_image(data=data, name=\"rand-image\")\n", + " \n", + "\n", + "# Define a function to update the image graphics \n", + "# with new randomly generated data\n", + "def set_random_frame(gp):\n", + " for subplot in gp:\n", + " new_data = np.random.rand(512, 512)\n", + " subplot[\"rand-image\"].data = new_data\n", + "\n", + "# add the animation\n", + "grid_plot.add_animations(set_random_frame)\n", + "grid_plot.show()" + ] + }, + { + "cell_type": "markdown", + "id": "4224f1c2-5e61-4894-8d72-0519598a3cef", + "metadata": {}, + "source": [ + "Indexing the gridplot to access subplots" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d88dd9b2-9359-42e8-9dfb-96dcbbb34b95", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "# can access subplot by name\n", + "grid_plot[\"subplot0\"]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a14df7ea-14c3-4a8a-84f2-2e2194236d9e", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "# can access subplot by index\n", + "grid_plot[0, 0]" + ] + }, + { + "cell_type": "markdown", + "id": "5f8a3427-7949-40a4-aec2-38d5d95ef156", + "metadata": {}, + "source": [ + "**subplots also support indexing!**\n", + "\n", + "this can be used to get graphics if they are named" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8c99fee0-ce46-4f18-8300-af025c9a967c", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "# can access graphic directly via name\n", + "grid_plot[\"subplot0\"][\"rand-image\"]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ed4eebb7-826d-4856-bbb8-db2de966a0c3", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "grid_plot[\"subplot0\"][\"rand-image\"].vmin = 0.6\n", + "grid_plot[\"subplot0\"][\"rand-image\"].vmax = 0.8" + ] + }, + { + "cell_type": "markdown", + "id": "ad322f6f-e7de-4eb3-a1d9-cf28701a2eae", + "metadata": {}, + "source": [ + "positional indexing also works event if subplots have string names" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "759d3966-d92b-460f-ba48-e57adabbf163", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "grid_plot[1, 0][\"rand-image\"].vim = 0.1\n", + "grid_plot[1, 0][\"rand-image\"].vmax = 0.3" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8a5753b9-ee71-4ed1-bb0d-52bdb4ea365f", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "grid_plot[1, 0][\"rand-image\"].type" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.3" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/examples/README.md b/examples/README.md deleted file mode 100644 index 1b79c879b..000000000 --- a/examples/README.md +++ /dev/null @@ -1,6 +0,0 @@ -# Examples - -**IMPORTANT NOTE: If you install `fastplotlib` and `pygfx` from `pypi`, i.e. `pip install pygfx`, you will need to use the examples from the following commit until `pygfx` publishes a new release to `pypi`: https://github.com/kushalkolar/fastplotlib/tree/f872155eb687b18e3cc9b3b720eb9e241a9f974c/examples . -The current examples will work if you installed `fastplotlib` and `pygfx` directly from github** - -Both `fastplotlib` and `pygfx` are rapidly evolving libraries, and we try to closely track `pygfx`. diff --git a/examples/buttons.ipynb b/examples/buttons.ipynb deleted file mode 100644 index b46e09d5f..000000000 --- a/examples/buttons.ipynb +++ /dev/null @@ -1,402 +0,0 @@ -{ - "cells": [ - { - "cell_type": "code", - "execution_count": 1, - "id": "6725ce7d-eea7-44f7-bedc-813e8ce5bf4f", - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "from fastplotlib import Plot, GridPlot, ImageWidget\n", - "from ipywidgets import HBox, Checkbox, Image, VBox, Layout, ToggleButton, Button, Dropdown\n", - "import numpy as np" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "id": "33bf59c4-14e5-43a8-8a16-69b6859864c5", - "metadata": { - "tags": [] - }, - "outputs": [ - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "5039947949ac4d5da76e561e082da8c2", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "RFBOutputContext()" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/home/clewis7/repos/fastplotlib/fastplotlib/graphics/features/_base.py:34: UserWarning: converting float64 array to float32\n", - " warn(f\"converting {array.dtype} array to float32\")\n" - ] - }, - { - "data": { - "text/plain": [ - "" - ] - }, - "execution_count": 2, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "plot = Plot()\n", - "xs = np.linspace(-10, 10, 100)\n", - "# sine wave\n", - "ys = np.sin(xs)\n", - "sine = np.dstack([xs, ys])[0]\n", - "plot.add_line(sine)" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "id": "68ea8011-d6fd-448f-9bf6-34073164d271", - "metadata": { - "tags": [] - }, - "outputs": [ - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "5819995040a04300b5ccf10abac8b69e", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "VBox(children=(JupyterWgpuCanvas(), HBox(children=(Button(icon='expand-arrows-alt', layout=Layout(width='auto'…" - ] - }, - "execution_count": 3, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "plot.show()" - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "id": "91a31531-818b-46a2-9587-5d9ef5b59b93", - "metadata": { - "tags": [] - }, - "outputs": [ - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "2f318829d0a4419798a008c5fe2d6677", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "RFBOutputContext()" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "gp = GridPlot(\n", - " shape=(1,2),\n", - " names=[['plot1', 'plot2']])" - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "id": "e96bbda7-3693-42f2-bd52-f668f39134f6", - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "img = np.random.rand(512,512)\n", - "for subplot in gp:\n", - " subplot.add_image(data=img)" - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "id": "03b877ba-cf9c-47d9-a0e5-b3e694274a28", - "metadata": { - "tags": [] - }, - "outputs": [ - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "ff5064077bb944c6a5248f135f052668", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "VBox(children=(JupyterWgpuCanvas(), HBox(children=(Button(icon='expand-arrows-alt', layout=Layout(width='auto'…" - ] - }, - "execution_count": 6, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "gp.show()" - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "id": "36f5e040-cc58-4b0a-beb1-1f66ea02ccb9", - "metadata": { - "tags": [] - }, - "outputs": [ - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "f21de0c607b24fd281364a7bec8ad837", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "RFBOutputContext()" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "gp2 = GridPlot(shape=(1,2))" - ] - }, - { - "cell_type": "code", - "execution_count": 8, - "id": "c6753d45-a0ae-4c96-8ed5-7638c4cf24e3", - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "for subplot in gp2:\n", - " subplot.add_image(data=img)" - ] - }, - { - "cell_type": "code", - "execution_count": 9, - "id": "5a769c0f-6d95-4969-ad9d-24636fc74b18", - "metadata": { - "tags": [] - }, - "outputs": [ - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "fcb816a9aaab42b3a7a7e443607ad127", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "VBox(children=(JupyterWgpuCanvas(), HBox(children=(Button(icon='expand-arrows-alt', layout=Layout(width='auto'…" - ] - }, - "execution_count": 9, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "gp2.show()" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "id": "3958829a-1a2b-4aa2-8c9d-408dce9ccf30", - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "data = np.random.rand(500, 512, 512)" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "id": "9f60c0b0-d0ee-4ea1-b961-708aff3b91ae", - "metadata": { - "tags": [] - }, - "outputs": [ - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "433485ec12b44aa68082a93c264e613c", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "RFBOutputContext()" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/home/clewis7/repos/fastplotlib/fastplotlib/graphics/features/_base.py:34: UserWarning: converting float64 array to float32\n", - " warn(f\"converting {array.dtype} array to float32\")\n" - ] - } - ], - "source": [ - "iw = ImageWidget(data=data, vmin_vmax_sliders=True)" - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "id": "346f241c-4bd0-4f90-afd5-3fa617d96dad", - "metadata": { - "tags": [] - }, - "outputs": [ - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "a1bdd59532f240948d33ae440d61c4a5", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "VBox(children=(VBox(children=(JupyterWgpuCanvas(), HBox(children=(Button(icon='expand-arrows-alt', layout=Layo…" - ] - }, - "execution_count": 4, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "iw.show()" - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "id": "0563ddfb-8fd3-4a99-bcee-3a83ae5d0f32", - "metadata": { - "tags": [] - }, - "outputs": [ - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "79e924078e7f4cf69be71eaf12a94854", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "RFBOutputContext()" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "iw2 = ImageWidget(data=[data, data, data, data], grid_plot_kwargs={'controllers': np.array([[0, 1], [2, 3]])}, slider_dims=\"t\", vmin_vmax_sliders=True)" - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "id": "d7fb94f8-825f-4447-b161-c9dafa1a068a", - "metadata": { - "tags": [] - }, - "outputs": [ - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "bb3b7dd92b4d4f74ace4adfbca35aaf3", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "VBox(children=(VBox(children=(JupyterWgpuCanvas(), HBox(children=(Button(icon='expand-arrows-alt', layout=Layo…" - ] - }, - "execution_count": 6, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "iw2.show()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "3743219d-6702-468a-bea6-0e4c4549e9e4", - "metadata": {}, - "outputs": [], - "source": [] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "68bafb86-db5a-4681-8176-37ec72ce04a8", - "metadata": {}, - "outputs": [], - "source": [] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3 (ipykernel)", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.11.3" - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} diff --git a/examples/data/iris.npy b/examples/desktop/data/iris.npy similarity index 100% rename from examples/data/iris.npy rename to examples/desktop/data/iris.npy diff --git a/examples/image/__init__.py b/examples/desktop/gridplot/__init__.py similarity index 100% rename from examples/image/__init__.py rename to examples/desktop/gridplot/__init__.py diff --git a/examples/gridplot/gridplot.py b/examples/desktop/gridplot/gridplot.py similarity index 67% rename from examples/gridplot/gridplot.py rename to examples/desktop/gridplot/gridplot.py index 211c671f7..3acf6a8ba 100644 --- a/examples/gridplot/gridplot.py +++ b/examples/desktop/gridplot/gridplot.py @@ -6,17 +6,13 @@ # test_example = true -from fastplotlib import GridPlot -import numpy as np +import fastplotlib as fpl import imageio.v3 as iio -from wgpu.gui.offscreen import WgpuCanvas -from pygfx import WgpuRenderer -canvas = WgpuCanvas() -renderer = WgpuRenderer(canvas) - -plot = GridPlot(shape=(2,2), canvas=canvas, renderer=renderer) +plot = fpl.GridPlot(shape=(2, 2)) +# to force a specific framework such as glfw: +# plot = fpl.GridPlot(canvas="glfw") im = iio.imread("imageio:clock.png") im2 = iio.imread("imageio:astronaut.png") @@ -35,7 +31,6 @@ for subplot in plot: subplot.auto_scale() -img = np.asarray(plot.renderer.target.draw()) - if __name__ == "__main__": print(__doc__) + fpl.run() diff --git a/examples/line/__init__.py b/examples/desktop/image/__init__.py similarity index 100% rename from examples/line/__init__.py rename to examples/desktop/image/__init__.py diff --git a/examples/image/image_cmap.py b/examples/desktop/image/image_cmap.py similarity index 62% rename from examples/image/image_cmap.py rename to examples/desktop/image/image_cmap.py index 3f061c9d4..9a9f0d497 100644 --- a/examples/image/image_cmap.py +++ b/examples/desktop/image/image_cmap.py @@ -5,17 +5,13 @@ """ # test_example = true -from fastplotlib import Plot -import numpy as np +import fastplotlib as fpl import imageio.v3 as iio -from wgpu.gui.offscreen import WgpuCanvas -from pygfx import WgpuRenderer -canvas = WgpuCanvas() -renderer = WgpuRenderer(canvas) - -plot = Plot(canvas=canvas, renderer=renderer) +plot = fpl.Plot() +# to force a specific framework such as glfw: +# plot = fpl.Plot(canvas="glfw") im = iio.imread("imageio:camera.png") @@ -30,7 +26,6 @@ image_graphic.cmap = "viridis" -img = np.asarray(plot.renderer.target.draw()) - if __name__ == "__main__": print(__doc__) + fpl.run() diff --git a/examples/image/image_rgb.py b/examples/desktop/image/image_rgb.py similarity index 57% rename from examples/image/image_rgb.py rename to examples/desktop/image/image_rgb.py index fbd4cf24a..f73077acf 100644 --- a/examples/image/image_rgb.py +++ b/examples/desktop/image/image_rgb.py @@ -5,17 +5,13 @@ """ # test_example = true -from fastplotlib import Plot -import numpy as np +import fastplotlib as fpl import imageio.v3 as iio -from wgpu.gui.offscreen import WgpuCanvas -from pygfx import WgpuRenderer -canvas = WgpuCanvas() -renderer = WgpuRenderer(canvas) - -plot = Plot(canvas=canvas, renderer=renderer) +plot = fpl.Plot() +# to force a specific framework such as glfw: +# plot = fpl.Plot(canvas="glfw") im = iio.imread("imageio:astronaut.png") @@ -28,7 +24,7 @@ plot.auto_scale() -img = np.asarray(plot.renderer.target.draw()) if __name__ == "__main__": - print(__doc__) \ No newline at end of file + print(__doc__) + fpl.run() diff --git a/examples/image/image_rgbvminvmax.py b/examples/desktop/image/image_rgbvminvmax.py similarity index 64% rename from examples/image/image_rgbvminvmax.py rename to examples/desktop/image/image_rgbvminvmax.py index f6b419b60..4891c5614 100644 --- a/examples/image/image_rgbvminvmax.py +++ b/examples/desktop/image/image_rgbvminvmax.py @@ -5,17 +5,13 @@ """ # test_example = true -from fastplotlib import Plot -import numpy as np +import fastplotlib as fpl import imageio.v3 as iio -from wgpu.gui.offscreen import WgpuCanvas -from pygfx import WgpuRenderer -canvas = WgpuCanvas() -renderer = WgpuRenderer(canvas) - -plot = Plot(canvas=canvas, renderer=renderer) +plot = fpl.Plot() +# to force a specific framework such as glfw: +# plot = fpl.Plot(canvas="glfw") im = iio.imread("imageio:astronaut.png") @@ -31,7 +27,7 @@ image_graphic.cmap.vmin = 0.5 image_graphic.cmap.vmax = 0.75 -img = np.asarray(plot.renderer.target.draw()) if __name__ == "__main__": print(__doc__) + fpl.run() diff --git a/examples/image/image_simple.py b/examples/desktop/image/image_simple.py similarity index 59% rename from examples/image/image_simple.py rename to examples/desktop/image/image_simple.py index afe5a608e..2d273ad68 100644 --- a/examples/image/image_simple.py +++ b/examples/desktop/image/image_simple.py @@ -6,17 +6,13 @@ # test_example = true -from fastplotlib import Plot -import numpy as np +import fastplotlib as fpl import imageio.v3 as iio -from wgpu.gui.offscreen import WgpuCanvas -from pygfx import WgpuRenderer -canvas = WgpuCanvas() -renderer = WgpuRenderer(canvas) - -plot = Plot(canvas=canvas, renderer=renderer) +plot = fpl.Plot() +# to force a specific framework such as glfw: +# plot = fpl.Plot(canvas="glfw") data = iio.imread("imageio:camera.png") @@ -29,7 +25,6 @@ plot.auto_scale() -img = np.asarray(plot.renderer.target.draw()) - if __name__ == "__main__": print(__doc__) + fpl.run() diff --git a/examples/image/image_vminvmax.py b/examples/desktop/image/image_vminvmax.py similarity index 63% rename from examples/image/image_vminvmax.py rename to examples/desktop/image/image_vminvmax.py index c2636bb17..ae5d102fa 100644 --- a/examples/image/image_vminvmax.py +++ b/examples/desktop/image/image_vminvmax.py @@ -5,18 +5,13 @@ """ # test_example = true -from fastplotlib import Plot -import numpy as np -from pathlib import Path +import fastplotlib as fpl import imageio.v3 as iio -from wgpu.gui.offscreen import WgpuCanvas -from pygfx import WgpuRenderer -canvas = WgpuCanvas() -renderer = WgpuRenderer(canvas) - -plot = Plot(canvas=canvas, renderer=renderer) +plot = fpl.Plot() +# to force a specific framework such as glfw: +# plot = fpl.Plot(canvas="glfw") data = iio.imread("imageio:astronaut.png") @@ -32,7 +27,7 @@ image_graphic.cmap.vmin = 0.5 image_graphic.cmap.vmax = 0.75 -img = np.asarray(plot.renderer.target.draw()) if __name__ == "__main__": print(__doc__) + fpl.run() diff --git a/examples/line_collection/__init__.py b/examples/desktop/line/__init__.py similarity index 100% rename from examples/line_collection/__init__.py rename to examples/desktop/line/__init__.py diff --git a/examples/line/line.py b/examples/desktop/line/line.py similarity index 85% rename from examples/line/line.py rename to examples/desktop/line/line.py index 45fc5eb5b..8cab1954f 100644 --- a/examples/line/line.py +++ b/examples/desktop/line/line.py @@ -6,11 +6,13 @@ # test_example = true -from fastplotlib import Plot +import fastplotlib as fpl import numpy as np -plot = Plot() +plot = fpl.Plot() +# to force a specific framework such as glfw: +# plot = fpl.Plot(canvas="glfw") xs = np.linspace(-10, 10, 100) # sine wave @@ -41,7 +43,7 @@ plot.auto_scale() -img = np.asarray(plot.renderer.target.draw()) if __name__ == "__main__": print(__doc__) + fpl.run() diff --git a/examples/line/line_cmap.py b/examples/desktop/line/line_cmap.py similarity index 89% rename from examples/line/line_cmap.py rename to examples/desktop/line/line_cmap.py index f2fa29d79..b196132ed 100644 --- a/examples/line/line_cmap.py +++ b/examples/desktop/line/line_cmap.py @@ -11,6 +11,8 @@ plot = fpl.Plot() +# to force a specific framework such as glfw: +# plot = fpl.Plot(canvas="glfw") xs = np.linspace(-10, 10, 100) # sine wave @@ -43,4 +45,5 @@ plot.canvas.set_logical_size(800, 800) if __name__ == "__main__": + print(__doc__) fpl.run() diff --git a/examples/line/line_colorslice.py b/examples/desktop/line/line_colorslice.py similarity index 91% rename from examples/line/line_colorslice.py rename to examples/desktop/line/line_colorslice.py index a82f43aa6..f757a7efe 100644 --- a/examples/line/line_colorslice.py +++ b/examples/desktop/line/line_colorslice.py @@ -6,11 +6,13 @@ # test_example = true -from fastplotlib import Plot +import fastplotlib as fpl import numpy as np -plot = Plot() +plot = fpl.Plot() +# to force a specific framework such as glfw: +# plot = fpl.Plot(canvas="glfw") xs = np.linspace(-10, 10, 100) # sine wave @@ -64,3 +66,4 @@ if __name__ == "__main__": print(__doc__) + fpl.run() diff --git a/examples/line/line_dataslice.py b/examples/desktop/line/line_dataslice.py similarity index 89% rename from examples/line/line_dataslice.py rename to examples/desktop/line/line_dataslice.py index ddc670cd2..ef3cccfe8 100644 --- a/examples/line/line_dataslice.py +++ b/examples/desktop/line/line_dataslice.py @@ -6,11 +6,13 @@ # test_example = true -from fastplotlib import Plot +import fastplotlib as fpl import numpy as np -plot = Plot() +plot = fpl.Plot() +# to force a specific framework such as glfw: +# plot = fpl.Plot(canvas="glfw") xs = np.linspace(-10, 10, 100) # sine wave @@ -53,3 +55,4 @@ if __name__ == "__main__": print(__doc__) + fpl.run() diff --git a/examples/line/line_present_scaling.py b/examples/desktop/line/line_present_scaling.py similarity index 87% rename from examples/line/line_present_scaling.py rename to examples/desktop/line/line_present_scaling.py index 9cf2706e1..b8e9be63c 100644 --- a/examples/line/line_present_scaling.py +++ b/examples/desktop/line/line_present_scaling.py @@ -6,11 +6,13 @@ # test_example = true -from fastplotlib import Plot +import fastplotlib as fpl import numpy as np -plot = Plot() +plot = fpl.Plot() +# to force a specific framework such as glfw: +# plot = fpl.Plot(canvas="glfw") xs = np.linspace(-10, 10, 100) # sine wave @@ -47,4 +49,4 @@ if __name__ == "__main__": print(__doc__) - + fpl.run() diff --git a/examples/scatter/__init__.py b/examples/desktop/line_collection/__init__.py similarity index 100% rename from examples/scatter/__init__.py rename to examples/desktop/line_collection/__init__.py diff --git a/examples/line_collection/line_collection.py b/examples/desktop/line_collection/line_collection.py similarity index 89% rename from examples/line_collection/line_collection.py rename to examples/desktop/line_collection/line_collection.py index 508aca190..071da2e2e 100644 --- a/examples/line_collection/line_collection.py +++ b/examples/desktop/line_collection/line_collection.py @@ -28,6 +28,8 @@ def make_circle(center, radius: float, n_points: int = 75) -> np.ndarray: pos_xy = np.vstack(circles) plot = fpl.Plot() +# to force a specific framework such as glfw: +# plot = fpl.Plot(canvas="glfw") plot.add_line_collection(circles, cmap="jet", thickness=5) @@ -36,4 +38,5 @@ def make_circle(center, radius: float, n_points: int = 75) -> np.ndarray: plot.canvas.set_logical_size(800, 800) if __name__ == "__main__": + print(__doc__) fpl.run() diff --git a/examples/line_collection/line_collection_cmap_values.py b/examples/desktop/line_collection/line_collection_cmap_values.py similarity index 92% rename from examples/line_collection/line_collection_cmap_values.py rename to examples/desktop/line_collection/line_collection_cmap_values.py index 749d25b38..3623c20c3 100644 --- a/examples/line_collection/line_collection_cmap_values.py +++ b/examples/desktop/line_collection/line_collection_cmap_values.py @@ -34,6 +34,8 @@ def make_circle(center, radius: float, n_points: int = 75) -> np.ndarray: cmap_values = [10] * 4 + [0] * 4 + [7] * 4 + [5] * 4 plot = fpl.Plot() +# to force a specific framework such as glfw: +# plot = fpl.Plot(canvas="glfw") plot.add_line_collection( circles, @@ -47,4 +49,5 @@ def make_circle(center, radius: float, n_points: int = 75) -> np.ndarray: plot.canvas.set_logical_size(800, 800) if __name__ == "__main__": + print(__doc__) fpl.run() diff --git a/examples/line_collection/line_collection_cmap_values_qualitative.py b/examples/desktop/line_collection/line_collection_cmap_values_qualitative.py similarity index 92% rename from examples/line_collection/line_collection_cmap_values_qualitative.py rename to examples/desktop/line_collection/line_collection_cmap_values_qualitative.py index f42c46ca3..f56d2ca02 100644 --- a/examples/line_collection/line_collection_cmap_values_qualitative.py +++ b/examples/desktop/line_collection/line_collection_cmap_values_qualitative.py @@ -40,6 +40,8 @@ def make_circle(center, radius: float, n_points: int = 75) -> np.ndarray: ] plot = fpl.Plot() +# to force a specific framework such as glfw: +# plot = fpl.Plot(canvas="glfw") plot.add_line_collection( circles, @@ -53,4 +55,5 @@ def make_circle(center, radius: float, n_points: int = 75) -> np.ndarray: plot.canvas.set_logical_size(800, 800) if __name__ == "__main__": + print(__doc__) fpl.run() diff --git a/examples/line_collection/line_collection_colors.py b/examples/desktop/line_collection/line_collection_colors.py similarity index 90% rename from examples/line_collection/line_collection_colors.py rename to examples/desktop/line_collection/line_collection_colors.py index bb1a2c833..d74f65d82 100644 --- a/examples/line_collection/line_collection_colors.py +++ b/examples/desktop/line_collection/line_collection_colors.py @@ -32,6 +32,8 @@ def make_circle(center, radius: float, n_points: int = 75) -> np.ndarray: colors = ["blue"] * 4 + ["red"] * 4 + ["yellow"] * 4 + ["w"] * 4 plot = fpl.Plot() +# to force a specific framework such as glfw: +# plot = fpl.Plot(canvas="glfw") plot.add_line_collection(circles, colors=colors, thickness=10) @@ -40,4 +42,5 @@ def make_circle(center, radius: float, n_points: int = 75) -> np.ndarray: plot.canvas.set_logical_size(800, 800) if __name__ == "__main__": + print(__doc__) fpl.run() diff --git a/examples/line_collection/line_stack.py b/examples/desktop/line_collection/line_stack.py similarity index 83% rename from examples/line_collection/line_stack.py rename to examples/desktop/line_collection/line_stack.py index 282137c40..5a94caee7 100644 --- a/examples/line_collection/line_stack.py +++ b/examples/desktop/line_collection/line_stack.py @@ -18,6 +18,8 @@ data = np.vstack([ys] * 25) plot = fpl.Plot() +# to force a specific framework such as glfw: +# plot = fpl.Plot(canvas="glfw") # line stack takes all the same arguments as line collection and behaves similarly plot.add_line_stack(data, cmap="jet") @@ -27,4 +29,5 @@ plot.canvas.set_logical_size(900, 600) if __name__ == "__main__": + print(__doc__) fpl.run() diff --git a/fastplotlib/graphics/features/_sizes.py b/examples/desktop/scatter/__init__.py similarity index 100% rename from fastplotlib/graphics/features/_sizes.py rename to examples/desktop/scatter/__init__.py diff --git a/examples/scatter/scatter.py b/examples/desktop/scatter/scatter.py similarity index 73% rename from examples/scatter/scatter.py rename to examples/desktop/scatter/scatter.py index c866c4907..243924035 100644 --- a/examples/scatter/scatter.py +++ b/examples/desktop/scatter/scatter.py @@ -6,17 +6,13 @@ # test_example = true -from fastplotlib import Plot +import fastplotlib as fpl import numpy as np from pathlib import Path -from wgpu.gui.offscreen import WgpuCanvas -from pygfx import WgpuRenderer - -canvas = WgpuCanvas() -renderer = WgpuRenderer(canvas) - -plot = Plot(canvas=canvas, renderer=renderer) +plot = fpl.Plot() +# to force a specific framework such as glfw: +# plot = fpl.Plot(canvas="glfw") data_path = Path(__file__).parent.parent.joinpath("data", "iris.npy") data = np.load(data_path) @@ -36,3 +32,4 @@ if __name__ == "__main__": print(__doc__) + fpl.run() diff --git a/examples/scatter/scatter_cmap.py b/examples/desktop/scatter/scatter_cmap.py similarity index 83% rename from examples/scatter/scatter_cmap.py rename to examples/desktop/scatter/scatter_cmap.py index b6ab5fb17..ae113537a 100644 --- a/examples/scatter/scatter_cmap.py +++ b/examples/desktop/scatter/scatter_cmap.py @@ -6,13 +6,15 @@ # test_example = true -from fastplotlib import Plot, run +import fastplotlib as fpl import numpy as np from pathlib import Path from sklearn.cluster import AgglomerativeClustering -plot = Plot() +plot = fpl.Plot() +# to force a specific framework such as glfw: +# plot = fpl.Plot(canvas="glfw") data_path = Path(__file__).parent.parent.joinpath("data", "iris.npy") data = np.load(data_path) @@ -43,4 +45,4 @@ if __name__ == "__main__": print(__doc__) - run() + fpl.run() diff --git a/examples/scatter/scatter_colorslice.py b/examples/desktop/scatter/scatter_colorslice.py similarity index 77% rename from examples/scatter/scatter_colorslice.py rename to examples/desktop/scatter/scatter_colorslice.py index d3dd681a2..f5f32f5be 100644 --- a/examples/scatter/scatter_colorslice.py +++ b/examples/desktop/scatter/scatter_colorslice.py @@ -6,17 +6,14 @@ # test_example = true -from fastplotlib import Plot +import fastplotlib as fpl import numpy as np from pathlib import Path -from wgpu.gui.offscreen import WgpuCanvas -from pygfx import WgpuRenderer -canvas = WgpuCanvas() -renderer = WgpuRenderer(canvas) - -plot = Plot(canvas=canvas, renderer=renderer) +plot = fpl.Plot() +# to force a specific framework such as glfw: +# plot = fpl.Plot(canvas="glfw") data_path = Path(__file__).parent.parent.joinpath("data", "iris.npy") data = np.load(data_path) @@ -40,3 +37,4 @@ if __name__ == "__main__": print(__doc__) + fpl.run() diff --git a/examples/scatter/scatter_dataslice.py b/examples/desktop/scatter/scatter_dataslice.py similarity index 81% rename from examples/scatter/scatter_dataslice.py rename to examples/desktop/scatter/scatter_dataslice.py index c522ca729..7b80d6c9e 100644 --- a/examples/scatter/scatter_dataslice.py +++ b/examples/desktop/scatter/scatter_dataslice.py @@ -6,17 +6,14 @@ # test_example = true -from fastplotlib import Plot +import fastplotlib as fpl import numpy as np from pathlib import Path -from wgpu.gui.offscreen import WgpuCanvas -from pygfx import WgpuRenderer -canvas = WgpuCanvas() -renderer = WgpuRenderer(canvas) - -plot = Plot(canvas=canvas, renderer=renderer) +plot = fpl.Plot() +# to force a specific framework such as glfw: +# plot = fpl.Plot(canvas="glfw") data_path = Path(__file__).parent.parent.joinpath("data", "iris.npy") data = np.load(data_path) @@ -43,4 +40,4 @@ if __name__ == "__main__": print(__doc__) - \ No newline at end of file + fpl.run() diff --git a/examples/scatter/scatter_present.py b/examples/desktop/scatter/scatter_present.py similarity index 79% rename from examples/scatter/scatter_present.py rename to examples/desktop/scatter/scatter_present.py index 8770a1b92..fe0a3bf4f 100644 --- a/examples/scatter/scatter_present.py +++ b/examples/desktop/scatter/scatter_present.py @@ -6,17 +6,12 @@ # test_example = true -from fastplotlib import Plot +import fastplotlib as fpl import numpy as np from pathlib import Path -from wgpu.gui.offscreen import WgpuCanvas -from pygfx import WgpuRenderer -canvas = WgpuCanvas() -renderer = WgpuRenderer(canvas) - -plot = Plot(canvas=canvas, renderer=renderer) +plot = fpl.Plot() data_path = Path(__file__).parent.parent.joinpath("data", "iris.npy") data = np.load(data_path) @@ -41,3 +36,4 @@ if __name__ == "__main__": print(__doc__) + fpl.run() diff --git a/examples/screenshots/gridplot.png b/examples/desktop/screenshots/gridplot.png similarity index 100% rename from examples/screenshots/gridplot.png rename to examples/desktop/screenshots/gridplot.png diff --git a/examples/screenshots/image_cmap.png b/examples/desktop/screenshots/image_cmap.png similarity index 100% rename from examples/screenshots/image_cmap.png rename to examples/desktop/screenshots/image_cmap.png diff --git a/examples/screenshots/image_rgb.png b/examples/desktop/screenshots/image_rgb.png similarity index 100% rename from examples/screenshots/image_rgb.png rename to examples/desktop/screenshots/image_rgb.png diff --git a/examples/screenshots/image_rgbvminvmax.png b/examples/desktop/screenshots/image_rgbvminvmax.png similarity index 100% rename from examples/screenshots/image_rgbvminvmax.png rename to examples/desktop/screenshots/image_rgbvminvmax.png diff --git a/examples/screenshots/image_simple.png b/examples/desktop/screenshots/image_simple.png similarity index 100% rename from examples/screenshots/image_simple.png rename to examples/desktop/screenshots/image_simple.png diff --git a/examples/screenshots/image_vminvmax.png b/examples/desktop/screenshots/image_vminvmax.png similarity index 100% rename from examples/screenshots/image_vminvmax.png rename to examples/desktop/screenshots/image_vminvmax.png diff --git a/examples/screenshots/line.png b/examples/desktop/screenshots/line.png similarity index 100% rename from examples/screenshots/line.png rename to examples/desktop/screenshots/line.png diff --git a/examples/screenshots/line_cmap.png b/examples/desktop/screenshots/line_cmap.png similarity index 100% rename from examples/screenshots/line_cmap.png rename to examples/desktop/screenshots/line_cmap.png diff --git a/examples/screenshots/line_collection.png b/examples/desktop/screenshots/line_collection.png similarity index 100% rename from examples/screenshots/line_collection.png rename to examples/desktop/screenshots/line_collection.png diff --git a/examples/screenshots/line_collection_cmap_values.png b/examples/desktop/screenshots/line_collection_cmap_values.png similarity index 100% rename from examples/screenshots/line_collection_cmap_values.png rename to examples/desktop/screenshots/line_collection_cmap_values.png diff --git a/examples/screenshots/line_collection_cmap_values_qualitative.png b/examples/desktop/screenshots/line_collection_cmap_values_qualitative.png similarity index 100% rename from examples/screenshots/line_collection_cmap_values_qualitative.png rename to examples/desktop/screenshots/line_collection_cmap_values_qualitative.png diff --git a/examples/screenshots/line_collection_colors.png b/examples/desktop/screenshots/line_collection_colors.png similarity index 100% rename from examples/screenshots/line_collection_colors.png rename to examples/desktop/screenshots/line_collection_colors.png diff --git a/examples/screenshots/line_colorslice.png b/examples/desktop/screenshots/line_colorslice.png similarity index 100% rename from examples/screenshots/line_colorslice.png rename to examples/desktop/screenshots/line_colorslice.png diff --git a/examples/screenshots/line_dataslice.png b/examples/desktop/screenshots/line_dataslice.png similarity index 100% rename from examples/screenshots/line_dataslice.png rename to examples/desktop/screenshots/line_dataslice.png diff --git a/examples/screenshots/line_present_scaling.png b/examples/desktop/screenshots/line_present_scaling.png similarity index 100% rename from examples/screenshots/line_present_scaling.png rename to examples/desktop/screenshots/line_present_scaling.png diff --git a/examples/screenshots/line_stack.png b/examples/desktop/screenshots/line_stack.png similarity index 100% rename from examples/screenshots/line_stack.png rename to examples/desktop/screenshots/line_stack.png diff --git a/examples/screenshots/scatter.png b/examples/desktop/screenshots/scatter.png similarity index 100% rename from examples/screenshots/scatter.png rename to examples/desktop/screenshots/scatter.png diff --git a/examples/screenshots/scatter_cmap.png b/examples/desktop/screenshots/scatter_cmap.png similarity index 100% rename from examples/screenshots/scatter_cmap.png rename to examples/desktop/screenshots/scatter_cmap.png diff --git a/examples/screenshots/scatter_colorslice.png b/examples/desktop/screenshots/scatter_colorslice.png similarity index 100% rename from examples/screenshots/scatter_colorslice.png rename to examples/desktop/screenshots/scatter_colorslice.png diff --git a/examples/screenshots/scatter_dataslice.png b/examples/desktop/screenshots/scatter_dataslice.png similarity index 100% rename from examples/screenshots/scatter_dataslice.png rename to examples/desktop/screenshots/scatter_dataslice.png diff --git a/examples/screenshots/scatter_present.png b/examples/desktop/screenshots/scatter_present.png similarity index 100% rename from examples/screenshots/scatter_present.png rename to examples/desktop/screenshots/scatter_present.png diff --git a/examples/misc/garbage_collection.py b/examples/misc/garbage_collection.py new file mode 100644 index 000000000..baa92e848 --- /dev/null +++ b/examples/misc/garbage_collection.py @@ -0,0 +1,60 @@ +import numpy as np +from wgpu.gui.auto import WgpuCanvas, run +import pygfx as gfx +import subprocess + +canvas = WgpuCanvas() +renderer = gfx.WgpuRenderer(canvas) +scene = gfx.Scene() +camera = gfx.OrthographicCamera(5000, 5000) +camera.position.x = 2048 +camera.position.y = 2048 + + +def make_image(): + data = np.random.rand(4096, 4096).astype(np.float32) + + return gfx.Image( + gfx.Geometry(grid=gfx.Texture(data, dim=2)), + gfx.ImageBasicMaterial(clim=(0, 1)), + ) + + +def draw(): + renderer.render(scene, camera) + canvas.request_draw() + + +def print_nvidia(msg=""): + print(msg) + print( + subprocess.check_output(["nvidia-smi", "--format=csv", "--query-gpu=memory.used"]).decode().split("\n")[1] + ) + print() + + +def add_img(*args): + print_nvidia("Before creating image") + img = make_image() + print_nvidia("After creating image") + scene.add(img) + img.add_event_handler(remove_img, "click") + draw() + print_nvidia("After add image to scene") + + +def remove_img(*args): + img = scene.children[0] + scene.remove(img) + draw() + print_nvidia("After remove image from scene") + del img + draw() + print_nvidia("After del image") + renderer.add_event_handler(print_nvidia, "pointer_move") + + +renderer.add_event_handler(add_img, "double_click") + +draw() +run() diff --git a/examples/misc/large_img.py b/examples/misc/large_img.py new file mode 100644 index 000000000..021bbd6f6 --- /dev/null +++ b/examples/misc/large_img.py @@ -0,0 +1,35 @@ +from fastplotlib import Plot, run +import numpy as np + +temporal = np.load("./array_10-000x108-000.npy") + +from PIL import Image + +Image.MAX_IMAGE_PIXELS = None + +img = Image.open("/home/kushal/Downloads/gigahour_stitched_0042_bbs.png") + +a = np.array(img) + +r = np.random.randint(0, 50, a.size, dtype=np.uint8).reshape(a.shape) + +plot = Plot(renderer_kwargs={"show_fps": True}) +plot.add_heatmap(r) +# plot.camera.scale.y = 0.2 +plot.show() + +r = np.random.randint(0, 50, a.size, dtype=np.uint8).reshape(a.shape) +r2 = np.random.randint(0, 50, a.size, dtype=np.uint8).reshape(a.shape) +r3 = np.random.randint(0, 50, a.size, dtype=np.uint8).reshape(a.shape) + +rs = [r, r2, r3] +i = 0 + +def update_frame(p): + global i + p.graphics[0].data[:] = rs[i % 3] + i +=1 + +plot.add_animations(update_frame) + +run() diff --git a/examples/selector_performance.ipynb b/examples/misc/selector_performance.ipynb similarity index 100% rename from examples/selector_performance.ipynb rename to examples/misc/selector_performance.ipynb diff --git a/notebooks/gridplot.ipynb b/examples/notebooks/gridplot.ipynb similarity index 100% rename from notebooks/gridplot.ipynb rename to examples/notebooks/gridplot.ipynb diff --git a/notebooks/gridplot_simple.ipynb b/examples/notebooks/gridplot_simple.ipynb similarity index 100% rename from notebooks/gridplot_simple.ipynb rename to examples/notebooks/gridplot_simple.ipynb diff --git a/examples/notebooks/heatmap.ipynb b/examples/notebooks/heatmap.ipynb new file mode 100644 index 000000000..82583b1df --- /dev/null +++ b/examples/notebooks/heatmap.ipynb @@ -0,0 +1,110 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "d8c90f4b-b635-4027-b7d5-080d77bd40a3", + "metadata": {}, + "source": [ + "# The `HeatmapGraphic` is useful for looking at very large arrays\n", + "\n", + "`ImageGraphic` is limited to a max size of `8192 x 8192`" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "49b2498d-56ae-4559-9282-c8484f3e6b6d", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "import numpy as np\n", + "import fastplotlib as fpl" + ] + }, + { + "cell_type": "markdown", + "id": "908f93f8-68c3-4a36-8f40-e0aab560955d", + "metadata": {}, + "source": [ + "## Generate some sine and cosine data" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "40718465-abf6-4727-8bd7-4acdd59843d5", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "xs = np.linspace(0, 50, 10_000)\n", + "\n", + "sine_data = np.sin(xs)\n", + "\n", + "cosine_data = np.cos(xs)\n", + "\n", + "data = np.vstack([(sine_data, cosine_data) for i in range(5)])" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "02b072eb-2909-40c8-8739-950f07efbbc2", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "data.shape" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "84deb31b-5464-4cce-a938-694371011021", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "plot = fpl.Plot()\n", + "\n", + "plot.add_heatmap(data, cmap=\"viridis\")\n", + "\n", + "plot.show(maintain_aspect=False)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "df3f8994-0f5b-4578-a36d-4cd9bf0733c0", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.3" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/notebooks/image_widget.ipynb b/examples/notebooks/image_widget.ipynb similarity index 67% rename from notebooks/image_widget.ipynb rename to examples/notebooks/image_widget.ipynb index f50d83d36..5b7de6145 100644 --- a/notebooks/image_widget.ipynb +++ b/examples/notebooks/image_widget.ipynb @@ -2,7 +2,7 @@ "cells": [ { "cell_type": "code", - "execution_count": 1, + "execution_count": null, "id": "04f453ca-d0bc-411f-b2a6-d38294dd0a26", "metadata": { "tags": [] @@ -13,6 +13,74 @@ "import numpy as np" ] }, + { + "cell_type": "markdown", + "id": "bd632552-dba1-4e48-b8b2-595da7757d0f", + "metadata": {}, + "source": [ + "# Single image" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c798a5e0-07a0-4468-8e22-9b53b8243ab5", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "a = np.random.rand(512, 512)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b0dbf913-c1c6-4c2a-8191-45a87b2ce310", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "iw = ImageWidget(\n", + " data=a,\n", + " vmin_vmax_sliders=True,\n", + " cmap=\"viridis\"\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8264fd19-661f-4c50-bdb4-d3998ffd5ff5", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "iw.show()" + ] + }, + { + "cell_type": "markdown", + "id": "b718162f-9aa6-4091-a7a4-c620676b48bd", + "metadata": {}, + "source": [ + "### can dynamically change features" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "393375df-327c-409a-9e3e-75121a0df6cb", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "iw.gridplot[0, 0].graphics[0].cmap = \"gnuplot2\"" + ] + }, { "cell_type": "markdown", "id": "e933771b-f172-4fa9-b2f8-129723efb808", @@ -23,7 +91,7 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": null, "id": "ea87f9a6-437f-41f6-8739-c957fb04bdbf", "metadata": { "tags": [] @@ -35,27 +103,12 @@ }, { "cell_type": "code", - "execution_count": 20, + "execution_count": null, "id": "8b7a6066-ff69-4bee-bae6-160fb4038393", "metadata": { "tags": [] }, - "outputs": [ - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "5ccfeea584fb491b9431b9284ab45993", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "RFBOutputContext()" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], + "outputs": [], "source": [ "iw = ImageWidget(\n", " data=a, \n", @@ -67,28 +120,12 @@ }, { "cell_type": "code", - "execution_count": 21, + "execution_count": null, "id": "3d4cb44e-2c71-4bff-aeed-b2129f34d724", "metadata": { "tags": [] }, - "outputs": [ - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "8624f2dba4d94e5881879d14464cd370", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "VBox(children=(VBox(children=(JupyterWgpuCanvas(), HBox(children=(Button(icon='expand-arrows-alt', layout=Layo…" - ] - }, - "execution_count": 21, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "iw.show()" ] @@ -105,7 +142,7 @@ }, { "cell_type": "code", - "execution_count": 22, + "execution_count": null, "id": "f278b26a-1b71-4e76-9cc7-efaddbd7b122", "metadata": { "tags": [] @@ -118,7 +155,7 @@ }, { "cell_type": "code", - "execution_count": 23, + "execution_count": null, "id": "cb4d4b7c-919f-41c0-b1cc-b4496473d760", "metadata": { "tags": [] @@ -131,7 +168,7 @@ }, { "cell_type": "code", - "execution_count": 24, + "execution_count": null, "id": "2eea6432-4d38-4d42-ab75-f6aa1bab36f4", "metadata": { "tags": [] @@ -144,7 +181,7 @@ }, { "cell_type": "code", - "execution_count": 25, + "execution_count": null, "id": "afa2436f-2741-49d6-87f6-7a91a343fe0e", "metadata": { "tags": [] @@ -165,7 +202,7 @@ }, { "cell_type": "code", - "execution_count": 26, + "execution_count": null, "id": "370d1d24-2944-491f-9da0-fa0c7ed073ef", "metadata": { "tags": [] @@ -186,7 +223,7 @@ }, { "cell_type": "code", - "execution_count": 9, + "execution_count": null, "id": "882162eb-c873-42df-a945-d5e05ad141c9", "metadata": { "tags": [] @@ -199,27 +236,12 @@ }, { "cell_type": "code", - "execution_count": 10, + "execution_count": null, "id": "bf9f92b6-38ad-4d78-b88c-a32d473b6462", "metadata": { "tags": [] }, - "outputs": [ - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "befbd479926a4195ad6ee395da0aaa89", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "RFBOutputContext()" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], + "outputs": [], "source": [ "iw = ImageWidget(\n", " data=data, \n", @@ -242,28 +264,12 @@ }, { "cell_type": "code", - "execution_count": 11, + "execution_count": null, "id": "403dde31-981a-46fb-b005-1bcef19c4f2c", "metadata": { "tags": [] }, - "outputs": [ - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "48534008501f483b865c8df4de77e204", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "VBox(children=(VBox(children=(JupyterWgpuCanvas(), HBox(children=(Button(icon='expand-arrows-alt', layout=Layo…" - ] - }, - "execution_count": 11, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "iw.show()" ] @@ -278,26 +284,12 @@ }, { "cell_type": "code", - "execution_count": 13, + "execution_count": null, "id": "b59d95e2-9092-4915-beef-01661d164781", "metadata": { "tags": [] }, - "outputs": [ - { - "data": { - "text/plain": [ - "two: Subplot @ 0x7f362c02e350\n", - " parent: None\n", - " Graphics:\n", - "\t'image': ImageGraphic @ 0x7f362c0f69d0" - ] - }, - "execution_count": 13, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "iw.gridplot[\"two\"]" ] @@ -312,7 +304,7 @@ }, { "cell_type": "code", - "execution_count": 15, + "execution_count": null, "id": "a8f070db-da11-4062-95aa-f19b96351ee8", "metadata": { "tags": [] @@ -332,27 +324,12 @@ }, { "cell_type": "code", - "execution_count": 16, + "execution_count": null, "id": "b1587410-a08e-484c-8795-195a413d6374", "metadata": { "tags": [] }, - "outputs": [ - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "11be69e0a3c1411792cd25548c0ed273", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "RFBOutputContext()" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], + "outputs": [], "source": [ "dims = (256, 256, 5, 100)\n", "data = [np.random.rand(*dims) for i in range(4)]\n", @@ -370,28 +347,12 @@ }, { "cell_type": "code", - "execution_count": 17, + "execution_count": null, "id": "3ccea6c6-9580-4720-bce8-a5507cf867a3", "metadata": { "tags": [] }, - "outputs": [ - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "95b68c9c58754953a8a3e92980973bd6", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "VBox(children=(VBox(children=(JupyterWgpuCanvas(), HBox(children=(Button(icon='expand-arrows-alt', layout=Layo…" - ] - }, - "execution_count": 17, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "iw.show()" ] @@ -406,7 +367,7 @@ }, { "cell_type": "code", - "execution_count": 18, + "execution_count": null, "id": "fd4433a9-2add-417c-a618-5891371efae0", "metadata": { "tags": [] diff --git a/notebooks/linear_region_selector.ipynb b/examples/notebooks/linear_region_selector.ipynb similarity index 100% rename from notebooks/linear_region_selector.ipynb rename to examples/notebooks/linear_region_selector.ipynb diff --git a/notebooks/linear_selector.ipynb b/examples/notebooks/linear_selector.ipynb similarity index 100% rename from notebooks/linear_selector.ipynb rename to examples/notebooks/linear_selector.ipynb diff --git a/notebooks/lineplot.ipynb b/examples/notebooks/lineplot.ipynb similarity index 100% rename from notebooks/lineplot.ipynb rename to examples/notebooks/lineplot.ipynb diff --git a/examples/notebooks/lines_cmap.ipynb b/examples/notebooks/lines_cmap.ipynb new file mode 100644 index 000000000..c6dc604b4 --- /dev/null +++ b/examples/notebooks/lines_cmap.ipynb @@ -0,0 +1,288 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "id": "b169210c-b148-4701-91d2-87f8be2c90da", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "import numpy as np\n", + "import fastplotlib as fpl" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5d2ef4aa-0e4c-4694-ae2e-05da1153a413", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "# this is only for testing, you do not need this to use fastplotlib\n", + "from nb_test_utils import plot_test, notebook_finished" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a6615d45-6a6e-4a1e-a998-18f7cc52f6b9", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "# linspace, create 100 evenly spaced x values from -10 to 10\n", + "xs = np.linspace(-10, 10, 100)\n", + "# sine wave\n", + "ys = np.sin(xs)\n", + "sine = np.dstack([xs, ys])[0]\n", + "\n", + "# cosine wave\n", + "ys = np.cos(xs)\n", + "cosine = np.dstack([xs, ys])[0]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "52a91e8a-25b7-4121-a06f-623d7412b558", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "plot = fpl.Plot()\n", + "\n", + "plot.add_line(sine, thickness=10)\n", + "\n", + "plot.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "727282c3-aadf-420f-a88e-9dd4d4e91263", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "plot_test(\"lines-cmap-white\", plot)" + ] + }, + { + "cell_type": "markdown", + "id": "889b1858-ed64-4d6b-96ad-3883fbe4d38e", + "metadata": {}, + "source": [ + "# Fancy indexing of line colormaps" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "13185547-07bc-4771-ac6d-83314622bf30", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "plot.graphics[0].cmap = \"jet\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3c9b0bc8-b176-425c-8036-63dc55ab7466", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "# for testing, ignore\n", + "plot_test(\"lines-cmap-jet\", plot)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ee9ec4d7-d9a2-417c-92bd-b01a9a019801", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "plot.graphics[0].cmap.values = sine[:, 1]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6b19d2d4-90e7-40ed-afb9-13abe5474ace", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "# for testing, ignore\n", + "plot_test(\"lines-cmap-jet-values\", plot)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ebf9f494-782d-4529-9ef6-a2a4032f097d", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "plot.graphics[0].cmap.values = cosine[:, 1]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "0a6c4739-fa61-4532-865e-21107eab76f9", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "# for testing, ignore\n", + "plot_test(\"lines-cmap-jet-values-cosine\", plot)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5ddc95cf-b3be-4212-b525-1c628dc1e091", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "plot.graphics[0].cmap = \"viridis\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "45acfd2f-09f5-418c-bca5-3e574348b7d5", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "# for testing, ignore\n", + "plot_test(\"lines-cmap-viridis\", plot)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7712d313-16cd-49e5-89ca-91364412f194", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "cmap_values = [0] * 25 + [1] * 5 + [2] * 50 + [3] * 20" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c8c13c03-56f0-48c3-b44e-65545a3bc3bc", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "plot.graphics[0].cmap.values = cmap_values" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7548407f-05ed-4c47-93cc-131c61f8e242", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "# for testing, ignore\n", + "plot_test(\"lines-cmap-viridis-values\", plot)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f64d036d-8a9e-4799-b77f-e78afa441fec", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "plot.graphics[0].cmap = \"tab10\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c290c642-ba5f-4a46-9a17-c434cb39de26", + "metadata": {}, + "outputs": [], + "source": [ + "# for testing, ignore\n", + "plot_test(\"lines-cmap-tab-10\", plot)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c4b9e735-72e9-4f0e-aa3e-43db57e65c99", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "# for testing, ignore\n", + "notebook_finished()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f6735cc0-910c-4854-ac50-8ee553a6475e", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.3" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/examples/notebooks/nb_test_utils.py b/examples/notebooks/nb_test_utils.py new file mode 100644 index 000000000..e16ed2eaf --- /dev/null +++ b/examples/notebooks/nb_test_utils.py @@ -0,0 +1,87 @@ +from typing import * +import os +from pathlib import Path + +import imageio.v3 as iio +import numpy as np + +from fastplotlib import Plot, GridPlot + +# make dirs for screenshots and diffs +current_dir = Path(__file__).parent + +SCREENSHOTS_DIR = current_dir.joinpath("screenshots") +DIFFS_DIR = current_dir.joinpath("diffs") + +os.makedirs(SCREENSHOTS_DIR, exist_ok=True) +os.makedirs(DIFFS_DIR, exist_ok=True) + + +# store all the failures to allow the nb to proceed to test other examples +FAILURES = list() + + +def plot_test(name, plot: Union[Plot, GridPlot]): + snapshot = plot.canvas.snapshot() + + if "REGENERATE_SCREENSHOTS" in os.environ.keys(): + if os.environ["REGENERATE_SCREENSHOTS"] == "1": + regenerate_screenshot(name, snapshot.data) + + try: + assert_screenshot_equal(name, snapshot.data) + except AssertionError: + FAILURES.append(name) + + +def regenerate_screenshot(name, data): + iio.imwrite(SCREENSHOTS_DIR.joinpath(f"nb-{name}.png"), data) + + +def assert_screenshot_equal(name, data): + ground_truth = iio.imread(SCREENSHOTS_DIR.joinpath(f"nb-{name}.png")) + + is_similar = np.allclose(data, ground_truth) + + update_diffs(name, is_similar, data, ground_truth) + + assert is_similar, ( + f"notebook snapshot for {name} has changed" + ) + + +def update_diffs(name, is_similar, img, ground_truth): + diffs_rgba = None + + def get_diffs_rgba(slicer): + # lazily get and cache the diff computation + nonlocal diffs_rgba + if diffs_rgba is None: + # cast to float32 to avoid overflow + # compute absolute per-pixel difference + diffs_rgba = np.abs(ground_truth.astype("f4") - img) + # magnify small values, making it easier to spot small errors + diffs_rgba = ((diffs_rgba / 255) ** 0.25) * 255 + # cast back to uint8 + diffs_rgba = diffs_rgba.astype("u1") + return diffs_rgba[..., slicer] + + # split into an rgb and an alpha diff + diffs = { + DIFFS_DIR.joinpath(f"nb-diff-{name}-rgb.png"): slice(0, 3), + DIFFS_DIR.joinpath(f"nb-diff-{name}-alpha.png"): 3, + } + + for path, slicer in diffs.items(): + if not is_similar: + diff = get_diffs_rgba(slicer) + iio.imwrite(path, diff) + elif path.exists(): + path.unlink() + + +def notebook_finished(): + if len(FAILURES) > 0: + raise AssertionError( + f"Failures for plots:\n{FAILURES}" + ) diff --git a/examples/notebooks/scatter.ipynb b/examples/notebooks/scatter.ipynb new file mode 100644 index 000000000..948403f11 --- /dev/null +++ b/examples/notebooks/scatter.ipynb @@ -0,0 +1,201 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "eb204b20-160a-48ef-8ac6-54d263e497e4", + "metadata": { + "tags": [] + }, + "source": [ + "# Scatter plots in a `GridPlot` layout with a mix of 2d an 3d cameras" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9b3041ad-d94e-4b2a-af4d-63bcd19bf6c2", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "import numpy as np\n", + "from fastplotlib import GridPlot" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "51f1d76a-f815-460f-a884-097fe3ea81ac", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "# create a random distribution of 10,000 xyz coordinates\n", + "n_points = 10_000\n", + "\n", + "# if you have a good GPU go for 1.2 million points :D \n", + "# this is multiplied by 3\n", + "#n_points = 400_000\n", + "dims = (n_points, 3)\n", + "\n", + "offset = 15\n", + "\n", + "normal = np.random.normal(size=dims, scale=5)\n", + "cloud = np.vstack(\n", + " [\n", + " normal - offset,\n", + " normal,\n", + " normal + offset,\n", + " ]\n", + ")\n", + "\n", + "colors = [\"yellow\"] * n_points + [\"cyan\"] * n_points + [\"magenta\"] * n_points" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "922990b6-24e9-4fa0-977b-6577f9752d84", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "# grid with 2 rows and 2 columns\n", + "shape = (2, 2)\n", + "\n", + "# define the camera\n", + "# a mix of 2d and 3d\n", + "cameras = [\n", + " ['2d', '3d'], \n", + " ['3d', '2d']\n", + "]\n", + "\n", + "# pan-zoom controllers for each view\n", + "# views are synced if they have the \n", + "# same controller ID\n", + "# you can only sync controllers that use the same camera type\n", + "# i.e. you cannot sync between 2d and 3d subplots\n", + "controllers = [\n", + " [0, 1],\n", + " [1, 0]\n", + "]\n", + "\n", + "# create the grid plot\n", + "grid_plot = GridPlot(\n", + " shape=shape,\n", + " cameras=cameras,\n", + " controllers=controllers\n", + ")\n", + "\n", + "for subplot in grid_plot:\n", + " subplot.add_scatter(data=cloud, colors=colors, alpha=0.7, sizes=5)\n", + " \n", + " subplot.set_axes_visibility(True)\n", + " subplot.set_grid_visibility(True)\n", + "\n", + "\n", + "grid_plot.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7b912961-f72e-46ef-889f-c03234831059", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "grid_plot[0, 1].graphics[0].colors[n_points:int(n_points * 1.5)] = \"r\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c6085806-c001-4632-ab79-420b4692693a", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "grid_plot[0, 1].graphics[0].colors[:n_points:10] = \"blue\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6f416825-df31-4e5d-b66b-07f23b48e7db", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "grid_plot[0, 1].graphics[0].colors[n_points:] = \"green\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c0fd611e-73e5-49e6-a25c-9d5b64afa5f4", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "grid_plot[0, 1].graphics[0].colors[n_points:, -1] = 0" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "cd390542-3a44-4973-8172-89e5583433bc", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "grid_plot[0, 1].graphics[0].data[:n_points] = grid_plot[0, 1].graphics[0].data[n_points * 2:]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "fb49930f-b795-4b41-bbc6-014a27c2f463", + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7d8aac54-4f36-41d4-8e5b-8d8da2f0d17d", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.3" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/examples/notebooks/screenshots/nb-astronaut.png b/examples/notebooks/screenshots/nb-astronaut.png new file mode 100644 index 000000000..e8345f7b2 --- /dev/null +++ b/examples/notebooks/screenshots/nb-astronaut.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:36a11f5c0a80e1cfbdeb318b314886f4d8e02ba8a763bed0db9994ef451bfd42 +size 128068 diff --git a/examples/notebooks/screenshots/nb-astronaut_RGB.png b/examples/notebooks/screenshots/nb-astronaut_RGB.png new file mode 100644 index 000000000..0ff257ccf --- /dev/null +++ b/examples/notebooks/screenshots/nb-astronaut_RGB.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:dc27fc081b464bb53afd98d3748b8bc75764537d76a8012b9f1b2c1d4c10613d +size 125492 diff --git a/examples/notebooks/screenshots/nb-camera.png b/examples/notebooks/screenshots/nb-camera.png new file mode 100644 index 000000000..cbf936192 --- /dev/null +++ b/examples/notebooks/screenshots/nb-camera.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cbf213d944a16cf9f72542e7a2172330fefa97c8577905f07df12559eb4485c3 +size 89303 diff --git a/examples/notebooks/screenshots/nb-lines-3d.png b/examples/notebooks/screenshots/nb-lines-3d.png new file mode 100644 index 000000000..6bb05537a --- /dev/null +++ b/examples/notebooks/screenshots/nb-lines-3d.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b7e61fb22db10e515a7d249649c5e220731c6ea5a83bb626f06dcf41167f117e +size 23052 diff --git a/examples/notebooks/screenshots/nb-lines-cmap-jet-values-cosine.png b/examples/notebooks/screenshots/nb-lines-cmap-jet-values-cosine.png new file mode 100644 index 000000000..b1045cde6 --- /dev/null +++ b/examples/notebooks/screenshots/nb-lines-cmap-jet-values-cosine.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1f55806e64a8ffde2f11eed1dc75a874371800046c062da21e71554abedda251 +size 17136 diff --git a/examples/notebooks/screenshots/nb-lines-cmap-jet-values.png b/examples/notebooks/screenshots/nb-lines-cmap-jet-values.png new file mode 100644 index 000000000..53b3d4cbd --- /dev/null +++ b/examples/notebooks/screenshots/nb-lines-cmap-jet-values.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e5e9bcb785fe5efee324bdde451d62158668dafa0c026179bd11d38298fb0002 +size 18526 diff --git a/examples/notebooks/screenshots/nb-lines-cmap-jet.png b/examples/notebooks/screenshots/nb-lines-cmap-jet.png new file mode 100644 index 000000000..8bfd0d577 --- /dev/null +++ b/examples/notebooks/screenshots/nb-lines-cmap-jet.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2d6fd17a9a704b2d9c5341e85763f1ba9c5e3026da88858f004e66a781e02eaa +size 16310 diff --git a/examples/notebooks/screenshots/nb-lines-cmap-tab-10.png b/examples/notebooks/screenshots/nb-lines-cmap-tab-10.png new file mode 100644 index 000000000..3e76883bf --- /dev/null +++ b/examples/notebooks/screenshots/nb-lines-cmap-tab-10.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:672da2cc5e500ce3bbdabb01eaf5a7d2b9fb6ea4e6e95cb3392b2a0573a970d9 +size 14882 diff --git a/examples/notebooks/screenshots/nb-lines-cmap-viridis-values.png b/examples/notebooks/screenshots/nb-lines-cmap-viridis-values.png new file mode 100644 index 000000000..4b6212a6a --- /dev/null +++ b/examples/notebooks/screenshots/nb-lines-cmap-viridis-values.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1e1224f75ce0286c4721b5f65af339fc922dcb2308f8d2fa3def10ead48cdce8 +size 15096 diff --git a/examples/notebooks/screenshots/nb-lines-cmap-viridis.png b/examples/notebooks/screenshots/nb-lines-cmap-viridis.png new file mode 100644 index 000000000..35c38c881 --- /dev/null +++ b/examples/notebooks/screenshots/nb-lines-cmap-viridis.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f6201cd8dc9273adca73329b0eae81faf6aed42c3bf8f7ee503b9251af499dcd +size 19203 diff --git a/examples/notebooks/screenshots/nb-lines-cmap-white.png b/examples/notebooks/screenshots/nb-lines-cmap-white.png new file mode 100644 index 000000000..67c2fc116 --- /dev/null +++ b/examples/notebooks/screenshots/nb-lines-cmap-white.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ecb2d4d591b852bda8758efcf91d389442f916bbb4a06c5216d52dcf72172370 +size 12955 diff --git a/examples/notebooks/screenshots/nb-lines-colors.png b/examples/notebooks/screenshots/nb-lines-colors.png new file mode 100644 index 000000000..b9972c8f4 --- /dev/null +++ b/examples/notebooks/screenshots/nb-lines-colors.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a8eefa5106414bfb540b282d74372831ef3c4a9d941aaf50026ea64a3d3009f7 +size 40544 diff --git a/examples/notebooks/screenshots/nb-lines-data.png b/examples/notebooks/screenshots/nb-lines-data.png new file mode 100644 index 000000000..14d6f89f0 --- /dev/null +++ b/examples/notebooks/screenshots/nb-lines-data.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8e89906d0d749f443e751eeb43b017622a46dfaa91545e9135d0a519e0aad0eb +size 54446 diff --git a/examples/notebooks/screenshots/nb-lines-underlay.png b/examples/notebooks/screenshots/nb-lines-underlay.png new file mode 100644 index 000000000..d8809f301 --- /dev/null +++ b/examples/notebooks/screenshots/nb-lines-underlay.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:61ed6bde5639d57694cb8752052dda08a5f2f7dcc32966ab62385bc866c299e3 +size 55936 diff --git a/examples/notebooks/screenshots/nb-lines.png b/examples/notebooks/screenshots/nb-lines.png new file mode 100644 index 000000000..3dcc1767e --- /dev/null +++ b/examples/notebooks/screenshots/nb-lines.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:39478dbf9af2f74ae0e0240616d94480569d53dcbd5f046315eeff3855d4cb2e +size 37711 diff --git a/notebooks/simple.ipynb b/examples/notebooks/simple.ipynb similarity index 68% rename from notebooks/simple.ipynb rename to examples/notebooks/simple.ipynb index 9ca764283..e994bfba8 100644 --- a/notebooks/simple.ipynb +++ b/examples/notebooks/simple.ipynb @@ -9,14 +9,48 @@ "source": [ "# Introduction to `fastplotlib`\n", "\n", - "This notebook goes the basic components of the `fastplotlib` API, image, image updates, line plots, and scatter plots. " + "This notebook goes through the basic components of the `fastplotlib` API, image, image updates, line plots, and scatter plots. " + ] + }, + { + "cell_type": "markdown", + "id": "ae07272b-e94b-4262-b486-6b3ddac63038", + "metadata": {}, + "source": [ + "**The example images are from `imageio` so you will need to install it for this example notebook. But `imageio` is not required to use `fasptlotlib`**" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6674c90b-bfe3-4a71-ab7d-21e9cc03c050", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "!pip install imageio" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5c50e177-5800-4e19-a4f6-d0e0a082e4cd", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "import imageio.v3 as iio" ] }, { "cell_type": "code", "execution_count": null, "id": "fb57c3d3-f20d-4d88-9e7a-04b9309bc637", - "metadata": {}, + "metadata": { + "tags": [] + }, "outputs": [], "source": [ "from fastplotlib import Plot\n", @@ -24,29 +58,44 @@ "import numpy as np" ] }, + { + "cell_type": "code", + "execution_count": null, + "id": "1d374d5e-70e0-4946-937f-82d16a56009f", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "# this is only for testing, you do not need this to use fastplotlib\n", + "from nb_test_utils import plot_test, notebook_finished" + ] + }, { "cell_type": "markdown", "id": "a9b386ac-9218-4f8f-97b3-f29b4201ef55", "metadata": {}, "source": [ - "### Simple image" + "## Simple image" ] }, { "cell_type": "code", "execution_count": null, "id": "237823b7-e2c0-4e2f-9ee8-e3fc2b4453c4", - "metadata": {}, + "metadata": { + "tags": [] + }, "outputs": [], "source": [ "# create a `Plot` instance\n", "plot = Plot()\n", "\n", - "# make some random 2D image data\n", - "data = np.random.rand(512, 512)\n", + "# get a grayscale image\n", + "data = iio.imread(\"imageio:camera.png\")\n", "\n", "# plot the image data\n", - "image_graphic = plot.add_image(data=data, name=\"random-image\")\n", + "image_graphic = plot.add_image(data=data, name=\"sample-image\")\n", "\n", "# show the plot\n", "plot.show()" @@ -57,7 +106,21 @@ "id": "be5b408f-dd91-4e36-807a-8c22c8d7d216", "metadata": {}, "source": [ - "### Use the handle on the bottom right corner of the _canvas_ to resize it. You can also pan and zoom using your mouse!" + "**Use the handle on the bottom right corner of the _canvas_ to resize it. You can also pan and zoom using your mouse!**\n", + "\n", + "By default the origin is on the bottom left, you can click the flip button to flip the y-axis, or use `plot.camera.world.scale_y *= -1`" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "58c1dc0b-9bf0-4ad5-8579-7c10396fc6bc", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "plot.camera.world.scale_y *= -1" ] }, { @@ -65,385 +128,491 @@ "id": "7c3b637c-a26b-416e-936c-705275852a8a", "metadata": {}, "source": [ - "Changing graphic \"features\"" + "Changing graphic **\"features\"**" ] }, { "cell_type": "code", "execution_count": null, "id": "de816c88-1c4a-4071-8a5e-c46c93671ef5", - "metadata": {}, + "metadata": { + "tags": [] + }, "outputs": [], "source": [ "image_graphic.cmap = \"viridis\"" ] }, + { + "cell_type": "markdown", + "id": "da1efe85-c5b8-42e8-ae81-6cbddccc30f7", + "metadata": {}, + "source": [ + "### Slicing data\n", + "\n", + "**Most features, such as `data` support slicing!**\n", + "\n", + "Out image data is of shape [n_rows, n_cols]" + ] + }, { "cell_type": "code", "execution_count": null, - "id": "09350854-5058-4574-a01d-84d00e276c57", - "metadata": {}, + "id": "a04afe48-5534-4ef6-a159-f6e6a4337d8d", + "metadata": { + "tags": [] + }, "outputs": [], "source": [ - "image_graphic.data = 0" + "image_graphic.data().shape" ] }, { "cell_type": "code", "execution_count": null, "id": "83b2db1b-2783-4e89-bcf3-66bb6e09e18a", - "metadata": {}, + "metadata": { + "tags": [] + }, "outputs": [], "source": [ "image_graphic.data[::15, :] = 1\n", "image_graphic.data[:, ::15] = 1" ] }, + { + "cell_type": "markdown", + "id": "135db5d2-53fb-4d50-8164-2c1f00560c25", + "metadata": {}, + "source": [ + "**Fancy indexing**" + ] + }, { "cell_type": "code", "execution_count": null, - "id": "3e298c1c-7551-4401-ade0-b9af7d2bbe23", - "metadata": {}, + "id": "a89120eb-108b-4df3-8d3f-8192c9315aa6", + "metadata": { + "tags": [] + }, "outputs": [], "source": [ - "image_graphic.data = np.random.rand(512, 512)" + "image_graphic.data[data > 175] = 255" ] }, { "cell_type": "markdown", - "id": "67b92ffd-40cc-43fe-9df9-0e0d94763d8e", + "id": "096ccb73-bf6d-4dba-8168-788a63450406", "metadata": {}, "source": [ - "### Plots are indexable and give you their graphics by name" + "Adjust vmin vmax" ] }, { "cell_type": "code", "execution_count": null, - "id": "e6ba689c-ff4a-44ef-9663-f2c8755072c4", - "metadata": {}, + "id": "f8e69df8-7aaf-4d7c-92e3-861d9ebc8c5f", + "metadata": { + "tags": [] + }, "outputs": [], "source": [ - "plot.graphics" + "image_graphic.cmap.vmin = 50\n", + "image_graphic.cmap.vmax = 150" ] }, { "cell_type": "code", "execution_count": null, - "id": "5b18f4e3-e13b-46d5-af1f-285c5a7fdc12", - "metadata": {}, + "id": "aa67b34a-2694-4ec0-9ba2-e88c469f1a06", + "metadata": { + "tags": [] + }, "outputs": [], "source": [ - "plot[\"random-image\"]" + "# testing cell, ignore\n", + "plot_test(\"camera\", plot)" ] }, { "cell_type": "markdown", - "id": "4316a8b5-5f33-427a-8f52-b101d1daab67", + "id": "da9c9b25-7c8b-49b2-9531-7c741debd71d", "metadata": {}, "source": [ - "#### The `Graphic` instance is also returned when you call `plot.add_`." + "**Set the entire data array again**\n", + "\n", + "Note: The shape of the new data array must match the current data shown in the Graphic." ] }, { "cell_type": "code", "execution_count": null, - "id": "2b5c1321-1fd4-44bc-9433-7439ad3e22cf", - "metadata": {}, + "id": "089170fd-016e-4b2f-a090-c30beb85cc1b", + "metadata": { + "tags": [] + }, "outputs": [], "source": [ - "image_graphic" + "new_data = iio.imread(\"imageio:astronaut.png\")\n", + "new_data.shape" ] }, { - "cell_type": "code", - "execution_count": null, - "id": "b12bf75e-4e93-4930-9146-e96324fdf3f6", + "cell_type": "markdown", + "id": "d14cf14a-282f-40c6-b086-9bcf332ed0c8", "metadata": {}, - "outputs": [], "source": [ - "image_graphic is plot[\"random-image\"]" + "This is an RGB image, convert to grayscale to maintain the shape of (512, 512)" ] }, { - "cell_type": "markdown", - "id": "1cb03f42-1029-4b16-a16b-35447d9e2955", + "cell_type": "code", + "execution_count": null, + "id": "ec9b2874-ce1a-49c6-9b84-ee8f14d55966", "metadata": { "tags": [] }, + "outputs": [], "source": [ - "### Image updates\n", - "\n", - "This examples show how you can define animation functions that run on every render cycle." + "gray = new_data.dot([0.3, 0.6, 0.1])\n", + "gray.shape" ] }, { "cell_type": "code", "execution_count": null, - "id": "aadd757f-6379-4f52-a709-46aa57c56216", - "metadata": {}, + "id": "8a8fc1d3-19ba-42c0-b9ec-39f6ddd23314", + "metadata": { + "tags": [] + }, "outputs": [], "source": [ - "# create another `Plot` instance\n", - "plot_v = Plot()\n", - "\n", - "plot.canvas.max_buffered_frames = 1\n", - "\n", - "# make some random data again\n", - "data = np.random.rand(512, 512)\n", - "\n", - "# plot the data\n", - "plot_v.add_image(data=data, name=\"random-image\")\n", - "\n", - "# a function to update the image_graphic\n", - "# a plot will pass its plot instance to the animation function as an arugment\n", - "def update_data(plot_instance):\n", - " new_data = np.random.rand(512, 512)\n", - " plot_instance[\"random-image\"].data = new_data\n", - "\n", - "#add this as an animation function\n", - "plot_v.add_animations(update_data)\n", - "\n", - "# show the plot\n", - "plot_v.show()" + "image_graphic.data = gray" ] }, { "cell_type": "markdown", - "id": "b313eda1-6e6c-466f-9fd5-8b70c1d3c110", + "id": "bb568f89-ac92-4dde-9359-789049dc758a", "metadata": {}, "source": [ - "### We can share controllers across plots\n", "\n", - "This example creates a new plot, but it synchronizes the pan-zoom controller" + "\n", + "reset vmin vmax" ] }, { "cell_type": "code", "execution_count": null, - "id": "86e70b1e-4328-4035-b992-70dff16d2a69", - "metadata": {}, + "id": "de09d977-88ea-472c-8d89-9e24abc845a9", + "metadata": { + "tags": [] + }, "outputs": [], "source": [ - "plot_sync = Plot(controller=plot_v.controller)\n", - "\n", - "data = np.random.rand(512, 512)\n", - "\n", - "image_graphic_instance = plot_sync.add_image(data=data, cmap=\"viridis\")\n", - "\n", - "# you will need to define a new animation function for this graphic\n", - "def update_data_2():\n", - " new_data = np.random.rand(512, 512)\n", - " # alternatively, you can use the stored reference to the graphic as well instead of indexing the Plot\n", - " image_graphic_instance.data = new_data\n", - "\n", - "plot_sync.add_animations(update_data_2)\n", - "\n", - "plot_sync.show()" + "image_graphic.cmap.reset_vmin_vmax()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9cf84998-03e1-41b3-8e63-92d5b59426e6", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "# testing cell, ignore\n", + "plot_test(\"astronaut\", plot)" ] }, { "cell_type": "markdown", - "id": "f226c9c2-8d0e-41ab-9ab9-1ae31fd91de5", + "id": "b53bc11a-ddf1-4786-8dca-8f3d2eaf993d", "metadata": {}, "source": [ - "#### Keeping a reference to the Graphic instance, as shown above `image_graphic_instance`, is useful if you're creating something where you need flexibility in the naming of the graphics" + "### Indexing plots" ] }, { "cell_type": "markdown", - "id": "d11fabb7-7c76-4e94-893d-80ed9ee3be3d", + "id": "67b92ffd-40cc-43fe-9df9-0e0d94763d8e", "metadata": {}, "source": [ - "### You can also use `ipywidgets.VBox` and `HBox` to stack plots. See the `gridplot` notebooks for a proper gridplot interface for more automated subplotting" + "**Plots are indexable and give you their graphics by name**" ] }, { "cell_type": "code", "execution_count": null, - "id": "ef9743b3-5f81-4b79-9502-fa5fca08e56d", - "metadata": {}, + "id": "e6ba689c-ff4a-44ef-9663-f2c8755072c4", + "metadata": { + "tags": [] + }, "outputs": [], "source": [ - "VBox([plot_v.show(), plot_sync.show()])" + "plot" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5b18f4e3-e13b-46d5-af1f-285c5a7fdc12", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "plot[\"sample-image\"]" ] }, { "cell_type": "markdown", - "id": "be53ca2c-1d97-4cbd-b88b-a414bf0bcc4a", + "id": "a64314bf-a737-4858-803b-ea2adbd3578c", "metadata": {}, "source": [ - "# Please note that `HBox` can be buggy and crash the kernel, avoid using it\n", - "### This is an upstream issue in `jupyter-rfb`" + "**You can also use numerical indexing on `plot.graphics`**" ] }, { "cell_type": "code", "execution_count": null, - "id": "11839d95-8ff7-444c-ae13-6b072c3112c5", - "metadata": {}, + "id": "c09a1924-70f8-4d9e-9e92-510d700ac715", + "metadata": { + "tags": [] + }, "outputs": [], "source": [ - "HBox([plot_v.show(), plot_sync.show()])" + "plot.graphics" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ec9e6ba6-553f-4718-ba13-471c8c7c3c4e", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "plot.graphics[0]" ] }, { "cell_type": "markdown", - "id": "eaaeac07-5046-4e17-ab17-b685645e65f4", + "id": "4316a8b5-5f33-427a-8f52-b101d1daab67", "metadata": {}, "source": [ - "# Sliders to scroll through image data\n", - "\n", - "We often already have large image arrays (whether in RAM or through lazy loading), and want to view 2D frames across one or more dimensions. There is an `ImageWidget` that should really be used for this, but this example just shows how you can use `ipywidgets` to change data or any **`GraphicFeature`**" + "The `Graphic` instance is also returned when you call `plot.add_`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2b5c1321-1fd4-44bc-9433-7439ad3e22cf", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "image_graphic" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b12bf75e-4e93-4930-9146-e96324fdf3f6", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "image_graphic == plot[\"sample-image\"]" ] }, { "cell_type": "markdown", - "id": "6c9616e3-6ad0-4aa6-9f9f-f3282b05b0f1", + "id": "5694dca1-1041-4e09-a1da-85b293c5af47", "metadata": {}, "source": [ - "### Some code to generate a bunch of time-varying Gaussians. This code is NOT important for understanding `fastplotlib`, it just generates some video-like data for us to visualize!" + "### RGB images are also supported\n", + "\n", + "`cmap` arguments are ignored for rgb images, but vmin vmax still works" ] }, { "cell_type": "code", "execution_count": null, - "id": "0bcedf83-cbdd-4ec2-b8d5-172aa72a3e04", - "metadata": {}, + "id": "d6b8ca51-073d-47aa-a464-44511fcaccbc", + "metadata": { + "tags": [] + }, "outputs": [], "source": [ - "import numpy as np\n", - "from scipy.stats import multivariate_normal\n", + "plot_rgb = Plot()\n", "\n", - "# set up gaussians centered at component_centers\n", - "n_frames = 1000\n", - "spatial_dims = 512\n", + "plot_rgb.add_image(new_data, name=\"rgb-image\")\n", "\n", - "frame_shape = [512, 512]\n", - "\n", - "n_components = 32\n", - "component_centers = (np.random.rand(n_components, 2) * spatial_dims).astype(int)\n", - "\n", - "# create component images: stack of images one for ech component\n", - "spatial_sigma = 50\n", - "x, y = np.meshgrid(\n", - " np.arange(0, spatial_dims),\n", - " np.arange(0, spatial_dims)\n", - ")\n", + "plot_rgb.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "71eae361-3bbf-4d1f-a903-3615d35b557b", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "plot_rgb.camera.world.scale_y *= -1" + ] + }, + { + "cell_type": "markdown", + "id": "7fc66377-00e8-4f32-9671-9cf63f74529f", + "metadata": {}, + "source": [ + "vmin and vmax are still applicable to rgb images" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "cafaa403-50a2-403c-b8e7-b0938d48cadd", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "plot_rgb[\"rgb-image\"].cmap.vmin = 100" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b8d600c7-aa80-4c3f-8ec0-6641e9359c3a", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "# testing cell, ignore\n", + "plot_test(\"astronaut_RGB\", plot_rgb)" + ] + }, + { + "cell_type": "markdown", + "id": "1cb03f42-1029-4b16-a16b-35447d9e2955", + "metadata": { + "tags": [] + }, + "source": [ + "### Image updates\n", "\n", - "pos = np.dstack((x, y))\n", - "component_sigma = np.array(\n", - " [[spatial_sigma, 0],\n", - " [0, spatial_sigma]]\n", - ")\n", + "This examples show how you can define animation functions that run on every render cycle." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "aadd757f-6379-4f52-a709-46aa57c56216", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "# create another `Plot` instance\n", + "plot_v = Plot()\n", "\n", - "component_images = []\n", - "for comp_ix in range(n_components):\n", - " comp_mean = component_centers[comp_ix]\n", - " gauss_rep = multivariate_normal(comp_mean, component_sigma)\n", - " gauss_img = gauss_rep.pdf(pos)\n", - " component_images.append(gauss_img)\n", + "plot.canvas.max_buffered_frames = 1\n", "\n", - "component_images = np.array(component_images)\n", + "# make some random data again\n", + "data = np.random.rand(512, 512)\n", "\n", + "# plot the data\n", + "plot_v.add_image(data=data, name=\"random-image\")\n", "\n", - "# generate traces\n", - "tau = 10\n", - "max_amp = 2000\n", - "amps_all = []\n", + "# a function to update the image_graphic\n", + "# a plot will pass its plot instance to the animation function as an arugment\n", + "def update_data(plot_instance):\n", + " new_data = np.random.rand(512, 512)\n", + " plot_instance[\"random-image\"].data = new_data\n", "\n", - "for component_num in range(n_components):\n", - " amps = []\n", - " amp = 0\n", - " for time_step in np.arange(n_frames):\n", - " if np.random.uniform(0,1) > 0.98:\n", - " amp = max_amp\n", - " else:\n", - " amp = np.max(np.array([amp - amp/tau, 0]));\n", - " amps.append(amp)\n", - " amps = np.array(amps)\n", - " amps_all.append(amps)\n", - "amps_all = np.array(amps_all)\n", + "#add this as an animation function\n", + "plot_v.add_animations(update_data)\n", "\n", - "# create movie\n", - "movie = np.zeros((n_frames, spatial_dims, spatial_dims))\n", - "for frame_ix in np.arange(n_frames):\n", - " for comp_ix in range(n_components):\n", - " movie[frame_ix] += amps_all[comp_ix][frame_ix] * component_images[comp_ix]" + "# show the plot\n", + "plot_v.show()" ] }, { "cell_type": "markdown", - "id": "9ac18409-56d8-46cc-86bf-32456fcece48", + "id": "b313eda1-6e6c-466f-9fd5-8b70c1d3c110", "metadata": {}, "source": [ - "### The point is, we have a movie of the following shape, an image sequence" + "### We can share controllers across plots\n", + "\n", + "This example creates a new plot, but it synchronizes the pan-zoom controller" ] }, { "cell_type": "code", "execution_count": null, - "id": "8b560151-c258-415c-a20d-3cccd421f44a", + "id": "86e70b1e-4328-4035-b992-70dff16d2a69", "metadata": {}, "outputs": [], "source": [ - "movie.shape" + "plot_sync = Plot(controller=plot_v.controller)\n", + "\n", + "data = np.random.rand(512, 512)\n", + "\n", + "image_graphic_instance = plot_sync.add_image(data=data, cmap=\"viridis\")\n", + "\n", + "# you will need to define a new animation function for this graphic\n", + "def update_data_2():\n", + " new_data = np.random.rand(512, 512)\n", + " # alternatively, you can use the stored reference to the graphic as well instead of indexing the Plot\n", + " image_graphic_instance.data = new_data\n", + "\n", + "plot_sync.add_animations(update_data_2)\n", + "\n", + "plot_sync.show()" ] }, { "cell_type": "markdown", - "id": "f70cf836-222a-40ef-835f-1d2a02331a12", + "id": "f226c9c2-8d0e-41ab-9ab9-1ae31fd91de5", "metadata": {}, "source": [ - "### This is usually [time, x, y]" + "#### Keeping a reference to the Graphic instance, as shown above `image_graphic_instance`, is useful if you're creating something where you need flexibility in the naming of the graphics" ] }, { "cell_type": "markdown", - "id": "836fef2e-5a27-44ec-9d7e-943d496b7864", + "id": "d11fabb7-7c76-4e94-893d-80ed9ee3be3d", "metadata": {}, "source": [ - "## Plot and scroll through the first dimension with a slider" + "### You can also use `ipywidgets.VBox` and `HBox` to stack plots. See the `gridplot` notebooks for a proper gridplot interface for more automated subplotting" ] }, { "cell_type": "code", "execution_count": null, - "id": "62166a9f-ab43-45cc-a6db-6d441387e9a5", + "id": "ef9743b3-5f81-4b79-9502-fa5fca08e56d", "metadata": {}, "outputs": [], "source": [ - "plot_movie = Plot()\n", - "\n", - "# plot the first frame to initialize\n", - "movie_graphic = plot_movie.add_image(movie[0], vmin=0, vmax=movie.max(), cmap=\"gnuplot2\")\n", - "\n", - "# make a slider\n", - "slider = IntSlider(min=0, max=movie.shape[0] - 1, step=1, value=0)\n", - "\n", - "# function to update movie_graphic\n", - "def update_movie(change):\n", - " global movie\n", - " global movie_graphic\n", - " \n", - " index = change[\"new\"]\n", - " movie_graphic.data = movie[index]\n", - " \n", - "slider.observe(update_movie, \"value\")\n", - " \n", - "# Use an ipywidgets VBox to show the plot and slider\n", - "VBox([plot_movie.show(), slider])" + "VBox([plot_v.show(), plot_sync.show()])" ] }, { - "cell_type": "markdown", - "id": "876f1f89-c12e-44a5-9b00-9e6b4781b584", - "metadata": { - "jp-MarkdownHeadingCollapsed": true, - "tags": [] - }, + "cell_type": "code", + "execution_count": null, + "id": "11839d95-8ff7-444c-ae13-6b072c3112c5", + "metadata": {}, + "outputs": [], "source": [ - "#### Note that the use of globals in the `update_movie()` here can get messy, this is not recommended and you should create a class to properly handle combining widgets like this. _However_ if you want slider widgets for imaging data the recommended way to do this is by using the `ImageWidget`, see the `image_widget.ipynb` notebook for details." + "HBox([plot_v.show(), plot_sync.show()])" ] }, { @@ -522,6 +691,19 @@ "plot_l.show()" ] }, + { + "cell_type": "code", + "execution_count": null, + "id": "a4060576-2f29-4e4b-a86a-0410c766bd98", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "# testing cell, ignore\n", + "plot_test(\"lines\", plot_l)" + ] + }, { "cell_type": "markdown", "id": "22dde600-0f56-4370-b017-c8f23a6c01aa", @@ -529,7 +711,9 @@ "source": [ "### \"stretching\" the camera, useful for large timeseries data\n", "\n", - "Set `maintain_aspect = False` on a camera, and then use the right mouse button and move the mouse to stretch and squeeze the view!" + "Set `maintain_aspect = False` on a camera, and then use the right mouse button and move the mouse to stretch and squeeze the view!\n", + "\n", + "You can also click the **`1:1`** button to toggle this." ] }, { @@ -626,6 +810,19 @@ "cosine_graphic.colors[15:50:3] = \"cyan\"" ] }, + { + "cell_type": "code", + "execution_count": null, + "id": "ef8cab1b-8327-43e2-b021-176125b91ca9", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "# testing cell, ignore\n", + "plot_test(\"lines-colors\", plot_l)" + ] + }, { "cell_type": "markdown", "id": "c29f81f9-601b-49f4-b20c-575c56e58026", @@ -655,6 +852,19 @@ "cosine_graphic.data[0] = np.array([[-10, 0, 0]])" ] }, + { + "cell_type": "code", + "execution_count": null, + "id": "96086bd4-cdaa-467d-a68b-1f57002ad6c5", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "# testing cell, ignore\n", + "plot_test(\"lines-data\", plot_l)" + ] + }, { "cell_type": "markdown", "id": "3f6d264b-1b03-407e-9d83-cd6cfb02e706", @@ -721,6 +931,46 @@ "sinc_graphic.present = True" ] }, + { + "cell_type": "markdown", + "id": "05f93e93-283b-45d8-ab31-8d15a7671dd2", + "metadata": {}, + "source": [ + "### You can set the z-positions of graphics to have them appear under other graphics" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6bb33406-5bef-455b-86ea-358a7d3ffa94", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "img = iio.imread(\"imageio:camera.png\")\n", + "\n", + "plot_l.add_image(img[::20, ::20], name=\"image\", cmap=\"gray\")\n", + "\n", + "# z axix position -1 so it is below all the lines\n", + "plot_l[\"image\"].position_z = -1\n", + "plot_l[\"image\"].position_x = -8\n", + "plot_l[\"image\"].position_y = -8" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ae3e3dc9-e49b-430a-8471-5d0a0d659d20", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "# testing cell, ignore\n", + "plot_test(\"lines-underlay\", plot_l)" + ] + }, { "cell_type": "markdown", "id": "2c90862e-2f2a-451f-a468-0cf6b857e87a", @@ -767,6 +1017,19 @@ "plot_l3d.auto_scale(maintain_aspect=True)" ] }, + { + "cell_type": "code", + "execution_count": null, + "id": "5135f3f1-a004-4451-86cd-ead6acea6e13", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "# testing cell, ignore\n", + "plot_test(\"lines-3d\", plot_l3d)" + ] + }, { "cell_type": "markdown", "id": "a202b3d0-2a0b-450a-93d4-76d0a1129d1d", @@ -803,7 +1066,7 @@ "\n", "# if you have a good GPU go for 1.5 million points :D \n", "# this is multiplied by 3\n", - "n_points = 500_000\n", + "#n_points = 500_000\n", "\n", "# dimensions always have to be [n_points, xyz]\n", "dims = (n_points, 3)\n", @@ -925,6 +1188,17 @@ "id": "370d5837-aecf-4e52-9323-b899ac458bbf", "metadata": {}, "outputs": [], + "source": [ + "# for testing, ignore\n", + "notebook_finished()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "52b6c281-ab27-4984-9a6e-f1e27f609e44", + "metadata": {}, + "outputs": [], "source": [] } ], diff --git a/examples/tests/testutils.py b/examples/tests/testutils.py index 7bc271e02..6155f8763 100644 --- a/examples/tests/testutils.py +++ b/examples/tests/testutils.py @@ -9,7 +9,7 @@ ROOT = Path(__file__).parents[2] # repo root -examples_dir = ROOT / "examples" +examples_dir = ROOT / "examples" / "desktop" screenshots_dir = examples_dir / "screenshots" diffs_dir = examples_dir / "diffs" diff --git a/fastplotlib/VERSION b/fastplotlib/VERSION index 31af98a76..99bed0205 100644 --- a/fastplotlib/VERSION +++ b/fastplotlib/VERSION @@ -1 +1 @@ -0.1.0.a11 +0.1.0.a12 diff --git a/fastplotlib/__init__.py b/fastplotlib/__init__.py index 7eef88276..301412aff 100644 --- a/fastplotlib/__init__.py +++ b/fastplotlib/__init__.py @@ -1,9 +1,10 @@ from pathlib import Path -from wgpu.gui.auto import run +from .layouts import Plot, GridPlot +from .graphics import * +from .graphics.selectors import * -from .plot import Plot -from .layouts import GridPlot +from wgpu.gui.auto import run try: import ipywidgets @@ -15,3 +16,10 @@ with open(Path(__file__).parent.joinpath("VERSION"), "r") as f: __version__ = f.read().split("\n")[0] + +__all__ = [ + "Plot", + "GridPlot", + "run", + "ImageWidget" +] diff --git a/fastplotlib/graphics/__init__.py b/fastplotlib/graphics/__init__.py index 5a4786ca2..2a008015e 100644 --- a/fastplotlib/graphics/__init__.py +++ b/fastplotlib/graphics/__init__.py @@ -1,4 +1,3 @@ -from .histogram import HistogramGraphic from .line import LineGraphic from .scatter import ScatterGraphic from .image import ImageGraphic, HeatmapGraphic @@ -9,9 +8,8 @@ "ImageGraphic", "ScatterGraphic", "LineGraphic", - "HistogramGraphic", "HeatmapGraphic", "LineCollection", "LineStack", - "TextGraphic" + "TextGraphic", ] diff --git a/fastplotlib/graphics/_base.py b/fastplotlib/graphics/_base.py index 5d186747b..d30f7175f 100644 --- a/fastplotlib/graphics/_base.py +++ b/fastplotlib/graphics/_base.py @@ -1,17 +1,14 @@ from typing import * import weakref from warnings import warn +from abc import ABC, abstractmethod +from dataclasses import dataclass import numpy as np -from .features._base import cleanup_slice - -from pygfx import WorldObject, Group -from .features import GraphicFeature, PresentFeature, GraphicFeatureIndexable - -from abc import ABC, abstractmethod -from dataclasses import dataclass +from pygfx import WorldObject +from ._features import GraphicFeature, PresentFeature, GraphicFeatureIndexable # dict that holds all world objects for a given python kernel/session # Graphic objects only use proxies to WorldObjects @@ -37,21 +34,22 @@ class BaseGraphic: def __init_subclass__(cls, **kwargs): """set the type of the graphic in lower case like "image", "line_collection", etc.""" - cls.type = cls.__name__\ - .lower()\ - .replace("graphic", "")\ - .replace("collection", "_collection")\ + cls.type = ( + cls.__name__.lower() + .replace("graphic", "") + .replace("collection", "_collection") .replace("stack", "_stack") + ) super().__init_subclass__(**kwargs) class Graphic(BaseGraphic): def __init__( - self, - name: str = None, - metadata: Any = None, - collection_index: int = None, + self, + name: str = None, + metadata: Any = None, + collection_index: int = None, ): """ @@ -77,6 +75,7 @@ def __init__( @property def world_object(self) -> WorldObject: """Associated pygfx WorldObject. Always returns a proxy, real object cannot be accessed directly.""" + # We use weakref to simplify garbage collection return weakref.proxy(WORLD_OBJECTS[hex(id(self))]) def _set_world_object(self, wo: WorldObject): @@ -84,20 +83,22 @@ def _set_world_object(self, wo: WorldObject): @property def position(self) -> np.ndarray: - """The position of the graphic. You can access or change - using position.x, position.y, etc.""" + """position of the graphic, [x, y, z]""" return self.world_object.world.position @property def position_x(self) -> float: + """x-axis position of the graphic""" return self.world_object.world.x @property def position_y(self) -> float: + """y-axis position of the graphic""" return self.world_object.world.y @property def position_z(self) -> float: + """z-axis position of the graphic""" return self.world_object.world.z @position.setter @@ -163,6 +164,7 @@ def __del__(self): class Interaction(ABC): """Mixin class that makes graphics interactive""" + @abstractmethod def _set_feature(self, feature: str, new_data: Any, indices: Any): pass @@ -172,13 +174,13 @@ def _reset_feature(self, feature: str): pass def link( - self, - event_type: str, - target: Any, - feature: str, - new_data: Any, - callback: callable = None, - bidirectional: bool = False + self, + event_type: str, + target: Any, + feature: str, + new_data: Any, + callback: callable = None, + bidirectional: bool = False, ): """ Link this graphic to another graphic upon an ``event_type`` to change the ``feature`` @@ -192,14 +194,18 @@ def link( or appropriate feature event (ex. colors, data, etc.) associated with the graphic (can use ``graphic_instance.feature_events`` to get a tuple of the valid feature events for the graphic) + target: Any graphic to be linked to + feature: str feature (ex. colors, data, etc.) of the target graphic that will change following the event + new_data: Any appropriate data that will be changed in the feature of the target graphic after the event occurs + callback: callable, optional user-specified callable that will handle event, the callable must take the following four arguments @@ -207,9 +213,11 @@ def link( | ''target'' - the graphic to be changed following the event | ''event'' - the ''pygfx event'' or ''feature event'' that occurs | ''new_data'' - the appropriate data of the ''target'' that will be changed + bidirectional: bool, default False if True, the target graphic is also linked back to this graphic instance using the same arguments + For example: .. code-block::python @@ -231,21 +239,32 @@ def link( feature_instance.add_event_handler(self._event_handler) else: - raise ValueError(f"Invalid event, valid events are: {PYGFX_EVENTS + self.feature_events}") + raise ValueError( + f"Invalid event, valid events are: {PYGFX_EVENTS + self.feature_events}" + ) # make sure target feature is valid if feature is not None: if feature not in target.feature_events: - raise ValueError(f"Invalid feature for target, valid features are: {target.feature_events}") + raise ValueError( + f"Invalid feature for target, valid features are: {target.feature_events}" + ) if event_type not in self.registered_callbacks.keys(): self.registered_callbacks[event_type] = list() - callback_data = CallbackData(target=target, feature=feature, new_data=new_data, callback_function=callback) + callback_data = CallbackData( + target=target, + feature=feature, + new_data=new_data, + callback_function=callback, + ) for existing_callback_data in self.registered_callbacks[event_type]: if existing_callback_data == callback_data: - warn("linkage already exists for given event, target, and data, skipping") + warn( + "linkage already exists for given event, target, and data, skipping" + ) return self.registered_callbacks[event_type].append(callback_data) @@ -254,7 +273,7 @@ def link( if event_type in PYGFX_EVENTS: warn("cannot use bidirectional link for pygfx events") return - + target.link( event_type=event_type, target=self, @@ -262,7 +281,7 @@ def link( new_data=new_data, callback=callback, bidirectional=False # else infinite recursion, otherwise target will call - # this instance .link(), and then it will happen again etc. + # this instance .link(), and then it will happen again etc. ) def _event_handler(self, event): @@ -271,7 +290,12 @@ def _event_handler(self, event): for target_info in self.registered_callbacks[event.type]: if target_info.callback_function is not None: # if callback_function is not None, then callback function should handle the entire event - target_info.callback_function(source=self, target=target_info.target, event=event, new_data=target_info.new_data) + target_info.callback_function( + source=self, + target=target_info.target, + event=event, + new_data=target_info.new_data, + ) elif isinstance(self, GraphicCollection): # if target is a GraphicCollection, then indices will be stored in collection_index @@ -288,16 +312,24 @@ def _event_handler(self, event): # the real world object in the pick_info and not the proxy if wo is event.pick_info["world_object"]: indices = i - target_info.target._set_feature(feature=target_info.feature, new_data=target_info.new_data, indices=indices) + target_info.target._set_feature( + feature=target_info.feature, + new_data=target_info.new_data, + indices=indices, + ) else: # if target is a single graphic, then indices do not matter - target_info.target._set_feature(feature=target_info.feature, new_data=target_info.new_data, - indices=None) + target_info.target._set_feature( + feature=target_info.feature, + new_data=target_info.new_data, + indices=None, + ) @dataclass class CallbackData: """Class for keeping track of the info necessary for interactivity after event occurs.""" + target: Any feature: str new_data: Any @@ -329,6 +361,7 @@ def __eq__(self, other): @dataclass class PreviouslyModifiedData: """Class for keeping track of previously modified data at indices""" + data: Any indices: Any @@ -350,7 +383,9 @@ def __init__(self, name: str = None): def graphics(self) -> np.ndarray[Graphic]: """The Graphics within this collection. Always returns a proxy to the Graphics.""" if self._graphics_changed: - proxies = [weakref.proxy(COLLECTION_GRAPHICS[loc]) for loc in self._graphics] + proxies = [ + weakref.proxy(COLLECTION_GRAPHICS[loc]) for loc in self._graphics + ] self._graphics_array = np.array(proxies) self._graphics_array.flags["WRITEABLE"] = False self._graphics_changed = False @@ -358,8 +393,20 @@ def graphics(self) -> np.ndarray[Graphic]: return self._graphics_array def add_graphic(self, graphic: Graphic, reset_index: False): - """Add a graphic to the collection""" - if not isinstance(graphic, self.child_type): + """ + Add a graphic to the collection. + + Parameters + ---------- + graphic: Graphic + graphic to add, must be a real ``Graphic`` not a proxy + + reset_index: bool, default ``False`` + reset the collection index + + """ + + if not type(graphic).__name__ == self.child_type: raise TypeError( f"Can only add graphics of the same type to a collection, " f"You can only add {self.child_type} to a {self.__class__.__name__}, " @@ -381,7 +428,19 @@ def add_graphic(self, graphic: Graphic, reset_index: False): self._graphics_changed = True def remove_graphic(self, graphic: Graphic, reset_index: True): - """Remove a graphic from the collection""" + """ + Remove a graphic from the collection. + + Parameters + ---------- + graphic: Graphic + graphic to remove + + reset_index: bool, default ``False`` + reset the collection index + + """ + self._graphics.remove(graphic.loc) if reset_index: @@ -395,15 +454,14 @@ def __getitem__(self, key): return CollectionIndexer( parent=self, selection=self.graphics[key], - # selection_indices=key ) - + def __del__(self): self.world_object.clear() for loc in self._graphics: del COLLECTION_GRAPHICS[loc] - + super().__del__() def _reset_index(self): @@ -420,11 +478,11 @@ def __repr__(self): class CollectionIndexer: """Collection Indexer""" + def __init__( - self, - parent: GraphicCollection, - selection: List[Graphic], - # selection_indices: Union[list, range], + self, + parent: GraphicCollection, + selection: List[Graphic], ): """ @@ -436,13 +494,10 @@ def __init__( selection: list of Graphics a list of the selected Graphics from the parent GraphicCollection based on the ``selection_indices`` - selection_indices: Union[list, range] - the corresponding indices from the parent GraphicCollection that were selected """ self._parent = weakref.proxy(parent) self._selection = selection - # self._selection_indices = selection_indices # we use parent.graphics[0] instead of selection[0] # because the selection can be empty @@ -450,17 +505,16 @@ def __init__( attr = getattr(self._parent.graphics[0], attr_name) if isinstance(attr, GraphicFeature): collection_feature = CollectionFeature( - parent, - self._selection, - # selection_indices=self._selection_indices, - feature=attr_name + self._selection, feature=attr_name + ) + collection_feature.__doc__ = ( + f"indexable <{attr_name}> feature for collection" ) - collection_feature.__doc__ = f"indexable <{attr_name}> feature for collection" setattr(self, attr_name, collection_feature) @property - def graphics(self) -> Tuple[Graphic]: - """Returns a tuple of the selected graphics.""" + def graphics(self) -> np.ndarray[Graphic]: + """Returns an array of the selected graphics. Always returns a proxy to the Graphic""" return tuple(self._selection) def __setattr__(self, key, value): @@ -476,31 +530,26 @@ def __len__(self): return len(self._selection) def __repr__(self): - return f"{self.__class__.__name__} @ {hex(id(self))}\n" \ - f"Selection of <{len(self._selection)}> {self._selection[0].__class__.__name__}" + return ( + f"{self.__class__.__name__} @ {hex(id(self))}\n" + f"Selection of <{len(self._selection)}> {self._selection[0].__class__.__name__}" + ) class CollectionFeature: """Collection Feature""" - def __init__( - self, - parent: GraphicCollection, - selection: List[Graphic], - # selection_indices, - feature: str - ): + + def __init__(self, selection: List[Graphic], feature: str): """ - parent: GraphicCollection - GraphicCollection feature instance that is being indexed selection: list of Graphics a list of the selected Graphics from the parent GraphicCollection based on the ``selection_indices`` - selection_indices: Union[list, range] - the corresponding indices from the parent GraphicCollection that were selected + feature: str feature of Graphics in the GraphicCollection being indexed + """ + self._selection = selection - # self._selection_indices = selection_indices self._feature = feature self._feature_instances: List[GraphicFeature] = list() @@ -550,4 +599,3 @@ def block_events(self, b: bool): def __repr__(self): return f"Collection feature for: <{self._feature}>" - diff --git a/fastplotlib/graphics/_features/__init__.py b/fastplotlib/graphics/_features/__init__.py new file mode 100644 index 000000000..8e78a6260 --- /dev/null +++ b/fastplotlib/graphics/_features/__init__.py @@ -0,0 +1,24 @@ +from ._colors import ColorFeature, CmapFeature, ImageCmapFeature, HeatmapCmapFeature +from ._data import PointsDataFeature, ImageDataFeature, HeatmapDataFeature +from ._present import PresentFeature +from ._thickness import ThicknessFeature +from ._base import GraphicFeature, GraphicFeatureIndexable, FeatureEvent, to_gpu_supported_dtype +from ._selection_features import LinearSelectionFeature, LinearRegionSelectionFeature + +__all__ = [ + "ColorFeature", + "CmapFeature", + "ImageCmapFeature", + "HeatmapCmapFeature", + "PointsDataFeature", + "ImageDataFeature", + "HeatmapDataFeature", + "PresentFeature", + "ThicknessFeature", + "GraphicFeature", + "GraphicFeatureIndexable", + "FeatureEvent", + "to_gpu_supported_dtype", + "LinearSelectionFeature", + "LinearRegionSelectionFeature", +] diff --git a/fastplotlib/graphics/features/_base.py b/fastplotlib/graphics/_features/_base.py similarity index 83% rename from fastplotlib/graphics/features/_base.py rename to fastplotlib/graphics/_features/_base.py index bb8d35469..5616eec19 100644 --- a/fastplotlib/graphics/features/_base.py +++ b/fastplotlib/graphics/_features/_base.py @@ -3,10 +3,10 @@ from warnings import warn from typing import * import weakref -from dataclasses import dataclass import numpy as np -from pygfx import Buffer, Texture + +import pygfx supported_dtypes = [ @@ -17,7 +17,7 @@ np.int16, np.int32, np.float16, - np.float32 + np.float32, ] @@ -34,7 +34,9 @@ def to_gpu_supported_dtype(array): warn(f"converting {array.dtype} array to float32") return array.astype(np.float32, copy=False) else: - raise TypeError("Unsupported type, supported array types must be int or float dtypes") + raise TypeError( + "Unsupported type, supported array types must be int or float dtypes" + ) return array @@ -47,7 +49,7 @@ class FeatureEvent: ---------- type: str, example "colors" - pick_info: dict in the form: + pick_info: dict: ============== ============================================================================= key value @@ -57,20 +59,27 @@ class FeatureEvent: "new_data: the new data for this feature ============== ============================================================================= + .. note:: + pick info varies between features, this is just the general structure + """ + def __init__(self, type: str, pick_info: dict): self.type = type self.pick_info = pick_info def __repr__(self): - return f"{self.__class__.__name__} @ {hex(id(self))}\n" \ - f"type: {self.type}\n" \ - f"pick_info: {self.pick_info}\n" + return ( + f"{self.__class__.__name__} @ {hex(id(self))}\n" + f"type: {self.type}\n" + f"pick_info: {self.pick_info}\n" + ) class GraphicFeature(ABC): def __init__(self, parent, data: Any, collection_index: int = None): # not shown as a docstring so it doesn't show up in the docs + # # Parameters # ---------- # parent @@ -79,6 +88,7 @@ def __init__(self, parent, data: Any, collection_index: int = None): # # collection_index: int # if part of a collection, index of this graphic within the collection + self._parent = weakref.proxy(parent) self._data = to_gpu_supported_dtype(data) @@ -90,8 +100,17 @@ def __init__(self, parent, data: Any, collection_index: int = None): def __call__(self, *args, **kwargs): return self._data - def block_events(self, b: bool): - self._block_events = b + def block_events(self, val: bool): + """ + Block all events from this feature + + Parameters + ---------- + val: bool + ``True`` or ``False`` + + """ + self._block_events = val @abstractmethod def _set(self, value): @@ -106,9 +125,10 @@ def _parse_set_value(self, value): def add_event_handler(self, handler: callable): """ Add an event handler. All added event handlers are called when this feature changes. - The `handler` can optionally accept ``FeatureEvent`` as the first and only argument. + The ``handler`` can optionally accept a :class:`.FeatureEvent` as the first and only argument. The ``FeatureEvent`` only has two attributes, ``type`` which denotes the type of event - as a ``str`` in the form of "", such as "color". + as a ``str`` in the form of "", such as "color". And ``pick_info`` which contains + information about the event and Graphic that triggered it. Parameters ---------- @@ -127,7 +147,7 @@ def add_event_handler(self, handler: callable): def remove_event_handler(self, handler: callable): """ - Remove a registered event handler + Remove a registered event ``handler``. Parameters ---------- @@ -141,9 +161,10 @@ def remove_event_handler(self, handler: callable): self._event_handlers.remove(handler) def clear_event_handlers(self): + """Clear all event handlers""" self._event_handlers.clear() - #TODO: maybe this can be implemented right here in the base class + # TODO: maybe this can be implemented right here in the base class @abstractmethod def _feature_changed(self, key: Union[int, slice, Tuple[slice]], new_data: Any): """Called whenever a feature changes, and it calls all funcs in self._event_handlers""" @@ -165,7 +186,9 @@ def _call_event_handlers(self, event_data: FeatureEvent): else: func() except TypeError: - warn(f"Event handler {func} has an unresolvable argspec, calling it without arguments") + warn( + f"Event handler {func} has an unresolvable argspec, calling it without arguments" + ) func() @@ -192,9 +215,6 @@ def cleanup_slice(key: Union[int, slice], upper_bound) -> Union[slice, int]: if isinstance(key, np.ndarray): return cleanup_array_slice(key, upper_bound) - # if isinstance(key, np.integer): - # return int(key) - if isinstance(key, tuple): # if tuple of slice we only need the first obj # since the first obj is the datapoint indices @@ -222,14 +242,15 @@ def cleanup_slice(key: Union[int, slice], upper_bound) -> Union[slice, int]: stop = upper_bound elif stop > upper_bound: - raise IndexError(f"Index: `{stop}` out of bounds for feature array of size: `{upper_bound}`") + raise IndexError( + f"Index: `{stop}` out of bounds for feature array of size: `{upper_bound}`" + ) step = key.step if step is None: step = 1 return slice(start, stop, step) - # return slice(int(start), int(stop), int(step)) def cleanup_array_slice(key: np.ndarray, upper_bound) -> Union[np.ndarray, None]: @@ -253,9 +274,7 @@ def cleanup_array_slice(key: np.ndarray, upper_bound) -> Union[np.ndarray, None] """ if key.ndim > 1: - raise TypeError( - f"Can only use 1D boolean or integer arrays for fancy indexing" - ) + raise TypeError(f"Can only use 1D boolean or integer arrays for fancy indexing") # if boolean array convert to integer array of indices if key.dtype == bool: @@ -266,15 +285,15 @@ def cleanup_array_slice(key: np.ndarray, upper_bound) -> Union[np.ndarray, None] # make sure indices within bounds of feature buffer range if key[-1] > upper_bound: - raise IndexError(f"Index: `{key[-1]}` out of bounds for feature array of size: `{upper_bound}`") + raise IndexError( + f"Index: `{key[-1]}` out of bounds for feature array of size: `{upper_bound}`" + ) # make sure indices are integers if np.issubdtype(key.dtype, np.integer): return key - raise TypeError( - f"Can only use 1D boolean or integer arrays for fancy indexing" - ) + raise TypeError(f"Can only use 1D boolean or integer arrays for fancy indexing") class GraphicFeatureIndexable(GraphicFeature): @@ -298,7 +317,7 @@ def _update_range(self, key): @property @abstractmethod - def buffer(self) -> Union[Buffer, Texture]: + def buffer(self) -> Union[pygfx.Buffer, pygfx.Texture]: """Underlying buffer for this feature""" pass @@ -331,9 +350,6 @@ def _update_range_indices(self, key): # TODO: See how efficient this is with large indexing elif isinstance(key, np.ndarray): self.buffer.update_range() - # for ix in key: - # self.buffer.update_range(int(ix), size=1) else: raise TypeError("must pass int or slice to update range") - diff --git a/fastplotlib/graphics/features/_colors.py b/fastplotlib/graphics/_features/_colors.py similarity index 85% rename from fastplotlib/graphics/features/_colors.py rename to fastplotlib/graphics/_features/_colors.py index 414bdeccc..256a5d65f 100644 --- a/fastplotlib/graphics/features/_colors.py +++ b/fastplotlib/graphics/_features/_colors.py @@ -1,8 +1,14 @@ import numpy as np +import pygfx -from ._base import GraphicFeature, GraphicFeatureIndexable, cleanup_slice, FeatureEvent, cleanup_array_slice -from ...utils import make_colors, get_cmap_texture, make_pygfx_colors, parse_cmap_values -from pygfx import Color +from ...utils import make_colors, get_cmap_texture, make_pygfx_colors, parse_cmap_values, quick_min_max +from ._base import ( + GraphicFeature, + GraphicFeatureIndexable, + cleanup_slice, + FeatureEvent, + cleanup_array_slice, +) class ColorFeature(GraphicFeatureIndexable): @@ -20,16 +26,23 @@ class ColorFeature(GraphicFeatureIndexable): "world_object" pygfx.WorldObject world object ==================== =============================== ========================================================================= - """ + @property - def buffer(self): + def buffer(self) -> pygfx.Buffer: return self._parent.world_object.geometry.colors def __getitem__(self, item): return self.buffer.data[item] - def __init__(self, parent, colors, n_colors: int, alpha: float = 1.0, collection_index: int = None): + def __init__( + self, + parent, + colors, + n_colors: int, + alpha: float = 1.0, + collection_index: int = None, + ): """ ColorFeature @@ -55,11 +68,7 @@ def __init__(self, parent, colors, n_colors: int, alpha: float = 1.0, collection # if the color is provided as a numpy array if isinstance(colors, np.ndarray): if colors.shape == (4,): # single RGBA array - data = np.repeat( - np.array([colors]), - n_colors, - axis=0 - ) + data = np.repeat(np.array([colors]), n_colors, axis=0) # else assume it's already a stack of RGBA arrays, keep this directly as the data elif colors.ndim == 2: if colors.shape[1] != 4 and colors.shape[0] != n_colors: @@ -84,11 +93,11 @@ def __init__(self, parent, colors, n_colors: int, alpha: float = 1.0, collection f"where the length of the iterable is the same as the number of datapoints." ) - data = np.vstack([np.array(Color(c)) for c in colors]) + data = np.vstack([np.array(pygfx.Color(c)) for c in colors]) # if it's a single RGBA array as a tuple/list elif len(colors) == 4: - c = Color(colors) + c = pygfx.Color(colors) data = np.repeat(np.array([c]), n_colors, axis=0) else: @@ -109,7 +118,9 @@ def __init__(self, parent, colors, n_colors: int, alpha: float = 1.0, collection if alpha != 1.0: data[:, -1] = alpha - super(ColorFeature, self).__init__(parent, data, collection_index=collection_index) + super(ColorFeature, self).__init__( + parent, data, collection_index=collection_index + ) def __setitem__(self, key, value): # parse numerical slice indices @@ -130,7 +141,9 @@ def __setitem__(self, key, value): ) if len(key) != 2: - raise ValueError("fancy indexing for colors must be 2-dimension, i.e. [n_datapoints, RGBA]") + raise ValueError( + "fancy indexing for colors must be 2-dimension, i.e. [n_datapoints, RGBA]" + ) # set the user passed data directly self.buffer.data[key] = value @@ -151,17 +164,17 @@ def __setitem__(self, key, value): indices = key else: - raise TypeError("Graphic features only support integer and numerical fancy indexing") + raise TypeError( + "Graphic features only support integer and numerical fancy indexing" + ) new_data_size = len(indices) if not isinstance(value, np.ndarray): - color = np.array(Color(value)) # pygfx color parser + color = np.array(pygfx.Color(value)) # pygfx color parser # make it of shape [n_colors_modify, 4] new_colors = np.repeat( - np.array([color]).astype(np.float32), - new_data_size, - axis=0 + np.array([color]).astype(np.float32), new_data_size, axis=0 ) # if already a numpy array @@ -172,14 +185,14 @@ def __setitem__(self, key, value): # if there are more than 1 datapoint color to modify if new_data_size > 1: new_colors = np.repeat( - np.array([new_colors]).astype(np.float32), - new_data_size, - axis=0 + np.array([new_colors]).astype(np.float32), new_data_size, axis=0 ) elif value.ndim == 2: if value.shape[1] != 4 and value.shape[0] != new_data_size: - raise ValueError("numpy array passed to color must be of shape (4,) or (n_colors_modify, 4)") + raise ValueError( + "numpy array passed to color must be of shape (4,) or (n_colors_modify, 4)" + ) # if there is a single datapoint to change color of but user has provided shape [1, 4] if new_data_size == 1: new_colors = value.ravel().astype(np.float32) @@ -187,7 +200,9 @@ def __setitem__(self, key, value): new_colors = value.astype(np.float32) else: - raise ValueError("numpy array passed to color must be of shape (4,) or (n_colors_modify, 4)") + raise ValueError( + "numpy array passed to color must be of shape (4,) or (n_colors_modify, 4)" + ) self.buffer.data[key] = new_colors @@ -226,6 +241,7 @@ class CmapFeature(ColorFeature): Same event pick info as :class:`ColorFeature` """ + def __init__(self, parent, colors, cmap_name: str, cmap_values: np.ndarray): super(ColorFeature, self).__init__(parent, colors) @@ -235,8 +251,10 @@ def __init__(self, parent, colors, cmap_name: str, cmap_values: np.ndarray): def __setitem__(self, key, cmap_name): key = cleanup_slice(key, self._upper_bound) if not isinstance(key, (slice, np.ndarray)): - raise TypeError("Cannot set cmap on single indices, must pass a slice object, " - "numpy.ndarray or set it on the entire data.") + raise TypeError( + "Cannot set cmap on single indices, must pass a slice object, " + "numpy.ndarray or set it on the entire data." + ) if isinstance(key, slice): n_colors = len(range(key.start, key.stop, key.step)) @@ -246,9 +264,7 @@ def __setitem__(self, key, cmap_name): n_colors = key.size colors = parse_cmap_values( - n_colors=n_colors, - cmap_name=cmap_name, - cmap_values=self._cmap_values + n_colors=n_colors, cmap_name=cmap_name, cmap_values=self._cmap_values ) self._cmap_name = cmap_name @@ -264,9 +280,7 @@ def values(self, values: np.ndarray): values = np.array(values) colors = parse_cmap_values( - n_colors=self().shape[0], - cmap_name=self._cmap_name, - cmap_values=values + n_colors=self().shape[0], cmap_name=self._cmap_name, cmap_values=values ) self._cmap_values = values @@ -290,8 +304,8 @@ class ImageCmapFeature(GraphicFeature): "vmax" ``float`` maximum value ================ =================== =============== - """ + def __init__(self, parent, cmap: str): cmap_texture_view = get_cmap_texture(cmap) super(ImageCmapFeature, self).__init__(parent, cmap_texture_view) @@ -317,7 +331,7 @@ def vmin(self, value: float): """Minimum contrast limit.""" self._parent.world_object.material.clim = ( value, - self._parent.world_object.material.clim[1] + self._parent.world_object.material.clim[1], ) self._feature_changed(key=None, new_data=None) @@ -331,10 +345,14 @@ def vmax(self, value: float): """Maximum contrast limit.""" self._parent.world_object.material.clim = ( self._parent.world_object.material.clim[0], - value + value, ) self._feature_changed(key=None, new_data=None) + def reset_vmin_vmax(self): + """Reset vmin vmax values based on current data""" + self.vmin, self.vmax = quick_min_max(self._parent.data()) + def _feature_changed(self, key, new_data): # this is a non-indexable feature so key=None @@ -343,7 +361,7 @@ def _feature_changed(self, key, new_data): "world_object": self._parent.world_object, "name": self.name, "vmin": self.vmin, - "vmax": self.vmax + "vmax": self.vmax, } event_data = FeatureEvent(type="cmap", pick_info=pick_info) diff --git a/fastplotlib/graphics/features/_data.py b/fastplotlib/graphics/_features/_data.py similarity index 92% rename from fastplotlib/graphics/features/_data.py rename to fastplotlib/graphics/_features/_data.py index 0228e2a15..0d22299ed 100644 --- a/fastplotlib/graphics/features/_data.py +++ b/fastplotlib/graphics/_features/_data.py @@ -1,9 +1,16 @@ from typing import * import numpy as np -from pygfx import Buffer, Texture -from ._base import GraphicFeatureIndexable, cleanup_slice, FeatureEvent, to_gpu_supported_dtype, cleanup_array_slice +import pygfx + +from ._base import ( + GraphicFeatureIndexable, + cleanup_slice, + FeatureEvent, + to_gpu_supported_dtype, + cleanup_array_slice, +) class PointsDataFeature(GraphicFeatureIndexable): @@ -11,12 +18,15 @@ class PointsDataFeature(GraphicFeatureIndexable): Access to the vertex buffer data shown in the graphic. Supports fancy indexing if the data array also supports it. """ + def __init__(self, parent, data: Any, collection_index: int = None): data = self._fix_data(data, parent) - super(PointsDataFeature, self).__init__(parent, data, collection_index=collection_index) + super(PointsDataFeature, self).__init__( + parent, data, collection_index=collection_index + ) @property - def buffer(self) -> Buffer: + def buffer(self) -> pygfx.Buffer: return self._parent.world_object.geometry.positions def __getitem__(self, item): @@ -83,7 +93,7 @@ def _feature_changed(self, key, new_data): "index": indices, "collection-index": self._collection_index, "world_object": self._parent.world_object, - "new_data": new_data + "new_data": new_data, } event_data = FeatureEvent(type="data", pick_info=pick_info) @@ -106,7 +116,7 @@ def __init__(self, parent, data: Any): super(ImageDataFeature, self).__init__(parent, data) @property - def buffer(self) -> Texture: + def buffer(self) -> pygfx.Texture: """Texture buffer for the image data""" return self._parent.world_object.geometry.grid @@ -147,7 +157,7 @@ def _feature_changed(self, key, new_data): pick_info = { "index": indices, "world_object": self._parent.world_object, - "new_data": new_data + "new_data": new_data, } event_data = FeatureEvent(type="data", pick_info=pick_info) @@ -157,7 +167,7 @@ def _feature_changed(self, key, new_data): class HeatmapDataFeature(ImageDataFeature): @property - def buffer(self) -> List[Texture]: + def buffer(self) -> List[pygfx.Texture]: """list of Texture buffer for the image data""" return [img.geometry.grid for img in self._parent.world_object.children] @@ -195,7 +205,7 @@ def _feature_changed(self, key, new_data): pick_info = { "index": indices, "world_object": self._parent.world_object, - "new_data": new_data + "new_data": new_data, } event_data = FeatureEvent(type="data", pick_info=pick_info) diff --git a/fastplotlib/graphics/features/_present.py b/fastplotlib/graphics/_features/_present.py similarity index 96% rename from fastplotlib/graphics/features/_present.py rename to fastplotlib/graphics/_features/_present.py index 820c1d123..ba257e60b 100644 --- a/fastplotlib/graphics/features/_present.py +++ b/fastplotlib/graphics/_features/_present.py @@ -1,6 +1,7 @@ -from ._base import GraphicFeature, FeatureEvent from pygfx import Scene, Group +from ._base import GraphicFeature, FeatureEvent + class PresentFeature(GraphicFeature): """ @@ -17,8 +18,9 @@ class PresentFeature(GraphicFeature): "new_data" ``bool`` new data, ``True`` or ``False`` "collection-index" int the index of the graphic within the collection that triggered the event "world_object" pygfx.WorldObject world object - ==================== ======================== ======================================================================== + ==================== ======================== ========================================================================= """ + def __init__(self, parent, present: bool = True, collection_index: int = False): self._scene = None super(PresentFeature, self).__init__(parent, present, collection_index) @@ -58,7 +60,7 @@ def _feature_changed(self, key, new_data): "index": None, "collection-index": self._collection_index, "world_object": self._parent.world_object, - "new_data": new_data + "new_data": new_data, } event_data = FeatureEvent(type="present", pick_info=pick_info) diff --git a/fastplotlib/graphics/_features/_selection_features.py b/fastplotlib/graphics/_features/_selection_features.py new file mode 100644 index 000000000..ae486026e --- /dev/null +++ b/fastplotlib/graphics/_features/_selection_features.py @@ -0,0 +1,315 @@ +from typing import Tuple, Union, Any + +import numpy as np + +from ._base import GraphicFeature, FeatureEvent + + +""" +positions for indexing the BoxGeometry to set the "width" and "size" of the box +hacky, but I don't think we can morph meshes in pygfx yet: https://github.com/pygfx/pygfx/issues/346 +""" + +x_right = np.array( + [ + True, + True, + True, + True, + False, + False, + False, + False, + False, + True, + False, + True, + True, + False, + True, + False, + False, + True, + False, + True, + True, + False, + True, + False, + ] +) + +x_left = np.array( + [ + False, + False, + False, + False, + True, + True, + True, + True, + True, + False, + True, + False, + False, + True, + False, + True, + True, + False, + True, + False, + False, + True, + False, + True, + ] +) + +y_top = np.array( + [ + False, + True, + False, + True, + False, + True, + False, + True, + True, + True, + True, + True, + False, + False, + False, + False, + False, + False, + True, + True, + False, + False, + True, + True, + ] +) + +y_bottom = np.array( + [ + True, + False, + True, + False, + True, + False, + True, + False, + False, + False, + False, + False, + True, + True, + True, + True, + True, + True, + False, + False, + True, + True, + False, + False, + ] +) + + +class LinearSelectionFeature(GraphicFeature): + # A bit much to have a class for this but this allows it to integrate with the fastplotlib callback system + """ + Manages the linear selection and callbacks + + **event pick info** + + =================== =============================== ================================================================================================= + key type selection + =================== =============================== ================================================================================================= + "selected_index" ``int`` the graphic data index that corresponds to the selector position + "world_object" ``pygfx.WorldObject`` pygfx WorldObject + "new_data" ``numpy.ndarray`` or ``None`` the new selector position in world coordinates, not necessarily the same as "selected_index" + "graphic" ``Graphic`` the selector graphic + "delta" ``numpy.ndarray`` the delta vector of the graphic in NDC + "pygfx_event" ``pygfx.Event`` pygfx Event + =================== =============================== ================================================================================================= + + """ + + def __init__(self, parent, axis: str, value: float, limits: Tuple[int, int]): + super(LinearSelectionFeature, self).__init__(parent, data=value) + + self.axis = axis + self.limits = limits + + def _set(self, value: float): + if not (self.limits[0] <= value <= self.limits[1]): + return + + if self.axis == "x": + self._parent.position_x = value + else: + self._parent.position_y = value + + self._data = value + self._feature_changed(key=None, new_data=value) + + def _feature_changed(self, key: Union[int, slice, Tuple[slice]], new_data: Any): + if len(self._event_handlers) < 1: + return + + if self._parent.parent is not None: + g_ix = self._parent.get_selected_index() + else: + g_ix = None + + # get pygfx event and reset it + pygfx_ev = self._parent._pygfx_event + self._parent._pygfx_event = None + + pick_info = { + "world_object": self._parent.world_object, + "new_data": new_data, + "selected_index": g_ix, + "graphic": self._parent, + "pygfx_event": pygfx_ev, + "delta": self._parent.delta, + } + + event_data = FeatureEvent(type="selection", pick_info=pick_info) + + self._call_event_handlers(event_data) + + +class LinearRegionSelectionFeature(GraphicFeature): + """ + Feature for a linearly bounding region + + **event pick info** + + ===================== =============================== ======================================================================================= + key type description + ===================== =============================== ======================================================================================= + "selected_indices" ``numpy.ndarray`` or ``None`` selected graphic data indices + "world_object" ``pygfx.WorldObject`` pygfx World Object + "new_data" ``(float, float)`` current bounds in world coordinates, NOT necessarily the same as "selected_indices". + "graphic" ``Graphic`` the selection graphic + "delta" ``numpy.ndarray`` the delta vector of the graphic in NDC + "pygfx_event" ``pygfx.Event`` pygfx Event + "selected_data" ``numpy.ndarray`` or ``None`` selected graphic data + "move_info" ``MoveInfo`` last position and event source (pygfx.Mesh or pygfx.Line) + ===================== =============================== ======================================================================================= + + """ + + def __init__( + self, parent, selection: Tuple[int, int], axis: str, limits: Tuple[int, int] + ): + super(LinearRegionSelectionFeature, self).__init__(parent, data=selection) + + self._axis = axis + self.limits = limits + + self._set(selection) + + @property + def axis(self) -> str: + """one of "x" | "y" """ + return self._axis + + def _set(self, value: Tuple[float, float]): + # sets new bounds + if not isinstance(value, tuple): + raise TypeError( + "Bounds must be a tuple in the form of `(min_bound, max_bound)`, " + "where `min_bound` and `max_bound` are numeric values." + ) + + # make sure bounds not exceeded + for v in value: + if not (self.limits[0] <= v <= self.limits[1]): + return + + # make sure `selector width >= 2`, left edge must not move past right edge! + # or bottom edge must not move past top edge! + # has to be at least 2 otherwise can't join datapoints for lines + if not (value[1] - value[0]) >= 2: + return + + if self.axis == "x": + # change left x position of the fill mesh + self._parent.fill.geometry.positions.data[x_left, 0] = value[0] + + # change right x position of the fill mesh + self._parent.fill.geometry.positions.data[x_right, 0] = value[1] + + # change x position of the left edge line + self._parent.edges[0].geometry.positions.data[:, 0] = value[0] + + # change x position of the right edge line + self._parent.edges[1].geometry.positions.data[:, 0] = value[1] + + elif self.axis == "y": + # change bottom y position of the fill mesh + self._parent.fill.geometry.positions.data[y_bottom, 1] = value[0] + + # change top position of the fill mesh + self._parent.fill.geometry.positions.data[y_top, 1] = value[1] + + # change y position of the bottom edge line + self._parent.edges[0].geometry.positions.data[:, 1] = value[0] + + # change y position of the top edge line + self._parent.edges[1].geometry.positions.data[:, 1] = value[1] + + self._data = value # (value[0], value[1]) + + # send changes to GPU + self._parent.fill.geometry.positions.update_range() + + self._parent.edges[0].geometry.positions.update_range() + self._parent.edges[1].geometry.positions.update_range() + + # calls any events + self._feature_changed(key=None, new_data=value) + + def _feature_changed(self, key: Union[int, slice, Tuple[slice]], new_data: Any): + if len(self._event_handlers) < 1: + return + + if self._parent.parent is not None: + selected_ixs = self._parent.get_selected_indices() + selected_data = self._parent.get_selected_data() + else: + selected_ixs = None + selected_data = None + + # get pygfx event and reset it + pygfx_ev = self._parent._pygfx_event + self._parent._pygfx_event = None + + pick_info = { + "world_object": self._parent.world_object, + "new_data": new_data, + "selected_indices": selected_ixs, + "selected_data": selected_data, + "graphic": self._parent, + "delta": self._parent.delta, + "pygfx_event": pygfx_ev, + "move_info": self._parent._move_info, + } + + event_data = FeatureEvent(type="selection", pick_info=pick_info) + + self._call_event_handlers(event_data) diff --git a/fastplotlib/graphics/_features/_sizes.py b/fastplotlib/graphics/_features/_sizes.py new file mode 100644 index 000000000..e69de29bb diff --git a/fastplotlib/graphics/features/_thickness.py b/fastplotlib/graphics/_features/_thickness.py similarity index 87% rename from fastplotlib/graphics/features/_thickness.py rename to fastplotlib/graphics/_features/_thickness.py index ce9c3cbc4..cae3828b7 100644 --- a/fastplotlib/graphics/features/_thickness.py +++ b/fastplotlib/graphics/_features/_thickness.py @@ -7,15 +7,16 @@ class ThicknessFeature(GraphicFeature): **event pick info:** - ===================== ======================== ========================================================================= + ==================== ======================== ========================================================================= key type description ==================== ======================== ========================================================================= "index" ``None`` not used "new_data" ``float`` new thickness value "collection-index" int the index of the graphic within the collection that triggered the event "world_object" pygfx.WorldObject world object - ==================== ======================== ======================================================================== + ==================== ======================== ========================================================================= """ + def __init__(self, parent, thickness: float): self._scene = None super(ThicknessFeature, self).__init__(parent, thickness) @@ -33,7 +34,7 @@ def _feature_changed(self, key, new_data): "index": None, "collection-index": self._collection_index, "world_object": self._parent.world_object, - "new_data": new_data + "new_data": new_data, } event_data = FeatureEvent(type="thickness", pick_info=pick_info) diff --git a/fastplotlib/graphics/features/__init__.py b/fastplotlib/graphics/features/__init__.py deleted file mode 100644 index 0e1e5f512..000000000 --- a/fastplotlib/graphics/features/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -from ._colors import ColorFeature, CmapFeature, ImageCmapFeature, HeatmapCmapFeature -from ._data import PointsDataFeature, ImageDataFeature, HeatmapDataFeature -from ._present import PresentFeature -from ._thickness import ThicknessFeature -from ._base import GraphicFeature, GraphicFeatureIndexable diff --git a/fastplotlib/graphics/histogram.py b/fastplotlib/graphics/histogram.py index 749d1a441..6efd83a96 100644 --- a/fastplotlib/graphics/histogram.py +++ b/fastplotlib/graphics/histogram.py @@ -2,6 +2,7 @@ from typing import Union, Dict import numpy as np + import pygfx from ._base import Graphic @@ -16,14 +17,14 @@ def __int__(self, *args, **kwargs): class HistogramGraphic(Graphic): def __init__( - self, - data: np.ndarray = None, - bins: Union[int, str] = 'auto', - pre_computed: Dict[str, np.ndarray] = None, - colors: np.ndarray = "w", - draw_scale_factor: float = 100.0, - draw_bin_width_scale: float = 1.0, - **kwargs + self, + data: np.ndarray = None, + bins: Union[int, str] = "auto", + pre_computed: Dict[str, np.ndarray] = None, + colors: np.ndarray = "w", + draw_scale_factor: float = 100.0, + draw_bin_width_scale: float = 1.0, + **kwargs, ): """ Create a Histogram Graphic @@ -54,21 +55,29 @@ def __init__( if pre_computed is None: self.hist, self.bin_edges = np.histogram(data, bins) else: - if not set(pre_computed.keys()) == {'hist', 'bin_edges'}: - raise ValueError("argument to `pre_computed` must be a `dict` with keys 'hist' and 'bin_edges'") + if not set(pre_computed.keys()) == {"hist", "bin_edges"}: + raise ValueError( + "argument to `pre_computed` must be a `dict` with keys 'hist' and 'bin_edges'" + ) if not all(isinstance(v, np.ndarray) for v in pre_computed.values()): - raise ValueError("argument to `pre_computed` must be a `dict` where the values are numpy.ndarray") + raise ValueError( + "argument to `pre_computed` must be a `dict` where the values are numpy.ndarray" + ) self.hist, self.bin_edges = pre_computed["hist"], pre_computed["bin_edges"] self.bin_interval = (self.bin_edges[1] - self.bin_edges[0]) / 2 self.bin_centers = (self.bin_edges + self.bin_interval)[:-1] # scale between 0 - draw_scale_factor - scaled_bin_edges = ((self.bin_edges - self.bin_edges.min()) / (np.ptp(self.bin_edges))) * draw_scale_factor + scaled_bin_edges = ( + (self.bin_edges - self.bin_edges.min()) / (np.ptp(self.bin_edges)) + ) * draw_scale_factor bin_interval_scaled = scaled_bin_edges[1] / 2 # get the centers of the bins from the edges - x_positions_bins = (scaled_bin_edges + bin_interval_scaled)[:-1].astype(np.float32) + x_positions_bins = (scaled_bin_edges + bin_interval_scaled)[:-1].astype( + np.float32 + ) n_bins = x_positions_bins.shape[0] bin_width = (draw_scale_factor / n_bins) * draw_bin_width_scale @@ -77,16 +86,22 @@ def __init__( for bad_val in [np.nan, np.inf, -np.inf]: if bad_val in self.hist: - warn(f"Problematic value <{bad_val}> found in histogram, replacing with zero") + warn( + f"Problematic value <{bad_val}> found in histogram, replacing with zero" + ) self.hist[self.hist == bad_val] = 0 data = np.vstack([x_positions_bins, self.hist]) - super(HistogramGraphic, self).__init__(data=data, colors=colors, n_colors=n_bins, **kwargs) + super(HistogramGraphic, self).__init__( + data=data, colors=colors, n_colors=n_bins, **kwargs + ) self._world_object: pygfx.Group = pygfx.Group() - for x_val, y_val, bin_center in zip(x_positions_bins, self.hist, self.bin_centers): + for x_val, y_val, bin_center in zip( + x_positions_bins, self.hist, self.bin_centers + ): geometry = pygfx.plane_geometry( width=bin_width, height=y_val, @@ -99,4 +114,3 @@ def __init__( hist_bin_graphic.frequency = y_val self.world_object.add(hist_bin_graphic) - diff --git a/fastplotlib/graphics/image.py b/fastplotlib/graphics/image.py index c7bfb44de..d60fa36b2 100644 --- a/fastplotlib/graphics/image.py +++ b/fastplotlib/graphics/image.py @@ -4,19 +4,27 @@ import weakref import numpy as np + import pygfx -from ._base import Graphic, Interaction, PreviouslyModifiedData -from .selectors import LinearSelector, LinearRegionSelector -from .features import ImageCmapFeature, ImageDataFeature, HeatmapDataFeature, HeatmapCmapFeature, PresentFeature -from .features._base import to_gpu_supported_dtype from ..utils import quick_min_max - - -class _ImageHeatmapSelectorsMixin: - def add_linear_selector(self, selection: int = None, padding: float = None, **kwargs) -> LinearSelector: +from ._base import Graphic, Interaction +from .selectors import LinearSelector, LinearRegionSelector +from ._features import ( + ImageCmapFeature, + ImageDataFeature, + HeatmapDataFeature, + HeatmapCmapFeature, + to_gpu_supported_dtype, +) + + +class _AddSelectorsMixin: + def add_linear_selector( + self, selection: int = None, padding: float = None, **kwargs + ) -> LinearSelector: """ - Adds a linear selector. + Adds a :class:`.LinearSelector`. Parameters ---------- @@ -26,7 +34,7 @@ def add_linear_selector(self, selection: int = None, padding: float = None, **kw padding: float, optional pad the length of the selector - kwargs + kwargs: passed to :class:`.LinearSelector` Returns @@ -41,20 +49,29 @@ def add_linear_selector(self, selection: int = None, padding: float = None, **kw else: axis = "x" - bounds_init, limits, size, origin, axis, end_points = self._get_linear_selector_init_args(padding, **kwargs) + ( + bounds_init, + limits, + size, + origin, + axis, + end_points, + ) = self._get_linear_selector_init_args(padding, **kwargs) if selection is None: selection = limits[0] if selection < limits[0] or selection > limits[1]: - raise ValueError(f"the passed selection: {selection} is beyond the limits: {limits}") + raise ValueError( + f"the passed selection: {selection} is beyond the limits: {limits}" + ) selector = LinearSelector( selection=selection, limits=limits, end_points=end_points, parent=weakref.proxy(self), - **kwargs + **kwargs, ) self._plot_area.add_graphic(selector, center=False) @@ -62,17 +79,18 @@ def add_linear_selector(self, selection: int = None, padding: float = None, **kw return weakref.proxy(selector) - def add_linear_region_selector(self, padding: float = None, **kwargs) -> LinearRegionSelector: + def add_linear_region_selector( + self, padding: float = None, **kwargs + ) -> LinearRegionSelector: """ - Add a :class:`.LinearRegionSelector`. Selectors are just ``Graphic`` objects, so you can manage, - remove, or delete them from a plot area just like any other ``Graphic``. + Add a :class:`.LinearRegionSelector`. Parameters ---------- padding: float, optional Extends the linear selector along the y-axis to make it easier to interact with. - kwargs, optional + kwargs: optional passed to ``LinearRegionSelector`` Returns @@ -82,7 +100,14 @@ def add_linear_region_selector(self, padding: float = None, **kwargs) -> LinearR """ - bounds_init, limits, size, origin, axis, end_points = self._get_linear_selector_init_args(padding, **kwargs) + ( + bounds_init, + limits, + size, + origin, + axis, + end_points, + ) = self._get_linear_selector_init_args(padding, **kwargs) # create selector selector = LinearRegionSelector( @@ -92,7 +117,7 @@ def add_linear_region_selector(self, padding: float = None, **kwargs) -> LinearR origin=origin, parent=weakref.proxy(self), fill_color=(0, 0, 0.35, 0.2), - **kwargs + **kwargs, ) self._plot_area.add_graphic(selector, center=False) @@ -170,23 +195,19 @@ def _add_plot_area_hook(self, plot_area): self._plot_area = plot_area -class ImageGraphic(Graphic, Interaction, _ImageHeatmapSelectorsMixin): - feature_events = ( - "data", - "cmap", - "present" - ) +class ImageGraphic(Graphic, Interaction, _AddSelectorsMixin): + feature_events = ("data", "cmap", "present") def __init__( - self, - data: Any, - vmin: int = None, - vmax: int = None, - cmap: str = 'plasma', - filter: str = "nearest", - isolated_buffer: bool = True, - *args, - **kwargs + self, + data: Any, + vmin: int = None, + vmax: int = None, + cmap: str = "plasma", + filter: str = "nearest", + isolated_buffer: bool = True, + *args, + **kwargs, ): """ Create an Image Graphic @@ -197,20 +218,27 @@ def __init__( array-like, usually numpy.ndarray, must support ``memoryview()`` Tensorflow Tensors also work **probably**, but not thoroughly tested | shape must be ``[x_dim, y_dim]`` or ``[x_dim, y_dim, rgb]`` + vmin: int, optional minimum value for color scaling, calculated from data if not provided + vmax: int, optional maximum value for color scaling, calculated from data if not provided + cmap: str, optional, default "plasma" colormap to use to display the image data, ignored if data is RGB + filter: str, optional, default "nearest" interpolation filter, one of "nearest" or "linear" + isolated_buffer: bool, default True If True, initialize a buffer with the same shape as the input data and then set the data, useful if the data arrays are ready-only such as memmaps. If False, the input array is itself used as the buffer. + args: additional arguments passed to Graphic + kwargs: additional keyword arguments passed to Graphic @@ -226,21 +254,6 @@ def __init__( **present**: :class:`.PresentFeature` Control the presence of the Graphic in the scene - - - Examples - -------- - .. code-block:: python - - from fastplotlib import Plot - # create a `Plot` instance - plot = Plot() - # make some random 2D image data - data = np.random.rand(512, 512) - # plot the image data - plot.add_image(data=data) - # show the plot - plot.show() """ super().__init__(*args, **kwargs) @@ -268,16 +281,16 @@ def __init__( # if data is RGB or RGBA if data.ndim > 2: - - material = pygfx.ImageBasicMaterial(clim=(vmin, vmax), map_interpolation=filter) + material = pygfx.ImageBasicMaterial( + clim=(vmin, vmax), map_interpolation=filter + ) # if data is just 2D without color information, use colormap LUT else: - material = pygfx.ImageBasicMaterial(clim=(vmin, vmax), map=self.cmap(), map_interpolation=filter) + material = pygfx.ImageBasicMaterial( + clim=(vmin, vmax), map=self.cmap(), map_interpolation=filter + ) - world_object = pygfx.Image( - geometry, - material - ) + world_object = pygfx.Image(geometry, material) self._set_world_object(world_object) @@ -303,6 +316,7 @@ class _ImageTile(pygfx.Image): Similar to pygfx.Image, only difference is that it contains a few properties to keep track of row chunk index, column chunk index """ + def _wgpu_get_pick_info(self, pick_value): pick_info = super()._wgpu_get_pick_info(pick_value) @@ -310,7 +324,7 @@ def _wgpu_get_pick_info(self, pick_value): return { **pick_info, "row_chunk_index": self.row_chunk_index, - "col_chunk_index": self.col_chunk_index + "col_chunk_index": self.col_chunk_index, } @property @@ -330,23 +344,23 @@ def col_chunk_index(self, index: int): self._col_chunk_index = index -class HeatmapGraphic(Graphic, Interaction, _ImageHeatmapSelectorsMixin): +class HeatmapGraphic(Graphic, Interaction, _AddSelectorsMixin): feature_events = ( "data", "cmap", ) def __init__( - self, - data: Any, - vmin: int = None, - vmax: int = None, - cmap: str = 'plasma', - filter: str = "nearest", - chunk_size: int = 8192, - isolated_buffer: bool = True, - *args, - **kwargs + self, + data: Any, + vmin: int = None, + vmax: int = None, + cmap: str = "plasma", + filter: str = "nearest", + chunk_size: int = 8192, + isolated_buffer: bool = True, + *args, + **kwargs, ): """ Create an Image Graphic @@ -357,26 +371,33 @@ def __init__( array-like, usually numpy.ndarray, must support ``memoryview()`` Tensorflow Tensors also work **probably**, but not thoroughly tested | shape must be ``[x_dim, y_dim]`` + vmin: int, optional minimum value for color scaling, calculated from data if not provided + vmax: int, optional maximum value for color scaling, calculated from data if not provided + cmap: str, optional, default "plasma" colormap to use to display the data + filter: str, optional, default "nearest" interpolation filter, one of "nearest" or "linear" + chunk_size: int, default 8192, max 8192 chunk size for each tile used to make up the heatmap texture + isolated_buffer: bool, default True If True, initialize a buffer with the same shape as the input data and then set the data, useful if the data arrays are ready-only such as memmaps. If False, the input array is itself used as the buffer. + args: additional arguments passed to Graphic + kwargs: additional keyword arguments passed to Graphic - Features -------- @@ -389,23 +410,6 @@ def __init__( **present**: :class:`.PresentFeature` Control the presence of the Graphic in the scene - - Examples - -------- - .. code-block:: python - - from fastplotlib import Plot - # create a `Plot` instance - plot = Plot() - - # make some random 2D heatmap data - data = np.random.rand(10_000, 8_000) - - # add a heatmap - plot.add_heatmap(data=data) - - # show the plot - plot.show() """ super().__init__(*args, **kwargs) @@ -441,7 +445,9 @@ def __init__( vmin, vmax = quick_min_max(data) self.cmap = HeatmapCmapFeature(self, cmap) - self._material = pygfx.ImageBasicMaterial(clim=(vmin, vmax), map=self.cmap(), map_interpolation=filter) + self._material = pygfx.ImageBasicMaterial( + clim=(vmin, vmax), map=self.cmap(), map_interpolation=filter + ) for start, stop, chunk in zip(start_ixs, stop_ixs, chunks): row_start, col_start = start @@ -450,7 +456,9 @@ def __init__( # x and y positions of the Tile in world space coordinates y_pos, x_pos = row_start, col_start - texture = pygfx.Texture(buffer_init[row_start:row_stop, col_start:col_stop], dim=2) + texture = pygfx.Texture( + buffer_init[row_start:row_stop, col_start:col_stop], dim=2 + ) geometry = pygfx.Geometry(grid=texture) # material = pygfx.ImageBasicMaterial(clim=(0, 1), map=self.cmap()) @@ -460,8 +468,8 @@ def __init__( img.row_chunk_index = chunk[0] img.col_chunk_index = chunk[1] - img.position_x = x_pos - img.position_y = y_pos + img.world.x = x_pos + img.world.y = y_pos self.world_object.add(img) @@ -480,10 +488,7 @@ def vmin(self) -> float: @vmin.setter def vmin(self, value: float): """Minimum contrast limit.""" - self._material.clim = ( - value, - self._material.clim[1] - ) + self._material.clim = (value, self._material.clim[1]) @property def vmax(self) -> float: @@ -493,10 +498,7 @@ def vmax(self) -> float: @vmax.setter def vmax(self, value: float): """Maximum contrast limit.""" - self._material.clim = ( - self._material.clim[0], - value - ) + self._material.clim = (self._material.clim[0], value) def _set_feature(self, feature: str, new_data: Any, indices: Any): pass diff --git a/fastplotlib/graphics/line.py b/fastplotlib/graphics/line.py index 6114fdd83..aeeeea3b0 100644 --- a/fastplotlib/graphics/line.py +++ b/fastplotlib/graphics/line.py @@ -2,35 +2,30 @@ import weakref import numpy as np + import pygfx +from ..utils import parse_cmap_values from ._base import Graphic, Interaction, PreviouslyModifiedData -from .features import PointsDataFeature, ColorFeature, CmapFeature, ThicknessFeature +from ._features import PointsDataFeature, ColorFeature, CmapFeature, ThicknessFeature from .selectors import LinearRegionSelector, LinearSelector -from ..utils import parse_cmap_values class LineGraphic(Graphic, Interaction): - feature_events = ( - "data", - "colors", - "cmap", - "thickness", - "present" - ) + feature_events = ("data", "colors", "cmap", "thickness", "present") def __init__( - self, - data: Any, - thickness: float = 2.0, - colors: Union[str, np.ndarray, Iterable] = "w", - alpha: float = 1.0, - cmap: str = None, - cmap_values: Union[np.ndarray, List] = None, - z_position: float = None, - collection_index: int = None, - *args, - **kwargs + self, + data: Any, + thickness: float = 2.0, + colors: Union[str, np.ndarray, Iterable] = "w", + alpha: float = 1.0, + cmap: str = None, + cmap_values: Union[np.ndarray, List] = None, + z_position: float = None, + collection_index: int = None, + *args, + **kwargs, ): """ Create a line Graphic, 2d or 3d @@ -50,7 +45,7 @@ def __init__( cmap: str, optional apply a colormap to the line instead of assigning colors manually, this overrides any argument passed to "colors" - + cmap_values: 1D array-like or list of numerical values, optional if provided, these values are used to map the colors from the cmap @@ -71,11 +66,15 @@ def __init__( **data**: :class:`.ImageDataFeature` Manages the line [x, y, z] positions data buffer, allows regular and fancy indexing. - ex: ``scatter.data[:, 0] = 5```, ``scatter.data[xs > 5] = 3`` **colors**: :class:`.ColorFeature` Manages the color buffer, allows regular and fancy indexing. - ex: ``scatter.data[:, 1] = 0.5``, ``scatter.colors[xs > 5] = "cyan"`` + + **cmap**: :class:`.CmapFeature` + Manages the cmap, wraps :class:`.ColorFeature` to add additional functionality relevant to cmaps. + + **thickness**: :class:`.ThicknessFeature` + Manages the thickness feature of the lines. **present**: :class:`.PresentFeature` Control the presence of the Graphic in the scene, set to ``True`` or ``False`` @@ -88,9 +87,7 @@ def __init__( n_datapoints = self.data().shape[0] colors = parse_cmap_values( - n_colors=n_datapoints, - cmap_name=cmap, - cmap_values=cmap_values + n_colors=n_datapoints, cmap_name=cmap, cmap_values=cmap_values ) self.colors = ColorFeature( @@ -98,14 +95,11 @@ def __init__( colors, n_colors=self.data().shape[0], alpha=alpha, - collection_index=collection_index + collection_index=collection_index, ) self.cmap = CmapFeature( - self, - self.colors(), - cmap_name=cmap, - cmap_values=cmap_values + self, self.colors(), cmap_name=cmap, cmap_values=cmap_values ) super(LineGraphic, self).__init__(*args, **kwargs) @@ -120,7 +114,7 @@ def __init__( world_object: pygfx.Line = pygfx.Line( # self.data.feature_data because data is a Buffer geometry=pygfx.Geometry(positions=self.data(), colors=self.colors()), - material=material(thickness=self.thickness(), vertex_colors=True) + material=material(thickness=self.thickness(), vertex_colors=True), ) self._set_world_object(world_object) @@ -128,7 +122,9 @@ def __init__( if z_position is not None: self.position_z = z_position - def add_linear_selector(self, selection: int = None, padding: float = 50, **kwargs) -> LinearSelector: + def add_linear_selector( + self, selection: int = None, padding: float = 50, **kwargs + ) -> LinearSelector: """ Adds a linear selector. @@ -149,20 +145,29 @@ def add_linear_selector(self, selection: int = None, padding: float = 50, **kwar """ - bounds_init, limits, size, origin, axis, end_points = self._get_linear_selector_init_args(padding, **kwargs) + ( + bounds_init, + limits, + size, + origin, + axis, + end_points, + ) = self._get_linear_selector_init_args(padding, **kwargs) if selection is None: selection = limits[0] if selection < limits[0] or selection > limits[1]: - raise ValueError(f"the passed selection: {selection} is beyond the limits: {limits}") + raise ValueError( + f"the passed selection: {selection} is beyond the limits: {limits}" + ) selector = LinearSelector( selection=selection, limits=limits, end_points=end_points, parent=self, - **kwargs + **kwargs, ) self._plot_area.add_graphic(selector, center=False) @@ -170,7 +175,9 @@ def add_linear_selector(self, selection: int = None, padding: float = 50, **kwar return weakref.proxy(selector) - def add_linear_region_selector(self, padding: float = 100.0, **kwargs) -> LinearRegionSelector: + def add_linear_region_selector( + self, padding: float = 100.0, **kwargs + ) -> LinearRegionSelector: """ Add a :class:`.LinearRegionSelector`. Selectors are just ``Graphic`` objects, so you can manage, remove, or delete them from a plot area just like any other ``Graphic``. @@ -190,7 +197,14 @@ def add_linear_region_selector(self, padding: float = 100.0, **kwargs) -> Linear """ - bounds_init, limits, size, origin, axis, end_points = self._get_linear_selector_init_args(padding, **kwargs) + ( + bounds_init, + limits, + size, + origin, + axis, + end_points, + ) = self._get_linear_selector_init_args(padding, **kwargs) # create selector selector = LinearRegionSelector( @@ -199,7 +213,7 @@ def add_linear_region_selector(self, padding: float = 100.0, **kwargs) -> Linear size=size, origin=origin, parent=self, - **kwargs + **kwargs, ) self._plot_area.add_graphic(selector, center=False) @@ -236,7 +250,10 @@ def _get_linear_selector_init_args(self, padding: float, **kwargs): # endpoints of the data range # used by linear selector but not linear region - end_points = (self.data()[:, 1].min() - padding, self.data()[:, 1].max() + padding) + end_points = ( + self.data()[:, 1].min() - padding, + self.data()[:, 1].max() + padding, + ) else: offset = self.position_y # y limits @@ -251,7 +268,10 @@ def _get_linear_selector_init_args(self, padding: float, **kwargs): # need x offset too for this origin = (position_x + self.position_x, limits[0] - offset) - end_points = (self.data()[:, 0].min() - padding, self.data()[:, 0].max() + padding) + end_points = ( + self.data()[:, 0].min() - padding, + self.data()[:, 0].max() + padding, + ) # initial bounds are 20% of the limits range bounds_init = (limits[0], int(np.ptp(limits) * 0.2) + offset) @@ -278,7 +298,9 @@ def _set_feature(self, feature: str, new_data: Any, indices: Any = None): self._previous_data[feature].data = previous self._previous_data[feature].indices = indices else: - self._previous_data[feature] = PreviouslyModifiedData(data=previous, indices=indices) + self._previous_data[feature] = PreviouslyModifiedData( + data=previous, indices=indices + ) def _reset_feature(self, feature: str): if feature not in self._previous_data.keys(): @@ -289,4 +311,4 @@ def _reset_feature(self, feature: str): if prev_ixs is not None: feature_instance[prev_ixs] = self._previous_data[feature].data else: - feature_instance._set(self._previous_data[feature].data) \ No newline at end of file + feature_instance._set(self._previous_data[feature].data) diff --git a/fastplotlib/graphics/line_collection.py b/fastplotlib/graphics/line_collection.py index 4756013d9..06f260ee7 100644 --- a/fastplotlib/graphics/line_collection.py +++ b/fastplotlib/graphics/line_collection.py @@ -1,44 +1,38 @@ from typing import * from copy import deepcopy import weakref -import traceback import numpy as np + import pygfx +from ..utils import parse_cmap_values from ._base import Interaction, PreviouslyModifiedData, GraphicCollection -from .features import GraphicFeature +from ._features import GraphicFeature from .line import LineGraphic from .selectors import LinearRegionSelector, LinearSelector -from ..utils import make_colors, get_cmap, QUALITATIVE_CMAPS, normalize_min_max, parse_cmap_values class LineCollection(GraphicCollection, Interaction): - child_type = LineGraphic - feature_events = ( - "data", - "colors", - "cmap", - "thickness", - "present" - ) + child_type = LineGraphic.__name__ + feature_events = ("data", "colors", "cmap", "thickness", "present") def __init__( - self, - data: List[np.ndarray], - z_position: Union[List[float], float] = None, - thickness: Union[float, List[float]] = 2.0, - colors: Union[List[np.ndarray], np.ndarray] = "w", - alpha: float = 1.0, - cmap: Union[List[str], str] = None, - cmap_values: Union[np.ndarray, List] = None, - name: str = None, - metadata: Union[list, tuple, np.ndarray] = None, - *args, - **kwargs + self, + data: List[np.ndarray], + z_position: Union[List[float], float] = None, + thickness: Union[float, List[float]] = 2.0, + colors: Union[List[np.ndarray], np.ndarray] = "w", + alpha: float = 1.0, + cmap: Union[List[str], str] = None, + cmap_values: Union[np.ndarray, List] = None, + name: str = None, + metadata: Union[list, tuple, np.ndarray] = None, + *args, + **kwargs, ): """ - Create a Line Collection + Create a collection of :class:`.LineGraphic` Parameters ---------- @@ -64,7 +58,9 @@ def __init__( cmap: list of str or str, optional | if ``str``, single cmap will be used for all lines | if ``list`` of ``str``, each cmap will apply to the individual lines - **Note:** ``cmap`` overrides any arguments passed to ``colors`` + + .. note:: + ``cmap`` overrides any arguments passed to ``colors`` cmap_values: 1D array-like or list of numerical values, optional if provided, these values are used to map the colors from the cmap @@ -82,91 +78,33 @@ def __init__( kwargs passed to GraphicCollection - Features -------- Collections support the same features as the underlying graphic. You just have to slice the selection. - .. code-block:: python - - # slice only the collection - line_collection[10:20].colors = "blue" - - # slice the collection and a feature - line_collection[20:30].colors[10:30] = "red" - - # the data feature also works like this - See :class:`LineGraphic` details on the features. - - Examples - -------- - .. code-block:: python - - from fastplotlib import Plot - from fastplotlib.graphics import LineCollection - - # creating data for sine and cosine waves - xs = np.linspace(-10, 10, 100) - ys = np.sin(xs) - - sine = np.dstack([xs, ys])[0] - - ys = np.sin(xs) + 10 - sine2 = np.dstack([xs, ys])[0] - - ys = np.cos(xs) + 5 - cosine = np.dstack([xs, ys])[0] - - # creating plot - plot = Plot() - - # creating a line collection using the sine and cosine wave data - line_collection = LineCollection(data=[sine, cosine, sine2], cmap=["Oranges", "Blues", "Reds"], thickness=20.0) - - # add graphic to plot - plot.add_graphic(line_collection) - - # show plot - plot.show() - - # change the color of the sine wave to white - line_collection[0].colors = "w" - - # change certain color indexes of the cosine data to red - line_collection[1].colors[0:15] = "r" - - # toggle presence of sine2 and rescale graphics - line_collection[2].present = False - - plot.autoscale() - - line_collection[2].present = True - - plot.autoscale() - - # can also do slicing - line_collection[1:].colors[35:70] = "magenta" - """ super(LineCollection, self).__init__(name) if not isinstance(z_position, float) and z_position is not None: if len(data) != len(z_position): - raise ValueError("z_position must be a single float or an iterable with same length as data") + raise ValueError( + "z_position must be a single float or an iterable with same length as data" + ) if not isinstance(thickness, (float, int)): if len(thickness) != len(data): - raise ValueError("args must be a single float or an iterable with same length as data") + raise ValueError( + "args must be a single float or an iterable with same length as data" + ) if metadata is not None: if len(metadata) != len(data): raise ValueError( - f"len(metadata) != len(data)\n" - f"{len(metadata)} != {len(data)}" + f"len(metadata) != len(data)\n" f"{len(metadata)} != {len(data)}" ) self._cmap_values = cmap_values @@ -177,21 +115,23 @@ def __init__( # cmap across lines if isinstance(cmap, str): colors = parse_cmap_values( - n_colors=len(data), - cmap_name=cmap, - cmap_values=cmap_values + n_colors=len(data), cmap_name=cmap, cmap_values=cmap_values ) single_color = False cmap = None elif isinstance(cmap, (tuple, list)): if len(cmap) != len(data): - raise ValueError("cmap argument must be a single cmap or a list of cmaps " - "with the same length as the data") + raise ValueError( + "cmap argument must be a single cmap or a list of cmaps " + "with the same length as the data" + ) single_color = False else: - raise ValueError("cmap argument must be a single cmap or a list of cmaps " - "with the same length as the data") + raise ValueError( + "cmap argument must be a single cmap or a list of cmaps " + "with the same length as the data" + ) else: if isinstance(colors, np.ndarray): # single color for all lines in the collection as RGBA @@ -270,7 +210,7 @@ def __init__( z_position=_z, cmap=_cmap, collection_index=i, - metadata=_m + metadata=_m, ) self.add_graphic(lg, reset_index=False) @@ -282,14 +222,14 @@ def cmap(self) -> str: @cmap.setter def cmap(self, cmap: str): colors = parse_cmap_values( - n_colors=len(self), - cmap_name=cmap, - cmap_values=self.cmap_values + n_colors=len(self), cmap_name=cmap, cmap_values=self.cmap_values ) for i, g in enumerate(self.graphics): g.colors = colors[i] + self._cmap_str = cmap + @property def cmap_values(self) -> np.ndarray: return self._cmap_values @@ -297,20 +237,17 @@ def cmap_values(self) -> np.ndarray: @cmap_values.setter def cmap_values(self, values: Union[np.ndarray, list]): colors = parse_cmap_values( - n_colors=len(self), - cmap_name=self.cmap, - cmap_values=values - + n_colors=len(self), cmap_name=self.cmap, cmap_values=values ) for i, g in enumerate(self.graphics): g.colors = colors[i] - def add_linear_selector(self, selection: int = None, padding: float = 50, **kwargs) -> LinearSelector: + def add_linear_selector( + self, selection: int = None, padding: float = 50, **kwargs + ) -> LinearSelector: """ Adds a :class:`.LinearSelector` . - Selectors are just ``Graphic`` objects, so you can manage, remove, or delete them from a plot area just like - any other ``Graphic``. Parameters ---------- @@ -329,20 +266,29 @@ def add_linear_selector(self, selection: int = None, padding: float = 50, **kwar """ - bounds, limits, size, origin, axis, end_points = self._get_linear_selector_init_args(padding, **kwargs) + ( + bounds, + limits, + size, + origin, + axis, + end_points, + ) = self._get_linear_selector_init_args(padding, **kwargs) if selection is None: selection = limits[0] if selection < limits[0] or selection > limits[1]: - raise ValueError(f"the passed selection: {selection} is beyond the limits: {limits}") + raise ValueError( + f"the passed selection: {selection} is beyond the limits: {limits}" + ) selector = LinearSelector( selection=selection, limits=limits, end_points=end_points, parent=self, - **kwargs + **kwargs, ) self._plot_area.add_graphic(selector, center=False) @@ -350,11 +296,11 @@ def add_linear_selector(self, selection: int = None, padding: float = 50, **kwar return weakref.proxy(selector) - def add_linear_region_selector(self, padding: float = 100.0, **kwargs) -> LinearRegionSelector: + def add_linear_region_selector( + self, padding: float = 100.0, **kwargs + ) -> LinearRegionSelector: """ Add a :class:`.LinearRegionSelector` - Selectors are just ``Graphic`` objects, so you can manage, remove, or delete them from a plot area just like - any other ``Graphic``. Parameters ---------- @@ -371,7 +317,14 @@ def add_linear_region_selector(self, padding: float = 100.0, **kwargs) -> Linear """ - bounds, limits, size, origin, axis, end_points = self._get_linear_selector_init_args(padding, **kwargs) + ( + bounds, + limits, + size, + origin, + axis, + end_points, + ) = self._get_linear_selector_init_args(padding, **kwargs) selector = LinearRegionSelector( bounds=bounds, @@ -379,7 +332,7 @@ def add_linear_region_selector(self, padding: float = 100.0, **kwargs) -> Linear size=size, origin=origin, parent=self, - **kwargs + **kwargs, ) self._plot_area.add_graphic(selector, center=False) @@ -395,8 +348,14 @@ def _get_linear_selector_init_args(self, padding, **kwargs): end_points = list() for g in self.graphics: - _bounds_init, _limits, _size, _origin, axis, _end_points = \ - g._get_linear_selector_init_args(padding=0, **kwargs) + ( + _bounds_init, + _limits, + _size, + _origin, + axis, + _end_points, + ) = g._get_linear_selector_init_args(padding=0, **kwargs) bounds_init.append(_bounds_init) limits.append(_limits) @@ -409,13 +368,16 @@ def _get_linear_selector_init_args(self, padding, **kwargs): bounds = (b[:, 0].min(), b[:, 1].max()) # set the limits using the extents of the collection - l = np.vstack(limits) - limits = (l[:, 0].min(), l[:, 1].max()) + limits = np.vstack(limits) + limits = (limits[:, 0].min(), limits[:, 1].max()) # stack endpoints end_points = np.vstack(end_points) # use the min endpoint for index 0, highest endpoint for index 1 - end_points = [end_points[:, 0].min() - padding, end_points[:, 1].max() + padding] + end_points = [ + end_points[:, 0].min() - padding, + end_points[:, 1].max() + padding, + ] # TODO: refactor this to use `LineStack.graphics[-1].position.y` if isinstance(self, LineStack): @@ -427,7 +389,11 @@ def _get_linear_selector_init_args(self, padding, **kwargs): # a better way to get the max y value? # graphics y-position + data y-max + padding - end_points[1] = self.graphics[-1].position_y + self.graphics[-1].data()[:, 1].max() + padding + end_points[1] = ( + self.graphics[-1].position_y + + self.graphics[-1].data()[:, 1].max() + + padding + ) else: # just the biggest one if not stacked @@ -480,7 +446,9 @@ def _set_feature(self, feature: str, new_data: Any, indices: Any): self._previous_data[feature].data = previous_data self._previous_data[feature].indices = indices else: - self._previous_data[feature] = PreviouslyModifiedData(data=previous_data, indices=indices) + self._previous_data[feature] = PreviouslyModifiedData( + data=previous_data, indices=indices + ) # finally set the new data # this MUST occur after setting the previous data attribute to prevent recursion @@ -500,29 +468,25 @@ def _reset_feature(self, feature: str): coll_feature.block_events(False) -axes = { - "x": 0, - "y": 1, - "z": 2 -} +axes = {"x": 0, "y": 1, "z": 2} class LineStack(LineCollection): def __init__( - self, - data: List[np.ndarray], - z_position: Union[List[float], float] = None, - thickness: Union[float, List[float]] = 2.0, - colors: Union[List[np.ndarray], np.ndarray] = "w", - cmap: Union[List[str], str] = None, - separation: float = 10, - separation_axis: str = "y", - name: str = None, - *args, - **kwargs + self, + data: List[np.ndarray], + z_position: Union[List[float], float] = None, + thickness: Union[float, List[float]] = 2.0, + colors: Union[List[np.ndarray], np.ndarray] = "w", + cmap: Union[List[str], str] = None, + separation: float = 10, + separation_axis: str = "y", + name: str = None, + *args, + **kwargs, ): """ - Create a line stack + Create a stack of :class:`.LineGraphic` that are separated along the "x" or "y" axis. Parameters ---------- @@ -547,7 +511,9 @@ def __init__( cmap: list of str or str, optional | if ``str``, single cmap will be used for all lines | if ``list`` of ``str``, each cmap will apply to the individual lines - **Note:** ``cmap`` overrides any arguments passed to ``colors`` + + .. note:: + ``cmap`` overrides any arguments passed to ``colors`` name: str, optional name of the line stack @@ -573,56 +539,8 @@ def __init__( Collections support the same features as the underlying graphic. You just have to slice the selection. - .. code-block:: python - - # slice only the collection - line_collection[10:20].colors = "blue" - - # slice the collection and a feature - line_collection[20:30].colors[10:30] = "red" - - # the data feature also works like this - See :class:`LineGraphic` details on the features. - - Examples - -------- - .. code-block:: python - - from fastplotlib import Plot - from fastplotlib.graphics import LineStack - - # create plot - plot = Plot() - - # create line data - xs = np.linspace(-10, 10, 100) - ys = np.sin(xs) - - sine = np.dstack([xs, ys])[0] - - ys = np.sin(xs) - cosine = np.dstack([xs, ys])[0] - - # create line stack - line_stack = LineStack(data=[sine, cosine], cmap=["Oranges", "Blues"], thickness=20.0, separation=5.0) - - # add graphic to plot - plot.add_graphic(line_stack) - - # show plot - plot.show() - - # change the color of the sine wave to white - line_stack[0].colors = "w" - - # change certain color indexes of the cosine data to red - line_stack[1].colors[0:15] = "r" - - # more slicing - line_stack[0].colors[35:70] = "magenta" - """ super(LineStack, self).__init__( data=data, @@ -631,7 +549,7 @@ def __init__( colors=colors, cmap=cmap, name=name, - **kwargs + **kwargs, ) axis_zero = 0 @@ -641,6 +559,8 @@ def __init__( elif separation_axis == "y": line.position_y = axis_zero - axis_zero = axis_zero + line.data()[:, axes[separation_axis]].max() + separation + axis_zero = ( + axis_zero + line.data()[:, axes[separation_axis]].max() + separation + ) self.separation = separation diff --git a/fastplotlib/graphics/scatter.py b/fastplotlib/graphics/scatter.py index b2a92ea95..9e162c57a 100644 --- a/fastplotlib/graphics/scatter.py +++ b/fastplotlib/graphics/scatter.py @@ -3,23 +3,25 @@ import numpy as np import pygfx +from ..utils import parse_cmap_values from ._base import Graphic -from .features import PointsDataFeature, ColorFeature, CmapFeature -from ..utils import make_colors, parse_cmap_values +from ._features import PointsDataFeature, ColorFeature, CmapFeature class ScatterGraphic(Graphic): + feature_events = ("data", "colors", "cmap", "present") + def __init__( - self, - data: np.ndarray, - sizes: Union[int, np.ndarray, list] = 1, - colors: np.ndarray = "w", - alpha: float = 1.0, - cmap: str = None, - cmap_values: Union[np.ndarray, List] = None, - z_position: float = 0.0, - *args, - **kwargs + self, + data: np.ndarray, + sizes: Union[int, np.ndarray, list] = 1, + colors: np.ndarray = "w", + alpha: float = 1.0, + cmap: str = None, + cmap_values: Union[np.ndarray, List] = None, + z_position: float = 0.0, + *args, + **kwargs, ): """ Create a Scatter Graphic, 2d or 3d @@ -59,12 +61,13 @@ def __init__( -------- **data**: :class:`.ImageDataFeature` - Manages the scatter [x, y, z] positions data buffer, allows regular and fancy indexing. - ex: ``scatter.data[:, 0] = 5```, ``scatter.data[xs > 5] = 3`` + Manages the line [x, y, z] positions data buffer, allows regular and fancy indexing. **colors**: :class:`.ColorFeature` Manages the color buffer, allows regular and fancy indexing. - ex: ``scatter.data[:, 1] = 0.5``, ``scatter.colors[xs > 5] = "cyan"`` + + **cmap**: :class:`.CmapFeature` + Manages the cmap, wraps :class:`.ColorFeature` to add additional functionality relevant to cmaps. **present**: :class:`.PresentFeature` Control the presence of the Graphic in the scene, set to ``True`` or ``False`` @@ -75,34 +78,33 @@ def __init__( if cmap is not None: colors = parse_cmap_values( - n_colors=n_datapoints, - cmap_name=cmap, - cmap_values=cmap_values + n_colors=n_datapoints, cmap_name=cmap, cmap_values=cmap_values ) self.colors = ColorFeature(self, colors, n_colors=n_datapoints, alpha=alpha) self.cmap = CmapFeature( - self, - self.colors(), - cmap_name=cmap, - cmap_values=cmap_values + self, self.colors(), cmap_name=cmap, cmap_values=cmap_values ) if isinstance(sizes, int): sizes = np.full(self.data().shape[0], sizes, dtype=np.float32) elif isinstance(sizes, np.ndarray): if (sizes.ndim != 1) or (sizes.size != self.data().shape[0]): - raise ValueError(f"numpy array of `sizes` must be 1 dimensional with " - f"the same length as the number of datapoints") + raise ValueError( + f"numpy array of `sizes` must be 1 dimensional with " + f"the same length as the number of datapoints" + ) elif isinstance(sizes, list): if len(sizes) != self.data().shape[0]: - raise ValueError("list of `sizes` must have the same length as the number of datapoints") + raise ValueError( + "list of `sizes` must have the same length as the number of datapoints" + ) super(ScatterGraphic, self).__init__(*args, **kwargs) world_object = pygfx.Points( pygfx.Geometry(positions=self.data(), sizes=sizes, colors=self.colors()), - material=pygfx.PointsMaterial(vertex_colors=True, vertex_sizes=True) + material=pygfx.PointsMaterial(vertex_colors=True, vertex_sizes=True), ) self._set_world_object(world_object) diff --git a/fastplotlib/graphics/selectors/__init__.py b/fastplotlib/graphics/selectors/__init__.py index 8ebcaf053..1fb0c453e 100644 --- a/fastplotlib/graphics/selectors/__init__.py +++ b/fastplotlib/graphics/selectors/__init__.py @@ -1,4 +1,12 @@ from ._linear import LinearSelector from ._linear_region import LinearRegionSelector -from ._rectangle_region import RectangleRegionSelector +from ._polygon import PolygonSelector + from ._sync import Synchronizer + +__all__ = [ + "LinearSelector", + "LinearRegionSelector", + "PolygonSelector", + "Synchronizer", +] diff --git a/fastplotlib/graphics/selectors/_base_selector.py b/fastplotlib/graphics/selectors/_base_selector.py index 777eb09dc..a4159c194 100644 --- a/fastplotlib/graphics/selectors/_base_selector.py +++ b/fastplotlib/graphics/selectors/_base_selector.py @@ -32,14 +32,16 @@ class MoveInfo: # Selector base class class BaseSelector: + feature_events = ("selection",) + def __init__( - self, - edges: Tuple[Line, ...] = None, - fill: Tuple[Mesh, ...] = None, - vertices: Tuple[Points, ...] = None, - hover_responsive: Tuple[WorldObject, ...] = None, - arrow_keys_modifier: str = None, - axis: str = None + self, + edges: Tuple[Line, ...] = None, + fill: Tuple[Mesh, ...] = None, + vertices: Tuple[Points, ...] = None, + hover_responsive: Tuple[WorldObject, ...] = None, + arrow_keys_modifier: str = None, + axis: str = None, ): if edges is None: edges = tuple() @@ -54,7 +56,9 @@ def __init__( self._fill: Tuple[Mesh, ...] = fill self._vertices: Tuple[Points, ...] = vertices - self._world_objects: Tuple[WorldObject, ...] = self._edges + self._fill + self._vertices + self._world_objects: Tuple[WorldObject, ...] = ( + self._edges + self._fill + self._vertices + ) self._hover_responsive: Tuple[WorldObject, ...] = hover_responsive @@ -81,12 +85,15 @@ def __init__( self._edge_hovered: bool = False def get_selected_index(self): + """Not implemented for this selector""" raise NotImplementedError def get_selected_indices(self): + """Not implemented for this selector""" raise NotImplementedError def get_selected_data(self): + """Not implemented for this selector""" raise NotImplementedError def _get_source(self, graphic): @@ -181,10 +188,7 @@ def _move_start(self, event_source: WorldObject, ev): """ last_position = self._plot_area.map_screen_to_world(ev) - self._move_info = MoveInfo( - last_position=last_position, - source=event_source - ) + self._move_info = MoveInfo(last_position=last_position, source=event_source) def _move(self, ev): """ @@ -250,10 +254,14 @@ def _move_to_pointer(self, ev): # use fill by default as the source, such as in region selectors if len(self._fill) > 0: - self._move_info = MoveInfo(last_position=current_position, source=self._fill[0]) + self._move_info = MoveInfo( + last_position=current_position, source=self._fill[0] + ) # else use an edge, such as for linear selector else: - self._move_info = MoveInfo(last_position=current_position, source=self._edges[0]) + self._move_info = MoveInfo( + last_position=current_position, source=self._edges[0] + ) self._move_graphic(self.delta) self._move_info = None @@ -305,7 +313,10 @@ def _key_hold(self): def _key_down(self, ev): # key bind modifier must be set and must be used for the event # for example. if "Shift" is set as a modifier, then "Shift" must be used as a modifier during this event - if self.arrow_keys_modifier is not None and self.arrow_keys_modifier not in ev.modifiers: + if ( + self.arrow_keys_modifier is not None + and self.arrow_keys_modifier not in ev.modifiers + ): return # ignore if non-arrow key is pressed diff --git a/fastplotlib/graphics/selectors/_linear.py b/fastplotlib/graphics/selectors/_linear.py index 639434aa3..39710305d 100644 --- a/fastplotlib/graphics/selectors/_linear.py +++ b/fastplotlib/graphics/selectors/_linear.py @@ -7,95 +7,30 @@ try: import ipywidgets + HAS_IPYWIDGETS = True -except: +except (ImportError, ModuleNotFoundError): HAS_IPYWIDGETS = False -from .._base import Graphic, GraphicFeature, GraphicCollection -from ..features._base import FeatureEvent +from .._base import Graphic, GraphicCollection +from .._features._selection_features import LinearSelectionFeature from ._base_selector import BaseSelector -class LinearSelectionFeature(GraphicFeature): - # A bit much to have a class for this but this allows it to integrate with the fastplotlib callback system - """ - Manages the slider selection and callbacks - - **event pick info** - - =================== =============================== ================================================================================================= - key type selection - =================== =============================== ================================================================================================= - "selected_index" ``int`` the graphic data index that corresponds to the selector position - "world_object" ``pygfx.WorldObject`` pygfx WorldObject - "new_data" ``numpy.ndarray`` or ``None`` the new selector position in world coordinates, not necessarily the same as "selected_index" - "graphic" ``Graphic`` the selector graphic - "delta" ``numpy.ndarray`` the delta vector of the graphic in NDC - "pygfx_event" ``pygfx.Event`` pygfx Event - =================== =============================== ================================================================================================= - - """ - def __init__(self, parent, axis: str, value: float, limits: Tuple[int, int]): - super(LinearSelectionFeature, self).__init__(parent, data=value) - - self.axis = axis - self.limits = limits - - def _set(self, value: float): - if not (self.limits[0] <= value <= self.limits[1]): - return - - if self.axis == "x": - self._parent.position_x = value - else: - self._parent.position_y = value - - self._data = value - self._feature_changed(key=None, new_data=value) - - def _feature_changed(self, key: Union[int, slice, Tuple[slice]], new_data: Any): - if len(self._event_handlers) < 1: - return - - if self._parent.parent is not None: - g_ix = self._parent.get_selected_index() - else: - g_ix = None - - # get pygfx event and reset it - pygfx_ev = self._parent._pygfx_event - self._parent._pygfx_event = None - - pick_info = { - "world_object": self._parent.world_object, - "new_data": new_data, - "selected_index": g_ix, - "graphic": self._parent, - "pygfx_event": pygfx_ev, - "delta": self._parent.delta, - } - - event_data = FeatureEvent(type="selection", pick_info=pick_info) - - self._call_event_handlers(event_data) - - class LinearSelector(Graphic, BaseSelector): - feature_events = ("selection",) - # TODO: make `selection` arg in graphics data space not world space def __init__( - self, - selection: int, - limits: Tuple[int, int], - axis: str = "x", - parent: Graphic = None, - end_points: Tuple[int, int] = None, - arrow_keys_modifier: str = "Shift", - ipywidget_slider = None, - thickness: float = 2.5, - color: Any = "w", - name: str = None, + self, + selection: int, + limits: Tuple[int, int], + axis: str = "x", + parent: Graphic = None, + end_points: Tuple[int, int] = None, + arrow_keys_modifier: str = "Shift", + ipywidget_slider=None, + thickness: float = 2.5, + color: Any = "w", + name: str = None, ): """ Create a horizontal or vertical line slider that is synced to an ipywidget IntSlider @@ -137,10 +72,13 @@ def __init__( Features -------- - selection: :class:`LinearSelectionFeature` - ``selection()`` returns the current slider position in world coordinates - use ``selection.add_event_handler()`` to add callback functions that are - called when the LinearSelector selection changes. See feature class for event pick_info table + selection: :class:`.LinearSelectionFeature` + ``selection()`` returns the current selector position in world coordinates. + Use ``get_selected_index()`` to get the currently selected index in data + space. + Use ``selection.add_event_handler()`` to add callback functions that are + called when the LinearSelector selection changes. See feature class for + event pick_info table """ if len(limits) != 2: @@ -166,7 +104,6 @@ def __init__( line_data = line_data.astype(np.float32) - # super(LinearSelector, self).__init__(name=name) # init Graphic Graphic.__init__(self, name=name) @@ -180,12 +117,12 @@ def __init__( line_inner = pygfx.Line( # self.data.feature_data because data is a Buffer geometry=pygfx.Geometry(positions=line_data), - material=material(thickness=thickness, color=color) + material=material(thickness=thickness, color=color), ) self.line_outer = pygfx.Line( geometry=pygfx.Geometry(positions=line_data), - material=material(thickness=thickness + 6, color=self.colors_outer) + material=material(thickness=thickness + 6, color=self.colors_outer), ) line_inner.world.z = self.line_outer.world.z + 1 @@ -203,7 +140,9 @@ def __init__( else: self.position_y = selection - self.selection = LinearSelectionFeature(self, axis=axis, value=selection, limits=limits) + self.selection = LinearSelectionFeature( + self, axis=axis, value=selection, limits=limits + ) self.ipywidget_slider = ipywidget_slider @@ -272,7 +211,9 @@ def make_ipywidget_slider(self, kind: str = "IntSlider", **kwargs): raise AttributeError("Already has ipywidget slider") if not HAS_IPYWIDGETS: - raise ImportError("Must installed `ipywidgets` to use `make_ipywidget_slider()`") + raise ImportError( + "Must installed `ipywidgets` to use `make_ipywidget_slider()`" + ) cls = getattr(ipywidgets, kind) @@ -281,7 +222,7 @@ def make_ipywidget_slider(self, kind: str = "IntSlider", **kwargs): max=self.selection.limits[1], value=int(self.selection()), step=1, - **kwargs + **kwargs, ) self.ipywidget_slider = slider self._setup_ipywidget_slider(slider) @@ -332,12 +273,19 @@ def _get_selected_index(self, graphic): # get closest data index to the world space position of the slider idx = np.searchsorted(geo_positions, find_value, side="left") - if idx > 0 and (idx == len(geo_positions) or math.fabs(find_value - geo_positions[idx - 1]) < math.fabs(find_value - geo_positions[idx])): + if idx > 0 and ( + idx == len(geo_positions) + or math.fabs(find_value - geo_positions[idx - 1]) + < math.fabs(find_value - geo_positions[idx]) + ): return int(idx - 1) else: return int(idx) - if "Heatmap" in graphic.__class__.__name__ or "Image" in graphic.__class__.__name__: + if ( + "Heatmap" in graphic.__class__.__name__ + or "Image" in graphic.__class__.__name__ + ): # indices map directly to grid geometry for image data buffer index = self.selection() - offset return int(index) diff --git a/fastplotlib/graphics/selectors/_linear_region.py b/fastplotlib/graphics/selectors/_linear_region.py index 49c914300..0759cd4fc 100644 --- a/fastplotlib/graphics/selectors/_linear_region.py +++ b/fastplotlib/graphics/selectors/_linear_region.py @@ -4,150 +4,24 @@ import pygfx from .._base import Graphic, GraphicCollection -from ..features._base import GraphicFeature, FeatureEvent from ._base_selector import BaseSelector - -from ._mesh_positions import x_right, x_left, y_top, y_bottom - - -class LinearRegionSelectionFeature(GraphicFeature): - feature_events = ( - "data", - ) - """ - Feature for a linearly bounding region - - Pick Info - --------- - - +--------------------+-------------------------------+--------------------------------------------------------------------------------------+ - | key | type | description | - +====================+===============================+======================================================================================+ - | "selected_indices" | ``numpy.ndarray`` or ``None`` | selected graphic data indices | - | "world_object" | ``pygfx.WorldObject`` | pygfx World Object | - | "new_data" | ``(float, float)`` | current bounds in world coordinates, NOT necessarily the same as "selected_indices". | - | "graphic" | ``Graphic`` | the selection graphic | - | "delta" | ``numpy.ndarray`` | the delta vector of the graphic in NDC | - | "pygfx_event" | ``pygfx.Event`` | pygfx Event | - | "selected_data" | ``numpy.ndarray`` or ``None`` | selected graphic data | - | "move_info" | ``MoveInfo`` | last position and event source (pygfx.Mesh or pygfx.Line) | - +--------------------+-------------------------------+--------------------------------------------------------------------------------------+ - - """ - def __init__(self, parent, selection: Tuple[int, int], axis: str, limits: Tuple[int, int]): - super(LinearRegionSelectionFeature, self).__init__(parent, data=selection) - - self._axis = axis - self.limits = limits - - self._set(selection) - - @property - def axis(self) -> str: - """one of "x" | "y" """ - return self._axis - - def _set(self, value: Tuple[float, float]): - # sets new bounds - if not isinstance(value, tuple): - raise TypeError( - "Bounds must be a tuple in the form of `(min_bound, max_bound)`, " - "where `min_bound` and `max_bound` are numeric values." - ) - - # make sure bounds not exceeded - for v in value: - if not (self.limits[0] <= v <= self.limits[1]): - return - - # make sure `selector width >= 2`, left edge must not move past right edge! - # or bottom edge must not move past top edge! - # has to be at least 2 otherwise can't join datapoints for lines - if not (value[1] - value[0]) >= 2: - return - - if self.axis == "x": - # change left x position of the fill mesh - self._parent.fill.geometry.positions.data[x_left, 0] = value[0] - - # change right x position of the fill mesh - self._parent.fill.geometry.positions.data[x_right, 0] = value[1] - - # change x position of the left edge line - self._parent.edges[0].geometry.positions.data[:, 0] = value[0] - - # change x position of the right edge line - self._parent.edges[1].geometry.positions.data[:, 0] = value[1] - - elif self.axis == "y": - # change bottom y position of the fill mesh - self._parent.fill.geometry.positions.data[y_bottom, 1] = value[0] - - # change top position of the fill mesh - self._parent.fill.geometry.positions.data[y_top, 1] = value[1] - - # change y position of the bottom edge line - self._parent.edges[0].geometry.positions.data[:, 1] = value[0] - - # change y position of the top edge line - self._parent.edges[1].geometry.positions.data[:, 1] = value[1] - - self._data = value#(value[0], value[1]) - - # send changes to GPU - self._parent.fill.geometry.positions.update_range() - - self._parent.edges[0].geometry.positions.update_range() - self._parent.edges[1].geometry.positions.update_range() - - # calls any events - self._feature_changed(key=None, new_data=value) - - def _feature_changed(self, key: Union[int, slice, Tuple[slice]], new_data: Any): - if len(self._event_handlers) < 1: - return - - if self._parent.parent is not None: - selected_ixs = self._parent.get_selected_indices() - selected_data = self._parent.get_selected_data() - else: - selected_ixs = None - selected_data = None - - # get pygfx event and reset it - pygfx_ev = self._parent._pygfx_event - self._parent._pygfx_event = None - - pick_info = { - "world_object": self._parent.world_object, - "new_data": new_data, - "selected_indices": selected_ixs, - "selected_data": selected_data, - "graphic": self._parent, - "delta": self._parent.delta, - "pygfx_event": pygfx_ev, - "move_info": self._parent._move_info - } - - event_data = FeatureEvent(type="selection", pick_info=pick_info) - - self._call_event_handlers(event_data) +from .._features._selection_features import LinearRegionSelectionFeature class LinearRegionSelector(Graphic, BaseSelector): def __init__( - self, - bounds: Tuple[int, int], - limits: Tuple[int, int], - size: int, - origin: Tuple[int, int], - axis: str = "x", - parent: Graphic = None, - resizable: bool = True, - fill_color=(0, 0, 0.35), - edge_color=(0.8, 0.8, 0), - arrow_keys_modifier: str = "Shift", - name: str = None + self, + bounds: Tuple[int, int], + limits: Tuple[int, int], + size: int, + origin: Tuple[int, int], + axis: str = "x", + parent: Graphic = None, + resizable: bool = True, + fill_color=(0, 0, 0.35), + edge_color=(0.8, 0.8, 0), + arrow_keys_modifier: str = "Shift", + name: str = None, ): """ Create a LinearRegionSelector graphic which can be moved only along either the x-axis or y-axis. @@ -193,6 +67,18 @@ def __init__( name: str name for this selector graphic + + Features + -------- + + selection: :class:`.LinearRegionSelectionFeature` + ``selection()`` returns the current selector bounds in world coordinates. + Use ``get_selected_indices()`` to return the selected indices in data + space, and ``get_selected_data()`` to return the selected data. + Use ``selection.add_event_handler()`` to add callback functions that are + called when the LinearSelector selection changes. See feature class for + event pick_info table. + """ # lots of very close to zero values etc. so round them @@ -229,14 +115,14 @@ def __init__( if axis == "x": mesh = pygfx.Mesh( - pygfx.box_geometry(1, size, 1), - pygfx.MeshBasicMaterial(color=pygfx.Color(fill_color)) + pygfx.box_geometry(1, size, 1), + pygfx.MeshBasicMaterial(color=pygfx.Color(fill_color)), ) elif axis == "y": mesh = pygfx.Mesh( pygfx.box_geometry(size, 1, 1), - pygfx.MeshBasicMaterial(color=pygfx.Color(fill_color)) + pygfx.MeshBasicMaterial(color=pygfx.Color(fill_color)), ) else: raise ValueError("`axis` must be one of 'x' or 'y'") @@ -252,50 +138,57 @@ def __init__( if axis == "x": # position data for the left edge line left_line_data = np.array( - [[origin[0], (-size / 2) + origin[1], 0.5], - [origin[0], (size / 2) + origin[1], 0.5]] + [ + [origin[0], (-size / 2) + origin[1], 0.5], + [origin[0], (size / 2) + origin[1], 0.5], + ] ).astype(np.float32) left_line = pygfx.Line( pygfx.Geometry(positions=left_line_data), - pygfx.LineMaterial(thickness=3, color=edge_color) + pygfx.LineMaterial(thickness=3, color=edge_color), ) # position data for the right edge line right_line_data = np.array( - [[bounds[1], (-size / 2) + origin[1], 0.5], - [bounds[1], (size / 2) + origin[1], 0.5]] + [ + [bounds[1], (-size / 2) + origin[1], 0.5], + [bounds[1], (size / 2) + origin[1], 0.5], + ] ).astype(np.float32) right_line = pygfx.Line( pygfx.Geometry(positions=right_line_data), - pygfx.LineMaterial(thickness=3, color=edge_color) + pygfx.LineMaterial(thickness=3, color=edge_color), ) self.edges: Tuple[pygfx.Line, pygfx.Line] = (left_line, right_line) elif axis == "y": # position data for the left edge line - bottom_line_data = \ - np.array( - [[(-size / 2) + origin[0], origin[1], 0.5], - [(size / 2) + origin[0], origin[1], 0.5]] - ).astype(np.float32) + bottom_line_data = np.array( + [ + [(-size / 2) + origin[0], origin[1], 0.5], + [(size / 2) + origin[0], origin[1], 0.5], + ] + ).astype(np.float32) bottom_line = pygfx.Line( pygfx.Geometry(positions=bottom_line_data), - pygfx.LineMaterial(thickness=3, color=edge_color) + pygfx.LineMaterial(thickness=3, color=edge_color), ) # position data for the right edge line top_line_data = np.array( - [[(-size / 2) + origin[0], bounds[1], 0.5], - [(size / 2) + origin[0], bounds[1], 0.5]] + [ + [(-size / 2) + origin[0], bounds[1], 0.5], + [(size / 2) + origin[0], bounds[1], 0.5], + ] ).astype(np.float32) top_line = pygfx.Line( pygfx.Geometry(positions=top_line_data), - pygfx.LineMaterial(thickness=3, color=edge_color) + pygfx.LineMaterial(thickness=3, color=edge_color), ) self.edges: Tuple[pygfx.Line, pygfx.Line] = (bottom_line, top_line) @@ -309,8 +202,9 @@ def __init__( self.world_object.add(edge) # set the initial bounds of the selector - self.selection = LinearRegionSelectionFeature(self, bounds, axis=axis, limits=limits) - # self._bounds: LinearBoundsFeature = bounds + self.selection = LinearRegionSelectionFeature( + self, bounds, axis=axis, limits=limits + ) BaseSelector.__init__( self, @@ -321,7 +215,9 @@ def __init__( axis=axis, ) - def get_selected_data(self, graphic: Graphic = None) -> Union[np.ndarray, List[np.ndarray], None]: + def get_selected_data( + self, graphic: Graphic = None + ) -> Union[np.ndarray, List[np.ndarray], None]: """ Get the ``Graphic`` data bounded by the current selection. Returns a view of the full data array. @@ -370,14 +266,19 @@ def get_selected_data(self, graphic: Graphic = None) -> Union[np.ndarray, List[n s = slice(ixs[0], ixs[-1]) return source.data.buffer.data[s] - if "Heatmap" in source.__class__.__name__ or "Image" in source.__class__.__name__: + if ( + "Heatmap" in source.__class__.__name__ + or "Image" in source.__class__.__name__ + ): s = slice(ixs[0], ixs[-1]) if self.axis == "x": return source.data()[:, s] elif self.axis == "y": return source.data()[s] - def get_selected_indices(self, graphic: Graphic = None) -> Union[np.ndarray, List[np.ndarray]]: + def get_selected_indices( + self, graphic: Graphic = None + ) -> Union[np.ndarray, List[np.ndarray]]: """ Returns the indices of the ``Graphic`` data bounded by the current selection. This is useful because the ``bounds`` min and max are not necessarily the same @@ -419,18 +320,23 @@ def get_selected_indices(self, graphic: Graphic = None) -> Union[np.ndarray, Lis for g in source.graphics: # map for each graphic in the collection g_ixs = np.where( - (g.data()[:, dim] >= offset_bounds[0]) & (g.data()[:, dim] <= offset_bounds[1]) + (g.data()[:, dim] >= offset_bounds[0]) + & (g.data()[:, dim] <= offset_bounds[1]) )[0] ixs.append(g_ixs) else: # map this only this graphic ixs = np.where( - (source.data()[:, dim] >= offset_bounds[0]) & (source.data()[:, dim] <= offset_bounds[1]) + (source.data()[:, dim] >= offset_bounds[0]) + & (source.data()[:, dim] <= offset_bounds[1]) )[0] return ixs - if "Heatmap" in source.__class__.__name__ or "Image" in source.__class__.__name__: + if ( + "Heatmap" in source.__class__.__name__ + or "Image" in source.__class__.__name__ + ): # indices map directly to grid geometry for image data buffer ixs = np.arange(*self.selection(), dtype=int) return ixs diff --git a/fastplotlib/graphics/selectors/_mesh_positions.py b/fastplotlib/graphics/selectors/_mesh_positions.py index 9542aee58..07ff60498 100644 --- a/fastplotlib/graphics/selectors/_mesh_positions.py +++ b/fastplotlib/graphics/selectors/_mesh_positions.py @@ -1,31 +1,2 @@ import numpy as np - -""" -positions for indexing the BoxGeometry to set the "width" and "size" of the box -hacky, but I don't think we can morph meshes in pygfx yet: https://github.com/pygfx/pygfx/issues/346 -""" - -x_right = np.array([ - True, True, True, True, False, False, False, False, False, - True, False, True, True, False, True, False, False, True, - False, True, True, False, True, False -]) - -x_left = np.array([ - False, False, False, False, True, True, True, True, True, - False, True, False, False, True, False, True, True, False, - True, False, False, True, False, True -]) - -y_top = np.array([ - False, True, False, True, False, True, False, True, True, - True, True, True, False, False, False, False, False, False, - True, True, False, False, True, True -]) - -y_bottom = np.array([ - True, False, True, False, True, False, True, False, False, - False, False, False, True, True, True, True, True, True, - False, False, True, True, False, False -]) \ No newline at end of file diff --git a/fastplotlib/graphics/selectors/_polygon.py b/fastplotlib/graphics/selectors/_polygon.py new file mode 100644 index 000000000..244ad7b66 --- /dev/null +++ b/fastplotlib/graphics/selectors/_polygon.py @@ -0,0 +1,136 @@ +from typing import * + +import numpy as np + +import pygfx + +from ._base_selector import BaseSelector, MoveInfo +from .._base import Graphic + + +class PolygonSelector(Graphic, BaseSelector): + def __init__( + self, + edge_color="magenta", + edge_width: float = 3, + parent: Graphic = None, + name: str = None, + ): + Graphic.__init__(self, name=name) + + self.parent = parent + + group = pygfx.Group() + + self._set_world_object(group) + + self.edge_color = edge_color + self.edge_width = edge_width + + self._move_info: MoveInfo = None + + self._current_mode = None + + def get_vertices(self) -> np.ndarray: + """Get the vertices for the polygon""" + vertices = list() + for child in self.world_object.children: + vertices.append(child.geometry.positions.data[:, :2]) + + return np.vstack(vertices) + + def _add_plot_area_hook(self, plot_area): + self._plot_area = plot_area + + # click to add new segment + self._plot_area.renderer.add_event_handler(self._add_segment, "click") + + # pointer move to change endpoint of segment + self._plot_area.renderer.add_event_handler(self._move_segment_endpoint, "pointer_move") + + # click to finish existing segment + self._plot_area.renderer.add_event_handler(self._finish_segment, "click") + + # double click to finish polygon + self._plot_area.renderer.add_event_handler(self._finish_polygon, "double_click") + + self.position_z = len(self._plot_area) + 10 + + def _add_segment(self, ev): + """After click event, adds a new line segment""" + self._current_mode = "add" + + last_position = self._plot_area.map_screen_to_world(ev) + self._move_info = MoveInfo(last_position=last_position, source=None) + + # line with same position for start and end until mouse moves + data = np.array([last_position, last_position]) + + new_line = pygfx.Line( + geometry=pygfx.Geometry(positions=data.astype(np.float32)), + material=pygfx.LineMaterial(thickness=self.edge_width, color=pygfx.Color(self.edge_color)) + ) + + self.world_object.add(new_line) + + def _move_segment_endpoint(self, ev): + """After mouse pointer move event, moves endpoint of current line segment""" + if self._move_info is None: + return + self._current_mode = "move" + + world_pos = self._plot_area.map_screen_to_world(ev) + + if world_pos is None: + return + + # change endpoint + self.world_object.children[-1].geometry.positions.data[1] = np.array([world_pos]).astype(np.float32) + self.world_object.children[-1].geometry.positions.update_range() + + def _finish_segment(self, ev): + """After click event, ends a line segment""" + # should start a new segment + if self._move_info is None: + return + + # since both _add_segment and _finish_segment use the "click" callback + # this is to block _finish_segment right after a _add_segment call + if self._current_mode == "add": + return + + # just make move info None so that _move_segment_endpoint is not called + # and _add_segment gets triggered for "click" + self._move_info = None + + self._current_mode = "finish-segment" + + def _finish_polygon(self, ev): + """finishes the polygon, disconnects events""" + world_pos = self._plot_area.map_screen_to_world(ev) + + if world_pos is None: + return + + # make new line to connect first and last vertices + data = np.vstack([ + world_pos, + self.world_object.children[0].geometry.positions.data[0] + ]) + + new_line = pygfx.Line( + geometry=pygfx.Geometry(positions=data.astype(np.float32)), + material=pygfx.LineMaterial(thickness=self.edge_width, color=pygfx.Color(self.edge_color)) + ) + + self.world_object.add(new_line) + + handlers = { + self._add_segment: "click", + self._move_segment_endpoint: "pointer_move", + self._finish_segment: "click", + self._finish_polygon: "double_click" + } + + for handler, event in handlers.items(): + self._plot_area.renderer.remove_event_handler(handler, event) diff --git a/fastplotlib/graphics/selectors/_rectangle_region.py b/fastplotlib/graphics/selectors/_rectangle_region.py index 7065abe2d..a5a9a31cb 100644 --- a/fastplotlib/graphics/selectors/_rectangle_region.py +++ b/fastplotlib/graphics/selectors/_rectangle_region.py @@ -3,10 +3,9 @@ import pygfx -from .._base import Graphic, GraphicCollection -from ..features._base import GraphicFeature, FeatureEvent +from .._base import Graphic +from .._features import GraphicFeature from ._base_selector import BaseSelector - from ._mesh_positions import x_right, x_left, y_top, y_bottom @@ -14,8 +13,7 @@ class RectangleBoundsFeature(GraphicFeature): """ Feature for a linearly bounding region - Pick Info - --------- + **event pick info** +--------------------+-------------------------------+--------------------------------------------------------------------------------------+ | key | type | description | @@ -26,7 +24,10 @@ class RectangleBoundsFeature(GraphicFeature): +--------------------+-------------------------------+--------------------------------------------------------------------------------------+ """ - def __init__(self, parent, bounds: Tuple[int, int], axis: str, limits: Tuple[int, int]): + + def __init__( + self, parent, bounds: Tuple[int, int], axis: str, limits: Tuple[int, int] + ): super(RectangleBoundsFeature, self).__init__(parent, data=bounds) self._axis = axis @@ -78,37 +79,25 @@ def _set(self, value: Tuple[float, float, float, float]): # left line z = self._parent.edges[0].geometry.positions.data[:, -1][0] self._parent.edges[0].geometry.positions.data[:] = np.array( - [ - [xmin, ymin, z], - [xmin, ymax, z] - ] + [[xmin, ymin, z], [xmin, ymax, z]] ) # right line self._parent.edges[1].geometry.positions.data[:] = np.array( - [ - [xmax, ymin, z], - [xmax, ymax, z] - ] + [[xmax, ymin, z], [xmax, ymax, z]] ) # bottom line self._parent.edges[2].geometry.positions.data[:] = np.array( - [ - [xmin, ymin, z], - [xmax, ymin, z] - ] + [[xmin, ymin, z], [xmax, ymin, z]] ) # top line self._parent.edges[3].geometry.positions.data[:] = np.array( - [ - [xmin, ymax, z], - [xmax, ymax, z] - ] + [[xmin, ymax, z], [xmax, ymax, z]] ) - self._data = value#(value[0], value[1]) + self._data = value # (value[0], value[1]) # send changes to GPU self._parent.fill.geometry.positions.update_range() @@ -150,22 +139,20 @@ def _feature_changed(self, key: Union[int, slice, Tuple[slice]], new_data: Any): class RectangleRegionSelector(Graphic, BaseSelector): - feature_events = ( - "bounds" - ) + feature_events = "bounds" def __init__( - self, - bounds: Tuple[int, int, int, int], - limits: Tuple[int, int], - origin: Tuple[int, int], - axis: str = "x", - parent: Graphic = None, - resizable: bool = True, - fill_color=(0, 0, 0.35), - edge_color=(0.8, 0.8, 0), - arrow_keys_modifier: str = "Shift", - name: str = None + self, + bounds: Tuple[int, int, int, int], + limits: Tuple[int, int], + origin: Tuple[int, int], + axis: str = "x", + parent: Graphic = None, + resizable: bool = True, + fill_color=(0, 0, 0.35), + edge_color=(0.8, 0.8, 0), + arrow_keys_modifier: str = "Shift", + name: str = None, ): """ Create a LinearRegionSelector graphic which can be moved only along either the x-axis or y-axis. @@ -227,7 +214,7 @@ def __init__( self.fill = pygfx.Mesh( pygfx.box_geometry(width, height, 1), - pygfx.MeshBasicMaterial(color=pygfx.Color(fill_color)) + pygfx.MeshBasicMaterial(color=pygfx.Color(fill_color)), ) self.fill.position.set(*origin, -2) @@ -235,51 +222,61 @@ def __init__( # position data for the left edge line left_line_data = np.array( - [[origin[0], (-height / 2) + origin[1], 0.5], - [origin[0], (height / 2) + origin[1], 0.5]] + [ + [origin[0], (-height / 2) + origin[1], 0.5], + [origin[0], (height / 2) + origin[1], 0.5], + ] ).astype(np.float32) left_line = pygfx.Line( pygfx.Geometry(positions=left_line_data), - pygfx.LineMaterial(thickness=3, color=edge_color) + pygfx.LineMaterial(thickness=3, color=edge_color), ) # position data for the right edge line right_line_data = np.array( - [[bounds[1], (-height / 2) + origin[1], 0.5], - [bounds[1], (height / 2) + origin[1], 0.5]] + [ + [bounds[1], (-height / 2) + origin[1], 0.5], + [bounds[1], (height / 2) + origin[1], 0.5], + ] ).astype(np.float32) right_line = pygfx.Line( pygfx.Geometry(positions=right_line_data), - pygfx.LineMaterial(thickness=3, color=edge_color) + pygfx.LineMaterial(thickness=3, color=edge_color), ) # position data for the left edge line - bottom_line_data = \ - np.array( - [[(-width / 2) + origin[0], origin[1], 0.5], - [(width / 2) + origin[0], origin[1], 0.5]] - ).astype(np.float32) + bottom_line_data = np.array( + [ + [(-width / 2) + origin[0], origin[1], 0.5], + [(width / 2) + origin[0], origin[1], 0.5], + ] + ).astype(np.float32) bottom_line = pygfx.Line( pygfx.Geometry(positions=bottom_line_data), - pygfx.LineMaterial(thickness=3, color=edge_color) + pygfx.LineMaterial(thickness=3, color=edge_color), ) # position data for the right edge line top_line_data = np.array( - [[(-width / 2) + origin[0], bounds[1], 0.5], - [(width / 2) + origin[0], bounds[1], 0.5]] + [ + [(-width / 2) + origin[0], bounds[1], 0.5], + [(width / 2) + origin[0], bounds[1], 0.5], + ] ).astype(np.float32) top_line = pygfx.Line( pygfx.Geometry(positions=top_line_data), - pygfx.LineMaterial(thickness=3, color=edge_color) + pygfx.LineMaterial(thickness=3, color=edge_color), ) self.edges: Tuple[pygfx.Line, ...] = ( - left_line, right_line, bottom_line, top_line + left_line, + right_line, + bottom_line, + top_line, ) # left line, right line, bottom line, top line # add the edge lines diff --git a/fastplotlib/graphics/selectors/_sync.py b/fastplotlib/graphics/selectors/_sync.py index 385f2cea1..b01823394 100644 --- a/fastplotlib/graphics/selectors/_sync.py +++ b/fastplotlib/graphics/selectors/_sync.py @@ -1,5 +1,3 @@ -from typing import * - from . import LinearSelector diff --git a/fastplotlib/graphics/text.py b/fastplotlib/graphics/text.py index 42bc3dba8..2648e2fa6 100644 --- a/fastplotlib/graphics/text.py +++ b/fastplotlib/graphics/text.py @@ -7,14 +7,14 @@ class TextGraphic(Graphic): def __init__( - self, - text: str, - position: Tuple[int] = (0, 0, 0), - size: int = 10, - face_color: Union[str, np.ndarray] = "w", - outline_color: Union[str, np.ndarray] = "w", - outline_thickness=0, - name: str = None, + self, + text: str, + position: Tuple[int] = (0, 0, 0), + size: int = 10, + face_color: Union[str, np.ndarray] = "w", + outline_color: Union[str, np.ndarray] = "w", + outline_thickness=0, + name: str = None, ): """ Create a text Graphic @@ -23,24 +23,36 @@ def __init__( ---------- text: str display text + position: int tuple, default (0, 0, 0) int tuple indicating location of text in scene + size: int, default 10 text size + face_color: str or array, default "w" str or RGBA array to set the color of the text + outline_color: str or array, default "w" str or RGBA array to set the outline color of the text + outline_thickness: int, default 0 text outline thickness + name: str, optional name of graphic, passed to Graphic + """ + super(TextGraphic, self).__init__(name=name) world_object = pygfx.Text( pygfx.TextGeometry(text=str(text), font_size=size, screen_space=False), - pygfx.TextMaterial(color=face_color, outline_color=outline_color, outline_thickness=outline_thickness) + pygfx.TextMaterial( + color=face_color, + outline_color=outline_color, + outline_thickness=outline_thickness, + ), ) self._set_world_object(world_object) diff --git a/fastplotlib/layouts/__init__.py b/fastplotlib/layouts/__init__.py index 682d6dfc9..aaed4c5a4 100644 --- a/fastplotlib/layouts/__init__.py +++ b/fastplotlib/layouts/__init__.py @@ -1,5 +1,4 @@ from ._gridplot import GridPlot +from ._plot import Plot -__all__ = [ - "GridPlot" -] +__all__ = ["Plot", "GridPlot"] diff --git a/fastplotlib/layouts/_base.py b/fastplotlib/layouts/_base.py index 0167fb1af..69f50800e 100644 --- a/fastplotlib/layouts/_base.py +++ b/fastplotlib/layouts/_base.py @@ -4,12 +4,19 @@ import numpy as np import pygfx -from pygfx import Scene, OrthographicCamera, PerspectiveCamera, PanZoomController, OrbitController, \ - Viewport, WgpuRenderer +from pygfx import ( + Scene, + OrthographicCamera, + PerspectiveCamera, + PanZoomController, + OrbitController, + Viewport, + WgpuRenderer, +) from pylinalg import vec_transform, vec_unproject from wgpu.gui.auto import WgpuCanvas -from ..graphics._base import Graphic, GraphicCollection +from ..graphics._base import Graphic from ..graphics.selectors._base_selector import BaseSelector # dict to store Graphic instances @@ -21,15 +28,15 @@ class PlotArea: def __init__( - self, - parent, - position: Any, - camera: Union[OrthographicCamera, PerspectiveCamera], - controller: Union[PanZoomController, OrbitController], - scene: Scene, - canvas: WgpuCanvas, - renderer: WgpuRenderer, - name: str = None, + self, + parent, + position: Any, + camera: Union[OrthographicCamera, PerspectiveCamera], + controller: Union[PanZoomController, OrbitController], + scene: Scene, + canvas: WgpuCanvas, + renderer: WgpuRenderer, + name: str = None, ): """ Base class for plot creation and management. ``PlotArea`` is not intended to be instantiated by users @@ -39,24 +46,33 @@ def __init__( ---------- parent: PlotArea parent class of subclasses will be a ``PlotArea`` instance + position: Any typical use will be for ``subplots`` in a ``gridplot``, position would correspond to the ``[row, column]`` location of the ``subplot`` in its ``gridplot`` + camera: pygfx OrthographicCamera or pygfx PerspectiveCamera ``OrthographicCamera`` type is used to visualize 2D content and ``PerspectiveCamera`` type is used to view 3D content, used to view the scene + controller: pygfx PanZoomController or pygfx OrbitController ``PanZoomController`` type is used for 2D pan-zoom camera control and ``OrbitController`` type is used for rotating the camera around a center position, used to control the camera + scene: pygfx Scene represents the root of a scene graph, will be viewed by the given ``camera`` + canvas: WgpuCanvas provides surface on which a scene will be rendered + renderer: WgpuRenderer object used to render scenes using wgpu + name: str, optional name of ``subplot`` or ``plot`` subclass being instantiated + """ + self._parent: PlotArea = parent self._position = position @@ -101,12 +117,12 @@ def parent(self): @property def position(self) -> Union[Tuple[int, int], Any]: - """Used by subclass to manage its own referencing system""" + """Position of this plot area within a larger layout (such as GridPlot) if relevant""" return self._position @property def scene(self) -> Scene: - """The Scene where Graphics live""" + """The Scene where Graphics lie in this plot area""" return self._scene @property @@ -121,6 +137,7 @@ def renderer(self) -> WgpuRenderer: @property def viewport(self) -> Viewport: + """The rectangular area of the renderer associated to this plot area""" return self._viewport @property @@ -156,6 +173,7 @@ def selectors(self) -> Tuple[BaseSelector, ...]: @property def name(self) -> Any: + """The name of this plot area""" return self._name @name.setter @@ -166,7 +184,9 @@ def get_rect(self) -> Tuple[float, float, float, float]: """allows setting the region occupied by the viewport w.r.t. the parent""" raise NotImplementedError("Must be implemented in subclass") - def map_screen_to_world(self, pos: Union[Tuple[float, float], pygfx.PointerEvent]) -> np.ndarray: + def map_screen_to_world( + self, pos: Union[Tuple[float, float], pygfx.PointerEvent] + ) -> np.ndarray: """ Map screen position to world position @@ -191,11 +211,7 @@ def map_screen_to_world(self, pos: Union[Tuple[float, float], pygfx.PointerEvent ) # convert screen position to NDC - pos_ndc = ( - pos_rel[0] / vs[0] * 2 - 1, - -(pos_rel[1] / vs[1] * 2 - 1), - 0 - ) + pos_ndc = (pos_rel[0] / vs[0] * 2 - 1, -(pos_rel[1] / vs[1] * 2 - 1), 0) # get world position pos_ndc += vec_transform(self.camera.world.position, self.camera.camera_matrix) @@ -220,7 +236,7 @@ def add_graphic(self, graphic: Graphic, center: bool = True): Parameters ---------- - graphic: Graphic or GraphicCollection + graphic: Graphic or `:ref:GraphicCollection` Add a Graphic or a GraphicCollection to the plot area. Note: this must be a real Graphic instance and not a proxy @@ -230,40 +246,44 @@ def add_graphic(self, graphic: Graphic, center: bool = True): """ self._add_or_insert_graphic(graphic=graphic, center=center, action="add") - graphic.position_z = len(self._graphics) + graphic.position_z = len(self) def insert_graphic( - self, - graphic: Graphic, - center: bool = True, - index: int = 0, - z_position: int = None + self, + graphic: Graphic, + center: bool = True, + index: int = 0, + z_position: int = None, ): """ Insert graphic into scene at given position ``index`` in stored graphics. Parameters ---------- - graphic: Graphic or GraphicCollection - Add a Graphic or a GraphicCollection to the plot area at a given position. + graphic: Graphic + Add a Graphic to the plot area at a given position. Note: must be a real Graphic instance, not a weakref proxy to a Graphic center: bool, default True Center the camera on the newly added Graphic index: int, default 0 - Index to insert graphic. + Index to insert graphic. z_position: int, default None z axis position to place Graphic. If ``None``, uses value of `index` argument """ if index > len(self._graphics): - raise IndexError(f"Position {index} is out of bounds for number of graphics currently " - f"in the PlotArea: {len(self._graphics)}\n" - f"Call `add_graphic` method to insert graphic in the last position of the stored graphics") + raise IndexError( + f"Position {index} is out of bounds for number of graphics currently " + f"in the PlotArea: {len(self._graphics)}\n" + f"Call `add_graphic` method to insert graphic in the last position of the stored graphics" + ) - self._add_or_insert_graphic(graphic=graphic, center=center, action="insert", index=index) + self._add_or_insert_graphic( + graphic=graphic, center=center, action="insert", index=index + ) if z_position is None: graphic.position_z = index @@ -271,11 +291,11 @@ def insert_graphic( graphic.position_z = z_position def _add_or_insert_graphic( - self, - graphic: Graphic, - center: bool = True, - action: str = Union["insert", "add"], - index: int = 0 + self, + graphic: Graphic, + center: bool = True, + action: str = Union["insert", "add"], + index: int = 0, ): """Private method to handle inserting or adding a graphic to a PlotArea.""" if not isinstance(graphic, Graphic): @@ -289,7 +309,9 @@ def _add_or_insert_graphic( if isinstance(graphic, BaseSelector): # store in SELECTORS dict loc = graphic.loc - SELECTORS[loc] = graphic # add hex id string for referencing this graphic instance + SELECTORS[ + loc + ] = graphic # add hex id string for referencing this graphic instance # don't manage garbage collection of LineSliders for now if action == "insert": self._selectors.insert(index, loc) @@ -298,7 +320,9 @@ def _add_or_insert_graphic( else: # store in GRAPHICS dict loc = graphic.loc - GRAPHICS[loc] = graphic # add hex id string for referencing this graphic instance + GRAPHICS[ + loc + ] = graphic # add hex id string for referencing this graphic instance if action == "insert": self._graphics.insert(index, loc) @@ -324,7 +348,9 @@ def _check_graphic_name_exists(self, name): graphic_names.append(s.name) if name in graphic_names: - raise ValueError(f"graphics must have unique names, current graphic names are:\n {graphic_names}") + raise ValueError( + f"graphics must have unique names, current graphic names are:\n {graphic_names}" + ) def center_graphic(self, graphic: Graphic, zoom: float = 1.35): """ @@ -332,7 +358,7 @@ def center_graphic(self, graphic: Graphic, zoom: float = 1.35): Parameters ---------- - graphic: Graphic or GraphicCollection + graphic: Graphic The graphic instance to center on zoom: float, default 1.3 @@ -410,7 +436,7 @@ def remove_graphic(self, graphic: Graphic): Parameters ---------- - graphic: Graphic or GraphicCollection + graphic: Graphic The graphic to remove from the scene """ @@ -423,7 +449,7 @@ def delete_graphic(self, graphic: Graphic): Parameters ---------- - graphic: Graphic or GraphicCollection + graphic: Graphic The graphic to delete """ @@ -440,7 +466,9 @@ def delete_graphic(self, graphic: Graphic): kind = "selector" glist = self._selectors else: - raise KeyError(f"Graphic with following address not found in plot area: {loc}") + raise KeyError( + f"Graphic with following address not found in plot area: {loc}" + ) # remove from scene if necessary if graphic.world_object in self.scene.children: @@ -477,9 +505,16 @@ def __getitem__(self, name: str): graphic_names = list() for g in self.graphics: graphic_names.append(g.name) + + selector_names = list() for s in self.selectors: - graphic_names.append(s.name) - raise IndexError(f"no graphic of given name, the current graphics are:\n {graphic_names}") + selector_names.append(s.name) + + raise IndexError( + f"No graphic or selector of given name.\n" + f"The current graphics are:\n {graphic_names}\n" + f"The current selectors are:\n {selector_names}" + ) def __str__(self): if self.name is None: @@ -492,8 +527,13 @@ def __str__(self): def __repr__(self): newline = "\n\t" - return f"{self}\n" \ - f" parent: {self.parent}\n" \ - f" Graphics:\n" \ - f"\t{newline.join(graphic.__repr__() for graphic in self.graphics)}" \ - f"\n" + return ( + f"{self}\n" + f" parent: {self.parent}\n" + f" Graphics:\n" + f"\t{newline.join(graphic.__repr__() for graphic in self.graphics)}" + f"\n" + ) + + def __len__(self) -> int: + return len(self._graphics) + len(self.selectors) diff --git a/fastplotlib/layouts/_defaults.py b/fastplotlib/layouts/_defaults.py index 314774751..9a223855f 100644 --- a/fastplotlib/layouts/_defaults.py +++ b/fastplotlib/layouts/_defaults.py @@ -2,19 +2,21 @@ from typing import * camera_types = { - '2d': pygfx.OrthographicCamera, - '3d': pygfx.PerspectiveCamera, + "2d": pygfx.OrthographicCamera, + "3d": pygfx.PerspectiveCamera, } controller_types = { - '2d': pygfx.PanZoomController, - '3d': pygfx.OrbitController, + "2d": pygfx.PanZoomController, + "3d": pygfx.OrbitController, pygfx.OrthographicCamera: pygfx.PanZoomController, pygfx.PerspectiveCamera: pygfx.OrbitController, } -def create_camera(camera_type: str, big_camera: bool = False) -> Union[pygfx.OrthographicCamera, pygfx.PerspectiveCamera]: +def create_camera( + camera_type: str, big_camera: bool = False +) -> Union[pygfx.OrthographicCamera, pygfx.PerspectiveCamera]: camera_type = camera_type.split("-") # kinda messy but works for now diff --git a/fastplotlib/layouts/_gridplot.py b/fastplotlib/layouts/_gridplot.py index 8f25f927b..b339e8659 100644 --- a/fastplotlib/layouts/_gridplot.py +++ b/fastplotlib/layouts/_gridplot.py @@ -5,6 +5,7 @@ from typing import * from inspect import getfullargspec from warnings import warn +import os import pygfx @@ -34,14 +35,14 @@ def to_array(a) -> np.ndarray: class GridPlot(RecordMixin): def __init__( - self, - shape: Tuple[int, int], - cameras: Union[np.ndarray, str] = '2d', - controllers: Union[np.ndarray, str] = None, - canvas: Union[str, WgpuCanvas, pygfx.Texture] = None, - renderer: pygfx.Renderer = None, - size: Tuple[int, int] = (500, 300), - **kwargs + self, + shape: Tuple[int, int], + cameras: Union[np.ndarray, str] = "2d", + controllers: Union[np.ndarray, str] = None, + canvas: Union[str, WgpuCanvas, pygfx.Texture] = None, + renderer: pygfx.WgpuRenderer = None, + size: Tuple[int, int] = (500, 300), + **kwargs, ): """ A grid of subplots. @@ -53,16 +54,16 @@ def __init__( cameras: np.ndarray or str, optional | One of ``"2d"`` or ``"3d"`` indicating 2D or 3D cameras for all subplots - | OR - - | Array of ``2d`` and/or ``3d`` that specifies the camera type for each subplot: + | Array of ``2d`` and/or ``3d`` that specifies the camera type for each subplot controllers: np.ndarray or str, optional | If `None` a unique controller is created for each subplot | If "sync" all the subplots use the same controller | If ``numpy.array``, its shape must be the same as ``grid_shape``. + This allows custom assignment of controllers + | Example: | unique controllers for a 2x2 gridplot: np.array([[0, 1], [2, 3]]) | same controllers for first 2 plots and last 2 plots: np.array([[0, 0, 1], [2, 3, 3]]) @@ -77,6 +78,7 @@ def __init__( starting size of canvas, default (500, 300) """ + self.shape = shape self.toolbar = None @@ -84,13 +86,19 @@ def __init__( if isinstance(cameras, str): if cameras not in valid_cameras: - raise ValueError(f"If passing a str, `cameras` must be one of: {valid_cameras}") + raise ValueError( + f"If passing a str, `cameras` must be one of: {valid_cameras}" + ) # create the array representing the views for each subplot in the grid - cameras = np.array([cameras] * self.shape[0] * self.shape[1]).reshape(self.shape) + cameras = np.array([cameras] * self.shape[0] * self.shape[1]).reshape( + self.shape + ) if isinstance(controllers, str): if controllers == "sync": - controllers = np.zeros(self.shape[0] * self.shape[1], dtype=int).reshape(self.shape) + controllers = np.zeros( + self.shape[0] * self.shape[1], dtype=int + ).reshape(self.shape) if controllers is None: controllers = np.arange(self.shape[0] * self.shape[1]).reshape(self.shape) @@ -109,20 +117,26 @@ def __init__( # create controllers if the arguments were integers if np.issubdtype(controllers.dtype, np.integer): - if not np.all(np.sort(np.unique(controllers)) == np.arange(np.unique(controllers).size)): + if not np.all( + np.sort(np.unique(controllers)) + == np.arange(np.unique(controllers).size) + ): raise ValueError("controllers must be consecutive integers") for controller in np.unique(controllers): cam = np.unique(cameras[controllers == controller]) if cam.size > 1: raise ValueError( - f"Controller id: {controller} has been assigned to multiple different camera types") + f"Controller id: {controller} has been assigned to multiple different camera types" + ) self._controllers[controllers == controller] = create_controller(cam[0]) # else assume it's a single pygfx.Controller instance or a list of controllers else: if isinstance(controllers, pygfx.Controller): - self._controllers = np.array([controllers] * shape[0] * shape[1]).reshape(shape) + self._controllers = np.array( + [controllers] * shape[0] * shape[1] + ).reshape(shape) else: self._controllers = np.array(controllers).reshape(shape) @@ -139,17 +153,14 @@ def __init__( else: self.names = None - self.canvas = canvas - self.renderer = renderer + self._canvas = canvas + self._renderer = renderer nrows, ncols = self.shape - self._subplots: np.ndarray[Subplot] = np.ndarray(shape=(nrows, ncols), dtype=object) - # self.viewports: np.ndarray[Subplot] = np.ndarray(shape=(nrows, ncols), dtype=object) - - # self._controllers: List[pygfx.PanZoomController] = [ - # pygfx.PanZoomController() for i in range(np.unique(controllers).size) - # ] + self._subplots: np.ndarray[Subplot] = np.ndarray( + shape=(nrows, ncols), dtype=object + ) for i, j in self._get_iterator(): position = (i, j) @@ -162,13 +173,14 @@ def __init__( name = None self._subplots[i, j] = Subplot( + parent=self, position=position, parent_dims=(nrows, ncols), camera=camera, controller=controller, canvas=canvas, renderer=renderer, - name=name + name=name, ) self._animate_funcs_pre: List[callable] = list() @@ -180,6 +192,16 @@ def __init__( RecordMixin.__init__(self) + @property + def canvas(self) -> WgpuCanvas: + """The canvas associated to this GridPlot""" + return self._canvas + + @property + def renderer(self) -> pygfx.WgpuRenderer: + """The renderer associated to this GridPlot""" + return self._renderer + def __getitem__(self, index: Union[Tuple[int, int], str]) -> Subplot: if isinstance(index, str): for subplot in self._subplots.ravel(): @@ -217,10 +239,10 @@ def _call_animate_functions(self, funcs: Iterable[callable]): fn() def add_animations( - self, - *funcs: Iterable[callable], - pre_render: bool = True, - post_render: bool = False + self, + *funcs: Iterable[callable], + pre_render: bool = True, + post_render: bool = False, ): """ Add function(s) that are called on every render cycle. @@ -272,10 +294,7 @@ def remove_animation(self, func): self._animate_funcs_post.remove(func) def show( - self, - autoscale: bool = True, - maintain_aspect: bool = None, - toolbar: bool = True + self, autoscale: bool = True, maintain_aspect: bool = None, toolbar: bool = True ): """ Begins the rendering event loop and returns the canvas @@ -295,7 +314,7 @@ def show( ------- WgpuCanvas the canvas - + """ self.canvas.request_draw(self.render) @@ -309,13 +328,20 @@ def show( _maintain_aspect = maintain_aspect subplot.auto_scale(maintain_aspect=_maintain_aspect, zoom=0.95) + if "NB_SNAPSHOT" in os.environ.keys(): + # used for docs + if os.environ["NB_SNAPSHOT"] == "1": + return self.canvas.snapshot() + # check if in jupyter notebook, or if toolbar is False if (self.canvas.__class__.__name__ != "JupyterWgpuCanvas") or (not toolbar): return self.canvas if self.toolbar is None: self.toolbar = GridPlotToolBar(self) - self.toolbar.maintain_aspect_button.value = self[0, 0].camera.maintain_aspect + self.toolbar.maintain_aspect_button.value = self[ + 0, 0 + ].camera.maintain_aspect return VBox([self.canvas, self.toolbar.widget]) @@ -347,8 +373,7 @@ def __repr__(self): class GridPlotToolBar: - def __init__(self, - plot: GridPlot): + def __init__(self, plot: GridPlot): """ Basic toolbar for a GridPlot instance. @@ -358,20 +383,50 @@ def __init__(self, """ self.plot = plot - self.autoscale_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.autoscale_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.flip_camera_button = Button(value=False, disabled=False, icon='arrows-v', - layout=Layout(width='auto'), tooltip='flip') - - self.record_button = ToggleButton(value=False, disabled=False, icon='video', - layout=Layout(width='auto'), tooltip='record') + self.flip_camera_button = Button( + value=False, + disabled=False, + icon="arrows-v", + layout=Layout(width="auto"), + tooltip="flip", + ) + + self.record_button = ToggleButton( + value=False, + disabled=False, + icon="video", + layout=Layout(width="auto"), + tooltip="record", + ) positions = list(product(range(self.plot.shape[0]), range(self.plot.shape[1]))) values = list() @@ -380,23 +435,31 @@ def __init__(self, values.append(self.plot[pos].name) else: values.append(str(pos)) - self.dropdown = Dropdown(options=values, disabled=False, description='Subplots:', - layout=Layout(width='200px')) - - self.widget = HBox([self.autoscale_button, - self.center_scene_button, - self.panzoom_controller_button, - self.maintain_aspect_button, - self.flip_camera_button, - self.record_button, - self.dropdown]) - - self.panzoom_controller_button.observe(self.panzoom_control, 'value') + self.dropdown = Dropdown( + options=values, + disabled=False, + description="Subplots:", + layout=Layout(width="200px"), + ) + + self.widget = HBox( + [ + self.autoscale_button, + self.center_scene_button, + self.panzoom_controller_button, + self.maintain_aspect_button, + self.flip_camera_button, + self.record_button, + self.dropdown, + ] + ) + + self.panzoom_controller_button.observe(self.panzoom_control, "value") self.autoscale_button.on_click(self.auto_scale) self.center_scene_button.on_click(self.center_scene) - self.maintain_aspect_button.observe(self.maintain_aspect, 'value') + self.maintain_aspect_button.observe(self.maintain_aspect, "value") self.flip_camera_button.on_click(self.flip_camera) - self.record_button.observe(self.record_plot, 'value') + self.record_button.observe(self.record_plot, "value") self.plot.renderer.add_event_handler(self.update_current_subplot, "click") @@ -444,7 +507,9 @@ def update_current_subplot(self, ev): def record_plot(self, obj): if self.record_button.value: try: - self.plot.record_start(f"./{datetime.now().isoformat(timespec='seconds').replace(':', '_')}.mp4") + self.plot.record_start( + f"./{datetime.now().isoformat(timespec='seconds').replace(':', '_')}.mp4" + ) except Exception: traceback.print_exc() self.record_button.value = False diff --git a/fastplotlib/plot.py b/fastplotlib/layouts/_plot.py similarity index 56% rename from fastplotlib/plot.py rename to fastplotlib/layouts/_plot.py index e3c358bc2..1f91bb303 100644 --- a/fastplotlib/plot.py +++ b/fastplotlib/layouts/_plot.py @@ -1,6 +1,7 @@ from typing import * from datetime import datetime import traceback +import os import pygfx from wgpu.gui.auto import WgpuCanvas, is_jupyter @@ -8,19 +9,20 @@ if is_jupyter(): from ipywidgets import HBox, Layout, Button, ToggleButton, VBox -from .layouts._subplot import Subplot -from .layouts._record_mixin import RecordMixin +from ._subplot import Subplot +from ._record_mixin import RecordMixin +from ..graphics.selectors import PolygonSelector class Plot(Subplot, RecordMixin): def __init__( - self, - canvas: WgpuCanvas = None, - renderer: pygfx.Renderer = None, - camera: str = '2d', - controller: Union[pygfx.PanZoomController, pygfx.OrbitController] = None, - size: Tuple[int, int] = (500, 300), - **kwargs + self, + canvas: WgpuCanvas = None, + renderer: pygfx.WgpuRenderer = None, + camera: str = "2d", + controller: Union[pygfx.PanZoomController, pygfx.OrbitController] = None, + size: Tuple[int, int] = (500, 300), + **kwargs, ): """ Simple Plot object. @@ -46,53 +48,16 @@ def __init__( kwargs passed to Subplot, for example ``name`` - Examples - -------- - - Simple example - - .. code-block:: python - - from fastplotlib import Plot - - # create a `Plot` instance - plot1 = Plot() - - # make some random 2D image data - data = np.random.rand(512, 512) - - # plot the image data - plot1.add_image(data=data) - - # show the plot - plot1.show() - - Sharing controllers, start from the previous example and create a new jupyter cell - - .. code-block:: python - - # use the controller from the previous plot - # this will sync the pan & zoom controller - plot2 = Plot(controller=plot1.controller) - - # make some random 2D image data - data = np.random.rand(512, 512) - - # plot the image data - plot2.add_image(data=data) - - # show the plot - plot2.show() - """ super(Plot, self).__init__( + parent=None, position=(0, 0), parent_dims=(1, 1), canvas=canvas, renderer=renderer, camera=camera, controller=controller, - **kwargs + **kwargs, ) RecordMixin.__init__(self) @@ -107,10 +72,7 @@ def render(self): self.canvas.request_draw() def show( - self, - autoscale: bool = True, - maintain_aspect: bool = None, - toolbar: bool = True + self, autoscale: bool = True, maintain_aspect: bool = None, toolbar: bool = True ): """ Begins the rendering event loop and returns the canvas @@ -133,7 +95,7 @@ def show( """ self.canvas.request_draw(self.render) - + self.canvas.set_logical_size(*self._starting_size) if maintain_aspect is None: @@ -142,6 +104,11 @@ def show( if autoscale: self.auto_scale(maintain_aspect=maintain_aspect, zoom=0.95) + if "NB_SNAPSHOT" in os.environ.keys(): + # used for docs + if os.environ["NB_SNAPSHOT"] == "1": + return self.canvas.snapshot() + # check if in jupyter notebook, or if toolbar is False if (self.canvas.__class__.__name__ != "JupyterWgpuCanvas") or (not toolbar): return self.canvas @@ -161,8 +128,7 @@ def close(self): class ToolBar: - def __init__(self, - plot: Plot): + def __init__(self, plot: Plot): """ Basic toolbar for a Plot instance. @@ -172,34 +138,78 @@ def __init__(self, """ self.plot = plot - self.autoscale_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.autoscale_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.flip_camera_button = Button(value=False, disabled=False, icon='arrows-v', - layout=Layout(width='auto'), tooltip='flip') - self.record_button = ToggleButton(value=False, disabled=False, icon='video', - layout=Layout(width='auto'), tooltip='record') - - self.widget = HBox([self.autoscale_button, - self.center_scene_button, - self.panzoom_controller_button, - self.maintain_aspect_button, - self.flip_camera_button, - self.record_button]) - - self.panzoom_controller_button.observe(self.panzoom_control, 'value') + self.flip_camera_button = Button( + value=False, + disabled=False, + icon="arrows-v", + layout=Layout(width="auto"), + tooltip="flip", + ) + + self.add_polygon_button = Button( + value=False, + disabled=False, + icon="draw-polygon", + layout=Layout(width="auto"), + tooltip="add PolygonSelector" + ) + + self.record_button = ToggleButton( + value=False, + disabled=False, + icon="video", + layout=Layout(width="auto"), + tooltip="record", + ) + + self.widget = HBox( + [ + self.autoscale_button, + self.center_scene_button, + self.panzoom_controller_button, + self.maintain_aspect_button, + self.flip_camera_button, + self.add_polygon_button, + self.record_button, + ] + ) + + self.panzoom_controller_button.observe(self.panzoom_control, "value") self.autoscale_button.on_click(self.auto_scale) self.center_scene_button.on_click(self.center_scene) - self.maintain_aspect_button.observe(self.maintain_aspect, 'value') + self.maintain_aspect_button.observe(self.maintain_aspect, "value") self.flip_camera_button.on_click(self.flip_camera) - self.record_button.observe(self.record_plot, 'value') + self.add_polygon_button.on_click(self.add_polygon) + self.record_button.observe(self.record_plot, "value") def auto_scale(self, obj): self.plot.auto_scale(maintain_aspect=self.plot.camera.maintain_aspect) @@ -216,10 +226,17 @@ def maintain_aspect(self, obj): def flip_camera(self, obj): self.plot.camera.world.scale_y *= -1 + def add_polygon(self, obj): + ps = PolygonSelector(edge_width=3, edge_color="magenta") + + self.plot.add_graphic(ps, center=False) + def record_plot(self, obj): if self.record_button.value: try: - self.plot.record_start(f"./{datetime.now().isoformat(timespec='seconds').replace(':', '_')}.mp4") + self.plot.record_start( + f"./{datetime.now().isoformat(timespec='seconds').replace(':', '_')}.mp4" + ) except Exception: traceback.print_exc() self.record_button.value = False diff --git a/fastplotlib/layouts/_record_mixin.py b/fastplotlib/layouts/_record_mixin.py index 6af722624..e3a491915 100644 --- a/fastplotlib/layouts/_record_mixin.py +++ b/fastplotlib/layouts/_record_mixin.py @@ -13,16 +13,17 @@ class VideoWriterAV(Process): """Video writer, uses PyAV in an external process to write frames to disk""" + def __init__( - self, - path: Union[Path, str], - queue: Queue, - fps: int, - width: int, - height: int, - codec: str, - pixel_format: str, - options: dict = None + self, + path: Union[Path, str], + queue: Queue, + fps: int, + width: int, + height: int, + codec: str, + pixel_format: str, + options: dict = None, ): super().__init__() self.queue = queue @@ -56,8 +57,10 @@ def run(self): break frame = av.VideoFrame.from_ndarray( - frame[:self.stream.height, :self.stream.width], # trim if necessary because of x264 - format="rgb24" + frame[ + : self.stream.height, : self.stream.width + ], # trim if necessary because of x264 + format="rgb24", ) for packet in self.stream.encode(frame): @@ -104,12 +107,12 @@ def _record(self): self._video_writer_queue.put(ss.data[..., :-1]) def record_start( - self, - path: Union[str, Path], - fps: int = 25, - codec: str = "mpeg4", - pixel_format: str = "yuv420p", - options: dict = None + self, + path: Union[str, Path], + fps: int = 25, + codec: str = "mpeg4", + pixel_format: str = "yuv420p", + options: dict = None, ): """ Start a recording, experimental. Call ``record_end()`` to end a recording. @@ -198,7 +201,7 @@ def record_start( height=ss.height, codec=codec, pixel_format=pixel_format, - options=options + options=options, ) # start writer process diff --git a/fastplotlib/layouts/_subplot.py b/fastplotlib/layouts/_subplot.py index e2ae59d7e..a8cd4852b 100644 --- a/fastplotlib/layouts/_subplot.py +++ b/fastplotlib/layouts/_subplot.py @@ -1,86 +1,79 @@ from typing import * -import numpy as np -from math import copysign -from functools import partial -import weakref -from inspect import signature, getfullargspec +from inspect import getfullargspec from warnings import warn -import traceback - -from pygfx import Scene, OrthographicCamera, PanZoomController, OrbitController, \ - AxesHelper, GridHelper, WgpuRenderer, Texture -from wgpu.gui.auto import WgpuCanvas - - -# TODO: this determination can be better -try: - from wgpu.gui.jupyter import JupyterWgpuCanvas -except ImportError: - JupyterWgpuCanvas = False - -try: - from wgpu.gui.qt import QWgpuCanvas -except ImportError: - QWgpuCanvas = False - -try: - from wgpu.gui.glfw import GlfwWgpuCanvas -except ImportError: - GlfwWgpuCanvas = False +import numpy as np -CANVAS_OPTIONS = ["jupyter", "glfw", "qt"] -CANVAS_OPTIONS_AVAILABLE = { - "jupyter": JupyterWgpuCanvas, - "glfw": GlfwWgpuCanvas, - "qt": QWgpuCanvas -} +from pygfx import ( + Scene, + OrthographicCamera, + PanZoomController, + OrbitController, + AxesHelper, + GridHelper, + WgpuRenderer, + Texture, +) +from wgpu.gui.auto import WgpuCanvas +from ..graphics import TextGraphic from ._utils import make_canvas_and_renderer from ._base import PlotArea -from .. import graphics -from ..graphics import TextGraphic from ._defaults import create_camera, create_controller +from .graphic_methods_mixin import GraphicMethodsMixin -class Subplot(PlotArea): +class Subplot(PlotArea, GraphicMethodsMixin): def __init__( - self, - position: Tuple[int, int] = None, - parent_dims: Tuple[int, int] = None, - camera: str = '2d', - controller: Union[PanZoomController, OrbitController] = None, - canvas: Union[str, WgpuCanvas, Texture] = None, - renderer: WgpuRenderer = None, - name: str = None, - **kwargs + self, + parent: Any = None, + position: Tuple[int, int] = None, + parent_dims: Tuple[int, int] = None, + camera: str = "2d", + controller: Union[PanZoomController, OrbitController] = None, + canvas: Union[str, WgpuCanvas, Texture] = None, + renderer: WgpuRenderer = None, + name: str = None, + **kwargs, ): """ General plot object that composes a ``Gridplot``. Each ``Gridplot`` instance will have [n rows, n columns] of subplots. + .. important:: + ``Subplot`` is not meant to be constructed directly, it only exists as part of a ``GridPlot`` + Parameters ---------- position: int tuple, optional corresponds to the [row, column] position of the subplot within a ``Gridplot`` + parent_dims: int tuple, optional dimensions of the parent ``GridPlot`` + camera: str, default '2d' indicates the kind of pygfx camera that will be instantiated, '2d' uses pygfx ``OrthographicCamera`` and '3d' uses pygfx ``PerspectiveCamera`` + controller: PanZoomController or OrbitOrthoController, optional ``PanZoomController`` type is used for 2D pan-zoom camera control and ``OrbitController`` type is used for rotating the camera around a center position, used to control the camera + canvas: WgpuCanvas, Texture, or one of "jupyter", "glfw", "qt", 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. + renderer: WgpuRenderer, optional object used to render scenes using wgpu + name: str, optional name of the subplot, will appear as ``TextGraphic`` above the subplot + """ + super(GraphicMethodsMixin, self).__init__() + canvas, renderer = make_canvas_and_renderer(canvas, renderer) if position is None: @@ -94,7 +87,7 @@ def __init__( if controller is None: controller = create_controller(camera) - self.docked_viewports = dict() + self._docks = dict() self.spacing = 2 @@ -108,52 +101,26 @@ def __init__( self._animate_funcs_post = list() super(Subplot, self).__init__( - parent=None, + parent=parent, position=position, camera=create_camera(camera), controller=controller, scene=Scene(), canvas=canvas, renderer=renderer, - name=name + name=name, ) for pos in ["left", "top", "right", "bottom"]: - dv = _DockedViewport(self, pos, size=0) + dv = Dock(self, pos, size=0) dv.name = pos - self.docked_viewports[pos] = dv + self.docks[pos] = dv self.children.append(dv) - # attach all the add_ methods - for graphic_cls_name in graphics.__all__: - cls = getattr(graphics, graphic_cls_name) - - pfunc = partial(self._create_graphic, cls) - pfunc.__signature__ = signature(cls) - pfunc.__doc__ = cls.__init__.__doc__ - - # cls.type is defined in Graphic.__init_subclass__ - setattr(self, f"add_{cls.type}", pfunc) - self._title_graphic: TextGraphic = None if self.name is not None: self.set_title(self.name) - def _create_graphic(self, graphic_class, *args, **kwargs) -> weakref.proxy: - if "center" in kwargs.keys(): - center = kwargs.pop("center") - else: - center = False - - if "name" in kwargs.keys(): - self._check_graphic_name_exists(kwargs["name"]) - - graphic = graphic_class(*args, **kwargs) - self.add_graphic(graphic, center=center) - - # only return a proxy to the real graphic - return weakref.proxy(graphic) - @property def name(self) -> Any: return self._name @@ -163,8 +130,23 @@ def name(self, name: Any): self._name = name self.set_title(name) + @property + def docks(self) -> dict: + """ + The docks of this plot area. Each ``dock`` is basically just a PlotArea too. + + The docks are: ["left", "top", "right", "bottom"] + + Returns + ------- + Dict[str, Dock] + {dock_name: Dock} + + """ + return self._docks + def set_title(self, text: Any): - """Sets the name of a subplot to 'top' viewport if defined.""" + """Sets the plot title, stored as a ``TextGraphic`` in the "top" dock area""" if text is None: return @@ -175,8 +157,8 @@ def set_title(self, text: Any): tg = TextGraphic(text) self._title_graphic = tg - self.docked_viewports["top"].size = 35 - self.docked_viewports["top"].add_graphic(tg) + self.docks["top"].size = 35 + self.docks["top"].add_graphic(tg) self.center_title() @@ -186,7 +168,7 @@ def center_title(self): raise AttributeError("No title graphic is set") self._title_graphic.world_object.position = (0, 0, 0) - self.docked_viewports["top"].center_graphic(self._title_graphic, zoom=1.5) + self.docks["top"].center_graphic(self._title_graphic, zoom=1.5) self._title_graphic.world_object.position_y = -3.5 def get_rect(self): @@ -194,19 +176,18 @@ def get_rect(self): row_ix, col_ix = self.position width_canvas, height_canvas = self.renderer.logical_size - x_pos = ((width_canvas / self.ncols) + ((col_ix - 1) * (width_canvas / self.ncols))) + self.spacing - y_pos = ((height_canvas / self.nrows) + ((row_ix - 1) * (height_canvas / self.nrows))) + self.spacing + x_pos = ( + (width_canvas / self.ncols) + ((col_ix - 1) * (width_canvas / self.ncols)) + ) + self.spacing + 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 - rect = np.array([ - x_pos, - y_pos, - width_subplot, - height_subplot - ]) + rect = np.array([x_pos, y_pos, width_subplot, height_subplot]) - for dv in self.docked_viewports.values(): + for dv in self.docks.values(): rect = rect + dv.get_parent_rect_adjust() return rect @@ -238,10 +219,10 @@ def _call_animate_functions(self, funcs: Iterable[callable]): fn() def add_animations( - self, - *funcs: Iterable[callable], - pre_render: bool = True, - post_render: bool = False + self, + *funcs: Iterable[callable], + pre_render: bool = True, + post_render: bool = False, ): """ Add function(s) that are called on every render cycle. @@ -307,41 +288,35 @@ def set_grid_visibility(self, visible: bool): self.scene.remove(self._grid) -class _DockedViewport(PlotArea): - _valid_positions = [ - "right", - "left", - "top", - "bottom" - ] +class Dock(PlotArea): + _valid_positions = ["right", "left", "top", "bottom"] def __init__( - self, - parent: Subplot, - position: str, - size: int, + self, + parent: Subplot, + position: str, + size: int, ): if position not in self._valid_positions: - raise ValueError(f"the `position` of an AnchoredViewport must be one of: {self._valid_positions}") + raise ValueError( + f"the `position` of an AnchoredViewport must be one of: {self._valid_positions}" + ) self._size = size - super(_DockedViewport, self).__init__( + super(Dock, self).__init__( parent=parent, position=position, camera=OrthographicCamera(), controller=PanZoomController(), scene=Scene(), canvas=parent.canvas, - renderer=parent.renderer + renderer=parent.renderer, ) - # self.scene.add( - # Background(None, BackgroundMaterial((0.2, 0.0, 0, 1), (0, 0.0, 0.2, 1))) - # ) - @property def size(self) -> int: + """Get or set the size of this dock""" return self._size @size.setter @@ -361,28 +336,59 @@ def get_rect(self, *args): 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) - self.size - y_pos = ((height_canvas / self.parent.nrows) + ((row_ix_parent - 1) * (height_canvas / self.parent.nrows))) + spacing - width_viewport = self.size - height_viewport = (height_canvas / self.parent.nrows) - spacing + x_pos = ( + (width_canvas / self.parent.ncols) + + ((col_ix_parent - 1) * (width_canvas / self.parent.ncols)) + + (width_canvas / self.parent.ncols) + - self.size + ) + y_pos = ( + (height_canvas / self.parent.nrows) + + ((row_ix_parent - 1) * (height_canvas / self.parent.nrows)) + ) + spacing + width_viewport = self.size + height_viewport = (height_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)) - y_pos = ((height_canvas / self.parent.nrows) + ((row_ix_parent - 1) * (height_canvas / self.parent.nrows))) + spacing - width_viewport = self.size - height_viewport = (height_canvas / self.parent.nrows) - spacing + x_pos = (width_canvas / self.parent.ncols) + ( + (col_ix_parent - 1) * (width_canvas / self.parent.ncols) + ) + y_pos = ( + (height_canvas / self.parent.nrows) + + ((row_ix_parent - 1) * (height_canvas / self.parent.nrows)) + ) + spacing + width_viewport = self.size + height_viewport = (height_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)) + spacing - y_pos = ((height_canvas / self.parent.nrows) + ((row_ix_parent - 1) * (height_canvas / self.parent.nrows))) + spacing - width_viewport = (width_canvas / self.parent.ncols) - spacing - height_viewport = self.size + x_pos = ( + (width_canvas / self.parent.ncols) + + ((col_ix_parent - 1) * (width_canvas / self.parent.ncols)) + + spacing + ) + y_pos = ( + (height_canvas / self.parent.nrows) + + ((row_ix_parent - 1) * (height_canvas / self.parent.nrows)) + ) + spacing + width_viewport = (width_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)) + spacing - y_pos = ((height_canvas / self.parent.nrows) + ((row_ix_parent - 1) * (height_canvas / self.parent.nrows))) + (height_canvas / self.parent.nrows) - self.size - width_viewport = (width_canvas / self.parent.ncols) - spacing - height_viewport = self.size + x_pos = ( + (width_canvas / self.parent.ncols) + + ((col_ix_parent - 1) * (width_canvas / self.parent.ncols)) + + spacing + ) + y_pos = ( + ( + (height_canvas / self.parent.nrows) + + ((row_ix_parent - 1) * (height_canvas / self.parent.nrows)) + ) + + (height_canvas / self.parent.nrows) + - self.size + ) + width_viewport = (width_canvas / self.parent.ncols) - spacing + height_viewport = self.size else: raise ValueError("invalid position") @@ -390,39 +396,47 @@ def get_rect(self, *args): def get_parent_rect_adjust(self): if self.position == "right": - return np.array([ - 0, # parent subplot x-position is same - 0, - -self.size, # width of parent subplot is `self.size` smaller - 0 - ]) + return np.array( + [ + 0, # parent subplot x-position is same + 0, + -self.size, # width of parent subplot is `self.size` smaller + 0, + ] + ) elif self.position == "left": - return np.array([ - self.size, # `self.size` added to parent subplot x-position - 0, - -self.size, # width of parent subplot is `self.size` smaller - 0 - ]) + return np.array( + [ + self.size, # `self.size` added to parent subplot x-position + 0, + -self.size, # width of parent subplot is `self.size` smaller + 0, + ] + ) elif self.position == "top": - return np.array([ - 0, - self.size, # `self.size` added to parent subplot y-position - 0, - -self.size, # height of parent subplot is `self.size` smaller - ]) + return np.array( + [ + 0, + self.size, # `self.size` added to parent subplot y-position + 0, + -self.size, # height of parent subplot is `self.size` smaller + ] + ) elif self.position == "bottom": - return np.array([ - 0, - 0, # parent subplot y-position is same, - 0, - -self.size, # height of parent subplot is `self.size` smaller - ]) + return np.array( + [ + 0, + 0, # parent subplot y-position is same, + 0, + -self.size, # height of parent subplot is `self.size` smaller + ] + ) def render(self): if self.size == 0: return - super(_DockedViewport, self).render() + super(Dock, self).render() diff --git a/fastplotlib/layouts/_utils.py b/fastplotlib/layouts/_utils.py index 76f3e4cee..ebfe9e306 100644 --- a/fastplotlib/layouts/_utils.py +++ b/fastplotlib/layouts/_utils.py @@ -28,16 +28,15 @@ CANVAS_OPTIONS_AVAILABLE = { "jupyter": JupyterWgpuCanvas, "glfw": GlfwWgpuCanvas, - "qt": QWgpuCanvas + "qt": QWgpuCanvas, } def make_canvas_and_renderer( - canvas: Union[str, WgpuCanvas, Texture, None], - renderer: [WgpuRenderer, None] + canvas: Union[str, WgpuCanvas, Texture, None], renderer: [WgpuRenderer, None] ): """ - Parses arguments and returns the appropriate canvas and renderer instances + Parses arguments and returns the appropriate canvas and renderer instances as a tuple (canvas, renderer) """ @@ -46,9 +45,7 @@ def make_canvas_and_renderer( elif isinstance(canvas, str): if canvas not in CANVAS_OPTIONS: - raise ValueError( - f"str canvas argument must be one of: {CANVAS_OPTIONS}" - ) + raise ValueError(f"str canvas argument must be one of: {CANVAS_OPTIONS}") elif not CANVAS_OPTIONS_AVAILABLE[canvas]: raise ImportError( f"The {canvas} framework is not installed for using this canvas" diff --git a/fastplotlib/layouts/graphic_methods_mixin.py b/fastplotlib/layouts/graphic_methods_mixin.py new file mode 100644 index 000000000..760083cb9 --- /dev/null +++ b/fastplotlib/layouts/graphic_methods_mixin.py @@ -0,0 +1,402 @@ +# This is an auto-generated file and should not be modified directly + +from typing import * + +import numpy +import weakref + +from ..graphics import * +from ..graphics._base import Graphic + + +class GraphicMethodsMixin: + def __init__(self): + pass + + def _create_graphic(self, graphic_class, *args, **kwargs) -> Graphic: + if 'center' in kwargs.keys(): + center = kwargs.pop('center') + else: + center = False + + if 'name' in kwargs.keys(): + self._check_graphic_name_exists(kwargs['name']) + + graphic = graphic_class(*args, **kwargs) + self.add_graphic(graphic, center=center) + + # only return a proxy to the real graphic + return weakref.proxy(graphic) + + def add_heatmap(self, data: Any, vmin: int = None, vmax: int = None, cmap: str = 'plasma', filter: str = 'nearest', chunk_size: int = 8192, isolated_buffer: bool = True, *args, **kwargs) -> HeatmapGraphic: + """ + + Create an Image Graphic + + Parameters + ---------- + data: array-like + array-like, usually numpy.ndarray, must support ``memoryview()`` + Tensorflow Tensors also work **probably**, but not thoroughly tested + | shape must be ``[x_dim, y_dim]`` + + vmin: int, optional + minimum value for color scaling, calculated from data if not provided + + vmax: int, optional + maximum value for color scaling, calculated from data if not provided + + cmap: str, optional, default "plasma" + colormap to use to display the data + + filter: str, optional, default "nearest" + interpolation filter, one of "nearest" or "linear" + + chunk_size: int, default 8192, max 8192 + chunk size for each tile used to make up the heatmap texture + + isolated_buffer: bool, default True + If True, initialize a buffer with the same shape as the input data and then + set the data, useful if the data arrays are ready-only such as memmaps. + If False, the input array is itself used as the buffer. + + args: + additional arguments passed to Graphic + + kwargs: + additional keyword arguments passed to Graphic + + Features + -------- + + **data**: :class:`.HeatmapDataFeature` + Manages the data buffer displayed in the HeatmapGraphic + + **cmap**: :class:`.HeatmapCmapFeature` + Manages the colormap + + **present**: :class:`.PresentFeature` + Control the presence of the Graphic in the scene + + + """ + return self._create_graphic(HeatmapGraphic, data, vmin, vmax, cmap, filter, chunk_size, isolated_buffer, *args, **kwargs) + + def add_image(self, data: Any, vmin: int = None, vmax: int = None, cmap: str = 'plasma', filter: str = 'nearest', isolated_buffer: bool = True, *args, **kwargs) -> ImageGraphic: + """ + + Create an Image Graphic + + Parameters + ---------- + data: array-like + array-like, usually numpy.ndarray, must support ``memoryview()`` + Tensorflow Tensors also work **probably**, but not thoroughly tested + | shape must be ``[x_dim, y_dim]`` or ``[x_dim, y_dim, rgb]`` + + vmin: int, optional + minimum value for color scaling, calculated from data if not provided + + vmax: int, optional + maximum value for color scaling, calculated from data if not provided + + cmap: str, optional, default "plasma" + colormap to use to display the image data, ignored if data is RGB + + filter: str, optional, default "nearest" + interpolation filter, one of "nearest" or "linear" + + isolated_buffer: bool, default True + If True, initialize a buffer with the same shape as the input data and then + set the data, useful if the data arrays are ready-only such as memmaps. + If False, the input array is itself used as the buffer. + + args: + additional arguments passed to Graphic + + kwargs: + additional keyword arguments passed to Graphic + + Features + -------- + + **data**: :class:`.ImageDataFeature` + Manages the data buffer displayed in the ImageGraphic + + **cmap**: :class:`.ImageCmapFeature` + Manages the colormap + + **present**: :class:`.PresentFeature` + Control the presence of the Graphic in the scene + + + """ + return self._create_graphic(ImageGraphic, data, vmin, vmax, cmap, filter, isolated_buffer, *args, **kwargs) + + def add_line_collection(self, data: List[numpy.ndarray], z_position: Union[List[float], float] = None, thickness: Union[float, List[float]] = 2.0, colors: Union[List[numpy.ndarray], numpy.ndarray] = 'w', alpha: float = 1.0, cmap: Union[List[str], str] = None, cmap_values: Union[numpy.ndarray, List] = None, name: str = None, metadata: Union[list, tuple, numpy.ndarray] = None, *args, **kwargs) -> LineCollection: + """ + + Create a collection of :class:`.LineGraphic` + + Parameters + ---------- + + data: list of array-like or array + List of line data to plot, each element must be a 1D, 2D, or 3D numpy array + if elements are 2D, interpreted as [y_vals, n_lines] + + z_position: list of float or float, optional + | if ``float``, single position will be used for all lines + | if ``list`` of ``float``, each value will apply to the individual lines + + thickness: float or list of float, default 2.0 + | if ``float``, single thickness will be used for all lines + | if ``list`` of ``float``, each value will apply to the individual lines + + colors: str, RGBA array, list of RGBA array, or list of str, default "w" + | if single ``str`` such as "w", "r", "b", etc, represents a single color for all lines + | if single ``RGBA array`` (tuple or list of size 4), represents a single color for all lines + | if ``list`` of ``str``, represents color for each individual line, example ["w", "b", "r",...] + | if ``RGBA array`` of shape [data_size, 4], represents a single RGBA array for each line + + cmap: list of str or str, optional + | if ``str``, single cmap will be used for all lines + | if ``list`` of ``str``, each cmap will apply to the individual lines + + .. note:: + ``cmap`` overrides any arguments passed to ``colors`` + + cmap_values: 1D array-like or list of numerical values, optional + if provided, these values are used to map the colors from the cmap + + name: str, optional + name of the line collection + + metadata: list, tuple, or array + metadata associated with this collection, this is for the user to manage. + ``len(metadata)`` must be same as ``len(data)`` + + args + passed to GraphicCollection + + kwargs + passed to GraphicCollection + + Features + -------- + + Collections support the same features as the underlying graphic. You just have to slice the selection. + + See :class:`LineGraphic` details on the features. + + + """ + return self._create_graphic(LineCollection, data, z_position, thickness, colors, alpha, cmap, cmap_values, name, metadata, *args, **kwargs) + + def add_line(self, data: Any, thickness: float = 2.0, colors: Union[str, numpy.ndarray, Iterable] = 'w', alpha: float = 1.0, cmap: str = None, cmap_values: Union[numpy.ndarray, List] = None, z_position: float = None, collection_index: int = None, *args, **kwargs) -> LineGraphic: + """ + + Create a line Graphic, 2d or 3d + + Parameters + ---------- + data: array-like + Line data to plot, 2D must be of shape [n_points, 2], 3D must be of shape [n_points, 3] + + thickness: float, optional, default 2.0 + thickness of the line + + colors: str, array, or iterable, default "w" + specify colors as a single human-readable string, a single RGBA array, + or an iterable of strings or RGBA arrays + + cmap: str, optional + apply a colormap to the line instead of assigning colors manually, this + overrides any argument passed to "colors" + + cmap_values: 1D array-like or list of numerical values, optional + if provided, these values are used to map the colors from the cmap + + alpha: float, optional, default 1.0 + alpha value for the colors + + z_position: float, optional + z-axis position for placing the graphic + + args + passed to Graphic + + kwargs + passed to Graphic + + Features + -------- + + **data**: :class:`.ImageDataFeature` + Manages the line [x, y, z] positions data buffer, allows regular and fancy indexing. + + **colors**: :class:`.ColorFeature` + Manages the color buffer, allows regular and fancy indexing. + + **cmap**: :class:`.CmapFeature` + Manages the cmap, wraps :class:`.ColorFeature` to add additional functionality relevant to cmaps. + + **thickness**: :class:`.ThicknessFeature` + Manages the thickness feature of the lines. + + **present**: :class:`.PresentFeature` + Control the presence of the Graphic in the scene, set to ``True`` or ``False`` + + + """ + return self._create_graphic(LineGraphic, data, thickness, colors, alpha, cmap, cmap_values, z_position, collection_index, *args, **kwargs) + + def add_line_stack(self, data: List[numpy.ndarray], z_position: Union[List[float], float] = None, thickness: Union[float, List[float]] = 2.0, colors: Union[List[numpy.ndarray], numpy.ndarray] = 'w', cmap: Union[List[str], str] = None, separation: float = 10, separation_axis: str = 'y', name: str = None, *args, **kwargs) -> LineStack: + """ + + Create a stack of :class:`.LineGraphic` that are separated along the "x" or "y" axis. + + Parameters + ---------- + data: list of array-like + List of line data to plot, each element must be a 1D, 2D, or 3D numpy array + if elements are 2D, interpreted as [y_vals, n_lines] + + z_position: list of float or float, optional + | if ``float``, single position will be used for all lines + | if ``list`` of ``float``, each value will apply to individual lines + + thickness: float or list of float, default 2.0 + | if ``float``, single thickness will be used for all lines + | if ``list`` of ``float``, each value will apply to the individual lines + + colors: str, RGBA array, list of RGBA array, or list of str, default "w" + | if single ``str`` such as "w", "r", "b", etc, represents a single color for all lines + | if single ``RGBA array`` (tuple or list of size 4), represents a single color for all lines + | is ``list`` of ``str``, represents color for each individual line, example ["w", "b", "r",...] + | if ``list`` of ``RGBA array`` of shape [data_size, 4], represents a single RGBA array for each line + + cmap: list of str or str, optional + | if ``str``, single cmap will be used for all lines + | if ``list`` of ``str``, each cmap will apply to the individual lines + + .. note:: + ``cmap`` overrides any arguments passed to ``colors`` + + name: str, optional + name of the line stack + + separation: float, default 10 + space in between each line graphic in the stack + + separation_axis: str, default "y" + axis in which the line graphics in the stack should be separated + + name: str, optional + name of the line stack + + args + passed to LineCollection + + kwargs + passed to LineCollection + + + Features + -------- + + Collections support the same features as the underlying graphic. You just have to slice the selection. + + See :class:`LineGraphic` details on the features. + + + """ + return self._create_graphic(LineStack, data, z_position, thickness, colors, cmap, separation, separation_axis, name, *args, **kwargs) + + def add_scatter(self, data: numpy.ndarray, sizes: Union[int, numpy.ndarray, list] = 1, colors: numpy.ndarray = 'w', alpha: float = 1.0, cmap: str = None, cmap_values: Union[numpy.ndarray, List] = None, z_position: float = 0.0, *args, **kwargs) -> ScatterGraphic: + """ + + Create a Scatter Graphic, 2d or 3d + + Parameters + ---------- + data: array-like + Scatter data to plot, 2D must be of shape [n_points, 2], 3D must be of shape [n_points, 3] + + sizes: float or iterable of float, optional, default 1.0 + size of the scatter points + + colors: str, array, or iterable, default "w" + specify colors as a single human readable string, a single RGBA array, + or an iterable of strings or RGBA arrays + + cmap: str, optional + apply a colormap to the scatter instead of assigning colors manually, this + overrides any argument passed to "colors" + + cmap_values: 1D array-like or list of numerical values, optional + if provided, these values are used to map the colors from the cmap + + alpha: float, optional, default 1.0 + alpha value for the colors + + z_position: float, optional + z-axis position for placing the graphic + + args + passed to Graphic + + kwargs + passed to Graphic + + Features + -------- + + **data**: :class:`.ImageDataFeature` + Manages the line [x, y, z] positions data buffer, allows regular and fancy indexing. + + **colors**: :class:`.ColorFeature` + Manages the color buffer, allows regular and fancy indexing. + + **cmap**: :class:`.CmapFeature` + Manages the cmap, wraps :class:`.ColorFeature` to add additional functionality relevant to cmaps. + + **present**: :class:`.PresentFeature` + Control the presence of the Graphic in the scene, set to ``True`` or ``False`` + + + """ + return self._create_graphic(ScatterGraphic, data, sizes, colors, alpha, cmap, cmap_values, z_position, *args, **kwargs) + + def add_text(self, text: str, position: Tuple[int] = (0, 0, 0), size: int = 10, face_color: Union[str, numpy.ndarray] = 'w', outline_color: Union[str, numpy.ndarray] = 'w', outline_thickness=0, name: str = None) -> TextGraphic: + """ + + Create a text Graphic + + Parameters + ---------- + text: str + display text + + position: int tuple, default (0, 0, 0) + int tuple indicating location of text in scene + + size: int, default 10 + text size + + face_color: str or array, default "w" + str or RGBA array to set the color of the text + + outline_color: str or array, default "w" + str or RGBA array to set the outline color of the text + + outline_thickness: int, default 0 + text outline thickness + + name: str, optional + name of graphic, passed to Graphic + + + """ + return self._create_graphic(TextGraphic, text, position, size, face_color, outline_color, outline_thickness, name, *args, **kwargs) + diff --git a/fastplotlib/utils/functions.py b/fastplotlib/utils/functions.py index ce6740f71..3013559d5 100644 --- a/fastplotlib/utils/functions.py +++ b/fastplotlib/utils/functions.py @@ -1,30 +1,44 @@ -from typing import Union, List - -import numpy as np -from pygfx import Texture, Color from collections import OrderedDict from typing import * from pathlib import Path + +import numpy as np + +from pygfx import Texture, Color + # some funcs adapted from mesmerize -QUALITATIVE_CMAPS = ['Pastel1', 'Pastel2', 'Paired', 'Accent', 'Dark2', 'Set1', - 'Set2', 'Set3', 'tab10', 'tab20', 'tab20b', 'tab20c'] +QUALITATIVE_CMAPS = [ + "Pastel1", + "Pastel2", + "Paired", + "Accent", + "Dark2", + "Set1", + "Set2", + "Set3", + "tab10", + "tab20", + "tab20b", + "tab20c", +] def get_cmap(name: str, alpha: float = 1.0) -> np.ndarray: - cmap_path = Path(__file__).absolute().parent.joinpath('colormaps', name) + cmap_path = Path(__file__).absolute().parent.joinpath("colormaps", name) if cmap_path.is_file(): cmap = np.loadtxt(cmap_path) else: try: from .generate_colormaps import make_cmap + cmap = make_cmap(name, alpha) - except ModuleNotFoundError as e: + except (ImportError, ModuleNotFoundError): raise ModuleNotFoundError( "Couldn't find colormap files, matplotlib is required to generate them " - "if they aren't found. Please install `matplotlib`." + "if they aren't found. Please install `matplotlib`" ) cmap[:, -1] = alpha @@ -60,8 +74,10 @@ def make_colors(n_colors: int, cmap: str, alpha: float = 1.0) -> np.ndarray: if name in QUALITATIVE_CMAPS: max_colors = cmap.shape[0] if n_colors > cmap.shape[0]: - raise ValueError(f"You have requested <{n_colors}> but only <{max_colors} existing for the " - f"chosen cmap: <{cmap}>") + raise ValueError( + f"You have requested <{n_colors}> but only <{max_colors} existing for the " + f"chosen cmap: <{cmap}>" + ) return cmap[:n_colors] cm_ixs = np.linspace(0, 255, n_colors, dtype=int) @@ -133,13 +149,25 @@ def make_colors_dict(labels: iter, cmap: str, **kwargs) -> OrderedDict: def quick_min_max(data: np.ndarray) -> Tuple[float, float]: - # adapted from pyqtgraph.ImageView - # Estimate the min/max values of *data* by subsampling. - # Returns [(min, max), ...] with one item per channel + """ + Adapted from pyqtgraph.ImageView. + Estimate the min/max values of *data* by subsampling. + + Parameters + ---------- + data: np.ndarray or array-like with `min` and `max` attributes + + Returns + ------- + (float, float) + (min, max) + """ if hasattr(data, "min") and hasattr(data, "max"): # if value is pre-computed - if isinstance(data.min, (float, int, np.number)) and isinstance(data.max, (float, int, np.number)): + if isinstance(data.min, (float, int, np.number)) and isinstance( + data.max, (float, int, np.number) + ): return data.min, data.max while data.size > 1e6: @@ -152,9 +180,26 @@ def quick_min_max(data: np.ndarray) -> Tuple[float, float]: def make_pygfx_colors(colors, n_colors): - """parse and make colors array using pyfx.Color""" + """ + Parse and make colors array using pyfx.Color + + Parameters + ---------- + colors: str, list, tuple, or np.ndarray + pygfx parseable color + + n_colors: int + number of repeats of the color + + Returns + ------- + np.ndarray + shape is [n_colors, 4], i.e. [n_colors, RGBA] + """ + c = Color(colors) colors_array = np.repeat(np.array([c]), n_colors, axis=0) + return colors_array @@ -164,10 +209,7 @@ def calculate_gridshape(n_subplots: int) -> Tuple[int, int]: """ sr = np.sqrt(n_subplots) - return ( - int(np.round(sr)), - int(np.ceil(sr)) - ) + return (int(np.round(sr)), int(np.ceil(sr))) def normalize_min_max(a): @@ -176,9 +218,9 @@ def normalize_min_max(a): def parse_cmap_values( - n_colors: int, - cmap_name: str, - cmap_values: Union[np.ndarray, List[Union[int, float]]] = None + n_colors: int, + cmap_name: str, + cmap_values: Union[np.ndarray, List[Union[int, float]]] = None, ) -> np.ndarray: """ diff --git a/fastplotlib/utils/generate_add_methods.py b/fastplotlib/utils/generate_add_methods.py new file mode 100644 index 000000000..3fe16260c --- /dev/null +++ b/fastplotlib/utils/generate_add_methods.py @@ -0,0 +1,73 @@ +import inspect +import pathlib + +# if there is an existing mixin class, replace it with an empty class +# so that fastplotlib will import +# hacky but it works +current_module = pathlib.Path(__file__).parent.parent.resolve() +with open(current_module.joinpath('layouts/graphic_methods_mixin.py'), 'w') as f: + f.write( + f"class GraphicMethodsMixin:\n" + f" pass" + ) + +from fastplotlib import graphics + + +modules = list() + +for name, obj in inspect.getmembers(graphics): + if inspect.isclass(obj): + modules.append(obj) + + +def generate_add_graphics_methods(): + # clear file and regenerate from scratch + + f = open(current_module.joinpath('layouts/graphic_methods_mixin.py'), 'w') + + f.write('# This is an auto-generated file and should not be modified directly\n\n') + + f.write('from typing import *\n\n') + f.write('import numpy\n') + f.write('import weakref\n\n') + f.write('from ..graphics import *\n') + f.write('from ..graphics._base import Graphic\n\n') + + f.write("\nclass GraphicMethodsMixin:\n") + f.write(" def __init__(self):\n") + f.write(" pass\n\n") + + f.write(" def _create_graphic(self, graphic_class, *args, **kwargs) -> Graphic:\n") + f.write(" if 'center' in kwargs.keys():\n") + f.write(" center = kwargs.pop('center')\n") + f.write(" else:\n") + f.write(" center = False\n\n") + f.write(" if 'name' in kwargs.keys():\n") + f.write(" self._check_graphic_name_exists(kwargs['name'])\n\n") + f.write(" graphic = graphic_class(*args, **kwargs)\n") + f.write(" self.add_graphic(graphic, center=center)\n\n") + f.write(" # only return a proxy to the real graphic\n") + f.write(" return weakref.proxy(graphic)\n\n") + + for m in modules: + class_name = m + method_name = class_name.type + + class_args = inspect.getfullargspec(class_name)[0][1:] + class_args = [arg + ', ' for arg in class_args] + s = "" + for a in class_args: + s += a + + f.write(f" def add_{method_name}{inspect.signature(class_name.__init__)} -> {class_name.__name__}:\n") + f.write(' """\n') + f.write(f' {class_name.__init__.__doc__}\n') + f.write(' """\n') + f.write(f" return self._create_graphic({class_name.__name__}, {s}*args, **kwargs)\n\n") + + f.close() + + +if __name__ == '__main__': + generate_add_graphics_methods() diff --git a/fastplotlib/utils/generate_colormaps.py b/fastplotlib/utils/generate_colormaps.py index 029cce2b0..e56a9f226 100644 --- a/fastplotlib/utils/generate_colormaps.py +++ b/fastplotlib/utils/generate_colormaps.py @@ -3,31 +3,108 @@ class ColormapNames: - perceptually_uniform = ['viridis', 'plasma', 'inferno', 'magma', 'cividis'] - sequential = ['Greys', 'Purples', 'Blues', 'Greens', 'Oranges', 'Reds', - 'YlOrBr', 'YlOrRd', 'OrRd', 'PuRd', 'RdPu', 'BuPu', - 'GnBu', 'PuBu', 'YlGnBu', 'PuBuGn', 'BuGn', 'YlGn'] + perceptually_uniform = ["viridis", "plasma", "inferno", "magma", "cividis"] + sequential = [ + "Greys", + "Purples", + "Blues", + "Greens", + "Oranges", + "Reds", + "YlOrBr", + "YlOrRd", + "OrRd", + "PuRd", + "RdPu", + "BuPu", + "GnBu", + "PuBu", + "YlGnBu", + "PuBuGn", + "BuGn", + "YlGn", + ] - sequential2 = ['binary', 'gist_yarg', 'gist_gray', 'gray', 'bone', - 'pink', 'spring', 'summer', 'autumn', 'winter', 'cool', - 'Wistia', 'hot', 'afmhot', 'gist_heat', 'copper'] + sequential2 = [ + "binary", + "gist_yarg", + "gist_gray", + "gray", + "bone", + "pink", + "spring", + "summer", + "autumn", + "winter", + "cool", + "Wistia", + "hot", + "afmhot", + "gist_heat", + "copper", + ] - diverging = ['PiYG', 'PRGn', 'BrBG', 'PuOr', 'RdGy', 'RdBu', 'RdYlBu', - 'RdYlGn', 'Spectral', 'coolwarm', 'bwr', 'seismic'] + diverging = [ + "PiYG", + "PRGn", + "BrBG", + "PuOr", + "RdGy", + "RdBu", + "RdYlBu", + "RdYlGn", + "Spectral", + "coolwarm", + "bwr", + "seismic", + ] - cyclic = ['twilight', 'twilight_shifted', 'hsv'] + cyclic = ["twilight", "twilight_shifted", "hsv"] - qualitative = ['Pastel1', 'Pastel2', 'Paired', 'Accent', 'Dark2', - 'Set1', 'Set2', 'Set3', 'tab10', 'tab20', 'tab20b', - 'tab20c'] + qualitative = [ + "Pastel1", + "Pastel2", + "Paired", + "Accent", + "Dark2", + "Set1", + "Set2", + "Set3", + "tab10", + "tab20", + "tab20b", + "tab20c", + ] - miscellaneous = ['flag', 'prism', 'ocean', 'gist_earth', 'terrain', - 'gist_stern', 'gnuplot', 'gnuplot2', 'CMRmap', - 'cubehelix', 'brg', 'gist_rainbow', 'rainbow', 'jet', - 'turbo', 'nipy_spectral', 'gist_ncar'] + miscellaneous = [ + "flag", + "prism", + "ocean", + "gist_earth", + "terrain", + "gist_stern", + "gnuplot", + "gnuplot2", + "CMRmap", + "cubehelix", + "brg", + "gist_rainbow", + "rainbow", + "jet", + "turbo", + "nipy_spectral", + "gist_ncar", + ] - all = perceptually_uniform + sequential + sequential2 + \ - diverging + cyclic + qualitative + miscellaneous + all = ( + perceptually_uniform + + sequential + + sequential2 + + diverging + + cyclic + + qualitative + + miscellaneous + ) def make_cmap(name: str, alpha: float = 1.0) -> np.ndarray: @@ -44,6 +121,6 @@ def make_cmap(name: str, alpha: float = 1.0) -> np.ndarray: return cmap.astype(np.float32) -if __name__ == '__main__': +if __name__ == "__main__": for name in ColormapNames().all: - np.savetxt(f'./colormaps/{name}', make_cmap(name)) + np.savetxt(f"./colormaps/{name}", make_cmap(name)) diff --git a/fastplotlib/widgets/__init__.py b/fastplotlib/widgets/__init__.py index 553e990bf..30a68d672 100644 --- a/fastplotlib/widgets/__init__.py +++ b/fastplotlib/widgets/__init__.py @@ -1 +1,3 @@ from .image import ImageWidget + +__all__ = ["ImageWidget"] diff --git a/fastplotlib/widgets/image.py b/fastplotlib/widgets/image.py index e57bae216..962a94151 100644 --- a/fastplotlib/widgets/image.py +++ b/fastplotlib/widgets/image.py @@ -1,3 +1,4 @@ +import weakref from typing import * from warnings import warn from functools import partial @@ -5,20 +6,29 @@ import numpy as np from wgpu.gui.auto import is_jupyter -from ipywidgets.widgets import IntSlider, VBox, HBox, Layout, FloatRangeSlider, Button, BoundedIntText, Play, jslink +from ipywidgets.widgets import ( + IntSlider, + VBox, + HBox, + Layout, + FloatRangeSlider, + Button, + BoundedIntText, + Play, + jslink, +) from ..layouts import GridPlot from ..graphics import ImageGraphic from ..utils import quick_min_max, calculate_gridshape -DEFAULT_DIMS_ORDER = \ - { - 2: "xy", - 3: "txy", - 4: "tzxy", - 5: "tzcxy", - } +DEFAULT_DIMS_ORDER = { + 2: "xy", + 3: "txy", + 4: "tzxy", + 5: "tzcxy", +} def _is_arraylike(obj) -> bool: @@ -26,11 +36,7 @@ def _is_arraylike(obj) -> bool: Checks if the object is array-like. For now just checks if obj has `__getitem__()` """ - for attr in [ - "__getitem__", - "shape", - "ndim" - ]: + for attr in ["__getitem__", "shape", "ndim"]: if not hasattr(obj, attr): return False @@ -38,14 +44,26 @@ def _is_arraylike(obj) -> bool: class _WindowFunctions: + """Stores window function and window size""" def __init__(self, func: callable, window_size: int): + self._func = None self.func = func self._window_size = 0 self.window_size = window_size + @property + def func(self) -> callable: + """Get or set the function""" + return self._func + + @func.setter + def func(self, func: callable): + self._func = func + @property def window_size(self) -> int: + """Get or set window size""" return self._window_size @window_size.setter @@ -84,11 +102,13 @@ def gridplot(self) -> GridPlot: return self._gridplot @property - def managed_graphics(self): + def managed_graphics(self) -> List[ImageGraphic]: """List of ``ImageWidget`` managed graphics.""" iw_managed = list() for subplot in self.gridplot: - iw_managed.append(subplot["image_widget_managed"]) + # empty subplots will not have any image widget data + if len(subplot.graphics) > 0: + iw_managed.append(subplot["image_widget_managed"]) return iw_managed @property @@ -118,22 +138,23 @@ def slider_dims(self) -> List[str]: @property def current_index(self) -> Dict[str, int]: - return self._current_index - - @current_index.setter - def current_index(self, index: Dict[str, int]): """ - Set the current index + Get or set the current index - Parameters - ---------- + Returns + ------- index: Dict[str, int] | ``dict`` for indexing each dimension, provide a ``dict`` with indices for all dimensions used by sliders or only a subset of dimensions used by the sliders. | example: if you have sliders for dims "t" and "z", you can pass either ``{"t": 10}`` to index to position 10 on dimension "t" or ``{"t": 5, "z": 20}`` to index to position 5 on dimension "t" and position 20 on dimension "z" simultaneously. + """ + return self._current_index + + @current_index.setter + def current_index(self, index: Dict[str, int]): 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. " @@ -146,8 +167,10 @@ def current_index(self, index: Dict[str, 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]}") + 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) @@ -163,17 +186,17 @@ def current_index(self, index: Dict[str, int]): ig.data = frame def __init__( - self, - data: Union[np.ndarray, List[np.ndarray]], - dims_order: Union[str, Dict[int, str]] = None, - slider_dims: Union[str, int, List[Union[str, int]]] = None, - window_funcs: Union[int, Dict[str, int]] = None, - frame_apply: Union[callable, Dict[int, callable]] = None, - vmin_vmax_sliders: bool = False, - grid_shape: Tuple[int, int] = None, - names: List[str] = None, - grid_plot_kwargs: dict = None, - **kwargs + self, + data: Union[np.ndarray, List[np.ndarray]], + dims_order: Union[str, Dict[int, str]] = None, + slider_dims: Union[str, int, List[Union[str, int]]] = None, + window_funcs: Union[int, Dict[str, int]] = None, + frame_apply: Union[callable, Dict[int, callable]] = None, + vmin_vmax_sliders: bool = False, + grid_shape: Tuple[int, int] = None, + names: List[str] = None, + grid_plot_kwargs: dict = None, + **kwargs, ): """ A high level widget for displaying n-dimensional image data in conjunction with automatically generated @@ -238,7 +261,9 @@ def __init__( kwargs: Any passed to fastplotlib.graphics.Image + """ + if not is_jupyter(): raise EnvironmentError( "ImageWidget is currently not supported outside of jupyter" @@ -256,7 +281,9 @@ def __init__( # verify that user-specified grid shape is large enough for the number of image arrays passed elif grid_shape[0] * grid_shape[1] < len(data): grid_shape = calculate_gridshape(len(data)) - warn(f"Invalid `grid_shape` passed, setting grid shape to: {grid_shape}") + warn( + f"Invalid `grid_shape` passed, setting grid shape to: {grid_shape}" + ) _ndim = [d.ndim for d in data] @@ -272,7 +299,9 @@ def __init__( if names is not None: if not all([isinstance(n, str) for n in names]): - raise TypeError("optinal argument `names` must be a list of str") + raise TypeError( + "optinal argument `names` must be a list of str" + ) if len(names) != len(self.data): raise ValueError( @@ -314,7 +343,9 @@ def __init__( ) self._dims_order: List[str] = [dims_order] * len(self.data) elif isinstance(dims_order, dict): - self._dims_order: List[str] = [DEFAULT_DIMS_ORDER[self.ndim]] * len(self.data) + self._dims_order: List[str] = [DEFAULT_DIMS_ORDER[self.ndim]] * len( + self.data + ) # dict of {array_ix: dims_order_str} for data_ix in list(dims_order.keys()): @@ -342,7 +373,8 @@ def __init__( ) else: raise TypeError( - f"`dims_order` must be a or , you have passed a: <{type(dims_order)}>") + f"`dims_order` must be a or , you have passed a: <{type(dims_order)}>" + ) if not len(self.dims_order[0]) == self.ndim: raise ValueError( @@ -431,7 +463,9 @@ def __init__( ) else: - raise TypeError(f"`slider_dims` must a , or , you have passed a: {type(slider_dims)}") + raise TypeError( + f"`slider_dims` must a , or , you have passed a: {type(slider_dims)}" + ) self.frame_apply: Dict[int, callable] = dict() @@ -440,7 +474,9 @@ def __init__( self.frame_apply = {0: frame_apply} elif isinstance(frame_apply, dict): - self.frame_apply: Dict[int, callable] = dict.fromkeys(list(range(len(self.data)))) + self.frame_apply: Dict[int, callable] = dict.fromkeys( + list(range(len(self.data))) + ) # dict of {array: dims_order_str} for data_ix in list(frame_apply.keys()): @@ -455,14 +491,13 @@ def __init__( else: raise TypeError( f"`frame_apply` must be a callable or , " - f"you have passed a: <{type(frame_apply)}>") + f"you have passed a: <{type(frame_apply)}>" + ) self._window_funcs = None self.window_funcs = window_funcs self._sliders: Dict[str, IntSlider] = dict() - self._vertical_sliders = list() - self._horizontal_sliders = list() # current_index stores {dimension_index: slice_index} for every dimension self._current_index: Dict[str, int] = {sax: 0 for sax in self.slider_dims} @@ -473,7 +508,9 @@ def __init__( self._dims_max_bounds: Dict[str, int] = {k: np.inf for k in self.slider_dims} for _dim in list(self._dims_max_bounds.keys()): for array, order in zip(self.data, self.dims_order): - self._dims_max_bounds[_dim] = min(self._dims_max_bounds[_dim], array.shape[order.index(_dim)]) + self._dims_max_bounds[_dim] = min( + self._dims_max_bounds[_dim], array.shape[order.index(_dim)] + ) if grid_plot_kwargs is None: grid_plot_kwargs = {"controllers": "sync"} @@ -501,12 +538,11 @@ def __init__( step=data_range / 150, description=f"mm: {name_slider}", readout=True, - readout_format='.3f', + readout_format=".3f", ) minmax_slider.observe( - partial(self._vmin_vmax_slider_changed, data_ix), - names="value" + partial(self._vmin_vmax_slider_changed, data_ix), names="value" ) self.vmin_vmax_sliders.append(minmax_slider) @@ -521,61 +557,38 @@ def __init__( self.gridplot.renderer.add_event_handler(self._set_slider_layout, "resize") for sdm in self.slider_dims: - if sdm == "z": - # TODO: once ipywidgets plays nicely with HBox and jupyter-rfb, use vertical - # orientation = "vertical" - orientation = "horizontal" - else: - orientation = "horizontal" - slider = IntSlider( min=0, max=self._dims_max_bounds[sdm] - 1, step=1, value=0, description=f"dimension: {sdm}", - orientation=orientation + orientation="horizontal", ) - slider.observe( - partial(self._slider_value_changed, sdm), - names="value" - ) + slider.observe(partial(self._slider_value_changed, sdm), names="value") self._sliders[sdm] = slider - if orientation == "horizontal": - self._horizontal_sliders.append(slider) - elif orientation == "vertical": - self._vertical_sliders.append(slider) # will change later # prevent the slider callback if value is self.current_index is changed programmatically self.block_sliders: bool = False # TODO: So just stack everything vertically for now - self._vbox_sliders = VBox([ - *list(self._sliders.values()), - *self.vmin_vmax_sliders - ]) - - # TODO: there is currently an issue with ipywidgets or jupyter-rfb and HBox doesn't work with RFB canvas - # self.widget = None - # hbox = None - # if len(self.vertical_sliders) > 0: - # hbox = HBox(self.vertical_sliders) - # - # if len(self.horizontal_sliders) > 0: - # if hbox is not None: - # self.widget = VBox([ - # HBox([self.plot.canvas, hbox]), - # *self.horizontal_sliders, - # ]) - # - # else: - # self.widget = VBox([self.plot.canvas, *self.horizontal_sliders]) + self._vbox_sliders = VBox( + [*list(self._sliders.values()), *self.vmin_vmax_sliders] + ) @property def window_funcs(self) -> Dict[str, _WindowFunctions]: + """ + Get or set the window functions + + Returns + ------- + Dict[str, _WindowFunctions] + + """ return self._window_funcs @window_funcs.setter @@ -601,7 +614,9 @@ def window_funcs(self, sa: Union[int, Dict[str, int]]): # for multiple dims elif isinstance(sa, dict): - if not all([isinstance(_sa, tuple) or (_sa is None) for _sa in sa.values()]): + if not all( + [isinstance(_sa, tuple) or (_sa is None) for _sa in sa.values()] + ): raise TypeError( "dict argument to `window_funcs` must be in the form of: " "`{dimension: (func, window_size)}`. " @@ -609,7 +624,9 @@ def window_funcs(self, sa: Union[int, Dict[str, int]]): ) for v in sa.values(): if v is not None: - if not callable(v[0]) or not (isinstance(v[1], int) or v[1] is None): + if not callable(v[0]) or not ( + isinstance(v[1], int) or v[1] is None + ): raise TypeError( "dict argument to `window_funcs` must be in the form of: " "`{dimension: (func, window_size)}`. " @@ -632,9 +649,7 @@ def window_funcs(self, sa: Union[int, Dict[str, int]]): ) def _process_indices( - self, - array: np.ndarray, - slice_indices: Dict[Union[int, str], int] + self, array: np.ndarray, slice_indices: Dict[Union[int, str], int] ) -> np.ndarray: """ Get the 2D array from the given slice indices. If not returning a 2D slice (such as due to window_funcs) @@ -668,9 +683,7 @@ def _process_indices( data_ix = i break if data_ix is None: - raise ValueError( - f"Given `array` not found in `self.data`" - ) + raise ValueError(f"Given `array` not found in `self.data`") # get axes order for that specific array numerical_dim = self.dims_order[data_ix].index(dim) else: @@ -735,7 +748,9 @@ def _get_window_indices(self, data_ix, dim, indices_dim): half_window = int((window_size - 1) / 2) # half-window size # get the max bound for that dimension max_bound = self._dims_max_bounds[dim_str] - indices_dim = range(max(0, ix - half_window), min(max_bound, ix + half_window)) + indices_dim = range( + max(0, ix - half_window), min(max_bound, ix + half_window) + ) return indices_dim def _process_frame_apply(self, array, data_ix) -> np.ndarray: @@ -750,31 +765,20 @@ def _process_frame_apply(self, array, data_ix) -> np.ndarray: return array - def _slider_value_changed( - self, - dimension: str, - change: dict - ): + def _slider_value_changed(self, dimension: str, change: dict): if self.block_sliders: return self.current_index = {dimension: change["new"]} - def _vmin_vmax_slider_changed( - self, - data_ix: int, - change: dict - ): + def _vmin_vmax_slider_changed(self, data_ix: int, change: dict): vmin, vmax = change["new"] self.managed_graphics[data_ix].cmap.vmin = vmin self.managed_graphics[data_ix].cmap.vmax = vmax def _set_slider_layout(self, *args): w, h = self.gridplot.renderer.logical_size - for hs in self._horizontal_sliders: - hs.layout = Layout(width=f"{w}px") - - for vs in self._vertical_sliders: - vs.layout = Layout(height=f"{h}px") + for k, v in self.sliders.items(): + v.layout = Layout(width=f"{w}px") for mm in self.vmin_vmax_sliders: mm.layout = Layout(width=f"{w}px") @@ -800,7 +804,7 @@ def _get_vmin_vmax_range(self, data: np.ndarray) -> tuple: minmax, data_range, minmax[0] - data_range_40p, - minmax[1] + data_range_40p + minmax[1] + data_range_40p, ) return _range @@ -817,7 +821,7 @@ def reset_vmin_vmax(self): "value": mm[0], "step": mm[1] / 150, "min": mm[2], - "max": mm[3] + "max": mm[3], } self.vmin_vmax_sliders[i].set_state(state) @@ -825,10 +829,10 @@ def reset_vmin_vmax(self): ig.cmap.vmin, ig.cmap.vmax = mm[0] def set_data( - self, - new_data: Union[np.ndarray, List[np.ndarray]], - reset_vmin_vmax: bool = True, - reset_indices: bool = True + self, + new_data: Union[np.ndarray, List[np.ndarray]], + reset_vmin_vmax: bool = True, + reset_indices: bool = True, ): """ Change data of widget. Note: sliders max currently update only for ``txy`` and ``tzxy`` data. @@ -872,7 +876,9 @@ def set_data( ) # if checks pass, update with new data - for i, (new_array, current_array, subplot) in enumerate(zip(new_data, self._data, self.gridplot)): + for i, (new_array, current_array, subplot) in enumerate( + zip(new_data, self._data, self.gridplot) + ): # check last two dims (x and y) to see if data shape is changing old_data_shape = self._data[i].shape[-2:] self._data[i] = new_array @@ -881,7 +887,9 @@ def set_data( # delete graphics at index zero subplot.delete_graphic(graphic=subplot["image_widget_managed"]) # insert new graphic at index zero - frame = self._process_indices(new_array, slice_indices=self._current_index) + frame = self._process_indices( + new_array, slice_indices=self._current_index + ) frame = self._process_frame_apply(frame, i) new_graphic = ImageGraphic(data=frame, name="image_widget_managed") subplot.insert_graphic(graphic=new_graphic) @@ -932,8 +940,7 @@ def show(self, toolbar: bool = True): class ImageWidgetToolbar: - def __init__(self, - iw: ImageWidget): + def __init__(self, iw: ImageWidget): """ Basic toolbar for a ImageWidget instance. @@ -944,30 +951,67 @@ def __init__(self, self.iw = iw self.plot = iw.gridplot - self.reset_vminvmax_button = Button(value=False, disabled=False, icon='adjust', - layout=Layout(width='auto'), tooltip='reset vmin/vmax') + self.reset_vminvmax_button = Button( + value=False, + disabled=False, + icon="adjust", + layout=Layout(width="auto"), + tooltip="reset vmin/vmax", + ) + + # only for xy data, no time point slider needed + if self.iw.ndim == 2: + self.widget = HBox([self.reset_vminvmax_button]) + # for txy, tzxy, etc. data + else: + self.step_size_setter = BoundedIntText( + value=1, + min=1, + max=self.iw.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=iw.sliders["t"].min, + max=iw.sliders["t"].max, + step=iw.sliders["t"].step, + description="play/pause", + disabled=False, + ) + self.widget = HBox( + [self.reset_vminvmax_button, self.play_button, self.step_size_setter, self.speed_text] + ) - self.step_size_setter = BoundedIntText(value=1, min=1, max=self.iw.sliders['t'].max, step=1, - description='Step Size:', disabled=False, - description_tooltip='set slider step', layout=Layout(width='150px')) - self.play_button = Play( - value=0, - min=iw.sliders["t"].min, - max=iw.sliders["t"].max, - step=iw.sliders["t"].step, - description="play/pause", - disabled=False) + self.play_button.interval = 10 - self.widget = HBox([self.reset_vminvmax_button, self.play_button, self.step_size_setter]) + self.step_size_setter.observe(self._change_stepsize, "value") + self.speed_text.observe(self._change_framerate, "value") + jslink((self.play_button, "value"), (self.iw.sliders["t"], "value")) + jslink((self.play_button, "max"), (self.iw.sliders["t"], "max")) - self.reset_vminvmax_button.on_click(self.reset_vminvmax) - self.step_size_setter.observe(self.change_stepsize, 'value') - jslink((self.play_button, 'value'), (self.iw.sliders["t"], 'value')) - jslink((self.play_button, "max"), (self.iw.sliders["t"], "max")) + self.reset_vminvmax_button.on_click(self._reset_vminvmax) - def reset_vminvmax(self, obj): + def _reset_vminvmax(self, obj): if len(self.iw.vmin_vmax_sliders) != 0: self.iw.reset_vmin_vmax() - def change_stepsize(self, obj): - self.iw.sliders['t'].step = self.step_size_setter.value \ No newline at end of file + def _change_stepsize(self, obj): + self.iw.sliders["t"].step = self.step_size_setter.value + + def _change_framerate(self, change): + interval = int(1000 / change["new"]) + self.play_button.interval = interval diff --git a/notebooks/scatter.ipynb b/notebooks/scatter.ipynb deleted file mode 100644 index 094204b63..000000000 --- a/notebooks/scatter.ipynb +++ /dev/null @@ -1,229 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "id": "eb204b20-160a-48ef-8ac6-54d263e497e4", - "metadata": { - "tags": [] - }, - "source": [ - "# Scatter plots in a `GridPlot` layout with a mix of 2d an 3d cameras" - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "id": "9b3041ad-d94e-4b2a-af4d-63bcd19bf6c2", - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "import numpy as np\n", - "from fastplotlib import GridPlot" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "id": "51f1d76a-f815-460f-a884-097fe3ea81ac", - "metadata": {}, - "outputs": [], - "source": [ - "# create a random distribution of 10,000 xyz coordinates\n", - "n_points = 10_000\n", - "\n", - "# if you have a good GPU go for 1.2 million points :D \n", - "# this is multiplied by 3\n", - "n_points = 400_000\n", - "dims = (n_points, 3)\n", - "\n", - "offset = 15\n", - "\n", - "normal = np.random.normal(size=dims, scale=5)\n", - "cloud = np.vstack(\n", - " [\n", - " normal - offset,\n", - " normal,\n", - " normal + offset,\n", - " ]\n", - ")\n", - "\n", - "colors = [\"yellow\"] * n_points + [\"cyan\"] * n_points + [\"magenta\"] * n_points" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "id": "922990b6-24e9-4fa0-977b-6577f9752d84", - "metadata": {}, - "outputs": [ - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "0d49371132174eb4a9501964b4584d67", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "RFBOutputContext()" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/home/kushalk/repos/fastplotlib/fastplotlib/layouts/_base.py:214: UserWarning: `center_scene()` not yet implemented for `PerspectiveCamera`\n", - " warn(\"`center_scene()` not yet implemented for `PerspectiveCamera`\")\n" - ] - }, - { - "data": { - "text/html": [ - "
initial snapshot
" - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "a94246496c054599bc44a0a77ea7d58e", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "JupyterWgpuCanvas()" - ] - }, - "execution_count": 3, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# grid with 2 rows and 2 columns\n", - "shape = (2, 2)\n", - "\n", - "# define the camera\n", - "# a mix of 2d and 3d\n", - "cameras = [\n", - " ['2d', '3d'], \n", - " ['3d', '2d']\n", - "]\n", - "\n", - "# pan-zoom controllers for each view\n", - "# views are synced if they have the \n", - "# same controller ID\n", - "# you can only sync controllers that use the same camera type\n", - "# i.e. you cannot sync between 2d and 3d subplots\n", - "controllers = [\n", - " [0, 1],\n", - " [1, 0]\n", - "]\n", - "\n", - "# create the grid plot\n", - "grid_plot = GridPlot(\n", - " shape=shape,\n", - " cameras=cameras,\n", - " controllers=controllers\n", - ")\n", - "\n", - "for subplot in grid_plot:\n", - " subplot.add_scatter(data=cloud, colors=colors, alpha=0.7, sizes=5)\n", - " \n", - " subplot.set_axes_visibility(True)\n", - " subplot.set_grid_visibility(True)\n", - "\n", - "\n", - "grid_plot.show()" - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "id": "7b912961-f72e-46ef-889f-c03234831059", - "metadata": {}, - "outputs": [], - "source": [ - "grid_plot[0, 1].graphics[0].colors[400_000:600_000] = \"r\"" - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "id": "c6085806-c001-4632-ab79-420b4692693a", - "metadata": {}, - "outputs": [], - "source": [ - "grid_plot[0, 1].graphics[0].colors[:100_000:10] = \"blue\"" - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "id": "6f416825-df31-4e5d-b66b-07f23b48e7db", - "metadata": {}, - "outputs": [], - "source": [ - "grid_plot[0, 1].graphics[0].colors[800_000:] = \"green\"" - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "id": "c0fd611e-73e5-49e6-a25c-9d5b64afa5f4", - "metadata": {}, - "outputs": [], - "source": [ - "grid_plot[0, 1].graphics[0].colors[800_000:, -1] = 0" - ] - }, - { - "cell_type": "code", - "execution_count": 9, - "id": "cd390542-3a44-4973-8172-89e5583433bc", - "metadata": {}, - "outputs": [], - "source": [ - "grid_plot[0, 1].graphics[0].data[:400_000] = grid_plot[0, 1].graphics[0].data[800_000:]" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "fb49930f-b795-4b41-bbc6-014a27c2f463", - "metadata": {}, - "outputs": [], - "source": [] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3 (ipykernel)", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.11.3" - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} diff --git a/setup.py b/setup.py index 1f4e5cb3a..2616093fc 100644 --- a/setup.py +++ b/setup.py @@ -3,23 +3,30 @@ install_requires = [ - 'numpy', - 'pygfx>=0.1.13', + "numpy>=1.23.0", + "pygfx>=0.1.13", ] extras_require = { "docs": [ "sphinx", - "pydata-sphinx-theme<0.10.0", + "furo", "glfw", - "jupyter_rfb" # required so ImageWidget docs show up + "jupyter-rfb>=0.4.1", # required so ImageWidget docs show up + "ipywidgets>=8.0.0,<9", + "sphinx-copybutton", + "sphinx-design", + "nbsphinx", + "pandoc", + "jupyterlab" ], "notebook": [ - 'jupyterlab', - 'jupyter-rfb', + "jupyterlab", + "jupyter-rfb>=0.4.1", + "ipywidgets>=8.0.0,<9" ], "tests": @@ -29,8 +36,10 @@ "scipy", "imageio", "jupyterlab", - "jupyter-rfb", + "jupyter-rfb>=0.4.1", + "ipywidgets>=8.0.0,<9", "scikit-learn", + "tqdm" ] } pFad - Phonifier reborn

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

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


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy