Skip to content

Instantly share code, notes, and snippets.

@Giammaria
Last active November 3, 2025 13:19
Show Gist options
  • Save Giammaria/a4fdb5e3aebc15c6b276fb956465bb91 to your computer and use it in GitHub Desktop.
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: 500, 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": "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},
{
"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": "animStartTick",
"init": "0",
"on": [
{
"events": {"signal": "sortChangeCount"},
"update": "!isAnimating ? timer : animStartTick"
},
{
"events": "@node-clickable-rect:pointerup, @y-axis-label-clickable-rect:pointerup",
"update": "!isAnimating && datum.hasChildren ? timer : animStartTick"
},
{
"events": "@expand-all-collapse-all-button-background:pointerdown{0,0}",
"update": "rowAnimationTEased === 1 ? timer : animStartTick"
}
]
},
{
"name": "animateCount",
"init": "0",
"on": [
{
"events": {"signal": "sortChangeCount"},
"update": "isAnimating ? animateCount : animateCount + 1"
},
{
"events": {"signal": "nodeClickedDatum"},
"update": "isAnimating || !isValid(nodeClickedDatum) || !nodeClickedDatum.datum.hasChildren ? animateCount : animateCount + 1"
}
]
},
{
"name": "rowAnimation",
"update": "!isValid(interactionType) ? rowAnimation : {active: (timer < (animStartTick + (slice(interactionType, 0, 4)=== 'sort' ? configAnimationDuration.sort : configAnimationDuration.nodeExpandCollapse))), 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": "timer",
"init": "now()",
"on": [{"events": {"type": "timer"}, "update": "now()"}]
},
{"name": "initialTimestamp", "init": "now()"},
{
"name": "nodeClickedDatum",
"init": "null",
"on": [
{
"events": "@node-clickable-rect:click, @y-axis-label-clickable-rect:click",
"update": "rowAnimationTEased === 1 ? 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": "min((isPowerBIVisual ? (containerSize()[1] || windowSize()[1]) : configDesiredChartHeight), actualHeight)"
},
{
"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": {
"tooltip": {"signal": "datum.sourceValues"},
"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 ? 1 : 0.65 : 1"
}
}
}
}
]
}
]
},
{
"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": "sortOrderDescending ? lerp([-180, 0], rowAnimationTEased) : lerp([0, -180], rowAnimationTEased)"
},
{"name": "x", "update": "4"},
{
"name": "dy",
"update": "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"}
}
]
},
{
"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": {
"x": {"value": 0},
"x2": {
"signal": "scale('scaleX', datum.descendantQuantity)"
},
"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": {"value": 0},
"x2": {
"signal": "scale('scaleX', datum.descendantQuantity)"
},
"y": {"field": "y1WithPadding"},
"y2": {"field": "y2WithPadding"},
"fill": {"signal": "background"}
}
}
},
{
"name": "node-rect",
"type": "rect",
"from": {"data": "hierarchy-animation"},
"interactive": false,
"encode": {
"update": {
"x": {"value": 0},
"x2": {
"signal": "scale('scaleX', datum.descendantQuantity)"
},
"y": {"field": "y1WithPadding"},
"y2": {"field": "y2WithPadding"},
"fill": {
"signal": "datum.hasChildren ? configColorScheme.bars.parent.fill : configColorScheme.bars.parent.leaf.fill"
},
"fillOpacity": {
"signal": "datum.hasChildren ? configColorScheme.bars.parent.fillOpacity : configColorScheme.bars.parent.leaf.fillOpacity"
},
"stroke": {
"signal": "datum.hasChildren ? configColorScheme.bars.parent.stroke : configColorScheme.bars.parent.leaf.stroke"
},
"strokeOpacity": {
"signal": "datum.hasChildren ? configColorScheme.bars.parent.strokeOpacity : configColorScheme.bars.parent.leaf.strokeOpacity"
},
"opacity": {
"signal": "isValid(nodeMouseOverDatum) ? nodeMouseOverDatum.id === datum.id ? 1 : 0.65 : 1"
}
}
}
},
{
"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": {"data": "hierarchy-visible", "field": "descendantQuantity"},
"range": {"signal": "[0, width-configAxes.y.width]"},
"clamp": 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": "stratify", "key": "id", "parentKey": "parentId"},
{
"type": "formula",
"expr": "{id: datum.id, parentId: datum.parentId}",
"as": "idObj"
}
]
},
{
"name": "hierarchy-initial",
"source": "dataset-formatted",
"transform": [
{
"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": "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 || datum.id === datum.immediateChildrenIds.parentId"
},
{
"type": "aggregate",
"ops": ["values"],
"fields": ["idObs"],
"groupby": [
"id",
"parentId",
"name",
"label",
"childQuantity",
"descendantQuantity",
"level",
"hasChildren",
"ancestorIds"
],
"as": ["immediateChildrenIds"]
},
{
"type": "formula",
"expr": "pluck(slice(pluck(datum.immediateChildrenIds, 'immediateChildrenIds'), 1), 'id')",
"as": "immediateChildrenIds"
},
{
"type": "formula",
"expr": "datum.level === 1 ? datum.id : length(datum.ancestorIds) === 0 ? null : length(datum.ancestorIds) === 1 ? datum.ancestorIds[0] : slice(datum.ancestorIds,-2)[1-1]",
"as": "firstAncestorId"
},
{"type": "formula", "expr": "null", "as": "isExpanded"},
{
"type": "formula",
"expr": "!datum.hasChildren ? null : isInitial ? datum.level < 1 ? 1 : 0 : datum.isExpanded",
"as": "isExpanded"
},
{
"type": "formula",
"expr": "!datum.hasChildren ? null : interactionTypeHistory[0] === 'expandAll' ? 1 : interactionTypeHistory[0] === 'collapseAll' ? 0 : datum.isExpanded",
"as": "isExpanded"
},
{
"type": "formula",
"expr": "!datum.hasChildren ? null : isValid(nodeClickedDatum) && nodeClickedDatum.datum.id === datum.id ? nodeClickedDatum.datum.isExpanded === 0 ? 1 : 0 : datum.isExpanded",
"as": "isExpanded"
},
{
"type": "formula",
"expr": "interactionTypeHistory[0] || 'sort '+ (sortOrder)",
"as": "type"
}
]
},
{
"name": "hierarchy-current-parent-collapsed-candidates",
"source": "hierarchy-initial",
"transform": [
{"type": "filter", "expr": "datum.isExpanded === 0"},
{"type": "filter", "expr": "datum.hasChildren"},
{"type": "project", "fields": ["id", "name"]}
]
},
{
"name": "hierarchy-current-parent-expanded-candidates",
"source": "hierarchy-initial",
"transform": [
{"type": "filter", "expr": "datum.isExpanded === 1"},
{"type": "filter", "expr": "datum.hasChildren"},
{"type": "project", "fields": ["id", "name"]}
]
},
{
"name": "hierarchy-visible",
"source": "hierarchy-initial",
"transform": [
{
"type": "lookup",
"from": "hierarchy-initial",
"key": "parentId",
"fields": ["id"],
"values": ["isExpanded"],
"as": ["parentIsExpanded"]
},
{
"type": "formula",
"expr": "pluck(data('hierarchy-current-parent-collapsed-candidates'), 'id')",
"as": "collapsedIds"
},
{
"type": "formula",
"expr": "pluck(data('hierarchy-current-parent-expanded-candidates'), 'id')",
"as": "expandedIds"
},
{
"type": "filter",
"expr": "!test('(\\\\b\\\\d+\\\\b).*\\\\b\\\\1\\\\b', (join(datum.ancestorIds, ',')+','+join(datum.collapsedIds, ','))) || (datum.parentIsExpanded && indexof(datum.expandedIds , datum.parentId) >= 0)"
},
{
"type": "formula",
"expr": "1 >= datum.level || indexof(pluck(data('hierarchy-current-parent-expanded-candidates'), 'id'), datum.parentId) >= 0",
"as": "isVisible"
},
{"type": "filter", "expr": "datum.isVisible"},
{
"type": "collect",
"sort": {"field": "sort", "order": {"signal": "sortOrder"}}
},
{
"type": "window",
"ops": ["row_number"],
"as": ["index"],
"sort": {"field": "sort", "order": {"signal": "sortOrder"}}
},
{
"type": "joinaggregate",
"ops": ["min"],
"fields": ["index"],
"as": ["minIndex"]
},
{
"type": "formula",
"expr": "datum.index-datum.minIndex",
"as": "index"
},
{
"type": "formula",
"expr": "(datum.index)*configRow.rowHeight",
"as": "y1"
},
{
"type": "formula",
"expr": "isInitial && datum.level <= 1 ? 1 : null",
"as": "opacity"
}
]
},
{
"name": "hierarchy-last-visible-target",
"on": [
{
"trigger": "nodeClickStart",
"remove": false,
"insert": "{data: data('hierarchy-visible-target'), timestamp: now(), source: 'hierarchy-visible-target'}"
},
{
"trigger": "sortChangeCount",
"remove": false,
"insert": "{data: data('hierarchy-visible-target'), timestamp: now(), source: 'hierarchy-visible-target'}"
},
{
"trigger": "timer < (initialTimestamp + 1000)",
"remove": false,
"insert": "{data: data('hierarchy-initial'), timestamp: now(), source: 'hierarchy-initial'}"
}
],
"transform": [
{
"type": "window",
"ops": ["row_number"],
"sort": { "field": "timestamp", "order": "descending" },
"as": ["index"]
},
{
"type": "filter",
"expr": "datum.index === 1"
},
{
"type": "flatten",
"fields": ["data"]
},
{
"type": "filter",
"expr": "isValid(datum.data)"
},
/* promote everything downstream expects */
{ "type": "formula", "expr": "datum.data.id", "as": "id" },
{ "type": "formula", "expr": "datum.data.parentId", "as": "parentId" },
{ "type": "formula", "expr": "datum.data.name", "as": "name" },
{ "type": "formula", "expr": "datum.data.level", "as": "level" },
{ "type": "formula", "expr": "datum.data.hasChildren", "as": "hasChildren" },
{ "type": "formula", "expr": "datum.data.ancestorIds", "as": "ancestorIds" },
{ "type": "formula", "expr": "datum.data.immediateChildrenIds", "as": "immediateChildrenIds" },
{ "type": "formula", "expr": "datum.data.isExpanded", "as": "isExpanded" },
{ "type": "formula", "expr": "datum.data.isVisible", "as": "isVisible" },
/* sorting / positioning fields that the animation lookup uses */
{ "type": "formula", "expr": "datum.data.sort", "as": "sort" },
{ "type": "formula", "expr": "datum.data.sortOverall", "as": "sortOverall" },
{ "type": "formula", "expr": "datum.data.y1", "as": "y1" },
{ "type": "formula", "expr": "datum.data.opacity", "as": "opacity" },
/* keep quantities since they're in the original hierarchy */
{ "type": "formula", "expr": "datum.data.childQuantity", "as": "childQuantity" },
{ "type": "formula", "expr": "datum.data.descendantQuantity", "as": "descendantQuantity" },
/* keep index/animateCount if present on the target */
{ "type": "formula", "expr": "datum.data.index", "as": "index" },
{ "type": "formula", "expr": "datum.data.animateCount", "as": "animateCount" }
]
}
,
{
"name": "hierarchy-visible-source",
"on": [
{
"trigger": "data('hierarchy-last-visible-target')",
"remove": true,
"insert": "data('hierarchy-last-visible-target')"
}
],
"transform": []
},
{
"name": "height",
"source": "hierarchy-visible",
"transform": [
{"type": "aggregate", "ops": ["count"], "as": ["height"]},
{
"type": "formula",
"expr": "(datum.height)*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-visible-target",
"source": "hierarchy-initial",
"transform": [
{
"type": "lookup",
"from": "hierarchy-visible",
"key": "id",
"fields": ["id"],
"as": ["visibleValues"]
},
{
"type": "formula",
"expr": "isValid(datum.visibleValues)",
"as": "isVisible"
},
{
"type": "window",
"ops": ["row_number"],
"groupby": ["level"],
"sort": {
"field": "descendantQuantity",
"order": {"signal": "sortOrder"}
},
"as": ["sort"]
},
{
"type": "formula",
"expr": "slice('000000'+datum.level, -6)+'_'+slice('000000'+datum.sort, -6)",
"as": "sortOverall"
},
{
"type": "window",
"ops": ["row_number"],
"sort": {"field": "sortOverall", "order": {"signal": "sortOrder"}},
"as": ["sortOverall"]
},
{
"type": "collect",
"sort": {"field": "sortOverall", "order": {"signal": "sortOrder"}}
},
{
"type": "formula",
"expr": "(datum.sort-1)*configRow.rowHeight",
"as": "y1"
},
{
"type": "window",
"ops": ["max"],
"fields": ["y1"],
"frame": [null, 0],
"sort": {"field": "sort"},
"as": ["y1"]
},
{
"type": "formula",
"expr": "(datum.level <= 1 || datum.isVisible) ? 1 : 0",
"as": "opacity"
},
{
"type": "window",
"ops": ["dense_rank"],
"as": ["index"],
"sort": {"field": "sort", "order": {"signal": "sortOrder"}}
},
{"type": "formula", "expr": "animateCount", "as": "animateCount"},
{
"type": "collect",
"sort": {"field": "sortOverall", "order": {"signal": "sortOrder"}}
}
]
},
{
"name": "hierarchy-pre-animation",
"on": [
{
"trigger": "animateCount",
"insert": "data('hierarchy-initial')",
"remove": true
},
{
"trigger": "timer < (initialTimestamp+1000)",
"remove": true,
"insert": "data('hierarchy-initial')"
}
],
"transform": [
{
"type": "lookup",
"key": "id",
"fields": ["id"],
"from": "hierarchy-visible-source",
"as": ["sourceValues"]
},
{
"type": "lookup",
"key": "id",
"fields": ["id"],
"from": "hierarchy-visible-target",
"as": ["targetValues"]
},
{
"type": "filter",
"expr": "isValid(datum.sourceValues) || isValid(datum.targetValues)"
},
{
"type": "formula",
"as": "sourceOpacity",
"expr": "isValid(datum.sourceValues) ? datum.sourceValues.opacity : 0"
},
{
"type": "formula",
"as": "targetOpacity",
"expr": "isValid(datum.targetValues) ? datum.targetValues.opacity : 0"
},
{
"type": "formula",
"as": "sourceY1",
"expr": "isValid(datum.sourceValues) ? datum.sourceValues.y1 : (isValid(datum.targetValues) ? datum.targetValues.collapseAllY1 : null)"
},
{
"type": "formula",
"as": "targetY1",
"expr": "isValid(datum.targetValues) ? datum.targetValues.y1 : (isValid(datum.sourceValues) ? datum.sourceValues.collapseAllY1 : null)"
},
{
"type": "filter",
"expr": "isValid(datum.sourceY1) && isValid(datum.targetY1)"
},
{
"type": "formula",
"expr": "datum.targetValues.isExpanded",
"as": "isExpanded"
},
{"type": "collect", "sort": {"field": "sort"}}
]
},
{
"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-pre-animation",
"transform": [
{
"type": "lookup",
"from": "dataset-formatted",
"key": "id",
"fields": ["id"],
"values": ["name", "childQuantity", "descendantQuantity", "label"]
},
{
"type": "formula",
"expr": "scale('scaleXGanttTimeSeries', datum.startDate)",
"as": "x1"
},
{
"type": "formula",
"expr": "scale('scaleXGanttTimeSeries', datum.endDate)",
"as": "x2"
},
{
"type": "formula",
"expr": "scale('scaleXGanttTimeSeries', span([datum.startDate, datum.endDate])*datum.decimalPercentComplete+datum.startDate)",
"as": "x2Progress"
},
{
"type": "formula",
"expr": "lerp([datum.sourceValues.sort, datum.targetValues.sort], rowAnimationTEased)",
"as": "sort"
},
{
"type": "formula",
"expr": "(datum.sort-1)*configRow.rowHeight",
"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": "lerp([datum.sourceOpacity, datum.targetOpacity], rowAnimationTEased)*0.35",
"as": "opacity"
},
{
"type": "formula",
"expr": "lerp([datum.sourceOpacity, datum.targetOpacity], rowAnimationTEased)",
"as": "fullOpacity"
},
{"type": "filter", "expr": "datum.opacity>0"},
{"type": "collect", "sort": {"field": "sort"}}
]
}
]
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment