コンテンツへスキップ
Pivotick
Main Navigation ホーム
はじめに
設定
生成された API ドキュメント
ギャラリープロジェクト試してみる!

日本語

English
简体中文
Français
Deutsch
Português
Nederlands
العربية
Español

日本語

English
简体中文
Français
Deutsch
Português
Nederlands
العربية
Español

外観

Sidebar Navigation

はじめに

Pivotick を使ったプロジェクト

設定

コールバック

エッジレイヤー

簡略化

レイアウト

描画

シミュレーション

UI のカスタマイズ

サイドバー

ツールチップ

コンテキストメニュー

フィルター

凡例

データ表

UI のスタイル

UI の翻訳

プラグイン

Pivotick API

ピボットとデータ拡充

ピボット結果の保存

元に戻す操作と履歴

セキュリティ

API ドキュメント

ギャラリー

基本

ノードのスタイル

エッジのスタイル

レイアウト

イベントとコールバック

UI のカスタマイズ

編集と作成

メモと注釈

フィルター・検索・階層

プログラムによる制御

テーマとパフォーマンス

ピボットとデータ拡充

注目のサンプル

ズーム後の詳細 ​

1 つのノードに 4 つの図面。 tiers は、現在のノードの大きさからそれらの中から選択します。 レンダリングされるため、概要はドットで、クローズアップはカードであり、どちらもである必要はありません。 相手が耐えられる妥協をする。

図面として宣言される現れる
裸の点ベーススタイル32ピクセルでも多すぎる場合
色付きのドットtiers[0], 32×32ノードが 32 ピクセル幅でレンダリングされると
ラベル付きチップtiers[1], 110×46110、つまり半分のズームをレンダリングすると
概要カードtiers[2], 220×110220 をレンダリングすると、ここではズーム 1 になります。
詳細カードfocusTier, 280×150ホバー時または単独選択時、任意のズーム時
もう一度概要カードインジケーターの focusTierホバーすると、tiers[2] がすでに表示されるまで

tiers は配列であり、その長さに制限はありません。最も広いものが設置面積を設定するため、 ズーム 1 で動作し、正確に設計サイズで描画されるものです。

挑発する価値のあるもの:

  • スクロールアウトしてから戻ってきます。 キャンバス全体の描画が一度に変わります。 ノードはフットプリントを共有するため、しきい値も共有します。それが起こっている間は何も動きません。 レイアウト内で各ノードが予約するボックスが最も幅の広い層であり、内部の描画が変更されます。 それ。
  • しきい値をゆっくりとスクロールします。 描画はカットではなくクロスフェードするため、 何も点滅しません。送信メッセージは画面上に保持され、新しいメッセージはその下に表示されます。 それ。 render.detailTransition はそれにかかる時間を設定し、0 はハードカットを元に戻します。
  • ズームをしきい値のすぐ上に置き、少しずつ動かします。 ホールドを使用している層 レンダリングされるサイズが、それに関与したサイズの 85% に低下するまで、代わりに画像が安定します。 ちらつきの。
  • 途中でチップに注目してください。 中間層が width / footprint で関与します。 ズーム 1 なので、設計されたボックスよりも小さく表示され、ボックス内に成長します。つまり 1 つを狭めるというトレード: 図面が小さいほど必要なスペースが少なくなるため、より早く表示されます。 しかし、それも小さく見えます。そのタイプは、実際にズームするためのグラフ単位でサイズ設定されます。 に住んでいます。もっとスペースを待ちたい場合は、明示的に minRenderedSize を付けてください 描き直さずに。
  • ズームアウト中に点にカーソルを合わせます。 詳細カードはズームに反して拡大縮小されるため、 ノードがどんなに小さくても、同じ 280×150 ピクセルです。 1 つのノードを読み取ることは意味がありません それにズームします。
  • IP、ファイル、または電子メールにマウスを置き、ズームアウトしてからズーム 1 にします。 フォーカス 描画は概要カードであり、focusTierYieldsAt: 2 概要層が終了すると廃止されます。 とにかく描画するので、近くにホバーしても何も追加されません。キャンペーンはまだ始まっています 詳細カード。
  • ** ノードを 1 つ選択し、Shift キーを押しながら 2 番目のノードを選択します。** 1 つ選択するとプロモートされます。 2つはそうではありません。 20 カードはすぐに壁になるでしょう。

フットプリントは測定されるものではなく宣言される

width と height は、レイアウトにどのくらいのスペースがあるかを知る必要があるため、ユーザーが指定する数字です。 最初のティックの前に離れる - 測定されたカードは、ティックが終了した後にのみそのサイズを報告します。 描かれた。

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

完全なコントラクトについては、 レンダリング → ズーム に続く詳細を参照してください。 層を選択するもの、しきい値を移動する方法、フォーカス描画がいつ開始されるか。

Pager
次のページはじめに