如何在Vega JS中实现树节点切换?

问题描述 投票:0回答:3

我正在使用 Vega JS 来构建树形图。总的来说,我的问题如下:

Vega 文档有一个很棒的树形布局示例。我怎样才能扩展它并具有折叠和展开其节点的能力?

更具体地说,让我们考虑一下我正在 Vega 编辑器中构建的树形图示例。

如果单击节点,它们将切换(展开或折叠),使您可以看到树的特定分支。除非您尝试折叠顶级节点(区域),同时保持二级节点(区域)展开,否则此功能可以正常工作。在这种情况下,树将如下所示:

发生这种情况是因为我处理这种交互的方式:

    当您单击节点时,会触发
  1. toggledNode
     信号,进而触发 
    toggle
     数据数组中的 
    expandedNodes
     操作。 IE。通过单击一个节点,我可以将该节点添加到或删除到 
    expandedNodes
     数组中(更准确地说,我们添加/删除仅具有 
    name
     属性的简化对象)
  2. 因此
  3. expandedNodes
    数据包含有关哪些节点被
    显式扩展的信息。但它不知道这些展开的节点是否位于折叠的父节点内部。
  4. 然后,为了找出哪些节点实际上可见,我使用
  5. visibleNodes
     数据。在那里,我使用以下表达式应用 
    filter
     变换:
    !datum.parent || indata('expandedNodes', 'name', datum.parent)
    。 IE。我仅检查上一级:如果节点的父节点存在于 
    expandedNodes
     数组中,我认为该节点是可见的。
问题如下:

我找不到任何跨多个级别扩展此功能的方法。

也许我可以编写一些钩子来检查 2 或 3 个级别的相同条件,例如:

!datum.parent || indata('expandedNodes', 'name', datum.parent) && indata('expandedNodes', 'name', datum.myCustomFieldWithParentNode.parent) && indata('expandedNodes', 'name', datum.myCustomFieldWithParentNode.myCustomFieldWithParentNode.parent)
但是对于这样一个简单的问题来说似乎太复杂了,而且这也不是最终的解决方案。理论上,一棵树可能包含几十个嵌套级别:那该怎么办?

我在 Vega 中发现了一个有用的表达方式:

treeAncestors。我可以轻松地用 JavaScript 编写一个解决方案,其中有循环和数组方法,例如 .some()

.every()
。但显然 Vega 不支持任何表达式来迭代数组。因此,即使我可以使用 
treeAncestors
 函数获取树节点祖先数组,但我无法用它做任何事情来验证所有祖先是否都已展开。

要么我的方法是错误的,有人可以找到更好的算法来做同样的事情,这不需要迭代数组(

data

indata
表达式除外) - 或者这是Vega的当前限制。

javascript charts tree vega
3个回答
2
投票
您可以使用treeAncestors,然后使用展平变换来获取可以查询的数据集。在你的情况下,它看起来像:

{ "transform": [ { "as": "treeAncestors", "type": "formula", "expr": "treeAncestors('tree', datum.id, 'root')" } ], "name": "tree-ancestors", "source": "tree" }, { "transform": [{"fields": ["treeAncestors"], "type": "flatten"}], "name": "tree-ancestors-flatt", "source": "tree-ancestors" }, { "transform": [ { "type": "filter", "expr": "indata('selected', 'value', datum.treeAncestors.id)" } ], "name": "filtered", "source": "tree-ancestors-flatt" }, { "transform": [{"type": "aggregate", "groupby": ["id"]}], "name": "filtered-aggregate", "source": "filtered" }, { "transform": [ { "type": "filter", "expr": "indata('filtered-aggregate', 'id', datum.id) " } ], "name": "filtered-tree", "source": "tree" }
    

1
投票
Vega 似乎没有递归方法来解决你的问题“嘿,如果我所有的父母都展开了,那么我作为一个节点可见”。

您可以检查您想要定义的所有级别的实际条件。

{ "type": "filter", "expr": "!datum.parent || indata('expandedNodes','name',datum.parent)&&datum.depth==1||(indata('expandedNodes','name',datum.firstParent)&&indata('expandedNodes','name',datum.secondParent)&&datum.depth==2)||(indata('expandedNodes','name',datum.firstParent)&&indata('expandedNodes','name',datum.secondParent)&&indata('expandedNodes','name',datum.thirdParent)&&datum.depth==3)" }
上面的代码对 VEGA 说:“嘿,检查一下我所有的

定义的父母是否都已展开,并过滤我是否有我的父母存在未展开

要查看您案例的完整解决方案,请检查:

规格


1
投票
这是我创建的用于展开和折叠树节点的示例(还支持平移和缩放)。

有一个名为treeClickStorePerm的数据集,它存储画布上已打开的节点的状态。它由名为 treeClickStoreTemp 的中间数据集填充,而该中间数据集又由规范顶部定义的各种信号驱动。

编辑器

{ "$schema": "https://vega.github.io/schema/vega/v5.json", "description": "Zoomable, collapsable tree by David Bacci: https://www.linkedin.com/in/davbacci/", "width": {"signal": "1400"}, "height": {"signal": "1000"}, "background": "#f5f5f5", "autosize": "pad", "padding": 5, "signals": [ {"name": "nodeWidth", "value": 190}, {"name": "nodeHeight", "value": 45}, { "name": "startingDepth", "value": 1, "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": "mousedown", "update": "xy()" } ] }, { "name": "xcur", "value": null, "on": [ { "events": "mousedown", "update": "slice(xdom)" } ] }, { "name": "ycur", "value": null, "on": [ { "events": "mousedown", "update": "slice(ydom)" } ] }, { "name": "delta", "value": [0, 0], "on": [ { "events": [ { "source": "window", "type": "mousemove", "consume": true, "between": [ {"type": "mousedown"}, { "source": "window", "type": "mouseup" } ] } ], "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())]" } ] }, { "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))" } ] }, { "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": "source", "url": "https://raw.githubusercontent.com/PBI-David/Deneb-Showcase/main/Organisation%20Tree%20Chart/data.json" }, { "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": "fold", "fields": [ "l1", "l2", "l3", "l4", "l5" ] }, { "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 != '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": "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+10"}, {"signal": "nodeWidth+140"} ], "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": "indexof(nodeHighlight, datum.target.id)> -1? 2.5:0.4" }, "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": "merge(hsl(scale('colour', datum.treeParent)), {l:0.94})" }, "stroke": { "signal": "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" }, "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": { "signal": "scaledKPIHeight" }, "width": { "signal": "(item.mark.group.width)" }, "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": { "signal": "scaledKPIHeight" }, "width": { "signal": "scale('kpiscale',parent.kpi)" }, "fill": { "scale": "colour", "signal": "parent.treeParent" } } } }, { "type": "text", "interactive": false, "name": "name", "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.person" }, "fontSize": { "signal": "scaledFont13" }, "limit": { "signal": "scaledNodeWidth-scaledLimit" }, "font": { "value": "Calibri" } } } }, { "type": "text", "interactive": false, "name": "title", "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.title" }, "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" } } } } ] } ] }
    
© www.soinside.com 2019 - 2024. All rights reserved.