Filter / query engine
Real datasets are too big to show all at once. graph.queryEngine filters the graph by node attributes: setFilter(key, spec) keeps nodes whose data[key] matches — an exact value, a list, or a numeric { min, max } range — and multiple filters AND together. resetFilters() clears them.
Attribute rules aside, excludeNode(id) hides one node by hand; includeNode(id) brings it back and clearNodeExclusions() restores every manually hidden node.
The buttons below drive the engine directly. The same filtering is available without code: in full mode the header carries a Graph Filters panel (the funnel icon, or press Shift+K). Code and panel stay in sync — filter from a button, open the panel, and its controls already reflect the active filter, while hidden nodes appear in its Hidden nodes list.
This card declares its facets (UI.filter.facets), so the panel contains exactly the six controls below with the labels and widgets given — including two that aren't node-data keys at all. Omit facets and the panel instead derives a control per data key, inferring each widget from the values it finds; see Filter options.
tags is array-valued, so filtering it tests membership: tag = critical keeps every node carrying that tag. Depends on load ≥ is computed — an accessor reads the dependencies' load instead of the node's own, which is why web-1 (the busiest service at 82%) drops out of it while the quieter db-1 survives.
Stack them: type = api then load ≥ 70 leaves just the one busy API.
// 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)
}
]// 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()
}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 }
}
}
}// 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' }
]
}