holoviz / holoviews

With Holoviews, your data visualizes itself.
https://holoviews.org
BSD 3-Clause "New" or "Revised" License
2.7k stars 402 forks source link

Overlaying annotations on target plot breaks RangeToolLink #6010

Closed droumis closed 4 months ago

droumis commented 10 months ago

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

https://github.com/holoviz/holonote/assets/6613202/4ad2e7e7-f9ed-4c51-b16b-200a1e0ee84b

droumis commented 10 months ago

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)

https://github.com/holoviz/holonote/assets/6613202/06b6b762-1b8c-42a7-9763-2bc0a0fc8c28

hoxbro commented 10 months ago

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)
droumis commented 8 months ago

Just checking which of my issues are still valid.

This is still a valid issue.

droumis commented 7 months ago

@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 commented 4 months ago

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.