From 96217404837f078f643180691fba3e7a5af35532 Mon Sep 17 00:00:00 2001 From: Talley Lambert Date: Mon, 4 Dec 2023 19:32:58 -0500 Subject: [PATCH 01/66] update contrib (#391) --- .gitignore | 1 + CONTRIBUTING.md | 9 ++++++--- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/.gitignore b/.gitignore index f87eb1c51..9857fe4b2 100644 --- a/.gitignore +++ b/.gitignore @@ -131,3 +131,4 @@ dmypy.json # Pycharm .idea/ +examples/desktop/diffs/*.png diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 7bf5c69ea..23810b956 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -8,7 +8,7 @@ Contributions are welcome! :smile: ![image](https://github.com/kushalkolar/fastplotlib/assets/9403332/82612021-37b2-48dd-b7e4-01a919535c17) -2. Clone the repo and install according to the development instructions. Replace the `YOUR_ACCOUNT` in the repo URL to the fork on your account: +2. Clone the repo and install according to the development instructions. Replace the `YOUR_ACCOUNT` in the repo URL to the fork on your account. Note that fastplotlib uses [git-lfs](https://git-lfs.com) for storing large files, so you will need to [install it](https://github.com/git-lfs/git-lfs#installing) before cloning the repo. ```bash git clone https://github.com/YOUR_ACCOUNT/fastplotlib.git @@ -18,11 +18,14 @@ cd fastplotlib pip install -e ".[notebook,docs,tests] ``` +> If you cloned before installing `git-lfs`, you can run `git lfs pull` at any +> time to properly download files. + 3. Checkout the `main` branch, and then checkout your feature or bug fix branch, and run tests: > **Warning** -> Do not commit or add any changes from `examples/screenshots` or `examples/diffs`. -> If you are creating new test examples that generate or change screenshots please post an issue on the repo and we will help you. The screenshots will be generated on github actions servers, which you can then copy into the screenshots dir. :) +> Do not commit or add any changes from `examples/desktop/screenshots`. +> If you are creating new test examples that generate or change screenshots please post an issue on the repo and we will help you. The screenshots will be generated on github actions servers, which you can then copy into the screenshots dir. :) ```bash cd fastplotlib From e45d8f8d00b1c9b35d3bd923b7fc89512da115b2 Mon Sep 17 00:00:00 2001 From: Kushal Kolar Date: Fri, 8 Dec 2023 02:20:21 -0500 Subject: [PATCH 02/66] Update README.md --- README.md | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index eb64f3cc3..0c15e916d 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ [**Examples**](https://github.com/kushalkolar/fastplotlib#examples) | [**Contributing**](https://github.com/kushalkolar/fastplotlib#heart-contributing) -A fast plotting library built using the [`pygfx`](https://github.com/pygfx/pygfx) rendering engine that can utilize [Vulkan](https://en.wikipedia.org/wiki/Vulkan), [DX12](https://en.wikipedia.org/wiki/DirectX#DirectX_12), or [Metal](https://developer.apple.com/metal/) via WGPU, so it is very fast! We also aim to be an expressive plotting library that enables rapid prototyping for large scale explorative scientific visualization. +Next-gen plotting library built using the [`pygfx`](https://github.com/pygfx/pygfx) rendering engine that can utilize [Vulkan](https://en.wikipedia.org/wiki/Vulkan), [DX12](https://en.wikipedia.org/wiki/DirectX#DirectX_12), or [Metal](https://developer.apple.com/metal/) via WGPU, so it is very fast! We also aim to be an expressive plotting library that enables rapid prototyping for large scale explorative scientific visualization. ![scipy-fpl](https://github.com/fastplotlib/fastplotlib/assets/9403332/b981a54c-05f9-443f-a8e4-52cd01cd802a) @@ -70,7 +70,7 @@ pip install fastplotlib pip install "fastplotlib[notebook]" ``` -**Optional: install `simplejpeg` for much faster notebook visualization, you will need C compilers and [libjpeg-turbo](https://libjpeg-turbo.org/) to install it:** +**Optional: install `simplejpeg` for much faster notebook visualization** ```bash pip install simplejpeg @@ -96,7 +96,8 @@ 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 main branch. Note that `fastplotlib` code is basically identical between desktop and notebook usage. The differences are: -- Running in `Qt` or `glfw` require a `fastplotlib.run()` call (which is really just a `wgpu` `run()` call) +- Running in `glfw` requires a `fastplotlib.run()` call (which is really just a `wgpu` `run()` call) +- To use it in `Qt` you must encapsulate it within a `QApplication`, see `examples/qt` - Notebooks plots have ipywidget-based toolbars and widgets 😄 ### Desktop examples using `glfw` or `Qt` @@ -129,7 +130,7 @@ pip install imageio ### Video -You can watch our SciPy 2023 talk if you prefer watching demos: https://github.com/fastplotlib/fastplotlib#scipy-talk +Our SciPy 2023 talk walks through numerous demos: https://github.com/fastplotlib/fastplotlib#scipy-talk ## Graphics drivers @@ -140,9 +141,9 @@ For more information see: https://wgpu-py.readthedocs.io/en/stable/start.html#pl ### Windows: Vulkan drivers should be installed by default on Windows 11, but you will need to install your GPU manufacturer's driver package (Nvidia or AMD). If you have an integrated GPU within your CPU, you might still need to install a driver package too, check your CPU manufacturer's info. -We also recommend installing C compilers so that you can install `simplejpeg` which improves remote frame buffer performance in notebooks. - ### Linux: +You will generally need a linux distro that is from ~2020 or newer (ex. Ubuntu 18.04 won't work), this is due to the `glibc` requirements of the `wgpu-native` binary. + Debian based distros: ```bash From 5053f02582d68c18d0bec5b9b3c4c5371e785139 Mon Sep 17 00:00:00 2001 From: Kushal Kolar Date: Fri, 8 Dec 2023 02:31:40 -0500 Subject: [PATCH 03/66] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 0c15e916d..568b0ee4a 100644 --- a/README.md +++ b/README.md @@ -70,7 +70,7 @@ pip install fastplotlib pip install "fastplotlib[notebook]" ``` -**Optional: install `simplejpeg` for much faster notebook visualization** +**Recommended: install `simplejpeg` for much faster notebook visualization, this requires you to first install [libjpeg-turbo](https://libjpeg-turbo.org/)** ```bash pip install simplejpeg From 65ccc8fb537d7f73806cdf6543bd802add2f8bf1 Mon Sep 17 00:00:00 2001 From: Kushal Kolar Date: Thu, 15 Feb 2024 00:57:30 -0500 Subject: [PATCH 04/66] add faq (#400) --- docs/source/index.rst | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/docs/source/index.rst b/docs/source/index.rst index 7e1d3865a..a6d36dd4f 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -39,12 +39,23 @@ For installation please see the instructions on GitHub: https://github.com/kushalkolar/fastplotlib#installation +FAQ +=== + +1. Axes, axis, ticks, labels, legends + +A: They are on the `roadmap `_ and expected by summer 2024 :) + +2. Why the parrot logo? + +A: The logo is a `swift parrot `_, they are the fastest species of parrot and they are colorful like fastplotlib visualizations :D + Contributing ============ 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. +Also take a look at the `Roadmap 2025 `_ for future plans or ways in which you could contribute. Indices and tables ================== From 1d72a15966272a726e099a9cd5896f1a01ca75c0 Mon Sep 17 00:00:00 2001 From: Kushal Kolar Date: Sat, 17 Feb 2024 01:25:53 -0500 Subject: [PATCH 05/66] Create GOVERNANCE.md --- GOVERNANCE.md | 119 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 119 insertions(+) create mode 100644 GOVERNANCE.md diff --git a/GOVERNANCE.md b/GOVERNANCE.md new file mode 100644 index 000000000..5102707ce --- /dev/null +++ b/GOVERNANCE.md @@ -0,0 +1,119 @@ +# fastplotlib governance + +The governance of fastplotlib applies to all fastplotlib related activities. This includes the fastplotlib github organization, all repositories under the fastplotlib github organization, as well as any events or workshops organized by members of the fastplotlib Leadership Team. + +## Mission + +Leverage new graphics APIs and modern GPU hardware to create fast and interactive scientific visualizations using an expressive and elegant API. + +## Leadership team + +### Maintainers + +The maintainers are the core developers of fastplotlib and together have a complete understanding of the codebase. + +1. [Kushal Kolar](https://github.com/kushalkolar) +1. [Caitlin Lewis](https://github.com/clewis7) + + +### Advisory Committee + +The advisory committee hold a significant stake in fastplotlib as determined by the **Maintainers**. The responsibilities of the **Advisory Committee** are to 1) attend a yearly roadmap meeting, 2) be available for conflict resolution. + +1. Eric Thomson +1. Guillaume Viejo +1. Andrea Giovannucci +1. John Pearson + +### Neutral moderator + +No voting power, has no stake in the fastplotlib project + +* Reagan Bullins + +### Voting power distribution + +Maintainers: 50% + +Advisory Committee: 50% + +Veto: Any vote can be vetoed by a unanimous vote within the maintainers. + +Note that currently the voting power is primarily held by the maintainers - Kushal Kolar & Caitlin Lewis. This is intentional since the library is in an early stage. Knowledge of the codebase and its inner workings are predominantly held by Kushal & Caitlin, and nobody else. + +### Voting Process + +1. For the purpose of conflict resolution the leadership team must be convened by the neutral moderator. In all other situations the leadership team may be convened by one of the maintainers. +1. Once the leadership committee is convened and discussions have occurred, voting must conclude within 1 hour. +1. Voting is performed anonymously and handled solely by the neutral moderator in the case of conflict resolution, or by one of the maintainers. +1. After voting has finished, any maintainer may invoke a veto vote. If the veto succeeds, then the same item may not be voted on for 100 days. + +## Adding members to the Leadership Team + +### Confirming Maintainers + +* Given to individuals on merit basis after they have demonstrated strong expertise of the library through contributions, reviews and discussions. +* Adding a maintainer requires one of: + * a unanimous vote by current maintainers + * 75% vote within the advisory committee +* For continued membership in the maintainer group the individual has to demonstrate strong and continued alignment with the fastplotlib mission. The individual must also actively commit to the repo, respond to issues, and review pull requests. +* The membership is for an individual, not a company or organization. +* There must always be a minimum of 2 maintainers. +* A maintainer may be removed by one of: + * 60% vote within the maintainers + * 75% vote within the advisory committee + +### Confirming an advisory committee member + +* A candidate advisory committee member may only be nominated by a current maintainer or advisory committee member. +* Candidate must have used fastplotlib in their own work or library, or made contributions to fastplotlib. +* Candidate must be committed to the mission and demonstrate, with examples, how their role on the advisory committee would further the mission. +* If the Individual fulfills the above criteria they may be considered. The Leadership Team then requires an overall 80% vote to add the candidate as an advisory member. +* The membership is for an individual, not a company or organization. + * 60% vote within the maintainers + * 75% vote within the advisory committee + +## Invoking a vote from the leadership team + +### Conflict + +Anyone (absolutely anyone, not just the leadership team members) who feels that the code of conduct or governance document has been breached may invoke a vote by contacting the neutral moderator. + +#### Process + +1. Contact the neutral moderator with a description of the conflict, max of 250 words. +2. Neutral moderator must schedule a vote within 15 days. If that is not possible then within the next 45 days. +3. If there is a special case that requires urgency, such as upcoming events, workshops, etc., the neutral moderator may try to schedule a vote ASAP. Exercising this urgency is solely at the discretion of the neutral moderator. +4. The individual who has invoked the conflict vote can choose to present their case, or they may choose to let the neutral moderator represent them. + * Every individual involved in the conflict is given a maximum of 15 minutes to be represented. This time limit may be expanded at the discretion of the neutral moderator if a justifiable reason is provided. +5. The Leadership Team votes on one of the actions from “Enforcement Guidelines”: https://www.contributor-covenant.org/version/2/1/code_of_conduct/. It is advised that the first offense leads to action (1) “Correction” and then go down the list for repeated conflicts from the same individual/organization. Very bad behavior, as determined by the leadership team, can justify a first offense resulting in (3) “Temporary Ban” or (4) “Permanent Ban”. +6. Voting is performed as described in the section **Voting Process**. + +### Adding a member to the leadership team +1. A current maintainer or advisory committee member may nominate a candidate and then contact the neutral moderator with a written summary, of no more than 100 words, with reasons for adding the candidate to the Leadership Team. +1. The nominator should specify whether the candidate should be a maintainer or advisory committee member. +1. The neutral moderator must then schedule a vote within 100 days. +1. The nominator and candidate may then speak for no more than 10 minutes each on why the candidate should be added to the leadership team. +1. Voting is performed as described in the section **Voting Process**. + +## Changes to this governance document + +#### Until February 28, 2025 + +During early stages of fastplotlib development, changes to the governance document may be made directly through unanimous approval by the original maintainers, Kushal Kolar & Caitlin Lewis, without contacting the neutral moderator. + +#### After February 28, 2025 + +Any member of the leadership team can propose changes to the governance document. Approving the changes requires a 75% vote within the maintainers and a 75% vote within the advisory committee. + +## Reasons for invoking a vote + +Things that can be voted on include but aren’t limited to: +1. Nominating a member to the leadership team +1. Adding a nominee to the leadership team +1. Removing a member from the leadership team +1. Banning any person (not just leadership team members) or organization from interacting with the fastplotlib github repository and/or fastplotlib github organization. +1. Appeal to un-ban a previously banned person or organization. + * Can only occur 3 months after the initial ban + * Person must contact the neutral moderator directly to start an appeal + * Maximum of 3 appeal attempts are allowed From 30cc44eb6a9eede3d234e206e8d11cab699b89e2 Mon Sep 17 00:00:00 2001 From: Kushal Kolar Date: Sat, 17 Feb 2024 02:28:10 -0500 Subject: [PATCH 06/66] Update GOVERNANCE.md --- GOVERNANCE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/GOVERNANCE.md b/GOVERNANCE.md index 5102707ce..8c4f9b3cf 100644 --- a/GOVERNANCE.md +++ b/GOVERNANCE.md @@ -100,7 +100,7 @@ Anyone (absolutely anyone, not just the leadership team members) who feels that #### Until February 28, 2025 -During early stages of fastplotlib development, changes to the governance document may be made directly through unanimous approval by the original maintainers, Kushal Kolar & Caitlin Lewis, without contacting the neutral moderator. +During early stages of fastplotlib development, changes to the governance document may be made directly through unanimous approval by the original maintainers, Kushal Kolar & Caitlin Lewis, without contacting the neutral moderator or consulting with the advisory committee. They (Kushal & Caitlin) may also add new members to the advisory committee through unanimous approval. #### After February 28, 2025 From cf4a04b95ec75bb83f8b1243478fe4b4b683b094 Mon Sep 17 00:00:00 2001 From: Kushal Kolar Date: Mon, 19 Feb 2024 05:43:45 -0500 Subject: [PATCH 07/66] fix bug when remove_graphic() is used (#405) --- fastplotlib/layouts/_plot_area.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/fastplotlib/layouts/_plot_area.py b/fastplotlib/layouts/_plot_area.py index 9522832d3..819efa205 100644 --- a/fastplotlib/layouts/_plot_area.py +++ b/fastplotlib/layouts/_plot_area.py @@ -373,6 +373,12 @@ def add_graphic(self, graphic: Graphic, center: bool = True): Center the camera on the newly added Graphic """ + + if graphic in self: + # graphic is already in this plot but was removed from the scene, add it back + self.scene.add(graphic.world_object) + return + self._add_or_insert_graphic(graphic=graphic, center=center, action="add") graphic.position_z = len(self) From 78f1d2a7a1251d8b9fb92b67cadd4201ce941786 Mon Sep 17 00:00:00 2001 From: Kushal Kolar Date: Mon, 19 Feb 2024 22:35:35 -0500 Subject: [PATCH 08/66] add 'Deleted' as a graphic feature (#404) --- fastplotlib/graphics/_base.py | 11 +++++- fastplotlib/graphics/_features/__init__.py | 2 ++ fastplotlib/graphics/_features/_deleted.py | 41 ++++++++++++++++++++++ fastplotlib/graphics/image.py | 7 ++-- fastplotlib/graphics/line.py | 2 +- fastplotlib/graphics/line_collection.py | 2 +- fastplotlib/graphics/scatter.py | 2 +- 7 files changed, 60 insertions(+), 7 deletions(-) create mode 100644 fastplotlib/graphics/_features/_deleted.py diff --git a/fastplotlib/graphics/_base.py b/fastplotlib/graphics/_base.py index a0b4881fb..eea78142c 100644 --- a/fastplotlib/graphics/_base.py +++ b/fastplotlib/graphics/_base.py @@ -8,7 +8,7 @@ from pygfx import WorldObject -from ._features import GraphicFeature, PresentFeature, GraphicFeatureIndexable +from ._features import GraphicFeature, PresentFeature, GraphicFeatureIndexable, Deleted # dict that holds all world objects for a given python kernel/session # Graphic objects only use proxies to WorldObjects @@ -45,6 +45,12 @@ def __init_subclass__(cls, **kwargs): class Graphic(BaseGraphic): + feature_events = {} + + def __init_subclass__(cls, **kwargs): + # all graphics give off a feature event when deleted + cls.feature_events = {*cls.feature_events, "deleted"} + def __init__( self, name: str = None, @@ -72,6 +78,8 @@ def __init__( # store hex id str of Graphic instance mem location self.loc: str = hex(id(self)) + self.deleted = Deleted(self, False) + @property def world_object(self) -> WorldObject: """Associated pygfx WorldObject. Always returns a proxy, real object cannot be accessed directly.""" @@ -168,6 +176,7 @@ def _cleanup(self): pass def __del__(self): + self.deleted = True del WORLD_OBJECTS[self.loc] diff --git a/fastplotlib/graphics/_features/__init__.py b/fastplotlib/graphics/_features/__init__.py index a6ce9c3a3..a1769b010 100644 --- a/fastplotlib/graphics/_features/__init__.py +++ b/fastplotlib/graphics/_features/__init__.py @@ -5,6 +5,7 @@ from ._thickness import ThicknessFeature from ._base import GraphicFeature, GraphicFeatureIndexable, FeatureEvent, to_gpu_supported_dtype from ._selection_features import LinearSelectionFeature, LinearRegionSelectionFeature +from ._deleted import Deleted __all__ = [ "ColorFeature", @@ -23,4 +24,5 @@ "to_gpu_supported_dtype", "LinearSelectionFeature", "LinearRegionSelectionFeature", + "Deleted", ] diff --git a/fastplotlib/graphics/_features/_deleted.py b/fastplotlib/graphics/_features/_deleted.py new file mode 100644 index 000000000..2fca1c719 --- /dev/null +++ b/fastplotlib/graphics/_features/_deleted.py @@ -0,0 +1,41 @@ +from ._base import GraphicFeature, FeatureEvent + + +class Deleted(GraphicFeature): + """ + Used when a graphic is deleted, triggers events that can be useful to indicate this graphic has been deleted + + **event pick info:** + + ==================== ======================== ========================================================================= + key type description + ==================== ======================== ========================================================================= + "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, value: bool): + super(Deleted, self).__init__(parent, value) + + def _set(self, value: bool): + value = self._parse_set_value(value) + self._feature_changed(key=None, new_data=value) + + def _feature_changed(self, key, new_data): + # this is a non-indexable feature so key=None + + pick_info = { + "index": None, + "collection-index": self._collection_index, + "world_object": self._parent.world_object, + "new_data": new_data, + } + + event_data = FeatureEvent(type="deleted", pick_info=pick_info) + + self._call_event_handlers(event_data) + + def __repr__(self) -> str: + s = f"DeletedFeature for {self._parent}" + return s diff --git a/fastplotlib/graphics/image.py b/fastplotlib/graphics/image.py index 10f09eefb..3d629c10f 100644 --- a/fastplotlib/graphics/image.py +++ b/fastplotlib/graphics/image.py @@ -196,7 +196,7 @@ def _add_plot_area_hook(self, plot_area): class ImageGraphic(Graphic, Interaction, _AddSelectorsMixin): - feature_events = ("data", "cmap", "present") + feature_events = {"data", "cmap", "present"} def __init__( self, @@ -345,10 +345,11 @@ def col_chunk_index(self, index: int): class HeatmapGraphic(Graphic, Interaction, _AddSelectorsMixin): - feature_events = ( + feature_events = { "data", "cmap", - ) + "present" + } def __init__( self, diff --git a/fastplotlib/graphics/line.py b/fastplotlib/graphics/line.py index d6f061ab0..9ac7568a7 100644 --- a/fastplotlib/graphics/line.py +++ b/fastplotlib/graphics/line.py @@ -12,7 +12,7 @@ class LineGraphic(Graphic, Interaction): - feature_events = ("data", "colors", "cmap", "thickness", "present") + feature_events = {"data", "colors", "cmap", "thickness", "present"} def __init__( self, diff --git a/fastplotlib/graphics/line_collection.py b/fastplotlib/graphics/line_collection.py index 38597a830..a5c398130 100644 --- a/fastplotlib/graphics/line_collection.py +++ b/fastplotlib/graphics/line_collection.py @@ -15,7 +15,7 @@ class LineCollection(GraphicCollection, Interaction): child_type = LineGraphic.__name__ - feature_events = ("data", "colors", "cmap", "thickness", "present") + feature_events = {"data", "colors", "cmap", "thickness", "present"} def __init__( self, diff --git a/fastplotlib/graphics/scatter.py b/fastplotlib/graphics/scatter.py index 961324c23..63689fad9 100644 --- a/fastplotlib/graphics/scatter.py +++ b/fastplotlib/graphics/scatter.py @@ -9,7 +9,7 @@ class ScatterGraphic(Graphic): - feature_events = ("data", "sizes", "colors", "cmap", "present") + feature_events = {"data", "sizes", "colors", "cmap", "present"} def __init__( self, From 5ac4c9c05e510e8735751d2d643f1b09e9b00119 Mon Sep 17 00:00:00 2001 From: Kushal Kolar Date: Tue, 20 Feb 2024 14:30:04 -0500 Subject: [PATCH 09/66] Apply suggestions from code review Co-authored-by: Caitlin Lewis <69729525+clewis7@users.noreply.github.com> --- GOVERNANCE.md | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/GOVERNANCE.md b/GOVERNANCE.md index 8c4f9b3cf..1abd48611 100644 --- a/GOVERNANCE.md +++ b/GOVERNANCE.md @@ -1,12 +1,12 @@ # fastplotlib governance -The governance of fastplotlib applies to all fastplotlib related activities. This includes the fastplotlib github organization, all repositories under the fastplotlib github organization, as well as any events or workshops organized by members of the fastplotlib Leadership Team. +The governance of fastplotlib applies to all fastplotlib related activities. This includes the fastplotlib GitHub organization, all repositories under the fastplotlib GitHub organization, as well as any events or workshops organized by members of the fastplotlib Leadership Team. ## Mission Leverage new graphics APIs and modern GPU hardware to create fast and interactive scientific visualizations using an expressive and elegant API. -## Leadership team +## Leadership Team ### Maintainers @@ -18,7 +18,7 @@ The maintainers are the core developers of fastplotlib and together have a compl ### Advisory Committee -The advisory committee hold a significant stake in fastplotlib as determined by the **Maintainers**. The responsibilities of the **Advisory Committee** are to 1) attend a yearly roadmap meeting, 2) be available for conflict resolution. +The Advisory Committee hold a significant stake in fastplotlib as determined by the **Maintainers**. The responsibilities of the **Advisory Committee** are to 1) attend a yearly Roadmap meeting, 2) be available for conflict resolution. 1. Eric Thomson 1. Guillaume Viejo @@ -27,7 +27,7 @@ The advisory committee hold a significant stake in fastplotlib as determined by ### Neutral moderator -No voting power, has no stake in the fastplotlib project +No voting power, has no stake in the fastplotlib project. * Reagan Bullins @@ -43,8 +43,8 @@ Note that currently the voting power is primarily held by the maintainers - Kush ### Voting Process -1. For the purpose of conflict resolution the leadership team must be convened by the neutral moderator. In all other situations the leadership team may be convened by one of the maintainers. -1. Once the leadership committee is convened and discussions have occurred, voting must conclude within 1 hour. +1. For the purpose of conflict resolution the Leadership Team must be convened by the neutral moderator. In all other situations the Leadership Team may be convened by one of the maintainers. +1. Once the Leadership Team is convened and discussions have occurred, voting must conclude within 1 hour. 1. Voting is performed anonymously and handled solely by the neutral moderator in the case of conflict resolution, or by one of the maintainers. 1. After voting has finished, any maintainer may invoke a veto vote. If the veto succeeds, then the same item may not be voted on for 100 days. @@ -104,15 +104,15 @@ During early stages of fastplotlib development, changes to the governance docume #### After February 28, 2025 -Any member of the leadership team can propose changes to the governance document. Approving the changes requires a 75% vote within the maintainers and a 75% vote within the advisory committee. +Any member of the Leadership Team can propose changes to the governance document. Approving the changes requires a 75% vote within the maintainers and a 75% vote within the advisory committee. ## Reasons for invoking a vote Things that can be voted on include but aren’t limited to: -1. Nominating a member to the leadership team -1. Adding a nominee to the leadership team -1. Removing a member from the leadership team -1. Banning any person (not just leadership team members) or organization from interacting with the fastplotlib github repository and/or fastplotlib github organization. +1. Nominating a member to the Leadership Team +1. Adding a nominee to the Leadership Team +1. Removing a member from the Leadership Team +1. Banning any person (not just Leadership Team members) or organization from interacting with the fastplotlib GitHub repository and/or fastplotlib GitHub organization. 1. Appeal to un-ban a previously banned person or organization. * Can only occur 3 months after the initial ban * Person must contact the neutral moderator directly to start an appeal From 1b76169570e32e6a4055b9b38df82ac4a31bf5cc Mon Sep 17 00:00:00 2001 From: Kushal Kolar Date: Tue, 20 Feb 2024 23:56:11 -0500 Subject: [PATCH 10/66] Update GOVERNANCE.md --- GOVERNANCE.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/GOVERNANCE.md b/GOVERNANCE.md index 1abd48611..ff7b107b5 100644 --- a/GOVERNANCE.md +++ b/GOVERNANCE.md @@ -2,6 +2,8 @@ The governance of fastplotlib applies to all fastplotlib related activities. This includes the fastplotlib GitHub organization, all repositories under the fastplotlib GitHub organization, as well as any events or workshops organized by members of the fastplotlib Leadership Team. +The purpose of this document is to formalize the governance process used by the fastplotlib project, to clarify how decisions are made and how the various elements of our community interact. + ## Mission Leverage new graphics APIs and modern GPU hardware to create fast and interactive scientific visualizations using an expressive and elegant API. From e831f152b37e674d3e6d7c017dfc5a7687222f74 Mon Sep 17 00:00:00 2001 From: Caitlin Lewis <69729525+clewis7@users.noreply.github.com> Date: Sat, 24 Feb 2024 21:47:25 -0500 Subject: [PATCH 11/66] Update README.md (#411) Update README install instructions to be from fastplotlib org. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 568b0ee4a..37a2d0779 100644 --- a/README.md +++ b/README.md @@ -82,7 +82,7 @@ pip install simplejpeg ### For developers ```bash -git clone https://github.com/kushalkolar/fastplotlib.git +git clone https://github.com/fastplotlib/fastplotlib.git cd fastplotlib # install all extras in place From 7921516f3f72a281a071b5c4b2375b8b28f58da1 Mon Sep 17 00:00:00 2001 From: Kushal Kolar Date: Tue, 27 Feb 2024 01:14:44 -0500 Subject: [PATCH 12/66] gpu info upon notebook import (#410) * displaying fpl logo and gpu info works on import in nb * add face logo for nb and add to MANIFEST * remove line that snuck in from other branch * git lfs to pypi build action * fix import and add easter egg --- .github/workflows/pypi-publish.yml | 7 ++ MANIFEST.in | 2 + fastplotlib/__init__.py | 11 ++++ fastplotlib/assets/egg.gif | 3 + fastplotlib/assets/fastplotlib_face_logo.png | 3 + .../layouts/_frame/_ipywidget_toolbar.py | 16 +++++ fastplotlib/utils/__init__.py | 14 ++++ fastplotlib/utils/_gpu_info.py | 66 +++++++++++++++++++ 8 files changed, 122 insertions(+) create mode 100644 fastplotlib/assets/egg.gif create mode 100644 fastplotlib/assets/fastplotlib_face_logo.png create mode 100644 fastplotlib/utils/_gpu_info.py diff --git a/.github/workflows/pypi-publish.yml b/.github/workflows/pypi-publish.yml index 207d92351..9ebe52b87 100644 --- a/.github/workflows/pypi-publish.yml +++ b/.github/workflows/pypi-publish.yml @@ -21,7 +21,14 @@ jobs: runs-on: ubuntu-latest steps: + - name: Install git-lfs + run: | + sudo apt install --no-install-recommends -y git-lfs - uses: actions/checkout@v3 + - name: fetch git lfs files + run: | + git lfs fetch --all + git lfs pull - name: Set up Python uses: actions/setup-python@v3 with: diff --git a/MANIFEST.in b/MANIFEST.in index 121ea2fd0..b8debd28d 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,2 +1,4 @@ recursive-include fastplotlib/utils/colormaps/ * include fastplotlib/VERSION +recursive-include fastplotlib/assets/ * + diff --git a/fastplotlib/__init__.py b/fastplotlib/__init__.py index 301412aff..b3434012f 100644 --- a/fastplotlib/__init__.py +++ b/fastplotlib/__init__.py @@ -3,6 +3,7 @@ from .layouts import Plot, GridPlot from .graphics import * from .graphics.selectors import * +from .utils import _notebook_print_banner, config from wgpu.gui.auto import run @@ -13,6 +14,16 @@ else: from .widgets import ImageWidget +from wgpu.backends.wgpu_native import enumerate_adapters + +adapters = [a.request_adapter_info() for a in enumerate_adapters()] + +if len(adapters) < 1: + raise IndexError( + "No WGPU adapters found, fastplotlib will not work." + ) + +_notebook_print_banner() with open(Path(__file__).parent.joinpath("VERSION"), "r") as f: __version__ = f.read().split("\n")[0] diff --git a/fastplotlib/assets/egg.gif b/fastplotlib/assets/egg.gif new file mode 100644 index 000000000..0ff189075 --- /dev/null +++ b/fastplotlib/assets/egg.gif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7251090fd17fa81eae8a5ad176608755b4a68d620b125ebd571acc6d68daa017 +size 4589 diff --git a/fastplotlib/assets/fastplotlib_face_logo.png b/fastplotlib/assets/fastplotlib_face_logo.png new file mode 100644 index 000000000..a5a8bd90e --- /dev/null +++ b/fastplotlib/assets/fastplotlib_face_logo.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:84976200545d143f559b6840a332f3121d58f8d7de8dad7cb8a6d027d854c153 +size 10239 diff --git a/fastplotlib/layouts/_frame/_ipywidget_toolbar.py b/fastplotlib/layouts/_frame/_ipywidget_toolbar.py index f27856e61..552ee9827 100644 --- a/fastplotlib/layouts/_frame/_ipywidget_toolbar.py +++ b/fastplotlib/layouts/_frame/_ipywidget_toolbar.py @@ -3,6 +3,7 @@ from itertools import product from math import copysign from functools import partial +from pathlib import Path from typing import * @@ -17,10 +18,12 @@ BoundedIntText, Play, jslink, + Image, ) from ...graphics.selectors import PolygonSelector from ._toolbar import ToolBar +from ...utils import config class IpywidgetToolBar(HBox, ToolBar): @@ -92,6 +95,19 @@ def __init__(self, plot): self._record_button, ] + if config.party_parrot: + gif_path = Path(__file__).parent.parent.parent.joinpath("assets", "egg.gif") + with open(gif_path, "rb") as f: + gif = f.read() + + image = Image( + value=gif, + format="png", + width=35, + height=25, + ) + widgets.append(image) + if hasattr(self.plot, "_subplots"): positions = list(product(range(self.plot.shape[0]), range(self.plot.shape[1]))) values = list() diff --git a/fastplotlib/utils/__init__.py b/fastplotlib/utils/__init__.py index c8f754883..addea140d 100644 --- a/fastplotlib/utils/__init__.py +++ b/fastplotlib/utils/__init__.py @@ -1 +1,15 @@ +from dataclasses import dataclass + + from .functions import * +from ._gpu_info import _notebook_print_banner + + +@dataclass +class _Config: + party_parrot: bool + + +config = _Config( + party_parrot=False +) diff --git a/fastplotlib/utils/_gpu_info.py b/fastplotlib/utils/_gpu_info.py new file mode 100644 index 000000000..9387f3ece --- /dev/null +++ b/fastplotlib/utils/_gpu_info.py @@ -0,0 +1,66 @@ +from pathlib import Path + +from wgpu.backends.wgpu_native import enumerate_adapters +from wgpu.utils import get_default_device + +try: + ip = get_ipython() + from ipywidgets import Image + from wgpu.gui.jupyter import JupyterWgpuCanvas +except (NameError, ModuleNotFoundError, ImportError): + NOTEBOOK = False +else: + from IPython.display import display + if ip.has_trait("kernel") and (JupyterWgpuCanvas is not False): + NOTEBOOK = True + else: + NOTEBOOK = False + + +def _notebook_print_banner(): + if NOTEBOOK is False: + return + + logo_path = Path(__file__).parent.parent.joinpath("assets", "fastplotlib_face_logo.png") + + with open(logo_path, "rb") as f: + logo_data = f.read() + + image = Image( + value=logo_data, + format="png", + width=300, + height=55 + ) + + display(image) + + # print logo and adapter info + adapters = [a for a in enumerate_adapters()] + adapters_info = [a.request_adapter_info() for a in adapters] + + ix_default = adapters_info.index(get_default_device().adapter.request_adapter_info()) + + if len(adapters) > 0: + print("Available devices:") + + for ix, adapter in enumerate(adapters_info): + atype = adapter["adapter_type"] + backend = adapter["backend_type"] + driver = adapter["description"] + device = adapter["device"] + + if atype == "DiscreteGPU" and backend != "OpenGL": + charactor = chr(0x2705) + elif atype == "IntegratedGPU" and backend != "OpenGL": + charactor = chr(0x0001FBC4) + else: + charactor = chr(0x2757) + + if ix == ix_default: + default = " (default) " + else: + default = " " + + output_str = f"{charactor}{default}| {device} | {atype} | {backend} | {driver}" + print(output_str) From baa859b151a0668e30ce5b6a32d44772ede8b3df Mon Sep 17 00:00:00 2001 From: Kushal Kolar Date: Tue, 27 Feb 2024 02:12:09 -0500 Subject: [PATCH 13/66] use default renderer pixel_ratio=2 (#412) --- fastplotlib/layouts/_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fastplotlib/layouts/_utils.py b/fastplotlib/layouts/_utils.py index 7db1d84c4..1662f00c5 100644 --- a/fastplotlib/layouts/_utils.py +++ b/fastplotlib/layouts/_utils.py @@ -86,7 +86,7 @@ def make_canvas_and_renderer( ) if renderer is None: - renderer = WgpuRenderer(canvas) + renderer = WgpuRenderer(canvas, pixel_ratio=2) return canvas, renderer From a8b021a369a974d96ae4fae0898fe32578cb6972 Mon Sep 17 00:00:00 2001 From: Kushal Kolar Date: Tue, 27 Feb 2024 02:12:41 -0500 Subject: [PATCH 14/66] add gpu info to docs (#409) --- docs/source/index.rst | 6 +++ docs/source/user_guide/gpu.rst | 82 ++++++++++++++++++++++++++++++++++ 2 files changed, 88 insertions(+) create mode 100644 docs/source/user_guide/gpu.rst diff --git a/docs/source/index.rst b/docs/source/index.rst index a6d36dd4f..9dbd30783 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -12,6 +12,12 @@ Welcome to fastplotlib's documentation! quickstart +.. toctree:: + :caption: User Guide + :maxdepth: 1 + + GPU Info + .. toctree:: :maxdepth: 1 :caption: API diff --git a/docs/source/user_guide/gpu.rst b/docs/source/user_guide/gpu.rst new file mode 100644 index 000000000..006f78872 --- /dev/null +++ b/docs/source/user_guide/gpu.rst @@ -0,0 +1,82 @@ +GPU Info +******** + +FAQ +--- + +1. Do I need a GPU? + +Technically no, you can perform limited software rendering on linux using lavapipe (see drivers link below). However +``fastplotlib`` is intentionally built for realtime rendering using the latest GPU technologies, so we strongly +recommend that you use a GPU. + +2. My kernel keeps crashing when I create visualizations. + +This can happen under the following circumstances: + +- You have ran out of GPU VRAM. +- Driver issues (see next section). + +If you aren't able to solve it please post an issue on GitHub. :) + +Drivers +------- + +See the README: https://github.com/fastplotlib/fastplotlib?tab=readme-ov-file#graphics-drivers + +If you notice weird graphic artifacts, things not rendering, or other glitches try updating to the latest stable +drivers. + + +View available GPU +------------------ + +You can view all GPUs that are available to ``WGPU`` like this:: + + from wgpu.backends.wgpu_native import enumerate_adapters + from pprint import pprint + + for adapter in enumerate_adapters(): + pprint(adapter.request_adapter_info()) + +For example, on a Thinkpad AMD laptop with a dedicated nvidia GPU this returns:: + + {'adapter_type': 'IntegratedGPU', + 'architecture': '', + 'backend_type': 'Vulkan', + 'description': 'Mesa 22.3.6', + 'device': 'AMD Radeon Graphics (RADV REMBRANDT)', + 'vendor': 'radv'} + {'adapter_type': 'DiscreteGPU', + 'architecture': '', + 'backend_type': 'Vulkan', + 'description': '535.129.03', + 'device': 'NVIDIA T1200 Laptop GPU', + 'vendor': 'NVIDIA'} + {'adapter_type': 'CPU', + 'architecture': '', + 'backend_type': 'Vulkan', + 'description': 'Mesa 22.3.6 (LLVM 15.0.6)', + 'device': 'llvmpipe (LLVM 15.0.6, 256 bits)', + 'vendor': 'llvmpipe'} + {'adapter_type': 'Unknown', + 'architecture': '', + 'backend_type': 'OpenGL', + 'description': '', + 'device': 'AMD Radeon Graphics (rembrandt, LLVM 15.0.6, DRM 3.52, ' + '6.4.0-0.deb12.2-amd64)', + 'vendor': ''} + +GPU currently in use +-------------------- + +If you want to know the GPU that a current plot is using you can check the adapter that the renderer is using:: + + # for example if we make a plot + plot = fpl.Plot() + plot.add_image(np.random.rand(100, 100)) + plot.show() + + # GPU that is currently in use by the renderer + plot.renderer.device.adapter.request_adapter_info() + From 87f6469d8ff716a3f4d2c9221f3cb7088ee0e0f2 Mon Sep 17 00:00:00 2001 From: Kushal Kolar Date: Tue, 27 Feb 2024 17:42:31 -0500 Subject: [PATCH 15/66] update screenshots, pygfx line shader changes (#414) --- examples/desktop/screenshots/line.png | 4 ++-- examples/desktop/screenshots/line_cmap.png | 4 ++-- examples/desktop/screenshots/line_collection.png | 4 ++-- examples/desktop/screenshots/line_collection_cmap_values.png | 4 ++-- .../screenshots/line_collection_cmap_values_qualitative.png | 4 ++-- examples/desktop/screenshots/line_collection_colors.png | 4 ++-- examples/desktop/screenshots/line_colorslice.png | 4 ++-- examples/desktop/screenshots/line_dataslice.png | 4 ++-- examples/desktop/screenshots/line_present_scaling.png | 4 ++-- examples/desktop/screenshots/line_stack.png | 4 ++-- .../notebooks/screenshots/nb-image-widget-movie-set_data.png | 4 ++-- .../screenshots/nb-image-widget-movie-single-0-reset.png | 4 ++-- .../notebooks/screenshots/nb-image-widget-movie-single-0.png | 4 ++-- .../screenshots/nb-image-widget-movie-single-279.png | 4 ++-- .../nb-image-widget-movie-single-50-window-max-33.png | 4 ++-- .../nb-image-widget-movie-single-50-window-mean-13.png | 4 ++-- .../nb-image-widget-movie-single-50-window-mean-33.png | 4 ++-- .../nb-image-widget-movie-single-50-window-reset.png | 4 ++-- .../notebooks/screenshots/nb-image-widget-movie-single-50.png | 4 ++-- .../notebooks/screenshots/nb-image-widget-single-gnuplot2.png | 4 ++-- examples/notebooks/screenshots/nb-image-widget-single.png | 4 ++-- .../nb-image-widget-zfish-frame-50-frame-apply-gaussian.png | 4 ++-- .../nb-image-widget-zfish-frame-50-frame-apply-reset.png | 4 ++-- .../nb-image-widget-zfish-frame-50-max-window-13.png | 4 ++-- .../nb-image-widget-zfish-frame-50-mean-window-13.png | 4 ++-- .../nb-image-widget-zfish-frame-50-mean-window-5.png | 4 ++-- .../notebooks/screenshots/nb-image-widget-zfish-frame-50.png | 4 ++-- .../notebooks/screenshots/nb-image-widget-zfish-frame-99.png | 4 ++-- ...-image-widget-zfish-grid-frame-50-frame-apply-gaussian.png | 4 ++-- .../nb-image-widget-zfish-grid-frame-50-frame-apply-reset.png | 4 ++-- .../nb-image-widget-zfish-grid-frame-50-max-window-13.png | 4 ++-- .../nb-image-widget-zfish-grid-frame-50-mean-window-13.png | 4 ++-- .../nb-image-widget-zfish-grid-frame-50-mean-window-5.png | 4 ++-- .../screenshots/nb-image-widget-zfish-grid-frame-50.png | 4 ++-- .../screenshots/nb-image-widget-zfish-grid-frame-99.png | 4 ++-- .../nb-image-widget-zfish-grid-init-mean-window-5.png | 4 ++-- ...b-image-widget-zfish-grid-set_data-reset-indices-false.png | 4 ++-- ...nb-image-widget-zfish-grid-set_data-reset-indices-true.png | 4 ++-- .../screenshots/nb-image-widget-zfish-init-mean-window-5.png | 4 ++-- examples/notebooks/screenshots/nb-lines-3d.png | 4 ++-- .../notebooks/screenshots/nb-lines-cmap-jet-values-cosine.png | 4 ++-- examples/notebooks/screenshots/nb-lines-cmap-jet-values.png | 4 ++-- examples/notebooks/screenshots/nb-lines-cmap-jet.png | 4 ++-- examples/notebooks/screenshots/nb-lines-cmap-tab-10.png | 4 ++-- .../notebooks/screenshots/nb-lines-cmap-viridis-values.png | 4 ++-- examples/notebooks/screenshots/nb-lines-cmap-viridis.png | 4 ++-- examples/notebooks/screenshots/nb-lines-cmap-white.png | 4 ++-- examples/notebooks/screenshots/nb-lines-colors.png | 4 ++-- examples/notebooks/screenshots/nb-lines-data.png | 4 ++-- examples/notebooks/screenshots/nb-lines-underlay.png | 4 ++-- examples/notebooks/screenshots/nb-lines.png | 4 ++-- 51 files changed, 102 insertions(+), 102 deletions(-) diff --git a/examples/desktop/screenshots/line.png b/examples/desktop/screenshots/line.png index cbc0a7b21..8e3e6ae64 100644 --- a/examples/desktop/screenshots/line.png +++ b/examples/desktop/screenshots/line.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:018f4a36c60b3de2f1406aa2823b751de5fae2c2340f9d49368d007ba7379637 -size 44422 +oid sha256:81038ebca5d41f22f5dde3fd152d94760ef51cc055ff248be18298bc7537b569 +size 44312 diff --git a/examples/desktop/screenshots/line_cmap.png b/examples/desktop/screenshots/line_cmap.png index a07b15d36..b96c9a1dd 100644 --- a/examples/desktop/screenshots/line_cmap.png +++ b/examples/desktop/screenshots/line_cmap.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c6df49f5f900caa217a5a0b01f601d6e83e1ee5c3a60c8968b9be837df785905 -size 43673 +oid sha256:e7a9cf65bbd19290ed96e418930e896fd0ec463dc2c6797f8b407d56e9e4444d +size 43730 diff --git a/examples/desktop/screenshots/line_collection.png b/examples/desktop/screenshots/line_collection.png index 60ec82bc8..89d613c2c 100644 --- a/examples/desktop/screenshots/line_collection.png +++ b/examples/desktop/screenshots/line_collection.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e0ddd75bc2fd8e4844ccb46c53fe463dc604927737ed00c8fb6b1c29fd2b1ab2 -size 146797 +oid sha256:09e8c2be9815edf5c29a98dc61758fac3aeb2603c8547aa8c3c05b01538886e3 +size 147244 diff --git a/examples/desktop/screenshots/line_collection_cmap_values.png b/examples/desktop/screenshots/line_collection_cmap_values.png index 7223db9ee..38d9adc6e 100644 --- a/examples/desktop/screenshots/line_collection_cmap_values.png +++ b/examples/desktop/screenshots/line_collection_cmap_values.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:311c7ef6f6f46e32983a5a531adc179b0179f382300f3cafbb6d8a7d4aeab565 -size 93676 +oid sha256:98d7468dc1701d3523c779a0e857cf185201766b751731e1db44e3c2bc753335 +size 93839 diff --git a/examples/desktop/screenshots/line_collection_cmap_values_qualitative.png b/examples/desktop/screenshots/line_collection_cmap_values_qualitative.png index 500de82bb..4f14e49b9 100644 --- a/examples/desktop/screenshots/line_collection_cmap_values_qualitative.png +++ b/examples/desktop/screenshots/line_collection_cmap_values_qualitative.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8714dd43aa3d5c791fb9359e745895447d3a3234e3e8598d171ea3666a3fd7a3 -size 95660 +oid sha256:31d79a2aeb2e43b569a9045edd21fb59e7bddf2b5cc18133f4b346e0d6be7fd1 +size 95696 diff --git a/examples/desktop/screenshots/line_collection_colors.png b/examples/desktop/screenshots/line_collection_colors.png index f60faae32..c71c623c6 100644 --- a/examples/desktop/screenshots/line_collection_colors.png +++ b/examples/desktop/screenshots/line_collection_colors.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e2fad15985608a4c6b1fda3005dc89950b4cad5fed956f5d26672257385985d0 -size 82753 +oid sha256:381fc320bda54f5aa235b4cd0183ff511e3a0484c00ab37f76c89df959d48626 +size 82806 diff --git a/examples/desktop/screenshots/line_colorslice.png b/examples/desktop/screenshots/line_colorslice.png index 7b652f165..7775c1918 100644 --- a/examples/desktop/screenshots/line_colorslice.png +++ b/examples/desktop/screenshots/line_colorslice.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5bd80cee80f491da6ab64c868a3c70254a68072e0bc0caad80c7999cadcb2df9 -size 50497 +oid sha256:3ade0416355a05de01e8f4a55485aacc40bd4c3d57a1a9fcff43317e13a43856 +size 50560 diff --git a/examples/desktop/screenshots/line_dataslice.png b/examples/desktop/screenshots/line_dataslice.png index d68a554bd..2907dd64a 100644 --- a/examples/desktop/screenshots/line_dataslice.png +++ b/examples/desktop/screenshots/line_dataslice.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:823651905a775b5cbcc2ce0f79d25d69b29f17b4c060c244d80ae87019f05d5b -size 69332 +oid sha256:8fc4265c0ac6b4cee0476acbc5362968f60a965a387c7a0a3b66e89e522eb21c +size 69917 diff --git a/examples/desktop/screenshots/line_present_scaling.png b/examples/desktop/screenshots/line_present_scaling.png index 02cd2b1f8..b4b883855 100644 --- a/examples/desktop/screenshots/line_present_scaling.png +++ b/examples/desktop/screenshots/line_present_scaling.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3d18669d5e75cee3326d0380ae5dd26cab71ea97725ff99bc5228d2555d51454 -size 30373 +oid sha256:f37f2a227136af0cfc112bb1e5c9ba01fb362f33bad0971f5253adb61e89785e +size 30264 diff --git a/examples/desktop/screenshots/line_stack.png b/examples/desktop/screenshots/line_stack.png index 443184247..47e59ba8c 100644 --- a/examples/desktop/screenshots/line_stack.png +++ b/examples/desktop/screenshots/line_stack.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:29580e5ebb0597d54adb2b7f2f91de44e4480cf23c0c271ee44426203b4c1c44 -size 360892 +oid sha256:0081d587956056934c4feb0c0f695f69ea19a253243f1af1dc7de80c6406a642 +size 365226 diff --git a/examples/notebooks/screenshots/nb-image-widget-movie-set_data.png b/examples/notebooks/screenshots/nb-image-widget-movie-set_data.png index 61c3aeb8c..a728df223 100644 --- a/examples/notebooks/screenshots/nb-image-widget-movie-set_data.png +++ b/examples/notebooks/screenshots/nb-image-widget-movie-set_data.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:60318615e4850d37a4ffae16ca1e3bbf2985ddafd0dd65ba6fae997e1d123d67 -size 31251 +oid sha256:794d4ba4e31884a95c5a98596d142aa2d2af75647c5ad73dc573ff18493c1a07 +size 31256 diff --git a/examples/notebooks/screenshots/nb-image-widget-movie-single-0-reset.png b/examples/notebooks/screenshots/nb-image-widget-movie-single-0-reset.png index d09ce18e4..8e624128e 100644 --- a/examples/notebooks/screenshots/nb-image-widget-movie-single-0-reset.png +++ b/examples/notebooks/screenshots/nb-image-widget-movie-single-0-reset.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6b9187c64b7439f629a87a3828cc46a855e1b4609eca52d5484223d2c24e8bf7 -size 62562 +oid sha256:edd99517550a035ee55fb81301fb1c3e08e3e90bc4345e781449d83fa307a8a9 +size 62630 diff --git a/examples/notebooks/screenshots/nb-image-widget-movie-single-0.png b/examples/notebooks/screenshots/nb-image-widget-movie-single-0.png index d09ce18e4..8e624128e 100644 --- a/examples/notebooks/screenshots/nb-image-widget-movie-single-0.png +++ b/examples/notebooks/screenshots/nb-image-widget-movie-single-0.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6b9187c64b7439f629a87a3828cc46a855e1b4609eca52d5484223d2c24e8bf7 -size 62562 +oid sha256:edd99517550a035ee55fb81301fb1c3e08e3e90bc4345e781449d83fa307a8a9 +size 62630 diff --git a/examples/notebooks/screenshots/nb-image-widget-movie-single-279.png b/examples/notebooks/screenshots/nb-image-widget-movie-single-279.png index d4c299683..d27e49568 100644 --- a/examples/notebooks/screenshots/nb-image-widget-movie-single-279.png +++ b/examples/notebooks/screenshots/nb-image-widget-movie-single-279.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:11eb83e3489a8e9c55fcffb0e67f1cf773e538629ddf98e109601749927caa56 -size 72525 +oid sha256:3d5f5889af4490cdc81a47fd8235493bdf0384a015edcb5177ffea274efb64b1 +size 72626 diff --git a/examples/notebooks/screenshots/nb-image-widget-movie-single-50-window-max-33.png b/examples/notebooks/screenshots/nb-image-widget-movie-single-50-window-max-33.png index ef5db6693..12dce091c 100644 --- a/examples/notebooks/screenshots/nb-image-widget-movie-single-50-window-max-33.png +++ b/examples/notebooks/screenshots/nb-image-widget-movie-single-50-window-max-33.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:51bc6a90fba5c67935838b8e44dca477e250cbc4ee2b98ddd69f931e683ec17a -size 63906 +oid sha256:ec662212f6299cd27c3e26bb6b9fa1a99b4dd2808934536640e2f54d8bf8f699 +size 63968 diff --git a/examples/notebooks/screenshots/nb-image-widget-movie-single-50-window-mean-13.png b/examples/notebooks/screenshots/nb-image-widget-movie-single-50-window-mean-13.png index 86287ea9e..44ccb8020 100644 --- a/examples/notebooks/screenshots/nb-image-widget-movie-single-50-window-mean-13.png +++ b/examples/notebooks/screenshots/nb-image-widget-movie-single-50-window-mean-13.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1a2318ab1f242d70045b9784337e4421c635b2345331b6a5e8edc0f32ff15f07 -size 54432 +oid sha256:ffe73fa414fa1af8e2daa0bdb7fa75fd47d9d575b60c729a9426deb42225bb5a +size 54513 diff --git a/examples/notebooks/screenshots/nb-image-widget-movie-single-50-window-mean-33.png b/examples/notebooks/screenshots/nb-image-widget-movie-single-50-window-mean-33.png index 14c101960..f0354cbb2 100644 --- a/examples/notebooks/screenshots/nb-image-widget-movie-single-50-window-mean-33.png +++ b/examples/notebooks/screenshots/nb-image-widget-movie-single-50-window-mean-33.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6c5b5df5b0efb0b3b641129e08429e4a92bd5d2c62b1c63c97337287867d685e -size 50341 +oid sha256:10d82b8d15a5d1099f8a909401d78cc6a87d22708a7cd4c32d96bb316d570da3 +size 50431 diff --git a/examples/notebooks/screenshots/nb-image-widget-movie-single-50-window-reset.png b/examples/notebooks/screenshots/nb-image-widget-movie-single-50-window-reset.png index 0df0df92b..25b65bd48 100644 --- a/examples/notebooks/screenshots/nb-image-widget-movie-single-50-window-reset.png +++ b/examples/notebooks/screenshots/nb-image-widget-movie-single-50-window-reset.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f2e5a5734992333165ef2d5f6f810d869e157e59fb7f54c8dd5f413ac750a3fa -size 65067 +oid sha256:79776dadbe6947e9f20c96e5dc2d6ee718100f24fc09579609e7b25614869b74 +size 65150 diff --git a/examples/notebooks/screenshots/nb-image-widget-movie-single-50.png b/examples/notebooks/screenshots/nb-image-widget-movie-single-50.png index 0df0df92b..25b65bd48 100644 --- a/examples/notebooks/screenshots/nb-image-widget-movie-single-50.png +++ b/examples/notebooks/screenshots/nb-image-widget-movie-single-50.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f2e5a5734992333165ef2d5f6f810d869e157e59fb7f54c8dd5f413ac750a3fa -size 65067 +oid sha256:79776dadbe6947e9f20c96e5dc2d6ee718100f24fc09579609e7b25614869b74 +size 65150 diff --git a/examples/notebooks/screenshots/nb-image-widget-single-gnuplot2.png b/examples/notebooks/screenshots/nb-image-widget-single-gnuplot2.png index da3033219..75fa3ef5a 100644 --- a/examples/notebooks/screenshots/nb-image-widget-single-gnuplot2.png +++ b/examples/notebooks/screenshots/nb-image-widget-single-gnuplot2.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:10912f4358278fb26c5efa030a335b0bdb80ebcc2a57fd97838fea3780f9f5d1 -size 143543 +oid sha256:7e292d6e43e7a98cb72ca077e62bbc9ad3acde4753098ea2248c66ee07a9fc46 +size 143675 diff --git a/examples/notebooks/screenshots/nb-image-widget-single.png b/examples/notebooks/screenshots/nb-image-widget-single.png index 346c1a987..1321d01bb 100644 --- a/examples/notebooks/screenshots/nb-image-widget-single.png +++ b/examples/notebooks/screenshots/nb-image-widget-single.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bf23c969c93bc526244360af4babf72a1377a171f55b1f44443dc026faf12631 -size 134432 +oid sha256:50f17ab342efff0d1d3bc06f36d2b7b372eed4426e282ffa2b12e3e7dc913b68 +size 134516 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50-frame-apply-gaussian.png b/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50-frame-apply-gaussian.png index 6be1058e9..2543f9a5d 100644 --- a/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50-frame-apply-gaussian.png +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50-frame-apply-gaussian.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:49b65e35bb12e0bce8e753f6700084b2e1100eb6efd0336afc219d9e26972901 -size 64206 +oid sha256:5834c92427c08b210a0971667018b4a63fd4e8d916bb02a582ba352cb0d5aad7 +size 64281 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50-frame-apply-reset.png b/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50-frame-apply-reset.png index ead51e894..1a062cdf7 100644 --- a/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50-frame-apply-reset.png +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50-frame-apply-reset.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:df13f86bb18ad52f962f8944cd780579440b4e1d40941019632d4f46a4d9dc2d -size 50107 +oid sha256:00f8173e7476a781826617bbaf03241348cece3c25d355e13832961cb022145f +size 50155 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50-max-window-13.png b/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50-max-window-13.png index 295180169..00af23a9a 100644 --- a/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50-max-window-13.png +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50-max-window-13.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:42676358a0199022972b69263c32a977f90a27ce6c3789d18129c67e9b730679 -size 121369 +oid sha256:676217864c9f5ca4792c08a4b51574acdceb8c02b186e467633583b358bdb0f8 +size 121420 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50-mean-window-13.png b/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50-mean-window-13.png index adf129ab2..bd31b760d 100644 --- a/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50-mean-window-13.png +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50-mean-window-13.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f3d1972c575f1659fc2c611a2c703cb408b74ca47e30c2e4564641dc0a6ad887 -size 76550 +oid sha256:4bb119d1e38ec69285092c551da7ff12541a15c4ef144689e6535c520e0e988b +size 76596 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50-mean-window-5.png b/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50-mean-window-5.png index f123f83a9..ccbbd13d6 100644 --- a/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50-mean-window-5.png +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50-mean-window-5.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e24ea022d48ff41174206e43da911632f38237f6250340f73a7169a43d55f2a6 -size 72238 +oid sha256:57202d813c24f93fc106a1e66edd04af5b877ebf291380b3894b583ddccbda19 +size 72288 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50.png b/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50.png index 8c65f7840..d4020153c 100644 --- a/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50.png +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:983cf6804561b4287f6acfdac04e2a31810c9d3191108e2da4623cbb852258d5 -size 56707 +oid sha256:cc156be2788a1d82f587a1ae0a7b29f4b6d75f07bfd83055ecefe92b213c01fd +size 56749 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-frame-99.png b/examples/notebooks/screenshots/nb-image-widget-zfish-frame-99.png index fdc616d07..368c1b9f8 100644 --- a/examples/notebooks/screenshots/nb-image-widget-zfish-frame-99.png +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-frame-99.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4e6f528bf9508397a97e33d463ccd8b3584d3ebd04499b976b4989e001648626 -size 45174 +oid sha256:03da34d72c3437de1bebea5a545e1a75fb7b6a7ea309cbd88340031acc5cb852 +size 45223 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50-frame-apply-gaussian.png b/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50-frame-apply-gaussian.png index 66d5557ee..c722c8ec1 100644 --- a/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50-frame-apply-gaussian.png +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50-frame-apply-gaussian.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:69f8a085b84ee851ad1579791ff96fe6bc73551d86f376524d1194c43edf819f -size 74941 +oid sha256:309384b0062003c9af321a005ddb5e0a78db382cf2b40285dd8ca43423cb13dd +size 74975 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50-frame-apply-reset.png b/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50-frame-apply-reset.png index 3f53f463b..9be3a311f 100644 --- a/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50-frame-apply-reset.png +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50-frame-apply-reset.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0616c08a8cf008fbc976638cfb7d76d533547bdb6cbfa56071567dac3c7e703d -size 75619 +oid sha256:28291e3913c28a2933affa12810254627f352fd4059a77d3637e2756737d11c0 +size 75617 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50-max-window-13.png b/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50-max-window-13.png index e2f0161b7..912374687 100644 --- a/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50-max-window-13.png +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50-max-window-13.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e4f7893445c9f2d26f126a5ac87c1fdf0737b31fec730f754ba4c771eae5ec5b -size 116744 +oid sha256:c248700a71fb8284d039714d9b11672f6788bcd0df716c26aa936436c5035c28 +size 116784 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50-mean-window-13.png b/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50-mean-window-13.png index fde3c576b..66971bfb1 100644 --- a/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50-mean-window-13.png +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50-mean-window-13.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a73e0ef31acf4304bc9a649f0761e0d282eb4517ade89538edfe6b7d8a9b61d0 -size 75487 +oid sha256:a5fe9fce8c8e63e497b558370d0d3edd8f86b0ad27939feec02715dd84fe7353 +size 75550 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50-mean-window-5.png b/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50-mean-window-5.png index c100dcaf3..76f6c71a2 100644 --- a/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50-mean-window-5.png +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50-mean-window-5.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:00ade44cd4478bf77e7bd33ccee64e2905136e35620171f013be7e90ddaa0ba6 -size 79123 +oid sha256:b7d563033c432006b88adce07aabf0ab6a2c6559b519f6f820ebb58066f4d1b2 +size 79214 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50.png b/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50.png index 08b244755..90639b14f 100644 --- a/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50.png +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1eb4d6a4cfbdbfffc198dbe5987f621e5cc3fdcf574699c544a12cf3b28bcdb7 -size 82281 +oid sha256:8408e6e841d09b4c2f1704803515c878cdc46e4f7d4cd67b1634efb4efba8e98 +size 82327 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-99.png b/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-99.png index 6868bf1ff..97e3787ea 100644 --- a/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-99.png +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-99.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:758c4f444ab3792c6328e51d619c51f59f27fe2589795cc82bdef7fb5daf57e3 -size 79663 +oid sha256:4c21bbd9a9d1548d5480f9a0f0f6d4ace0abfd80e4d09e295557163310c7095d +size 79733 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-grid-init-mean-window-5.png b/examples/notebooks/screenshots/nb-image-widget-zfish-grid-init-mean-window-5.png index 3bcfe6c24..24eda7928 100644 --- a/examples/notebooks/screenshots/nb-image-widget-zfish-grid-init-mean-window-5.png +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-grid-init-mean-window-5.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b75c179afde357fd69d7c3e918ad34e072685c536e388b1424596bd493b041e8 -size 81563 +oid sha256:2b6222d1a7291609f5d2637e97e9455a4e734b6dca4d6ebf9246ea46f32ce4de +size 81628 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-grid-set_data-reset-indices-false.png b/examples/notebooks/screenshots/nb-image-widget-zfish-grid-set_data-reset-indices-false.png index 4a8ee413a..3285e7875 100644 --- a/examples/notebooks/screenshots/nb-image-widget-zfish-grid-set_data-reset-indices-false.png +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-grid-set_data-reset-indices-false.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e6d9460835d55d696ffcef1bff7cb7a86672d4c7345112c62050312669055870 -size 66010 +oid sha256:b6f5d03a9929a12232628da3a0c89673a650c94737ead73bf74e0daa587e75f9 +size 65977 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-grid-set_data-reset-indices-true.png b/examples/notebooks/screenshots/nb-image-widget-zfish-grid-set_data-reset-indices-true.png index 984f1537c..63bd3d070 100644 --- a/examples/notebooks/screenshots/nb-image-widget-zfish-grid-set_data-reset-indices-true.png +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-grid-set_data-reset-indices-true.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7b83f6353ce227d76fbf7362218bcc35afa26216e5600cf145cbcce666bb6dd6 -size 66520 +oid sha256:182410c74215dc70140561200cdb414555e98a1ea7767205764a19cc9b72bf6d +size 66489 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-init-mean-window-5.png b/examples/notebooks/screenshots/nb-image-widget-zfish-init-mean-window-5.png index 2767f8699..e82e34241 100644 --- a/examples/notebooks/screenshots/nb-image-widget-zfish-init-mean-window-5.png +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-init-mean-window-5.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:130c7a58ceee6ffe0ec58f194bce04e6f0892c97448b43ef9f90ced85c9c10ba -size 62566 +oid sha256:59eaa00b1ba39188f664719964036857df5cb143e2f70171d86e822958e98fa7 +size 62617 diff --git a/examples/notebooks/screenshots/nb-lines-3d.png b/examples/notebooks/screenshots/nb-lines-3d.png index 727450428..4ac6b8b92 100644 --- a/examples/notebooks/screenshots/nb-lines-3d.png +++ b/examples/notebooks/screenshots/nb-lines-3d.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1da691b87d324d1a5b2c4f9231be89c0c3dddb0584615a07f28a4d027dc59d5c -size 23057 +oid sha256:467cf0f08f6861c9556a412ed57e0bc6ff4499e62d576fa10dcdc2ca9ef4de6a +size 23693 diff --git a/examples/notebooks/screenshots/nb-lines-cmap-jet-values-cosine.png b/examples/notebooks/screenshots/nb-lines-cmap-jet-values-cosine.png index 2f149e7f4..0c875da24 100644 --- a/examples/notebooks/screenshots/nb-lines-cmap-jet-values-cosine.png +++ b/examples/notebooks/screenshots/nb-lines-cmap-jet-values-cosine.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a755f36126b805b8d63d6ea679ffa270dc20d976a9a3a2cd1420ccdf0e981474 -size 17158 +oid sha256:c870f3010432310ba6d1937410a23836de6ddb656eb83bfb1b928d7d1211bd09 +size 17263 diff --git a/examples/notebooks/screenshots/nb-lines-cmap-jet-values.png b/examples/notebooks/screenshots/nb-lines-cmap-jet-values.png index 45a34c5f0..e193c85b3 100644 --- a/examples/notebooks/screenshots/nb-lines-cmap-jet-values.png +++ b/examples/notebooks/screenshots/nb-lines-cmap-jet-values.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:76e55108ce7ace466fb9b90b852a1102ff8b4c931e16f05af231a854519c8467 -size 18505 +oid sha256:5c98af9372102a7fef96fe45650ca7e0b4f6e3589abd5fc3c1205130f3613c08 +size 18859 diff --git a/examples/notebooks/screenshots/nb-lines-cmap-jet.png b/examples/notebooks/screenshots/nb-lines-cmap-jet.png index ed8138ab1..acc52de78 100644 --- a/examples/notebooks/screenshots/nb-lines-cmap-jet.png +++ b/examples/notebooks/screenshots/nb-lines-cmap-jet.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7e463ac93808329ffa4fe421838ff8d39ce49081b888d09954405cd170af8a85 -size 16267 +oid sha256:bf3494a4cc2d4ea061fbd3465bd2133120b63bab6b248681307687e4e8e395a2 +size 16322 diff --git a/examples/notebooks/screenshots/nb-lines-cmap-tab-10.png b/examples/notebooks/screenshots/nb-lines-cmap-tab-10.png index e10d6f5e9..d18cadb51 100644 --- a/examples/notebooks/screenshots/nb-lines-cmap-tab-10.png +++ b/examples/notebooks/screenshots/nb-lines-cmap-tab-10.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:99c10da11e298d69cd85d587b133fe31a5528657ba456e5f0050ca0e48ed0f31 -size 14865 +oid sha256:b66364f0b7ba1da27b713e48cc757215f84143f6646451e8dfe7938d90a69102 +size 14863 diff --git a/examples/notebooks/screenshots/nb-lines-cmap-viridis-values.png b/examples/notebooks/screenshots/nb-lines-cmap-viridis-values.png index da5693226..ecdbb0db7 100644 --- a/examples/notebooks/screenshots/nb-lines-cmap-viridis-values.png +++ b/examples/notebooks/screenshots/nb-lines-cmap-viridis-values.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d356938c29834263d9879c3217782414d631e998ea02f11f3c00c0f64d8a63a6 -size 15084 +oid sha256:2e1b04f1ffc5c63dd93c9676bbf751f8ca698e113ce3403534f40ab868421960 +size 14974 diff --git a/examples/notebooks/screenshots/nb-lines-cmap-viridis.png b/examples/notebooks/screenshots/nb-lines-cmap-viridis.png index ddcb6d54a..a78bd9d54 100644 --- a/examples/notebooks/screenshots/nb-lines-cmap-viridis.png +++ b/examples/notebooks/screenshots/nb-lines-cmap-viridis.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:dfd17b5b191edf58377451b0c34636d047f69de1a89087d9d2d561d967c4d236 -size 19118 +oid sha256:69166f21efd52253b2b8444c32c9770a87983b3f21afcf49f4d0a94c51e5280c +size 19206 diff --git a/examples/notebooks/screenshots/nb-lines-cmap-white.png b/examples/notebooks/screenshots/nb-lines-cmap-white.png index 93efe221c..c61050a7d 100644 --- a/examples/notebooks/screenshots/nb-lines-cmap-white.png +++ b/examples/notebooks/screenshots/nb-lines-cmap-white.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d3b512a195ab30075a028d19884716c5f776acd6f118e682046d6659d1e1095e -size 12962 +oid sha256:1556f875c6e5538ca289db5ced0e59530174d899d6baa19876e1ad753571d4d1 +size 12921 diff --git a/examples/notebooks/screenshots/nb-lines-colors.png b/examples/notebooks/screenshots/nb-lines-colors.png index a8af1a4be..33a751e98 100644 --- a/examples/notebooks/screenshots/nb-lines-colors.png +++ b/examples/notebooks/screenshots/nb-lines-colors.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c8db3d4ccb1e56919866a6ba510033dc903d23a5af06a961f90ddb7382d16ec7 -size 40477 +oid sha256:fb407b2008cb7a6793e7e53d487b959e1178de2a3b7fbd504b8ef3b92a7b09ea +size 40842 diff --git a/examples/notebooks/screenshots/nb-lines-data.png b/examples/notebooks/screenshots/nb-lines-data.png index 369a999b2..7447b5b86 100644 --- a/examples/notebooks/screenshots/nb-lines-data.png +++ b/examples/notebooks/screenshots/nb-lines-data.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2907942f9307eb21764ea02d363a8c6c8b7e4fbf375257b7c19225e1f7b66279 -size 54404 +oid sha256:3fe83c11993dc99085526e11db60ac6797756b2b35145458009b36e56991a0d9 +size 55297 diff --git a/examples/notebooks/screenshots/nb-lines-underlay.png b/examples/notebooks/screenshots/nb-lines-underlay.png index d6b630362..692aa76ea 100644 --- a/examples/notebooks/screenshots/nb-lines-underlay.png +++ b/examples/notebooks/screenshots/nb-lines-underlay.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:70800d1739bb5ba7d5d9d2399335ebc1ce8a0874042ed4b6256b6d041014eb18 -size 55884 +oid sha256:f65834683f945ae52612fa1ae8415d25866ccd6d776dd94798b30b08769b0387 +size 56338 diff --git a/examples/notebooks/screenshots/nb-lines.png b/examples/notebooks/screenshots/nb-lines.png index 2fcd0637f..b89e9f47c 100644 --- a/examples/notebooks/screenshots/nb-lines.png +++ b/examples/notebooks/screenshots/nb-lines.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4f70a9dd8cc631337ba1ccc7931ef2d412d0d6713d5403011994d59258f61e34 -size 37714 +oid sha256:cc0511ba508333566c902a3f1a902e76044a33a41f75bfc43ee158ff5b7bd6c7 +size 37671 From 481c5ea4a5d4e741a8b48e45fd9fe46b4b955305 Mon Sep 17 00:00:00 2001 From: Kushal Kolar Date: Tue, 27 Feb 2024 23:41:22 -0500 Subject: [PATCH 16/66] Update .gitattributes (#415) add gif to .gitattributes for git lfs --- .gitattributes | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitattributes b/.gitattributes index e4a509285..c921772ae 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,3 +1,3 @@ *.png filter=lfs diff=lfs merge=lfs -text +*.gif filter=lfs diff=lfs merge=lfs -text *.npy filter=lfs diff=lfs merge=lfs -text - From 8919d7c09d9b23a7ec63efd91450cc73f92efa9e Mon Sep 17 00:00:00 2001 From: Kushal Kolar Date: Wed, 28 Feb 2024 23:29:23 -0500 Subject: [PATCH 17/66] legends, part 1 (#406) * start legends, not functional yet * add faq (#400) * fix bug when remove_graphic() is used (#405) * add 'Deleted' as a graphic feature (#404) * very basic adding line legends works * use OrderedDict for legend items * allow accessing legend items via Graphic, updating colors works * legend mesh resizes properly * remove legend items and reorder works * enforce all legend labels to be unique * add legends property to PlotArea * checks for Graphic name * highlight linegraphic when legend item is clicked * legend is moveable * remove weird characters that were committed for some reason * progress on legend grid placement, not yet working * max_rows works for legend * just allow max_rows kwarg for legends, no cols * line that snuck in from another branch * small changes --- fastplotlib/__init__.py | 7 +- fastplotlib/graphics/_base.py | 21 +- fastplotlib/layouts/_plot_area.py | 25 ++- fastplotlib/legends/__init__.py | 3 + fastplotlib/legends/legend.py | 354 ++++++++++++++++++++++++++++++ fastplotlib/utils/mesh_masks.py | 128 +++++++++++ 6 files changed, 530 insertions(+), 8 deletions(-) create mode 100644 fastplotlib/legends/__init__.py create mode 100644 fastplotlib/legends/legend.py create mode 100644 fastplotlib/utils/mesh_masks.py diff --git a/fastplotlib/__init__.py b/fastplotlib/__init__.py index b3434012f..ca872f4e4 100644 --- a/fastplotlib/__init__.py +++ b/fastplotlib/__init__.py @@ -3,7 +3,7 @@ from .layouts import Plot, GridPlot from .graphics import * from .graphics.selectors import * -from .utils import _notebook_print_banner, config +from .legends import * from wgpu.gui.auto import run @@ -23,8 +23,6 @@ "No WGPU adapters found, fastplotlib will not work." ) -_notebook_print_banner() - with open(Path(__file__).parent.joinpath("VERSION"), "r") as f: __version__ = f.read().split("\n")[0] @@ -32,5 +30,6 @@ "Plot", "GridPlot", "run", - "ImageWidget" + "ImageWidget", + "Legend", ] diff --git a/fastplotlib/graphics/_base.py b/fastplotlib/graphics/_base.py index eea78142c..4f342faa2 100644 --- a/fastplotlib/graphics/_base.py +++ b/fastplotlib/graphics/_base.py @@ -48,6 +48,7 @@ class Graphic(BaseGraphic): feature_events = {} def __init_subclass__(cls, **kwargs): + super().__init_subclass__(**kwargs) # all graphics give off a feature event when deleted cls.feature_events = {*cls.feature_events, "deleted"} @@ -62,14 +63,16 @@ def __init__( Parameters ---------- name: str, optional - name this graphic, makes it indexable within plots + name this graphic to use it as a key to access from the plot metadata: Any, optional metadata attached to this Graphic, this is for the user to manage """ + if (name is not None) and (not isinstance(name, str)): + raise TypeError("Graphic `name` must be of type ") - self.name = name + self._name = name self.metadata = metadata self.collection_index = collection_index self.registered_callbacks = dict() @@ -80,6 +83,20 @@ def __init__( self.deleted = Deleted(self, False) + self._plot_area = None + + @property + def name(self) -> Union[str, None]: + """str name reference for this item""" + return self._name + + @name.setter + def name(self, name: str): + if not isinstance(name, str): + raise TypeError("`Graphic` name must be of type ") + if self._plot_area is not None: + self._plot_area._check_graphic_name_exists(name) + @property def world_object(self) -> WorldObject: """Associated pygfx WorldObject. Always returns a proxy, real object cannot be accessed directly.""" diff --git a/fastplotlib/layouts/_plot_area.py b/fastplotlib/layouts/_plot_area.py index 819efa205..cc7526398 100644 --- a/fastplotlib/layouts/_plot_area.py +++ b/fastplotlib/layouts/_plot_area.py @@ -12,6 +12,7 @@ from ._utils import create_camera, create_controller from ..graphics._base import Graphic from ..graphics.selectors._base_selector import BaseSelector +from ..legends import Legend # dict to store Graphic instances # this is the only place where the real references to Graphics are stored in a Python session @@ -62,7 +63,7 @@ def __init__( name: str, optional name this ``subplot`` or ``plot`` - + """ self._parent: PlotArea = parent @@ -208,6 +209,8 @@ def graphics(self) -> Tuple[Graphic, ...]: proxies = list() for loc in self._graphics: p = weakref.proxy(GRAPHICS[loc]) + if p.__class__.__name__ == "Legend": + continue proxies.append(p) return tuple(proxies) @@ -222,6 +225,17 @@ def selectors(self) -> Tuple[BaseSelector, ...]: return tuple(proxies) + @property + def legends(self) -> Tuple[Legend, ...]: + """Legends in the plot area.""" + proxies = list() + for loc in self._graphics: + p = weakref.proxy(GRAPHICS[loc]) + if p.__class__.__name__ == "Legend": + proxies.append(p) + + return tuple(proxies) + @property def name(self) -> Any: """The name of this plot area""" @@ -486,6 +500,9 @@ def _check_graphic_name_exists(self, name): for s in self.selectors: graphic_names.append(s.name) + for l in self.legends: + graphic_names.append(l.name) + if name in graphic_names: raise ValueError( f"graphics must have unique names, current graphic names are:\n {graphic_names}" @@ -666,6 +683,10 @@ def __getitem__(self, name: str): if selector.name == name: return selector + for legend in self.legends: + if legend.name == name: + return legend + graphic_names = list() for g in self.graphics: graphic_names.append(g.name) @@ -681,7 +702,7 @@ def __getitem__(self, name: str): ) def __contains__(self, item: Union[str, Graphic]): - to_check = [*self.graphics, *self.selectors] + to_check = [*self.graphics, *self.selectors, *self.legends] if isinstance(item, Graphic): if item in to_check: diff --git a/fastplotlib/legends/__init__.py b/fastplotlib/legends/__init__.py new file mode 100644 index 000000000..507251f59 --- /dev/null +++ b/fastplotlib/legends/__init__.py @@ -0,0 +1,3 @@ +from .legend import Legend + +__all__ = ["Legend"] diff --git a/fastplotlib/legends/legend.py b/fastplotlib/legends/legend.py new file mode 100644 index 000000000..e29665e0f --- /dev/null +++ b/fastplotlib/legends/legend.py @@ -0,0 +1,354 @@ +from functools import partial +from collections import OrderedDict +from typing import * + +import numpy as np +import pygfx + +from ..graphics._base import Graphic +from ..graphics._features._base import FeatureEvent +from ..graphics import LineGraphic, ScatterGraphic, ImageGraphic +from ..utils import mesh_masks + + +class LegendItem: + def __init__( + self, + label: str, + color: pygfx.Color, + ): + """ + + Parameters + ---------- + label: str + + color: pygfx.Color + """ + self._label = label + self._color = color + + +class LineLegendItem(LegendItem): + def __init__( + self, + parent, + graphic: LineGraphic, + label: str, + position: Tuple[int, int] + ): + """ + + Parameters + ---------- + graphic: LineGraphic + + label: str + + position: [x, y] + """ + + if label is not None: + pass + + elif graphic.name is not None: + pass + + else: + raise ValueError("Must specify `label` or Graphic must have a `name` to auto-use as the label") + + # for now only support lines with a single color + if np.unique(graphic.colors(), axis=0).shape[0] > 1: + raise ValueError("Use colorbars for multi-colored lines, not legends") + + color = pygfx.Color(np.unique(graphic.colors(), axis=0).ravel()) + + self._parent = parent + + super().__init__(label, color) + + graphic.colors.add_event_handler(self._update_color) + + # construct Line WorldObject + data = np.array( + [[0, 0, 0], + [3, 0, 0]], + dtype=np.float32 + ) + + material = pygfx.LineMaterial + + self._line_world_object = pygfx.Line( + geometry=pygfx.Geometry(positions=data), + material=material(thickness=8, color=self._color) + ) + + # self._line_world_object.world.x = position[0] + + self._label_world_object = pygfx.Text( + geometry=pygfx.TextGeometry( + text=str(label), + font_size=6, + screen_space=False, + anchor="middle-left", + ), + material=pygfx.TextMaterial( + color="w", + outline_color="w", + outline_thickness=0, + ) + ) + + self.world_object = pygfx.Group() + self.world_object.add(self._line_world_object, self._label_world_object) + + self.world_object.world.x = position[0] + # add 10 to x to account for space for the line + self._label_world_object.world.x = position[0] + 10 + + self.world_object.world.y = position[1] + self.world_object.world.z = 2 + + self.world_object.add_event_handler(partial(self._highlight_graphic, graphic), "click") + + @property + def label(self) -> str: + return self._label + + @label.setter + def label(self, text: str): + self._parent._check_label_unique(text) + self._label_world_object.geometry.set_text(text) + + def _update_color(self, ev: FeatureEvent): + new_color = ev.pick_info["new_data"] + if np.unique(new_color, axis=0).shape[0] > 1: + raise ValueError("LegendError: LineGraphic colors no longer appropriate for legend") + + self._color = new_color[0] + self._line_world_object.material.color = pygfx.Color(self._color) + + def _highlight_graphic(self, graphic, ev): + graphic_color = pygfx.Color(np.unique(graphic.colors(), axis=0).ravel()) + + if graphic_color == self._parent.highlight_color: + graphic.colors = self._color + else: + # hacky but fine for now + orig_color = pygfx.Color(self._color) + graphic.colors = self._parent.highlight_color + self._color = orig_color + + +class Legend(Graphic): + def __init__( + self, + plot_area, + highlight_color: Union[str, tuple, np.ndarray] = "w", + max_rows: int = 5, + *args, + **kwargs + ): + """ + + Parameters + ---------- + plot_area: Union[Plot, Subplot, Dock] + plot area to put the legend in + + highlight_color: Union[str, tuple, np.ndarray], default "w" + highlight color + + max_rows: int, default 5 + maximum number of rows allowed in the legend + + """ + self._graphics: List[Graphic] = list() + + # hex id of Graphic, i.e. graphic.loc are the keys + self._items: OrderedDict[str: LegendItem] = OrderedDict() + + super().__init__(*args, **kwargs) + + group = pygfx.Group() + self._legend_items_group = pygfx.Group() + self._set_world_object(group) + + self._mesh = pygfx.Mesh( + pygfx.box_geometry(50, 10, 1), + pygfx.MeshBasicMaterial(color=pygfx.Color([0.1, 0.1, 0.1, 1]), wireframe_thickness=10) + ) + + self.world_object.add(self._mesh) + self.world_object.add(self._legend_items_group) + + self.highlight_color = pygfx.Color(highlight_color) + + self._plot_area = plot_area + self._plot_area.add_graphic(self) + + if self._plot_area.__class__.__name__ == "Dock": + if self._plot_area.size < 1: + self._plot_area.size = 100 + + # TODO: refactor with "moveable graphic" base class once that's done + self._mesh.add_event_handler(self._pointer_down, "pointer_down") + self._plot_area.renderer.add_event_handler(self._pointer_move, "pointer_move") + self._plot_area.renderer.add_event_handler(self._pointer_up, "pointer_up") + + self._last_position = None + self._initial_controller_state = self._plot_area.controller.enabled + + self._max_rows = max_rows + + self._row_counter = 0 + self._col_counter = 0 + + def graphics(self) -> Tuple[Graphic, ...]: + return tuple(self._graphics) + + def _check_label_unique(self, label): + for legend_item in self._items.values(): + if legend_item.label == label: + raise ValueError( + f"You have passed the label '{label}' which is already used for another legend item. " + f"All labels within a legend must be unique." + ) + + def add_graphic(self, graphic: Graphic, label: str = None): + if graphic in self._graphics: + raise KeyError( + f"Graphic already exists in legend with label: '{self._items[graphic.loc].label}'" + ) + + self._check_label_unique(label) + + new_col_ix = self._col_counter + new_row_ix = self._row_counter + + x_pos = 0 + y_pos = 0 + + if self._row_counter == self._max_rows: + # set counters + new_col_ix = self._col_counter + 1 + + # get x position offset for this new column of LegendItems + # start by getting the LegendItems in the previous column + prev_column_items: List[LegendItem] = list(self._items.values())[-self._max_rows:] + # x position of LegendItems in previous column + x_pos = prev_column_items[-1].world_object.world.x + max_width = 0 + # get width of widest LegendItem in previous column to add to x_pos offset for this column + for item in prev_column_items: + bbox = item.world_object.get_world_bounding_box() + width, height, depth = bbox.ptp(axis=0) + max_width = max(max_width, width) + + # x position offset for this new column + x_pos = x_pos + max_width + 15 # add 15 for spacing + + # rest row index for next iteration + new_row_ix = 1 + else: + if len(self._items) > 0: + x_pos = list(self._items.values())[-1].world_object.world.x + + y_pos = new_row_ix * -10 + new_row_ix = self._row_counter + 1 + + if isinstance(graphic, LineGraphic): + legend_item = LineLegendItem(self, graphic, label, position=(x_pos, y_pos)) + else: + raise ValueError("Legend only supported for LineGraphic for now.") + + self._legend_items_group.add(legend_item.world_object) + self._reset_mesh_dims() + + self._graphics.append(graphic) + self._items[graphic.loc] = legend_item + + graphic.deleted.add_event_handler(partial(self.remove_graphic, graphic)) + + self._col_counter = new_col_ix + self._row_counter = new_row_ix + + def _reset_mesh_dims(self): + bbox = self._legend_items_group.get_world_bounding_box() + + width, height, _ = bbox.ptp(axis=0) + + self._mesh.geometry.positions.data[mesh_masks.x_right] = width + 7 + self._mesh.geometry.positions.data[mesh_masks.x_left] = -5 + self._mesh.geometry.positions.data[mesh_masks.y_bottom] = 0 + self._mesh.geometry.positions.data[mesh_masks.y_bottom] = -height - 3 + self._mesh.geometry.positions.update_range() + + def remove_graphic(self, graphic: Graphic): + self._graphics.remove(graphic) + legend_item = self._items.pop(graphic.loc) + self._legend_items_group.remove(legend_item.world_object) + self._reset_item_positions() + + def _reset_item_positions(self): + for i, (graphic_loc, legend_item) in enumerate(self._items.items()): + y_pos = i * -10 + legend_item.world_object.world.y = y_pos + + self._reset_mesh_dims() + + def reorder(self, labels: Iterable[str]): + all_labels = [legend_item.label for legend_item in self._items.values()] + + if not set(labels) == set(all_labels): + raise ValueError("Must pass all existing legend labels") + + new_items = OrderedDict() + + for label in labels: + for graphic_loc, legend_item in self._items.items(): + if label == legend_item.label: + new_items[graphic_loc] = self._items.pop(graphic_loc) + break + + self._items = new_items + self._reset_item_positions() + + def _pointer_down(self, ev): + self._last_position = self._plot_area.map_screen_to_world(ev) + self._initial_controller_state = self._plot_area.controller.enabled + + def _pointer_move(self, ev): + if self._last_position is None: + return + + self._plot_area.controller.enabled = False + + world_pos = self._plot_area.map_screen_to_world(ev) + + # outside viewport + if world_pos is None: + return + + delta = world_pos - self._last_position + + self.world_object.world.x = self.world_object.world.x + delta[0] + self.world_object.world.y = self.world_object.world.y + delta[1] + + self._last_position = world_pos + + self._plot_area.controller.enabled = self._initial_controller_state + + def _pointer_up(self, ev): + self._last_position = None + if self._initial_controller_state is not None: + self._plot_area.controller.enabled = self._initial_controller_state + + def __getitem__(self, graphic: Graphic) -> LegendItem: + if not isinstance(graphic, Graphic): + raise TypeError("Must index Legend with Graphics") + + if graphic.loc not in self._items.keys(): + raise KeyError("Graphic not in legend") + + return self._items[graphic.loc] diff --git a/fastplotlib/utils/mesh_masks.py b/fastplotlib/utils/mesh_masks.py new file mode 100644 index 000000000..600e5ab6d --- /dev/null +++ b/fastplotlib/utils/mesh_masks.py @@ -0,0 +1,128 @@ +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, + ] +) + +x_right = (x_right, 0) +x_left = (x_left, 0) +y_top = (y_top, 1) +y_bottom = (y_bottom, 1) \ No newline at end of file From 0fbc7d44da270f67d8d49b0da1ad626a82e0b815 Mon Sep 17 00:00:00 2001 From: Kushal Kolar Date: Mon, 4 Mar 2024 06:01:04 -0500 Subject: [PATCH 18/66] use pytest<8 until things catch up (#419) --- setup.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index 8e8977b57..1d274fc0a 100644 --- a/setup.py +++ b/setup.py @@ -33,7 +33,7 @@ "tests": [ - "pytest", + "pytest<8.0.0", "nbmake", "scipy", "imageio[pyav]", @@ -47,7 +47,7 @@ "tests-desktop": [ - "pytest", + "pytest<8.0.0", "scipy", "imageio", "scikit-learn", From 30cf1fc10371b2b74c280804aefbdc7f32852920 Mon Sep 17 00:00:00 2001 From: Kushal Kolar Date: Mon, 4 Mar 2024 06:16:35 -0500 Subject: [PATCH 19/66] more detailed contrib docs (#418) --- CONTRIBUTING.md | 171 +++++++++++++++++++++++++++++- fastplotlib/layouts/_plot_area.py | 23 +++- fastplotlib/layouts/_subplot.py | 3 +- 3 files changed, 191 insertions(+), 6 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 23810b956..84114c61f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -2,7 +2,7 @@ Contributions are welcome! :smile: -## Instructions +## Installation 1. Fork the repo to your own GitHub account, click the "Fork" button at the top: @@ -53,3 +53,172 @@ git push origin my-new-feature-branch ``` 4. Finally make a **draft** PR against the `main` branch. When you think the PR is ready, mark it for review to trigger tests using our CI pipeline. If you need to make changes, please set the PR to a draft when pushing further commits until it's ready for review scion. We will get back to your with any further suggestions! + +## How fastplotlib works + +Fastplotlib uses the [`pygfx`](https://github.com/pygfx/pygfx) rendering engine to give users a high-level scientific +plotting library. Some degree of familiarity with [`pygfx`](https://github.com/pygfx/pygfx) or rendering engines may +be useful depending on the type of contribution you're working on. + +There are currently 2 major subpackages within `fastplotlib`, `layouts` and `graphics`. The two user-facing public +classes within `layouts` are `Plot` and `GridPlot`. A user is intended to create either a `Plot` or `GridPlot`, and +then add *Graphics* to that layout, such as an `ImageGraphic`, `LineGraphic`, etc. + +### Graphics + +A `Graphic` is something that can be added to a `PlotArea` (described in detail in a later section). All the various +fastplotlib graphics, such as `ImageGraphic`, `ScatterGraphic`, etc. inherit from the `Graphic` base class in +`fastplotlib/graphics/_base.py`. It has a few properties that mostly wrap `pygfx` `WorldObject` properties and transforms. +These might change in the future (ex. `Graphic.position_x` etc.). + +All graphics can be given a string name for the user's convenience. This allows graphics to be easily accessed from +plots, ex: `plot["some_image"]`. + +All graphics contain a `world_object` property which is just the `pygfx.WorldObject` that this graphic uses. Fastplotlib +keeps a *private* global dictionary of all `WorldObject` instances and users are only given a weakref proxy to this world object. +This is due to garbage collection. This may be quite complicated for beginners, for more details see this PR: https://github.com/fastplotlib/fastplotlib/pull/160 . +If you are curious or have more questions on garbage collection in fastplotlib you're welcome to post an issue :D. + +#### Graphic Features + +There is one important thing that `fastplotlib` uses which we call "graphic features". +The "graphic features" subpackage can be found at `fastplotlib/graphics/_features`. As we can see this +is a private subpackage and never meant to be accessible to users. In `fastplotlib` "graphic features" are the various +aspects of a graphic that the user can change. Users can also run callbacks whenever a graphic feature changes. + +##### LineGraphic + +For example let's look at `LineGraphic` in `fastplotlib/graphics/line.py`. Every graphic has a class variable called +`feature_events` which is a set of all graphic features. It has the following graphic features: "data", "colors", "cmap", "thickness", "present". + +Now look at the constructor for `LineGraphic`, it first creates an instance of `PointsDataFeature`. This is basically a +class that wraps the positions buffer, the vertex positions that define the line, and provides additional useful functionality. +For example, every time that the `data` is changed event handlers will be called (if any event handlers are registered). + +`ColorFeature`behaves similarly, but it can perform additional parsing that can create the colors buffer from different forms of user input. For example if a user runs: +`line_graphic.colors = "blue"`, then `ColorFeature.__setitem__()` will create a buffer that corresponds to what `pygfx.Color` thinks is "blue". +Users can also take advantage of fancy indexing, ex: `line_graphics.colors[bool_array] = "red"` :smile: + +`LineGraphic` also has a `CmapFeature`, this is a subclass of `ColorFeature` which can parse colormaps, for example: +`line_graphic.cmap = "jet"` or even `line_graphic.cmap[50:] = "viridis"`. + +`LineGraphic` also has `ThicknessFeature` which is pretty simple, `PresentFeature` which indicates if a graphic is +currently in the scene, and `DeletedFeature` which is useful if you need callbacks to indicate that the graphic has been +deleted (for example, removing references to a graphic from a legend). + +Other graphics have graphic features that are relevant to them, for example `ImageGraphic` has a `cmap` feature which is +unique to images or heatmaps. + +#### Selectors + +Selectors are a fairly new subpackage at `fastplotlib/graphics/selectors` which is likely to change significantly +after https://github.com/pygfx/pygfx/pull/665 . This subpackage contains selection tools, such as line selectors +(horizontal or vertical lines that can be moved), linear region selectors, and a primitive polygon drawing selection tool. +All selector tools inherit from `BaseSelector` in `graphics/selectors/_base_selector.py` but this is likely to change +after the aforementioned `Input` class PR in `pygfx` and after https://github.com/fastplotlib/fastplotlib/pull/413 . + +### Layouts + +#### PlotArea + +This is the main base class within layouts. Every kind of "plot area", whether it's a single `Plot`, subplots within a +`GridPlot`, or `Dock` area, use `PlotArea` in some way. + +`PlotArea` has the following key properties that allow it to be a "plot area" that can be used to view graphical objects: + +* scene - instance of `pygfx.Scene` +* canvas - instance of `WgpuCanvas` +* renderer - instance of `pygfx.WgpuRenderer` +* viewport - instance of `pygfx.Viewport` +* camera - instance of `pygfx.PerspectiveCamera`, we always just use `PerspectiveCamera` and just set `camera.fov = 0` for orthographic projections +* controller - instance of `pygfx.Controller` + +Abstract method that must be implemented in subclasses: + +* get_rect - musut return [x, y, width, height] that defines the viewport rect for this `PlotArea` + +Properties specifically used by subplots in a gridplot: + +* parent - A parent if relevant, used by individual `Subplots` in `GridPlot`, and by `Dock` which are "docked" subplots at the edges of a subplot. +* position - if a subplot within a gridplot, it is the position of this subplot within the `GridPlot` + +Other important properties: + +* graphics - a tuple of weakref proxies to all `Graphics` within this `PlotArea`, users are only given weakref proxies to `Graphic` objects, all `Graphic` objects are stored in a private global dict. +* selectors - a tuple of weakref proxies to all selectors within this `PlotArea` +* legend - a tuple of weakref proxies to all legend graphics within this `PlotArea` +* name - plot areas are allowed to have names that the user can use for their convenience + +Important methods: + +* add_graphic - add a `Graphic` to the `PlotArea`, append to the end of the `PlotArea._graphics` list +* insert_graphic - insert a `Graphic` to the `PlotArea`, insert to a specific position of the `PlotArea._graphics` list +* remove_graphic - remove a graphic from the `Scene`, **does not delete it** +* delete_graphic - delete a graphic from the `PlotArea`, performs garbage collection +* clear - deletes all graphics from the `PlotArea` +* center_graphic - center camera w.r.t. a `Graphic` +* center_scene - center camera w.r.t. entire `Scene` +* auto_scale - Auto-scale the camera w.r.t to the `Scene` + +In addition, `PlotArea` supports `__getitem__`, so you can do: `plot_area["graphic_name"]` to retrieve a `Graphic` by +name :smile: + +You can also check if a `PlotArea` has certain graphics, ex: `"some_image_name" in plot_area`, or `graphic_instance in plot_area` + +#### Subplot + +This class inherits from `PlotArea` and `GraphicMethodsMixin`. + +`GraphicMethodsMixin` is a simple class that just has all the `add_` methods. It is autogenerated by a utility script like this: + +```bash +python fastplotlib/utils/generate_add_methods.py +``` + +Each `add_` method basically creates an instance of `Graphic`, adds it to the `Subplot`, and returns a weakref +proxy to the `Graphic`. + +Subplot has one property that is not in `PlotArea`: + +* docks: a `dict` of `PlotAreas` which are located at the "top", "right", "left", and "bottom" edges of a `Subplot`. By default their size is `0`. They are useful for putting things like histogram LUT tools. + +The key method in `Subplot` is an implementation of `get_rect` that returns the viewport rect for this subplot. + +#### Plot, GridPlot, and Frame + +Now that we have understood `PlotArea` and `Subplot` we need a way for the user to create either single plots or gridplots +and display them! + +There's one more class to talk about, `Frame`. This is a class that "frames" a `Plot` or `GridPlot`. Depending on +whether the plot's `Canvas` is a Qt or jupyter canvas, `Frame.show()` will create a plot toolbar and place this toolbar +below the `Canvas`. If using a glfw canvas it just returns the canvas. + +`Plot` and `GridPlot` both inherit from `Frame` which gives them `show()`. `Plot` is just a single `Subplot` with the +addition of `Frame`. `GridPlot.__init__` basically does a lot of parsing of user arguments to determine how to create +the subplots. All subplots within a `GridPlot` share the same canvas and use different viewports to create the subplots. + +## Running tests + +The CI pipeline for a plotting library is is supposed to produce things that "look visually correct" is a bit more +complicated than the CI pipeline for most libraries. + +Our CI pipeline is modelled after `pygfx`. Basically, a bunch of examples exist within the `examples` dir. Each of these +examples are run and a screenshot of the canvas is taken and compared with a ground-truth screenshot that we have +manually inspected. Screenshots are stored using `git-lfs`. + +At the moment these tests will produce slightly different imperceptible (to a human) results on different hardware, but +nonetheless the image arrays will have a small difference. Because of this, we only run tests on GitHub actions. There +is a specific actions workflow that only generate screenshots and doesn't run tests which is run specifically to create +ground-truth screenshot on GitHub actions servers to use for test. + +If your contribution modifies a ground-truth test screenshot the general workflow is like this, if you have questions +don't hesitate to ask us :smile: + +1. Create a PR with your code changes, **do not upload any new/modified test screenshots" +1. See if the CI failed, if so it will indicate the specific examples that have failed. +1. Go to the details page for the "Screenshots / Regenerate" workflow. Click on "Summary" in the top left, and download +the build artifact which is named "screenshots". This is a zip file of generated ground-truth screenshots. +1. Visually inspect the specific new/modified screenshot that corresponds to your code change. Make sure it looks like, +past a copy of it in the PR as a reply +1. Copy over the new/modified screenshots to your local repo. +1. Add and commit the new/modified screenshot files, and push. The tests should now pass. diff --git a/fastplotlib/layouts/_plot_area.py b/fastplotlib/layouts/_plot_area.py index cc7526398..d4fc62e1f 100644 --- a/fastplotlib/layouts/_plot_area.py +++ b/fastplotlib/layouts/_plot_area.py @@ -108,7 +108,7 @@ def __init__( # several read-only properties @property def parent(self): - """The parent PlotArea""" + """A parent if relevant, used by individual Subplots in GridPlot""" return self._parent @property @@ -237,16 +237,31 @@ def legends(self) -> Tuple[Legend, ...]: return tuple(proxies) @property - def name(self) -> Any: + def name(self) -> str: """The name of this plot area""" return self._name @name.setter - def name(self, name: Any): + def name(self, name: str): + if name is None: + self._name = None + return + + if not isinstance(name, str): + raise TypeError("PlotArea `name` must be of type ") self._name = name def get_rect(self) -> Tuple[float, float, float, float]: - """allows setting the region occupied by the viewport w.r.t. the parent""" + """ + Returns the viewport rect to define the rectangle + occupied by the viewport w.r.t. the Canvas. + + If this is a subplot within a GridPlot, it returns the rectangle + for only this subplot w.r.t. the parent canvas. + + Must return: [x_pos, y_pos, width_viewport, height_viewport] + + """ raise NotImplementedError("Must be implemented in subclass") def map_screen_to_world( diff --git a/fastplotlib/layouts/_subplot.py b/fastplotlib/layouts/_subplot.py index e9eae7603..e776fddb6 100644 --- a/fastplotlib/layouts/_subplot.py +++ b/fastplotlib/layouts/_subplot.py @@ -90,6 +90,8 @@ def __init__( self._grid: pygfx.GridHelper = pygfx.GridHelper(size=100, thickness=1) + self._title_graphic: TextGraphic = None + super(Subplot, self).__init__( parent=parent, position=position, @@ -107,7 +109,6 @@ def __init__( self.docks[pos] = dv self.children.append(dv) - self._title_graphic: TextGraphic = None if self.name is not None: self.set_title(self.name) From 835f0c56717a175f9088f3c53b18f05e634ebb7a Mon Sep 17 00:00:00 2001 From: Kushal Kolar Date: Mon, 4 Mar 2024 07:11:41 -0500 Subject: [PATCH 20/66] ruff format everything (#421) * ruff format everything * dont use formatting on GraphicMethodsMixin since it is autogenerated --- fastplotlib/__init__.py | 4 +- fastplotlib/graphics/_base.py | 2 +- fastplotlib/graphics/_features/__init__.py | 7 ++- fastplotlib/graphics/_features/_colors.py | 10 +++- fastplotlib/graphics/_features/_sizes.py | 16 +++-- fastplotlib/graphics/image.py | 6 +- fastplotlib/graphics/scatter.py | 4 +- .../graphics/selectors/_base_selector.py | 2 +- fastplotlib/graphics/selectors/_linear.py | 19 +++--- .../graphics/selectors/_linear_region.py | 20 +++---- .../graphics/selectors/_mesh_positions.py | 1 - fastplotlib/graphics/selectors/_polygon.py | 26 +++++---- fastplotlib/graphics/selectors/_sync.py | 4 +- fastplotlib/graphics/text.py | 3 +- fastplotlib/layouts/_defaults.py | 2 - fastplotlib/layouts/_frame/_frame.py | 24 ++++---- .../layouts/_frame/_ipywidget_toolbar.py | 19 ++++-- fastplotlib/layouts/_frame/_jupyter_output.py | 13 +++-- fastplotlib/layouts/_frame/_qt_output.py | 9 +-- fastplotlib/layouts/_frame/_qt_toolbar.py | 30 +++++++--- fastplotlib/layouts/_gridplot.py | 28 ++++++--- fastplotlib/layouts/_plot_area.py | 38 ++++++------ fastplotlib/legends/legend.py | 58 ++++++++++--------- fastplotlib/utils/__init__.py | 4 +- fastplotlib/utils/_gpu_info.py | 16 ++--- fastplotlib/utils/generate_add_methods.py | 39 +++++++------ fastplotlib/utils/mesh_masks.py | 2 +- fastplotlib/widgets/histogram_lut.py | 47 +++++++++------ fastplotlib/widgets/image.py | 27 +++++---- 29 files changed, 275 insertions(+), 205 deletions(-) diff --git a/fastplotlib/__init__.py b/fastplotlib/__init__.py index ca872f4e4..f0f3cc7b1 100644 --- a/fastplotlib/__init__.py +++ b/fastplotlib/__init__.py @@ -19,9 +19,7 @@ adapters = [a.request_adapter_info() for a in enumerate_adapters()] if len(adapters) < 1: - raise IndexError( - "No WGPU adapters found, fastplotlib will not work." - ) + raise IndexError("No WGPU adapters found, fastplotlib will not work.") with open(Path(__file__).parent.joinpath("VERSION"), "r") as f: __version__ = f.read().split("\n")[0] diff --git a/fastplotlib/graphics/_base.py b/fastplotlib/graphics/_base.py index 4f342faa2..91ccb143e 100644 --- a/fastplotlib/graphics/_base.py +++ b/fastplotlib/graphics/_base.py @@ -315,7 +315,7 @@ def link( feature=feature, new_data=new_data, callback=callback, - bidirectional=False # else infinite recursion, otherwise target will call + bidirectional=False, # else infinite recursion, otherwise target will call # this instance .link(), and then it will happen again etc. ) diff --git a/fastplotlib/graphics/_features/__init__.py b/fastplotlib/graphics/_features/__init__.py index a1769b010..fb25db287 100644 --- a/fastplotlib/graphics/_features/__init__.py +++ b/fastplotlib/graphics/_features/__init__.py @@ -3,7 +3,12 @@ from ._sizes import PointsSizesFeature from ._present import PresentFeature from ._thickness import ThicknessFeature -from ._base import GraphicFeature, GraphicFeatureIndexable, FeatureEvent, to_gpu_supported_dtype +from ._base import ( + GraphicFeature, + GraphicFeatureIndexable, + FeatureEvent, + to_gpu_supported_dtype, +) from ._selection_features import LinearSelectionFeature, LinearRegionSelectionFeature from ._deleted import Deleted diff --git a/fastplotlib/graphics/_features/_colors.py b/fastplotlib/graphics/_features/_colors.py index b6723b34b..ad9e673ef 100644 --- a/fastplotlib/graphics/_features/_colors.py +++ b/fastplotlib/graphics/_features/_colors.py @@ -1,7 +1,13 @@ import numpy as np import pygfx -from ...utils import make_colors, get_cmap_texture, make_pygfx_colors, parse_cmap_values, quick_min_max +from ...utils import ( + make_colors, + get_cmap_texture, + make_pygfx_colors, + parse_cmap_values, + quick_min_max, +) from ._base import ( GraphicFeature, GraphicFeatureIndexable, @@ -426,4 +432,4 @@ def vmax(self) -> float: @vmax.setter def vmax(self, value: float): """Maximum contrast limit.""" - self._parent._material.clim = (self._parent._material.clim[0], value) \ No newline at end of file + self._parent._material.clim = (self._parent._material.clim[0], value) diff --git a/fastplotlib/graphics/_features/_sizes.py b/fastplotlib/graphics/_features/_sizes.py index e951064e4..8fceb8df3 100644 --- a/fastplotlib/graphics/_features/_sizes.py +++ b/fastplotlib/graphics/_features/_sizes.py @@ -34,12 +34,16 @@ def __getitem__(self, item): def _fix_sizes(self, sizes, parent): graphic_type = parent.__class__.__name__ - + n_datapoints = parent.data().shape[0] if not isinstance(sizes, (list, tuple, np.ndarray)): - sizes = np.full(n_datapoints, sizes, dtype=np.float32) # force it into a float to avoid weird gpu errors - elif not isinstance(sizes, np.ndarray): # if it's not a ndarray already, make it one - sizes = np.array(sizes, dtype=np.float32) # read it in as a numpy.float32 + sizes = np.full( + n_datapoints, sizes, dtype=np.float32 + ) # force it into a float to avoid weird gpu errors + elif not isinstance( + sizes, np.ndarray + ): # if it's not a ndarray already, make it one + sizes = np.array(sizes, dtype=np.float32) # read it in as a numpy.float32 if (sizes.ndim != 1) or (sizes.size != parent.data().shape[0]): raise ValueError( f"sequence of `sizes` must be 1 dimensional with " @@ -49,7 +53,9 @@ def _fix_sizes(self, sizes, parent): sizes = to_gpu_supported_dtype(sizes) if any(s < 0 for s in sizes): - raise ValueError("All sizes must be positive numbers greater than or equal to 0.0.") + raise ValueError( + "All sizes must be positive numbers greater than or equal to 0.0." + ) if sizes.ndim == 1: if graphic_type == "ScatterGraphic": diff --git a/fastplotlib/graphics/image.py b/fastplotlib/graphics/image.py index 3d629c10f..1cad33f22 100644 --- a/fastplotlib/graphics/image.py +++ b/fastplotlib/graphics/image.py @@ -345,11 +345,7 @@ def col_chunk_index(self, index: int): class HeatmapGraphic(Graphic, Interaction, _AddSelectorsMixin): - feature_events = { - "data", - "cmap", - "present" - } + feature_events = {"data", "cmap", "present"} def __init__( self, diff --git a/fastplotlib/graphics/scatter.py b/fastplotlib/graphics/scatter.py index 63689fad9..f6104aeb7 100644 --- a/fastplotlib/graphics/scatter.py +++ b/fastplotlib/graphics/scatter.py @@ -90,7 +90,9 @@ def __init__( super(ScatterGraphic, self).__init__(*args, **kwargs) world_object = pygfx.Points( - pygfx.Geometry(positions=self.data(), sizes=self.sizes(), colors=self.colors()), + pygfx.Geometry( + positions=self.data(), sizes=self.sizes(), colors=self.colors() + ), material=pygfx.PointsMaterial(color_mode="vertex", vertex_sizes=True), ) diff --git a/fastplotlib/graphics/selectors/_base_selector.py b/fastplotlib/graphics/selectors/_base_selector.py index e6796f270..6c1f8c6ae 100644 --- a/fastplotlib/graphics/selectors/_base_selector.py +++ b/fastplotlib/graphics/selectors/_base_selector.py @@ -44,7 +44,7 @@ def __init__( hover_responsive: Tuple[WorldObject, ...] = None, arrow_keys_modifier: str = None, axis: str = None, - name: str = None + name: str = None, ): if edges is None: edges = tuple() diff --git a/fastplotlib/graphics/selectors/_linear.py b/fastplotlib/graphics/selectors/_linear.py index 16ccab1b4..ff617c5e3 100644 --- a/fastplotlib/graphics/selectors/_linear.py +++ b/fastplotlib/graphics/selectors/_linear.py @@ -28,10 +28,10 @@ def limits(self, values: Tuple[float, float]): # check that `values` is an iterable of two real numbers # using `Real` here allows it to work with builtin `int` and `float` types, and numpy scaler types if len(values) != 2 or not all(map(lambda v: isinstance(v, Real), values)): - raise TypeError( - "limits must be an iterable of two numeric values" - ) - self._limits = tuple(map(round, values)) # if values are close to zero things get weird so round them + raise TypeError("limits must be an iterable of two numeric values") + self._limits = tuple( + map(round, values) + ) # if values are close to zero things get weird so round them self.selection._limits = self._limits # TODO: make `selection` arg in graphics data space not world space @@ -267,11 +267,7 @@ def make_ipywidget_slider(self, kind: str = "IntSlider", **kwargs): return slider - def add_ipywidget_handler( - self, - widget, - step: Union[int, float] = None - ): + def add_ipywidget_handler(self, widget, step: Union[int, float] = None): """ Bidirectionally connect events with a ipywidget slider @@ -285,7 +281,10 @@ def add_ipywidget_handler( """ - if not isinstance(widget, (ipywidgets.IntSlider, ipywidgets.FloatSlider, ipywidgets.FloatLogSlider)): + if not isinstance( + widget, + (ipywidgets.IntSlider, ipywidgets.FloatSlider, ipywidgets.FloatLogSlider), + ): raise TypeError( f"`widget` must be one of: ipywidgets.IntSlider, ipywidgets.FloatSlider, or ipywidgets.FloatLogSlider\n" f"You have passed a: <{type(widget)}" diff --git a/fastplotlib/graphics/selectors/_linear_region.py b/fastplotlib/graphics/selectors/_linear_region.py index 2a7547d5b..4ffbd2cc2 100644 --- a/fastplotlib/graphics/selectors/_linear_region.py +++ b/fastplotlib/graphics/selectors/_linear_region.py @@ -27,10 +27,10 @@ def limits(self, values: Tuple[float, float]): # check that `values` is an iterable of two real numbers # using `Real` here allows it to work with builtin `int` and `float` types, and numpy scaler types if len(values) != 2 or not all(map(lambda v: isinstance(v, Real), values)): - raise TypeError( - "limits must be an iterable of two numeric values" - ) - self._limits = tuple(map(round, values)) # if values are close to zero things get weird so round them + raise TypeError("limits must be an iterable of two numeric values") + self._limits = tuple( + map(round, values) + ) # if values are close to zero things get weird so round them self.selection._limits = self._limits def __init__( @@ -243,7 +243,7 @@ def __init__( hover_responsive=self.edges, arrow_keys_modifier=arrow_keys_modifier, axis=axis, - name=name + name=name, ) def get_selected_data( @@ -417,11 +417,7 @@ def make_ipywidget_slider(self, kind: str = "IntRangeSlider", **kwargs): return slider - def add_ipywidget_handler( - self, - widget, - step: Union[int, float] = None - ): + def add_ipywidget_handler(self, widget, step: Union[int, float] = None): """ Bidirectionally connect events with a ipywidget slider @@ -434,7 +430,9 @@ def add_ipywidget_handler( step size, if ``None`` 100 steps are created """ - if not isinstance(widget, (ipywidgets.IntRangeSlider, ipywidgets.FloatRangeSlider)): + if not isinstance( + widget, (ipywidgets.IntRangeSlider, ipywidgets.FloatRangeSlider) + ): raise TypeError( f"`widget` must be one of: ipywidgets.IntRangeSlider or ipywidgets.FloatRangeSlider\n" f"You have passed a: <{type(widget)}" diff --git a/fastplotlib/graphics/selectors/_mesh_positions.py b/fastplotlib/graphics/selectors/_mesh_positions.py index 07ff60498..a22b22b17 100644 --- a/fastplotlib/graphics/selectors/_mesh_positions.py +++ b/fastplotlib/graphics/selectors/_mesh_positions.py @@ -1,2 +1 @@ import numpy as np - diff --git a/fastplotlib/graphics/selectors/_polygon.py b/fastplotlib/graphics/selectors/_polygon.py index b347da0f4..44d378329 100644 --- a/fastplotlib/graphics/selectors/_polygon.py +++ b/fastplotlib/graphics/selectors/_polygon.py @@ -16,7 +16,6 @@ def __init__( parent: Graphic = None, name: str = None, ): - self.parent = parent group = pygfx.Group() @@ -47,7 +46,9 @@ def _add_plot_area_hook(self, plot_area): 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") + 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") @@ -69,7 +70,9 @@ def _add_segment(self, ev): 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)) + material=pygfx.LineMaterial( + thickness=self.edge_width, color=pygfx.Color(self.edge_color) + ), ) self.world_object.add(new_line) @@ -86,7 +89,9 @@ def _move_segment_endpoint(self, ev): 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.data[1] = np.array( + [world_pos] + ).astype(np.float32) self.world_object.children[-1].geometry.positions.update_range() def _finish_segment(self, ev): @@ -114,14 +119,15 @@ def _finish_polygon(self, ev): return # make new line to connect first and last vertices - data = np.vstack([ - world_pos, - self.world_object.children[0].geometry.positions.data[0] - ]) + 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)) + material=pygfx.LineMaterial( + thickness=self.edge_width, color=pygfx.Color(self.edge_color) + ), ) self.world_object.add(new_line) @@ -130,7 +136,7 @@ def _finish_polygon(self, ev): self._add_segment: "click", self._move_segment_endpoint: "pointer_move", self._finish_segment: "click", - self._finish_polygon: "double_click" + self._finish_polygon: "double_click", } for handler, event in handlers.items(): diff --git a/fastplotlib/graphics/selectors/_sync.py b/fastplotlib/graphics/selectors/_sync.py index 9414a2e20..ce903aab8 100644 --- a/fastplotlib/graphics/selectors/_sync.py +++ b/fastplotlib/graphics/selectors/_sync.py @@ -3,7 +3,9 @@ class Synchronizer: - def __init__(self, *selectors: LinearSelector, key_bind: Union[str, None] = "Shift"): + def __init__( + self, *selectors: LinearSelector, key_bind: Union[str, None] = "Shift" + ): """ Synchronize the movement of `Selectors`. Selectors will move in sync only when the selected `"key_bind"` is used during the mouse movement event. Valid key binds are: ``"Control"``, ``"Shift"`` and ``"Alt"``. diff --git a/fastplotlib/graphics/text.py b/fastplotlib/graphics/text.py index a159d9560..a8a873287 100644 --- a/fastplotlib/graphics/text.py +++ b/fastplotlib/graphics/text.py @@ -17,7 +17,7 @@ def __init__( screen_space: bool = True, anchor: str = "middle-center", *args, - **kwargs + **kwargs, ): """ Create a text Graphic @@ -144,4 +144,3 @@ def outline_color(self, color: Union[str, np.ndarray]): raise ValueError("Outline color must be of type str or np.ndarray") self.world_object.material.outline_color = color - diff --git a/fastplotlib/layouts/_defaults.py b/fastplotlib/layouts/_defaults.py index b28b04f64..8b1378917 100644 --- a/fastplotlib/layouts/_defaults.py +++ b/fastplotlib/layouts/_defaults.py @@ -1,3 +1 @@ - - diff --git a/fastplotlib/layouts/_frame/_frame.py b/fastplotlib/layouts/_frame/_frame.py index abd79759e..2b76b8124 100644 --- a/fastplotlib/layouts/_frame/_frame.py +++ b/fastplotlib/layouts/_frame/_frame.py @@ -27,15 +27,14 @@ def __call__(self, *args, **kwargs): JupyterOutputContext = UnavailableOutputContext( "Jupyter", "You must install fastplotlib using the `'notebook'` option to use this context:\n" - 'pip install "fastplotlib[notebook]"' + 'pip install "fastplotlib[notebook]"', ) if CANVAS_OPTIONS_AVAILABLE["qt"]: from ._qt_output import QOutputContext else: QtOutput = UnavailableOutputContext( - "Qt", - "You must install `PyQt6` to use this output context" + "Qt", "You must install `PyQt6` to use this output context" ) @@ -45,6 +44,7 @@ class Frame: Gives them their `show()` call that returns the appropriate output context. """ + def __init__(self): self._output = None @@ -83,13 +83,13 @@ def start_render(self): self.canvas.set_logical_size(*self._starting_size) def show( - self, - autoscale: bool = True, - maintain_aspect: bool = None, - toolbar: bool = True, - sidecar: bool = False, - sidecar_kwargs: dict = None, - add_widgets: list = None, + self, + autoscale: bool = True, + maintain_aspect: bool = None, + toolbar: bool = True, + sidecar: bool = False, + sidecar_kwargs: dict = None, + add_widgets: list = None, ): """ Begins the rendering event loop and shows the plot in the desired output context (jupyter, qt or glfw). @@ -168,9 +168,7 @@ def show( elif self.canvas.__class__.__name__ == "QWgpuCanvas": self._output = QOutputContext( - frame=self, - make_toolbar=toolbar, - add_widgets=add_widgets + frame=self, make_toolbar=toolbar, add_widgets=add_widgets ) else: # assume GLFW, the output context is just the canvas diff --git a/fastplotlib/layouts/_frame/_ipywidget_toolbar.py b/fastplotlib/layouts/_frame/_ipywidget_toolbar.py index 552ee9827..72976a445 100644 --- a/fastplotlib/layouts/_frame/_ipywidget_toolbar.py +++ b/fastplotlib/layouts/_frame/_ipywidget_toolbar.py @@ -28,6 +28,7 @@ class IpywidgetToolBar(HBox, ToolBar): """Basic toolbar using ipywidgets""" + def __init__(self, plot): ToolBar.__init__(self, plot) @@ -82,7 +83,7 @@ def __init__(self, plot): disabled=False, icon="draw-polygon", layout=Layout(width="auto"), - tooltip="add PolygonSelector" + tooltip="add PolygonSelector", ) widgets = [ @@ -109,7 +110,9 @@ def __init__(self, plot): widgets.append(image) if hasattr(self.plot, "_subplots"): - positions = list(product(range(self.plot.shape[0]), range(self.plot.shape[1]))) + positions = list( + product(range(self.plot.shape[0]), range(self.plot.shape[1])) + ) values = list() for pos in positions: if self.plot[pos].name is not None: @@ -150,7 +153,9 @@ def _get_subplot_dropdown_value(self) -> str: return self._dropdown.value def auto_scale_handler(self, obj): - self.current_subplot.auto_scale(maintain_aspect=self.current_subplot.camera.maintain_aspect) + self.current_subplot.auto_scale( + maintain_aspect=self.current_subplot.camera.maintain_aspect + ) def center_scene_handler(self, obj): self.current_subplot.center_scene() @@ -230,7 +235,7 @@ def __init__(self, iw): icon="adjust", description="reset", layout=Layout(width="auto"), - tooltip="reset vmin/vmax and reset histogram using current frame" + tooltip="reset vmin/vmax and reset histogram using current frame", ) self.sliders: Dict[str, IntSlider] = dict() @@ -250,7 +255,9 @@ def __init__(self, iw): orientation="horizontal", ) - slider.observe(partial(self.iw._slider_value_changed, dim), names="value") + slider.observe( + partial(self.iw._slider_value_changed, dim), names="value" + ) self.sliders[dim] = slider @@ -287,7 +294,7 @@ def __init__(self, iw): self.reset_vminvmax_hlut_button, self.play_button, self.step_size_setter, - self.speed_text + self.speed_text, ] self.play_button.interval = 10 diff --git a/fastplotlib/layouts/_frame/_jupyter_output.py b/fastplotlib/layouts/_frame/_jupyter_output.py index 25f5e2a2e..786041bcf 100644 --- a/fastplotlib/layouts/_frame/_jupyter_output.py +++ b/fastplotlib/layouts/_frame/_jupyter_output.py @@ -13,13 +13,14 @@ class JupyterOutputContext(VBox): Basically vstacks plot canvas, toolbar, and other widgets. Uses sidecar if desired. """ + def __init__( - self, - frame, - make_toolbar: bool, - use_sidecar: bool, - sidecar_kwargs: dict, - add_widgets: List[Widget], + self, + frame, + make_toolbar: bool, + use_sidecar: bool, + sidecar_kwargs: dict, + add_widgets: List[Widget], ): """ diff --git a/fastplotlib/layouts/_frame/_qt_output.py b/fastplotlib/layouts/_frame/_qt_output.py index b4c7cffd9..e8be2d050 100644 --- a/fastplotlib/layouts/_frame/_qt_output.py +++ b/fastplotlib/layouts/_frame/_qt_output.py @@ -9,11 +9,12 @@ class QOutputContext(QtWidgets.QWidget): Basically vstacks plot canvas, toolbar, and other widgets. """ + def __init__( - self, - frame, - make_toolbar, - add_widgets, + self, + frame, + make_toolbar, + add_widgets, ): """ diff --git a/fastplotlib/layouts/_frame/_qt_toolbar.py b/fastplotlib/layouts/_frame/_qt_toolbar.py index 9d4e0b48f..4ee073701 100644 --- a/fastplotlib/layouts/_frame/_qt_toolbar.py +++ b/fastplotlib/layouts/_frame/_qt_toolbar.py @@ -11,8 +11,11 @@ from ._qtoolbar_template import Ui_QToolbar -class QToolbar(ToolBar, QtWidgets.QWidget): # inheritance order MUST be Toolbar first, QWidget second! Else breaks +class QToolbar( + ToolBar, QtWidgets.QWidget +): # inheritance order MUST be Toolbar first, QWidget second! Else breaks """Toolbar for Qt context""" + def __init__(self, output_context, plot): QtWidgets.QWidget.__init__(self, parent=output_context) ToolBar.__init__(self, plot) @@ -49,7 +52,9 @@ def __init__(self, output_context, plot): self.setMaximumHeight(35) # set the initial values for buttons - self.ui.maintain_aspect_button.setChecked(self.current_subplot.camera.maintain_aspect) + self.ui.maintain_aspect_button.setChecked( + self.current_subplot.camera.maintain_aspect + ) self.ui.panzoom_button.setChecked(self.current_subplot.controller.enabled) if copysign(1, self.current_subplot.camera.local.scale_y) == -1: @@ -70,7 +75,9 @@ def update_current_subplot(self, ev): # set buttons w.r.t. current subplot self.ui.panzoom_button.setChecked(subplot.controller.enabled) - self.ui.maintain_aspect_button.setChecked(subplot.camera.maintain_aspect) + self.ui.maintain_aspect_button.setChecked( + subplot.camera.maintain_aspect + ) if copysign(1, subplot.camera.local.scale_y) == -1: self.ui.y_direction_button.setText("v") @@ -81,7 +88,9 @@ def _get_subplot_dropdown_value(self) -> str: return self.ui.current_subplot.text() def auto_scale_handler(self, *args): - self.current_subplot.auto_scale(maintain_aspect=self.current_subplot.camera.maintain_aspect) + self.current_subplot.auto_scale( + maintain_aspect=self.current_subplot.camera.maintain_aspect + ) def center_scene_handler(self, *args): self.current_subplot.center_scene() @@ -128,6 +137,7 @@ class SliderInterface: This interface makes a QSlider behave somewhat like a ipywidget IntSlider, enough for ImageWidget to function. """ + def __init__(self, qslider): self.qslider = qslider @@ -176,7 +186,9 @@ def __init__(self, image_widget): self.reset_vmin_vmax_hlut_button = QtWidgets.QPushButton(self) self.reset_vmin_vmax_hlut_button.setText("reset histogram-lut") - self.reset_vmin_vmax_hlut_button.clicked.connect(self.image_widget.reset_vmin_vmax_frame) + self.reset_vmin_vmax_hlut_button.clicked.connect( + self.image_widget.reset_vmin_vmax_frame + ) hlayout_buttons.addWidget(self.reset_vmin_vmax_hlut_button) self.vlayout.addLayout(hlayout_buttons) @@ -187,7 +199,9 @@ def __init__(self, image_widget): if self.image_widget.ndim > 2: # create a slider, spinbox and dimension label for each dimension in the ImageWidget for dim in self.image_widget.slider_dims: - hlayout = QtWidgets.QHBoxLayout() # horizontal stack for label, slider, spinbox + hlayout = ( + QtWidgets.QHBoxLayout() + ) # horizontal stack for label, slider, spinbox # max value for current dimension max_val = self.image_widget._dims_max_bounds[dim] - 1 @@ -213,7 +227,9 @@ def __init__(self, image_widget): spinbox.valueChanged.connect(slider.setValue) # connect slider to change the index within the dimension - slider.valueChanged.connect(partial(self.image_widget._slider_value_changed, dim)) + slider.valueChanged.connect( + partial(self.image_widget._slider_value_changed, dim) + ) # slider dimension label slider_label = QtWidgets.QLabel(self) diff --git a/fastplotlib/layouts/_gridplot.py b/fastplotlib/layouts/_gridplot.py index 459aca5fd..98e5643f3 100644 --- a/fastplotlib/layouts/_gridplot.py +++ b/fastplotlib/layouts/_gridplot.py @@ -84,7 +84,9 @@ def __init__( if names is not None: if len(list(chain(*names))) != self.shape[0] * self.shape[1]: - raise ValueError("must provide same number of subplot `names` as specified by gridplot shape") + raise ValueError( + "must provide same number of subplot `names` as specified by gridplot shape" + ) self.names = to_array(names).reshape(self.shape) else: @@ -111,7 +113,9 @@ def __init__( if controller_ids is None: # individual controller for each subplot - controller_ids = np.arange(self.shape[0] * self.shape[1]).reshape(self.shape) + controller_ids = np.arange(self.shape[0] * self.shape[1]).reshape( + self.shape + ) elif isinstance(controller_ids, str): if controller_ids == "sync": @@ -129,7 +133,9 @@ def __init__( # list of str of subplot names, convert this to integer ids if all([isinstance(item, str) for item in ids_flat]): if self.names is None: - raise ValueError("must specify subplot `names` to use list of str for `controller_ids`") + raise ValueError( + "must specify subplot `names` to use list of str for `controller_ids`" + ) # make sure each controller_id str is a subplot name if not all([n in self.names for n in ids_flat]): @@ -148,7 +154,9 @@ def __init__( # set id based on subplot position for each synced sublist for i, sublist in enumerate(controller_ids): for name in sublist: - ids_init[self.names == name] = -(i + 1) # use negative numbers because why not + ids_init[self.names == name] = -( + i + 1 + ) # use negative numbers because why not controller_ids = ids_init @@ -163,11 +171,15 @@ def __init__( ) if controller_ids.shape != self.shape: - raise ValueError("Number of controller_ids does not match the number of subplots") + raise ValueError( + "Number of controller_ids does not match the number of subplots" + ) if controller_types is None: # `create_controller()` will auto-determine controller for each subplot based on defaults - controller_types = np.array(["default"] * self.shape[0] * self.shape[1]).reshape(self.shape) + controller_types = np.array( + ["default"] * self.shape[0] * self.shape[1] + ).reshape(self.shape) # validate controller types types_flat = list(chain(*controller_types)) @@ -180,7 +192,9 @@ def __init__( if controller_type is None: continue - if (controller_type not in valid_str) and (not isinstance(controller_type, valid_instances)): + if (controller_type not in valid_str) and ( + not isinstance(controller_type, valid_instances) + ): raise ValueError( f"You have passed an invalid controller type, valid controller_types arguments are:\n" f"{valid_str} or instances of {[c.__name__ for c in valid_instances]}" diff --git a/fastplotlib/layouts/_plot_area.py b/fastplotlib/layouts/_plot_area.py index d4fc62e1f..08a09baa7 100644 --- a/fastplotlib/layouts/_plot_area.py +++ b/fastplotlib/layouts/_plot_area.py @@ -63,7 +63,7 @@ def __init__( name: str, optional name this ``subplot`` or ``plot`` - + """ self._parent: PlotArea = parent @@ -163,9 +163,13 @@ def camera(self, new_camera: Union[str, pygfx.PerspectiveCamera]): self._camera.fov = 50 else: - raise ValueError("camera must be one of '2d', '3d' or a pygfx.PerspectiveCamera instance") + raise ValueError( + "camera must be one of '2d', '3d' or a pygfx.PerspectiveCamera instance" + ) else: - raise ValueError("camera must be one of '2d', '3d' or a pygfx.PerspectiveCamera instance") + raise ValueError( + "camera must be one of '2d', '3d' or a pygfx.PerspectiveCamera instance" + ) # in the future we can think about how to allow changing the controller @property @@ -188,9 +192,7 @@ def controller(self, new_controller: Union[str, pygfx.Controller]): for camera in cameras_list: new_controller.add_camera(camera) - new_controller.register_events( - self.viewport - ) + new_controller.register_events(self.viewport) # TODO: monkeypatch until we figure out a better # pygfx plans on refactoring viewports anyways @@ -246,7 +248,7 @@ def name(self, name: str): if name is None: self._name = None return - + if not isinstance(name, str): raise TypeError("PlotArea `name` must be of type ") self._name = name @@ -473,9 +475,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) @@ -484,9 +486,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) @@ -566,10 +568,10 @@ def center_scene(self, *, zoom: float = 1.35): camera.zoom = zoom def auto_scale( - self, - *, # since this is often used as an event handler, don't want to coerce maintain_aspect = True - maintain_aspect: Union[None, bool] = None, - zoom: float = 0.8 + self, + *, # since this is often used as an event handler, don't want to coerce maintain_aspect = True + maintain_aspect: Union[None, bool] = None, + zoom: float = 0.8, ): """ Auto-scale the camera w.r.t to the scene @@ -737,7 +739,7 @@ def __contains__(self, item: Union[str, Graphic]): return False raise TypeError("PlotArea `in` operator accepts only `Graphic` or `str` types") - + def __str__(self): if self.name is None: name = "unnamed" diff --git a/fastplotlib/legends/legend.py b/fastplotlib/legends/legend.py index e29665e0f..291c25ff3 100644 --- a/fastplotlib/legends/legend.py +++ b/fastplotlib/legends/legend.py @@ -13,9 +13,9 @@ class LegendItem: def __init__( - self, - label: str, - color: pygfx.Color, + self, + label: str, + color: pygfx.Color, ): """ @@ -31,11 +31,7 @@ def __init__( class LineLegendItem(LegendItem): def __init__( - self, - parent, - graphic: LineGraphic, - label: str, - position: Tuple[int, int] + self, parent, graphic: LineGraphic, label: str, position: Tuple[int, int] ): """ @@ -55,7 +51,9 @@ def __init__( pass else: - raise ValueError("Must specify `label` or Graphic must have a `name` to auto-use as the label") + raise ValueError( + "Must specify `label` or Graphic must have a `name` to auto-use as the label" + ) # for now only support lines with a single color if np.unique(graphic.colors(), axis=0).shape[0] > 1: @@ -70,17 +68,13 @@ def __init__( graphic.colors.add_event_handler(self._update_color) # construct Line WorldObject - data = np.array( - [[0, 0, 0], - [3, 0, 0]], - dtype=np.float32 - ) + data = np.array([[0, 0, 0], [3, 0, 0]], dtype=np.float32) material = pygfx.LineMaterial self._line_world_object = pygfx.Line( geometry=pygfx.Geometry(positions=data), - material=material(thickness=8, color=self._color) + material=material(thickness=8, color=self._color), ) # self._line_world_object.world.x = position[0] @@ -96,7 +90,7 @@ def __init__( color="w", outline_color="w", outline_thickness=0, - ) + ), ) self.world_object = pygfx.Group() @@ -109,7 +103,9 @@ def __init__( self.world_object.world.y = position[1] self.world_object.world.z = 2 - self.world_object.add_event_handler(partial(self._highlight_graphic, graphic), "click") + self.world_object.add_event_handler( + partial(self._highlight_graphic, graphic), "click" + ) @property def label(self) -> str: @@ -123,7 +119,9 @@ def label(self, text: str): def _update_color(self, ev: FeatureEvent): new_color = ev.pick_info["new_data"] if np.unique(new_color, axis=0).shape[0] > 1: - raise ValueError("LegendError: LineGraphic colors no longer appropriate for legend") + raise ValueError( + "LegendError: LineGraphic colors no longer appropriate for legend" + ) self._color = new_color[0] self._line_world_object.material.color = pygfx.Color(self._color) @@ -142,12 +140,12 @@ def _highlight_graphic(self, graphic, ev): class Legend(Graphic): def __init__( - self, - plot_area, - highlight_color: Union[str, tuple, np.ndarray] = "w", - max_rows: int = 5, - *args, - **kwargs + self, + plot_area, + highlight_color: Union[str, tuple, np.ndarray] = "w", + max_rows: int = 5, + *args, + **kwargs, ): """ @@ -166,7 +164,7 @@ def __init__( self._graphics: List[Graphic] = list() # hex id of Graphic, i.e. graphic.loc are the keys - self._items: OrderedDict[str: LegendItem] = OrderedDict() + self._items: OrderedDict[str:LegendItem] = OrderedDict() super().__init__(*args, **kwargs) @@ -176,7 +174,9 @@ def __init__( self._mesh = pygfx.Mesh( pygfx.box_geometry(50, 10, 1), - pygfx.MeshBasicMaterial(color=pygfx.Color([0.1, 0.1, 0.1, 1]), wireframe_thickness=10) + pygfx.MeshBasicMaterial( + color=pygfx.Color([0.1, 0.1, 0.1, 1]), wireframe_thickness=10 + ), ) self.world_object.add(self._mesh) @@ -235,7 +235,9 @@ def add_graphic(self, graphic: Graphic, label: str = None): # get x position offset for this new column of LegendItems # start by getting the LegendItems in the previous column - prev_column_items: List[LegendItem] = list(self._items.values())[-self._max_rows:] + prev_column_items: List[LegendItem] = list(self._items.values())[ + -self._max_rows : + ] # x position of LegendItems in previous column x_pos = prev_column_items[-1].world_object.world.x max_width = 0 @@ -267,7 +269,7 @@ def add_graphic(self, graphic: Graphic, label: str = None): self._graphics.append(graphic) self._items[graphic.loc] = legend_item - + graphic.deleted.add_event_handler(partial(self.remove_graphic, graphic)) self._col_counter = new_col_ix diff --git a/fastplotlib/utils/__init__.py b/fastplotlib/utils/__init__.py index addea140d..305af90a8 100644 --- a/fastplotlib/utils/__init__.py +++ b/fastplotlib/utils/__init__.py @@ -10,6 +10,4 @@ class _Config: party_parrot: bool -config = _Config( - party_parrot=False -) +config = _Config(party_parrot=False) diff --git a/fastplotlib/utils/_gpu_info.py b/fastplotlib/utils/_gpu_info.py index 9387f3ece..93e95d281 100644 --- a/fastplotlib/utils/_gpu_info.py +++ b/fastplotlib/utils/_gpu_info.py @@ -11,6 +11,7 @@ NOTEBOOK = False else: from IPython.display import display + if ip.has_trait("kernel") and (JupyterWgpuCanvas is not False): NOTEBOOK = True else: @@ -21,17 +22,14 @@ def _notebook_print_banner(): if NOTEBOOK is False: return - logo_path = Path(__file__).parent.parent.joinpath("assets", "fastplotlib_face_logo.png") + logo_path = Path(__file__).parent.parent.joinpath( + "assets", "fastplotlib_face_logo.png" + ) with open(logo_path, "rb") as f: logo_data = f.read() - image = Image( - value=logo_data, - format="png", - width=300, - height=55 - ) + image = Image(value=logo_data, format="png", width=300, height=55) display(image) @@ -39,7 +37,9 @@ def _notebook_print_banner(): adapters = [a for a in enumerate_adapters()] adapters_info = [a.request_adapter_info() for a in adapters] - ix_default = adapters_info.index(get_default_device().adapter.request_adapter_info()) + ix_default = adapters_info.index( + get_default_device().adapter.request_adapter_info() + ) if len(adapters) > 0: print("Available devices:") diff --git a/fastplotlib/utils/generate_add_methods.py b/fastplotlib/utils/generate_add_methods.py index 3fe16260c..9cb87baab 100644 --- a/fastplotlib/utils/generate_add_methods.py +++ b/fastplotlib/utils/generate_add_methods.py @@ -5,11 +5,8 @@ # 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" - ) +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 @@ -24,21 +21,23 @@ def generate_add_graphics_methods(): # clear file and regenerate from scratch - f = open(current_module.joinpath('layouts/graphic_methods_mixin.py'), 'w') + 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("# 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("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( + " 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") @@ -55,19 +54,23 @@ def generate_add_graphics_methods(): method_name = class_name.type class_args = inspect.getfullargspec(class_name)[0][1:] - class_args = [arg + ', ' for arg in class_args] + 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( + 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(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.write( + f" return self._create_graphic({class_name.__name__}, {s}*args, **kwargs)\n\n" + ) f.close() -if __name__ == '__main__': +if __name__ == "__main__": generate_add_graphics_methods() diff --git a/fastplotlib/utils/mesh_masks.py b/fastplotlib/utils/mesh_masks.py index 600e5ab6d..c44588b6c 100644 --- a/fastplotlib/utils/mesh_masks.py +++ b/fastplotlib/utils/mesh_masks.py @@ -125,4 +125,4 @@ x_right = (x_right, 0) x_left = (x_left, 0) y_top = (y_top, 1) -y_bottom = (y_bottom, 1) \ No newline at end of file +y_bottom = (y_bottom, 1) diff --git a/fastplotlib/widgets/histogram_lut.py b/fastplotlib/widgets/histogram_lut.py index 64feb8df6..31f6ab8e9 100644 --- a/fastplotlib/widgets/histogram_lut.py +++ b/fastplotlib/widgets/histogram_lut.py @@ -13,12 +13,12 @@ # TODO: This is a widget, we can think about a BaseWidget class later if necessary class HistogramLUT(Graphic): def __init__( - self, - data: np.ndarray, - image_graphic: ImageGraphic, - nbins: int = 100, - flank_divisor: float = 5.0, - **kwargs + self, + data: np.ndarray, + image_graphic: ImageGraphic, + nbins: int = 100, + flank_divisor: float = 5.0, + **kwargs, ): """ @@ -58,11 +58,14 @@ def __init__( size=size, origin=origin, axis="y", - edge_thickness=8 + edge_thickness=8, ) # there will be a small difference with the histogram edges so this makes them both line up exactly - self.linear_region.selection = (image_graphic.cmap.vmin, image_graphic.cmap.vmax) + self.linear_region.selection = ( + image_graphic.cmap.vmin, + image_graphic.cmap.vmax, + ) self._vmin = self.image_graphic.cmap.vmin self._vmax = self.image_graphic.cmap.vmax @@ -105,9 +108,7 @@ def __init__( self._text_vmax.position_x = -120 self._text_vmax.position_y = self.linear_region.selection()[1] - self.linear_region.selection.add_event_handler( - self._linear_region_handler - ) + self.linear_region.selection.add_event_handler(self._linear_region_handler) self.image_graphic.cmap.add_event_handler(self._image_cmap_handler) @@ -168,12 +169,16 @@ def _calculate_histogram(self, data): flank_size = flank_nbins * bin_width flank_left = np.arange(edges[0] - flank_size, edges[0], bin_width) - flank_right = np.arange(edges[-1] + bin_width, edges[-1] + flank_size, bin_width) + flank_right = np.arange( + edges[-1] + bin_width, edges[-1] + flank_size, bin_width + ) edges_flanked = np.concatenate((flank_left, edges, flank_right)) np.unique(np.diff(edges_flanked)) - hist_flanked = np.concatenate((np.zeros(flank_nbins), hist, np.zeros(flank_nbins))) + hist_flanked = np.concatenate( + (np.zeros(flank_nbins), hist, np.zeros(flank_nbins)) + ) # scale 0-100 to make it easier to see # float32 data can produce unnecessarily high values @@ -181,7 +186,7 @@ def _calculate_histogram(self, data): if edges_flanked.size > hist_scaled.size: # we don't care about accuracy here so if it's off by 1-2 bins that's fine - edges_flanked = edges_flanked[:hist_scaled.size] + edges_flanked = edges_flanked[: hist_scaled.size] return hist, edges, hist_scaled, edges_flanked @@ -209,7 +214,10 @@ def vmin(self, value: float): # must use world coordinate values directly from selection() # otherwise the linear region bounds jump to the closest bin edges - self.linear_region.selection = (value * self._scale_factor, self.linear_region.selection()[1]) + self.linear_region.selection = ( + value * self._scale_factor, + self.linear_region.selection()[1], + ) self.image_graphic.cmap.vmin = value self._block_events(False) @@ -230,7 +238,10 @@ def vmax(self, value: float): # must use world coordinate values directly from selection() # otherwise the linear region bounds jump to the closest bin edges - self.linear_region.selection = (self.linear_region.selection()[0], value * self._scale_factor) + self.linear_region.selection = ( + self.linear_region.selection()[0], + value * self._scale_factor, + ) self.image_graphic.cmap.vmax = value self._block_events(False) @@ -280,9 +291,7 @@ def image_graphic(self, graphic): ) # cleanup events from current image graphic - self._image_graphic.cmap.remove_event_handler( - self._image_cmap_handler - ) + self._image_graphic.cmap.remove_event_handler(self._image_cmap_handler) self._image_graphic = graphic diff --git a/fastplotlib/widgets/image.py b/fastplotlib/widgets/image.py index a3f6335c9..0da1bb520 100644 --- a/fastplotlib/widgets/image.py +++ b/fastplotlib/widgets/image.py @@ -40,6 +40,7 @@ def _is_arraylike(obj) -> bool: class _WindowFunctions: """Stores window function and window size""" + def __init__(self, image_widget, func: callable, window_size: int): self._image_widget = image_widget self._func = None @@ -131,7 +132,9 @@ def cmap(self) -> List[str]: def cmap(self, names: Union[str, List[str]]): if isinstance(names, list): if not all([isinstance(n, str) for n in names]): - raise TypeError(f"Must pass cmap name as a `str` of list of `str`, you have passed:\n{names}") + raise TypeError( + f"Must pass cmap name as a `str` of list of `str`, you have passed:\n{names}" + ) if not len(names) == len(self.managed_graphics): raise IndexError( @@ -558,7 +561,9 @@ def __init__( # user specified kwargs will overwrite the defaults grid_plot_kwargs_default.update(grid_plot_kwargs) - self._gridplot: GridPlot = GridPlot(shape=grid_shape, **grid_plot_kwargs_default) + self._gridplot: GridPlot = GridPlot( + shape=grid_shape, **grid_plot_kwargs_default + ) for data_ix, (d, subplot) in enumerate(zip(self.data, self.gridplot)): if self._names is not None: @@ -574,11 +579,7 @@ def __init__( subplot.set_title(name) if histogram_widget: - hlut = HistogramLUT( - data=d, - image_graphic=ig, - name="histogram_lut" - ) + hlut = HistogramLUT(data=d, image_graphic=ig, name="histogram_lut") subplot.docks["right"].add_graphic(hlut) subplot.docks["right"].size = 80 @@ -596,7 +597,7 @@ def frame_apply(self) -> Union[dict, None]: def frame_apply(self, frame_apply: Dict[int, callable]): if frame_apply is None: frame_apply = dict() - + self._frame_apply = frame_apply # force update image graphic self.current_index = self.current_index @@ -901,7 +902,9 @@ def set_data( max_lengths["z"] = min(max_lengths["z"], new_array.shape[1] - 1) # set histogram widget - subplot.docks["right"]["histogram_lut"].set_data(new_array, reset_vmin_vmax=reset_vmin_vmax) + subplot.docks["right"]["histogram_lut"].set_data( + new_array, reset_vmin_vmax=reset_vmin_vmax + ) # set slider maxes # TODO: maybe make this stuff a property, like ndims, n_frames etc. and have it set the sliders @@ -912,7 +915,9 @@ def set_data( # force graphics to update self.current_index = self.current_index - def show(self, toolbar: bool = True, sidecar: bool = False, sidecar_kwargs: dict = None): + def show( + self, toolbar: bool = True, sidecar: bool = False, sidecar_kwargs: dict = None + ): """ Show the widget. @@ -931,7 +936,7 @@ def show(self, toolbar: bool = True, sidecar: bool = False, sidecar_kwargs: dict toolbar=toolbar, sidecar=sidecar, sidecar_kwargs=sidecar_kwargs, - add_widgets=[self._image_widget_toolbar] + add_widgets=[self._image_widget_toolbar], ) return self._output From c9ce4958eea542209fa5e7a541a8a50fd54d4928 Mon Sep 17 00:00:00 2001 From: Kushal Kolar Date: Mon, 4 Mar 2024 22:36:26 -0500 Subject: [PATCH 21/66] add zmq example (#422) --- .../notebooks/multiprocessing_zmq/README.md | 3 + .../multiprocessing_zmq_compute.ipynb | 73 +++++++++++ .../multiprocessing_zmq_plot.ipynb | 114 ++++++++++++++++++ 3 files changed, 190 insertions(+) create mode 100644 examples/notebooks/multiprocessing_zmq/README.md create mode 100644 examples/notebooks/multiprocessing_zmq/multiprocessing_zmq_compute.ipynb create mode 100644 examples/notebooks/multiprocessing_zmq/multiprocessing_zmq_plot.ipynb diff --git a/examples/notebooks/multiprocessing_zmq/README.md b/examples/notebooks/multiprocessing_zmq/README.md new file mode 100644 index 000000000..184453d0c --- /dev/null +++ b/examples/notebooks/multiprocessing_zmq/README.md @@ -0,0 +1,3 @@ +This example shows how to use a zmq publisher-subscriber pattern to perform a computation in one process and visualize results in another process. First, run all cells in `multiprocessing_zmq_plot.ipynb`, and then run cells in `multiprocessing_zmq_compute.ipynb`. The raw bytes for the numpy array are sent using zmq in the compute notebook and received in the plot notebook and displayed. + +For more information on zmq see: https://zeromq.org/languages/python/ diff --git a/examples/notebooks/multiprocessing_zmq/multiprocessing_zmq_compute.ipynb b/examples/notebooks/multiprocessing_zmq/multiprocessing_zmq_compute.ipynb new file mode 100644 index 000000000..7f24f6411 --- /dev/null +++ b/examples/notebooks/multiprocessing_zmq/multiprocessing_zmq_compute.ipynb @@ -0,0 +1,73 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "id": "ca2817a3-869c-4cc6-901b-c34509518175", + "metadata": {}, + "outputs": [], + "source": [ + "import numpy as np\n", + "import zmq" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "dd0b9780-b507-4ea2-af09-134abd76f45b", + "metadata": {}, + "outputs": [], + "source": [ + "context = zmq.Context()\n", + "\n", + "# create publisher\n", + "socket = context.socket(zmq.PUB)\n", + "socket.bind(\"tcp://127.0.0.1:5555\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4729bfe8-3474-4bc9-a489-a57d02e5a287", + "metadata": {}, + "outputs": [], + "source": [ + "for i in range(2_000):\n", + " # make some data, make note of the dtype\n", + " data = np.random.rand(512, 512).astype(np.float32)\n", + "\n", + " # sent bytes over the socket\n", + " socket.send(data.tobytes())" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d47dab72-1061-439f-bf6e-a88b9ee8e5aa", + "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/multiprocessing_zmq/multiprocessing_zmq_plot.ipynb b/examples/notebooks/multiprocessing_zmq/multiprocessing_zmq_plot.ipynb new file mode 100644 index 000000000..2c6e93d8f --- /dev/null +++ b/examples/notebooks/multiprocessing_zmq/multiprocessing_zmq_plot.ipynb @@ -0,0 +1,114 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "id": "491e6050-64ae-4bfc-a480-5805cd684710", + "metadata": {}, + "outputs": [], + "source": [ + "import fastplotlib as fpl\n", + "import numpy as np\n", + "import zmq" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "97135f98-6810-49b6-a8de-d0e114720d6c", + "metadata": {}, + "outputs": [], + "source": [ + "context = zmq.Context()\n", + "\n", + "# create subscriber\n", + "sub = context.socket(zmq.SUB)\n", + "sub.setsockopt(zmq.SUBSCRIBE, b\"\")\n", + "\n", + "# keep only the most recent message\n", + "sub.setsockopt(zmq.CONFLATE, 1)\n", + "\n", + "# publisher address and port\n", + "sub.connect(\"tcp://127.0.0.1:5555\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2d4420f2-364a-445a-9658-63e9ffa586c3", + "metadata": {}, + "outputs": [], + "source": [ + "def get_bytes():\n", + " \"\"\"\n", + " Gets the bytes from the publisher\n", + " \"\"\"\n", + " try:\n", + " b = sub.recv(zmq.NOBLOCK)\n", + " except zmq.Again:\n", + " pass\n", + " else:\n", + " return b\n", + " \n", + " return None" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "42d20d77-b884-4379-80e4-e08738506eeb", + "metadata": {}, + "outputs": [], + "source": [ + "plot = fpl.Plot()\n", + "\n", + "# initialize some data, must be of same dtype and shape as data sent by publisher\n", + "data = np.random.rand(512, 512).astype(np.float32)\n", + "plot.add_image(data, name=\"image\")\n", + "\n", + "def update_frame(p):\n", + " # recieve bytes\n", + " b = get_bytes()\n", + " \n", + " if b is not None:\n", + " # numpy array from bytes, MUST specify dtype and make sure it matches what you sent\n", + " a = np.frombuffer(b, dtype=np.float32).reshape(512, 512)\n", + " \n", + " # set graphic data\n", + " p[\"image\"].data = a\n", + "\n", + "plot.add_animations(update_frame)\n", + "plot.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "0f8ac188-9359-4d3c-b8f1-384be84d1585", + "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 +} From 5978e7d9f2727eb953a243780af92592dd6ad416 Mon Sep 17 00:00:00 2001 From: Kushal Kolar Date: Wed, 6 Mar 2024 05:30:28 -0500 Subject: [PATCH 22/66] fix fastplotlib.__init__ (#424) --- fastplotlib/__init__.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/fastplotlib/__init__.py b/fastplotlib/__init__.py index f0f3cc7b1..98dc17e26 100644 --- a/fastplotlib/__init__.py +++ b/fastplotlib/__init__.py @@ -4,6 +4,7 @@ from .graphics import * from .graphics.selectors import * from .legends import * +from .utils import _notebook_print_banner, config from wgpu.gui.auto import run @@ -16,13 +17,15 @@ from wgpu.backends.wgpu_native import enumerate_adapters +with open(Path(__file__).parent.joinpath("VERSION"), "r") as f: + __version__ = f.read().split("\n")[0] + adapters = [a.request_adapter_info() for a in enumerate_adapters()] if len(adapters) < 1: raise IndexError("No WGPU adapters found, fastplotlib will not work.") -with open(Path(__file__).parent.joinpath("VERSION"), "r") as f: - __version__ = f.read().split("\n")[0] +_notebook_print_banner() __all__ = [ "Plot", From 463cd15d7e3de4b7ff51836c64b4cd90b0bed30a Mon Sep 17 00:00:00 2001 From: Almar Klein Date: Fri, 8 Mar 2024 17:01:49 +0100 Subject: [PATCH 23/66] Fix typo (#423) --- CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 84114c61f..ad484db5d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -15,7 +15,7 @@ git clone https://github.com/YOUR_ACCOUNT/fastplotlib.git cd fastplotlib # install all extras in place -pip install -e ".[notebook,docs,tests] +pip install -e ".[notebook,docs,tests]" ``` > If you cloned before installing `git-lfs`, you can run `git lfs pull` at any From 97dd7581598e08d2320c1a208737164be2cdbdb7 Mon Sep 17 00:00:00 2001 From: Almar Klein Date: Fri, 8 Mar 2024 17:56:50 +0100 Subject: [PATCH 24/66] Just use asarray (#426) --- fastplotlib/layouts/_gridplot.py | 18 ++++-------------- 1 file changed, 4 insertions(+), 14 deletions(-) diff --git a/fastplotlib/layouts/_gridplot.py b/fastplotlib/layouts/_gridplot.py index 98e5643f3..04046cd01 100644 --- a/fastplotlib/layouts/_gridplot.py +++ b/fastplotlib/layouts/_gridplot.py @@ -15,16 +15,6 @@ from ._record_mixin import RecordMixin -def to_array(a) -> np.ndarray: - if isinstance(a, np.ndarray): - return a - - if not isinstance(a, list): - raise TypeError("must pass list or numpy array") - - return np.array(a) - - class GridPlot(Frame, RecordMixin): def __init__( self, @@ -88,7 +78,7 @@ def __init__( "must provide same number of subplot `names` as specified by gridplot shape" ) - self.names = to_array(names).reshape(self.shape) + self.names = np.asarray(names).reshape(self.shape) else: self.names = None @@ -101,7 +91,7 @@ def __init__( ) # list -> array if necessary - cameras = to_array(cameras).reshape(self.shape) + cameras = np.asarray(cameras).reshape(self.shape) if cameras.shape != self.shape: raise ValueError("Number of cameras does not match the number of subplots") @@ -162,7 +152,7 @@ def __init__( # integer ids elif all([isinstance(item, (int, np.integer)) for item in ids_flat]): - controller_ids = to_array(controller_ids).reshape(self.shape) + controller_ids = np.asarray(controller_ids).reshape(self.shape) else: raise TypeError( @@ -200,7 +190,7 @@ def __init__( f"{valid_str} or instances of {[c.__name__ for c in valid_instances]}" ) - controller_types = to_array(controller_types).reshape(self.shape) + controller_types = np.asarray(controller_types).reshape(self.shape) # make the real controllers for each subplot self._controllers = np.empty(shape=self.shape, dtype=object) From d45c72301200923b4c0e6f20793ed7d7c3c11917 Mon Sep 17 00:00:00 2001 From: Almar Klein Date: Tue, 12 Mar 2024 15:02:00 +0100 Subject: [PATCH 25/66] Image widget glfw example (#429) * Add destkop image widget example * black (to examples/desktop/image only) * Add not * Always import ImageWidget --- examples/desktop/image/image_cmap.py | 1 + examples/desktop/image/image_rgb.py | 1 + examples/desktop/image/image_rgbvminvmax.py | 1 + examples/desktop/image/image_vminvmax.py | 1 + examples/desktop/image/image_widget.py | 20 ++++++++++++++++++++ fastplotlib/__init__.py | 10 ++-------- 6 files changed, 26 insertions(+), 8 deletions(-) create mode 100644 examples/desktop/image/image_widget.py diff --git a/examples/desktop/image/image_cmap.py b/examples/desktop/image/image_cmap.py index 9a9f0d497..b7f7b39af 100644 --- a/examples/desktop/image/image_cmap.py +++ b/examples/desktop/image/image_cmap.py @@ -3,6 +3,7 @@ ============ Example showing simple plot creation and subsequent cmap change with Standard image from imageio. """ + # test_example = true import fastplotlib as fpl diff --git a/examples/desktop/image/image_rgb.py b/examples/desktop/image/image_rgb.py index f73077acf..2642962fd 100644 --- a/examples/desktop/image/image_rgb.py +++ b/examples/desktop/image/image_rgb.py @@ -3,6 +3,7 @@ ============ Example showing the simple plot creation with 512 x 512 2D RGB image. """ + # test_example = true import fastplotlib as fpl diff --git a/examples/desktop/image/image_rgbvminvmax.py b/examples/desktop/image/image_rgbvminvmax.py index 4891c5614..e5c4af531 100644 --- a/examples/desktop/image/image_rgbvminvmax.py +++ b/examples/desktop/image/image_rgbvminvmax.py @@ -3,6 +3,7 @@ ============ Example showing the simple plot followed by changing the vmin/vmax with 512 x 512 2D RGB image. """ + # test_example = true import fastplotlib as fpl diff --git a/examples/desktop/image/image_vminvmax.py b/examples/desktop/image/image_vminvmax.py index ae5d102fa..e764f6775 100644 --- a/examples/desktop/image/image_vminvmax.py +++ b/examples/desktop/image/image_vminvmax.py @@ -3,6 +3,7 @@ ============ Example showing the simple plot creation followed by changing the vmin/vmax with Standard imageio image. """ + # test_example = true import fastplotlib as fpl diff --git a/examples/desktop/image/image_widget.py b/examples/desktop/image/image_widget.py new file mode 100644 index 000000000..c50d914d3 --- /dev/null +++ b/examples/desktop/image/image_widget.py @@ -0,0 +1,20 @@ +""" +Image widget +============ +Example showing the image widget in action. +When run in a notebook, or with the Qt GUI backend, sliders are also shown. +""" + +import numpy as np +import fastplotlib as fpl +import imageio.v3 as iio # not a fastplotlib dependency, only used for examples + + +a = iio.imread("imageio:camera.png") +iw = fpl.widgets.ImageWidget(data=a, cmap="viridis") +iw.show() + + +if __name__ == "__main__": + print(__doc__) + fpl.run() diff --git a/fastplotlib/__init__.py b/fastplotlib/__init__.py index 98dc17e26..27545f0ad 100644 --- a/fastplotlib/__init__.py +++ b/fastplotlib/__init__.py @@ -4,19 +4,13 @@ from .graphics import * from .graphics.selectors import * from .legends import * +from .widgets import ImageWidget from .utils import _notebook_print_banner, config from wgpu.gui.auto import run - -try: - import ipywidgets -except (ModuleNotFoundError, ImportError): - pass -else: - from .widgets import ImageWidget - from wgpu.backends.wgpu_native import enumerate_adapters + with open(Path(__file__).parent.joinpath("VERSION"), "r") as f: __version__ = f.read().split("\n")[0] From 775556ed4426dc4d861e9db862ce9a6d86c1f391 Mon Sep 17 00:00:00 2001 From: Almar Klein Date: Tue, 12 Mar 2024 15:08:14 +0100 Subject: [PATCH 26/66] Remove duplicate code (mesh_masks) (#430) * Remove duplicate code * Ah, even better --- .../graphics/_features/_selection_features.py | 131 +----------------- fastplotlib/graphics/_features/_sizes.py | 12 +- .../graphics/selectors/_mesh_positions.py | 1 - .../graphics/selectors/_rectangle_region.py | 10 +- 4 files changed, 18 insertions(+), 136 deletions(-) delete mode 100644 fastplotlib/graphics/selectors/_mesh_positions.py diff --git a/fastplotlib/graphics/_features/_selection_features.py b/fastplotlib/graphics/_features/_selection_features.py index 9a2696f7c..294bb15d6 100644 --- a/fastplotlib/graphics/_features/_selection_features.py +++ b/fastplotlib/graphics/_features/_selection_features.py @@ -2,131 +2,10 @@ import numpy as np +from ...utils import mesh_masks 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 """ @@ -253,10 +132,10 @@ def _set(self, value: Tuple[float, float]): if self.axis == "x": # change left x position of the fill mesh - self._parent.fill.geometry.positions.data[x_left, 0] = value[0] + self._parent.fill.geometry.positions.data[mesh_masks.x_left] = value[0] # change right x position of the fill mesh - self._parent.fill.geometry.positions.data[x_right, 0] = value[1] + self._parent.fill.geometry.positions.data[mesh_masks.x_right] = value[1] # change x position of the left edge line self._parent.edges[0].geometry.positions.data[:, 0] = value[0] @@ -266,10 +145,10 @@ def _set(self, value: Tuple[float, float]): elif self.axis == "y": # change bottom y position of the fill mesh - self._parent.fill.geometry.positions.data[y_bottom, 1] = value[0] + self._parent.fill.geometry.positions.data[mesh_masks.y_bottom] = value[0] # change top position of the fill mesh - self._parent.fill.geometry.positions.data[y_top, 1] = value[1] + self._parent.fill.geometry.positions.data[mesh_masks.y_top] = value[1] # change y position of the bottom edge line self._parent.edges[0].geometry.positions.data[:, 1] = value[0] diff --git a/fastplotlib/graphics/_features/_sizes.py b/fastplotlib/graphics/_features/_sizes.py index 8fceb8df3..403760508 100644 --- a/fastplotlib/graphics/_features/_sizes.py +++ b/fastplotlib/graphics/_features/_sizes.py @@ -61,8 +61,10 @@ def _fix_sizes(self, sizes, parent): if graphic_type == "ScatterGraphic": sizes = np.array(sizes) else: - raise ValueError(f"Sizes must be an array of shape (n,) where n == the number of data points provided.\ - Received shape={sizes.shape}.") + raise ValueError( + f"Sizes must be an array of shape (n,) where n == the number of data points provided.\ + Received shape={sizes.shape}." + ) return np.array(sizes) @@ -78,8 +80,10 @@ def __setitem__(self, key, value): # numpy will throw errors if it can't broadcast if value.size != self.buffer.data[key].size: - raise ValueError(f"{value.size} is not equal to buffer size {self.buffer.data[key].size}.\ - If you want to set size to a non-scalar value, make sure it's the right length!") + raise ValueError( + f"{value.size} is not equal to buffer size {self.buffer.data[key].size}.\ + If you want to set size to a non-scalar value, make sure it's the right length!" + ) self.buffer.data[key] = value self._update_range(key) diff --git a/fastplotlib/graphics/selectors/_mesh_positions.py b/fastplotlib/graphics/selectors/_mesh_positions.py deleted file mode 100644 index a22b22b17..000000000 --- a/fastplotlib/graphics/selectors/_mesh_positions.py +++ /dev/null @@ -1 +0,0 @@ -import numpy as np diff --git a/fastplotlib/graphics/selectors/_rectangle_region.py b/fastplotlib/graphics/selectors/_rectangle_region.py index a5a9a31cb..0d7dd3661 100644 --- a/fastplotlib/graphics/selectors/_rectangle_region.py +++ b/fastplotlib/graphics/selectors/_rectangle_region.py @@ -3,10 +3,10 @@ import pygfx +from ...utils import mesh_masks 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 class RectangleBoundsFeature(GraphicFeature): @@ -58,16 +58,16 @@ def _set(self, value: Tuple[float, float, float, float]): # change fill mesh # change left x position of the fill mesh - self._parent.fill.geometry.positions.data[x_left, 0] = xmin + self._parent.fill.geometry.positions.data[mesh_masks.x_left] = xmin # change right x position of the fill mesh - self._parent.fill.geometry.positions.data[x_right, 0] = xmax + self._parent.fill.geometry.positions.data[mesh_masks.x_right] = xmax # change bottom y position of the fill mesh - self._parent.fill.geometry.positions.data[y_bottom, 1] = ymin + self._parent.fill.geometry.positions.data[mesh_masks.y_bottom] = ymin # change top position of the fill mesh - self._parent.fill.geometry.positions.data[y_top, 1] = ymax + self._parent.fill.geometry.positions.data[mesh_masks.y_top] = ymax # change the edge lines From fe7417980a83145466ebf7398d75ca71eb28bdcc Mon Sep 17 00:00:00 2001 From: Almar Klein Date: Thu, 14 Mar 2024 09:36:22 +0100 Subject: [PATCH 27/66] Lazy import av (#432) --- fastplotlib/layouts/_frame/_toolbar.py | 2 +- fastplotlib/layouts/_record_mixin.py | 25 +++++++++++++------------ 2 files changed, 14 insertions(+), 13 deletions(-) diff --git a/fastplotlib/layouts/_frame/_toolbar.py b/fastplotlib/layouts/_frame/_toolbar.py index 94410b8ea..6a0485655 100644 --- a/fastplotlib/layouts/_frame/_toolbar.py +++ b/fastplotlib/layouts/_frame/_toolbar.py @@ -1,4 +1,4 @@ -from fastplotlib.layouts._subplot import Subplot +from .._subplot import Subplot class ToolBar: diff --git a/fastplotlib/layouts/_record_mixin.py b/fastplotlib/layouts/_record_mixin.py index e3a491915..e3bfdeba5 100644 --- a/fastplotlib/layouts/_record_mixin.py +++ b/fastplotlib/layouts/_record_mixin.py @@ -3,12 +3,17 @@ from multiprocessing import Queue, Process from time import time -try: - import av -except ImportError: - HAS_AV = False -else: - HAS_AV = True + +def _get_av(): + try: + import av + except ImportError: + raise ModuleNotFoundError( + "Recording to video file requires `av`:\n" + "https://github.com/PyAV-Org/PyAV" + ) from None + else: + return av class VideoWriterAV(Process): @@ -28,6 +33,7 @@ def __init__( super().__init__() self.queue = queue + av = _get_av() self.container = av.open(path, mode="w") self.stream = self.container.add_stream(codec, rate=fps, options=options) @@ -45,6 +51,7 @@ def __init__( self.stream.pix_fmt = pixel_format def run(self): + av = _get_av() while True: if self.queue.empty(): # no frame to write continue @@ -177,12 +184,6 @@ def record_start( """ - if not HAS_AV: - raise ModuleNotFoundError( - "Recording to video file requires `av`:\n" - "https://github.com/PyAV-Org/PyAV" - ) - if Path(path).exists(): raise FileExistsError(f"File already exists at given path: {path}") From 8ebbe5cdce7fe0fdab6c0cacf492620ae0d7219a Mon Sep 17 00:00:00 2001 From: Almar Klein Date: Thu, 14 Mar 2024 21:03:28 +0100 Subject: [PATCH 28/66] A bit of black (#433) * Script that generates methods also blackens the code. * Also blacken docs subdir --- docs/source/conf.py | 20 +- docs/source/generate_api.py | 56 ++--- fastplotlib/layouts/graphic_methods_mixin.py | 239 ++++++++++++++++--- fastplotlib/utils/generate_add_methods.py | 23 +- setup.py | 37 ++- 5 files changed, 262 insertions(+), 113 deletions(-) diff --git a/docs/source/conf.py b/docs/source/conf.py index 7b33a309e..a6a9fd1f6 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -7,9 +7,9 @@ # -- Project information ----------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information -project = 'fastplotlib' -copyright = '2023, Kushal Kolar, Caitlin Lewis' -author = 'Kushal Kolar, Caitlin Lewis' +project = "fastplotlib" +copyright = "2023, Kushal Kolar, Caitlin Lewis" +author = "Kushal Kolar, Caitlin Lewis" release = fastplotlib.__version__ # -- General configuration --------------------------------------------------- @@ -28,21 +28,21 @@ autosummary_generate = True -templates_path = ['_templates'] +templates_path = ["_templates"] exclude_patterns = [] -napoleon_custom_sections = ['Features'] +napoleon_custom_sections = ["Features"] # -- Options for HTML output ------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output html_theme = "furo" -html_static_path = ['_static'] +html_static_path = ["_static"] html_logo = "_static/logo.png" html_title = f"v{release}" -autodoc_member_order = 'groupwise' +autodoc_member_order = "groupwise" autoclass_content = "both" add_module_names = False @@ -50,9 +50,9 @@ autodoc_typehints_description_target = "documented_params" intersphinx_mapping = { - 'python': ('https://docs.python.org/3', None), - 'numpy': ('https://numpy.org/doc/stable/', None), - 'pygfx': ('https://pygfx.readthedocs.io/en/latest', None) + "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 = { diff --git a/docs/source/generate_api.py b/docs/source/generate_api.py index 05e8b0f1c..19b739d1b 100644 --- a/docs/source/generate_api.py +++ b/docs/source/generate_api.py @@ -26,7 +26,7 @@ GRAPHICS_DIR, GRAPHIC_FEATURES_DIR, SELECTORS_DIR, - WIDGETS_DIR + WIDGETS_DIR, ] for source_dir in doc_sources: @@ -66,13 +66,9 @@ def generate_class( ): name = cls.__name__ methods, properties = get_public_members(cls) - methods = [ - f"{name}.{m}" for m in methods - ] + methods = [f"{name}.{m}" for m in methods] - properties = [ - f"{name}.{p}" for p in properties - ] + properties = [f"{name}.{p}" for p in properties] underline = "=" * len(name) @@ -149,32 +145,28 @@ def main(): page_name="Plot", classes=[fastplotlib.Plot], modules=["fastplotlib"], - source_path=LAYOUTS_DIR.joinpath("plot.rst") + source_path=LAYOUTS_DIR.joinpath("plot.rst"), ) generate_page( page_name="GridPlot", classes=[fastplotlib.GridPlot], modules=["fastplotlib"], - source_path=LAYOUTS_DIR.joinpath("gridplot.rst") + source_path=LAYOUTS_DIR.joinpath("gridplot.rst"), ) generate_page( page_name="Subplot", classes=[Subplot], modules=["fastplotlib.layouts._subplot"], - source_path=LAYOUTS_DIR.joinpath("subplot.rst") + 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_classes = [getattr(graphics, g) for g in graphics.__all__] - graphic_class_names = [ - g.__name__ for g in graphic_classes - ] + graphic_class_names = [g.__name__ for g in graphic_classes] graphic_class_names_str = "\n ".join([""] + graphic_class_names) @@ -194,17 +186,13 @@ def main(): page_name=graphic_cls.__name__, classes=[graphic_cls], modules=["fastplotlib"], - source_path=GRAPHICS_DIR.joinpath(f"{graphic_cls.__name__}.rst") + source_path=GRAPHICS_DIR.joinpath(f"{graphic_cls.__name__}.rst"), ) ############################################################################## - feature_classes = [ - getattr(_features, f) for f in _features.__all__ - ] + feature_classes = [getattr(_features, f) for f in _features.__all__] - feature_class_names = [ - f.__name__ for f in feature_classes - ] + feature_class_names = [f.__name__ for f in feature_classes] feature_class_names_str = "\n ".join([""] + feature_class_names) @@ -223,17 +211,13 @@ def main(): page_name=feature_cls.__name__, classes=[feature_cls], modules=["fastplotlib.graphics._features"], - source_path=GRAPHIC_FEATURES_DIR.joinpath(f"{feature_cls.__name__}.rst") + source_path=GRAPHIC_FEATURES_DIR.joinpath(f"{feature_cls.__name__}.rst"), ) ############################################################################## - selector_classes = [ - getattr(selectors, s) for s in selectors.__all__ - ] + selector_classes = [getattr(selectors, s) for s in selectors.__all__] - selector_class_names = [ - s.__name__ for s in selector_classes - ] + selector_class_names = [s.__name__ for s in selector_classes] selector_class_names_str = "\n ".join([""] + selector_class_names) @@ -252,17 +236,13 @@ def main(): page_name=selector_cls.__name__, classes=[selector_cls], modules=["fastplotlib"], - source_path=SELECTORS_DIR.joinpath(f"{selector_cls.__name__}.rst") + source_path=SELECTORS_DIR.joinpath(f"{selector_cls.__name__}.rst"), ) ############################################################################## - widget_classes = [ - getattr(widgets, w) for w in widgets.__all__ - ] + widget_classes = [getattr(widgets, w) for w in widgets.__all__] - widget_class_names = [ - w.__name__ for w in widget_classes - ] + widget_class_names = [w.__name__ for w in widget_classes] widget_class_names_str = "\n ".join([""] + widget_class_names) @@ -281,7 +261,7 @@ def main(): page_name=widget_cls.__name__, classes=[widget_cls], modules=["fastplotlib"], - source_path=WIDGETS_DIR.joinpath(f"{widget_cls.__name__}.rst") + source_path=WIDGETS_DIR.joinpath(f"{widget_cls.__name__}.rst"), ) ############################################################################## diff --git a/fastplotlib/layouts/graphic_methods_mixin.py b/fastplotlib/layouts/graphic_methods_mixin.py index b00187df7..0376fd777 100644 --- a/fastplotlib/layouts/graphic_methods_mixin.py +++ b/fastplotlib/layouts/graphic_methods_mixin.py @@ -14,13 +14,13 @@ def __init__(self): pass def _create_graphic(self, graphic_class, *args, **kwargs) -> Graphic: - if 'center' in kwargs.keys(): - center = kwargs.pop('center') + if "center" in kwargs.keys(): + center = kwargs.pop("center") else: center = False - if 'name' in kwargs.keys(): - self._check_graphic_name_exists(kwargs['name']) + if "name" in kwargs.keys(): + self._check_graphic_name_exists(kwargs["name"]) graphic = graphic_class(*args, **kwargs) self.add_graphic(graphic, center=center) @@ -28,9 +28,20 @@ def _create_graphic(self, graphic_class, *args, **kwargs) -> Graphic: # 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: + 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 @@ -78,13 +89,34 @@ def add_heatmap(self, data: Any, vmin: int = None, vmax: int = None, cmap: str = **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: """ - + 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 @@ -129,13 +161,36 @@ def add_image(self, data: Any, vmin: int = None, vmax: int = None, cmap: str = ' **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: """ - + 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 @@ -189,13 +244,38 @@ def add_line_collection(self, data: List[numpy.ndarray], z_position: Union[List[ 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: """ - + 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 @@ -247,13 +327,37 @@ def add_line(self, data: Any, thickness: float = 2.0, colors: Union[str, numpy.n **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: """ - + 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 @@ -309,13 +413,36 @@ def add_line_stack(self, data: List[numpy.ndarray], z_position: Union[List[float 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, float, 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: """ - + 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, float, 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 @@ -364,13 +491,36 @@ def add_scatter(self, data: numpy.ndarray, sizes: Union[int, float, numpy.ndarra **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 = 14, face_color: Union[str, numpy.ndarray] = 'w', outline_color: Union[str, numpy.ndarray] = 'w', outline_thickness=0, screen_space: bool = True, anchor: str = 'middle-center', *args, **kwargs) -> TextGraphic: """ - + 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 = 14, + face_color: Union[str, numpy.ndarray] = "w", + outline_color: Union[str, numpy.ndarray] = "w", + outline_thickness=0, + screen_space: bool = True, + anchor: str = "middle-center", + *args, + **kwargs + ) -> TextGraphic: + """ + Create a text Graphic Parameters @@ -405,7 +555,18 @@ def add_text(self, text: str, position: Tuple[int] = (0, 0, 0), size: int = 14, * Vertical values: "top", "middle", "baseline", "bottom" * Horizontal values: "left", "center", "right" - - """ - return self._create_graphic(TextGraphic, text, position, size, face_color, outline_color, outline_thickness, screen_space, anchor, *args, **kwargs) + """ + return self._create_graphic( + TextGraphic, + text, + position, + size, + face_color, + outline_color, + outline_thickness, + screen_space, + anchor, + *args, + **kwargs + ) diff --git a/fastplotlib/utils/generate_add_methods.py b/fastplotlib/utils/generate_add_methods.py index 9cb87baab..100ad7757 100644 --- a/fastplotlib/utils/generate_add_methods.py +++ b/fastplotlib/utils/generate_add_methods.py @@ -1,11 +1,15 @@ import inspect import pathlib +import black + +root = pathlib.Path(__file__).parent.parent.resolve() +filename = root.joinpath("layouts/graphic_methods_mixin.py") + # 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: +with open(filename, "w") as f: f.write(f"class GraphicMethodsMixin:\n" f" pass") from fastplotlib import graphics @@ -20,8 +24,7 @@ def generate_add_graphics_methods(): # clear file and regenerate from scratch - - f = open(current_module.joinpath("layouts/graphic_methods_mixin.py"), "w") + f = open(filename, "w", encoding="utf-8") f.write("# This is an auto-generated file and should not be modified directly\n\n") @@ -72,5 +75,17 @@ def generate_add_graphics_methods(): f.close() +def blacken(): + with open(filename, "r", encoding="utf-8") as f: + text = f.read() + + mode = black.FileMode(line_length=88) + text = black.format_str(text, mode=mode) + + with open(filename, "w", encoding="utf-8") as f: + f.write(text) + + if __name__ == "__main__": generate_add_graphics_methods() + blacken() diff --git a/setup.py b/setup.py index 1d274fc0a..2622b1406 100644 --- a/setup.py +++ b/setup.py @@ -20,19 +20,15 @@ "nbsphinx", "pandoc", "jupyterlab", - "sidecar" + "sidecar", ], - - "notebook": - [ + "notebook": [ "jupyterlab", "jupyter-rfb>=0.4.1", "ipywidgets>=8.0.0,<9", - "sidecar" + "sidecar", ], - - "tests": - [ + "tests": [ "pytest<8.0.0", "nbmake", "scipy", @@ -42,17 +38,15 @@ "ipywidgets>=8.0.0,<9", "scikit-learn", "tqdm", - "sidecar" + "sidecar", ], - - "tests-desktop": - [ + "tests-desktop": [ "pytest<8.0.0", "scipy", "imageio", "scikit-learn", "tqdm", - ] + ], } @@ -72,19 +66,18 @@ setup( - name='fastplotlib', + name="fastplotlib", version=ver, long_description=readme, - long_description_content_type='text/markdown', + long_description_content_type="text/markdown", packages=find_packages(), - url='https://github.com/fastplotlib/fastplotlib', - license='Apache 2.0', - author='Kushal Kolar, Caitlin Lewis', - author_email='', - python_requires='>=3.9', + url="https://github.com/fastplotlib/fastplotlib", + license="Apache 2.0", + author="Kushal Kolar, Caitlin Lewis", + author_email="", + python_requires=">=3.9", install_requires=install_requires, extras_require=extras_require, include_package_data=True, - description='A fast plotting library built using the pygfx render engine' + description="A fast plotting library built using the pygfx render engine", ) - From 576ccbf29844b3bd3222ba69ae669f51e220605e Mon Sep 17 00:00:00 2001 From: Eric Thomson Date: Tue, 19 Mar 2024 22:56:39 -0400 Subject: [PATCH 29/66] Add black to setup tests reqs and add black CI action (#436) --- .github/workflows/black.yml | 12 ++++++++++++ .github/workflows/ci.yml | 1 - .gitignore | 3 +++ setup.py | 1 + 4 files changed, 16 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/black.yml diff --git a/.github/workflows/black.yml b/.github/workflows/black.yml new file mode 100644 index 000000000..bcb2d2b33 --- /dev/null +++ b/.github/workflows/black.yml @@ -0,0 +1,12 @@ +name: Lint + +on: [push, pull_request] + +jobs: + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: psf/black@stable + with: + src: "./fastplotlib" \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fddfae5f4..40d39c4f6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,7 +14,6 @@ on: - ready_for_review jobs: - docs-build: name: Docs runs-on: bigmem diff --git a/.gitignore b/.gitignore index 9857fe4b2..c599d5f8c 100644 --- a/.gitignore +++ b/.gitignore @@ -131,4 +131,7 @@ dmypy.json # Pycharm .idea/ +# vs code +.vscode/ + examples/desktop/diffs/*.png diff --git a/setup.py b/setup.py index 2622b1406..a06a879a5 100644 --- a/setup.py +++ b/setup.py @@ -31,6 +31,7 @@ "tests": [ "pytest<8.0.0", "nbmake", + "black", "scipy", "imageio[pyav]", "jupyterlab", From fb8aedc7e4a1d6ecc1e2bd1b7613f8353280439d Mon Sep 17 00:00:00 2001 From: Caitlin Lewis <69729525+clewis7@users.noreply.github.com> Date: Fri, 22 Mar 2024 21:17:33 -0400 Subject: [PATCH 30/66] update for changes in pygfx@main (#443) * update points material kwarg * update scatter examples & screenshots --- examples/desktop/scatter/scatter_cmap.py | 6 +----- examples/desktop/scatter/scatter_dataslice.py | 2 +- examples/desktop/scatter/scatter_size.py | 21 +++++++------------ examples/desktop/screenshots/scatter.png | 4 ++-- examples/desktop/screenshots/scatter_cmap.png | 4 ++-- .../screenshots/scatter_colorslice.png | 4 ++-- .../desktop/screenshots/scatter_dataslice.png | 4 ++-- .../desktop/screenshots/scatter_present.png | 4 ++-- examples/desktop/screenshots/scatter_size.png | 4 ++-- fastplotlib/graphics/scatter.py | 2 +- 10 files changed, 23 insertions(+), 32 deletions(-) diff --git a/examples/desktop/scatter/scatter_cmap.py b/examples/desktop/scatter/scatter_cmap.py index edc55a4b1..3e986d5d5 100644 --- a/examples/desktop/scatter/scatter_cmap.py +++ b/examples/desktop/scatter/scatter_cmap.py @@ -26,11 +26,7 @@ scatter_graphic = plot.add_scatter( - data=data[:, :-1], - sizes=15, - alpha=0.7, - cmap="Set1", - cmap_values=agg.labels_ + data=data[:, :-1], sizes=15, alpha=0.7, cmap="Set1", cmap_values=agg.labels_ ) plot.show() diff --git a/examples/desktop/scatter/scatter_dataslice.py b/examples/desktop/scatter/scatter_dataslice.py index 22c495bff..3008aab61 100644 --- a/examples/desktop/scatter/scatter_dataslice.py +++ b/examples/desktop/scatter/scatter_dataslice.py @@ -34,7 +34,7 @@ scatter_graphic.data[2] = np.array([[5.2, 2.7, 1.7]]) scatter_graphic.data[10:15] = scatter_graphic.data[0:5] + np.array([1, 1, 1]) -scatter_graphic.data[50:100:2] = scatter_graphic.data[100:150:2] + np.array([1,1,0]) +scatter_graphic.data[50:100:2] = scatter_graphic.data[100:150:2] + np.array([1, 1, 0]) if __name__ == "__main__": diff --git a/examples/desktop/scatter/scatter_size.py b/examples/desktop/scatter/scatter_size.py index 2ad995584..5c1f97703 100644 --- a/examples/desktop/scatter/scatter_size.py +++ b/examples/desktop/scatter/scatter_size.py @@ -12,28 +12,23 @@ grid_shape = (2, 1) # you can give string names for each subplot within the gridplot -names = [ - ["scalar_size"], - ["array_size"] -] +names = [["scalar_size"], ["array_size"]] # Create the grid plot -plot = fpl.GridPlot( - shape=grid_shape, - names=names, - size=(1000, 1000) -) +plot = fpl.GridPlot(shape=grid_shape, names=names, size=(1000, 1000)) # get y_values using sin function -angles = np.arange(0, 20*np.pi+0.001, np.pi / 20) -y_values = 30*np.sin(angles) # 1 thousand points +angles = np.arange(0, 20 * np.pi + 0.001, np.pi / 20) +y_values = 30 * np.sin(angles) # 1 thousand points x_values = np.array([x for x in range(len(y_values))], dtype=np.float32) data = np.column_stack([x_values, y_values]) -plot["scalar_size"].add_scatter(data=data, sizes=5, colors="blue") # add a set of scalar sizes +plot["scalar_size"].add_scatter( + data=data, sizes=5, colors="blue" +) # add a set of scalar sizes -non_scalar_sizes = np.abs((y_values / np.pi)) # ensure minimum size of 5 +non_scalar_sizes = np.abs((y_values / np.pi)) # ensure minimum size of 5 plot["array_size"].add_scatter(data=data, sizes=non_scalar_sizes, colors="red") for graph in plot: diff --git a/examples/desktop/screenshots/scatter.png b/examples/desktop/screenshots/scatter.png index bf5e8c92a..d01d36707 100644 --- a/examples/desktop/screenshots/scatter.png +++ b/examples/desktop/screenshots/scatter.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bd38399b77e09d915c5bb1e7ee022f936ae90682f598357bc774a95c372dc78f -size 25231 +oid sha256:8d840f02d1c4be5ea11adfc224481a5b8b306cbc904e099af4d3fdd5ab7f383f +size 26683 diff --git a/examples/desktop/screenshots/scatter_cmap.png b/examples/desktop/screenshots/scatter_cmap.png index eec22566a..7f0bba38a 100644 --- a/examples/desktop/screenshots/scatter_cmap.png +++ b/examples/desktop/screenshots/scatter_cmap.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e712693b403166909dcaa65256131eacba0a15892cd144ad97fdecb6b9835e93 -size 57273 +oid sha256:e9aba2f37c7682d68569e1bac7afac5f243afb98ab94d9957de4b59f9d3dd1c0 +size 57257 diff --git a/examples/desktop/screenshots/scatter_colorslice.png b/examples/desktop/screenshots/scatter_colorslice.png index 0da0fcd9f..27249e63e 100644 --- a/examples/desktop/screenshots/scatter_colorslice.png +++ b/examples/desktop/screenshots/scatter_colorslice.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c620cad9976f390e44a5b037f3ff61fb80e6487e17f4be8118be5df55f276a35 -size 23664 +oid sha256:39c49529552d6ace3d67b37f0c660e9734fcb763bdc165484f356ad8cffc908e +size 25218 diff --git a/examples/desktop/screenshots/scatter_dataslice.png b/examples/desktop/screenshots/scatter_dataslice.png index 32f56ad11..155510885 100644 --- a/examples/desktop/screenshots/scatter_dataslice.png +++ b/examples/desktop/screenshots/scatter_dataslice.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:69d2f0999b0bb334e48320702095fc76444f4d89d43a51ac6c5c8f49e1df96ac -size 25999 +oid sha256:20757e215c4c208e08027f8e2b798691f421ce7662dc86be3615dc41084686f3 +size 27392 diff --git a/examples/desktop/screenshots/scatter_present.png b/examples/desktop/screenshots/scatter_present.png index 8c1e5eed4..87685fe90 100644 --- a/examples/desktop/screenshots/scatter_present.png +++ b/examples/desktop/screenshots/scatter_present.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e45c1a936771e569e562ed3496421e498e725325093e84243ab494c0718ead3a -size 23639 +oid sha256:7a8ceb8f3d7203f0569374993784f2229448ace08b0abee94910e6ae71ceca29 +size 24587 diff --git a/examples/desktop/screenshots/scatter_size.png b/examples/desktop/screenshots/scatter_size.png index da211cde1..afe8a0b35 100644 --- a/examples/desktop/screenshots/scatter_size.png +++ b/examples/desktop/screenshots/scatter_size.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:10533aa5831a50a0f9b38c0a60b89a9b6c33311ecb3a569c5e0b4c82379dc20a -size 66037 +oid sha256:a184ce0a202bc03fa61b3f4149109e44160bfd326c26f46db3b83ce0cf1699a6 +size 67710 diff --git a/fastplotlib/graphics/scatter.py b/fastplotlib/graphics/scatter.py index f6104aeb7..1c579eaa5 100644 --- a/fastplotlib/graphics/scatter.py +++ b/fastplotlib/graphics/scatter.py @@ -93,7 +93,7 @@ def __init__( pygfx.Geometry( positions=self.data(), sizes=self.sizes(), colors=self.colors() ), - material=pygfx.PointsMaterial(color_mode="vertex", vertex_sizes=True), + material=pygfx.PointsMaterial(color_mode="vertex", size_mode="vertex"), ) self._set_world_object(world_object) From 758d3ed45253195aab09528beca2494b2a128151 Mon Sep 17 00:00:00 2001 From: Caitlin Lewis <69729525+clewis7@users.noreply.github.com> Date: Fri, 22 Mar 2024 21:35:51 -0400 Subject: [PATCH 31/66] add codeowners file (#442) --- .github/CODEOWNERS | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 .github/CODEOWNERS diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 000000000..a3e89ef76 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,2 @@ +* @kushalkolar +* @clewis7 \ No newline at end of file From 97168ff2bef8c6d732da4266afd8fd8f10e983e6 Mon Sep 17 00:00:00 2001 From: Edoardo Balzani Date: Wed, 27 Mar 2024 19:40:39 -0400 Subject: [PATCH 32/66] Rotation (#439) * added rotation property * added a rotate method to rotate along axis --- fastplotlib/graphics/_base.py | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/fastplotlib/graphics/_base.py b/fastplotlib/graphics/_base.py index 91ccb143e..f7a0ec102 100644 --- a/fastplotlib/graphics/_base.py +++ b/fastplotlib/graphics/_base.py @@ -5,6 +5,7 @@ from dataclasses import dataclass import numpy as np +import pylinalg as la from pygfx import WorldObject @@ -142,6 +143,14 @@ def position_y(self, val): def position_z(self, val): self.world_object.world.z = val + @property + def rotation(self): + return self.world_object.local.rotation + + @rotation.setter + def rotation(self, val): + self.world_object.local.rotation = val + @property def visible(self) -> bool: """Access or change the visibility.""" @@ -196,6 +205,28 @@ def __del__(self): self.deleted = True del WORLD_OBJECTS[self.loc] + def rotate(self, alpha: float, axis: Literal["x", "y", "z"] = "y"): + """Rotate the Graphic with respect to the world. + + Parameters + ---------- + alpha : + Rotation angle in radians. + axis : + Rotation axis label. + """ + if axis == "x": + rot = la.quat_from_euler((alpha, 0), order="XY") + elif axis == "y": + rot = la.quat_from_euler((0, alpha), order="XY") + elif axis == "z": + rot = la.quat_from_euler((0, alpha), order="XZ") + else: + raise ValueError( + f"`axis` must be either `x`, `y`, or `z`. `{axis}` provided instead!" + ) + self.rotation = la.quat_mul(rot, self.rotation) + class Interaction(ABC): """Mixin class that makes graphics interactive""" From 99e8a0b1036ba7a8e635c358869c3b1ee6a3c50d Mon Sep 17 00:00:00 2001 From: Kushal Kolar Date: Wed, 27 Mar 2024 19:44:06 -0400 Subject: [PATCH 33/66] require only one reviewer (#455) --- .github/CODEOWNERS | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index a3e89ef76..f33328dce 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1,2 +1 @@ -* @kushalkolar -* @clewis7 \ No newline at end of file +* @kushalkolar @clewis7 From 002165259d0a83eec93e37174e8a1b59e07a1a11 Mon Sep 17 00:00:00 2001 From: Almar Klein Date: Fri, 29 Mar 2024 02:20:19 +0100 Subject: [PATCH 34/66] Use simpler form of super() where possible (#453) --- fastplotlib/graphics/_base.py | 2 +- fastplotlib/graphics/_features/_colors.py | 11 +++++------ fastplotlib/graphics/_features/_data.py | 6 ++---- fastplotlib/graphics/_features/_deleted.py | 2 +- fastplotlib/graphics/_features/_present.py | 2 +- fastplotlib/graphics/_features/_selection_features.py | 4 ++-- fastplotlib/graphics/_features/_sizes.py | 4 +--- fastplotlib/graphics/_features/_thickness.py | 2 +- fastplotlib/graphics/histogram.py | 6 ++---- fastplotlib/graphics/line.py | 2 +- fastplotlib/graphics/line_collection.py | 4 ++-- fastplotlib/graphics/scatter.py | 2 +- fastplotlib/graphics/selectors/_rectangle_region.py | 2 +- fastplotlib/graphics/text.py | 2 +- fastplotlib/layouts/_plot.py | 4 ++-- fastplotlib/layouts/_subplot.py | 4 ++-- 16 files changed, 26 insertions(+), 33 deletions(-) diff --git a/fastplotlib/graphics/_base.py b/fastplotlib/graphics/_base.py index f7a0ec102..fdf120268 100644 --- a/fastplotlib/graphics/_base.py +++ b/fastplotlib/graphics/_base.py @@ -439,7 +439,7 @@ class GraphicCollection(Graphic): """Graphic Collection base class""" def __init__(self, name: str = None): - super(GraphicCollection, self).__init__(name) + super().__init__(name) self._graphics: List[str] = list() self._graphics_changed: bool = True diff --git a/fastplotlib/graphics/_features/_colors.py b/fastplotlib/graphics/_features/_colors.py index ad9e673ef..48405e74c 100644 --- a/fastplotlib/graphics/_features/_colors.py +++ b/fastplotlib/graphics/_features/_colors.py @@ -124,9 +124,7 @@ def __init__( if alpha != 1.0: data[:, -1] = alpha - super(ColorFeature, self).__init__( - parent, data, collection_index=collection_index - ) + super().__init__(parent, data, collection_index=collection_index) def __setitem__(self, key, value): # parse numerical slice indices @@ -253,6 +251,7 @@ class CmapFeature(ColorFeature): """ def __init__(self, parent, colors, cmap_name: str, cmap_values: np.ndarray): + # Skip the ColorFeature's __init__ super(ColorFeature, self).__init__(parent, colors) self._cmap_name = cmap_name @@ -278,7 +277,7 @@ def __setitem__(self, key, cmap_name): ) self._cmap_name = cmap_name - super(CmapFeature, self).__setitem__(key, colors) + super().__setitem__(key, colors) @property def name(self) -> str: @@ -299,7 +298,7 @@ def values(self, values: np.ndarray): self._cmap_values = values - super(CmapFeature, self).__setitem__(slice(None), colors) + super().__setitem__(slice(None), colors) def __repr__(self) -> str: s = f"CmapFeature for {self._parent}, to get name or values: `.cmap.name`, `.cmap.values`" @@ -330,7 +329,7 @@ class ImageCmapFeature(GraphicFeature): def __init__(self, parent, cmap: str): cmap_texture_view = get_cmap_texture(cmap) - super(ImageCmapFeature, self).__init__(parent, cmap_texture_view) + super().__init__(parent, cmap_texture_view) self._name = cmap def _set(self, cmap_name: str): diff --git a/fastplotlib/graphics/_features/_data.py b/fastplotlib/graphics/_features/_data.py index 23e80b470..bcfe9446a 100644 --- a/fastplotlib/graphics/_features/_data.py +++ b/fastplotlib/graphics/_features/_data.py @@ -21,9 +21,7 @@ class PointsDataFeature(GraphicFeatureIndexable): 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().__init__(parent, data, collection_index=collection_index) @property def buffer(self) -> pygfx.Buffer: @@ -117,7 +115,7 @@ def __init__(self, parent, data: Any): "``[x_dim, y_dim]`` or ``[x_dim, y_dim, rgb]``" ) - super(ImageDataFeature, self).__init__(parent, data) + super().__init__(parent, data) @property def buffer(self) -> pygfx.Texture: diff --git a/fastplotlib/graphics/_features/_deleted.py b/fastplotlib/graphics/_features/_deleted.py index 2fca1c719..7900385eb 100644 --- a/fastplotlib/graphics/_features/_deleted.py +++ b/fastplotlib/graphics/_features/_deleted.py @@ -16,7 +16,7 @@ class Deleted(GraphicFeature): """ def __init__(self, parent, value: bool): - super(Deleted, self).__init__(parent, value) + super().__init__(parent, value) def _set(self, value: bool): value = self._parse_set_value(value) diff --git a/fastplotlib/graphics/_features/_present.py b/fastplotlib/graphics/_features/_present.py index 6fbf93b48..a73d66523 100644 --- a/fastplotlib/graphics/_features/_present.py +++ b/fastplotlib/graphics/_features/_present.py @@ -23,7 +23,7 @@ class PresentFeature(GraphicFeature): def __init__(self, parent, present: bool = True, collection_index: int = False): self._scene = None - super(PresentFeature, self).__init__(parent, present, collection_index) + super().__init__(parent, present, collection_index) def _set(self, present: bool): present = self._parse_set_value(present) diff --git a/fastplotlib/graphics/_features/_selection_features.py b/fastplotlib/graphics/_features/_selection_features.py index 294bb15d6..21e5d0a09 100644 --- a/fastplotlib/graphics/_features/_selection_features.py +++ b/fastplotlib/graphics/_features/_selection_features.py @@ -27,7 +27,7 @@ class LinearSelectionFeature(GraphicFeature): """ def __init__(self, parent, axis: str, value: float, limits: Tuple[int, int]): - super(LinearSelectionFeature, self).__init__(parent, data=value) + super().__init__(parent, data=value) self._axis = axis self._limits = limits @@ -99,7 +99,7 @@ class LinearRegionSelectionFeature(GraphicFeature): def __init__( self, parent, selection: Tuple[int, int], axis: str, limits: Tuple[int, int] ): - super(LinearRegionSelectionFeature, self).__init__(parent, data=selection) + super().__init__(parent, data=selection) self._axis = axis self._limits = limits diff --git a/fastplotlib/graphics/_features/_sizes.py b/fastplotlib/graphics/_features/_sizes.py index 403760508..2ceeb7862 100644 --- a/fastplotlib/graphics/_features/_sizes.py +++ b/fastplotlib/graphics/_features/_sizes.py @@ -21,9 +21,7 @@ class PointsSizesFeature(GraphicFeatureIndexable): def __init__(self, parent, sizes: Any, collection_index: int = None): sizes = self._fix_sizes(sizes, parent) - super(PointsSizesFeature, self).__init__( - parent, sizes, collection_index=collection_index - ) + super().__init__(parent, sizes, collection_index=collection_index) @property def buffer(self) -> pygfx.Buffer: diff --git a/fastplotlib/graphics/_features/_thickness.py b/fastplotlib/graphics/_features/_thickness.py index f9190f0b1..fc90ef96f 100644 --- a/fastplotlib/graphics/_features/_thickness.py +++ b/fastplotlib/graphics/_features/_thickness.py @@ -19,7 +19,7 @@ class ThicknessFeature(GraphicFeature): def __init__(self, parent, thickness: float): self._scene = None - super(ThicknessFeature, self).__init__(parent, thickness) + super().__init__(parent, thickness) def _set(self, value: float): value = self._parse_set_value(value) diff --git a/fastplotlib/graphics/histogram.py b/fastplotlib/graphics/histogram.py index 6efd83a96..b78be39d3 100644 --- a/fastplotlib/graphics/histogram.py +++ b/fastplotlib/graphics/histogram.py @@ -10,7 +10,7 @@ class _HistogramBin(pygfx.Mesh): def __int__(self, *args, **kwargs): - super(_HistogramBin, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) self.bin_center: float = None self.frequency: Union[int, float] = None @@ -93,9 +93,7 @@ def __init__( data = np.vstack([x_positions_bins, self.hist]) - super(HistogramGraphic, self).__init__( - data=data, colors=colors, n_colors=n_bins, **kwargs - ) + super().__init__(data=data, colors=colors, n_colors=n_bins, **kwargs) self._world_object: pygfx.Group = pygfx.Group() diff --git a/fastplotlib/graphics/line.py b/fastplotlib/graphics/line.py index 9ac7568a7..d76c8e704 100644 --- a/fastplotlib/graphics/line.py +++ b/fastplotlib/graphics/line.py @@ -102,7 +102,7 @@ def __init__( self, self.colors(), cmap_name=cmap, cmap_values=cmap_values ) - super(LineGraphic, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) if thickness < 1.1: material = pygfx.LineThinMaterial diff --git a/fastplotlib/graphics/line_collection.py b/fastplotlib/graphics/line_collection.py index a5c398130..bb7bb2444 100644 --- a/fastplotlib/graphics/line_collection.py +++ b/fastplotlib/graphics/line_collection.py @@ -87,7 +87,7 @@ def __init__( """ - super(LineCollection, self).__init__(name) + super().__init__(name) if not isinstance(z_position, float) and z_position is not None: if len(data) != len(z_position): @@ -544,7 +544,7 @@ def __init__( See :class:`LineGraphic` details on the features. """ - super(LineStack, self).__init__( + super().__init__( data=data, z_position=z_position, thickness=thickness, diff --git a/fastplotlib/graphics/scatter.py b/fastplotlib/graphics/scatter.py index 1c579eaa5..3f04f644e 100644 --- a/fastplotlib/graphics/scatter.py +++ b/fastplotlib/graphics/scatter.py @@ -87,7 +87,7 @@ def __init__( ) self.sizes = PointsSizesFeature(self, sizes) - super(ScatterGraphic, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) world_object = pygfx.Points( pygfx.Geometry( diff --git a/fastplotlib/graphics/selectors/_rectangle_region.py b/fastplotlib/graphics/selectors/_rectangle_region.py index 0d7dd3661..1081a49a9 100644 --- a/fastplotlib/graphics/selectors/_rectangle_region.py +++ b/fastplotlib/graphics/selectors/_rectangle_region.py @@ -28,7 +28,7 @@ class RectangleBoundsFeature(GraphicFeature): def __init__( self, parent, bounds: Tuple[int, int], axis: str, limits: Tuple[int, int] ): - super(RectangleBoundsFeature, self).__init__(parent, data=bounds) + super().__init__(parent, data=bounds) self._axis = axis self.limits = limits diff --git a/fastplotlib/graphics/text.py b/fastplotlib/graphics/text.py index a8a873287..a486b1bd2 100644 --- a/fastplotlib/graphics/text.py +++ b/fastplotlib/graphics/text.py @@ -55,7 +55,7 @@ def __init__( * Vertical values: "top", "middle", "baseline", "bottom" * Horizontal values: "left", "center", "right" """ - super(TextGraphic, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) self._text = text diff --git a/fastplotlib/layouts/_plot.py b/fastplotlib/layouts/_plot.py index 34027a276..77aba0adc 100644 --- a/fastplotlib/layouts/_plot.py +++ b/fastplotlib/layouts/_plot.py @@ -45,7 +45,7 @@ def __init__( passed to Subplot, for example ``name`` """ - super(Plot, self).__init__( + super().__init__( parent=None, position=(0, 0), parent_dims=(1, 1), @@ -62,7 +62,7 @@ def __init__( def render(self): """performs a single render of the plot, not for the user""" - super(Plot, self).render() + super().render() self.renderer.flush() self.canvas.request_draw() diff --git a/fastplotlib/layouts/_subplot.py b/fastplotlib/layouts/_subplot.py index e776fddb6..6fa5f890e 100644 --- a/fastplotlib/layouts/_subplot.py +++ b/fastplotlib/layouts/_subplot.py @@ -214,7 +214,7 @@ def __init__( self._size = size - super(Dock, self).__init__( + super().__init__( parent=parent, position=position, camera=pygfx.OrthographicCamera(), @@ -349,4 +349,4 @@ def render(self): if self.size == 0: return - super(Dock, self).render() + super().render() From 60cf919cd5c38dd9d500437cbb0dbb7bfef8a8af Mon Sep 17 00:00:00 2001 From: Kushal Kolar Date: Sat, 30 Mar 2024 01:48:49 -0400 Subject: [PATCH 35/66] FASTPLOTLIB_NB_TESTS required for nb test funcs to run (#460) --- .github/workflows/ci.yml | 2 +- examples/notebooks/nb_test_utils.py | 15 +++++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 40d39c4f6..8f92bafb8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -93,7 +93,7 @@ jobs: PYGFX_EXPECT_LAVAPIPE: true run: | pytest -v examples - pytest --nbmake examples/notebooks/ + FASTPLOTLIB_NB_TESTS=1 pytest --nbmake examples/notebooks/ - uses: actions/upload-artifact@v3 if: ${{ failure() }} with: diff --git a/examples/notebooks/nb_test_utils.py b/examples/notebooks/nb_test_utils.py index e16ed2eaf..71d2d5114 100644 --- a/examples/notebooks/nb_test_utils.py +++ b/examples/notebooks/nb_test_utils.py @@ -21,7 +21,19 @@ FAILURES = list() +def _run_tests(): + if "FASTPLOTLIB_NB_TESTS" not in os.environ.keys(): + return False + + if os.environ["FASTPLOTLIB_NB_TESTS"] == "1": + return True + + return False + + def plot_test(name, plot: Union[Plot, GridPlot]): + if not _run_tests(): + return snapshot = plot.canvas.snapshot() if "REGENERATE_SCREENSHOTS" in os.environ.keys(): @@ -81,6 +93,9 @@ def get_diffs_rgba(slicer): def notebook_finished(): + if not _run_tests(): + return + if len(FAILURES) > 0: raise AssertionError( f"Failures for plots:\n{FAILURES}" From af23b584ba50d0a5b9fc7848dc166cc3aa702c37 Mon Sep 17 00:00:00 2001 From: Almar Klein Date: Sun, 31 Mar 2024 01:22:30 +0100 Subject: [PATCH 36/66] Improvements to GUI selection and Qt support (#431) * Improved GUI selection wip * Support all modern qt libs * comment * make the ipywidgets imports conditional * Adjust for new wgpu * set min wgpu version * fixes * top might not necessarily the default --- docs/source/user_guide/gpu.rst | 39 ++----- fastplotlib/__init__.py | 9 +- fastplotlib/graphics/selectors/_linear.py | 16 ++- .../graphics/selectors/_linear_region.py | 18 ++- fastplotlib/layouts/_frame/_frame.py | 40 +------ fastplotlib/layouts/_frame/_qt_output.py | 3 +- fastplotlib/layouts/_frame/_qt_toolbar.py | 3 +- .../layouts/_frame/_qtoolbar_template.py | 5 +- fastplotlib/layouts/_gridplot.py | 12 +- fastplotlib/layouts/_plot.py | 4 +- fastplotlib/layouts/_plot_area.py | 6 +- fastplotlib/layouts/_subplot.py | 4 +- fastplotlib/layouts/_utils.py | 74 ++---------- fastplotlib/utils/__init__.py | 1 - fastplotlib/utils/_gpu_info.py | 66 ----------- fastplotlib/utils/gui.py | 108 ++++++++++++++++++ fastplotlib/widgets/image.py | 16 +-- setup.py | 1 + 18 files changed, 172 insertions(+), 253 deletions(-) delete mode 100644 fastplotlib/utils/_gpu_info.py create mode 100644 fastplotlib/utils/gui.py diff --git a/docs/source/user_guide/gpu.rst b/docs/source/user_guide/gpu.rst index 006f78872..18fb81cdf 100644 --- a/docs/source/user_guide/gpu.rst +++ b/docs/source/user_guide/gpu.rst @@ -33,39 +33,18 @@ View available GPU You can view all GPUs that are available to ``WGPU`` like this:: - from wgpu.backends.wgpu_native import enumerate_adapters - from pprint import pprint + import wgpu - for adapter in enumerate_adapters(): - pprint(adapter.request_adapter_info()) + for adapter in wgpu.gpu.enumerate_adapters(): + print(adapter.summary) For example, on a Thinkpad AMD laptop with a dedicated nvidia GPU this returns:: - {'adapter_type': 'IntegratedGPU', - 'architecture': '', - 'backend_type': 'Vulkan', - 'description': 'Mesa 22.3.6', - 'device': 'AMD Radeon Graphics (RADV REMBRANDT)', - 'vendor': 'radv'} - {'adapter_type': 'DiscreteGPU', - 'architecture': '', - 'backend_type': 'Vulkan', - 'description': '535.129.03', - 'device': 'NVIDIA T1200 Laptop GPU', - 'vendor': 'NVIDIA'} - {'adapter_type': 'CPU', - 'architecture': '', - 'backend_type': 'Vulkan', - 'description': 'Mesa 22.3.6 (LLVM 15.0.6)', - 'device': 'llvmpipe (LLVM 15.0.6, 256 bits)', - 'vendor': 'llvmpipe'} - {'adapter_type': 'Unknown', - 'architecture': '', - 'backend_type': 'OpenGL', - 'description': '', - 'device': 'AMD Radeon Graphics (rembrandt, LLVM 15.0.6, DRM 3.52, ' - '6.4.0-0.deb12.2-amd64)', - 'vendor': ''} + AMD Radeon Graphics (RADV REMBRANDT) (IntegratedGPU) on Vulkan + NVIDIA T1200 Laptop GPU (DiscreteGPU) on Vulkan + llvmpipe (LLVM 15.0.6, 256 bits) (CPU) on Vulkan + AMD Radeon Graphics (rembrandt, LLVM 15.0.6, DRM 3.52, 6.4.0-0.deb12.2-amd64) (Unknown) on OpenGL + GPU currently in use -------------------- @@ -78,5 +57,5 @@ If you want to know the GPU that a current plot is using you can check the adapt plot.show() # GPU that is currently in use by the renderer - plot.renderer.device.adapter.request_adapter_info() + print(plot.renderer.device.adapter.summary) diff --git a/fastplotlib/__init__.py b/fastplotlib/__init__.py index 27545f0ad..33db8c79d 100644 --- a/fastplotlib/__init__.py +++ b/fastplotlib/__init__.py @@ -5,21 +5,20 @@ from .graphics.selectors import * from .legends import * from .widgets import ImageWidget -from .utils import _notebook_print_banner, config +from .utils import config +from .utils.gui import run -from wgpu.gui.auto import run -from wgpu.backends.wgpu_native import enumerate_adapters +import wgpu with open(Path(__file__).parent.joinpath("VERSION"), "r") as f: __version__ = f.read().split("\n")[0] -adapters = [a.request_adapter_info() for a in enumerate_adapters()] +adapters = [a.summary for a in wgpu.gpu.enumerate_adapters()] if len(adapters) < 1: raise IndexError("No WGPU adapters found, fastplotlib will not work.") -_notebook_print_banner() __all__ = [ "Plot", diff --git a/fastplotlib/graphics/selectors/_linear.py b/fastplotlib/graphics/selectors/_linear.py index ff617c5e3..886ccbaaf 100644 --- a/fastplotlib/graphics/selectors/_linear.py +++ b/fastplotlib/graphics/selectors/_linear.py @@ -3,21 +3,19 @@ from numbers import Real import numpy as np - import pygfx -try: - import ipywidgets - - HAS_IPYWIDGETS = True -except (ImportError, ModuleNotFoundError): - HAS_IPYWIDGETS = False - +from ...utils.gui import IS_JUPYTER from .._base import Graphic, GraphicCollection from .._features._selection_features import LinearSelectionFeature from ._base_selector import BaseSelector +if IS_JUPYTER: + # If using the jupyter backend, user has jupyter_rfb, and thus also ipywidgets + import ipywidgets + + class LinearSelector(BaseSelector): @property def limits(self) -> Tuple[float, float]: @@ -240,7 +238,7 @@ def make_ipywidget_slider(self, kind: str = "IntSlider", **kwargs): """ - if not HAS_IPYWIDGETS: + if not IS_JUPYTER: raise ImportError( "Must installed `ipywidgets` to use `make_ipywidget_slider()`" ) diff --git a/fastplotlib/graphics/selectors/_linear_region.py b/fastplotlib/graphics/selectors/_linear_region.py index 4ffbd2cc2..b88174ddb 100644 --- a/fastplotlib/graphics/selectors/_linear_region.py +++ b/fastplotlib/graphics/selectors/_linear_region.py @@ -1,20 +1,18 @@ from typing import * from numbers import Real -try: - import ipywidgets - - HAS_IPYWIDGETS = True -except (ImportError, ModuleNotFoundError): - HAS_IPYWIDGETS = False - import numpy as np - import pygfx +from ...utils.gui import IS_JUPYTER from .._base import Graphic, GraphicCollection -from ._base_selector import BaseSelector from .._features._selection_features import LinearRegionSelectionFeature +from ._base_selector import BaseSelector + + +if IS_JUPYTER: + # If using the jupyter backend, user has jupyter_rfb, and thus also ipywidgets + import ipywidgets class LinearRegionSelector(BaseSelector): @@ -390,7 +388,7 @@ def make_ipywidget_slider(self, kind: str = "IntRangeSlider", **kwargs): """ - if not HAS_IPYWIDGETS: + if not IS_JUPYTER: raise ImportError( "Must installed `ipywidgets` to use `make_ipywidget_slider()`" ) diff --git a/fastplotlib/layouts/_frame/_frame.py b/fastplotlib/layouts/_frame/_frame.py index 2b76b8124..219a59082 100644 --- a/fastplotlib/layouts/_frame/_frame.py +++ b/fastplotlib/layouts/_frame/_frame.py @@ -1,41 +1,7 @@ import os -from ._toolbar import ToolBar - from ...graphics import ImageGraphic - -from .._utils import CANVAS_OPTIONS_AVAILABLE - - -class UnavailableOutputContext: - # called when a requested output context is not available - # ex: if trying to force jupyter_rfb canvas but jupyter_rfb is not installed - def __init__(self, context_name, msg): - self.context_name = context_name - self.msg = msg - - def __call__(self, *args, **kwargs): - raise ModuleNotFoundError( - f"The following output context is not available: {self.context_name}\n{self.msg}" - ) - - -# TODO: potentially put all output context and toolbars in their own module and have this determination done at import -if CANVAS_OPTIONS_AVAILABLE["jupyter"]: - from ._jupyter_output import JupyterOutputContext -else: - JupyterOutputContext = UnavailableOutputContext( - "Jupyter", - "You must install fastplotlib using the `'notebook'` option to use this context:\n" - 'pip install "fastplotlib[notebook]"', - ) - -if CANVAS_OPTIONS_AVAILABLE["qt"]: - from ._qt_output import QOutputContext -else: - QtOutput = UnavailableOutputContext( - "Qt", "You must install `PyQt6` to use this output context" - ) +from ._toolbar import ToolBar class Frame: @@ -158,6 +124,8 @@ def show( # return the appropriate OutputContext based on the current canvas if self.canvas.__class__.__name__ == "JupyterWgpuCanvas": + from ._jupyter_output import JupyterOutputContext # noqa - inline import + self._output = JupyterOutputContext( frame=self, make_toolbar=toolbar, @@ -167,6 +135,8 @@ def show( ) elif self.canvas.__class__.__name__ == "QWgpuCanvas": + from ._qt_output import QOutputContext # noqa - inline import + self._output = QOutputContext( frame=self, make_toolbar=toolbar, add_widgets=add_widgets ) diff --git a/fastplotlib/layouts/_frame/_qt_output.py b/fastplotlib/layouts/_frame/_qt_output.py index e8be2d050..d7e7f2612 100644 --- a/fastplotlib/layouts/_frame/_qt_output.py +++ b/fastplotlib/layouts/_frame/_qt_output.py @@ -1,5 +1,4 @@ -from PyQt6 import QtWidgets - +from ...utils.gui import QtWidgets from ._qt_toolbar import QToolbar diff --git a/fastplotlib/layouts/_frame/_qt_toolbar.py b/fastplotlib/layouts/_frame/_qt_toolbar.py index 4ee073701..d62994c2d 100644 --- a/fastplotlib/layouts/_frame/_qt_toolbar.py +++ b/fastplotlib/layouts/_frame/_qt_toolbar.py @@ -4,8 +4,7 @@ import traceback from typing import * -from PyQt6 import QtWidgets, QtCore - +from ...utils.gui import QtCore, QtWidgets from ...graphics.selectors import PolygonSelector from ._toolbar import ToolBar from ._qtoolbar_template import Ui_QToolbar diff --git a/fastplotlib/layouts/_frame/_qtoolbar_template.py b/fastplotlib/layouts/_frame/_qtoolbar_template.py index a8a1c6f86..d2311c595 100644 --- a/fastplotlib/layouts/_frame/_qtoolbar_template.py +++ b/fastplotlib/layouts/_frame/_qtoolbar_template.py @@ -5,8 +5,7 @@ # WARNING: Any manual changes made to this file will be lost when pyuic6 is # run again. Do not edit this file unless you know what you are doing. - -from PyQt6 import QtCore, QtGui, QtWidgets +from ...utils.gui import QtGui, QtCore, QtWidgets class Ui_QToolbar(object): @@ -30,7 +29,7 @@ def setupUi(self, QToolbar): self.maintain_aspect_button = QtWidgets.QPushButton(parent=QToolbar) font = QtGui.QFont() font.setBold(True) - font.setWeight(75) + font.setWeight(QtGui.QFont.Weight.Bold) self.maintain_aspect_button.setFont(font) self.maintain_aspect_button.setCheckable(True) self.maintain_aspect_button.setObjectName("maintain_aspect_button") diff --git a/fastplotlib/layouts/_gridplot.py b/fastplotlib/layouts/_gridplot.py index 04046cd01..fa987b661 100644 --- a/fastplotlib/layouts/_gridplot.py +++ b/fastplotlib/layouts/_gridplot.py @@ -6,7 +6,7 @@ import pygfx -from wgpu.gui.auto import WgpuCanvas +from wgpu.gui import WgpuCanvasBase from ._frame import Frame from ._utils import make_canvas_and_renderer, create_controller, create_camera @@ -22,7 +22,7 @@ def __init__( cameras: Union[str, list, np.ndarray] = "2d", controller_types: Union[str, list, np.ndarray] = None, controller_ids: Union[str, list, np.ndarray] = None, - canvas: Union[str, WgpuCanvas, pygfx.Texture] = None, + canvas: Union[str, WgpuCanvasBase, pygfx.Texture] = None, renderer: pygfx.WgpuRenderer = None, size: Tuple[int, int] = (500, 300), names: Union[list, np.ndarray] = None, @@ -219,12 +219,6 @@ def __init__( for cam in cams[1:]: _controller.add_camera(cam) - if canvas is None: - canvas = WgpuCanvas() - - if renderer is None: - renderer = pygfx.renderers.WgpuRenderer(canvas) - self._canvas = canvas self._renderer = renderer @@ -266,7 +260,7 @@ def __init__( Frame.__init__(self) @property - def canvas(self) -> WgpuCanvas: + def canvas(self) -> WgpuCanvasBase: """The canvas associated to this GridPlot""" return self._canvas diff --git a/fastplotlib/layouts/_plot.py b/fastplotlib/layouts/_plot.py index 77aba0adc..4656649e6 100644 --- a/fastplotlib/layouts/_plot.py +++ b/fastplotlib/layouts/_plot.py @@ -1,7 +1,7 @@ from typing import * import pygfx -from wgpu.gui.auto import WgpuCanvas +from wgpu.gui import WgpuCanvasBase from ._subplot import Subplot from ._frame import Frame @@ -11,7 +11,7 @@ class Plot(Subplot, Frame, RecordMixin): def __init__( self, - canvas: Union[str, WgpuCanvas] = None, + canvas: Union[str, WgpuCanvasBase] = None, renderer: pygfx.WgpuRenderer = None, camera: Union[str, pygfx.PerspectiveCamera] = "2d", controller: Union[str, pygfx.Controller] = None, diff --git a/fastplotlib/layouts/_plot_area.py b/fastplotlib/layouts/_plot_area.py index 08a09baa7..2c93d7e9e 100644 --- a/fastplotlib/layouts/_plot_area.py +++ b/fastplotlib/layouts/_plot_area.py @@ -7,7 +7,7 @@ import pygfx from pylinalg import vec_transform, vec_unproject -from wgpu.gui.auto import WgpuCanvas +from wgpu.gui import WgpuCanvasBase from ._utils import create_camera, create_controller from ..graphics._base import Graphic @@ -29,7 +29,7 @@ def __init__( camera: Union[pygfx.PerspectiveCamera], controller: Union[pygfx.Controller], scene: pygfx.Scene, - canvas: WgpuCanvas, + canvas: WgpuCanvasBase, renderer: pygfx.WgpuRenderer, name: str = None, ): @@ -122,7 +122,7 @@ def scene(self) -> pygfx.Scene: return self._scene @property - def canvas(self) -> WgpuCanvas: + def canvas(self) -> WgpuCanvasBase: """Canvas associated to the plot area""" return self._canvas diff --git a/fastplotlib/layouts/_subplot.py b/fastplotlib/layouts/_subplot.py index 6fa5f890e..509840fa7 100644 --- a/fastplotlib/layouts/_subplot.py +++ b/fastplotlib/layouts/_subplot.py @@ -4,7 +4,7 @@ import pygfx -from wgpu.gui.auto import WgpuCanvas +from wgpu.gui import WgpuCanvasBase from ..graphics import TextGraphic from ._utils import make_canvas_and_renderer, create_camera, create_controller @@ -20,7 +20,7 @@ def __init__( parent_dims: Tuple[int, int] = None, camera: Union[str, pygfx.PerspectiveCamera] = "2d", controller: Union[str, pygfx.Controller] = None, - canvas: Union[str, WgpuCanvas, pygfx.Texture] = None, + canvas: Union[str, WgpuCanvasBase, pygfx.Texture] = None, renderer: pygfx.WgpuRenderer = None, name: str = None, ): diff --git a/fastplotlib/layouts/_utils.py b/fastplotlib/layouts/_utils.py index 1662f00c5..5ee930b67 100644 --- a/fastplotlib/layouts/_utils.py +++ b/fastplotlib/layouts/_utils.py @@ -1,64 +1,15 @@ from typing import * +import importlib import pygfx from pygfx import WgpuRenderer, Texture +from wgpu.gui import WgpuCanvasBase -# default auto-determined canvas -from wgpu.gui.auto import WgpuCanvas -from wgpu.gui.base import WgpuCanvasBase - - -# TODO: this determination can be better -try: - from wgpu.gui.jupyter import JupyterWgpuCanvas -except ImportError: - JupyterWgpuCanvas = False - -try: - import PyQt6 - from wgpu.gui.qt import QWgpuCanvas -except ImportError: - QWgpuCanvas = False - -try: - from wgpu.gui.glfw import GlfwWgpuCanvas -except ImportError: - GlfwWgpuCanvas = False - - -CANVAS_OPTIONS = ["jupyter", "glfw", "qt"] -CANVAS_OPTIONS_AVAILABLE = { - "jupyter": JupyterWgpuCanvas, - "glfw": GlfwWgpuCanvas, - "qt": QWgpuCanvas, -} - - -def auto_determine_canvas(): - try: - ip = get_ipython() - if ip.has_trait("kernel"): - if hasattr(ip.kernel, "app"): - if ip.kernel.app.__class__.__name__ == "QApplication": - return QWgpuCanvas - else: - return JupyterWgpuCanvas - except NameError: - pass - - else: - if CANVAS_OPTIONS_AVAILABLE["qt"]: - return QWgpuCanvas - elif CANVAS_OPTIONS_AVAILABLE["glfw"]: - return GlfwWgpuCanvas - - # We go with the wgpu auto guess - # for example, offscreen canvas etc. - return WgpuCanvas +from ..utils import gui def make_canvas_and_renderer( - canvas: Union[str, WgpuCanvas, Texture, None], renderer: [WgpuRenderer, None] + canvas: Union[str, WgpuCanvasBase, Texture, None], renderer: [WgpuRenderer, None] ): """ Parses arguments and returns the appropriate canvas and renderer instances @@ -66,23 +17,14 @@ def make_canvas_and_renderer( """ if canvas is None: - Canvas = auto_determine_canvas() - canvas = Canvas(max_fps=60) - + canvas = gui.WgpuCanvas(max_fps=60) elif isinstance(canvas, str): - if canvas not in 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" - ) - else: - canvas = CANVAS_OPTIONS_AVAILABLE[canvas](max_fps=60) - + m = importlib.import_module("wgpu.gui." + canvas) + canvas = m.WgpuCanvas(max_fps=60) elif not isinstance(canvas, (WgpuCanvasBase, Texture)): raise ValueError( f"canvas option must either be a valid WgpuCanvas implementation, a pygfx Texture" - f" or a str from the following options: {CANVAS_OPTIONS}" + f" or a str with the wgpu gui backend name." ) if renderer is None: diff --git a/fastplotlib/utils/__init__.py b/fastplotlib/utils/__init__.py index 305af90a8..6759b2497 100644 --- a/fastplotlib/utils/__init__.py +++ b/fastplotlib/utils/__init__.py @@ -2,7 +2,6 @@ from .functions import * -from ._gpu_info import _notebook_print_banner @dataclass diff --git a/fastplotlib/utils/_gpu_info.py b/fastplotlib/utils/_gpu_info.py deleted file mode 100644 index 93e95d281..000000000 --- a/fastplotlib/utils/_gpu_info.py +++ /dev/null @@ -1,66 +0,0 @@ -from pathlib import Path - -from wgpu.backends.wgpu_native import enumerate_adapters -from wgpu.utils import get_default_device - -try: - ip = get_ipython() - from ipywidgets import Image - from wgpu.gui.jupyter import JupyterWgpuCanvas -except (NameError, ModuleNotFoundError, ImportError): - NOTEBOOK = False -else: - from IPython.display import display - - if ip.has_trait("kernel") and (JupyterWgpuCanvas is not False): - NOTEBOOK = True - else: - NOTEBOOK = False - - -def _notebook_print_banner(): - if NOTEBOOK is False: - return - - logo_path = Path(__file__).parent.parent.joinpath( - "assets", "fastplotlib_face_logo.png" - ) - - with open(logo_path, "rb") as f: - logo_data = f.read() - - image = Image(value=logo_data, format="png", width=300, height=55) - - display(image) - - # print logo and adapter info - adapters = [a for a in enumerate_adapters()] - adapters_info = [a.request_adapter_info() for a in adapters] - - ix_default = adapters_info.index( - get_default_device().adapter.request_adapter_info() - ) - - if len(adapters) > 0: - print("Available devices:") - - for ix, adapter in enumerate(adapters_info): - atype = adapter["adapter_type"] - backend = adapter["backend_type"] - driver = adapter["description"] - device = adapter["device"] - - if atype == "DiscreteGPU" and backend != "OpenGL": - charactor = chr(0x2705) - elif atype == "IntegratedGPU" and backend != "OpenGL": - charactor = chr(0x0001FBC4) - else: - charactor = chr(0x2757) - - if ix == ix_default: - default = " (default) " - else: - default = " " - - output_str = f"{charactor}{default}| {device} | {atype} | {backend} | {driver}" - print(output_str) diff --git a/fastplotlib/utils/gui.py b/fastplotlib/utils/gui.py new file mode 100644 index 000000000..b59c7799b --- /dev/null +++ b/fastplotlib/utils/gui.py @@ -0,0 +1,108 @@ +import sys +import importlib +from pathlib import Path + +import wgpu + + +# --- Prepare + + +# Ultimately, we let wgpu-py decide, but we can prime things a bit to create our +# own preferred order, by importing a Qt lib. But we only do this if no GUI has +# been imported yet. + +# Qt libs that we will try to import +qt_libs = ["PySide6", "PyQt6", "PySide2", "PyQt5"] + +# Other known libs that, if imported, we should probably not try to force qt +other_libs = ["glfw", "wx", "ipykernel"] + +already_imported = [name for name in (qt_libs + other_libs) if name in sys.modules] +if not already_imported: + for name in qt_libs: + try: + importlib.import_module(name) + except Exception: + pass + else: + break + + +# --- Triage + + +# Let wgpu do the auto gui selection +from wgpu.gui.auto import WgpuCanvas, run + +# Get the name of the backend ('qt', 'glfw', 'jupyter') +GUI_BACKEND = WgpuCanvas.__module__.split(".")[-1] +IS_JUPYTER = GUI_BACKEND == "jupyter" + + +# --- Some backend-specific preparations + + +def _notebook_print_banner(): + + from ipywidgets import Image + from IPython.display import display + + logo_path = Path(__file__).parent.parent.joinpath( + "assets", "fastplotlib_face_logo.png" + ) + + with open(logo_path, "rb") as f: + logo_data = f.read() + + image = Image(value=logo_data, format="png", width=300, height=55) + + display(image) + + # print logo and adapter info + adapters = [a for a in wgpu.gpu.enumerate_adapters()] + adapters_info = [a.request_adapter_info() for a in adapters] + + default_adapter_info = wgpu.gpu.request_adapter().request_adapter_info() + default_ix = adapters_info.index(default_adapter_info) + + if len(adapters) > 0: + print("Available devices:") + + for ix, adapter in enumerate(adapters_info): + atype = adapter["adapter_type"] + backend = adapter["backend_type"] + driver = adapter["description"] + device = adapter["device"] + + if atype == "DiscreteGPU" and backend != "OpenGL": + charactor = chr(0x2705) + elif atype == "IntegratedGPU" and backend != "OpenGL": + charactor = chr(0x0001FBC4) + else: + charactor = chr(0x2757) + + if ix == default_ix: + default = " (default) " + else: + default = " " + + output_str = f"{charactor}{default}| {device} | {atype} | {backend} | {driver}" + print(output_str) + + +if GUI_BACKEND == "jupyter": + _notebook_print_banner() + +elif GUI_BACKEND == "qt": + from wgpu.gui.qt import get_app, libname + + # create and store ref to qt app + _qt_app = get_app() + + # Import submodules of PySide6/PyQt6/PySid2/PyQt5 + # For the way that fpl uses Qt, the supported Qt libs seems compatible enough. + # If necessary we can do some qtpy-like monkey-patching here. + QtCore = importlib.import_module(".QtCore", libname) + QtGui = importlib.import_module(".QtGui", libname) + QtWidgets = importlib.import_module(".QtWidgets", libname) diff --git a/fastplotlib/widgets/image.py b/fastplotlib/widgets/image.py index 0da1bb520..9412f7cc5 100644 --- a/fastplotlib/widgets/image.py +++ b/fastplotlib/widgets/image.py @@ -8,14 +8,6 @@ from ..graphics import ImageGraphic from ..utils import calculate_gridshape from .histogram_lut import HistogramLUT -from ..layouts._utils import CANVAS_OPTIONS_AVAILABLE - - -if CANVAS_OPTIONS_AVAILABLE["jupyter"]: - from ..layouts._frame._ipywidget_toolbar import IpywidgetImageWidgetToolbar - -if CANVAS_OPTIONS_AVAILABLE["qt"]: - from ..layouts._frame._qt_toolbar import QToolbarImageWidget DEFAULT_DIMS_ORDER = { @@ -927,9 +919,17 @@ def show( ImageWidget just uses the Gridplot output context """ if self.gridplot.canvas.__class__.__name__ == "JupyterWgpuCanvas": + from ..layouts._frame._ipywidget_toolbar import ( + IpywidgetImageWidgetToolbar, + ) # noqa - inline import + self._image_widget_toolbar = IpywidgetImageWidgetToolbar(self) elif self.gridplot.canvas.__class__.__name__ == "QWgpuCanvas": + from ..layouts._frame._qt_toolbar import ( + QToolbarImageWidget, + ) # noqa - inline import + self._image_widget_toolbar = QToolbarImageWidget(self) self._output = self.gridplot.show( diff --git a/setup.py b/setup.py index a06a879a5..e8f2613d9 100644 --- a/setup.py +++ b/setup.py @@ -4,6 +4,7 @@ install_requires = [ "numpy>=1.23.0", + "wgpu>=0.15.1", "pygfx>=0.1.14", ] From 9a1bfbee6ceda2e8b0a7cdc9210ef1450201ba0f Mon Sep 17 00:00:00 2001 From: Kushal Kolar Date: Sun, 31 Mar 2024 00:52:54 -0400 Subject: [PATCH 37/66] simplify screenshot testing (#447) * rgb and rmse funcs work * increase screenshot rmse threshold to 0.025 * drop py3.9, add mac actions * workflow name * skip mac CI for now * just use numpy to convert RGBA -> RGB * rgb for nb tests * forgot to remove rgba screenshot saving for nb * forgot to add rmse to nb test utils * more tweaks * allow setting rmse tolerance for nb tests * use slightly higher tolerance for iw nb tests * proper way to set tolerance for nb --- .github/workflows/ci.yml | 92 ++++++++- .github/workflows/screenshots.yml | 2 +- examples/desktop/screenshots/gridplot.png | 4 +- .../screenshots/gridplot_non_square.png | 4 +- examples/desktop/screenshots/heatmap.png | 4 +- examples/desktop/screenshots/heatmap_cmap.png | 4 +- examples/desktop/screenshots/heatmap_data.png | 4 +- .../desktop/screenshots/heatmap_vmin_vmax.png | 4 +- examples/desktop/screenshots/image_cmap.png | 4 +- examples/desktop/screenshots/image_rgb.png | 4 +- .../desktop/screenshots/image_rgbvminvmax.png | 4 +- examples/desktop/screenshots/image_simple.png | 4 +- .../desktop/screenshots/image_vminvmax.png | 4 +- examples/desktop/screenshots/line.png | 4 +- examples/desktop/screenshots/line_cmap.png | 4 +- .../desktop/screenshots/line_collection.png | 4 +- .../line_collection_cmap_values.png | 4 +- ...ine_collection_cmap_values_qualitative.png | 4 +- .../screenshots/line_collection_colors.png | 4 +- .../desktop/screenshots/line_colorslice.png | 4 +- .../desktop/screenshots/line_dataslice.png | 4 +- .../screenshots/line_present_scaling.png | 4 +- examples/desktop/screenshots/line_stack.png | 4 +- examples/desktop/screenshots/scatter.png | 4 +- examples/desktop/screenshots/scatter_cmap.png | 4 +- .../screenshots/scatter_colorslice.png | 4 +- .../desktop/screenshots/scatter_dataslice.png | 4 +- .../desktop/screenshots/scatter_present.png | 4 +- examples/desktop/screenshots/scatter_size.png | 4 +- examples/notebooks/image_widget_test.ipynb | 25 ++- examples/notebooks/nb_test_utils.py | 86 +++++++- .../notebooks/screenshots/nb-astronaut.png | 4 +- .../screenshots/nb-astronaut_RGB.png | 4 +- examples/notebooks/screenshots/nb-camera.png | 4 +- .../nb-image-widget-movie-set_data.png | 4 +- .../nb-image-widget-movie-single-0-reset.png | 4 +- .../nb-image-widget-movie-single-0.png | 4 +- .../nb-image-widget-movie-single-279.png | 4 +- ...e-widget-movie-single-50-window-max-33.png | 4 +- ...-widget-movie-single-50-window-mean-13.png | 4 +- ...-widget-movie-single-50-window-mean-33.png | 4 +- ...ge-widget-movie-single-50-window-reset.png | 4 +- .../nb-image-widget-movie-single-50.png | 4 +- .../nb-image-widget-single-gnuplot2.png | 4 +- .../screenshots/nb-image-widget-single.png | 4 +- ...et-zfish-frame-50-frame-apply-gaussian.png | 4 +- ...idget-zfish-frame-50-frame-apply-reset.png | 4 +- ...ge-widget-zfish-frame-50-max-window-13.png | 4 +- ...e-widget-zfish-frame-50-mean-window-13.png | 4 +- ...ge-widget-zfish-frame-50-mean-window-5.png | 4 +- .../nb-image-widget-zfish-frame-50.png | 4 +- .../nb-image-widget-zfish-frame-99.png | 4 +- ...ish-grid-frame-50-frame-apply-gaussian.png | 4 +- ...-zfish-grid-frame-50-frame-apply-reset.png | 4 +- ...dget-zfish-grid-frame-50-max-window-13.png | 4 +- ...get-zfish-grid-frame-50-mean-window-13.png | 4 +- ...dget-zfish-grid-frame-50-mean-window-5.png | 4 +- .../nb-image-widget-zfish-grid-frame-50.png | 4 +- .../nb-image-widget-zfish-grid-frame-99.png | 4 +- ...e-widget-zfish-grid-init-mean-window-5.png | 4 +- ...fish-grid-set_data-reset-indices-false.png | 4 +- ...zfish-grid-set_data-reset-indices-true.png | 4 +- ...-image-widget-zfish-init-mean-window-5.png | 4 +- .../notebooks/screenshots/nb-imagewidget.png | 3 + .../notebooks/screenshots/nb-lines-3d.png | 4 +- .../nb-lines-cmap-jet-values-cosine.png | 4 +- .../screenshots/nb-lines-cmap-jet-values.png | 4 +- .../screenshots/nb-lines-cmap-jet.png | 4 +- .../screenshots/nb-lines-cmap-tab-10.png | 4 +- .../nb-lines-cmap-viridis-values.png | 4 +- .../screenshots/nb-lines-cmap-viridis.png | 4 +- .../screenshots/nb-lines-cmap-white.png | 4 +- .../notebooks/screenshots/nb-lines-colors.png | 4 +- .../notebooks/screenshots/nb-lines-data.png | 4 +- .../screenshots/nb-lines-underlay.png | 4 +- examples/notebooks/screenshots/nb-lines.png | 4 +- examples/tests/test_examples.py | 47 ++++- examples/tests/testutils.py | 188 ++++++++++++++++++ 78 files changed, 552 insertions(+), 175 deletions(-) create mode 100644 examples/notebooks/screenshots/nb-imagewidget.png diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8f92bafb8..53e88bdf8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -46,15 +46,13 @@ jobs: make html SPHINXOPTS="-W --keep-going" test-build-full: - name: Test examples, env with notebook and glfw + name: Test Linux, notebook + glfw runs-on: bigmem if: ${{ !github.event.pull_request.draft }} strategy: fail-fast: false matrix: include: - - name: Test py39 - pyversion: '3.9' - name: Test py310 pyversion: '3.10' - name: Test py311 @@ -103,15 +101,13 @@ jobs: examples/notebooks/diffs test-build-desktop: - name: Test examples, env with only glfw + name: Test Linux, only glfw runs-on: bigmem if: ${{ !github.event.pull_request.draft }} strategy: fail-fast: false matrix: include: - - name: Test py39 - pyversion: '3.9' - name: Test py310 pyversion: '3.10' - name: Test py311 @@ -156,3 +152,87 @@ jobs: name: screenshot-diffs path: | examples/desktop/diffs + +# test-build-full-mac: +# name: Test Mac, notebook + glfw +# runs-on: macos-14 +# if: ${{ !github.event.pull_request.draft }} +# strategy: +# fail-fast: false +# matrix: +# include: +# - name: Test py310 +# pyversion: '3.10' +# - name: Test py311 +# pyversion: '3.11' +# - name: Test py312 +# pyversion: '3.12' +# steps: +# - uses: actions/checkout@v3 +# with: +# lfs: true +# - name: Set up Python +# uses: actions/setup-python@v3 +# with: +# python-version: ${{ matrix.pyversion }} +# - name: Install dev dependencies +# run: | +# python -m pip install --upgrade pip setuptools +# # remove pygfx from install_requires, we install using pygfx@main +# pip install -e ".["tests"]" +# pip install git+https://github.com/pygfx/pygfx.git@main +# - name: Show wgpu backend +# run: +# python -c "from examples.tests.testutils import wgpu_backend; print(wgpu_backend)" +# - name: Test examples +# run: | +# pytest -v examples +# pytest --nbmake examples/notebooks/ +# - uses: actions/upload-artifact@v3 +# if: ${{ failure() }} +# with: +# name: screenshot-diffs +# path: | +# examples/desktop/diffs +# examples/notebooks/diffs +# +# test-build-glfw-mac: +# name: Test Mac, glfw +# runs-on: macos-14 +# if: ${{ !github.event.pull_request.draft }} +# strategy: +# fail-fast: false +# matrix: +# include: +# - name: Test py310 +# pyversion: '3.10' +# - name: Test py311 +# pyversion: '3.11' +# - name: Test py312 +# pyversion: '3.12' +# steps: +# - uses: actions/checkout@v3 +# with: +# lfs: true +# - name: Set up Python +# uses: actions/setup-python@v3 +# with: +# python-version: ${{ matrix.pyversion }} +# - name: Install dev dependencies +# run: | +# python -m pip install --upgrade pip setuptools +# # remove pygfx from install_requires, we install using pygfx@main +# pip install -e ".["tests-desktop"]" +# pip install git+https://github.com/pygfx/pygfx.git@main +# - name: Show wgpu backend +# run: +# python -c "from examples.tests.testutils import wgpu_backend; print(wgpu_backend)" +# - name: Test examples +# run: | +# pytest -v examples +# - uses: actions/upload-artifact@v3 +# if: ${{ failure() }} +# with: +# name: screenshot-diffs +# path: | +# examples/desktop/diffs diff --git a/.github/workflows/screenshots.yml b/.github/workflows/screenshots.yml index d3cdb919b..baad8b655 100644 --- a/.github/workflows/screenshots.yml +++ b/.github/workflows/screenshots.yml @@ -48,7 +48,7 @@ jobs: run: | # regenerate screenshots REGENERATE_SCREENSHOTS=1 pytest -v examples - REGENERATE_SCREENSHOTS=1 pytest --nbmake examples/notebooks/ + FASTPLOTLIB_NB_TESTS=1 REGENERATE_SCREENSHOTS=1 pytest --nbmake examples/notebooks/ - uses: actions/upload-artifact@v3 if: always() with: diff --git a/examples/desktop/screenshots/gridplot.png b/examples/desktop/screenshots/gridplot.png index bc35ccf8c..ebf2d3a97 100644 --- a/examples/desktop/screenshots/gridplot.png +++ b/examples/desktop/screenshots/gridplot.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e416fc968edd3788513e369f4d265b0abd7216a7ef19ec8b84659c30ca7c8ca1 -size 307384 +oid sha256:f972f67b8830657ab14899f749fb385a080280304377d8868e6cd39c766a0afd +size 267084 diff --git a/examples/desktop/screenshots/gridplot_non_square.png b/examples/desktop/screenshots/gridplot_non_square.png index 82b2b0eb4..bc642b729 100644 --- a/examples/desktop/screenshots/gridplot_non_square.png +++ b/examples/desktop/screenshots/gridplot_non_square.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ab5598b67b80efce0d2559e400e77098e734c91608a3f49b691ddaa030d47edb -size 203434 +oid sha256:352bf94c68444a330b000d7b6b3ec51b5b694ff3a0ce810299b325315923d9af +size 175938 diff --git a/examples/desktop/screenshots/heatmap.png b/examples/desktop/screenshots/heatmap.png index a0655cf3a..a8c8b73fe 100644 --- a/examples/desktop/screenshots/heatmap.png +++ b/examples/desktop/screenshots/heatmap.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6872c3cc3e35ec918b054fb2d76525bbd3d82d8b49916aca1046aa1be65ff923 -size 111825 +oid sha256:5620e4dcb964dbf3318ac77e566af395a35b9762e0687dec2e1a2864eb291fd3 +size 102994 diff --git a/examples/desktop/screenshots/heatmap_cmap.png b/examples/desktop/screenshots/heatmap_cmap.png index 2eb769c14..cee81dd30 100644 --- a/examples/desktop/screenshots/heatmap_cmap.png +++ b/examples/desktop/screenshots/heatmap_cmap.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f2eba96c2bfb1d07365810a69e99c79b068741f5dcf74fc745c13d5ff21f16f2 -size 106671 +oid sha256:8863461569f5b89d1443e3051a5512f3987487fcb9e057215d2f030a180fa09f +size 97996 diff --git a/examples/desktop/screenshots/heatmap_data.png b/examples/desktop/screenshots/heatmap_data.png index 50a8ae79e..316a73753 100644 --- a/examples/desktop/screenshots/heatmap_data.png +++ b/examples/desktop/screenshots/heatmap_data.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f0576063658e05e19b7723b4c88dc4d55a8178b090b4a88e33251fc92408b4a1 -size 18051 +oid sha256:a975179e82893dbb04e4674310761e7b02bb62ae6abb1b89397720bddf96ae5f +size 19084 diff --git a/examples/desktop/screenshots/heatmap_vmin_vmax.png b/examples/desktop/screenshots/heatmap_vmin_vmax.png index f10382e87..357683d82 100644 --- a/examples/desktop/screenshots/heatmap_vmin_vmax.png +++ b/examples/desktop/screenshots/heatmap_vmin_vmax.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:639d50f2f5fb07dba461e7a38de3886092f8754277eadbb5e305e32023289abd -size 124403 +oid sha256:9592f3724016db1b7431bc100b16bec175e197c111e7b442dc2255d51da3f5e8 +size 114957 diff --git a/examples/desktop/screenshots/image_cmap.png b/examples/desktop/screenshots/image_cmap.png index bed07a41a..bbf51ab18 100644 --- a/examples/desktop/screenshots/image_cmap.png +++ b/examples/desktop/screenshots/image_cmap.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e1d78cc0681079a5c43d9fdb4142f5fee75d477d9f9a1469fca8bc8933c244fc -size 216210 +oid sha256:555fd969606d0cb231ac152724f7c9717a2220ce22db663c5e7d5793f828ed34 +size 189654 diff --git a/examples/desktop/screenshots/image_rgb.png b/examples/desktop/screenshots/image_rgb.png index a21c0658b..9a5082b12 100644 --- a/examples/desktop/screenshots/image_rgb.png +++ b/examples/desktop/screenshots/image_rgb.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:820a73b9b2e5bbaed84fb11438e2c5672b76c8b57a33823f4840a71be03d7dd1 -size 251438 +oid sha256:95f3cae6caf8d64d1a6b4799df52dc61cc05bd6b6ea465edbec06a9678f32435 +size 218089 diff --git a/examples/desktop/screenshots/image_rgbvminvmax.png b/examples/desktop/screenshots/image_rgbvminvmax.png index 88acfadc5..00bbdc0c5 100644 --- a/examples/desktop/screenshots/image_rgbvminvmax.png +++ b/examples/desktop/screenshots/image_rgbvminvmax.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5f2f562573f8104342ae01b5852f71c960341bdd70ae0bc7967df663166edbd3 -size 39604 +oid sha256:4fc06b8cdd72040cf2ffc44cde80d5ae21ca392daac25d79fe175b5865b13552 +size 34894 diff --git a/examples/desktop/screenshots/image_simple.png b/examples/desktop/screenshots/image_simple.png index 098d5a055..94fcd3061 100644 --- a/examples/desktop/screenshots/image_simple.png +++ b/examples/desktop/screenshots/image_simple.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a4ed42d042d8bb7e35f31b5ad0a3e3a495cf9c3164516eb457d8b41d7fae6bab -size 213075 +oid sha256:3dcfb5d48d0e4db920c33ee725e2c66f3c8e04a66e03d283a6481f42a4121a16 +size 190178 diff --git a/examples/desktop/screenshots/image_vminvmax.png b/examples/desktop/screenshots/image_vminvmax.png index 88acfadc5..00bbdc0c5 100644 --- a/examples/desktop/screenshots/image_vminvmax.png +++ b/examples/desktop/screenshots/image_vminvmax.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5f2f562573f8104342ae01b5852f71c960341bdd70ae0bc7967df663166edbd3 -size 39604 +oid sha256:4fc06b8cdd72040cf2ffc44cde80d5ae21ca392daac25d79fe175b5865b13552 +size 34894 diff --git a/examples/desktop/screenshots/line.png b/examples/desktop/screenshots/line.png index 8e3e6ae64..74cbae39a 100644 --- a/examples/desktop/screenshots/line.png +++ b/examples/desktop/screenshots/line.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:81038ebca5d41f22f5dde3fd152d94760ef51cc055ff248be18298bc7537b569 -size 44312 +oid sha256:c8b4f4a08d1791b80d226c8c3099e37d33d8cdd7a400e4f85fb7072ee2aa3c2e +size 29121 diff --git a/examples/desktop/screenshots/line_cmap.png b/examples/desktop/screenshots/line_cmap.png index b96c9a1dd..9cd93f05d 100644 --- a/examples/desktop/screenshots/line_cmap.png +++ b/examples/desktop/screenshots/line_cmap.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e7a9cf65bbd19290ed96e418930e896fd0ec463dc2c6797f8b407d56e9e4444d -size 43730 +oid sha256:d60b4ff117298f973be892773dbfc620ac855c35ca7dea42437e20bf7fcef804 +size 31050 diff --git a/examples/desktop/screenshots/line_collection.png b/examples/desktop/screenshots/line_collection.png index 89d613c2c..bcfe85309 100644 --- a/examples/desktop/screenshots/line_collection.png +++ b/examples/desktop/screenshots/line_collection.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:09e8c2be9815edf5c29a98dc61758fac3aeb2603c8547aa8c3c05b01538886e3 -size 147244 +oid sha256:7ca99b8d74fdf7f87b0f2fc5637c59c9090b91bef868e85ddd75dbcb1264f699 +size 95146 diff --git a/examples/desktop/screenshots/line_collection_cmap_values.png b/examples/desktop/screenshots/line_collection_cmap_values.png index 38d9adc6e..b7fcdbcae 100644 --- a/examples/desktop/screenshots/line_collection_cmap_values.png +++ b/examples/desktop/screenshots/line_collection_cmap_values.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:98d7468dc1701d3523c779a0e857cf185201766b751731e1db44e3c2bc753335 -size 93839 +oid sha256:68090603856eb5b961092cf2ad2d89a1e9cfd7e31f6d089b3abad101874f65d4 +size 61032 diff --git a/examples/desktop/screenshots/line_collection_cmap_values_qualitative.png b/examples/desktop/screenshots/line_collection_cmap_values_qualitative.png index 4f14e49b9..9f89a24cc 100644 --- a/examples/desktop/screenshots/line_collection_cmap_values_qualitative.png +++ b/examples/desktop/screenshots/line_collection_cmap_values_qualitative.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:31d79a2aeb2e43b569a9045edd21fb59e7bddf2b5cc18133f4b346e0d6be7fd1 -size 95696 +oid sha256:9cff99e5f9faf319909571778631453c043237f5c94eece6680b028a5d7a5ac2 +size 64149 diff --git a/examples/desktop/screenshots/line_collection_colors.png b/examples/desktop/screenshots/line_collection_colors.png index c71c623c6..7bb4152fd 100644 --- a/examples/desktop/screenshots/line_collection_colors.png +++ b/examples/desktop/screenshots/line_collection_colors.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:381fc320bda54f5aa235b4cd0183ff511e3a0484c00ab37f76c89df959d48626 -size 82806 +oid sha256:a4aa17a65806300da65f4bbbfccb970d6a7207c4ca4d48b25615f627630fb484 +size 51174 diff --git a/examples/desktop/screenshots/line_colorslice.png b/examples/desktop/screenshots/line_colorslice.png index 7775c1918..3d04c473f 100644 --- a/examples/desktop/screenshots/line_colorslice.png +++ b/examples/desktop/screenshots/line_colorslice.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3ade0416355a05de01e8f4a55485aacc40bd4c3d57a1a9fcff43317e13a43856 -size 50560 +oid sha256:aa941eaf5b940b4eebab89ed836cbd092e16b4758abafa3722c296db65c0c4b5 +size 33233 diff --git a/examples/desktop/screenshots/line_dataslice.png b/examples/desktop/screenshots/line_dataslice.png index 2907dd64a..0863751bf 100644 --- a/examples/desktop/screenshots/line_dataslice.png +++ b/examples/desktop/screenshots/line_dataslice.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8fc4265c0ac6b4cee0476acbc5362968f60a965a387c7a0a3b66e89e522eb21c -size 69917 +oid sha256:78ccd51d1891fb6a345cb2885a341f276d8ad7a6fa506deda6cae6ef14c64094 +size 45843 diff --git a/examples/desktop/screenshots/line_present_scaling.png b/examples/desktop/screenshots/line_present_scaling.png index b4b883855..ba7142106 100644 --- a/examples/desktop/screenshots/line_present_scaling.png +++ b/examples/desktop/screenshots/line_present_scaling.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f37f2a227136af0cfc112bb1e5c9ba01fb362f33bad0971f5253adb61e89785e -size 30264 +oid sha256:06f7dd45eb495fecfcf46478c6430a658640ceb2855c4797bc184cf4134571e3 +size 20180 diff --git a/examples/desktop/screenshots/line_stack.png b/examples/desktop/screenshots/line_stack.png index 47e59ba8c..c13f05f04 100644 --- a/examples/desktop/screenshots/line_stack.png +++ b/examples/desktop/screenshots/line_stack.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0081d587956056934c4feb0c0f695f69ea19a253243f1af1dc7de80c6406a642 -size 365226 +oid sha256:5480aefe6e723863b919a4eeb4755310fe7036b27beb8e2e2402e04943ee8c1e +size 201102 diff --git a/examples/desktop/screenshots/scatter.png b/examples/desktop/screenshots/scatter.png index d01d36707..94fb858e1 100644 --- a/examples/desktop/screenshots/scatter.png +++ b/examples/desktop/screenshots/scatter.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8d840f02d1c4be5ea11adfc224481a5b8b306cbc904e099af4d3fdd5ab7f383f -size 26683 +oid sha256:4fc16a1ba74a8eca99a2fc7937f8896ca93207b99e231bc4f53845b0d2bdaed7 +size 15283 diff --git a/examples/desktop/screenshots/scatter_cmap.png b/examples/desktop/screenshots/scatter_cmap.png index 7f0bba38a..87a6e0ded 100644 --- a/examples/desktop/screenshots/scatter_cmap.png +++ b/examples/desktop/screenshots/scatter_cmap.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e9aba2f37c7682d68569e1bac7afac5f243afb98ab94d9957de4b59f9d3dd1c0 -size 57257 +oid sha256:a02d2b5d4735d656d1b754ac3681a7700d961d7e4a43dfaf3a7dd0d4f6516ba6 +size 37808 diff --git a/examples/desktop/screenshots/scatter_colorslice.png b/examples/desktop/screenshots/scatter_colorslice.png index 27249e63e..cede76dfd 100644 --- a/examples/desktop/screenshots/scatter_colorslice.png +++ b/examples/desktop/screenshots/scatter_colorslice.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:39c49529552d6ace3d67b37f0c660e9734fcb763bdc165484f356ad8cffc908e -size 25218 +oid sha256:f7956e02d6c231bab091adb4ce9102ad4943050ccf171a0594a899a381880771 +size 14712 diff --git a/examples/desktop/screenshots/scatter_dataslice.png b/examples/desktop/screenshots/scatter_dataslice.png index 155510885..7a1429663 100644 --- a/examples/desktop/screenshots/scatter_dataslice.png +++ b/examples/desktop/screenshots/scatter_dataslice.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:20757e215c4c208e08027f8e2b798691f421ce7662dc86be3615dc41084686f3 -size 27392 +oid sha256:0ecc6454dd197e6a3f146d0a04881db91b099673b9d74903536ca103b2418c89 +size 15657 diff --git a/examples/desktop/screenshots/scatter_present.png b/examples/desktop/screenshots/scatter_present.png index 87685fe90..08bc610b3 100644 --- a/examples/desktop/screenshots/scatter_present.png +++ b/examples/desktop/screenshots/scatter_present.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7a8ceb8f3d7203f0569374993784f2229448ace08b0abee94910e6ae71ceca29 -size 24587 +oid sha256:bd072918f21ed0ce4ea4e1f4499ec1ff66d867cfdc0ecd6b3ed8092141cd348e +size 14195 diff --git a/examples/desktop/screenshots/scatter_size.png b/examples/desktop/screenshots/scatter_size.png index afe8a0b35..056d2a531 100644 --- a/examples/desktop/screenshots/scatter_size.png +++ b/examples/desktop/screenshots/scatter_size.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a184ce0a202bc03fa61b3f4149109e44160bfd326c26f46db3b83ce0cf1699a6 -size 67710 +oid sha256:5ccfbac94de6ba122ea420dce58e4a576b2d58d9282aaf8d64de399278df57b3 +size 38076 diff --git a/examples/notebooks/image_widget_test.ipynb b/examples/notebooks/image_widget_test.ipynb index 90747757c..c236ce9b7 100644 --- a/examples/notebooks/image_widget_test.ipynb +++ b/examples/notebooks/image_widget_test.ipynb @@ -23,7 +23,9 @@ "metadata": {}, "outputs": [], "source": [ - "from nb_test_utils import plot_test, notebook_finished " + "from nb_test_utils import plot_test, notebook_finished\n", + "import nb_test_utils\n", + "nb_test_utils.TOLERANCE = 0.035" ] }, { @@ -57,7 +59,8 @@ "source": [ "iw = ImageWidget(\n", " data=a,\n", - " cmap=\"viridis\"\n", + " cmap=\"viridis\",\n", + " grid_plot_kwargs={\"size\": (900, 600)},\n", ")" ] }, @@ -128,7 +131,8 @@ "iw_movie = ImageWidget(\n", " data=gray_movie, \n", " slider_dims=[\"t\"],\n", - " cmap=\"gray\"\n", + " cmap=\"gray\",\n", + " grid_plot_kwargs={\"size\": (900, 600)},\n", ")" ] }, @@ -271,6 +275,9 @@ "execution_count": null, "id": "76535d56-e514-4c16-aa48-a6359f8019d5", "metadata": { + "jupyter": { + "source_hidden": true + }, "tags": [] }, "outputs": [], @@ -280,6 +287,7 @@ " window_funcs={\"t\": (np.mean, 5)},\n", " names=[f\"plane-{i}\" for i in range(n_planes)],\n", " cmap=\"gnuplot2\", \n", + " grid_plot_kwargs={\"size\": (900, 600)},\n", ")" ] }, @@ -379,6 +387,7 @@ " data=zfish_data, # you can also provide a list of tzxy arrays\n", " window_funcs={\"t\": (np.mean, 5)},\n", " cmap=\"gnuplot2\", \n", + " grid_plot_kwargs={\"size\": (900, 600)},\n", ")" ] }, @@ -444,6 +453,14 @@ "source": [ "notebook_finished()" ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b8fff1a6-119e-4f03-ba3a-4c7b9e8c212b", + "metadata": {}, + "outputs": [], + "source": [] } ], "metadata": { @@ -462,7 +479,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.11.2" + "version": "3.11.3" } }, "nbformat": 4, diff --git a/examples/notebooks/nb_test_utils.py b/examples/notebooks/nb_test_utils.py index 71d2d5114..90b7158ad 100644 --- a/examples/notebooks/nb_test_utils.py +++ b/examples/notebooks/nb_test_utils.py @@ -16,11 +16,73 @@ os.makedirs(SCREENSHOTS_DIR, exist_ok=True) os.makedirs(DIFFS_DIR, exist_ok=True) +TOLERANCE = 0.025 # store all the failures to allow the nb to proceed to test other examples FAILURES = list() +# TODO: consolidate testing functions into one module so we don't have this separate one for notebooks + +def rgba_to_rgb(img: np.ndarray) -> np.ndarray: + black = np.zeros(img.shape).astype(np.uint8) + black[:, :, -1] = 255 + + img_alpha = img[..., -1] / 255 + + rgb = img[..., :-1] * img_alpha[..., None] + black[..., :-1] * np.ones( + img_alpha.shape + )[..., None] * (1 - img_alpha[..., None]) + + return rgb.round().astype(np.uint8) + + +# image comparison functions from: https://github.com/pygfx/image-comparison +def image_similarity(src, target, threshold=0.2): + """Compute normalized RMSE 0..1 and decide if similar based on threshold. + + For every pixel, the euclidian distance between RGB values is computed, + and normalized by the maximum possible distance (between black and white). + The RMSE is then computed from those errors. + + The normalized RMSE is used to compute the + similarity metric, so larger errors (euclidian distance + between two RGB colors) will have a disproportionately + larger effect on the score than smaller errors. + + In other words, lots of small errors will lead to a good score + (closer to 0) whereas a few large errors will lead to a bad score + (closer to 1). + """ + float_type = np.float64 + src = np.asarray(src, dtype=float_type) + target = np.asarray(target, dtype=float_type) + denom = np.sqrt(np.mean(src * src)) + mse = np.mean((src - target) ** 2) + rmse = np.sqrt(mse) / denom + + similar = bool(rmse < threshold) + return similar, rmse + + +def normalize_image(img): + """Discard the alpha channel and convert from 0..255 uint8 to 0..1 float.""" + assert len(img.shape) == 3 + + # normalize to 0..1 range + if img.dtype == "u1" or np.max(img) > 1: + img = img / 255 + assert np.min(img) >= 0 and np.max(img) <= 1 + + # discard alpha channel + # unsupported if it's not fully opaque + if img.shape[-1] == 4: + assert np.max(img[..., 3]) == 1 + img = img[..., :-1] + + return img + + def _run_tests(): if "FASTPLOTLIB_NB_TESTS" not in os.environ.keys(): return False @@ -34,16 +96,15 @@ def _run_tests(): def plot_test(name, plot: Union[Plot, GridPlot]): if not _run_tests(): return + snapshot = plot.canvas.snapshot() + rgb_img = rgba_to_rgb(snapshot.data) if "REGENERATE_SCREENSHOTS" in os.environ.keys(): if os.environ["REGENERATE_SCREENSHOTS"] == "1": - regenerate_screenshot(name, snapshot.data) + regenerate_screenshot(name, rgb_img) - try: - assert_screenshot_equal(name, snapshot.data) - except AssertionError: - FAILURES.append(name) + assert_screenshot_equal(name, rgb_img) def regenerate_screenshot(name, data): @@ -53,13 +114,17 @@ def regenerate_screenshot(name, 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) + img = normalize_image(data) + ref_img = normalize_image(ground_truth) - update_diffs(name, is_similar, data, ground_truth) + similar, rmse = image_similarity(img, ref_img, threshold=TOLERANCE) - assert is_similar, ( - f"notebook snapshot for {name} has changed" - ) + update_diffs(name, similar, data, ground_truth) + + if not similar: + FAILURES.append( + (name, rmse) + ) def update_diffs(name, is_similar, img, ground_truth): @@ -81,7 +146,6 @@ def get_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(): diff --git a/examples/notebooks/screenshots/nb-astronaut.png b/examples/notebooks/screenshots/nb-astronaut.png index 2faf79def..378260288 100644 --- a/examples/notebooks/screenshots/nb-astronaut.png +++ b/examples/notebooks/screenshots/nb-astronaut.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4845a61b99f7a489ac82a8688cc3350ce66e4771bb1399354591fd39688a58a2 -size 127977 +oid sha256:e584533ea04b9758634ba62dceeb72991861c509d01dc082436c54c272686409 +size 112104 diff --git a/examples/notebooks/screenshots/nb-astronaut_RGB.png b/examples/notebooks/screenshots/nb-astronaut_RGB.png index 22b2627cb..bf11bf667 100644 --- a/examples/notebooks/screenshots/nb-astronaut_RGB.png +++ b/examples/notebooks/screenshots/nb-astronaut_RGB.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2c34bd21fd7bf98bab25431019e8fee30b0f4912b6b4495ad963fb9e107b1f21 -size 125479 +oid sha256:db9602a610f258803d74ac03cd46447dd5a7ad62241ec26a4c3df30c1d6de299 +size 110408 diff --git a/examples/notebooks/screenshots/nb-camera.png b/examples/notebooks/screenshots/nb-camera.png index 32e83f3ba..9db4005bc 100644 --- a/examples/notebooks/screenshots/nb-camera.png +++ b/examples/notebooks/screenshots/nb-camera.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8ce695e954332a9b9122d418645d785900506fc30a897844bdf7fdce0bffb316 -size 89342 +oid sha256:4bb9080b99c2717e093bf6ae4986bf0689a8d377e137a7022c9c6929b9a335d3 +size 77965 diff --git a/examples/notebooks/screenshots/nb-image-widget-movie-set_data.png b/examples/notebooks/screenshots/nb-image-widget-movie-set_data.png index a728df223..5be8f55a3 100644 --- a/examples/notebooks/screenshots/nb-image-widget-movie-set_data.png +++ b/examples/notebooks/screenshots/nb-image-widget-movie-set_data.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:794d4ba4e31884a95c5a98596d142aa2d2af75647c5ad73dc573ff18493c1a07 -size 31256 +oid sha256:66a310e312add59a310ff0a50335db97ac557d7f2967d8251a7d811c25a4de28 +size 40517 diff --git a/examples/notebooks/screenshots/nb-image-widget-movie-single-0-reset.png b/examples/notebooks/screenshots/nb-image-widget-movie-single-0-reset.png index 8e624128e..8572f6472 100644 --- a/examples/notebooks/screenshots/nb-image-widget-movie-single-0-reset.png +++ b/examples/notebooks/screenshots/nb-image-widget-movie-single-0-reset.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:edd99517550a035ee55fb81301fb1c3e08e3e90bc4345e781449d83fa307a8a9 -size 62630 +oid sha256:ff83d6bab26b9bbccf66ed100764ffdfc7556f4cb04f0b85f50c2497ba0ab257 +size 134419 diff --git a/examples/notebooks/screenshots/nb-image-widget-movie-single-0.png b/examples/notebooks/screenshots/nb-image-widget-movie-single-0.png index 8e624128e..8572f6472 100644 --- a/examples/notebooks/screenshots/nb-image-widget-movie-single-0.png +++ b/examples/notebooks/screenshots/nb-image-widget-movie-single-0.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:edd99517550a035ee55fb81301fb1c3e08e3e90bc4345e781449d83fa307a8a9 -size 62630 +oid sha256:ff83d6bab26b9bbccf66ed100764ffdfc7556f4cb04f0b85f50c2497ba0ab257 +size 134419 diff --git a/examples/notebooks/screenshots/nb-image-widget-movie-single-279.png b/examples/notebooks/screenshots/nb-image-widget-movie-single-279.png index d27e49568..e241ce5a6 100644 --- a/examples/notebooks/screenshots/nb-image-widget-movie-single-279.png +++ b/examples/notebooks/screenshots/nb-image-widget-movie-single-279.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3d5f5889af4490cdc81a47fd8235493bdf0384a015edcb5177ffea274efb64b1 -size 72626 +oid sha256:337a22f11649b350f7f47d68d82be165633caeb7f8cef581e50f981d6ec0c52c +size 169615 diff --git a/examples/notebooks/screenshots/nb-image-widget-movie-single-50-window-max-33.png b/examples/notebooks/screenshots/nb-image-widget-movie-single-50-window-max-33.png index 12dce091c..b827fc536 100644 --- a/examples/notebooks/screenshots/nb-image-widget-movie-single-50-window-max-33.png +++ b/examples/notebooks/screenshots/nb-image-widget-movie-single-50-window-max-33.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ec662212f6299cd27c3e26bb6b9fa1a99b4dd2808934536640e2f54d8bf8f699 -size 63968 +oid sha256:22ff3ed815fcbe8bc95c321c806a4b42536e7014209cd43ac597a5ccefd8b9c6 +size 149261 diff --git a/examples/notebooks/screenshots/nb-image-widget-movie-single-50-window-mean-13.png b/examples/notebooks/screenshots/nb-image-widget-movie-single-50-window-mean-13.png index 44ccb8020..d37f44a3a 100644 --- a/examples/notebooks/screenshots/nb-image-widget-movie-single-50-window-mean-13.png +++ b/examples/notebooks/screenshots/nb-image-widget-movie-single-50-window-mean-13.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ffe73fa414fa1af8e2daa0bdb7fa75fd47d9d575b60c729a9426deb42225bb5a -size 54513 +oid sha256:11143f73a297d0b59c92db1b115ceac1bc1304135a9925302e616a4fd3669b25 +size 125012 diff --git a/examples/notebooks/screenshots/nb-image-widget-movie-single-50-window-mean-33.png b/examples/notebooks/screenshots/nb-image-widget-movie-single-50-window-mean-33.png index f0354cbb2..46d3fa9b3 100644 --- a/examples/notebooks/screenshots/nb-image-widget-movie-single-50-window-mean-33.png +++ b/examples/notebooks/screenshots/nb-image-widget-movie-single-50-window-mean-33.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:10d82b8d15a5d1099f8a909401d78cc6a87d22708a7cd4c32d96bb316d570da3 -size 50431 +oid sha256:7438018c3b55d423f57c42a9d03c1af6d5de168a2dfa5df9d535ef2ae1f1c8e9 +size 113981 diff --git a/examples/notebooks/screenshots/nb-image-widget-movie-single-50-window-reset.png b/examples/notebooks/screenshots/nb-image-widget-movie-single-50-window-reset.png index 25b65bd48..6146a7985 100644 --- a/examples/notebooks/screenshots/nb-image-widget-movie-single-50-window-reset.png +++ b/examples/notebooks/screenshots/nb-image-widget-movie-single-50-window-reset.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:79776dadbe6947e9f20c96e5dc2d6ee718100f24fc09579609e7b25614869b74 -size 65150 +oid sha256:9b57a1e6640de9471540fa4d86faadb706d6de8cc1de6a29fb65d709b91eef1e +size 146429 diff --git a/examples/notebooks/screenshots/nb-image-widget-movie-single-50.png b/examples/notebooks/screenshots/nb-image-widget-movie-single-50.png index 25b65bd48..6146a7985 100644 --- a/examples/notebooks/screenshots/nb-image-widget-movie-single-50.png +++ b/examples/notebooks/screenshots/nb-image-widget-movie-single-50.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:79776dadbe6947e9f20c96e5dc2d6ee718100f24fc09579609e7b25614869b74 -size 65150 +oid sha256:9b57a1e6640de9471540fa4d86faadb706d6de8cc1de6a29fb65d709b91eef1e +size 146429 diff --git a/examples/notebooks/screenshots/nb-image-widget-single-gnuplot2.png b/examples/notebooks/screenshots/nb-image-widget-single-gnuplot2.png index 75fa3ef5a..b8bf7adeb 100644 --- a/examples/notebooks/screenshots/nb-image-widget-single-gnuplot2.png +++ b/examples/notebooks/screenshots/nb-image-widget-single-gnuplot2.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7e292d6e43e7a98cb72ca077e62bbc9ad3acde4753098ea2248c66ee07a9fc46 -size 143675 +oid sha256:dbfa1e7aeb7f0a068a33f2f11023a06f834332f7b3d8e4cf97b51222536fd6cb +size 434782 diff --git a/examples/notebooks/screenshots/nb-image-widget-single.png b/examples/notebooks/screenshots/nb-image-widget-single.png index 1321d01bb..86119e247 100644 --- a/examples/notebooks/screenshots/nb-image-widget-single.png +++ b/examples/notebooks/screenshots/nb-image-widget-single.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:50f17ab342efff0d1d3bc06f36d2b7b372eed4426e282ffa2b12e3e7dc913b68 -size 134516 +oid sha256:7ceee2cdd73092cb84b4b0f2876fc08d838b8a47bb94d431a6c19c8a4793a153 +size 403521 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50-frame-apply-gaussian.png b/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50-frame-apply-gaussian.png index 2543f9a5d..82cee281f 100644 --- a/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50-frame-apply-gaussian.png +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50-frame-apply-gaussian.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5834c92427c08b210a0971667018b4a63fd4e8d916bb02a582ba352cb0d5aad7 -size 64281 +oid sha256:47e3e6cea0e738b2731060488886606f05595cfdfb0d81e6db1aa099dc8e3a84 +size 148181 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50-frame-apply-reset.png b/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50-frame-apply-reset.png index 1a062cdf7..0b7832eee 100644 --- a/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50-frame-apply-reset.png +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50-frame-apply-reset.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:00f8173e7476a781826617bbaf03241348cece3c25d355e13832961cb022145f -size 50155 +oid sha256:0af4ceb50ed269aa80667c3012a871c87f73777cd8cb497ebb243b53932b9bad +size 72377 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50-max-window-13.png b/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50-max-window-13.png index 00af23a9a..2bc2db3a5 100644 --- a/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50-max-window-13.png +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50-max-window-13.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:676217864c9f5ca4792c08a4b51574acdceb8c02b186e467633583b358bdb0f8 -size 121420 +oid sha256:9053c70da35fd42fe44a76e0ace8788ba79667b33c596409ca1e1f2f6d6ba3ad +size 195906 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50-mean-window-13.png b/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50-mean-window-13.png index bd31b760d..d5999dd0f 100644 --- a/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50-mean-window-13.png +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50-mean-window-13.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4bb119d1e38ec69285092c551da7ff12541a15c4ef144689e6535c520e0e988b -size 76596 +oid sha256:77d4a8542a5507e3eda1203a6da29a2f533bbbe2988ad297948c74e44a4337ec +size 177152 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50-mean-window-5.png b/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50-mean-window-5.png index ccbbd13d6..29af0398d 100644 --- a/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50-mean-window-5.png +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50-mean-window-5.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:57202d813c24f93fc106a1e66edd04af5b877ebf291380b3894b583ddccbda19 -size 72288 +oid sha256:3422923039d45b20ea6150f0ad545bdf876596ba60b156df5ec4004590a29a3e +size 139029 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50.png b/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50.png index d4020153c..bb07b8fbb 100644 --- a/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50.png +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cc156be2788a1d82f587a1ae0a7b29f4b6d75f07bfd83055ecefe92b213c01fd -size 56749 +oid sha256:ba9e055298372238ce0cd0c5ac4d75db8cd53f3f4acffbcc22bf7d503b40ec57 +size 79174 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-frame-99.png b/examples/notebooks/screenshots/nb-image-widget-zfish-frame-99.png index 368c1b9f8..6e8274659 100644 --- a/examples/notebooks/screenshots/nb-image-widget-zfish-frame-99.png +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-frame-99.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:03da34d72c3437de1bebea5a545e1a75fb7b6a7ea309cbd88340031acc5cb852 -size 45223 +oid sha256:c6aed15f9f1b6bae442687613c5b04621f42e78f1dbda1e3560b000d652ba0b3 +size 61523 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50-frame-apply-gaussian.png b/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50-frame-apply-gaussian.png index c722c8ec1..28704bd2d 100644 --- a/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50-frame-apply-gaussian.png +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50-frame-apply-gaussian.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:309384b0062003c9af321a005ddb5e0a78db382cf2b40285dd8ca43423cb13dd -size 74975 +oid sha256:499fab9183f2528297fdfa3c96a25eb58b2376a44556d790ef06928e0379af3a +size 174612 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50-frame-apply-reset.png b/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50-frame-apply-reset.png index 9be3a311f..d163fd22a 100644 --- a/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50-frame-apply-reset.png +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50-frame-apply-reset.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:28291e3913c28a2933affa12810254627f352fd4059a77d3637e2756737d11c0 -size 75617 +oid sha256:72832b86f802ee90b4eb54cb64d64aff59527fe0c7dcb87a4d8ab281ad15726b +size 142136 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50-max-window-13.png b/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50-max-window-13.png index 912374687..52ebd8591 100644 --- a/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50-max-window-13.png +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50-max-window-13.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c248700a71fb8284d039714d9b11672f6788bcd0df716c26aa936436c5035c28 -size 116784 +oid sha256:b414fbb8f6935901b65851de9c7cb37df628d7b24759dc7f407ee130389216a3 +size 371687 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50-mean-window-13.png b/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50-mean-window-13.png index 66971bfb1..6c406a621 100644 --- a/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50-mean-window-13.png +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50-mean-window-13.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a5fe9fce8c8e63e497b558370d0d3edd8f86b0ad27939feec02715dd84fe7353 -size 75550 +oid sha256:e0ef52156509f308d972533fb45509ba7451b4d6149400d519aae28274609e41 +size 212053 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50-mean-window-5.png b/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50-mean-window-5.png index 76f6c71a2..aaed804b8 100644 --- a/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50-mean-window-5.png +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50-mean-window-5.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b7d563033c432006b88adce07aabf0ab6a2c6559b519f6f820ebb58066f4d1b2 -size 79214 +oid sha256:8fd94e597074094dc3718153c8bb94fb0b1bf58e58e34601e5c7281f938f52bd +size 200278 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50.png b/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50.png index 90639b14f..3110fa7cf 100644 --- a/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50.png +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8408e6e841d09b4c2f1704803515c878cdc46e4f7d4cd67b1634efb4efba8e98 -size 82327 +oid sha256:76ab21314fbd7846c1b94aeeed9ef7b97be99d2f2f7f09c13c0474a889c21179 +size 159319 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-99.png b/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-99.png index 97e3787ea..0cfad54e7 100644 --- a/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-99.png +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-99.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4c21bbd9a9d1548d5480f9a0f0f6d4ace0abfd80e4d09e295557163310c7095d -size 79733 +oid sha256:77f247de5374a8cb7ce27a338ab8be880d53d9586b52f646b400901ba70be3aa +size 146217 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-grid-init-mean-window-5.png b/examples/notebooks/screenshots/nb-image-widget-zfish-grid-init-mean-window-5.png index 24eda7928..c74807939 100644 --- a/examples/notebooks/screenshots/nb-image-widget-zfish-grid-init-mean-window-5.png +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-grid-init-mean-window-5.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2b6222d1a7291609f5d2637e97e9455a4e734b6dca4d6ebf9246ea46f32ce4de -size 81628 +oid sha256:33c3dfa77bbc558493634ab83fd1539ef76022a2e20d10e1353d2bd0a0e94a2c +size 183739 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-grid-set_data-reset-indices-false.png b/examples/notebooks/screenshots/nb-image-widget-zfish-grid-set_data-reset-indices-false.png index 3285e7875..a2841b1d5 100644 --- a/examples/notebooks/screenshots/nb-image-widget-zfish-grid-set_data-reset-indices-false.png +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-grid-set_data-reset-indices-false.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b6f5d03a9929a12232628da3a0c89673a650c94737ead73bf74e0daa587e75f9 -size 65977 +oid sha256:715f7909db0d374c2e618bb41f7a6341a8cc8891b1e3e8678a6f934fd71159a4 +size 127129 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-grid-set_data-reset-indices-true.png b/examples/notebooks/screenshots/nb-image-widget-zfish-grid-set_data-reset-indices-true.png index 63bd3d070..9064f2323 100644 --- a/examples/notebooks/screenshots/nb-image-widget-zfish-grid-set_data-reset-indices-true.png +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-grid-set_data-reset-indices-true.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:182410c74215dc70140561200cdb414555e98a1ea7767205764a19cc9b72bf6d -size 66489 +oid sha256:76708e8e6e6865d700aa5286fca4d58ba4eb91f21ab3b0243bb128e9a84f063c +size 131192 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-init-mean-window-5.png b/examples/notebooks/screenshots/nb-image-widget-zfish-init-mean-window-5.png index e82e34241..1fbaec974 100644 --- a/examples/notebooks/screenshots/nb-image-widget-zfish-init-mean-window-5.png +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-init-mean-window-5.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:59eaa00b1ba39188f664719964036857df5cb143e2f70171d86e822958e98fa7 -size 62617 +oid sha256:51c62474b9ebee76242ef82a710a83b90e0183792790f6a2cd00213642b76755 +size 99519 diff --git a/examples/notebooks/screenshots/nb-imagewidget.png b/examples/notebooks/screenshots/nb-imagewidget.png new file mode 100644 index 000000000..9acfdb0f9 --- /dev/null +++ b/examples/notebooks/screenshots/nb-imagewidget.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0f7a4a260ef4f9f2bdee9adab4ed376147cff39fcdd2f07eaf6e87e8b899f7d3 +size 89842 diff --git a/examples/notebooks/screenshots/nb-lines-3d.png b/examples/notebooks/screenshots/nb-lines-3d.png index 4ac6b8b92..a3b75de58 100644 --- a/examples/notebooks/screenshots/nb-lines-3d.png +++ b/examples/notebooks/screenshots/nb-lines-3d.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:467cf0f08f6861c9556a412ed57e0bc6ff4499e62d576fa10dcdc2ca9ef4de6a -size 23693 +oid sha256:677f544d93cf7a733003c38666f555f0b598d41b92681dd2f09e6c11faa4aed0 +size 14186 diff --git a/examples/notebooks/screenshots/nb-lines-cmap-jet-values-cosine.png b/examples/notebooks/screenshots/nb-lines-cmap-jet-values-cosine.png index 0c875da24..0d9ff5729 100644 --- a/examples/notebooks/screenshots/nb-lines-cmap-jet-values-cosine.png +++ b/examples/notebooks/screenshots/nb-lines-cmap-jet-values-cosine.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c870f3010432310ba6d1937410a23836de6ddb656eb83bfb1b928d7d1211bd09 -size 17263 +oid sha256:0d981d57d7905879ab68af95da84d2cf530a9a40dc4d0ffb138119a11f4966be +size 11808 diff --git a/examples/notebooks/screenshots/nb-lines-cmap-jet-values.png b/examples/notebooks/screenshots/nb-lines-cmap-jet-values.png index e193c85b3..dbcbf1e7f 100644 --- a/examples/notebooks/screenshots/nb-lines-cmap-jet-values.png +++ b/examples/notebooks/screenshots/nb-lines-cmap-jet-values.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5c98af9372102a7fef96fe45650ca7e0b4f6e3589abd5fc3c1205130f3613c08 -size 18859 +oid sha256:706bffa485dd7994a70602ef2076aee79e1206dd508fbac903549fce526087b6 +size 13095 diff --git a/examples/notebooks/screenshots/nb-lines-cmap-jet.png b/examples/notebooks/screenshots/nb-lines-cmap-jet.png index acc52de78..6a3ae0c1c 100644 --- a/examples/notebooks/screenshots/nb-lines-cmap-jet.png +++ b/examples/notebooks/screenshots/nb-lines-cmap-jet.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bf3494a4cc2d4ea061fbd3465bd2133120b63bab6b248681307687e4e8e395a2 -size 16322 +oid sha256:cfd3f55e1671ac1fa45a8eb26aeb425ccb8d1ac033f5766f4002fee4380b2a77 +size 11174 diff --git a/examples/notebooks/screenshots/nb-lines-cmap-tab-10.png b/examples/notebooks/screenshots/nb-lines-cmap-tab-10.png index d18cadb51..9bb368e0e 100644 --- a/examples/notebooks/screenshots/nb-lines-cmap-tab-10.png +++ b/examples/notebooks/screenshots/nb-lines-cmap-tab-10.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b66364f0b7ba1da27b713e48cc757215f84143f6646451e8dfe7938d90a69102 -size 14863 +oid sha256:338dbcdf1a87266eee33367bfa08bcaec7eac42ef2dd928bbc699b3a0412ebaf +size 9889 diff --git a/examples/notebooks/screenshots/nb-lines-cmap-viridis-values.png b/examples/notebooks/screenshots/nb-lines-cmap-viridis-values.png index ecdbb0db7..23137bdf3 100644 --- a/examples/notebooks/screenshots/nb-lines-cmap-viridis-values.png +++ b/examples/notebooks/screenshots/nb-lines-cmap-viridis-values.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2e1b04f1ffc5c63dd93c9676bbf751f8ca698e113ce3403534f40ab868421960 -size 14974 +oid sha256:e36a7a74ac39dac5ac96a4e7c8e56990794ab1cda1b8ee5276087e9814dd1696 +size 10100 diff --git a/examples/notebooks/screenshots/nb-lines-cmap-viridis.png b/examples/notebooks/screenshots/nb-lines-cmap-viridis.png index a78bd9d54..2fcd4749c 100644 --- a/examples/notebooks/screenshots/nb-lines-cmap-viridis.png +++ b/examples/notebooks/screenshots/nb-lines-cmap-viridis.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:69166f21efd52253b2b8444c32c9770a87983b3f21afcf49f4d0a94c51e5280c -size 19206 +oid sha256:b12ee5e31f64415b57536c59587b91c7a3a7c74e95be3459d8036a43d073d7db +size 13821 diff --git a/examples/notebooks/screenshots/nb-lines-cmap-white.png b/examples/notebooks/screenshots/nb-lines-cmap-white.png index c61050a7d..397b5fc94 100644 --- a/examples/notebooks/screenshots/nb-lines-cmap-white.png +++ b/examples/notebooks/screenshots/nb-lines-cmap-white.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1556f875c6e5538ca289db5ced0e59530174d899d6baa19876e1ad753571d4d1 -size 12921 +oid sha256:6239f23d3b5c1745879f5706352abb905d5638522b171776bff051e511426c2f +size 8359 diff --git a/examples/notebooks/screenshots/nb-lines-colors.png b/examples/notebooks/screenshots/nb-lines-colors.png index 33a751e98..149321e52 100644 --- a/examples/notebooks/screenshots/nb-lines-colors.png +++ b/examples/notebooks/screenshots/nb-lines-colors.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fb407b2008cb7a6793e7e53d487b959e1178de2a3b7fbd504b8ef3b92a7b09ea -size 40842 +oid sha256:9a5e99c5a872d9bbf8dd909498459ddee7f22f08d3fe3cd68b3ea57c105ab51b +size 27634 diff --git a/examples/notebooks/screenshots/nb-lines-data.png b/examples/notebooks/screenshots/nb-lines-data.png index 7447b5b86..9ed38b1f5 100644 --- a/examples/notebooks/screenshots/nb-lines-data.png +++ b/examples/notebooks/screenshots/nb-lines-data.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3fe83c11993dc99085526e11db60ac6797756b2b35145458009b36e56991a0d9 -size 55297 +oid sha256:cc0d3d6819ce0d9f5d531276bbde0397ef35e3084ac1f9b3575f0209eea07456 +size 39512 diff --git a/examples/notebooks/screenshots/nb-lines-underlay.png b/examples/notebooks/screenshots/nb-lines-underlay.png index 692aa76ea..d4b3d9f6d 100644 --- a/examples/notebooks/screenshots/nb-lines-underlay.png +++ b/examples/notebooks/screenshots/nb-lines-underlay.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f65834683f945ae52612fa1ae8415d25866ccd6d776dd94798b30b08769b0387 -size 56338 +oid sha256:9b12c8f29436be8d17c38f420120ab3d54b0eee9bef751eea2f99d01b1a8fa43 +size 50761 diff --git a/examples/notebooks/screenshots/nb-lines.png b/examples/notebooks/screenshots/nb-lines.png index b89e9f47c..603f4a8fc 100644 --- a/examples/notebooks/screenshots/nb-lines.png +++ b/examples/notebooks/screenshots/nb-lines.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cc0511ba508333566c902a3f1a902e76044a33a41f75bfc43ee158ff5b7bd6c7 -size 37671 +oid sha256:eeef2c47e7dde62038307fa7929a306801bf8b708fbcf1062ed9c751727bfb2b +size 24300 diff --git a/examples/tests/test_examples.py b/examples/tests/test_examples.py index 876533fa6..a570b4f36 100644 --- a/examples/tests/test_examples.py +++ b/examples/tests/test_examples.py @@ -1,6 +1,7 @@ """ Test that examples run without error. """ + import importlib import runpy import pytest @@ -8,6 +9,7 @@ import numpy as np import imageio.v3 as iio + from .testutils import ( ROOT, examples_dir, @@ -15,7 +17,11 @@ find_examples, wgpu_backend, is_lavapipe, - diffs_dir + diffs_dir, + generate_diff, + image_similarity, + normalize_image, + prep_for_write, ) # run all tests unless they opt-out @@ -52,7 +58,12 @@ def test_that_we_are_on_lavapipe(): def test_example_screenshots(module, force_offscreen): """Make sure that every example marked outputs the expected.""" # (relative) module name from project root - module_name = module.relative_to(ROOT/"examples").with_suffix("").as_posix().replace("/", ".") + module_name = ( + module.relative_to(ROOT / "examples") + .with_suffix("") + .as_posix() + .replace("/", ".") + ) # import the example module example = importlib.import_module(module_name) @@ -69,20 +80,35 @@ def test_example_screenshots(module, force_offscreen): screenshot_path = screenshots_dir / f"{module.stem}.png" + black = np.zeros(img.shape).astype(np.uint8) + black[:, :, -1] = 255 + + img_alpha = img[..., -1] / 255 + + rgb = img[..., :-1] * img_alpha[..., None] + black[..., :-1] * np.ones( + img_alpha.shape + )[..., None] * (1 - img_alpha[..., None]) + + rgb = rgb.round().astype(np.uint8) + if "REGENERATE_SCREENSHOTS" in os.environ.keys(): if os.environ["REGENERATE_SCREENSHOTS"] == "1": - iio.imwrite(screenshot_path, img) - #np.save(screenshot_path, img) + iio.imwrite(screenshot_path, rgb) assert ( screenshot_path.exists() ), "found # test_example = true but no reference screenshot available" - #stored_img = np.load(screenshot_path) - stored_img = iio.imread(screenshot_path) - is_similar = np.allclose(img, stored_img, atol=1) - update_diffs(module.stem, is_similar, img, stored_img) - assert is_similar, ( - f"rendered image for example {module.stem} changed, see " + + ref_img = iio.imread(screenshot_path) + + rgb = normalize_image(rgb) + ref_img = normalize_image(ref_img) + + similar, rmse = image_similarity(rgb, ref_img, threshold=0.025) + + update_diffs(module.stem, similar, rgb, ref_img) + assert similar, ( + f"diff {rmse} above threshold for {module.stem}, see " f"the {diffs_dir.relative_to(ROOT).as_posix()} folder" " for visual diffs (you can download this folder from" " CI build artifacts as well)" @@ -110,7 +136,6 @@ def get_diffs_rgba(slicer): # split into an rgb and an alpha diff diffs = { diffs_dir / f"diff-{module}-rgb.png": slice(0, 3), - diffs_dir / f"diff-{module}-alpha.png": 3, } for path, slicer in diffs.items(): diff --git a/examples/tests/testutils.py b/examples/tests/testutils.py index 5f6772fb7..f62ae7602 100644 --- a/examples/tests/testutils.py +++ b/examples/tests/testutils.py @@ -7,6 +7,8 @@ import sys from itertools import chain +import numpy as np + ROOT = Path(__file__).parents[2] # repo root examples_dir = ROOT / "examples" / "desktop" @@ -65,3 +67,189 @@ def find_examples(query=None, negative_query=None, return_stems=False): result = [r.stem for r in result] return result + +# image comparison functions from: https://github.com/pygfx/image-comparison +def image_similarity(src, target, threshold=0.2): + """Compute normalized RMSE 0..1 and decide if similar based on threshold. + + For every pixel, the euclidian distance between RGB values is computed, + and normalized by the maximum possible distance (between black and white). + The RMSE is then computed from those errors. + + The normalized RMSE is used to compute the + similarity metric, so larger errors (euclidian distance + between two RGB colors) will have a disproportionately + larger effect on the score than smaller errors. + + In other words, lots of small errors will lead to a good score + (closer to 0) whereas a few large errors will lead to a bad score + (closer to 1). + """ + float_type = np.float64 + src = np.asarray(src, dtype=float_type) + target = np.asarray(target, dtype=float_type) + denom = np.sqrt(np.mean(src * src)) + mse = np.mean((src - target) ** 2) + rmse = np.sqrt(mse) / denom + + similar = bool(rmse < threshold) + return similar, rmse + + +def normalize_image(img): + """Discard the alpha channel and convert from 0..255 uint8 to 0..1 float.""" + assert len(img.shape) == 3 + + # normalize to 0..1 range + if img.dtype == "u1" or np.max(img) > 1: + img = img / 255 + assert np.min(img) >= 0 and np.max(img) <= 1 + + # discard alpha channel + # unsupported if it's not fully opaque + if img.shape[-1] == 4: + assert np.max(img[..., 3]) == 1 + img = img[..., :-1] + + return img + + +def prep_for_write(img): + """Convert 0..1 float back to 0..255 uint8.""" + assert len(img.shape) == 3 + assert np.min(img) >= 0 and np.max(img) <= 1 + if img.dtype != "u1": + img = np.round(img * 255).astype("u1") + return img + + +def rescale_arr(arr, min, max): + """ + histogram rescale utility function + e.g. if the values are 0.3..0.7 + they are rescaled to min..max + """ + return np.interp(arr, (arr.min(), arr.max()), (min, max)) + + +def rgb_to_hls(rgb): + """ + convert rgb to hls + assumes input ranges are 0..1 + returns values in range 0..1 + + vectorized version of colorsys.rgb_to_hls + """ + maxc = np.max(rgb, axis=-1) + minc = np.min(rgb, axis=-1) + hls = np.empty_like(rgb) + l = (minc + maxc) / 2.0 # noqa: E741 + + with np.errstate(invalid="ignore"): + mask = l <= 0.5 + idx = np.where(mask) + hls[(*idx, 2)] = (maxc[idx] - minc[idx]) / (maxc[idx] + minc[idx]) + + idx = np.where(~mask) + hls[(*idx, 2)] = (maxc[idx] - minc[idx]) / (2.0 - maxc[idx] - minc[idx]) + + maxc_minc = maxc - minc + rc = (maxc - rgb[..., 0]) / maxc_minc + gc = (maxc - rgb[..., 1]) / maxc_minc + bc = (maxc - rgb[..., 2]) / maxc_minc + + mask1 = rgb[..., 0] == maxc + idx = np.where(mask1) + hls[(*idx, 0)] = bc[idx] - gc[idx] + + mask2 = rgb[..., 1] == maxc + idx = np.where(~mask1 & mask2) + hls[(*idx, 0)] = 2.0 + rc[idx] - bc[idx] + + idx = np.where(~mask1 & ~mask2) + hls[(*idx, 0)] = 4.0 + gc[idx] - rc[idx] + + hls[..., 0] = (hls[..., 0] / 6.0) % 1.0 + + idx = np.where(minc == maxc) + hls[idx] = 0.0 + hls[..., 1] = l + + return hls + + +def hls_to_rgb(hls): + """ + convert hls to rgb + assumes input ranges are 0..1 + returns values in range 0..1 + + vectorized version of colorsys.hls_to_rgb + """ + rgb = np.empty_like(hls) + + m2 = np.empty_like(hls[..., 1]) + mask = hls[..., 1] <= 0.5 + idx = np.where(mask) + m2[idx] = hls[(*idx, 1)] * (1.0 + hls[(*idx, 2)]) + idx = np.where(~mask) + m2[idx] = hls[(*idx, 1)] + hls[(*idx, 2)] - (hls[(*idx, 1)] * hls[(*idx, 2)]) + m1 = 2.0 * hls[..., 1] - m2 + + h1 = (hls[..., 0] + 1 / 3) % 1.0 + h2 = hls[..., 0] % 1.0 + h3 = (hls[..., 0] - 1 / 3) % 1.0 + + for i, h in enumerate([h1, h2, h3]): + mask1 = h < 1 / 6 + idx = np.where(mask1) + rgb[(*idx, i)] = m1[idx] + (m2[idx] - m1[idx]) * h[idx] * 6.0 + + mask2 = h < 0.5 + idx = np.where(~mask1 & mask2) + rgb[(*idx, i)] = m2[idx] + + mask3 = h < 2 / 3 + idx = np.where(~mask1 & ~mask2 & mask3) + rgb[(*idx, i)] = m1[idx] + (m2[idx] - m1[idx]) * ((2 / 3) - h[idx]) * 6.0 + + idx = np.where(~mask1 & ~mask2 & ~mask3) + rgb[(*idx, i)] = m1[idx] + + return rgb + + +def generate_diff(src, target, fuzz=0.05): + """ + Generate an image that + highlights the differences between src and target image + any pixels with a euclidian color distance < fuzz will be ignored + fuzz is expressed as a percentage of the maximum possible distance + which is the distance between (0,0,0) and (1,1,1) = sqrt(3). + """ + # compute euclidian distance between pixels + # and normalize to 0..1 + max_dist = np.linalg.norm([1, 1, 1], axis=-1) + error = np.linalg.norm(np.abs(target - src), axis=-1) / max_dist + # apply fuzz + error_idx = np.where(error > fuzz) + + diff_img_hls = rgb_to_hls(target) + # lighten the whole image + diff_img_hls[..., 1] = rescale_arr(diff_img_hls[..., 1], 0.25, 1.0) + diff_img_hls[..., 1] **= 0.2 + # reduce the color saturation + diff_img_hls[..., 2] = rescale_arr(diff_img_hls[..., 2], 0.0, 0.75) + diff_img_hls[..., 2] **= 2 + + # make the diff pixels red + diff_img_hls[(*error_idx, 0)] = 0 + # give them the same lighting level + diff_img_hls[(*error_idx, 1)] = 0.5 + # saturate based on the error + diff_img_hls[(*error_idx, 2)] = 0.5 + error[error_idx] * 0.5 + + # convert back to rgb + diff_img = hls_to_rgb(diff_img_hls) + + return diff_img From 704b36a2be501cb1e7e5093bbe20f46c7792e604 Mon Sep 17 00:00:00 2001 From: Kushal Kolar Date: Sun, 31 Mar 2024 04:21:26 -0400 Subject: [PATCH 38/66] Update CONTRIBUTING.md update contributing w.r.t. simpler screenshot testing --- CONTRIBUTING.md | 83 +++++++++++++++++++++++++++++-------------------- 1 file changed, 50 insertions(+), 33 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ad484db5d..facca91db 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -8,7 +8,7 @@ Contributions are welcome! :smile: ![image](https://github.com/kushalkolar/fastplotlib/assets/9403332/82612021-37b2-48dd-b7e4-01a919535c17) -2. Clone the repo and install according to the development instructions. Replace the `YOUR_ACCOUNT` in the repo URL to the fork on your account. Note that fastplotlib uses [git-lfs](https://git-lfs.com) for storing large files, so you will need to [install it](https://github.com/git-lfs/git-lfs#installing) before cloning the repo. +2. Clone the repo and install according to the development instructions. Replace the `YOUR_ACCOUNT` in the repo URL to the fork on your account. We use [git-lfs](https://git-lfs.com) for storing large files, such as ground-truths for tests, so you will need to [install it](https://github.com/git-lfs/git-lfs#installing) before cloning the repo. ```bash git clone https://github.com/YOUR_ACCOUNT/fastplotlib.git @@ -18,14 +18,12 @@ cd fastplotlib pip install -e ".[notebook,docs,tests]" ``` -> If you cloned before installing `git-lfs`, you can run `git lfs pull` at any -> time to properly download files. +> If you cloned the repo before installing `git-lfs`, you can run `git lfs pull` at any +> time to download the files stored on LFS 3. Checkout the `main` branch, and then checkout your feature or bug fix branch, and run tests: -> **Warning** -> Do not commit or add any changes from `examples/desktop/screenshots`. -> If you are creating new test examples that generate or change screenshots please post an issue on the repo and we will help you. The screenshots will be generated on github actions servers, which you can then copy into the screenshots dir. :) +If your contributions modify how visualizations look, see the "Tests in detail" section at the very bottom. ```bash cd fastplotlib @@ -35,14 +33,14 @@ git checkout main # checkout your new branch from main git checkout -b my-new-feature-branch -# make your changes -# run tests -REGENERATE_SCREENSHOTS=1 pytest -v -k examples - -# make some changes, lint with black, and commit +# make some changes, lint with black black . -# add only your changed files, not the entire repo, do not add changes to examples/screenshots +# run tests from the repo root dir +pytest -v examples +FASTPLOTLIB_NB_TESTS=1 pytest --nbmake examples/notebooks/ + +# add your changed files, do not add any changes from screenshot diff dirs git add my_changed_files # commit changes @@ -172,7 +170,7 @@ This class inherits from `PlotArea` and `GraphicMethodsMixin`. `GraphicMethodsMixin` is a simple class that just has all the `add_` methods. It is autogenerated by a utility script like this: ```bash -python fastplotlib/utils/generate_add_methods.py +python scripts/generate_add_methods.py ``` Each `add_` method basically creates an instance of `Graphic`, adds it to the `Subplot`, and returns a weakref @@ -197,28 +195,47 @@ below the `Canvas`. If using a glfw canvas it just returns the canvas. addition of `Frame`. `GridPlot.__init__` basically does a lot of parsing of user arguments to determine how to create the subplots. All subplots within a `GridPlot` share the same canvas and use different viewports to create the subplots. -## Running tests +## Tests in detail + +The CI pipeline for a plotting library that is supposed to produce things that "look visually correct". Each example +within the `examples` dir is run and an image of the canvas is taken and compared with a ground-truth +screenshot that we have manually inspected. Ground-truth image are stored using `git-lfs`. + +The ground-truth images are in: + +``` +examples/desktop/screenshots +examples/notebooks/screenshots +``` + +The tests will produce slightly different imperceptible (to a human) results on different hardware when compared to the +ground-truth. A small RMSE tolerance has been chosen, `0.025` for most examples. If the output image and +ground-truth image are within that tolerance the test will pass. -The CI pipeline for a plotting library is is supposed to produce things that "look visually correct" is a bit more -complicated than the CI pipeline for most libraries. -Our CI pipeline is modelled after `pygfx`. Basically, a bunch of examples exist within the `examples` dir. Each of these -examples are run and a screenshot of the canvas is taken and compared with a ground-truth screenshot that we have -manually inspected. Screenshots are stored using `git-lfs`. +To run tests: -At the moment these tests will produce slightly different imperceptible (to a human) results on different hardware, but -nonetheless the image arrays will have a small difference. Because of this, we only run tests on GitHub actions. There -is a specific actions workflow that only generate screenshots and doesn't run tests which is run specifically to create -ground-truth screenshot on GitHub actions servers to use for test. +```bash +# desktop examples +pytest -v examples + +# notebook examples +FASTPLOTLIB_NB_TESTS=1 pytest --nbmake examples/notebooks/ +``` + +If your contribution modifies a ground-truth test screenshot then replace the ground-truth image along with your PR and +also notify us of this in the PR. Likewise, if your contribution requires a new test or new ground-truth then include +this new image in your PR. -If your contribution modifies a ground-truth test screenshot the general workflow is like this, if you have questions -don't hesitate to ask us :smile: +You can create/regenerate ground-truths for the examples like this: + +```bash +# desktop examples +REGENERATE_SCREENSHOTS=1 pytest -v examples/ + +# notebook examples +FASTPLOTLIB_NB_TESTS=1 REGENERATE_SCREENSHOTS=1 pytest --nbmake examples/notebooks/image_widget_test.ipynb +``` -1. Create a PR with your code changes, **do not upload any new/modified test screenshots" -1. See if the CI failed, if so it will indicate the specific examples that have failed. -1. Go to the details page for the "Screenshots / Regenerate" workflow. Click on "Summary" in the top left, and download -the build artifact which is named "screenshots". This is a zip file of generated ground-truth screenshots. -1. Visually inspect the specific new/modified screenshot that corresponds to your code change. Make sure it looks like, -past a copy of it in the PR as a reply -1. Copy over the new/modified screenshots to your local repo. -1. Add and commit the new/modified screenshot files, and push. The tests should now pass. +**Please only commit ground-truth images that correspond to your PR** since this will generate ground-truth images for +the entire test suite. From 2f49497da288f0a6de56be5d9eef6908f8dd1a29 Mon Sep 17 00:00:00 2001 From: Kushal Kolar Date: Sun, 31 Mar 2024 04:21:44 -0400 Subject: [PATCH 39/66] min version py3.10, cleanup type annotations (#465) --- fastplotlib/graphics/_base.py | 18 ++-- fastplotlib/graphics/line.py | 6 +- fastplotlib/graphics/line_collection.py | 77 +++++++++-------- fastplotlib/graphics/scatter.py | 6 +- fastplotlib/layouts/_defaults.py | 1 - .../layouts/_frame/_ipywidget_toolbar.py | 4 +- fastplotlib/layouts/_frame/_jupyter_output.py | 4 +- ...ods_mixin.py => _graphic_methods_mixin.py} | 82 ++++++++++--------- fastplotlib/layouts/_gridplot.py | 57 ++++++++----- fastplotlib/layouts/_plot_area.py | 77 +++++++++-------- fastplotlib/layouts/_record_mixin.py | 5 +- fastplotlib/layouts/_subplot.py | 30 ++++--- fastplotlib/layouts/_utils.py | 15 ++-- fastplotlib/legends/legend.py | 12 +-- fastplotlib/utils/functions.py | 7 +- fastplotlib/widgets/histogram_lut.py | 3 +- .../generate_add_graphic_methods.py | 2 +- setup.py | 2 +- 18 files changed, 218 insertions(+), 190 deletions(-) delete mode 100644 fastplotlib/layouts/_defaults.py rename fastplotlib/layouts/{graphic_methods_mixin.py => _graphic_methods_mixin.py} (87%) rename fastplotlib/utils/generate_add_methods.py => scripts/generate_add_graphic_methods.py (97%) diff --git a/fastplotlib/graphics/_base.py b/fastplotlib/graphics/_base.py index fdf120268..4442c851e 100644 --- a/fastplotlib/graphics/_base.py +++ b/fastplotlib/graphics/_base.py @@ -1,4 +1,4 @@ -from typing import * +from typing import Any, Literal import weakref from warnings import warn from abc import ABC, abstractmethod @@ -13,7 +13,7 @@ # dict that holds all world objects for a given python kernel/session # Graphic objects only use proxies to WorldObjects -WORLD_OBJECTS: Dict[str, WorldObject] = dict() #: {hex id str: WorldObject} +WORLD_OBJECTS: dict[str, WorldObject] = dict() #: {hex id str: WorldObject} PYGFX_EVENTS = [ @@ -87,7 +87,7 @@ def __init__( self._plot_area = None @property - def name(self) -> Union[str, None]: + def name(self) -> str | None: """str name reference for this item""" return self._name @@ -162,7 +162,7 @@ def visible(self, v: bool): self.world_object.visible = v @property - def children(self) -> List[WorldObject]: + def children(self) -> list[WorldObject]: """Return the children of the WorldObject.""" return self.world_object.children @@ -432,7 +432,7 @@ class PreviouslyModifiedData: indices: Any -COLLECTION_GRAPHICS: Dict[str, Graphic] = dict() +COLLECTION_GRAPHICS: dict[str, Graphic] = dict() class GraphicCollection(Graphic): @@ -440,7 +440,7 @@ class GraphicCollection(Graphic): def __init__(self, name: str = None): super().__init__(name) - self._graphics: List[str] = list() + self._graphics: list[str] = list() self._graphics_changed: bool = True self._graphics_array: np.ndarray[Graphic] = None @@ -548,7 +548,7 @@ class CollectionIndexer: def __init__( self, parent: GraphicCollection, - selection: List[Graphic], + selection: list[Graphic], ): """ @@ -605,7 +605,7 @@ def __repr__(self): class CollectionFeature: """Collection Feature""" - def __init__(self, selection: List[Graphic], feature: str): + def __init__(self, selection: list[Graphic], feature: str): """ selection: list of Graphics a list of the selected Graphics from the parent GraphicCollection based on the ``selection_indices`` @@ -618,7 +618,7 @@ def __init__(self, selection: List[Graphic], feature: str): self._selection = selection self._feature = feature - self._feature_instances: List[GraphicFeature] = list() + self._feature_instances: list[GraphicFeature] = list() if len(self._selection) > 0: for graphic in self._selection: diff --git a/fastplotlib/graphics/line.py b/fastplotlib/graphics/line.py index d76c8e704..f44347a58 100644 --- a/fastplotlib/graphics/line.py +++ b/fastplotlib/graphics/line.py @@ -18,10 +18,10 @@ def __init__( self, data: Any, thickness: float = 2.0, - colors: Union[str, np.ndarray, Iterable] = "w", + colors: str | np.ndarray | Iterable = "w", alpha: float = 1.0, cmap: str = None, - cmap_values: Union[np.ndarray, List] = None, + cmap_values: np.ndarray | Iterable = None, z_position: float = None, collection_index: int = None, *args, @@ -46,7 +46,7 @@ def __init__( 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 + cmap_values: 1D array-like or Iterable of numerical values, optional if provided, these values are used to map the colors from the cmap alpha: float, optional, default 1.0 diff --git a/fastplotlib/graphics/line_collection.py b/fastplotlib/graphics/line_collection.py index bb7bb2444..8488ec15e 100644 --- a/fastplotlib/graphics/line_collection.py +++ b/fastplotlib/graphics/line_collection.py @@ -20,14 +20,14 @@ class LineCollection(GraphicCollection, Interaction): 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", + z_position: Iterable[float] | float = None, + thickness: float | Iterable[float] = 2.0, + colors: str | Iterable[str] | np.ndarray | Iterable[np.ndarray] = "w", alpha: float = 1.0, - cmap: Union[List[str], str] = None, - cmap_values: Union[np.ndarray, List] = None, + cmap: Iterable[str] | str = None, + cmap_values: np.ndarray | List = None, name: str = None, - metadata: Union[list, tuple, np.ndarray] = None, + metadata: Iterable[Any] | np.ndarray = None, *args, **kwargs, ): @@ -36,39 +36,41 @@ def __init__( 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 + z_position: Iterable 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 + thickness: float or Iterable 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" + colors: str, RGBA array, Iterable of RGBA array, or Iterable 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 + alpha: float, optional + alpha value for colors, if colors is a ``str`` + + cmap: Iterable 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 + cmap_values: 1D array-like or Iterable 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: Iterable or array metadata associated with this collection, this is for the user to manage. ``len(metadata)`` must be same as ``len(data)`` @@ -235,7 +237,7 @@ def cmap_values(self) -> np.ndarray: return self._cmap_values @cmap_values.setter - def cmap_values(self, values: Union[np.ndarray, list]): + def cmap_values(self, values: np.ndarray | Iterable): colors = parse_cmap_values( n_colors=len(self), cmap_name=self.cmap, cmap_values=values ) @@ -477,13 +479,16 @@ 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", + z_position: Iterable[float] | float = None, + thickness: float | Iterable[float] = 2.0, + colors: str | Iterable[str] | np.ndarray | Iterable[np.ndarray] = "w", + alpha: float = 1.0, + cmap: Iterable[str] | str = None, + cmap_values: np.ndarray | List = None, name: str = None, + metadata: Iterable[Any] | np.ndarray = None, + separation: float = 10.0, + separation_axis: str = "y", *args, **kwargs, ): @@ -492,33 +497,37 @@ def __init__( Parameters ---------- - data: list of array-like + 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 + z_position: Iterable 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 + | if ``list`` of ``float``, each value will apply to the individual lines - thickness: float or list of float, default 2.0 + thickness: float or Iterable 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" + colors: str, RGBA array, Iterable of RGBA array, or Iterable 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 + | 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 + cmap: Iterable 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 + cmap_values: 1D array-like or Iterable of numerical values, optional + if provided, these values are used to map the colors from the cmap + + metadata: Iterable or array + metadata associated with this collection, this is for the user to manage. + ``len(metadata)`` must be same as ``len(data)`` separation: float, default 10 space in between each line graphic in the stack @@ -529,13 +538,9 @@ def __init__( name: str, optional name of the line stack - args - passed to LineCollection - kwargs passed to LineCollection - Features -------- @@ -549,8 +554,12 @@ def __init__( z_position=z_position, thickness=thickness, colors=colors, + alpha=alpha, cmap=cmap, + cmap_values=cmap_values, + metadata=metadata, name=name, + *args, **kwargs, ) diff --git a/fastplotlib/graphics/scatter.py b/fastplotlib/graphics/scatter.py index 3f04f644e..2557cd637 100644 --- a/fastplotlib/graphics/scatter.py +++ b/fastplotlib/graphics/scatter.py @@ -14,11 +14,11 @@ class ScatterGraphic(Graphic): def __init__( self, data: np.ndarray, - sizes: Union[int, float, np.ndarray, list] = 1, - colors: np.ndarray = "w", + sizes: float | np.ndarray | Iterable[float] = 1, + colors: str | np.ndarray | Iterable[str] = "w", alpha: float = 1.0, cmap: str = None, - cmap_values: Union[np.ndarray, List] = None, + cmap_values: np.ndarray | List = None, z_position: float = 0.0, *args, **kwargs, diff --git a/fastplotlib/layouts/_defaults.py b/fastplotlib/layouts/_defaults.py deleted file mode 100644 index 8b1378917..000000000 --- a/fastplotlib/layouts/_defaults.py +++ /dev/null @@ -1 +0,0 @@ - diff --git a/fastplotlib/layouts/_frame/_ipywidget_toolbar.py b/fastplotlib/layouts/_frame/_ipywidget_toolbar.py index 72976a445..5b42c8eab 100644 --- a/fastplotlib/layouts/_frame/_ipywidget_toolbar.py +++ b/fastplotlib/layouts/_frame/_ipywidget_toolbar.py @@ -4,8 +4,6 @@ from math import copysign from functools import partial from pathlib import Path -from typing import * - from ipywidgets.widgets import ( IntSlider, @@ -238,7 +236,7 @@ def __init__(self, iw): tooltip="reset vmin/vmax and reset histogram using current frame", ) - self.sliders: Dict[str, IntSlider] = dict() + self.sliders: dict[str, IntSlider] = dict() # only for xy data, no time point slider needed if self.iw.ndim == 2: diff --git a/fastplotlib/layouts/_frame/_jupyter_output.py b/fastplotlib/layouts/_frame/_jupyter_output.py index 786041bcf..9ebf0941d 100644 --- a/fastplotlib/layouts/_frame/_jupyter_output.py +++ b/fastplotlib/layouts/_frame/_jupyter_output.py @@ -1,5 +1,3 @@ -from typing import * - from ipywidgets import VBox, Widget from sidecar import Sidecar from IPython.display import display @@ -20,7 +18,7 @@ def __init__( make_toolbar: bool, use_sidecar: bool, sidecar_kwargs: dict, - add_widgets: List[Widget], + add_widgets: list[Widget], ): """ diff --git a/fastplotlib/layouts/graphic_methods_mixin.py b/fastplotlib/layouts/_graphic_methods_mixin.py similarity index 87% rename from fastplotlib/layouts/graphic_methods_mixin.py rename to fastplotlib/layouts/_graphic_methods_mixin.py index 0376fd777..a7acb5eec 100644 --- a/fastplotlib/layouts/graphic_methods_mixin.py +++ b/fastplotlib/layouts/_graphic_methods_mixin.py @@ -178,14 +178,14 @@ def add_image( 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", + z_position: Union[Iterable[float], float] = None, + thickness: Union[float, Iterable[float]] = 2.0, + colors: Union[str, Iterable[str], numpy.ndarray, Iterable[numpy.ndarray]] = "w", alpha: float = 1.0, - cmap: Union[List[str], str] = None, + cmap: Union[Iterable[str], str] = None, cmap_values: Union[numpy.ndarray, List] = None, name: str = None, - metadata: Union[list, tuple, numpy.ndarray] = None, + metadata: Union[Iterable[Any], numpy.ndarray] = None, *args, **kwargs ) -> LineCollection: @@ -195,39 +195,41 @@ def add_line_collection( 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 + z_position: Iterable 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 + thickness: float or Iterable 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" + colors: str, RGBA array, Iterable of RGBA array, or Iterable 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 + alpha: float, optional + alpha value for colors, if colors is a ``str`` + + cmap: Iterable 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 + cmap_values: 1D array-like or Iterable 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: Iterable or array metadata associated with this collection, this is for the user to manage. ``len(metadata)`` must be same as ``len(data)`` @@ -268,7 +270,7 @@ def add_line( colors: Union[str, numpy.ndarray, Iterable] = "w", alpha: float = 1.0, cmap: str = None, - cmap_values: Union[numpy.ndarray, List] = None, + cmap_values: Union[numpy.ndarray, Iterable] = None, z_position: float = None, collection_index: int = None, *args, @@ -294,7 +296,7 @@ def add_line( 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 + cmap_values: 1D array-like or Iterable of numerical values, optional if provided, these values are used to map the colors from the cmap alpha: float, optional, default 1.0 @@ -346,13 +348,16 @@ def add_line( 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", + z_position: Union[Iterable[float], float] = None, + thickness: Union[float, Iterable[float]] = 2.0, + colors: Union[str, Iterable[str], numpy.ndarray, Iterable[numpy.ndarray]] = "w", + alpha: float = 1.0, + cmap: Union[Iterable[str], str] = None, + cmap_values: Union[numpy.ndarray, List] = None, name: str = None, + metadata: Union[Iterable[Any], numpy.ndarray] = None, + separation: float = 10.0, + separation_axis: str = "y", *args, **kwargs ) -> LineStack: @@ -362,33 +367,37 @@ def add_line_stack( Parameters ---------- - data: list of array-like + 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 + z_position: Iterable 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 + | if ``list`` of ``float``, each value will apply to the individual lines - thickness: float or list of float, default 2.0 + thickness: float or Iterable 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" + colors: str, RGBA array, Iterable of RGBA array, or Iterable 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 + | 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 + cmap: Iterable 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 + cmap_values: 1D array-like or Iterable of numerical values, optional + if provided, these values are used to map the colors from the cmap + + metadata: Iterable or array + metadata associated with this collection, this is for the user to manage. + ``len(metadata)`` must be same as ``len(data)`` separation: float, default 10 space in between each line graphic in the stack @@ -399,13 +408,9 @@ def add_line_stack( name: str, optional name of the line stack - args - passed to LineCollection - kwargs passed to LineCollection - Features -------- @@ -421,10 +426,13 @@ def add_line_stack( z_position, thickness, colors, + alpha, cmap, + cmap_values, + name, + metadata, separation, separation_axis, - name, *args, **kwargs ) @@ -432,8 +440,8 @@ def add_line_stack( def add_scatter( self, data: numpy.ndarray, - sizes: Union[int, float, numpy.ndarray, list] = 1, - colors: numpy.ndarray = "w", + sizes: Union[float, numpy.ndarray, Iterable[float]] = 1, + colors: Union[str, numpy.ndarray, Iterable[str]] = "w", alpha: float = 1.0, cmap: str = None, cmap_values: Union[numpy.ndarray, List] = None, diff --git a/fastplotlib/layouts/_gridplot.py b/fastplotlib/layouts/_gridplot.py index fa987b661..5f7f3086d 100644 --- a/fastplotlib/layouts/_gridplot.py +++ b/fastplotlib/layouts/_gridplot.py @@ -1,6 +1,6 @@ from itertools import product, chain import numpy as np -from typing import * +from typing import Literal from inspect import getfullargspec from warnings import warn @@ -18,14 +18,23 @@ class GridPlot(Frame, RecordMixin): def __init__( self, - shape: Tuple[int, int], - cameras: Union[str, list, np.ndarray] = "2d", - controller_types: Union[str, list, np.ndarray] = None, - controller_ids: Union[str, list, np.ndarray] = None, - canvas: Union[str, WgpuCanvasBase, pygfx.Texture] = None, + shape: tuple[int, int], + cameras: ( + Literal["2d", "3d"] + | list[Literal["2d", "3d"]] + | list[pygfx.PerspectiveCamera] + | np.ndarray + ) = "2d", + controller_types: ( + Literal["panzoom", "fly", "trackball", "orbit"] + | list[Literal["panzoom", "fly", "trackball", "orbit"]] + | np.ndarray + ) = None, + controller_ids: str | list[int] | np.ndarray | list[list[str]] = None, + canvas: str | WgpuCanvasBase | pygfx.Texture = None, renderer: pygfx.WgpuRenderer = None, - size: Tuple[int, int] = (500, 300), - names: Union[list, np.ndarray] = None, + size: tuple[int, int] = (500, 300), + names: list | np.ndarray = None, ): """ A grid of subplots. @@ -35,27 +44,31 @@ def __init__( shape: (int, int) (n_rows, n_cols) - cameras: str, list, or np.ndarray, optional - | One of ``"2d"`` or ``"3d"`` indicating 2D or 3D cameras for all subplots + cameras: "2d", "3", list of "2d" | "3d", list of camera instances, or np.ndarray of "2d" | "3d", optional + | if str, one of ``"2d"`` or ``"3d"`` indicating 2D or 3D cameras for all subplots | list/array of ``2d`` and/or ``3d`` that specifies the camera type for each subplot | list/array of pygfx.PerspectiveCamera instances controller_types: str, list or np.ndarray, optional list or array that specifies the controller type for each subplot, or list/array of - pygfx.Controller instances + pygfx.Controller instances. Valid controller types: "panzoom", "fly", "trackball", "orbit". + If not specified a default controller is chosen based on the camera type. + Orthographic projections, i.e. "2d" cameras, use a "panzoom" controller by default. + Perspective projections with a FOV > 0, i.e. "3d" cameras, use a "fly" controller by default. - controller_ids: str, list or np.ndarray of int or str ids, optional + + controller_ids: str, list of int, np.ndarray of int, or list with sublists of subplot str names, 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``. + | If array/list it must be reshapeable to ``grid_shape``. This allows custom assignment of controllers | Example with integers: | sync first 2 plots, and sync last 2 plots: [[0, 0, 1], [2, 3, 3]] | Example with str subplot names: - | list of lists of subplot names, each sublist is synced: [[subplot_a, subplot_b], [subplot_f, subplot_c]] - | this syncs subplot_a and subplot_b together; syncs subplot_f and subplot_c together + | list of lists of subplot names, each sublist is synced: [[subplot_a, subplot_b, subplot_e], [subplot_c, subplot_d]] + | this syncs subplot_a, subplot_b and subplot_e together; syncs subplot_c and subplot_d together canvas: WgpuCanvas, optional Canvas for drawing @@ -249,8 +262,8 @@ def __init__( name=name, ) - self._animate_funcs_pre: List[callable] = list() - self._animate_funcs_post: List[callable] = list() + self._animate_funcs_pre: list[callable] = list() + self._animate_funcs_post: list[callable] = list() self._current_iter = None @@ -269,12 +282,12 @@ def renderer(self) -> pygfx.WgpuRenderer: """The renderer associated to this GridPlot""" return self._renderer - def __getitem__(self, index: Union[Tuple[int, int], str]) -> Subplot: + def __getitem__(self, index: tuple[int, int] | str) -> Subplot: if isinstance(index, str): for subplot in self._subplots.ravel(): if subplot.name == index: return subplot - raise IndexError("no subplot with given name") + raise IndexError(f"no subplot with given name: {index}") else: return self._subplots[index[0], index[1]] @@ -291,7 +304,7 @@ def render(self): # call post-render animate functions self._call_animate_functions(self._animate_funcs_post) - def _call_animate_functions(self, funcs: Iterable[callable]): + def _call_animate_functions(self, funcs: list[callable]): for fn in funcs: try: if len(getfullargspec(fn).args) > 0: @@ -307,7 +320,7 @@ def _call_animate_functions(self, funcs: Iterable[callable]): def add_animations( self, - *funcs: Iterable[callable], + *funcs: callable, pre_render: bool = True, post_render: bool = False, ): @@ -317,7 +330,7 @@ def add_animations( Parameters ---------- - *funcs: callable or iterable of callable + *funcs: callable(s) function(s) that are called on each render cycle pre_render: bool, default ``True``, optional keyword-only argument diff --git a/fastplotlib/layouts/_plot_area.py b/fastplotlib/layouts/_plot_area.py index 2c93d7e9e..299bc6e5d 100644 --- a/fastplotlib/layouts/_plot_area.py +++ b/fastplotlib/layouts/_plot_area.py @@ -1,5 +1,5 @@ from inspect import getfullargspec -from typing import * +from typing import TypeAlias, Literal, Union import weakref from warnings import warn @@ -9,7 +9,7 @@ from pylinalg import vec_transform, vec_unproject from wgpu.gui import WgpuCanvasBase -from ._utils import create_camera, create_controller +from ._utils import create_controller from ..graphics._base import Graphic from ..graphics.selectors._base_selector import BaseSelector from ..legends import Legend @@ -17,17 +17,18 @@ # dict to store Graphic instances # this is the only place where the real references to Graphics are stored in a Python session # {hex id str: Graphic} -GRAPHICS: Dict[str, Graphic] = dict() -SELECTORS: Dict[str, BaseSelector] = dict() +HexStr: TypeAlias = str +GRAPHICS: dict[HexStr, Graphic] = dict() +SELECTORS: dict[HexStr, BaseSelector] = dict() class PlotArea: def __init__( self, - parent, - position: Any, - camera: Union[pygfx.PerspectiveCamera], - controller: Union[pygfx.Controller], + parent: Union["PlotArea", "GridPlot"], + position: tuple[int, int] | str, + camera: pygfx.PerspectiveCamera, + controller: pygfx.Controller, scene: pygfx.Scene, canvas: WgpuCanvasBase, renderer: pygfx.WgpuRenderer, @@ -39,18 +40,18 @@ def __init__( Parameters ---------- - parent: PlotArea - parent class of subclasses will be a ``PlotArea`` instance + parent: PlotArea or GridPlot + parent object 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`` + position of the plot area. In a ``subplot`` position would correspond to the ``[row, column]`` + index of the ``subplot``. In docks this would correspond to a str name, "top", "right", "bottom" or "left" camera: pygfx.PerspectiveCamera - Use perspective camera for both perspective and orthographic views. Set fov = 0 for orthographic mode. + Use perspective camera for both perspective and orthographic views. Set fov = 0 for orthographic projection controller: pygfx.Controller - One of the pygfx controllers, panzoom, fly, orbit, or trackball + One of the pygfx controllers: "panzoom", "fly", "trackball", "orbit" scene: pygfx.Scene represents the root of a scene graph, will be viewed by the given ``camera`` @@ -62,20 +63,17 @@ def __init__( renders the scene onto the canvas name: str, optional - name this ``subplot`` or ``plot`` + name this plot area """ - self._parent: PlotArea = parent + self._parent = parent self._position = position self._scene = scene self._canvas = canvas self._renderer = renderer - if parent is None: - self._viewport: pygfx.Viewport = pygfx.Viewport(renderer) - else: - self._viewport = pygfx.Viewport(parent.renderer) + self._viewport: pygfx.Viewport = pygfx.Viewport(renderer) self._camera = camera self._controller = controller @@ -85,18 +83,18 @@ def __init__( self.viewport, ) - self._animate_funcs_pre = list() - self._animate_funcs_post = list() + self._animate_funcs_pre: list[callable] = list() + self._animate_funcs_post: list[callable] = list() self.renderer.add_event_handler(self.set_viewport_rect, "resize") # list of hex id strings for all graphics managed by this PlotArea # the real Graphic instances are stored in the ``GRAPHICS`` dict - self._graphics: List[str] = list() + self._graphics: list[str] = list() # selectors are in their own list so they can be excluded from scene bbox calculations # managed similar to GRAPHICS for garbage collection etc. - self._selectors: List[str] = list() + self._selectors: list[str] = list() self._name = name @@ -108,11 +106,11 @@ def __init__( # several read-only properties @property def parent(self): - """A parent if relevant, used by individual Subplots in GridPlot""" + """A parent if relevant""" return self._parent @property - def position(self) -> Union[Tuple[int, int], Any]: + def position(self) -> tuple[int, int] | str: """Position of this plot area within a larger layout (such as GridPlot) if relevant""" return self._position @@ -142,7 +140,7 @@ def camera(self) -> pygfx.PerspectiveCamera: return self._camera @camera.setter - def camera(self, new_camera: Union[str, pygfx.PerspectiveCamera]): + def camera(self, new_camera: str | pygfx.PerspectiveCamera): # user wants to set completely new camera, remove current camera from controller if isinstance(new_camera, pygfx.PerspectiveCamera): self.controller.remove_camera(self._camera) @@ -178,7 +176,7 @@ def controller(self) -> pygfx.Controller: return self._controller @controller.setter - def controller(self, new_controller: Union[str, pygfx.Controller]): + def controller(self, new_controller: str | pygfx.Controller): new_controller = create_controller(new_controller, self._camera) cameras_list = list() @@ -206,7 +204,7 @@ def controller(self, new_controller: Union[str, pygfx.Controller]): self._controller = new_controller @property - def graphics(self) -> Tuple[Graphic, ...]: + def graphics(self) -> tuple[Graphic, ...]: """Graphics in the plot area. Always returns a proxy to the Graphic instances.""" proxies = list() for loc in self._graphics: @@ -218,7 +216,7 @@ def graphics(self) -> Tuple[Graphic, ...]: return tuple(proxies) @property - def selectors(self) -> Tuple[BaseSelector, ...]: + def selectors(self) -> tuple[BaseSelector, ...]: """Selectors in the plot area. Always returns a proxy to the Graphic instances.""" proxies = list() for loc in self._selectors: @@ -228,7 +226,7 @@ def selectors(self) -> Tuple[BaseSelector, ...]: return tuple(proxies) @property - def legends(self) -> Tuple[Legend, ...]: + def legends(self) -> tuple[Legend, ...]: """Legends in the plot area.""" proxies = list() for loc in self._graphics: @@ -253,7 +251,7 @@ def name(self, name: str): raise TypeError("PlotArea `name` must be of type ") self._name = name - def get_rect(self) -> Tuple[float, float, float, float]: + def get_rect(self) -> tuple[float, float, float, float]: """ Returns the viewport rect to define the rectangle occupied by the viewport w.r.t. the Canvas. @@ -267,7 +265,7 @@ def get_rect(self) -> Tuple[float, float, float, float]: raise NotImplementedError("Must be implemented in subclass") def map_screen_to_world( - self, pos: Union[Tuple[float, float], pygfx.PointerEvent] + self, pos: tuple[float, float] | pygfx.PointerEvent ) -> np.ndarray: """ Map screen position to world position @@ -316,7 +314,7 @@ def render(self): self._call_animate_functions(self._animate_funcs_post) - def _call_animate_functions(self, funcs: Iterable[callable]): + def _call_animate_functions(self, funcs: list[callable]): for fn in funcs: try: args = getfullargspec(fn).args @@ -337,7 +335,7 @@ def _call_animate_functions(self, funcs: Iterable[callable]): def add_animations( self, - *funcs: Iterable[callable], + *funcs: callable, pre_render: bool = True, post_render: bool = False, ): @@ -347,7 +345,7 @@ def add_animations( Parameters ---------- - *funcs: callable or iterable of callable + *funcs: callable(s) function(s) that are called on each render cycle pre_render: bool, default ``True``, optional keyword-only argument @@ -460,7 +458,7 @@ def _add_or_insert_graphic( self, graphic: Graphic, center: bool = True, - action: str = Union["insert", "add"], + action: str = Literal["insert", "add"], index: int = 0, ): """Private method to handle inserting or adding a graphic to a PlotArea.""" @@ -570,7 +568,7 @@ def center_scene(self, *, zoom: float = 1.35): def auto_scale( self, *, # since this is often used as an event handler, don't want to coerce maintain_aspect = True - maintain_aspect: Union[None, bool] = None, + maintain_aspect: None | bool = None, zoom: float = 0.8, ): """ @@ -650,6 +648,7 @@ def delete_graphic(self, graphic: Graphic): """ # TODO: proper gc of selectors, RAM is freed for regular graphics but not selectors # TODO: references to selectors must be lingering somewhere + # TODO: update March 2024, I think selectors are gc properly, should check # get location loc = graphic.loc @@ -718,7 +717,7 @@ def __getitem__(self, name: str): f"The current selectors are:\n {selector_names}" ) - def __contains__(self, item: Union[str, Graphic]): + def __contains__(self, item: str | Graphic): to_check = [*self.graphics, *self.selectors, *self.legends] if isinstance(item, Graphic): diff --git a/fastplotlib/layouts/_record_mixin.py b/fastplotlib/layouts/_record_mixin.py index e3bfdeba5..59a8e92e4 100644 --- a/fastplotlib/layouts/_record_mixin.py +++ b/fastplotlib/layouts/_record_mixin.py @@ -1,4 +1,3 @@ -from typing import * from pathlib import Path from multiprocessing import Queue, Process from time import time @@ -21,7 +20,7 @@ class VideoWriterAV(Process): def __init__( self, - path: Union[Path, str], + path: Path | str, queue: Queue, fps: int, width: int, @@ -115,7 +114,7 @@ def _record(self): def record_start( self, - path: Union[str, Path], + path: str | Path, fps: int = 25, codec: str = "mpeg4", pixel_format: str = "yuv420p", diff --git a/fastplotlib/layouts/_subplot.py b/fastplotlib/layouts/_subplot.py index 509840fa7..4b1e92c51 100644 --- a/fastplotlib/layouts/_subplot.py +++ b/fastplotlib/layouts/_subplot.py @@ -1,4 +1,4 @@ -from typing import * +from typing import Literal, Union import numpy as np @@ -9,18 +9,22 @@ from ..graphics import TextGraphic from ._utils import make_canvas_and_renderer, create_camera, create_controller from ._plot_area import PlotArea -from .graphic_methods_mixin import GraphicMethodsMixin +from ._graphic_methods_mixin import GraphicMethodsMixin class Subplot(PlotArea, GraphicMethodsMixin): def __init__( self, - parent: Any = None, - position: Tuple[int, int] = None, - parent_dims: Tuple[int, int] = None, - camera: Union[str, pygfx.PerspectiveCamera] = "2d", - controller: Union[str, pygfx.Controller] = None, - canvas: Union[str, WgpuCanvasBase, pygfx.Texture] = None, + parent: Union["GridPlot", None] = None, + position: tuple[int, int] = None, + parent_dims: tuple[int, int] = None, + camera: Literal["2d", "3d"] | pygfx.PerspectiveCamera = "2d", + controller: ( + Literal["panzoom", "fly", "trackball", "orbit"] | pygfx.Controller + ) = None, + canvas: ( + Literal["glfw", "jupyter", "qt", "wx"] | WgpuCanvasBase | pygfx.Texture + ) = None, renderer: pygfx.WgpuRenderer = None, name: str = None, ): @@ -33,7 +37,7 @@ def __init__( Parameters ---------- - parent: Any + parent: 'GridPlot' | None parent GridPlot instance position: (int, int), optional @@ -51,7 +55,7 @@ def __init__( | if ``str``, must be one of: `"panzoom", "fly", "trackball", or "orbit"`. | also accepts a pygfx.Controller instance - canvas: one of "jupyter", "glfw", "qt", WgpuCanvas, or pygfx.Texture, optional + canvas: one of "jupyter", "glfw", "qt", "ex, a WgpuCanvas, or a pygfx.Texture, optional Provides surface on which a scene will be rendered. Can optionally provide a WgpuCanvas instance or a str to force the PlotArea to use a specific canvas from one of the following options: "jupyter", "glfw", "qt". Can also provide a pygfx Texture to render to. @@ -113,11 +117,11 @@ def __init__( self.set_title(self.name) @property - def name(self) -> Any: + def name(self) -> str: return self._name @name.setter - def name(self, name: Any): + def name(self, name: str): self._name = name self.set_title(name) @@ -136,7 +140,7 @@ def docks(self) -> dict: """ return self._docks - def set_title(self, text: Any): + def set_title(self, text: str): """Sets the plot title, stored as a ``TextGraphic`` in the "top" dock area""" if text is None: return diff --git a/fastplotlib/layouts/_utils.py b/fastplotlib/layouts/_utils.py index 5ee930b67..6994838d5 100644 --- a/fastplotlib/layouts/_utils.py +++ b/fastplotlib/layouts/_utils.py @@ -1,15 +1,14 @@ -from typing import * import importlib import pygfx -from pygfx import WgpuRenderer, Texture +from pygfx import WgpuRenderer, Texture, Renderer from wgpu.gui import WgpuCanvasBase from ..utils import gui def make_canvas_and_renderer( - canvas: Union[str, WgpuCanvasBase, Texture, None], renderer: [WgpuRenderer, None] + canvas: str | WgpuCanvasBase | Texture | None, renderer: Renderer | None ): """ Parses arguments and returns the appropriate canvas and renderer instances @@ -22,19 +21,23 @@ def make_canvas_and_renderer( m = importlib.import_module("wgpu.gui." + canvas) canvas = m.WgpuCanvas(max_fps=60) elif not isinstance(canvas, (WgpuCanvasBase, Texture)): - raise ValueError( + raise TypeError( f"canvas option must either be a valid WgpuCanvas implementation, a pygfx Texture" f" or a str with the wgpu gui backend name." ) if renderer is None: renderer = WgpuRenderer(canvas, pixel_ratio=2) + elif not isinstance(renderer, Renderer): + raise TypeError( + f"renderer option must be a pygfx.Renderer instance such as pygfx.WgpuRenderer" + ) return canvas, renderer def create_camera( - camera_type: Union[pygfx.PerspectiveCamera, str], + camera_type: pygfx.PerspectiveCamera | str, ) -> pygfx.PerspectiveCamera: if isinstance(camera_type, pygfx.PerspectiveCamera): return camera_type @@ -61,7 +64,7 @@ def create_camera( def create_controller( - controller_type: Union[pygfx.Controller, None, str], + controller_type: pygfx.Controller | None | str, camera: pygfx.PerspectiveCamera, ) -> pygfx.Controller: """ diff --git a/fastplotlib/legends/legend.py b/fastplotlib/legends/legend.py index 291c25ff3..be90004aa 100644 --- a/fastplotlib/legends/legend.py +++ b/fastplotlib/legends/legend.py @@ -1,6 +1,6 @@ from functools import partial from collections import OrderedDict -from typing import * +from typing import Iterable import numpy as np import pygfx @@ -31,7 +31,7 @@ def __init__( class LineLegendItem(LegendItem): def __init__( - self, parent, graphic: LineGraphic, label: str, position: Tuple[int, int] + self, parent, graphic: LineGraphic, label: str, position: tuple[int, int] ): """ @@ -142,7 +142,7 @@ class Legend(Graphic): def __init__( self, plot_area, - highlight_color: Union[str, tuple, np.ndarray] = "w", + highlight_color: str | tuple | np.ndarray = "w", max_rows: int = 5, *args, **kwargs, @@ -161,7 +161,7 @@ def __init__( maximum number of rows allowed in the legend """ - self._graphics: List[Graphic] = list() + self._graphics: list[Graphic] = list() # hex id of Graphic, i.e. graphic.loc are the keys self._items: OrderedDict[str:LegendItem] = OrderedDict() @@ -204,7 +204,7 @@ def __init__( self._row_counter = 0 self._col_counter = 0 - def graphics(self) -> Tuple[Graphic, ...]: + def graphics(self) -> tuple[Graphic, ...]: return tuple(self._graphics) def _check_label_unique(self, label): @@ -235,7 +235,7 @@ def add_graphic(self, graphic: Graphic, label: str = None): # get x position offset for this new column of LegendItems # start by getting the LegendItems in the previous column - prev_column_items: List[LegendItem] = list(self._items.values())[ + prev_column_items: list[LegendItem] = list(self._items.values())[ -self._max_rows : ] # x position of LegendItems in previous column diff --git a/fastplotlib/utils/functions.py b/fastplotlib/utils/functions.py index 8d1e8694f..da781b521 100644 --- a/fastplotlib/utils/functions.py +++ b/fastplotlib/utils/functions.py @@ -1,5 +1,4 @@ from collections import OrderedDict -from typing import * from pathlib import Path import numpy as np @@ -165,7 +164,7 @@ def make_colors_dict(labels: iter, cmap: str, **kwargs) -> OrderedDict: return OrderedDict(zip(labels, colors)) -def quick_min_max(data: np.ndarray) -> Tuple[float, float]: +def quick_min_max(data: np.ndarray) -> tuple[float, float]: """ Adapted from pyqtgraph.ImageView. Estimate the min/max values of *data* by subsampling. @@ -220,7 +219,7 @@ def make_pygfx_colors(colors, n_colors): return colors_array -def calculate_gridshape(n_subplots: int) -> Tuple[int, int]: +def calculate_gridshape(n_subplots: int) -> tuple[int, int]: """ Returns ``(n_rows, n_cols)`` from given number of subplots ``n_subplots`` """ @@ -240,7 +239,7 @@ 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, + cmap_values: np.ndarray | list[int | float] = None, ) -> np.ndarray: """ diff --git a/fastplotlib/widgets/histogram_lut.py b/fastplotlib/widgets/histogram_lut.py index 31f6ab8e9..43f2b48b3 100644 --- a/fastplotlib/widgets/histogram_lut.py +++ b/fastplotlib/widgets/histogram_lut.py @@ -1,4 +1,3 @@ -from typing import * import weakref import numpy as np @@ -112,7 +111,7 @@ def __init__( self.image_graphic.cmap.add_event_handler(self._image_cmap_handler) - def _get_vmin_vmax_str(self) -> Tuple[str, str]: + def _get_vmin_vmax_str(self) -> tuple[str, str]: if self.vmin < 0.001 or self.vmin > 99_999: vmin_str = f"{self.vmin:.2e}" else: diff --git a/fastplotlib/utils/generate_add_methods.py b/scripts/generate_add_graphic_methods.py similarity index 97% rename from fastplotlib/utils/generate_add_methods.py rename to scripts/generate_add_graphic_methods.py index 100ad7757..2a480d884 100644 --- a/fastplotlib/utils/generate_add_methods.py +++ b/scripts/generate_add_graphic_methods.py @@ -4,7 +4,7 @@ import black root = pathlib.Path(__file__).parent.parent.resolve() -filename = root.joinpath("layouts/graphic_methods_mixin.py") +filename = root.joinpath("fastplotlib", "layouts", "_graphic_methods_mixin.py") # if there is an existing mixin class, replace it with an empty class # so that fastplotlib will import diff --git a/setup.py b/setup.py index e8f2613d9..b50a6a9bf 100644 --- a/setup.py +++ b/setup.py @@ -77,7 +77,7 @@ license="Apache 2.0", author="Kushal Kolar, Caitlin Lewis", author_email="", - python_requires=">=3.9", + python_requires=">=3.10", install_requires=install_requires, extras_require=extras_require, include_package_data=True, From 5ec3fb29946bdff1ade94654b5ec37d034f1b0aa Mon Sep 17 00:00:00 2001 From: Kushal Kolar Date: Sun, 31 Mar 2024 05:35:08 -0400 Subject: [PATCH 40/66] revert setting pixel ratio (#466) --- fastplotlib/layouts/_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fastplotlib/layouts/_utils.py b/fastplotlib/layouts/_utils.py index 6994838d5..85c35532c 100644 --- a/fastplotlib/layouts/_utils.py +++ b/fastplotlib/layouts/_utils.py @@ -27,7 +27,7 @@ def make_canvas_and_renderer( ) if renderer is None: - renderer = WgpuRenderer(canvas, pixel_ratio=2) + renderer = WgpuRenderer(canvas) elif not isinstance(renderer, Renderer): raise TypeError( f"renderer option must be a pygfx.Renderer instance such as pygfx.WgpuRenderer" From bc2de99d8af5d9639488e39cea0de04162378783 Mon Sep 17 00:00:00 2001 From: Kushal Kolar Date: Mon, 1 Apr 2024 23:59:04 -0400 Subject: [PATCH 41/66] add `References` object to `PlotArea`, other cleanup, better garbage collection (#467) * add References object to PlotArea, other cleanup * more cleanup * black * add refcount utility to References, fix Graphic.name setter bug * actually fix Graphic.name setter bug * more generalized event handler cleanup in Graphic * all gc works now * black --- examples/notebooks/test_gc.ipynb | 200 +++++++++++++++ fastplotlib/graphics/_base.py | 78 ++++-- fastplotlib/graphics/line.py | 2 +- fastplotlib/graphics/line_collection.py | 3 +- .../graphics/selectors/_base_selector.py | 39 +-- fastplotlib/graphics/selectors/_linear.py | 30 +-- .../graphics/selectors/_linear_region.py | 7 +- fastplotlib/graphics/selectors/_polygon.py | 2 +- fastplotlib/layouts/_plot_area.py | 240 +++++++++--------- fastplotlib/legends/legend.py | 12 +- fastplotlib/widgets/histogram_lut.py | 67 ++--- fastplotlib/widgets/image.py | 14 +- 12 files changed, 471 insertions(+), 223 deletions(-) create mode 100644 examples/notebooks/test_gc.ipynb diff --git a/examples/notebooks/test_gc.ipynb b/examples/notebooks/test_gc.ipynb new file mode 100644 index 000000000..6caf6a9e3 --- /dev/null +++ b/examples/notebooks/test_gc.ipynb @@ -0,0 +1,200 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "id": "9dfba6cf-38af-4003-90b9-463c0cb1063f", + "metadata": {}, + "outputs": [], + "source": [ + "import fastplotlib as fpl\n", + "import numpy as np\n", + "import pytest" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7552eedc-3b9b-4682-8e3b-7d44e0e5510d", + "metadata": {}, + "outputs": [], + "source": [ + "def test_references(plot_objects):\n", + " for i in range(len(plot_objects)):\n", + " with pytest.raises(ReferenceError) as failure:\n", + " plot_objects[i]\n", + " pytest.fail(f\"GC failed for object: {objects[i]}\")" + ] + }, + { + "cell_type": "markdown", + "id": "948108e8-a4fa-4dc7-9953-a956428128cf", + "metadata": {}, + "source": [ + "# Add graphics and selectors, add feature event handlers, test gc occurs" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3d96bf14-b484-455e-bcd7-5b2fe7b45fb4", + "metadata": {}, + "outputs": [], + "source": [ + "xs = np.linspace(0, 20 * np.pi, 1_000)\n", + "ys = np.sin(xs)\n", + "zs = np.zeros(xs.size)\n", + "\n", + "points_data = np.column_stack([xs, ys, zs])\n", + "\n", + "line_collection_data = [points_data[:, 1].copy() for i in range(10)]\n", + "\n", + "img_data = np.random.rand(2_000, 2_000)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "284b83e1-8cfc-4105-b7c2-6214137dab31", + "metadata": {}, + "outputs": [], + "source": [ + "gp = fpl.GridPlot((2, 2))\n", + "\n", + "line = gp[0, 0].add_line(points_data, name=\"line\")\n", + "scatter = gp[0, 1].add_scatter(points_data.copy(), name=\"scatter\")\n", + "line_stack = gp[1, 0].add_line_stack(line_collection_data, name=\"line-stack\")\n", + "image = gp[1, 1].add_image(img_data, name=\"image\")\n", + "\n", + "linear_sel = line.add_linear_selector(name=\"line_linear_sel\")\n", + "linear_region_sel = line.add_linear_region_selector(name=\"line_region_sel\")\n", + "\n", + "linear_sel2 = line_stack.add_linear_selector(name=\"line-stack_linear_sel\")\n", + "linear_region_sel2 = line_stack.add_linear_region_selector(name=\"line-stack_region_sel\")\n", + "\n", + "linear_sel_img = image.add_linear_selector(name=\"image_linear_sel\")\n", + "linear_region_sel_img = image.add_linear_region_selector(name=\"image_linear_region_sel\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "bb2083c1-f6b7-417c-86b8-9980819917db", + "metadata": {}, + "outputs": [], + "source": [ + "def feature_changed_handler(ev):\n", + " pass\n", + "\n", + "\n", + "objects = list()\n", + "for subplot in gp:\n", + " objects += subplot.objects\n", + "\n", + "\n", + "for g in objects:\n", + " for feature in g.feature_events:\n", + " if isinstance(g, fpl.LineCollection):\n", + " continue # skip collections for now\n", + " \n", + " f = getattr(g, feature)\n", + " f.add_event_handler(feature_changed_handler)\n", + "\n", + "gp.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ba9fffeb-45bd-4a0c-a941-e7c7e68f2e55", + "metadata": {}, + "outputs": [], + "source": [ + "gp.clear()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e33bf32d-b13a-474b-92ca-1d1e1c7b820b", + "metadata": {}, + "outputs": [], + "source": [ + "test_references(objects)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8078a7d2-9bc6-48a1-896c-7e169c5bbdcf", + "metadata": {}, + "outputs": [], + "source": [ + "movies = [np.random.rand(100, 100, 100) for i in range(6)]\n", + "\n", + "iw = fpl.ImageWidget(movies)\n", + "\n", + "# add some events onto all the image graphics\n", + "for g in iw.managed_graphics:\n", + " for f in g.feature_events:\n", + " fea = getattr(g, f)\n", + " fea.add_event_handler(feature_changed_handler)\n", + "\n", + "iw.show()" + ] + }, + { + "cell_type": "markdown", + "id": "189bcd7a-40a2-4e84-abcf-c334e50f5544", + "metadata": {}, + "source": [ + "# Test that setting new data with different dims clears old ImageGraphics" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "38557b63-997f-433a-b744-e562e30be6ae", + "metadata": {}, + "outputs": [], + "source": [ + "old_graphics = iw.managed_graphics\n", + "\n", + "new_movies = [np.random.rand(100, 200, 200) for i in range(6)]\n", + "\n", + "iw.set_data(new_movies)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "59e3c193-5672-4a66-bdca-12f1dd675d32", + "metadata": {}, + "outputs": [], + "source": [ + "test_references(old_graphics)" + ] + } + ], + "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/fastplotlib/graphics/_base.py b/fastplotlib/graphics/_base.py index 4442c851e..3a5b043f5 100644 --- a/fastplotlib/graphics/_base.py +++ b/fastplotlib/graphics/_base.py @@ -1,4 +1,4 @@ -from typing import Any, Literal +from typing import Any, Literal, TypeAlias import weakref from warnings import warn from abc import ABC, abstractmethod @@ -11,9 +11,12 @@ from ._features import GraphicFeature, PresentFeature, GraphicFeatureIndexable, Deleted + +HexStr: TypeAlias = str + # dict that holds all world objects for a given python kernel/session # Graphic objects only use proxies to WorldObjects -WORLD_OBJECTS: dict[str, WorldObject] = dict() #: {hex id str: WorldObject} +WORLD_OBJECTS: dict[HexStr, WorldObject] = dict() #: {hex id str: WorldObject} PYGFX_EVENTS = [ @@ -80,7 +83,7 @@ def __init__( self.present = PresentFeature(parent=self) # store hex id str of Graphic instance mem location - self.loc: str = hex(id(self)) + self._fpl_address: HexStr = hex(id(self)) self.deleted = Deleted(self, False) @@ -93,19 +96,25 @@ def name(self) -> str | None: @name.setter def name(self, name: str): + if self.name == name: + return + if not isinstance(name, str): raise TypeError("`Graphic` name must be of type ") + if self._plot_area is not None: self._plot_area._check_graphic_name_exists(name) + self._name = name + @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))]) + return weakref.proxy(WORLD_OBJECTS[self._fpl_address]) def _set_world_object(self, wo: WorldObject): - WORLD_OBJECTS[hex(id(self))] = wo + WORLD_OBJECTS[self._fpl_address] = wo @property def position(self) -> np.ndarray: @@ -166,6 +175,9 @@ def children(self) -> list[WorldObject]: """Return the children of the WorldObject.""" return self.world_object.children + def _fpl_add_plot_area_hook(self, plot_area): + self._plot_area = plot_area + def __setattr__(self, key, value): if hasattr(self, key): attr = getattr(self, key) @@ -187,23 +199,52 @@ def __eq__(self, other): if not isinstance(other, Graphic): raise TypeError("`==` operator is only valid between two Graphics") - if self.loc == other.loc: + if self._fpl_address == other._fpl_address: return True return False - def _cleanup(self): + def _fpl_cleanup(self): """ Cleans up the graphic in preparation for __del__(), such as removing event handlers from plot renderer, feature event handlers, etc. Optionally implemented in subclasses """ - pass + # clear any attached event handlers and animation functions + for attr in dir(self): + try: + method = getattr(self, attr) + except: + continue + + if not callable(method): + continue + + for ev_type in PYGFX_EVENTS: + try: + self._plot_area.renderer.remove_event_handler(method, ev_type) + except (KeyError, TypeError): + pass + + try: + self._plot_area.remove_animation(method) + except KeyError: + pass + + for child in self.world_object.children: + child._event_handlers.clear() + + self.world_object._event_handlers.clear() + + feature_names = getattr(self, "feature_events") + for n in feature_names: + fea = getattr(self, n) + fea.clear_event_handlers() def __del__(self): self.deleted = True - del WORLD_OBJECTS[self.loc] + del WORLD_OBJECTS[self._fpl_address] def rotate(self, alpha: float, axis: Literal["x", "y", "z"] = "y"): """Rotate the Graphic with respect to the world. @@ -372,7 +413,7 @@ def _event_handler(self, event): else: # get index of world object that made this event for i, item in enumerate(self.graphics): - wo = WORLD_OBJECTS[item.loc] + wo = WORLD_OBJECTS[item._fpl_address] # we only store hex id of worldobject, but worldobject `pick_info` is always the real object # so if pygfx worldobject triggers an event by itself, such as `click`, etc., this will be # the real world object in the pick_info and not the proxy @@ -432,7 +473,8 @@ class PreviouslyModifiedData: indices: Any -COLLECTION_GRAPHICS: dict[str, Graphic] = dict() +# Dict that holds all collection graphics in one python instance +COLLECTION_GRAPHICS: dict[HexStr, Graphic] = dict() class GraphicCollection(Graphic): @@ -450,7 +492,7 @@ 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 + weakref.proxy(COLLECTION_GRAPHICS[addr]) for addr in self._graphics ] self._graphics_array = np.array(proxies) self._graphics_array.flags["WRITEABLE"] = False @@ -479,10 +521,10 @@ def add_graphic(self, graphic: Graphic, reset_index: False): f"you are trying to add a {graphic.__class__.__name__}." ) - loc = hex(id(graphic)) - COLLECTION_GRAPHICS[loc] = graphic + addr = graphic._fpl_address + COLLECTION_GRAPHICS[addr] = graphic - self._graphics.append(loc) + self._graphics.append(addr) if reset_index: self._reset_index() @@ -507,7 +549,7 @@ def remove_graphic(self, graphic: Graphic, reset_index: True): """ - self._graphics.remove(graphic.loc) + self._graphics.remove(graphic._fpl_address) if reset_index: self._reset_index() @@ -525,8 +567,8 @@ def __getitem__(self, key): def __del__(self): self.world_object.clear() - for loc in self._graphics: - del COLLECTION_GRAPHICS[loc] + for addr in self._graphics: + del COLLECTION_GRAPHICS[addr] super().__del__() diff --git a/fastplotlib/graphics/line.py b/fastplotlib/graphics/line.py index f44347a58..cfb697dff 100644 --- a/fastplotlib/graphics/line.py +++ b/fastplotlib/graphics/line.py @@ -278,7 +278,7 @@ def _get_linear_selector_init_args(self, padding: float, **kwargs): return bounds_init, limits, size, origin, axis, end_points - def _add_plot_area_hook(self, plot_area): + def _fpl_add_plot_area_hook(self, plot_area): self._plot_area = plot_area def set_feature(self, feature: str, new_data: Any, indices: Any = None): diff --git a/fastplotlib/graphics/line_collection.py b/fastplotlib/graphics/line_collection.py index 8488ec15e..1c2e151e8 100644 --- a/fastplotlib/graphics/line_collection.py +++ b/fastplotlib/graphics/line_collection.py @@ -15,7 +15,6 @@ class LineCollection(GraphicCollection, Interaction): child_type = LineGraphic.__name__ - feature_events = {"data", "colors", "cmap", "thickness", "present"} def __init__( self, @@ -416,7 +415,7 @@ def _get_linear_selector_init_args(self, padding, **kwargs): return bounds, limits, size, origin, axis, end_points - def _add_plot_area_hook(self, plot_area): + def _fpl_add_plot_area_hook(self, plot_area): self._plot_area = plot_area def set_feature(self, feature: str, new_data: Any, indices: Any): diff --git a/fastplotlib/graphics/selectors/_base_selector.py b/fastplotlib/graphics/selectors/_base_selector.py index 6c1f8c6ae..f20eba4a0 100644 --- a/fastplotlib/graphics/selectors/_base_selector.py +++ b/fastplotlib/graphics/selectors/_base_selector.py @@ -1,6 +1,7 @@ from typing import * from dataclasses import dataclass from functools import partial +import weakref import numpy as np @@ -123,7 +124,7 @@ def _get_source(self, graphic): return source - def _add_plot_area_hook(self, plot_area): + def _fpl_add_plot_area_hook(self, plot_area): self._plot_area = plot_area # when the pointer is pressed on a fill, edge or vertex @@ -136,8 +137,10 @@ def _add_plot_area_hook(self, plot_area): for fill in self._fill: if fill.material.color_is_transparent: - pfunc_fill = partial(self._check_fill_pointer_event, fill) - self._plot_area.renderer.add_event_handler(pfunc_fill, "pointer_down") + self._pfunc_fill = partial(self._check_fill_pointer_event, fill) + self._plot_area.renderer.add_event_handler( + self._pfunc_fill, "pointer_down" + ) # when the pointer moves self._plot_area.renderer.add_event_handler(self._move, "pointer_move") @@ -356,26 +359,10 @@ def _key_up(self, ev): self._move_info = None - def _cleanup(self): - """ - Cleanup plot renderer event handlers etc. - """ - self._plot_area.renderer.remove_event_handler(self._move, "pointer_move") - self._plot_area.renderer.remove_event_handler(self._move_end, "pointer_up") - self._plot_area.renderer.remove_event_handler(self._move_to_pointer, "click") - - self._plot_area.renderer.remove_event_handler(self._key_down, "key_down") - self._plot_area.renderer.remove_event_handler(self._key_up, "key_up") - - # remove animation func - self._plot_area.remove_animation(self._key_hold) - - # clear wo event handlers - for wo in self._world_objects: - wo._event_handlers.clear() - - if hasattr(self, "feature_events"): - feature_names = getattr(self, "feature_events") - for n in feature_names: - fea = getattr(self, n) - fea.clear_event_handlers() + def _fpl_cleanup(self): + if hasattr(self, "_pfunc_fill"): + self._plot_area.renderer.remove_event_handler( + self._pfunc_fill, "pointer_down" + ) + del self._pfunc_fill + super()._fpl_cleanup() diff --git a/fastplotlib/graphics/selectors/_linear.py b/fastplotlib/graphics/selectors/_linear.py index 886ccbaaf..4b77a6cd9 100644 --- a/fastplotlib/graphics/selectors/_linear.py +++ b/fastplotlib/graphics/selectors/_linear.py @@ -140,18 +140,6 @@ def __init__( world_object.add(self.line_outer) world_object.add(line_inner) - self._set_world_object(world_object) - - # set x or y position - if axis == "x": - self.position_x = selection - else: - self.position_y = selection - - self.selection = LinearSelectionFeature( - self, axis=axis, value=selection, limits=self._limits - ) - self._move_info: dict = None self.parent = parent @@ -170,6 +158,14 @@ def __init__( name=name, ) + self._set_world_object(world_object) + + self.selection = LinearSelectionFeature( + self, axis=axis, value=selection, limits=self._limits + ) + + self.selection = selection + def _setup_ipywidget_slider(self, widget): # setup an ipywidget slider with bidirectional callbacks to this LinearSelector value = self.selection() @@ -208,8 +204,8 @@ def _ipywidget_callback(self, change): self.selection = change["new"] - def _add_plot_area_hook(self, plot_area): - super()._add_plot_area_hook(plot_area=plot_area) + def _fpl_add_plot_area_hook(self, plot_area): + super()._fpl_add_plot_area_hook(plot_area=plot_area) # resize the slider widgets when the canvas is resized self._plot_area.renderer.add_event_handler(self._set_slider_layout, "resize") @@ -375,10 +371,8 @@ def _move_graphic(self, delta: np.ndarray): else: self.selection = self.selection() + delta[1] - def _cleanup(self): - super()._cleanup() - + def _fpl_cleanup(self): for widget in self._handled_widgets: widget.unobserve(self._ipywidget_callback, "value") - self._plot_area.renderer.remove_event_handler(self._set_slider_layout, "resize") + super()._fpl_cleanup() diff --git a/fastplotlib/graphics/selectors/_linear_region.py b/fastplotlib/graphics/selectors/_linear_region.py index b88174ddb..47191bfb1 100644 --- a/fastplotlib/graphics/selectors/_linear_region.py +++ b/fastplotlib/graphics/selectors/_linear_region.py @@ -135,7 +135,6 @@ def __init__( # basic mesh for the fill area of the selector # line for each edge of the selector group = pygfx.Group() - self._set_world_object(group) if axis == "x": mesh = pygfx.Mesh( @@ -155,7 +154,7 @@ def __init__( self.fill = mesh self.fill.world.position = (*origin, -2) - self.world_object.add(self.fill) + group.add(self.fill) self._resizable = resizable @@ -223,7 +222,7 @@ def __init__( # add the edge lines for edge in self.edges: edge.world.z = -1 - self.world_object.add(edge) + group.add(edge) # set the initial bounds of the selector self.selection = LinearRegionSelectionFeature( @@ -244,6 +243,8 @@ def __init__( name=name, ) + self._set_world_object(group) + def get_selected_data( self, graphic: Graphic = None ) -> Union[np.ndarray, List[np.ndarray], None]: diff --git a/fastplotlib/graphics/selectors/_polygon.py b/fastplotlib/graphics/selectors/_polygon.py index 44d378329..3d2ee98fd 100644 --- a/fastplotlib/graphics/selectors/_polygon.py +++ b/fastplotlib/graphics/selectors/_polygon.py @@ -39,7 +39,7 @@ def get_vertices(self) -> np.ndarray: return np.vstack(vertices) - def _add_plot_area_hook(self, plot_area): + def _fpl_add_plot_area_hook(self, plot_area): self._plot_area = plot_area # click to add new segment diff --git a/fastplotlib/layouts/_plot_area.py b/fastplotlib/layouts/_plot_area.py index 299bc6e5d..37a25bbcc 100644 --- a/fastplotlib/layouts/_plot_area.py +++ b/fastplotlib/layouts/_plot_area.py @@ -1,4 +1,5 @@ from inspect import getfullargspec +from sys import getrefcount from typing import TypeAlias, Literal, Union import weakref from warnings import warn @@ -14,15 +15,81 @@ from ..graphics.selectors._base_selector import BaseSelector from ..legends import Legend -# dict to store Graphic instances -# this is the only place where the real references to Graphics are stored in a Python session -# {hex id str: Graphic} + HexStr: TypeAlias = str -GRAPHICS: dict[HexStr, Graphic] = dict() -SELECTORS: dict[HexStr, BaseSelector] = dict() + + +class References: + """ + This is the only place where the real graphic objects are stored. Everywhere else gets a proxy. + """ + + _graphics: dict[HexStr, Graphic] = dict() + _selectors: dict[HexStr, BaseSelector] = dict() + _legends: dict[HexStr, Legend] = dict() + + def add(self, graphic: Graphic | BaseSelector | Legend): + """Adds the real graphic to the dict""" + addr = graphic._fpl_address + + if isinstance(graphic, BaseSelector): + self._selectors[addr] = graphic + + elif isinstance(graphic, Legend): + self._legends[addr] = graphic + + elif isinstance(graphic, Graphic): + self._graphics[addr] = graphic + + else: + raise TypeError("Can only add Graphic, Selector or Legend types") + + def remove(self, address): + if address in self._graphics.keys(): + del self._graphics[address] + elif address in self._selectors.keys(): + del self._selectors[address] + elif address in self._legends.keys(): + del self._legends[address] + else: + raise KeyError(f"graphic with address not found: {address}") + + def get_proxies(self, refs: list[HexStr]) -> tuple[weakref.proxy]: + proxies = list() + for key in refs: + if key in self._graphics.keys(): + proxies.append(weakref.proxy(self._graphics[key])) + + elif key in self._selectors.keys(): + proxies.append(weakref.proxy(self._selectors[key])) + + elif key in self._legends.keys(): + proxies.append(weakref.proxy(self._legends[key])) + + else: + raise KeyError(f"graphic object with address not found: {key}") + + return tuple(proxies) + + def get_refcounts(self) -> dict[HexStr:int]: + counts = dict() + + for item in (self._graphics, self._selectors, self._legends): + for k in item.keys(): + counts[(k, item[k].name, item[k].__class__.__name__)] = getrefcount( + item[k] + ) + + return counts + + +REFERENCES = References() class PlotArea: + def get_refcounts(self): + return REFERENCES.get_refcounts() + def __init__( self, parent: Union["PlotArea", "GridPlot"], @@ -89,12 +156,15 @@ def __init__( self.renderer.add_event_handler(self.set_viewport_rect, "resize") # list of hex id strings for all graphics managed by this PlotArea - # the real Graphic instances are stored in the ``GRAPHICS`` dict - self._graphics: list[str] = list() + # the real Graphic instances are managed by REFERENCES + self._graphics: list[HexStr] = list() # selectors are in their own list so they can be excluded from scene bbox calculations # managed similar to GRAPHICS for garbage collection etc. - self._selectors: list[str] = list() + self._selectors: list[HexStr] = list() + + # legends, managed just like other graphics as explained above + self._legends: list[HexStr] = list() self._name = name @@ -206,35 +276,21 @@ def controller(self, new_controller: str | pygfx.Controller): @property def graphics(self) -> tuple[Graphic, ...]: """Graphics in the plot area. Always returns a proxy to the Graphic instances.""" - proxies = list() - for loc in self._graphics: - p = weakref.proxy(GRAPHICS[loc]) - if p.__class__.__name__ == "Legend": - continue - proxies.append(p) - - return tuple(proxies) + return REFERENCES.get_proxies(self._graphics) @property def selectors(self) -> tuple[BaseSelector, ...]: """Selectors in the plot area. Always returns a proxy to the Graphic instances.""" - proxies = list() - for loc in self._selectors: - p = weakref.proxy(SELECTORS[loc]) - proxies.append(p) - - return tuple(proxies) + return REFERENCES.get_proxies(self._selectors) @property def legends(self) -> tuple[Legend, ...]: """Legends in the plot area.""" - proxies = list() - for loc in self._graphics: - p = weakref.proxy(GRAPHICS[loc]) - if p.__class__.__name__ == "Legend": - proxies.append(p) + return REFERENCES.get_proxies(self._legends) - return tuple(proxies) + @property + def objects(self) -> tuple[Graphic | BaseSelector | Legend, ...]: + return *self.graphics, *self.selectors, *self.legends @property def name(self) -> str: @@ -470,28 +526,28 @@ def _add_or_insert_graphic( if graphic.name is not None: # skip for those that have no name self._check_graphic_name_exists(graphic.name) + addr = graphic._fpl_address + if isinstance(graphic, BaseSelector): - # store in SELECTORS dict - loc = graphic.loc - 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) - else: - self._selectors.append(loc) + addr_list = self._selectors + + elif isinstance(graphic, Legend): + addr_list = self._legends + + elif isinstance(graphic, Graphic): + addr_list = self._graphics + else: - # store in GRAPHICS dict - loc = graphic.loc - GRAPHICS[loc] = ( - graphic # add hex id string for referencing this graphic instance - ) + raise TypeError("graphic must be of type Graphic | BaseSelector | Legend") - if action == "insert": - self._graphics.insert(index, loc) - else: - self._graphics.append(loc) + if action == "insert": + addr_list.insert(index, addr) + elif action == "add": + addr_list.append(addr) + else: + raise ValueError("valid actions are 'insert' | 'add'") + + REFERENCES.add(graphic) # now that it's in the dict, just use the weakref graphic = weakref.proxy(graphic) @@ -503,24 +559,13 @@ def _add_or_insert_graphic( self.center_graphic(graphic) # if we don't use the weakref above, then the object lingers if a plot hook is used! - if hasattr(graphic, "_add_plot_area_hook"): - graphic._add_plot_area_hook(self) + graphic._fpl_add_plot_area_hook(self) def _check_graphic_name_exists(self, name): - graphic_names = list() - - for g in self.graphics: - graphic_names.append(g.name) - - for s in self.selectors: - graphic_names.append(s.name) - - for l in self.legends: - graphic_names.append(l.name) - - if name in graphic_names: + if name in self: raise ValueError( - f"graphics must have unique names, current graphic names are:\n {graphic_names}" + f"Graphic with given name already exists in subplot or plot area. " + f"All graphics within a subplot or plot area must have a unique name." ) def center_graphic(self, graphic: Graphic, zoom: float = 1.35): @@ -649,85 +694,50 @@ def delete_graphic(self, graphic: Graphic): # TODO: proper gc of selectors, RAM is freed for regular graphics but not selectors # TODO: references to selectors must be lingering somewhere # TODO: update March 2024, I think selectors are gc properly, should check - # get location - loc = graphic.loc - - # check which dict it's in - if loc in self._graphics: - glist = self._graphics - kind = "graphic" - elif loc in self._selectors: - kind = "selector" - glist = self._selectors - else: - raise KeyError( - f"Graphic with following address not found in plot area: {loc}" - ) + # get memory address + address = graphic._fpl_address + + if graphic not in self: + raise KeyError(f"Graphic not found in plot area: {graphic}") + + # check which type it is + for l in [self._graphics, self._selectors, self._legends]: + if address in l: + l.remove(address) + break # remove from scene if necessary if graphic.world_object in self.scene.children: self.scene.remove(graphic.world_object) - # remove from list of addresses - glist.remove(loc) - # cleanup - graphic._cleanup() + graphic._fpl_cleanup() - if kind == "graphic": - del GRAPHICS[loc] - elif kind == "selector": - del SELECTORS[loc] + REFERENCES.remove(address) def clear(self): """ Clear the Plot or Subplot. Also performs garbage collection, i.e. runs ``delete_graphic`` on all graphics. """ - - for g in self.graphics: + for g in self.objects: self.delete_graphic(g) - for s in self.selectors: - self.delete_graphic(s) - def __getitem__(self, name: str): - for graphic in self.graphics: + for graphic in self.objects: if graphic.name == name: return graphic - for selector in self.selectors: - if selector.name == name: - return selector - - for legend in self.legends: - if legend.name == name: - return legend - - graphic_names = list() - for g in self.graphics: - graphic_names.append(g.name) - - selector_names = list() - for s in self.selectors: - 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}" - ) + raise IndexError(f"No graphic or selector of given name in plot area.\n") def __contains__(self, item: str | Graphic): - to_check = [*self.graphics, *self.selectors, *self.legends] - if isinstance(item, Graphic): - if item in to_check: + if item in self.objects: return True else: return False elif isinstance(item, str): - for graphic in to_check: + for graphic in self.objects: # only check named graphics if graphic.name is None: continue diff --git a/fastplotlib/legends/legend.py b/fastplotlib/legends/legend.py index be90004aa..b7e55f321 100644 --- a/fastplotlib/legends/legend.py +++ b/fastplotlib/legends/legend.py @@ -163,7 +163,7 @@ def __init__( """ self._graphics: list[Graphic] = list() - # hex id of Graphic, i.e. graphic.loc are the keys + # hex id of Graphic, i.e. graphic._fpl_address are the keys self._items: OrderedDict[str:LegendItem] = OrderedDict() super().__init__(*args, **kwargs) @@ -218,7 +218,7 @@ def _check_label_unique(self, label): def add_graphic(self, graphic: Graphic, label: str = None): if graphic in self._graphics: raise KeyError( - f"Graphic already exists in legend with label: '{self._items[graphic.loc].label}'" + f"Graphic already exists in legend with label: '{self._items[graphic._fpl_address].label}'" ) self._check_label_unique(label) @@ -268,7 +268,7 @@ def add_graphic(self, graphic: Graphic, label: str = None): self._reset_mesh_dims() self._graphics.append(graphic) - self._items[graphic.loc] = legend_item + self._items[graphic._fpl_address] = legend_item graphic.deleted.add_event_handler(partial(self.remove_graphic, graphic)) @@ -288,7 +288,7 @@ def _reset_mesh_dims(self): def remove_graphic(self, graphic: Graphic): self._graphics.remove(graphic) - legend_item = self._items.pop(graphic.loc) + legend_item = self._items.pop(graphic._fpl_address) self._legend_items_group.remove(legend_item.world_object) self._reset_item_positions() @@ -350,7 +350,7 @@ def __getitem__(self, graphic: Graphic) -> LegendItem: if not isinstance(graphic, Graphic): raise TypeError("Must index Legend with Graphics") - if graphic.loc not in self._items.keys(): + if graphic._fpl_address not in self._items.keys(): raise KeyError("Graphic not in legend") - return self._items[graphic.loc] + return self._items[graphic._fpl_address] diff --git a/fastplotlib/widgets/histogram_lut.py b/fastplotlib/widgets/histogram_lut.py index 43f2b48b3..67af972b8 100644 --- a/fastplotlib/widgets/histogram_lut.py +++ b/fastplotlib/widgets/histogram_lut.py @@ -44,14 +44,14 @@ def __init__( line_data = np.column_stack([hist_scaled, edges_flanked]) - self.line = LineGraphic(line_data) + self._histogram_line = LineGraphic(line_data) bounds = (edges[0], edges[-1]) limits = (edges_flanked[0], edges_flanked[-1]) size = 120 # since it's scaled to 100 origin = (hist_scaled.max() / 2, 0) - self.linear_region = LinearRegionSelector( + self._linear_region_selector = LinearRegionSelector( bounds=bounds, limits=limits, size=size, @@ -61,7 +61,7 @@ def __init__( ) # there will be a small difference with the histogram edges so this makes them both line up exactly - self.linear_region.selection = ( + self._linear_region_selector.selection = ( image_graphic.cmap.vmin, image_graphic.cmap.vmax, ) @@ -91,8 +91,8 @@ def __init__( widget_wo = Group() widget_wo.add( - self.line.world_object, - self.linear_region.world_object, + self._histogram_line.world_object, + self._linear_region_selector.world_object, self._text_vmin.world_object, self._text_vmax.world_object, ) @@ -102,12 +102,14 @@ def __init__( self.world_object.local.scale_x *= -1 self._text_vmin.position_x = -120 - self._text_vmin.position_y = self.linear_region.selection()[0] + self._text_vmin.position_y = self._linear_region_selector.selection()[0] self._text_vmax.position_x = -120 - self._text_vmax.position_y = self.linear_region.selection()[1] + self._text_vmax.position_y = self._linear_region_selector.selection()[1] - self.linear_region.selection.add_event_handler(self._linear_region_handler) + self._linear_region_selector.selection.add_event_handler( + self._linear_region_handler + ) self.image_graphic.cmap.add_event_handler(self._image_cmap_handler) @@ -124,10 +126,10 @@ def _get_vmin_vmax_str(self) -> tuple[str, str]: return vmin_str, vmax_str - def _add_plot_area_hook(self, plot_area): + def _fpl_add_plot_area_hook(self, plot_area): self._plot_area = plot_area - self.linear_region._add_plot_area_hook(plot_area) - self.line._add_plot_area_hook(plot_area) + self._linear_region_selector._fpl_add_plot_area_hook(plot_area) + self._histogram_line._fpl_add_plot_area_hook(plot_area) self._plot_area.auto_scale() @@ -192,7 +194,7 @@ def _calculate_histogram(self, data): def _linear_region_handler(self, ev): # must use world coordinate values directly from selection() # otherwise the linear region bounds jump to the closest bin edges - vmin, vmax = self.linear_region.selection() + vmin, vmax = self._linear_region_selector.selection() vmin, vmax = vmin / self._scale_factor, vmax / self._scale_factor self.vmin, self.vmax = vmin, vmax @@ -201,7 +203,7 @@ def _image_cmap_handler(self, ev): def _block_events(self, b: bool): self.image_graphic.cmap.block_events(b) - self.linear_region.selection.block_events(b) + self._linear_region_selector.selection.block_events(b) @property def vmin(self) -> float: @@ -213,9 +215,9 @@ def vmin(self, value: float): # must use world coordinate values directly from selection() # otherwise the linear region bounds jump to the closest bin edges - self.linear_region.selection = ( + self._linear_region_selector.selection = ( value * self._scale_factor, - self.linear_region.selection()[1], + self._linear_region_selector.selection()[1], ) self.image_graphic.cmap.vmin = value @@ -224,7 +226,7 @@ def vmin(self, value: float): self._vmin = value vmin_str, vmax_str = self._get_vmin_vmax_str() - self._text_vmin.position_y = self.linear_region.selection()[0] + self._text_vmin.position_y = self._linear_region_selector.selection()[0] self._text_vmin.text = vmin_str @property @@ -237,8 +239,8 @@ def vmax(self, value: float): # must use world coordinate values directly from selection() # otherwise the linear region bounds jump to the closest bin edges - self.linear_region.selection = ( - self.linear_region.selection()[0], + self._linear_region_selector.selection = ( + self._linear_region_selector.selection()[0], value * self._scale_factor, ) self.image_graphic.cmap.vmax = value @@ -248,7 +250,7 @@ def vmax(self, value: float): self._vmax = value vmin_str, vmax_str = self._get_vmin_vmax_str() - self._text_vmax.position_y = self.linear_region.selection()[1] + self._text_vmax.position_y = self._linear_region_selector.selection()[1] self._text_vmax.text = vmax_str def set_data(self, data, reset_vmin_vmax: bool = True): @@ -256,7 +258,7 @@ def set_data(self, data, reset_vmin_vmax: bool = True): line_data = np.column_stack([hist_scaled, edges_flanked]) - self.line.data = line_data + self._histogram_line.data = line_data bounds = (edges[0], edges[-1]) limits = (edges_flanked[0], edges_flanked[-11]) @@ -265,12 +267,12 @@ def set_data(self, data, reset_vmin_vmax: bool = True): if reset_vmin_vmax: # reset according to the new data - self.linear_region.limits = limits - self.linear_region.selection = bounds + self._linear_region_selector.limits = limits + self._linear_region_selector.selection = bounds else: # don't change the current selection self._block_events(True) - self.linear_region.limits = limits + self._linear_region_selector.limits = limits self._block_events(False) self._data = weakref.proxy(data) @@ -289,14 +291,21 @@ def image_graphic(self, graphic): f"HistogramLUT can only use ImageGraphic types, you have passed: {type(graphic)}" ) - # cleanup events from current image graphic - self._image_graphic.cmap.remove_event_handler(self._image_cmap_handler) + if self._image_graphic is not None: + # cleanup events from current image graphic + self._image_graphic.cmap.remove_event_handler(self._image_cmap_handler) self._image_graphic = graphic self.image_graphic.cmap.add_event_handler(self._image_cmap_handler) - def _cleanup(self): - self.linear_region._cleanup() - del self.line - del self.linear_region + def disconnect_image_graphic(self): + self._image_graphic.cmap.remove_event_handler(self._image_cmap_handler) + del self._image_graphic + # self._image_graphic = None + + def _fpl_cleanup(self): + self._linear_region_selector._fpl_cleanup() + self._histogram_line._fpl_cleanup() + del self._histogram_line + del self._linear_region_selector diff --git a/fastplotlib/widgets/image.py b/fastplotlib/widgets/image.py index 9412f7cc5..acef26a7d 100644 --- a/fastplotlib/widgets/image.py +++ b/fastplotlib/widgets/image.py @@ -875,17 +875,23 @@ def set_data( self._data[i] = new_array if old_data_shape != new_array.shape[-2:]: - # delete graphics at index zero - subplot.delete_graphic(graphic=subplot["image_widget_managed"]) - # insert new graphic at index zero + # make a new graphic with the new xy dims frame = self._process_indices( new_array, slice_indices=self._current_index ) frame = self._process_frame_apply(frame, i) + + # make new graphic first new_graphic = ImageGraphic(data=frame, name="image_widget_managed") - subplot.insert_graphic(graphic=new_graphic) + + # set hlut tool to use new graphic subplot.docks["right"]["histogram_lut"].image_graphic = new_graphic + # delete old graphic after setting hlut tool to new graphic + # this ensures gc + subplot.delete_graphic(graphic=subplot["image_widget_managed"]) + subplot.insert_graphic(graphic=new_graphic) + if new_array.ndim > 2: # to set max of time slider, txy or tzxy max_lengths["t"] = min(max_lengths["t"], new_array.shape[0] - 1) From 418a73b2aef82043af004f392faf6b4d2c577deb Mon Sep 17 00:00:00 2001 From: Kushal Kolar Date: Sun, 7 Apr 2024 06:44:56 -0400 Subject: [PATCH 42/66] gridplot controllers kwarg is back, other improvements to gp (#477) * gridplot controllers kwarg is back, other improvements to gp * gridplot manipulation tests * black * done wtih gp tests * move non example tests to new tests dir * force offscreen canvas * force offscreen canvas with env var for now --- .github/workflows/ci.yml | 2 + fastplotlib/layouts/_gridplot.py | 327 ++++++++++++++++++++----------- tests/test_gridplot.py | 164 ++++++++++++++++ 3 files changed, 375 insertions(+), 118 deletions(-) create mode 100644 tests/test_gridplot.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 53e88bdf8..7db694d01 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -90,6 +90,7 @@ jobs: env: PYGFX_EXPECT_LAVAPIPE: true run: | + WGPU_FORCE_OFFSCREEN=1 pytest -v tests/ pytest -v examples FASTPLOTLIB_NB_TESTS=1 pytest --nbmake examples/notebooks/ - uses: actions/upload-artifact@v3 @@ -145,6 +146,7 @@ jobs: env: PYGFX_EXPECT_LAVAPIPE: true run: | + WGPU_FORCE_OFFSCREEN=1 pytest -v tests/ pytest -v examples - uses: actions/upload-artifact@v3 if: ${{ failure() }} diff --git a/fastplotlib/layouts/_gridplot.py b/fastplotlib/layouts/_gridplot.py index 5f7f3086d..472d3dd2e 100644 --- a/fastplotlib/layouts/_gridplot.py +++ b/fastplotlib/layouts/_gridplot.py @@ -1,6 +1,6 @@ from itertools import product, chain import numpy as np -from typing import Literal +from typing import Literal, Iterable from inspect import getfullargspec from warnings import warn @@ -21,16 +21,21 @@ def __init__( shape: tuple[int, int], cameras: ( Literal["2d", "3d"] - | list[Literal["2d", "3d"]] - | list[pygfx.PerspectiveCamera] - | np.ndarray + | Iterable[Iterable[Literal["2d", "3d"]]] + | pygfx.PerspectiveCamera + | Iterable[Iterable[pygfx.PerspectiveCamera]] ) = "2d", controller_types: ( - Literal["panzoom", "fly", "trackball", "orbit"] - | list[Literal["panzoom", "fly", "trackball", "orbit"]] - | np.ndarray + Iterable[Iterable[Literal["panzoom", "fly", "trackball", "orbit"]]] + | Iterable[Literal["panzoom", "fly", "trackball", "orbit"]] ) = None, - controller_ids: str | list[int] | np.ndarray | list[list[str]] = None, + controller_ids: ( + Literal["sync"] + | Iterable[int] + | Iterable[Iterable[int]] + | Iterable[Iterable[str]] + ) = None, + controllers: pygfx.Controller | Iterable[Iterable[pygfx.Controller]] = None, canvas: str | WgpuCanvasBase | pygfx.Texture = None, renderer: pygfx.WgpuRenderer = None, size: tuple[int, int] = (500, 300), @@ -44,19 +49,18 @@ def __init__( shape: (int, int) (n_rows, n_cols) - cameras: "2d", "3", list of "2d" | "3d", list of camera instances, or np.ndarray of "2d" | "3d", optional + cameras: "2d", "3", list of "2d" | "3d", Iterable of camera instances, or Iterable of "2d" | "3d", optional | if str, one of ``"2d"`` or ``"3d"`` indicating 2D or 3D cameras for all subplots - | list/array of ``2d`` and/or ``3d`` that specifies the camera type for each subplot - | list/array of pygfx.PerspectiveCamera instances + | Iterable/list/array of ``2d`` and/or ``3d`` that specifies the camera type for each subplot + | Iterable/list/array of pygfx.PerspectiveCamera instances - controller_types: str, list or np.ndarray, optional - list or array that specifies the controller type for each subplot, or list/array of - pygfx.Controller instances. Valid controller types: "panzoom", "fly", "trackball", "orbit". + controller_types: str, Iterable, optional + list/array that specifies the controller type for each subplot. + Valid controller types: "panzoom", "fly", "trackball", "orbit". If not specified a default controller is chosen based on the camera type. Orthographic projections, i.e. "2d" cameras, use a "panzoom" controller by default. Perspective projections with a FOV > 0, i.e. "3d" cameras, use a "fly" controller by default. - controller_ids: str, list of int, np.ndarray of int, or list with sublists of subplot str names, optional | If `None` a unique controller is created for each subplot | If "sync" all the subplots use the same controller @@ -70,6 +74,11 @@ def __init__( | list of lists of subplot names, each sublist is synced: [[subplot_a, subplot_b, subplot_e], [subplot_c, subplot_d]] | this syncs subplot_a, subplot_b and subplot_e together; syncs subplot_c and subplot_d together + controllers: pygfx.Controller | list[pygfx.Controller] | np.ndarray[pygfx.Controller], optional + directly provide pygfx.Controller instances(s). Useful if you want to use a controller from an existing + plot/subplot. Other controller kwargs, i.e. ``controller_Types`` and ``controller_ids`` are ignored if + ``controllers`` are provided. + canvas: WgpuCanvas, optional Canvas for drawing @@ -83,25 +92,23 @@ def __init__( subplot names """ - self.shape = shape + self._shape = shape if names is not None: - if len(list(chain(*names))) != self.shape[0] * self.shape[1]: + if len(list(chain(*names))) != len(self): raise ValueError( "must provide same number of subplot `names` as specified by gridplot shape" ) - self.names = np.asarray(names).reshape(self.shape) + subplot_names = np.asarray(names).reshape(self.shape) else: - self.names = None + subplot_names = None canvas, renderer = make_canvas_and_renderer(canvas, renderer) if isinstance(cameras, str): # 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] * len(self)).reshape(self.shape) # list -> array if necessary cameras = np.asarray(cameras).reshape(self.shape) @@ -110,127 +117,177 @@ def __init__( raise ValueError("Number of cameras does not match the number of subplots") # create the cameras - self._cameras = np.empty(self.shape, dtype=object) + subplot_cameras = np.empty(self.shape, dtype=object) for i, j in product(range(self.shape[0]), range(self.shape[1])): - self._cameras[i, j] = create_camera(camera_type=cameras[i, j]) + subplot_cameras[i, j] = create_camera(camera_type=cameras[i, j]) - if controller_ids is None: - # individual controller for each subplot - controller_ids = np.arange(self.shape[0] * self.shape[1]).reshape( - self.shape - ) + # if controller instances have been specified for each subplot + if controllers is not None: + + # one controller for all subplots + if isinstance(controllers, pygfx.Controller): + controllers = [controllers] * len(self) + # subplot_controllers[:] = controllers + # # subplot_controllers = np.asarray([controllers] * len(self), dtype=object) - elif isinstance(controller_ids, str): - if controller_ids == "sync": - controller_ids = np.zeros(self.shape, dtype=int) + # individual controller instance specified for each subplot else: - raise ValueError( - f"`controller_ids` must be one of 'sync', an array/list of subplot names, or an array/list of " - f"integer ids. See the docstring for more details." - ) + # I found that this is better than list(*chain()) because chain doesn't give the right + # result we want for arrays + for item in controllers: + if isinstance(item, pygfx.Controller): + pass + elif all(isinstance(c, pygfx.Controller) for c in item): + pass + else: + raise TypeError( + "controllers argument must be a single pygfx.Controller instance of a Iterable of " + "pygfx.Controller instances" + ) - # list controller_ids - elif isinstance(controller_ids, (list, np.ndarray)): - ids_flat = list(chain(*controller_ids)) + try: + controllers = np.asarray(controllers).reshape(shape) + except ValueError: + raise ValueError( + f"number of controllers passed must be the same as the number of subplots specified " + f"by shape: {self.shape}. You have passed: <{controllers.size}> controllers" + ) from None - # list of str of subplot names, convert this to integer ids - if all([isinstance(item, str) for item in ids_flat]): - if self.names is None: - raise ValueError( - "must specify subplot `names` to use list of str for `controller_ids`" - ) + subplot_controllers: np.ndarray[pygfx.Controller] = np.empty( + self.shape, dtype=object + ) - # make sure each controller_id str is a subplot name - if not all([n in self.names for n in ids_flat]): - raise KeyError( - f"all `controller_ids` strings must be one of the subplot names" - ) + for i, j in product(range(self.shape[0]), range(self.shape[1])): + subplot_controllers[i, j] = controllers[i, j] + subplot_controllers[i, j].add_camera(subplot_cameras[i, j]) - if len(ids_flat) > len(set(ids_flat)): + # parse controller_ids and controller_types to make desired controller for each supblot + else: + if controller_ids is None: + # individual controller for each subplot + controller_ids = np.arange(len(self)).reshape(self.shape) + + elif isinstance(controller_ids, str): + if controller_ids == "sync": + # this will eventually make one controller for all subplots + controller_ids = np.zeros(self.shape, dtype=int) + else: raise ValueError( - "id strings must not appear twice in `controller_ids`" + f"`controller_ids` must be one of 'sync', an array/list of subplot names, or an array/list of " + f"integer ids. See the docstring for more details." ) - # initialize controller_ids array - ids_init = np.arange(self.shape[0] * self.shape[1]).reshape(self.shape) + # list controller_ids + elif isinstance(controller_ids, (list, np.ndarray)): + ids_flat = list(chain(*controller_ids)) - # set id based on subplot position for each synced sublist - for i, sublist in enumerate(controller_ids): - for name in sublist: - ids_init[self.names == name] = -( - i + 1 - ) # use negative numbers because why not + # list of str of subplot names, convert this to integer ids + if all([isinstance(item, str) for item in ids_flat]): + if subplot_names is None: + raise ValueError( + "must specify subplot `names` to use list of str for `controller_ids`" + ) - controller_ids = ids_init + # make sure each controller_id str is a subplot name + if not all([n in subplot_names for n in ids_flat]): + raise KeyError( + f"all `controller_ids` strings must be one of the subplot names" + ) - # integer ids - elif all([isinstance(item, (int, np.integer)) for item in ids_flat]): - controller_ids = np.asarray(controller_ids).reshape(self.shape) + if len(ids_flat) > len(set(ids_flat)): + raise ValueError( + "id strings must not appear twice in `controller_ids`" + ) - else: - raise TypeError( - f"list argument to `controller_ids` must be a list of `str` or `int`, " - f"you have passed: {controller_ids}" - ) + # initialize controller_ids array + ids_init = np.arange(len(self)).reshape(self.shape) - if controller_ids.shape != self.shape: - raise ValueError( - "Number of controller_ids does not match the number of subplots" - ) + # set id based on subplot position for each synced sublist + for i, sublist in enumerate(controller_ids): + for name in sublist: + ids_init[subplot_names == name] = -( + i + 1 + ) # use negative numbers because why not - if controller_types is None: - # `create_controller()` will auto-determine controller for each subplot based on defaults - controller_types = np.array( - ["default"] * self.shape[0] * self.shape[1] - ).reshape(self.shape) + controller_ids = ids_init - # validate controller types - types_flat = list(chain(*controller_types)) - # str controller_type or pygfx instances - valid_str = list(valid_controller_types.keys()) + ["default"] - valid_instances = tuple(valid_controller_types.values()) + # integer ids + elif all([isinstance(item, (int, np.integer)) for item in ids_flat]): + controller_ids = np.asarray(controller_ids).reshape(self.shape) - # make sure each controller type is valid - for controller_type in types_flat: - if controller_type is None: - continue + else: + raise TypeError( + f"list argument to `controller_ids` must be a list of `str` or `int`, " + f"you have passed: {controller_ids}" + ) - if (controller_type not in valid_str) and ( - not isinstance(controller_type, valid_instances) - ): + if controller_ids.shape != self.shape: raise ValueError( - f"You have passed an invalid controller type, valid controller_types arguments are:\n" - f"{valid_str} or instances of {[c.__name__ for c in valid_instances]}" + "Number of controller_ids does not match the number of subplots" ) - controller_types = np.asarray(controller_types).reshape(self.shape) + if controller_types is None: + # `create_controller()` will auto-determine controller for each subplot based on defaults + controller_types = np.array(["default"] * len(self)).reshape(self.shape) - # make the real controllers for each subplot - self._controllers = np.empty(shape=self.shape, dtype=object) - for cid in np.unique(controller_ids): - cont_type = controller_types[controller_ids == cid] - if np.unique(cont_type).size > 1: - raise ValueError( - "Multiple controller types have been assigned to the same controller id. " - "All controllers with the same id must use the same type of controller." - ) + elif isinstance(controller_types, str): + if controller_types not in valid_controller_types.keys(): + raise ValueError( + f"invalid controller_types argument, you may pass either a single controller type as a str, or an" + f"iterable of controller types from the selection: {valid_controller_types.keys()}" + ) + + # valid controller types + types_flat = list(chain(*controller_types)) + # str controller_type or pygfx instances + valid_str = list(valid_controller_types.keys()) + ["default"] + valid_instances = tuple(valid_controller_types.values()) + + # make sure each controller type is valid + for controller_type in types_flat: + if controller_type is None: + continue + + if (controller_type not in valid_str) and ( + not isinstance(controller_type, valid_instances) + ): + raise ValueError( + f"You have passed an invalid controller type, valid controller_types arguments are:\n" + f"{valid_str} or instances of {[c.__name__ for c in valid_instances]}" + ) - cont_type = cont_type[0] + controller_types: np.ndarray[pygfx.Controller] = np.asarray( + controller_types + ).reshape(self.shape) - # get all the cameras that use this controller - cams = self._cameras[controller_ids == cid].ravel() + # make the real controllers for each subplot + subplot_controllers = np.empty(shape=self.shape, dtype=object) + for cid in np.unique(controller_ids): + cont_type = controller_types[controller_ids == cid] + if np.unique(cont_type).size > 1: + raise ValueError( + "Multiple controller types have been assigned to the same controller id. " + "All controllers with the same id must use the same type of controller." + ) - if cont_type == "default": - # hacky fix for now because of how `create_controller()` works - cont_type = None - _controller = create_controller(controller_type=cont_type, camera=cams[0]) + cont_type = cont_type[0] - self._controllers[controller_ids == cid] = _controller + # get all the cameras that use this controller + cams = subplot_cameras[controller_ids == cid].ravel() - # add the other cameras that go with this controller - if cams.size > 1: - for cam in cams[1:]: - _controller.add_camera(cam) + if cont_type == "default": + # hacky fix for now because of how `create_controller()` works + cont_type = None + _controller = create_controller( + controller_type=cont_type, camera=cams[0] + ) + + subplot_controllers[controller_ids == cid] = _controller + + # add the other cameras that go with this controller + if cams.size > 1: + for cam in cams[1:]: + _controller.add_camera(cam) self._canvas = canvas self._renderer = renderer @@ -243,11 +300,11 @@ def __init__( for i, j in self._get_iterator(): position = (i, j) - camera = self._cameras[i, j] - controller = self._controllers[i, j] + camera = subplot_cameras[i, j] + controller = subplot_controllers[i, j] - if self.names is not None: - name = self.names[i, j] + if subplot_names is not None: + name = subplot_names[i, j] else: name = None @@ -272,6 +329,11 @@ def __init__( RecordMixin.__init__(self) Frame.__init__(self) + @property + def shape(self) -> tuple[int, int]: + """[n_rows, n_cols]""" + return self._shape + @property def canvas(self) -> WgpuCanvasBase: """The canvas associated to this GridPlot""" @@ -282,6 +344,31 @@ def renderer(self) -> pygfx.WgpuRenderer: """The renderer associated to this GridPlot""" return self._renderer + @property + def controllers(self) -> np.ndarray[pygfx.Controller]: + """controllers, read-only array, access individual subplots to change a controller""" + controllers = np.asarray( + [subplot.controller for subplot in self], dtype=object + ).reshape(self.shape) + controllers.flags.writeable = False + return controllers + + @property + def cameras(self) -> np.ndarray[pygfx.Camera]: + """cameras, read-only array, access individual subplots to change a camera""" + cameras = np.asarray( + [subplot.camera for subplot in self], dtype=object + ).reshape(self.shape) + cameras.flags.writeable = False + return cameras + + @property + def names(self) -> np.ndarray[str]: + """subplot names, read-only array, access individual subplots to change a name""" + names = np.asarray([subplot.name for subplot in self]).reshape(self.shape) + names.flags.writeable = False + return names + def __getitem__(self, index: tuple[int, int] | str) -> Subplot: if isinstance(index, str): for subplot in self._subplots.ravel(): @@ -389,6 +476,10 @@ def __next__(self) -> Subplot: pos = self._current_iter.__next__() return self._subplots[pos] + def __len__(self): + """number of subplots""" + return self.shape[0] * self.shape[1] + def __str__(self): return f"{self.__class__.__name__} @ {hex(id(self))}" diff --git a/tests/test_gridplot.py b/tests/test_gridplot.py new file mode 100644 index 000000000..3814664d7 --- /dev/null +++ b/tests/test_gridplot.py @@ -0,0 +1,164 @@ +import numpy as np +import pytest + +import fastplotlib as fpl +import pygfx + + +def test_cameras_controller_properties(): + cameras = [ + ["2d", "3d", "3d"], + ["3d", "3d", "3d"] + ] + + controller_types = [ + ["panzoom", "panzoom", "fly"], + ["orbit", "trackball", "panzoom"] + ] + + gp = fpl.GridPlot( + shape=(2, 3), + cameras=cameras, + controller_types=controller_types, + canvas="offscreen" + ) + + print(gp.canvas) + + subplot_cameras = [subplot.camera for subplot in gp] + subplot_controllers = [subplot.controller for subplot in gp] + + for c1, c2 in zip(subplot_cameras, gp.cameras.ravel()): + assert c1 is c2 + + for c1, c2 in zip(subplot_controllers, gp.controllers.ravel()): + assert c1 is c2 + + for camera_type, subplot_camera in zip(np.asarray(cameras).ravel(), gp.cameras.ravel()): + if camera_type == "2d": + assert subplot_camera.fov == 0 + else: + assert subplot_camera.fov == 50 + + for controller_type, subplot_controller in zip(np.asarray(controller_types).ravel(), gp.controllers.ravel()): + match controller_type: + case "panzoom": + assert isinstance(subplot_controller, pygfx.PanZoomController) + case "fly": + assert isinstance(subplot_controller, pygfx.FlyController) + case "orbit": + assert isinstance(subplot_controller, pygfx.OrbitController) + case "trackball": + assert isinstance(subplot_controller, pygfx.TrackballController) + + # check changing cameras + gp[0, 0].camera = "3d" + assert gp[0, 0].camera.fov == 50 + gp[1, 0].camera = "2d" + assert gp[1, 0].camera.fov == 0 + + # test changing controller + gp[1, 1].controller = "fly" + assert isinstance(gp[1, 1].controller, pygfx.FlyController) + assert gp[1, 1].controller is gp.controllers[1, 1] + gp[0, 2].controller = "panzoom" + assert isinstance(gp[0, 2].controller, pygfx.PanZoomController) + assert gp[0, 2].controller is gp.controllers[0, 2] + + +def test_gridplot_controller_ids_int(): + ids = [ + [0, 1, 1], + [0, 2, 3], + [4, 1, 2] + ] + + gp = fpl.GridPlot(shape=(3, 3), controller_ids=ids, canvas="offscreen") + + assert gp[0, 0].controller is gp[1, 0].controller + assert gp[0, 1].controller is gp[0, 2].controller is gp[2, 1].controller + assert gp[1, 1].controller is gp[2, 2].controller + + +def test_gridplot_controller_ids_int_change_controllers(): + ids = [ + [0, 1, 1], + [0, 2, 3], + [4, 1, 2] + ] + + cameras = [ + ["2d", "3d", "3d"], + ["2d", "3d", "2d"], + ["3d", "3d", "3d"] + ] + + gp = fpl.GridPlot(shape=(3, 3), cameras=cameras, controller_ids=ids, canvas="offscreen") + + assert isinstance(gp[0, 1].controller, pygfx.FlyController) + + # changing controller when id matches should change the others too + gp[0, 1].controller = "panzoom" + assert isinstance(gp[0, 1].controller, pygfx.PanZoomController) + assert gp[0, 1].controller is gp[0, 2].controller is gp[2, 1].controller + assert set(gp[0, 1].controller.cameras) == {gp[0, 1].camera, gp[0, 2].camera, gp[2, 1].camera} + + # change to orbit + gp[0, 1].controller = "orbit" + assert isinstance(gp[0, 1].controller, pygfx.OrbitController) + assert gp[0, 1].controller is gp[0, 2].controller is gp[2, 1].controller + assert set(gp[0, 1].controller.cameras) == {gp[0, 1].camera, gp[0, 2].camera, gp[2, 1].camera} + + +def test_gridplot_controller_ids_str(): + names = [ + ["a", "b", "c"], + ["d", "e", "f"] + ] + + controller_ids = [ + ["a", "f"], + ["b", "d", "e"] + ] + + gp = fpl.GridPlot(shape=(2, 3), controller_ids=controller_ids, names=names, canvas="offscreen") + + assert gp[0, 0].controller is gp[1, 2].controller is gp["a"].controller is gp["f"].controller + assert gp[0, 1].controller is gp[1, 0].controller is gp[1, 1].controller is gp["b"].controller is gp["d"].controller is gp["e"].controller + + # make sure subplot c is unique + exclude_c = [gp[n].controller for n in ["a", "b", "d", "e", "f"]] + assert gp["c"] not in exclude_c + + +def test_set_gridplot_controllers_from_existing_controllers(): + gp = fpl.GridPlot(shape=(3, 3), canvas="offscreen") + gp2 = fpl.GridPlot(shape=gp.shape, controllers=gp.controllers, canvas="offscreen") + + assert gp.controllers[:-1].size == 6 + with pytest.raises(ValueError): + gp3 = fpl.GridPlot(shape=gp.shape, controllers=gp.controllers[:-1], canvas="offscreen") + + for sp_gp, sp_gp2 in zip(gp, gp2): + assert sp_gp.controller is sp_gp2.controller + + cameras = [ + [pygfx.PerspectiveCamera(), "3d"], + ["3d", "2d"] + ] + + controllers = [ + [pygfx.FlyController(cameras[0][0]), pygfx.TrackballController()], + [pygfx.OrbitController(), pygfx.PanZoomController()] + ] + + gp = fpl.GridPlot(shape=(2, 2), cameras=cameras, controllers=controllers, canvas="offscreen") + + assert gp[0, 0].controller is controllers[0][0] + assert gp[0, 1].controller is controllers[0][1] + assert gp[1, 0].controller is controllers[1][0] + assert gp[1, 1].controller is controllers[1][1] + + assert gp[0, 0].camera is cameras[0][0] + + assert gp[0, 1].camera.fov == 50 From e8876478fc472549bfaef41c86028b3caf9e4228 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Wed, 10 Apr 2024 19:37:57 -0400 Subject: [PATCH 43/66] update gov --- GOVERNANCE.md | 117 +++++++++++++++++++++++--------------------------- 1 file changed, 54 insertions(+), 63 deletions(-) diff --git a/GOVERNANCE.md b/GOVERNANCE.md index ff7b107b5..3ad450dcb 100644 --- a/GOVERNANCE.md +++ b/GOVERNANCE.md @@ -6,116 +6,107 @@ The purpose of this document is to formalize the governance process used by the ## Mission -Leverage new graphics APIs and modern GPU hardware to create fast and interactive scientific visualizations using an expressive and elegant API. +The mission of `fastplotlib` is to leverage new graphics APIs and modern GPU hardware to create fast and interactive scientific visualizations using an expressive and elegant API. ## Leadership Team ### Maintainers -The maintainers are the core developers of fastplotlib and together have a complete understanding of the codebase. +The maintainers are the core developers of fastplotlib and together have a complete understanding of the codebase. They are also known as code-owners. + +The current maintainers are: 1. [Kushal Kolar](https://github.com/kushalkolar) 1. [Caitlin Lewis](https://github.com/clewis7) +Responsibilities: + +* Carry out the `fastplotlib` mission. +* Work towards completion of the roadmap. +* Timely responses to issues and pull requests. +* Code review. +* Attend a yearly Roadmap meeting. +* Be available for conflict resolution. ### Advisory Committee -The Advisory Committee hold a significant stake in fastplotlib as determined by the **Maintainers**. The responsibilities of the **Advisory Committee** are to 1) attend a yearly Roadmap meeting, 2) be available for conflict resolution. +The Advisory Committee holds a significant interest in fastplotlib as determined solely by the **Maintainers**. The responsibilities of the **Advisory Committee** are to 1) attend a yearly Roadmap meeting, 2) be available for conflict resolution. +1. Amol Pasarkar 1. Eric Thomson 1. Guillaume Viejo 1. Andrea Giovannucci 1. John Pearson +Responsibilities: + +* Help carry out the `fastplotlib` mission. +* Provide strategic guidance. +* Attend a yearly Roadmap meeting. +* Be available for conflict resolution. + ### Neutral moderator No voting power, has no stake in the fastplotlib project. * Reagan Bullins -### Voting power distribution +Responsibilities: + +* Facilitate conflict resolution without voting power. + +## Adding a member to the advisory committee +1. Only individuals, not organizations, may be added to the leadership team. A candidate individual must be nominated by a current member of the leadership team. +2. A candidate must: + * Be committed to the fastplotlib mission. + * Have demonstrated contibutions to `fastplotlib` through one of: + * Significant contributions to the codebase. + * Significant application of fastplotlib in a dependent package. + * Significant technical guidance or feedback on the development of `fastplotlib`. -Maintainers: 50% +## Adding a maintainer -Advisory Committee: 50% +Candidate maintainers must have demonstrated prolonged and significant contributions to the codebase over a long period of time. A candidate can be nominated by any current maintainer. The candidate may then be added as a maintainer through a unanimous vote within the current maintainers. -Veto: Any vote can be vetoed by a unanimous vote within the maintainers. +## Decision making -Note that currently the voting power is primarily held by the maintainers - Kushal Kolar & Caitlin Lewis. This is intentional since the library is in an early stage. Knowledge of the codebase and its inner workings are predominantly held by Kushal & Caitlin, and nobody else. +Decisions about the future of the project are made through discussion with all members of the community. All non-sensitive project management discussion takes place on the issue tracker. Occasionally, sensitive discussions may occur on a private core developer medium. -### Voting Process +Decisions should be made in accordance with the mission and code of conduct of the `fastplotlib` project. -1. For the purpose of conflict resolution the Leadership Team must be convened by the neutral moderator. In all other situations the Leadership Team may be convened by one of the maintainers. -1. Once the Leadership Team is convened and discussions have occurred, voting must conclude within 1 hour. -1. Voting is performed anonymously and handled solely by the neutral moderator in the case of conflict resolution, or by one of the maintainers. -1. After voting has finished, any maintainer may invoke a veto vote. If the veto succeeds, then the same item may not be voted on for 100 days. +We use a “consensus seeking” process for making decisions. The Leadership Team tries to find a resolution that has no open objections among Leadership Team members. Leadership Team members are expected to distinguish between fundamental objections to a proposal and minor perceived flaws that they can live with, and not hold up the decision-making process for the latter. If no option can be found without objections, the decision is escalated to the maintainers who have ultimate authority. -## Adding members to the Leadership Team +Decisions are made according to the following rules: -### Confirming Maintainers +Minor documentation changes, such as typo fixes, or addition / correction of a sentence, require approval by a maintainer and no disagreement or requested changes by other maintainers on the issue or pull request page via lazy consensus. Pull-request authors are expected to give “reasonable time” to others to give their opinion on the pull request if they’re not confident others would agree. -* Given to individuals on merit basis after they have demonstrated strong expertise of the library through contributions, reviews and discussions. -* Adding a maintainer requires one of: - * a unanimous vote by current maintainers - * 75% vote within the advisory committee -* For continued membership in the maintainer group the individual has to demonstrate strong and continued alignment with the fastplotlib mission. The individual must also actively commit to the repo, respond to issues, and review pull requests. -* The membership is for an individual, not a company or organization. -* There must always be a minimum of 2 maintainers. -* A maintainer may be removed by one of: - * 60% vote within the maintainers - * 75% vote within the advisory committee +Code changes and major documentation changes require agreement by one maintainer and no disagreement or requested changes by other maintainers on the issue or pull-request page (lazy consensus). For all changes of this type, maintainers are expected to give “reasonable time” after approval and before merging for others to weigh in on the pull request in its final state. -### Confirming an advisory committee member +Changes to the API principles require a dedicated issue on our issue tracker and follow the decision-making process outlined above. -* A candidate advisory committee member may only be nominated by a current maintainer or advisory committee member. -* Candidate must have used fastplotlib in their own work or library, or made contributions to fastplotlib. -* Candidate must be committed to the mission and demonstrate, with examples, how their role on the advisory committee would further the mission. -* If the Individual fulfills the above criteria they may be considered. The Leadership Team then requires an overall 80% vote to add the candidate as an advisory member. -* The membership is for an individual, not a company or organization. - * 60% vote within the maintainers - * 75% vote within the advisory committee +Changes to this governance model or our mission, vision, and values require a dedicated issue on our issue tracker and follow the decision-making process outlined above. -## Invoking a vote from the leadership team +If an objection is raised on a lazy consensus, the proposer can appeal to the Leadership Team and the change can be approved or rejected by escalating to the maintainers. -### Conflict +## Conflict Resolution Anyone (absolutely anyone, not just the leadership team members) who feels that the code of conduct or governance document has been breached may invoke a vote by contacting the neutral moderator. -#### Process +### Process 1. Contact the neutral moderator with a description of the conflict, max of 250 words. 2. Neutral moderator must schedule a vote within 15 days. If that is not possible then within the next 45 days. -3. If there is a special case that requires urgency, such as upcoming events, workshops, etc., the neutral moderator may try to schedule a vote ASAP. Exercising this urgency is solely at the discretion of the neutral moderator. -4. The individual who has invoked the conflict vote can choose to present their case, or they may choose to let the neutral moderator represent them. +3. The individual who has invoked the conflict vote can choose to present their case, or they may choose to let the neutral moderator represent them. * Every individual involved in the conflict is given a maximum of 15 minutes to be represented. This time limit may be expanded at the discretion of the neutral moderator if a justifiable reason is provided. -5. The Leadership Team votes on one of the actions from “Enforcement Guidelines”: https://www.contributor-covenant.org/version/2/1/code_of_conduct/. It is advised that the first offense leads to action (1) “Correction” and then go down the list for repeated conflicts from the same individual/organization. Very bad behavior, as determined by the leadership team, can justify a first offense resulting in (3) “Temporary Ban” or (4) “Permanent Ban”. -6. Voting is performed as described in the section **Voting Process**. +4. The maintainers vote on one of the actions from “Enforcement Guidelines”: https://www.contributor-covenant.org/version/2/1/code_of_conduct/. It is advised that the first offense leads to action (1) “Correction”. Repeated or serious offenses from the same individual/organization may lead to escalating levels of actions. Very bad behavior, as determined by the leadership team, can justify a first offense resulting in (3) “Temporary Ban” or (4) “Permanent Ban”. +5. The advisory committee members may advise on the actions, but the ultimate decision is voted on by the maintainers. -### Adding a member to the leadership team -1. A current maintainer or advisory committee member may nominate a candidate and then contact the neutral moderator with a written summary, of no more than 100 words, with reasons for adding the candidate to the Leadership Team. -1. The nominator should specify whether the candidate should be a maintainer or advisory committee member. -1. The neutral moderator must then schedule a vote within 100 days. -1. The nominator and candidate may then speak for no more than 10 minutes each on why the candidate should be added to the leadership team. -1. Voting is performed as described in the section **Voting Process**. +## Transparency + +Governance decisions, meeting minutes, and voting outcomes are publicly documented and accessible. We aim for transparency to allow the broader community to understand and trust the governance process. ## Changes to this governance document -#### Until February 28, 2025 +### Until February 28, 2025 During early stages of fastplotlib development, changes to the governance document may be made directly through unanimous approval by the original maintainers, Kushal Kolar & Caitlin Lewis, without contacting the neutral moderator or consulting with the advisory committee. They (Kushal & Caitlin) may also add new members to the advisory committee through unanimous approval. - -#### After February 28, 2025 - -Any member of the Leadership Team can propose changes to the governance document. Approving the changes requires a 75% vote within the maintainers and a 75% vote within the advisory committee. - -## Reasons for invoking a vote - -Things that can be voted on include but aren’t limited to: -1. Nominating a member to the Leadership Team -1. Adding a nominee to the Leadership Team -1. Removing a member from the Leadership Team -1. Banning any person (not just Leadership Team members) or organization from interacting with the fastplotlib GitHub repository and/or fastplotlib GitHub organization. -1. Appeal to un-ban a previously banned person or organization. - * Can only occur 3 months after the initial ban - * Person must contact the neutral moderator directly to start an appeal - * Maximum of 3 appeal attempts are allowed From e7d940d3d4f0098baaff7ea5a55d307bd00855b5 Mon Sep 17 00:00:00 2001 From: Kushal Kolar Date: Fri, 12 Apr 2024 00:48:44 -0400 Subject: [PATCH 44/66] Iw refactor (#482) * fefactor imwidget, restrict to tzxy, txy, xy, added RGB(A) support (#459) * add new iw test screenshots * update api docs --------- Co-authored-by: Amol Pasarkar --- docs/source/api/widgets/ImageWidget.rst | 4 +- examples/notebooks/image_widget.ipynb | 1 - examples/notebooks/image_widget_test.ipynb | 63 ++- ...dget-zfish-mixed-rgb-cockatoo-frame-50.png | 3 + ...dget-zfish-mixed-rgb-cockatoo-set-data.png | 3 + ...get-zfish-mixed-rgb-cockatoo-windowrgb.png | 3 + fastplotlib/widgets/image.py | 515 +++++++++--------- 7 files changed, 306 insertions(+), 286 deletions(-) create mode 100644 examples/notebooks/screenshots/nb-image-widget-zfish-mixed-rgb-cockatoo-frame-50.png create mode 100644 examples/notebooks/screenshots/nb-image-widget-zfish-mixed-rgb-cockatoo-set-data.png create mode 100644 examples/notebooks/screenshots/nb-image-widget-zfish-mixed-rgb-cockatoo-windowrgb.png diff --git a/docs/source/api/widgets/ImageWidget.rst b/docs/source/api/widgets/ImageWidget.rst index 08bce8d7a..2b4708007 100644 --- a/docs/source/api/widgets/ImageWidget.rst +++ b/docs/source/api/widgets/ImageWidget.rst @@ -23,9 +23,11 @@ Properties ImageWidget.cmap ImageWidget.current_index ImageWidget.data - ImageWidget.dims_order + ImageWidget.frame_apply ImageWidget.gridplot ImageWidget.managed_graphics + ImageWidget.n_img_dims + ImageWidget.n_scrollable_dims ImageWidget.ndim ImageWidget.slider_dims ImageWidget.sliders diff --git a/examples/notebooks/image_widget.ipynb b/examples/notebooks/image_widget.ipynb index 56d5c8a81..a7527601a 100644 --- a/examples/notebooks/image_widget.ipynb +++ b/examples/notebooks/image_widget.ipynb @@ -115,7 +115,6 @@ "source": [ "iw_movie = ImageWidget(\n", " data=gray_movie, \n", - " slider_dims=[\"t\"],\n", " cmap=\"gray\"\n", ")" ] diff --git a/examples/notebooks/image_widget_test.ipynb b/examples/notebooks/image_widget_test.ipynb index c236ce9b7..39cf0b887 100644 --- a/examples/notebooks/image_widget_test.ipynb +++ b/examples/notebooks/image_widget_test.ipynb @@ -2,7 +2,7 @@ "cells": [ { "cell_type": "code", - "execution_count": null, + "execution_count": 3, "id": "07019035-83f2-4753-9e7c-628ae439b441", "metadata": { "tags": [] @@ -18,7 +18,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 4, "id": "10b8ab40-944d-472c-9b7e-cae8a129e7ce", "metadata": {}, "outputs": [], @@ -130,7 +130,6 @@ "source": [ "iw_movie = ImageWidget(\n", " data=gray_movie, \n", - " slider_dims=[\"t\"],\n", " cmap=\"gray\",\n", " grid_plot_kwargs={\"size\": (900, 600)},\n", ")" @@ -275,9 +274,6 @@ "execution_count": null, "id": "76535d56-e514-4c16-aa48-a6359f8019d5", "metadata": { - "jupyter": { - "source_hidden": true - }, "tags": [] }, "outputs": [], @@ -444,23 +440,66 @@ "iw_z.close()" ] }, + { + "cell_type": "markdown", + "id": "6716f255-44c2-400d-a2bf-254683e4cd9d", + "metadata": {}, + "source": [ + "# Test Mixed Shapes, RGB (and set data)" + ] + }, { "cell_type": "code", - "execution_count": null, - "id": "870627ef-09d8-44e4-8952-aedb702d1526", + "execution_count": 30, + "id": "ed783360-992d-40f8-bb6f-152a59edff43", "metadata": {}, "outputs": [], "source": [ - "notebook_finished()" + "zfish_data = np.load(\"./zfish_test.npy\")\n", + "zfish_frame_1 = zfish_data[0, 0, :, :]\n", + "zfish_frame_2 = zfish_data[20, 3, :, :]\n", + "movie = iio.imread(\"imageio:cockatoo.mp4\")\n", + "\n", + "iw_mixed_shapes = ImageWidget(\n", + " data=[zfish_frame_1, movie], # you can also provide a list of tzxy arrays\n", + " rgb=[False, True],\n", + " histogram_widget=True,\n", + " cmap=\"gnuplot2\", \n", + " grid_plot_kwargs = {\"controller_ids\": None},\n", + ")\n", + "\n", + "iw_mixed_shapes.show()" + ] + }, + { + "cell_type": "code", + "execution_count": 29, + "id": "274c67b4-aa07-4fcf-a094-1b1e70d0378a", + "metadata": {}, + "outputs": [], + "source": [ + "iw_mixed_shapes.sliders[\"t\"].value = 50\n", + "plot_test(\"image-widget-zfish-mixed-rgb-cockatoo-frame-50\", iw_mixed_shapes.gridplot)\n", + "\n", + "#Set the data, changing the first array and also the size of the \"T\" slider\n", + "iw_mixed_shapes.set_data([zfish_frame_2, movie[:200, :, :, :]], reset_indices=True)\n", + "plot_test(\"image-widget-zfish-mixed-rgb-cockatoo-set-data\", iw_mixed_shapes.gridplot)\n", + "\n", + "#Check how a window function might work on the RGB data\n", + "iw_mixed_shapes.window_funcs = {\"t\": (np.mean, 4)}\n", + "iw_mixed_shapes.sliders[\"t\"].value = 20\n", + "plot_test(\"image-widget-zfish-mixed-rgb-cockatoo-windowrgb\", iw_mixed_shapes.gridplot)" ] }, { "cell_type": "code", "execution_count": null, - "id": "b8fff1a6-119e-4f03-ba3a-4c7b9e8c212b", + "id": "870627ef-09d8-44e4-8952-aedb702d1526", "metadata": {}, "outputs": [], - "source": [] + "source": [ + "notebook_finished()" + ] } ], "metadata": { @@ -479,7 +518,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.11.3" + "version": "3.11.8" } }, "nbformat": 4, diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-mixed-rgb-cockatoo-frame-50.png b/examples/notebooks/screenshots/nb-image-widget-zfish-mixed-rgb-cockatoo-frame-50.png new file mode 100644 index 000000000..5e0750ac8 --- /dev/null +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-mixed-rgb-cockatoo-frame-50.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7f8f74a0a5fa24e10a88d3723836306913243fa5fc23f46f44bbdae4c0209075 +size 58878 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-mixed-rgb-cockatoo-set-data.png b/examples/notebooks/screenshots/nb-image-widget-zfish-mixed-rgb-cockatoo-set-data.png new file mode 100644 index 000000000..8df83fe33 --- /dev/null +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-mixed-rgb-cockatoo-set-data.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0809b2dda0e773b7f100386f97144c40d36d51cd935c86ef1dcd4a938fce3981 +size 56319 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-mixed-rgb-cockatoo-windowrgb.png b/examples/notebooks/screenshots/nb-image-widget-zfish-mixed-rgb-cockatoo-windowrgb.png new file mode 100644 index 000000000..5bbefc7ae --- /dev/null +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-mixed-rgb-cockatoo-windowrgb.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a2e2e2cf7ac6be1a4fccec54494c3fd48af673765653675438fa2469c549e90c +size 55055 diff --git a/fastplotlib/widgets/image.py b/fastplotlib/widgets/image.py index acef26a7d..86671b1fc 100644 --- a/fastplotlib/widgets/image.py +++ b/fastplotlib/widgets/image.py @@ -3,20 +3,30 @@ import numpy as np - from ..layouts import GridPlot from ..graphics import ImageGraphic from ..utils import calculate_gridshape from .histogram_lut import HistogramLUT -DEFAULT_DIMS_ORDER = { - 2: "xy", - 3: "txy", - 4: "tzxy", - 5: "tzcxy", +# Number of dimensions that represent one image/one frame. For grayscale shape will be [x, y], i.e. 2 dims, for RGB(A) +# shape will be [x, y, c] where c is of size 3 (RGB) or 4 (RGBA) +IMAGE_DIM_COUNTS = {"gray": 2, "rgb": 3} + +# Map boolean (indicating whether we use RGB or grayscale) to the string. Used to index RGB_DIM_MAP +RGB_BOOL_MAP = {False: "gray", True: "rgb"} + +# Dimensions that can be scrolled from a given data array +SCROLLABLE_DIMS_ORDER = { + 0: "", + 1: "t", + 2: "tz", } +ALLOWED_SLIDER_DIMS = {0: "t", 1: "z"} + +ALLOWED_WINDOW_DIMS = {"t", "z"} + def _is_arraylike(obj) -> bool: """ @@ -149,13 +159,16 @@ def data(self) -> List[np.ndarray]: @property def ndim(self) -> int: - """number of dimensions in the image data displayed in the widget""" + """Number of dimensions of grayscale data displayed in the widget (it will be 1 more for RGB(A) data)""" return self._ndim @property - def dims_order(self) -> List[str]: - """dimension order of the data displayed in the widget""" - return self._dims_order + def n_scrollable_dims(self) -> List[int]: + """ + list indicating the number of dimenensions that are scrollable for each data array + All other dimensions are frame/image data, i.e. [x, y] or [x, y, c] + """ + return self._n_scrollable_dims @property def sliders(self) -> Dict[str, Any]: @@ -184,6 +197,56 @@ def current_index(self) -> Dict[str, int]: """ return self._current_index + @property + def n_img_dims(self) -> list[int]: + """ + list indicating the number of dimensions that contain image/single frame data for each data array. + if 2: data are grayscale, i.e. [x, y] dims, if 3: data are [x, y, c] where c is RGB or RGBA, + this is the complement of `n_scrollable_dims` + """ + return self._n_img_dims + + def _get_n_scrollable_dims(self, curr_arr: np.ndarray, rgb: bool) -> list[int]: + """ + For a given ``array`` displayed in the ImageWidget, this function infers how many of the dimensions are + supported by sliders (aka scrollable). Ex: "xy" data has 0 scrollable dims, "txy" has 1, "tzxy" has 2. + + Parameters + ---------- + curr_arr: np.ndarray + np.ndarray or a list of array-like + + rgb: bool + True if we view this as RGB(A) and False if grayscale + + Returns + ------- + int + Number of scrollable dimensions for each ``array`` in the dataset. + """ + + n_img_dims = IMAGE_DIM_COUNTS[RGB_BOOL_MAP[rgb]] + # Make sure each image stack at least ``n_img_dims`` dimensions + if len(curr_arr.shape) < n_img_dims: + raise ValueError( + f"Your array has shape {curr_arr.shape} " + f"but you specified that each image in your array is {n_img_dims}D " + ) + + # If RGB(A), last dim must be 3 or 4 + if n_img_dims == 3: + if not (curr_arr.shape[-1] == 3 or curr_arr.shape[-1] == 4): + raise ValueError( + f"Expected size 3 or 4 for last dimension of RGB(A) array, got: {curr_arr.shape[-1]}." + ) + + n_scrollable_dims = len(curr_arr.shape) - n_img_dims + + if n_scrollable_dims not in SCROLLABLE_DIMS_ORDER.keys(): + raise ValueError(f"Array had shape {curr_arr.shape} which is not supported") + + return n_scrollable_dims + @current_index.setter def current_index(self, index: Dict[str, int]): # ignore if output context has not been created yet @@ -223,30 +286,29 @@ def current_index(self, index: Dict[str, int]): def __init__( self, data: Union[np.ndarray, List[np.ndarray]], - dims_order: Union[str, Dict[int, str]] = None, - slider_dims: Union[str, int, List[Union[str, int]]] = None, window_funcs: Union[int, Dict[str, int]] = None, frame_apply: Union[callable, Dict[int, callable]] = None, grid_shape: Tuple[int, int] = None, names: List[str] = None, grid_plot_kwargs: dict = None, histogram_widget: bool = True, + rgb: list[bool] = None, **kwargs, ): """ - A high level widget for displaying n-dimensional image data in conjunction with automatically generated - sliders for navigating through 1-2 selected dimensions within image data. - - Can display a single n-dimensional image array or a grid of n-dimensional images. + This widget facilitates high-level navigation through image stacks, which are arrays containing one or more + images. It includes sliders for key dimensions such as "t" (time) and "z", enabling users to smoothly navigate + through one or multiple image stacks simultaneously. - Default dimension orders: + Allowed dimensions orders for each image stack: Note that each has a an optional (c) channel which refers to + RGB(A) a channel. So this channel should be either 3 or 4. ======= ========== n_dims dims order ======= ========== - 2 "xy" - 3 "txy" - 4 "tzxy" + 2 "xy(c)" + 3 "txy(c)" + 4 "tzxy(c)" ======= ========== Parameters @@ -254,31 +316,18 @@ def __init__( data: Union[np.ndarray, List[np.ndarray] array-like or a list of array-like - dims_order: Optional[Union[str, Dict[np.ndarray, str]]] - | ``str`` or a dict mapping to indicate dimension order - | a single ``str`` if ``data`` is a single array, or a list of arrays with the same dimension order - | examples: ``"xyt"``, ``"tzxy"`` - | ``dict`` mapping of ``{array_index: axis_order}`` if specific arrays have a non-default axes order. - | "array_index" is the position of the corresponding array in the data list. - | examples: ``{array_index: "tzxy", another_array_index: "xytz"}`` - - slider_dims: Optional[Union[str, int, List[Union[str, int]]]] - | The dimensions for which to create a slider - | can be a single ``str`` such as **"t"**, **"z"** or a numerical ``int`` that indexes the desired dimension - | can also be a list of ``str`` or ``int`` if multiple sliders are desired for multiple dimensions - | examples: ``"t"``, ``["t", "z"]`` - window_funcs: Dict[Union[int, str], int] - | average one or more dimensions using a given window - | if a slider exists for only one dimension this can be an ``int``. - | if multiple sliders exist, then it must be a `dict`` mapping in the form of: ``{dimension: window_size}`` - | dimension/axes can be specified using ``str`` such as "t", "z" etc. or ``int`` that indexes the dimension - | if window_size is not an odd number, adds 1 - | use ``None`` to disable averaging for a dimension, example: ``{"t": 5, "z": None}`` + | Apply function(s) with rolling windows along "t" and/or "z" dimensions of the `data` arrays. + | Pass a dict in the form: {dimension: (func, window_size)}, `func` must take a slice of the data array as the + | first argument and must take `axis` as a kwarg. + | Ex: mean along "t" dimension: {"t": (np.mean, 11)}, if `current_index` of "t" is 50, it will pass frames + | 45 to 55 to `np.mean` with `axis = 0`. + | Ex2: max along z dim: {"z": (np.max, 3)}, passes current, previous and next frame to `np.max` with `axis = 1` frame_apply: Union[callable, Dict[int, callable]] - | apply a function to slices of the array before displaying the frame - | pass a single function or a dict of functions to apply to each array individually + | Apply function(s) to `data` arrays before to generate final 2D image that is displayed. + | Ex: apply a spatial Gaussian filter + | Pass a single function or a dict of functions to apply to each array individually | examples: ``{array_index: to_grayscale}``, ``{0: to_grayscale, 2: threshold_img}`` | "array_index" is the position of the corresponding array in the data list. | if `window_funcs` is used, then this function is applied after `window_funcs` @@ -297,19 +346,27 @@ def __init__( histogram_widget: bool, default False make histogram LUT widget for each subplot + rgb: bool | list[bool], default None + Includes a True or False for each ``array`` in the ImageWidget, indicating whether images are displayed as + grayscale or RGB(A). + kwargs: Any passed to fastplotlib.graphics.Image """ - self._names = None # output context self._output = None + if _is_arraylike(data): + data = [data] + if isinstance(data, list): # verify that it's a list of np.ndarray if all([_is_arraylike(d) for d in data]): + + # Grid computations if grid_shape is None: grid_shape = calculate_gridshape(len(data)) @@ -320,17 +377,44 @@ def __init__( f"Invalid `grid_shape` passed, setting grid shape to: {grid_shape}" ) - _ndim = [d.ndim for d in data] + self._data: List[np.ndarray] = data - # verify that all image arrays have same number of dimensions - # sliders get messy otherwise - if not len(set(_ndim)) == 1: + # Establish number of image dimensions and number of scrollable dimensions for each array + if rgb is None: + rgb = [False] * len(self.data) + if rgb is bool: + rgb = [rgb] + if not isinstance(rgb, list): + raise TypeError( + f"rgb_disp parameter must be a list, a {type(rgb)} was provided" + ) + if not len(rgb) == len(self.data): raise ValueError( - f"Number of dimensions of all data arrays must match, your ndims are: {_ndim}" + f"rgb had length {len(rgb)} but there are {len(self.data)} data arrays; these must be equal" ) - self._data: List[np.ndarray] = data - self._ndim = self.data[0].ndim # all ndim must be same + self._rgb = rgb + + self._n_img_dims = [ + IMAGE_DIM_COUNTS[RGB_BOOL_MAP[self._rgb[i]]] + for i in range(len(self.data)) + ] + + self._n_scrollable_dims = [ + self._get_n_scrollable_dims(self.data[i], self._rgb[i]) + for i in range(len(self.data)) + ] + + # Define ndim of ImageWidget instance as largest number of scrollable dims + 2 (grayscale dimensions) + self._ndim = ( + max( + [ + self.n_scrollable_dims[i] + for i in range(len(self.n_scrollable_dims)) + ] + ) + + IMAGE_DIM_COUNTS[RGB_BOOL_MAP[False]] + ) if names is not None: if not all([isinstance(n, str) for n in names]): @@ -351,12 +435,6 @@ def __init__( f"You have passed the following types:\n" f"{[type(a) for a in data]}" ) - - elif _is_arraylike(data): - self._data = [data] - self._ndim = self.data[0].ndim - - grid_shape = calculate_gridshape(len(self._data)) else: raise TypeError( f"`data` must be an array-like type representing an n-dimensional image " @@ -364,149 +442,20 @@ def __init__( f"You have passed the following type {type(data)}" ) - # default dims order if not passed - # updated later if passed - self._dims_order: List[str] = [DEFAULT_DIMS_ORDER[self.ndim]] * len(self.data) - - if dims_order is not None: - if isinstance(dims_order, str): - dims_order = dims_order.lower() - if len(dims_order) != self.ndim: - raise ValueError( - f"number of dims '{len(dims_order)} passed to `dims_order` " - f"does not match ndim '{self.ndim}' of data" - ) - self._dims_order: List[str] = [dims_order] * len(self.data) - elif isinstance(dims_order, dict): - self._dims_order: List[str] = [DEFAULT_DIMS_ORDER[self.ndim]] * len( - self.data - ) - - # dict of {array_ix: dims_order_str} - for data_ix in list(dims_order.keys()): - if not isinstance(data_ix, int): - raise TypeError("`dims_order` dict keys must be ") - if len(dims_order[data_ix]) != self.ndim: - raise ValueError( - f"number of dims '{len(dims_order)} passed to `dims_order` " - f"does not match ndim '{self.ndim}' of data" - ) - _do = dims_order[data_ix].lower() - # make sure the same dims are present - if not set(_do) == set(DEFAULT_DIMS_ORDER[self.ndim]): - raise ValueError( - f"Invalid `dims_order` passed for one of your arrays, " - f"valid `dims_order` for given number of dimensions " - f"can only contain the following characters: " - f"{DEFAULT_DIMS_ORDER[self.ndim]}" - ) - try: - self.dims_order[data_ix] = _do - except Exception: - raise IndexError( - f"index {data_ix} out of bounds for `dims_order`, the bounds are 0 - {len(self.data)}" - ) - else: - raise TypeError( - f"`dims_order` must be a or , you have passed a: <{type(dims_order)}>" - ) - - if not len(self.dims_order[0]) == self.ndim: - raise ValueError( - f"Number of dims specified by `dims_order`: {len(self.dims_order[0])} does not" - f" match number of dimensions in the `data`: {self.ndim}" - ) - - ao = np.array([sorted(v) for v in self.dims_order]) - - if not np.all(ao == ao[0]): - raise ValueError( - f"`dims_order` for all arrays must contain the same combination of dimensions, your `dims_order` are: " - f"{self.dims_order}" - ) - - # if slider_dims not provided - if slider_dims is None: - # by default sliders are made for all dimensions except the last 2 - default_dim_names = {0: "t", 1: "z", 2: "c"} - slider_dims = list() - for dim in range(self.ndim - 2): - if dim in default_dim_names.keys(): - slider_dims.append(default_dim_names[dim]) - else: - slider_dims.append(f"{dim}") - - # slider for only one of the dimensions - if isinstance(slider_dims, (int, str)): - # if numerical dimension is specified - if isinstance(slider_dims, int): - ao = np.array([v for v in self.dims_order]) - if not np.all(ao == ao[0]): - raise ValueError( - f"`dims_order` for all arrays must be identical if passing in a `slider_dims` argument. " - f"Pass in a argument if the `dims_order` are different for each array." - ) - self._slider_dims: List[str] = [self.dims_order[0][slider_dims]] - - # if dimension specified by str - elif isinstance(slider_dims, str): - if slider_dims not in self.dims_order[0]: - raise ValueError( - f"if `slider_dims` is a , it must be a character found in `dims_order`. " - f"Your `dims_order` characters are: {set(self.dims_order[0])}." - ) - self._slider_dims: List[str] = [slider_dims] - - # multiple sliders, one for each dimension - elif isinstance(slider_dims, list): - self._slider_dims: List[str] = list() - - # make sure window_funcs and frame_apply are dicts if multiple sliders are desired - if (not isinstance(window_funcs, dict)) and (window_funcs is not None): - raise TypeError( - f"`window_funcs` must be a if multiple `slider_dims` are provided. You must specify the " - f"window for each dimension." - ) - if (not isinstance(frame_apply, dict)) and (frame_apply is not None): - raise TypeError( - f"`frame_apply` must be a if multiple `slider_dims` are provided. You must specify a " - f"function for each dimension." - ) - - for sdm in slider_dims: - if isinstance(sdm, int): - ao = np.array([v for v in self.dims_order]) - if not np.all(ao == ao[0]): - raise ValueError( - f"`dims_order` for all arrays must be identical if passing in a `slider_dims` argument. " - f"Pass in a argument if the `dims_order` are different for each array." - ) - # parse int to a str - self.slider_dims.append(self.dims_order[0][sdm]) - - elif isinstance(sdm, str): - if sdm not in self.dims_order[0]: - raise ValueError( - f"if `slider_dims` is a , it must be a character found in `dims_order`. " - f"Your `dims_order` characters are: {set(self.dims_order[0])}." - ) - self.slider_dims.append(sdm) - - else: - raise TypeError( - "If passing a list for `slider_dims` each element must be either an or " - ) - - else: - raise TypeError( - f"`slider_dims` must a , or , you have passed a: {type(slider_dims)}" - ) + # Sliders are made for all dimensions except the image dimensions + self._slider_dims = list() + max_scrollable = max( + [self.n_scrollable_dims[i] for i in range(len(self.n_scrollable_dims))] + ) + for dim in range(max_scrollable): + if dim in ALLOWED_SLIDER_DIMS.keys(): + self.slider_dims.append(ALLOWED_SLIDER_DIMS[dim]) self._frame_apply: Dict[int, callable] = dict() if frame_apply is not None: if callable(frame_apply): - self._frame_apply = {0: frame_apply} + self._frame_apply = frame_apply elif isinstance(frame_apply, dict): self._frame_apply: Dict[int, callable] = dict.fromkeys( @@ -537,13 +486,19 @@ def __init__( self._sliders: Dict[str, Any] = dict() - # get max bound for all data arrays for all dimensions - self._dims_max_bounds: Dict[str, int] = {k: np.inf for k in self.slider_dims} - for _dim in list(self._dims_max_bounds.keys()): - for array, order in zip(self.data, self.dims_order): - self._dims_max_bounds[_dim] = min( - self._dims_max_bounds[_dim], array.shape[order.index(_dim)] - ) + # get max bound for all data arrays for all slider dimensions and ensure compatibility across slider dims + self._dims_max_bounds: Dict[str, int] = {k: 0 for k in self.slider_dims} + for i, _dim in enumerate(list(self._dims_max_bounds.keys())): + for array, partition in zip(self.data, self.n_scrollable_dims): + if partition <= i: + continue + else: + if 0 < self._dims_max_bounds[_dim] != array.shape[i]: + raise ValueError(f"Two arrays differ along dimension {_dim}") + else: + self._dims_max_bounds[_dim] = max( + self._dims_max_bounds[_dim], array.shape[i] + ) grid_plot_kwargs_default = {"controller_ids": "sync"} if grid_plot_kwargs is None: @@ -557,6 +512,7 @@ def __init__( shape=grid_shape, **grid_plot_kwargs_default ) + self._histogram_widget = histogram_widget for data_ix, (d, subplot) in enumerate(zip(self.data, self.gridplot)): if self._names is not None: name = self._names[data_ix] @@ -570,7 +526,7 @@ def __init__( subplot.name = name subplot.set_title(name) - if histogram_widget: + if self._histogram_widget: hlut = HistogramLUT(data=d, image_graphic=ig, name="histogram_lut") subplot.docks["right"].add_graphic(hlut) @@ -607,62 +563,54 @@ def window_funcs(self) -> Dict[str, _WindowFunctions]: return self._window_funcs @window_funcs.setter - def window_funcs(self, sa: Union[int, Dict[str, int]]): - if sa is None: + def window_funcs(self, callable_dict: Dict[str, int]): + if callable_dict is None: self._window_funcs = None # force frame to update self.current_index = self.current_index return - # for a single dim - elif isinstance(sa, tuple): - if len(self.slider_dims) > 1: - raise TypeError( - "Must pass dict argument to window_funcs if using multiple sliders. See the docstring." - ) - if not callable(sa[0]) or not isinstance(sa[1], int): - raise TypeError( - "Tuple argument to `window_funcs` must be in the form of (func, window_size). See the docstring." + elif isinstance(callable_dict, dict): + if not set(callable_dict.keys()).issubset(ALLOWED_WINDOW_DIMS): + raise ValueError( + f"The only allowed keys to window funcs are {list(ALLOWED_WINDOW_DIMS)} " + f"Your window func passed in these keys: {list(callable_dict.keys())}" ) - - dim_str = self.slider_dims[0] - self._window_funcs = dict() - self._window_funcs[dim_str] = _WindowFunctions(self, *sa) - - # for multiple dims - elif isinstance(sa, dict): if not all( - [isinstance(_sa, tuple) or (_sa is None) for _sa in sa.values()] + [ + isinstance(_callable_dict, tuple) + for _callable_dict in callable_dict.values() + ] ): raise TypeError( "dict argument to `window_funcs` must be in the form of: " "`{dimension: (func, window_size)}`. " "See the docstring." ) - for v in sa.values(): - if v is not None: - if not callable(v[0]) or not ( - isinstance(v[1], int) or v[1] is None - ): - raise TypeError( - "dict argument to `window_funcs` must be in the form of: " - "`{dimension: (func, window_size)}`. " - "See the docstring." - ) + for v in callable_dict.values(): + if not callable(v[0]): + raise TypeError( + "dict argument to `window_funcs` must be in the form of: " + "`{dimension: (func, window_size)}`. " + "See the docstring." + ) + if not isinstance(v[1], int): + raise TypeError( + f"dict argument to `window_funcs` must be in the form of: " + "`{dimension: (func, window_size)}`. " + f"where window_size is integer. you passed in {v[1]} for window_size" + ) if not isinstance(self._window_funcs, dict): self._window_funcs = dict() - for k in list(sa.keys()): - if sa[k] is None: - self._window_funcs[k] = None - else: - self._window_funcs[k] = _WindowFunctions(self, *sa[k]) + for k in list(callable_dict.keys()): + self._window_funcs[k] = _WindowFunctions(self, *callable_dict[k]) else: raise TypeError( - f"`window_funcs` must be of type `int` if using a single slider or a dict if using multiple sliders. " - f"You have passed a {type(sa)}. See the docstring." + f"`window_funcs` must be either Nonetype or dict." + f"You have passed a {type(callable_dict)}. See the docstring." ) # force frame to update @@ -684,7 +632,7 @@ def _process_indices( dict in form of {dimension_index: slice_index} For example if an array has shape [1000, 30, 512, 512] corresponding to [t, z, x, y]: To get the 100th timepoint and 3rd z-plane pass: - {"t": 100, "z": 3}, or {0: 100, 1: 3} + {"t": 100, "z": 3} Returns ------- @@ -692,26 +640,32 @@ def _process_indices( array-like, 2D slice """ - indexer = [slice(None)] * self.ndim + + data_ix = None + for i in range(len(self.data)): + if self.data[i] is array: + data_ix = i + break numerical_dims = list() + + # Totally number of dimensions for this specific array + curr_ndim = self.data[data_ix].ndim + + # Initialize slices for each dimension of array + indexer = [slice(None)] * curr_ndim + + # Maps from n_scrollable_dims to one of "", "t", "tz", etc. + curr_scrollable_format = SCROLLABLE_DIMS_ORDER[self.n_scrollable_dims[data_ix]] for dim in list(slice_indices.keys()): - if isinstance(dim, str): - data_ix = None - for i in range(len(self.data)): - if self.data[i] is array: - data_ix = i - break - if data_ix is None: - raise ValueError(f"Given `array` not found in `self.data`") - # get axes order for that specific array - numerical_dim = self.dims_order[data_ix].index(dim) - else: - numerical_dim = dim + if dim not in curr_scrollable_format: + continue + # get axes order for that specific array + numerical_dim = curr_scrollable_format.index(dim) indices_dim = slice_indices[dim] - # takes care of averaging if it was specified + # takes care of index selection (window slicing) for this specific axis indices_dim = self._get_window_indices(data_ix, numerical_dim, indices_dim) # set the indices for this dimension @@ -724,9 +678,9 @@ def _process_indices( if self.window_funcs is not None: a = array for i, dim in enumerate(sorted(numerical_dims)): - dim_str = self.dims_order[data_ix][dim] + dim_str = curr_scrollable_format[dim] dim = dim - i # since we loose a dimension every iteration - _indexer = [slice(None)] * (self.ndim - i) + _indexer = [slice(None)] * (curr_ndim - i) _indexer[dim] = indexer[dim + i] # if the indexer is an int, this dim has no window func @@ -737,7 +691,6 @@ def _process_indices( func = self.window_funcs[dim_str].func window = a[tuple(_indexer)] a = func(window, axis=dim) - # a = np.mean(a[tuple(_indexer)], axis=dim) return a else: return array[tuple(indexer)] @@ -749,7 +702,7 @@ def _get_window_indices(self, data_ix, dim, indices_dim): else: ix = indices_dim - dim_str = self.dims_order[data_ix][dim] + dim_str = SCROLLABLE_DIMS_ORDER[self.n_scrollable_dims[data_ix]][dim] # if no window stuff specified for this dim if dim_str not in self.window_funcs.keys(): @@ -848,9 +801,11 @@ def set_data( self.sliders[key].value = 0 # set slider max according to new data - max_lengths = {"t": np.inf, "z": np.inf} + max_lengths = dict() + for scroll_dim in self.slider_dims: + max_lengths[scroll_dim] = np.inf - if isinstance(new_data, np.ndarray): + if _is_arraylike(new_data): new_data = [new_data] if len(self._data) != len(new_data): @@ -866,16 +821,24 @@ def set_data( f"does not equal current data ndim {current_array.ndim}" ) + # Computes the number of scrollable dims and also validates new_array + new_scrollable_dims = self._get_n_scrollable_dims(new_array, self._rgb[i]) + + if self.n_scrollable_dims[i] != new_scrollable_dims: + raise ValueError( + f"number of dimensions of data arrays must match number of dimensions of " + f"existing data arrays" + ) + # if checks pass, update with new data for i, (new_array, current_array, subplot) in enumerate( zip(new_data, self._data, self.gridplot) ): # check last two dims (x and y) to see if data shape is changing - old_data_shape = self._data[i].shape[-2:] + old_data_shape = self._data[i].shape[-self.n_img_dims[i] :] self._data[i] = new_array - if old_data_shape != new_array.shape[-2:]: - # make a new graphic with the new xy dims + if old_data_shape != new_array.shape[-self.n_img_dims[i] :]: frame = self._process_indices( new_array, slice_indices=self._current_index ) @@ -886,23 +849,31 @@ def set_data( # set hlut tool to use new graphic subplot.docks["right"]["histogram_lut"].image_graphic = new_graphic - # delete old graphic after setting hlut tool to new graphic # this ensures gc subplot.delete_graphic(graphic=subplot["image_widget_managed"]) subplot.insert_graphic(graphic=new_graphic) - if new_array.ndim > 2: - # to set max of time slider, txy or tzxy - max_lengths["t"] = min(max_lengths["t"], new_array.shape[0] - 1) - - if new_array.ndim > 3: # tzxy - max_lengths["z"] = min(max_lengths["z"], new_array.shape[1] - 1) + # Returns "", "t", or "tz" + curr_scrollable_format = SCROLLABLE_DIMS_ORDER[self.n_scrollable_dims[i]] + + for scroll_dim in self.slider_dims: + if scroll_dim in curr_scrollable_format: + new_length = new_array.shape[ + curr_scrollable_format.index(scroll_dim) + ] + if max_lengths[scroll_dim] == np.inf: + max_lengths[scroll_dim] = new_length + elif max_lengths[scroll_dim] != new_length: + raise ValueError( + f"New arrays have differing values along dim {scroll_dim}" + ) # set histogram widget - subplot.docks["right"]["histogram_lut"].set_data( - new_array, reset_vmin_vmax=reset_vmin_vmax - ) + if self._histogram_widget: + subplot.docks["right"]["histogram_lut"].set_data( + new_array, reset_vmin_vmax=reset_vmin_vmax + ) # set slider maxes # TODO: maybe make this stuff a property, like ndims, n_frames etc. and have it set the sliders From 25aa9e3b63c9881660f3cbbdd081e09483b286a3 Mon Sep 17 00:00:00 2001 From: Kushal Kolar Date: Fri, 12 Apr 2024 02:29:19 -0400 Subject: [PATCH 45/66] `Gridplot` -> `Figure` (#479) * figure refactor seems to work * update nbs w.r.t. new figure * update test util * black * remove lingering uses of gridplot * update API doc generation * update contributing * typo Co-authored-by: Caitlin Lewis <69729525+clewis7@users.noreply.github.com> * Iw refactor (#482) * fefactor imwidget, restrict to tzxy, txy, xy, added RGB(A) support (#459) * add new iw test screenshots * update api docs --------- Co-authored-by: Amol Pasarkar * figure refactor seems to work * update nbs w.r.t. new figure * update test util * black * remove lingering uses of gridplot * update API doc generation * update contributing * typos * update iw with FIgure * update api docs * black * update quickstart * update iw examples * update iw type annotations * default cmap iw * remove gp docs * update readme * forgot to delete more api docs * I need to run nbs before committing them * more docs --------- Co-authored-by: Caitlin Lewis <69729525+clewis7@users.noreply.github.com> Co-authored-by: Amol Pasarkar --- CONTRIBUTING.md | 36 +- README.md | 13 +- docs/source/api/graphic_features/Deleted.rst | 33 ++ docs/source/api/graphic_features/index.rst | 1 + docs/source/api/graphics/HeatmapGraphic.rst | 3 + docs/source/api/graphics/ImageGraphic.rst | 3 + docs/source/api/graphics/LineCollection.rst | 3 + docs/source/api/graphics/LineGraphic.rst | 3 + docs/source/api/graphics/LineStack.rst | 3 + docs/source/api/graphics/ScatterGraphic.rst | 3 + docs/source/api/graphics/TextGraphic.rst | 3 + docs/source/api/layouts/figure.rst | 44 +++ docs/source/api/layouts/gridplot.rst | 42 --- docs/source/api/layouts/plot.rst | 73 ----- docs/source/api/layouts/subplot.rst | 3 + .../api/selectors/LinearRegionSelector.rst | 3 + docs/source/api/selectors/LinearSelector.rst | 3 + docs/source/api/selectors/PolygonSelector.rst | 3 + docs/source/api/widgets/ImageWidget.rst | 2 +- docs/source/generate_api.py | 13 +- docs/source/index.rst | 3 +- docs/source/quickstart.ipynb | 203 ++++++------ examples/desktop/gridplot/gridplot.py | 18 +- .../desktop/gridplot/gridplot_non_square.py | 16 +- examples/desktop/heatmap/heatmap.py | 13 +- examples/desktop/heatmap/heatmap_cmap.py | 13 +- examples/desktop/heatmap/heatmap_data.py | 13 +- examples/desktop/heatmap/heatmap_vmin_vmax.py | 13 +- examples/desktop/image/image_cmap.py | 15 +- examples/desktop/image/image_rgb.py | 14 +- examples/desktop/image/image_rgbvminvmax.py | 14 +- examples/desktop/image/image_simple.py | 12 +- examples/desktop/image/image_vminvmax.py | 12 +- examples/desktop/image/image_widget.py | 3 +- examples/desktop/line/line.py | 16 +- examples/desktop/line/line_cmap.py | 12 +- examples/desktop/line/line_colorslice.py | 16 +- examples/desktop/line/line_dataslice.py | 16 +- examples/desktop/line/line_present_scaling.py | 16 +- .../line_collection/line_collection.py | 10 +- .../line_collection_cmap_values.py | 10 +- ...line_collection_cmap_values_qualitative.py | 10 +- .../line_collection/line_collection_colors.py | 10 +- .../desktop/line_collection/line_stack.py | 10 +- examples/desktop/scatter/scatter.py | 12 +- examples/desktop/scatter/scatter_cmap.py | 15 +- .../desktop/scatter/scatter_colorslice.py | 12 +- examples/desktop/scatter/scatter_dataslice.py | 12 +- examples/desktop/scatter/scatter_present.py | 12 +- examples/desktop/scatter/scatter_size.py | 14 +- examples/notebooks/heatmap.ipynb | 6 +- examples/notebooks/image_widget.ipynb | 6 +- examples/notebooks/image_widget_test.ipynb | 80 ++--- .../notebooks/linear_region_selector.ipynb | 41 ++- examples/notebooks/linear_selector.ipynb | 14 +- examples/notebooks/lineplot.ipynb | 24 +- examples/notebooks/lines_cmap.ipynb | 32 +- .../multiprocessing_zmq_plot.ipynb | 12 +- examples/notebooks/nb_test_utils.py | 6 +- examples/notebooks/scatter.ipynb | 26 +- .../notebooks/scatter_sizes_animation.ipynb | 52 +-- examples/notebooks/scatter_sizes_grid.ipynb | 22 +- examples/notebooks/simple.ipynb | 229 +++++++------ .../{gridplot.ipynb => subplots.ipynb} | 80 +++-- ...lot_simple.ipynb => subplots_simple.ipynb} | 85 +++-- examples/notebooks/test_gc.ipynb | 16 +- examples/tests/test_examples.py | 2 +- fastplotlib/__init__.py | 10 +- fastplotlib/layouts/__init__.py | 5 +- .../layouts/{_gridplot.py => _figure.py} | 307 +++++++++++++++++- fastplotlib/layouts/_frame/__init__.py | 1 - fastplotlib/layouts/_frame/_frame.py | 152 --------- fastplotlib/layouts/_frame/_qt_toolbar.py | 250 -------------- fastplotlib/layouts/_plot.py | 68 ---- fastplotlib/layouts/_plot_area.py | 12 +- fastplotlib/layouts/_record_mixin.py | 241 -------------- fastplotlib/layouts/_subplot.py | 14 +- fastplotlib/layouts/_video_writer.py | 82 +++++ fastplotlib/layouts/output/__init__.py | 0 .../{_frame => output}/_ipywidget_toolbar.py | 175 ++-------- fastplotlib/layouts/output/_qt_toolbar.py | 125 +++++++ .../{_frame => output}/_qtoolbar_template.py | 0 .../layouts/{_frame => output}/_toolbar.py | 12 +- .../jupyter_output.py} | 0 .../_qt_output.py => output/qt_output.py} | 2 +- .../layouts/{_frame => output}/qtoolbar.ui | 0 fastplotlib/utils/functions.py | 2 +- .../_image_widget_ipywidget_toolbar.py | 135 ++++++++ .../widgets/_image_widget_qt_toolbar.py | 127 ++++++++ fastplotlib/widgets/image.py | 155 +++++---- tests/test_figure.py | 164 ++++++++++ tests/test_gridplot.py | 164 ---------- 92 files changed, 1839 insertions(+), 1952 deletions(-) create mode 100644 docs/source/api/graphic_features/Deleted.rst create mode 100644 docs/source/api/layouts/figure.rst delete mode 100644 docs/source/api/layouts/gridplot.rst delete mode 100644 docs/source/api/layouts/plot.rst rename examples/notebooks/{gridplot.ipynb => subplots.ipynb} (72%) rename examples/notebooks/{gridplot_simple.ipynb => subplots_simple.ipynb} (69%) rename fastplotlib/layouts/{_gridplot.py => _figure.py} (67%) delete mode 100644 fastplotlib/layouts/_frame/__init__.py delete mode 100644 fastplotlib/layouts/_frame/_frame.py delete mode 100644 fastplotlib/layouts/_frame/_qt_toolbar.py delete mode 100644 fastplotlib/layouts/_plot.py delete mode 100644 fastplotlib/layouts/_record_mixin.py create mode 100644 fastplotlib/layouts/_video_writer.py create mode 100644 fastplotlib/layouts/output/__init__.py rename fastplotlib/layouts/{_frame => output}/_ipywidget_toolbar.py (54%) create mode 100644 fastplotlib/layouts/output/_qt_toolbar.py rename fastplotlib/layouts/{_frame => output}/_qtoolbar_template.py (100%) rename fastplotlib/layouts/{_frame => output}/_toolbar.py (82%) rename fastplotlib/layouts/{_frame/_jupyter_output.py => output/jupyter_output.py} (100%) rename fastplotlib/layouts/{_frame/_qt_output.py => output/qt_output.py} (95%) rename fastplotlib/layouts/{_frame => output}/qtoolbar.ui (100%) create mode 100644 fastplotlib/widgets/_image_widget_ipywidget_toolbar.py create mode 100644 fastplotlib/widgets/_image_widget_qt_toolbar.py create mode 100644 tests/test_figure.py delete mode 100644 tests/test_gridplot.py diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index facca91db..fe9c90242 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -58,9 +58,9 @@ Fastplotlib uses the [`pygfx`](https://github.com/pygfx/pygfx) rendering engine plotting library. Some degree of familiarity with [`pygfx`](https://github.com/pygfx/pygfx) or rendering engines may be useful depending on the type of contribution you're working on. -There are currently 2 major subpackages within `fastplotlib`, `layouts` and `graphics`. The two user-facing public -classes within `layouts` are `Plot` and `GridPlot`. A user is intended to create either a `Plot` or `GridPlot`, and -then add *Graphics* to that layout, such as an `ImageGraphic`, `LineGraphic`, etc. +There are currently 2 major subpackages within `fastplotlib`, `layouts` and `graphics`. The user-facing public +class within `layouts` is `Figure`. A user is intended to create a `Figure`, and +then add *Graphics* to subplots within that `Figure`. ### Graphics @@ -70,7 +70,7 @@ fastplotlib graphics, such as `ImageGraphic`, `ScatterGraphic`, etc. inherit fro These might change in the future (ex. `Graphic.position_x` etc.). All graphics can be given a string name for the user's convenience. This allows graphics to be easily accessed from -plots, ex: `plot["some_image"]`. +plots, ex: `subplot["some_image"]`. All graphics contain a `world_object` property which is just the `pygfx.WorldObject` that this graphic uses. Fastplotlib keeps a *private* global dictionary of all `WorldObject` instances and users are only given a weakref proxy to this world object. @@ -119,8 +119,8 @@ after the aforementioned `Input` class PR in `pygfx` and after https://github.co #### PlotArea -This is the main base class within layouts. Every kind of "plot area", whether it's a single `Plot`, subplots within a -`GridPlot`, or `Dock` area, use `PlotArea` in some way. +This is the main base class within layouts. Subplots within a `Figure` and `Dock` areas within a `Subplot`, +inherit from `PlotArea`. `PlotArea` has the following key properties that allow it to be a "plot area" that can be used to view graphical objects: @@ -135,10 +135,10 @@ Abstract method that must be implemented in subclasses: * get_rect - musut return [x, y, width, height] that defines the viewport rect for this `PlotArea` -Properties specifically used by subplots in a gridplot: +Properties specifically used by subplots in a Figure: -* parent - A parent if relevant, used by individual `Subplots` in `GridPlot`, and by `Dock` which are "docked" subplots at the edges of a subplot. -* position - if a subplot within a gridplot, it is the position of this subplot within the `GridPlot` +* parent - A parent if relevant, used by individual `Subplots` in `Figure`, and by `Dock` which are "docked" subplots at the edges of a subplot. +* position - if a subplot within a Figure, it is the position of this subplot within the `Figure` Other important properties: @@ -182,18 +182,13 @@ Subplot has one property that is not in `PlotArea`: The key method in `Subplot` is an implementation of `get_rect` that returns the viewport rect for this subplot. -#### Plot, GridPlot, and Frame +#### Figure -Now that we have understood `PlotArea` and `Subplot` we need a way for the user to create either single plots or gridplots -and display them! +Now that we have understood `PlotArea` and `Subplot` we need a way for the user to create them! -There's one more class to talk about, `Frame`. This is a class that "frames" a `Plot` or `GridPlot`. Depending on -whether the plot's `Canvas` is a Qt or jupyter canvas, `Frame.show()` will create a plot toolbar and place this toolbar -below the `Canvas`. If using a glfw canvas it just returns the canvas. - -`Plot` and `GridPlot` both inherit from `Frame` which gives them `show()`. `Plot` is just a single `Subplot` with the -addition of `Frame`. `GridPlot.__init__` basically does a lot of parsing of user arguments to determine how to create -the subplots. All subplots within a `GridPlot` share the same canvas and use different viewports to create the subplots. +A `Figure` contains a grid of subplot and has methods such as `show()` to output the figure. +`Figure.__init__` basically does a lot of parsing of user arguments to determine how to create +the subplots. All subplots within a `Figure` share the same canvas and use different viewports to create the subplots. ## Tests in detail @@ -216,6 +211,9 @@ ground-truth image are within that tolerance the test will pass. To run tests: ```bash +# tests basic backend functionality +WGPU_FORCE_OFFSCREEN=1 pytest -v -s tests/ + # desktop examples pytest -v examples diff --git a/README.md b/README.md index 37a2d0779..64e1649e8 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,6 @@ [![CI](https://github.com/kushalkolar/fastplotlib/actions/workflows/ci.yml/badge.svg)](https://github.com/kushalkolar/fastplotlib/actions/workflows/ci.yml) [![PyPI version](https://badge.fury.io/py/fastplotlib.svg)](https://badge.fury.io/py/fastplotlib) [![Documentation Status](https://readthedocs.org/projects/fastplotlib/badge/?version=latest)](https://fastplotlib.readthedocs.io/en/latest/?badge=latest) -[![Gitter](https://badges.gitter.im/fastplotlib/community.svg)](https://gitter.im/fastplotlib/community?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) [**Installation**](https://github.com/kushalkolar/fastplotlib#installation) | [**GPU Drivers**](https://github.com/kushalkolar/fastplotlib#graphics-drivers) | @@ -15,7 +14,7 @@ [**Examples**](https://github.com/kushalkolar/fastplotlib#examples) | [**Contributing**](https://github.com/kushalkolar/fastplotlib#heart-contributing) -Next-gen plotting library built using the [`pygfx`](https://github.com/pygfx/pygfx) rendering engine that can utilize [Vulkan](https://en.wikipedia.org/wiki/Vulkan), [DX12](https://en.wikipedia.org/wiki/DirectX#DirectX_12), or [Metal](https://developer.apple.com/metal/) via WGPU, so it is very fast! We also aim to be an expressive plotting library that enables rapid prototyping for large scale explorative scientific visualization. +Next-gen plotting library built using the [`pygfx`](https://github.com/pygfx/pygfx) rendering engine that can utilize [Vulkan](https://en.wikipedia.org/wiki/Vulkan), [DX12](https://en.wikipedia.org/wiki/DirectX#DirectX_12), or [Metal](https://developer.apple.com/metal/) via WGPU, so it is very fast! `fastplotlib` also aims to be an expressive plotting library that enables rapid prototyping for large scale explorative scientific visualization. ![scipy-fpl](https://github.com/fastplotlib/fastplotlib/assets/9403332/b981a54c-05f9-443f-a8e4-52cd01cd802a) @@ -23,8 +22,8 @@ Next-gen plotting library built using the [`pygfx`](https://github.com/pygfx/pyg [![fpl_thumbnail](http://i3.ytimg.com/vi/Q-UJpAqljsU/hqdefault.jpg)](https://www.youtube.com/watch?v=Q-UJpAqljsU) -Notebooks from talk: https://github.com/fastplotlib/fastplotlib-scipy2023 - +Note that the API is currently evolving quickly. We recommend using the latest notebooks from the repo but the general +concepts are similar to those from the API shown in the video. # Supported frameworks @@ -36,9 +35,9 @@ Notebooks from talk: https://github.com/fastplotlib/fastplotlib-scipy2023 :heavy_check_mark: `wxPython` **Notes:**\ -:heavy_check_mark: Non-blocking Qt output is supported in ipython and notebooks by using [`%gui qt`](https://ipython.readthedocs.io/en/stable/interactive/magics.html#magic-gui) before creating plots. This hook only supports pyqt6 at the moment.\ +:heavy_check_mark: Non-blocking Qt/PySide output is supported in ipython and notebooks by using [`%gui qt`](https://ipython.readthedocs.io/en/stable/interactive/magics.html#magic-gui). This **must** be called *before* importing `fastplotlib`! :grey_exclamation: We do not officially support `jupyter notebook` through `jupyter_rfb`, this may change with notebook v7\ -:disappointed: [`jupyter_rfb`](https://github.com/vispy/jupyter_rfb) does not work in collab yet, see https://github.com/vispy/jupyter_rfb/pull/77 +:disappointed: [`jupyter_rfb`](https://github.com/vispy/jupyter_rfb) does not work in collab, see https://github.com/vispy/jupyter_rfb/pull/77 > **Note** > @@ -169,4 +168,4 @@ WGPU uses Metal instead of Vulkan on Mac. You will need at least Mac OSX 10.13. We welcome contributions! See the contributing guide: https://github.com/kushalkolar/fastplotlib/blob/main/CONTRIBUTING.md -You can also take a look at our [**Roadmap for 2024**](https://github.com/kushalkolar/fastplotlib/issues/55) and [**Issues**](https://github.com/kushalkolar/fastplotlib/issues) for ideas on how to contribute! +You can also take a look at our [**Roadmap for 2025**](https://github.com/kushalkolar/fastplotlib/issues/55) and [**Issues**](https://github.com/kushalkolar/fastplotlib/issues) for ideas on how to contribute! diff --git a/docs/source/api/graphic_features/Deleted.rst b/docs/source/api/graphic_features/Deleted.rst new file mode 100644 index 000000000..998e94588 --- /dev/null +++ b/docs/source/api/graphic_features/Deleted.rst @@ -0,0 +1,33 @@ +.. _api.Deleted: + +Deleted +******* + +======= +Deleted +======= +.. currentmodule:: fastplotlib.graphics._features + +Constructor +~~~~~~~~~~~ +.. autosummary:: + :toctree: Deleted_api + + Deleted + +Properties +~~~~~~~~~~ +.. autosummary:: + :toctree: Deleted_api + + +Methods +~~~~~~~ +.. autosummary:: + :toctree: Deleted_api + + Deleted.add_event_handler + Deleted.block_events + Deleted.clear_event_handlers + Deleted.remove_event_handler + diff --git a/docs/source/api/graphic_features/index.rst b/docs/source/api/graphic_features/index.rst index 1c4b33392..06e3119e5 100644 --- a/docs/source/api/graphic_features/index.rst +++ b/docs/source/api/graphic_features/index.rst @@ -20,3 +20,4 @@ Graphic Features to_gpu_supported_dtype LinearSelectionFeature LinearRegionSelectionFeature + Deleted diff --git a/docs/source/api/graphics/HeatmapGraphic.rst b/docs/source/api/graphics/HeatmapGraphic.rst index 3bd2f2baa..ffa86eb16 100644 --- a/docs/source/api/graphics/HeatmapGraphic.rst +++ b/docs/source/api/graphics/HeatmapGraphic.rst @@ -21,10 +21,12 @@ Properties :toctree: HeatmapGraphic_api HeatmapGraphic.children + HeatmapGraphic.name HeatmapGraphic.position HeatmapGraphic.position_x HeatmapGraphic.position_y HeatmapGraphic.position_z + HeatmapGraphic.rotation HeatmapGraphic.visible HeatmapGraphic.world_object @@ -37,5 +39,6 @@ Methods HeatmapGraphic.add_linear_selector HeatmapGraphic.link HeatmapGraphic.reset_feature + HeatmapGraphic.rotate HeatmapGraphic.set_feature diff --git a/docs/source/api/graphics/ImageGraphic.rst b/docs/source/api/graphics/ImageGraphic.rst index 871462701..00b27340d 100644 --- a/docs/source/api/graphics/ImageGraphic.rst +++ b/docs/source/api/graphics/ImageGraphic.rst @@ -21,10 +21,12 @@ Properties :toctree: ImageGraphic_api ImageGraphic.children + ImageGraphic.name ImageGraphic.position ImageGraphic.position_x ImageGraphic.position_y ImageGraphic.position_z + ImageGraphic.rotation ImageGraphic.visible ImageGraphic.world_object @@ -37,5 +39,6 @@ Methods ImageGraphic.add_linear_selector ImageGraphic.link ImageGraphic.reset_feature + ImageGraphic.rotate ImageGraphic.set_feature diff --git a/docs/source/api/graphics/LineCollection.rst b/docs/source/api/graphics/LineCollection.rst index 3f67feed9..8d10d8376 100644 --- a/docs/source/api/graphics/LineCollection.rst +++ b/docs/source/api/graphics/LineCollection.rst @@ -24,10 +24,12 @@ Properties LineCollection.cmap LineCollection.cmap_values LineCollection.graphics + LineCollection.name LineCollection.position LineCollection.position_x LineCollection.position_y LineCollection.position_z + LineCollection.rotation LineCollection.visible LineCollection.world_object @@ -42,5 +44,6 @@ Methods LineCollection.link LineCollection.remove_graphic LineCollection.reset_feature + LineCollection.rotate LineCollection.set_feature diff --git a/docs/source/api/graphics/LineGraphic.rst b/docs/source/api/graphics/LineGraphic.rst index 4aae4bbee..8b6fedf22 100644 --- a/docs/source/api/graphics/LineGraphic.rst +++ b/docs/source/api/graphics/LineGraphic.rst @@ -21,10 +21,12 @@ Properties :toctree: LineGraphic_api LineGraphic.children + LineGraphic.name LineGraphic.position LineGraphic.position_x LineGraphic.position_y LineGraphic.position_z + LineGraphic.rotation LineGraphic.visible LineGraphic.world_object @@ -37,5 +39,6 @@ Methods LineGraphic.add_linear_selector LineGraphic.link LineGraphic.reset_feature + LineGraphic.rotate LineGraphic.set_feature diff --git a/docs/source/api/graphics/LineStack.rst b/docs/source/api/graphics/LineStack.rst index 36ae6808e..a39db46f8 100644 --- a/docs/source/api/graphics/LineStack.rst +++ b/docs/source/api/graphics/LineStack.rst @@ -24,10 +24,12 @@ Properties LineStack.cmap LineStack.cmap_values LineStack.graphics + LineStack.name LineStack.position LineStack.position_x LineStack.position_y LineStack.position_z + LineStack.rotation LineStack.visible LineStack.world_object @@ -42,5 +44,6 @@ Methods LineStack.link LineStack.remove_graphic LineStack.reset_feature + LineStack.rotate LineStack.set_feature diff --git a/docs/source/api/graphics/ScatterGraphic.rst b/docs/source/api/graphics/ScatterGraphic.rst index 3c4bf3909..44d87d008 100644 --- a/docs/source/api/graphics/ScatterGraphic.rst +++ b/docs/source/api/graphics/ScatterGraphic.rst @@ -21,10 +21,12 @@ Properties :toctree: ScatterGraphic_api ScatterGraphic.children + ScatterGraphic.name ScatterGraphic.position ScatterGraphic.position_x ScatterGraphic.position_y ScatterGraphic.position_z + ScatterGraphic.rotation ScatterGraphic.visible ScatterGraphic.world_object @@ -33,4 +35,5 @@ Methods .. autosummary:: :toctree: ScatterGraphic_api + ScatterGraphic.rotate diff --git a/docs/source/api/graphics/TextGraphic.rst b/docs/source/api/graphics/TextGraphic.rst index 6290dcc2e..23425cf41 100644 --- a/docs/source/api/graphics/TextGraphic.rst +++ b/docs/source/api/graphics/TextGraphic.rst @@ -22,12 +22,14 @@ Properties TextGraphic.children TextGraphic.face_color + TextGraphic.name TextGraphic.outline_color TextGraphic.outline_size TextGraphic.position TextGraphic.position_x TextGraphic.position_y TextGraphic.position_z + TextGraphic.rotation TextGraphic.text TextGraphic.text_size TextGraphic.visible @@ -38,4 +40,5 @@ Methods .. autosummary:: :toctree: TextGraphic_api + TextGraphic.rotate diff --git a/docs/source/api/layouts/figure.rst b/docs/source/api/layouts/figure.rst new file mode 100644 index 000000000..a2d5e5758 --- /dev/null +++ b/docs/source/api/layouts/figure.rst @@ -0,0 +1,44 @@ +.. _api.Figure: + +Figure +****** + +====== +Figure +====== +.. currentmodule:: fastplotlib + +Constructor +~~~~~~~~~~~ +.. autosummary:: + :toctree: Figure_api + + Figure + +Properties +~~~~~~~~~~ +.. autosummary:: + :toctree: Figure_api + + Figure.cameras + Figure.canvas + Figure.controllers + Figure.names + Figure.output + Figure.renderer + Figure.shape + Figure.toolbar + +Methods +~~~~~~~ +.. autosummary:: + :toctree: Figure_api + + Figure.add_animations + Figure.clear + Figure.close + Figure.remove_animation + Figure.render + Figure.show + Figure.start_render + diff --git a/docs/source/api/layouts/gridplot.rst b/docs/source/api/layouts/gridplot.rst deleted file mode 100644 index b5b03bfa4..000000000 --- a/docs/source/api/layouts/gridplot.rst +++ /dev/null @@ -1,42 +0,0 @@ -.. _api.GridPlot: - -GridPlot -******** - -======== -GridPlot -======== -.. currentmodule:: fastplotlib - -Constructor -~~~~~~~~~~~ -.. autosummary:: - :toctree: GridPlot_api - - GridPlot - -Properties -~~~~~~~~~~ -.. autosummary:: - :toctree: GridPlot_api - - GridPlot.canvas - GridPlot.renderer - GridPlot.toolbar - GridPlot.widget - -Methods -~~~~~~~ -.. autosummary:: - :toctree: GridPlot_api - - GridPlot.add_animations - GridPlot.clear - GridPlot.close - GridPlot.record_start - GridPlot.record_stop - GridPlot.remove_animation - GridPlot.render - GridPlot.show - GridPlot.start_render - diff --git a/docs/source/api/layouts/plot.rst b/docs/source/api/layouts/plot.rst deleted file mode 100644 index bd38720b4..000000000 --- a/docs/source/api/layouts/plot.rst +++ /dev/null @@ -1,73 +0,0 @@ -.. _api.Plot: - -Plot -**** - -==== -Plot -==== -.. currentmodule:: fastplotlib - -Constructor -~~~~~~~~~~~ -.. autosummary:: - :toctree: Plot_api - - Plot - -Properties -~~~~~~~~~~ -.. autosummary:: - :toctree: Plot_api - - Plot.camera - Plot.canvas - Plot.controller - Plot.docks - Plot.graphics - Plot.name - Plot.parent - Plot.position - Plot.renderer - Plot.scene - Plot.selectors - Plot.toolbar - Plot.viewport - Plot.widget - -Methods -~~~~~~~ -.. autosummary:: - :toctree: Plot_api - - Plot.add_animations - Plot.add_graphic - Plot.add_heatmap - Plot.add_image - Plot.add_line - Plot.add_line_collection - Plot.add_line_stack - Plot.add_scatter - Plot.add_text - Plot.auto_scale - Plot.center_graphic - Plot.center_scene - Plot.center_title - Plot.clear - Plot.close - Plot.delete_graphic - Plot.get_rect - Plot.insert_graphic - Plot.map_screen_to_world - Plot.record_start - Plot.record_stop - Plot.remove_animation - Plot.remove_graphic - Plot.render - Plot.set_axes_visibility - Plot.set_grid_visibility - Plot.set_title - Plot.set_viewport_rect - Plot.show - Plot.start_render - diff --git a/docs/source/api/layouts/subplot.rst b/docs/source/api/layouts/subplot.rst index c61c46e05..91884557a 100644 --- a/docs/source/api/layouts/subplot.rst +++ b/docs/source/api/layouts/subplot.rst @@ -25,7 +25,9 @@ Properties Subplot.controller Subplot.docks Subplot.graphics + Subplot.legends Subplot.name + Subplot.objects Subplot.parent Subplot.position Subplot.renderer @@ -54,6 +56,7 @@ Methods Subplot.clear Subplot.delete_graphic Subplot.get_rect + Subplot.get_refcounts Subplot.insert_graphic Subplot.map_screen_to_world Subplot.remove_animation diff --git a/docs/source/api/selectors/LinearRegionSelector.rst b/docs/source/api/selectors/LinearRegionSelector.rst index ce0d8d9b6..1b59e80c9 100644 --- a/docs/source/api/selectors/LinearRegionSelector.rst +++ b/docs/source/api/selectors/LinearRegionSelector.rst @@ -22,10 +22,12 @@ Properties LinearRegionSelector.children LinearRegionSelector.limits + LinearRegionSelector.name LinearRegionSelector.position LinearRegionSelector.position_x LinearRegionSelector.position_y LinearRegionSelector.position_z + LinearRegionSelector.rotation LinearRegionSelector.visible LinearRegionSelector.world_object @@ -39,4 +41,5 @@ Methods LinearRegionSelector.get_selected_index LinearRegionSelector.get_selected_indices LinearRegionSelector.make_ipywidget_slider + LinearRegionSelector.rotate diff --git a/docs/source/api/selectors/LinearSelector.rst b/docs/source/api/selectors/LinearSelector.rst index 4056bcc46..3278559d0 100644 --- a/docs/source/api/selectors/LinearSelector.rst +++ b/docs/source/api/selectors/LinearSelector.rst @@ -22,10 +22,12 @@ Properties LinearSelector.children LinearSelector.limits + LinearSelector.name LinearSelector.position LinearSelector.position_x LinearSelector.position_y LinearSelector.position_z + LinearSelector.rotation LinearSelector.visible LinearSelector.world_object @@ -39,4 +41,5 @@ Methods LinearSelector.get_selected_index LinearSelector.get_selected_indices LinearSelector.make_ipywidget_slider + LinearSelector.rotate diff --git a/docs/source/api/selectors/PolygonSelector.rst b/docs/source/api/selectors/PolygonSelector.rst index aaa434dbf..8de87ec74 100644 --- a/docs/source/api/selectors/PolygonSelector.rst +++ b/docs/source/api/selectors/PolygonSelector.rst @@ -21,10 +21,12 @@ Properties :toctree: PolygonSelector_api PolygonSelector.children + PolygonSelector.name PolygonSelector.position PolygonSelector.position_x PolygonSelector.position_y PolygonSelector.position_z + PolygonSelector.rotation PolygonSelector.visible PolygonSelector.world_object @@ -37,4 +39,5 @@ Methods PolygonSelector.get_selected_index PolygonSelector.get_selected_indices PolygonSelector.get_vertices + PolygonSelector.rotate diff --git a/docs/source/api/widgets/ImageWidget.rst b/docs/source/api/widgets/ImageWidget.rst index 2b4708007..3ca384968 100644 --- a/docs/source/api/widgets/ImageWidget.rst +++ b/docs/source/api/widgets/ImageWidget.rst @@ -23,8 +23,8 @@ Properties ImageWidget.cmap ImageWidget.current_index ImageWidget.data + ImageWidget.figure ImageWidget.frame_apply - ImageWidget.gridplot ImageWidget.managed_graphics ImageWidget.n_img_dims ImageWidget.n_scrollable_dims diff --git a/docs/source/generate_api.py b/docs/source/generate_api.py index 19b739d1b..a5f668130 100644 --- a/docs/source/generate_api.py +++ b/docs/source/generate_api.py @@ -142,17 +142,10 @@ def generate_page( def main(): generate_page( - page_name="Plot", - classes=[fastplotlib.Plot], + page_name="Figure", + classes=[fastplotlib.Figure], modules=["fastplotlib"], - source_path=LAYOUTS_DIR.joinpath("plot.rst"), - ) - - generate_page( - page_name="GridPlot", - classes=[fastplotlib.GridPlot], - modules=["fastplotlib"], - source_path=LAYOUTS_DIR.joinpath("gridplot.rst"), + source_path=LAYOUTS_DIR.joinpath("figure.rst"), ) generate_page( diff --git a/docs/source/index.rst b/docs/source/index.rst index 9dbd30783..0ceb146e4 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -22,8 +22,7 @@ Welcome to fastplotlib's documentation! :maxdepth: 1 :caption: API - Plot - Gridplot + Figure Subplot Graphics Graphic Features diff --git a/docs/source/quickstart.ipynb b/docs/source/quickstart.ipynb index 0de4667bf..6a892399e 100644 --- a/docs/source/quickstart.ipynb +++ b/docs/source/quickstart.ipynb @@ -79,17 +79,17 @@ }, "outputs": [], "source": [ - "# create a `Plot` instance\n", - "plot = fpl.Plot()\n", + "# create a `Figure` instance\n", + "fig = fpl.Figure()\n", "\n", "# get a grayscale image\n", "data = iio.imread(\"imageio:camera.png\")\n", "\n", "# plot the image data\n", - "image_graphic = plot.add_image(data=data, name=\"sample-image\")\n", + "image_graphic = fig[0, 0].add_image(data=data, name=\"sample-image\")\n", "\n", "# show the plot\n", - "plot.show()" + "fig.show()" ] }, { @@ -117,7 +117,7 @@ }, "outputs": [], "source": [ - "plot.canvas.snapshot()" + "fig.canvas.snapshot()" ] }, { @@ -149,7 +149,7 @@ }, "outputs": [], "source": [ - "plot.canvas.snapshot()" + "fig.canvas.snapshot()" ] }, { @@ -198,7 +198,7 @@ }, "outputs": [], "source": [ - "plot.canvas.snapshot()" + "fig.canvas.snapshot()" ] }, { @@ -230,7 +230,7 @@ }, "outputs": [], "source": [ - "plot.canvas.snapshot()" + "fig.canvas.snapshot()" ] }, { @@ -263,7 +263,7 @@ }, "outputs": [], "source": [ - "plot.canvas.snapshot()" + "fig.canvas.snapshot()" ] }, { @@ -331,7 +331,7 @@ }, "outputs": [], "source": [ - "plot.canvas.snapshot()" + "fig.canvas.snapshot()" ] }, { @@ -363,7 +363,7 @@ }, "outputs": [], "source": [ - "plot.canvas.snapshot()" + "fig.canvas.snapshot()" ] }, { @@ -391,7 +391,7 @@ }, "outputs": [], "source": [ - "plot" + "fig" ] }, { @@ -403,7 +403,7 @@ }, "outputs": [], "source": [ - "plot[\"sample-image\"]" + "fig[0, 0][\"sample-image\"]" ] }, { @@ -423,7 +423,7 @@ }, "outputs": [], "source": [ - "plot.graphics" + "fig[0, 0].graphics" ] }, { @@ -435,7 +435,7 @@ }, "outputs": [], "source": [ - "plot.graphics[0]" + "fig[0, 0].graphics[0]" ] }, { @@ -467,7 +467,7 @@ }, "outputs": [], "source": [ - "image_graphic == plot[\"sample-image\"]" + "image_graphic is fig[0, 0][\"sample-image\"]" ] }, { @@ -489,11 +489,11 @@ }, "outputs": [], "source": [ - "plot_rgb = fpl.Plot()\n", + "fig_rgb = fpl.Figure()\n", "\n", - "plot_rgb.add_image(new_data, name=\"rgb-image\")\n", + "fig_rgb[0, 0].add_image(new_data, name=\"rgb-image\")\n", "\n", - "plot_rgb.show()" + "fig_rgb.show()" ] }, { @@ -505,7 +505,7 @@ }, "outputs": [], "source": [ - "plot_rgb.canvas.snapshot()" + "fig_rgb.canvas.snapshot()" ] }, { @@ -525,7 +525,7 @@ }, "outputs": [], "source": [ - "plot_rgb[\"rgb-image\"].cmap.vmin = 100" + "fig_rgb[0, 0][\"rgb-image\"].cmap.vmin = 100" ] }, { @@ -537,7 +537,7 @@ }, "outputs": [], "source": [ - "plot_rgb.canvas.snapshot()" + "fig_rgb.canvas.snapshot()" ] }, { @@ -561,28 +561,26 @@ }, "outputs": [], "source": [ - "# create another `Plot` instance\n", - "plot_v = fpl.Plot()\n", - "\n", - "plot.canvas.max_buffered_frames = 1\n", + "# create another `Figure` instance\n", + "fig_vid = fpl.Figure()\n", "\n", "# make some random data again\n", "data = np.random.rand(512, 512)\n", "\n", "# plot the data\n", - "plot_v.add_image(data=data, name=\"random-image\")\n", + "fig_vid[0, 0].add_image(data=data, name=\"random-image\")\n", "\n", "# a function to update the image_graphic\n", - "# a plot will pass its plot instance to the animation function as an argument\n", - "def update_data(plot_instance):\n", + "# a subplot will pass its instance to the animation function as an argument\n", + "def update_data(subplot):\n", " new_data = np.random.rand(512, 512)\n", - " plot_instance[\"random-image\"].data = new_data\n", + " subplot[\"random-image\"].data = new_data\n", "\n", - "#add this as an animation function\n", - "plot_v.add_animations(update_data)\n", + "#add this as an animation function to the subplot\n", + "fig_vid[0, 0].add_animations(update_data)\n", "\n", "# show the plot\n", - "plot_v.show()" + "fig_vid.show()" ] }, { @@ -602,11 +600,11 @@ "metadata": {}, "outputs": [], "source": [ - "plot_sync = fpl.Plot(controller=plot_v.controller)\n", + "fig_sync = fpl.Figure(controllers=fig_vid.controllers)\n", "\n", "data = np.random.rand(512, 512)\n", "\n", - "image_graphic_instance = plot_sync.add_image(data=data, cmap=\"viridis\")\n", + "image_graphic_instance = fig_sync[0, 0].add_image(data=data, cmap=\"viridis\")\n", "\n", "# you will need to define a new animation function for this graphic\n", "def update_data_2():\n", @@ -614,9 +612,10 @@ " # alternatively, you can use the stored reference to the graphic as well instead of indexing the Plot\n", " image_graphic_instance.data = new_data\n", "\n", - "plot_sync.add_animations(update_data_2)\n", + "# add the animation function to the figure instead of the subplot\n", + "fig_sync.add_animations(update_data_2)\n", "\n", - "plot_sync.show()" + "fig_sync.show()" ] }, { @@ -718,19 +717,19 @@ "outputs": [], "source": [ "# Create a plot instance\n", - "plot_l = fpl.Plot()\n", + "fig_line = fpl.Figure()\n", "\n", "# plot sine wave, use a single color\n", - "sine_graphic = plot_l.add_line(data=sine, thickness=5, colors=\"magenta\")\n", + "sine_graphic = fig_line[0, 0].add_line(data=sine, thickness=5, colors=\"magenta\")\n", "\n", "# you can also use colormaps for lines!\n", - "cosine_graphic = plot_l.add_line(data=cosine, thickness=12, cmap=\"autumn\")\n", + "cosine_graphic = fig_line[0, 0].add_line(data=cosine, thickness=12, cmap=\"autumn\")\n", "\n", "# or a list of colors for each datapoint\n", "colors = [\"r\"] * 25 + [\"purple\"] * 25 + [\"y\"] * 25 + [\"b\"] * 25\n", - "sinc_graphic = plot_l.add_line(data=sinc, thickness=5, colors = colors)\n", + "sinc_graphic = fig_line[0, 0].add_line(data=sinc, thickness=5, colors = colors)\n", "\n", - "plot_l.show()" + "fig_line.show()" ] }, { @@ -754,7 +753,7 @@ }, "outputs": [], "source": [ - "plot_l.camera.maintain_aspect = False" + "fig_line[0, 0].camera.maintain_aspect = False" ] }, { @@ -774,7 +773,7 @@ }, "outputs": [], "source": [ - "plot_l.auto_scale(maintain_aspect=True)" + "fig_line[0, 0].auto_scale(maintain_aspect=True)" ] }, { @@ -822,7 +821,7 @@ }, "outputs": [], "source": [ - "plot_l.canvas.snapshot()" + "fig_line.canvas.snapshot()" ] }, { @@ -868,7 +867,7 @@ }, "outputs": [], "source": [ - "plot_l.canvas.snapshot()" + "fig_line.canvas.snapshot()" ] }, { @@ -909,7 +908,7 @@ }, "outputs": [], "source": [ - "plot_l.canvas.snapshot()" + "fig_line.canvas.snapshot()" ] }, { @@ -939,7 +938,7 @@ }, "outputs": [], "source": [ - "plot_l.canvas.snapshot()" + "fig_line.canvas.snapshot()" ] }, { @@ -961,7 +960,7 @@ }, "outputs": [], "source": [ - "plot_l.canvas.snapshot()" + "fig_line.canvas.snapshot()" ] }, { @@ -979,7 +978,7 @@ "metadata": {}, "outputs": [], "source": [ - "sinc_graphic.present.add_event_handler(plot_l.auto_scale)" + "sinc_graphic.present.add_event_handler(fig_line[0, 0].auto_scale)" ] }, { @@ -1001,7 +1000,7 @@ }, "outputs": [], "source": [ - "plot_l.canvas.snapshot()" + "fig_line.canvas.snapshot()" ] }, { @@ -1023,7 +1022,7 @@ }, "outputs": [], "source": [ - "plot_l.canvas.snapshot()" + "fig_line.canvas.snapshot()" ] }, { @@ -1045,11 +1044,11 @@ "source": [ "img = np.random.rand(20, 100)\n", "\n", - "plot_l.add_image(img, name=\"image\", cmap=\"gray\")\n", + "fig_line[0, 0].add_image(img, name=\"image\", cmap=\"gray\")\n", "\n", "# z axis position -1 so it is below all the lines\n", - "plot_l[\"image\"].position_z = -1\n", - "plot_l[\"image\"].position_x = -50" + "fig_line[0, 0][\"image\"].position_z = -1\n", + "fig_line[0, 0][\"image\"].position_x = -50" ] }, { @@ -1061,7 +1060,7 @@ }, "outputs": [], "source": [ - "plot_l.canvas.snapshot()" + "fig_line.canvas.snapshot()" ] }, { @@ -1080,7 +1079,7 @@ "outputs": [], "source": [ "# just set the camera as \"3d\", the rest is basically the same :D \n", - "plot_l3d = fpl.Plot(camera='3d')\n", + "fig_line_3d = fpl.Figure(cameras='3d')\n", "\n", "# create a spiral\n", "phi = np.linspace(0, 30, 200)\n", @@ -1093,9 +1092,9 @@ "# note: you usually mix 3D and 2D graphics on the same plot\n", "spiral = np.dstack([xs, ys, zs])[0]\n", "\n", - "plot_l3d.add_line(data=spiral, thickness=2, cmap='winter')\n", + "fig_line_3d[0, 0].add_line(data=spiral, thickness=2, cmap='winter')\n", "\n", - "plot_l3d.show()" + "fig_line_3d.show()" ] }, { @@ -1107,7 +1106,7 @@ }, "outputs": [], "source": [ - "plot_l3d.auto_scale(maintain_aspect=True)" + "fig_line_3d[0, 0].auto_scale(maintain_aspect=True)" ] }, { @@ -1157,12 +1156,12 @@ "colors = [\"yellow\"] * n_points + [\"cyan\"] * n_points + [\"magenta\"] * n_points\n", "\n", "# create plot\n", - "plot_s = fpl.Plot()\n", + "fig_scatter = fpl.Figure()\n", "\n", "# use an alpha value since this will be a lot of points\n", - "scatter_graphic = plot_s.add_scatter(data=cloud, sizes=3, colors=colors, alpha=0.7)\n", + "scatter_graphic = fig_scatter[0, 0].add_scatter(data=cloud, sizes=3, colors=colors, alpha=0.7)\n", "\n", - "plot_s.show()" + "fig_scatter.show()" ] }, { @@ -1193,7 +1192,7 @@ }, "outputs": [], "source": [ - "plot_s.canvas.snapshot()" + "fig_scatter.canvas.snapshot()" ] }, { @@ -1216,7 +1215,7 @@ }, "outputs": [], "source": [ - "plot_s.canvas.snapshot()" + "fig_scatter.canvas.snapshot()" ] }, { @@ -1239,7 +1238,7 @@ }, "outputs": [], "source": [ - "plot_s.canvas.snapshot()" + "fig_scatter.canvas.snapshot()" ] }, { @@ -1262,7 +1261,7 @@ }, "outputs": [], "source": [ - "plot_s.canvas.snapshot()" + "fig_scatter.canvas.snapshot()" ] }, { @@ -1285,7 +1284,7 @@ }, "outputs": [], "source": [ - "plot_s.canvas.snapshot()" + "fig_scatter.canvas.snapshot()" ] }, { @@ -1318,11 +1317,7 @@ "id": "a26c0063-b7e0-4f36-bb14-db06bafa31aa", "metadata": {}, "source": [ - "## Gridplot\n", - "\n", - "Subplots within a `GridPlot` behave the same as simple `Plot` instances! \n", - "\n", - "💡 `Plot` is actually a subclass of `Subplot`!" + "## More subplots" ] }, { @@ -1334,11 +1329,11 @@ }, "outputs": [], "source": [ - "# GridPlot of shape 2 x 3 with all controllers synced\n", - "grid_plot = fpl.GridPlot(shape=(2, 3), controller_ids=\"sync\")\n", + "# Figure of shape 2 x 3 with all controllers synced\n", + "figure_grid = fpl.Figure(shape=(2, 3), controller_ids=\"sync\")\n", "\n", "# Make a random image graphic for each subplot\n", - "for subplot in grid_plot:\n", + "for subplot in figure_grid:\n", " # create image data\n", " data = np.random.rand(512, 512)\n", " # add an image to the subplot\n", @@ -1346,17 +1341,17 @@ "\n", "# Define a function to update the image graphics with new data\n", "# add_animations will pass the gridplot to the animation function\n", - "def update_data(gp):\n", - " for sp in gp:\n", + "def update_data(f):\n", + " for subplot in f:\n", " new_data = np.random.rand(512, 512)\n", " # index the image graphic by name and set the data\n", - " sp[\"rand-img\"].data = new_data\n", + " subplot[\"rand-img\"].data = new_data\n", " \n", "# add the animation function\n", - "grid_plot.add_animations(update_data)\n", + "figure_grid.add_animations(update_data)\n", "\n", "# show the gridplot \n", - "grid_plot.show()" + "figure_grid.show()" ] }, { @@ -1378,7 +1373,7 @@ "source": [ "# positional indexing\n", "# row 0 and col 0\n", - "grid_plot[0, 0]" + "figure_grid[0, 0]" ] }, { @@ -1398,7 +1393,7 @@ }, "outputs": [], "source": [ - "grid_plot[0, 1].graphics" + "figure_grid[0, 1].graphics" ] }, { @@ -1418,7 +1413,7 @@ }, "outputs": [], "source": [ - "grid_plot[0, 1].graphics[0].vmax = 0.5" + "figure_grid[0, 1].graphics[0].vmax = 0.5" ] }, { @@ -1439,7 +1434,7 @@ "outputs": [], "source": [ "# you can give subplots human-readable string names\n", - "grid_plot[0, 2].name = \"top-right-plot\"" + "figure_grid[0, 2].name = \"top-right-plot\"" ] }, { @@ -1451,7 +1446,7 @@ }, "outputs": [], "source": [ - "grid_plot[\"top-right-plot\"]" + "figure_grid[\"top-right-plot\"]" ] }, { @@ -1464,7 +1459,7 @@ "outputs": [], "source": [ "# view its position\n", - "grid_plot[\"top-right-plot\"].position" + "figure_grid[\"top-right-plot\"].position" ] }, { @@ -1477,7 +1472,7 @@ "outputs": [], "source": [ "# these are really the same\n", - "grid_plot[\"top-right-plot\"] is grid_plot[0, 2]" + "figure_grid[\"top-right-plot\"] is figure_grid[0, 2]" ] }, { @@ -1497,7 +1492,7 @@ }, "outputs": [], "source": [ - "grid_plot[\"top-right-plot\"][\"rand-img\"].vmin = 0.5" + "figure_grid[\"top-right-plot\"][\"rand-img\"].vmin = 0.5" ] }, { @@ -1505,7 +1500,7 @@ "id": "6a5b4368-ae4d-442c-a11f-45c70267339b", "metadata": {}, "source": [ - "## GridPlot customization" + "## Figure subplot customization" ] }, { @@ -1517,8 +1512,8 @@ }, "outputs": [], "source": [ - "# grid with 2 rows and 3 columns\n", - "grid_shape = (2, 3)\n", + "# 2 rows and 3 columns\n", + "shape = (2, 3)\n", "\n", "# pan-zoom controllers for each view\n", "# views are synced if they have the \n", @@ -1536,15 +1531,15 @@ "]\n", "\n", "# Create the grid plot\n", - "grid_plot = fpl.GridPlot(\n", - " shape=grid_shape,\n", + "figure_grid = fpl.Figure(\n", + " shape=shape,\n", " controller_ids=controller_ids,\n", " names=names,\n", ")\n", "\n", "\n", "# Make a random image graphic for each subplot\n", - "for subplot in grid_plot:\n", + "for subplot in figure_grid:\n", " data = np.random.rand(512, 512)\n", " # create and add an ImageGraphic\n", " subplot.add_image(data=data, name=\"rand-image\")\n", @@ -1558,8 +1553,8 @@ " subplot[\"rand-image\"].data = new_data\n", "\n", "# add the animation\n", - "grid_plot.add_animations(set_random_frame)\n", - "grid_plot.show()" + "figure_grid.add_animations(set_random_frame)\n", + "figure_grid.show()" ] }, { @@ -1580,7 +1575,7 @@ "outputs": [], "source": [ "# can access subplot by name\n", - "grid_plot[\"subplot0\"]" + "figure_grid[\"subplot0\"]" ] }, { @@ -1593,7 +1588,7 @@ "outputs": [], "source": [ "# can access subplot by index\n", - "grid_plot[0, 0]" + "figure_grid[0, 0]" ] }, { @@ -1616,7 +1611,7 @@ "outputs": [], "source": [ "# can access graphic directly via name\n", - "grid_plot[\"subplot0\"][\"rand-image\"]" + "figure_grid[\"subplot0\"][\"rand-image\"]" ] }, { @@ -1628,8 +1623,8 @@ }, "outputs": [], "source": [ - "grid_plot[\"subplot0\"][\"rand-image\"].vmin = 0.6\n", - "grid_plot[\"subplot0\"][\"rand-image\"].vmax = 0.8" + "figure_grid[\"subplot0\"][\"rand-image\"].vmin = 0.6\n", + "figure_grid[\"subplot0\"][\"rand-image\"].vmax = 0.8" ] }, { @@ -1649,8 +1644,8 @@ }, "outputs": [], "source": [ - "grid_plot[1, 0][\"rand-image\"].vim = 0.1\n", - "grid_plot[1, 0][\"rand-image\"].vmax = 0.3" + "figure_grid[1, 0][\"rand-image\"].vim = 0.1\n", + "figure_grid[1, 0][\"rand-image\"].vmax = 0.3" ] } ], diff --git a/examples/desktop/gridplot/gridplot.py b/examples/desktop/gridplot/gridplot.py index 3acf6a8ba..2669dd49b 100644 --- a/examples/desktop/gridplot/gridplot.py +++ b/examples/desktop/gridplot/gridplot.py @@ -10,25 +10,23 @@ import imageio.v3 as iio -plot = fpl.GridPlot(shape=(2, 2)) -# to force a specific framework such as glfw: -# plot = fpl.GridPlot(canvas="glfw") +fig = fpl.Figure(shape=(2, 2)) im = iio.imread("imageio:clock.png") im2 = iio.imread("imageio:astronaut.png") im3 = iio.imread("imageio:coffee.png") im4 = iio.imread("imageio:hubble_deep_field.png") -plot[0, 0].add_image(data=im) -plot[0, 1].add_image(data=im2) -plot[1, 0].add_image(data=im3) -plot[1, 1].add_image(data=im4) +fig[0, 0].add_image(data=im) +fig[0, 1].add_image(data=im2) +fig[1, 0].add_image(data=im3) +fig[1, 1].add_image(data=im4) -plot.show() +fig.show() -plot.canvas.set_logical_size(800, 800) +fig.canvas.set_logical_size(800, 800) -for subplot in plot: +for subplot in fig: subplot.auto_scale() if __name__ == "__main__": diff --git a/examples/desktop/gridplot/gridplot_non_square.py b/examples/desktop/gridplot/gridplot_non_square.py index fe43a3c04..ea93096dc 100644 --- a/examples/desktop/gridplot/gridplot_non_square.py +++ b/examples/desktop/gridplot/gridplot_non_square.py @@ -10,23 +10,21 @@ import imageio.v3 as iio -plot = fpl.GridPlot(shape=(2, 2), controller_ids="sync") -# to force a specific framework such as glfw: -# plot = fpl.GridPlot(canvas="glfw") +fig = fpl.Figure(shape=(2, 2), controller_ids="sync") im = iio.imread("imageio:clock.png") im2 = iio.imread("imageio:astronaut.png") im3 = iio.imread("imageio:coffee.png") -plot[0, 0].add_image(data=im) -plot[0, 1].add_image(data=im2) -plot[1, 0].add_image(data=im3) +fig[0, 0].add_image(data=im) +fig[0, 1].add_image(data=im2) +fig[1, 0].add_image(data=im3) -plot.show() +fig.show() -plot.canvas.set_logical_size(800, 800) +fig.canvas.set_logical_size(800, 800) -for subplot in plot: +for subplot in fig: subplot.auto_scale() if __name__ == "__main__": diff --git a/examples/desktop/heatmap/heatmap.py b/examples/desktop/heatmap/heatmap.py index 45c340cbd..fa5ec6715 100644 --- a/examples/desktop/heatmap/heatmap.py +++ b/examples/desktop/heatmap/heatmap.py @@ -9,9 +9,8 @@ import fastplotlib as fpl import numpy as np -plot = fpl.Plot() -# to force a specific framework such as glfw: -# plot = fpl.Plot(canvas="glfw") + +fig = fpl.Figure() xs = np.linspace(0, 1_000, 10_000) @@ -24,13 +23,13 @@ data[1::2] = cosine # plot the image data -heatmap_graphic = plot.add_heatmap(data=data, name="heatmap") +heatmap_graphic = fig[0, 0].add_heatmap(data=data, name="heatmap") -plot.show() +fig.show() -plot.canvas.set_logical_size(1500, 1500) +fig.canvas.set_logical_size(1500, 1500) -plot.auto_scale() +fig[0, 0].auto_scale() if __name__ == "__main__": print(__doc__) diff --git a/examples/desktop/heatmap/heatmap_cmap.py b/examples/desktop/heatmap/heatmap_cmap.py index afc67f5b8..a1434bb0e 100644 --- a/examples/desktop/heatmap/heatmap_cmap.py +++ b/examples/desktop/heatmap/heatmap_cmap.py @@ -9,9 +9,8 @@ import fastplotlib as fpl import numpy as np -plot = fpl.Plot() -# to force a specific framework such as glfw: -# plot = fpl.Plot(canvas="glfw") + +fig = fpl.Figure() xs = np.linspace(0, 1_000, 10_000) @@ -24,13 +23,13 @@ data[1::2] = cosine # plot the image data -heatmap_graphic = plot.add_heatmap(data=data, name="heatmap") +heatmap_graphic = fig[0, 0].add_heatmap(data=data, name="heatmap") -plot.show() +fig.show() -plot.canvas.set_logical_size(1500, 1500) +fig.canvas.set_logical_size(1500, 1500) -plot.auto_scale() +fig[0, 0].auto_scale() heatmap_graphic.cmap = "viridis" diff --git a/examples/desktop/heatmap/heatmap_data.py b/examples/desktop/heatmap/heatmap_data.py index 78e819ab8..67aee1668 100644 --- a/examples/desktop/heatmap/heatmap_data.py +++ b/examples/desktop/heatmap/heatmap_data.py @@ -9,9 +9,8 @@ import fastplotlib as fpl import numpy as np -plot = fpl.Plot() -# to force a specific framework such as glfw: -# plot = fpl.Plot(canvas="glfw") + +fig = fpl.Figure() xs = np.linspace(0, 1_000, 10_000) @@ -24,13 +23,13 @@ data[1::2] = cosine # plot the image data -heatmap_graphic = plot.add_heatmap(data=data, name="heatmap") +heatmap_graphic = fig[0, 0].add_heatmap(data=data, name="heatmap") -plot.show() +fig.show() -plot.canvas.set_logical_size(1500, 1500) +fig.canvas.set_logical_size(1500, 1500) -plot.auto_scale() +fig[0, 0].auto_scale() heatmap_graphic.data[:5_000] = sine heatmap_graphic.data[5_000:] = cosine diff --git a/examples/desktop/heatmap/heatmap_vmin_vmax.py b/examples/desktop/heatmap/heatmap_vmin_vmax.py index 7aae1d6d3..6fe8a08b8 100644 --- a/examples/desktop/heatmap/heatmap_vmin_vmax.py +++ b/examples/desktop/heatmap/heatmap_vmin_vmax.py @@ -9,9 +9,8 @@ import fastplotlib as fpl import numpy as np -plot = fpl.Plot() -# to force a specific framework such as glfw: -# plot = fpl.Plot(canvas="glfw") + +fig = fpl.Figure() xs = np.linspace(0, 1_000, 10_000) @@ -24,13 +23,13 @@ data[1::2] = cosine # plot the image data -heatmap_graphic = plot.add_heatmap(data=data, name="heatmap") +heatmap_graphic = fig[0, 0].add_heatmap(data=data, name="heatmap") -plot.show() +fig.show() -plot.canvas.set_logical_size(1500, 1500) +fig.canvas.set_logical_size(1500, 1500) -plot.auto_scale() +fig[0, 0].auto_scale() heatmap_graphic.cmap.vmin = -0.5 heatmap_graphic.cmap.vmax = 0.5 diff --git a/examples/desktop/image/image_cmap.py b/examples/desktop/image/image_cmap.py index b7f7b39af..bb8e9f9d8 100644 --- a/examples/desktop/image/image_cmap.py +++ b/examples/desktop/image/image_cmap.py @@ -9,21 +9,18 @@ import fastplotlib as fpl import imageio.v3 as iio - -plot = fpl.Plot() -# to force a specific framework such as glfw: -# plot = fpl.Plot(canvas="glfw") - im = iio.imread("imageio:camera.png") +fig = fpl.Figure() + # plot the image data -image_graphic = plot.add_image(data=im, name="random-image") +image_graphic = fig[0, 0].add_image(data=im, name="random-image") -plot.show() +fig.show() -plot.canvas.set_logical_size(800, 800) +fig.canvas.set_logical_size(800, 800) -plot.auto_scale() +fig[0, 0].auto_scale() image_graphic.cmap = "viridis" diff --git a/examples/desktop/image/image_rgb.py b/examples/desktop/image/image_rgb.py index 2642962fd..ce7e151d0 100644 --- a/examples/desktop/image/image_rgb.py +++ b/examples/desktop/image/image_rgb.py @@ -10,20 +10,18 @@ import imageio.v3 as iio -plot = fpl.Plot() -# to force a specific framework such as glfw: -# plot = fpl.Plot(canvas="glfw") - im = iio.imread("imageio:astronaut.png") +fig = fpl.Figure() + # plot the image data -image_graphic = plot.add_image(data=im, name="iio astronaut") +image_graphic = fig[0, 0].add_image(data=im, name="iio astronaut") -plot.show() +fig.show() -plot.canvas.set_logical_size(800, 800) +fig.canvas.set_logical_size(800, 800) -plot.auto_scale() +fig[0, 0].auto_scale() if __name__ == "__main__": diff --git a/examples/desktop/image/image_rgbvminvmax.py b/examples/desktop/image/image_rgbvminvmax.py index e5c4af531..9725c038a 100644 --- a/examples/desktop/image/image_rgbvminvmax.py +++ b/examples/desktop/image/image_rgbvminvmax.py @@ -10,20 +10,18 @@ import imageio.v3 as iio -plot = fpl.Plot() -# to force a specific framework such as glfw: -# plot = fpl.Plot(canvas="glfw") - im = iio.imread("imageio:astronaut.png") +fig = fpl.Figure() + # plot the image data -image_graphic = plot.add_image(data=im, name="iio astronaut") +image_graphic = fig[0, 0].add_image(data=im, name="iio astronaut") -plot.show() +fig.show() -plot.canvas.set_logical_size(800, 800) +fig.canvas.set_logical_size(800, 800) -plot.auto_scale() +fig[0, 0].auto_scale() image_graphic.cmap.vmin = 0.5 image_graphic.cmap.vmax = 0.75 diff --git a/examples/desktop/image/image_simple.py b/examples/desktop/image/image_simple.py index 2d273ad68..a640974ed 100644 --- a/examples/desktop/image/image_simple.py +++ b/examples/desktop/image/image_simple.py @@ -10,20 +10,18 @@ import imageio.v3 as iio -plot = fpl.Plot() -# to force a specific framework such as glfw: -# plot = fpl.Plot(canvas="glfw") +fig = fpl.Figure() data = iio.imread("imageio:camera.png") # plot the image data -image_graphic = plot.add_image(data=data, name="iio camera") +image_graphic = fig[0, 0].add_image(data=data, name="iio camera") -plot.show() +fig.show() -plot.canvas.set_logical_size(800, 800) +fig.canvas.set_logical_size(800, 800) -plot.auto_scale() +fig[0, 0].auto_scale() if __name__ == "__main__": print(__doc__) diff --git a/examples/desktop/image/image_vminvmax.py b/examples/desktop/image/image_vminvmax.py index e764f6775..3c8607aef 100644 --- a/examples/desktop/image/image_vminvmax.py +++ b/examples/desktop/image/image_vminvmax.py @@ -10,20 +10,18 @@ import imageio.v3 as iio -plot = fpl.Plot() -# to force a specific framework such as glfw: -# plot = fpl.Plot(canvas="glfw") +fig = fpl.Figure() data = iio.imread("imageio:astronaut.png") # plot the image data -image_graphic = plot.add_image(data=data, name="iio astronaut") +image_graphic = fig[0, 0].add_image(data=data, name="iio astronaut") -plot.show() +fig.show() -plot.canvas.set_logical_size(800, 800) +fig.canvas.set_logical_size(800, 800) -plot.auto_scale() +fig[0, 0].auto_scale() image_graphic.cmap.vmin = 0.5 image_graphic.cmap.vmax = 0.75 diff --git a/examples/desktop/image/image_widget.py b/examples/desktop/image/image_widget.py index c50d914d3..80aafe0b1 100644 --- a/examples/desktop/image/image_widget.py +++ b/examples/desktop/image/image_widget.py @@ -5,13 +5,12 @@ When run in a notebook, or with the Qt GUI backend, sliders are also shown. """ -import numpy as np import fastplotlib as fpl import imageio.v3 as iio # not a fastplotlib dependency, only used for examples a = iio.imread("imageio:camera.png") -iw = fpl.widgets.ImageWidget(data=a, cmap="viridis") +iw = fpl.ImageWidget(data=a, cmap="viridis") iw.show() diff --git a/examples/desktop/line/line.py b/examples/desktop/line/line.py index 8cab1954f..56575a810 100644 --- a/examples/desktop/line/line.py +++ b/examples/desktop/line/line.py @@ -10,9 +10,7 @@ import numpy as np -plot = fpl.Plot() -# to force a specific framework such as glfw: -# plot = fpl.Plot(canvas="glfw") +fig = fpl.Figure() xs = np.linspace(-10, 10, 100) # sine wave @@ -28,20 +26,20 @@ ys = np.sinc(xs) * 3 + 8 sinc = np.dstack([xs, ys])[0] -sine_graphic = plot.add_line(data=sine, thickness=5, colors="magenta") +sine_graphic = fig[0, 0].add_line(data=sine, thickness=5, colors="magenta") # you can also use colormaps for lines! -cosine_graphic = plot.add_line(data=cosine, thickness=12, cmap="autumn") +cosine_graphic = fig[0, 0].add_line(data=cosine, thickness=12, cmap="autumn") # or a list of colors for each datapoint colors = ["r"] * 25 + ["purple"] * 25 + ["y"] * 25 + ["b"] * 25 -sinc_graphic = plot.add_line(data=sinc, thickness=5, colors=colors) +sinc_graphic = fig[0, 0].add_line(data=sinc, thickness=5, colors=colors) -plot.show() +fig.show() -plot.canvas.set_logical_size(800, 800) +fig.canvas.set_logical_size(800, 800) -plot.auto_scale() +fig[0, 0].auto_scale() if __name__ == "__main__": diff --git a/examples/desktop/line/line_cmap.py b/examples/desktop/line/line_cmap.py index b196132ed..7d8e1e7d6 100644 --- a/examples/desktop/line/line_cmap.py +++ b/examples/desktop/line/line_cmap.py @@ -10,9 +10,7 @@ import numpy as np -plot = fpl.Plot() -# to force a specific framework such as glfw: -# plot = fpl.Plot(canvas="glfw") +fig = fpl.Figure() xs = np.linspace(-10, 10, 100) # sine wave @@ -24,7 +22,7 @@ cosine = np.dstack([xs, ys])[0] # cmap_values from an array, so the colors on the sine line will be based on the sine y-values -sine_graphic = plot.add_line( +sine_graphic = fig[0, 0].add_line( data=sine, thickness=10, cmap="plasma", @@ -33,16 +31,16 @@ # qualitative colormaps, useful for cluster labels or other types of categorical labels cmap_values = [0] * 25 + [5] * 10 + [1] * 35 + [2] * 30 -cosine_graphic = plot.add_line( +cosine_graphic = fig[0, 0].add_line( data=cosine, thickness=10, cmap="tab10", cmap_values=cmap_values ) -plot.show() +fig.show() -plot.canvas.set_logical_size(800, 800) +fig.canvas.set_logical_size(800, 800) if __name__ == "__main__": print(__doc__) diff --git a/examples/desktop/line/line_colorslice.py b/examples/desktop/line/line_colorslice.py index f2aca8125..4df666531 100644 --- a/examples/desktop/line/line_colorslice.py +++ b/examples/desktop/line/line_colorslice.py @@ -10,9 +10,7 @@ import numpy as np -plot = fpl.Plot() -# to force a specific framework such as glfw: -# plot = fpl.Plot(canvas="glfw") +fig = fpl.Figure() xs = np.linspace(-10, 10, 100) # sine wave @@ -28,16 +26,16 @@ ys = np.sinc(xs) * 3 + 8 sinc = np.dstack([xs, ys])[0] -sine_graphic = plot.add_line(data=sine, thickness=5, colors="magenta") +sine_graphic = fig[0, 0].add_line(data=sine, thickness=5, colors="magenta") # you can also use colormaps for lines! -cosine_graphic = plot.add_line(data=cosine, thickness=12, cmap="autumn") +cosine_graphic = fig[0, 0].add_line(data=cosine, thickness=12, cmap="autumn") # or a list of colors for each datapoint colors = ["r"] * 25 + ["purple"] * 25 + ["y"] * 25 + ["b"] * 25 -sinc_graphic = plot.add_line(data=sinc, thickness=5, colors=colors) +sinc_graphic = fig[0, 0].add_line(data=sinc, thickness=5, colors=colors) -plot.show() +fig.show() # indexing of colors cosine_graphic.colors[:15] = "magenta" @@ -58,9 +56,9 @@ key2 = np.array([True, False, True, False, True, True, True, True]) cosine_graphic.colors[key2] = "Green" -plot.canvas.set_logical_size(800, 800) +fig.canvas.set_logical_size(800, 800) -plot.auto_scale() +fig[0, 0].auto_scale() if __name__ == "__main__": diff --git a/examples/desktop/line/line_dataslice.py b/examples/desktop/line/line_dataslice.py index ea87ba552..12a5f0f04 100644 --- a/examples/desktop/line/line_dataslice.py +++ b/examples/desktop/line/line_dataslice.py @@ -10,9 +10,7 @@ import numpy as np -plot = fpl.Plot() -# to force a specific framework such as glfw: -# plot = fpl.Plot(canvas="glfw") +fig = fpl.Figure() xs = np.linspace(-10, 10, 100) # sine wave @@ -28,16 +26,16 @@ ys = np.sinc(xs) * 3 + 8 sinc = np.dstack([xs, ys])[0] -sine_graphic = plot.add_line(data=sine, thickness=5, colors="magenta") +sine_graphic = fig[0, 0].add_line(data=sine, thickness=5, colors="magenta") # you can also use colormaps for lines! -cosine_graphic = plot.add_line(data=cosine, thickness=12, cmap="autumn") +cosine_graphic = fig[0, 0].add_line(data=cosine, thickness=12, cmap="autumn") # or a list of colors for each datapoint colors = ["r"] * 25 + ["purple"] * 25 + ["y"] * 25 + ["b"] * 25 -sinc_graphic = plot.add_line(data=sinc, thickness=5, colors=colors) +sinc_graphic = fig[0, 0].add_line(data=sinc, thickness=5, colors=colors) -plot.show() +fig.show() cosine_graphic.data[10:50:5, :2] = sine[10:50:5] cosine_graphic.data[90:, 1] = 7 @@ -47,9 +45,9 @@ key2 = np.array([True, False, True, False, True, True, True, True]) sinc_graphic.data[key2] = np.array([[5, 1, 2]]) -plot.canvas.set_logical_size(800, 800) +fig.canvas.set_logical_size(800, 800) -plot.auto_scale() +fig[0, 0].auto_scale() if __name__ == "__main__": diff --git a/examples/desktop/line/line_present_scaling.py b/examples/desktop/line/line_present_scaling.py index 327186c16..d334e6fbd 100644 --- a/examples/desktop/line/line_present_scaling.py +++ b/examples/desktop/line/line_present_scaling.py @@ -10,9 +10,7 @@ import numpy as np -plot = fpl.Plot() -# to force a specific framework such as glfw: -# plot = fpl.Plot(canvas="glfw") +fig = fpl.Figure() xs = np.linspace(-10, 10, 100) # sine wave @@ -28,22 +26,22 @@ ys = np.sinc(xs) * 3 + 8 sinc = np.dstack([xs, ys])[0] -sine_graphic = plot.add_line(data=sine, thickness=5, colors="magenta") +sine_graphic = fig[0, 0].add_line(data=sine, thickness=5, colors="magenta") # you can also use colormaps for lines! -cosine_graphic = plot.add_line(data=cosine, thickness=12, cmap="autumn") +cosine_graphic = fig[0, 0].add_line(data=cosine, thickness=12, cmap="autumn") # or a list of colors for each datapoint colors = ["r"] * 25 + ["purple"] * 25 + ["y"] * 25 + ["b"] * 25 -sinc_graphic = plot.add_line(data=sinc, thickness=5, colors=colors) +sinc_graphic = fig[0, 0].add_line(data=sinc, thickness=5, colors=colors) -plot.show() +fig.show() sinc_graphic.present = False -plot.canvas.set_logical_size(800, 800) +fig.canvas.set_logical_size(800, 800) -plot.auto_scale() +fig[0, 0].auto_scale() if __name__ == "__main__": diff --git a/examples/desktop/line_collection/line_collection.py b/examples/desktop/line_collection/line_collection.py index 071da2e2e..dd6f3ca33 100644 --- a/examples/desktop/line_collection/line_collection.py +++ b/examples/desktop/line_collection/line_collection.py @@ -27,15 +27,13 @@ def make_circle(center, radius: float, n_points: int = 75) -> np.ndarray: pos_xy = np.vstack(circles) -plot = fpl.Plot() -# to force a specific framework such as glfw: -# plot = fpl.Plot(canvas="glfw") +fig = fpl.Figure() -plot.add_line_collection(circles, cmap="jet", thickness=5) +fig[0, 0].add_line_collection(circles, cmap="jet", thickness=5) -plot.show() +fig.show() -plot.canvas.set_logical_size(800, 800) +fig.canvas.set_logical_size(800, 800) if __name__ == "__main__": print(__doc__) diff --git a/examples/desktop/line_collection/line_collection_cmap_values.py b/examples/desktop/line_collection/line_collection_cmap_values.py index 3623c20c3..9eeef40f8 100644 --- a/examples/desktop/line_collection/line_collection_cmap_values.py +++ b/examples/desktop/line_collection/line_collection_cmap_values.py @@ -33,20 +33,18 @@ def make_circle(center, radius: float, n_points: int = 75) -> np.ndarray: # highest values, lowest values, mid-high values, mid values cmap_values = [10] * 4 + [0] * 4 + [7] * 4 + [5] * 4 -plot = fpl.Plot() -# to force a specific framework such as glfw: -# plot = fpl.Plot(canvas="glfw") +fig = fpl.Figure() -plot.add_line_collection( +fig[0, 0].add_line_collection( circles, cmap="bwr", cmap_values=cmap_values, thickness=10 ) -plot.show() +fig.show() -plot.canvas.set_logical_size(800, 800) +fig.canvas.set_logical_size(800, 800) if __name__ == "__main__": print(__doc__) diff --git a/examples/desktop/line_collection/line_collection_cmap_values_qualitative.py b/examples/desktop/line_collection/line_collection_cmap_values_qualitative.py index f56d2ca02..85f0724d8 100644 --- a/examples/desktop/line_collection/line_collection_cmap_values_qualitative.py +++ b/examples/desktop/line_collection/line_collection_cmap_values_qualitative.py @@ -39,20 +39,18 @@ def make_circle(center, radius: float, n_points: int = 75) -> np.ndarray: 1, 1, 1, 5 ] -plot = fpl.Plot() -# to force a specific framework such as glfw: -# plot = fpl.Plot(canvas="glfw") +fig = fpl.Figure() -plot.add_line_collection( +fig[0, 0].add_line_collection( circles, cmap="tab10", cmap_values=cmap_values, thickness=10 ) -plot.show() +fig.show() -plot.canvas.set_logical_size(800, 800) +fig.canvas.set_logical_size(800, 800) if __name__ == "__main__": print(__doc__) diff --git a/examples/desktop/line_collection/line_collection_colors.py b/examples/desktop/line_collection/line_collection_colors.py index d74f65d82..d53afcd5b 100644 --- a/examples/desktop/line_collection/line_collection_colors.py +++ b/examples/desktop/line_collection/line_collection_colors.py @@ -31,15 +31,13 @@ def make_circle(center, radius: float, n_points: int = 75) -> np.ndarray: # this will produce 16 circles so we will define 16 colors colors = ["blue"] * 4 + ["red"] * 4 + ["yellow"] * 4 + ["w"] * 4 -plot = fpl.Plot() -# to force a specific framework such as glfw: -# plot = fpl.Plot(canvas="glfw") +fig = fpl.Figure() -plot.add_line_collection(circles, colors=colors, thickness=10) +fig[0, 0].add_line_collection(circles, colors=colors, thickness=10) -plot.show() +fig.show() -plot.canvas.set_logical_size(800, 800) +fig.canvas.set_logical_size(800, 800) if __name__ == "__main__": print(__doc__) diff --git a/examples/desktop/line_collection/line_stack.py b/examples/desktop/line_collection/line_stack.py index 5a94caee7..cf5d933e3 100644 --- a/examples/desktop/line_collection/line_stack.py +++ b/examples/desktop/line_collection/line_stack.py @@ -17,16 +17,14 @@ # make 25 lines data = np.vstack([ys] * 25) -plot = fpl.Plot() -# to force a specific framework such as glfw: -# plot = fpl.Plot(canvas="glfw") +fig = fpl.Figure() # line stack takes all the same arguments as line collection and behaves similarly -plot.add_line_stack(data, cmap="jet") +fig[0, 0].add_line_stack(data, cmap="jet") -plot.show(maintain_aspect=False) +fig.show(maintain_aspect=False) -plot.canvas.set_logical_size(900, 600) +fig.canvas.set_logical_size(900, 600) if __name__ == "__main__": print(__doc__) diff --git a/examples/desktop/scatter/scatter.py b/examples/desktop/scatter/scatter.py index 778f37deb..c47306722 100644 --- a/examples/desktop/scatter/scatter.py +++ b/examples/desktop/scatter/scatter.py @@ -10,9 +10,7 @@ import numpy as np from pathlib import Path -plot = fpl.Plot() -# to force a specific framework such as glfw: -# plot = fpl.Plot(canvas="glfw") +fig = fpl.Figure() data_path = Path(__file__).parent.parent.joinpath("data", "iris.npy") data = np.load(data_path) @@ -20,13 +18,13 @@ n_points = 50 colors = ["yellow"] * n_points + ["cyan"] * n_points + ["magenta"] * n_points -scatter_graphic = plot.add_scatter(data=data[:, :-1], sizes=6, alpha=0.7, colors=colors) +scatter_graphic = fig[0, 0].add_scatter(data=data[:, :-1], sizes=6, alpha=0.7, colors=colors) -plot.show() +fig.show() -plot.canvas.set_logical_size(800, 800) +fig.canvas.set_logical_size(800, 800) -plot.auto_scale() +fig[0, 0].auto_scale() if __name__ == "__main__": diff --git a/examples/desktop/scatter/scatter_cmap.py b/examples/desktop/scatter/scatter_cmap.py index 3e986d5d5..f1bba98c3 100644 --- a/examples/desktop/scatter/scatter_cmap.py +++ b/examples/desktop/scatter/scatter_cmap.py @@ -12,28 +12,23 @@ from sklearn.cluster import AgglomerativeClustering -plot = fpl.Plot() -# to force a specific framework such as glfw: -# plot = fpl.Plot(canvas="glfw") +fig = fpl.Figure() data_path = Path(__file__).parent.parent.joinpath("data", "iris.npy") data = np.load(data_path) - agg = AgglomerativeClustering(n_clusters=3) - agg.fit_predict(data) - -scatter_graphic = plot.add_scatter( +scatter_graphic = fig[0, 0].add_scatter( data=data[:, :-1], sizes=15, alpha=0.7, cmap="Set1", cmap_values=agg.labels_ ) -plot.show() +fig.show() -plot.canvas.set_logical_size(800, 800) +fig.canvas.set_logical_size(800, 800) -plot.auto_scale() +fig[0, 0].auto_scale() scatter_graphic.cmap = "tab10" diff --git a/examples/desktop/scatter/scatter_colorslice.py b/examples/desktop/scatter/scatter_colorslice.py index d752cacbd..43f405b06 100644 --- a/examples/desktop/scatter/scatter_colorslice.py +++ b/examples/desktop/scatter/scatter_colorslice.py @@ -11,9 +11,7 @@ from pathlib import Path -plot = fpl.Plot() -# to force a specific framework such as glfw: -# plot = fpl.Plot(canvas="glfw") +fig = fpl.Figure() data_path = Path(__file__).parent.parent.joinpath("data", "iris.npy") data = np.load(data_path) @@ -21,13 +19,13 @@ n_points = 50 colors = ["yellow"] * n_points + ["cyan"] * n_points + ["magenta"] * n_points -scatter_graphic = plot.add_scatter(data=data[:, :-1], sizes=6, alpha=0.7, colors=colors) +scatter_graphic = fig[0, 0].add_scatter(data=data[:, :-1], sizes=6, alpha=0.7, colors=colors) -plot.show() +fig.show() -plot.canvas.set_logical_size(800, 800) +fig.canvas.set_logical_size(800, 800) -plot.auto_scale() +fig[0, 0].auto_scale() scatter_graphic.colors[0:75] = "red" scatter_graphic.colors[75:150] = "white" diff --git a/examples/desktop/scatter/scatter_dataslice.py b/examples/desktop/scatter/scatter_dataslice.py index 3008aab61..989b7c21c 100644 --- a/examples/desktop/scatter/scatter_dataslice.py +++ b/examples/desktop/scatter/scatter_dataslice.py @@ -11,9 +11,7 @@ from pathlib import Path -plot = fpl.Plot() -# to force a specific framework such as glfw: -# plot = fpl.Plot(canvas="glfw") +fig = fpl.Figure() data_path = Path(__file__).parent.parent.joinpath("data", "iris.npy") data = np.load(data_path) @@ -21,13 +19,13 @@ n_points = 50 colors = ["yellow"] * n_points + ["cyan"] * n_points + ["magenta"] * n_points -scatter_graphic = plot.add_scatter(data=data[:, :-1], sizes=6, alpha=0.7, colors=colors) +scatter_graphic = fig[0, 0].add_scatter(data=data[:, :-1], sizes=6, alpha=0.7, colors=colors) -plot.show() +fig.show() -plot.canvas.set_logical_size(800, 800) +fig.canvas.set_logical_size(800, 800) -plot.auto_scale() +fig[0, 0].auto_scale() scatter_graphic.data[0] = np.array([[5, 3, 1.5]]) scatter_graphic.data[1] = np.array([[4.3, 3.2, 1.3]]) diff --git a/examples/desktop/scatter/scatter_present.py b/examples/desktop/scatter/scatter_present.py index ad4be837f..5da4610bd 100644 --- a/examples/desktop/scatter/scatter_present.py +++ b/examples/desktop/scatter/scatter_present.py @@ -11,7 +11,7 @@ from pathlib import Path -plot = fpl.Plot() +fig = fpl.Figure() data_path = Path(__file__).parent.parent.joinpath("data", "iris.npy") data = np.load(data_path) @@ -19,16 +19,16 @@ n_points = 50 colors = ["yellow"] * n_points + ["cyan"] * n_points + ["magenta"] * n_points -scatter_graphic = plot.add_scatter(data=data[:, :-1], sizes=6, alpha=0.7, colors=colors) +scatter_graphic = fig[0, 0].add_scatter(data=data[:, :-1], sizes=6, alpha=0.7, colors=colors) colors = ["red"] * n_points + ["white"] * n_points + ["blue"] * n_points -scatter_graphic2 = plot.add_scatter(data=data[:, 1:], sizes=6, alpha=0.7, colors=colors) +scatter_graphic2 = fig[0, 0].add_scatter(data=data[:, 1:], sizes=6, alpha=0.7, colors=colors) -plot.show() +fig.show() -plot.canvas.set_logical_size(800, 800) +fig.canvas.set_logical_size(800, 800) -plot.auto_scale() +fig[0, 0].auto_scale() scatter_graphic.present = False diff --git a/examples/desktop/scatter/scatter_size.py b/examples/desktop/scatter/scatter_size.py index 5c1f97703..41a97ad53 100644 --- a/examples/desktop/scatter/scatter_size.py +++ b/examples/desktop/scatter/scatter_size.py @@ -8,14 +8,14 @@ import numpy as np import fastplotlib as fpl -# grid with 2 rows and 3 columns -grid_shape = (2, 1) +# figure with 2 rows and 3 columns +shape = (2, 1) # you can give string names for each subplot within the gridplot names = [["scalar_size"], ["array_size"]] # Create the grid plot -plot = fpl.GridPlot(shape=grid_shape, names=names, size=(1000, 1000)) +fig = fpl.Figure(shape=shape, names=names, size=(1000, 1000)) # get y_values using sin function angles = np.arange(0, 20 * np.pi + 0.001, np.pi / 20) @@ -24,17 +24,17 @@ data = np.column_stack([x_values, y_values]) -plot["scalar_size"].add_scatter( +fig["scalar_size"].add_scatter( data=data, sizes=5, colors="blue" ) # add a set of scalar sizes non_scalar_sizes = np.abs((y_values / np.pi)) # ensure minimum size of 5 -plot["array_size"].add_scatter(data=data, sizes=non_scalar_sizes, colors="red") +fig["array_size"].add_scatter(data=data, sizes=non_scalar_sizes, colors="red") -for graph in plot: +for graph in fig: graph.auto_scale(maintain_aspect=True) -plot.show() +fig.show() if __name__ == "__main__": print(__doc__) diff --git a/examples/notebooks/heatmap.ipynb b/examples/notebooks/heatmap.ipynb index 82583b1df..90c07a3cb 100644 --- a/examples/notebooks/heatmap.ipynb +++ b/examples/notebooks/heatmap.ipynb @@ -70,11 +70,11 @@ }, "outputs": [], "source": [ - "plot = fpl.Plot()\n", + "fig = fpl.Figure()\n", "\n", - "plot.add_heatmap(data, cmap=\"viridis\")\n", + "fig[0, 0].add_heatmap(data, cmap=\"viridis\")\n", "\n", - "plot.show(maintain_aspect=False)" + "fig.show(maintain_aspect=False)" ] }, { diff --git a/examples/notebooks/image_widget.ipynb b/examples/notebooks/image_widget.ipynb index a7527601a..5136ba028 100644 --- a/examples/notebooks/image_widget.ipynb +++ b/examples/notebooks/image_widget.ipynb @@ -78,7 +78,7 @@ }, "outputs": [], "source": [ - "iw.gridplot[0, 0][\"image_widget_managed\"].cmap = \"gnuplot2\"" + "iw.figure[0, 0][\"image_widget_managed\"].cmap = \"gnuplot2\"" ] }, { @@ -231,7 +231,7 @@ "outputs": [], "source": [ "iw_movie.set_data(new_data=new_data)\n", - "iw_movie.gridplot[0, 0].auto_scale()# sidecar is optional" + "iw_movie.figure[0, 0].auto_scale()# sidecar is optional" ] }, { @@ -340,7 +340,7 @@ }, "outputs": [], "source": [ - "iw_zfish.gridplot[\"plane-2\"]" + "iw_zfish.figure[\"plane-2\"]" ] }, { diff --git a/examples/notebooks/image_widget_test.ipynb b/examples/notebooks/image_widget_test.ipynb index 39cf0b887..321f7b84f 100644 --- a/examples/notebooks/image_widget_test.ipynb +++ b/examples/notebooks/image_widget_test.ipynb @@ -60,7 +60,7 @@ "iw = ImageWidget(\n", " data=a,\n", " cmap=\"viridis\",\n", - " grid_plot_kwargs={\"size\": (900, 600)},\n", + " figure_kwargs={\"size\": (900, 600)},\n", ")" ] }, @@ -81,9 +81,9 @@ "metadata": {}, "outputs": [], "source": [ - "plot_test(\"image-widget-single\", iw.gridplot)\n", - "iw.gridplot[0, 0][\"image_widget_managed\"].cmap = \"gnuplot2\"\n", - "plot_test(\"image-widget-single-gnuplot2\", iw.gridplot)" + "plot_test(\"image-widget-single\", iw.figure)\n", + "iw.figure[0, 0][\"image_widget_managed\"].cmap = \"gnuplot2\"\n", + "plot_test(\"image-widget-single-gnuplot2\", iw.figure)" ] }, { @@ -131,7 +131,7 @@ "iw_movie = ImageWidget(\n", " data=gray_movie, \n", " cmap=\"gray\",\n", - " grid_plot_kwargs={\"size\": (900, 600)},\n", + " figure_kwargs={\"size\": (900, 600)},\n", ")" ] }, @@ -157,23 +157,23 @@ "# testing cell ignore\n", "assert iw_movie.sliders[\"t\"].max == gray_movie.shape[0] - 1\n", "assert iw_movie.sliders[\"t\"].min == 0\n", - "plot_test(\"image-widget-movie-single-0\", iw_movie.gridplot)\n", + "plot_test(\"image-widget-movie-single-0\", iw_movie.figure)\n", "iw_movie.sliders[\"t\"].value = 50\n", - "plot_test(\"image-widget-movie-single-50\", iw_movie.gridplot)\n", + "plot_test(\"image-widget-movie-single-50\", iw_movie.figure)\n", "iw_movie.sliders[\"t\"].value = 279\n", - "plot_test(\"image-widget-movie-single-279\", iw_movie.gridplot)\n", + "plot_test(\"image-widget-movie-single-279\", iw_movie.figure)\n", "iw_movie.sliders[\"t\"].value = 0\n", - "plot_test(\"image-widget-movie-single-0-reset\", iw_movie.gridplot)\n", + "plot_test(\"image-widget-movie-single-0-reset\", iw_movie.figure)\n", "iw_movie.sliders[\"t\"].value = 50\n", "iw_movie.window_funcs = {\"t\": (np.mean, 13)}\n", "# testing cell ignore\n", - "plot_test(\"image-widget-movie-single-50-window-mean-13\", iw_movie.gridplot)\n", + "plot_test(\"image-widget-movie-single-50-window-mean-13\", iw_movie.figure)\n", "iw_movie.window_funcs[\"t\"].window_size = 33\n", - "plot_test(\"image-widget-movie-single-50-window-mean-33\", iw_movie.gridplot)\n", + "plot_test(\"image-widget-movie-single-50-window-mean-33\", iw_movie.figure)\n", "iw_movie.window_funcs[\"t\"].func = np.max\n", - "plot_test(\"image-widget-movie-single-50-window-max-33\", iw_movie.gridplot)\n", + "plot_test(\"image-widget-movie-single-50-window-max-33\", iw_movie.figure)\n", "iw_movie.window_funcs = None\n", - "plot_test(\"image-widget-movie-single-50-window-reset\", iw_movie.gridplot)\n", + "plot_test(\"image-widget-movie-single-50-window-reset\", iw_movie.figure)\n", "iw_movie.sliders[\"t\"].value = 0" ] }, @@ -205,7 +205,7 @@ "outputs": [], "source": [ "iw_movie.set_data(new_data=new_data)\n", - "iw_movie.gridplot[0, 0].auto_scale()" + "iw_movie.figure[0, 0].auto_scale()" ] }, { @@ -215,7 +215,7 @@ "metadata": {}, "outputs": [], "source": [ - "plot_test(\"image-widget-movie-set_data\", iw_movie.gridplot)" + "plot_test(\"image-widget-movie-set_data\", iw_movie.figure)" ] }, { @@ -283,7 +283,7 @@ " window_funcs={\"t\": (np.mean, 5)},\n", " names=[f\"plane-{i}\" for i in range(n_planes)],\n", " cmap=\"gnuplot2\", \n", - " grid_plot_kwargs={\"size\": (900, 600)},\n", + " figure_kwargs={\"size\": (900, 600)},\n", ")" ] }, @@ -309,25 +309,25 @@ "# testing cell ignore\n", "assert iw_zfish.sliders[\"t\"].max == zfish_data.shape[0] - 1\n", "assert iw_zfish.sliders[\"t\"].min == 0\n", - "plot_test(\"image-widget-zfish-grid-init-mean-window-5\", iw_zfish.gridplot)\n", + "plot_test(\"image-widget-zfish-grid-init-mean-window-5\", iw_zfish.figure)\n", "iw_zfish.sliders[\"t\"].value = 50\n", - "plot_test(\"image-widget-zfish-grid-frame-50-mean-window-5\", iw_zfish.gridplot)\n", + "plot_test(\"image-widget-zfish-grid-frame-50-mean-window-5\", iw_zfish.figure)\n", "iw_zfish.window_funcs[\"t\"].window_size = 13\n", - "plot_test(\"image-widget-zfish-grid-frame-50-mean-window-13\", iw_zfish.gridplot)\n", + "plot_test(\"image-widget-zfish-grid-frame-50-mean-window-13\", iw_zfish.figure)\n", "iw_zfish.window_funcs = None\n", - "plot_test(\"image-widget-zfish-grid-frame-50\", iw_zfish.gridplot)\n", + "plot_test(\"image-widget-zfish-grid-frame-50\", iw_zfish.figure)\n", "iw_zfish.sliders[\"t\"].value = 99\n", - "plot_test(\"image-widget-zfish-grid-frame-99\", iw_zfish.gridplot)\n", + "plot_test(\"image-widget-zfish-grid-frame-99\", iw_zfish.figure)\n", "iw_zfish.sliders[\"t\"].value = 50\n", "iw_zfish.window_funcs = {\"t\": (np.max, 13)}\n", - "plot_test(\"image-widget-zfish-grid-frame-50-max-window-13\", iw_zfish.gridplot)\n", + "plot_test(\"image-widget-zfish-grid-frame-50-max-window-13\", iw_zfish.figure)\n", "iw_zfish.window_funcs = None\n", "iw_zfish.frame_apply = lambda frame: gaussian_filter(frame.astype(np.float32), sigma=3)\n", "iw_zfish.reset_vmin_vmax()\n", - "plot_test(\"image-widget-zfish-grid-frame-50-frame-apply-gaussian\", iw_zfish.gridplot)\n", + "plot_test(\"image-widget-zfish-grid-frame-50-frame-apply-gaussian\", iw_zfish.figure)\n", "iw_zfish.frame_apply = None\n", "iw_zfish.reset_vmin_vmax()\n", - "plot_test(\"image-widget-zfish-grid-frame-50-frame-apply-reset\", iw_zfish.gridplot)" + "plot_test(\"image-widget-zfish-grid-frame-50-frame-apply-reset\", iw_zfish.figure)" ] }, { @@ -343,13 +343,13 @@ " reset_indices=False\n", ")\n", "\n", - "plot_test(\"image-widget-zfish-grid-set_data-reset-indices-false\", iw_zfish.gridplot)\n", + "plot_test(\"image-widget-zfish-grid-set_data-reset-indices-false\", iw_zfish.figure)\n", "\n", "iw_zfish.set_data(\n", " [zfish_data[:, i] for i in range(n_planes - 1, -1, -1)],\n", " reset_indices=True\n", ")\n", - "plot_test(\"image-widget-zfish-grid-set_data-reset-indices-true\", iw_zfish.gridplot)" + "plot_test(\"image-widget-zfish-grid-set_data-reset-indices-true\", iw_zfish.figure)" ] }, { @@ -383,7 +383,7 @@ " data=zfish_data, # you can also provide a list of tzxy arrays\n", " window_funcs={\"t\": (np.mean, 5)},\n", " cmap=\"gnuplot2\", \n", - " grid_plot_kwargs={\"size\": (900, 600)},\n", + " figure_kwargs={\"size\": (900, 600)},\n", ")" ] }, @@ -406,28 +406,28 @@ "metadata": {}, "outputs": [], "source": [ - "# same tests as with the gridplot\n", + "# same tests as with the figure\n", "assert iw_z.sliders[\"t\"].max == zfish_data.shape[0] - 1\n", "assert iw_z.sliders[\"t\"].min == 0\n", - "plot_test(\"image-widget-zfish-init-mean-window-5\", iw_z.gridplot)\n", + "plot_test(\"image-widget-zfish-init-mean-window-5\", iw_z.figure)\n", "iw_z.sliders[\"t\"].value = 50\n", - "plot_test(\"image-widget-zfish-frame-50-mean-window-5\", iw_z.gridplot)\n", + "plot_test(\"image-widget-zfish-frame-50-mean-window-5\", iw_z.figure)\n", "iw_z.window_funcs[\"t\"].window_size = 13\n", - "plot_test(\"image-widget-zfish-frame-50-mean-window-13\", iw_z.gridplot)\n", + "plot_test(\"image-widget-zfish-frame-50-mean-window-13\", iw_z.figure)\n", "iw_z.window_funcs = None\n", - "plot_test(\"image-widget-zfish-frame-50\", iw_z.gridplot)\n", + "plot_test(\"image-widget-zfish-frame-50\", iw_z.figure)\n", "iw_z.sliders[\"t\"].value = 99\n", - "plot_test(\"image-widget-zfish-frame-99\", iw_z.gridplot)\n", + "plot_test(\"image-widget-zfish-frame-99\", iw_z.figure)\n", "iw_z.sliders[\"t\"].value = 50\n", "iw_z.window_funcs = {\"t\": (np.max, 13)}\n", - "plot_test(\"image-widget-zfish-frame-50-max-window-13\", iw_z.gridplot)\n", + "plot_test(\"image-widget-zfish-frame-50-max-window-13\", iw_z.figure)\n", "iw_z.window_funcs = None\n", "iw_z.frame_apply = lambda frame: gaussian_filter(frame.astype(np.float32), sigma=3)\n", "iw_z.reset_vmin_vmax()\n", - "plot_test(\"image-widget-zfish-frame-50-frame-apply-gaussian\", iw_z.gridplot)\n", + "plot_test(\"image-widget-zfish-frame-50-frame-apply-gaussian\", iw_z.figure)\n", "iw_z.frame_apply = None\n", "iw_z.reset_vmin_vmax()\n", - "plot_test(\"image-widget-zfish-frame-50-frame-apply-reset\", iw_z.gridplot)" + "plot_test(\"image-widget-zfish-frame-50-frame-apply-reset\", iw_z.figure)" ] }, { @@ -465,7 +465,7 @@ " rgb=[False, True],\n", " histogram_widget=True,\n", " cmap=\"gnuplot2\", \n", - " grid_plot_kwargs = {\"controller_ids\": None},\n", + " figure_kwargs = {\"controller_ids\": None},\n", ")\n", "\n", "iw_mixed_shapes.show()" @@ -479,16 +479,16 @@ "outputs": [], "source": [ "iw_mixed_shapes.sliders[\"t\"].value = 50\n", - "plot_test(\"image-widget-zfish-mixed-rgb-cockatoo-frame-50\", iw_mixed_shapes.gridplot)\n", + "plot_test(\"image-widget-zfish-mixed-rgb-cockatoo-frame-50\", iw_mixed_shapes.figure)\n", "\n", "#Set the data, changing the first array and also the size of the \"T\" slider\n", "iw_mixed_shapes.set_data([zfish_frame_2, movie[:200, :, :, :]], reset_indices=True)\n", - "plot_test(\"image-widget-zfish-mixed-rgb-cockatoo-set-data\", iw_mixed_shapes.gridplot)\n", + "plot_test(\"image-widget-zfish-mixed-rgb-cockatoo-set-data\", iw_mixed_shapes.figure)\n", "\n", "#Check how a window function might work on the RGB data\n", "iw_mixed_shapes.window_funcs = {\"t\": (np.mean, 4)}\n", "iw_mixed_shapes.sliders[\"t\"].value = 20\n", - "plot_test(\"image-widget-zfish-mixed-rgb-cockatoo-windowrgb\", iw_mixed_shapes.gridplot)" + "plot_test(\"image-widget-zfish-mixed-rgb-cockatoo-windowrgb\", iw_mixed_shapes.figure)" ] }, { diff --git a/examples/notebooks/linear_region_selector.ipynb b/examples/notebooks/linear_region_selector.ipynb index 43cea4f81..2ba40ed54 100644 --- a/examples/notebooks/linear_region_selector.ipynb +++ b/examples/notebooks/linear_region_selector.ipynb @@ -19,7 +19,7 @@ "import numpy as np\n", "from ipywidgets import IntRangeSlider, FloatRangeSlider, VBox\n", "\n", - "gp = fpl.GridPlot((2, 2))\n", + "fig = fpl.Figure((2, 2))\n", "\n", "# preallocated size for zoomed data\n", "zoomed_prealloc = 1_000\n", @@ -29,14 +29,14 @@ "sine = np.sin(xs) * 20\n", "\n", "# make sine along x axis\n", - "sine_graphic_x = gp[0, 0].add_line(sine)\n", + "sine_graphic_x = fig[0, 0].add_line(sine)\n", "\n", "# just something that looks different for line along y-axis\n", "sine_y = sine\n", "sine_y[sine_y > 0] = 0\n", "\n", "# sine along y axis\n", - "sine_graphic_y = gp[0, 1].add_line(np.column_stack([sine_y, xs]))\n", + "sine_graphic_y = fig[0, 1].add_line(np.column_stack([sine_y, xs]))\n", "\n", "# offset the position of the graphic to demonstrate `get_selected_data()` later\n", "sine_graphic_y.position_x = 50\n", @@ -50,8 +50,8 @@ "zoomed_init = np.column_stack([np.arange(zoomed_prealloc), np.random.rand(zoomed_prealloc)])\n", "\n", "# make line graphics for displaying zoomed data\n", - "zoomed_x = gp[1, 0].add_line(zoomed_init)\n", - "zoomed_y = gp[1, 1].add_line(zoomed_init)\n", + "zoomed_x = fig[1, 0].add_line(zoomed_init)\n", + "zoomed_y = fig[1, 1].add_line(zoomed_init)\n", "\n", "\n", "def interpolate(subdata: np.ndarray, axis: int):\n", @@ -67,21 +67,21 @@ " \"\"\"sets zoomed x selector data\"\"\"\n", " selected_data = ev.pick_info[\"selected_data\"]\n", " zoomed_x.data = interpolate(selected_data, axis=1) # use the y-values\n", - " gp[1, 0].auto_scale()\n", + " fig[1, 0].auto_scale()\n", "\n", "\n", "def set_zoom_y(ev):\n", " \"\"\"sets zoomed y selector data\"\"\"\n", " selected_data = ev.pick_info[\"selected_data\"]\n", " zoomed_y.data = -interpolate(selected_data, axis=0) # use the x-values\n", - " gp[1, 1].auto_scale()\n", + " fig[1, 1].auto_scale()\n", "\n", "\n", "# update zoomed plots when bounds change\n", "ls_x.selection.add_event_handler(set_zoom_x)\n", "ls_y.selection.add_event_handler(set_zoom_y)\n", "\n", - "gp.show()" + "fig.show()" ] }, { @@ -147,28 +147,25 @@ "metadata": {}, "outputs": [], "source": [ - "import fastplotlib as fpl\n", - "import numpy as np\n", - "\n", "# data to plot\n", "xs = np.linspace(0, 100, 1_000)\n", "sine = np.sin(xs) * 20\n", "cosine = np.cos(xs) * 20\n", "\n", - "plot = fpl.GridPlot((5, 1))\n", + "fig_stack = fpl.Figure((5, 1))\n", "\n", "# sines and cosines\n", "sines = [sine] * 2\n", "cosines = [cosine] * 2\n", "\n", "# make line stack\n", - "line_stack = plot[0, 0].add_line_stack(sines + cosines, separation=50)\n", + "line_stack = fig_stack[0, 0].add_line_stack(sines + cosines, separation=50)\n", "\n", "# make selector\n", "selector = line_stack.add_linear_region_selector()\n", "\n", "# populate subplots with preallocated graphics\n", - "for i, subplot in enumerate(plot):\n", + "for i, subplot in enumerate(fig_stack):\n", " if i == 0:\n", " # skip the first one\n", " continue\n", @@ -182,12 +179,12 @@ " \n", " for i in range(len(zoomed_data)):\n", " data = interpolate(zoomed_data[i], axis=1)\n", - " plot[i + 1, 0][\"zoomed\"].data = data\n", - " plot[i + 1, 0].auto_scale()\n", + " fig_stack[i + 1, 0][\"zoomed\"].data = data\n", + " fig_stack[i + 1, 0].auto_scale()\n", "\n", "\n", "selector.selection.add_event_handler(update_zoomed_subplots)\n", - "plot.show()" + "fig_stack.show()" ] }, { @@ -213,19 +210,19 @@ "sine = np.sin(xs) * 20\n", "cosine = np.cos(xs) * 20\n", "\n", - "plot = fpl.GridPlot((1, 2))\n", + "fig_stack_large = fpl.Figure((1, 2))\n", "\n", "# sines and cosines\n", "sines = [sine] * 1_00\n", "cosines = [cosine] * 1_00\n", "\n", "# make line stack\n", - "line_stack = plot[0, 0].add_line_stack(sines + cosines, separation=50)\n", + "line_stack = fig_stack_large[0, 0].add_line_stack(sines + cosines, separation=50)\n", "\n", "# make selector\n", "stack_selector = line_stack.add_linear_region_selector(padding=200)\n", "\n", - "zoomed_line_stack = plot[0, 1].add_line_stack([zoomed_init] * 2_000, separation=50, name=\"zoomed\")\n", + "zoomed_line_stack = fig_stack_large[0, 1].add_line_stack([zoomed_init] * 2_000, separation=50, name=\"zoomed\")\n", " \n", "def update_zoomed_stack(ev):\n", " \"\"\"update the zoomed subplots\"\"\"\n", @@ -235,11 +232,11 @@ " data = interpolate(zoomed_data[i], axis=1)\n", " zoomed_line_stack.graphics[i].data = data\n", " \n", - " plot[0, 1].auto_scale()\n", + " fig_stack_large[0, 1].auto_scale()\n", "\n", "\n", "stack_selector.selection.add_event_handler(update_zoomed_stack)\n", - "plot.show()" + "fig_stack_large.show()" ] }, { diff --git a/examples/notebooks/linear_selector.ipynb b/examples/notebooks/linear_selector.ipynb index 0f81bc36b..e9c8e664a 100644 --- a/examples/notebooks/linear_selector.ipynb +++ b/examples/notebooks/linear_selector.ipynb @@ -21,14 +21,14 @@ "import numpy as np\n", "from ipywidgets import VBox, IntSlider, FloatSlider\n", "\n", - "plot = fpl.Plot()\n", + "fig = fpl.Figure()\n", "\n", "# data to plot\n", "xs = np.linspace(0, 100, 1000)\n", "sine = np.sin(xs) * 20\n", "\n", "# make sine along x axis\n", - "sine_graphic = plot.add_line(np.column_stack([xs, sine]).astype(np.float32))\n", + "sine_graphic = fig[0, 0].add_line(np.column_stack([xs, sine]).astype(np.float32))\n", "\n", "# make some selectors\n", "selector = sine_graphic.add_linear_selector()\n", @@ -56,8 +56,8 @@ "selector2.add_ipywidget_handler(ipywidget_slider2, step=5)\n", "selector3.add_ipywidget_handler(ipywidget_slider3, step=0.1)\n", "\n", - "plot.auto_scale()\n", - "plot.show(add_widgets=[ipywidget_slider])" + "fig[0, 0].auto_scale()\n", + "fig.show(add_widgets=[ipywidget_slider])" ] }, { @@ -95,9 +95,9 @@ "source": [ "sines = [sine] * 10\n", "\n", - "plot = fpl.Plot()\n", + "fig = fpl.Figure()\n", "\n", - "sine_stack = plot.add_line_stack(sines)\n", + "sine_stack = fig[0, 0].add_line_stack(sines)\n", "\n", "colors = \"y\", \"blue\", \"red\", \"green\"\n", "\n", @@ -108,7 +108,7 @@ " \n", "ss = Synchronizer(*selectors)\n", "\n", - "plot.show()" + "fig.show()" ] }, { diff --git a/examples/notebooks/lineplot.ipynb b/examples/notebooks/lineplot.ipynb index 667cae178..85ebb60f5 100644 --- a/examples/notebooks/lineplot.ipynb +++ b/examples/notebooks/lineplot.ipynb @@ -7,7 +7,7 @@ "tags": [] }, "source": [ - "# A more complex example combing different graphics, gridplot and multiple perspectives" + "# A more complex example combing different graphics, subplots and multiple perspectives" ] }, { @@ -18,7 +18,7 @@ "outputs": [], "source": [ "import numpy as np\n", - "from fastplotlib import GridPlot" + "import fastplotlib as fpl" ] }, { @@ -46,11 +46,11 @@ "metadata": {}, "outputs": [], "source": [ - "# grid with 2 rows and 2 columns\n", + "# figure with 2 rows and 2 columns\n", "shape = (2, 2)\n", "\n", - "# pan-zoom controllers for each view\n", - "# views are synced if they have the \n", + "# pan-zoom controllers for each subplot\n", + "# subplots are synced if they have the\n", "# same controller ID\n", "# in this example the first view has its own controller\n", "# and the last 3 views are synced\n", @@ -59,14 +59,14 @@ " [1, 1]\n", "]\n", "\n", - "# create the grid plot\n", - "grid_plot = GridPlot(\n", + "# create the figure\n", + "fig = fpl.Figure(\n", " shape=shape,\n", - " cameras='3d', # 3D view for all subplots within the grid\n", + " cameras='3d', # 3D view for all subplots within the figure\n", " controller_ids=controller_ids\n", ")\n", "\n", - "for i, subplot in enumerate(grid_plot):\n", + "for i, subplot in enumerate(fig):\n", " # create and add the LineGraphic\n", " line_graphic = subplot.add_line(data=spiral, thickness=3, cmap='jet')\n", " \n", @@ -87,13 +87,13 @@ " if marker_index == spiral.shape[0]:\n", " marker_index = 0\n", " \n", - " for subplot in grid_plot:\n", + " for subplot in fig:\n", " subplot[\"marker\"].data = spiral[marker_index]\n", " \n", "# add `move_marker` to the animations\n", - "grid_plot.add_animations(move_marker)\n", + "fig.add_animations(move_marker)\n", "\n", - "grid_plot.show()" + "fig.show()" ] }, { diff --git a/examples/notebooks/lines_cmap.ipynb b/examples/notebooks/lines_cmap.ipynb index c6dc604b4..dbcbb3e16 100644 --- a/examples/notebooks/lines_cmap.ipynb +++ b/examples/notebooks/lines_cmap.ipynb @@ -55,11 +55,11 @@ }, "outputs": [], "source": [ - "plot = fpl.Plot()\n", + "fig = fpl.Figure()\n", "\n", - "plot.add_line(sine, thickness=10)\n", + "fig[0, 0].add_line(sine, thickness=10)\n", "\n", - "plot.show()" + "fig.show()" ] }, { @@ -71,7 +71,7 @@ }, "outputs": [], "source": [ - "plot_test(\"lines-cmap-white\", plot)" + "plot_test(\"lines-cmap-white\", fig)" ] }, { @@ -91,7 +91,7 @@ }, "outputs": [], "source": [ - "plot.graphics[0].cmap = \"jet\"" + "fig[0, 0].graphics[0].cmap = \"jet\"" ] }, { @@ -104,7 +104,7 @@ "outputs": [], "source": [ "# for testing, ignore\n", - "plot_test(\"lines-cmap-jet\", plot)" + "plot_test(\"lines-cmap-jet\", fig)" ] }, { @@ -116,7 +116,7 @@ }, "outputs": [], "source": [ - "plot.graphics[0].cmap.values = sine[:, 1]" + "fig[0, 0].graphics[0].cmap.values = sine[:, 1]" ] }, { @@ -129,7 +129,7 @@ "outputs": [], "source": [ "# for testing, ignore\n", - "plot_test(\"lines-cmap-jet-values\", plot)" + "plot_test(\"lines-cmap-jet-values\", fig)" ] }, { @@ -141,7 +141,7 @@ }, "outputs": [], "source": [ - "plot.graphics[0].cmap.values = cosine[:, 1]" + "fig[0, 0].graphics[0].cmap.values = cosine[:, 1]" ] }, { @@ -154,7 +154,7 @@ "outputs": [], "source": [ "# for testing, ignore\n", - "plot_test(\"lines-cmap-jet-values-cosine\", plot)" + "plot_test(\"lines-cmap-jet-values-cosine\", fig)" ] }, { @@ -166,7 +166,7 @@ }, "outputs": [], "source": [ - "plot.graphics[0].cmap = \"viridis\"" + "fig[0, 0].graphics[0].cmap = \"viridis\"" ] }, { @@ -179,7 +179,7 @@ "outputs": [], "source": [ "# for testing, ignore\n", - "plot_test(\"lines-cmap-viridis\", plot)" + "plot_test(\"lines-cmap-viridis\", fig)" ] }, { @@ -203,7 +203,7 @@ }, "outputs": [], "source": [ - "plot.graphics[0].cmap.values = cmap_values" + "fig[0, 0].graphics[0].cmap.values = cmap_values" ] }, { @@ -216,7 +216,7 @@ "outputs": [], "source": [ "# for testing, ignore\n", - "plot_test(\"lines-cmap-viridis-values\", plot)" + "plot_test(\"lines-cmap-viridis-values\", fig)" ] }, { @@ -228,7 +228,7 @@ }, "outputs": [], "source": [ - "plot.graphics[0].cmap = \"tab10\"" + "fig[0, 0].graphics[0].cmap = \"tab10\"" ] }, { @@ -239,7 +239,7 @@ "outputs": [], "source": [ "# for testing, ignore\n", - "plot_test(\"lines-cmap-tab-10\", plot)" + "plot_test(\"lines-cmap-tab-10\", fig)" ] }, { diff --git a/examples/notebooks/multiprocessing_zmq/multiprocessing_zmq_plot.ipynb b/examples/notebooks/multiprocessing_zmq/multiprocessing_zmq_plot.ipynb index 2c6e93d8f..564512451 100644 --- a/examples/notebooks/multiprocessing_zmq/multiprocessing_zmq_plot.ipynb +++ b/examples/notebooks/multiprocessing_zmq/multiprocessing_zmq_plot.ipynb @@ -60,13 +60,13 @@ "metadata": {}, "outputs": [], "source": [ - "plot = fpl.Plot()\n", + "fig = fpl.Figure()\n", "\n", "# initialize some data, must be of same dtype and shape as data sent by publisher\n", "data = np.random.rand(512, 512).astype(np.float32)\n", - "plot.add_image(data, name=\"image\")\n", + "fig[0, 0].add_image(data, name=\"image\")\n", "\n", - "def update_frame(p):\n", + "def update_frame(subplot):\n", " # recieve bytes\n", " b = get_bytes()\n", " \n", @@ -75,10 +75,10 @@ " a = np.frombuffer(b, dtype=np.float32).reshape(512, 512)\n", " \n", " # set graphic data\n", - " p[\"image\"].data = a\n", + " subplot[\"image\"].data = a\n", "\n", - "plot.add_animations(update_frame)\n", - "plot.show()" + "fig[0, 0].add_animations(update_frame)\n", + "fig.show()" ] }, { diff --git a/examples/notebooks/nb_test_utils.py b/examples/notebooks/nb_test_utils.py index 90b7158ad..cb84a5271 100644 --- a/examples/notebooks/nb_test_utils.py +++ b/examples/notebooks/nb_test_utils.py @@ -5,7 +5,7 @@ import imageio.v3 as iio import numpy as np -from fastplotlib import Plot, GridPlot +import fastplotlib as fpl # make dirs for screenshots and diffs current_dir = Path(__file__).parent @@ -93,11 +93,11 @@ def _run_tests(): return False -def plot_test(name, plot: Union[Plot, GridPlot]): +def plot_test(name, fig: fpl.Figure): if not _run_tests(): return - snapshot = plot.canvas.snapshot() + snapshot = fig.canvas.snapshot() rgb_img = rgba_to_rgb(snapshot.data) if "REGENERATE_SCREENSHOTS" in os.environ.keys(): diff --git a/examples/notebooks/scatter.ipynb b/examples/notebooks/scatter.ipynb index 9d7ff099f..b78521064 100644 --- a/examples/notebooks/scatter.ipynb +++ b/examples/notebooks/scatter.ipynb @@ -20,7 +20,7 @@ "outputs": [], "source": [ "import numpy as np\n", - "from fastplotlib import GridPlot" + "import fastplotlib as fpl" ] }, { @@ -63,7 +63,7 @@ }, "outputs": [], "source": [ - "# grid with 2 rows and 2 columns\n", + "# figure with 2 rows and 2 columns\n", "shape = (2, 2)\n", "\n", "# define the camera\n", @@ -73,8 +73,8 @@ " ['3d', '2d']\n", "]\n", "\n", - "# pan-zoom controllers for each view\n", - "# views are synced if they have the \n", + "# pan-zoom controllers for each subplot\n", + "# subplots are synced if they have the\n", "# same controller ID\n", "# you can only sync controllers that use the same camera type\n", "# i.e. you cannot sync between 2d and 3d subplots\n", @@ -83,21 +83,21 @@ " [1, 0]\n", "]\n", "\n", - "# create the grid plot\n", - "grid_plot = GridPlot(\n", + "# create the figure\n", + "fig = fpl.Figure(\n", " shape=shape,\n", " cameras=cameras,\n", " controller_ids=controller_ids\n", ")\n", "\n", - "for subplot in grid_plot:\n", + "for subplot in fig:\n", " subplot.add_scatter(data=cloud, colors=colors, alpha=0.7, sizes=5)\n", " \n", " subplot.set_axes_visibility(True)\n", " subplot.set_grid_visibility(True)\n", "\n", "\n", - "grid_plot.show()" + "fig.show()" ] }, { @@ -109,7 +109,7 @@ }, "outputs": [], "source": [ - "grid_plot[0, 1].graphics[0].colors[n_points:int(n_points * 1.5)] = \"r\"" + "fig[0, 1].graphics[0].colors[n_points:int(n_points * 1.5)] = \"r\"" ] }, { @@ -121,7 +121,7 @@ }, "outputs": [], "source": [ - "grid_plot[0, 1].graphics[0].colors[:n_points:10] = \"blue\"" + "fig[0, 1].graphics[0].colors[:n_points:10] = \"blue\"" ] }, { @@ -133,7 +133,7 @@ }, "outputs": [], "source": [ - "grid_plot[0, 1].graphics[0].colors[n_points:] = \"green\"" + "fig[0, 1].graphics[0].colors[n_points:] = \"green\"" ] }, { @@ -145,7 +145,7 @@ }, "outputs": [], "source": [ - "grid_plot[0, 1].graphics[0].colors[n_points:, -1] = 0" + "fig[0, 1].graphics[0].colors[n_points:, -1] = 0" ] }, { @@ -157,7 +157,7 @@ }, "outputs": [], "source": [ - "grid_plot[0, 1].graphics[0].data[:n_points] = grid_plot[0, 1].graphics[0].data[n_points * 2:]" + "fig[0, 1].graphics[0].data[:n_points] = fig[0, 1].graphics[0].data[n_points * 2:]" ] }, { diff --git a/examples/notebooks/scatter_sizes_animation.ipynb b/examples/notebooks/scatter_sizes_animation.ipynb index 06a6b11a2..9ca067bee 100644 --- a/examples/notebooks/scatter_sizes_animation.ipynb +++ b/examples/notebooks/scatter_sizes_animation.ipynb @@ -2,67 +2,37 @@ "cells": [ { "cell_type": "code", - "execution_count": 1, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "5d9f9913391a42af95d4d43d07c17b19", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "RFBOutputContext()" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "9cd08c319b814934a09fd266a1b6322b", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "JupyterOutputContext(children=(JupyterWgpuCanvas(), IpywidgetToolBar(children=(Button(icon='expand-arrows-alt'…" - ] - }, - "execution_count": 1, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "from time import time\n", "\n", "import numpy as np\n", "import fastplotlib as fpl\n", "\n", - "plot = fpl.Plot()\n", + "fig = fpl.Figure()\n", "\n", "points = np.array([[-1,0,1],[-1,0,1]], dtype=np.float32).swapaxes(0,1)\n", "size_delta_scales = np.array([10, 40, 100], dtype=np.float32)\n", "min_sizes = 6\n", "\n", - "def update_positions():\n", + "def update_positions(subplot):\n", " current_time = time()\n", " newPositions = points + np.sin(((current_time / 4) % 1)*np.pi)\n", - " plot.graphics[0].data = newPositions\n", + " subplot.graphics[0].data = newPositions\n", "\n", - "def update_sizes():\n", + "def update_sizes(subplot):\n", " current_time = time()\n", " sin_sample = np.sin(((current_time / 4) % 1)*np.pi)\n", " size_delta = sin_sample*size_delta_scales\n", - " plot.graphics[0].sizes = min_sizes + size_delta\n", + " subplot.graphics[0].sizes = min_sizes + size_delta\n", "\n", - "scatter = plot.add_scatter(points, colors=[\"red\", \"green\", \"blue\"], sizes=12)\n", - "plot.add_animations(update_positions, update_sizes)\n", + "scatter = fig[0, 0].add_scatter(points, colors=[\"red\", \"green\", \"blue\"], sizes=12)\n", + "fig[0, 0].add_animations(update_positions, update_sizes)\n", "\n", - "plot.camera.width = 12\n", - "plot.show(autoscale=False)" + "fig[0, 0].camera.width = 12\n", + "fig.show(autoscale=False)" ] }, { diff --git a/examples/notebooks/scatter_sizes_grid.ipynb b/examples/notebooks/scatter_sizes_grid.ipynb index e152056c9..21985f189 100644 --- a/examples/notebooks/scatter_sizes_grid.ipynb +++ b/examples/notebooks/scatter_sizes_grid.ipynb @@ -16,18 +16,18 @@ "import numpy as np\n", "import fastplotlib as fpl\n", "\n", - "# grid with 2 rows and 3 columns\n", - "grid_shape = (2,1)\n", + "# figure with 2 rows and 3 columns\n", + "shape = (2, 1)\n", "\n", - "# you can give string names for each subplot within the gridplot\n", + "# you can give string names for each subplot\n", "names = [\n", " [\"scalar_size\"],\n", " [\"array_size\"]\n", "]\n", "\n", - "# Create the grid plot\n", - "plot = fpl.GridPlot(\n", - " shape=grid_shape,\n", + "# Create the figure\n", + "fig = fpl.Figure(\n", + " shape=shape,\n", " names=names,\n", " size=(1000, 1000)\n", ")\n", @@ -39,15 +39,15 @@ "\n", "data = np.column_stack([x_values, y_values])\n", "\n", - "plot[\"scalar_size\"].add_scatter(data=data, sizes=5, colors=\"blue\") # add a set of scalar sizes\n", + "fig[\"scalar_size\"].add_scatter(data=data, sizes=5, colors=\"blue\") # add a set of scalar sizes\n", "\n", "non_scalar_sizes = np.abs((y_values / np.pi)) # ensure minimum size of 5\n", - "plot[\"array_size\"].add_scatter(data=data, sizes=non_scalar_sizes, colors=\"red\")\n", + "fig[\"array_size\"].add_scatter(data=data, sizes=non_scalar_sizes, colors=\"red\")\n", "\n", - "for graph in plot:\n", - " graph.auto_scale(maintain_aspect=True)\n", + "for subplot in fig:\n", + " subplot.auto_scale(maintain_aspect=True)\n", "\n", - "plot.show()" + "fig.show()" ] }, { diff --git a/examples/notebooks/simple.ipynb b/examples/notebooks/simple.ipynb index f5901080b..3b42385c8 100644 --- a/examples/notebooks/simple.ipynb +++ b/examples/notebooks/simple.ipynb @@ -53,7 +53,7 @@ }, "outputs": [], "source": [ - "from fastplotlib import Plot\n", + "import fastplotlib as fpl\n", "from ipywidgets import VBox, HBox, IntSlider\n", "import numpy as np" ] @@ -90,17 +90,18 @@ }, "outputs": [], "source": [ - "# create a `Plot` instance\n", - "plot = Plot()\n", + "# create a `Figure` instance\n", + "# by default the figure will have 1 subplot\n", + "fig = fpl.Figure()\n", "\n", "# get a grayscale image\n", "data = iio.imread(\"imageio:camera.png\")\n", "\n", "# plot the image data\n", - "image_graphic = plot.add_image(data=data, name=\"sample-image\")\n", + "image_graphic = fig[0, 0].add_image(data=data, name=\"sample-image\")\n", "\n", "# show the plot\n", - "plot.show(sidecar=True)" + "fig.show(sidecar=True)" ] }, { @@ -221,7 +222,7 @@ "outputs": [], "source": [ "# testing cell, ignore\n", - "plot_test(\"camera\", plot)" + "plot_test(\"camera\", fig)" ] }, { @@ -312,7 +313,7 @@ "outputs": [], "source": [ "# testing cell, ignore\n", - "plot_test(\"astronaut\", plot)" + "plot_test(\"astronaut\", fig)" ] }, { @@ -324,7 +325,7 @@ }, "outputs": [], "source": [ - "plot.canvas.get_logical_size()" + "fig.canvas.get_logical_size()" ] }, { @@ -332,7 +333,7 @@ "id": "b53bc11a-ddf1-4786-8dca-8f3d2eaf993d", "metadata": {}, "source": [ - "### Indexing plots" + "### Indexing subplots" ] }, { @@ -340,7 +341,7 @@ "id": "67b92ffd-40cc-43fe-9df9-0e0d94763d8e", "metadata": {}, "source": [ - "**Plots are indexable and give you their graphics by name**" + "**Subplots are indexable and give you their graphics by name**" ] }, { @@ -352,27 +353,37 @@ }, "outputs": [], "source": [ - "plot" + "fig[0, 0]" ] }, { - "cell_type": "code", - "execution_count": null, - "id": "5b18f4e3-e13b-46d5-af1f-285c5a7fdc12", + "cell_type": "markdown", + "id": "e6eccef1", "metadata": { - "tags": [] + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } }, - "outputs": [], "source": [ - "plot[\"sample-image\"]" + "Access graphics in a subplot" ] }, { - "cell_type": "markdown", - "id": "a64314bf-a737-4858-803b-ea2adbd3578c", - "metadata": {}, + "cell_type": "code", + "execution_count": null, + "id": "7d8eaaf3", + "metadata": { + "collapsed": false, + "is_executing": true, + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], "source": [ - "**You can also use numerical indexing on `plot.graphics`**" + "# by name\n", + "fig[0, 0][\"sample-image\"]" ] }, { @@ -384,7 +395,8 @@ }, "outputs": [], "source": [ - "plot.graphics" + "# or through the .graphics property of a subplot\n", + "fig[0, 0].graphics" ] }, { @@ -396,7 +408,8 @@ }, "outputs": [], "source": [ - "plot.graphics[0]" + "# these are the same!\n", + "fig[0, 0].graphics[0] is fig[0, 0][\"sample-image\"]" ] }, { @@ -428,7 +441,7 @@ }, "outputs": [], "source": [ - "image_graphic == plot[\"sample-image\"]" + "image_graphic == fig[0, 0][\"sample-image\"]" ] }, { @@ -438,8 +451,8 @@ "metadata": {}, "outputs": [], "source": [ - "# close the plot\n", - "plot.close()" + "# close the figure\n", + "fig.close()" ] }, { @@ -461,12 +474,12 @@ }, "outputs": [], "source": [ - "plot_rgb = Plot()\n", + "fig_rgb = fpl.Figure()\n", "\n", - "plot_rgb.add_image(new_data, name=\"rgb-image\")\n", + "fig_rgb[0, 0].add_image(new_data, name=\"rgb-image\")\n", "\n", - "# show the plot\n", - "plot_rgb.show()" + "# show the figure\n", + "fig_rgb.show()" ] }, { @@ -486,7 +499,7 @@ }, "outputs": [], "source": [ - "plot_rgb[\"rgb-image\"].cmap.vmin = 100" + "fig_rgb[0, 0][\"rgb-image\"].cmap.vmin = 100" ] }, { @@ -499,7 +512,7 @@ "outputs": [], "source": [ "# testing cell, ignore\n", - "plot_test(\"astronaut_RGB\", plot_rgb)" + "plot_test(\"astronaut_RGB\", fig_rgb)" ] }, { @@ -509,8 +522,8 @@ "metadata": {}, "outputs": [], "source": [ - "# close plot\n", - "plot_rgb.close()" + "# close figure\n", + "fig_rgb.close()" ] }, { @@ -534,28 +547,35 @@ }, "outputs": [], "source": [ - "# create another `Plot` instance\n", - "plot_v = Plot()\n", + "# create a figure\n", + "fig_v = fpl.Figure()\n", "\n", - "plot.canvas.max_buffered_frames = 1\n", + "fig.canvas.max_buffered_frames = 1\n", "\n", "# make some random data again\n", "data = np.random.rand(512, 512)\n", "\n", "# plot the data\n", - "plot_v.add_image(data=data, name=\"random-image\")\n", + "fig_v[0, 0].add_image(data=data, name=\"random-image\")\n", "\n", "# a function to update the image_graphic\n", - "# a plot will pass its plot instance to the animation function as an argument\n", - "def update_data(plot_instance):\n", + "# a figure-level animation function will optionally take the figure as an argument\n", + "def update_data(figure_instance):\n", " new_data = np.random.rand(512, 512)\n", - " plot_instance[\"random-image\"].data = new_data\n", + " figure_instance[0, 0][\"random-image\"].data = new_data\n", + "\n", + "# you can also add animation functions to individual subplots\n", + "def update_data_subplot(subplot_instance):\n", + " pass\n", "\n", - "#add this as an animation function\n", - "plot_v.add_animations(update_data)\n", + "# add this as an animation function\n", + "fig_v.add_animations(update_data)\n", + "\n", + "# similarly you can add animation function to a subplot\n", + "# fig_v[0, 0].add_animations(update_data_subplot)\n", "\n", "# show the plot\n", - "plot_v.show()" + "fig_v.show()" ] }, { @@ -565,7 +585,7 @@ "source": [ "### We can share controllers across plots\n", "\n", - "This example creates a new plot, but it synchronizes the pan-zoom controller" + "This example creates a new figure, but it share the pan-zoom controllers from the previous figure!" ] }, { @@ -575,21 +595,21 @@ "metadata": {}, "outputs": [], "source": [ - "plot_sync = Plot(controller=plot_v.controller)\n", + "fig_sync = fpl.Figure(controllers=fig_v.controllers)\n", "\n", "data = np.random.rand(512, 512)\n", "\n", - "image_graphic_instance = plot_sync.add_image(data=data, cmap=\"viridis\")\n", + "image_graphic_instance = fig_sync[0, 0].add_image(data=data, cmap=\"viridis\")\n", "\n", "# you will need to define a new animation function for this graphic\n", "def update_data_2():\n", " new_data = np.random.rand(512, 512)\n", - " # alternatively, you can use the stored reference to the graphic as well instead of indexing the Plot\n", + " # alternatively, you can use the stored reference to the graphic as well instead of indexing the subplot\n", " image_graphic_instance.data = new_data\n", "\n", - "plot_sync.add_animations(update_data_2)\n", + "fig_sync.add_animations(update_data_2)\n", "\n", - "plot_sync.show()" + "fig_sync.show()" ] }, { @@ -605,7 +625,7 @@ "id": "d11fabb7-7c76-4e94-893d-80ed9ee3be3d", "metadata": {}, "source": [ - "### You can also use `ipywidgets.VBox` and `HBox` to stack plots. See the `gridplot` notebooks for a proper gridplot interface for more automated subplotting" + "### You can also use `ipywidgets.VBox` and `HBox` to stack plots. See the `subplot` notebooks for more automated subplotting" ] }, { @@ -615,7 +635,7 @@ "metadata": {}, "outputs": [], "source": [ - "HBox([plot_v.show(), plot_sync.show()])" + "HBox([fig_v.show(), fig_sync.show()])" ] }, { @@ -625,9 +645,9 @@ "metadata": {}, "outputs": [], "source": [ - "# close plot\n", - "plot_v.close()\n", - "plot_sync.close()" + "# close figures\n", + "fig_v.close()\n", + "fig_sync.close()" ] }, { @@ -690,21 +710,24 @@ "metadata": {}, "outputs": [], "source": [ - "# Create a plot instance\n", - "plot_l = Plot()\n", + "# Create a figure\n", + "fig_lines = fpl.Figure()\n", + "\n", + "# we will add all the lines to the same subplot\n", + "subplot = fig_lines[0, 0]\n", "\n", "# plot sine wave, use a single color\n", - "sine_graphic = plot_l.add_line(data=sine, thickness=5, colors=\"magenta\")\n", + "sine_graphic = subplot.add_line(data=sine, thickness=5, colors=\"magenta\")\n", "\n", "# you can also use colormaps for lines!\n", - "cosine_graphic = plot_l.add_line(data=cosine, thickness=12, cmap=\"autumn\")\n", + "cosine_graphic = subplot.add_line(data=cosine, thickness=12, cmap=\"autumn\")\n", "\n", "# or a list of colors for each datapoint\n", "colors = [\"r\"] * 25 + [\"purple\"] * 25 + [\"y\"] * 25 + [\"b\"] * 25\n", - "sinc_graphic = plot_l.add_line(data=sinc, thickness=5, colors = colors)\n", + "sinc_graphic = subplot.add_line(data=sinc, thickness=5, colors = colors)\n", "\n", "# show the plot\n", - "plot_l.show(sidecar=True, sidecar_kwargs={\"title\": \"lines\"})" + "fig_lines.show(sidecar=True, sidecar_kwargs={\"title\": \"lines\"})" ] }, { @@ -717,7 +740,7 @@ "outputs": [], "source": [ "# testing cell, ignore\n", - "plot_test(\"lines\", plot_l)" + "plot_test(\"lines\", fig_lines)" ] }, { @@ -729,7 +752,7 @@ "\n", "Set `maintain_aspect = False` on a camera, and then use the right mouse button and move the mouse to stretch and squeeze the view!\n", "\n", - "You can also click the **`1:1`** button to toggle this, or use `plot.camera.maintain_aspect`" + "You can also click the **`1:1`** button to toggle this, or use `subplot.camera.maintain_aspect`" ] }, { @@ -749,7 +772,7 @@ }, "outputs": [], "source": [ - "plot_l.auto_scale(maintain_aspect=True)" + "subplot.auto_scale(maintain_aspect=True)" ] }, { @@ -824,7 +847,7 @@ "outputs": [], "source": [ "# testing cell, ignore\n", - "plot_test(\"lines-colors\", plot_l)" + "plot_test(\"lines-colors\", fig_lines)" ] }, { @@ -832,7 +855,7 @@ "id": "c29f81f9-601b-49f4-b20c-575c56e58026", "metadata": {}, "source": [ - "## Graphic _data_ is also indexable" + "## Graphic _data_ is also slicable and settable" ] }, { @@ -866,7 +889,7 @@ "outputs": [], "source": [ "# testing cell, ignore\n", - "plot_test(\"lines-data\", plot_l)" + "plot_test(\"lines-data\", fig_lines)" ] }, { @@ -912,7 +935,7 @@ "metadata": {}, "outputs": [], "source": [ - "sinc_graphic.present.add_event_handler(plot_l.auto_scale)" + "sinc_graphic.present.add_event_handler(subplot.auto_scale)" ] }, { @@ -954,12 +977,12 @@ "source": [ "img = iio.imread(\"imageio:camera.png\")\n", "\n", - "plot_l.add_image(img[::20, ::20], name=\"image\", cmap=\"gray\")\n", + "subplot.add_image(img[::20, ::20], name=\"image\", cmap=\"gray\")\n", "\n", "# z axis position -1 so it is below all the lines\n", - "plot_l[\"image\"].position_z = -1\n", - "plot_l[\"image\"].position_x = -8\n", - "plot_l[\"image\"].position_y = -8" + "subplot[\"image\"].position_z = -1\n", + "subplot[\"image\"].position_x = -8\n", + "subplot[\"image\"].position_y = -8" ] }, { @@ -972,7 +995,7 @@ "outputs": [], "source": [ "# testing cell, ignore\n", - "plot_test(\"lines-underlay\", plot_l)" + "plot_test(\"lines-underlay\", fig_lines)" ] }, { @@ -983,7 +1006,7 @@ "outputs": [], "source": [ "# close plot\n", - "plot_l.close()" + "fig_lines.close()" ] }, { @@ -1001,8 +1024,8 @@ "metadata": {}, "outputs": [], "source": [ - "# just set the camera as \"3d\", the rest is basically the same :D \n", - "plot_l3d = Plot(camera='3d')\n", + "# just set the camera as \"3d\", the rest is basically the same :D\n", + "fig_l3d = fpl.Figure(cameras=\"3d\")\n", "\n", "# create a spiral\n", "phi = np.linspace(0, 30, 200)\n", @@ -1015,9 +1038,9 @@ "# note: you usually mix 3D and 2D graphics on the same plot\n", "spiral = np.dstack([xs, ys, zs])[0]\n", "\n", - "plot_l3d.add_line(data=spiral, thickness=2, cmap='winter')\n", + "fig_l3d[0, 0].add_line(data=spiral, thickness=2, cmap='winter')\n", "\n", - "plot_l3d.show()" + "fig_l3d.show()" ] }, { @@ -1037,7 +1060,7 @@ }, "outputs": [], "source": [ - "plot_l3d.auto_scale(maintain_aspect=True)" + "fig_l3d[0, 0].auto_scale(maintain_aspect=True)" ] }, { @@ -1050,7 +1073,7 @@ "outputs": [], "source": [ "# testing cell, ignore\n", - "plot_test(\"lines-3d\", plot_l3d)" + "plot_test(\"lines-3d\", fig_l3d)" ] }, { @@ -1061,7 +1084,23 @@ "outputs": [], "source": [ "# change the FOV of the persepctive camera\n", - "plot_l3d.camera.fov = 70" + "fig_l3d[0, 0].camera.fov = 70" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9e126e6c", + "metadata": { + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "# change the controller, ex. from the current \"fly\" controller to a \"panzoom\" controller\n", + "fig_l3d[0, 0].controller = \"panzoom\"" ] }, { @@ -1074,7 +1113,7 @@ "outputs": [], "source": [ "# close plot\n", - "plot_l3d.close()" + "fig_l3d.close()" ] }, { @@ -1089,18 +1128,6 @@ "#### There might be a small delay for a few seconds before the plot shows, this is due to shaders being compiled and a few other things. The plot should be very fast and responsive once it is displayed and future modifications should also be fast!" ] }, - { - "cell_type": "code", - "execution_count": null, - "id": "2ecb2385-8fa4-4239-881c-b754c24aed9f", - "metadata": {}, - "outputs": [], - "source": [ - "from fastplotlib import Plot\n", - "from ipywidgets import VBox, HBox, IntSlider\n", - "import numpy as np" - ] - }, { "cell_type": "code", "execution_count": null, @@ -1135,12 +1162,12 @@ "colors = [\"yellow\"] * n_points + [\"cyan\"] * n_points + [\"magenta\"] * n_points\n", "\n", "# create plot\n", - "plot_s = Plot()\n", - "\n", + "fig_scatter = fpl.Figure()\n", + "subplot_scatter = fig_scatter[0, 0]\n", "# use an alpha value since this will be a lot of points\n", - "scatter_graphic = plot_s.add_scatter(data=cloud, sizes=3, colors=colors, alpha=0.7)\n", + "scatter_graphic = subplot_scatter.add_scatter(data=cloud, sizes=3, colors=colors, alpha=0.7)\n", "\n", - "plot_s.show()" + "fig_scatter.show()" ] }, { @@ -1221,8 +1248,8 @@ "metadata": {}, "outputs": [], "source": [ - "plot_s.camera = \"3d\"\n", - "plot_s.controller = \"fly\"" + "subplot_scatter.camera = \"3d\"\n", + "subplot_scatter.controller = \"fly\"" ] }, { @@ -1233,7 +1260,7 @@ "outputs": [], "source": [ "# close plot\n", - "plot_s.close()" + "fig_scatter.close()" ] }, { diff --git a/examples/notebooks/gridplot.ipynb b/examples/notebooks/subplots.ipynb similarity index 72% rename from examples/notebooks/gridplot.ipynb rename to examples/notebooks/subplots.ipynb index f1ceb2180..72b4b3007 100644 --- a/examples/notebooks/gridplot.ipynb +++ b/examples/notebooks/subplots.ipynb @@ -5,7 +5,7 @@ "id": "3cfc2d9f-6a09-42f4-a47c-3ba51f1a1801", "metadata": {}, "source": [ - "### More in-depth on `GridPlot`" + "### More in-depth on subplots with a Figure" ] }, { @@ -16,7 +16,7 @@ "outputs": [], "source": [ "import numpy as np\n", - "from fastplotlib import GridPlot" + "import fastplotlib as fpl" ] }, { @@ -27,10 +27,10 @@ "outputs": [], "source": [ "# grid with 2 rows and 3 columns\n", - "grid_shape = (2, 3)\n", + "shape = (2, 3)\n", "\n", - "# pan-zoom controllers for each view\n", - "# views are synced if they have the \n", + "# pan-zoom controllers for each subplot\n", + "# subplots are synced if they have the\n", "# same controller ID\n", "controller_ids = [\n", " [0, -3, 1], # id each controller with an integer\n", @@ -44,22 +44,22 @@ "]\n", "\n", "\n", - "# you can give string names for each subplot within the gridplot\n", + "# you can give string names for each subplot within the figure\n", "names = [\n", " [\"subplot0\", \"subplot1\", \"subplot2\"],\n", " [\"subplot3\", \"subplot4\", \"subplot5\"]\n", "]\n", "\n", - "# Create the grid plot\n", - "grid_plot = GridPlot(\n", - " shape=grid_shape,\n", + "# Create the figure\n", + "fig = fpl.Figure(\n", + " shape=shape,\n", " controller_ids=controller_ids,\n", " names=names,\n", ")\n", "\n", "\n", "# Make a random image graphic for each subplot\n", - "for subplot in grid_plot:\n", + "for subplot in fig:\n", " data = np.random.rand(512, 512)\n", " # create and add an ImageGraphic\n", " subplot.add_image(data=data, name=\"rand-image\")\n", @@ -73,8 +73,8 @@ " subplot[\"rand-image\"].data = new_data\n", "\n", "# add the animation\n", - "grid_plot.add_animations(set_random_frame)\n", - "grid_plot.show()" + "fig.add_animations(set_random_frame)\n", + "fig.show()" ] }, { @@ -82,18 +82,20 @@ "id": "2867bcd6-7691-4073-91d9-9c33e8fdb896", "metadata": {}, "source": [ - "### Indexing the gridplot to access subplots" + "### Accessing subplots" ] }, { "cell_type": "code", "execution_count": null, "id": "2a6f7eb5-776e-42a6-b6c2-c26009a26795", - "metadata": {}, + "metadata": { + "is_executing": true + }, "outputs": [], "source": [ - "# can access subplot by name\n", - "grid_plot[\"subplot0\"]" + "# by name\n", + "fig[\"subplot0\"]" ] }, { @@ -103,8 +105,8 @@ "metadata": {}, "outputs": [], "source": [ - "# can access subplot by index\n", - "grid_plot[0, 0]" + "# by index\n", + "fig[0, 0]" ] }, { @@ -112,7 +114,7 @@ "id": "3272b8b3-3063-47a4-94c8-15ceeeaecc69", "metadata": {}, "source": [ - "## subplots also support indexing!\n", + "## getting graphics within subplots!\n", "this can be used to get graphics if they are named" ] }, @@ -124,7 +126,7 @@ "outputs": [], "source": [ "# can access graphic directly via name\n", - "grid_plot[\"subplot0\"][\"rand-image\"]" + "fig[\"subplot0\"][\"rand-image\"]" ] }, { @@ -134,17 +136,39 @@ "metadata": {}, "outputs": [], "source": [ - "grid_plot[\"subplot0\"][\"rand-image\"].cmap.vmin = 0.6\n", - "grid_plot[\"subplot0\"][\"rand-image\"].cmap.vmax = 0.8" + "fig[\"subplot0\"][\"rand-image\"].cmap.vmin = 0.6\n", + "fig[\"subplot0\"][\"rand-image\"].cmap.vmax = 0.8" ] }, { "cell_type": "markdown", - "id": "516a46e1-cc53-4137-b49b-d5fb94e212d7", - "metadata": {}, "source": [ - "### positional indexing also works event if subplots have string names" - ] + "If they are not named use .graphics" + ], + "metadata": { + "collapsed": false + } + }, + { + "cell_type": "code", + "execution_count": null, + "outputs": [], + "source": [ + "fig[\"subplot0\"].graphics" + ], + "metadata": { + "collapsed": false, + "is_executing": true + } + }, + { + "cell_type": "markdown", + "source": [ + "### positional indexing also works" + ], + "metadata": { + "collapsed": false + } }, { "cell_type": "code", @@ -153,8 +177,8 @@ "metadata": {}, "outputs": [], "source": [ - "grid_plot[1, 0][\"rand-image\"].cmap.vim = 0.1\n", - "grid_plot[1, 0][\"rand-image\"].cmap.vmax = 0.3" + "fig[1, 0][\"rand-image\"].cmap.vim = 0.1\n", + "fig[1, 0][\"rand-image\"].cmap.vmax = 0.3" ] }, { diff --git a/examples/notebooks/gridplot_simple.ipynb b/examples/notebooks/subplots_simple.ipynb similarity index 69% rename from examples/notebooks/gridplot_simple.ipynb rename to examples/notebooks/subplots_simple.ipynb index 74807f55a..e519584d3 100644 --- a/examples/notebooks/gridplot_simple.ipynb +++ b/examples/notebooks/subplots_simple.ipynb @@ -5,12 +5,12 @@ "id": "0e42f03b-9cdf-484f-b158-78b07fdf524d", "metadata": {}, "source": [ - "## This notebook shows how you can use more of the `fastplotlib` API to create `Graphic` objects and add them to a `GridPlot`" + "## This notebook shows how you can use more of the `fastplotlib` API to create `Graphic` objects and add them to subplots" ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 1, "id": "5171a06e-1bdc-4908-9726-3c1fd45dbb9d", "metadata": { "ExecuteTime": { @@ -19,10 +19,43 @@ }, "tags": [] }, - "outputs": [], + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "301d76bd4c5c42c7912cdd28651e2899", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "Image(value=b'\\x89PNG\\r\\n\\x1a\\n\\x00\\x00\\x00\\rIHDR\\x00\\x00\\x01,\\x00\\x00\\x007\\x08\\x06\\x00\\x00\\x00\\xb6\\x1bw\\x99\\x…" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Unable to find extension: VK_EXT_swapchain_colorspace\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Available devices:\n", + "✅ (default) | AMD RADV POLARIS10 (ACO) | DiscreteGPU | Vulkan | Mesa 20.3.5 (ACO)\n", + "❗ | llvmpipe (LLVM 11.0.1, 256 bits) | CPU | Vulkan | Mesa 20.3.5 (LLVM 11.0.1)\n", + "✅ | NVIDIA GeForce RTX 3080 | DiscreteGPU | Vulkan | 530.30.02\n", + "❗ | Radeon RX 570 Series (POLARIS10, DRM 3.40.0, 5.10.0-21-amd64, LLVM 11.0.1) | Unknown | OpenGL | \n" + ] + } + ], "source": [ "import numpy as np\n", - "from fastplotlib import GridPlot" + "import fastplotlib as fpl" ] }, { @@ -38,29 +71,29 @@ }, "outputs": [], "source": [ - "# GridPlot of shape 2 x 3 with all controllers synced\n", - "grid_plot = GridPlot(shape=(2, 3), controller_ids=\"sync\")\n", + "# Figure of shape 2 x 3 with all controllers synced\n", + "fig = fpl.Figure(shape=(2, 3), controller_ids=\"sync\")\n", "\n", "# Make a random image graphic for each subplot\n", - "for subplot in grid_plot:\n", + "for subplot in fig:\n", " # create image data\n", " data = np.random.rand(512, 512)\n", " # add an image to the subplot\n", " subplot.add_image(data, name=\"rand-img\")\n", "\n", "# Define a function to update the image graphics with new data\n", - "# add_animations will pass the gridplot to the animation function\n", - "def update_data(gp):\n", - " for sp in gp:\n", + "# add_animations will pass the figure to the animation function\n", + "def update_data(f):\n", + " for sp in f:\n", " new_data = np.random.rand(512, 512)\n", " # index the image graphic by name and set the data\n", " sp[\"rand-img\"].data = new_data\n", " \n", "# add the animation function\n", - "grid_plot.add_animations(update_data)\n", + "fig.add_animations(update_data)\n", "\n", - "# show the gridplot \n", - "grid_plot.show()" + "# show the figure\n", + "fig.show()" ] }, { @@ -68,7 +101,7 @@ "id": "e7801781-c3e9-490f-ab12-1cd2f480d3e9", "metadata": {}, "source": [ - "## Accessing subplots within `GridPlot`" + "## Accessing subplots within `Figure`" ] }, { @@ -78,7 +111,7 @@ "metadata": {}, "outputs": [], "source": [ - "grid_plot" + "fig" ] }, { @@ -92,7 +125,7 @@ "source": [ "# positional indexing\n", "# row 0 and col 0\n", - "grid_plot[0, 0]" + "fig[0, 0]" ] }, { @@ -100,7 +133,7 @@ "id": "276dfede-e9bc-4488-b9e6-3ca5cf91e4dc", "metadata": {}, "source": [ - "### You can get the graphics within a subplot, just like with simple `Plot`" + "### You can get the graphics within a subplot" ] }, { @@ -112,7 +145,7 @@ }, "outputs": [], "source": [ - "grid_plot[0, 1].graphics" + "fig[0, 1].graphics" ] }, { @@ -132,7 +165,7 @@ }, "outputs": [], "source": [ - "grid_plot[0, 1].graphics[0].cmap.vmax = 0.5" + "fig[0, 1].graphics[0].cmap.vmax = 0.5" ] }, { @@ -140,7 +173,7 @@ "id": "00506fa1-2dc0-4435-96a0-e50667d3174f", "metadata": {}, "source": [ - "### more indexing with `GridPlot`" + "### more indexing" ] }, { @@ -153,7 +186,7 @@ "outputs": [], "source": [ "# you can give subplots human-readable string names\n", - "grid_plot[0, 2].name = \"top-right-plot\"" + "fig[0, 2].name = \"top-right-plot\"" ] }, { @@ -165,7 +198,7 @@ }, "outputs": [], "source": [ - "grid_plot[\"top-right-plot\"]" + "fig[\"top-right-plot\"]" ] }, { @@ -178,7 +211,7 @@ "outputs": [], "source": [ "# view its position\n", - "grid_plot[\"top-right-plot\"].position" + "fig[\"top-right-plot\"].position" ] }, { @@ -191,7 +224,7 @@ "outputs": [], "source": [ "# these are really the same\n", - "grid_plot[\"top-right-plot\"] is grid_plot[0, 2]" + "fig[\"top-right-plot\"] is fig[0, 2]" ] }, { @@ -211,7 +244,7 @@ }, "outputs": [], "source": [ - "grid_plot[\"top-right-plot\"][\"rand-img\"].cmap.vmin = 0.5" + "fig[\"top-right-plot\"][\"rand-img\"].cmap.vmin = 0.5" ] }, { @@ -223,7 +256,7 @@ }, "outputs": [], "source": [ - "grid_plot.close()" + "fig.close()" ] }, { diff --git a/examples/notebooks/test_gc.ipynb b/examples/notebooks/test_gc.ipynb index 6caf6a9e3..39f964cf7 100644 --- a/examples/notebooks/test_gc.ipynb +++ b/examples/notebooks/test_gc.ipynb @@ -59,12 +59,12 @@ "metadata": {}, "outputs": [], "source": [ - "gp = fpl.GridPlot((2, 2))\n", + "fig = fpl.Figure((2, 2))\n", "\n", - "line = gp[0, 0].add_line(points_data, name=\"line\")\n", - "scatter = gp[0, 1].add_scatter(points_data.copy(), name=\"scatter\")\n", - "line_stack = gp[1, 0].add_line_stack(line_collection_data, name=\"line-stack\")\n", - "image = gp[1, 1].add_image(img_data, name=\"image\")\n", + "line = fig[0, 0].add_line(points_data, name=\"line\")\n", + "scatter = fig[0, 1].add_scatter(points_data.copy(), name=\"scatter\")\n", + "line_stack = fig[1, 0].add_line_stack(line_collection_data, name=\"line-stack\")\n", + "image = fig[1, 1].add_image(img_data, name=\"image\")\n", "\n", "linear_sel = line.add_linear_selector(name=\"line_linear_sel\")\n", "linear_region_sel = line.add_linear_region_selector(name=\"line_region_sel\")\n", @@ -88,7 +88,7 @@ "\n", "\n", "objects = list()\n", - "for subplot in gp:\n", + "for subplot in fig:\n", " objects += subplot.objects\n", "\n", "\n", @@ -100,7 +100,7 @@ " f = getattr(g, feature)\n", " f.add_event_handler(feature_changed_handler)\n", "\n", - "gp.show()" + "fig.show()" ] }, { @@ -110,7 +110,7 @@ "metadata": {}, "outputs": [], "source": [ - "gp.clear()" + "fig.clear()" ] }, { diff --git a/examples/tests/test_examples.py b/examples/tests/test_examples.py index a570b4f36..b8369e368 100644 --- a/examples/tests/test_examples.py +++ b/examples/tests/test_examples.py @@ -69,7 +69,7 @@ def test_example_screenshots(module, force_offscreen): example = importlib.import_module(module_name) # render a frame - img = np.asarray(example.plot.renderer.target.draw()) + img = np.asarray(example.fig.renderer.target.draw()) # check if _something_ was rendered assert img is not None and img.size > 0 diff --git a/fastplotlib/__init__.py b/fastplotlib/__init__.py index 33db8c79d..8e6341156 100644 --- a/fastplotlib/__init__.py +++ b/fastplotlib/__init__.py @@ -1,12 +1,13 @@ from pathlib import Path -from .layouts import Plot, GridPlot +from .utils.gui import run from .graphics import * from .graphics.selectors import * from .legends import * +from .layouts import Figure + from .widgets import ImageWidget from .utils import config -from .utils.gui import run import wgpu @@ -21,9 +22,8 @@ __all__ = [ - "Plot", - "GridPlot", + "Figure", "run", - "ImageWidget", + # "ImageWidget", "Legend", ] diff --git a/fastplotlib/layouts/__init__.py b/fastplotlib/layouts/__init__.py index aaed4c5a4..60111cabc 100644 --- a/fastplotlib/layouts/__init__.py +++ b/fastplotlib/layouts/__init__.py @@ -1,4 +1,3 @@ -from ._gridplot import GridPlot -from ._plot import Plot +from ._figure import Figure -__all__ = ["Plot", "GridPlot"] +__all__ = ["Figure"] diff --git a/fastplotlib/layouts/_gridplot.py b/fastplotlib/layouts/_figure.py similarity index 67% rename from fastplotlib/layouts/_gridplot.py rename to fastplotlib/layouts/_figure.py index 472d3dd2e..7e83e103a 100644 --- a/fastplotlib/layouts/_gridplot.py +++ b/fastplotlib/layouts/_figure.py @@ -1,4 +1,9 @@ +import os from itertools import product, chain +from multiprocessing import Queue +from pathlib import Path +from time import time + import numpy as np from typing import Literal, Iterable from inspect import getfullargspec @@ -8,17 +13,17 @@ from wgpu.gui import WgpuCanvasBase -from ._frame import Frame +from ._video_writer import VideoWriterAV from ._utils import make_canvas_and_renderer, create_controller, create_camera from ._utils import controller_types as valid_controller_types from ._subplot import Subplot -from ._record_mixin import RecordMixin +from .. import ImageGraphic -class GridPlot(Frame, RecordMixin): +class Figure: def __init__( self, - shape: tuple[int, int], + shape: tuple[int, int] = (1, 1), cameras: ( Literal["2d", "3d"] | Iterable[Iterable[Literal["2d", "3d"]]] @@ -46,7 +51,7 @@ def __init__( Parameters ---------- - shape: (int, int) + shape: (int, int), default (1, 1) (n_rows, n_cols) cameras: "2d", "3", list of "2d" | "3d", Iterable of camera instances, or Iterable of "2d" | "3d", optional @@ -76,7 +81,7 @@ def __init__( controllers: pygfx.Controller | list[pygfx.Controller] | np.ndarray[pygfx.Controller], optional directly provide pygfx.Controller instances(s). Useful if you want to use a controller from an existing - plot/subplot. Other controller kwargs, i.e. ``controller_Types`` and ``controller_ids`` are ignored if + plot/subplot. Other controller kwargs, i.e. ``controller_types`` and ``controller_ids`` are ignored if ``controllers`` are provided. canvas: WgpuCanvas, optional @@ -97,7 +102,7 @@ def __init__( if names is not None: if len(list(chain(*names))) != len(self): raise ValueError( - "must provide same number of subplot `names` as specified by gridplot shape" + "must provide same number of subplot `names` as specified by Figure `shape`" ) subplot_names = np.asarray(names).reshape(self.shape) @@ -141,7 +146,7 @@ def __init__( pass else: raise TypeError( - "controllers argument must be a single pygfx.Controller instance of a Iterable of " + "controllers argument must be a single pygfx.Controller instance, or a Iterable of " "pygfx.Controller instances" ) @@ -326,8 +331,22 @@ def __init__( self._starting_size = size - RecordMixin.__init__(self) - Frame.__init__(self) + self._output = None + + if self.canvas.__class__.__name__ == "JupyterWgpuCanvas": + self.recorder = FigureRecorder(self) + else: + self.recorder = None + + @property + def toolbar(self): + """ipywidget or QToolbar instance""" + return self._output.toolbar + + @property + def output(self): + """ipywidget or QWidget that contains this plot""" + return self._output @property def shape(self) -> tuple[int, int]: @@ -336,12 +355,12 @@ def shape(self) -> tuple[int, int]: @property def canvas(self) -> WgpuCanvasBase: - """The canvas associated to this GridPlot""" + """The canvas associated to this Figure""" return self._canvas @property def renderer(self) -> pygfx.WgpuRenderer: - """The renderer associated to this GridPlot""" + """The renderer associated to this Figure""" return self._renderer @property @@ -391,6 +410,114 @@ def render(self): # call post-render animate functions self._call_animate_functions(self._animate_funcs_post) + def start_render(self): + """start render cycle""" + self.canvas.request_draw(self.render) + self.canvas.set_logical_size(*self._starting_size) + + def show( + self, + autoscale: bool = True, + maintain_aspect: bool = None, + toolbar: bool = True, + sidecar: bool = False, + sidecar_kwargs: dict = None, + add_widgets: list = None, + ): + """ + Begins the rendering event loop and shows the plot in the desired output context (jupyter, qt or glfw). + + Parameters + ---------- + autoscale: bool, default ``True`` + autoscale the Scene + + maintain_aspect: bool, default ``True`` + maintain aspect ratio + + toolbar: bool, default ``True`` + show toolbar + + sidecar: bool, default ``True`` + display plot in a ``jupyterlab-sidecar``, only for jupyter output context + + sidecar_kwargs: dict, default ``None`` + kwargs for sidecar instance to display plot + i.e. title, layout + + add_widgets: list of widgets + a list of ipywidgets or QWidget that are vertically stacked below the plot + + Returns + ------- + OutputContext + In jupyter, it will display the plot in the output cell or sidecar + + In Qt, it will display the Plot, toolbar, etc. as stacked widget, use `Plot.widget` to access it. + """ + + # show was already called, return existing output context + if self._output is not None: + return self._output + + self.start_render() + + if sidecar_kwargs is None: + sidecar_kwargs = dict() + + if add_widgets is None: + add_widgets = list() + + # flip y-axis if ImageGraphics are present + for subplot in self: + for g in subplot.graphics: + if isinstance(g, ImageGraphic): + subplot.camera.local.scale_y *= -1 + break + + if autoscale: + for subplot in self: + if maintain_aspect is None: + _maintain_aspect = subplot.camera.maintain_aspect + else: + _maintain_aspect = maintain_aspect + subplot.auto_scale(maintain_aspect=_maintain_aspect, zoom=0.95) + + # used for generating images in docs using nbsphinx + if "NB_SNAPSHOT" in os.environ.keys(): + if os.environ["NB_SNAPSHOT"] == "1": + return self.canvas.snapshot() + + # return the appropriate OutputContext based on the current canvas + if self.canvas.__class__.__name__ == "JupyterWgpuCanvas": + from .output.jupyter_output import ( + JupyterOutputContext, + ) # noqa - inline import + + self._output = JupyterOutputContext( + frame=self, + make_toolbar=toolbar, + use_sidecar=sidecar, + sidecar_kwargs=sidecar_kwargs, + add_widgets=add_widgets, + ) + + elif self.canvas.__class__.__name__ == "QWgpuCanvas": + from .output.qt_output import QOutputContext # noqa - inline import + + self._output = QOutputContext( + frame=self, make_toolbar=toolbar, add_widgets=add_widgets + ) + + else: # assume GLFW, the output context is just the canvas + self._output = self.canvas + + # return the output context, this call is required for jupyter but not for Qt + return self._output + + def close(self): + self.output.close() + def _call_animate_functions(self, funcs: list[callable]): for fn in funcs: try: @@ -413,7 +540,7 @@ def add_animations( ): """ Add function(s) that are called on every render cycle. - These are called at the GridPlot level. + These are called at the Figure level. Parameters ---------- @@ -492,3 +619,157 @@ def __repr__(self): f"\t{newline.join(subplot.__str__() for subplot in self)}" f"\n" ) + + +class FigureRecorder: + def __init__(self, figure: Figure): + self._figure = figure + self._video_writer: VideoWriterAV = None + self._video_writer_queue = Queue() + self._record_fps = 25 + self._record_timer = 0 + self._record_start_time = 0 + + def _record(self): + """ + Sends frame to VideoWriter through video writer queue + """ + # current time + t = time() + + # put frame in queue only if enough time as passed according to the desired framerate + # otherwise it tries to record EVERY frame on every rendering cycle, which just blocks the rendering + if t - self._record_timer < (1 / self._record_fps): + return + + # reset timer + self._record_timer = t + + if self._video_writer is not None: + ss = self._figure.canvas.snapshot() + # exclude alpha channel + self._video_writer_queue.put(ss.data[..., :-1]) + + def start( + self, + path: str | Path, + fps: int = 25, + codec: str = "mpeg4", + pixel_format: str = "yuv420p", + options: dict = None, + ): + """ + Start a recording, experimental. Call ``record_end()`` to end a recording. + Note: playback duration does not exactly match recording duration. + + Requires PyAV: https://github.com/PyAV-Org/PyAV + + **Do not resize canvas during a recording, the width and height must remain constant!** + + Parameters + ---------- + path: str or Path + path to save the recording + + fps: int, default ``25`` + framerate, do not use > 25 within jupyter + + codec: str, default "mpeg4" + codec to use, see ``ffmpeg`` list: https://www.ffmpeg.org/ffmpeg-codecs.html . + In general, ``"mpeg4"`` should work on most systems. ``"libx264"`` is a + better option if you have it installed. + + pixel_format: str, default "yuv420p" + pixel format + + options: dict, optional + Codec options. For example, if using ``"mpeg4"`` you can use ``{"q:v": "20"}`` to set the quality between + 1-31, where "1" is highest and "31" is lowest. If using ``"libx264"``` you can use ``{"crf": "30"}`` where + the "crf" value is between "0" (highest quality) and "50" (lowest quality). See ``ffmpeg`` docs for more + info on codec options + + Examples + -------- + + With ``"mpeg4"`` + + .. code-block:: python + + # start recording video + figure.recorder.start("./video.mp4", options={"q:v": "20"} + + # do stuff like interacting with the plot, change things, etc. + + # end recording + figure.recorder.stop() + + With ``"libx264"`` + + .. code-block:: python + + # start recording video + figure.recorder.start("./vid_x264.mp4", codec="libx264", options={"crf": "25"}) + + # do stuff like interacting with the plot, change things, etc. + + # end recording + figure.recorder.stop() + + """ + + if Path(path).exists(): + raise FileExistsError(f"File already exists at given path: {path}") + + # queue for sending frames to VideoWriterAV process + self._video_writer_queue = Queue() + + # snapshot to get canvas width height + ss = self._figure.canvas.snapshot() + + # writer process + self._video_writer = VideoWriterAV( + path=str(path), + queue=self._video_writer_queue, + fps=int(fps), + width=ss.width, + height=ss.height, + codec=codec, + pixel_format=pixel_format, + options=options, + ) + + # start writer process + self._video_writer.start() + + # 1.3 seems to work well to reduce that difference between playback time and recording time + # will properly investigate later + self._record_fps = fps * 1.3 + self._record_start_time = time() + + # record timer used to maintain desired framerate + self._record_timer = time() + + self._figure.add_animations(self._record) + + def stop(self) -> float: + """ + End a current recording. Returns the real duration of the recording + + Returns + ------- + float + recording duration + """ + + # tell video writer that recording has finished + self._video_writer_queue.put(None) + + # wait for writer to finish + self._video_writer.join(timeout=5) + + self._video_writer = None + + # so self._record() is no longer called on every render cycle + self._figure.remove_animation(self._record) + + return time() - self._record_start_time diff --git a/fastplotlib/layouts/_frame/__init__.py b/fastplotlib/layouts/_frame/__init__.py deleted file mode 100644 index c34884022..000000000 --- a/fastplotlib/layouts/_frame/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from ._frame import Frame diff --git a/fastplotlib/layouts/_frame/_frame.py b/fastplotlib/layouts/_frame/_frame.py deleted file mode 100644 index 219a59082..000000000 --- a/fastplotlib/layouts/_frame/_frame.py +++ /dev/null @@ -1,152 +0,0 @@ -import os - -from ...graphics import ImageGraphic -from ._toolbar import ToolBar - - -class Frame: - """ - Mixin class for Plot and GridPlot that "frames" the plot. - - Gives them their `show()` call that returns the appropriate output context. - """ - - def __init__(self): - self._output = None - - @property - def toolbar(self) -> ToolBar: - """ipywidget or QToolbar instance""" - return self._output.toolbar - - @property - def widget(self): - """ipywidget or QWidget that contains this plot""" - # @caitlin: this is the same as the output context, but I figure widget is a simpler public name - return self._output - - def render(self): - """render call implemented in subclass""" - raise NotImplemented - - def _autoscale_init(self, maintain_aspect: bool): - """autoscale function that is called only during show()""" - if hasattr(self, "_subplots"): - for subplot in self: - if maintain_aspect is None: - _maintain_aspect = subplot.camera.maintain_aspect - else: - _maintain_aspect = maintain_aspect - subplot.auto_scale(maintain_aspect=_maintain_aspect, zoom=0.95) - else: - if maintain_aspect is None: - maintain_aspect = self.camera.maintain_aspect - self.auto_scale(maintain_aspect=maintain_aspect, zoom=0.95) - - def start_render(self): - """start render cycle""" - self.canvas.request_draw(self.render) - self.canvas.set_logical_size(*self._starting_size) - - def show( - self, - autoscale: bool = True, - maintain_aspect: bool = None, - toolbar: bool = True, - sidecar: bool = False, - sidecar_kwargs: dict = None, - add_widgets: list = None, - ): - """ - Begins the rendering event loop and shows the plot in the desired output context (jupyter, qt or glfw). - - Parameters - ---------- - autoscale: bool, default ``True`` - autoscale the Scene - - maintain_aspect: bool, default ``True`` - maintain aspect ratio - - toolbar: bool, default ``True`` - show toolbar - - sidecar: bool, default ``True`` - display plot in a ``jupyterlab-sidecar``, only for jupyter output context - - sidecar_kwargs: dict, default ``None`` - kwargs for sidecar instance to display plot - i.e. title, layout - - add_widgets: list of widgets - a list of ipywidgets or QWidget that are vertically stacked below the plot - - Returns - ------- - OutputContext - In jupyter, it will display the plot in the output cell or sidecar - - In Qt, it will display the Plot, toolbar, etc. as stacked widget, use `Plot.widget` to access it. - """ - - # show was already called, return existing output context - if self._output is not None: - return self._output - - self.start_render() - - if sidecar_kwargs is None: - sidecar_kwargs = dict() - - if add_widgets is None: - add_widgets = list() - - # flip y axis if ImageGraphics are present - if hasattr(self, "_subplots"): - for subplot in self: - for g in subplot.graphics: - if isinstance(g, ImageGraphic): - subplot.camera.local.scale_y *= -1 - break - else: - for g in self.graphics: - if isinstance(g, ImageGraphic): - self.camera.local.scale_y *= -1 - break - - if autoscale: - self._autoscale_init(maintain_aspect) - - # used for generating images in docs using nbsphinx - if "NB_SNAPSHOT" in os.environ.keys(): - if os.environ["NB_SNAPSHOT"] == "1": - return self.canvas.snapshot() - - # return the appropriate OutputContext based on the current canvas - if self.canvas.__class__.__name__ == "JupyterWgpuCanvas": - from ._jupyter_output import JupyterOutputContext # noqa - inline import - - self._output = JupyterOutputContext( - frame=self, - make_toolbar=toolbar, - use_sidecar=sidecar, - sidecar_kwargs=sidecar_kwargs, - add_widgets=add_widgets, - ) - - elif self.canvas.__class__.__name__ == "QWgpuCanvas": - from ._qt_output import QOutputContext # noqa - inline import - - self._output = QOutputContext( - frame=self, make_toolbar=toolbar, add_widgets=add_widgets - ) - - else: # assume GLFW, the output context is just the canvas - self._output = self.canvas - - # return the output context, this call is required for jupyter but not for Qt - return self._output - - def close(self): - """Close the output context""" - self._output.close() diff --git a/fastplotlib/layouts/_frame/_qt_toolbar.py b/fastplotlib/layouts/_frame/_qt_toolbar.py deleted file mode 100644 index d62994c2d..000000000 --- a/fastplotlib/layouts/_frame/_qt_toolbar.py +++ /dev/null @@ -1,250 +0,0 @@ -from datetime import datetime -from functools import partial -from math import copysign -import traceback -from typing import * - -from ...utils.gui import QtCore, QtWidgets -from ...graphics.selectors import PolygonSelector -from ._toolbar import ToolBar -from ._qtoolbar_template import Ui_QToolbar - - -class QToolbar( - ToolBar, QtWidgets.QWidget -): # inheritance order MUST be Toolbar first, QWidget second! Else breaks - """Toolbar for Qt context""" - - def __init__(self, output_context, plot): - QtWidgets.QWidget.__init__(self, parent=output_context) - ToolBar.__init__(self, plot) - - # initialize UI - self.ui = Ui_QToolbar() - self.ui.setupUi(self) - - # connect button events - self.ui.auto_scale_button.clicked.connect(self.auto_scale_handler) - self.ui.center_button.clicked.connect(self.center_scene_handler) - self.ui.panzoom_button.toggled.connect(self.panzoom_handler) - self.ui.maintain_aspect_button.toggled.connect(self.maintain_aspect_handler) - self.ui.y_direction_button.clicked.connect(self.y_direction_handler) - - # the subplot labels that update when a user click on subplots - if hasattr(self.plot, "_subplots"): - subplot = self.plot[0, 0] - # set label from first subplot name - if subplot.name is not None: - name = subplot.name - else: - name = str(subplot.position) - - # here we will just use a simple label, not a dropdown like ipywidgets - # the dropdown implementation is tedious with Qt - self.ui.current_subplot = QtWidgets.QLabel(parent=self) - self.ui.current_subplot.setText(name) - self.ui.horizontalLayout.addWidget(self.ui.current_subplot) - - # update the subplot label when a subplot is clicked into - self.plot.renderer.add_event_handler(self.update_current_subplot, "click") - - self.setMaximumHeight(35) - - # set the initial values for buttons - self.ui.maintain_aspect_button.setChecked( - self.current_subplot.camera.maintain_aspect - ) - self.ui.panzoom_button.setChecked(self.current_subplot.controller.enabled) - - if copysign(1, self.current_subplot.camera.local.scale_y) == -1: - self.ui.y_direction_button.setText("v") - else: - self.ui.y_direction_button.setText("^") - - def update_current_subplot(self, ev): - """update the text label for the current subplot""" - for subplot in self.plot: - pos = subplot.map_screen_to_world((ev.x, ev.y)) - if pos is not None: - if subplot.name is not None: - name = subplot.name - else: - name = str(subplot.position) - self.ui.current_subplot.setText(name) - - # set buttons w.r.t. current subplot - self.ui.panzoom_button.setChecked(subplot.controller.enabled) - self.ui.maintain_aspect_button.setChecked( - subplot.camera.maintain_aspect - ) - - if copysign(1, subplot.camera.local.scale_y) == -1: - self.ui.y_direction_button.setText("v") - else: - self.ui.y_direction_button.setText("^") - - def _get_subplot_dropdown_value(self) -> str: - return self.ui.current_subplot.text() - - def auto_scale_handler(self, *args): - self.current_subplot.auto_scale( - maintain_aspect=self.current_subplot.camera.maintain_aspect - ) - - def center_scene_handler(self, *args): - self.current_subplot.center_scene() - - def panzoom_handler(self, value: bool): - self.current_subplot.controller.enabled = value - - def maintain_aspect_handler(self, value: bool): - for camera in self.current_subplot.controller.cameras: - camera.maintain_aspect = value - - def y_direction_handler(self, *args): - # flip every camera under the same controller - for camera in self.current_subplot.controller.cameras: - camera.local.scale_y *= -1 - - if copysign(1, self.current_subplot.camera.local.scale_y) == -1: - self.ui.y_direction_button.setText("v") - else: - self.ui.y_direction_button.setText("^") - - def record_handler(self, ev): - if self.ui.record_button.isChecked(): - try: - self.plot.record_start( - f"./{datetime.now().isoformat(timespec='seconds').replace(':', '_')}.mp4" - ) - except Exception: - traceback.print_exc() - self.ui.record_button.setChecked(False) - else: - self.plot.record_stop() - - def add_polygon(self, *args): - ps = PolygonSelector(edge_width=3, edge_color="mageneta") - self.current_subplot.add_graphic(ps, center=False) - - -# TODO: There must be a better way to do this -# TODO: Check if an interface exists between ipywidgets and Qt -class SliderInterface: - """ - This exists so that ImageWidget has a common interface for Sliders. - - This interface makes a QSlider behave somewhat like a ipywidget IntSlider, enough for ImageWidget to function. - """ - - def __init__(self, qslider): - self.qslider = qslider - - @property - def value(self) -> int: - return self.qslider.value() - - @value.setter - def value(self, value: int): - self.qslider.setValue(value) - - @property - def max(self) -> int: - return self.qslider.maximum() - - @max.setter - def max(self, value: int): - self.qslider.setMaximum(value) - - @property - def min(self): - return self.qslider.minimum() - - @min.setter - def min(self, value: int): - self.qslider.setMinimum(value) - - -class QToolbarImageWidget(QtWidgets.QWidget): - """Toolbar for ImageWidget""" - - def __init__(self, image_widget): - QtWidgets.QWidget.__init__(self) - - # vertical layout - self.vlayout = QtWidgets.QVBoxLayout(self) - - self.image_widget = image_widget - - hlayout_buttons = QtWidgets.QHBoxLayout() - - self.reset_vmin_vmax_button = QtWidgets.QPushButton(self) - self.reset_vmin_vmax_button.setText("auto-contrast") - self.reset_vmin_vmax_button.clicked.connect(self.image_widget.reset_vmin_vmax) - hlayout_buttons.addWidget(self.reset_vmin_vmax_button) - - self.reset_vmin_vmax_hlut_button = QtWidgets.QPushButton(self) - self.reset_vmin_vmax_hlut_button.setText("reset histogram-lut") - self.reset_vmin_vmax_hlut_button.clicked.connect( - self.image_widget.reset_vmin_vmax_frame - ) - hlayout_buttons.addWidget(self.reset_vmin_vmax_hlut_button) - - self.vlayout.addLayout(hlayout_buttons) - - self.sliders: Dict[str, SliderInterface] = dict() - - # has time and/or z-volume - if self.image_widget.ndim > 2: - # create a slider, spinbox and dimension label for each dimension in the ImageWidget - for dim in self.image_widget.slider_dims: - hlayout = ( - QtWidgets.QHBoxLayout() - ) # horizontal stack for label, slider, spinbox - - # max value for current dimension - max_val = self.image_widget._dims_max_bounds[dim] - 1 - - # make slider - slider = QtWidgets.QSlider(self) - slider.setOrientation(QtCore.Qt.Orientation.Horizontal) - slider.setMinimum(0) - slider.setMaximum(max_val) - slider.setValue(0) - slider.setSingleStep(1) - slider.setPageStep(10) - - # make spinbox - spinbox = QtWidgets.QSpinBox(self) - spinbox.setMinimum(0) - spinbox.setMaximum(max_val) - spinbox.setValue(0) - spinbox.setSingleStep(1) - - # link slider and spinbox - slider.valueChanged.connect(spinbox.setValue) - spinbox.valueChanged.connect(slider.setValue) - - # connect slider to change the index within the dimension - slider.valueChanged.connect( - partial(self.image_widget._slider_value_changed, dim) - ) - - # slider dimension label - slider_label = QtWidgets.QLabel(self) - slider_label.setText(dim) - - # add the widgets to the horizontal layout - hlayout.addWidget(slider_label) - hlayout.addWidget(slider) - hlayout.addWidget(spinbox) - - # add horizontal layout to the vertical layout - self.vlayout.addLayout(hlayout) - - # add to sliders dict for easier access to users - self.sliders[dim] = SliderInterface(slider) - - max_height = 35 + (35 * len(self.sliders.keys())) - - self.setMaximumHeight(max_height) diff --git a/fastplotlib/layouts/_plot.py b/fastplotlib/layouts/_plot.py deleted file mode 100644 index 4656649e6..000000000 --- a/fastplotlib/layouts/_plot.py +++ /dev/null @@ -1,68 +0,0 @@ -from typing import * - -import pygfx -from wgpu.gui import WgpuCanvasBase - -from ._subplot import Subplot -from ._frame import Frame -from ._record_mixin import RecordMixin - - -class Plot(Subplot, Frame, RecordMixin): - def __init__( - self, - canvas: Union[str, WgpuCanvasBase] = None, - renderer: pygfx.WgpuRenderer = None, - camera: Union[str, pygfx.PerspectiveCamera] = "2d", - controller: Union[str, pygfx.Controller] = None, - size: Tuple[int, int] = (500, 300), - **kwargs, - ): - """ - Simple Plot object. - - Parameters - ---------- - canvas: WgpuCanvas, optional - Canvas for drawing - - renderer: pygfx.Renderer, optional - pygfx renderer instance - - camera: str or pygfx.PerspectiveCamera, optional - | One of ``"2d"`` or ``"3d"`` indicating 2D or 3D camera - - controller: str or pygfx.Controller, optional - Usually ``None``, you can pass an existing controller from another - ``Plot`` or ``Subplot`` to synchronize them. - - You can also pass str arguments of valid controller names, see Subplot docstring for valid names - - size: (int, int) - starting size of canvas, default (500, 300) - - kwargs - passed to Subplot, for example ``name`` - - """ - super().__init__( - parent=None, - position=(0, 0), - parent_dims=(1, 1), - canvas=canvas, - renderer=renderer, - camera=camera, - controller=controller, - **kwargs, - ) - RecordMixin.__init__(self) - Frame.__init__(self) - - self._starting_size = size - - def render(self): - """performs a single render of the plot, not for the user""" - super().render() - - self.renderer.flush() - self.canvas.request_draw() diff --git a/fastplotlib/layouts/_plot_area.py b/fastplotlib/layouts/_plot_area.py index 37a25bbcc..bbc5b0e15 100644 --- a/fastplotlib/layouts/_plot_area.py +++ b/fastplotlib/layouts/_plot_area.py @@ -92,7 +92,7 @@ def get_refcounts(self): def __init__( self, - parent: Union["PlotArea", "GridPlot"], + parent: Union["PlotArea", "Figure"], position: tuple[int, int] | str, camera: pygfx.PerspectiveCamera, controller: pygfx.Controller, @@ -103,11 +103,11 @@ def __init__( ): """ Base class for plot creation and management. ``PlotArea`` is not intended to be instantiated by users - but rather to provide functionality for ``subplot`` in ``gridplot`` and single ``plot``. + but rather to provide functionality for ``subplots`` in a user ``Figure`` Parameters ---------- - parent: PlotArea or GridPlot + parent: PlotArea or Figure parent object position: Any @@ -181,7 +181,7 @@ def parent(self): @property def position(self) -> tuple[int, int] | str: - """Position of this plot area within a larger layout (such as GridPlot) if relevant""" + """Position of this plot area within a larger layout (such as a Figure) if relevant""" return self._position @property @@ -265,7 +265,7 @@ def controller(self, new_controller: str | pygfx.Controller): # TODO: monkeypatch until we figure out a better # pygfx plans on refactoring viewports anyways if self.parent is not None: - if self.parent.__class__.__name__ == "GridPlot": + if self.parent.__class__.__name__ == "Figure": for subplot in self.parent: if subplot.camera in cameras_list: new_controller.register_events(subplot.viewport) @@ -312,7 +312,7 @@ def get_rect(self) -> tuple[float, float, float, float]: Returns the viewport rect to define the rectangle occupied by the viewport w.r.t. the Canvas. - If this is a subplot within a GridPlot, it returns the rectangle + If this is a subplot within a Figure, it returns the rectangle for only this subplot w.r.t. the parent canvas. Must return: [x_pos, y_pos, width_viewport, height_viewport] diff --git a/fastplotlib/layouts/_record_mixin.py b/fastplotlib/layouts/_record_mixin.py deleted file mode 100644 index 59a8e92e4..000000000 --- a/fastplotlib/layouts/_record_mixin.py +++ /dev/null @@ -1,241 +0,0 @@ -from pathlib import Path -from multiprocessing import Queue, Process -from time import time - - -def _get_av(): - try: - import av - except ImportError: - raise ModuleNotFoundError( - "Recording to video file requires `av`:\n" - "https://github.com/PyAV-Org/PyAV" - ) from None - else: - return av - - -class VideoWriterAV(Process): - """Video writer, uses PyAV in an external process to write frames to disk""" - - def __init__( - self, - path: Path | str, - queue: Queue, - fps: int, - width: int, - height: int, - codec: str, - pixel_format: str, - options: dict = None, - ): - super().__init__() - self.queue = queue - - av = _get_av() - self.container = av.open(path, mode="w") - - self.stream = self.container.add_stream(codec, rate=fps, options=options) - - # in case libx264, trim last rows and/or column - # because libx264 doesn't like non-even number width or height - if width % 2 != 0: - width -= 1 - if height % 2 != 0: - height -= 1 - - self.stream.width = width - self.stream.height = height - - self.stream.pix_fmt = pixel_format - - def run(self): - av = _get_av() - while True: - if self.queue.empty(): # no frame to write - continue - - frame = self.queue.get() - - # recording has ended - if frame is None: - self.container.close() - break - - frame = av.VideoFrame.from_ndarray( - frame[ - : self.stream.height, : self.stream.width - ], # trim if necessary because of x264 - format="rgb24", - ) - - for packet in self.stream.encode(frame): - self.container.mux(packet) - - # I don't exactly know what this does, copied from pyav example - for packet in self.stream.encode(): - self.container.mux(packet) - - # close file - self.container.close() - - # close process, release resources - self.close() - - -# adds recording functionality to GridPlot and Plot -class RecordMixin: - def __init__(self): - self._video_writer: VideoWriterAV = None - self._video_writer_queue = Queue() - self._record_fps = 25 - self._record_timer = 0 - self._record_start_time = 0 - - def _record(self): - """ - Sends frame to VideoWriter through video writer queue - """ - # current time - t = time() - - # put frame in queue only if enough time as passed according to the desired framerate - # otherwise it tries to record EVERY frame on every rendering cycle, which just blocks the rendering - if t - self._record_timer < (1 / self._record_fps): - return - - # reset timer - self._record_timer = t - - if self._video_writer is not None: - ss = self.canvas.snapshot() - # exclude alpha channel - self._video_writer_queue.put(ss.data[..., :-1]) - - def record_start( - self, - path: str | Path, - fps: int = 25, - codec: str = "mpeg4", - pixel_format: str = "yuv420p", - options: dict = None, - ): - """ - Start a recording, experimental. Call ``record_end()`` to end a recording. - Note: playback duration does not exactly match recording duration. - - Requires PyAV: https://github.com/PyAV-Org/PyAV - - **Do not resize canvas during a recording, the width and height must remain constant!** - - Parameters - ---------- - path: str or Path - path to save the recording - - fps: int, default ``25`` - framerate, do not use > 25 within jupyter - - codec: str, default "mpeg4" - codec to use, see ``ffmpeg`` list: https://www.ffmpeg.org/ffmpeg-codecs.html . - In general, ``"mpeg4"`` should work on most systems. ``"libx264"`` is a - better option if you have it installed. - - pixel_format: str, default "yuv420p" - pixel format - - options: dict, optional - Codec options. For example, if using ``"mpeg4"`` you can use ``{"q:v": "20"}`` to set the quality between - 1-31, where "1" is highest and "31" is lowest. If using ``"libx264"``` you can use ``{"crf": "30"}`` where - the "crf" value is between "0" (highest quality) and "50" (lowest quality). See ``ffmpeg`` docs for more - info on codec options - - Examples - -------- - - With ``"mpeg4"`` - - .. code-block:: python - - # create a plot or gridplot etc - - # start recording video - plot.record_start("./video.mp4", options={"q:v": "20"} - - # do stuff like interacting with the plot, change things, etc. - - # end recording - plot.record_end() - - With ``"libx264"`` - - .. code-block:: python - - # create a plot or gridplot etc - - # start recording video - plot.record_start("./vid_x264.mp4", codec="libx264", options={"crf": "25"}) - - # do stuff like interacting with the plot, change things, etc. - - # end recording - plot.record_end() - - """ - - if Path(path).exists(): - raise FileExistsError(f"File already exists at given path: {path}") - - # queue for sending frames to VideoWriterAV process - self._video_writer_queue = Queue() - - # snapshot to get canvas width height - ss = self.canvas.snapshot() - - # writer process - self._video_writer = VideoWriterAV( - path=str(path), - queue=self._video_writer_queue, - fps=int(fps), - width=ss.width, - height=ss.height, - codec=codec, - pixel_format=pixel_format, - options=options, - ) - - # start writer process - self._video_writer.start() - - # 1.3 seems to work well to reduce that difference between playback time and recording time - # will properly investigate later - self._record_fps = fps * 1.3 - self._record_start_time = time() - - # record timer used to maintain desired framerate - self._record_timer = time() - - self.add_animations(self._record) - - def record_stop(self) -> float: - """ - End a current recording. Returns the real duration of the recording - - Returns - ------- - float - recording duration - """ - - # tell video writer that recording has finished - self._video_writer_queue.put(None) - - # wait for writer to finish - self._video_writer.join(timeout=5) - - self._video_writer = None - - # so self._record() is no longer called on every render cycle - self.remove_animation(self._record) - - return time() - self._record_start_time diff --git a/fastplotlib/layouts/_subplot.py b/fastplotlib/layouts/_subplot.py index 4b1e92c51..a541c9d78 100644 --- a/fastplotlib/layouts/_subplot.py +++ b/fastplotlib/layouts/_subplot.py @@ -15,7 +15,7 @@ class Subplot(PlotArea, GraphicMethodsMixin): def __init__( self, - parent: Union["GridPlot", None] = None, + parent: Union["Figure", None] = None, position: tuple[int, int] = None, parent_dims: tuple[int, int] = None, camera: Literal["2d", "3d"] | pygfx.PerspectiveCamera = "2d", @@ -29,22 +29,22 @@ def __init__( name: str = None, ): """ - General plot object that composes a ``Gridplot``. Each ``Gridplot`` instance will have [n rows, n columns] + General plot object is found within a ``Figure``. Each ``Figure`` instance will have [n rows, n columns] of subplots. .. important:: - ``Subplot`` is not meant to be constructed directly, it only exists as part of a ``GridPlot`` + ``Subplot`` is not meant to be constructed directly, it only exists as part of a ``Figure`` Parameters ---------- - parent: 'GridPlot' | None - parent GridPlot instance + parent: 'Figure' | None + parent Figure instance position: (int, int), optional - corresponds to the [row, column] position of the subplot within a ``Gridplot`` + corresponds to the [row, column] position of the subplot within a ``Figure`` parent_dims: (int, int), optional - dimensions of the parent ``GridPlot`` + dimensions of the parent ``Figure`` camera: str or pygfx.PerspectiveCamera, default '2d' indicates the FOV for the camera, '2d' sets ``fov = 0``, '3d' sets ``fov = 50``. diff --git a/fastplotlib/layouts/_video_writer.py b/fastplotlib/layouts/_video_writer.py new file mode 100644 index 000000000..b7e111b50 --- /dev/null +++ b/fastplotlib/layouts/_video_writer.py @@ -0,0 +1,82 @@ +from pathlib import Path +from multiprocessing import Queue, Process + + +def _get_av(): + try: + import av + except ImportError: + raise ModuleNotFoundError( + "Recording to video file requires `av`:\n" + "https://github.com/PyAV-Org/PyAV" + ) from None + else: + return av + + +class VideoWriterAV(Process): + """Video writer, uses PyAV in an external process to write frames to disk""" + + def __init__( + self, + path: Path | str, + queue: Queue, + fps: int, + width: int, + height: int, + codec: str, + pixel_format: str, + options: dict = None, + ): + super().__init__() + self.queue = queue + + av = _get_av() + self.container = av.open(path, mode="w") + + self.stream = self.container.add_stream(codec, rate=fps, options=options) + + # in case libx264, trim last rows and/or column + # because libx264 doesn't like non-even number width or height + if width % 2 != 0: + width -= 1 + if height % 2 != 0: + height -= 1 + + self.stream.width = width + self.stream.height = height + + self.stream.pix_fmt = pixel_format + + def run(self): + av = _get_av() + while True: + if self.queue.empty(): # no frame to write + continue + + frame = self.queue.get() + + # recording has ended + if frame is None: + self.container.close() + break + + frame = av.VideoFrame.from_ndarray( + frame[ + : self.stream.height, : self.stream.width + ], # trim if necessary because of x264 + format="rgb24", + ) + + for packet in self.stream.encode(frame): + self.container.mux(packet) + + # I don't exactly know what this does, copied from pyav example + for packet in self.stream.encode(): + self.container.mux(packet) + + # close file + self.container.close() + + # close process, release resources + self.close() diff --git a/fastplotlib/layouts/output/__init__.py b/fastplotlib/layouts/output/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/fastplotlib/layouts/_frame/_ipywidget_toolbar.py b/fastplotlib/layouts/output/_ipywidget_toolbar.py similarity index 54% rename from fastplotlib/layouts/_frame/_ipywidget_toolbar.py rename to fastplotlib/layouts/output/_ipywidget_toolbar.py index 5b42c8eab..787c8d442 100644 --- a/fastplotlib/layouts/_frame/_ipywidget_toolbar.py +++ b/fastplotlib/layouts/output/_ipywidget_toolbar.py @@ -2,20 +2,14 @@ from datetime import datetime from itertools import product from math import copysign -from functools import partial from pathlib import Path from ipywidgets.widgets import ( - IntSlider, - VBox, HBox, ToggleButton, Dropdown, Layout, Button, - BoundedIntText, - Play, - jslink, Image, ) @@ -27,8 +21,8 @@ class IpywidgetToolBar(HBox, ToolBar): """Basic toolbar using ipywidgets""" - def __init__(self, plot): - ToolBar.__init__(self, plot) + def __init__(self, figure): + ToolBar.__init__(self, figure) self._auto_scale_button = Button( value=False, @@ -107,27 +101,26 @@ def __init__(self, plot): ) widgets.append(image) - if hasattr(self.plot, "_subplots"): - positions = list( - product(range(self.plot.shape[0]), range(self.plot.shape[1])) - ) - values = list() - for pos in positions: - if self.plot[pos].name is not None: - values.append(self.plot[pos].name) - else: - values.append(str(pos)) - - self._dropdown = Dropdown( - options=values, - disabled=False, - description="Subplots:", - layout=Layout(width="200px"), - ) + positions = list( + product(range(self.figure.shape[0]), range(self.figure.shape[1])) + ) + values = list() + for pos in positions: + if self.figure[pos].name is not None: + values.append(self.figure[pos].name) + else: + values.append(str(pos)) + + self._dropdown = Dropdown( + options=values, + disabled=False, + description="Subplots:", + layout=Layout(width="200px"), + ) - self.plot.renderer.add_event_handler(self.update_current_subplot, "click") + self.figure.renderer.add_event_handler(self.update_current_subplot, "click") - widgets.append(self._dropdown) + widgets.append(self._dropdown) self._panzoom_controller_button.observe(self.panzoom_handler, "value") self._auto_scale_button.on_click(self.auto_scale_handler) @@ -176,7 +169,7 @@ def y_direction_handler(self, obj): self._y_direction_button.icon = "arrow-up" def update_current_subplot(self, ev): - for subplot in self.plot: + for subplot in self.figure: pos = subplot.map_screen_to_world((ev.x, ev.y)) if pos is not None: # update self.dropdown @@ -195,137 +188,15 @@ def update_current_subplot(self, ev): def record_plot(self, obj): if self._record_button.value: try: - self.plot.record_start( + self.figure.recorder.start( f"./{datetime.now().isoformat(timespec='seconds').replace(':', '_')}.mp4" ) except Exception: traceback.print_exc() self._record_button.value = False else: - self.plot.record_stop() + self.figure.recorder.stop() def add_polygon(self, obj): ps = PolygonSelector(edge_width=3, edge_color="magenta") self.current_subplot.add_graphic(ps, center=False) - - -class IpywidgetImageWidgetToolbar(VBox): - def __init__(self, iw): - """ - Basic toolbar for a ImageWidget instance. - - Parameters - ---------- - plot: - """ - self.iw = iw - - self.reset_vminvmax_button = Button( - value=False, - disabled=False, - icon="adjust", - layout=Layout(width="auto"), - tooltip="reset vmin/vmax", - ) - - self.reset_vminvmax_hlut_button = Button( - value=False, - icon="adjust", - description="reset", - layout=Layout(width="auto"), - tooltip="reset vmin/vmax and reset histogram using current frame", - ) - - self.sliders: dict[str, IntSlider] = dict() - - # only for xy data, no time point slider needed - if self.iw.ndim == 2: - widgets = [self.reset_vminvmax_button] - # for txy, tzxy, etc. data - else: - for dim in self.iw.slider_dims: - slider = IntSlider( - min=0, - max=self.iw._dims_max_bounds[dim] - 1, - step=1, - value=0, - description=f"dimension: {dim}", - orientation="horizontal", - ) - - slider.observe( - partial(self.iw._slider_value_changed, dim), names="value" - ) - - self.sliders[dim] = slider - - self.step_size_setter = BoundedIntText( - value=1, - min=1, - max=self.sliders["t"].max, - step=1, - description="Step Size:", - disabled=False, - description_tooltip="set slider step", - layout=Layout(width="150px"), - ) - self.speed_text = BoundedIntText( - value=100, - min=1, - max=1_000, - step=50, - description="Speed", - disabled=False, - description_tooltip="Playback speed, this is NOT framerate.\nArbitrary units between 1 - 1,000", - layout=Layout(width="150px"), - ) - self.play_button = Play( - value=0, - min=self.sliders["t"].min, - max=self.sliders["t"].max, - step=self.sliders["t"].step, - description="play/pause", - disabled=False, - ) - widgets = [ - self.reset_vminvmax_button, - self.reset_vminvmax_hlut_button, - self.play_button, - self.step_size_setter, - self.speed_text, - ] - - self.play_button.interval = 10 - - self.step_size_setter.observe(self._change_stepsize, "value") - self.speed_text.observe(self._change_framerate, "value") - jslink((self.play_button, "value"), (self.sliders["t"], "value")) - jslink((self.play_button, "max"), (self.sliders["t"], "max")) - - self.reset_vminvmax_button.on_click(self._reset_vminvmax) - self.reset_vminvmax_hlut_button.on_click(self._reset_vminvmax_frame) - - self.iw.gridplot.renderer.add_event_handler(self._set_slider_layout, "resize") - - # the buttons - self.hbox = HBox(widgets) - - super().__init__((self.hbox, *list(self.sliders.values()))) - - def _reset_vminvmax(self, obj): - self.iw.reset_vmin_vmax() - - def _reset_vminvmax_frame(self, obj): - self.iw.reset_vmin_vmax_frame() - - def _change_stepsize(self, obj): - self.sliders["t"].step = self.step_size_setter.value - - def _change_framerate(self, change): - interval = int(1000 / change["new"]) - self.play_button.interval = interval - - def _set_slider_layout(self, *args): - w, h = self.iw.gridplot.renderer.logical_size - for k, v in self.sliders.items(): - v.layout = Layout(width=f"{w}px") diff --git a/fastplotlib/layouts/output/_qt_toolbar.py b/fastplotlib/layouts/output/_qt_toolbar.py new file mode 100644 index 000000000..4334f1369 --- /dev/null +++ b/fastplotlib/layouts/output/_qt_toolbar.py @@ -0,0 +1,125 @@ +from datetime import datetime +from math import copysign +import traceback + +from ...utils.gui import QtWidgets +from ...graphics.selectors import PolygonSelector +from ._toolbar import ToolBar +from ._qtoolbar_template import Ui_QToolbar + + +class QToolbar( + ToolBar, QtWidgets.QWidget +): # inheritance order MUST be Toolbar first, QWidget second! Else breaks + """Toolbar for Qt context""" + + def __init__(self, output_context, figure): + QtWidgets.QWidget.__init__(self, parent=output_context) + ToolBar.__init__(self, figure) + + # initialize UI + self.ui = Ui_QToolbar() + self.ui.setupUi(self) + + # connect button events + self.ui.auto_scale_button.clicked.connect(self.auto_scale_handler) + self.ui.center_button.clicked.connect(self.center_scene_handler) + self.ui.panzoom_button.toggled.connect(self.panzoom_handler) + self.ui.maintain_aspect_button.toggled.connect(self.maintain_aspect_handler) + self.ui.y_direction_button.clicked.connect(self.y_direction_handler) + + # subplot labels update when a user click on subplots + subplot = self.figure[0, 0] + # set label from first subplot name + if subplot.name is not None: + name = subplot.name + else: + name = str(subplot.position) + + # here we will just use a simple label, not a dropdown like ipywidgets + # the dropdown implementation is tedious with Qt + self.ui.current_subplot = QtWidgets.QLabel(parent=self) + self.ui.current_subplot.setText(name) + self.ui.horizontalLayout.addWidget(self.ui.current_subplot) + + # update the subplot label when a subplot is clicked into + self.figure.renderer.add_event_handler(self.update_current_subplot, "click") + + self.setMaximumHeight(35) + + # set the initial values for buttons + self.ui.maintain_aspect_button.setChecked( + self.current_subplot.camera.maintain_aspect + ) + self.ui.panzoom_button.setChecked(self.current_subplot.controller.enabled) + + if copysign(1, self.current_subplot.camera.local.scale_y) == -1: + self.ui.y_direction_button.setText("v") + else: + self.ui.y_direction_button.setText("^") + + def update_current_subplot(self, ev): + """update the text label for the current subplot""" + for subplot in self.figure: + pos = subplot.map_screen_to_world((ev.x, ev.y)) + if pos is not None: + if subplot.name is not None: + name = subplot.name + else: + name = str(subplot.position) + self.ui.current_subplot.setText(name) + + # set buttons w.r.t. current subplot + self.ui.panzoom_button.setChecked(subplot.controller.enabled) + self.ui.maintain_aspect_button.setChecked( + subplot.camera.maintain_aspect + ) + + if copysign(1, subplot.camera.local.scale_y) == -1: + self.ui.y_direction_button.setText("v") + else: + self.ui.y_direction_button.setText("^") + + def _get_subplot_dropdown_value(self) -> str: + return self.ui.current_subplot.text() + + def auto_scale_handler(self, *args): + self.current_subplot.auto_scale( + maintain_aspect=self.current_subplot.camera.maintain_aspect + ) + + def center_scene_handler(self, *args): + self.current_subplot.center_scene() + + def panzoom_handler(self, value: bool): + self.current_subplot.controller.enabled = value + + def maintain_aspect_handler(self, value: bool): + for camera in self.current_subplot.controller.cameras: + camera.maintain_aspect = value + + def y_direction_handler(self, *args): + # flip every camera under the same controller + for camera in self.current_subplot.controller.cameras: + camera.local.scale_y *= -1 + + if copysign(1, self.current_subplot.camera.local.scale_y) == -1: + self.ui.y_direction_button.setText("v") + else: + self.ui.y_direction_button.setText("^") + + def record_handler(self, ev): + if self.ui.record_button.isChecked(): + try: + self.figure.record_start( + f"./{datetime.now().isoformat(timespec='seconds').replace(':', '_')}.mp4" + ) + except Exception: + traceback.print_exc() + self.ui.record_button.setChecked(False) + else: + self.figure.record_stop() + + def add_polygon(self, *args): + ps = PolygonSelector(edge_width=3, edge_color="mageneta") + self.current_subplot.add_graphic(ps, center=False) diff --git a/fastplotlib/layouts/_frame/_qtoolbar_template.py b/fastplotlib/layouts/output/_qtoolbar_template.py similarity index 100% rename from fastplotlib/layouts/_frame/_qtoolbar_template.py rename to fastplotlib/layouts/output/_qtoolbar_template.py diff --git a/fastplotlib/layouts/_frame/_toolbar.py b/fastplotlib/layouts/output/_toolbar.py similarity index 82% rename from fastplotlib/layouts/_frame/_toolbar.py rename to fastplotlib/layouts/output/_toolbar.py index 6a0485655..5edd201fa 100644 --- a/fastplotlib/layouts/_frame/_toolbar.py +++ b/fastplotlib/layouts/output/_toolbar.py @@ -2,8 +2,8 @@ class ToolBar: - def __init__(self, plot): - self.plot = plot + def __init__(self, figure): + self.figure = figure def _get_subplot_dropdown_value(self) -> str: raise NotImplemented @@ -11,17 +11,17 @@ def _get_subplot_dropdown_value(self) -> str: @property def current_subplot(self) -> Subplot: """Returns current subplot""" - if hasattr(self.plot, "_subplots"): + if hasattr(self.figure, "_subplots"): # parses dropdown or label value as plot name or position current = self._get_subplot_dropdown_value() if current[0] == "(": # str representation of int tuple to tuple of int current = tuple(int(i) for i in current.strip("()").split(",")) - return self.plot[current] + return self.figure[current] else: - return self.plot[current] + return self.figure[current] else: - return self.plot + return self.figure def panzoom_handler(self, ev): raise NotImplemented diff --git a/fastplotlib/layouts/_frame/_jupyter_output.py b/fastplotlib/layouts/output/jupyter_output.py similarity index 100% rename from fastplotlib/layouts/_frame/_jupyter_output.py rename to fastplotlib/layouts/output/jupyter_output.py diff --git a/fastplotlib/layouts/_frame/_qt_output.py b/fastplotlib/layouts/output/qt_output.py similarity index 95% rename from fastplotlib/layouts/_frame/_qt_output.py rename to fastplotlib/layouts/output/qt_output.py index d7e7f2612..20aaef2d1 100644 --- a/fastplotlib/layouts/_frame/_qt_output.py +++ b/fastplotlib/layouts/output/qt_output.py @@ -37,7 +37,7 @@ def __init__( self.vlayout.addWidget(self.frame.canvas) if make_toolbar: # make toolbar and add to layout - self.toolbar = QToolbar(output_context=self, plot=frame) + self.toolbar = QToolbar(output_context=self, figure=frame) self.vlayout.addWidget(self.toolbar) for w in add_widgets: # add any additional widgets to layout diff --git a/fastplotlib/layouts/_frame/qtoolbar.ui b/fastplotlib/layouts/output/qtoolbar.ui similarity index 100% rename from fastplotlib/layouts/_frame/qtoolbar.ui rename to fastplotlib/layouts/output/qtoolbar.ui diff --git a/fastplotlib/utils/functions.py b/fastplotlib/utils/functions.py index da781b521..561863b0c 100644 --- a/fastplotlib/utils/functions.py +++ b/fastplotlib/utils/functions.py @@ -219,7 +219,7 @@ def make_pygfx_colors(colors, n_colors): return colors_array -def calculate_gridshape(n_subplots: int) -> tuple[int, int]: +def calculate_figure_shape(n_subplots: int) -> tuple[int, int]: """ Returns ``(n_rows, n_cols)`` from given number of subplots ``n_subplots`` """ diff --git a/fastplotlib/widgets/_image_widget_ipywidget_toolbar.py b/fastplotlib/widgets/_image_widget_ipywidget_toolbar.py new file mode 100644 index 000000000..24f7a6279 --- /dev/null +++ b/fastplotlib/widgets/_image_widget_ipywidget_toolbar.py @@ -0,0 +1,135 @@ +from functools import partial + +from ipywidgets import ( + VBox, + Button, + Layout, + IntSlider, + BoundedIntText, + Play, + jslink, + HBox, +) + + +class IpywidgetImageWidgetToolbar(VBox): + def __init__(self, iw): + """ + Basic toolbar for a ImageWidget instance. + + Parameters + ---------- + plot: + """ + self.iw = iw + + self.reset_vminvmax_button = Button( + value=False, + disabled=False, + icon="adjust", + layout=Layout(width="auto"), + tooltip="reset vmin/vmax", + ) + + self.reset_vminvmax_hlut_button = Button( + value=False, + icon="adjust", + description="reset", + layout=Layout(width="auto"), + tooltip="reset vmin/vmax and reset histogram using current frame", + ) + + self.sliders: dict[str, IntSlider] = dict() + + # only for xy data, no time point slider needed + if self.iw.ndim == 2: + widgets = [self.reset_vminvmax_button] + # for txy, tzxy, etc. data + else: + for dim in self.iw.slider_dims: + slider = IntSlider( + min=0, + max=self.iw._dims_max_bounds[dim] - 1, + step=1, + value=0, + description=f"dimension: {dim}", + orientation="horizontal", + ) + + slider.observe( + partial(self.iw._slider_value_changed, dim), names="value" + ) + + self.sliders[dim] = slider + + self.step_size_setter = BoundedIntText( + value=1, + min=1, + max=self.sliders["t"].max, + step=1, + description="Step Size:", + disabled=False, + description_tooltip="set slider step", + layout=Layout(width="150px"), + ) + self.speed_text = BoundedIntText( + value=100, + min=1, + max=1_000, + step=50, + description="Speed", + disabled=False, + description_tooltip="Playback speed, this is NOT framerate.\nArbitrary units between 1 - 1,000", + layout=Layout(width="150px"), + ) + self.play_button = Play( + value=0, + min=self.sliders["t"].min, + max=self.sliders["t"].max, + step=self.sliders["t"].step, + description="play/pause", + disabled=False, + ) + widgets = [ + self.reset_vminvmax_button, + self.reset_vminvmax_hlut_button, + self.play_button, + self.step_size_setter, + self.speed_text, + ] + + self.play_button.interval = 10 + + self.step_size_setter.observe(self._change_stepsize, "value") + self.speed_text.observe(self._change_framerate, "value") + jslink((self.play_button, "value"), (self.sliders["t"], "value")) + jslink((self.play_button, "max"), (self.sliders["t"], "max")) + + self.reset_vminvmax_button.on_click(self._reset_vminvmax) + self.reset_vminvmax_hlut_button.on_click(self._reset_vminvmax_frame) + + self.iw.figure.renderer.add_event_handler(self._set_slider_layout, "resize") + + # the buttons + self.hbox = HBox(widgets) + + super().__init__((self.hbox, *list(self.sliders.values()))) + + def _reset_vminvmax(self, obj): + self.iw.reset_vmin_vmax() + + def _reset_vminvmax_frame(self, obj): + self.iw.reset_vmin_vmax_frame() + + def _change_stepsize(self, obj): + self.sliders["t"].step = self.step_size_setter.value + + def _change_framerate(self, change): + interval = int(1000 / change["new"]) + self.play_button.interval = interval + + def _set_slider_layout(self, *args): + w, h = self.iw.figure.renderer.logical_size + + for k, v in self.sliders.items(): + v.layout = Layout(width=f"{w}px") diff --git a/fastplotlib/widgets/_image_widget_qt_toolbar.py b/fastplotlib/widgets/_image_widget_qt_toolbar.py new file mode 100644 index 000000000..2117f95ab --- /dev/null +++ b/fastplotlib/widgets/_image_widget_qt_toolbar.py @@ -0,0 +1,127 @@ +from functools import partial +from typing import Dict + +from fastplotlib.utils.gui import QtWidgets, QtCore + + +# TODO: There must be a better way to do this +# TODO: Check if an interface exists between ipywidgets and Qt +# TODO: Or we won't need it anyways once we have UI in pygfx +class SliderInterface: + """ + This exists so that ImageWidget has a common interface for Sliders. + + This interface makes a QSlider behave somewhat like a ipywidget IntSlider, enough for ImageWidget to function. + """ + + def __init__(self, qslider): + self.qslider = qslider + + @property + def value(self) -> int: + return self.qslider.value() + + @value.setter + def value(self, value: int): + self.qslider.setValue(value) + + @property + def max(self) -> int: + return self.qslider.maximum() + + @max.setter + def max(self, value: int): + self.qslider.setMaximum(value) + + @property + def min(self): + return self.qslider.minimum() + + @min.setter + def min(self, value: int): + self.qslider.setMinimum(value) + + +class QToolbarImageWidget(QtWidgets.QWidget): + """Toolbar for ImageWidget""" + + def __init__(self, image_widget): + QtWidgets.QWidget.__init__(self) + + # vertical layout + self.vlayout = QtWidgets.QVBoxLayout(self) + + self.image_widget = image_widget + + hlayout_buttons = QtWidgets.QHBoxLayout() + + self.reset_vmin_vmax_button = QtWidgets.QPushButton(self) + self.reset_vmin_vmax_button.setText("auto-contrast") + self.reset_vmin_vmax_button.clicked.connect(self.image_widget.reset_vmin_vmax) + hlayout_buttons.addWidget(self.reset_vmin_vmax_button) + + self.reset_vmin_vmax_hlut_button = QtWidgets.QPushButton(self) + self.reset_vmin_vmax_hlut_button.setText("reset histogram-lut") + self.reset_vmin_vmax_hlut_button.clicked.connect( + self.image_widget.reset_vmin_vmax_frame + ) + hlayout_buttons.addWidget(self.reset_vmin_vmax_hlut_button) + + self.vlayout.addLayout(hlayout_buttons) + + self.sliders: Dict[str, SliderInterface] = dict() + + # has time and/or z-volume + if self.image_widget.ndim > 2: + # create a slider, spinbox and dimension label for each dimension in the ImageWidget + for dim in self.image_widget.slider_dims: + hlayout = ( + QtWidgets.QHBoxLayout() + ) # horizontal stack for label, slider, spinbox + + # max value for current dimension + max_val = self.image_widget._dims_max_bounds[dim] - 1 + + # make slider + slider = QtWidgets.QSlider(self) + slider.setOrientation(QtCore.Qt.Orientation.Horizontal) + slider.setMinimum(0) + slider.setMaximum(max_val) + slider.setValue(0) + slider.setSingleStep(1) + slider.setPageStep(10) + + # make spinbox + spinbox = QtWidgets.QSpinBox(self) + spinbox.setMinimum(0) + spinbox.setMaximum(max_val) + spinbox.setValue(0) + spinbox.setSingleStep(1) + + # link slider and spinbox + slider.valueChanged.connect(spinbox.setValue) + spinbox.valueChanged.connect(slider.setValue) + + # connect slider to change the index within the dimension + slider.valueChanged.connect( + partial(self.image_widget._slider_value_changed, dim) + ) + + # slider dimension label + slider_label = QtWidgets.QLabel(self) + slider_label.setText(dim) + + # add the widgets to the horizontal layout + hlayout.addWidget(slider_label) + hlayout.addWidget(slider) + hlayout.addWidget(spinbox) + + # add horizontal layout to the vertical layout + self.vlayout.addLayout(hlayout) + + # add to sliders dict for easier access to users + self.sliders[dim] = SliderInterface(slider) + + max_height = 35 + (35 * len(self.sliders.keys())) + + self.setMaximumHeight(max_height) diff --git a/fastplotlib/widgets/image.py b/fastplotlib/widgets/image.py index 86671b1fc..2a4dc31b4 100644 --- a/fastplotlib/widgets/image.py +++ b/fastplotlib/widgets/image.py @@ -1,11 +1,11 @@ -from typing import * +from typing import Any, Literal, Callable from warnings import warn import numpy as np -from ..layouts import GridPlot +from ..layouts import Figure from ..graphics import ImageGraphic -from ..utils import calculate_gridshape +from ..utils import calculate_figure_shape from .histogram_lut import HistogramLUT @@ -99,11 +99,11 @@ def __repr__(self): class ImageWidget: @property - def gridplot(self) -> GridPlot: + def figure(self) -> Figure: """ - ``GridPlot`` instance within the `ImageWidget`. + ``Figure`` used by `ImageWidget`. """ - return self._gridplot + return self._figure @property def widget(self): @@ -113,17 +113,17 @@ def widget(self): return self._output @property - def managed_graphics(self) -> List[ImageGraphic]: + def managed_graphics(self) -> list[ImageGraphic]: """List of ``ImageWidget`` managed graphics.""" iw_managed = list() - for subplot in self.gridplot: + for subplot in self.figure: # empty subplots will not have any image widget data if len(subplot.graphics) > 0: iw_managed.append(subplot["image_widget_managed"]) return iw_managed @property - def cmap(self) -> List[str]: + def cmap(self) -> list[str]: cmaps = list() for g in self.managed_graphics: cmaps.append(g.cmap.name) @@ -131,7 +131,7 @@ def cmap(self) -> List[str]: return cmaps @cmap.setter - def cmap(self, names: Union[str, List[str]]): + def cmap(self, names: str | list[str]): if isinstance(names, list): if not all([isinstance(n, str) for n in names]): raise TypeError( @@ -153,7 +153,7 @@ def cmap(self, names: Union[str, List[str]]): g.cmap = names @property - def data(self) -> List[np.ndarray]: + def data(self) -> list[np.ndarray]: """data currently displayed in the widget""" return self._data @@ -163,7 +163,7 @@ def ndim(self) -> int: return self._ndim @property - def n_scrollable_dims(self) -> List[int]: + def n_scrollable_dims(self) -> list[int]: """ list indicating the number of dimenensions that are scrollable for each data array All other dimensions are frame/image data, i.e. [x, y] or [x, y, c] @@ -171,17 +171,17 @@ def n_scrollable_dims(self) -> List[int]: return self._n_scrollable_dims @property - def sliders(self) -> Dict[str, Any]: + def sliders(self) -> dict[str, Any]: """the ipywidget IntSlider or QSlider instances used by the widget for indexing the desired dimensions""" return self._image_widget_toolbar.sliders @property - def slider_dims(self) -> List[str]: + def slider_dims(self) -> list[str]: """the dimensions that the sliders index""" return self._slider_dims @property - def current_index(self) -> Dict[str, int]: + def current_index(self) -> dict[str, int]: """ Get or set the current index @@ -248,7 +248,7 @@ def _get_n_scrollable_dims(self, curr_arr: np.ndarray, rgb: bool) -> list[int]: return n_scrollable_dims @current_index.setter - def current_index(self, index: Dict[str, int]): + def current_index(self, index: dict[str, int]): # ignore if output context has not been created yet if self.widget is None: return @@ -285,15 +285,16 @@ def current_index(self, index: Dict[str, int]): def __init__( self, - data: Union[np.ndarray, List[np.ndarray]], - window_funcs: Union[int, Dict[str, int]] = None, - frame_apply: Union[callable, Dict[int, callable]] = None, - grid_shape: Tuple[int, int] = None, - names: List[str] = None, - grid_plot_kwargs: dict = None, + data: np.ndarray | list[np.ndarray], + window_funcs: dict[str, tuple[Callable, int]] = None, + frame_apply: Callable | dict[int, Callable] = None, + figure_shape: tuple[int, int] = None, + names: list[str] = None, + figure_kwargs: dict = None, histogram_widget: bool = True, rgb: list[bool] = None, - **kwargs, + cmap: str = "plasma", + graphic_kwargs: dict = None, ): """ This widget facilitates high-level navigation through image stacks, which are arrays containing one or more @@ -316,17 +317,17 @@ def __init__( data: Union[np.ndarray, List[np.ndarray] array-like or a list of array-like - window_funcs: Dict[Union[int, str], int] + window_funcs: dict[str, tuple[Callable, int]], i.e. {"t" or "z": (callable, int)} | Apply function(s) with rolling windows along "t" and/or "z" dimensions of the `data` arrays. - | Pass a dict in the form: {dimension: (func, window_size)}, `func` must take a slice of the data array as the - | first argument and must take `axis` as a kwarg. + | Pass a dict in the form: {dimension: (func, window_size)}, `func` must take a slice of the data array as + | the first argument and must take `axis` as a kwarg. | Ex: mean along "t" dimension: {"t": (np.mean, 11)}, if `current_index` of "t" is 50, it will pass frames - | 45 to 55 to `np.mean` with `axis = 0`. - | Ex2: max along z dim: {"z": (np.max, 3)}, passes current, previous and next frame to `np.max` with `axis = 1` + | 45 to 55 to `np.mean` with `axis=0`. + | Ex: max along z dim: {"z": (np.max, 3)}, passes current, previous & next frame to `np.max` with `axis=1` frame_apply: Union[callable, Dict[int, callable]] | Apply function(s) to `data` arrays before to generate final 2D image that is displayed. - | Ex: apply a spatial Gaussian filter + | Ex: apply a spatial gaussian filter | Pass a single function or a dict of functions to apply to each array individually | examples: ``{array_index: to_grayscale}``, ``{0: to_grayscale, 2: threshold_img}`` | "array_index" is the position of the corresponding array in the data list. @@ -334,10 +335,10 @@ def __init__( | this function must be a callable that returns a 2D array | example use case: converting an RGB frame from video to a 2D grayscale frame - grid_shape: Optional[Tuple[int, int]] - manually provide the shape for a gridplot, otherwise a square gridplot is approximated. + figure_shape: Optional[Tuple[int, int]] + manually provide the shape for the Figure, otherwise the number of rows and columns is estimated - grid_plot_kwargs: dict, optional + figure_kwargs: dict, optional passed to `GridPlot` names: Optional[str] @@ -350,8 +351,8 @@ def __init__( Includes a True or False for each ``array`` in the ImageWidget, indicating whether images are displayed as grayscale or RGB(A). - kwargs: Any - passed to fastplotlib.graphics.Image + graphic_kwargs: Any + passed to each ImageGraphic in the ImageWidget figure subplots """ self._names = None @@ -367,17 +368,17 @@ def __init__( if all([_is_arraylike(d) for d in data]): # Grid computations - if grid_shape is None: - grid_shape = calculate_gridshape(len(data)) + if figure_shape is None: + figure_shape = calculate_figure_shape(len(data)) - # verify that user-specified grid shape is large enough for the number of image arrays passed - elif grid_shape[0] * grid_shape[1] < len(data): - grid_shape = calculate_gridshape(len(data)) + # verify that user-specified figure shape is large enough for the number of image arrays passed + elif figure_shape[0] * figure_shape[1] < len(data): + figure_shape = calculate_figure_shape(len(data)) warn( - f"Invalid `grid_shape` passed, setting grid shape to: {grid_shape}" + f"Invalid `figure_shape` passed, setting figure shape to: {figure_shape}" ) - self._data: List[np.ndarray] = data + self._data: list[np.ndarray] = data # Establish number of image dimensions and number of scrollable dimensions for each array if rgb is None: @@ -437,8 +438,7 @@ def __init__( ) else: raise TypeError( - f"`data` must be an array-like type representing an n-dimensional image " - f"or a list of array-like representing a grid of n-dimensional images. " + f"`data` must be an array-like type or a list of array-like." f"You have passed the following type {type(data)}" ) @@ -451,14 +451,14 @@ def __init__( if dim in ALLOWED_SLIDER_DIMS.keys(): self.slider_dims.append(ALLOWED_SLIDER_DIMS[dim]) - self._frame_apply: Dict[int, callable] = dict() + self._frame_apply: dict[int, callable] = dict() if frame_apply is not None: if callable(frame_apply): self._frame_apply = frame_apply elif isinstance(frame_apply, dict): - self._frame_apply: Dict[int, callable] = dict.fromkeys( + self._frame_apply: dict[int, callable] = dict.fromkeys( list(range(len(self.data))) ) @@ -479,15 +479,15 @@ def __init__( ) # current_index stores {dimension_index: slice_index} for every dimension - self._current_index: Dict[str, int] = {sax: 0 for sax in self.slider_dims} + self._current_index: dict[str, int] = {sax: 0 for sax in self.slider_dims} self._window_funcs = None self.window_funcs = window_funcs - self._sliders: Dict[str, Any] = dict() + self._sliders: dict[str, Any] = dict() # get max bound for all data arrays for all slider dimensions and ensure compatibility across slider dims - self._dims_max_bounds: Dict[str, int] = {k: 0 for k in self.slider_dims} + self._dims_max_bounds: dict[str, int] = {k: 0 for k in self.slider_dims} for i, _dim in enumerate(list(self._dims_max_bounds.keys())): for array, partition in zip(self.data, self.n_scrollable_dims): if partition <= i: @@ -500,20 +500,23 @@ def __init__( self._dims_max_bounds[_dim], array.shape[i] ) - grid_plot_kwargs_default = {"controller_ids": "sync"} - if grid_plot_kwargs is None: - grid_plot_kwargs = dict() + figure_kwargs_default = {"controller_ids": "sync"} + if figure_kwargs is None: + figure_kwargs = dict() # update the default kwargs with any user-specified kwargs # user specified kwargs will overwrite the defaults - grid_plot_kwargs_default.update(grid_plot_kwargs) + figure_kwargs_default.update(figure_kwargs) - self._gridplot: GridPlot = GridPlot( - shape=grid_shape, **grid_plot_kwargs_default - ) + if graphic_kwargs is None: + graphic_kwargs = dict() + + graphic_kwargs.update({"cmap": cmap}) + + self._figure: Figure = Figure(shape=figure_shape, **figure_kwargs_default) self._histogram_widget = histogram_widget - for data_ix, (d, subplot) in enumerate(zip(self.data, self.gridplot)): + for data_ix, (d, subplot) in enumerate(zip(self.data, self.figure)): if self._names is not None: name = self._names[data_ix] else: @@ -521,7 +524,7 @@ def __init__( frame = self._process_indices(d, slice_indices=self._current_index) frame = self._process_frame_apply(frame, data_ix) - ig = ImageGraphic(frame, name="image_widget_managed", **kwargs) + ig = ImageGraphic(frame, name="image_widget_managed", **graphic_kwargs) subplot.add_graphic(ig) subplot.name = name subplot.set_title(name) @@ -538,11 +541,11 @@ def __init__( self._image_widget_toolbar = None @property - def frame_apply(self) -> Union[dict, None]: + def frame_apply(self) -> dict | None: return self._frame_apply @frame_apply.setter - def frame_apply(self, frame_apply: Dict[int, callable]): + def frame_apply(self, frame_apply: dict[int, callable]): if frame_apply is None: frame_apply = dict() @@ -551,7 +554,7 @@ def frame_apply(self, frame_apply: Dict[int, callable]): self.current_index = self.current_index @property - def window_funcs(self) -> Dict[str, _WindowFunctions]: + def window_funcs(self) -> dict[str, _WindowFunctions]: """ Get or set the window functions @@ -563,7 +566,7 @@ def window_funcs(self) -> Dict[str, _WindowFunctions]: return self._window_funcs @window_funcs.setter - def window_funcs(self, callable_dict: Dict[str, int]): + def window_funcs(self, callable_dict: dict[str, int]): if callable_dict is None: self._window_funcs = None # force frame to update @@ -617,7 +620,7 @@ def window_funcs(self, callable_dict: Dict[str, int]): self.current_index = self.current_index def _process_indices( - self, array: np.ndarray, slice_indices: Dict[Union[int, str], int] + self, array: np.ndarray, slice_indices: dict[str, int] ) -> np.ndarray: """ Get the 2D array from the given slice indices. If not returning a 2D slice (such as due to window_funcs) @@ -628,8 +631,8 @@ def _process_indices( array: np.ndarray array-like to get a 2D slice from - slice_indices: Dict[int, int] - dict in form of {dimension_index: slice_index} + slice_indices: Dict[str, int] + dict in form of {dimension_index: current_index} For example if an array has shape [1000, 30, 512, 512] corresponding to [t, z, x, y]: To get the 100th timepoint and 3rd z-plane pass: {"t": 100, "z": 3} @@ -738,7 +741,7 @@ def _process_frame_apply(self, array, data_ix) -> np.ndarray: return array - def _slider_value_changed(self, dimension: str, change: Union[dict, int]): + def _slider_value_changed(self, dimension: str, change: dict | int): if self.block_sliders: return if isinstance(change, dict): @@ -764,7 +767,7 @@ def reset_vmin_vmax_frame(self): TODO: We could think of applying the frame_apply funcs to a subsample of the entire array to get a better estimate of vmin vmax? """ - for subplot in self.gridplot: + for subplot in self.figure: if "histogram_lut" not in subplot.docks["right"]: continue @@ -774,7 +777,7 @@ def reset_vmin_vmax_frame(self): def set_data( self, - new_data: Union[np.ndarray, List[np.ndarray]], + new_data: np.ndarray | list[np.ndarray], reset_vmin_vmax: bool = True, reset_indices: bool = True, ): @@ -832,7 +835,7 @@ 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) + zip(new_data, self._data, self.figure) ): # check last two dims (x and y) to see if data shape is changing old_data_shape = self._data[i].shape[-self.n_img_dims[i] :] @@ -895,21 +898,17 @@ def show( OutputContext ImageWidget just uses the Gridplot output context """ - if self.gridplot.canvas.__class__.__name__ == "JupyterWgpuCanvas": - from ..layouts._frame._ipywidget_toolbar import ( - IpywidgetImageWidgetToolbar, - ) # noqa - inline import + if self.figure.canvas.__class__.__name__ == "JupyterWgpuCanvas": + from ._image_widget_ipywidget_toolbar import IpywidgetImageWidgetToolbar self._image_widget_toolbar = IpywidgetImageWidgetToolbar(self) - elif self.gridplot.canvas.__class__.__name__ == "QWgpuCanvas": - from ..layouts._frame._qt_toolbar import ( - QToolbarImageWidget, - ) # noqa - inline import + elif self.figure.canvas.__class__.__name__ == "QWgpuCanvas": + from ._image_widget_qt_toolbar import QToolbarImageWidget self._image_widget_toolbar = QToolbarImageWidget(self) - self._output = self.gridplot.show( + self._output = self.figure.show( toolbar=toolbar, sidecar=sidecar, sidecar_kwargs=sidecar_kwargs, @@ -920,4 +919,4 @@ def show( def close(self): """Close Widget""" - self.gridplot.close() + self.figure.close() diff --git a/tests/test_figure.py b/tests/test_figure.py new file mode 100644 index 000000000..27b74c0b6 --- /dev/null +++ b/tests/test_figure.py @@ -0,0 +1,164 @@ +import numpy as np +import pytest + +import fastplotlib as fpl +import pygfx + + +def test_cameras_controller_properties(): + cameras = [ + ["2d", "3d", "3d"], + ["3d", "3d", "3d"] + ] + + controller_types = [ + ["panzoom", "panzoom", "fly"], + ["orbit", "trackball", "panzoom"] + ] + + fig = fpl.Figure( + shape=(2, 3), + cameras=cameras, + controller_types=controller_types, + canvas="offscreen" + ) + + print(fig.canvas) + + subplot_cameras = [subplot.camera for subplot in fig] + subplot_controllers = [subplot.controller for subplot in fig] + + for c1, c2 in zip(subplot_cameras, fig.cameras.ravel()): + assert c1 is c2 + + for c1, c2 in zip(subplot_controllers, fig.controllers.ravel()): + assert c1 is c2 + + for camera_type, subplot_camera in zip(np.asarray(cameras).ravel(), fig.cameras.ravel()): + if camera_type == "2d": + assert subplot_camera.fov == 0 + else: + assert subplot_camera.fov == 50 + + for controller_type, subplot_controller in zip(np.asarray(controller_types).ravel(), fig.controllers.ravel()): + match controller_type: + case "panzoom": + assert isinstance(subplot_controller, pygfx.PanZoomController) + case "fly": + assert isinstance(subplot_controller, pygfx.FlyController) + case "orbit": + assert isinstance(subplot_controller, pygfx.OrbitController) + case "trackball": + assert isinstance(subplot_controller, pygfx.TrackballController) + + # check changing cameras + fig[0, 0].camera = "3d" + assert fig[0, 0].camera.fov == 50 + fig[1, 0].camera = "2d" + assert fig[1, 0].camera.fov == 0 + + # test changing controller + fig[1, 1].controller = "fly" + assert isinstance(fig[1, 1].controller, pygfx.FlyController) + assert fig[1, 1].controller is fig.controllers[1, 1] + fig[0, 2].controller = "panzoom" + assert isinstance(fig[0, 2].controller, pygfx.PanZoomController) + assert fig[0, 2].controller is fig.controllers[0, 2] + + +def test_controller_ids_int(): + ids = [ + [0, 1, 1], + [0, 2, 3], + [4, 1, 2] + ] + + fig = fpl.Figure(shape=(3, 3), controller_ids=ids, canvas="offscreen") + + assert fig[0, 0].controller is fig[1, 0].controller + assert fig[0, 1].controller is fig[0, 2].controller is fig[2, 1].controller + assert fig[1, 1].controller is fig[2, 2].controller + + +def test_controller_ids_int_change_controllers(): + ids = [ + [0, 1, 1], + [0, 2, 3], + [4, 1, 2] + ] + + cameras = [ + ["2d", "3d", "3d"], + ["2d", "3d", "2d"], + ["3d", "3d", "3d"] + ] + + fig = fpl.Figure(shape=(3, 3), cameras=cameras, controller_ids=ids, canvas="offscreen") + + assert isinstance(fig[0, 1].controller, pygfx.FlyController) + + # changing controller when id matches should change the others too + fig[0, 1].controller = "panzoom" + assert isinstance(fig[0, 1].controller, pygfx.PanZoomController) + assert fig[0, 1].controller is fig[0, 2].controller is fig[2, 1].controller + assert set(fig[0, 1].controller.cameras) == {fig[0, 1].camera, fig[0, 2].camera, fig[2, 1].camera} + + # change to orbit + fig[0, 1].controller = "orbit" + assert isinstance(fig[0, 1].controller, pygfx.OrbitController) + assert fig[0, 1].controller is fig[0, 2].controller is fig[2, 1].controller + assert set(fig[0, 1].controller.cameras) == {fig[0, 1].camera, fig[0, 2].camera, fig[2, 1].camera} + + +def test_controller_ids_str(): + names = [ + ["a", "b", "c"], + ["d", "e", "f"] + ] + + controller_ids = [ + ["a", "f"], + ["b", "d", "e"] + ] + + fig = fpl.Figure(shape=(2, 3), controller_ids=controller_ids, names=names, canvas="offscreen") + + assert fig[0, 0].controller is fig[1, 2].controller is fig["a"].controller is fig["f"].controller + assert fig[0, 1].controller is fig[1, 0].controller is fig[1, 1].controller is fig["b"].controller is fig["d"].controller is fig["e"].controller + + # make sure subplot c is unique + exclude_c = [fig[n].controller for n in ["a", "b", "d", "e", "f"]] + assert fig["c"] not in exclude_c + + +def test_set_controllers_from_existing_controllers(): + fig = fpl.Figure(shape=(3, 3), canvas="offscreen") + fig2 = fpl.Figure(shape=fig.shape, controllers=fig.controllers, canvas="offscreen") + + assert fig.controllers[:-1].size == 6 + with pytest.raises(ValueError): + fig3 = fpl.Figure(shape=fig.shape, controllers=fig.controllers[:-1], canvas="offscreen") + + for fig1_subplot, fig2_subplot in zip(fig, fig2): + assert fig1_subplot.controller is fig2_subplot.controller + + cameras = [ + [pygfx.PerspectiveCamera(), "3d"], + ["3d", "2d"] + ] + + controllers = [ + [pygfx.FlyController(cameras[0][0]), pygfx.TrackballController()], + [pygfx.OrbitController(), pygfx.PanZoomController()] + ] + + fig = fpl.Figure(shape=(2, 2), cameras=cameras, controllers=controllers, canvas="offscreen") + + assert fig[0, 0].controller is controllers[0][0] + assert fig[0, 1].controller is controllers[0][1] + assert fig[1, 0].controller is controllers[1][0] + assert fig[1, 1].controller is controllers[1][1] + + assert fig[0, 0].camera is cameras[0][0] + + assert fig[0, 1].camera.fov == 50 diff --git a/tests/test_gridplot.py b/tests/test_gridplot.py deleted file mode 100644 index 3814664d7..000000000 --- a/tests/test_gridplot.py +++ /dev/null @@ -1,164 +0,0 @@ -import numpy as np -import pytest - -import fastplotlib as fpl -import pygfx - - -def test_cameras_controller_properties(): - cameras = [ - ["2d", "3d", "3d"], - ["3d", "3d", "3d"] - ] - - controller_types = [ - ["panzoom", "panzoom", "fly"], - ["orbit", "trackball", "panzoom"] - ] - - gp = fpl.GridPlot( - shape=(2, 3), - cameras=cameras, - controller_types=controller_types, - canvas="offscreen" - ) - - print(gp.canvas) - - subplot_cameras = [subplot.camera for subplot in gp] - subplot_controllers = [subplot.controller for subplot in gp] - - for c1, c2 in zip(subplot_cameras, gp.cameras.ravel()): - assert c1 is c2 - - for c1, c2 in zip(subplot_controllers, gp.controllers.ravel()): - assert c1 is c2 - - for camera_type, subplot_camera in zip(np.asarray(cameras).ravel(), gp.cameras.ravel()): - if camera_type == "2d": - assert subplot_camera.fov == 0 - else: - assert subplot_camera.fov == 50 - - for controller_type, subplot_controller in zip(np.asarray(controller_types).ravel(), gp.controllers.ravel()): - match controller_type: - case "panzoom": - assert isinstance(subplot_controller, pygfx.PanZoomController) - case "fly": - assert isinstance(subplot_controller, pygfx.FlyController) - case "orbit": - assert isinstance(subplot_controller, pygfx.OrbitController) - case "trackball": - assert isinstance(subplot_controller, pygfx.TrackballController) - - # check changing cameras - gp[0, 0].camera = "3d" - assert gp[0, 0].camera.fov == 50 - gp[1, 0].camera = "2d" - assert gp[1, 0].camera.fov == 0 - - # test changing controller - gp[1, 1].controller = "fly" - assert isinstance(gp[1, 1].controller, pygfx.FlyController) - assert gp[1, 1].controller is gp.controllers[1, 1] - gp[0, 2].controller = "panzoom" - assert isinstance(gp[0, 2].controller, pygfx.PanZoomController) - assert gp[0, 2].controller is gp.controllers[0, 2] - - -def test_gridplot_controller_ids_int(): - ids = [ - [0, 1, 1], - [0, 2, 3], - [4, 1, 2] - ] - - gp = fpl.GridPlot(shape=(3, 3), controller_ids=ids, canvas="offscreen") - - assert gp[0, 0].controller is gp[1, 0].controller - assert gp[0, 1].controller is gp[0, 2].controller is gp[2, 1].controller - assert gp[1, 1].controller is gp[2, 2].controller - - -def test_gridplot_controller_ids_int_change_controllers(): - ids = [ - [0, 1, 1], - [0, 2, 3], - [4, 1, 2] - ] - - cameras = [ - ["2d", "3d", "3d"], - ["2d", "3d", "2d"], - ["3d", "3d", "3d"] - ] - - gp = fpl.GridPlot(shape=(3, 3), cameras=cameras, controller_ids=ids, canvas="offscreen") - - assert isinstance(gp[0, 1].controller, pygfx.FlyController) - - # changing controller when id matches should change the others too - gp[0, 1].controller = "panzoom" - assert isinstance(gp[0, 1].controller, pygfx.PanZoomController) - assert gp[0, 1].controller is gp[0, 2].controller is gp[2, 1].controller - assert set(gp[0, 1].controller.cameras) == {gp[0, 1].camera, gp[0, 2].camera, gp[2, 1].camera} - - # change to orbit - gp[0, 1].controller = "orbit" - assert isinstance(gp[0, 1].controller, pygfx.OrbitController) - assert gp[0, 1].controller is gp[0, 2].controller is gp[2, 1].controller - assert set(gp[0, 1].controller.cameras) == {gp[0, 1].camera, gp[0, 2].camera, gp[2, 1].camera} - - -def test_gridplot_controller_ids_str(): - names = [ - ["a", "b", "c"], - ["d", "e", "f"] - ] - - controller_ids = [ - ["a", "f"], - ["b", "d", "e"] - ] - - gp = fpl.GridPlot(shape=(2, 3), controller_ids=controller_ids, names=names, canvas="offscreen") - - assert gp[0, 0].controller is gp[1, 2].controller is gp["a"].controller is gp["f"].controller - assert gp[0, 1].controller is gp[1, 0].controller is gp[1, 1].controller is gp["b"].controller is gp["d"].controller is gp["e"].controller - - # make sure subplot c is unique - exclude_c = [gp[n].controller for n in ["a", "b", "d", "e", "f"]] - assert gp["c"] not in exclude_c - - -def test_set_gridplot_controllers_from_existing_controllers(): - gp = fpl.GridPlot(shape=(3, 3), canvas="offscreen") - gp2 = fpl.GridPlot(shape=gp.shape, controllers=gp.controllers, canvas="offscreen") - - assert gp.controllers[:-1].size == 6 - with pytest.raises(ValueError): - gp3 = fpl.GridPlot(shape=gp.shape, controllers=gp.controllers[:-1], canvas="offscreen") - - for sp_gp, sp_gp2 in zip(gp, gp2): - assert sp_gp.controller is sp_gp2.controller - - cameras = [ - [pygfx.PerspectiveCamera(), "3d"], - ["3d", "2d"] - ] - - controllers = [ - [pygfx.FlyController(cameras[0][0]), pygfx.TrackballController()], - [pygfx.OrbitController(), pygfx.PanZoomController()] - ] - - gp = fpl.GridPlot(shape=(2, 2), cameras=cameras, controllers=controllers, canvas="offscreen") - - assert gp[0, 0].controller is controllers[0][0] - assert gp[0, 1].controller is controllers[0][1] - assert gp[1, 0].controller is controllers[1][0] - assert gp[1, 1].controller is controllers[1][1] - - assert gp[0, 0].camera is cameras[0][0] - - assert gp[0, 1].camera.fov == 50 From cc6decebe78fb12bddbfbdd25e1878c444c686e8 Mon Sep 17 00:00:00 2001 From: Kushal Kolar Date: Fri, 12 Apr 2024 14:57:04 -0400 Subject: [PATCH 46/66] making picking default for all graphics (#484) --- fastplotlib/graphics/image.py | 12 ++++++++--- fastplotlib/graphics/line.py | 4 +++- fastplotlib/graphics/scatter.py | 4 +++- fastplotlib/graphics/selectors/_linear.py | 6 ++++-- .../graphics/selectors/_linear_region.py | 20 +++++++++++++------ fastplotlib/graphics/selectors/_polygon.py | 8 ++++++-- .../graphics/selectors/_rectangle_region.py | 2 +- fastplotlib/graphics/text.py | 1 + fastplotlib/widgets/histogram_lut.py | 4 ++++ 9 files changed, 45 insertions(+), 16 deletions(-) diff --git a/fastplotlib/graphics/image.py b/fastplotlib/graphics/image.py index 1cad33f22..ce736dab2 100644 --- a/fastplotlib/graphics/image.py +++ b/fastplotlib/graphics/image.py @@ -282,12 +282,15 @@ def __init__( # if data is RGB or RGBA if data.ndim > 2: material = pygfx.ImageBasicMaterial( - clim=(vmin, vmax), map_interpolation=filter + clim=(vmin, vmax), map_interpolation=filter, pick_write=True ) # if data is just 2D without color information, use colormap LUT else: material = pygfx.ImageBasicMaterial( - clim=(vmin, vmax), map=self.cmap(), map_interpolation=filter + clim=(vmin, vmax), + map=self.cmap(), + map_interpolation=filter, + pick_write=True, ) world_object = pygfx.Image(geometry, material) @@ -443,7 +446,10 @@ def __init__( self.cmap = HeatmapCmapFeature(self, cmap) self._material = pygfx.ImageBasicMaterial( - clim=(vmin, vmax), map=self.cmap(), map_interpolation=filter + clim=(vmin, vmax), + map=self.cmap(), + map_interpolation=filter, + pick_write=True, ) for start, stop, chunk in zip(start_ixs, stop_ixs, chunks): diff --git a/fastplotlib/graphics/line.py b/fastplotlib/graphics/line.py index cfb697dff..0371fe59b 100644 --- a/fastplotlib/graphics/line.py +++ b/fastplotlib/graphics/line.py @@ -114,7 +114,9 @@ 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(), color_mode="vertex"), + material=material( + thickness=self.thickness(), color_mode="vertex", pick_write=True + ), ) self._set_world_object(world_object) diff --git a/fastplotlib/graphics/scatter.py b/fastplotlib/graphics/scatter.py index 2557cd637..8682df3d5 100644 --- a/fastplotlib/graphics/scatter.py +++ b/fastplotlib/graphics/scatter.py @@ -93,7 +93,9 @@ def __init__( pygfx.Geometry( positions=self.data(), sizes=self.sizes(), colors=self.colors() ), - material=pygfx.PointsMaterial(color_mode="vertex", size_mode="vertex"), + material=pygfx.PointsMaterial( + color_mode="vertex", size_mode="vertex", pick_write=True + ), ) self._set_world_object(world_object) diff --git a/fastplotlib/graphics/selectors/_linear.py b/fastplotlib/graphics/selectors/_linear.py index 4b77a6cd9..82e553f0a 100644 --- a/fastplotlib/graphics/selectors/_linear.py +++ b/fastplotlib/graphics/selectors/_linear.py @@ -125,12 +125,14 @@ 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, pick_write=True), ) 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, pick_write=True + ), ) line_inner.world.z = self.line_outer.world.z + 1 diff --git a/fastplotlib/graphics/selectors/_linear_region.py b/fastplotlib/graphics/selectors/_linear_region.py index 47191bfb1..09c134800 100644 --- a/fastplotlib/graphics/selectors/_linear_region.py +++ b/fastplotlib/graphics/selectors/_linear_region.py @@ -139,13 +139,13 @@ def __init__( if axis == "x": mesh = pygfx.Mesh( pygfx.box_geometry(1, size, 1), - pygfx.MeshBasicMaterial(color=pygfx.Color(fill_color)), + pygfx.MeshBasicMaterial(color=pygfx.Color(fill_color), pick_write=True), ) 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), pick_write=True), ) else: raise ValueError("`axis` must be one of 'x' or 'y'") @@ -169,7 +169,9 @@ def __init__( left_line = pygfx.Line( pygfx.Geometry(positions=left_line_data), - pygfx.LineMaterial(thickness=edge_thickness, color=edge_color), + pygfx.LineMaterial( + thickness=edge_thickness, color=edge_color, pick_write=True + ), ) # position data for the right edge line @@ -182,7 +184,9 @@ def __init__( right_line = pygfx.Line( pygfx.Geometry(positions=right_line_data), - pygfx.LineMaterial(thickness=edge_thickness, color=edge_color), + pygfx.LineMaterial( + thickness=edge_thickness, color=edge_color, pick_write=True + ), ) self.edges: Tuple[pygfx.Line, pygfx.Line] = (left_line, right_line) @@ -198,7 +202,9 @@ def __init__( bottom_line = pygfx.Line( pygfx.Geometry(positions=bottom_line_data), - pygfx.LineMaterial(thickness=edge_thickness, color=edge_color), + pygfx.LineMaterial( + thickness=edge_thickness, color=edge_color, pick_write=True + ), ) # position data for the right edge line @@ -211,7 +217,9 @@ def __init__( top_line = pygfx.Line( pygfx.Geometry(positions=top_line_data), - pygfx.LineMaterial(thickness=edge_thickness, color=edge_color), + pygfx.LineMaterial( + thickness=edge_thickness, color=edge_color, pick_write=True + ), ) self.edges: Tuple[pygfx.Line, pygfx.Line] = (bottom_line, top_line) diff --git a/fastplotlib/graphics/selectors/_polygon.py b/fastplotlib/graphics/selectors/_polygon.py index 3d2ee98fd..a4ecd440c 100644 --- a/fastplotlib/graphics/selectors/_polygon.py +++ b/fastplotlib/graphics/selectors/_polygon.py @@ -71,7 +71,9 @@ def _add_segment(self, ev): 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) + thickness=self.edge_width, + color=pygfx.Color(self.edge_color), + pick_write=True, ), ) @@ -126,7 +128,9 @@ def _finish_polygon(self, ev): 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) + thickness=self.edge_width, + color=pygfx.Color(self.edge_color), + pick_write=True, ), ) diff --git a/fastplotlib/graphics/selectors/_rectangle_region.py b/fastplotlib/graphics/selectors/_rectangle_region.py index 1081a49a9..bc2cad5b1 100644 --- a/fastplotlib/graphics/selectors/_rectangle_region.py +++ b/fastplotlib/graphics/selectors/_rectangle_region.py @@ -214,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), pick_write=True), ) self.fill.position.set(*origin, -2) diff --git a/fastplotlib/graphics/text.py b/fastplotlib/graphics/text.py index a486b1bd2..49b4ac4be 100644 --- a/fastplotlib/graphics/text.py +++ b/fastplotlib/graphics/text.py @@ -70,6 +70,7 @@ def __init__( color=face_color, outline_color=outline_color, outline_thickness=outline_thickness, + pick_write=True, ), ) diff --git a/fastplotlib/widgets/histogram_lut.py b/fastplotlib/widgets/histogram_lut.py index 67af972b8..971bc1a28 100644 --- a/fastplotlib/widgets/histogram_lut.py +++ b/fastplotlib/widgets/histogram_lut.py @@ -80,6 +80,8 @@ def __init__( outline_thickness=1, ) + self._text_vmin.world_object.material.pick_write = False + self._text_vmax = TextGraphic( text=vmax_str, size=16, @@ -89,6 +91,8 @@ def __init__( outline_thickness=1, ) + self._text_vmax.world_object.material.pick_write = False + widget_wo = Group() widget_wo.add( self._histogram_line.world_object, From 504ddcf925bd8d43b8354444c921b014b7399aef Mon Sep 17 00:00:00 2001 From: Kushal Kolar Date: Fri, 12 Apr 2024 21:50:11 -0400 Subject: [PATCH 47/66] update qt examples (#485) --- examples/qt/{video.py => embed.py} | 27 ++++++++++++--------------- examples/qt/imagewidget.py | 15 +++++++++------ examples/qt/minimal.py | 29 +++++++++-------------------- 3 files changed, 30 insertions(+), 41 deletions(-) rename examples/qt/{video.py => embed.py} (54%) diff --git a/examples/qt/video.py b/examples/qt/embed.py similarity index 54% rename from examples/qt/video.py rename to examples/qt/embed.py index 9fd77a999..a3b156021 100644 --- a/examples/qt/video.py +++ b/examples/qt/embed.py @@ -5,28 +5,22 @@ import fastplotlib as fpl import imageio.v3 as iio -# Qt app MUST be instantiated before creating any fpl objects, or any other Qt objects -app = QtWidgets.QApplication([]) video = iio.imread("imageio:cockatoo.mp4") -# force qt canvas, wgpu will sometimes pick glfw by default even if Qt is present -plot = fpl.Plot(canvas="qt") +# fastplotlib and wgpu will auto-detect if Qt is imported and then use the Qt canvas and output context +fig = fpl.Figure() -plot.add_image(video[0], name="video") -plot.camera.local.scale *= -1 +fig[0, 0].add_image(video[0], name="video") def update_frame(ix): - plot["video"].data = video[ix] - # you can also do plot.graphics[0].data = video[ix] + fig[0, 0]["video"].data = video[ix] + # you can also do fig[0, 0].graphics[0].data = video[ix] -# create a QMainWindow, set the plot canvas as the main widget -# The canvas does not have to be in a QMainWindow and it does -# not have to be the central widget, it will work like any QWidget +# create a QMainWindow main_window = QtWidgets.QMainWindow() -main_window.setCentralWidget(plot.canvas) # Create a QSlider for updating frames slider = QtWidgets.QSlider(QtCore.Qt.Orientation.Horizontal) @@ -44,8 +38,11 @@ def update_frame(ix): dock ) -# calling plot.show() is required to start the rendering loop -plot.show() +# calling fig.show() is required to start the rendering loop +qwidget = fig.show() + +# set the qwidget as the central widget +main_window.setCentralWidget(qwidget) # set window size from width and height of video main_window.resize(video.shape[2], video.shape[1]) @@ -54,4 +51,4 @@ def update_frame(ix): main_window.show() # execute Qt app -app.exec() +fpl.run() diff --git a/examples/qt/imagewidget.py b/examples/qt/imagewidget.py index ab1a055f1..f82d082a0 100644 --- a/examples/qt/imagewidget.py +++ b/examples/qt/imagewidget.py @@ -4,14 +4,13 @@ import numpy as np from PyQt6 import QtWidgets import fastplotlib as fpl +import imageio.v3 as iio -# Qt app MUST be instantiated before creating any fpl objects, or any other Qt objects -app = QtWidgets.QApplication([]) images = np.random.rand(100, 512, 512) -# create image widget, force Qt canvas so it doesn't pick glfw -iw = fpl.ImageWidget(images, grid_plot_kwargs={"canvas": "qt"}) +# fastplotlib and wgpu will auto-detect if Qt is imported and then use the Qt canvas +iw = fpl.ImageWidget(images) iw.show() iw.widget.resize(800, 800) @@ -20,10 +19,14 @@ iw_mult = fpl.ImageWidget( images_list, - grid_plot_kwargs={"canvas": "qt"}, cmap="viridis" ) iw_mult.show() iw_mult.widget.resize(800, 800) -app.exec() +# image widget with rgb data +rgb_video = iio.imread("imageio:cockatoo.mp4") +iw_rgb = fpl.ImageWidget(rgb_video, rgb=[True]) +iw_rgb.show() + +fpl.run() diff --git a/examples/qt/minimal.py b/examples/qt/minimal.py index e4e5f6c2f..0d9009ba7 100644 --- a/examples/qt/minimal.py +++ b/examples/qt/minimal.py @@ -1,35 +1,24 @@ """ Minimal PyQt example that displays an image. Press "r" key to autoscale """ +# import Qt or PySide from PyQt6 import QtWidgets import fastplotlib as fpl import imageio.v3 as iio -# Qt app MUST be instantiated before creating any fpl objects, or any other Qt objects -app = QtWidgets.QApplication([]) - img = iio.imread("imageio:astronaut.png") -# force qt canvas, wgpu will sometimes pick glfw by default even if Qt is present -plot = fpl.Plot(canvas="qt") +# fastplotlib and wgpu will auto-detect if Qt is imported and then use the Qt canvas and Qt output context +fig = fpl.Figure() -plot.add_image(img) -plot.camera.local.scale *= -1 +fig[0, 0].add_image(img) -# must call plot.show() to start rendering loop -plot.show() +# must call fig.show() to start rendering loop and show the QWidget containing the fastplotlib figure +qwidget = fig.show() # set QWidget initial size from image width and height -plot.canvas.resize(*img.shape[:2]) - - -def autoscale(ev): - if ev.key == "r": - plot.auto_scale() - - -# useful if you pan/zoom away from the image -plot.renderer.add_event_handler(autoscale, "key_down") +qwidget.resize(*img.shape[:2]) # execute Qt app -app.exec() +# if this is part of a larger Qt QApplication, you can also call app.exec() where app is the QApplication instance +fpl.run() From 9d22d624f51231c76c5b5875e62e6b53d6ba4e63 Mon Sep 17 00:00:00 2001 From: Kushal Kolar Date: Sat, 13 Apr 2024 02:15:39 -0400 Subject: [PATCH 48/66] Update GOVERNANCE.md --- GOVERNANCE.md | 1 - 1 file changed, 1 deletion(-) diff --git a/GOVERNANCE.md b/GOVERNANCE.md index 3ad450dcb..f57479816 100644 --- a/GOVERNANCE.md +++ b/GOVERNANCE.md @@ -34,7 +34,6 @@ The Advisory Committee holds a significant interest in fastplotlib as determined 1. Amol Pasarkar 1. Eric Thomson -1. Guillaume Viejo 1. Andrea Giovannucci 1. John Pearson From bac18773384e54459cd1f3260eefb6bff33dda22 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Mon, 15 Apr 2024 18:37:03 -0400 Subject: [PATCH 49/66] update gov --- GOVERNANCE.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/GOVERNANCE.md b/GOVERNANCE.md index f57479816..6c538079e 100644 --- a/GOVERNANCE.md +++ b/GOVERNANCE.md @@ -30,7 +30,7 @@ Responsibilities: ### Advisory Committee -The Advisory Committee holds a significant interest in fastplotlib as determined solely by the **Maintainers**. The responsibilities of the **Advisory Committee** are to 1) attend a yearly Roadmap meeting, 2) be available for conflict resolution. +The Advisory Committee holds a significant interest in fastplotlib as determined solely by the **Maintainers**. 1. Amol Pasarkar 1. Eric Thomson @@ -108,4 +108,4 @@ Governance decisions, meeting minutes, and voting outcomes are publicly document ### Until February 28, 2025 -During early stages of fastplotlib development, changes to the governance document may be made directly through unanimous approval by the original maintainers, Kushal Kolar & Caitlin Lewis, without contacting the neutral moderator or consulting with the advisory committee. They (Kushal & Caitlin) may also add new members to the advisory committee through unanimous approval. +During early stages of fastplotlib development, changes to the governance document may be made directly through unanimous approval by the original maintainers, Kushal Kolar & Caitlin Lewis. They (Kushal & Caitlin) may also add new members to the advisory committee through unanimous approval. From 41a694c632243eebdb204f4665bef49368c02781 Mon Sep 17 00:00:00 2001 From: Kushal Kolar Date: Mon, 15 Apr 2024 23:14:40 -0400 Subject: [PATCH 50/66] remove __all__ from top level (#490) --- fastplotlib/__init__.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/fastplotlib/__init__.py b/fastplotlib/__init__.py index 8e6341156..7a6290d95 100644 --- a/fastplotlib/__init__.py +++ b/fastplotlib/__init__.py @@ -19,11 +19,3 @@ if len(adapters) < 1: raise IndexError("No WGPU adapters found, fastplotlib will not work.") - - -__all__ = [ - "Figure", - "run", - # "ImageWidget", - "Legend", -] From 1abe5ed27249059976fcef91c623e9b787f7e5b1 Mon Sep 17 00:00:00 2001 From: Kushal Kolar Date: Tue, 16 Apr 2024 19:31:34 -0400 Subject: [PATCH 51/66] Select gpu (#491) * add functions to choose GPU and get adapter info --- docs/source/api/gpu.rst | 5 + docs/source/conf.py | 1 + docs/source/index.rst | 9 +- docs/source/user_guide/gpu.rst | 287 ++++++++++++++++++++++++++++++--- fastplotlib/__init__.py | 8 +- fastplotlib/utils/__init__.py | 1 + fastplotlib/utils/gpu.py | 24 +++ 7 files changed, 304 insertions(+), 31 deletions(-) create mode 100644 docs/source/api/gpu.rst create mode 100644 fastplotlib/utils/gpu.py diff --git a/docs/source/api/gpu.rst b/docs/source/api/gpu.rst new file mode 100644 index 000000000..62ffd5797 --- /dev/null +++ b/docs/source/api/gpu.rst @@ -0,0 +1,5 @@ +fastplotlib.utils +***************** + +.. automodule:: fastplotlib.utils.gpu + :members: diff --git a/docs/source/conf.py b/docs/source/conf.py index a6a9fd1f6..f681a8101 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -53,6 +53,7 @@ "python": ("https://docs.python.org/3", None), "numpy": ("https://numpy.org/doc/stable/", None), "pygfx": ("https://pygfx.readthedocs.io/en/latest", None), + "wgpu": ("https://wgpu-py.readthedocs.io/en/latest", None), } html_theme_options = { diff --git a/docs/source/index.rst b/docs/source/index.rst index 0ceb146e4..0bca839b9 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -1,8 +1,3 @@ -.. fastplotlib documentation master file, created by - sphinx-quickstart on Wed Dec 28 12:46:56 2022. - You can adapt this file completely to your liking, but it should at least - contain the root `toctree` directive. - Welcome to fastplotlib's documentation! ======================================= @@ -14,7 +9,7 @@ Welcome to fastplotlib's documentation! .. toctree:: :caption: User Guide - :maxdepth: 1 + :maxdepth: 2 GPU Info @@ -29,6 +24,7 @@ Welcome to fastplotlib's documentation! Selectors Widgets Utils + GPU Summary ======= @@ -36,7 +32,6 @@ Summary A fast plotting library built using the `pygfx `_ render engine utilizing `Vulkan `_, `DX12 `_, or `Metal `_ via `WGPU `_, so it is very fast! We also aim to be an expressive plotting library that enables rapid prototyping for large scale explorative scientific visualization. `fastplotlib` will run on any framework that ``pygfx`` runs on, this includes ``glfw``, ``Qt`` and ``jupyter lab`` - Installation ============ diff --git a/docs/source/user_guide/gpu.rst b/docs/source/user_guide/gpu.rst index 18fb81cdf..3f4ff4bb4 100644 --- a/docs/source/user_guide/gpu.rst +++ b/docs/source/user_guide/gpu.rst @@ -1,8 +1,8 @@ -GPU Info -******** +GPU Info and selection +********************** FAQ ---- +=== 1. Do I need a GPU? @@ -10,7 +10,7 @@ Technically no, you can perform limited software rendering on linux using lavapi ``fastplotlib`` is intentionally built for realtime rendering using the latest GPU technologies, so we strongly recommend that you use a GPU. -2. My kernel keeps crashing when I create visualizations. +2. My kernel keeps crashing. This can happen under the following circumstances: @@ -19,24 +19,32 @@ This can happen under the following circumstances: If you aren't able to solve it please post an issue on GitHub. :) +3. Nothing renders or rendering is weird, or I see graphical artifacts. + +- Probably driver issues (see next section). + Drivers -------- +======= See the README: https://github.com/fastplotlib/fastplotlib?tab=readme-ov-file#graphics-drivers If you notice weird graphic artifacts, things not rendering, or other glitches try updating to the latest stable drivers. +GPU Info +======== + +View available adapters +----------------------- -View available GPU ------------------- +You can get a summary of all adapters that are available to ``WGPU`` like this:: -You can view all GPUs that are available to ``WGPU`` like this:: + import fastplotlib as fpl - import wgpu + adapters = fpl.enumerate_adapters() - for adapter in wgpu.gpu.enumerate_adapters(): - print(adapter.summary) + for a in adapters: + print(a.summary) For example, on a Thinkpad AMD laptop with a dedicated nvidia GPU this returns:: @@ -45,17 +53,260 @@ For example, on a Thinkpad AMD laptop with a dedicated nvidia GPU this returns:: llvmpipe (LLVM 15.0.6, 256 bits) (CPU) on Vulkan AMD Radeon Graphics (rembrandt, LLVM 15.0.6, DRM 3.52, 6.4.0-0.deb12.2-amd64) (Unknown) on OpenGL +In jupyter all the available adapters are also listed when ``fastplotlib`` is imported. + +You can get more detailed info on each adapter like this:: + + import pprint + for a in fpl.enumerate_adapters(): + pprint.pprint(a.request_adapter_info()) -GPU currently in use --------------------- +General description of the fields: + * vendor: GPU manufacturer + * device: specific GPU model + * description: GPU driver version + * adapter_type: indicates whether this is a discrete GPU, integrated GPU, or software rendering adapter (CPU) + * backend_type: one of "Vulkan", "Metal", or "D3D12" -If you want to know the GPU that a current plot is using you can check the adapter that the renderer is using:: +For more information on the fields see: https://gpuweb.github.io/gpuweb/#gpuadapterinfo + +Adapter currently in use +------------------------ + +If you want to know the adapter that a figure is using you can check the adapter on the renderer:: # for example if we make a plot - plot = fpl.Plot() - plot.add_image(np.random.rand(100, 100)) - plot.show() + fig = fpl.Figure() + fig[0, 0].add_image(np.random.rand(100, 100)) + fig.show() # GPU that is currently in use by the renderer - print(plot.renderer.device.adapter.summary) + print(fig.renderer.device.adapter.summary) + + +Diagnostic info +--------------- + +After creating a figure you can view WGPU diagnostic info like this:: + + fpl.print_wgpu_report() + + +Example output:: + + ██ system: + + platform: Linux-5.10.0-21-amd64-x86_64-with-glibc2.31 + python_implementation: CPython + python: 3.11.3 + + ██ versions: + + wgpu: 0.15.1 + cffi: 1.15.1 + jupyter_rfb: 0.4.2 + numpy: 1.26.4 + pygfx: 0.2.0 + pylinalg: 0.4.1 + fastplotlib: 0.1.0.a16 + + ██ wgpu_native_info: + + expected_version: 0.19.3.1 + lib_version: 0.19.3.1 + lib_path: ./resources/libwgpu_native-release.so + + ██ object_counts: + + count resource_mem + + Adapter: 1 + BindGroup: 3 + BindGroupLayout: 3 + Buffer: 6 696 + CanvasContext: 1 + CommandBuffer: 0 + CommandEncoder: 0 + ComputePassEncoder: 0 + ComputePipeline: 0 + Device: 1 + PipelineLayout: 0 + QuerySet: 0 + Queue: 1 + RenderBundle: 0 + RenderBundleEncoder: 0 + RenderPassEncoder: 0 + RenderPipeline: 3 + Sampler: 2 + ShaderModule: 3 + Texture: 6 9.60M + TextureView: 6 + + total: 36 9.60M + + ██ wgpu_native_counts: + + count mem backend a k r e el_size + + Adapter: 1 1.98K vulkan: 1 1 3 0 1.98K + BindGroup: 3 1.10K vulkan: 3 3 0 0 368 + BindGroupLayout: 3 960 vulkan: 5 3 2 0 320 + Buffer: 6 1.77K vulkan: 7 6 1 0 296 + CanvasContext: 0 0 0 0 0 0 160 + CommandBuffer: 1 1.25K vulkan: 0 0 0 1 1.25K + ComputePipeline: 0 0 vulkan: 0 0 0 0 288 + Device: 1 11.8K vulkan: 1 1 0 0 11.8K + PipelineLayout: 0 0 vulkan: 3 0 3 0 200 + QuerySet: 0 0 vulkan: 0 0 0 0 80 + Queue: 1 184 vulkan: 1 1 0 0 184 + RenderBundle: 0 0 vulkan: 0 0 0 0 848 + RenderPipeline: 3 1.68K vulkan: 3 3 0 0 560 + Sampler: 2 160 vulkan: 2 2 0 0 80 + ShaderModule: 3 2.40K vulkan: 3 3 0 0 800 + Texture: 6 4.94K vulkan: 7 6 1 0 824 + TextureView: 6 1.48K vulkan: 6 6 1 0 248 + + total: 36 29.7K + + * The a, k, r, e are allocated, kept, released, and error, respectively. + * Reported memory does not include buffer/texture data. + + ██ pygfx_adapter_info: + + vendor: radv + architecture: + device: AMD RADV POLARIS10 (ACO) + description: Mesa 20.3.5 (ACO) + vendor_id: 4.09K + device_id: 26.5K + adapter_type: DiscreteGPU + backend_type: Vulkan + + ██ pygfx_features: + + adapter device + + bgra8unorm-storage: - - + depth32float-stencil8: ✓ - + depth-clip-control: ✓ - + float32-filterable: ✓ ✓ + indirect-first-instance: ✓ - + rg11b10ufloat-renderable: ✓ - + shader-f16: - - + texture-compression-astc: - - + texture-compression-bc: ✓ - + texture-compression-etc2: - - + timestamp-query: ✓ - + MultiDrawIndirect: ✓ - + MultiDrawIndirectCount: ✓ - + PushConstants: ✓ - + TextureAdapterSpecificFormatFeatures: ✓ - + VertexWritableStorage: ✓ - + + ██ pygfx_limits: + + adapter device + + max_bind_groups: 8 8 + max_bind_groups_plus_vertex_buffers: 0 0 + max_bindings_per_bind_group: 1.00K 1.00K + max_buffer_size: 2.14G 2.14G + max_color_attachment_bytes_per_sample: 0 0 + max_color_attachments: 0 0 + max_compute_invocations_per_workgroup: 1.02K 1.02K + max_compute_workgroup_size_x: 1.02K 1.02K + max_compute_workgroup_size_y: 1.02K 1.02K + max_compute_workgroup_size_z: 1.02K 1.02K + max_compute_workgroup_storage_size: 32.7K 32.7K + max_compute_workgroups_per_dimension: 65.5K 65.5K + max_dynamic_storage_buffers_per_pipeline_layout: 8 8 + max_dynamic_uniform_buffers_per_pipeline_layout: 16 16 + max_inter_stage_shader_components: 128 128 + max_inter_stage_shader_variables: 0 0 + max_sampled_textures_per_shader_stage: 8.38M 8.38M + max_samplers_per_shader_stage: 8.38M 8.38M + max_storage_buffer_binding_size: 2.14G 2.14G + max_storage_buffers_per_shader_stage: 8.38M 8.38M + max_storage_textures_per_shader_stage: 8.38M 8.38M + max_texture_array_layers: 2.04K 2.04K + max_texture_dimension1d: 16.3K 16.3K + max_texture_dimension2d: 16.3K 16.3K + max_texture_dimension3d: 2.04K 2.04K + max_uniform_buffer_binding_size: 2.14G 2.14G + max_uniform_buffers_per_shader_stage: 8.38M 8.38M + max_vertex_attributes: 32 32 + max_vertex_buffer_array_stride: 2.04K 2.04K + max_vertex_buffers: 16 16 + min_storage_buffer_offset_alignment: 32 32 + min_uniform_buffer_offset_alignment: 32 32 + + ██ pygfx_caches: + + count hits misses + + full_quad_objects: 1 0 2 + mipmap_pipelines: 0 0 0 + layouts: 1 0 3 + bindings: 1 0 1 + shader_modules: 2 0 2 + pipelines: 2 0 2 + shadow_pipelines: 0 0 0 + + ██ pygfx_resources: + + Texture: 8 + Buffer: 23 + + +Select GPU (adapter) +==================== + +You can select an adapter by passing one of the ``wgpu.GPUAdapter`` instances returned by ``fpl.enumerate_adapters()`` +to ``fpl.select_adapter()``:: + + # get info or summary of all adapters to pick an adapter + print([a.request_adapter_info() for a in fpl.enumerate_adapters()]) + + # example, pick adapter at index 2 + chosen_gpu = fpl.enumerate_adapters()[2] + fpl.select_adapter(chosen_gpu) + +**You must select an adapter before creating a** ``Figure`` **, otherwise the default adapter will be selected. Once a** +``Figure`` **is created the adapter cannot be changed.** + +Note that using this function reduces the portability of your code, because +it's highly specific for your current machine/environment. + +The order of the adapters returned by ``wgpu.gpu.enumerate_adapters()`` is +such that Vulkan adapters go first, then Metal, then D3D12, then OpenGL. +Within each category, the order as provided by the particular backend is +maintained. Note that the same device may be present via multiple backends +(e.g. vulkan/opengl). + +We cannot make guarantees about whether the order of the adapters matches +the order as reported by e.g. ``nvidia-smi``. We have found that on a Linux +multi-gpu cluster, the order does match, but we cannot promise that this is +always the case. If you want to make sure, do some testing by allocating big +buffers and checking memory usage using ``nvidia-smi`` + +Example to allocate and check GPU mem usage:: + + import subprocess + + import wgpu + import torch + + def allocate_gpu_mem_with_wgpu(idx): + a = wgpu.gpu.enumerate_adapters()[idx] + d = a.request_device() + b = d.create_buffer(size=10*2**20, usage=wgpu.BufferUsage.COPY_DST) + return b + + def allocate_gpu_mem_with_torch(idx): + d = torch.device(f"cuda:{idx}") + return torch.ones([2000, 10], dtype=torch.float32, device=d) + + def show_mem_usage(): + print(subprocess.run(["nvidia-smi"])) +See https://github.com/pygfx/wgpu-py/issues/482 for more details. diff --git a/fastplotlib/__init__.py b/fastplotlib/__init__.py index 7a6290d95..c1e4d2f2c 100644 --- a/fastplotlib/__init__.py +++ b/fastplotlib/__init__.py @@ -7,15 +7,11 @@ from .layouts import Figure from .widgets import ImageWidget -from .utils import config - -import wgpu +from .utils import config, enumerate_adapters, select_adapter, print_wgpu_report with open(Path(__file__).parent.joinpath("VERSION"), "r") as f: __version__ = f.read().split("\n")[0] -adapters = [a.summary for a in wgpu.gpu.enumerate_adapters()] - -if len(adapters) < 1: +if len(enumerate_adapters()) < 1: raise IndexError("No WGPU adapters found, fastplotlib will not work.") diff --git a/fastplotlib/utils/__init__.py b/fastplotlib/utils/__init__.py index 6759b2497..e1eef6447 100644 --- a/fastplotlib/utils/__init__.py +++ b/fastplotlib/utils/__init__.py @@ -2,6 +2,7 @@ from .functions import * +from .gpu import enumerate_adapters, select_adapter, print_wgpu_report @dataclass diff --git a/fastplotlib/utils/gpu.py b/fastplotlib/utils/gpu.py new file mode 100644 index 000000000..72d303d23 --- /dev/null +++ b/fastplotlib/utils/gpu.py @@ -0,0 +1,24 @@ +import wgpu +from pygfx.renderers.wgpu import select_adapter as pygfx_select_adapter +from pygfx import print_wgpu_report as pygfx_print_wgpu_report + + +def enumerate_adapters() -> list[wgpu.GPUAdapter]: + return wgpu.gpu.enumerate_adapters() + + +enumerate_adapters.__doc__ = wgpu.gpu.enumerate_adapters.__doc__ + + +def select_adapter(adapter: wgpu.GPUAdapter): + return pygfx_select_adapter(adapter) + + +select_adapter.__doc__ = pygfx_select_adapter.__doc__ + + +def print_wgpu_report(): + return pygfx_print_wgpu_report() + + +print_wgpu_report.__doc__ = pygfx_print_wgpu_report.__doc__ From 08733b952f292628c502436c2b64ed5064b74af7 Mon Sep 17 00:00:00 2001 From: Caitlin Lewis <69729525+clewis7@users.noreply.github.com> Date: Sun, 21 Apr 2024 12:21:55 -0400 Subject: [PATCH 52/66] modify mission statement --- GOVERNANCE.md | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/GOVERNANCE.md b/GOVERNANCE.md index 6c538079e..27acb1c45 100644 --- a/GOVERNANCE.md +++ b/GOVERNANCE.md @@ -8,11 +8,20 @@ The purpose of this document is to formalize the governance process used by the The mission of `fastplotlib` is to leverage new graphics APIs and modern GPU hardware to create fast and interactive scientific visualizations using an expressive and elegant API. +`fastplotlib` aims to provide a library that allows for the following: +- Rapid prototyping and algorithm development +- Realtime analysis and visualization +- Efficient rendering of thousands of objects +- Compatibility with lazy-loading and lazy-compute objects +- Shipping dependent packages as a distributable (ex: PyInstaller) + +Ultimately, `fastplotlib` is, and will always be, a free and open-source project that belongs to the community. Our end goal is to aid in the advancement of science, and as a result, we will guide the project in a direction that best serves our community in achieving this purpose. + ## Leadership Team ### Maintainers -The maintainers are the core developers of fastplotlib and together have a complete understanding of the codebase. They are also known as code-owners. +The maintainers are the core developers of fastplotlib and together have a complete understanding of the codebase. They are also known as code-owners. At any given time, there must be a minimum of two maintainers. The current maintainers are: From b2cf625fd5a335e27a49665fc435207a299a9e56 Mon Sep 17 00:00:00 2001 From: Kushal Kolar Date: Wed, 1 May 2024 02:41:05 -0400 Subject: [PATCH 53/66] Small cleanup (#498) * make TESTING var in nb_test_utils, useful to check if nb is in testing state * rename z_position to z_offset in line_collection, fix bug where manual z_offset was ignored and force to 1.0 * if camera is not orthographic, then adding new graphics to a PlotArea does not stack them along the z-axis * update graphic methods mixin * fix bug when a single controller_type is specified * update simple.ipynb, add line animation examples, add subplots * rename simple.ipynb -> quickstart.ipynb * update README * better error message when enumerate_adapters fails * black --- README.md | 35 +- examples/notebooks/nb_test_utils.py | 21 +- .../{simple.ipynb => quickstart.ipynb} | 690 +++++++++++++++++- fastplotlib/__init__.py | 16 +- fastplotlib/graphics/line_collection.py | 26 +- fastplotlib/layouts/_figure.py | 19 +- fastplotlib/layouts/_graphic_methods_mixin.py | 16 +- fastplotlib/layouts/_plot_area.py | 16 +- 8 files changed, 765 insertions(+), 74 deletions(-) rename examples/notebooks/{simple.ipynb => quickstart.ipynb} (65%) diff --git a/README.md b/README.md index 64e1649e8..5c4eb2d45 100644 --- a/README.md +++ b/README.md @@ -55,21 +55,19 @@ Questions, issues, ideas? Post an [issue](https://github.com/fastplotlib/fastplo # Installation -Install using `pip`. - ### Minimal, use with your own `Qt` or `glfw` applications ```bash pip install fastplotlib ``` -**This does not give you `Qt` or `glfw`, you will have to install one of them yourself depending on your preference**. +**This does not give you `PyQt`/`PySide` or `glfw`, you will have to install your preferred GUI framework separately**. ### Notebook ```bash pip install "fastplotlib[notebook]" ``` -**Recommended: install `simplejpeg` for much faster notebook visualization, this requires you to first install [libjpeg-turbo](https://libjpeg-turbo.org/)** +**Strongly recommended: install `simplejpeg` for much faster notebook visualization, this requires you to first install [libjpeg-turbo](https://libjpeg-turbo.org/)** ```bash pip install simplejpeg @@ -77,9 +75,12 @@ pip install simplejpeg > **Note** > -> `fastplotlib` and `pygfx` are fast evolving projects, the version available through pip might be outdated, you will need to follow the "For developers" instructions below if you want the latest features. You can find the release history on pypi here: https://pypi.org/project/fastplotlib/#history +> `fastplotlib` and `pygfx` are fast evolving projects, the version available through pip might be outdated, you will need to follow the "For developers" instructions below if you want the latest features. You can find the release history here: https://github.com/fastplotlib/fastplotlib/releases ### For developers + +Make sure you have [git-lfs](https://github.com/git-lfs/git-lfs#installing) installed. + ```bash git clone https://github.com/fastplotlib/fastplotlib.git cd fastplotlib @@ -88,16 +89,20 @@ cd fastplotlib pip install -e ".[notebook,docs,tests]" ``` +Se [Contributing](https://github.com/fastplotlib/fastplotlib?tab=readme-ov-file#heart-contributing) for more details on development + # Examples -> **Note** -> -> `fastplotlib` and `pygfx` are fast evolving, you may require the latest `pygfx` and `fastplotlib` from github to use the examples in the main branch. +> **Note:** `fastplotlib` and `pygfx` are fast evolving, you will probably require the latest `pygfx` and `fastplotlib` from github to use the examples in the main branch. + +`fastplotlib` code is identical across notebook (`jupyter`), and desktop use with `Qt`/`PySide` or `glfw`. -Note that `fastplotlib` code is basically identical between desktop and notebook usage. The differences are: +Even if you do not intend to use notebooks with `fastplotlib`, the `quickstart.ipynb` notebook is currently the best way to get familiar with the API: https://github.com/fastplotlib/fastplotlib/tree/main/examples/notebooks/quickstart.ipynb + +The specifics for running `fastplotlib` in different GUI frameworks are: - Running in `glfw` requires a `fastplotlib.run()` call (which is really just a `wgpu` `run()` call) -- To use it in `Qt` you must encapsulate it within a `QApplication`, see `examples/qt` -- Notebooks plots have ipywidget-based toolbars and widgets 😄 +- With `Qt` you can encapsulate it within a `QApplication`, see `examples/qt` +- Notebooks plots have ipywidget-based toolbars and widgets. There are plans to move toward an identical in-canvas toolbar with UI elements across all supported frameworks 😄 ### Desktop examples using `glfw` or `Qt` @@ -120,7 +125,7 @@ Notebook examples are here: https://github.com/fastplotlib/fastplotlib/tree/main/examples/notebooks -**Start with `simple.ipynb`.** +**Start with `quickstart.ipynb`.** Some of the examples require imageio: ``` @@ -135,7 +140,9 @@ Our SciPy 2023 talk walks through numerous demos: https://github.com/fastplotlib You will need a relatively modern GPU (newer integrated GPUs in CPUs are usually fine). Generally if your GPU is from 2017 or later it should be fine. -For more information see: https://wgpu-py.readthedocs.io/en/stable/start.html#platform-requirements +For more detailed information, such as use on cloud computing infrastructure, see: https://wgpu-py.readthedocs.io/en/stable/start.html#platform-requirements + +Some more information on GPUs is here: https://fastplotlib.readthedocs.io/en/latest/user_guide/gpu.html ### Windows: Vulkan drivers should be installed by default on Windows 11, but you will need to install your GPU manufacturer's driver package (Nvidia or AMD). If you have an integrated GPU within your CPU, you might still need to install a driver package too, check your CPU manufacturer's info. @@ -162,7 +169,7 @@ sudo apt install llvm-dev libturbojpeg* libgl1-mesa-dev libgl1-mesa-glx libglapi ``` ### Mac OSX: -WGPU uses Metal instead of Vulkan on Mac. You will need at least Mac OSX 10.13. The OS should come with Metal pre-installed so you should be good to go! +WGPU uses Metal instead of Vulkan on Mac. You will need at least Mac OSX 10.13. The OS should come with Metal pre-installed, so you should be good to go! # :heart: Contributing diff --git a/examples/notebooks/nb_test_utils.py b/examples/notebooks/nb_test_utils.py index cb84a5271..791640fe2 100644 --- a/examples/notebooks/nb_test_utils.py +++ b/examples/notebooks/nb_test_utils.py @@ -21,6 +21,13 @@ # store all the failures to allow the nb to proceed to test other examples FAILURES = list() +if "FASTPLOTLIB_NB_TESTS" not in os.environ.keys(): + TESTING = False + +else: + if os.environ["FASTPLOTLIB_NB_TESTS"] == "1": + TESTING = True + # TODO: consolidate testing functions into one module so we don't have this separate one for notebooks @@ -83,18 +90,8 @@ def normalize_image(img): return img -def _run_tests(): - if "FASTPLOTLIB_NB_TESTS" not in os.environ.keys(): - return False - - if os.environ["FASTPLOTLIB_NB_TESTS"] == "1": - return True - - return False - - def plot_test(name, fig: fpl.Figure): - if not _run_tests(): + if not TESTING: return snapshot = fig.canvas.snapshot() @@ -157,7 +154,7 @@ def get_diffs_rgba(slicer): def notebook_finished(): - if not _run_tests(): + if not TESTING: return if len(FAILURES) > 0: diff --git a/examples/notebooks/simple.ipynb b/examples/notebooks/quickstart.ipynb similarity index 65% rename from examples/notebooks/simple.ipynb rename to examples/notebooks/quickstart.ipynb index 3b42385c8..c40e5c9ff 100644 --- a/examples/notebooks/simple.ipynb +++ b/examples/notebooks/quickstart.ipynb @@ -7,9 +7,9 @@ "tags": [] }, "source": [ - "# Introduction to `fastplotlib`\n", + "# Introduction to `fastplotlib` 🚀\n", "\n", - "This notebook goes through 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, line, scatter plots, subplots and simple animations" ] }, { @@ -37,6 +37,7 @@ "execution_count": null, "id": "5c50e177-5800-4e19-a4f6-d0e0a082e4cd", "metadata": { + "is_executing": true, "tags": [] }, "outputs": [], @@ -68,7 +69,7 @@ "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" + "from nb_test_utils import plot_test, notebook_finished, TESTING" ] }, { @@ -683,16 +684,16 @@ "xs = np.linspace(-10, 10, 100)\n", "# sine wave\n", "ys = np.sin(xs)\n", - "sine = np.dstack([xs, ys])[0]\n", + "sine = np.column_stack([xs, ys])\n", "\n", "# cosine wave\n", "ys = np.cos(xs) + 5\n", - "cosine = np.dstack([xs, ys])[0]\n", + "cosine = np.column_stack([xs, ys])\n", "\n", "# sinc function\n", "a = 0.5\n", "ys = np.sinc(xs) * 3 + 8\n", - "sinc = np.dstack([xs, ys])[0]" + "sinc = np.column_stack([xs, ys])" ] }, { @@ -1009,6 +1010,104 @@ "fig_lines.close()" ] }, + { + "cell_type": "markdown", + "id": "3ada943c-f02c-419b-b384-3865ecbe25fb", + "metadata": {}, + "source": [ + "# Animation example with lines" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8fb64bed-3b47-43e3-9ef5-f8223005b7d2", + "metadata": {}, + "outputs": [], + "source": [ + "# just another example of animations\n", + "start, stop = 0, 2 * np.pi\n", + "increment = (2 * np.pi) / 50\n", + "\n", + "# make a simple sine wave\n", + "xs = np.linspace(start, stop, 100)\n", + "ys = np.sin(xs)\n", + "\n", + "fig = fpl.Figure()\n", + "fig[0, 0].add_line(ys, name=\"sine\")\n", + "\n", + "fig.show(maintain_aspect=False)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3002bc55-2612-40c8-a088-c07e138b739a", + "metadata": {}, + "outputs": [], + "source": [ + "# increment along the x-axis on each render loop :D \n", + "def update_line(subplot):\n", + " global increment, start, stop\n", + " xs = np.linspace(start + increment, stop + increment, 100)\n", + " ys = np.sin(xs)\n", + " \n", + " start += increment\n", + " stop += increment\n", + "\n", + " # change only the y-axis values of the line\n", + " subplot[\"sine\"].data[:, 1] = ys\n", + "\n", + "\n", + "fig[0, 0].add_animations(update_line)" + ] + }, + { + "cell_type": "markdown", + "id": "fc8c68af-810a-4564-b97d-020054b57f37", + "metadata": {}, + "source": [ + "You can remove an animation" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6321c989-60b2-4c9b-a638-a7ac1a2e4a84", + "metadata": {}, + "outputs": [], + "source": [ + "fig[0, 0].remove_animation(update_line)" + ] + }, + { + "cell_type": "markdown", + "id": "21bb17a8-cfca-4f4b-adc9-614fcffad447", + "metadata": {}, + "source": [ + "And add it back" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7749a085-d853-4859-bf99-f2bbecb50306", + "metadata": {}, + "outputs": [], + "source": [ + "fig[0, 0].add_animations(update_line)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2cf02a6c-cb1a-4972-ae96-09e6ba37e9dd", + "metadata": {}, + "outputs": [], + "source": [ + "fig.close()" + ] + }, { "cell_type": "markdown", "id": "2c90862e-2f2a-451f-a468-0cf6b857e87a", @@ -1036,7 +1135,7 @@ "\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", + "spiral = np.column_stack([xs, ys, zs])\n", "\n", "fig_l3d[0, 0].add_line(data=spiral, thickness=2, cmap='winter')\n", "\n", @@ -1103,6 +1202,17 @@ "fig_l3d[0, 0].controller = \"panzoom\"" ] }, + { + "cell_type": "code", + "execution_count": null, + "id": "11577230-8268-4b1d-a384-62d4c7f2483f", + "metadata": {}, + "outputs": [], + "source": [ + "# or an orbit controller\n", + "fig_l3d[0, 0].controller = \"orbit\"" + ] + }, { "cell_type": "code", "execution_count": null, @@ -1116,6 +1226,141 @@ "fig_l3d.close()" ] }, + { + "cell_type": "markdown", + "id": "4221ecae-74dc-464c-addf-f4fe91614a26", + "metadata": {}, + "source": [ + "# A travelling electromagnetic wave :D " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "34139d49-af6e-4dc9-90cc-fbf193a64e7f", + "metadata": {}, + "outputs": [], + "source": [ + "import fastplotlib as fpl\n", + "import numpy as np" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "0716a5c9-cf7b-417c-b809-032f5a217c4d", + "metadata": {}, + "outputs": [], + "source": [ + "fig_em = fpl.Figure(\n", + " cameras=\"3d\", \n", + " controller_types=\"orbit\", \n", + " size=(700, 400)\n", + ")\n", + "\n", + "start, stop = 0, 4 * np.pi\n", + "\n", + "# let's define the x, y and z axes for each with direction of wave propogation along the z-axis\n", + "# electric field in the xz plane travelling along\n", + "zs = np.linspace(start, stop, 200)\n", + "e_ys = np.zeros(200)\n", + "e_xs = np.sin(zs)\n", + "electric = np.column_stack([e_xs, e_ys, zs])\n", + "\n", + "# magnetic field in the yz plane\n", + "zs = np.linspace(start, stop, 200)\n", + "m_ys = np.sin(zs)\n", + "m_xs = np.zeros(200)\n", + "magnetic = np.column_stack([m_xs, m_ys, zs])\n", + "\n", + "# add the lines\n", + "fig_em[0, 0].add_line(electric, colors=\"blue\", thickness=2, name=\"e\")\n", + "fig_em[0, 0].add_line(magnetic, colors=\"red\", thickness=2, name=\"m\")\n", + "\n", + "# draw vector line at every 10th position\n", + "electric_vectors = [np.array([[0, 0, z], [x, 0, z]]) for (x, z) in zip(e_xs[::10], zs[::10])]\n", + "magnetic_vectors = [np.array([[0, 0, z], [0, y, z]]) for (y, z) in zip(m_ys[::10], zs[::10])]\n", + "\n", + "# add as a line collection\n", + "fig_em[0, 0].add_line_collection(electric_vectors, colors=\"blue\", thickness=1.5, name=\"e-vec\", z_offset=0)\n", + "fig_em[0, 0].add_line_collection(magnetic_vectors, colors=\"red\", thickness=1.5, name=\"m-vec\", z_offset=0)\n", + "# note that the z_offset in `add_line_collection` is not data-related\n", + "# it is the z-offset for where to place the *graphic*, by default with Orthographic cameras (i.e. 2D views)\n", + "# it will increment by 1 for each line in the collection, we want to disable this so set z_position=0\n", + "\n", + "# axes are a WIP, just draw a white line along z for now\n", + "z_axis = np.array([[0, 0, 0], [0, 0, stop]])\n", + "fig_em[0, 0].add_line(z_axis, colors=\"w\", thickness=1)\n", + "\n", + "# just a pre-saved camera state\n", + "state = {\n", + " 'position': np.array([-8.0 , 6.0, -2.0]),\n", + " 'rotation': np.array([0.09, 0.9 , 0.2, -0.5]),\n", + " 'scale': np.array([1., 1., 1.]),\n", + " 'reference_up': np.array([0., 1., 0.]),\n", + " 'fov': 50.0,\n", + " 'width': 12,\n", + " 'height': 12,\n", + " 'zoom': 1.35,\n", + " 'maintain_aspect': True,\n", + " 'depth_range': None\n", + "}\n", + "\n", + "\n", + "fig_em[0, 0].camera.set_state(state)\n", + "\n", + "fig_em.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "faa52d55-2631-422d-8836-ec371be728c0", + "metadata": {}, + "outputs": [], + "source": [ + "fig_em[0, 0].camera.zoom = 1.5" + ] + }, + { + "cell_type": "markdown", + "id": "d886c63b-7bcb-40d4-b315-dffff71f82f0", + "metadata": {}, + "source": [ + "## Animation for the EM wave" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7ae54258-0c5a-40cc-9ca0-40b47d25de9a", + "metadata": {}, + "outputs": [], + "source": [ + "increment = np.pi * 4 / 100\n", + "\n", + "# moves the wave one step along the z-axis\n", + "def tick(subplot):\n", + " global increment, start, stop, zs\n", + " new_zs = np.linspace(start, stop, 200)\n", + " new_data = np.sin(new_zs)\n", + "\n", + " # just change the x-axis vals for the electric field\n", + " subplot[\"e\"].data[:, 0] = new_data\n", + " # and y-axis vals for magnetic field\n", + " subplot[\"m\"].data[:, 1] = new_data\n", + "\n", + " # update the vector lines\n", + " for i, (value, z) in enumerate(zip(new_data[::10], zs[::10])):\n", + " subplot[\"e-vec\"].graphics[i].data = np.array([[0, 0, z], [value, 0, z]])\n", + " subplot[\"m-vec\"].graphics[i].data = np.array([[0, 0, z], [0, value, z]])\n", + " \n", + " start += increment\n", + " stop += increment\n", + "\n", + "fig_em[0, 0].add_animations(tick)" + ] + }, { "cell_type": "markdown", "id": "a202b3d0-2a0b-450a-93d4-76d0a1129d1d", @@ -1263,6 +1508,437 @@ "fig_scatter.close()" ] }, + { + "cell_type": "markdown", + "id": "b354b04d", + "metadata": { + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } + }, + "source": [ + "## More subplots" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e0797523", + "metadata": { + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "# Figure of shape 2 x 3 with all controllers synced\n", + "figure_grid = fpl.Figure(shape=(2, 3), controller_ids=\"sync\")\n", + "\n", + "# Make a random image graphic for each subplot\n", + "for subplot in figure_grid:\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(f):\n", + " for subplot in f:\n", + " new_data = np.random.rand(512, 512)\n", + " # index the image graphic by name and set the data\n", + " subplot[\"rand-img\"].data = new_data\n", + "\n", + "# add the animation function\n", + "figure_grid.add_animations(update_data)\n", + "\n", + "# show the gridplot\n", + "figure_grid.show()" + ] + }, + { + "cell_type": "markdown", + "id": "0c5b20e5", + "metadata": { + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } + }, + "source": [ + "### Slicing GridPlot" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a14b7e90", + "metadata": { + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "# positional indexing\n", + "# row 0 and col 0\n", + "figure_grid[0, 0]" + ] + }, + { + "cell_type": "markdown", + "id": "45f29bed", + "metadata": { + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } + }, + "source": [ + "You can get the graphics within a subplot, just like with simple `Plot`" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "fbe632aa", + "metadata": { + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "figure_grid[0, 1].graphics" + ] + }, + { + "cell_type": "markdown", + "id": "44ccf745", + "metadata": { + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } + }, + "source": [ + "and change their properties" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "85e6bf84", + "metadata": { + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "figure_grid[0, 1].graphics[0].vmax = 0.5" + ] + }, + { + "cell_type": "markdown", + "id": "fb4155b9", + "metadata": { + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } + }, + "source": [ + "more slicing with `GridPlot`" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e6c3af07", + "metadata": { + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "# you can give subplots human-readable string names\n", + "figure_grid[0, 2].name = \"top-right-plot\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8848486b", + "metadata": { + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "figure_grid[\"top-right-plot\"]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "cb7566a5", + "metadata": { + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "# view its position\n", + "figure_grid[\"top-right-plot\"].position" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a002a426", + "metadata": { + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "# these are really the same\n", + "figure_grid[\"top-right-plot\"] is figure_grid[0, 2]" + ] + }, + { + "cell_type": "markdown", + "id": "df361421", + "metadata": { + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } + }, + "source": [ + "Indexing with subplot name and graphic name" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c9915469", + "metadata": { + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "figure_grid[\"top-right-plot\"][\"rand-img\"].vmin = 0.5" + ] + }, + { + "cell_type": "markdown", + "id": "219648d3", + "metadata": { + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } + }, + "source": [ + "## Figure subplot customization" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1dcfe24c", + "metadata": { + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "# 2 rows and 3 columns\n", + "shape = (2, 3)\n", + "\n", + "# pan-zoom controllers for each view\n", + "# views are synced if they have the\n", + "# same controller ID\n", + "controller_ids = [\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", + "figure_grid = fpl.Figure(\n", + " shape=shape,\n", + " controller_ids=controller_ids,\n", + " names=names,\n", + ")\n", + "\n", + "\n", + "# Make a random image graphic for each subplot\n", + "for subplot in figure_grid:\n", + " data = np.random.rand(512, 512)\n", + " # create and add an ImageGraphic\n", + " subplot.add_image(data=data, name=\"rand-image\")\n", + "\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", + "figure_grid.add_animations(set_random_frame)\n", + "figure_grid.show()" + ] + }, + { + "cell_type": "markdown", + "id": "be699284", + "metadata": { + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } + }, + "source": [ + "Indexing the gridplot to access subplots" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "212a6e4f", + "metadata": { + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "# can access subplot by name\n", + "figure_grid[\"subplot0\"]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b758b240", + "metadata": { + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "# can access subplot by index\n", + "figure_grid[0, 0]" + ] + }, + { + "cell_type": "markdown", + "id": "868f0de4", + "metadata": { + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } + }, + "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": "bc14549d", + "metadata": { + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "# can access graphic directly via name\n", + "figure_grid[\"subplot0\"][\"rand-image\"]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "99e3726e", + "metadata": { + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "figure_grid[\"subplot0\"][\"rand-image\"].vmin = 0.6\n", + "figure_grid[\"subplot0\"][\"rand-image\"].vmax = 0.8" + ] + }, + { + "cell_type": "markdown", + "id": "e3350b37", + "metadata": { + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } + }, + "source": [ + "positional indexing also works event if subplots have string names" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3b1986f5", + "metadata": { + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "figure_grid[1, 0][\"rand-image\"].vim = 0.1\n", + "figure_grid[1, 0][\"rand-image\"].vmax = 0.3" + ] + }, { "cell_type": "code", "execution_count": null, diff --git a/fastplotlib/__init__.py b/fastplotlib/__init__.py index c1e4d2f2c..8b46dcc0b 100644 --- a/fastplotlib/__init__.py +++ b/fastplotlib/__init__.py @@ -1,6 +1,6 @@ from pathlib import Path -from .utils.gui import run +from .utils.gui import run # noqa from .graphics import * from .graphics.selectors import * from .legends import * @@ -14,4 +14,16 @@ __version__ = f.read().split("\n")[0] if len(enumerate_adapters()) < 1: - raise IndexError("No WGPU adapters found, fastplotlib will not work.") + raise IndexError( + f"WGPU could not enumerate any adapters, fastplotlib will not work.\n" + f"This is caused by one of the following:\n" + f"1. You do not have a hardware GPU installed and you do not have " + f"software rendering (ex. lavapipe) installed either\n" + f"2. Your GPU drivers are not installed or something is wrong with your GPU driver installation, " + f"re-installing the latest drivers from your hardware vendor (probably Nvidia or AMD) may help.\n" + f"3. You are missing system libraries that are required for WGPU to access GPU(s), this is " + f"common in cloud computing environments.\n" + f"These two links can help you troubleshoot:\n" + f"https://wgpu-py.readthedocs.io/en/stable/start.html#platform-requirements\n" + f"https://fastplotlib.readthedocs.io/en/latest/user_guide/gpu.html\n" + ) diff --git a/fastplotlib/graphics/line_collection.py b/fastplotlib/graphics/line_collection.py index 1c2e151e8..da74cc54e 100644 --- a/fastplotlib/graphics/line_collection.py +++ b/fastplotlib/graphics/line_collection.py @@ -19,7 +19,7 @@ class LineCollection(GraphicCollection, Interaction): def __init__( self, data: List[np.ndarray], - z_position: Iterable[float] | float = None, + z_offset: Iterable[float | int] | float | int = None, thickness: float | Iterable[float] = 2.0, colors: str | Iterable[str] | np.ndarray | Iterable[np.ndarray] = "w", alpha: float = 1.0, @@ -39,9 +39,9 @@ def __init__( 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: Iterable 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 + z_offset: Iterable of float or float, optional + | if ``float`` | ``int``, single offset will be used for all lines + | if ``list`` of ``float`` | ``int``, each value will apply to the individual lines thickness: float or Iterable of float, default 2.0 | if ``float``, single thickness will be used for all lines @@ -90,8 +90,8 @@ def __init__( super().__init__(name) - if not isinstance(z_position, float) and z_position is not None: - if len(data) != len(z_position): + if not isinstance(z_offset, (float, int)) and z_offset is not None: + if len(data) != len(z_offset): raise ValueError( "z_position must be a single float or an iterable with same length as data" ) @@ -178,10 +178,10 @@ def __init__( self._set_world_object(pygfx.Group()) for i, d in enumerate(data): - if isinstance(z_position, list): - _z = z_position[i] + if isinstance(z_offset, list): + _z = z_offset[i] else: - _z = 1.0 + _z = z_offset if isinstance(thickness, list): _s = thickness[i] @@ -478,7 +478,7 @@ class LineStack(LineCollection): def __init__( self, data: List[np.ndarray], - z_position: Iterable[float] | float = None, + z_offset: Iterable[float] | float = None, thickness: float | Iterable[float] = 2.0, colors: str | Iterable[str] | np.ndarray | Iterable[np.ndarray] = "w", alpha: float = 1.0, @@ -500,8 +500,8 @@ def __init__( 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: Iterable of float or float, optional - | if ``float``, single position will be used for all lines + z_offset: Iterable of float or float, optional + | if ``float``, single offset will be used for all lines | if ``list`` of ``float``, each value will apply to the individual lines thickness: float or Iterable of float, default 2.0 @@ -550,7 +550,7 @@ def __init__( """ super().__init__( data=data, - z_position=z_position, + z_offset=z_offset, thickness=thickness, colors=colors, alpha=alpha, diff --git a/fastplotlib/layouts/_figure.py b/fastplotlib/layouts/_figure.py index 7e83e103a..1c7439613 100644 --- a/fastplotlib/layouts/_figure.py +++ b/fastplotlib/layouts/_figure.py @@ -235,30 +235,23 @@ def __init__( # `create_controller()` will auto-determine controller for each subplot based on defaults controller_types = np.array(["default"] * len(self)).reshape(self.shape) - elif isinstance(controller_types, str): - if controller_types not in valid_controller_types.keys(): - raise ValueError( - f"invalid controller_types argument, you may pass either a single controller type as a str, or an" - f"iterable of controller types from the selection: {valid_controller_types.keys()}" - ) - # valid controller types + if isinstance(controller_types, str): + controller_types = [[controller_types]] + types_flat = list(chain(*controller_types)) # str controller_type or pygfx instances valid_str = list(valid_controller_types.keys()) + ["default"] - valid_instances = tuple(valid_controller_types.values()) # make sure each controller type is valid for controller_type in types_flat: if controller_type is None: continue - if (controller_type not in valid_str) and ( - not isinstance(controller_type, valid_instances) - ): + if controller_type not in valid_str: raise ValueError( - f"You have passed an invalid controller type, valid controller_types arguments are:\n" - f"{valid_str} or instances of {[c.__name__ for c in valid_instances]}" + f"You have passed the invalid `controller_type`: {controller_type}. " + f"Valid `controller_types` arguments are:\n {valid_str}" ) controller_types: np.ndarray[pygfx.Controller] = np.asarray( diff --git a/fastplotlib/layouts/_graphic_methods_mixin.py b/fastplotlib/layouts/_graphic_methods_mixin.py index a7acb5eec..9f82cfed5 100644 --- a/fastplotlib/layouts/_graphic_methods_mixin.py +++ b/fastplotlib/layouts/_graphic_methods_mixin.py @@ -178,7 +178,7 @@ def add_image( def add_line_collection( self, data: List[numpy.ndarray], - z_position: Union[Iterable[float], float] = None, + z_offset: Union[Iterable[float], float] = None, thickness: Union[float, Iterable[float]] = 2.0, colors: Union[str, Iterable[str], numpy.ndarray, Iterable[numpy.ndarray]] = "w", alpha: float = 1.0, @@ -199,8 +199,8 @@ def add_line_collection( 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: Iterable of float or float, optional - | if ``float``, single position will be used for all lines + z_offset: Iterable of float or float, optional + | if ``float``, single offset will be used for all lines | if ``list`` of ``float``, each value will apply to the individual lines thickness: float or Iterable of float, default 2.0 @@ -251,7 +251,7 @@ def add_line_collection( return self._create_graphic( LineCollection, data, - z_position, + z_offset, thickness, colors, alpha, @@ -348,7 +348,7 @@ def add_line( def add_line_stack( self, data: List[numpy.ndarray], - z_position: Union[Iterable[float], float] = None, + z_offset: Union[Iterable[float], float] = None, thickness: Union[float, Iterable[float]] = 2.0, colors: Union[str, Iterable[str], numpy.ndarray, Iterable[numpy.ndarray]] = "w", alpha: float = 1.0, @@ -371,8 +371,8 @@ def add_line_stack( 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: Iterable of float or float, optional - | if ``float``, single position will be used for all lines + z_offset: Iterable of float or float, optional + | if ``float``, single offset will be used for all lines | if ``list`` of ``float``, each value will apply to the individual lines thickness: float or Iterable of float, default 2.0 @@ -423,7 +423,7 @@ def add_line_stack( return self._create_graphic( LineStack, data, - z_position, + z_offset, thickness, colors, alpha, diff --git a/fastplotlib/layouts/_plot_area.py b/fastplotlib/layouts/_plot_area.py index bbc5b0e15..6ff07a748 100644 --- a/fastplotlib/layouts/_plot_area.py +++ b/fastplotlib/layouts/_plot_area.py @@ -466,7 +466,10 @@ 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) + if self.camera.fov == 0: + # for orthographic positions stack objects along the z-axis + # for perspective projections we assume the user wants full 3D control + graphic.position_z = len(self) def insert_graphic( self, @@ -505,10 +508,13 @@ def insert_graphic( graphic=graphic, center=center, action="insert", index=index ) - if z_position is None: - graphic.position_z = index - else: - graphic.position_z = z_position + if self.camera.fov == 0: + # for orthographic positions stack objects along the z-axis + # for perspective projections we assume the user wants full 3D control + if z_position is None: + graphic.position_z = index + else: + graphic.position_z = z_position def _add_or_insert_graphic( self, From b79610cd6dd8508f87e6ff0414236087fbd11c58 Mon Sep 17 00:00:00 2001 From: Kushal Kolar Date: Wed, 1 May 2024 16:12:34 -0400 Subject: [PATCH 54/66] simple scatter animation example in quickstart (#500) --- examples/notebooks/quickstart.ipynb | 47 ++++++++++++++++++++++++++++- 1 file changed, 46 insertions(+), 1 deletion(-) diff --git a/examples/notebooks/quickstart.ipynb b/examples/notebooks/quickstart.ipynb index c40e5c9ff..84886ecae 100644 --- a/examples/notebooks/quickstart.ipynb +++ b/examples/notebooks/quickstart.ipynb @@ -1361,6 +1361,16 @@ "fig_em[0, 0].add_animations(tick)" ] }, + { + "cell_type": "code", + "execution_count": null, + "id": "774dba2e-f4c1-4c97-a427-c6f447139342", + "metadata": {}, + "outputs": [], + "source": [ + "fig_em.close()" + ] + }, { "cell_type": "markdown", "id": "a202b3d0-2a0b-450a-93d4-76d0a1129d1d", @@ -1410,7 +1420,7 @@ "fig_scatter = fpl.Figure()\n", "subplot_scatter = fig_scatter[0, 0]\n", "# use an alpha value since this will be a lot of points\n", - "scatter_graphic = subplot_scatter.add_scatter(data=cloud, sizes=3, colors=colors, alpha=0.7)\n", + "scatter_graphic = subplot_scatter.add_scatter(data=cloud, sizes=3, colors=colors, alpha=0.6)\n", "\n", "fig_scatter.show()" ] @@ -1434,6 +1444,17 @@ "scatter_graphic.colors[:n_points:2] = \"r\"" ] }, + { + "cell_type": "code", + "execution_count": null, + "id": "a5962263-8032-40ad-9981-fa0a649e2643", + "metadata": {}, + "outputs": [], + "source": [ + "# other half of the first cloud's points to purple\n", + "scatter_graphic.colors[1:n_points:2] = \"purple\"" + ] + }, { "cell_type": "code", "execution_count": null, @@ -1497,6 +1518,30 @@ "subplot_scatter.controller = \"fly\"" ] }, + { + "cell_type": "markdown", + "id": "43ae13f1-d59b-4673-b0b3-669542b4c127", + "metadata": {}, + "source": [ + "## Animation\n", + "\n", + "Move the cloud by a small delta on every render cycle" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "50d2e96f-718c-4925-9e81-a92e81134741", + "metadata": {}, + "outputs": [], + "source": [ + "def update_points(subplot):\n", + " deltas = np.random.normal(size=scatter_graphic.data().shape, loc=0, scale=0.15)\n", + " scatter_graphic.data = scatter_graphic.data() + deltas\n", + "\n", + "subplot_scatter.add_animations(update_points)" + ] + }, { "cell_type": "code", "execution_count": null, From 9a309ef1359244eb3791ddcfebc8b08b2697ae9f Mon Sep 17 00:00:00 2001 From: Kushal Kolar Date: Wed, 1 May 2024 19:30:17 -0400 Subject: [PATCH 55/66] cycle colors in scatter example (#501) --- examples/notebooks/quickstart.ipynb | 34 ++++++++++++++++++++++++++--- 1 file changed, 31 insertions(+), 3 deletions(-) diff --git a/examples/notebooks/quickstart.ipynb b/examples/notebooks/quickstart.ipynb index 84886ecae..9bfd822ab 100644 --- a/examples/notebooks/quickstart.ipynb +++ b/examples/notebooks/quickstart.ipynb @@ -1422,7 +1422,7 @@ "# use an alpha value since this will be a lot of points\n", "scatter_graphic = subplot_scatter.add_scatter(data=cloud, sizes=3, colors=colors, alpha=0.6)\n", "\n", - "fig_scatter.show()" + "fig_scatter.show(sidecar=True)" ] }, { @@ -1536,12 +1536,40 @@ "outputs": [], "source": [ "def update_points(subplot):\n", + " # move every point by a small amount\n", " deltas = np.random.normal(size=scatter_graphic.data().shape, loc=0, scale=0.15)\n", - " scatter_graphic.data = scatter_graphic.data() + deltas\n", + " scatter_graphic.data = scatter_graphic.data() + deltas \n", "\n", "subplot_scatter.add_animations(update_points)" ] }, + { + "cell_type": "markdown", + "id": "1592c6cd-d10a-4bda-ac4b-e06d428ffa1d", + "metadata": {}, + "source": [ + "Another animation function to cycle the colors of one of the clouds" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "fb0394a3-47d9-4620-a754-d04d5f313cc7", + "metadata": {}, + "outputs": [], + "source": [ + "i = 0.05\n", + "def cycle_colors(subplot):\n", + " global i\n", + " # cycle the red values\n", + " scatter_graphic.colors[n_points * 2:, 0] = np.abs(np.sin(i))\n", + " scatter_graphic.colors[n_points * 2:, 1] = np.abs(np.sin(i + (np.pi / 4)))\n", + " scatter_graphic.colors[n_points * 2:, 2] = np.abs(np.cos(i))\n", + " i += 0.05\n", + "\n", + "subplot_scatter.add_animations(cycle_colors)" + ] + }, { "cell_type": "code", "execution_count": null, @@ -2020,7 +2048,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.11.3" + "version": "3.11.2" } }, "nbformat": 4, From 815e8b3ff6aed9fe17104a8c30677bd111958c70 Mon Sep 17 00:00:00 2001 From: Kushal Kolar Date: Sun, 16 Jun 2024 18:14:59 -0400 Subject: [PATCH 56/66] Graphic features refactor (#511) * just a start * pushing to continue on my desktop * progress on buffer manager cleanup_key * more progress, still lots to do * slicing working with PointsDataFeature, negative slices too, still major WIP * comitting stuff * _update_range() is pretty good now * comment * if statement * simply colors setting * type annotation * simpler slice parsing * remove cleanup_key :D :D :D git status! * all fancy and negative indexing working :D * start tests * exception message * more on tests * refactor sizes, not tested yet * start parameterizing buffer tests * better buffer tests * more variants * add array fancy indexing to same parameterization * parameterize tuple tests * remove repr * test offset and size * test offset and size * test create colors * also test with direct truth indices in colors * points tests, works * remove imports * sizes test working, other cleanup * export sizes feature again * ideas for sharing and unsharing buffers between graphics * ideas for sharing and unsharing buffers between graphics, nto tested * typing * attach and detach buffers to a graphic, not tested * import * more int point tests * Graphic.add_event_handler * adding and removing data feature event works and tested * common features, WIP * regular features and refactor line and scatter into positions graphic * uniform sizes * implement sizes and uniform size for scatter * VertexCmap feature, not yet tested * start image features, not tested, add thickness, not tested * better cmap parsing * cleanup * image features * cleanup * start selection feature refactor * more on selection features * offset and rotation for base graphic * feature event table * position feature event tables * rotation and offset features * work on selectors, WIP, linear region selector inits properly and moves for x-axis * proper centering * much simpliified and better linear region selector * linear region selector works well on x axis with events and data selection * vertex cmap fix, delete synchronizer * linear selector works * cleanup * update graphic methods mixin * update selector example nbs, still WIP * type annotation in setter * add notes to tests comments * refactor image stuff * image selector tool * return selectors as proxies * image stuff works * fix offsets adding graphics, fix positions_graphic cmap bug, quickstart runs :D * fix add_graphic args and mixin * simpler graphic collection stuff * more line collection * remove old events system * fix some examples * remove lingering older interaction stuff * cleanup * fill color arg * black * update examples * fix image tiling, better heatmap example * update hm examples * fix hlut * refactor graphics base * update line examples * bug fixes * update text to use new gfeatures * cleanup * implement iterator for TextureArray, much simpler now :D * basic texture tests * bugfix, cleanup * image graphic tests * type annot * test stuff * tests for common and visible kwarg for Graphic * bugfix * test remove event hanlders common fea * rename * start test positions graphics * test progress * black * add __len__ to buffer managers, add __array_interface__ raises error * TextureArray has len() * docstring * updates and tests * black * uniform colors tests and bug fix * black * bugfix uniform color and sizes * sizes and thickness tests * tests update * lotta team work * docstring, small things * rename UniformSizes -> UniformSize * update graphic methods mixin * update graphic methods mixin * bugfix * test data slice for positiosn graphics * test colors property within buffer tests * cleanup * emit user key not parsed key * rename cmap_values to transform * cmap_values -> cmap_transform in mixin * test graphics in vertex data buffer manager tests * cmap transform tests * make Graphic._features private, add Graphic.events property * color events tests * move data slice test to just buffer tests * data events tests * cleanup * cleanup, remove old histogram graphic * text changes and tests * texture array tests with graphic * image graphic tests * update image features * black * append data and world xy for graphic pointer events * black * move constructor to top * example tests for wide and square hm * update ci * fix message * update examples * bugfix, docstrings * docstrings, exception messages * update api docs * apparently we use __all__ in graphics to generate api docs * remove nbsphinx * bump version * docstring * finish up line collection * fix adding line selector to line collection * finish line collection * update line collection examples * graphic collections are now iterables, add examples for setting properties, add names and metadatas args, separate kwargs for collection and individual lines * 3d line stack example with animation * fix line collection cmap with additional args * black * update kwargs for line collection because of mixin * add numpy.integer check for buffermanager parse slice * rename * docstrings * line linear selector init logic * fix line collection init selectors * fix selector * add plot area hook for collections * docstring * update selector nbs * update example nb * update nb * cleanup * update nb * update nbs * feature is private class attr * fix * remove anim example from screenshot tests * update screenshots * update screenshot * update CONTRIBUTING * smaller hm data test for CI * exclude heatmap change data from tests, too large RAM usage probably * black * change dtype to save ram usage for CI * remove large square heatmap from screenshot tests * black * disable all but one hm test * fix gc * update screenshots * replace one more test image * fix docs * update image screenshots * fix docs * docstrings, Graphic.events -> Graphic.supported_events * update docs --------- Co-authored-by: Caitlin --- .github/workflows/black.yml | 16 +- .github/workflows/ci.yml | 1 + CONTRIBUTING.md | 43 +- docs/source/api/gpu.rst | 7 +- .../api/graphic_features/CmapFeature.rst | 36 - .../api/graphic_features/ColorFeature.rst | 34 - docs/source/api/graphic_features/Deleted.rst | 2 + .../api/graphic_features/FeatureEvent.rst | 29 - docs/source/api/graphic_features/FontSize.rst | 35 + .../api/graphic_features/GraphicFeature.rst | 33 - .../GraphicFeatureIndexable.rst | 34 - .../graphic_features/HeatmapCmapFeature.rst | 37 - .../graphic_features/HeatmapDataFeature.rst | 35 - .../source/api/graphic_features/ImageCmap.rst | 35 + .../api/graphic_features/ImageCmapFeature.rst | 37 - .../ImageCmapInterpolation.rst | 35 + .../api/graphic_features/ImageDataFeature.rst | 35 - .../graphic_features/ImageInterpolation.rst | 35 + .../source/api/graphic_features/ImageVmax.rst | 35 + .../source/api/graphic_features/ImageVmin.rst | 35 + .../LinearRegionSelectionFeature.rst | 2 + .../LinearSelectionFeature.rst | 2 + docs/source/api/graphic_features/Name.rst | 35 + docs/source/api/graphic_features/Offset.rst | 35 + .../graphic_features/PointsDataFeature.rst | 34 - .../graphic_features/PointsSizesFeature.rst | 3 + .../api/graphic_features/PresentFeature.rst | 33 - docs/source/api/graphic_features/Rotation.rst | 35 + docs/source/api/graphic_features/TextData.rst | 35 + .../api/graphic_features/TextFaceColor.rst | 35 + .../api/graphic_features/TextOutlineColor.rst | 35 + .../graphic_features/TextOutlineThickness.rst | 35 + .../api/graphic_features/TextureArray.rst | 39 + .../source/api/graphic_features/Thickness.rst | 35 + .../api/graphic_features/ThicknessFeature.rst | 33 - .../api/graphic_features/UniformColor.rst | 35 + .../api/graphic_features/UniformSize.rst | 35 + .../api/graphic_features/VertexCmap.rst | 40 + .../api/graphic_features/VertexColors.rst | 37 + .../api/graphic_features/VertexPositions.rst | 37 + docs/source/api/graphic_features/Visible.rst | 35 + docs/source/api/graphic_features/index.rst | 34 +- .../to_gpu_supported_dtype.rst | 29 - docs/source/api/graphics/HeatmapGraphic.rst | 44 - docs/source/api/graphics/ImageGraphic.rst | 25 +- docs/source/api/graphics/LineCollection.rst | 27 +- docs/source/api/graphics/LineGraphic.rst | 22 +- docs/source/api/graphics/LineStack.rst | 27 +- docs/source/api/graphics/ScatterGraphic.rst | 19 +- docs/source/api/graphics/TextGraphic.rst | 19 +- docs/source/api/graphics/index.rst | 5 +- docs/source/api/layouts/subplot.rst | 1 - .../api/selectors/LinearRegionSelector.rst | 18 +- docs/source/api/selectors/LinearSelector.rst | 18 +- docs/source/api/selectors/PolygonSelector.rst | 43 - docs/source/api/selectors/Synchronizer.rst | 33 - docs/source/api/selectors/index.rst | 2 - docs/source/conf.py | 3 +- docs/source/generate_api.py | 6 + docs/source/index.rst | 6 - docs/source/quickstart.ipynb | 1673 ----------------- examples/desktop/heatmap/heatmap.py | 12 +- examples/desktop/heatmap/heatmap_cmap.py | 16 +- examples/desktop/heatmap/heatmap_data.py | 20 +- examples/desktop/heatmap/heatmap_square.py | 33 + examples/desktop/heatmap/heatmap_vmin_vmax.py | 18 +- examples/desktop/heatmap/heatmap_wide.py | 32 + examples/desktop/image/image_rgbvminvmax.py | 4 +- examples/desktop/image/image_vminvmax.py | 4 +- examples/desktop/line/line_cmap.py | 8 +- examples/desktop/line/line_colorslice.py | 55 +- examples/desktop/line/line_dataslice.py | 6 +- examples/desktop/line/line_present_scaling.py | 49 - .../line_collection/line_collection.py | 4 +- .../line_collection_cmap_values.py | 11 +- ...line_collection_cmap_values_qualitative.py | 8 +- .../line_collection/line_collection_colors.py | 6 +- .../line_collection_slicing.py | 68 + .../desktop/line_collection/line_stack.py | 22 +- .../desktop/line_collection/line_stack_3d.py | 103 + examples/desktop/scatter/scatter_cmap.py | 6 +- .../desktop/scatter/scatter_colorslice.py | 7 +- examples/desktop/scatter/scatter_present.py | 38 - examples/desktop/screenshots/gridplot.png | 4 +- .../screenshots/gridplot_non_square.png | 4 +- examples/desktop/screenshots/heatmap.png | 4 +- examples/desktop/screenshots/heatmap_cmap.png | 4 +- examples/desktop/screenshots/heatmap_data.png | 4 +- .../desktop/screenshots/heatmap_square.png | 3 + .../desktop/screenshots/heatmap_vmin_vmax.png | 4 +- examples/desktop/screenshots/heatmap_wide.png | 3 + examples/desktop/screenshots/image_cmap.png | 4 +- examples/desktop/screenshots/image_rgb.png | 4 +- .../desktop/screenshots/image_rgbvminvmax.png | 4 +- examples/desktop/screenshots/image_simple.png | 4 +- .../desktop/screenshots/image_vminvmax.png | 4 +- .../screenshots/line_collection_slicing.png | 3 + .../desktop/screenshots/line_colorslice.png | 4 +- .../desktop/screenshots/line_dataslice.png | 4 +- .../screenshots/line_present_scaling.png | 3 - examples/desktop/screenshots/line_stack.png | 4 +- examples/desktop/screenshots/scatter_cmap.png | 4 +- .../desktop/screenshots/scatter_present.png | 3 - examples/notebooks/heatmap.ipynb | 14 +- .../notebooks/linear_region_selector.ipynb | 118 +- examples/notebooks/linear_selector.ipynb | 30 +- examples/notebooks/lines_cmap.ipynb | 30 +- examples/notebooks/quickstart.ipynb | 102 +- .../notebooks/scatter_sizes_animation.ipynb | 22 +- .../notebooks/screenshots/nb-astronaut.png | 4 +- .../screenshots/nb-astronaut_RGB.png | 4 +- examples/notebooks/screenshots/nb-camera.png | 4 +- .../nb-image-widget-movie-set_data.png | 4 +- .../nb-image-widget-single-gnuplot2.png | 4 +- .../screenshots/nb-image-widget-single.png | 4 +- ...et-zfish-frame-50-frame-apply-gaussian.png | 4 +- ...idget-zfish-frame-50-frame-apply-reset.png | 4 +- ...ge-widget-zfish-frame-50-max-window-13.png | 4 +- ...e-widget-zfish-frame-50-mean-window-13.png | 4 +- ...ge-widget-zfish-frame-50-mean-window-5.png | 4 +- .../nb-image-widget-zfish-frame-50.png | 4 +- .../nb-image-widget-zfish-frame-99.png | 4 +- ...ish-grid-frame-50-frame-apply-gaussian.png | 4 +- ...-zfish-grid-frame-50-frame-apply-reset.png | 4 +- ...dget-zfish-grid-frame-50-max-window-13.png | 4 +- ...get-zfish-grid-frame-50-mean-window-13.png | 4 +- ...dget-zfish-grid-frame-50-mean-window-5.png | 4 +- .../nb-image-widget-zfish-grid-frame-50.png | 4 +- .../nb-image-widget-zfish-grid-frame-99.png | 4 +- ...e-widget-zfish-grid-init-mean-window-5.png | 4 +- ...fish-grid-set_data-reset-indices-false.png | 4 +- ...zfish-grid-set_data-reset-indices-true.png | 4 +- ...-image-widget-zfish-init-mean-window-5.png | 4 +- ...dget-zfish-mixed-rgb-cockatoo-frame-50.png | 4 +- ...dget-zfish-mixed-rgb-cockatoo-set-data.png | 4 +- ...get-zfish-mixed-rgb-cockatoo-windowrgb.png | 4 +- .../screenshots/nb-lines-underlay.png | 4 +- examples/notebooks/subplots.ipynb | 54 +- examples/notebooks/subplots_simple.ipynb | 41 +- examples/notebooks/test_gc.ipynb | 24 +- fastplotlib/VERSION | 2 +- fastplotlib/graphics/__init__.py | 8 +- fastplotlib/graphics/_base.py | 782 +++----- fastplotlib/graphics/_collection_base.py | 388 ++++ fastplotlib/graphics/_features/__init__.py | 71 +- fastplotlib/graphics/_features/_base.py | 430 ++--- fastplotlib/graphics/_features/_colors.py | 434 ----- fastplotlib/graphics/_features/_common.py | 123 ++ fastplotlib/graphics/_features/_data.py | 219 --- fastplotlib/graphics/_features/_deleted.py | 41 - fastplotlib/graphics/_features/_image.py | 262 +++ .../graphics/_features/_positions_graphics.py | 458 +++++ fastplotlib/graphics/_features/_present.py | 72 - .../graphics/_features/_selection_features.py | 244 ++- fastplotlib/graphics/_features/_sizes.py | 120 -- fastplotlib/graphics/_features/_text.py | 92 + fastplotlib/graphics/_features/_thickness.py | 46 - fastplotlib/graphics/_features/utils.py | 87 + fastplotlib/graphics/_positions_base.py | 185 ++ fastplotlib/graphics/histogram.py | 114 -- fastplotlib/graphics/image.py | 628 +++---- fastplotlib/graphics/line.py | 308 ++- fastplotlib/graphics/line_collection.py | 561 +++--- fastplotlib/graphics/scatter.py | 127 +- fastplotlib/graphics/selectors/__init__.py | 8 +- .../graphics/selectors/_base_selector.py | 23 +- fastplotlib/graphics/selectors/_linear.py | 129 +- .../graphics/selectors/_linear_region.py | 394 ++-- fastplotlib/graphics/selectors/_sync.py | 90 - fastplotlib/graphics/text.py | 146 +- fastplotlib/layouts/_figure.py | 1 - fastplotlib/layouts/_graphic_methods_mixin.py | 386 ++-- fastplotlib/layouts/_plot_area.py | 15 +- fastplotlib/layouts/_subplot.py | 2 +- fastplotlib/utils/functions.py | 43 +- fastplotlib/utils/gui.py | 1 - fastplotlib/widgets/histogram_lut.py | 83 +- fastplotlib/widgets/image.py | 5 +- scripts/generate_add_graphic_methods.py | 2 +- setup.py | 1 - tests/__init__.py | 0 tests/events.py | 91 + tests/test_colors_buffer_manager.py | 250 +++ tests/test_common_features.py | 282 +++ tests/test_figure.py | 96 +- tests/test_image_graphic.py | 205 ++ tests/test_positions_data_buffer_manager.py | 208 ++ tests/test_positions_graphics.py | 446 +++++ tests/test_sizes_buffer_manager.py | 76 + tests/test_text_graphic.py | 101 + tests/test_texture_array.py | 230 +++ tests/utils.py | 185 ++ 192 files changed, 7204 insertions(+), 6537 deletions(-) delete mode 100644 docs/source/api/graphic_features/CmapFeature.rst delete mode 100644 docs/source/api/graphic_features/ColorFeature.rst delete mode 100644 docs/source/api/graphic_features/FeatureEvent.rst create mode 100644 docs/source/api/graphic_features/FontSize.rst delete mode 100644 docs/source/api/graphic_features/GraphicFeature.rst delete mode 100644 docs/source/api/graphic_features/GraphicFeatureIndexable.rst delete mode 100644 docs/source/api/graphic_features/HeatmapCmapFeature.rst delete mode 100644 docs/source/api/graphic_features/HeatmapDataFeature.rst create mode 100644 docs/source/api/graphic_features/ImageCmap.rst delete mode 100644 docs/source/api/graphic_features/ImageCmapFeature.rst create mode 100644 docs/source/api/graphic_features/ImageCmapInterpolation.rst delete mode 100644 docs/source/api/graphic_features/ImageDataFeature.rst create mode 100644 docs/source/api/graphic_features/ImageInterpolation.rst create mode 100644 docs/source/api/graphic_features/ImageVmax.rst create mode 100644 docs/source/api/graphic_features/ImageVmin.rst create mode 100644 docs/source/api/graphic_features/Name.rst create mode 100644 docs/source/api/graphic_features/Offset.rst delete mode 100644 docs/source/api/graphic_features/PointsDataFeature.rst delete mode 100644 docs/source/api/graphic_features/PresentFeature.rst create mode 100644 docs/source/api/graphic_features/Rotation.rst create mode 100644 docs/source/api/graphic_features/TextData.rst create mode 100644 docs/source/api/graphic_features/TextFaceColor.rst create mode 100644 docs/source/api/graphic_features/TextOutlineColor.rst create mode 100644 docs/source/api/graphic_features/TextOutlineThickness.rst create mode 100644 docs/source/api/graphic_features/TextureArray.rst create mode 100644 docs/source/api/graphic_features/Thickness.rst delete mode 100644 docs/source/api/graphic_features/ThicknessFeature.rst create mode 100644 docs/source/api/graphic_features/UniformColor.rst create mode 100644 docs/source/api/graphic_features/UniformSize.rst create mode 100644 docs/source/api/graphic_features/VertexCmap.rst create mode 100644 docs/source/api/graphic_features/VertexColors.rst create mode 100644 docs/source/api/graphic_features/VertexPositions.rst create mode 100644 docs/source/api/graphic_features/Visible.rst delete mode 100644 docs/source/api/graphic_features/to_gpu_supported_dtype.rst delete mode 100644 docs/source/api/graphics/HeatmapGraphic.rst delete mode 100644 docs/source/api/selectors/PolygonSelector.rst delete mode 100644 docs/source/api/selectors/Synchronizer.rst delete mode 100644 docs/source/quickstart.ipynb create mode 100644 examples/desktop/heatmap/heatmap_square.py create mode 100644 examples/desktop/heatmap/heatmap_wide.py delete mode 100644 examples/desktop/line/line_present_scaling.py create mode 100644 examples/desktop/line_collection/line_collection_slicing.py create mode 100644 examples/desktop/line_collection/line_stack_3d.py delete mode 100644 examples/desktop/scatter/scatter_present.py create mode 100644 examples/desktop/screenshots/heatmap_square.png create mode 100644 examples/desktop/screenshots/heatmap_wide.png create mode 100644 examples/desktop/screenshots/line_collection_slicing.png delete mode 100644 examples/desktop/screenshots/line_present_scaling.png delete mode 100644 examples/desktop/screenshots/scatter_present.png create mode 100644 fastplotlib/graphics/_collection_base.py delete mode 100644 fastplotlib/graphics/_features/_colors.py create mode 100644 fastplotlib/graphics/_features/_common.py delete mode 100644 fastplotlib/graphics/_features/_data.py delete mode 100644 fastplotlib/graphics/_features/_deleted.py create mode 100644 fastplotlib/graphics/_features/_image.py create mode 100644 fastplotlib/graphics/_features/_positions_graphics.py delete mode 100644 fastplotlib/graphics/_features/_present.py delete mode 100644 fastplotlib/graphics/_features/_sizes.py create mode 100644 fastplotlib/graphics/_features/_text.py delete mode 100644 fastplotlib/graphics/_features/_thickness.py create mode 100644 fastplotlib/graphics/_features/utils.py create mode 100644 fastplotlib/graphics/_positions_base.py delete mode 100644 fastplotlib/graphics/histogram.py delete mode 100644 fastplotlib/graphics/selectors/_sync.py create mode 100644 tests/__init__.py create mode 100644 tests/events.py create mode 100644 tests/test_colors_buffer_manager.py create mode 100644 tests/test_common_features.py create mode 100644 tests/test_image_graphic.py create mode 100644 tests/test_positions_data_buffer_manager.py create mode 100644 tests/test_positions_graphics.py create mode 100644 tests/test_sizes_buffer_manager.py create mode 100644 tests/test_text_graphic.py create mode 100644 tests/test_texture_array.py create mode 100644 tests/utils.py diff --git a/.github/workflows/black.yml b/.github/workflows/black.yml index bcb2d2b33..bec47fdc5 100644 --- a/.github/workflows/black.yml +++ b/.github/workflows/black.yml @@ -1,12 +1,24 @@ name: Lint -on: [push, pull_request] +on: + push: + branches: + - main + pull_request: + branches: + - main + types: + - opened + - reopened + - synchronize + - ready_for_review jobs: lint: runs-on: ubuntu-latest + if: ${{ !github.event.pull_request.draft }} steps: - uses: actions/checkout@v4 - uses: psf/black@stable with: - src: "./fastplotlib" \ No newline at end of file + src: "./fastplotlib" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7db694d01..9adb67f77 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,6 +17,7 @@ jobs: docs-build: name: Docs runs-on: bigmem + if: ${{ !github.event.pull_request.draft }} strategy: fail-fast: false steps: diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index fe9c90242..0786596b4 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -77,35 +77,37 @@ keeps a *private* global dictionary of all `WorldObject` instances and users are This is due to garbage collection. This may be quite complicated for beginners, for more details see this PR: https://github.com/fastplotlib/fastplotlib/pull/160 . If you are curious or have more questions on garbage collection in fastplotlib you're welcome to post an issue :D. -#### Graphic Features +#### Graphic properties -There is one important thing that `fastplotlib` uses which we call "graphic features". +Graphic properties are all evented, and internally we called these "graphic features". They are the various +aspects of a graphic that the user can change. The "graphic features" subpackage can be found at `fastplotlib/graphics/_features`. As we can see this -is a private subpackage and never meant to be accessible to users. In `fastplotlib` "graphic features" are the various -aspects of a graphic that the user can change. Users can also run callbacks whenever a graphic feature changes. +is a private subpackage and never meant to be accessible to users.. ##### LineGraphic For example let's look at `LineGraphic` in `fastplotlib/graphics/line.py`. Every graphic has a class variable called -`feature_events` which is a set of all graphic features. It has the following graphic features: "data", "colors", "cmap", "thickness", "present". +`_features` which is a set of all graphic properties that are evented. It has the following evented properties: +`"data", "colors", "cmap", "thickness"` in addition to properties common to all graphics, such as `"name", "offset", "rotation", and "visible"` -Now look at the constructor for `LineGraphic`, it first creates an instance of `PointsDataFeature`. This is basically a -class that wraps the positions buffer, the vertex positions that define the line, and provides additional useful functionality. -For example, every time that the `data` is changed event handlers will be called (if any event handlers are registered). +Now look at the constructor for the `LineGraphic` base class `PositionsGraphic`, it first creates an instance of `VertexPositions`. +This is a class that manages vertex positions buffer. It defines the line, and provides additional useful functionality. +For example, every time that the `data` is changed, the new data will be marked for upload to the GPU before the next draw. +In addition, event handlers will be called if any event handlers are registered. -`ColorFeature`behaves similarly, but it can perform additional parsing that can create the colors buffer from different forms of user input. For example if a user runs: -`line_graphic.colors = "blue"`, then `ColorFeature.__setitem__()` will create a buffer that corresponds to what `pygfx.Color` thinks is "blue". -Users can also take advantage of fancy indexing, ex: `line_graphics.colors[bool_array] = "red"` :smile: +`VertexColors`behaves similarly, but it can perform additional parsing that can create the colors buffer from different +forms of user input. For example if a user runs: `line_graphic.colors = "blue"`, then `VertexColors.__setitem__()` will +create a buffer that corresponds to what `pygfx.Color` thinks is "blue". Users can also take advantage of fancy indexing, +ex: `line_graphics.colors[bool_array] = "red"` :smile: -`LineGraphic` also has a `CmapFeature`, this is a subclass of `ColorFeature` which can parse colormaps, for example: +`LineGraphic` also has a `VertexCmap`, this manages the line `VertexColors` instance to parse colormaps, for example: `line_graphic.cmap = "jet"` or even `line_graphic.cmap[50:] = "viridis"`. -`LineGraphic` also has `ThicknessFeature` which is pretty simple, `PresentFeature` which indicates if a graphic is -currently in the scene, and `DeletedFeature` which is useful if you need callbacks to indicate that the graphic has been -deleted (for example, removing references to a graphic from a legend). +`LineGraphic` also has a `thickness` property which is pretty simple, and `DeletedFeature` which is useful if you need +callbacks to indicate that the graphic has been deleted (for example, removing references to a graphic from a legend). -Other graphics have graphic features that are relevant to them, for example `ImageGraphic` has a `cmap` feature which is -unique to images or heatmaps. +Other graphics have properties that are relevant to them, for example `ImageGraphic` has `cmap`, `vmin`, `vmax`, +properties unique to images. #### Selectors @@ -192,9 +194,10 @@ the subplots. All subplots within a `Figure` share the same canvas and use diffe ## Tests in detail -The CI pipeline for a plotting library that is supposed to produce things that "look visually correct". Each example -within the `examples` dir is run and an image of the canvas is taken and compared with a ground-truth -screenshot that we have manually inspected. Ground-truth image are stored using `git-lfs`. +Backend tests are in `tests/`, in addition as a plotting library CI pipeline produces things that +"look visually correct". Each example within the `examples` dir is run and an image of the canvas +is taken and compared with a ground-truth screenshot that we have manually inspected. +Ground-truth image are stored using `git-lfs`. The ground-truth images are in: diff --git a/docs/source/api/gpu.rst b/docs/source/api/gpu.rst index 62ffd5797..6f94aff23 100644 --- a/docs/source/api/gpu.rst +++ b/docs/source/api/gpu.rst @@ -1,5 +1,6 @@ -fastplotlib.utils -***************** +fastplotlib.utils.gpu +********************* -.. automodule:: fastplotlib.utils.gpu +.. currentmodule:: fastplotlib.utils.gpu +.. automodule:: fastplotlib :members: diff --git a/docs/source/api/graphic_features/CmapFeature.rst b/docs/source/api/graphic_features/CmapFeature.rst deleted file mode 100644 index 7cc2f681f..000000000 --- a/docs/source/api/graphic_features/CmapFeature.rst +++ /dev/null @@ -1,36 +0,0 @@ -.. _api.CmapFeature: - -CmapFeature -*********** - -=========== -CmapFeature -=========== -.. currentmodule:: fastplotlib.graphics._features - -Constructor -~~~~~~~~~~~ -.. autosummary:: - :toctree: CmapFeature_api - - CmapFeature - -Properties -~~~~~~~~~~ -.. autosummary:: - :toctree: CmapFeature_api - - CmapFeature.buffer - CmapFeature.name - 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 deleted file mode 100644 index 3ed84cd70..000000000 --- a/docs/source/api/graphic_features/ColorFeature.rst +++ /dev/null @@ -1,34 +0,0 @@ -.. _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/Deleted.rst b/docs/source/api/graphic_features/Deleted.rst index 998e94588..09131c4a7 100644 --- a/docs/source/api/graphic_features/Deleted.rst +++ b/docs/source/api/graphic_features/Deleted.rst @@ -20,6 +20,7 @@ Properties .. autosummary:: :toctree: Deleted_api + Deleted.value Methods ~~~~~~~ @@ -30,4 +31,5 @@ Methods Deleted.block_events Deleted.clear_event_handlers Deleted.remove_event_handler + Deleted.set_value diff --git a/docs/source/api/graphic_features/FeatureEvent.rst b/docs/source/api/graphic_features/FeatureEvent.rst deleted file mode 100644 index f22ee3ef4..000000000 --- a/docs/source/api/graphic_features/FeatureEvent.rst +++ /dev/null @@ -1,29 +0,0 @@ -.. _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/FontSize.rst b/docs/source/api/graphic_features/FontSize.rst new file mode 100644 index 000000000..4b8df9826 --- /dev/null +++ b/docs/source/api/graphic_features/FontSize.rst @@ -0,0 +1,35 @@ +.. _api.FontSize: + +FontSize +******** + +======== +FontSize +======== +.. currentmodule:: fastplotlib.graphics._features + +Constructor +~~~~~~~~~~~ +.. autosummary:: + :toctree: FontSize_api + + FontSize + +Properties +~~~~~~~~~~ +.. autosummary:: + :toctree: FontSize_api + + FontSize.value + +Methods +~~~~~~~ +.. autosummary:: + :toctree: FontSize_api + + FontSize.add_event_handler + FontSize.block_events + FontSize.clear_event_handlers + FontSize.remove_event_handler + FontSize.set_value + diff --git a/docs/source/api/graphic_features/GraphicFeature.rst b/docs/source/api/graphic_features/GraphicFeature.rst deleted file mode 100644 index 7abc3e6b2..000000000 --- a/docs/source/api/graphic_features/GraphicFeature.rst +++ /dev/null @@ -1,33 +0,0 @@ -.. _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 deleted file mode 100644 index 7bd1383bc..000000000 --- a/docs/source/api/graphic_features/GraphicFeatureIndexable.rst +++ /dev/null @@ -1,34 +0,0 @@ -.. _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 deleted file mode 100644 index bac43c9b9..000000000 --- a/docs/source/api/graphic_features/HeatmapCmapFeature.rst +++ /dev/null @@ -1,37 +0,0 @@ -.. _api.HeatmapCmapFeature: - -HeatmapCmapFeature -****************** - -================== -HeatmapCmapFeature -================== -.. currentmodule:: fastplotlib.graphics._features - -Constructor -~~~~~~~~~~~ -.. autosummary:: - :toctree: HeatmapCmapFeature_api - - HeatmapCmapFeature - -Properties -~~~~~~~~~~ -.. autosummary:: - :toctree: HeatmapCmapFeature_api - - HeatmapCmapFeature.name - HeatmapCmapFeature.vmax - HeatmapCmapFeature.vmin - -Methods -~~~~~~~ -.. autosummary:: - :toctree: HeatmapCmapFeature_api - - HeatmapCmapFeature.add_event_handler - HeatmapCmapFeature.block_events - HeatmapCmapFeature.clear_event_handlers - HeatmapCmapFeature.remove_event_handler - HeatmapCmapFeature.reset_vmin_vmax - diff --git a/docs/source/api/graphic_features/HeatmapDataFeature.rst b/docs/source/api/graphic_features/HeatmapDataFeature.rst deleted file mode 100644 index 029f0e199..000000000 --- a/docs/source/api/graphic_features/HeatmapDataFeature.rst +++ /dev/null @@ -1,35 +0,0 @@ -.. _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/ImageCmap.rst b/docs/source/api/graphic_features/ImageCmap.rst new file mode 100644 index 000000000..23d16a4a2 --- /dev/null +++ b/docs/source/api/graphic_features/ImageCmap.rst @@ -0,0 +1,35 @@ +.. _api.ImageCmap: + +ImageCmap +********* + +========= +ImageCmap +========= +.. currentmodule:: fastplotlib.graphics._features + +Constructor +~~~~~~~~~~~ +.. autosummary:: + :toctree: ImageCmap_api + + ImageCmap + +Properties +~~~~~~~~~~ +.. autosummary:: + :toctree: ImageCmap_api + + ImageCmap.value + +Methods +~~~~~~~ +.. autosummary:: + :toctree: ImageCmap_api + + ImageCmap.add_event_handler + ImageCmap.block_events + ImageCmap.clear_event_handlers + ImageCmap.remove_event_handler + ImageCmap.set_value + diff --git a/docs/source/api/graphic_features/ImageCmapFeature.rst b/docs/source/api/graphic_features/ImageCmapFeature.rst deleted file mode 100644 index ae65744c7..000000000 --- a/docs/source/api/graphic_features/ImageCmapFeature.rst +++ /dev/null @@ -1,37 +0,0 @@ -.. _api.ImageCmapFeature: - -ImageCmapFeature -**************** - -================ -ImageCmapFeature -================ -.. currentmodule:: fastplotlib.graphics._features - -Constructor -~~~~~~~~~~~ -.. autosummary:: - :toctree: ImageCmapFeature_api - - ImageCmapFeature - -Properties -~~~~~~~~~~ -.. autosummary:: - :toctree: ImageCmapFeature_api - - ImageCmapFeature.name - ImageCmapFeature.vmax - ImageCmapFeature.vmin - -Methods -~~~~~~~ -.. autosummary:: - :toctree: ImageCmapFeature_api - - ImageCmapFeature.add_event_handler - ImageCmapFeature.block_events - ImageCmapFeature.clear_event_handlers - ImageCmapFeature.remove_event_handler - ImageCmapFeature.reset_vmin_vmax - diff --git a/docs/source/api/graphic_features/ImageCmapInterpolation.rst b/docs/source/api/graphic_features/ImageCmapInterpolation.rst new file mode 100644 index 000000000..7e04ec788 --- /dev/null +++ b/docs/source/api/graphic_features/ImageCmapInterpolation.rst @@ -0,0 +1,35 @@ +.. _api.ImageCmapInterpolation: + +ImageCmapInterpolation +********************** + +====================== +ImageCmapInterpolation +====================== +.. currentmodule:: fastplotlib.graphics._features + +Constructor +~~~~~~~~~~~ +.. autosummary:: + :toctree: ImageCmapInterpolation_api + + ImageCmapInterpolation + +Properties +~~~~~~~~~~ +.. autosummary:: + :toctree: ImageCmapInterpolation_api + + ImageCmapInterpolation.value + +Methods +~~~~~~~ +.. autosummary:: + :toctree: ImageCmapInterpolation_api + + ImageCmapInterpolation.add_event_handler + ImageCmapInterpolation.block_events + ImageCmapInterpolation.clear_event_handlers + ImageCmapInterpolation.remove_event_handler + ImageCmapInterpolation.set_value + diff --git a/docs/source/api/graphic_features/ImageDataFeature.rst b/docs/source/api/graphic_features/ImageDataFeature.rst deleted file mode 100644 index 35fe74cf7..000000000 --- a/docs/source/api/graphic_features/ImageDataFeature.rst +++ /dev/null @@ -1,35 +0,0 @@ -.. _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/ImageInterpolation.rst b/docs/source/api/graphic_features/ImageInterpolation.rst new file mode 100644 index 000000000..866e76333 --- /dev/null +++ b/docs/source/api/graphic_features/ImageInterpolation.rst @@ -0,0 +1,35 @@ +.. _api.ImageInterpolation: + +ImageInterpolation +****************** + +================== +ImageInterpolation +================== +.. currentmodule:: fastplotlib.graphics._features + +Constructor +~~~~~~~~~~~ +.. autosummary:: + :toctree: ImageInterpolation_api + + ImageInterpolation + +Properties +~~~~~~~~~~ +.. autosummary:: + :toctree: ImageInterpolation_api + + ImageInterpolation.value + +Methods +~~~~~~~ +.. autosummary:: + :toctree: ImageInterpolation_api + + ImageInterpolation.add_event_handler + ImageInterpolation.block_events + ImageInterpolation.clear_event_handlers + ImageInterpolation.remove_event_handler + ImageInterpolation.set_value + diff --git a/docs/source/api/graphic_features/ImageVmax.rst b/docs/source/api/graphic_features/ImageVmax.rst new file mode 100644 index 000000000..b7dfe7e2d --- /dev/null +++ b/docs/source/api/graphic_features/ImageVmax.rst @@ -0,0 +1,35 @@ +.. _api.ImageVmax: + +ImageVmax +********* + +========= +ImageVmax +========= +.. currentmodule:: fastplotlib.graphics._features + +Constructor +~~~~~~~~~~~ +.. autosummary:: + :toctree: ImageVmax_api + + ImageVmax + +Properties +~~~~~~~~~~ +.. autosummary:: + :toctree: ImageVmax_api + + ImageVmax.value + +Methods +~~~~~~~ +.. autosummary:: + :toctree: ImageVmax_api + + ImageVmax.add_event_handler + ImageVmax.block_events + ImageVmax.clear_event_handlers + ImageVmax.remove_event_handler + ImageVmax.set_value + diff --git a/docs/source/api/graphic_features/ImageVmin.rst b/docs/source/api/graphic_features/ImageVmin.rst new file mode 100644 index 000000000..0d4634894 --- /dev/null +++ b/docs/source/api/graphic_features/ImageVmin.rst @@ -0,0 +1,35 @@ +.. _api.ImageVmin: + +ImageVmin +********* + +========= +ImageVmin +========= +.. currentmodule:: fastplotlib.graphics._features + +Constructor +~~~~~~~~~~~ +.. autosummary:: + :toctree: ImageVmin_api + + ImageVmin + +Properties +~~~~~~~~~~ +.. autosummary:: + :toctree: ImageVmin_api + + ImageVmin.value + +Methods +~~~~~~~ +.. autosummary:: + :toctree: ImageVmin_api + + ImageVmin.add_event_handler + ImageVmin.block_events + ImageVmin.clear_event_handlers + ImageVmin.remove_event_handler + ImageVmin.set_value + diff --git a/docs/source/api/graphic_features/LinearRegionSelectionFeature.rst b/docs/source/api/graphic_features/LinearRegionSelectionFeature.rst index a15825530..b8958c86b 100644 --- a/docs/source/api/graphic_features/LinearRegionSelectionFeature.rst +++ b/docs/source/api/graphic_features/LinearRegionSelectionFeature.rst @@ -21,6 +21,7 @@ Properties :toctree: LinearRegionSelectionFeature_api LinearRegionSelectionFeature.axis + LinearRegionSelectionFeature.value Methods ~~~~~~~ @@ -31,4 +32,5 @@ Methods LinearRegionSelectionFeature.block_events LinearRegionSelectionFeature.clear_event_handlers LinearRegionSelectionFeature.remove_event_handler + LinearRegionSelectionFeature.set_value diff --git a/docs/source/api/graphic_features/LinearSelectionFeature.rst b/docs/source/api/graphic_features/LinearSelectionFeature.rst index aeb1ca66b..ad7b8645a 100644 --- a/docs/source/api/graphic_features/LinearSelectionFeature.rst +++ b/docs/source/api/graphic_features/LinearSelectionFeature.rst @@ -20,6 +20,7 @@ Properties .. autosummary:: :toctree: LinearSelectionFeature_api + LinearSelectionFeature.value Methods ~~~~~~~ @@ -30,4 +31,5 @@ Methods LinearSelectionFeature.block_events LinearSelectionFeature.clear_event_handlers LinearSelectionFeature.remove_event_handler + LinearSelectionFeature.set_value diff --git a/docs/source/api/graphic_features/Name.rst b/docs/source/api/graphic_features/Name.rst new file mode 100644 index 000000000..288fcfc22 --- /dev/null +++ b/docs/source/api/graphic_features/Name.rst @@ -0,0 +1,35 @@ +.. _api.Name: + +Name +**** + +==== +Name +==== +.. currentmodule:: fastplotlib.graphics._features + +Constructor +~~~~~~~~~~~ +.. autosummary:: + :toctree: Name_api + + Name + +Properties +~~~~~~~~~~ +.. autosummary:: + :toctree: Name_api + + Name.value + +Methods +~~~~~~~ +.. autosummary:: + :toctree: Name_api + + Name.add_event_handler + Name.block_events + Name.clear_event_handlers + Name.remove_event_handler + Name.set_value + diff --git a/docs/source/api/graphic_features/Offset.rst b/docs/source/api/graphic_features/Offset.rst new file mode 100644 index 000000000..683aaf763 --- /dev/null +++ b/docs/source/api/graphic_features/Offset.rst @@ -0,0 +1,35 @@ +.. _api.Offset: + +Offset +****** + +====== +Offset +====== +.. currentmodule:: fastplotlib.graphics._features + +Constructor +~~~~~~~~~~~ +.. autosummary:: + :toctree: Offset_api + + Offset + +Properties +~~~~~~~~~~ +.. autosummary:: + :toctree: Offset_api + + Offset.value + +Methods +~~~~~~~ +.. autosummary:: + :toctree: Offset_api + + Offset.add_event_handler + Offset.block_events + Offset.clear_event_handlers + Offset.remove_event_handler + Offset.set_value + diff --git a/docs/source/api/graphic_features/PointsDataFeature.rst b/docs/source/api/graphic_features/PointsDataFeature.rst deleted file mode 100644 index 078b1c535..000000000 --- a/docs/source/api/graphic_features/PointsDataFeature.rst +++ /dev/null @@ -1,34 +0,0 @@ -.. _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/PointsSizesFeature.rst b/docs/source/api/graphic_features/PointsSizesFeature.rst index 7915cb09d..3dcc4eeb2 100644 --- a/docs/source/api/graphic_features/PointsSizesFeature.rst +++ b/docs/source/api/graphic_features/PointsSizesFeature.rst @@ -21,6 +21,8 @@ Properties :toctree: PointsSizesFeature_api PointsSizesFeature.buffer + PointsSizesFeature.shared + PointsSizesFeature.value Methods ~~~~~~~ @@ -31,4 +33,5 @@ Methods PointsSizesFeature.block_events PointsSizesFeature.clear_event_handlers PointsSizesFeature.remove_event_handler + PointsSizesFeature.set_value diff --git a/docs/source/api/graphic_features/PresentFeature.rst b/docs/source/api/graphic_features/PresentFeature.rst deleted file mode 100644 index 1ddbf1ec4..000000000 --- a/docs/source/api/graphic_features/PresentFeature.rst +++ /dev/null @@ -1,33 +0,0 @@ -.. _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/Rotation.rst b/docs/source/api/graphic_features/Rotation.rst new file mode 100644 index 000000000..f8963b0fd --- /dev/null +++ b/docs/source/api/graphic_features/Rotation.rst @@ -0,0 +1,35 @@ +.. _api.Rotation: + +Rotation +******** + +======== +Rotation +======== +.. currentmodule:: fastplotlib.graphics._features + +Constructor +~~~~~~~~~~~ +.. autosummary:: + :toctree: Rotation_api + + Rotation + +Properties +~~~~~~~~~~ +.. autosummary:: + :toctree: Rotation_api + + Rotation.value + +Methods +~~~~~~~ +.. autosummary:: + :toctree: Rotation_api + + Rotation.add_event_handler + Rotation.block_events + Rotation.clear_event_handlers + Rotation.remove_event_handler + Rotation.set_value + diff --git a/docs/source/api/graphic_features/TextData.rst b/docs/source/api/graphic_features/TextData.rst new file mode 100644 index 000000000..1c27b6e48 --- /dev/null +++ b/docs/source/api/graphic_features/TextData.rst @@ -0,0 +1,35 @@ +.. _api.TextData: + +TextData +******** + +======== +TextData +======== +.. currentmodule:: fastplotlib.graphics._features + +Constructor +~~~~~~~~~~~ +.. autosummary:: + :toctree: TextData_api + + TextData + +Properties +~~~~~~~~~~ +.. autosummary:: + :toctree: TextData_api + + TextData.value + +Methods +~~~~~~~ +.. autosummary:: + :toctree: TextData_api + + TextData.add_event_handler + TextData.block_events + TextData.clear_event_handlers + TextData.remove_event_handler + TextData.set_value + diff --git a/docs/source/api/graphic_features/TextFaceColor.rst b/docs/source/api/graphic_features/TextFaceColor.rst new file mode 100644 index 000000000..5dae54192 --- /dev/null +++ b/docs/source/api/graphic_features/TextFaceColor.rst @@ -0,0 +1,35 @@ +.. _api.TextFaceColor: + +TextFaceColor +************* + +============= +TextFaceColor +============= +.. currentmodule:: fastplotlib.graphics._features + +Constructor +~~~~~~~~~~~ +.. autosummary:: + :toctree: TextFaceColor_api + + TextFaceColor + +Properties +~~~~~~~~~~ +.. autosummary:: + :toctree: TextFaceColor_api + + TextFaceColor.value + +Methods +~~~~~~~ +.. autosummary:: + :toctree: TextFaceColor_api + + TextFaceColor.add_event_handler + TextFaceColor.block_events + TextFaceColor.clear_event_handlers + TextFaceColor.remove_event_handler + TextFaceColor.set_value + diff --git a/docs/source/api/graphic_features/TextOutlineColor.rst b/docs/source/api/graphic_features/TextOutlineColor.rst new file mode 100644 index 000000000..f7831b0df --- /dev/null +++ b/docs/source/api/graphic_features/TextOutlineColor.rst @@ -0,0 +1,35 @@ +.. _api.TextOutlineColor: + +TextOutlineColor +**************** + +================ +TextOutlineColor +================ +.. currentmodule:: fastplotlib.graphics._features + +Constructor +~~~~~~~~~~~ +.. autosummary:: + :toctree: TextOutlineColor_api + + TextOutlineColor + +Properties +~~~~~~~~~~ +.. autosummary:: + :toctree: TextOutlineColor_api + + TextOutlineColor.value + +Methods +~~~~~~~ +.. autosummary:: + :toctree: TextOutlineColor_api + + TextOutlineColor.add_event_handler + TextOutlineColor.block_events + TextOutlineColor.clear_event_handlers + TextOutlineColor.remove_event_handler + TextOutlineColor.set_value + diff --git a/docs/source/api/graphic_features/TextOutlineThickness.rst b/docs/source/api/graphic_features/TextOutlineThickness.rst new file mode 100644 index 000000000..75d485781 --- /dev/null +++ b/docs/source/api/graphic_features/TextOutlineThickness.rst @@ -0,0 +1,35 @@ +.. _api.TextOutlineThickness: + +TextOutlineThickness +******************** + +==================== +TextOutlineThickness +==================== +.. currentmodule:: fastplotlib.graphics._features + +Constructor +~~~~~~~~~~~ +.. autosummary:: + :toctree: TextOutlineThickness_api + + TextOutlineThickness + +Properties +~~~~~~~~~~ +.. autosummary:: + :toctree: TextOutlineThickness_api + + TextOutlineThickness.value + +Methods +~~~~~~~ +.. autosummary:: + :toctree: TextOutlineThickness_api + + TextOutlineThickness.add_event_handler + TextOutlineThickness.block_events + TextOutlineThickness.clear_event_handlers + TextOutlineThickness.remove_event_handler + TextOutlineThickness.set_value + diff --git a/docs/source/api/graphic_features/TextureArray.rst b/docs/source/api/graphic_features/TextureArray.rst new file mode 100644 index 000000000..79707c453 --- /dev/null +++ b/docs/source/api/graphic_features/TextureArray.rst @@ -0,0 +1,39 @@ +.. _api.TextureArray: + +TextureArray +************ + +============ +TextureArray +============ +.. currentmodule:: fastplotlib.graphics._features + +Constructor +~~~~~~~~~~~ +.. autosummary:: + :toctree: TextureArray_api + + TextureArray + +Properties +~~~~~~~~~~ +.. autosummary:: + :toctree: TextureArray_api + + TextureArray.buffer + TextureArray.col_indices + TextureArray.row_indices + TextureArray.shared + TextureArray.value + +Methods +~~~~~~~ +.. autosummary:: + :toctree: TextureArray_api + + TextureArray.add_event_handler + TextureArray.block_events + TextureArray.clear_event_handlers + TextureArray.remove_event_handler + TextureArray.set_value + diff --git a/docs/source/api/graphic_features/Thickness.rst b/docs/source/api/graphic_features/Thickness.rst new file mode 100644 index 000000000..061f96fe8 --- /dev/null +++ b/docs/source/api/graphic_features/Thickness.rst @@ -0,0 +1,35 @@ +.. _api.Thickness: + +Thickness +********* + +========= +Thickness +========= +.. currentmodule:: fastplotlib.graphics._features + +Constructor +~~~~~~~~~~~ +.. autosummary:: + :toctree: Thickness_api + + Thickness + +Properties +~~~~~~~~~~ +.. autosummary:: + :toctree: Thickness_api + + Thickness.value + +Methods +~~~~~~~ +.. autosummary:: + :toctree: Thickness_api + + Thickness.add_event_handler + Thickness.block_events + Thickness.clear_event_handlers + Thickness.remove_event_handler + Thickness.set_value + diff --git a/docs/source/api/graphic_features/ThicknessFeature.rst b/docs/source/api/graphic_features/ThicknessFeature.rst deleted file mode 100644 index 80219a2cd..000000000 --- a/docs/source/api/graphic_features/ThicknessFeature.rst +++ /dev/null @@ -1,33 +0,0 @@ -.. _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/UniformColor.rst b/docs/source/api/graphic_features/UniformColor.rst new file mode 100644 index 000000000..7370589b7 --- /dev/null +++ b/docs/source/api/graphic_features/UniformColor.rst @@ -0,0 +1,35 @@ +.. _api.UniformColor: + +UniformColor +************ + +============ +UniformColor +============ +.. currentmodule:: fastplotlib.graphics._features + +Constructor +~~~~~~~~~~~ +.. autosummary:: + :toctree: UniformColor_api + + UniformColor + +Properties +~~~~~~~~~~ +.. autosummary:: + :toctree: UniformColor_api + + UniformColor.value + +Methods +~~~~~~~ +.. autosummary:: + :toctree: UniformColor_api + + UniformColor.add_event_handler + UniformColor.block_events + UniformColor.clear_event_handlers + UniformColor.remove_event_handler + UniformColor.set_value + diff --git a/docs/source/api/graphic_features/UniformSize.rst b/docs/source/api/graphic_features/UniformSize.rst new file mode 100644 index 000000000..e342d6a70 --- /dev/null +++ b/docs/source/api/graphic_features/UniformSize.rst @@ -0,0 +1,35 @@ +.. _api.UniformSize: + +UniformSize +*********** + +=========== +UniformSize +=========== +.. currentmodule:: fastplotlib.graphics._features + +Constructor +~~~~~~~~~~~ +.. autosummary:: + :toctree: UniformSize_api + + UniformSize + +Properties +~~~~~~~~~~ +.. autosummary:: + :toctree: UniformSize_api + + UniformSize.value + +Methods +~~~~~~~ +.. autosummary:: + :toctree: UniformSize_api + + UniformSize.add_event_handler + UniformSize.block_events + UniformSize.clear_event_handlers + UniformSize.remove_event_handler + UniformSize.set_value + diff --git a/docs/source/api/graphic_features/VertexCmap.rst b/docs/source/api/graphic_features/VertexCmap.rst new file mode 100644 index 000000000..a3311d6e6 --- /dev/null +++ b/docs/source/api/graphic_features/VertexCmap.rst @@ -0,0 +1,40 @@ +.. _api.VertexCmap: + +VertexCmap +********** + +========== +VertexCmap +========== +.. currentmodule:: fastplotlib.graphics._features + +Constructor +~~~~~~~~~~~ +.. autosummary:: + :toctree: VertexCmap_api + + VertexCmap + +Properties +~~~~~~~~~~ +.. autosummary:: + :toctree: VertexCmap_api + + VertexCmap.alpha + VertexCmap.buffer + VertexCmap.name + VertexCmap.shared + VertexCmap.transform + VertexCmap.value + +Methods +~~~~~~~ +.. autosummary:: + :toctree: VertexCmap_api + + VertexCmap.add_event_handler + VertexCmap.block_events + VertexCmap.clear_event_handlers + VertexCmap.remove_event_handler + VertexCmap.set_value + diff --git a/docs/source/api/graphic_features/VertexColors.rst b/docs/source/api/graphic_features/VertexColors.rst new file mode 100644 index 000000000..3c2089a78 --- /dev/null +++ b/docs/source/api/graphic_features/VertexColors.rst @@ -0,0 +1,37 @@ +.. _api.VertexColors: + +VertexColors +************ + +============ +VertexColors +============ +.. currentmodule:: fastplotlib.graphics._features + +Constructor +~~~~~~~~~~~ +.. autosummary:: + :toctree: VertexColors_api + + VertexColors + +Properties +~~~~~~~~~~ +.. autosummary:: + :toctree: VertexColors_api + + VertexColors.buffer + VertexColors.shared + VertexColors.value + +Methods +~~~~~~~ +.. autosummary:: + :toctree: VertexColors_api + + VertexColors.add_event_handler + VertexColors.block_events + VertexColors.clear_event_handlers + VertexColors.remove_event_handler + VertexColors.set_value + diff --git a/docs/source/api/graphic_features/VertexPositions.rst b/docs/source/api/graphic_features/VertexPositions.rst new file mode 100644 index 000000000..9669ab6d5 --- /dev/null +++ b/docs/source/api/graphic_features/VertexPositions.rst @@ -0,0 +1,37 @@ +.. _api.VertexPositions: + +VertexPositions +*************** + +=============== +VertexPositions +=============== +.. currentmodule:: fastplotlib.graphics._features + +Constructor +~~~~~~~~~~~ +.. autosummary:: + :toctree: VertexPositions_api + + VertexPositions + +Properties +~~~~~~~~~~ +.. autosummary:: + :toctree: VertexPositions_api + + VertexPositions.buffer + VertexPositions.shared + VertexPositions.value + +Methods +~~~~~~~ +.. autosummary:: + :toctree: VertexPositions_api + + VertexPositions.add_event_handler + VertexPositions.block_events + VertexPositions.clear_event_handlers + VertexPositions.remove_event_handler + VertexPositions.set_value + diff --git a/docs/source/api/graphic_features/Visible.rst b/docs/source/api/graphic_features/Visible.rst new file mode 100644 index 000000000..957b4433a --- /dev/null +++ b/docs/source/api/graphic_features/Visible.rst @@ -0,0 +1,35 @@ +.. _api.Visible: + +Visible +******* + +======= +Visible +======= +.. currentmodule:: fastplotlib.graphics._features + +Constructor +~~~~~~~~~~~ +.. autosummary:: + :toctree: Visible_api + + Visible + +Properties +~~~~~~~~~~ +.. autosummary:: + :toctree: Visible_api + + Visible.value + +Methods +~~~~~~~ +.. autosummary:: + :toctree: Visible_api + + Visible.add_event_handler + Visible.block_events + Visible.clear_event_handlers + Visible.remove_event_handler + Visible.set_value + diff --git a/docs/source/api/graphic_features/index.rst b/docs/source/api/graphic_features/index.rst index 06e3119e5..87504ea8a 100644 --- a/docs/source/api/graphic_features/index.rst +++ b/docs/source/api/graphic_features/index.rst @@ -4,20 +4,28 @@ Graphic Features .. toctree:: :maxdepth: 1 - ColorFeature - CmapFeature - ImageCmapFeature - HeatmapCmapFeature - PointsDataFeature + VertexColors + UniformColor + UniformSize + Thickness + VertexPositions PointsSizesFeature - ImageDataFeature - HeatmapDataFeature - PresentFeature - ThicknessFeature - GraphicFeature - GraphicFeatureIndexable - FeatureEvent - to_gpu_supported_dtype + VertexCmap + TextureArray + ImageCmap + ImageVmin + ImageVmax + ImageInterpolation + ImageCmapInterpolation + TextData + FontSize + TextFaceColor + TextOutlineColor + TextOutlineThickness LinearSelectionFeature LinearRegionSelectionFeature + Name + Offset + Rotation + Visible Deleted diff --git a/docs/source/api/graphic_features/to_gpu_supported_dtype.rst b/docs/source/api/graphic_features/to_gpu_supported_dtype.rst deleted file mode 100644 index 984a76157..000000000 --- a/docs/source/api/graphic_features/to_gpu_supported_dtype.rst +++ /dev/null @@ -1,29 +0,0 @@ -.. _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/HeatmapGraphic.rst b/docs/source/api/graphics/HeatmapGraphic.rst deleted file mode 100644 index ffa86eb16..000000000 --- a/docs/source/api/graphics/HeatmapGraphic.rst +++ /dev/null @@ -1,44 +0,0 @@ -.. _api.HeatmapGraphic: - -HeatmapGraphic -************** - -============== -HeatmapGraphic -============== -.. currentmodule:: fastplotlib - -Constructor -~~~~~~~~~~~ -.. autosummary:: - :toctree: HeatmapGraphic_api - - HeatmapGraphic - -Properties -~~~~~~~~~~ -.. autosummary:: - :toctree: HeatmapGraphic_api - - HeatmapGraphic.children - HeatmapGraphic.name - HeatmapGraphic.position - HeatmapGraphic.position_x - HeatmapGraphic.position_y - HeatmapGraphic.position_z - HeatmapGraphic.rotation - HeatmapGraphic.visible - HeatmapGraphic.world_object - -Methods -~~~~~~~ -.. autosummary:: - :toctree: HeatmapGraphic_api - - HeatmapGraphic.add_linear_region_selector - HeatmapGraphic.add_linear_selector - HeatmapGraphic.link - HeatmapGraphic.reset_feature - HeatmapGraphic.rotate - HeatmapGraphic.set_feature - diff --git a/docs/source/api/graphics/ImageGraphic.rst b/docs/source/api/graphics/ImageGraphic.rst index 00b27340d..a0ae8a5ed 100644 --- a/docs/source/api/graphics/ImageGraphic.rst +++ b/docs/source/api/graphics/ImageGraphic.rst @@ -20,14 +20,20 @@ Properties .. autosummary:: :toctree: ImageGraphic_api - ImageGraphic.children + ImageGraphic.block_events + ImageGraphic.cmap + ImageGraphic.cmap_interpolation + ImageGraphic.data + ImageGraphic.deleted + ImageGraphic.event_handlers + ImageGraphic.interpolation ImageGraphic.name - ImageGraphic.position - ImageGraphic.position_x - ImageGraphic.position_y - ImageGraphic.position_z + ImageGraphic.offset ImageGraphic.rotation + ImageGraphic.supported_events ImageGraphic.visible + ImageGraphic.vmax + ImageGraphic.vmin ImageGraphic.world_object Methods @@ -35,10 +41,13 @@ Methods .. autosummary:: :toctree: ImageGraphic_api + ImageGraphic.add_event_handler ImageGraphic.add_linear_region_selector ImageGraphic.add_linear_selector - ImageGraphic.link - ImageGraphic.reset_feature + ImageGraphic.clear_event_handlers + ImageGraphic.remove_event_handler + ImageGraphic.reset_vmin_vmax ImageGraphic.rotate - ImageGraphic.set_feature + ImageGraphic.share_property + ImageGraphic.unshare_property diff --git a/docs/source/api/graphics/LineCollection.rst b/docs/source/api/graphics/LineCollection.rst index 8d10d8376..c000b7334 100644 --- a/docs/source/api/graphics/LineCollection.rst +++ b/docs/source/api/graphics/LineCollection.rst @@ -20,17 +20,24 @@ Properties .. autosummary:: :toctree: LineCollection_api - LineCollection.children + LineCollection.block_events LineCollection.cmap - LineCollection.cmap_values + LineCollection.colors + LineCollection.data + LineCollection.deleted + LineCollection.event_handlers LineCollection.graphics + LineCollection.metadatas LineCollection.name - LineCollection.position - LineCollection.position_x - LineCollection.position_y - LineCollection.position_z + LineCollection.names + LineCollection.offset + LineCollection.offsets LineCollection.rotation + LineCollection.rotations + LineCollection.supported_events + LineCollection.thickness LineCollection.visible + LineCollection.visibles LineCollection.world_object Methods @@ -38,12 +45,14 @@ Methods .. autosummary:: :toctree: LineCollection_api + LineCollection.add_event_handler LineCollection.add_graphic LineCollection.add_linear_region_selector LineCollection.add_linear_selector - LineCollection.link + LineCollection.clear_event_handlers + LineCollection.remove_event_handler LineCollection.remove_graphic - LineCollection.reset_feature LineCollection.rotate - LineCollection.set_feature + LineCollection.share_property + LineCollection.unshare_property diff --git a/docs/source/api/graphics/LineGraphic.rst b/docs/source/api/graphics/LineGraphic.rst index 8b6fedf22..d260c3214 100644 --- a/docs/source/api/graphics/LineGraphic.rst +++ b/docs/source/api/graphics/LineGraphic.rst @@ -20,13 +20,17 @@ Properties .. autosummary:: :toctree: LineGraphic_api - LineGraphic.children + LineGraphic.block_events + LineGraphic.cmap + LineGraphic.colors + LineGraphic.data + LineGraphic.deleted + LineGraphic.event_handlers LineGraphic.name - LineGraphic.position - LineGraphic.position_x - LineGraphic.position_y - LineGraphic.position_z + LineGraphic.offset LineGraphic.rotation + LineGraphic.supported_events + LineGraphic.thickness LineGraphic.visible LineGraphic.world_object @@ -35,10 +39,12 @@ Methods .. autosummary:: :toctree: LineGraphic_api + LineGraphic.add_event_handler LineGraphic.add_linear_region_selector LineGraphic.add_linear_selector - LineGraphic.link - LineGraphic.reset_feature + LineGraphic.clear_event_handlers + LineGraphic.remove_event_handler LineGraphic.rotate - LineGraphic.set_feature + LineGraphic.share_property + LineGraphic.unshare_property diff --git a/docs/source/api/graphics/LineStack.rst b/docs/source/api/graphics/LineStack.rst index a39db46f8..18b35932d 100644 --- a/docs/source/api/graphics/LineStack.rst +++ b/docs/source/api/graphics/LineStack.rst @@ -20,17 +20,24 @@ Properties .. autosummary:: :toctree: LineStack_api - LineStack.children + LineStack.block_events LineStack.cmap - LineStack.cmap_values + LineStack.colors + LineStack.data + LineStack.deleted + LineStack.event_handlers LineStack.graphics + LineStack.metadatas LineStack.name - LineStack.position - LineStack.position_x - LineStack.position_y - LineStack.position_z + LineStack.names + LineStack.offset + LineStack.offsets LineStack.rotation + LineStack.rotations + LineStack.supported_events + LineStack.thickness LineStack.visible + LineStack.visibles LineStack.world_object Methods @@ -38,12 +45,14 @@ Methods .. autosummary:: :toctree: LineStack_api + LineStack.add_event_handler LineStack.add_graphic LineStack.add_linear_region_selector LineStack.add_linear_selector - LineStack.link + LineStack.clear_event_handlers + LineStack.remove_event_handler LineStack.remove_graphic - LineStack.reset_feature LineStack.rotate - LineStack.set_feature + LineStack.share_property + LineStack.unshare_property diff --git a/docs/source/api/graphics/ScatterGraphic.rst b/docs/source/api/graphics/ScatterGraphic.rst index 44d87d008..8f2b17fd6 100644 --- a/docs/source/api/graphics/ScatterGraphic.rst +++ b/docs/source/api/graphics/ScatterGraphic.rst @@ -20,13 +20,17 @@ Properties .. autosummary:: :toctree: ScatterGraphic_api - ScatterGraphic.children + ScatterGraphic.block_events + ScatterGraphic.cmap + ScatterGraphic.colors + ScatterGraphic.data + ScatterGraphic.deleted + ScatterGraphic.event_handlers ScatterGraphic.name - ScatterGraphic.position - ScatterGraphic.position_x - ScatterGraphic.position_y - ScatterGraphic.position_z + ScatterGraphic.offset ScatterGraphic.rotation + ScatterGraphic.sizes + ScatterGraphic.supported_events ScatterGraphic.visible ScatterGraphic.world_object @@ -35,5 +39,10 @@ Methods .. autosummary:: :toctree: ScatterGraphic_api + ScatterGraphic.add_event_handler + ScatterGraphic.clear_event_handlers + ScatterGraphic.remove_event_handler ScatterGraphic.rotate + ScatterGraphic.share_property + ScatterGraphic.unshare_property diff --git a/docs/source/api/graphics/TextGraphic.rst b/docs/source/api/graphics/TextGraphic.rst index 23425cf41..a3cd9bbb9 100644 --- a/docs/source/api/graphics/TextGraphic.rst +++ b/docs/source/api/graphics/TextGraphic.rst @@ -20,18 +20,18 @@ Properties .. autosummary:: :toctree: TextGraphic_api - TextGraphic.children + TextGraphic.block_events + TextGraphic.deleted + TextGraphic.event_handlers TextGraphic.face_color + TextGraphic.font_size TextGraphic.name + TextGraphic.offset TextGraphic.outline_color - TextGraphic.outline_size - TextGraphic.position - TextGraphic.position_x - TextGraphic.position_y - TextGraphic.position_z + TextGraphic.outline_thickness TextGraphic.rotation + TextGraphic.supported_events TextGraphic.text - TextGraphic.text_size TextGraphic.visible TextGraphic.world_object @@ -40,5 +40,10 @@ Methods .. autosummary:: :toctree: TextGraphic_api + TextGraphic.add_event_handler + TextGraphic.clear_event_handlers + TextGraphic.remove_event_handler TextGraphic.rotate + TextGraphic.share_property + TextGraphic.unshare_property diff --git a/docs/source/api/graphics/index.rst b/docs/source/api/graphics/index.rst index 611ee5833..b64ac53c0 100644 --- a/docs/source/api/graphics/index.rst +++ b/docs/source/api/graphics/index.rst @@ -4,10 +4,9 @@ Graphics .. toctree:: :maxdepth: 1 + LineGraphic ImageGraphic ScatterGraphic - LineGraphic - HeatmapGraphic + TextGraphic LineCollection LineStack - TextGraphic diff --git a/docs/source/api/layouts/subplot.rst b/docs/source/api/layouts/subplot.rst index 91884557a..61f5da307 100644 --- a/docs/source/api/layouts/subplot.rst +++ b/docs/source/api/layouts/subplot.rst @@ -42,7 +42,6 @@ Methods Subplot.add_animations Subplot.add_graphic - Subplot.add_heatmap Subplot.add_image Subplot.add_line Subplot.add_line_collection diff --git a/docs/source/api/selectors/LinearRegionSelector.rst b/docs/source/api/selectors/LinearRegionSelector.rst index 1b59e80c9..c9140bc7d 100644 --- a/docs/source/api/selectors/LinearRegionSelector.rst +++ b/docs/source/api/selectors/LinearRegionSelector.rst @@ -20,14 +20,17 @@ Properties .. autosummary:: :toctree: LinearRegionSelector_api - LinearRegionSelector.children + LinearRegionSelector.axis + LinearRegionSelector.block_events + LinearRegionSelector.deleted + LinearRegionSelector.event_handlers LinearRegionSelector.limits LinearRegionSelector.name - LinearRegionSelector.position - LinearRegionSelector.position_x - LinearRegionSelector.position_y - LinearRegionSelector.position_z + LinearRegionSelector.offset + LinearRegionSelector.parent LinearRegionSelector.rotation + LinearRegionSelector.selection + LinearRegionSelector.supported_events LinearRegionSelector.visible LinearRegionSelector.world_object @@ -36,10 +39,15 @@ Methods .. autosummary:: :toctree: LinearRegionSelector_api + LinearRegionSelector.add_event_handler LinearRegionSelector.add_ipywidget_handler + LinearRegionSelector.clear_event_handlers LinearRegionSelector.get_selected_data LinearRegionSelector.get_selected_index LinearRegionSelector.get_selected_indices LinearRegionSelector.make_ipywidget_slider + LinearRegionSelector.remove_event_handler LinearRegionSelector.rotate + LinearRegionSelector.share_property + LinearRegionSelector.unshare_property diff --git a/docs/source/api/selectors/LinearSelector.rst b/docs/source/api/selectors/LinearSelector.rst index 3278559d0..fa21f8f15 100644 --- a/docs/source/api/selectors/LinearSelector.rst +++ b/docs/source/api/selectors/LinearSelector.rst @@ -20,14 +20,17 @@ Properties .. autosummary:: :toctree: LinearSelector_api - LinearSelector.children + LinearSelector.axis + LinearSelector.block_events + LinearSelector.deleted + LinearSelector.event_handlers LinearSelector.limits LinearSelector.name - LinearSelector.position - LinearSelector.position_x - LinearSelector.position_y - LinearSelector.position_z + LinearSelector.offset + LinearSelector.parent LinearSelector.rotation + LinearSelector.selection + LinearSelector.supported_events LinearSelector.visible LinearSelector.world_object @@ -36,10 +39,15 @@ Methods .. autosummary:: :toctree: LinearSelector_api + LinearSelector.add_event_handler LinearSelector.add_ipywidget_handler + LinearSelector.clear_event_handlers LinearSelector.get_selected_data LinearSelector.get_selected_index LinearSelector.get_selected_indices LinearSelector.make_ipywidget_slider + LinearSelector.remove_event_handler LinearSelector.rotate + LinearSelector.share_property + LinearSelector.unshare_property diff --git a/docs/source/api/selectors/PolygonSelector.rst b/docs/source/api/selectors/PolygonSelector.rst deleted file mode 100644 index 8de87ec74..000000000 --- a/docs/source/api/selectors/PolygonSelector.rst +++ /dev/null @@ -1,43 +0,0 @@ -.. _api.PolygonSelector: - -PolygonSelector -*************** - -=============== -PolygonSelector -=============== -.. currentmodule:: fastplotlib - -Constructor -~~~~~~~~~~~ -.. autosummary:: - :toctree: PolygonSelector_api - - PolygonSelector - -Properties -~~~~~~~~~~ -.. autosummary:: - :toctree: PolygonSelector_api - - PolygonSelector.children - PolygonSelector.name - PolygonSelector.position - PolygonSelector.position_x - PolygonSelector.position_y - PolygonSelector.position_z - PolygonSelector.rotation - PolygonSelector.visible - PolygonSelector.world_object - -Methods -~~~~~~~ -.. autosummary:: - :toctree: PolygonSelector_api - - PolygonSelector.get_selected_data - PolygonSelector.get_selected_index - PolygonSelector.get_selected_indices - PolygonSelector.get_vertices - PolygonSelector.rotate - diff --git a/docs/source/api/selectors/Synchronizer.rst b/docs/source/api/selectors/Synchronizer.rst deleted file mode 100644 index 2b28fe351..000000000 --- a/docs/source/api/selectors/Synchronizer.rst +++ /dev/null @@ -1,33 +0,0 @@ -.. _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.clear - Synchronizer.remove - diff --git a/docs/source/api/selectors/index.rst b/docs/source/api/selectors/index.rst index 01c040728..ffa4054db 100644 --- a/docs/source/api/selectors/index.rst +++ b/docs/source/api/selectors/index.rst @@ -6,5 +6,3 @@ Selectors LinearSelector LinearRegionSelector - PolygonSelector - Synchronizer diff --git a/docs/source/conf.py b/docs/source/conf.py index f681a8101..38133c901 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -23,7 +23,6 @@ "sphinx.ext.viewcode", "sphinx_copybutton", "sphinx_design", - "nbsphinx", ] autosummary_generate = True @@ -52,7 +51,7 @@ intersphinx_mapping = { "python": ("https://docs.python.org/3", None), "numpy": ("https://numpy.org/doc/stable/", None), - "pygfx": ("https://pygfx.readthedocs.io/en/latest", None), + "pygfx": ("https://pygfx.com/latest", None), "wgpu": ("https://wgpu-py.readthedocs.io/en/latest", None), } diff --git a/docs/source/generate_api.py b/docs/source/generate_api.py index a5f668130..0150836ec 100644 --- a/docs/source/generate_api.py +++ b/docs/source/generate_api.py @@ -263,6 +263,12 @@ def main(): with open(API_DIR.joinpath("utils.rst"), "w") as f: f.write(utils_str) + # gpu selection + fpl_functions = generate_functions_module(fastplotlib, "fastplotlib.utils.gpu") + + with open(API_DIR.joinpath("gpu.rst"), "w") as f: + f.write(fpl_functions) + if __name__ == "__main__": main() diff --git a/docs/source/index.rst b/docs/source/index.rst index 0bca839b9..e99e38c52 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -1,12 +1,6 @@ Welcome to fastplotlib's documentation! ======================================= -.. toctree:: - :caption: Quick Start - :maxdepth: 2 - - quickstart - .. toctree:: :caption: User Guide :maxdepth: 2 diff --git a/docs/source/quickstart.ipynb b/docs/source/quickstart.ipynb deleted file mode 100644 index 6a892399e..000000000 --- a/docs/source/quickstart.ipynb +++ /dev/null @@ -1,1673 +0,0 @@ -{ - "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 `Figure` instance\n", - "fig = fpl.Figure()\n", - "\n", - "# get a grayscale image\n", - "data = iio.imread(\"imageio:camera.png\")\n", - "\n", - "# plot the image data\n", - "image_graphic = fig[0, 0].add_image(data=data, name=\"sample-image\")\n", - "\n", - "# show the plot\n", - "fig.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!**" - ] - }, - { - "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": [ - "fig.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": [ - "fig.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": [ - "fig.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": [ - "fig.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": [ - "fig.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": [ - "fig.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": [ - "fig.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": [ - "fig" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "5551861f-9860-4515-8222-2f1c6d6a3220", - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "fig[0, 0][\"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": [ - "fig[0, 0].graphics" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "119bd6af-c486-4378-bc23-79b1759aa3a4", - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "fig[0, 0].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 is fig[0, 0][\"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": [ - "fig_rgb = fpl.Figure()\n", - "\n", - "fig_rgb[0, 0].add_image(new_data, name=\"rgb-image\")\n", - "\n", - "fig_rgb.show()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "a47b1eaf-3638-470a-88a5-0026c81d7e2b", - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "fig_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": [ - "fig_rgb[0, 0][\"rgb-image\"].cmap.vmin = 100" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "161468ba-b836-4021-8d11-8dfc140b94eb", - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "fig_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 `Figure` instance\n", - "fig_vid = fpl.Figure()\n", - "\n", - "# make some random data again\n", - "data = np.random.rand(512, 512)\n", - "\n", - "# plot the data\n", - "fig_vid[0, 0].add_image(data=data, name=\"random-image\")\n", - "\n", - "# a function to update the image_graphic\n", - "# a subplot will pass its instance to the animation function as an argument\n", - "def update_data(subplot):\n", - " new_data = np.random.rand(512, 512)\n", - " subplot[\"random-image\"].data = new_data\n", - "\n", - "#add this as an animation function to the subplot\n", - "fig_vid[0, 0].add_animations(update_data)\n", - "\n", - "# show the plot\n", - "fig_vid.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": [ - "fig_sync = fpl.Figure(controllers=fig_vid.controllers)\n", - "\n", - "data = np.random.rand(512, 512)\n", - "\n", - "image_graphic_instance = fig_sync[0, 0].add_image(data=data, cmap=\"viridis\")\n", - "\n", - "# you will need to define a new animation function for this graphic\n", - "def update_data_2():\n", - " new_data = np.random.rand(512, 512)\n", - " # alternatively, you can use the stored reference to the graphic as well instead of indexing the Plot\n", - " image_graphic_instance.data = new_data\n", - "\n", - "# add the animation function to the figure instead of the subplot\n", - "fig_sync.add_animations(update_data_2)\n", - "\n", - "fig_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", - "fig_line = fpl.Figure()\n", - "\n", - "# plot sine wave, use a single color\n", - "sine_graphic = fig_line[0, 0].add_line(data=sine, thickness=5, colors=\"magenta\")\n", - "\n", - "# you can also use colormaps for lines!\n", - "cosine_graphic = fig_line[0, 0].add_line(data=cosine, thickness=12, cmap=\"autumn\")\n", - "\n", - "# or a list of colors for each datapoint\n", - "colors = [\"r\"] * 25 + [\"purple\"] * 25 + [\"y\"] * 25 + [\"b\"] * 25\n", - "sinc_graphic = fig_line[0, 0].add_line(data=sinc, thickness=5, colors = colors)\n", - "\n", - "fig_line.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": [ - "fig_line[0, 0].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": [ - "fig_line[0, 0].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": [ - "fig_line.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": [ - "fig_line.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": [ - "fig_line.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": [ - "fig_line.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": [ - "fig_line.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(fig_line[0, 0].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": [ - "fig_line.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": [ - "fig_line.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", - "fig_line[0, 0].add_image(img, name=\"image\", cmap=\"gray\")\n", - "\n", - "# z axis position -1 so it is below all the lines\n", - "fig_line[0, 0][\"image\"].position_z = -1\n", - "fig_line[0, 0][\"image\"].position_x = -50" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "5b586a89-ca3e-4e88-a801-bdd665384f59", - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "fig_line.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", - "fig_line_3d = fpl.Figure(cameras='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", - "fig_line_3d[0, 0].add_line(data=spiral, thickness=2, cmap='winter')\n", - "\n", - "fig_line_3d.show()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "28eb7014-4773-4a34-8bfc-bd3a46429012", - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "fig_line_3d[0, 0].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", - "fig_scatter = fpl.Figure()\n", - "\n", - "# use an alpha value since this will be a lot of points\n", - "scatter_graphic = fig_scatter[0, 0].add_scatter(data=cloud, sizes=3, colors=colors, alpha=0.7)\n", - "\n", - "fig_scatter.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": [ - "fig_scatter.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": [ - "fig_scatter.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": [ - "fig_scatter.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": [ - "fig_scatter.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": [ - "fig_scatter.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": [ - "## More subplots" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "6b7e1129-ae8e-4a0f-82dc-bd8fb65871fc", - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "# Figure of shape 2 x 3 with all controllers synced\n", - "figure_grid = fpl.Figure(shape=(2, 3), controller_ids=\"sync\")\n", - "\n", - "# Make a random image graphic for each subplot\n", - "for subplot in figure_grid:\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(f):\n", - " for subplot in f:\n", - " new_data = np.random.rand(512, 512)\n", - " # index the image graphic by name and set the data\n", - " subplot[\"rand-img\"].data = new_data\n", - " \n", - "# add the animation function\n", - "figure_grid.add_animations(update_data)\n", - "\n", - "# show the gridplot \n", - "figure_grid.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", - "figure_grid[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": [ - "figure_grid[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": [ - "figure_grid[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", - "figure_grid[0, 2].name = \"top-right-plot\"" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "73300d2c-3e70-43ad-b5a2-40341b701ac8", - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "figure_grid[\"top-right-plot\"]" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "834d9905-35e9-4711-9375-5b1828c80ee2", - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "# view its position\n", - "figure_grid[\"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", - "figure_grid[\"top-right-plot\"] is figure_grid[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": [ - "figure_grid[\"top-right-plot\"][\"rand-img\"].vmin = 0.5" - ] - }, - { - "cell_type": "markdown", - "id": "6a5b4368-ae4d-442c-a11f-45c70267339b", - "metadata": {}, - "source": [ - "## Figure subplot customization" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "175d45a6-3351-4b75-8ff3-08797fe0a389", - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "# 2 rows and 3 columns\n", - "shape = (2, 3)\n", - "\n", - "# pan-zoom controllers for each view\n", - "# views are synced if they have the \n", - "# same controller ID\n", - "controller_ids = [\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", - "figure_grid = fpl.Figure(\n", - " shape=shape,\n", - " controller_ids=controller_ids,\n", - " names=names,\n", - ")\n", - "\n", - "\n", - "# Make a random image graphic for each subplot\n", - "for subplot in figure_grid:\n", - " data = np.random.rand(512, 512)\n", - " # create and add an ImageGraphic\n", - " subplot.add_image(data=data, name=\"rand-image\")\n", - " \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", - "figure_grid.add_animations(set_random_frame)\n", - "figure_grid.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", - "figure_grid[\"subplot0\"]" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "a14df7ea-14c3-4a8a-84f2-2e2194236d9e", - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "# can access subplot by index\n", - "figure_grid[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", - "figure_grid[\"subplot0\"][\"rand-image\"]" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "ed4eebb7-826d-4856-bbb8-db2de966a0c3", - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "figure_grid[\"subplot0\"][\"rand-image\"].vmin = 0.6\n", - "figure_grid[\"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": [ - "figure_grid[1, 0][\"rand-image\"].vim = 0.1\n", - "figure_grid[1, 0][\"rand-image\"].vmax = 0.3" - ] - } - ], - "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/desktop/heatmap/heatmap.py b/examples/desktop/heatmap/heatmap.py index fa5ec6715..f3a1bf460 100644 --- a/examples/desktop/heatmap/heatmap.py +++ b/examples/desktop/heatmap/heatmap.py @@ -12,18 +12,14 @@ fig = fpl.Figure() -xs = np.linspace(0, 1_000, 10_000) +xs = np.linspace(0, 1_000, 10_000, dtype=np.float32) -sine = np.sin(xs) -cosine = np.cos(xs) +sine = np.sin(np.sqrt(xs)) -# alternating sines and cosines -data = np.zeros((10_000, 10_000), dtype=np.float32) -data[::2] = sine -data[1::2] = cosine +data = np.vstack([sine * i for i in range(20_000)]) # plot the image data -heatmap_graphic = fig[0, 0].add_heatmap(data=data, name="heatmap") +img = fig[0, 0].add_image(data=data, name="heatmap") fig.show() diff --git a/examples/desktop/heatmap/heatmap_cmap.py b/examples/desktop/heatmap/heatmap_cmap.py index a1434bb0e..39e697c93 100644 --- a/examples/desktop/heatmap/heatmap_cmap.py +++ b/examples/desktop/heatmap/heatmap_cmap.py @@ -4,7 +4,7 @@ Change the cmap of a heatmap """ -# test_example = true +# test_example = false import fastplotlib as fpl import numpy as np @@ -12,18 +12,14 @@ fig = fpl.Figure() -xs = np.linspace(0, 1_000, 10_000) +xs = np.linspace(0, 1_000, 10_000, dtype=np.float32) -sine = np.sin(xs) -cosine = np.cos(xs) +sine = np.sin(np.sqrt(xs)) -# alternating sines and cosines -data = np.zeros((10_000, 10_000), dtype=np.float32) -data[::2] = sine -data[1::2] = cosine +data = np.vstack([sine * i for i in range(20_000)]) # plot the image data -heatmap_graphic = fig[0, 0].add_heatmap(data=data, name="heatmap") +img = fig[0, 0].add_image(data=data, name="heatmap") fig.show() @@ -31,7 +27,7 @@ fig[0, 0].auto_scale() -heatmap_graphic.cmap = "viridis" +img.cmap = "viridis" if __name__ == "__main__": print(__doc__) diff --git a/examples/desktop/heatmap/heatmap_data.py b/examples/desktop/heatmap/heatmap_data.py index 67aee1668..75ef3ce41 100644 --- a/examples/desktop/heatmap/heatmap_data.py +++ b/examples/desktop/heatmap/heatmap_data.py @@ -4,7 +4,7 @@ Change the data of a heatmap """ -# test_example = true +# test_example = false import fastplotlib as fpl import numpy as np @@ -12,28 +12,24 @@ fig = fpl.Figure() -xs = np.linspace(0, 1_000, 10_000) +xs = np.linspace(0, 1_000, 9_000, dtype=np.float32) -sine = np.sin(xs) -cosine = np.cos(xs) +sine = np.sin(np.sqrt(xs)) -# alternating sines and cosines -data = np.zeros((10_000, 10_000), dtype=np.float32) -data[::2] = sine -data[1::2] = cosine +data = np.vstack([sine * i for i in range(9_000)]) # plot the image data -heatmap_graphic = fig[0, 0].add_heatmap(data=data, name="heatmap") +img = fig[0, 0].add_image(data=data, name="heatmap") fig.show() fig.canvas.set_logical_size(1500, 1500) fig[0, 0].auto_scale() +cosine = np.cos(np.sqrt(xs)[:3000]) -heatmap_graphic.data[:5_000] = sine -heatmap_graphic.data[5_000:] = cosine - +# change first 2,000 rows and 3,000 columns +img.data[:2_000, :3_000] = np.vstack([cosine * i * 4 for i in range(2_000)]) if __name__ == "__main__": print(__doc__) diff --git a/examples/desktop/heatmap/heatmap_square.py b/examples/desktop/heatmap/heatmap_square.py new file mode 100644 index 000000000..f776b74e1 --- /dev/null +++ b/examples/desktop/heatmap/heatmap_square.py @@ -0,0 +1,33 @@ +""" +Square Heatmap +============== +square heatmap test +""" + +# test_example = false + +import fastplotlib as fpl +import numpy as np + + +fig = fpl.Figure() + +xs = np.linspace(0, 1_000, 20_000, dtype=np.float32) + +sine = np.sin(np.sqrt(xs)) + +data = np.vstack([sine * i for i in range(20_000)]) + +# plot the image data +img = fig[0, 0].add_image(data=data, name="heatmap") + +del data # data no longer needed after given to graphic +fig.show() + +fig.canvas.set_logical_size(1500, 1500) + +fig[0, 0].auto_scale() + +if __name__ == "__main__": + print(__doc__) + fpl.run() diff --git a/examples/desktop/heatmap/heatmap_vmin_vmax.py b/examples/desktop/heatmap/heatmap_vmin_vmax.py index 6fe8a08b8..75b6b7b68 100644 --- a/examples/desktop/heatmap/heatmap_vmin_vmax.py +++ b/examples/desktop/heatmap/heatmap_vmin_vmax.py @@ -4,7 +4,7 @@ Change the vmin vmax of a heatmap """ -# test_example = true +# test_example = false import fastplotlib as fpl import numpy as np @@ -12,18 +12,14 @@ fig = fpl.Figure() -xs = np.linspace(0, 1_000, 10_000) +xs = np.linspace(0, 1_000, 10_000, dtype=np.float32) -sine = np.sin(xs) -cosine = np.cos(xs) +sine = np.sin(np.sqrt(xs)) -# alternating sines and cosines -data = np.zeros((10_000, 10_000), dtype=np.float32) -data[::2] = sine -data[1::2] = cosine +data = np.vstack([sine * i for i in range(20_000)]) # plot the image data -heatmap_graphic = fig[0, 0].add_heatmap(data=data, name="heatmap") +img = fig[0, 0].add_image(data=data, name="heatmap") fig.show() @@ -31,8 +27,8 @@ fig[0, 0].auto_scale() -heatmap_graphic.cmap.vmin = -0.5 -heatmap_graphic.cmap.vmax = 0.5 +img.vmin = -5_000 +img.vmax = 10_000 if __name__ == "__main__": print(__doc__) diff --git a/examples/desktop/heatmap/heatmap_wide.py b/examples/desktop/heatmap/heatmap_wide.py new file mode 100644 index 000000000..251c25fa4 --- /dev/null +++ b/examples/desktop/heatmap/heatmap_wide.py @@ -0,0 +1,32 @@ +""" +Wide Heatmap +============ +Wide example +""" + +# test_example = false + +import fastplotlib as fpl +import numpy as np + + +fig = fpl.Figure() + +xs = np.linspace(0, 1_000, 20_000, dtype=np.float32) + +sine = np.sin(np.sqrt(xs)) + +data = np.vstack([sine * i for i in range(10_000)]) + +# plot the image data +img = fig[0, 0].add_image(data=data, name="heatmap") + +fig.show() + +fig.canvas.set_logical_size(1500, 1500) + +fig[0, 0].auto_scale() + +if __name__ == "__main__": + print(__doc__) + fpl.run() diff --git a/examples/desktop/image/image_rgbvminvmax.py b/examples/desktop/image/image_rgbvminvmax.py index 9725c038a..56114e1e3 100644 --- a/examples/desktop/image/image_rgbvminvmax.py +++ b/examples/desktop/image/image_rgbvminvmax.py @@ -23,8 +23,8 @@ fig[0, 0].auto_scale() -image_graphic.cmap.vmin = 0.5 -image_graphic.cmap.vmax = 0.75 +image_graphic.vmin = 0.5 +image_graphic.vmax = 0.75 if __name__ == "__main__": diff --git a/examples/desktop/image/image_vminvmax.py b/examples/desktop/image/image_vminvmax.py index 3c8607aef..d24d1f18c 100644 --- a/examples/desktop/image/image_vminvmax.py +++ b/examples/desktop/image/image_vminvmax.py @@ -23,8 +23,8 @@ fig[0, 0].auto_scale() -image_graphic.cmap.vmin = 0.5 -image_graphic.cmap.vmax = 0.75 +image_graphic.vmin = 0.5 +image_graphic.vmax = 0.75 if __name__ == "__main__": diff --git a/examples/desktop/line/line_cmap.py b/examples/desktop/line/line_cmap.py index 7d8e1e7d6..f18ceb201 100644 --- a/examples/desktop/line/line_cmap.py +++ b/examples/desktop/line/line_cmap.py @@ -21,21 +21,21 @@ ys = np.cos(xs) - 5 cosine = np.dstack([xs, ys])[0] -# cmap_values from an array, so the colors on the sine line will be based on the sine y-values +# cmap_transform from an array, so the colors on the sine line will be based on the sine y-values sine_graphic = fig[0, 0].add_line( data=sine, thickness=10, cmap="plasma", - cmap_values=sine[:, 1] + cmap_transform=sine[:, 1] ) # qualitative colormaps, useful for cluster labels or other types of categorical labels -cmap_values = [0] * 25 + [5] * 10 + [1] * 35 + [2] * 30 +labels = [0] * 25 + [5] * 10 + [1] * 35 + [2] * 30 cosine_graphic = fig[0, 0].add_line( data=cosine, thickness=10, cmap="tab10", - cmap_values=cmap_values + cmap_transform=labels ) fig.show() diff --git a/examples/desktop/line/line_colorslice.py b/examples/desktop/line/line_colorslice.py index 4df666531..28b877793 100644 --- a/examples/desktop/line/line_colorslice.py +++ b/examples/desktop/line/line_colorslice.py @@ -1,6 +1,6 @@ """ Line Plot -============ +========= Example showing color slicing with cosine, sine, sinc lines. """ @@ -15,25 +15,48 @@ xs = np.linspace(-10, 10, 100) # sine wave ys = np.sin(xs) -sine = np.dstack([xs, ys])[0] +sine = np.column_stack([xs, ys]) # cosine wave -ys = np.cos(xs) + 5 -cosine = np.dstack([xs, ys])[0] +ys = np.cos(xs) +cosine = np.column_stack([xs, ys]) # sinc function a = 0.5 -ys = np.sinc(xs) * 3 + 8 -sinc = np.dstack([xs, ys])[0] +ys = np.sinc(xs) * 3 +sinc = np.column_stack([xs, ys]) -sine_graphic = fig[0, 0].add_line(data=sine, thickness=5, colors="magenta") +sine_graphic = fig[0, 0].add_line( + data=sine, + thickness=5, + colors="magenta" +) # you can also use colormaps for lines! -cosine_graphic = fig[0, 0].add_line(data=cosine, thickness=12, cmap="autumn") +cosine_graphic = fig[0, 0].add_line( + data=cosine, + thickness=12, + cmap="autumn", + offset=(0, 3, 0) # places the graphic at a y-axis offset of 3, offsets don't affect data +) # or a list of colors for each datapoint colors = ["r"] * 25 + ["purple"] * 25 + ["y"] * 25 + ["b"] * 25 -sinc_graphic = fig[0, 0].add_line(data=sinc, thickness=5, colors=colors) +sinc_graphic = fig[0, 0].add_line( + data=sinc, + thickness=5, + colors=colors, + offset=(0, 6, 0) +) + +zeros = np.zeros(xs.size) +zeros_data = np.column_stack([xs, zeros]) +zeros_graphic = fig[0, 0].add_line( + data=zeros_data, + thickness=8, + colors="w", + offset=(0, 10, 0) +) fig.show() @@ -42,10 +65,6 @@ cosine_graphic.colors[90:] = "red" cosine_graphic.colors[60] = "w" -# indexing to assign colormaps to entire lines or segments -sinc_graphic.cmap[10:50] = "gray" -sine_graphic.cmap = "seismic" - # more complex indexing, set the blue value directly from an array cosine_graphic.colors[65:90, 0] = np.linspace(0, 1, 90-65) @@ -53,8 +72,14 @@ key = np.array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 67, 19]) sinc_graphic.colors[key] = "Red" -key2 = np.array([True, False, True, False, True, True, True, True]) -cosine_graphic.colors[key2] = "Green" +# boolean fancy indexing +zeros_graphic.colors[xs < -5] = "green" + +# assign colormap to an entire line +sine_graphic.cmap = "seismic" +# or to segments of a line +zeros_graphic.cmap[50:75] = "jet" +zeros_graphic.cmap[75:] = "viridis" fig.canvas.set_logical_size(800, 800) diff --git a/examples/desktop/line/line_dataslice.py b/examples/desktop/line/line_dataslice.py index 12a5f0f04..c2c6b9d36 100644 --- a/examples/desktop/line/line_dataslice.py +++ b/examples/desktop/line/line_dataslice.py @@ -41,9 +41,9 @@ cosine_graphic.data[90:, 1] = 7 cosine_graphic.data[0] = np.array([[-10, 0, 0]]) -# additional fancy indexing using numpy -key2 = np.array([True, False, True, False, True, True, True, True]) -sinc_graphic.data[key2] = np.array([[5, 1, 2]]) +# additional fancy indexing with boolean array +bool_key = [True, True, True, False, False] * 20 +sinc_graphic.data[bool_key, 1] = 7 # y vals to 1 fig.canvas.set_logical_size(800, 800) diff --git a/examples/desktop/line/line_present_scaling.py b/examples/desktop/line/line_present_scaling.py deleted file mode 100644 index d334e6fbd..000000000 --- a/examples/desktop/line/line_present_scaling.py +++ /dev/null @@ -1,49 +0,0 @@ -""" -Line Plot -============ -Example showing present and scaling feature for lines. -""" - -# test_example = true - -import fastplotlib as fpl -import numpy as np - - -fig = fpl.Figure() - -xs = np.linspace(-10, 10, 100) -# sine wave -ys = np.sin(xs) -sine = np.dstack([xs, ys])[0] - -# cosine wave -ys = np.cos(xs) + 5 -cosine = np.dstack([xs, ys])[0] - -# sinc function -a = 0.5 -ys = np.sinc(xs) * 3 + 8 -sinc = np.dstack([xs, ys])[0] - -sine_graphic = fig[0, 0].add_line(data=sine, thickness=5, colors="magenta") - -# you can also use colormaps for lines! -cosine_graphic = fig[0, 0].add_line(data=cosine, thickness=12, cmap="autumn") - -# or a list of colors for each datapoint -colors = ["r"] * 25 + ["purple"] * 25 + ["y"] * 25 + ["b"] * 25 -sinc_graphic = fig[0, 0].add_line(data=sinc, thickness=5, colors=colors) - -fig.show() - -sinc_graphic.present = False - -fig.canvas.set_logical_size(800, 800) - -fig[0, 0].auto_scale() - - -if __name__ == "__main__": - print(__doc__) - fpl.run() diff --git a/examples/desktop/line_collection/line_collection.py b/examples/desktop/line_collection/line_collection.py index dd6f3ca33..db99e32ed 100644 --- a/examples/desktop/line_collection/line_collection.py +++ b/examples/desktop/line_collection/line_collection.py @@ -1,6 +1,6 @@ """ -Line Plot -============ +Line collection +=============== Example showing how to plot line collections """ diff --git a/examples/desktop/line_collection/line_collection_cmap_values.py b/examples/desktop/line_collection/line_collection_cmap_values.py index 9eeef40f8..5ffc032e9 100644 --- a/examples/desktop/line_collection/line_collection_cmap_values.py +++ b/examples/desktop/line_collection/line_collection_cmap_values.py @@ -1,7 +1,7 @@ """ -Line Plot -============ -Example showing how to plot line collections +Line collections quantitative cmap +================================== +Example showing a line collection with a quantitative cmap """ # test_example = true @@ -36,10 +36,7 @@ def make_circle(center, radius: float, n_points: int = 75) -> np.ndarray: fig = fpl.Figure() fig[0, 0].add_line_collection( - circles, - cmap="bwr", - cmap_values=cmap_values, - thickness=10 + circles, cmap="bwr", cmap_transform=cmap_values, thickness=10 ) fig.show() diff --git a/examples/desktop/line_collection/line_collection_cmap_values_qualitative.py b/examples/desktop/line_collection/line_collection_cmap_values_qualitative.py index 85f0724d8..f96fd3aac 100644 --- a/examples/desktop/line_collection/line_collection_cmap_values_qualitative.py +++ b/examples/desktop/line_collection/line_collection_cmap_values_qualitative.py @@ -1,7 +1,7 @@ """ -Line Plot -============ -Example showing how to plot line collections +Line collections qualitative cmaps +================================== +Example showing a line collection with a qualitative cmap """ # test_example = true @@ -44,7 +44,7 @@ def make_circle(center, radius: float, n_points: int = 75) -> np.ndarray: fig[0, 0].add_line_collection( circles, cmap="tab10", - cmap_values=cmap_values, + cmap_transform=cmap_values, thickness=10 ) diff --git a/examples/desktop/line_collection/line_collection_colors.py b/examples/desktop/line_collection/line_collection_colors.py index d53afcd5b..3ee561d8f 100644 --- a/examples/desktop/line_collection/line_collection_colors.py +++ b/examples/desktop/line_collection/line_collection_colors.py @@ -1,7 +1,7 @@ """ -Line Plot -============ -Example showing how to plot line collections +Line collection colors +====================== +Example showing one way ot setting colors for individual lines in a collection """ # test_example = true diff --git a/examples/desktop/line_collection/line_collection_slicing.py b/examples/desktop/line_collection/line_collection_slicing.py new file mode 100644 index 000000000..9eaebdd7e --- /dev/null +++ b/examples/desktop/line_collection/line_collection_slicing.py @@ -0,0 +1,68 @@ +""" +Line collection slicing +======================= +Example showing how to slice a line collection +""" + +# test_example = true + +import numpy as np +import fastplotlib as fpl + + +xs = np.linspace(0, np.pi * 10, 100) +# sine wave +ys = np.sin(xs) + +data = np.column_stack([xs, ys]) +multi_data = np.stack([data] * 15) + + +fig = fpl.Figure() + +lines = fig[0, 0].add_line_stack( + multi_data, + thickness=[2, 10, 2, 5, 5, 5, 8, 8, 8, 9, 3, 3, 3, 4, 4], + separation=1, + metadatas=list(range(15)), # some metadata + names=list("abcdefghijklmno"), # unique name for each line +) + +print("slice a collection to return a collection indexer") +print(lines[1:5]) # lines 1, 2, 3, 4 + +print("collections supports fancy indexing!") +print(lines[::3]) + +print("fancy index using properties of individual lines!") +print(lines[lines.thickness < 3]) +print(lines[lines.metadatas > 10]) + +# set line properties, such as data +# set y-values of lines 3, 4, 5 +lines[3:6].data[:, 1] = np.cos(xs) +# set these same lines to a different color +lines[3:6].colors = "cyan" + +# setting properties using fancy indexing +# set cmap along the line collection +lines[-3:].cmap = "plasma" + +# set cmap of along a single line +lines[7].cmap = "jet" + +# fancy indexing using line properties! +lines[lines.thickness > 8].colors = "r" +lines[lines.names == "a"].colors = "b" + +# fancy index at the level of lines and individual line properties! +lines[::2].colors[::5] = "magenta" # set every 5th point of every other line to magenta +lines[3:6].colors[50:, -1] = 0.6 # set half the points alpha to 0.6 + +fig.show(maintain_aspect=False) + +fig.canvas.set_logical_size(900, 600) + +if __name__ == "__main__": + print(__doc__) + fpl.run() diff --git a/examples/desktop/line_collection/line_stack.py b/examples/desktop/line_collection/line_stack.py index cf5d933e3..676e6e5c2 100644 --- a/examples/desktop/line_collection/line_stack.py +++ b/examples/desktop/line_collection/line_stack.py @@ -1,7 +1,7 @@ """ -Line Plot -============ -Example showing how to plot line collections +Line stack +========== +Example showing how to plot a stack of lines """ # test_example = true @@ -10,17 +10,21 @@ import fastplotlib as fpl -xs = np.linspace(0, 100, 1000) +xs = np.linspace(0, np.pi * 10, 100) # sine wave -ys = np.sin(xs) * 20 +ys = np.sin(xs) -# make 25 lines -data = np.vstack([ys] * 25) +data = np.column_stack([xs, ys]) +multi_data = np.stack([data] * 10) fig = fpl.Figure() -# line stack takes all the same arguments as line collection and behaves similarly -fig[0, 0].add_line_stack(data, cmap="jet") +line_stack = fig[0, 0].add_line_stack( + multi_data, # shape: (10, 100, 2), i.e. [n_lines, n_points, xy] + cmap="jet", # applied along n_lines + thickness=5, + separation=1, # spacing between lines along the separation axis, default separation along "y" axis +) fig.show(maintain_aspect=False) diff --git a/examples/desktop/line_collection/line_stack_3d.py b/examples/desktop/line_collection/line_stack_3d.py new file mode 100644 index 000000000..41914e2d2 --- /dev/null +++ b/examples/desktop/line_collection/line_stack_3d.py @@ -0,0 +1,103 @@ +""" +Line stack 3D +============= +Example showing a 3D stack of lines with animations +""" + +# test_example = false + +import numpy as np +import fastplotlib as fpl + + +xs = np.linspace(0, np.pi * 10, 100) +# spiral +ys = np.sin(xs) +zs = np.cos(xs) + +data = np.column_stack([xs, ys, zs]) +multi_data = np.stack([data] * 10) + +# create figure to plot lines and use an orbit controller in 3D +fig = fpl.Figure(cameras="3d", controller_types="orbit") + +line_stack = fig[0, 0].add_line_stack( + multi_data, # shape: (10, 100, 2), i.e. [n_lines, n_points, xy] + cmap="jet", # applied along n_lines + thickness=3, + separation=1, # spacing between lines along the separation axis, default separation along "y" axis + name="lines", +) + + +x_increment = 0.1 + + +def animate_data(subplot): + """animate with different rates of spinning the spirals""" + global xs # x vals + global x_increment # increment + + # calculate the new data + # new a different spinning rate for each spiral + # top ones will spin faster than the bottom ones + new_xs = [xs + (factor * x_increment) for factor in np.linspace(0.5, 1.5, 10)] + y = [np.sin(x) for x in new_xs] + z = [np.cos(x) for x in new_xs] + + # iterate through collection and set data of each line + for i, line in enumerate(subplot["lines"]): + # set y and z values + line.data[:, 1:] = np.column_stack([y[i], z[i]]) + + x_increment += 0.1 + + +colors_iteration = 0 + + +def animate_colors(subplot): + """animate the colors""" + global colors_iteration + + # change the colors only on every 50th render cycle + # otherwise it just looks like flickering because it's too fast :) + if colors_iteration % 50 != 0: + colors_iteration += 1 + return + + # use cmap_transform to shift the cmap + cmap_transform = np.roll(np.arange(10), shift=int(colors_iteration / 50)) + + # set cmap with the transform + subplot["lines"].cmap = "jet", cmap_transform + + colors_iteration += 1 + + +fig[0, 0].add_animations(animate_data, animate_colors) + +# just a pre-saved camera state +camera_state = { + "position": np.array([-18.0, 9.0, 8.0]), + "rotation": np.array([0.00401791, -0.5951809, 0.00297593, 0.80357619]), + "scale": np.array([1.0, 1.0, 1.0]), + "reference_up": np.array([0.0, 1.0, 0.0]), + "fov": 50.0, + "width": 32, + "height": 20, + "zoom": 1, + "maintain_aspect": True, + "depth_range": None, +} + +fig.show(maintain_aspect=False) + +fig[0, 0].camera.set_state(camera_state) + +fig.canvas.set_logical_size(500, 500) + + +if __name__ == "__main__": + print(__doc__) + fpl.run() diff --git a/examples/desktop/scatter/scatter_cmap.py b/examples/desktop/scatter/scatter_cmap.py index f1bba98c3..58c43c0ea 100644 --- a/examples/desktop/scatter/scatter_cmap.py +++ b/examples/desktop/scatter/scatter_cmap.py @@ -21,7 +21,11 @@ agg.fit_predict(data) scatter_graphic = fig[0, 0].add_scatter( - data=data[:, :-1], sizes=15, alpha=0.7, cmap="Set1", cmap_values=agg.labels_ + data=data[:, :-1], # use only xy data + sizes=15, + alpha=0.7, + cmap="Set1", + cmap_transform=agg.labels_ # use the labels as a transform to map colors from the colormap ) fig.show() diff --git a/examples/desktop/scatter/scatter_colorslice.py b/examples/desktop/scatter/scatter_colorslice.py index 43f405b06..60433b5f5 100644 --- a/examples/desktop/scatter/scatter_colorslice.py +++ b/examples/desktop/scatter/scatter_colorslice.py @@ -19,7 +19,12 @@ n_points = 50 colors = ["yellow"] * n_points + ["cyan"] * n_points + ["magenta"] * n_points -scatter_graphic = fig[0, 0].add_scatter(data=data[:, :-1], sizes=6, alpha=0.7, colors=colors) +scatter_graphic = fig[0, 0].add_scatter( + data=data[:, :-1], + sizes=6, + alpha=0.7, + colors=colors # use colors from the list of strings +) fig.show() diff --git a/examples/desktop/scatter/scatter_present.py b/examples/desktop/scatter/scatter_present.py deleted file mode 100644 index 5da4610bd..000000000 --- a/examples/desktop/scatter/scatter_present.py +++ /dev/null @@ -1,38 +0,0 @@ -""" -Scatter Plot -============ -Example showing present feature for scatter plot. -""" - -# test_example = true - -import fastplotlib as fpl -import numpy as np -from pathlib import Path - - -fig = fpl.Figure() - -data_path = Path(__file__).parent.parent.joinpath("data", "iris.npy") -data = np.load(data_path) - -n_points = 50 -colors = ["yellow"] * n_points + ["cyan"] * n_points + ["magenta"] * n_points - -scatter_graphic = fig[0, 0].add_scatter(data=data[:, :-1], sizes=6, alpha=0.7, colors=colors) - -colors = ["red"] * n_points + ["white"] * n_points + ["blue"] * n_points -scatter_graphic2 = fig[0, 0].add_scatter(data=data[:, 1:], sizes=6, alpha=0.7, colors=colors) - -fig.show() - -fig.canvas.set_logical_size(800, 800) - -fig[0, 0].auto_scale() - -scatter_graphic.present = False - - -if __name__ == "__main__": - print(__doc__) - fpl.run() diff --git a/examples/desktop/screenshots/gridplot.png b/examples/desktop/screenshots/gridplot.png index ebf2d3a97..9e81fe8c6 100644 --- a/examples/desktop/screenshots/gridplot.png +++ b/examples/desktop/screenshots/gridplot.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f972f67b8830657ab14899f749fb385a080280304377d8868e6cd39c766a0afd -size 267084 +oid sha256:462a06e9c74dc9f0958aa265349dfac9c31d77a3ab3915f85596c85f2e7a6f3a +size 266056 diff --git a/examples/desktop/screenshots/gridplot_non_square.png b/examples/desktop/screenshots/gridplot_non_square.png index bc642b729..b74be7065 100644 --- a/examples/desktop/screenshots/gridplot_non_square.png +++ b/examples/desktop/screenshots/gridplot_non_square.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:352bf94c68444a330b000d7b6b3ec51b5b694ff3a0ce810299b325315923d9af -size 175938 +oid sha256:9ab4b1f8188824b81fe29b5c6ac7177734fb2b9958133e19f02919d1da98b96c +size 174978 diff --git a/examples/desktop/screenshots/heatmap.png b/examples/desktop/screenshots/heatmap.png index a8c8b73fe..ec6cf9955 100644 --- a/examples/desktop/screenshots/heatmap.png +++ b/examples/desktop/screenshots/heatmap.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5620e4dcb964dbf3318ac77e566af395a35b9762e0687dec2e1a2864eb291fd3 -size 102994 +oid sha256:754bd8713617bf61d1adf57b3e84c1257b038bf15412aa3c8bd466d1405086e7 +size 48524 diff --git a/examples/desktop/screenshots/heatmap_cmap.png b/examples/desktop/screenshots/heatmap_cmap.png index cee81dd30..c495cf72c 100644 --- a/examples/desktop/screenshots/heatmap_cmap.png +++ b/examples/desktop/screenshots/heatmap_cmap.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8863461569f5b89d1443e3051a5512f3987487fcb9e057215d2f030a180fa09f -size 97996 +oid sha256:d2ba0b76e982ceb1439c5ebaabfaf089ea9b09e50934718eaaa29d7492272196 +size 42746 diff --git a/examples/desktop/screenshots/heatmap_data.png b/examples/desktop/screenshots/heatmap_data.png index 316a73753..229d6c2cc 100644 --- a/examples/desktop/screenshots/heatmap_data.png +++ b/examples/desktop/screenshots/heatmap_data.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a975179e82893dbb04e4674310761e7b02bb62ae6abb1b89397720bddf96ae5f -size 19084 +oid sha256:a7160c4f034214f8052a6d88001dac706b0a85a5a4df076958ba1a176344b85a +size 53854 diff --git a/examples/desktop/screenshots/heatmap_square.png b/examples/desktop/screenshots/heatmap_square.png new file mode 100644 index 000000000..00a01133e --- /dev/null +++ b/examples/desktop/screenshots/heatmap_square.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8d01171b2bd05b5c88df4312c303094fdede36b1cf930455ace6d1fb12d8eb36 +size 81274 diff --git a/examples/desktop/screenshots/heatmap_vmin_vmax.png b/examples/desktop/screenshots/heatmap_vmin_vmax.png index 357683d82..b028291f7 100644 --- a/examples/desktop/screenshots/heatmap_vmin_vmax.png +++ b/examples/desktop/screenshots/heatmap_vmin_vmax.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9592f3724016db1b7431bc100b16bec175e197c111e7b442dc2255d51da3f5e8 -size 114957 +oid sha256:61c3754de3a7e6622ce1a77dbbf9bbd6ccfd3ccad3b1463b009bf93511258034 +size 44426 diff --git a/examples/desktop/screenshots/heatmap_wide.png b/examples/desktop/screenshots/heatmap_wide.png new file mode 100644 index 000000000..927b933d6 --- /dev/null +++ b/examples/desktop/screenshots/heatmap_wide.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:271e0d769b608d0f34a153ab8b8353f1e5d127f239951fc407ccedd3eee5e2e5 +size 82687 diff --git a/examples/desktop/screenshots/image_cmap.png b/examples/desktop/screenshots/image_cmap.png index bbf51ab18..be16ba213 100644 --- a/examples/desktop/screenshots/image_cmap.png +++ b/examples/desktop/screenshots/image_cmap.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:555fd969606d0cb231ac152724f7c9717a2220ce22db663c5e7d5793f828ed34 -size 189654 +oid sha256:552a4d5141a5a87baaedd8a9d7d911dfdddee5792c024c77012665268af865e9 +size 189479 diff --git a/examples/desktop/screenshots/image_rgb.png b/examples/desktop/screenshots/image_rgb.png index 9a5082b12..8d391f07c 100644 --- a/examples/desktop/screenshots/image_rgb.png +++ b/examples/desktop/screenshots/image_rgb.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:95f3cae6caf8d64d1a6b4799df52dc61cc05bd6b6ea465edbec06a9678f32435 -size 218089 +oid sha256:55e76cea92eb34e1e25d730d2533a9a0d845921e78bc980708d320bb353a2d73 +size 218413 diff --git a/examples/desktop/screenshots/image_rgbvminvmax.png b/examples/desktop/screenshots/image_rgbvminvmax.png index 00bbdc0c5..eabe85d28 100644 --- a/examples/desktop/screenshots/image_rgbvminvmax.png +++ b/examples/desktop/screenshots/image_rgbvminvmax.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4fc06b8cdd72040cf2ffc44cde80d5ae21ca392daac25d79fe175b5865b13552 -size 34894 +oid sha256:7ee27d89170b30a3da7fe6d752961b30e17712d7905d8fa0686f9597debe68f9 +size 34620 diff --git a/examples/desktop/screenshots/image_simple.png b/examples/desktop/screenshots/image_simple.png index 94fcd3061..853eb2f01 100644 --- a/examples/desktop/screenshots/image_simple.png +++ b/examples/desktop/screenshots/image_simple.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3dcfb5d48d0e4db920c33ee725e2c66f3c8e04a66e03d283a6481f42a4121a16 -size 190178 +oid sha256:e943bd3b1e00acaed274dd185f5362210e39330e0f541db9ceee489fa0a9a344 +size 189822 diff --git a/examples/desktop/screenshots/image_vminvmax.png b/examples/desktop/screenshots/image_vminvmax.png index 00bbdc0c5..eabe85d28 100644 --- a/examples/desktop/screenshots/image_vminvmax.png +++ b/examples/desktop/screenshots/image_vminvmax.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4fc06b8cdd72040cf2ffc44cde80d5ae21ca392daac25d79fe175b5865b13552 -size 34894 +oid sha256:7ee27d89170b30a3da7fe6d752961b30e17712d7905d8fa0686f9597debe68f9 +size 34620 diff --git a/examples/desktop/screenshots/line_collection_slicing.png b/examples/desktop/screenshots/line_collection_slicing.png new file mode 100644 index 000000000..ba4170874 --- /dev/null +++ b/examples/desktop/screenshots/line_collection_slicing.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:01090d611117fd0d2b3f9971e359871c9598a634a1829e74848b1c78a770d437 +size 131764 diff --git a/examples/desktop/screenshots/line_colorslice.png b/examples/desktop/screenshots/line_colorslice.png index 3d04c473f..789265530 100644 --- a/examples/desktop/screenshots/line_colorslice.png +++ b/examples/desktop/screenshots/line_colorslice.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:aa941eaf5b940b4eebab89ed836cbd092e16b4758abafa3722c296db65c0c4b5 -size 33233 +oid sha256:25e87f566a667c98b54d4acdf115d16b486e047242b9ce8b141e5724b9d0a46a +size 33191 diff --git a/examples/desktop/screenshots/line_dataslice.png b/examples/desktop/screenshots/line_dataslice.png index 0863751bf..e55a6111e 100644 --- a/examples/desktop/screenshots/line_dataslice.png +++ b/examples/desktop/screenshots/line_dataslice.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:78ccd51d1891fb6a345cb2885a341f276d8ad7a6fa506deda6cae6ef14c64094 -size 45843 +oid sha256:e76275ea6f5719e16ff0ef3401dc33fe4b70c4c9010b3b673fca26812f33b9e8 +size 46400 diff --git a/examples/desktop/screenshots/line_present_scaling.png b/examples/desktop/screenshots/line_present_scaling.png deleted file mode 100644 index ba7142106..000000000 --- a/examples/desktop/screenshots/line_present_scaling.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:06f7dd45eb495fecfcf46478c6430a658640ceb2855c4797bc184cf4134571e3 -size 20180 diff --git a/examples/desktop/screenshots/line_stack.png b/examples/desktop/screenshots/line_stack.png index c13f05f04..29d941fd4 100644 --- a/examples/desktop/screenshots/line_stack.png +++ b/examples/desktop/screenshots/line_stack.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5480aefe6e723863b919a4eeb4755310fe7036b27beb8e2e2402e04943ee8c1e -size 201102 +oid sha256:73226917c233f3fd3d7ec0b40d5a7ded904d275c871242dc0578bddf4c19d0bd +size 93687 diff --git a/examples/desktop/screenshots/scatter_cmap.png b/examples/desktop/screenshots/scatter_cmap.png index 87a6e0ded..560f1942d 100644 --- a/examples/desktop/screenshots/scatter_cmap.png +++ b/examples/desktop/screenshots/scatter_cmap.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a02d2b5d4735d656d1b754ac3681a7700d961d7e4a43dfaf3a7dd0d4f6516ba6 -size 37808 +oid sha256:9479bb3995bd145a163a2f25592a4c85c52c663d33381efee7743ffc1f16aef1 +size 32894 diff --git a/examples/desktop/screenshots/scatter_present.png b/examples/desktop/screenshots/scatter_present.png deleted file mode 100644 index 08bc610b3..000000000 --- a/examples/desktop/screenshots/scatter_present.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:bd072918f21ed0ce4ea4e1f4499ec1ff66d867cfdc0ecd6b3ed8092141cd348e -size 14195 diff --git a/examples/notebooks/heatmap.ipynb b/examples/notebooks/heatmap.ipynb index 90c07a3cb..7de3af2a0 100644 --- a/examples/notebooks/heatmap.ipynb +++ b/examples/notebooks/heatmap.ipynb @@ -5,9 +5,7 @@ "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`" + "# Looking at very large arrays" ] }, { @@ -40,13 +38,11 @@ }, "outputs": [], "source": [ - "xs = np.linspace(0, 50, 10_000)\n", - "\n", - "sine_data = np.sin(xs)\n", + "xs = np.linspace(0, 1_000, 20_000)\n", "\n", - "cosine_data = np.cos(xs)\n", + "sine = np.sin(np.sqrt(xs))\n", "\n", - "data = np.vstack([(sine_data, cosine_data) for i in range(5)])" + "data = np.vstack([sine * i for i in range(10_000)])" ] }, { @@ -72,7 +68,7 @@ "source": [ "fig = fpl.Figure()\n", "\n", - "fig[0, 0].add_heatmap(data, cmap=\"viridis\")\n", + "fig[0, 0].add_image(data, cmap=\"viridis\")\n", "\n", "fig.show(maintain_aspect=False)" ] diff --git a/examples/notebooks/linear_region_selector.ipynb b/examples/notebooks/linear_region_selector.ipynb index 2ba40ed54..57a72bdec 100644 --- a/examples/notebooks/linear_region_selector.ipynb +++ b/examples/notebooks/linear_region_selector.ipynb @@ -17,7 +17,7 @@ "source": [ "import fastplotlib as fpl\n", "import numpy as np\n", - "from ipywidgets import IntRangeSlider, FloatRangeSlider, VBox\n", + "# from ipywidgets import IntRangeSlider, FloatRangeSlider, VBox\n", "\n", "fig = fpl.Figure((2, 2))\n", "\n", @@ -25,11 +25,12 @@ "zoomed_prealloc = 1_000\n", "\n", "# data to plot\n", - "xs = np.linspace(0, 100, 1_000)\n", - "sine = np.sin(xs) * 20\n", + "xs = np.linspace(0, 10* np.pi, 1_000)\n", + "sine = np.sin(xs)\n", + "sine += 100\n", "\n", "# make sine along x axis\n", - "sine_graphic_x = fig[0, 0].add_line(sine)\n", + "sine_graphic_x = fig[0, 0].add_line(np.column_stack([xs, sine]), offset=(10, 0, 0))\n", "\n", "# just something that looks different for line along y-axis\n", "sine_y = sine\n", @@ -47,7 +48,7 @@ "ls_y = sine_graphic_y.add_linear_region_selector(axis=\"y\")\n", "\n", "# preallocate array for storing zoomed in data\n", - "zoomed_init = np.column_stack([np.arange(zoomed_prealloc), np.random.rand(zoomed_prealloc)])\n", + "zoomed_init = np.column_stack([np.arange(zoomed_prealloc), np.zeros(zoomed_prealloc)])\n", "\n", "# make line graphics for displaying zoomed data\n", "zoomed_x = fig[1, 0].add_line(zoomed_init)\n", @@ -62,62 +63,54 @@ " # interpolate to preallocated size\n", " return np.interp(x, xp, fp=subdata[:, axis]) # use the y-values\n", "\n", - "\n", + "@ls_x.add_event_handler(\"selection\")\n", "def set_zoom_x(ev):\n", " \"\"\"sets zoomed x selector data\"\"\"\n", - " selected_data = ev.pick_info[\"selected_data\"]\n", - " zoomed_x.data = interpolate(selected_data, axis=1) # use the y-values\n", + " # get the selected data\n", + " selected_data = ev.get_selected_data()\n", + " if selected_data.size == 0:\n", + " # no data selected\n", + " zoomed_x.data[:, 1] = 0\n", + "\n", + " # set the y-values\n", + " zoomed_x.data[:, 1] = interpolate(selected_data, axis=1)\n", " fig[1, 0].auto_scale()\n", "\n", "\n", "def set_zoom_y(ev):\n", - " \"\"\"sets zoomed y selector data\"\"\"\n", - " selected_data = ev.pick_info[\"selected_data\"]\n", - " zoomed_y.data = -interpolate(selected_data, axis=0) # use the x-values\n", + " \"\"\"sets zoomed x selector data\"\"\"\n", + " # get the selected data\n", + " selected_data = ev.get_selected_data()\n", + " if selected_data.size == 0:\n", + " # no data selected\n", + " zoomed_y.data[:, 0] = 0\n", + "\n", + " # set the x-values\n", + " zoomed_y.data[:, 0] = -interpolate(selected_data, axis=1)\n", " fig[1, 1].auto_scale()\n", "\n", "\n", - "# update zoomed plots when bounds change\n", - "ls_x.selection.add_event_handler(set_zoom_x)\n", - "ls_y.selection.add_event_handler(set_zoom_y)\n", - "\n", - "fig.show()" - ] - }, - { - "cell_type": "markdown", - "id": "0bad4a35-f860-4f85-9061-920154ab682b", - "metadata": {}, - "source": [ - "### On the x-axis we have a 1-1 mapping from the data that we have passed and the line geometry positions. So the `bounds` min max corresponds directly to the data indices." + "fig.show(maintain_aspect=False)" ] }, { "cell_type": "code", "execution_count": null, - "id": "2c96a3ff-c2e7-4683-8097-8491e97dd6d3", + "id": "2f29e913-c4f8-44a6-8692-eb14436849a5", "metadata": {}, "outputs": [], "source": [ - "ls_x.selection()" + "sine_graphic_x.data[:, 1].ptp()" ] }, { "cell_type": "code", "execution_count": null, - "id": "3ec71e3f-291c-43c6-a954-0a082ba5981c", + "id": "1947a477-5dd2-4df9-aecd-6967c6ab45fe", "metadata": {}, "outputs": [], "source": [ - "ls_x.get_selected_indices()" - ] - }, - { - "cell_type": "markdown", - "id": "1588a89e-1da4-4ada-92e2-7437ba942065", - "metadata": {}, - "source": [ - "### However, for the y-axis line we have passed a 2D array where we've used a linspace, so there is not a 1-1 mapping from the data to the line geometry positions. Use `get_selected_indices()` to get the indices of the data bounded by the current selection. In addition the position of the Graphic is not `(0, 0)`. You must use `get_selected_indices()` whenever you want the indices of the selected data." + "np.clip(-0.1, 0, 10)" ] }, { @@ -127,7 +120,7 @@ "metadata": {}, "outputs": [], "source": [ - "ls_y.selection()" + "ls_y.selection" ] }, { @@ -173,17 +166,18 @@ " subplot.add_line(zoomed_init, name=\"zoomed\")\n", "\n", "\n", + "@selector.add_event_handler(\"selection\")\n", "def update_zoomed_subplots(ev):\n", " \"\"\"update the zoomed subplots\"\"\"\n", - " zoomed_data = selector.get_selected_data()\n", + " zoomed_data = ev.get_selected_data()\n", " \n", " for i in range(len(zoomed_data)):\n", + " # interpolate y-vals\n", " data = interpolate(zoomed_data[i], axis=1)\n", - " fig_stack[i + 1, 0][\"zoomed\"].data = data\n", + " fig_stack[i + 1, 0][\"zoomed\"].data[:, 1] = data\n", " fig_stack[i + 1, 0].auto_scale()\n", "\n", "\n", - "selector.selection.add_event_handler(update_zoomed_subplots)\n", "fig_stack.show()" ] }, @@ -195,50 +189,6 @@ "# Large line stack with selector" ] }, - { - "cell_type": "code", - "execution_count": null, - "id": "d5ffb678-c989-49ee-85a9-4fd7822f033c", - "metadata": {}, - "outputs": [], - "source": [ - "import fastplotlib as fpl\n", - "import numpy as np\n", - "\n", - "# data to plot\n", - "xs = np.linspace(0, 250, 10_000)\n", - "sine = np.sin(xs) * 20\n", - "cosine = np.cos(xs) * 20\n", - "\n", - "fig_stack_large = fpl.Figure((1, 2))\n", - "\n", - "# sines and cosines\n", - "sines = [sine] * 1_00\n", - "cosines = [cosine] * 1_00\n", - "\n", - "# make line stack\n", - "line_stack = fig_stack_large[0, 0].add_line_stack(sines + cosines, separation=50)\n", - "\n", - "# make selector\n", - "stack_selector = line_stack.add_linear_region_selector(padding=200)\n", - "\n", - "zoomed_line_stack = fig_stack_large[0, 1].add_line_stack([zoomed_init] * 2_000, separation=50, name=\"zoomed\")\n", - " \n", - "def update_zoomed_stack(ev):\n", - " \"\"\"update the zoomed subplots\"\"\"\n", - " zoomed_data = stack_selector.get_selected_data()\n", - " \n", - " for i in range(len(zoomed_data)):\n", - " data = interpolate(zoomed_data[i], axis=1)\n", - " zoomed_line_stack.graphics[i].data = data\n", - " \n", - " fig_stack_large[0, 1].auto_scale()\n", - "\n", - "\n", - "stack_selector.selection.add_event_handler(update_zoomed_stack)\n", - "fig_stack_large.show()" - ] - }, { "cell_type": "code", "execution_count": null, diff --git a/examples/notebooks/linear_selector.ipynb b/examples/notebooks/linear_selector.ipynb index e9c8e664a..bac8df182 100644 --- a/examples/notebooks/linear_selector.ipynb +++ b/examples/notebooks/linear_selector.ipynb @@ -5,7 +5,7 @@ "id": "a06e1fd9-47df-42a3-a76c-19e23d7b89fd", "metadata": {}, "source": [ - "## `LinearSelector`, draggable selector that can optionally associated with an ipywidget." + "## `LinearSelector`, draggable selector that can also be linked to an ipywidget slider" ] }, { @@ -16,7 +16,6 @@ "outputs": [], "source": [ "import fastplotlib as fpl\n", - "from fastplotlib.graphics.selectors import Synchronizer\n", "\n", "import numpy as np\n", "from ipywidgets import VBox, IntSlider, FloatSlider\n", @@ -35,16 +34,14 @@ "selector2 = sine_graphic.add_linear_selector(20)\n", "selector3 = sine_graphic.add_linear_selector(40)\n", "\n", - "ss = Synchronizer(selector, selector2, selector3)\n", - "\n", + "# one of the selectors will change the line colors when it moves\n", + "@selector.add_event_handler(\"selection\")\n", "def set_color_at_index(ev):\n", " # changes the color at the index where the slider is\n", - " ix = ev.pick_info[\"selected_index\"]\n", - " g = ev.pick_info[\"graphic\"].parent\n", + " ix = ev.get_selected_index()\n", + " g = ev.graphic.parent\n", " g.colors[ix] = \"green\"\n", "\n", - "selector.selection.add_event_handler(set_color_at_index)\n", - "\n", "# fastplotlib LineSelector can make an ipywidget slider and return it :D \n", "ipywidget_slider = selector.make_ipywidget_slider()\n", "ipywidget_slider.description = \"slider1\"\n", @@ -57,7 +54,15 @@ "selector3.add_ipywidget_handler(ipywidget_slider3, step=0.1)\n", "\n", "fig[0, 0].auto_scale()\n", - "fig.show(add_widgets=[ipywidget_slider])" + "VBox([fig.show(), ipywidget_slider, ipywidget_slider2, ipywidget_slider3])" + ] + }, + { + "cell_type": "markdown", + "id": "d83caca6-e9b6-45df-b93c-0dfe0498d20e", + "metadata": {}, + "source": [ + "Double click the first selctor, and then use `Shift` + Right/Left Arrow Key to move it!" ] }, { @@ -67,13 +72,16 @@ "metadata": {}, "outputs": [], "source": [ + "# this controls the step-size of arrow key movements\n", "selector.step = 0.1" ] }, { "cell_type": "markdown", "id": "3b0f448f-bbe4-4b87-98e3-093f561c216c", - "metadata": {}, + "metadata": { + "jp-MarkdownHeadingCollapsed": true + }, "source": [ "### Drag linear selectors with the mouse, hold \"Shift\" to synchronize movement of all the selectors" ] @@ -105,8 +113,6 @@ "for i, c in enumerate(colors):\n", " sel = sine_stack.add_linear_selector(i * 100, color=c, name=str(i))\n", " selectors.append(sel)\n", - " \n", - "ss = Synchronizer(*selectors)\n", "\n", "fig.show()" ] diff --git a/examples/notebooks/lines_cmap.ipynb b/examples/notebooks/lines_cmap.ipynb index dbcbb3e16..3ceb25326 100644 --- a/examples/notebooks/lines_cmap.ipynb +++ b/examples/notebooks/lines_cmap.ipynb @@ -39,11 +39,11 @@ "xs = np.linspace(-10, 10, 100)\n", "# sine wave\n", "ys = np.sin(xs)\n", - "sine = np.dstack([xs, ys])[0]\n", + "sine = np.column_stack([xs, ys])\n", "\n", "# cosine wave\n", "ys = np.cos(xs)\n", - "cosine = np.dstack([xs, ys])[0]" + "cosine = np.column_stack([xs, ys])" ] }, { @@ -107,6 +107,14 @@ "plot_test(\"lines-cmap-jet\", fig)" ] }, + { + "cell_type": "markdown", + "id": "13c1c034-2b3b-4568-b979-7c0bbea698ae", + "metadata": {}, + "source": [ + "map colors from sine data values by setting the cmap transform" + ] + }, { "cell_type": "code", "execution_count": null, @@ -116,7 +124,7 @@ }, "outputs": [], "source": [ - "fig[0, 0].graphics[0].cmap.values = sine[:, 1]" + "fig[0, 0].graphics[0].cmap.transform = sine[:, 1]" ] }, { @@ -141,7 +149,8 @@ }, "outputs": [], "source": [ - "fig[0, 0].graphics[0].cmap.values = cosine[:, 1]" + "# set transform from cosine\n", + "fig[0, 0].graphics[0].cmap.transform = cosine[:, 1]" ] }, { @@ -166,6 +175,7 @@ }, "outputs": [], "source": [ + "# change cmap\n", "fig[0, 0].graphics[0].cmap = \"viridis\"" ] }, @@ -182,6 +192,14 @@ "plot_test(\"lines-cmap-viridis\", fig)" ] }, + { + "cell_type": "markdown", + "id": "1f52bfdc-8151-4bab-973c-1bac36011802", + "metadata": {}, + "source": [ + "use cmap transform to map for a qualitative transform" + ] + }, { "cell_type": "code", "execution_count": null, @@ -191,7 +209,7 @@ }, "outputs": [], "source": [ - "cmap_values = [0] * 25 + [1] * 5 + [2] * 50 + [3] * 20" + "cmap_transform = [0] * 25 + [1] * 5 + [2] * 50 + [3] * 20" ] }, { @@ -203,7 +221,7 @@ }, "outputs": [], "source": [ - "fig[0, 0].graphics[0].cmap.values = cmap_values" + "fig[0, 0].graphics[0].cmap.transform = cmap_transform" ] }, { diff --git a/examples/notebooks/quickstart.ipynb b/examples/notebooks/quickstart.ipynb index 9bfd822ab..5c5040418 100644 --- a/examples/notebooks/quickstart.ipynb +++ b/examples/notebooks/quickstart.ipynb @@ -156,7 +156,9 @@ }, "outputs": [], "source": [ - "image_graphic.data().shape" + "# some graphic properties behave like arrays\n", + "# access the underlying array using .values\n", + "image_graphic.data.value.shape" ] }, { @@ -209,8 +211,8 @@ }, "outputs": [], "source": [ - "image_graphic.cmap.vmin = 50\n", - "image_graphic.cmap.vmax = 150" + "image_graphic.vmin = 50\n", + "image_graphic.vmax = 150" ] }, { @@ -301,7 +303,7 @@ }, "outputs": [], "source": [ - "image_graphic.cmap.reset_vmin_vmax()" + "image_graphic.reset_vmin_vmax()" ] }, { @@ -500,7 +502,7 @@ }, "outputs": [], "source": [ - "fig_rgb[0, 0][\"rgb-image\"].cmap.vmin = 100" + "fig_rgb[0, 0][\"rgb-image\"].vmin = 100" ] }, { @@ -893,72 +895,6 @@ "plot_test(\"lines-data\", fig_lines)" ] }, - { - "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": "763b9943-53a4-4e2a-b47a-4e9e5c9d7be3", - "metadata": {}, - "outputs": [], - "source": [ - "sinc_graphic.present = True" - ] - }, - { - "cell_type": "markdown", - "id": "86f4e535-ce88-415a-b8d2-53612a2de7b9", - "metadata": {}, - "source": [ - "### You can create callbacks to this 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(subplot.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": "f05981c3-c768-4631-ae62-6a8407b20c4c", - "metadata": {}, - "outputs": [], - "source": [ - "sinc_graphic.present = True" - ] - }, { "cell_type": "markdown", "id": "05f93e93-283b-45d8-ab31-8d15a7671dd2", @@ -978,12 +914,13 @@ "source": [ "img = iio.imread(\"imageio:camera.png\")\n", "\n", - "subplot.add_image(img[::20, ::20], name=\"image\", cmap=\"gray\")\n", + "subplot.add_image(\n", + " img[::20, ::20],\n", + " name=\"image\",\n", + " cmap=\"gray\",\n", + ")\n", "\n", - "# z axis position -1 so it is below all the lines\n", - "subplot[\"image\"].position_z = -1\n", - "subplot[\"image\"].position_x = -8\n", - "subplot[\"image\"].position_y = -8" + "subplot[\"image\"].offset = (-12, -10, -1)" ] }, { @@ -1282,11 +1219,8 @@ "magnetic_vectors = [np.array([[0, 0, z], [0, y, z]]) for (y, z) in zip(m_ys[::10], zs[::10])]\n", "\n", "# add as a line collection\n", - "fig_em[0, 0].add_line_collection(electric_vectors, colors=\"blue\", thickness=1.5, name=\"e-vec\", z_offset=0)\n", - "fig_em[0, 0].add_line_collection(magnetic_vectors, colors=\"red\", thickness=1.5, name=\"m-vec\", z_offset=0)\n", - "# note that the z_offset in `add_line_collection` is not data-related\n", - "# it is the z-offset for where to place the *graphic*, by default with Orthographic cameras (i.e. 2D views)\n", - "# it will increment by 1 for each line in the collection, we want to disable this so set z_position=0\n", + "fig_em[0, 0].add_line_collection(electric_vectors, colors=\"blue\", thickness=1.5, name=\"e-vec\")\n", + "fig_em[0, 0].add_line_collection(magnetic_vectors, colors=\"red\", thickness=1.5, name=\"m-vec\")\n", "\n", "# axes are a WIP, just draw a white line along z for now\n", "z_axis = np.array([[0, 0, 0], [0, 0, stop]])\n", @@ -1537,8 +1471,8 @@ "source": [ "def update_points(subplot):\n", " # move every point by a small amount\n", - " deltas = np.random.normal(size=scatter_graphic.data().shape, loc=0, scale=0.15)\n", - " scatter_graphic.data = scatter_graphic.data() + deltas \n", + " deltas = np.random.normal(size=scatter_graphic.data.value.shape, loc=0, scale=0.15)\n", + " scatter_graphic.data = scatter_graphic.data[:] + deltas\n", "\n", "subplot_scatter.add_animations(update_points)" ] @@ -2048,7 +1982,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.11.2" + "version": "3.11.3" } }, "nbformat": 4, diff --git a/examples/notebooks/scatter_sizes_animation.ipynb b/examples/notebooks/scatter_sizes_animation.ipynb index 9ca067bee..0cd301fb1 100644 --- a/examples/notebooks/scatter_sizes_animation.ipynb +++ b/examples/notebooks/scatter_sizes_animation.ipynb @@ -17,16 +17,17 @@ "size_delta_scales = np.array([10, 40, 100], dtype=np.float32)\n", "min_sizes = 6\n", "\n", + "\n", "def update_positions(subplot):\n", - " current_time = time()\n", - " newPositions = points + np.sin(((current_time / 4) % 1)*np.pi)\n", - " subplot.graphics[0].data = newPositions\n", + " g = subplot.graphics[0]\n", + " g.data[:, :-1] += np.sin(((time() / 4))*np.pi)\n", + "\n", "\n", "def update_sizes(subplot):\n", - " current_time = time()\n", - " sin_sample = np.sin(((current_time / 4) % 1)*np.pi)\n", - " size_delta = sin_sample*size_delta_scales\n", - " subplot.graphics[0].sizes = min_sizes + size_delta\n", + " sin_sample = np.abs(np.sin((time() / 1)*np.pi))\n", + " size_delta = sin_sample * size_delta_scales\n", + " subplot.graphics[0].sizes = size_delta\n", + "\n", "\n", "scatter = fig[0, 0].add_scatter(points, colors=[\"red\", \"green\", \"blue\"], sizes=12)\n", "fig[0, 0].add_animations(update_positions, update_sizes)\n", @@ -34,13 +35,6 @@ "fig[0, 0].camera.width = 12\n", "fig.show(autoscale=False)" ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [] } ], "metadata": { diff --git a/examples/notebooks/screenshots/nb-astronaut.png b/examples/notebooks/screenshots/nb-astronaut.png index 378260288..3e55979ee 100644 --- a/examples/notebooks/screenshots/nb-astronaut.png +++ b/examples/notebooks/screenshots/nb-astronaut.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e584533ea04b9758634ba62dceeb72991861c509d01dc082436c54c272686409 -size 112104 +oid sha256:a0fdb5b319347b4db4611dcf92cf08359c938f42a64b05d0dd163e0ca289e3c3 +size 112299 diff --git a/examples/notebooks/screenshots/nb-astronaut_RGB.png b/examples/notebooks/screenshots/nb-astronaut_RGB.png index bf11bf667..fbb514e3e 100644 --- a/examples/notebooks/screenshots/nb-astronaut_RGB.png +++ b/examples/notebooks/screenshots/nb-astronaut_RGB.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:db9602a610f258803d74ac03cd46447dd5a7ad62241ec26a4c3df30c1d6de299 -size 110408 +oid sha256:2d312ce9097114bc32886c0370861bcf7deebfb4fda99e03817ebec2226eabdc +size 110338 diff --git a/examples/notebooks/screenshots/nb-camera.png b/examples/notebooks/screenshots/nb-camera.png index 9db4005bc..5629bd211 100644 --- a/examples/notebooks/screenshots/nb-camera.png +++ b/examples/notebooks/screenshots/nb-camera.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4bb9080b99c2717e093bf6ae4986bf0689a8d377e137a7022c9c6929b9a335d3 -size 77965 +oid sha256:0a415917cc16f09ab7b78eea5e5579d7dd45b6d92e80d87ba0970e9dd0568eb2 +size 77419 diff --git a/examples/notebooks/screenshots/nb-image-widget-movie-set_data.png b/examples/notebooks/screenshots/nb-image-widget-movie-set_data.png index 5be8f55a3..486c89963 100644 --- a/examples/notebooks/screenshots/nb-image-widget-movie-set_data.png +++ b/examples/notebooks/screenshots/nb-image-widget-movie-set_data.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:66a310e312add59a310ff0a50335db97ac557d7f2967d8251a7d811c25a4de28 -size 40517 +oid sha256:3dbb4d04175c5603ff7e56a04438c8f0cfff7deff61889a06c342cedc04ac323 +size 43172 diff --git a/examples/notebooks/screenshots/nb-image-widget-single-gnuplot2.png b/examples/notebooks/screenshots/nb-image-widget-single-gnuplot2.png index b8bf7adeb..02423a02a 100644 --- a/examples/notebooks/screenshots/nb-image-widget-single-gnuplot2.png +++ b/examples/notebooks/screenshots/nb-image-widget-single-gnuplot2.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:dbfa1e7aeb7f0a068a33f2f11023a06f834332f7b3d8e4cf97b51222536fd6cb -size 434782 +oid sha256:d7384d1a69629cfcdbebdc9e9e6a152383446f3cb696e69a11543253cdde2e64 +size 434060 diff --git a/examples/notebooks/screenshots/nb-image-widget-single.png b/examples/notebooks/screenshots/nb-image-widget-single.png index 86119e247..408739d6e 100644 --- a/examples/notebooks/screenshots/nb-image-widget-single.png +++ b/examples/notebooks/screenshots/nb-image-widget-single.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7ceee2cdd73092cb84b4b0f2876fc08d838b8a47bb94d431a6c19c8a4793a153 -size 403521 +oid sha256:b57dffe179b6f52d204968085c885d70183d1f6a9a3f5a1dc2d005229b7acd01 +size 404179 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50-frame-apply-gaussian.png b/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50-frame-apply-gaussian.png index 82cee281f..596548486 100644 --- a/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50-frame-apply-gaussian.png +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50-frame-apply-gaussian.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:47e3e6cea0e738b2731060488886606f05595cfdfb0d81e6db1aa099dc8e3a84 -size 148181 +oid sha256:da1c660e4fb779ac6a4baed3d329cf80274981288ea076f243bb43aee7fb8eff +size 157731 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50-frame-apply-reset.png b/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50-frame-apply-reset.png index 0b7832eee..1318be413 100644 --- a/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50-frame-apply-reset.png +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50-frame-apply-reset.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0af4ceb50ed269aa80667c3012a871c87f73777cd8cb497ebb243b53932b9bad -size 72377 +oid sha256:0d4f6407c3b029b01088fab9522a21f7d90d7a87def1ecbbb45f0fb4f8508f87 +size 69106 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50-max-window-13.png b/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50-max-window-13.png index 2bc2db3a5..e5fdbdd28 100644 --- a/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50-max-window-13.png +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50-max-window-13.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9053c70da35fd42fe44a76e0ace8788ba79667b33c596409ca1e1f2f6d6ba3ad -size 195906 +oid sha256:e4176109805f4f521a1b630b68df1dce80a63b82a5ed01a6ba4c2cae0dfeb6bd +size 184423 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50-mean-window-13.png b/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50-mean-window-13.png index d5999dd0f..bf9548962 100644 --- a/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50-mean-window-13.png +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50-mean-window-13.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:77d4a8542a5507e3eda1203a6da29a2f533bbbe2988ad297948c74e44a4337ec -size 177152 +oid sha256:12df56b1045cdaddb94355b7e960aa58137a44eff4ff22aab3596c53ea7944c8 +size 179403 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50-mean-window-5.png b/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50-mean-window-5.png index 29af0398d..7b3e6bfba 100644 --- a/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50-mean-window-5.png +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50-mean-window-5.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3422923039d45b20ea6150f0ad545bdf876596ba60b156df5ec4004590a29a3e -size 139029 +oid sha256:d5fd2f0918f4a29769ebb572f5163abb52667cf24e89efdd1d99bc57a0f5f607 +size 140124 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50.png b/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50.png index bb07b8fbb..a72245f3b 100644 --- a/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50.png +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ba9e055298372238ce0cd0c5ac4d75db8cd53f3f4acffbcc22bf7d503b40ec57 -size 79174 +oid sha256:981c925f52ae8789f7f0d24ef3fe34efb7a08f221a7bc6079dd12f01099c3d25 +size 75054 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-frame-99.png b/examples/notebooks/screenshots/nb-image-widget-zfish-frame-99.png index 6e8274659..19c19dc1f 100644 --- a/examples/notebooks/screenshots/nb-image-widget-zfish-frame-99.png +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-frame-99.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c6aed15f9f1b6bae442687613c5b04621f42e78f1dbda1e3560b000d652ba0b3 -size 61523 +oid sha256:6a5ecd1f966250ead16a96df996ff39877b4ee28534b7724a4a8e1db9c8984d2 +size 58334 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50-frame-apply-gaussian.png b/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50-frame-apply-gaussian.png index 28704bd2d..bcf663279 100644 --- a/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50-frame-apply-gaussian.png +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50-frame-apply-gaussian.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:499fab9183f2528297fdfa3c96a25eb58b2376a44556d790ef06928e0379af3a -size 174612 +oid sha256:dbe1375ae8d2f348ad5d7e030fa67d45c250c6ed263c67179d542d0bd903e0d3 +size 177334 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50-frame-apply-reset.png b/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50-frame-apply-reset.png index d163fd22a..963290515 100644 --- a/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50-frame-apply-reset.png +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50-frame-apply-reset.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:72832b86f802ee90b4eb54cb64d64aff59527fe0c7dcb87a4d8ab281ad15726b -size 142136 +oid sha256:0ff341df374d816411e58c82d432e98a9f4cec1551725dafcd65cdb0c43edb12 +size 138235 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50-max-window-13.png b/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50-max-window-13.png index 52ebd8591..a049a484c 100644 --- a/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50-max-window-13.png +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50-max-window-13.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b414fbb8f6935901b65851de9c7cb37df628d7b24759dc7f407ee130389216a3 -size 371687 +oid sha256:bd4c51f7e07e46d7c660d28563aff1b7d3759387fc10db10addca29dfc0919b0 +size 365838 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50-mean-window-13.png b/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50-mean-window-13.png index 6c406a621..ada15017c 100644 --- a/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50-mean-window-13.png +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50-mean-window-13.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e0ef52156509f308d972533fb45509ba7451b4d6149400d519aae28274609e41 -size 212053 +oid sha256:1d13cc8a32b40f5c721ab30020632d7dc1c679c8c8e5857476176e986de18ad3 +size 211240 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50-mean-window-5.png b/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50-mean-window-5.png index aaed804b8..2e71fd30d 100644 --- a/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50-mean-window-5.png +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50-mean-window-5.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8fd94e597074094dc3718153c8bb94fb0b1bf58e58e34601e5c7281f938f52bd -size 200278 +oid sha256:39044f4bb54038eee17f0d75940fd848b304629858d9e747ac0c81ce076d3c25 +size 199075 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50.png b/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50.png index 3110fa7cf..690b1c578 100644 --- a/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50.png +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:76ab21314fbd7846c1b94aeeed9ef7b97be99d2f2f7f09c13c0474a889c21179 -size 159319 +oid sha256:109a4c8d708114e1b36d1a9fa129dbd9a768617baa953f37500310c0968b688a +size 154169 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-99.png b/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-99.png index 0cfad54e7..3e577698c 100644 --- a/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-99.png +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-99.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:77f247de5374a8cb7ce27a338ab8be880d53d9586b52f646b400901ba70be3aa -size 146217 +oid sha256:568ae05e8889cec56530db0149613826f2697f55d8252cffbd32ff692b565fcf +size 141338 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-grid-init-mean-window-5.png b/examples/notebooks/screenshots/nb-image-widget-zfish-grid-init-mean-window-5.png index c74807939..1ab48d117 100644 --- a/examples/notebooks/screenshots/nb-image-widget-zfish-grid-init-mean-window-5.png +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-grid-init-mean-window-5.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:33c3dfa77bbc558493634ab83fd1539ef76022a2e20d10e1353d2bd0a0e94a2c -size 183739 +oid sha256:e7847dc083d6df2b20788b42153674c557b627b75db74d9446b06e165aa5a50a +size 182713 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-grid-set_data-reset-indices-false.png b/examples/notebooks/screenshots/nb-image-widget-zfish-grid-set_data-reset-indices-false.png index a2841b1d5..0b0f05fc3 100644 --- a/examples/notebooks/screenshots/nb-image-widget-zfish-grid-set_data-reset-indices-false.png +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-grid-set_data-reset-indices-false.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:715f7909db0d374c2e618bb41f7a6341a8cc8891b1e3e8678a6f934fd71159a4 -size 127129 +oid sha256:839dd3fdc9db98d7044d88e816c179bff34f30584ba26ce7a96ea3b35fc3374e +size 122463 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-grid-set_data-reset-indices-true.png b/examples/notebooks/screenshots/nb-image-widget-zfish-grid-set_data-reset-indices-true.png index 9064f2323..534403b1e 100644 --- a/examples/notebooks/screenshots/nb-image-widget-zfish-grid-set_data-reset-indices-true.png +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-grid-set_data-reset-indices-true.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:76708e8e6e6865d700aa5286fca4d58ba4eb91f21ab3b0243bb128e9a84f063c -size 131192 +oid sha256:f7088e11517a4a78492d16746ac8101b2e5e9142ebd61966030c555ab173443e +size 126267 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-init-mean-window-5.png b/examples/notebooks/screenshots/nb-image-widget-zfish-init-mean-window-5.png index 1fbaec974..94993c688 100644 --- a/examples/notebooks/screenshots/nb-image-widget-zfish-init-mean-window-5.png +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-init-mean-window-5.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:51c62474b9ebee76242ef82a710a83b90e0183792790f6a2cd00213642b76755 -size 99519 +oid sha256:709c7ec07e3e37e0415fad3fa542729d2896e8e546b6ea8d1373e7b46005bc26 +size 97278 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-mixed-rgb-cockatoo-frame-50.png b/examples/notebooks/screenshots/nb-image-widget-zfish-mixed-rgb-cockatoo-frame-50.png index 5e0750ac8..27c693c1a 100644 --- a/examples/notebooks/screenshots/nb-image-widget-zfish-mixed-rgb-cockatoo-frame-50.png +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-mixed-rgb-cockatoo-frame-50.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7f8f74a0a5fa24e10a88d3723836306913243fa5fc23f46f44bbdae4c0209075 -size 58878 +oid sha256:c66db583e0d455319b665d35a8c5c8a5f717653c11311cdaba90e2c85e64235f +size 58941 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-mixed-rgb-cockatoo-set-data.png b/examples/notebooks/screenshots/nb-image-widget-zfish-mixed-rgb-cockatoo-set-data.png index 8df83fe33..7444d7dbf 100644 --- a/examples/notebooks/screenshots/nb-image-widget-zfish-mixed-rgb-cockatoo-set-data.png +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-mixed-rgb-cockatoo-set-data.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0809b2dda0e773b7f100386f97144c40d36d51cd935c86ef1dcd4a938fce3981 -size 56319 +oid sha256:36867cd793634d00c46c782911abae6e7c579067aeeed891e40ddedbb0c228d9 +size 56505 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-mixed-rgb-cockatoo-windowrgb.png b/examples/notebooks/screenshots/nb-image-widget-zfish-mixed-rgb-cockatoo-windowrgb.png index 5bbefc7ae..3941f3120 100644 --- a/examples/notebooks/screenshots/nb-image-widget-zfish-mixed-rgb-cockatoo-windowrgb.png +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-mixed-rgb-cockatoo-windowrgb.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a2e2e2cf7ac6be1a4fccec54494c3fd48af673765653675438fa2469c549e90c -size 55055 +oid sha256:8b23f3655fcfcd85f6138f4de5cedf228a6dbad0c8cff0c87900042f83b8f409 +size 55269 diff --git a/examples/notebooks/screenshots/nb-lines-underlay.png b/examples/notebooks/screenshots/nb-lines-underlay.png index d4b3d9f6d..e7f6aeb0c 100644 --- a/examples/notebooks/screenshots/nb-lines-underlay.png +++ b/examples/notebooks/screenshots/nb-lines-underlay.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9b12c8f29436be8d17c38f420120ab3d54b0eee9bef751eea2f99d01b1a8fa43 -size 50761 +oid sha256:8ed174c362c7e7c491eba02b32102f59423af41537577c694fdcd54d69c065b3 +size 50422 diff --git a/examples/notebooks/subplots.ipynb b/examples/notebooks/subplots.ipynb index 72b4b3007..c9774029f 100644 --- a/examples/notebooks/subplots.ipynb +++ b/examples/notebooks/subplots.ipynb @@ -136,39 +136,51 @@ "metadata": {}, "outputs": [], "source": [ - "fig[\"subplot0\"][\"rand-image\"].cmap.vmin = 0.6\n", - "fig[\"subplot0\"][\"rand-image\"].cmap.vmax = 0.8" + "fig[\"subplot0\"][\"rand-image\"].vmin = 0.6\n", + "fig[\"subplot0\"][\"rand-image\"].vmax = 0.8" ] }, { "cell_type": "markdown", + "id": "39c8a5acbad7980b", + "metadata": { + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } + }, "source": [ "If they are not named use .graphics" - ], - "metadata": { - "collapsed": false - } + ] }, { "cell_type": "code", "execution_count": null, + "id": "d27af25002237db5", + "metadata": { + "collapsed": false, + "is_executing": true, + "jupyter": { + "outputs_hidden": false + } + }, "outputs": [], "source": [ "fig[\"subplot0\"].graphics" - ], - "metadata": { - "collapsed": false, - "is_executing": true - } + ] }, { "cell_type": "markdown", + "id": "2299a8ae23e39c37", + "metadata": { + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } + }, "source": [ "### positional indexing also works" - ], - "metadata": { - "collapsed": false - } + ] }, { "cell_type": "code", @@ -177,17 +189,9 @@ "metadata": {}, "outputs": [], "source": [ - "fig[1, 0][\"rand-image\"].cmap.vim = 0.1\n", - "fig[1, 0][\"rand-image\"].cmap.vmax = 0.3" + "fig[1, 0][\"rand-image\"].vim = 0.1\n", + "fig[1, 0][\"rand-image\"].vmax = 0.3" ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "a61e34a5-ee1b-4abb-8718-ec4715ffaa52", - "metadata": {}, - "outputs": [], - "source": [] } ], "metadata": { diff --git a/examples/notebooks/subplots_simple.ipynb b/examples/notebooks/subplots_simple.ipynb index e519584d3..9ff4e4284 100644 --- a/examples/notebooks/subplots_simple.ipynb +++ b/examples/notebooks/subplots_simple.ipynb @@ -10,7 +10,7 @@ }, { "cell_type": "code", - "execution_count": 1, + "execution_count": null, "id": "5171a06e-1bdc-4908-9726-3c1fd45dbb9d", "metadata": { "ExecuteTime": { @@ -19,40 +19,7 @@ }, "tags": [] }, - "outputs": [ - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "301d76bd4c5c42c7912cdd28651e2899", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "Image(value=b'\\x89PNG\\r\\n\\x1a\\n\\x00\\x00\\x00\\rIHDR\\x00\\x00\\x01,\\x00\\x00\\x007\\x08\\x06\\x00\\x00\\x00\\xb6\\x1bw\\x99\\x…" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "name": "stderr", - "output_type": "stream", - "text": [ - "Unable to find extension: VK_EXT_swapchain_colorspace\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Available devices:\n", - "✅ (default) | AMD RADV POLARIS10 (ACO) | DiscreteGPU | Vulkan | Mesa 20.3.5 (ACO)\n", - "❗ | llvmpipe (LLVM 11.0.1, 256 bits) | CPU | Vulkan | Mesa 20.3.5 (LLVM 11.0.1)\n", - "✅ | NVIDIA GeForce RTX 3080 | DiscreteGPU | Vulkan | 530.30.02\n", - "❗ | Radeon RX 570 Series (POLARIS10, DRM 3.40.0, 5.10.0-21-amd64, LLVM 11.0.1) | Unknown | OpenGL | \n" - ] - } - ], + "outputs": [], "source": [ "import numpy as np\n", "import fastplotlib as fpl" @@ -165,7 +132,7 @@ }, "outputs": [], "source": [ - "fig[0, 1].graphics[0].cmap.vmax = 0.5" + "fig[0, 1].graphics[0].vmax = 0.5" ] }, { @@ -244,7 +211,7 @@ }, "outputs": [], "source": [ - "fig[\"top-right-plot\"][\"rand-img\"].cmap.vmin = 0.5" + "fig[\"top-right-plot\"][\"rand-img\"].vmin = 0.5" ] }, { diff --git a/examples/notebooks/test_gc.ipynb b/examples/notebooks/test_gc.ipynb index 39f964cf7..57d7bb576 100644 --- a/examples/notebooks/test_gc.ipynb +++ b/examples/notebooks/test_gc.ipynb @@ -93,12 +93,11 @@ "\n", "\n", "for g in objects:\n", - " for feature in g.feature_events:\n", - " if isinstance(g, fpl.LineCollection):\n", - " continue # skip collections for now\n", + " for feature in g._features:\n", + " # if isinstance(g, fpl.LineCollection):?\n", + " # continue # skip collections for now\n", " \n", - " f = getattr(g, feature)\n", - " f.add_event_handler(feature_changed_handler)\n", + " g.add_event_handler(feature_changed_handler, feature)\n", "\n", "fig.show()" ] @@ -136,9 +135,8 @@ "\n", "# add some events onto all the image graphics\n", "for g in iw.managed_graphics:\n", - " for f in g.feature_events:\n", - " fea = getattr(g, f)\n", - " fea.add_event_handler(feature_changed_handler)\n", + " for f in g._features:\n", + " g.add_event_handler(feature_changed_handler, f)\n", "\n", "iw.show()" ] @@ -174,6 +172,14 @@ "source": [ "test_references(old_graphics)" ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "712bb6ea-7244-4e03-8dfa-9419daa34915", + "metadata": {}, + "outputs": [], + "source": [] } ], "metadata": { @@ -192,7 +198,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.11.3" + "version": "3.11.2" } }, "nbformat": 4, diff --git a/fastplotlib/VERSION b/fastplotlib/VERSION index d9958b371..0ea3a944b 100644 --- a/fastplotlib/VERSION +++ b/fastplotlib/VERSION @@ -1 +1 @@ -0.1.0.a16 +0.2.0 diff --git a/fastplotlib/graphics/__init__.py b/fastplotlib/graphics/__init__.py index 2a008015e..ff96baa4c 100644 --- a/fastplotlib/graphics/__init__.py +++ b/fastplotlib/graphics/__init__.py @@ -1,15 +1,15 @@ from .line import LineGraphic from .scatter import ScatterGraphic -from .image import ImageGraphic, HeatmapGraphic +from .image import ImageGraphic from .text import TextGraphic from .line_collection import LineCollection, LineStack + __all__ = [ + "LineGraphic", "ImageGraphic", "ScatterGraphic", - "LineGraphic", - "HeatmapGraphic", + "TextGraphic", "LineCollection", "LineStack", - "TextGraphic", ] diff --git a/fastplotlib/graphics/_base.py b/fastplotlib/graphics/_base.py index 3a5b043f5..cab941894 100644 --- a/fastplotlib/graphics/_base.py +++ b/fastplotlib/graphics/_base.py @@ -1,22 +1,28 @@ +from collections import defaultdict +from functools import partial from typing import Any, Literal, TypeAlias import weakref -from warnings import warn -from abc import ABC, abstractmethod -from dataclasses import dataclass import numpy as np import pylinalg as la +from wgpu.gui.base import log_exception -from pygfx import WorldObject - -from ._features import GraphicFeature, PresentFeature, GraphicFeatureIndexable, Deleted +import pygfx +from ._features import ( + BufferManager, + Deleted, + Name, + Offset, + Rotation, + Visible, +) HexStr: TypeAlias = str # dict that holds all world objects for a given python kernel/session # Graphic objects only use proxies to WorldObjects -WORLD_OBJECTS: dict[HexStr, WorldObject] = dict() #: {hex id str: WorldObject} +WORLD_OBJECTS: dict[HexStr, pygfx.WorldObject] = dict() #: {hex id str: WorldObject} PYGFX_EVENTS = [ @@ -35,9 +41,11 @@ ] -class BaseGraphic: +class Graphic: + _features = {} + def __init_subclass__(cls, **kwargs): - """set the type of the graphic in lower case like "image", "line_collection", etc.""" + # set the type of the graphic in lower case like "image", "line_collection", etc. cls.type = ( cls.__name__.lower() .replace("graphic", "") @@ -45,22 +53,24 @@ def __init_subclass__(cls, **kwargs): .replace("stack", "_stack") ) + # set of all features + cls._features = { + *cls._features, + "name", + "offset", + "rotation", + "visible", + "deleted", + } super().__init_subclass__(**kwargs) - -class Graphic(BaseGraphic): - feature_events = {} - - def __init_subclass__(cls, **kwargs): - super().__init_subclass__(**kwargs) - # all graphics give off a feature event when deleted - cls.feature_events = {*cls.feature_events, "deleted"} - def __init__( self, name: str = None, + offset: np.ndarray | list | tuple = (0.0, 0.0, 0.0), + rotation: np.ndarray | list | tuple = (0.0, 0.0, 0.0, 1.0), + visible: bool = True, metadata: Any = None, - collection_index: int = None, ): """ @@ -69,6 +79,12 @@ def __init__( name: str, optional name this graphic to use it as a key to access from the plot + offset: (float, float, float), default (0., 0., 0.) + (x, y, z) vector to offset this graphic from the origin + + rotation: (float, float, float, float), default (0, 0, 0, 1) + rotation quaternion + metadata: Any, optional metadata attached to this Graphic, this is for the user to manage @@ -76,116 +92,254 @@ def __init__( if (name is not None) and (not isinstance(name, str)): raise TypeError("Graphic `name` must be of type ") - self._name = name self.metadata = metadata - self.collection_index = collection_index self.registered_callbacks = dict() - self.present = PresentFeature(parent=self) # store hex id str of Graphic instance mem location self._fpl_address: HexStr = hex(id(self)) - self.deleted = Deleted(self, False) - self._plot_area = None + # event handlers + self._event_handlers = defaultdict(set) + + # maps callbacks to their partials + self._event_handler_wrappers = defaultdict(set) + + # all the common features + self._name = Name(name) + self._deleted = Deleted(False) + self._rotation = Rotation(rotation) + self._offset = Offset(offset) + self._visible = Visible(visible) + self._block_events = False + + @property + def supported_events(self) -> tuple[str]: + """events supported by this graphic""" + return (*tuple(self._features), *PYGFX_EVENTS) + @property def name(self) -> str | None: - """str name reference for this item""" - return self._name + """Graphic name""" + return self._name.value @name.setter - def name(self, name: str): - if self.name == name: - return + def name(self, value: str): + self._name.set_value(self, value) + + @property + def offset(self) -> np.ndarray: + """Offset position of the graphic, array: [x, y, z]""" + return self._offset.value + + @offset.setter + def offset(self, value: np.ndarray | list | tuple): + self._offset.set_value(self, value) + + @property + def rotation(self) -> np.ndarray: + """Orientation of the graphic as a quaternion""" + return self._rotation.value + + @rotation.setter + def rotation(self, value: np.ndarray | list | tuple): + self._rotation.set_value(self, value) + + @property + def visible(self) -> bool: + """Whether the graphic is visible""" + return self._visible.value + + @visible.setter + def visible(self, value: bool): + self._visible.set_value(self, value) + + @property + def deleted(self) -> bool: + """used to emit an event after the graphic is deleted""" + return self._deleted.value - if not isinstance(name, str): - raise TypeError("`Graphic` name must be of type ") + @deleted.setter + def deleted(self, value: bool): + self._deleted.set_value(self, value) - if self._plot_area is not None: - self._plot_area._check_graphic_name_exists(name) + @property + def block_events(self) -> bool: + """Used to block events for a graphic and prevent recursion.""" + return self._block_events - self._name = name + @block_events.setter + def block_events(self, value: bool): + self._block_events = value @property - def world_object(self) -> WorldObject: + def world_object(self) -> pygfx.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[self._fpl_address]) - def _set_world_object(self, wo: WorldObject): + def _set_world_object(self, wo: pygfx.WorldObject): WORLD_OBJECTS[self._fpl_address] = wo - @property - def position(self) -> np.ndarray: - """position of the graphic, [x, y, z]""" - return self.world_object.world.position + self.world_object.visible = self.visible - @property - def position_x(self) -> float: - """x-axis position of the graphic""" - return self.world_object.world.x + # set offset if it's not (0., 0., 0.) + if not all(self.world_object.world.position == self.offset): + self.offset = self.offset - @property - def position_y(self) -> float: - """y-axis position of the graphic""" - return self.world_object.world.y + # set rotation if it's not (0., 0., 0., 1.) + if not all(self.world_object.world.rotation == self.rotation): + self.rotation = self.rotation + + def unshare_property(self, feature: str): + raise NotImplementedError + + def share_property(self, feature: BufferManager): + raise NotImplementedError @property - def position_z(self) -> float: - """z-axis position of the graphic""" - return self.world_object.world.z + def event_handlers(self) -> list[tuple[str, callable, ...]]: + """ + Registered event handlers. Read-only use ``add_event_handler()`` + and ``remove_event_handler()`` to manage callbacks + """ + return list(self._event_handlers.items()) - @position.setter - def position(self, val): - self.world_object.world.position = val + def add_event_handler(self, *args): + """ + Register an event handler. - @position_x.setter - def position_x(self, val): - self.world_object.world.x = val + Parameters + ---------- + callback: callable, the first argument + Event handler, must accept a single event argument + *types: list of strings + A list of event types, ex: "click", "data", "colors", "pointer_down" - @position_y.setter - def position_y(self, val): - self.world_object.world.y = val + For the available renderer event types, see + https://jupyter-rfb.readthedocs.io/en/stable/events.html - @position_z.setter - def position_z(self, val): - self.world_object.world.z = val + All feature support events, i.e. ``graphic.features`` will give a set of + all features that are evented - @property - def rotation(self): - return self.world_object.local.rotation + Can also be used as a decorator. - @rotation.setter - def rotation(self, val): - self.world_object.local.rotation = val + Example + ------- - @property - def visible(self) -> bool: - """Access or change the visibility.""" - return self.world_object.visible + .. code-block:: py - @visible.setter - def visible(self, v: bool): - """Access or change the visibility.""" - self.world_object.visible = v + def my_handler(event): + print(event) - @property - def children(self) -> list[WorldObject]: - """Return the children of the WorldObject.""" - return self.world_object.children + graphic.add_event_handler(my_handler, "pointer_up", "pointer_down") - def _fpl_add_plot_area_hook(self, plot_area): - self._plot_area = plot_area + Decorator usage example: + + .. code-block:: py + + @graphic.add_event_handler("click") + def my_handler(event): + print(event) + """ + + decorating = not callable(args[0]) + callback = None if decorating else args[0] + types = args if decorating else args[1:] - def __setattr__(self, key, value): - if hasattr(self, key): - attr = getattr(self, key) - if isinstance(attr, GraphicFeature): - attr._set(value) - return + unsupported_events = [t for t in types if t not in self.supported_events] - super().__setattr__(key, value) + if len(unsupported_events) > 0: + raise TypeError( + f"unsupported events passed: {unsupported_events} for {self.__class__.__name__}\n" + f"`graphic.events` will return a tuple of supported events" + ) + + def decorator(_callback): + _callback_wrapper = partial( + self._handle_event, _callback + ) # adds graphic instance as attribute and other things + + for t in types: + # add to our record + self._event_handlers[t].add(_callback) + + if t in self._features: + # fpl feature event + feature = getattr(self, f"_{t}") + feature.add_event_handler(_callback_wrapper) + else: + # wrap pygfx event + self.world_object._event_handlers[t].add(_callback_wrapper) + + # keep track of the partial too + self._event_handler_wrappers[t].add((_callback, _callback_wrapper)) + return _callback + + if decorating: + return decorator + + return decorator(callback) + + def clear_event_handlers(self): + """clear all event handlers added to this graphic""" + for ev, handlers in self.event_handlers: + handlers = list(handlers) + for h in handlers: + self.remove_event_handler(h, ev) + + def _handle_event(self, callback, event: pygfx.Event): + """Wrap pygfx event to add graphic to pick_info""" + event.graphic = self + + if self.block_events: + return + + if event.type in self._features: + # for feature events + event._target = self.world_object + + if isinstance(event, pygfx.PointerEvent): + # map from screen to world space and data space + world_xy = self._plot_area.map_screen_to_world(event) + + # subtract offset to map to data + data_xy = world_xy - self.offset + + # append attributes + event.x_world, event.y_world = world_xy[:2] + event.x_data, event.y_data = data_xy[:2] + + with log_exception(f"Error during handling {event.type} event"): + callback(event) + + def remove_event_handler(self, callback, *types): + # remove from our record first + for t in types: + for wrapper_map in self._event_handler_wrappers[t]: + # TODO: not sure if we can handle this mapping in a better way + if wrapper_map[0] == callback: + wrapper = wrapper_map[1] + self._event_handler_wrappers[t].remove(wrapper_map) + break + else: + raise KeyError( + f"event type: {t} with callback: {callback} is not registered" + ) + + self._event_handlers[t].remove(callback) + # remove callback wrapper from world object if pygfx event + if t in PYGFX_EVENTS: + print("pygfx event") + print(wrapper) + self.world_object.remove_event_handler(wrapper, t) + else: + feature = getattr(self, f"_{t}") + feature.remove_event_handler(wrapper) + + def _fpl_add_plot_area_hook(self, plot_area): + self._plot_area = plot_area def __repr__(self): rval = f"{self.__class__.__name__} @ {hex(id(self))}" @@ -211,6 +365,9 @@ def _fpl_cleanup(self): Optionally implemented in subclasses """ + # remove event handlers + self.clear_event_handlers() + # clear any attached event handlers and animation functions for attr in dir(self): try: @@ -237,9 +394,8 @@ def _fpl_cleanup(self): self.world_object._event_handlers.clear() - feature_names = getattr(self, "feature_events") - for n in feature_names: - fea = getattr(self, n) + for n in self._features: + fea = getattr(self, f"_{n}") fea.clear_event_handlers() def __del__(self): @@ -267,443 +423,3 @@ def rotate(self, alpha: float, axis: Literal["x", "y", "z"] = "y"): f"`axis` must be either `x`, `y`, or `z`. `{axis}` provided instead!" ) self.rotation = la.quat_mul(rot, self.rotation) - - -class Interaction(ABC): - """Mixin class that makes graphics interactive""" - - @abstractmethod - def set_feature(self, feature: str, new_data: Any, indices: Any): - pass - - @abstractmethod - 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, - ): - """ - Link this graphic to another graphic upon an ``event_type`` to change the ``feature`` - of a ``target`` graphic. - - Parameters - ---------- - event_type: str - can be a pygfx event ("key_down", "key_up","pointer_down", "pointer_move", "pointer_up", - "pointer_enter", "pointer_leave", "click", "double_click", "wheel", "close", "resize") - 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 - | ''source'' - this graphic instance - | ''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 - - Returns - ------- - None - - """ - if event_type in PYGFX_EVENTS: - self.world_object.add_event_handler(self._event_handler, event_type) - - # make sure event is valid - elif event_type in self.feature_events: - if isinstance(self, GraphicCollection): - feature_instance = getattr(self[:], event_type) - else: - feature_instance = getattr(self, event_type) - - feature_instance.add_event_handler(self._event_handler) - - else: - 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}" - ) - - 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, - ) - - 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" - ) - return - - self.registered_callbacks[event_type].append(callback_data) - - if bidirectional: - if event_type in PYGFX_EVENTS: - warn("cannot use bidirectional link for pygfx events") - return - - target.link( - event_type=event_type, - target=self, - feature=feature, - 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. - ) - - def _event_handler(self, event): - """Handles the event after it occurs when two graphic have been linked together.""" - if event.type in self.registered_callbacks.keys(): - 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, - ) - - elif isinstance(self, GraphicCollection): - # if target is a GraphicCollection, then indices will be stored in collection_index - if event.type in self.feature_events: - indices = event.pick_info["collection-index"] - - # for now we only have line collections so this works - else: - # get index of world object that made this event - for i, item in enumerate(self.graphics): - wo = WORLD_OBJECTS[item._fpl_address] - # we only store hex id of worldobject, but worldobject `pick_info` is always the real object - # so if pygfx worldobject triggers an event by itself, such as `click`, etc., this will be - # 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, - ) - 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, - ) - - -@dataclass -class CallbackData: - """Class for keeping track of the info necessary for interactivity after event occurs.""" - - target: Any - feature: str - new_data: Any - callback_function: callable = None - - def __eq__(self, other): - if not isinstance(other, CallbackData): - raise TypeError("Can only compare against other types") - - if other.target is not self.target: - return False - - if not other.feature == self.feature: - return False - - if not other.new_data == self.new_data: - return False - - if (self.callback_function is None) and (other.callback_function is None): - return True - - if other.callback_function is self.callback_function: - return True - - else: - return False - - -@dataclass -class PreviouslyModifiedData: - """Class for keeping track of previously modified data at indices""" - - data: Any - indices: Any - - -# Dict that holds all collection graphics in one python instance -COLLECTION_GRAPHICS: dict[HexStr, Graphic] = dict() - - -class GraphicCollection(Graphic): - """Graphic Collection base class""" - - def __init__(self, name: str = None): - super().__init__(name) - self._graphics: list[str] = list() - - self._graphics_changed: bool = True - self._graphics_array: np.ndarray[Graphic] = None - - @property - 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[addr]) for addr in self._graphics - ] - self._graphics_array = np.array(proxies) - self._graphics_array.flags["WRITEABLE"] = False - self._graphics_changed = False - - return self._graphics_array - - def add_graphic(self, graphic: Graphic, reset_index: False): - """ - 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__}, " - f"you are trying to add a {graphic.__class__.__name__}." - ) - - addr = graphic._fpl_address - COLLECTION_GRAPHICS[addr] = graphic - - self._graphics.append(addr) - - if reset_index: - self._reset_index() - elif graphic.collection_index is None: - graphic.collection_index = len(self) - - self.world_object.add(graphic.world_object) - - self._graphics_changed = True - - def remove_graphic(self, graphic: Graphic, reset_index: True): - """ - 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._fpl_address) - - if reset_index: - self._reset_index() - - self.world_object.remove(graphic.world_object) - - self._graphics_changed = True - - def __getitem__(self, key): - return CollectionIndexer( - parent=self, - selection=self.graphics[key], - ) - - def __del__(self): - self.world_object.clear() - - for addr in self._graphics: - del COLLECTION_GRAPHICS[addr] - - super().__del__() - - def _reset_index(self): - for new_index, graphic in enumerate(self._graphics): - graphic.collection_index = new_index - - def __len__(self): - return len(self._graphics) - - def __repr__(self): - rval = super().__repr__() - return f"{rval}\nCollection of <{len(self._graphics)}> Graphics" - - -class CollectionIndexer: - """Collection Indexer""" - - def __init__( - self, - parent: GraphicCollection, - selection: list[Graphic], - ): - """ - - Parameters - ---------- - parent: GraphicCollection - the GraphicCollection object that is being indexed - - selection: list of Graphics - a list of the selected Graphics from the parent GraphicCollection based on the ``selection_indices`` - - """ - - self._parent = weakref.proxy(parent) - self._selection = selection - - # we use parent.graphics[0] instead of selection[0] - # because the selection can be empty - for attr_name in self._parent.graphics[0].__dict__.keys(): - attr = getattr(self._parent.graphics[0], attr_name) - if isinstance(attr, GraphicFeature): - collection_feature = CollectionFeature( - self._selection, feature=attr_name - ) - collection_feature.__doc__ = ( - f"indexable <{attr_name}> feature for collection" - ) - setattr(self, attr_name, collection_feature) - - @property - 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): - if hasattr(self, key): - attr = getattr(self, key) - if isinstance(attr, CollectionFeature): - attr._set(value) - return - - super().__setattr__(key, value) - - 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__}" - ) - - -class CollectionFeature: - """Collection Feature""" - - def __init__(self, selection: list[Graphic], feature: str): - """ - selection: list of Graphics - a list of the selected Graphics from the parent GraphicCollection based on the ``selection_indices`` - - feature: str - feature of Graphics in the GraphicCollection being indexed - - """ - - self._selection = selection - self._feature = feature - - self._feature_instances: list[GraphicFeature] = list() - - if len(self._selection) > 0: - for graphic in self._selection: - fi = getattr(graphic, self._feature) - self._feature_instances.append(fi) - - if isinstance(fi, GraphicFeatureIndexable): - self._indexable = True - else: - self._indexable = False - else: # it's an empty selection so it doesn't really matter - self._indexable = False - - def _set(self, value): - self[:] = value - - def __getitem__(self, item): - # only for indexable graphic features - return [fi[item] for fi in self._feature_instances] - - def __setitem__(self, key, value): - if self._indexable: - for fi in self._feature_instances: - fi[key] = value - - else: - for fi in self._feature_instances: - fi._set(value) - - def add_event_handler(self, handler: callable): - """Adds an event handler to each of the selected Graphics from the parent GraphicCollection""" - for fi in self._feature_instances: - fi.add_event_handler(handler) - - def remove_event_handler(self, handler: callable): - """Removes an event handler from each of the selected Graphics of the parent GraphicCollection""" - for fi in self._feature_instances: - fi.remove_event_handler(handler) - - def block_events(self, b: bool): - """Blocks event handling from occurring.""" - for fi in self._feature_instances: - fi.block_events(b) - - def __repr__(self): - return f"Collection feature for: <{self._feature}>" diff --git a/fastplotlib/graphics/_collection_base.py b/fastplotlib/graphics/_collection_base.py new file mode 100644 index 000000000..2805c684d --- /dev/null +++ b/fastplotlib/graphics/_collection_base.py @@ -0,0 +1,388 @@ +from typing import Any +import weakref + +import numpy as np + +from ._base import HexStr, Graphic + +# Dict that holds all collection graphics in one python instance +COLLECTION_GRAPHICS: dict[HexStr, Graphic] = dict() + + +class CollectionProperties: + """ + Properties common to all Graphic Collections + + Allows getting and setting the common properties of the individual graphics in the collection + """ + + def _set_feature(self, feature, values): + if not len(values) == len(self): + raise IndexError + + for g, v in zip(self, values): + setattr(g, feature, v) + + @property + def names(self) -> np.ndarray[str | None]: + """get or set the name of the individual graphics in the collection""" + return np.asarray([g.name for g in self]) + + @names.setter + def names(self, values: np.ndarray[str] | list[str]): + self._set_feature("name", values) + + @property + def metadatas(self) -> np.ndarray[str | None]: + """get or set the metadata of the individual graphics in the collection""" + return np.asarray([g.metadata for g in self]) + + @metadatas.setter + def metadatas(self, values: np.ndarray[str] | list[str]): + self._set_feature("metadata", values) + + @property + def offsets(self) -> np.ndarray: + """get or set the offset of the individual graphics in the collection""" + return np.stack([g.offset for g in self]) + + @offsets.setter + def offsets(self, values: np.ndarray | list[np.ndarray]): + self._set_feature("offset", values) + + @property + def rotations(self) -> np.ndarray: + """get or set the rotation of the individual graphics in the collection""" + return np.stack([g.rotation for g in self]) + + @rotations.setter + def rotations(self, values: np.ndarray | list[np.ndarray]): + self._set_feature("rotation", values) + + # TODO: how to work with deleted feature in a collection + + @property + def visibles(self) -> np.ndarray[bool]: + """get or set the offsets of the individual graphics in the collection""" + return np.asarray([g.visible for g in self]) + + @visibles.setter + def visibles(self, values: np.ndarray[bool] | list[bool]): + self._set_feature("visible", values) + + +class CollectionIndexer(CollectionProperties): + """Collection Indexer""" + + def __init__(self, selection: np.ndarray[Graphic], features: set[str]): + """ + + Parameters + ---------- + + selection: np.ndarray of Graphics + array of the selected Graphics from the parent GraphicCollection based on the ``selection_indices`` + + """ + + if isinstance(selection, Graphic): + selection = np.asarray([selection]) + + self._selection = selection + self.features = features + + @property + def graphics(self) -> np.ndarray[Graphic]: + """Returns an array of the selected graphics""" + return tuple(self._selection) + + def add_event_handler(self, *args): + """ + Register an event handler. + + Parameters + ---------- + callback: callable, the first argument + Event handler, must accept a single event argument + *types: list of strings + A list of event types, ex: "click", "data", "colors", "pointer_down" + + For the available renderer event types, see + https://jupyter-rfb.readthedocs.io/en/stable/events.html + + All feature support events, i.e. ``graphic.features`` will give a set of + all features that are evented + + Can also be used as a decorator. + + Example + ------- + + .. code-block:: py + + def my_handler(event): + print(event) + + graphic.add_event_handler(my_handler, "pointer_up", "pointer_down") + + Decorator usage example: + + .. code-block:: py + + @graphic.add_event_handler("click") + def my_handler(event): + print(event) + """ + + decorating = not callable(args[0]) + types = args if decorating else args[1:] + + if decorating: + + def decorator(_callback): + for g in self: + g.add_event_handler(_callback, *types) + return _callback + + return decorator + + for g in self: + g.add_event_handler(*args) + + def remove_event_handler(self, callback, *types): + for g in self: + g.remove_event_handler(callback, *types) + + def clear_event_handlers(self): + for g in self: + g.clear_event_handlers() + + def __getitem__(self, item): + return self.graphics[item] + + def __len__(self): + return len(self._selection) + + def __iter__(self): + self._iter = iter(range(len(self))) + return self + + def __next__(self) -> Graphic: + index = next(self._iter) + + return self.graphics[index] + + def __repr__(self): + return ( + f"{self.__class__.__name__} @ {hex(id(self))}\n" + f"Selection of <{len(self._selection)}> {self._selection[0].__class__.__name__}" + ) + + +class GraphicCollection(Graphic, CollectionProperties): + """Graphic Collection base class""" + + _child_type: type + _indexer: type + + def __init_subclass__(cls, **kwargs): + super().__init_subclass__(**kwargs) + cls._features = cls._child_type._features + + def __init__(self, name: str = None, metadata: Any = None, **kwargs): + super().__init__(name=name, metadata=metadata, **kwargs) + + # list of mem locations of the graphics + self._graphics: list[str] = list() + + self._graphics_changed: bool = True + self._graphics_array: np.ndarray[Graphic] = None + + self._iter = None + + @property + 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[addr]) for addr in self._graphics + ] + self._graphics_array = np.array(proxies) + self._graphics_array.flags["WRITEABLE"] = False + self._graphics_changed = False + + return self._graphics_array + + def add_graphic(self, graphic: Graphic): + """ + Add a graphic to the collection. + + Parameters + ---------- + graphic: Graphic + graphic to add, must be a real ``Graphic`` not a proxy + + """ + + if not type(graphic) == self._child_type: + raise TypeError( + f"Can only add graphics of the same type to a collection.\n" + f"You can only add {self._child_type.__name__} to a {self.__class__.__name__}, " + f"you are trying to add a {graphic.__class__.__name__}." + ) + + addr = graphic._fpl_address + COLLECTION_GRAPHICS[addr] = graphic + + self._graphics.append(addr) + + self.world_object.add(graphic.world_object) + + self._graphics_changed = True + + def remove_graphic(self, graphic: Graphic): + """ + Remove a graphic from the collection. + + Note: Only removes the graphic from the collection. Does not remove + the graphic from the scene, and does not delete the graphic. + + Parameters + ---------- + graphic: Graphic + graphic to remove + + """ + + self._graphics.remove(graphic._fpl_address) + + self.world_object.remove(graphic.world_object) + + self._graphics_changed = True + + def add_event_handler(self, *args): + """ + Register an event handler. + + Parameters + ---------- + callback: callable, the first argument + Event handler, must accept a single event argument + *types: list of strings + A list of event types, ex: "click", "data", "colors", "pointer_down" + + For the available renderer event types, see + https://jupyter-rfb.readthedocs.io/en/stable/events.html + + All feature support events, i.e. ``graphic.features`` will give a set of + all features that are evented + + Can also be used as a decorator. + + Example + ------- + + .. code-block:: py + + def my_handler(event): + print(event) + + graphic.add_event_handler(my_handler, "pointer_up", "pointer_down") + + Decorator usage example: + + .. code-block:: py + + @graphic.add_event_handler("click") + def my_handler(event): + print(event) + """ + + return self[:].add_event_handler(*args) + + def remove_event_handler(self, callback, *types): + """remove an event handler""" + self[:].remove_event_handler(callback, *types) + + def clear_event_handlers(self): + self[:].clear_event_handlers() + + def _fpl_add_plot_area_hook(self, plot_area): + super()._fpl_add_plot_area_hook(plot_area) + + for g in self: + g._fpl_add_plot_area_hook(plot_area) + + def _fpl_cleanup(self): + """ + Cleans up the graphic in preparation for __del__(), such as removing event handlers from + plot renderer, feature event handlers, etc. + + Optionally implemented in subclasses + """ + # clear any attached event handlers and animation functions + self.world_object._event_handlers.clear() + + for g in self: + g._fpl_cleanup() + + def __getitem__(self, key) -> CollectionIndexer: + if np.issubdtype(type(key), np.integer): + addr = self._graphics[key] + return weakref.proxy(COLLECTION_GRAPHICS[addr]) + + return self._indexer(selection=self.graphics[key], features=self._features) + + def __del__(self): + self.world_object.clear() + + for addr in self._graphics: + del COLLECTION_GRAPHICS[addr] + + super().__del__() + + def __len__(self): + return len(self._graphics) + + def __iter__(self): + self._iter = iter(range(len(self))) + return self + + def __next__(self) -> Graphic: + index = next(self._iter) + addr = self._graphics[index] + + return weakref.proxy(COLLECTION_GRAPHICS[addr]) + + def __repr__(self): + rval = super().__repr__() + return f"{rval}\nCollection of <{len(self._graphics)}> Graphics" + + +class CollectionFeature: + """Collection Feature""" + + def __init__(self, selection: np.ndarray[Graphic], feature: str): + """ + selection: list of Graphics + a list of the selected Graphics from the parent GraphicCollection based on the ``selection_indices`` + + feature: str + feature of Graphics in the GraphicCollection being indexed + + """ + + self._selection = selection + self._feature = feature + + self._feature_instances = [getattr(g, feature) for g in self._selection] + + def __getitem__(self, item): + return np.stack([fi[item] for fi in self._feature_instances]) + + def __setitem__(self, key, value): + for fi in self._feature_instances: + fi[key] = value + + def __repr__(self): + return f"Collection feature for: <{self._feature}>" diff --git a/fastplotlib/graphics/_features/__init__.py b/fastplotlib/graphics/_features/__init__.py index fb25db287..e36de089e 100644 --- a/fastplotlib/graphics/_features/__init__.py +++ b/fastplotlib/graphics/_features/__init__.py @@ -1,33 +1,64 @@ -from ._colors import ColorFeature, CmapFeature, ImageCmapFeature, HeatmapCmapFeature -from ._data import PointsDataFeature, ImageDataFeature, HeatmapDataFeature -from ._sizes import PointsSizesFeature -from ._present import PresentFeature -from ._thickness import ThicknessFeature +from ._positions_graphics import ( + VertexColors, + UniformColor, + UniformSize, + Thickness, + VertexPositions, + PointsSizesFeature, + VertexCmap, +) +from ._image import ( + TextureArray, + ImageCmap, + ImageVmin, + ImageVmax, + ImageInterpolation, + ImageCmapInterpolation, + WGPU_MAX_TEXTURE_SIZE, +) from ._base import ( GraphicFeature, - GraphicFeatureIndexable, + BufferManager, FeatureEvent, to_gpu_supported_dtype, ) + +from ._text import ( + TextData, + FontSize, + TextFaceColor, + TextOutlineColor, + TextOutlineThickness, +) + from ._selection_features import LinearSelectionFeature, LinearRegionSelectionFeature -from ._deleted import Deleted +from ._common import Name, Offset, Rotation, Visible, Deleted + __all__ = [ - "ColorFeature", - "CmapFeature", - "ImageCmapFeature", - "HeatmapCmapFeature", - "PointsDataFeature", + "VertexColors", + "UniformColor", + "UniformSize", + "Thickness", + "VertexPositions", "PointsSizesFeature", - "ImageDataFeature", - "HeatmapDataFeature", - "PresentFeature", - "ThicknessFeature", - "GraphicFeature", - "GraphicFeatureIndexable", - "FeatureEvent", - "to_gpu_supported_dtype", + "VertexCmap", + "TextureArray", + "ImageCmap", + "ImageVmin", + "ImageVmax", + "ImageInterpolation", + "ImageCmapInterpolation", + "TextData", + "FontSize", + "TextFaceColor", + "TextOutlineColor", + "TextOutlineThickness", "LinearSelectionFeature", "LinearRegionSelectionFeature", + "Name", + "Offset", + "Rotation", + "Visible", "Deleted", ] diff --git a/fastplotlib/graphics/_features/_base.py b/fastplotlib/graphics/_features/_base.py index 99ebbf436..1b24d3b78 100644 --- a/fastplotlib/graphics/_features/_base.py +++ b/fastplotlib/graphics/_features/_base.py @@ -1,14 +1,17 @@ -from abc import ABC, abstractmethod -from inspect import getfullargspec from warnings import warn -from typing import * -import weakref +from typing import Any, Literal import numpy as np +from numpy.typing import NDArray + +from wgpu.gui.base import log_exception import pygfx +WGPU_MAX_TEXTURE_SIZE = 8192 + + supported_dtypes = [ np.uint8, np.uint16, @@ -41,64 +44,44 @@ def to_gpu_supported_dtype(array): return array -class FeatureEvent: +class FeatureEvent(pygfx.Event): """ - Dataclass that holds feature event information. Has ``type`` and ``pick_info`` attributes. - - Attributes - ---------- - type: str, example "colors" - - pick_info: dict: - - ============== ============================================================================= - key value - ============== ============================================================================= - "index" indices where feature data was changed, ``range`` object or ``List[int]`` - "world_object" world object the feature belongs to - "new_data: the new data for this feature - ============== ============================================================================= - - .. note:: - pick info varies between features, this is just the general structure + **All event instances have the following attributes** + + +------------+-------------+-----------------------------------------------+ + | attribute | type | description | + +============+=============+===============================================+ + | type | str | "colors" - name of the event | + +------------+-------------+-----------------------------------------------+ + | graphic | Graphic | graphic instance that the event is from | + +------------+-------------+-----------------------------------------------+ + | info | dict | event info dictionary (see below) | + +------------+-------------+-----------------------------------------------+ + | target | WorldObject | pygfx rendering engine object for the graphic | + +------------+-------------+-----------------------------------------------+ + | time_stamp | float | time when the event occured, in ms | + +------------+-------------+-----------------------------------------------+ """ - 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" - ) - - -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 - # - # data: Any - # - # collection_index: int - # if part of a collection, index of this graphic within the collection + def __init__(self, type: str, info: dict): + super().__init__(type=type) + self.info = info - self._parent = weakref.proxy(parent) - self._data = to_gpu_supported_dtype(data) - - self._collection_index = collection_index +class GraphicFeature: + def __init__(self, **kwargs): self._event_handlers = list() self._block_events = False - def __call__(self, *args, **kwargs): - return self._data + @property + def value(self) -> Any: + """Graphic Feature value, must be implemented in subclass""" + raise NotImplemented + + def set_value(self, graphic, value: float): + """Graphic Feature value setter, must be implemented in subclass""" + raise NotImplementedError def block_events(self, val: bool): """ @@ -112,23 +95,14 @@ def block_events(self, val: bool): """ self._block_events = val - @abstractmethod - def _set(self, value): - pass - - def _parse_set_value(self, value): - if isinstance(value, GraphicFeature): - return value() - - return 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 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". And ``pick_info`` which contains - information about the event and Graphic that triggered it. + + Used by `Graphic` classes to add to their event handlers, not meant for users. Users + add handlers to Graphic instances only. + + The ``handler`` must accept a :class:`.FeatureEvent` as the first and only argument. Parameters ---------- @@ -164,196 +138,202 @@ 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 - @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""" - pass - def _call_event_handlers(self, event_data: FeatureEvent): if self._block_events: return for func in self._event_handlers: - try: - args = getfullargspec(func).args - - if len(args) > 0: - if args[0] == "self" and not len(args) > 1: - func() - else: - func(event_data) - else: - func() - except TypeError: - warn( - f"Event handler {func} has an unresolvable argspec, calling it without arguments" - ) - func() - - @abstractmethod - def __repr__(self) -> str: - pass - - -def cleanup_slice(key: Union[int, slice], upper_bound) -> Union[slice, int]: - """ - - If the key in an `int`, it just returns it. Otherwise, - it parses it and removes the `None` vals and replaces - them with corresponding values that can be used to - create a `range`, get `len` etc. - - Parameters - ---------- - key - upper_bound - - Returns - ------- - - """ - if isinstance(key, int): - return key - - if isinstance(key, np.ndarray): - return cleanup_array_slice(key, upper_bound) - - if isinstance(key, tuple): - # if tuple of slice we only need the first obj - # since the first obj is the datapoint indices - if isinstance(key[0], slice): - key = key[0] + with log_exception( + f"Error during handling {self.__class__.__name__} event" + ): + func(event_data) + + +class BufferManager(GraphicFeature): + """Smaller wrapper for pygfx.Buffer""" + + def __init__( + self, + data: NDArray | pygfx.Buffer, + buffer_type: Literal["buffer", "texture", "texture-array"] = "buffer", + isolated_buffer: bool = True, + texture_dim: int = 2, + **kwargs, + ): + super().__init__() + if isolated_buffer and not isinstance(data, pygfx.Resource): + # useful if data is read-only, example: memmaps + bdata = np.zeros(data.shape, dtype=data.dtype) + bdata[:] = data[:] else: - raise TypeError("Tuple slicing must have slice object in first position") - - if not isinstance(key, slice): - raise TypeError("Must pass slice or int object") - - start = key.start - stop = key.stop - step = key.step - for attr in [start, stop, step]: - if attr is None: - continue - if attr < 0: - raise IndexError("Negative indexing not supported.") - - if start is None: - start = 0 - - if stop is None: - stop = upper_bound - - elif stop > 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 + # user's input array is used as the buffer + bdata = data + + if isinstance(data, pygfx.Resource): + # already a buffer, probably used for + # managing another BufferManager, example: VertexCmap manages VertexColors + self._buffer = data + elif buffer_type == "buffer": + self._buffer = pygfx.Buffer(bdata) + elif buffer_type == "texture": + # TODO: placeholder, not currently used since TextureArray is used specifically for Image graphics + self._buffer = pygfx.Texture(bdata, dim=texture_dim) + else: + raise ValueError( + "`data` must be a pygfx.Buffer instance or `buffer_type` must be one of: 'buffer' or 'texture'" + ) - return slice(start, stop, step) + self._event_handlers: list[callable] = list() + self._shared: int = 0 -def cleanup_array_slice(key: np.ndarray, upper_bound) -> Union[np.ndarray, None]: - """ - Cleanup numpy array used for fancy indexing, make sure key[-1] <= upper_bound. + @property + def value(self) -> np.ndarray: + """numpy array object representing the data managed by this buffer""" + return self.buffer.data - Returns None if nothing to change. + def set_value(self, graphic, value): + """Sets values on entire array""" + self[:] = value - Parameters - ---------- - key: np.ndarray - integer or boolean array + @property + def buffer(self) -> pygfx.Buffer | pygfx.Texture: + """managed buffer""" + return self._buffer - upper_bound + @property + def shared(self) -> int: + """Number of graphics that share this buffer""" + return self._shared - Returns - ------- - np.ndarray - integer indexing array + @property + def __array_interface__(self): + raise BufferError( + f"Cannot use graphic feature buffer as an array, use .value instead.\n" + f"Examples: line.data.value, line.colors.value, scatter.data.value, scatter.sizes.value" + ) - """ + def __getitem__(self, item): + return self.buffer.data[item] - if key.ndim > 1: - raise TypeError(f"Can only use 1D boolean or integer arrays for fancy indexing") + def __setitem__(self, key, value): + raise NotImplementedError - # if boolean array convert to integer array of indices - if key.dtype == bool: - key = np.nonzero(key)[0] + def _parse_offset_size( + self, + key: int | slice | np.ndarray[int | bool] | list[bool | int], + upper_bound: int, + ): + """ + parse offset and size for first, i.e. n_datapoints, dimension + """ + if np.issubdtype(type(key), np.integer): + # simplest case, just an int + offset = key + size = 1 + + elif isinstance(key, slice): + # TODO: off-by-one sometimes when step is used + # the offset can be one to the left or the size + # is one extra so it's not really an issue for now + # parse slice + start, stop, step = key.indices(upper_bound) + + # account for backwards indexing + if (start > stop) and step < 0: + offset = stop + else: + offset = start - if key.size < 1: - return None + # slice.indices will give -1 if None is passed + # which just means 0 here since buffers do not + # use negative indexing + offset = max(0, offset) - # 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}`" - ) + # number of elements to upload + # this is indexing so do not add 1 + size = abs(stop - start) - # make sure indices are integers - if np.issubdtype(key.dtype, np.integer): - return key + elif isinstance(key, (np.ndarray, list)): + if isinstance(key, list): + # convert to array + key = np.array(key) - raise TypeError(f"Can only use 1D boolean or integer arrays for fancy indexing") + if not key.ndim == 1: + raise TypeError( + f"can only use 1D arrays for fancy indexing, you have passed a data with: {key.ndim} dimensions" + ) + if key.dtype == bool: + # convert bool mask to integer indices + key = np.nonzero(key)[0] -class GraphicFeatureIndexable(GraphicFeature): - """An indexable Graphic Feature, colors, data, sizes etc.""" + if not np.issubdtype(key.dtype, np.integer): + # fancy indexing doesn't make sense with non-integer types + raise TypeError( + f"can only using integer or booleans arrays for fancy indexing, your array is of type: {key.dtype}" + ) - def _set(self, value): - value = self._parse_set_value(value) - self[:] = value + if key.size < 1: + # nothing to update + return - @abstractmethod - def __getitem__(self, item): - pass + # convert any negative integer indices to positive indices + key %= upper_bound - @abstractmethod - def __setitem__(self, key, value): - pass + # index of first element to upload + offset = key.min() - @abstractmethod - def _update_range(self, key): - pass + # size range to upload + # add 1 because this is direct + # passing of indices, not a start:stop + size = np.ptp(key) + 1 - @property - @abstractmethod - def buffer(self) -> Union[pygfx.Buffer, pygfx.Texture]: - """Underlying buffer for this feature""" - pass + else: + raise TypeError( + f"invalid key for indexing buffer: {key}\n" + f"valid ways to index buffers are using integers, slices, or fancy indexing with integers or bool" + ) + + return offset, size + + def _update_range( + self, + key: ( + int | slice | np.ndarray[int | bool] | list[bool | int] | tuple[slice, ...] + ), + ): + """ + Uses key from slicing to determine the offset and + size of the buffer to mark for upload to the GPU + """ + upper_bound = self.value.shape[0] - @property - def _upper_bound(self) -> int: - return self._data.shape[0] + if isinstance(key, tuple): + if any([k is Ellipsis for k in key]): + # let's worry about ellipsis later + raise TypeError("ellipses not supported for indexing buffers") + # if multiple dims are sliced, we only need the key for + # the first dimension corresponding to n_datapoints + key: int | np.ndarray[int | bool] | slice = key[0] - def _update_range_indices(self, key): - """Currently used by colors and positions data""" - if not isinstance(key, np.ndarray): - key = cleanup_slice(key, self._upper_bound) + offset, size = self._parse_offset_size(key, upper_bound) + self.buffer.update_range(offset=offset, size=size) - if isinstance(key, int): - self.buffer.update_range(key, size=1) + def _emit_event(self, type: str, key, value): + if len(self._event_handlers) < 1: return - # else if it's a slice obj - if isinstance(key, slice): - if key.step == 1: # we cleaned up the slice obj so step of None becomes 1 - # update range according to size using the offset - self.buffer.update_range(offset=key.start, size=key.stop - key.start) + event_info = { + "key": key, + "value": value, + } + event = FeatureEvent(type, info=event_info) - else: - step = key.step - # convert slice to indices - ixs = range(key.start, key.stop, step) - for ix in ixs: - self.buffer.update_range(ix, size=1) + self._call_event_handlers(event) - # TODO: See how efficient this is with large indexing - elif isinstance(key, np.ndarray): - self.buffer.update_range() + def __len__(self): + raise NotImplementedError - else: - raise TypeError("must pass int or slice to update range") + def __repr__(self): + return f"{self.__class__.__name__} buffer data:\n" f"{self.value.__repr__()}" diff --git a/fastplotlib/graphics/_features/_colors.py b/fastplotlib/graphics/_features/_colors.py deleted file mode 100644 index 48405e74c..000000000 --- a/fastplotlib/graphics/_features/_colors.py +++ /dev/null @@ -1,434 +0,0 @@ -import numpy as np -import pygfx - -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): - """ - Manages the color buffer for :class:`LineGraphic` or :class:`ScatterGraphic` - - **event pick info:** - - ==================== =============================== ========================================================================= - key type description - ==================== =============================== ========================================================================= - "index" ``numpy.ndarray`` or ``None`` changed indices in the buffer - "new_data" ``numpy.ndarray`` or ``None`` new buffer data at the changed indices - "collection-index" int the index of the graphic within the collection that triggered the event - "world_object" pygfx.WorldObject world object - ==================== =============================== ========================================================================= - - """ - - @property - 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, - ): - """ - ColorFeature - - Parameters - ---------- - parent: Graphic or GraphicCollection - - colors: str, array, or iterable - specify colors as a single human readable string, RGBA array, - or an iterable of strings or RGBA arrays - - n_colors: int - number of colors to hold, if passing in a single str or single RGBA array - - alpha: float - alpha value for the colors - - """ - # if provided as a numpy array of str - if isinstance(colors, np.ndarray): - if colors.dtype.kind in ["U", "S"]: - colors = colors.tolist() - # 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) - # 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: - raise ValueError( - "Valid array color arguments must be a single RGBA array or a stack of " - "RGBA arrays for each datapoint in the shape [n_datapoints, 4]" - ) - data = colors - else: - raise ValueError( - "Valid array color arguments must be a single RGBA array or a stack of " - "RGBA arrays for each datapoint in the shape [n_datapoints, 4]" - ) - - # if the color is provided as an iterable - elif isinstance(colors, (list, tuple, np.ndarray)): - # if iterable of str - if all([isinstance(val, str) for val in colors]): - if not len(colors) == n_colors: - raise ValueError( - f"Valid iterable color arguments must be a `tuple` or `list` of `str` " - f"where the length of the iterable is the same as the number of datapoints." - ) - - 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 = pygfx.Color(colors) - data = np.repeat(np.array([c]), n_colors, axis=0) - - else: - raise ValueError( - f"Valid iterable color arguments must be a `tuple` or `list` representing RGBA values or " - f"an iterable of `str` with the same length as the number of datapoints." - ) - elif isinstance(colors, str): - if colors == "random": - data = np.random.rand(n_colors, 4) - data[:, -1] = alpha - else: - data = make_pygfx_colors(colors, n_colors) - else: - # assume it's a single color, use pygfx.Color to parse it - data = make_pygfx_colors(colors, n_colors) - - if alpha != 1.0: - data[:, -1] = alpha - - super().__init__(parent, data, collection_index=collection_index) - - def __setitem__(self, key, value): - # parse numerical slice indices - if isinstance(key, slice): - _key = cleanup_slice(key, self._upper_bound) - indices = range(_key.start, _key.stop, _key.step) - - # or single numerical index - elif isinstance(key, (int, np.integer)): - key = cleanup_slice(key, self._upper_bound) - indices = [key] - - elif isinstance(key, tuple): - if not isinstance(value, (float, int, np.ndarray)): - raise ValueError( - "If using multiple-fancy indexing for color, you can only set numerical" - "values since this sets the RGBA array data directly." - ) - - if len(key) != 2: - 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 - - # update range - # first slice obj is going to be the indexing so use key[0] - # key[1] is going to be RGBA so get rid of it to pass to _update_range - # _key = cleanup_slice(key[0], self._upper_bound) - self._update_range(key) - self._feature_changed(key, value) - return - - elif isinstance(key, np.ndarray): - key = cleanup_array_slice(key, self._upper_bound) - if key is None: - return - - indices = key - - else: - 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(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 - ) - - # if already a numpy array - elif isinstance(value, np.ndarray): - # if a single color provided as numpy array - if value.shape == (4,): - new_colors = value.astype(np.float32) - # 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 - ) - - 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)" - ) - # 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) - else: - new_colors = value.astype(np.float32) - - else: - raise ValueError( - "numpy array passed to color must be of shape (4,) or (n_colors_modify, 4)" - ) - - self.buffer.data[key] = new_colors - - self._update_range(key) - self._feature_changed(key, new_colors) - - def _update_range(self, key): - self._update_range_indices(key) - - def _feature_changed(self, key, new_data): - key = cleanup_slice(key, self._upper_bound) - if isinstance(key, int): - indices = [key] - elif isinstance(key, slice): - indices = range(key.start, key.stop, key.step) - elif isinstance(key, np.ndarray): - indices = key - else: - raise TypeError("feature changed key must be slice or int") - - pick_info = { - "index": indices, - "collection-index": self._collection_index, - "world_object": self._parent.world_object, - "new_data": new_data, - } - - event_data = FeatureEvent(type="colors", pick_info=pick_info) - - self._call_event_handlers(event_data) - - def __repr__(self) -> str: - s = f"ColorsFeature for {self._parent}. Call `.colors()` to get values." - return s - - -class CmapFeature(ColorFeature): - """ - Indexable colormap feature, mostly wraps colors and just provides a way to set colormaps. - - Same event pick info as :class:`ColorFeature` - """ - - def __init__(self, parent, colors, cmap_name: str, cmap_values: np.ndarray): - # Skip the ColorFeature's __init__ - super(ColorFeature, self).__init__(parent, colors) - - self._cmap_name = cmap_name - self._cmap_values = cmap_values - - 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." - ) - - if isinstance(key, slice): - n_colors = len(range(key.start, key.stop, key.step)) - - else: - # numpy array - n_colors = key.size - - colors = parse_cmap_values( - n_colors=n_colors, cmap_name=cmap_name, cmap_values=self._cmap_values - ) - - self._cmap_name = cmap_name - super().__setitem__(key, colors) - - @property - def name(self) -> str: - return self._cmap_name - - @property - def values(self) -> np.ndarray: - return self._cmap_values - - @values.setter - def values(self, values: np.ndarray): - if not isinstance(values, np.ndarray): - values = np.array(values) - - colors = parse_cmap_values( - n_colors=self().shape[0], cmap_name=self._cmap_name, cmap_values=values - ) - - self._cmap_values = values - - super().__setitem__(slice(None), colors) - - def __repr__(self) -> str: - s = f"CmapFeature for {self._parent}, to get name or values: `.cmap.name`, `.cmap.values`" - return s - - -class ImageCmapFeature(GraphicFeature): - """ - Colormap for :class:`ImageGraphic`. - - .cmap() returns the Texture buffer for the cmap. - - .cmap.name returns the cmap name as a str. - - **event pick info:** - - ================ =================== =============== - key type description - ================ =================== =============== - "index" ``None`` not used - "name" ``str`` colormap name - "world_object" pygfx.WorldObject world object - "vmin" ``float`` minimum value - "vmax" ``float`` maximum value - ================ =================== =============== - - """ - - def __init__(self, parent, cmap: str): - cmap_texture_view = get_cmap_texture(cmap) - super().__init__(parent, cmap_texture_view) - self._name = cmap - - def _set(self, cmap_name: str): - if self._parent.data().ndim > 2: - return - - self._parent.world_object.material.map.data[:] = make_colors(256, cmap_name) - self._parent.world_object.material.map.update_range((0, 0, 0), size=(256, 1, 1)) - self._name = cmap_name - - self._feature_changed(key=None, new_data=self._name) - - @property - def name(self) -> str: - return self._name - - @property - def vmin(self) -> float: - """Minimum contrast limit.""" - return self._parent.world_object.material.clim[0] - - @vmin.setter - def vmin(self, value: float): - """Minimum contrast limit.""" - self._parent.world_object.material.clim = ( - value, - self._parent.world_object.material.clim[1], - ) - self._feature_changed(key=None, new_data=None) - - @property - def vmax(self) -> float: - """Maximum contrast limit.""" - return self._parent.world_object.material.clim[1] - - @vmax.setter - def vmax(self, value: float): - """Maximum contrast limit.""" - self._parent.world_object.material.clim = ( - self._parent.world_object.material.clim[0], - 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 - - pick_info = { - "index": None, - "world_object": self._parent.world_object, - "name": self._name, - "vmin": self.vmin, - "vmax": self.vmax, - } - - event_data = FeatureEvent(type="cmap", pick_info=pick_info) - - self._call_event_handlers(event_data) - - def __repr__(self) -> str: - s = f"ImageCmapFeature for {self._parent}. Use `.cmap.name` to get str name of cmap." - return s - - -class HeatmapCmapFeature(ImageCmapFeature): - """ - Colormap for :class:`HeatmapGraphic` - - Same event pick info as :class:`ImageCmapFeature` - """ - - def _set(self, cmap_name: str): - # in heatmap we use one material for all ImageTiles - self._parent._material.map.data[:] = make_colors(256, cmap_name) - self._parent._material.map.update_range((0, 0, 0), size=(256, 1, 1)) - self._name = cmap_name - - self._feature_changed(key=None, new_data=self.name) - - @property - def vmin(self) -> float: - """Minimum contrast limit.""" - return self._parent._material.clim[0] - - @vmin.setter - def vmin(self, value: float): - """Minimum contrast limit.""" - self._parent._material.clim = (value, self._parent._material.clim[1]) - - @property - def vmax(self) -> float: - """Maximum contrast limit.""" - return self._parent._material.clim[1] - - @vmax.setter - def vmax(self, value: float): - """Maximum contrast limit.""" - self._parent._material.clim = (self._parent._material.clim[0], value) diff --git a/fastplotlib/graphics/_features/_common.py b/fastplotlib/graphics/_features/_common.py new file mode 100644 index 000000000..fe32a485f --- /dev/null +++ b/fastplotlib/graphics/_features/_common.py @@ -0,0 +1,123 @@ +import numpy as np + +from ._base import GraphicFeature, FeatureEvent + + +class Name(GraphicFeature): + """Graphic name""" + + def __init__(self, value: str): + self._value = value + super().__init__() + + @property + def value(self) -> str: + return self._value + + def set_value(self, graphic, value: str): + if not isinstance(value, str): + raise TypeError("`Graphic` name must be of type ") + + if graphic._plot_area is not None: + graphic._plot_area._check_graphic_name_exists(value) + + self._value = value + + event = FeatureEvent(type="name", info={"value": value}) + self._call_event_handlers(event) + + +class Offset(GraphicFeature): + """Offset position of the graphic, [x, y, z]""" + + def __init__(self, value: np.ndarray | list | tuple): + self._validate(value) + self._value = np.array(value) + self._value.flags.writeable = False + super().__init__() + + def _validate(self, value): + if not len(value) == 3: + raise ValueError("offset must be a list, tuple, or array of 3 float values") + + @property + def value(self) -> np.ndarray: + return self._value + + def set_value(self, graphic, value: np.ndarray | list | tuple): + self._validate(value) + + graphic.world_object.world.position = value + self._value = graphic.world_object.world.position.copy() + self._value.flags.writeable = False + + event = FeatureEvent(type="offset", info={"value": value}) + self._call_event_handlers(event) + + +class Rotation(GraphicFeature): + """Graphic rotation quaternion""" + + def __init__(self, value: np.ndarray | list | tuple): + self._validate(value) + self._value = np.array(value) + self._value.flags.writeable = False + super().__init__() + + def _validate(self, value): + if not len(value) == 4: + raise ValueError( + "rotation quaternion must be a list, tuple, or array of 4 float values" + ) + + @property + def value(self) -> np.ndarray: + return self._value + + def set_value(self, graphic, value: np.ndarray | list | tuple): + self._validate(value) + + graphic.world_object.world.rotation = value + self._value = graphic.world_object.world.rotation.copy() + self._value.flags.writeable = False + + event = FeatureEvent(type="rotation", info={"value": value}) + self._call_event_handlers(event) + + +class Visible(GraphicFeature): + """Access or change the visibility.""" + + def __init__(self, value: bool): + self._value = value + super().__init__() + + @property + def value(self) -> bool: + return self._value + + def set_value(self, graphic, value: bool): + graphic.world_object.visible = value + self._value = value + + event = FeatureEvent(type="visible", info={"value": value}) + self._call_event_handlers(event) + + +class Deleted(GraphicFeature): + """ + Used when a graphic is deleted, triggers events that can be useful to indicate this graphic has been deleted + """ + + def __init__(self, value: bool): + self._value = value + super().__init__() + + @property + def value(self) -> bool: + return self._value + + def set_value(self, graphic, value: bool): + self._value = value + event = FeatureEvent(type="deleted", info={"value": value}) + self._call_event_handlers(event) diff --git a/fastplotlib/graphics/_features/_data.py b/fastplotlib/graphics/_features/_data.py deleted file mode 100644 index bcfe9446a..000000000 --- a/fastplotlib/graphics/_features/_data.py +++ /dev/null @@ -1,219 +0,0 @@ -from typing import * - -import numpy as np - -import pygfx - -from ._base import ( - GraphicFeatureIndexable, - cleanup_slice, - FeatureEvent, - to_gpu_supported_dtype, - cleanup_array_slice, -) - - -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().__init__(parent, data, collection_index=collection_index) - - @property - def buffer(self) -> pygfx.Buffer: - return self._parent.world_object.geometry.positions - - def __getitem__(self, item): - return self.buffer.data[item] - - def _fix_data(self, data, parent): - graphic_type = parent.__class__.__name__ - - data = to_gpu_supported_dtype(data) - - if data.ndim == 1: - # for scatter if we receive just 3 points in a 1d array, treat it as just a single datapoint - # this is different from fix_data for LineGraphic since there we assume that a 1d array - # is just y-values - if graphic_type == "ScatterGraphic": - data = np.array([data]) - elif graphic_type == "LineGraphic": - data = np.dstack([np.arange(data.size, dtype=data.dtype), data])[0] - - if data.shape[1] != 3: - if data.shape[1] != 2: - raise ValueError(f"Must pass 1D, 2D or 3D data to {graphic_type}") - - # zeros for z - zs = np.zeros(data.shape[0], dtype=data.dtype) - - data = np.dstack([data[:, 0], data[:, 1], zs])[0] - - return data - - def __setitem__(self, key, value): - if isinstance(key, np.ndarray): - # make sure 1D array of int or boolean - key = cleanup_array_slice(key, self._upper_bound) - - # put data into right shape if they're only indexing datapoints - if isinstance(key, (slice, int, np.ndarray, np.integer)): - value = self._fix_data(value, self._parent) - # otherwise assume that they have the right shape - # numpy will throw errors if it can't broadcast - - self.buffer.data[key] = value - self._update_range(key) - # avoid creating dicts constantly if there are no events to handle - if len(self._event_handlers) > 0: - self._feature_changed(key, value) - - def _update_range(self, key): - self._update_range_indices(key) - - def _feature_changed(self, key, new_data): - if key is not None: - key = cleanup_slice(key, self._upper_bound) - if isinstance(key, (int, np.integer)): - indices = [key] - elif isinstance(key, slice): - indices = range(key.start, key.stop, key.step) - elif isinstance(key, np.ndarray): - indices = key - elif key is None: - indices = None - - pick_info = { - "index": indices, - "collection-index": self._collection_index, - "world_object": self._parent.world_object, - "new_data": new_data, - } - - event_data = FeatureEvent(type="data", pick_info=pick_info) - - self._call_event_handlers(event_data) - - def __repr__(self) -> str: - s = f"PointsDataFeature for {self._parent}, call `.data()` to get values" - return s - - -class ImageDataFeature(GraphicFeatureIndexable): - """ - Access to the Texture buffer shown in an ImageGraphic. - """ - - def __init__(self, parent, data: Any): - if data.ndim not in (2, 3): - raise ValueError( - "`data.ndim` must be 2 or 3, ImageGraphic data shape must be " - "``[x_dim, y_dim]`` or ``[x_dim, y_dim, rgb]``" - ) - - super().__init__(parent, data) - - @property - def buffer(self) -> pygfx.Texture: - """Texture buffer for the image data""" - return self._parent.world_object.geometry.grid - - def update_gpu(self): - """Update the GPU with the buffer""" - self._update_range(None) - - def __call__(self, *args, **kwargs): - return self.buffer.data - - def __getitem__(self, item): - return self.buffer.data[item] - - def __setitem__(self, key, value): - # make sure float32 - value = to_gpu_supported_dtype(value) - - self.buffer.data[key] = value - self._update_range(key) - - # avoid creating dicts constantly if there are no events to handle - if len(self._event_handlers) > 0: - self._feature_changed(key, value) - - def _update_range(self, key): - self.buffer.update_range((0, 0, 0), size=self.buffer.size) - - def _feature_changed(self, key, new_data): - if key is not None: - key = cleanup_slice(key, self._upper_bound) - if isinstance(key, int): - indices = [key] - elif isinstance(key, slice): - indices = range(key.start, key.stop, key.step) - elif key is None: - indices = None - - pick_info = { - "index": indices, - "world_object": self._parent.world_object, - "new_data": new_data, - } - - event_data = FeatureEvent(type="data", pick_info=pick_info) - - self._call_event_handlers(event_data) - - def __repr__(self) -> str: - s = f"ImageDataFeature for {self._parent}, call `.data()` to get values" - return s - - -class HeatmapDataFeature(ImageDataFeature): - @property - 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] - - def __getitem__(self, item): - return self._data[item] - - def __call__(self, *args, **kwargs): - return self._data - - def __setitem__(self, key, value): - # make sure supported type, not float64 etc. - value = to_gpu_supported_dtype(value) - - self._data[key] = value - self._update_range(key) - - # avoid creating dicts constantly if there are no events to handle - if len(self._event_handlers) > 0: - self._feature_changed(key, value) - - def _update_range(self, key): - for buffer in self.buffer: - buffer.update_range((0, 0, 0), size=buffer.size) - - def _feature_changed(self, key, new_data): - if key is not None: - key = cleanup_slice(key, self._upper_bound) - if isinstance(key, int): - indices = [key] - elif isinstance(key, slice): - indices = range(key.start, key.stop, key.step) - elif key is None: - indices = None - - pick_info = { - "index": indices, - "world_object": self._parent.world_object, - "new_data": new_data, - } - - event_data = FeatureEvent(type="data", pick_info=pick_info) - - self._call_event_handlers(event_data) diff --git a/fastplotlib/graphics/_features/_deleted.py b/fastplotlib/graphics/_features/_deleted.py deleted file mode 100644 index 7900385eb..000000000 --- a/fastplotlib/graphics/_features/_deleted.py +++ /dev/null @@ -1,41 +0,0 @@ -from ._base import GraphicFeature, FeatureEvent - - -class Deleted(GraphicFeature): - """ - Used when a graphic is deleted, triggers events that can be useful to indicate this graphic has been deleted - - **event pick info:** - - ==================== ======================== ========================================================================= - key type description - ==================== ======================== ========================================================================= - "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, value: bool): - super().__init__(parent, value) - - def _set(self, value: bool): - value = self._parse_set_value(value) - self._feature_changed(key=None, new_data=value) - - def _feature_changed(self, key, new_data): - # this is a non-indexable feature so key=None - - pick_info = { - "index": None, - "collection-index": self._collection_index, - "world_object": self._parent.world_object, - "new_data": new_data, - } - - event_data = FeatureEvent(type="deleted", pick_info=pick_info) - - self._call_event_handlers(event_data) - - def __repr__(self) -> str: - s = f"DeletedFeature for {self._parent}" - return s diff --git a/fastplotlib/graphics/_features/_image.py b/fastplotlib/graphics/_features/_image.py new file mode 100644 index 000000000..e31184c4b --- /dev/null +++ b/fastplotlib/graphics/_features/_image.py @@ -0,0 +1,262 @@ +from itertools import product + +from math import ceil + +import numpy as np + +import pygfx +from ._base import GraphicFeature, FeatureEvent, WGPU_MAX_TEXTURE_SIZE + +from ...utils import ( + make_colors, + get_cmap_texture, +) + + +# manages an array of 8192x8192 Textures representing chunks of an image +class TextureArray(GraphicFeature): + def __init__(self, data, isolated_buffer: bool = True): + super().__init__() + + data = self._fix_data(data) + + if isolated_buffer: + # useful if data is read-only, example: memmaps + self._value = np.zeros(data.shape, dtype=data.dtype) + self.value[:] = data[:] + else: + # user's input array is used as the buffer + self._value = data + + # data start indices for each Texture + self._row_indices = np.arange( + 0, + ceil(self.value.shape[0] / WGPU_MAX_TEXTURE_SIZE) * WGPU_MAX_TEXTURE_SIZE, + WGPU_MAX_TEXTURE_SIZE, + ) + self._col_indices = np.arange( + 0, + ceil(self.value.shape[1] / WGPU_MAX_TEXTURE_SIZE) * WGPU_MAX_TEXTURE_SIZE, + WGPU_MAX_TEXTURE_SIZE, + ) + + # buffer will be an array of textures + self._buffer: np.ndarray[pygfx.Texture] = np.empty( + shape=(self.row_indices.size, self.col_indices.size), dtype=object + ) + + self._iter = None + + # iterate through each chunk of passed `data` + # create a pygfx.Texture from this chunk + for _, buffer_index, data_slice in self: + texture = pygfx.Texture(self.value[data_slice], dim=2) + + self.buffer[buffer_index] = texture + + self._shared: int = 0 + + @property + def value(self) -> np.ndarray: + return self._value + + def set_value(self, graphic, value): + self[:] = value + + @property + def buffer(self) -> np.ndarray[pygfx.Texture]: + return self._buffer + + @property + def row_indices(self) -> np.ndarray: + """ + row indices that are used to chunk the big data array + into individual Textures on the GPU + """ + return self._row_indices + + @property + def col_indices(self) -> np.ndarray: + """ + column indices that are used to chunk the big data array + into individual Textures on the GPU + """ + return self._col_indices + + @property + def shared(self) -> int: + return self._shared + + def _fix_data(self, data): + if data.ndim not in (2, 3): + raise ValueError( + "image data must be 2D with or without an RGB(A) dimension, i.e. " + "it must be of shape [x, y], [x, y, 3] or [x, y, 4]" + ) + + # let's just cast to float32 always + return data.astype(np.float32) + + def __iter__(self): + self._iter = product(enumerate(self.row_indices), enumerate(self.col_indices)) + return self + + def __next__(self) -> tuple[pygfx.Texture, tuple[int, int], tuple[slice, slice]]: + """ + Iterate through each Texture within the texture array + + Returns + ------- + Texture, tuple[int, int], tuple[slice, slice] + | Texture: pygfx.Texture + | tuple[int, int]: chunk index, i.e corresponding index of ``self.buffer`` array + | tuple[slice, slice]: data slice of big array in this chunk and Texture + """ + (chunk_row, data_row_start), (chunk_col, data_col_start) = next(self._iter) + + # indices for to self.buffer for this chunk + chunk_index = (chunk_row, chunk_col) + + # stop indices of big data array for this chunk + row_stop = min(self.value.shape[0] - 1, data_row_start + WGPU_MAX_TEXTURE_SIZE) + col_stop = min(self.value.shape[1] - 1, data_col_start + WGPU_MAX_TEXTURE_SIZE) + + # row and column slices that slice the data for this chunk from the big data array + data_slice = (slice(data_row_start, row_stop), slice(data_col_start, col_stop)) + + # texture for this chunk + texture = self.buffer[chunk_index] + + return texture, chunk_index, data_slice + + def __getitem__(self, item): + return self.value[item] + + def __setitem__(self, key, value): + self.value[key] = value + + for texture in self.buffer.ravel(): + texture.update_range((0, 0, 0), texture.size) + + event = FeatureEvent("data", info={"key": key, "value": value}) + self._call_event_handlers(event) + + def __len__(self): + return self.buffer.size + + +class ImageVmin(GraphicFeature): + """lower contrast limit""" + + def __init__(self, value: float): + self._value = value + super().__init__() + + @property + def value(self) -> float: + return self._value + + def set_value(self, graphic, value: float): + vmax = graphic._material.clim[1] + graphic._material.clim = (value, vmax) + self._value = value + + event = FeatureEvent(type="vmin", info={"value": value}) + self._call_event_handlers(event) + + +class ImageVmax(GraphicFeature): + """upper contrast limit""" + + def __init__(self, value: float): + self._value = value + super().__init__() + + @property + def value(self) -> float: + return self._value + + def set_value(self, graphic, value: float): + vmin = graphic._material.clim[0] + graphic._material.clim = (vmin, value) + self._value = value + + event = FeatureEvent(type="vmax", info={"value": value}) + self._call_event_handlers(event) + + +class ImageCmap(GraphicFeature): + """colormap for texture""" + + def __init__(self, value: str): + self._value = value + self.texture = get_cmap_texture(value) + super().__init__() + + @property + def value(self) -> str: + return self._value + + def set_value(self, graphic, value: str): + new_colors = make_colors(256, value) + graphic._material.map.data[:] = new_colors + graphic._material.map.update_range((0, 0, 0), size=(256, 1, 1)) + + self._value = value + event = FeatureEvent(type="cmap", info={"value": value}) + self._call_event_handlers(event) + + +class ImageInterpolation(GraphicFeature): + """Image interpolation method""" + + def __init__(self, value: str): + self._validate(value) + self._value = value + super().__init__() + + def _validate(self, value): + if value not in ["nearest", "linear"]: + raise ValueError("`interpolation` must be one of 'nearest' or 'linear'") + + @property + def value(self) -> str: + return self._value + + def set_value(self, graphic, value: str): + self._validate(value) + + graphic._material.interpolation = value + + self._value = value + event = FeatureEvent(type="interpolation", info={"value": value}) + self._call_event_handlers(event) + + +class ImageCmapInterpolation(GraphicFeature): + """Image cmap interpolation method""" + + def __init__(self, value: str): + self._validate(value) + self._value = value + super().__init__() + + def _validate(self, value): + if value not in ["nearest", "linear"]: + raise ValueError( + "`cmap_interpolation` must be one of 'nearest' or 'linear'" + ) + + @property + def value(self) -> str: + return self._value + + def set_value(self, graphic, value: str): + self._validate(value) + + # common material for all image tiles + graphic._material.map_interpolation = value + + self._value = value + event = FeatureEvent(type="cmap_interpolation", info={"value": value}) + self._call_event_handlers(event) diff --git a/fastplotlib/graphics/_features/_positions_graphics.py b/fastplotlib/graphics/_features/_positions_graphics.py new file mode 100644 index 000000000..ee7927a36 --- /dev/null +++ b/fastplotlib/graphics/_features/_positions_graphics.py @@ -0,0 +1,458 @@ +from typing import Any, List + +import numpy as np +import pygfx + +from ...utils import ( + parse_cmap_values, +) +from ._base import ( + GraphicFeature, + BufferManager, + FeatureEvent, + to_gpu_supported_dtype, +) +from .utils import parse_colors + + +class VertexColors(BufferManager): + """ + + **info dict** + +------------+-----------------------------------------------------------+----------------------------------------------------------------------------------+ + | dict key | value type | value description | + +============+===========================================================+==================================================================================+ + | key | int | slice | np.ndarray[int | bool] | tuple[slice, ...] | key at which colors were indexed/sliced | + +------------+-----------------------------------------------------------+----------------------------------------------------------------------------------+ + | value | np.ndarray | new color values for points that were changed, shape is [n_points_changed, RGBA] | + +------------+-----------------------------------------------------------+----------------------------------------------------------------------------------+ + | user_value | str | np.ndarray | tuple[float] | list[float] | list[str] | user input value that was parsed into the RGBA array | + +------------+-----------------------------------------------------------+----------------------------------------------------------------------------------+ + + """ + + def __init__( + self, + colors: str | np.ndarray | tuple[float] | list[float] | list[str], + n_colors: int, + alpha: float = None, + isolated_buffer: bool = True, + ): + """ + Manages the vertex color buffer for :class:`LineGraphic` or :class:`ScatterGraphic` + + Parameters + ---------- + colors: str | np.ndarray | tuple[float, float, float, float] | list[str] | list[float] | int | float + specify colors as a single human-readable string, RGBA array, + or an iterable of strings or RGBA arrays + + n_colors: int + number of colors, if passing in a single str or single RGBA array + + alpha: float, optional + alpha value for the colors + + """ + data = parse_colors(colors, n_colors, alpha) + + super().__init__(data=data, isolated_buffer=isolated_buffer) + + def __setitem__( + self, + key: int | slice | np.ndarray[int | bool] | tuple[slice, ...], + user_value: str | np.ndarray | tuple[float] | list[float] | list[str], + ): + user_key = key + + if isinstance(key, tuple): + # directly setting RGBA values for points, we do no parsing + if not isinstance(user_value, (int, float, np.ndarray)): + raise TypeError( + "Can only set from int, float, or array to set colors directly by slicing the entire array" + ) + value = user_value + + elif isinstance(key, int): + # set color of one point + n_colors = 1 + value = parse_colors(user_value, n_colors) + + elif isinstance(key, slice): + # find n_colors by converting slice to range and then parse colors + start, stop, step = key.indices(self.value.shape[0]) + + n_colors = len(range(start, stop, step)) + + value = parse_colors(user_value, n_colors) + + elif isinstance(key, (np.ndarray, list)): + if isinstance(key, list): + # convert to array + key = np.array(key) + + # make sure it's 1D + if not key.ndim == 1: + raise TypeError( + "If slicing colors with an array, it must be a 1D bool or int array" + ) + + if key.dtype == bool: + # make sure len is same + if not key.size == self.buffer.data.shape[0]: + raise IndexError( + f"Length of array for fancy indexing must match number of datapoints.\n" + f"There are {len(self.buffer.data.shape[0])} datapoints and you have passed {key.size} indices" + ) + n_colors = np.count_nonzero(key) + + elif np.issubdtype(key.dtype, np.integer): + n_colors = key.size + + else: + raise TypeError( + "If slicing colors with an array, it must be a 1D bool or int array" + ) + + value = parse_colors(user_value, n_colors) + + else: + raise TypeError( + f"invalid key for setting colors, you may set colors using integer indices, slices, or " + f"fancy indexing using an array of integers or bool" + ) + + self.buffer.data[key] = value + + self._update_range(key) + + if len(self._event_handlers) < 1: + return + + event_info = { + "key": user_key, + "value": value, + "user_value": user_value, + } + + event = FeatureEvent("colors", info=event_info) + self._call_event_handlers(event) + + def __len__(self): + return len(self.buffer.data) + + +# Manages uniform color for line or scatter material +class UniformColor(GraphicFeature): + def __init__( + self, value: str | np.ndarray | tuple | list | pygfx.Color, alpha: float = 1.0 + ): + v = (*tuple(pygfx.Color(value))[:-1], alpha) # apply alpha + self._value = pygfx.Color(v) + super().__init__() + + @property + def value(self) -> pygfx.Color: + return self._value + + def set_value(self, graphic, value: str | np.ndarray | tuple | list | pygfx.Color): + value = pygfx.Color(value) + graphic.world_object.material.color = value + self._value = value + + event = FeatureEvent(type="colors", info={"value": value}) + self._call_event_handlers(event) + + +# manages uniform size for scatter material +class UniformSize(GraphicFeature): + def __init__(self, value: int | float): + self._value = float(value) + super().__init__() + + @property + def value(self) -> float: + return self._value + + def set_value(self, graphic, value: float | int): + graphic.world_object.material.size = float(value) + self._value = value + + event = FeatureEvent(type="sizes", info={"value": value}) + self._call_event_handlers(event) + + +class VertexPositions(BufferManager): + """ + +----------+----------------------------------------------------------+------------------------------------------------------------------------------------------+ + | dict key | value type | value description | + +==========+==========================================================+==========================================================================================+ + | key | int | slice | np.ndarray[int | bool] | tuple[slice, ...] | key at which vertex positions data were indexed/sliced | + +----------+----------------------------------------------------------+------------------------------------------------------------------------------------------+ + | value | np.ndarray | float | list[float] | new data values for points that were changed, shape depends on the indices that were set | + +----------+----------------------------------------------------------+------------------------------------------------------------------------------------------+ + + """ + + def __init__(self, data: Any, isolated_buffer: bool = True): + """ + Manages the vertex positions buffer shown in the graphic. + Supports fancy indexing if the data array also supports it. + """ + + data = self._fix_data(data) + super().__init__(data, isolated_buffer=isolated_buffer) + + def _fix_data(self, data): + # data = to_gpu_supported_dtype(data) + + if data.ndim == 1: + # if user provides a 1D array, assume these are y-values + data = np.column_stack([np.arange(data.size, dtype=data.dtype), data]) + + if data.shape[1] != 3: + if data.shape[1] != 2: + raise ValueError(f"Must pass 1D, 2D or 3D data") + + # zeros for z + zs = np.zeros(data.shape[0], dtype=data.dtype) + + # column stack [x, y, z] to make data of shape [n_points, 3] + data = np.column_stack([data[:, 0], data[:, 1], zs]) + + return to_gpu_supported_dtype(data) + + def __setitem__( + self, + key: int | slice | np.ndarray[int | bool] | tuple[slice, ...], + value: np.ndarray | float | list[float], + ): + # directly use the key to slice the buffer + self.buffer.data[key] = value + + # _update_range handles parsing the key to + # determine offset and size for GPU upload + self._update_range(key) + + self._emit_event("data", key, value) + + def __len__(self): + return len(self.buffer.data) + + +class PointsSizesFeature(BufferManager): + """ + +----------+-------------------------------------------------------------------+----------------------------------------------+ + | dict key | value type | value description | + +==========+===================================================================+==============================================+ + | key | int | slice | np.ndarray[int | bool] | list[int | bool] | key at which point sizes indexed/sliced | + +----------+-------------------------------------------------------------------+----------------------------------------------+ + | value | int | float | np.ndarray | list[int | float] | tuple[int | float] | new size values for points that were changed | + +----------+-------------------------------------------------------------------+----------------------------------------------+ + """ + + def __init__( + self, + sizes: int | float | np.ndarray | list[int | float] | tuple[int | float], + n_datapoints: int, + isolated_buffer: bool = True, + ): + """ + Manages sizes buffer of scatter points. + """ + sizes = self._fix_sizes(sizes, n_datapoints) + super().__init__(data=sizes, isolated_buffer=isolated_buffer) + + def _fix_sizes( + self, + sizes: int | float | np.ndarray | list[int | float] | tuple[int | float], + n_datapoints: int, + ): + if np.issubdtype(type(sizes), np.number): + # single value given + sizes = np.full( + n_datapoints, sizes, dtype=np.float32 + ) # force it into a float to avoid weird gpu errors + + elif isinstance( + sizes, (np.ndarray, tuple, list) + ): # if it's not a ndarray already, make it one + sizes = np.asarray(sizes, dtype=np.float32) # read it in as a numpy.float32 + if (sizes.ndim != 1) or (sizes.size != n_datapoints): + raise ValueError( + f"sequence of `sizes` must be 1 dimensional with " + f"the same length as the number of datapoints" + ) + + else: + raise TypeError( + "sizes must be a single , , or a sequence (array, list, tuple) of int" + "or float with the length equal to the number of datapoints" + ) + + if np.count_nonzero(sizes < 0) > 1: + raise ValueError( + "All sizes must be positive numbers greater than or equal to 0.0." + ) + + return sizes + + def __setitem__( + self, + key: int | slice | np.ndarray[int | bool] | list[int | bool], + value: int | float | np.ndarray | list[int | float] | tuple[int | float], + ): + # this is a very simple 1D buffer, no parsing required, directly set buffer + self.buffer.data[key] = value + self._update_range(key) + + self._emit_event("sizes", key, value) + + def __len__(self): + return len(self.buffer.data) + + +class Thickness(GraphicFeature): + """line thickness""" + + def __init__(self, value: float): + self._value = value + super().__init__() + + @property + def value(self) -> float: + return self._value + + def set_value(self, graphic, value: float): + graphic.world_object.material.thickness = value + self._value = value + + event = FeatureEvent(type="thickness", info={"value": value}) + self._call_event_handlers(event) + + +class VertexCmap(BufferManager): + """ + Sliceable colormap feature, manages a VertexColors instance and + provides a way to set colormaps with arbitrary transforms + """ + + def __init__( + self, + vertex_colors: VertexColors, + cmap_name: str | None, + transform: np.ndarray | None, + alpha: float = 1.0, + ): + super().__init__(data=vertex_colors.buffer) + + self._vertex_colors = vertex_colors + self._cmap_name = cmap_name + self._transform = transform + self._alpha = alpha + + if self._cmap_name is not None: + if not isinstance(self._cmap_name, str): + raise TypeError( + f"cmap name must be of type , you have passed: {self._cmap_name} of type: {type(self._cmap_name)}" + ) + + if self._transform is not None: + self._transform = np.asarray(self._transform) + + n_datapoints = vertex_colors.value.shape[0] + + colors = parse_cmap_values( + n_colors=n_datapoints, + cmap_name=self._cmap_name, + transform=self._transform, + ) + colors[:, -1] = alpha + # set vertex colors from cmap + self._vertex_colors[:] = colors + + def __setitem__(self, key: slice, cmap_name): + if not isinstance(key, slice): + raise TypeError( + "fancy indexing not supported for VertexCmap, only slices " + "of a continuous are supported for apply a cmap" + ) + if key.step is not None: + raise TypeError( + "step sized indexing not currently supported for setting VertexCmap, " + "slices must be a continuous region" + ) + + # parse slice + start, stop, step = key.indices(self.value.shape[0]) + n_elements = len(range(start, stop, step)) + + colors = parse_cmap_values( + n_colors=n_elements, cmap_name=cmap_name, transform=self._transform + ) + colors[:, -1] = self.alpha + + self._cmap_name = cmap_name + self._vertex_colors[key] = colors + + # TODO: should we block vertex_colors from emitting an event? + # Because currently this will result in 2 emitted events, one + # for cmap and another from the colors + self._emit_event("cmap", key, cmap_name) + + @property + def name(self) -> str: + return self._cmap_name + + @property + def transform(self) -> np.ndarray | None: + """Get or set the cmap transform. Maps values from the transform array to the cmap colors""" + return self._transform + + @transform.setter + def transform( + self, + values: np.ndarray | list[float | int], + indices: slice | list | np.ndarray = None, + ): + if self._cmap_name is None: + raise AttributeError( + "cmap name is not set, set the cmap name before setting the transform" + ) + + values = np.asarray(values) + + colors = parse_cmap_values( + n_colors=self.value.shape[0], cmap_name=self._cmap_name, transform=values + ) + + colors[:, -1] = self.alpha + + self._transform = values + + if indices is None: + indices = slice(None) + + self._vertex_colors[indices] = colors + + self._emit_event("cmap.transform", indices, values) + + @property + def alpha(self) -> float: + """Get or set the alpha level""" + return self._alpha + + @alpha.setter + def alpha(self, value: float, indices: slice | list | np.ndarray = None): + self._vertex_colors[indices, -1] = value + self._alpha = value + + self._emit_event("cmap.alpha", indices, value) + + def __len__(self): + raise NotImplementedError( + "len not implemented for `cmap`, use len(colors) instead" + ) + + def __repr__(self): + return f"{self.__class__.__name__} | cmap: {self.name}\ntransform: {self.transform}" diff --git a/fastplotlib/graphics/_features/_present.py b/fastplotlib/graphics/_features/_present.py deleted file mode 100644 index a73d66523..000000000 --- a/fastplotlib/graphics/_features/_present.py +++ /dev/null @@ -1,72 +0,0 @@ -from pygfx import Scene, Group - -from ._base import GraphicFeature, FeatureEvent - - -class PresentFeature(GraphicFeature): - """ - Toggles if the object is present in the scene, different from visible. - Useful for computing bounding boxes from the Scene to only include graphics - that are present. - - **event pick info:** - - ==================== ======================== ========================================================================= - key type description - ==================== ======================== ========================================================================= - "index" ``None`` not used - "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().__init__(parent, present, collection_index) - - def _set(self, present: bool): - present = self._parse_set_value(present) - - i = 0 - wo = self._parent.world_object - while not isinstance(self._scene, (Group, Scene)): - wo_parent = wo.parent - self._scene = wo_parent - wo = wo_parent - i += 1 - - if i > 100: - raise RecursionError( - "Exceeded scene graph depth threshold, cannot find Scene associated with" - "this graphic." - ) - - if present: - if self._parent.world_object not in self._scene.children: - self._scene.add(self._parent.world_object) - - else: - if self._parent.world_object in self._scene.children: - self._scene.remove(self._parent.world_object) - - self._data = present - self._feature_changed(key=None, new_data=present) - - def _feature_changed(self, key, new_data): - # this is a non-indexable feature so key=None - - pick_info = { - "index": None, - "collection-index": self._collection_index, - "world_object": self._parent.world_object, - "new_data": new_data, - } - - event_data = FeatureEvent(type="present", pick_info=pick_info) - - self._call_event_handlers(event_data) - - def __repr__(self) -> str: - s = f"PresentFeature for {self._parent}, call `.present()` to get values" - return s diff --git a/fastplotlib/graphics/_features/_selection_features.py b/fastplotlib/graphics/_features/_selection_features.py index 21e5d0a09..71ba53425 100644 --- a/fastplotlib/graphics/_features/_selection_features.py +++ b/fastplotlib/graphics/_features/_selection_features.py @@ -1,4 +1,4 @@ -from typing import Tuple, Union, Any +from typing import Sequence import numpy as np @@ -7,196 +7,186 @@ 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 + **additional event attributes:** - **event pick info** + +--------------------+----------+------------------------------------+ + | attribute | type | description | + +====================+==========+====================================+ + | get_selected_index | callable | returns indices under the selector | + +--------------------+----------+------------------------------------+ - =================== =============================== ================================================================================================= - 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 - =================== =============================== ================================================================================================= + **info dict:** + + +----------+------------+-------------------------------+ + | dict key | value type | value description | + +==========+============+===============================+ + | value | np.ndarray | new x or y value of selection | + +----------+------------+-------------------------------+ """ - def __init__(self, parent, axis: str, value: float, limits: Tuple[int, int]): - super().__init__(parent, data=value) + def __init__(self, axis: str, value: float, limits: tuple[float, float]): + """ - self._axis = axis - self._limits = limits + Parameters + ---------- + axis: "x" | "y" + axis the selector is restricted to - def _set(self, value: float): - if not (self._limits[0] <= value <= self._limits[1]): - return + value: float + position of the slider in world space, NOT data space + limits: (float, float) + min, max limits of the selector - 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) + super().__init__() - def _feature_changed(self, key: Union[int, slice, Tuple[slice]], new_data: Any): - if len(self._event_handlers) < 1: - return + self._axis = axis + self._limits = limits + self._value = value + + @property + def value(self) -> float: + """ + selection, data x or y value + """ + return self._value - if self._parent.parent is not None: - g_ix = self._parent.get_selected_index() - else: - g_ix = None + def set_value(self, selector, value: float): + # clip value between limits + value = np.clip(value, self._limits[0], self._limits[1]) - # get pygfx event and reset it - pygfx_ev = self._parent._pygfx_event - self._parent._pygfx_event = None + # set position + if self._axis == "x": + dim = 0 + elif self._axis == "y": + dim = 1 - 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, - } + for edge in selector._edges: + edge.geometry.positions.data[:, dim] = value + edge.geometry.positions.update_range() - event_data = FeatureEvent(type="selection", pick_info=pick_info) + self._value = value - self._call_event_handlers(event_data) + event = FeatureEvent("selection", {"value": value}) + event.get_selected_index = selector.get_selected_index - def __repr__(self) -> str: - s = f"LinearSelectionFeature for {self._parent}" - return s + self._call_event_handlers(event) 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) - ===================== =============================== ======================================================================================= + **additional event attributes:** + + +----------------------+----------+------------------------------------+ + | attribute | type | description | + +======================+==========+====================================+ + | get_selected_indices | callable | returns indices under the selector | + +----------------------+----------+------------------------------------+ + | get_selected_data | callable | returns data under the selector | + +----------------------+----------+------------------------------------+ + + **info dict:** + + +----------+------------+-----------------------------+ + | dict key | value type | value description | + +==========+============+=============================+ + | value | np.ndarray | new [min, max] of selection | + +----------+------------+-----------------------------+ """ - def __init__( - self, parent, selection: Tuple[int, int], axis: str, limits: Tuple[int, int] - ): - super().__init__(parent, data=selection) + def __init__(self, value: tuple[int, int], axis: str, limits: tuple[float, float]): + super().__init__() self._axis = axis self._limits = limits + self._value = tuple(int(v) for v in value) - self._set(selection) + @property + def value(self) -> np.ndarray[float]: + """ + (min, max) of the selection, in data space + """ + return self._value @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): + def set_value(self, selector, value: Sequence[float]): + """ + Set start, stop range of selector + + Parameters + ---------- + selector: LinearRegionSelector + + value: (float, float) + (min, max) values in data space + + """ + if not len(value) == 2: raise TypeError( - "Bounds must be a tuple in the form of `(min_bound, max_bound)`, " - "where `min_bound` and `max_bound` are numeric values." + "selection must be a array, tuple, list, or sequence in the form of `(min, max)`, " + "where `min` and `max` are numeric values." ) - # make sure bounds not exceeded - for v in value: - if not (self._limits[0] <= v <= self._limits[1]): - return + # convert to array, clip values if they are beyond the limits + value = np.asarray(value, dtype=np.float32).clip(*self._limits) # 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: + if not (value[1] - value[0]) >= 0: return if self.axis == "x": # change left x position of the fill mesh - self._parent.fill.geometry.positions.data[mesh_masks.x_left] = value[0] + selector.fill.geometry.positions.data[mesh_masks.x_left] = value[0] # change right x position of the fill mesh - self._parent.fill.geometry.positions.data[mesh_masks.x_right] = value[1] + selector.fill.geometry.positions.data[mesh_masks.x_right] = value[1] # change x position of the left edge line - self._parent.edges[0].geometry.positions.data[:, 0] = value[0] + selector.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] + selector.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[mesh_masks.y_bottom] = value[0] + selector.fill.geometry.positions.data[mesh_masks.y_bottom] = value[0] # change top position of the fill mesh - self._parent.fill.geometry.positions.data[mesh_masks.y_top] = value[1] + selector.fill.geometry.positions.data[mesh_masks.y_top] = value[1] # change y position of the bottom edge line - self._parent.edges[0].geometry.positions.data[:, 1] = value[0] + selector.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] + selector.edges[1].geometry.positions.data[:, 1] = value[1] - self._data = value # (value[0], value[1]) + self._value = value # 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() + selector.fill.geometry.positions.update_range() - # calls any events - self._feature_changed(key=None, new_data=value) + selector.edges[0].geometry.positions.update_range() + selector.edges[1].geometry.positions.update_range() - def _feature_changed(self, key: Union[int, slice, Tuple[slice]], new_data: Any): + # send event 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) - - def __repr__(self) -> str: - s = f"LinearRegionSelectionFeature for {self._parent}" - return s + event = FeatureEvent("selection", {"value": self.value}) + + event.get_selected_indices = selector.get_selected_indices + event.get_selected_data = selector.get_selected_data + + self._call_event_handlers(event) + # TODO: user's selector event handlers can call event.graphic.get_selected_indices() to get the data index, + # and event.graphic.get_selected_data() to get the data under the selection + # this is probably a good idea so that the data isn't sliced until it's actually necessary diff --git a/fastplotlib/graphics/_features/_sizes.py b/fastplotlib/graphics/_features/_sizes.py deleted file mode 100644 index 2ceeb7862..000000000 --- a/fastplotlib/graphics/_features/_sizes.py +++ /dev/null @@ -1,120 +0,0 @@ -from typing import Any - -import numpy as np - -import pygfx - -from ._base import ( - GraphicFeatureIndexable, - cleanup_slice, - FeatureEvent, - to_gpu_supported_dtype, - cleanup_array_slice, -) - - -class PointsSizesFeature(GraphicFeatureIndexable): - """ - Access to the vertex buffer data shown in the graphic. - Supports fancy indexing if the data array also supports it. - """ - - def __init__(self, parent, sizes: Any, collection_index: int = None): - sizes = self._fix_sizes(sizes, parent) - super().__init__(parent, sizes, collection_index=collection_index) - - @property - def buffer(self) -> pygfx.Buffer: - return self._parent.world_object.geometry.sizes - - def __getitem__(self, item): - return self.buffer.data[item] - - def _fix_sizes(self, sizes, parent): - graphic_type = parent.__class__.__name__ - - n_datapoints = parent.data().shape[0] - if not isinstance(sizes, (list, tuple, np.ndarray)): - sizes = np.full( - n_datapoints, sizes, dtype=np.float32 - ) # force it into a float to avoid weird gpu errors - elif not isinstance( - sizes, np.ndarray - ): # if it's not a ndarray already, make it one - sizes = np.array(sizes, dtype=np.float32) # read it in as a numpy.float32 - if (sizes.ndim != 1) or (sizes.size != parent.data().shape[0]): - raise ValueError( - f"sequence of `sizes` must be 1 dimensional with " - f"the same length as the number of datapoints" - ) - - sizes = to_gpu_supported_dtype(sizes) - - if any(s < 0 for s in sizes): - raise ValueError( - "All sizes must be positive numbers greater than or equal to 0.0." - ) - - if sizes.ndim == 1: - if graphic_type == "ScatterGraphic": - sizes = np.array(sizes) - else: - raise ValueError( - f"Sizes must be an array of shape (n,) where n == the number of data points provided.\ - Received shape={sizes.shape}." - ) - - return np.array(sizes) - - def __setitem__(self, key, value): - if isinstance(key, np.ndarray): - # make sure 1D array of int or boolean - key = cleanup_array_slice(key, self._upper_bound) - - # put sizes into right shape if they're only indexing datapoints - if isinstance(key, (slice, int, np.ndarray, np.integer)): - value = self._fix_sizes(value, self._parent) - # otherwise assume that they have the right shape - # numpy will throw errors if it can't broadcast - - if value.size != self.buffer.data[key].size: - raise ValueError( - f"{value.size} is not equal to buffer size {self.buffer.data[key].size}.\ - If you want to set size to a non-scalar value, make sure it's the right length!" - ) - - self.buffer.data[key] = value - self._update_range(key) - # avoid creating dicts constantly if there are no events to handle - if len(self._event_handlers) > 0: - self._feature_changed(key, value) - - def _update_range(self, key): - self._update_range_indices(key) - - def _feature_changed(self, key, new_data): - if key is not None: - key = cleanup_slice(key, self._upper_bound) - if isinstance(key, (int, np.integer)): - indices = [key] - elif isinstance(key, slice): - indices = range(key.start, key.stop, key.step) - elif isinstance(key, np.ndarray): - indices = key - elif key is None: - indices = None - - pick_info = { - "index": indices, - "collection-index": self._collection_index, - "world_object": self._parent.world_object, - "new_data": new_data, - } - - event_data = FeatureEvent(type="sizes", pick_info=pick_info) - - self._call_event_handlers(event_data) - - def __repr__(self) -> str: - s = f"PointsSizesFeature for {self._parent}, call `.sizes()` to get values" - return s diff --git a/fastplotlib/graphics/_features/_text.py b/fastplotlib/graphics/_features/_text.py new file mode 100644 index 000000000..baa2734d5 --- /dev/null +++ b/fastplotlib/graphics/_features/_text.py @@ -0,0 +1,92 @@ +import numpy as np + +import pygfx + +from ._base import GraphicFeature, FeatureEvent + + +class TextData(GraphicFeature): + def __init__(self, value: str): + self._value = value + super().__init__() + + @property + def value(self) -> str: + return self._value + + def set_value(self, graphic, value: str): + graphic.world_object.geometry.set_text(value) + self._value = value + + event = FeatureEvent(type="text", info={"value": value}) + self._call_event_handlers(event) + + +class FontSize(GraphicFeature): + def __init__(self, value: float | int): + self._value = value + super().__init__() + + @property + def value(self) -> float | int: + return self._value + + def set_value(self, graphic, value: float | int): + graphic.world_object.geometry.font_size = value + self._value = graphic.world_object.geometry.font_size + + event = FeatureEvent(type="font_size", info={"value": value}) + self._call_event_handlers(event) + + +class TextFaceColor(GraphicFeature): + def __init__(self, value: str | np.ndarray | list[float] | tuple[float]): + self._value = pygfx.Color(value) + super().__init__() + + @property + def value(self) -> pygfx.Color: + return self._value + + def set_value(self, graphic, value: str | np.ndarray | list[float] | tuple[float]): + value = pygfx.Color(value) + graphic.world_object.material.color = value + self._value = graphic.world_object.material.color + + event = FeatureEvent(type="face_color", info={"value": value}) + self._call_event_handlers(event) + + +class TextOutlineColor(GraphicFeature): + def __init__(self, value: str | np.ndarray | list[float] | tuple[float]): + self._value = pygfx.Color(value) + super().__init__() + + @property + def value(self) -> pygfx.Color: + return self._value + + def set_value(self, graphic, value: str | np.ndarray | list[float] | tuple[float]): + value = pygfx.Color(value) + graphic.world_object.material.outline_color = value + self._value = graphic.world_object.material.outline_color + + event = FeatureEvent(type="outline_color", info={"value": value}) + self._call_event_handlers(event) + + +class TextOutlineThickness(GraphicFeature): + def __init__(self, value: float): + self._value = value + super().__init__() + + @property + def value(self) -> float: + return self._value + + def set_value(self, graphic, value: float): + graphic.world_object.material.outline_thickness = value + self._value = graphic.world_object.material.outline_thickness + + event = FeatureEvent(type="outline_thickness", info={"value": value}) + self._call_event_handlers(event) diff --git a/fastplotlib/graphics/_features/_thickness.py b/fastplotlib/graphics/_features/_thickness.py deleted file mode 100644 index fc90ef96f..000000000 --- a/fastplotlib/graphics/_features/_thickness.py +++ /dev/null @@ -1,46 +0,0 @@ -from ._base import GraphicFeature, FeatureEvent - - -class ThicknessFeature(GraphicFeature): - """ - Used by Line graphics for line material thickness. - - **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().__init__(parent, thickness) - - def _set(self, value: float): - value = self._parse_set_value(value) - - self._parent.world_object.material.thickness = value - self._feature_changed(key=None, new_data=value) - - def _feature_changed(self, key, new_data): - # this is a non-indexable feature so key=None - - pick_info = { - "index": None, - "collection-index": self._collection_index, - "world_object": self._parent.world_object, - "new_data": new_data, - } - - event_data = FeatureEvent(type="thickness", pick_info=pick_info) - - self._call_event_handlers(event_data) - - def __repr__(self) -> str: - s = f"ThicknessFeature for {self._parent}, call `.thickness()` to get value" - return s diff --git a/fastplotlib/graphics/_features/utils.py b/fastplotlib/graphics/_features/utils.py new file mode 100644 index 000000000..e2f6e3428 --- /dev/null +++ b/fastplotlib/graphics/_features/utils.py @@ -0,0 +1,87 @@ +import pygfx +import numpy as np + +from ._base import to_gpu_supported_dtype +from ...utils import make_pygfx_colors + + +def parse_colors( + colors: str | np.ndarray | list[str] | tuple[str], + n_colors: int | None, + alpha: float | None = None, +): + """ + + Parameters + ---------- + colors + n_colors + alpha + key + + Returns + ------- + + """ + + # if provided as a numpy array of str + if isinstance(colors, np.ndarray): + if colors.dtype.kind in ["U", "S"]: + colors = colors.tolist() + # 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) + # 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: + raise ValueError( + "Valid array color arguments must be a single RGBA array or a stack of " + "RGBA arrays for each datapoint in the shape [n_datapoints, 4]" + ) + data = colors + else: + raise ValueError( + "Valid array color arguments must be a single RGBA array or a stack of " + "RGBA arrays for each datapoint in the shape [n_datapoints, 4]" + ) + + # if the color is provided as list or tuple + elif isinstance(colors, (list, tuple)): + # if iterable of str + if all([isinstance(val, str) for val in colors]): + if not len(colors) == n_colors: + raise ValueError( + f"Valid iterable color arguments must be a `tuple` or `list` of `str` " + f"where the length of the iterable is the same as the number of datapoints." + ) + + 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 = pygfx.Color(colors) + data = np.repeat(np.array([c]), n_colors, axis=0) + + else: + raise ValueError( + f"Valid iterable color arguments must be a `tuple` or `list` representing RGBA values or " + f"an iterable of `str` with the same length as the number of datapoints." + ) + elif isinstance(colors, str): + if colors == "random": + data = np.random.rand(n_colors, 4) + data[:, -1] = alpha + else: + data = make_pygfx_colors(colors, n_colors) + else: + # assume it's a single color, use pygfx.Color to parse it + data = make_pygfx_colors(colors, n_colors) + + if alpha is not None: + if isinstance(alpha, float): + data[:, -1] = alpha + else: + raise TypeError("if alpha is provided it must be of type `float`") + + return to_gpu_supported_dtype(data) diff --git a/fastplotlib/graphics/_positions_base.py b/fastplotlib/graphics/_positions_base.py new file mode 100644 index 000000000..3727087cc --- /dev/null +++ b/fastplotlib/graphics/_positions_base.py @@ -0,0 +1,185 @@ +from typing import Any + +import numpy as np + +import pygfx +from ._base import Graphic +from ._features import ( + VertexPositions, + VertexColors, + UniformColor, + VertexCmap, + PointsSizesFeature, +) + + +class PositionsGraphic(Graphic): + """Base class for LineGraphic and ScatterGraphic""" + + @property + def data(self) -> VertexPositions: + """Get or set the vertex positions data""" + return self._data + + @data.setter + def data(self, value): + self._data[:] = value + + @property + def colors(self) -> VertexColors | pygfx.Color: + """Get or set the colors data""" + if isinstance(self._colors, VertexColors): + return self._colors + + elif isinstance(self._colors, UniformColor): + return self._colors.value + + @colors.setter + def colors(self, value: str | np.ndarray | tuple[float] | list[float] | list[str]): + if isinstance(self._colors, VertexColors): + self._colors[:] = value + + elif isinstance(self._colors, UniformColor): + self._colors.set_value(self, value) + + @property + def cmap(self) -> VertexCmap: + """Control the cmap, cmap transform, or cmap alpha""" + return self._cmap + + @cmap.setter + def cmap(self, name: str): + if self._cmap is None: + raise BufferError("Cannot use cmap with uniform_colors=True") + + self._cmap[:] = name + + def __init__( + self, + data: Any, + colors: str | np.ndarray | tuple[float] | list[float] | list[str] = "w", + uniform_color: bool = False, + alpha: float = 1.0, + cmap: str | VertexCmap = None, + cmap_transform: np.ndarray = None, + isolated_buffer: bool = True, + *args, + **kwargs, + ): + if isinstance(data, VertexPositions): + self._data = data + else: + self._data = VertexPositions(data, isolated_buffer=isolated_buffer) + + if cmap_transform is not None and cmap is None: + raise ValueError("must pass `cmap` if passing `cmap_transform`") + + if cmap is not None: + # if a cmap is specified it overrides colors argument + if uniform_color: + raise TypeError("Cannot use cmap if uniform_color=True") + + if isinstance(cmap, str): + # make colors from cmap + if isinstance(colors, VertexColors): + # share buffer with existing colors instance for the cmap + self._colors = colors + self._colors._shared += 1 + else: + # create vertex colors buffer + self._colors = VertexColors("w", n_colors=self._data.value.shape[0]) + # make cmap using vertex colors buffer + self._cmap = VertexCmap( + self._colors, + cmap_name=cmap, + transform=cmap_transform, + alpha=alpha, + ) + elif isinstance(cmap, VertexCmap): + # use existing cmap instance + self._cmap = cmap + self._colors = cmap._vertex_colors + else: + raise TypeError( + "`cmap` argument must be a cmap name or an existing `VertexCmap` instance" + ) + else: + # no cmap given + if isinstance(colors, VertexColors): + # share buffer with existing colors instance + self._colors = colors + self._colors._shared += 1 + # blank colormap instance + self._cmap = VertexCmap( + self._colors, cmap_name=None, transform=None, alpha=alpha + ) + else: + if uniform_color: + if not isinstance(colors, str): # not a single color + if not len(colors) in [3, 4]: # not an RGB(A) array + raise TypeError( + "must pass a single color if using `uniform_colors=True`" + ) + self._colors = UniformColor(colors, alpha=alpha) + self._cmap = None + else: + self._colors = VertexColors( + colors, + n_colors=self._data.value.shape[0], + alpha=alpha, + ) + self._cmap = VertexCmap( + self._colors, cmap_name=None, transform=None, alpha=alpha + ) + + super().__init__(*args, **kwargs) + + def unshare_property(self, property: str): + """unshare a shared property. Experimental and untested!""" + if not isinstance(property, str): + raise TypeError + + f = getattr(self, property) + if f.shared == 0: + raise BufferError("Cannot detach an independent buffer") + + if property == "colors" and isinstance(property, VertexColors): + self._colors._buffer = pygfx.Buffer(self._colors.value.copy()) + self.world_object.geometry.colors = self._colors.buffer + self._colors._shared -= 1 + + elif property == "data": + self._data._buffer = pygfx.Buffer(self._data.value.copy()) + self.world_object.geometry.positions = self._data.buffer + self._data._shared -= 1 + + elif property == "sizes": + self._sizes._buffer = pygfx.Buffer(self._sizes.value.copy()) + self.world_object.geometry.positions = self._sizes.buffer + self._sizes._shared -= 1 + + def share_property( + self, property: VertexPositions | VertexColors | PointsSizesFeature + ): + """share a property from another graphic. Experimental and untested!""" + if isinstance(property, VertexPositions): + # TODO: check if this causes a memory leak + self._data._shared -= 1 + + self._data = property + self._data._shared += 1 + self.world_object.geometry.positions = self._data.buffer + + elif isinstance(property, VertexColors): + self._colors._shared -= 1 + + self._colors = property + self._colors._shared += 1 + self.world_object.geometry.colors = self._colors.buffer + + elif isinstance(property, PointsSizesFeature): + self._sizes._shared -= 1 + + self._sizes = property + self._sizes._shared += 1 + self.world_object.geometry.sizes = self._sizes.buffer diff --git a/fastplotlib/graphics/histogram.py b/fastplotlib/graphics/histogram.py deleted file mode 100644 index b78be39d3..000000000 --- a/fastplotlib/graphics/histogram.py +++ /dev/null @@ -1,114 +0,0 @@ -from warnings import warn -from typing import Union, Dict - -import numpy as np - -import pygfx - -from ._base import Graphic - - -class _HistogramBin(pygfx.Mesh): - def __int__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.bin_center: float = None - self.frequency: Union[int, float] = None - - -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, - ): - """ - Create a Histogram Graphic - - Parameters - ---------- - data: np.ndarray or None, optional - data to create a histogram from, can be ``None`` if pre-computed values are provided to ``pre_computed`` - - bins: int or str, default is "auto", optional - this is directly just passed to ``numpy.histogram`` - - pre_computed: dict in the form {"hist": vals, "bin_edges" : vals}, optional - pre-computed histogram values - - colors: np.ndarray, optional - - draw_scale_factor: float, default ``100.0``, optional - scale the drawing of the entire Graphic - - draw_bin_width_scale: float, default ``1.0`` - scale the drawing of the bin widths - - kwargs - passed to Graphic - """ - - 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 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" - ) - 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 - - 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 - ) - - n_bins = x_positions_bins.shape[0] - bin_width = (draw_scale_factor / n_bins) * draw_bin_width_scale - - self.hist = self.hist.astype(np.float32) - - 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" - ) - self.hist[self.hist == bad_val] = 0 - - data = np.vstack([x_positions_bins, self.hist]) - - super().__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 - ): - geometry = pygfx.plane_geometry( - width=bin_width, - height=y_val, - ) - - material = pygfx.MeshBasicMaterial() - hist_bin_graphic = _HistogramBin(geometry, material) - hist_bin_graphic.position.set(x_val, (y_val) / 2, 0) - hist_bin_graphic.bin_center = bin_center - 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 ce736dab2..d6576c12d 100644 --- a/fastplotlib/graphics/image.py +++ b/fastplotlib/graphics/image.py @@ -1,202 +1,77 @@ from typing import * -from math import ceil -from itertools import product import weakref -import numpy as np - import pygfx from ..utils import quick_min_max -from ._base import Graphic, Interaction +from ._base import Graphic from .selectors import LinearSelector, LinearRegionSelector from ._features import ( - ImageCmapFeature, - ImageDataFeature, - HeatmapDataFeature, - HeatmapCmapFeature, - to_gpu_supported_dtype, + TextureArray, + ImageCmap, + ImageVmin, + ImageVmax, + ImageInterpolation, + ImageCmapInterpolation, ) -class _AddSelectorsMixin: - def add_linear_selector( - self, selection: int = None, padding: float = None, **kwargs - ) -> LinearSelector: - """ - Adds a :class:`.LinearSelector`. - - Parameters - ---------- - selection: int, optional - initial position of the selector - - padding: float, optional - pad the length of the selector - - kwargs: - passed to :class:`.LinearSelector` - - Returns - ------- - LinearSelector - - """ - - # default padding is 15% the height or width of the image - if "axis" in kwargs.keys(): - axis = kwargs["axis"] - else: - axis = "x" - - ( - 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}" - ) - - selector = LinearSelector( - selection=selection, - limits=limits, - end_points=end_points, - parent=weakref.proxy(self), - **kwargs, - ) - - self._plot_area.add_graphic(selector, center=False) - selector.position_z = self.position_z + 1 - - return weakref.proxy(selector) - - def add_linear_region_selector( - self, padding: float = None, **kwargs - ) -> LinearRegionSelector: - """ - 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 - passed to ``LinearRegionSelector`` +class _ImageTile(pygfx.Image): + """ + Similar to pygfx.Image, only difference is that it modifies the pick_info + by adding the data row start indices that correspond to this chunk of the big image + """ - Returns - ------- - LinearRegionSelector - linear selection graphic + def __init__( + self, + geometry, + material, + data_slice: tuple[slice, slice], + chunk_index: tuple[int, int], + **kwargs, + ): + super().__init__(geometry, material, **kwargs) - """ + self._data_slice = data_slice + self._chunk_index = chunk_index - ( - bounds_init, - limits, - size, - origin, - axis, - end_points, - ) = self._get_linear_selector_init_args(padding, **kwargs) + def _wgpu_get_pick_info(self, pick_value): + pick_info = super()._wgpu_get_pick_info(pick_value) - # create selector - selector = LinearRegionSelector( - bounds=bounds_init, - limits=limits, - size=size, - origin=origin, - parent=weakref.proxy(self), - fill_color=(0, 0, 0.35, 0.2), - **kwargs, + data_row_start, data_col_start = ( + self.data_slice[0].start, + self.data_slice[1].start, ) - self._plot_area.add_graphic(selector, center=False) - # so that it is above this graphic - selector.position_z = self.position_z + 3 - - # PlotArea manages this for garbage collection etc. just like all other Graphics - # so we should only work with a proxy on the user-end - return weakref.proxy(selector) - - # TODO: this method is a bit of a mess, can refactor later - def _get_linear_selector_init_args(self, padding: float, **kwargs): - # computes initial bounds, limits, size and origin of linear selectors - data = self.data() - - if "axis" in kwargs.keys(): - axis = kwargs["axis"] - else: - axis = "x" - - if padding is None: - if axis == "x": - # based on number of rows - padding = int(data.shape[0] * 0.15) - elif axis == "y": - # based on number of columns - padding = int(data.shape[1] * 0.15) - - if axis == "x": - offset = self.position_x - # x limits, number of columns - limits = (offset, data.shape[1] - 1) - - # size is number of rows + padding - # used by LinearRegionSelector but not LinearSelector - size = data.shape[0] + padding - - # initial position of the selector - # center row - position_y = data.shape[0] / 2 - - # need y offset too for this - origin = (limits[0] - offset, position_y + self.position_y) - - # endpoints of the data range - # used by linear selector but not linear region - # padding, n_rows + padding - end_points = (0 - padding, data.shape[0] + padding) - else: - offset = self.position_y - # y limits - limits = (offset, data.shape[0] - 1) - - # width + padding - # used by LinearRegionSelector but not LinearSelector - size = data.shape[1] + padding - - # initial position of the selector - position_x = data.shape[1] / 2 + # add the actual data row and col start indices + x, y = pick_info["index"] + x += data_col_start + y += data_row_start + pick_info["index"] = (x, y) - # need x offset too for this - origin = (position_x + self.position_x, limits[0] - offset) + xp, yp = pick_info["pixel_coord"] + xp += data_col_start + yp += data_row_start + pick_info["pixel_coord"] = (xp, yp) - # endpoints of the data range - # used by linear selector but not linear region - end_points = (0 - padding, data.shape[1] + padding) - - # initial bounds are 20% of the limits range - # used by LinearRegionSelector but not LinearSelector - bounds_init = (limits[0], int(np.ptp(limits) * 0.2) + offset) + # add row chunk and col chunk index to pick_info dict + return { + **pick_info, + "data_slice": self.data_slice, + "chunk_index": self.chunk_index, + } - return bounds_init, limits, size, origin, axis, end_points + @property + def data_slice(self) -> tuple[slice, slice]: + return self._data_slice - def _add_plot_area_hook(self, plot_area): - self._plot_area = plot_area + @property + def chunk_index(self) -> tuple[int, int]: + return self._chunk_index -class ImageGraphic(Graphic, Interaction, _AddSelectorsMixin): - feature_events = {"data", "cmap", "present"} +class ImageGraphic(Graphic): + _features = {"data", "cmap", "vmin", "vmax", "interpolation", "cmap_interpolation"} def __init__( self, @@ -204,9 +79,9 @@ def __init__( vmin: int = None, vmax: int = None, cmap: str = "plasma", - filter: str = "nearest", + interpolation: str = "nearest", + cmap_interpolation: str = "linear", isolated_buffer: bool = True, - *args, **kwargs, ): """ @@ -216,8 +91,7 @@ def __init__( ---------- 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]`` + | shape must be ``[x_dim, y_dim]`` vmin: int, optional minimum value for color scaling, calculated from data if not provided @@ -226,265 +100,289 @@ def __init__( 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 + colormap to use to display the data - filter: str, optional, default "nearest" + interpolation: str, optional, default "nearest" interpolation filter, one of "nearest" or "linear" + cmap_interpolation: str, optional, default "linear" + colormap interpolation method, 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 - """ - super().__init__(*args, **kwargs) + super().__init__(**kwargs) - data = to_gpu_supported_dtype(data) + world_object = pygfx.Group() - # TODO: we need to organize and do this better - if isolated_buffer: - # initialize a buffer with the same shape as the input data - # we do not directly use the input data array as the buffer - # because if the input array is a read-only type, such as - # numpy memmaps, we would not be able to change the image data - buffer_init = np.zeros(shape=data.shape, dtype=data.dtype) - else: - buffer_init = data + # texture array that manages the textures on the GPU for displaying this image + self._data = TextureArray(data, isolated_buffer=isolated_buffer) if (vmin is None) or (vmax is None): vmin, vmax = quick_min_max(data) - texture = pygfx.Texture(buffer_init, dim=2) + # other graphic features + self._vmin = ImageVmin(vmin) + self._vmax = ImageVmax(vmax) - geometry = pygfx.Geometry(grid=texture) + self._cmap = ImageCmap(cmap) - self.cmap = ImageCmapFeature(self, cmap) + self._interpolation = ImageInterpolation(interpolation) + self._cmap_interpolation = ImageCmapInterpolation(cmap_interpolation) - # if data is RGB or RGBA - if data.ndim > 2: - material = pygfx.ImageBasicMaterial( - clim=(vmin, vmax), map_interpolation=filter, pick_write=True - ) - # if data is just 2D without color information, use colormap LUT + # use cmap if not RGB + if self._data.value.ndim == 2: + _map = self._cmap.texture else: - material = pygfx.ImageBasicMaterial( - clim=(vmin, vmax), - map=self.cmap(), - map_interpolation=filter, - pick_write=True, - ) + _map = None - world_object = pygfx.Image(geometry, material) + # one common material is used for every Texture chunk + self._material = pygfx.ImageBasicMaterial( + clim=(vmin, vmax), + map=_map, + interpolation=self._interpolation.value, + map_interpolation=self._cmap_interpolation.value, + pick_write=True, + ) - self._set_world_object(world_object) + # iterate through each texture chunk and create + # an _ImageTIle, offset the tile using the data indices + for texture, chunk_index, data_slice in self._data: - self.cmap.vmin = vmin - self.cmap.vmax = vmax + # create an ImageTile using the texture for this chunk + img = _ImageTile( + geometry=pygfx.Geometry(grid=texture), + material=self._material, + data_slice=data_slice, # used to parse pick_info + chunk_index=chunk_index, + ) - self.data = ImageDataFeature(self, data) - # TODO: we need to organize and do this better - if isolated_buffer: - # if the buffer was initialized with zeros - # set it with the actual data - self.data = data + # row and column start index for this chunk + data_row_start = data_slice[0].start + data_col_start = data_slice[1].start - def set_feature(self, feature: str, new_data: Any, indices: Any): - pass + # offset tile position using the indices from the big data array + # that correspond to this chunk + img.world.x = data_col_start + img.world.y = data_row_start - def reset_feature(self, feature: str): - pass + world_object.add(img) + self._set_world_object(world_object) -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 - """ + @property + def data(self) -> TextureArray: + """Get or set the image data""" + return self._data - def _wgpu_get_pick_info(self, pick_value): - pick_info = super()._wgpu_get_pick_info(pick_value) + @data.setter + def data(self, data): + self._data[:] = data - # add row chunk and col chunk index to pick_info dict - return { - **pick_info, - "row_chunk_index": self.row_chunk_index, - "col_chunk_index": self.col_chunk_index, - } + @property + def cmap(self) -> str: + """colormap name""" + return self._cmap.value + + @cmap.setter + def cmap(self, name: str): + self._cmap.set_value(self, name) @property - def row_chunk_index(self) -> int: - return self._row_chunk_index + def vmin(self) -> float: + """lower contrast limit""" + return self._vmin.value - @row_chunk_index.setter - def row_chunk_index(self, index: int): - self._row_chunk_index = index + @vmin.setter + def vmin(self, value: float): + self._vmin.set_value(self, value) @property - def col_chunk_index(self) -> int: - return self._col_chunk_index + def vmax(self) -> float: + """upper contrast limit""" + return self._vmax.value - @col_chunk_index.setter - def col_chunk_index(self, index: int): - self._col_chunk_index = index + @vmax.setter + def vmax(self, value: float): + self._vmax.set_value(self, value) + @property + def interpolation(self) -> str: + """image data interpolation method""" + return self._interpolation.value -class HeatmapGraphic(Graphic, Interaction, _AddSelectorsMixin): - feature_events = {"data", "cmap", "present"} + @interpolation.setter + def interpolation(self, value: str): + self._interpolation.set_value(self, value) - 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, - ): - """ - Create an Image Graphic + @property + def cmap_interpolation(self) -> str: + """cmap interpolation method""" + return self._cmap_interpolation.value - 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]`` + @cmap_interpolation.setter + def cmap_interpolation(self, value: str): + self._cmap_interpolation.set_value(self, value) - vmin: int, optional - minimum value for color scaling, calculated from data if not provided + def reset_vmin_vmax(self): + """ + Reset the vmin, vmax by estimating it from the data - vmax: int, optional - maximum value for color scaling, calculated from data if not provided + Returns + ------- + None - cmap: str, optional, default "plasma" - colormap to use to display the data + """ - filter: str, optional, default "nearest" - interpolation filter, one of "nearest" or "linear" + vmin, vmax = quick_min_max(self._data.value) + self.vmin = vmin + self.vmax = vmax - chunk_size: int, default 8192, max 8192 - chunk size for each tile used to make up the heatmap texture + def add_linear_selector( + self, selection: int = None, axis: str = "x", padding: float = None, **kwargs + ) -> LinearSelector: + """ + Adds a :class:`.LinearSelector`. - 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. + Parameters + ---------- + selection: int, optional + initial position of the selector - args: - additional arguments passed to Graphic + padding: float, optional + pad the length of the selector kwargs: - additional keyword arguments passed to Graphic + passed to :class:`.LinearSelector` - Features - -------- + Returns + ------- + LinearSelector + + """ - **data**: :class:`.HeatmapDataFeature` - Manages the data buffer displayed in the HeatmapGraphic + if axis == "x": + size = self._data.value.shape[0] + center = size / 2 + limits = (0, self._data.value.shape[1]) + elif axis == "y": + size = self._data.value.shape[1] + center = size / 2 + limits = (0, self._data.value.shape[0]) + else: + raise ValueError("`axis` must be one of 'x' | 'y'") - **cmap**: :class:`.HeatmapCmapFeature` - Manages the colormap + # default padding is 25% the height or width of the image + if padding is None: + size *= 1.25 + else: + size += padding - **present**: :class:`.PresentFeature` - Control the presence of the Graphic in the scene + 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}" + ) - super().__init__(*args, **kwargs) + selector = LinearSelector( + selection=selection, + limits=limits, + size=size, + center=center, + axis=axis, + parent=weakref.proxy(self), + **kwargs, + ) - if chunk_size > 8192: - raise ValueError("Maximum chunk size is 8192") + self._plot_area.add_graphic(selector, center=False) - data = to_gpu_supported_dtype(data) + # place selector above this graphic + selector.offset = selector.offset + (0.0, 0.0, self.offset[-1] + 1) - # TODO: we need to organize and do this better - if isolated_buffer: - # initialize a buffer with the same shape as the input data - # we do not directly use the input data array as the buffer - # because if the input array is a read-only type, such as - # numpy memmaps, we would not be able to change the image data - buffer_init = np.zeros(shape=data.shape, dtype=data.dtype) - else: - buffer_init = data + return weakref.proxy(selector) - row_chunks = range(ceil(data.shape[0] / chunk_size)) - col_chunks = range(ceil(data.shape[1] / chunk_size)) + def add_linear_region_selector( + self, + selection: tuple[float, float] = None, + axis: str = "x", + padding: float = 0.0, + fill_color=(0, 0, 0.35, 0.2), + **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``. - chunks = list(product(row_chunks, col_chunks)) - # chunks is the index position of each chunk + Parameters + ---------- + selection: (float, float) + initial (min, max) of the selection - start_ixs = [list(map(lambda c: c * chunk_size, chunk)) for chunk in chunks] - stop_ixs = [list(map(lambda c: c + chunk_size, chunk)) for chunk in start_ixs] + axis: "x" | "y" + axis the selector can move along - world_object = pygfx.Group() - self._set_world_object(world_object) + padding: float, default 100.0 + Extends the linear selector along the perpendicular axis to make it easier to interact with. - if (vmin is None) or (vmax is None): - vmin, vmax = quick_min_max(data) + kwargs + passed to ``LinearRegionSelector`` - self.cmap = HeatmapCmapFeature(self, cmap) - self._material = pygfx.ImageBasicMaterial( - clim=(vmin, vmax), - map=self.cmap(), - map_interpolation=filter, - pick_write=True, - ) + Returns + ------- + LinearRegionSelector + linear selection graphic - for start, stop, chunk in zip(start_ixs, stop_ixs, chunks): - row_start, col_start = start - row_stop, col_stop = stop + """ - # x and y positions of the Tile in world space coordinates - y_pos, x_pos = row_start, col_start + if axis == "x": + size = self._data.value.shape[0] + center = size / 2 + limits = (0, self._data.value.shape[1]) + elif axis == "y": + size = self._data.value.shape[1] + center = size / 2 + limits = (0, self._data.value.shape[0]) + else: + raise ValueError("`axis` must be one of 'x' | 'y'") - 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()) + # default padding is 25% the height or width of the image + if padding is None: + size *= 1.25 + else: + size += padding - img = _ImageTile(geometry, self._material) + if selection is None: + selection = limits[0], int(limits[1] * 0.25) - # row and column chunk index for this Tile - img.row_chunk_index = chunk[0] - img.col_chunk_index = chunk[1] + if padding is None: + size *= 1.25 - img.world.x = x_pos - img.world.y = y_pos + else: + size += padding - self.world_object.add(img) + selector = LinearRegionSelector( + selection=selection, + limits=limits, + size=size, + center=center, + axis=axis, + fill_color=fill_color, + parent=weakref.proxy(self), + **kwargs, + ) - self.data = HeatmapDataFeature(self, buffer_init) - # TODO: we need to organize and do this better - if isolated_buffer: - # if the buffer was initialized with zeros - # set it with the actual data - self.data = data + self._plot_area.add_graphic(selector, center=False) - def set_feature(self, feature: str, new_data: Any, indices: Any): - pass + # place above this graphic + selector.offset = selector.offset + (0.0, 0.0, self.offset[-1] + 1) - def reset_feature(self, feature: str): - pass + return weakref.proxy(selector) diff --git a/fastplotlib/graphics/line.py b/fastplotlib/graphics/line.py index 0371fe59b..d0a8cc336 100644 --- a/fastplotlib/graphics/line.py +++ b/fastplotlib/graphics/line.py @@ -5,26 +5,24 @@ import pygfx -from ..utils import parse_cmap_values -from ._base import Graphic, Interaction, PreviouslyModifiedData -from ._features import PointsDataFeature, ColorFeature, CmapFeature, ThicknessFeature +from ._positions_base import PositionsGraphic from .selectors import LinearRegionSelector, LinearSelector +from ._features import Thickness -class LineGraphic(Graphic, Interaction): - feature_events = {"data", "colors", "cmap", "thickness", "present"} +class LineGraphic(PositionsGraphic): + _features = {"data", "colors", "cmap", "thickness"} def __init__( self, data: Any, thickness: float = 2.0, colors: str | np.ndarray | Iterable = "w", + uniform_color: bool = False, alpha: float = 1.0, cmap: str = None, - cmap_values: np.ndarray | Iterable = None, - z_position: float = None, - collection_index: int = None, - *args, + cmap_transform: np.ndarray | Iterable = None, + isolated_buffer: bool = True, **kwargs, ): """ @@ -42,101 +40,90 @@ def __init__( 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 Iterable of numerical values, optional - if provided, these values are used to map the colors from the cmap + uniform_color: bool, default ``False`` + if True, uses a uniform buffer for the line color, + basically saves GPU VRAM when the entire line has a single color alpha: float, optional, default 1.0 alpha value for the colors - z_position: float, optional - z-axis position for placing the graphic + cmap: str, optional + apply a colormap to the line instead of assigning colors manually, this + overrides any argument passed to "colors" - args - passed to Graphic + cmap_transform: 1D array-like of numerical values, optional + if provided, these values are used to map the colors from the cmap - kwargs + **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`` - """ - self.data = PointsDataFeature(self, data, collection_index=collection_index) - - if cmap is not None: - n_datapoints = self.data().shape[0] - - colors = parse_cmap_values( - n_colors=n_datapoints, cmap_name=cmap, cmap_values=cmap_values - ) - - self.colors = ColorFeature( - self, - colors, - n_colors=self.data().shape[0], + super().__init__( + data=data, + colors=colors, + uniform_color=uniform_color, alpha=alpha, - collection_index=collection_index, - ) - - self.cmap = CmapFeature( - self, self.colors(), cmap_name=cmap, cmap_values=cmap_values + cmap=cmap, + cmap_transform=cmap_transform, + isolated_buffer=isolated_buffer, + **kwargs, ) - super().__init__(*args, **kwargs) + self._thickness = Thickness(thickness) if thickness < 1.1: - material = pygfx.LineThinMaterial + MaterialCls = pygfx.LineThinMaterial else: - material = pygfx.LineMaterial - - self.thickness = ThicknessFeature(self, thickness) + MaterialCls = pygfx.LineMaterial + + if uniform_color: + geometry = pygfx.Geometry(positions=self._data.buffer) + material = MaterialCls( + thickness=self.thickness, + color_mode="uniform", + color=self.colors, + pick_write=True, + ) + else: + material = MaterialCls( + thickness=self.thickness, color_mode="vertex", pick_write=True + ) + geometry = pygfx.Geometry( + positions=self._data.buffer, colors=self._colors.buffer + ) - 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(), color_mode="vertex", pick_write=True - ), - ) + world_object: pygfx.Line = pygfx.Line(geometry=geometry, material=material) self._set_world_object(world_object) - if z_position is not None: - self.position_z = z_position + @property + def thickness(self) -> float: + """line thickness""" + return self._thickness.value + + @thickness.setter + def thickness(self, value: float): + self._thickness.set_value(self, value) def add_linear_selector( - self, selection: int = None, padding: float = 50, **kwargs + self, selection: float = None, padding: float = 0.0, axis: str = "x", **kwargs ) -> LinearSelector: """ Adds a linear selector. Parameters ---------- - selection: int - initial position of the selector + Parameters + ---------- + selection: float, optional + selected point on the linear selector, computed from data if not provided - padding: float - pad the length of the selector + axis: str, default "x" + axis that the selector resides on + + padding: float, default 0.0 + Extra padding to extend the linear selector along the orthogonal axis to make it easier to interact with. kwargs passed to :class:`.LinearSelector` @@ -147,38 +134,36 @@ def add_linear_selector( """ - ( - bounds_init, - limits, - size, - origin, - axis, - end_points, - ) = self._get_linear_selector_init_args(padding, **kwargs) + bounds_init, limits, size, center = self._get_linear_selector_init_args( + axis, padding + ) 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}" - ) + selection = bounds_init[0] selector = LinearSelector( selection=selection, limits=limits, - end_points=end_points, - parent=self, + size=size, + center=center, + axis=axis, + parent=weakref.proxy(self), **kwargs, ) self._plot_area.add_graphic(selector, center=False) - selector.position_z = self.position_z + 1 + + # place selector above this graphic + selector.offset = selector.offset + (0.0, 0.0, self.offset[-1] + 1) return weakref.proxy(selector) def add_linear_region_selector( - self, padding: float = 100.0, **kwargs + self, + selection: tuple[float, float] = None, + padding: float = 0.0, + axis: str = "x", + **kwargs, ) -> LinearRegionSelector: """ Add a :class:`.LinearRegionSelector`. Selectors are just ``Graphic`` objects, so you can manage, @@ -186,8 +171,14 @@ def add_linear_region_selector( Parameters ---------- - padding: float, default 100.0 - Extends the linear selector along the y-axis to make it easier to interact with. + selection: (float, float), optional + the starting bounds of the linear region selector, computed from data if not provided + + axis: str, default "x" + axis that the selector resides on + + padding: float, default 0.0 + Extra padding to extend the linear region selector along the orthogonal axis to make it easier to interact with. kwargs passed to ``LinearRegionSelector`` @@ -199,118 +190,61 @@ def add_linear_region_selector( """ - ( - bounds_init, - limits, - size, - origin, - axis, - end_points, - ) = self._get_linear_selector_init_args(padding, **kwargs) + bounds_init, limits, size, center = self._get_linear_selector_init_args( + axis, padding + ) + + if selection is None: + selection = bounds_init # create selector selector = LinearRegionSelector( - bounds=bounds_init, + selection=selection, limits=limits, size=size, - origin=origin, - parent=self, + center=center, + axis=axis, + parent=weakref.proxy(self), **kwargs, ) self._plot_area.add_graphic(selector, center=False) - # so that it is below this graphic - selector.position_z = self.position_z - 1 + + # place selector below this graphic + selector.offset = selector.offset + (0.0, 0.0, self.offset[-1] - 1) # PlotArea manages this for garbage collection etc. just like all other Graphics # so we should only work with a proxy on the user-end return weakref.proxy(selector) # TODO: this method is a bit of a mess, can refactor later - def _get_linear_selector_init_args(self, padding: float, **kwargs): - # computes initial bounds, limits, size and origin of linear selectors - data = self.data() + def _get_linear_selector_init_args( + self, axis: str, padding + ) -> tuple[tuple[float, float], tuple[float, float], float, float]: + # computes args to create selectors + n_datapoints = self.data.value.shape[0] + value_25p = int(n_datapoints / 4) - if "axis" in kwargs.keys(): - axis = kwargs["axis"] - else: - axis = "x" + # remove any nans + data = self.data.value[~np.any(np.isnan(self.data.value), axis=1)] if axis == "x": - offset = self.position_x - # x limits - limits = (data[0, 0] + offset, data[-1, 0] + offset) + # xvals + axis_vals = data[:, 0] - # height + padding - size = np.ptp(data[:, 1]) + padding + # yvals to get size and center + magn_vals = data[:, 1] + elif axis == "y": + axis_vals = data[:, 1] + magn_vals = data[:, 0] - # initial position of the selector - position_y = (data[:, 1].min() + data[:, 1].max()) / 2 + bounds_init = axis_vals[0], axis_vals[value_25p] + limits = axis_vals[0], axis_vals[-1] - # need y offset too for this - origin = (limits[0] - offset, position_y + self.position_y) + # width or height of selector + size = int(np.ptp(magn_vals) * 1.5 + padding) - # 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, - ) - else: - offset = self.position_y - # y limits - limits = (data[0, 1] + offset, data[-1, 1] + offset) - - # width + padding - size = np.ptp(data[:, 0]) + padding - - # initial position of the selector - position_x = (data[:, 0].min() + data[:, 0].max()) / 2 - - # 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, - ) - - # initial bounds are 20% of the limits range - bounds_init = (limits[0], int(np.ptp(limits) * 0.2) + offset) - - return bounds_init, limits, size, origin, axis, end_points + # center of selector along the other axis + center = np.nanmean(magn_vals) - def _fpl_add_plot_area_hook(self, plot_area): - self._plot_area = plot_area - - def set_feature(self, feature: str, new_data: Any, indices: Any = None): - if not hasattr(self, "_previous_data"): - self._previous_data = dict() - elif hasattr(self, "_previous_data"): - self.reset_feature(feature) - - feature_instance = getattr(self, feature) - if indices is not None: - previous = feature_instance[indices].copy() - feature_instance[indices] = new_data - else: - previous = feature_instance._data.copy() - feature_instance._set(new_data) - if feature in self._previous_data.keys(): - self._previous_data[feature].data = previous - self._previous_data[feature].indices = indices - else: - self._previous_data[feature] = PreviouslyModifiedData( - data=previous, indices=indices - ) - - def reset_feature(self, feature: str): - if feature not in self._previous_data.keys(): - return - - prev_ixs = self._previous_data[feature].indices - feature_instance = getattr(self, feature) - if prev_ixs is not None: - feature_instance[prev_ixs] = self._previous_data[feature].data - else: - feature_instance._set(self._previous_data[feature].data) + return bounds_init, limits, size, center diff --git a/fastplotlib/graphics/line_collection.py b/fastplotlib/graphics/line_collection.py index da74cc54e..92aad56b2 100644 --- a/fastplotlib/graphics/line_collection.py +++ b/fastplotlib/graphics/line_collection.py @@ -1,5 +1,4 @@ from typing import * -from copy import deepcopy import weakref import numpy as np @@ -7,27 +6,137 @@ import pygfx from ..utils import parse_cmap_values -from ._base import Interaction, PreviouslyModifiedData, GraphicCollection -from ._features import GraphicFeature +from ._collection_base import CollectionIndexer, GraphicCollection, CollectionFeature from .line import LineGraphic from .selectors import LinearRegionSelector, LinearSelector -class LineCollection(GraphicCollection, Interaction): - child_type = LineGraphic.__name__ +class _LineCollectionProperties: + """Mix-in class for LineCollection properties""" + + @property + def colors(self) -> CollectionFeature: + """get or set colors of lines in the collection""" + return CollectionFeature(self.graphics, "colors") + + @colors.setter + def colors(self, values: str | np.ndarray | tuple[float] | list[float] | list[str]): + if isinstance(values, str): + # set colors of all lines to one str color + for g in self: + g.colors = values + return + + elif all(isinstance(v, str) for v in values): + # individual str colors for each line + if not len(values) == len(self): + raise IndexError + + for g, v in zip(self.graphics, values): + g.colors = v + + return + + if isinstance(values, np.ndarray): + if values.ndim == 2: + # assume individual colors for each + for g, v in zip(self, values): + g.colors = v + return + + elif len(values) == 4: + # assume RGBA + self.colors[:] = values + + else: + # assume individual colors for each + for g, v in zip(self, values): + g.colors = v + + @property + def data(self) -> CollectionFeature: + """get or set data of lines in the collection""" + return CollectionFeature(self.graphics, "data") + + @data.setter + def data(self, values): + for g, v in zip(self, values): + g.data = v + + @property + def cmap(self) -> CollectionFeature: + """ + Get or set a cmap along the line collection. + + Optionally set using a tuple ("cmap", , ) to set the transform and/or alpha. + Example: + + line_collection.cmap = ("jet", sine_transform_vals, 0.7) + + """ + return CollectionFeature(self.graphics, "cmap") + + @cmap.setter + def cmap(self, args): + if isinstance(args, str): + name = args + transform, alpha = None, 1.0 + if len(args) == 1: + name = args[0] + transform, alpha = None, None + + elif len(args) == 2: + name, transform = args + alpha = None + + elif len(args) == 3: + name, transform, alpha = args + + colors = parse_cmap_values( + n_colors=len(self), cmap_name=name, transform=transform + ) + colors[:, -1] = alpha + self.colors = colors + + @property + def thickness(self) -> np.ndarray: + """get or set the thickness of the lines""" + return np.asarray([g.thickness for g in self]) + + @thickness.setter + def thickness(self, values: np.ndarray | list[float]): + if not len(values) == len(self): + raise IndexError + + for g, v in zip(self, values): + g.thickness = v + + +class LineCollectionIndexer(CollectionIndexer, _LineCollectionProperties): + """Indexer for line collections""" + + pass + + +class LineCollection(GraphicCollection, _LineCollectionProperties): + _child_type = LineGraphic + _indexer = LineCollectionIndexer def __init__( self, - data: List[np.ndarray], - z_offset: Iterable[float | int] | float | int = None, - thickness: float | Iterable[float] = 2.0, - colors: str | Iterable[str] | np.ndarray | Iterable[np.ndarray] = "w", + data: np.ndarray | List[np.ndarray], + thickness: float | Sequence[float] = 2.0, + colors: str | Sequence[str] | np.ndarray | Sequence[np.ndarray] = "w", + uniform_colors: bool = False, alpha: float = 1.0, - cmap: Iterable[str] | str = None, - cmap_values: np.ndarray | List = None, + cmap: Sequence[str] | str = None, + cmap_transform: np.ndarray | List = None, name: str = None, - metadata: Iterable[Any] | np.ndarray = None, - *args, + names: list[str] = None, + metadata: Any = None, + metadatas: Sequence[Any] | np.ndarray = None, + isolated_buffer: bool = True, + kwargs_lines: list[dict] = None, **kwargs, ): """ @@ -35,13 +144,11 @@ def __init__( 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] + data: list of array-like + List or array-like of multiple line data to plot - z_offset: Iterable of float or float, optional - | if ``float`` | ``int``, single offset will be used for all lines - | if ``list`` of ``float`` | ``int``, each value will apply to the individual lines + | if ``list`` each item in the list must be a 1D, 2D, or 3D numpy array + | if array-like, must be of shape [n_lines, n_points_line, y | xy | xyz] thickness: float or Iterable of float, default 2.0 | if ``float``, single thickness will be used for all lines @@ -63,52 +170,58 @@ def __init__( .. note:: ``cmap`` overrides any arguments passed to ``colors`` - cmap_values: 1D array-like or Iterable of numerical values, optional + cmap_transform: 1D array-like 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 + name of the line collection as a whole - metadata: Iterable or array - metadata associated with this collection, this is for the user to manage. - ``len(metadata)`` must be same as ``len(data)`` + names: list[str], optional + names of the individual lines in the collection, ``len(names)`` must equal ``len(data)`` - args - passed to GraphicCollection + metadata: Any + meatadata associated with the collection as a whole - kwargs - passed to GraphicCollection - - Features - -------- + metadatas: Iterable or array + metadata for each individual line associated with this collection, this is for the user to manage. + ``len(metadata)`` must be same as ``len(data)`` - Collections support the same features as the underlying graphic. You just have to slice the selection. + kwargs_lines: list[dict], optional + list of kwargs passed to the individual lines, ``len(kwargs_lines)`` must equal ``len(data)`` - See :class:`LineGraphic` details on the features. + kwargs_collection + kwargs for the collection, passed to GraphicCollection """ - super().__init__(name) + super().__init__(name=name, metadata=metadata, **kwargs) - if not isinstance(z_offset, (float, int)) and z_offset is not None: - if len(data) != len(z_offset): + if not isinstance(thickness, (float, int)): + if len(thickness) != len(data): raise ValueError( - "z_position must be a single float or an iterable with same length as data" + f"len(thickness) != len(data)\n" f"{len(thickness)} != {len(data)}" ) - if not isinstance(thickness, (float, int)): - if len(thickness) != len(data): + if names is not None: + if len(names) != len(data): raise ValueError( - "args must be a single float or an iterable with same length as data" + f"len(names) != len(data)\n" f"{len(names)} != {len(data)}" ) - if metadata is not None: - if len(metadata) != len(data): + if metadatas is not None: + if len(metadatas) != len(data): raise ValueError( - f"len(metadata) != len(data)\n" f"{len(metadata)} != {len(data)}" + f"len(metadata) != len(data)\n" f"{len(metadatas)} != {len(data)}" ) - self._cmap_values = cmap_values + if kwargs_lines is not None: + if len(kwargs_lines) != len(data): + raise ValueError( + f"len(kwargs_lines) != len(data)\n" + f"{len(kwargs_lines)} != {len(data)}" + ) + + self._cmap_transform = cmap_transform self._cmap_str = cmap # cmap takes priority over colors @@ -116,7 +229,7 @@ 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, transform=cmap_transform ) single_color = False cmap = None @@ -175,14 +288,12 @@ def __init__( "or must be a tuple/list of colors represented by a string with the same length as the data" ) + if kwargs_lines is None: + kwargs_lines = dict() + self._set_world_object(pygfx.Group()) for i, d in enumerate(data): - if isinstance(z_offset, list): - _z = z_offset[i] - else: - _z = z_offset - if isinstance(thickness, list): _s = thickness[i] else: @@ -199,66 +310,51 @@ def __init__( _cmap = cmap[i] _c = None - if metadata is not None: - _m = metadata[i] + if metadatas is not None: + _m = metadatas[i] else: _m = None + if names is not None: + _name = names[i] + else: + _name = None + lg = LineGraphic( data=d, thickness=_s, colors=_c, - z_position=_z, + uniform_color=uniform_colors, cmap=_cmap, - collection_index=i, + name=_name, metadata=_m, + isolated_buffer=isolated_buffer, + **kwargs_lines, ) - self.add_graphic(lg, reset_index=False) - - @property - def cmap(self) -> str: - return self._cmap_str - - @cmap.setter - def cmap(self, cmap: str): - colors = parse_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.add_graphic(lg) - self._cmap_str = cmap - - @property - def cmap_values(self) -> np.ndarray: - return self._cmap_values - - @cmap_values.setter - def cmap_values(self, values: np.ndarray | Iterable): - colors = parse_cmap_values( - n_colors=len(self), cmap_name=self.cmap, cmap_values=values - ) - - for i, g in enumerate(self.graphics): - g.colors = colors[i] - - self._cmap_values = values + def __getitem__(self, item) -> LineCollectionIndexer: + return super().__getitem__(item) def add_linear_selector( - self, selection: int = None, padding: float = 50, **kwargs + self, selection: float = None, padding: float = 0.0, axis: str = "x", **kwargs ) -> LinearSelector: """ - Adds a :class:`.LinearSelector` . + Adds a linear selector. Parameters ---------- - selection: int - initial position of the selector + Parameters + ---------- + selection: float, optional + selected point on the linear selector, computed from data if not provided + + axis: str, default "x" + axis that the selector resides on - padding: float - pad the length of the selector + padding: float, default 0.0 + Extra padding to extend the linear selector along the orthogonal axis to make it easier to interact with. kwargs passed to :class:`.LinearSelector` @@ -269,46 +365,51 @@ def add_linear_selector( """ - ( - bounds, - limits, - size, - origin, - axis, - end_points, - ) = self._get_linear_selector_init_args(padding, **kwargs) + bounds_init, limits, size, center = self._get_linear_selector_init_args( + axis, padding + ) 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}" - ) + selection = bounds_init[0] selector = LinearSelector( selection=selection, limits=limits, - end_points=end_points, - parent=self, + size=size, + center=center, + axis=axis, + parent=weakref.proxy(self), **kwargs, ) self._plot_area.add_graphic(selector, center=False) - selector.position_z = self.position_z + 1 + + # place selector above this graphic + selector.offset = selector.offset + (0.0, 0.0, self.offset[-1] + 1) return weakref.proxy(selector) def add_linear_region_selector( - self, padding: float = 100.0, **kwargs + self, + selection: tuple[float, float] = None, + padding: float = 0.0, + axis: str = "x", + **kwargs, ) -> LinearRegionSelector: """ - Add a :class:`.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 ---------- - padding: float, default 100.0 - Extends the linear selector along the y-axis to make it easier to interact with. + selection: (float, float), optional + the starting bounds of the linear region selector, computed from data if not provided + + axis: str, default "x" + axis that the selector resides on + + padding: float, default 0.0 + Extra padding to extend the linear region selector along the orthogonal axis to make it easier to interact with. kwargs passed to ``LinearRegionSelector`` @@ -320,155 +421,62 @@ def add_linear_region_selector( """ - ( - bounds, - limits, - size, - origin, - axis, - end_points, - ) = self._get_linear_selector_init_args(padding, **kwargs) + bounds_init, limits, size, center = self._get_linear_selector_init_args( + axis, padding + ) + + if selection is None: + selection = bounds_init + # create selector selector = LinearRegionSelector( - bounds=bounds, + selection=selection, limits=limits, size=size, - origin=origin, - parent=self, + center=center, + axis=axis, + parent=weakref.proxy(self), **kwargs, ) self._plot_area.add_graphic(selector, center=False) - selector.position_z = self.position_z - 1 - - return weakref.proxy(selector) - def _get_linear_selector_init_args(self, padding, **kwargs): - bounds_init = list() - limits = list() - sizes = list() - origin = list() - 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.append(_bounds_init) - limits.append(_limits) - sizes.append(_size) - origin.append(_origin) - end_points.append(_end_points) - - # set the init bounds using the extents of the collection - b = np.vstack(bounds_init) - bounds = (b[:, 0].min(), b[:, 1].max()) - - # set the limits using the extents of the collection - 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, - ] - - # TODO: refactor this to use `LineStack.graphics[-1].position.y` - if isinstance(self, LineStack): - stack_offset = self.separation * len(sizes) - # sum them if it's a stack - size = sum(sizes) - # add the separations - size += stack_offset - - # 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 - ) + # place selector below this graphic + selector.offset = selector.offset + (0.0, 0.0, self.offset[-1] - 1) - else: - # just the biggest one if not stacked - size = max(sizes) + # PlotArea manages this for garbage collection etc. just like all other Graphics + # so we should only work with a proxy on the user-end + return weakref.proxy(selector) - size += padding + def _get_linear_selector_init_args(self, axis, padding): + # use bbox to get size and center + bbox = self.world_object.get_world_bounding_box() if axis == "x": - o = np.vstack(origin) - origin_y = (o[:, 1].min() + o[:, 1].max()) / 2 - origin = (limits[0], origin_y) - else: - o = np.vstack(origin) - origin_x = (o[:, 0].min() + o[:, 0].max()) / 2 - origin = (origin_x, limits[0]) - - return bounds, limits, size, origin, axis, end_points - - def _fpl_add_plot_area_hook(self, plot_area): - self._plot_area = plot_area - - def set_feature(self, feature: str, new_data: Any, indices: Any): - # if single value force to be an array of size 1 - if isinstance(indices, (np.integer, int)): - indices = np.array([indices]) - if not hasattr(self, "_previous_data"): - self._previous_data = dict() - elif hasattr(self, "_previous_data"): - if feature in self._previous_data.keys(): - # for now assume same index won't be changed with diff data - # I can't think of a usecase where we'd have to check the data too - # so unless there is a bug we keep it like this - if self._previous_data[feature].indices == indices: - return # nothing to change, and this allows bidirectional linking without infinite recursion - - self.reset_feature(feature) + xdata = np.array(self.data[:, 0]) + xmin, xmax = (np.nanmin(xdata), np.nanmax(xdata)) + value_25p = (xmax - xmin) / 4 - # coll_feature = getattr(self[indices], feature) + bounds = (xmin, value_25p) + limits = (xmin, xmax) + # size from orthogonal axis + size = bbox[:, 1].ptp() * 1.5 + # center on orthogonal axis + center = bbox[:, 1].mean() - data = list() + elif axis == "y": + ydata = np.array(self.data[:, 1]) + xmin, xmax = (np.nanmin(ydata), np.nanmax(ydata)) + value_25p = (xmax - xmin) / 4 - for graphic in self.graphics[indices]: - feature_instance: GraphicFeature = getattr(graphic, feature) - data.append(feature_instance()) - - # later we can think about multi-index events - previous_data = deepcopy(data[0]) - - if feature in self._previous_data.keys(): - self._previous_data[feature].data = previous_data - self._previous_data[feature].indices = indices - else: - 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 - # since calling `feature._set()` triggers all the feature callbacks - feature_instance._set(new_data) - - def reset_feature(self, feature: str): - if feature not in self._previous_data.keys(): - return + bounds = (xmin, value_25p) + limits = (xmin, xmax) - # implemented for a single index at moment - prev_ixs = self._previous_data[feature].indices - coll_feature = getattr(self[prev_ixs], feature) + size = bbox[:, 0].ptp() * 1.5 + # center on orthogonal axis + center = bbox[:, 0].mean() - coll_feature.block_events(True) - coll_feature._set(self._previous_data[feature].data) - coll_feature.block_events(False) + return bounds, limits, size, center axes = {"x": 0, "y": 1, "z": 2} @@ -478,17 +486,19 @@ class LineStack(LineCollection): def __init__( self, data: List[np.ndarray], - z_offset: Iterable[float] | float = None, thickness: float | Iterable[float] = 2.0, colors: str | Iterable[str] | np.ndarray | Iterable[np.ndarray] = "w", alpha: float = 1.0, cmap: Iterable[str] | str = None, - cmap_values: np.ndarray | List = None, + cmap_transform: np.ndarray | List = None, name: str = None, - metadata: Iterable[Any] | np.ndarray = None, + names: list[str] = None, + metadata: Any = None, + metadatas: Sequence[Any] | np.ndarray = None, + isolated_buffer: bool = True, separation: float = 10.0, separation_axis: str = "y", - *args, + kwargs_lines: list[dict] = None, **kwargs, ): """ @@ -496,13 +506,11 @@ def __init__( 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] + data: list of array-like + List or array-like of multiple line data to plot - z_offset: Iterable of float or float, optional - | if ``float``, single offset will be used for all lines - | if ``list`` of ``float``, each value will apply to the individual lines + | if ``list`` each item in the list must be a 1D, 2D, or 3D numpy array + | if array-like, must be of shape [n_lines, n_points_line, y | xy | xyz] thickness: float or Iterable of float, default 2.0 | if ``float``, single thickness will be used for all lines @@ -514,6 +522,9 @@ def __init__( | 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 + alpha: float, optional + alpha value for colors, if colors is a ``str`` + cmap: Iterable 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 @@ -521,11 +532,20 @@ def __init__( .. note:: ``cmap`` overrides any arguments passed to ``colors`` - cmap_values: 1D array-like or Iterable of numerical values, optional + cmap_transform: 1D array-like of numerical values, optional if provided, these values are used to map the colors from the cmap - metadata: Iterable or array - metadata associated with this collection, this is for the user to manage. + name: str, optional + name of the line collection as a whole + + names: list[str], optional + names of the individual lines in the collection, ``len(names)`` must equal ``len(data)`` + + metadata: Any + metadata associated with the collection as a whole + + metadatas: Iterable or array + metadata for each individual line associated with this collection, this is for the user to manage. ``len(metadata)`` must be same as ``len(data)`` separation: float, default 10 @@ -534,43 +554,40 @@ def __init__( 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 - - kwargs - passed to LineCollection - - Features - -------- - Collections support the same features as the underlying graphic. You just have to slice the selection. + kwargs_lines: list[dict], optional + list of kwargs passed to the individual lines, ``len(kwargs_lines)`` must equal ``len(data)`` - See :class:`LineGraphic` details on the features. + kwargs_collection + kwargs for the collection, passed to GraphicCollection """ super().__init__( data=data, - z_offset=z_offset, thickness=thickness, colors=colors, alpha=alpha, cmap=cmap, - cmap_values=cmap_values, - metadata=metadata, + cmap_transform=cmap_transform, name=name, - *args, + names=names, + metadata=metadata, + metadatas=metadatas, + isolated_buffer=isolated_buffer, + kwargs_lines=kwargs_lines, **kwargs, ) axis_zero = 0 for i, line in enumerate(self.graphics): if separation_axis == "x": - line.position_x = axis_zero + line.offset = (axis_zero, *line.offset[1:]) + elif separation_axis == "y": - line.position_y = axis_zero + line.offset = (line.offset[0], axis_zero, line.offset[2]) axis_zero = ( - axis_zero + line.data()[:, axes[separation_axis]].max() + separation + axis_zero + line.data.value[:, axes[separation_axis]].max() + separation ) self.separation = separation diff --git a/fastplotlib/graphics/scatter.py b/fastplotlib/graphics/scatter.py index 8682df3d5..39d815c95 100644 --- a/fastplotlib/graphics/scatter.py +++ b/fastplotlib/graphics/scatter.py @@ -3,24 +3,24 @@ import numpy as np import pygfx -from ..utils import parse_cmap_values -from ._base import Graphic -from ._features import PointsDataFeature, ColorFeature, CmapFeature, PointsSizesFeature +from ._positions_base import PositionsGraphic +from ._features import PointsSizesFeature, UniformSize -class ScatterGraphic(Graphic): - feature_events = {"data", "sizes", "colors", "cmap", "present"} +class ScatterGraphic(PositionsGraphic): + _features = {"data", "sizes", "colors", "cmap"} def __init__( self, - data: np.ndarray, - sizes: float | np.ndarray | Iterable[float] = 1, - colors: str | np.ndarray | Iterable[str] = "w", + data: Any, + colors: str | np.ndarray | tuple[float] | list[float] | list[str] = "w", + uniform_color: bool = False, alpha: float = 1.0, cmap: str = None, - cmap_values: np.ndarray | List = None, - z_position: float = 0.0, - *args, + cmap_transform: np.ndarray = None, + isolated_buffer: bool = True, + sizes: float | np.ndarray | Iterable[float] = 1, + uniform_size: bool = False, **kwargs, ): """ @@ -31,73 +31,92 @@ def __init__( 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 + uniform_color: bool, default False + if True, uses a uniform buffer for the scatter point colors, + basically saves GPU VRAM when the entire line has a single color + + alpha: float, optional, default 1.0 + alpha value for the colors + cmap: str, optional apply a colormap to the scatter instead of assigning colors manually, this overrides any argument passed to "colors" - cmap_values: 1D array-like or list of numerical values, optional + cmap_transform: 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 + isolated_buffer: bool, default True + whether the buffers should be isolated from the user input array. + Generally always ``True``, ``False`` is for rare advanced use. - z_position: float, optional - z-axis position for placing the graphic + sizes: float or iterable of float, optional, default 1.0 + size of the scatter points - args - passed to Graphic + uniform_size: bool, default False + if True, uses a uniform buffer for the scatter point sizes, + basically saves GPU VRAM when all scatter points are the same size 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`` + super().__init__( + data=data, + colors=colors, + uniform_color=uniform_color, + alpha=alpha, + cmap=cmap, + cmap_transform=cmap_transform, + isolated_buffer=isolated_buffer, + **kwargs, + ) - """ - self.data = PointsDataFeature(self, data) - n_datapoints = self.data().shape[0] + n_datapoints = self.data.value.shape[0] - if cmap is not None: - colors = parse_cmap_values( - n_colors=n_datapoints, cmap_name=cmap, cmap_values=cmap_values - ) + geo_kwargs = {"positions": self._data.buffer} + material_kwargs = {"pick_write": True} - self.colors = ColorFeature(self, colors, n_colors=n_datapoints, alpha=alpha) - self.cmap = CmapFeature( - self, self.colors(), cmap_name=cmap, cmap_values=cmap_values - ) + if uniform_color: + material_kwargs["color_mode"] = "uniform" + material_kwargs["color"] = self.colors + else: + material_kwargs["color_mode"] = "vertex" + geo_kwargs["colors"] = self.colors.buffer - self.sizes = PointsSizesFeature(self, sizes) - super().__init__(*args, **kwargs) + if uniform_size: + material_kwargs["size_mode"] = "uniform" + self._sizes = UniformSize(sizes) + material_kwargs["size"] = self.sizes + else: + material_kwargs["size_mode"] = "vertex" + self._sizes = PointsSizesFeature(sizes, n_datapoints=n_datapoints) + geo_kwargs["sizes"] = self.sizes.buffer world_object = pygfx.Points( - pygfx.Geometry( - positions=self.data(), sizes=self.sizes(), colors=self.colors() - ), - material=pygfx.PointsMaterial( - color_mode="vertex", size_mode="vertex", pick_write=True - ), + pygfx.Geometry(**geo_kwargs), + material=pygfx.PointsMaterial(**material_kwargs), ) self._set_world_object(world_object) - self.position_z = z_position + @property + def sizes(self) -> PointsSizesFeature | float: + """Get or set the scatter point size(s)""" + if isinstance(self._sizes, PointsSizesFeature): + return self._sizes + + elif isinstance(self._sizes, UniformSize): + return self._sizes.value + + @sizes.setter + def sizes(self, value): + if isinstance(self._sizes, PointsSizesFeature): + self._sizes[:] = value + + elif isinstance(self._sizes, UniformSize): + self._sizes.set_value(self, value) diff --git a/fastplotlib/graphics/selectors/__init__.py b/fastplotlib/graphics/selectors/__init__.py index 1fb0c453e..4f28f571c 100644 --- a/fastplotlib/graphics/selectors/__init__.py +++ b/fastplotlib/graphics/selectors/__init__.py @@ -2,11 +2,5 @@ from ._linear_region import LinearRegionSelector from ._polygon import PolygonSelector -from ._sync import Synchronizer -__all__ = [ - "LinearSelector", - "LinearRegionSelector", - "PolygonSelector", - "Synchronizer", -] +__all__ = ["LinearSelector", "LinearRegionSelector"] diff --git a/fastplotlib/graphics/selectors/_base_selector.py b/fastplotlib/graphics/selectors/_base_selector.py index f20eba4a0..0fc48058d 100644 --- a/fastplotlib/graphics/selectors/_base_selector.py +++ b/fastplotlib/graphics/selectors/_base_selector.py @@ -35,7 +35,11 @@ class MoveInfo: # Selector base class class BaseSelector(Graphic): - feature_events = ("selection",) + _features = {"selection"} + + @property + def axis(self) -> str: + return self._axis def __init__( self, @@ -45,7 +49,8 @@ def __init__( hover_responsive: Tuple[WorldObject, ...] = None, arrow_keys_modifier: str = None, axis: str = None, - name: str = None, + parent: Graphic = None, + **kwargs, ): if edges is None: edges = tuple() @@ -71,7 +76,7 @@ def __init__( for wo in self._hover_responsive: self._original_colors[wo] = wo.material.color - self.axis = axis + self._axis = axis # current delta in world coordinates self.delta: np.ndarray = None @@ -95,7 +100,9 @@ def __init__( self._pygfx_event = None - Graphic.__init__(self, name=name) + self._parent = parent + + Graphic.__init__(self, **kwargs) def get_selected_index(self): """Not implemented for this selector""" @@ -110,7 +117,7 @@ def get_selected_data(self): raise NotImplementedError def _get_source(self, graphic): - if self.parent is None and graphic is None: + if self._parent is None and graphic is None: raise AttributeError( "No Graphic to apply selector. " "You must either set a ``parent`` Graphic on the selector, or pass a graphic." @@ -120,7 +127,7 @@ def _get_source(self, graphic): if graphic is not None: source = graphic else: - source = self.parent + source = self._parent return source @@ -262,7 +269,7 @@ def _move_to_pointer(self, ev): """ Calculates delta just using current world object position and calls self._move_graphic(). """ - current_position: np.ndarray = self.position + current_position: np.ndarray = self.offset # middle mouse button clicks if ev.button != 3: @@ -348,8 +355,6 @@ def _key_down(self, ev): if ev.key not in key_bind_direction.keys(): return - # print(ev.key) - self._key_move_value = ev.key def _key_up(self, ev): diff --git a/fastplotlib/graphics/selectors/_linear.py b/fastplotlib/graphics/selectors/_linear.py index 82e553f0a..22ba96a28 100644 --- a/fastplotlib/graphics/selectors/_linear.py +++ b/fastplotlib/graphics/selectors/_linear.py @@ -6,7 +6,8 @@ import pygfx from ...utils.gui import IS_JUPYTER -from .._base import Graphic, GraphicCollection +from .._base import Graphic +from .._collection_base import GraphicCollection from .._features._selection_features import LinearSelectionFeature from ._base_selector import BaseSelector @@ -17,6 +18,26 @@ class LinearSelector(BaseSelector): + @property + def parent(self) -> Graphic: + return self._parent + + @property + def selection(self) -> float: + """ + x or y value of selector's current position + """ + return self._selection.value + + @selection.setter + def selection(self, value: int): + graphic = self._parent + + if isinstance(graphic, GraphicCollection): + pass + + self._selection.set_value(self, value) + @property def limits(self) -> Tuple[float, float]: return self._limits @@ -35,14 +56,15 @@ def limits(self, values: Tuple[float, float]): # TODO: make `selection` arg in graphics data space not world space def __init__( self, - selection: int, - limits: Tuple[int, int], + selection: float, + limits: Sequence[float], + size: float, + center: float, axis: str = "x", parent: Graphic = None, - end_points: Tuple[int, int] = None, - arrow_keys_modifier: str = "Shift", + color: str | tuple = "w", thickness: float = 2.5, - color: Any = "w", + arrow_keys_modifier: str = "Shift", name: str = None, ): """ @@ -59,12 +81,12 @@ def __init__( axis: str, default "x" "x" | "y", the axis which the slider can move along + center: float + center offset of the selector on the orthogonal axis, by default the data mean + parent: Graphic parent graphic for this LineSelector - end_points: (int, int) - set length of slider by bounding it between two x-pos or two y-pos - arrow_keys_modifier: str modifier key that must be pressed to initiate movement using arrow keys, must be one of: "Control", "Shift", "Alt" or ``None``. Double click on the selector first to enable the @@ -79,34 +101,23 @@ def __init__( name: str, optional name of line slider - Features - -------- - - 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: raise ValueError("limits must be a tuple of 2 integers, i.e. (int, int)") - self._limits = tuple(map(round, limits)) + self._limits = np.asarray(limits) - selection = round(selection) + end_points = [-size / 2, size / 2] if axis == "x": - xs = np.zeros(2) + xs = np.array([selection, selection]) ys = np.array(end_points) zs = np.zeros(2) line_data = np.column_stack([xs, ys, zs]) elif axis == "y": xs = np.array(end_points) - ys = np.zeros(2) + ys = np.array([selection, selection]) zs = np.zeros(2) line_data = np.column_stack([xs, ys, zs]) @@ -144,12 +155,15 @@ def __init__( self._move_info: dict = None - self.parent = parent - self._block_ipywidget_call = False self._handled_widgets = list() + if axis == "x": + offset = (parent.offset[0], center, 0) + elif axis == "y": + offset = (center, parent.offset[1], 0) + # init base selector BaseSelector.__init__( self, @@ -157,20 +171,28 @@ def __init__( hover_responsive=(line_inner, self.line_outer), arrow_keys_modifier=arrow_keys_modifier, axis=axis, + parent=parent, name=name, + offset=offset, ) self._set_world_object(world_object) - self.selection = LinearSelectionFeature( - self, axis=axis, value=selection, limits=self._limits + self._selection = LinearSelectionFeature( + axis=axis, value=selection, limits=self._limits ) - self.selection = selection + if self._parent is not None: + self.selection = selection + else: + self._selection.set_value(self, selection) + + # update any ipywidgets + self.add_event_handler(self._update_ipywidgets, "selection") def _setup_ipywidget_slider(self, widget): # setup an ipywidget slider with bidirectional callbacks to this LinearSelector - value = self.selection() + value = self.selection if isinstance(widget, ipywidgets.IntSlider): value = int(value) @@ -180,16 +202,13 @@ def _setup_ipywidget_slider(self, widget): # user changes widget -> linear selection changes widget.observe(self._ipywidget_callback, "value") - # user changes linear selection -> widget changes - self.selection.add_event_handler(self._update_ipywidgets) - self._handled_widgets.append(widget) def _update_ipywidgets(self, ev): # update the ipywidget sliders when LinearSelector value changes self._block_ipywidget_call = True # prevent infinite recursion - value = ev.pick_info["new_data"] + value = ev.info["value"] # update all the handled slider widgets for widget in self._handled_widgets: if isinstance(widget, ipywidgets.IntSlider): @@ -200,7 +219,7 @@ def _update_ipywidgets(self, ev): self._block_ipywidget_call = False def _ipywidget_callback(self, change): - # update the LinearSelector if the ipywidget value changes + # update the LinearSelector when the ipywidget value changes if self._block_ipywidget_call or self._moving: return @@ -249,9 +268,9 @@ def make_ipywidget_slider(self, kind: str = "IntSlider", **kwargs): cls = getattr(ipywidgets, kind) - value = self.selection() + value = self.selection if "Int" in kind: - value = int(self.selection()) + value = int(self.selection) slider = cls( min=self.limits[0], @@ -327,34 +346,32 @@ def get_selected_index(self, graphic: Graphic = None) -> Union[int, List[int]]: def _get_selected_index(self, graphic): # the array to search for the closest value along that axis if self.axis == "x": - geo_positions = graphic.data()[:, 0] - offset = getattr(graphic, f"position_{self.axis}") - else: - geo_positions = graphic.data()[:, 1] - offset = getattr(graphic, f"position_{self.axis}") + data = graphic.data[:, 0] + elif self.axis == "y": + data = graphic.data[:, 1] - if "Line" in graphic.__class__.__name__: - # we want to find the index of the geometry position that is closest to the slider's geometry position - find_value = self.selection() - offset + if ( + "Line" in graphic.__class__.__name__ + or "Scatter" in graphic.__class__.__name__ + ): + # we want to find the index of the data closest to the slider position + find_value = self.selection # get closest data index to the world space position of the slider - idx = np.searchsorted(geo_positions, find_value, side="left") + idx = np.searchsorted(data, 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]) + idx == len(data) + or math.fabs(find_value - data[idx - 1]) + < math.fabs(find_value - data[idx]) ): return round(idx - 1) else: return round(idx) - if ( - "Heatmap" in graphic.__class__.__name__ - or "Image" in graphic.__class__.__name__ - ): + if "Image" in graphic.__class__.__name__: # indices map directly to grid geometry for image data buffer - index = self.selection() - offset + index = self.selection return round(index) def _move_graphic(self, delta: np.ndarray): @@ -369,9 +386,9 @@ def _move_graphic(self, delta: np.ndarray): """ if self.axis == "x": - self.selection = self.selection() + delta[0] + self.selection = self.selection + delta[0] else: - self.selection = self.selection() + delta[1] + self.selection = self.selection + delta[1] def _fpl_cleanup(self): for widget in self._handled_widgets: diff --git a/fastplotlib/graphics/selectors/_linear_region.py b/fastplotlib/graphics/selectors/_linear_region.py index 09c134800..ecc67b885 100644 --- a/fastplotlib/graphics/selectors/_linear_region.py +++ b/fastplotlib/graphics/selectors/_linear_region.py @@ -5,7 +5,8 @@ import pygfx from ...utils.gui import IS_JUPYTER -from .._base import Graphic, GraphicCollection +from .._base import Graphic +from .._collection_base import GraphicCollection from .._features._selection_features import LinearRegionSelectionFeature from ._base_selector import BaseSelector @@ -16,6 +17,35 @@ class LinearRegionSelector(BaseSelector): + @property + def parent(self) -> Graphic | None: + """graphic that the selector is associated with""" + return self._parent + + @property + def selection(self) -> Sequence[float] | List[Sequence[float]]: + """ + (min, max) of data value along selector's axis + """ + # TODO: This probably does not account for rotation since world.position + # does not account for rotation, we can do this later + + return self._selection.value.copy() + + # TODO: if no parent graphic is set, this just returns world positions + # but should we change it? + # return self._selection.value + + @selection.setter + def selection(self, selection: Sequence[float]): + # set (xmin, xmax), or (ymin, ymax) of the selector in data space + graphic = self._parent + + if isinstance(graphic, GraphicCollection): + pass + + self._selection.set_value(self, selection) + @property def limits(self) -> Tuple[float, float]: return self._limits @@ -29,54 +59,50 @@ def limits(self, values: Tuple[float, float]): self._limits = tuple( map(round, values) ) # if values are close to zero things get weird so round them - self.selection._limits = self._limits + self._selection._limits = self._limits def __init__( self, - bounds: Tuple[int, int], - limits: Tuple[int, int], - size: int, - origin: Tuple[int, int], + selection: Sequence[float], + limits: Sequence[float], + size: float, + center: float, axis: str = "x", parent: Graphic = None, resizable: bool = True, fill_color=(0, 0, 0.35), - edge_color=(0.8, 0.8, 0), - edge_thickness: int = 3, + edge_color=(0.8, 0.6, 0), + edge_thickness: float = 8, 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. - Allows sub-selecting data from a ``Graphic`` or from multiple Graphics. - - bounds[0], limits[0], and position[0] must be identical. - - Holding the right mouse button while dragging an edge will force the entire region selector to move. This is - a when using transparent fill areas due to ``pygfx`` picking limitations. + Allows sub-selecting data from a parent ``Graphic`` or from multiple Graphics. - **Note:** Events get very weird if the values of bounds, limits and origin are close to zero. If you need - a linear selector with small data, we recommend scaling the data and then using the selector. + Assumes that the data under the selector is a function of the axis on which the selector moves + along. Example: if the selector is along the x-axis, then there must be only one y-value for each + x-value, otherwise functions such as ``get_selected_data()`` do not make sense. Parameters ---------- - bounds: (int, int) - the initial bounds of the linear selector + selection: (float, float) + initial (min, max) x or y values - limits: (int, int) - (min limit, max limit) for the selector + limits: (float, float) + (min limit, max limit) within which the selector can move size: int height or width of the selector - origin: (int, int) - initial position of the selector + center: float + center offset of the selector on the orthogonal axis, by default the data mean axis: str, default "x" - "x" | "y", axis for the selector + "x" | "y", axis the selected can move on parent: Graphic, default ``None`` - associate this selector with a parent Graphic + associate this selector with a parent Graphic from which to fetch data or indices resizable: bool if ``True``, the edges can be dragged to resize the width of the linear selection @@ -87,6 +113,9 @@ def __init__( edge_color: str, array, or tuple edge color for the selector, passed to pygfx.Color + edge_thickness: float, default 8 + edge thickness + arrow_keys_modifier: str modifier key that must be pressed to initiate movement using arrow keys, must be one of: "Control", "Shift", "Alt" or ``None`` @@ -94,46 +123,22 @@ 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, otherwise things get weird - bounds = tuple(map(round, bounds)) - self._limits = tuple(map(round, limits)) - origin = tuple(map(round, origin)) + if not len(selection) == 2: + raise ValueError + + selection = np.asarray(selection) + + if not len(limits) == 2: + raise ValueError + + self._limits = np.asarray(limits) # TODO: sanity checks, we recommend users to add LinearSelection using the add_linear_selector() methods # TODO: so we can worry about the sanity checks later - # if axis == "x": - # if limits[0] != origin[0] != bounds[0]: - # raise ValueError( - # f"limits[0] != position[0] != bounds[0]\n" - # f"{limits[0]} != {origin[0]} != {bounds[0]}" - # ) - # - # elif axis == "y": - # # initial y-position is position[1] - # if limits[0] != origin[1] != bounds[0]: - # raise ValueError( - # f"limits[0] != position[1] != bounds[0]\n" - # f"{limits[0]} != {origin[1]} != {bounds[0]}" - # ) - - self.parent = parent - - # world object for this will be a group - # basic mesh for the fill area of the selector - # line for each edge of the selector + group = pygfx.Group() if axis == "x": @@ -152,89 +157,69 @@ def __init__( # the fill of the selection self.fill = mesh - self.fill.world.position = (*origin, -2) + # no x, y offsets for linear region selector + # everything is done by setting the mesh data + # and line positions + self.fill.world.position = (0, 0, -2) group.add(self.fill) self._resizable = resizable 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], - ] - ).astype(np.float32) - - left_line = pygfx.Line( - pygfx.Geometry(positions=left_line_data), - pygfx.LineMaterial( - thickness=edge_thickness, color=edge_color, pick_write=True - ), - ) - - # 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], - ] - ).astype(np.float32) - - right_line = pygfx.Line( - pygfx.Geometry(positions=right_line_data), - pygfx.LineMaterial( - thickness=edge_thickness, color=edge_color, pick_write=True - ), + # just some data to initialize the edge lines + init_line_data = np.array([[0, -size / 2, 0], [0, size / 2, 0]]).astype( + np.float32 ) - 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 = pygfx.Line( - pygfx.Geometry(positions=bottom_line_data), - pygfx.LineMaterial( - thickness=edge_thickness, color=edge_color, pick_write=True - ), - ) - - # position data for the right edge line - top_line_data = np.array( + # just some line data to initialize y axis edge lines + init_line_data = np.array( [ - [(-size / 2) + origin[0], bounds[1], 0.5], - [(size / 2) + origin[0], bounds[1], 0.5], + [-size / 2, 0, 0], + [size / 2, 0, 0], ] ).astype(np.float32) - top_line = pygfx.Line( - pygfx.Geometry(positions=top_line_data), - pygfx.LineMaterial( - thickness=edge_thickness, color=edge_color, pick_write=True - ), - ) - - self.edges: Tuple[pygfx.Line, pygfx.Line] = (bottom_line, top_line) - else: raise ValueError("axis argument must be one of 'x' or 'y'") + line0 = pygfx.Line( + pygfx.Geometry( + positions=init_line_data.copy() + ), # copy so the line buffer is isolated + pygfx.LineMaterial( + thickness=edge_thickness, color=edge_color, pick_write=True + ), + ) + line1 = pygfx.Line( + pygfx.Geometry( + positions=init_line_data.copy() + ), # copy so the line buffer is isolated + pygfx.LineMaterial( + thickness=edge_thickness, color=edge_color, pick_write=True + ), + ) + + self.edges: Tuple[pygfx.Line, pygfx.Line] = (line0, line1) + # add the edge lines for edge in self.edges: - edge.world.z = -1 + edge.world.z = -0.5 group.add(edge) + # TODO: if parent offset changes, we should set the selector offset too + # TODO: add check if parent is `None`, will throw error otherwise + if axis == "x": + offset = (parent.offset[0], center, 0) + elif axis == "y": + offset = (center, parent.offset[1], 0) + # set the initial bounds of the selector - self.selection = LinearRegionSelectionFeature( - self, bounds, axis=axis, limits=self._limits + # compensate for any offset from the parent graphic + # selection feature only works in world space, not data space + self._selection = LinearRegionSelectionFeature( + selection, axis=axis, limits=self._limits ) self._handled_widgets = list() @@ -248,17 +233,22 @@ def __init__( hover_responsive=self.edges, arrow_keys_modifier=arrow_keys_modifier, axis=axis, + parent=parent, name=name, + offset=offset, ) self._set_world_object(group) + self.selection = selection + def get_selected_data( self, graphic: Graphic = None - ) -> Union[np.ndarray, List[np.ndarray], None]: + ) -> Union[np.ndarray, List[np.ndarray]]: """ Get the ``Graphic`` data bounded by the current selection. - Returns a view of the full data array. + Returns a view of the data array. + If the ``Graphic`` is a collection, such as a ``LineStack``, it returns a list of views of the full array. Can be performed on the ``parent`` Graphic or on another graphic by passing to the ``graphic`` arg. @@ -269,15 +259,16 @@ def get_selected_data( Parameters ---------- - graphic: Graphic, optional + graphic: Graphic, optional, default ``None`` if provided, returns the data selection from this graphic instead of the graphic set as ``parent`` Returns ------- - np.ndarray, List[np.ndarray], or None + np.ndarray or List[np.ndarray] view or list of views of the full array, returns ``None`` if selection is empty """ + source = self._get_source(graphic) ixs = self.get_selected_indices(source) @@ -290,40 +281,47 @@ def get_selected_data( for i, g in enumerate(source.graphics): if ixs[i].size == 0: - data_selections.append(None) + data_selections.append( + np.array([], dtype=np.float32).reshape(0, 3) + ) else: - s = slice(ixs[i][0], ixs[i][-1]) - data_selections.append(g.data.buffer.data[s]) + s = slice( + ixs[i][0], ixs[i][-1] + 1 + ) # add 1 because these are direct indices + # slices n_datapoints dim + data_selections.append(g.data[s]) - return source[:].data[s] - # just for one Line graphic + return source.data[s] else: if ixs.size == 0: - return None + # empty selection + return np.array([], dtype=np.float32).reshape(0, 3) - s = slice(ixs[0], ixs[-1]) - return source.data.buffer.data[s] + s = slice( + ixs[0], ixs[-1] + 1 + ) # add 1 to end because these are direct indices + # slices n_datapoints dim + # slice with min, max is faster than using all the indices + return source.data[s] + + if "Image" in source.__class__.__name__: + s = slice(ixs[0], ixs[-1] + 1) - 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] + # slice columns + return source.data[:, s] + elif self.axis == "y": - return source.data()[s] + # slice rows + return source.data[s] 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 - as the Line Geometry positions x-vals or y-vals. For example, if if you used a - np.linspace(0, 100, 1000) for xvals in your line, then you will have 1,000 - x-positions. If the selection ``bounds`` are set to ``(0, 10)``, the returned - indices would be ``(0, 100)``. + + These are the data indices along the selector's "axis" which correspond to the data under the selector. Parameters ---------- @@ -333,51 +331,45 @@ def get_selected_indices( Returns ------- Union[np.ndarray, List[np.ndarray]] - data indices of the selection, list of np.ndarray if graphic is LineCollection + data indices of the selection, list of np.ndarray if graphic is a collection """ + # we get the indices from the source graphic source = self._get_source(graphic) - # if the graphic position is not at (0, 0) then the bounds must be offset - offset = getattr(source, f"position_{self.selection.axis}") - offset_bounds = tuple(v - offset for v in self.selection()) - - # need them to be int to use as indices - offset_bounds = tuple(map(int, offset_bounds)) - - if self.selection.axis == "x": + # get the offset of the source graphic + if self.axis == "x": dim = 0 - else: + elif self.axis == "y": dim = 1 - if "Line" in source.__class__.__name__: - # now we need to map from graphic space to data space - # we can have more than 1 datapoint between two integer locations in the world space + # selector (min, max) data values along axis + bounds = self.selection + + if ( + "Line" in source.__class__.__name__ + or "Scatter" in source.__class__.__name__ + ): + # gets indices corresponding to n_datapoints dim + # data is [n_datapoints, xyz], so we return + # indices that can be used to slice `n_datapoints` if isinstance(source, GraphicCollection): ixs = list() 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]) - )[0] + # indices for each graphic in the collection + data = g.data[:, dim] + g_ixs = np.where((data >= bounds[0]) & (data <= 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]) - )[0] + data = source.data[:, dim] + ixs = np.where((data >= bounds[0]) & (data <= bounds[1]))[0] return ixs - if ( - "Heatmap" in source.__class__.__name__ - or "Image" in source.__class__.__name__ - ): + if "Image" in source.__class__.__name__: # indices map directly to grid geometry for image data buffer - ixs = np.arange(*self.selection(), dtype=int) - return ixs + return np.arange(*bounds, dtype=int) def make_ipywidget_slider(self, kind: str = "IntRangeSlider", **kwargs): """ @@ -410,9 +402,9 @@ def make_ipywidget_slider(self, kind: str = "IntRangeSlider", **kwargs): cls = getattr(ipywidgets, kind) - value = self.selection() + value = self.selection if "Int" in kind: - value = tuple(map(int, self.selection())) + value = tuple(map(int, self.selection)) slider = cls( min=self.limits[0], @@ -457,7 +449,7 @@ def add_ipywidget_handler(self, widget, step: Union[int, float] = None): def _setup_ipywidget_slider(self, widget): # setup an ipywidget slider with bidirectional callbacks to this LinearSelector - value = self.selection() + value = self.selection if isinstance(widget, ipywidgets.IntSlider): value = tuple(map(int, value)) @@ -468,7 +460,7 @@ def _setup_ipywidget_slider(self, widget): widget.observe(self._ipywidget_callback, "value") # user changes linear selection -> widget changes - self.selection.add_event_handler(self._update_ipywidgets) + self.add_event_handler(self._update_ipywidgets, "selection") self._plot_area.renderer.add_event_handler(self._set_slider_layout, "resize") @@ -502,43 +494,39 @@ def _set_slider_layout(self, *args): widget.layout = ipywidgets.Layout(width=f"{w}px") def _move_graphic(self, delta: np.ndarray): - # add delta to current bounds to get new positions - if self.selection.axis == "x": - # min and max of current bounds, i.e. the edges - xmin, xmax = self.selection() + # add delta to current min, max to get new positions + if self.axis == "x": + # add x value + new_min, new_max = self.selection + delta[0] - # new left bound position - bound0_new = xmin + delta[0] - - # new right bound position - bound1_new = xmax + delta[0] - else: - # min and max of current bounds, i.e. the edges - ymin, ymax = self.selection() + elif self.axis == "y": + # add y value + new_min, new_max = self.selection + delta[1] - # new bottom bound position - bound0_new = ymin + delta[1] - - # new top bound position - bound1_new = ymax + delta[1] - - # move entire selector if source was fill + # move entire selector if event source was fill if self._move_info.source == self.fill: - # set the new bounds - self.selection = (bound0_new, bound1_new) + # prevent weird shrinkage of selector if one edge is already at the limit + if self.selection[0] == self.limits[0] and new_min < self.limits[0]: + # self._move_end(None) # TODO: cancel further movement to prevent weird asynchronization with pointer + return + if self.selection[1] == self.limits[1] and new_max > self.limits[1]: + # self._move_end(None) + return + + # move entire selector + self._selection.set_value(self, (new_min, new_max)) return - # if selector is not resizable do nothing + # if selector is not resizable return if not self._resizable: return - # if resizable, move edges + # if event source was an edge and selector is resizable, + # move the edge that caused the event if self._move_info.source == self.edges[0]: # change only left or bottom bound - self.selection = (bound0_new, self.selection()[1]) + self._selection.set_value(self, (new_min, self._selection.value[1])) elif self._move_info.source == self.edges[1]: # change only right or top bound - self.selection = (self.selection()[0], bound1_new) - else: - return + self._selection.set_value(self, (self.selection[0], new_max)) diff --git a/fastplotlib/graphics/selectors/_sync.py b/fastplotlib/graphics/selectors/_sync.py deleted file mode 100644 index ce903aab8..000000000 --- a/fastplotlib/graphics/selectors/_sync.py +++ /dev/null @@ -1,90 +0,0 @@ -from . import LinearSelector -from typing import * - - -class Synchronizer: - def __init__( - self, *selectors: LinearSelector, key_bind: Union[str, None] = "Shift" - ): - """ - Synchronize the movement of `Selectors`. Selectors will move in sync only when the selected `"key_bind"` is - used during the mouse movement event. Valid key binds are: ``"Control"``, ``"Shift"`` and ``"Alt"``. - If ``key_bind`` is ``None`` then the selectors will always be synchronized. - - Parameters - ---------- - selectors - selectors to synchronize - - key_bind: str, default ``"Shift"`` - one of ``"Control"``, ``"Shift"`` and ``"Alt"`` or ``None`` - """ - self._selectors = list() - self.key_bind = key_bind - - for s in selectors: - self.add(s) - - self.block_event = False - - self.enabled: bool = True - - @property - def selectors(self): - """Selectors managed by the Synchronizer""" - return self._selectors - - def add(self, selector): - """add a selector""" - selector.selection.add_event_handler(self._handle_event) - self._selectors.append(selector) - - def remove(self, selector): - """remove a selector""" - selector.selection.remove_event_handler(self._handle_event) - self._selectors.remove(selector) - - def clear(self): - for i in range(len(self.selectors)): - self.remove(self.selectors[0]) - - def _handle_event(self, ev): - if self.block_event: - # because infinite recursion - return - - if not self.enabled: - return - - self.block_event = True - - source = ev.pick_info["graphic"] - delta = ev.pick_info["delta"] - pygfx_ev = ev.pick_info["pygfx_event"] - - # only moves when modifier is used - if pygfx_ev is None: - self.block_event = False - return - - if self.key_bind is not None: - if self.key_bind not in pygfx_ev.modifiers: - self.block_event = False - return - - if delta is not None: - self._move_selectors(source, delta) - - self.block_event = False - - def _move_selectors(self, source, delta): - for s in self.selectors: - # must use == and not is to compare Graphics because they are weakref proxies! - if s == source: - # if it's the source, since it has already moved - continue - - s._move_graphic(delta) - - def __del__(self): - self.clear() diff --git a/fastplotlib/graphics/text.py b/fastplotlib/graphics/text.py index 49b4ac4be..fcee6129b 100644 --- a/fastplotlib/graphics/text.py +++ b/fastplotlib/graphics/text.py @@ -1,22 +1,35 @@ -from typing import * import pygfx import numpy as np from ._base import Graphic +from ._features import ( + TextData, + FontSize, + TextFaceColor, + TextOutlineColor, + TextOutlineThickness, +) class TextGraphic(Graphic): + _features = { + "text", + "font_size", + "face_color", + "outline_color", + "outline_thickness", + } + def __init__( self, text: str, - position: Tuple[int] = (0, 0, 0), - size: int = 14, - face_color: Union[str, np.ndarray] = "w", - outline_color: Union[str, np.ndarray] = "w", - outline_thickness=0, + font_size: float | int = 14, + face_color: str | np.ndarray | list[float] | tuple[float] = "w", + outline_color: str | np.ndarray | list[float] | tuple[float] = "w", + outline_thickness: float = 0.0, screen_space: bool = True, + offset: tuple[float] = (0, 0, 0), anchor: str = "middle-center", - *args, **kwargs, ): """ @@ -25,13 +38,10 @@ def __init__( Parameters ---------- text: str - display text - - position: int tuple, default (0, 0, 0) - int tuple indicating location of text in scene + text to display - size: int, default 10 - text size + font_size: float | int, default 10 + font size face_color: str or array, default "w" str or RGBA array to set the color of the text @@ -39,14 +49,14 @@ def __init__( 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 + outline_thickness: float, default 0 + relative outline thickness, value between 0.0 - 0.5 screen_space: bool = True - whether the text is rendered in screen space, in contrast to world space + if True, text size is in screen space, if False the text size is in data space - name: str, optional - name of graphic, passed to Graphic + offset: (float, float, float), default (0, 0, 0) + places the text at this location anchor: str, default "middle-center" position of the origin of the text @@ -54,94 +64,80 @@ def __init__( * Vertical values: "top", "middle", "baseline", "bottom" * Horizontal values: "left", "center", "right" + + **kwargs + passed to Graphic + """ - super().__init__(*args, **kwargs) - self._text = text + super().__init__(**kwargs) + + self._text = TextData(text) + self._font_size = FontSize(font_size) + self._face_color = TextFaceColor(face_color) + self._outline_color = TextOutlineColor(outline_color) + self._outline_thickness = TextOutlineThickness(outline_thickness) world_object = pygfx.Text( pygfx.TextGeometry( - text=str(text), - font_size=size, + text=self.text, + font_size=self.font_size, screen_space=screen_space, anchor=anchor, ), pygfx.TextMaterial( - color=face_color, - outline_color=outline_color, - outline_thickness=outline_thickness, + color=self.face_color, + outline_color=self.outline_color, + outline_thickness=self.outline_thickness, pick_write=True, ), ) self._set_world_object(world_object) - self.world_object.position = position + self.offset = offset @property - def text(self): - """Returns the text of this graphic.""" - return self._text + def text(self) -> str: + """the text displayed""" + return self._text.value @text.setter def text(self, text: str): - """Set the text of this graphic.""" - if not isinstance(text, str): - raise ValueError("Text must be of type str.") - - self._text = text - self.world_object.geometry.set_text(self._text) + self._text.set_value(self, text) @property - def text_size(self): - """Returns the text size of this graphic.""" - return self.world_object.geometry.font_size + def font_size(self) -> float | int: + """ "text font size""" + return self._font_size.value - @text_size.setter - def text_size(self, size: Union[int, float]): - """Set the text size of this graphic.""" - if not (isinstance(size, int) or isinstance(size, float)): - raise ValueError("Text size must be of type int or float") - - self.world_object.geometry.font_size = size + @font_size.setter + def font_size(self, size: float | int): + self._font_size.set_value(self, size) @property - def face_color(self): - """Returns the face color of this graphic.""" - return self.world_object.material.color + def face_color(self) -> pygfx.Color: + """text face color""" + return self._face_color.value @face_color.setter - def face_color(self, color: Union[str, np.ndarray]): - """Set the face color of this graphic.""" - if not (isinstance(color, str) or isinstance(color, np.ndarray)): - raise ValueError("Face color must be of type str or np.ndarray") - - color = pygfx.Color(color) - - self.world_object.material.color = color + def face_color(self, color: str | np.ndarray | list[float] | tuple[float]): + self._face_color.set_value(self, color) @property - def outline_size(self): - """Returns the outline size of this graphic.""" - return self.world_object.material.outline_thickness + def outline_thickness(self) -> float: + """text outline thickness""" + return self._outline_thickness.value - @outline_size.setter - def outline_size(self, size: Union[int, float]): - """Set the outline size of this text graphic.""" - if not (isinstance(size, int) or isinstance(size, float)): - raise ValueError("Outline size must be of type int or float") - - self.world_object.material.outline_thickness = size + @outline_thickness.setter + def outline_thickness(self, thickness: float): + self._outline_thickness.set_value(self, thickness) @property - def outline_color(self): - """Returns the outline color of this graphic.""" - return self.world_object.material.outline_color + def outline_color(self) -> pygfx.Color: + """text outline color""" + return self._outline_color.value @outline_color.setter - def outline_color(self, color: Union[str, np.ndarray]): - """Set the outline color of this graphic""" - if not (isinstance(color, str) or isinstance(color, np.ndarray)): - raise ValueError("Outline color must be of type str or np.ndarray") - - self.world_object.material.outline_color = color + def outline_color(self, color: str | np.ndarray | list[float] | tuple[float]): + self._outline_color.set_value(self, color) diff --git a/fastplotlib/layouts/_figure.py b/fastplotlib/layouts/_figure.py index 1c7439613..2c157db8f 100644 --- a/fastplotlib/layouts/_figure.py +++ b/fastplotlib/layouts/_figure.py @@ -128,7 +128,6 @@ def __init__( # if controller instances have been specified for each subplot if controllers is not None: - # one controller for all subplots if isinstance(controllers, pygfx.Controller): controllers = [controllers] * len(self) diff --git a/fastplotlib/layouts/_graphic_methods_mixin.py b/fastplotlib/layouts/_graphic_methods_mixin.py index 9f82cfed5..387549ade 100644 --- a/fastplotlib/layouts/_graphic_methods_mixin.py +++ b/fastplotlib/layouts/_graphic_methods_mixin.py @@ -28,18 +28,17 @@ def _create_graphic(self, graphic_class, *args, **kwargs) -> Graphic: # only return a proxy to the real graphic return weakref.proxy(graphic) - def add_heatmap( + def add_image( self, data: Any, vmin: int = None, vmax: int = None, cmap: str = "plasma", - filter: str = "nearest", - chunk_size: int = 8192, + interpolation: str = "nearest", + cmap_interpolation: str = "linear", isolated_buffer: bool = True, - *args, **kwargs - ) -> HeatmapGraphic: + ) -> ImageGraphic: """ Create an Image Graphic @@ -48,7 +47,6 @@ def add_heatmap( ---------- 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 @@ -60,107 +58,20 @@ def add_heatmap( cmap: str, optional, default "plasma" colormap to use to display the data - filter: str, optional, default "nearest" + interpolation: 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" + cmap_interpolation: str, optional, default "linear" + colormap interpolation method, 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( @@ -169,24 +80,27 @@ def add_image( vmin, vmax, cmap, - filter, + interpolation, + cmap_interpolation, isolated_buffer, - *args, **kwargs ) def add_line_collection( self, - data: List[numpy.ndarray], - z_offset: Union[Iterable[float], float] = None, - thickness: Union[float, Iterable[float]] = 2.0, - colors: Union[str, Iterable[str], numpy.ndarray, Iterable[numpy.ndarray]] = "w", + data: Union[numpy.ndarray, List[numpy.ndarray]], + thickness: Union[float, Sequence[float]] = 2.0, + colors: Union[str, Sequence[str], numpy.ndarray, Sequence[numpy.ndarray]] = "w", + uniform_colors: bool = False, alpha: float = 1.0, - cmap: Union[Iterable[str], str] = None, - cmap_values: Union[numpy.ndarray, List] = None, + cmap: Union[Sequence[str], str] = None, + cmap_transform: Union[numpy.ndarray, List] = None, name: str = None, - metadata: Union[Iterable[Any], numpy.ndarray] = None, - *args, + names: list[str] = None, + metadata: Any = None, + metadatas: Union[Sequence[Any], numpy.ndarray] = None, + isolated_buffer: bool = True, + kwargs_lines: list[dict] = None, **kwargs ) -> LineCollection: """ @@ -195,13 +109,11 @@ def add_line_collection( 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] + data: list of array-like + List or array-like of multiple line data to plot - z_offset: Iterable of float or float, optional - | if ``float``, single offset will be used for all lines - | if ``list`` of ``float``, each value will apply to the individual lines + | if ``list`` each item in the list must be a 1D, 2D, or 3D numpy array + | if array-like, must be of shape [n_lines, n_points_line, y | xy | xyz] thickness: float or Iterable of float, default 2.0 | if ``float``, single thickness will be used for all lines @@ -223,43 +135,45 @@ def add_line_collection( .. note:: ``cmap`` overrides any arguments passed to ``colors`` - cmap_values: 1D array-like or Iterable of numerical values, optional + cmap_transform: 1D array-like 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: Iterable or array - metadata associated with this collection, this is for the user to manage. - ``len(metadata)`` must be same as ``len(data)`` + name of the line collection as a whole - args - passed to GraphicCollection + names: list[str], optional + names of the individual lines in the collection, ``len(names)`` must equal ``len(data)`` - kwargs - passed to GraphicCollection + metadata: Any + meatadata associated with the collection as a whole - Features - -------- + metadatas: Iterable or array + metadata for each individual line associated with this collection, this is for the user to manage. + ``len(metadata)`` must be same as ``len(data)`` - Collections support the same features as the underlying graphic. You just have to slice the selection. + kwargs_lines: list[dict], optional + list of kwargs passed to the individual lines, ``len(kwargs_lines)`` must equal ``len(data)`` - See :class:`LineGraphic` details on the features. + kwargs_collection + kwargs for the collection, passed to GraphicCollection """ return self._create_graphic( LineCollection, data, - z_offset, thickness, colors, + uniform_colors, alpha, cmap, - cmap_values, + cmap_transform, name, + names, metadata, - *args, + metadatas, + isolated_buffer, + kwargs_lines, **kwargs ) @@ -268,12 +182,11 @@ def add_line( data: Any, thickness: float = 2.0, colors: Union[str, numpy.ndarray, Iterable] = "w", + uniform_color: bool = False, alpha: float = 1.0, cmap: str = None, - cmap_values: Union[numpy.ndarray, Iterable] = None, - z_position: float = None, - collection_index: int = None, - *args, + cmap_transform: Union[numpy.ndarray, Iterable] = None, + isolated_buffer: bool = True, **kwargs ) -> LineGraphic: """ @@ -292,43 +205,23 @@ def add_line( 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 Iterable of numerical values, optional - if provided, these values are used to map the colors from the cmap + uniform_color: bool, default ``False`` + if True, uses a uniform buffer for the line color, + basically saves GPU VRAM when the entire line has a single color alpha: float, optional, default 1.0 alpha value for the colors - z_position: float, optional - z-axis position for placing the graphic + cmap: str, optional + apply a colormap to the line instead of assigning colors manually, this + overrides any argument passed to "colors" - args - passed to Graphic + cmap_transform: 1D array-like of numerical values, optional + if provided, these values are used to map the colors from the cmap - kwargs + **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( @@ -336,29 +229,30 @@ def add_line( data, thickness, colors, + uniform_color, alpha, cmap, - cmap_values, - z_position, - collection_index, - *args, + cmap_transform, + isolated_buffer, **kwargs ) def add_line_stack( self, data: List[numpy.ndarray], - z_offset: Union[Iterable[float], float] = None, thickness: Union[float, Iterable[float]] = 2.0, colors: Union[str, Iterable[str], numpy.ndarray, Iterable[numpy.ndarray]] = "w", alpha: float = 1.0, cmap: Union[Iterable[str], str] = None, - cmap_values: Union[numpy.ndarray, List] = None, + cmap_transform: Union[numpy.ndarray, List] = None, name: str = None, - metadata: Union[Iterable[Any], numpy.ndarray] = None, + names: list[str] = None, + metadata: Any = None, + metadatas: Union[Sequence[Any], numpy.ndarray] = None, + isolated_buffer: bool = True, separation: float = 10.0, separation_axis: str = "y", - *args, + kwargs_lines: list[dict] = None, **kwargs ) -> LineStack: """ @@ -367,13 +261,11 @@ def add_line_stack( 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] + data: list of array-like + List or array-like of multiple line data to plot - z_offset: Iterable of float or float, optional - | if ``float``, single offset will be used for all lines - | if ``list`` of ``float``, each value will apply to the individual lines + | if ``list`` each item in the list must be a 1D, 2D, or 3D numpy array + | if array-like, must be of shape [n_lines, n_points_line, y | xy | xyz] thickness: float or Iterable of float, default 2.0 | if ``float``, single thickness will be used for all lines @@ -385,6 +277,9 @@ def add_line_stack( | 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 + alpha: float, optional + alpha value for colors, if colors is a ``str`` + cmap: Iterable 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 @@ -392,11 +287,20 @@ def add_line_stack( .. note:: ``cmap`` overrides any arguments passed to ``colors`` - cmap_values: 1D array-like or Iterable of numerical values, optional + cmap_transform: 1D array-like of numerical values, optional if provided, these values are used to map the colors from the cmap - metadata: Iterable or array - metadata associated with this collection, this is for the user to manage. + name: str, optional + name of the line collection as a whole + + names: list[str], optional + names of the individual lines in the collection, ``len(names)`` must equal ``len(data)`` + + metadata: Any + metadata associated with the collection as a whole + + metadatas: Iterable or array + metadata for each individual line associated with this collection, this is for the user to manage. ``len(metadata)`` must be same as ``len(data)`` separation: float, default 10 @@ -405,48 +309,45 @@ def add_line_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 - - kwargs - passed to LineCollection - Features - -------- + kwargs_lines: list[dict], optional + list of kwargs passed to the individual lines, ``len(kwargs_lines)`` must equal ``len(data)`` - Collections support the same features as the underlying graphic. You just have to slice the selection. - - See :class:`LineGraphic` details on the features. + kwargs_collection + kwargs for the collection, passed to GraphicCollection """ return self._create_graphic( LineStack, data, - z_offset, thickness, colors, alpha, cmap, - cmap_values, + cmap_transform, name, + names, metadata, + metadatas, + isolated_buffer, separation, separation_axis, - *args, + kwargs_lines, **kwargs ) def add_scatter( self, - data: numpy.ndarray, - sizes: Union[float, numpy.ndarray, Iterable[float]] = 1, - colors: Union[str, numpy.ndarray, Iterable[str]] = "w", + data: Any, + colors: str | numpy.ndarray | tuple[float] | list[float] | list[str] = "w", + uniform_color: bool = False, alpha: float = 1.0, cmap: str = None, - cmap_values: Union[numpy.ndarray, List] = None, - z_position: float = 0.0, - *args, + cmap_transform: numpy.ndarray = None, + isolated_buffer: bool = True, + sizes: Union[float, numpy.ndarray, Iterable[float]] = 1, + uniform_size: bool = False, **kwargs ) -> ScatterGraphic: """ @@ -458,73 +359,64 @@ def add_scatter( 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 + uniform_color: bool, default False + if True, uses a uniform buffer for the scatter point colors, + basically saves GPU VRAM when the entire line has a single color + + alpha: float, optional, default 1.0 + alpha value for the colors + cmap: str, optional apply a colormap to the scatter instead of assigning colors manually, this overrides any argument passed to "colors" - cmap_values: 1D array-like or list of numerical values, optional + cmap_transform: 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 + isolated_buffer: bool, default True + whether the buffers should be isolated from the user input array. + Generally always ``True``, ``False`` is for rare advanced use. - z_position: float, optional - z-axis position for placing the graphic + sizes: float or iterable of float, optional, default 1.0 + size of the scatter points - args - passed to Graphic + uniform_size: bool, default False + if True, uses a uniform buffer for the scatter point sizes, + basically saves GPU VRAM when all scatter points are the same size 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, + uniform_color, alpha, cmap, - cmap_values, - z_position, - *args, + cmap_transform, + isolated_buffer, + sizes, + uniform_size, **kwargs ) def add_text( self, text: str, - position: Tuple[int] = (0, 0, 0), - size: int = 14, - face_color: Union[str, numpy.ndarray] = "w", - outline_color: Union[str, numpy.ndarray] = "w", - outline_thickness=0, + font_size: float | int = 14, + face_color: str | numpy.ndarray | list[float] | tuple[float] = "w", + outline_color: str | numpy.ndarray | list[float] | tuple[float] = "w", + outline_thickness: float = 0.0, screen_space: bool = True, + offset: tuple[float] = (0, 0, 0), anchor: str = "middle-center", - *args, **kwargs ) -> TextGraphic: """ @@ -534,13 +426,10 @@ def add_text( Parameters ---------- text: str - display text + text to display - position: int tuple, default (0, 0, 0) - int tuple indicating location of text in scene - - size: int, default 10 - text size + font_size: float | int, default 10 + font size face_color: str or array, default "w" str or RGBA array to set the color of the text @@ -548,14 +437,14 @@ def add_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 + outline_thickness: float, default 0 + relative outline thickness, value between 0.0 - 0.5 screen_space: bool = True - whether the text is rendered in screen space, in contrast to world space + if True, text size is in screen space, if False the text size is in data space - name: str, optional - name of graphic, passed to Graphic + offset: (float, float, float), default (0, 0, 0) + places the text at this location anchor: str, default "middle-center" position of the origin of the text @@ -564,17 +453,20 @@ def add_text( * Vertical values: "top", "middle", "baseline", "bottom" * Horizontal values: "left", "center", "right" + **kwargs + passed to Graphic + + """ return self._create_graphic( TextGraphic, text, - position, - size, + font_size, face_color, outline_color, outline_thickness, screen_space, + offset, anchor, - *args, **kwargs ) diff --git a/fastplotlib/layouts/_plot_area.py b/fastplotlib/layouts/_plot_area.py index 6ff07a748..d8e0adebc 100644 --- a/fastplotlib/layouts/_plot_area.py +++ b/fastplotlib/layouts/_plot_area.py @@ -12,6 +12,7 @@ from ._utils import create_controller from ..graphics._base import Graphic +from ..graphics._collection_base import GraphicCollection from ..graphics.selectors._base_selector import BaseSelector from ..legends import Legend @@ -469,14 +470,14 @@ def add_graphic(self, graphic: Graphic, center: bool = True): if self.camera.fov == 0: # for orthographic positions stack objects along the z-axis # for perspective projections we assume the user wants full 3D control - graphic.position_z = len(self) + graphic.offset = (*graphic.offset[:-1], len(self)) def insert_graphic( self, graphic: Graphic, center: bool = True, index: int = 0, - z_position: int = None, + auto_offset: int = None, ): """ Insert graphic into scene at given position ``index`` in stored graphics. @@ -493,8 +494,8 @@ def insert_graphic( index: int, default 0 Index to insert graphic. - z_position: int, default None - z axis position to place Graphic. If ``None``, uses value of `index` argument + auto_offset: bool, default True + If True and using an orthographic projection, sets z-axis offset of graphic to `index` """ if index > len(self._graphics): @@ -511,10 +512,8 @@ def insert_graphic( if self.camera.fov == 0: # for orthographic positions stack objects along the z-axis # for perspective projections we assume the user wants full 3D control - if z_position is None: - graphic.position_z = index - else: - graphic.position_z = z_position + if auto_offset: + graphic.offset = (*graphic.offset[:-1], index) def _add_or_insert_graphic( self, diff --git a/fastplotlib/layouts/_subplot.py b/fastplotlib/layouts/_subplot.py index a541c9d78..059307e6b 100644 --- a/fastplotlib/layouts/_subplot.py +++ b/fastplotlib/layouts/_subplot.py @@ -149,7 +149,7 @@ def set_title(self, text: str): if self._title_graphic is not None: self._title_graphic.text = text else: - tg = TextGraphic(text=text, size=18) + tg = TextGraphic(text=text, font_size=18) self._title_graphic = tg self.docks["top"].size = 35 diff --git a/fastplotlib/utils/functions.py b/fastplotlib/utils/functions.py index 561863b0c..73752ba5e 100644 --- a/fastplotlib/utils/functions.py +++ b/fastplotlib/utils/functions.py @@ -91,8 +91,8 @@ def make_colors(n_colors: int, cmap: str, alpha: float = 1.0) -> np.ndarray: 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}>" + f"You have requested <{n_colors}> colors but only <{max_colors}> exist for the " + f"chosen cmap: <{name}>" ) return cmap[:n_colors] @@ -239,7 +239,7 @@ def normalize_min_max(a): def parse_cmap_values( n_colors: int, cmap_name: str, - cmap_values: np.ndarray | list[int | float] = None, + transform: np.ndarray | list[int | float] = None, ) -> np.ndarray: """ @@ -251,28 +251,25 @@ def parse_cmap_values( cmap_name: str colormap name - cmap_values: np.ndarray | List[int | float], optional - cmap values + transform: np.ndarray | List[int | float], optional + cmap transform Returns ------- """ - if cmap_values is None: - # use the cmap values linearly just along the collection indices - # for example, if len(data) = 10 and the cmap is "jet", then it will - # linearly go from blue to red from data[0] to data[-1] + if transform is None: colors = make_colors(n_colors, cmap_name) return colors else: - if not isinstance(cmap_values, np.ndarray): - cmap_values = np.array(cmap_values) + if not isinstance(transform, np.ndarray): + transform = np.array(transform) - # use the values within cmap_values to set the color of the corresponding data - # each individual data[i] has its color based on the "relative cmap_value intensity" - if len(cmap_values) != n_colors: + # use the of the cmap_transform to set the color of the corresponding data + # each individual data[i] has its color based on the transform values + if len(transform) != n_colors: raise ValueError( - f"len(cmap_values) != len(data): {len(cmap_values)} != {n_colors}" + f"len(cmap_values) != len(data): {len(transform)} != {n_colors}" ) colormap = get_cmap(cmap_name) @@ -280,23 +277,23 @@ def parse_cmap_values( n_colors = colormap.shape[0] - 1 if cmap_name in QUALITATIVE_CMAPS: - # check that cmap_values are and within the number of colors `n_colors` + # check that cmap_transform are and within the number of colors `n_colors` # do not scale, use directly - if not np.issubdtype(cmap_values.dtype, np.integer): + if not np.issubdtype(transform.dtype, np.integer): raise TypeError( - f" cmap_values should be used with qualitative colormaps, the dtype you " - f"have passed is {cmap_values.dtype}" + f" `cmap_transform` values should be used with qualitative colormaps, " + f"the dtype you have passed is {transform.dtype}" ) - if max(cmap_values) > n_colors: + if max(transform) > n_colors: raise IndexError( f"You have chosen the qualitative colormap <'{cmap_name}'> which only has " - f"<{n_colors}> colors, which is lower than the max value of your `cmap_values`." + f"<{n_colors}> colors, which is lower than the max value of your `cmap_transform`." f"Choose a cmap with more colors, or a non-quantitative colormap." ) - norm_cmap_values = cmap_values + norm_cmap_values = transform else: # scale between 0 - n_colors so we can just index the colormap as a LUT - norm_cmap_values = (normalize_min_max(cmap_values) * n_colors).astype(int) + norm_cmap_values = (normalize_min_max(transform) * n_colors).astype(int) # use colormap as LUT to map the cmap_values to the colormap index colors = np.vstack([colormap[val] for val in norm_cmap_values]) diff --git a/fastplotlib/utils/gui.py b/fastplotlib/utils/gui.py index b59c7799b..1941674ee 100644 --- a/fastplotlib/utils/gui.py +++ b/fastplotlib/utils/gui.py @@ -44,7 +44,6 @@ def _notebook_print_banner(): - from ipywidgets import Image from IPython.display import display diff --git a/fastplotlib/widgets/histogram_lut.py b/fastplotlib/widgets/histogram_lut.py index 971bc1a28..a3edffcbd 100644 --- a/fastplotlib/widgets/histogram_lut.py +++ b/fastplotlib/widgets/histogram_lut.py @@ -52,29 +52,30 @@ def __init__( origin = (hist_scaled.max() / 2, 0) self._linear_region_selector = LinearRegionSelector( - bounds=bounds, + selection=bounds, limits=limits, size=size, - origin=origin, + center=origin[0], axis="y", edge_thickness=8, + parent=self._histogram_line, ) # there will be a small difference with the histogram edges so this makes them both line up exactly self._linear_region_selector.selection = ( - image_graphic.cmap.vmin, - image_graphic.cmap.vmax, + self._image_graphic.vmin, + self._image_graphic.vmax, ) - self._vmin = self.image_graphic.cmap.vmin - self._vmax = self.image_graphic.cmap.vmax + self._vmin = self.image_graphic.vmin + self._vmax = self.image_graphic.vmax vmin_str, vmax_str = self._get_vmin_vmax_str() self._text_vmin = TextGraphic( text=vmin_str, - size=16, - position=(0, 0), + font_size=16, + offset=(0, 0, 0), anchor="top-left", outline_color="black", outline_thickness=1, @@ -84,8 +85,8 @@ def __init__( self._text_vmax = TextGraphic( text=vmax_str, - size=16, - position=(0, 0), + font_size=16, + offset=(0, 0, 0), anchor="bottom-left", outline_color="black", outline_thickness=1, @@ -105,17 +106,15 @@ def __init__( self.world_object.local.scale_x *= -1 - self._text_vmin.position_x = -120 - self._text_vmin.position_y = self._linear_region_selector.selection()[0] + self._text_vmin.offset = (-120, self._linear_region_selector.selection[0], 0) - self._text_vmax.position_x = -120 - self._text_vmax.position_y = self._linear_region_selector.selection()[1] + self._text_vmax.offset = (-120, self._linear_region_selector.selection[1], 0) - self._linear_region_selector.selection.add_event_handler( - self._linear_region_handler + self._linear_region_selector.add_event_handler( + self._linear_region_handler, "selection" ) - self.image_graphic.cmap.add_event_handler(self._image_cmap_handler) + self.image_graphic.add_event_handler(self._image_cmap_handler, "vmin", "vmax") def _get_vmin_vmax_str(self) -> tuple[str, str]: if self.vmin < 0.001 or self.vmin > 99_999: @@ -198,16 +197,13 @@ def _calculate_histogram(self, data): def _linear_region_handler(self, ev): # must use world coordinate values directly from selection() # otherwise the linear region bounds jump to the closest bin edges - vmin, vmax = self._linear_region_selector.selection() + selected_ixs = self._linear_region_selector.selection + vmin, vmax = selected_ixs[0], selected_ixs[1] vmin, vmax = vmin / self._scale_factor, vmax / self._scale_factor self.vmin, self.vmax = vmin, vmax def _image_cmap_handler(self, ev): - self.vmin, self.vmax = ev.pick_info["vmin"], ev.pick_info["vmax"] - - def _block_events(self, b: bool): - self.image_graphic.cmap.block_events(b) - self._linear_region_selector.selection.block_events(b) + setattr(self, ev.type, ev.info["value"]) @property def vmin(self) -> float: @@ -215,22 +211,24 @@ def vmin(self) -> float: @vmin.setter def vmin(self, value: float): - self._block_events(True) + self.image_graphic.block_events = True + self._linear_region_selector.block_events = True # must use world coordinate values directly from selection() # otherwise the linear region bounds jump to the closest bin edges self._linear_region_selector.selection = ( value * self._scale_factor, - self._linear_region_selector.selection()[1], + self._linear_region_selector.selection[1], ) - self.image_graphic.cmap.vmin = value + self.image_graphic.vmin = value - self._block_events(False) + self.image_graphic.block_events = False + self._linear_region_selector.block_events = False self._vmin = value vmin_str, vmax_str = self._get_vmin_vmax_str() - self._text_vmin.position_y = self._linear_region_selector.selection()[0] + self._text_vmin.offset = (-120, self._linear_region_selector.selection[0], 0) self._text_vmin.text = vmin_str @property @@ -239,22 +237,25 @@ def vmax(self) -> float: @vmax.setter def vmax(self, value: float): - self._block_events(True) + self.image_graphic.block_events = True + self._linear_region_selector.block_events = True # must use world coordinate values directly from selection() # otherwise the linear region bounds jump to the closest bin edges self._linear_region_selector.selection = ( - self._linear_region_selector.selection()[0], + self._linear_region_selector.selection[0], value * self._scale_factor, ) - self.image_graphic.cmap.vmax = value - self._block_events(False) + self.image_graphic.vmax = value + + self.image_graphic.block_events = False + self._linear_region_selector.block_events = False self._vmax = value vmin_str, vmax_str = self._get_vmin_vmax_str() - self._text_vmax.position_y = self._linear_region_selector.selection()[1] + self._text_vmax.offset = (-120, self._linear_region_selector.selection[1], 0) self._text_vmax.text = vmax_str def set_data(self, data, reset_vmin_vmax: bool = True): @@ -262,12 +263,12 @@ def set_data(self, data, reset_vmin_vmax: bool = True): line_data = np.column_stack([hist_scaled, edges_flanked]) - self._histogram_line.data = line_data + # set x and y vals + self._histogram_line.data[:, :2] = line_data bounds = (edges[0], edges[-1]) limits = (edges_flanked[0], edges_flanked[-11]) origin = (hist_scaled.max() / 2, 0) - # self.linear_region.fill.world.position = (*origin, -2) if reset_vmin_vmax: # reset according to the new data @@ -275,9 +276,11 @@ def set_data(self, data, reset_vmin_vmax: bool = True): self._linear_region_selector.selection = bounds else: # don't change the current selection - self._block_events(True) + self.image_graphic.block_events = True + self._linear_region_selector.block_events = True self._linear_region_selector.limits = limits - self._block_events(False) + self.image_graphic.block_events = False + self._linear_region_selector.block_events = False self._data = weakref.proxy(data) @@ -297,14 +300,14 @@ def image_graphic(self, graphic): if self._image_graphic is not None: # cleanup events from current image graphic - self._image_graphic.cmap.remove_event_handler(self._image_cmap_handler) + self._image_graphic.remove_event_handler(self._image_cmap_handler) self._image_graphic = graphic - self.image_graphic.cmap.add_event_handler(self._image_cmap_handler) + self.image_graphic.add_event_handler(self._image_cmap_handler) def disconnect_image_graphic(self): - self._image_graphic.cmap.remove_event_handler(self._image_cmap_handler) + self._image_graphic.remove_event_handler(self._image_cmap_handler) del self._image_graphic # self._image_graphic = None diff --git a/fastplotlib/widgets/image.py b/fastplotlib/widgets/image.py index 2a4dc31b4..df9b46b55 100644 --- a/fastplotlib/widgets/image.py +++ b/fastplotlib/widgets/image.py @@ -366,7 +366,6 @@ def __init__( if isinstance(data, list): # verify that it's a list of np.ndarray if all([_is_arraylike(d) for d in data]): - # Grid computations if figure_shape is None: figure_shape = calculate_figure_shape(len(data)) @@ -755,7 +754,7 @@ def reset_vmin_vmax(self): Reset the vmin and vmax w.r.t. the full data """ for ig in self.managed_graphics: - ig.cmap.reset_vmin_vmax() + ig.reset_vmin_vmax() def reset_vmin_vmax_frame(self): """ @@ -773,7 +772,7 @@ def reset_vmin_vmax_frame(self): hlut = subplot.docks["right"]["histogram_lut"] # set the data using the current image graphic data - hlut.set_data(subplot["image_widget_managed"].data()) + hlut.set_data(subplot["image_widget_managed"].data.value) def set_data( self, diff --git a/scripts/generate_add_graphic_methods.py b/scripts/generate_add_graphic_methods.py index 2a480d884..3f45d9007 100644 --- a/scripts/generate_add_graphic_methods.py +++ b/scripts/generate_add_graphic_methods.py @@ -69,7 +69,7 @@ def generate_add_graphics_methods(): 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" return self._create_graphic({class_name.__name__}, {s} **kwargs)\n\n" ) f.close() diff --git a/setup.py b/setup.py index b50a6a9bf..3ba77201d 100644 --- a/setup.py +++ b/setup.py @@ -18,7 +18,6 @@ "ipywidgets>=8.0.0,<9", "sphinx-copybutton", "sphinx-design", - "nbsphinx", "pandoc", "jupyterlab", "sidecar", diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/events.py b/tests/events.py new file mode 100644 index 000000000..ea160dec3 --- /dev/null +++ b/tests/events.py @@ -0,0 +1,91 @@ +from functools import partial +import pytest +import numpy as np +from numpy import testing as npt +import pygfx + +import fastplotlib as fpl +from fastplotlib.graphics._features import FeatureEvent + + +def make_positions_data() -> np.ndarray: + xs = np.linspace(0, 10 * np.pi, 10) + ys = np.sin(xs) + return np.column_stack([xs, ys]) + + +def make_line_graphic() -> fpl.LineGraphic: + return fpl.LineGraphic(make_positions_data()) + + +def make_scatter_graphic() -> fpl.ScatterGraphic: + return fpl.ScatterGraphic(make_positions_data()) + + +event_instance: FeatureEvent = None + + +def event_handler(event): + global event_instance + event_instance = event + + +decorated_event_instance: FeatureEvent = None + + +@pytest.mark.parametrize("graphic", [make_line_graphic(), make_scatter_graphic()]) +def test_positions_data_event(graphic: fpl.LineGraphic | fpl.ScatterGraphic): + global decorated_event_instance + global event_instance + + value = np.cos(np.linspace(0, 10 * np.pi, 10))[3:8] + + info = {"key": (slice(3, 8, None), 1), "value": value} + + expected = FeatureEvent(type="data", info=info) + + def validate(graphic, handler, expected_feature_event, event_to_test): + assert expected_feature_event.type == event_to_test.type + assert expected_feature_event.info["key"] == event_to_test.info["key"] + + npt.assert_almost_equal( + expected_feature_event.info["value"], event_to_test.info["value"] + ) + + # should only have one event handler + assert graphic._event_handlers["data"] == {handler} + + # make sure wrappers are correct + wrapper_map = tuple(graphic._event_handler_wrappers["data"])[0] + assert wrapper_map[0] is handler + assert isinstance(wrapper_map[1], partial) + assert wrapper_map[1].func == graphic._handle_event + assert wrapper_map[1].args[0] is handler + + # test remove handler + graphic.remove_event_handler(handler, "data") + assert len(graphic._event_handlers["click"]) == 0 + assert len(graphic._event_handler_wrappers["click"]) == 0 + assert len(graphic.world_object._event_handlers["click"]) == 0 + + # reset data + graphic.data[:, :-1] = make_positions_data() + event_to_test = None + + # test decorated function + @graphic.add_event_handler("data") + def decorated_handler(event): + global decorated_event_instance + decorated_event_instance = event + + # test decorated + graphic.data[3:8, 1] = value + validate(graphic, decorated_handler, expected, decorated_event_instance) + + # test regular + graphic.add_event_handler(event_handler, "data") + graphic.data[3:8, 1] = value + + validate(graphic, event_handler, expected, event_instance) + + event_instance = None diff --git a/tests/test_colors_buffer_manager.py b/tests/test_colors_buffer_manager.py new file mode 100644 index 000000000..252c6e5c3 --- /dev/null +++ b/tests/test_colors_buffer_manager.py @@ -0,0 +1,250 @@ +import numpy as np +from numpy import testing as npt +import pytest + +import pygfx + +import fastplotlib as fpl +from fastplotlib.graphics._features import VertexColors, FeatureEvent +from .utils import ( + generate_slice_indices, + assert_pending_uploads, + generate_color_inputs, + generate_positions_spiral_data, +) + + +def make_colors_buffer() -> VertexColors: + colors = VertexColors(colors="w", n_colors=10) + return colors + + +EVENT_RETURN_VALUE: FeatureEvent = None + + +def event_handler(ev): + global EVENT_RETURN_VALUE + EVENT_RETURN_VALUE = ev + + +@pytest.mark.parametrize( + "color_input", + [ + *generate_color_inputs("r"), + *generate_color_inputs("g"), + *generate_color_inputs("b"), + ], +) +def test_create_buffer(color_input): + colors = VertexColors(colors=color_input, n_colors=10) + truth = np.repeat([pygfx.Color(color_input)], 10, axis=0) + npt.assert_almost_equal(colors[:], truth) + + +@pytest.mark.parametrize("test_graphic", [False, "line", "scatter"]) +def test_int(test_graphic): + # setting single points + if test_graphic: + fig = fpl.Figure() + + data = generate_positions_spiral_data("xyz") + if test_graphic == "line": + graphic = fig[0, 0].add_line(data=data) + + elif test_graphic == "scatter": + graphic = fig[0, 0].add_scatter(data=data) + + colors = graphic.colors + global EVENT_RETURN_VALUE + graphic.add_event_handler(event_handler, "colors") + else: + colors = make_colors_buffer() + + # TODO: placeholder until I make a testing figure where we draw frames only on call + colors.buffer._gfx_pending_uploads.clear() + + colors[3] = "r" + npt.assert_almost_equal(colors[3], [1.0, 0.0, 0.0, 1.0]) + assert colors.buffer._gfx_pending_uploads[-1] == (3, 1) + + if test_graphic: + # test event + assert isinstance(EVENT_RETURN_VALUE, FeatureEvent) + assert EVENT_RETURN_VALUE.graphic == graphic + assert EVENT_RETURN_VALUE.target is graphic.world_object + assert EVENT_RETURN_VALUE.info["key"] == 3 + npt.assert_almost_equal( + EVENT_RETURN_VALUE.info["value"], np.array([[1, 0, 0, 1]]) + ) + assert EVENT_RETURN_VALUE.info["user_value"] == "r" + + colors[6] = [0.0, 1.0, 1.0, 1.0] + npt.assert_almost_equal(colors[6], [0.0, 1.0, 1.0, 1.0]) + + colors[7] = (0.0, 1.0, 1.0, 1.0) + npt.assert_almost_equal(colors[6], [0.0, 1.0, 1.0, 1.0]) + + colors[8] = np.array([1, 0, 1, 1]) + npt.assert_almost_equal(colors[8], [1.0, 0.0, 1.0, 1.0]) + + colors[2] = [1, 0, 1, 0.5] + npt.assert_almost_equal(colors[2], [1.0, 0.0, 1.0, 0.5]) + + +@pytest.mark.parametrize("test_graphic", [False, "line", "scatter"]) +@pytest.mark.parametrize( + "slice_method", [generate_slice_indices(i) for i in range(0, 16)] +) +def test_tuple(test_graphic, slice_method): + # setting entire array manually + if test_graphic: + fig = fpl.Figure() + + data = generate_positions_spiral_data("xyz") + if test_graphic == "line": + graphic = fig[0, 0].add_line(data=data) + + elif test_graphic == "scatter": + graphic = fig[0, 0].add_scatter(data=data) + + colors = graphic.colors + global EVENT_RETURN_VALUE + graphic.add_event_handler(event_handler, "colors") + else: + colors = make_colors_buffer() + + s = slice_method["slice"] + indices = slice_method["indices"] + others = slice_method["others"] + + # set all RGBA vals + colors[s, :] = 0.5 + truth = np.repeat([[0.5, 0.5, 0.5, 0.5]], repeats=len(indices), axis=0) + npt.assert_almost_equal(colors[indices], truth) + + if test_graphic: + # test event + assert isinstance(EVENT_RETURN_VALUE, FeatureEvent) + assert EVENT_RETURN_VALUE.graphic == graphic + assert EVENT_RETURN_VALUE.target is graphic.world_object + assert EVENT_RETURN_VALUE.info["key"] == (s, slice(None)) + npt.assert_almost_equal(EVENT_RETURN_VALUE.info["value"], truth) + assert EVENT_RETURN_VALUE.info["user_value"] == 0.5 + + # check others are not modified + others_truth = np.repeat([[1.0, 1.0, 1.0, 1.0]], repeats=len(others), axis=0) + npt.assert_almost_equal(colors[others], others_truth) + + # reset + if test_graphic: + # test setter + graphic.colors = "w" + else: + colors[:] = [1, 1, 1, 1] + truth = np.repeat([[1.0, 1.0, 1.0, 1.0]], 10, axis=0) + npt.assert_almost_equal(colors[:], truth) + + if test_graphic: + # test event + assert isinstance(EVENT_RETURN_VALUE, FeatureEvent) + assert EVENT_RETURN_VALUE.graphic == graphic + assert EVENT_RETURN_VALUE.target is graphic.world_object + assert EVENT_RETURN_VALUE.info["key"] == slice(None) + npt.assert_almost_equal(EVENT_RETURN_VALUE.info["value"], truth) + assert EVENT_RETURN_VALUE.info["user_value"] == "w" + + # set just R values + colors[s, 0] = 0.5 + truth = np.repeat([[0.5, 1.0, 1.0, 1.0]], repeats=len(indices), axis=0) + # check others not modified + npt.assert_almost_equal(colors[indices], truth) + npt.assert_almost_equal(colors[others], others_truth) + + # reset + colors[:] = (1, 1, 1, 1) + npt.assert_almost_equal(colors[:], np.repeat([[1.0, 1.0, 1.0, 1.0]], 10, axis=0)) + + # set green and blue + colors[s, 1:-1] = 0.7 + truth = np.repeat([[1.0, 0.7, 0.7, 1.0]], repeats=len(indices), axis=0) + npt.assert_almost_equal(colors[indices], truth) + npt.assert_almost_equal(colors[others], others_truth) + + # reset + colors[:] = (1, 1, 1, 1) + npt.assert_almost_equal(colors[:], np.repeat([[1.0, 1.0, 1.0, 1.0]], 10, axis=0)) + + # set only alpha + colors[s, -1] = 0.2 + truth = np.repeat([[1.0, 1.0, 1.0, 0.2]], repeats=len(indices), axis=0) + npt.assert_almost_equal(colors[indices], truth) + npt.assert_almost_equal(colors[others], others_truth) + + +@pytest.mark.parametrize("color_input", generate_color_inputs("red")) +# skip testing with int since that results in shape [1, 4] with np.repeat, int tested in independent unit test +@pytest.mark.parametrize( + "slice_method", [generate_slice_indices(i) for i in range(1, 16)] +) +@pytest.mark.parametrize("test_graphic", [False, "line", "scatter"]) +def test_slice(color_input, slice_method: dict, test_graphic: bool): + # slicing only first dim + if test_graphic: + fig = fpl.Figure() + + data = generate_positions_spiral_data("xyz") + if test_graphic == "line": + graphic = fig[0, 0].add_line(data=data) + + elif test_graphic == "scatter": + graphic = fig[0, 0].add_scatter(data=data) + + colors = graphic.colors + + global EVENT_RETURN_VALUE + graphic.add_event_handler(event_handler, "colors") + else: + colors = make_colors_buffer() + + # TODO: placeholder until I make a testing figure where we draw frames only on call + colors.buffer._gfx_pending_uploads.clear() + + s = slice_method["slice"] + indices = slice_method["indices"] + offset = slice_method["offset"] + size = slice_method["size"] + others = slice_method["others"] + + colors[s] = color_input + truth = np.repeat([pygfx.Color(color_input)], repeats=len(indices), axis=0) + # check that correct indices are modified + npt.assert_almost_equal(colors[s], truth) + npt.assert_almost_equal(colors[indices], truth) + + # check event + if test_graphic: + global EVENT_RETURN_VALUE + + assert isinstance(EVENT_RETURN_VALUE, FeatureEvent) + assert EVENT_RETURN_VALUE.graphic == graphic + assert EVENT_RETURN_VALUE.target is graphic.world_object + if isinstance(s, slice): + assert EVENT_RETURN_VALUE.info["key"] == s + else: + npt.assert_almost_equal(EVENT_RETURN_VALUE.info["key"], s) + npt.assert_almost_equal(EVENT_RETURN_VALUE.info["value"], truth) + if isinstance(color_input, str): + assert EVENT_RETURN_VALUE.info["user_value"] == color_input + else: + npt.assert_almost_equal(EVENT_RETURN_VALUE.info["user_value"], color_input) + + # make sure correct offset and size marked for pending upload + assert_pending_uploads(colors.buffer, offset, size) + + # check that others are not touched + others_truth = np.repeat([[1.0, 1.0, 1.0, 1.0]], repeats=len(others), axis=0) + npt.assert_almost_equal(colors[others], others_truth) + + # reset + colors[:] = (1, 1, 1, 1) + npt.assert_almost_equal(colors[:], np.repeat([[1.0, 1.0, 1.0, 1.0]], 10, axis=0)) diff --git a/tests/test_common_features.py b/tests/test_common_features.py new file mode 100644 index 000000000..332ac71ae --- /dev/null +++ b/tests/test_common_features.py @@ -0,0 +1,282 @@ +import numpy +import numpy as np +from numpy import testing as npt +import pytest + +import fastplotlib as fpl +from fastplotlib.graphics._features import FeatureEvent, Name, Offset, Rotation, Visible + + +def make_graphic(kind: str, **kwargs): + match kind: + case "image": + return fpl.ImageGraphic(np.random.rand(10, 10), **kwargs) + case "line": + return fpl.LineGraphic(np.random.rand(10), **kwargs) + case "scatter": + return fpl.ScatterGraphic( + np.column_stack([np.random.rand(10), np.random.rand(10)]), **kwargs + ) + case "text": + return fpl.TextGraphic("bah", **kwargs) + + +graphic_kinds = [ + "image", + "line", + "scatter", + "text", +] + + +RETURN_EVENT_VALUE: FeatureEvent = None +DECORATED_EVENT_VALUE: FeatureEvent = None + + +def return_event(ev: FeatureEvent): + global RETURN_EVENT_VALUE + RETURN_EVENT_VALUE = ev + + +@pytest.mark.parametrize("graphic", [make_graphic(k) for k in graphic_kinds]) +def test_name(graphic): + assert graphic.name is None + + graphic.add_event_handler(return_event, "name") + + graphic.name = "new_name" + + assert graphic.name == "new_name" + + global RETURN_EVENT_VALUE + + assert RETURN_EVENT_VALUE.type == "name" + assert RETURN_EVENT_VALUE.graphic is graphic + assert RETURN_EVENT_VALUE.target is graphic.world_object + assert RETURN_EVENT_VALUE.info["value"] == "new_name" + + # check removing event handler + RETURN_EVENT_VALUE = None + graphic.remove_event_handler(return_event, "name") + assert len(graphic._event_handlers["name"]) == 0 + + graphic.name = "new_name2" + + assert RETURN_EVENT_VALUE is None + assert graphic.name == "new_name2" + + # check adding event with decorator + global DECORATED_EVENT_VALUE + DECORATED_EVENT_VALUE = None + + @graphic.add_event_handler("name") + def decorated_handler(ev): + global DECORATED_EVENT_VALUE + DECORATED_EVENT_VALUE = ev + + graphic.name = "test_dec" + assert graphic.name == "test_dec" + + assert DECORATED_EVENT_VALUE.type == "name" + assert DECORATED_EVENT_VALUE.graphic is graphic + assert DECORATED_EVENT_VALUE.target is graphic.world_object + assert DECORATED_EVENT_VALUE.info["value"] == "test_dec" + + +@pytest.mark.parametrize( + "graphic", [make_graphic(k, name="init_name") for k in graphic_kinds] +) +def test_name_init(graphic): + assert graphic.name == "init_name" + + graphic.name = "new_name" + + assert graphic.name == "new_name" + + +@pytest.mark.parametrize("graphic", [make_graphic(k) for k in graphic_kinds]) +def test_offset(graphic): + npt.assert_almost_equal(graphic.offset, (0.0, 0.0, 0.0)) + npt.assert_almost_equal(graphic.world_object.world.position, (0.0, 0.0, 0.0)) + + graphic.add_event_handler(return_event, "offset") + + graphic.offset = (1.0, 2.0, 3.0) + + npt.assert_almost_equal(graphic.offset, (1.0, 2.0, 3.0)) + npt.assert_almost_equal(graphic.world_object.world.position, (1.0, 2.0, 3.0)) + + global RETURN_EVENT_VALUE + + assert RETURN_EVENT_VALUE.type == "offset" + assert RETURN_EVENT_VALUE.graphic is graphic + assert RETURN_EVENT_VALUE.target is graphic.world_object + npt.assert_almost_equal(RETURN_EVENT_VALUE.info["value"], (1.0, 2.0, 3.0)) + + # check removing event handler + RETURN_EVENT_VALUE = None + graphic.remove_event_handler(return_event, "offset") + assert len(graphic._event_handlers["offset"]) == 0 + + graphic.offset = (4, 5, 6) + + assert RETURN_EVENT_VALUE is None + npt.assert_almost_equal(graphic.offset, (4.0, 5.0, 6.0)) + + # check adding event with decorator + global DECORATED_EVENT_VALUE + DECORATED_EVENT_VALUE = None + + @graphic.add_event_handler("offset") + def decorated_handler(ev): + global DECORATED_EVENT_VALUE + DECORATED_EVENT_VALUE = ev + + graphic.offset = (7, 8, 9) + npt.assert_almost_equal(graphic.offset, (7.0, 8.0, 9.0)) + + assert DECORATED_EVENT_VALUE.type == "offset" + assert DECORATED_EVENT_VALUE.graphic is graphic + assert DECORATED_EVENT_VALUE.target is graphic.world_object + assert DECORATED_EVENT_VALUE.info["value"] == (7.0, 8.0, 9.0) + + +@pytest.mark.parametrize( + "graphic", [make_graphic(k, offset=(3.0, 4.0, 5.0)) for k in graphic_kinds] +) +def test_offset_init(graphic): + npt.assert_almost_equal(graphic.offset, (3.0, 4.0, 5.0)) + npt.assert_almost_equal(graphic.world_object.world.position, (3.0, 4.0, 5.0)) + + graphic.offset = (6.0, 7.0, 8.0) + + npt.assert_almost_equal(graphic.offset, (6.0, 7.0, 8.0)) + npt.assert_almost_equal(graphic.world_object.world.position, (6.0, 7.0, 8.0)) + + +@pytest.mark.parametrize("graphic", [make_graphic(k) for k in graphic_kinds]) +def test_rotation(graphic): + npt.assert_almost_equal(graphic.rotation, (0, 0, 0, 1)) + npt.assert_almost_equal(graphic.world_object.world.rotation, (0, 0, 0, 1)) + + graphic.add_event_handler(return_event, "rotation") + + graphic.rotation = (0.0, 0.0, 0.30001427, 0.95393471) + + npt.assert_almost_equal(graphic.rotation, (0.0, 0.0, 0.30001427, 0.95393471)) + npt.assert_almost_equal( + graphic.world_object.world.rotation, (0.0, 0.0, 0.30001427, 0.95393471) + ) + + global RETURN_EVENT_VALUE + + assert RETURN_EVENT_VALUE.type == "rotation" + assert RETURN_EVENT_VALUE.graphic is graphic + assert RETURN_EVENT_VALUE.target is graphic.world_object + npt.assert_almost_equal( + RETURN_EVENT_VALUE.info["value"], (0.0, 0.0, 0.30001427, 0.95393471) + ) + + # check removing event handler + RETURN_EVENT_VALUE = None + graphic.remove_event_handler(return_event, "rotation") + assert len(graphic._event_handlers["rotation"]) == 0 + + graphic.rotation = (0, 0, 0, 1) + + assert RETURN_EVENT_VALUE is None + npt.assert_almost_equal(graphic.rotation, (0, 0, 0, 1)) + + # check adding event with decorator + global DECORATED_EVENT_VALUE + DECORATED_EVENT_VALUE = None + + @graphic.add_event_handler("rotation") + def decorated_handler(ev): + global DECORATED_EVENT_VALUE + DECORATED_EVENT_VALUE = ev + + graphic.rotation = (0, 0, 0.6, 0.8) + npt.assert_almost_equal(graphic.rotation, (0, 0, 0.6, 0.8)) + + assert DECORATED_EVENT_VALUE.type == "rotation" + assert DECORATED_EVENT_VALUE.graphic is graphic + assert DECORATED_EVENT_VALUE.target is graphic.world_object + assert DECORATED_EVENT_VALUE.info["value"] == (0, 0, 0.6, 0.8) + + +@pytest.mark.parametrize( + "graphic", + [ + make_graphic(k, rotation=(0.0, 0.0, 0.30001427, 0.95393471)) + for k in graphic_kinds + ], +) +def test_rotation(graphic): + npt.assert_almost_equal(graphic.rotation, (0.0, 0.0, 0.30001427, 0.95393471)) + npt.assert_almost_equal( + graphic.world_object.world.rotation, (0.0, 0.0, 0.30001427, 0.95393471) + ) + + graphic.rotation = (0, 0.0, 0.6, 0.8) + + npt.assert_almost_equal(graphic.rotation, (0, 0.0, 0.6, 0.8)) + npt.assert_almost_equal(graphic.world_object.world.rotation, (0, 0.0, 0.6, 0.8)) + + +@pytest.mark.parametrize("graphic", [make_graphic(k) for k in graphic_kinds]) +def test_visible(graphic): + assert graphic.visible is True + assert graphic.world_object.visible is True + + graphic.add_event_handler(return_event, "rotation") + + graphic.visible = False + assert graphic.visible is False + assert graphic.world_object.visible is False + + global RETURN_EVENT_VALUE + + assert RETURN_EVENT_VALUE.type == "visible" + assert RETURN_EVENT_VALUE.graphic is graphic + assert RETURN_EVENT_VALUE.target is graphic.world_object + assert RETURN_EVENT_VALUE.info["value"] is False + + # check removing event handler + RETURN_EVENT_VALUE = None + graphic.remove_event_handler(return_event, "visible") + assert len(graphic._event_handlers["visible"]) == 0 + + graphic.visible = True + + assert RETURN_EVENT_VALUE is None + assert graphic.visible is True + + # check adding event with decorator + global DECORATED_EVENT_VALUE + DECORATED_EVENT_VALUE = None + + @graphic.add_event_handler("visible") + def decorated_handler(ev): + global DECORATED_EVENT_VALUE + DECORATED_EVENT_VALUE = ev + + graphic.visible = False + assert graphic.visible is False + + assert DECORATED_EVENT_VALUE.type == "visible" + assert DECORATED_EVENT_VALUE.graphic is graphic + assert DECORATED_EVENT_VALUE.target is graphic.world_object + assert DECORATED_EVENT_VALUE.info["value"] is False + + +@pytest.mark.parametrize( + "graphic", [make_graphic(k, visible=False) for k in graphic_kinds] +) +def test_visible(graphic): + assert graphic.visible is False + assert graphic.world_object.visible is False + + graphic.visible = True + assert graphic.visible is True + assert graphic.world_object.visible is True diff --git a/tests/test_figure.py b/tests/test_figure.py index 27b74c0b6..757b1eeae 100644 --- a/tests/test_figure.py +++ b/tests/test_figure.py @@ -6,21 +6,18 @@ def test_cameras_controller_properties(): - cameras = [ - ["2d", "3d", "3d"], - ["3d", "3d", "3d"] - ] + cameras = [["2d", "3d", "3d"], ["3d", "3d", "3d"]] controller_types = [ ["panzoom", "panzoom", "fly"], - ["orbit", "trackball", "panzoom"] + ["orbit", "trackball", "panzoom"], ] fig = fpl.Figure( shape=(2, 3), cameras=cameras, controller_types=controller_types, - canvas="offscreen" + canvas="offscreen", ) print(fig.canvas) @@ -34,13 +31,17 @@ def test_cameras_controller_properties(): for c1, c2 in zip(subplot_controllers, fig.controllers.ravel()): assert c1 is c2 - for camera_type, subplot_camera in zip(np.asarray(cameras).ravel(), fig.cameras.ravel()): + for camera_type, subplot_camera in zip( + np.asarray(cameras).ravel(), fig.cameras.ravel() + ): if camera_type == "2d": assert subplot_camera.fov == 0 else: assert subplot_camera.fov == 50 - for controller_type, subplot_controller in zip(np.asarray(controller_types).ravel(), fig.controllers.ravel()): + for controller_type, subplot_controller in zip( + np.asarray(controller_types).ravel(), fig.controllers.ravel() + ): match controller_type: case "panzoom": assert isinstance(subplot_controller, pygfx.PanZoomController) @@ -67,11 +68,7 @@ def test_cameras_controller_properties(): def test_controller_ids_int(): - ids = [ - [0, 1, 1], - [0, 2, 3], - [4, 1, 2] - ] + ids = [[0, 1, 1], [0, 2, 3], [4, 1, 2]] fig = fpl.Figure(shape=(3, 3), controller_ids=ids, canvas="offscreen") @@ -81,19 +78,13 @@ def test_controller_ids_int(): def test_controller_ids_int_change_controllers(): - ids = [ - [0, 1, 1], - [0, 2, 3], - [4, 1, 2] - ] + ids = [[0, 1, 1], [0, 2, 3], [4, 1, 2]] - cameras = [ - ["2d", "3d", "3d"], - ["2d", "3d", "2d"], - ["3d", "3d", "3d"] - ] + cameras = [["2d", "3d", "3d"], ["2d", "3d", "2d"], ["3d", "3d", "3d"]] - fig = fpl.Figure(shape=(3, 3), cameras=cameras, controller_ids=ids, canvas="offscreen") + fig = fpl.Figure( + shape=(3, 3), cameras=cameras, controller_ids=ids, canvas="offscreen" + ) assert isinstance(fig[0, 1].controller, pygfx.FlyController) @@ -101,30 +92,46 @@ def test_controller_ids_int_change_controllers(): fig[0, 1].controller = "panzoom" assert isinstance(fig[0, 1].controller, pygfx.PanZoomController) assert fig[0, 1].controller is fig[0, 2].controller is fig[2, 1].controller - assert set(fig[0, 1].controller.cameras) == {fig[0, 1].camera, fig[0, 2].camera, fig[2, 1].camera} + assert set(fig[0, 1].controller.cameras) == { + fig[0, 1].camera, + fig[0, 2].camera, + fig[2, 1].camera, + } # change to orbit fig[0, 1].controller = "orbit" assert isinstance(fig[0, 1].controller, pygfx.OrbitController) assert fig[0, 1].controller is fig[0, 2].controller is fig[2, 1].controller - assert set(fig[0, 1].controller.cameras) == {fig[0, 1].camera, fig[0, 2].camera, fig[2, 1].camera} + assert set(fig[0, 1].controller.cameras) == { + fig[0, 1].camera, + fig[0, 2].camera, + fig[2, 1].camera, + } def test_controller_ids_str(): - names = [ - ["a", "b", "c"], - ["d", "e", "f"] - ] + names = [["a", "b", "c"], ["d", "e", "f"]] - controller_ids = [ - ["a", "f"], - ["b", "d", "e"] - ] + controller_ids = [["a", "f"], ["b", "d", "e"]] - fig = fpl.Figure(shape=(2, 3), controller_ids=controller_ids, names=names, canvas="offscreen") + fig = fpl.Figure( + shape=(2, 3), controller_ids=controller_ids, names=names, canvas="offscreen" + ) - assert fig[0, 0].controller is fig[1, 2].controller is fig["a"].controller is fig["f"].controller - assert fig[0, 1].controller is fig[1, 0].controller is fig[1, 1].controller is fig["b"].controller is fig["d"].controller is fig["e"].controller + assert ( + fig[0, 0].controller + is fig[1, 2].controller + is fig["a"].controller + is fig["f"].controller + ) + assert ( + fig[0, 1].controller + is fig[1, 0].controller + is fig[1, 1].controller + is fig["b"].controller + is fig["d"].controller + is fig["e"].controller + ) # make sure subplot c is unique exclude_c = [fig[n].controller for n in ["a", "b", "d", "e", "f"]] @@ -137,22 +144,23 @@ def test_set_controllers_from_existing_controllers(): assert fig.controllers[:-1].size == 6 with pytest.raises(ValueError): - fig3 = fpl.Figure(shape=fig.shape, controllers=fig.controllers[:-1], canvas="offscreen") + fig3 = fpl.Figure( + shape=fig.shape, controllers=fig.controllers[:-1], canvas="offscreen" + ) for fig1_subplot, fig2_subplot in zip(fig, fig2): assert fig1_subplot.controller is fig2_subplot.controller - cameras = [ - [pygfx.PerspectiveCamera(), "3d"], - ["3d", "2d"] - ] + cameras = [[pygfx.PerspectiveCamera(), "3d"], ["3d", "2d"]] controllers = [ [pygfx.FlyController(cameras[0][0]), pygfx.TrackballController()], - [pygfx.OrbitController(), pygfx.PanZoomController()] + [pygfx.OrbitController(), pygfx.PanZoomController()], ] - fig = fpl.Figure(shape=(2, 2), cameras=cameras, controllers=controllers, canvas="offscreen") + fig = fpl.Figure( + shape=(2, 2), cameras=cameras, controllers=controllers, canvas="offscreen" + ) assert fig[0, 0].controller is controllers[0][0] assert fig[0, 1].controller is controllers[0][1] diff --git a/tests/test_image_graphic.py b/tests/test_image_graphic.py new file mode 100644 index 000000000..9f89e8aa8 --- /dev/null +++ b/tests/test_image_graphic.py @@ -0,0 +1,205 @@ +import numpy as np +from numpy import testing as npt +import imageio.v3 as iio + +import fastplotlib as fpl +from fastplotlib.graphics._features import FeatureEvent +from fastplotlib.utils import make_colors + +GRAY_IMAGE = iio.imread("imageio:camera.png") +RGB_IMAGE = iio.imread("imageio:astronaut.png") + + +COFFEE_IMAGE = iio.imread("imageio:coffee.png") + +# image cmap, vmin, vmax, interpolations +# new screenshot tests too for these when in graphics + + +EVENT_RETURN_VALUE: FeatureEvent = None + + +def event_handler(ev): + global EVENT_RETURN_VALUE + EVENT_RETURN_VALUE = ev + + +def check_event(graphic, feature, value): + global EVENT_RETURN_VALUE + assert isinstance(EVENT_RETURN_VALUE, FeatureEvent) + assert EVENT_RETURN_VALUE.type == feature + assert EVENT_RETURN_VALUE.graphic == graphic + assert EVENT_RETURN_VALUE.target == graphic.world_object + if isinstance(EVENT_RETURN_VALUE.info["value"], float): + # floating point error + npt.assert_almost_equal(EVENT_RETURN_VALUE.info["value"], value) + else: + assert EVENT_RETURN_VALUE.info["value"] == value + + +def check_set_slice( + data: np.ndarray, + image_graphic: fpl.ImageGraphic, + row_slice: slice, + col_slice: slice, +): + image_graphic.data[row_slice, col_slice] = 1 + data_values = image_graphic.data.value + npt.assert_almost_equal(data_values[row_slice, col_slice], 1) + + # make sure other vals unchanged + npt.assert_almost_equal(data_values[: row_slice.start], data[: row_slice.start]) + npt.assert_almost_equal(data_values[row_slice.stop :], data[row_slice.stop :]) + npt.assert_almost_equal( + data_values[:, : col_slice.start], data[:, : col_slice.start] + ) + npt.assert_almost_equal(data_values[:, col_slice.stop :], data[:, col_slice.stop :]) + + global EVENT_RETURN_VALUE + assert isinstance(EVENT_RETURN_VALUE, FeatureEvent) + assert EVENT_RETURN_VALUE.type == "data" + assert EVENT_RETURN_VALUE.graphic == image_graphic + assert EVENT_RETURN_VALUE.target == image_graphic.world_object + assert EVENT_RETURN_VALUE.info["key"] == (row_slice, col_slice) + npt.assert_almost_equal(EVENT_RETURN_VALUE.info["value"], 1) + + +def test_gray(): + fig = fpl.Figure() + ig = fig[0, 0].add_image(GRAY_IMAGE) + assert isinstance(ig, fpl.ImageGraphic) + + ig.add_event_handler( + event_handler, + "data", + "cmap", + "vmin", + "vmax", + "interpolation", + "cmap_interpolation", + ) + + npt.assert_almost_equal(ig.data.value, GRAY_IMAGE) + + ig.cmap = "viridis" + assert ig.cmap == "viridis" + check_event(graphic=ig, feature="cmap", value="viridis") + + new_colors = make_colors(256, "viridis") + for child in ig.world_object.children: + npt.assert_almost_equal(child.material.map.data, new_colors) + + ig.cmap = "jet" + assert ig.cmap == "jet" + + new_colors = make_colors(256, "jet") + for child in ig.world_object.children: + npt.assert_almost_equal(child.material.map.data, new_colors) + + assert ig.interpolation == "nearest" + for child in ig.world_object.children: + assert child.material.interpolation == "nearest" + + ig.interpolation = "linear" + assert ig.interpolation == "linear" + for child in ig.world_object.children: + assert child.material.interpolation == "linear" + check_event(graphic=ig, feature="interpolation", value="linear") + + assert ig.cmap_interpolation == "linear" + for child in ig.world_object.children: + assert child.material.map_interpolation == "linear" + + ig.cmap_interpolation = "nearest" + assert ig.cmap_interpolation == "nearest" + for child in ig.world_object.children: + assert child.material.map_interpolation == "nearest" + check_event(graphic=ig, feature="cmap_interpolation", value="nearest") + + npt.assert_almost_equal(ig.vmin, GRAY_IMAGE.min()) + npt.assert_almost_equal(ig.vmax, GRAY_IMAGE.max()) + + ig.vmin = 50 + assert ig.vmin == 50 + for child in ig.world_object.children: + assert child.material.clim == (50, ig.vmax) + check_event(graphic=ig, feature="vmin", value=50) + + ig.vmax = 100 + assert ig.vmax == 100 + for child in ig.world_object.children: + assert child.material.clim == (ig.vmin, 100) + check_event(graphic=ig, feature="vmax", value=100) + + # test reset + ig.reset_vmin_vmax() + npt.assert_almost_equal(ig.vmin, GRAY_IMAGE.min()) + npt.assert_almost_equal(ig.vmax, GRAY_IMAGE.max()) + + check_set_slice(GRAY_IMAGE, ig, slice(100, 200), slice(200, 300)) + + # test setting all values + ig.data = 1 + npt.assert_almost_equal(ig.data.value, 1) + + +def test_rgb(): + fig = fpl.Figure() + ig = fig[0, 0].add_image(RGB_IMAGE) + assert isinstance(ig, fpl.ImageGraphic) + + ig.add_event_handler(event_handler, "data") + + npt.assert_almost_equal(ig.data.value, RGB_IMAGE) + + assert ig.interpolation == "nearest" + for child in ig.world_object.children: + assert child.material.interpolation == "nearest" + + ig.interpolation = "linear" + assert ig.interpolation == "linear" + for child in ig.world_object.children: + assert child.material.interpolation == "linear" + + npt.assert_almost_equal(ig.vmin, RGB_IMAGE.min()) + npt.assert_almost_equal(ig.vmax, RGB_IMAGE.max()) + + ig.vmin = 50 + assert ig.vmin == 50 + for child in ig.world_object.children: + assert child.material.clim == (50, ig.vmax) + + ig.vmax = 100 + assert ig.vmax == 100 + for child in ig.world_object.children: + assert child.material.clim == (ig.vmin, 100) + + # test reset + ig.reset_vmin_vmax() + npt.assert_almost_equal(ig.vmin, RGB_IMAGE.min()) + npt.assert_almost_equal(ig.vmax, RGB_IMAGE.max()) + + check_set_slice(RGB_IMAGE, ig, slice(100, 200), slice(200, 300)) + + +def test_rgba(): + rgba = np.zeros(shape=(*COFFEE_IMAGE.shape[:2], 4), dtype=np.float32) + + fig = fpl.Figure() + ig = fig[0, 0].add_image(rgba) + assert isinstance(ig, fpl.ImageGraphic) + + npt.assert_almost_equal(ig.data.value, rgba) + + # fancy indexing + # set the blue values of some pixels with an alpha > 1 + ig.data[COFFEE_IMAGE[:, :, -1] > 200] = np.array([0.0, 0.0, 1.0, 0.6]).astype( + np.float32 + ) + + rgba[COFFEE_IMAGE[:, :, -1] > 200] = np.array([0.0, 0.0, 1.0, 0.6]).astype( + np.float32 + ) + + # check that fancy indexing works + npt.assert_almost_equal(ig.data.value, rgba) diff --git a/tests/test_positions_data_buffer_manager.py b/tests/test_positions_data_buffer_manager.py new file mode 100644 index 000000000..de9d179d8 --- /dev/null +++ b/tests/test_positions_data_buffer_manager.py @@ -0,0 +1,208 @@ +import numpy as np +from numpy import testing as npt +import pytest + +import fastplotlib as fpl +from fastplotlib.graphics._features import VertexPositions, FeatureEvent +from .utils import ( + generate_slice_indices, + assert_pending_uploads, + generate_positions_spiral_data, +) + + +EVENT_RETURN_VALUE: FeatureEvent = None + + +def event_handler(ev): + global EVENT_RETURN_VALUE + EVENT_RETURN_VALUE = ev + + +@pytest.mark.parametrize( + "data", [generate_positions_spiral_data(v) for v in ["y", "xy", "xyz"]] +) +def test_create_buffer(data): + points_data = VertexPositions(data) + + if data.ndim == 1: + # only y-vals specified + npt.assert_almost_equal(points_data[:, 1], generate_positions_spiral_data("y")) + # x-vals are auto generated just using arange + npt.assert_almost_equal(points_data[:, 0], np.arange(data.size)) + + elif data.shape[1] == 2: + # test 2D + npt.assert_almost_equal( + points_data[:, :-1], generate_positions_spiral_data("xy") + ) + npt.assert_almost_equal(points_data[:, -1], 0.0) + + elif data.shape[1] == 3: + # test 3D spiral + npt.assert_almost_equal(points_data[:], generate_positions_spiral_data("xyz")) + + +@pytest.mark.parametrize("test_graphic", [False, "line", "scatter"]) +def test_int(test_graphic): + # test setting single points + + data = generate_positions_spiral_data("xyz") + if test_graphic: + fig = fpl.Figure() + + if test_graphic == "line": + graphic = fig[0, 0].add_line(data=data) + + elif test_graphic == "scatter": + graphic = fig[0, 0].add_scatter(data=data) + + points = graphic.data + global EVENT_RETURN_VALUE + graphic.add_event_handler(event_handler, "data") + else: + points = VertexPositions(data) + + # set all x, y, z points, create a kink in the spiral + points[2] = 1.0 + npt.assert_almost_equal(points[2], 1.0) + # make sure other points are not affected + indices = list(range(10)) + indices.pop(2) + npt.assert_almost_equal(points[indices], data[indices]) + + # check event + if test_graphic: + assert isinstance(EVENT_RETURN_VALUE, FeatureEvent) + assert EVENT_RETURN_VALUE.graphic == graphic + assert EVENT_RETURN_VALUE.target is graphic.world_object + assert EVENT_RETURN_VALUE.info["key"] == 2 + npt.assert_almost_equal(EVENT_RETURN_VALUE.info["value"], 1.0) + + # reset + if test_graphic: + graphic.data = data + else: + points[:] = data + npt.assert_almost_equal(points[:], data) + + # check event + if test_graphic: + assert isinstance(EVENT_RETURN_VALUE, FeatureEvent) + assert EVENT_RETURN_VALUE.graphic == graphic + assert EVENT_RETURN_VALUE.target is graphic.world_object + assert EVENT_RETURN_VALUE.info["key"] == slice(None) + npt.assert_almost_equal(EVENT_RETURN_VALUE.info["value"], data) + + # just set y value + points[3, 1] = 1.0 + npt.assert_almost_equal(points[3, 1], 1.0) + # make sure others not modified + npt.assert_almost_equal(points[3, 0], data[3, 0]) + npt.assert_almost_equal(points[3, 2], data[3, 2]) + indices = list(range(10)) + indices.pop(3) + npt.assert_almost_equal(points[indices], data[indices]) + + +@pytest.mark.parametrize("test_graphic", [False, "line", "scatter"]) +@pytest.mark.parametrize( + "slice_method", [generate_slice_indices(i) for i in range(1, 16)] +) # int tested separately +@pytest.mark.parametrize("test_axis", ["y", "xy", "xyz"]) +def test_slice(test_graphic, slice_method: dict, test_axis: str): + data = generate_positions_spiral_data("xyz") + + if test_graphic: + fig = fpl.Figure() + + if test_graphic == "line": + graphic = fig[0, 0].add_line(data=data) + + elif test_graphic == "scatter": + graphic = fig[0, 0].add_scatter(data=data) + + points = graphic.data + global EVENT_RETURN_VALUE + graphic.add_event_handler(event_handler, "data") + else: + points = VertexPositions(data) + + s = slice_method["slice"] + indices = slice_method["indices"] + offset = slice_method["offset"] + size = slice_method["size"] + others = slice_method["others"] + + # TODO: placeholder until I make a testing figure where we draw frames only on call + points.buffer._gfx_pending_uploads.clear() + + match test_axis: + case "y": + points[s, 1] = -data[s, 1] + npt.assert_almost_equal(points[s, 1], -data[s, 1]) + npt.assert_almost_equal(points[indices, 1], -data[indices, 1]) + # make sure other points are not modified + npt.assert_almost_equal( + points[others, 1], data[others, 1] + ) # other points in same dimension + npt.assert_almost_equal( + points[:, 2:], data[:, 2:] + ) # dimensions that are not sliced + + # check event + if test_graphic: + assert isinstance(EVENT_RETURN_VALUE, FeatureEvent) + assert EVENT_RETURN_VALUE.graphic == graphic + assert EVENT_RETURN_VALUE.target is graphic.world_object + if isinstance(s, slice): + assert EVENT_RETURN_VALUE.info["key"] == (s, 1) + else: + npt.assert_almost_equal(EVENT_RETURN_VALUE.info["key"][0], s) + assert EVENT_RETURN_VALUE.info["key"][1] == 1 + npt.assert_almost_equal(EVENT_RETURN_VALUE.info["value"], -data[s, 1]) + + case "xy": + points[s, :-1] = -data[s, :-1] + npt.assert_almost_equal(points[s, :-1], -data[s, :-1]) + npt.assert_almost_equal(points[indices, :-1], -data[s, :-1]) + # make sure other points are not modified + npt.assert_almost_equal( + points[others, :-1], data[others, :-1] + ) # other points in the same dimensions + npt.assert_almost_equal( + points[:, -1], data[:, -1] + ) # dimensions that are not touched + + # check event + if test_graphic: + assert isinstance(EVENT_RETURN_VALUE, FeatureEvent) + assert EVENT_RETURN_VALUE.graphic == graphic + assert EVENT_RETURN_VALUE.target is graphic.world_object + if isinstance(s, slice): + assert EVENT_RETURN_VALUE.info["key"] == (s, slice(None, -1, None)) + else: + npt.assert_almost_equal(EVENT_RETURN_VALUE.info["key"][0], s) + assert EVENT_RETURN_VALUE.info["key"][1] == slice(None, -1, None) + npt.assert_almost_equal(EVENT_RETURN_VALUE.info["value"], -data[s, :-1]) + + case "xyz": + points[s] = -data[s] + npt.assert_almost_equal(points[s], -data[s]) + npt.assert_almost_equal(points[indices], -data[s]) + # make sure other points are not modified + npt.assert_almost_equal(points[others], data[others]) + + # check event + if test_graphic: + assert isinstance(EVENT_RETURN_VALUE, FeatureEvent) + assert EVENT_RETURN_VALUE.graphic == graphic + assert EVENT_RETURN_VALUE.target is graphic.world_object + if isinstance(s, slice): + assert EVENT_RETURN_VALUE.info["key"] == s + else: + npt.assert_almost_equal(EVENT_RETURN_VALUE.info["key"], s) + npt.assert_almost_equal(EVENT_RETURN_VALUE.info["value"], -data[s]) + + # make sure correct offset and size marked for pending upload + assert_pending_uploads(points.buffer, offset, size) diff --git a/tests/test_positions_graphics.py b/tests/test_positions_graphics.py new file mode 100644 index 000000000..d9c3a4871 --- /dev/null +++ b/tests/test_positions_graphics.py @@ -0,0 +1,446 @@ +import numpy as np +from numpy import testing as npt +import pytest + +import pygfx + +import fastplotlib as fpl +from fastplotlib.graphics._features import ( + VertexPositions, + VertexColors, + VertexCmap, + UniformColor, + UniformSize, + PointsSizesFeature, + Thickness, + FeatureEvent, +) + +from .utils import ( + generate_positions_spiral_data, + generate_color_inputs, + MULTI_COLORS_TRUTH, + generate_slice_indices, + assert_pending_uploads, +) + + +TRUTH_CMAPS = { + "jet": np.array( + [ + [0.0, 0.0, 0.5, 1.0], + [0.0, 0.0, 0.99910873, 1.0], + [0.0, 0.37843138, 1.0, 1.0], + [0.0, 0.8333333, 1.0, 1.0], + [0.30044276, 1.0, 0.66729915, 1.0], + [0.65464896, 1.0, 0.31309298, 1.0], + [1.0, 0.90123457, 0.0, 1.0], + [1.0, 0.4945534, 0.0, 1.0], + [1.0, 0.08787218, 0.0, 1.0], + [0.5, 0.0, 0.0, 1.0], + ], + dtype=np.float32, + ), + "viridis": np.array( + [ + [0.267004, 0.004874, 0.329415, 1.0], + [0.281412, 0.155834, 0.469201, 1.0], + [0.244972, 0.287675, 0.53726, 1.0], + [0.190631, 0.407061, 0.556089, 1.0], + [0.147607, 0.511733, 0.557049, 1.0], + [0.119483, 0.614817, 0.537692, 1.0], + [0.20803, 0.718701, 0.472873, 1.0], + [0.421908, 0.805774, 0.35191, 1.0], + [0.699415, 0.867117, 0.175971, 1.0], + [0.993248, 0.906157, 0.143936, 1.0], + ], + dtype=np.float32, + ), +} + + +EVENT_RETURN_VALUE: FeatureEvent = None + + +def event_handler(ev): + global EVENT_RETURN_VALUE + EVENT_RETURN_VALUE = ev + + +def test_sizes_slice(): + pass + + +@pytest.mark.parametrize("graphic_type", ["line", "scatter"]) +@pytest.mark.parametrize("colors", [None, *generate_color_inputs("b")]) +@pytest.mark.parametrize("uniform_color", [True, False]) +@pytest.mark.parametrize("alpha", [1.0, 0.5, 0.0]) +def test_uniform_color(graphic_type, colors, uniform_color, alpha): + fig = fpl.Figure() + + kwargs = dict() + for kwarg in ["colors", "uniform_color", "alpha"]: + if locals()[kwarg] is not None: + # add to dict of arguments that will be passed + kwargs[kwarg] = locals()[kwarg] + + data = generate_positions_spiral_data("xy") + + if graphic_type == "line": + graphic = fig[0, 0].add_line(data=data, **kwargs) + elif graphic_type == "scatter": + graphic = fig[0, 0].add_scatter(data=data, **kwargs) + + if uniform_color: + assert isinstance(graphic._colors, UniformColor) + assert isinstance(graphic.colors, pygfx.Color) + if colors is None: + # default white + assert graphic.colors == pygfx.Color([1, 1, 1, alpha]) + else: + # should be blue + assert graphic.colors == pygfx.Color([0, 0, 1, alpha]) + + # check pygfx material + npt.assert_almost_equal( + graphic.world_object.material.color, np.asarray(graphic.colors) + ) + else: + assert isinstance(graphic._colors, VertexColors) + assert isinstance(graphic.colors, VertexColors) + if colors is None: + # default white + npt.assert_almost_equal( + graphic.colors.value, + np.repeat([[1, 1, 1, alpha]], repeats=len(graphic.data), axis=0), + ) + else: + # blue + npt.assert_almost_equal( + graphic.colors.value, + np.repeat([[0, 0, 1, alpha]], repeats=len(graphic.data), axis=0), + ) + + # check geometry + npt.assert_almost_equal( + graphic.world_object.geometry.colors.data, graphic.colors.value + ) + + +@pytest.mark.parametrize("graphic_type", ["line", "scatter"]) +@pytest.mark.parametrize( + "data", [generate_positions_spiral_data(v) for v in ["y", "xy", "xyz"]] +) +def test_positions_graphics_data( + graphic_type, + data, +): + # tests with different ways of passing positions data, x, xy and xyz + fig = fpl.Figure() + + if graphic_type == "line": + graphic = fig[0, 0].add_line(data=data) + + elif graphic_type == "scatter": + graphic = fig[0, 0].add_scatter(data=data) + + assert isinstance(graphic._data, VertexPositions) + assert isinstance(graphic.data, VertexPositions) + + # n_datapoints must match + assert len(graphic.data.value) == len(data) + + # make sure data is correct + match data.shape[-1]: + case 1: # only y-vals given + npt.assert_almost_equal(graphic.data[:, 1], data) # y vals must match + npt.assert_almost_equal( + graphic.data[:, 0], np.arange(data.size) + ) # VertexData makes x-vals with arange + npt.assert_almost_equal(graphic.data[:, -1], 0) # z-vals must be zeros + case 2: # xy vals given + npt.assert_almost_equal(graphic.data[:, :-1], data) # x and y must match + npt.assert_almost_equal(graphic.data[:, -1], 0) # z-vals must be zero + case 3: # xyz vals given + npt.assert_almost_equal(graphic.data[:], data[:]) # everything must match + + +@pytest.mark.parametrize("graphic_type", ["line", "scatter"]) +@pytest.mark.parametrize("colors", [None, *generate_color_inputs("r")]) +@pytest.mark.parametrize("uniform_color", [None, False]) +@pytest.mark.parametrize("alpha", [None, 0.5, 0.0]) +def test_positions_graphic_vertex_colors( + graphic_type, + colors, + uniform_color, + alpha, +): + # test different ways of passing vertex colors + fig = fpl.Figure() + + kwargs = dict() + for kwarg in ["colors", "uniform_color", "alpha"]: + if locals()[kwarg] is not None: + # add to dict of arguments that will be passed + kwargs[kwarg] = locals()[kwarg] + + data = generate_positions_spiral_data("xy") + + if graphic_type == "line": + graphic = fig[0, 0].add_line(data=data, **kwargs) + elif graphic_type == "scatter": + graphic = fig[0, 0].add_scatter(data=data, **kwargs) + + if alpha is None: # default arg + alpha = 1 + + # color per vertex + # uniform colors is default False, or set to False + assert isinstance(graphic._colors, VertexColors) + assert isinstance(graphic.colors, VertexColors) + assert len(graphic.colors) == len(graphic.data) + + if colors is None: + # default + npt.assert_almost_equal( + graphic.colors.value, + np.repeat([[1, 1, 1, alpha]], repeats=len(graphic.data), axis=0), + ) + else: + if len(colors) != len(graphic.data): + # should be single red, regardless of input variant (i.e. str, array, RGBA tuple, etc. + npt.assert_almost_equal( + graphic.colors.value, + np.repeat([[1, 0, 0, alpha]], repeats=len(graphic.data), axis=0), + ) + else: + # multi colors + # use the truth for multi colors test that is pre-set + npt.assert_almost_equal(graphic.colors.value, MULTI_COLORS_TRUTH) + + +@pytest.mark.parametrize("graphic_type", ["line", "scatter"]) +@pytest.mark.parametrize("colors", [None, *generate_color_inputs("r")]) +@pytest.mark.parametrize("uniform_color", [None, False]) +@pytest.mark.parametrize("cmap", ["jet"]) +@pytest.mark.parametrize( + "cmap_transform", [None, [3, 5, 2, 1, 0, 6, 9, 7, 4, 8], np.arange(9, -1, -1)] +) +@pytest.mark.parametrize("alpha", [None, 0.5, 0.0]) +def test_cmap( + graphic_type, + colors, + uniform_color, + cmap, + cmap_transform, + alpha, +): + # test different ways of passing cmap args + fig = fpl.Figure() + + kwargs = dict() + for kwarg in ["cmap", "cmap_transform", "colors", "uniform_color", "alpha"]: + if locals()[kwarg] is not None: + # add to dict of arguments that will be passed + kwargs[kwarg] = locals()[kwarg] + + data = generate_positions_spiral_data("xy") + + if graphic_type == "line": + graphic = fig[0, 0].add_line(data=data, **kwargs) + elif graphic_type == "scatter": + graphic = fig[0, 0].add_scatter(data=data, **kwargs) + + if alpha is None: + alpha = 1.0 + + truth = TRUTH_CMAPS[cmap].copy() + truth[:, -1] = alpha + + # permute if transform is provided + if cmap_transform is not None: + truth = truth[cmap_transform] + npt.assert_almost_equal(graphic.cmap.transform, cmap_transform) + + assert isinstance(graphic._cmap, VertexCmap) + + assert graphic.cmap.name == cmap + + # make sure buffer is identical + # cmap overrides colors argument + assert graphic.colors.buffer is graphic.cmap.buffer + + npt.assert_almost_equal(graphic.cmap.value, truth) + npt.assert_almost_equal(graphic.colors.value, truth) + + # test changing cmap but not transform + graphic.cmap = "viridis" + truth = TRUTH_CMAPS["viridis"].copy() + truth[:, -1] = alpha + + if cmap_transform is not None: + truth = truth[cmap_transform] + + assert graphic.cmap.name == "viridis" + npt.assert_almost_equal(graphic.cmap.value, truth) + npt.assert_almost_equal(graphic.colors.value, truth) + + # test changing transform + cmap_transform = np.random.rand(10) + + # cmap transform is internally normalized between 0 - 1 + cmap_transform_norm = cmap_transform.copy() + cmap_transform_norm -= cmap_transform.min() + cmap_transform_norm /= cmap_transform_norm.max() + cmap_transform_norm *= 255 + + truth = fpl.utils.get_cmap("viridis", alpha=alpha) + truth = np.vstack([truth[val] for val in cmap_transform_norm.astype(int)]) + + graphic.cmap.transform = cmap_transform + npt.assert_almost_equal(graphic.cmap.transform, cmap_transform) + + npt.assert_almost_equal(graphic.cmap.value, truth) + npt.assert_almost_equal(graphic.colors.value, truth) + + +@pytest.mark.parametrize("graphic_type", ["line", "scatter"]) +@pytest.mark.parametrize("cmap", ["jet"]) +@pytest.mark.parametrize( + "colors", [None, *generate_color_inputs("multi")] +) # cmap arg overrides colors +@pytest.mark.parametrize( + "uniform_color", [True] # none of these will work with a uniform buffer +) +def test_incompatible_cmap_color_args(graphic_type, cmap, colors, uniform_color): + # test incompatible cmap args + fig = fpl.Figure() + + kwargs = dict() + for kwarg in ["cmap", "colors", "uniform_color"]: + if locals()[kwarg] is not None: + # add to dict of arguments that will be passed + kwargs[kwarg] = locals()[kwarg] + + data = generate_positions_spiral_data("xy") + + if graphic_type == "line": + with pytest.raises(TypeError): + graphic = fig[0, 0].add_line(data=data, **kwargs) + elif graphic_type == "scatter": + with pytest.raises(TypeError): + graphic = fig[0, 0].add_scatter(data=data, **kwargs) + + +@pytest.mark.parametrize("graphic_type", ["line", "scatter"]) +@pytest.mark.parametrize("colors", [*generate_color_inputs("multi")]) +@pytest.mark.parametrize( + "uniform_color", [True] # none of these will work with a uniform buffer +) +def test_incompatible_color_args(graphic_type, colors, uniform_color): + # test incompatible color args + fig = fpl.Figure() + + kwargs = dict() + for kwarg in ["colors", "uniform_color"]: + if locals()[kwarg] is not None: + # add to dict of arguments that will be passed + kwargs[kwarg] = locals()[kwarg] + + data = generate_positions_spiral_data("xy") + + if graphic_type == "line": + with pytest.raises(TypeError): + graphic = fig[0, 0].add_line(data=data, **kwargs) + elif graphic_type == "scatter": + with pytest.raises(TypeError): + graphic = fig[0, 0].add_scatter(data=data, **kwargs) + + +@pytest.mark.parametrize("sizes", [None, 5.0, np.linspace(3, 8, 10, dtype=np.float32)]) +@pytest.mark.parametrize("uniform_size", [None, False]) +def test_sizes(sizes, uniform_size): + # test scatter sizes + fig = fpl.Figure() + + kwargs = dict() + for kwarg in ["sizes"]: + if locals()[kwarg] is not None: + # add to dict of arguments that will be passed + kwargs[kwarg] = locals()[kwarg] + + data = generate_positions_spiral_data("xy") + + graphic = fig[0, 0].add_scatter(data=data, **kwargs) + + assert isinstance(graphic.sizes, PointsSizesFeature) + assert isinstance(graphic._sizes, PointsSizesFeature) + assert len(data) == len(graphic.sizes) + + if sizes is None: + sizes = 1 # default sizes + + npt.assert_almost_equal(graphic.sizes.value, sizes) + npt.assert_almost_equal( + graphic.world_object.geometry.sizes.data, graphic.sizes.value + ) + + +@pytest.mark.parametrize("sizes", [None, 5.0]) +@pytest.mark.parametrize("uniform_size", [True]) +def test_uniform_size(sizes, uniform_size): + fig = fpl.Figure() + + kwargs = dict() + for kwarg in ["sizes", "uniform_size"]: + if locals()[kwarg] is not None: + # add to dict of arguments that will be passed + kwargs[kwarg] = locals()[kwarg] + + data = generate_positions_spiral_data("xy") + + graphic = fig[0, 0].add_scatter(data=data, **kwargs) + + assert isinstance(graphic.sizes, (float, int)) + assert isinstance(graphic._sizes, UniformSize) + + if sizes is None: + sizes = 1 # default sizes + + npt.assert_almost_equal(graphic.sizes, sizes) + npt.assert_almost_equal(graphic.world_object.material.size, sizes) + + # test changing size + graphic.sizes = 10.0 + assert isinstance(graphic.sizes, float) + assert isinstance(graphic._sizes, UniformSize) + assert graphic.sizes == 10.0 + + +@pytest.mark.parametrize("thickness", [None, 0.5, 5.0]) +def test_thickness(thickness): + fig = fpl.Figure() + + kwargs = dict() + for kwarg in ["thickness"]: + if locals()[kwarg] is not None: + # add to dict of arguments that will be passed + kwargs[kwarg] = locals()[kwarg] + + data = generate_positions_spiral_data("xy") + + graphic = fig[0, 0].add_line(data=data, **kwargs) + + if thickness is None: + thickness = 2.0 # default thickness + + assert isinstance(graphic._thickness, Thickness) + + assert graphic.thickness == thickness + assert graphic.world_object.material.thickness == thickness + + if thickness == 0.5: + assert isinstance(graphic.world_object.material, pygfx.LineThinMaterial) + + else: + assert isinstance(graphic.world_object.material, pygfx.LineMaterial) diff --git a/tests/test_sizes_buffer_manager.py b/tests/test_sizes_buffer_manager.py new file mode 100644 index 000000000..0b34f9588 --- /dev/null +++ b/tests/test_sizes_buffer_manager.py @@ -0,0 +1,76 @@ +import numpy as np +from numpy import testing as npt +import pytest + +from fastplotlib.graphics._features import PointsSizesFeature +from .utils import generate_slice_indices, assert_pending_uploads + + +def generate_data(input_type: str) -> np.ndarray | float: + """ + Point sizes varying with a sine wave + + Parameters + ---------- + input_type: str + one of "sine", "cosine", or "float" + """ + if input_type == "float": + return 10.0 + xs = np.linspace(0, 10 * np.pi, 10) + + if input_type == "sine": + return np.abs(np.sin(xs)).astype(np.float32) + + if input_type == "cosine": + return np.abs(np.cos(xs)).astype(np.float32) + + +@pytest.mark.parametrize("data", [generate_data(v) for v in ["float", "sine"]]) +def test_create_buffer(data): + sizes = PointsSizesFeature(data, n_datapoints=10) + + if isinstance(data, float): + npt.assert_almost_equal(sizes[:], generate_data("float")) + + elif isinstance(data, np.ndarray): + npt.assert_almost_equal(sizes[:], generate_data("sine")) + + +@pytest.mark.parametrize( + "slice_method", [generate_slice_indices(i) for i in range(0, 16)] +) +@pytest.mark.parametrize("user_input", ["float", "cosine"]) +def test_slice(slice_method: dict, user_input: str): + data = generate_data("sine") + + s = slice_method["slice"] + indices = slice_method["indices"] + offset = slice_method["offset"] + size = slice_method["size"] + others = slice_method["others"] + + sizes = PointsSizesFeature(data, n_datapoints=10) + + # TODO: placeholder until I make a testing figure where we draw frames only on call + sizes.buffer._gfx_pending_uploads.clear() + + match user_input: + case "float": + sizes[s] = 20.0 + truth = np.full(len(indices), 20.0) + npt.assert_almost_equal(sizes[s], truth) + npt.assert_almost_equal(sizes[indices], truth) + # make sure other sizes not modified + npt.assert_almost_equal(sizes[others], data[others]) + + case "cosine": + cosine = generate_data("cosine") + sizes[s] = cosine[s] + npt.assert_almost_equal(sizes[s], cosine[s]) + npt.assert_almost_equal(sizes[indices], cosine[s]) + # make sure other sizes not modified + npt.assert_almost_equal(sizes[others], data[others]) + + # make sure correct offset and size marked for pending upload + assert_pending_uploads(sizes.buffer, offset, size) diff --git a/tests/test_text_graphic.py b/tests/test_text_graphic.py new file mode 100644 index 000000000..a13dfe690 --- /dev/null +++ b/tests/test_text_graphic.py @@ -0,0 +1,101 @@ +from numpy import testing as npt + +import fastplotlib as fpl +from fastplotlib.graphics._features import ( + FeatureEvent, + TextData, + FontSize, + TextFaceColor, + TextOutlineColor, + TextOutlineThickness, +) + +import pygfx + + +def test_create_graphic(): + fig = fpl.Figure() + data = "lorem ipsum" + text = fig[0, 0].add_text(data) + + assert isinstance(text, fpl.TextGraphic) + + assert isinstance(text._text, TextData) + assert text.text == data + + assert text.font_size == 14 + assert isinstance(text._font_size, FontSize) + assert text.world_object.geometry.font_size == 14 + + assert text.face_color == pygfx.Color("w") + assert isinstance(text._face_color, TextFaceColor) + assert text.world_object.material.color == pygfx.Color("w") + + assert text.outline_color == pygfx.Color("w") + assert isinstance(text._outline_color, TextOutlineColor) + assert text.world_object.material.outline_color == pygfx.Color("w") + + assert text.outline_thickness == 0 + assert isinstance(text._outline_thickness, TextOutlineThickness) + assert text.world_object.material.outline_thickness == 0 + + +EVENT_RETURN_VALUE: FeatureEvent = None + + +def event_handler(ev): + global EVENT_RETURN_VALUE + EVENT_RETURN_VALUE = ev + + +def check_event(graphic, feature, value): + global EVENT_RETURN_VALUE + assert isinstance(EVENT_RETURN_VALUE, FeatureEvent) + assert EVENT_RETURN_VALUE.type == feature + assert EVENT_RETURN_VALUE.graphic == graphic + assert EVENT_RETURN_VALUE.target == graphic.world_object + if isinstance(EVENT_RETURN_VALUE.info["value"], float): + # floating point error + npt.assert_almost_equal(EVENT_RETURN_VALUE.info["value"], value) + else: + assert EVENT_RETURN_VALUE.info["value"] == value + + +def test_text_changes_events(): + fig = fpl.Figure() + data = "lorem ipsum" + text = fig[0, 0].add_text(data) + + text.add_event_handler( + event_handler, + "text", + "font_size", + "face_color", + "outline_color", + "outline_thickness", + ) + + text.text = "bah" + assert text.text == "bah" + # TODO: seems like there isn't a way in pygfx to get the current text as a str? + check_event(graphic=text, feature="text", value="bah") + + text.font_size = 10.0 + assert text.font_size == 10.0 + assert text.world_object.geometry.font_size == 10 + check_event(text, "font_size", 10) + + text.face_color = "r" + assert text.face_color == pygfx.Color("r") + assert text.world_object.material.color == pygfx.Color("r") + check_event(text, "face_color", pygfx.Color("r")) + + text.outline_color = "b" + assert text.outline_color == pygfx.Color("b") + assert text.world_object.material.outline_color == pygfx.Color("b") + check_event(text, "outline_color", pygfx.Color("b")) + + text.outline_thickness = 0.3 + npt.assert_almost_equal(text.outline_thickness, 0.3) + npt.assert_almost_equal(text.world_object.material.outline_thickness, 0.3) + check_event(text, "outline_thickness", 0.3) diff --git a/tests/test_texture_array.py b/tests/test_texture_array.py new file mode 100644 index 000000000..5aecf49a5 --- /dev/null +++ b/tests/test_texture_array.py @@ -0,0 +1,230 @@ +import numpy as np +from numpy import testing as npt +import pytest + +import pygfx + +import fastplotlib as fpl +from fastplotlib.graphics._features import TextureArray, WGPU_MAX_TEXTURE_SIZE +from fastplotlib.graphics.image import _ImageTile + + +def make_data(n_rows: int, n_cols: int) -> np.ndarray: + """ + Makes a 2D array where the amplitude of the sine wave + is increasing along the y-direction (along rows), and + the wavelength is increasing along the x-axis (columns) + """ + xs = np.linspace(0, 1_000, n_cols) + + sine = np.sin(np.sqrt(xs)) + + return np.vstack([sine * i for i in range(n_rows)]).astype(np.float32) + + +def check_texture_array( + data: np.ndarray, + ta: TextureArray, + buffer_size: int, + buffer_shape: tuple[int, int], + row_indices_size: int, + col_indices_size: int, + row_indices_values: np.ndarray, + col_indices_values: np.ndarray, +): + + npt.assert_almost_equal(ta.value, data) + + assert ta.buffer.size == buffer_size + assert ta.buffer.shape == buffer_shape + + assert all([isinstance(texture, pygfx.Texture) for texture in ta.buffer.ravel()]) + + assert ta.row_indices.size == row_indices_size + assert ta.col_indices.size == col_indices_size + npt.assert_array_equal(ta.row_indices, row_indices_values) + npt.assert_array_equal(ta.col_indices, col_indices_values) + + # make sure chunking is correct + for texture, chunk_index, data_slice in ta: + assert ta.buffer[chunk_index] is texture + chunk_row, chunk_col = chunk_index + + data_row_start_index = chunk_row * WGPU_MAX_TEXTURE_SIZE + data_col_start_index = chunk_col * WGPU_MAX_TEXTURE_SIZE + + data_row_stop_index = min( + data.shape[0] - 1, data_row_start_index + WGPU_MAX_TEXTURE_SIZE + ) + data_col_stop_index = min( + data.shape[1] - 1, data_col_start_index + WGPU_MAX_TEXTURE_SIZE + ) + + row_slice = slice(data_row_start_index, data_row_stop_index) + col_slice = slice(data_col_start_index, data_col_stop_index) + + assert data_slice == (row_slice, col_slice) + + +def check_set_slice(data, ta, row_slice, col_slice): + ta[row_slice, col_slice] = 1 + npt.assert_almost_equal(ta[row_slice, col_slice], 1) + + # make sure other vals unchanged + npt.assert_almost_equal(ta[: row_slice.start], data[: row_slice.start]) + npt.assert_almost_equal(ta[row_slice.stop :], data[row_slice.stop :]) + npt.assert_almost_equal(ta[:, : col_slice.start], data[:, : col_slice.start]) + npt.assert_almost_equal(ta[:, col_slice.stop :], data[:, col_slice.stop :]) + + +def make_image_graphic(data) -> fpl.ImageGraphic: + fig = fpl.Figure() + return fig[0, 0].add_image(data) + + +def check_image_graphic(texture_array, graphic): + # make sure each ImageTile has the right texture + for (texture, chunk_index, data_slice), img in zip( + texture_array, graphic.world_object.children + ): + assert isinstance(img, _ImageTile) + assert img.geometry.grid is texture + assert img.world.x == data_slice[1].start + assert img.world.y == data_slice[0].start + + +@pytest.mark.parametrize("test_graphic", [False, True]) +def test_small_texture(test_graphic): + # tests TextureArray with dims that requires only 1 texture + data = make_data(1_000, 1_000) + + if test_graphic: + graphic = make_image_graphic(data) + ta = graphic.data + else: + ta = TextureArray(data) + + check_texture_array( + data=data, + ta=ta, + buffer_size=1, + buffer_shape=(1, 1), + row_indices_size=1, + col_indices_size=1, + row_indices_values=np.array([0]), + col_indices_values=np.array([0]), + ) + + if test_graphic: + check_image_graphic(ta, graphic) + + check_set_slice(data, ta, slice(50, 200), slice(600, 800)) + + +@pytest.mark.parametrize("test_graphic", [False, True]) +def test_texture_at_limit(test_graphic): + # tests TextureArray with data that is 8192 x 8192 + data = make_data(WGPU_MAX_TEXTURE_SIZE, WGPU_MAX_TEXTURE_SIZE) + + if test_graphic: + graphic = make_image_graphic(data) + ta = graphic.data + else: + ta = TextureArray(data) + + check_texture_array( + data, + ta=ta, + buffer_size=1, + buffer_shape=(1, 1), + row_indices_size=1, + col_indices_size=1, + row_indices_values=np.array([0]), + col_indices_values=np.array([0]), + ) + + if test_graphic: + check_image_graphic(ta, graphic) + + check_set_slice(data, ta, slice(5000, 8000), slice(2000, 3000)) + + +@pytest.mark.parametrize("test_graphic", [False, True]) +def test_wide(test_graphic): + data = make_data(10_000, 20_000) + + if test_graphic: + graphic = make_image_graphic(data) + ta = graphic.data + else: + ta = TextureArray(data) + + check_texture_array( + data, + ta=ta, + buffer_size=6, + buffer_shape=(2, 3), + row_indices_size=2, + col_indices_size=3, + row_indices_values=np.array([0, 8192]), + col_indices_values=np.array([0, 8192, 16384]), + ) + + if test_graphic: + check_image_graphic(ta, graphic) + + check_set_slice(data, ta, slice(6_000, 9_000), slice(12_000, 18_000)) + + +@pytest.mark.parametrize("test_graphic", [False, True]) +def test_tall(test_graphic): + data = make_data(20_000, 10_000) + + if test_graphic: + graphic = make_image_graphic(data) + ta = graphic.data + else: + ta = TextureArray(data) + + check_texture_array( + data, + ta=ta, + buffer_size=6, + buffer_shape=(3, 2), + row_indices_size=3, + col_indices_size=2, + row_indices_values=np.array([0, 8192, 16384]), + col_indices_values=np.array([0, 8192]), + ) + + if test_graphic: + check_image_graphic(ta, graphic) + + check_set_slice(data, ta, slice(12_000, 18_000), slice(6_000, 9_000)) + + +@pytest.mark.parametrize("test_graphic", [False, True]) +def test_square(test_graphic): + data = make_data(20_000, 20_000) + + if test_graphic: + graphic = make_image_graphic(data) + ta = graphic.data + else: + ta = TextureArray(data) + + check_texture_array( + data, + ta=ta, + buffer_size=9, + buffer_shape=(3, 3), + row_indices_size=3, + col_indices_size=3, + row_indices_values=np.array([0, 8192, 16384]), + col_indices_values=np.array([0, 8192, 16384]), + ) + + if test_graphic: + check_image_graphic(ta, graphic) + + check_set_slice(data, ta, slice(12_000, 18_000), slice(16_000, 19_000)) diff --git a/tests/utils.py b/tests/utils.py new file mode 100644 index 000000000..6a25968e1 --- /dev/null +++ b/tests/utils.py @@ -0,0 +1,185 @@ +import numpy as np + +import pygfx + + +def generate_slice_indices(kind: int): + n_elements = 10 + a = np.arange(n_elements) + + match kind: + case 0: + # simplest, just int + s = 2 + indices = [2] + + case 1: + # everything, [:] + s = slice(None, None, None) + indices = list(range(10)) + + case 2: + # positive continuous range, [1:5] + s = slice(1, 5, None) + indices = [1, 2, 3, 4] + + case 3: + # positive stepped range, [2:8:2] + s = slice(2, 8, 2) + indices = [2, 4, 6] + + case 4: + # negative continuous range, [-5:] + s = slice(-5, None, None) + indices = [5, 6, 7, 8, 9] + + case 5: + # negative backwards, [-5::-1] + s = slice(-5, None, -1) + indices = [5, 4, 3, 2, 1, 0] + + case 5: + # negative backwards stepped, [-5::-2] + s = slice(-5, None, -2) + indices = [5, 3, 1] + + case 6: + # negative stepped forward[-5::2] + s = slice(-5, None, 2) + indices = [5, 7, 9] + + case 7: + # both negative, [-8:-2] + s = slice(-8, -2, None) + indices = [2, 3, 4, 5, 6, 7] + + case 8: + # both negative and stepped, [-8:2:2] + s = slice(-8, -2, 2) + indices = [2, 4, 6] + + case 9: + # positive, negative, negative, [8:-9:-2] + s = slice(8, -9, -2) + indices = [8, 6, 4, 2] + + case 10: + # only stepped forward, [::2] + s = slice(None, None, 2) + indices = [0, 2, 4, 6, 8] + + case 11: + # only stepped backward, [::-3] + s = slice(None, None, -3) + indices = [9, 6, 3, 0] + + case 12: + # list indices + s = [2, 5, 9] + indices = [2, 5, 9] + + case 13: + # bool indices + s = a > 5 + indices = [6, 7, 8, 9] + + case 14: + # list indices with negatives + s = [1, 4, -2] + indices = [1, 4, 8] + + case 15: + # array indices + s = np.array([1, 4, -7, 9]) + indices = [1, 4, 3, 9] + + others = [i for i in a if i not in indices] + + offset, size = (min(indices), np.ptp(indices) + 1) + + return { + "slice": s, + "indices": indices, + "others": others, + "offset": offset, + "size": size, + } + + +def assert_pending_uploads(buffer: pygfx.Buffer, offset: int, size: int): + upload_offset, upload_size = buffer._gfx_pending_uploads[-1] + # sometimes when slicing with step, it will over-estimate offset + # but it overestimates to upload 1 extra point so it's fine + assert (upload_offset == offset) or (upload_offset == offset - 1) + + # sometimes when slicing with step, it will over-estimate size + # but it overestimates to upload 1 extra point so it's fine + assert (upload_size == size) or (upload_size == size + 1) + + +def generate_positions_spiral_data(inputs: str) -> np.ndarray: + """ + Generates a spiral/spring + + Only 10 points so a very pointy spiral but easier to spot changes :D + """ + xs = np.linspace(0, 10 * np.pi, 10) + ys = np.sin(xs) + zs = np.cos(xs) + + match inputs: + case "y": + data = ys + + case "xy": + data = np.column_stack([xs, ys]) + + case "xyz": + data = np.column_stack([xs, ys, zs]) + + return data.astype(np.float32) + + +def generate_color_inputs( + name: str, +) -> list[str, np.ndarray, list, tuple] | list[str, np.ndarray]: + if name == "multi": + s = [ + "r", + "g", + "b", + "cyan", + "magenta", + "green", + "yellow", + "white", + "purple", + "orange", + ] + array = np.vstack([pygfx.Color(c) for c in s]) + return [s, array] + + color = pygfx.Color(name) + + s = name + a = np.array(color) + l = list(color) + t = tuple(color) + + return [s, a, l, t] + + +MULTI_COLORS_TRUTH = np.array( + [ + [1.0, 0.0, 0.0, 1.0], + [0.0, 1.0, 0.0, 1.0], + [0.0, 0.0, 1.0, 1.0], + [0.0, 1.0, 1.0, 1.0], + [1.0, 0.0, 1.0, 1.0], + [0.0, 0.501960813999176, 0.0, 1.0], + [1.0, 1.0, 0.0, 1.0], + [1.0, 1.0, 1.0, 1.0], + [0.501960813999176, 0.0, 0.501960813999176, 1.0], + [1.0, 0.6470588445663452, 0.0, 1.0], + ] +) From 02a851532c91c82fb6b5117d4287fd598b2f4d41 Mon Sep 17 00:00:00 2001 From: Kushal Kolar Date: Mon, 17 Jun 2024 21:35:23 -0400 Subject: [PATCH 57/66] numpy v2 and wgpu v0.16.0 (#524) --- docs/source/user_guide/gpu.rst | 6 ++- .../notebooks/linear_region_selector.ipynb | 40 ------------------- examples/tests/testutils.py | 2 +- fastplotlib/graphics/line_collection.py | 4 +- fastplotlib/legends/legend.py | 4 +- fastplotlib/utils/gui.py | 4 +- fastplotlib/widgets/histogram_lut.py | 2 +- setup.py | 2 +- 8 files changed, 13 insertions(+), 51 deletions(-) diff --git a/docs/source/user_guide/gpu.rst b/docs/source/user_guide/gpu.rst index 3f4ff4bb4..f4ef89a0c 100644 --- a/docs/source/user_guide/gpu.rst +++ b/docs/source/user_guide/gpu.rst @@ -59,7 +59,7 @@ You can get more detailed info on each adapter like this:: import pprint for a in fpl.enumerate_adapters(): - pprint.pprint(a.request_adapter_info()) + pprint.pprint(a.info) General description of the fields: * vendor: GPU manufacturer @@ -265,7 +265,9 @@ You can select an adapter by passing one of the ``wgpu.GPUAdapter`` instances re to ``fpl.select_adapter()``:: # get info or summary of all adapters to pick an adapter - print([a.request_adapter_info() for a in fpl.enumerate_adapters()]) + import pprint + for a in fpl.enumerate_adapters(): + pprint.pprint(a.info) # example, pick adapter at index 2 chosen_gpu = fpl.enumerate_adapters()[2] diff --git a/examples/notebooks/linear_region_selector.ipynb b/examples/notebooks/linear_region_selector.ipynb index 57a72bdec..74b304a35 100644 --- a/examples/notebooks/linear_region_selector.ipynb +++ b/examples/notebooks/linear_region_selector.ipynb @@ -93,46 +93,6 @@ "fig.show(maintain_aspect=False)" ] }, - { - "cell_type": "code", - "execution_count": null, - "id": "2f29e913-c4f8-44a6-8692-eb14436849a5", - "metadata": {}, - "outputs": [], - "source": [ - "sine_graphic_x.data[:, 1].ptp()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "1947a477-5dd2-4df9-aecd-6967c6ab45fe", - "metadata": {}, - "outputs": [], - "source": [ - "np.clip(-0.1, 0, 10)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "18e10277-6d5d-42fe-8715-1733efabefa0", - "metadata": {}, - "outputs": [], - "source": [ - "ls_y.selection" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "8e9c42b9-60d2-4544-96c5-c8c6832b79e3", - "metadata": {}, - "outputs": [], - "source": [ - "ls_y.get_selected_indices()" - ] - }, { "cell_type": "code", "execution_count": null, diff --git a/examples/tests/testutils.py b/examples/tests/testutils.py index f62ae7602..1a95e249f 100644 --- a/examples/tests/testutils.py +++ b/examples/tests/testutils.py @@ -30,7 +30,7 @@ def get_wgpu_backend(): """ Query the configured wgpu backend driver. """ - code = "import wgpu.utils; info = wgpu.utils.get_default_device().adapter.request_adapter_info(); print(info['adapter_type'], info['backend_type'])" + code = "import wgpu.utils; info = wgpu.utils.get_default_device().adapter.info; print(info['adapter_type'], info['backend_type'])" result = subprocess.run( [ sys.executable, diff --git a/fastplotlib/graphics/line_collection.py b/fastplotlib/graphics/line_collection.py index 92aad56b2..4a55c1fb3 100644 --- a/fastplotlib/graphics/line_collection.py +++ b/fastplotlib/graphics/line_collection.py @@ -460,7 +460,7 @@ def _get_linear_selector_init_args(self, axis, padding): bounds = (xmin, value_25p) limits = (xmin, xmax) # size from orthogonal axis - size = bbox[:, 1].ptp() * 1.5 + size = np.ptp(bbox[:, 1]) * 1.5 # center on orthogonal axis center = bbox[:, 1].mean() @@ -472,7 +472,7 @@ def _get_linear_selector_init_args(self, axis, padding): bounds = (xmin, value_25p) limits = (xmin, xmax) - size = bbox[:, 0].ptp() * 1.5 + size = np.ptp(bbox[:, 0]) * 1.5 # center on orthogonal axis center = bbox[:, 0].mean() diff --git a/fastplotlib/legends/legend.py b/fastplotlib/legends/legend.py index b7e55f321..8ab3ddedb 100644 --- a/fastplotlib/legends/legend.py +++ b/fastplotlib/legends/legend.py @@ -244,7 +244,7 @@ def add_graphic(self, graphic: Graphic, label: str = None): # get width of widest LegendItem in previous column to add to x_pos offset for this column for item in prev_column_items: bbox = item.world_object.get_world_bounding_box() - width, height, depth = bbox.ptp(axis=0) + width, height, depth = np.ptp(bbox, axis=0) max_width = max(max_width, width) # x position offset for this new column @@ -278,7 +278,7 @@ def add_graphic(self, graphic: Graphic, label: str = None): def _reset_mesh_dims(self): bbox = self._legend_items_group.get_world_bounding_box() - width, height, _ = bbox.ptp(axis=0) + width, height, _ = np.ptp(bbox, axis=0) self._mesh.geometry.positions.data[mesh_masks.x_right] = width + 7 self._mesh.geometry.positions.data[mesh_masks.x_left] = -5 diff --git a/fastplotlib/utils/gui.py b/fastplotlib/utils/gui.py index 1941674ee..1f13c1406 100644 --- a/fastplotlib/utils/gui.py +++ b/fastplotlib/utils/gui.py @@ -60,9 +60,9 @@ def _notebook_print_banner(): # print logo and adapter info adapters = [a for a in wgpu.gpu.enumerate_adapters()] - adapters_info = [a.request_adapter_info() for a in adapters] + adapters_info = [a.info for a in adapters] - default_adapter_info = wgpu.gpu.request_adapter().request_adapter_info() + default_adapter_info = wgpu.gpu.request_adapter().info default_ix = adapters_info.index(default_adapter_info) if len(adapters) > 0: diff --git a/fastplotlib/widgets/histogram_lut.py b/fastplotlib/widgets/histogram_lut.py index a3edffcbd..02c21aa38 100644 --- a/fastplotlib/widgets/histogram_lut.py +++ b/fastplotlib/widgets/histogram_lut.py @@ -163,7 +163,7 @@ def _calculate_histogram(self, data): # used if data ptp <= 10 because event things get weird # with tiny world objects due to floating point error # so if ptp <= 10, scale up by a factor - self._scale_factor: int = max(1, 100 * int(10 / data_ss.ptp())) + self._scale_factor: int = max(1, 100 * int(10 / np.ptp(data_ss))) edges = edges * self._scale_factor diff --git a/setup.py b/setup.py index 3ba77201d..d75edf956 100644 --- a/setup.py +++ b/setup.py @@ -4,7 +4,7 @@ install_requires = [ "numpy>=1.23.0", - "wgpu>=0.15.1", + "wgpu>=0.16.0", "pygfx>=0.1.14", ] From 852ffdc839df7a3b948dfaac9128a65840058928 Mon Sep 17 00:00:00 2001 From: Kushal Kolar Date: Tue, 18 Jun 2024 10:39:21 -0400 Subject: [PATCH 58/66] update example w.r.t. gfeatures refactor (#528) --- examples/notebooks/quickstart.ipynb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/notebooks/quickstart.ipynb b/examples/notebooks/quickstart.ipynb index 5c5040418..09317110d 100644 --- a/examples/notebooks/quickstart.ipynb +++ b/examples/notebooks/quickstart.ipynb @@ -825,7 +825,7 @@ " print(event_data)\n", "\n", "# Will print event data when the color changes\n", - "cosine_graphic.colors.add_event_handler(callback_func)" + "cosine_graphic.add_event_handler(callback_func, \"colors\")" ] }, { From ae9ccc297a61334051a33b320b7c79aaa8271784 Mon Sep 17 00:00:00 2001 From: Kushal Kolar Date: Tue, 18 Jun 2024 10:39:45 -0400 Subject: [PATCH 59/66] always cast to float32, try to convert other array types too (#527) --- fastplotlib/graphics/_features/_base.py | 32 ++++++------------------- 1 file changed, 7 insertions(+), 25 deletions(-) diff --git a/fastplotlib/graphics/_features/_base.py b/fastplotlib/graphics/_features/_base.py index 1b24d3b78..a57f8a453 100644 --- a/fastplotlib/graphics/_features/_base.py +++ b/fastplotlib/graphics/_features/_base.py @@ -12,36 +12,18 @@ WGPU_MAX_TEXTURE_SIZE = 8192 -supported_dtypes = [ - np.uint8, - np.uint16, - np.uint32, - np.int8, - np.int16, - np.int32, - np.float16, - np.float32, -] - - def to_gpu_supported_dtype(array): """ - If ``array`` is a numpy array, converts it to a supported type. GPUs don't support 64 bit dtypes. + convert input array to float32 numpy array """ if isinstance(array, np.ndarray): - if array.dtype not in supported_dtypes: - if np.issubdtype(array.dtype, np.integer): - warn(f"converting {array.dtype} array to int32") - return array.astype(np.int32) - elif np.issubdtype(array.dtype, np.floating): - 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" - ) + if not array.dtype == np.float32: + warn(f"casting {array.dtype} array to float32") + return array.astype(np.float32) + return array - return array + # try to make a numpy array from it, should not copy, tested with jax arrays + return np.asarray(array).astype(np.float32) class FeatureEvent(pygfx.Event): From a07636c0b334ef93f83820de313cf3b5392edd3f Mon Sep 17 00:00:00 2001 From: Kushal Kolar Date: Tue, 18 Jun 2024 13:55:51 -0400 Subject: [PATCH 60/66] Get nearest graphics (#519) * bugfix * center works, need to figure out what's wrong with edge * allow passing graphic collection to subset * allow passing graphic collection to subset * move get_nearest_graphics to utils * move stuff * test for get nearest * black * more black --- fastplotlib/graphics/line_collection.py | 2 +- fastplotlib/utils/__init__.py | 1 + fastplotlib/utils/_plot_helpers.py | 53 +++++++++++++++++++++++++ tests/test_plot_helpers.py | 33 +++++++++++++++ 4 files changed, 88 insertions(+), 1 deletion(-) create mode 100644 fastplotlib/utils/_plot_helpers.py create mode 100644 tests/test_plot_helpers.py diff --git a/fastplotlib/graphics/line_collection.py b/fastplotlib/graphics/line_collection.py index 4a55c1fb3..01faa9164 100644 --- a/fastplotlib/graphics/line_collection.py +++ b/fastplotlib/graphics/line_collection.py @@ -81,7 +81,7 @@ def cmap(self, args): if isinstance(args, str): name = args transform, alpha = None, 1.0 - if len(args) == 1: + elif len(args) == 1: name = args[0] transform, alpha = None, None diff --git a/fastplotlib/utils/__init__.py b/fastplotlib/utils/__init__.py index e1eef6447..3ae83fb6b 100644 --- a/fastplotlib/utils/__init__.py +++ b/fastplotlib/utils/__init__.py @@ -3,6 +3,7 @@ from .functions import * from .gpu import enumerate_adapters, select_adapter, print_wgpu_report +from ._plot_helpers import * @dataclass diff --git a/fastplotlib/utils/_plot_helpers.py b/fastplotlib/utils/_plot_helpers.py new file mode 100644 index 000000000..ac0ff2cda --- /dev/null +++ b/fastplotlib/utils/_plot_helpers.py @@ -0,0 +1,53 @@ +from typing import Sequence + +import numpy as np + +from ..graphics._base import Graphic +from ..graphics._collection_base import GraphicCollection + + +def get_nearest_graphics( + pos: tuple[float, float] | tuple[float, float, float], + graphics: Sequence[Graphic] | GraphicCollection, +) -> np.ndarray[Graphic]: + """ + Returns the nearest ``graphics`` to the passed position ``pos`` in world space. + Uses the distance between ``pos`` and the center of the bounding sphere for each graphic. + + Parameters + ---------- + pos: (x, y) | (x, y, z) + position in world space, z-axis is ignored when calculating L2 norms if ``pos`` is 2D + + graphics: Sequence, i.e. array, list, tuple, etc. of Graphic | GraphicCollection + the graphics from which to return a sorted array of graphics in order of closest + to furthest graphic + + Returns + ------- + tuple[Graphic] + nearest graphics to ``pos`` in order + + """ + + if isinstance(graphics, GraphicCollection): + graphics = graphics.graphics + + if not all(isinstance(g, Graphic) for g in graphics): + raise TypeError("all elements of `graphics` must be Graphic objects") + + pos = np.asarray(pos) + + if pos.shape != (2,) or not pos.shape != (3,): + raise TypeError + + # get centers + centers = np.empty(shape=(len(graphics), len(pos))) + for i in range(centers.shape[0]): + centers[i] = graphics[i].world_object.get_world_bounding_sphere()[: len(pos)] + + # l2 + distances = np.linalg.norm(centers[:, : len(pos)] - pos, ord=2, axis=1) + + sort_indices = np.argsort(distances) + return np.asarray(graphics)[sort_indices] diff --git a/tests/test_plot_helpers.py b/tests/test_plot_helpers.py new file mode 100644 index 000000000..b4abe55fc --- /dev/null +++ b/tests/test_plot_helpers.py @@ -0,0 +1,33 @@ +import numpy as np +import fastplotlib as fpl + + +def make_circle(center, radius: float, n_points: int = 75) -> np.ndarray: + theta = np.linspace(0, 2 * np.pi, n_points) + xs = radius * np.sin(theta) + ys = radius * np.cos(theta) + + return np.column_stack([xs, ys]) + center + + +def test_get_nearest_graphics(): + circles = list() + + centers = [[0, 0], [0, 20], [20, 0], [20, 20]] + + for center in centers: + circles.append(make_circle(center, 5, n_points=75)) + + fig = fpl.Figure() + + lines = fig[0, 0].add_line_collection(circles, cmap="jet", thickness=5) + + fig[0, 0].add_scatter(np.array([[0, 12, 0]])) + + # check distances + nearest = fpl.utils.get_nearest_graphics((0, 12), lines) + assert nearest[0] is lines[1] # closest + assert nearest[1] is lines[0] + assert nearest[2] is lines[3] + assert nearest[3] is lines[2] # furthest + assert nearest[-1] is lines[2] From e08d1976081db4dff95b9a63fea8bc31d278e0df Mon Sep 17 00:00:00 2001 From: Caitlin Lewis <69729525+clewis7@users.noreply.github.com> Date: Tue, 18 Jun 2024 16:32:17 -0400 Subject: [PATCH 61/66] disable cmap property for RGB images (#529) --- fastplotlib/graphics/image.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/fastplotlib/graphics/image.py b/fastplotlib/graphics/image.py index d6576c12d..5805804c7 100644 --- a/fastplotlib/graphics/image.py +++ b/fastplotlib/graphics/image.py @@ -132,7 +132,11 @@ def __init__( self._vmin = ImageVmin(vmin) self._vmax = ImageVmax(vmax) - self._cmap = ImageCmap(cmap) + # set cmap to None for RGB images + if self._data.value.ndim == 3: + self._cmap = None + else: + self._cmap = ImageCmap(cmap) self._interpolation = ImageInterpolation(interpolation) self._cmap_interpolation = ImageCmapInterpolation(cmap_interpolation) @@ -189,10 +193,14 @@ def data(self, data): @property def cmap(self) -> str: """colormap name""" + if self.data.value.ndim == 3: + raise AttributeError("RGB images do not have a colormap property") return self._cmap.value @cmap.setter def cmap(self, name: str): + if self.data.value.ndim == 3: + raise AttributeError("RGB images do not have a colormap property") self._cmap.set_value(self, name) @property From bb0bb273ce0be1cdc1b56a1d0702b39d7cdbccc7 Mon Sep 17 00:00:00 2001 From: Caitlin Lewis <69729525+clewis7@users.noreply.github.com> Date: Tue, 18 Jun 2024 16:48:20 -0400 Subject: [PATCH 62/66] sphinx gallery (#509) * start sphinx gallery * relevant file changes * progress * edit setup.py to include missing dependencies * attempt to fix rtd * remove large heatmap example that breaks rtd * fix linter, attempt to fix quickstart bug * fix gridplot examples and gallery structure * remove nbsphinx * remove nbsphinx and quickstart.ipynb from docs * remove init files from examples * update examples and conf.py * add animation examples, edit conf.py, fix scatter examples * add sklearn to docs, example needs it * Update setup.py * Update setup.py * remove from gallery, kills readthedocs runner * update canvas size so thumbnails render better * change scatter examples, remove sklearn from docs dependencies * add iris scatter examples, exclude from screenshot tests * add more animation examples, add to tests * add simple multigraphic gridplot example * add simple event, fix multigraphic gridplot * Update line3d_animation.py * add simple event example * minor changes * replace screenshots and fix small bug * fix scatter examples for CI build * fix screenshots * add sklearn as docs dependency, include iris scatter examples in gallery * Update examples/desktop/misc/simple_event.py Co-authored-by: Kushal Kolar * update screenshots --------- Co-authored-by: Kushal Kolar --- docs/source/conf.py | 51 +++++++- docs/source/index.rst | 6 + examples/README.rst | 2 + .../{gridplot/__init__.py => README.rst} | 0 examples/desktop/gridplot/README.rst | 2 + examples/desktop/gridplot/gridplot.py | 23 ++-- .../desktop/gridplot/gridplot_non_square.py | 23 ++-- .../desktop/gridplot/multigraphic_gridplot.py | 116 ++++++++++++++++++ examples/desktop/heatmap/README.rst | 2 + examples/desktop/heatmap/__init__.py | 0 examples/desktop/heatmap/heatmap.py | 14 ++- examples/desktop/heatmap/heatmap_cmap.py | 15 ++- examples/desktop/heatmap/heatmap_data.py | 14 ++- examples/desktop/heatmap/heatmap_square.py | 11 +- examples/desktop/heatmap/heatmap_vmin_vmax.py | 14 ++- examples/desktop/heatmap/heatmap_wide.py | 11 +- examples/desktop/image/README.rst | 2 + examples/desktop/image/__init__.py | 0 examples/desktop/image/image_cmap.py | 18 +-- examples/desktop/image/image_rgb.py | 20 +-- examples/desktop/image/image_rgbvminvmax.py | 20 +-- examples/desktop/image/image_simple.py | 17 +-- examples/desktop/image/image_vminvmax.py | 20 +-- examples/desktop/image/image_widget.py | 7 +- examples/desktop/line/README.rst | 2 + examples/desktop/line/__init__.py | 0 examples/desktop/line/line.py | 24 ++-- examples/desktop/line/line_cmap.py | 19 +-- examples/desktop/line/line_colorslice.py | 26 ++-- examples/desktop/line/line_dataslice.py | 24 ++-- examples/desktop/line_collection/README.rst | 2 + examples/desktop/line_collection/__init__.py | 0 .../line_collection/line_collection.py | 16 ++- .../line_collection_cmap_values.py | 13 +- ...line_collection_cmap_values_qualitative.py | 17 +-- .../line_collection/line_collection_colors.py | 14 ++- .../line_collection_slicing.py | 10 +- .../desktop/line_collection/line_stack.py | 14 ++- .../desktop/line_collection/line_stack_3d.py | 17 +-- examples/desktop/misc/README.rst | 2 + examples/desktop/misc/cycle_animation.py | 62 ++++++++++ examples/desktop/misc/em_wave_animation.py | 105 ++++++++++++++++ examples/desktop/misc/image_animation.py | 38 ++++++ examples/desktop/misc/line3d_animation.py | 59 +++++++++ examples/desktop/misc/line_animation.py | 53 ++++++++ examples/desktop/misc/multiplot_animation.py | 49 ++++++++ examples/desktop/misc/scatter_animation.py | 59 +++++++++ examples/desktop/misc/simple_event.py | 56 +++++++++ examples/desktop/scatter/README.rst | 2 + examples/desktop/scatter/__init__.py | 0 examples/desktop/scatter/scatter.py | 42 +++++-- examples/desktop/scatter/scatter_cmap.py | 51 +++++--- examples/desktop/scatter/scatter_cmap_iris.py | 42 +++++++ .../desktop/scatter/scatter_colorslice.py | 54 +++++--- .../scatter/scatter_colorslice_iris.py | 42 +++++++ examples/desktop/scatter/scatter_dataslice.py | 42 ++++--- .../desktop/scatter/scatter_dataslice_iris.py | 41 +++++++ examples/desktop/scatter/scatter_iris.py | 38 ++++++ examples/desktop/scatter/scatter_size.py | 21 ++-- examples/desktop/screenshots/gridplot.png | 4 +- .../screenshots/gridplot_non_square.png | 4 +- examples/desktop/screenshots/heatmap.png | 4 +- examples/desktop/screenshots/heatmap_cmap.png | 4 +- examples/desktop/screenshots/heatmap_data.png | 4 +- .../desktop/screenshots/heatmap_square.png | 3 - .../desktop/screenshots/heatmap_vmin_vmax.png | 4 +- examples/desktop/screenshots/heatmap_wide.png | 3 - examples/desktop/screenshots/image_cmap.png | 4 +- examples/desktop/screenshots/image_rgb.png | 4 +- .../desktop/screenshots/image_rgbvminvmax.png | 4 +- examples/desktop/screenshots/image_simple.png | 4 +- .../desktop/screenshots/image_vminvmax.png | 4 +- examples/desktop/screenshots/line.png | 4 +- examples/desktop/screenshots/line_cmap.png | 4 +- .../desktop/screenshots/line_collection.png | 4 +- .../line_collection_cmap_values.png | 4 +- ...ine_collection_cmap_values_qualitative.png | 4 +- .../screenshots/line_collection_colors.png | 4 +- .../screenshots/line_collection_slicing.png | 4 +- .../desktop/screenshots/line_colorslice.png | 4 +- .../desktop/screenshots/line_dataslice.png | 4 +- .../screenshots/line_present_scaling.png | 3 + examples/desktop/screenshots/line_stack.png | 4 +- .../screenshots/multigraphic_gridplot.png | 3 + examples/desktop/screenshots/scatter.png | 4 +- examples/desktop/screenshots/scatter_cmap.png | 4 +- .../desktop/screenshots/scatter_cmap_iris.png | 3 + .../screenshots/scatter_colorslice.png | 4 +- .../screenshots/scatter_colorslice_iris.png | 3 + .../screenshots/scatter_dataslice_iris.png | 3 + examples/desktop/screenshots/scatter_iris.png | 3 + .../desktop/screenshots/scatter_present.png | 3 + examples/desktop/screenshots/scatter_size.png | 4 +- .../nb-image-widget-movie-single-0-reset.png | 4 +- .../nb-image-widget-movie-single-0.png | 4 +- .../nb-image-widget-movie-single-279.png | 4 +- ...e-widget-movie-single-50-window-max-33.png | 4 +- ...-widget-movie-single-50-window-mean-13.png | 4 +- ...-widget-movie-single-50-window-mean-33.png | 4 +- ...ge-widget-movie-single-50-window-reset.png | 4 +- .../nb-image-widget-movie-single-50.png | 4 +- .../notebooks/screenshots/nb-lines-3d.png | 4 +- .../nb-lines-cmap-jet-values-cosine.png | 4 +- .../screenshots/nb-lines-cmap-jet-values.png | 4 +- .../screenshots/nb-lines-cmap-jet.png | 4 +- .../screenshots/nb-lines-cmap-tab-10.png | 4 +- .../nb-lines-cmap-viridis-values.png | 4 +- .../screenshots/nb-lines-cmap-viridis.png | 4 +- .../screenshots/nb-lines-cmap-white.png | 4 +- .../notebooks/screenshots/nb-lines-colors.png | 4 +- .../notebooks/screenshots/nb-lines-data.png | 4 +- .../screenshots/nb-lines-underlay.png | 4 +- examples/notebooks/screenshots/nb-lines.png | 4 +- examples/tests/test_examples.py | 2 +- examples/tests/testutils.py | 3 +- fastplotlib/layouts/_figure.py | 5 - setup.py | 4 + 117 files changed, 1330 insertions(+), 355 deletions(-) create mode 100644 examples/README.rst rename examples/desktop/{gridplot/__init__.py => README.rst} (100%) create mode 100644 examples/desktop/gridplot/README.rst create mode 100644 examples/desktop/gridplot/multigraphic_gridplot.py create mode 100644 examples/desktop/heatmap/README.rst delete mode 100644 examples/desktop/heatmap/__init__.py create mode 100644 examples/desktop/image/README.rst delete mode 100644 examples/desktop/image/__init__.py create mode 100644 examples/desktop/line/README.rst delete mode 100644 examples/desktop/line/__init__.py create mode 100644 examples/desktop/line_collection/README.rst delete mode 100644 examples/desktop/line_collection/__init__.py create mode 100644 examples/desktop/misc/README.rst create mode 100644 examples/desktop/misc/cycle_animation.py create mode 100644 examples/desktop/misc/em_wave_animation.py create mode 100644 examples/desktop/misc/image_animation.py create mode 100644 examples/desktop/misc/line3d_animation.py create mode 100644 examples/desktop/misc/line_animation.py create mode 100644 examples/desktop/misc/multiplot_animation.py create mode 100644 examples/desktop/misc/scatter_animation.py create mode 100644 examples/desktop/misc/simple_event.py create mode 100644 examples/desktop/scatter/README.rst delete mode 100644 examples/desktop/scatter/__init__.py create mode 100644 examples/desktop/scatter/scatter_cmap_iris.py create mode 100644 examples/desktop/scatter/scatter_colorslice_iris.py create mode 100644 examples/desktop/scatter/scatter_dataslice_iris.py create mode 100644 examples/desktop/scatter/scatter_iris.py delete mode 100644 examples/desktop/screenshots/heatmap_square.png delete mode 100644 examples/desktop/screenshots/heatmap_wide.png create mode 100644 examples/desktop/screenshots/line_present_scaling.png create mode 100644 examples/desktop/screenshots/multigraphic_gridplot.png create mode 100644 examples/desktop/screenshots/scatter_cmap_iris.png create mode 100644 examples/desktop/screenshots/scatter_colorslice_iris.png create mode 100644 examples/desktop/screenshots/scatter_dataslice_iris.png create mode 100644 examples/desktop/screenshots/scatter_iris.png create mode 100644 examples/desktop/screenshots/scatter_present.png diff --git a/docs/source/conf.py b/docs/source/conf.py index 38133c901..4d94ec7e7 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -2,7 +2,24 @@ # # For the full list of built-in configuration values, see the documentation: # https://www.sphinx-doc.org/en/master/usage/configuration.html + +import os + +# need to force offscreen rendering before importing fpl +# otherwise fpl tries to select glfw canvas +os.environ["WGPU_FORCE_OFFSCREEN"] = "1" + import fastplotlib +from pygfx.utils.gallery_scraper import find_examples_for_gallery +from pathlib import Path +import sys +from sphinx_gallery.sorting import ExplicitOrder +import imageio.v3 as iio + +ROOT_DIR = Path(__file__).parents[1].parents[0] # repo root +EXAMPLES_DIR = Path.joinpath(ROOT_DIR, "examples", "desktop") + +sys.path.insert(0, str(ROOT_DIR)) # -- Project information ----------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information @@ -23,8 +40,40 @@ "sphinx.ext.viewcode", "sphinx_copybutton", "sphinx_design", + "sphinx_gallery.gen_gallery" ] +sphinx_gallery_conf = { + "gallery_dirs": "_gallery", + "backreferences_dir": "_gallery/backreferences", + "doc_module": ("fastplotlib",), + "image_scrapers": ("pygfx",), + "remove_config_comments": True, + "subsection_order": ExplicitOrder( + [ + "../../examples/desktop/image", + "../../examples/desktop/gridplot", + "../../examples/desktop/line", + "../../examples/desktop/line_collection", + "../../examples/desktop/scatter", + "../../examples/desktop/heatmap", + "../../examples/desktop/misc" + ] + ), + "ignore_pattern": r'__init__\.py', + "nested_sections": False, + "thumbnail_size": (250, 250) +} + +extra_conf = find_examples_for_gallery(EXAMPLES_DIR) +sphinx_gallery_conf.update(extra_conf) + +# download imageio examples for the gallery +iio.imread("imageio:clock.png") +iio.imread("imageio:astronaut.png") +iio.imread("imageio:coffee.png") +iio.imread("imageio:hubble_deep_field.png") + autosummary_generate = True templates_path = ["_templates"] @@ -56,7 +105,7 @@ } html_theme_options = { - "source_repository": "https://github.com/kushalkolar/fastplotlib", + "source_repository": "https://github.com/fastplotlib/fastplotlib", "source_branch": "main", "source_directory": "docs/", } diff --git a/docs/source/index.rst b/docs/source/index.rst index e99e38c52..6385c2aee 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -20,6 +20,12 @@ Welcome to fastplotlib's documentation! Utils GPU +.. toctree:: + :caption: Gallery + :maxdepth: 1 + + Gallery <_gallery/index> + Summary ======= diff --git a/examples/README.rst b/examples/README.rst new file mode 100644 index 000000000..138ec748b --- /dev/null +++ b/examples/README.rst @@ -0,0 +1,2 @@ +Examples that use fastplotlib +============================= diff --git a/examples/desktop/gridplot/__init__.py b/examples/desktop/README.rst similarity index 100% rename from examples/desktop/gridplot/__init__.py rename to examples/desktop/README.rst diff --git a/examples/desktop/gridplot/README.rst b/examples/desktop/gridplot/README.rst new file mode 100644 index 000000000..486e708e7 --- /dev/null +++ b/examples/desktop/gridplot/README.rst @@ -0,0 +1,2 @@ +GridPlot Examples +================= diff --git a/examples/desktop/gridplot/gridplot.py b/examples/desktop/gridplot/gridplot.py index 2669dd49b..044adae80 100644 --- a/examples/desktop/gridplot/gridplot.py +++ b/examples/desktop/gridplot/gridplot.py @@ -1,34 +1,37 @@ """ GridPlot Simple -============ +=============== + Example showing simple 2x2 GridPlot with Standard images from imageio. """ # test_example = true +# sphinx_gallery_pygfx_docs = 'screenshot' import fastplotlib as fpl import imageio.v3 as iio - -fig = fpl.Figure(shape=(2, 2)) +figure = fpl.Figure(shape=(2, 2)) im = iio.imread("imageio:clock.png") im2 = iio.imread("imageio:astronaut.png") im3 = iio.imread("imageio:coffee.png") im4 = iio.imread("imageio:hubble_deep_field.png") -fig[0, 0].add_image(data=im) -fig[0, 1].add_image(data=im2) -fig[1, 0].add_image(data=im3) -fig[1, 1].add_image(data=im4) +figure[0, 0].add_image(data=im) +figure[0, 1].add_image(data=im2) +figure[1, 0].add_image(data=im3) +figure[1, 1].add_image(data=im4) -fig.show() +figure.show() -fig.canvas.set_logical_size(800, 800) +figure.canvas.set_logical_size(700, 560) -for subplot in fig: +for subplot in figure: subplot.auto_scale() +# NOTE: `if __name__ == "__main__"` is NOT how to use fastplotlib interactively +# please see our docs for using fastplotlib interactively in ipython and jupyter if __name__ == "__main__": print(__doc__) fpl.run() diff --git a/examples/desktop/gridplot/gridplot_non_square.py b/examples/desktop/gridplot/gridplot_non_square.py index ea93096dc..c8a68cc85 100644 --- a/examples/desktop/gridplot/gridplot_non_square.py +++ b/examples/desktop/gridplot/gridplot_non_square.py @@ -1,32 +1,35 @@ """ -GridPlot Simple -============ +GridPlot Non-Square Example +=========================== + Example showing simple 2x2 GridPlot with Standard images from imageio. """ # test_example = true +# sphinx_gallery_pygfx_docs = 'screenshot' import fastplotlib as fpl import imageio.v3 as iio - -fig = fpl.Figure(shape=(2, 2), controller_ids="sync") +figure = fpl.Figure(shape=(2, 2), controller_ids="sync") im = iio.imread("imageio:clock.png") im2 = iio.imread("imageio:astronaut.png") im3 = iio.imread("imageio:coffee.png") -fig[0, 0].add_image(data=im) -fig[0, 1].add_image(data=im2) -fig[1, 0].add_image(data=im3) +figure[0, 0].add_image(data=im) +figure[0, 1].add_image(data=im2) +figure[1, 0].add_image(data=im3) -fig.show() +figure.show() -fig.canvas.set_logical_size(800, 800) +figure.canvas.set_logical_size(700, 560) -for subplot in fig: +for subplot in figure: subplot.auto_scale() +# NOTE: `if __name__ == "__main__"` is NOT how to use fastplotlib interactively +# please see our docs for using fastplotlib interactively in ipython and jupyter if __name__ == "__main__": print(__doc__) fpl.run() diff --git a/examples/desktop/gridplot/multigraphic_gridplot.py b/examples/desktop/gridplot/multigraphic_gridplot.py new file mode 100644 index 000000000..3d8dbe4a8 --- /dev/null +++ b/examples/desktop/gridplot/multigraphic_gridplot.py @@ -0,0 +1,116 @@ +""" +Multi-Graphic GridPlot +====================== + +Example showing a Figure with multiple subplots and multiple graphic types. +""" + +# test_example = true +# sphinx_gallery_pygfx_docs = 'screenshot' + +import fastplotlib as fpl +import numpy as np +import imageio.v3 as iio +from itertools import product + +# define figure +figure = fpl.Figure(shape=(2, 2), names=[["image-overlay", "circles"], ["line-stack", "scatter"]]) + +img = iio.imread("imageio:coffee.png") + +# add image to subplot +figure["image-overlay"].add_image(data=img) + +# generate overlay + +# empty array for overlay, shape is [nrows, ncols, RGBA] +overlay = np.zeros(shape=(*img.shape[:2], 4), dtype=np.float32) + +# set the blue values of some pixels with an alpha > 1 +overlay[img[:, :, -1] > 200] = np.array([0.0, 0.0, 1.0, 0.6]).astype(np.float32) + +# add overlay to image +figure["image-overlay"].add_image(data=overlay) + +# generate some circles +def make_circle(center, radius: float, n_points: int = 75) -> np.ndarray: + theta = np.linspace(0, 2 * np.pi, n_points) + xs = radius * np.sin(theta) + ys = radius * np.cos(theta) + + return np.column_stack([xs, ys]) + center + + +spatial_dims = (50, 50) + +# this makes 16 circles, so we can create 16 cmap values, so it will use these values to set the +# color of the line based by using the cmap as a LUT with the corresponding cmap_transform +circles = list() +for center in product(range(0, spatial_dims[0], 15), range(0, spatial_dims[1], 15)): + circles.append(make_circle(center, 5, n_points=75)) + +# things like class labels, cluster labels, etc. +cmap_transform = [ + 0, 1, 1, 2, + 0, 0, 1, 1, + 2, 2, 8, 3, + 1, 9, 1, 5 +] + +# add an image to overlay the circles on +img2 = np.ones((60, 60)) + +figure["circles"].add_image(data=img2) + +# add the circles to the figure +figure["circles"].add_line_collection( + circles, + cmap="tab10", + cmap_transform=cmap_transform, + thickness=3, + alpha=0.5, + name="circles-graphic" +) + +# move the circles graphic so that it is centered over the image +figure["circles"]["circles-graphic"].offset = np.array([7, 7, 2]) + +# generate some sine data +# linspace, create 100 evenly spaced x values from -10 to 10 +xs = np.linspace(-10, 10, 100) +# sine wave +ys = np.sin(xs) +sine = np.dstack([xs, ys])[0] + +# make 10 identical waves +sine_waves = 10 * [sine] + +# add the line stack to the figure +figure["line-stack"].add_line_stack(data=sine_waves, cmap="Wistia", separation=1) + +figure["line-stack"].auto_scale(maintain_aspect=True) + +# generate some scatter data +# create a gaussian cloud of 500 points +n_points = 500 + +mean = [0, 0] # mean of the Gaussian distribution +covariance = [[1, 0], [0, 1]] # covariance matrix + +gaussian_cloud = np.random.multivariate_normal(mean, covariance, n_points) +gaussian_cloud2 = np.random.multivariate_normal(mean, covariance, n_points) + +# add the scatter graphics to the figure +figure["scatter"].add_scatter(data=gaussian_cloud, sizes=1, cmap="jet") +figure["scatter"].add_scatter(data=gaussian_cloud2, colors="r", sizes=1) + +figure.show() + +figure.canvas.set_logical_size(700, 560) + +# NOTE: `if __name__ == "__main__"` is NOT how to use fastplotlib interactively +# please see our docs for using fastplotlib interactively in ipython and jupyter +if __name__ == "__main__": + print(__doc__) + fpl.run() + diff --git a/examples/desktop/heatmap/README.rst b/examples/desktop/heatmap/README.rst new file mode 100644 index 000000000..64294f46f --- /dev/null +++ b/examples/desktop/heatmap/README.rst @@ -0,0 +1,2 @@ +Heatmap Examples +================ diff --git a/examples/desktop/heatmap/__init__.py b/examples/desktop/heatmap/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/examples/desktop/heatmap/heatmap.py b/examples/desktop/heatmap/heatmap.py index f3a1bf460..08b284749 100644 --- a/examples/desktop/heatmap/heatmap.py +++ b/examples/desktop/heatmap/heatmap.py @@ -5,12 +5,12 @@ """ # test_example = true +# sphinx_gallery_pygfx_docs = 'screenshot' import fastplotlib as fpl import numpy as np - -fig = fpl.Figure() +figure = fpl.Figure() xs = np.linspace(0, 1_000, 10_000, dtype=np.float32) @@ -19,14 +19,16 @@ data = np.vstack([sine * i for i in range(20_000)]) # plot the image data -img = fig[0, 0].add_image(data=data, name="heatmap") +img = figure[0, 0].add_image(data=data, name="heatmap") -fig.show() +figure.show() -fig.canvas.set_logical_size(1500, 1500) +figure.canvas.set_logical_size(700, 560) -fig[0, 0].auto_scale() +figure[0, 0].auto_scale() +# NOTE: `if __name__ == "__main__"` is NOT how to use fastplotlib interactively +# please see our docs for using fastplotlib interactively in ipython and jupyter if __name__ == "__main__": print(__doc__) fpl.run() diff --git a/examples/desktop/heatmap/heatmap_cmap.py b/examples/desktop/heatmap/heatmap_cmap.py index 39e697c93..f51981bed 100644 --- a/examples/desktop/heatmap/heatmap_cmap.py +++ b/examples/desktop/heatmap/heatmap_cmap.py @@ -4,13 +4,14 @@ Change the cmap of a heatmap """ + # test_example = false +# sphinx_gallery_pygfx_docs = 'hidden' import fastplotlib as fpl import numpy as np - -fig = fpl.Figure() +figure = fpl.Figure() xs = np.linspace(0, 1_000, 10_000, dtype=np.float32) @@ -19,16 +20,18 @@ data = np.vstack([sine * i for i in range(20_000)]) # plot the image data -img = fig[0, 0].add_image(data=data, name="heatmap") +img = figure[0, 0].add_image(data=data, name="heatmap") -fig.show() +figure.show() -fig.canvas.set_logical_size(1500, 1500) +figure.canvas.set_logical_size(700, 560) -fig[0, 0].auto_scale() +figure[0, 0].auto_scale() img.cmap = "viridis" +# NOTE: `if __name__ == "__main__"` is NOT how to use fastplotlib interactively +# please see our docs for using fastplotlib interactively in ipython and jupyter if __name__ == "__main__": print(__doc__) fpl.run() diff --git a/examples/desktop/heatmap/heatmap_data.py b/examples/desktop/heatmap/heatmap_data.py index 75ef3ce41..9334ea4d7 100644 --- a/examples/desktop/heatmap/heatmap_data.py +++ b/examples/desktop/heatmap/heatmap_data.py @@ -5,12 +5,12 @@ """ # test_example = false +# sphinx_gallery_pygfx_docs = 'hidden' import fastplotlib as fpl import numpy as np - -fig = fpl.Figure() +figure = fpl.Figure() xs = np.linspace(0, 1_000, 9_000, dtype=np.float32) @@ -19,18 +19,20 @@ data = np.vstack([sine * i for i in range(9_000)]) # plot the image data -img = fig[0, 0].add_image(data=data, name="heatmap") +img = figure[0, 0].add_image(data=data, name="heatmap") -fig.show() +figure.show() -fig.canvas.set_logical_size(1500, 1500) +figure.canvas.set_logical_size(700, 560) -fig[0, 0].auto_scale() +figure[0, 0].auto_scale() cosine = np.cos(np.sqrt(xs)[:3000]) # change first 2,000 rows and 3,000 columns img.data[:2_000, :3_000] = np.vstack([cosine * i * 4 for i in range(2_000)]) +# NOTE: `if __name__ == "__main__"` is NOT how to use fastplotlib interactively +# please see our docs for using fastplotlib interactively in ipython and jupyter if __name__ == "__main__": print(__doc__) fpl.run() diff --git a/examples/desktop/heatmap/heatmap_square.py b/examples/desktop/heatmap/heatmap_square.py index f776b74e1..51e71695a 100644 --- a/examples/desktop/heatmap/heatmap_square.py +++ b/examples/desktop/heatmap/heatmap_square.py @@ -5,12 +5,13 @@ """ # test_example = false +# sphinx_gallery_pygfx_docs = 'hidden' import fastplotlib as fpl import numpy as np -fig = fpl.Figure() +figure = fpl.Figure() xs = np.linspace(0, 1_000, 20_000, dtype=np.float32) @@ -19,14 +20,14 @@ data = np.vstack([sine * i for i in range(20_000)]) # plot the image data -img = fig[0, 0].add_image(data=data, name="heatmap") +img = figure[0, 0].add_image(data=data, name="heatmap") del data # data no longer needed after given to graphic -fig.show() +figure.show() -fig.canvas.set_logical_size(1500, 1500) +figure.canvas.set_logical_size(1500, 1500) -fig[0, 0].auto_scale() +figure[0, 0].auto_scale() if __name__ == "__main__": print(__doc__) diff --git a/examples/desktop/heatmap/heatmap_vmin_vmax.py b/examples/desktop/heatmap/heatmap_vmin_vmax.py index 75b6b7b68..45c960fd8 100644 --- a/examples/desktop/heatmap/heatmap_vmin_vmax.py +++ b/examples/desktop/heatmap/heatmap_vmin_vmax.py @@ -5,12 +5,12 @@ """ # test_example = false +# sphinx_gallery_pygfx_docs = 'hidden' import fastplotlib as fpl import numpy as np - -fig = fpl.Figure() +figure = fpl.Figure() xs = np.linspace(0, 1_000, 10_000, dtype=np.float32) @@ -19,17 +19,19 @@ data = np.vstack([sine * i for i in range(20_000)]) # plot the image data -img = fig[0, 0].add_image(data=data, name="heatmap") +img = figure[0, 0].add_image(data=data, name="heatmap") -fig.show() +figure.show() -fig.canvas.set_logical_size(1500, 1500) +figure.canvas.set_logical_size(700, 560) -fig[0, 0].auto_scale() +figure[0, 0].auto_scale() img.vmin = -5_000 img.vmax = 10_000 +# NOTE: `if __name__ == "__main__"` is NOT how to use fastplotlib interactively +# please see our docs for using fastplotlib interactively in ipython and jupyter if __name__ == "__main__": print(__doc__) fpl.run() diff --git a/examples/desktop/heatmap/heatmap_wide.py b/examples/desktop/heatmap/heatmap_wide.py index 251c25fa4..dccf531e2 100644 --- a/examples/desktop/heatmap/heatmap_wide.py +++ b/examples/desktop/heatmap/heatmap_wide.py @@ -5,12 +5,13 @@ """ # test_example = false +# sphinx_gallery_pygfx_docs = 'hidden' import fastplotlib as fpl import numpy as np -fig = fpl.Figure() +figure = fpl.Figure() xs = np.linspace(0, 1_000, 20_000, dtype=np.float32) @@ -19,13 +20,13 @@ data = np.vstack([sine * i for i in range(10_000)]) # plot the image data -img = fig[0, 0].add_image(data=data, name="heatmap") +img = figure[0, 0].add_image(data=data, name="heatmap") -fig.show() +figure.show() -fig.canvas.set_logical_size(1500, 1500) +figure.canvas.set_logical_size(1500, 1500) -fig[0, 0].auto_scale() +figure[0, 0].auto_scale() if __name__ == "__main__": print(__doc__) diff --git a/examples/desktop/image/README.rst b/examples/desktop/image/README.rst new file mode 100644 index 000000000..028e85ec5 --- /dev/null +++ b/examples/desktop/image/README.rst @@ -0,0 +1,2 @@ +Image Examples +============== diff --git a/examples/desktop/image/__init__.py b/examples/desktop/image/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/examples/desktop/image/image_cmap.py b/examples/desktop/image/image_cmap.py index bb8e9f9d8..c70af7346 100644 --- a/examples/desktop/image/image_cmap.py +++ b/examples/desktop/image/image_cmap.py @@ -1,29 +1,33 @@ """ -Simple Plot -============ +Image Colormap +============== + Example showing simple plot creation and subsequent cmap change with Standard image from imageio. """ # test_example = true +# sphinx_gallery_pygfx_docs = 'screenshot' import fastplotlib as fpl import imageio.v3 as iio im = iio.imread("imageio:camera.png") -fig = fpl.Figure() +figure = fpl.Figure() # plot the image data -image_graphic = fig[0, 0].add_image(data=im, name="random-image") +image_graphic = figure[0, 0].add_image(data=im, name="random-image") -fig.show() +figure.show() -fig.canvas.set_logical_size(800, 800) +figure.canvas.set_logical_size(700, 560) -fig[0, 0].auto_scale() +figure[0, 0].auto_scale() image_graphic.cmap = "viridis" +# NOTE: `if __name__ == "__main__"` is NOT how to use fastplotlib interactively +# please see our docs for using fastplotlib interactively in ipython and jupyter if __name__ == "__main__": print(__doc__) fpl.run() diff --git a/examples/desktop/image/image_rgb.py b/examples/desktop/image/image_rgb.py index ce7e151d0..951142fd7 100644 --- a/examples/desktop/image/image_rgb.py +++ b/examples/desktop/image/image_rgb.py @@ -1,29 +1,31 @@ """ -Simple Plot -============ +RGB Image +========= + Example showing the simple plot creation with 512 x 512 2D RGB image. """ # test_example = true +# sphinx_gallery_pygfx_docs = 'screenshot' import fastplotlib as fpl import imageio.v3 as iio - im = iio.imread("imageio:astronaut.png") -fig = fpl.Figure() +figure = fpl.Figure() # plot the image data -image_graphic = fig[0, 0].add_image(data=im, name="iio astronaut") - -fig.show() +image_graphic = figure[0, 0].add_image(data=im, name="iio astronaut") -fig.canvas.set_logical_size(800, 800) +figure.show() -fig[0, 0].auto_scale() +figure.canvas.set_logical_size(700, 560) +figure[0, 0].auto_scale() +# NOTE: `if __name__ == "__main__"` is NOT how to use fastplotlib interactively +# please see our docs for using fastplotlib interactively in ipython and jupyter if __name__ == "__main__": print(__doc__) fpl.run() diff --git a/examples/desktop/image/image_rgbvminvmax.py b/examples/desktop/image/image_rgbvminvmax.py index 56114e1e3..25d3904e8 100644 --- a/examples/desktop/image/image_rgbvminvmax.py +++ b/examples/desktop/image/image_rgbvminvmax.py @@ -1,32 +1,34 @@ """ -Simple Plot -============ +RGB Image Vmin/Vmax +=================== + Example showing the simple plot followed by changing the vmin/vmax with 512 x 512 2D RGB image. """ # test_example = true +# sphinx_gallery_pygfx_docs = 'screenshot' import fastplotlib as fpl import imageio.v3 as iio - im = iio.imread("imageio:astronaut.png") -fig = fpl.Figure() +figure = fpl.Figure() # plot the image data -image_graphic = fig[0, 0].add_image(data=im, name="iio astronaut") +image_graphic = figure[0, 0].add_image(data=im, name="iio astronaut") -fig.show() +figure.show() -fig.canvas.set_logical_size(800, 800) +figure.canvas.set_logical_size(700, 560) -fig[0, 0].auto_scale() +figure[0, 0].auto_scale() image_graphic.vmin = 0.5 image_graphic.vmax = 0.75 - +# NOTE: `if __name__ == "__main__"` is NOT how to use fastplotlib interactively +# please see our docs for using fastplotlib interactively in ipython and jupyter if __name__ == "__main__": print(__doc__) fpl.run() diff --git a/examples/desktop/image/image_simple.py b/examples/desktop/image/image_simple.py index a640974ed..dab5188a1 100644 --- a/examples/desktop/image/image_simple.py +++ b/examples/desktop/image/image_simple.py @@ -1,28 +1,31 @@ """ -Simple Plot +Simple Image ============ + Example showing the simple plot creation with Standard imageio image. """ # test_example = true +# sphinx_gallery_pygfx_docs = 'screenshot' import fastplotlib as fpl import imageio.v3 as iio - -fig = fpl.Figure() +figure = fpl.Figure() data = iio.imread("imageio:camera.png") # plot the image data -image_graphic = fig[0, 0].add_image(data=data, name="iio camera") +image_graphic = figure[0, 0].add_image(data=data, name="iio camera") -fig.show() +figure.show() -fig.canvas.set_logical_size(800, 800) +figure.canvas.set_logical_size(700, 560) -fig[0, 0].auto_scale() +figure[0, 0].auto_scale() +# NOTE: `if __name__ == "__main__"` is NOT how to use fastplotlib interactively +# please see our docs for using fastplotlib interactively in ipython and jupyter if __name__ == "__main__": print(__doc__) fpl.run() diff --git a/examples/desktop/image/image_vminvmax.py b/examples/desktop/image/image_vminvmax.py index d24d1f18c..d9e49b18e 100644 --- a/examples/desktop/image/image_vminvmax.py +++ b/examples/desktop/image/image_vminvmax.py @@ -1,32 +1,34 @@ """ -Simple Plot -============ +Image Vmin/Vmax +=============== + Example showing the simple plot creation followed by changing the vmin/vmax with Standard imageio image. """ # test_example = true +# sphinx_gallery_pygfx_docs = 'screenshot' import fastplotlib as fpl import imageio.v3 as iio - -fig = fpl.Figure() +figure = fpl.Figure() data = iio.imread("imageio:astronaut.png") # plot the image data -image_graphic = fig[0, 0].add_image(data=data, name="iio astronaut") +image_graphic = figure[0, 0].add_image(data=data, name="iio astronaut") -fig.show() +figure.show() -fig.canvas.set_logical_size(800, 800) +figure.canvas.set_logical_size(700, 560) -fig[0, 0].auto_scale() +figure[0, 0].auto_scale() image_graphic.vmin = 0.5 image_graphic.vmax = 0.75 - +# NOTE: `if __name__ == "__main__"` is NOT how to use fastplotlib interactively +# please see our docs for using fastplotlib interactively in ipython and jupyter if __name__ == "__main__": print(__doc__) fpl.run() diff --git a/examples/desktop/image/image_widget.py b/examples/desktop/image/image_widget.py index 80aafe0b1..de1d27de1 100644 --- a/examples/desktop/image/image_widget.py +++ b/examples/desktop/image/image_widget.py @@ -1,19 +1,22 @@ """ Image widget ============ + Example showing the image widget in action. When run in a notebook, or with the Qt GUI backend, sliders are also shown. """ +# sphinx_gallery_pygfx_docs = 'hidden' + import fastplotlib as fpl import imageio.v3 as iio # not a fastplotlib dependency, only used for examples - a = iio.imread("imageio:camera.png") iw = fpl.ImageWidget(data=a, cmap="viridis") iw.show() - +# NOTE: `if __name__ == "__main__"` is NOT how to use fastplotlib interactively +# please see our docs for using fastplotlib interactively in ipython and jupyter if __name__ == "__main__": print(__doc__) fpl.run() diff --git a/examples/desktop/line/README.rst b/examples/desktop/line/README.rst new file mode 100644 index 000000000..b9970c543 --- /dev/null +++ b/examples/desktop/line/README.rst @@ -0,0 +1,2 @@ +Line Examples +============= diff --git a/examples/desktop/line/__init__.py b/examples/desktop/line/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/examples/desktop/line/line.py b/examples/desktop/line/line.py index 56575a810..cd661da1e 100644 --- a/examples/desktop/line/line.py +++ b/examples/desktop/line/line.py @@ -1,16 +1,17 @@ """ -Line Plot -============ +Simple Line Plot +================ + Example showing cosine, sine, sinc lines. """ # test_example = true +# sphinx_gallery_pygfx_docs = 'screenshot' import fastplotlib as fpl import numpy as np - -fig = fpl.Figure() +figure = fpl.Figure() xs = np.linspace(-10, 10, 100) # sine wave @@ -26,22 +27,23 @@ ys = np.sinc(xs) * 3 + 8 sinc = np.dstack([xs, ys])[0] -sine_graphic = fig[0, 0].add_line(data=sine, thickness=5, colors="magenta") +sine_graphic = figure[0, 0].add_line(data=sine, thickness=5, colors="magenta") # you can also use colormaps for lines! -cosine_graphic = fig[0, 0].add_line(data=cosine, thickness=12, cmap="autumn") +cosine_graphic = figure[0, 0].add_line(data=cosine, thickness=12, cmap="autumn") # or a list of colors for each datapoint colors = ["r"] * 25 + ["purple"] * 25 + ["y"] * 25 + ["b"] * 25 -sinc_graphic = fig[0, 0].add_line(data=sinc, thickness=5, colors=colors) - -fig.show() +sinc_graphic = figure[0, 0].add_line(data=sinc, thickness=5, colors=colors) -fig.canvas.set_logical_size(800, 800) +figure.show() -fig[0, 0].auto_scale() +figure.canvas.set_logical_size(700, 560) +figure[0, 0].auto_scale() +# NOTE: `if __name__ == "__main__"` is NOT how to use fastplotlib interactively +# please see our docs for using fastplotlib interactively in ipython and jupyter if __name__ == "__main__": print(__doc__) fpl.run() diff --git a/examples/desktop/line/line_cmap.py b/examples/desktop/line/line_cmap.py index f18ceb201..5ffea6fef 100644 --- a/examples/desktop/line/line_cmap.py +++ b/examples/desktop/line/line_cmap.py @@ -1,16 +1,17 @@ """ -Line Plot -============ +Line Plot Colormap +================== + Example showing cosine, sine, sinc lines. """ # test_example = true +# sphinx_gallery_pygfx_docs = 'screenshot' import fastplotlib as fpl import numpy as np - -fig = fpl.Figure() +figure = fpl.Figure() xs = np.linspace(-10, 10, 100) # sine wave @@ -22,7 +23,7 @@ cosine = np.dstack([xs, ys])[0] # cmap_transform from an array, so the colors on the sine line will be based on the sine y-values -sine_graphic = fig[0, 0].add_line( +sine_graphic = figure[0, 0].add_line( data=sine, thickness=10, cmap="plasma", @@ -31,17 +32,19 @@ # qualitative colormaps, useful for cluster labels or other types of categorical labels labels = [0] * 25 + [5] * 10 + [1] * 35 + [2] * 30 -cosine_graphic = fig[0, 0].add_line( +cosine_graphic = figure[0, 0].add_line( data=cosine, thickness=10, cmap="tab10", cmap_transform=labels ) -fig.show() +figure.show() -fig.canvas.set_logical_size(800, 800) +figure.canvas.set_logical_size(700, 560) +# NOTE: `if __name__ == "__main__"` is NOT how to use fastplotlib interactively +# please see our docs for using fastplotlib interactively in ipython and jupyter if __name__ == "__main__": print(__doc__) fpl.run() diff --git a/examples/desktop/line/line_colorslice.py b/examples/desktop/line/line_colorslice.py index 28b877793..3d18d74b7 100644 --- a/examples/desktop/line/line_colorslice.py +++ b/examples/desktop/line/line_colorslice.py @@ -1,16 +1,17 @@ """ -Line Plot -========= +Line Plot Color Slicing +======================= + Example showing color slicing with cosine, sine, sinc lines. """ # test_example = true +# sphinx_gallery_pygfx_docs = 'screenshot' import fastplotlib as fpl import numpy as np - -fig = fpl.Figure() +figure = fpl.Figure() xs = np.linspace(-10, 10, 100) # sine wave @@ -26,14 +27,14 @@ ys = np.sinc(xs) * 3 sinc = np.column_stack([xs, ys]) -sine_graphic = fig[0, 0].add_line( +sine_graphic = figure[0, 0].add_line( data=sine, thickness=5, colors="magenta" ) # you can also use colormaps for lines! -cosine_graphic = fig[0, 0].add_line( +cosine_graphic = figure[0, 0].add_line( data=cosine, thickness=12, cmap="autumn", @@ -42,7 +43,7 @@ # or a list of colors for each datapoint colors = ["r"] * 25 + ["purple"] * 25 + ["y"] * 25 + ["b"] * 25 -sinc_graphic = fig[0, 0].add_line( +sinc_graphic = figure[0, 0].add_line( data=sinc, thickness=5, colors=colors, @@ -51,14 +52,14 @@ zeros = np.zeros(xs.size) zeros_data = np.column_stack([xs, zeros]) -zeros_graphic = fig[0, 0].add_line( +zeros_graphic = figure[0, 0].add_line( data=zeros_data, thickness=8, colors="w", offset=(0, 10, 0) ) -fig.show() +figure.show() # indexing of colors cosine_graphic.colors[:15] = "magenta" @@ -81,11 +82,12 @@ zeros_graphic.cmap[50:75] = "jet" zeros_graphic.cmap[75:] = "viridis" -fig.canvas.set_logical_size(800, 800) - -fig[0, 0].auto_scale() +figure.canvas.set_logical_size(700, 560) +figure[0, 0].auto_scale() +# NOTE: `if __name__ == "__main__"` is NOT how to use fastplotlib interactively +# please see our docs for using fastplotlib interactively in ipython and jupyter if __name__ == "__main__": print(__doc__) fpl.run() diff --git a/examples/desktop/line/line_dataslice.py b/examples/desktop/line/line_dataslice.py index c2c6b9d36..eac765c68 100644 --- a/examples/desktop/line/line_dataslice.py +++ b/examples/desktop/line/line_dataslice.py @@ -1,16 +1,17 @@ """ -Line Plot -============ +Line Plot Data Slicing +====================== + Example showing data slicing with cosine, sine, sinc lines. """ # test_example = true +# sphinx_gallery_pygfx_docs = 'screenshot' import fastplotlib as fpl import numpy as np - -fig = fpl.Figure() +figure = fpl.Figure() xs = np.linspace(-10, 10, 100) # sine wave @@ -26,16 +27,16 @@ ys = np.sinc(xs) * 3 + 8 sinc = np.dstack([xs, ys])[0] -sine_graphic = fig[0, 0].add_line(data=sine, thickness=5, colors="magenta") +sine_graphic = figure[0, 0].add_line(data=sine, thickness=5, colors="magenta") # you can also use colormaps for lines! -cosine_graphic = fig[0, 0].add_line(data=cosine, thickness=12, cmap="autumn") +cosine_graphic = figure[0, 0].add_line(data=cosine, thickness=12, cmap="autumn") # or a list of colors for each datapoint colors = ["r"] * 25 + ["purple"] * 25 + ["y"] * 25 + ["b"] * 25 -sinc_graphic = fig[0, 0].add_line(data=sinc, thickness=5, colors=colors) +sinc_graphic = figure[0, 0].add_line(data=sinc, thickness=5, colors=colors) -fig.show() +figure.show() cosine_graphic.data[10:50:5, :2] = sine[10:50:5] cosine_graphic.data[90:, 1] = 7 @@ -45,11 +46,12 @@ bool_key = [True, True, True, False, False] * 20 sinc_graphic.data[bool_key, 1] = 7 # y vals to 1 -fig.canvas.set_logical_size(800, 800) - -fig[0, 0].auto_scale() +figure.canvas.set_logical_size(700, 560) +figure[0, 0].auto_scale() +# NOTE: `if __name__ == "__main__"` is NOT how to use fastplotlib interactively +# please see our docs for using fastplotlib interactively in ipython and jupyter if __name__ == "__main__": print(__doc__) fpl.run() diff --git a/examples/desktop/line_collection/README.rst b/examples/desktop/line_collection/README.rst new file mode 100644 index 000000000..3dbe05f7f --- /dev/null +++ b/examples/desktop/line_collection/README.rst @@ -0,0 +1,2 @@ +LineCollection Examples +======================= diff --git a/examples/desktop/line_collection/__init__.py b/examples/desktop/line_collection/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/examples/desktop/line_collection/line_collection.py b/examples/desktop/line_collection/line_collection.py index db99e32ed..44b765319 100644 --- a/examples/desktop/line_collection/line_collection.py +++ b/examples/desktop/line_collection/line_collection.py @@ -1,10 +1,12 @@ """ -Line collection -=============== +Line Collection Simple +====================== + Example showing how to plot line collections """ # test_example = true +# sphinx_gallery_pygfx_docs = 'screenshot' from itertools import product import numpy as np @@ -27,14 +29,16 @@ def make_circle(center, radius: float, n_points: int = 75) -> np.ndarray: pos_xy = np.vstack(circles) -fig = fpl.Figure() +figure = fpl.Figure() -fig[0, 0].add_line_collection(circles, cmap="jet", thickness=5) +figure[0, 0].add_line_collection(circles, cmap="jet", thickness=5) -fig.show() +figure.show() -fig.canvas.set_logical_size(800, 800) +figure.canvas.set_logical_size(700, 560) +# NOTE: `if __name__ == "__main__"` is NOT how to use fastplotlib interactively +# please see our docs for using fastplotlib interactively in ipython and jupyter if __name__ == "__main__": print(__doc__) fpl.run() diff --git a/examples/desktop/line_collection/line_collection_cmap_values.py b/examples/desktop/line_collection/line_collection_cmap_values.py index 5ffc032e9..e94a161ad 100644 --- a/examples/desktop/line_collection/line_collection_cmap_values.py +++ b/examples/desktop/line_collection/line_collection_cmap_values.py @@ -1,16 +1,17 @@ """ Line collections quantitative cmap ================================== + Example showing a line collection with a quantitative cmap """ # test_example = true +# sphinx_gallery_pygfx_docs = 'screenshot' from itertools import product import numpy as np import fastplotlib as fpl - def make_circle(center, radius: float, n_points: int = 75) -> np.ndarray: theta = np.linspace(0, 2 * np.pi, n_points) xs = radius * np.sin(theta) @@ -33,16 +34,18 @@ def make_circle(center, radius: float, n_points: int = 75) -> np.ndarray: # highest values, lowest values, mid-high values, mid values cmap_values = [10] * 4 + [0] * 4 + [7] * 4 + [5] * 4 -fig = fpl.Figure() +figure = fpl.Figure() -fig[0, 0].add_line_collection( +figure[0, 0].add_line_collection( circles, cmap="bwr", cmap_transform=cmap_values, thickness=10 ) -fig.show() +figure.show() -fig.canvas.set_logical_size(800, 800) +figure.canvas.set_logical_size(700, 560) +# NOTE: `if __name__ == "__main__"` is NOT how to use fastplotlib interactively +# please see our docs for using fastplotlib interactively in ipython and jupyter if __name__ == "__main__": print(__doc__) fpl.run() diff --git a/examples/desktop/line_collection/line_collection_cmap_values_qualitative.py b/examples/desktop/line_collection/line_collection_cmap_values_qualitative.py index f96fd3aac..5f9ea0000 100644 --- a/examples/desktop/line_collection/line_collection_cmap_values_qualitative.py +++ b/examples/desktop/line_collection/line_collection_cmap_values_qualitative.py @@ -1,16 +1,17 @@ """ -Line collections qualitative cmaps -================================== +Line Collection Qualitative Colormap +==================================== + Example showing a line collection with a qualitative cmap """ # test_example = true +# sphinx_gallery_pygfx_docs = 'screenshot' from itertools import product import numpy as np import fastplotlib as fpl - def make_circle(center, radius: float, n_points: int = 75) -> np.ndarray: theta = np.linspace(0, 2 * np.pi, n_points) xs = radius * np.sin(theta) @@ -39,19 +40,21 @@ def make_circle(center, radius: float, n_points: int = 75) -> np.ndarray: 1, 1, 1, 5 ] -fig = fpl.Figure() +figure = fpl.Figure() -fig[0, 0].add_line_collection( +figure[0, 0].add_line_collection( circles, cmap="tab10", cmap_transform=cmap_values, thickness=10 ) -fig.show() +figure.show() -fig.canvas.set_logical_size(800, 800) +figure.canvas.set_logical_size(700, 560) +# NOTE: `if __name__ == "__main__"` is NOT how to use fastplotlib interactively +# please see our docs for using fastplotlib interactively in ipython and jupyter if __name__ == "__main__": print(__doc__) fpl.run() diff --git a/examples/desktop/line_collection/line_collection_colors.py b/examples/desktop/line_collection/line_collection_colors.py index 3ee561d8f..bf3e818cd 100644 --- a/examples/desktop/line_collection/line_collection_colors.py +++ b/examples/desktop/line_collection/line_collection_colors.py @@ -1,10 +1,12 @@ """ -Line collection colors +Line Collection Colors ====================== + Example showing one way ot setting colors for individual lines in a collection """ # test_example = true +# sphinx_gallery_pygfx_docs = 'screenshot' from itertools import product import numpy as np @@ -31,14 +33,16 @@ def make_circle(center, radius: float, n_points: int = 75) -> np.ndarray: # this will produce 16 circles so we will define 16 colors colors = ["blue"] * 4 + ["red"] * 4 + ["yellow"] * 4 + ["w"] * 4 -fig = fpl.Figure() +figure = fpl.Figure() -fig[0, 0].add_line_collection(circles, colors=colors, thickness=10) +figure[0, 0].add_line_collection(circles, colors=colors, thickness=10) -fig.show() +figure.show() -fig.canvas.set_logical_size(800, 800) +figure.canvas.set_logical_size(700, 560) +# NOTE: `if __name__ == "__main__"` is NOT how to use fastplotlib interactively +# please see our docs for using fastplotlib interactively in ipython and jupyter if __name__ == "__main__": print(__doc__) fpl.run() diff --git a/examples/desktop/line_collection/line_collection_slicing.py b/examples/desktop/line_collection/line_collection_slicing.py index 9eaebdd7e..a7525f7ba 100644 --- a/examples/desktop/line_collection/line_collection_slicing.py +++ b/examples/desktop/line_collection/line_collection_slicing.py @@ -1,10 +1,12 @@ """ Line collection slicing ======================= + Example showing how to slice a line collection """ # test_example = true +# sphinx_gallery_pygfx_docs = 'screenshot' import numpy as np import fastplotlib as fpl @@ -18,9 +20,9 @@ multi_data = np.stack([data] * 15) -fig = fpl.Figure() +figure = fpl.Figure() -lines = fig[0, 0].add_line_stack( +lines = figure[0, 0].add_line_stack( multi_data, thickness=[2, 10, 2, 5, 5, 5, 8, 8, 8, 9, 3, 3, 3, 4, 4], separation=1, @@ -59,9 +61,9 @@ lines[::2].colors[::5] = "magenta" # set every 5th point of every other line to magenta lines[3:6].colors[50:, -1] = 0.6 # set half the points alpha to 0.6 -fig.show(maintain_aspect=False) +figure.show(maintain_aspect=False) -fig.canvas.set_logical_size(900, 600) +figure.canvas.set_logical_size(700, 580) if __name__ == "__main__": print(__doc__) diff --git a/examples/desktop/line_collection/line_stack.py b/examples/desktop/line_collection/line_stack.py index 676e6e5c2..e7f7125e1 100644 --- a/examples/desktop/line_collection/line_stack.py +++ b/examples/desktop/line_collection/line_stack.py @@ -1,10 +1,12 @@ """ -Line stack +Line Stack ========== + Example showing how to plot a stack of lines """ # test_example = true +# sphinx_gallery_pygfx_docs = 'screenshot' import numpy as np import fastplotlib as fpl @@ -17,19 +19,21 @@ data = np.column_stack([xs, ys]) multi_data = np.stack([data] * 10) -fig = fpl.Figure() +figure = fpl.Figure() -line_stack = fig[0, 0].add_line_stack( +line_stack = figure[0, 0].add_line_stack( multi_data, # shape: (10, 100, 2), i.e. [n_lines, n_points, xy] cmap="jet", # applied along n_lines thickness=5, separation=1, # spacing between lines along the separation axis, default separation along "y" axis ) -fig.show(maintain_aspect=False) +figure.show(maintain_aspect=False) -fig.canvas.set_logical_size(900, 600) +figure.canvas.set_logical_size(700, 560) +# NOTE: `if __name__ == "__main__"` is NOT how to use fastplotlib interactively +# please see our docs for using fastplotlib interactively in ipython and jupyter if __name__ == "__main__": print(__doc__) fpl.run() diff --git a/examples/desktop/line_collection/line_stack_3d.py b/examples/desktop/line_collection/line_stack_3d.py index 41914e2d2..314a97ff2 100644 --- a/examples/desktop/line_collection/line_stack_3d.py +++ b/examples/desktop/line_collection/line_stack_3d.py @@ -1,10 +1,12 @@ """ Line stack 3D ============= + Example showing a 3D stack of lines with animations """ # test_example = false +# sphinx_gallery_pygfx_docs = 'animate' import numpy as np import fastplotlib as fpl @@ -19,9 +21,9 @@ multi_data = np.stack([data] * 10) # create figure to plot lines and use an orbit controller in 3D -fig = fpl.Figure(cameras="3d", controller_types="orbit") +figure = fpl.Figure(cameras="3d", controller_types="orbit") -line_stack = fig[0, 0].add_line_stack( +line_stack = figure[0, 0].add_line_stack( multi_data, # shape: (10, 100, 2), i.e. [n_lines, n_points, xy] cmap="jet", # applied along n_lines thickness=3, @@ -75,7 +77,7 @@ def animate_colors(subplot): colors_iteration += 1 -fig[0, 0].add_animations(animate_data, animate_colors) +figure[0, 0].add_animations(animate_data, animate_colors) # just a pre-saved camera state camera_state = { @@ -91,13 +93,14 @@ def animate_colors(subplot): "depth_range": None, } -fig.show(maintain_aspect=False) - -fig[0, 0].camera.set_state(camera_state) +figure.show(maintain_aspect=False) -fig.canvas.set_logical_size(500, 500) +figure[0, 0].camera.set_state(camera_state) +figure.canvas.set_logical_size(700, 560) +# NOTE: `if __name__ == "__main__"` is NOT how to use fastplotlib interactively +# please see our docs for using fastplotlib interactively in ipython and jupyter if __name__ == "__main__": print(__doc__) fpl.run() diff --git a/examples/desktop/misc/README.rst b/examples/desktop/misc/README.rst new file mode 100644 index 000000000..cc51fd686 --- /dev/null +++ b/examples/desktop/misc/README.rst @@ -0,0 +1,2 @@ +Other Examples +============== diff --git a/examples/desktop/misc/cycle_animation.py b/examples/desktop/misc/cycle_animation.py new file mode 100644 index 000000000..bb402a1f7 --- /dev/null +++ b/examples/desktop/misc/cycle_animation.py @@ -0,0 +1,62 @@ +""" +Scatter Animation Colors +======================== + +Example showing animation with a scatter plot. +""" + +# test_example = false +# sphinx_gallery_pygfx_docs = 'animate' + +import fastplotlib as fpl +import numpy as np + +# create a random distribution of 10,000 xyz coordinates +n_points = 10_000 + +# dimensions always have to be [n_points, xyz] +dims = (n_points, 3) + +clouds_offset = 15 + +# create some random clouds +normal = np.random.normal(size=dims, scale=5) +# stack the data into a single array +cloud = np.vstack( + [ + normal - clouds_offset, + normal, + normal + clouds_offset, + ] +) + +# color each of them separately +colors = ["yellow"] * n_points + ["cyan"] * n_points + ["magenta"] * n_points + +# create plot +figure = fpl.Figure() +subplot_scatter = figure[0, 0] +# use an alpha value since this will be a lot of points +scatter_graphic = subplot_scatter.add_scatter(data=cloud, sizes=3, colors=colors, alpha=0.6) + + +i = 0.05 +def cycle_colors(subplot): + global i + # cycle the red values + scatter_graphic.colors[n_points * 2:, 0] = np.abs(np.sin(i)) + scatter_graphic.colors[n_points * 2:, 1] = np.abs(np.sin(i + (np.pi / 4))) + scatter_graphic.colors[n_points * 2:, 2] = np.abs(np.cos(i)) + i += 0.05 + +subplot_scatter.add_animations(cycle_colors) + +figure.show() + +subplot_scatter.canvas.set_logical_size(700, 560) + +# NOTE: `if __name__ == "__main__"` is NOT how to use fastplotlib interactively +# please see our docs for using fastplotlib interactively in ipython and jupyter +if __name__ == "__main__": + print(__doc__) + fpl.run() \ No newline at end of file diff --git a/examples/desktop/misc/em_wave_animation.py b/examples/desktop/misc/em_wave_animation.py new file mode 100644 index 000000000..50ab27ed6 --- /dev/null +++ b/examples/desktop/misc/em_wave_animation.py @@ -0,0 +1,105 @@ +""" +Electromagnetic Wave Animation +============================== + +Example showing animation of an electromagnetic wave. +""" + +# test_example = false +# sphinx_gallery_pygfx_docs = 'animate' + +import fastplotlib as fpl +import numpy as np + +figure = fpl.Figure( + cameras="3d", + controller_types="orbit", + size=(700, 400) +) + +start, stop = 0, 4 * np.pi + +# let's define the x, y and z axes for each with direction of wave propogation along the z-axis +# electric field in the xz plane travelling along +zs = np.linspace(start, stop, 200) +e_ys = np.zeros(200) +e_xs = np.sin(zs) +electric = np.column_stack([e_xs, e_ys, zs]) + +# magnetic field in the yz plane +zs = np.linspace(start, stop, 200) +m_ys = np.sin(zs) +m_xs = np.zeros(200) +magnetic = np.column_stack([m_xs, m_ys, zs]) + +# add the lines +figure[0, 0].add_line(electric, colors="blue", thickness=2, name="e") +figure[0, 0].add_line(magnetic, colors="red", thickness=2, name="m") + +# draw vector line at every 10th position +electric_vectors = [np.array([[0, 0, z], [x, 0, z]]) for (x, z) in zip(e_xs[::10], zs[::10])] +magnetic_vectors = [np.array([[0, 0, z], [0, y, z]]) for (y, z) in zip(m_ys[::10], zs[::10])] + +# add as a line collection +figure[0, 0].add_line_collection(electric_vectors, colors="blue", thickness=1.5, name="e-vec") +figure[0, 0].add_line_collection(magnetic_vectors, colors="red", thickness=1.5, name="m-vec") +# note that the z_offset in `add_line_collection` is not data-related +# it is the z-offset for where to place the *graphic*, by default with Orthographic cameras (i.e. 2D views) +# it will increment by 1 for each line in the collection, we want to disable this so set z_position=0 + +# axes are a WIP, just draw a white line along z for now +z_axis = np.array([[0, 0, 0], [0, 0, stop]]) +figure[0, 0].add_line(z_axis, colors="w", thickness=1) + +# just a pre-saved camera state +state = { + 'position': np.array([-8.0 , 6.0, -2.0]), + 'rotation': np.array([0.09, 0.9 , 0.2, -0.5]), + 'scale': np.array([1., 1., 1.]), + 'reference_up': np.array([0., 1., 0.]), + 'fov': 50.0, + 'width': 12, + 'height': 12, + 'zoom': 1.35, + 'maintain_aspect': True, + 'depth_range': None +} + + +figure[0, 0].camera.set_state(state) + +figure.show() + +figure[0, 0].camera.zoom = 1.5 + +increment = np.pi * 4 / 100 + +figure.canvas.set_logical_size(700, 560) + +# moves the wave one step along the z-axis +def tick(subplot): + global increment, start, stop, zs + new_zs = np.linspace(start, stop, 200) + new_data = np.sin(new_zs) + + # just change the x-axis vals for the electric field + subplot["e"].data[:, 0] = new_data + # and y-axis vals for magnetic field + subplot["m"].data[:, 1] = new_data + + # update the vector lines + for i, (value, z) in enumerate(zip(new_data[::10], zs[::10])): + subplot["e-vec"].graphics[i].data = np.array([[0, 0, z], [value, 0, z]]) + subplot["m-vec"].graphics[i].data = np.array([[0, 0, z], [0, value, z]]) + + start += increment + stop += increment + + +figure[0, 0].add_animations(tick) + +# NOTE: `if __name__ == "__main__"` is NOT how to use fastplotlib interactively +# please see our docs for using fastplotlib interactively in ipython and jupyter +if __name__ == "__main__": + print(__doc__) + fpl.run() \ No newline at end of file diff --git a/examples/desktop/misc/image_animation.py b/examples/desktop/misc/image_animation.py new file mode 100644 index 000000000..df84f3c5a --- /dev/null +++ b/examples/desktop/misc/image_animation.py @@ -0,0 +1,38 @@ +""" +Simple Image Update +=================== + +Example showing updating a single plot with new random 512x512 data. +""" + +# test_example = false +# sphinx_gallery_pygfx_docs = 'animate' + +import fastplotlib as fpl +import numpy as np + +data = np.random.rand(512, 512) + +figure = fpl.Figure() + +# plot the image data +image_graphic = figure[0, 0].add_image(data=data, name="random-image") + + +# a function to update the image_graphic +# a figure-level animation function will optionally take the figure as an argument +def update_data(figure_instance): + new_data = np.random.rand(512, 512) + figure_instance[0, 0]["random-image"].data = new_data + +figure.add_animations(update_data) + +figure.show() + +figure.canvas.set_logical_size(700, 560) + +# NOTE: `if __name__ == "__main__"` is NOT how to use fastplotlib interactively +# please see our docs for using fastplotlib interactively in ipython and jupyter +if __name__ == "__main__": + print(__doc__) + fpl.run() diff --git a/examples/desktop/misc/line3d_animation.py b/examples/desktop/misc/line3d_animation.py new file mode 100644 index 000000000..27d22c78a --- /dev/null +++ b/examples/desktop/misc/line3d_animation.py @@ -0,0 +1,59 @@ +""" +Simple 3D Line Animation +======================== + +Example showing animation with 3D lines. +""" + +# test_example = false +# sphinx_gallery_pygfx_docs = 'animate 5s' + +import numpy as np +import fastplotlib as fpl + +# create data in the shape of a spiral +phi = np.linspace(0, 30, 200) + +xs = phi * np.cos(phi) +ys = phi * np.sin(phi) +zs = phi + +# make data 3d, with shape [, 3] +spiral = np.dstack([xs, ys, zs])[0] + +figure = fpl.Figure(cameras="3d") + +line_graphic = figure[0,0].add_line(data=spiral, thickness=3, cmap='jet') + +marker = figure[0,0].add_scatter(data=spiral[0], sizes=10, name="marker") + +marker_index = 0 + + +# a function to move the ball along the spiral +def move_marker(): + global marker_index + + marker_index += 1 + + if marker_index == spiral.shape[0]: + marker_index = 0 + + for subplot in figure: + subplot["marker"].data = spiral[marker_index] + + +# add `move_marker` to the animations +figure.add_animations(move_marker) + +figure.show() + +figure.canvas.set_logical_size(700, 560) + +figure[0,0].auto_scale(maintain_aspect=False) + +# NOTE: `if __name__ == "__main__"` is NOT how to use fastplotlib interactively +# please see our docs for using fastplotlib interactively in ipython and jupyter +if __name__ == "__main__": + print(__doc__) + fpl.run() diff --git a/examples/desktop/misc/line_animation.py b/examples/desktop/misc/line_animation.py new file mode 100644 index 000000000..50faad5c7 --- /dev/null +++ b/examples/desktop/misc/line_animation.py @@ -0,0 +1,53 @@ +""" +Simple Line Animation +===================== + +Example showing animation with lines. +""" + +# test_example = false +# sphinx_gallery_pygfx_docs = 'animate' + +import fastplotlib as fpl +import numpy as np + +# generate some data +start, stop = 0, 2 * np.pi +increment = (2 * np.pi) / 50 + +# make a simple sine wave +xs = np.linspace(start, stop, 100) +ys = np.sin(xs) + +figure = fpl.Figure() + +# plot the image data +sine = figure[0, 0].add_line(ys, name="sine", colors="r") + + +# increment along the x-axis on each render loop :D +def update_line(subplot): + global increment, start, stop + xs = np.linspace(start + increment, stop + increment, 100) + ys = np.sin(xs) + + start += increment + stop += increment + + # change only the y-axis values of the line + subplot["sine"].data[:, 1] = ys + + +figure[0, 0].add_animations(update_line) + +figure.show() + +figure.canvas.set_logical_size(700, 560) + +figure[0,0].auto_scale(maintain_aspect=False) + +# NOTE: `if __name__ == "__main__"` is NOT how to use fastplotlib interactively +# please see our docs for using fastplotlib interactively in ipython and jupyter +if __name__ == "__main__": + print(__doc__) + fpl.run() \ No newline at end of file diff --git a/examples/desktop/misc/multiplot_animation.py b/examples/desktop/misc/multiplot_animation.py new file mode 100644 index 000000000..a712ce9ef --- /dev/null +++ b/examples/desktop/misc/multiplot_animation.py @@ -0,0 +1,49 @@ +""" +Multi-Subplot Image Update +========================== + +Example showing updating a single plot with new random 512x512 data. +""" + +# test_example = false +# sphinx_gallery_pygfx_docs = 'animate' + +import fastplotlib as fpl +import numpy as np + +# Figure of shape 2 x 3 with all controllers synced +figure = fpl.Figure(shape=(2, 3), controller_ids="sync") + +# Make a random image graphic for each subplot +for subplot in figure: + # create image data + data = np.random.rand(512, 512) + # add an image to the subplot + subplot.add_image(data, name="rand-img") + +figure[0,1]["rand-img"].cmap = "viridis" +figure[1,0]["rand-img"].cmap = "Wistia" +figure[0,2]["rand-img"].cmap = "gray" +figure[1,1]["rand-img"].cmap = "spring" + +# Define a function to update the image graphics with new data +# add_animations will pass the gridplot to the animation function +def update_data(f): + for subplot in f: + new_data = np.random.rand(512, 512) + # index the image graphic by name and set the data + subplot["rand-img"].data = new_data + +# add the animation function +figure.add_animations(update_data) + +# show the gridplot +figure.show() + +figure.canvas.set_logical_size(700, 560) + +# NOTE: `if __name__ == "__main__"` is NOT how to use fastplotlib interactively +# please see our docs for using fastplotlib interactively in ipython and jupyter +if __name__ == "__main__": + print(__doc__) + fpl.run() \ No newline at end of file diff --git a/examples/desktop/misc/scatter_animation.py b/examples/desktop/misc/scatter_animation.py new file mode 100644 index 000000000..aa1495dd9 --- /dev/null +++ b/examples/desktop/misc/scatter_animation.py @@ -0,0 +1,59 @@ +""" +Scatter Animation Data +====================== + +Example showing animation with a scatter plot. +""" + +# test_example = false +# sphinx_gallery_pygfx_docs = 'animate' + +import fastplotlib as fpl +import numpy as np + +# create a random distribution of 10,000 xyz coordinates +n_points = 10_000 + +# dimensions always have to be [n_points, xyz] +dims = (n_points, 3) + +clouds_offset = 15 + +# create some random clouds +normal = np.random.normal(size=dims, scale=5) +# stack the data into a single array +cloud = np.vstack( + [ + normal - clouds_offset, + normal, + normal + clouds_offset, + ] +) + +# color each of them separately +colors = ["yellow"] * n_points + ["cyan"] * n_points + ["magenta"] * n_points + +# create plot +figure = fpl.Figure() +subplot_scatter = figure[0, 0] +# use an alpha value since this will be a lot of points +scatter_graphic = subplot_scatter.add_scatter(data=cloud, sizes=3, colors=colors, alpha=0.6) + + +def update_points(subplot): + # move every point by a small amount + deltas = np.random.normal(size=scatter_graphic.data.value.shape, loc=0, scale=0.15) + scatter_graphic.data = scatter_graphic.data.value + deltas + + +subplot_scatter.add_animations(update_points) + +figure.show() + +subplot_scatter.canvas.set_logical_size(700, 560) + +# NOTE: `if __name__ == "__main__"` is NOT how to use fastplotlib interactively +# please see our docs for using fastplotlib interactively in ipython and jupyter +if __name__ == "__main__": + print(__doc__) + fpl.run() \ No newline at end of file diff --git a/examples/desktop/misc/simple_event.py b/examples/desktop/misc/simple_event.py new file mode 100644 index 000000000..b6d408862 --- /dev/null +++ b/examples/desktop/misc/simple_event.py @@ -0,0 +1,56 @@ +""" +Simple Event +============ + +Example showing how to add a simple callback event. +""" + +# test_example = false +# sphinx_gallery_pygfx_docs = 'screenshot' + +import fastplotlib as fpl +import imageio.v3 as iio + +data = iio.imread("imageio:camera.png") + +# Create a figure +figure = fpl.Figure() + +# plot sine wave, use a single color +image_graphic = figure[0,0].add_image(data=data) + +# show the plot +figure.show() + + +# define callback function to print the event data +def callback_func(event_data): + print(event_data.info) + + +# Will print event data when the color changes +image_graphic.add_event_handler(callback_func, "cmap") + +image_graphic.cmap = "viridis" + + +# adding a click event, we can also use decorators to add event handlers +@image_graphic.add_event_handler("click") +def click_event(event_data): + # get the click location in screen coordinates + xy = (event_data.x, event_data.y) + + # map the screen coordinates to world coordinates + xy = figure[0,0].map_screen_to_world(xy)[:-1] + + # print the click location + print(xy) + + +figure.canvas.set_logical_size(700, 560) + +# NOTE: `if __name__ == "__main__"` is NOT how to use fastplotlib interactively +# please see our docs for using fastplotlib interactively in ipython and jupyter +if __name__ == "__main__": + print(__doc__) + fpl.run() diff --git a/examples/desktop/scatter/README.rst b/examples/desktop/scatter/README.rst new file mode 100644 index 000000000..278170fb4 --- /dev/null +++ b/examples/desktop/scatter/README.rst @@ -0,0 +1,2 @@ +Scatter Examples +================ diff --git a/examples/desktop/scatter/__init__.py b/examples/desktop/scatter/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/examples/desktop/scatter/scatter.py b/examples/desktop/scatter/scatter.py index c47306722..05dd7a99b 100644 --- a/examples/desktop/scatter/scatter.py +++ b/examples/desktop/scatter/scatter.py @@ -1,32 +1,54 @@ """ Scatter Plot ============ + Example showing scatter plot. """ -# test_example = true +# test_example = false +# sphinx_gallery_pygfx_docs = 'screenshot' import fastplotlib as fpl import numpy as np -from pathlib import Path -fig = fpl.Figure() +figure = fpl.Figure() + +# create a random distribution of 10,000 xyz coordinates +n_points = 5_000 + +# dimensions always have to be [n_points, xyz] +dims = (n_points, 3) + +clouds_offset = 15 -data_path = Path(__file__).parent.parent.joinpath("data", "iris.npy") -data = np.load(data_path) +# create some random clouds +normal = np.random.normal(size=dims, scale=5) +# stack the data into a single array +cloud = np.vstack( + [ + normal - clouds_offset, + normal, + normal + clouds_offset, + ] +) -n_points = 50 +# color each of them separately colors = ["yellow"] * n_points + ["cyan"] * n_points + ["magenta"] * n_points -scatter_graphic = fig[0, 0].add_scatter(data=data[:, :-1], sizes=6, alpha=0.7, colors=colors) +# create plot +figure = fpl.Figure() -fig.show() +# use an alpha value since this will be a lot of points +figure[0,0].add_scatter(data=cloud, sizes=3, colors=colors, alpha=0.6) -fig.canvas.set_logical_size(800, 800) +figure.show() -fig[0, 0].auto_scale() +figure.canvas.set_logical_size(700, 560) +figure[0, 0].auto_scale() +# NOTE: `if __name__ == "__main__"` is NOT how to use fastplotlib interactively +# please see our docs for using fastplotlib interactively in ipython and jupyter if __name__ == "__main__": print(__doc__) fpl.run() diff --git a/examples/desktop/scatter/scatter_cmap.py b/examples/desktop/scatter/scatter_cmap.py index 58c43c0ea..0adf72509 100644 --- a/examples/desktop/scatter/scatter_cmap.py +++ b/examples/desktop/scatter/scatter_cmap.py @@ -1,42 +1,53 @@ """ -Scatter Plot -============ +Scatter Colormap +================ + Example showing cmap change for scatter plot. """ -# test_example = true +# test_example = false +# sphinx_gallery_pygfx_docs = 'screenshot' import fastplotlib as fpl import numpy as np -from pathlib import Path -from sklearn.cluster import AgglomerativeClustering +figure = fpl.Figure() -fig = fpl.Figure() +# create a random distribution of 10,000 xyz coordinates +n_points = 5_000 -data_path = Path(__file__).parent.parent.joinpath("data", "iris.npy") -data = np.load(data_path) +# dimensions always have to be [n_points, xyz] +dims = (n_points, 3) -agg = AgglomerativeClustering(n_clusters=3) -agg.fit_predict(data) +clouds_offset = 15 -scatter_graphic = fig[0, 0].add_scatter( - data=data[:, :-1], # use only xy data - sizes=15, - alpha=0.7, - cmap="Set1", - cmap_transform=agg.labels_ # use the labels as a transform to map colors from the colormap +# create some random clouds +normal = np.random.normal(size=dims, scale=5) +# stack the data into a single array +cloud = np.vstack( + [ + normal - clouds_offset, + normal, + normal + clouds_offset, + ] ) -fig.show() +# color each of them separately +colors = ["yellow"] * n_points + ["cyan"] * n_points + ["magenta"] * n_points + +# use an alpha value since this will be a lot of points +figure[0,0].add_scatter(data=cloud, sizes=3, colors=colors, alpha=0.6) -fig.canvas.set_logical_size(800, 800) +figure.show() -fig[0, 0].auto_scale() +figure[0,0].graphics[0].cmap = "viridis" -scatter_graphic.cmap = "tab10" +figure.canvas.set_logical_size(700, 560) +figure[0, 0].auto_scale() +# NOTE: `if __name__ == "__main__"` is NOT how to use fastplotlib interactively +# please see our docs for using fastplotlib interactively in ipython and jupyter if __name__ == "__main__": print(__doc__) fpl.run() diff --git a/examples/desktop/scatter/scatter_cmap_iris.py b/examples/desktop/scatter/scatter_cmap_iris.py new file mode 100644 index 000000000..700f5c136 --- /dev/null +++ b/examples/desktop/scatter/scatter_cmap_iris.py @@ -0,0 +1,42 @@ +""" +Iris Scatter Colormap +===================== + +Example showing cmap change for scatter plot. +""" + +# test_example = true +# sphinx_gallery_pygfx_docs = 'screenshot' + +import fastplotlib as fpl +from sklearn.cluster import AgglomerativeClustering +from sklearn import datasets + + +figure = fpl.Figure() + +data = datasets.load_iris()["data"] + +agg = AgglomerativeClustering(n_clusters=3) +agg.fit_predict(data) + +scatter_graphic = figure[0, 0].add_scatter( + data=data[:, :-1], # use only xy data + sizes=15, + alpha=0.7, + cmap="Set1", + cmap_transform=agg.labels_ # use the labels as a transform to map colors from the colormap +) + +figure.show() + +figure.canvas.set_logical_size(700, 560) + +figure[0, 0].auto_scale() + +scatter_graphic.cmap = "tab10" + + +if __name__ == "__main__": + print(__doc__) + fpl.run() diff --git a/examples/desktop/scatter/scatter_colorslice.py b/examples/desktop/scatter/scatter_colorslice.py index 60433b5f5..3d3a3fa26 100644 --- a/examples/desktop/scatter/scatter_colorslice.py +++ b/examples/desktop/scatter/scatter_colorslice.py @@ -1,42 +1,60 @@ """ -Scatter Plot -============ +Scatter Plot Color Slicing +========================== + Example showing color slice for scatter plot. """ -# test_example = true +# test_example = false +# sphinx_gallery_pygfx_docs = 'screenshot' import fastplotlib as fpl import numpy as np -from pathlib import Path +figure = fpl.Figure() -fig = fpl.Figure() +# create a random distribution of 10,000 xyz coordinates +n_points = 5_000 -data_path = Path(__file__).parent.parent.joinpath("data", "iris.npy") -data = np.load(data_path) +# dimensions always have to be [n_points, xyz] +dims = (n_points, 3) -n_points = 50 -colors = ["yellow"] * n_points + ["cyan"] * n_points + ["magenta"] * n_points +clouds_offset = 30 -scatter_graphic = fig[0, 0].add_scatter( - data=data[:, :-1], - sizes=6, - alpha=0.7, - colors=colors # use colors from the list of strings +# create some random clouds +normal = np.random.normal(size=dims, scale=5) +# stack the data into a single array +cloud = np.vstack( + [ + normal - clouds_offset, + normal, + normal + clouds_offset, + ] ) -fig.show() +# color each of them separately +colors = ["yellow"] * n_points + ["cyan"] * n_points + ["magenta"] * n_points + +# create plot +figure = fpl.Figure() -fig.canvas.set_logical_size(800, 800) +# use an alpha value since this will be a lot of points +figure[0,0].add_scatter(data=cloud, sizes=3, colors=colors, alpha=0.6) -fig[0, 0].auto_scale() +figure.show() + +figure.canvas.set_logical_size(700, 560) + +scatter_graphic = figure[0, 0].graphics[0] + +figure[0, 0].auto_scale() scatter_graphic.colors[0:75] = "red" scatter_graphic.colors[75:150] = "white" scatter_graphic.colors[::2] = "blue" - +# NOTE: `if __name__ == "__main__"` is NOT how to use fastplotlib interactively +# please see our docs for using fastplotlib interactively in ipython and jupyter if __name__ == "__main__": print(__doc__) fpl.run() diff --git a/examples/desktop/scatter/scatter_colorslice_iris.py b/examples/desktop/scatter/scatter_colorslice_iris.py new file mode 100644 index 000000000..a1e6d5318 --- /dev/null +++ b/examples/desktop/scatter/scatter_colorslice_iris.py @@ -0,0 +1,42 @@ +""" +Iris Scatter Plot Color Slicing +=============================== + +Example showing color slice for scatter plot. +""" + +# test_example = true +# sphinx_gallery_pygfx_docs = 'screenshot' + +import fastplotlib as fpl +from sklearn import datasets + + +figure = fpl.Figure() + +data = datasets.load_iris()["data"] + +n_points = 50 +colors = ["yellow"] * n_points + ["cyan"] * n_points + ["magenta"] * n_points + +scatter_graphic = figure[0, 0].add_scatter( + data=data[:, :-1], + sizes=6, + alpha=0.7, + colors=colors # use colors from the list of strings +) + +figure.show() + +figure.canvas.set_logical_size(700, 560) + +figure[0, 0].auto_scale() + +scatter_graphic.colors[0:75] = "red" +scatter_graphic.colors[75:150] = "white" +scatter_graphic.colors[::2] = "blue" + + +if __name__ == "__main__": + print(__doc__) + fpl.run() diff --git a/examples/desktop/scatter/scatter_dataslice.py b/examples/desktop/scatter/scatter_dataslice.py index 989b7c21c..af2fffebd 100644 --- a/examples/desktop/scatter/scatter_dataslice.py +++ b/examples/desktop/scatter/scatter_dataslice.py @@ -1,40 +1,46 @@ """ -Scatter Plot -============ +Scatter Plot Data Slicing +========================= + Example showing data slice for scatter plot. """ -# test_example = true +# test_example = false +# sphinx_gallery_pygfx_docs = 'screenshot' import fastplotlib as fpl import numpy as np -from pathlib import Path -fig = fpl.Figure() +figure = fpl.Figure() -data_path = Path(__file__).parent.parent.joinpath("data", "iris.npy") -data = np.load(data_path) +# create a gaussian cloud of 5_000 points +n_points = 1_000 -n_points = 50 -colors = ["yellow"] * n_points + ["cyan"] * n_points + ["magenta"] * n_points +mean = [0, 0] # mean of the Gaussian distribution +covariance = [[1, 0], [0, 1]] # covariance matrix -scatter_graphic = fig[0, 0].add_scatter(data=data[:, :-1], sizes=6, alpha=0.7, colors=colors) +gaussian_cloud = np.random.multivariate_normal(mean, covariance, n_points) +gaussian_cloud2 = np.random.multivariate_normal(mean, covariance, n_points) -fig.show() +# create plot +figure = fpl.Figure() -fig.canvas.set_logical_size(800, 800) +# use an alpha value since this will be a lot of points +scatter1 = figure[0,0].add_scatter(data=gaussian_cloud, sizes=3) +scatter2 = figure[0,0].add_scatter(data=gaussian_cloud2, colors="r", sizes=3) -fig[0, 0].auto_scale() +figure.show() -scatter_graphic.data[0] = np.array([[5, 3, 1.5]]) -scatter_graphic.data[1] = np.array([[4.3, 3.2, 1.3]]) -scatter_graphic.data[2] = np.array([[5.2, 2.7, 1.7]]) +figure.canvas.set_logical_size(700, 560) -scatter_graphic.data[10:15] = scatter_graphic.data[0:5] + np.array([1, 1, 1]) -scatter_graphic.data[50:100:2] = scatter_graphic.data[100:150:2] + np.array([1, 1, 0]) +figure[0, 0].auto_scale() +scatter1.data[:500] = np.array([0 , 0, 0]) +scatter2.data[500:] = np.array([0 , 0, 0]) +# NOTE: `if __name__ == "__main__"` is NOT how to use fastplotlib interactively +# please see our docs for using fastplotlib interactively in ipython and jupyter if __name__ == "__main__": print(__doc__) fpl.run() diff --git a/examples/desktop/scatter/scatter_dataslice_iris.py b/examples/desktop/scatter/scatter_dataslice_iris.py new file mode 100644 index 000000000..0d47c6efd --- /dev/null +++ b/examples/desktop/scatter/scatter_dataslice_iris.py @@ -0,0 +1,41 @@ +""" +Iris Scatter Plot Data Slicing +============================== + +Example showing data slice for scatter plot. +""" + +# test_example = true +# sphinx_gallery_pygfx_docs = 'screenshot' + +import fastplotlib as fpl +import numpy as np +from sklearn import datasets + + +figure = fpl.Figure() + +data = datasets.load_iris()["data"] + +n_points = 50 +colors = ["yellow"] * n_points + ["cyan"] * n_points + ["magenta"] * n_points + +scatter_graphic = figure[0, 0].add_scatter(data=data[:, :-1], sizes=6, alpha=0.7, colors=colors) + +figure.show() + +figure.canvas.set_logical_size(700, 560) + +figure[0, 0].auto_scale() + +scatter_graphic.data[0] = np.array([[5, 3, 1.5]]) +scatter_graphic.data[1] = np.array([[4.3, 3.2, 1.3]]) +scatter_graphic.data[2] = np.array([[5.2, 2.7, 1.7]]) + +scatter_graphic.data[10:15] = scatter_graphic.data[0:5] + np.array([1, 1, 1]) +scatter_graphic.data[50:100:2] = scatter_graphic.data[100:150:2] + np.array([1, 1, 0]) + + +if __name__ == "__main__": + print(__doc__) + fpl.run() diff --git a/examples/desktop/scatter/scatter_iris.py b/examples/desktop/scatter/scatter_iris.py new file mode 100644 index 000000000..c16a4b135 --- /dev/null +++ b/examples/desktop/scatter/scatter_iris.py @@ -0,0 +1,38 @@ +""" +Iris Scatter Plot +================= + +Example showing scatter plot using sklearn iris dataset. +""" + +# test_example = true +# sphinx_gallery_pygfx_docs = 'hidden' + +import fastplotlib as fpl +import numpy as np +from pathlib import Path +import sys + +figure = fpl.Figure() + +current_file = Path(sys.argv[0]).resolve() + +data_path = Path(__file__).parent.parent.joinpath("data", "iris.npy") +data = np.load(data_path) + +n_points = 50 +colors = ["yellow"] * n_points + ["cyan"] * n_points + ["magenta"] * n_points + +scatter_graphic = figure[0, 0].add_scatter(data=data[:, :-1], sizes=6, alpha=0.7, colors=colors) + +figure.show() + +figure.canvas.set_logical_size(700, 560) + +figure[0, 0].auto_scale() + +# NOTE: `if __name__ == "__main__"` is NOT how to use fastplotlib interactively +# please see our docs for using fastplotlib interactively in ipython and jupyter +if __name__ == "__main__": + print(__doc__) + fpl.run() \ No newline at end of file diff --git a/examples/desktop/scatter/scatter_size.py b/examples/desktop/scatter/scatter_size.py index 41a97ad53..bd4e2db2b 100644 --- a/examples/desktop/scatter/scatter_size.py +++ b/examples/desktop/scatter/scatter_size.py @@ -1,10 +1,13 @@ """ -Scatter Plot -============ +Scatter Plot Size +================= + Example showing point size change for scatter plot. """ # test_example = true +# sphinx_gallery_pygfx_docs = 'screenshot' + import numpy as np import fastplotlib as fpl @@ -15,7 +18,7 @@ names = [["scalar_size"], ["array_size"]] # Create the grid plot -fig = fpl.Figure(shape=shape, names=names, size=(1000, 1000)) +figure = fpl.Figure(shape=shape, names=names, size=(1000, 1000)) # get y_values using sin function angles = np.arange(0, 20 * np.pi + 0.001, np.pi / 20) @@ -24,18 +27,22 @@ data = np.column_stack([x_values, y_values]) -fig["scalar_size"].add_scatter( +figure["scalar_size"].add_scatter( data=data, sizes=5, colors="blue" ) # add a set of scalar sizes non_scalar_sizes = np.abs((y_values / np.pi)) # ensure minimum size of 5 -fig["array_size"].add_scatter(data=data, sizes=non_scalar_sizes, colors="red") +figure["array_size"].add_scatter(data=data, sizes=non_scalar_sizes, colors="red") -for graph in fig: +for graph in figure: graph.auto_scale(maintain_aspect=True) -fig.show() +figure.show() + +figure.canvas.set_logical_size(700, 560) +# NOTE: `if __name__ == "__main__"` is NOT how to use fastplotlib interactively +# please see our docs for using fastplotlib interactively in ipython and jupyter if __name__ == "__main__": print(__doc__) fpl.run() diff --git a/examples/desktop/screenshots/gridplot.png b/examples/desktop/screenshots/gridplot.png index 9e81fe8c6..6567c57da 100644 --- a/examples/desktop/screenshots/gridplot.png +++ b/examples/desktop/screenshots/gridplot.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:462a06e9c74dc9f0958aa265349dfac9c31d77a3ab3915f85596c85f2e7a6f3a -size 266056 +oid sha256:6499a000911de783b69a7958a1bf2b0290b5a3117fe14ade792baca95557b2a7 +size 263883 diff --git a/examples/desktop/screenshots/gridplot_non_square.png b/examples/desktop/screenshots/gridplot_non_square.png index b74be7065..847ed65cb 100644 --- a/examples/desktop/screenshots/gridplot_non_square.png +++ b/examples/desktop/screenshots/gridplot_non_square.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9ab4b1f8188824b81fe29b5c6ac7177734fb2b9958133e19f02919d1da98b96c -size 174978 +oid sha256:e4a0a002caf10e1e80ca0177bac4085e2f837ad3977f2546830acb42f0106f3f +size 173182 diff --git a/examples/desktop/screenshots/heatmap.png b/examples/desktop/screenshots/heatmap.png index ec6cf9955..1adcc1e1d 100644 --- a/examples/desktop/screenshots/heatmap.png +++ b/examples/desktop/screenshots/heatmap.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:754bd8713617bf61d1adf57b3e84c1257b038bf15412aa3c8bd466d1405086e7 -size 48524 +oid sha256:3e742d06167a49ec80253cb3984da88e6d623dc645f93bcfbd1a82966ba44a84 +size 40051 diff --git a/examples/desktop/screenshots/heatmap_cmap.png b/examples/desktop/screenshots/heatmap_cmap.png index c495cf72c..cee81dd30 100644 --- a/examples/desktop/screenshots/heatmap_cmap.png +++ b/examples/desktop/screenshots/heatmap_cmap.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d2ba0b76e982ceb1439c5ebaabfaf089ea9b09e50934718eaaa29d7492272196 -size 42746 +oid sha256:8863461569f5b89d1443e3051a5512f3987487fcb9e057215d2f030a180fa09f +size 97996 diff --git a/examples/desktop/screenshots/heatmap_data.png b/examples/desktop/screenshots/heatmap_data.png index 229d6c2cc..316a73753 100644 --- a/examples/desktop/screenshots/heatmap_data.png +++ b/examples/desktop/screenshots/heatmap_data.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a7160c4f034214f8052a6d88001dac706b0a85a5a4df076958ba1a176344b85a -size 53854 +oid sha256:a975179e82893dbb04e4674310761e7b02bb62ae6abb1b89397720bddf96ae5f +size 19084 diff --git a/examples/desktop/screenshots/heatmap_square.png b/examples/desktop/screenshots/heatmap_square.png deleted file mode 100644 index 00a01133e..000000000 --- a/examples/desktop/screenshots/heatmap_square.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:8d01171b2bd05b5c88df4312c303094fdede36b1cf930455ace6d1fb12d8eb36 -size 81274 diff --git a/examples/desktop/screenshots/heatmap_vmin_vmax.png b/examples/desktop/screenshots/heatmap_vmin_vmax.png index b028291f7..357683d82 100644 --- a/examples/desktop/screenshots/heatmap_vmin_vmax.png +++ b/examples/desktop/screenshots/heatmap_vmin_vmax.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:61c3754de3a7e6622ce1a77dbbf9bbd6ccfd3ccad3b1463b009bf93511258034 -size 44426 +oid sha256:9592f3724016db1b7431bc100b16bec175e197c111e7b442dc2255d51da3f5e8 +size 114957 diff --git a/examples/desktop/screenshots/heatmap_wide.png b/examples/desktop/screenshots/heatmap_wide.png deleted file mode 100644 index 927b933d6..000000000 --- a/examples/desktop/screenshots/heatmap_wide.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:271e0d769b608d0f34a153ab8b8353f1e5d127f239951fc407ccedd3eee5e2e5 -size 82687 diff --git a/examples/desktop/screenshots/image_cmap.png b/examples/desktop/screenshots/image_cmap.png index be16ba213..9ad74e927 100644 --- a/examples/desktop/screenshots/image_cmap.png +++ b/examples/desktop/screenshots/image_cmap.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:552a4d5141a5a87baaedd8a9d7d911dfdddee5792c024c77012665268af865e9 -size 189479 +oid sha256:009326a4c3605622980bf80b20cc8cc807fa610858d155304285a1a5d478b56c +size 187517 diff --git a/examples/desktop/screenshots/image_rgb.png b/examples/desktop/screenshots/image_rgb.png index 8d391f07c..50dfe6761 100644 --- a/examples/desktop/screenshots/image_rgb.png +++ b/examples/desktop/screenshots/image_rgb.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:55e76cea92eb34e1e25d730d2533a9a0d845921e78bc980708d320bb353a2d73 -size 218413 +oid sha256:c7e026664f350a4abd83297dc55a324a12ddcf8fd94625d26381d09a3fdd953b +size 216191 diff --git a/examples/desktop/screenshots/image_rgbvminvmax.png b/examples/desktop/screenshots/image_rgbvminvmax.png index eabe85d28..73707177c 100644 --- a/examples/desktop/screenshots/image_rgbvminvmax.png +++ b/examples/desktop/screenshots/image_rgbvminvmax.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7ee27d89170b30a3da7fe6d752961b30e17712d7905d8fa0686f9597debe68f9 -size 34620 +oid sha256:a6e18e9782514b3f525d8e7f8ed83699fae48fced2b633f3f1831e4b46751c44 +size 33627 diff --git a/examples/desktop/screenshots/image_simple.png b/examples/desktop/screenshots/image_simple.png index 853eb2f01..3857baf9d 100644 --- a/examples/desktop/screenshots/image_simple.png +++ b/examples/desktop/screenshots/image_simple.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e943bd3b1e00acaed274dd185f5362210e39330e0f541db9ceee489fa0a9a344 -size 189822 +oid sha256:1ab7ea5e0aa322c8b4249df30a80cbf3d7b55e0b7a0418f4bed99f81d14fdd7e +size 187665 diff --git a/examples/desktop/screenshots/image_vminvmax.png b/examples/desktop/screenshots/image_vminvmax.png index eabe85d28..73707177c 100644 --- a/examples/desktop/screenshots/image_vminvmax.png +++ b/examples/desktop/screenshots/image_vminvmax.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7ee27d89170b30a3da7fe6d752961b30e17712d7905d8fa0686f9597debe68f9 -size 34620 +oid sha256:a6e18e9782514b3f525d8e7f8ed83699fae48fced2b633f3f1831e4b46751c44 +size 33627 diff --git a/examples/desktop/screenshots/line.png b/examples/desktop/screenshots/line.png index 74cbae39a..c21a522e7 100644 --- a/examples/desktop/screenshots/line.png +++ b/examples/desktop/screenshots/line.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c8b4f4a08d1791b80d226c8c3099e37d33d8cdd7a400e4f85fb7072ee2aa3c2e -size 29121 +oid sha256:aa12348340af55e7472ceb9986a73be35b398761d057e579e199055fe6bcd9ef +size 27740 diff --git a/examples/desktop/screenshots/line_cmap.png b/examples/desktop/screenshots/line_cmap.png index 9cd93f05d..5e092c844 100644 --- a/examples/desktop/screenshots/line_cmap.png +++ b/examples/desktop/screenshots/line_cmap.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d60b4ff117298f973be892773dbfc620ac855c35ca7dea42437e20bf7fcef804 -size 31050 +oid sha256:7177ca064170e815870192afd9b01dd9e23332b7cbdd962592932dacac842a76 +size 29567 diff --git a/examples/desktop/screenshots/line_collection.png b/examples/desktop/screenshots/line_collection.png index bcfe85309..cc6bdcd72 100644 --- a/examples/desktop/screenshots/line_collection.png +++ b/examples/desktop/screenshots/line_collection.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7ca99b8d74fdf7f87b0f2fc5637c59c9090b91bef868e85ddd75dbcb1264f699 -size 95146 +oid sha256:8762da25c8d01ad135288e80c91750522e32c13d824862386ad3612f63e29cfc +size 93068 diff --git a/examples/desktop/screenshots/line_collection_cmap_values.png b/examples/desktop/screenshots/line_collection_cmap_values.png index b7fcdbcae..5dbe1cfda 100644 --- a/examples/desktop/screenshots/line_collection_cmap_values.png +++ b/examples/desktop/screenshots/line_collection_cmap_values.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:68090603856eb5b961092cf2ad2d89a1e9cfd7e31f6d089b3abad101874f65d4 -size 61032 +oid sha256:ba04a638118cc6a2dff2a253fa40fae099aa2acfe347570ff4a0044403a2a5b7 +size 59102 diff --git a/examples/desktop/screenshots/line_collection_cmap_values_qualitative.png b/examples/desktop/screenshots/line_collection_cmap_values_qualitative.png index 9f89a24cc..00fde0fdd 100644 --- a/examples/desktop/screenshots/line_collection_cmap_values_qualitative.png +++ b/examples/desktop/screenshots/line_collection_cmap_values_qualitative.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9cff99e5f9faf319909571778631453c043237f5c94eece6680b028a5d7a5ac2 -size 64149 +oid sha256:a592bf90017c25abf163936c4c475b8a96ac0775e450c8d7355011fe20bbfb22 +size 62416 diff --git a/examples/desktop/screenshots/line_collection_colors.png b/examples/desktop/screenshots/line_collection_colors.png index 7bb4152fd..cf7880a6c 100644 --- a/examples/desktop/screenshots/line_collection_colors.png +++ b/examples/desktop/screenshots/line_collection_colors.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a4aa17a65806300da65f4bbbfccb970d6a7207c4ca4d48b25615f627630fb484 -size 51174 +oid sha256:052a46223f73099eacf2b4672348c1110c14b22a85131b73561ac1feb8e9c796 +size 49693 diff --git a/examples/desktop/screenshots/line_collection_slicing.png b/examples/desktop/screenshots/line_collection_slicing.png index ba4170874..54d2a1098 100644 --- a/examples/desktop/screenshots/line_collection_slicing.png +++ b/examples/desktop/screenshots/line_collection_slicing.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:01090d611117fd0d2b3f9971e359871c9598a634a1829e74848b1c78a770d437 -size 131764 +oid sha256:07a02cb8a93527e913d35a0caed5dfc2feb01234d63b73ba3c3e244bbfda5e59 +size 129082 diff --git a/examples/desktop/screenshots/line_colorslice.png b/examples/desktop/screenshots/line_colorslice.png index 789265530..2bcf008a5 100644 --- a/examples/desktop/screenshots/line_colorslice.png +++ b/examples/desktop/screenshots/line_colorslice.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:25e87f566a667c98b54d4acdf115d16b486e047242b9ce8b141e5724b9d0a46a -size 33191 +oid sha256:69c4994a1259511a6480c2227a31472ab527f33f747cd44b9018f409e03ba1f1 +size 32353 diff --git a/examples/desktop/screenshots/line_dataslice.png b/examples/desktop/screenshots/line_dataslice.png index e55a6111e..395d2f1bf 100644 --- a/examples/desktop/screenshots/line_dataslice.png +++ b/examples/desktop/screenshots/line_dataslice.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e76275ea6f5719e16ff0ef3401dc33fe4b70c4c9010b3b673fca26812f33b9e8 -size 46400 +oid sha256:268a0e9a6a32e015466b7a72b8cb9675597db2b3212edaf818871a27602a751c +size 44986 diff --git a/examples/desktop/screenshots/line_present_scaling.png b/examples/desktop/screenshots/line_present_scaling.png new file mode 100644 index 000000000..ba7142106 --- /dev/null +++ b/examples/desktop/screenshots/line_present_scaling.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:06f7dd45eb495fecfcf46478c6430a658640ceb2855c4797bc184cf4134571e3 +size 20180 diff --git a/examples/desktop/screenshots/line_stack.png b/examples/desktop/screenshots/line_stack.png index 29d941fd4..edaae0d0a 100644 --- a/examples/desktop/screenshots/line_stack.png +++ b/examples/desktop/screenshots/line_stack.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:73226917c233f3fd3d7ec0b40d5a7ded904d275c871242dc0578bddf4c19d0bd -size 93687 +oid sha256:2b3ae8275e536669dfabdc8c3b715d1ca9c73f17062ab1f90f9650bd86b1c4f7 +size 91285 diff --git a/examples/desktop/screenshots/multigraphic_gridplot.png b/examples/desktop/screenshots/multigraphic_gridplot.png new file mode 100644 index 000000000..e814eadde --- /dev/null +++ b/examples/desktop/screenshots/multigraphic_gridplot.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e879a39032759006685eff1d5c2331a66b9126b13bc148734c3622bd6cdc68a7 +size 163902 diff --git a/examples/desktop/screenshots/scatter.png b/examples/desktop/screenshots/scatter.png index 94fb858e1..195c5ae64 100644 --- a/examples/desktop/screenshots/scatter.png +++ b/examples/desktop/screenshots/scatter.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4fc16a1ba74a8eca99a2fc7937f8896ca93207b99e231bc4f53845b0d2bdaed7 -size 15283 +oid sha256:b0f6c3d3b5e7216cc17c70ad3ff114092b1556fb9dcd171ae737f28d52ce51c9 +size 106634 diff --git a/examples/desktop/screenshots/scatter_cmap.png b/examples/desktop/screenshots/scatter_cmap.png index 560f1942d..55442aceb 100644 --- a/examples/desktop/screenshots/scatter_cmap.png +++ b/examples/desktop/screenshots/scatter_cmap.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9479bb3995bd145a163a2f25592a4c85c52c663d33381efee7743ffc1f16aef1 -size 32894 +oid sha256:0655f8f5f1fdeecbd5d097cf90a4776dd8125a16d0e8edb86aa37f77daba0d9b +size 107892 diff --git a/examples/desktop/screenshots/scatter_cmap_iris.png b/examples/desktop/screenshots/scatter_cmap_iris.png new file mode 100644 index 000000000..eb8802c0e --- /dev/null +++ b/examples/desktop/screenshots/scatter_cmap_iris.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:088579410d42768e68d7e7b298d2ed9e409eaff28ba5a2dd2ec8283e5994b862 +size 31599 diff --git a/examples/desktop/screenshots/scatter_colorslice.png b/examples/desktop/screenshots/scatter_colorslice.png index cede76dfd..7c43dd6cb 100644 --- a/examples/desktop/screenshots/scatter_colorslice.png +++ b/examples/desktop/screenshots/scatter_colorslice.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f7956e02d6c231bab091adb4ce9102ad4943050ccf171a0594a899a381880771 -size 14712 +oid sha256:d3319afffb554eb624d9bba5b87daf331b619ac5ec5006f13e479b613f43ca32 +size 72083 diff --git a/examples/desktop/screenshots/scatter_colorslice_iris.png b/examples/desktop/screenshots/scatter_colorslice_iris.png new file mode 100644 index 000000000..8692f53b3 --- /dev/null +++ b/examples/desktop/screenshots/scatter_colorslice_iris.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f21ab8499384298a74db3e5fbb5aa0113f714d6f77e1e3d614da3d278a2c7a78 +size 13478 diff --git a/examples/desktop/screenshots/scatter_dataslice_iris.png b/examples/desktop/screenshots/scatter_dataslice_iris.png new file mode 100644 index 000000000..c0c6aa5e7 --- /dev/null +++ b/examples/desktop/screenshots/scatter_dataslice_iris.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2032ce0b3c39f35e9dc37bc54fac016dc43aef716cfc65e6d65fd7fc977e74cf +size 14473 diff --git a/examples/desktop/screenshots/scatter_iris.png b/examples/desktop/screenshots/scatter_iris.png new file mode 100644 index 000000000..aa1d2b743 --- /dev/null +++ b/examples/desktop/screenshots/scatter_iris.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:320a02df6de51235b1912b26df4ea7633e9513df4d4034605a4e00724ceb57dc +size 14131 diff --git a/examples/desktop/screenshots/scatter_present.png b/examples/desktop/screenshots/scatter_present.png new file mode 100644 index 000000000..08bc610b3 --- /dev/null +++ b/examples/desktop/screenshots/scatter_present.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bd072918f21ed0ce4ea4e1f4499ec1ff66d867cfdc0ecd6b3ed8092141cd348e +size 14195 diff --git a/examples/desktop/screenshots/scatter_size.png b/examples/desktop/screenshots/scatter_size.png index 056d2a531..2d4e559fe 100644 --- a/examples/desktop/screenshots/scatter_size.png +++ b/examples/desktop/screenshots/scatter_size.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5ccfbac94de6ba122ea420dce58e4a576b2d58d9282aaf8d64de399278df57b3 -size 38076 +oid sha256:a96a5da476b22f3ebd42ec927c3d1c1dee4dc20f261a97feaced69ed4142c645 +size 35484 diff --git a/examples/notebooks/screenshots/nb-image-widget-movie-single-0-reset.png b/examples/notebooks/screenshots/nb-image-widget-movie-single-0-reset.png index 8572f6472..1369669e6 100644 --- a/examples/notebooks/screenshots/nb-image-widget-movie-single-0-reset.png +++ b/examples/notebooks/screenshots/nb-image-widget-movie-single-0-reset.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ff83d6bab26b9bbccf66ed100764ffdfc7556f4cb04f0b85f50c2497ba0ab257 -size 134419 +oid sha256:7049dc9344a8d51bcfc3ba7f96a25c84db27f5072f14e8845cd2f01c3f4f5310 +size 133683 diff --git a/examples/notebooks/screenshots/nb-image-widget-movie-single-0.png b/examples/notebooks/screenshots/nb-image-widget-movie-single-0.png index 8572f6472..1369669e6 100644 --- a/examples/notebooks/screenshots/nb-image-widget-movie-single-0.png +++ b/examples/notebooks/screenshots/nb-image-widget-movie-single-0.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ff83d6bab26b9bbccf66ed100764ffdfc7556f4cb04f0b85f50c2497ba0ab257 -size 134419 +oid sha256:7049dc9344a8d51bcfc3ba7f96a25c84db27f5072f14e8845cd2f01c3f4f5310 +size 133683 diff --git a/examples/notebooks/screenshots/nb-image-widget-movie-single-279.png b/examples/notebooks/screenshots/nb-image-widget-movie-single-279.png index e241ce5a6..5817cd299 100644 --- a/examples/notebooks/screenshots/nb-image-widget-movie-single-279.png +++ b/examples/notebooks/screenshots/nb-image-widget-movie-single-279.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:337a22f11649b350f7f47d68d82be165633caeb7f8cef581e50f981d6ec0c52c -size 169615 +oid sha256:1ca687c654679ee9d47a053196fb65d92105ed6cce76f0d2d1775582053e7b07 +size 168750 diff --git a/examples/notebooks/screenshots/nb-image-widget-movie-single-50-window-max-33.png b/examples/notebooks/screenshots/nb-image-widget-movie-single-50-window-max-33.png index b827fc536..2f2e310d0 100644 --- a/examples/notebooks/screenshots/nb-image-widget-movie-single-50-window-max-33.png +++ b/examples/notebooks/screenshots/nb-image-widget-movie-single-50-window-max-33.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:22ff3ed815fcbe8bc95c321c806a4b42536e7014209cd43ac597a5ccefd8b9c6 -size 149261 +oid sha256:dd51516d18f550f62b9f2c989cfae82d267eb6de1d8757cf44949bb41c3b13c2 +size 148408 diff --git a/examples/notebooks/screenshots/nb-image-widget-movie-single-50-window-mean-13.png b/examples/notebooks/screenshots/nb-image-widget-movie-single-50-window-mean-13.png index d37f44a3a..39a4f1274 100644 --- a/examples/notebooks/screenshots/nb-image-widget-movie-single-50-window-mean-13.png +++ b/examples/notebooks/screenshots/nb-image-widget-movie-single-50-window-mean-13.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:11143f73a297d0b59c92db1b115ceac1bc1304135a9925302e616a4fd3669b25 -size 125012 +oid sha256:9613c10e63261ddbcf3274a9804bdf3a1be0734c1190720663b01f8b2ae48389 +size 124532 diff --git a/examples/notebooks/screenshots/nb-image-widget-movie-single-50-window-mean-33.png b/examples/notebooks/screenshots/nb-image-widget-movie-single-50-window-mean-33.png index 46d3fa9b3..b9feacb82 100644 --- a/examples/notebooks/screenshots/nb-image-widget-movie-single-50-window-mean-33.png +++ b/examples/notebooks/screenshots/nb-image-widget-movie-single-50-window-mean-33.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7438018c3b55d423f57c42a9d03c1af6d5de168a2dfa5df9d535ef2ae1f1c8e9 -size 113981 +oid sha256:8fa86b26f11bd9655c60e43ddd88ada618cea1ba06b3dcddcb408569c09a7750 +size 113897 diff --git a/examples/notebooks/screenshots/nb-image-widget-movie-single-50-window-reset.png b/examples/notebooks/screenshots/nb-image-widget-movie-single-50-window-reset.png index 6146a7985..ae280a970 100644 --- a/examples/notebooks/screenshots/nb-image-widget-movie-single-50-window-reset.png +++ b/examples/notebooks/screenshots/nb-image-widget-movie-single-50-window-reset.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9b57a1e6640de9471540fa4d86faadb706d6de8cc1de6a29fb65d709b91eef1e -size 146429 +oid sha256:aca573ba6dcda8ad8f018a96790b1b1aa5da25fe9584a57c2c946e1cf2df9e5c +size 145275 diff --git a/examples/notebooks/screenshots/nb-image-widget-movie-single-50.png b/examples/notebooks/screenshots/nb-image-widget-movie-single-50.png index 6146a7985..ae280a970 100644 --- a/examples/notebooks/screenshots/nb-image-widget-movie-single-50.png +++ b/examples/notebooks/screenshots/nb-image-widget-movie-single-50.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9b57a1e6640de9471540fa4d86faadb706d6de8cc1de6a29fb65d709b91eef1e -size 146429 +oid sha256:aca573ba6dcda8ad8f018a96790b1b1aa5da25fe9584a57c2c946e1cf2df9e5c +size 145275 diff --git a/examples/notebooks/screenshots/nb-lines-3d.png b/examples/notebooks/screenshots/nb-lines-3d.png index a3b75de58..6c19c965c 100644 --- a/examples/notebooks/screenshots/nb-lines-3d.png +++ b/examples/notebooks/screenshots/nb-lines-3d.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:677f544d93cf7a733003c38666f555f0b598d41b92681dd2f09e6c11faa4aed0 -size 14186 +oid sha256:c6ab8d74c980a02505d5d626d170b60847ce2b733ea5b47278a1f48228011a38 +size 14181 diff --git a/examples/notebooks/screenshots/nb-lines-cmap-jet-values-cosine.png b/examples/notebooks/screenshots/nb-lines-cmap-jet-values-cosine.png index 0d9ff5729..f75d83ba4 100644 --- a/examples/notebooks/screenshots/nb-lines-cmap-jet-values-cosine.png +++ b/examples/notebooks/screenshots/nb-lines-cmap-jet-values-cosine.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0d981d57d7905879ab68af95da84d2cf530a9a40dc4d0ffb138119a11f4966be -size 11808 +oid sha256:d2b87297587b5f76062132370fdf7e8318cbde68d38d12a0179548f9790c2af8 +size 11765 diff --git a/examples/notebooks/screenshots/nb-lines-cmap-jet-values.png b/examples/notebooks/screenshots/nb-lines-cmap-jet-values.png index dbcbf1e7f..a8229cfc8 100644 --- a/examples/notebooks/screenshots/nb-lines-cmap-jet-values.png +++ b/examples/notebooks/screenshots/nb-lines-cmap-jet-values.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:706bffa485dd7994a70602ef2076aee79e1206dd508fbac903549fce526087b6 -size 13095 +oid sha256:99f6a7a9a1399aaf8d1070812c41f03f80ba41812a1acf9d4bdd17641bc589f4 +size 13038 diff --git a/examples/notebooks/screenshots/nb-lines-cmap-jet.png b/examples/notebooks/screenshots/nb-lines-cmap-jet.png index 6a3ae0c1c..ccc897f66 100644 --- a/examples/notebooks/screenshots/nb-lines-cmap-jet.png +++ b/examples/notebooks/screenshots/nb-lines-cmap-jet.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cfd3f55e1671ac1fa45a8eb26aeb425ccb8d1ac033f5766f4002fee4380b2a77 -size 11174 +oid sha256:cac3d7cb23d7ec207cdf7fd7032037c7749e30563754d6c2cac50d67a7731a2d +size 11086 diff --git a/examples/notebooks/screenshots/nb-lines-cmap-tab-10.png b/examples/notebooks/screenshots/nb-lines-cmap-tab-10.png index 9bb368e0e..8ddc44ead 100644 --- a/examples/notebooks/screenshots/nb-lines-cmap-tab-10.png +++ b/examples/notebooks/screenshots/nb-lines-cmap-tab-10.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:338dbcdf1a87266eee33367bfa08bcaec7eac42ef2dd928bbc699b3a0412ebaf -size 9889 +oid sha256:d38a2c27e5b598883c40b44e45b347f029a7369b638bc4111ef2d69978245378 +size 9943 diff --git a/examples/notebooks/screenshots/nb-lines-cmap-viridis-values.png b/examples/notebooks/screenshots/nb-lines-cmap-viridis-values.png index 23137bdf3..6bf270874 100644 --- a/examples/notebooks/screenshots/nb-lines-cmap-viridis-values.png +++ b/examples/notebooks/screenshots/nb-lines-cmap-viridis-values.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e36a7a74ac39dac5ac96a4e7c8e56990794ab1cda1b8ee5276087e9814dd1696 -size 10100 +oid sha256:d0ead67733470d6afcb2228645ee1a056acd525d369d74c16b1054783d6ab4f5 +size 10155 diff --git a/examples/notebooks/screenshots/nb-lines-cmap-viridis.png b/examples/notebooks/screenshots/nb-lines-cmap-viridis.png index 2fcd4749c..4f99e5fb5 100644 --- a/examples/notebooks/screenshots/nb-lines-cmap-viridis.png +++ b/examples/notebooks/screenshots/nb-lines-cmap-viridis.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b12ee5e31f64415b57536c59587b91c7a3a7c74e95be3459d8036a43d073d7db -size 13821 +oid sha256:15f4decaa6ba08f09a988771c5dc433629cb9e3bccfa08f820268366a3ff06a6 +size 13761 diff --git a/examples/notebooks/screenshots/nb-lines-cmap-white.png b/examples/notebooks/screenshots/nb-lines-cmap-white.png index 397b5fc94..85e43aa5e 100644 --- a/examples/notebooks/screenshots/nb-lines-cmap-white.png +++ b/examples/notebooks/screenshots/nb-lines-cmap-white.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6239f23d3b5c1745879f5706352abb905d5638522b171776bff051e511426c2f -size 8359 +oid sha256:8a623f31d28533cc6052143856d0ef2e827b42891397fa4ac20d358f44d751d7 +size 8332 diff --git a/examples/notebooks/screenshots/nb-lines-colors.png b/examples/notebooks/screenshots/nb-lines-colors.png index 149321e52..ab50dfaaf 100644 --- a/examples/notebooks/screenshots/nb-lines-colors.png +++ b/examples/notebooks/screenshots/nb-lines-colors.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9a5e99c5a872d9bbf8dd909498459ddee7f22f08d3fe3cd68b3ea57c105ab51b -size 27634 +oid sha256:0cbcafef567bff33762d64bcf6f83c7a1982015ea21d508b1e6564cb0d8435d2 +size 27420 diff --git a/examples/notebooks/screenshots/nb-lines-data.png b/examples/notebooks/screenshots/nb-lines-data.png index 9ed38b1f5..88f1001ad 100644 --- a/examples/notebooks/screenshots/nb-lines-data.png +++ b/examples/notebooks/screenshots/nb-lines-data.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cc0d3d6819ce0d9f5d531276bbde0397ef35e3084ac1f9b3575f0209eea07456 -size 39512 +oid sha256:f62dc16cd1ea212cf29974a64e6ad2dd3d7ae10d7b3eef573c25316bbe89b097 +size 38740 diff --git a/examples/notebooks/screenshots/nb-lines-underlay.png b/examples/notebooks/screenshots/nb-lines-underlay.png index e7f6aeb0c..f9650e253 100644 --- a/examples/notebooks/screenshots/nb-lines-underlay.png +++ b/examples/notebooks/screenshots/nb-lines-underlay.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8ed174c362c7e7c491eba02b32102f59423af41537577c694fdcd54d69c065b3 -size 50422 +oid sha256:ec7881cc0084b0ea2182f8d4f6f23388a4d48b88dbbf9c06b9dc2fa8125989b7 +size 49553 diff --git a/examples/notebooks/screenshots/nb-lines.png b/examples/notebooks/screenshots/nb-lines.png index 603f4a8fc..7df6ea14f 100644 --- a/examples/notebooks/screenshots/nb-lines.png +++ b/examples/notebooks/screenshots/nb-lines.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:eeef2c47e7dde62038307fa7929a306801bf8b708fbcf1062ed9c751727bfb2b -size 24300 +oid sha256:b925337438aa24f0179a8b5c1688e154679fa002402a16746b6bb3cd6684af1e +size 24246 diff --git a/examples/tests/test_examples.py b/examples/tests/test_examples.py index b8369e368..c08df9005 100644 --- a/examples/tests/test_examples.py +++ b/examples/tests/test_examples.py @@ -69,7 +69,7 @@ def test_example_screenshots(module, force_offscreen): example = importlib.import_module(module_name) # render a frame - img = np.asarray(example.fig.renderer.target.draw()) + img = np.asarray(example.figure.renderer.target.draw()) # check if _something_ was rendered assert img is not None and img.size > 0 diff --git a/examples/tests/testutils.py b/examples/tests/testutils.py index 1a95e249f..0e4cd2e1b 100644 --- a/examples/tests/testutils.py +++ b/examples/tests/testutils.py @@ -22,7 +22,8 @@ "scatter/*.py", "line/*.py", "line_collection/*.py", - "gridplot/*.py" + "gridplot/*.py", + "misc/*.py" ] diff --git a/fastplotlib/layouts/_figure.py b/fastplotlib/layouts/_figure.py index 2c157db8f..17bb28095 100644 --- a/fastplotlib/layouts/_figure.py +++ b/fastplotlib/layouts/_figure.py @@ -475,11 +475,6 @@ def show( _maintain_aspect = maintain_aspect subplot.auto_scale(maintain_aspect=_maintain_aspect, zoom=0.95) - # used for generating images in docs using nbsphinx - if "NB_SNAPSHOT" in os.environ.keys(): - if os.environ["NB_SNAPSHOT"] == "1": - return self.canvas.snapshot() - # return the appropriate OutputContext based on the current canvas if self.canvas.__class__.__name__ == "JupyterWgpuCanvas": from .output.jupyter_output import ( diff --git a/setup.py b/setup.py index d75edf956..ef79e4774 100644 --- a/setup.py +++ b/setup.py @@ -12,6 +12,7 @@ extras_require = { "docs": [ "sphinx", + "sphinx-gallery", "furo", "glfw", "jupyter-rfb>=0.4.1", # required so ImageWidget docs show up @@ -21,6 +22,9 @@ "pandoc", "jupyterlab", "sidecar", + "imageio", + "matplotlib", + "scikit-learn" ], "notebook": [ "jupyterlab", From 3f5cf9d41a65077c6d0058523cfb0ad153fbf705 Mon Sep 17 00:00:00 2001 From: Kushal Kolar Date: Tue, 18 Jun 2024 18:07:35 -0400 Subject: [PATCH 63/66] Fix off by one error (#530) * ndarray.ptp -> np.ptp for numpy v2 * fix off by one error in Image feature, add test * fix off by one error in Image feature, add test * add example test for small image to prevent off by one errors * update screenshots * update texture array test * update multi graphic gallery example, do not use for tests --- .../desktop/gridplot/multigraphic_gridplot.py | 10 +++--- examples/desktop/image/image_small.py | 33 +++++++++++++++++++ examples/desktop/screenshots/gridplot.png | 4 +-- .../screenshots/gridplot_non_square.png | 4 +-- examples/desktop/screenshots/heatmap.png | 4 +-- examples/desktop/screenshots/heatmap_cmap.png | 3 -- examples/desktop/screenshots/heatmap_data.png | 3 -- .../desktop/screenshots/heatmap_vmin_vmax.png | 3 -- examples/desktop/screenshots/image_cmap.png | 4 +-- examples/desktop/screenshots/image_rgb.png | 4 +-- .../desktop/screenshots/image_rgbvminvmax.png | 4 +-- examples/desktop/screenshots/image_simple.png | 4 +-- examples/desktop/screenshots/image_small.png | 3 ++ .../desktop/screenshots/image_vminvmax.png | 4 +-- examples/desktop/screenshots/line.png | 4 +-- examples/desktop/screenshots/line_cmap.png | 4 +-- .../desktop/screenshots/line_collection.png | 4 +-- .../line_collection_cmap_values.png | 4 +-- ...ine_collection_cmap_values_qualitative.png | 4 +-- .../screenshots/line_collection_colors.png | 4 +-- .../screenshots/line_collection_slicing.png | 4 +-- .../desktop/screenshots/line_colorslice.png | 4 +-- .../desktop/screenshots/line_dataslice.png | 4 +-- .../screenshots/line_present_scaling.png | 3 -- examples/desktop/screenshots/line_stack.png | 4 +-- .../screenshots/multigraphic_gridplot.png | 3 -- examples/desktop/screenshots/scatter.png | 3 -- examples/desktop/screenshots/scatter_cmap.png | 3 -- .../desktop/screenshots/scatter_cmap_iris.png | 4 +-- .../screenshots/scatter_colorslice.png | 3 -- .../screenshots/scatter_colorslice_iris.png | 4 +-- .../desktop/screenshots/scatter_dataslice.png | 3 -- .../screenshots/scatter_dataslice_iris.png | 4 +-- examples/desktop/screenshots/scatter_iris.png | 4 +-- .../desktop/screenshots/scatter_present.png | 3 -- examples/desktop/screenshots/scatter_size.png | 4 +-- .../notebooks/screenshots/nb-astronaut.png | 4 +-- .../screenshots/nb-astronaut_RGB.png | 4 +-- examples/notebooks/screenshots/nb-camera.png | 4 +-- .../nb-image-widget-movie-set_data.png | 4 +-- .../nb-image-widget-movie-single-0-reset.png | 4 +-- .../nb-image-widget-movie-single-0.png | 4 +-- .../nb-image-widget-movie-single-279.png | 4 +-- ...e-widget-movie-single-50-window-max-33.png | 4 +-- ...-widget-movie-single-50-window-mean-13.png | 4 +-- ...-widget-movie-single-50-window-mean-33.png | 4 +-- ...ge-widget-movie-single-50-window-reset.png | 4 +-- .../nb-image-widget-movie-single-50.png | 4 +-- .../nb-image-widget-single-gnuplot2.png | 4 +-- .../screenshots/nb-image-widget-single.png | 4 +-- ...et-zfish-frame-50-frame-apply-gaussian.png | 4 +-- ...idget-zfish-frame-50-frame-apply-reset.png | 4 +-- ...ge-widget-zfish-frame-50-max-window-13.png | 4 +-- ...e-widget-zfish-frame-50-mean-window-13.png | 4 +-- ...ge-widget-zfish-frame-50-mean-window-5.png | 4 +-- .../nb-image-widget-zfish-frame-50.png | 4 +-- .../nb-image-widget-zfish-frame-99.png | 4 +-- ...ish-grid-frame-50-frame-apply-gaussian.png | 4 +-- ...-zfish-grid-frame-50-frame-apply-reset.png | 4 +-- ...dget-zfish-grid-frame-50-max-window-13.png | 4 +-- ...get-zfish-grid-frame-50-mean-window-13.png | 4 +-- ...dget-zfish-grid-frame-50-mean-window-5.png | 4 +-- .../nb-image-widget-zfish-grid-frame-50.png | 4 +-- .../nb-image-widget-zfish-grid-frame-99.png | 4 +-- ...e-widget-zfish-grid-init-mean-window-5.png | 4 +-- ...fish-grid-set_data-reset-indices-false.png | 4 +-- ...zfish-grid-set_data-reset-indices-true.png | 4 +-- ...-image-widget-zfish-init-mean-window-5.png | 4 +-- ...dget-zfish-mixed-rgb-cockatoo-frame-50.png | 4 +-- ...dget-zfish-mixed-rgb-cockatoo-set-data.png | 4 +-- ...get-zfish-mixed-rgb-cockatoo-windowrgb.png | 4 +-- .../notebooks/screenshots/nb-imagewidget.png | 3 -- .../notebooks/screenshots/nb-lines-3d.png | 4 +-- .../nb-lines-cmap-jet-values-cosine.png | 4 +-- .../screenshots/nb-lines-cmap-jet-values.png | 4 +-- .../screenshots/nb-lines-cmap-jet.png | 4 +-- .../screenshots/nb-lines-cmap-tab-10.png | 4 +-- .../nb-lines-cmap-viridis-values.png | 4 +-- .../screenshots/nb-lines-cmap-viridis.png | 4 +-- .../screenshots/nb-lines-cmap-white.png | 4 +-- .../notebooks/screenshots/nb-lines-colors.png | 4 +-- .../notebooks/screenshots/nb-lines-data.png | 4 +-- .../screenshots/nb-lines-underlay.png | 4 +-- examples/notebooks/screenshots/nb-lines.png | 4 +-- fastplotlib/graphics/_features/_image.py | 4 +-- tests/test_image_graphic.py | 5 +++ tests/test_texture_array.py | 4 +-- 87 files changed, 190 insertions(+), 182 deletions(-) create mode 100644 examples/desktop/image/image_small.py delete mode 100644 examples/desktop/screenshots/heatmap_cmap.png delete mode 100644 examples/desktop/screenshots/heatmap_data.png delete mode 100644 examples/desktop/screenshots/heatmap_vmin_vmax.png create mode 100644 examples/desktop/screenshots/image_small.png delete mode 100644 examples/desktop/screenshots/line_present_scaling.png delete mode 100644 examples/desktop/screenshots/multigraphic_gridplot.png delete mode 100644 examples/desktop/screenshots/scatter.png delete mode 100644 examples/desktop/screenshots/scatter_cmap.png delete mode 100644 examples/desktop/screenshots/scatter_colorslice.png delete mode 100644 examples/desktop/screenshots/scatter_dataslice.png delete mode 100644 examples/desktop/screenshots/scatter_present.png delete mode 100644 examples/notebooks/screenshots/nb-imagewidget.png diff --git a/examples/desktop/gridplot/multigraphic_gridplot.py b/examples/desktop/gridplot/multigraphic_gridplot.py index 3d8dbe4a8..edb0aaafd 100644 --- a/examples/desktop/gridplot/multigraphic_gridplot.py +++ b/examples/desktop/gridplot/multigraphic_gridplot.py @@ -5,7 +5,7 @@ Example showing a Figure with multiple subplots and multiple graphic types. """ -# test_example = true +# test_example = false # sphinx_gallery_pygfx_docs = 'screenshot' import fastplotlib as fpl @@ -58,9 +58,9 @@ def make_circle(center, radius: float, n_points: int = 75) -> np.ndarray: ] # add an image to overlay the circles on -img2 = np.ones((60, 60)) +img2 = iio.imread("imageio:coins.png")[10::5, 5::5] -figure["circles"].add_image(data=img2) +figure["circles"].add_image(data=img2, cmap="gray") # add the circles to the figure figure["circles"].add_line_collection( @@ -101,8 +101,8 @@ def make_circle(center, radius: float, n_points: int = 75) -> np.ndarray: gaussian_cloud2 = np.random.multivariate_normal(mean, covariance, n_points) # add the scatter graphics to the figure -figure["scatter"].add_scatter(data=gaussian_cloud, sizes=1, cmap="jet") -figure["scatter"].add_scatter(data=gaussian_cloud2, colors="r", sizes=1) +figure["scatter"].add_scatter(data=gaussian_cloud, sizes=2, cmap="jet") +figure["scatter"].add_scatter(data=gaussian_cloud2, colors="r", sizes=2) figure.show() diff --git a/examples/desktop/image/image_small.py b/examples/desktop/image/image_small.py new file mode 100644 index 000000000..95c263a28 --- /dev/null +++ b/examples/desktop/image/image_small.py @@ -0,0 +1,33 @@ +""" +Small Image +=========== + +Test image to verify dims +""" + +import numpy as np + +# test_example = true +# sphinx_gallery_pygfx_docs = 'hidden' + +import fastplotlib as fpl + +figure = fpl.Figure() + +data = np.array( + [[0, 1, 2], + [3, 4, 5]] +) +image_graphic = figure[0, 0].add_image(data) + +figure.show() + +figure.canvas.set_logical_size(700, 560) + +figure[0, 0].auto_scale() + +# NOTE: `if __name__ == "__main__"` is NOT how to use fastplotlib interactively +# please see our docs for using fastplotlib interactively in ipython and jupyter +if __name__ == "__main__": + print(__doc__) + fpl.run() diff --git a/examples/desktop/screenshots/gridplot.png b/examples/desktop/screenshots/gridplot.png index 6567c57da..315958673 100644 --- a/examples/desktop/screenshots/gridplot.png +++ b/examples/desktop/screenshots/gridplot.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6499a000911de783b69a7958a1bf2b0290b5a3117fe14ade792baca95557b2a7 -size 263883 +oid sha256:d43e6972bf76aa2de400616bde4275cd05d3a945475742ec7f63f7658628292b +size 264437 diff --git a/examples/desktop/screenshots/gridplot_non_square.png b/examples/desktop/screenshots/gridplot_non_square.png index 847ed65cb..689585b40 100644 --- a/examples/desktop/screenshots/gridplot_non_square.png +++ b/examples/desktop/screenshots/gridplot_non_square.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e4a0a002caf10e1e80ca0177bac4085e2f837ad3977f2546830acb42f0106f3f -size 173182 +oid sha256:703285790dc96500a7a376f6e78953c943643f4ecf3102182072c2bd0bf8190c +size 173753 diff --git a/examples/desktop/screenshots/heatmap.png b/examples/desktop/screenshots/heatmap.png index 1adcc1e1d..0514daf94 100644 --- a/examples/desktop/screenshots/heatmap.png +++ b/examples/desktop/screenshots/heatmap.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3e742d06167a49ec80253cb3984da88e6d623dc645f93bcfbd1a82966ba44a84 -size 40051 +oid sha256:03b3ab1fc8aa602eb94beed1f5fa5712452ee802bb3230c4fd066d073bdd4ad2 +size 40100 diff --git a/examples/desktop/screenshots/heatmap_cmap.png b/examples/desktop/screenshots/heatmap_cmap.png deleted file mode 100644 index cee81dd30..000000000 --- a/examples/desktop/screenshots/heatmap_cmap.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:8863461569f5b89d1443e3051a5512f3987487fcb9e057215d2f030a180fa09f -size 97996 diff --git a/examples/desktop/screenshots/heatmap_data.png b/examples/desktop/screenshots/heatmap_data.png deleted file mode 100644 index 316a73753..000000000 --- a/examples/desktop/screenshots/heatmap_data.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:a975179e82893dbb04e4674310761e7b02bb62ae6abb1b89397720bddf96ae5f -size 19084 diff --git a/examples/desktop/screenshots/heatmap_vmin_vmax.png b/examples/desktop/screenshots/heatmap_vmin_vmax.png deleted file mode 100644 index 357683d82..000000000 --- a/examples/desktop/screenshots/heatmap_vmin_vmax.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:9592f3724016db1b7431bc100b16bec175e197c111e7b442dc2255d51da3f5e8 -size 114957 diff --git a/examples/desktop/screenshots/image_cmap.png b/examples/desktop/screenshots/image_cmap.png index 9ad74e927..91124db6a 100644 --- a/examples/desktop/screenshots/image_cmap.png +++ b/examples/desktop/screenshots/image_cmap.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:009326a4c3605622980bf80b20cc8cc807fa610858d155304285a1a5d478b56c -size 187517 +oid sha256:f18a55da8cede25dbb77b18e8cf374d158a66b823d029714983218e55ee68249 +size 187688 diff --git a/examples/desktop/screenshots/image_rgb.png b/examples/desktop/screenshots/image_rgb.png index 50dfe6761..8ae39eaad 100644 --- a/examples/desktop/screenshots/image_rgb.png +++ b/examples/desktop/screenshots/image_rgb.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c7e026664f350a4abd83297dc55a324a12ddcf8fd94625d26381d09a3fdd953b -size 216191 +oid sha256:3851bea9ee908a460750b40a0a5709aff1b28afa6adf11c9ad2ed8239958caa4 +size 216343 diff --git a/examples/desktop/screenshots/image_rgbvminvmax.png b/examples/desktop/screenshots/image_rgbvminvmax.png index 73707177c..478ce40fe 100644 --- a/examples/desktop/screenshots/image_rgbvminvmax.png +++ b/examples/desktop/screenshots/image_rgbvminvmax.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a6e18e9782514b3f525d8e7f8ed83699fae48fced2b633f3f1831e4b46751c44 -size 33627 +oid sha256:2ec8ddd362197ba802f8381d5baea226dc30689eee5e5dc744c2da710f0b3482 +size 33860 diff --git a/examples/desktop/screenshots/image_simple.png b/examples/desktop/screenshots/image_simple.png index 3857baf9d..c60293498 100644 --- a/examples/desktop/screenshots/image_simple.png +++ b/examples/desktop/screenshots/image_simple.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1ab7ea5e0aa322c8b4249df30a80cbf3d7b55e0b7a0418f4bed99f81d14fdd7e -size 187665 +oid sha256:216791f48cee8ddb9979ecc8b7b7435c0fe22c2734148c25314f1827a5c9ad66 +size 187868 diff --git a/examples/desktop/screenshots/image_small.png b/examples/desktop/screenshots/image_small.png new file mode 100644 index 000000000..cda3a2584 --- /dev/null +++ b/examples/desktop/screenshots/image_small.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3f2af0ed16ec82842ad9d45d5a8b6189e77a2f2f8adb21dd82bc1636979cd2c7 +size 2325 diff --git a/examples/desktop/screenshots/image_vminvmax.png b/examples/desktop/screenshots/image_vminvmax.png index 73707177c..478ce40fe 100644 --- a/examples/desktop/screenshots/image_vminvmax.png +++ b/examples/desktop/screenshots/image_vminvmax.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a6e18e9782514b3f525d8e7f8ed83699fae48fced2b633f3f1831e4b46751c44 -size 33627 +oid sha256:2ec8ddd362197ba802f8381d5baea226dc30689eee5e5dc744c2da710f0b3482 +size 33860 diff --git a/examples/desktop/screenshots/line.png b/examples/desktop/screenshots/line.png index c21a522e7..605540225 100644 --- a/examples/desktop/screenshots/line.png +++ b/examples/desktop/screenshots/line.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:aa12348340af55e7472ceb9986a73be35b398761d057e579e199055fe6bcd9ef -size 27740 +oid sha256:d7f3736d4464cfd942e87d21be1a18d09f5d0d239a7e1c7679e918dcc5c9331c +size 26701 diff --git a/examples/desktop/screenshots/line_cmap.png b/examples/desktop/screenshots/line_cmap.png index 5e092c844..cab91220f 100644 --- a/examples/desktop/screenshots/line_cmap.png +++ b/examples/desktop/screenshots/line_cmap.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7177ca064170e815870192afd9b01dd9e23332b7cbdd962592932dacac842a76 -size 29567 +oid sha256:1f154346cffbaa0957a9986d8b7beef417b66ef0cec7dbed3c20780d91425567 +size 29231 diff --git a/examples/desktop/screenshots/line_collection.png b/examples/desktop/screenshots/line_collection.png index cc6bdcd72..f3fb5052b 100644 --- a/examples/desktop/screenshots/line_collection.png +++ b/examples/desktop/screenshots/line_collection.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8762da25c8d01ad135288e80c91750522e32c13d824862386ad3612f63e29cfc -size 93068 +oid sha256:ca08ce57a1cf57c334add1c41351f3b823f06ad8da463017d0815cf7cfea03b3 +size 91085 diff --git a/examples/desktop/screenshots/line_collection_cmap_values.png b/examples/desktop/screenshots/line_collection_cmap_values.png index 5dbe1cfda..33af5b917 100644 --- a/examples/desktop/screenshots/line_collection_cmap_values.png +++ b/examples/desktop/screenshots/line_collection_cmap_values.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ba04a638118cc6a2dff2a253fa40fae099aa2acfe347570ff4a0044403a2a5b7 -size 59102 +oid sha256:12ddca084dc83478c6b3d263f11f456f8b81e7a8a291d6b9024dbcecbfb049c0 +size 57107 diff --git a/examples/desktop/screenshots/line_collection_cmap_values_qualitative.png b/examples/desktop/screenshots/line_collection_cmap_values_qualitative.png index 00fde0fdd..57f45605b 100644 --- a/examples/desktop/screenshots/line_collection_cmap_values_qualitative.png +++ b/examples/desktop/screenshots/line_collection_cmap_values_qualitative.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a592bf90017c25abf163936c4c475b8a96ac0775e450c8d7355011fe20bbfb22 -size 62416 +oid sha256:74d5999cdd0b992f73bafb1bd74c318fd9cf058aed232068ab7dcb76d86df556 +size 60881 diff --git a/examples/desktop/screenshots/line_collection_colors.png b/examples/desktop/screenshots/line_collection_colors.png index cf7880a6c..9c27854ed 100644 --- a/examples/desktop/screenshots/line_collection_colors.png +++ b/examples/desktop/screenshots/line_collection_colors.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:052a46223f73099eacf2b4672348c1110c14b22a85131b73561ac1feb8e9c796 -size 49693 +oid sha256:a152331c51ed5440c5faf2a59439d90832521fbb1498d9635ddae088219ca353 +size 46941 diff --git a/examples/desktop/screenshots/line_collection_slicing.png b/examples/desktop/screenshots/line_collection_slicing.png index 54d2a1098..1145e84dc 100644 --- a/examples/desktop/screenshots/line_collection_slicing.png +++ b/examples/desktop/screenshots/line_collection_slicing.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:07a02cb8a93527e913d35a0caed5dfc2feb01234d63b73ba3c3e244bbfda5e59 -size 129082 +oid sha256:bdfdc2b2c5799e814ef5a1e32748a2a6d2dd88005f6fa0d9c456b8dadfada5db +size 124609 diff --git a/examples/desktop/screenshots/line_colorslice.png b/examples/desktop/screenshots/line_colorslice.png index 2bcf008a5..825ce8e3f 100644 --- a/examples/desktop/screenshots/line_colorslice.png +++ b/examples/desktop/screenshots/line_colorslice.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:69c4994a1259511a6480c2227a31472ab527f33f747cd44b9018f409e03ba1f1 -size 32353 +oid sha256:de5a56c96a062ed0ec154ae21f3a3a67087e0c8aef6d8e4681c67a016424144a +size 31971 diff --git a/examples/desktop/screenshots/line_dataslice.png b/examples/desktop/screenshots/line_dataslice.png index 395d2f1bf..71c3d1918 100644 --- a/examples/desktop/screenshots/line_dataslice.png +++ b/examples/desktop/screenshots/line_dataslice.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:268a0e9a6a32e015466b7a72b8cb9675597db2b3212edaf818871a27602a751c -size 44986 +oid sha256:e4dece6f721068a1ae37c6830110f97df64ea57c467ef4d7f42b73575d2ee476 +size 43995 diff --git a/examples/desktop/screenshots/line_present_scaling.png b/examples/desktop/screenshots/line_present_scaling.png deleted file mode 100644 index ba7142106..000000000 --- a/examples/desktop/screenshots/line_present_scaling.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:06f7dd45eb495fecfcf46478c6430a658640ceb2855c4797bc184cf4134571e3 -size 20180 diff --git a/examples/desktop/screenshots/line_stack.png b/examples/desktop/screenshots/line_stack.png index edaae0d0a..026b1f61e 100644 --- a/examples/desktop/screenshots/line_stack.png +++ b/examples/desktop/screenshots/line_stack.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2b3ae8275e536669dfabdc8c3b715d1ca9c73f17062ab1f90f9650bd86b1c4f7 -size 91285 +oid sha256:1384f1030e81fc05b24db040ac47a3bd62663358dcbdd0e77b3d675d5edd4357 +size 86938 diff --git a/examples/desktop/screenshots/multigraphic_gridplot.png b/examples/desktop/screenshots/multigraphic_gridplot.png deleted file mode 100644 index e814eadde..000000000 --- a/examples/desktop/screenshots/multigraphic_gridplot.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:e879a39032759006685eff1d5c2331a66b9126b13bc148734c3622bd6cdc68a7 -size 163902 diff --git a/examples/desktop/screenshots/scatter.png b/examples/desktop/screenshots/scatter.png deleted file mode 100644 index 195c5ae64..000000000 --- a/examples/desktop/screenshots/scatter.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:b0f6c3d3b5e7216cc17c70ad3ff114092b1556fb9dcd171ae737f28d52ce51c9 -size 106634 diff --git a/examples/desktop/screenshots/scatter_cmap.png b/examples/desktop/screenshots/scatter_cmap.png deleted file mode 100644 index 55442aceb..000000000 --- a/examples/desktop/screenshots/scatter_cmap.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:0655f8f5f1fdeecbd5d097cf90a4776dd8125a16d0e8edb86aa37f77daba0d9b -size 107892 diff --git a/examples/desktop/screenshots/scatter_cmap_iris.png b/examples/desktop/screenshots/scatter_cmap_iris.png index eb8802c0e..2a6ae7016 100644 --- a/examples/desktop/screenshots/scatter_cmap_iris.png +++ b/examples/desktop/screenshots/scatter_cmap_iris.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:088579410d42768e68d7e7b298d2ed9e409eaff28ba5a2dd2ec8283e5994b862 -size 31599 +oid sha256:b781b387476028a5eaf2083c40d57306afbcbc2a6754dce6fb66cf71ddd689d1 +size 31719 diff --git a/examples/desktop/screenshots/scatter_colorslice.png b/examples/desktop/screenshots/scatter_colorslice.png deleted file mode 100644 index 7c43dd6cb..000000000 --- a/examples/desktop/screenshots/scatter_colorslice.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:d3319afffb554eb624d9bba5b87daf331b619ac5ec5006f13e479b613f43ca32 -size 72083 diff --git a/examples/desktop/screenshots/scatter_colorslice_iris.png b/examples/desktop/screenshots/scatter_colorslice_iris.png index 8692f53b3..45c5d940c 100644 --- a/examples/desktop/screenshots/scatter_colorslice_iris.png +++ b/examples/desktop/screenshots/scatter_colorslice_iris.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f21ab8499384298a74db3e5fbb5aa0113f714d6f77e1e3d614da3d278a2c7a78 -size 13478 +oid sha256:68f93c08d361232c9be2220a68db8659c9c3c81c3cdb4e1a1ce9b366fb28b4f5 +size 13215 diff --git a/examples/desktop/screenshots/scatter_dataslice.png b/examples/desktop/screenshots/scatter_dataslice.png deleted file mode 100644 index 7a1429663..000000000 --- a/examples/desktop/screenshots/scatter_dataslice.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:0ecc6454dd197e6a3f146d0a04881db91b099673b9d74903536ca103b2418c89 -size 15657 diff --git a/examples/desktop/screenshots/scatter_dataslice_iris.png b/examples/desktop/screenshots/scatter_dataslice_iris.png index c0c6aa5e7..1121d032c 100644 --- a/examples/desktop/screenshots/scatter_dataslice_iris.png +++ b/examples/desktop/screenshots/scatter_dataslice_iris.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2032ce0b3c39f35e9dc37bc54fac016dc43aef716cfc65e6d65fd7fc977e74cf -size 14473 +oid sha256:5d662e151062a136a17dac1f8693ba13f41daac05e91e32ee9c7053715f9ee17 +size 14437 diff --git a/examples/desktop/screenshots/scatter_iris.png b/examples/desktop/screenshots/scatter_iris.png index aa1d2b743..7d107d964 100644 --- a/examples/desktop/screenshots/scatter_iris.png +++ b/examples/desktop/screenshots/scatter_iris.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:320a02df6de51235b1912b26df4ea7633e9513df4d4034605a4e00724ceb57dc -size 14131 +oid sha256:4fc88e52cc4ede6d1453746461da645f8b3df0a3099155caf639768a5ad4424c +size 14148 diff --git a/examples/desktop/screenshots/scatter_present.png b/examples/desktop/screenshots/scatter_present.png deleted file mode 100644 index 08bc610b3..000000000 --- a/examples/desktop/screenshots/scatter_present.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:bd072918f21ed0ce4ea4e1f4499ec1ff66d867cfdc0ecd6b3ed8092141cd348e -size 14195 diff --git a/examples/desktop/screenshots/scatter_size.png b/examples/desktop/screenshots/scatter_size.png index 2d4e559fe..66b31cab9 100644 --- a/examples/desktop/screenshots/scatter_size.png +++ b/examples/desktop/screenshots/scatter_size.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a96a5da476b22f3ebd42ec927c3d1c1dee4dc20f261a97feaced69ed4142c645 -size 35484 +oid sha256:9d1eeb96dc1f52c4d48889a8b00387387cccb7b83d479c1c4b47789b281a1cd5 +size 34222 diff --git a/examples/notebooks/screenshots/nb-astronaut.png b/examples/notebooks/screenshots/nb-astronaut.png index 3e55979ee..9c28b6cfa 100644 --- a/examples/notebooks/screenshots/nb-astronaut.png +++ b/examples/notebooks/screenshots/nb-astronaut.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a0fdb5b319347b4db4611dcf92cf08359c938f42a64b05d0dd163e0ca289e3c3 -size 112299 +oid sha256:afb405dfcd90d9165b4be8c2b79a82b45964debb119d25851835b8a6e2f18785 +size 111986 diff --git a/examples/notebooks/screenshots/nb-astronaut_RGB.png b/examples/notebooks/screenshots/nb-astronaut_RGB.png index fbb514e3e..1939c12d7 100644 --- a/examples/notebooks/screenshots/nb-astronaut_RGB.png +++ b/examples/notebooks/screenshots/nb-astronaut_RGB.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2d312ce9097114bc32886c0370861bcf7deebfb4fda99e03817ebec2226eabdc -size 110338 +oid sha256:2f86ef886266279ace4672904860bdaeee49dd23498998c8f68ae0b36cecc529 +size 110588 diff --git a/examples/notebooks/screenshots/nb-camera.png b/examples/notebooks/screenshots/nb-camera.png index 5629bd211..cfdf2673e 100644 --- a/examples/notebooks/screenshots/nb-camera.png +++ b/examples/notebooks/screenshots/nb-camera.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0a415917cc16f09ab7b78eea5e5579d7dd45b6d92e80d87ba0970e9dd0568eb2 -size 77419 +oid sha256:124e52fdb8c200be3295f79331f25a51d423d159a7f8cde1863daa00e54c0894 +size 77665 diff --git a/examples/notebooks/screenshots/nb-image-widget-movie-set_data.png b/examples/notebooks/screenshots/nb-image-widget-movie-set_data.png index 486c89963..e49ad3c38 100644 --- a/examples/notebooks/screenshots/nb-image-widget-movie-set_data.png +++ b/examples/notebooks/screenshots/nb-image-widget-movie-set_data.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3dbb4d04175c5603ff7e56a04438c8f0cfff7deff61889a06c342cedc04ac323 -size 43172 +oid sha256:5acd7eeccbf47af45aa8306befb040f9b53d21f1727e7366b536d73261b407ce +size 43494 diff --git a/examples/notebooks/screenshots/nb-image-widget-movie-single-0-reset.png b/examples/notebooks/screenshots/nb-image-widget-movie-single-0-reset.png index 1369669e6..dfcb98736 100644 --- a/examples/notebooks/screenshots/nb-image-widget-movie-single-0-reset.png +++ b/examples/notebooks/screenshots/nb-image-widget-movie-single-0-reset.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7049dc9344a8d51bcfc3ba7f96a25c84db27f5072f14e8845cd2f01c3f4f5310 -size 133683 +oid sha256:9ca702fffc4eebea5ba31b77062b60f848c2e5d689568d16b39a62561a0b8b73 +size 134201 diff --git a/examples/notebooks/screenshots/nb-image-widget-movie-single-0.png b/examples/notebooks/screenshots/nb-image-widget-movie-single-0.png index 1369669e6..dfcb98736 100644 --- a/examples/notebooks/screenshots/nb-image-widget-movie-single-0.png +++ b/examples/notebooks/screenshots/nb-image-widget-movie-single-0.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7049dc9344a8d51bcfc3ba7f96a25c84db27f5072f14e8845cd2f01c3f4f5310 -size 133683 +oid sha256:9ca702fffc4eebea5ba31b77062b60f848c2e5d689568d16b39a62561a0b8b73 +size 134201 diff --git a/examples/notebooks/screenshots/nb-image-widget-movie-single-279.png b/examples/notebooks/screenshots/nb-image-widget-movie-single-279.png index 5817cd299..787e2757e 100644 --- a/examples/notebooks/screenshots/nb-image-widget-movie-single-279.png +++ b/examples/notebooks/screenshots/nb-image-widget-movie-single-279.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1ca687c654679ee9d47a053196fb65d92105ed6cce76f0d2d1775582053e7b07 -size 168750 +oid sha256:73bdd6a91ab679dcf237626bc7d3edd267d402ea8de2b6e2c3db7bba9b9418ac +size 169211 diff --git a/examples/notebooks/screenshots/nb-image-widget-movie-single-50-window-max-33.png b/examples/notebooks/screenshots/nb-image-widget-movie-single-50-window-max-33.png index 2f2e310d0..ca2357ddd 100644 --- a/examples/notebooks/screenshots/nb-image-widget-movie-single-50-window-max-33.png +++ b/examples/notebooks/screenshots/nb-image-widget-movie-single-50-window-max-33.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:dd51516d18f550f62b9f2c989cfae82d267eb6de1d8757cf44949bb41c3b13c2 -size 148408 +oid sha256:afb9c5bfbfbc2ce800d613f779021b0a93d098f415d89157f994cc9b1632361b +size 149454 diff --git a/examples/notebooks/screenshots/nb-image-widget-movie-single-50-window-mean-13.png b/examples/notebooks/screenshots/nb-image-widget-movie-single-50-window-mean-13.png index 39a4f1274..ac3f4cb61 100644 --- a/examples/notebooks/screenshots/nb-image-widget-movie-single-50-window-mean-13.png +++ b/examples/notebooks/screenshots/nb-image-widget-movie-single-50-window-mean-13.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9613c10e63261ddbcf3274a9804bdf3a1be0734c1190720663b01f8b2ae48389 -size 124532 +oid sha256:c3c07d75cd4673e411d814c1dab1e62d6543b26c89f208eed15ccb941bbe3ab2 +size 124795 diff --git a/examples/notebooks/screenshots/nb-image-widget-movie-single-50-window-mean-33.png b/examples/notebooks/screenshots/nb-image-widget-movie-single-50-window-mean-33.png index b9feacb82..3a77efced 100644 --- a/examples/notebooks/screenshots/nb-image-widget-movie-single-50-window-mean-33.png +++ b/examples/notebooks/screenshots/nb-image-widget-movie-single-50-window-mean-33.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8fa86b26f11bd9655c60e43ddd88ada618cea1ba06b3dcddcb408569c09a7750 -size 113897 +oid sha256:5f39d68bbc2c7d52cc13609ff60274dbfe49bea4d4a03cfbf1d1c15cf7fb8e8c +size 114013 diff --git a/examples/notebooks/screenshots/nb-image-widget-movie-single-50-window-reset.png b/examples/notebooks/screenshots/nb-image-widget-movie-single-50-window-reset.png index ae280a970..e34f9deb3 100644 --- a/examples/notebooks/screenshots/nb-image-widget-movie-single-50-window-reset.png +++ b/examples/notebooks/screenshots/nb-image-widget-movie-single-50-window-reset.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:aca573ba6dcda8ad8f018a96790b1b1aa5da25fe9584a57c2c946e1cf2df9e5c -size 145275 +oid sha256:2825af49b1964fb76dcf2ccd494bb61623df4d5fffad7be30cf389b9b7e6d4bf +size 146186 diff --git a/examples/notebooks/screenshots/nb-image-widget-movie-single-50.png b/examples/notebooks/screenshots/nb-image-widget-movie-single-50.png index ae280a970..e34f9deb3 100644 --- a/examples/notebooks/screenshots/nb-image-widget-movie-single-50.png +++ b/examples/notebooks/screenshots/nb-image-widget-movie-single-50.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:aca573ba6dcda8ad8f018a96790b1b1aa5da25fe9584a57c2c946e1cf2df9e5c -size 145275 +oid sha256:2825af49b1964fb76dcf2ccd494bb61623df4d5fffad7be30cf389b9b7e6d4bf +size 146186 diff --git a/examples/notebooks/screenshots/nb-image-widget-single-gnuplot2.png b/examples/notebooks/screenshots/nb-image-widget-single-gnuplot2.png index 02423a02a..4cd3248a0 100644 --- a/examples/notebooks/screenshots/nb-image-widget-single-gnuplot2.png +++ b/examples/notebooks/screenshots/nb-image-widget-single-gnuplot2.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d7384d1a69629cfcdbebdc9e9e6a152383446f3cb696e69a11543253cdde2e64 -size 434060 +oid sha256:aff55757a29cac06c1c158599681e8c10e27fd772425c6b3137a06d5d604f95e +size 435106 diff --git a/examples/notebooks/screenshots/nb-image-widget-single.png b/examples/notebooks/screenshots/nb-image-widget-single.png index 408739d6e..dd37a74db 100644 --- a/examples/notebooks/screenshots/nb-image-widget-single.png +++ b/examples/notebooks/screenshots/nb-image-widget-single.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b57dffe179b6f52d204968085c885d70183d1f6a9a3f5a1dc2d005229b7acd01 -size 404179 +oid sha256:1e70812decf8d1c591b1d97c24346159255e8b5cba5722f9c4d67c5b5aa92a8a +size 403368 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50-frame-apply-gaussian.png b/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50-frame-apply-gaussian.png index 596548486..9be76e5bd 100644 --- a/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50-frame-apply-gaussian.png +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50-frame-apply-gaussian.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:da1c660e4fb779ac6a4baed3d329cf80274981288ea076f243bb43aee7fb8eff -size 157731 +oid sha256:8d6b97c351f51ee8b0429e7001ba16cb3862c9cfc4f4e0f0227524b8c20d5906 +size 157300 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50-frame-apply-reset.png b/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50-frame-apply-reset.png index 1318be413..c877ac887 100644 --- a/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50-frame-apply-reset.png +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50-frame-apply-reset.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0d4f6407c3b029b01088fab9522a21f7d90d7a87def1ecbbb45f0fb4f8508f87 -size 69106 +oid sha256:d74649c5ca7b0401a8e42ffe9b73cebeebdce80953c4790f44a99bfe6624902b +size 71618 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50-max-window-13.png b/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50-max-window-13.png index e5fdbdd28..7613ae2a9 100644 --- a/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50-max-window-13.png +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50-max-window-13.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e4176109805f4f521a1b630b68df1dce80a63b82a5ed01a6ba4c2cae0dfeb6bd -size 184423 +oid sha256:e9c99c189dbfffbc3fa24fb6f48015518a2e1c3e681191abb45cf4e29185dcff +size 196855 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50-mean-window-13.png b/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50-mean-window-13.png index bf9548962..e803cdc68 100644 --- a/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50-mean-window-13.png +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50-mean-window-13.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:12df56b1045cdaddb94355b7e960aa58137a44eff4ff22aab3596c53ea7944c8 -size 179403 +oid sha256:916800ae449d12e875f14be3d13a75db85339524dbd594f9963074b9fc5316ae +size 177769 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50-mean-window-5.png b/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50-mean-window-5.png index 7b3e6bfba..5b5ef1009 100644 --- a/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50-mean-window-5.png +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50-mean-window-5.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d5fd2f0918f4a29769ebb572f5163abb52667cf24e89efdd1d99bc57a0f5f607 -size 140124 +oid sha256:3006a07bfbf6276967ca55809788f61a1852db9851f959cc1db00016a9b3747f +size 140019 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50.png b/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50.png index a72245f3b..4e8803a7b 100644 --- a/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50.png +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-frame-50.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:981c925f52ae8789f7f0d24ef3fe34efb7a08f221a7bc6079dd12f01099c3d25 -size 75054 +oid sha256:3e55ffde023955d00804a7272a4e963b4d2a74b74fb401962d32c1a29d76bc24 +size 80880 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-frame-99.png b/examples/notebooks/screenshots/nb-image-widget-zfish-frame-99.png index 19c19dc1f..061195a98 100644 --- a/examples/notebooks/screenshots/nb-image-widget-zfish-frame-99.png +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-frame-99.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6a5ecd1f966250ead16a96df996ff39877b4ee28534b7724a4a8e1db9c8984d2 -size 58334 +oid sha256:405495c384aa52d6e3c8a65237876682f4be62967dce1b6af526e4d069fa44d3 +size 62621 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50-frame-apply-gaussian.png b/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50-frame-apply-gaussian.png index bcf663279..0da3abb21 100644 --- a/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50-frame-apply-gaussian.png +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50-frame-apply-gaussian.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:dbe1375ae8d2f348ad5d7e030fa67d45c250c6ed263c67179d542d0bd903e0d3 -size 177334 +oid sha256:7b30ef1dca9711bd72eb28f0035552f93e143a683f818c3f2aec8df4323306e4 +size 178459 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50-frame-apply-reset.png b/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50-frame-apply-reset.png index 963290515..21ea17c27 100644 --- a/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50-frame-apply-reset.png +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50-frame-apply-reset.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0ff341df374d816411e58c82d432e98a9f4cec1551725dafcd65cdb0c43edb12 -size 138235 +oid sha256:b3e8fc84f5ea2d5a93bc02e19965781fbe9ec697b660430a5203cb1c91803974 +size 142748 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50-max-window-13.png b/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50-max-window-13.png index a049a484c..ece0fee5f 100644 --- a/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50-max-window-13.png +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50-max-window-13.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bd4c51f7e07e46d7c660d28563aff1b7d3759387fc10db10addca29dfc0919b0 -size 365838 +oid sha256:7b01f2385991f4941f35d1b913fe54c72cbe42c62522ab181ddb2466b2f2be8d +size 372324 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50-mean-window-13.png b/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50-mean-window-13.png index ada15017c..93dd3b254 100644 --- a/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50-mean-window-13.png +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50-mean-window-13.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1d13cc8a32b40f5c721ab30020632d7dc1c679c8c8e5857476176e986de18ad3 -size 211240 +oid sha256:4bac6aedfebab2bf97497dbecd17f59b36cb78b27dcdb1547c6d78f902d5f89b +size 213579 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50-mean-window-5.png b/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50-mean-window-5.png index 2e71fd30d..b6392f095 100644 --- a/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50-mean-window-5.png +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50-mean-window-5.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:39044f4bb54038eee17f0d75940fd848b304629858d9e747ac0c81ce076d3c25 -size 199075 +oid sha256:5458f9488a19207c7d4f8a971de06a028dfb22e4a2847c3a0b1e1f45c41109f0 +size 200566 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50.png b/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50.png index 690b1c578..8165824cb 100644 --- a/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50.png +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-50.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:109a4c8d708114e1b36d1a9fa129dbd9a768617baa953f37500310c0968b688a -size 154169 +oid sha256:8588b720e7d970a0c5d0b9e43c68ee0695d7ced8c51797d50143b0737d3ae2c1 +size 160340 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-99.png b/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-99.png index 3e577698c..f46e58b4f 100644 --- a/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-99.png +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-grid-frame-99.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:568ae05e8889cec56530db0149613826f2697f55d8252cffbd32ff692b565fcf -size 141338 +oid sha256:b86bc324f13ca3a958d0db80251874478e0191b0c30c301f3022913e7b1f62d5 +size 147084 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-grid-init-mean-window-5.png b/examples/notebooks/screenshots/nb-image-widget-zfish-grid-init-mean-window-5.png index 1ab48d117..8e3e7e2de 100644 --- a/examples/notebooks/screenshots/nb-image-widget-zfish-grid-init-mean-window-5.png +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-grid-init-mean-window-5.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e7847dc083d6df2b20788b42153674c557b627b75db74d9446b06e165aa5a50a -size 182713 +oid sha256:9993fe8f8d3e6d6e48d863b251fdd7b37926ba7b97b2d70683cbc3ab45910c99 +size 184668 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-grid-set_data-reset-indices-false.png b/examples/notebooks/screenshots/nb-image-widget-zfish-grid-set_data-reset-indices-false.png index 0b0f05fc3..aae5c9066 100644 --- a/examples/notebooks/screenshots/nb-image-widget-zfish-grid-set_data-reset-indices-false.png +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-grid-set_data-reset-indices-false.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:839dd3fdc9db98d7044d88e816c179bff34f30584ba26ce7a96ea3b35fc3374e -size 122463 +oid sha256:f4cdb28c8aa72b1cd968f4f78f3c2413d2338b6a2b5c200df02ecdd2bce1568b +size 126337 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-grid-set_data-reset-indices-true.png b/examples/notebooks/screenshots/nb-image-widget-zfish-grid-set_data-reset-indices-true.png index 534403b1e..346495cfc 100644 --- a/examples/notebooks/screenshots/nb-image-widget-zfish-grid-set_data-reset-indices-true.png +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-grid-set_data-reset-indices-true.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f7088e11517a4a78492d16746ac8101b2e5e9142ebd61966030c555ab173443e -size 126267 +oid sha256:19000f2cc6d78e2cc18dd5213778e595ee6710ca3fcd71cb4cbe6286b42b1e8b +size 130255 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-init-mean-window-5.png b/examples/notebooks/screenshots/nb-image-widget-zfish-init-mean-window-5.png index 94993c688..2298f904e 100644 --- a/examples/notebooks/screenshots/nb-image-widget-zfish-init-mean-window-5.png +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-init-mean-window-5.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:709c7ec07e3e37e0415fad3fa542729d2896e8e546b6ea8d1373e7b46005bc26 -size 97278 +oid sha256:4a141cd3e0d3647accb18c55d84026d16ca2280611b80682737a61151dd9c377 +size 99397 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-mixed-rgb-cockatoo-frame-50.png b/examples/notebooks/screenshots/nb-image-widget-zfish-mixed-rgb-cockatoo-frame-50.png index 27c693c1a..58f4fd87e 100644 --- a/examples/notebooks/screenshots/nb-image-widget-zfish-mixed-rgb-cockatoo-frame-50.png +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-mixed-rgb-cockatoo-frame-50.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c66db583e0d455319b665d35a8c5c8a5f717653c11311cdaba90e2c85e64235f -size 58941 +oid sha256:cbd3cb8399c32cc611a86bb482782bfe55393ec73f2c2a3f4eb0d4e8af2442d6 +size 58842 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-mixed-rgb-cockatoo-set-data.png b/examples/notebooks/screenshots/nb-image-widget-zfish-mixed-rgb-cockatoo-set-data.png index 7444d7dbf..0eff22834 100644 --- a/examples/notebooks/screenshots/nb-image-widget-zfish-mixed-rgb-cockatoo-set-data.png +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-mixed-rgb-cockatoo-set-data.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:36867cd793634d00c46c782911abae6e7c579067aeeed891e40ddedbb0c228d9 -size 56505 +oid sha256:e6e201ecce9db938796d1fc710a154ae8bc49e0a7e1f51d9af586f29f4ee63de +size 57116 diff --git a/examples/notebooks/screenshots/nb-image-widget-zfish-mixed-rgb-cockatoo-windowrgb.png b/examples/notebooks/screenshots/nb-image-widget-zfish-mixed-rgb-cockatoo-windowrgb.png index 3941f3120..03a1fc30c 100644 --- a/examples/notebooks/screenshots/nb-image-widget-zfish-mixed-rgb-cockatoo-windowrgb.png +++ b/examples/notebooks/screenshots/nb-image-widget-zfish-mixed-rgb-cockatoo-windowrgb.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8b23f3655fcfcd85f6138f4de5cedf228a6dbad0c8cff0c87900042f83b8f409 -size 55269 +oid sha256:608c9a0b1466886652299887a4f0f16a77dfb400fc46200a453df25c5a0e7016 +size 55903 diff --git a/examples/notebooks/screenshots/nb-imagewidget.png b/examples/notebooks/screenshots/nb-imagewidget.png deleted file mode 100644 index 9acfdb0f9..000000000 --- a/examples/notebooks/screenshots/nb-imagewidget.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:0f7a4a260ef4f9f2bdee9adab4ed376147cff39fcdd2f07eaf6e87e8b899f7d3 -size 89842 diff --git a/examples/notebooks/screenshots/nb-lines-3d.png b/examples/notebooks/screenshots/nb-lines-3d.png index 6c19c965c..d1e46a618 100644 --- a/examples/notebooks/screenshots/nb-lines-3d.png +++ b/examples/notebooks/screenshots/nb-lines-3d.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c6ab8d74c980a02505d5d626d170b60847ce2b733ea5b47278a1f48228011a38 -size 14181 +oid sha256:91f74b1ad6d4eeb08da8a33bfccfc0e9e80d48fc33b2a783cb94890f3c603a94 +size 14131 diff --git a/examples/notebooks/screenshots/nb-lines-cmap-jet-values-cosine.png b/examples/notebooks/screenshots/nb-lines-cmap-jet-values-cosine.png index f75d83ba4..db1a0e658 100644 --- a/examples/notebooks/screenshots/nb-lines-cmap-jet-values-cosine.png +++ b/examples/notebooks/screenshots/nb-lines-cmap-jet-values-cosine.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d2b87297587b5f76062132370fdf7e8318cbde68d38d12a0179548f9790c2af8 -size 11765 +oid sha256:58af931da3307204f2699b2ac04d8546b93aa0b4d3c058ab6d181656fd79fae8 +size 11674 diff --git a/examples/notebooks/screenshots/nb-lines-cmap-jet-values.png b/examples/notebooks/screenshots/nb-lines-cmap-jet-values.png index a8229cfc8..9bb734365 100644 --- a/examples/notebooks/screenshots/nb-lines-cmap-jet-values.png +++ b/examples/notebooks/screenshots/nb-lines-cmap-jet-values.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:99f6a7a9a1399aaf8d1070812c41f03f80ba41812a1acf9d4bdd17641bc589f4 -size 13038 +oid sha256:9949949767455061caa08b96dfdf0948d511d604d39ded4a028a9a50deca9797 +size 12990 diff --git a/examples/notebooks/screenshots/nb-lines-cmap-jet.png b/examples/notebooks/screenshots/nb-lines-cmap-jet.png index ccc897f66..10f9252f3 100644 --- a/examples/notebooks/screenshots/nb-lines-cmap-jet.png +++ b/examples/notebooks/screenshots/nb-lines-cmap-jet.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cac3d7cb23d7ec207cdf7fd7032037c7749e30563754d6c2cac50d67a7731a2d -size 11086 +oid sha256:c04746bb9c6e168644981e808b83b878d5d72e2101f441979765c74bb36c087a +size 10979 diff --git a/examples/notebooks/screenshots/nb-lines-cmap-tab-10.png b/examples/notebooks/screenshots/nb-lines-cmap-tab-10.png index 8ddc44ead..a769ff769 100644 --- a/examples/notebooks/screenshots/nb-lines-cmap-tab-10.png +++ b/examples/notebooks/screenshots/nb-lines-cmap-tab-10.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d38a2c27e5b598883c40b44e45b347f029a7369b638bc4111ef2d69978245378 -size 9943 +oid sha256:704cddf180de18dfc02cccced26dc57a7d8bff3938ceaf5ca9b6db7ccaed5928 +size 9582 diff --git a/examples/notebooks/screenshots/nb-lines-cmap-viridis-values.png b/examples/notebooks/screenshots/nb-lines-cmap-viridis-values.png index 6bf270874..861efcef5 100644 --- a/examples/notebooks/screenshots/nb-lines-cmap-viridis-values.png +++ b/examples/notebooks/screenshots/nb-lines-cmap-viridis-values.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d0ead67733470d6afcb2228645ee1a056acd525d369d74c16b1054783d6ab4f5 -size 10155 +oid sha256:67310ed0deb418bf0d6d10e1184e902f928f0e914518b91c23e948f3bb9e7b25 +size 9850 diff --git a/examples/notebooks/screenshots/nb-lines-cmap-viridis.png b/examples/notebooks/screenshots/nb-lines-cmap-viridis.png index 4f99e5fb5..2d71b4428 100644 --- a/examples/notebooks/screenshots/nb-lines-cmap-viridis.png +++ b/examples/notebooks/screenshots/nb-lines-cmap-viridis.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:15f4decaa6ba08f09a988771c5dc433629cb9e3bccfa08f820268366a3ff06a6 -size 13761 +oid sha256:6295649505902ac1f37ae6453e278dbbcdacb64426f1c51e27e16ef38650f8a8 +size 13725 diff --git a/examples/notebooks/screenshots/nb-lines-cmap-white.png b/examples/notebooks/screenshots/nb-lines-cmap-white.png index 85e43aa5e..b450a8ea4 100644 --- a/examples/notebooks/screenshots/nb-lines-cmap-white.png +++ b/examples/notebooks/screenshots/nb-lines-cmap-white.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8a623f31d28533cc6052143856d0ef2e827b42891397fa4ac20d358f44d751d7 -size 8332 +oid sha256:a1abc26476bbabf31094bd70929afc918e4064a1996d7742adb716ed6e9c2617 +size 7532 diff --git a/examples/notebooks/screenshots/nb-lines-colors.png b/examples/notebooks/screenshots/nb-lines-colors.png index ab50dfaaf..88fef4e39 100644 --- a/examples/notebooks/screenshots/nb-lines-colors.png +++ b/examples/notebooks/screenshots/nb-lines-colors.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0cbcafef567bff33762d64bcf6f83c7a1982015ea21d508b1e6564cb0d8435d2 -size 27420 +oid sha256:bbbb1b63c69ef4061f0b64fc2360e0c613ee4732d581929657068f55141d6fd9 +size 27274 diff --git a/examples/notebooks/screenshots/nb-lines-data.png b/examples/notebooks/screenshots/nb-lines-data.png index 88f1001ad..b8c5bf582 100644 --- a/examples/notebooks/screenshots/nb-lines-data.png +++ b/examples/notebooks/screenshots/nb-lines-data.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f62dc16cd1ea212cf29974a64e6ad2dd3d7ae10d7b3eef573c25316bbe89b097 -size 38740 +oid sha256:6f677a3c0a1b2fb57771af6118d45d23b1d86f88d3431ca06ef89b79a48dad06 +size 38880 diff --git a/examples/notebooks/screenshots/nb-lines-underlay.png b/examples/notebooks/screenshots/nb-lines-underlay.png index f9650e253..93edd81d6 100644 --- a/examples/notebooks/screenshots/nb-lines-underlay.png +++ b/examples/notebooks/screenshots/nb-lines-underlay.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ec7881cc0084b0ea2182f8d4f6f23388a4d48b88dbbf9c06b9dc2fa8125989b7 -size 49553 +oid sha256:35e0ea48cac0242e79da491629bda9fccedb94814e8d3d1188323c7d9668e513 +size 49940 diff --git a/examples/notebooks/screenshots/nb-lines.png b/examples/notebooks/screenshots/nb-lines.png index 7df6ea14f..e28486bf4 100644 --- a/examples/notebooks/screenshots/nb-lines.png +++ b/examples/notebooks/screenshots/nb-lines.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b925337438aa24f0179a8b5c1688e154679fa002402a16746b6bb3cd6684af1e -size 24246 +oid sha256:17ee8c3de59b9e80d66c30d61287a38ac06ee996833f32648506a6bf1ebb0da8 +size 23317 diff --git a/fastplotlib/graphics/_features/_image.py b/fastplotlib/graphics/_features/_image.py index e31184c4b..2d93745bf 100644 --- a/fastplotlib/graphics/_features/_image.py +++ b/fastplotlib/graphics/_features/_image.py @@ -118,8 +118,8 @@ def __next__(self) -> tuple[pygfx.Texture, tuple[int, int], tuple[slice, slice]] chunk_index = (chunk_row, chunk_col) # stop indices of big data array for this chunk - row_stop = min(self.value.shape[0] - 1, data_row_start + WGPU_MAX_TEXTURE_SIZE) - col_stop = min(self.value.shape[1] - 1, data_col_start + WGPU_MAX_TEXTURE_SIZE) + row_stop = min(self.value.shape[0], data_row_start + WGPU_MAX_TEXTURE_SIZE) + col_stop = min(self.value.shape[1], data_col_start + WGPU_MAX_TEXTURE_SIZE) # row and column slices that slice the data for this chunk from the big data array data_slice = (slice(data_row_start, row_stop), slice(data_col_start, col_stop)) diff --git a/tests/test_image_graphic.py b/tests/test_image_graphic.py index 9f89e8aa8..541129079 100644 --- a/tests/test_image_graphic.py +++ b/tests/test_image_graphic.py @@ -79,8 +79,13 @@ def test_gray(): "cmap_interpolation", ) + # make sure entire data is the same npt.assert_almost_equal(ig.data.value, GRAY_IMAGE) + # since this entire image is under the wgpu max texture limit, + # the entire image should be in the single Texture buffer + npt.assert_almost_equal(ig.data.buffer[0, 0].data, GRAY_IMAGE) + ig.cmap = "viridis" assert ig.cmap == "viridis" check_event(graphic=ig, feature="cmap", value="viridis") diff --git a/tests/test_texture_array.py b/tests/test_texture_array.py index 5aecf49a5..e1a6a1753 100644 --- a/tests/test_texture_array.py +++ b/tests/test_texture_array.py @@ -54,10 +54,10 @@ def check_texture_array( data_col_start_index = chunk_col * WGPU_MAX_TEXTURE_SIZE data_row_stop_index = min( - data.shape[0] - 1, data_row_start_index + WGPU_MAX_TEXTURE_SIZE + data.shape[0], data_row_start_index + WGPU_MAX_TEXTURE_SIZE ) data_col_stop_index = min( - data.shape[1] - 1, data_col_start_index + WGPU_MAX_TEXTURE_SIZE + data.shape[1], data_col_start_index + WGPU_MAX_TEXTURE_SIZE ) row_slice = slice(data_row_start_index, data_row_stop_index) From a8bc4ee85cb36efa9e7ac49f2130f6ee9b865033 Mon Sep 17 00:00:00 2001 From: Kushal Kolar Date: Tue, 18 Jun 2024 22:23:57 -0400 Subject: [PATCH 64/66] Figure.export() (#531) --- docs/source/api/layouts/figure.rst | 1 + fastplotlib/layouts/_figure.py | 47 ++++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+) diff --git a/docs/source/api/layouts/figure.rst b/docs/source/api/layouts/figure.rst index a2d5e5758..817284e18 100644 --- a/docs/source/api/layouts/figure.rst +++ b/docs/source/api/layouts/figure.rst @@ -37,6 +37,7 @@ Methods Figure.add_animations Figure.clear Figure.close + Figure.export Figure.remove_animation Figure.render Figure.show diff --git a/fastplotlib/layouts/_figure.py b/fastplotlib/layouts/_figure.py index 17bb28095..d330c6928 100644 --- a/fastplotlib/layouts/_figure.py +++ b/fastplotlib/layouts/_figure.py @@ -128,6 +128,7 @@ def __init__( # if controller instances have been specified for each subplot if controllers is not None: + # one controller for all subplots if isinstance(controllers, pygfx.Controller): controllers = [controllers] * len(self) @@ -579,6 +580,52 @@ def clear(self): for subplot in self: subplot.clear() + def export(self, uri: str | Path | bytes, **kwargs): + """ + Use ``imageio`` for writing the current Figure to a file, or return a byte string. + Must have ``imageio`` installed. + + Parameters + ---------- + uri: str | Path | bytes + + kwargs: passed to imageio.v3.imwrite, see: https://imageio.readthedocs.io/en/stable/_autosummary/imageio.v3.imwrite.html + + Returns + ------- + None | bytes + see https://imageio.readthedocs.io/en/stable/_autosummary/imageio.v3.imwrite.html + """ + try: + import imageio.v3 as iio + except ModuleNotFoundError: + raise ImportError( + "imageio is required to use Figure.export(). Install it using pip or conda:\n" + "pip install imageio\n" + "conda install -c conda-forge imageio\n" + ) + else: + snapshot = self.renderer.snapshot() + remove_alpha = True + + # image formats that support alpha channel: + # https://en.wikipedia.org/wiki/Alpha_compositing#Image_formats_supporting_alpha_channels + alpha_support = [".png", ".exr", ".tiff", ".tif", ".gif", ".jxl", ".svg"] + + if isinstance(uri, str): + if any([uri.endswith(ext) for ext in alpha_support]): + remove_alpha = False + + elif isinstance(uri, Path): + if uri.suffix in alpha_support: + remove_alpha = False + + if remove_alpha: + # remove alpha channel if it's not supported + snapshot = snapshot[..., :-1].shape + + return iio.imwrite(uri, snapshot, **kwargs) + def _get_iterator(self): return product(range(self.shape[0]), range(self.shape[1])) From 385aced6b1e86e26df4a9c2639121bccd3f0c221 Mon Sep 17 00:00:00 2001 From: Kushal Kolar Date: Tue, 18 Jun 2024 22:24:14 -0400 Subject: [PATCH 65/66] Update README.md (#532) --- README.md | 39 +++++++++++++-------------------------- 1 file changed, 13 insertions(+), 26 deletions(-) diff --git a/README.md b/README.md index 5c4eb2d45..9f3f9b236 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ [**Examples**](https://github.com/kushalkolar/fastplotlib#examples) | [**Contributing**](https://github.com/kushalkolar/fastplotlib#heart-contributing) -Next-gen plotting library built using the [`pygfx`](https://github.com/pygfx/pygfx) rendering engine that can utilize [Vulkan](https://en.wikipedia.org/wiki/Vulkan), [DX12](https://en.wikipedia.org/wiki/DirectX#DirectX_12), or [Metal](https://developer.apple.com/metal/) via WGPU, so it is very fast! `fastplotlib` also aims to be an expressive plotting library that enables rapid prototyping for large scale explorative scientific visualization. +Next-gen plotting library built using the [`pygfx`](https://github.com/pygfx/pygfx) rendering engine that can utilize [Vulkan](https://en.wikipedia.org/wiki/Vulkan), [DX12](https://en.wikipedia.org/wiki/DirectX#DirectX_12), or [Metal](https://developer.apple.com/metal/) via WGPU, so it is very fast! `fastplotlib` is an expressive plotting library that enables rapid prototyping for large scale explorative scientific visualization. ![scipy-fpl](https://github.com/fastplotlib/fastplotlib/assets/9403332/b981a54c-05f9-443f-a8e4-52cd01cd802a) @@ -41,17 +41,15 @@ concepts are similar to those from the API shown in the video. > **Note** > -> `fastplotlib` is currently in the **alpha stage with breaking changes every ~month**, but you're welcome to try it out or contribute! See our [Roadmap](https://github.com/kushalkolar/fastplotlib/issues/55). See this for a discussion on API stability: https://github.com/fastplotlib/fastplotlib/issues/121 +> `fastplotlib` is currently in the **late alpha stage**, but you're welcome to try it out or contribute! See our [Roadmap](https://github.com/kushalkolar/fastplotlib/issues/55). See this for a discussion on API stability: https://github.com/fastplotlib/fastplotlib/issues/121 # Documentation http://fastplotlib.readthedocs.io/ -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/main/examples +The examples are interactive if you run them locally on your computer. If someone wants to integrate `pyodide` with `pygfx` we would be able to have live interactive examples on the website! -If someone wants to integrate `pyodide` with `pygfx` we would be able to have live interactive examples! :smiley: - -Questions, issues, ideas? Post an [issue](https://github.com/fastplotlib/fastplotlib/issues) or post on the [discussion forum](https://github.com/fastplotlib/fastplotlib/discussions)! +Questions, issues, ideas? You are welcome to post an [issue](https://github.com/fastplotlib/fastplotlib/issues) or post on the [discussion forum](https://github.com/fastplotlib/fastplotlib/discussions)! :smiley: # Installation @@ -87,51 +85,40 @@ cd fastplotlib # install all extras in place pip install -e ".[notebook,docs,tests]" + +# install latest pygfx +pip install git+https://github.com/pygfx/pygfx.git@main ``` Se [Contributing](https://github.com/fastplotlib/fastplotlib?tab=readme-ov-file#heart-contributing) for more details on development # Examples +Examples gallery: https://fastplotlib.readthedocs.io/en/latest/_gallery/index.html + > **Note:** `fastplotlib` and `pygfx` are fast evolving, you will probably require the latest `pygfx` and `fastplotlib` from github to use the examples in the main branch. `fastplotlib` code is identical across notebook (`jupyter`), and desktop use with `Qt`/`PySide` or `glfw`. -Even if you do not intend to use notebooks with `fastplotlib`, the `quickstart.ipynb` notebook is currently the best way to get familiar with the API: https://github.com/fastplotlib/fastplotlib/tree/main/examples/notebooks/quickstart.ipynb +Even if you do not intend to use notebooks with `fastplotlib`, the `quickstart.ipynb` tutorial notebook is the best way to get familiar with the API: https://github.com/fastplotlib/fastplotlib/tree/main/examples/notebooks/quickstart.ipynb The specifics for running `fastplotlib` in different GUI frameworks are: - Running in `glfw` requires a `fastplotlib.run()` call (which is really just a `wgpu` `run()` call) - With `Qt` you can encapsulate it within a `QApplication`, see `examples/qt` - Notebooks plots have ipywidget-based toolbars and widgets. There are plans to move toward an identical in-canvas toolbar with UI elements across all supported frameworks 😄 -### Desktop examples using `glfw` or `Qt` +### Embedding in a `Qt` app -GLFW examples are here. GLFW is a "minimal" desktop framework. - -https://github.com/fastplotlib/fastplotlib/tree/main/examples/desktop - -Qt examples are here: +See these for examples on embedding within a Qt app. Note that you can also use `fastplotlib` with qt interactively using `%gui qt` in jupyter or ipython. https://github.com/fastplotlib/fastplotlib/tree/main/examples/qt -Some of the examples require imageio: -``` -pip install imageio -``` - ### Notebook examples -Notebook examples are here: +Notebook examples are here, these include examples on selector tools. https://github.com/fastplotlib/fastplotlib/tree/main/examples/notebooks -**Start with `quickstart.ipynb`.** - -Some of the examples require imageio: -``` -pip install imageio -``` - ### Video Our SciPy 2023 talk walks through numerous demos: https://github.com/fastplotlib/fastplotlib#scipy-talk From 0edfe7f117fd2dd740f3ae6eab0e4d18d45ee6a3 Mon Sep 17 00:00:00 2001 From: Kushal Kolar Date: Tue, 18 Jun 2024 22:24:38 -0400 Subject: [PATCH 66/66] Update deps (#533) * pin to wgpu<0.16.0, set pygfx max ver pin * pin wgpu max version for release * another instance of reverting info to get_adapter_info * typo --- docs/source/user_guide/gpu.rst | 4 ++-- examples/tests/testutils.py | 2 +- fastplotlib/utils/gui.py | 4 ++-- setup.py | 4 ++-- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/source/user_guide/gpu.rst b/docs/source/user_guide/gpu.rst index f4ef89a0c..1bbc0c030 100644 --- a/docs/source/user_guide/gpu.rst +++ b/docs/source/user_guide/gpu.rst @@ -59,7 +59,7 @@ You can get more detailed info on each adapter like this:: import pprint for a in fpl.enumerate_adapters(): - pprint.pprint(a.info) + pprint.pprint(a.request_adapter_info()) General description of the fields: * vendor: GPU manufacturer @@ -267,7 +267,7 @@ to ``fpl.select_adapter()``:: # get info or summary of all adapters to pick an adapter import pprint for a in fpl.enumerate_adapters(): - pprint.pprint(a.info) + pprint.pprint(a.request_adapter_info()) # example, pick adapter at index 2 chosen_gpu = fpl.enumerate_adapters()[2] diff --git a/examples/tests/testutils.py b/examples/tests/testutils.py index 0e4cd2e1b..22747ce08 100644 --- a/examples/tests/testutils.py +++ b/examples/tests/testutils.py @@ -31,7 +31,7 @@ def get_wgpu_backend(): """ Query the configured wgpu backend driver. """ - code = "import wgpu.utils; info = wgpu.utils.get_default_device().adapter.info; print(info['adapter_type'], info['backend_type'])" + code = "import wgpu.utils; info = wgpu.utils.get_default_device().adapter.request_adapter_info(); print(info['adapter_type'], info['backend_type'])" result = subprocess.run( [ sys.executable, diff --git a/fastplotlib/utils/gui.py b/fastplotlib/utils/gui.py index 1f13c1406..1941674ee 100644 --- a/fastplotlib/utils/gui.py +++ b/fastplotlib/utils/gui.py @@ -60,9 +60,9 @@ def _notebook_print_banner(): # print logo and adapter info adapters = [a for a in wgpu.gpu.enumerate_adapters()] - adapters_info = [a.info for a in adapters] + adapters_info = [a.request_adapter_info() for a in adapters] - default_adapter_info = wgpu.gpu.request_adapter().info + default_adapter_info = wgpu.gpu.request_adapter().request_adapter_info() default_ix = adapters_info.index(default_adapter_info) if len(adapters) > 0: diff --git a/setup.py b/setup.py index ef79e4774..7229dcf25 100644 --- a/setup.py +++ b/setup.py @@ -4,8 +4,8 @@ install_requires = [ "numpy>=1.23.0", - "wgpu>=0.16.0", - "pygfx>=0.1.14", + "wgpu<0.16.0", + "pygfx>=0.1.14,<=0.2.0", ] 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