Bokeh DateRangeSlider
小部件的int
属性需要step
值,该值必须是毫秒级的时间值。将步长设置为seconds
,minutes
,hours
,days
或years
时,效果很好。但是我需要滑块上的month分辨率。
当步长设置为31天时,它对于开始日期一直有效,直到3月,我得到的是1 March
而不是4 March
。然后,显示值从每月的1号开始变大。
我希望能够设置并显示两侧的滑块范围始终为每月的第一天,例如1 March
,1 April
,1 May
,1 June
等...就像在DataFrame
中一样。
考虑以下代码,实现它的最佳方法是什么(可能使用JS回调)?
import pandas as pd
from bokeh.plotting import show
from bokeh.models import 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)
show(range_slider)import pandas as pd
from bokeh.plotting import show
from bokeh.models import 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)
show(range_slider)
经过一些努力之后,我想到了这个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))