Saturday, 30 August 2025

Box visual fixed v3

{

  "$schema": "https://vega.github.io/schema/vega/v5.json",

  "description": "Zoomable, collapsable tree by David Bacci: https://www.linkedin.com/in/davbacci/",

  "width": {"signal": "1240"},

  "height": {"signal": "600"},

  "background": "#f5f5f5",

  "autosize": "pad",

  "padding": 5,

  "signals": [

    {

      "name": "slicerSelection",

      "value": null,

      "update": "data('dataset') && length(data('dataset')) > 0 ? data('dataset')[0]['selected_path'] : null"

    },

    {

      "name": "slicerHighlightPath",

      "value": [],

      "update": "slicerSelection ? pluck(treeAncestors('treeCalcs', slicerSelection), 'id') : []"

    },

    {

      "name": "slicerParentPath",

      "value": [],

      "update": "slicerSelection ? slice(slicerHighlightPath, 0, -1) : []"

    },

    {

      "name": "slicerDescendantsPath",

      "value": [],

      "update": "slicerSelection ? pluck(data('slicerDescendantsData'), 'id') : []"

    },

    {"name": "nodeWidth", "value": 190},

    {"name": "nodeHeight", "value": 45},

    {

      "name": "verticalNodeGap",

      "value": 10

    },

    {

      "name": "horizontalNodeGap",

      "value": 140

    },

    {

      "name": "startingDepth",

      "value": 8,

      "on": [

        {

          "events": {

            "type": "timer",

            "throttle": 0

          },

          "update": "-1"

        }

      ]

    },

    {

      "name": "node",

      "value": 0,

      "on": [

        {

          "events": {

            "type": "click",

            "markname": "node"

          },

          "update": "datum.id"

        },

        {

          "events": {

            "type": "timer",

            "throttle": 10

          },

          "update": "0"

        }

      ]

    },

    {

      "name": "nodeHighlight",

      "value": "[0]",

      "on": [

        {

          "events": {

            "type": "mouseover",

            "markname": "node"

          },

          "update": "pluck(treeAncestors('treeCalcs', datum.id), 'id')"

        },

        {

          "events": {

            "type": "mouseout"

          },

          "update": "[0]"

        }

      ]

    },

    {

      "name": "isExpanded",

      "value": 0,

      "on": [

        {

          "events": {

            "type": "click",

            "markname": "node"

          },

          "update": "datum.children > 0 && indata('treeClickStorePerm', 'id', datum.childrenIds[0])?true:false"

        }

      ]

    },

    {

      "name": "xrange",

      "update": "[0, width]"

    },

    {

      "name": "yrange",

      "update": "[0, height]"

    },

    {

      "name": "down",

      "value": null,

      "on": [

        {

          "events": "touchend",

          "update": "null"

        },

        {

          "events": "mousedown, touchstart",

          "update": "xy()"

        }

      ]

    },

    {

      "name": "xcur",

      "value": null,

      "on": [

        {

          "events": "mousedown, touchstart, touchend",

          "update": "slice(xdom)"

        }

      ]

    },

    {

      "name": "ycur",

      "value": null,

      "on": [

        {

          "events": "mousedown, touchstart, touchend",

          "update": "slice(ydom)"

        }

      ]

    },

    {

      "name": "delta",

      "value": [0, 0],

      "on": [

        {

          "events": [

            {

              "source": "window",

              "type": "mousemove",

              "consume": true,

              "between": [

                {"type": "mousedown"},

                {

                  "source": "window",

                  "type": "mouseup"

                }

              ]

            },

            {

              "type": "touchmove",

              "consume": true,

              "filter": "event.touches.length === 1"

            }

          ],

          "update": "down ? [down[0]-x(), down[1]-y()] : [0,0]"

        }

      ]

    },

    {

      "name": "anchor",

      "value": [0, 0],

      "on": [

        {

          "events": "wheel",

          "update": "[invert('xscale', x()), invert('yscale', y())]"

        },

        {

          "events": {

            "type": "touchstart",

            "filter": "event.touches.length===2"

          },

          "update": "[(xdom[0] + xdom[1]) / 2, (ydom[0] + ydom[1]) / 2]"

        }

      ]

    },

    {

      "name": "zoom",

      "value": 1,

      "on": [

        {

          "events": "wheel!",

          "force": true,

          "update": "pow(1.001, event.deltaY * pow(16, event.deltaMode))"

        },

        {

          "events": {"signal": "dist2"},

          "force": true,

          "update": "dist1 / dist2"

        }

      ]

    },

    {

      "name": "dist1",

      "value": 0,

      "on": [

        {

          "events": {

            "type": "touchstart",

            "filter": "event.touches.length===2"

          },

          "update": "pinchDistance(event)"

        },

        {

          "events": {"signal": "dist2"},

          "update": "dist2"

        }

      ]

    },

    {

      "name": "dist2",

      "value": 0,

      "on": [

        {

          "events": {

            "type": "touchmove",

            "consume": true,

            "filter": "event.touches.length===2"

          },

          "update": "pinchDistance(event)"

        }

      ]

    },

    {

      "name": "initialXDom",

      "update": "data('treeExtents') && length(data('treeExtents')) > 0 ? [data('treeExtents')[0].paddedMinX, data('treeExtents')[0].paddedMaxX] : [0, width]"

    },

    {

      "name": "initialYDom",

      "update": "data('treeExtents') && length(data('treeExtents')) > 0 ? [data('treeExtents')[0].paddedMinY, data('treeExtents')[0].paddedMaxY] : [0, height]"

    },

    {

      "name": "xdom",

      "update": "initialXDom",

      "on": [

        {

          "events": {"signal": "delta"},

          "update": "[xcur[0] + span(xcur) * delta[0] / width, xcur[1] + span(xcur) * delta[0] / width]"

        },

        {

          "events": {"signal": "zoom"},

          "update": "[anchor[0] + (xdom[0] - anchor[0]) * zoom, anchor[0] + (xdom[1] - anchor[0]) * zoom]"

        },

        {

          "events": "dblclick",

          "update": "initialXDom"

        }

      ]

    },

    {

      "name": "ydom",

      "update": "initialYDom",

      "on": [

        {

          "events": {"signal": "delta"},

          "update": "[ycur[0] + span(ycur) * delta[1] / height, ycur[1] + span(ycur) * delta[1] / height]"

        },

        {

          "events": {"signal": "zoom"},

          "update": "[anchor[1] + (ydom[0] - anchor[1]) * zoom, anchor[1] + (ydom[1] - anchor[1]) * zoom]"

        },

        {

          "events": "dblclick",

          "update": "initialYDom"

        }

      ]

    },

    {

      "name": "scaledNodeWidth",

      "update": "(nodeWidth/ span(xdom))*width"

    },

    {

      "name": "scaledNodeHeight",

      "update": "abs(nodeHeight/ span(ydom))*height"

    },

    {

      "name": "scaledFont13",

      "update": "(13/ span(xdom))*width"

    },

    {

      "name": "scaledFont12",

      "update": "(12/ span(xdom))*width"

    },

    {

      "name": "scaledFont11",

      "update": "(11/ span(xdom))*width"

    },

    {

      "name": "scaledKPIHeight",

      "update": "(5/ span(xdom))*width"

    },

    {

      "name": "scaledLimit",

      "update": "(20/ span(xdom))*width"

    }

  ],

  "data": [

    {"name": "dataset"},

    {"name": "source",

      "source": "dataset"

    },

    {

      "name": "wideToTall",

      "source": "source",

      "transform": [

        {

          "type": "formula",

          "expr": "{key: datum.level1,parent: null, person:datum.person, kpi:datum.kpi}",

          "as": "l1"

        },

        {

          "type": "formula",

          "expr": "{key: datum.level1+ '/'+datum.level2,parent: datum.level1, person:datum.person, kpi:datum.kpi}",

          "as": "l2"

        },

        {

          "type": "formula",

          "expr": "{key:datum.level1 + '/'+datum.level2+ '/'+datum.level3,parent: datum.level1+ '/'+datum.level2, person:datum.person, kpi:datum.kpi}",

          "as": "l3"

        },

        {

          "type": "formula",

          "expr": "{key:datum.level1 + '/'+datum.level2+ '/'+datum.level3+ '/'+ datum.level4,parent: datum.level1 + '/'+datum.level2+ '/'+datum.level3, person:datum.person, kpi:datum.kpi}",

          "as": "l4"

        },

        {

          "type": "formula",

          "expr": "{key:datum.level1 + '/'+datum.level2+ '/'+datum.level3+ '/'+ datum.level4+ '/'+ datum.level5,parent: datum.level1 + '/'+datum.level2+ '/'+datum.level3+ '/'+ datum.level4, person:datum.person, kpi:datum.kpi}",

          "as": "l5"

        },

        {

          "type": "formula",

          "expr": "{key:datum.level1 + '/'+datum.level2+ '/'+datum.level3+ '/'+ datum.level4+ '/'+ datum.level5 + '/'+ datum.level6,parent: datum.level1 + '/'+datum.level2+ '/'+datum.level3+ '/'+ datum.level4 + '/'+ datum.level5, person:datum.person, kpi:datum.kpi}",

          "as": "l6"

        },

        {

          "type": "formula",

          "expr": "{key:datum.level1 + '/'+datum.level2+ '/'+datum.level3+ '/'+ datum.level4+ '/'+ datum.level5 + '/'+ datum.level6+ '/'+ datum.level7,parent: datum.level1 + '/'+datum.level2+ '/'+datum.level3+ '/'+ datum.level4 + '/'+ datum.level5+ '/'+ datum.level6, person:datum.person, kpi:datum.kpi}",

          "as": "l7"

        },

        {

          "type": "fold",

          "fields": [

            "l1",

            "l2",

            "l3",

            "l4",

            "l5",

            "l6",

            "l7"

          ]

        },

        {

          "type": "project",

          "fields": ["key", "value"]

        },

        {

          "type": "formula",

          "expr": "datum.value.key",

          "as": "id"

        },

        {

          "type": "formula",

          "expr": "reverse(split(datum.value.key,'/'))[0]",

          "as": "title"

        },

        {

          "type": "formula",

          "expr": "datum.value.parent",

          "as": "parent"

        },

        {

          "type": "filter",

          "expr": "datum.title && trim(datum.title) != '' && datum.title != 'null' && datum.title != 'undefined'"

        },

        {

          "type": "aggregate",

          "groupby": [

            "id",

            "parent",

            "title",

            "value"

          ]

        },

        {

          "type": "formula",

          "expr": "datum.value.person",

          "as": "person"

        },

        {

          "type": "formula",

          "expr": "datum.value.kpi",

          "as": "kpi"

        }

      ]

    },

    {

      "name": "treeCalcs",

      "source": "wideToTall",

      "transform": [

        {

          "type": "stratify",

          "key": "id",

          "parentKey": "parent"

        },

        {

          "type": "tree",

          "method": {

            "signal": "'tidy'"

          },

          "separation": {

            "signal": "false"

          },

          "as": [

            "y",

            "x",

            "depth",

            "children"

          ]

        },

        {

          "as": "parent",

          "type": "formula",

          "expr": "datum.parent"

        }

      ]

    },

    {

      "name": "slicerDescendantsData",

      "source": "treeCalcs",

      "transform": [

        {

          "type": "filter",

          "expr": "slicerSelection && indexof(pluck(treeAncestors('treeCalcs', datum.id), 'id'), slicerSelection) > -1 && datum.id !== slicerSelection"

        }

      ]

    },

    {

      "name": "treeChildren",

      "source": "treeCalcs",

      "transform": [

        {

          "type": "aggregate",

          "groupby": ["parent"],

          "fields": ["parent"],

          "ops": ["values"],

          "as": ["childrenObjects"]

        },

        {

          "type": "formula",

          "expr": "pluck(datum.childrenObjects,'id')",

          "as": "childrenIds"

        }

      ]

    },

    {

      "name": "treeAncestors",

      "source": "treeCalcs",

      "transform": [

        {

          "type": "formula",

          "as": "treeAncestors",

          "expr": "treeAncestors('treeCalcs', datum.id, 'root')"

        },

        {

          "type": "flatten",

          "fields": ["treeAncestors"]

        },

        {

          "type": "formula",

          "expr": "datum.treeAncestors.parent",

          "as": "allParents"

        }

      ]

    },

    {

      "name": "treeChildrenAll",

      "source": "treeAncestors",

      "transform": [

        {

          "type": "project",

          "fields": [

            "allParents",

            "id",

            "name",

            "parent",

            "x",

            "y",

            "depth",

            "children"

          ]

        },

        {

          "type": "aggregate",

          "fields": [

            "parent",

            "parent",

            "id"

          ],

          "ops": [

            "values",

            "count",

            "min"

          ],

          "groupby": ["allParents"],

          "as": [

            "allChildrenObjects",

            "allChildrenCount",

            "id"

          ]

        },

        {

          "type": "formula",

          "expr": "pluck(datum.allChildrenObjects,'id')",

          "as": "allChildrenIds"

        }

      ]

    },

    {

      "name": "treeClickStoreTemp",

      "source": "treeAncestors",

      "transform": [

        {

          "type": "filter",

          "expr": "startingDepth!=-1?datum.depth <= startingDepth:node !=0 && !isExpanded? datum.parent == node: node !=0 && isExpanded? datum.allParents == node:false"

        },

        {

          "type": "project",

          "fields": [

            "id",

            "name",

            "parent",

            "x",

            "y",

            "depth",

            "children"

          ]

        },

        {

          "type": "aggregate",

          "fields": ["id"],

          "ops": ["min"],

          "groupby": [

            "id",

            "name",

            "parent",

            "x",

            "y",

            "depth",

            "children"

          ]

        }

      ]

    },

    {

      "name": "treeClickStorePerm",

      "values": [],

      "on": [

        {

          "trigger": "startingDepth>=0",

          "insert": "data('treeClickStoreTemp')"

        },

        {

          "trigger": "node",

          "insert": "!isExpanded? data('treeClickStoreTemp'):false"

        },

        {

          "trigger": "node",

          "remove": "isExpanded?data('treeClickStoreTemp'):false"

        }

      ]

    },

    {

      "name": "treeLayout",

      "source": "wideToTall",

      "transform": [

        {

          "type": "filter",

          "expr": "indata('treeClickStorePerm', 'id', datum.id)"

        },

        {

          "type": "stratify",

          "key": "id",

          "parentKey": "parent"

        },

        {

          "type": "tree",

          "method": {

            "signal": "'tidy'"

          },

          "nodeSize": [

            {

              "signal": "nodeHeight+verticalNodeGap"

            },

            {

              "signal": "nodeWidth+horizontalNodeGap"

            }

          ],

          "separation": {

            "signal": "false"

          },

          "as": [

            "y",

            "x",

            "depth",

            "children"

          ]

        },

        {

          "type": "formula",

          "expr": "datum.y+(height/2)",

          "as": "y"

        },

        {

          "type": "formula",

          "expr": "scale('xscale',datum.x)",

          "as": "xscaled"

        },

        {

          "as": "parent",

          "type": "formula",

          "expr": "datum.parent"

        }

      ]

    },

    {

      "name": "treeExtents",

      "source": "treeLayout",

      "transform": [

        {

          "type": "aggregate",

          "fields": ["x", "x", "y", "y"],

          "ops": ["min", "max", "min", "max"],

          "as": ["minX", "maxX", "minY", "maxY"]

        },

        {

          "type": "formula",

          "expr": "datum.minX - nodeWidth",

          "as": "paddedMinX"

        },

        {

          "type": "formula",

          "expr": "datum.maxX + nodeWidth * 2",

          "as": "paddedMaxX"

        },

        {

          "type": "formula",

          "expr": "datum.minY - nodeHeight",

          "as": "paddedMinY"

        },

        {

          "type": "formula",

          "expr": "datum.maxY + nodeHeight",

          "as": "paddedMaxY"

        }

      ]

    },

    {

      "name": "fullTreeLayout",

      "source": "treeLayout",

      "transform": [

        {

          "type": "lookup",

          "from": "treeChildren",

          "key": "parent",

          "fields": ["id"],

          "values": [

            "childrenObjects",

            "childrenIds"

          ]

        },

        {

          "type": "lookup",

          "from": "treeChildrenAll",

          "key": "allParents",

          "fields": ["id"],

          "values": [

            "allChildrenIds",

            "allChildrenObjects"

          ]

        },

        {

          "type": "lookup",

          "from": "treeCalcs",

          "key": "id",

          "fields": ["id"],

          "values": ["children"]

        },

        {

          "type": "formula",

          "expr": "reverse(pluck(treeAncestors('treeCalcs', datum.id), 'id'))[1]",

          "as": "treeParent"

        }

      ]

    },

    {

      "name": "visibleNodes",

      "source": "fullTreeLayout",

      "transform": [

        {

          "type": "filter",

          "expr": "indata('treeClickStorePerm', 'id', datum.id)"

        }

      ]

    },

    {

      "name": "maxWidthAndHeight",

      "source": "visibleNodes",

      "transform": [

        {

          "type": "aggregate",

          "groupby": ["depth"],

          "fields": ["depth", "x", "y"],

          "ops": [

            "count",

            "max",

            "max"

          ],

          "as": ["count", "x", "y"]

        },

        {

          "type": "aggregate",

          "fields": [

            "depth",

            "count",

            "x",

            "y"

          ],

          "ops": [

            "max",

            "max",

            "max",

            "max"

          ],

          "as": [

            "maxDepth",

            "maxNodes",

            "maxX",

            "maxY"

          ]

        }

      ]

    },

    {

      "name": "links",

      "source": "treeLayout",

      "transform": [

        {"type": "treelinks"},

        {

          "type": "linkpath",

          "orient": "horizontal",

          "shape": "diagonal",

          "sourceY": {

            "expr": "scale('yscale', datum.source.y)"

          },

          "sourceX": {

            "expr": "scale('xscale', datum.source.x+nodeWidth)"

          },

          "targetY": {

            "expr": "scale('yscale', datum.target.y)"

          },

          "targetX": {

            "expr": "scale('xscale', datum.target.x)"

          }

        },

        {

          "type": "filter",

          "expr": " indata('treeClickStorePerm', 'id', datum.target.id)"

        }

      ]

    }

  ],

  "scales": [

    {

      "name": "xscale",

      "zero": false,

      "domain": {"signal": "xdom"},

      "range": {"signal": "xrange"}

    },

    {

      "name": "yscale",

      "zero": false,

      "domain": {"signal": "ydom"},

      "range": {"signal": "yrange"}

    },

    {

      "name": "kpiscale",

      "zero": false,

      "domain": [0, 100],

      "range": {

        "signal": "[0,scaledNodeWidth]"

      }

    },

    {

      "name": "colour",

      "type": "ordinal",

      "range": [

        "#6f6f6f",

        "#4472C4",

        "#3A8E50",

        "#ED7D31",

        "#a63939",

        "#6338a6",

        "#3843a6",

        "#38a695"

      ],

      "domain": {

        "data": "visibleNodes",

        "field": "treeParent"

      }

    }

  ],

  "marks": [

    {

      "type": "path",

      "interactive": false,

      "from": {"data": "links"},

      "encode": {

        "update": {

          "path": {"field": "path"},

          "StrokeWidth": {

            "signal": "slicerSelection && slicerSelection !== '' ? (indexof(slicerHighlightPath, datum.target.id) > -1 ? '#e60000' : '#d3d3d3') : scale('colour', reverse(pluck(treeAncestors('treeCalcs', datum.target.id), 'id'))[1])"

          },

          "stroke": {

            "scale": "colour",

            "signal": "reverse(pluck(treeAncestors('treeCalcs', datum.target.id), 'id'))[1]"

          }

        }

      }

    },

    {

      "name": "node",

      "description": "The parent node",

      "type": "group",

      "clip": false,

      "from": {"data": "visibleNodes"},

      "encode": {

        "update": {

          "x": {

            "field": "x",

            "scale": "xscale"

          },

          "width": {

            "signal": "scaledNodeWidth"

          },

          "yc": {

            "field": "y",

            "scale": "yscale"

          },

          "height": {

            "signal": "scaledNodeHeight"

          },

          "fill": {

            "signal": "slicerSelection && slicerSelection !== '' ? (datum.id === slicerSelection ? '#d4edda' : indexof(slicerParentPath, datum.id) > -1 ? '#f8d7da' : indexof(slicerDescendantsPath, datum.id) > -1 ? '#fff3cd' : '#ffffff') : merge(hsl(scale('colour', datum.treeParent)), {l:0.94})"

          },

          "stroke": {

            "signal": "slicerSelection && slicerSelection !== '' ? (datum.id === slicerSelection ? '#155724' : indexof(slicerParentPath, datum.id) > -1 ? '#721c24' : indexof(slicerDescendantsPath, datum.id) > -1 ? '#856404' : '#d3d3d3') : merge(hsl(scale('colour', datum.treeParent)), {l:0.79})"

          },

          "cornerRadius": {"value": 2},

          "cursor": {

            "signal": "datum.children>0?'pointer':''"

          },

          "tooltip": {"signal": ""}

        }

      },

      "marks": [

        {

          "name": "highlight",

          "description": "highlight (seems like a Vega bug as this doens't work on the group element)",

          "type": "rect",

          "interactive": false,

          "encode": {

            "update": {

              "x": {

                "signal": "item.mark.group.x1"

              },

              "y": {"signal": "0"},

              "fill": {

                "signal": "indexof(nodeHighlight, parent.id)> -1? merge(hsl(scale('colour', parent.treeParent)), {l:0.82}):0"

              },

              "stroke": {

                "signal": "indexof(nodeHighlight, parent.id)> -1? merge(hsl(scale('colour', parent.treeParent)), {l:0.79}):0"

              },

              "height": {

                "signal": "item.mark.group.height"

              },

              "width": {

                "signal": "item.mark.group.width"

              }

            }

          }

        },

        {

          "name": "KPI background",

          "description": "KPI background",

          "type": "rect",

          "interactive": false,

          "clip": true,

          "encode": {

            "update": {

              "x": {

                "signal": "item.mark.group.x1"

              },

              "y": {

                "signal": "item.mark.group.height-scaledKPIHeight"

              },

              "height": {"value": 0},

              "width": {"value": 0},

              "fill": {

                "scale": "colour",

                "signal": "parent.treeParent"

              },

              "opacity": {"value": 0.2}

            }

          }

        },

        {

          "name": "KPI",

          "description": "KPI",

          "type": "rect",

          "interactive": false,

          "clip": true,

          "encode": {

            "update": {

              "x": {

                "signal": "item.mark.group.x1"

              },

              "y": {

                "signal": "item.mark.group.height-scaledKPIHeight"

              },

              "height": {"value": 0},

              "width": {"value": 0},

              "fill": {

                "scale": "colour",

                "signal": "parent.treeParent"

              }

            }

          }

        },

        {

          "type": "text",

          "interactive": false,

          "name": "title",

          "encode": {

            "update": {

              "x": {

                "signal": "(10/ span(xdom))*width"

              },

              "y": {

                "signal": "(6/ span(xdom))*width"

              },

              "fontWeight": {

                "value": "600"

              },

              "baseline": {

                "value": "top"

              },

              "fill": {

                "scale": "colour",

                "signal": "parent.treeParent"

              },

              "text": {

                "signal": "parent.title"

              },

              "fontSize": {

                "signal": "scaledFont13"

              },

              "limit": {

                "signal": "scaledNodeWidth-scaledLimit"

              },

              "font": {

                "value": "Calibri"

              }

            }

          }

        },

        {

          "type": "text",

          "interactive": false,

          "name": "name",

          "encode": {

            "update": {

              "x": {

                "signal": "(10/ span(xdom))*width"

              },

              "y": {

                "signal": "(22/ span(xdom))*width"

              },

              "align": {

                "value": "left"

              },

              "baseline": {

                "value": "top"

              },

              "fill": {

                "signal": "'#4D4B44'"

              },

              "text": {

                "signal": "parent.depth == 0 ? '' : parent.person"

              },

              "fontSize": {

                "signal": "scaledFont11"

              },

              "limit": {

                "signal": "scaledNodeWidth-scaledLimit"

              },

              "font": {

                "value": "Calibri"

              }

            }

          }

        },

        {

          "type": "text",

          "interactive": false,

          "name": "node children",

          "encode": {

            "update": {

              "x": {

                "signal": "item.mark.group.width - (9/ span(xdom))*width"

              },

              "y": {

                "signal": "item.mark.group.height/2"

              },

              "align": {

                "value": "right"

              },

              "baseline": {

                "value": "middle"

              },

              "fill": {

                "scale": "colour",

                "signal": "parent.treeParent"

              },

              "text": {

                "signal": "parent.children>0?parent.children:''"

              },

              "fontSize": {

                "signal": "scaledFont12"

              },

              "font": {

                "value": "Calibri"

              }

            }

          }

        }

      ]

    }

  ]

}

No comments:

Post a Comment