From 0c88f57359ecadb561ef0bcbdfe7ccdcafe34e20 Mon Sep 17 00:00:00 2001 From: Andrew Huang Date: Thu, 30 May 2024 11:41:56 -0700 Subject: [PATCH 1/9] add points 2d --- holonote/annotate/display.py | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/holonote/annotate/display.py b/holonote/annotate/display.py index ebaa904..95eef10 100644 --- a/holonote/annotate/display.py +++ b/holonote/annotate/display.py @@ -177,8 +177,6 @@ def points_2d( cls, data, region_labels, fields_labels, invert_axes=False, groupby: str | None = None ): "Vectorizes point regions to VLines * HLines. Note does not support hover info" - msg = "2D point regions not supported yet" - raise NotImplementedError(msg) vdims = [*fields_labels, "__selected__"] element = hv.Points(data, kdims=region_labels, vdims=vdims) hover = cls._build_hover_tool(data) @@ -316,8 +314,8 @@ def _infer_kdim_dtypes(cls, element): def clear_indicated_region(self): "Clear any region currently indicated on the plot by the editor" self._edit_streams[0].event(bounds=None) - self._edit_streams[1].event(x=None, y=None) - self._edit_streams[2].event(geometry=None) + # self._edit_streams[1].event(x=None, y=None) + # self._edit_streams[2].event(geometry=None) self.annotator.clear_regions() def _make_empty_element(self) -> hv.Curve | hv.Image: @@ -423,13 +421,21 @@ def get_indices_by_position(self, **inputs) -> list[Any]: iter_mask = ( (df[f"start[{k}]"] <= v) & (v < df[f"end[{k}]"]) for k, v in inputs.items() ) + subset = reduce(np.logical_and, iter_mask) + out = list(df[subset].index) elif "point" in self.region_format: - iter_mask = ((df[f"point[{k}]"] - v).abs().argmin() for k, v in inputs.items()) + xk, yk = list(inputs.keys()) + distance = ( + (df[f"point[{xk}]"] - inputs[xk]) ** 2 + (df[f"point[{yk}]"] - inputs[yk]) ** 2 + ) ** 0.5 + if (distance > inputs[xk] / 1e2).all(): + return [] + out = [df.loc[distance.idxmin()].name] # index == name of series else: msg = f"{self.region_format} not implemented" raise NotImplementedError(msg) - return list(df[reduce(np.logical_and, iter_mask)].index) + return out def register_tap_selector(self, element: hv.Element) -> hv.Element: def tap_selector(x, y) -> None: # Tap tool must be enabled on the element @@ -481,7 +487,9 @@ def overlay(self, indicators=True, editor=True) -> hv.Overlay: def static_indicators(self, **events): fields_labels = self.annotator.all_fields - region_labels = [k for k in self.data.columns if k not in fields_labels] + region_labels = [ + k for k in self.data.columns if k not in fields_labels and k != "__selected__" + ] self.data["__selected__"] = self.data.index.isin(self.annotator.selected_indices) From bd11ee061655cee07cf314d7d4ec5af8a7ef6ed7 Mon Sep 17 00:00:00 2001 From: Andrew Huang Date: Thu, 30 May 2024 11:46:40 -0700 Subject: [PATCH 2/9] uncomment --- holonote/annotate/display.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/holonote/annotate/display.py b/holonote/annotate/display.py index 95eef10..df9b4f8 100644 --- a/holonote/annotate/display.py +++ b/holonote/annotate/display.py @@ -314,8 +314,8 @@ def _infer_kdim_dtypes(cls, element): def clear_indicated_region(self): "Clear any region currently indicated on the plot by the editor" self._edit_streams[0].event(bounds=None) - # self._edit_streams[1].event(x=None, y=None) - # self._edit_streams[2].event(geometry=None) + self._edit_streams[1].event(x=None, y=None) + self._edit_streams[2].event(geometry=None) self.annotator.clear_regions() def _make_empty_element(self) -> hv.Curve | hv.Image: From 058a43936683199561543ab6b789fe36d92fa2c6 Mon Sep 17 00:00:00 2001 From: Andrew Huang Date: Thu, 27 Jun 2024 09:51:58 -0700 Subject: [PATCH 3/9] address comment --- holonote/annotate/display.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/holonote/annotate/display.py b/holonote/annotate/display.py index df9b4f8..3e044b1 100644 --- a/holonote/annotate/display.py +++ b/holonote/annotate/display.py @@ -234,6 +234,15 @@ class AnnotationDisplay(param.Parameterized): data = param.DataFrame(doc="Combined dataframe of annotation data", constant=True) + nearest_2d_point_threshold = param.Number( + default=0.1, + bounds=(0, None), + doc=""" + Threshold for selecting an existing 2D point; anything over + this threshold will create a new point instead. + """, + ) + _count = param.Integer(default=0, precedence=-1) def __init__(self, annotator: Annotator, **params) -> None: @@ -423,14 +432,17 @@ def get_indices_by_position(self, **inputs) -> list[Any]: ) subset = reduce(np.logical_and, iter_mask) out = list(df[subset].index) - elif "point" in self.region_format: + elif self.region_format == "point-point": xk, yk = list(inputs.keys()) distance = ( (df[f"point[{xk}]"] - inputs[xk]) ** 2 + (df[f"point[{yk}]"] - inputs[yk]) ** 2 ) ** 0.5 - if (distance > inputs[xk] / 1e2).all(): + if (distance > self.nearest_2d_point_threshold).all(): return [] out = [df.loc[distance.idxmin()].name] # index == name of series + elif "point" in self.region_format: + iter_mask = ((df[f"point[{k}]"] - v).abs().argmin() for k, v in inputs.items()) + out = list(df[reduce(np.logical_and, iter_mask)].index) else: msg = f"{self.region_format} not implemented" raise NotImplementedError(msg) From 66a1fabdd3d3f293d144211e4238c16a8807daa3 Mon Sep 17 00:00:00 2001 From: Andrew Huang Date: Thu, 27 Jun 2024 10:17:45 -0700 Subject: [PATCH 4/9] add test --- holonote/tests/test_display.py | 54 ++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 holonote/tests/test_display.py diff --git a/holonote/tests/test_display.py b/holonote/tests/test_display.py new file mode 100644 index 0000000..67e4863 --- /dev/null +++ b/holonote/tests/test_display.py @@ -0,0 +1,54 @@ +import holoviews as hv + +hv.extension("bokeh") + + +class TestPoint2D: + def test_get_indices_by_position_exact(self, annotator_point2d): + x, y = 0.5, 0.3 + description = "A test annotation!" + annotator_point2d.set_regions(x=x, y=y) + annotator_point2d.add_annotation(description=description) + display = annotator_point2d.get_display("x", "y") + indices = display.get_indices_by_position(x=x, y=y) + assert len(indices) == 1 + + def test_get_indices_by_position_nearest_2d_point_threshold(self, annotator_point2d): + x, y = 0.5, 0.3 + description = "A test annotation!" + annotator_point2d.set_regions(x=x, y=y) + annotator_point2d.add_annotation(description=description) + display = annotator_point2d.get_display("x", "y") + indices = display.get_indices_by_position(x=x + 0.5, y=y + 0.5) + assert len(indices) == 0 + + display.nearest_2d_point_threshold = 5 + indices = display.get_indices_by_position(x=x + 0.5, y=y + 0.5) + assert len(indices) == 1 + + def test_get_indices_by_position_empty(self, annotator_point2d): + display = annotator_point2d.get_display("x", "y") + indices = display.get_indices_by_position(x=0.5, y=0.3) + assert len(indices) == 0 + + def test_get_indices_by_position_no_position(self, annotator_point2d): + display = annotator_point2d.get_display("x", "y") + indices = display.get_indices_by_position(x=None, y=None) + assert len(indices) == 0 + + def test_get_indices_by_position_multi_choice(self, annotator_point2d): + x, y = 0.5, 0.3 + description = "A test annotation!" + annotator_point2d.set_regions(x=x, y=y) + annotator_point2d.add_annotation(description=description) + + x2, y2 = 0.51, 0.31 + description = "A test annotation!" + annotator_point2d.set_regions(x=x2, y=y2) + annotator_point2d.add_annotation(description=description) + + display = annotator_point2d.get_display("x", "y") + display.nearest_2d_point_threshold = 1000 + + indices = display.get_indices_by_position(x=x, y=y) + assert len(indices) == 1 From 8352fba1e30ef7b6bb6e4ae5a54b75a941b8f295 Mon Sep 17 00:00:00 2001 From: Andrew Huang Date: Thu, 27 Jun 2024 14:07:04 -0700 Subject: [PATCH 5/9] address comments --- holonote/annotate/display.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/holonote/annotate/display.py b/holonote/annotate/display.py index 1f3b0a2..bd5761f 100644 --- a/holonote/annotate/display.py +++ b/holonote/annotate/display.py @@ -234,12 +234,14 @@ class AnnotationDisplay(param.Parameterized): data = param.DataFrame(doc="Combined dataframe of annotation data", constant=True) - nearest_2d_point_threshold = param.Number( + _nearest_2d_point_threshold = param.Number( default=0.1, bounds=(0, None), doc=""" - Threshold for selecting an existing 2D point; anything over - this threshold will create a new point instead. + Threshold In the distance in data coordinates between the two dimensions; + it does not consider the unit and magnitude differences between the dimensions + for selecting an existing 2D point; anything over this threshold will create + a new point instead. """, ) @@ -436,10 +438,10 @@ def get_indices_by_position(self, **inputs) -> list[Any]: out = list(df[subset].index) elif self.region_format == "point-point": xk, yk = list(inputs.keys()) - distance = ( - (df[f"point[{xk}]"] - inputs[xk]) ** 2 + (df[f"point[{yk}]"] - inputs[yk]) ** 2 - ) ** 0.5 - if (distance > self.nearest_2d_point_threshold).all(): + distance = (df[f"point[{xk}]"] - inputs[xk]) ** 2 + ( + df[f"point[{yk}]"] - inputs[yk] + ) ** 2 + if (distance > self._nearest_2d_point_threshold**2).all(): return [] out = [df.loc[distance.idxmin()].name] # index == name of series elif "point" in self.region_format: From 7d94ee242ed0d42329866e143969a855ec1ac4db Mon Sep 17 00:00:00 2001 From: Andrew Huang Date: Thu, 27 Jun 2024 14:10:04 -0700 Subject: [PATCH 6/9] fix de tests --- holonote/tests/test_display.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/holonote/tests/test_display.py b/holonote/tests/test_display.py index 67e4863..cef4514 100644 --- a/holonote/tests/test_display.py +++ b/holonote/tests/test_display.py @@ -22,7 +22,7 @@ def test_get_indices_by_position_nearest_2d_point_threshold(self, annotator_poin indices = display.get_indices_by_position(x=x + 0.5, y=y + 0.5) assert len(indices) == 0 - display.nearest_2d_point_threshold = 5 + display._nearest_2d_point_threshold = 5 indices = display.get_indices_by_position(x=x + 0.5, y=y + 0.5) assert len(indices) == 1 @@ -48,7 +48,7 @@ def test_get_indices_by_position_multi_choice(self, annotator_point2d): annotator_point2d.add_annotation(description=description) display = annotator_point2d.get_display("x", "y") - display.nearest_2d_point_threshold = 1000 + display._nearest_2d_point_threshold = 1000 indices = display.get_indices_by_position(x=x, y=y) assert len(indices) == 1 From 77df8bbfcbc19c3bdf1f3a59baaf64b4029a2931 Mon Sep 17 00:00:00 2001 From: Andrew Huang Date: Mon, 1 Jul 2024 12:30:26 -0700 Subject: [PATCH 7/9] up threshold and add opts --- holonote/annotate/display.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/holonote/annotate/display.py b/holonote/annotate/display.py index bd5761f..fe4fe4d 100644 --- a/holonote/annotate/display.py +++ b/holonote/annotate/display.py @@ -88,12 +88,14 @@ class Style(param.Parameterized): line_opts = _StyleOpts(default={}) span_opts = _StyleOpts(default={}) rectangle_opts = _StyleOpts(default={}) + points_opts = _StyleOpts(default={}) # Editor opts edit_opts = _StyleOpts(default={"line_color": "black"}) edit_line_opts = _StyleOpts(default={}) edit_span_opts = _StyleOpts(default={}) edit_rectangle_opts = _StyleOpts(default={}) + edit_points_opts = _StyleOpts(default={}) _groupby = () _colormap = None @@ -133,6 +135,7 @@ def indicator(self, **select_opts) -> tuple[hv.Options, ...]: hv.opts.HSpans(**opts, **self.span_opts), hv.opts.VLines(**opts, **self.line_opts), hv.opts.HLines(**opts, **self.line_opts), + hv.opts.Points(**opts, **self.points_opts), ) def editor(self) -> tuple[hv.Options, ...]: @@ -148,6 +151,7 @@ def editor(self) -> tuple[hv.Options, ...]: hv.opts.HSpan(**opts, **self.edit_span_opts), hv.opts.VLine(**opts, **self.edit_line_opts), hv.opts.HLine(**opts, **self.edit_line_opts), + hv.opts.Points(**opts, **self.edit_points_opts), ) def reset(self) -> None: @@ -235,7 +239,7 @@ class AnnotationDisplay(param.Parameterized): data = param.DataFrame(doc="Combined dataframe of annotation data", constant=True) _nearest_2d_point_threshold = param.Number( - default=0.1, + default=1, bounds=(0, None), doc=""" Threshold In the distance in data coordinates between the two dimensions; @@ -438,12 +442,12 @@ def get_indices_by_position(self, **inputs) -> list[Any]: out = list(df[subset].index) elif self.region_format == "point-point": xk, yk = list(inputs.keys()) - distance = (df[f"point[{xk}]"] - inputs[xk]) ** 2 + ( - df[f"point[{yk}]"] - inputs[yk] - ) ** 2 - if (distance > self._nearest_2d_point_threshold**2).all(): + xdist = (df[f"point[{xk}]"] - inputs[xk]) ** 2 + ydist = (df[f"point[{yk}]"] - inputs[yk]) ** 2 + distance_squared = xdist + ydist + if (distance_squared > self._nearest_2d_point_threshold**2).all(): return [] - out = [df.loc[distance.idxmin()].name] # index == name of series + out = [df.loc[distance_squared.idxmin()].name] # index == name of series elif "point" in self.region_format: iter_mask = ((df[f"point[{k}]"] - v).abs().argmin() for k, v in inputs.items()) out = list(df[reduce(np.logical_and, iter_mask)].index) From 6d86fc1440ac704bd38bb6e2fb387eb0377eaa56 Mon Sep 17 00:00:00 2001 From: Andrew Huang Date: Mon, 1 Jul 2024 12:34:58 -0700 Subject: [PATCH 8/9] fix test --- holonote/tests/test_display.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/holonote/tests/test_display.py b/holonote/tests/test_display.py index cef4514..8865cb0 100644 --- a/holonote/tests/test_display.py +++ b/holonote/tests/test_display.py @@ -19,7 +19,7 @@ def test_get_indices_by_position_nearest_2d_point_threshold(self, annotator_poin annotator_point2d.set_regions(x=x, y=y) annotator_point2d.add_annotation(description=description) display = annotator_point2d.get_display("x", "y") - indices = display.get_indices_by_position(x=x + 0.5, y=y + 0.5) + indices = display.get_indices_by_position(x=x + 1.5, y=y + 1.5) assert len(indices) == 0 display._nearest_2d_point_threshold = 5 From 21e6ac5275e14954b23a30406ae452991d723f80 Mon Sep 17 00:00:00 2001 From: Andrew Huang Date: Tue, 2 Jul 2024 07:56:01 -0700 Subject: [PATCH 9/9] make public and tests --- holonote/annotate/display.py | 11 +++++++---- holonote/tests/test_display.py | 18 ++++++++++++++++-- 2 files changed, 23 insertions(+), 6 deletions(-) diff --git a/holonote/annotate/display.py b/holonote/annotate/display.py index fe4fe4d..e2aebea 100644 --- a/holonote/annotate/display.py +++ b/holonote/annotate/display.py @@ -238,14 +238,14 @@ class AnnotationDisplay(param.Parameterized): data = param.DataFrame(doc="Combined dataframe of annotation data", constant=True) - _nearest_2d_point_threshold = param.Number( - default=1, + nearest_2d_point_threshold = param.Number( + default=None, bounds=(0, None), doc=""" Threshold In the distance in data coordinates between the two dimensions; it does not consider the unit and magnitude differences between the dimensions for selecting an existing 2D point; anything over this threshold will create - a new point instead. + a new point instead. This parameter is experimental and is subject to change. """, ) @@ -445,7 +445,10 @@ def get_indices_by_position(self, **inputs) -> list[Any]: xdist = (df[f"point[{xk}]"] - inputs[xk]) ** 2 ydist = (df[f"point[{yk}]"] - inputs[yk]) ** 2 distance_squared = xdist + ydist - if (distance_squared > self._nearest_2d_point_threshold**2).all(): + if ( + self.nearest_2d_point_threshold + and (distance_squared > self.nearest_2d_point_threshold**2).all() + ): return [] out = [df.loc[distance_squared.idxmin()].name] # index == name of series elif "point" in self.region_format: diff --git a/holonote/tests/test_display.py b/holonote/tests/test_display.py index 8865cb0..3f28b05 100644 --- a/holonote/tests/test_display.py +++ b/holonote/tests/test_display.py @@ -15,6 +15,7 @@ def test_get_indices_by_position_exact(self, annotator_point2d): def test_get_indices_by_position_nearest_2d_point_threshold(self, annotator_point2d): x, y = 0.5, 0.3 + annotator_point2d.get_display("x", "y").nearest_2d_point_threshold = 1 description = "A test annotation!" annotator_point2d.set_regions(x=x, y=y) annotator_point2d.add_annotation(description=description) @@ -22,7 +23,20 @@ def test_get_indices_by_position_nearest_2d_point_threshold(self, annotator_poin indices = display.get_indices_by_position(x=x + 1.5, y=y + 1.5) assert len(indices) == 0 - display._nearest_2d_point_threshold = 5 + display.nearest_2d_point_threshold = 5 + indices = display.get_indices_by_position(x=x + 0.5, y=y + 0.5) + assert len(indices) == 1 + + def test_get_indices_by_position_nearest(self, annotator_point2d): + x, y = 0.5, 0.3 + description = "A test annotation!" + annotator_point2d.set_regions(x=x, y=y) + annotator_point2d.add_annotation(description=description) + display = annotator_point2d.get_display("x", "y") + indices = display.get_indices_by_position(x=x + 1.5, y=y + 1.5) + assert len(indices) == 1 + + display.nearest_2d_point_threshold = 5 indices = display.get_indices_by_position(x=x + 0.5, y=y + 0.5) assert len(indices) == 1 @@ -48,7 +62,7 @@ def test_get_indices_by_position_multi_choice(self, annotator_point2d): annotator_point2d.add_annotation(description=description) display = annotator_point2d.get_display("x", "y") - display._nearest_2d_point_threshold = 1000 + display.nearest_2d_point_threshold = 1000 indices = display.get_indices_by_position(x=x, y=y) assert len(indices) == 1