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

如何在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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.21 11:00:56