Last active
          April 16, 2025 16:54 
        
      - 
      
- 
        Save Giammaria/d7491f2bad63178f3daf6868762eea02 to your computer and use it in GitHub Desktop. 
    20250409_hierarchical_gantt_v_v2
  
        
  
    
      This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
      Learn more about bidirectional Unicode characters
    
  
  
    
  | { | |
| "$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