☰Read-only archive⌕
Looking for help with Bokeh?

This is a read-only archive. Ask questions and join the conversation on GitHub.

Visit Community Support on GitHub →
Archived discussions · Captured October 28, 2026

How to drag a plotted line?

Community Support
Bryan
Bryan

Your question is a bit vague—do you want to drag the entire line at once? Or be able to “edit” individual points on the line?

If the latter, there is a PointEditTool you can easily add.

If the former, there’s not any good solution I can think of. There is a Drag event you could add a callback for but that will trigger anywhere on the canvas, not just if the line is hit. The callback for the drag event could do its own manual hit-testing, of course, but that’s some amount of work.

⌁   Link to post
gibsms
gibsms

The code below should do what you have asked. Simply tap the mouse over the line, move the mouse up or down and tap the mouse again to end the move. The code divides a line into segments and calculates the distance from the mouse point to the closest point on each line segment when you click on the plot (tap event). If the distance is less than the specified snap distance, the X, Y coordinates of the line will update when you move the mouse up or down. If the distance is greater than the specified snap distance, nothing will happen. You will need to adjust the snap distance to suit your plot. You could also make the snap distance dynamic and set it as a percentage of the plot dimensions.

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

output_file("linedrag.html")

x = [1, 2, 3, 4, 5, 6, 7, 8]
y = [6, 7, 8, 7, 9, 5, 6, 7]

source = ColumnDataSource(data=dict(x=x, y=y))

p = figure(x_range=(0, 10), y_range=(0, 10), plot_width=600, plot_height=600)

# add both a line and circles on the same plot
p.line('x', 'y', source = source, line_width=2)

callback_tap = '''

if (true === Bokeh.drag) {
    Bokeh.drag = false;
    console.log("drag line complete");
}

else {
    Bokeh.mouse_y_old = cb_obj.y;

    var snap_distance = 0.2;

    var pts_x = source.data['x'];
    var pts_y = source.data['y'];

    for (var i=0;i<pts_x.length; i++){               
        if (i == pts_x.length -1){
            break;
        }    
        else {
            // calculate distance to closest point on line segments
            var A = cb_obj.x - pts_x[i];
            var B = cb_obj.y - pts_y[i]; 
            var C = pts_x[i + 1] - pts_x[i];
            var D = pts_y[i + 1] - pts_y[i];

            var dot = A * C + B * D;
            var len_sq = C * C + D * D;
            var param = -1;
                
            if (len_sq != 0) { //in case of 0 length line
                param = dot / len_sq;
            }
            var xx, yy;

            if (param < 0) {
                xx = pts_x[i];
                yy = pts_y[i];
            }
           
            else if (param > 1) {
                xx = pts_x[i + 1];
                yy = pts_y[i + 1];
            }
        
            else {
                xx = pts_x[i] + param * C;
                yy = pts_y[i] + param * D;
            }

            var dx = cb_obj.x - xx;
            var dy = cb_obj.y - yy;
            var distance_segment = Math.sqrt(dx * dx + dy * dy);

            if (distance_segment < snap_distance) {
                Bokeh.drag = true;
                break;
            }    
        }       
    }
}'''

callback_mousemove = '''
if (Bokeh.drag) {  
    var y_change = Bokeh.mouse_y_old - cb_obj.y;
    Bokeh.mouse_y_old = cb_obj.y
    var pts_y = source.data['y'];
    for (var i=0;i<pts_y.length; i++){
        source.data['y'][i] = source.data['y'][i] - y_change;
    }
    source.change.emit();
}'''

p.js_on_event('tap', CustomJS(args = {'source': source}, code = callback_tap))
p.js_on_event('mousemove', CustomJS(args = {'source': source}, code = callback_mousemove))

show(p)
⌁   Link to post
gibsms
gibsms

Hi
DoubleTap would work. Adding a RadioButtonGroup to let the user select a vertical or horizontal move is another option (see code below).

from bokeh.models import ColumnDataSource, CustomJS, RadioButtonGroup, Column
from bokeh.plotting import figure, output_file, show

output_file("linedrag.html")

x = [1, 2, 3, 4, 5, 6, 7, 8]
y = [6, 7, 8, 7, 9, 5, 6, 7]

source = ColumnDataSource(data=dict(x=x, y=y))

# Setup plot
p = figure(x_range=(0, 10), y_range=(0, 10), plot_width=900, plot_height=600)
p.line('x', 'y', source = source, line_width=2)

# Setup radio button group
LABELS = ["Vertical", "Horizontal"]
radio_button_group = RadioButtonGroup(labels=LABELS, active=0, width=300)

# Setup line drag callbacks
callback_tap = '''

if (true === Bokeh.drag) {
    Bokeh.drag = false;
    console.log("drag line complete");
}

else {
    Bokeh.mouse_y_old = cb_obj.y;
    Bokeh.mouse_x_old = cb_obj.x;

    var snap_distance = 0.2;

    var pts_x = source.data['x'];
    var pts_y = source.data['y'];

    for (var i=0;i<pts_x.length; i++){               
        if (i == pts_x.length -1){
            break;
        }    
        else {
            // calculate distance to closest point on line segments
            var A = cb_obj.x - pts_x[i];
            var B = cb_obj.y - pts_y[i]; 
            var C = pts_x[i + 1] - pts_x[i];
            var D = pts_y[i + 1] - pts_y[i];

            var dot = A * C + B * D;
            var len_sq = C * C + D * D;
            var param = -1;
                
            if (len_sq != 0) { //in case of 0 length line
                param = dot / len_sq;
            }
            var xx, yy;

            if (param < 0) {
                xx = pts_x[i];
                yy = pts_y[i];
            }
           
            else if (param > 1) {
                xx = pts_x[i + 1];
                yy = pts_y[i + 1];
            }
        
            else {
                xx = pts_x[i] + param * C;
                yy = pts_y[i] + param * D;
            }

            var dx = cb_obj.x - xx;
            var dy = cb_obj.y - yy;
            var distance_segment = Math.sqrt(dx * dx + dy * dy);

            if (distance_segment < snap_distance) {
                Bokeh.drag = true;
                break;
            }    
        }       
    }
}'''

callback_mousemove = '''
if (Bokeh.drag) {  
    if (radio_button_group.active === 0) {
        var y_change = Bokeh.mouse_y_old - cb_obj.y;
        Bokeh.mouse_y_old = cb_obj.y
        var pts_y = source.data['y'];
        for (var i=0;i<pts_y.length; i++){
            source.data['y'][i] = source.data['y'][i] - y_change;
        }
        source.change.emit();
    }
    if (radio_button_group.active === 1) {
        var x_change = Bokeh.mouse_x_old - cb_obj.x;
        Bokeh.mouse_x_old = cb_obj.x
        var pts_x = source.data['x'];
        for (var i=0;i<pts_x.length; i++){
            source.data['x'][i] = source.data['x'][i] - x_change;
        }
        source.change.emit();
    }       
}'''

p.js_on_event('tap', CustomJS(args = {'source': source, 'radio_button_group' : radio_button_group}, code = callback_tap))
p.js_on_event('mousemove', CustomJS(args = {'source': source, 'radio_button_group' : radio_button_group}, code = callback_mousemove))

show(Column(radio_button_group, p))
⌁   Link to post
gibsms
gibsms

I’m assuming you always want the line to be moved in either a fixed horizontal or vertical direction only. However if that isn’t important, the code can easily be modified to move the line in any direction you wanted after taping on it.

⌁   Link to post
Umang_Garg
Umang_Garg

Hi! Thanks for the code.

I actually don’t want radio_button_group. Because there are other things in the chart and the app as well.
It’s better if we can move the line vertically after single tap and if we doubleclick on the chart, it allows us to move it horizontally (or something like this). Can you edit this code so that it moves horizontally on doubletap?

⌁   Link to post

End of archived discussion · Replies are disabled