How to drag a plotted line?

I have plotted a line with some defined x and y-axis points. Can I make the chart such that I can drag this line up/down interactively?

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.

1 Like

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)