边缘层
该图同时承载四种关系:相互服务call、数据库replicates、监控中心monitors一半资产、以及一条管道记录deployed-with什么。如果画得一模一样的话,它们就是一个毛团。拆开来看,它们仍然是一个毛团——因为你要阅读的调用图被隐藏在两层上下文之下。
图层是解决这个问题的三个步骤,它们应该一起使用:
- 命名该种类 —
render.edgeTypeAccessor。 - 将其分开 —
render.edgeStyleMap。 - 关闭 —
UI.filter.edgeFacets。
尝试一下:打开 Filter Graph 并使用 Relationships 行,或单击图例中的 Relationship 样本 - 它们是出现两次的相同过滤器。关闭monitors和deployed-with,调用图突然清晰可见。
执行此操作时观察节点。它们不会移动。切换层会改变绘制的线,而不会改变其他任何内容:链接力在单独的标志上进行门控,因此层关闭的边会继续将其端点拉在一起。布局、选择和相机都完全相同。如果隐藏图层重新流动图形,则无法使用切换来读取图形,这就是重点。
有两种值得激发的行为:
- 关闭
monitors。Prometheus现在根本没有可见的边缘 - 并保留在画布上。隐藏它将是一个节点过滤器决策,并且边缘面永远不会采取一个。 - 关闭每一行。图形保留所有十个节点并且不绘制线条。对于边缘层,空拾取意味着 * * 的每一层;对于节点多重选择,空选择意味着无约束。控件会写入保留的内容,因此它没有任何其他含义。
图例将节点部分和边缘部分堆叠在一张卡中:type 驱动节点颜色,kind 驱动笔划,边缘部分的样本为 lines — 从渲染器实际绘制的内容中采样的颜色、虚线和标记。它从不指定一种风格;它报告了一项。
按钮直接驱动 queryEngine.setEdgeFilter — 面板和图例重新亮起以匹配,因为只有一个过滤器。无论 UI 模式如何,边缘面都会注册,因此这也适用于没有可供单击的面板的 viewer 和 static。
请参阅 边缘层 了解完整的选项集 — 普通图层之外的方面词汇、折叠簇如何过滤以及edgeStyleMap 所在的样式优先级。
js
// import { ColorPaletteMapper } from 'pivotick'
// Node colour keys on `type`; edge style keys on `kind`. Two independent
// encodings, which is why the legend below carries a section for each.
const palette = new ColorPaletteMapper('okabe-ito')
const options = {
render: {
// 1. Name the kind. Mirrors `nodeTypeAccessor` exactly.
edgeTypeAccessor: (edge) => edge.getData()?.kind,
// 2. Style it apart. Each value is a partial EdgeStyle merged over
// `defaultEdgeStyle`, so a kind only states what makes it different.
edgeStyleMap: {
calls: { strokeColor: '#0072B2', strokeWidth: 2.5 },
replicates: { strokeColor: '#009E73', strokeWidth: 2.5, markerEnd: 'diamond' },
// `animateDash: false` keeps the two context layers calm. A dashed edge
// animates its dash by default, which is a lot of motion for something
// you are meant to read past.
monitors: { strokeColor: '#999999', strokeWidth: 1.5, dashed: true, animateDash: false },
'deployed-with': { strokeColor: '#E69F00', strokeWidth: 1.5, dashed: true, animateDash: false }
},
defaultNodeStyle: {
size: 18,
strokeColor: '#ffffff',
textColor: '#334155',
text: (node) => node.getData()?.name,
// 1.6 addition: draw the whole label instead of middle-ellipsising it.
textTruncate: false,
textVerticalShift: -2.2,
color: (node) => palette.getColor(node.getData()?.type)
}
},
// Not part of the feature — just room. Ten labelled nodes with four relation
// kinds between them need more space than `Auto` gives a graph this small,
// and naming any of these knobs opts the graph out of auto tuning.
simulation: {
d3LinkDistance: 115,
d3ManyBodyStrength: -1100,
d3CollideRadiusMultiplier: 3
},
UI: {
// Layers need the filter panel and the legend, both of which are `full`-mode
// chrome. The facets themselves are registered whatever the mode, so
// `queryEngine.setEdgeFilter` works in `viewer` and `static` too.
mode: 'full',
// Off: `full` mode also brings a minimap and the data dock, and this card is
// about neither.
minimap: false,
table: false,
filter: {
// 3. Make the kind a layer. `{ key: 'kind' }` alone would do — the label
// and the options are both derived. Nothing about edge facets is ever
// auto-derived, though: declare none and layers don't exist.
edgeFacets: [{ key: 'kind', label: 'Relationship' }]
},
legend: {
// Top-left: a two-section card is tall enough to reach the mode rail in
// the bottom-left corner.
position: 'top-left',
sections: [
{ key: 'type', title: 'Service' },
// `scope: 'edge'` keys on edges and draws a *line* swatch — stroke
// colour, dash and marker as the renderer resolved them. It adopts
// the `kind` facet declared above, so this section and the panel's
// Relationships toggles are two views of one filter.
{ key: 'kind', title: 'Relationship', scope: 'edge' }
]
}
}
}js
// Layers are filters, so they are drivable from code in any mode. `value` is the
// list of kinds that stay *on*.
function callsOnly(graph) {
graph.queryEngine.setEdgeFilter('kind', { value: ['calls'], matchMode: 'exact' })
}
// Everything except the two context layers.
function hideContext(graph) {
graph.queryEngine.setEdgeFilter('kind', { value: ['calls', 'replicates'], matchMode: 'exact' })
}
// Removing the filter is how every layer comes back on. Writing an empty list
// would mean the opposite — every layer off.
function showAllLayers(graph) {
graph.queryEngine.removeEdgeFilter('kind')
}
// How many edges the active layers are hiding, for a status line of your own.
function reportHidden(graph) {
console.log(`${graph.queryEngine.getHiddenEdgeCount()} edges hidden by layer`)
}js
// A service graph carrying four *kinds* of relation at once. `calls` is the one
// people come for; the other three are context that buries it — which is the
// problem layers solve.
const data = {
nodes: [
{ id: 'web-1', data: { name: 'Web app', type: 'web' } },
{ id: 'web-2', data: { name: 'Mobile web', type: 'web' } },
{ id: 'api-1', data: { name: 'Auth API', type: 'api' } },
{ id: 'api-2', data: { name: 'Orders API', type: 'api' } },
{ id: 'api-3', data: { name: 'Billing API', type: 'api' } },
{ id: 'db-1', data: { name: 'Postgres', type: 'database' } },
{ id: 'db-2', data: { name: 'Replica', type: 'database' } },
{ id: 'cache-1', data: { name: 'Redis', type: 'cache' } },
{ id: 'mon-1', data: { name: 'Metrics', type: 'ops' } },
{ id: 'ci-1', data: { name: 'Deploys', type: 'ops' } }
],
edges: [
// The call graph — the structure everything else hangs off.
{ from: 'web-1', to: 'api-1', data: { kind: 'calls' } },
{ from: 'web-1', to: 'api-2', data: { kind: 'calls' } },
{ from: 'web-2', to: 'api-1', data: { kind: 'calls' } },
{ from: 'api-2', to: 'api-3', data: { kind: 'calls' } },
{ from: 'api-1', to: 'cache-1', data: { kind: 'calls' } },
{ from: 'api-2', to: 'db-1', data: { kind: 'calls' } },
{ from: 'api-3', to: 'db-1', data: { kind: 'calls' } },
// Data movement.
{ from: 'db-1', to: 'db-2', data: { kind: 'replicates' } },
// Observability: one hub wired to everything, which is exactly what makes
// the call graph hard to read while this layer is on.
{ from: 'mon-1', to: 'api-1', data: { kind: 'monitors' } },
{ from: 'mon-1', to: 'api-2', data: { kind: 'monitors' } },
{ from: 'mon-1', to: 'db-1', data: { kind: 'monitors' } },
{ from: 'mon-1', to: 'cache-1', data: { kind: 'monitors' } },
// Co-location — inferred, not structural.
{ from: 'ci-1', to: 'api-1', data: { kind: 'deployed-with' } },
{ from: 'ci-1', to: 'api-2', data: { kind: 'deployed-with' } },
{ from: 'ci-1', to: 'api-3', data: { kind: 'deployed-with' } }
]
}