图形布局
Pivotick支持多种定位节点的布局策略。您可以通过layout选项配置布局,也可以通过simulation.layout来配置布局;当两者都设置时,layout获胜。
| 选项 | 类型 | 默认值 | 说明 |
|---|---|---|---|
type | 'force' | 'tree' | 'structured' | 'force' | 要使用的布局算法。 'force' 应用基于物理的力布局。 'tree' 在树结构中排列节点。 'structured' 以人的方式绘制小图,请参阅结构化布局。 |
树布局选项
当选择 type: 'tree' 时,以下附加选项可用:
| 选项 | 类型 | 默认值 | 说明 |
|---|---|---|---|
rootId | string | undefined | 指定用作根的节点的 ID。如果不提供,Pivotick将自动根据rootIdAlgorithmFinder选择根。 |
rootIdAlgorithmFinder | - | MaxReachability | 用于自动查找根节点的算法。 |
parentKey | string | undefined | 保存节点父节点 ID 的 node.data 键的名称。未置可否,为人父母的身份来自边缘。请参阅声明层次结构。 |
depthKey | string | undefined | 保存节点所在行的 node.data 键的名称,从 0 开始计数。未设置时,节点位于其父节点下方一行。 |
strength | number | 0.1 | 维持树结构的力量。 |
horizontal | boolean | false | 水平排列节点而不是垂直排列。 |
radial | boolean | false | 将节点放置在径向布局中,而不是垂直布局中。 |
radialGap | number | 750 | 径向布局中各层之间的间隙。 |
spacing | 'auto' | 'manual' | 'auto' | 下面的两个乘法器是否根据节点的大小和树的形状进行自我调整。设置任一者都明确意味着 'manual'。 |
levelSpacing | number | 1 | 乘以连续级别之间的距离(环之间,当 radial 时)。像以前一样,1 使树适合画布。 |
siblingSpacing | number | 1 | 将级别内节点之间的距离相乘。当 radial 时被忽略,其中级别始终跨越整个圆。 |
循环和断开的图
层次结构是从广度优先的生成树构建的,根部为rootId,而不是从原始边缘构建,因此这些都不会停止树布局:
- 循环。 到达节点的第一条边使其成为该节点的父节点;到达已放置节点的边根本不是层次结构的一部分,并且像任何其他边一样跨级别绘制。具有两个父节点的节点同样被其中一个节点声明。
- 多个组件。根无法到达的每个组件都有自己的根 - 更喜欢没有传入边缘的节点,因此是层次结构的组件被绘制为一个 - 并且组件并排放置。
- 根本没有边缘的节点。它们在层次结构中没有位置,因此它们是停放的而不是给定的:它们填充了浅层旁边的死区,在布局的后缘,一棵树将其留空,因为它在下降时变宽。在径向布局中,它们在最后一个之外有一个自己的环。只有没有指向 * 和 * 的节点才会被停放,因此没有任何边从树延伸到停车区域。
请注意,rootIdAlgorithmFinder: 'MinMaxDistance'和'MinHeight'是相同的搜索 - 最长路径最短的节点 - 并且都是基于拓扑排序的,因此在具有循环的图上,它们会警告并回退到第一个节点。默认的'MaxReachability'没有这样的限制。
树根查找器
在基于树的布局中,必须将一个节点指定为根。您可以使用 rootId 选项指定根节点。
如果您不确定哪个节点应该是根节点,请不要担心! Pivotick将使用rootIdAlgorithmFinder中定义的算法自动选择一个。
一个名为 rootId 的行走忽略边缘方向,因此任何节点都可以作为整棵树的根 - 选择一片叶子,图会重新挂在它下面,其前一个父节点向下一层。 (沿着叶子上的箭头不会到达任何东西,并且图形的其余部分将作为第二个组件放置在它旁边。)边缘本身仍然按原样绘制,因此以相反方式使用的链接呈现为指向级别的箭头。 rootIdAlgorithmFinder选择的根继续沿着箭头行走:它首先被从箭头上读出。
除非箭头不是层次结构。在收敛的数据上——每个叶子都是一个源,它们都指向几个中心,这是大多数出处和“可见”数据的形状——无论询问哪个发现者,都没有节点沿着箭头到达图形。然后,有向行走几乎将每个节点都留下为自己的根,并且树会以存根梳的形式出现,其中大多数边缘飞过布局。因此,当根本没有节点可以通过箭头覆盖其自身组件的一半时,布局将停止读取方向:它会双向遍历每条边并在图的中间重新扎根。边缘仍然是绘制,这意味着它们的箭头沿着树向上指向根部——数据的诚实渲染指向那个方向。
这是后备方案,而不是规则,测试是关于图而不是所选的根:
- 在箭头确实形成层次结构的地方,它们是最好的布局依据,每个发现者都有自己的答案——包括
'MinHeight',它在树上的答案总是叶子,故意没有达到任何结果。只有当没有其他东西可以覆盖更多的情况时,覆盖很少图的根才会被否决。 - 几个单独的层次结构的图表仍然显示为森林:覆盖范围是根据根自己的组件来衡量的,因此完美的多组件层次结构不会被误认为是失败。
- 固定的
rootId会完全跳过测试——它已经在忽略方向了。
rootId 命名不在图中的节点(在折叠的集群内被过滤、删除),只要这是真的,就会被忽略,而查找器会选择根。该选项被保留,因此返回的节点将重新建立树的根。
根也可以在运行时更改 - 只要树布局处于活动状态,物理弹出按钮就会将其作为 Root 行上的菜单提供。菜单为每个取景器命名并说明其功能;它的第一个条目是“我选择的节点”,它会显示为灰色,直到恰好选择了一个节点。固定节点后,该行将显示该节点的标签而不是查找器的名称:
graph.simulation.setTreeRoot({ rootId: 'node-42' }) // pin the tree to a node
graph.simulation.setTreeRoot({ algorithm: 'MinHeight' }) // drop the pin, let the finder choose
graph.simulation.getTreeRoot() // { rootId: undefined, algorithm: 'MinHeight' }声明层次结构
通常,Pivotick会自行解决层次结构:来自边缘上方生成树的父节点,每个节点在其父节点下方一行。有两种选择让数据来代替。
parentKey命名保存节点父 ID 的node.data键。depthKey命名持有其所在行的node.data键,从最浅根的0开始计数。
默认情况下,两者都未设置,并且在您命名一个键之前不会读取任何内容 - 因此,数据恰好带有 depth 或 parent 字段的图表不会受到影响,直到您提出要求。
任何单个节点中的任何一个密钥都可能丢失。没有声明父节点的节点照常从边缘获取一个;没有声明行的节点位于其父级的下方。声明两者都不是的图表的布局与以往完全相同。
从更下方开始一棵树
这就是depthKey的主要用途。由几棵独立的树组成的图是一个森林,它的根都共享最浅的一行——以前没有办法说其中一棵属于更下面的行。给该根一行:
const data = {
nodes: [
{ id: 'a' }, { id: 'a1' }, { id: 'a2' },
{ id: 'b', data: { level: 2 } }, { id: 'b1' }, { id: 'b2' },
],
edges: [
{ from: 'a', to: 'a1' }, { from: 'a', to: 'a2' },
{ from: 'b', to: 'b1' }, { from: 'b', to: 'b2' },
],
}
const graph = new Pivotick(container, data, {
layout: { type: 'tree', depthKey: 'level' },
})row 0 a
row 1 a1 a2
row 2 b <- b asked for row 2
row 3 b1 b2整棵树随着它的根移动:b的孩子跟随它向下移动。在 radial 布局中,一行是一个 ring,因此相同的声明会从两个环开始该树。
行的含义是什么
一行只能将节点进一步向下推。一棵整齐的树将子级放置在其父级下方恰好一行,因此无法绘制不在父级下方的行 - 它被限制为 parent + 1 并发出警告,指出有多少行。另一种方法是通过将节点与其父节点分离来尊重该行,这会让一个错误的数字将树分成额外的部分。
数据跳过的行留为 ** 空,并且它们占用实际空间** — 这就是使声明的行具有任何意义的原因。树会缩放以适合画布,因此在本来很浅的图表上声明行 50 会使其达到 51 行高,并将每个实际行压缩为空。 levelSpacing和自动间距是退出的方法。
声明的父母做什么
无论边是否连接到该对,已声明的父节点都会受到的尊重。这使得它能够陈述边缘不包含的层次结构 - 但这也意味着这样的分支在绘制时没有线条,因为那里没有边缘可供绘制。声明是层次结构的来源;边缘只会按原样绘制。
两个有用的结果:具有多个传入边的节点可以确定哪个是其真正的父代(否则生成树将采用先到达它的父代),并且根本没有声明父代的边的节点在树中获得适当的位置而不是停放区域。
对于该节点,无法使用的声明将被删除,然后该节点会回退到边缘,并且一个警告会将它们全部记录下来:
- 父节点命名未布局的节点 - 被过滤、删除或位于折叠的集群内 - 与
rootId在其命名的节点丢失时回退的方式相同; - 将关闭循环的父级(关闭它的链接是被删除的链接);
- 不是数字
0或更大的行。
优先级
rootId 排名超过 parentKey。固定根(在选项中,或通过在物理轨道中选择一个根)是从该节点重新挂起树的请求,因此从边缘重新派生父关系,并为该通道预留声明的父节点。声明的 rows 不受影响:它们表示节点所在的深度,而不是它悬挂的位置。
egoTree 忽略这两个选项;它从根的邻居中构建了一颗星。
间距
一棵树的布局是为了 fit 画布,因此它的自然间距已经遵循画布大小以及树的深度和宽度。 levelSpacing和siblingSpacing适合几何形状的比例:2分别表示在深度和宽度轴上“距离为适合布局的两倍”。两者都默认为 1,这是适合的布局本身,并且都接受 0.5 到 10。
它们也可以在运行时更改 - 只要树布局处于活动状态,物理弹出按钮就会将它们作为滑块提供(代替树布局忽略的模拟旋钮):
graph.simulation.setTreeSpacing({ levelSpacing: 2 })
graph.simulation.getTreeSpacing() // { levelSpacing: 2, siblingSpacing: 1 }自动间距
默认情况下,树有自己的间距。树布局的大小是根据画布确定的,并且从不考虑节点有多大,因此一棵小点树和一棵大标记节点树的布局相同 - 第二个重叠。 spacing: 'auto' 测量每个轴上最紧密的一对邻居,计算出它们所需的半径(加上层之间箭头的空间),并缩放乘数以适应。每当图表发生变化时,它就会重新导出它们。
有两件事它故意不做:
- 它永远不会将树压缩得比合适的布局更紧。 Auto 只会将乘数提高到
1以上,因此从未拥挤的图表会像以前一样准确地布局。 - 它永远不会覆盖您所做的选择。 设置
levelSpacing或siblingSpacing的树被视为已下定决心,并且手动移动任一滑块将永远保留自动状态。将乘数交还给simulation.enableAutoTreeSpacing()。
在 radial 布局中,水平面始终跨越整个圆,只有通过将环进一步向外推才能缓解环内的拥挤 - 因此两个测量都驱动 levelSpacing 在那里。
结构化布局
type: 'structured' 以人们绘制的方式布置一个小图表,以便嵌入式查看器必须一目了然。它是计算的而不是模拟的:每个节点都被放置和固定,因此相同的图形总是给出相同的图片,并且拖动节点仅移动该节点。
- 组件并排。每个连接的组件都单独布局,并且组件按行打包。行宽是在最大缩放时使整个布局适合画布的宽度。
- 风扇排队。 三个或更多相同类型的叶子悬挂在相同的一个或两个节点上(叶子最多有两个邻居),它们排列成均匀间隔的行或列,它们悬挂的节点位于其一侧。该分组是 Simplify 的 same neighbours 规则使用的一种,由
UI.simplify.typeOf输入,否则为render.nodeTypeAccessor。没有任何东西被折叠起来。 - 距离遵循边缘。其他所有内容均按应力主要化放置:相距一条边的节点相距一条边长度,两条边相距两倍,依此类推。轮毂的邻居围绕着它出现,链条则笔直地出现,两者都没有特殊情况。
- 用于松散节点的托盘。根本没有边缘的节点网格化在其余节点下方或右侧。
- 自由端上的标签。浮动标签(
textVerticalShift或textHorizontalShift为1或更多)绘制在其边缘自由的节点一侧,尽可能远离样式。风扇的标签全部背向其悬挂的节点。
每当可见图形发生变化时,就会重做布局,并且在第一次绘制之后,当节点的实际大小已知时,会再次重做布局。节点之间的间隔为 getLayoutRadius(),因此声明 layoutSize 的节点样式无论绘制什么尺寸都会以该尺寸进行布局。它适用于最多数百个节点的图表。
| 选项 | 类型 | 默认值 | 说明 |
|---|---|---|---|
groupMin | number | 3 | 共享同一类型的叶子数量最少,并且它们的邻居排列成扇形。较少的是一一放置的。 |
direction | 'auto' | 'row' | 'column' | 'auto' | 粉丝如何排队。 'auto' 将其放置在其悬挂的节点的线上。 |
gap | number | 30 | 清除两个节点之间的空间。一条边的长度是两倍;组件和托盘的距离是这个距离的三倍。 |
trayPosition | 'bottom' | 'right' | 'bottom' | 没有边的节点去哪里。 |
labelSides | boolean | true | 将每个浮动标签移动到其边缘空闲的一侧。 false 将标签留在样式放置的位置。 |
new Pivotick(container, data, {
layout: { type: 'structured', trayPosition: 'right' },
render: {
nodeTypeAccessor: (node) => node.getData().type,
defaultNodeStyle: { text: (node) => node.getData().label, textVerticalShift: -1.4 },
},
})graph.simulation.getLabelSide(id)报告为节点选择的边:'top'、'bottom'、'left'或'right',或任何其他布局下的undefined。
视图适合节点,而不是它们的标签,因此布局边缘节点旁边的长标签可以穿过画布。
危险
添加两种布局的示例!
垂直树
const options = {
layout: {
type: 'tree'
},
UI: {
mode: 'static'
}
}垂直树
const options = {
layout: {
type: 'tree',
radial: true,
radialGap: 300,
},
UI: {
mode: 'static'
}
}