Plugins
Un plugin est un ensemble autonome d'éléments d'interface utilisateur, de raccourcis clavier et de hooks de cycle de vie qui s'installe dans un graphique sans que le noyau ait besoin de savoir qu'il existe. Enregistrez-en un de manière déclarative ou impérative :
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' }))Un plugin installé après la mise en ligne du graphique est rattrapé par la phase actuelle du cycle de vie, il n'y a donc pas de "trop tard" - et tout ce qu'il enregistre est supprimé avec l'interface utilisateur.
La forme d'un plugin
import type { PivotickPlugin } from 'pivotick'
const myPlugin: PivotickPlugin = {
name: 'my-plugin', // used for de-duplication and logging
install(ctx) { /* … */ }, // called once
}L'installation du même name deux fois avertit et ignore le doublon, de sorte qu'un plugin peut être répertorié dans plugins et réappliqué via graph.use sans doubler.
Quel install est remis
ctx est un {@link PluginContext} :
| Membre | A quoi ça sert |
|---|---|
graph | le live Graph — nœuds, bords, le bus d'événements de données, queryEngine |
ui | le UIManager — options, notifications, le mode magasin |
layout | l'échafaudage DOM, lisez live (jamais un instantané). layout.canvas est l'endroit où va le chrome ancré dans le canevas |
addElement(element, slot?) | mettre un UIComponent dans le cycle de vie, monté dans slot |
addPanel(panel) / removePanel(id) / refreshPanel(id?) | panneaux de la barre latérale — la même porte que UI.extraPanels |
addDockTab(tab) / removeDockTab(id) | une vitre dans le quai inférieur - la même porte par laquelle passe la table intégrée |
addRailMode(mode) / removeRailMode(id) | un mode sur le rail de gauche, à côté de Sélectionner/Créer/Afficher/Physique |
addTopBarAction(action) / removeTopBarAction(id) | une pilule dans la barre supérieure — la même porte que UI.topBar.actions |
addPivot(definition) | un enrichissement exécutable — voir Pivots et enrichissement |
onPhase(phase, cb) | crochet afterMount / graphReady / destroy ; renvoie un désabonnement |
addKeybinding(binding) | un raccourci qui est supprimé lorsque l'interface utilisateur est démolie |
keyManager | le registre des raccourcis clavier, pour tout ce qui est plus complexe |
Le key d'une liaison est constitué des modificateurs puis de la clé, rejoints par + : 'Shift+K', 'Ctrl+F', 'ArrowUp'. Écrivez les modificateurs sous la forme Ctrl, Meta (Cmd sur macOS), Shift, Alt, dans cet ordre, et utilisez Mod pour correspondre à Ctrl ou Meta — ce qui correspond à ce qu'un raccourci signifiait fonctionner sur les deux les plateformes veulent. La clé elle-même est KeyboardEvent.key textuellement, elle est donc sensible à la casse et Shift la modifie : les deux moitiés d'une paire d'annulation sont 'Mod+z' et 'Mod+Shift+Z'. Les liaisons ne se déclenchent jamais tant qu'un champ de texte a le focus.
Les layout emplacements disponibles dépendent du mode : canvas et notification toujours, graphnavigation dans tous les modes sauf static, mainheader / modal / slidePanel / moderail / toolpanel / flyout / legend en full et light, et sidebar en full uniquement. Lisez l'emplacement au moment où vous en avez besoin plutôt que de le mettre en cache.
Contribuer à un élément d'interface utilisateur
Prolongez UIComponent et le cycle de vie est piloté pour vous, y compris le démontage de tout ce qui est enregistré via track / listen / trackInteraction :
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),
}Les quatre phases sont mount(slot) → afterMount() → graphReady() → destroy(). graphReady se déclenche une fois la simulation stabilisée, cela peut donc prendre secondes après le montage sur un grand graphique — tout ce qui devrait être à l'écran appartient immédiatement à onMount / onAfterMount (une image différée s'il a besoin de graph.renderer, qui est construit après l'interface utilisateur).
Voir la carte de galerie Extend with a plugin pour un exemple complet et en direct.
Contribuer à un volet Dock
ctx.addDockTab place un volet dans le bottom dock, à côté de la table de données. Le dock est propriétaire de la région — sa hauteur, son séparateur, son pli et la bande qui nomme les volets — et votre volet est propriétaire de ce qu'il contient :
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(),
}),
}Quatre choses sont à savoir :
- Un onglet est un volet, pas une vue d'un seul. Si votre volet a plusieurs vues qui lui sont propres, il reste un seul onglet de dock et dessine son propre commutateur en
toolbar, appelanthandle.refresh()pour changer de corps — ce qui ré-invoquerender. C'est exactement ce que fait la table de données pourNodes/Edges, et pourquoi la bande du dock n'aplatit jamais les vues d'un volet à côté d'un autre volet. Changer votre propre DOM dans le dos du dock ne fonctionne pas : il conserve l'élémentrenderqui lui a été donné et rattacherait un nœud obsolète lors de la prochaine activation. Dessinez un commutateur interne sous la forme d'un contrôle segmenté, et non sous forme d'onglets : le niveau externe ressemble déjà à des onglets.pvt-dock-viewssur la bande etpvt-dock-viewsur chaque bouton (plusactive) sont publics, cela ressemble donc au switch de la table et suit le thème sans que vous le répétiez non plus. - Le premier volet construit la région. Installation des plugins après l'interface utilisateur est construite, donc un onglet arrive toujours trop tard pour que la propre porte de mode du dock ait dit oui en son nom. En enregistrer un donne naissance au dock, ce qui signifie que votre plugin fonctionne avec
UI.table: falseet n'a besoin que du modefull. renderest appelé une fois, paresseusement, à la première ouverture du volet ; l'élément est conservé et réattaché par la suite, il conserve donc sa propre position de défilement.toolbarest reconstruit à chaque activation, afin que ses contrôles puissent lire l'état actuel de votre volet.onActivate/onDeactivatesont le seul signal indiquant que vous êtes hors écran, et ce qu'il faut en faire dépend de votre volet. Le contenu qui est fonction de l'état actuel du graphique peut cesser de fonctionner lorsqu'il est masqué et redérivé au retour — c'est ce que fait la table. Le contenu qui manquer quelque chose (un volet surveillant un bus en direct — un événement est terminé une fois qu'il s'est déclenché) doit continuer à fonctionner et simplement arrêter de peinture, vidant son retard à son retour. Rien à propos des crochets ne préfère non plus.
Un onglet n'est pas un UIComponent, donc rien n'y intègre les phases du cycle de vie. Lorsque votre volet a besoin de graphReady, maintenez un UIComponent, un addElement et appelez addDockTab à partir de son onMount — voir Contribuer à un élément d'interface utilisateur ci-dessus.
Consultez la carte de galerie Ajouter un volet de dock pour un exemple complet et en direct : un volet avec deux vues propres, à côté de la table de données.
Contribuer à un mode ferroviaire
Les quatre modes du rail de gauche — Sélection, Création, Affichage, Physique — sont intégrés. Tout le reste vous appartient : addRailMode place votre propre mode sous un diviseur, après eux, classé parmi ses pairs par order.
Un mode pointeur possède le panneau d'outils contextuel. Déclarez son tools et le panneau les dessine de la même manière que les sélections et les créations sont dessinées, avec l'armement, les états activés/désactivés et le repli gérés pour vous :
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] })Le kind d'un outil décide de son comportement. Un 'action' s'exécute une fois et quitte le mode tel qu'il était. Un 'toggle' arme un outil modal : le panneau s'effondre et ** le bouton du rail se transforme en l'icône et l'étiquette de cet outil **, exactement au moment où l'emplacement de Select devient Lasso. Un 'default' est l'outil sur lequel repose le mode ; nommez-le dans defaultTool et c'est à cela que revient une bascule.
tools peut également être une fonction ****, relue à chaque fois que le panneau est dessiné — utilisez-la lorsque les lignes dépendent de ce qui est sélectionné, plutôt que de simplement griser. Pour tout ce qu'une ligne ne peut pas exprimer, render() renvoie un élément ajouté sous aux lignes :
ctx.addRailMode({
id: 'explore', label: 'Explore', icon: compassSvg,
tools: () => toolsFor(ctx.graph.renderer.getGraphInteraction().getSelectedNode()),
render: () => depthSlider(),
})Un mode flyout ouvre une superposition de paramètres, comme Affichage et Physique. Sous-classe Flyout et main addRailMode une usine — le noyau monte le panneau, l'ouvre exactement pendant que votre mode est actif et l'enlève avec le mode :
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),
})Les modes s'excluent mutuellement, le vôtre exclut donc gratuitement View et Physics : un magasin, un mode actif.
Trois choses à savoir :
iconest une chaîne SVG brute, injectée sous la forme HTML et jamais désinfectée. Elle doit provenir d'une source de confiance. CSS le dimensionne à 20px sur le rail, 18px dans le panneau.- **
shortcutsuit les règles normales de liaison de touches ** : revendiquer une clé, quelque chose d'autre la possède déjà, l'ombre, avec un avertissement, jusqu'à ce que votre mode soit supprimé. - **
onExitest l'endroit où vous désarmez. ** Il s'exécute lorsque le mode est quitté et lorsqu'il est supprimé alors qu'il est actif — auquel cas le rail revient à Select. Il ne fonctionne pas lors du démontage de l'interface utilisateur ; les broyeurs que votre plugin suit déjà couvrent cela.
addRailMode renvoie un broyeur et fonctionne dans tous les modes — le bouton n'est dessiné que là où il y a un rail, ce qui signifie full et light.
Pilotage de la fenêtre
Tout ce qui parcourt le graphique (une mini-carte, une vue d'ensemble, un contrôle « accéder à ») utilise deux méthodes de rendu :
// 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 laisse l'échelle seule à moins que vous n'en passiez une, c'est donc la primitive pour le panoramique. Pour lire où la vue est actuellement est, inversez les coins du canevas plutôt que de rechercher une transformation de zoom - ceci est indépendant du moteur de rendu :
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() (s'adapte à tout), zoomIn() / zoomOut() et focusElement(nodeOrEdge) restent les raccourcis pour les cas courants.
La mini-carte
Un plugin propriétaire : un aperçu en cache de l'ensemble du graphique ancré dans un coin du canevas, avec un rectangle montrant ce qui est à l'écran. Cliquez dessus pour recentrer la vue ; faites glisser le rectangle vers lequel effectuer un panoramique. Une très petite bascule dans le coin auquel il fait face le replie sur ce bouton et le ramène.
Le mode full en monte un pour vous — il fait partie du chrome de ce mode, comme l'en-tête, la barre latérale et le rail de mode. Tous les autres modes vous laissent le choix :
// 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 prend le même {@link MinimapOptions} que le plugin, donc { minimap: { position: 'top-left' } } configure celui que le mode complet apporte. UI.minimap: false le supprime. Passer votre propre minimap() dans plugins est également gagnant – le mode complet est mis de côté plutôt que d'en monter un deuxième – de sorte qu'un plugins: [minimap({ … })] existant continue de fonctionner exactement comme il le faisait.
<!-- browser build: it hangs off the global, like Node and Edge -->
<script>new Pivotick(el, data, { plugins: [Pivotick.minimap()] })</script>Options
| Options | Type | Valeur par défaut | Ce que ça fait |
|---|---|---|---|
position | 'bottom-right' | 'bottom-left' | 'top-right' | 'top-left' | 'bottom-right' | Dans quel coin il s'ancre. 'bottom-right' est le seul coin que le chrome intégré laisse libre en mode full. |
width | number | 200 | Largeur en CSS pixels, bordure incluse. |
height | number | dérivé | Hauteur en CSS pixels. Omis, il suit le rapport hauteur/largeur du canevas (bridé entre 70 et 400 px), de sorte que le rectangle conserve la forme de la fenêtre d'affichage réelle. |
collapsed | boolean | 'auto' | false, ou 'auto' pour les montures en mode full | Dans quel état il s'ouvre. La bascule est toujours là ; c'est seulement là que ça commence. Voir S'écarter pour 'auto'. |
Rien d'autre n'est configurable, car rien d'autre ne doit l'être : le niveau de détail et la cadence de redessinage s'adaptent au graphique.
Ce qu'il dessine
Les nœuds sont des points de la couleur que leur a donnée le moteur de rendu, les bords sont des lignes fines en dessous et les notes – le seul contenu ayant sa propre taille – sont dessinées à l'échelle sous forme de blocs translucides dans leur propre couleur, sur les nœuds comme ils le sont sur le canevas. Le contenu masqué et filtré est laissé de côté. Au-delà de quelques milliers de nœuds, l'image devient une carte de densité (un tampon par nœud, pas de bords), qui se lit mieux que des dizaines de milliers de points superposés ; les notes sont dessinées dans les deux sens. Tout est mis en cache et re-rastérisé uniquement lorsque l'image a vraiment changé - un nœud supprimé, une note déplacée, la mise en page réglée, un filtre appliqué - de sorte que le panoramique et le zoom restent libres quelle que soit la taille du graphique.
Une carte HTML (html sur shape: 'none') n'a pas de couleur que la mini-carte peut lire, donc son point prend la couleur du plus petit dessin qui est une forme : le style de base, ou bien le premier de ces niveaux en tiers. Sans aucun, ou avec une couleur transparent, le point est dessiné avec la propre encre de la mini-carte (--pvt-minimap-ink). Les points sont dimensionnés en fonction de ce que couvre le nœud, donc une carte ou un nœud renderNode obtient un point de la taille d'une carte plutôt qu'un point de taille size.
S'écarter du chemin
Une mini-carte que vous avez demandée reste là où vous l'avez placée. Le mode full monté en votre nom n'a pas été demandé, il ouvre donc collapsed: 'auto' et prend en compte le canevas : il reste ouvert tant que le canevas mesure au moins quatre mini-cartes de large et de haut, et se replie jusqu'à la bascule en dessous. Il continue de suivre la toile à partir de ce moment-là – se repliant lorsque la barre latérale s'ouvre dessus ou que la fenêtre se rétrécit, et revient lorsque la pièce le fait.
Au moment où quelqu'un le plie ou le ramène - par la bascule ou via setCollapsed() - cela s'arrête : un choix explicite reste, et aucun redimensionnement ultérieur ne l'écrase. Passez un collapsed: true / false explicite pour vous désinscrire du 'auto' dès le début.
La flèche de la bascule pointe vers le coin dans lequel la mini-carte s'ancre – la direction dans laquelle elle se replie – et se retourne une fois qu'elle est réduite. Replié, il ne dessine rien, pas même le rectangle, donc il ne coûte rien tant qu'il est à l'écart ; l'ouvrir à nouveau pixellise l'apparence de la toile à ce moment-là. setCollapsed(boolean) et isCollapsed() sur l'instance Minimap génèrent la même chose à partir du code.
Ce que ça dessine et ce que ça coûte
La mini-carte mappe les limites du contenu ** avec la fenêtre d'affichage actuelle **, de sorte que le rectangle est toujours visible et de taille honnête - effectuez un zoom arrière et le graphique se rétrécit à l'intérieur du cadre plutôt que le rectangle en glisse.
Il conserve le graphique dans un bitmap hors écran et ne le re-rastérise que lorsque l'image a réellement changé : lors d'un changement de données, lorsqu'un filtre masque ou restaure des nœuds, lorsqu'un nœud est déposé après un glissement, tous les 10 ticks de simulation pendant qu'une mise en page se stabilise et lors du redimensionnement. Le panoramique et le zoom redessinent une image et un rectangle, donc la navigation coûte le même que le graphique comporte 20 nœuds ou 50 000.
Les détails se dégradent avec la taille, volontairement :
| Graphique | Dessiné comme |
|---|---|
| ≤ 1500 nœuds | un point par nœud dans la couleur résolue par le moteur de rendu, plus les bords fins (dépassés au-delà de 4 000 bords) |
| > 1500 nœuds | un tampon par nœud dans une seule encre, accumulation d'alpha – régions denses lues comme une carte de densité, et aucun style par nœud n'est résolu du tout |
Il fonctionne dans les modes full, light et viewer, et full est celui qui le monte sans qu'on le lui demande. Dans static — qui ne promet aucune interaction — il n'est pas monté et son installation ici prévient.
Voir la carte de la galerie Minimap pour une carte en direct.