8000 Add support for wheel zoom renderers under the cursor by mattpap · Pull Request #13826 · bokeh/bokeh · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
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 support for wheel zoom renderers under the cursor #13826

Merged
merged 16 commits into from
Jun 13, 2024

Conversation

mattpap
Copy link
Contributor
@mattpap mattpap commented Apr 15, 2024

This allows to zoom only renderers that were hit tested under the position of the device pointer, e.g.:

From examples/interaction/tools/subcoordinates_zoom example; showing grouping behavior (even/odd):

Screencast.from.07.06.2024.11.26.39.webm

fixes #13728

@mattpap
Copy link
Contributor Author
mattpap commented May 28, 2024

This tentatively ready (tests are still needed). I implemented the feature request from #13728 (comment) in hit_test_behavior property.

@droumis droumis requested a review from maximlt May 28, 2024 14:57
@mattpap mattpap force-pushed the mattpap/13728_hit_test_zoom branch from 2d881cc to baa1f11 Compare June 3, 2024 19:32
@droumis
Copy link
Member
droumis commented Jun 6, 2024

I'm not sure this is sufficient to address #13728. I commented there.

@droumis
Copy link
Member
droumis commented Jun 6, 2024

One possible idea: adjust the group_tools function to maintain the renderer groups..?

@mattpap
Copy link
Contributor Author
mattpap commented Jun 6, 2024

I'm currently implementing grouping behavior as part of WheelZoomTool.hit_test_behavior. This will include at least explicitly defined groups (something like hit_test_behavior=GroupBy(groups=[renderer_group_0, ...])), maybe grouping by Model.name or something like that.

@droumis
Copy link
Member
droumis commented Jun 7, 2024

ok thank you! As you suggested, I think something explicit like hit_test_behavior=GroupBy(groups=[renderer_group_0, ...]) could work. Or alternatively something like hit_test_behavior = 'by_group' and then allow for renderers = GroupBy(groups=[renderer_group_0, ...].

@mattpap mattpap force-pushed the mattpap/13728_hit_test_zoom branch from baa1f11 to b891efc Compare June 7, 2024 09:26
@mattpap
Copy link
Contributor Author
mattpap commented Jun 7, 2024

@droumis, the new behavior has landed. See the updated example and screen cast.

@mattpap
Copy link
Contributor Author
mattpap commented Jun 7, 2024

Alternatively to subcoorinates_zoom.py example, apply this patch to get grouping by name:

diff --git a/examples/interaction/tools/subcoordinates_zoom.py b/examples/interaction/tools/subcoordinates_zoom.py
index 971831bff2..5f8d6d4f8b 100644
--- a/examples/interaction/tools/subcoordinates_zoom.py
+++ b/examples/interaction/tools/subcoordinates_zoom.py
@@ -4,7 +4,7 @@ from bokeh.core.properties import field
 from bokeh.io import show
 from bokeh.layouts import column, row
 from bokeh.models import (ColumnDataSource, CustomJS, Div, FactorRange,
-                          GroupByModels, HoverTool, Range1d, Switch,
+                          GroupByName, HoverTool, Range1d, Switch,
                           WheelZoomTool, ZoomInTool, ZoomOutTool)
 from bokeh.palettes import Category10
 from bokeh.plotting import figure
@@ -41,15 +41,13 @@ for i, channel in enumerate(channels):
 
     source.data[channel] = data[i]
     line = xy.line(field("time"), field(channel), color=Category10[10][i], source=source, name=channel)
+    line.name = "even" if i % 2 == 0 else "odd"
     renderers.append(line)
 
 level = 1
 hit_test = False
 
-even_renderers = [ r for i, r in enumerate(renderers) if i % 2 == 0 ]
-odd_renderers = [ r for i, r in enumerate(renderers) if i % 2 == 1 ]
-
-behavior = GroupByModels(groups=[even_renderers, odd_renderers])
+behavior = GroupByName()
 
 ywheel_zoom = WheelZoomTool(renderers=renderers, level=level, hit_test=hit_test, hit_test_mode="hline", hit_test_behavior=behavior, dimensions="height")
 xwheel_zoom = WheelZoomTool(renderers=renderers, level=level, hit_test=hit_test, hit_test_mode="hline", hit_test_behavior=behavior, dimensions="width")

@mattpap mattpap force-pushed the mattpap/13728_hit_test_zoom branch from b891efc to 442b796 Compare June 7, 2024 09:33
@droumis
Copy link
Member
droumis commented Jun 7, 2024

NICE! 👏

Code
from bokeh.layouts import column
from bokeh.plotting import figure, curdoc
import numpy as np

from bokeh.core.properties import field
from bokeh.layouts import column, row
from bokeh.models import (ColumnDataSource, CustomJS, Div, FactorRange, HoverTool,
                          Range1d, Switch, WheelZoomTool, ZoomInTool, ZoomOutTool, GroupByModels)
from bokeh.palettes import Category10
from bokeh.plotting import figure

n_eeg_channels = 7
n_pos_channels = 3
n_channels = n_eeg_channels + n_pos_channels
n_seconds = 15

total_samples = 512*n_seconds
time = np.linspace(0, n_seconds, total_samples)
data = np.random.randn(n_channels, total_samples).cumsum(axis=1)
channels = [f"EEG {i}" for i in range(n_eeg_channels)] + [f"POS {i}" for i in range(n_pos_channels)]

hover = HoverTool(tooltips=[
    ("Channel", "$name"),
    ("Time", "$x s"),
    ("Amplitude", "$y μV"),
])

x_range = Range1d(start=time.min(), end=time.max())
y_range = FactorRange(factors=channels)

p = figure(x_range=x_range, y_range=y_range, lod_threshold=None, tools="pan,reset,xcrosshair")

source = ColumnDataSource(data=dict(time=time))
eeg_renderers = []
pos_renderers = []

for i, channel in enumerate(channels):
    is_eeg = channel.startswith('EEG')
    xy = p.subplot(
        x_source=p.x_range,
        y_source=Range1d(start=data[i].min(), end=data[i].max()),
        x_target=p.x_range,
        y_target=Range1d(start=i, end=i + 1),
    )

    source.data[channel] = data[i]
    if is_eeg:
        line = xy.line(field("time"), field(channel), color='black', source=source, name=channel)
        eeg_renderers.append(line)
    else:
        line = xy.line(field("time"), field(channel), color=Category10[10][i], source=source, name=channel)
        pos_renderers.append(line)
    
all_renderers = eeg_renderers + pos_renderers

level = 1
hit_test = True

behavior = GroupByModels(groups=[eeg_renderers, pos_renderers])

ywheel_zoom = WheelZoomTool(renderers=all_renderers, level=level, hit_test=hit_test, hit_test_mode="hline", hit_test_behavior=behavior, dimensions="height")
xwheel_zoom = WheelZoomTool(renderers=all_renderers, level=level, dimensions="width")
zoom_in = ZoomInTool(renderers=all_renderers, level=level, dimensions="height")
zoom_out = ZoomOutTool(renderers=all_renderers, level=level, dimensions="height")

p.add_tools(ywheel_zoom, xwheel_zoom, zoom_in, zoom_out, hover)
p.toolbar.active_scroll = ywheel_zoom

level_switch = Switch(active=level == 1)
level_switch.js_on_change("active", CustomJS(
    args=dict(tools=[ywheel_zoom, zoom_in, zoom_out]),
    code="""
export default ({tools}, obj) => {
    const level = obj.active ? 1 : 0
    for (const tool of tools) {
        tool.level = level
    }
}
"""))

hit_test_switch = Switch(active=hit_test)
hit_test_switch.js_on_change("active", CustomJS(
    args=dict(tool=ywheel_zoom),
    code="""
export default ({tool}, obj) => {
    tool.hit_test = obj.active
}
"""))

layout = column(
    row(Div(text="Zoom sub-coordinates:"), level_switch),
    row(Div(text="Zoom hit-tested:"), hit_test_switch),
    p,
)

curdoc().add_root(layout)
video1253097405.mp4

@mattpap mattpap force-pushed the mattpap/13728_hit_test_zoom branch from 442b796 to 9043e89 Compare June 11, 2024 07:38
@mattpap
Copy link
Contributor Author
mattpap commented Jun 11, 2024

@droumis, @hoxbro, this is not now fully ready for review.

@hoxbro
Copy link
Contributor
hoxbro commented Jun 11, 2024

@mattpap, did you mean now?

@mattpap
Copy link
Contributor Author
mattpap commented Jun 11, 2024

@mattpap, did you mean now?

Yes. I fixed the original comment.

@philippjfr
Copy link
Contributor

This makes sense to me, in particular we're likely just going to be using the list of lists format which gets converted to GroupByModels. I don't have much opinion on GroupByName but would probably have a more generic, GroupByCustomJS instead.

examples/interaction/tools/subcoordinates_zoom.py Outdated Show resolved Hide resolved
examples/interaction/tools/subcoordinates_zoom.py Outdated Show resolved Hide resolved
bokehjs/test/integration/tools/wheel_zoom_tool.ts Outdated Show resolved Hide resolved
examples/interaction/tools/subcoordinates_zoom.py Outdated Show resolved Hide resolved
@mattpap
Copy link
Contributor Author
mattpap commented Jun 12, 2024

The example with improved UX:

Screencast.from.12.06.2024.22.12.50.webm

@hoxbro
Copy link
Contributor
hoxbro commented Jun 13, 2024

The UX looks much better. Can we merge the zoom hit test switch and the select widget? With an option just being No hit test (or something similar).

@mattpap
Copy link
Contributor Author
mattpap commented Jun 13, 2024

The UX looks much better. Can we merge the zoom hit test switch and the select widget? With an option just being No hit test (or something similar).

The current UI reflects the API, so I'm not in favour of this. Maybe different wording instead of "Zoom hit-tested" would suffice, e.g. "Enable hit-testing based zooming"?

@mattpap mattpap merged commit eabfe9d into branch-3.5 Jun 13, 2024
25 checks passed
@mattpap mattpap deleted the mattpap/13728_hit_test_zoom branch June 13, 2024 16:34
@jbednar
Copy link
Contributor
jbednar commented Jun 13, 2024

"Enable hit-tested zooming"?

Chiemezuo pushed a commit to Chiemezuo/bokeh that referenced this pull request Aug 27, 2024
* Add hit_test() method to data renderers

* Add support for hit-test based wheel zooming

* Update subcoordinates_zoom.py example

* Add release notes

* Make the new API more granular

* Implement WheelZoomTool.hit_test_behavior

* Add support for {x,y}crosshair aliases

* Update subcoordinates_zoom.py example

* Add support for grouping hit test behavior

* Add visual integration tests

* Update examples and docstrings

* Add convenience to WheelZoomTool.hit_test_behavior

* Improve UX in subcoordinates_zoom.py example

* Add a comment regarding DELTA magic constant

* Add PlotActions.scroll_{up,down}() convenience methods

* Update wording in the example
Copy link

This pull request has been automatically locked since there has not been any recent activity after it was closed. Please open a new issue for related bugs.

@github-actions github-actions bot locked as resolved and limited conversation to collaborators Oct 25, 2024
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
grant: CZI R5 Funded by CZI Round 5 grant status: accepted
Projects
None yet
Development

Successfully merging this pull request may close these issues.

[FEATURE] Create option for wheel zoom tool to apply only to subplot nearest to the cursor position
5 participants
0