From 0bdcbdae0a35e70e672ff2bb8be618b581a3ad97 Mon Sep 17 00:00:00 2001 From: Demetris Roumis Date: Tue, 25 Jun 2024 17:31:03 -0700 Subject: [PATCH 1/3] conditionally invert axis to align annotations with elements --- holonote/annotate/annotator.py | 14 +++++++++----- holonote/annotate/display.py | 6 ++++-- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/holonote/annotate/annotator.py b/holonote/annotate/annotator.py index f81fd90..687fb4e 100644 --- a/holonote/annotate/annotator.py +++ b/holonote/annotate/annotator.py @@ -328,17 +328,21 @@ def _infer_kdim_dtypes(self, element: hv.Element) -> dict: return AnnotationDisplay._infer_kdim_dtypes(element) def _create_annotation_element(self, element_key: tuple[str, ...]) -> AnnotationDisplay: + # Invert axis if first kdim is None, ensuring overlaying annotations align with underlying elements + invert_axis = element_key[0] is None for key in element_key: - if key not in self.spec: + if key is not None and key not in self.spec: msg = f"Dimension {key!r} not in spec" raise ValueError(msg) - return AnnotationDisplay(self, kdims=list(element_key)) + return AnnotationDisplay( + self, kdims=[e for e in element_key if e is not None], invert_axis=invert_axis + ) def get_element(self, *kdims: str | hv.Dimension) -> hv.DynamicMap: return self.get_display(*kdims).element def get_display(self, *kdims: str | hv.Dimension) -> AnnotationDisplay: - element_key = tuple(map(str, kdims)) + element_key = tuple(str(x) if x is not None else None for x in kdims) if element_key not in self._displays: self._displays[element_key] = self._create_annotation_element(element_key) return self._displays[element_key] @@ -351,8 +355,8 @@ def _get_kdims_from_other_element(self, other): kdims = other.kdims if not kdims or kdims == ["Element"]: kdims = next(k for el in other.values() if (k := el.kdims)) - kdims = [kdim for kdim in kdims if kdim.name in self.spec] - if kdims: + kdims = [kdim if kdim.name in self.spec else None for kdim in kdims] + if any(kdims): return kdims else: msg = "No valid kdims found in element" diff --git a/holonote/annotate/display.py b/holonote/annotate/display.py index ebaa904..8f53fa7 100644 --- a/holonote/annotate/display.py +++ b/holonote/annotate/display.py @@ -208,7 +208,7 @@ def ranges_1d( """ vdims = [*fields_labels, "__selected__"] ds = hv.Dataset(data, kdims=region_labels, vdims=vdims) - element = ds.to(hv.VSpans, groupby=groupby) + element = ds.to(hv.HSpans if invert_axes else hv.VSpans, groupby=groupby) hover = cls._build_hover_tool(data) return element.opts(tools=[hover]) @@ -236,6 +236,8 @@ class AnnotationDisplay(param.Parameterized): data = param.DataFrame(doc="Combined dataframe of annotation data", constant=True) + invert_axis = param.Boolean(default=False, doc="Switch the annotation axis") + _count = param.Integer(default=0, precedence=-1) def __init__(self, annotator: Annotator, **params) -> None: @@ -489,7 +491,7 @@ def static_indicators(self, **events): "data": self.data, "region_labels": region_labels, "fields_labels": fields_labels, - "invert_axes": False, # Not yet handled + "invert_axes": self.invert_axis, "groupby": self.annotator.groupby, } From 2eadade24daf47ef8897bbc6dc0cfe65dd0f4eee Mon Sep 17 00:00:00 2001 From: Demetris Roumis Date: Tue, 25 Jun 2024 17:31:42 -0700 Subject: [PATCH 2/3] add tests --- holonote/tests/test_annotators_element.py | 76 +++++++++++++++++++---- holonote/tests/util.py | 25 ++++++++ 2 files changed, 90 insertions(+), 11 deletions(-) diff --git a/holonote/tests/test_annotators_element.py b/holonote/tests/test_annotators_element.py index 6ebe802..cf62ed6 100644 --- a/holonote/tests/test_annotators_element.py +++ b/holonote/tests/test_annotators_element.py @@ -1,20 +1,16 @@ from __future__ import annotations import holoviews as hv +import numpy as np import pytest from holoviews.operation.datashader import rasterize -from holonote.tests.util import get_editor, get_indicator - - -def get_editor_data(annotator, element_type, kdims=None): - el = get_editor(annotator, element_type, kdims) - return getattr(el, "data", None) - - -def get_indicator_data(annotator, element_type, kdims=None): - for el in get_indicator(annotator, element_type, kdims): - yield el.data +from holonote.tests.util import ( + get_display_data_from_plot, + get_editor_data, + get_indicator, + get_indicator_data, +) def test_set_regions_range1d(annotator_range1d) -> None: @@ -137,6 +133,64 @@ def test_set_regions_multiple(multiple_annotators): assert output2 == expected2 +def test_single_shared_axis_hspan(annotator_range2d): + annotator = annotator_range2d + bounds = (-1, -1, 1, 1) + data = np.array([[0, 1], [1, 0]]) + img = hv.Image(data, kdims=["x", "y"], bounds=bounds) + img_right = hv.Image(data, kdims=["z", "y"], bounds=bounds) # y as second kdim! so HSpan + + left_plot = annotator * img + right_plot = annotator * img_right + layout = left_plot + right_plot + hv.render(layout) + + annotator.set_regions(x=(-0.15, 0.15), y=(-0.25, 0.25)) + annotator.add_annotation(description="Test") + + left_display_data = get_display_data_from_plot(left_plot, hv.Rectangles, ["x", "y"]) + right_display_data = get_display_data_from_plot(right_plot, hv.HSpans, ["y"]) + + expected_left = [-0.15, -0.25, 0.15, 0.25] + assert ( + left_display_data == expected_left + ), f"Expected {expected_left}, but got {left_display_data}" + + expected_right = [-0.25, 0.25] + assert ( + right_display_data == expected_right + ), f"Expected {expected_right}, but got {right_display_data}" + + +def test_single_shared_axis_vspan(annotator_range2d): + annotator = annotator_range2d + bounds = (-1, -1, 1, 1) + data = np.array([[0, 1], [1, 0]]) + img = hv.Image(data, kdims=["x", "y"], bounds=bounds) + img_right = hv.Image(data, kdims=["y", "z"], bounds=bounds) # y as first kdim! so VSpan + + left_plot = annotator * img + right_plot = annotator * img_right + layout = left_plot + right_plot + hv.render(layout) + + annotator.set_regions(x=(-0.15, 0.15), y=(-0.25, 0.25)) + annotator.add_annotation(description="Test") + + left_display_data = get_display_data_from_plot(left_plot, hv.Rectangles, ["x", "y"]) + right_display_data = get_display_data_from_plot(right_plot, hv.VSpans, ["y"]) + + expected_left = [-0.15, -0.25, 0.15, 0.25] + assert ( + left_display_data == expected_left + ), f"Expected {expected_left}, but got {left_display_data}" + + expected_right = [-0.25, 0.25] + assert ( + right_display_data == expected_right + ), f"Expected {expected_right}, but got {right_display_data}" + + def test_editable_enabled(annotator_range1d): annotator_range1d.get_display("TIME") assert annotator_range1d._displays diff --git a/holonote/tests/util.py b/holonote/tests/util.py index 860feb5..ef61368 100644 --- a/holonote/tests/util.py +++ b/holonote/tests/util.py @@ -19,6 +19,31 @@ def get_editor(annotator, element_type, kdims=None): return e +def get_editor_data(annotator, element_type, kdims=None): + el = get_editor(annotator, element_type, kdims) + return getattr(el, "data", None) + + def get_indicator(annotator, element_type, kdims=None): si = _get_display(annotator, kdims).indicators().last yield from si.data.values() + + +def get_indicator_data(annotator, element_type, kdims=None): + for el in get_indicator(annotator, element_type, kdims): + yield el.data + + +def get_display_data_from_plot(plot, element_type, kdims): + display_data = None + + for el in plot.traverse(): + if isinstance(el, element_type): + display_data = getattr(el, "data", None) + break + + if display_data is not None: + cols = [f"start[{dim}]" for dim in kdims] + [f"end[{dim}]" for dim in kdims] + display_data = display_data.iloc[0][cols].to_list() + + return display_data From 2dddc30a6f13d2c5443b26c73e1c0aa6460fb43d Mon Sep 17 00:00:00 2001 From: Demetris Roumis Date: Wed, 26 Jun 2024 07:06:14 -0700 Subject: [PATCH 3/3] add remove comments --- holonote/annotate/display.py | 2 +- holonote/tests/test_annotators_element.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/holonote/annotate/display.py b/holonote/annotate/display.py index 8f53fa7..d707d86 100644 --- a/holonote/annotate/display.py +++ b/holonote/annotate/display.py @@ -491,7 +491,7 @@ def static_indicators(self, **events): "data": self.data, "region_labels": region_labels, "fields_labels": fields_labels, - "invert_axes": self.invert_axis, + "invert_axes": self.invert_axis, # Only handled for range1D "groupby": self.annotator.groupby, } diff --git a/holonote/tests/test_annotators_element.py b/holonote/tests/test_annotators_element.py index 3abc900..e30f022 100644 --- a/holonote/tests/test_annotators_element.py +++ b/holonote/tests/test_annotators_element.py @@ -141,7 +141,7 @@ def test_single_shared_axis_hspan(annotator_range2d): bounds = (-1, -1, 1, 1) data = np.array([[0, 1], [1, 0]]) img = hv.Image(data, kdims=["x", "y"], bounds=bounds) - img_right = hv.Image(data, kdims=["z", "y"], bounds=bounds) # y as second kdim! so HSpan + img_right = hv.Image(data, kdims=["z", "y"], bounds=bounds) left_plot = annotator * img right_plot = annotator * img_right @@ -170,7 +170,7 @@ def test_single_shared_axis_vspan(annotator_range2d): bounds = (-1, -1, 1, 1) data = np.array([[0, 1], [1, 0]]) img = hv.Image(data, kdims=["x", "y"], bounds=bounds) - img_right = hv.Image(data, kdims=["y", "z"], bounds=bounds) # y as first kdim! so VSpan + img_right = hv.Image(data, kdims=["y", "z"], bounds=bounds) left_plot = annotator * img right_plot = annotator * img_right