Pular para o conteúdo

Plug-ins ​

Um plugin é um pacote independente de elementos de UI, atalhos de teclado e ganchos de ciclo de vida que se instalam em um gráfico sem que o núcleo precise saber que ele existe. Registre um de forma declarativa ou imperativa:

ts
import { Pivotick, minimap } from 'pivotick'

// declaratively
const graph = new Pivotick(container, data, { plugins: [minimap()] })

// …or at any point later
graph.use(minimap({ position: 'bottom-left' }))

Um plug-in instalado depois que o gráfico está ativo é atualizado para a fase atual do ciclo de vida, portanto, não há "tarde demais" - e tudo o que ele registra é destruído pela interface do usuário.

A forma de um plugin ​

ts
import type { PivotickPlugin } from 'pivotick'

const myPlugin: PivotickPlugin = {
    name: 'my-plugin',          // used for de-duplication and logging
    install(ctx) { /* … */ },   // called once
}

Instalar o mesmo name duas vezes avisa e ignora a duplicata, então um plugin pode ser listado em plugins e reaplicado através de graph.use sem duplicar.

O que install é entregue ​

ctx é um {@link PluginContext}:

MembroPara que serve
grapho Graph ativo — nós, arestas, o barramento de eventos de dados, queryEngine
uio UIManager — opções, notificações, armazenamento de modo
layouto andaime DOM, leia live (nunca um instantâneo). layout.canvas é para onde vai o cromo encaixado na tela
addElement(element, slot?)coloque um UIComponent no ciclo de vida, montado em slot
addPanel(panel) / removePanel(id) / refreshPanel(id?)painéis da barra lateral — a mesma porta de UI.extraPanels
addDockTab(tab) / removeDockTab(id)um painel na doca inferior – a mesma porta pela qual a mesa embutida passa
addRailMode(mode) / removeRailMode(id)um modo no painel esquerdo, ao lado de Selecionar / Criar / Visualizar / Física
addTopBarAction(action) / removeTopBarAction(id)uma pílula na barra superior — a mesma porta de UI.topBar.actions
addPivot(definition)um enriquecimento executável — consulte Pivots & enriquecimento
onPhase(phase, cb)gancho afterMount / graphReady / destroy; retorna um cancelamento de inscrição
addKeybinding(binding)um atalho que é removido quando a UI é desmontada
keyManagero registro de atalhos de teclado, para qualquer coisa mais envolvida

O key de uma ligação são modificadores da chave, unidos por +: 'Shift+K', 'Ctrl+F', 'ArrowUp'. Escreva modificadores como Ctrl, Meta (Cmd em macOS), Shift, Alt, nessa ordem, e use Mod para corresponder a Ctrl ou Meta - que é o atalho que deve funcionar ambas as plataformas desejam. A chave em si é KeyboardEvent.key literalmente, por isso diferencia maiúsculas de minúsculas e muda com shift: as duas metades de um par de desfazer são 'Mod+z' e 'Mod+Shift+Z'. As vinculações nunca são acionadas enquanto um campo de texto está em foco.

Quais slots layout existem dependem do modo: canvas e notification sempre, graphnavigation em todos os modos, exceto static, mainheader / modal / slidePanel / moderail / toolpanel / flyout / legend em full e light, e sidebar em full apenas. Leia o slot no momento em que precisar, em vez de armazená-lo em cache.

Contribuindo com um elemento de UI ​

Estenda UIComponent e o ciclo de vida será direcionado para você - incluindo a desmontagem de qualquer coisa registrada por meio de track / listen / trackInteraction:

ts
import { UIComponent } from 'pivotick'

class Watermark extends UIComponent {
    onMount(slot) {
        this.element = document.createElement('div')
        this.element.textContent = 'draft'
        slot?.appendChild(this.element)
    }

    onGraphReady() {
        // `listen` and `trackInteraction` unsubscribe themselves on destroy
        this.trackInteraction('canvasZoom', () => this.reposition())
    }

    onDestroy() {
        this.element?.remove()
    }
}

const watermark: PivotickPlugin = {
    name: 'watermark',
    install: (ctx) => ctx.addElement(new Watermark(ctx.ui), ctx.layout?.canvas),
}

As quatro fases são mount(slot) → afterMount() → graphReady() → destroy(). graphReady dispara assim que a simulação é estabelecida, então pode ser segundos após a montagem em um gráfico grande - qualquer coisa que deveria estar na tela imediatamente pertence a onMount / onAfterMount (adiado um quadro se precisar de graph.renderer, que é construído após a UI).

Veja o cartão da galeria Extend with a plugin para um exemplo completo e ao vivo.

Contribuindo com um painel de encaixe ​

ctx.addDockTab coloca um painel no ponto inferior, ao lado da tabela de dados. O dock é dono da região – sua altura, sua divisória, sua dobra e a faixa que nomeia os painéis – e seu painel é dono do que está nele:

ts
const auditLog: PivotickPlugin = {
    name: 'auditLog',
    install: (ctx) => ctx.addDockTab({
        label: 'Audit',
        render: () => buildPane(ctx.graph),   // once, on first activation
        toolbar: () => [clearButton],         // on every activation
        onActivate: () => resumePainting(),
        onDeactivate: () => stopPainting(),
    }),
}

Vale a pena saber quatro coisas:

  • Uma guia é um painel, não uma visualização de um. Se o seu painel tiver várias visualizações próprias, ele permanece como uma única guia de encaixe e desenha seu próprio switch em toolbar, chamando handle.refresh() para alterar o corpo - o que invoca novamente render. Isso é exatamente o que a tabela de dados faz para Nodes / Edges, e é por isso que a faixa do dock nunca nivela as visualizações de um painel ao lado de outro painel. Trocar seu próprio DOM pelas costas do dock não funciona: ele mantém o elemento render fornecido e anexaria novamente um nó obsoleto na próxima ativação. Desenhe um switch interno como um controle segmentado, não como guias — o nível externo já se parece com guias. pvt-dock-views na faixa e pvt-dock-view em cada botão (mais active) são públicos, então parece a mudança da mesa e segue o tema sem você reafirmar também.
  • O primeiro painel constrói a região. Os plug-ins são instalados após a UI é construída, então uma guia sempre chega tarde demais para que o portão de modo do próprio dock tenha dito sim em seu nome. Registrar um dá origem ao dock, o que significa que seu plugin funciona com UI.table: false e não precisa de nada ativado, exceto o modo full.
  • render é chamado uma vez, preguiçosamente, na primeira vez que o painel é aberto; o elemento é mantido e reanexado posteriormente, mantendo sua própria posição de rolagem. toolbar é reconstruído a cada ativação, para que seus controles possam ler o estado atual do seu painel.
  • onActivate / onDeactivate são o único sinal de que você está fora da tela, e o que fazer com eles depende do seu painel. O conteúdo que é função do estado atual do gráfico pode parar de funcionar enquanto está oculto e ser derivado novamente no retorno - é isso que a tabela faz. O conteúdo que perderia alguma coisa (um painel assistindo a um ônibus ao vivo — um evento desaparece depois de disparado) precisa continuar funcionando e simplesmente parar de pintar, liberando seu backlog quando voltar. Nada sobre os ganchos prefere também.

Uma guia não é UIComponent, então nada direciona as fases do ciclo de vida para ela. Quando seu painel precisar de graphReady, segure UIComponent, addElement e chame addDockTab de seu onMount — consulte Contribuindo com um elemento de UI acima.

Consulte o cartão da galeria Adicionar um painel de encaixe para obter um exemplo completo e dinâmico — um painel com duas visualizações próprias, ao lado da tabela de dados.

Contribuindo com um modo ferroviário ​

Os quatro modos do painel esquerdo - Selecionar, Criar, Visualizar, Física - estão integrados. Qualquer resto é seu: addRailMode coloca um modo próprio abaixo de um divisor, depois deles, ordenado entre seus pares por order.

Um modo ponteiro possui o painel de ferramentas contextual. Declare seu tools e o painel os desenha da mesma forma que Select e Create são desenhados, com o armamento, os estados habilitado/desabilitado e o recolhimento manipulados para você:

js
const explore = {
    name: 'explore-mode',
    install(ctx) {
        const selected = () => ctx.graph.renderer.getGraphInteraction().getSelectedNode()?.node

        ctx.addRailMode({
            id: 'explore',
            label: 'Explore',
            icon: compassSvg,
            shortcut: 'E',
            tools: [
                { id: 'expand', label: 'Expand neighbours', icon: plusSvg, kind: 'action',
                  enabled: () => !!selected(),
                  run: () => expandFrom(ctx.graph, selected()) },
                { id: 'walk', label: 'Path walk', icon: pathSvg, kind: 'toggle',
                  run: (armed) => armPathWalk(ctx.graph, armed) },
            ],
            onExit: () => armPathWalk(ctx.graph, false),
        })
    },
}

new Pivotick(el, data, { plugins: [explore] })

O kind de uma ferramenta decide como ela se comporta. Um 'action' é executado uma vez e deixa o modo como estava. Um 'toggle' arma uma ferramenta modal: o painel é recolhido e o botão ferroviário se transforma no próprio ícone e rótulo dessa ferramenta, exatamente quando o slot do Select se torna Lasso. Um 'default' é a ferramenta na qual o modo se baseia; nomeie-o em defaultTool e é para isso que a alternância reverte.

tools também pode ser uma função, relida toda vez que o painel for desenhado — use-a quando as linhas dependerem do que está selecionado, em vez de apenas ficarem acinzentadas. Para qualquer coisa que uma linha não possa expressar, render() retorna um elemento anexado abaixo das linhas:

js
ctx.addRailMode({
    id: 'explore', label: 'Explore', icon: compassSvg,
    tools: () => toolsFor(ctx.graph.renderer.getGraphInteraction().getSelectedNode()),
    render: () => depthSlider(),
})

Um modo flyout abre uma sobreposição de configurações, como Visualização e Física. Subclasse Flyout e mão addRailMode uma fábrica — core monta o painel, abre-o exatamente enquanto seu modo está ativo e o leva embora com o modo:

js
import { Pivotick, Flyout } from 'pivotick'

class EnrichFlyout extends Flyout {
    mode = 'enrich'                     // must match the definition's id
    template() { return this.headerRow(sparklesSvg, 'Enrich') + this.toggleRow('auto', gearSvg, 'Auto-enrich', 'Enrich on load') }
    wire() { this.wireToggle('auto', () => toggleAuto(), () => isAuto()) }
}

ctx.addRailMode({
    id: 'enrich', label: 'Enrich', icon: sparklesSvg,
    kind: 'flyout',
    flyout: (ui) => new EnrichFlyout(ui),
})

Os modos são mutuamente exclusivos, então o seu exclui Visualização e Física gratuitamente – uma loja, um modo ativo.

Três coisas que vale a pena saber:

  • icon é uma string SVG bruta, injetada como HTML e nunca higienizada. Deve vir de uma fonte em que você confia. CSS dimensiona para 20px no trilho, 18px no painel.
  • shortcut segue as regras normais de atalho de teclado: reivindicar uma chave que outra pessoa já possui a oculta, com um aviso, até que seu modo seja removido.
  • onExit é onde você desarma. Ele é executado quando o modo é abandonado e quando é removido enquanto ativo - nesse caso o trilho volta para Selecionar. Ele não é executado na desmontagem da UI; os trituradores que seu plugin já rastreia cobrem isso.

addRailMode retorna um triturador e funciona em todos os modos — o botão só é desenhado onde há um trilho, o que significa full e light.

Conduzindo a janela de visualização ​

Qualquer coisa que navegue no gráfico – um minimapa, uma visão geral, um controle “pular para” – usa dois métodos de renderização:

ts
// The extent of everything drawn, in graph coordinates (null when there's nothing).
const bounds = graph.renderer.getContentBounds()

// Put a graph-space point in the middle of the canvas.
graph.renderer.setViewport({ x: 120, y: -40 })
graph.renderer.setViewport({ x: 0, y: 0, scale: 1.5, animate: true })

setViewport deixa a escala em paz, a menos que você passe por um, então é o primitivo para panorâmica. Para ler onde a visualização atualmente está, inverta os cantos da tela em vez de usar uma transformação de zoom — isso é independente do renderizador:

ts
const rect = graph.UIManager.layout.canvas.getBoundingClientRect()
const topLeft = graph.renderer.screenToGraphCoordinates(rect.left, rect.top)
const bottomRight = graph.renderer.screenToGraphCoordinates(rect.right, rect.bottom)

fitAndCenter() (cabe em tudo), zoomIn() / zoomOut() e focusElement(nodeOrEdge) continuam sendo os atalhos para os casos comuns.

O minimapa ​

Um plugin original: uma visão geral em cache de todo o gráfico encaixado em um canto da tela, com um retângulo mostrando o que está na tela. Clique para recentrar a visualização; arraste o retângulo para deslocar. Uma pequena alternância no canto voltado para ele dobra-o apenas para aquele botão e o traz de volta.

O modo full monta um para você — faz parte do cromo desse modo, como o cabeçalho, a barra lateral e o trilho de modo. Todos os outros modos deixam isso para você:

ts
// full mode: already there, nothing to install
new Pivotick(container, data, { UI: { mode: 'full' } })

// any other mode: ask for it, with `UI.minimap` …
new Pivotick(container, data, { UI: { mode: 'light', minimap: true } })

// … or as the plugin it is
import { Pivotick, minimap } from 'pivotick'
new Pivotick(container, data, { UI: { mode: 'light' }, plugins: [minimap()] })

UI.minimap usa o mesmo {@link MinimapOptions} que o plugin faz, então { minimap: { position: 'top-left' } } configura aquele que o modo completo traz consigo. UI.minimap: false suprime. Passar o seu próprio minimap() em plugins também vence - o modo completo fica de lado em vez de montar um segundo - então um plugins: [minimap({ … })] existente continua funcionando exatamente como antes.

html
<!-- browser build: it hangs off the global, like Node and Edge -->
<script>new Pivotick(el, data, { plugins: [Pivotick.minimap()] })</script>

Opções ​

OpçãoTipoPadrãoO que isso faz
position'bottom-right' | 'bottom-left' | 'top-right' | 'top-left''bottom-right'Em qual canto ele se encaixa. 'bottom-right' é o único canto que o cromo integrado deixa livre no modo full.
widthnumber200Largura em CSS pixels, borda incluída.
heightnumberderivadoAltura em CSS pixels. Omitido, ele segue a proporção da tela (fixada em 70–400px) para que o retângulo mantenha a forma da janela de visualização real.
collapsedboolean | 'auto'false ou 'auto' para as montagens do modo fullEm qual estado ele abre. A alternância está sempre lá; é apenas aqui que tudo começa. Consulte Saindo do caminho para 'auto'.

Nada mais é configurável, porque nada mais precisa ser: o nível de detalhe e a cadência de redesenho adaptam-se ao gráfico.

O que isso desenha ​

Os nós são pontos na cor que o renderizador lhes deu, as bordas são linhas finas abaixo deles e as notas - o único conteúdo com tamanho próprio - são desenhadas em escala como blocos translúcidos em sua própria cor, sobre os nós como estão na tela. O conteúdo oculto e filtrado é deixado de fora. Depois de alguns milhares de nós, a imagem torna-se um mapa de densidade (um selo por nó, sem bordas), que tem uma leitura melhor do que dezenas de milhares de pontos sobrepostos; as notas são desenhadas de qualquer maneira. Tudo é armazenado em cache e apenas rasterizado novamente quando a imagem realmente muda - um nó é descartado, uma nota é movida, o layout é ajustado, um filtro é aplicado - para que o movimento panorâmico e o zoom permaneçam livres, independentemente do tamanho do gráfico.

Uma carta HTML (html em shape: 'none') não tem cor que o minimapa possa ler, então seu ponto assume a cor do menor desenho que é uma forma: o estilo base, ou então a primeira camada em tiers. Sem nenhuma, ou com a cor transparent, o ponto é desenhado com a própria tinta do minimapa (--pvt-minimap-ink). Os pontos são dimensionados de acordo com o que o nó cobre, então um cartão ou um nó renderNode recebe um ponto do tamanho de um cartão em vez de um tamanho de size.

Saindo do caminho ​

Um minimapa que você pediu permanece onde você o colocou. O modo full montado em seu nome não foi solicitado, então ele abre collapsed: 'auto' e leva a tela em consideração: ele permanece aberto enquanto a tela tem pelo menos quatro minimapas de largura e altura, e se dobra para alternar abaixo disso. Ele continua seguindo a tela a partir de então – dobrando-se quando a barra lateral se abre sobre ela ou a janela se estreita, voltando quando a sala o faz.

No momento em que alguém o dobra ou traz de volta - pelo botão de alternância ou por setCollapsed() - isso para: uma escolha explícita permanece e nenhum redimensionamento posterior a substitui. Passe um collapsed: true / false explícito para cancelar 'auto' desde o início.

A seta do botão de alternância aponta para o canto em que o minimapa se encaixa – a direção em que ele se dobra – e vira quando é recolhido. Dobrado, ele não desenha absolutamente nada, nem mesmo o retângulo, portanto não custa nada enquanto estiver fora do caminho; abri-lo rasteriza novamente a aparência da tela até então. setCollapsed(boolean) e isCollapsed() na instância Minimap geram a mesma coisa no código.

O que atrai e quanto custa ​

O minimapa mapeia os limites de conteúdo junto com a janela de visualização atual, de modo que o retângulo esteja sempre visível e com tamanho honesto - diminua o zoom e o gráfico encolhe dentro do quadro em vez de o retângulo deslizar para fora dele.

Ele mantém o gráfico em um bitmap fora da tela e apenas o rasteriza novamente quando a imagem realmente muda: em uma alteração de dados, quando um filtro oculta ou restaura nós, quando um nó é descartado após um arrasto, a cada 10 tiques de simulação enquanto um layout é ajustado e ao redimensionar. Panorâmica e zoom redesenham uma imagem e um retângulo, portanto, a navegação custa o mesmo, quer o gráfico tenha 20 nós ou 50.000.

Os detalhes degradam com o tamanho, propositalmente:

GráficoDesenhado como
≤ 1.500 nósum ponto por nó na cor que o renderizador resolveu, além de bordas finas (passando de 4.000 bordas)
> 1500 nósum carimbo por nó em uma única tinta, acumulação alfa — regiões densas lidas como um mapa de densidade e nenhum estilo por nó é resolvido

Funciona nos modos full, light e viewer, e full é quem o monta sem ser solicitado. Em static — que não promete interações — ele não está montado, e instalá-lo lá avisa.

Veja o cartão da galeria Minimapa para ver um ao vivo.