jofroe jofroe - 8 days ago 5
Python Question

bokeh server get mouse position

I am developing an interactive app with bokeh (0.12.2) that updates plots based on specific interactions.

For now I use sliders to change positions of a glyph in a plot, but I actually want to access the position of my mouse within a specific plot.

The dataset is a multidimensional matrix (tensor), dense data, and each plot displays one dimension at a specific location. If I change the position of the marker glyph on one plot, the other plots need to be updated, which means I have to slice my dataset according to the updated position.

Here's a simple example I tried to get the mouse data in my bokeh server update function using the hover tool:

from bokeh.plotting import figure, ColumnDataSource
from bokeh.models import CustomJS, HoverTool
from bokeh.io import curdoc

s = ColumnDataSource(data=dict(x=[0, 1], y=[0, 1]))
callback = CustomJS(args=dict(s=s), code="""
var geometry = cb_data['geometry'];
var mouse_x = geometry.x;
var mouse_y = geometry.y;
var x = s.get('data')['x'];
var y = s.get('data')['y'];
x[0] = mouse_x;
y[0] = mouse_y;
s.trigger('change');
""")
hover_tool = HoverTool(callback=callback)
p = figure(x_range=(0, 1), y_range=(0, 1), tools=[hover_tool])
p.circle(x='x', y='y', source=s)


def update():
print s.data

curdoc().add_root(p)
curdoc().add_periodic_callback(update, 1000)


Unfortunately, the server only outputs:


{'y': [0, 1], 'x': [0, 1]}

{'y': [0, 1], 'x': [0, 1]}

{'y': [0, 1], 'x': [0, 1]}

{'y': [0, 1], 'x': [0, 1]}


Is there a way to access the mouse position (in python code)? Even accessing the position of a glyph would be sufficient (because I can change the position of the glyph with some javascript code).




EDIT: So I recently found out that there is this tool_events.on_change() that I could use for this purpose. Unfortunately it does only work for TapTool, LassoSelectTool and BoxSelectTool, not for HoverTool:

from bokeh.plotting import figure
from bokeh.io import curdoc
from bokeh.models.tools import BoxSelectTool, TapTool, HoverTool, LassoSelectTool
from bokeh.models.ranges import Range1d

TOOLS = [TapTool(), LassoSelectTool(), BoxSelectTool(), HoverTool()]
p = figure(tools=TOOLS,
x_range=Range1d(start=0.0, end=10.0),
y_range=Range1d(start=0.0, end=10.0))

def tool_events_callback(attr, old, new):
print attr, 'callback', new

p.tool_events.on_change('geometries', tool_events_callback)
curdoc().add_root(p)


Based on an answer I found here: How can I get data from a ColumnDataSource object which is synchronized with local variables of Bokeh's CustomJS function?. The problem with this solution is that I cannot use pan and trigger the tool_events callback. I can only click (TapTool) or pan and trigger a callback only once (Lasso/BoxSelectTool). I actually wish to trigger such a callback on every mouse move..

Answer

So I recently found out that you can use custom models for this purpose. This means, extending an existing tool, e.g. GestureTool but implementing / overriding your own functions. You need to run a bokeh server (obviously).

$ bokeh serve dir_with_mainfile/

What I am using right now is the following: Create a file MouseMoveTool.py:

from bokeh.models import Tool
class MouseMoveTool(Tool):
    # assuming your models are saved in subdirectory models/
    with open('models/MouseMoveTool.coffee', 'r') as f:
        controls = f.read()
    __implementation__ = controls

Then create MouseMoveTool.coffee:

p = require "core/properties"
GestureTool = require "models/tools/gestures/gesture_tool"

class MouseMoveToolView extends GestureTool.View
     ### Override the _pan function ###
     _pan: (e) ->
        frame = @plot_model.frame
        canvas = @plot_view.canvas

        vx = canvas.sx_to_vx(e.bokeh.sx)
        vy = canvas.sy_to_vy(e.bokeh.sy)
        if not frame.contains(vx, vy)
            return null

        # x and y are your mouse coordinates relative to the axes values
        x = frame.x_mappers.default.map_from_target(vx)
        y = frame.y_mappers.default.map_from_target(vy)

        # update the model's geometry attribute. this will trigger
        # the tool_events.on_change('geometries', ..) callback
        # in your python code.
        @plot_model.plot.tool_events.geometries = [{x:x, y:y}]

class MouseMoveTool extends GestureTool.Model
    default_view: MouseMoveToolView
    type: "MouseMoveTool"

    tool_name: "Mouse Move Tool"
    icon: "bk-tool-icon-pan"
    event_type: "pan"
    default_order: 13

module.exports =
    Model: MouseMoveTool
    View: MouseMoveToolView

After that you can use your tool in your main.py program:

from models.MouseMoveTool import MouseMoveTool
p = figure(plot_width=300, plot_height=300, tools=[MouseMoveTool()])
p.tool_events.on_change('geometries', on_mouse_move)

def on_mouse_move(attr, old, new):
    print new[0] # will print {x:.., y:..} coordinates