Pular para o conteúdo
Pivotick
Main Navigation Início
Primeiros passos
Configuração
Referência da API
GaleriaProjetosExperimente!

Português

English
简体中文
Français
Deutsch

Português

English
简体中文
Français
Deutsch

Aparência

Sidebar Navigation

Primeiros passos

Projetos que usam o Pivotick

Configuração

Funções de retorno

Camadas de arestas

Simplificar

Layout

Renderização

Simulação

Personalizar a interface

Barra lateral

Dica

Menu de contexto

Filtros

Legenda

Tabela de dados

Estilo da interface

Traduzir a interface

Plugins

API Pivotick

Pivôs e enriquecimento

Salvar resultados dos pivôs

Desfazer e histórico

Segurança

Documentação da API

Galeria

Fundamentos

Estilo dos nós

Estilo das arestas

Layouts

Eventos e funções de retorno

Personalização da interface

Edição e criação

Notas e anotações

Filtros, pesquisa e hierarquia

Controle por programação

Temas e desempenho

Pivôs e enriquecimento

Demonstrações completas

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:

  1. Nomeie o tipo — render.edgeTypeAccessor.
  2. Estilize-o à parte — render.edgeStyleMap.
  3. 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.

js
// 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' }
            ]
        }
    }
}
js
// 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`)
}
js
// 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' } }
    ]
}
Pager
Próxima páginaPrimeiros passos