Bokeh Colorbar 颜色条右侧的垂直标题?

Bokeh Colorbar Vertical title to right of colorbar?

我正在尝试做一些我通常认为微不足道但在散景中似乎非常困难的事情:将垂直颜色条添加到绘图中,然后获得颜色条的标题 (a.k.a。颜色映射后面的变量)出现在颜色条的一侧,但从水平方向顺时针旋转了 90 度。

根据我对 bokeh ColorBar() 接口的了解(查看文档并使用 python 解释器的 help() 函数来处理这个元素),这是不可能的。无奈之下,我添加了自己的基于 Label() 的注释。这可行但很笨拙,并且在散景服务情况下部署时会显示奇怪的行为——绘图上数据的宽度 window 与标题颜色条的标题字符串的长度成反比变化。

下面我包含了散景服务器 mpg 示例的修改版本。对其复杂性表示歉意,但我认为这是使用散景附带的 infrastructure/data 来说明问题的最佳方式。对于那些不熟悉 bokeh serve 的人,此代码片段需要保存到一个名为 main.py 的文件中,该文件位于一个目录中——为了论证,我们假设是 CrossFilter2——以及 CrossFilter2 的 parent 目录中需要调用命令

bokeh serve --show CrossFilter2

这将显示在浏览器中 window (localhost:5006/CrossFilter2) 如果您使用颜色选择小部件,您会明白我的意思,即短变量名称,例如 'hp' 或 'mpg' 比 'accel' 或 'weight' 等更长的变量名称导致更宽的数据显示 windows。我怀疑 label 元素的大小可能存在错误——它们的 x 和 y 维度被交换了——并且 bokeh 不理解 label 元素已经旋转。

我的问题是:

  1. 我真的必须费这么大劲才能得到一个简单的颜色条标签功能吗?我在 matplotlib/plotly 中遇到了 little-to-no 的麻烦?
  2. 如果我必须经历你在我的示例代码中看到的麻烦,是否有其他方法可以避免数据 window 宽度问题?

    import numpy as np
    import pandas as pd
    
    from bokeh.layouts import row, widgetbox
    from bokeh.models import Select
    from bokeh.models import HoverTool, ColorBar, LinearColorMapper, Label
    from bokeh.palettes import Spectral5
    from bokeh.plotting import curdoc, figure, ColumnDataSource
    from bokeh.sampledata.autompg import autompg_clean as df
    
    df = df.copy()
    
    SIZES = list(range(6, 22, 3))
    COLORS = Spectral5
    
    # data cleanup
    df.cyl = df.cyl.astype(str)
    df.yr = df.yr.astype(str)
    columns = sorted(df.columns)
    
    discrete = [x for x in columns if df[x].dtype == object]
    continuous = [x for x in columns if x not in discrete]
    quantileable = [x for x in continuous if len(df[x].unique()) > 20]
    
    def create_figure():
        xs = df[x.value].tolist()
        ys = df[y.value].tolist()
        x_title = x.value.title()
        y_title = y.value.title()
       name = df['name'].tolist()
    
       kw = dict()
       if x.value in discrete:
           kw['x_range'] = sorted(set(xs))
       if y.value in discrete:
           kw['y_range'] = sorted(set(ys))
       kw['title'] = "%s vs %s" % (y_title, x_title)
    
       p = figure(plot_height=600, plot_width=800,
                  tools='pan,box_zoom,wheel_zoom,lasso_select,reset,save',
                   toolbar_location='above', **kw)
    
       p.xaxis.axis_label = x_title
       p.yaxis.axis_label = y_title
    
       if x.value in discrete:
           p.xaxis.major_label_orientation = pd.np.pi / 4
    
       if size.value != 'None':
           groups = pd.qcut(df[size.value].values, len(SIZES))
           sz = [SIZES[xx] for xx in groups.codes]
       else:
           sz = [9] * len(xs)        
    
       if color.value != 'None':
           coloring = df[color.value].tolist()
           cv_95 = np.percentile(np.asarray(coloring), 95)
           mapper = LinearColorMapper(palette=Spectral5, 
                                      low=cv_min, high=cv_95)
           mapper.low_color = 'blue'
           mapper.high_color = 'red'
           add_color_bar = True
           ninety_degrees = pd.np.pi / 2.
           color_bar = ColorBar(color_mapper=mapper, title='',
                                #title=color.value.title(),
                                title_text_font_style='bold',
                                title_text_font_size='20px',
                                title_text_align='center',
                                orientation='vertical',
                                major_label_text_font_size='16px',
                                major_label_text_font_style='bold',
                                label_standoff=8,
                                major_tick_line_color='black',
                                major_tick_line_width=3,
                                major_tick_in=12,
                                location=(0,0))
        else:
            c = ['#31AADE'] * len(xs)
            add_color_bar = False
    
        if add_color_bar:
             source = ColumnDataSource(data=dict(x=xs, y=ys, 
                                       c=coloring, size=sz, name=name))
        else:
             source = ColumnDataSource(data=dict(x=xs, y=ys, color=c, 
                                       size=sz, name=name))
    
        if add_color_bar:
            p.circle('x', 'y', fill_color={'field': 'c', 
                     'transform': mapper},
                     line_color=None, size='size', source=source)
        else:
            p.circle('x', 'y', color='color', size='size', source=source)
    
        p.add_tools(HoverTool(tooltips=[('x', '@x'), ('y', '@y'), 
                    ('desc', '@name')]))
    
        if add_color_bar:
            color_bar_label = Label(text=color.value.title(),
                                    angle=ninety_degrees,
                                    text_color='black',
                                    text_font_style='bold',
                                    text_font_size='20px',
                                    x=25, y=300, 
                                    x_units='screen', y_units='screen')
             p.add_layout(color_bar, 'right')
             p.add_layout(color_bar_label, 'right')
    
    
        return p
    
    
    def update(attr, old, new):
        layout.children[1] = create_figure()
    
    
    x = Select(title='X-Axis', value='mpg', options=columns)
    x.on_change('value', update)
    
    y = Select(title='Y-Axis', value='hp', options=columns)
    y.on_change('value', update)
    
    size = Select(title='Size', value='None', 
                  options=['None'] + quantileable)
    size.on_change('value', update)
    
    color = Select(title='Color', value='None', 
                   options=['None'] + quantileable)
    color.on_change('value', update)
    
    controls = widgetbox([x, y, color, size], width=200)
    layout = row(controls, create_figure())
    
    curdoc().add_root(layout)
    curdoc().title = "Crossfilter"
    

从 Bokeh 0.12.10 开始,没有可用于颜色条的内置标签。除了您的方法或类似的方法之外,另一种可能性是 custom extension,尽管这同样不是微不足道的。

顺便说一句,colobar 标签似乎是值得考虑的合理事情。关于它应该是平凡可用的概念,如果您就他们认为应该平凡可用的内容对所有用户进行民意调查,将会有数以千计的 不同 优先级建议。正如在 OSS 世界中经常发生的情况一样,可以做的事情远远多于做这些事情的人(在这种情况下少于 3 个)。所以,首先建议 GitHub Issue 请求该功能,其次,如果你有能力,自愿帮助实现它。您的贡献将很有价值,并会受到许多人的赞赏。

您可以将垂直标签添加到颜色栏,方法是将其绘制在单独的轴上并向该轴添加标题。为了说明这一点,这里是 Bokeh 的标准 Colorbar 示例的修改版本(找到 here):

import numpy as np

from bokeh.plotting import figure, output_file, show
from bokeh.models import LogColorMapper, LogTicker, ColorBar
from bokeh.layouts import row

plot_height = 500
plot_width = 500
color_bar_height = plot_height + 11
color_bar_width = 180

output_file('color_bar.html')

def normal2d(X, Y, sigx=1.0, sigy=1.0, mux=0.0, muy=0.0):
    z = (X-mux)**2 / sigx**2 + (Y-muy)**2 / sigy**2
    return np.exp(-z/2) / (2 * np.pi * sigx * sigy)

X, Y = np.mgrid[-3:3:100j, -2:2:100j]
Z = normal2d(X, Y, 0.1, 0.2, 1.0, 1.0) + 0.1*normal2d(X, Y, 1.0, 1.0)
image = Z * 1e6

color_mapper = LogColorMapper(palette="Viridis256", low=1, high=1e7)

plot = figure(x_range=(0,1), y_range=(0,1), toolbar_location=None,
             width=plot_width, height=plot_height)
plot.image(image=[image], color_mapper=color_mapper,
           dh=[1.0], dw=[1.0], x=[0], y=[0])

现在,要制作 Colorbar,请创建一个单独的虚拟图,将 Colorbar 添加到虚拟图并将其放在主图旁边。添加 Colorbar 标签作为虚拟图的标题并将其适当居中。

color_bar = ColorBar(color_mapper=color_mapper, ticker=LogTicker(),
                     label_standoff=12, border_line_color=None, location=(0,0))

color_bar_plot = figure(title="My color bar title", title_location="right", 
                        height=color_bar_height, width=color_bar_width, 
                        toolbar_location=None, min_border=0, 
                        outline_line_color=None)

color_bar_plot.add_layout(color_bar, 'right')
color_bar_plot.title.align="center"
color_bar_plot.title.text_font_size = '12pt'

layout = row(plot, color_bar_plot)

show(layout)

这给出了以下输出图像:

需要注意的一件事是 color_bar_width 设置得足够宽以包含颜色栏、其轴标签和颜色栏标签。如果宽度设置得太小,你会得到一个错误并且绘图不会呈现。