【问题标题】:Bokeh Dynamic Axis Scaling散景动态轴缩放
【发布时间】:2017-08-14 05:35:50
【问题描述】:

我的问题类似于this question,但我有代码示例。我在一个 Django 应用程序中创建了一个散景图,它绘制了随着时间的推移在竞技游泳比赛中游泳的时间,并使用一个图

plot = figure(
        title='Event Progress',
        x_axis_label='Date',
        y_axis_label='Time',
        x_axis_type='datetime',
        plot_width=400,
        plot_height=200,
        tools=tools,
        responsive=True,
    )

Select 小部件和 CustomJS 函数一次只显示一行。问题是,如果一个事件的时间为 10 分钟,而另一个事件的时间为 25 秒,则 y 轴(时间)会按比例缩放,以便在它们可见时都可以看到它们。我声明每一行像

plot_lines[event] = plot.line('x_'+event, 
                              'y_'+event, 
                              line_width=4, 
                              source=source)

Select 小部件类似

multi_select = Select(title="Select Event:", 
                      value=events[0], 
                      options=events, 
                      callback=callback)

如果有帮助的话。我还将 y 轴自定义格式为

plot.yaxis.formatter = FuncTickFormatter(code="""
    return Math.floor(tick/60) + ":" + tick.toFixed(2)
""")

以便正确显示时间。其他一切只是格式化数据,将行设置为可见或不可见以及回调函数。有没有办法为每条线动态缩放单个轴?我想理想情况下我可以为每条线设置 y-max 和 y-min 以使其保持在图表范围内,但我不确定这是否可能或我的其他选项是什么。

更新:这是我的完整代码。

def graph_event(swimmer):
hover_tool = date_time_hover_tool()
tools = ['pan', 'box_zoom', hover_tool, 'reset', 'save']
plot = figure(
    title='Event Progress',
    x_axis_label='Date',
    y_axis_label='Time',
    x_axis_type='datetime',
    plot_width=400,
    plot_height=200,
    tools=tools,
    responsive=True,
)
# format datetime.timedelta objects to MM:ss.mm
plot.yaxis.formatter = FuncTickFormatter(code=
    """
    return Math.floor(tick/60) + ":" + tick.toFixed(2)
    """
)

data_source = {}
events = []
first_event = None
for event in EVENT_CHOICE:
    e = '_'.join([word.lower() for word in event[0].split()])

    results = Event.objects.filter(swimmer=swimmer).filter(event=event[0]).order_by('date')
    if results.exists():
        if len(results) == 1: # one point will not display well on graph
            data_source['x_'+e] = None
            data_source['y_'+e] = None
            data_source['date_'+e] = None
            data_source['time_'+e] = None
            continue

        events.append(event[1])
        if first_event == None:
            first_event = e

        x, y = [], []
        date, time = [], []
        for r in results.iterator():
            d = r.date
            t = r.time.total_seconds()
            x.append(d)
            y.append(t)
            date.append(d.strftime('%m/%d/%y')) # date to string for hover
            time.append('{:d}:{:.2f}'.format(int(t)/60, t)) # time to string for hover

        data_source['x_'+e] = x
        data_source['y_'+e] = y
        data_source['date_'+e] = date
        data_source['time_'+e] = time

    else:
        # eliminates KeyError exceptions
        data_source['x_'+e] = None
        data_source['y_'+e] = None
        data_source['date_'+e] = None
        data_source['time_'+e] = None

# set initial graph
source = ColumnDataSource(data=dict(
    x=data_source['x_'+first_event],
    y=data_source['y_'+first_event],
    date=data_source['date_'+first_event],
    time=data_source['time_'+first_event]
))
plot.line('x', 'y', source=source)

try:
    select = Select(title="Select Event:", value=events[0], options=events)
except IndexError:
    return None, None

# callback modifies data source depending on Select box
callback = CustomJS(args=dict(source=source, select=select), code="""
        data = %s;

        if (select.value == "50 Freestyle") {
            source.data['x'] = data.x_50_free;
            source.data['y'] = data.y_50_free;
            source.data['date'] = data.date_50_free;
            source.data['time'] = data.time_50_free;
        } else if (select.value == "100 Freestyle") {
            source.data['x'] = data.x_100_free;
            source.data['y'] = data.y_100_free;
            source.data['date'] = data.date_100_free;
            source.data['time'] = data.time_100_free;
        } else if (select.value == "200 Freestyle") {
            console.log(select.value);
            source.data['x'] = data.x_200_free;
            source.data['y'] = data.y_200_free;
            source.data['date'] = data.date_200_free;
            source.data['time'] = data.time_200_free;
        } else if (select.value == "500 Freestyle") {
            source.data['x'] = data.x_500_free;
            source.data['y'] = data.y_500_free;
            source.data['date'] = data.date_500_free;
            source.data['time'] = data.time_500_free;
        } else if (select.value == "1000 Freestyle") {
            source.data['x'] = data.x_1000_free;
            source.data['y'] = data.y_1000_free;
            source.data['date'] = data.date_1000_free;
            source.data['time'] = data.time_1000_free;
        } else if (select.value == "50 Backstroke") {
            source.data['x'] = data.x_50_back;
            source.data['y'] = data.y_50_back;
            source.data['date'] = data.date_50_back;
            source.data['time'] = data.time_50_back;
        } else if (select.value == "100 Backstroke") {
            source.data['x'] = data.x_100_back;
            source.data['y'] = data.y_100_back;
            source.data['date'] = data.date_100_back;
            source.data['time'] = data.time_100_back;
        } else if (select.value == "200 Backstroke") {
            source.data['x'] = data.x_200_back;
            source.data['y'] = data.y_200_back;
            source.data['date'] = data.date_200_back;
            source.data['time'] = data.time_200_back;
        } else if (select.value == "50 Breaststroke") {
            source.data['x'] = data.x_50_breast;
            source.data['y'] = data.y_50_breast;
            source.data['date'] = data.date_50_breast;
            source.data['time'] = data.time_50_breast;
        } else if (select.value == "100 Breaststroke") {
            source.data['x'] = data.x_100_breast;
            source.data['y'] = data.y_100_breast;
            source.data['date'] = data.date_100_breast;
            source.data['time'] = data.time_100_breast;
        } else if (select.value == "200 Breaststroke") {
            source.data['x'] = data.x_200_breast;
            source.data['y'] = data.y_200_breast;
            source.data['date'] = data.date_200_breast;
            source.data['time'] = data.time_200_breast;
        } else if (select.value == "50 Butterfly") {
            source.data['x'] = data.x_50_fly;
            source.data['y'] = data.y_50_fly;
            source.data['date'] = data.date_50_fly;
            source.data['time'] = data.time_50_fly;
        } else if (select.value == "100 Butterfly") {
            source.data['x'] = data.x_100_fly;
            source.data['y'] = data.y_100_fly;
            source.data['date'] = data.date_100_fly;
            source.data['time'] = data.time_100_fly;
        } else if (select.value == "200 Butterfly") {
            source.data['x'] = data.x_200_fly;
            source.data['y'] = data.y_200_fly;
            source.data['date'] = data.date_200_fly;
            source.data['time'] = data.time_200_fly;
        } else if (select.value == "100 IM") {
            source.data['x'] = data.x_100_im;
            source.data['y'] = data.y_100_im;
            source.data['date'] = data.date_100_im;
            source.data['time'] = data.time_100_im;
        } else if (select.value == "200 IM") {
            source.data['x'] = data.x_200_im;
            source.data['y'] = data.y_200_im;
            source.data['date'] = data.date_200_im;
            source.data['time'] = data.time_200_im;
        } else if (select.value == "400 IM") {
            source.data['x'] = data.x_400_im;
            source.data['y'] = data.y_400_im;
            source.data['date'] = data.date_400_im;
            source.data['time'] = data.time_400_im;
        } else if (select.value == "Base Freestyle") {
            source.data['x'] = data.x_base_free;
            source.data['y'] = data.y_base_free;
            source.data['date'] = data.date_base_free;
            source.data['time'] = data.time_base_free;
        } else if (select.value == "Base Backstroke") {
            source.data['x'] = data.x_base_back;
            source.data['y'] = data.y_base_back;
            source.data['date'] = data.date_base_back;
            source.data['time'] = data.time_base_back;
        } else if (select.value == "Base Breaststroke") {
            source.data['x'] = data.x_base_breast;
            source.data['y'] = data.y_base_breast;
            source.data['date'] = data.date_base_breast;
            source.data['time'] = data.time_base_breast;
        } else if (select.value == "Base Butterfly") {
            source.data['x'] = data.x_base_fly;
            source.data['y'] = data.y_base_fly;
            source.data['date'] = data.date_base_fly;
            source.data['time'] = data.time_base_fly;
        } else if (select.value == "Base IM") {
            source.data['x'] = data.x_base_im;
            source.data['y'] = data.y_base_im;
            source.data['date'] = data.date_base_im;
            source.data['time'] = data.time_base_im;
        }

        source.change.emit()
""" % json.dumps(data_source, cls=DatetimeEncoder))

select.callback = callback

return components(column(select, plot, responsive=True))

这是悬停工具和 JSON 日期时间序列化程序。

class DatetimeEncoder(json.JSONEncoder):
"""
Encodes Python datetime.date objects to make compatible with JSON serialization.
"""
def default(self, obj):
    try:
        return super(DatetimeEncoder, obj).default(obj)
    except TypeError:
        return str(obj)

def date_time_hover_tool():
    """
    Generates the HTML for the Bokeh's hover data tool on our graph.
    """
    hover_html = """
      <div>
        <span class="hover-tooltip">@date</span>
      </div>
      <div>
        <span class="hover-tooltip">@time</span>
      </div>
    """
    return HoverTool(tooltips=hover_html)

希望这不会太令人困惑。基本上,我会遍历该运动员的每个事件并获取数据(如果存在),然后在回调中设置源数据。

【问题讨论】:

    标签: javascript python django bokeh


    【解决方案1】:

    如果您只想一次显示一行,那么更简单的解决方案可能是更新一行的数据。一种方法是直接将数据“模板化”到回调文本中。我并不总是推荐这种方法,但对于不是很大的数据量,它可以很好地工作:

    import json
    from math import sin
    
    from bokeh.io import output_file, show
    from bokeh.layouts import column
    from bokeh.models import ColumnDataSource, CustomJS, Select
    from bokeh.plotting import figure
    
    output_file("foo.html")
    
    xl, yl = list(range(100)), [sin(x) for x in xl]
    xs, ys = list(range(500)), [sin(x) for x in xs]
    source = ColumnDataSource(data=dict(x=xl, y=yl))
    
    plot = figure()
    plot.line('x', 'y', source=source)
    
    select = Select(title="Event:", value="long", options=["long", "short"])
    
    callback = CustomJS(args=dict(source=source, select=select), code="""
        data = %s;
        if (select.value == "long") {
            source.data['x'] = data.xl
            source.data['y'] = data.yl
        } else {
            source.data['x'] = data.xs
            source.data['y'] = data.ys
        }
        source.change.emit()
    """ % json.dumps(dict(xl=xl, yl=yl, xs=xs, ys=ys)))
    
    select.callback = callback
    
    show(column(select, plot))
    

    如果您真的需要单独的线渲染器,那么DataRange1d 模型具有.renderers 属性,用于指定要“自动排列”哪些渲染器。但它没有设置为响应式,除非在启动和重置时。因此,您的 JS 回调需要在范围上适当地设置 .renderers,并在范围上调用 .reset 方法以使更改生效。我认为会按预期工作。

    【讨论】:

    • 这样会根据 Select 值改变 y 轴刻度吗?因为如果是这样的话,那可能比我做的更容易。
    • 我不明白你的问题。上面的示例是完整的,可以按原样运行,以便您评估它在完整用例的上下文中的运行方式。
    • 抱歉,这来得太晚了,但我对你的建议有一点小问题。我已经到了页面加载没有任何问题并显示初始图形的地步,但是如果我使用选择小部件,图形和轴将被擦除,就像图形正在更改数据源但视觉部分没有得到更新。我已经用代码更新了我的问题,希望你能检查出来。
    • 我知道这是旧的,但希望@bigreddot 看到这个。我很好奇我是否有一个plot=figure(y_range=DataRange1d()),然后在一个CustomJS 回调中你说有人应该打电话给plot.y_range.renderers.reset()?因为我正在尝试并没有重置任何功能,还是应该只使用plot.y_range.reset
    • DataRange1d 上有一个 reset,但 AFAICT 它没有做任何相关的事情,至少现在没有了。此外,renderers 似乎现在是动态计算的,所以我希望它可以随时更新。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-05-07
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-02-21
    • 1970-01-01
    相关资源
    最近更新 更多