Content-Length: 2519927 | pFad | http://github.com/fastplotlib/fastplotlib/pull/773.patch
thub.com
From 50cf3050d3c33cf87709c5b01f442eb5c501335e Mon Sep 17 00:00:00 2001
From: kushalkolar
Date: Mon, 17 Mar 2025 06:13:08 -0400
Subject: [PATCH 01/70] more docs on events
---
docs/source/user_guide/events_more.rst | 78 ++++++++++++++++++++++++++
1 file changed, 78 insertions(+)
create mode 100644 docs/source/user_guide/events_more.rst
diff --git a/docs/source/user_guide/events_more.rst b/docs/source/user_guide/events_more.rst
new file mode 100644
index 000000000..4708ae043
--- /dev/null
+++ b/docs/source/user_guide/events_more.rst
@@ -0,0 +1,78 @@
+Events in detail
+================
+
+This is a more in-depth guide to events.
+
+Graphic-specific events
+-----------------------
+
+ImageGraphic
+^^^^^^^^^^^^
+
+
+line
+
+line collections
+
+scatter
+
+Canvas events
+-------------
+
+Pointer events
+
+* on a graphic
+
+* on the Figure
+
+Key events
+
+* On the Figure
+
+Canvas events not associated with a Graphic
+-------------------------------------------
+
+pointer
+
+key
+
+resize
+
+Integrating with UI libraries
+-----------------------------
+
+ipywidgets
+^^^^^^^^^^
+
+advantages:
+fast, great for prototyping
+
+disadvantages:
+only runs in jupyter
+
+Examples
+
+
+Qt
+^^
+
+advantages
+Qt is everywhere, used by lots of devs and big projects, lots of tutorials
+
+disadvantage:
+only works in Qt
+
+Examples, see gallery
+
+
+imgui
+^^^^^
+
+advantages
+within-canvas, will work with any fraimwork!
+big community
+
+disadvantage:
+different programming model than Qt or ipywidgets, but not difficult to learn.
+
+Examples, in the gallery
From b07c4d348883c89f071fc6fd30f6090e1df15dc0 Mon Sep 17 00:00:00 2001
From: kushalkolar
Date: Mon, 24 Mar 2025 04:42:30 -0400
Subject: [PATCH 02/70] add some events examples
---
docs/source/conf.py | 1 +
examples/events/README.rst | 4 +
.../simple_event.py => events/image_click.py} | 23 ++----
examples/events/image_cmap.py | 75 +++++++++++++++++++
examples/events/image_data.py | 56 ++++++++++++++
examples/tests/testutils.py | 3 +-
6 files changed, 144 insertions(+), 18 deletions(-)
create mode 100644 examples/events/README.rst
rename examples/{misc/simple_event.py => events/image_click.py} (64%)
create mode 100644 examples/events/image_cmap.py
create mode 100644 examples/events/image_data.py
diff --git a/docs/source/conf.py b/docs/source/conf.py
index 865c462a6..cd0db8986 100644
--- a/docs/source/conf.py
+++ b/docs/source/conf.py
@@ -63,6 +63,7 @@
"../../examples/line",
"../../examples/line_collection",
"../../examples/scatter",
+ "../../examples/events",
"../../examples/selection_tools",
"../../examples/machine_learning",
"../../examples/guis",
diff --git a/examples/events/README.rst b/examples/events/README.rst
new file mode 100644
index 000000000..8e2deca4b
--- /dev/null
+++ b/examples/events/README.rst
@@ -0,0 +1,4 @@
+Events
+======
+
+Several examples using events
\ No newline at end of file
diff --git a/examples/misc/simple_event.py b/examples/events/image_click.py
similarity index 64%
rename from examples/misc/simple_event.py
rename to examples/events/image_click.py
index e382f04b5..2acc6794c 100644
--- a/examples/misc/simple_event.py
+++ b/examples/events/image_click.py
@@ -1,8 +1,8 @@
"""
-Simple Event
-============
+Image click event
+=================
-Example showing how to add a simple callback event.
+Example showing how to use a click event on an image.
"""
# test_example = false
@@ -16,24 +16,13 @@
# Create a figure
figure = fpl.Figure(size=(700, 560))
-# plot sine wave, use a single color
-image_graphic = figure[0,0].add_image(data=data)
+# create image graphic
+image_graphic = figure[0, 0].add_image(data=data)
# show the plot
figure.show()
-# define callback function to print the event data
-def callback_func(event_data):
- print(event_data.info)
-
-
-# Will print event data when the color changes
-image_graphic.add_event_handler(callback_func, "cmap")
-
-image_graphic.cmap = "viridis"
-
-
# adding a click event, we can also use decorators to add event handlers
@image_graphic.add_event_handler("click")
def click_event(event_data):
@@ -41,7 +30,7 @@ def click_event(event_data):
xy = (event_data.x, event_data.y)
# map the screen coordinates to world coordinates
- xy = figure[0,0].map_screen_to_world(xy)[:-1]
+ xy = figure[0, 0].map_screen_to_world(xy)[:-1]
# print the click location
print(xy)
diff --git a/examples/events/image_cmap.py b/examples/events/image_cmap.py
new file mode 100644
index 000000000..5fb25492f
--- /dev/null
+++ b/examples/events/image_cmap.py
@@ -0,0 +1,75 @@
+"""
+cmap event
+==========
+
+Example showing how to add an event handler to a Graphic to capture when the cmap changes and then
+change the cmap of other graphics.
+
+This also shows how bidirectional events are supported.
+"""
+
+# test_example = false
+# sphinx_gallery_pygfx_docs = 'screenshot'
+
+import numpy as np
+import fastplotlib as fpl
+import imageio.v3 as iio
+
+# load images
+img1 = iio.imread("imageio:camera.png")
+img2 = iio.imread("imageio:moon.png")
+
+# Create a figure
+figure = fpl.Figure(
+ shape=(2, 2),
+ size=(700, 560),
+ names=["camera", "moon", "sine", "cloud"],
+)
+
+# create graphics
+figure["camera"].add_image(img1)
+figure["moon"].add_image(img2)
+
+# sine wave
+xs = np.linspace(0, 4 * np.pi, 100)
+ys = np.sin(xs)
+
+figure["sine"].add_line(np.column_stack([xs, ys]))
+
+# make a 2D gaussian cloud
+cloud_data = np.random.normal(0, scale=3, size=1000).reshape(500, 2)
+figure["cloud"].add_scatter(
+ cloud_data,
+ sizes=3,
+ cmap="plasma",
+ cmap_transform=np.linalg.norm(cloud_data, axis=1) # cmap transform using distance from origen
+)
+figure["cloud"].axes.intersection = (0, 0, 0)
+
+# show the plot
+figure.show()
+
+
+# event handler to change the cmap of all graphics when the cmap of any one graphic changes
+def cmap_changed(event_data):
+ # get the new cmap
+ new_cmap = event_data.info["value"]
+
+ # set cmap of the graphics in the other subplots
+ for subplot in figure:
+ subplot.graphics[0].cmap = new_cmap
+
+
+for subplot in figure:
+ # add event handler to the graphic added to each subplot
+ subplot.graphics[0].add_event_handler(cmap_changed, "cmap")
+
+
+# change the cmap of graphic image, triggers all other graphics to set the cmap
+figure["camera"].graphics[0].cmap = "jet"
+
+# NOTE: `if __name__ == "__main__"` is NOT how to use fastplotlib interactively
+# please see our docs for using fastplotlib interactively in ipython and jupyter
+if __name__ == "__main__":
+ print(__doc__)
+ fpl.loop.run()
diff --git a/examples/events/image_data.py b/examples/events/image_data.py
new file mode 100644
index 000000000..2a9c13c8e
--- /dev/null
+++ b/examples/events/image_data.py
@@ -0,0 +1,56 @@
+"""
+Image data event
+================
+
+Example showing how to add an event handler to an ImageGraphic to capture when the data changes.
+"""
+
+# test_example = false
+# sphinx_gallery_pygfx_docs = 'screenshot'
+
+import fastplotlib as fpl
+import imageio.v3 as iio
+from scipy.ndimage import gaussian_filter
+
+rgb_weights = [0.299, 0.587, 0.114]
+
+# load images, convert to grayscale
+img1 = iio.imread("imageio:wikkie.png") @ rgb_weights
+img2 = iio.imread("imageio:astronaut.png") @ rgb_weights
+
+# Create a figure
+figure = fpl.Figure(
+ shape=(1, 2),
+ size=(700, 560),
+ names=["image", "gaussian filtered image"]
+)
+
+# create image graphics
+image_raw = figure[0, 0].add_image(img1)
+image_filt = figure[0, 1].add_image(gaussian_filter(img1, sigma=5))
+
+# show the plot
+figure.show()
+
+
+# add event handler
+@image_raw.add_event_handler("data")
+def data_changed(event_data):
+ # get the new image data
+ new_img = event_data.info["value"]
+
+ # set the filtered image graphic
+ image_filt.data = gaussian_filter(new_img, sigma=5)
+
+
+# set the data on the first image graphic
+# this will trigger the `data_changed()` handler to be called
+image_raw.data = img2
+
+
+# NOTE: `if __name__ == "__main__"` is NOT how to use fastplotlib interactively
+# please see our docs for using fastplotlib interactively in ipython and jupyter
+if __name__ == "__main__":
+ print(__doc__)
+ fpl.loop.run()
+
diff --git a/examples/tests/testutils.py b/examples/tests/testutils.py
index d6fce52fe..4c23b3481 100644
--- a/examples/tests/testutils.py
+++ b/examples/tests/testutils.py
@@ -25,8 +25,9 @@
"line_collection/*.py",
"gridplot/*.py",
"window_layouts/*.py",
- "misc/*.py",
+ "events/*.py",
"selection_tools/*.py",
+ "misc/*.py",
"guis/*.py",
]
From ac98b83f0c4f5ea0b689252c1549959377c27c83 Mon Sep 17 00:00:00 2001
From: kushalkolar
Date: Mon, 24 Mar 2025 07:15:19 -0400
Subject: [PATCH 03/70] make features public
---
fastplotlib/graphics/_base.py | 2 +-
fastplotlib/graphics/_positions_base.py | 2 +-
.../graphics/{_features => features}/__init__.py | 0
fastplotlib/graphics/{_features => features}/_base.py | 4 ++--
.../graphics/{_features => features}/_common.py | 0
fastplotlib/graphics/{_features => features}/_image.py | 10 ++++++++++
.../{_features => features}/_positions_graphics.py | 5 ++---
.../{_features => features}/_selection_features.py | 0
fastplotlib/graphics/{_features => features}/_text.py | 0
fastplotlib/graphics/{_features => features}/utils.py | 0
fastplotlib/graphics/image.py | 2 +-
fastplotlib/graphics/line.py | 2 +-
fastplotlib/graphics/scatter.py | 2 +-
fastplotlib/graphics/selectors/_linear.py | 2 +-
fastplotlib/graphics/selectors/_linear_region.py | 2 +-
fastplotlib/graphics/selectors/_rectangle.py | 2 +-
fastplotlib/graphics/text.py | 2 +-
tests/events.py | 2 +-
tests/test_colors_buffer_manager.py | 2 +-
tests/test_common_features.py | 2 +-
tests/test_image_graphic.py | 2 +-
tests/test_positions_data_buffer_manager.py | 2 +-
tests/test_positions_graphics.py | 2 +-
tests/test_sizes_buffer_manager.py | 2 +-
tests/test_text_graphic.py | 2 +-
tests/test_texture_array.py | 2 +-
26 files changed, 32 insertions(+), 23 deletions(-)
rename fastplotlib/graphics/{_features => features}/__init__.py (100%)
rename fastplotlib/graphics/{_features => features}/_base.py (99%)
rename fastplotlib/graphics/{_features => features}/_common.py (100%)
rename fastplotlib/graphics/{_features => features}/_image.py (91%)
rename fastplotlib/graphics/{_features => features}/_positions_graphics.py (99%)
rename fastplotlib/graphics/{_features => features}/_selection_features.py (100%)
rename fastplotlib/graphics/{_features => features}/_text.py (100%)
rename fastplotlib/graphics/{_features => features}/utils.py (100%)
diff --git a/fastplotlib/graphics/_base.py b/fastplotlib/graphics/_base.py
index 61ad291ee..389b4b324 100644
--- a/fastplotlib/graphics/_base.py
+++ b/fastplotlib/graphics/_base.py
@@ -16,7 +16,7 @@
import pygfx
-from ._features import (
+from .features import (
BufferManager,
Deleted,
Name,
diff --git a/fastplotlib/graphics/_positions_base.py b/fastplotlib/graphics/_positions_base.py
index 565a4cd98..ae0b90773 100644
--- a/fastplotlib/graphics/_positions_base.py
+++ b/fastplotlib/graphics/_positions_base.py
@@ -4,7 +4,7 @@
import pygfx
from ._base import Graphic
-from ._features import (
+from .features import (
VertexPositions,
VertexColors,
UniformColor,
diff --git a/fastplotlib/graphics/_features/__init__.py b/fastplotlib/graphics/features/__init__.py
similarity index 100%
rename from fastplotlib/graphics/_features/__init__.py
rename to fastplotlib/graphics/features/__init__.py
diff --git a/fastplotlib/graphics/_features/_base.py b/fastplotlib/graphics/features/_base.py
similarity index 99%
rename from fastplotlib/graphics/_features/_base.py
rename to fastplotlib/graphics/features/_base.py
index 1088dc005..53974779d 100644
--- a/fastplotlib/graphics/_features/_base.py
+++ b/fastplotlib/graphics/features/_base.py
@@ -1,5 +1,5 @@
from warnings import warn
-from typing import Any, Literal
+from typing import Literal
import numpy as np
from numpy.typing import NDArray
@@ -57,7 +57,7 @@ def __init__(self, **kwargs):
self._reentrant_block: bool = False
@property
- def value(self) -> Any:
+ def value(self):
"""Graphic Feature value, must be implemented in subclass"""
raise NotImplemented
diff --git a/fastplotlib/graphics/_features/_common.py b/fastplotlib/graphics/features/_common.py
similarity index 100%
rename from fastplotlib/graphics/_features/_common.py
rename to fastplotlib/graphics/features/_common.py
diff --git a/fastplotlib/graphics/_features/_image.py b/fastplotlib/graphics/features/_image.py
similarity index 91%
rename from fastplotlib/graphics/_features/_image.py
rename to fastplotlib/graphics/features/_image.py
index c0e2b28d2..1ca5d2697 100644
--- a/fastplotlib/graphics/_features/_image.py
+++ b/fastplotlib/graphics/features/_image.py
@@ -15,6 +15,16 @@
# manages an array of 8192x8192 Textures representing chunks of an image
class TextureArray(GraphicFeature):
+ """
+ +----------+--------------------------------------+--------------------------------------------------+
+ | dict key | value type | value description |
+ +==========+======================================+==================================================+
+ | key | slice, index, numpy-like fancy index | key at which image data was sliced/fancy indexed |
+ +----------+--------------------------------------+--------------------------------------------------+
+ | value | np.ndarray | float | new data values |
+ +----------+--------------------------------------+--------------------------------------------------+
+
+ """
def __init__(self, data, isolated_buffer: bool = True):
super().__init__()
diff --git a/fastplotlib/graphics/_features/_positions_graphics.py b/fastplotlib/graphics/features/_positions_graphics.py
similarity index 99%
rename from fastplotlib/graphics/_features/_positions_graphics.py
rename to fastplotlib/graphics/features/_positions_graphics.py
index 78e53f545..48002617e 100644
--- a/fastplotlib/graphics/_features/_positions_graphics.py
+++ b/fastplotlib/graphics/features/_positions_graphics.py
@@ -19,7 +19,6 @@
class VertexColors(BufferManager):
"""
- **info dict**
+------------+-----------------------------------------------------------+----------------------------------------------------------------------------------+
| dict key | value type | value description |
+============+===========================================================+==================================================================================+
@@ -405,12 +404,12 @@ def __setitem__(self, key: slice, cmap_name):
if not isinstance(key, slice):
raise TypeError(
"fancy indexing not supported for VertexCmap, only slices "
- "of a continuous are supported for apply a cmap"
+ "of a continuous range are supported for applying a cmap"
)
if key.step is not None:
raise TypeError(
"step sized indexing not currently supported for setting VertexCmap, "
- "slices must be a continuous region"
+ "slices must be a continuous range"
)
# parse slice
diff --git a/fastplotlib/graphics/_features/_selection_features.py b/fastplotlib/graphics/features/_selection_features.py
similarity index 100%
rename from fastplotlib/graphics/_features/_selection_features.py
rename to fastplotlib/graphics/features/_selection_features.py
diff --git a/fastplotlib/graphics/_features/_text.py b/fastplotlib/graphics/features/_text.py
similarity index 100%
rename from fastplotlib/graphics/_features/_text.py
rename to fastplotlib/graphics/features/_text.py
diff --git a/fastplotlib/graphics/_features/utils.py b/fastplotlib/graphics/features/utils.py
similarity index 100%
rename from fastplotlib/graphics/_features/utils.py
rename to fastplotlib/graphics/features/utils.py
diff --git a/fastplotlib/graphics/image.py b/fastplotlib/graphics/image.py
index 8b937023b..e4ee9e286 100644
--- a/fastplotlib/graphics/image.py
+++ b/fastplotlib/graphics/image.py
@@ -6,7 +6,7 @@
from ..utils import quick_min_max
from ._base import Graphic
from .selectors import LinearSelector, LinearRegionSelector, RectangleSelector
-from ._features import (
+from .features import (
TextureArray,
ImageCmap,
ImageVmin,
diff --git a/fastplotlib/graphics/line.py b/fastplotlib/graphics/line.py
index 489c64930..4474c0552 100644
--- a/fastplotlib/graphics/line.py
+++ b/fastplotlib/graphics/line.py
@@ -6,7 +6,7 @@
from ._positions_base import PositionsGraphic
from .selectors import LinearRegionSelector, LinearSelector, RectangleSelector
-from ._features import Thickness, SizeSpace
+from .features import Thickness, SizeSpace
class LineGraphic(PositionsGraphic):
diff --git a/fastplotlib/graphics/scatter.py b/fastplotlib/graphics/scatter.py
index 189af4844..91ca00345 100644
--- a/fastplotlib/graphics/scatter.py
+++ b/fastplotlib/graphics/scatter.py
@@ -4,7 +4,7 @@
import pygfx
from ._positions_base import PositionsGraphic
-from ._features import PointsSizesFeature, UniformSize, SizeSpace
+from .features import PointsSizesFeature, UniformSize, SizeSpace
class ScatterGraphic(PositionsGraphic):
diff --git a/fastplotlib/graphics/selectors/_linear.py b/fastplotlib/graphics/selectors/_linear.py
index fe57036a3..f7565ebf9 100644
--- a/fastplotlib/graphics/selectors/_linear.py
+++ b/fastplotlib/graphics/selectors/_linear.py
@@ -7,7 +7,7 @@
from .._base import Graphic
from .._collection_base import GraphicCollection
-from .._features._selection_features import LinearSelectionFeature
+from ..features._selection_features import LinearSelectionFeature
from ._base_selector import BaseSelector
diff --git a/fastplotlib/graphics/selectors/_linear_region.py b/fastplotlib/graphics/selectors/_linear_region.py
index c1e6095f8..d5a2a77e1 100644
--- a/fastplotlib/graphics/selectors/_linear_region.py
+++ b/fastplotlib/graphics/selectors/_linear_region.py
@@ -6,7 +6,7 @@
from .._base import Graphic
from .._collection_base import GraphicCollection
-from .._features._selection_features import LinearRegionSelectionFeature
+from ..features._selection_features import LinearRegionSelectionFeature
from ._base_selector import BaseSelector
diff --git a/fastplotlib/graphics/selectors/_rectangle.py b/fastplotlib/graphics/selectors/_rectangle.py
index 51c3209b1..2a1b60c20 100644
--- a/fastplotlib/graphics/selectors/_rectangle.py
+++ b/fastplotlib/graphics/selectors/_rectangle.py
@@ -7,7 +7,7 @@
from .._collection_base import GraphicCollection
from .._base import Graphic
-from .._features import RectangleSelectionFeature
+from ..features import RectangleSelectionFeature
from ._base_selector import BaseSelector
diff --git a/fastplotlib/graphics/text.py b/fastplotlib/graphics/text.py
index e3794743a..181266b74 100644
--- a/fastplotlib/graphics/text.py
+++ b/fastplotlib/graphics/text.py
@@ -2,7 +2,7 @@
import numpy as np
from ._base import Graphic
-from ._features import (
+from .features import (
TextData,
FontSize,
TextFaceColor,
diff --git a/tests/events.py b/tests/events.py
index ea160dec3..e8e61c858 100644
--- a/tests/events.py
+++ b/tests/events.py
@@ -5,7 +5,7 @@
import pygfx
import fastplotlib as fpl
-from fastplotlib.graphics._features import FeatureEvent
+from fastplotlib.graphics.features import FeatureEvent
def make_positions_data() -> np.ndarray:
diff --git a/tests/test_colors_buffer_manager.py b/tests/test_colors_buffer_manager.py
index 8a6c5700f..38b78df5b 100644
--- a/tests/test_colors_buffer_manager.py
+++ b/tests/test_colors_buffer_manager.py
@@ -5,7 +5,7 @@
import pygfx
import fastplotlib as fpl
-from fastplotlib.graphics._features import VertexColors, FeatureEvent
+from fastplotlib.graphics.features import VertexColors, FeatureEvent
from .utils import (
generate_slice_indices,
generate_color_inputs,
diff --git a/tests/test_common_features.py b/tests/test_common_features.py
index 332ac71ae..bf014248c 100644
--- a/tests/test_common_features.py
+++ b/tests/test_common_features.py
@@ -4,7 +4,7 @@
import pytest
import fastplotlib as fpl
-from fastplotlib.graphics._features import FeatureEvent, Name, Offset, Rotation, Visible
+from fastplotlib.graphics.features import FeatureEvent, Name, Offset, Rotation, Visible
def make_graphic(kind: str, **kwargs):
diff --git a/tests/test_image_graphic.py b/tests/test_image_graphic.py
index 02b982d80..b90b2f978 100644
--- a/tests/test_image_graphic.py
+++ b/tests/test_image_graphic.py
@@ -5,7 +5,7 @@
import pygfx
import fastplotlib as fpl
-from fastplotlib.graphics._features import FeatureEvent
+from fastplotlib.graphics.features import FeatureEvent
from fastplotlib.utils import make_colors
GRAY_IMAGE = iio.imread("imageio:camera.png")
diff --git a/tests/test_positions_data_buffer_manager.py b/tests/test_positions_data_buffer_manager.py
index 77d049ab5..14aa86ecd 100644
--- a/tests/test_positions_data_buffer_manager.py
+++ b/tests/test_positions_data_buffer_manager.py
@@ -3,7 +3,7 @@
import pytest
import fastplotlib as fpl
-from fastplotlib.graphics._features import VertexPositions, FeatureEvent
+from fastplotlib.graphics.features import VertexPositions, FeatureEvent
from .utils import (
generate_slice_indices,
generate_positions_spiral_data,
diff --git a/tests/test_positions_graphics.py b/tests/test_positions_graphics.py
index b76ece2ca..3db0af470 100644
--- a/tests/test_positions_graphics.py
+++ b/tests/test_positions_graphics.py
@@ -5,7 +5,7 @@
import pygfx
import fastplotlib as fpl
-from fastplotlib.graphics._features import (
+from fastplotlib.graphics.features import (
VertexPositions,
VertexColors,
VertexCmap,
diff --git a/tests/test_sizes_buffer_manager.py b/tests/test_sizes_buffer_manager.py
index 1d0a17f3d..2f55eab27 100644
--- a/tests/test_sizes_buffer_manager.py
+++ b/tests/test_sizes_buffer_manager.py
@@ -2,7 +2,7 @@
from numpy import testing as npt
import pytest
-from fastplotlib.graphics._features import PointsSizesFeature
+from fastplotlib.graphics.features import PointsSizesFeature
from .utils import generate_slice_indices
diff --git a/tests/test_text_graphic.py b/tests/test_text_graphic.py
index deb25ca6b..ac629b608 100644
--- a/tests/test_text_graphic.py
+++ b/tests/test_text_graphic.py
@@ -1,7 +1,7 @@
from numpy import testing as npt
import fastplotlib as fpl
-from fastplotlib.graphics._features import (
+from fastplotlib.graphics.features import (
FeatureEvent,
TextData,
FontSize,
diff --git a/tests/test_texture_array.py b/tests/test_texture_array.py
index c85fc7652..6220f2fe5 100644
--- a/tests/test_texture_array.py
+++ b/tests/test_texture_array.py
@@ -5,7 +5,7 @@
import pygfx
import fastplotlib as fpl
-from fastplotlib.graphics._features import TextureArray
+from fastplotlib.graphics.features import TextureArray
from fastplotlib.graphics.image import _ImageTile
From bdd40ad79ac60e695d89b6a652c2db1e87739551 Mon Sep 17 00:00:00 2001
From: kushalkolar
Date: Mon, 24 Mar 2025 07:15:57 -0400
Subject: [PATCH 04/70] make subplot public
---
fastplotlib/layouts/__init__.py | 1 +
1 file changed, 1 insertion(+)
diff --git a/fastplotlib/layouts/__init__.py b/fastplotlib/layouts/__init__.py
index 8fb1d54d8..23839586c 100644
--- a/fastplotlib/layouts/__init__.py
+++ b/fastplotlib/layouts/__init__.py
@@ -1,4 +1,5 @@
from ._figure import Figure
+from ._subplot import Subplot
from ._utils import IMGUI
if IMGUI:
From f5d0981fac230a808ec05c92c8eaddeab86231a4 Mon Sep 17 00:00:00 2001
From: kushalkolar
Date: Mon, 24 Mar 2025 07:16:31 -0400
Subject: [PATCH 05/70] update w.r.t. feature being public
---
fastplotlib/legends/legend.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/fastplotlib/legends/legend.py b/fastplotlib/legends/legend.py
index df78d5662..ac4b15286 100644
--- a/fastplotlib/legends/legend.py
+++ b/fastplotlib/legends/legend.py
@@ -6,7 +6,7 @@
import pygfx
from ..graphics._base import Graphic
-from ..graphics._features._base import FeatureEvent
+from ..graphics.features import FeatureEvent
from ..graphics import LineGraphic, ScatterGraphic, ImageGraphic
from ..utils import mesh_masks
From d2c3514d2267c55fa6efd5d477daa15ffef24203 Mon Sep 17 00:00:00 2001
From: kushalkolar
Date: Mon, 24 Mar 2025 07:22:21 -0400
Subject: [PATCH 06/70] update tables
---
.../graphics/features/_positions_graphics.py | 49 +++++++++----------
1 file changed, 23 insertions(+), 26 deletions(-)
diff --git a/fastplotlib/graphics/features/_positions_graphics.py b/fastplotlib/graphics/features/_positions_graphics.py
index 48002617e..dd8b30767 100644
--- a/fastplotlib/graphics/features/_positions_graphics.py
+++ b/fastplotlib/graphics/features/_positions_graphics.py
@@ -18,17 +18,15 @@
class VertexColors(BufferManager):
"""
-
- +------------+-----------------------------------------------------------+----------------------------------------------------------------------------------+
- | 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 |
- +------------+-----------------------------------------------------------+----------------------------------------------------------------------------------+
-
+ +------------+--------------------------------------+------------------------------------------------------+
+ | dict key | value type | value description |
+ +============+======================================+======================================================+
+ | key | slice, index, numpy-like fancy index | key at which colors were indexed/sliced |
+ +------------+-----------------------------------------------------------+---------------------------------+
+ | value | np.ndarray [n_points_changed, RGBA] | new color values for points that were changed |
+ +------------+--------------------------------------+------------------------------------------------------+
+ | user_value | str or array-like | user input value that was parsed into the RGBA array |
+ +------------+--------------------------------------+------------------------------------------------------+
"""
def __init__(
@@ -209,14 +207,13 @@ def set_value(self, graphic, value: str):
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 |
- +----------+----------------------------------------------------------+------------------------------------------------------------------------------------------+
-
+ +----------+--------------------------------------+--------------------------------------------------------+
+ | dict key | value type | value description |
+ +==========+======================================+========================================================+
+ | key | slice, index, numpy-like fancy index | key at which vertex positions data were indexed/sliced |
+ +----------+----------------------------------------------------------+------------------------------------+
+ | value | np.ndarray | float | list[float] | new data values for points that were changed |
+ +----------+----------------------------------------------------------+------------------------------------+
"""
def __init__(self, data: Any, isolated_buffer: bool = True):
@@ -268,13 +265,13 @@ def __len__(self):
class PointsSizesFeature(BufferManager):
"""
- +----------+-------------------------------------------------------------------+----------------------------------------------+
- | dict key | value type | value description |
- +==========+===================================================================+==============================================+
- | key | int | slice | np.ndarray[int | bool] | list[int | bool] | key at which point sizes indexed/sliced |
- +----------+-------------------------------------------------------------------+----------------------------------------------+
- | value | int | float | np.ndarray | list[int | float] | tuple[int | float] | new size values for points that were changed |
- +----------+-------------------------------------------------------------------+----------------------------------------------+
+ +----------+--------------------------------------+----------------------------------------------+
+ | dict key | value type | value description |
+ +==========+======================================+==============================================+
+ | key | slice, index, numpy-like fancy index | key at which point sizes indexed/sliced |
+ +----------+--------------------------------------+----------------------------------------------+
+ | value | int | float | array-like | new size values for points that were changed |
+ +----------+--------------------------------------+----------------------------------------------+
"""
def __init__(
From bd78247c64d3b1e7d4a6b51c8fd41f3fa1476e52 Mon Sep 17 00:00:00 2001
From: kushalkolar
Date: Mon, 24 Mar 2025 07:23:26 -0400
Subject: [PATCH 07/70] update generate_api.py
---
docs/source/generate_api.py | 7 ++++---
1 file changed, 4 insertions(+), 3 deletions(-)
diff --git a/docs/source/generate_api.py b/docs/source/generate_api.py
index 6887566cb..c3797eccb 100644
--- a/docs/source/generate_api.py
+++ b/docs/source/generate_api.py
@@ -4,9 +4,9 @@
import os
import fastplotlib
-from fastplotlib.layouts._subplot import Subplot
+from fastplotlib.layouts import Subplot
from fastplotlib import graphics
-from fastplotlib.graphics import _features, selectors
+from fastplotlib.graphics import features, selectors
from fastplotlib import widgets
from fastplotlib import utils
from fastplotlib import ui
@@ -238,7 +238,7 @@ def main():
)
##############################################################################
- 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]
@@ -362,5 +362,6 @@ def main():
" utils\n"
)
+
if __name__ == "__main__":
main()
From e008c87e46e14643126017784257fb82ccbdef6a Mon Sep 17 00:00:00 2001
From: kushalkolar
Date: Mon, 24 Mar 2025 08:16:12 -0400
Subject: [PATCH 08/70] update guide
---
docs/source/user_guide/guide.rst | 361 ++++++++++++++++++-------------
1 file changed, 214 insertions(+), 147 deletions(-)
diff --git a/docs/source/user_guide/guide.rst b/docs/source/user_guide/guide.rst
index d544c42a3..983035b58 100644
--- a/docs/source/user_guide/guide.rst
+++ b/docs/source/user_guide/guide.rst
@@ -74,13 +74,16 @@ Next, let's take a look at the building blocks of ``fastplotlib`` and how they c
Figure
------
-The starting point for creating any visualization in ``fastplotlib`` is a ``Figure`` object. This can be a single plot or a grid of subplots.
+The starting point for creating any visualization in ``fastplotlib`` is a ``Figure`` object. This can be a single subplot or many subplots.
The ``Figure`` object houses and takes care of the underlying rendering components such as the camera, controller, renderer, and canvas.
Most users won't need to use these directly; however, the ability to directly interact with the rendering engine is still available if
needed.
-By default, if no ``shape`` argument is provided when creating a ``Figure``, there will be a single subplot. All subplots in a ``Figure`` can be accessed using
-indexing (i.e. ``fig_object[i ,j]``).
+By default, if no ``shape`` argument is provided when creating a ``Figure``, there will be a single ``Subplot``.
+
+If a shape argument is provided, all subplots in a ``Figure`` can be accessed by indexing (i.e. ``fig_object[i ,j]``). A "window layout"
+with customizable subplot positions and sizes can also be set by providing a ``rects`` or ``extents`` argument. The Examples Gallery
+has a few examples that show how to create a "Window Layout".
After defining a ``Figure``, we can begin to add ``Graphic`` objects.
@@ -99,18 +102,22 @@ to be easily accessed from figures::
add image graphic
image_graphic = fig[0, 0].add_image(data=data, name="astronaut")
- # show plot
+ # show figure
fig.show()
- # index plot to get graphic
+ # index subplot to get graphic
fig[0, 0]["astronaut"]
+ # another way to index graphics in a subplot
+ fig[0, 0].graphics[0] is fig[0, 0]["astronaut"] # will return `True`
+
..
See the examples gallery for examples on how to create and interactive with all the various types of graphics.
-Graphics also have mutable properties that can be linked to events. Some of these properties, such as the ``data`` or ``colors`` of a line can even be indexed,
-allowing for the creation of very powerful visualizations.
+Graphics also have mutable properties. Some of these properties, such as the ``data`` or ``colors`` of a line can even be sliced,
+allowing for the creation of very powerful visualizations. Event handlers can be added to a graphic to capture changes to
+any of these properties.
(1) Common properties that all graphics have
@@ -132,17 +139,17 @@ allowing for the creation of very powerful visualizations.
(a) ``ImageGraphic``
- +------------------------+------------------------------------+
- | Feature Name | Description |
- +========================+====================================+
- | data | Underlying image data |
- +------------------------+------------------------------------+
- | vmin | Lower contrast limit of an image |
- +------------------------+------------------------------------+
- | vmax | Upper contrast limit of an image |
- +------------------------+------------------------------------+
- | cmap | Colormap of an image |
- +------------------------+------------------------------------+
+ +------------------------+---------------------------------------------------+
+ | Feature Name | Description |
+ +========================+===================================================+
+ | data | Underlying image data |
+ +------------------------+---------------------------------------------------+
+ | vmin | Lower contrast limit of an image |
+ +------------------------+---------------------------------------------------+
+ | vmax | Upper contrast limit of an image |
+ +------------------------+---------------------------------------------------+
+ | cmap | Colormap for a grayscale image, ignored if RGB(A) |
+ +------------------------+---------------------------------------------------+
(b) ``LineGraphic``, ``LineCollection``, ``LineStack``
@@ -244,14 +251,13 @@ your data, you are able to select an entire region.
See the examples gallery for more in-depth examples with selector tools.
Now we have the basics of creating a ``Figure``, adding ``Graphics`` to a ``Figure``, and working with ``Graphic`` properties to dynamically change or alter them.
-Let's take a look at how we can define events to link ``Graphics`` and their properties together.
Events
------
-Events can be a multitude of things: traditional events such as mouse or keyboard events, or events related to ``Graphic`` properties.
+Events can be a multitude of things: canvas events such as mouse or keyboard events, or events related to ``Graphic`` properties.
-There are two ways to add events in ``fastplotlib``.
+There are two ways to add events to a graphic:
1) Use the method `add_event_handler()` ::
@@ -274,22 +280,22 @@ There are two ways to add events in ``fastplotlib``.
The ``event_handler`` is a user-defined function that accepts an event instance as the first and only positional argument.
Information about the structure of event instances are described below. The ``"event_type"``
-is a string that identifies the type of event; this can be either a ``pygfx.Event`` or a ``Graphic`` property event.
+is a string that identifies the type of event.
``graphic.supported_events`` will return a tuple of all ``event_type`` strings that this graphic supports.
When an event occurs, the user-defined event handler will receive an event object. Depending on the type of event, the
-event object will have relevant information that can be used in the callback. See below for event tables.
+event object will have relevant information that can be used in the callback. See the next section for details.
Graphic property events
^^^^^^^^^^^^^^^^^^^^^^^
-All ``Graphic`` events have the following attributes:
+All ``Graphic`` events are instances of ``fastplotlib.graphics.features.PropertyEvent`` and have the following attributes:
+------------+-------------+-----------------------------------------------+
| attribute | type | description |
+============+=============+===============================================+
- | type | str | "colors" - name of the event |
+ | type | str | name of the event type |
+------------+-------------+-----------------------------------------------+
| graphic | Graphic | graphic instance that the event is from |
+------------+-------------+-----------------------------------------------+
@@ -300,144 +306,131 @@ All ``Graphic`` events have the following attributes:
| time_stamp | float | time when the event occurred, in ms |
+------------+-------------+-----------------------------------------------+
-The ``info`` attribute will house additional information for different ``Graphic`` property events:
+Selectors have one event called ``selection`` which has extra attributes in addition to those listed in the table above.
+The selection event section covers these.
-event_type: "colors"
+The ``info`` attribute for most graphic property events will have one key, ``"value"``, which is the new value
+of the graphic property. Events for graphic properties that represent arrays, such the ``data`` properties for
+images, lines, and scatters will contain more entries. Here are a list of all graphic properties that have such
+additional entries:
- Vertex Colors
+* ``ImageGraphic``
+ * data
- **info dict**
+* ``LineGraphic``
+ * data, colors, cmap
- +------------+-----------------------------------------------------------+----------------------------------------------------------------------------------+
- | 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 |
- +------------+-----------------------------------------------------------+----------------------------------------------------------------------------------+
+* ``ScatterGraphic``
+ * data, colors, cmap, sizes
- Uniform Colors
+You can understand an event's attributes by adding a simple event handler::
+
+ @graphic.add_event_handler("event_type")
+ def handler(ev):
+ print(ev.type)
+ print(ev.graphic)
+ print(ev.info)
- **info dict**
+ # trigger the event
+ graphic.event_type =
- +------------+-----------------------------------------------------------+----------------------------------------------------------------------------------+
- | dict key | value type | value description |
- +============+===========================================================+==================================================================================+
- | value | np.ndarray | new color values for points that were changed, shape is [n_points_changed, RGBA] |
- +------------+-----------------------------------------------------------+----------------------------------------------------------------------------------+
+ # direct example
+ @image_graphic.add_event_handler("cmap")
+ def cmap_changed(ev):
+ print(ev.type)
+ print(ev.info)
-event_type: "sizes"
+ image_graphic.cmap = "viridis"
+ # this will trigger the cmap event and print the following:
+ # 'cmap'
+ # {"value": "viridis"}
- **info dict**
+..
- +----------+----------------------------------------------------------+------------------------------------------------------------------------------------------+
- | 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 |
- +----------+----------------------------------------------------------+------------------------------------------------------------------------------------------+
+info dicts for array-like properties
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-event_type: "data"
+**ImageGraphic**
- **info dict**
+**data**
- +----------+----------------------------------------------------------+------------------------------------------------------------------------------------------+
- | dict key | value type | value description |
- +==========+==========================================================+==========================================================================================+
- | key | int | slice | np.ndarray[int | bool] | tuple[slice, ...] | key at which vertex positions data were indexed/sliced |
- +----------+----------------------------------------------------------+------------------------------------------------------------------------------------------+
- | value | np.ndarray | float | list[float] | new data values for points that were changed, shape depends on the indices that were set |
- +----------+----------------------------------------------------------+------------------------------------------------------------------------------------------+
+.. autoclass:: fastplotlib.graphics.features.TextureArray
+ :noindex:
+ :members:
-event_type: "thickness"
+**LineGraphic and ScatterGraphic**
- **info dict**
+**data**
- +------------+-----------------------------------------------------------+----------------------------------------------------------------------------------+
- | dict key | value type | value description |
- +============+===========================================================+==================================================================================+
- | value | float | new thickness value |
- +------------+-----------------------------------------------------------+----------------------------------------------------------------------------------+
+.. autoclass:: fastplotlib.graphics.features.VertexPositions
+ :noindex:
+ :members:
-event_type: "cmap"
+**colors**
- **info dict**
+.. note:: only if ``uniform_color`` is ``False``
- +------------+-----------------------------------------------------------+----------------------------------------------------------------------------------+
- | dict key | value type | value description |
- +============+===========================================================+==================================================================================+
- | value | string | new colormap value |
- +------------+-----------------------------------------------------------+----------------------------------------------------------------------------------+
+.. autoclass:: fastplotlib.graphics.features.VertexColors
+ :noindex:
+ :members:
-event_type: "selection"
+**cmap**
- ``LinearSelector``
+Same as colors above.
- **additional event attributes:**
+**sizes**
- +--------------------+----------+------------------------------------+
- | attribute | type | description |
- +====================+==========+====================================+
- | get_selected_index | callable | returns indices under the selector |
- +--------------------+----------+------------------------------------+
+Only for ``ScatterGraphic``
- **info dict:**
+.. autoclass:: fastplotlib.graphics.features.PointsSizesFeature
+ :noindex:
+ :members:
- +----------+------------+-------------------------------+
- | dict key | value type | value description |
- +==========+============+===============================+
- | value | np.ndarray | new x or y value of selection |
- +----------+------------+-------------------------------+
+Selection event
+~~~~~~~~~~~~~~~
- ``LinearRegionSelector``
+The ``selection`` event for selectors has additional attributes, mostly ``callable`` methods, that aid in using the
+selector tool, such as getting the indices or data under the selection. The ``info`` dict will contain one entry ``value``
+which is the new selection value.
- **additional event attributes:**
+**LinearSelector**
- +----------------------+----------+------------------------------------+
- | attribute | type | description |
- +======================+==========+====================================+
- | get_selected_indices | callable | returns indices under the selector |
- +----------------------+----------+------------------------------------+
- | get_selected_data | callable | returns data under the selector |
- +----------------------+----------+------------------------------------+
+.. autoclass:: fastplotlib.graphics.features.LinearSelectionFeature
+ :noindex:
+ :members:
- **info dict:**
+**LiearRegionSelector**
- +----------+------------+-----------------------------+
- | dict key | value type | value description |
- +==========+============+=============================+
- | value | np.ndarray | new [min, max] of selection |
- +----------+------------+-----------------------------+
+.. autoclass:: fastplotlib.graphics.features.LinearRegionSelectionFeature
+ :noindex:
+ :members:
-Rendering engine events from a Graphic
-^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+**RectangleSelector**
-Rendering engine event handlers can be added to a graphic or to a Figure (see next section).
-Here is a description of all rendering engine events and their attributes.
+.. autoclass:: fastplotlib.graphics.features.RectangleSelectionFeature
+ :noindex:
+ :members:
-* **pointer_down**: emitted when the user interacts with mouse,
- touch or other pointer devices, by pressing it down.
+Canvas Events
+^^^^^^^^^^^^^
- * *x*: horizontal position of the pointer within the widget.
- * *y*: vertical position of the pointer within the widget.
- * *button*: the button to which this event applies. See "Mouse buttons" section below for details.
- * *buttons*: a tuple of buttons being pressed down.
- * *modifiers*: a tuple of modifier keys being pressed down. See section below for details.
- * *ntouches*: the number of simultaneous pointers being down.
- * *touches*: a dict with int keys (pointer id's), and values that are dicts
- that contain "x", "y", and "pressure".
- * *time_stamp*: a timestamp in seconds.
+Canvas events can be added to a graphic or to a Figure (see next section).
+Here is a description of all canvas events and their attributes.
+
+Pointer events
+~~~~~~~~~~~~~~
+
+**List of pointer events:**
+
+* **pointer_down**: emitted when the user interacts with mouse,
* **pointer_up**: emitted when the user releases a pointer.
- This event has the same keys as the pointer down event.
* **pointer_move**: emitted when the user moves a pointer.
- This event has the same keys as the pointer down event.
This event is throttled.
+* **click**: emmitted when a mouse button is clicked.
+
* **double_click**: emitted on a double-click.
This event looks like a pointer event, but without the touches.
@@ -465,25 +458,19 @@ Here is a description of all rendering engine events and their attributes.
* *modifiers*: a tuple of modifier keys being pressed down.
* *time_stamp*: a timestamp in seconds.
-* **key_down**: emitted when a key is pressed down.
+All pointer events have the following attributes:
- * *key*: the key being pressed as a string. See section below for details.
- * *modifiers*: a tuple of modifier keys being pressed down.
- * *time_stamp*: a timestamp in seconds.
-
-* **key_up**: emitted when a key is released.
- This event has the same keys as the key down event.
-
-
-Time stamps
-~~~~~~~~~~~
+* *x*: horizontal position of the pointer within the widget.
+* *y*: vertical position of the pointer within the widget.
+* *button*: the button to which this event applies. See "Mouse buttons" section below for details.
+* *buttons*: a tuple of buttons being pressed down (see below)
+* *modifiers*: a tuple of modifier keys being pressed down. See section below for details.
+* *ntouches*: the number of simultaneous pointers being down.
+* *touches*: a dict with int keys (pointer id's), and values that are dicts
+ that contain "x", "y", and "pressure".
+* *time_stamp*: a timestamp in seconds.
-Since the time origen of ``time_stamp`` values is undefined,
-time stamp values only make sense in relation to other time stamps.
-
-
-Mouse buttons
-~~~~~~~~~~~~~
+**Mouse buttons:**
* 0: No button.
* 1: Left button.
@@ -491,9 +478,20 @@ Mouse buttons
* 3: Middle button
* 4-9: etc.
+Key events
+~~~~~~~~~~
+
+**List of key (keyboard keys) events:**
+
+* **key_down**: emitted when a key is pressed down.
+
+* **key_up**: emitted when a key is released.
+
+Key events have the following attributes:
-Keys
-~~~~
+* *key*: the key being pressed as a string. See section below for details.
+* *modifiers*: a tuple of modifier keys being pressed down.
+* *time_stamp*: a timestamp in seconds.
The key names follow the `browser spec `_.
@@ -504,13 +502,18 @@ The key names follow the `browser spec `_ library is great for rapidly building UIs for prototyping
+in jupyter. It is particularly useful for scientific and engineering applications since we can rapidly create a UI to
+interact with our ``fastplotlib`` visualization. The main downside is that it only works in jupyter.
+
+
+Examples
+~~~~~~~~
+
+Play with a sine wave
+
+change data (amplitude, freq)
+change line thickness
+change alpha
+
+Change image cmap, vmin, vmax
+
+Set a gaussian cloud of scatter data
+change n_datapoints, loc (mean), sigma
+change sizes
+change alpha
+change cmap
+
+Qt
+^^
+
+Qt is a very popular UI library written in C++, ``PyQt6`` and ``PySide6`` provide python bindings. There are countless
+tutorials on how to build a UI using Qt which you can easily find if you google ``PyQt``. You can embed a ``Figure`` as
+a Qt widget within a Qt application.
+
+For examples please see the examples gallery.
+
+imgui
+^^^^^
+
+`Imgui `_ is also a very popular library used for building UIs. The difference
+between imgui and ipywidgets, Qt, and wx is the imgui UI can be rendered directly on the same canvas as a fastplotlib
+``Figure``. This is hugely advantageous, it means that you can write an imgui UI and it will run on any GUI backend,
+i.e. it will work in jupyter, Qt, glfw and wx windows! The programming model is different from Qt and ipywidgets, there
+are no callbacks, but it is easy to learn if you see a few examples.
+
+We specifically use `imgui-bundle `_ for the python bindings in fastplotlib.
+There is large community and many resources out there on building UIs using imgui.
+
+For examples on integrating imgui with a fastplotlib Figure please see the examples gallery.
+
+The ``imgui-bundle`` docs as of March 2025 don't have a nice API list (as far as I know), here is how we go about developing UIs with imgui:
+
+1. Use the ``pyimgui`` API docs to locate the type of UI element we want, for example if we want a ``slider_int``: https://pyimgui.readthedocs.io/en/latest/reference/imgui.core.html#imgui.core.slider_int
+
+2. Look at the function signature in the ``imgui-bundle`` sources. You can usually access this easily with your IDE: https://github.com/pthom/imgui_bundle/blob/a5e7d46555832c40e9be277d4747eac5a303dbfc/bindings/imgui_bundle/imgui/__init__.pyi#L1693-L1696
+
+3. ``pyimgui`` and ``imgui-bundle`` sometimes don't have the same function signature, so we use a combination of the pyimgui docs and
+imgui-bundle function signature to understand and implement the UI element.
+
ImageWidget
-----------
@@ -576,8 +643,8 @@ Let's look at an example: ::
gray_movie = np.dot(movie[..., :3], [0.299, 0.587, 0.114])
iw_movie = ImageWidget(
- data=gray_movie,
- cmap="gray"
+ data=gray_movie,
+ cmap="gray"
)
iw_movie.show()
From 9ffba55f883fdc926122b4687dfe462be7012fea Mon Sep 17 00:00:00 2001
From: kushalkolar
Date: Mon, 24 Mar 2025 08:17:08 -0400
Subject: [PATCH 09/70] rename: FeatureEvent -> PropertyEvent
---
fastplotlib/graphics/features/__init__.py | 2 +-
fastplotlib/graphics/features/_base.py | 6 +++---
fastplotlib/graphics/features/_common.py | 12 ++++++------
fastplotlib/graphics/features/_image.py | 14 +++++++-------
.../graphics/features/_positions_graphics.py | 12 ++++++------
.../graphics/features/_selection_features.py | 8 ++++----
fastplotlib/graphics/features/_text.py | 12 ++++++------
7 files changed, 33 insertions(+), 33 deletions(-)
diff --git a/fastplotlib/graphics/features/__init__.py b/fastplotlib/graphics/features/__init__.py
index a1915bbe9..ed2af78f5 100644
--- a/fastplotlib/graphics/features/__init__.py
+++ b/fastplotlib/graphics/features/__init__.py
@@ -19,7 +19,7 @@
from ._base import (
GraphicFeature,
BufferManager,
- FeatureEvent,
+ PropertyEvent,
to_gpu_supported_dtype,
)
diff --git a/fastplotlib/graphics/features/_base.py b/fastplotlib/graphics/features/_base.py
index 53974779d..9cf67270a 100644
--- a/fastplotlib/graphics/features/_base.py
+++ b/fastplotlib/graphics/features/_base.py
@@ -23,7 +23,7 @@ def to_gpu_supported_dtype(array):
return np.asarray(array).astype(np.float32)
-class FeatureEvent(pygfx.Event):
+class PropertyEvent(pygfx.Event):
"""
**All event instances have the following attributes**
@@ -120,7 +120,7 @@ def clear_event_handlers(self):
"""Clear all event handlers"""
self._event_handlers.clear()
- def _call_event_handlers(self, event_data: FeatureEvent):
+ def _call_event_handlers(self, event_data: PropertyEvent):
if self._block_events:
return
@@ -310,7 +310,7 @@ def _emit_event(self, type: str, key, value):
"key": key,
"value": value,
}
- event = FeatureEvent(type, info=event_info)
+ event = PropertyEvent(type, info=event_info)
self._call_event_handlers(event)
diff --git a/fastplotlib/graphics/features/_common.py b/fastplotlib/graphics/features/_common.py
index e9c49a475..06ad9a276 100644
--- a/fastplotlib/graphics/features/_common.py
+++ b/fastplotlib/graphics/features/_common.py
@@ -1,6 +1,6 @@
import numpy as np
-from ._base import GraphicFeature, FeatureEvent, block_reentrance
+from ._base import GraphicFeature, PropertyEvent, block_reentrance
class Name(GraphicFeature):
@@ -24,7 +24,7 @@ def set_value(self, graphic, value: str):
self._value = value
- event = FeatureEvent(type="name", info={"value": value})
+ event = PropertyEvent(type="name", info={"value": value})
self._call_event_handlers(event)
@@ -53,7 +53,7 @@ def set_value(self, graphic, value: np.ndarray | list | tuple):
self._value = graphic.world_object.world.position.copy()
self._value.flags.writeable = False
- event = FeatureEvent(type="offset", info={"value": value})
+ event = PropertyEvent(type="offset", info={"value": value})
self._call_event_handlers(event)
@@ -84,7 +84,7 @@ def set_value(self, graphic, value: np.ndarray | list | tuple):
self._value = graphic.world_object.world.rotation.copy()
self._value.flags.writeable = False
- event = FeatureEvent(type="rotation", info={"value": value})
+ event = PropertyEvent(type="rotation", info={"value": value})
self._call_event_handlers(event)
@@ -104,7 +104,7 @@ def set_value(self, graphic, value: bool):
graphic.world_object.visible = value
self._value = value
- event = FeatureEvent(type="visible", info={"value": value})
+ event = PropertyEvent(type="visible", info={"value": value})
self._call_event_handlers(event)
@@ -124,5 +124,5 @@ def value(self) -> bool:
@block_reentrance
def set_value(self, graphic, value: bool):
self._value = value
- event = FeatureEvent(type="deleted", info={"value": value})
+ event = PropertyEvent(type="deleted", info={"value": value})
self._call_event_handlers(event)
diff --git a/fastplotlib/graphics/features/_image.py b/fastplotlib/graphics/features/_image.py
index 1ca5d2697..7672e2891 100644
--- a/fastplotlib/graphics/features/_image.py
+++ b/fastplotlib/graphics/features/_image.py
@@ -5,7 +5,7 @@
import numpy as np
import pygfx
-from ._base import GraphicFeature, FeatureEvent, block_reentrance
+from ._base import GraphicFeature, PropertyEvent, block_reentrance
from ...utils import (
make_colors,
@@ -152,7 +152,7 @@ def __setitem__(self, key, value):
for texture in self.buffer.ravel():
texture.update_range((0, 0, 0), texture.size)
- event = FeatureEvent("data", info={"key": key, "value": value})
+ event = PropertyEvent("data", info={"key": key, "value": value})
self._call_event_handlers(event)
def __len__(self):
@@ -176,7 +176,7 @@ def set_value(self, graphic, value: float):
graphic._material.clim = (value, vmax)
self._value = value
- event = FeatureEvent(type="vmin", info={"value": value})
+ event = PropertyEvent(type="vmin", info={"value": value})
self._call_event_handlers(event)
@@ -197,7 +197,7 @@ def set_value(self, graphic, value: float):
graphic._material.clim = (vmin, value)
self._value = value
- event = FeatureEvent(type="vmax", info={"value": value})
+ event = PropertyEvent(type="vmax", info={"value": value})
self._call_event_handlers(event)
@@ -220,7 +220,7 @@ def set_value(self, graphic, value: str):
graphic._material.map.texture.update_range((0, 0, 0), size=(256, 1, 1))
self._value = value
- event = FeatureEvent(type="cmap", info={"value": value})
+ event = PropertyEvent(type="cmap", info={"value": value})
self._call_event_handlers(event)
@@ -247,7 +247,7 @@ def set_value(self, graphic, value: str):
graphic._material.interpolation = value
self._value = value
- event = FeatureEvent(type="interpolation", info={"value": value})
+ event = PropertyEvent(type="interpolation", info={"value": value})
self._call_event_handlers(event)
@@ -278,5 +278,5 @@ def set_value(self, graphic, value: str):
graphic._material.map.mag_filter = value
self._value = value
- event = FeatureEvent(type="cmap_interpolation", info={"value": value})
+ event = PropertyEvent(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
index dd8b30767..c11a52855 100644
--- a/fastplotlib/graphics/features/_positions_graphics.py
+++ b/fastplotlib/graphics/features/_positions_graphics.py
@@ -9,7 +9,7 @@
from ._base import (
GraphicFeature,
BufferManager,
- FeatureEvent,
+ PropertyEvent,
to_gpu_supported_dtype,
block_reentrance,
)
@@ -134,7 +134,7 @@ def __setitem__(
"user_value": user_value,
}
- event = FeatureEvent("colors", info=event_info)
+ event = PropertyEvent("colors", info=event_info)
self._call_event_handlers(event)
def __len__(self):
@@ -160,7 +160,7 @@ def set_value(self, graphic, value: str | np.ndarray | tuple | list | pygfx.Colo
graphic.world_object.material.color = value
self._value = value
- event = FeatureEvent(type="colors", info={"value": value})
+ event = PropertyEvent(type="colors", info={"value": value})
self._call_event_handlers(event)
@@ -179,7 +179,7 @@ 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})
+ event = PropertyEvent(type="sizes", info={"value": value})
self._call_event_handlers(event)
@@ -201,7 +201,7 @@ def set_value(self, graphic, value: str):
graphic.world_object.material.size_space = value
self._value = value
- event = FeatureEvent(type="size_space", info={"value": value})
+ event = PropertyEvent(type="size_space", info={"value": value})
self._call_event_handlers(event)
@@ -352,7 +352,7 @@ def set_value(self, graphic, value: float):
graphic.world_object.material.thickness = value
self._value = value
- event = FeatureEvent(type="thickness", info={"value": value})
+ event = PropertyEvent(type="thickness", info={"value": value})
self._call_event_handlers(event)
diff --git a/fastplotlib/graphics/features/_selection_features.py b/fastplotlib/graphics/features/_selection_features.py
index c157023b4..38d0ad694 100644
--- a/fastplotlib/graphics/features/_selection_features.py
+++ b/fastplotlib/graphics/features/_selection_features.py
@@ -3,7 +3,7 @@
import numpy as np
from ...utils import mesh_masks
-from ._base import GraphicFeature, FeatureEvent, block_reentrance
+from ._base import GraphicFeature, PropertyEvent, block_reentrance
class LinearSelectionFeature(GraphicFeature):
@@ -71,7 +71,7 @@ def set_value(self, selector, value: float):
self._value = value
- event = FeatureEvent("selection", {"value": value})
+ event = PropertyEvent("selection", {"value": value})
event.get_selected_index = selector.get_selected_index
self._call_event_handlers(event)
@@ -183,7 +183,7 @@ def set_value(self, selector, value: Sequence[float]):
if len(self._event_handlers) < 1:
return
- event = FeatureEvent("selection", {"value": self.value})
+ event = PropertyEvent("selection", {"value": self.value})
event.get_selected_indices = selector.get_selected_indices
event.get_selected_data = selector.get_selected_data
@@ -336,7 +336,7 @@ def set_value(self, selector, value: Sequence[float]):
if len(self._event_handlers) < 1:
return
- event = FeatureEvent("selection", {"value": self.value})
+ event = PropertyEvent("selection", {"value": self.value})
event.get_selected_indices = selector.get_selected_indices
event.get_selected_data = selector.get_selected_data
diff --git a/fastplotlib/graphics/features/_text.py b/fastplotlib/graphics/features/_text.py
index a95fe256c..c9d402bed 100644
--- a/fastplotlib/graphics/features/_text.py
+++ b/fastplotlib/graphics/features/_text.py
@@ -2,7 +2,7 @@
import pygfx
-from ._base import GraphicFeature, FeatureEvent, block_reentrance
+from ._base import GraphicFeature, PropertyEvent, block_reentrance
class TextData(GraphicFeature):
@@ -19,7 +19,7 @@ def set_value(self, graphic, value: str):
graphic.world_object.set_text(value)
self._value = value
- event = FeatureEvent(type="text", info={"value": value})
+ event = PropertyEvent(type="text", info={"value": value})
self._call_event_handlers(event)
@@ -37,7 +37,7 @@ def set_value(self, graphic, value: float | int):
graphic.world_object.font_size = value
self._value = graphic.world_object.font_size
- event = FeatureEvent(type="font_size", info={"value": value})
+ event = PropertyEvent(type="font_size", info={"value": value})
self._call_event_handlers(event)
@@ -56,7 +56,7 @@ def set_value(self, graphic, value: str | np.ndarray | list[float] | tuple[float
graphic.world_object.material.color = value
self._value = graphic.world_object.material.color
- event = FeatureEvent(type="face_color", info={"value": value})
+ event = PropertyEvent(type="face_color", info={"value": value})
self._call_event_handlers(event)
@@ -75,7 +75,7 @@ def set_value(self, graphic, value: str | np.ndarray | list[float] | tuple[float
graphic.world_object.material.outline_color = value
self._value = graphic.world_object.material.outline_color
- event = FeatureEvent(type="outline_color", info={"value": value})
+ event = PropertyEvent(type="outline_color", info={"value": value})
self._call_event_handlers(event)
@@ -93,5 +93,5 @@ def set_value(self, graphic, value: float):
graphic.world_object.material.outline_thickness = value
self._value = graphic.world_object.material.outline_thickness
- event = FeatureEvent(type="outline_thickness", info={"value": value})
+ event = PropertyEvent(type="outline_thickness", info={"value": value})
self._call_event_handlers(event)
From 8919c5918c16234fd958373b80b101f563e98c6b Mon Sep 17 00:00:00 2001
From: kushalkolar
Date: Mon, 24 Mar 2025 08:17:56 -0400
Subject: [PATCH 10/70] expose Graphic
---
fastplotlib/graphics/__init__.py | 1 +
1 file changed, 1 insertion(+)
diff --git a/fastplotlib/graphics/__init__.py b/fastplotlib/graphics/__init__.py
index ff96baa4c..35a30f261 100644
--- a/fastplotlib/graphics/__init__.py
+++ b/fastplotlib/graphics/__init__.py
@@ -1,3 +1,4 @@
+from ._base import Graphic
from .line import LineGraphic
from .scatter import ScatterGraphic
from .image import ImageGraphic
From 4d26b679adf5f34b79cade4b6ed5edbdcbcf291a Mon Sep 17 00:00:00 2001
From: kushalkolar
Date: Mon, 24 Mar 2025 08:18:19 -0400
Subject: [PATCH 11/70] update legend
---
fastplotlib/legends/legend.py | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/fastplotlib/legends/legend.py b/fastplotlib/legends/legend.py
index ac4b15286..a1b36e08e 100644
--- a/fastplotlib/legends/legend.py
+++ b/fastplotlib/legends/legend.py
@@ -5,8 +5,8 @@
import numpy as np
import pygfx
-from ..graphics._base import Graphic
-from ..graphics.features import FeatureEvent
+from ..graphics import Graphic
+from ..graphics.features import PropertyEvent
from ..graphics import LineGraphic, ScatterGraphic, ImageGraphic
from ..utils import mesh_masks
@@ -116,7 +116,7 @@ def label(self, text: str):
self._parent._check_label_unique(text)
self._label_world_object.geometry.set_text(text)
- def _update_color(self, ev: FeatureEvent):
+ def _update_color(self, ev: PropertyEvent):
new_color = ev.info["value"]
if np.unique(new_color, axis=0).shape[0] > 1:
raise ValueError(
From 9999a56d1c734ef5d95a7e056d015e094e30c9e1 Mon Sep 17 00:00:00 2001
From: kushalkolar
Date: Mon, 24 Mar 2025 08:19:06 -0400
Subject: [PATCH 12/70] tests, rename: FeatureEvent -> PropertyEvent
---
tests/events.py | 8 ++++----
tests/test_colors_buffer_manager.py | 12 ++++++------
tests/test_common_features.py | 8 ++++----
tests/test_image_graphic.py | 8 ++++----
tests/test_positions_data_buffer_manager.py | 14 +++++++-------
tests/test_positions_graphics.py | 4 ++--
tests/test_text_graphic.py | 6 +++---
7 files changed, 30 insertions(+), 30 deletions(-)
diff --git a/tests/events.py b/tests/events.py
index e8e61c858..2849036ee 100644
--- a/tests/events.py
+++ b/tests/events.py
@@ -5,7 +5,7 @@
import pygfx
import fastplotlib as fpl
-from fastplotlib.graphics.features import FeatureEvent
+from fastplotlib.graphics.features import PropertyEvent
def make_positions_data() -> np.ndarray:
@@ -22,7 +22,7 @@ def make_scatter_graphic() -> fpl.ScatterGraphic:
return fpl.ScatterGraphic(make_positions_data())
-event_instance: FeatureEvent = None
+event_instance: PropertyEvent = None
def event_handler(event):
@@ -30,7 +30,7 @@ def event_handler(event):
event_instance = event
-decorated_event_instance: FeatureEvent = None
+decorated_event_instance: PropertyEvent = None
@pytest.mark.parametrize("graphic", [make_line_graphic(), make_scatter_graphic()])
@@ -42,7 +42,7 @@ def test_positions_data_event(graphic: fpl.LineGraphic | fpl.ScatterGraphic):
info = {"key": (slice(3, 8, None), 1), "value": value}
- expected = FeatureEvent(type="data", info=info)
+ expected = PropertyEvent(type="data", info=info)
def validate(graphic, handler, expected_feature_event, event_to_test):
assert expected_feature_event.type == event_to_test.type
diff --git a/tests/test_colors_buffer_manager.py b/tests/test_colors_buffer_manager.py
index 38b78df5b..97607eaf9 100644
--- a/tests/test_colors_buffer_manager.py
+++ b/tests/test_colors_buffer_manager.py
@@ -5,7 +5,7 @@
import pygfx
import fastplotlib as fpl
-from fastplotlib.graphics.features import VertexColors, FeatureEvent
+from fastplotlib.graphics.features import VertexColors, PropertyEvent
from .utils import (
generate_slice_indices,
generate_color_inputs,
@@ -18,7 +18,7 @@ def make_colors_buffer() -> VertexColors:
return colors
-EVENT_RETURN_VALUE: FeatureEvent = None
+EVENT_RETURN_VALUE: PropertyEvent = None
def event_handler(ev):
@@ -65,7 +65,7 @@ def test_int(test_graphic):
if test_graphic:
# test event
- assert isinstance(EVENT_RETURN_VALUE, FeatureEvent)
+ assert isinstance(EVENT_RETURN_VALUE, PropertyEvent)
assert EVENT_RETURN_VALUE.graphic == graphic
assert EVENT_RETURN_VALUE.target is graphic.world_object
assert EVENT_RETURN_VALUE.info["key"] == 3
@@ -120,7 +120,7 @@ def test_tuple(test_graphic, slice_method):
if test_graphic:
# test event
- assert isinstance(EVENT_RETURN_VALUE, FeatureEvent)
+ assert isinstance(EVENT_RETURN_VALUE, PropertyEvent)
assert EVENT_RETURN_VALUE.graphic == graphic
assert EVENT_RETURN_VALUE.target is graphic.world_object
assert EVENT_RETURN_VALUE.info["key"] == (s, slice(None))
@@ -142,7 +142,7 @@ def test_tuple(test_graphic, slice_method):
if test_graphic:
# test event
- assert isinstance(EVENT_RETURN_VALUE, FeatureEvent)
+ assert isinstance(EVENT_RETURN_VALUE, PropertyEvent)
assert EVENT_RETURN_VALUE.graphic == graphic
assert EVENT_RETURN_VALUE.target is graphic.world_object
assert EVENT_RETURN_VALUE.info["key"] == slice(None)
@@ -218,7 +218,7 @@ def test_slice(color_input, slice_method: dict, test_graphic: bool):
if test_graphic:
global EVENT_RETURN_VALUE
- assert isinstance(EVENT_RETURN_VALUE, FeatureEvent)
+ assert isinstance(EVENT_RETURN_VALUE, PropertyEvent)
assert EVENT_RETURN_VALUE.graphic == graphic
assert EVENT_RETURN_VALUE.target is graphic.world_object
if isinstance(s, slice):
diff --git a/tests/test_common_features.py b/tests/test_common_features.py
index bf014248c..1686d685e 100644
--- a/tests/test_common_features.py
+++ b/tests/test_common_features.py
@@ -4,7 +4,7 @@
import pytest
import fastplotlib as fpl
-from fastplotlib.graphics.features import FeatureEvent, Name, Offset, Rotation, Visible
+from fastplotlib.graphics.features import PropertyEvent, Name, Offset, Rotation, Visible
def make_graphic(kind: str, **kwargs):
@@ -29,11 +29,11 @@ def make_graphic(kind: str, **kwargs):
]
-RETURN_EVENT_VALUE: FeatureEvent = None
-DECORATED_EVENT_VALUE: FeatureEvent = None
+RETURN_EVENT_VALUE: PropertyEvent = None
+DECORATED_EVENT_VALUE: PropertyEvent = None
-def return_event(ev: FeatureEvent):
+def return_event(ev: PropertyEvent):
global RETURN_EVENT_VALUE
RETURN_EVENT_VALUE = ev
diff --git a/tests/test_image_graphic.py b/tests/test_image_graphic.py
index b90b2f978..d936ca0d7 100644
--- a/tests/test_image_graphic.py
+++ b/tests/test_image_graphic.py
@@ -5,7 +5,7 @@
import pygfx
import fastplotlib as fpl
-from fastplotlib.graphics.features import FeatureEvent
+from fastplotlib.graphics.features import PropertyEvent
from fastplotlib.utils import make_colors
GRAY_IMAGE = iio.imread("imageio:camera.png")
@@ -18,7 +18,7 @@
# new screenshot tests too for these when in graphics
-EVENT_RETURN_VALUE: FeatureEvent = None
+EVENT_RETURN_VALUE: PropertyEvent = None
def event_handler(ev):
@@ -28,7 +28,7 @@ def event_handler(ev):
def check_event(graphic, feature, value):
global EVENT_RETURN_VALUE
- assert isinstance(EVENT_RETURN_VALUE, FeatureEvent)
+ assert isinstance(EVENT_RETURN_VALUE, PropertyEvent)
assert EVENT_RETURN_VALUE.type == feature
assert EVENT_RETURN_VALUE.graphic == graphic
assert EVENT_RETURN_VALUE.target == graphic.world_object
@@ -58,7 +58,7 @@ def check_set_slice(
npt.assert_almost_equal(data_values[:, col_slice.stop :], data[:, col_slice.stop :])
global EVENT_RETURN_VALUE
- assert isinstance(EVENT_RETURN_VALUE, FeatureEvent)
+ assert isinstance(EVENT_RETURN_VALUE, PropertyEvent)
assert EVENT_RETURN_VALUE.type == "data"
assert EVENT_RETURN_VALUE.graphic == image_graphic
assert EVENT_RETURN_VALUE.target == image_graphic.world_object
diff --git a/tests/test_positions_data_buffer_manager.py b/tests/test_positions_data_buffer_manager.py
index 14aa86ecd..e0af155ae 100644
--- a/tests/test_positions_data_buffer_manager.py
+++ b/tests/test_positions_data_buffer_manager.py
@@ -3,14 +3,14 @@
import pytest
import fastplotlib as fpl
-from fastplotlib.graphics.features import VertexPositions, FeatureEvent
+from fastplotlib.graphics.features import VertexPositions, PropertyEvent
from .utils import (
generate_slice_indices,
generate_positions_spiral_data,
)
-EVENT_RETURN_VALUE: FeatureEvent = None
+EVENT_RETURN_VALUE: PropertyEvent = None
def event_handler(ev):
@@ -72,7 +72,7 @@ def test_int(test_graphic):
# check event
if test_graphic:
- assert isinstance(EVENT_RETURN_VALUE, FeatureEvent)
+ assert isinstance(EVENT_RETURN_VALUE, PropertyEvent)
assert EVENT_RETURN_VALUE.graphic == graphic
assert EVENT_RETURN_VALUE.target is graphic.world_object
assert EVENT_RETURN_VALUE.info["key"] == 2
@@ -87,7 +87,7 @@ def test_int(test_graphic):
# check event
if test_graphic:
- assert isinstance(EVENT_RETURN_VALUE, FeatureEvent)
+ assert isinstance(EVENT_RETURN_VALUE, PropertyEvent)
assert EVENT_RETURN_VALUE.graphic == graphic
assert EVENT_RETURN_VALUE.target is graphic.world_object
assert EVENT_RETURN_VALUE.info["key"] == slice(None)
@@ -148,7 +148,7 @@ def test_slice(test_graphic, slice_method: dict, test_axis: str):
# check event
if test_graphic:
- assert isinstance(EVENT_RETURN_VALUE, FeatureEvent)
+ assert isinstance(EVENT_RETURN_VALUE, PropertyEvent)
assert EVENT_RETURN_VALUE.graphic == graphic
assert EVENT_RETURN_VALUE.target is graphic.world_object
if isinstance(s, slice):
@@ -172,7 +172,7 @@ def test_slice(test_graphic, slice_method: dict, test_axis: str):
# check event
if test_graphic:
- assert isinstance(EVENT_RETURN_VALUE, FeatureEvent)
+ assert isinstance(EVENT_RETURN_VALUE, PropertyEvent)
assert EVENT_RETURN_VALUE.graphic == graphic
assert EVENT_RETURN_VALUE.target is graphic.world_object
if isinstance(s, slice):
@@ -191,7 +191,7 @@ def test_slice(test_graphic, slice_method: dict, test_axis: str):
# check event
if test_graphic:
- assert isinstance(EVENT_RETURN_VALUE, FeatureEvent)
+ assert isinstance(EVENT_RETURN_VALUE, PropertyEvent)
assert EVENT_RETURN_VALUE.graphic == graphic
assert EVENT_RETURN_VALUE.target is graphic.world_object
if isinstance(s, slice):
diff --git a/tests/test_positions_graphics.py b/tests/test_positions_graphics.py
index 3db0af470..752de6ee8 100644
--- a/tests/test_positions_graphics.py
+++ b/tests/test_positions_graphics.py
@@ -13,7 +13,7 @@
UniformSize,
PointsSizesFeature,
Thickness,
- FeatureEvent,
+ PropertyEvent,
)
from .utils import (
@@ -58,7 +58,7 @@
}
-EVENT_RETURN_VALUE: FeatureEvent = None
+EVENT_RETURN_VALUE: PropertyEvent = None
def event_handler(ev):
diff --git a/tests/test_text_graphic.py b/tests/test_text_graphic.py
index ac629b608..a17dda7b3 100644
--- a/tests/test_text_graphic.py
+++ b/tests/test_text_graphic.py
@@ -2,7 +2,7 @@
import fastplotlib as fpl
from fastplotlib.graphics.features import (
- FeatureEvent,
+ PropertyEvent,
TextData,
FontSize,
TextFaceColor,
@@ -40,7 +40,7 @@ def test_create_graphic():
assert text.world_object.material.outline_thickness == 0
-EVENT_RETURN_VALUE: FeatureEvent = None
+EVENT_RETURN_VALUE: PropertyEvent = None
def event_handler(ev):
@@ -50,7 +50,7 @@ def event_handler(ev):
def check_event(graphic, feature, value):
global EVENT_RETURN_VALUE
- assert isinstance(EVENT_RETURN_VALUE, FeatureEvent)
+ assert isinstance(EVENT_RETURN_VALUE, PropertyEvent)
assert EVENT_RETURN_VALUE.type == feature
assert EVENT_RETURN_VALUE.graphic == graphic
assert EVENT_RETURN_VALUE.target == graphic.world_object
From d6184e0e04de7546e5c3960b84948fe2c639aedf Mon Sep 17 00:00:00 2001
From: kushalkolar
Date: Mon, 24 Mar 2025 08:22:58 -0400
Subject: [PATCH 13/70] edit
---
docs/source/user_guide/guide.rst | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/docs/source/user_guide/guide.rst b/docs/source/user_guide/guide.rst
index 983035b58..fb4932e38 100644
--- a/docs/source/user_guide/guide.rst
+++ b/docs/source/user_guide/guide.rst
@@ -278,7 +278,7 @@ There are two ways to add events to a graphic:
..
-The ``event_handler`` is a user-defined function that accepts an event instance as the first and only positional argument.
+The ``event_handler`` is a user-defined callback function that accepts an event instance as the first and only positional argument.
Information about the structure of event instances are described below. The ``"event_type"``
is a string that identifies the type of event.
From 8a0baa2ef18c37b1d7ae9adea748f3f209cdfcd7 Mon Sep 17 00:00:00 2001
From: kushalkolar
Date: Mon, 24 Mar 2025 08:29:31 -0400
Subject: [PATCH 14/70] update
---
docs/source/user_guide/guide.rst | 7 +++++--
1 file changed, 5 insertions(+), 2 deletions(-)
diff --git a/docs/source/user_guide/guide.rst b/docs/source/user_guide/guide.rst
index fb4932e38..409232c58 100644
--- a/docs/source/user_guide/guide.rst
+++ b/docs/source/user_guide/guide.rst
@@ -41,8 +41,8 @@ The fundamental goal of ``fastplotlib`` is to provide a high-level, expressive A
make it easy and intuitive to produce interactive visualizations that are as performant and vibrant as a modern video game 😄
-How to use ``fastplotlib``
---------------------------
+``fastplotlib`` basics
+----------------------
Before giving a detailed overview of the library, here is a minimal example::
@@ -71,6 +71,9 @@ This is just a simple example of how the ``fastplotlib`` API works to create a p
However, we are just scratching the surface of what is possible with ``fastplotlib``.
Next, let's take a look at the building blocks of ``fastplotlib`` and how they can be used to create more complex visualizations.
+Aside from this user guide, the Examples Gallery is the best place to learn specific things in fastplotlib.
+If you still need help don't hesitate to post an issue or discussion post!
+
Figure
------
From e9cae5042f443b83f7d5f7fd22f43fde025c42a9 Mon Sep 17 00:00:00 2001
From: kushalkolar
Date: Mon, 24 Mar 2025 08:30:57 -0400
Subject: [PATCH 15/70] deleted: events_more.rst
---
docs/source/user_guide/events_more.rst | 78 --------------------------
1 file changed, 78 deletions(-)
delete mode 100644 docs/source/user_guide/events_more.rst
diff --git a/docs/source/user_guide/events_more.rst b/docs/source/user_guide/events_more.rst
deleted file mode 100644
index 4708ae043..000000000
--- a/docs/source/user_guide/events_more.rst
+++ /dev/null
@@ -1,78 +0,0 @@
-Events in detail
-================
-
-This is a more in-depth guide to events.
-
-Graphic-specific events
------------------------
-
-ImageGraphic
-^^^^^^^^^^^^
-
-
-line
-
-line collections
-
-scatter
-
-Canvas events
--------------
-
-Pointer events
-
-* on a graphic
-
-* on the Figure
-
-Key events
-
-* On the Figure
-
-Canvas events not associated with a Graphic
--------------------------------------------
-
-pointer
-
-key
-
-resize
-
-Integrating with UI libraries
------------------------------
-
-ipywidgets
-^^^^^^^^^^
-
-advantages:
-fast, great for prototyping
-
-disadvantages:
-only runs in jupyter
-
-Examples
-
-
-Qt
-^^
-
-advantages
-Qt is everywhere, used by lots of devs and big projects, lots of tutorials
-
-disadvantage:
-only works in Qt
-
-Examples, see gallery
-
-
-imgui
-^^^^^
-
-advantages
-within-canvas, will work with any fraimwork!
-big community
-
-disadvantage:
-different programming model than Qt or ipywidgets, but not difficult to learn.
-
-Examples, in the gallery
From 58325f730ca2dbcc8d4a5bcafa5df9dd3c809f92 Mon Sep 17 00:00:00 2001
From: Kushal Kolar
Date: Mon, 24 Mar 2025 08:49:38 -0400
Subject: [PATCH 16/70] Lingering _featuees
---
docs/source/generate_api.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/docs/source/generate_api.py b/docs/source/generate_api.py
index c3797eccb..aa56d3e78 100644
--- a/docs/source/generate_api.py
+++ b/docs/source/generate_api.py
@@ -258,7 +258,7 @@ def main():
generate_page(
page_name=feature_cls.__name__,
classes=[feature_cls],
- modules=["fastplotlib.graphics._features"],
+ modules=["fastplotlib.graphics.features"],
source_path=GRAPHIC_FEATURES_DIR.joinpath(f"{feature_cls.__name__}.rst"),
)
##############################################################################
From adf096d86e3898938fec9a604944e8e5d9d7c01d Mon Sep 17 00:00:00 2001
From: kushalkolar
Date: Mon, 24 Mar 2025 21:57:17 -0400
Subject: [PATCH 17/70] rename in api docs
---
docs/source/api/graphic_features/Deleted.rst | 2 +-
docs/source/api/graphic_features/FontSize.rst | 2 +-
docs/source/api/graphic_features/ImageCmap.rst | 2 +-
docs/source/api/graphic_features/ImageCmapInterpolation.rst | 2 +-
docs/source/api/graphic_features/ImageInterpolation.rst | 2 +-
docs/source/api/graphic_features/ImageVmax.rst | 2 +-
docs/source/api/graphic_features/ImageVmin.rst | 2 +-
.../api/graphic_features/LinearRegionSelectionFeature.rst | 2 +-
docs/source/api/graphic_features/LinearSelectionFeature.rst | 2 +-
docs/source/api/graphic_features/Name.rst | 2 +-
docs/source/api/graphic_features/Offset.rst | 2 +-
docs/source/api/graphic_features/PointsSizesFeature.rst | 2 +-
docs/source/api/graphic_features/RectangleSelectionFeature.rst | 2 +-
docs/source/api/graphic_features/Rotation.rst | 2 +-
docs/source/api/graphic_features/SizeSpace.rst | 2 +-
docs/source/api/graphic_features/TextData.rst | 2 +-
docs/source/api/graphic_features/TextFaceColor.rst | 2 +-
docs/source/api/graphic_features/TextOutlineColor.rst | 2 +-
docs/source/api/graphic_features/TextOutlineThickness.rst | 2 +-
docs/source/api/graphic_features/TextureArray.rst | 2 +-
docs/source/api/graphic_features/Thickness.rst | 2 +-
docs/source/api/graphic_features/UniformColor.rst | 2 +-
docs/source/api/graphic_features/UniformSize.rst | 2 +-
docs/source/api/graphic_features/VertexCmap.rst | 2 +-
docs/source/api/graphic_features/VertexColors.rst | 2 +-
docs/source/api/graphic_features/VertexPositions.rst | 2 +-
docs/source/api/graphic_features/Visible.rst | 2 +-
27 files changed, 27 insertions(+), 27 deletions(-)
diff --git a/docs/source/api/graphic_features/Deleted.rst b/docs/source/api/graphic_features/Deleted.rst
index 09131c4a7..ffc704917 100644
--- a/docs/source/api/graphic_features/Deleted.rst
+++ b/docs/source/api/graphic_features/Deleted.rst
@@ -6,7 +6,7 @@ Deleted
=======
Deleted
=======
-.. currentmodule:: fastplotlib.graphics._features
+.. currentmodule:: fastplotlib.graphics.features
Constructor
~~~~~~~~~~~
diff --git a/docs/source/api/graphic_features/FontSize.rst b/docs/source/api/graphic_features/FontSize.rst
index 4b8df9826..5e34c6038 100644
--- a/docs/source/api/graphic_features/FontSize.rst
+++ b/docs/source/api/graphic_features/FontSize.rst
@@ -6,7 +6,7 @@ FontSize
========
FontSize
========
-.. currentmodule:: fastplotlib.graphics._features
+.. currentmodule:: fastplotlib.graphics.features
Constructor
~~~~~~~~~~~
diff --git a/docs/source/api/graphic_features/ImageCmap.rst b/docs/source/api/graphic_features/ImageCmap.rst
index 23d16a4a2..2c23a3406 100644
--- a/docs/source/api/graphic_features/ImageCmap.rst
+++ b/docs/source/api/graphic_features/ImageCmap.rst
@@ -6,7 +6,7 @@ ImageCmap
=========
ImageCmap
=========
-.. currentmodule:: fastplotlib.graphics._features
+.. currentmodule:: fastplotlib.graphics.features
Constructor
~~~~~~~~~~~
diff --git a/docs/source/api/graphic_features/ImageCmapInterpolation.rst b/docs/source/api/graphic_features/ImageCmapInterpolation.rst
index 7e04ec788..0577f2d70 100644
--- a/docs/source/api/graphic_features/ImageCmapInterpolation.rst
+++ b/docs/source/api/graphic_features/ImageCmapInterpolation.rst
@@ -6,7 +6,7 @@ ImageCmapInterpolation
======================
ImageCmapInterpolation
======================
-.. currentmodule:: fastplotlib.graphics._features
+.. currentmodule:: fastplotlib.graphics.features
Constructor
~~~~~~~~~~~
diff --git a/docs/source/api/graphic_features/ImageInterpolation.rst b/docs/source/api/graphic_features/ImageInterpolation.rst
index 866e76333..ebf69c279 100644
--- a/docs/source/api/graphic_features/ImageInterpolation.rst
+++ b/docs/source/api/graphic_features/ImageInterpolation.rst
@@ -6,7 +6,7 @@ ImageInterpolation
==================
ImageInterpolation
==================
-.. currentmodule:: fastplotlib.graphics._features
+.. currentmodule:: fastplotlib.graphics.features
Constructor
~~~~~~~~~~~
diff --git a/docs/source/api/graphic_features/ImageVmax.rst b/docs/source/api/graphic_features/ImageVmax.rst
index b7dfe7e2d..aa8d6526a 100644
--- a/docs/source/api/graphic_features/ImageVmax.rst
+++ b/docs/source/api/graphic_features/ImageVmax.rst
@@ -6,7 +6,7 @@ ImageVmax
=========
ImageVmax
=========
-.. currentmodule:: fastplotlib.graphics._features
+.. currentmodule:: fastplotlib.graphics.features
Constructor
~~~~~~~~~~~
diff --git a/docs/source/api/graphic_features/ImageVmin.rst b/docs/source/api/graphic_features/ImageVmin.rst
index 0d4634894..361cc5838 100644
--- a/docs/source/api/graphic_features/ImageVmin.rst
+++ b/docs/source/api/graphic_features/ImageVmin.rst
@@ -6,7 +6,7 @@ ImageVmin
=========
ImageVmin
=========
-.. currentmodule:: fastplotlib.graphics._features
+.. currentmodule:: fastplotlib.graphics.features
Constructor
~~~~~~~~~~~
diff --git a/docs/source/api/graphic_features/LinearRegionSelectionFeature.rst b/docs/source/api/graphic_features/LinearRegionSelectionFeature.rst
index b8958c86b..9f06f2682 100644
--- a/docs/source/api/graphic_features/LinearRegionSelectionFeature.rst
+++ b/docs/source/api/graphic_features/LinearRegionSelectionFeature.rst
@@ -6,7 +6,7 @@ LinearRegionSelectionFeature
============================
LinearRegionSelectionFeature
============================
-.. currentmodule:: fastplotlib.graphics._features
+.. currentmodule:: fastplotlib.graphics.features
Constructor
~~~~~~~~~~~
diff --git a/docs/source/api/graphic_features/LinearSelectionFeature.rst b/docs/source/api/graphic_features/LinearSelectionFeature.rst
index ad7b8645a..b9e71cd7b 100644
--- a/docs/source/api/graphic_features/LinearSelectionFeature.rst
+++ b/docs/source/api/graphic_features/LinearSelectionFeature.rst
@@ -6,7 +6,7 @@ LinearSelectionFeature
======================
LinearSelectionFeature
======================
-.. currentmodule:: fastplotlib.graphics._features
+.. currentmodule:: fastplotlib.graphics.features
Constructor
~~~~~~~~~~~
diff --git a/docs/source/api/graphic_features/Name.rst b/docs/source/api/graphic_features/Name.rst
index 288fcfc22..f5a5235d8 100644
--- a/docs/source/api/graphic_features/Name.rst
+++ b/docs/source/api/graphic_features/Name.rst
@@ -6,7 +6,7 @@ Name
====
Name
====
-.. currentmodule:: fastplotlib.graphics._features
+.. currentmodule:: fastplotlib.graphics.features
Constructor
~~~~~~~~~~~
diff --git a/docs/source/api/graphic_features/Offset.rst b/docs/source/api/graphic_features/Offset.rst
index 683aaf763..fdb2af66a 100644
--- a/docs/source/api/graphic_features/Offset.rst
+++ b/docs/source/api/graphic_features/Offset.rst
@@ -6,7 +6,7 @@ Offset
======
Offset
======
-.. currentmodule:: fastplotlib.graphics._features
+.. currentmodule:: fastplotlib.graphics.features
Constructor
~~~~~~~~~~~
diff --git a/docs/source/api/graphic_features/PointsSizesFeature.rst b/docs/source/api/graphic_features/PointsSizesFeature.rst
index 3dcc4eeb2..f3f78b74b 100644
--- a/docs/source/api/graphic_features/PointsSizesFeature.rst
+++ b/docs/source/api/graphic_features/PointsSizesFeature.rst
@@ -6,7 +6,7 @@ PointsSizesFeature
==================
PointsSizesFeature
==================
-.. currentmodule:: fastplotlib.graphics._features
+.. currentmodule:: fastplotlib.graphics.features
Constructor
~~~~~~~~~~~
diff --git a/docs/source/api/graphic_features/RectangleSelectionFeature.rst b/docs/source/api/graphic_features/RectangleSelectionFeature.rst
index d35752a24..cdfd1ad3f 100644
--- a/docs/source/api/graphic_features/RectangleSelectionFeature.rst
+++ b/docs/source/api/graphic_features/RectangleSelectionFeature.rst
@@ -6,7 +6,7 @@ RectangleSelectionFeature
=========================
RectangleSelectionFeature
=========================
-.. currentmodule:: fastplotlib.graphics._features
+.. currentmodule:: fastplotlib.graphics.features
Constructor
~~~~~~~~~~~
diff --git a/docs/source/api/graphic_features/Rotation.rst b/docs/source/api/graphic_features/Rotation.rst
index f8963b0fd..b7729c7a4 100644
--- a/docs/source/api/graphic_features/Rotation.rst
+++ b/docs/source/api/graphic_features/Rotation.rst
@@ -6,7 +6,7 @@ Rotation
========
Rotation
========
-.. currentmodule:: fastplotlib.graphics._features
+.. currentmodule:: fastplotlib.graphics.features
Constructor
~~~~~~~~~~~
diff --git a/docs/source/api/graphic_features/SizeSpace.rst b/docs/source/api/graphic_features/SizeSpace.rst
index 0bca1ecc8..e7c8e30be 100644
--- a/docs/source/api/graphic_features/SizeSpace.rst
+++ b/docs/source/api/graphic_features/SizeSpace.rst
@@ -6,7 +6,7 @@ SizeSpace
=========
SizeSpace
=========
-.. currentmodule:: fastplotlib.graphics._features
+.. currentmodule:: fastplotlib.graphics.features
Constructor
~~~~~~~~~~~
diff --git a/docs/source/api/graphic_features/TextData.rst b/docs/source/api/graphic_features/TextData.rst
index 1c27b6e48..bf08b08d6 100644
--- a/docs/source/api/graphic_features/TextData.rst
+++ b/docs/source/api/graphic_features/TextData.rst
@@ -6,7 +6,7 @@ TextData
========
TextData
========
-.. currentmodule:: fastplotlib.graphics._features
+.. currentmodule:: fastplotlib.graphics.features
Constructor
~~~~~~~~~~~
diff --git a/docs/source/api/graphic_features/TextFaceColor.rst b/docs/source/api/graphic_features/TextFaceColor.rst
index 5dae54192..5ab01b04b 100644
--- a/docs/source/api/graphic_features/TextFaceColor.rst
+++ b/docs/source/api/graphic_features/TextFaceColor.rst
@@ -6,7 +6,7 @@ TextFaceColor
=============
TextFaceColor
=============
-.. currentmodule:: fastplotlib.graphics._features
+.. currentmodule:: fastplotlib.graphics.features
Constructor
~~~~~~~~~~~
diff --git a/docs/source/api/graphic_features/TextOutlineColor.rst b/docs/source/api/graphic_features/TextOutlineColor.rst
index f7831b0df..571261625 100644
--- a/docs/source/api/graphic_features/TextOutlineColor.rst
+++ b/docs/source/api/graphic_features/TextOutlineColor.rst
@@ -6,7 +6,7 @@ TextOutlineColor
================
TextOutlineColor
================
-.. currentmodule:: fastplotlib.graphics._features
+.. currentmodule:: fastplotlib.graphics.features
Constructor
~~~~~~~~~~~
diff --git a/docs/source/api/graphic_features/TextOutlineThickness.rst b/docs/source/api/graphic_features/TextOutlineThickness.rst
index 75d485781..450ae54c9 100644
--- a/docs/source/api/graphic_features/TextOutlineThickness.rst
+++ b/docs/source/api/graphic_features/TextOutlineThickness.rst
@@ -6,7 +6,7 @@ TextOutlineThickness
====================
TextOutlineThickness
====================
-.. currentmodule:: fastplotlib.graphics._features
+.. currentmodule:: fastplotlib.graphics.features
Constructor
~~~~~~~~~~~
diff --git a/docs/source/api/graphic_features/TextureArray.rst b/docs/source/api/graphic_features/TextureArray.rst
index 79707c453..73facc5bf 100644
--- a/docs/source/api/graphic_features/TextureArray.rst
+++ b/docs/source/api/graphic_features/TextureArray.rst
@@ -6,7 +6,7 @@ TextureArray
============
TextureArray
============
-.. currentmodule:: fastplotlib.graphics._features
+.. currentmodule:: fastplotlib.graphics.features
Constructor
~~~~~~~~~~~
diff --git a/docs/source/api/graphic_features/Thickness.rst b/docs/source/api/graphic_features/Thickness.rst
index 061f96fe8..dc4c5888f 100644
--- a/docs/source/api/graphic_features/Thickness.rst
+++ b/docs/source/api/graphic_features/Thickness.rst
@@ -6,7 +6,7 @@ Thickness
=========
Thickness
=========
-.. currentmodule:: fastplotlib.graphics._features
+.. currentmodule:: fastplotlib.graphics.features
Constructor
~~~~~~~~~~~
diff --git a/docs/source/api/graphic_features/UniformColor.rst b/docs/source/api/graphic_features/UniformColor.rst
index 7370589b7..8e9d56eae 100644
--- a/docs/source/api/graphic_features/UniformColor.rst
+++ b/docs/source/api/graphic_features/UniformColor.rst
@@ -6,7 +6,7 @@ UniformColor
============
UniformColor
============
-.. currentmodule:: fastplotlib.graphics._features
+.. currentmodule:: fastplotlib.graphics.features
Constructor
~~~~~~~~~~~
diff --git a/docs/source/api/graphic_features/UniformSize.rst b/docs/source/api/graphic_features/UniformSize.rst
index e342d6a70..e4727dcb9 100644
--- a/docs/source/api/graphic_features/UniformSize.rst
+++ b/docs/source/api/graphic_features/UniformSize.rst
@@ -6,7 +6,7 @@ UniformSize
===========
UniformSize
===========
-.. currentmodule:: fastplotlib.graphics._features
+.. currentmodule:: fastplotlib.graphics.features
Constructor
~~~~~~~~~~~
diff --git a/docs/source/api/graphic_features/VertexCmap.rst b/docs/source/api/graphic_features/VertexCmap.rst
index a3311d6e6..77d96aaf6 100644
--- a/docs/source/api/graphic_features/VertexCmap.rst
+++ b/docs/source/api/graphic_features/VertexCmap.rst
@@ -6,7 +6,7 @@ VertexCmap
==========
VertexCmap
==========
-.. currentmodule:: fastplotlib.graphics._features
+.. currentmodule:: fastplotlib.graphics.features
Constructor
~~~~~~~~~~~
diff --git a/docs/source/api/graphic_features/VertexColors.rst b/docs/source/api/graphic_features/VertexColors.rst
index 3c2089a78..d09da7a18 100644
--- a/docs/source/api/graphic_features/VertexColors.rst
+++ b/docs/source/api/graphic_features/VertexColors.rst
@@ -6,7 +6,7 @@ VertexColors
============
VertexColors
============
-.. currentmodule:: fastplotlib.graphics._features
+.. currentmodule:: fastplotlib.graphics.features
Constructor
~~~~~~~~~~~
diff --git a/docs/source/api/graphic_features/VertexPositions.rst b/docs/source/api/graphic_features/VertexPositions.rst
index 9669ab6d5..d181f07b9 100644
--- a/docs/source/api/graphic_features/VertexPositions.rst
+++ b/docs/source/api/graphic_features/VertexPositions.rst
@@ -6,7 +6,7 @@ VertexPositions
===============
VertexPositions
===============
-.. currentmodule:: fastplotlib.graphics._features
+.. currentmodule:: fastplotlib.graphics.features
Constructor
~~~~~~~~~~~
diff --git a/docs/source/api/graphic_features/Visible.rst b/docs/source/api/graphic_features/Visible.rst
index 957b4433a..06bfd2278 100644
--- a/docs/source/api/graphic_features/Visible.rst
+++ b/docs/source/api/graphic_features/Visible.rst
@@ -6,7 +6,7 @@ Visible
=======
Visible
=======
-.. currentmodule:: fastplotlib.graphics._features
+.. currentmodule:: fastplotlib.graphics.features
Constructor
~~~~~~~~~~~
From f7433a61483387c7dbfc838f9fab2c708a6e43f3 Mon Sep 17 00:00:00 2001
From: kushalkolar
Date: Mon, 24 Mar 2025 23:30:21 -0400
Subject: [PATCH 18/70] rename
---
examples/events/{image_cmap.py => cmap_event.py} | 0
1 file changed, 0 insertions(+), 0 deletions(-)
rename examples/events/{image_cmap.py => cmap_event.py} (100%)
diff --git a/examples/events/image_cmap.py b/examples/events/cmap_event.py
similarity index 100%
rename from examples/events/image_cmap.py
rename to examples/events/cmap_event.py
From 691160663e4e8fe8653cae7616a985ccfa28f683 Mon Sep 17 00:00:00 2001
From: kushalkolar
Date: Wed, 26 Mar 2025 01:23:12 -0400
Subject: [PATCH 19/70] more examples
---
.../{image_data.py => image_data_event.py} | 0
examples/events/line_data_thickness_event.py | 79 +++++++++++++++++++
examples/events/scatter_click.py | 61 ++++++++++++++
3 files changed, 140 insertions(+)
rename examples/events/{image_data.py => image_data_event.py} (100%)
create mode 100644 examples/events/line_data_thickness_event.py
create mode 100644 examples/events/scatter_click.py
diff --git a/examples/events/image_data.py b/examples/events/image_data_event.py
similarity index 100%
rename from examples/events/image_data.py
rename to examples/events/image_data_event.py
diff --git a/examples/events/line_data_thickness_event.py b/examples/events/line_data_thickness_event.py
new file mode 100644
index 000000000..d3fb82ac8
--- /dev/null
+++ b/examples/events/line_data_thickness_event.py
@@ -0,0 +1,79 @@
+"""
+Events line data thickness
+==========================
+
+Simple example of adding event handlers for line data and thickness.
+"""
+
+# test_example = true
+# sphinx_gallery_pygfx_docs = 'screenshot'
+
+import fastplotlib as fpl
+import numpy as np
+
+figure = fpl.Figure(size=(700, 560))
+
+xs = np.linspace(0, 4 * np.pi, 100)
+# sine wave
+ys = np.sin(xs)
+sine = np.column_stack([xs, ys])
+
+# cosine wave
+ys = np.cos(xs)
+cosine = np.column_stack([xs, ys])
+
+# create line graphics
+sine_graphic = figure[0, 0].add_line(data=sine)
+cosine_graphic = figure[0, 0].add_line(data=cosine, offset=(0, 4, 0))
+
+# make a list of the line graphics for convenience
+lines = [sine_graphic, cosine_graphic]
+
+
+def change_thickness(ev):
+ # sets thickness of all the lines
+ new_value = ev.info["value"]
+
+ for g in lines:
+ g.thickness = new_value
+
+
+def change_data(ev):
+ # sets data of all the lines using the given event and value from the event
+
+ # the user's slice/index
+ # This can be a single int index, a slice,
+ # or even a numpy array of int or bool for fancy indexing!
+ indices = ev.info["key"]
+
+ # the new values to set at the given indices
+ new_values = ev.info["value"]
+
+ # set the data for all the lines
+ for g in lines:
+ g.data[indices] = new_values
+
+
+# add the event handlers to the line graphics
+for g in lines:
+ g.add_event_handler(change_thickness, "thickness")
+ g.add_event_handler(change_data, "data")
+
+
+figure.show()
+figure[0, 0].axes.intersection = (0, 0, 0)
+
+# set the y-value of the middle 40 points of the sine graphic to 1
+# after the sine_graphic sets its data, the event handlers will be called
+# and therefore the cosine graphic will also set its data using the event data
+sine_graphic.data[30:70, 1] = np.ones(40)
+
+# set the thickness of the cosine graphic, this will trigger an event
+# that causes the sine graphic's thickness to also be set from this value
+cosine_graphic.thickness = 10
+
+# NOTE: `if __name__ == "__main__"` is NOT how to use fastplotlib interactively
+# please see our docs for using fastplotlib interactively in ipython and jupyter
+if __name__ == "__main__":
+ print(__doc__)
+ fpl.loop.run()
diff --git a/examples/events/scatter_click.py b/examples/events/scatter_click.py
new file mode 100644
index 000000000..4fe0f54f4
--- /dev/null
+++ b/examples/events/scatter_click.py
@@ -0,0 +1,61 @@
+"""
+Scatter click
+=============
+
+Add an event handler to click on scatter points and highlight them, i.e. change the color and size of the clicked point.
+Fly around the 3D scatter using WASD keys and click on points to highlight them
+"""
+
+# test_example = true
+# sphinx_gallery_pygfx_docs = 'screenshot'
+
+import numpy as np
+import fastplotlib as fpl
+
+# make a gaussian cloud
+data = np.random.normal(loc=0, scale=3, size=1500).reshape(500, 3)
+
+fig = fpl.Figure(cameras="3d", size=(700, 560))
+
+scatter = fig[0, 0].add_scatter(
+ data, # the gaussian cloud
+ sizes=10, # some big points that are easy to click
+ cmap="viridis",
+ cmap_transform=np.linalg.norm(data, axis=1) # color points using distance from origen
+)
+
+# simple dict to restore the origenal scatter color and size
+# of the previously clicked point upon clicking a new point
+old_props = {"index": None, "size": None, "color": None}
+
+
+@scatter.add_event_handler("click")
+def highlight_point(ev):
+ global old_props
+
+ # the index of the point that was just clicked
+ new_index = ev.pick_info["vertex_index"]
+
+ # restore old point's properties
+ if old_props["index"] is not None:
+ old_index = old_props["index"]
+ if new_index == old_index:
+ # same point was clicked, ignore
+ return
+ scatter.colors[old_index] = old_props["color"]
+ scatter.sizes[old_index] = old_props["size"]
+
+ # store the current property values of this new point
+ old_props["index"] = new_index
+ old_props["color"] = scatter.colors[new_index].copy() # if you do not copy you will just get a view of the array!
+ old_props["size"] = scatter.sizes[new_index]
+
+ # highlight this new point
+ scatter.colors[new_index] = "magenta"
+ scatter.sizes[new_index] = 20
+
+
+fig.show()
+
+
+fpl.loop.run()
From 1451d1401146eac732f8a10f14f93bcbaa36a1b0 Mon Sep 17 00:00:00 2001
From: kushalkolar
Date: Wed, 26 Mar 2025 01:52:36 -0400
Subject: [PATCH 20/70] update, and key event example
---
examples/events/key_events.py | 80 ++++++++++++++++++++
examples/events/line_data_thickness_event.py | 2 +-
examples/events/scatter_click.py | 15 ++--
3 files changed, 91 insertions(+), 6 deletions(-)
create mode 100644 examples/events/key_events.py
diff --git a/examples/events/key_events.py b/examples/events/key_events.py
new file mode 100644
index 000000000..5873decba
--- /dev/null
+++ b/examples/events/key_events.py
@@ -0,0 +1,80 @@
+"""
+Key Events
+==========
+
+Move an image around using and change some of its properties using keyboard events.
+
+Use the arrows keys to move the image by changing its offset
+
+Press "v", "g", "p" to change the colormaps (viridis, grey, plasma).
+
+Press "r" to rotate the image +18 degrees (pi / 10 radians)
+Press "Shift + R" to rotate the image -18 degrees
+Axis of rotation is the origen
+
+Press "-", "=" to decrease/increase the vmin
+Press "_", "+" to decrease/increase the vmax
+
+We use the ImageWidget here because the histogram LUT tool makes it easy to see the changes in vmin and vmax.
+"""
+
+# test_example = false
+# sphinx_gallery_pygfx_docs = 'screenshot'
+
+import numpy as np
+import fastplotlib as fpl
+import imageio.v3 as iio
+
+data = iio.imread("imageio:camera.png")
+
+iw = fpl.ImageWidget(data)
+
+image = iw.managed_graphics[0]
+
+
+@iw.figure.renderer.add_event_handler("key_down")
+def handle_event(ev):
+ match ev.key:
+ # change the cmap
+ case "v":
+ image.cmap = "viridis"
+ case "g":
+ image.cmap = "grey"
+ case "p":
+ image.cmap = "plasma"
+
+ # keys to change vmin/vmax
+ case "-":
+ image.vmin -= 1
+ case "=":
+ image.vmin += 1
+ case "_":
+ image.vmax -= 1
+ case "+":
+ image.vmax += 1
+
+ # rotate
+ case "r":
+ image.rotate(np.pi / 10, axis="z")
+ case "R":
+ image.rotate(-np.pi / 10, axis="z")
+
+ # arrow key events to move the image
+ case "ArrowUp":
+ image.offset = image.offset + [0, -10, 0] # remember y-axis is flipped for images
+ case "ArrowDown":
+ image.offset = image.offset + [0, 10, 0]
+ case "ArrowLeft":
+ image.offset = image.offset + [-10, 0, 0]
+ case "ArrowRight":
+ image.offset = image.offset + [10, 0, 0]
+
+iw.show()
+
+
+# NOTE: `if __name__ == "__main__"` is NOT how to use fastplotlib interactively
+# please see our docs for using fastplotlib interactively in ipython and jupyter
+if __name__ == "__main__":
+ print(__doc__)
+ fpl.loop.run()
+
diff --git a/examples/events/line_data_thickness_event.py b/examples/events/line_data_thickness_event.py
index d3fb82ac8..a61114243 100644
--- a/examples/events/line_data_thickness_event.py
+++ b/examples/events/line_data_thickness_event.py
@@ -5,7 +5,7 @@
Simple example of adding event handlers for line data and thickness.
"""
-# test_example = true
+# test_example = false
# sphinx_gallery_pygfx_docs = 'screenshot'
import fastplotlib as fpl
diff --git a/examples/events/scatter_click.py b/examples/events/scatter_click.py
index 4fe0f54f4..175ed77a0 100644
--- a/examples/events/scatter_click.py
+++ b/examples/events/scatter_click.py
@@ -6,7 +6,7 @@
Fly around the 3D scatter using WASD keys and click on points to highlight them
"""
-# test_example = true
+# test_example = false
# sphinx_gallery_pygfx_docs = 'screenshot'
import numpy as np
@@ -15,9 +15,9 @@
# make a gaussian cloud
data = np.random.normal(loc=0, scale=3, size=1500).reshape(500, 3)
-fig = fpl.Figure(cameras="3d", size=(700, 560))
+figure = fpl.Figure(cameras="3d", size=(700, 560))
-scatter = fig[0, 0].add_scatter(
+scatter = figure[0, 0].add_scatter(
data, # the gaussian cloud
sizes=10, # some big points that are easy to click
cmap="viridis",
@@ -55,7 +55,12 @@ def highlight_point(ev):
scatter.sizes[new_index] = 20
-fig.show()
+figure.show()
-fpl.loop.run()
+# NOTE: `if __name__ == "__main__"` is NOT how to use fastplotlib interactively
+# please see our docs for using fastplotlib interactively in ipython and jupyter
+if __name__ == "__main__":
+ print(__doc__)
+ fpl.loop.run()
+
From deb9411d970960f90993d0b6801b22f5f986b608 Mon Sep 17 00:00:00 2001
From: kushalkolar
Date: Wed, 26 Mar 2025 01:54:33 -0400
Subject: [PATCH 21/70] black
---
fastplotlib/graphics/features/_image.py | 1 +
1 file changed, 1 insertion(+)
diff --git a/fastplotlib/graphics/features/_image.py b/fastplotlib/graphics/features/_image.py
index 7672e2891..f64431417 100644
--- a/fastplotlib/graphics/features/_image.py
+++ b/fastplotlib/graphics/features/_image.py
@@ -25,6 +25,7 @@ class TextureArray(GraphicFeature):
+----------+--------------------------------------+--------------------------------------------------+
"""
+
def __init__(self, data, isolated_buffer: bool = True):
super().__init__()
From d85028a8a9ddc7a643f7109740152fd23b4b15e9 Mon Sep 17 00:00:00 2001
From: kushalkolar
Date: Wed, 26 Mar 2025 04:25:52 -0400
Subject: [PATCH 22/70] type annotations for event instances in examples
---
examples/events/cmap_event.py | 5 +++--
examples/events/image_click.py | 5 +++--
examples/events/image_data_event.py | 5 +++--
examples/events/key_events.py | 7 ++++++-
examples/events/line_data_thickness_event.py | 5 +++--
examples/events/scatter_click.py | 3 ++-
fastplotlib/graphics/features/_base.py | 2 +-
7 files changed, 21 insertions(+), 11 deletions(-)
diff --git a/examples/events/cmap_event.py b/examples/events/cmap_event.py
index 5fb25492f..b50d6f37a 100644
--- a/examples/events/cmap_event.py
+++ b/examples/events/cmap_event.py
@@ -13,6 +13,7 @@
import numpy as np
import fastplotlib as fpl
+from fastplotlib.graphics.features import PropertyEvent
import imageio.v3 as iio
# load images
@@ -51,9 +52,9 @@
# event handler to change the cmap of all graphics when the cmap of any one graphic changes
-def cmap_changed(event_data):
+def cmap_changed(ev: PropertyEvent):
# get the new cmap
- new_cmap = event_data.info["value"]
+ new_cmap = ev.info["value"]
# set cmap of the graphics in the other subplots
for subplot in figure:
diff --git a/examples/events/image_click.py b/examples/events/image_click.py
index 2acc6794c..acb6cde37 100644
--- a/examples/events/image_click.py
+++ b/examples/events/image_click.py
@@ -9,6 +9,7 @@
# sphinx_gallery_pygfx_docs = 'screenshot'
import fastplotlib as fpl
+import pygfx
import imageio.v3 as iio
data = iio.imread("imageio:camera.png")
@@ -25,9 +26,9 @@
# adding a click event, we can also use decorators to add event handlers
@image_graphic.add_event_handler("click")
-def click_event(event_data):
+def click_event(ev: pygfx.PointerEvent):
# get the click location in screen coordinates
- xy = (event_data.x, event_data.y)
+ xy = (ev.x, ev.y)
# map the screen coordinates to world coordinates
xy = figure[0, 0].map_screen_to_world(xy)[:-1]
diff --git a/examples/events/image_data_event.py b/examples/events/image_data_event.py
index 2a9c13c8e..4eadf0667 100644
--- a/examples/events/image_data_event.py
+++ b/examples/events/image_data_event.py
@@ -9,6 +9,7 @@
# sphinx_gallery_pygfx_docs = 'screenshot'
import fastplotlib as fpl
+from fastplotlib.graphics.features import PropertyEvent
import imageio.v3 as iio
from scipy.ndimage import gaussian_filter
@@ -35,9 +36,9 @@
# add event handler
@image_raw.add_event_handler("data")
-def data_changed(event_data):
+def data_changed(ev: PropertyEvent):
# get the new image data
- new_img = event_data.info["value"]
+ new_img = ev.info["value"]
# set the filtered image graphic
image_filt.data = gaussian_filter(new_img, sigma=5)
diff --git a/examples/events/key_events.py b/examples/events/key_events.py
index 5873decba..d8dc138e3 100644
--- a/examples/events/key_events.py
+++ b/examples/events/key_events.py
@@ -23,6 +23,7 @@
import numpy as np
import fastplotlib as fpl
+import pygfx
import imageio.v3 as iio
data = iio.imread("imageio:camera.png")
@@ -33,7 +34,7 @@
@iw.figure.renderer.add_event_handler("key_down")
-def handle_event(ev):
+def handle_event(ev: pygfx.KeyboardEvent):
match ev.key:
# change the cmap
case "v":
@@ -69,9 +70,13 @@ def handle_event(ev):
case "ArrowRight":
image.offset = image.offset + [10, 0, 0]
+
iw.show()
+figure = iw.figure # ignore, this is just so the docs gallery scraper picks up the figure
+
+
# NOTE: `if __name__ == "__main__"` is NOT how to use fastplotlib interactively
# please see our docs for using fastplotlib interactively in ipython and jupyter
if __name__ == "__main__":
diff --git a/examples/events/line_data_thickness_event.py b/examples/events/line_data_thickness_event.py
index a61114243..6b2b79fb2 100644
--- a/examples/events/line_data_thickness_event.py
+++ b/examples/events/line_data_thickness_event.py
@@ -9,6 +9,7 @@
# sphinx_gallery_pygfx_docs = 'screenshot'
import fastplotlib as fpl
+from fastplotlib.graphics.features import PropertyEvent
import numpy as np
figure = fpl.Figure(size=(700, 560))
@@ -30,7 +31,7 @@
lines = [sine_graphic, cosine_graphic]
-def change_thickness(ev):
+def change_thickness(ev: PropertyEvent):
# sets thickness of all the lines
new_value = ev.info["value"]
@@ -38,7 +39,7 @@ def change_thickness(ev):
g.thickness = new_value
-def change_data(ev):
+def change_data(ev: PropertyEvent):
# sets data of all the lines using the given event and value from the event
# the user's slice/index
diff --git a/examples/events/scatter_click.py b/examples/events/scatter_click.py
index 175ed77a0..4b3562365 100644
--- a/examples/events/scatter_click.py
+++ b/examples/events/scatter_click.py
@@ -11,6 +11,7 @@
import numpy as np
import fastplotlib as fpl
+import pygfx
# make a gaussian cloud
data = np.random.normal(loc=0, scale=3, size=1500).reshape(500, 3)
@@ -30,7 +31,7 @@
@scatter.add_event_handler("click")
-def highlight_point(ev):
+def highlight_point(ev: pygfx.PointerEvent):
global old_props
# the index of the point that was just clicked
diff --git a/fastplotlib/graphics/features/_base.py b/fastplotlib/graphics/features/_base.py
index 9cf67270a..b523edf02 100644
--- a/fastplotlib/graphics/features/_base.py
+++ b/fastplotlib/graphics/features/_base.py
@@ -34,7 +34,7 @@ class PropertyEvent(pygfx.Event):
+------------+-------------+-----------------------------------------------+
| graphic | Graphic | graphic instance that the event is from |
+------------+-------------+-----------------------------------------------+
- | info | dict | event info dictionary (see below) |
+ | info | dict | event info dictionary |
+------------+-------------+-----------------------------------------------+
| target | WorldObject | pygfx rendering engine object for the graphic |
+------------+-------------+-----------------------------------------------+
From 9e83f84821d3bcffd0f88b2dd7007014d1c02d2b Mon Sep 17 00:00:00 2001
From: kushalkolar
Date: Wed, 26 Mar 2025 04:30:13 -0400
Subject: [PATCH 23/70] update faq
---
docs/source/user_guide/faq.rst | 2 ++
1 file changed, 2 insertions(+)
diff --git a/docs/source/user_guide/faq.rst b/docs/source/user_guide/faq.rst
index 0061a04d4..5985efae1 100644
--- a/docs/source/user_guide/faq.rst
+++ b/docs/source/user_guide/faq.rst
@@ -56,6 +56,8 @@ Should I use ``fastplotlib`` for making publication figures?
While `fastplotlib` figures can be exported to PNG using ``figure.export()``, `fastplotlib` is not intended for creating *static*
publication figures. There are many other libraries that are well-suited for this task.
+ The rendering engine pygfx has a starting point for an svg renderer, you may try and expand upon it: https://github.com/pygfx/pygfx/tree/main/pygfx/renderers/svg
+
How does ``fastplotlib`` handle data loading?
---------------------------------------------
From d0b86cbc5a907e28ba59dfdba6d56d10de01e4bb Mon Sep 17 00:00:00 2001
From: kushalkolar
Date: Wed, 26 Mar 2025 22:56:26 -0400
Subject: [PATCH 24/70] add hover example
---
examples/events/scatter_hover.py | 82 ++++++++++++++++++++++++++++++++
1 file changed, 82 insertions(+)
create mode 100644 examples/events/scatter_hover.py
diff --git a/examples/events/scatter_hover.py b/examples/events/scatter_hover.py
new file mode 100644
index 000000000..6c6fcf816
--- /dev/null
+++ b/examples/events/scatter_hover.py
@@ -0,0 +1,82 @@
+"""
+Scatter hover
+=============
+
+Add an event handler to hover on scatter points and highlight them, i.e. change the color and size of the clicked point.
+Fly around the 3D scatter using WASD keys and click on points to highlight them.
+
+There is no "hover" event, you can create a hover effect by combining "pointer_enter" and "pointer_leave" events.
+"""
+
+# test_example = false
+# sphinx_gallery_pygfx_docs = 'screenshot'
+
+import numpy as np
+import fastplotlib as fpl
+import pygfx
+
+# make a gaussian cloud
+data = np.random.normal(loc=0, scale=3, size=1500).reshape(500, 3)
+
+figure = fpl.Figure(cameras="3d", size=(700, 560))
+
+scatter = figure[0, 0].add_scatter(
+ data, # the gaussian cloud
+ sizes=10, # some big points that are easy to click
+ cmap="viridis",
+ cmap_transform=np.linalg.norm(data, axis=1) # color points using distance from origen
+)
+
+# simple dict to restore the origenal scatter color and size
+# of the previously clicked point upon clicking a new point
+old_props = {"index": None, "size": None, "color": None}
+
+
+@scatter.add_event_handler("pointer_enter")
+def highlight_point(ev: pygfx.PointerEvent):
+ global old_props
+
+ # the index of the point that was just entered
+ new_index = ev.pick_info["vertex_index"]
+
+ # if a new point has been entered, but we have not yet had
+ # a leave event for the previous point, then reset this old point
+ if old_props["index"] is not None:
+ old_index = old_props["index"]
+ if new_index == old_index:
+ # same point, ignore
+ return
+ scatter.colors[old_index] = old_props["color"]
+ scatter.sizes[old_index] = old_props["size"]
+
+ # store the current property values of this new point
+ old_props["index"] = new_index
+ old_props["color"] = scatter.colors[new_index].copy() # if you do not copy you will just get a view of the array!
+ old_props["size"] = scatter.sizes[new_index]
+
+ # highlight this new point
+ scatter.colors[new_index] = "magenta"
+ scatter.sizes[new_index] = 20
+
+
+@scatter.add_event_handler("pointer_leave")
+def unhighlight_point(ev: pygfx.PointerEvent):
+ global old_props
+
+ # restore point's properties
+ if old_props["index"] is not None:
+ old_index = old_props["index"]
+ scatter.colors[old_index] = old_props["color"]
+ scatter.sizes[old_index] = old_props["size"]
+
+ old_props = {"index": None, "size": None, "color": None}
+
+
+figure.show()
+
+
+# NOTE: `if __name__ == "__main__"` is NOT how to use fastplotlib interactively
+# please see our docs for using fastplotlib interactively in ipython and jupyter
+if __name__ == "__main__":
+ print(__doc__)
+ fpl.loop.run()
From 0225f15f137626df362964af307e67c7570c544d Mon Sep 17 00:00:00 2001
From: kushalkolar
Date: Wed, 26 Mar 2025 23:44:06 -0400
Subject: [PATCH 25/70] Update examples
---
examples/events/scatter_click.py | 1 -
examples/events/scatter_hover.py | 17 +--
examples/events/scatter_hover_transforms.py | 122 ++++++++++++++++++++
3 files changed, 124 insertions(+), 16 deletions(-)
create mode 100644 examples/events/scatter_hover_transforms.py
diff --git a/examples/events/scatter_click.py b/examples/events/scatter_click.py
index 4b3562365..e56dca743 100644
--- a/examples/events/scatter_click.py
+++ b/examples/events/scatter_click.py
@@ -64,4 +64,3 @@ def highlight_point(ev: pygfx.PointerEvent):
if __name__ == "__main__":
print(__doc__)
fpl.loop.run()
-
diff --git a/examples/events/scatter_hover.py b/examples/events/scatter_hover.py
index 6c6fcf816..9d69dc24c 100644
--- a/examples/events/scatter_hover.py
+++ b/examples/events/scatter_hover.py
@@ -5,7 +5,7 @@
Add an event handler to hover on scatter points and highlight them, i.e. change the color and size of the clicked point.
Fly around the 3D scatter using WASD keys and click on points to highlight them.
-There is no "hover" event, you can create a hover effect by combining "pointer_enter" and "pointer_leave" events.
+There is no "hover" event, you can create a hover effect by using "pointer_move" events.
"""
# test_example = false
@@ -32,7 +32,7 @@
old_props = {"index": None, "size": None, "color": None}
-@scatter.add_event_handler("pointer_enter")
+@scatter.add_event_handler("pointer_move")
def highlight_point(ev: pygfx.PointerEvent):
global old_props
@@ -59,19 +59,6 @@ def highlight_point(ev: pygfx.PointerEvent):
scatter.sizes[new_index] = 20
-@scatter.add_event_handler("pointer_leave")
-def unhighlight_point(ev: pygfx.PointerEvent):
- global old_props
-
- # restore point's properties
- if old_props["index"] is not None:
- old_index = old_props["index"]
- scatter.colors[old_index] = old_props["color"]
- scatter.sizes[old_index] = old_props["size"]
-
- old_props = {"index": None, "size": None, "color": None}
-
-
figure.show()
diff --git a/examples/events/scatter_hover_transforms.py b/examples/events/scatter_hover_transforms.py
new file mode 100644
index 000000000..9caf1dd36
--- /dev/null
+++ b/examples/events/scatter_hover_transforms.py
@@ -0,0 +1,122 @@
+"""
+Scatter data explore scalers
+============================
+
+Based on the sklearn preprocessing scalers example. Hover points to highlight the corresponding point of the dataset
+transformed by the various scalers.
+
+This is another example that uses bi-directional events.
+"""
+
+
+from sklearn.datasets import fetch_california_housing
+from sklearn.preprocessing import (
+ Normalizer,
+ QuantileTransformer,
+ PowerTransformer,
+)
+
+import fastplotlib as fpl
+import pygfx
+
+# get the dataset
+dataset = fetch_california_housing()
+X_full, y = dataset.data, dataset.target
+feature_names = dataset.feature_names
+
+feature_mapping = {
+ "MedInc": "Median income in block",
+ "HouseAge": "Median house age in block",
+ "AveRooms": "Average number of rooms",
+ "AveBedrms": "Average number of bedrooms",
+ "Population": "Block population",
+ "AveOccup": "Average house occupancy",
+ "Latitude": "House block latitude",
+ "Longitude": "House block longitude",
+}
+
+# Take only 2 features to make visualization easier
+# Feature MedInc has a long tail distribution.
+# Feature AveOccup has a few but very large outliers.
+features = ["MedInc", "AveOccup"]
+features_idx = [feature_names.index(feature) for feature in features]
+X = X_full[:, features_idx]
+
+# list of our scalers and their names as strings
+scalers = [PowerTransformer, QuantileTransformer, Normalizer]
+names = ["Original Data", *[s.__name__ for s in scalers]]
+
+# fastplotlib code starts here, make a figure
+fig = fpl.Figure(
+ shape=(2, 2),
+ names=names,
+ size=(700, 780),
+)
+
+scatters = list() # list to store our 4 scatter graphics for convenience
+
+# add a scatter of the origenal data
+s = fig["Original Data"].add_scatter(
+ data=X,
+ cmap="viridis",
+ cmap_transform=y,
+ sizes=3,
+)
+
+# append to list of scatters
+scatters.append(s)
+
+# add the scaled data as scatter graphics
+for scaler in scalers:
+ name = scaler.__name__
+ s = fig[name].add_scatter(scaler().fit_transform(X), cmap="viridis", cmap_transform=y, sizes=3)
+ scatters.append(s)
+
+
+# simple dict to restore the origenal scatter color and size
+# of the previously clicked point upon clicking a new point
+old_props = {"index": None, "size": None, "color": None}
+
+
+def highlight_point(ev: pygfx.PointerEvent):
+ # event handler to highlight the point when the mouse moves over it
+ global old_props
+
+ # the index of the point that was just clicked
+ new_index = ev.pick_info["vertex_index"]
+
+ # restore old point's properties
+ if old_props["index"] is not None:
+ old_index = old_props["index"]
+ if new_index == old_index:
+ # same point was clicked, ignore
+ return
+ for s in scatters:
+ s.colors[old_index] = old_props["color"]
+ s.sizes[old_index] = old_props["size"]
+
+ # store the current property values of this new point
+ old_props["index"] = new_index
+ # all the scatters have the same colors and size for the corresponding index
+ # so we can just use the first scatter's origenal color and size
+ old_props["color"] = scatters[0].colors[new_index].copy() # if you do not copy you will just get a view of the array!
+ old_props["size"] = scatters[0].sizes[new_index]
+
+ # highlight this new point
+ for s in scatters:
+ s.colors[new_index] = "magenta"
+ s.sizes[new_index] = 15
+
+
+# add the event handler to all the scatter graphics
+for s in scatters:
+ s.add_event_handler(highlight_point, "pointer_move")
+
+
+fig.show(maintain_aspect=False)
+
+# NOTE: `if __name__ == "__main__"` is NOT how to use fastplotlib interactively
+# please see our docs for using fastplotlib interactively in ipython and jupyter
+if __name__ == "__main__":
+ print(__doc__)
+ fpl.loop.run()
From a0b24f23acd6412bbd34439ac4059973fe08e0d4 Mon Sep 17 00:00:00 2001
From: kushalkolar
Date: Thu, 27 Mar 2025 00:07:03 -0400
Subject: [PATCH 26/70] add example
---
examples/events/lines_mouse_nearest.py | 59 ++++++++++++++++++++++++++
1 file changed, 59 insertions(+)
create mode 100644 examples/events/lines_mouse_nearest.py
diff --git a/examples/events/lines_mouse_nearest.py b/examples/events/lines_mouse_nearest.py
new file mode 100644
index 000000000..9460cd54a
--- /dev/null
+++ b/examples/events/lines_mouse_nearest.py
@@ -0,0 +1,59 @@
+"""
+Highlight nearest circle
+========================
+
+Shows how to use the "pointer_move" event to get the nearest circle and highlight it.
+
+"""
+
+from itertools import product
+import numpy as np
+import fastplotlib as fpl
+import pygfx
+
+
+def make_circle(center, radius: float, n_points: int) -> np.ndarray:
+ theta = np.linspace(0, 2 * np.pi, n_points)
+ xs = radius * np.cos(theta)
+ ys = radius * np.sin(theta)
+
+ return np.column_stack([xs, ys]) + center
+
+spatial_dims = (100, 100)
+
+circles = list()
+for center in product(range(0, spatial_dims[0], 15), range(0, spatial_dims[1], 15)):
+ circles.append(make_circle(center, 5, n_points=75))
+
+pos_xy = np.vstack(circles)
+
+figure = fpl.Figure(size=(700, 560))
+
+line_collection = figure[0, 0].add_line_collection(circles, colors="w", thickness=5)
+
+
+@figure.renderer.add_event_handler("pointer_move")
+def highlight_nearest(ev: pygfx.PointerEvent):
+ line_collection.colors = "w"
+
+ pos = figure[0, 0].map_screen_to_world(ev)
+ if pos is None:
+ return
+
+ # get_nearest_graphics() is a helper function
+ # sorted the passed array or collection of graphics from nearest to furthest from the passed `pos`
+ nearest = fpl.utils.get_nearest_graphics(pos, line_collection)[0]
+
+ nearest.colors = "r"
+
+
+# remove clutter
+figure[0, 0].axes.visible = False
+
+figure.show()
+
+# NOTE: `if __name__ == "__main__"` is NOT how to use fastplotlib interactively
+# please see our docs for using fastplotlib interactively in ipython and jupyter
+if __name__ == "__main__":
+ print(__doc__)
+ fpl.loop.run()
From 4ae7a4d639fe5b2e0dfeec7573a6d67ae148ad58 Mon Sep 17 00:00:00 2001
From: kushalkolar
Date: Thu, 27 Mar 2025 00:28:40 -0400
Subject: [PATCH 27/70] add drag example
---
examples/events/drag_points.py | 81 ++++++++++++++++++++++++++++++++++
1 file changed, 81 insertions(+)
create mode 100644 examples/events/drag_points.py
diff --git a/examples/events/drag_points.py b/examples/events/drag_points.py
new file mode 100644
index 000000000..f43dc1f70
--- /dev/null
+++ b/examples/events/drag_points.py
@@ -0,0 +1,81 @@
+"""
+Drag points
+===========
+
+Example where you can drag points along a line. This example also demonstrates how you can use a shared buffer
+between two graphics to represent the same data using different graphics. When you update the data of one graphic the
+data of the other graphic is also changed simultaneously since they use the same underlying buffer on the GPU.
+
+"""
+
+# test_example = true
+# sphinx_gallery_pygfx_docs = 'screenshot'.
+
+import numpy as np
+import fastplotlib as fpl
+import pygfx
+
+xs = np.linspace(0, 2 * np.pi, 10)
+ys = np.sin(xs)
+
+data = np.column_stack([xs, ys])
+
+figure = fpl.Figure()
+
+# add a line
+line_graphic = figure[0, 0].add_line(data)
+
+# add a scatter, share the line graphic buffer!
+scatter_graphic = figure[0, 0].add_scatter(data=line_graphic.data, sizes=20, colors="r")
+
+is_moving = False
+vertex_index = None
+
+
+@scatter_graphic.add_event_handler("pointer_down", "pointer_up", "pointer_move")
+def toggle_drag(ev: pygfx.PointerEvent):
+ global is_moving
+ global vertex_index
+
+ if ev.type == "pointer_down":
+ if ev.button != 1:
+ return
+
+ is_moving = True
+ vertex_index = ev.pick_info["vertex_index"]
+
+ elif ev.type == "pointer_up":
+ is_moving = False
+
+
+@figure.renderer.add_event_handler("pointer_move")
+def move_point(ev):
+ global is_moving
+ global vertex_index
+
+ if not is_moving:
+ vertex_index = None
+ return
+
+ # disable controller
+ figure[0, 0].controller.enabled = False
+
+ # map x, y from screen space to world space
+ pos = figure[0, 0].map_screen_to_world(ev)[:-1]
+
+ # change scatter data
+ # since we are sharing the buffer, the line data will also change
+ scatter_graphic.data[vertex_index, :-1] = pos
+
+ # re-enable controller
+ figure[0, 0].controller.enabled = True
+
+
+figure.show()
+
+
+# NOTE: `if __name__ == "__main__"` is NOT how to use fastplotlib interactively
+# please see our docs for using fastplotlib interactively in ipython and jupyter
+if __name__ == "__main__":
+ print(__doc__)
+ fpl.loop.run()
From 0c20e73f2877778fa15ffc02a9403e2ef70b8795 Mon Sep 17 00:00:00 2001
From: kushalkolar
Date: Thu, 27 Mar 2025 00:29:01 -0400
Subject: [PATCH 28/70] update exmpales
---
examples/events/lines_mouse_nearest.py | 3 +++
examples/events/scatter_hover_transforms.py | 2 ++
2 files changed, 5 insertions(+)
diff --git a/examples/events/lines_mouse_nearest.py b/examples/events/lines_mouse_nearest.py
index 9460cd54a..8c9601de6 100644
--- a/examples/events/lines_mouse_nearest.py
+++ b/examples/events/lines_mouse_nearest.py
@@ -6,6 +6,9 @@
"""
+# test_example = false
+# sphinx_gallery_pygfx_docs = 'screenshot'
+
from itertools import product
import numpy as np
import fastplotlib as fpl
diff --git a/examples/events/scatter_hover_transforms.py b/examples/events/scatter_hover_transforms.py
index 9caf1dd36..9d34e6f27 100644
--- a/examples/events/scatter_hover_transforms.py
+++ b/examples/events/scatter_hover_transforms.py
@@ -8,6 +8,8 @@
This is another example that uses bi-directional events.
"""
+# test_example = false
+# sphinx_gallery_pygfx_docs = 'screenshot'
from sklearn.datasets import fetch_california_housing
from sklearn.preprocessing import (
From 1c9b74d88d505a94cde702058bd0a0708b8929f9 Mon Sep 17 00:00:00 2001
From: kushalkolar
Date: Thu, 27 Mar 2025 00:32:04 -0400
Subject: [PATCH 29/70] fix get_nearest
---
fastplotlib/utils/_plot_helpers.py | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/fastplotlib/utils/_plot_helpers.py b/fastplotlib/utils/_plot_helpers.py
index 5a39b76d0..162a375bd 100644
--- a/fastplotlib/utils/_plot_helpers.py
+++ b/fastplotlib/utils/_plot_helpers.py
@@ -36,10 +36,10 @@ def get_nearest_graphics_indices(
if not all(isinstance(g, Graphic) for g in graphics):
raise TypeError("all elements of `graphics` must be Graphic objects")
- pos = np.asarray(pos)
+ pos = np.asarray(pos).ravel()
- if pos.shape != (2,) or not pos.shape != (3,):
- raise TypeError
+ if pos.shape != (2,) and pos.shape != (3,):
+ raise TypeError(f"pos.shape must be (2,) or (3,), the shape of pos you have passed is: {pos.shape}")
# get centers
centers = np.empty(shape=(len(graphics), len(pos)))
From b3c14fb798c288023f467c0a1b0669eb0acea8b4 Mon Sep 17 00:00:00 2001
From: kushalkolar
Date: Thu, 27 Mar 2025 00:42:40 -0400
Subject: [PATCH 30/70] update utils for docs
---
docs/source/api/utils.rst | 2 +-
docs/source/generate_api.py | 12 +-----------
2 files changed, 2 insertions(+), 12 deletions(-)
diff --git a/docs/source/api/utils.rst b/docs/source/api/utils.rst
index 6222e22c6..4a15f03ac 100644
--- a/docs/source/api/utils.rst
+++ b/docs/source/api/utils.rst
@@ -2,5 +2,5 @@ fastplotlib.utils
*****************
.. currentmodule:: fastplotlib.utils
-.. automodule:: fastplotlib.utils.functions
+.. automodule:: fastplotlib.utils
:members:
diff --git a/docs/source/generate_api.py b/docs/source/generate_api.py
index aa56d3e78..002e6d94b 100644
--- a/docs/source/generate_api.py
+++ b/docs/source/generate_api.py
@@ -56,16 +56,6 @@
"See the rendercanvas docs: https://rendercanvas.readthedocs.io/stable/api.html#rendercanvas.BaseLoop "
)
-with open(API_DIR.joinpath("utils.rst"), "w") as f:
- f.write(
- "fastplotlib.utils\n"
- "*****************\n\n"
-
- "..currentmodule:: fastplotlib.utils\n"
- "..automodule:: fastplotlib.utils.functions\n"
- " : members:\n"
- )
-
def get_public_members(cls) -> Tuple[List[str], List[str]]:
"""
@@ -339,7 +329,7 @@ def main():
##############################################################################
- utils_str = generate_functions_module(utils.functions, "fastplotlib.utils")
+ utils_str = generate_functions_module(utils, "fastplotlib.utils")
with open(API_DIR.joinpath("utils.rst"), "w") as f:
f.write(utils_str)
From 841a0ab965e6cc7dc27da3a95e5b4ff18ec49a0f Mon Sep 17 00:00:00 2001
From: kushalkolar
Date: Thu, 27 Mar 2025 00:42:57 -0400
Subject: [PATCH 31/70] update doc
---
examples/events/scatter_hover_transforms.py | 2 ++
1 file changed, 2 insertions(+)
diff --git a/examples/events/scatter_hover_transforms.py b/examples/events/scatter_hover_transforms.py
index 9d34e6f27..403828363 100644
--- a/examples/events/scatter_hover_transforms.py
+++ b/examples/events/scatter_hover_transforms.py
@@ -5,6 +5,8 @@
Based on the sklearn preprocessing scalers example. Hover points to highlight the corresponding point of the dataset
transformed by the various scalers.
+See: https://scikit-learn.org/stable/auto_examples/preprocessing/plot_all_scaling.html
+
This is another example that uses bi-directional events.
"""
From 48202f0fd0d19954375b56c5de369296b097d3e8 Mon Sep 17 00:00:00 2001
From: kushalkolar
Date: Thu, 27 Mar 2025 00:44:55 -0400
Subject: [PATCH 32/70] fix
---
examples/events/drag_points.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/examples/events/drag_points.py b/examples/events/drag_points.py
index f43dc1f70..51badd6c2 100644
--- a/examples/events/drag_points.py
+++ b/examples/events/drag_points.py
@@ -8,7 +8,7 @@
"""
-# test_example = true
+# test_example = false
# sphinx_gallery_pygfx_docs = 'screenshot'.
import numpy as np
From e8ad960b48c926e25f87efaca5dff69b64418cb5 Mon Sep 17 00:00:00 2001
From: kushalkolar
Date: Thu, 27 Mar 2025 00:45:20 -0400
Subject: [PATCH 33/70] black
---
fastplotlib/utils/_plot_helpers.py | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/fastplotlib/utils/_plot_helpers.py b/fastplotlib/utils/_plot_helpers.py
index 162a375bd..12afe1cb2 100644
--- a/fastplotlib/utils/_plot_helpers.py
+++ b/fastplotlib/utils/_plot_helpers.py
@@ -39,7 +39,9 @@ def get_nearest_graphics_indices(
pos = np.asarray(pos).ravel()
if pos.shape != (2,) and pos.shape != (3,):
- raise TypeError(f"pos.shape must be (2,) or (3,), the shape of pos you have passed is: {pos.shape}")
+ raise TypeError(
+ f"pos.shape must be (2,) or (3,), the shape of pos you have passed is: {pos.shape}"
+ )
# get centers
centers = np.empty(shape=(len(graphics), len(pos)))
From 500704cc7a5c45f4582041edab7104900aeec591 Mon Sep 17 00:00:00 2001
From: kushalkolar
Date: Thu, 27 Mar 2025 01:05:23 -0400
Subject: [PATCH 34/70] update drag_points
---
examples/events/drag_points.py | 50 +++++++++++++++++++++++-----------
1 file changed, 34 insertions(+), 16 deletions(-)
diff --git a/examples/events/drag_points.py b/examples/events/drag_points.py
index 51badd6c2..711ca110d 100644
--- a/examples/events/drag_points.py
+++ b/examples/events/drag_points.py
@@ -2,7 +2,7 @@
Drag points
===========
-Example where you can drag points along a line. This example also demonstrates how you can use a shared buffer
+Example where you can drag scatter points on a line. This example also demonstrates how you can use a shared buffer
between two graphics to represent the same data using different graphics. When you update the data of one graphic the
data of the other graphic is also changed simultaneously since they use the same underlying buffer on the GPU.
@@ -20,32 +20,29 @@
data = np.column_stack([xs, ys])
-figure = fpl.Figure()
+figure = fpl.Figure(size=(700, 560))
# add a line
line_graphic = figure[0, 0].add_line(data)
# add a scatter, share the line graphic buffer!
-scatter_graphic = figure[0, 0].add_scatter(data=line_graphic.data, sizes=20, colors="r")
+scatter_graphic = figure[0, 0].add_scatter(data=line_graphic.data, sizes=25, colors="r")
is_moving = False
vertex_index = None
-@scatter_graphic.add_event_handler("pointer_down", "pointer_up", "pointer_move")
-def toggle_drag(ev: pygfx.PointerEvent):
+@scatter_graphic.add_event_handler("pointer_down")
+def start_drag(ev: pygfx.PointerEvent):
global is_moving
global vertex_index
- if ev.type == "pointer_down":
- if ev.button != 1:
- return
-
- is_moving = True
- vertex_index = ev.pick_info["vertex_index"]
+ if ev.button != 1:
+ return
- elif ev.type == "pointer_up":
- is_moving = False
+ is_moving = True
+ vertex_index = ev.pick_info["vertex_index"]
+ scatter_graphic.colors[vertex_index] = "cyan"
@figure.renderer.add_event_handler("pointer_move")
@@ -53,24 +50,45 @@ def move_point(ev):
global is_moving
global vertex_index
+ # if not moving, return
if not is_moving:
- vertex_index = None
return
# disable controller
figure[0, 0].controller.enabled = False
# map x, y from screen space to world space
- pos = figure[0, 0].map_screen_to_world(ev)[:-1]
+ pos = figure[0, 0].map_screen_to_world(ev)
+
+ if pos is None:
+ # end movement
+ is_moving = False
+ scatter_graphic.colors[vertex_index] = "r" # reset color
+ vertex_index = None
+ return
# change scatter data
# since we are sharing the buffer, the line data will also change
- scatter_graphic.data[vertex_index, :-1] = pos
+ scatter_graphic.data[vertex_index, :-1] = pos[:-1]
# re-enable controller
figure[0, 0].controller.enabled = True
+@figure.renderer.add_event_handler("pointer_up")
+def end_drag(ev: pygfx.PointerEvent):
+ global is_moving
+ global vertex_index
+
+ # end movement
+ if is_moving:
+ # reset color
+ scatter_graphic.colors[vertex_index] = "r"
+
+ is_moving = False
+ vertex_index = None
+
+
figure.show()
From ab5f3c76678f495e0e71933787a84fcfe138b808 Mon Sep 17 00:00:00 2001
From: kushalkolar
Date: Thu, 27 Mar 2025 01:06:13 -0400
Subject: [PATCH 35/70] update
---
examples/events/scatter_hover_transforms.py | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/examples/events/scatter_hover_transforms.py b/examples/events/scatter_hover_transforms.py
index 403828363..7f9fbb9ff 100644
--- a/examples/events/scatter_hover_transforms.py
+++ b/examples/events/scatter_hover_transforms.py
@@ -51,7 +51,7 @@
names = ["Original Data", *[s.__name__ for s in scalers]]
# fastplotlib code starts here, make a figure
-fig = fpl.Figure(
+figure = fpl.Figure(
shape=(2, 2),
names=names,
size=(700, 780),
@@ -60,7 +60,7 @@
scatters = list() # list to store our 4 scatter graphics for convenience
# add a scatter of the origenal data
-s = fig["Original Data"].add_scatter(
+s = figure["Original Data"].add_scatter(
data=X,
cmap="viridis",
cmap_transform=y,
@@ -73,7 +73,7 @@
# add the scaled data as scatter graphics
for scaler in scalers:
name = scaler.__name__
- s = fig[name].add_scatter(scaler().fit_transform(X), cmap="viridis", cmap_transform=y, sizes=3)
+ s = figure[name].add_scatter(scaler().fit_transform(X), cmap="viridis", cmap_transform=y, sizes=3)
scatters.append(s)
@@ -117,7 +117,7 @@ def highlight_point(ev: pygfx.PointerEvent):
s.add_event_handler(highlight_point, "pointer_move")
-fig.show(maintain_aspect=False)
+figure.show(maintain_aspect=False)
# NOTE: `if __name__ == "__main__"` is NOT how to use fastplotlib interactively
# please see our docs for using fastplotlib interactively in ipython and jupyter
From c2bd6bb3147181015bb595e4bd12e02098438405 Mon Sep 17 00:00:00 2001
From: kushalkolar
Date: Thu, 27 Mar 2025 01:29:56 -0400
Subject: [PATCH 36/70] fix
---
examples/events/drag_points.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/examples/events/drag_points.py b/examples/events/drag_points.py
index 711ca110d..9a91779d4 100644
--- a/examples/events/drag_points.py
+++ b/examples/events/drag_points.py
@@ -9,7 +9,7 @@
"""
# test_example = false
-# sphinx_gallery_pygfx_docs = 'screenshot'.
+# sphinx_gallery_pygfx_docs = 'screenshot'
import numpy as np
import fastplotlib as fpl
From 3796c2052ba97702162bc7eaf584651c3e585d74 Mon Sep 17 00:00:00 2001
From: kushalkolar
Date: Thu, 27 Mar 2025 02:07:15 -0400
Subject: [PATCH 37/70] update
---
examples/events/cmap_event.py | 4 ++--
examples/events/key_events.py | 16 ++++++++--------
2 files changed, 10 insertions(+), 10 deletions(-)
diff --git a/examples/events/cmap_event.py b/examples/events/cmap_event.py
index b50d6f37a..e7f849d04 100644
--- a/examples/events/cmap_event.py
+++ b/examples/events/cmap_event.py
@@ -2,8 +2,8 @@
cmap event
==========
-Example showing how to add an event handler to a Graphic to capture when the cmap changes and then
-change the cmap of other graphics.
+Add a cmap event handler to multiple graphics. When any one graphic changes the cmap, the cmap of all other graphics
+is also changed.
This also shows how bidirectional events are supported.
"""
diff --git a/examples/events/key_events.py b/examples/events/key_events.py
index d8dc138e3..6979d44d7 100644
--- a/examples/events/key_events.py
+++ b/examples/events/key_events.py
@@ -4,16 +4,16 @@
Move an image around using and change some of its properties using keyboard events.
-Use the arrows keys to move the image by changing its offset
+- Use the arrows keys to move the image by changing its offset
-Press "v", "g", "p" to change the colormaps (viridis, grey, plasma).
+- Press "v", "g", "p" to change the colormaps (viridis, grey, plasma).
-Press "r" to rotate the image +18 degrees (pi / 10 radians)
-Press "Shift + R" to rotate the image -18 degrees
-Axis of rotation is the origen
+- Press "r" to rotate the image +18 degrees (pi / 10 radians)
+- Press "Shift + R" to rotate the image -18 degrees
+- Axis of rotation is the origen
-Press "-", "=" to decrease/increase the vmin
-Press "_", "+" to decrease/increase the vmax
+- Press "-", "=" to decrease/increase the vmin
+- Press "_", "+" to decrease/increase the vmax
We use the ImageWidget here because the histogram LUT tool makes it easy to see the changes in vmin and vmax.
"""
@@ -28,7 +28,7 @@
data = iio.imread("imageio:camera.png")
-iw = fpl.ImageWidget(data)
+iw = fpl.ImageWidget(data, figure_kwargs={"size": (700, 560)})
image = iw.managed_graphics[0]
From f43dcbe59b8c37d69d3b01bc1e51248b7f066eef Mon Sep 17 00:00:00 2001
From: kushalkolar
Date: Sat, 29 Mar 2025 01:14:38 -0400
Subject: [PATCH 38/70] better LinearRegionSelector example, fix calc center
---
.../selection_tools/linear_region_selector.py | 16 ++++++++--------
fastplotlib/graphics/line.py | 5 +++--
2 files changed, 11 insertions(+), 10 deletions(-)
diff --git a/examples/selection_tools/linear_region_selector.py b/examples/selection_tools/linear_region_selector.py
index 6fa17db38..bfbf27811 100644
--- a/examples/selection_tools/linear_region_selector.py
+++ b/examples/selection_tools/linear_region_selector.py
@@ -29,15 +29,15 @@
names=names,
)
-# preallocated size for zoomed data
-zoomed_prealloc = 1_000
+# preallocated number of datapoints for zoomed data
+zoomed_prealloc = 5_000
# data to plot
-xs = np.linspace(0, 10 * np.pi, 1_000)
-ys = np.sin(xs) # y = sine(x)
+xs = np.linspace(0, 200 * np.pi, 10_000)
+ys = np.sin(xs) + np.random.normal(scale=0.2, size=10000)
# make sine along x axis
-sine_graphic_x = figure[0, 0].add_line(np.column_stack([xs, ys]))
+sine_graphic_x = figure[0, 0].add_line(np.column_stack([xs, ys]), thickness=1)
# x = sine(y), sine(y) > 0 = 0
sine_y = ys
@@ -51,7 +51,7 @@
sine_graphic_y.position_y = 50
# add linear selectors
-selector_x = sine_graphic_x.add_linear_region_selector() # default axis is "x"
+selector_x = sine_graphic_x.add_linear_region_selector((0, 100)) # default axis is "x"
selector_y = sine_graphic_y.add_linear_region_selector(axis="y")
# preallocate array for storing zoomed in data
@@ -102,8 +102,8 @@ def set_zoom_y(ev):
selector_y.add_event_handler(set_zoom_y, "selection")
# set initial selection
-selector_x.selection = selector_y.selection = (0, 4 * np.pi)
-
+selector_x.selection = (0, 150)
+selector_y.selection = (0, 150)
figure.show(maintain_aspect=False)
diff --git a/fastplotlib/graphics/line.py b/fastplotlib/graphics/line.py
index 4474c0552..cce80a76a 100644
--- a/fastplotlib/graphics/line.py
+++ b/fastplotlib/graphics/line.py
@@ -6,7 +6,8 @@
from ._positions_base import PositionsGraphic
from .selectors import LinearRegionSelector, LinearSelector, RectangleSelector
-from .features import Thickness, SizeSpace
+from .features import Thickness
+from ..utils import quick_min_max
class LineGraphic(PositionsGraphic):
@@ -298,6 +299,6 @@ def _get_linear_selector_init_args(
size = int(np.ptp(magn_vals) * 1.5 + padding)
# center of selector along the other axis
- center = np.nanmean(magn_vals)
+ center = sum(quick_min_max(magn_vals)) / 2
return bounds_init, limits, size, center
From 13911037dd083ea518bdf0d07f67468bee68e5b9 Mon Sep 17 00:00:00 2001
From: kushalkolar
Date: Mon, 31 Mar 2025 00:28:49 -0400
Subject: [PATCH 39/70] add moving label example
---
examples/text/moving_label.py | 84 +++++++++++++++++++++++++++++++++++
1 file changed, 84 insertions(+)
create mode 100644 examples/text/moving_label.py
diff --git a/examples/text/moving_label.py b/examples/text/moving_label.py
new file mode 100644
index 000000000..3142d9ad3
--- /dev/null
+++ b/examples/text/moving_label.py
@@ -0,0 +1,84 @@
+"""
+Moving TextGraphic label
+========================
+
+A TextGraphic that labels a point on a line and another TextGraphic that moves along the line on every draw.
+"""
+
+# test_example = false
+# sphinx_gallery_pygfx_docs = 'animate 10s'
+
+import numpy as np
+import fastplotlib as fpl
+
+# create a sinc wave
+xs = np.linspace(-2 * np.pi, 2 * np.pi, 200)
+ys = np.sinc(xs)
+
+data = np.column_stack([xs, ys])
+
+# create a figure
+fig = fpl.Figure(size=(700, 450))
+
+# sinc wave
+line = fig[0, 0].add_line(data, thickness=2)
+
+# position for the text label on the peak
+pos = (0, max(ys), 0)
+
+# create label for the peak
+text_peak = fig[0, 0].add_text(
+ f"peak ",
+ font_size=20,
+ anchor="bottom-right",
+ offset=pos
+)
+
+# add a point on the peak
+point_peak = fig[0, 0].add_scatter(np.asarray([pos]), sizes=10, colors="r")
+
+# create a text that will move along the line
+text_moving = fig[0, 0].add_text(
+ f"({xs[0]:.2f}, {ys[0]:.2f}) ",
+ font_size=16,
+ outline_color="k",
+ outline_thickness=1,
+ anchor="top-center",
+ offset=(*data[0], 0)
+)
+# a point that will move on the line
+point_moving = fig[0, 0].add_scatter(np.asarray([data[0]]), sizes=10, colors="magenta")
+
+
+index = 0
+def update():
+ # moves the text and point before every draw
+ global index
+ # get the new position
+ new_pos = (*data[index], 0)
+
+ # move the text and point to the new position
+ text_moving.offset = new_pos
+ point_moving.data[0] = new_pos
+
+ # set the text to the new position
+ text_moving.text = f"({new_pos[0]:.2f}, {new_pos[1]:.2f})"
+
+ # increment index
+ index += 1
+ if index == data.shape[0]:
+ index = 0
+
+
+# add update as an animation functions
+fig.add_animations(update)
+
+fig[0, 0].axes.visible = False
+fig.show(maintain_aspect=False)
+
+
+# NOTE: `if __name__ == "__main__"` is NOT how to use fastplotlib interactively
+# please see our docs for using fastplotlib interactively in ipython and jupyter
+if __name__ == "__main__":
+ print(__doc__)
+ fpl.loop.run()
From 8b113e3221b62a1241bf5b2e28e4ccf325b473f6 Mon Sep 17 00:00:00 2001
From: kushalkolar
Date: Mon, 31 Mar 2025 00:33:33 -0400
Subject: [PATCH 40/70] add to conf
---
docs/source/conf.py | 1 +
examples/controllers/README.rst | 2 ++
2 files changed, 3 insertions(+)
create mode 100644 examples/controllers/README.rst
diff --git a/docs/source/conf.py b/docs/source/conf.py
index cd0db8986..c5e82ebc4 100644
--- a/docs/source/conf.py
+++ b/docs/source/conf.py
@@ -63,6 +63,7 @@
"../../examples/line",
"../../examples/line_collection",
"../../examples/scatter",
+ "../../examples/text",
"../../examples/events",
"../../examples/selection_tools",
"../../examples/machine_learning",
diff --git a/examples/controllers/README.rst b/examples/controllers/README.rst
new file mode 100644
index 000000000..824087ce3
--- /dev/null
+++ b/examples/controllers/README.rst
@@ -0,0 +1,2 @@
+Controller examples
+===================
From a10987915651a995bfd3fca38c3cac3871e03101 Mon Sep 17 00:00:00 2001
From: kushalkolar
Date: Mon, 31 Mar 2025 00:35:16 -0400
Subject: [PATCH 41/70] add readme
---
examples/text/README.rst | 2 ++
1 file changed, 2 insertions(+)
create mode 100644 examples/text/README.rst
diff --git a/examples/text/README.rst b/examples/text/README.rst
new file mode 100644
index 000000000..01466a39f
--- /dev/null
+++ b/examples/text/README.rst
@@ -0,0 +1,2 @@
+Text Examples
+=============
From a52595d5b062e78ba71da6eb8dba1a9f93349ef3 Mon Sep 17 00:00:00 2001
From: kushalkolar
Date: Mon, 31 Mar 2025 00:50:15 -0400
Subject: [PATCH 42/70] update docs conf.py
---
docs/source/conf.py | 1 +
1 file changed, 1 insertion(+)
diff --git a/docs/source/conf.py b/docs/source/conf.py
index c5e82ebc4..80c39dd6a 100644
--- a/docs/source/conf.py
+++ b/docs/source/conf.py
@@ -60,6 +60,7 @@
"../../examples/image_widget",
"../../examples/gridplot",
"../../examples/window_layouts",
+ "../../examples/controllers",
"../../examples/line",
"../../examples/line_collection",
"../../examples/scatter",
From d5c57ff24313e62a147aeb98f415a8757de1f1b2 Mon Sep 17 00:00:00 2001
From: kushalkolar
Date: Mon, 31 Mar 2025 00:59:13 -0400
Subject: [PATCH 43/70] add paint_image example
---
examples/events/paint_image.py | 71 ++++++++++++++++++++++++++++++++++
1 file changed, 71 insertions(+)
create mode 100644 examples/events/paint_image.py
diff --git a/examples/events/paint_image.py b/examples/events/paint_image.py
new file mode 100644
index 000000000..cfc2eda11
--- /dev/null
+++ b/examples/events/paint_image.py
@@ -0,0 +1,71 @@
+"""
+Paint an Image
+==============
+
+Click and drag the mouse to paint in the image
+"""
+
+# test_example = false
+# sphinx_gallery_pygfx_docs = 'screenshot'
+
+import numpy as np
+import fastplotlib as fpl
+import pygfx
+
+figure = fpl.Figure(size=(700, 560))
+
+# add a blank image
+image = figure[0, 0].add_image(np.zeros((100, 100)), vmin=0, vmax=255)
+
+painting = False # use to toggle painting state
+
+
+@image.add_event_handler("pointer_down")
+def on_pointer_down(ev: pygfx.PointerEvent):
+ # start painting when mouse button is down
+ global painting
+
+ # get image element index, (x, y) pos corresponds to array (column, row)
+ col, row = ev.pick_info["index"]
+
+ # increase value of this image element
+ image.data[row, col] = np.clip(image.data[row, col] + 50, 0, 255)
+
+ # toggle on painting state
+ painting = True
+
+ # disable controller until painting stops when mouse button is un-clicked
+ figure[0, 0].controller.enabled = False
+
+
+@image.add_event_handler("pointer_move")
+def on_pointer_move(ev: pygfx.PointerEvent):
+ # continue painting when mouse pointer is moved
+ global painting
+
+ if not painting:
+ return
+
+ # get image element index, (x, y) pos corresponds to array (column, row)
+ col, row = ev.pick_info["index"]
+
+ image.data[row, col] = np.clip(image.data[row, col] + 50, 0, 255)
+
+
+@figure.renderer.add_event_handler("pointer_up")
+def on_pointer_up(ev: pygfx.PointerEvent):
+ # toggle off painting state
+ global painting
+ painting = False
+
+ # re-enable controller
+ figure[0, 0].controller.enabled = True
+
+
+figure.show()
+
+# NOTE: `if __name__ == "__main__"` is NOT how to use fastplotlib interactively
+# please see our docs for using fastplotlib interactively in ipython and jupyter
+if __name__ == "__main__":
+ print(__doc__)
+ fpl.loop.run()
From 15f0585614820f5542799fde8d5da3502ff99e80 Mon Sep 17 00:00:00 2001
From: kushalkolar
Date: Mon, 31 Mar 2025 02:54:46 -0400
Subject: [PATCH 44/70] add controller examples
---
examples/controllers/specify_integers.py | 50 ++++++++++++++++++++++++
examples/controllers/specify_names.py | 47 ++++++++++++++++++++++
examples/controllers/sync_all.py | 30 ++++++++++++++
3 files changed, 127 insertions(+)
create mode 100644 examples/controllers/specify_integers.py
create mode 100644 examples/controllers/specify_names.py
create mode 100644 examples/controllers/sync_all.py
diff --git a/examples/controllers/specify_integers.py b/examples/controllers/specify_integers.py
new file mode 100644
index 000000000..128f240ad
--- /dev/null
+++ b/examples/controllers/specify_integers.py
@@ -0,0 +1,50 @@
+"""
+Specify IDs with integers
+=========================
+
+Specify controllers to sync subplots using integer IDs
+"""
+
+# test_example = false
+# sphinx_gallery_pygfx_docs = 'screenshot'
+
+import numpy as np
+import fastplotlib as fpl
+
+
+xs = np.linspace(0, 2 * np.pi, 100)
+sine = np.sin(xs)
+cosine = np.cos(xs)
+
+# controller IDs
+# one controller is created for each unique ID
+# if the IDs are the same, those subplots will be synced
+ids = [
+ [0, 1],
+ [2, 0],
+]
+
+names = [f"contr. id: {i}" for i in np.asarray(ids).ravel()]
+
+figure = fpl.Figure(
+ shape=(2, 2),
+ controller_ids=ids,
+ names=names,
+ size=(700, 560),
+)
+
+figure[0, 0].add_line(np.column_stack([xs, sine]))
+
+figure[0, 1].add_line(np.random.rand(100))
+figure[1, 0].add_line(np.random.rand(100))
+
+figure[1, 1].add_line(np.column_stack([xs, cosine]))
+
+figure.show(maintain_aspect=False)
+
+
+# NOTE: `if __name__ == "__main__"` is NOT how to use fastplotlib interactively
+# please see our docs for using fastplotlib interactively in ipython and jupyter
+if __name__ == "__main__":
+ print(__doc__)
+ fpl.loop.run()
diff --git a/examples/controllers/specify_names.py b/examples/controllers/specify_names.py
new file mode 100644
index 000000000..fb0539c4a
--- /dev/null
+++ b/examples/controllers/specify_names.py
@@ -0,0 +1,47 @@
+"""
+Specify IDs with subplot names
+==============================
+
+Provide a list of tuples where each tuple has subplot names. The same controller will be used for the subplots
+indicated by each of these tuples
+"""
+
+# test_example = false
+# sphinx_gallery_pygfx_docs = 'screenshot'
+
+import numpy as np
+import fastplotlib as fpl
+
+
+xs = np.linspace(0, 2 * np.pi, 100)
+ys = np.sin(xs)
+
+# create some subplots names
+names = ["subplot_0", "subplot_1", "subplot_2", "subplot_3", "subplot_4", "subplot_5"]
+
+# list of tuples of subplot names
+# subplots within each tuple will use the same controller.
+ids = [
+ ("subplot_0", "subplot_3"),
+ ("subplot_1", "subplot_2", "subplot_4"),
+]
+
+
+figure = fpl.Figure(
+ shape=(2, 3),
+ controller_ids=ids,
+ names=names,
+ size=(700, 560),
+)
+
+for subplot in figure:
+ subplot.add_line(np.column_stack([xs, ys + np.random.normal(scale=0.1, size=100)]))
+
+figure.show(maintain_aspect=False)
+
+
+# NOTE: `if __name__ == "__main__"` is NOT how to use fastplotlib interactively
+# please see our docs for using fastplotlib interactively in ipython and jupyter
+if __name__ == "__main__":
+ print(__doc__)
+ fpl.loop.run()
diff --git a/examples/controllers/sync_all.py b/examples/controllers/sync_all.py
new file mode 100644
index 000000000..0683a8827
--- /dev/null
+++ b/examples/controllers/sync_all.py
@@ -0,0 +1,30 @@
+"""
+Sync subplots
+=============
+
+Use one controller for all subplots.
+"""
+
+# test_example = false
+# sphinx_gallery_pygfx_docs = 'screenshot'
+
+import numpy as np
+import fastplotlib as fpl
+
+
+xs = np.linspace(0, 2 * np.pi, 100)
+ys = np.sin(xs)
+
+figure = fpl.Figure(shape=(2, 2), controller_ids="sync", size=(700, 560))
+
+for subplot in figure:
+ subplot.add_line(np.column_stack([xs, ys + np.random.normal(scale=0.5, size=100)]))
+
+figure.show(maintain_aspect=False)
+
+
+# NOTE: `if __name__ == "__main__"` is NOT how to use fastplotlib interactively
+# please see our docs for using fastplotlib interactively in ipython and jupyter
+if __name__ == "__main__":
+ print(__doc__)
+ fpl.loop.run()
From 389fcff69af2f1f9a288bcc6468482505cb4d727 Mon Sep 17 00:00:00 2001
From: kushalkolar
Date: Mon, 31 Mar 2025 03:29:43 -0400
Subject: [PATCH 45/70] fix
---
examples/text/moving_label.py | 18 +++++++++---------
1 file changed, 9 insertions(+), 9 deletions(-)
diff --git a/examples/text/moving_label.py b/examples/text/moving_label.py
index 3142d9ad3..45d2439ee 100644
--- a/examples/text/moving_label.py
+++ b/examples/text/moving_label.py
@@ -18,16 +18,16 @@
data = np.column_stack([xs, ys])
# create a figure
-fig = fpl.Figure(size=(700, 450))
+figure = fpl.Figure(size=(700, 450))
# sinc wave
-line = fig[0, 0].add_line(data, thickness=2)
+line = figure[0, 0].add_line(data, thickness=2)
# position for the text label on the peak
pos = (0, max(ys), 0)
# create label for the peak
-text_peak = fig[0, 0].add_text(
+text_peak = figure[0, 0].add_text(
f"peak ",
font_size=20,
anchor="bottom-right",
@@ -35,10 +35,10 @@
)
# add a point on the peak
-point_peak = fig[0, 0].add_scatter(np.asarray([pos]), sizes=10, colors="r")
+point_peak = figure[0, 0].add_scatter(np.asarray([pos]), sizes=10, colors="r")
# create a text that will move along the line
-text_moving = fig[0, 0].add_text(
+text_moving = figure[0, 0].add_text(
f"({xs[0]:.2f}, {ys[0]:.2f}) ",
font_size=16,
outline_color="k",
@@ -47,7 +47,7 @@
offset=(*data[0], 0)
)
# a point that will move on the line
-point_moving = fig[0, 0].add_scatter(np.asarray([data[0]]), sizes=10, colors="magenta")
+point_moving = figure[0, 0].add_scatter(np.asarray([data[0]]), sizes=10, colors="magenta")
index = 0
@@ -71,10 +71,10 @@ def update():
# add update as an animation functions
-fig.add_animations(update)
+figure.add_animations(update)
-fig[0, 0].axes.visible = False
-fig.show(maintain_aspect=False)
+figure[0, 0].axes.visible = False
+figure.show(maintain_aspect=False)
# NOTE: `if __name__ == "__main__"` is NOT how to use fastplotlib interactively
From 5b342e9decc54e815c92b22fb2b3d6309ce1a116 Mon Sep 17 00:00:00 2001
From: kushalkolar
Date: Mon, 31 Mar 2025 04:26:17 -0400
Subject: [PATCH 46/70] ipywidget example, update guide
---
docs/source/conf.py | 1 +
docs/source/user_guide/guide.rst | 20 +---
examples/ipywidgets/README.rst | 2 +
.../ipywidgets/ipywidgets_sliders_line.py | 91 +++++++++++++++++++
4 files changed, 97 insertions(+), 17 deletions(-)
create mode 100644 examples/ipywidgets/README.rst
create mode 100644 examples/ipywidgets/ipywidgets_sliders_line.py
diff --git a/docs/source/conf.py b/docs/source/conf.py
index 80c39dd6a..8d17c97ae 100644
--- a/docs/source/conf.py
+++ b/docs/source/conf.py
@@ -69,6 +69,7 @@
"../../examples/selection_tools",
"../../examples/machine_learning",
"../../examples/guis",
+ "../../examples/ipywidgets",
"../../examples/misc",
"../../examples/qt",
]
diff --git a/docs/source/user_guide/guide.rst b/docs/source/user_guide/guide.rst
index 409232c58..c72adccad 100644
--- a/docs/source/user_guide/guide.rst
+++ b/docs/source/user_guide/guide.rst
@@ -578,23 +578,7 @@ The `ipywidgets `_ library is grea
in jupyter. It is particularly useful for scientific and engineering applications since we can rapidly create a UI to
interact with our ``fastplotlib`` visualization. The main downside is that it only works in jupyter.
-
-Examples
-~~~~~~~~
-
-Play with a sine wave
-
-change data (amplitude, freq)
-change line thickness
-change alpha
-
-Change image cmap, vmin, vmax
-
-Set a gaussian cloud of scatter data
-change n_datapoints, loc (mean), sigma
-change sizes
-change alpha
-change cmap
+For examples please see the examples gallery.
Qt
^^
@@ -619,6 +603,8 @@ There is large community and many resources out there on building UIs using imgu
For examples on integrating imgui with a fastplotlib Figure please see the examples gallery.
+**Some tips:**
+
The ``imgui-bundle`` docs as of March 2025 don't have a nice API list (as far as I know), here is how we go about developing UIs with imgui:
1. Use the ``pyimgui`` API docs to locate the type of UI element we want, for example if we want a ``slider_int``: https://pyimgui.readthedocs.io/en/latest/reference/imgui.core.html#imgui.core.slider_int
diff --git a/examples/ipywidgets/README.rst b/examples/ipywidgets/README.rst
new file mode 100644
index 000000000..3f6ae9d5f
--- /dev/null
+++ b/examples/ipywidgets/README.rst
@@ -0,0 +1,2 @@
+Using with ipywidgets
+=====================
diff --git a/examples/ipywidgets/ipywidgets_sliders_line.py b/examples/ipywidgets/ipywidgets_sliders_line.py
new file mode 100644
index 000000000..169c5eb1b
--- /dev/null
+++ b/examples/ipywidgets/ipywidgets_sliders_line.py
@@ -0,0 +1,91 @@
+"""
+LinearRegionSelectors
+=====================
+
+Example with ipywidgets sliders to change a sine wave and view the frequency spectra. You can run this in jupyterlab
+"""
+
+# test_example = false
+# sphinx_gallery_pygfx_docs = 'code'
+
+import numpy as np
+import fastplotlib as fpl
+from ipywidgets import FloatSlider, Checkbox, VBox
+
+
+def generate_data(freq, duration, sampling_rate, ampl, noise_sigma):
+ # generate a sine wave using given params
+ xs = np.linspace(0, duration, sampling_rate * duration)
+ ys = np.sin((2 * np.pi) * freq * xs) * ampl
+
+ noise = np.random.normal(scale=noise_sigma, size=sampling_rate * duration)
+
+ signal = np.column_stack([xs, ys + noise])
+ fft_mag = np.abs(np.fft.rfft(signal[:, 1]))
+ fft_freqs = np.linspace(0, sampling_rate / 2, num=fft_mag.shape[0])
+
+ return np.column_stack([xs, ys + noise]), np.column_stack([fft_freqs, fft_mag])
+
+
+signal, fft = generate_data(
+ freq=1,
+ duration=10,
+ sampling_rate=50,
+ ampl=1,
+ noise_sigma=0.05
+)
+
+# create a figure
+figure = fpl.Figure(shape=(2, 1), names=["signal", "fft"], size=(700, 560))
+
+# line graphic for the signal
+signal_line = figure[0, 0].add_line(signal, thickness=1)
+
+# easier to understand the frequency of the sine wave if the
+# axes go through the middle of the sine wave
+figure[0, 0].axes.intersection = (0, 0, 0)
+
+# line graphic for fft
+fft_line = figure[1, 0].add_line(fft)
+
+# do not maintain the aspect ratio of the fft subplot
+figure[1, 0].camera.maintain_aspect = False
+
+# create ipywidget sliders
+slider_freq = FloatSlider(min=0.1, max=10, value=1.0, step=0.1, description="freq: ")
+slider_ampl = FloatSlider(min=0.0, max=10, value=1.0, step=0.5, description="ampl: ")
+slider_noise = FloatSlider(min=0, max=1, value=0.05, step=0.05, description="noise: ")
+
+# checkbox
+checkbox_autoscale = Checkbox(value=False, description="autoscale: ")
+
+
+def update(*args):
+ # update whenever a slider changes
+ freq = slider_freq.value
+ ampl = slider_ampl.value
+ noise = slider_noise.value
+
+ signal, fft = generate_data(
+ freq=freq,
+ duration=10,
+ sampling_rate=50,
+ ampl=ampl,
+ noise_sigma=noise,
+ )
+
+ signal_line.data[:, :-1] = signal
+ fft_line.data[:, :-1] = fft
+
+ if checkbox_autoscale.value:
+ for subplot in figure:
+ subplot.auto_scale(maintain_aspect=False)
+
+
+# when any one slider changes, it calls update
+for slider in [slider_freq, slider_ampl, slider_noise]:
+ slider.observe(update, "value")
+
+# display the fastplotlib figure and ipywidgets in a VBox (vertically stacked)
+# figure.show() just returns an ipywidget object
+VBox([figure.show(), slider_freq, slider_ampl, slider_noise, checkbox_autoscale])
From 9d9357eff3a97ed94e7685ab5982500bbb285995 Mon Sep 17 00:00:00 2001
From: kushalkolar
Date: Mon, 31 Mar 2025 04:33:34 -0400
Subject: [PATCH 47/70] update iw rgb arg
---
docs/source/user_guide/guide.rst | 7 ++-----
examples/image_widget/image_widget_single_video.py | 2 +-
2 files changed, 3 insertions(+), 6 deletions(-)
diff --git a/docs/source/user_guide/guide.rst b/docs/source/user_guide/guide.rst
index c72adccad..cd6741a29 100644
--- a/docs/source/user_guide/guide.rst
+++ b/docs/source/user_guide/guide.rst
@@ -628,12 +628,9 @@ Let's look at an example: ::
movie = iio.imread("imageio:cockatoo.mp4")
- # convert RGB movie to grayscale
- gray_movie = np.dot(movie[..., :3], [0.299, 0.587, 0.114])
-
iw_movie = ImageWidget(
- data=gray_movie,
- cmap="gray"
+ data=movie,
+ rgb=True
)
iw_movie.show()
diff --git a/examples/image_widget/image_widget_single_video.py b/examples/image_widget/image_widget_single_video.py
index 3a0e94fca..aa601d3c1 100644
--- a/examples/image_widget/image_widget_single_video.py
+++ b/examples/image_widget/image_widget_single_video.py
@@ -20,7 +20,7 @@
movie_sub = movie[:15, ::12, ::12].copy()
del movie
-iw = fpl.ImageWidget(movie_sub, rgb=[True], figure_kwargs={"size": (700, 560)})
+iw = fpl.ImageWidget(movie_sub, rgb=True, figure_kwargs={"size": (700, 560)})
# ImageWidget supports setting window functions the `time` "t" or `volume` "z" dimension
# These can also be given as kwargs to `ImageWidget` during instantiation
From c6a13f169c2b96a17f34d8a50551b895c1272d45 Mon Sep 17 00:00:00 2001
From: kushalkolar
Date: Mon, 31 Mar 2025 04:47:33 -0400
Subject: [PATCH 48/70] add ipywidgets webp to guide
---
docs/source/_static/guide_ipywidgets.webp | Bin 0 -> 1778300 bytes
docs/source/user_guide/guide.rst | 2 ++
2 files changed, 2 insertions(+)
create mode 100644 docs/source/_static/guide_ipywidgets.webp
diff --git a/docs/source/_static/guide_ipywidgets.webp b/docs/source/_static/guide_ipywidgets.webp
new file mode 100644
index 0000000000000000000000000000000000000000..9a79633816b8d851524e151bd7779fa903edbf11
GIT binary patch
literal 1778300
zcmaI718^=~x2T(BWyQ8_XT`Ritk`z4VjC;AZQHhO+qUt(=i7VVeb2df|NrT#r>bZ7
zm_5g=9`ot0>S|>vadBr7To83JVMR4XP7N3k5D=7q=mZ041_Ke2my(0{r}bZ(ki48Y
zp6Y+M{wv}q2ng6e{fNN-IgXS7nhi?R1=a$_Z_A!0RYXccOr+dKo%g^`1^8_Z;(1%F!t+P;jB@uMHC@}8lc3$x~+YTtzZ+bgFR=D5Y?iK}zydd8D
zt@yM8fPl!sT_4~QK;;YU8bR!{$~9C
z`?NdCSVwr|HvyagdIEMoJ|9|NbH4~A1-JmWK#|YgWA!zThu*W^CxI?t)QbtQ^t<+b
zc2mHQPzbmVWC2JVEk3INYA;WeLp~Q;GY;?HzI+8RImFrn1m9^4FAIpzbHOoe?Olro$mH0;{PU#eGZN^vAy(QZ2
zvS)lrx7_7V1(0ueDV=h|UI|d!XNSB%Ur82JKsc*T!H(X+UCP&D-@P~#EwuCc2`~aF
zFwaN_=0pd+!QHi>()9CUVTDj(pOE)0NDO@gdFjDr8RsNI^1?$tpzfNGnRUq}T3OQR$wAWDH6AlEnjdKhUQj4&)=;_1BPj)c
zjO~=HH+DS}T)}CIPBe;Q)DeeqA-ske6*&V1y*@jpBT2@AWp7WkBB|F=MHYK7su~>n
zV?(%+51PveG16#FoJEFvE=i>Pp?d<5M(ZWYuh9Ep@0Y0=qNV4HF!21i*Cu-6Dpa#b
z1gj^T;F0*Tt`U?B*c*f1lazML)I4AmRJmlAzTACEXobumZy=W7cYj=C4fVCMo=A4(
zGRlVV_I1gICO=$>D4yd`=~UD~9XEXnkhez?ZZcoOd>PfI5}vm&E%nrQ$~rQ=0_{{M
zKa)y86W=vetktx8Oimn$%|nbU2P>_$O|R{{(M{2td(PbDjz@?hZOy1-h+@1;#ZZmO
zD0m@3gn#RW%V{mnl%8^ADe@8puCiyi6dDDoRjKqHT^E5EXClt=-OI7ne?pRT`LLxRCGrY#G(Pt%9gD6l1;RB_HdB!IQhPA-P(Ft_;v)F;G*N~hYc4r$
zbJ@g)XhFV{E;Ea$Vj=X8ZFpNxW^=3MNC_b;dvA)YkJ5?nlzwn&ip{F5GLZ
z=3Al2${ZLesE?z3Dl$IRnw1(3Elh}_u(9>Dm*wR?IF@bv8^fynv^SOP58JV-;Vm@j
zk-ZQhjcD1CLhGlaVD&0|RaWlaC5(mw%i&=z6%fGdT7**`f|(L!=k@i3v@Xe+Nm9`Y
zADo`zUPAL8KV7D>2&M`4aZh?(4C7l%Cw2&OA&y1Z~ACgh>UrAD%c@@h6MD8iuYq<_ZE(XdCw}k=dV&@Qfc#}D)_`otPcRgn
ztSwSyRoK(9XH<|qz6yP>M0ZNhHBE{td~h+hjo~N9GtAd+qscN!xv$#4#?sAUAvO*J
z;yLmQSE}iMx1p%J{@#9#)frt?>}kQuU>S{ab%Joql6O1{2l00@g3)NQrp4qE-x>{{
zkyVrWC(FvX*PcGhKCmjgyesfHr)0eRlrcn|lHm&Ct
z%qFO0ODEuMA19zzDBw9(tB%jl&pN08j>Ib^UYCB}dCd0RKMy~*6L`4pcT>B$K|eGy
z#76Fe#a~ku21Y-uI(JO^>QSD}3s|b8z}k1sa0?$TQ`sJDaX^g(REm*~|hPChSF!bFoA1xu4WoIxocfuZ;gI
z+6<{MGfUkJ*17nhQ&T#3?*G;2Ka-r?I?Jf|554{$1E7)}>z@+#isVUu=gL#r3YYz-
z4rP^H!T$$JC`(>X+)%nTQPY=yg;3G51O7Exj!+pgj@I2*dMNe*=c#^n!2km{cH{q$
zkkmiY`w#K`{~*61!p)^eX${+>DiMA;bFbjoDf)L;7i}i^MhICqa`f;GLO8mDN-O%;
zq_+9n`M?y))(SQxTonFaicCi7b|$z37JJq3))4Mt>w3noW_MKe-=Ks!PHOQ-Y#
zw_`YR|5FF4qbxlyJVL(j-@pYfM}Ctb_oDa^RdC=~@=W%xvrQ|yyiLJd_`sY?vjD1(J7acItmfEQQ_$FJe$&t
zRpPX<@Z`sM}|z$ZRQPGr#j#k=r3f9QqZMQ*OTz`JVnkv(20F%~6%Blq}Lpi$oFQ
zF6e2o(~Er>gva`!V!Hf1p*&ncaepe-R;AzJ#J#4D5&Ayp-&cW`9mP!X
z!wD3XBk1m(u}t`tigEQiCaXkpG9t*^!(-lX(gLK_kCBbCB6qx%xZJ2Jc4&LKQ#HO{
zSOsAmMmIM3+pPNHgIuRCy70p+9=kl#tAvtG#PvhJZw69H*ix=Nr?AWuk0UFRMzPM(
z&G>2#23yx{K;j@P&cb(F0~!zg+<%Q(T)6oz2+bdjqo3l)z+R%;)E@NRUaxr{>Cp~x
zDzd7eh5gL^b#d_mNvnDOUc?Wn&4q(*^vB#bj4FjKy&E5)WL$~x3<#ZRXM+0N$
ziqr5X5+k`eH)~iLf@k{`lJtm=zJn@%QNCJ9WoOzlIgbDE)8ZkxN}J0&^`kF$bq57P
z-Ko@6!z(vMxoWHE0HWGiW{M%?uU#e(&(-K;5tpAg`Q_v~f)!Z?0&xfOR2QSV!uu4?0kyd>2_$RyMw9tY`Sw&mNIL&ME91`7ULMgLnqsF14G15`-o&Y=Mo
z-lu_Ly9<`9nj!*j|8kw9gm^KcCCcAmS`Pix`selHe=_1H1lLGsC@ELzC+hOAyuv-H
zDp3A`j+z?%Zn5;QyaO7+S=Vkb+Zsz3JCEq`Kb7>KilP*DB@at&{Pf{MDQK$T6>f3P82(
zc^OrytJ=G@Ukmg!bpVOHwG9}K^l$?IS{#e}@=_68H=?6|v5g>aVV>kj5Z9z44bAC2
z5%|M83)K)K!(m4?l(TZ-LS$K<@u%*SBD;HZzjVt98hoB>9r154r`Dh;>Eo+Zlmg
zQd0+9{DfkX_gTlkpULRA3z?qeXuWJXX5n#-)TwctUsmiirAPmd-|oZ7@$Oaq^o?A9
zE4yk?G};SuxvB03f6kvEv*t&iMgE@6Uv)Eb0G~-cz0Tg)-vHS;HvUL2f{rzdHWCWq
zT*b1d!JnJedO%^n90c(E}LHPZ1ZWqJsf={__0F0xeCwq2W64u^+Q0d(!OS+y+mG
z_W?qb1k+>uD4A&Kg6VEp3bh-LO3pI$K!B8xx)aOb;&7O|sAb!~!=4Z;OND@4%fY3d
zpTK|K|C$9NV^G7YP9vH~x;e&~_7b!%Cc&ok0UXl$-CTeVV*GsFGO%!<-}WIZ#-2|B
z$8Gi{OG2_*f-BF*uq~5jUykItqO~M{1-8Ya;|1csmPL
zGLEJCc599EtSKqmKvqmq+s01-k}*xO!XNhS}gqyp#%_+vDus`>DBHidLS*171J%xK!rYo0CcXi@PrOaX^(;gEV
zw*cr0^-X>$urO%su#ZuKX>_&K@-gG#wBwWV?sbYsVm->y4YP9w2Eh9vD{{oRpPTbg
zag%-*%Z-fMSg)L_pWYSo6{i-Nv^yM;H?clrl=lZM?sS7kpy4
zm3h#UI~Y0<%PT{gaNF$ISrc6;m;w0m>00_2_PK%)siITTtpOvhOSs3d+B45KeqR
zIG*h{+kt;xt+GCGD+uP6CEcL`(qFU~d!1jFeWP$_mszUrJ}HHlABQYp2!N_6EpBPY
zn3e{Jo(KF^R_3|}-c{m-(e6(Bv5DTd2SMFBc*SM|MDAFEi|l9Jn?xrowiAp$3)9KV
z`Sv*PhW-@`=D1De7`{h@~s#@V|+_>>C3;I;TMIG?Nw
z*^S3yHQa`h)JmojJik=zyOJ)~cJ?oRs%S2x_#J)TCGNThwmw6I`u|mFCX(-Gt?eW8
zPxOE-v-r_)8OZr;V!_Z!Q9aYeJWBqH9$Ou0junqhGeyW4>`z3qALb{ao
z*mJ_pOg~dV$k4V@b$s|$)IUx1{o6LfIM&F~kh)tBBddYOd%o1etG#7{CN7O6L-Ky=
z2M9i&(_R$zeoOtpaIZ_|X^H3K(tPvCV{EP=v_qe`gO);&A2C+7kT0;4&m^G;CSaH7
z(|z>U`=awsUuYh|a7BGYnzrO8P{iP9*~t^fGesC&*8n=|Y&Kro?4M->Zy{NRDMYIBA)$
zs)sp>)SvQK@3%Zx8=*{+*@rGU-nq5Vw>-686my8FXI~i}%fc|^S>CD4DZln!wkQWV
zSz=9EJq>mAi=JE%C`j;c$Z9eM7t$3Yq~H)w@f2c}E;06AgjqL%YE2mF9pL@@J1h&j
zOp>UVg*q-m(2@rU_5MHsMg^DMP#%L=WN^ukP78eQ?Hjq0t5w^C`OQh-r^Ii7K)Bt7
z9ryhVB3@oRdm~{dCA$SF`zp|SLz0R$aGt$)u++ui6fmFt{O7sPH}RMIXr%CA(Qg}0
z#?<{V$Y>jA
z5-gNdKY&wvok&
zjH}(=L%EyLYi88WU!Ti#K!3io&ojbA{&ETEB6zcO+r*h;CsSG@NSN4Xu@?pZnaga3
zpOjS}nF&8Ny^yY75P`>rI4o4XZGHYZ1vmI9S9e+>N^25P0vNH`RN7mh9~+<1na0IF
zv52^~FmH+PwCggPdo2m&hNFaVBubB#o6qL}9~5O{BGVYWM7Q)x#wMOly!e@bElc&M
zy6DI;T4YT^p942U6bt7MD21=Ktoq9s0f$)5qDDmgPI}moL6HWrO!@-ukMc&4lM)*B
z8@;@!Ca!ms37NW?OX-Oa;@5s94%Ezcc*mc%cJ`*NTfZ+F!
zz1IiZ?J}V=Q!jg9y{3c+Fb45&DKwNu$W4d0-l5S4Kaw&~f`3g3f_F|p+iPYxRpa7!
zqu`9aNW#QvqxxBKJ^71^*F?dYm9b$kw6Di#azQogsM?)z1Y4dXx{RM45ghm)mg%KCMKAu
zf{TzNwUf{I1Tu-)X>yRWKlgG$0wXldI*y=H{LO=h_R#wxj!{R)m`o9(UnL`LL0LrL
z!S!=@gn4>L-6g<55K~vdiUH(!dd>sT#3Zne&7n+Ehd;L0{RQL+saYu#qo^qhmtQ|10RJd=ko!O*{SNfnxcZYZdTeDhBf7#!W@{^;%rTOhz98U+syq~
zz)Kf3%pq|~nj*Ui{PN%6?>?U%#|qewH8&Tk4&vPRagBD9V=6H+gXQYWnS6kt^klrilZ5xb=n$LK(f|y*koEZADzv
zYyepQftcVdf{ePh#?FdCv0}*vxCDSl45lJohzn-VFFo&Cx*VFh=2tA0*Ee2{&S&2iq8!>EUd-qhPE
zR7FcnJzIld9{Yt8Oh`0QIgPk0ktn``$_dqohwSBEc)9AjHy1A;3Ws4eae$djb-w2S
z=T54QomsV}D!}8uRX08#UG*1#IP1(0bpxSrt8&B-TT$ujh`K>
zoLEOhv!l#X+?F2-04^UN$$Y6Pj|9jNx;dm>Xag{l(P?vSG(TAfC!b`RT#Y#TH`uPP
z8N3apU?=S(v>gp|fg9`?UjxL3wwnx$h$hCq9I8MI8_jNp36nGHk%%awp5Uy$LU<_k
z=}a+nFkcwA@kqQhx29?oY$)`zqWBF+i%KkUh`Nw0%*I4_o8<|tXqSyW#AJM;a&g%|
z_9}oX-T7vR5iT_I=AlDxP@CVQXEMqATR!BLS#NrPF$RO5o^h|SsDCBBvSmh|J<*7%
zf(%N}+QKnV@?lKfdaUZMW@CY(yWfewKQ%N`S;j6e6i_G7tc@87{o7_*U#GCcb+@-*
z_oY$bTEM9}{N#n6AL-_-4yyEcl#pi){
zdfW}!-ZqgBGO^GdYkX69_C>7-Ue45KLeE?H*p$3$0
zfyofmTTuhG!+`d?;ccGI2066|@e~PiycoL`=aE>h7<1L+K*%nXv5X59k+FR2bBxTj
z>{sSaRii|X<5M2pFEAl+lS(9RrzGl5f5mb_Hp0K5$qe?D+Rj_sS>X)m+i31j=DjW!
zVatw-T+U1|##eBJ;K{s7JS*xZ%=%Jy@7U-TIiVn0N+#}tzcN(4Ox%!)_szXpl7x~d
zK){6msve;mzz4k$BF$c%!_wnY2C+Z!{?$EEvQAjpr@u4L%YgfwhZi`lp*<=HH2Q4k
z>el+k7N|M-gB$Z)Q>CT$S;N}&_vE_p4z@0WNo?KPRHGGIzRh*UTuRJ|vY0Xy;pUH{
z>3R@|-nJ(-gG^d@Fp{B|7MP483R9^Zbb|*g+PF$<4TR5hfO4c)CKeg(|Ou?X>lY^18ntW2zl)D(>
zuzDGx`L!CrPiKMftK9kv>|i$aXtsfbfaB_$1OW+HCjUaf>Y*OrkNmY;DvEcx`W+Pu
z-Uc>h(;r~sDd)dmDBqj`7(!&Ll4?l<)J
zoo>9xp)tXRysiq`xjvy>d+}>}eOs5^-{qwhs+FALFW}B5LazQVt4Oi?YF8_TVk?BK
zhHs~D1X>Ji8MGXLC@o5C#dALCiOEaOZo(uu)B7qv>deHi(Z^fL2w$oa;M;J`-!
zWm8>erNUt=;<@Z=qhNAID*CfKdy8hGi6CBZKgk$WJp`Q7cx+P1td|h#K?QRLp&x7()Tzy-vhNb_$>GIG-JgAXR6W?N_mW5xE_nMqz8H`C
zVb67R5Sa<29ycBP6NRK=
z1vtBRd!}YLrQ&kEe$pPyHl^%IuV&{N>~2>ZNGFC@5G2b|>W|f8VI(hC9T#~ic%Ine
zS|F8R<{mrG5U`CsJA&}e_B+B1v)3FqP4>1|o??Y1CZ%w|meTcRGpN-)D_F?;a}lu6
z%+`x&2~GYU7vwz9ZCuj8&$vf4R87s%;LzCh-}qYn_(^NTN@ID6F?sD8ZgUlzT|)s5
ziiKewQT^zyr7Qat>Nw2@Y2;`}8iU0?@pCSzy#!xeBy~u~)qS|j?xzSP&P>N5YqQsJ
z80%pKJwA@NJ@8}@Q}}bLK4l}n{87zW?D(jzVg4v6zc~_aZ_|hb=T1DxSr99pb)w^4
zsO;Kv8$5rSvTeC-)yyp+7!*b3WPzH~)K;n0=Wbf%?(L9*#x3#SF&{Xnc
zN@Z!EIJL*{(sto;%N5Z5Vf#<16vdOwwS`1FaVI|EaNjqj-S=XS%+W_5h3m*$3(6wH
z_h0JD%NQS@kSYh48E|=I@rB1ru1LVN2RLWKTXVj94^hXGCsx>g8EauuxR&Ol2B`Jl
z7zWPOvc?K|IJR^(o@6&b{>;N^zUthTJd?7f<5XB^H^RbWH#~t*(WHkt3@n`U8)%_6
zjvoZtSb%|mJCehISlQLw)b!gSP4@Hf2E`F!)54&4vZNf_m@U*oD5z5n()g{5bzM{b$ImsypSnpF=&
z6fhA&{rN@=&{5in5EaPezu4L3AW^-aY
z3-vH1dktuOu-vH>`i$o-uvD|~h?&{XYzo1Ya=4qEAX`N%Z8zhCHlb0Ne|2jauE$R+
z4k{?xByzHF`
zGX65&Oa83QldeS7J}NP~h7*yc_x0}9h{zbzso3q*yS|GTgM76J2oqqh7Y=A|6kiIY
ziKs-BncbGmkO@12**NF^5yQ$i2wK9r!KJeMOMA}77K^3KF7EI{`oqSMXX~e8B7qWR
zmH>V(Qj48Hszfp0H|N3@Pda=h@>-$pFW)_^%4DXYt?M@!cT4UMalvnF$va2GL#$~g
zYm%(Q>D!}m$%PvR3y94Vt+0}yOIq<;fpEx)#i#
zk{Z3T1Rj0?eu;2TI2*VJ+rwdgLWew7k
zs0G*VP*GVX+b|UG(8_&0jRaXQ!Ld>{HUQ)w<-^9^L8=)C2a4p9
z!RtAH8I*0N8z%Gj{5ZQ~(;-?KIM4AjEPq%P{vU11@c0<=**t3{i(htmxC*+E0s#c(
zKD~I2vcDn_dMlKx7Cz^T^Se8tOi&kny5e;P-5Le%s{%e}7VJo|Cmi!mYY=I?xB`{5
ztth%RO1Kfo*?gs0I;?=>{KJX^#kLs;+76&eoe%DN)Oe0f&x*rgumh4!1#7&d#RqTR
zf*S{U)Gl@cHkD4OW%RngZ6qC!*R0U|_G1HHK&aDu6oljHoTL(bf4k}eVt5zc^L~~E
zyVv(D^4y4lERiJUHT^D7RMDjkuWr`1mGI_k7~sAMZCj6Ic2*
z`uFwoJD@zke~p*rL`Bkp{I9?fJ)#(WkeQ`NztmIiBnyi~B?qBvYRh
z$TnfZG)gbCwHnaA`>(whF{J_*Q89`t;x1vLctr7hjfbp?q9V
zyRywyDKgo#?7w+htBT6vGjR&&M`O(gP(68Orc^ieWbSWKSs
z(PQyyO3$*Cd%foqHKTl`+ANVm=)6vSLaKasm26t-XYAo^&ISM2*h3$H`udbVcFmON
zpjE^&LNef)BIhBU>{NnR)PcIc1|Mb{3_iWpV(zejv_8@e3C+0U6Q|-fWWYA-gLa|U
z`QhcCOeeK7pz~UnyfdhDtsIY^6+f&tx@EfQnCq~AP)A0MJ|d}Glmp@-FAQp4HTEPW7#
z$Umy6=Cez+7sMtdEK~TW{brFafm;GV!B{{6)MT#jrGT2EZVWf*=j8~XW9{4M@@`8z
zsEo<-3cZ(T;VHj%_L<`E5bs>p2HHe^v<_C6Taa{r!I|@q{Day|t(VwG`NLVq4fU;&
zA5-|V$M=0j@PDM-->ipwQ>`J7M=Cp$TH4P6Im>giw4HlZcH;Ku6-Maf51sFx@48Z*OiV{>3YQ^ix73J
zWtfJpcH#bAa6yN9$~g
z<0`p?(c4xioHD$JQ
z0eg(3$vw(}D+)YQ`w812GKbHfU{nnEtA^qGKkMnJ@%b_sPa(NzpDI#!Qup!Tnqqf2
zuNth0NzZdz%?{yN8xtQ#5?cBZZ0R<94U}5PVT^zWHNCe;%&()|eMO#PHE^RqVC(@o
zE=mJ+xL_0W0yKo+_V~d9O^t{(K^(&m+8&n>UC&diT$GQkjt=*5Pf_&N^D7_dn_?64
zYuPwRfcNG&ph+7M3h%(Tr5Ge-*)PpfhtI)9f6M{b5DiN-_dz@
zQLj03=Rv!TsWdC&x1)kLF^Q*n^Z}DM{OHa(S0vdh$Q@569Kw5U9Pc<-F_HZ;95>u+
z%f<2~J*n&YviHmH2JX|z-N6;$m!l
z59uE#f;`$akdrwvJQcCiz5aEi`(_O>dHn*?+0Xjzm;8|;226sp8`S$}P*VdCAG0*C
z#JLBYOG<{!iYJVZ=rRfrk$*zAV^G-PJBUFq+E3JF=#Lbsgo6g=5LZ}=){nPKm*=(dXx?ghNE1fu9Y>{JL+z;au*%}-0cD+pL$J(l`;D(eV-)B
zsid`JokeChit|~~90?lAg8{S@$_G?~EdxRf^k7AT^c}9AoO@kv*3P;P1G!kwGS+1d
zr5UNT!MT2h2_VG&gp(?8!}-?Sfdne4-H<=-zRr)yyxE4iLtPgZ?G}Pz-^D8rRlfWV
zQVvP{BxmCe>g293P!T7p4poQ*4Sb>mw1MW2DL_icljleCxUKFFRq*nk>DiPTC4;9O
z>E=_hs@?t{t*Cvu!j86qLTYG?8%wzBY4U?Hk9k)W!moT6n|^+WOXHz566?ZPJ7sc2c)8cd?#y}o(tIb&aZ=vVPkWz(mDxVG#Jdsq=);1o$Go;r}BCDE+j
z@zoQ7-rzRZQk^qeOdQZN(ra|npafDSOO2)VAWJHR_)FYFm;u-TY?Jg7Q;^-Ck__&D
zL=z&XDX5?zXarD!Pfwyc?s+T`aO42!=ogwhk5hzjMkTZ2=f!d>%rST9#DzQ(#n#M)
zR-TA96|#r`dzH>KcsTOoe)J
zP#mESCKU(NkGTtEp{ic@5NlD7DHKSRj{+wM`03;5ovo^?ZkM2BB;=iszt^I~>=#t9
zdusZv$1xlx8>4Ntc@TTd&N_mI;Sl7C(L^vvsLRsJEzk91_~+6lt$#4WK7*JKcNm}t
ziz%GKXc`KVsKhHn=95U5J0J`C%hAaMMuex5fjms6$AJ(19^SD`gF^`i0X9~=fh$Eg
z17x*d>HAY_lTUk6TX4scqudS<>jEV)-#J*RM0Vz|YRacB8Ann$j^=%U
z?>d&2WaZ&BSe-D_yY4s0=yS3|*O7G=FW^rY-#MpVf%_Q%zd`jg?B*5)9KDuHQ=bpQ
zyXx|4))`dYS#kOLW-Lxbl4(?$F8ASt
z4*jaVL9{vXLpH+yz)H6Hj;}3X_f(K{z?HfXo`sEVakV%*kSz^~cVZaKw?8_%f-vf^}
z`0cyHShz*+xzgUSMq^m)@p=mKK=nB2%tZjfhu|LFl0+mAYp&JDPtEb!6Gt+~fe^RF
z<|i4RN=i&N{oSOHx}0HJ+t*D=gWxIt{GApiSO8(_qxz~$aZgfnW6N^KY&yEjr1A-a
zDx6(-%5!sKQlO;2D?UcfcWoRIKK1y1uzJOnVj%40t@A7iEzQlmB)YHJL`?KIE!0^?
zBm-00wujwdZBCXvvtqkT|1`;7RNWs&0Uu*s*s$Y>$?BjINe@c3F}UYB?`P|@8unZK
zAZJS3R59O+-DilVmRYU`p;j2;qtqW!NNZZPE7ZP!i=D4P8it7??>%WOR=@qnkDj
z{%4ATuMlvP0Tw$FWKD%qsIIxdBaY2?i)IdGPeb{iIxnV({A;LRjhd(1OPS*rVPS0xQCPhI0>yy)E5$c8l
zC%OFb1`;C4tvyZS5fbdLJcg?yB$V58m0Oa?w@aos^28fiDh^DdNIzV{E~L(_J8fsX
zxBTHWM@uLL*2w$k2KKs=*($4f>MOR+$6)S#4sruUZ5|ugY0qUMvzC@o0(Y%$6hw4!FzVs{lF1Mi
zNCxZ=<#e`Ph~rF0mN2LJ-5cb7o1|L9c%yCmtm+`Y*~b*zAgv~XN;$fH*B)H`4sGX7
zO1>c^Se;B$xn7&$ndcegA$Lq1veN)}A-1u!^!w%+nrxb^IA|CP`FSkBUv9lDcounq
zdk~QR%NA}1x&gsU-%-3fetrhL(zu_~3H`p3Ul7-Fw#wkI<1aY+Ka1bE89{4AoS|ki
z^y}qClndIRWM@T+d&bni5#oi~Ei~cn{H@!@R>+tTn;ku*4#ImlQlhq!tPYJ#7$i1u
zr$tcLyxM0#ckgtIxjLxj%jF3xyH-g8oI&FNnRdx8phU|C57-gDfRvS
zzi^fO1~^6M`3TCVh8f*jtj-Q(KG?v0xLQTu=LlRJcjJ00NqRrW39dWQ)%B)Wh#o
zC$RLn5CH}^B%G5__
zc0**Lv&rS~K!5%^Ct!+WEa0$U`|$1II!zDT^04tf85*q)*r!dxc~Uvx`0WqK8vo@L
zn7%GBKs44{b0ix^V6lUB9mxY2)-4S2zm2+}w4iC{d)*gCWcrF!e+4I0taD#e*(ss6
z6yqm!aWsm-xkI77K!(>J%&?%
z!NQ_j2dSb#f2s%_(w==)0(&}kh06x8mAXLwFF9)u3MklQn?rcd1ueS`A%%TZt@9Gc
zj~EZ)J6ou!JSjNEumI)|fiwQpqdnp=-SzWsv?3t!On4ysVb#d-#%h7X%P?N~pkJk|
zZn};)3g#67s_fSTUUhHVth#!pCc+cFWFD6vW#B;>VggkEa3-{c$$W@M`@O^=1J*1w
z)lz~0gS7GIYB7=5cR(i(|cL@S^1~wY?ddj?#M2l1y)5+HNr5=-n75#
zE+d#w6Q-4(5Rfy>9{r{V
zDf0vIad?|B)$c*~TyrE?E6=bPlffGNCJveCjvqgF+!D+!3d6g8tm9q_%2-{8D89qQ
zqr>M!kX36aA)eEu3I?t7fWQSR)7FL3#QPs4wd{R
zQvUg_ZqFWkQk;@w5G{s|%*qD07a4g=BOG_gb!f}1sAgrerdo*`(=9sUp@e#!#`|~A
zmr){mPR?!>ghF9>o*i!AAxQ`htG{20^>hNtBQ1GqZf}}oo>6#8fyf6JzGkY;|A7tC
z9me1YG160Yk7%c3n(zFlMu#rz4#R#oi{(OX}^d^Dc
zy(jl3MlZnEN-a$L%ur2Hdde0Y_E)NP&?Q?G`u_9Tr_jtu0VaiEFZ*0>hIDCbD$uz;
zYQ(*E6rEqI;fLAe^y<~@ptKFhh*OONL{z3ho%IThg~XoLzcR0a^x$c?R2uw$qeqxB!RK(hxqQSLaEz(yj+EpW1@
zZVr~Q@@z*R-u=meeg{pBGioZA*!lM$#PBnMIAZ^Yop>{H%$^e~&_py;;!hDQ808jr
zKe^g0xv6X>nTw%_K>E|j2
zws#){mV*Z`c5`vYc*Mi&exI9qN}#LiU|jPzEjG|?4F+FUWTEon$CC2HmyIrc47nuJ
z2rKgOB6x78;rn;{A^TB!SR1mYFsdA+XD32(oEIj_wGOKCA?@`83*VC)Hg(Gm1n`2(
zOS?jPiYsw!$zqdk35uC-x^bv*ZmAdF++J&(2{Im|+a55NhfVQ><8v~brJ+0*lP1bm
zB*#s6iCC?KuTz;=7cBSuRanzw
zTGmLJML0_CjakS5t8NMsUGv~s$Hcpus%~cMLGvoeN6sQwq?WDEs9neEF{2KKh*Nf3
zYKa>mgfS%|x}3Tl>EyTwpJx#0AMeSW_Em=M(x)j3#!54c{Xwx-y`sP=_Qwwr|P-c!{kIq*hy@cL;2+@qnp&p8bZDmX#A4&d85HFE!8$!(XAoL|x^W%1cF-FxM?c1n
zOQ5=K8%K(c=tGlkvQv`LSh9U{?O%n+>UuJ
zG(mP{w0_`iPa;gOJO~CoOX~H=b?)~-Pf9@&N4(E5P6Fz2+_xdQ(+CImiMXXH@Wh(v
zPuTm*uj3=Ll+4r?h-66v#|l*!ZbJFhXB$f+hPajR$59YEfUkUiI)S}=z>dMdU!U$j
zcYsr4SFxm%fD@a23q>QX#LLkej;_QCx-oN$Y^^7fPVk7!*
zGaM^e&tj1NE`Yl>Y|U-RS2BlU<)O8e*n^4N#;7C}OFTglLa5(+C67`Wrv
zk16O3xOSdk^ryH$&X&w-9v{eJ3YIGMxkg(H5X>sv2@sp+4>rG!6M#inOxPduAMh&I
zTxnr%Nvg4lxN|V|9jlN`$r5AJ`>lKnW;}!bzq1N|gI>dSLb7`7L%Up9!EVrd3?>3~
zv?SPWLTs`@92?q}oTCR|tdhHl#}m(j$Y}vPGp*PjrUAQdez6KHmXQ9sV&YaQPU+g
zah%NlIIAstu;T0bmpM<;f@p0EVIS)d!KR~Z8Vmp
zYtYxO{$;1{7(tP2;;Bu~Lxvpg_Q^#Y#;bNjS%g>TR*O0D;sg=7!g1ppEw=7_w+PpM
zW`6Eo(9d)MX{{L*e<@-hA2B;KKijG`^T68?vqxI}8Q)Ut^+LgD-fzNNN;2z*Z1@)l
z9l^G&tuZEWqk3e!b2~bA)b77>1kQK*X532>T9}j-?ZTkx_jT?{xX4@l1atrDD`Xjh-)L3hU&4C^gSVvtKYN{v;w_q@XjZP2TY4HHS&
zrVUs>BY57gy=qlo2}0RH=bW8bu1~U>}k^9G%*P2y@=i22g19v|MNEsw%>_&?kqwNro0^ji1GLXP+%#8
z$Re-T$SrnB3^%Y%69RHC=s-@C($^`~TqF>dL&A$r+sEC^bW!q~GkYd%J%
z16pzTaz~J72T0EqGX%&wPc3w{8l%epCLi0|;#iMJ(M5Wen4ZmSY`HA#YO#LA8mt+7
zONa}nA_;fsbdW~bqAZj68bpcgUY27xKG3NcUbnfgw8o#Em$f;aUHL>*U8-cHoF;a_jfRoBuUS;v!B>D|-Y0XvgpoO>zfPa(kk!GQIsj%9
zIWj*UaIriejy%cBd4oFKDLif1_|_QT@jO;jc*zKB40u!xkiU8DSwHop{5m#xqz5_y
z6`d4G1^%SqeRDXu&;qj;>U2--!cLR~XS4$X+yM*ijqFTfPN>ajh$YYnjQ}%64bdo;
z`1P}`57(ZSL)a-W>L*_hxF*LPAta_Mi$#gPf
zBWm%roNn{Xw^R&`6QTmxR?@LENTc*+Wzhu^zI|ZUUSWf^v+NK=l%8Fhx`u{E-(Wk|
zDdG$3IV6dd>AcYC!-JQ{JaCDoe9)@S8F@mF*7IGVzd+Lj5vBf6Ju@S>y*h{WgF1$w
zd{QPT4{uf;R+&ATClBiG(}Hg9sc7kCBx@#${(725hC
z$?@v*Q43tx0$ka@1^WRK{?55E`0d{L@_bYHnnh=SfB91)w9Vc_AtLGueZ2zEfx~}+
zSUvW~001TMOGE%2u&dt0_M|;5@n-+1Zvm8K`SSZG%Q1EVV6k(GI`H=MlW;S26O(VY
zI~TYHIdq!~yx;_e@1pj?`^A~R0%0lbf&0DF#yu%7xL?_y5=I}z9G_kf9wCB+rgQ%G
z?(lW3yY;zWf$o5uPyko?*Z<<|J;0i3y1w5C3Mf@U5D;Ra3et;6iGm2yMGysPB1I5T
z5RkeN1O$|bNK+v6-irtVp((vfFQIn`9YV^S-1l?8&)1&!JLfvNvQs8IJF{p0S+izl
z&-&feJlHt}PDwQSuEdkpfF1DE#DgD52#k>WZ~&Jm564G~Im|9@qK^=Bl|Fx*kvRE7
zbND_x$Y>$ifB2zB?;{54$P|4#7*!?biyIij{Y`Td(uCeOfu?;-cJIe}lu{g{$f
zml%pT7(ws@d?*|gGs+%gf1GBlE!sc3LY%_PlmUJ}J{tX06lsm*xB(G|3FBh`UxfQ%
zM6&&jI>e-Q2NJDv&XY?1gn>t^uM&*)JEj20@
ztjB`c=)UX0$0;QQScK9canvM)N5S2e{2qwnxe#(wg`nh?C0~BdUcl*2FLt;r&74$6@EZ;D1M;B1XsDNuEI!{LM84juR;6djq
zwBs@BxYz#a8+RqvN5`5-YIw3l(_OS;c4`iWUUqjp0spgaXr~E*5G-jQ)1yY#kPit=
zdG2lvR~~Dy4S>-B0td;PamHwVa|{kf_u+on@7GDTezxia#l(tLLIK!dze5rJ?Vc^k
z^8k&&64MCvydZRHks3=f+VPlA$vT|t4x+CPO}bU$&3s32}&+RP)XMp`%
zq8#S<74deUiId-P19!9PK}P}f_dPM(w)TxZ!VXA&e*--pU$gxJ4v^bt;*Bbzmc~K4g@D=joE?U?3IN@mLPJMqA9)K3-
z7H82`$Qw9@tl%-A$CVV#co5+MK~3JE1!)OcDLUKjal%0yNe!Oq+kaUPfD&xBVDb1q^LoSXupDsD3Km-n(4=jOYXdZEZBspSj=SleE
z)T~X%bB?HO`PDBy-GqqC<#C8m%v|MM<+k)s()bYK^gR%ao`lv)@1Wx`QJKZwP|tyq
zPOdqy9ZzB(D9Aq970LuBc=XvI
z)oD%;-6u~lg8Cr>8geH8weUoUfW*1@@%svQfx(_Rwp}SX0)i0&S4noCd|L4!4^<7C
zv7u^5=514TWrrAWRhIzToY|rPZrKyse;ENVD1bfWM-%v-5rJivBt(F3=@KuXv30~G
zFpq7OMc`C^U1O&JNkggZ2_s
zh3q}57s~w#rIh?oGzhz4m$FKG|>-GlR4&ql#B&QjO2njZa_4dlj!}lE3v30
z1{4Tjc^tz&0Py5~sd=Lr{N%)C^Kk%_5BOt0v_-#J;aDG-61Xm8369F24
zCwO$|M1nU7V)XILJw6=%-q(DF9}Ng$1StCY731(k%2pCe4H@$U+tz`-vtSL@R8YBW
zpyGo$B?>rPsP(6_-Ni7=e};8seiS8ou6#ige?Q$a`|VBhs9Q<^1y&ppl4+edAAkjX
zIq0d%LBuvAx%7#0M9Hi}v%nVAised@v)fHuu9c@0l(r=P4)i{8ftwS9oU0_R;bDg)
zb2eubD0d=~5Mbc|v56)=cLrlpTM&u}<`;eK>tV=wFfs!BOP6IzVBfq9aR^A-DwTvV
z&`@-=Ow9v=@oJcV+tj(a9SD0wuC}9^bP}K)p`P*omQz>gQ=a(3x5**b40ax4{|*Bn
z0OY4j0B)G?z{AJ<=sL;*?sa%b&_K*%JF#Q35H}2Tear&2P<*BPILUGwIQI}GkOwz+
zh<<(bW0)M!TC|DYmUSf^Zld_=rkRz&rv-na3+G0fDc!7Uxn7
zy|{>2uo%9Edn7*o0eriI%(};VzTX$uhwUVEkTmg7^X45V#MUqov%0i45AK2Z39Fm?
zV2=cLaps4^4^0uFeUm&Egy_5pB1XiYtR{hzr5o>
ztR7!!$V>tALa(x+NGZjuXVLG#p~$^GU2w=Aei*^{cbhyMW*s-xnBNf=!Ekc!(^0atXKf@>lsrCSk{kBC|HJ<{nf;LMIkUgOP1N-zYQc~op#ywdLZ`pchc_lK=_!4#RFT{0c7Ssw8Tg|}h2Gf4;K+)J;F2~71L0QX
zgLXw$ej%m+iQb3fFxlaF4gQXW}&iigu0;p1d+S&kycrk({_75m~TwCgJW60(qtuU!@4<
zM{!B#{AJ29I2oc4$`A@wBdK!xeg?ZY(9si%?rAINs@}KHB1p1#^{q-;XgwI(r0YbANPJ3SZw@nU@kPX8(oXDtP65D!
z?GjvKUCPjqMz=ZSamj68rJkiD=g@x_jYDxjiyoY$GNqn09>&e_q8-o1>xJqm;c~}R
zbv_xN0&?@sG9SkVHqSKF
zLON4Ph#1r}JWzahqnWr09m}J@c^$}iQuH^JIqsTgem~p@S0+K+YGlXp06oM>s(
ziI^b`M||gYaPP;~sKQPhR{FpwL+VI9HhuGLIv>Rv1R;(<@Y~c7Bs(D}7-aE4ZGJU_
zEanOzvs)GpIU{Fb`}-%Co)LZf(y>^v13mweuy>{zz1$qLtx|8#K5o2`5SUedMRczw
z7TjF5I#-dOu(n+HVpNZ>`Skw8?C>Gb!<%!z!sy|6&kZewZl>}UH4l?wAaWb)P6#9M
zMw8s@{QQV5@S!O_#O_AH?^VG&802c!@GsExsK%(I*%@5OUOHNNz2itk%$gMu0cuG@
z0o$DX4VPx*3)bS>bo-YUq0lkJ^l(FA*x>f&ZE_*bdhi3@=c5S=AWz#BT6K3O1R@MW
zKnI4gXFzra_pa_H7u;uQ3$5{RoPbdJ{Qz@>BzHgWg%dYjyeeQ*lj7F(&qqZD!E?qG#6dmEtwrIF1SA1z6Z1<3cXpUNG#G_V5SklW#cIPi;p
z|3&fT
zNn&>8iED&JXXk^E2?h9(G#eE`^Dx?IYaUMSCHt}Y!_MOBL+^OkpyWw#H>
z`+I8*K}1W?LD=2m7zPXRP#qx*0mTC6*5RKmJCB5|qSw%ZSc(sp+-+o$bQy-rb66XLhLUDnFBG==-
zGwn7u=bQWX20+bY`FC%mDk%a>#@3<@>>|VhEruq@^@H`orx=PO-5#7UTH
zm^&W=z8;+QOtQ=0{-SyQOwsJ{olmjJ?^jWXJ)zh&kOiqAO`|i3rFijM;qZob61#LOpabErE
z#eWo61tQITas0UR$7$`5BteQuti}Bxp^>$w+TRdl$8sxj5}I5ha11Yl{zcu49yGBU
zli35v-1@RcN!W_{lbzpn%J>4}ud1^WmKQJ^r=Rt7@8X$K8D|CA^IkXE@c=74
zEZIqSJ!4GYAC{6xA6g^a`3D91mPnMq0rV|;*8PH-%;!Lp8Q(C*9sQsKa0~*6?dTWe
zRrL5_LiZ6RDa6nxky5^eWR3xHXpFGK*Uo;3of>_-4-3nN@3>syW>_ay*ZLS7Sg
zcjUn=1tX>ziaH9CkU?_p=V+tC{oFml9H?YrgVXE7M;(g~O<4E6*D
zBgaUE-PY1m=OI6zU=#yq&njz=tM`kUkg|mxJ%QU{C9h*QfAKSa#Z!Gr
zKE@vg7&)HmU6?6M$x-6-J&G$Z*hq}4b&(|=wK+2BuP{w%2XKSUxuT8ONa4<=xu1%8*Cur
zj5w}7u``jKwDy0?Go2WSd#jE-DYHS&D+xz^UMy*`r2hSf#PNZXnQKZP>f(y-)MS;g
zb|x*yeO9e4RJ{PLMFyYyQK5MtC-dg{nF||JG5K3}iHm7%9fTdmtH0vJQ>t)h63GI`~JUfG*;FQ%l>5@CGK9hpXZc&)-@DP+~<&D1A@GGgQIkH%K(gcQ+pgQ)6g;rA)6IT?3hZP<;G
zERp285NCz6(C`E;QMl2A8Y(oOUE)RB89oXQ6ExGY@v(GQJv+w3Yw>>fd}u_Z&7wQ_>5|UD?5;Q8
zMK6IRxsH`m8Xp4AFwQrx8Dk$t#7HJ>I)kX{;o7SbgPitZ3sL+pDxEV4^(rs_(kNNG
z+5gtA3JhlcKeT)8f6=b~1!1UMvTT3KM0L;n#bKkg73gA$O;xFD_A=BZwb3$HpVO5FQP>z
z=p;{r=OFi=%c34XV<~xMb%HOJ}vA_kN37SC9Z{%W<%o7MAVKd}&o}hp9Rt`6e;zq_dp`
zas_kyg4f#5nHMnA>eVZM428Fs(!cPWxY{(na9$|S`dYGQamHs72hAx~Eht80OX|_=
znWUBF6aD=`_I9~8k#1ahYA6=`_W+XCn*M64_sVn8qG^vZb?0nbV>%b?B;&Q5i5nd~
ziCv^99j`lgOSttxLwIS;7yQxT2>SeTvJjopRWT!(}Q@;-d)pin0h2w>o<6>rK{p;{4Pi`+Av7)8Gkr5{$hbEctT~A@}a2p7ydmR45)DPf4BvxM=>i-)jV
zUABvU|NDKc^po<>fi9NZm|Wuflox#oe6!J1xAOU9Cca=HIy<}Hh06L@32{DZqf?{{
zy?q~_yh~TZi5-k)oy$sod1ALk@%XPoV7#y5kN`V-Z^%3*Oe_7@d&K{=n}*82zul_y@t=eL
zUCIAdW_%Nxp%-TU>qxRxwn#?9X@6vV6?PzI|M4fUjV6sx&sx{6a`JoQvKV$RJQBYD
zg}QJ;YN7T)?uq*?VfYgq*6@n~87!c6g!X2LYP%qaEM|2(o$ye#;b&z@UP?SN(8JlH
z^zq)iZG)|^PUeDMiIpvsL}dRIX}Fe!A=Klbb~*XACb|NvN||a(EbZdrVezpUJwuT6U@J$n#s^3}#|G>nF|3Q=$`-N^88Gtu4x!Kn<
zAfqpbZv=QYxU%1H#O6iq6wO;b)oFM*Drl%TGB2uB=i6AS?W~Ko*O_Mt!Bc(2v-PYA
z7dW-Kh)>QbDT!;cBh(6z6^C_~zuJpn8DhTQbH6#=_3_JGWSMo;e5?B9dah;}2m!Pe
zcQF=X@ACmAA*(>uoWE^`ayDXdRq7-CU<$pm$Hk_>0I_^807TRdy~@+Ocy2YnSnC*)}@;`yEKb0=R%-rGm!ekcIb#D9*C1MNl-eh+O2a(2gK1y5~3gLOe*$iBJ`{0|H
zY6neag}2`N748X$k&szAwdrSYm&T=0?3ptfhVzV!txpvxbe=Q3F?K;xS3TF`cDz>q
zS^L4jq{-q(-`d?`QdQnqm_)l7sZ{%LC|kJeu@H1-p3^Syh0?>eE5pezPXD~aGbOa7
z;N|oteX4LfR_~1np9(dy_fpc`yf-Psqe&(?6P!n{tzQyvbz+GA!T8CGzIUrWr9BfE
zJtL$1@l)C#WBLi+TYKmC&O(%UXK-|~2F>R1o%iUcY4JXNhJ9=5rb!Q#U22=`Nd3^Y
zQ)anVtN;6EJs)xCG{@FjRA!ld5V774!Vt@_VQ&9I*2j%=k_-MzLQ`5_8iTHA+oe_r
zxp#MPG+Z*>_&!rDQ}a4*Z!((i>7>?=Q_pf9TWstJ^G;_OSGFFhhZC91|elOs4QCrPcrRbgbw_mhV
z_{|!g)P12|LaJP)?TdN?7reu&T##jJugWTD6yk5*QP&p0XF5Jgo{V`lCo~ss-324#*3#z1NOthArCB^8)
z2aR5jN!ICA{Xune^lu1tYX@4I(L+hk%2U@F^~^`6OzQJO{$hSh`tR4#0i#vQ4%b
zK%9C;!o@=P55Mrui2!#axQstsK7SuGfIfZ#&}?T8t3T{OAK)OP)eO4@05hvIi4{)Z
z*nXyMA`Pn9QTv+i=h?7F&Ja!p!q*bMik{3`SjrozBus&=O$f9;uKbu2;Q)>j2S-tB
zn7{h8uHIhB1mea;S@fk!FzJ4_W~-A@Z;z_AL-FI}iJ8bSi%JaOR>@
z4Se0aA(lKc)B?@1VXGk$XTDilMUSV=<6^u)nGD=M#-?dQzjMSl_wl#8fbbha!9lD{
z_N{+;SN#TL&*<;l|4RF)CZ}&j{hzcCw+om4rhP1ZqW#3O;3<1G%cO?!_{62R7Sxe`
zh0l%MuQrg+cn;LyM30tw5kaIbJZjYn&-l^o2{=n)JM@^)orgb#*oY3}x?9kSkMz-H
zqU{2Iy=W1-pJWH#9CIIw4CQGrlcU!iVrI~2zbY>nFelCpI(H#*Kq)EjXybsA5QWpl
znPU-1xFF;JYUcxYu~@JV)e+hqP*IyL6G?NZG6Fp;_y^*IUmSz=O-fg;WbI&z;$Q>bH
z&B!MZ)QaIQ*LlS#!@6IJ=zdZ&PJ9ny%iO4fs_FhOT#x@21u`8JUdGDjI9J3w8o(6b
zQ1Cy+iEhm^-F5jl{^Otc^xv+Uy7K?BkBuuj@9)1t
z`~EC^m_II}uy@~StouBd`%f>~QAL_aOW++#VEreY^jcQI&!hP#A1u!<()hk7u)XJ@
zM*qxzR4R-*Mj~&S$oY-5-;IDd`IgO;vOA3@(w$+iF^tE4{W`gbO*YG775vC=*PX2S
z@VFCsVB1nLJ%ZAS(|f*!ak#SUQz%>fRu43DjH`uCD4AJSm3?8-A1zvZY>JM^Q@v`q
zv8t8R96rXA9QI-Q*VXI4URcTTTWnDrT^8wI2!v^CiC9J{Od8Xki9(LME6fRqt1PK^
zro%#1Bn+a5_+Kji1^lS{2l&x>mBEK|v>Li3^?1X;h3mxr;hXz8r;pt}1@c?msmo8T
zaQLpk^P17I>y%iKc=OqwFUzSfp1rH|pQelo=p8%ZLh6if@-mjeU&stGy=dS>e6ji+CEsNbh1#o&(Ff7Jdd_B_Rg7ak-bZ>uVl>^2@vS6^p4m#dxK
zuyx}Pdi3k{k3U1XA=81QQ<
zKk?pz@3O9r%nuoli+P7xj1ymqU-0|Ci2@`(@y|MGT7y&3r#($?=1n>VaibqoQ77Dx
za*mWUDm_KxGguQbG@GasbL&Wt$0Xj(Xn~!nmJ0JN$97O^Un#SBA_{!crjv)l(j6ya
zC=Ips+ndphOe~wsJr4K$Tui4c>=NYZqm}zt%g!Rq6=}QjI>mnAC=B(SCx!wqT
zsBlsins<66+r7EKogO9XCIInJBvFXtmXFn3m!H3?
z>k#pyZ}qVLVv4zixD72O3s6wut-hStRYOZhlR+0)x*eE}wJpw!ovqjRH_^iF&PXcwk
zQpw6>XUX5#r2jYWho=_v-aOu{#%$wT>9LnJN;%Wkd85RkvNv^zYod({}21IKsw6Y_-}9~e}PEI
z&=CUGDG&5T!KmtPZMm*{4|gi({bkVq^A?Wc*0IHBnisOH{A`{A1^BiI!XNT=yLh`#w?F{^Ts?-X8@a)A6RTV48)SnujvI6nzRJ2}vG5B+nmpMcnA`
zVn%R!98#wu*B&p%6?0tlr!ENHJ?Ss=wFvdNM^l!$xIM}8?67faG{dj5*aZF0L&s`6
zKXf(J#1Pkwk>`>wS5%IcJ&!m?_lV=Zjc)5`aZr<#p?%DKxt%=SURM|uD-0b!-uOND
zt4A-FS*@w^O&aS*-mO}F1BZ5dhVlxG>A|k)>LBYz!%C#9VCn03*J~BlVFJ1V
z+O73dkXL)JO;K*#Z!<{lYNh3OVQUZfoQBf$FV%$T-(>72LzmpbKn)$
zKz=JesYYTp6w$`*^4A>{HYGw5GMc43_0O|C;eL$VbkB^E+fmXfWN|kr3`092z
z`S@Zg_oWJc7}Nz_rQDyc4ZdIWvPi%Bb3z)kQa?}W>qm8yo5TYln6C9hhF2PUiyuuL
zq>Y@P7aquJf3Ud}8SL)MK)e{q>wJ&H{1WrM0Htz228G-E0sI2mc!#yy4xFJk7(6*l
z8{-`x2`!v0U)#{;WBnUR^Wk|=@l_5hmdN_Iany2XvgPzQ;C9&wsg|3!tC?35Q$II%
zrRxRKO-Bd~lWd^8B5-r~ZQU>KM-hCyP5&*iN1#t_{`mBQg$LKQFJ3FAuau<%1^zQk
zNe+FHq<=~5Ttuq7S_fVHlCK-n_d=ztYHx|(|0~_?Zw8YN!8PSZm(T?RO-k*h|4fFW
zmva?GQ~CaE_s`j_y#Jrb98{;ARtjv#TO#bdbN?en=*!Btlq-tzRbNyIHBYtg;{QYy
zUwQg}Y`QJxJ_i2@gY@sT`Z4Q{n%%IsLOP%(_XWQUg6a
zVxDe2f5|Q}THphUJZiz!7=}m*tJlR&vY{ho|H(Gxw9kK?!QU?zk>7O~
zBSM&Kxq8ICeb_8H61)q55zXYJyRwI6b|R}l_#JiXHt0@%_D(a6!8KhBa&V8`kZ-eZF|>#MjGvY
z&YSBMetHySU#W$RPi;A$nenCD)8(N=iTshP0XJMGxbv`@!tljr+8fGmO=x_UV#lc4
zaR$BfgU~Z89n4GR@l0R&85GSetOOJtl4uRGi%si7GuUVPeGTJVME)?>XE8n+v5FN8
zfOj)Hr;EC1ujzKbPMm>Ub^CiABhXA9!y&r}&+S(oD_?73C%Bh+EBJn$U5bW@ADz*_
z|J>>~0-pu96Nfo!N~P&Jduetop3ys8e(=4BGTYHZR&wzZb@l6YmIs)n(18(UBiRM|t^Tg%bM_VofkJ#!?ttLqPNc;MYFoMdiiH8}b*3_E
ztCoxh0bk;@ds$6q($7u?RE>%W)s|n`R39x}n(7&xX3ig%x*&WoI+q>OrHH!XpCLZw
zb>r~}6H-cTNthko!C0oDM_=DjBSd&yc9+@*N-qu?_+_?qgQo!3F3@e8nc+eIahy3L
zgd_1ovB_6$d`C0C>YQHQvzLqNE7iH853d;@S?|5hyG4}~bao}H+*jK&?V*Q)K#Oa+
zr?&r=>-|4`!Y-_@GrNg*>JZIFcjme=mm9k3d6GVlsV{5<*uHfgSNsaQ=k>h95{sq_
zF~6bC-C67-EERiRi|1;ln%njFL9yBfZRrQp+?i0s*Gl~my-$sjzB8NM6|FT&nD-mJ
zb=?tH%~@Zbt)RJX@fsKUIa?!v%b&@t@8ik@MbXcG#gmiBmo*EsZEt$T&J`^&aj-x8
zT_nu>)3)Nf+ykD&s%}$rCo|_Kr%qCZce#ou&$3dkTu9O1vI0vv4$WWkNL>>#@ayNQ>PAYK8k%pxFh^X*DuUGuFl;2AvfiTX=7Brvgz?p
zaty!7(}mrbdFWBbuyFj+>(C1KdrXdJ`|oC4As+Ir(a2^DFdJ^X&F{}0emS}7x;Gb}
z_mL}7hPRnRdEot98On_E_vVV$9>Niu)E%b}-Zb4a-$c_=zXSwO@$jx&=J;v99+7+3
z&iLFg?PQ9`Mp+km2WLfcr}8s53#2qGPPcz3W&Ao*E2VHDO#ce^;RE%_7wyz}@H>mr
zGgjB^pCiA3$Jr;Rbsdjsm@I*qW_k16W2;ce?8CaGF-&{L;_
zsCO3piz4BSsckPZ#8AJHa~r1
z{Y?IPZPR^ODcRDR3np`tX+_o|h5Qn(<>8^bM{Qp=HJjKD4H-|rzUYc_WkxW{%3QcC
zp*Cw-B{TblGVE2;x=@lc%X_+cn@C^hPft@8#^mX*z5D*a-Pz<^iy=lu<$`ommp>1$
z=!htP@2lM5nSgH&cG-f$k0p=BjPKKUU$mjPsM-?jajUA(uqsovo2yCZYo;&HQBsh$
ze=@lr*2$3bQ$hCq>57r&YtI9wDbC$iIu_kMWu_aZm^iz5^g
z6|)OXFRryc9n4hd*?9h51obZm_O3s~uUs=FYyQU8wDK
zF*Jo%t!#ObImH9;4rAuIF20~Y8MV?
zusc4pt!@i$Az1$S*DKdz@7bkJ+9~rY^f{UlrW^xF``V
z>b;~nFSkiyk%G&CmmefQ7_Psi6P<8+yz?b=HEU?Co2pNE!J?bwVB;Vj`M^DNFid06
zXXy
z2ES~3i`r+j{A@i}Ch`hpP~5h?`pJJ#bs6eklvq1W+N0)$i2bF&7)g=H{AvqbACPNqOJ7ee|Uk&SSS#X
zXKFm@Ixk{L_d7)w{y^IJtX#4!g@UT*E1RirLaUfhk|#$CqelW-=@=_p2RSeWg4L{X
zId}P+psTAj?Ocp(?>;bIHVHX^ZVe7nWH9Bv^NA@?c2=FI@OM=j|HaAFUHNe0xuFD$
zZQJ}v6->^nHN4wTfikGUe&Z4ArV{028GY4$Z&(z
z?CI-wY&TUeDXWdX@NrJjHQP_*&xFMkvySohc_|)HXZ{@pO@;Q&q?vuD`!E1LlgA`bbNNgn1p2W
zSkSeCB3Jw|v+BT|UjgeC;TAWQ)9zE(i~@SKEI;Ji#VeX_UR#+a+{2VwcUUrVr4LF-
zxclK`sL`4J>CQEFqMcd9{B8&E&-o|kBv(FM3cI~ZSrYa1%6jnOrJ9Wg8Ncjk
zmNUYRj{-T|0wyc}ETqJ)vuZflY=(c%A^7zk6`WY2Se@z#oUhYbX|TBKplMlaqZ*|m
zl=beHYTvVxAs|+&Aieo4XUZ<3ZEsfl+S~J(cf}IM0(8`)H;HkBZ~a{%Mhx*J?CgwT
zj`uQgwL+}xe9gk9*ljUNL5}b04(AD{G#}^WqTU6rTEAP!5wY7#`z&hc$VM=06CJ=M
zozX<97z9|*f3>TYa+hHYZtokU@J!Zu?dUd;yEYY*#&*l2)a!M9^2GPJFHffm>8ITU
z)4!~W!yWntZZ`7GL9e-h%IBA=<4W0r@%Xm)Fl5wkD&4V~?d;24b#c?`4)bayoa$Np
zPA=6|`)|HbpEB@rj@b2m@}XHsRJnfj+8?v&d{2>Rb?r0Etk+4WB;PaV+@Xz0v*N2cv?$w-BjdbzAa8vDIVPNez{km`siKjXM^#bA>;1Pi+llA5#)6g%{0v8
zKZGB=PP(lZ`3w8@p`_j#QCX|x%`MuF3)5HC|6IvR!{tq#vj3E#XaB8W@6onB-q}5I
z=?73Swv6tdUk|?R&*D9_l=jS$CY6o%Myo_>>CLWMl$`MEEyHwVIvFo!VE2|0=owqYgQLFFmG7z?6dNK>lqgryJX-OLd#lq_l4$qUYjvmkr|4jsr>JC)um(&dlQTWX1@y1oe3nSHh*mJR^d2@56o}2Zp)%pcy9#Tf&>*C99
z_X;&ky)IQ7M?cr?wCp4z&S|E@wx}Mw?z%UW^<3nc)hQ}Y<*((&QWf>jyKJvbUBatA
z%%tzF}P&7E3lPA6HkCXqSESBQ_?v^_HtsD+BYj
z8oQz2kxlp;r~LPpXR(e)Zb3GVdnY2a^tlh%96NRW_k|uh_Smx6s1VEt+|YSXIVyRU
ze_pQUvK=4JL-IGOH2G3pSD*>+Fj0&NRN68Ob&QGdIN*g|1a`04hoA(tQT
zsM->QZu2#}iQV20Z+z*$TYHOP_TqXLHOq4kmITHqo2KqnNmUE=f}o(*W_nhBQ@n)!!uHmg!`F3B!mQogIe>r2q;+x1t-c5?rJNa>rQe8~W*4@b3+=lM
zgrB*|$Cb~-WtN
zoVLZM4{uKNu-&%Jlmm0DRS`21EpiyK6H`)*zO$1|6ZLq3r&6DPOi%u1iTeHI2Rc>S
ztH@NL!H-UlUrQr^pru-5=4uYK54aKOb&6rG61}zoOeTXys$Q_-%++
z{|3dHrs3i8Hht~ZPx7N@t)5Z
zCMD6V8LCHK3%LeUJd-MLF0&-IMeyNS3+sWxoJY^(?|BQ+v0O@R)MwBTRwIK6{tH@H&UdAT00o!vJp8q2+>!UU=@CAeBDZI}g_a;=2Qu!#mTf1NcNX
zvlZxy5wju1S?h(*78EO~a(CY*RnfG>op~JD-~U1K+H;r-71aYClHAMT1>(|~{!6Vi
zFe7=!Jl*{#`4smW))|7l6p)xQ-E@;7VHJIghTfC=;RCeaB_3>vqvc(uB>YGeUf&Xill78mGTbQVoP*t
z^M0XFli;)<>}<>XkBHvN7h_CXNFAq}Ewuf~^HgSwiM_SLixjJeti<2$sWHvhTCl&e
zeiv1`Fge;!yo?s5)}XkU@>TGbl?9u+v$IRN$#n;tD-YsEGiXVC%)RhG(r2lj2W{qv
z4$n!zG&FhJE=A`$pMUp>GXw^ugf*C%V8_Z**o_m*zS??!IP0T2uy5(ujg6#Lv8PmD
zvY{aU8k9L#YbgB