Skip to content

Commit

Permalink
Merge branch 'main' into handle_empty
Browse files Browse the repository at this point in the history
  • Loading branch information
droumis authored Jul 2, 2024
2 parents 5fcdf26 + 96c5997 commit 8a44fc0
Show file tree
Hide file tree
Showing 5 changed files with 148 additions and 33 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/build.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,6 @@ jobs:
- name: Publish to PyPI
uses: pypa/gh-action-pypi-publish@release/v1
with:
user: ${{ secrets.PPU }}
password: ${{ secrets.PPP }}
user: "__token__"
password: ${{ secrets.pypi_password }} # not on pyviz yet
repository-url: "https://upload.pypi.org/legacy/"
2 changes: 1 addition & 1 deletion .github/workflows/jupyterlite.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ jobs:
- uses: holoviz-dev/holoviz_tasks/pixi_install@pixi
with:
environments: lite
- name: Build documentation
- name: Build
run: pixi run -e lite lite-build
- uses: actions/upload-artifact@v4
if: always()
Expand Down
38 changes: 34 additions & 4 deletions holonote/annotate/display.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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, ...]:
Expand All @@ -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:
Expand Down Expand Up @@ -177,8 +181,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)
Expand Down Expand Up @@ -236,6 +238,17 @@ class AnnotationDisplay(param.Parameterized):

data = param.DataFrame(doc="Combined dataframe of annotation data", constant=True)

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. This parameter is experimental and is subject to change.
""",
)

invert_axis = param.Boolean(default=False, doc="Switch the annotation axis")

_count = param.Integer(default=0, precedence=-1)
Expand Down Expand Up @@ -425,13 +438,27 @@ 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 self.region_format == "point-point":
xk, yk = list(inputs.keys())
xdist = (df[f"point[{xk}]"] - inputs[xk]) ** 2
ydist = (df[f"point[{yk}]"] - inputs[yk]) ** 2
distance_squared = xdist + ydist
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:
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)

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
Expand Down Expand Up @@ -473,6 +500,7 @@ def overlay(self, indicators=True, editor=True) -> hv.Overlay:
active_tools += ["box_select"]
elif self.region_format == "point-point":
active_tools += ["tap"]

layers.append(self._element.opts(tools=self.edit_tools, active_tools=active_tools))

if indicators:
Expand All @@ -483,7 +511,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)

Expand Down
69 changes: 43 additions & 26 deletions holonote/app/panel.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,6 @@ def _create_visible_widget(self):
position: absolute;
border-radius: 50%;
left: calc(100% - var(--design-unit, 4) * 2px - 3px);
top: 20%;
border: 1px solid black;
opacity: 0.5;
}"""
Expand Down Expand Up @@ -225,51 +224,67 @@ def _callback_apply(self, event):
elif self._widget_mode_group.value == "-" and selected_ind is not None:
self.annotator.delete_annotation(selected_ind)

def _get_layout(self, name):
def _add_layout(self, name):
"""
Add a layout to the panel, by cloning the root layout, linking the close button,
and returning it visibly.
"""

def close_layout(event):
layout.visible = False

layout = self._layouts.get(name)
if not layout:
layout = self._layout.clone(visible=False)
layout = self._layout.clone(visible=True)
self._widget_apply_button.on_click(close_layout)
self._layouts[name] = layout
return layout

def _hide_layouts(self):
for layout in self._layouts.values():
layout.visible = False
def _hide_layouts_except(self, desired_name) -> pn.Column:
"""
Prevents multiple layouts from being visible at the same time.
"""
desired_layout = None
for name, layout in self._layouts.items():
if name == desired_name:
layout.visible = True
desired_layout = layout
elif name != "__panel__":
layout.visible = False

# If the desired layout is not found, create it
if desired_name is not None and desired_layout is None:
desired_layout = self._add_layout(desired_name)
return desired_layout

def _register_stream_popup(self, stream):
def _popup(*args, **kwargs):
layout = self._get_layout(stream.name)
with param.parameterized.batch_call_watchers(self):
self._hide_layouts()
self._widget_mode_group.value = "+"
layout.visible = True
return layout
# If the annotation widgets are laid out on the side in a Column/Row/etc,
# while as_popup=True, do not show the popup during subtract or edit mode
widgets_on_side = any(name == "__panel__" for name in self._layouts)
if widgets_on_side and self._widget_mode_group.value in ("-", "✏"):
return
self._widget_mode_group.value = "+"
return self._hide_layouts_except(stream.name)

stream.popup = _popup

def _register_tap_popup(self, display):
def tap_popup(x, y) -> None: # Tap tool must be enabled on the element
layout = self._get_layout("tap")
if self.annotator.selection_enabled:
with param.parameterized.batch_call_watchers(self):
self._hide_layouts()
layout.visible = True
return layout
if self.annotator.selected_indices:
return self._hide_layouts_except("tap")

display._tap_stream.popup = tap_popup

def _register_double_tap_clear(self, display):
def double_tap_toggle(x, y):
layout = self._get_layout("doubletap")
if layout.visible:
with param.parameterized.batch_call_watchers(self):
self._hide_layouts()
layout.visible = True
return layout
# Toggle the visibility of the doubletap layout
if any(layout.visible for layout in self._layouts.values()):
# Clear all open layouts
self._hide_layouts_except(None)
else:
# Open specifically the doubletap layout
return self._hide_layouts_except("doubletap")

try:
tools = display._element.opts["tools"]
Expand All @@ -285,7 +300,6 @@ def _watcher_selected_indices(self, event):
if len(event.new) != 1:
return
selected_index = event.new[0]
# if self._widget_mode_group.value == '✏':
for name, widget in self._fields_widgets.items():
value = self.annotator.annotation_table._field_df.loc[selected_index][name]
widget.value = value
Expand All @@ -294,6 +308,7 @@ def _watcher_mode_group(self, event):
with param.parameterized.batch_call_watchers(self):
if event.new in ("-", "✏"):
self.annotator.selection_enabled = True
self.annotator.editable_enabled = False
elif event.new == "+":
self.annotator.editable_enabled = True
self.annotator.selection_enabled = False
Expand All @@ -309,4 +324,6 @@ def _set_standard_callbacks(self):
self._widget_mode_group.param.watch(self._watcher_mode_group, "value")

def __panel__(self):
return self._layout.clone(visible=True)
layout = self._layout.clone(visible=True)
self._layouts["__panel__"] = layout
return layout
68 changes: 68 additions & 0 deletions holonote/tests/test_display.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
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
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)
display = annotator_point2d.get_display("x", "y")
indices = display.get_indices_by_position(x=x + 1.5, y=y + 1.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_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

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

0 comments on commit 8a44fc0

Please sign in to comment.