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.
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:
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:
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.