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

Subcoordinate y-range from RangeXY stream with DynamicMap #6136

Closed
ahuang11 opened this issue Feb 28, 2024 · 8 comments
Closed

Subcoordinate y-range from RangeXY stream with DynamicMap #6136

ahuang11 opened this issue Feb 28, 2024 · 8 comments
Assignees
Labels
tag: feature: subcoordinate_y type: enhancement Minor feature or improvement to an existing feature

Comments

@ahuang11
Copy link
Collaborator

ahuang11 commented Feb 28, 2024

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 ahuang11 added the TRIAGE Needs triaging label Feb 28, 2024
@ahuang11
Copy link
Collaborator Author

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

@maximlt
Copy link
Member

maximlt commented Feb 28, 2024

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

image

@droumis
Copy link
Member

droumis commented Feb 28, 2024

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: #6010

@droumis
Copy link
Member

droumis commented Feb 28, 2024

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 holoviz-topics/neuro#87 (comment) 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)
Screen.Recording.2024-02-28.at.1.10.33.PM.mov

@ahuang11
Copy link
Collaborator Author

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

@maximlt
Copy link
Member

maximlt commented Feb 28, 2024

Yes!

@droumis
Copy link
Member

droumis commented Mar 26, 2024

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

@droumis droumis changed the title Unsure how to use subcoordinate y-range from RangeXY stream Subcoordinate y-range from RangeXY stream with DynamicMap Apr 2, 2024
@droumis droumis assigned philippjfr and unassigned maximlt Apr 5, 2024
@philippjfr
Copy link
Member

Okay, unlike #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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
tag: feature: subcoordinate_y type: enhancement Minor feature or improvement to an existing feature
Projects
None yet
Development

No branches or pull requests

4 participants