如何在Bokeh的同一图表中叠加多条折线?
解决方案:在Bokeh中叠加分组折线并通过CustomJS控制可见性
问题原因
你遇到的E-1024错误是因为Bokeh的Line、Patch这类连接拓扑型Glyph无法与CDSView过滤配合使用——过滤会打乱数据源的行顺序,导致折线无法正确连接分组内的点。
替代实现方案
以下提供两种符合你需求的方案,均无需使用multi_line:
方案1:为每个分组创建独立数据源,用CustomJS控制折线可见性
这种方法更直观,通过控制每个Line渲染器的visible属性来切换显示,同时保留散点图的全量数据源。
完整代码:
import pandas as pd import numpy as np from bokeh.plotting import figure, show, ColumnDataSource from bokeh.models import CheckboxGroup, CustomJS, factor_mark, factor_cmap, Category10 from bokeh.layouts import column # 构造数据 df = pd.DataFrame( { "seq": list(range(5)) + list(range(5)), "a": ["foo"] * 5 + ["bar"] * 5, "b": np.concatenate( (np.linspace(1, 5, 5, dtype=int), np.linspace(1, 5, 5, dtype=int) + 3) ), } ) # 主数据源(用于散点图) cds_scatter = ColumnDataSource(df) # 分组数据源(用于折线图) cds_foo = ColumnDataSource(df[df["a"] == "foo"]) cds_bar = ColumnDataSource(df[df["a"] == "bar"]) # 创建图表 myfig = figure(y_range=(-1, 10), height=400, width=400) # 绘制散点图 myfig.scatter( "seq", "b", source=cds_scatter, marker=factor_mark("a", ["circle", "diamond"], ["foo", "bar"]), color=factor_cmap("a", Category10[3], ["foo", "bar"]), size=10, ) # 绘制分组折线,分别保存渲染器对象 line_foo = myfig.line("seq", "b", source=cds_foo, color=Category10[3][0], line_width=2) line_bar = myfig.line("seq", "b", source=cds_bar, color=Category10[3][1], line_width=2) # 创建复选框控件,用于切换折线可见性 checkbox = CheckboxGroup(labels=["foo", "bar"], active=[0, 1]) # 编写CustomJS回调,控制折线的visible属性 callback = CustomJS(args=dict(line_foo=line_foo, line_bar=line_bar, checkbox=checkbox), code=""" line_foo.visible = checkbox.active.includes(0); line_bar.visible = checkbox.active.includes(1); """) checkbox.js_on_change("active", callback) # 布局并显示 show(column(checkbox, myfig))
方案2:通过CustomJS重置主数据源实现折线切换
如果你坚持要通过重置cds1.data来切换折线,可以将所有分组数据整合到主数据源的不同列中,通过CustomJS修改折线的y轴字段来实现切换(或替换整个数据源)。
完整代码:
import pandas as pd import numpy as np from bokeh.plotting import figure, show, ColumnDataSource from bokeh.models import Select, CustomJS, factor_mark, factor_cmap, Category10 from bokeh.layouts import column # 构造数据并整理分组列 df = pd.DataFrame( { "seq": list(range(5)) + list(range(5)), "a": ["foo"] * 5 + ["bar"] * 5, "b": np.concatenate( (np.linspace(1, 5, 5, dtype=int), np.linspace(1, 5, 5, dtype=int) + 3) ), } ) # 添加分组专属列:foo_b和bar_b,空值用NaN填充(折线会自动跳过NaN) df["foo_b"] = df.apply(lambda x: x["b"] if x["a"] == "foo" else np.nan, axis=1) df["bar_b"] = df.apply(lambda x: x["b"] if x["a"] == "bar" else np.nan, axis=1) cds1 = ColumnDataSource(df) # 创建图表 myfig = figure(y_range=(-1, 10), height=400, width=400) # 绘制散点图 myfig.scatter( "seq", "b", source=cds1, marker=factor_mark("a", ["circle", "diamond"], ["foo", "bar"]), color=factor_cmap("a", Category10[3], ["foo", "bar"]), size=10, ) # 绘制折线,初始显示foo分组 line = myfig.line("seq", "foo_b", source=cds1, color=Category10[3][0], line_width=2) # 创建下拉选择控件 select = Select(title="显示折线分组", options=["foo", "bar", "全部"], value="foo") # CustomJS回调:修改折线的y字段,或重置数据源实现多线显示 callback = CustomJS(args=dict(line=line, cds=cds1, select=select), code=""" const value = select.value; if (value === "foo") { line.glyph.y = {field: "foo_b"}; line.glyph.line_color = "#1f77b4"; } else if (value === "bar") { line.glyph.y = {field: "bar_b"}; line.glyph.line_color = "#ff7f0e"; } else { // 全部显示:临时修改数据源,添加合并后的折线数据 const data = cds.data; // 构造包含两组连续点的数据(保证seq顺序) const new_seq = [...data.seq.slice(0,5), ...data.seq.slice(5,10)]; const new_b = [...data.b.slice(0,5), ...data.b.slice(5,10)]; cds.data = {seq: new_seq, b: new_b}; line.glyph.y = {field: "b"}; line.glyph.line_color = "#1f77b4"; } """) select.js_on_change("value", callback) # 布局并显示 show(column(select, myfig))
注意事项
- 方案1的多渲染器方式更稳定,适合需要独立控制每条折线的场景;
- 方案2的数据源重置方式需要注意数据顺序,保证折线连接的正确性;
- 如果需要同时显示多条折线,方案1的复选框控制会更灵活。
内容的提问来源于stack exchange,提问作者suvayu
相关产品推荐
相关产品推荐

