当在一个图中移动十字准线(尺寸=宽度)时,我希望在其他图中看到相同的位置。我的情节共享相同的x轴。
这是情节设置和示例:
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from bokeh.plotting import figure, ColumnDataSource, output_file, save
from bokeh.models import Span, CrosshairTool, HoverTool, ResetTool, PanTool, WheelZoomTool
from datetime import datetime
def timeline_figure(title=None, x_range=None, y_range=None):
# TODO: align x-axis
# TOOLS = "resize,crosshair,pan,wheel_zoom,box_zoom,reset,box_select,lasso_select,save"
# TOOLS = "resize,crosshair,xpan,xwheel_zoom,box_zoom,reset,save"
TOOLS = [CrosshairTool(dimensions=['height']),
PanTool(dimensions=['width']),
HoverTool(tooltips=[("Dato", "@Date")]),
WheelZoomTool(dimensions=['width']),
ResetTool()]
# Setting up the bokeh figure
fig = figure(width=800, height=250, title=title, x_axis_type="datetime",
x_range=x_range, y_range=y_range, tools=TOOLS)
# make the outline "invisible"
fig.outline_line_color = 'white'
# change just some things about the x-grid
fig.xgrid.grid_line_color = None
fig.ygrid.grid_line_color = None
# change just some things about the y-grid
fig.yaxis.minor_tick_line_color = None
year = 2016
dec = Span(location=datetime(year-1, 12, 1, 0, 0, 0).timestamp() * 1000,
dimension='height', line_color='grey', line_dash='dashed', line_width=1)
jan = Span(location=datetime(year, 1, 1, 0, 0, 0).timestamp() * 1000,
dimension='height', line_color='grey', line_dash='dashed', line_width=1)
feb = Span(location=datetime(year, 2, 1, 0, 0, 0).timestamp() * 1000,
dimension='height', line_color='grey', line_dash='dashed', line_width=1)
mar = Span(location=datetime(year, 3, 1, 0, 0, 0).timestamp() * 1000,
dimension='height', line_color='grey', line_dash='dashed', line_width=1)
apr = Span(location=datetime(year, 4, 1, 0, 0, 0).timestamp() * 1000,
dimension='height', line_color='grey', line_dash='dashed', line_width=1)
may = Span(location=datetime(year, 5, 1, 0, 0, 0).timestamp() * 1000,
dimension='height', line_color='grey', line_dash='dashed', line_width=1)
fig.renderers.extend([dec, jan, feb, mar, apr, may])
return fig
def usage():
import numpy as np
from datetime import timedelta
from bokeh.io import gridplot
output_file("test.html", mode="cdn")
d_start = datetime(2016, 6, 1)
d_step = timedelta(days=1)
t = [d_start + (i * d_step) for i in range(0, 12)]
s1 = np.random. randint(2, 10, 12)
s2 = np.random.randint(2, 10, 12)
source = ColumnDataSource({'t': t, 's1': s1, 's2': s2})
p1 = timeline_figure()
p1.triangle(x='t', y='s1', source=source, size=10, color="blue")
p2 = timeline_figure()
p2.square(x='t', y='s2', source=source, size=10, color="red")
p = gridplot([[p1], [p2]])
save(p)
if __name__ == "__main__":
usage()
感谢任何建议。
卡斯滕
答案 0 :(得分:10)
Bokeh当前没有对此的内置支持。我想出了如何使用JavaScript回调来做到这一点。以下功能适用于散景0.13上的两个垂直对齐的图:
from bokeh.models import CustomJS, CrosshairTool
def add_vlinked_crosshairs(fig1, fig2):
cross1 = CrosshairTool()
cross2 = CrosshairTool()
fig1.add_tools(cross1)
fig2.add_tools(cross2)
js_move = '''
if(cb_obj.x >= fig.x_range.start && cb_obj.x <= fig.x_range.end &&
cb_obj.y >= fig.y_range.start && cb_obj.y <= fig.y_range.end)
{
cross.spans.height.computed_location = cb_obj.sx
}
else
{
cross.spans.height.computed_location = null
}
'''
js_leave = 'cross.spans.height.computed_location = null'
args = {'cross': cross2, 'fig': fig1}
fig1.js_on_event('mousemove', CustomJS(args=args, code=js_move))
fig1.js_on_event('mouseleave', CustomJS(args=args, code=js_leave))
args = {'cross': cross1, 'fig': fig2}
fig2.js_on_event('mousemove', CustomJS(args=args, code=js_move))
fig2.js_on_event('mouseleave', CustomJS(args=args, code=js_leave))
这个想法是在每个绘图上添加一个鼠标移动回调,从而触发要绘制的另一个绘图上十字线的垂直部分。这是通过使用鼠标回调(spans.height.computed_location
)提供的屏幕位置更新十字准线的cb_obj.sx
成员来完成的。
鼠标移动事件在图的整个区域(包括轴,边界等)上触发。添加检查以确保鼠标位于数据空间内(cb_obj.x
和cb_obj.y
是轴坐标),如果没有,则删除该行。还添加了mouseleave
事件,因为在情节之外快速移动可能不会在边界区域触发事件。
如果图垂直对齐,则此方法有效。对于水平对齐(按照OP),只需更改cross.spans.height.computed_location
-> cross.spans.width.computed_location
和cb_obj.sx
-> cb_obj.sy
。
这仅在地块大小相同时有效,否则将需要进一步检查。
答案 1 :(得分:3)
更紧凑的示例,其中包含任意数量的图和两个CrossHair尺寸(已针对Bokeh v1.0.4更新):
from bokeh.models import CustomJS, CrosshairTool
from bokeh.plotting import figure, show, curdoc
from bokeh.layouts import gridplot
import numpy as np
def addLinkedCrosshairs(plots):
js_move = ''' start = fig.x_range.start, end = fig.x_range.end
if(cb_obj.x>=start && cb_obj.x<=end && cb_obj.y>=start && cb_obj.y<=end)
{ cross.spans.height.computed_location=cb_obj.sx }
else { cross.spans.height.computed_location = null }
if(cb_obj.y>=start && cb_obj.y<=end && cb_obj.x>=start && cb_obj.x<=end)
{ cross.spans.width.computed_location=cb_obj.sy }
else { cross.spans.width.computed_location=null }'''
js_leave = '''cross.spans.height.computed_location=null; cross.spans.width.computed_location=null'''
figures = plots[:]
for plot in plots:
crosshair = CrosshairTool(dimensions = 'both')
plot.add_tools(crosshair)
for figure in figures:
if figure != plot:
args = {'cross': crosshair, 'fig': figure}
figure.js_on_event('mousemove', CustomJS(args = args, code = js_move))
figure.js_on_event('mouseleave', CustomJS(args = args, code = js_leave))
plots = [figure(plot_width = 200, plot_height = 200, tools = '') for i in range(9)]
[plot.line(np.arange(10), np.random.random(10)) for plot in plots]
addLinkedCrosshairs(plots)
show(gridplot(children = [plot for plot in plots], ncols = 3))
要缩小为一维(垂直或水平),请删除回调的相应“ if / else”部分
结果:
答案 2 :(得分:2)
已解决的解决方案(已针对Bokeh v1.0.4更新)
from bokeh.layouts import gridplot
from bokeh.models import CustomJS, CrosshairTool
from bokeh.plotting import figure, ColumnDataSource, output_file, save, show
from bokeh.models import Span, CrosshairTool, HoverTool, ResetTool, PanTool, WheelZoomTool
from datetime import datetime
from datetime import timedelta
import numpy as np
import time
def add_vlinked_crosshairs(fig1, fig2):
js_move = '''if(cb_obj.x >= fig.x_range.start && cb_obj.x <= fig.x_range.end && cb_obj.y >= fig.y_range.start && cb_obj.y <= fig.y_range.end)
{ cross.spans.height.computed_location = cb_obj.sx }
else
{ cross.spans.height.computed_location = null }'''
js_leave = 'cross.spans.height.computed_location = null'
cross1 = CrosshairTool()
cross2 = CrosshairTool()
fig1.add_tools(cross1)
fig2.add_tools(cross2)
args = {'cross': cross2, 'fig': fig1}
fig1.js_on_event('mousemove', CustomJS(args = args, code = js_move))
fig1.js_on_event('mouseleave', CustomJS(args = args, code = js_leave))
args = {'cross': cross1, 'fig': fig2}
fig2.js_on_event('mousemove', CustomJS(args = args, code = js_move))
fig2.js_on_event('mouseleave', CustomJS(args = args, code = js_leave))
def to_seconds(date):
return time.mktime(date.timetuple())
def timeline_figure(title = None, x_range = None, y_range = None):
TOOLS = [CrosshairTool(dimensions = 'height'), PanTool(dimensions = 'width'), HoverTool(tooltips = [("Date", "@t")]), WheelZoomTool(dimensions = 'width'), ResetTool()]
fig = figure(width = 800, height = 250, title = title, x_axis_type = "datetime", x_range = x_range, y_range = y_range, tools = TOOLS)
fig.outline_line_color = 'white'
fig.xgrid.grid_line_color = None
fig.ygrid.grid_line_color = None
fig.yaxis.minor_tick_line_color = None
year = 2016
dec = Span(location = to_seconds(datetime(year - 1, 12, 1, 0, 0, 0)))
jan = Span(location = to_seconds(datetime(year, 1, 1, 0, 0, 0)))
feb = Span(location = to_seconds(datetime(year, 2, 1, 0, 0, 0)))
mar = Span(location = to_seconds(datetime(year, 3, 1, 0, 0, 0)))
apr = Span(location = to_seconds(datetime(year, 4, 1, 0, 0, 0)))
may = Span(location = to_seconds(datetime(year, 5, 1, 0, 0, 0)))
fig.renderers.extend([dec, jan, feb, mar, apr, may])
return fig
def usage():
output_file("test_linked_crosshair.html", mode = "cdn")
d_start = datetime(2016, 6, 1)
d_step = timedelta(days = 1)
t = [d_start + (i * d_step) for i in range(0, 12)]
s1 = np.random. randint(2, 10, 12)
s2 = np.random.randint(2, 10, 12)
source = ColumnDataSource({'t': t, 's1': s1, 's2': s2})
p1 = timeline_figure()
p1.triangle(x = 't', y = 's1', source = source, size = 10, color = "blue")
p2 = timeline_figure(x_range = p1.x_range)
p2.square(x = 't', y = 's2', source = source, size = 10, color = "red")
add_vlinked_crosshairs(p1, p2)
p = gridplot([[p1], [p2]])
show(p)
if __name__ == "__main__":
usage()
结果:
答案 3 :(得分:2)
从bokeh v2.2.1开始,解决方案得到了简化。我不确定在以前的版本中是否已经可以这样做。这里是一个在bokeh v2.2.1的网格图中共享9个图中两个维度的十字准线的示例:
import numpy as np
from bokeh.plotting import figure, show
from bokeh.layouts import gridplot
from bokeh.models import CrosshairTool
plots = [figure() for i in range(6)]
[plot.line(np.arange(10), np.random.random(10)) for plot in plots]
crosshair = CrosshairTool(dimensions="both")
for plot in plots:
plot.add_tools(crosshair)
show(gridplot(children=[plot for plot in plots], ncols=3))
答案 4 :(得分:0)
此答案适用于像我一样喜欢Graeme解决方案,但需要像我一样将其应用于两个以上数字的人:
from bokeh.models import CustomJS, CrosshairTool
def add_vlinked_crosshairs(figs):
js_leave = ''
js_move = 'if(cb_obj.x >= fig.x_range.start && cb_obj.x <= fig.x_range.end &&\n'
js_move += 'cb_obj.y >= fig.y_range.start && cb_obj.y <= fig.y_range.end){\n'
for i in range(len(figs)-1):
js_move += '\t\t\tother%d.spans.height.computed_location = cb_obj.sx\n' % i
js_move += '}else{\n'
for i in range(len(figs)-1):
js_move += '\t\t\tother%d.spans.height.computed_location = null\n' % i
js_leave += '\t\t\tother%d.spans.height.computed_location = null\n' % i
js_move += '}'
crosses = [CrosshairTool() for fig in figs]
for i, fig in enumerate(figs):
fig.add_tools(crosses[i])
args = {'fig': fig}
k = 0
for j in range(len(figs)):
if i != j:
args['other%d'%k] = crosses[j]
k += 1
fig.js_on_event('mousemove', CustomJS(args=args, code=js_move))
fig.js_on_event('mouseleave', CustomJS(args=args, code=js_leave))