How to use David Bacci’s Gantt Deneb Template

You may have noticed that if you’re trying to make a Gantt chart in Power BI, there aren’t a ton of really great options, particularly in the “free” category. I picked up David Bacci’s Deneb template because it does milestones, dependencies, and phases, which the Microsoft Gantt is pretty lacking in feature-wise. David Bacci has a whole set of really great open source Deneb templates here.

The idea is, you install the custom Deneb visual, which is a well-known free tool for creating custom visuals, and then add the JSON you want to use for the template. David has made his available; I’ve made some changes to it here – namely I swapped all of the date formats to mm/dd/yyyy (instead of dd/mm/yyyy), adjusted some visual bits that were overlapping for whatever reason, and added an Assignees column. The full code block is at the bottom of this post (it’s very lengthy…).

Getting started

To create a Gantt chart with the template, import the Deneb visual from app source to your report, add your task data fields, and then edit the visual settings via the ellipsis menu:

Select the Vega option, click “create” and paste the JSON code (from the bottom of this post, if you are working with Planner data, or from David’s Github if you want the original) you want to use into the specification box.

Considerations

This template expects all tasks to have start and end dates – the column will display NaN if you have null date values in your data. It also expects the column names to match the names in the template. Supposedly you can map these in Deneb, but the mapping button doesn’t seem to work for me so I’ve been renaming them inside the visual field well. They should be named thusly:

If you are using dependencies, add that too (each row should be comma-separated id values for the tasks it depends on, blanks are ok). Planner doesn’t have a place to hold dependency information, but you could create a calculated column or merge dependency data if you want to.

There’s no word-wrap on the text, so you might want to do comma-separated first names for the assignees if you have multiple or if they have long names. I duplicated my assignee name field in Power Query and split on the first space for this to keep it simple.

Using the template with Planner data

If you don’t have your Planner data in Power BI, do that first. I have a video series on how to get Planner data into Power BI via Power Automate here.

One thing you’ll run into when you go to use this template with Planner data is that Planner data does not have a percentage complete field. It has a status: Not Started, In Progress and Complete. These DO translate back to a percentage complete in the API, with values of 0, 50, and 100. They look like a percentage complete when you pull them into a dataset, but if you use them as numbers like that you’ll likely cause some confusion.

The Bacci Gantt wants a percentage complete (this is the “completion” field), and it rolls the tasks into an overall percentage complete for the grouping they’re in. The way I’d personally recommend dealing with this is to take off the numeric values on the bars – that way the progress bar will reflect an approximation of the status, which uses a half-full circle for in-progress in Planner anyway – it’ll look familiar.

sample planner plan
Here’s the sample Planner plan I’m working with – you can see the in-progress status as the blue half circles.

The percentage on the Gantt bars

To remove the percentages on the bars, we need to find and remove the code from the JSON. If you just do a “find” on this and erase it, you should be good to go. Here’s the code to look for if you’re using David’s code straight from the source.

{
              "name": "completeText",
              "description": "Completion Text",
              "type": "text",
              "encode": {
                "update": {
                  "x": {
                    "signal": "(item.mark.group.width/100)* parent.completion"
                  },
                  "align": {
                    "signal": "'right'"
                  },
                  "dx": {
                    "signal": "-2"
                  },
                  "y": {
                    "signal": "item.mark.group.height-bandwidth('y')/2+1"
                  },
                  "baseline": {
                    "value": "middle"
                  },
                  "text": {
                    "signal": "((item.mark.group.width/100)* parent.completion)>item.mark.group.b && parent.completion>0?parent.completionLabel:''"
                  },
                  "fill": {
                    "value": "white"
                  }
                }
              }
            },

The percentage on the Progress bars

Next we remove the progress percentages from the bars. Note the comma at the top of the code below, we need to remove that because it’s the last item in the array and we don’t want a stray trailing comma.

,
        {
          "type": "text",
          "from": {"data": "y_scale"},
          "encode": {
            "update": {
              "align": {
                "value": "left"
              },
              "dx": {"value": 3},
              "fill": {
                "signal": "'#666666'"
              },
              "y": {
                "signal": "scale('y',datum.task)+bandwidth('y')/2"
              },
              "text": {
                "signal": "datum.completion+'%'"
              },
              "baseline": {
                "value": "middle"
              }
            }
          }
        }

The tooltips

For the tooltips, we just want to remove the progress percentage, which is this little piece:

,'Progress':parent.completionLabel

Groupings / Phases

What I did was use the bucket names in our Planner data as the phases, just renaming the field in the field well to “phase”. You could opt to use your project names here instead if you were displaying multiple projects in a single Gantt.

Milestones

The Deneb template wants a true/false column to identify which tasks are milestones. What I did for this is to set any of my tasks that I wanted to appear as a milestone to have a start/end date that was the same, then added a conditional column to flag the true/false on that:

The result

I doubled the width of the canvas to “fit” more visual content in, since it doesn’t dynamically scale. It would be nice to be able to show by quarter or similar, but that appears to be more complicated than you’d expect – if anyone figures out how to do that please share! 🙂

The code

Here’s the code I used with the modifications above:

{
  "$schema": "https://vega.github.io/schema/vega/v5.json",
  "description": "Dataviz by David Bacci: https://www.linkedin.com/in/davbacci/",
  "autosize": "pad",
  "background": "white",
  "padding": {
    "left": 10,
    "right": 10,
    "top": 10,
    "bottom": 10
  },
  "signals": [
    {"name": "y_step", "value": 33},
    {"name": "x_step", "value": 26},
    {
      "name": "days",
      "update": "data('days')[0]['days']"
    },
    {
      "name": "yPaddingInner",
      "value": 0.45
    },
    {
      "name": "yPaddingOuter",
      "value": 0.2
    },
    {
      "name": "taskColumn",
      "value": 200
    },
    {
      "name": "assigneeColumn",
      "value": 100
    },
    {
      "name": "startColumn",
      "value": 45
    },
    {"name": "endColumn", "value": 45},
    {"name": "daysColumn", "value": 35},
    {
      "name": "progressColumn",
      "value": 55
    },
    {
      "name": "columnPadding",
      "value": 15
    },
    {
      "name": "height",
      "update": "bandspace(domain('y').length, yPaddingInner, yPaddingOuter) * y_step"
    },
    {
      "name": "ganttWidth",
      "update": "days * x_step"
    },
    {
      "name": "width",
      "update": "ganttWidth"
    },
    {
      "name": "length",
      "update": "span(domain('xDays'))"
    },
    {
      "name": "today",
      "update": "timeFormat(datetime(now()),'%m/%d/%y')"
    },
    {
      "name": "todayRule",
      "update": "today"
    }
  ],
  "data": [
    {"name": "dataset", "values":[{}]},
    {
      "name": "input",
      "source": "dataset",
      "transform": [
        {
          "type": "formula",
          "as": "encodedStart",
          "expr": "timeFormat(datum.start,'%m/%d/%y')"
        },
        {
          "type": "formula",
          "as": "updatedEnd",
          "expr": "datetime(toNumber(datum.end)+(1000*60*60*24))"
        },
        {
          "type": "formula",
          "as": "encodedEnd",
          "expr": "timeFormat(datum.updatedEnd,'%m/%d/%y')"
        },
        {
          "type": "formula",
          "as": "days",
          "expr": "round((datum.updatedEnd-datum.start)/1000/60/60/24)"
        },
        {
          "type": "formula",
          "as": "completionLabel",
          "expr": "datum.completion+'%'"
        },
        {
          "type": "window",
          "sort": {
            "field": "start",
            "order": "ascending"
          },
          "ops": ["rank"],
          "as": ["taskSort"],
          "groupby": ["phase"]
        }
      ]
    },
    {
      "name": "phases",
      "source": "input",
      "transform": [
        {
          "type": "aggregate",
          "fields": [
            "start",
            "end",
            "completion",
            "task",
            "completion"
          ],
          "ops": [
            "min",
            "max",
            "sum",
            "count",
            "mean"
          ],
          "as": [
            "start",
            "end",
            "sum",
            "count",
            "completion"
          ],
          "groupby": ["phase"]
        },
        {
          "type": "lookup",
          "from": "input",
          "key": "start",
          "values": ["encodedStart"],
          "fields": ["start"]
        },
        {
          "type": "lookup",
          "from": "input",
          "key": "end",
          "values": ["encodedEnd"],
          "fields": ["end"]
        },
        {
          "type": "formula",
          "as": "task",
          "expr": "datum.phase"
        },
        {
          "type": "formula",
          "as": "taskSort",
          "expr": "0"
        },
        {
          "type": "formula",
          "as": "completion",
          "expr": "round(datum.completion)"
        },
        {
          "type": "formula",
          "as": "days",
          "expr": "round((datum.end-datum.start)/1000/60/60/24)+1"
        },
        {
          "type": "window",
          "sort": {
            "field": "start",
            "order": "ascending"
          },
          "ops": ["rank"],
          "as": ["phaseSort"]
        }
      ]
    },
    {
      "name": "tasks",
      "source": "input",
      "transform": [
        {
          "type": "filter",
          "expr": "datum.milestone != true"
        },
        {
          "type": "lookup",
          "from": "phases",
          "key": "phase",
          "values": ["phaseSort"],
          "fields": ["phase"]
        }
      ]
    },
    {
      "name": "milestones",
      "source": "input",
      "transform": [
        {
          "type": "filter",
          "expr": "datum.milestone == true"
        },
        {
          "type": "lookup",
          "from": "phases",
          "key": "phase",
          "values": ["phaseSort"],
          "fields": ["phase"]
        }
      ]
    },
    {
      "name": "y_scale",
      "source": [
        "tasks",
        "phases",
        "milestones"
      ],
      "transform": [
        {
          "type": "window",
          "sort": {
            "field": [
              "phaseSort",
              "taskSort"
            ],
            "order": [
              "ascending",
              "ascending"
            ]
          },
          "ops": ["row_number"],
          "as": ["finalSort"]
        }
      ]
    },
    {
      "name": "days",
      "source": "input",
      "transform": [
        {
          "type": "aggregate",
          "fields": ["start", "end"],
          "ops": ["min", "max"],
          "as": ["s", "e"]
        },
        {
          "type": "formula",
          "as": "days",
          "expr": "round((datum.e-datum.s)/1000/60/60/24)"
        }
      ]
    },
    {
      "name": "dayScale",
      "transform": [
        {
          "type": "sequence",
          "start": -1,
          "stop": {"signal": "days+8"},
          "as": "sequence"
        },
        {
          "type": "formula",
          "as": "date",
          "expr": "datetime(toNumber(data('days')[0]['s'])+((1000*60*60*24)*datum.sequence))"
        },
        {
          "type": "formula",
          "as": "encodedDate",
          "expr": "timeFormat(datum.date,'%m/%d/%y')"
        }
      ]
    },
    {
      "name": "weekends",
      "source": "dayScale",
      "transform": [
        {
          "type": "filter",
          "expr": "day(datum.date) == 6 || day(datum.date) == 0"
        }
      ]
    },
    {
      "name": "dependencyArrows",
      "source": "input",
      "transform": [
        {
          "type": "filter",
          "expr": "isValid(datum.dependencies) && datum.dependencies!='' "
        }
      ]
    },
    {
      "name": "dependencyLines",
      "source": "y_scale",
      "transform": [
        {
          "type": "filter",
          "expr": "isValid(datum.dependencies) && datum.dependencies!='' "
        },
        {
          "type": "formula",
          "expr": "split(datum.dependencies,',')",
          "as": "dependencies"
        },
        {
          "type": "flatten",
          "fields": ["dependencies"]
        },
        {
          "type": "lookup",
          "from": "y_scale",
          "key": "id",
          "values": [
            "task",
            "finalSort",
            "encodedEnd",
            "start",
            "end"
          ],
          "fields": ["dependencies"],
          "as": [
            "sourceTask",
            "sourceFinalSort",
            "sourceEncodedEnd",
            "sourceStart",
            "sourceEnd"
          ]
        },
        {
          "type": "formula",
          "as": "a",
          "expr": "[scale('xDays',datum.encodedStart),scale('y',datum.task)+bandwidth('y')/2]"
        },
        {
          "type": "formula",
          "as": "b",
          "expr": "[datum.start > datum.sourceEnd?scale('xDays',datum.sourceEncodedEnd) - bandwidth('xDays')/2:scale('xDays',datum.encodedStart) - bandwidth('xDays')/2,scale('y',datum.task)+bandwidth('y')/2]"
        },
        {
          "type": "formula",
          "as": "c",
          "expr": "[scale('xDays',datum.sourceEncodedEnd) - bandwidth('xDays')/2,scale('y',datum.sourceTask)+bandwidth('y')/2]"
        },
        {
          "type": "formula",
          "as": "d",
          "expr": "[datum.start <= datum.sourceEnd?scale('xDays',datum.encodedStart) - bandwidth('xDays')/2:null ,datum.start <= datum.sourceEnd?scale('y',datum.sourceTask)+(bandwidth('y')*1.5):null]"
        },
        {
          "type": "formula",
          "as": "e",
          "expr": "[datum.start <= datum.sourceEnd?scale('xDays',datum.sourceEncodedEnd) - bandwidth('xDays')/2:null ,datum.start <= datum.sourceEnd?scale('y',datum.sourceTask)+(bandwidth('y')*1.5):null]"
        },
        {
          "type": "fold",
          "fields": [
            "a",
            "b",
            "d",
            "e",
            "c"
          ]
        },
        {
          "type": "filter",
          "expr": "datum.value[0] != null"
        }
      ]
    }
  ],
  "layout": {
    "padding": {
      "signal": "columnPadding"
    },
    "bounds": "flush",
    "align": "none"
  },
  "marks": [
    {
      "type": "group",
      "name": "taskColumn",
      "style": "cell",
      "title": {
        "text": "Task",
        "anchor": "start",
        "frame": "group",
        "align": "left",
        "color": "#666666"
      },
      "encode": {
        "update": {
          "width": {
            "signal": "taskColumn"
          },
          "height": {"signal": "height"}
        }
      },
      "marks": [
        {
          "type": "text",
          "style": "col",
          "from": {"data": "y_scale"},
          "encode": {
            "update": {
              "align": {
                "value": "left"
              },
              "fill": {
                "signal": "datum.phase == datum.task?'#666666':'#666666'"
              },
              "dx": {
                "signal": "datum.phase != datum.task?'0':'0'"
              },
              "y": {
                "signal": "scale('y',datum.task)+bandwidth('y')/2"
              },
              "text": {
                "signal": "datum.phase == datum.task?upper(datum.task):datum.task"
              },
              "fontWeight": {
                "signal": "datum.phase == datum.task?'bold':'normal'"
              },
              "baseline": {
                "value": "middle"
              },
              "limit": {
                "signal": "taskColumn"
              }
            }
          }
        },
        {
          "type": "symbol",
          "from": {"data": "y_scale"},
          "encode": {
            "update": {
              "fill": {
                "signal": "datum.phase == datum.task?scale('cStroke', datum.phase):'transparent'"
              },
              "x2": {
                "signal": "datum.phase != datum.task?'0':'-10'"
              },
              "yc": {
                "signal": "(scale('y',datum.task)+bandwidth('y')/2)-1"
              },
              "shape": {
                "value": "square"
              }
            }
          }
        }
      ]
    },
    {
      "type": "group",
      "name": "assigneeColumn",
      "style": "cell",
      "title": {
        "text": "Assignee",
        "anchor": "start",
        "frame": "group",
        "align": "left",
        "color": "#666666"
      },
      "encode": {
        "update": {
          "width": {
            "signal": "assigneeColumn"
          },
          "height": {"signal": "height"}
        }
      },
      "marks": [
        {
          "type": "text",
          "style": "col",
          "from": {"data": "y_scale"},
          "encode": {
            "update": {
              "align": {
                "value": "right"
              },
              "x": {
                "signal": "assigneeColumn"
              },
              "fill": {
                "signal": "datum.phase == datum.task?'#666666':'#666666'"
              },
              "y": {
                "signal": "scale('y',datum.task)+bandwidth('y')/2"
              },
              "text": {
                "signal": "datum.phase == datum.assignee?upper(datum.assignee):datum.assignee"
              },
              "baseline": {
                "value": "middle"
              },
              "fontWeight": {
                "signal": "datum.phase == datum.task?'bold':'normal'"
              }
            }
          }
        }
      ]
    },
    {
      "type": "group",
      "name": "startColumn",
      "style": "cell",
      "title": {
        "text": "Start",
        "anchor": "end",
        "frame": "group",
        "align": "right",
        "color": "#666666"
      },
      "encode": {
        "update": {
          "width": {
            "signal": "startColumn"
          },
          "height": {"signal": "height"}
        }
      },
      "marks": [
        {
          "type": "text",
          "style": "col",
          "from": {"data": "y_scale"},
          "encode": {
            "update": {
              "align": {
                "value": "right"
              },
              "x": {
                "signal": "startColumn"
              },
              "fill": {
                "signal": "datum.phase == datum.task?'#666666':'#666666'"
              },
              "y": {
                "signal": "scale('y',datum.task)+bandwidth('y')/2"
              },
              "text": {
                "signal": "timeFormat(datum.start,' %m/%d/%y')"
              },
              "baseline": {
                "value": "middle"
              },
              "fontWeight": {
                "signal": "datum.phase == datum.task?'bold':'normal'"
              }
            }
          }
        }
      ]
    },
    {
      "type": "group",
      "name": "endColumn",
      "style": "cell",
      "title": {
        "text": "End",
        "anchor": "end",
        "frame": "group",
        "align": "right",
        "color": "#666666"
      },
      "encode": {
        "update": {
          "width": {
            "signal": "endColumn"
          },
          "height": {"signal": "height"}
        }
      },
      "marks": [
        {
          "type": "text",
          "style": "col",
          "from": {"data": "y_scale"},
          "encode": {
            "update": {
              "align": {
                "value": "right"
              },
              "x": {
                "signal": "endColumn"
              },
              "fill": {
                "signal": "datum.phase == datum.task?'#666666':'#666666'"
              },
              "y": {
                "signal": "scale('y',datum.task)+bandwidth('y')/2"
              },
              "text": {
                "signal": "timeFormat(datum.end,' %m/%d/%y')"
              },
              "baseline": {
                "value": "middle"
              },
              "fontWeight": {
                "signal": "datum.phase == datum.task?'bold':'normal'"
              }
            }
          }
        }
      ]
    },
    {
      "type": "group",
      "name": "daysColumn",
      "style": "cell",
      "title": {
        "text": "Days",
        "anchor": "end",
        "frame": "group",
        "align": "right",
        "color": "#666666"
      },
      "encode": {
        "update": {
          "width": {
            "signal": "daysColumn"
          },
          "height": {"signal": "height"}
        }
      },
      "marks": [
        {
          "type": "text",
          "style": "col",
          "from": {"data": "y_scale"},
          "encode": {
            "update": {
              "align": {
                "value": "right"
              },
              "fill": {
                "signal": "datum.phase == datum.task?'#666666':'#666666'"
              },
              "x": {
                "signal": "daysColumn"
              },
              "y": {
                "signal": "scale('y',datum.task)+bandwidth('y')/2"
              },
              "text": {
                "signal": "datum.days+' d'"
              },
              "baseline": {
                "value": "middle"
              },
              "fontWeight": {
                "signal": "datum.phase == datum.task?'bold':'normal'"
              }
            }
          }
        }
      ]
    },
    {
      "type": "group",
      "name": "completionColumn",
      "style": "cell",
      "title": {
        "text": "Progress",
        "anchor": "start",
        "frame": "group",
        "color": "#666666"
      },
      "encode": {
        "update": {
          "width": {
            "signal": "progressColumn"
          },
          "height": {"signal": "height"}
        }
      },
      "marks": [
        {
          "type": "rect",
          "from": {"data": "y_scale"},
          "encode": {
            "update": {
              "x": {"signal": "0"},
              "width": {
                "signal": "item.mark.group.width"
              },
              "stroke": {
                "signal": "'#a0d786'"
              },
              "yc": {
                "signal": "(scale('y',datum.task)+bandwidth('y')/2)"
              },
              "strokeWidth": {
                "value": 1
              },
              "height": {
                "signal": "bandwidth('y')"
              }
            }
          }
        },
        {
          "type": "rect",
          "from": {"data": "y_scale"},
          "encode": {
            "update": {
              "x": {"signal": "0"},
              "width": {
                "signal": "(item.mark.group.width/100)*datum.completion"
              },
              "fill": {
                "signal": "'#c6ecb5'"
              },
              "yc": {
                "signal": "(scale('y',datum.task)+bandwidth('y')/2)"
              },
              "strokeWidth": {
                "value": 1
              },
              "height": {
                "signal": "bandwidth('y')"
              }
            }
          }
        }
      ]
    },
    {
      "type": "group",
      "name": "gantt",
      "encode": {
        "update": {
          "width": {
            "signal": "ganttWidth"
          },
          "height": {"signal": "height"}
        }
      },
      "marks": [
        {
          "name": "labelSizes",
          "clip": true,
          "type": "text",
          "from": {"data": "tasks"},
          "encode": {
            "enter": {
              "fill": {
                "value": "transparent"
              },
              "text": {
                "signal": "datum.completionLabel"
              },
              "x": {"value": 0},
              "y": {"value": 10}
            }
          }
        },
        {
          "type": "rect",
          "description": "Weekend shading",
          "name": "weekendShading",
          "from": {"data": "weekends"},
          "encode": {
            "update": {
              "x": {
                "field": "encodedDate",
                "scale": "xDays"
              },
              "width": {
                "signal": "bandwidth('xDays')"
              },
              "y": {"value": -15},
              "height": {
                "signal": "height+15"
              },
              "strokeWidth": {
                "signal": "1"
              },
              "stroke": {
                "value": "#f1f1f1"
              },
              "fill": {
                "value": "#f1f1f1"
              }
            }
          }
        },
        {
          "type": "group",
          "from": {
            "facet": {
              "name": "dependencyLinesFacet",
              "data": "dependencyLines",
              "groupby": [
                "id",
                "dependencies"
              ]
            }
          },
          "marks": [
            {
              "type": "line",
              "from": {
                "data": "dependencyLinesFacet"
              },
              "encode": {
                "enter": {
                  "x": {
                    "signal": "datum.value[0]"
                  },
                  "y": {
                    "signal": "datum.value[1]"
                  },
                  "stroke": {
                    "value": "#888888"
                  },
                  "strokeWidth": {
                    "value": 1.5
                  },
                  "interpolate": {
                    "value": "linear"
                  },
                  "strokeJoin": {
                    "value": "bevel"
                  },
                  "strokeCap": {
                    "value": "round"
                  },
                  "strokeDash": {
                    "signal": "0"
                  },
                  "defined": {
                    "value": true
                  }
                }
              }
            }
          ]
        },
        {
          "name": "todayRule",
          "description": "Today rule",
          "type": "rule",
          "data": [{}],
          "encode": {
            "update": {
              "x": {
                "signal": "scale('xDays',todayRule) + (bandwidth('xDays')/2)"
              },
              "y": {"value": 0},
              "y2": {
                "signal": "height"
              },
              "height": {
                "signal": "height"
              },
              "strokeWidth": {
                "signal": "indata('dayScale', 'encodedDate', todayRule)? 1:0"
              },
              "stroke": {
                "value": "#377eb9"
              },
              "strokeDash": {
                "value": [2, 2]
              },
              "opacity": {"value": 0.8}
            }
          }
        },
        {
          "name": "todayText",
          "description": "Today text",
          "type": "text",
          "data": [{}],
          "encode": {
            "update": {
              "x": {
                "signal": "scale('xDays',todayRule) + (bandwidth('xDays')/2)"
              },
              "y": {"value": 0},
              "fill": {
                "value": "#377eb9"
              },
              "text": {
                "signal": "indata('dayScale', 'encodedDate', todayRule)? 'Today':''"
              },
              "angle": {"signal": "90"},
              "baseline": {
                "value": "bottom"
              },
              "dx": {"value": 10},
              "dy": {"value": -4},
              "opacity": {"value": 1},
              "font": {"value": ""}
            }
          }
        },
        {
          "name": "todayHighlight",
          "description": "Today highlight",
          "type": "rect",
          "data": [{}],
          "encode": {
            "update": {
              "cornerRadius": {
                "value": 0
              },
              "x": {
                "signal": "scale('xDays',todayRule) "
              },
              "width": {
                "signal": "bandwidth('xDays')"
              },
              "y": {"value": -15},
              "height": {
                "signal": "15"
              },
              "fill": {
                "value": "#a5c8e4"
              },
              "opacity": {
                "signal": "indata('dayScale', 'encodedDate', todayRule)? 1:0"
              }
            }
          }
        },
        {
          "name": "taskBars",
          "description": "The task bars (serve as an outline for percent complete)",
          "type": "group",
          "clip": false,
          "from": {"data": "tasks"},
          "encode": {
            "update": {
              "x": {
                "scale": "xDays",
                "field": "encodedStart"
              },
              "x2": {
                "scale": "xDays",
                "field": "encodedEnd"
              },
              "y": {
                "scale": "y",
                "field": "task"
              },
              "height": {
                "signal": "bandwidth('y')"
              },
              "tooltip": {
                "signal": "{'Phase':datum.phase ,'Task':datum.task , 'Start':timeFormat(datum.start,'%a, %d %B %Y' ),'End':timeFormat(datum.end,'%a, %d %B %Y' ), 'Days':datum.days,'Progress':datum.completionLabel }"
              },
              "fill": {
                "field": "phase",
                "scale": "cFill"
              },
              "stroke": {
                "field": "phase",
                "scale": "cStroke"
              },
              "strokeWidth": {
                "value": 1
              },
              "cornerRadius": {
                "value": 5
              },
              "zindex": {"value": 2}
            }
          },
          "transform": [
            {
              "type": "lookup",
              "from": "labelSizes",
              "key": "text",
              "fields": [
                "datum.completionLabel"
              ],
              "values": [
                "bounds.x1",
                "bounds.x2"
              ],
              "as": ["a", "b"]
            }
          ],
          "marks": [
            {
              "name": "fills",
              "description": "Percent complete for each task",
              "type": "rect",
              "clip": true,
              "encode": {
                "update": {
                  "x": {
                    "signal": "item.mark.group.x1"
                  },
                  "y": {
                    "signal": "item.mark.group.y1"
                  },
                  "height": {
                    "signal": "item.mark.group.height"
                  },
                  "width": {
                    "signal": "(item.mark.group.width/100)* item.mark.group.datum.completion"
                  },
                  "fill": {
                    "signal": "item.mark.group.datum.phase",
                    "scale": "cStroke"
                  },
                  "stroke": {
                    "signal": "item.mark.group.datum.phase",
                    "scale": "cStroke"
                  },
                  "strokeWidth": {
                    "value": 2
                  },
                  "cornerRadiusBottomLeft": {
                    "value": 5
                  },
                  "cornerRadiusTopLeft": {
                    "value": 5
                  },
                  "tooltip": {
                    "signal": "{'Phase':parent.phase ,'Task':parent.task , 'Start':timeFormat(parent.start,'%a, %d %B %Y' ),'End':timeFormat(parent.end,'%a, %d %B %Y' ), 'Days':parent.days }"
                  }
                }
              }
            },
            {
              "name": "taskName",
              "description": "Task name",
              "type": "text",
              "encode": {
                "update": {
                  "x": {
                    "signal": "item.mark.group.width"
                  },
                  "align": {
                    "signal": "'left'"
                  },
                  "dx": {"signal": "5"},
                  "y": {
                    "signal": "item.mark.group.height-bandwidth('y')/2+1"
                  },
                  "baseline": {
                    "value": "middle"
                  },
                  "text": {
                    "signal": "parent.task + ' ('+ parent.days+' d'+')'"
                  },
                  "fill": {
                    "value": "#666666"
                  }
                }
              }
            }
          ]
        },
        {
          "name": "phaseBars",
          "description": "The phase bars (serve as an outline for percent complete)",
          "type": "group",
          "clip": false,
          "from": {"data": "phases"},
          "encode": {
            "update": {
              "x": {
                "scale": "xDays",
                "field": "encodedStart"
              },
              "x2": {
                "scale": "xDays",
                "field": "encodedEnd"
              },
              "y": {
                "signal": "scale('y', datum.phase)+bandwidth('y')/4"
              },
              "height": {
                "signal": "bandwidth('y')/2"
              },
              "tooltip": {
                "signal": "{'Phase':datum.phase ,'Task':datum.task , 'Start':timeFormat(datum.start,'%a, %d %B %Y' ),'End':timeFormat(datum.end,'%a, %d %B %Y' ), 'Days':datum.days,'Progress':datum.completion+'%' }"
              },
              "fill": {
                "field": "phase",
                "scale": "cFill"
              },
              "stroke": {
                "field": "phase",
                "scale": "cStroke"
              },
              "strokeWidth": {
                "value": 1
              },
              "zindex": {"value": 2}
            }
          },
          "marks": [
            {
              "name": "fills",
              "description": "Percent complete for each phase",
              "type": "rect",
              "clip": true,
              "encode": {
                "update": {
                  "x": {
                    "signal": "item.mark.group.x1"
                  },
                  "y": {
                    "signal": "item.mark.group.y1"
                  },
                  "height": {
                    "signal": "item.mark.group.height"
                  },
                  "width": {
                    "signal": "(item.mark.group.width/100)* item.mark.group.datum.completion"
                  },
                  "fill": {
                    "signal": "item.mark.group.datum.phase",
                    "scale": "cStroke"
                  },
                  "stroke": {
                    "signal": "item.mark.group.datum.phase",
                    "scale": "cStroke"
                  },
                  "strokeWidth": {
                    "value": 2
                  },
                  "tooltip": {
                    "signal": "{'Phase':parent.phase ,'Task':parent.task , 'Start':timeFormat(parent.start,'%a, %d %B %Y' ),'End':timeFormat(parent.end,'%a, %d %B %Y' ), 'Days':parent.days,'Progress':parent.completion+'%' }"
                  }
                }
              }
            },
            {
              "type": "path",
              "name": "phaseStart",
              "description": "Phase start",
              "encode": {
                "update": {
                  "x": {
                    "signal": "item.mark.group.x1"
                  },
                  "y": {
                    "signal": "item.mark.group.height"
                  },
                  "strokeWidth": {
                    "value": 0
                  },
                  "scaleX": {
                    "signal": "item.mark.group.height/60"
                  },
                  "scaleY": {
                    "signal": "item.mark.group.height/60"
                  },
                  "path": {
                    "value": "M 0,0 C 0,20 0,40 0,60 20,40 40,20 60,0 40,0 20,0 0,0 Z"
                  },
                  "angle": {"value": 0},
                  "fill": {
                    "signal": "item.mark.group.datum.phase",
                    "scale": "cStroke"
                  },
                  "stroke": {
                    "signal": "item.mark.group.datum.phase",
                    "scale": "cStroke"
                  },
                  "tooltip": {
                    "signal": "item.mark.group.datum"
                  }
                }
              }
            },
            {
              "type": "path",
              "name": "phaseEnd",
              "description": "Phase end ",
              "encode": {
                "update": {
                  "x": {
                    "signal": "item.mark.group.width-(item.mark.group.height)+1"
                  },
                  "y": {
                    "signal": "item.mark.group.height"
                  },
                  "strokeWidth": {
                    "value": 0
                  },
                  "scaleX": {
                    "signal": "item.mark.group.height/60"
                  },
                  "scaleY": {
                    "signal": "item.mark.group.height/60"
                  },
                  "path": {
                    "value": "m 60,0 c 0,20 0,40 0,60 C 40,40 20,20 0,0 20,0 40,0 60,0 Z"
                  },
                  "angle": {"value": 0},
                  "fill": {
                    "signal": "item.mark.group.datum.phase",
                    "scale": "cStroke"
                  },
                  "stroke": {
                    "signal": "item.mark.group.datum.phase",
                    "scale": "cStroke"
                  },
                  "tooltip": {
                    "signal": "item.mark.group.datum"
                  }
                }
              }
            },
            {
              "name": "taskName",
              "description": "Task name",
              "type": "text",
              "encode": {
                "update": {
                  "x": {
                    "signal": "item.mark.group.width"
                  },
                  "align": {
                    "signal": "'left'"
                  },
                  "dx": {"signal": "5"},
                  "y": {
                    "signal": "item.mark.group.height-bandwidth('y')/4+1"
                  },
                  "baseline": {
                    "value": "middle"
                  },
                  "text": {
                    "signal": "item.mark.group.datum.task + ' ('+ item.mark.group.datum.days+' d'+')'"
                  },
                  "fill": {
                    "value": "#666666"
                  }
                }
              }
            }
          ]
        },
        {
          "name": "milestoneSymbols",
          "description": "Milestones",
          "type": "symbol",
          "from": {
            "data": "milestones"
          },
          "encode": {
            "update": {
              "x": {
                "signal": "scale('xDays',datum.encodedStart)+bandwidth('xDays')/2"
              },
              "y": {
                "signal": "scale('y',datum.task)+bandwidth('y')/2"
              },
              "size": {
                "signal": "pow( min(y_step,x_step),1.85)"
              },
              "shape": {
                "value": "diamond"
              },
              "fill": {
                "signal": "datum.completion > 0? scale('cStroke', datum.phase):scale('cFill', datum.phase)"
              },
              "stroke": {
                "field": "phase",
                "scale": "cStroke"
              },
              "strokeWidth": {
                "value": 1
              },
              "tooltip": {
                "signal": "{'Phase':datum.phase ,'Task':datum.task , 'Start':timeFormat(datum.start,'%a, %d %B %Y' ),'End':timeFormat(datum.end,'%a, %d %B %Y' ), 'Days':datum.days,'Progress':datum.completionLabel }"
              }
            }
          }
        },
        {
          "name": "milestoneName",
          "description": "Milestone name",
          "from": {
            "data": "milestoneSymbols"
          },
          "type": "text",
          "encode": {
            "update": {
              "x": {
                "signal": "datum.x"
              },
              "align": {
                "signal": "'left'"
              },
              "dx": {"signal": "15"},
              "y": {
                "signal": "datum.y"
              },
              "baseline": {
                "value": "middle"
              },
              "text": {
                "signal": "datum.datum.task"
              },
              "fill": {
                "value": "#666666"
              }
            }
          }
        },
        {
          "name": "dependencyArrowSymbol",
          "description": "Dependency arrows",
          "type": "symbol",
          "from": {
            "data": "dependencyArrows"
          },
          "encode": {
            "update": {
              "shape": {
                "value": "triangle-right"
              },
              "x": {
                "signal": "scale('xDays',datum.encodedStart)-3"
              },
              "y": {
                "signal": "scale('y',datum.task)+bandwidth('y')/2"
              },
              "fill": {
                "value": "#6a6a6a"
              },
              "size": {
                "signal": "pow( min(y_step,x_step),1.3)"
              }
            }
          }
        }
      ],
      "axes": [
        {
          "description": "Day axis",
          "ticks": true,
          "labelPadding": -12,
          "scale": "xDays",
          "tickSize": 15,
          "orient": "top",
          "bandPosition": 0,
          "grid": false,
          "zindex": 1,
          "encode": {
            "labels": {
              "update": {
                "text": [
                  {
                    "signal": "timeFormat(timeParse(datum.label,'%m/%d/%y'),'%d')"
                  }
                ]
              }
            }
          }
        },
        {
          "description": "Month axis",
          "scale": "xDays",
          "domain": false,
          "orient": "top",
          "offset": 15,
          "tickSize": 25,
          "labelFontSize": 12,
          "bandPosition": 0,
          "grid": false,
          "zindex": 0,
          "encode": {
            "ticks": {
              "update": {
                "strokeOpacity": [
                  {
                    "test": "timeFormat(timeParse(datum.label,'%m/%d/%y'),'%d')   == '01'",
                    "value": 1
                  },
                  {"value": 0}
                ]
              }
            },
            "labels": {
              "update": {
                "text": [
                  {
                    "test": "timeFormat(timeParse(datum.label,'%m/%d/%y'),'%d')   == '15'",
                    "signal": "timeFormat(timeParse(datum.label,'%m/%d/%y'),'%B')"
                  },
                  {"value": ""}
                ]
              }
            }
          }
        },
        {
          "scale": "y",
          "orient": "left",
          "encode": {
            "ticks": {
              "update": {
                "x2": {
                  "signal": "-taskColumn-assigneeColumn-startColumn-endColumn-daysColumn-progressColumn-(columnPadding*5)-15"
                }
              }
            }
          },
          "tickColor": "#f1f1f1",
          "bandPosition": 1.35,
          "labels": false,
          "title": "",
          "ticks": true,
          "zindex": 0
        }
      ]
    }
  ],
  "scales": [
    {
      "name": "xDays",
      "type": "band",
      "domain": {
        "data": "dayScale",
        "fields": ["encodedDate"]
      },
      "range": {
        "signal": "[0,ganttWidth]"
      }
    },
    {
      "name": "y",
      "type": "band",
      "domain": {
        "fields": [
          {
            "data": "y_scale",
            "field": "task"
          }
        ],
        "sort": {
          "op": "min",
          "field": "finalSort",
          "order": "ascending"
        }
      },
      "range": {
        "step": {"signal": "y_step"}
      },
      "paddingInner": {
        "signal": "yPaddingInner"
      },
      "paddingOuter": {
        "signal": "yPaddingOuter"
      }
    },
    {
      "name": "cStroke",
      "type": "ordinal",
      "range": [
        "hsl(207, 54%, 47%)",
        "hsl(118, 41%, 49%)",
        "hsl(292, 35%, 47%)",
        "hsl(30, 100%, 50%)",
        "hsl(359, 80%, 50%)"
      ],
      "domain": {
        "data": "input",
        "field": "phase",
        "sort": {
          "op": "min",
          "field": "start",
          "order": "ascending"
        }
      }
    },
    {
      "name": "cFill",
      "type": "ordinal",
      "range": [
        "hsl(207, 54%, 77%)",
        "hsl(118, 41%, 79%)",
        "hsl(292, 35%, 77%)",
        "hsl(30, 100%, 80%)",
        "hsl(359, 80%, 80%)"
      ],
      "domain": {
        "data": "input",
        "field": "phase",
        "sort": {
          "op": "min",
          "field": "start",
          "order": "ascending"
        }
      }
    }
  ],
  "config": {
    "view": {"stroke": "transparent"},
    "style": {
      "col": {"fontSize": 11},
      "cell": {
        "strokeWidth": {"signal": "0"}
      }
    },
    "font": "Arial",
    "text": {
      "font": "Arial",
      "fontSize": 10,
      "fill": "#666666"
    },
    "axis": {
      "labelColor": "#666666",
      "labelFontSize": 10,
      "titleFont": "arial",
      "titleColor": "#252423",
      "titleFontSize": 16,
      "titleFontWeight": "normal"
    },
    "axisY": {"labelPadding": 10}
  }
}

Leave a Comment