-
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
Support points 2d #108
Support points 2d #108
Conversation
CodSpeed Performance ReportMerging #108 will not alter performanceComparing Summary
|
Can you reduce your example code to show only the relevant part for this PR? |
Sure from holonote.annotate import Annotator
from holonote.app import PanelWidgets
import xarray as xr
import panel as pn
import holoviews as hv
hv.extension("bokeh")
ds = xr.tutorial.open_dataset("air_temperature")
def plot_image(time):
image = hv.Image(ds.sel(time=time), ["lon", "lat"], ["air"]).opts(
cmap="RdBu_r",
title=time,
colorbar=True,
clim=(220, 340),
width=500,
height=300,
tools=["tap"],
)
return image
def plot_timeseries_by_select(indices):
if not indices:
return
# get the selected point
row = annotator.df.loc[indices[0]]
x = row["point[lon]"]
y = row["point[lat]"]
ds_sel = ds.sel(lon=x, lat=y, method="nearest")
time_series.object = hv.Curve(ds_sel["air"]).opts(title="Time Series")
times = ds.time.dt.strftime("%Y-%m-%d %H:%M").values.tolist()
# start annotation
annotator = Annotator(
{"lon": float, "lat": float},
fields=["Description", "Time", "Z"],
default_region="point",
)
annotator_widgets = PanelWidgets(annotator, field_values={"Time": times, "Z": 100})
# make image dependent on the selected time
time_input = annotator_widgets.fields_widgets[1]
time_input.value = times[0]
image = hv.DynamicMap(pn.bind(plot_image, time_input))
time_series = pn.pane.HoloViews()
# # update plot time when a new point is SELECTED
pn.bind(plot_timeseries_by_select, annotator.param.selected_indices, watch=True)
pn.Row(annotator_widgets, pn.Column(annotator * image), time_series).show() |
Actually, at its very core: from holonote.annotate import Annotator
from holonote.app import PanelWidgets
import xarray as xr
import panel as pn
import holoviews as hv
hv.extension("bokeh")
ds = xr.tutorial.open_dataset("air_temperature")
annotator = Annotator(
{"lon": float, "lat": float},
fields=["Description", "Z"],
default_region="point",
)
annotator_widgets = PanelWidgets(annotator, field_values={"Z": 100})
image = hv.Image(ds.isel(time=0), ["lon", "lat"], ["air"]).opts(cmap="RdBu_r")
pn.Row(annotator_widgets, annotator * image).show() |
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 have left some comments.
I would also like to see some unit tests.
Codecov ReportAttention: Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #108 +/- ##
==========================================
+ Coverage 86.18% 86.77% +0.58%
==========================================
Files 24 25 +1
Lines 2592 2662 +70
==========================================
+ Hits 2234 2310 +76
+ Misses 358 352 -6 ☔ View full report in Codecov by Sentry. |
holonote/annotate/display.py
Outdated
if (distance > self._nearest_2d_point_threshold**2).all(): | ||
return [] |
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.
This prevents editing and deleting existing annotations, at least in my testing. I'm leaning toward deleting this logic.
Using your example from: #108 (comment)
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.
After playing around with it a bit more (and also adding the style options), I can see why this is done.
I still think it is the correct thing to prioritize the simple example containing "holonote" only, over the more complex one.
Therefore, I would suggest that self._nearest_2d_point_threshold
is disabled by default. Also, update the docstring with information that if the value is chosen too small, you can lose the options to edit and delete annotations.
My style diff:
diff --git a/holonote/annotate/display.py b/holonote/annotate/display.py
index 304b42b..27a96c7 100644
--- a/holonote/annotate/display.py
+++ b/holonote/annotate/display.py
@@ -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
@@ -133,6 +135,7 @@ class Style(param.Parameterized):
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, ...]:
@@ -148,6 +151,7 @@ class Style(param.Parameterized):
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.points_opts),
)
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 reviewing. I believe the last line of the opts should be edit_points_opts
? If so, I've implemented that.
Therefore, I would suggest that self._nearest_2d_point_threshold is disabled by default
I disagree; without it, users click anywhere, and it selects and highlights the nearest existing point, but that can be misleading the users because they see the previous point highlighted instead of where they clicked, and that's no where near what they want.
Screen.Recording.2024-07-01.at.12.29.04.PM.mov
Based on these comments:
This prevents editing and deleting existing annotations, at least in my testing.
I still think it is the correct thing to prioritize the simple example containing "holonote" only, over the more complex one.
We can just up the threshold to 1, and it works fine.
Screen.Recording.2024-07-01.at.12.29.51.PM.mov
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 believe the last line of the opts should be edit_points_opts?
Correct. Copy / paste error.
We can just up the threshold to 1, and it works fine.
It works "fine" with the current magnitude of lon
/lat
. If we edit this, we will see the same problems.
factor = 1e3 # or 1e-3
ds["lon"] = ds["lon"] * factor
ds["lat"] = ds["lat"] * factor
It can be hard to hit on a larger scale. On a smaller scale, we have the same problem as without the limit. So, having it enabled by default, no matter the value, we end up in a Goldilocks situation where it only works if it is just right and creates confusing behavior if it is off.
large.mp4
small.mp4
previous point highlighted instead of where they clicked
This is somewhat improved with the updated style, which highlights the selected annotation.
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.
On a smaller scale, we have the same problem as without the limit. So, having it enabled by default, no matter the value, we end up in a Goldilocks situation where it only works if it is just right and creates confusing behavior if it is off.
Which is why I made it a public param, similar to np.testing.assert_almost_equal. Alternatively, we could get the order of magnitude of the ranges in the dataset and use that.
This is somewhat improved with the updated style, which highlights the selected annotation.
To clarify, I meant while creating a new point, not editing an existing.
Screen.Recording.2024-05-30.at.11.40.30.AM.mov