Opções de renderização
Pivotick permite personalizar como nós, arestas e rótulos são renderizados por meio da opção render.
aviso
Todos os estilos definidos aqui se aplicam somente quando render.type está definido como svg.
| Opção | Tipo | Padrão | Descrição |
|---|---|---|---|
type | 'svg' | 'canvas' | 'svg' | O método de renderização. 'svg' usa elementos SVG, 'canvas' usa a tela HTML (experimental - não totalmente implementado). |
renderNode | (node: Node) => HTMLElement | string | void | undefined | Renderizador personalizado para nós. |
renderLabel | (edge: Edge) => HTMLElement | string | void | undefined | Renderizador personalizado para rótulos de borda. |
defaultNodeStyle | NodeStyle | defaultNodeStyle | Estilo padrão aplicado a todos os nós. |
defaultEdgeStyle | EdgeStyle | defaultEdgeStyle | Estilo padrão aplicado a todas as arestas. |
defaultLabelStyle | LabelStyle | defaultLabelStyle | Estilo padrão aplicado a todos os rótulos de borda. |
markerStyleMap | Record<string, MarkerStyle> | defaultMarkerStyle | Estilos personalizados para marcadores de borda ('arrow', 'diamond', etc.). Veja MarkerStyle |
nodeTypeAccessor | (node: Node) => string | undefined | undefined | Função para acessar o tipo de nó, usada com nodeStyleMap. |
nodeStyleMap | Record<string, NodeStyle> | {} | Mapeia tipos de nós (de nodeTypeAccessor) para estilos. |
edgeTypeAccessor | (edge: Edge) => string | undefined | undefined | O kind de uma aresta, usado com edgeStyleMap e como uma chave layer. |
edgeStyleMap | Record<string, Partial<EdgeStyle>> | undefined | Mapeia tipos de arestas (de edgeTypeAccessor) para estilos. Consulte Camadas de borda. |
minZoom | number | 0.1 | Nível mínimo de zoom. |
maxZoom | number | 10 | Nível máximo de zoom. |
minFitScale | number | undefined | O zoom mais baixo para o qual um ajuste vai. Um gráfico grande demais para caber nele é aberto com esse zoom, centralizado em fitAnchor, e o minimapa mostra o resto. |
fitAnchor | string | undefined | Id do nó em que um ajuste é mantido em minFitScale é centralizado. A visualização para nas bordas do gráfico, portanto, uma âncora em um lado permanece próxima a esse lado. |
zoomEnabled | boolean | true | Ative o zoom. false também usa os botões de zoom do trilho da janela de visualização, mantendo o ajuste e o centro. |
selectionBox | SelectionBox | undefined | Seleção de região: enabled: false remove o letreiro de arrastar ** e ** a ferramenta Selecionar ▸ Laço. |
enableNodeExpansion | boolean | true | Expandindo um cluster: false leva a divisa, o atalho Enter e o contorno recolhido tracejado com ele. Um filho selecionado ainda ilumina seu cluster fechado. |
Tipo de renderização
dica
Pivotick oferece controle flexível sobre a renderização, desde padrões simples até renderização totalmente personalizada. Aqui está um rápido resumo:
renderNodeerenderLabelpermitem personalizar totalmente como os nós e rótulos são desenhados (você pode retornar HTML ou texto).defaultNodeStyle.html(por nó ou por tipo) é a mesma saída de emergência dentro da cadeia de estilo - consulte HTML nós.nodeTypeAccessor+nodeStyleMappermitem estilo dinâmico com base no tipo de cada elemento.defaultNodeStyle,defaultEdgeStyleedefaultLabelStyledefinem a aparência base de todos os elementos.edgeTypeAccessor+edgeStyleMapsão as contrapartes da borda e funcionam como a chave pela qual uma camada de relação é desativada - consulte Camadas de borda.
Seu cartão é medido, seja qual for o formato
Pivotick mede o que você retorna para dimensionar seu <foreignObject> e para alimentar o raio de colisão do layout de força. O cartão é medido dentro de uma caixa retrátil da própria biblioteca, então você não precisa fazer seu autodimensionamento raiz: width: 100% nele é resolvido em relação ao seu próprio conteúdo e não no espaço reservado.
Os próximos três exemplos de renderização usam os seguintes dados:
const data = {
nodes: [
{ id: 1, data: { label: 'A', type: 'hub' } },
{ id: 2, data: { label: 'B', type: 'spoke' } }
],
edges: [{ from: 1, to: 2 }]
}const options = {
render: {
defaultNodeStyle: {
shape: 'circle',
color: '#007acc',
size: 12,
strokeColor: '#fff',
strokeWidth: 2,
fontFamily: 'system-ui',
textColor: '#fff'
},
defaultEdgeStyle: {
markerStart: (edge: Edge) => edge.getData().mstart,
markerEnd: 'arrow',
curveStyle: 'straight',
},
}
}const options = {
render: {
nodeTypeAccessor: (node) => node.getData()?.type,
nodeStyleMap: {
'hub': {
shape: 'hexagon',
color: '#f90',
size: 25,
text: (node) => node.getData()?.label
},
'spoke': {
shape: 'triangle',
color: '#09f'
}
},
},
UI: {
mode: 'static'
}
}const options = {
render: {
renderNode: (node: Node): HTMLElement | string | void => {
const size = 12
const color = '#09f'
const style = [
'display: block',
`width: ${size}px`,
`height: ${size}px`,
`background-color: ${color}`,
'border: 2px solid #fff',
'border-radius: 50%',
'opacity: 1',
].join(';')
return `<span style="${style}"></span>`
}
}
}Exemplo de renderização do Map-Accessor
HTML nós
Quando nenhuma combinação de shape, color, icon e text servir, entregue ao renderizador alguma marcação. Existem duas maneiras de entrar, e a diferença é apenas onde são declaradas:
| Declarado em | Alcança | |
|---|---|---|
NodeStyle.html | defaultNodeStyle, nodeStyleMap[type], estilo próprio de um nó, um retorno styleCb | um tipo ou um nó |
render.renderNode | as opções de renderização | cada nó, à frente da cadeia de estilo |
html é o primeiro a ser alcançado: é um canal de estilo comum, portanto resolve através da mesma cadeia de precedência que color ou shape, e um cartão pode ser dado a um tipo de nó enquanto o resto mantém suas formas.
const options = {
render: {
nodeTypeAccessor: (node) => node.getData()?.kind,
nodeStyleMap: {
// A card, and nothing else: `shape: 'none'` is what makes it the whole node.
service: {
shape: 'none',
html: (node) => {
const card = document.createElement('div')
card.className = 'my-service-card'
card.textContent = node.getData()?.name
return card
},
},
// Same graph, ordinary shapes.
host: { shape: 'circle', size: 18, color: '#0891b2' },
},
},
}shape: 'none'
Sem ele, a forma ainda está desenhada atrás do seu cartão, e size é o menor que o nó pode ser - que é o que você deseja para um cartão colocado em um disco colorido, e não o que você deseja para um cartão que é o nó. shape: 'none' não desenha nenhuma forma e, em vez disso, entrega o raio de colisão do nó e as âncoras de borda ao cartão.
É um valor de forma como qualquer outro, então compõe: um nó somente rótulo é shape: 'none' com text e nenhum cartão, e um nó somente ícone é shape: 'none' com iconClass. Um nó disforme sem nenhum conteúdo é invisível - mas ainda está lá, ainda pode ser arrastado e ainda selecionável em 2 × size.
O que um cartão não desliga
text é um canal separado, então um cartão e um rótulo de nó são desenhados juntos. Se o seu cartão tiver seu próprio título, defina text: '' — um valor herdado não pode ser apagado com undefined, que corresponde ao que quer que o mapa de estilo diga.
Dimensionamento
Seu cartão é medido e o nó cresce até ele, de modo que nunca é cortado, a força de colisão espaça os cartões reais e as bordas ficam na borda do cartão, em vez de em um círculo ao redor dele. Você não precisa fazer nada para que isso funcione - retornar uma raiz width: 100% é bom, porque a medição acontece dentro de uma caixa que pode ser reduzida para caber.
Duas consequências que vale a pena conhecer:
- A medição é assíncrona (um cartão não pode ser medido antes de estar na tela), então o raio de um nó é denominado
sizepara o primeiro ou dois quadros e então se torna o do cartão. O layout é ajustado uma vez quando chega. - Um cartão é medido uma vez, na renderização. Se seu conteúdo posteriormente mudar de tamanho por conta própria, renderize novamente o nó para medi-lo novamente.
Cardando apenas alguns nós
Ambos os retornos de chamada podem retornar nothing para um nó, que devolve esse nó à cadeia de estilo - forma, entrada do mapa de estilo, ícones e rótulo como de costume. Portanto, um único renderNode global pode cardar os nós que merecem um:
renderNode: (node) => node.getData()?.featured ? buildCard(node) : undefinedSomente marcação confiável
Tudo o que você retornar será inserido como está. Construa-o com textContent ou escape dele — não interpole os dados do nó em uma string de marcação. Consulte o guia de segurança.
Rótulos de nós completos
Um rótulo de nó maior que a sala que possui é encurtado com reticências no meio (Supe…abel). Defina textTruncate: false para desenhá-lo por completo - como padrão, por tipo de nó ou por nó:
const options = {
render: {
defaultNodeStyle: { textTruncate: false },
},
}Entre os dois, textMaxWidth limita o rótulo na largura que você escolher, em unidades gráficas (CSS pixels com zoom 1), em vez do espaço que o nó fornece. Uma etiqueta que caiba é desenhada por completo e uma mais longa é encurtada com reticências no meio para essa largura:
const options = {
render: {
defaultNodeStyle: { textTruncate: true, textMaxWidth: 160 },
},
}Tamanho da fonte da etiqueta
Um rótulo de nó é desenhado em max(12, size × 0.45) por padrão, portanto cresce com o nó. Quando um nó é grande pelo que desenha e não pelo quanto é importante, como uma miniatura de imagem, defina textFontSize (unidades gráficas) para manter seu rótulo alinhado com seus vizinhos:
const options = {
render: {
nodeStyleMap: {
image: { size: 80, imagePath: (node) => node.getData()?.url, textFontSize: 12 },
},
},
}O rótulo ainda fica logo abaixo do nó, o orçamento integrado ainda tira sua largura do nó (portanto, uma fonte menor cabe em mais caracteres) e o zoom gate julga pelo tamanho que você definiu.
A tampa é dimensionada com o zoom como o resto do nó e pode ser uma função do nó. É ignorado quando textTruncate é false.
Um rótulo completo ou tampado geralmente ultrapassa o formato do nó, por isso é desenhado no mesmo comprimido temático que um rótulo flutuante para permanecer legível na tela. Emparelhe-o com textVerticalShift: 1 (ou textHorizontalShift) para mover todo o rótulo para fora do nó. Os rótulos das bordas nunca são truncados.
text é um rótulo em um nó, não um substituto para ele. Um nó que é seu texto deve ser desenhado como o nó, através de html, renderNode ou um camada - esses são os desenhos do nó, e o zoom os mantém independentemente minLabelFontSize faz com os rótulos.
Emblemas de nó
No momento em que um gráfico é útil, color, shape, size e iconClass geralmente carregam alguma coisa. badges é um canal próprio – pequenos indicadores fixados na borda do nó, sem gastar nenhum dos outros:
const options = {
render: {
defaultNodeStyle: {
badges: (node) => {
const { notes, disputed } = node.getData()
return [
notes && { text: String(notes), color: '#2563eb', title: `${notes} notes` },
disputed && { iconClass: 'fa fa-exclamation', color: '#b94a48', title: 'Disputed' },
].filter(Boolean)
},
},
},
}Dê um emblema text (uma contagem, ou um caractere ou dois), um iconClass, iconUnicode ou svgIcon e um title para sua dica de ferramenta nativa. text ganha se você fornecer ambos. Um texto mais longo transforma o emblema em uma pílula e, após três caracteres, ele renderiza 99+.
Para onde eles vão
Omita position e os emblemas preencham os cantos livres no sentido horário a partir de 'ne'. Nomeie um ('ne' | 'nw' | 'se' | 'sw') e ele será honrado literalmente, incluindo sobreposições.
Quatro cabem em um nó simples, dois em um com filhos. O controle de expansão/recolhimento fica a nordeste quando um nó é recolhido e a sudeste quando é expandido, portanto, em qualquer nó expansível ambos Os cantos leste são reservados - caso contrário, cada crachá mudaria de canto no momento em que um cluster fosse aberto. Os emblemas além do que cabem são dobrados em um +n cuja dica os nomeia.
Os emblemas são dimensionados com o nó entre o chão e o teto, de modo que permanecem legíveis em um nó minúsculo sem inchar em um nó grande, e ficam na borda real da forma - o canto de um quadrado ou de uma imagem emoldurada, o ponto de 45° de um círculo.
Cliques
Um emblema é testável em termos de acerto, mas é transparente por padrão: sem nenhum manipulador, seu clique falha e seleciona o nó abaixo, portanto, um emblema nunca é um ponto morto.
{ text: '3', onClick: (event, node, badge) => openNotes(node) }Dê a um emblema um onClick — ou declare callbacks.onBadgeClick pelo comportamento que todos compartilham — e ele pega o cursor do ponteiro e consome seu clique, deixando o nó desmarcado. Ambos disparam, primeiro o do distintivo. Pressionar um crachá ainda arrasta o nó em qualquer direção, e um ouvinte badgeClick no barramento de interação pode cancel() ambos.
Escopo
Os emblemas descrevem apenas o nó em que estão. Um cluster recolhido não agrega seus filhos - ande node.children em sua própria função badges se é isso que você deseja, pois só você sabe se um fato soma, vence ou nenhum dos dois.
Detalhe que acompanha o zoom
Um desenho que pode ser lido bem de perto é ilegível em uma visão geral, e um ponto que se ajusta a uma visão geral não pode dizer o que é. tiers permite que um nó carregue vários desenhos e escolha entre eles o tamanho que o nó renderiza atualmente.
const options = {
render: {
defaultNodeStyle: {
tiers: [
// Small: a coloured dot.
{ width: 32, height: 32, style: { shape: 'circle', size: 16 } },
// Once there is room for it: a labelled chip.
{ width: 140, height: 44, style: { shape: 'none', html: buildChip } },
],
},
},
}As camadas são ordenadas primeiro e a mais rica que se encaixa vence. Quando nada o faz, o nó desenha em seu estilo base, então o estilo que você já possui é o piso: um gráfico que declara não tiers não é afetado em todos os aspectos.
O que uma camada herda
Uma camada é mesclada com o estilo base, portanto, um canal que ela não nomeia é herdado. O desenho é a exceção: iconUnicode, iconClass, svgIcon, imagePath e html são um slot, então uma camada que nomeia qualquer um deles substitui o desenho da base (e seu imageFit) imediatamente. Um glifo em repouso com uma camada de carta não precisa de nada mais do que a carta:
const options = {
render: {
defaultNodeStyle: {
svgIcon: buildGlyph,
tiers: [
{ width: 140, height: 44, style: { shape: 'none', html: buildChip } },
],
},
},
}Uma camada que não nomeia nenhum desenho, apenas size, color ou text digamos, mantém o glifo da base.
Para retirar um canal que a camada não substitui de outra forma, defina-o como null, o que significa que a camada atrai aquele não. strokeColor: null elimina o contorno da base, por exemplo. undefined não pode substituir: significa "Não estou nomeando este canal", que é exatamente como o valor da base sobrevive.
Esta é a única camada onde null funciona dessa maneira. Em todos os outros lugares da cadeia de estilos null ainda cai na camada abaixo, porque um styleCb devolver um nulo diretamente dos dados do nó é uma forma comum e sempre significou "usar o padrão".
Para tornar um nó sem forma, use shape: 'none' em vez de limpar shape.
O que escolhe um nível
O tamanho renderizado: a área ocupada pelo nó em CSS pixels, que é footprint × zoom. Uma camada se envolve quando atinge seu minRenderedSize, cujo padrão é o próprio width da camada, então { width: 140 } é lido como "envolver-se quando houver espaço para desenhar 140 pixels de nó".
A escala de zoom por si só não funcionaria. Ele multiplica as coordenadas do layout inventado, de modo que a mesma escala significa um tamanho aparente diferente em um gráfico diferente, e um limite ajustado em um conjunto de dados estaria errado no próximo.
Uma camada engajada é mantida até que o tamanho renderizado caia para 85% do seu limite. Cada nó compartilha um limite, portanto, sem essa banda, uma tela estacionada em um deles piscaria como um todo.
A pegada permanece firme
A caixa que um nó reserva no layout não muda quando seu desenho muda. É layoutSize e o padrão é metade da camada declarada mais larga, portanto, a declaração de camadas fornece ao gráfico o espaçamento correto sem um segundo número. Nada se move quando uma camada muda: o desenho muda dentro de uma caixa que já tinha o tamanho certo e as forças nunca percebem a diferença.
width e height são declarados em vez de medidos, pelo mesmo motivo. O layout deve conhecer a pegada antes do primeiro tick, e um cartão medido só informa seu tamanho depois de desenhado.
Defina layoutSize para espaçar o gráfico de forma mais estreita ou mais larga do que a camada mais larga. Observe que uma camada é desenhada em seu tamanho de projeto somente quando sua largura é igual à área ocupada; um mais estreito engata proporcionalmente mais cedo e é desenhado proporcionalmente menor. Dê um minRenderedSize explícito se isso for importante.
O desenho de foco
focusTier é o desenho que um nó usa enquanto passa o mouse ou é selecionado por conta própria, qualquer que seja o zoom. É aqui que um nó diz tudo sobre si mesmo, portanto, ler um nó nunca significa dar zoom nele.
const options = {
render: {
defaultNodeStyle: {
tiers: [/* … */],
focusTier: { shape: 'none', html: buildDetailCard },
},
},
}Ele se funde no estilo base do nó, em vez de na camada ativa, portanto, shape ou html de uma camada de chip não pode vazar para ele. Ele mantém um tamanho constante na tela: o desenho é contra-escalado em relação ao zoom, então um cartão de 280×150 tem 280×150 CSS pixels, por mais distante que esteja o gráfico. E nunca toca a geometria do nó, então as arestas continuam pousando na camada abaixo e pairando não move nada.
O desenho de foco usa os badges do nó em sua própria borda, no mesmo tamanho de tela do cartão. Os próprios emblemas do nó ficam ocultos enquanto ele é exibido, portanto, nenhum é desenhado duas vezes. Dê a focusTier um badges próprio para mostrar um conjunto diferente no cartão.
Apenas um nó selecionado por conta própria é promovido. Uma seleção de caixa de cinquenta nós seria uma parede de cartas sobrepostas.
render.focusTierTrigger diz quando dispara: 'both' (o padrão), 'hover', 'selection' ou 'off'. Alcance 'off' quando você estiver incorporando uma predefinição de estilo enviada por outra pessoa e quiser que seu desenho de foco seja suprimido sem editá-lo.
Quando uma camada já mostra isso
Às vezes, o desenho do foco é o mesmo de uma das camadas de zoom: um glifo em repouso que paira em um chip, com o chip também desenhado quando o gráfico é ampliado o suficiente. Lá, passar o mouse pintaria um segundo chip menor sobre aquele que já estava na tela. focusTierYieldsAt nomeia o índice em tiers a partir do qual o desenho de foco para de disparar:
defaultNodeStyle: {
tiers: [{ width: 140, height: 44, style: { shape: 'none', html: chipFor } }],
focusTier: { shape: 'none', html: chipFor },
focusTierYieldsAt: 0,
}Enquanto o nó é desenhado nessa camada ou em uma camada mais rica, passe o mouse e uma seleção solitária deixe-o como está. Ele segue a camada ativa, não o nível de zoom, portanto usa os mesmos limites da troca. Aumentar o zoom com o cartão aberto faz com que ele diminua conforme a camada aumenta, e diminuir o zoom o traz de volta sem mover o ponteiro. Deixe-o sem definição em um nó cujo desenho de foco diz mais do que qualquer camada, e um nó sem tiers o ignora.
As dicas de ferramentas ainda disparam
Um cartão de foco não substitui a dica de ferramenta. Os dois são independentes e uma dica de ferramenta pode conter algo que o cartão não contém.
A troca em si
Um nó passa de um desenho para o outro, em vez de cortá-lo. O desenho de saída é mantido em uma camada própria e os dois desaparecem um ao lado do outro, de modo que um nó nunca esteja ausente no meio da troca, apenas brevemente mais suave onde eles se sobrepõem.
render.detailTransition é quanto tempo leva, em milissegundos. O padrão é 160 e cobre todos os desenhos que o zoom decide: um cruzamento de camadas, o desenho de foco e um cruzamento de rótulo minLabelFontSize. Defina como 0 para substituir o desenho em um único quadro:
const options = {
render: {
detailTransition: 0,
defaultNodeStyle: { tiers: [/* … */] },
},
}Cada nó na tela se cruza no mesmo momento, então um cruzamento inicia tantos fades quantos nós mudam. Medido em 210 nós, cada um contendo uma placa 140×44, o fade funciona a 60fps em toda a sua extensão; o que custa são cerca de 26ms no único quadro que a travessia já gasta, e esse número não cresce com a duração. Desative-o em um gráfico onde o quadro é mais importante do que a transição.
prefers-reduced-motion desliga sozinho, seja qual for a opção indicada.
Labels at a readable size
Um rótulo percorre o gráfico e é dimensionado com ele, portanto, quanto mais distante a visualização estiver, menor ela será pintada. Um gráfico abre em qualquer escala que o ajuste escolher, e essa escala cai à medida que o gráfico cresce: depois de algumas centenas de nós, o quadro de abertura é um texto que ninguém consegue ler, desenhado sobre o gráfico que está nomeando.
render.minLabelFontSize é o tamanho renderizado, em CSS pixels, abaixo do qual um rótulo não é desenhado. O padrão é 9.
const options = {
render: {
minLabelFontSize: 9,
},
}O teste é fontSize × zoom, em pixels renderizados em vez da escala de zoom: uma escala multiplica as coordenadas do layout inventado, então o mesmo valor significa um tamanho aparente diferente no próximo conjunto de dados.
É um número para toda a tela, rótulos de borda e rótulos de nós. A diferença entre eles já está no tamanho da fonte: um rótulo de borda é desenhado em defaultLabelStyle.fontSize (12 por padrão), enquanto um nó é derivado do nó max(12, size × 0.45), então um nó grande mantém seu rótulo com um zoom menor que um pequeno. O textFontSize de um estilo de nó substitui esse tamanho derivado.
0 desliga o portão e desenha todos os rótulos em cada zoom.
O que conta como um rótulo
O texto que a biblioteca coloca: label de uma aresta, text de um nó e o que quer que render.renderLabel retorne. O html, renderNode ou cartão de nível de um nó é seu desenho, e tiers é o que escolhe entre os desenhos.
Um rótulo exibido é mantido até que seu tamanho renderizado caia para 85% do limite, de modo que uma visualização estacionada na linha se estabilize em vez de piscar. Cada rótulo desenhado em um tamanho responde junto, seja qual for o elemento ao qual pertence.
Uma borda selecionada por si só mostra seu rótulo independentemente do zoom, escalonado em contador para que seja legível em vez dos dois pixels em que estava oculto. Uma aresta não possui dica de ferramenta nem painel, portanto seu rótulo é a única coisa que responde "o que é isso". Um nó não precisa de nada disso: ele já responde através de sua dica de ferramenta, da barra lateral e de seu desenho focusTier.
Um rótulo oculto não é apenas invisível, não está no gráfico – que é para onde vai o custo. Um rótulo é reposicionado a cada tick de simulação e, em um gráfico grande, passar sozinho custa mais que um quadro. O que resta depois do portão é o golpe, que é barato.
O que não faz
Acima do limite, os rótulos ainda podem se sobrepor. Um rótulo de aresta normalmente é mais largo que a aresta que ele nomeia e essa proporção não muda com o zoom. Esta opção é uma questão de legibilidade, não de organização.
API
Pivotick expõe um controlador de renderização que permite interagir diretamente com o mecanismo de renderização. Todos os métodos estão disponíveis online
Ajustar e aplicar zoom
graph.renderer.fitAndCenter()graph.renderer.zoomIn()
graph.renderer.zoomOut()