Skip to content

Instantly share code, notes, and snippets.

@Giammaria
Last active April 16, 2025 16:54
Show Gist options
  • Save Giammaria/d7491f2bad63178f3daf6868762eea02 to your computer and use it in GitHub Desktop.
Save Giammaria/d7491f2bad63178f3daf6868762eea02 to your computer and use it in GitHub Desktop.
20250409_hierarchical_gantt_v_v2
{
"$schema": "https://vega.github.io/schema/vega/v6.json",
"background": "#fff",
"signals": [
{"name": "desiredHeight", "update": "300"},
{
"name": "adjustedHeight",
"description": "he initial height of the visualization, set to 300. Rows that go beyond this height will require scrolling/panning",
"update": "min(desiredHeight, actualHeight)"
},
{
"name": "width",
"description": "The sum of the columns section width, Gantt width, and vertical scroll bar width",
"value": 1200
},
{
"name": "resetLevel",
"description": "the number of levels deep to revert to when collapsing all nodes",
"init": "configInitialLevel",
"on": [
{
"events": "@expandAll_collapseAll_buttons:click",
"update": "!isValid(datum.datum) ? resetLevel : datum.datum.name === 'expandAll' ? 9999999999999 : configInitialLevel"
}
]
},
{
"name": "configGantt",
"description": "configurations for the Gantt",
"update": "{x: columnsWidth, childRect: {cornerRadius: 1, percentOfRowHeight: 0.5}, parentRect: {percentOfRowHeight: 0.25}, label: {xOffset: 7.5, font: 'Segoe UI', fontSize: 11, align: 'left', fontWeight: 400, fill: '#999'}, todayLine: {label: {text: 'Today'}, stroke: '#708090', strokeWidth: 0.5, strokeDash: [2,3] }}"
},
{
"name": "configIncludeRoot",
"description": "boolean to indicate whether the root node should be visible",
"init": "false"
},
{
"name": "configInitialLevel",
"description": "the number of levels deep to start with",
"value": 1
},
{
"name": "configRow",
"description": "configurations for the rows",
"init": "{rowHeight: 25, levelIndentWidth: 15, defaultFill: '#40407d'}"
},
{
"name": "columnsWidthPercent",
"description": "For testing purposes only; will repurpose once development is complete. Defines the percentage of total width allocated to columns. Initialized to 0.25. It is clamped between 0 and 0.5 to ensure reasonable layout proportions",
"value": 0.25,
"update": "clamp(columnsWidthPercent, 0, 0.5)"
},
{
"name": "columnsWidth",
"description": "Calculates the actual column width as width * columnsWidthPercent. Updates dynamically based on width and columnsWidthPercent",
"update": "width*columnsWidthPercent"
},
{
"name": "ganttWidth",
"description": "Calculates the width of the Gantt chart as width * (1 - columnsWidthPercent). If vertical scrolling is enabled (actualHeight > adjustedHeight), it subtracts extra space for a scrollbar",
"update": "width*(1-columnsWidthPercent)-(actualHeight>adjustedHeight ? configVerticalScrollbar.track.width*1.5 : 0)"
},
{
"name": "timeSeriesBlockPadding",
"description": "A static signal that defines the inner and outer padding in pixels for each time-series block.",
"value": 3.5
},
{
"name": "actualHeight",
"description": "Computes the total height based on the number of all time-series blocks times configRow.rowHeight. Updates dynamically when the dataset changes",
"update": "data('height')[0]['height']"
},
{
"name": "mouseoverRectDatum",
"description": "the datum associated with the node that is currently being moused over",
"init": "null",
"on": [
{
"events": "@row_clickable_rect:mouseover",
"update": "isValid(datum) ? datum : null"
},
{"events": "@row_clickable_rect:mouseout", "update": "null"}
]
},
{
"name": "expandAllClicked",
"description": "boolean indicating whether expandAll was the last element clicked",
"init": "false",
"on": [
{
"events": "@expandAll_collapseAll_buttons:click",
"update": "!isValid(datum.datum) ? resetLevel : datum.datum.name === 'expandAll' ? true : false"
},
{
"events": "@row_clickable_rect:click[!event.ctrlKey]",
"update": "false"
}
]
},
{
"name": "lastClickedNode",
"description": "node in the hierarchy with children that was last expanded or collapsed",
"init": "null",
"on": [
{
"events": "@row_clickable_rect:click[!event.ctrlKey]",
"update": "isValid(datum) && datum.hasChildren ? {timestamp: now(), datum: datum} : lastClickedNode"
}
]
},
{
"name": "isInitial",
"description": "boolean indicating if this is considered the initial expand/collapse state",
"init": "true",
"on": [
{
"events": "@row_clickable_rect:click[!event.ctrlKey]",
"update": "isValid(datum) && datum.hasChildren ? false : isInitial"
},
{"events": "@expandAll_collapseAll_buttons:click", "update": "true"}
]
},
{
"name": "configVerticalScrollbar",
"description": "configurations for the vertical scroll bar",
"update": "{enabled: actualHeight>adjustedHeight ,innerPadding: 10, track: {width: 10, height: extent([actualHeight, adjustedHeight])[0], fill: '#F3F3F3'}, handle: {height: max((adjustedHeight/actualHeight)*extent([actualHeight, adjustedHeight])[0], 30), fill: '#ddd', hover: {fill: '#888'}}}"
},
{
"name": "configTogglepanAndZoomMode",
"description": "configurations for the mouse wheel granularity control",
"update": "{enabled: true, initialValue: false, xOffset: 0, track: {height: 7.5, width: 25, cornerRadius: 5, fill: '#d1e0ec', stroke: '#777', strokeWidth: 1}, handle: {stroke: '#777', strokeWidth: 1, fill: '#fff'}, label: {text: 'Mouse wheel granularity', font: 'Segoe UI', fontSize: 11, fill: '#888', fontStyle: 'regular', dx: 5}, on: {fill: '#d1e0ec', fillOpacity: 1, stroke: '#777', strokeWidth: 1}, tooltip: {object: {title: 'Mouse Wheel Scroll vs. Zoom', '‎‎':'‎', '• ‎ ‎ ‎ ‎ ‎ ‎': 'Click here to toggle between vertical scrolling and date granularity zooming.', '‎‎‎':'‎', '• ‎ ‎ ‎ ‎ ‎ ‎‎': 'Alternatively, press the Ctrl key to quickly toggle between modes.'}}}"
},
{
"name": "configDateStepSlider",
"description": "configurations for the date granularity slider control",
"update": "{enabled: true, innerPadding: 10, track: {height: 7.5, width: 130, cornerRadius: 5, fill: '#EEE', stroke: '#777', strokeWidth: 1}, handle: {outerStroke: '#777', outerStrokeWidth: 1, outerfill: '#fff', innerStroke: '#777', innerStrokeWidth: 1, innerFill: '#666'}, label: {text: 'Date Granularity', font: 'Segoe UI', fontSize: 10, fill: '#666', fontStyle: 'regular', dy: 10}, progress: {fill: '#d1e0ec', fillOpacity: 1}, tooltip: {text: 'Adjust date granularity'}, reset: {iconPath: 'M75 75L41 41C25.9 25.9 0 36.6 0 57.9L0 168c0 13.3 10.7 24 24 24l110.1 0c21.4 0 32.1-25.9 17-41l-30.8-30.8C155 85.5 203 64 256 64c106 0 192 86 192 192s-86 192-192 192c-40.8 0-78.6-12.7-109.7-34.4c-14.5-10.1-34.4-6.6-44.6 7.9s-6.6 34.4 7.9 44.6C151.2 495 201.7 512 256 512c141.4 0 256-114.6 256-256S397.4 0 256 0C185.3 0 121.3 28.7 75 75zm181 53c-13.3 0-24 10.7-24 24l0 104c0 6.4 2.5 12.5 7 17l72 72c9.4 9.4 24.6 9.4 33.9 0s9.4-24.6 0-33.9l-65-65 0-94.1c0-13.3-10.7-24-24-24z', tooltipText: 'Reset Granularity', fill:'#888', hoverFill: '#333'}}"
},
{
"name": "panAndZoomMode",
"description": "A boolean indicating whether the visualization is in pan-and-zoom mode. Initialized from configTogglepanAndZoomMode.initialValue. Updates when the mouseWheel-granularity-interactive-rect is clicked (toggling its value) or when the Ctrl key is pressed.",
"init": "configTogglepanAndZoomMode.initialValue",
"on": [
{
"events": "@mouseWheel-granularity-interactive-rect:pointerdown",
"update": "!panAndZoomMode"
},
{
"events": "window:keydown[event.ctrlKey]{0, 100}",
"update": "panAndZoomMode ? false : true"
}
]
},
{
"name": "verticalScrollbarMouseDown",
"description": "A boolean indicating whether the vertical scrollbar is being clicked. Initialized to false. Updates to true when the group_verticalScrollbar mark is clicked (if panAndZoomMode is off and the scrollbar is enabled). Resets to false on pointer release or when the cursor moves out of the scrollbar.",
"value": false,
"on": [
{
"events": "@group_verticalScrollbar:pointerdown",
"update": "!panAndZoomMode && configVerticalScrollbar.enabled"
},
{
"events": {
"type": "mouseout",
"scope": "view",
"markname": "group_verticalScrollbar",
"filter": ["!event.pointerdown"]
},
"update": "false"
},
{
"events": {
"type": "mouseover",
"scope": "scope",
"markname": "group_verticalScrollbar",
"filter": ["event.pointerdown"]
},
"update": "!panAndZoomMode"
},
{"events": {"type": "pointerup"}, "update": "false"}
]
},
{
"name": "verticalScrollbarMouseOver",
"description": "A boolean indicating whether the vertical scrollbar is being hovered over. Initialized to false. Updates to true when hovered and resets to false when the cursor leaves.",
"value": false,
"on": [
{
"events": "@group_verticalScrollbar:mouseover",
"update": "!panAndZoomMode && configVerticalScrollbar.enabled"
},
{"events": "@group_verticalScrollbar:mouseout", "update": "false"}
]
},
{
"name": "verticalScrollIncrement",
"description": "Defines the vertical scroll step size as 1% of the scrollable height. Updates dynamically based on actualHeight and adjustedHeight.",
"update": "0.01 * (actualHeight-adjustedHeight)/actualHeight"
},
{
"name": "horizontalScrollIncrement",
"description": "Defines the horizontal scroll step size based on the visible domain's span and position. Uses a quadratic formula to ensure a smooth scrolling experience.",
"update": "horizontalPercentageBounds.spanPercentage / 20 * (0.2 + 0.8 * (1 - pow(abs((horizontalScrollPercentage.current - (horizontalPercentageBounds.min + horizontalPercentageBounds.max) / 2)) / ((horizontalPercentageBounds.max - horizontalPercentageBounds.min) / 2), 2)))"
},
{
"name": "xScrollFactor",
"description": "Controls the zoom factor dynamically based on the difference between xDomainInitial and a reference span (180,000,000). Clamped between 0.5 and 2.",
"update": "clamp(2 - (abs(span(xDomainInitial) - 180000000) / 90000000), 0.5, 2)"
},
{
"name": "xScrollDomain",
"description": "Defines the domain for scrolling along the x-axis, extending xDomainInitial symmetrically by xScrollFactor.",
"update": "[xDomainInitial[0] - span(xDomainInitial) * xScrollFactor, xDomainInitial[1] + span(xDomainInitial) * xScrollFactor]"
},
{
"name": "horizontalPercentageBounds",
"description": "Defines percentage-based bounds for horizontal scrolling. Updates when xDomain changes.",
"init": "{min: span(xDomain)/span(domain('xScaleHorizontalScrollMap'))/2, max: 1-span(xDomain)/span(domain('xScaleHorizontalScrollMap'))/2, spanPercentage: span(xDomain)/span(domain('xScaleHorizontalScrollMap'))}",
"on": [
{
"events": {"signal": "xDomain"},
"update": "{min: span(xDomain)/span(domain('xScaleHorizontalScrollMap'))/2, max: 1-span(xDomain)/span(domain('xScaleHorizontalScrollMap'))/2, spanPercentage: span(xDomain)/span(domain('xScaleHorizontalScrollMap'))}"
}
]
},
{
"name": "yPanStart",
"description": "Stores the initial vertical pan position when clicking the background. Updates when clicking rect-gantt-background, clamping the value within scrollable limits.",
"value": null,
"on": [
{
"events": {
"type": "pointerdown",
"source": "scope",
"markname": "rect-gantt-background"
},
"update": "clamp(invert('scaleScrollHandleY', y(group()) + range('scaleScrollHandleY')[0]), 0, (actualHeight-adjustedHeight)/actualHeight)"
}
]
},
{
"name": "yPanPrevious",
"description": "Stores the previous vertical scroll percentage. Updates when clicking rect-gantt-background.",
"value": null,
"on": [
{
"events": {
"type": "pointerdown",
"source": "scope",
"markname": "rect-gantt-background"
},
"update": "verticalScrollPercentage"
}
]
},
{
"name": "verticalScrollVelocity",
"description": "Computes the vertical scroll velocity based on pointer movement between pointerdown and pointerup. Uses an easing formula to maintain smooth scrolling.",
"value": 0,
"on": [
{
"events": {
"type": "pointermove",
"source": "scope",
"consume": true,
"markname": "rect-gantt-background",
"between": [{"type": "pointerdown"}, {"type": "pointerup"}]
},
"update": "(clamp(yPanPrevious + ((invert('scaleScrollHandleY', y(group())) - yPanStart)), 0, (actualHeight-adjustedHeight)/actualHeight) - verticalScrollPercentage) * 0.2 + verticalScrollVelocity * 0.8"
}
]
},
{
"name": "verticalScrollPercentage",
"description": "Tracks the current vertical scroll position as a percentage. Updates on: Mouse wheel events (if panAndZoomMode is off); Arrow key presses (ArrowUp or ArrowDown); Dragging interactions within rect-gantt-background; Clicking the vertical scrollbar; When the scrollbar is disabled, resets to 0.",
"value": 0,
"on": [
{
"events": {
"type": "wheel",
"consume": true,
"force": true,
"source": "view",
"filter": ["!event.ctrlKey", "!event.shiftKey"]
},
"update": "panAndZoomMode ? verticalScrollPercentage : clamp(verticalScrollPercentage - (-event.deltaY * pow(4, event.deltaMode) * 0.002 * adjustedHeight / actualHeight), 0, (actualHeight-adjustedHeight)/actualHeight)"
},
{
"events": "window:keydown[event.key === 'ArrowUp' || event.key === 'ArrowDown']",
"update": "clamp(verticalScrollPercentage + verticalScrollIncrement * (event.key === 'ArrowDown' ? 1 : -1), 0, (actualHeight-adjustedHeight)/actualHeight)"
},
{
"events": {
"type": "pointermove",
"source": "scope",
"consume": true,
"markname": "rect-gantt-background",
"throttle": 30,
"between": [{"type": "pointerdown"}, {"type": "pointerup"}]
},
"update": "panAndZoomMode && isValid(yPanStart) && isValid(y(group())) ? clamp(verticalScrollPercentage + (yPanPrevious + (invert('scaleScrollHandleY', y(group()) + range('scaleScrollHandleY')[0]) - yPanStart) - verticalScrollPercentage) * 0.35, 0, (actualHeight - adjustedHeight) / actualHeight) : verticalScrollPercentage"
},
{
"events": "@group_verticalScrollbar:pointerdown",
"update": "panAndZoomMode ? verticalScrollPercentage : !configVerticalScrollbar.enabled ? 0 : clamp(verticalScrollPercentage - (-event.deltaY * pow(4, event.deltaMode) * 0.001), 0, (actualHeight-adjustedHeight)/actualHeight)"
},
{
"events": {
"type": "pointermove",
"source": "scope",
"markname": "group_verticalScrollbar",
"between": [{"type": "pointerdown"}, {"type": "pointerup"}]
},
"update": "panAndZoomMode ? verticalScrollPercentage : !configVerticalScrollbar.enabled ? 0 : invert('scaleScrollHandleY', y(group()))"
},
{
"events": {
"type": "pointermove",
"source": "scope",
"markname": "group_verticalScrollbar",
"between": [{"type": "pointerdown"}, {"type": "mouseout"}]
},
"update": "panAndZoomMode ? verticalScrollPercentage : !configVerticalScrollbar.enabled ? 0 : isValid(group()) ? invert('scaleScrollHandleY', y(group())) : verticalScrollPercentage"
},
{
"events": {"signal": "!configVerticalScrollbar.enabled"},
"update": "0"
}
]
},
{
"name": "xPanStart",
"description": "Stores the initial x-pan position when clicking rect-gantt-background. Updates only when panAndZoomMode is enabled.",
"value": null,
"on": [
{
"events": {
"type": "pointerdown",
"source": "scope",
"markname": "rect-gantt-background"
},
"update": "panAndZoomMode ? invert('x', x() - columnsWidth) : xPanStart"
}
]
},
{
"name": "xPanPrevious",
"description": "Stores the previous horizontal scroll percentage when clicking rect-gantt-background.",
"value": null,
"on": [
{
"events": {
"type": "pointerdown",
"source": "scope",
"markname": "rect-gantt-background"
},
"update": "horizontalScrollPercentage.current"
}
]
},
{
"name": "horizontalScrollVelocity",
"description": "Computes horizontal scroll velocity based on pointer movement while dragging. Uses easing to maintain smooth scrolling.",
"value": 0,
"on": [
{
"events": {
"type": "pointermove",
"source": "scope",
"consume": true,
"markname": "rect-gantt-background",
"between": [{"type": "pointerdown"}, {"type": "pointerup"}]
},
"update": "!panAndZoomMode ? horizontalScrollVelocity : (clamp(xPanPrevious + ((invert('xScaleHorizontalScrollMap', x()-columnsWidth) - xPanStart) / span(xScrollDomain) * (span(xDomainPreliminary) / span(xDomainInitial))), horizontalPercentageBounds.min, horizontalPercentageBounds.max) - horizontalScrollPercentage.current) * (0.1 * pow(span(domain('xScaleHorizontalScrollMap')) / span(xDomain), 0.5)) + ((horizontalScrollVelocity > 0 && (invert('xScaleHorizontalScrollMap', x()-columnsWidth) - xPanStart) < 0) || (horizontalScrollVelocity < 0 && (invert('xScaleHorizontalScrollMap', x()-columnsWidth) - xPanStart) > 0) ? horizontalScrollVelocity * 0.25 : horizontalScrollVelocity * 0.8)"
}
]
},
{
"name": "horizontalScrollPercentage",
"description": "Tracks the current horizontal scroll percentage. Updates on: Pointer drag events; Clicking the horizontal scroll map; Mouse wheel scrolling (if Shift is not pressed); Arrow key presses (ArrowLeft or ArrowRight); Reset animation triggers.",
"init": "{current: 0.5, previous: 0.5, debug: 'start', delta: 0}",
"on": [
{
"events": {
"type": "pointermove",
"source": "scope",
"consume": true,
"markname": "rect-gantt-background",
"throttle": 40,
"between": [{"type": "pointerdown"}, {"type": "pointerup"}]
},
"update": "!panAndZoomMode ? horizontalScrollPercentage : {current: clamp(horizontalScrollPercentage.current + horizontalScrollVelocity * 0.5 * (span(xDomainPreliminary) / span(xDomainInitial)), horizontalPercentageBounds.min, horizontalPercentageBounds.max), previous: horizontalScrollPercentage.current, delta: horizontalScrollVelocity > 0 ? 1 : (horizontalScrollVelocity < 0 ? -1 : 0), type: 'velocity'}"
},
{
"events": {
"type": "pointermove",
"source": "scope",
"consume": true,
"markname": "gantt-horizontal-scroll-map-interactive-rect",
"between": [{"type": "pointerdown"}, {"type": "pointerup"}]
},
"update": "{current: clamp((invert('xScaleHorizontalScrollMap', x(group()-60)-columnsWidth) - xScrollDomain[0]) / span(xScrollDomain), horizontalPercentageBounds.min, horizontalPercentageBounds.max), previous: horizontalScrollPercentage.current, delta: (invert('xScaleHorizontalScrollMap', x(group())-columnsWidth) - xScrollDomain[0]) > (horizontalScrollPercentage.current * span(xScrollDomain) + xScrollDomain[0]) ? 1 : -1, directionChange: ((invert('xScaleHorizontalScrollMap', x(group())-columnsWidth) - xScrollDomain[0]) > (horizontalScrollPercentage.current * span(xScrollDomain) + xScrollDomain[0]) ? 1 : -1) !== horizontalScrollPercentage.delta, type: 'pointer'}"
},
{
"events": {
"type": "pointerdown",
"source": "scope",
"consume": true,
"markname": "gantt-horizontal-scroll-map-interactive-rect"
},
"update": "{current: clamp((invert('xScaleHorizontalScrollMap', x(group()-60)-columnsWidth) - xScrollDomain[0]) / span(xScrollDomain), horizontalPercentageBounds.min, horizontalPercentageBounds.max), previous: horizontalScrollPercentage.current, delta: (invert('xScaleHorizontalScrollMap', x(group())-columnsWidth) - xScrollDomain[0]) > (horizontalScrollPercentage.current * span(xScrollDomain) + xScrollDomain[0]) ? 1 : -1, directionChange: ((invert('xScaleHorizontalScrollMap', x(group())-columnsWidth) - xScrollDomain[0]) > (horizontalScrollPercentage.current * span(xScrollDomain) + xScrollDomain[0]) ? 1 : -1) !== horizontalScrollPercentage.delta, type: 'pointer'}"
},
{
"events": {"type": "pointerup"},
"update": "horizontalScrollPercentage"
},
{
"events": "wheel![event.shiftKey]",
"force": true,
"update": "{current: clamp(horizontalScrollPercentage.current + horizontalScrollIncrement * (event.deltaY < 0 ? -1 : 1), horizontalPercentageBounds.min, horizontalPercentageBounds.max), previous: horizontalScrollPercentage.current, delta: event.deltaY !== 0 ? (event.deltaY < 0 ? -1 : 1) : horizontalScrollPercentage.delta, directionChange: (event.deltaY !== 0 ? (event.deltaY < 0 ? -1 : 1) : horizontalScrollPercentage.delta) !== horizontalScrollPercentage.delta, type: 'mousewheel'}"
},
{
"events": "window:keydown[event.key === 'ArrowLeft' || event.key === 'ArrowRight']",
"update": "{current: clamp(horizontalScrollPercentage.current + horizontalScrollIncrement * (event.key === 'ArrowLeft' ? -1 : 1), horizontalPercentageBounds.min, horizontalPercentageBounds.max), previous: horizontalScrollPercentage.current, delta: event.key === 'ArrowLeft' ? -1 : 1, type: 'arrowkey'}"
},
{
"events": {"signal": "resetAnimationBounds"},
"update": "!isValid(resetAnimationBounds) || !isValid(horizontalScrollPercentage.delta) ? horizontalScrollPercentage : horizontalScrollPercentage.delta === -1 ? {current: 0.49, previous: 0.51} : {current: 0.51, previous: 0.49}"
}
]
},
{
"name": "ganttHorizontalScrollMapMouseDown",
"description": "A boolean indicating whether the horizontal scroll map is being clicked. Updates on pointer down and resets on pointer up.",
"value": false,
"on": [
{
"events": "@gantt-horizontal-scroll-map-interactive-rect:pointerdown",
"update": "true"
},
{"events": {"type": "pointerup"}, "update": "false"}
]
},
{
"name": "ganttHorizontalScrollMapMouseOver",
"description": "A boolean indicating whether the horizontal scroll map is being hovered over. Updates when hovered and resets when the cursor leaves.",
"value": false,
"on": [
{
"events": "@gantt-horizontal-scroll-map-interactive-rect:mouseover",
"update": "true"
},
{
"events": "@gantt-horizontal-scroll-map-interactive-rect:mouseout",
"update": "false"
}
]
},
{
"name": "todayDate",
"description": "Stores the UTC date for today.",
"update": "utc(year(now()),month(now()),date(now()))"
},
{
"name": "xDomainInitial",
"description": "Stores the initial x-axis domain derived from the dataset.",
"update": "data('xDomainInitial')[0]['domain']"
},
{
"name": "initialDomainCenter",
"description": "Computes the center of xDomainInitial.",
"update": "xDomainInitial[0] + span(xDomainInitial) / 2"
},
{
"name": "currentDomainCenter",
"description": "Tracks the center of the current xDomain, updating whenever xDomain changes.",
"init": "initialDomainCenter",
"on": [
{
"events": {"signal": "xDomain"},
"update": "xDomain[0] + span(xDomain) / 2"
}
]
},
{
"name": "xDomainBounds",
"description": "Defines the minimum and maximum width of xDomain.",
"update": "{min: 180000000, max: round((width/0.075)* scale('dateUnitIncrementMS', 'day'))}"
},
{
"name": "xDomainPreliminary",
"description": "Stores a preliminary xDomain used during zooming and scrolling. Updates on zoom changes, granularity percentage updates, or horizontal scrolling.",
"init": "xDomainInitial",
"on": [
{
"events": {"signal": "mousewheelGranularityZoom"},
"update": "wheelDelta === 0 || resetAnimationTEased !== 1 ? xDomainPreliminary : [currentDomainCenter + (xDomain[0] - currentDomainCenter) * mousewheelGranularityZoom, currentDomainCenter + (xDomain[1] - currentDomainCenter) * mousewheelGranularityZoom]"
},
{
"events": {"signal": "granularityPercentage"},
"update": "[round(currentDomainCenter - (granularityPercentage*span(domain('xScaleHorizontalScrollMap')))/2), round(currentDomainCenter + (granularityPercentage*span(domain('xScaleHorizontalScrollMap')))/2)]"
},
{
"events": {"signal": "horizontalScrollPercentage.current"},
"update": "[scale('scaleXScroll', horizontalScrollPercentage.current) - span(xDomain)/2, scale('scaleXScroll', horizontalScrollPercentage.current) + span(xDomain)/2]"
},
{
"events": {"signal": "resetAnimationBounds"},
"update": "!isValid(resetAnimationBounds) ? xDomainPreliminary : [lerp([resetAnimationBounds.xDomain[0], xDomainInitial[0]], resetAnimationTEased), lerp([resetAnimationBounds.xDomain[1], xDomainInitial[1]], resetAnimationTEased)]"
}
]
},
{
"name": "xDomain",
"description": "Stores the final x-axis domain, ensuring it stays within valid bounds.",
"init": "xDomainInitial",
"on": [
{
"events": {"signal": "xDomainPreliminary"},
"update": "span(xDomainPreliminary)<xDomainBounds.min ? [currentDomainCenter - xDomainBounds.min/2, currentDomainCenter + xDomainBounds.min/2] : [max(xDomainPreliminary[0], xScrollDomain[0]), min(xDomainPreliminary[1], xScrollDomain[1])]"
}
]
},
{
"name": "minDateBandwidth",
"description": "Defines a minimum pixel bandwidth for date-based scaling.",
"value": 20
},
{
"name": "currentXBandwidth",
"description": "Computes the current bandwidth of x-axis units based on the scale.",
"update": "(round((scale('x', timeOffset('hours', utchours(0,0,0,0),1)) - scale('x', utchours(0,0,0,0))) * 100)/100) * (ganttWidth < 358 ? 4.5 : ganttWidth < 593 ? 2.5 : ganttWidth < 889 ? 1.5 : 1)"
},
{
"name": "thresholdMinuteBandwidth",
"description": "Static threshold that when greater than or equal to currentXBandwidth, indicates that the current unit is 'hour'.",
"update": "17.78"
},
{
"name": "thresholdHourBandwidth",
"description": "Static threshold that when greater than or equal to currentXBandwidth, indicates that the current unit is 'hour'.",
"update": "0.6"
},
{
"name": "thresholdDayBandwidth",
"description": "Static threshold that when greater than or equal to currentXBandwidth, indicates that the current unit is 'day'.",
"update": "0.26"
},
{
"name": "thresholdMonthBandwidth",
"description": "Static threshold that when greater than or equal to currentXBandwidth, indicates that the current unit is 'month'.",
"update": "0.04"
},
{
"name": "thresholdYearBandwidth",
"description": "Static threshold that when greater than or equal to currentXBandwidth, indicates that the current unit is 'year'.",
"update": "0.03"
},
{
"name": "xCurrentUnit",
"description": "Determines the time unit displayed based on currentXBandwidth compared to the thresholds",
"update": "(currentXBandwidth>=thresholdMinuteBandwidth ? 'hour': currentXBandwidth>=thresholdHourBandwidth ? 'day' : currentXBandwidth >= thresholdDayBandwidth ? 'month' : currentXBandwidth >= thresholdMonthBandwidth ? 'month' : 'year')"
},
{
"name": "xCurrentDateFormat",
"description": "Defines the date format used for axis labels based on xCurrentUnit",
"update": "['%H', '%d', currentXBandwidth >= 0.12 ? '%B %Y' : '%b', '%Y'][indexof(['hour', 'day', 'month', 'year'],xCurrentUnit)]"
},
{
"name": "xCurrentDateLabelDX",
"description": "Computes axis label positioning adjustments for x-axis labels.",
"update": "xCurrentUnit === 'hour' ? ((scale('x', utchours(0,0,0,0))-scale('x', timeOffset('hours', utchours(0,0,0,0), -1)))/2) : (scale('x', now())-scale('x', timeOffset(xCurrentUnit, now(), -1)))/2"
},
{
"name": "xCurrentUnitOffset",
"description": "Determines the offset unit used for secondary axis labels.",
"update": "xCurrentUnit === 'year' ? null : ['hour', 'day', 'month', 'year'][indexof(['hour','day', 'month'], xCurrentUnit)+1]"
},
{
"name": "xCurrentDateFormatOffset",
"description": "Defines the date format for used for secondary axis labels",
"update": "['%H 🕐', '%d %b %Y 🕐', '%B %Y', '%Y'][indexof(['hour', 'day', 'month', 'year'],xCurrentUnitOffset)]"
},
{
"name": "xCurrentDateOffsetLabelDX",
"description": "Computes positioning adjustments for offset labels.",
"update": "xCurrentUnitOffset === 'hour' ? ((scale('x', utchours(0,0,0,0))-scale('x', timeOffset('hours', utchours(0,0,0,0), -1)))/2) : (scale('x', now())-scale('x', timeOffset(xCurrentUnitOffset, now(), -1)))/2"
},
{
"name": "wheelDelta",
"description": "Computes the zoom factor based on the mouse wheel delta, adjusting dynamically.",
"value": 0,
"on": [
{
"events": "wheel!",
"force": true,
"update": "!isValid(resetAnimationBounds) && panAndZoomMode && x() > columnsWidth ? pow(1.001 + 0.0005 * log(1 + span(xScrollDomain) / span(xDomain)), (event.deltaY) * pow(8, event.deltaMode)) : 0"
}
]
},
{
"name": "mousewheelGranularityZoom",
"description": "Stores the current zoom level. Updates based on mouse wheel events if panAndZoomMode is enabled.",
"value": 1,
"on": [
{
"events": "wheel![!event.shiftKey]",
"force": true,
"update": "!panAndZoomMode || wheelDelta === 0 ? mousewheelGranularityZoom : wheelDelta"
}
]
},
{
"name": "granularityPercentage",
"description": "Stores the current position of the granularity slider, updating as the slider moves.",
"on": [
{
"events": {
"type": "pointermove",
"source": "scope",
"markname": "dateGranularitySliderInteractive_rect",
"between": [{"type": "pointerdown"}, {"type": "pointerup"}]
},
"update": "1-((x(group())-columnsWidth-event.item.mark.group.bounds.x1+datum.bounds.x1)/configDateStepSlider.track.width)"
},
{
"events": {
"type": "pointerdown",
"source": "scope",
"markname": "dateGranularitySliderInteractive_rect"
},
"update": "1-((x(group())-columnsWidth-event.item.mark.group.bounds.x1+datum.bounds.x1)/configDateStepSlider.track.width)"
}
]
},
{
"name": "resetAnimationBounds",
"init": "null",
"on": [
{
"events": "@granularity-reset-interactive-rect:click",
"update": "isValid(resetAnimationBounds) ? resetAnimationBounds : {start: now(), end: now()+resetAnimationDuration, t: 0, xDomain: isValid(resetAnimationBounds) ? null : xDomain}"
},
{
"events": {"signal": "timer"},
"update": "!isValid(resetAnimationBounds) ? null : now() > resetAnimationBounds.end ? null : {start: resetAnimationBounds.start, end: resetAnimationBounds.end, t: (now()-resetAnimationBounds.start)/resetAnimationDuration, xDomain: isValid(resetAnimationBounds) ? resetAnimationBounds.xDomain : xDomain}"
}
]
},
{
"name": "resetAnimationTEased",
"value": 1,
"on": [
{
"events": {"signal": "resetAnimationBounds"},
"update": "!isValid(resetAnimationBounds) ? 1 : resetAnimationBounds.t < 0.5 ? 4 * pow(resetAnimationBounds.t, 3) : 1 - pow(-2 * resetAnimationBounds.t + 2, 3) / 2"
}
]
},
{
"name": "resetAnimationDuration",
"value": 750,
"on": [
{
"events": "@granularity-reset-interactive-rect:click",
"update": "clamp(100 + pow(abs(span(xDomain) - span(xDomainInitial)) / span(xDomainInitial), 0.5) * 200 + pow(abs(horizontalScrollPercentage.current - horizontalScrollPercentage.previous), 0.5) * 2000, 750, 2000)"
}
]
},
{
"name": "granularityResetMouseover",
"description": "A boolean indicating whether the reset granularity button is being hovered over.",
"value": false,
"on": [
{
"events": "@granularity-reset-interactive-rect:mouseover",
"update": "true"
},
{
"events": "@granularity-reset-interactive-rect:mouseout",
"update": "false"
}
]
},
{
"name": "dateGranularitySliderMouseDown",
"description": "A boolean indicating whether the date granularity slider is being clicked. Updates on pointer down and resets on pointer up.",
"value": false,
"on": [
{
"events": "@dateGranularitySliderInteractive_rect:pointerdown",
"update": "true"
},
{"events": "pointerup", "update": "false"}
]
},
{
"name": "padding",
"description": "Defines the padding values based on different interaction states.",
"update": "{'top': 5, 'right': panAndZoomMode ? 5 : 5, 'bottom': ganttHorizontalScrollMapMouseDown ? 5 : 5, 'left': 5}"
},
{
"name": "horizontalScrollMapHeight",
"description": "Defines the height of the horizontal scroll map.",
"value": 10
},
{
"name": "timer",
"description": "Stores the current time, updating continuously.",
"init": "now()",
"on": [{"events": "timer{0}", "update": "now()"}]
},
{
"name": "cursor",
"description": "Defines the cursor style based on interaction mode. Updates on pointer movement or reset on pointer up.",
"value": "default",
"on": [
{
"events": {
"type": "pointermove",
"source": "scope",
"consume": true,
"markname": "rect-gantt-background",
"between": [{"type": "pointerdown"}, {"type": "pointerup"}]
},
"update": "panAndZoomMode ? 'move' : 'default'"
},
{"events": "window:pointerup", "update": "'default'"}
]
},
{
"name": "height",
"update": "min(data('height')[0].height, adjustedHeight)"
}
],
"marks": [
{
"name": "group-header",
"type": "group",
"encode": {
"update": {
"x": {"value": 0},
"width": {"signal": "width"},
"height": {"signal": "20"},
"y": {"value": -50}
}
},
"marks": [
{
"name": "group_panAndZoomMode",
"description": "group for the marks that make up the toggle control to hide/show details",
"type": "group",
"interactive": false,
"clip": false,
"encode": {
"update": {
"y": {"value": 0},
"x": {"signal": "columnsWidth+ganttWidth"}
}
},
"marks": [
{
"name": "panAndZoomMode_text_zoom",
"description": "the title for the toggle control",
"type": "text",
"interactive": false,
"encode": {
"update": {
"x": {"signal": "0"},
"y": {"signal": "configTogglepanAndZoomMode.track.height/2"},
"text": {"signal": "'Pan & Zoom Mode'"},
"fontWeight": {
"signal": "panAndZoomMode ? 'bold' : 'regular'"
},
"baseline": {"value": "top"},
"font": {"signal": "configTogglepanAndZoomMode.label.font"},
"fontSize": {
"signal": "configTogglepanAndZoomMode.label.fontSize"
},
"fontStyle": {
"signal": "configTogglepanAndZoomMode.label.fontStyle"
},
"align": {"value": "right"},
"fill": {"signal": "configTogglepanAndZoomMode.label.fill"}
}
}
},
{
"name": "track_rect",
"description": "the track for the toggle control",
"type": "rect",
"from": {"data": "panAndZoomMode_text_zoom"},
"interactive": false,
"encode": {
"update": {
"y": {"signal": "(datum.bounds.y2-datum.bounds.y1)/2"},
"height": {
"signal": "configTogglepanAndZoomMode.track.height"
},
"x": {
"signal": "datum.bounds.x1-configTogglepanAndZoomMode.track.width-18"
},
"width": {"signal": "configTogglepanAndZoomMode.track.width"},
"cornerRadius": {
"signal": "configTogglepanAndZoomMode.track.cornerRadius"
},
"fill": {
"signal": "panAndZoomMode ? configTogglepanAndZoomMode.on.fill : configTogglepanAndZoomMode.track.fill"
},
"stroke": {
"signal": "configTogglepanAndZoomMode.track.stroke"
},
"strokeWidth": {
"signal": "configTogglepanAndZoomMode.track.strokeWidth"
}
}
}
},
{
"name": "toggle_outer_arc",
"description": "the circle mark that serves as the the 'toggle handle'",
"from": {"data": "track_rect"},
"type": "arc",
"interactive": false,
"encode": {
"enter": {
"y": {
"signal": "datum.bounds.y1+configTogglepanAndZoomMode.track.height/2"
},
"innerRadius": {"value": 0},
"outerRadius": {
"signal": "configTogglepanAndZoomMode.track.height*0.9"
},
"startAngle": {"signal": "0"},
"endAngle": {"signal": "2*PI"},
"stroke": {
"signal": "configTogglepanAndZoomMode.handle.stroke || '#BBB'"
},
"strokeWidth": {
"signal": "configTogglepanAndZoomMode.handle.strokeWidth"
},
"fill": {
"signal": "configTogglepanAndZoomMode.handle.fill || '#fff'"
}
},
"update": {
"x": {
"signal": "panAndZoomMode ? datum.bounds.x2-configTogglepanAndZoomMode.track.height*0.9 : datum.bounds.x1+configTogglepanAndZoomMode.track.height*0.9"
}
}
}
},
{
"name": "panAndZoomMode_text_scroll",
"description": "the title for the toggle control",
"type": "text",
"from": {"data": "track_rect"},
"interactive": false,
"encode": {
"update": {
"x": {"signal": "datum.bounds.x1"},
"dx": {"signal": "-configTogglepanAndZoomMode.label.dx"},
"y": {
"signal": "datum.bounds.y1+(datum.bounds.y2-datum.bounds.y1)/2"
},
"text": {"signal": "'Scroll Mode'"},
"fontWeight": {
"signal": "panAndZoomMode ? 'regular' : 'bold'"
},
"baseline": {"value": "middle"},
"font": {"signal": "configTogglepanAndZoomMode.label.font"},
"fontSize": {
"signal": "configTogglepanAndZoomMode.label.fontSize"
},
"fontStyle": {
"signal": "configTogglepanAndZoomMode.label.fontStyle"
},
"align": {"value": "right"},
"fill": {"signal": "configTogglepanAndZoomMode.label.fill"}
}
}
}
]
},
{
"name": "mouseWheel-granularity-interactive-rect",
"type": "rect",
"from": {"data": "group_panAndZoomMode"},
"interactive": true,
"encode": {
"update": {
"y": {"signal": "datum.bounds.y1"},
"y2": {"signal": "datum.bounds.y2"},
"x": {"signal": "datum.bounds.x1-5"},
"x2": {"signal": "datum.bounds.x2"},
"fill": {"value": "#fff"},
"fillOpacity": {"value": 0.15},
"tooltip": {
"signal": "configTogglepanAndZoomMode.tooltip.object"
},
"cursor": {"value": "pointer"}
},
"hover": {"fill": {"value": "transparent"}}
}
}
]
},
{
"name": "rect-background",
"type": "rect",
"encode": {
"update": {
"width": {"signal": "columnsWidth+ganttWidth-1"},
"height": {"signal": "adjustedHeight"},
"fill": {"value": "transparent"},
"stroke": {"value": "#AAA"},
"strokeWidth": {"value": 0.1}
}
}
},
{
"name": "rect-gantt-background",
"type": "rect",
"encode": {
"update": {
"x": {"signal": "columnsWidth"},
"width": {"signal": "ganttWidth-1"},
"height": {"signal": "adjustedHeight"},
"fill": {"value": "transparent"},
"stroke": {"value": "#AAA"},
"strokeWidth": {"value": 0.1}
}
}
},
{
"name": "group-gantt",
"type": "group",
"clip": true,
"encode": {
"update": {
"x": {"signal": "columnsWidth"},
"y": {
"signal": "actualHeight > adjustedHeight ? clamp(-verticalScrollPercentage*actualHeight,-(actualHeight-adjustedHeight), 0) : 0"
},
"width": {"signal": "ganttWidth"},
"height": {"signal": "1000000000"},
"clip": {"value": true}
}
},
"marks": [
{
"name": "rect-weekends",
"type": "rect",
"from": {"data": "weekendDates"},
"encode": {
"update": {
"x": {"scale": "x", "field": "startDate"},
"x2": {"scale": "x", "field": "endDate"},
"height": {"signal": "1000000000"},
"fill": {"value": "#fbfcfd"},
"opacity": {
"signal": "indexof(['%B %Y', '%d', '%H'], xCurrentDateFormat) >= 0 ? 1 : 0"
}
}
}
},
{
"name": "gantt_node_marks_group",
"description": "The marks that make up the bars Gantt's bars",
"type": "group",
"marks": [
{
"name": "row_clickable_rect",
"description": "the invisible rect used for interactions for each row that spans across the visual horizontally",
"type": "rect",
"from": {"data": "hierarchy_master_buffered"},
"interactive": true,
"encode": {
"update": {
"tooltip": {"signal": "datum.id"},
"x": {"field": "x"},
"x2": {"field": "x2"},
"y": {"field": "y"},
"height": {"field": "height"},
"cursor": {
"signal": "datum.hasChildren ? 'pointer' : 'default'"
},
"fill": {"value": "transparent"},
"zindex": {"signal": "-datum.rowNumberAll"}
}
}
},
{
"name": "gantt_parent_rect_start_marker",
"description": "the vertical line that appears at the start of parent node rects",
"type": "rect",
"from": {"data": "hierarchy_master_buffered"},
"interactive": false,
"encode": {
"update": {
"x": {"field": "x", "offset": 1},
"x2": {"field": "x", "offset": 2},
"y": {"field": "yWithPadding", "offset": -0.5},
"height": {"signal": "datum.height*0.5"},
"fill": {"signal": "datum.color"},
"opacity": {
"signal": "datum.shapeType === 'parentRect' && inrange(datum.startDate, domain('x')) ? (datum.decimalPercentComplete > 0 ? 1 : 0.35) : 0"
},
"fillOpacity": {
"signal": "isValid(mouseoverRectDatum) && mouseoverRectDatum.id === datum.id ? 1 : 0.5"
},
"zindex": {"signal": "-datum.rowNumberAll"}
}
}
},
{
"name": "gantt_parent_rect_end_marker",
"description": "the vertical line that appears at the end of parent node rects",
"type": "rect",
"from": {"data": "hierarchy_master_buffered"},
"interactive": false,
"encode": {
"update": {
"x": {"field": "x2", "offset": -1.5},
"x2": {"field": "x2", "offset": -0.5},
"y": {"field": "yWithPadding", "offset": -0.5},
"height": {"signal": "datum.height*0.5"},
"fill": {"signal": "datum.color"},
"opacity": {
"signal": "datum.shapeType === 'parentRect' && inrange(datum.endDate, domain('x')) ? (datum.decimalPercentComplete === 1 ? 1 : 0.35) : 0"
},
"fillOpacity": {
"signal": "isValid(mouseoverRectDatum) && mouseoverRectDatum.id === datum.id ? 1 : 0.5"
},
"zindex": {"signal": "-datum.rowNumberAll"}
}
}
},
{
"name": "gantt_rect_container",
"type": "rect",
"description": "semi-opaque gantt rect that will act as the background",
"from": {"data": "hierarchy_master_buffered"},
"interactive": false,
"encode": {
"update": {
"x": {"field": "x", "offset": 2},
"x2": {"field": "x2", "offset": -2},
"y": {"field": "yWithPadding"},
"height": {"field": "heightWithPadding"},
"cornerRadius": {
"signal": "datum.shapeType === 'parentRect' ? 0 : configGantt.childRect.cornerRadius"
},
"fill": {"signal": "datum.color"},
"fillOpacity": {"value": 0.35},
"stroke": {"signal": "datum.color"},
"strokeWidth": {"value": 1.5},
"strokeOpacity": {
"signal": "isValid(mouseoverRectDatum) && mouseoverRectDatum.id === datum.id ? 1 : 0.35"
},
"opacity": {
"signal": "datum.shapeType === 'parentRect' || datum.shapeType === 'childRect' ? 1 : 0"
},
"zindex": {"signal": "-datum.rowNumberAll"}
}
}
},
{
"name": "gantt_rect",
"type": "rect",
"description": "the rect that indicates percent complete",
"from": {"data": "gantt_rect_container"},
"interactive": false,
"encode": {
"update": {
"x": {"signal": "datum.x"},
"x2": {
"signal": "datum.datum.decimalPercentComplete === 1 ? datum.x2 : datum.datum.x2Progress"
},
"y": {"signal": "datum.datum.yWithPadding"},
"height": {"signal": "datum.datum.heightWithPadding"},
"cornerRadiusTopLeft": {
"signal": "datum.datum.shapeType === 'parentRect' ? 0 : configGantt.childRect.cornerRadius"
},
"cornerRadiusBottomLeft": {
"signal": "datum.datum.shapeType === 'parentRect' ? 0 : configGantt.childRect.cornerRadius"
},
"cornerRadiusTopRight": {
"signal": "datum.datum.shapeType === 'parentRect' ? 0 : (datum.datum.decimalPercentComplete === 1 ? configGantt.childRect.cornerRadius : 0)"
},
"cornerRadiusBottomRight": {
"signal": "datum.datum.shapeType === 'parentRect' ? 0 :(datum.datum.decimalPercentComplete === 1 ? configGantt.childRect.cornerRadius : 0)"
},
"fill": {"signal": "datum.datum.color"},
"fillOpacity": {
"signal": "isValid(mouseoverRectDatum) && mouseoverRectDatum.id === datum.datum.id ? 1 : 0.35"
},
"opacity": {
"signal": "datum.datum.shapeType === 'parentRect' || datum.datum.shapeType === 'childRect' ? (datum.datum.decimalPercentComplete > 0 && (datum.datum.progressDate > domain('x')[0]) ? 1 : 0) : 0"
},
"stroke": {"signal": "datum.datum.color"},
"strokeOpacity": {
"signal": "isValid(mouseoverRectDatum) && mouseoverRectDatum.id === datum.datum.id ? 1 : 0.35"
},
"zindex": {"signal": "-datum.zindex"}
}
}
},
{
"name": "gantt_milestone_symbol",
"type": "symbol",
"description": "the symbol that represents a milestone",
"from": {"data": "gantt_rect_container"},
"interactive": false,
"encode": {
"update": {
"x": {"signal": "datum.datum.x"},
"y": {"signal": "datum.datum.yWithPadding"},
"shape": {"value": "diamond"},
"fill": {"signal": "datum.datum.color"},
"size": {"signal": "datum.datum.milestoneSize"},
"fillOpacity": {
"signal": "isValid(mouseoverRectDatum) && mouseoverRectDatum.id === datum.datum.id ? 1 : datum.datum.decimalPercentComplete === 1 ? 0.55 : 0.35"
},
"opacity": {
"signal": "datum.datum.shapeType === 'milestone' ? 1 : 0"
},
"stroke": {"signal": "background || '#fff'"},
"strokeOpacity": {"signal": "1"},
"zindex": {"signal": "-datum.zindex"}
}
}
},
{
"name": "gantt_label_text",
"description": "the node's text label",
"type": "text",
"from": {"data": "gantt_rect_container"},
"interactive": false,
"encode": {
"update": {
"text": {"signal": "datum.datum.label"},
"x": {"signal": "datum.bounds.x2"},
"dx": {
"signal": "configGantt.label.xOffset*(datum.datum.shapeType === 'milestone' ? 2.5 : 1)"
},
"y": {
"signal": "datum.datum.y+configRow.rowHeight/2"
},
"font": {"signal": "configGantt.label.font"},
"fontSize": {"signal": "configGantt.label.fontSize"},
"align": {"value": "left"},
"baseline": {"value": "middle"},
"limit": {"signal": "width-(datum.datum.x2+10)"},
"fill": {"signal": "configGantt.label.fill"},
"fontWeight": {
"signal": "isValid(mouseoverRectDatum) && mouseoverRectDatum.id === datum.datum.id ? 600 : configGantt.label.fontWeight"
},
"opacity": {
"signal": "inrange(datum.datum.endDate, domain('x')) ? datum.datum.join === 'update' ? 1 : datum.datum.join === 'enter' ? datum.datum.t > 0.5 ? datum.t : 0 : datum.datum.t < 0.5 ? 1-datum.t : 0 : 0"
},
"zindex": {"signal": "-datum.zindex"}
}
}
}
]
}
]
},
{
"name": "group-gantt-horizontal-scroll-map",
"type": "group",
"encode": {
"update": {
"x": {
"signal": "columnsWidth+ (ganttHorizontalScrollMapMouseDown ? 0 : 0)"
},
"y": {
"signal": "ganttHorizontalScrollMapMouseDown ? 0 : adjustedHeight+5"
}
}
},
"marks": [
{
"name": "track",
"type": "rect",
"encode": {
"update": {
"x": {"signal": "ganttHorizontalScrollMapMouseDown ? 0 : 0"},
"width": {"signal": "range('xScaleHorizontalScrollMap')[1]"},
"y": {
"signal": "(ganttHorizontalScrollMapMouseDown ? adjustedHeight+5 : 0) + horizontalScrollMapHeight/2",
"offset": -0.15
},
"height": {"value": 0.3},
"fill": {"value": "#999"},
"opacity": {
"signal": "scale('xScaleHorizontalScrollMap', xDomain[0]) === range('xScaleHorizontalScrollMap')[0] ? 0 : 1"
}
}
}
},
{
"name": "rect-domain-max-brush",
"type": "rect",
"from": {"data": "track"},
"encode": {
"update": {
"y": {"signal": "datum.bounds.y1 - horizontalScrollMapHeight/2"},
"x": {
"scale": "xScaleHorizontalScrollMap",
"signal": "xDomain[0]",
"offset": {
"signal": "(ganttHorizontalScrollMapMouseDown ? 0 : 0) -0.5"
}
},
"x2": {
"scale": "xScaleHorizontalScrollMap",
"signal": "xDomain[1]",
"offset": {
"signal": "(ganttHorizontalScrollMapMouseDown ? 0 : 0) + 0.5"
}
},
"height": {"signal": "horizontalScrollMapHeight"},
"fill": {
"signal": "ganttHorizontalScrollMapMouseOver ? '#f3f7fa' : '#fff'"
},
"opacity": {
"signal": "scale('xScaleHorizontalScrollMap', xDomain[0]) === range('xScaleHorizontalScrollMap')[0] && scale('xScaleHorizontalScrollMap', xDomain[1]) === range('xScaleHorizontalScrollMap')[1] ? 0 : 1"
}
}
}
},
{
"name": "time-series-rect",
"type": "rect",
"from": {"data": "track"},
"encode": {
"update": {
"x": {
"signal": "datum.bounds.x1+scale('xScaleHorizontalScrollMap', xDomainInitial[0])"
},
"x2": {
"signal": "datum.bounds.x1+scale('xScaleHorizontalScrollMap', xDomainInitial[1])"
},
"y": {
"signal": "datum.bounds.y1-(datum.bounds.y2-datum.bounds.y1)/2"
},
"y2": {
"signal": "datum.bounds.y1+(datum.bounds.y2-datum.bounds.y1)/2"
},
"strokeWidth": {"signal": "horizontalScrollMapHeight/4"},
"stroke": {"value": "#CCC"},
"fill": {"value": "transparent"},
"cursor": {
"signal": "(ganttHorizontalScrollMapMouseOver || ganttHorizontalScrollMapMouseDown) && scale('xScaleHorizontalScrollMap', xDomain[0]) !== range('xScaleHorizontalScrollMap')[0] ? 'pointer' : 'default'"
}
}
}
},
{
"name": "rect-domain-min-tick-tick",
"from": {"data": "rect-domain-max-brush"},
"type": "text",
"encode": {
"update": {
"x": {"signal": "datum.bounds.x1-2"},
"y": {
"signal": "datum.bounds.y1+(datum.bounds.y2-datum.bounds.y1)/2"
},
"text": {"signal": "'|'"},
"baseline": {"value": "middle"},
"fill": {"signal": "'#999'"},
"fontWeight": {"value": "600"},
"fontSize": {"value": 12},
"opacity": {
"signal": "xDomain[0] <= xScrollDomain[0] && xDomain[1] >= xScrollDomain[1] ? 0 : 1"
}
}
}
},
{
"name": "rect-domain-max-tick",
"from": {"data": "rect-domain-max-brush"},
"type": "text",
"encode": {
"update": {
"x": {"signal": "datum.bounds.x2-1"},
"y": {
"signal": "datum.bounds.y1+(datum.bounds.y2-datum.bounds.y1)/2"
},
"text": {"signal": "'|'"},
"baseline": {"value": "middle"},
"fill": {"signal": "'#999'"},
"fontWeight": {"value": "600"},
"fontSize": {"value": 12},
"opacity": {
"signal": "xDomain[0] <= xScrollDomain[0] && xDomain[1] >= xScrollDomain[1] ? 0 : 1"
}
}
}
},
{
"name": "text-date-unit-label",
"type": "text",
"from": {"data": "track"},
"encode": {
"update": {
"x": {"signal": "datum.bounds.x1"},
"y": {"signal": "datum.bounds.y1+15"},
"baseline": {"value": "top"},
"text": {
"signal": "upper(substring(xCurrentUnit, 0, 1))+substring(xCurrentUnit, 1, length(xCurrentUnit))+' View'"
},
"fontSize": {"value": 9},
"fill": {"value": "#666"}
}
}
},
{
"name": "text-date-granularity-reset-label",
"type": "text",
"from": {"data": "text-date-unit-label"},
"encode": {
"update": {
"x": {"signal": "ganttWidth-20"},
"y": {"signal": "datum.bounds.y1"},
"align": {"value": "right"},
"baseline": {"value": "top"},
"text": {"signal": "'Reset'"},
"fontSize": {"signal": "configDateStepSlider.label.fontSize"},
"fill": {"signal": "granularityResetMouseover ? '#222' : '#666'"}
}
}
},
{
"name": "text-date-granularity-reset-icon",
"type": "symbol",
"from": {"data": "text-date-granularity-reset-label"},
"encode": {
"update": {
"x": {"signal": "datum.bounds.x2 + 6.5"},
"y": {"signal": "datum.bounds.y1-2.5"},
"shape": {
"value": "'M75 75L41 41C25.9 25.9 0 36.6 0 57.9L0 168c0 13.3 10.7 24 24 24l110.1 0c21.4 0 32.1-25.9 17-41l-30.8-30.8C155 85.5 203 64 256 64c106 0 192 86 192 192s-86 192-192 192c-40.8 0-78.6-12.7-109.7-34.4c-14.5-10.1-34.4-6.6-44.6 7.9s-6.6 34.4 7.9 44.6C151.2 495 201.7 512 256 512c141.4 0 256-114.6 256-256S397.4 0 256 0C185.3 0 121.3 28.7 75 75zm181 53c-13.3 0-24 10.7-24 24l0 104c0 6.4 2.5 12.5 7 17l72 72c9.4 9.4 24.6 9.4 33.9 0s9.4-24.6 0-33.9l-65-65 0-94.1c0-13.3-10.7-24-24-24z'"
},
"size": {"signal": "0.0025"},
"fill": {"signal": "granularityResetMouseover ? '#333' : '#888'"}
}
}
},
{
"name": "group_dateGranularitySlider",
"description": "the group of marks that makes up the slider control",
"type": "group",
"from": {"data": "text-date-granularity-reset-label"},
"interactive": false,
"clip": false,
"encode": {
"update": {
"y": {"signal": "datum.bounds.y1"},
"height": {"signal": "configDateStepSlider.track.height"},
"x": {
"signal": "datum.bounds.x1-configDateStepSlider.track.cornerRadius*2"
},
"x2": {
"signal": "datum.bounds.x1-configDateStepSlider.track.width-configDateStepSlider.track.cornerRadius*2"
}
}
},
"marks": [
{
"name": "labelDateGranularitySlider_text",
"description": "the title for the slider control",
"type": "text",
"interactive": false,
"encode": {
"update": {
"y": {"signal": "configDateStepSlider.track.height/2"},
"text": {"signal": "configDateStepSlider.label.text || ''"},
"baseline": {"value": "middle"},
"font": {"signal": "configDateStepSlider.label.font"},
"fontSize": {"signal": "configDateStepSlider.label.fontSize"},
"fontStyle": {
"signal": "configDateStepSlider.label.fontStyle"
},
"align": {"value": "right"},
"dx": {
"signal": "-configDateStepSlider.track.cornerRadius*2"
},
"fill": {"signal": "configDateStepSlider.label.fill"}
}
}
},
{
"name": "track_rect",
"description": "the track for the slider control",
"type": "rect",
"interactive": false,
"encode": {
"update": {
"y": {"signal": "0"},
"height": {"signal": "configDateStepSlider.track.height"},
"x": {"signal": "0"},
"x2": {"signal": "configDateStepSlider.track.width"},
"cornerRadius": {
"signal": "configDateStepSlider.track.cornerRadius"
},
"fill": {"signal": "configDateStepSlider.track.fill"},
"stroke": {"signal": "configDateStepSlider.track.stroke"},
"strokeWidth": {
"signal": "configDateStepSlider.track.strokeWidth"
}
}
}
},
{
"name": "sliderPercentage_rect",
"description": "the rect that indicates the slider percentage",
"type": "rect",
"from": {"data": "labelDateGranularitySlider_text"},
"interactive": false,
"encode": {
"update": {
"height": {"signal": "configDateStepSlider.track.height"},
"x": {"value": 0},
"x2": {
"signal": "scale('xSliderGranularityPercentage', span(xDomain)/span(domain('xScaleHorizontalScrollMap')))"
},
"cornerRadius": {
"signal": "configDateStepSlider.track.cornerRadius"
},
"fill": {"signal": "configDateStepSlider.progress.fill"},
"fillOpacity": {
"signal": "configDateStepSlider.progress.fillOpacity"
},
"stroke": {"signal": "configDateStepSlider.track.stroke"},
"strokeWidth": {
"signal": "configDateStepSlider.track.strokeWidth"
}
}
}
},
{
"name": "dateGranularitySliderInteractive_rect",
"description": "the invisible rect that is used for capturing events related to the control",
"from": {"data": "labelDateGranularitySlider_text"},
"type": "rect",
"interactive": true,
"encode": {
"update": {
"y": {
"signal": "dateGranularitySliderMouseDown ? -configDateStepSlider.track.height*6 : 0"
},
"x": {
"signal": "dateGranularitySliderMouseDown ? -configDateStepSlider.track.width/4 : 0"
},
"width": {
"signal": "configDateStepSlider.track.width + (dateGranularitySliderMouseDown ? configDateStepSlider.track.width/2 : 0)"
},
"height": {
"signal": "dateGranularitySliderMouseDown ? configDateStepSlider.track.height*10 : configDateStepSlider.track.height"
},
"fill": {"value": "transparent"},
"cursor": {"value": "pointer"},
"tooltip": {
"signal": "dateGranularitySliderMouseDown ? '' : configDateStepSlider.tooltip.text"
}
}
}
},
{
"name": "handles_outer_arc",
"description": "the outer circle mark that serves as the the 'slider handle'",
"from": {"data": "labelDateGranularitySlider_text"},
"type": "arc",
"interactive": false,
"encode": {
"enter": {
"y": {"signal": "configDateStepSlider.track.height/2"},
"innerRadius": {"value": 0},
"outerRadius": {
"signal": "configDateStepSlider.track.height*0.9"
},
"startAngle": {"signal": "0"},
"endAngle": {"signal": "2*PI"},
"stroke": {
"signal": "configDateStepSlider.handle.outerStroke || '#BBB'"
},
"strokeWidth": {
"signal": "configDateStepSlider.handle.outerStrokeWidth"
},
"fill": {
"signal": "configDateStepSlider.handle.outerFill || '#fff'"
}
},
"update": {
"x": {
"signal": "scale('xSliderGranularityPercentage', span(xDomain)/span(domain('xScaleHorizontalScrollMap')))"
}
}
}
},
{
"name": "handles_inner_arc",
"description": "the inner circle mark that serves as the the 'slider handle'",
"type": "arc",
"from": {"data": "labelDateGranularitySlider_text"},
"interactive": false,
"encode": {
"enter": {
"y": {"signal": "configDateStepSlider.track.height/2"},
"innerRadius": {"value": 0},
"outerRadius": {"signal": "1"},
"startAngle": {"signal": "0"},
"endAngle": {"signal": "2*PI"},
"stroke": {
"signal": "configDateStepSlider.handle.innerStroke"
},
"fill": {
"signal": "configDateStepSlider.handle.innerFill || '#999'"
}
},
"update": {
"x": {
"signal": "scale('xSliderGranularityPercentage', span(xDomain)/span(domain('xScaleHorizontalScrollMap')))"
}
}
}
}
]
},
{
"name": "gantt-horizontal-scroll-map-interactive-rect",
"type": "rect",
"from": {"data": "track"},
"encode": {
"update": {
"x": {
"signal": "ganttHorizontalScrollMapMouseDown ? 0 : datum.bounds.x1"
},
"x2": {
"signal": "dateGranularitySliderMouseDown || panAndZoomMode ? 0 : datum.bounds.x2"
},
"y": {
"signal": "ganttHorizontalScrollMapMouseDown ? 0 : datum.bounds.y1 - horizontalScrollMapHeight/2-5"
},
"y2": {
"signal": "datum.bounds.y2 + horizontalScrollMapHeight/2+5 + (ganttHorizontalScrollMapMouseDown ? 18 : 0)"
},
"fillOpacity": {"value": 0},
"cursor": {
"signal": "scale('xScaleHorizontalScrollMap', xDomain[0]) === range('xScaleHorizontalScrollMap')[0] ? 'default' : ganttHorizontalScrollMapMouseDown ? 'pointer' : ganttHorizontalScrollMapMouseOver ? 'pointer' : 'default'"
}
}
}
},
{
"name": "granularity-reset-interactive-rect",
"type": "rect",
"from": {"data": "text-date-granularity-reset-label"},
"encode": {
"update": {
"x": {"signal": "datum.bounds.x1-5"},
"x2": {"signal": "datum.bounds.x2+(ganttWidth-datum.bounds.x2)"},
"y": {"signal": "datum.bounds.y1-5"},
"y2": {"signal": "datum.bounds.y2+5"},
"fillOpacity": {"value": 0},
"cursor": {
"signal": "granularityResetMouseover ? 'pointer' : 'default'"
},
"tooltip": {"value": "Reset date granularity"}
}
}
}
]
},
{
"name": "group_verticalScrollbar",
"description": "the group of marks that make up the vertical scrollbar",
"type": "group",
"encode": {
"update": {
"x": {
"signal": "verticalScrollbarMouseDown ? 0 : width-configVerticalScrollbar.track.width"
},
"width": {
"signal": "actualHeight > adjustedHeight && configVerticalScrollbar.enabled ? verticalScrollbarMouseDown ? width+configVerticalScrollbar.track.width : configVerticalScrollbar.track.width : 0"
},
"y": {"signal": "configVerticalScrollbar.enabled ? -30 : 0"},
"height": {
"signal": "actualHeight > adjustedHeight && configVerticalScrollbar.enabled ? configVerticalScrollbar.track.height + horizontalScrollMapHeight*3 + 30 : 0"
},
"fill": {"value": "transparent"},
"cursor": {"signal": "panAndZoomMode ? 'default' : 'pointer'"},
"clip": {"value": true}
}
},
"marks": [
{
"name": "rect_verticalScrollbar_track",
"description": "the track for the scrollbar",
"type": "rect",
"interactive": false,
"encode": {
"update": {
"x": {
"signal": "verticalScrollbarMouseDown ? width-configVerticalScrollbar.track.width : 0"
},
"width": {
"signal": "configVerticalScrollbar.enabled ? configVerticalScrollbar.track.width : 0"
},
"y": {"value": 30},
"height": {
"signal": "configVerticalScrollbar.enabled ? configVerticalScrollbar.track.height : 0"
},
"fill": {"signal": "configVerticalScrollbar.track.fill"},
"fillOpacity": {"signal": "panAndZoomMode ? 0.25 : 1"},
"cursor": {"value": "pointer"}
}
}
},
{
"name": "rect_verticalScrollbar_handle",
"description": "the handle for the scrollbar",
"type": "rect",
"interactive": false,
"encode": {
"update": {
"x": {
"signal": "verticalScrollbarMouseDown ? width-configVerticalScrollbar.track.width : 0"
},
"width": {
"signal": "configVerticalScrollbar.enabled ? configVerticalScrollbar.track.width : 0"
},
"y": {
"signal": "scale('scaleScrollHandleY', verticalScrollPercentage)-configVerticalScrollbar.handle.height+30"
},
"y2": {
"signal": "scale('scaleScrollHandleY', verticalScrollPercentage)+30"
},
"fill": {
"signal": "verticalScrollbarMouseOver ? configVerticalScrollbar.handle.hover.fill : configVerticalScrollbar.handle.fill"
},
"fillOpacity": {"signal": "panAndZoomMode ? 0.25 : 1"},
"cursor": {"value": "pointer"}
}
}
}
]
}
],
"scales": [
{
"name": "dateUnitIncrementMS",
"type": "ordinal",
"domain": ["hours", "day", "month", "year"],
"range": [3600000, 86400000, 2628000000, 31540000000]
},
{
"name": "x",
"type": "time",
"domain": {"signal": "xDomain"},
"range": {"signal": "[0,ganttWidth]"}
},
{
"name": "scaleXScroll",
"type": "linear",
"clamp": true,
"domain": {"signal": "[0, 1]"},
"range": {"signal": "xScrollDomain"}
},
{
"name": "xScaleHorizontalScrollMap",
"type": "time",
"clamp": true,
"domain": {"signal": "xScrollDomain"},
"range": {"signal": "[1, ganttWidth-2]"}
},
{
"name": "xSliderGranularityPercentage",
"type": "time",
"clamp": true,
"domain": {"signal": "[1,0]"},
"range": {"signal": "[0, configDateStepSlider.track.width]"}
},
{
"name": "scaleScrollHandleY",
"type": "linear",
"domain": [0, {"signal": "(actualHeight-adjustedHeight)/actualHeight"}],
"range": {
"signal": "[configVerticalScrollbar.handle.height, configVerticalScrollbar.track.height]"
},
"clamp": true
},
{
"name": "scaleGridY",
"type": "linear",
"domain": [0, {"signal": "(actualHeight-adjustedHeight)/actualHeight"}],
"range": {"signal": "[0, configVerticalScrollbar.track.height]"},
"clamp": true
}
],
"axes": [
{
"description": "Bottom date axis",
"ticks": true,
"labelPadding": {"signal": "xCurrentDateFormat === '%B %Y' ? 2 :-12"},
"scale": "x",
"position": {"signal": "columnsWidth"},
"orient": "top",
"domainColor": "#CCC",
"domainWidth": 0.25,
"tickSize": 15,
"tickOpacity": 1,
"grid": true,
"gridScale": "scaleGridY",
"zindex": -2,
"labelOverlap": true,
"formatType": "time",
"labelBound": true,
"tickCount": {
"signal": "xCurrentUnit === 'hour' ? 24*(span(xDomain)/86400000) : xCurrentUnit"
},
"format": {"signal": "xCurrentDateFormat"},
"labelSeparation": 3,
"tickExtra": true,
"encode": {
"labels": {
"update": {
"dx": {"signal": "xCurrentDateLabelDX"},
"text": {
"signal": "xCurrentDateFormat === '%B %Y' ? split(datum.label, ' ') : datum.label"
}
}
},
"ticks": {
"update": {
"stroke": {"value": "#CCC"},
"strokeWidth": {
"signal": "xCurrentUnit === 'day' && datum.label === '01' ? 0 : indexof(datum.label, 'Jan') >= 0 ? 0 : 0.35"
}
}
},
"grid": {
"update": {
"stroke": {"value": "#CCC"},
"strokeWidth": {
"signal": "xCurrentUnit === 'day' && datum.label === '01' ? 0 : indexof(datum.label, 'Jan') >= 0 ? 0 : 0.35"
}
}
}
}
},
{
"description": "Top date axis",
"title": {
"signal": "xCurrentDateFormatOffset === '%B %Y' && (toDate(utcFormat((xDomain[0] + (xDomain[1]-xDomain[0])/2), '01-%B-%Y')) < domain('x')[0]) ? utcFormat((xDomain[0] + (xDomain[1]-xDomain[0])/2), '%B %Y') : null"
},
"titleX": {"signal": "ganttWidth/2"},
"titleY": -25,
"titleBaseline": {"value": "middle"},
"titleFontSize": {"value": 10},
"titleFontWeight": "400",
"scale": "x",
"position": {"signal": "columnsWidth"},
"domain": false,
"orient": "top",
"offset": 0,
"tickSize": 15,
"tickOpacity": 1,
"tickWidth": 0.5,
"tickColor": "#666",
"labelBaseline": "bottom",
"grid": true,
"gridWidth": 0.5,
"gridColor": {"value": "#666"},
"gridScale": "scaleGridY",
"zindex": -1,
"labelPadding": 5,
"labelOverlap": true,
"formatType": "time",
"labelBound": true,
"tickCount": {"signal": "(xCurrentUnitOffset || 0)"},
"format": {"signal": "xCurrentDateFormatOffset"},
"labelSeparation": 5,
"encode": {
"labels": {
"update": {
"text": {
"signal": "xCurrentDateFormat === '%B %Y' ? null : datum.label"
},
"x": {
"signal": "xCurrentDateFormatOffset === '%Y' ? toDate(utcFormat(datum.value, '01-Jun-%Y')) > xDomain[1] ? scale('x', datum.value)+(scale('x', xDomain[1])-scale('x', datum.value))/2 : scale('x', toDate(utcFormat(datum.value, '01-Jun-%Y'))) : xCurrentDateFormatOffset === '%B %Y' ? toDate(utcFormat(datum.value, '17-%b-%Y')) > xDomain[1] ? scale('x', datum.value)+(scale('x', xDomain[1])-scale('x', datum.value))/2 : scale('x', toDate(utcFormat(datum.value, '15-%b-%Y'))) : scale('x', datum.value)"
},
"dx": {
"signal": "xCurrentDateFormatOffset === '%Y' || xCurrentDateFormatOffset === '%B %Y' ? 0 : xCurrentDateOffsetLabelDX"
},
"align": {"value": "center"}
}
}
}
}
],
"data": [
{
"name": "dataset",
"url": "https://raw.githubusercontent.com/Giammaria/PublicFiles/master/data/20240511_hierarchical_gantt_dataset.json",
"format": {
"parse": {
"id": "number",
"parentId": "number",
"name": "string",
"startDate": "date",
"endDate": "date",
"decimalPercentComplete": "number",
"dependencyId": "string"
}
},
"transform": []
},
{
"name": "dataset_formatted",
"source": "dataset",
"transform": [
{
"type": "formula",
"expr": "utcFormat(datum.startDate, '%Y-%m-%dT%H:%M:%S.%LZ')",
"as": "startDateFormatted"
},
{
"type": "formula",
"expr": "utcFormat(datum.endDate, '%Y-%m-%dT%H:%M:%S.%LZ')",
"as": "endDateFormatted"
},
{
"type": "formula",
"expr": "isValid(datum.color) ? datum.color : configRow.defaultFill",
"as": "color"
},
{
"type": "formula",
"expr": "toDate(utcFormat(datum.startDate, '%Y-%m-%d'))",
"as": "startDate"
},
{
"type": "formula",
"expr": "toDate(utcFormat(datum.endDate, '%Y-%m-%d'))",
"as": "endDate"
},
{
"type": "formula",
"expr": "!isValid(datum['startDate']) ? null : datum['endDate']",
"as": "endDate"
},
{"type": "stratify", "key": "id", "parentKey": "parentId"},
{"type": "window", "ops": ["row_number"], "as": ["sort"]},
{
"type": "formula",
"expr": "{id: datum.id, parentId: datum.parentId}",
"as": "idObj"
},
{"type": "window", "ops": ["row_number"], "as": ["index"]}
]
},
{
"name": "hierarchy_initial",
"source": "dataset_formatted",
"transform": [
{"type": "formula", "expr": "resetLevel", "as": "resetLevel"},
{
"type": "filter",
"expr": "configIncludeRoot ? true : (isValid(datum['parentId']))"
},
{
"type": "formula",
"expr": "length(treeAncestors('dataset_formatted', datum['id']))-(configIncludeRoot ? 0 : 1)",
"as": "level"
},
{
"type": "formula",
"expr": "indexof(pluck(data('dataset_formatted'), 'parentId'), datum['id'])>=0",
"as": "hasChildren"
},
{
"type": "formula",
"expr": "datum.hasChildren ? configRow.levelIndentWidth*(datum['level']-0.25) : null",
"as": "expandCollapseIndicatorX"
},
{
"type": "formula",
"expr": "configRow.levelIndentWidth*datum['level']",
"as": "indentWidth"
},
{
"type": "formula",
"expr": "slice(pluck(treeAncestors('dataset_formatted', datum['id']), 'id'), 1)",
"as": "ancestorIds"
},
{
"type": "formula",
"expr": "pluck(data('dataset_formatted'), 'idObj')",
"as": "immediateChildrenIds"
},
{
"type": "flatten",
"fields": ["immediateChildrenIds"],
"as": ["immediateChildrenIds"]
},
{
"type": "filter",
"expr": "datum.id === datum.immediateChildrenIds.id "
},
{
"type": "aggregate",
"ops": ["values"],
"fields": ["idObs"],
"groupby": [
"id",
"parentId",
"name",
"startDate",
"endDate",
"decimalPercentComplete",
"dependencyId",
"sort",
"level",
"expandCollapseIndicatorX",
"indentWidth",
"ancestorIds",
"hasChildren",
"resetLevel",
"color"
],
"as": ["immediateChildrenIds"]
},
{
"type": "formula",
"expr": "pluck(slice(pluck(datum.immediateChildrenIds, 'immediateChildrenIds'), 1), 'id')",
"as": "immediateChildrenIds"
},
{
"type": "formula",
"expr": "isValid(datum.isExpanded) ? datum.isExpanded : !datum.hasChildren ? null : datum.resetLevel > datum.level ? 1 : 0",
"as": "isExpanded"
},
{
"type": "formula",
"expr": "!expandAllClicked && datum.hasChildren && isValid(lastClickedNode) && lastClickedNode.datum.id === datum.id ? lastClickedNode.datum.isExpanded === 1 ? 0 : 1 : datum.isExpanded",
"as": "isExpanded"
},
{
"type": "formula",
"expr": "datum.hasChildren && isInitial ? datum.level >= datum.resetLevel ? 0 : 1 : datum.isExpanded",
"as": "isExpanded"
}
]
},
{
"name": "hierarchy_hidden_ancestorIds",
"source": "hierarchy_initial",
"transform": [
{"type": "filter", "expr": "datum.isExpanded === 0"},
{"type": "filter", "expr": "datum.hasChildren"},
{"type": "project", "fields": ["id", "name"]}
]
},
{
"name": "hierarchy_master",
"source": "hierarchy_initial",
"transform": [
{
"type": "formula",
"expr": "pluck(data('hierarchy_hidden_ancestorIds'), 'id')",
"as": "collapsedIds"
},
{
"type": "formula",
"expr": "!test('(\\\\b\\\\d+\\\\b).*\\\\b\\\\1\\\\b', (join(datum.ancestorIds, ',')+','+join(datum.collapsedIds, ',')))",
"as": "isIncluded"
},
{"type": "formula", "expr": "isInitial", "as": "isInitial"},
{
"type": "formula",
"expr": "datum.isInitial ? (datum.level <= datum.resetLevel) ? 1 : 0 : indexof(lastClickedNode.datum.immediateChildrenIds, datum.id)>=0 ? lastClickedNode.datum.isExpanded === 1 ? 0 : 1 : 1",
"as": "isVisible"
},
{
"type": "collect",
"sort": {"field": ["sort"], "order": ["ascending"]}
},
{
"type": "window",
"ops": ["row_number"],
"as": ["rowNumber"],
"groupby": ["isIncluded"],
"sort": {"field": ["sort"], "order": ["ascending"]}
},
{
"type": "formula",
"expr": "datum.isVisible ? datum.rowNumber-1 : null",
"as": "rowNumber"
},
{
"type": "window",
"ops": ["row_number"],
"as": ["rowNumberAll"],
"sort": {"field": ["sort"], "order": ["ascending"]}
},
{
"type": "formula",
"expr": "datum.isVisible ? datum.rowNumberAll-1 : null",
"as": "rowNumberAll"
},
{
"type": "formula",
"expr": "!datum.hasChildren ? 0 : datum.isExpanded === 1 ? 90 : 0",
"as": "expandCollapseIndicatorAngle"
},
{
"type": "formula",
"expr": "datum.startDate === datum.endDate ? 'milestone' : datum.isExpanded ? 'parentRect':'childRect'",
"as": "shapeType"
},
{
"type": "formula",
"expr": "datum.name + ' ('+format(datum.decimalPercentComplete, '.0%')+')'",
"as": "label"
}
]
},
{
"name": "hierarchy_master_buffered",
"source": "hierarchy_master",
"transform": [
{
"type": "formula",
"expr": "verticalScrollPercentage",
"as": "verticalScrollPercentage"
},
{"type": "formula", "expr": "actualHeight", "as": "actualHeight"},
{"type": "formula", "expr": "adjustedHeight", "as": "adjustedHeight"},
{
"type": "formula",
"expr": "clamp(datum.verticalScrollPercentage*datum.actualHeight,0, (datum.actualHeight-datum.adjustedHeight))",
"as": "viewportY"
},
{
"type": "formula",
"expr": "datum.viewportY + adjustedHeight",
"as": "viewportY2"
},
{
"type": "formula",
"expr": "configRow.rowHeight*datum.rowNumber",
"as": "unadjustedY"
},
{
"type": "formula",
"expr": "datum.startDate+(datum.endDate-datum.startDate)*datum.decimalPercentComplete",
"as": "progressDate"
},
{
"type": "filter",
"expr": "inrange(datum.unadjustedY, [datum.viewportY-configRow.rowHeight, datum.viewportY2+configRow.rowHeight])"
},
{
"type": "formula",
"expr": "isValid(lastClickedNode) && lastClickedNode.datum.id === datum.parentId ? datum.isIncluded ? 'enter' : 'exit' : 'update'",
"as": "join"
},
{
"type": "formula",
"expr": "clamp(isValid(lastClickedNode) && indexof(['enter', 'exit'], datum.join) >=0 ? (timer-lastClickedNode.timestamp)/250 : 1, 0,1)",
"as": "t"
},
{
"type": "formula",
"expr": "scale('x', datum.startDate)",
"as": "x"
},
{
"type": "formula",
"expr": "scale('x', datum.endDate)",
"as": "x2"
},
{
"type": "formula",
"expr": "scale('x', datum.progressDate)",
"as": "x2Progress"
},
{
"type": "formula",
"expr": "isValid(lastClickedNode) ? lerp( (datum.join === 'exit' ? [datum.rowNumberAll, lastClickedNode.datum.rowNumber] : [lastClickedNode.datum.rowNumber,datum.rowNumber]), datum.t)*configRow.rowHeight : configRow.rowHeight*datum.rowNumber",
"as": "y"
},
{
"type": "formula",
"expr": "datum.y+configRow.rowHeight*(datum.shapeType === 'milestone' ? 0.5 : 0.25)",
"as": "yWithPadding"
},
{
"type": "formula",
"expr": "datum.shapeType === 'milestone' ? null : configRow.rowHeight",
"as": "height"
},
{
"type": "formula",
"expr": "datum.shapeType === 'milestone' ? null : (datum.height)*(datum.shapeType === 'parentRect' ? configGantt.parentRect.percentOfRowHeight : configGantt.childRect.percentOfRowHeight)",
"as": "heightWithPadding"
},
{
"type": "formula",
"expr": "datum.shapeType === 'milestone' ? 12*configRow.rowHeight : null",
"as": "milestoneSize"
},
{
"type": "filter",
"expr": "datum.isIncluded || (datum.t < 1 && indexof(['enter', 'exit'], datum.join) >=0)"
}
]
},
{
"name": "height",
"source": "hierarchy_master",
"transform": [
{"type": "filter", "expr": "datum.isIncluded"},
{"type": "aggregate", "ops": ["count"], "as": ["height"]},
{
"type": "formula",
"expr": "(datum.height)*configRow.rowHeight",
"as": "height"
}
]
},
{
"name": "xDomainInitial",
"source": "dataset_formatted",
"transform": [
{
"type": "aggregate",
"fields": ["startDate", "endDate"],
"ops": ["min", "max"],
"as": ["minDate", "maxDate"]
},
{
"type": "formula",
"expr": "[datum.minDate-span([datum.minDate, datum.maxDate])*0.01,datum.maxDate+span([datum.minDate, datum.maxDate])*0.01]",
"as": "domain"
},
{
"type": "formula",
"expr": "[utcFormat(datum.domain[0], '%d-%b-%Y %H:%M:%S.%LZ'), utcFormat(datum.domain[1], '%d-%b-%Y %H:%M:%S.%LZ')]",
"as": "domainFormatted"
},
{
"type": "formula",
"expr": "[datum.minDate-scale('dateUnitIncrementMS', 'hours'), datum.minDate + ((ganttWidth-minDateBandwidth)/minDateBandwidth)*scale('dateUnitIncrementMS', 'hours')]",
"as": "hourDomain"
},
{
"type": "formula",
"expr": "[datum.minDate-scale('dateUnitIncrementMS', 'day'), datum.minDate + ((ganttWidth-minDateBandwidth)/minDateBandwidth)*scale('dateUnitIncrementMS', 'day')]",
"as": "dayDomain"
},
{
"type": "formula",
"expr": "ceil(span(datum.domain)/scale('dateUnitIncrementMS', 'year'))",
"as": "yearCount"
}
]
},
{
"name": "weekendDates",
"values": [{}],
"transform": [
{
"type": "formula",
"expr": "timeSequence('day', xDomain[0], xDomain[1])",
"as": "date"
},
{"type": "flatten", "fields": ["date"]},
{"type": "formula", "expr": "+datum.date", "as": "startDate"},
{
"type": "formula",
"expr": "+datum.date + (scale('dateUnitIncrementMS', 'day')-1)",
"as": "endDate"
},
{
"type": "formula",
"expr": "indexof([0,6], day(datum.date)) >= 0",
"as": "isWeekend"
},
{"type": "filter", "expr": "datum.isWeekend"}
]
}
]
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment