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

Make tools active state updateable #4567

Closed
birdsarah opened this issue Jun 18, 2016 · 19 comments · Fixed by #10095
Closed

Make tools active state updateable #4567

birdsarah opened this issue Jun 18, 2016 · 19 comments · Fixed by #10095

Comments

@birdsarah
Copy link
Member

birdsarah commented Jun 18, 2016

#4522 offers the ability to set tools that you want active on initialization.

Would be nice to have the ability to programmatically continue changing the active state of tools

@hensh2ss
Copy link

hensh2ss commented Jan 16, 2018

@bryevdv @birdsarah, I was wondering what the status was on implementing this ticket? I am trying to implement a brushing actions using the bokeh.events module but need to be able to programmatically turn on/off the active drag tool in the toolbar. Any help you can provide would be much appreciated!!

Here is an example of what I am trying to do

import time
from bokeh.events import Pan
from bokeh.io import curdoc
from bokeh.layouts import row
from bokeh.models import ColumnDataSource, BoxAnnotation, CheckboxGroup, WidgetBox, PanTool
import numpy as np
from datetime import datetime
from bokeh.plotting import figure

# Setting up the initial Data Source
N = 200
st = time.time()
t = [datetime.utcfromtimestamp(st+i) for i in xrange(N)]
x = np.linspace(0, 4*np.pi, N)
y = np.sin(x)
source = ColumnDataSource(data=dict(t=t, x=x, y=y))

# Setting up the controls
brushEnable = CheckboxGroup(labels=["Spectral Brush"])
brush = BoxAnnotation(fill_alpha=0.3, fill_color='red')
controlLayout = WidgetBox(brushEnable)

# Set up plot
panTool = PanTool()
plot = figure(plot_height=400, plot_width=400, title="my sine wave",
              tools="crosshair,reset,save,wheel_zoom",
              y_range=[-2.5, 2.5], x_axis_type="datetime"
              )
plot.add_tools(panTool)
plot.line('t', 'y', source=source, line_width=3, line_alpha=0.6)

# Connecting controls to plot
plot.add_layout(brush)

#Adding Event Handlers
def brushEnableChange(events):
    print("Brush enable changed",events)
    if events[1] == 1:
        plot.toolbar.active_drag = None
    else:
        plot.toolbar.active_drag = panTool


brushEnable.on_click(brushEnableChange)

# TODO Implement this method to change the BoxAnnotation
def panOngoing(events):
    print("\tPan ongoing", events)

plot.on_event(Pan, panOngoing)

curdoc().add_root(row(controlLayout, plot))
curdoc().title = "Event Brush Example"

And here is the error I am getting:
screen shot 2018-01-16 at 8 49 52 am

@bryevdv
Copy link
Member

bryevdv commented Mar 8, 2019

noting that the correct code for the callback is

def brushEnableChange(events):
    print("Brush enable changed", events)
    if events == [0]:
        plot.toolbar.active_drag = None
    else:
        plot.toolbar.active_drag = panTool

Tho still appears the plumbing is not hooked up

@cvdelannoy
Copy link

cvdelannoy commented Jan 20, 2020

Indeed Python-side changes to active_scroll (or active_drag etc.) attribute didn't work for me either. Then I tried changing what I assumed was the corresponding JS attribute, but nothing happens:

window.Bokeh.documents[0]._all_models_by_name._dict['ts'][0].toolbar.attributes.active_scroll = window.Bokeh.documents[0]._all_models_by_name._dict['ts'][0].toolbar.attributes.tools[2]

Are there any other workarounds I could try for now?

@bryevdv
Copy link
Member

bryevdv commented Jan 22, 2020

No the issue is missing event plumbing, there is not going to be any obvious or simple way to work around

@p-himik
Copy link
Contributor

p-himik commented Jan 22, 2020

That's what I use in one of my projects:

def set_tools(plot, tools, active_scroll=None, active_drag=None):
    # Bokeh processes only the `tools` change, so
    # `active_scroll` *has* to be set first.
    plot.toolbar.active_scroll = active_scroll
    plot.toolbar.active_drag = active_drag
    plot.toolbar.tools = tools

@chrisbarber
Copy link
Contributor

chrisbarber commented May 29, 2020

I was using this to change tools on the javascript side, and it was working

var bokeh_box_select_tool_button = document.getElementsByClassName("bk-tool-icon-box-select")[0];
bokeh_box_select_tool_button.click();

But now I am on bokeh master and it no longer works; haven't figured out why.

@chrisbarber
Copy link
Contributor

chrisbarber commented May 30, 2020

I don't know how we've gotten to the point in web front-end where we can't just .click() an element. I guess this more a comment on hammerjs.

I'm tempted to do this:

diff --git a/bokehjs/src/lib/models/tools/button_tool.ts b/bokehjs/src/lib/models/tools/button_tool.ts
index 84ae26044..0f81d5fcd 100644
--- a/bokehjs/src/lib/models/tools/button_tool.ts
+++ b/bokehjs/src/lib/models/tools/button_tool.ts
@@ -42,6 +42,7 @@ export abstract class ButtonToolButtonView extends DOMView {
       inputClass: Hammer.TouchMouseInput, // https://github.com/bokeh/bokeh/issues/9187
     })
     this.connect(this.model.change, () => this.render())
+    this.model.tools.forEach((t) => t.click = () => this._clicked());
     this._hammer.on("tap", (e) => {
       if (this._menu?.is_open) {
         this._menu.hide()

Then I can do this:

var bokeh_box_select_tool_button = Bokeh.documents[0].get_model_by_name('box_select_tool');
bokeh_box_select_tool_button.click()

Would it be unreasonable to provide a click method on the various *Tool models in bokehjs like this? Sure, maybe there is a better place to register this method on to the tool than my diff above, but it any case it should be just a one-liner like this. Whereas the "missing event plumbing" sounds a bit more involved.

@p-himik
Copy link
Contributor

p-himik commented May 30, 2020

click does not belong on a model. And tool models already have the active property.
This works just fine:

from bokeh.io import show
from bokeh.layouts import column, row
from bokeh.models import Button, CustomJS, WheelZoomTool, BoxZoomTool
from bokeh.plotting import figure

p = figure(tools='wheel_zoom,box_zoom')
p.x(0, 0)

bb = Button(label='box')
bb.js_on_click(CustomJS(args=dict(t=p.select_one(BoxZoomTool)),
                        code="t.active = !t.active;"))
bw = Button(label='wheel')
bw.js_on_click(CustomJS(args=dict(t=p.select_one(WheelZoomTool)),
                        code="t.active = !t.active;"))

show(column(p, row(bb, bw)))

Given this example, I'm now wondering if the ability to change toolbar.active_* properties is even that useful.

@mattpap
Copy link
Contributor

mattpap commented May 30, 2020

Given this example, I'm now wondering if the ability to change toolbar.active_* properties is even that useful.

At least for the sake of consistency it is, but realistically the code you proposed is the way to go. I went ahead and hooked up appropriate signals in PR #10095.

@chrisbarber
Copy link
Contributor

chrisbarber commented May 30, 2020

If #4567 (comment) works, the shouldn't this work as well, run from the javascript console?

> var box_select_tool = Bokeh.documents[0].get_model_by_name('box_select_tool')
> box_select_tool
BoxSelectTool {_subtype: undefined, document: Document, destroyed: Signal0, change: Signal0, transformchange: Signal0, …}
> box_select_tool.active = !box_select_tool.active

For me this has no effect. I am sure it is the right box select tool because it does affect the correct button when I call my hacked-in click() method on it.

click does not belong on a model.

That makes sense. But where would it belong then? On a *View? I am not familiar with this codebase but saw things like *Views and *Proxys all over. A view sounds logical to me coming from the outside. But how do I obtain a view to interact with? Or if I'm not supposed to interact with a view... then where is some place where a click method could go that I also could interact with from the outside of Bokeh on the js side?

@mattpap
Copy link
Contributor

mattpap commented May 30, 2020

If #4567 (comment) works, the shouldn't this work as well, run from the javascript console?

This works fine, at least for me. What version of bokehjs do you use, what browser? Perhaps there's an unrelated bug.

@p-himik
Copy link
Contributor

p-himik commented May 30, 2020

shouldn't this work as well, run from the javascript console?

This works fine, at least for me

It should not work on the latest master because of https://github.com/bokeh/bokeh/pull/9994/files#diff-cb2f7b44ff7ff368e48c778f75cf0484R45-R54

Update: huh, GitHub doesn't select the lines even though the URL contains the info. Anyway, the lines of interest start with this._hammer.on("tap", (e) => {.

@p-himik
Copy link
Contributor

p-himik commented May 30, 2020

@chrisbarber To answer all of your questions at once - don't do it that way. What's wrong with my code snippet?

@chrisbarber
Copy link
Contributor

chrisbarber commented May 30, 2020

Okay yeah I am on master as of yesterday or something (2.0.0-187-ge9f1962cb-dirty)

@chrisbarber To answer all of your questions at once - don't do it that way. What's wrong with my code snippet?

Maybe nothing; they just seemed like they ought to be equivalent. It's a bit hard to understand what way is the right way. I'm fine with some short term ambiguity on what the right way is though. I might just be rushing a bit here; it was easier to test my way in the console versus setting things up with your snippet. Sorry for the noise; I'll revisit this when I have some more time to test the various options.

@p-himik
Copy link
Contributor

p-himik commented May 30, 2020

The right way is always by changing Bokeh models. Well, maybe something else if it's described in the documentation. Anything else relies on implementation details and can break unexpectedly with even a minor update, as it was in your case.

Sorry for the noise

Well, this "noise" made me come up with that example, so it was useful after all. :)

@chrisbarber
Copy link
Contributor

chrisbarber commented Jun 2, 2020

Okay a bit more time to test; it looks like in my case the problem is I am doing this with a gridplot. I know that merging of tools (merge_tools=False) might simplify the matter but in my application I really do want the merged tools.

I did some poking around and was hoping to find either a) that one of the originating tool objects becomes the "master" and can control the others or b) that a new tool object is created which can be found and then used as desired.

I see neither happening. The original Tool models remain but changing their active property does not have any effect. Then I look for other Tool models on the document and I see that the original ones still exist, and there is no magical 3rd one created by the gridplot tool merging process. I guess next the question would be then: how is the merging being done and does it provide any means to access Tool models which actually reflect the tied/merged tools visible in the UI.

Example extended from #4567 (comment) with which to experiment:

from bokeh.io import curdoc
from bokeh.layouts import column, row, gridplot
from bokeh.models import Button, CustomJS, WheelZoomTool, BoxZoomTool
from bokeh.plotting import figure

p1 = figure(tools='wheel_zoom,box_zoom')
p1.x(0, 0)
p2 = figure(tools='wheel_zoom,box_zoom')
p2.x(0, 0)

print(p1.select_one(BoxZoomTool), p2.select_one(BoxZoomTool))
print(p1.select_one(WheelZoomTool), p2.select_one(WheelZoomTool))

gp = gridplot([[p1, p2]])

print(p1.select_one(BoxZoomTool), p2.select_one(BoxZoomTool))
print(p1.select_one(WheelZoomTool), p2.select_one(WheelZoomTool))

bb1 = Button(label='box 1')
bb1.js_on_click(CustomJS(args=dict(t=p1.select_one(dict(type=BoxZoomTool))),
                         code="t.active = !t.active;"))
bw1 = Button(label='wheel 1')
bw1.js_on_click(CustomJS(args=dict(t=p1.select_one(dict(type=WheelZoomTool))),
                        code="t.active = !t.active;"))

bb2 = Button(label='box 2')
bb2.js_on_click(CustomJS(args=dict(t=p2.select_one(dict(type=BoxZoomTool))),
                         code="t.active = !t.active;"))
bw2 = Button(label='wheel 2')
bw2.js_on_click(CustomJS(args=dict(t=p2.select_one(dict(type=WheelZoomTool))),
                        code="t.active = !t.active;"))

print(list(t.id for t in gp.select(dict(type=BoxZoomTool))))
print(list(t.id for t in gp.select(dict(type=WheelZoomTool))))

curdoc().add_root(column(gp, row(bb1, bw1, bb2, bw2)))

print(list(t.id for t in curdoc().select(dict(type=BoxZoomTool))))
print(list(t.id for t in curdoc().select(dict(type=WheelZoomTool))))

@chrisbarber
Copy link
Contributor

chrisbarber commented Jun 2, 2020

I see that the tools are collected together into a ProxyToolbar. I would sort of expect this to be the most likely example to work then, if that proxy thing is just broadcasting state changes to all the tools inside it. Just changes the active state in synchrony between the two tools.

from bokeh.io import curdoc
from bokeh.layouts import column, row, gridplot
from bokeh.models import Button, CustomJS, WheelZoomTool, BoxZoomTool
from bokeh.plotting import figure

p1 = figure(tools='wheel_zoom,box_zoom')
p1.x(0, 0)
p2 = figure(tools='wheel_zoom,box_zoom')
p2.x(0, 0)

gp = gridplot([[p1, p2]])

bb = Button(label='box')
bb.js_on_click(CustomJS(args=dict(t1=p1.select_one(dict(type=BoxZoomTool)),
                                  t2=p2.select_one(dict(type=BoxZoomTool))),
                        code="t1.active = !t1.active;t2.active = t1.active;"))
bw = Button(label='wheel')
bw.js_on_click(CustomJS(args=dict(t1=p1.select_one(dict(type=WheelZoomTool)),
                                   t2=p2.select_one(dict(type=WheelZoomTool))),
                         code="t1.active = !t1.active;t2.active = t1.active;"))

curdoc().add_root(column(gp, row(bb, bw)))

@p-himik
Copy link
Contributor

p-himik commented Jun 2, 2020

It works the other way around - when the active state of a proxy tool changes, then all proxied tools are changed. Given that, what you want to achieve is not trivial and definitely requires a lot more of JS code and using some implementation details.

@chrisbarber
Copy link
Contributor

chrisbarber commented Jun 2, 2020

It works the other way around - when the active state of a proxy tool changes, then all proxied tools are changed. Given that, what you want to achieve is not trivial and definitely requires a lot more of JS code and using some implementation details.

I was able to find the proxy objects and determine their tool type and set .active on them. I opened a feature request before I figured this out (here #10107). For my application I am content to just have this short snippet of JS that is somewhat dependent on bokeh internals. If this bit breaks in a future release I can solve the issue again. But I left the feature request open and edited it to simplify it and include this snippet, and maybe I can chip in if it makes sense to pursue it.

Thanks for the pointers; this was not so bad after all.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging a pull request may close this issue.

8 participants