复杂散景图中CheckboxButtonGroup与Legend之间的交互

时间:2018-02-28 14:48:54

标签: python pandas bokeh

我有一个复杂的多变量数据集,其结构与此相似:

import pandas as pd
import numpy as np
import datetime as dt
from itertools import cycle, islice

N = 24
start_date = dt.date(2016,1,1)
nbdays = int(365 / N)

df = pd.DataFrame({'Date': [start_date + dt.timedelta(days=i*nbdays) for i in range(1,N+1)], 
                   'Rating':    [(100/N)*i for i in range(1,N+1)], 
                   'Plot':      list(islice(cycle(range(1, 9)), 0, N)), 
                   'Treatment': list(islice(cycle(range(1, 7)), 0, N)), 
                   'Trial':     list(islice(cycle(range(1, 4)), 0, N)), 
                   'Name':      list(islice(cycle("ABCDEF"), 0, N)), 
                   'Target':    list(islice(cycle("JKLMNOP"), 0, N)), 
                   'Part':      list(islice(cycle("WXYZ"), 0, N)) 
                   })

我想:

  • Date
  • 绘制RatingTreatment的关系图
  • 有一个交互式图例,以便点击治疗可以切换治疗的可见性
  • 在其他参数(PlotTrialNameTargetPart)的图表侧面设置按钮,以便点击在一个按钮上切换相应点的可见性
  • 显示将鼠标悬停在某个点上时的所有参数

这是我的代码(变量df中的数据集):

from bokeh.plotting import figure
from bokeh.models import ColumnDataSource
from bokeh.palettes import Set1 
from bokeh.models import (CDSView, BooleanFilter, Legend,
                          DatetimeTickFormatter, Range1d,
                          HoverTool)
from bokeh.models.widgets import CheckboxButtonGroup, Div
from bokeh.layouts import widgetbox, layout
from bokeh.io import curdoc

columns = ['Treatment', 'Plot', 'Trial', 'Name', 'Target', 'Part']
categories = [sorted(df[column].unique()) for column in columns]
all_columns = ['Date', 'Rating'] + columns

treatment_colormap = dict(zip(categories[0], Set1[6])) 

# Create Input controls
divs = [Div(text=column+':') for column in columns[1:]]
controls   = [CheckboxButtonGroup(labels=list(map(str, category)), active=list(range(len(category)))) for category in categories[1:]]

# Create Column Data Source that will be used by the plot
source = ColumnDataSource(data=dict((column, []) for column in all_columns))


def select():
    actives = [control.active for control in controls]
    actives_names = [[category[a] for a in active] for (active, category) in zip(actives, categories[1:])]

    presence = [df[column].isin(active_names) for (column, active_names) in zip(columns[1:], actives_names)]
    result = df[np.logical_and.reduce(presence)] # https://stackoverflow.com/a/49027984/50065
    return result

def update():
    sdf = select()
    source.data = dict((column, sdf[column]) for column in all_columns)

    glyphs = []
    selected_treatments = sorted(sdf['Treatment'].unique())
    for treatment in selected_treatments:
        booleans = [value == treatment for value in source.data['Treatment']]
        view = CDSView(source=source, filters=[BooleanFilter(booleans)])
        color = treatment_colormap[treatment]
        glyphs.append(p.circle(x='Date', y='Rating', source=source, view=view, line_color=color, fill_color=color))

    legend = Legend(items=[
        ("treatment {}".format(treatment), [glyph]) for treatment, glyph
        in zip(selected_treatments, glyphs)
        ])

    p.add_layout(legend, 'below')
    p.legend.click_policy='hide'
    p.legend.location = 'bottom_center'
    p.legend.orientation = 'horizontal'

for control in controls:
    control.on_change('active', lambda attr, old, new: update())

def datetime_in_miliseconds(date):
    date = dt.datetime.strptime(date, '%d/%m/%Y')
    epoch = dt.datetime.utcfromtimestamp(0)
    return (date - epoch).total_seconds() * 1000.0

hover = HoverTool(tooltips=[('Date', '@Date{%d/%m/%Y}')] + [(column, '@'+column) 
                            for column in all_columns[1:]], formatters={
                             'Date': 'datetime', # use 'datetime' formatter for 'Date' field
                             })


p = figure(x_axis_type="datetime", tools=[hover])
p.title.text = 'Date vs Rating'
p.xaxis.axis_label = 'Date'
p.xaxis.formatter = DatetimeTickFormatter(days = ['%d/%m/%y'])
start = datetime_in_miliseconds('01/01/2016')
end = datetime_in_miliseconds('31/12/2016')
p.x_range=Range1d(start, end)
p.yaxis.axis_label = 'Rating'
p.ygrid.band_fill_color="olive"
p.ygrid.band_fill_alpha = 0.1
p.y_range=Range1d(0,100)

sizing_mode = 'scale_width'
inputs = widgetbox(*sum(zip(divs, controls), tuple()), sizing_mode=sizing_mode)

l = layout([[p, inputs]], sizing_mode=sizing_mode)
update()  # initial load of the data
curdoc().add_root(l)

在您运行bokeh serve --show main.pybokeh版本0.12.10)时看起来像这样:

plot

什么有效:

  • 点击图例可以切换治疗的可见性

什么不起作用:

  • hoverlabels中显示的信息不正确(前6个点在其hoverlabels中具有相同的信息,接下来的6个点也具有相同的hoverlabels,等等。)
  • 点击右边的按钮搞砸了情节:轴标签消失,第二个图例显示在情节上而不是在它下方)

如何修复最后两点?

1 个答案:

答案 0 :(得分:2)

以下是一些想法:

  • 使用CustomJSFilter包装javascript函数来执行数据 过滤。
  • 只使用p.circle()的一次调用来绘制所有圈子。
  • 使用factor_cmap将处理列映射到颜色。
  • 使用tags 用于在Python中保存数据并在javascript中读取的属性。

由于只有一个GlyphRenderer,因此可见性切换不适用于它的传奇。

要解决此问题,请创建一个虚拟ColumnDataSource并多次调用p.circle()以创建虚拟GlyphRenderer列表。 为这些虚拟GlyphRenderer创建图例,并将其可见属性更改与调用CustomJS的{​​{1}}相关联 重新绘制数字。

因为所有过滤器计算都是由javascript执行的,所以您可以创建一个可以与用户输入交互的静态html文件。

这是笔记本:

http://nbviewer.jupyter.org/gist/ruoyu0088/01ddf28ed041508304843f49a794d66a

source.change.emit()