Camadas de borda
Este gráfico carrega quatro tipos de relação ao mesmo tempo: serviços call entre si, um banco de dados replicates, um hub de monitoramento monitors metade da propriedade e um pipeline registra o que era deployed-with o quê. Desenhados da mesma forma, seriam uma bola de pelo. Separados, eles ainda são uma bola de cabelo - porque o gráfico de chamadas que você leu está enterrado sob duas camadas de contexto.
Camadas são as três etapas que corrigem isso e devem ser usadas juntas:
- Nomeie o tipo —
render.edgeTypeAccessor. - Estilize-o à parte —
render.edgeStyleMap. - Desligue —
UI.filter.edgeFacets.
Experimente: abra Filter Graph e use as linhas Relationships ou clique nas amostras Relationship na legenda - elas são o mesmo filtro visto duas vezes. Desligue monitors e deployed-with e o gráfico de chamadas ficará subitamente legível.
Observe os nós enquanto você faz isso. Eles não se movem. A troca de uma camada altera quais linhas são desenhadas e nada mais: o link força as portas em um sinalizador separado, de modo que uma aresta cuja camada está desativada continua unindo suas extremidades. Layout, seleção e câmera voltam pouco a pouco idênticos. Se ocultar uma camada refluísse no gráfico, você não poderia usar o botão de alternância para ler o gráfico, que é o ponto principal.
Dois comportamentos que vale a pena provocar:
- Desligar
monitors. Prometheus agora não tem bordas visíveis — e permanece na tela. Ocultá-lo seria uma decisão de filtro node, e uma faceta de borda nunca toma uma. - Desative todas as linhas. O gráfico mantém todos os dez nós e não desenha linhas. Para uma camada de borda, uma seleção vazia significa todas as camadas desativadas; para uma seleção múltipla de nó, uma seleção vazia significa sem restrição. O controle escreve o que permanece ativado, então não há mais nada que isso possa significar.
A legenda empilha uma seção de nó e uma seção de borda em um cartão: type direciona as cores do nó, kind direciona os traços e as amostras da seção de borda são linhas — cor, traço e marcador amostrados do que o renderizador realmente pintou. Nunca atribui um estilo; ele relata um.
Os botões direcionam queryEngine.setEdgeFilter diretamente - o painel e a legenda são reiniciados para corresponder, porque há apenas um filtro. As facetas da borda são registradas independentemente do modo da UI, então isso funciona também em viewer e static, onde não há painel para clicar.
Consulte Camadas de borda para o conjunto completo de opções - o vocabulário de facetas além de uma camada simples, como os clusters recolhidos são filtrados e a precedência de estilo edgeStyleMap se enquadra.
// 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' } }
]
}