Unsure how to use subcoordinate y-range from RangeXY stream
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.
Also, dragging the bounding box doesn't exactly work either; using holoviews main.
Had a look at it with @Hoxbro and saw two issues:
- The
y_rangevalue 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
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
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
Yes that's it! Thanks for coming up with the proper MRE.
Yes!
@maximlt, if you have time for CZI next week, please prioritize this
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.