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

如何在Deneb甘特图中实现按经办人分组的层级Y轴?

问题描述

我正在制作Deneb甘特图,想要实现按经办人(Assignee)分组展示项目的效果,类似Power BI表格里把经办人和项目堆叠在行上,支持点击展开/折叠查看经办人所有项目的形式。目前我把经办人放在Y轴,任务用范围条形图展示,但如果不在Y轴列出项目(比如经办人1同时负责Project1和Project2的任务),图表会变得十分混乱。我尝试用分面(facet)但没效果,想知道甘特图是否支持分面,或者该怎么实现这个需求。

示例数据

ProjectTaskAssigneeStartDateEndDate
Project1Task1Assignee101/05/202330/06/2023
Project1Task1Assignee201/04/202331/05/2023
Project1Task2Assignee301/07/202314/07/2023
Project2Task1Assignee101/07/202331/07/2023

示例代码

{
  "title": {
    "text": "Title",
    "fontSize": 16,
    "color": "#34282C"
  },
  "data": {"name": "dataset"},
  "layer": [
    {
      "transform": [
        {
          "calculate": "now()",
          "as": "Today"
        }
      ],
      "mark": {
        "type": "rule",
        "color": "Sienna",
        "strokeDash": [0, 0],
        "size": 1,
        "tooltip": true
      },
      "encoding": {
        "x": {
          "field": "Today",
          "type": "temporal"
        }
      }
    },
    {
      "layer": [
        {
          "params": [
            {
              "name": "grid",
              "select": "interval",
              "bind": "scales"
            },
            {
              "name": "highlight",
              "select": {
                "type": "point",
                "on": "mouseover"
              }
            },
            {
              "name": "select",
              "select": "point"
            }
          ],
          "mark": {
            "type": "bar",
            "opacity": 1,
            "cornerRadius": 6,
            "height": 25,
            "tooltip": true,
            "color": "#FFFFFF"
          }
        },
        {
          "mark": {
            "type": "bar",
            "opacity": 0.6,
            "cornerRadius": 6,
            "height": 20,
            "tooltip": true,
            "stroke": "black"
          },
          "encoding": {
            "color": {
              "field": "Assignee",
              "legend": null
            }
          }
        },
        {
          "mark": {
            "type": "text",
            "align": "left",
            "dx": 5,
            "fontSize": 12
          },
          "encoding": {
            "text": {
              "field": "Task",
              "type": "nominal"
            }
          }
        }
      ],
      "encoding": {
        "y": {
          "field": "Assignee",
          "type": "ordinal",
          "axis": {
            "title": null,
            "grid": true,
            "gridWidth": 2,
            "tickBand": "extent",
            "labelFontSize": 15,
            "labelColor": "#34282C",
            "zindex": 1
          }
        },
        "x": {
          "field": "StartDate",
          "type": "temporal",
          "axis": {
            "title": null,
            "format": "%m/%y",
            "labelExpr": "[timeFormat(datum.value, '%m/%y'), timeFormat(datum.value, '%m') == '01' ? timeFormat(datum.value, '%Y') : '']",
            "orient": "top",
            "grid": true,
            "gridWidth": 2,
            "gridColor": "#ddd",
            "gridDash": {
              "condition": {
                "test": {
                  "field": "value",
                  "timeUnit": "month",
                  "equal": 1
                },
                "value": []
              },
              "value": [2, 2]
            },
            "labelOffset": 15,
            "labelAlign": "left",
            "labelPadding": 0
          }
        },
        "x2": {"field": "EndDate"},
        "yOffset": {
          "field": "Assignee"
        },
        "fillOpacity": {
          "condition": {
            "param": "select",
            "value": 1
          },
          "value": 0.3
        },
        "strokeWidth": {
          "condition": [
            {
              "param": "select",
              "empty": false,
              "value": 2
            },
            {
              "param": "highlight",
              "empty": false,
              "value": 1
            }
          ],
          "value": 0
        }
      }
    }
  ]
}

示例图表

示例甘特图


解决方案

一、Deneb甘特图是否支持分面?

Deneb基于Vega-Lite,本身支持分面(facet),但直接用facet是将图表拆分为独立子图,无法实现你需要的“同一轴上层级堆叠+展开/折叠”效果,因此不适用。

二、实现经办人+项目层级堆叠与展开/折叠的方法

通过调整Y轴层级编码、添加交互参数、优化任务条显示三个步骤实现需求:

1. 构建Y轴层级结构

将Assignee和Project组合为复合字段,让项目作为经办人的子项展示,避免同一经办人下的任务重叠:

"y": {
  "field": ["Assignee", "Project"],
  "type": "ordinal",
  "axis": {
    "title": null,
    "grid": true,
    "gridWidth": 2,
    "tickBand": "extent",
    "labelFontSize": 15,
    "labelColor": "#34282C",
    "zindex": 1
  }
}

2. 添加展开/折叠交互

通过定义参数控制项目的显示/隐藏,点击经办人即可切换其下项目的展开状态:

"params": [
  {
    "name": "expandedAssignees",
    "value": [],
    "select": {"type": "point", "fields": ["Assignee"]},
    "on": "click",
    "update": "datum.Assignee && indexof(expandedAssignees, datum.Assignee) > -1 ? remove(expandedAssignees, datum.Assignee) : concat(expandedAssignees, [datum.Assignee])"
  }
],
"transform": [
  {
    "filter": "indexof(expandedAssignees, datum.Assignee) > -1"
  }
]

3. 优化任务条显示

调整任务条高度、添加项目文本标签,让层级关系更清晰:

"layer": [
  // 外层白色背景条
  {
    "mark": {
      "type": "bar",
      "opacity": 1,
      "cornerRadius": 6,
      "height": 20,
      "tooltip": true,
      "color": "#FFFFFF"
    }
  },
  // 内层任务条
  {
    "mark": {
      "type": "bar",
      "opacity": 0.6,
      "cornerRadius": 6,
      "height": 15,
      "tooltip": true,
      "stroke": "black"
    },
    "encoding": {
      "color": {
        "field": "Assignee",
        "legend": null
      }
    }
  },
  // 任务文本
  {
    "mark": {
      "type": "text",
      "align": "left",
      "dx": 5,
      "fontSize": 12
    },
    "encoding": {
      "text": {"field": "Task", "type": "nominal"}
    }
  },
  // 项目文本(右侧标注)
  {
    "mark": {
      "type": "text",
      "align": "right",
      "dx": -5,
      "fontSize": 12,
      "fontWeight": "bold"
    },
    "encoding": {
      "text": {"field": "Project", "type": "nominal"}
    }
  }
]

三、完整调整后的代码

{
  "title": {
    "text": "按经办人-项目分组的甘特图",
    "fontSize": 16,
    "color": "#34282C"
  },
  "data": {"name": "dataset"},
  "params": [
    {
      "name": "expandedAssignees",
      "value": [],
      "select": {"type": "point", "fields": ["Assignee"]},
      "on": "click",
      "update": "datum.Assignee && indexof(expandedAssignees, datum.Assignee) > -1 ? remove(expandedAssignees, datum.Assignee) : concat(expandedAssignees, [datum.Assignee])"
    },
    {
      "name": "grid",
      "select": "interval",
      "bind": "scales"
    },
    {
      "name": "highlight",
      "select": {"type": "point", "on": "mouseover"}
    },
    {
      "name": "select",
      "select": "point"
    }
  ],
  "transform": [
    {
      "filter": "indexof(expandedAssignees, datum.Assignee) > -1"
    }
  ],
  "layer": [
    {
      "transform": [
        {
          "calculate": "now()",
          "as": "Today"
        }
      ],
      "mark": {
        "type": "rule",
        "color": "Sienna",
        "strokeDash": [0, 0],
        "size": 1,
        "tooltip": true
      },
      "encoding": {
        "x": {
          "field": "Today",
          "type": "temporal"
        }
      }
    },
    {
      "layer": [
        {
          "mark": {
            "type": "bar",
            "opacity": 1,
            "cornerRadius": 6,
            "height": 20,
            "tooltip": true,
            "color": "#FFFFFF"
          }
        },
        {
          "mark": {
            "type": "bar",
            "opacity": 0.6,
            "cornerRadius": 6,
            "height": 15,
            "tooltip": true,
            "stroke": "black"
          },
          "encoding": {
            "color": {
              "field": "Assignee",
              "legend": null
            }
          }
        },
        {
          "mark": {
            "type": "text",
            "align": "left",
            "dx": 5,
            "fontSize": 12
          },
          "encoding": {
            "text": {
              "field": "Task",
              "type": "nominal"
            }
          }
        },
        {
          "mark": {
            "type": "text",
            "align": "right",
            "dx": -5,
            "fontSize": 12,
            "fontWeight": "bold"
          },
          "encoding": {
            "text": {
              "field": "Project",
              "type": "nominal"
            }
          }
        }
      ],
      "encoding": {
        "y": {
          "field": ["Assignee", "Project"],
          "type": "ordinal",
          "axis": {
            "title": null,
            "grid": true,
            "gridWidth": 2,
            "tickBand": "extent",
            "labelFontSize": 15,
            "labelColor": "#34282C",
            "zindex": 1
          }
        },
        "x": {
          "field": "StartDate",
          "type": "temporal",
          "axis": {
            "title": null,
            "format": "%m/%y",
            "labelExpr": "[timeFormat(datum.value, '%m/%y'), timeFormat(datum.value, '%m') == '01' ? timeFormat(datum.value, '%Y') : '']",
            "orient": "top",
            "grid": true,
            "gridWidth": 2,
            "gridColor": "#ddd",
            "gridDash": {
              "condition": {
                "test": {
                  "field": "value",
                  "timeUnit": "month",
                  "equal": 1
                },
                "value": []
              },
              "value": [2, 2]
            },
            "labelOffset": 15,
            "labelAlign": "left",
            "labelPadding": 0
          }
        },
        "x2": {"field": "EndDate"},
        "fillOpacity": {
          "condition": {
            "param": "select",
            "value": 1
          },
          "value": 0.3
        },
        "strokeWidth": {
          "condition": [
            {
              "param": "select",
              "empty": false,
              "value": 2
            },
            {
              "param": "highlight",
              "empty": false,
              "value": 1
            }
          ],
          "value": 0
        }
      }
    }
  ]
}

四、关键说明

  • 复合Y轴字段["Assignee", "Project"]自动构建层级结构,经办人作为父项、项目作为子项;
  • expandedAssignees参数通过点击事件切换经办人下项目的显示状态,实现展开/折叠;
  • 双层任务条+左右文本标签,让任务归属的层级关系更直观。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.23 12:59:57