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.
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)
Thanks man!
can you also suggest the code to move the line right/left? Maybe we can do this on the event doubletap?
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))
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.
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?