如何实现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:
- Use
ColumnDataSourcefor all data: This lets Bokeh track and update data dynamically, which is essential for interactive features. - Create dynamic histogram data sources: Instead of static histograms, we'll use a
ColumnDataSourcefor the histograms so we can update their values. - 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
- Removed Redundant Code: We got rid of the unnecessary
array_data1/array_data2conversions—we can use the original DataFrames directly withColumnDataSource. - Per-Group Data Sources: Each group has its own
ColumnDataSource, making it easy to access their data when checking visibility. - Dynamic Histogram Sources: The histograms now use
ColumnDataSourceinstead of static arrays, so we can update their values on the fly. - 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
visibleproperty, 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.histogramdoes—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
相关产品推荐
相关产品推荐

