Drag and drop rectangles along y axis

Hello everyone,

First of all, I beg your indulgence
I’ve been struggling with this problem for a few weeks, I haven’t found a way to get what I want.

I’m trying to drag and drop rectangles along the oy axis using TapTool (but another would be nice). I’ve tried to adapt this case
https://discourse.bokeh.org/t/solution-drag-and-drop-for-labels-and-etc/4239/1,
without success.

I’m just starting to learn js with the examples provided here and there.

My example code is as follows:

from bokeh.plotting import figure, show, ColumnDataSource
import pandas as pd
import datetime
pd.options.mode.chained_assignment = None  # default='warn'
import numpy as np
from bokeh.io import curdoc
from bokeh.models import DatetimeTickFormatter, HoverTool, TapTool
from bokeh.layouts import Column, Row
#
#
TaskDF = pd.DataFrame({
	'SKILL':[1,7,11,12,13,19,2,4,5,9,14,15,17,18,23,3,6,8,10,16,20,21,22],
	'TYPE':["A","B","C","D","C","C","A","B","B","B","C","D","C","C","A","A",
	"B","B","C","C","E","E","A"],
	'GROUPE':[1,1,1,1,1,1,2,2,2,2,2,2,2,2,2,3,3,3,3,3,3,3,3],
	'STARTTIME':['03/04/2024 19:35','03/04/2024 17:40','03/04/2024 16:05','03/04/2024 15:10',
	'03/04/2024 20:15','03/04/2024 14:05','03/04/2024 20:30','03/04/2024 15:10',
	'03/04/2024 17:24','03/04/2024 13:35','03/04/2024 16:25','03/04/2024 16:25',
	'03/04/2024 19:40','03/04/2024 18:40','03/04/2024 13:00','03/04/2024 20:30',
	'03/04/2024 16:25','03/04/2024 19:10','03/04/2024 13:05','03/04/2024 18:10',
	'03/04/2024 13:55','03/04/2024 14:45','03/04/2024 14:25'],
	'ENDTIME':['03/04/2024 20:05','03/04/2024 19:10','03/04/2024 16:45','03/04/2024 16:15',
	'03/04/2024 20:55','03/04/2024 14:45','03/04/2024 21:00','03/04/2024 16:10',
	'03/04/2024 18:25','03/04/2024 14:57','03/04/2024 17:10','03/04/2024 16:55',
	'03/04/2024 20:15','03/04/2024 19:25','03/04/2024 13:30','03/04/2024 21:00',
	'03/04/2024 17:45','03/04/2024 20:25','03/04/2024 13:45','03/04/2024 18:45',
	'03/04/2024 14:20','03/04/2024 16:15','03/04/2024 14:45']
	}
	)

Hshift = 40

TaskDF['STARTTIME']    = pd.to_datetime(TaskDF['STARTTIME'], format="%d/%m/%Y %H:%M")
TaskDF['ENDTIME']    = pd.to_datetime(TaskDF['ENDTIME'], format="%d/%m/%Y %H:%M")
TaskDF['XCENTER']    = TaskDF[['STARTTIME','ENDTIME']].apply(lambda x: x['STARTTIME'] + 
	(x['ENDTIME'] - x['STARTTIME'])/2, axis=1)
TaskDF['WIDTH']      = TaskDF[['STARTTIME','ENDTIME']].apply(lambda x: x['ENDTIME'] - 
	x['STARTTIME'], axis=1)
#
TaskDF['TYPE']  = TaskDF['TYPE'].apply(lambda x: 'lightgray' if str(x)=='D'
	else ('aquamarine' if str(x)=='C' else ('gold' if str(x)=='E'
		else ('plum' if str(x)=='A' else ('coral')))))
#
TaskDF['HEIGHT']  = Hshift-6
#
dayformater={
    '@Time'        : 'datetime',}
#
MaxY = 2*Hshift*len(TaskDF['GROUPE'].unique())
TaskDF['YCENTER'] = np.linspace(1,MaxY,num=len(TaskDF['GROUPE']), endpoint=True)
#
TaskSRC = ColumnDataSource(data=TaskDF)
#
plot = figure(height=int(MaxY), width=750, x_axis_type = "datetime", tools=['tap,reset,box_select'])

ptasks = plot.rect(x='XCENTER', y='YCENTER', width='WIDTH', height='HEIGHT', 
	color="TYPE", source=TaskSRC, alpha=0.5, border_radius=5, selection_color="firebrick",)

plot.xaxis.major_label_orientation = np.pi/4
plot.xaxis.ticker.desired_num_ticks = 20
plot.xaxis.formatter = DatetimeTickFormatter(hours='%Hh %M')

curdoc().add_root(plot)

Thanks for your help.

Translated with DeepL.com (free version)

Hi,

I’ve been able to make some progress since opening this case.
By adding the PointDrawTool tool with add argument to False,

tool = PointDrawTool(renderers=[ptasks], add = False, drag=True)
plot.add_tools(tool)
plot.toolbar.active_tap = tool

I can select a rectangle and move it. All I need to do now is constrain the movement to the y axis only.

Does anyone have any ideas on how to do this?

thanks

The edit tools are geared towards specific modes of usage, and there are not currently any options e.g. to constrain editing to a single dimension. Your best bet might actually be to use js_on_event with the relevant UI events PanStart, Pan, and PanEnd and implement the custom logic you need in those callbacks.

Thanks Bryan for your indications, I feel that the solution is close but as in this example I’m diverting bokeh from what it’s intended for (dataviz) I’m making slow progress.
I’ve changed the GROUP column to category and updated the figure, added a callback and replaced PointDrawTool with TapTool.
Here’s the new clean code where I’ve left out the data from the dataframe.

TaskDF['STARTTIME']    = pd.to_datetime(TaskDF['STARTTIME'], format="%d/%m/%Y %H:%M")
TaskDF['ENDTIME']    = pd.to_datetime(TaskDF['ENDTIME'], format="%d/%m/%Y %H:%M")
TaskDF['XCENTER']    = TaskDF[['STARTTIME','ENDTIME']].apply(lambda x: x['STARTTIME'] + 
	(x['ENDTIME'] - x['STARTTIME'])/2, axis=1)
TaskDF['WIDTH']      = TaskDF[['STARTTIME','ENDTIME']].apply(lambda x: x['ENDTIME'] - 
	x['STARTTIME'], axis=1)
#
TaskDF['TYPE']  = TaskDF['TYPE'].apply(lambda x: 'lightgray' if str(x)=='D'
	else ('aquamarine' if str(x)=='C' else ('gold' if str(x)=='E'
		else ('plum' if str(x)=='A' else ('coral')))))
#
TaskSRC = ColumnDataSource(data=TaskDF)
#
plot = figure(y_range=TaskDF['GROUPE'].unique(), width=750, x_axis_type = "datetime", tools=['yzoom_in,reset'])

ptasks = plot.rect(x='XCENTER', y='GROUPE', width='WIDTH', height=0.2, 
	color="TYPE", source=TaskSRC, alpha=0.5, border_radius=5, selection_color="firebrick",)

callbackjs = CustomJS(
	args = {'source': TaskSRC}, code ="""
		var ev_name = cb_obj.event_name
		var data = source.data;
		var sel_idx = source.selected.indices
		if(ev_name == 'pan' && sel_idx[0] != undefined){
		var yD = parseInt(cb_obj.y);
		data['GROUPE'][sel_idx[0]] = yD-0.5
		source.data = data
		source.change.emit()
		}
    """)

tap_tool = TapTool(renderers=[ptasks], behavior = 'select', mode = 'replace')
plot.add_tools(tap_tool)
plot.toolbar.active_tap = tap_tool

plot.js_on_event(events.Tap, callbackjs)
plot.js_on_event(events.Pan, callbackjs)
plot.js_on_event(events.PanEnd, callbackjs)
plot.js_on_event(events.PanStart, callbackjs)

plot.xaxis.major_label_orientation = np.pi/4
plot.xaxis.ticker.desired_num_ticks = 40
plot.xaxis.formatter = DatetimeTickFormatter(hours='%Hh %M')

curdoc().add_root(plot)

Moving from one level to another works well.

Now I just need to find a way to make the movement gradual (so that it looks like the square you’re moving is moving with the mouse).