Skip to content
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

Conditionally invert axis to align annotations with elements #113

Merged
merged 4 commits into from
Jun 26, 2024
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
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
14 changes: 9 additions & 5 deletions holonote/annotate/annotator.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand All @@ -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"
Expand Down
6 changes: 4 additions & 2 deletions holonote/annotate/display.py
Original file line number Diff line number Diff line change
Expand Up @@ -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])

Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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,
droumis marked this conversation as resolved.
Show resolved Hide resolved
"groupby": self.annotator.groupby,
}

Expand Down
76 changes: 65 additions & 11 deletions holonote/tests/test_annotators_element.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,23 +3,19 @@
from importlib.util import find_spec

import holoviews as hv
import numpy as np
import pytest

from holonote.tests.util import get_editor, get_indicator
from holonote.tests.util import (
get_display_data_from_plot,
get_editor_data,
get_indicator,
get_indicator_data,
)

datashader = find_spec("datashader")


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


def test_set_regions_range1d(annotator_range1d) -> None:
annotator = annotator_range1d
annotator.get_display("TIME")
Expand Down Expand Up @@ -140,6 +136,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}"
droumis marked this conversation as resolved.
Show resolved Hide resolved


def test_editable_enabled(annotator_range1d):
annotator_range1d.get_display("TIME")
assert annotator_range1d._displays
Expand Down
25 changes: 25 additions & 0 deletions holonote/tests/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -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