Skip to content
Merged
Show file tree
Hide file tree
Changes from 23 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions docs/source/api/graphic_features/RectangleSelectionFeature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
.. _api.RectangleSelectionFeature:

RectangleSelectionFeature
*************************

=========================
RectangleSelectionFeature
=========================
.. currentmodule:: fastplotlib.graphics._features

Constructor
~~~~~~~~~~~
.. autosummary::
:toctree: RectangleSelectionFeature_api

RectangleSelectionFeature

Properties
~~~~~~~~~~
.. autosummary::
:toctree: RectangleSelectionFeature_api

RectangleSelectionFeature.value

Methods
~~~~~~~
.. autosummary::
:toctree: RectangleSelectionFeature_api

RectangleSelectionFeature.add_event_handler
RectangleSelectionFeature.block_events
RectangleSelectionFeature.clear_event_handlers
RectangleSelectionFeature.remove_event_handler
RectangleSelectionFeature.set_value

1 change: 1 addition & 0 deletions docs/source/api/graphic_features/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ Graphic Features
TextOutlineThickness
LinearSelectionFeature
LinearRegionSelectionFeature
RectangleSelectionFeature
Name
Offset
Rotation
Expand Down
1 change: 1 addition & 0 deletions docs/source/api/graphics/ImageGraphic.rst
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ Methods
ImageGraphic.add_event_handler
ImageGraphic.add_linear_region_selector
ImageGraphic.add_linear_selector
ImageGraphic.add_rectangle_selector
ImageGraphic.clear_event_handlers
ImageGraphic.remove_event_handler
ImageGraphic.reset_vmin_vmax
Expand Down
1 change: 1 addition & 0 deletions docs/source/api/graphics/LineCollection.rst
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ Methods
LineCollection.add_graphic
LineCollection.add_linear_region_selector
LineCollection.add_linear_selector
LineCollection.add_rectangle_selector
LineCollection.clear_event_handlers
LineCollection.remove_event_handler
LineCollection.remove_graphic
Expand Down
1 change: 1 addition & 0 deletions docs/source/api/graphics/LineGraphic.rst
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ Methods
LineGraphic.add_event_handler
LineGraphic.add_linear_region_selector
LineGraphic.add_linear_selector
LineGraphic.add_rectangle_selector
LineGraphic.clear_event_handlers
LineGraphic.remove_event_handler
LineGraphic.rotate
Expand Down
1 change: 1 addition & 0 deletions docs/source/api/graphics/LineStack.rst
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ Methods
LineStack.add_graphic
LineStack.add_linear_region_selector
LineStack.add_linear_selector
LineStack.add_rectangle_selector
LineStack.clear_event_handlers
LineStack.remove_event_handler
LineStack.remove_graphic
Expand Down
53 changes: 53 additions & 0 deletions docs/source/api/selectors/RectangleSelector.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
.. _api.RectangleSelector:

RectangleSelector
*****************

=================
RectangleSelector
=================
.. currentmodule:: fastplotlib

Constructor
~~~~~~~~~~~
.. autosummary::
:toctree: RectangleSelector_api

RectangleSelector

Properties
~~~~~~~~~~
.. autosummary::
:toctree: RectangleSelector_api

RectangleSelector.axes
RectangleSelector.axis
RectangleSelector.block_events
RectangleSelector.deleted
RectangleSelector.event_handlers
RectangleSelector.limits
RectangleSelector.name
RectangleSelector.offset
RectangleSelector.parent
RectangleSelector.rotation
RectangleSelector.selection
RectangleSelector.supported_events
RectangleSelector.visible
RectangleSelector.world_object

Methods
~~~~~~~
.. autosummary::
:toctree: RectangleSelector_api

RectangleSelector.add_axes
RectangleSelector.add_event_handler
RectangleSelector.clear_event_handlers
RectangleSelector.get_selected_data
RectangleSelector.get_selected_index
RectangleSelector.get_selected_indices
RectangleSelector.remove_event_handler
RectangleSelector.rotate
RectangleSelector.share_property
RectangleSelector.unshare_property

1 change: 1 addition & 0 deletions docs/source/api/selectors/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@ Selectors

LinearSelector
LinearRegionSelector
RectangleSelector
2 changes: 1 addition & 1 deletion docs/source/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@
intersphinx_mapping = {
"python": ("https://docs.python.org/3", None),
"numpy": ("https://numpy.org/doc/stable/", None),
"pygfx": ("https://pygfx.org/stable", None),
"pygfx": ("https://docs.pygfx.org/stable/", None),
"wgpu": ("https://wgpu-py.readthedocs.io/en/latest", None),
"fastplotlib": ("https://fastplotlib.readthedocs.io/en/latest/", None),
}
Expand Down
66 changes: 66 additions & 0 deletions examples/desktop/selectors/rectangle_selector.py
Comment thread
clewis7 marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
"""
Rectangle Selectors
===================

Example showing how to use a `RectangleSelector` with lines, line collections, and images
"""

# test_example = false
# sphinx_gallery_pygfx_docs = 'screenshot'

import numpy as np
import fastplotlib as fpl
from itertools import product

# create a figure
figure = fpl.Figure(
size=(700, 560)
)


# generate some data
def make_circle(center, radius: float, n_points: int = 75) -> np.ndarray:
theta = np.linspace(0, 2 * np.pi, n_points)
xs = radius * np.sin(theta)
ys = radius * np.cos(theta)

return np.column_stack([xs, ys]) + center


spatial_dims = (50, 50)

circles = list()
for center in product(range(0, spatial_dims[0], 9), range(0, spatial_dims[1], 9)):
circles.append(make_circle(center, 3, n_points=75))

pos_xy = np.vstack(circles)

# add image
line_collection = figure[0, 0].add_line_collection(circles, cmap="jet", thickness=5)

# add rectangle selector to image graphic
rectangle_selector = line_collection.add_rectangle_selector()


# add event handler to highlight selected indices
@rectangle_selector.add_event_handler("selection")
def color_indices(ev):
line_collection.cmap = "jet"
ixs = ev.get_selected_indices()

# iterate through each of the selected indices, if the array size > 0 that mean it's under the selection
selected_line_ixs = [i for i in range(len(ixs)) if ixs[i].size > 0]
line_collection[selected_line_ixs].colors = "w"


# manually move selector to make a nice gallery image :D
rectangle_selector.selection = (15, 30, 15, 30)


figure.show()

# NOTE: `if __name__ == "__main__"` is NOT how to use fastplotlib interactively
# please see our docs for using fastplotlib interactively in ipython and jupyter
if __name__ == "__main__":
print(__doc__)
fpl.run()
7 changes: 6 additions & 1 deletion fastplotlib/graphics/_features/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,11 @@
TextOutlineThickness,
)

from ._selection_features import LinearSelectionFeature, LinearRegionSelectionFeature
from ._selection_features import (
LinearSelectionFeature,
LinearRegionSelectionFeature,
RectangleSelectionFeature,
)
from ._common import Name, Offset, Rotation, Visible, Deleted


Expand All @@ -56,6 +60,7 @@
"TextOutlineThickness",
"LinearSelectionFeature",
"LinearRegionSelectionFeature",
"RectangleSelectionFeature",
"Name",
"Offset",
"Rotation",
Expand Down
152 changes: 151 additions & 1 deletion fastplotlib/graphics/_features/_selection_features.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from typing import Sequence
from typing import Sequence, Tuple

import numpy as np

Expand Down Expand Up @@ -190,3 +190,153 @@ def set_value(self, selector, value: Sequence[float]):
# TODO: user's selector event handlers can call event.graphic.get_selected_indices() to get the data index,
# and event.graphic.get_selected_data() to get the data under the selection
# this is probably a good idea so that the data isn't sliced until it's actually necessary


class RectangleSelectionFeature(GraphicFeature):
"""
**additional event attributes:**

+----------------------+----------+------------------------------------+
| attribute | type | description |
+======================+==========+====================================+
| get_selected_indices | callable | returns indices under the selector |
+----------------------+----------+------------------------------------+
| get_selected_data | callable | returns data under the selector |
+----------------------+----------+------------------------------------+

**info dict:**

+----------+------------+-------------------------------------------+
| dict key | value type | value description |
+==========+============+===========================================+
| value | np.ndarray | new [xmin, xmax, ymin, ymax] of selection |
+----------+------------+-------------------------------------------+

"""

def __init__(
self,
value: tuple[float, float, float, float],
limits: tuple[float, float, float, float],
):
super().__init__()

self._limits = limits
self._value = tuple(int(v) for v in value)

@property
def value(self) -> np.ndarray[float]:
"""
(xmin, xmax, ymin, ymax) of the selection, in data space
"""
return self._value

def set_value(self, selector, value: Sequence[float]):
"""
Set the selection of the rectangle selector.

Parameters
----------
selector: RectangleSelector

value: (float, float, float, float)
new values (xmin, xmax, ymin, ymax) of the selection
"""
if not len(value) == 4:
raise TypeError(
"Selection must be an array, tuple, list, or sequence in the form of `(xmin, xmax, ymin, ymax)`, "
"where `xmin`, `xmax`, `ymin`, `ymax` are numeric values."
)

# convert to array
value = np.asarray(value, dtype=np.float32)

# clip values if they are beyond the limits
value[:2] = value[:2].clip(self._limits[0], self._limits[1])
# clip y
value[2:] = value[2:].clip(self._limits[2], self._limits[3])

xmin, xmax, ymin, ymax = value

# make sure `selector width >= 2` and selector height >=2 , left edge must not move past right edge!
# or bottom edge must not move past top edge!
if not (xmax - xmin) >= 0 or not (ymax - ymin) >= 0:
return

# change fill mesh
# change left x position of the fill mesh
selector.fill.geometry.positions.data[mesh_masks.x_left] = xmin

# change right x position of the fill mesh
selector.fill.geometry.positions.data[mesh_masks.x_right] = xmax

# change bottom y position of the fill mesh
selector.fill.geometry.positions.data[mesh_masks.y_bottom] = ymin

# change top position of the fill mesh
selector.fill.geometry.positions.data[mesh_masks.y_top] = ymax

# change the edge lines

# each edge line is defined by two end points which are stored in the
# geometry.positions
# [x0, y0, z0]
# [x1, y1, z0]

# left line
z = selector.edges[0].geometry.positions.data[:, -1][0]
selector.edges[0].geometry.positions.data[:] = np.array(
[[xmin, ymin, z], [xmin, ymax, z]]
)

# right line
selector.edges[1].geometry.positions.data[:] = np.array(
[[xmax, ymin, z], [xmax, ymax, z]]
)

# bottom line
selector.edges[2].geometry.positions.data[:] = np.array(
[[xmin, ymin, z], [xmax, ymin, z]]
)

# top line
selector.edges[3].geometry.positions.data[:] = np.array(
[[xmin, ymax, z], [xmax, ymax, z]]
)

# change the vertex positions

# bottom left
selector.vertices[0].geometry.positions.data[:] = np.array([[xmin, ymin, 1]])

# bottom right
selector.vertices[1].geometry.positions.data[:] = np.array([[xmax, ymin, 1]])

# top left
selector.vertices[2].geometry.positions.data[:] = np.array([[xmin, ymax, 1]])

# top right
selector.vertices[3].geometry.positions.data[:] = np.array([[xmax, ymax, 1]])

self._value = value

# send changes to GPU
selector.fill.geometry.positions.update_range()

for edge in selector.edges:
edge.geometry.positions.update_range()

for vertex in selector.vertices:
vertex.geometry.positions.update_range()

# send event
if len(self._event_handlers) < 1:
return

event = FeatureEvent("selection", {"value": self.value})

event.get_selected_indices = selector.get_selected_indices
event.get_selected_data = selector.get_selected_data

# calls any events
self._call_event_handlers(event)
Loading