Zum Inhalt springen
Pivotick
Main Navigation Startseite
Erste Schritte
Konfiguration
API-Referenz
GalerieProjekteAusprobieren!

Deutsch

English
简体中文
Français
Português

Deutsch

English
简体中文
Français
Português

Darstellung

Sidebar Navigation

Erste Schritte

Projekte mit Pivotick

Konfiguration

Callbacks

Kantenebenen

Vereinfachen

Layout

Darstellung

Simulation

Oberfläche anpassen

Seitenleiste

Tooltip

Kontextmenü

Filter

Legende

Datentabelle

Oberflächengestaltung

Oberfläche übersetzen

Plugins

Pivotick-API

Pivots und Anreicherung

Pivot-Ergebnisse speichern

Rückgängig und Verlauf

Sicherheit

API-Dokumentation

Galerie

Grundlagen

Knotengestaltung

Kantengestaltung

Layouts

Ereignisse und Callbacks

Oberflächenanpassung

Bearbeiten und Erstellen

Notizen und Anmerkungen

Filter, Suche und Hierarchie

Programmatische Steuerung

Themes und Leistung

Pivots und Anreicherung

Anwendungsbeispiele

Detail, das dem Zoom folgt ​

Ein Knoten, vier Zeichnungen. tiers wählt zwischen ihnen aus, je nachdem, wie groß der Knoten derzeit gerendert wird, also besteht eine Übersicht aus Punkten und eine Nahaufnahme aus Karten, und keiner muss der Kompromiss sein, mit dem der andere leben kann.

ZeichnenDeklariert alsErscheint
ein bloßer Punktder Basisstilwenn sogar 32 Pixel zu viel sind
ein farbiger Punkttiers[0], 32×32Sobald der Knoten 32 Pixel breit ist
ein beschrifteter Chiptiers[1], 110×46Sobald es 110 rendert, also halber Zoom
eine Übersichtskartetiers[2], 220×110Sobald es 220 rendert, ist das hier Zoom 1
eine DetailkartefocusTier, 280×150beim Hover oder einer einzelnen Auswahl, bei jedem Zoom
noch einmal die ÜbersichtskartefocusTier eines Indikatorsbeim Schweben, bis tiers[2] es bereits anzeigt

tiers ist ein Array und nichts begrenzt seine Länge. Die breiteste Option legt den Footprint fest, sodass sie bei Zoom 1 aktiviert und genau in der Designgröße gezeichnet wird.

Dinge, die es wert sind, provoziert zu werden:

  • Scrollen Sie raus und dann wieder hinein. Die gesamte Arbeitsfläche ändert ihre Zeichnung auf einmal, da jeder Knoten einen gemeinsamen Footprint und damit einen Schwellenwert hat. Dabei bewegt sich nichts: Die Box, die jeder Knoten im Layout reserviert, ist die breiteste Ebene, und die Zeichnung ändert sich darin.
  • Scrollen Sie langsam über eine Schwelle. Die Zeichnungen werden überblendet, nicht geschnitten, sodass nichts blinkt: Die ausgehende Zeichnung bleibt auf dem Bildschirm, während die neue darunter erscheint. render.detailTransition legt fest, wie lange das dauert, und 0 setzt den harten Schnitt zurück.
  • Parken Sie den Zoom direkt auf einer Schwelle und verschieben Sie ihn. Eine aktivierte Ebene bleibt bestehen, bis die gerenderte Größe auf 85 % der aktivierten Größe fällt, sodass sich das Bild stabilisiert, anstatt zu flackern.
  • Beobachten Sie den Chip auf dem Weg hinein. Eine Zwischenebene rastet bei width / footprint von Zoom 1 ein, sodass sie kleiner erscheint als die Box, für die sie entworfen wurde, und hineinwächst. Das ist der Nachteil beim Einschränken: Eine kleinere Zeichnung benötigt weniger Platz und wird daher früher angezeigt, wirkt aber auch kleiner. Die Größe seines Typs wird in Grafikeinheiten für die Zooms angegeben, mit denen er tatsächlich arbeitet. Geben Sie ihm ein explizites minRenderedSize, wenn Sie möchten, dass es lieber auf mehr Platz wartet, ohne es neu zu zeichnen.
  • Bewegen Sie den Mauszeiger über einen Punkt, während Sie herausgezoomt sind. Die Detailkarte wird im Verhältnis zum Zoom skaliert, sodass sie die gleichen 280×150 Pixel hat, egal wie klein der Knoten ist. Das Lesen eines Knotens bedeutet niemals, darauf zu zoomen.
  • Bewegen Sie den Mauszeiger über die IP, die Datei oder die E-Mail, verkleinern Sie sie und dann auf Zoom 1. Ihre Fokuszeichnung ist die Zusammenfassungskarte, die focusTierYieldsAt: 2 in den Ruhestand geht, sobald die Zusammenfassungsebene sie ohnehin zeichnet, sodass das Bewegen des Mauszeigers aus der Nähe nichts hinzufügt. Die Kampagne öffnet weiterhin ihre Detailkarte.
  • Wählen Sie einen Knoten aus und wählen Sie dann bei gedrückter Umschalttaste einen zweiten aus. Eine Auswahl befördert; zwei nicht. Zwanzig Karten auf einmal wären eine Wand.

Der Fußabdruck wird deklariert, nicht gemessen

width und height sind Zahlen, die Sie angeben, da das Layout wissen muss, wie viel Platz vor dem ersten Tick gelassen werden muss – und eine gemessene Karte ihre Größe erst anzeigt, nachdem sie gezogen wurde.

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' },
    ],
}

Den vollständigen Vertrag finden Sie unter Rendering → Detail, das dem Zoom folgt: Was wählt eine Ebene aus, wie verschiebt man einen Schwellenwert und wann wird die Fokuszeichnung ausgelöst?

Pager
Nächste SeiteErste Schritte