Couches de bord
Ce graphique comporte quatre types de relations à la fois : des services call entre eux, une base de données replicates, un centre de surveillance monitors la moitié du domaine et un pipeline enregistre ce qui était deployed-with quoi. Dessinés de la même manière, ils formeraient une boule de poils. Séparés, ils ne sont toujours qu’une boule de poils – parce que le graphique d’appel que vous êtes venu lire est enfoui sous deux couches de contexte.
Les calques sont les trois étapes qui résolvent ce problème, et ils sont destinés à être utilisés ensemble :
- Nommez le genre —
render.edgeTypeAccessor. - Démarquez-le —
render.edgeStyleMap. - Éteignez-le —
UI.filter.edgeFacets.
Essayez-le : ouvrez Filter Graph et utilisez les lignes Relationships, ou cliquez sur les échantillons Relationship dans la légende — ce sont le même filtre vu deux fois. Désactivez monitors et deployed-with et le graphique d'appel est soudainement lisible.
Surveillez les nœuds pendant que vous le faites. Ils ne bougent pas. Changer de calque change les lignes qui sont dessinées et rien d'autre : le lien force les portes sur un drapeau séparé, donc un bord dont le calque est désactivé continue de rassembler ses extrémités. La mise en page, la sélection et la caméra reviennent peu à peu identiques. Si le fait de masquer un calque redistribuait le graphique, vous ne pouviez pas utiliser la bascule pour lire le graphique, ce qui est tout l'intérêt.
Deux comportements méritent d’être provoqués :
- Désactivez
monitors. Prometheus n'a désormais aucun bord visible et reste sur la toile. Le cacher serait une décision de filtre node, et une facette de bord n'en prend jamais. - Désactivez chaque ligne. Le graphique conserve les dix nœuds et ne trace aucune ligne. Pour un calque de bord, un choix vide signifie *chaque calque * ; pour un nœud à sélection multiple, un choix vide signifie aucune contrainte. Le contrôle écrit ce qui reste activé, donc cela ne peut rien signifier d'autre.
La légende empile une section de nœud et une section de bord sur une seule carte : type détermine les couleurs des nœuds, kind détermine les traits et les échantillons de la section de bord sont lignes — couleur, tiret et marqueur échantillonnés à partir de ce que le moteur de rendu a réellement peint. Il n'attribue jamais de style ; il en rapporte un.
Les boutons pilotent directement queryEngine.setEdgeFilter — le panneau et la légende s'allument pour correspondre, car il n'y a qu'un seul filtre. Les facettes Edge sont enregistrées quel que soit le mode de l'interface utilisateur, cela fonctionne donc également dans viewer et static, où il n'y a pas de panneau sur lequel cliquer.
Voir Couches de bord pour l'ensemble complet d'options - le vocabulaire des facettes au-delà d'un calque simple, la façon dont les clusters réduits filtrent et la priorité de style edgeStyleMap s'y trouve.
// 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' }
]
}
}
}// 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`)
}// 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' } }
]
}