Thursday, 28 August 2025

Box visual fixed V2

{
  "$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": 10,
      "on": [
        {
          "events": {
            "type": "timer",
            "throttle": 0
          },
          "update": "10"
        }
      ]
    },
    {
      "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": "xext",
      "update": "[0,width]"
    },
    {
      "name": "yext",
      "update": "[0,height]"
    },
    {
      "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": "xdom",
      "update": "slice(xext)",
      "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": "[0,width]"
        }
      ]
    },
    {
      "name": "ydom",
      "update": "slice(yext)",
      "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": "[0,height]"
        }
      ]
    },
    {
      "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": "datum.depth <= startingDepth"
        },
        {
          "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": "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": true,
      "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-(20/ span(xdom))*width"
              },
              "ellipsis": {
                "value": "…"
              },
              "lineBreak": {
                "value": " "
              },
              "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-(20/ span(xdom))*width"
              },
              "ellipsis": {
                "value": "…"
              },
              "lineBreak": {
                "value": " "
              },
              "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