Pivots et enrichissement
Un pivot est un enrichissement exécutable : "montre-moi ce que la source a d'autre à ce sujet". Vous le déclarez, la bibliothèque l'exécute, organise les résultats pour le tri et les valide dans le graphique avec la provenance de chaque élément une fois que l'analyste le dit.
Le problème qu’il résout est que le nœud intéressant a généralement trop de connexions. Un seul indicateur peut être relié à des milliers d’autres. Ainsi, un pivot est constitué de deux appels plutôt qu'un : un appel bon marché qui dit ce qui existe, et un appel coûteux qui ne fonctionne que sur un numéro accepté par l'analyste.
Vocabulaire, utilisé de manière cohérente partout ci-dessous
- pivot : l'enrichissement exécutable
- provider : les deux fonctions que vous fournissez
- candidat : résultats récupérés, pas encore dans le graphique
- ingest : engager les candidats choisis dans le graphe
- reject : un licenciement explicite, retenu pour la séance
- source : la balise de provenance écrite sur tout ce qu'un pivot atterrit
- save : écriture d'un résultat ingéré en retour out, dans le système d'où il provient
- non enregistré : sur la toile, pas encore réécrit — un décompte exact, pas une estimation
summarize → fetch → triage → ingest → save
"2,143" the 210 the 12 on canvas in the source systemSeul fetch est requis. Tout le reste sur cette ligne est une étape à laquelle vous optez.
Voyez-le fonctionner : Pivot & enrich pour le flux de rétrécissement, et Reject & retry pour le triage et les états d'échec.
Utiliser un est ce qu'un analyste fait, dans l'ordre, et ce que chaque étape coûte au backend. Tout à partir de Enregistrer un est la référence pour celui qui en connecte un, et Ce que votre source a à offrir est le même contrat lu du côté du service.
En utiliser un
1 · Entrez en mode Pivot. Appuyez sur P ou cliquez sur l'emplacement Pivot sur le rail gauche. Le slot n'existe que tant qu'au moins un pivot est enregistré. Rien n'a encore été demandé à un backend, et la sélection de nœuds pendant que le mode est fermé ne demande rien non plus. La sélection de cinquante nœuds dans une boîte pour les faire glisser ne devrait pas déclencher cinquante requêtes de comptage.
2 · Choisissez une origine. L'origine est l'ensemble de nœuds sur lequel porte la question. Pick origin est l'outil de repos, donc un simple clic le définit. Origine Lasso dessine autour de plusieurs. Tout ce qui était déjà sélectionné lorsque vous êtes entré dans le mode devient l'origine et Clear le vide. Une origine vide a encore son utilité : les pivots déclarés origin: 'none' sont ceux qui s'appliquent lorsque rien n'est sélectionné.
3 · Lisez ce qui s'applique. L'entrée dans le mode est l'intention qui exécute summarize, une fois par pivot applicable, regroupée en un seul appel pour une origine multi-nœuds. Chaque entrée montre ce que prétend son fournisseur, écrit sous la forme ~2,143. Le tilde marque un nombre affirmé par le fournisseur plutôt que celui compté par la bibliothèque. Un pivot sans summarize affiche un simple Run à la place. Si rien ne s'applique, le panneau l'indique plutôt que d'afficher une liste vide.
4 · Étroit jusqu'à ce que la porte s'ouvre. Un pivot qui déclare maxCandidates maintient Fetch hors de portée tant que le décompte le dépasse, et indique le nombre, la limite et la voie à suivre. Cocher un contrôle de rétrécissement demande à nouveau summarize avec ce choix, donc 2 143 devient 210 et Fetch s'allume tout seul. Rétrécissement clair recommence. À côté, Fetch & ingest affiche chaque résultat sur le graphique sans examen. Il est proposé uniquement lorsque le pivot a laissé autoIngest non réglé.
5 · Triez ce qui est revenu. Les résultats ne touchent pas le graphique. Ils ouvrent l'onglet Review dans le dock inférieur, un volet par fournisseur que vous avez exécuté, répertorié sur le côté. Cliquez n'importe où sur une ligne pour la marquer, Maj-clic pour porter la marque sur une plage, et Sélectionner tous les n correspondants respecte le filtre actuel ; Rechercher des lignes… filtres (regex facultatif) et tri des colonnes. Rejeter la sélection et Rejeter tous les restes éliminent explicitement les lignes et sont mémorisés pour la session, tandis que le × ferme simplement un volet et ne rejette rien. Re-run demande à nouveau avec le même rétrécissement. Au pied de la bande, Close failed supprime chaque récupération qui n'a rien rapporté et Close all vide la file d'attente ; ni l’un ni l’autre ne rejette quoi que ce soit. Les candidats ne sont pas le graphique a le reste.
6 · Ingérer et annuler si c'était faux. Ingérer sélectionné (12) valide exactement ces douze, placés autour du nœud à partir duquel vous avez pivoté et étiquetés avec le pivot comme source. Ingérer tous les 213 à côté ignore le marquage et valide tout ce que le fournisseur propose toujours, quelle que soit la table filtrée. Le toast indique Ingested 12 nodes, 14 edges. Le reprendre est le contrôle d'annulation dans la barre supérieure, qui est là en permanence plutôt que aussi longtemps que dure un toast, et inverse toute la course - ses bords et tous les enfants dans lesquels il a fusionné sont inclus.
**7 · Enregistrez-le, si le pivot le peut. ** L'ingestion met les douze sur le canevas ; il ne les écrit nulle part. Le pied du panneau Pivot indique combien d'éléments cette session a extraits et non réécrits, et Save les écrit. Un pivot qui déclare non save ne contribue jamais à ce nombre — voir Enregistrement des résultats du pivot.
Une ingestion qui ne laisse rien à déterminer ferme le volet et la bande passe au fournisseur suivant en attente. Les restes le maintiennent ouvert, tout comme une nouvelle exécution qui y attend. Une fois le dernier volet terminé, un dock ouvert par la révision se replie là où il se trouvait, de sorte qu'un pivot quitte la mise en page qu'il a trouvée.
Le pivot en un clic
Les étapes 2 à 4 concernent une question qui mérite d'être lue en premier. La plupart ne le sont pas. Cliquez avec le bouton droit sur un nœud et Pivot ▸ répertorie chaque pivot qui s'y applique avec ce que chacun annonce. Choisir une ligne l'exécute ici et là : pas de panneau, rien à remplir. L'origine est la sélection lorsque le nœud cliqué lui appartient et ce nœud seul sinon, donc un pivot groupé est accessible sans quitter le menu.
L'ouverture du sous-menu est également ce qui demande. Chaque pivot avec un summarize se voit poser la propre question du panel, cette origine sans rien rétrécir, et sa ligne contient un espace réservé jusqu'à ce que le décompte arrive : ~2,143, consultatif comme chaque numéro annoncé par un fournisseur. Un pivot qui n'annonce rien ne montre rien et fonctionne quand même. Les questions ne s'éteignent qu'une fois le sous-menu maintenu un instant, donc un pointeur traversant Pivot ▸ en route vers un autre endroit ne demande rien, et elles sont annulées s'il se ferme en premier. Les réponses sont mises en cache dans les deux sens : un deuxième regard apparaît immédiatement et le panneau réutilise ce que le menu a appris. Un registre plus long que le menu peut contenir des parchemins, coupé au milieu de la rangée pour qu'il se lise comme continu.
La destination des résultats dépend du nombre d’entre eux qui sont nouveaux sur le canevas :
- Jusqu'à
pivotQuickIngestLimitnouveaux candidats (50 par défaut) atterrissent directement, étiquetés et annulables comme n'importe quelle autre ingest. - Plus que cela ouvrez plutôt l'onglet Review, exactement comme le ferait une exécution de panneau.
- Un refus de plafond se produit avant que quoi que ce soit ne soit récupéré, il n'y a donc rien à vérifier. Le panneau Pivot s'ouvre sur ce pivot avec le numéro et le rétrécissement qui le soulève.
Une rangée s'étend sans rien rétrécir. Lorsqu'un pivot comporte en réalité plusieurs exécutions, l'analyste choisit entre, comme enrichir un objet via l'un de ses attributs, déclarez menuChoices : la ligne ouvre alors un sous-menu avec une ligne par choix, chacune exécutant le pivot avec son propre rétrécissement, et une dernière ligne ouvrant le panneau sur ce pivot. Il est synchrone et s'exécute à chaque ouverture du menu, comme appliesTo. Un refus de plafond d'un choix ouvre le panneau sur le rétrécissement du choix. Renvoyez [] pour une ligne simple.
menuChoices: ([object]) => attributesOf(object).map((a) => ({
label: `${a.type}: ${a.value}`,
narrowing: { attribute: [a.id] },
})),Les candidats déjà sur le canevas ne sont pas nouveaux et ne dépassent jamais la limite, donc réexécuter un pivot sur le terrain que vous avez parcouru reste un geste en un seul clic. Un bord entre deux nœuds déjà affichés à l’écran compte, car c’est une ligne sur laquelle on vous aurait autrement posé des questions. Un pivot qui a déclaré autoIngest a déjà répondu à la question et sa réponse est valable : true atterrit toujours, false révise toujours. La limite ne décide que pour les pivots qui l'ont laissée inactive, et seulement pour ce geste ; un panneau Fetch met toujours en scène.
Jusqu'à ce que la réponse soit connue, l'exécution n'a rien proposé, il n'est donc pas nécessaire d'afficher l'écran pour dire qu'il fonctionne. Un onglet Review rejoint la bande de lecture du dock Review… pendant que le fetch est sorti, sans déplier le dock ni avancer une vitre ; cliquez dessus pour regarder la course ou l'annuler. Seul un résultat qui doit être décidé se présente, et une course qui atterrit enlève sa note avec elle. Les deux réponses sans rien à décider sont celles du notifiant : un failed fetch le dit avec une nouvelle tentative, et un résultat supérieur à l'absolu candidateCeiling indique le nombre et la limite.
Trois façons d'y accéder, donc : le mode rail, le sous-menu Pivot ▸ du menu contextuel d'un nœud (absent, jamais désactivé, quand rien ne s'applique), et, avec pivotRimBadgeVisible: 'always', un badge de jante, qui ouvre le mode limité à son propre pivot, ou, lorsque la jante porte un badge pour chacun d'eux, au nœud. Un badge pour un nœud avec exactement un pivot qui n'a rien à restreindre l'exécute directement, comme une ligne de menu.
En enregistrer un
Trois portes, un registre, dans l'ordre dans lequel vous les atteindrez :
// 1. Options, at construction. Lands before the UI is built, so the rail button is
// already right on the first paint.
const graph = new Pivotick(el, data, { pivots: [correlations] })
// 2. Imperatively, at any point. Returns a disposer.
const remove = graph.pivots.register(correlations)
// 3. From a plugin's install, beside addPanel / addDockTab / addRailMode.
ctx.addPivot(correlations)graph.pivots est à la fois le registre et le runtime. for(), run(), undo(), invalidate() et le modèle candidat y sont tous suspendus.
Un fournisseur complet
Tout ce dont un pivot en activité a besoin, sans rien laisser de côté. Le reste de cette page est la référence de chaque pièce ; c'est la chose à copier en premier et à réduire.
post ci-dessous est votre propre assistant JSON — il lance un non-2xx et passe signal, de sorte que le pivot se lit comme le contrat plutôt que comme HTTP.
const correlations = {
id: 'correlations', // the provenance tag written on everything it lands
label: 'Correlations', // shown verbatim, so translate it yourself
maxCandidates: 2000, // refuse to fetch while the count is above this
// Which nodes this offers itself for — return the ones you accept, and a mixed
// selection runs it against just those. Omit it and it applies to everything.
appliesTo: nodes => nodes.filter(node => node.getData()?.type !== 'case'),
// The cheap call: what is out there, and what you could narrow by.
summarize: async (nodes, narrowing, { signal }) => {
const { total, byType } = await post('/api/correlations/count', {
ids: nodes.map(node => node.id),
types: narrowing.type ?? [], // pass the narrowing on so the count tracks it
}, signal)
return {
total, // judged against maxCandidates
facets: [{
key: 'type', // the key you read back out of `narrowing`
label: 'Type',
type: 'multiselect',
options: byType.map(row => ({ label: row.label, value: row.value, count: row.count })),
}],
}
},
// The expensive call: a flat fragment, run only on a number the analyst agreed to.
fetch: async (nodes, narrowing, { signal }) => {
const { items } = await post('/api/correlations', {
ids: nodes.map(node => node.id),
types: narrowing.type ?? [],
}, signal)
return {
nodes: items.map(item => ({
id: item.uuid, // an id already on canvas is skipped, never overwritten
data: { label: item.value, type: item.type, seen: item.first_seen },
})),
edges: items.map(item => ({
from: item.origin_uuid, // both endpoints must land for the edge to land
to: item.uuid,
data: { type: 'correlation' },
})),
}
},
// The write half, and the only optional one here. Omit it and this pivot's results
// are *not savable* — never counted unsaved, and no Save offered. See below.
async save({ origin, nodes, edges }, ctx) {
const written = await post('/api/correlations/save', {
event: origin[0]?.id,
nodes: nodes.map((node) => node.getData()),
}, ctx.signal)
return {
savedNodeIds: written.ok,
savedEdgeIds: edges.map((edge) => edge.id),
message: written.refused ? `${written.refused} refused by the server` : undefined,
}
},
}
new Pivotick(document.querySelector('#graph'), data, { pivots: [correlations] })Quatre choses sur lesquelles il est facile de se tromper, toutes visibles ci-dessus :
- **
narrowingest à vous d'honorer. ** Il arrive sous la forme{ [facetKey]: value }, en utilisant les clés que vos propres facettes ont déclarées. L’ignorer est légal, mais le décompte ne bouge jamais et un pivot au-dessus du plafond ne peut jamais être débloqué. signaln'est pas une politesse facultative. Quitter le mode Pivot ou changer l'origine annule les questions en vol. Transmettez-le à chaque demande, ou vous payez pour des réponses que personne n'attend.ctx.isStale()est le même contrôle pour les transports qui ne prennent aucun signal.- Lancer pour un échec, renvoyer vide pour un vide. Une erreur levée fait surface sur l'entrée pivot avec un Retry.
{ total: 0 }est un honnête « rien là-bas », et les deux se lisent complètement différemment pour un analyste. - Dites quand vous ne savez pas. Une source interrogée en direct peut ne pas avoir de décompte avant de répondre. Renvoie
{ total: null, facets }: aucun décompte n'est tiré,maxCandidatesne le refuse pas, etcandidateCeilinglimite toujours ce quefetchrenvoie.0dirait qu'il n'y a rien. - Les identifiants sont la clé de déduplication. Renvoie l'identifiant stable du système source, pas un identifiant par réponse, ou chaque réexécution fait atterrir à nouveau les mêmes nœuds en tant que nouveaux candidats.
Le contrat de prestataire
interface PivotDefinition {
id: string // provenance tag, menu key, triage key
label: string // used verbatim, so it can be translated
icon?: string // trusted SVG, injected as-is
origin?: 'selection' | 'none'
appliesTo?: (nodes: Node[]) => boolean | Node[]
summarize?: (nodes: Node[], narrowing: PivotNarrowing, ctx: PivotContext) => PivotSummary | Promise<…>
fetch: (nodes: Node[], narrowing: PivotNarrowing, ctx: PivotContext) => PivotResult | Promise<…>
autoIngest?: boolean
maxCandidates?: number
save?: (payload: PivotSavePayload, ctx: PivotSaveContext) => PivotSaveOutcome | Promise<…>
autoSave?: boolean
}Les deux appels prennent un array de nœuds, donc un pivot sur cinquante d'entre eux est une requête backend plutôt que cinquante. Les deux prennent l'actuel rétrécissement. Les deux obtiennent un PivotContext portant signal et isStale(), le même modèle d'annulation utilisé par chaque hook asynchrone de la bibliothèque.
appliesTo : toute l'origine, ou une partie
Renvoyez un booléen pour une règle sur l'origine dans son ensemble, et les nœuds que vous acceptez pour une règle qui lit un nœud à la fois. Un tableau vide signifie la même chose que false.
appliesTo: nodes => nodes.length >= 2 // whole origin
appliesTo: nodes => nodes.filter(n => n.getData()?.type === 'domain') // per nodeLa forme par nœud compte dès qu'une sélection est mixte : un domaine et une IP choisis ensemble conservent chacun leurs propres fournisseurs plutôt que de ne laisser que ceux qui acceptent les deux, et le panneau l'indique sur l'entrée — S'applique à 3 des 5 sélectionnés.
Tout ce qu'il conserve est l'origine avec laquelle le fournisseur est appelé : summarize et fetch ne voient jamais un nœud qu'il a refusé, et le résumé mis en cache est saisi par cette origine restreinte. Il est relu à chaque changement d'origine, alors gardez-le synchrone et bon marché.
graph.pivots.for(nodes) // everything that applies, partially included
graph.pivots.originFor('whois', nodes) // the share this one would run onAppeler run avec une origine qu'un pivot rejette résout entièrement 'failed' plutôt que de récupérer une origine vide - une exécution qui semblait bien et ne répondait à rien serait pire.
summarize : le moins cher
Affiché dans son intégralité sous Un fournisseur complet. Son total est le nombre par rapport auquel le capuchon est jugé, et son facets devient les contrôles de rétrécissement : vous déclarez la forme, la bibliothèque dessine les widgets. Transmettez le rétrécissement à votre backend afin que le décompte suive ce que l'analyste a choisi, sinon la porte ne pourra jamais se lever.
Une facette peut déclarer un default : les valeurs d'option a multiselect commencent par cochée, ou la valeur à laquelle commence un select ou boolean. Le panneau le dessine et Effacer le rétrécissement y revient, mais la clé reste en dehors du rétrécissement pendant qu'elle est maintenue, votre fournisseur doit donc lire une clé manquante par défaut. Un multiselect vidé avec une valeur par défaut est envoyé comme [], ce qui signifie aucun.
{ key: 'element', label: 'Element', type: 'multiselect', default: ['attribute', 'object'],
options: [/* attribute, object, tag, … */] }Omettez entièrement summarize et le pivot n'offre aucun décompte ni aucun rétrécissement, juste un bouton Exécuter. C'est légal, et alors fetch doit pouvoir être appelé en toute sécurité à l'aveugle, car rien ne peut le bloquer.
Les décomptes sont consultatifs. Toujours.
summarize renvoie une réclamation, pas un contrat, et il diffère légitimement de ce qui ingère des terres : déduplication d'abord, puis autorisations, obsolescence et fournisseurs qui se rapprochent délibérément. L'interface utilisateur marque la différence avec un tilde : ~2,143 correlations est le numéro du fournisseur, tandis que 210 fetched et 12 selected sont ceux de la bibliothèque. Un rétrécissement n'est jamais une erreur, et un décompte n'est jamais porteur pour la mise en page.
fetch : le vrai
Renvoie un fragment plat : { nodes: RawNode[], edges: RawEdge[] }. Aucun nœud renvoyé ne nomme un parent existant sur le canevas. La nidification se déplace dans RawNode.children, donc un conteneur arrive en tant que nouveau nœud portant son propre sous-arbre. Un résultat edges-only est légal : les bords dont les extrémités sont toutes déjà sur le canevas deviennent leurs propres lignes de tri.
Types de facettes
Le rétrécissement utilise tous les types de facettes connus du moteur de requête, à l'exception de regex, qu'aucun backend ne peut être invité à évaluer : text, select, multiselect, numberRange, boolean. Regex réside dans les filtres côté client du volet de tri, où les lignes sont déjà disponibles.
Comment le panneau les dessine et d'où vient la ligne de répartition
Un multiselect est dessiné sous la forme d'une liste de cases à cocher avec chaque option et son count à l'écran, de sorte que les nombres que vous affinez restent visibles pendant que vous affinez. Un choix unique select reste une liste déroulante et porte son décompte dans les étiquettes d'option.
Sous le total, le panneau imprime de quoi il est fait, pris à partir de la première facette multiselect : ces options partitionnent le résultat. Une fois que cette facette a un choix, envoyé ou par défaut, seules les options sélectionnées sont répertoriées. Les décomptes d'une facette à choix unique sont des alternatives plutôt que des parties, ils n'y sont donc pas additionnés.
Gating : le capuchon et le plafond
maxCandidates: 2000 // per pivot: refuse to fetch while the count exceeds it
new Pivotick(el, data, { pivotCandidateCeiling: 10_000 }) // absolute, all pivotsLes deux refusent, et ni les troncatures ni les échantillons. Un refus comporte toujours trois choses dans l'ordre : le nombre, la limite et la voie à suivre. « Trop de résultats » sans chiffre n'est pas une chose sur laquelle un analyste peut agir.
Le plafond est jugé sur la base du rétrécissement actuel, ce qui lui permet de se soulever. Affinez de 2 143 à 210 et Fetch s'allume tout seul. Le plafond concerne la mémoire des objets candidats plutôt que la table, qui virtualise.
Les candidats ne sont pas le graphique
Rien de ce qu'un pivot renvoie n'est dans le graphique, dans le tableau de données ou dans le nombre de facettes jusqu'à ce qu'il soit ingéré. Les résultats préparés pour le tri ouvrent l'onglet Review du dock - un volet par pivot, répertorié sous forme d'onglets verticaux sur le côté, chacun avec son propre nombre et sa propre sortie - avec les propres colonnes du fournisseur, les filtres de la table de données, le tri, la pagination et une action d'ingestion.
- Le rejet est explicite et mémorisé pour la session, saisi par pivot. La prochaine élection ne propose plus ce candidat, et la ligne d'en-tête indique combien de candidats ont été supprimés. Une ligne rejetée est barrée sur place plutôt que déplacée et reste réversible.
- Ce qu'un pivot retient est répertorié sur son entrée dans le panneau Pivot, nommé comme le tableau l'a nommé, avec restore sur chaque ligne et Restaurer tout en dessous. Il reste là une fois le volet fermé, donc un rejet peut être reconsidéré sans exécuter à nouveau le pivot pour le retrouver.
- Les nœuds et les arêtes sont des blocs séparés, chacun nommé et compté lorsqu'une exécution renvoie les deux. Un résultat constitué uniquement de bords est une table à part entière, et non un volet vide.
- Une ligne de conteneur s'ouvre. Elle porte un nombre Children de ce qu'elle contient directement, et un curseur répertorie le contenu, avec les attributs partagés de chaque enfant et une note où un enfant est lui-même un conteneur. Ouvrir une ligne ne la marque pas, et rien à l'intérieur ne peut être récupéré seul : l'ingestion de la ligne prend tout le conteneur.
- La fermeture d'un fournisseur ne rejette rien. Les restes non triés reviennent lors de la prochaine exécution. Rejeter tout ce qui reste est le seul geste qui les élimine.
- Une réexécution remplace le volet de ce fournisseur. Si vous avez des lignes marquées, cela le dit et propose Afficher le nouveau / Continuez le tri, plutôt que de jeter votre tri sans vous le demander.
- Ingérer dans un groupe fait plier les lignes sélectionnées, un groupe par type, hors de l'origine. Voir Atterrissages pivotants. Il est proposé sauf si
UI.simplifyest désactivé. AvecpivotIngestGrouped: true, chaque atterrissage est regroupé et le bouton indique Ingest loose à la place.
Tout ce que fait le volet est également accessible par programme :
graph.pivots.candidates('correlations') // the staged set
graph.pivots.mark('correlations', id) // …markAll / reject / rejectRemaining
await graph.pivots.ingest('correlations') // commit the marked ones
await graph.pivots.ingest('correlations', 'triage', { group: true }) // …folded, one group per type
graph.pivots.discard('correlations') // close it; rejects nothing
graph.pivots.rejectedRows('correlations') // what the session is holding back
graph.pivots.unreject('correlations', id) // …unrejectAll for every one of themautoIngest
Pour un petit résultat fiable que personne ne veut comprendre, comme l'expansion d'un conteneur dans sa douzaine d'enfants, déclarez autoIngest: true. Les résultats arrivent directement et l'annulation de la barre supérieure les reprend comme n'importe quelle autre acquisition. Le prestataire en décide, car lui seul sait si ses propres résultats sont suffisamment petits et suffisamment fiables pour éviter le tri.
Le laisser non défini n'est pas la même chose que false. Il indique que le pivot n'a pas de vue, ce qui laisse un pivot en un clic libre de décider seul de la taille ; false indique que les résultats ont toujours besoin d'être surveillés et sont honorés quelle que soit la manière dont la course a commencé.
autoIngest: true // always lands
autoIngest: false // always reviewed
// unset: a one-click run decides on size
new Pivotick(el, data, { pivotQuickIngestLimit: 50 }) // that size, all pivotsQuelles que soient les terres, d'ici ou de Review, peuvent arriver pliées en groupes d'atterrissage au lieu d'être détachées :
new Pivotick(el, data, { pivotIngestGrouped: true }) // every landing, one group per typeIngérer et ce qu'il est permis de faire
Un geste, un lot, un dataBatchChanged, dans cet ordre :
- Dedup — un identifiant déjà sur le canevas est ignoré, jamais écrasé, qu'il était là lorsque les candidats ont été mis en scène ou qu'une autre exécution l'a obtenu pendant qu'ils attendaient.
- Union des enfants par identifiant.
- Placement près du nœud d'origine, instable. Les propres
x/yd'un fournisseur gagnent et des graines de course sans origine au centre de la fenêtre d'affichage. - Provenance, puis le lot atterrit.
L'ingestion est purement additive. Les seules choses qui sont supprimées sont removeBySource et exécutez l'annulation.
Une porte, appelée une fois avec l'ensemble :
callbacks: {
onBeforeIngest: async ({ pivotId, origin, candidates, trigger, confirm }) => {
if (candidates.nodes.length < 50) return true
return await confirm({ title: `Add ${candidates.nodes.length} nodes?` })
},
}Renvoyez false pour veto, ou { accept: true, nodes, edges } pour obtenir un sous-ensemble. Une clé omise signifie "comme demandé", exactement comme le lit onBeforeDelete.
Provenance
Chaque nœud et bord porte le set de sources qui en témoignent. Un ensemble plutôt qu'un scalaire, car deux pivots se chevauchant sur un nœud est le cas normal. Avec un scalaire, le deuxième arrivé efface la revendication du premier, et la suppression de l'un ou l'autre entraîne le nœud avec lui.
node.getSources() // ['seed', 'correlations'], where 'seed' is the loaded data
node.hasSource('correlations')
edge.getSources() // edges carry it too, which is the load-bearing half
graph.removeBySource('correlations') // drop the tag; delete only when the set emptiesFaire une course en arrière
Une ingestion est une entrée dans graph.history, aux côtés des autres éléments qui modifient ce que contient le canevas : suppressions, masquages durables et éléments dessinés à la main.
graph.history.entries() // newest first; a pivot entry's `id` is its runId
graph.history.undo() // the newest entry
graph.history.undo(runId) // that ingest, and contiguously anything done since
graph.history.redo() // re-lands the recorded delta; no refetch, no re-gatingL'annulation est contigu : viser une entrée plus ancienne annule également toutes les entrées au-dessus, comme un seul lot. L'inversion d'une ancienne exécution à elle seule est graph.removeBySource(pivotId), qui supprime la garantie de cette source et supprime uniquement ce dont rien d'autre ne garantit - une opération en avant, enregistrée comme telle.
Une ingestion partielle d'un ensemble intermédiaire obtient son propre runId, chaque lot est donc une entrée distincte. Et l'annulation d'une acquisition qui est toujours l'entrée la plus récente remet ses candidats dans le volet Révision plutôt que de les demander à nouveau au fournisseur. Voir Annuler et historique pour l'intégralité.
Sauvegarde des résultats
Ingest met les candidats sur la toile ; il ne les écrit nulle part. Un pivot qui déclare save peut renvoyer ses propres résultats dans le système d'où ils proviennent, et la bibliothèque conserve le grand livre : quels éléments une exécution a créés, s'ils ont été écrits, lesquels ont échoué et ce qu'une nouvelle tentative doit contenir.
Omettez save et les résultats du pivot sont ne peuvent pas être sauvegardés. Ils n'entrent jamais dans le grand livre, ne sont jamais comptés non enregistrés et aucune sauvegarde n'apparaît pour eux. C'est la bonne déclaration pour un pivot sur les données dérivées - la sortie d'un moteur de corrélation ne vous appartient pas de réécrire - et c'est ce qui arrête un "210 non sauvegardé" permanent sans aucun remède.
L'ensemble, y compris les écritures partielles, les tentatives, les identifiants qu'un système source crée et ce que l'annulation atteindra et n'atteindra pas : Sauvegarde des résultats du pivot.
Le mode rail Pivot
En utilisant un parcourt la surface. Ce qui compte lorsque vous le configurez plutôt que de le piloter : entrer dans le mode est l'intention qui appelle un fournisseur - tout comme maintenir Pivot ▸ ouvert dans le menu contextuel - le laisser arrête chaque question en cours, et la sélection à elle seule ne coûte rien. La sélection est également le geste de glisser, de styliser et de modifier en masse, de sorte que la bibliothèque n'appelle jamais un fournisseur de manière spéculative.
Le mode est gated sur le registre :
UI: { pivotMode: 'auto' } // default: the button exists only while a pivot is registered
UI: { pivotMode: true } // force it, for pivots that arrive asynchronously
UI: { pivotMode: false } // neverUn consommateur qui n’enregistre aucun pivot ne voit donc aucune trace de la fonctionnalité. Il n'est jamais monté en mode viewer ou static.
Pivots sans origine
Certains enrichissements n'ont aucun nœud sur lequel s'exécuter, comme la recherche, l'importation ou le collage d'une liste d'indicateurs. Déclarez origin: 'none' et le pivot reçoit [], s'applique lorsque l'origine est vide et se replie en un groupe Sans origine une fois celui-ci choisi. Les résultats transitent par le même volet de tri, la même porte et le même modèle de provenance, et atterrissent au centre de la fenêtre d'affichage.
appliesTo n'a aucun sens pour l'un d'entre eux et n'est pas consulté.
Potentiel déclaré
Un indice sur le bord du nœud, disant "il y a 2 100 corrélations ici", mis là par celui qui a chargé les données.
node.setPotential('correlations', 2100) // 0 clears it
node.getPotential('correlations')
node.setPotential(2199) // the total across every pivot
node.getPotential()Il est déclaré, jamais interrogé. La bibliothèque n'appellera pas un fournisseur pour dessiner un badge, car un décompte qui se matérialiserait sur un nœud au moment où il serait interrogé laisserait le canevas raconter deux histoires différentes. Cliquer sur le badge ouvre le mode Pivot limité à ce pivot.
Comme tous les autres setter sur Node, il marque le nœud comme sale : appelez graph.renderer.update() si rien d'autre n'est sur le point d'être rendu.
Ce que dessine la jante
Par défaut, les badges de jante s'affichent uniquement lorsque le mode Pivot est ouvert, où ils aident à choisir une origine ; en cliquant sur celui-ci, vous étendez le panneau à ce nœud et à ce pivot. pivotRimBadgeVisible change lorsqu'ils s'affichent :
new Pivotick(el, data, { pivots, pivotRimBadgeVisible: 'always' })
graph.pivots.rimBadgeVisible = 'pivot-mode' // or later; it dirties every node'pivot-mode' * (par défaut)* | Uniquement lorsque le mode rail Pivot est ouvert. |
'always' | Quel que soit le mode, un badge est donc aussi un moyen d'accéder au mode Pivot. |
'never' | Pas du tout. |
Une fois montrée, leur forme est le prochain choix.
Un nœud a quatre coins de bordure, et seulement deux une fois qu'il a des enfants – l'augmentation des capacités réserve le côté Est. Ainsi un badge par prestataire est lisible à une poignée et impossible à une centaine. pivotRimBadge sélectionne la forme :
new Pivotick(el, data, { pivots, pivotRimBadge: 'summary' })
graph.pivots.rimBadge = 'per-pivot' // or later; it dirties every node'per-pivot' * (par défaut)* | Un badge par pivot qui a déclaré un potentiel, chaque ouverture de ce pivot. Plusieurs d'entre eux s'effondrent en un +n au début d'un nœud avec des enfants. |
'summary' | Un badge, quel que soit le décompte du prestataire. Il montre le total déclaré sans identifiant de pivot ou, sans rien déclaré, combien de pivots s'appliquent. |
'off' | Rien. NodeStyle.badges dessine toujours. |
Que fait un badge summary lorsqu'on clique dessus et d'où vient son décompte
Dans summary, le badge est un geste : il ouvre le panneau Pivot sur ce nœud, car chez tout fournisseur réel, le panneau est l'endroit où se trouvent les décomptes et les choix. L'exception est un nœud où exactement un pivot applique et, il ne déclare aucun summarize — rien à lire en premier et rien à affiner — là où le clic l'exécute. Là où le résultat arrive, c'est l'appel du fournisseur : autoIngest décide entre le canevas et le volet de tri, comme partout ailleurs.
Le nombre d’applications de pivots est dérivé et non déclaré, il ne coûte donc aucun appel au fournisseur. Il est conservé entre les modifications du graphique et actualisé lorsque le registre ou les nœuds du graphique se déplacent ; un appliesTo lisant autre chose peut être en retard sur la jante, et le panneau est toujours la réponse exacte.
Mise en cache
La bibliothèque met en cache une chose : summarize résultats, saisis par (pivot, origine, rétrécissement), car un résumé multi-sélection est un agrégat que rien ne peut décomposer. Tout le reste vous appartient :
graph.pivots.invalidate() // all of it
graph.pivots.invalidate('correlations') // one pivot's
graph.pivots.invalidate('correlations', nodes) // only what was asked about theseL'authentification, la nouvelle tentative, la limitation de débit et tout cache personnel appartiennent aux fonctions de votre fournisseur, qui sont des fonctions simples que vous avez écrites.
Ce que votre source a à offrir
Les pages ci-dessus sont destinées à celui qui écrit le fournisseur. Il s’agit du même contrat lu de l’autre côté, pour celui qui possède le service qu’il appelle.
- Un appel fetch, renvoyant les objets et leurs relations. Le seul obligatoire. Deux choses sont importantes à ce sujet : il faut plusieurs identifiants dans une seule requête, donc un pivot sur cinquante nœuds coûte un appel, et les identifiants qu'il renvoie sont stables d'une exécution à l'autre, car c'est sur cela que fonctionnent les clés de déduplication. Un identifiant par réponse renvoie à nouveau les résultats d'hier en tant que nouveaux candidats.
- Un appel de comptage, prenant les mêmes identifiants et les mêmes filtres, renvoyant un total et le nombre par valeur pour tout ce qui peut être réduit. C'est ce qui fait fonctionner le portail et le rétrécissement, et cela est redemandé à chaque modification, donc cela doit être bon marché. Le chiffre approximatif est très bien : les décomptes sont consultatifs. Sans cela, un pivot fonctionne toujours, avec un simple Run et rien pour le bloquer.
- Les mêmes filtres honorés par les deux. Un décompte qui les ignore ne bouge jamais, donc un pivot au-dessus de son plafond ne peut jamais être débloqué.
- Une manière d'annuler. Les questions sont abandonnées lorsque l'analyste passe à autre chose, et le
signalqui est remis au prestataire ne vaut la peine d'être transmis que si le transport agit en conséquence. - Un appel d'écriture, si les résultats vous appartiennent. Il doit renvoyer les identifiants qu'il a attribués, afin que
canonicalIdspuisse empêcher l'exécution de demain de dupliquer ce qui a été enregistré et être en mesure de signaler un succès partiel.
Les deux appels de lecture doivent donner une réponse vide en dehors d'un échec : aucun résultat est un fait sur les données, une erreur est un fait sur le service, et ils se lisent complètement différemment pour un analyste.
Rien ici n'a besoin d'une pagination, d'un curseur ou d'un transport en streaming. Le rétrécissement est ce qui les remplace.
Ce que ce n'est pas
- Pas de streaming et pas de curseur de serveur. Le rétrécissement les remplace : plutôt que de récupérer deux mille lignes, demandez ce qu'il y a et laissez l'analyste choisir. Le contrat ne nécessite jamais de backend de streaming, car de nombreuses sources n'en ont pas.
- Pas de re-parentage. Un nœud renvoyé ne nomme jamais un parent existant. L'union des enfants par identifiant est le seul moyen par lequel l'ingestion touche les enfants d'un nœud existant.
- Aucune annulation de modification de propriété.
graph.historycouvre ce que le canevas contient et montre – ce qui est entré, ce qui est sorti, ce qui est caché. Les données d'un nœud sont un état backend que la bibliothèque n'a pas créé, donc restaurer un champ localement est le travail deonBeforeNodeEditCommit, pas le nôtre. - Aucune politique de transport.
saveréécrit, mais les informations d'identification, les tentatives, la limitation de débit et la détection de conflits appartiennent toutes au consommateur. La seule nouvelle tentative de la bibliothèque est que l'analyste appuie sur Retry. - Aucune mémoire de rejet lors d'un rechargement. Un rejet est un verdict dans le contexte d'une session, et le même candidat peut valoir une réponse différente dans un autre graphique. Les refus sont retenus pour la session et pas au-delà.
- Aucune actualisation de ce qui est déjà ici. Un candidat dont l'identifiant correspond laisse le nœud existant intact ; la réconciliation d'un nœud avec une copie plus récente nécessite une politique de conflit que la bibliothèque n'a pas.