Как реализовать "шаг за 1 месяц" в Bokeh DateRangeSlider? - PullRequest
0 голосов
/ 20 февраля 2020

Виджет Bokeh DateRangeSlider требует int значений для своего атрибута step, который должен быть значением времени в миллисекундах. Это хорошо работает, когда шаг установлен на seconds, minutes, hours, days или years. Однако мне нужно разрешение month на слайдере.

Когда шаг установлен на 31 день, он хорошо работает для даты начала до марта, когда вместо 1 March я получаю 4 March , Затем смещение с 1-го числа месяца в отображаемом значении становится все больше и больше.

Я хочу иметь возможность устанавливать и отображать диапазон ползунка с обеих сторон, чтобы всегда быть 1-м днем ​​месяца, например 1 March, 1 April, 1 May, 1 June et c ... как в 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)

enter image description here

1 Ответ

0 голосов
/ 20 февраля 2020

После некоторой борьбы я пришел к этому обратному вызову 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)

enter image description here

Или, возможно, лучший вариант - не использовать 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)] + '&nbsp;.&nbsp;.&nbsp;.&nbsp;' + 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))

enter image description here

...