コンテンツへスキップ
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 のカスタマイズ

編集と作成

メモと注釈

フィルター・検索・階層

プログラムによる制御

テーマとパフォーマンス

ピボットとデータ拡充

注目のサンプル

カスタム HTML ノード ​

shape、color、icon、text の組み合わせがうまくいかない場合は、レンダラーにいくつかの値を渡します マークアップ。 htmlは通常のスタイルチャンネルなので、カードは同じ方法で解決されます。 他の優先チェーンと同様に — つまり、カードは * の 1 種類に与えるものです。 ノード*、グラフ全体ではありません。

以下のすべてのノードは 1 つの nodeStyleMap から生じます。

ノードとして宣言される得られるもの
3枚のチームカードshape: 'none' + htmlカードは ノードです。その背後に形状はなく、その測定されたボックスがレイアウトとエッジを決定します。
オンコールshape: 'circle' + html円盤はまだ描かれており、sizeはまだ床です — 形の上に乗った錠剤
staging · prodいいえ html同じマップからの単純な六角形
Q3 フリーズshape: 'none'、いいえ html裸のラベル: テキスト以外は何も描かれていませんが、ドラッグと選択は可能です
オペレーションノード自身の style.html1 つのノードがそのタイプのカードをオーバーライドする
エンジニアリングrender.renderNodeこのノードを のみ にカードし、残りには何も返さないグローバル コールバック

挑発に値する 4 つの事柄:

  • カードをクリックします。 他のノードと同様にハイライト表示されます。カード専用ノードにより、非表示状態が維持されます。 選択してホバーした外観が着地するものになるように、ボックスを正確に下に配置します。 これがなければ、カスタム ノードは選択された状態をまったく持ちません。
  • カードをドラッグします。 衝突力は、実 カードボックスの間隔をあけます。 エッジは、カードの周囲に描かれた円ではなく、各カードの境界線で止まります。何もない それに合わせてレイアウトを調整する必要がありました。
  • Operations と On call を比較してください。 両方とも html です。後ろに形があるのは1つだけですが、 これが shape: 'none' の機能のすべてです。
  • 「Operations」マークアップを見てください。 そのルートは width: 100% — 以前の形状です。 プレースホルダーボックスの上に折りたたんでクリップします。カードはシュリンクトゥフィットの内側で測定されます ボックスなので、それ自体のコンテンツに対して解決されるため、何もする必要はありません。

信頼できるマークアップのみ

返されたものはすべてそのまま挿入されるため、これらのカードはむしろ textContent で構築されています ノード データをマークアップ文字列に補間するよりも、 セキュリティを参照してください。

js
/** A team card. Built with `textContent`, never by interpolating data into markup. */
function teamCard(node) {
    const d = node.getData()
    const card = document.createElement('div')
    card.setAttribute('style', [
        'display: flex',
        'align-items: center',
        'gap: 8px',
        'padding: 8px 12px',
        'border-radius: 12px',
        'background: #ffffff',
        `border: 2px solid ${d.tone}`,
        'box-shadow: 0 2px 6px rgba(15, 23, 42, 0.18)',
        'font-family: system-ui, sans-serif',
        'white-space: nowrap',
    ].join(';'))

    const emoji = document.createElement('span')
    emoji.style.fontSize = '20px'
    emoji.textContent = d.emoji

    const stack = document.createElement('span')
    stack.setAttribute('style', 'display: flex; flex-direction: column; line-height: 1.2')
    const title = document.createElement('strong')
    title.setAttribute('style', 'font-size: 13px; color: #0f172a')
    title.textContent = d.name
    const lead = document.createElement('span')
    lead.setAttribute('style', `font-size: 11px; color: ${d.tone}`)
    lead.textContent = d.lead
    stack.append(title, lead)

    card.append(emoji, stack)
    return card
}

/** A small pill, drawn on top of the shape it is given rather than replacing it. */
function rotaPill(node) {
    const pill = document.createElement('div')
    pill.setAttribute('style', [
        'display: inline-flex',
        'padding: 2px 8px',
        'border-radius: 999px',
        'background: #0f172a',
        'color: #fff',
        'font: 600 10px system-ui, sans-serif',
        'white-space: nowrap',
    ].join(';'))
    pill.textContent = node.getData().lead
    return pill
}

const options = {
    render: {
        nodeTypeAccessor: (node) => node.getData()?.kind,
        nodeStyleMap: {
            // A card and nothing else. `shape: 'none'` is what makes it the whole node:
            // no disc behind it, and the node's radius and edge anchors come from the
            // measured card instead of from `size`.
            team: {
                shape: 'none',
                html: teamCard,
                // The card carries its own title, so switch the SVG label off. `undefined`
                // would fall through to whatever the chain said; `''` is how you clear it.
                text: '',
            },
            // The other way round: the shape is still drawn, and `size` is still the
            // smallest the node may be, so the pill rides on a coloured disc.
            rota: {
                shape: 'circle',
                size: 30,
                color: '#f59e0b',
                html: rotaPill,
                text: (node) => node.getData()?.name,
                textVerticalShift: -1.5,
            },
            // No card at all — ordinary shapes, from the same style map.
            env: {
                shape: 'hexagon',
                size: 22,
                color: '#16a34a',
                text: (node) => node.getData()?.name,
                textVerticalShift: -1.4,
            },
            // Shapeless with no card either: a label floating on the canvas. Still
            // draggable and selectable, over `2 × size`.
            milestone: {
                shape: 'none',
                size: 26,
                text: (node) => node.getData()?.name,
                // Nothing behind it to shorten against, so draw it whole.
                textTruncate: false,
            },
        },
        // A global callback can card *some* nodes now: returning nothing hands the node
        // back to the chain above, so everything else is styled as usual. Here it draws
        // the badge on whichever team owns the release.
        renderNode: (node) => {
            if (node.id !== 'eng') return undefined
            const card = teamCard(node)
            card.style.borderWidth = '3px'
            const flag = document.createElement('span')
            flag.setAttribute('style', [
                'margin-left: 8px',
                'padding: 2px 6px',
                'border-radius: 6px',
                'background: #fee2e2',
                'color: #b91c1c',
                'font: 700 10px system-ui, sans-serif',
            ].join(';'))
            flag.textContent = 'RELEASE OWNER'
            card.append(flag)
            return card
        },
        defaultEdgeStyle: { markerEnd: 'arrow', strokeWidth: 1.5 },
    },
    simulation: {
        d3LinkDistance: 110,
        d3ManyBodyStrength: -700,
        d3CollideRadiusMultiplier: 1.15,
    },
    UI: {
        mode: 'full',
        minimap: false,
        table: false,
        // This card is about the nodes, so nothing else competes for the canvas.
        legend: false,
    },
}
js
const data = {
    nodes: [
        // Cards, from a `nodeStyleMap` entry.
        { id: 'design', data: { kind: 'team', name: 'Design', lead: 'Lena', emoji: '🎨', tone: '#8b5cf6' } },
        { id: 'eng', data: { kind: 'team', name: 'Engineering', lead: 'Raj', emoji: '⚙️', tone: '#2563eb' } },
        { id: 'data', data: { kind: 'team', name: 'Data', lead: 'Omar', emoji: '📊', tone: '#0891b2' } },
        // A card on a shape: the disc is still drawn behind it.
        { id: 'oncall', data: { kind: 'rota', name: 'On call', lead: 'Maya' } },
        // Plain shapes, from the same style map.
        { id: 'staging', data: { kind: 'env', name: 'staging' } },
        { id: 'prod', data: { kind: 'env', name: 'prod' } },
        // A label and no shape at all.
        { id: 'q3', data: { kind: 'milestone', name: 'Q3 freeze' } },
        // One node overriding its type's card with its own.
        {
            id: 'ops', data: { kind: 'team', name: 'Operations', lead: 'Theo', emoji: '🧭', tone: '#ea580c' },
            style: {
                shape: 'none',
                html: (node) => {
                    const d = node.getData()
                    const card = document.createElement('div')
                    // `width: 100%` and it still measures its content: the card is
                    // measured inside a shrink-to-fit box, not the placeholder.
                    card.setAttribute('style', [
                        'display: flex',
                        'align-items: center',
                        'gap: 8px',
                        'width: 100%',
                        'padding: 10px 14px',
                        'border-radius: 999px',
                        `background: ${d.tone}`,
                        'color: #fff',
                        'font: 600 13px system-ui, sans-serif',
                        'white-space: nowrap',
                    ].join(';'))
                    card.textContent = `${d.emoji} ${d.name} — always on`
                    return card
                },
            },
        },
    ],
    edges: [
        { from: 'design', to: 'eng' },
        { from: 'eng', to: 'data' },
        { from: 'ops', to: 'eng' },
        { from: 'oncall', to: 'ops' },
        { from: 'eng', to: 'staging' },
        { from: 'staging', to: 'prod' },
        { from: 'q3', to: 'prod' },
        { from: 'data', to: 'prod' },
        { from: 'design', to: 'ops' },
    ],
}

詳しい仕様は描画 → HTML ノードを参照してください。カードのサイズを測定するタイミング、カード表示でも無効にならない機能、一部のノードだけをカードとして表示する方法を説明しています。

Pager
次のページはじめに