From 56a22c1f8fb14b9a01dc30cd0fc35925949b3e9c Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Mon, 17 Jun 2024 00:22:39 -0400 Subject: [PATCH 1/9] bugfix --- fastplotlib/graphics/line_collection.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fastplotlib/graphics/line_collection.py b/fastplotlib/graphics/line_collection.py index 92aad56b2..5a15fa47f 100644 --- a/fastplotlib/graphics/line_collection.py +++ b/fastplotlib/graphics/line_collection.py @@ -81,7 +81,7 @@ def cmap(self, args): if isinstance(args, str): name = args transform, alpha = None, 1.0 - if len(args) == 1: + elif len(args) == 1: name = args[0] transform, alpha = None, None From 39ce0af2974abb6c23097a093e7b60f7280e7f2a Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Mon, 17 Jun 2024 00:23:01 -0400 Subject: [PATCH 2/9] center works, need to figure out what's wrong with edge --- fastplotlib/layouts/_plot_area.py | 71 +++++++++++++++++++++++++++++++ 1 file changed, 71 insertions(+) diff --git a/fastplotlib/layouts/_plot_area.py b/fastplotlib/layouts/_plot_area.py index d8e0adebc..75f66d5ef 100644 --- a/fastplotlib/layouts/_plot_area.py +++ b/fastplotlib/layouts/_plot_area.py @@ -357,6 +357,77 @@ def map_screen_to_world( # default z is zero for now return np.array([*pos_world[:2], 0]) + def get_nearest_graphics( + self, + pos: tuple[float, float] | tuple[float, float, float], + method: str = "center", + subset: np.ndarray[Graphic] = None, + ) -> np.ndarray[Graphic]: + """ + Returns the nearest ``n_graphics`` to the passed position ``pos`` in world space + + Parameters + ---------- + pos: (x, y) | (x, y, z) + position in world space, not data space + + method: "center" | "edge" + Determine the nearest objects using the "center" of graphics or the "edge". + Note that the "edge" method uses an axis-aligned bounding box and therefore + maybe be inaccurate if the Graphic is not axis-aligned, ex. if it is rotated + + subset: np.ndarray[Graphic], optional + + + Returns + ------- + tuple[Graphic] + nearest graphics to ``pos`` in order + + """ + + if subset is not None: + if not isinstance(subset, np.ndarray): + raise TypeError("`subset` must be a numpy array of Graphic objects") + + if not all(isinstance(g, Graphic) for g in subset): + raise TypeError("all elements of `subset` must be Graphic objects") + + graphics = subset + + else: + graphics = self.graphics + + pos = np.asarray(pos) + + if pos.shape != (2,) or not pos.shape != (3,): + raise TypeError + + match method: + case "center": + # use sphere + centers = np.empty(shape=(len(graphics), len(pos))) + for i in range(centers.shape[0]): + centers[i] = graphics[i].world_object.get_world_bounding_sphere()[:len(pos)] + + distances = np.linalg.norm(centers[:, :len(pos)] - pos, ord=2, axis=1) + + case "edge": + # use bbox + distances = np.empty(shape=(len(graphics),)) + for i in range(distances.size): + bbox = graphics[i].world_object.get_world_bounding_box() + + distances[i] = np.linalg.norm( + np.abs(bbox[:, :len(pos)] - pos).min(axis=0) + ) + + case _: + raise ValueError("method must be one of 'center' or 'edge'") + + sort_indices = np.argsort(distances) + return np.asarray(graphics)[sort_indices] + def set_viewport_rect(self, *args): self.viewport.rect = self.get_rect() From d1cc1caaa41d176884328af120cec92a89a0c963 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Mon, 17 Jun 2024 00:36:48 -0400 Subject: [PATCH 3/9] allow passing graphic collection to subset --- fastplotlib/layouts/_plot_area.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/fastplotlib/layouts/_plot_area.py b/fastplotlib/layouts/_plot_area.py index 75f66d5ef..f59d83ac0 100644 --- a/fastplotlib/layouts/_plot_area.py +++ b/fastplotlib/layouts/_plot_area.py @@ -361,7 +361,7 @@ def get_nearest_graphics( self, pos: tuple[float, float] | tuple[float, float, float], method: str = "center", - subset: np.ndarray[Graphic] = None, + subset: GraphicCollection | np.ndarray[Graphic] = None, ) -> np.ndarray[Graphic]: """ Returns the nearest ``n_graphics`` to the passed position ``pos`` in world space @@ -387,6 +387,9 @@ def get_nearest_graphics( """ if subset is not None: + if isinstance(subset, GraphicCollection): + subset = subset.graphics + if not isinstance(subset, np.ndarray): raise TypeError("`subset` must be a numpy array of Graphic objects") From fee3d8dfd5c818c30f8df470185472e192a74267 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Mon, 17 Jun 2024 00:36:48 -0400 Subject: [PATCH 4/9] allow passing graphic collection to subset --- fastplotlib/layouts/_plot_area.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/fastplotlib/layouts/_plot_area.py b/fastplotlib/layouts/_plot_area.py index 75f66d5ef..f59d83ac0 100644 --- a/fastplotlib/layouts/_plot_area.py +++ b/fastplotlib/layouts/_plot_area.py @@ -361,7 +361,7 @@ def get_nearest_graphics( self, pos: tuple[float, float] | tuple[float, float, float], method: str = "center", - subset: np.ndarray[Graphic] = None, + subset: GraphicCollection | np.ndarray[Graphic] = None, ) -> np.ndarray[Graphic]: """ Returns the nearest ``n_graphics`` to the passed position ``pos`` in world space @@ -387,6 +387,9 @@ def get_nearest_graphics( """ if subset is not None: + if isinstance(subset, GraphicCollection): + subset = subset.graphics + if not isinstance(subset, np.ndarray): raise TypeError("`subset` must be a numpy array of Graphic objects") From 5db9d3e2bff8ffa13c82862e32ef68e5d2cf3292 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Mon, 17 Jun 2024 23:44:05 -0400 Subject: [PATCH 5/9] move get_nearest_graphics to utils --- fastplotlib/layouts/_plot_area.py | 74 ------------------------------ fastplotlib/utils/__init__.py | 2 +- fastplotlib/utils/_plot_helpers.py | 53 +++++++++++++++++++++ 3 files changed, 54 insertions(+), 75 deletions(-) create mode 100644 fastplotlib/utils/_plot_helpers.py diff --git a/fastplotlib/layouts/_plot_area.py b/fastplotlib/layouts/_plot_area.py index f59d83ac0..d8e0adebc 100644 --- a/fastplotlib/layouts/_plot_area.py +++ b/fastplotlib/layouts/_plot_area.py @@ -357,80 +357,6 @@ def map_screen_to_world( # default z is zero for now return np.array([*pos_world[:2], 0]) - def get_nearest_graphics( - self, - pos: tuple[float, float] | tuple[float, float, float], - method: str = "center", - subset: GraphicCollection | np.ndarray[Graphic] = None, - ) -> np.ndarray[Graphic]: - """ - Returns the nearest ``n_graphics`` to the passed position ``pos`` in world space - - Parameters - ---------- - pos: (x, y) | (x, y, z) - position in world space, not data space - - method: "center" | "edge" - Determine the nearest objects using the "center" of graphics or the "edge". - Note that the "edge" method uses an axis-aligned bounding box and therefore - maybe be inaccurate if the Graphic is not axis-aligned, ex. if it is rotated - - subset: np.ndarray[Graphic], optional - - - Returns - ------- - tuple[Graphic] - nearest graphics to ``pos`` in order - - """ - - if subset is not None: - if isinstance(subset, GraphicCollection): - subset = subset.graphics - - if not isinstance(subset, np.ndarray): - raise TypeError("`subset` must be a numpy array of Graphic objects") - - if not all(isinstance(g, Graphic) for g in subset): - raise TypeError("all elements of `subset` must be Graphic objects") - - graphics = subset - - else: - graphics = self.graphics - - pos = np.asarray(pos) - - if pos.shape != (2,) or not pos.shape != (3,): - raise TypeError - - match method: - case "center": - # use sphere - centers = np.empty(shape=(len(graphics), len(pos))) - for i in range(centers.shape[0]): - centers[i] = graphics[i].world_object.get_world_bounding_sphere()[:len(pos)] - - distances = np.linalg.norm(centers[:, :len(pos)] - pos, ord=2, axis=1) - - case "edge": - # use bbox - distances = np.empty(shape=(len(graphics),)) - for i in range(distances.size): - bbox = graphics[i].world_object.get_world_bounding_box() - - distances[i] = np.linalg.norm( - np.abs(bbox[:, :len(pos)] - pos).min(axis=0) - ) - - case _: - raise ValueError("method must be one of 'center' or 'edge'") - - sort_indices = np.argsort(distances) - return np.asarray(graphics)[sort_indices] - def set_viewport_rect(self, *args): self.viewport.rect = self.get_rect() diff --git a/fastplotlib/utils/__init__.py b/fastplotlib/utils/__init__.py index e1eef6447..fc6887f37 100644 --- a/fastplotlib/utils/__init__.py +++ b/fastplotlib/utils/__init__.py @@ -3,7 +3,7 @@ from .functions import * from .gpu import enumerate_adapters, select_adapter, print_wgpu_report - +from ._plot_helpers import * @dataclass class _Config: diff --git a/fastplotlib/utils/_plot_helpers.py b/fastplotlib/utils/_plot_helpers.py new file mode 100644 index 000000000..7db94cded --- /dev/null +++ b/fastplotlib/utils/_plot_helpers.py @@ -0,0 +1,53 @@ +from typing import Sequence + +import numpy as np + +from ..graphics._base import Graphic +from ..graphics._collection_base import GraphicCollection + + +def get_nearest_graphics( + graphics: Sequence[Graphic] | GraphicCollection, + pos: tuple[float, float] | tuple[float, float, float], +) -> np.ndarray[Graphic]: + """ + Returns the nearest ``n_graphics`` to the passed position ``pos`` in world space. + Uses the distance between ``pos`` and the center of the bounding sphere for each graphic. + + Parameters + ---------- + graphics: Sequence, i.e. array, list, tuple, etc. of Graphic | GraphicCollection + the graphics from which to return a sorted array of graphics in order of closest + to furthest graphic + + pos: (x, y) | (x, y, z) + position in world space, z-axis is ignored when calculating L2 norms if ``pos`` is 2D + + Returns + ------- + tuple[Graphic] + nearest graphics to ``pos`` in order + + """ + + if isinstance(graphics, GraphicCollection): + graphics = graphics.graphics + + if not all(isinstance(g, Graphic) for g in graphics): + raise TypeError("all elements of `graphics` must be Graphic objects") + + pos = np.asarray(pos) + + if pos.shape != (2,) or not pos.shape != (3,): + raise TypeError + + # get centers + centers = np.empty(shape=(len(graphics), len(pos))) + for i in range(centers.shape[0]): + centers[i] = graphics[i].world_object.get_world_bounding_sphere()[:len(pos)] + + # l2 + distances = np.linalg.norm(centers[:, :len(pos)] - pos, ord=2, axis=1) + + sort_indices = np.argsort(distances) + return np.asarray(graphics)[sort_indices] From 5aef21d89bde1cdb374f191d18a5a69d65050cf7 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Tue, 18 Jun 2024 00:05:14 -0400 Subject: [PATCH 6/9] move stuff --- fastplotlib/utils/_plot_helpers.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/fastplotlib/utils/_plot_helpers.py b/fastplotlib/utils/_plot_helpers.py index 7db94cded..7bc2069df 100644 --- a/fastplotlib/utils/_plot_helpers.py +++ b/fastplotlib/utils/_plot_helpers.py @@ -7,22 +7,22 @@ def get_nearest_graphics( - graphics: Sequence[Graphic] | GraphicCollection, pos: tuple[float, float] | tuple[float, float, float], + graphics: Sequence[Graphic] | GraphicCollection, ) -> np.ndarray[Graphic]: """ - Returns the nearest ``n_graphics`` to the passed position ``pos`` in world space. + Returns the nearest ``graphics`` to the passed position ``pos`` in world space. Uses the distance between ``pos`` and the center of the bounding sphere for each graphic. Parameters ---------- + pos: (x, y) | (x, y, z) + position in world space, z-axis is ignored when calculating L2 norms if ``pos`` is 2D + graphics: Sequence, i.e. array, list, tuple, etc. of Graphic | GraphicCollection the graphics from which to return a sorted array of graphics in order of closest to furthest graphic - pos: (x, y) | (x, y, z) - position in world space, z-axis is ignored when calculating L2 norms if ``pos`` is 2D - Returns ------- tuple[Graphic] From 440d9f889116fe640eff92faabc1673105339fa7 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Tue, 18 Jun 2024 00:05:27 -0400 Subject: [PATCH 7/9] test for get nearest --- tests/test_plot_helpers.py | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 tests/test_plot_helpers.py diff --git a/tests/test_plot_helpers.py b/tests/test_plot_helpers.py new file mode 100644 index 000000000..7a328cce6 --- /dev/null +++ b/tests/test_plot_helpers.py @@ -0,0 +1,38 @@ +import numpy as np +import fastplotlib as fpl + + +def make_circle(center, radius: float, n_points: int = 75) -> np.ndarray: + theta = np.linspace(0, 2 * np.pi, n_points) + xs = radius * np.sin(theta) + ys = radius * np.cos(theta) + + return np.column_stack([xs, ys]) + center + + +def test_get_nearest_graphics(): + circles = list() + + centers = [ + [0, 0], + [0, 20], + [20, 0], + [20, 20] + ] + + for center in centers: + circles.append(make_circle(center, 5, n_points=75)) + + fig = fpl.Figure() + + lines = fig[0, 0].add_line_collection(circles, cmap="jet", thickness=5) + + fig[0, 0].add_scatter(np.array([[0, 12, 0]])) + + # check distances + nearest = fpl.utils.get_nearest_graphics((0, 12), lines) + assert nearest[0] is lines[1] # closest + assert nearest[1] is lines[0] + assert nearest[2] is lines[3] + assert nearest[3] is lines[2] # furthest + assert nearest[-1] is lines[2] From 7b41a1eda1a91cec332e93093a1b096903ae1179 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Tue, 18 Jun 2024 00:12:00 -0400 Subject: [PATCH 8/9] black --- fastplotlib/utils/_plot_helpers.py | 8 ++++---- tests/test_plot_helpers.py | 7 +------ 2 files changed, 5 insertions(+), 10 deletions(-) diff --git a/fastplotlib/utils/_plot_helpers.py b/fastplotlib/utils/_plot_helpers.py index 7bc2069df..ac0ff2cda 100644 --- a/fastplotlib/utils/_plot_helpers.py +++ b/fastplotlib/utils/_plot_helpers.py @@ -7,8 +7,8 @@ def get_nearest_graphics( - pos: tuple[float, float] | tuple[float, float, float], - graphics: Sequence[Graphic] | GraphicCollection, + pos: tuple[float, float] | tuple[float, float, float], + graphics: Sequence[Graphic] | GraphicCollection, ) -> np.ndarray[Graphic]: """ Returns the nearest ``graphics`` to the passed position ``pos`` in world space. @@ -44,10 +44,10 @@ def get_nearest_graphics( # get centers centers = np.empty(shape=(len(graphics), len(pos))) for i in range(centers.shape[0]): - centers[i] = graphics[i].world_object.get_world_bounding_sphere()[:len(pos)] + centers[i] = graphics[i].world_object.get_world_bounding_sphere()[: len(pos)] # l2 - distances = np.linalg.norm(centers[:, :len(pos)] - pos, ord=2, axis=1) + distances = np.linalg.norm(centers[:, : len(pos)] - pos, ord=2, axis=1) sort_indices = np.argsort(distances) return np.asarray(graphics)[sort_indices] diff --git a/tests/test_plot_helpers.py b/tests/test_plot_helpers.py index 7a328cce6..b4abe55fc 100644 --- a/tests/test_plot_helpers.py +++ b/tests/test_plot_helpers.py @@ -13,12 +13,7 @@ def make_circle(center, radius: float, n_points: int = 75) -> np.ndarray: def test_get_nearest_graphics(): circles = list() - centers = [ - [0, 0], - [0, 20], - [20, 0], - [20, 20] - ] + centers = [[0, 0], [0, 20], [20, 0], [20, 20]] for center in centers: circles.append(make_circle(center, 5, n_points=75)) From 196972b4241ed48d092448363bc808f4343914a6 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Tue, 18 Jun 2024 00:17:39 -0400 Subject: [PATCH 9/9] more black --- fastplotlib/utils/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/fastplotlib/utils/__init__.py b/fastplotlib/utils/__init__.py index fc6887f37..3ae83fb6b 100644 --- a/fastplotlib/utils/__init__.py +++ b/fastplotlib/utils/__init__.py @@ -5,6 +5,7 @@ from .gpu import enumerate_adapters, select_adapter, print_wgpu_report from ._plot_helpers import * + @dataclass class _Config: party_parrot: bool