过滤/查询引擎
真实的数据集太大,无法一次显示全部。 graph.queryEngine 按节点属性过滤图表:setFilter(key, spec) 将 data[key] 匹配的节点(精确值、列表或数字 { min, max } 范围)和多个过滤器 AND 保留在一起。 resetFilters()清除它们。
抛开属性规则不谈,excludeNode(id)手动隐藏一个节点; includeNode(id) 将其恢复,clearNodeExclusions() 恢复每个手动隐藏的节点。
下面的按钮直接驱动发动机。无需代码即可实现相同的过滤:在完整模式中,标题带有图形过滤器面板(漏斗图标,或按Shift+K)。代码和面板保持同步 - 通过按钮进行过滤,打开面板,其控件已经反映了活动过滤器,而隐藏节点出现在其隐藏节点列表中。
该卡声明了其方面(UI.filter.facets),因此该面板恰好包含下面的六个控件以及给定的标签和小部件 - 包括两个根本不是节点数据键的控件。省略 facets ,面板会为每个数据键派生一个控件,从它找到的值推断每个小部件;请参阅过滤器选项。
tags 是数组值,因此过滤它会测试成员资格:tag = critical 使每个节点都带有该标签。 Depends on load ≥ 是计算的 — accessor 读取依赖项' 负载,而不是节点自己的负载,这就是为什么 web-1(最繁忙的服务,82%)从其中退出,而更安静的 db-1 幸存下来。
将它们堆叠起来: type = api 然后 load ≥ 70 只剩下一个繁忙的 API。
js
// Distinct values of a key across the graph, flattening array-valued data — so a
// select's options follow the data without the facet set itself churning.
function distinct(graph, key) {
const values = new Set()
for (const node of graph.getNodes()) {
const value = node.getData()[key]
if (Array.isArray(value)) value.forEach((entry) => values.add(entry))
else if (value !== null && value !== undefined) values.add(String(value))
}
return [...values].sort().map((value) => ({ label: value, value }))
}
// Declaring facets replaces the panel's data-scanning: exactly these fields, in
// this order, with these labels and widgets. Omit `UI.filter.facets` entirely and
// the panel derives a control per data key instead (the zero-config default).
const facets = [
{ key: 'type', label: 'Service type', type: 'multiselect', options: (g) => distinct(g, 'type') },
{ key: 'zone', label: 'Region', type: 'select', options: (g) => distinct(g, 'zone') },
{ key: 'load', label: 'Load (%)', type: 'numberRange' },
// Array-valued data: a filter tests membership, so picking `critical` matches
// every node carrying that tag — one control, no flattened shadow data key.
{ key: 'tags', label: 'Tag', type: 'multiselect', options: (g) => distinct(g, 'tags') },
// Computed: `accessor` reads whatever you like, here the *dependencies'* load
// rather than the node's own data — "which services depend on a busy one?".
{
key: 'dep_load', label: 'Depends on load ≥', type: 'numberRange',
accessor: (node) => node.getEdgesOut().map((edge) => edge.to.getData().load)
},
// Full control: decide membership yourself.
{
key: 'name', label: 'Name matches', type: 'regex',
predicate: (node, value) => new RegExp(value, 'i').test(node.id)
}
]js
// Everything the built-in panel does is available programmatically on
// graph.queryEngine. setFilter(key, spec) keys off the facet's key and applies at
// once; call it for several keys and the filters AND together.
function filterByType(graph, type) {
graph.queryEngine.setFilter('type', { value: type, matchMode: 'exact' })
}
// A numeric spec takes a { min, max } range (either bound may be omitted).
function filterByLoad(graph, min) {
graph.queryEngine.setFilter('load', { value: { min, max: undefined } })
}
// Array-valued data matches by membership: this keeps every node tagged 'critical'.
// Pass an array for any-of, or add matchMode: 'all' to require every tag.
function filterByTag(graph, tag) {
graph.queryEngine.setFilter('tags', { value: tag })
}
// The computed facet: the node's own load is irrelevant, its dependencies' isn't.
function filterByDependencyLoad(graph, min) {
graph.queryEngine.setFilter('dep_load', { value: { min, max: undefined } })
}
// resetFilters clears every active filter and restores the full graph.
function clearFilters(graph) {
graph.queryEngine.resetFilters()
}
// Attribute rules aside, excludeNode(id) hides a single node by hand. It's tracked
// separately from setFilter rules and listed in the panel's "Hidden nodes" section;
// includeNode(id) restores one, clearNodeExclusions() restores them all.
function excludeNode(graph, id) {
graph.queryEngine.excludeNode(id)
}
function clearExclusions(graph) {
graph.queryEngine.clearNodeExclusions()
}js
const options = {
// Full mode ships the built-in Graph Filters panel (open it from the header's
// funnel, or Shift+K). Its form is generated from the facets declared below;
// without `filter.facets` it auto-discovers your node attributes instead.
UI: {
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,
filter: { facets }
},
render: {
nodeTypeAccessor: (node) => node.getData().type,
nodeStyleMap: {
web: { color: palette.web },
api: { color: palette.api },
db: { color: palette.db }
}
}
}js
// A small service-dependency graph. Each node carries categorical fields (type,
// zone), a numeric one (load) and an array-valued one (tags) — exactly the kind of
// data the filter engine and its built-in UI slice on. Colouring by type is purely
// so the filtering reads clearly (see the Node styling cards for the technique).
const palette = { web: '#0072B2', api: '#E69F00', db: '#009E73' }
const data = {
nodes: [
{ id: 'web-1', data: { type: 'web', zone: 'eu', load: 82, tags: ['public', 'critical'] } },
{ id: 'web-2', data: { type: 'web', zone: 'us', load: 45, tags: ['public'] } },
{ id: 'api-1', data: { type: 'api', zone: 'eu', load: 68, tags: ['internal'] } },
{ id: 'api-2', data: { type: 'api', zone: 'us', load: 91, tags: ['internal', 'critical'] } },
{ id: 'api-3', data: { type: 'api', zone: 'eu', load: 30, tags: [] } },
{ id: 'db-1', data: { type: 'db', zone: 'eu', load: 55, tags: ['critical'] } },
{ id: 'db-2', data: { type: 'db', zone: 'us', load: 74, tags: ['critical'] } }
],
edges: [
{ from: 'web-1', to: 'api-1' },
{ from: 'web-1', to: 'api-3' },
{ from: 'web-2', to: 'api-2' },
{ from: 'api-1', to: 'db-1' },
{ from: 'api-2', to: 'db-2' },
{ from: 'api-3', to: 'db-1' },
{ from: 'db-1', to: 'db-2' }
]
}