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

Adicionar um painel de encaixe ​

O encaixe inferior é uma região compartilhada, não a propriedade privada da tabela de dados: uma linha de grade, uma altura, uma dobra, não importa quantos painéis haja nela. addDockTab() é como você coloca outra coisa lá - a mesma porta pela qual a mesa embutida passa, então um painel que você registra é igual e não convidado.

Este cartão adiciona um painel Resumo que rola o gráfico por proprietário ou por tipo. A faixa da doca diz Table │ Summary; clique e o corpo * e * os controles de cabeçalho mudam com ele, porque Select all / CSV / Columns pertencem à tabela e não significam nada em um roll-up.

Uma guia é um painel ​

O painel Resumo mostra o gráfico de duas maneiras - Por proprietário e Por tipo - e essas são visualizações de um painel, não de dois painéis. Então ele registra uma aba de encaixe single e desenha seu próprio switch, chamando refresh() para mudar o corpo.

Essa distinção é todo o design. Registrar as duas visualizações como duas guias de encaixe as listaria na faixa do encaixe ao lado de Table, alegando que uma visualização do resumo e de toda a tabela de dados são o mesmo tipo de coisa. A tabela integrada faz exatamente o que este painel faz: Nodes e Edges são suas guias, desenhadas em sua própria barra.

Os dois níveis são desenhados de forma diferente para que possam ficar próximos um do outro e ainda assim serem lidos como externo e interno:

OlhaClasse
Table │ Summaryguias de altura total, sublinhadas quando ativas, fechadas por uma regrapvt-dock-tabs / pvt-dock-tab
Nodes │ Edges, By owner │ By kindum pequeno grupo de comprimidospvt-dock-views / pvt-dock-view

O par interno é público, e é por isso que a opção abaixo tem três nomes de classe em vez de um bloco de estilo embutido: seu painel obtém o mesmo controle que a tabela interna possui, e um consumidor que retifica --pvt-theme-primary ou substitui qualquer classe retifica ambos os níveis de uma vez.

refresh() não é educação opcional ​

O dock mantém o elemento que seu render retornou e o anexa novamente quando o painel volta para a frente. Um painel que trocasse seu próprio DOM deixaria o dock contendo um nó obsoleto para devolver mais tarde. Portanto, a mudança de corpo passa por refresh(), que chama render novamente.

Saber quando ninguém está olhando ​

onActivate / onDeactivate são o único sinal que um painel recebe de que está fora da tela, e o que fazer com eles é sua escolha:

  • O conteúdo que é uma função do estado atual do gráfico pode parar de funcionar enquanto estiver oculto e ser derivado novamente no retorno. É isso que este painel faz e o que a tabela de dados faz.
  • O conteúdo que perde algo precisa continuar funcionando e simplesmente parar de pintar – um painel assistindo a um ônibus ao vivo, digamos, já que um evento desaparece depois de disparado.

Nada sobre os ganchos prefere também.

Ele traz a doca com ele ​

Os plug-ins são instalados após a interface do usuário é construída, portanto, uma guia de encaixe sempre chega após a execução do portão de modo da própria região. Registrar um, portanto, constrói o dock — seu plugin funciona com UI.table: false e não precisa de nada ativado, exceto o modo full. Com a mesa desligada e um painel próprio, nenhuma faixa é desenhada: o encaixe é simplesmente o seu painel.

UI.dock configura a própria região (open, collapsed, height) — alcance-a em vez de UI.table quando a mesa estiver desligada, já que essa é a única porta.

Consulte Plugins para o contrato e Tabela de dados para as opções do próprio dock.

js
// The bottom dock is a shared region: one row, one height, one fold, however many panes
// are in it. `addDockTab` is the door — the same one the built-in data table comes
// through, so a pane you register is its equal rather than its guest.
//
// **One tab is one pane, not one view of one.** This pane rolls the graph up two ways,
// by owner and by kind. Those are two views of *this* pane, so it registers a single
// dock tab and draws its own switch, calling `refresh()` to change body. Registering
// them as two dock tabs would put them in the dock's strip beside `Table`, claiming a
// view of the summary and the whole data table are the same kind of thing.
//
// That is also why the two levels look different: the dock draws panes as full-height
// underlined tabs, and a pane's own views a lighter pill group. That inner look is public
// — `pvt-dock-views` on the strip, `pvt-dock-view` on each button, `active` on the current
// one — so your pane matches the built-in table for free, and follows the theme when a
// consumer overrides `--pvt-theme-primary` or either class.

const VIEWS = [
    { key: 'owner', label: 'By owner' },
    { key: 'kind', label: 'By kind' },
]

/** Count the nodes under each value of `key`, biggest group first. */
function rollUp(graph, key) {
    const counts = new Map()
    for (const node of graph.getNodes()) {
        const value = node.getData()?.[key] ?? '—'
        counts.set(value, (counts.get(value) ?? 0) + 1)
    }
    return [...counts.entries()].sort((a, b) => b[1] - a[1])
}

/**
 * The pane's body. `render` is called once per pane, lazily, the first time it is
 * opened — and again on every `refresh()`, which is how the view switch works.
 */
function renderSummary(graph, key) {
    const rows = rollUp(graph, key)
    const biggest = Math.max(...rows.map(([, count]) => count), 1)

    const body = document.createElement('div')
    body.style.cssText = 'padding: 8px 12px; font: 12px/1.9 sans-serif'

    for (const [value, count] of rows) {
        const row = document.createElement('div')
        row.style.cssText = 'display: grid; grid-template-columns: 130px 1fr 34px; gap: 10px; align-items: center'

        const name = document.createElement('span')
        name.textContent = value

        // A bar rather than a number alone: the shape of the split is the point.
        const track = document.createElement('span')
        track.style.cssText = 'height: 7px; border-radius: 4px; background: var(--pvt-bg-color-4)'
        const fill = document.createElement('span')
        fill.style.cssText = `display: block; height: 100%; border-radius: 4px; background: var(--pvt-vibrant-blue); width: ${(count / biggest) * 100}%`
        track.appendChild(fill)

        const tally = document.createElement('span')
        tally.textContent = String(count)
        tally.style.cssText = 'text-align: right; color: var(--pvt-text-color-3)'

        row.append(name, track, tally)
        body.appendChild(row)
    }
    return body
}

/**
 * The pane's own controls, in the header slot the dock hands it. `toolbar` is re-invoked
 * on every activation, so these always read the pane's current state.
 */
function renderControls(current, pick) {
    const strip = document.createElement('div')
    strip.className = 'pvt-dock-views'

    for (const view of VIEWS) {
        const button = document.createElement('button')
        button.type = 'button'
        button.className = 'pvt-dock-view'
        button.textContent = view.label
        button.classList.toggle('active', view.key === current)
        button.setAttribute('aria-pressed', String(view.key === current))
        button.addEventListener('click', () => pick(view.key))
        strip.appendChild(button)
    }
    return strip
}

/**
 * Ships as a plugin, so it installs itself and the core never needs to know it exists.
 * A plugin's tab also *builds* the dock if there isn't one — plugins install after the
 * UI, so it always arrives after the region's own gate has run.
 */
function summaryPane() {
    return {
        name: 'summaryPane',
        install(ctx) {
            let view = 'owner'
            // Whether the pane is the one on show, and whether the graph moved while it
            // wasn't. `onActivate` / `onDeactivate` are the only signal you get, and this
            // is the shape most panes want: don't work while hidden, catch up on return.
            let visible = false
            let stale = false

            ctx.addDockTab({
                id: 'summary',
                label: 'Summary',
                render: () => renderSummary(ctx.graph, view),
                toolbar: (pane) => renderControls(view, (key) => {
                    view = key
                    // The dock keeps the element `render` gave it, so ask it to rebuild
                    // rather than swapping the DOM behind its back.
                    pane.refresh()
                }),
                onActivate: (pane) => {
                    visible = true
                    if (stale) { stale = false; pane.refresh() }
                },
                onDeactivate: () => { visible = false },
            })

            // The graph outlives nothing here, but a real plugin should keep the
            // unsubscribe — see the Extend with a plugin card.
            ctx.graph.on('dataBatchChanged', () => {
                if (visible) ctx.refreshDockTab('summary')
                else stale = true
            })
        },
    }
}

const options = {
    UI: {
        mode: 'full',
        // The dock is the subject, so it starts expanded; the table is the pane our own
        // one sits beside, and is what makes the strip appear at all.
        dock: { open: true, height: 0.4 },
        table: { columns: [
            { key: 'label', label: 'Service', type: 'text' },
            { key: 'kind', label: 'Kind', type: 'select' },
            { key: 'owner', label: 'Owner', type: 'select' },
        ] },
        // Full mode's minimap isn't what this card is about.
        minimap: false,
    },
    plugins: [summaryPane()],
}
js
const data = {
    nodes: [
        { id: 'api', data: { label: 'api-gateway', kind: 'service', owner: 'Platform' } },
        { id: 'auth', data: { label: 'auth-service', kind: 'service', owner: 'Identity' } },
        { id: 'billing', data: { label: 'billing-service', kind: 'service', owner: 'Payments' } },
        { id: 'search', data: { label: 'search-service', kind: 'service', owner: 'Discovery' } },
        { id: 'users-db', data: { label: 'users-db', kind: 'datastore', owner: 'Identity' } },
        { id: 'orders-db', data: { label: 'orders-db', kind: 'datastore', owner: 'Payments' } },
        { id: 'index', data: { label: 'search-index', kind: 'datastore', owner: 'Discovery' } },
        { id: 'cache', data: { label: 'edge-cache', kind: 'datastore', owner: 'Platform' } },
        { id: 'mailer', data: { label: 'mailer', kind: 'worker', owner: 'Payments' } },
        { id: 'reindexer', data: { label: 'reindexer', kind: 'worker', owner: 'Discovery' } },
    ],
    edges: [
        { from: 'api', to: 'auth', data: { label: 'authenticates' } },
        { from: 'api', to: 'search', data: { label: 'queries' } },
        { from: 'api', to: 'billing', data: { label: 'charges' } },
        { from: 'api', to: 'cache', data: { label: 'reads' } },
        { from: 'auth', to: 'users-db', data: { label: 'reads' } },
        { from: 'billing', to: 'orders-db', data: { label: 'writes' } },
        { from: 'billing', to: 'mailer', data: { label: 'enqueues' } },
        { from: 'search', to: 'index', data: { label: 'reads' } },
        { from: 'reindexer', to: 'index', data: { label: 'writes' } },
        { from: 'reindexer', to: 'orders-db', data: { label: 'reads' } },
    ],
}
Pager
Próxima páginaPrimeiros passos