Plotly 甘特图:删除重复的 Y 轴标签并堆叠并行任务

Plotly Gantt Chart: Remove Duplicate Y-Axis Labels and Stack Parallel Tasks

我打算使用 python 库 'Plotly' 来构建甘特图。具体来说:https://plotly.com/python/gantt/#group-tasks-together.

但是,每个“作业”可以有多个任务,并且这些任务可以 运行 并行进行。根据我的观察,Plotly 不会将任务 运行 并行堆叠在一起,这使得阅读图表变得异常困难。这是一个示例,其中“作业 A”有两个任务 运行 并行但只有一个可见:

data = [dict(Task="Job A", Start='2009-01-01', Finish='2009-02-28'),
      dict(Task="Job A", Start='2009-01-01', Finish='2009-02-28'),
      dict(Task="Job B", Start='2009-03-05', Finish='2009-04-15'),
      dict(Task="Job C", Start='2009-02-20', Finish='2009-05-30')]

# Without group_tasks=True, There would be two separate "Job A" labels
fig = ff.create_gantt(data, group_tasks=True)
fig.show()

我想要的是两个“作业 A”任务都可见但垂直堆叠,“作业 A”位于其任务占用的垂直 space 中心。像这样但没有两个“工作 A”标签:

如果有人对我的甘特图项目有任何图书馆建议,请随时分享!谢谢!

一个起点是使用 fig.add_shape 在原始 Task.

下方添加一个相同的 Task 作为矩形

为此,我们需要每个矩形的 y 坐标,但为了方便起见,第一个条形位于 y=0,第二个条形位于 y=1,依此类推。因此,按顺序列出的唯一任务的索引也是 y 坐标(唯一任务是 [Job A, Job B, Job C],因此作业 C 栏将以 y=3 为中心)。每个条的默认宽度为 0.8,因此如果 y0 是条的起始 y 坐标,y1 应该在 y0-0.4 处结束。

请注意,带注释的形状不会有任何悬停模板,并且每个条形的颜色与当前的书写方式相同。

import numpy as np
import pandas as pd
import plotly.express as px
import plotly.graph_objects as go

## added additional duplicate Task to demonstrate generalizability
data = [dict(Task="Job A", Start='2009-01-01', Finish='2009-02-28'),
      dict(Task="Job A", Start='2009-01-01', Finish='2009-02-28'),
      dict(Task="Job B", Start='2009-03-05', Finish='2009-04-15'),
      dict(Task="Job C", Start='2009-02-20', Finish='2009-05-30'),
      dict(Task="Job C", Start='2009-02-20', Finish='2009-05-30')]

df = pd.DataFrame(data)

# Without group_tasks=True, There would be two separate "Job A" labels
# fig = ff.create_gantt(data, group_tasks=True)

## plot the non-duplicate rows
fig = px.timeline(df.loc[~df['Task'].duplicated()], x_start="Start", x_end="Finish", y="Task")

## plot the duplicate rows using rectangular shapes
for row in df.loc[df['Task'].duplicated()].itertuples():
      y_val = np.where(df.Task.unique()==row[1])[0][0]
      # print(f"found {row[1]} at index {y_val}")
      fig.add_shape(type="rect",
            xref="x", yref="y",
            x0=row[2], x1=row[3], 
            y0=y_val, y1=y_val-0.4,
            line_width=0,
            fillcolor="salmon",
)
fig.show()

import plotly.express as px
import pandas as pd

data = [dict(Task="Job A", Start='2009-01-01', Finish='2009-02-28'),
      dict(Task="Job A", Start='2009-01-01', Finish='2009-02-28'),
      dict(Task="Job B", Start='2009-03-05', Finish='2009-04-15'),
      dict(Task="Job C", Start='2009-02-20', Finish='2009-05-30')]

df = pd.DataFrame(data)

df['JobNum'] = ""
df.loc[0,'JobNum'] = 1
for idx in range(1,df.shape[0]):
    if df.loc[idx-1,'Task'] == df.loc[idx,'Task']:
        df.loc[idx,'JobNum'] = df.loc[idx-1,'JobNum'] + 1
    else:
        df.loc[idx,'JobNum'] = 1

df['hoverName'] = df.apply(lambda x: x['Task'] + "|" + str(x['JobNum']), axis=1)

方法 1:使用 Facet 行

fig = px.timeline(df
                  , x_start="Start"
                  , x_end="Finish"
                  , y="Task"
                  , hover_name= "Task"
                  , color_discrete_sequence=px.colors.qualitative.Prism
                  , opacity=.7
                  , template='plotly_white'
                  , color='Task'
                  , facet_row= 'JobNum'
                  , hover_data = ['Start','Finish']
                 )
fig.show()

Appraoch 1 Output

方法二:调整宽度和偏移量。当有两个以上的并行任务时,需要对此进行概括。

fig = px.timeline(df
                  , x_start="Start"
                  , x_end="Finish"
                  , y="Task"
                  , hover_name= "hoverName"
                  , color_discrete_sequence=px.colors.qualitative.Prism
                  , opacity=.7
                  , template='plotly_white'
                  , color='JobNum'
                  , hover_data = ['Start','Finish']
                 )

for obj in fig.data:
    Task, JobNum = obj.hovertext[0].split("|")
    if (int(JobNum) == 1):
        obj.width = 0.1
        obj.offset = 0.05
    elif (int(JobNum) == 2):
        obj.width = 0.1
        obj.offset = -0.05

fig.show()

Approach 2 output