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:

screenshot of deneb visual and ellipsis context menu with edit option

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:

screenshot of values well

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 screenshot
Here’s the sample Planner plan I’m working with – you can see the in-progress status as the blue half circles.

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:

screenshot of conditional column with task start equaling task due and output being true

Assignees

This template doesn’t have assignees in it. We can add a column in ourselves though! To do that, duplicate what’s been done for one of the other columns, like the task start date. There’s three places you need to add to – the beginning, where we configure the the width with the rest of the colums, the “marks” which defines what is shown in the visual, then make sure to add it to the section where it defines the width of all the columns together, or it’ll overlap the Gantt.

My technique was to just copy/paste references and update names. Here’s the code below if you’d rather paste it in – this has been updated for the 2.0 Gantt from his GitHub:

{
  "$schema": "https://vega.github.io/schema/vega/v5.json",
  "description": "Dataviz by David Bacci: https://www.linkedin.com/in/davbacci/",
  "autosize": "pad",
  "padding": {
    "left": 5,
    "right": 0,
    "top": 5,
    "bottom": 0
  },
  "signals": [
    {
      "name": "height",
      "update": "pbiContainerHeight-65"
    },
    {
      "name": "width",
      "update": "pbiContainerWidth"
    },
    {
      "name": "showTooltips",
      "value": true
    },
    {
      "name": "showButtons",
      "value": true
    },
    {
      "name": "showDomainSpanLabel",
      "value": false
    },
    {
      "name": "startGrain",
      "value": "Months",
      "description": "Days, Months, Years or All"
    },
    {
      "name": "textColour",
      "value": "#666666"
    },
    {
      "name": "coloursDark",
      "value": [
        "#377eb9",
        "#4db04a",
        "#974ea2",
        "#ff8000",
        "#e61a1d"
      ]
    },
    {
      "name": "coloursLight",
      "value": [
        "#a5c8e4",
        "#b5dfb3",
        "#d3b0d9",
        "#ffcc99",
        "#f5a3a5"
      ]
    },
    {
      "name": "yRowHeight",
      "value": 33,
      "description": "Height in pixels"
    },
    {
      "name": "yRowPadding",
      "value": 0.22,
      "description": "Row padding as % of yRowHeight (each side)"
    },
    {
      "name": "yPaddingInner",
      "update": "yRowPadding * yRowHeight"
    },
    {
      "name": "taskColumnWidth",
      "value": 155
    },
    {
      "name": "startColumnWidth",
      "value": 45
    },
    {
      "name": "endColumnWidth",
      "value": 45
    },
    {
      "name": "daysColumnWidth",
      "value": 35
    },
    {
      "name": "assigneeColumnWidth",
      "value": 90
    },
    {
      "name": "progressColumnWidth",
      "value": 55
    },
    {
      "name": "columnPadding",
      "value": 15
    },
    {
      "name": "oneDay",
      "update": "1000*60*60*24"
    },
    {
      "name": "dayBandwidth",
      "update": "scale('x', timeOffset('day', datetime(2000,1,1),1)) - scale('x', datetime(2000,1,1))"
    },
    {
      "name": "dayBandwidthRound",
      "update": "(round(dayBandwidth *100)/100)"
    },
    {
      "name": "minDayBandwidth",
      "value": 20
    },
    {
      "name": "minMonthBandwidth",
      "value": 3
    },
    {
      "name": "minYearBandwidth",
      "value": 0.95
    },
    {
      "name": "milestoneSymbolSize",
      "value": 400
    },
    {
      "name": "arrowSymbolSize",
      "value": 70
    },
    {
      "name": "phaseSymbolHeight",
      "update": "bandwidth('y')-yPaddingInner-5"
    },
    {
      "name": "phaseSymbolWidth",
      "value": 10
    },
    {
      "name": "columnsWidth",
      "update": "taskColumnWidth+startColumnWidth+endColumnWidth+daysColumnWidth+assigneeColumnWidth+progressColumnWidth+(columnPadding*5)"
    },
    {
      "name": "ganttWidth",
      "update": "width-columnsWidth-minDayBandwidth"
    },
    {
      "name": "dayExt",
      "update": "[data('xExt')[0]['s']-oneDay,data('xExt')[0]['s']+ ((ganttWidth-minDayBandwidth)/minDayBandwidth)*oneDay]"
    },
    {
      "name": "monthExt",
      "update": "[data('xExt')[0]['s']-oneDay ,data('xExt')[0]['s'] + ganttWidth/2*oneDay]"
    },
    {
      "name": "yearExt",
      "update": "[data('xExt')[0]['s']-oneDay,data('xExt')[0]['s'] + ganttWidth/0.35*oneDay]"
    },
    {
      "name": "allExt",
      "update": "[data('xExt')[0]['s']-oneDay,data('xExt')[0]['e']+oneDay*9]"
    },
    {
      "name": "xExt",
      "update": "startGrain=='All'?dayExt:startGrain=='Years'?yearExt:startGrain=='Months'?monthExt:dayExt"
    },
    {
      "name": "today",
      "update": "utc(year(now()),month(now()),date(now()))"
    },
    {
      "name": "todayRule",
      "update": "timeFormat(today,'%d/%m/%y')"
    },
    {
      "name": "zoom",
      "value": 1,
      "on": [
        {
          "events": "wheel!",
          "force": true,
          "update": "x()>columnsWidth?pow(1.001, (event.deltaY) * pow(16, event.deltaMode)):1"
        }
      ]
    },
    {
      "name": "xDomMinSpan",
      "update": "span(dayExt)"
    },
    {
      "name": "xDomMaxSpan",
      "update": "round((ganttWidth/0.13)*oneDay)"
    },
    {
      "name": "xDom",
      "update": "xExt",
      "on": [
        {
          "events": {
            "signal": "xDomPre"
          },
          "update": "span(xDomPre)<xDomMinSpan?[anchor + (xDom[0] - anchor) * (zoom*(xDomMinSpan/span(xDomPre))), anchor + (xDom[1] - anchor) * (zoom*(xDomMinSpan/span(xDomPre)))]:span(xDomPre)>xDomMaxSpan?[anchor + (xDom[0] - anchor) * (zoom*(xDomMaxSpan/span(xDomPre))), anchor + (xDom[1] - anchor) * (zoom*(xDomMaxSpan/span(xDomPre)))] :xDomPre"
        },
        {
          "events": {"signal": "delta"},
          "update": "[xCur[0] + span(xCur) * delta[0] / width, xCur[1] + span(xCur) * delta[0] / width]"
        },
        {
          "events": "dblclick",
          "update": "xExt"
        },
        {
          "events": "@buttonMarks:click",
          "update": "datum.text=='All'?allExt:datum.text=='Years'?yearExt:datum.text=='Months'?monthExt:datum.text=='Days'?dayExt:xDom"
        }
      ]
    },
    {
      "name": "scaledHeight",
      "update": "data('yScale').length * yRowHeight"
    },
    {
      "name": "yRange",
      "update": "[yRange!=null?yRange[0]:0,yRange!=null?yRange[0]+scaledHeight:scaledHeight]",
      "on": [
        {
          "events": [
            {"signal": "delta"}
          ],
          "update": "clampRange( [yCur[0] + span(yCur) * delta[1] / scaledHeight, yCur[1] + span(yCur) * delta[1] / scaledHeight],height>=scaledHeight?0: height-scaledHeight,height>=scaledHeight?height:scaledHeight)"
        },
        {
          "events": "dblclick",
          "update": "[0,scaledHeight]"
        },
        {
          "events": {
            "signal": "closeAll"
          },
          "update": "closeAll?[0, scaledHeight]:yRange"
        }
      ]
    },
    {
      "name": "xDomPre",
      "value": [0, 0],
      "on": [
        {
          "events": {"signal": "zoom"},
          "update": "[anchor + (xDom[0] - anchor) * zoom, anchor + (xDom[1] - anchor) * zoom]"
        }
      ]
    },
    {
      "name": "anchor",
      "value": 0,
      "on": [
        {
          "events": "wheel",
          "update": "+invert('x', x()-columnsWidth)"
        }
      ]
    },
    {
      "name": "xCur",
      "value": [0, 0],
      "on": [
        {
          "events": "pointerdown",
          "update": "slice(xDom)"
        }
      ]
    },
    {
      "name": "yCur",
      "value": [0, 0],
      "on": [
        {
          "events": "pointerdown",
          "update": "slice(yRange)"
        }
      ]
    },
    {
      "name": "delta",
      "value": [0, 0],
      "on": [
        {
          "events": [
            {
              "source": "window",
              "type": "pointermove",
              "consume": true,
              "between": [
                {"type": "pointerdown"},
                {
                  "source": "window",
                  "type": "pointerup"
                }
              ]
            }
          ],
          "update": "down ? [down[0]-x(), y()-down[1]] : [0,0]"
        }
      ]
    },
    {
      "name": "down",
      "value": null,
      "on": [
        {
          "events": "pointerdown",
          "update": "xy()"
        },
        {
          "events": "pointerup",
          "update": "null"
        }
      ]
    },
    {
      "name": "phaseClicked",
      "value": null,
      "on": [
        {
          "events": "@taskSelector:click,@phaseOutline:click",
          "update": " yCur[0]==yRange[0] && yCur[1]==yRange[1]&& xCur[0]===xDom[0]&& xCur[1]===xDom[1] && datum.phase==datum.task?  {phase: datum.phase}:null",
          "force": true
        },
        {
          "events": "@taskTooltips:click",
          "update": " yCur[0]==yRange[0] && yCur[1]==yRange[1]&& xCur[0]===xDom[0]&& xCur[1]===xDom[1] && datum.datum.phase==datum.datum.task?  {phase: datum.datum.phase}:null",
          "force": true
        }
      ]
    },
    {
      "name": "itemHovered",
      "value": {
        "id": "",
        "dependencies": []
      },
      "on": [
        {
          "events": "@taskSelector:mouseover,@phaseOutline:mouseover,@milestoneSymbols:mouseover,@taskBars:mouseover,@taskNames:mouseover,@taskLabels:mouseover",
          "update": "{'id': toString(datum.id), 'dependencies':split(datum.dependencies,',')}"
        },
        {
          "events": "@taskTooltips:mouseover",
          "update": "{'id': toString(datum.datum.id), 'dependencies':split(datum.datum.dependencies,',')}"
        },
        {
          "events": "@taskSelector:mouseout,@phaseOutline:mouseout,@milestoneSymbols:mouseout,@taskBars:mouseout,@taskNames:mouseout,@taskLabels:mouseout,@taskTooltips:mouseout",
          "update": "{'id': '', 'dependencies':[]}"
        }
      ]
    },
    {
      "name": "hover",
      "value": "",
      "on": [
        {
          "events": "@buttonMarks:pointerover",
          "update": "datum.text?datum.text:''",
          "force": true
        },
        {
          "events": "@buttonMarks:pointerout",
          "update": "''",
          "force": true
        }
      ]
    },
    {
      "name": "closeAll",
      "on": [
        {
          "events": "@buttonMarks:click",
          "update": "datum.text=='Close'?true:false",
          "force": true
        }
      ]
    },
    {
      "name": "openAll",
      "on": [
        {
          "events": "@buttonMarks:click",
          "update": "datum.text=='Open'?true:false",
          "force": true
        }
      ]
    }
  ],
  "data": [
    {"name": "dataset"},
    {
      "name": "collapsedPhases",
      "on": [
        {
          "trigger": "phaseClicked",
          "toggle": "phaseClicked"
        },
        {
          "trigger": "closeAll",
          "remove": true
        },
        {
          "trigger": "closeAll",
          "insert": "data('phases')"
        },
        {
          "trigger": "openAll",
          "remove": true
        }
      ]
    },
    {
      "name": "input",
      "source": "dataset",
      "transform": [
        {
          "type": "formula",
          "as": "start",
          "expr": "utc(year(datum.start),month(datum.start),date(datum.start))"
        },
        {
          "type": "formula",
          "as": "end",
          "expr": "utc(year(datum.end),month(datum.end),date(datum.end))"
        },
        {
          "type": "formula",
          "as": "labelEnd",
          "expr": "datum.end"
        },
        {
          "type": "formula",
          "as": "end",
          "expr": "datetime(+datum.end+oneDay)"
        },
        {
          "type": "formula",
          "as": "days",
          "expr": "(datum.end-datum.start)/oneDay"
        },
        {
          "type": "formula",
          "as": "completionLabel",
          "expr": "datum.completion+'%'"
        },
        {
          "type": "window",
          "sort": {
            "field": "start",
            "order": "ascending"
          },
          "ops": ["rank"],
          "as": ["taskSort"],
          "groupby": ["phase"]
        },
        {
          "type": "formula",
          "as": "start",
          "expr": "+datum.start"
        },
        {
          "type": "formula",
          "as": "end",
          "expr": "+datum.end"
        }
      ]
    },
    {
      "name": "phases",
      "source": "input",
      "transform": [
        {
          "type": "aggregate",
          "fields": [
            "start",
            "end",
            "completion",
            "task",
            "completion",
            "labelEnd"
          ],
          "ops": [
            "min",
            "max",
            "sum",
            "count",
            "mean",
            "max"
          ],
          "as": [
            "start",
            "end",
            "sum",
            "count",
            "completion",
            "labelEnd"
          ],
          "groupby": ["phase"]
        },
        {
          "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": "(datum.end-datum.start)/oneDay"
        },
        {
          "type": "window",
          "sort": {
            "field": "start",
            "order": "ascending"
          },
          "ops": [
            "row_number",
            "row_number"
          ],
          "as": ["phaseSort", "id"]
        },
        {
          "type": "formula",
          "as": "id",
          "expr": "length(data('input'))+datum.id+'^^^^^'"
        }
      ]
    },
    {
      "name": "phasePaths",
      "source": "phases",
      "transform": [
        {
          "type": "formula",
          "as": "phasePath",
          "expr": "'M ' + scale('x', datum.start)+' '  +   (scale('y', datum.id)+yPaddingInner) + ' H ' +  scale('x', datum.end)+' '   + ' v ' +  phaseSymbolHeight + ' L ' +  (scale('x', datum.end) - phaseSymbolWidth) +' '  +   (scale('y', datum.id)+yPaddingInner+phaseSymbolHeight/2 ) + ' L ' +  (scale('x', datum.start)+phaseSymbolWidth) + ' '  +   (scale('y', datum.id)+yPaddingInner+phaseSymbolHeight/2) + ' L ' +  (scale('x', datum.start)) + ' '  +   (scale('y', datum.id)+ yPaddingInner+phaseSymbolHeight) + ' z'"
        }
      ]
    },
    {
      "name": "tasks",
      "source": "input",
      "transform": [
        {
          "type": "filter",
          "expr": "datum.milestone != true"
        },
        {
          "type": "filter",
          "expr": "!indata('collapsedPhases', 'phase', datum.phase)"
        }
      ]
    },
    {
      "name": "milestones",
      "source": "input",
      "transform": [
        {
          "type": "filter",
          "expr": "datum.milestone == true"
        },
        {
          "type": "filter",
          "expr": "!indata('collapsedPhases', 'phase', datum.phase)"
        }
      ]
    },
    {
      "name": "yScale",
      "source": [
        "tasks",
        "phases",
        "milestones"
      ],
      "transform": [
        {
          "type": "lookup",
          "from": "phases",
          "key": "phase",
          "values": ["phaseSort"],
          "fields": ["phase"]
        },
        {
          "type": "window",
          "sort": {
            "field": [
              "phaseSort",
              "taskSort"
            ],
            "order": [
              "ascending",
              "ascending"
            ]
          },
          "ops": ["row_number"],
          "as": ["finalSort"]
        }
      ]
    },
    {
      "name": "xExt",
      "source": "input",
      "transform": [
        {
          "type": "aggregate",
          "fields": ["start", "end"],
          "ops": ["min", "max"],
          "as": ["s", "e"]
        },
        {
          "type": "formula",
          "as": "days",
          "expr": "(datum.e-datum.s)/oneDay"
        }
      ]
    },
    {
      "name": "weekends",
      "transform": [
        {
          "type": "sequence",
          "start": 0,
          "stop": {
            "signal": "dayBandwidthRound>=minMonthBandwidth? span(xDom)/oneDay:0"
          },
          "as": "sequence"
        },
        {
          "type": "formula",
          "as": "start",
          "expr": "datetime(utc(year(xDom[0]),month(xDom[0]),date(xDom[0]))   +(oneDay*datum.sequence))"
        },
        {
          "type": "filter",
          "expr": "day(datum.start) == 6 || day(datum.start) == 0 "
        },
        {
          "type": "formula",
          "as": "end",
          "expr": "datetime(+datum.start+(oneDay))"
        }
      ]
    },
    {
      "name": "taskDependencyArrows",
      "source": "yScale",
      "transform": [
        {
          "type": "filter",
          "expr": "isValid(datum.dependencies) && datum.dependencies!='' "
        }
      ]
    },
    {
      "name": "phaseDependencyArrows",
      "source": "input",
      "transform": [
        {
          "type": "filter",
          "expr": "indata('collapsedPhases', 'phase', datum.phase) "
        },
        {
          "type": "joinaggregate",
          "fields": ["id", "start"],
          "ops": ["values", "min"],
          "as": [
            "allPhaseIds",
            "start"
          ],
          "groupby": ["phase"]
        },
        {
          "type": "formula",
          "as": "id",
          "expr": "toString(datum.id)"
        },
        {
          "type": "formula",
          "as": "allPhaseIds",
          "expr": "pluck(datum.allPhaseIds, 'id')"
        },
        {
          "type": "formula",
          "as": "dependencies",
          "expr": "split(datum.dependencies,',')"
        },
        {
          "type": "flatten",
          "fields": ["dependencies"]
        },
        {
          "type": "formula",
          "as": "internalDependenciesIndex",
          "expr": "indexof(datum.allPhaseIds,datum.dependencies)"
        },
        {
          "type": "formula",
          "as": "milestone",
          "expr": "null"
        },
        {
          "type": "filter",
          "expr": "datum.dependencies!='null' && datum.dependencies!='' && datum.internalDependenciesIndex == -1 "
        },
        {
          "type": "lookup",
          "from": "phases",
          "key": "phase",
          "values": ["id"],
          "fields": ["phase"],
          "as": ["id"]
        }
      ]
    },
    {
      "name": "dependencyArrows",
      "source": [
        "taskDependencyArrows",
        "phaseDependencyArrows"
      ]
    },
    {
      "name": "dependencyLines",
      "source": [
        "yScale",
        "phaseDependencyArrows"
      ],
      "transform": [
        {
          "type": "filter",
          "expr": "isValid(datum.dependencies) && datum.dependencies!='' "
        },
        {
          "type": "formula",
          "as": "dependencies",
          "expr": "split(datum.dependencies,',')"
        },
        {
          "type": "flatten",
          "fields": ["dependencies"]
        },
        {
          "type": "lookup",
          "from": "input",
          "key": "id",
          "values": [
            "id",
            "end",
            "phase"
          ],
          "fields": ["dependencies"],
          "as": [
            "sourceId",
            "sourceEnd",
            "sourcePhase"
          ]
        },
        {
          "type": "lookup",
          "from": "phases",
          "key": "phase",
          "values": ["id", "end"],
          "fields": ["sourcePhase"],
          "as": [
            "sourcePhaseId",
            "sourcePhaseEnd"
          ]
        },
        {
          "type": "formula",
          "as": "sourceId",
          "expr": "indata('collapsedPhases', 'phase', datum.sourcePhase) == true?datum.sourcePhaseId:datum.sourceId"
        },
        {
          "type": "formula",
          "as": "sourceEnd",
          "expr": "indata('collapsedPhases', 'phase', datum.sourcePhase) == true?datum.sourcePhaseEnd:datum.sourceEnd"
        },
        {
          "type": "formula",
          "as": "plottedStart",
          "expr": "datum.milestone == null || datum.milestone == false?scale('x',datum.start)- sqrt(arrowSymbolSize) - 1:(scale('x',datum.start) +(dayBandwidth/2) - (sqrt(milestoneSymbolSize)/2) - sqrt(arrowSymbolSize)) - 1"
        },
        {
          "type": "formula",
          "as": "plottedSourceEnd",
          "expr": "scale('x',datum.sourceEnd) - (dayBandwidth/2) "
        },
        {
          "type": "formula",
          "as": "a",
          "expr": "[datum.milestone == null || datum.milestone == false?scale('x',datum.start):scale('x',datum.start) +(dayBandwidth/2) - (sqrt(milestoneSymbolSize)/2)  ,scale('y', datum.id)+bandwidth('y')/2 ]"
        },
        {
          "type": "formula",
          "as": "b",
          "expr": "[datum.plottedStart >= datum.plottedSourceEnd?datum.plottedSourceEnd :datum.plottedStart ,scale('y', datum.id)+bandwidth('y')/2]"
        },
        {
          "type": "formula",
          "as": "c",
          "expr": "[datum.plottedSourceEnd,scale('y',datum.sourceId)+bandwidth('y')/2]"
        },
        {
          "type": "formula",
          "as": "d",
          "expr": "[datum.plottedStart > datum.plottedSourceEnd?null:datum.plottedStart ,datum.plottedStart > datum.plottedSourceEnd?null:scale('y',datum.sourceId)+(bandwidth('y'))]"
        },
        {
          "type": "formula",
          "as": "e",
          "expr": "[datum.plottedStart > datum.plottedSourceEnd?null:datum.plottedSourceEnd,datum.plottedStart > datum.plottedSourceEnd?null:scale('y',datum.sourceId)+(bandwidth('y'))]"
        },
        {
          "type": "fold",
          "fields": [
            "a",
            "b",
            "d",
            "e",
            "c"
          ]
        },
        {
          "type": "filter",
          "expr": "datum.value[0] != null"
        },
        {
          "type": "formula",
          "as": "value0",
          "expr": "datum.value[0]"
        },
        {
          "type": "formula",
          "as": "value1",
          "expr": "datum.value[1]"
        },
        {
          "type": "window",
          "ops": ["row_number"],
          "as": ["duplicates"],
          "groupby": [
            "id",
            "sourceId",
            "value0",
            "value1"
          ]
        },
        {
          "type": "filter",
          "expr": "datum.duplicates == 1"
        }
      ]
    },
    {
      "name": "buttons",
      "values": [
        {
          "side": "left",
          "text": "Close",
          "x": 15,
          "leftRadius": 4
        },
        {
          "side": "left",
          "text": "Open",
          "x": 65,
          "rightRadius": 4
        },
        {
          "side": "right",
          "text": "All",
          "x": 50,
          "rightRadius": 4
        },
        {
          "side": "right",
          "text": "Years",
          "x": 100
        },
        {
          "side": "right",
          "text": "Months",
          "x": 150
        },
        {
          "side": "right",
          "text": "Days",
          "x": 200,
          "leftRadius": 4
        }
      ]
    }
  ],
  "marks": [
    {
      "name": "buttonMarks",
      "description": "All buttons",
      "type": "group",
      "from": {"data": "buttons"},
      "clip": {
        "signal": "!showButtons"
      },
      "encode": {
        "update": {
          "x": {
            "signal": "datum.side=='left'?datum.x:columnsWidth+ganttWidth-datum.x"
          },
          "width": {"value": 50},
          "y": {"value": -60},
          "height": {"signal": "18"},
          "stroke": {
            "signal": "'#7f7f7f'"
          },
          "strokeWidth": {"value": 1},
          "cornerRadiusTopLeft": {
            "field": "leftRadius"
          },
          "cornerRadiusBottomLeft": {
            "field": "leftRadius"
          },
          "cornerRadiusTopRight": {
            "field": "rightRadius"
          },
          "cornerRadiusBottomRight": {
            "field": "rightRadius"
          },
          "cursor": {
            "value": "pointer"
          },
          "fill": [
            {
              "test": "indexof( hover,datum.text)>-1",
              "value": "#4e95d9"
            },
            {
              "test": "datum.text=='Close' && data('collapsedPhases').length == data('phases').length",
              "value": "#4e95d9"
            },
            {
              "test": "datum.text=='Open' && data('collapsedPhases').length == 0",
              "value": "#4e95d9"
            },
            {
              "test": "datum.text=='Days' && dayBandwidthRound == minDayBandwidth",
              "value": "#4e95d9"
            },
            {
              "test": "datum.text=='Months' && dayBandwidthRound>=minYearBandwidth && dayBandwidthRound<minDayBandwidth",
              "value": "#4e95d9"
            },
            {
              "test": "datum.text=='Years' && dayBandwidthRound<minYearBandwidth",
              "value": "#4e95d9"
            },
            {"value": "white"}
          ]
        }
      },
      "marks": [
        {
          "name": "buttonText",
          "interactive": false,
          "type": "text",
          "encode": {
            "update": {
              "text": {
                "signal": "parent.text"
              },
              "baseline": {
                "value": "middle"
              },
              "align": {
                "value": "center"
              },
              "x": {
                "signal": "item.mark.group.width/2"
              },
              "y": {"signal": "10"},
              "fill": [
                {
                  "test": "indexof( hover,parent.text)>-1",
                  "value": "white"
                },
                {
                  "test": "parent.text=='Close' && data('collapsedPhases').length == data('phases').length",
                  "value": "white"
                },
                {
                  "test": "parent.text=='Open' && data('collapsedPhases').length == 0",
                  "value": "white"
                },
                {
                  "test": "parent.text=='Days' && dayBandwidthRound == minDayBandwidth",
                  "value": "white"
                },
                {
                  "test": "parent.text=='Months' && dayBandwidthRound>=minYearBandwidth && dayBandwidthRound<minDayBandwidth",
                  "value": "white"
                },
                {
                  "test": "parent.text=='Years' && dayBandwidthRound<minYearBandwidth",
                  "value": "white"
                },
                {"value": "#7f7f7f"}
              ]
            }
          }
        }
      ]
    },
    {
      "name": "xDomainText",
      "interactive": false,
      "type": "text",
      "encode": {
        "update": {
          "text": {
            "signal": "showDomainSpanLabel?timeFormat(xDom[0],'%d/%m/%y') +' - ' + timeFormat(xDom[1],'%d/%m/%y'):null"
          },
          "baseline": {"value": "top"},
          "align": {"value": "right"},
          "x": {
            "signal": "columnsWidth+ganttWidth"
          },
          "y": {
            "signal": "showDomainSpanLabel?height+15:0"
          },
          "fill": {
            "signal": "textColour"
          }
        }
      }
    },
    {
      "name": "phaseBackgrounds",
      "description": "Background rect for phases",
      "type": "rect",
      "clip": true,
      "zindex": 0,
      "from": {"data": "phases"},
      "encode": {
        "update": {
          "x": {"value": 0},
          "x2": {
            "signal": "columnsWidth"
          },
          "y": {
            "signal": "scale('y', datum.id)"
          },
          "height": {
            "signal": "bandwidth('y')"
          },
          "fill": {"value": "#dceaf7"},
          "opacity": {"value": 0.3}
        }
      }
    },
    {
      "name": "taskLabelSizes",
      "description": "Hidden label sizes to support tooltips when the task name doesn't completely fit",
      "type": "text",
      "clip": true,
      "from": {"data": "yScale"},
      "encode": {
        "enter": {
          "x": {"value": -100},
          "y": {"value": -100},
          "fill": {
            "value": "transparent"
          },
          "text": {
            "signal": "datum.task"
          },
          "fontSize": {"value": 11}
        }
      }
    },
    {
      "type": "rect",
      "name": "taskTooltips",
      "description": "Hidden rect to support tooltips when the task name doesn't completely fit",
      "from": {
        "data": "taskLabelSizes"
      },
      "clip": true,
      "zindex": 101,
      "encode": {
        "update": {
          "x": {"value": -15},
          "x2": {
            "signal": "taskColumnWidth"
          },
          "y": {
            "signal": "scale('y', datum.datum.id)"
          },
          "height": {
            "signal": "bandwidth('y')"
          },
          "fill": {
            "value": "transparent"
          },
          "tooltip": {
            "signal": "datum.bounds.x2 - datum.bounds.x1>=taskColumnWidth-16? datum.datum.task:null"
          },
          "cursor": {
            "signal": "datum.datum.phase == datum.datum.task?'pointer':'auto'"
          },
          "href": {
            "field": "datum.hyperlink"
          }
        }
      }
    },
    {
      "type": "group",
      "name": "columnHolder",
      "style": "cell",
      "layout": {
        "padding": {
          "signal": "columnPadding"
        },
        "bounds": "flush",
        "align": "each"
      },
      "encode": {
        "enter": {
          "x": {"signal": "0"},
          "stroke": {
            "value": "transparent"
          },
          "width": {
            "signal": "columnsWidth"
          },
          "height": {"signal": "height"}
        }
      },
      "marks": [
        {
          "type": "group",
          "name": "taskColumnWidth",
          "style": "cell",
          "title": {
            "text": "Task",
            "anchor": "start",
            "frame": "group",
            "align": "left",
            "dx": 16
          },
          "encode": {
            "enter": {
              "stroke": {
                "value": "transparent"
              },
              "width": {
                "signal": "taskColumnWidth"
              },
              "height": {
                "signal": "height"
              }
            }
          },
          "marks": [
            {
              "type": "text",
              "style": "col",
              "clip": true,
              "from": {
                "data": "yScale"
              },
              "encode": {
                "update": {
                  "align": {
                    "value": "left"
                  },
                  "dx": {"value": 16},
                  "y": {
                    "signal": "scale('y', datum.id)+bandwidth('y')/2"
                  },
                  "text": {
                    "signal": "datum.phase == datum.task?upper(datum.task):datum.task"
                  },
                  "font": {
                    "signal": "datum.phase == datum.task?'Arial':'Segoe UI'"
                  },
                  "fontWeight": {
                    "signal": "datum.phase == datum.task?'bold':'normal'"
                  },
                  "limit": {
                    "signal": "taskColumnWidth-16"
                  },
                  "fill": {
                    "signal": "toString(datum.id) == itemHovered.id  ?merge(hsl(scale('cDark', datum.phase)), {l:0.40}):textColour"
                  }
                }
              }
            },
            {
              "type": "symbol",
              "clip": true,
              "from": {
                "data": "yScale"
              },
              "encode": {
                "update": {
                  "fill": {
                    "signal": "toString(datum.id) == itemHovered.id && datum.phase == datum.task ?merge(hsl(scale('cDark', datum.phase)), {l:0.40}):datum.phase == datum.task ?scale('cDark', datum.phase):'transparent'"
                  },
                  "x": {
                    "signal": "sqrt(90)/2"
                  },
                  "size": {"value": 90},
                  "yc": {
                    "signal": "(scale('y', datum.id)+bandwidth('y')/2)-1"
                  },
                  "shape": {
                    "signal": "datum.phase == datum.task && !indata('collapsedPhases', 'phase', datum.phase)?'triangle-down':datum.phase == datum.task && indata('collapsedPhases', 'phase', datum.phase)?'triangle-right':''"
                  }
                }
              }
            }
          ]
        },
        {
          "type": "group",
          "name": "startColumnWidth",
          "style": "cell",
          "title": {
            "text": "Start",
            "anchor": "end",
            "frame": "group",
            "align": "right"
          },
          "encode": {
            "update": {
              "width": {
                "signal": "startColumnWidth"
              },
              "height": {
                "signal": "height"
              },
              "stroke": {
                "value": "transparent"
              }
            }
          },
          "marks": [
            {
              "type": "text",
              "style": "col",
              "clip": true,
              "from": {
                "data": "yScale"
              },
              "encode": {
                "update": {
                  "align": {
                    "value": "right"
                  },
                  "x": {
                    "signal": "startColumnWidth"
                  },
                  "y": {
                    "signal": "scale('y', datum.id)+bandwidth('y')/2"
                  },
                  "text": {
                    "signal": "timeFormat(datum.start,' %d/%m/%y')"
                  },
                  "font": {
                    "signal": "datum.phase == datum.task?'Arial':'Segoe UI'"
                  },
                  "fontWeight": {
                    "signal": "datum.phase == datum.task?'bold':'normal'"
                  },
                  "fill": {
                    "signal": "toString(datum.id) == itemHovered.id  ?merge(hsl(scale('cDark', datum.phase)), {l:0.40}):textColour"
                  }
                }
              }
            }
          ]
        },
        {
          "type": "group",
          "name": "endColumnWidth",
          "style": "cell",
          "title": {
            "text": "End",
            "anchor": "end",
            "frame": "group",
            "align": "right"
          },
          "encode": {
            "update": {
              "width": {
                "signal": "endColumnWidth"
              },
              "stroke": {
                "value": "transparent"
              },
              "height": {
                "signal": "height"
              }
            }
          },
          "marks": [
            {
              "type": "text",
              "style": "col",
              "clip": true,
              "from": {
                "data": "yScale"
              },
              "encode": {
                "update": {
                  "align": {
                    "value": "right"
                  },
                  "x": {
                    "signal": "endColumnWidth"
                  },
                  "y": {
                    "signal": "scale('y', datum.id)+bandwidth('y')/2"
                  },
                  "text": {
                    "signal": "timeFormat(datum.labelEnd,' %d/%m/%y')"
                  },
                  "font": {
                    "signal": "datum.phase == datum.task?'Arial':'Segoe UI'"
                  },
                  "fontWeight": {
                    "signal": "datum.phase == datum.task?'bold':'normal'"
                  },
                  "fill": {
                    "signal": "toString(datum.id) == itemHovered.id  ?merge(hsl(scale('cDark', datum.phase)), {l:0.40}):textColour"
                  }
                }
              }
            }
          ]
        },
        {
          "type": "group",
          "name": "assigneeColumnWidth",
          "style": "cell",
          "title": {
            "text": "Assignee",
            "anchor": "end",
            "frame": "group",
            "align": "right"
          },
          "encode": {
            "update": {
              "width": {
                "signal": "assigneeColumnWidth"
              },
              "stroke": {
                "value": "transparent"
              },
              "height": {
                "signal": "height"
              }
            }
          },
          "marks": [
            {
              "type": "text",
              "style": "col",
              "clip": true,
              "from": {
                "data": "yScale"
              },
              "encode": {
                "update": {
                  "align": {
                    "value": "right"
                  },
                  "x": {
                    "signal": "assigneeColumnWidth"
                  },
                  "y": {
                    "signal": "scale('y', datum.id)+bandwidth('y')/2"
                  },
                  "text": {
                    "signal": "datum.assignee"
                  },
                  "font": {
                    "signal": "datum.phase == datum.task?'Arial':'Segoe UI'"
                  },
                  "fontWeight": {
                    "signal": "datum.phase == datum.task?'bold':'normal'"
                  },
                  "fill": {
                    "signal": "toString(datum.id) == itemHovered.id  ?merge(hsl(scale('cDark', datum.phase)), {l:0.40}):textColour"
                  }
                }
              }
            }
          ]
        },
        {
          "type": "group",
          "name": "daysColumnWidth",
          "style": "cell",
          "title": {
            "text": "Days",
            "anchor": "end",
            "frame": "group",
            "align": "right"
          },
          "encode": {
            "update": {
              "width": {
                "signal": "daysColumnWidth"
              },
              "stroke": {
                "value": "transparent"
              },
              "height": {
                "signal": "height"
              }
            }
          },
          "marks": [
            {
              "type": "text",
              "style": "col",
              "clip": true,
              "from": {
                "data": "yScale"
              },
              "encode": {
                "update": {
                  "align": {
                    "value": "right"
                  },
                  "x": {
                    "signal": "daysColumnWidth"
                  },
                  "y": {
                    "signal": "scale('y', datum.id)+bandwidth('y')/2"
                  },
                  "text": {
                    "signal": "datum.days+' d'"
                  },
                  "fontWeight": {
                    "signal": "datum.phase == datum.task?'bold':'normal'"
                  },
                  "font": {
                    "signal": "datum.phase == datum.task?'Arial':'Segoe UI'"
                  },
                  "fill": {
                    "signal": "toString(datum.id) == itemHovered.id  ?merge(hsl(scale('cDark', datum.phase)), {l:0.40}):textColour"
                  }
                }
              }
            }
          ]
        },
        {
          "type": "group",
          "name": "completionColumn",
          "style": "cell",
          "title": {
            "text": "Progress",
            "anchor": "start",
            "frame": "group"
          },
          "encode": {
            "update": {
              "width": {
                "signal": "progressColumnWidth"
              },
              "stroke": {
                "value": "transparent"
              },
              "height": {
                "signal": "height"
              }
            }
          },
          "marks": [
            {
              "type": "rect",
              "clip": true,
              "from": {
                "data": "yScale"
              },
              "encode": {
                "update": {
                  "x": {"signal": "1"},
                  "width": {
                    "signal": "item.mark.group.width-2"
                  },
                  "stroke": {
                    "signal": "'#a0d786'"
                  },
                  "yc": {
                    "signal": "(scale('y',datum.id)+bandwidth('y')/2)"
                  },
                  "fill": {
                    "value": "white"
                  },
                  "height": {
                    "signal": "bandwidth('y')-yPaddingInner*2"
                  },
                  "strokeWidth": {
                    "signal": "datum.id == itemHovered.id?2:1"
                  }
                }
              }
            },
            {
              "type": "rect",
              "clip": true,
              "from": {
                "data": "yScale"
              },
              "encode": {
                "update": {
                  "x": {"signal": "1"},
                  "width": {
                    "signal": "(item.mark.group.width/100)*datum.completion"
                  },
                  "fill": {
                    "signal": "'#c6ecb5'"
                  },
                  "yc": {
                    "signal": "(scale('y',datum.id)+bandwidth('y')/2)"
                  },
                  "strokeWidth": {
                    "value": 1
                  },
                  "height": {
                    "signal": "bandwidth('y')-yPaddingInner*2"
                  }
                }
              }
            },
            {
              "type": "text",
              "clip": true,
              "from": {
                "data": "yScale"
              },
              "encode": {
                "update": {
                  "align": {
                    "value": "left"
                  },
                  "dx": {"value": 3},
                  "fill": {
                    "signal": "textColour"
                  },
                  "y": {
                    "signal": "scale('y',datum.id)+bandwidth('y')/2"
                  },
                  "text": {
                    "signal": "datum.completion+'%'"
                  }
                }
              }
            }
          ]
        }
      ]
    },
    {
      "type": "group",
      "name": "weekendContainer",
      "encode": {
        "update": {
          "x": {
            "signal": "columnsWidth"
          },
          "y": {"signal": "-15"},
          "clip": {"signal": "true"},
          "height": {
            "signal": "height+15"
          },
          "width": {
            "signal": "ganttWidth"
          },
          "fill": {
            "value": "transparent"
          }
        }
      },
      "marks": [
        {
          "type": "rect",
          "description": "Weekend shading",
          "name": "weekendShading",
          "from": {"data": "weekends"},
          "encode": {
            "update": {
              "x": {
                "signal": "scale('x',datum.start)"
              },
              "x2": {
                "signal": "scale('x',datum.end)"
              },
              "y": {
                "signal": "dayBandwidthRound>=minDayBandwidth?0:15"
              },
              "y2": {
                "signal": "scaledHeight<height?yRange[1]+15:height+15"
              },
              "strokeWidth": {
                "signal": "1"
              },
              "stroke": {
                "value": "#f1f1f1"
              },
              "fill": {
                "value": "#f1f1f1"
              }
            }
          }
        },
        {
          "name": "todayHighlight",
          "description": "Today highlight",
          "type": "rect",
          "data": [{}],
          "encode": {
            "update": {
              "x": {
                "signal": "scale('x',today) "
              },
              "width": {
                "signal": "dayBandwidth"
              },
              "y": {"value": 0},
              "height": {
                "signal": "15"
              },
              "fill": {
                "value": "#a5c8e4"
              }
            }
          }
        }
      ]
    },
    {
      "type": "group",
      "name": "ganttContainer",
      "encode": {
        "update": {
          "x": {
            "signal": "columnsWidth"
          },
          "y": {"signal": "0"},
          "clip": {"signal": "true"},
          "height": {
            "signal": "height"
          },
          "width": {
            "signal": "ganttWidth"
          },
          "fill": {
            "value": "transparent"
          }
        }
      },
      "marks": [
        {
          "name": "completionLabelSizes",
          "type": "text",
          "from": {"data": "tasks"},
          "encode": {
            "enter": {
              "fill": {
                "value": "transparent"
              },
              "text": {
                "signal": "datum.completionLabel"
              }
            }
          }
        },
        {
          "name": "taskLabels",
          "description": "Task, milestone and phase names",
          "from": {"data": "yScale"},
          "type": "text",
          "encode": {
            "update": {
              "x": {
                "scale": "x",
                "field": "end"
              },
              "align": {
                "signal": "'left'"
              },
              "dx": {
                "signal": "datum.milestone?sqrt(milestoneSymbolSize)/2 - dayBandwidth/2 + 5:5"
              },
              "y": {
                "signal": "datum.phase == datum.task?scale('y', datum.id)-2:scale('y', datum.id)"
              },
              "dy": {
                "signal": "bandwidth('y')/2"
              },
              "fill": {
                "signal": "toString(datum.id) == itemHovered.id  ?merge(hsl(scale('cDark', datum.phase)), {l:0.40}):textColour"
              },
              "text": {
                "signal": "datum.milestone?datum.task:datum.task && datum.assignee? datum.task + ' ('+ datum.days+' d'+')' +' - ' + datum.assignee:datum.task + ' ('+ datum.days+' d'+')'"
              }
            }
          }
        },
        {
          "type": "group",
          "from": {
            "facet": {
              "name": "dependencyLinesFacet",
              "data": "dependencyLines",
              "groupby": [
                "id",
                "sourceId"
              ]
            }
          },
          "marks": [
            {
              "type": "line",
              "from": {
                "data": "dependencyLinesFacet"
              },
              "encode": {
                "enter": {
                  "x": {
                    "signal": "datum.value[0]"
                  },
                  "y": {
                    "signal": "datum.value[1]"
                  },
                  "stroke": {
                    "value": "#888888"
                  },
                  "strokeWidth": {
                    "value": 1
                  },
                  "interpolate": {
                    "value": "linear"
                  },
                  "strokeJoin": {
                    "value": "bevel"
                  },
                  "strokeCap": {
                    "value": "round"
                  },
                  "defined": {
                    "value": true
                  }
                },
                "update": {
                  "stroke": {
                    "signal": "toString(datum.id) == itemHovered.id  ?merge(hsl(scale('cDark', datum.phase)), {l:0.40}):'#888888'"
                  },
                  "strokeWidth": {
                    "signal": "datum.id == itemHovered.id?1.5:1"
                  }
                }
              }
            }
          ]
        },
        {
          "name": "todayRule",
          "description": "Today rule",
          "type": "rule",
          "data": [{}],
          "encode": {
            "update": {
              "x": {
                "signal": "scale('x',today+oneDay/2) "
              },
              "y2": {
                "signal": "scaledHeight<height?yRange[1]:height"
              },
              "strokeWidth": {
                "value": 1
              },
              "stroke": {
                "value": "#377eb9"
              },
              "strokeDash": {
                "value": [2, 2]
              },
              "opacity": {"value": 0.8}
            }
          }
        },
        {
          "name": "todayText",
          "description": "Today text",
          "type": "text",
          "data": [{}],
          "encode": {
            "update": {
              "x": {
                "signal": "scale('x',today+oneDay/2)"
              },
              "fill": {
                "value": "#377eb9"
              },
              "text": {
                "value": "Today"
              },
              "angle": {"signal": "90"},
              "baseline": {
                "value": "bottom"
              },
              "dx": {"value": 10},
              "dy": {"value": -4},
              "opacity": {"value": 0.7}
            }
          }
        },
        {
          "name": "taskBars",
          "description": "The task bars (serve as an outline for percent complete)",
          "type": "group",
          "from": {"data": "tasks"},
          "encode": {
            "update": {
              "clip": {
                "signal": "true"
              },
              "x": {
                "scale": "x",
                "field": "start"
              },
              "x2": {
                "scale": "x",
                "field": "end"
              },
              "yc": {
                "signal": "(scale('y',datum.id)+bandwidth('y')/2)"
              },
              "height": {
                "signal": "bandwidth('y')-yPaddingInner*2"
              },
              "tooltip": {
                "signal": "showTooltips&&down==null?{'Phase':datum.phase ,'Task':datum.task , 'Start':timeFormat(datum.start,'%a, %d %B %Y' ),'End':timeFormat(datum.labelEnd,'%a, %d %B %Y' ), 'Days':datum.days, 'Assignee':datum.assignee ,'Progress':datum.completionLabel }:null"
              },
              "fill": {
                "signal": "toString(datum.id) == itemHovered.id  || indexof(itemHovered.dependencies,toString(datum.id) )> -1 ?merge(hsl(scale('cLight', datum.phase)), {l:0.65}):scale('cLight', datum.phase)"
              },
              "stroke": {
                "signal": "toString(datum.id) == itemHovered.id  || indexof(itemHovered.dependencies,toString(datum.id) )> -1 ?merge(hsl(scale('cDark', datum.phase)), {l:0.40}):scale('cDark', datum.phase)"
              },
              "cornerRadius": {
                "value": 5
              },
              "zindex": {"value": 101},
              "strokeWidth": {
                "signal": "toString(datum.id) == itemHovered.id  || indexof(itemHovered.dependencies,toString(datum.id) )> -1 ?1.5:1"
              },
              "href": {
                "field": "hyperlink"
              }
            }
          },
          "transform": [
            {
              "type": "lookup",
              "from": "completionLabelSizes",
              "key": "datum.id",
              "fields": ["datum.id"],
              "values": [
                "bounds.x1",
                "bounds.x2"
              ],
              "as": ["a", "b"]
            }
          ],
          "marks": [
            {
              "name": "taskFills",
              "description": "Percent complete for each task",
              "type": "rect",
              "interactive": false,
              "encode": {
                "update": {
                  "x": {"signal": "0"},
                  "y": {"signal": "0"},
                  "height": {
                    "signal": "item.mark.group.height"
                  },
                  "width": {
                    "signal": "(item.mark.group.width/100)* item.mark.group.datum.completion"
                  },
                  "fill": {
                    "signal": "toString(item.mark.group.datum.id) == itemHovered.id  || indexof(itemHovered.dependencies,toString(item.mark.group.datum.id) )> -1 ?merge(hsl(scale('cDark', item.mark.group.datum.phase)), {l:0.40}):scale('cDark', item.mark.group.datum.phase)"
                  },
                  "strokeWidth": {
                    "value": 0
                  },
                  "cornerRadiusBottomLeft": {
                    "value": 5
                  },
                  "cornerRadiusTopLeft": {
                    "value": 5
                  }
                }
              }
            },
            {
              "name": "completeText",
              "description": "Completion Text",
              "type": "text",
              "interactive": false,
              "encode": {
                "update": {
                  "x": {
                    "signal": "(item.mark.group.width/100)* parent.completion"
                  },
                  "align": {
                    "signal": "'right'"
                  },
                  "dx": {
                    "signal": "-2"
                  },
                  "baseline": {
                    "value": "middle"
                  },
                  "y": {
                    "signal": "item.mark.group.height/2+1"
                  },
                  "text": {
                    "signal": "round(((item.mark.group.width/100)* parent.completion))>=(item.mark.group.b+4) && parent.completion>0?parent.completionLabel:''"
                  },
                  "fill": {
                    "signal": "luminance(item.mark.group.stroke) >=0.45?'black':'white'"
                  }
                }
              }
            }
          ]
        },
        {
          "name": "phaseOutline",
          "description": "The phase bar outlines",
          "type": "path",
          "from": {
            "data": "phasePaths"
          },
          "encode": {
            "update": {
              "path": {
                "signal": "datum.phasePath"
              },
              "fill": {
                "signal": "toString(datum.id) == itemHovered.id  || indexof(itemHovered.dependencies,toString(datum.id) )> -1 ?merge(hsl(scale('cLight', datum.phase)), {l:0.65}):scale('cLight', datum.phase)"
              },
              "stroke": {
                "signal": "toString(datum.id) == itemHovered.id  || indexof(itemHovered.dependencies,toString(datum.id) )> -1 ?merge(hsl(scale('cDark', datum.phase)), {l:0.40}):scale('cDark', datum.phase)"
              },
              "strokeWidth": {
                "signal": "datum.id == itemHovered.id?1.5:1"
              },
              "tooltip": {
                "signal": "showTooltips&&down==null?{'Phase':datum.phase , 'Start':timeFormat(datum.start,'%a, %d %B %Y' ),'End':timeFormat(datum.labelEnd,'%a, %d %B %Y' ), 'Days':datum.days,'Progress':datum.completion+'%' }:null"
              },
              "cursor": {
                "value": "pointer"
              }
            }
          }
        },
        {
          "name": "phaseGroup",
          "description": "Group to hold the x y coordinates for the SVG clipping fills",
          "type": "group",
          "clip": true,
          "from": {
            "data": "phasePaths"
          },
          "encode": {
            "update": {
              "strokeWidth": {
                "value": 0
              },
              "stroke": {
                "value": "red"
              },
              "x": {
                "scale": "x",
                "field": "start",
                "offset": 0
              },
              "x2": {
                "scale": "x",
                "field": "end",
                "offset": 0
              },
              "yc": {
                "signal": "scale('y',datum.id)+bandwidth('y')/2"
              },
              "height": {
                "signal": "bandwidth('y')-yPaddingInner*2"
              }
            }
          },
          "marks": [
            {
              "name": "phaseFills",
              "description": "Percent complete for each phase. Clipping path signal has to be here as it fails to update on zoom when coming from a dataset. The only value available in the clipping path signal is parent!",
              "type": "rect",
              "interactive": false,
              "clip": {
                "path": {
                  "signal": "'M 0 0'  + ' L ' +   (scale('x', parent.end) - scale('x', parent.start)) +' 0'    + ' v ' +  phaseSymbolHeight + ' L ' + (scale('x', parent.end) - scale('x', parent.start) - phaseSymbolWidth) +' '  +   (phaseSymbolHeight/2) + ' H ' +  phaseSymbolWidth + ' L 0 '   +   phaseSymbolHeight + ' z'"
                }
              },
              "encode": {
                "update": {
                  "height": {
                    "signal": "phaseSymbolHeight"
                  },
                  "width": {
                    "signal": "(item.mark.group.width/100)* item.mark.group.datum.completion"
                  },
                  "fill": {
                    "signal": "toString(item.mark.group.datum.id) == itemHovered.id  || indexof(itemHovered.dependencies,toString(item.mark.group.datum.id) )> -1 ?merge(hsl(scale('cDark', item.mark.group.datum.phase)), {l:0.40}):scale('cDark', item.mark.group.datum.phase)"
                  },
                  "strokeWidth": {
                    "value": 0
                  },
                  "stroke": {
                    "value": "red"
                  }
                }
              }
            }
          ]
        },
        {
          "name": "milestoneSymbols",
          "description": "Milestones",
          "type": "symbol",
          "from": {
            "data": "milestones"
          },
          "encode": {
            "update": {
              "x": {
                "signal": "scale('x',datum.start)+dayBandwidth/2"
              },
              "y": {
                "signal": "scale('y', datum.id)+bandwidth('y')/2"
              },
              "size": {
                "signal": "milestoneSymbolSize"
              },
              "shape": {
                "value": "diamond"
              },
              "fill": {
                "signal": "(toString(datum.id) == itemHovered.id  || indexof(itemHovered.dependencies,toString(datum.id) )> -1)  && datum.completion > 0 ?merge(hsl(scale( 'cDark', datum.phase)), {l:0.40}):toString(datum.id) == itemHovered.id  || indexof(itemHovered.dependencies,toString(datum.id) )> -1   ?merge(hsl(scale( 'cLight', datum.phase)), {l:0.65}):datum.completion > 0?  scale('cDark', datum.phase):scale('cLight', datum.phase)"
              },
              "stroke": {
                "signal": "toString(datum.id) == itemHovered.id  || indexof(itemHovered.dependencies,toString(datum.id) )> -1 ?merge(hsl(scale('cDark', datum.phase)), {l:0.40}):scale('cDark', datum.phase)"
              },
              "strokeWidth": {
                "signal": "toString(datum.id) == itemHovered.id  || indexof(itemHovered.dependencies,toString(datum.id) )> -1  ?1.5:1"
              },
              "tooltip": {
                "signal": "showTooltips&&down==null?{'Phase':datum.phase ,'Task':datum.task , 'Start':timeFormat(datum.start,'%a, %d %B %Y' ),'End':timeFormat(datum.labelEnd,'%a, %d %B %Y' ), 'Days':datum.days,'Assignee':datum.assignee ,'Progress':datum.completionLabel }:null"
              }
            }
          }
        },
        {
          "name": "taskDependencyArrowsymbol",
          "description": "Dependency arrows",
          "type": "symbol",
          "from": {
            "data": "dependencyArrows"
          },
          "encode": {
            "update": {
              "shape": {
                "value": "triangle-right"
              },
              "x": {
                "signal": "scale('x',datum.start)",
                "offset": {
                  "signal": "datum.milestone!=null && datum.milestone != false? -(sqrt(arrowSymbolSize))/2 + dayBandwidth/2 - (sqrt(milestoneSymbolSize))/2 +1:-(sqrt(arrowSymbolSize)/2) +1"
                }
              },
              "y": {
                "signal": "scale('y', datum.id)+bandwidth('y')/2"
              },
              "fill": {
                "signal": "toString(datum.id) == itemHovered.id  ?merge(hsl(scale('cDark', datum.phase)), {l:0.40}):'#6a6a6a'"
              },
              "size": {
                "signal": "arrowSymbolSize"
              }
            }
          }
        }
      ]
    },
    {
      "name": "taskSelector",
      "description": "Hidden rect to support phase expand and collapse",
      "type": "rect",
      "clip": true,
      "zindex": 99,
      "from": {"data": "yScale"},
      "encode": {
        "update": {
          "x": {"value": -15},
          "x2": {
            "signal": "columnsWidth"
          },
          "y": {
            "signal": "scale('y', datum.id)"
          },
          "height": {
            "signal": "bandwidth('y')"
          },
          "fill": {
            "value": "transparent"
          },
          "cursor": {
            "signal": "datum.phase == datum.task?'pointer':'auto'"
          },
          "href": {"field": "hyperlink"}
        }
      }
    },
    {
      "type": "group",
      "name": "axisClipper",
      "style": "cell",
      "clip": true,
      "encode": {
        "enter": {
          "width": {
            "signal": "columnsWidth"
          },
          "stroke": {
            "value": "transparent"
          },
          "height": {"signal": "height"}
        }
      },
      "axes": [
        {
          "scale": "y",
          "orient": "right",
          "encode": {
            "ticks": {
              "update": {
                "x2": {
                  "signal": "-columnsWidth"
                }
              }
            }
          },
          "tickColor": "#f1f1f1",
          "labels": false,
          "title": "",
          "grid": false,
          "ticks": true,
          "bandPosition": {
            "signal": "0"
          }
        }
      ]
    }
  ],
  "axes": [
    {
      "description": "Bottom date axis",
      "ticks": true,
      "labelPadding": -12,
      "scale": "x",
      "position": {
        "signal": "columnsWidth"
      },
      "orient": "top",
      "tickSize": 15,
      "grid": false,
      "zindex": 1,
      "labelOverlap": false,
      "formatType": "time",
      "tickCount": {
        "signal": "dayBandwidthRound>=minYearBandwidth?'day':'month'"
      },
      "encode": {
        "ticks": {
          "update": {
            "strokeWidth": [
              {
                "test": "dayBandwidthRound>=minDayBandwidth",
                "value": 1
              },
              {
                "test": "dayBandwidthRound>=minMonthBandwidth && dayBandwidthRound<minDayBandwidth && date(datum.value) == 1",
                "value": 1
              },
              {
                "test": "dayBandwidthRound>=minYearBandwidth && dayBandwidthRound<minMonthBandwidth && date(datum.value) == 1",
                "value": 1
              },
              {
                "test": "dayBandwidthRound<minYearBandwidth && dayofyear(datum.value) == 1",
                "value": 1
              },
              {"value": 0}
            ]
          }
        },
        "labels": {
          "update": {
            "text": [
              {
                "test": "dayBandwidthRound>=minDayBandwidth",
                "signal": "timeFormat(datum.value,'%d')"
              },
              {
                "test": "dayBandwidthRound>=minMonthBandwidth && dayBandwidthRound<minDayBandwidth && date(datum.value) == 15",
                "signal": "timeFormat(datum.value,'%B %y')"
              },
              {
                "test": "dayBandwidthRound>=minYearBandwidth && dayBandwidthRound<minMonthBandwidth && date(datum.value) == 15",
                "signal": "timeFormat(datum.value,'%b')"
              },
              {
                "test": "dayBandwidthRound<minYearBandwidth && month(datum.value) == 6",
                "signal": "timeFormat(datum.value,'%Y')"
              },
              {"value": ""}
            ],
            "dx": {
              "signal": "dayBandwidthRound/2"
            }
          }
        }
      }
    },
    {
      "description": "Top date axis",
      "scale": "x",
      "position": {
        "signal": "columnsWidth"
      },
      "domain": false,
      "orient": "top",
      "offset": 0,
      "tickSize": 22,
      "labelBaseline": "middle",
      "grid": false,
      "zindex": 0,
      "tickCount": {
        "signal": "dayBandwidthRound>=minYearBandwidth?'day':'month'"
      },
      "encode": {
        "ticks": {
          "update": {
            "strokeWidth": [
              {
                "test": "dayBandwidthRound>=minDayBandwidth && date(datum.value) == 1",
                "value": 1
              },
              {
                "test": "dayBandwidthRound>=minYearBandwidth && dayBandwidthRound<minDayBandwidth && dayofyear(datum.value) == 1",
                "value": 1
              },
              {"value": 0}
            ]
          }
        },
        "labels": {
          "update": {
            "text": [
              {
                "test": "dayBandwidthRound>=minDayBandwidth && date(datum.value) == 15",
                "signal": "timeFormat(datum.value,'%B %y')"
              },
              {
                "test": "dayBandwidthRound>=minYearBandwidth && dayBandwidthRound<minMonthBandwidth && month(datum.value) == 5 && date(datum.value) == 15",
                "signal": "timeFormat(datum.value,'%Y')"
              },
              {"value": ""}
            ],
            "dx": {
              "signal": "dayBandwidthRound/2"
            }
          }
        }
      }
    },
    {
      "description": "Month grid lines",
      "scale": "x",
      "position": {
        "signal": "columnsWidth"
      },
      "domain": false,
      "orient": "top",
      "labels": false,
      "grid": true,
      "tickSize": 0,
      "zindex": 0,
      "tickCount": {
        "signal": " dayBandwidthRound>=minMonthBandwidth || dayBandwidthRound<=0.35?0:'month'"
      }
    }
  ],
  "scales": [
    {
      "name": "x",
      "type": "time",
      "domain": {"signal": "xDom"},
      "range": {
        "signal": "[0,ganttWidth]"
      }
    },
    {
      "name": "y",
      "type": "band",
      "domain": {
        "fields": [
          {
            "data": "yScale",
            "field": "id"
          }
        ],
        "sort": {
          "op": "min",
          "field": "finalSort",
          "order": "ascending"
        }
      },
      "range": {"signal": "yRange"}
    },
    {
      "name": "cDark",
      "type": "ordinal",
      "range": {
        "signal": "coloursDark"
      },
      "domain": {
        "data": "input",
        "field": "phase"
      }
    },
    {
      "name": "cLight",
      "type": "ordinal",
      "range": {
        "signal": "coloursLight"
      },
      "domain": {
        "data": "input",
        "field": "phase"
      }
    }
  ],
  "config": {
    "view": {"stroke": "transparent"},
    "style": {
      "col": {"fontSize": 11},
      "cell": {
        "strokeWidth": {"value": "0"}
      }
    },
    "font": "Segoe UI",
    "text": {
      "font": "Segoe UI",
      "fontSize": 10,
      "baseline": "middle"
    },
    "axis": {
      "labelColor": {
        "signal": "textColour"
      },
      "labelFontSize": 10
    },
    "title": {
      "color": {"signal": "textColour"}
    }
  }
}

The end result looks like this:

screenshot of bacci deneb gantt

There’s no word wrap on the columns, so you’ll want to give it a width that fits the widest name you have in the top bit of the code – I’m using 100 here.

Leave a Comment