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

Flask中Plotly时间线图Y轴标签分组实现(保留单数据线)

问题

我有一个用于生成时间线图的Flask函数,希望按数据列PRODUCT对Y轴标签进行分组,但保留单个数据线条。

原函数代码:

def drawpipelinechart(ta='All'):
    table = pipeline_table().pipe(calc_fpfv_countdown)
    cols = [
        'ID',
        'PROTOCOL',
        'Quarter_Year',
        'Supplier_Selected',
        'TA',
        'PRODUCT'
    ]
    table = table[cols]
    table['End'] = pd.to_datetime(table['Quarter_Year'].str.split(
        ',').str[-1] + '-' + table['Quarter_Year'].str.split(',').str[0].str[-1] + '-01')
    table['Start'] = table['End'] - pd.offsets.MonthBegin(3)
    table['link'] = table.apply(lambda x: f'<a href="/vo/editpipelinerecord/{x["ID"]}" target="_blank" class="nav-link" style="cursor: pointer;" target="_blank"></a>', axis=1)

    now = pd.Timestamp.now()
    end_date = now + pd.offsets.MonthEnd(1) + pd.offsets.DateOffset(months=18)
    table = table[(table['End'] > now) & (table['End'] <= end_date)]
    table = table[table['TA']==ta] if ta != 'All' else table
    table.loc[:, 'Supplier_Selected'] = table['Supplier_Selected'].fillna('Open')
    fig = px.timeline(table, x_start="Start", x_end="End", y="PROTOCOL", text='PROTOCOL',
                      color="Supplier_Selected", hover_name="PROTOCOL", template="simple_white",
                      labels={"Supplier_Selected": "Supplier"}, custom_data=['link'],
                      )
    
    fig.update_yaxes(autorange="reversed")
        .update_xaxes(
            showgrid=True,
            gridwidth=.5,
            gridcolor='lightgray',
            tickformat='%Y-Q%q',
            dtick='M3'
        )
        .update_layout(
            margin=dict(t=100),
            xaxis=dict(side='top'),
            height=200+len(table['PROTOCOL'].unique())*40,
            autosize=True,
            yaxis_title=None,
        )
        .update_traces(textposition='inside',
                       )

    # Add a horizontal line at each y-coordinate
    for y in table['PROTOCOL'].unique():
        fig.add_hline(y=y, line_dash="dot", line_width=1, line_color="gray")

    graphJSON = json.dumps(fig, cls=plotly.utils.PlotlyJSONEncoder)
    
    return graphJSON

当前效果:Y轴每个标签都是单独的PROTOCOL值,无分组。我尝试通过自定义Y轴刻度映射将PROTOCOL替换为PRODUCT,代码如下:

yaxis=dict(
            title=None,
            tickmode='array',
            ticktext=table['PRODUCT'],
            tickvals=table['PROTOCOL'].unique()
        )

修改后效果:Y轴标签变成了PRODUCT值,但所有PROTOCOL都对应到相同的PRODUCT标签,没有实现分组区分,也无法看出每个PRODUCT下包含哪些PROTOCOL。预期效果是Y轴按PRODUCT分组显示,每个分组下展示对应的PROTOCOL线条,分组之间有明显分隔。

解决方案

要实现Y轴按PRODUCT分组,需要通过数值化Y轴位置+自定义刻度与分组标注的方式来实现,具体步骤如下:

1. 预处理数据,为每个PROTOCOL分配数值型Y轴位置

先对数据按PRODUCT和PROTOCOL排序,然后给每个PROTOCOL分配唯一的数值Y值,同时记录每个PRODUCT对应的Y值范围,方便后续添加分组分隔线和标签。

在原代码中,创建fig之前添加以下数据处理代码:

# 按PRODUCT和PROTOCOL排序,确保同组PROTOCOL连续
table = table.sort_values(['PRODUCT', 'PROTOCOL'])

# 为每个PROTOCOL分配唯一的数值Y值
unique_protocols = table['PROTOCOL'].unique()
protocol_y_map = {proto: i for i, proto in enumerate(unique_protocols)}
table['y_val'] = table['PROTOCOL'].map(protocol_y_map)

# 记录每个PRODUCT对应的Y值范围(用于分组分隔)
product_y_ranges = table.groupby('PRODUCT')['y_val'].agg(['min', 'max'])

2. 修改timeline的Y轴为数值型字段

创建fig时,将y参数改为我们刚生成的y_val,同时保留hover_name等字段:

fig = px.timeline(table, x_start="Start", x_end="End", y="y_val", text='PROTOCOL',
                  color="Supplier_Selected", hover_name="PROTOCOL", template="simple_white",
                  labels={"Supplier_Selected": "Supplier"}, custom_data=['link'],
                  )

3. 自定义Y轴刻度,显示PROTOCOL和分组标签

在update_layout中配置Y轴,将数值y_val映射回PROTOCOL标签,同时加大左侧边距容纳分组标题:

# 配置Y轴刻度:显示PROTOCOL名称
fig.update_layout(
    margin=dict(t=100, l=150),  # 加大左侧边距,容纳分组标签
    xaxis=dict(side='top'),
    height=200+len(unique_protocols)*40,
    autosize=True,
    yaxis=dict(
        title=None,
        tickmode='array',
        ticktext=unique_protocols,  # 显示PROTOCOL名称
        tickvals=list(protocol_y_map.values()),  # 对应数值Y值
        autorange="reversed",
        tickfont=dict(size=10),
    )
)

4. 添加分组分隔线和PRODUCT标签

遍历每个PRODUCT的Y值范围,添加分隔线和分组标题:

# 添加PRODUCT分组标签和分隔线
for product, (y_min, y_max) in product_y_ranges.iterrows():
    # 添加分组标题标注
    fig.add_annotation(
        x=-0.1,  # 调整位置到Y轴左侧
        y=(y_min + y_max)/2,
        text=product,
        showarrow=False,
        font=dict(size=12, weight='bold'),
        xref="paper",
        yref="y"
    )
    # 添加分组顶部分隔线
    fig.add_hline(y=y_max + 0.5, line_dash="solid", line_width=1.5, line_color="black")
# 添加最底部的分隔线
first_product_min = product_y_ranges['min'].iloc[0]
fig.add_hline(y=first_product_min - 0.5, line_dash="solid", line_width=1.5, line_color="black")

# 替换原有的每个PROTOCOL的虚线
for y in protocol_y_map.values():
    fig.add_hline(y=y, line_dash="dot", line_width=1, line_color="gray")

完整修改后的函数代码

def drawpipelinechart(ta='All'):
    table = pipeline_table().pipe(calc_fpfv_countdown)
    cols = [
        'ID',
        'PROTOCOL',
        'Quarter_Year',
        'Supplier_Selected',
        'TA',
        'PRODUCT'
    ]
    table = table[cols]
    table['End'] = pd.to_datetime(table['Quarter_Year'].str.split(
        ',').str[-1] + '-' + table['Quarter_Year'].str.split(',').str[0].str[-1] + '-01')
    table['Start'] = table['End'] - pd.offsets.MonthBegin(3)
    table['link'] = table.apply(lambda x: f'<a href="/vo/editpipelinerecord/{x["ID"]}" target="_blank" class="nav-link" style="cursor: pointer;" target="_blank"></a>', axis=1)

    now = pd.Timestamp.now()
    end_date = now + pd.offsets.MonthEnd(1) + pd.offsets.DateOffset(months=18)
    table = table[(table['End'] > now) & (table['End'] <= end_date)]
    table = table[table['TA']==ta] if ta != 'All' else table
    table.loc[:, 'Supplier_Selected'] = table['Supplier_Selected'].fillna('Open')

    # --- 新增数据预处理:分配数值Y值,按PRODUCT分组 ---
    table = table.sort_values(['PRODUCT', 'PROTOCOL'])
    unique_protocols = table['PROTOCOL'].unique()
    protocol_y_map = {proto: i for i, proto in enumerate(unique_protocols)}
    table['y_val'] = table['PROTOCOL'].map(protocol_y_map)
    product_y_ranges = table.groupby('PRODUCT')['y_val'].agg(['min', 'max'])

    # --- 修改timeline的Y轴为数值型字段 ---
    fig = px.timeline(table, x_start="Start", x_end="End", y="y_val", text='PROTOCOL',
                      color="Supplier_Selected", hover_name="PROTOCOL", template="simple_white",
                      labels={"Supplier_Selected": "Supplier"}, custom_data=['link'],
                      )
    
    fig.update_xaxes(
            showgrid=True,
            gridwidth=.5,
            gridcolor='lightgray',
            tickformat='%Y-Q%q',
            dtick='M3'
        )
        .update_layout(
            margin=dict(t=100, l=150),  # 加大左侧边距容纳分组标签
            xaxis=dict(side='top'),
            height=200+len(unique_protocols)*40,
            autosize=True,
            yaxis=dict(
                title=None,
                tickmode='array',
                ticktext=unique_protocols,
                tickvals=list(protocol_y_map.values()),
                autorange="reversed",
                tickfont=dict(size=10),
            )
        )
        .update_traces(textposition='inside',
                       )

    # --- 添加PRODUCT分组标签和分隔线 ---
    for product, (y_min, y_max) in product_y_ranges.iterrows():
        fig.add_annotation(
            x=-0.1,
            y=(y_min + y_max)/2,
            text=product,
            showarrow=False,
            font=dict(size=12, weight='bold'),
            xref="paper",
            yref="y"
        )
        fig.add_hline(y=y_max + 0.5, line_dash="solid", line_width=1.5, line_color="black")
    # 添加最底部的分隔线
    first_product_min = product_y_ranges['min'].iloc[0]
    fig.add_hline(y=first_product_min - 0.5, line_dash="solid", line_width=1.5, line_color="black")

    # 添加每个PROTOCOL的水平虚线
    for y in protocol_y_map.values():
        fig.add_hline(y=y, line_dash="dot", line_width=1, line_color="gray")

    graphJSON = json.dumps(fig, cls=plotly.utils.PlotlyJSONEncoder)
    
    return graphJSON

效果说明

  • Y轴左侧显示加粗的PRODUCT分组标题,每个分组下对应所属的PROTOCOL时间线
  • 分组之间用粗实线分隔,每个PROTOCOL之间保留原有的虚线
  • 所有单个数据线条完整保留,hover和交互逻辑不受影响

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.30 15:51:44