可过滤的图例
颜色编码的图表需要一个键 - 一旦有了键,该键就是 UI 中最快的过滤器。 UI.legend 将一个停靠在画布角落:每个类别有一个样本、一个标签和一个节点计数,单击一行会隐藏该类别。指向一行首先会点亮画布上的节点,并使其余节点变暗,因此可以在关闭之前找到类别。
一行配置它:legend: { key: 'type' }。该库收集 data.type 的不同值,从 渲染器已解析的的颜色中读取每个样本,并计算其后面的节点。通常,您根本不需要线条 - 声明了 render.nodeTypeAccessor 后,一旦显示该尺寸,就会出现一个图例来解释颜色(请参阅Legend);该卡声明了显示旋钮的块。图例纯粹是描述性的 - 这里的颜色来自选项中的ColorPaletteMapper,与按类别的颜色完全相同,并且图例从未分配颜色。更改调色板,图例如下。
单击行会通过 graph.queryEngine 进行过滤,因此与 same 过滤标题的 图形过滤器 面板一样。该卡在图例自己的键下声明了一个type面,这使得二合一控制:关闭图例中的api,打开面板(漏斗,或Shift+K)并且其服务类型多选已将其丢弃。 resetFilters() — 或面板的 重置 — 重新点亮每一行。
根本没有 type 的节点不会获得任何行,并且图例永远不会隐藏它们:它只能作用于它列出的类别。
一把钥匙很少能说明整个故事。 同时使用两个键在同一个停靠卡中按尺寸堆叠一个标题部分 - 按下面的type +区域:type继续驱动面板的小面,zone获得自己的一个部分,并且两个过滤器和在一起,因此隐藏api 和 dmz 留下了两者都不是的东西。 zone 声明其自己的样本颜色,因为画布在其颜色中编码 type,并且 zone 的采样样本只是巧合。
部分也可以对 edges 而不是节点进行关键帧 — scope: 'edge' 使用线样本列出图形的关系类型,并在单击一个样本时隐藏整个层。这需要不同类型的边缘,因此它位于 Edge 层 卡上。
在节标题中: 显示全部、反转,以及将该节折叠到其标题的 V 形图标 - alt 键单击 V 形图标以折叠每个节。 悬停一行以在画布上点亮其类别; 按住 alt 键单击 即可仅显示该类别。切换通过 legendToggle 记录到控制台,这为其部分命名。
// import { ColorPaletteMapper } from 'pivotick'
// The consumer owns the colours — here a palette mapper over `type`, exactly as in
// the "Color by category" card. The legend never assigns a colour.
const palette = new ColorPaletteMapper('okabe-ito')
// `key` is all a legend needs: it collects the distinct values of `data.type`,
// reads each swatch out of the colour the renderer resolved, counts the nodes
// behind it, and makes every row a filter toggle.
//
// You can also leave `UI.legend` out entirely: with a `render.nodeTypeAccessor`
// declared, a legend appears by itself as soon as that dimension is shown to
// explain the colours. `UI.legend: false` opts out.
const legend = {
key: 'type',
title: 'Service type',
// Every one of these is a default — spelled out here to show the knobs.
position: 'bottom-left',
showCounts: true,
collapsible: true,
filterable: true,
maxVisibleEntries: 12
}const options = {
UI: {
// The legend is chrome: `full` and `light` modes have it, `viewer` and
// `static` don't. Full mode also ships the Graph Filters panel, which the
// legend below shares a filter with.
mode: 'full',
// Off: `full` mode brings a minimap and the data dock, and this card is about
// neither (see the Minimap and Data table cards for those).
minimap: false,
table: false,
legend,
// Declaring a facet under the *same key* the legend uses makes the two one
// control: toggle a swatch and the panel's multiselect follows, and the
// other way round. (The trade-off: an empty multiselect means "no
// constraint" to the panel, so the legend won't let you switch off the last
// remaining category.)
filter: {
facets: [
{
key: 'type', label: 'Service type', type: 'multiselect',
options: (graph) => [...new Set(graph.getNodes().map((node) => node.getData().type))]
.sort().map((value) => ({ label: value, value }))
}
]
}
},
render: {
defaultNodeStyle: {
size: 15,
strokeColor: '#ffffff',
textColor: '#334155',
text: (node) => node.getData()?.name,
textVerticalShift: -1.8,
color: (node) => palette.getColor(node.getData()?.type)
}
}
}// The legend announces every toggle on the data bus, so the choice can be
// persisted, mirrored elsewhere, or logged.
function watchLegend(graph) {
graph.on('legendToggle', ({ section, hidden, visible }) => {
console.log(`legend [${section}]: showing ${visible.join(', ') || '(none)'} — hiding ${hidden.join(', ') || '(none)'}`)
})
}
// Swap the legend at runtime — a graph that started without one gets it built on
// the spot, and `undefined` removes it (clearing its filter with it).
function legendByType(graph) {
graph.setLegend({ key: 'type', title: 'Service type' })
}
function removeLegend(graph) {
graph.setLegend(false)
}
// Declared entries, for when the categories aren't a plain data key: supply the
// label, the colour and the predicate yourself. An array works too; a function is
// re-resolved whenever the data changes.
function legendByTier(graph) {
graph.setLegend({
title: 'Tier',
entries: [
{
id: 'edge', label: 'Edge', color: '#0072B2',
predicate: (node) => node.getData().type === 'web'
},
{
id: 'service', label: 'Services', color: '#E69F00',
predicate: (node) => node.getData().type === 'api'
},
{
id: 'storage', label: 'Storage', color: '#009E73',
predicate: (node) => ['database', 'cache'].includes(node.getData().type)
}
]
})
}
// Two keys at once: `sections` stacks one titled block per dimension in a single
// docked card. Each section filters on its own, and the filters *and* together —
// hide `api` above and `dmz` below and what is left is neither.
function legendByTypeAndZone(graph) {
graph.setLegend({
// `position` belongs to the card, not to a section. Top-left, because a
// two-section card is tall enough to reach the mode rail in the corner the
// single-key legend uses.
position: 'top-left',
sections: [
// Still adopts the declared `type` facet, so this section and the filter
// panel remain one control.
{ key: 'type', title: 'Service type' },
{
key: 'zone',
title: 'Network zone',
// The colours encode `type`, not `zone`, and the legend can only
// sample colours — so this section declares swatches of its own
// rather than showing one that would be a coincidence. `key` still
// supplies the predicate.
entries: [
{ id: 'dmz', label: 'DMZ', color: '#CC79A7' },
{ id: 'internal', label: 'Internal', color: '#56B4E9' }
]
}
]
})
}// A service graph whose `type` drives both the colours and the legend. The counts
// differ per type on purpose — the legend reports them. `zone` is a second
// dimension the colours say nothing about — see the stacked legend below.
const data = {
nodes: [
{ id: 'web-1', data: { name: 'Web app', type: 'web', zone: 'dmz' } },
{ id: 'web-2', data: { name: 'Mobile web', type: 'web', zone: 'dmz' } },
{ id: 'api-1', data: { name: 'Auth API', type: 'api', zone: 'internal' } },
{ id: 'api-2', data: { name: 'Orders API', type: 'api', zone: 'internal' } },
{ id: 'api-3', data: { name: 'Billing API', type: 'api', zone: 'internal' } },
{ id: 'db-1', data: { name: 'Postgres', type: 'database', zone: 'internal' } },
{ id: 'db-2', data: { name: 'Replica', type: 'database', zone: 'internal' } },
{ id: 'cache-1', data: { name: 'Redis', type: 'cache', zone: 'internal' } }
],
edges: [
{ from: 'web-1', to: 'api-1' },
{ from: 'web-1', to: 'api-2' },
{ from: 'web-2', to: 'api-1' },
{ from: 'api-2', to: 'api-3' },
{ from: 'api-1', to: 'cache-1' },
{ from: 'api-2', to: 'db-1' },
{ from: 'api-3', to: 'db-1' },
{ from: 'db-1', to: 'db-2' }
]
}