Aller au contenu
Pivotick
Main Navigation Accueil
Bien démarrer
Configuration
Référence de l’API
GalerieProjetsEssayer !

Français

English
简体中文
Deutsch
Português

Français

English
简体中文
Deutsch
Português

Apparence

Sidebar Navigation

Bien démarrer

Projets utilisant Pivotick

Configuration

Fonctions de rappel

Couches d’arêtes

Simplifier

Disposition

Rendu

Simulation

Personnaliser l’interface

Barre latérale

Infobulle

Menu contextuel

Filtres

Légende

Tableau de données

Style de l’interface

Traduire l’interface

Extensions

API Pivotick

Pivots et enrichissement

Enregistrer les résultats des pivots

Annulation et historique

Sécurité

Documentation de l’API

Galerie

Bases

Style des nœuds

Style des arêtes

Dispositions

Événements et rappels

Personnalisation de l’interface

Édition et création

Notes et annotations

Filtres, recherche et hiérarchie

Contrôle par programmation

Thèmes et performances

Pivots et enrichissement

Démonstrations complètes

Détail qui suit le zoom ​

Un nœud, quatre dessins. tiers choisit entre eux en fonction de la taille actuelle du nœud, donc un aperçu est constitué de points et un gros plan est constitué de cartes, et aucun des deux ne doit être le compromis avec lequel l'autre peut vivre.

DessinDéclaré commeSe présente
un point nule style de basequand même 32 pixels, c'est trop
un point colorétiers[0], 32×32une fois que le nœud affiche une largeur de 32 pixels
une puce étiquetéetiers[1], 110×46une fois qu'il rend 110, c'est-à-dire un demi-zoom
une fiche récapitulativetiers[2], 220×110une fois qu'il a rendu 220, ce qui est le zoom 1 ici
une fiche détailléefocusTier, 280×150en survol ou sur une sélection isolée, à n'importe quel zoom
à nouveau la fiche récapitulativeun indicateur focusTieren survol, jusqu'à ce que tiers[2] l'affiche déjà

tiers est un tableau et rien ne limite sa longueur. Le plus large définit l'empreinte, c'est donc celui qui s'enclenche au zoom 1 et est dessiné exactement à sa taille de conception.

Choses qui valent la peine d'être provoquées :

  • Faites défiler vers l'extérieur, puis revenez. L'ensemble du canevas change de dessin en même temps, car chaque nœud partage une empreinte et donc un seuil. Rien ne bouge pendant que cela se produit : la case réservée par chaque nœud dans le layout est celle du niveau le plus large, et le dessin change à l'intérieur.
  • Faites défiler lentement un seuil. Les dessins s'effectuent en fondu plutôt que coupés, donc rien ne clignote : le dessin sortant est maintenu à l'écran tandis que le nouveau apparaît en dessous. render.detailTransition définit le temps que cela prend et 0 remet la coupe dure en arrière.
  • Garez le zoom juste sur un seuil et poussez-le. Un niveau qui s'est engagé est maintenu jusqu'à ce que la taille rendue tombe à 85 % de ce qui l'a engagé, de sorte que l'image se stabilise au lieu de scintiller.
  • Observez la puce en entrant. Un niveau intermédiaire s'engage à width / footprint du zoom 1, il apparaît donc plus petit que la boîte pour laquelle il a été conçu et s'y développe. C'est ça le métier de rétrécir un dessin : un dessin plus petit demande moins de place et apparaît donc plus tôt, mais il apparaît aussi plus petit. Son type est dimensionné en unités graphiques pour les zooms auxquels il réside réellement ; donnez-lui un minRenderedSize explicite si vous préférez qu'il attende plus de place sans le redessiner.
  • Survolez un point lors d'un zoom arrière. La carte de détail est à contre-échelle par rapport au zoom, elle est donc la même 280 × 150 pixels, quelle que soit la taille du nœud. Lire un nœud ne signifie jamais zoomer dessus.
  • Survolez l'adresse IP, le fichier ou l'e-mail, faites un zoom arrière puis au zoom 1. Leur dessin principal est la carte récapitulative, qui focusTierYieldsAt: 2 se retire une fois que le niveau récapitulatif l'a dessiné de toute façon, donc les survoler de près n'ajoute rien. La campagne ouvre toujours sa fiche détail.
  • **Sélectionnez un nœud, puis déplacez-en-sélectionnez un second. ** Une sélection est promue ; deux ne le font pas. Vingt cartes à la fois constitueraient un mur.

L'empreinte est déclarée, non mesurée

width et height sont des nombres que vous donnez, car la mise en page doit savoir combien d'espace laisser avant le premier tick - et une carte mesurée ne rapporte sa taille qu'après avoir été tirée.

js
/** Shared shell for the three card drawings, so they differ only in what they carry. */
function shell(node, width, height) {
    const d = node.getData()
    const box = document.createElement('div')
    box.setAttribute('style', [
        'box-sizing: border-box',
        `width: ${width}px`,
        `height: ${height}px`,
        'padding: 6px 10px',
        'border-radius: 8px',
        `border: 2px solid ${SEV[d.sev].tone}`,
        `background: ${SEV[d.sev].wash}`,
        'font-family: system-ui, sans-serif',
        'overflow: hidden',
    ].join(';'))
    return box
}

function row(label, value, tone, size = 11) {
    const line = document.createElement('div')
    line.setAttribute('style', `display: flex; justify-content: space-between; gap: 10px; font-size: ${size}px; line-height: 1.6`)
    const left = document.createElement('span')
    left.setAttribute('style', 'color: #64748b')
    left.textContent = label
    const right = document.createElement('span')
    right.setAttribute('style', `color: ${tone}; font-weight: 600`)
    right.textContent = value
    line.append(left, right)
    return line
}

// Every line states its own line-height. Left to `normal` the browser picks one from the
// font, and a box sized to fit on one machine clips its last line on another.
const LINE = 1.2

function title(text, size, weight = 600) {
    const el = document.createElement('div')
    el.setAttribute('style', `font: ${weight} ${size}px/${LINE} system-ui, sans-serif; color: #0f172a;`
        + 'white-space: nowrap; overflow: hidden; text-overflow: ellipsis')
    el.textContent = text
    return el
}

function kindLine(node, size, gap = 0) {
    const el = document.createElement('div')
    el.setAttribute('style', `font-size: ${size}px; line-height: ${LINE};`
        + `color: ${SEV[node.getData().sev].tone}; margin-bottom: ${gap}px`)
    el.textContent = node.getData().kind
    return el
}

/**
 * The small card: a name and what kind of thing it is.
 *
 * 110 wide against the 220 footprint, so it takes over at half zoom — a narrower drawing
 * asks for less room, and its threshold is its width.
 *
 * Its type is set larger than the bigger cards' below, which looks backwards until you
 * notice these sizes are in graph units. This tier is only ever drawn between k = 0.5 and
 * k = 1, so 18 units lands at 9-18 CSS pixels; the card below is never drawn under k = 1,
 * where 14 units is already 14 pixels. Each drawing is sized for the zoom it lives at.
 */
function chip(node) {
    const d = node.getData()
    const box = shell(node, 110, 46)
    box.style.padding = '5px 8px'
    box.append(title(d.name, 18), kindLine(node, 11))
    return box
}

/** The medium card: room for the two facts you sort on, without opening anything. */
function summaryCard(node) {
    const d = node.getData()
    const box = shell(node, 220, 110)
    box.style.padding = '8px 12px'

    box.append(title(d.name, 14, 700), kindLine(node, 11, 4),
        row('Severity', d.sev, SEV[d.sev].tone, 12),
        row('Sightings', String(d.count), '#0f172a', 12))
    return box
}

/** The focus drawing: everything the node knows, at a size that is readable at any zoom. */
function detailCard(node) {
    const d = node.getData()
    const box = shell(node, 280, 150)
    box.style.padding = '10px 14px'

    box.append(title(d.name, 14, 700), kindLine(node, 11, 6),
        row('Severity', d.sev, SEV[d.sev].tone),
        row('First seen', d.first, '#0f172a'),
        row('Sightings', String(d.count), '#0f172a'),
        row('Source', 'internal feed', '#0f172a'))
    return box
}

const INDICATORS = new Set(['IP', 'File', 'Email'])

const options = {
    render: {
        defaultNodeStyle: {
            // The floor: what a node draws when no tier qualifies at all.
            shape: 'circle',
            size: 7,
            color: (node) => SEV[node.getData().sev].tone,

            // Ordered smallest first; the richest one that fits wins. There is no limit on
            // how many there are — three here, and the base style above is the floor when
            // none of them qualifies.
            //
            // Each tier's `width` is both its design box and, by default, the rendered size
            // at which it takes over. The widest one sets the footprint, so it engages at
            // exactly k = 1 and is drawn at its design size; the two narrower ones engage
            // proportionally earlier, which is why their type is sized for the zoom they
            // live at rather than for their box.
            tiers: [
                {
                    width: 32, height: 32,
                    style: { shape: 'circle', size: 16, color: (node) => SEV[node.getData().sev].tone },
                },
                {
                    width: 110, height: 46,
                    style: { shape: 'none', html: chip },
                },
                {
                    width: 220, height: 110,
                    style: { shape: 'none', html: summaryCard },
                },
            ],

            // Whatever the zoom, a hovered or singly-selected node draws this instead. It
            // merges over the style above rather than over the active tier, and holds a
            // constant size on screen.
            focusTier: { shape: 'none', html: detailCard },
        },
        // A plain indicator has nothing beyond its summary to show, so its focus drawing is
        // the summary card, and it steps aside once `tiers[2]` draws that card anyway.
        nodeTypeAccessor: (node) => INDICATORS.has(node.getData().kind) ? 'indicator' : undefined,
        nodeStyleMap: {
            indicator: {
                focusTier: { shape: 'none', html: summaryCard },
                focusTierYieldsAt: 2,
            },
        },
    },
    simulation: {
        // Tight enough that the graph opens on chips: the collision force already holds the
        // footprints apart, so the links only have to stop it spreading further.
        d3LinkDistance: 60,
    },
    UI: {
        // The focus tier already draws everything a hover would say, so the tooltip beside
        // it would only repeat itself. Both fire on hover; here only one should.
        tooltip: { enabled: false },
    },
}
js
const SEV = {
    high: { tone: '#dc2626', wash: '#fef2f2' },
    medium: { tone: '#d97706', wash: '#fffbeb' },
    low: { tone: '#0891b2', wash: '#ecfeff' },
}

const data = {
    nodes: [
        { id: 'camp', data: { kind: 'Campaign', name: 'SILENT-QUAY', sev: 'high', first: '2026-02-11', count: 41 } },
        { id: 'actor', data: { kind: 'Threat actor', name: 'UNC-4417', sev: 'high', first: '2025-11-02', count: 63 } },
        { id: 'mal1', data: { kind: 'Malware', name: 'Dropbear', sev: 'high', first: '2026-03-04', count: 22 } },
        { id: 'c2a', data: { kind: 'Host', name: 'cdn-a.example', sev: 'high', first: '2026-03-06', count: 18 } },
        { id: 'ip1', data: { kind: 'IP', name: '203.0.113.44', sev: 'medium', first: '2026-03-09', count: 6 } },
        { id: 'hash', data: { kind: 'File', name: '9f2c…a10e', sev: 'high', first: '2026-03-04', count: 14 } },
        { id: 'mail', data: { kind: 'Email', name: 'ops@parcel.example', sev: 'low', first: '2026-02-27', count: 2 } },
        { id: 'vict', data: { kind: 'Sector', name: 'Maritime logistics', sev: 'low', first: '2026-02-14', count: 5 } },
    ],
    edges: [
        { from: 'actor', to: 'camp' }, { from: 'camp', to: 'mal1' }, { from: 'camp', to: 'mail' },
        { from: 'camp', to: 'vict' }, { from: 'mal1', to: 'c2a' }, { from: 'mal1', to: 'hash' },
        { from: 'c2a', to: 'ip1' }, { from: 'actor', to: 'mal1' },
    ],
}

Voir Rendu → Détail qui suit le zoom pour le contrat complet : ce qui sélectionne un niveau, comment déplacer un seuil et quand le dessin de focus se déclenche.

Pager
Page suivanteBien démarrer