После некоторой борьбы я пришел к этому обратному вызову JS, который временно меняет шаг на 1 день, чтобы иметь возможность исправить дату. Он также временно изменяет диапазон, так что при восстановлении шага ручка ползунка остается на своем месте. Совсем не идеально, но работает:
import pandas as pd
from bokeh.plotting import show
from bokeh.models import CustomJS, DateRangeSlider
data = {'date_start': ['201812', '201901', '201902', '201903', '201904', '201905', '201906', '201907', '201908', '201909', '201910', '201911'],
'date_end': [ '201901', '201902', '201903', '201904', '201905', '201906', '201907', '201908', '201909', '201910', '201911', '201912'],
'values' : [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]}
df = pd.DataFrame(data)
df['Start'] = pd.to_datetime(df['date_start'], format='%Y%m')
df['End'] = pd.to_datetime(df['date_end'], format='%Y%m')
start_date = df['Start'].min()
end_date = df['End'].max()
range_slider = DateRangeSlider(start=start_date, end=end_date, value=(start_date, end_date), step=31*24*60*60*1000, title="Date Range", callback_policy = 'mouseup', tooltips = False, width=600)
code = '''
console.log('start, end', cb_obj.start, cb_obj.end)
for (i in cb_obj.value) {
if (getDay(cb_obj.value[i]) != 1) {
correctDate(day, i)
}
}
function getDay(value) {
date = new Date(value)
str_date = date.toString()
day = str_date.split(' ')[2]
return Number(day)
}
function correctDate(day, side) {
if (day < 15) {
console.log('day < 15')
difference = day - 1
difference_milliseconds = -1 * difference*24*60*60*1000
}
else {
console.log('day >= 15')
difference = 0
new_day = -1
while(new_day != 1) {
difference_milliseconds = difference*24*60*60*1000
new_date = new Date(cb_obj.value[0] + difference_milliseconds)
new_day = Number(new_date.getDate())
difference += 1
}
}
cb_obj.step = 1*24*60*60*1000 // set slider step to 1 day to be able to correct
if (side == 0) {
cb_obj.start = cb_obj.start + difference_milliseconds
cb_obj.value = [cb_obj.value[0] + difference_milliseconds, cb_obj.value[1]]
}
else if (side == 1) {
cb_obj.end = cb_obj.end + difference_milliseconds + 4*24*60*60*1000
cb_obj.value = [cb_obj.value[0], cb_obj.value[1] + difference_milliseconds]
}
setTimeout(resetStep, 50, cb_obj) // reset step to 31 days
}
function resetStep(cb_obj) {
cb_obj.step = 31*24*60*60*1000
}
'''
range_slider.js_on_change('value_throttled', CustomJS(args = {'end_date': end_date}, code=code))
show(range_slider)
Или, возможно, лучший вариант - не использовать DateRangeSlider
вообще для шага месяца. В приведенном ниже решении используется RangeSlider
в сочетании с Div
для реализации той же функциональности, которая выглядит намного лучше:
import pandas as pd
from bokeh.plotting import show
from bokeh.models import RangeSlider, Div, Column, CustomJS
data = {'date_start': ['2018-12', '2019-01', '2019-02', '2019-03', '2019-04', '2019-05', '2019-06', '2019-07', '2019-08', '2019-09', '2019-10', '2019-11'],
'date_end': [ '2019-01', '2019-02', '2019-03', '2019-04', '2019-05', '2019-06', '2019-07', '2019-08', '2019-09', '2019-10', '2019-11', '2019-12'],
'values' : [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]}
df = pd.DataFrame(data)
df['Start'] = pd.to_datetime(df['date_start'], format='%Y-%m')
df['End'] = pd.to_datetime(df['date_end'], format='%Y-%m')
number_dates = len(list(df.date_start.unique()))
start_dates = df.date_start.to_list()
end_dates = df.date_end.to_list()
range_slider = RangeSlider(start=0, end=number_dates, value=(0, number_dates), step=1, title="", callback_policy = 'mouseup', tooltips = False, width=600, show_value = False)
div = Div(text = "Date Range: <b>" + str(start_dates[range_slider.value[0]]) + ' . . . ' + str(end_dates[range_slider.value[1]-1]) + '</b>', render_as_text = False, width = 575)
code = '''
range = Math.round(Number(cb_obj.value[1] - cb_obj.value[0]), 10)
range = range < 10 ? '0' + range : range
div.text = "Date Range: <b>" + start_dates[Math.round(cb_obj.value[0], 10)] + ' . . . ' + end_dates[Math.round(cb_obj.value[1], 10) + -1] + '</b>'
'''
range_slider.js_on_change('value_throttled', CustomJS(args = {'div': div, 'start_dates': start_dates, 'end_dates': end_dates}, code=code))
show(Column(div, range_slider))