-
Notifications
You must be signed in to change notification settings - Fork 2
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
Update visible UI from annotator events #123
Conversation
CodSpeed Performance ReportMerging #123 will not alter performanceComparing Summary
|
Codecov ReportAttention: Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #123 +/- ##
==========================================
+ Coverage 86.34% 86.74% +0.40%
==========================================
Files 26 26
Lines 2775 2920 +145
==========================================
+ Hits 2396 2533 +137
- Misses 379 387 +8 ☔ View full report in Codecov by Sentry. |
I'm stuck. The coloring seems pretty broken/out of sync with the actual annotations, and I don't think I'll be able to fix it myself. Any help would be very welcome! I was hoping it would be easy to just grab and apply a colormap from the annotations and use that for the visibility widget, but I don't see a way. I'm happy to forget about imposing the persistent colormap idea (added in this PR for the visible UI), but even then, I don't understand how to sync with the colors of the annotations on the plot per groupby field category/type. It's a bit complicated because the user may or may not provide a colormap as an hv.dim. If they do, this hv.dim may be partial, as users can continue to create/edit additional categories not originally specified by the hv.dim. example of user-specified colors
from holonote.annotate import Annotator
from holonote.app.tabulator import AnnotatorTable
from holonote.app import PanelWidgets
import panel as pn; pn.extension('tabulator')
import holoviews as hv; hv.extension('bokeh')
from holonote.annotate.connector import SQLiteDB
annotator = Annotator({"height": float, "width": float}, fields=["type"],
connector=SQLiteDB(filename=':memory:'))
color_dim = hv.dim("type").categorize(
categories={"A": "blue", "B": "red", "C": "green"}, default="grey"
)
annotator.style.color = color_dim
annotator.groupby = "type"
annotator_widgets = pn.Column(PanelWidgets(annotator), AnnotatorTable(annotator))
pn.Column(annotator_widgets, annotator * hv.Image([], ['width', 'height'])).servable() Remaining problem of the colors of the annotations not being colored the same way as the visibility UI legend:GMT20240710-004941_Clip_Demetris.Roumis.s.Clip.07_09_2024.mp4 |
To clarify the issue, when you delete the box annotation, the colors get out of sync and you want to grab the color of the new box? |
holonote/app/panel.py
Outdated
if color not in used_colors: | ||
return color | ||
# If all default colors are used, cycle through again | ||
return _default_color[len(self.colormap) % len(_default_color)] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wondering if you could use hv.Cycle
here.
Yes, except the colors were never really technically in sync; it just so happens that, in some conditions (e.g. if no color mapping is specified by the user), they appear to be in sync when they really are not. And this lack of syncing becomes apparent when you delete all of a certain group of annotations, or when a new group is created that gets sorted between existing groups. The way the visible UI is generating colors is just different than how colors are generated for the annotation boxes. I haven't figured out yet how to sync them. |
A way to sync I think is by taking inspiration from how HoloViews curve and scatter plots have the same colors, by both having their own instances of |
hmmm, I'm not sure I understand how to implement this in the context of a potentially partially user-specified colormap |
I believe |
Something like this? import holoviews as hv
import matplotlib
hv.extension("bokeh")
# get list from colormaps
cmap = matplotlib.colormaps["RdBu_r"]
colors = [matplotlib.colors.rgb2hex(cmap(i)) for i in range(2)]
cmap_a = hv.Cycle(colors)
cmap_b = hv.Cycle(colors)
hv.Curve([1, 2, 3]).opts(color=cmap_a) * hv.Curve([3, 2, 1]).opts(color=cmap_b) |
holonote/app/panel.py
Outdated
self.visible_widget.options = sorted([*old_options, new_option]) | ||
self.visible_widget.value = [*old_values, new_option] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Always better to update all parameters at once.
self.visible_widget.options = sorted([*old_options, new_option]) | |
self.visible_widget.value = [*old_values, new_option] | |
self.visible_widget.param.update( | |
options=sorted([*old_options, new_option]), | |
value=[*old_values, new_option] | |
) |
The same applies below.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
thanks for the tip, I committed this change in this particular 'create' event type. The other place where two parameters are updated is under the 'update' event, but I will keep that separate because if a group (e.g. 'A') had been set to not be visible, and then an annotation that is the last of it's kind is updated (e.g. from 'B' to 'A'), then I would want the group ('A') to remain not visible, even though we now have to update the 'options' parameter. That is why I am keeping them separate for that particular event type.
Condition: color is not defined by user, groupby is applied, no annotations at initializationChecking if colors stay synced with the default colormap when groups become resorted (from update, creation, or deletion) Codefrom holonote.annotate import Annotator
from holonote.app.tabulator import AnnotatorTable
from holonote.app import PanelWidgets
import panel as pn; pn.extension('tabulator')
import holoviews as hv; hv.extension('bokeh')
from holonote.annotate.connector import SQLiteDB
annotator = Annotator({"x": float,}, fields=["type"],
connector=SQLiteDB(filename=':memory:'))
annotator.groupby = "type"
annotator_widgets = pn.Column(PanelWidgets(annotator), AnnotatorTable(annotator))
pn.Column(annotator_widgets, annotator * hv.Curve([]).opts(xlim=(-1,3))).servable() GMT20240719-230730_Clip_Color.is.not.defined.by.user.groupby.is.applied.no.annotations.at.initialization.mp4 |
Condition: color is defined by user with hv.dim, groupby is applied, annotations present at initializationChecking same as above, but also if new annotation types that are not specified in the hv.dim dict get assigned to the 'default' color grey. Also checking if sole annotations (only in its type) get assigned to same color after deletion and then recreation. Codeimport pandas as pd
from holonote.annotate import Annotator
from holonote.app.tabulator import AnnotatorTable
from holonote.app import PanelWidgets
import panel as pn; pn.extension('tabulator')
import holoviews as hv; hv.extension('bokeh')
from holonote.annotate.connector import SQLiteDB
data = {
'type': ['A', 'B', 'C'],
'start': range(3),
'end': [i + .8 for i in range(3)]
}
annotations_df = pd.DataFrame(data)
annotator = Annotator({"x": float,}, fields=["type"],
connector=SQLiteDB(filename=':memory:'))
annotator.define_annotations(annotations_df, x=("start", "end"))
color_dim = hv.dim("type").categorize(
categories={"A": "purple", "B": "orange", "C": "green"}, default="grey"
)
annotator.style.color = color_dim
annotator.groupby = "type"
annotator_widgets = pn.Column(PanelWidgets(annotator), AnnotatorTable(annotator))
pn.Column(annotator_widgets, annotator * hv.Curve([]).opts(xlim=(-1,3))).servable() GMT20240719-231455_Clip_color.is.defined.by.user.with.hvdim.groupby.is.applied.no.annotations.at.initializ.mp4 |
Condition: color is defined by user with hv.dim, groupby is applied, BUT only SOME annotations are present at initialization which match with the entries of the color dimChecking specifically that the color remains consistent when I create>delete>create an annotation for that group which did not have an annotation at initialization, but whose color is specified by the user regardless. Codeimport pandas as pd
from holonote.annotate import Annotator
from holonote.app.tabulator import AnnotatorTable
from holonote.app import PanelWidgets
import panel as pn; pn.extension('tabulator')
import holoviews as hv; hv.extension('bokeh')
from holonote.annotate.connector import SQLiteDB
data = {
'type': ['A', 'B'],
'start': range(2),
'end': [i + .8 for i in range(2)]
}
annotations_df = pd.DataFrame(data)
annotator = Annotator({"x": float,}, fields=["type"],
connector=SQLiteDB(filename=':memory:'))
annotator.define_annotations(annotations_df, x=("start", "end"))
color_dim = hv.dim("type").categorize(
categories={"A": "purple", "B": "orange", "C": "green"}, default="grey"
)
annotator.style.color = color_dim
annotator.groupby = "type"
annotator_widgets = pn.Column(PanelWidgets(annotator), AnnotatorTable(annotator))
pn.Column(annotator_widgets, annotator * hv.Curve([]).opts(xlim=(-1,3))).servable() GMT20240719-232111_Clip_Partial.init.annotations.dim.color.specified.mp4 |
Condition: Color is defined by user as a string, groupby applied, no annotations at initializationChecking for any signs of chaos.. should be a straightforward condition. Code## COLOR IS STRING, GROUPBY, NO ANNOTATIONS INIT
import pandas as pd
from holonote.annotate import Annotator
from holonote.app.tabulator import AnnotatorTable
from holonote.app import PanelWidgets
import panel as pn; pn.extension('tabulator')
import holoviews as hv; hv.extension('bokeh')
from holonote.annotate.connector import SQLiteDB
data = {
'type': ['A', 'B', 'C'],
'start': range(3),
'end': [i + .8 for i in range(3)]
}
annotations_df = pd.DataFrame(data)
annotator = Annotator({"x": float,}, fields=["type"],
connector=SQLiteDB(filename=':memory:'))
annotator.define_annotations(annotations_df, x=("start", "end"))
annotator.style.color = 'red'
annotator.groupby = "type"
annotator_widgets = pn.Column(PanelWidgets(annotator), AnnotatorTable(annotator))
pn.Column(annotator_widgets, annotator * hv.Curve([]).opts(xlim=(-1,3))).servable() GMT20240719-232653_Clip_Demetris.Roumis.s.Clip.07_19_2024.mp4 |
Condition: color is defined by user with hv.dim, groupby is applied, annotations are not present at initializationChecking that the widgets can be initialized without data and then correctly applied the specified colormap I also now made it so that an entry requires data to be included in the visibility widget.. preventing issues with Codefrom holonote.annotate import Annotator
from holonote.app.tabulator import AnnotatorTable
from holonote.app import PanelWidgets
import panel as pn; pn.extension('tabulator')
import holoviews as hv; hv.extension('bokeh')
from holonote.annotate.connector import SQLiteDB
annotator = Annotator({"x": float,}, fields=["type"],
connector=SQLiteDB(filename=':memory:'))
color_dim = hv.dim("type").categorize(
categories={"A": "purple", "B": "orange", "C": "green"}, default="grey"
)
annotator.style.color = color_dim
annotator.groupby = "type"
annotator_widgets = pn.Column(PanelWidgets(annotator), AnnotatorTable(annotator))
pn.Column(annotator_widgets, annotator * hv.Curve([]).opts(xlim=(-1,3))).servable() vizui.mp4 |
elif isinstance(style.color, str): | ||
self.colormap = dict(zip(options, [style.color] * len(options))) | ||
elif isinstance(style.color, hv.dim): | ||
self.colormap = self.annotator.style.color.ops[0]["kwargs"]["categories"] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
self.colormap = self.annotator.style.color.ops[0]["kwargs"]["categories"] | |
self.colormap = dict(self.annotator.style.color.ops[0]["kwargs"]["categories"]) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Alternatively could you do something like:
colormap = dict(zip(options, style.color.apply(hv.Dataset(options, [self.annotator.groupby]))))
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think @hoxbro had an initial impression that this suggestion might be too heavy for this particular use case, but I'll let him clarify/expound in his review
@hoxbro , is this ready to merge? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good.
Can you check if the changes look in Google Chrome, Firefox, and Safari with respect to the stylesheet change?
@hoxbro. Good call. Firefox was a bit misaligned and Safari doesn't display the circles at all. Firefox was an easy fix, but unfortunately, it seems like the circles were never appearing in Safari. From what I can tell, styling a select element isn't easily done in Safari. I tried to placing the circles inline with the option element as SVGs so as to avoid using ':after' psedo class. I also started trying to created a select wrapper class, but I couldn't get it to work with Safari at all. I propose that we merge this PR, knowing the colored circles are not yet showing in Safari, and then work towards replacing the Multiselect component with something like a selectable tabulator. This would also make it easier to add additional group-level batch actions: recoloring, deleting, renaming, etc because we could put additional buttons in the additional columns. BeforeAfter most recent commit: |
Fixes #124
Previously, the PanelWidgets' Visibility UI did not update when new annotations were added/removed/changed. This PR attempts to synchronize the visibility UI with annotator events: create, delete, and update (edit).
I've also changed how tabulator on_edit triggers events.. previously any change was triggering both field and region update events, and now it's either one xor the other.
I've also moved the legend color circles to the left of the visibility UI instead of the right, given that if you added more than 4 groupby field types, then a scrollbar appears and obscures the legend color circles on the right. The aesthetics of this can still be much improved, but at least it's not interfering now.
TODO:
Make annotator glyph coloring to respect the new persistent approach - (a new groupby field value gets assigned a color and it will stay that color regardless if you delete all the annotations of that field value and then create more).Instead of the persistent approach (saved for a future discussion/PR), I'm now just following what the annotation glyphs are doing.. essentially annotation groups (if color is unspecified by the user) will change color based on sorted order.Code
Basic Functionality working pretty well:
GMT20240710-004725_Clip_Demetris.Roumis.s.Clip.07_09_2024.mp4