Add drag event to plot

Hi team, I would like to add a drag capability to my plot so when there is a change such as the user dragging the plot right or left then the whole plot and figure resize accordingly. Here is my code so far, am totally lost on this, thanks

from bokeh.plotting import figure, show
from bokeh.models import CustomJS

p = figure(width = 645, height = 300, active_drag='box_select', tools='box_select')
       # do not know how to make tools work or even if this is the right approach
       # trying to add drag capability to right side of figure
p.ellipse(x = 0,y = 0, width = 10, height = 10, fill_color = None, line_width = 2)

callback = CustomJS(
    #args = {'xxx': p}, #pass figure width as an arg somehow?
    code = '''
        //change width of figure dynamically by dragging figure then make figure wider or less wide
        //need to drag right side of figure right or left and shape of corresponding ellipse will also change
        //when xxx is more or less wide then show the updated / resized figure
#source.js_on_change('xxxxxx??', callback) #when figure width is dragged then trigger the callback to resize the figure


I am trying to move this along so I tried the following:

from bokeh.plotting import figure, show
from bokeh.models import CustomJS, PointDrawTool
from bokeh.layouts import column

p = figure(width = 645, height = 300, tools = [])
#p = figure(width = 645, height = 300)
#ptasks = figure(width = 645, height = 300, active_drag='box_select', tools='box_select')
ptasks = figure(width = 645, height = 300, tools='box_select')

#p.ellipse(x = 0,y = 0, width = 10, height = 10, fill_color = None, line_width = 2)

#ptasks = p.ellipse(x = 0,y = 0, width = 10, height = 10, fill_color = None, line_width = 2)
tool = PointDrawTool(renderers=[ptasks], add = False, drag=True)
p.toolbar.active_tap = tool

callback = CustomJS(
    #args = {'xxx': p}, #pass figure width as an arg somehow?
    code = '''
//not sure if I need callback
//trying to drag just right side of figure so ellipse changes shape
#source.js_on_change('xxxxxx??', callback) #when figure width is dragged then trigger the callback to resize the figure


Your best bet here is to use js_on_event with the relevant UI events PanStart , Pan , and PanEnd (these are the “drag” events") and implement the custom logic you need in those callbacks.

“Changing the ellipse shape” could mean a few things, it’s not clear what you are after:

  • changing the data for the ellipse, keeping the range extents and canvas size constant (ellipse itself will change)
  • changing the range extents, keeping the data and canvas size constant (ellipse will scale based on new data extents)
  • changing the canvas (“plot”) size, keeping the data and range extents constant (ellipse will scale to based on new pixel extents)

thanks Bryan, I would like to change the plot size so when the elipse scales, its width property is larger or smaller depending on which way I drag it. I think I need a mousemove event (drag/resize handle). I thought the Pan events move the whole object. I would like just a resizing based on draging the plot right or left so object is resized not moved.

Pan events don’t do anything at all by themselves, they just report the start, stop, and “middles” of a sequence of pan/drag events. They are called “Pan” events because not all platforms are mouse-based and terminology can differ. There is an example on the page I linked that shows the outputs from all the events, including the pan events.

A note regarding resizing the plot. It may or may not work. Bokeh itself used to have a built-in “resize” tool in the early days. [1] It was removed a long time ago as capability grew from “single plots” to “multiple plots in a layout”. There has always been a tension between the “top-down” layout that has to happen inside the plot’s raster canvas, and the “bottom-up” layout outside, for the DOM that the canvas inhabits. The old resize tool just did not work well in that more general context, so it was deprecated and abandoned.

If your use case is very simple, e.g. just a single fixed-size plot in the page, then maybe you can get away with a CustomJS that just updates the plot.width and plot.height. If your situation is more complicated, has multiple plots, embeds in external templates, uses responsive sizing modes, etc. then it is entirely like that something somewhere will not play nice with manual canvas size adjustments.

  1. You can even see it here in this ancient gallery example from almost a decade ago: color_scatter — Bokeh 0.10.0 documentation ↩︎

thanks for the example but it does not resize the whole plot canvas.

my use case is very simple, I want to provide a demo to my students, so when the plot width changes, the whole plot / canvas / object resizes. Ideally I only want a resize handle on right side of canvas so when event mousemove occurs the width value changes and therefore the plot is resized.

in js it is

document.addEventListener("mousemove",mouseMove = function mousMove(e) {
	   	 document.getElementById(elem).style.width = blah, blah

I linked a few things, I am not sure which one you are referring to. I linked an obsolete example for historical interest, that does certainly “resize the whole plot canvas”, and re-scale all the data inside to match. And I referred to a current example the prints out the event data payloads for reference. But printing that data is all it does. You would need to hook up a CustomJS callback to use the data provided by the pan events to set the plot.width and plot.height accordingly.

Edit: more specifically, if you click and drag the plot: you can see the pan event payload in the output, which has things like cursor data and screen coordinates of the event as well as “delta” movement values in each direction.

Note that panning/dragging on the plot also updates the ranges, but that is only because the example happens to add a PanTool to the plot. But that is not required at all, you can omit the PanTool or disable it by default. [1] The underlying pan events are triggered always regardless, and are just event data, that you can respond to however you need (e.g. by updating the plot.width and plot.height based on the event values)

  1. i.e. adding a PanTool to update the ranges was purely an optional “editorial” decision—it seemed like it would be make it more clear that pan events were happening if “something moved” at the same time. ↩︎

BTW adding a “handle” is not going to be completely trivial at the moment. There is in fact an open PR to add initial support for draggable “handles” right now:

Add support for resize and drag handles to `BoxAnnotation` by mattpap · Pull Request #13906 · bokeh/bokeh · GitHub

But this work is not yet in any release.

TBH it’s still very unclear what you are trying to achieve. Why do you want users to need to manually resize plots at all? Generally it would seem preferable to just use a sizing mode like stretch_width and have the plot always just take up a much horizontal space as the viewport allows automatically.

I am trying to demonstrate, starting with an ellipse, that if we resize this object leftwards it eventually becomes a circle. At this point our plot grid is equalized. Once the grid is equalized then I can calculate slopes and angles correctly which is entirely another program I already developed.

X-Y Problem strikes again, I think. Based on this description I would suggest a far simpler approach: Add a slider above or below the plot, and update the plot.width based on the slider value. That’s probably even achievable with js_link and no CustomJS callback at all.

Alternatively, if you really must have draggable interactive handles for resizing, then it’s probably much better to find some existing JS layout framework that offers draggable interactive handles for resizing, and embed Bokeh plots in that framework. With a sizing mode set to stretch_both the Bokeh plot will adapt to the size fo the enclosing DOM element.

Slider idea code:

from bokeh.plotting import column, figure, show
from bokeh.models import Slider

p = figure(width=200, height=400, x_range=(-1, 1), y_range=(-1, 1))
p.ellipse(x=0, y=0, width=0.5, height=0.5)

slider = Slider(start=200, end=600, value=200)
slider.js_link('value', p, 'width')

show(column(p, slider))


this slider idea is a real simple solution.
ps: also in the process I wanted an example that was interactive…forgot to mention.
So this solution solves both (width changing and interactivity)

thanks again !!!

1 Like