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
相关产品推荐
相关产品推荐

