数据源#

任何数据可视化的基础都是底层数据。本节介绍了向Bokeh提供数据的各种方法,从直接传递数据值到创建ColumnDataSource (CDS)以及使用CDSView过滤数据。

使用Python列表提供数据#

使用标准的Python数据列表将值直接传递到绘图函数中。

在此示例中,列表x_valuesy_values将数据传递给circle()函数(有关更多示例,请参阅绘图函数

from bokeh.plotting import figure

x_values = [1, 2, 3, 4, 5]
y_values = [6, 7, 2, 3, 6]

p = figure()
p.circle(x=x_values, y=y_values)

提供NumPy数据#

与使用Python列表和数组类似,您也可以在Bokeh中使用NumPy数据结构。

import numpy as np
from bokeh.plotting import figure

x = [1, 2, 3, 4, 5]
random = np.random.standard_normal(5)
cosine = np.cos(x)

p = figure()
p.circle(x=x, y=random)
p.line(x=x, y=cosine)

将数据作为ColumnDataSource提供#

ColumnDataSource (CDS)是大多数Bokeh绘图的核心。它为绘图的图元提供数据。

当您将Python列表或NumPy数组之类的序列传递给Bokeh渲染器时,Bokeh会自动为您创建包含此数据的ColumnDataSource。但是,自己创建ColumnDataSource可以访问更多高级选项。

例如:创建您自己的ColumnDataSource允许您在多个绘图和部件之间共享数据。如果您将单个ColumnDataSource与多个渲染器一起使用,则这些渲染器还将共享有关您使用Bokeh工具栏中的选择工具选择的数据的信息(请参阅链接选择)。

将ColumnDataSource视为每个都有自己唯一列名称的数据序列的集合。

创建ColumnDataSource#

要创建基本的ColumnDataSource对象,您需要一个Python字典传递给对象的data参数。

  • Bokeh使用字典的键作为列名。

  • 字典的值用作ColumnDataSource的数据值。

您作为字典的一部分传递的数据可以是任何非字符串的有序值序列,例如列表或数组(包括NumPy数组和pandas Series)。

data = {'x_values': [1, 2, 3, 4, 5],
        'y_values': [6, 7, 2, 3, 6]}

source = ColumnDataSource(data=data)

注意

ColumnDataSource中的所有列都具有相同的长度。因此,您传递给单个ColumnDataSource的所有值序列也必须具有相同的长度。如果您尝试传递不同长度的序列,Bokeh将无法创建您的ColumnDataSource。

使用ColumnDataSource绘图#

要将ColumnDataSource与渲染器函数一起使用,您需要传递至少以下三个参数。

  • x:包含绘图x值的ColumnDataSource列的名称。

  • y:包含绘图y值的ColumnDataSource列的名称。

  • source:包含您刚刚为xy参数引用的列的ColumnDataSource的名称。

例如

from bokeh.plotting import figure
from bokeh.models import ColumnDataSource

# create a Python dict as the basis of your ColumnDataSource
data = {'x_values': [1, 2, 3, 4, 5],
        'y_values': [6, 7, 2, 3, 6]}

# create a ColumnDataSource by passing the dict
source = ColumnDataSource(data=data)

# create a plot using the ColumnDataSource's two columns
p = figure()
p.circle(x='x_values', y='y_values', source=source)

修改ColumnDataSource#

要修改现有ColumnDataSource的数据,请更新ColumnDataSource对象的.data属性。

  • 要向现有ColumnDataSource添加新列。

    new_sequence = [8, 1, 4, 7, 3]
    source.data["new_column"] = new_sequence
    

    注意

    您添加的列的长度必须与现有列的长度匹配。

  • 要替换现有ColumnDataSource中的所有数据,请为.data属性分配一个全新的字典。

    source.data = new_dict
    

    注意

    替换ColumnDataSource的整个内容也是更新其列长度的唯一方法。当您以更改任何列长度的方式更新数据时,必须通过传递一个新字典同时更新所有列。无法一次更新一个列的列长度。

使用pandas DataFrame#

data参数也可以是pandas DataFrameGroupBy对象。

source = ColumnDataSource(df)

如果您使用pandas DataFrame,则Bokeh中的结果ColumnDataSource将具有与DataFrame的列相对应的列。列的命名遵循以下规则。

  • 如果DataFrame具有命名的索引列,则ColumnDataSource也将具有此名称的列。

  • 如果索引名称为None,则ColumnDataSource将具有通用名称:index(如果该名称可用)或level_0

使用pandas MultiIndex#

如果您使用pandas MultiIndex作为Bokeh ColumnDataSource的基础,则Bokeh会在创建ColumnDataSource之前展平列和索引。对于索引,Bokeh创建元组的索引并将MultiIndex的名称用下划线连接。列名也将用下划线连接。例如

df = pd.DataFrame({('a', 'b'): {('A', 'B'): 1, ('A', 'C'): 2},
                   ('b', 'a'): {('A', 'C'): 7, ('A', 'B'): 8},
                   ('b', 'b'): {('A', 'D'): 9, ('A', 'B'): 10}})
cds = ColumnDataSource(df)

这将导致名为index的列,其中包含[(A, B), (A, C), (A, D)],以及名为a_bb_ab_b的列。

此过程仅适用于字符串列名。如果您使用非字符串列名,则需要在将其用作Bokeh ColumnDataSource的基础之前手动展平DataFrame

使用pandas GroupBy#

group = df.groupby(('colA', 'ColB'))
source = ColumnDataSource(group)

如果您使用pandas GroupBy对象,则ColumnDataSource的列对应于调用group.describe()的结果。describe方法为所有未分组的原始列生成统计度量(如meancount)的列。

结果DataFrame具有MultiIndex列,其中包含原始列名和计算的度量。Bokeh使用上面描述的规则展平数据。

例如:如果一个 DataFrame 包含列 'year''mpg',将 df.groupby('year') 传递给 ColumnDataSource 将导致出现诸如 'mpg_mean' 这样的列。

注意

调整 GroupBy 对象需要 pandas 版本 0.20.0 或更高版本。

向 ColumnDataSource 追加数据#

ColumnDataSource 流式传输是向 ColumnDataSource 追加新数据的有效方法。当您使用 stream() 方法时,Bokeh 只将新数据发送到浏览器,而不是发送整个数据集。

stream() 方法接受一个 new_data 参数。此参数需要一个字典,该字典将列名映射到您想要追加到相应列的数据序列。

该方法还带有一个可选参数 rollover。这是要保留数据的最大长度。当数据超过最大值定义时,Bokeh 将丢弃该列开头的数据。 rollover 的默认值为 None。此默认值允许数据无限增长。

source = ColumnDataSource(data=dict(foo=[], bar=[]))

# has new, identical-length updates for all columns in source
new_data = {
    'foo' : [10, 20],
    'bar' : [100, 200],
}

source.stream(new_data)

有关使用流式传输的示例,请参阅 examples/server/app/ohlc

替换 ColumnDataSource 中的数据#

ColumnDataSource 修补是一种有效更新数据源切片的方法。通过使用 patch() 方法,Bokeh 只将新数据发送到浏览器,而不是发送整个数据集。

patch() 需要一个字典,该字典将列名映射到表示要应用的修补更改的元组列表。

您可以与 patch() 一起使用的元组示例

(index, new_value)  # replace a single column value

# or

(slice, new_values) # replace several column values

有关完整示例,请参阅 examples/server/app/patch_app.py

转换数据#

到目前为止,您已将数据添加到 ColumnDataSource 以控制 Bokeh 图表。但是,您也可以在浏览器中直接执行一些数据操作。

例如,在浏览器中动态计算颜色映射可以减少 Python 代码量。如果颜色映射的必要计算直接在浏览器中发生,您也需要发送较少的数据。

本节概述了可用的不同转换对象。

客户端颜色映射#

通过颜色映射,您可以将数据序列中的值编码为特定颜色。

Bokeh 提供三个函数,用于直接在浏览器中执行颜色映射

所有三个函数的操作方式类似,并接受以下参数

  • 包含要映射颜色的数据的 ColumnDataSource 列的名称

  • 调色板(可以是 Bokeh 的预定义调色板 之一或自定义颜色列表)

  • 颜色映射范围的 minmax 值。

颜色映射函数将数据源中的数值映射到调色板的颜色,从 minmax 值。

例如,使用 linear_cmap() 函数,范围为 [0,99],颜色为 ['red', 'green', 'blue'],将导致以下值到颜色的映射

      x < 0  : 'red'     # values < low are clamped
 0 >= x < 33 : 'red'
33 >= x < 66 : 'green'
66 >= x < 99 : 'blue'
99 >= x      : 'blue'    # values > high are clamped

例如

fill_color=linear_cmap('counts', 'Viridis256', min=0, max=10)

将颜色映射与绘图对象的 color 属性 一起使用,例如 fill_color

from numpy.random import standard_normal

from bokeh.plotting import figure, show
from bokeh.transform import linear_cmap
from bokeh.util.hex import hexbin

x = standard_normal(50000)
y = standard_normal(50000)

bins = hexbin(x, y, 0.1)

p = figure(tools="", match_aspect=True, background_fill_color='#440154')
p.grid.visible = False

p.hex_tile(q="q", r="r", size=0.1, line_color=None, source=bins,
           fill_color=linear_cmap('counts', 'Viridis256', 0, max(bins.counts)))

show(p)

映射器函数返回的 dataspec 包含 bokeh.transform。您可以访问此数据以在不同的上下文中使用映射器函数的结果。例如,要创建 ColorBar

from bokeh.models import ColumnDataSource
from bokeh.plotting import figure, show
from bokeh.transform import linear_cmap

x = list(range(1, 11))
y = list(range(1, 11))

source = ColumnDataSource(dict(x=x,y=y))

p = figure(width=300, height=300, title="Linear color map based on Y")

# use the field name of the column source
cmap = linear_cmap(field_name='y', palette="Spectral6", low=min(y), high=max(y))

r = p.scatter(x='x', y='y', color=cmap, size=15, source=source)

# create a color bar from the scatter glyph renderer
color_bar = r.construct_color_bar(width=10)

p.add_layout(color_bar, 'right')

show(p)

映射标记类型#

当您使用分类数据时,您可以为数据中的每个类别使用不同的标记。使用 factor_mark() 函数自动为不同的类别分配不同的标记

from bokeh.plotting import figure, show
from bokeh.sampledata.penguins import data
from bokeh.transform import factor_cmap, factor_mark

SPECIES = sorted(data.species.unique())
MARKERS = ['hex', 'circle_x', 'triangle']

p = figure(title = "Penguin size", background_fill_color="#fafafa")
p.xaxis.axis_label = 'Flipper Length (mm)'
p.yaxis.axis_label = 'Body Mass (g)'

p.scatter("flipper_length_mm", "body_mass_g", source=data,
          legend_group="species", fill_alpha=0.4, size=12,
          marker=factor_mark('species', MARKERS, SPECIES),
          color=factor_cmap('species', 'Category10_3', SPECIES))

p.legend.location = "top_left"
p.legend.title = "Species"

show(p)

此示例还使用 factor_cmap() 对这些相同的类别进行颜色映射。

注意

factor_mark() 转换通常仅在 scatter 图形方法中才有用,因为按标记类型进行参数化仅对散点图有意义。

使用 CustomJSTransform 包含 JavaScript 代码#

除了上述内置转换函数外,您还可以使用自己的 JavaScript 代码。使用 CustomJSTransform() 函数添加在浏览器中执行的自定义 JavaScript 代码。

下面的示例使用 CustomJSTransform() 函数以及参数 v_funcv_func 是“矢量化函数”的缩写。您提供给 v_func 的 JavaScript 代码需要在变量 xs 中期望一个输入数组,并返回一个包含转换值的 JavaScript 数组

v_func = """
    const first = xs[0]
    const norm = new Float64Array(xs.length)
    for (let i = 0; i < xs.length; i++) {
        norm[i] = xs[i] / first
    }
    return norm
"""
normalize = CustomJSTransform(v_func=v_func)

plot.line(x='aapl_date', y=transform('aapl_close', normalize), line_width=2,
          color='#cf3c4d', alpha=0.6,legend="Apple", source=aapl_source)

此示例中的代码将原始价格数据转换为相对于第一个数据点的归一化收益序列

过滤数据#

Bokeh 使用一个称为“视图”的概念来选择数据的子集。视图由 Bokeh 的 CDSView 类表示。当您使用视图时,您可以使用一个或多个过滤器来选择特定的数据点,而无需更改基础数据。您还可以跨不同的图表共享这些视图。

要使用过滤后的数据子集进行绘图,请将 CDSView 传递到 Bokeh 图表上任何渲染器方法的 view 参数。

CDSView 具有一个属性, filter

  • filterFilter 模型的实例,如下所列和描述。

在此示例中,您创建一个名为 viewCDSViewview 使用 ColumnDataSource source 以及两个过滤器 filter1filter2 的交集。然后将 view 传递给 circle() 渲染器函数

from bokeh.plotting import figure
from bokeh.models import ColumnDataSource, CDSView

filter1 = ... # IndexFilter(), BooleanFilter(), etc.
filter2 = ...

source = ColumnDataSource(some_data)
view = CDSView(filter=filter1 & filter2)

p = figure()
p.circle(x="x", y="y", source=source, view=view)

IndexFilter#

IndexFilter 是最简单的过滤器类型。它具有一个 indices 属性,该属性是一个整数列表,表示您想要包含在绘图中的数据的索引。

from bokeh.layouts import gridplot
from bokeh.models import CDSView, ColumnDataSource, IndexFilter
from bokeh.plotting import figure, show

source = ColumnDataSource(data=dict(x=[1, 2, 3, 4, 5], y=[1, 2, 3, 4, 5]))
view = CDSView(filter=IndexFilter([0, 2, 4]))

TOOLS = "box_select,hover,reset"

p1 = figure(height=300, width=300, tools=TOOLS)
p1.scatter(x="x", y="y", size=10, hover_color="red", source=source)

p2 = figure(height=300, width=300, tools=TOOLS)
p2.scatter(x="x", y="y", size=10, hover_color="red", source=source, view=view)

show(gridplot([[p1, p2]]))

BooleanFilter#

BooleanFilter 使用其 booleans 属性中的一系列 TrueFalse 值从数据源中选择行。

from bokeh.layouts import gridplot
from bokeh.models import BooleanFilter, CDSView, ColumnDataSource
from bokeh.plotting import figure, show

source = ColumnDataSource(data=dict(x=[1, 2, 3, 4, 5], y=[1, 2, 3, 4, 5]))

bools = [True if y_val > 2 else False for y_val in source.data['y']]
view = CDSView(filter=BooleanFilter(bools))

TOOLS = "box_select,hover,reset"

p1 = figure(height=300, width=300, tools=TOOLS)
p1.scatter(x="x", y="y", size=10, hover_color="red", source=source)

p2 = figure(height=300, width=300, tools=TOOLS,
            x_range=p1.x_range, y_range=p1.y_range)
p2.scatter(x="x", y="y", size=10, hover_color="red", source=source, view=view)

show(gridplot([[p1, p2]]))

GroupFilter#

GroupFilter 是用于分类数据的过滤器。使用此过滤器,您可以从数据集中选择属于特定类别的行。

GroupFilter 具有两个属性

  • column_name:应用过滤器的 ColumnDataSource 中的列名

  • group:要选择的类别的名称

在下面的示例中,数据集 data 包含一个名为 species 的分类变量。所有数据都属于三种物种类别之一:AdelieChinstrapGentoo。此示例中的第二个图使用 GroupFilter 仅显示属于 Adelie 类别的点。

from bokeh.layouts import gridplot
from bokeh.models import CDSView, ColumnDataSource, GroupFilter
from bokeh.plotting import figure, show
from bokeh.sampledata.penguins import data

source = ColumnDataSource(data)
view = CDSView(filter=GroupFilter(column_name="species", group="Adelie"))

TOOLS = "box_select,reset,help"

p1 = figure(title="Full data set", height=300, width=300, tools=TOOLS)
p1.scatter(x="bill_length_mm", y="bill_depth_mm", source=source)

p2 = figure(title="Adelie only", height=300, width=300,
            tools=TOOLS, x_range=p1.x_range, y_range=p1.y_range)
p2.scatter(x="bill_length_mm", y="bill_depth_mm", size=6,
           source=source, view=view, color='darkorange')

show(gridplot([[p1, p2]]))

CustomJSFilter#

您还可以使用您自己的 JavaScript 或 TypeScript 代码来创建自定义过滤器。要包含您的自定义过滤器代码,请使用 Bokeh 的 CustomJSFilter 类。将您的代码作为字符串传递给 CustomJSFilter 的参数 code

您的 JavaScript 或 TypeScript 代码需要返回一个索引列表或一个布尔值列表,表示过滤后的子集。您可以使用 CDSView 在您的 JavaScript 或 TypeScript 代码中访问您正在使用的 ColumnDataSource。Bokeh 通过变量 source 提供 ColumnDataSource。

custom_filter = CustomJSFilter(code='''
const indices = [];

// iterate through rows of data source and see if each satisfies some constraint
for (let i = 0; i < source.get_length(); i++){
    if (source.data['some_column'][i] == 'some_value'){
        indices.push(true);
    } else {
        indices.push(false);
    }
}
return indices;
''')

AjaxDataSource#

更新和流式传输数据与 Bokeh 服务器应用程序 配合得很好。但是,在独立文档中也可以使用类似的功能。 AjaxDataSource 提供了此功能,无需 Bokeh 服务器。

要设置 AjaxDataSource,您需要使用 REST 端点的 URL 和轮询间隔对其进行配置。

在浏览器中,数据源会以指定的间隔从端点请求数据。然后,它使用来自端点的数据来更新本地数据。

本地数据更新可以通过两种方式进行:要么完全替换现有的本地数据,要么将新数据追加到现有数据(最多可配置的 max_size)。替换本地数据是默认设置。将 "replace""append" 作为 AjaxDataSource 的 mode 参数传递以控制此行为。

您与 AjaxDataSource 一起使用的端点应返回一个 JSON dict,该 dict 匹配标准的 ColumnDataSource 格式,即一个 JSON dict,它将名称映射到值的数组。

{
    'x' : [1, 2, 3, ...],
    'y' : [9, 3, 2, ...]
}

或者,如果 REST API 返回不同的格式,则可以通过此数据源的 adapter 属性提供 CustomJS 回调以将 REST 响应转换为 Bokeh 格式。

否则,使用 AjaxDataSource 与使用标准 ColumnDataSource 完全相同。

# setup AjaxDataSource with URL and polling interval
source = AjaxDataSource(data_url='http://some.api.com/data',
                        polling_interval=100)

# use the AjaxDataSource just like a ColumnDataSource
p.circle('x', 'y', source=source)

这是使用 AjaxDataSource 的 Bokeh 中实时数据流的预览。

Animated image showing a timeseries scatter plot updating periodically via the ajax streaming data source.

有关完整示例,请参阅 Bokeh 的 GitHub 存储库中的 examples/basic/data/ajax_source.py

链接选择#

如果两个图都使用相同的 ColumnDataSource,则可以在它们之间共享选择。

from bokeh.layouts import gridplot
from bokeh.models import ColumnDataSource
from bokeh.plotting import figure, show
from bokeh.sampledata.penguins import data
from bokeh.transform import factor_cmap

SPECIES = sorted(data.species.unique())

TOOLS = "box_select,lasso_select,help"

source = ColumnDataSource(data)

left = figure(width=300, height=400, title=None, tools=TOOLS,
              background_fill_color="#fafafa")
left.scatter("bill_length_mm", "body_mass_g", source=source,
             color=factor_cmap('species', 'Category10_3', SPECIES))

right = figure(width=300, height=400, title=None, tools=TOOLS,
               background_fill_color="#fafafa", y_axis_location="right")
right.scatter("bill_depth_mm", "body_mass_g", source=source,
              color=factor_cmap('species', 'Category10_3', SPECIES))

show(gridplot([[left, right]]))

使用过滤数据的链接选择#

使用 ColumnDataSource,您还可以有两个基于相同数据但每个使用该数据不同子集的图。这两个图仍然通过它们所基于的 ColumnDataSource 共享选择和悬停检查。

以下示例演示了此行为。

  • 第二个图是第一个图数据的子集。第二个图使用 CDSView 仅包含大于 250 或小于 100 的 y 值。

  • 如果您使用任一图中的 BoxSelect 工具进行选择,则该选择也会自动反映在另一个图中。

  • 如果您将鼠标悬停在一个图中的一个点上,则另一个图中相应的点也会自动突出显示(如果存在)。

from bokeh.layouts import gridplot
from bokeh.models import BooleanFilter, CDSView, ColumnDataSource
from bokeh.plotting import figure, show

x = list(range(-20, 21))
y0 = [abs(xx) for xx in x]
y1 = [xx**2 for xx in x]

# create a column data source for the plots to share
source = ColumnDataSource(data=dict(x=x, y0=y0, y1=y1))

# create a view of the source for one plot to use
view = CDSView(filter=BooleanFilter([True if y > 250 or y < 100 else False for y in y1]))

TOOLS = "box_select,lasso_select,hover,help"

# create a new plot and add a renderer
left = figure(tools=TOOLS, width=300, height=300, title=None)
left.scatter("x", "y0", size=10, hover_color="firebrick", source=source)

# create another new plot, add a renderer that uses the view of the data source
right = figure(tools=TOOLS, width=300, height=300, title=None)
right.scatter("x", "y1", size=10, hover_color="firebrick", source=source, view=view)

p = gridplot([[left, right]])

show(p)

其他数据类型#

您还可以使用 Bokeh 来呈现网络图数据和地理数据。有关如何为这些类型的图设置数据的更多信息,请参阅 网络图地理数据