如何在Shiny for Python中实现点击柱状图分段筛选表格?
解决Shiny for Python中Plotly堆叠柱状图点击筛选问题
问题拆解
你的代码存在几个关键问题导致点击事件无响应且无法实现筛选:
- 仅为第一个trace绑定了点击事件,未覆盖所有柱状分段
customdata未正确传递每个分段的实际分类值- 点击处理函数使用了
@reactive.calc装饰器,无法接收点击事件的参数 - 未将点击事件与数据集的反应式筛选逻辑关联
修复方案
1. 添加反应式变量存储点击筛选条件
在server函数开头定义一个反应式变量,用于记录用户点击的柱状分段:
clicked_filters = reactive.Value({})
2. 编写正确的点击处理函数
移除@reactive.calc装饰器,定义能接收trace、points和state参数的处理函数,实现点击切换筛选的逻辑:
def filter_fn(trace, points, state): # 获取点击位置对应的分类值 clicked_category = input.category() clicked_value = trace.customdata[points.point_inds[0]] # 切换筛选状态:点击添加/移除该分类值 current_filters = clicked_filters.get() if clicked_value in current_filters.get(clicked_category, []): current_filters[clicked_category].remove(clicked_value) if not current_filters[clicked_category]: del current_filters[clicked_category] else: current_filters.setdefault(clicked_category, []).append(clicked_value) # 更新反应式变量,触发后续数据更新 clicked_filters.set(current_filters)
3. 正确配置customdata并绑定所有trace的点击事件
在生成Plotly图表时,为每个trace设置正确的customdata,并遍历所有trace绑定点击事件:
@render_widget def penguin_plot(): df_plot = df_summarized() bar_columns = list(df_plot['year'].unique()) bar_segments = list(df_plot[input.category()].unique()) # 为每个分段创建trace,正确设置customdata data = [go.Bar( name=segment, x=bar_columns, y=list(df_plot[df_plot[input.category()] == segment]['count'].values), customdata=[segment] * len(bar_columns) # 每个bar对应当前分段的分类值 ) for segment in bar_segments] fig = go.Figure(data) fig.update_layout(barmode="stack") fig = go.FigureWidget(fig) # 为所有trace绑定点击事件 for trace in fig.data: trace.on_click(filter_fn) return fig
4. 整合点击筛选到数据集处理逻辑
修改df_filtered_stage2,结合侧边栏筛选和点击筛选:
@reactive.calc def df_filtered_stage2(): df = df_filtered_stage1() current_filters = clicked_filters.get() # 应用点击的筛选条件 for col, values in current_filters.items(): if values: df = df[df[col].isin(values)] return df
5. 修复参数面板的语法错误
原代码中parameter_shelf函数末尾多了一个逗号,导致返回元组而非UI组件,需修正:
def parameter_shelf(): return ui.card( ui.card_header( 'Parameters', align='center', ), ui.input_radio_buttons( 'category', label = 'View Penguins by:', choices=dict_category, selected = 'species', ), ) # 移除末尾的逗号
修改后的完整代码
# Load data and compute static values from shiny import App, reactive, render, ui from shinywidgets import output_widget, render_widget import plotly.graph_objects as go import palmerpenguins df_penguins = palmerpenguins.load_penguins() df_penguins = df_penguins.dropna(subset=['sex']) # 清理缺失值,避免筛选出错 dict_category = {'species':'Species','island':'Island','sex':'Gender'} def filter_shelf(): return ui.card( ui.card_header( "Filters", align="center", ), # Gender Filter ui.input_checkbox_group( 'sex_filter', label='Gender', choices={value:(value.capitalize() if isinstance(value, str) else value) for value in df_penguins['sex'].unique()}, selected=list(df_penguins['sex'].unique()), ), # Species Filter ui.input_checkbox_group( 'species_filter', label='Species', choices=list(df_penguins['species'].unique()), selected=list(df_penguins['species'].unique()), ), # Island Filter ui.input_checkbox_group( 'island_filter', label='Island', choices=list(df_penguins['island'].unique()), selected=list(df_penguins['island'].unique()), ), ) def parameter_shelf(): return ui.card( ui.card_header( 'Parameters', align='center', ), ui.input_radio_buttons( 'category', label = 'View Penguins by:', choices=dict_category, selected = 'species', ), ) app_ui = ui.page_fluid( ui.h1("Palmer Penguins Analysis"), ui.layout_sidebar( # Left Sidebar ui.sidebar( filter_shelf(), parameter_shelf(), width=250, ), # Main Panel ui.card( # Plot ui.card_header(ui.output_text('chart_title')), output_widget('penguin_plot'), ), ui.card( # Table ui.card_header(ui.output_text('total_rows')), ui.column( 12, #width ui.output_table('table_view'), style="height:300px; overflow-y: scroll" ) ) ), ) def server (input, output, session): # 存储点击的筛选条件 clicked_filters = reactive.Value({}) @reactive.calc def category(): '''This function caches the appropriate Capitalized form of the selected category''' return dict_category[input.category()] # Dynamic Chart Title @render.text def chart_title(): return "Number of Palmer Penguins by Year, colored by " + category() @reactive.calc def df_filtered_stage1(): '''This function caches the filtered datframe based on selections in the view''' return df_penguins[ (df_penguins['species'].isin(input.species_filter())) & (df_penguins['island'].isin(input.island_filter())) & (df_penguins['sex'].isin(input.sex_filter()))] @reactive.calc def df_filtered_stage2(): df = df_filtered_stage1() current_filters = clicked_filters.get() # 应用点击筛选条件 for col, values in current_filters.items(): if values: df = df[df[col].isin(values)] return df @reactive.calc def df_summarized(): return df_filtered_stage2().groupby(['year', input.category()], as_index=False).count()\ .rename({'body_mass_g': "count"}, axis=1)[['year', input.category(), 'count']] def filter_fn(trace, points, state): # 获取点击的分类值 clicked_category = input.category() clicked_value = trace.customdata[points.point_inds[0]] # 切换筛选状态 current_filters = clicked_filters.get() if clicked_value in current_filters.get(clicked_category, []): current_filters[clicked_category].remove(clicked_value) if not current_filters[clicked_category]: del current_filters[clicked_category] else: current_filters.setdefault(clicked_category, []).append(clicked_value) clicked_filters.set(current_filters) @render_widget def penguin_plot(): df_plot = df_summarized() bar_columns = list(df_plot['year'].unique()) bar_segments = list(df_plot[input.category()].unique()) data = [go.Bar( name=segment, x=bar_columns, y=list(df_plot[df_plot[input.category()] == segment]['count'].values), customdata=[segment] * len(bar_columns) ) for segment in bar_segments] fig = go.Figure(data) fig.update_layout(barmode="stack") fig = go.FigureWidget(fig) # 为所有trace绑定点击事件 for trace in fig.data: trace.on_click(filter_fn) return fig @render.text def total_rows(): return "Total Rows: " + str(df_filtered_stage2().shape[0]) @render.table def table_view(): return df_filtered_stage2() app = App(app_ui, server)
关键改进说明
- 实现了点击柱状分段切换筛选的功能,重复点击可取消筛选
- 所有trace均绑定点击事件,无需单独为每个分段手动绑定
customdata正确传递每个bar对应的分类值,确保点击时能获取准确的筛选条件- 整合了侧边栏筛选和点击筛选的逻辑,双重筛选生效
- 修复了原代码中的语法错误和数据缺失问题
内容的提问来源于stack exchange,提问作者Dave Guenther
相关产品推荐
相关产品推荐

