如何在用户停止滑动和缩放时同步范围?

2024-07-06 19:12:00 发布

您现在位置:Python中文网/ 问答频道 /正文

我有很多样地,每个样地都有很多样地。我需要放大和平移所有的图。此外,所有范围必须实时同步。如果我共享一个范围,在一些图中效果很好,但是对于许多图则会变得滞后。然后,为了解决这个问题,我想在平移或缩放操作完成时触发同步。在

当用户停止平移时,会触发一个PanEnd事件。但是我不能对滚轮缩放做同样的操作,因为没有MouseWheelEnd事件,只有一个MouseWheel事件,所以我无法检测用户何时停止。最后,我添加了一个定期回调来不时更新范围。但我不喜欢这个解决方案。在

我也尝试过LODStart和loend事件(与下采样相关),我不得不强制lod_threshold=1。但有时LODEnd不被触发,只有{}总是被触发。在

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)

还有其他建议吗?在


Tags: fromimporteventplotmodelsbokehrangestart
1条回答
网友
1楼 · 发布于 2024-07-06 19:12:00

虽然我不太喜欢,但我还是成功了。 它涉及一些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\}计时器,以便其他wheel事件在更新之前什么都不做。如果它与0不同,它什么也不做。在

dum_txt_timer的回调需要dum_txt_trigger,即存储绘图ID和所有绘图范围的mod_source数据源。 在timeout函数结束时更新dum_txt_timer值之前,回调不执行任何操作。否则,它首先更新dum_txt_trigger的值,该值第二次单击dum_button,并停止计时器(将其重置为0。然后更新所有绘图的范围。在

在这个例子中,更新之前的时间是由按钮回调中的timeout函数设置的。在

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

相关问题 更多 >