Skip to content

Instantly share code, notes, and snippets.

@Giammaria
Last active November 3, 2025 22:31
Show Gist options
  • Select an option

  • Save Giammaria/a4fdb5e3aebc15c6b276fb956465bb91 to your computer and use it in GitHub Desktop.

Select an option

Save Giammaria/a4fdb5e3aebc15c6b276fb956465bb91 to your computer and use it in GitHub Desktop.
20251031_hierarchical_bar_chart_v_v1.0
{
"$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