Last active
November 3, 2025 22:31
-
-
Save Giammaria/a4fdb5e3aebc15c6b276fb956465bb91 to your computer and use it in GitHub Desktop.
20251031_hierarchical_bar_chart_v_v1.0
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", | |
| "width": 800, | |
| "background": "#fff", | |
| "signals": [ | |
| {"name": "padding", "value": 10}, | |
| {"name": "isPowerBIVisual", "value": false}, | |
| {"name": "configDesiredChartHeight", "init": "150"}, | |
| { | |
| "name": "configFields", | |
| "update": "{id: 'id', parentId: 'parentId', name: 'name', label: 'name', 'childQuantity': 'childQuantity', descendantQuantity: 'descendantQuantity'}" | |
| }, | |
| { | |
| "name": "configColorScheme", | |
| "init": "{axes: {x: {text: {fill: '#555'}, grid: {stroke: '#dddddd'}, ticks: {stroke: '#dddddd'}}, y: {text: {fill: '#555'}}}, bars: {parent: {fill: '#eb6123', fillOpacity: 0.35, stroke: '#eb6123', strokeOpacity: 1}, leaf: {'fill': '#eee', fillOpacity: 1, stroke: '#eee', strokeOpacity: 1}}}" | |
| }, | |
| {"name": "configHeader", "init": "{height: 25, verticalOffset: 2.5}"}, | |
| {"name": "configFooter", "init": "{height: 25, verticalOffset: 2.5}"}, | |
| { | |
| "name": "configAxes", | |
| "init": "{x: {height: 40, title: {text: 'Quantity'}}, y: {width: 0.15*width, labels: {padding: 2.5}}}" | |
| }, | |
| {"name": "configIncludeRoot", "value": false}, | |
| { | |
| "name": "configRow", | |
| "description": "configurations for the rows", | |
| "init": "{rowHeight: 25, defaultFill: '#40407d'}" | |
| }, | |
| { | |
| "name": "configAnimationDuration", | |
| "init": "{showDetails: 750, nodeExpandCollapse: 2500, sort: 500}" | |
| }, | |
| { | |
| "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": "currentNodeId", | |
| "init": "data('dataset-formatted')[0].id", | |
| "on": [ | |
| { | |
| "events": {"signal": "timer<(initialTimestamp+500)"}, | |
| "update": "data('dataset-formatted')[0].id" | |
| }, | |
| { | |
| "events": "@node-clickable-rect:pointerdown, @y-axis-label-clickable-rect:pointerdown", | |
| "update": "datum.hasChildren ? datum.id : currentNodeId" | |
| } | |
| ] | |
| }, | |
| { | |
| "name": "sortIndicatorMouseOver", | |
| "init": "true", | |
| "on": [ | |
| {"events": "@sort-order-group:mouseover", "update": "true"}, | |
| {"events": "@sort-order-group:mouseout", "update": "false"} | |
| ] | |
| }, | |
| { | |
| "name": "sortOrderDescending", | |
| "init": "true", | |
| "on": [ | |
| { | |
| "events": "@sort-order-group:click", | |
| "update": "isAnimating ? sortOrderDescending : !sortOrderDescending" | |
| } | |
| ] | |
| }, | |
| { | |
| "name": "sortChangeCount", | |
| "init": "0", | |
| "on": [ | |
| { | |
| "events": {"signal": "sortOrderDescending"}, | |
| "update": "sortChangeCount + (isAnimating ? 0 : 1)" | |
| } | |
| ] | |
| }, | |
| { | |
| "name": "sortOrder", | |
| "update": "sortOrderDescending ? 'descending' : 'ascending'" | |
| }, | |
| { | |
| "name": "isInitial", | |
| "value": true, | |
| "on": [{"events": {"signal": "interactionType"}, "update": "false"}] | |
| }, | |
| { | |
| "name": "interactionTypeHistory", | |
| "init": "[]", | |
| "on": [ | |
| { | |
| "events": {"signal": "sortChangeCount"}, | |
| "update": "length(interactionTypeHistory) === 0 ? ['sort '+(sortOrder)] : split((('sort '+ (sortOrder))+','+join(interactionTypeHistory)),',',2)" | |
| }, | |
| { | |
| "events": "@node-clickable-rect:pointerdown, @y-axis-label-clickable-rect:pointerdown", | |
| "update": "!datum.hasChildren ? interactionTypeHistory : length(interactionTypeHistory) === 0 ? ['node click'] : split('node click,' + join(interactionTypeHistory), ',',2)" | |
| } | |
| ] | |
| }, | |
| { | |
| "name": "interactionType", | |
| "update": "length(interactionTypeHistory) === 0 ? [] : interactionTypeHistory[0]" | |
| }, | |
| { | |
| "name": "plotAreaDimensions", | |
| "update": "{width: width-configAxes.y.width-(actualHeight > adjustedHeight ? configVerticalScrollbar.track.width/2 : 0), height: adjustedHeight}" | |
| }, | |
| { | |
| "name": "scrollY", | |
| "update": "actualHeight > adjustedHeight ? clamp(-verticalScrollPercentage*actualHeight, -(actualHeight-adjustedHeight), 0) : 0" | |
| }, | |
| { | |
| "name": "isAnimating", | |
| "init": "false", | |
| "on": [ | |
| { | |
| "events": {"type": "timer"}, | |
| "update": "!isValid(data('hierarchy-animation-bounds')) || !isValid(data('hierarchy-animation-bounds')[0]) ? false : !(timer > data('hierarchy-animation-bounds')[0].end+configAnimationDuration.nodeExpandCollapse)" | |
| } | |
| ] | |
| }, | |
| { | |
| "name": "scaleXTargetMax", | |
| "update": "length(data('xscale-target-bounds'))>0 ? (isValid(data('xscale-target-bounds')[0].maxQuantity) ? data('xscale-target-bounds')[0].maxQuantity : 0) : 0" | |
| }, | |
| { | |
| "name": "scaleXSourceMax", | |
| "init": "scaleXTargetMax", | |
| "on": [ | |
| {"events": {"signal": "nodeClickStart"}, "update": "scaleXTargetMax"}, | |
| {"events": {"signal": "sortChangeCount"}, "update": "scaleXTargetMax"} | |
| ] | |
| }, | |
| { | |
| "name": "scaleXDomainAnimatedMax", | |
| "update": "lerp([scaleXSourceMax, scaleXTargetMax], rowAnimationTEasedSecond)" | |
| }, | |
| { | |
| "name": "animStartTick", | |
| "init": "0", | |
| "on": [ | |
| { | |
| "events": {"signal": "sortChangeCount"}, | |
| "update": "!isAnimating ? timer : animStartTick" | |
| }, | |
| { | |
| "events": "@node-clickable-rect:pointerdown, @y-axis-label-clickable-rect:pointerdown", | |
| "update": "!isAnimating && datum.hasChildren ? timer : animStartTick" | |
| } | |
| ] | |
| }, | |
| { | |
| "name": "animStartTickFirst", | |
| "init": "0", | |
| "on": [ | |
| { | |
| "events": {"signal": "sortChangeCount"}, | |
| "update": "!isAnimating ? timer : animStartTick" | |
| }, | |
| { | |
| "events": "@node-clickable-rect:pointerdown, @y-axis-label-clickable-rect:pointerdown", | |
| "update": "!isAnimating && datum.hasChildren ? timer : animStartTick" | |
| } | |
| ] | |
| }, | |
| { | |
| "name": "animStartTickSecond", | |
| "update": "length(interactionType) === 0 ? 0 : animStartTickFirst+(slice(interactionType, 0, 4) === 'sort' ? configAnimationDuration.sort : configAnimationDuration.nodeExpandCollapse)/2" | |
| }, | |
| { | |
| "name": "animateCount", | |
| "init": "0", | |
| "on": [ | |
| { | |
| "events": {"signal": "sortChangeCount"}, | |
| "update": "isAnimating ? animateCount : animateCount + 1" | |
| }, | |
| { | |
| "events": {"signal": "nodeClickedDatum"}, | |
| "update": "isAnimating || !isValid(nodeClickedDatum) || !nodeClickedDatum.hasChildren ? animateCount : animateCount + 1" | |
| } | |
| ] | |
| }, | |
| { | |
| "name": "rowAnimation", | |
| "update": "!isValid(interactionType) ? rowAnimation : {active: (isValid(rowAnimation) && rowAnimation.t < 1), t: clamp((timer - animStartTick) / (slice(interactionType, 0, 4) === 'sort' ? configAnimationDuration.sort : configAnimationDuration.nodeExpandCollapse), 0, 1)}" | |
| }, | |
| { | |
| "name": "rowAnimationTEased", | |
| "update": "rowAnimation.t < 0.5 ? 4*pow(rowAnimation.t,3) : 1 - pow(-2*rowAnimation.t + 2, 3)/2" | |
| }, | |
| { | |
| "name": "rowAnimationFirst", | |
| "update": "!isValid(interactionType) ? rowAnimation : {active: (isValid(rowAnimationFirst) && rowAnimationFirst.t < 1), t: clamp((timer - animStartTick) / (slice(interactionType, 0, 4) === 'sort' ? configAnimationDuration.sort : configAnimationDuration.nodeExpandCollapse)*2, 0, 1)}" | |
| }, | |
| { | |
| "name": "rowAnimationTEasedFirst", | |
| "update": "rowAnimationFirst.t < 0.5 ? 4*pow(rowAnimationFirst.t,3) : 1 - pow(-2*rowAnimationFirst.t + 2, 3)/2" | |
| }, | |
| { | |
| "name": "rowAnimationSecond", | |
| "update": "!isValid(interactionType) ? rowAnimation : {active: (isValid(rowAnimationSecond) && rowAnimationSecond.t < 1 && rowAnimationSecond.t !== 0), t: clamp((timer - animStartTickSecond) / (slice(interactionType, 0, 4) === 'sort' ? configAnimationDuration.sort : configAnimationDuration.nodeExpandCollapse)*2, 0, 1)}" | |
| }, | |
| { | |
| "name": "rowAnimationTEasedSecond", | |
| "update": "rowAnimationSecond.t < 0.5 ? 4*pow(rowAnimationSecond.t,3) : 1 - pow(-2*rowAnimationSecond.t + 2, 3)/2" | |
| }, | |
| { | |
| "name": "timer", | |
| "init": "now()", | |
| "on": [{"events": {"type": "timer"}, "update": "now()"}] | |
| }, | |
| {"name": "initialTimestamp", "init": "now()"}, | |
| { | |
| "name": "nodeClickedDatum", | |
| "init": "null", | |
| "on": [ | |
| { | |
| "events": "@node-clickable-rect:pointerdown, @y-axis-label-clickable-rect:pointerdown", | |
| "update": "!isAnimating ? datum : nodeClickedDatum" | |
| } | |
| ] | |
| }, | |
| { | |
| "name": "nodeClickStart", | |
| "init": "null", | |
| "on": [ | |
| { | |
| "events": "@node-clickable-rect:pointerdown, @y-axis-label-clickable-rect:pointerdown", | |
| "update": "datum" | |
| } | |
| ] | |
| }, | |
| { | |
| "name": "nodeMouseOverDatum", | |
| "init": "null", | |
| "on": [ | |
| { | |
| "events": "@node-clickable-rect:mouseover, @y-axis-label-clickable-rect:mouseover", | |
| "update": "datum" | |
| }, | |
| { | |
| "events": "@node-clickable-rect:mouseout, @y-axis-label-clickable-rect:mouseout", | |
| "update": "null" | |
| } | |
| ] | |
| }, | |
| { | |
| "name": "verticalScrollIncrement", | |
| "description": "Defines the vertical scroll step size as 1% of the scrollable height. Updates dynamically based on actualHeight and adjustedHeight.", | |
| "update": "0.1 * (actualHeight-adjustedHeight)/actualHeight" | |
| }, | |
| { | |
| "name": "verticalScrollbarMouseDown", | |
| "value": false, | |
| "on": [ | |
| { | |
| "events": "@vertical-scrollbar-group:pointerdown", | |
| "update": "configVerticalScrollbar.enabled" | |
| }, | |
| { | |
| "events": { | |
| "type": "mouseout", | |
| "scope": "view", | |
| "markname": "vertical-scrollbar-group", | |
| "filter": ["!event.pointerdown"] | |
| }, | |
| "update": "false" | |
| }, | |
| { | |
| "events": { | |
| "type": "mouseover", | |
| "scope": "scope", | |
| "markname": "vertical-scrollbar-group", | |
| "filter": ["event.pointerdown"] | |
| }, | |
| "update": "true" | |
| }, | |
| {"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": "@vertical-scrollbar-group:mouseover", | |
| "update": "configVerticalScrollbar.enabled" | |
| }, | |
| {"events": "@vertical-scrollbar-group:mouseout", "update": "false"} | |
| ] | |
| }, | |
| { | |
| "name": "verticalScrollPercentage", | |
| "description": "Tracks the current vertical scroll position as a percentage. Updates on: Mouse wheel events; 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": "clamp(verticalScrollPercentage - (-event.deltaY * pow(4, event.deltaMode) * 0.0015 * adjustedHeight / actualHeight), 0, (actualHeight-adjustedHeight)/actualHeight)" | |
| }, | |
| { | |
| "events": "window:keydown[event.key === 'ArrowUp' || event.key === 'ArrowDown']{0,0}", | |
| "update": "clamp(verticalScrollPercentage + verticalScrollIncrement * (event.key === 'ArrowDown' ? 1 : -1), 0, (actualHeight-adjustedHeight)/actualHeight)" | |
| }, | |
| { | |
| "events": { | |
| "type": "pointermove", | |
| "source": "scope", | |
| "markname": "vertical-scrollbar-group", | |
| "throttle": 50, | |
| "between": [{"type": "pointerdown"}, {"type": "pointerup"}] | |
| }, | |
| "update": "!configVerticalScrollbar.enabled ? 0 : invert('scaleScrollHandleY', y(group()))" | |
| }, | |
| { | |
| "events": {"signal": "!configVerticalScrollbar.enabled"}, | |
| "update": "0" | |
| }, | |
| { | |
| "events": {"signal": "verticalScrollPercentage"}, | |
| "update": "isFinite(verticalScrollPercentage) ? verticalScrollPercentage : 0" | |
| } | |
| ] | |
| }, | |
| { | |
| "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-animation')[0]['animatedHeight']" | |
| }, | |
| { | |
| "name": "adjustedHeight", | |
| "description": "The initial height of the visualization. Rows that go beyond this height will require scrolling/panning", | |
| "update": "max(min((isPowerBIVisual ? (containerSize()[1] || windowSize()[1]) : configDesiredChartHeight), actualHeight), 800)" | |
| }, | |
| { | |
| "name": "height", | |
| "update": "configHeader.height+configHeader.verticalOffset+configFooter.height+configFooter.verticalOffset+adjustedHeight" | |
| } | |
| ], | |
| "marks": [ | |
| { | |
| "name": "everything-group", | |
| "type": "group", | |
| "marks": [ | |
| { | |
| "name": "header-group", | |
| "type": "group", | |
| "encode": { | |
| "update": { | |
| "width": {"signal": "width"}, | |
| "height": {"signal": "configHeader.height"}, | |
| "clip": {"value": true} | |
| } | |
| } | |
| }, | |
| { | |
| "name": "static-chart-area-group", | |
| "type": "group", | |
| "from": {"data": "header-group"}, | |
| "encode": { | |
| "update": { | |
| "y": { | |
| "signal": "datum.bounds.y2", | |
| "offset": {"signal": "configHeader.verticalOffset"} | |
| }, | |
| "width": {"signal": "width"}, | |
| "height": { | |
| "signal": "plotAreaDimensions.height+configAxes.x.height" | |
| }, | |
| "clip": {"value": true} | |
| } | |
| }, | |
| "marks": [ | |
| { | |
| "name": "y-axis-group", | |
| "type": "group", | |
| "encode": { | |
| "update": { | |
| "y": {"signal": "configAxes.x.height"}, | |
| "width": {"signal": "configAxes.y.width"}, | |
| "height": {"signal": "plotAreaDimensions.height"}, | |
| "clip": {"value": true} | |
| } | |
| }, | |
| "marks": [ | |
| { | |
| "name": "y-axis-labels-group", | |
| "type": "group", | |
| "encode": {"update": {"y": {"signal": "scrollY"}}}, | |
| "marks": [ | |
| { | |
| "name": "y-axis-label-clickable-rect", | |
| "type": "rect", | |
| "from": {"data": "hierarchy-animation"}, | |
| "encode": { | |
| "update": { | |
| "x": {"value": 0}, | |
| "width": {"signal": "configAxes.y.width"}, | |
| "y": {"field": "y1"}, | |
| "y2": {"field": "y2"}, | |
| "fill": {"value": "transparent"}, | |
| "cursor": { | |
| "signal": "datum.hasChildren ? 'pointer' : 'default'" | |
| } | |
| } | |
| } | |
| }, | |
| { | |
| "name": "y-axis-labels", | |
| "type": "text", | |
| "from": {"data": "hierarchy-animation"}, | |
| "interactive": false, | |
| "encode": { | |
| "update": { | |
| "text": {"field": "label"}, | |
| "y": { | |
| "field": "y1", | |
| "offset": {"signal": "configRow.rowHeight/2"} | |
| }, | |
| "fill": { | |
| "signal": "configColorScheme.axes.y.text.fill" | |
| }, | |
| "limit": { | |
| "signal": "configAxes.y.width-configAxes.y.labels.padding" | |
| }, | |
| "baseline": {"value": "middle"}, | |
| "fontWeight": { | |
| "signal": "isValid(nodeMouseOverDatum) ? nodeMouseOverDatum.id === datum.id ? 600 : 400 : 400" | |
| }, | |
| "opacity": { | |
| "signal": "isValid(nodeMouseOverDatum) ? nodeMouseOverDatum.id === datum.id ? datum.fullOpacity : datum.fullOpacity*0.65 : datum.fullOpacity" | |
| } | |
| } | |
| } | |
| } | |
| ] | |
| } | |
| ] | |
| }, | |
| { | |
| "name": "sort-order-group", | |
| "type": "group", | |
| "from": {"data": "y-axis-group"}, | |
| "interactive": true, | |
| "encode": { | |
| "update": { | |
| "tooltip": {"signal": "'Sort ' + sortOrder"}, | |
| "y": {"signal": "datum.bounds.y1-configRow.rowHeight"}, | |
| "y2": {"signal": "datum.bounds.y1"}, | |
| "x": {"value": 0}, | |
| "fill": {"value": "transparent"}, | |
| "width": {"signal": "configAxes.y.width/2"}, | |
| "cursor": {"value": "pointer"} | |
| } | |
| }, | |
| "signals": [ | |
| { | |
| "name": "sortIndicatorAngle", | |
| "update": "interactionType === 'node click' ? sortIndicatorAngle : sortOrderDescending ? lerp([-180, 0], rowAnimationTEased) : lerp([0, -180], rowAnimationTEased)" | |
| }, | |
| {"name": "x", "update": "4"}, | |
| { | |
| "name": "dy", | |
| "update": "interactionType === 'node click' ? dy : sortOrderDescending ? lerp([2.5, 0], rowAnimationTEased) : lerp([0, 2.5], rowAnimationTEased)" | |
| } | |
| ], | |
| "marks": [ | |
| { | |
| "name": "y-axis-labels", | |
| "type": "text", | |
| "interactive": false, | |
| "encode": { | |
| "update": { | |
| "x": {"signal": "x"}, | |
| "text": {"value": "⮟"}, | |
| "angle": {"signal": "sortIndicatorAngle"}, | |
| "align": {"value": "center"}, | |
| "y": {"signal": "configRow.rowHeight/2"}, | |
| "dy": {"signal": "dy"}, | |
| "fill": {"signal": "configColorScheme.axes.y.text.fill"}, | |
| "opacity": { | |
| "signal": "sortIndicatorMouseOver ? 1 : 0.65" | |
| }, | |
| "baseline": {"value": "middle"} | |
| } | |
| } | |
| } | |
| ] | |
| }, | |
| { | |
| "name": "x-axis-group", | |
| "type": "group", | |
| "from": {"data": "y-axis-group"}, | |
| "encode": { | |
| "update": { | |
| "x": {"signal": "datum.bounds.x2"}, | |
| "y": {"signal": "datum.bounds.y1"}, | |
| "width": {"signal": "plotAreaDimensions.width"} | |
| } | |
| }, | |
| "axes": [ | |
| { | |
| "scale": "scaleX", | |
| "orient": "top", | |
| "grid": true, | |
| "gridColor": { | |
| "signal": "configColorScheme.axes.x.grid.stroke" | |
| }, | |
| "tickColor": { | |
| "signal": "configColorScheme.axes.x.ticks.stroke" | |
| }, | |
| "domain": false, | |
| "title": {"signal": "configAxes.x.title.text"}, | |
| "titleY": {"signal": "-configAxes.x.height"}, | |
| "titleBaseline": "top", | |
| "labelColor": { | |
| "signal": "configColorScheme.axes.x.text.fill" | |
| }, | |
| "encode": { | |
| "labels": { | |
| "update": { | |
| "text": { | |
| "signal": "plotAreaDimensions.width-scale('scaleX', datum.value) > 20 ? datum.label : null" | |
| } | |
| } | |
| }, | |
| "ticks": { | |
| "update": { | |
| "opacity": { | |
| "signal": "plotAreaDimensions.width-scale('scaleX', datum.value) > 20 ? 1 : 0" | |
| } | |
| } | |
| } | |
| } | |
| } | |
| ] | |
| }, | |
| { | |
| "name": "static-plot-area-group", | |
| "type": "group", | |
| "encode": { | |
| "update": { | |
| "x": {"signal": "configAxes.y.width"}, | |
| "y": {"signal": "configAxes.x.height"}, | |
| "width": {"signal": "plotAreaDimensions.width"}, | |
| "height": {"signal": "plotAreaDimensions.height"}, | |
| "clip": {"value": true} | |
| } | |
| }, | |
| "marks": [ | |
| { | |
| "name": "plot-area-group", | |
| "type": "group", | |
| "encode": {"update": {"y": {"signal": "scrollY"}}}, | |
| "interactive": true, | |
| "marks": [ | |
| { | |
| "name": "plot-background-rect", | |
| "type": "rect", | |
| "encode": { | |
| "update": { | |
| "x": {"value": 1}, | |
| "y": {"signal": "-scrollY+1"}, | |
| "width": {"signal": "plotAreaDimensions.width-1"}, | |
| "height": {"signal": "plotAreaDimensions.height-1"}, | |
| "fill": {"value": "transparent"} | |
| } | |
| } | |
| }, | |
| { | |
| "name": "node-clickable-rect", | |
| "type": "rect", | |
| "from": {"data": "hierarchy-animation"}, | |
| "interactive": true, | |
| "encode": { | |
| "update": { | |
| "tooltip": {"signal": "datum"}, | |
| "x": {"field": "x1"}, | |
| "x2": {"field": "x2"}, | |
| "y": {"field": "y1"}, | |
| "y2": {"field": "y2"}, | |
| "fill": {"value": "transparent"}, | |
| "cursor": { | |
| "signal": "datum.hasChildren ? 'pointer' : 'default'" | |
| } | |
| } | |
| } | |
| }, | |
| { | |
| "name": "node-background-rect", | |
| "type": "rect", | |
| "from": {"data": "hierarchy-animation"}, | |
| "interactive": false, | |
| "encode": { | |
| "update": { | |
| "x": {"field": "x1"}, | |
| "x2": {"field": "x2"}, | |
| "y": {"field": "y1WithPadding"}, | |
| "y2": {"field": "y2WithPadding"}, | |
| "fill": {"signal": "background"}, | |
| "opacity": { | |
| "signal": "isValid(nodeMouseOverDatum) ? nodeMouseOverDatum.id === datum.id ? datum.fullOpacity : datum.fullOpacity : datum.fullOpacity" | |
| } | |
| } | |
| } | |
| }, | |
| { | |
| "name": "node-rect", | |
| "type": "rect", | |
| "from": {"data": "hierarchy-animation"}, | |
| "interactive": false, | |
| "encode": { | |
| "update": { | |
| "x": {"field": "x1"}, | |
| "x2": {"field": "x2"}, | |
| "y": {"field": "y1WithPadding"}, | |
| "y2": {"field": "y2WithPadding"}, | |
| "fill": { | |
| "signal": "datum.hasChildren ? configColorScheme.bars.parent.fill : configColorScheme.bars.leaf.fill" | |
| }, | |
| "fillOpacity": { | |
| "signal": "datum.hasChildren ? configColorScheme.bars.parent.fillOpacity : configColorScheme.bars.leaf.fillOpacity" | |
| }, | |
| "stroke": { | |
| "signal": "datum.hasChildren ? configColorScheme.bars.parent.stroke : configColorScheme.bars.leaf.stroke" | |
| }, | |
| "strokeOpacity": { | |
| "signal": "datum.hasChildren ? configColorScheme.bars.parent.strokeOpacity : configColorScheme.bars.leaf.strokeOpacity" | |
| }, | |
| "opacity": { | |
| "signal": "isValid(nodeMouseOverDatum) ? nodeMouseOverDatum.id === datum.id ? datum.fullOpacity : datum.fullOpacity*0.65 : datum.fullOpacity" | |
| } | |
| } | |
| } | |
| }, | |
| { | |
| "name": "plot-container-rect", | |
| "type": "rect", | |
| "interactive": false, | |
| "encode": { | |
| "update": { | |
| "x": {"value": 0.5}, | |
| "y": {"signal": "-scrollY+1"}, | |
| "width": {"signal": "plotAreaDimensions.width-1"}, | |
| "height": {"signal": "plotAreaDimensions.height-1"}, | |
| "fill": {"value": "transparent"}, | |
| "stroke": {"value": "#888"} | |
| } | |
| } | |
| } | |
| ] | |
| } | |
| ] | |
| } | |
| ] | |
| }, | |
| { | |
| "name": "footer-group", | |
| "type": "group", | |
| "from": {"data": "static-chart-area-group"}, | |
| "encode": { | |
| "update": { | |
| "y": {"signal": "datum.bounds.y2+configHeader.verticalOffset"}, | |
| "width": {"signal": "width"}, | |
| "height": {"signal": "configHeader.height"}, | |
| "clip": {"value": true} | |
| } | |
| } | |
| }, | |
| { | |
| "name": "vertical-scrollbar-group", | |
| "description": "the group of marks that make up the vertical scrollbar", | |
| "type": "group", | |
| "from": {"data": "static-chart-area-group"}, | |
| "interactive": true, | |
| "encode": { | |
| "update": { | |
| "y": { | |
| "signal": "datum.bounds.y1+configAxes.x.height+(verticalScrollbarMouseDown ? -configAxes.x.height-configHeader.height-configHeader.verticalOffset: 1)" | |
| }, | |
| "x": { | |
| "signal": "verticalScrollbarMouseDown ? 0 : datum.bounds.x2" | |
| }, | |
| "width": { | |
| "signal": "actualHeight > adjustedHeight && configVerticalScrollbar.enabled ? verticalScrollbarMouseDown ? datum.bounds.x2+plotAreaDimensions.width+configVerticalScrollbar.track.width : configVerticalScrollbar.track.width : 0" | |
| }, | |
| "height": { | |
| "signal": "actualHeight > adjustedHeight && configVerticalScrollbar.enabled ? ((verticalScrollbarMouseDown ? +configAxes.x.height+configHeader.height+configHeader.verticalOffset+configFooter.height+configFooter.verticalOffset : 0) + plotAreaDimensions.height) : 0" | |
| }, | |
| "fill": {"value": "transparent"}, | |
| "cursor": {"signal": "'pointer'"}, | |
| "zindex": {"value": 999} | |
| } | |
| }, | |
| "marks": [ | |
| { | |
| "name": "rect_verticalScrollbar_track", | |
| "description": "the track for the scrollbar", | |
| "type": "rect", | |
| "interactive": false, | |
| "encode": { | |
| "update": { | |
| "y": { | |
| "signal": "verticalScrollbarMouseDown ? configAxes.x.height+configHeader.height+configHeader.verticalOffset : 0" | |
| }, | |
| "x": { | |
| "signal": "verticalScrollbarMouseDown ? parent.bounds.x2: 0" | |
| }, | |
| "width": { | |
| "signal": "configVerticalScrollbar.enabled ? configVerticalScrollbar.track.width : 0" | |
| }, | |
| "height": { | |
| "signal": "configVerticalScrollbar.enabled ? configVerticalScrollbar.track.height : 0" | |
| }, | |
| "fill": {"signal": "configVerticalScrollbar.track.fill"}, | |
| "stroke": {"value": "#888"}, | |
| "strokeWidth": {"signal": "1"} | |
| } | |
| } | |
| }, | |
| { | |
| "name": "rect_verticalScrollbar_handle", | |
| "description": "the handle for the scrollbar", | |
| "type": "rect", | |
| "interactive": false, | |
| "encode": { | |
| "update": { | |
| "x": { | |
| "signal": "verticalScrollbarMouseDown ? parent.bounds.x2: 0" | |
| }, | |
| "width": { | |
| "signal": "configVerticalScrollbar.enabled ? configVerticalScrollbar.track.width : 0" | |
| }, | |
| "y": { | |
| "signal": "(scale('scaleScrollHandleY', verticalScrollPercentage)-configVerticalScrollbar.handle.height)" | |
| }, | |
| "y2": { | |
| "signal": "scale('scaleScrollHandleY', verticalScrollPercentage)" | |
| }, | |
| "fill": { | |
| "signal": "verticalScrollbarMouseOver || verticalScrollbarMouseDown ? configVerticalScrollbar.handle.hover.fill : configVerticalScrollbar.handle.fill" | |
| } | |
| } | |
| } | |
| } | |
| ] | |
| } | |
| ] | |
| } | |
| ], | |
| "scales": [ | |
| { | |
| "name": "scaleX", | |
| "type": "linear", | |
| "domain": [0, {"signal": "scaleXDomainAnimatedMax"}], | |
| "range": [0, {"signal": "plotAreaDimensions.width"}], | |
| "nice": true, | |
| "zero": true | |
| }, | |
| { | |
| "name": "scaleXSource", | |
| "type": "linear", | |
| "domain": [0, {"signal": "scaleXSourceMax"}], | |
| "range": [0, {"signal": "plotAreaDimensions.width"}], | |
| "nice": true, | |
| "zero": true | |
| }, | |
| { | |
| "name": "scaleScrollHandleY", | |
| "type": "linear", | |
| "domain": [0, {"signal": "(actualHeight-adjustedHeight)/actualHeight"}], | |
| "range": { | |
| "signal": "[(verticalScrollbarMouseDown ? configAxes.x.height+configHeader.height+configHeader.verticalOffset: 0)+configVerticalScrollbar.handle.height, (verticalScrollbarMouseDown ? configAxes.x.height+configHeader.height+configHeader.verticalOffset : 0)+configVerticalScrollbar.track.height]" | |
| }, | |
| "clamp": true | |
| } | |
| ], | |
| "data": [ | |
| { | |
| "name": "dataset", | |
| "url": "https://raw.githubusercontent.com/Giammaria/PublicFiles/refs/heads/master/data/20251031_halloween_candy_hierarchy" | |
| }, | |
| { | |
| "name": "dataset-formatted", | |
| "source": "dataset", | |
| "transform": [ | |
| {"type": "formula", "expr": "datum[configFields.id]", "as": "id"}, | |
| { | |
| "type": "formula", | |
| "expr": "datum[configFields.parentId]", | |
| "as": "parentId" | |
| }, | |
| {"type": "formula", "expr": "datum[configFields.label]", "as": "label"}, | |
| { | |
| "type": "formula", | |
| "expr": "replace(replace(lower((datum[configFields.name] || '')), /[^a-z0-9]+/g, '_'), /^_+|_+$/g, '')", | |
| "as": "name" | |
| }, | |
| { | |
| "type": "formula", | |
| "expr": "datum[configFields.childQuantity]", | |
| "as": "childQuantity" | |
| }, | |
| { | |
| "type": "formula", | |
| "expr": "datum[configFields.descendantQuantity]", | |
| "as": "descendantQuantity" | |
| }, | |
| { | |
| "type": "formula", | |
| "expr": "max(datum.childQuantity, datum.descendantQuantity)", | |
| "as": "quantity" | |
| }, | |
| {"type": "stratify", "key": "id", "parentKey": "parentId"}, | |
| { | |
| "type": "formula", | |
| "expr": "{id: datum.id, parentId: datum.parentId}", | |
| "as": "idObj" | |
| } | |
| ] | |
| }, | |
| { | |
| "name": "hierarchy-sort-base", | |
| "source": "dataset-formatted", | |
| "transform": [ | |
| { | |
| "type": "formula", | |
| "expr": "length(treeAncestors('dataset-formatted', datum['id']))-(configIncludeRoot ? 0 : 1)", | |
| "as": "level" | |
| }, | |
| { | |
| "type": "joinaggregate", | |
| "ops": ["max"], | |
| "fields": ["id"], | |
| "as": ["idLength"] | |
| }, | |
| { | |
| "type": "formula", | |
| "expr": "length(toString(datum.idLength))", | |
| "as": "idLength" | |
| }, | |
| { | |
| "type": "window", | |
| "ops": ["row_number"], | |
| "sort": {"field": "quantity", "order": {"signal": "sortOrder"}}, | |
| "groupby": ["level"], | |
| "as": ["quantitySort"] | |
| }, | |
| { | |
| "type": "formula", | |
| "expr": "slice(pad('', datum.idLength, '0') + datum.quantitySort, -datum.idLength)", | |
| "as": "quantitySortPadded" | |
| }, | |
| {"type": "stratify", "key": "id", "parentKey": "parentId"}, | |
| { | |
| "type": "filter", | |
| "expr": "configIncludeRoot ? true : (isValid(datum['parentId']))" | |
| } | |
| ] | |
| }, | |
| { | |
| "name": "hierarchy-sort", | |
| "source": "hierarchy-sort-base", | |
| "transform": [ | |
| { | |
| "type": "formula", | |
| "expr": "join(reverse(slice(pluck(treeAncestors('hierarchy-sort-base', datum['id']), 'quantitySortPadded'),1)), ',') + ',' + datum.quantitySortPadded", | |
| "as": "hierSortKey" | |
| }, | |
| { | |
| "type": "window", | |
| "ops": ["row_number"], | |
| "sort": {"field": "hierSortKey", "order": "ascending"}, | |
| "as": ["sort"] | |
| }, | |
| {"type": "collect", "sort": {"field": "sort", "order": "ascending"}} | |
| ] | |
| }, | |
| { | |
| "name": "current-node", | |
| "source": "dataset-formatted", | |
| "transform": [{"type": "filter", "expr": "datum.id === currentNodeId"}] | |
| }, | |
| { | |
| "name": "current-node-parent", | |
| "source": "dataset-formatted", | |
| "transform": [ | |
| { | |
| "type": "filter", | |
| "expr": "length(data('current-node'))>0 && isValid(data('current-node')[0].parentId) ? datum.id === data('current-node')[0].parentId : false" | |
| } | |
| ] | |
| }, | |
| { | |
| "name": "hierarchy-current-children", | |
| "source": "dataset-formatted", | |
| "transform": [ | |
| {"type": "filter", "expr": "datum.parentId === currentNodeId"}, | |
| { | |
| "type": "collect", | |
| "sort": {"field": "quantity", "order": {"signal": "sortOrder"}} | |
| } | |
| ] | |
| }, | |
| { | |
| "name": "hierarchy-current-view", | |
| "on": [ | |
| {"trigger": "true", "remove": true}, | |
| { | |
| "trigger": "true", | |
| "insert": "length(data('current-node-parent'))>0 ? [{id: '__back__', parentId: currentNodeId, label: '⬅ Back', quantity: 0, __type: 'back'}] : []" | |
| }, | |
| {"trigger": "true", "insert": "data('hierarchy-current-children')"} | |
| ], | |
| "transform": [ | |
| { | |
| "type": "formula", | |
| "expr": "indexof(pluck(data('dataset-formatted'), 'parentId'), datum['id'])>=0", | |
| "as": "hasChildren" | |
| }, | |
| { | |
| "type": "collect", | |
| "sort": {"field": "quantity", "order": "descending"} | |
| } | |
| ] | |
| }, | |
| { | |
| "name": "hierarchy-current-view-source", | |
| "on": [ | |
| { | |
| "trigger": "timer<(initialTimestamp+500)", | |
| "remove": true, | |
| "insert": "data('hierarchy-current-view')" | |
| }, | |
| { | |
| "trigger": "animStartTick", | |
| "remove": true, | |
| "insert": "data('hierarchy-current-view')" | |
| } | |
| ] | |
| }, | |
| { | |
| "name": "hierarchy-current-view-target", | |
| "source": "hierarchy-current-view" | |
| }, | |
| { | |
| "name": "hierarchy-current-pre-animation", | |
| "source": "hierarchy-current-view", | |
| "transform": [ | |
| { | |
| "type": "lookup", | |
| "from": "hierarchy-current-view-source", | |
| "key": "id", | |
| "fields": ["id"], | |
| "as": ["sourceRow"] | |
| }, | |
| { | |
| "type": "lookup", | |
| "from": "hierarchy-current-view", | |
| "key": "id", | |
| "fields": ["id"], | |
| "as": ["targetRow"] | |
| }, | |
| { | |
| "type": "filter", | |
| "expr": "isValid(datum.sourceRow) || isValid(datum.targetRow)" | |
| }, | |
| { | |
| "type": "formula", | |
| "expr": "isValid(datum.sourceRow) && !isValid(datum.targetRow) ? 'exit' : (!isValid(datum.sourceRow) && isValid(datum.targetRow) ? 'enter' : 'stay')", | |
| "as": "join" | |
| }, | |
| { | |
| "type": "window", | |
| "ops": ["row_number"], | |
| "sort": {"field": "quantity", "order": {"signal": "sortOrder"}}, | |
| "as": ["targetIndexTmp"] | |
| }, | |
| { | |
| "type": "formula", | |
| "expr": "(datum.targetIndexTmp - 1) * configRow.rowHeight", | |
| "as": "targetY1" | |
| }, | |
| { | |
| "type": "formula", | |
| "expr": "isValid(datum.sourceRow) && isValid(datum.sourceRow.y1) ? datum.sourceRow.y1 : datum.targetY1", | |
| "as": "sourceY1" | |
| }, | |
| { | |
| "type": "formula", | |
| "expr": "isValid(datum.sourceRow) ? (isValid(datum.sourceRow.opacity) ? datum.sourceRow.opacity : 1) : 0", | |
| "as": "sourceOpacity" | |
| }, | |
| { | |
| "type": "formula", | |
| "expr": "isValid(datum.targetRow) ? (isValid(datum.targetRow.opacity) ? datum.targetRow.opacity : 1) : 0", | |
| "as": "targetOpacity" | |
| }, | |
| { | |
| "type": "formula", | |
| "expr": "isValid(datum.targetRow) ? datum.targetRow.quantity : (isValid(datum.sourceRow) ? datum.sourceRow.quantity : 0)", | |
| "as": "quantity" | |
| }, | |
| { | |
| "type": "formula", | |
| "expr": "scale('scaleXSource', datum.quantity)", | |
| "as": "barWidth" | |
| }, | |
| { | |
| "type": "formula", | |
| "expr": "datum.join === 'enter' ? datum.barWidth : 0", | |
| "as": "enterWidth" | |
| }, | |
| { | |
| "type": "window", | |
| "ops": ["sum"], | |
| "fields": ["enterWidth"], | |
| "sort": {"field": "targetY1", "order": "ascending"}, | |
| "as": ["enterWidthCum"] | |
| }, | |
| { | |
| "type": "formula", | |
| "expr": "datum.join === 'enter' ? datum.enterWidthCum - datum.barWidth : 0", | |
| "as": "x1First" | |
| }, | |
| { | |
| "type": "formula", | |
| "expr": "datum.join === 'enter' ? datum.enterWidthCum : datum.barWidth", | |
| "as": "x2First" | |
| }, | |
| { | |
| "type": "formula", | |
| "expr": "datum.join === 'exit' ? datum.x1First : 0", | |
| "as": "x1Second" | |
| }, | |
| { | |
| "type": "formula", | |
| "expr": "datum.join === 'exit' ? datum.x2First : scale('scaleX', datum.quantity)", | |
| "as": "x2Second" | |
| }, | |
| { | |
| "type": "window", | |
| "ops": ["row_number"], | |
| "sort": {"field": "quantity", "order": {"signal": "sortOrder"}}, | |
| "as": ["sort"] | |
| }, | |
| {"type": "collect", "sort": {"field": "sort", "order": "ascending"}} | |
| ] | |
| }, | |
| { | |
| "name": "height", | |
| "values": [{"height": null}], | |
| "transform": [ | |
| { | |
| "type": "formula", | |
| "expr": "length(data('hierarchy-current-view')) * configRow.rowHeight", | |
| "as": "height" | |
| } | |
| ] | |
| }, | |
| { | |
| "name": "height-animation", | |
| "values": [{"height": null, "timestamp": null}], | |
| "on": [ | |
| { | |
| "trigger": "animStartTick", | |
| "insert": "{height: data('height')[0].height, timestamp: now()}", | |
| "remove": false | |
| } | |
| ], | |
| "transform": [ | |
| { | |
| "type": "formula", | |
| "expr": "datum.height || data('height')[0].height", | |
| "as": "height" | |
| }, | |
| { | |
| "type": "window", | |
| "ops": ["row_number"], | |
| "sort": {"field": "timestamp", "order": "descending"}, | |
| "as": ["index"] | |
| }, | |
| { | |
| "type": "window", | |
| "ops": ["lead"], | |
| "fields": ["height"], | |
| "sort": {"field": "index"}, | |
| "as": ["previousHeight"] | |
| }, | |
| {"type": "filter", "expr": "(datum.height !== datum.previousHeight)"}, | |
| { | |
| "type": "formula", | |
| "expr": "datum.previousHeight || datum.height", | |
| "as": "previousHeight" | |
| }, | |
| { | |
| "type": "window", | |
| "ops": ["row_number"], | |
| "sort": {"field": "index", "order": "ascending"}, | |
| "as": ["index"] | |
| }, | |
| {"type": "filter", "expr": "datum.index === 1"}, | |
| {"type": "formula", "expr": "datum.timestamp || now()", "as": "start"}, | |
| { | |
| "type": "formula", | |
| "expr": "datum.start + configAnimationDuration.nodeExpandCollapse", | |
| "as": "end" | |
| }, | |
| { | |
| "type": "formula", | |
| "expr": "timer <= datum.end ? clamp((timer-datum.start)/(datum.end-datum.start), 0,1) : 1", | |
| "as": "t" | |
| }, | |
| { | |
| "type": "formula", | |
| "expr": "datum.t < 0.5 ? 4 * pow(datum.t, 3) : 1 - pow(-2 * datum.t + 2, 3) / 2", | |
| "as": "tEased" | |
| }, | |
| { | |
| "type": "formula", | |
| "expr": "datum.previousHeight+(datum.height-datum.previousHeight)*datum.tEased", | |
| "as": "animatedHeight" | |
| } | |
| ] | |
| }, | |
| { | |
| "name": "hierarchy-animation-bounds", | |
| "values": [{"start": 0, "end": -1}], | |
| "on": [ | |
| { | |
| "trigger": "animStartTick", | |
| "insert": "{start: animStartTick, end: animStartTick+configAnimationDuration.nodeExpandCollapse}", | |
| "remove": true | |
| } | |
| ], | |
| "transform": [] | |
| }, | |
| { | |
| "name": "hierarchy-animation", | |
| "source": "hierarchy-current-pre-animation", | |
| "transform": [ | |
| { | |
| "type": "lookup", | |
| "from": "dataset-formatted", | |
| "key": "id", | |
| "fields": ["id"], | |
| "values": ["name", "childQuantity", "descendantQuantity", "label"] | |
| }, | |
| { | |
| "type": "formula", | |
| "expr": "rowAnimationFirst.active ? datum.x1First : lerp([datum.x1First, datum.x1Second], rowAnimationTEasedSecond)", | |
| "as": "x1" | |
| }, | |
| { | |
| "type": "formula", | |
| "expr": "rowAnimationFirst.active ? datum.x2First : lerp([datum.x2First, datum.x2Second], rowAnimationTEasedSecond)", | |
| "as": "x2" | |
| }, | |
| { | |
| "type": "formula", | |
| "expr": "datum.join === 'exit' ? datum.sourceY1 : lerp([datum.sourceY1, datum.targetY1], rowAnimationTEasedFirst)", | |
| "as": "y1" | |
| }, | |
| {"type": "formula", "expr": "datum.y1+configRow.rowHeight", "as": "y2"}, | |
| {"type": "formula", "expr": "scrollY+datum.y2", "as": "bufferHeight"}, | |
| { | |
| "type": "filter", | |
| "expr": "datum.bufferHeight <= (adjustedHeight+configRow.rowHeight) && (datum.bufferHeight) >= 0" | |
| }, | |
| { | |
| "type": "formula", | |
| "expr": "datum.y1+configRow.rowHeight*(0.225)", | |
| "as": "y1WithPadding" | |
| }, | |
| { | |
| "type": "formula", | |
| "expr": "datum.y2-configRow.rowHeight*(0.225)", | |
| "as": "y2WithPadding" | |
| }, | |
| { | |
| "type": "formula", | |
| "expr": "datum.join === 'exit' ? lerp([datum.sourceOpacity, 0], rowAnimationTEasedFirst) : lerp([datum.sourceOpacity, datum.targetOpacity], rowAnimationTEasedFirst)*0.35", | |
| "as": "opacity" | |
| }, | |
| { | |
| "type": "formula", | |
| "expr": "datum.join === 'exit' ? lerp([datum.sourceOpacity, 0], rowAnimationTEasedFirst) : lerp([datum.sourceOpacity, datum.targetOpacity], rowAnimationTEasedFirst)", | |
| "as": "fullOpacity" | |
| }, | |
| {"type": "filter", "expr": "datum.opacity>0"}, | |
| {"type": "collect", "sort": {"field": "sort"}} | |
| ] | |
| }, | |
| { | |
| "name": "xscale-target-bounds", | |
| "source": "hierarchy-current-view", | |
| "transform": [ | |
| { | |
| "type": "aggregate", | |
| "ops": ["max"], | |
| "fields": ["quantity"], | |
| "as": ["maxQuantity"] | |
| } | |
| ] | |
| } | |
| ] | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment