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

Overlaying annotations on target plot breaks RangeToolLink #6010

Closed
droumis opened this issue Dec 6, 2023 · 6 comments · Fixed by #6256
Closed

Overlaying annotations on target plot breaks RangeToolLink #6010

droumis opened this issue Dec 6, 2023 · 6 comments · Fixed by #6256

Comments

@droumis
Copy link
Member

droumis commented Dec 6, 2023

ALL software version info

holonote 0.1.0
holoviews 1.18.1

Description of expected behavior and the observed behavior

Overlaying annotations on a target plot prevents the RangeToolLink in a source plot from updating the dimension in the unbounded dimension of the annotation. For instance, with x-range annotations, the y-dimension link is broken.

Complete, minimal, self-contained example code that reproduces the issue

Code
import numpy as np
import holoviews as hv
from bokeh.models import HoverTool
from holoviews.plotting.links import RangeToolLink
from scipy.stats import zscore
from holoviews.operation.datashader import rasterize

hv.extension('bokeh')

N_CHANNELS = 10
N_SECONDS = 5
SAMPLING_RATE = 200
INIT_FREQ = 2  # Initial frequency in Hz
FREQ_INC = 5  # Frequency increment
AMPLITUDE = 1

# Generate time and channel labels
total_samples = N_SECONDS * SAMPLING_RATE
time = np.linspace(0, N_SECONDS, total_samples)
channels = [f'EEG {i}' for i in range(N_CHANNELS)]

# Generate sine wave data
data = np.array([AMPLITUDE * np.sin(2 * np.pi * (INIT_FREQ + i * FREQ_INC) * time)
                     for i in range(N_CHANNELS)])

hover = HoverTool(tooltips=[
    ("Channel", "@channel"),
    ("Time", "$x s"),
    ("Amplitude", "$y µV")
])

channel_curves = []
for channel, channel_data in zip(channels, data):
    ds = hv.Dataset((time, channel_data, channel), ["Time", "Amplitude", "channel"])
    curve = hv.Curve(ds, "Time", ["Amplitude", "channel"], label=channel)
    curve.opts(
        subcoordinate_y=True, color="black", line_width=1, tools=[hover],
    )
    channel_curves.append(curve)

eeg_curves = hv.Overlay(channel_curves, kdims="Channel")

annotator = Annotator({"Time": float}, fields=["category"])
annotations_df = pd.DataFrame({'start': [1], 'end': [2], 'category': ['demo']})
annotator.define_annotations(annotations_df, Time=("start", "end"))
annotations_overlay = annotator.get_element("Time")

eeg_app = (annotations_overlay * eeg_curves).opts(
    xlabel="Time (s)", ylabel="Channel", show_legend=False, aspect=3, responsive=True,
)

y_positions = range(N_CHANNELS)
yticks = [(i , ich) for i, ich in enumerate(channels)]

z_data = zscore(data, axis=1)

minimap = rasterize(hv.Image((time, y_positions , z_data), ["Time (s)", "Channel"], "Amplitude (uV)"))
minimap = minimap.opts(
    cmap="RdBu_r", xlabel='Time (s)', alpha=.5, yticks=[yticks[0], yticks[-1]],
    height=150, responsive=True, default_tools=[], clim=(-z_data.std(), z_data.std())
)

RangeToolLink(
    minimap, eeg_curves, axes=["x", "y"],
    boundsx=(None, 2), boundsy=(None, 6.5)
)

dashboard = (eeg_app + minimap * annotations_overlay).opts(merge_tools=False).cols(1)
dashboard

Screenshots or screencasts of the bug in action

Screen.Recording.2023-12-06.at.1.47.59.PM.mov
@droumis
Copy link
Member Author

droumis commented Dec 6, 2023

hmmm... a simpler example works. so something about the original example code above is throwing things off...

import holoviews as hv; hv.extension('bokeh')
from holoviews.plotting.links import RangeToolLink
from holonote.annotate import Annotator
import pandas as pd

annotator = Annotator({"Time": float}, fields=["category"])
annotations_df = pd.DataFrame({'start': [.5], 'end': [.7], 'category': ['demo']})
annotator.define_annotations(annotations_df, Time=("start", "end"))
annotations_overlay = annotator.get_element("Time")

target = hv.Curve([1, 2], kdims='Time')
target_overlay = (annotations_overlay * target)

source = hv.Curve([1, 2], kdims='Time')

RangeToolLink(source, target, axes=["x", "y"])

(target_overlay + source * annotations_overlay).opts(shared_axes=False).cols(1)
Screen.Recording.2023-12-06.at.5.00.25.PM.mov

@hoxbro
Copy link
Member

hoxbro commented Dec 7, 2023

Here is an MRE and some other examples that do not work. None of them is using HoloNote so I will move this issue to HoloViews.

It seems like it is caused by RangeToolLink, subcoordinate_y, and DynamicMap.

import holoviews as hv
from holoviews.plotting.links import RangeToolLink
import pandas as pd

hv.extension("bokeh")

# Set up
curve_fn = lambda i: hv.Curve([i, i], kdims="Time", label=str(i)).opts(
    subcoordinate_y=True
)
target = hv.Overlay([curve_fn(1), curve_fn(2)])
source = hv.Curve([1, 2], kdims="Time")
RangeToolLink(source, target, axes=["x", "y"])

# What HoloNote does behind the scene
annotations_overlay = hv.DynamicMap(lambda: hv.VSpans(([0], [1])))
target_overlay = annotations_overlay * target

# Other example 1 (does not work)
annotations_overlay = hv.DynamicMap(lambda: hv.VSpans(([0], [1])))
target_overlay = target * annotations_overlay

# Curve example 1 (does not work)
annotations_overlay = hv.DynamicMap(lambda: hv.Curve([0, 1]))
target_overlay = annotations_overlay * target

# Curve example 2 (does not work)
annotations_overlay = hv.DynamicMap(lambda: hv.Curve([0, 1]))
target_overlay = target * annotations_overlay

# Curve example 3 (does not work and seems to remove the link)
annotations_overlay = hv.Curve([0, 1])
target_overlay = target * annotations_overlay

# Final
(target_overlay + source).opts(shared_axes=False).cols(1)

@hoxbro hoxbro transferred this issue from holoviz/holonote Dec 7, 2023
@droumis
Copy link
Member Author

droumis commented Feb 5, 2024

Just checking which of my issues are still valid.

This is still a valid issue.

@droumis
Copy link
Member Author

droumis commented Feb 28, 2024

@hoxbro , will you have a chance to look into this soon? It takes priority over scale bars, and it's right below the priority of finishing off the pandas index support to realize those performance gains 💪 . Hoping we can get all three completed by the end of March.

@philippjfr
Copy link
Member

I'm a little confused by a few of these MROs posted above. Going through them 1-by-1:

# What HoloNote does behind the scene
annotations_overlay = hv.DynamicMap(lambda: hv.VSpans(([0], [1])))
target_overlay = annotations_overlay * target

Behavior I'm seeing is that the range tool correctly links the x-axis. It's also linking the y-axis of the first subcoordinate Curve. I'm not sure that's necessarily expected but it's poorly defined behavior. Would we expect all sub-coordinate ranges to be linked here?

# Other example 1 (does not work)
annotations_overlay = hv.DynamicMap(lambda: hv.VSpans(([0], [1])))
target_overlay = target * annotations_overlay

This does work, but it's not specified correctly. Since the target and source now share the exact same x-dimension the ranges end up linked, you could therefore use target_overlay + source.opts(axiswise=True).

The other cases are just variations of the first two. So the only real "issue" I see here is the behavior of the subcoordinate-y ranges. Do we expect all of them to be linked in these cases? I'd agree with that but it doesn't seem urgent since it's not needed for the CZI related tasks.

Copy link

This issue has been automatically locked since there has not been any recent activity after it was closed. Please open a new issue for related bugs.

@github-actions github-actions bot locked as resolved and limited conversation to collaborators Oct 23, 2024
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

Successfully merging a pull request may close this issue.

4 participants