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

Add min_interval and max_interval to the RangeToolLink #6134

Merged
merged 10 commits into from
May 17, 2024
6 changes: 3 additions & 3 deletions examples/gallery/demos/bokeh/eeg_viewer.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,6 @@
"metadata": {},
"outputs": [],
"source": [
"\n",
"N_CHANNELS = 10\n",
"N_SECONDS = 5\n",
"SAMPLING_RATE = 200\n",
Expand Down Expand Up @@ -141,7 +140,7 @@
"source": [
"## Building the dashboard\n",
"\n",
"Finally, we use [`RangeToolLink`](../../../user_guide/Linking_Plots.ipynb) to connect the minimap `Image` and the EEG `Overlay`, setting bounds for the initial viewable area. Once the plots are linked and assembled into a unified dashboard, you can interact with it. Experiment by dragging the selection box on the minimap or resizing it by clicking and dragging its edges."
"Finally, we use [`RangeToolLink`](../../../user_guide/Linking_Plots.ipynb) to connect the minimap `Image` and the EEG `Overlay`, setting bounds for the initially viewable area with `boundsx` and `boundsy`, and finally a max range of 2 seconds with `intervalsx`. Once the plots are linked and assembled into a unified dashboard, you can interact with it. Experiment by dragging the selection box on the minimap or resizing it by clicking and dragging its edges."
]
},
{
Expand All @@ -153,7 +152,8 @@
"source": [
"RangeToolLink(\n",
" minimap, eeg, axes=[\"x\", \"y\"],\n",
" boundsx=(None, 2), boundsy=(None, 6.5)\n",
" boundsx=(None, 2), boundsy=(None, 6.5),\n",
" intervalsx=(None, 2),\n",
")\n",
"\n",
"dashboard = (eeg + minimap).opts(merge_tools=False).cols(1)\n",
Expand Down
44 changes: 34 additions & 10 deletions holoviews/plotting/bokeh/links.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
VertexTableLink,
)
from ..plot import GenericElementPlot, GenericOverlayPlot
from .util import bokeh34


class LinkCallback:
Expand Down Expand Up @@ -141,23 +142,46 @@ def __init__(self, root_model, link, source_plot, target_plot):
continue

axes[f'{axis}_range'] = target_plot.handles[f'{axis}_range']
bounds = getattr(link, f'bounds{axis}', None)
if bounds is None:
continue
interval = getattr(link, f'intervals{axis}', None)
if interval is not None and bokeh34:
min, max = interval
if min is not None:
axes[f'{axis}_range'].min_interval = min
if max is not None:
axes[f'{axis}_range'].max_interval = max
self._set_range_for_interval(axes[f'{axis}_range'], max)

start, end = bounds
if start is not None:
axes[f'{axis}_range'].start = start
axes[f'{axis}_range'].reset_start = start
if end is not None:
axes[f'{axis}_range'].end = end
axes[f'{axis}_range'].reset_end = end
bounds = getattr(link, f'bounds{axis}', None)
if bounds is not None:
start, end = bounds
if start is not None:
axes[f'{axis}_range'].start = start
axes[f'{axis}_range'].reset_start = start
if end is not None:
axes[f'{axis}_range'].end = end
axes[f'{axis}_range'].reset_end = end

tool = RangeTool(**axes)
source_plot.state.add_tools(tool)
if toolbars:
toolbars[0].tools.append(tool)

def _set_range_for_interval(self, axis, max):
# Changes the existing Range1d axis range to be in the interval
for n in ("", "reset_"):
start = getattr(axis, f"{n}start")
try:
end = start + max
except Exception as e:
# Handle combinations of datetime axis and timedelta interval
# Likely a better way to do this
try:
import pandas as pd
end = (pd.array([start]) + pd.array([max]))[0]
except Exception:
raise e from None
setattr(axis, f"{n}end", end)


class DataLinkCallback(LinkCallback):
"""
Expand Down
6 changes: 6 additions & 0 deletions holoviews/plotting/links.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,12 @@ class RangeToolLink(Link):
boundsy = param.Tuple(default=None, length=2, doc="""
(start, end) bounds for the y-axis""")

intervalsx = param.Tuple(default=None, length=2, doc="""
(min, max) intervals for the x-axis""")

intervalsy = param.Tuple(default=None, length=2, doc="""
(min, max) intervals for the y-axis""")

_requires_target = True


Expand Down
29 changes: 13 additions & 16 deletions holoviews/tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,33 +74,30 @@ def ibis_sqlite_backend():
ibis.set_backend(None)


@pytest.fixture
def bokeh_backend():
hv.renderer("bokeh")
def _plotting_backend(backend):
pytest.importorskip(backend)
if not hv.extension._loaded:
hv.extension(backend)
hv.renderer(backend)
prev_backend = hv.Store.current_backend
hv.Store.current_backend = "bokeh"
hv.Store.current_backend = backend
yield
hv.Store.current_backend = prev_backend


@pytest.fixture
def bokeh_backend():
yield from _plotting_backend("bokeh")


@pytest.fixture
def mpl_backend():
pytest.importorskip("matplotlib")
hv.renderer("matplotlib")
prev_backend = hv.Store.current_backend
hv.Store.current_backend = "matplotlib"
yield
hv.Store.current_backend = prev_backend
yield from _plotting_backend("matplotlib")


@pytest.fixture
def plotly_backend():
pytest.importorskip("plotly")
hv.renderer("plotly")
prev_backend = hv.Store.current_backend
hv.Store.current_backend = "plotly"
yield
hv.Store.current_backend = prev_backend
yield from _plotting_backend("plotly")


@pytest.fixture
Expand Down
53 changes: 53 additions & 0 deletions holoviews/tests/ui/bokeh/test_links.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
from datetime import timedelta

import numpy as np
import pandas as pd
import pytest
from bokeh.sampledata.stocks import AAPL

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

from .. import expect

pytestmark = pytest.mark.ui


@pytest.mark.usefixtures("bokeh_backend")
@pytest.mark.parametrize(
["index", "intervalsx", "x_range_src", "x_range_tgt"],
[
(range(len(AAPL["date"])), (100, 365), (0, 365), (0, 3269)),
(
pd.to_datetime(AAPL["date"]),
(timedelta(days=100), timedelta(days=365)),
(
np.array(["2000-03-01"], dtype="datetime64[ns]")[0],
pd.Timestamp("2001-03-01"),
),
np.array(["2000-03-01", "2013-03-01"], dtype="datetime64[ns]"),
),
],
ids=["int", "datetime"],
)
def test_rangetool_link_interval(serve_hv, index, intervalsx, x_range_src, x_range_tgt):
df = pd.DataFrame(AAPL["close"], columns=["close"], index=index)
df.index.name = "Date"

aapl_curve = hv.Curve(df, "Date", ("close", "Price ($)"))
tgt = aapl_curve.relabel("AAPL close price").opts(width=800, labelled=["y"])
src = aapl_curve.opts(width=800, height=100, yaxis=None)

RangeToolLink(src, tgt, axes=["x", "y"], intervalsx=intervalsx)
layout = (tgt + src).cols(1)
layout.opts(hv.opts.Layout(shared_axes=False))

page = serve_hv(layout)
hv_plot = page.locator(".bk-events")
expect(hv_plot).to_have_count(2)

bk_model = hv.render(layout)
bk_src = bk_model.children[0][0]
np.testing.assert_equal((bk_src.x_range.start, bk_src.x_range.end), x_range_src)
bk_tgt = bk_model.children[1][0]
np.testing.assert_equal((bk_tgt.x_range.start, bk_tgt.x_range.end), x_range_tgt)