You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何实现Bokeh选择直方图轴数据随散点图显示/隐藏同步更新?

Syncing Bokeh Histograms with Legend Click Visibility

Hey there! Great job putting together that linked scatter plot and histograms—let's get those histograms updating when you click the legend to show/hide groups. I'll break this down step by step since you mentioned you're new to Python and Bokeh, and we'll also clean up some redundant code along the way.

Key Concepts to Fix This

To sync the histograms with the scatter plot's visibility, we need three main changes:

  1. Use ColumnDataSource for all data: This lets Bokeh track and update data dynamically, which is essential for interactive features.
  2. Create dynamic histogram data sources: Instead of static histograms, we'll use a ColumnDataSource for the histograms so we can update their values.
  3. Add a JavaScript callback: Bokeh uses JavaScript for client-side interactions (like legend clicks). We'll write a callback that checks which groups are visible, recalculates the histograms, and updates the plot.

Updated Full Code

import numpy as np
import pandas as pd
from bokeh.layouts import row, column
from bokeh.models import BoxSelectTool, LassoSelectTool, Spacer, ColumnDataSource, CustomJS
from bokeh.plotting import figure, show
from bokeh.palettes import Spectral4

# Generate sample data (no changes here, just keeping it clean)
data1 = pd.DataFrame({'m/z': np.random.random(100), 'RT': np.random.random(100)})
data2 = pd.DataFrame({'m/z': np.random.random(100), 'RT': np.random.random(100)})

# Create ColumnDataSources for each group (critical for interactivity)
source_035 = ColumnDataSource(data1)
source_044 = ColumnDataSource(data2)

# ----------------------
# Scatter Plot Setup
# ----------------------
p = figure(
    x_axis_label='m/z', y_axis_label='RT',
    tools="", plot_width=600, plot_height=600,
    min_border=10, min_border_left=50,
    toolbar_location="above", title="Mass Recovery Comparison"
)
p.background_fill_color = "#fafafa"
p.select(BoxSelectTool).select_every_mousemove = False
p.select(LassoSelectTool).select_every_mousemove = False

# Add scatter glyphs using our data sources
glyph_035 = p.scatter(
    'm/z', 'RT', source=source_035,
    line_width=2, color=Spectral4[0], alpha=0.8,
    legend_label='035'
)
glyph_044 = p.scatter(
    'm/z', 'RT', source=source_044,
    line_width=2, color=Spectral4[1], alpha=0.8,
    legend_label='044'
)

# Legend setup (keep the click hide policy)
p.legend.location = "top_right"
p.legend.click_policy = "hide"

# ----------------------
# Histogram Setup (Dynamic!)
# ----------------------
# Calculate initial histogram values for all data
bins = 20
all_mz = pd.concat([data1['m/z'], data2['m/z']])
h_hist, h_edges = np.histogram(all_mz, bins=bins)
all_rt = pd.concat([data1['RT'], data2['RT']])
v_hist, v_edges = np.histogram(all_rt, bins=bins)

# Create data sources for histograms so we can update them
h_source = ColumnDataSource(data={
    'left': h_edges[:-1], 'right': h_edges[1:],
    'top': h_hist, 'bottom': [0]*bins
})
v_source = ColumnDataSource(data={
    'bottom': v_edges[:-1], 'top': v_edges[1:],
    'right': v_hist, 'left': [0]*bins
})

# Horizontal histogram (m/z)
ph = figure(
    toolbar_location=None, plot_width=p.plot_width, plot_height=200,
    x_range=p.x_range, y_range=(0, max(h_hist)*1.1),
    min_border=10, min_border_left=50, y_axis_location="right"
)
ph.xgrid.grid_line_color = None
ph.yaxis.major_label_orientation = np.pi/4
ph.background_fill_color = "#fafafa"
ph.quad(
    bottom='bottom', left='left', right='right', top='top',
    source=h_source, color="white", line_color="#3A5785"
)

# Vertical histogram (RT)
pv = figure(
    toolbar_location=None, plot_width=200, plot_height=p.plot_height,
    x_range=(0, max(v_hist)*1.1), y_range=p.y_range,
    min_border=10, y_axis_location="right"
)
pv.ygrid.grid_line_color = None
pv.xaxis.major_label_orientation = np.pi/4
pv.background_fill_color = "#fafafa"
pv.quad(
    left='left', bottom='bottom', top='top', right='right',
    source=v_source, color="white", line_color="#3A5785"
)

# ----------------------
# Callback to Sync Histograms
# ----------------------
sync_callback = CustomJS(
    args={
        'sources': [source_035, source_044],
        'h_source': h_source,
        'v_source': v_source,
        'glyphs': [glyph_035, glyph_044],
        'bins': bins
    },
    code="""
    // Collect data from visible groups
    let mz_data = [];
    let rt_data = [];
    
    for (let i = 0; i < glyphs.length; i++) {
        if (glyphs[i].visible) {
            mz_data = mz_data.concat(sources[i].data['m/z']);
            rt_data = rt_data.concat(sources[i].data['RT']);
        }
    }

    // Helper function to calculate histogram in JavaScript
    function compute_histogram(data, num_bins) {
        if (data.length === 0) {
            // If no data, return empty histogram
            return {
                hist: new Array(num_bins).fill(0),
                edges: new Array(num_bins + 1).fill(0)
            };
        }
        
        const min = Math.min(...data);
        const max = Math.max(...data);
        const bin_width = (max - min) / num_bins;
        const edges = [];
        const hist = new Array(num_bins).fill(0);
        
        // Calculate bin edges
        for (let i = 0; i <= num_bins; i++) {
            edges.push(min + i * bin_width);
        }
        
        // Count data points per bin
        data.forEach(d => {
            let bin_idx = Math.floor((d - min) / bin_width);
            // Ensure we don't go out of bounds for max value
            bin_idx = Math.min(bin_idx, num_bins - 1);
            hist[bin_idx]++;
        });
        
        return { hist, edges };
    }

    // Update horizontal (m/z) histogram
    const h_histogram = compute_histogram(mz_data, bins);
    h_source.data = {
        left: h_histogram.edges.slice(0, -1),
        right: h_histogram.edges.slice(1),
        top: h_histogram.hist,
        bottom: new Array(bins).fill(0)
    };

    // Update vertical (RT) histogram
    const v_histogram = compute_histogram(rt_data, bins);
    v_source.data = {
        bottom: v_histogram.edges.slice(0, -1),
        top: v_histogram.edges.slice(1),
        right: v_histogram.hist,
        left: new Array(bins).fill(0)
    };

    // Tell Bokeh to update the plots
    h_source.change.emit();
    v_source.change.emit();
"""
)

# Attach callback to glyph visibility changes (triggered by legend clicks)
glyph_035.js_on_change('visible', sync_callback)
glyph_044.js_on_change('visible', sync_callback)

# Layout and show
layout = column(row(p, pv), row(ph, Spacer(width=200, height=200)))
show(layout)

What Changed & Why

  1. Removed Redundant Code: We got rid of the unnecessary array_data1/array_data2 conversions—we can use the original DataFrames directly with ColumnDataSource.
  2. Per-Group Data Sources: Each group has its own ColumnDataSource, making it easy to access their data when checking visibility.
  3. Dynamic Histogram Sources: The histograms now use ColumnDataSource instead of static arrays, so we can update their values on the fly.
  4. JavaScript Callback: The callback checks which glyphs are visible, collects the corresponding data, recalculates the histograms, and updates the histogram data sources. We attached this callback to each glyph's visible property, which changes when you click the legend.

Notes for Beginners

  • Client-Side vs Server-Side: This uses client-side JavaScript (no Bokeh Server needed), which is great for static plots you can share as HTML. If you were using Bokeh Server, you could write Python callbacks instead.
  • Histogram Calculation: The JavaScript histogram function mirrors what np.histogram does—we calculate bin edges, count data points per bin, and handle edge cases (like no visible data).
  • Testing: Try clicking the legend items—you'll see the histograms update immediately to only include the visible scatter points!

内容的提问来源于stack exchange,提问作者bwinn

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.27 07:15:49