holoviews icon indicating copy to clipboard operation
holoviews copied to clipboard

Unsure how to use subcoordinate y-range from RangeXY stream

Open ahuang11 opened this issue 2 years ago • 6 comments

import pandas as pd
import holoviews as hv

from holoviews.plotting.links import RangeToolLink

from bokeh.sampledata.stocks import AAPL, MSFT
from bokeh.models import WheelZoomTool

hv.extension("bokeh")


def show_curves(x_range, y_range):
    print(y_range)
    aapl_curve = hv.Curve(
        aapl_df[slice(*map(int, x_range))], "Date", ("close", "Price ($)"), label="AAPL"
    ).opts(
        subcoordinate_scale=3,
        subcoordinate_y=True,
    )
    msft_curve = hv.Curve(
        msft_df[slice(*map(int, x_range))], "date", ("close", "Price ($)"), label="MSFT"
    ).opts(
        subcoordinate_scale=3,
        subcoordinate_y=True,
    )
    curves = aapl_curve * msft_curve
    return curves.opts(
        width=800,
        height=300,
        labelled=["y"],
        framewise=True,
        axiswise=True,
        tools=[WheelZoomTool()],
    )


aapl_df = pd.DataFrame(
    AAPL["close"], columns=["close"], index=pd.to_datetime(AAPL["date"])
)
aapl_df.index.name = "Date"
aapl_df.index = range(len(aapl_df))

msft_df = pd.DataFrame(
    MSFT["close"], columns=["close"], index=pd.to_datetime(MSFT["date"])
)
msft_df.index.name = "Date"
msft_df.index = range(len(msft_df))

range_stream = hv.streams.RangeXY(x_range=(0, 2500), y_range=(0, 1))
curves = hv.DynamicMap(show_curves, streams=[range_stream])

src = hv.Curve(msft_df, "date", ("close")).opts(
    width=800, height=100, yaxis=None, default_tools=[]
)

RangeToolLink(src, curves, axes=["x", "y"], boundsx=(0, 2500))

layout = (curves + src).cols(1)
layout

It gives scaled / unscaled values I think, and does it twice.

(0, 1)  # <-- this can be a float with more curves
(13.12, 215.04)

I also tried playing around with subcoordinate_y as a tuple, but it makes the curves way too spread apart.

ahuang11 avatar Feb 28 '24 00:02 ahuang11

Also, dragging the bounding box doesn't exactly work either; using holoviews main.

ahuang11 avatar Feb 28 '24 00:02 ahuang11

Had a look at it with @Hoxbro and saw two issues:

  • The y_range value passed by the stream is the min/max of APPL in the viewport. It should be the min/max values of the outer range (something like (-0.5, 1.5)). I'll look into fixing that.
  • In this example, having the minimap y-linked to the plot doesn't really make sense to me. Should it zoom on APPL only? I think having an example closer to the setup in https://github.com/holoviz-topics/neuro/issues/87#issuecomment-1967959556 would help understand why y-linking isn't working as expected

image

maximlt avatar Feb 28 '24 10:02 maximlt

The y_range value passed by the stream is the min/max of APPL in the viewport. It should be the min/max values of the outer range (something like (-0.5, 1.5)). I'll look into fixing that.

Similar lack of y-dim control was documented here with combined overlay and dmap: https://github.com/holoviz/holoviews/issues/6010

droumis avatar Feb 28 '24 20:02 droumis

In this example, having the minimap y-linked to the plot doesn't really make sense to me. Should it zoom on APPL only? I think having an example closer to the setup in https://github.com/holoviz-topics/neuro/issues/87#issuecomment-1967959556 would help understand why y-linking isn't working as expected

@ahuang11 , below is dummy data and plotting for something closer to the intended use-case. I think it captures what @maximlt is asking for above and the bug you are reporting, but please confirm!

Code for example that's closer to intended use-case
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

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

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")
])

def show_curves(x_range, y_range):
    # when y_range for subcoords is fixed, we could try to also drop out of view channels 
    print(y_range) 
    if x_range is None:  # Fallback if no range is selected
        x_range = (0, N_SECONDS)
    # Calculate indices for slicing data based on x_range
    start_idx = max(int((x_range[0] / N_SECONDS) * total_samples), 0)
    end_idx = min(int((x_range[1] / N_SECONDS) * total_samples), total_samples)
    
    channel_curves = []
    for channel, channel_data in zip(channels, data):
        sliced_time = time[start_idx:end_idx]
        sliced_data = channel_data[start_idx:end_idx]
        ds = hv.Dataset((sliced_time, sliced_data, channel), ["Time", "Amplitude", "channel"])
        curve = hv.Curve(ds, "Time", ["Amplitude", "channel"], label=channel).opts(
            color="black", line_width=1, tools=[hover], responsive=True,
            height=400, show_legend=False,subcoordinate_y=True,
        )
        channel_curves.append(curve)
    return hv.Overlay(channel_curves)

range_stream = hv.streams.RangeXY(x_range=(0, N_SECONDS), y_range=(0, 1))

curves = hv.DynamicMap(show_curves, streams=[range_stream])

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)")).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, curves, axes=["x", "y"], boundsx=(None, 2), boundsy=(None, 6.5))

(curves + minimap).opts(merge_tools=False).cols(1)

https://github.com/holoviz/holoviews/assets/6613202/ab51c505-4f3a-44c2-ac4b-0aae4b3a24ce

droumis avatar Feb 28 '24 21:02 droumis

Yes that's it! Thanks for coming up with the proper MRE.

ahuang11 avatar Feb 28 '24 21:02 ahuang11

Yes!

maximlt avatar Feb 28 '24 21:02 maximlt

@maximlt, if you have time for CZI next week, please prioritize this

droumis avatar Mar 26 '24 17:03 droumis

Okay, unlike https://github.com/holoviz/holoviews/issues/6010 this issue makes sense to me. The hope is to link the outer y-range of the subcoordinate-y instead of linking the internal subcoordinates. I'll work on that now.

philippjfr avatar Jun 04 '24 12:06 philippjfr