-
Notifications
You must be signed in to change notification settings - Fork 52
LinearRegionSelector #164
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
LinearRegionSelector #164
Changes from 4 commits
Commits
Show all changes
25 commits
Select commit
Hold shift + click to select a range
f045c7e
linear selector, basic functionality
kushalkolar 6c5bc77
edges move with fill
kushalkolar 39c6150
linear selector width can change by dragging edge lines
kushalkolar 81c8563
docstring
kushalkolar 885a082
comments
kushalkolar 1ab25e6
highlight linear selector edges on mouse hover
kushalkolar ae11b62
add get_selected_data()
kushalkolar 9e91966
can add selector from linegraphic, organization
kushalkolar a71f01e
allow using get_selected_data() from another graphic
kushalkolar 971f5e8
update docstring
kushalkolar 5628814
linearselector compensates for graphic worldobject position, rename h…
kushalkolar 485dd15
fill and edges properly track mouse with movements
kushalkolar 062a63a
del print
kushalkolar 146a30e
line selector works on y axis too
kushalkolar 495a3b6
remove prints
kushalkolar 2ef80dd
fix position checks for y
kushalkolar 9abe206
attempt at gc for LinearSelector, will do later
kushalkolar beaf442
add get_selected_indices()
kushalkolar 5b7bcbb
properly compensate for graphics with position offset from 0, 0
kushalkolar 6d35691
linear selector works for line collections and line stack
kushalkolar e078fc5
add linestack separation to size
kushalkolar 033c4b1
add linear selector example nb
kushalkolar 7f9b9b9
add selected_data and selected_indices to event pick info
kushalkolar a679493
rename to LinearRegionSelector
kushalkolar f9cb8d1
update example nb
kushalkolar File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Empty file.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,246 @@ | ||
from typing import * | ||
import numpy as np | ||
from functools import partial | ||
|
||
import pygfx | ||
from pygfx.linalg import Vector3 | ||
|
||
from .._base import Graphic, Interaction | ||
from ..features._base import GraphicFeature, FeatureEvent | ||
|
||
|
||
# positions for indexing the BoxGeometry to set the "width" and "height" of the box | ||
# hacky but I don't think we can morph meshes in pygfx yet: https://github.com/pygfx/pygfx/issues/346 | ||
x_right = np.array([ | ||
True, True, True, True, False, False, False, False, False, | ||
True, False, True, True, False, True, False, False, True, | ||
False, True, True, False, True, False | ||
]) | ||
|
||
x_left = np.array([ | ||
False, False, False, False, True, True, True, True, True, | ||
False, True, False, False, True, False, True, True, False, | ||
True, False, False, True, False, True | ||
]) | ||
|
||
y_top = np.array([ | ||
False, True, False, True, False, True, False, True, True, | ||
True, True, True, False, False, False, False, False, False, | ||
True, True, False, False, True, True | ||
]) | ||
|
||
y_bottom = np.array([ | ||
True, False, True, False, True, False, True, False, False, | ||
False, False, False, True, True, True, True, True, True, | ||
False, False, True, True, False, False | ||
]) | ||
|
||
|
||
class LinearBoundsFeature(GraphicFeature): | ||
def __init__(self, parent, bounds: Tuple[int, int]): | ||
super(LinearBoundsFeature, self).__init__(parent, data=bounds) | ||
|
||
def _set(self, value): | ||
# sets new bounds | ||
if not isinstance(value, tuple): | ||
raise TypeError( | ||
"Bounds must be a tuple in the form of `(min_bound, max_bound)`, " | ||
"where `min_bound` and `max_bound` are numeric values." | ||
) | ||
|
||
self._parent.fill.geometry.positions.data[x_left, 0] = value[0] | ||
self._parent.fill.geometry.positions.data[x_right, 0] = value[1] | ||
|
||
self._parent.edges[0].geometry.positions.data[:, 0] = value[0] | ||
self._parent.edges[1].geometry.positions.data[:, 0] = value[1] | ||
|
||
self._data = (value[0], value[1]) | ||
|
||
self._parent.fill.geometry.positions.update_range() | ||
|
||
self._parent.edges[0].geometry.positions.update_range() | ||
self._parent.edges[1].geometry.positions.update_range() | ||
|
||
self._feature_changed(key=None, new_data=value) | ||
|
||
def _feature_changed(self, key: Union[int, slice, Tuple[slice]], new_data: Any): | ||
pick_info = { | ||
"index": None, | ||
"collection-index": self._collection_index, | ||
"world_object": self._parent.world_object, | ||
"new_data": new_data | ||
} | ||
|
||
event_data = FeatureEvent(type="bounds", pick_info=pick_info) | ||
|
||
self._call_event_handlers(event_data) | ||
kushalkolar marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
|
||
class LinearSelector(Graphic, Interaction): | ||
"""Linear region selector, for lines or line collections.""" | ||
feature_events = ( | ||
"bounds" | ||
) | ||
|
||
def __init__( | ||
self, | ||
bounds: Tuple[int, int], | ||
limits: Tuple[int, int], | ||
height: int, | ||
position: Tuple[int, int], | ||
resizable: bool = False, | ||
fill_color=(0, 0, 0.35), | ||
edge_color=(0.8, 0.8, 0), | ||
name: str = None | ||
): | ||
""" | ||
Create a LinearSelector graphic which can be moved only along the x-axis. Useful for sub-selecting | ||
data Line graphics or Heatmap graphics. | ||
|
||
bounds[0], limits[0], and position[0] must be identical | ||
|
||
Parameters | ||
---------- | ||
bounds: (int, int) | ||
the initial bounds of the linear selector | ||
|
||
kushalkolar marked this conversation as resolved.
Show resolved
Hide resolved
|
||
limits: (int, int) | ||
(min limit, max limit) for the selector | ||
|
||
height: int | ||
height of the selector | ||
|
||
position: (int, int) | ||
initial position of the selector | ||
|
||
fill_color: str, array, or tuple | ||
fill color for the selector, passed to pygfx.Color | ||
|
||
edge_color: str, array, or tuple | ||
edge color for the selector, passed to pygfx.Color | ||
|
||
name: str | ||
name for this selector graphic | ||
""" | ||
|
||
if limits[0] != position[0] != bounds[0]: | ||
raise ValueError("limits[0] != position[0] != bounds[0]") | ||
|
||
super(LinearSelector, self).__init__(name=name) | ||
|
||
group = pygfx.Group() | ||
self._set_world_object(group) | ||
|
||
self.fill = pygfx.Mesh( | ||
pygfx.box_geometry(1, height, 1), | ||
pygfx.MeshBasicMaterial(color=pygfx.Color(fill_color)) | ||
) | ||
|
||
self.fill.position.set(*position, -2) | ||
|
||
self.world_object.add(self.fill) | ||
|
||
self._move_info = None | ||
self._event_source: str = None | ||
|
||
self.limits = limits | ||
self._resizable = resizable | ||
|
||
left_line_data = np.array( | ||
[[position[0], (-height / 2) + position[1], 0.5], | ||
[position[0], (height / 2) + position[1], 0.5]] | ||
).astype(np.float32) | ||
|
||
left_line = pygfx.Line( | ||
pygfx.Geometry(positions=left_line_data, colors=np.repeat([pygfx.Color(edge_color)], 2, axis=0)), | ||
pygfx.LineMaterial(thickness=5, vertex_colors=True) | ||
) | ||
|
||
right_line_data = np.array( | ||
[[bounds[1], (-height / 2) + position[1], 0.5], | ||
[bounds[1], (height / 2) + position[1], 0.5]] | ||
).astype(np.float32) | ||
|
||
right_line = pygfx.Line( | ||
pygfx.Geometry(positions=right_line_data, colors=np.repeat([pygfx.Color(edge_color)], 2, axis=0)), | ||
pygfx.LineMaterial(thickness=5, vertex_colors=True) | ||
) | ||
|
||
self.world_object.add(left_line) | ||
self.world_object.add(right_line) | ||
|
||
self.edges: Tuple[pygfx.Line, pygfx.Line] = (left_line, right_line) | ||
|
||
self.bounds = LinearBoundsFeature(self, bounds) | ||
self.bounds = bounds | ||
|
||
def _add_plot_area_hook(self, plot_area): | ||
# called when this selector is added to a plot area | ||
self._plot_area = plot_area | ||
|
||
move_start_fill = partial(self._move_start, "fill") | ||
move_start_edge_left = partial(self._move_start, "edge-left") | ||
move_start_edge_right = partial(self._move_start, "edge-right") | ||
|
||
self.fill.add_event_handler(move_start_fill, "pointer_down") | ||
|
||
if self._resizable: | ||
self.edges[0].add_event_handler(move_start_edge_left, "pointer_down") | ||
self.edges[1].add_event_handler(move_start_edge_right, "pointer_down") | ||
|
||
self._plot_area.renderer.add_event_handler(self._move, "pointer_move") | ||
self._plot_area.renderer.add_event_handler(self._move_end, "pointer_up") | ||
|
||
def _move_start(self, event_source: str, ev): | ||
self._plot_area.controller.enabled = False | ||
self._move_info = {"last_pos": (ev.x, ev.y)} | ||
self._event_source = event_source | ||
|
||
def _move(self, ev): | ||
if self._move_info is None: | ||
return | ||
|
||
self._plot_area.controller.enabled = False | ||
|
||
last = self._move_info["last_pos"] | ||
|
||
delta = (last[0] - ev.x, last[1] - ev.y) | ||
|
||
self._move_info = {"last_pos": (ev.x, ev.y)} | ||
|
||
if self._event_source == "edge-left": | ||
left_bound = self.bounds()[0] - delta[0] | ||
right_bound = self.bounds()[1] | ||
|
||
elif self._event_source == "edge-right": | ||
left_bound = self.bounds()[0] | ||
right_bound = self.bounds()[1] - delta[0] | ||
|
||
elif self._event_source == "fill": | ||
left_bound = self.bounds()[0] - delta[0] | ||
right_bound = self.bounds()[1] - delta[0] | ||
|
||
# clip based on the limits | ||
if left_bound < self.limits[0] or right_bound > self.limits[1]: | ||
return | ||
|
||
# make sure width > 2 | ||
# has to be at least 2 otherwise can't join datapoints for lines | ||
if (right_bound - left_bound) < 2: | ||
return | ||
|
||
# set the new bounds | ||
self.bounds = (left_bound, right_bound) | ||
|
||
self._plot_area.controller.enabled = True | ||
|
||
def _move_end(self, ev): | ||
self._move_info = None | ||
# sometimes weird stuff happens so we want to make sure the controller is reset | ||
self._plot_area.controller.enabled = True | ||
|
||
def _set_feature(self, feature: str, new_data: Any, indices: Any): | ||
pass | ||
|
||
def _reset_feature(self, feature: str): | ||
pass |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.