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

Improve PanelWidgets with visible widget and make color deterministic #100

Merged
merged 7 commits into from
Apr 23, 2024
Merged
Show file tree
Hide file tree
Changes from all 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
39 changes: 31 additions & 8 deletions holonote/annotate/display.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,9 +77,7 @@ class Style(param.Parameterized):
default=0.4, bounds=(0, 1), allow_refs=True, doc="Alpha value for editing regions"
)

color = param.Parameter(
default=hv.Cycle(_default_color), doc="Color of the indicator", allow_refs=True
)
color = param.Parameter(default=None, doc="Color of the indicator", allow_refs=True)
edit_color = param.Parameter(default="blue", doc="Color of the editor", allow_refs=True)
selection_color = param.Parameter(
default=None, doc="Color of selection, by the default the same as color", allow_refs=True
Expand All @@ -97,19 +95,38 @@ class Style(param.Parameterized):
edit_span_opts = _StyleOpts(default={})
edit_rectangle_opts = _StyleOpts(default={})

_groupby = ()
_colormap = None

@property
def _color(self):
if self.color is None:
if self._groupby:
# This is the main point of this method
# we use this to be able to memorize the colormap
# so the color are the same no matter what
# the order of the groupby is
dim = hv.dim(self._groupby[0])
self._colormap = dict(zip(self._groupby[1], _default_color))
return dim.categorize(self._colormap)
else:
return _default_color[0]

return self.color

@property
def _indicator_selection(self) -> dict[str, tuple]:
select = {"alpha": (self.selection_alpha, self.alpha)}
if self.selection_color is not None:
if isinstance(self.color, hv.dim):
msg = "'Style.color' cannot be a `hv.dim` when 'Style.selection_color' is not None"
if isinstance(self._color, hv.dim):
msg = "'Style.color' cannot be a `hv.dim` / `None` when 'Style.selection_color' is not None"
raise ValueError(msg)
else:
select["color"] = (self.selection_color, self.color)
select["color"] = (self.selection_color, self._color)
return select

def indicator(self, **select_opts) -> tuple[hv.Options, ...]:
opts = {**_default_opts, "color": self.color, **select_opts, **self.opts}
opts = {**_default_opts, "color": self._color, **select_opts, **self.opts}
return (
hv.opts.Rectangles(**opts, **self.rectangle_opts),
hv.opts.VSpans(**opts, **self.span_opts),
Expand All @@ -134,6 +151,7 @@ def editor(self) -> tuple[hv.Options, ...]:
)

def reset(self) -> None:
self._colormap = None
params = self.param.objects().items()
self.param.update(**{k: v.default for k, v in params if k != "name"})

Expand Down Expand Up @@ -253,6 +271,11 @@ def _set_region_format(self) -> None:
def _update_data(self):
with param.edit_constant(self):
self.data = self.annotator.get_dataframe(dims=self.kdims)
if self.annotator.groupby:
self.annotator.style._groupby = (
self.annotator.groupby,
sorted(self.data[self.annotator.groupby].unique()),
)

@property
def element(self):
Expand All @@ -274,7 +297,7 @@ def edit_tools(self) -> list[Tool]:
@classmethod
def _infer_kdim_dtypes(cls, element):
if not isinstance(element, hv.Element):
msg = "Supplied object {element} is not a bare HoloViews Element"
msg = f"Supplied object {element} is not a bare HoloViews Element"
raise ValueError(msg)
kdim_dtypes = {}
for kdim in element.dimensions(selection="key"):
Expand Down
47 changes: 46 additions & 1 deletion holonote/app/panel.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
from packaging.version import Version
from panel.viewable import Viewer

from ..annotate.display import _default_color

if TYPE_CHECKING:
from holonote.annotate import Annotator

Expand Down Expand Up @@ -42,9 +44,50 @@ def __init__(self, annotator: Annotator, field_values: dict[str, Any] | None = N
else:
self._fields_values = {k: field_values.get(k, "") for k in self.annotator.fields}
self._fields_widgets = self._create_fields_widgets(self._fields_values)
self._create_visible_widget()

self._set_standard_callbacks()

def _create_visible_widget(self):
if self.annotator.groupby is None:
self.visible_widget = None
return
style = self.annotator.style
if style.color is None and style._colormap is None:
data = sorted(self.annotator.df[self.annotator.groupby].unique())
colormap = dict(zip(data, _default_color))
else:
colormap = style._colormap
if isinstance(colormap, dict):
stylesheet = """
option:after {
content: "";
width: 10px;
height: 10px;
position: absolute;
border-radius: 50%;
left: calc(100% - var(--design-unit, 4) * 2px - 3px);
top: 20%;
border: 1px solid black;
opacity: 0.5;
}"""
for i, color in enumerate(colormap.values()):
stylesheet += f"""
option:nth-child({i + 1}):after {{
background-color: {color};
}}"""
else:
stylesheet = ""

options = list(colormap)
self.visible_widget = pn.widgets.MultiSelect(
name="Visible",
options=options,
value=self.annotator.visible or options,
stylesheets=[stylesheet],
)
self.annotator.visible = self.visible_widget

def _add_button_description(self):
from bokeh.models import Tooltip
from bokeh.models.dom import HTML
Expand Down Expand Up @@ -181,4 +224,6 @@ def _set_standard_callbacks(self):
self._widget_mode_group.param.watch(self._watcher_mode_group, "value")

def __panel__(self):
return pn.Column(self.fields_widgets, self.tool_widgets)
if self.visible_widget is None:
return pn.Column(self.fields_widgets, self.tool_widgets)
return pn.Column(self.visible_widget, self.fields_widgets, self.tool_widgets)
15 changes: 9 additions & 6 deletions holonote/tests/test_annotators_style.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,19 @@
import pytest

from holonote.annotate import Style
from holonote.annotate.display import _default_color
from holonote.tests.util import get_editor, get_indicator


def compare_indicator_color(indicator, style):
if isinstance(indicator.opts["color"], hv.dim):
if isinstance(style.color, hv.dim):
assert str(indicator.opts["color"]) == str(style.color)
elif style.color is None and style._colormap:
assert dict(zip(style._groupby[1], _default_color)) == style._colormap
else:
style_color = (
style.color.values[0] if isinstance(style.color, hv.Cycle) else style.color
)
expected_dim = hv.dim("__selected__").categorize(
categories={True: style.selection_color}, default=style_color
categories={True: style.selection_color}, default=style.color
)
assert str(indicator.opts["color"]) == str(expected_dim)
else:
Expand Down Expand Up @@ -86,6 +86,9 @@ def test_style_color_dim(cat_annotator):
compare_style(cat_annotator)


@pytest.mark.xfail(
reason="hv.dim is not supported for selection.color in the current implementation"
)
def test_style_selection_color(cat_annotator):
style = cat_annotator.style
style.selection_color = "blue"
Expand Down Expand Up @@ -114,7 +117,7 @@ def test_style_error_color_dim_and_selection(cat_annotator):
categories={"B": "red", "A": "blue", "C": "green"}, default="grey"
)
style.selection_color = "blue"
msg = r"'Style\.color' cannot be a `hv.dim` when 'Style.selection_color' is not None"
msg = "'Style.color' cannot be a `hv.dim` / `None` when 'Style.selection_color' is not None"
with pytest.raises(ValueError, match=msg):
compare_style(cat_annotator)

Expand Down Expand Up @@ -157,6 +160,6 @@ def test_groupby_color_change(cat_annotator) -> None:
cat_annotator.visible = ["A", "B", "C"]

indicators = hv.render(cat_annotator.get_display("x").static_indicators()).renderers
color_cycle = cat_annotator.style.color.values
color_cycle = cat_annotator.style._colormap.values()
for indicator, expected_color in zip(indicators, color_cycle):
assert indicator.glyph.fill_color == expected_color
Loading