Как синхронизировать диапазоны только тогда, когда пользователь прекращает панорамирование и масштабирование? - PullRequest
0 голосов
/ 23 мая 2019

У меня много участков и много образцов на участок.Мне нужно увеличить и панорамировать все графики.Кроме того, все диапазоны должны быть синхронизированы в режиме реального времени.Если я разделяю диапазон, он хорошо работает с несколькими графиками, но на многих графиках он становится медленным.Затем, чтобы решить эту проблему, я хотел бы запустить синхронизацию сразу после завершения действия панорамирования или масштабирования.

Существует событие PanEnd, которое запускается, когда пользователь прекращает панорамирование.Но я не могу сделать то же самое с масштабированием колеса, потому что нет события MouseWheelEnd, просто событие MouseWheel, поэтому я не могу определить, когда пользователь останавливается.Наконец я добавил периодический обратный вызов, чтобы время от времени обновлять диапазоны.Но мне не нравится это решение.

Я также пробовал события LODStart и LODEnd (связанные с понижающей дискретизацией), и мне пришлось принудительно lod_threshold=1.Но иногда LODEnd не срабатывает, всегда срабатывает только LODStart.

from bokeh.plotting import figure
from bokeh.models.sources import ColumnDataSource, CDSView
from bokeh.models.filters import IndexFilter
from bokeh.models.markers import Scatter, Circle
from bokeh.models.tools import LassoSelectTool
from bokeh.models.ranges import DataRange1d
from bokeh.plotting import curdoc, gridplot
from bokeh.events import MouseWheel, PanEnd
import numpy as np

N = 3500
x = np.random.random(size=N) * 200
y = np.random.random(size=N) * 200
source = ColumnDataSource(data=dict(x=x, y=y))

plots = []
x_ranges = []
y_ranges = []
p_last_modified = -1
def render_plot(i, p_last_modified):
    range_padding = 0.25
    x_range = DataRange1d(
        range_padding=range_padding,
        renderers=[]
    )
    y_range = DataRange1d(
        range_padding=range_padding,
        renderers=[]
    )

    plot = figure(
        width=500,
        height=500,
        x_range=x_range,
        y_range=y_range,
        toolbar_location='left',
        tools='pan,wheel_zoom,tap,lasso_select',
        output_backend='webgl',
    )
    c = plot.scatter(
        x='x',
        y='y',
        size=3,
        fill_color='blue',
        line_color=None,
        line_alpha=1.0,
        source=source,

        nonselection_fill_color='blue',
        nonselection_line_color=None,
        nonselection_fill_alpha=1.0,
    )
    c.selection_glyph = Scatter(
        fill_color='yellow',
        line_color='red',
        line_alpha=1.0,
    )

    def mouse_wheel_event(event):
        print('>> MOUSE WHEEL EVENT: PLOT NUMBER: {}'.format(i))
        global p_last_modified
        p_last_modified = i

    plot.on_event(MouseWheel, mouse_wheel_event)

    def pan_end_event(event):
        print('>> PAN END: {}'.format(i))
        for p in range(len(plots)):
            if p != i:
                plots[p].x_range.end = plots[i].x_range.end
                plots[p].x_range.start = plots[i].x_range.start
                plots[p].y_range.end = plots[i].y_range.end
                plots[p].y_range.start = plots[i].y_range.start

    plot.on_event(PanEnd, pan_end_event)

    plots.append(plot)
    x_ranges.append(x_range)
    y_ranges.append(y_range)

for i in range(12):
    render_plot(i, p_last_modified)

gp = gridplot(
    children=plots,
    ncols=4,
    plot_width=300,
    plot_height=300,
    toolbar_location='left',
)

def callback():
    global p_last_modified
    print('-- CALLBACK: last_modified: {}'.format(p_last_modified))
    if p_last_modified != -1:
        for p in range(len(plots)):
            if p != p_last_modified:
                plots[p].x_range.end = plots[p_last_modified].x_range.end
                plots[p].x_range.start = plots[p_last_modified].x_range.start
                plots[p].y_range.end = plots[p_last_modified].y_range.end
                plots[p].y_range.start = plots[p_last_modified].y_range.start
        p_last_modified = -1

curdoc().add_periodic_callback(callback, 3000)

curdoc().add_root(gp)

Любое другое предложение?

1 Ответ

1 голос
/ 25 мая 2019

У меня все получилось, хотя мне это не очень нравится. Он включает несколько виджетов JS и 3 «фиктивных», я бы ожидал, что будет более простой способ, но в любом случае это один из способов.

dum_txt_timer - это текстовый ввод, который будет использоваться в качестве таймера, его значение указывается в секундах и будет обновляться с желаемым временным шагом. Когда значение достигнет желаемого порога, будет запущено обновление диапазонов. Когда значение ниже порога, оно ничего не делает

dum_button - это кнопка, которая выполняет две функции: первый щелчок запускает таймер в dum_txt_timer, второй щелчок останавливает таймер.

dum_txt_trigger - это еще один текстовый вход, который используется для нажатия dum_button и запуска / остановки таймера.

Функция mouse_wheel_event срабатывает на каждой итерации колеса мыши. Значение графика, на котором находится мышь, хранится в mod_source, источнике данных, который передается обратному вызову dum_txt_timer. Он проверяет, равно ли значение dum_txt_timer 0, и обновляет ли оно значение в dum_txt_trigger, который нажимает кнопку и запускает таймер, и обновляет dum_txt_timer, чтобы другие события колеса ничего не делали до обновления. Если он отличается от 0, он ничего не делает.

Для обратного вызова dum_txt_timer требуется dum_txt_trigger, источник данных mod_source, в котором хранится идентификатор графика и все диапазоны графика. Обратный вызов ничего не делает, пока значение dum_txt_timer не будет обновлено в конце функции тайм-аута. В противном случае он сначала обновляет значение dum_txt_trigger, которое нажимает dum_button во второй раз и останавливает таймер (сбрасывает его на 0. Затем обновляется диапазон всех графиков.

В этом примере время до обновления устанавливается функцией тайм-аута в функции обратного вызова кнопки.

from bokeh.io import curdoc
from bokeh.plotting import figure
from bokeh.models import ColumnDataSource, CDSView, IndexFilter, Scatter, Circle, LassoSelectTool, DataRange1d, CustomJS, TextInput, Button
from bokeh.events import MouseWheel, PanEnd
from bokeh.layouts import widgetbox, gridplot
import numpy as np

N = 3500
x = np.random.random(size=N) * 200
y = np.random.random(size=N) * 200
source = ColumnDataSource(data=dict(x=x, y=y))

dum_txt_timer = TextInput(value='0',visible=False)

# javascript code for a dummy (invisible) button, it starts and stops a timer that will be written in dum_txt_timer
dum_button_code = """
if (cb_obj.button_type.includes('success')){
// start a timer in dum_txt by updating its value with a fixed timestep
var start = new Date(); 
var intervalID = setInterval(function(){var current = new Date(); var diff=((current-start)/1000.0).toFixed(4); dum_txt_timer.value=diff.toString();  }, 500)
cb_obj.button_type = 'warning';
} else {
// stop the timer and set the dum_txt_timer value back to 0
var noIntervals = setInterval(function(){});
for (var i = 0; i<noIntervals; i++) { window.clearInterval(i);}
dum_txt_timer.value='0';
cb_obj.button_type = 'success';
}
"""
dum_button = Button(label='dummy_button',button_type='success',visible=False) # the dummy button itself
dum_button.callback = CustomJS(args={'dum_txt_timer':dum_txt_timer},code=dum_button_code) # the callback of the button

# dummy textinput to click the dummy button
dum_txt_trigger = TextInput(value='0',visible=False)
dum_txt_trigger_code = """
// click the dummy button
var button_list = document.getElementsByTagName('button');

for(var i=0;i<button_list.length;i++){
    if(button_list[i].textContent==="dummy_button"){button_list[i].click()}
}   
"""
dum_txt_trigger.js_on_change('value',CustomJS(code=dum_txt_trigger_code))

dum_box = widgetbox(dum_txt_timer,dum_txt_trigger,dum_button,visible=False)

plots = []
x_ranges = []
y_ranges = []
mod_source = ColumnDataSource(data={'x':[]})
reference = None
def render_plot(i):
    range_padding = 0.25
    x_range = DataRange1d(range_padding=range_padding,renderers=[])
    y_range = DataRange1d(range_padding=range_padding,renderers=[])

    plot = figure(width=500,height=500,x_range=x_range,y_range=y_range,toolbar_location='left',tools='pan,wheel_zoom,tap,lasso_select',output_backend='webgl',)
    c = plot.scatter(x='x',y='y',size=3,fill_color='blue',line_color=None,line_alpha=1.0,source=source,nonselection_fill_color='blue',nonselection_line_color=None,nonselection_fill_alpha=1.0,)
    c.selection_glyph = Scatter(fill_color='yellow',line_color='red',line_alpha=1.0,)

    def mouse_wheel_event(event):        

        if dum_txt_timer.value != '0': 
            return

        # if the timer value is 0, start the timer    
        dum_txt_trigger.value =  str(int(dum_txt_trigger.value)+1)
        dum_txt_timer.value = '0.0001' # immediatly update the timer value for the check on 0 in the python callback to work immediatly

        mod_source.data.update({'x':[i]})

    plot.on_event(MouseWheel, mouse_wheel_event)

    def pan_end_event(event):
        print('>> PAN END: {}'.format(i))
        for p in range(len(plots)):
            if p != i:
                plots[p].x_range.end = plots[i].x_range.end
                plots[p].x_range.start = plots[i].x_range.start
                plots[p].y_range.end = plots[i].y_range.end
                plots[p].y_range.start = plots[i].y_range.start

    plot.on_event(PanEnd, pan_end_event)

    plots.append(plot)
    x_ranges.append(x_range)
    y_ranges.append(y_range)

for i in range(12):
    render_plot(i)

dum_txt_timer_args = {'dum_txt_trigger':dum_txt_trigger,'mod_source':mod_source}
dum_txt_timer_args.update( {'xrange{}'.format(i):plot.x_range for i,plot in enumerate(plots)} )
dum_txt_timer_args.update( {'yrange{}'.format(i):plot.y_range for i,plot in enumerate(plots)} )

set_arg_list = "var xrange_list = [{}];".format(','.join(['xrange{}'.format(i) for i in range(len(plots))]))
set_arg_list += "var yrange_list = [{}];".format(','.join(['yrange{}'.format(i) for i in range(len(plots))]))

# code that triggers when the dum_txt_timer value is changed, so every 100 ms, but only clicks dum_button when the value is greater than 2 (seconds)
dum_txt_timer_code = set_arg_list + """
var timer = Number(cb_obj.value);
var trigger_val = Number(dum_txt_trigger.value);

// only do something when the value is greater than 2 (seconds)
if (timer>0.0001) {
    trigger_val = trigger_val + 1;
    dum_txt_trigger.value = trigger_val.toString(); // click button again to stop the timer

    // update the plot ranges
    var p_last_modified = mod_source.data['x'][0];
    var nplots = xrange_list.length;

    for (var i=0; i<nplots; i++){
        if (i!=p_last_modified){
            xrange_list[i].start = xrange_list[p_last_modified].start;
            xrange_list[i].end = xrange_list[p_last_modified].end;
            yrange_list[i].start = yrange_list[p_last_modified].start;
            yrange_list[i].end = yrange_list[p_last_modified].end;
        }
    }
}
"""

dum_txt_timer.js_on_change('value',CustomJS(args=dum_txt_timer_args,code=dum_txt_timer_code))

gp = gridplot(children=plots,ncols=4,plot_width=300,plot_height=300,toolbar_location='left',)

grid = gridplot([[gp],[dum_box]],toolbar_location=None)

curdoc().add_root(grid)

Одна приятная вещь заключается в том, что одни и те же фиктивные виджеты можно использовать для установки задержки при обновлении диапазона от различных событий, обратный вызов события просто необходимо обновить dum_txt_trigger, как в mouse_wheel_event

...