Shumoku Docs

Search documentation

Server · next

Loading search…

日本語

This documentation describes the Server currently in development.

apps/server/docs/topology-rendering.md

トポロジー描画

Serverにおけるトポロジー描画の仕組みです。

This document is currently available in Japanese only.

サーバー Web アプリのトポロジー描画スタック — @shumoku/renderer を 中核に、その上に複数のオーバーレイ(weathermap / node-status / highlight / tooltip / camera)が乗る構造 — を アーキテクチャ・ 構造・相関・フロー の4軸で説明する。

オーバーレイは大きく 2 種類 に分かれる:

  • 構造化 overlay: renderer の linkOverlay / nodeOverlay 等の snippet で、各 SVG エンティティの中に直接 render される
  • サイドカー overlay: renderer から svgElement だけを受け取って、 自前で DOM 操作(class 付与、event listener、tooltip 配置等)する

実装の中核:

  • libs/@shumoku/renderer/ — 描画中核(Svelte / Web Component / camera)
  • apps/server/web/src/lib/components/topology/ — サーバ専用 Svelte オーバーレイ群
  • apps/server/web/src/lib/weathermap/ — weathermap 用の色 / duration / ジオメトリ helper

1. 概要

トポロジー描画は数十〜数百のリンクを同時に表示しつつ、ページは パン/ズーム、ホバー、WebSocket メトリクス更新も並行して処理する。 3 つの設計原則でこれを軽くしている。

  1. renderer がドメイン構造を所有する。 subgraph / link / node / port の DOM 順は renderer が決め、外側からは触らない。z-order は この宣言順だけで決まる。
  2. オーバーレイは 2 つの形に揃える。
    • 各エンティティの中に何かを描き足したい → renderer が公開する linkOverlay / nodeOverlay / subgraphOverlay / portOverlay snippet を使う
    • エンティティ横断 / DOM event / グローバル装飾 → svgElement を 受け取るサイドカーとして書く
  3. 毎フレームのアニメは CSS が動かす。 ブラウザが stroke-dashoffset / pulse / transition の keyframe ループを 所有し、JS は値の更新だけを担う。

2. アーキテクチャ

2.1 消費モード

@shumoku/renderer は 2 つのモードで使える。サイドカー overlay は どちらでも動くが、構造化 overlay (snippet 方式) は Svelte 限定。

モード使用場所DOMsnippet
Svelte component<ShumokuRenderer>apps/editor, apps/server/weblight DOM
Web Component<shumoku-renderer> カスタム要素任意の HTML/他フレームワークShadow DOM(mode: 'open')✗(代替案は 7 節)

WC 版は libs/@shumoku/renderer/src/wc.svelte.tsvitedist/wc/wc.js にバンドルし、customElements.define('shumoku-renderer', ...) でタグ登録する。

2.2 パッケージ境界

libs/@shumoku/renderer/           ← 描画の中核(外部パッケージ)
├── components/
│   ├── ShumokuRenderer.svelte    ← 外向き Svelte component
│   └── svg/
│       ├── SvgCanvas.svelte
│       ├── SvgEdge.svelte        ← path.link + linkOverlay snippet host
│       ├── SvgNode.svelte        ← g.node + nodeOverlay snippet host
│       └── SvgPort.svelte        ← port box + portOverlay snippet host
├── lib/
│   ├── camera.ts                 ← attachCamera 関数(d3-zoom + wheel-gestures)
│   └── overlays.ts               ← snippet の型定義(LinkOverlayContext 等)
├── wc.svelte.ts                  ← Web Component ラッパー
└── index.ts                      ← 公開 API

apps/server/web/src/lib/
├── components/topology/          ← server-web 専用 Svelte
│   ├── TopologyViewer.svelte     ← renderer を mount + sidecar を呼ぶ
│   ├── WeathermapLinkOverlay.svelte   (構造化, linkOverlay snippet)
│   ├── NodeStatusOverlay.svelte  (サイドカー)
│   ├── HighlightOverlay.svelte   (サイドカー、imperative API もあり)
│   └── TooltipOverlay.svelte     (サイドカー、DOM event)
└── weathermap/
    └── index.ts                  ← weathermap helper(色 / duration / 幾何)

依存の向きは常に上向き(server-web は renderer に依存するが、 renderer は server-web を知らない)。renderer 単体で editor / docs / CLI でも動く。

2.3 オーバーレイの2分類

構造化 overlay(snippet)

renderer が描画順を所有したまま、各ドメイン要素の に Svelte snippet で拡張点を公開する。snippet は domain object と context を 受け取り、その position に直接 render される。

snippet挿入位置主な用途実装例
subgraphOverlay(subgraph, ctx)g.subgraph 背景 rect 後、label 前group 背景装飾(未使用)
linkOverlay(edge, ctx)g.link-group の base link 後、hit path / label 前weathermap lane、link 装飾WeathermapLinkOverlay
nodeOverlay(node, ctx)g.node-bg 後、g.node-fgnode 内バッジ、状態装飾(未使用)
portOverlay(port, ctx)port box 後、label 前port 内状態、利用率表示(未使用)

context には domain object 参照DOM 参照 の両方が入る:

// libs/@shumoku/renderer/src/lib/overlays.ts
export interface LinkOverlayContext {
  selected: boolean
  groupElement: SVGGElement | null      // ← 親 g.link-group
  pathElement: SVGPathElement | null    // ← base path.link
  pathD: string
  width: number                         // ← getLinkWidth(link)
  fromPort: ResolvedPort | null
  toPort: ResolvedPort | null
  ...
}

これにより overlay は closest('g.link-group') のような class 名逆引き を一切しない。renderer が DOM 構造を refactor しても snippet 契約だけ 保てば壊れない。

サイドカー overlay

renderer 本体を直接 import せず、svgElement: SVGSVGElement だけ 受け取って、その中で DOM 操作する。複数エンティティを横断する処理 / DOM event ハンドリング / グローバル装飾はこちら。

flowchart LR
  HOST["ホスト<br/>Svelte ページ or 任意の HTML"]
  RND["@shumoku/renderer"]
  SVG["SVG element<br/>(svgElement)"]

  subgraph SIDECARS["Sidecar overlays"]
    direction TB
    NSO["NodeStatusOverlay"]
    HLO["HighlightOverlay"]
    TTO["TooltipOverlay"]
    CAM["attachCamera"]
  end

  HOST -->|graph prop| RND
  RND -->|svgElement 公開| HOST
  HOST -->|svgElement 手渡し| SIDECARS
  SIDECARS -->|class 付与<br/>event listener<br/>tooltip 配置| SVG

サイドカーの契約:

  1. 入力は svgElement のみ。renderer 内部に依存しない
  2. DOM 操作は渡された svg 配下に閉じる(document.querySelector は使わない)
  3. destroy() / detach() で自分が付けたものを全部クリーンアップ
  4. renderer の DOM 属性は読むだけ、書き換えない
  5. CSS 注入は svelte:head または scoped style(現状 light DOM 前提、詳細は 7 節)

サイドカーは現状 renderer の class 名 / data-attribute に依存する (e.g. g.node[data-id]g.link-group[data-link-id])。これらは renderer の “public DOM contract” として安定扱い。

2.4 描画型モデルと設計思想

トポロジーの概念構造はグラフであり、SVG DOM は木構造である。特に Port は「Node の所有物」である一方、「Link の端点」でもあるため、 DOM の親子だけで概念を表そうとすると破綻しやすい。

このため、描画型モデルは 所有(ownership)参照(reference) を分けている。

Node owns Port
Link references endpoint Ports
ResolvedEdge owns resolved endpoint references
Renderer owns SVG order
Overlay receives typed context

Core model

core の生モデルでは、Node / Subgraph / Link がユーザー入力に近い ドメインオブジェクト。Port は Node に属するインターフェースであり、 Linkfrom / to endpoint で node/port を参照する。

Node
  └─ Port definition

Link
  ├─ from: { node, port? }
  └─ to:   { node, port? }

ここで Link が Port の実体コピーを持たないのは、ラベル変更、ポート サイズ、位置、削除時の正本を Node 側に一本化するため。Port を Node と Link の両方が実体所有すると、同期問題が発生する。

Resolved layout model

layout 後は、描画に必要な座標と関係を Resolved* 型に落とす。

役割
Nodeposition 済みの node 本体
Subgraphbounds 済みの group
ResolvedPortNode 所有の port を絶対座標に解決したもの
ResolvedEdgeLink の routing 結果 + endpoint port 参照

ResolvedEdgefromPortId / toPortId だけでなく、 fromPort / toPort も持つ。

interface ResolvedEdge {
  id: string
  fromPortId: string | null
  toPortId: string | null
  fromPort: ResolvedPort | null
  toPort: ResolvedPort | null
  points: Position[]
  width: number
  link: Link
}

これは「Port の所有者は Node」という原則を保ったまま、Link 側の描画 や overlay が endpoint port を型付きで直接扱えるようにするため。 renderer 側で ports.get(edge.fromPortId) のように逆引きしない。

Renderer model

renderer は ResolvedLayout を SVG DOM に落とす責務を持つ。描画順は renderer が所有し、overlay はこの順序を変更しない。

g.viewport
  g.subgraph
  g.link-group
  g.node
  g.port

この順序は z-order そのものでもある。SVG は後に出た要素ほど上に描かれる ため、z-order を CSS や overlay 側 DOM 操作で調整しない。

Overlay context model

構造化 overlay は、DOM を query して必要情報を探すのではなく、renderer から typed context として受け取る。

linkOverlay(edge, ctx)

edge には routing 済みの path と endpoint port 参照があり、ctx には renderer が生成した DOM 参照が入る。

interface LinkOverlayContext {
  groupElement: SVGGElement | null
  pathElement: SVGPathElement | null
  pathD: string
  width: number
  fromPort: ResolvedPort | null
  toPort: ResolvedPort | null
}

この分担により、overlay は .link-grouppath.link という class 名を 知る必要がない。class 名に依存するのは、複数要素を横断するサイドカー overlay に限定する。

2.5 Renderer boundary

renderer は リッチなブラウザ機能を内蔵する場所ではない。renderer の 責務は、core の topology domain を安定した SVG 構造へ落とし、外側の ブラウザ層が安全に拡張できる場所と型を提供すること。

言い換えると、renderer は「何を意味するか」ではなく 「どこに、どの 構造で描けるか」 を所有する。

renderer = stable structure provider
host layer = semantic behavior provider
overlay = typed bridge between them

renderer が所有するもの

責務理由
NetworkGraph / ResolvedLayout を SVG DOM に落とすrenderer の本分
subgraph → link → node → port の z-orderSVG の描画順は DOM 順で決まるため
各 entity 内の基本 DOM 構造overlay が安全に乗る土台
hit target / selection の基本イベントeditor / server-web / docs で共通
linkOverlay 等の typed extension point外側の機能を renderer に混ぜないため
svgElement の公開camera / tooltip 等の sidecar を可能にするため
汎用的な mode / themetopology renderer として横断的に意味があるため

renderer が所有しないもの

責務所有者
live metrics / WebSocket / pollinghost app / store
traffic flow / weathermap の意味論server-web overlay
node status の解釈(up/down/degraded 等)server-web overlay
highlight のルール / spotlight / dashboard eventhost app / overlay
tooltip の内容生成host app / overlay
utilization color map / alert thresholddomain-specific layer
dashboard widget の状態 / configserver-web
API fetch / auth / tenant contextserver-web

これらを renderer に入れ始めると、renderer が @shumoku/core の汎用描画 ではなく server-web dashboard 専用 UI になってしまう。たとえば metrics / nodeStatus / showTrafficFlow / tooltipBuilder のような props は renderer に追加しない。

判断基準

新しい機能を renderer に入れるか overlay / host layer に置くか迷ったら、 以下で判断する。

問いyes なら
NetworkGraph / ResolvedLayout だけで説明できるか?renderer 候補
editor / docs / WC / server-web のどれでも同じ意味があるか?renderer 候補
SVG 構造、z-order、hit area、汎用 event に関するものか?renderer 候補
server-web の store / API / live metrics を知る必要があるか?host / overlay
状態の意味解釈(status, utilization, alert 等)を含むか?host / overlay
見た目の効果は汎用だが、発火条件が業務固有か?renderer は拡張点だけ提供

基本方針は、renderer には 拡張点と型 を足し、意味論は外側に置くこと。 今回の weathermap も renderer に traffic 機能を入れたのではなく、 linkOverlay(edge, ctx) という link 内拡張点を公開し、server-web 側が その上に traffic 表現を載せている。


3. 構造

3.1 Svelte コンポーネントツリー

flowchart TD
  PAGE["ページ<br/>graph / metrics ストア保持"]
  TV["TopologyViewer.svelte<br/>layout キャッシュ / attachCamera / ViewerContext 出力"]
  SR["ShumokuRenderer.svelte<br/>(from @shumoku/renderer)"]
  SC["SvgCanvas.svelte"]
  SE["SvgEdge.svelte<br/>+ linkOverlay snippet host"]
  SN["SvgNode.svelte<br/>+ nodeOverlay snippet host"]
  SP["SvgPort.svelte<br/>+ portOverlay snippet host"]
  WML["WeathermapLinkOverlay<br/>(構造化)"]
  NSO["NodeStatusOverlay<br/>(sidecar)"]
  HLO["HighlightOverlay<br/>(sidecar)"]
  TTO["TooltipOverlay<br/>(sidecar)"]

  PAGE -->|graph / metrics| TV
  TV -->|layout / theme<br/>bind:svgElement| SR
  SR --> SC
  SC --> SE
  SC --> SN
  SC --> SP
  TV -.linkOverlay snippet.-> WML
  TV -.svgElement.-> NSO
  TV -.svgElement.-> HLO
  TV -.svgElement.-> TTO

  classDef renderer fill:#eef6ff,stroke:#6aa0ff
  classDef structured fill:#fff5e6,stroke:#f0a040
  classDef sidecar fill:#e6ffe6,stroke:#50b050
  class SR,SC,SE,SN,SP renderer
  class WML structured
  class NSO,HLO,TTO sidecar

青 = renderer 内部 / 橙 = 構造化 overlay / 緑 = サイドカー overlay

3.2 描画後の SVG DOM ツリー

flowchart TD
  SVG["svg"]
  VP["g.viewport<br/>(d3-zoom が transform)"]
  CBG["g.canvas-bg"]
  SGS["g.subgraph"]
  LG["g.link-group<br/>active 時に .wm-active"]
  LK["path.link"]
  WMI["path.wm-overlay direction=in<br/>(WeathermapLinkOverlay)"]
  WMO["path.wm-overlay direction=out<br/>(WeathermapLinkOverlay)"]
  LH["path.link-hit"]
  N["g.node<br/>+ .status-up/down/...<br/>+ .monitoring-healthy/failing/...<br/>+ .node-highlighted/dimmed"]
  NB["g.node-bg rect"]
  NF["g.node-fg<br/>(icon, label)"]
  MB["g.shumoku-monitoring-badge<br/>(injected by NodeStatusOverlay)"]
  P["g.port<br/>(port box + label)"]

  SVG --> VP
  VP --> CBG
  VP --> SGS
  VP --> LG
  LG --> LK
  LG --> WMI
  LG --> WMO
  LG --> LH
  VP --> N
  N --> NB
  N --> NF
  N --> MB
  VP --> P

z-order の不変条件: renderer が .viewport 内の大枠順序 subgraphs → edges → nodes → ports を所有する。各エンティティの内部順序 (base → overlay snippet → hit area / label)も renderer が固定する。 overlay 側で DOM 挿入位置を弄る必要は無い。


4. 相関

4.1 Svelte ↔ TS 実装の wrap 関係

オーバーレイの実装パターンは 3 種類:

パターンDOM 操作状態管理
構造化 overlay(.svelte 単独)WeathermapLinkOverlaytemplate の <path> を Svelte が render$derived / $effect
サイドカー(.svelte 単独)NodeStatusOverlay / HighlightOverlay / TooltipOverlaysvgElement.querySelectorAll(...) + classList 操作$effect
サイドカー(関数)attachCamerasvgElement に listener 追加 + d3-zoomclosure
flowchart LR
  subgraph SVELTE["Svelte コンポーネント層"]
    direction TB
    WML["WeathermapLinkOverlay<br/>構造化 .svelte"]
    NSO["NodeStatusOverlay<br/>sidecar .svelte"]
    HLO["HighlightOverlay<br/>sidecar .svelte"]
    TTO["TooltipOverlay<br/>sidecar .svelte"]
  end

  subgraph HELPERS["TS 実装"]
    direction TB
    WMG["weathermap helpers<br/>lib/weathermap/index.ts<br/>(色 / duration / 幾何)"]
    CAM["attachCamera 関数<br/>renderer/lib/camera.ts"]
  end

  WML -->|getUtilizationColor<br/>bpsToDurationMs<br/>utilizationToDurationMs| WMG
  WML -->|bezierOffsetPath| BEZ["@shumoku/core<br/>layout/bezier-path.ts"]
  TTO -. addEventListener .-> SVG[svgElement]
  HLO -. classList + querySelector .-> SVG
  NSO -. classList + querySelector .-> SVG
  TVCAM[TopologyViewer<br/>camera 部分] -->|attach / detach| CAM

  classDef svelte fill:#fff5e6,stroke:#f0a040
  classDef ts fill:#e6ffe6,stroke:#50b050
  class WML,NSO,HLO,TTO svelte
  class WMG,CAM ts

4.2 ライフサイクルの手綱

各オーバーレイは Svelte 5 runes($props / $effect / $derived) の標準パターン:

// 構造化 overlay の骨格 (WeathermapLinkOverlay.svelte)
let { context, metrics, enabled, animation }: Props = $props()
const baseColor = $derived(/* ... */)

$effect(() => {
  const group = context.groupElement
  if (!group || !active) {
    group?.classList.remove('wm-active')
    return
  }
  group.classList.add('wm-active')
  group.style.setProperty('--wm-base-color', baseColor)
  return () => {
    group.classList.remove('wm-active')
    group.style.removeProperty('--wm-base-color')
  }
})
// サイドカー overlay の骨格 (NodeStatusOverlay.svelte)
let { svgElement, status, enabled }: Props = $props()

$effect(() => {
  if (!svgElement || !enabled) return
  const svg = svgElement
  for (const [id, meta] of Object.entries(status ?? {})) {
    const node = svg.querySelector(`g.node[data-id="${CSS.escape(id)}"]`)
    if (!node) continue
    // Device 状態 = 枠 (stroke)
    node.classList.add(`status-${meta.status}`)
    // Monitoring 健全性 = 角バッジ (separate SVG element)
    if (meta.monitoring) {
      node.classList.add(`monitoring-${meta.monitoring}`)
      ensureBadge(node)  // <g class="shumoku-monitoring-badge"><circle/></g>
    }
  }
  return () => {
    /* clear classes + remove badges on cleanup */
  }
})

共通の不変条件:

  • svgElement / context が変わったら自動的に reset($effect 依存追跡)
  • 小さい props 変更だけでは再構築しない(必要な計算は $derived で memoize)
  • unmount で必ず cleanup

5. フロー

5.1 Svelte 視点 — props / bind の向き

flowchart LR
  subgraph STORE["Svelte store (ページ保持)"]
    G[graph]
    M[metricsData]
    HID[highlightedIds]
  end

  TV[TopologyViewer]
  SVG["svgElement"]

  subgraph OV["オーバーレイ"]
    WML[WeathermapLinkOverlay]
    NSO[NodeStatusOverlay]
    HLO[HighlightOverlay]
    TTO[TooltipOverlay]
  end

  G -->|graph prop| TV
  TV -->|bind:svgElement| SVG
  TV -->|linkOverlay snippet| WML
  M -->|metrics| WML
  SVG -->|svgElement| NSO
  M -->|status| NSO
  SVG -->|svgElement| HLO
  HID -->|highlightedIds| HLO
  SVG -->|svgElement| TTO
  G -->|graph| TTO

  classDef store fill:#e6f0ff,stroke:#5080c0
  class G,M,HID store
  • graph は上から props で流れ下る(page → TopologyViewer → ShumokuRenderer)
  • 構造化 overlay は renderer の snippet で各エンティティ内に入る
  • サイドカー overlay は svgElement を受け取って自前で DOM 操作
  • オーバーレイは store に直接触らない(props 経由のみ — テスタビリティと WC 対応のため)

5.2 時間軸 — one-shot と per-tick

flowchart LR
  subgraph ONESHOT["One-shot (page mount / sheet 切替)"]
    direction TB
    G1["サーバ /graph で<br/>mapping.bandwidth を<br/>link.bandwidth に合流"]
    G2["renderer: layout 計算<br/>(libavoid + custom Sugiyama)"]
    G3["SvgEdge / SvgNode が<br/>SVG 出力"]
    G4["camera attach"]
    G1 --> G2 --> G3 --> G4
  end

  subgraph TICK["Per-tick (WS metrics 更新)"]
    direction TB
    T1["WS broadcast<br/>→ metricsData store"]
    T2A["WeathermapLinkOverlay<br/>reactive render"]
    T2B["NodeStatusOverlay<br/>$effect"]
    T1 --> T2A
    T1 --> T2B
  end

  subgraph EVENT["Event-driven"]
    direction TB
    E1["mouse hover"] --> E2["TooltipOverlay<br/>分類 + 配置"]
    E3["highlight 要求"] --> E4["HighlightOverlay<br/>classList 切替"]
    E5["wheel/drag"] --> E6["attachCamera<br/>d3-zoom transform"]
  end
  • One-shot は page mount / sheet 切替時に 1 回。重い layout 計算が ここに集中
  • Per-tick は数秒おき。表示中エンティティの差分更新だけ走る
  • Event-driven はユーザー操作毎。debounce / throttle は各 overlay 内で必要に応じて

6. オーバーレイ詳細

各オーバーレイの入力 / 出力 / DOM 操作 / カスタマイズ点をまとめる。

6.1 Weathermap(構造化、linkOverlay snippet)

用途: 各リンク上に流量を 2 レーンのドットで可視化、利用率に応じて base pipe を tint する。

入力:

  • context: LinkOverlayContext(renderer から)
  • metrics: LinkFlowMetrics(in/out 利用率と bps)
  • enabled / animation('full' / 'reduced' / 'off')

DOM 出力(g.link-group の中):

  • path.wm-overlay[data-direction="in"]
  • path.wm-overlay[data-direction="out"]
  • g.link-group.wm-active クラス + --wm-base-color 変数

CSS 契約(WeathermapLinkOverlay.svelte の <style>):

変数スコープ用途
--wm-colorレーン path色(利用率マップ or down 時赤)
--wm-widthレーン path太さ = max(baseWidth / 2, 2)
--wm-dashレーン pathdash パターン(通常 "3 21"、down "8 4")
--wm-opacityレーン path透明度(通常 0.9、down 0.5)
--wm-durationレーン path周期(bpsToDurationMs(bps)、300ms–2s)
--wm-playレーン pathrunning / paused
--wm-base-colorg.link-groupbase pipe の tint 色

レーン幾何(stroke の内側に 2 本):

laneWidth  = max(baseWidth / 2, 2)
laneOffset = baseWidth / 4
in  lane:  offset = +laneOffset  (stroke 上半分)
out lane:  offset = -laneOffset  (stroke 下半分)

10G リンク(baseWidth 14)の例:

┌───── baseWidth 14 ─────┐
│  ═══ ═  ═══  ═══  ═══ │  ← out lane (width 7, offset -3.5)
│───────── base ──────── │  ← path.link (stroke は --wm-base-color で tint)
│    ═══  ═══  ═══ ═    │  ← in  lane (width 7, offset +3.5)
└────────────────────────┘

アニメ(stroke-dashoffset の keyframe):

@keyframes wm-flow-in  { from { stroke-dashoffset: 0; } to { stroke-dashoffset: -24; } }
@keyframes wm-flow-out { from { stroke-dashoffset: 0; } to { stroke-dashoffset:  24; } }

-24 / +24dash (3) + gap (21) = 24 px 。1 周期で dash+gap 1 単位 進む。

Base tint(SVG 属性を CSS で上書き):

:global(.wm-active > path.link) {
  stroke: var(--wm-base-color, currentColor);
  opacity: 0.55;
  transition: stroke 200ms ease, opacity 200ms ease;
}

:global() は同コンポーネントで定義した変数を、外側 (g.link-group / path.link は SvgEdge.svelte が出してる) のスコープに当てるため。

モード:

モード動作
'full'ドットアニメ + tint
'reduced'.wm-static で keyframe 停止、solid lane
'off'render 自体スキップ
prefers-reduced-motion@media で keyframe 強制停止(tint は残す)
@media printapp.css 側でオーバーレイ非表示、tint 解除

6.2 Node Status(サイドカー、<svelte:head> で CSS 注入)

用途: ノードの状態を 3 軸独立 で可視化する。

意味どこに出る
Mappingこのノードがモニタリング側のホストに割り当て済みかmetrics 入力に entry が無ければ装飾なし
Device 状態機材自体が up/down か (= ICMP 等の直接信号)g.nodestatus-* クラス → 枠(stroke)
Monitoring 健全性監視路自体が機能してるか (= データ収集できてるか)g.nodemonitoring-* クラス + 右下に小バッジ inject

3 軸が独立してるので、status-up + monitoring-paused(動いてるけど メンテ中で値は捨ててる)や status-unknown + monitoring-failing (監視到達不能、機材状態は不明)のような状態が互いに上書きせず 共存する。

入力:

  • svgElement: SVGSVGElement
  • status: Record<nodeId, { status: string; monitoring?: string }>
  • allowedStatuses? (default: ['up', 'down', 'unknown', 'warning', 'degraded'])

DOM 操作:

  • g.node[data-id]status-*monitoring-* のクラスを付与
  • monitoring-* が付くノードには <g class="shumoku-monitoring-badge"> を append。中身は <circle r=5> 一枚で、右下角に transform で 位置決め。塗り色は CSS が monitoring-* クラスから決める
  • バッジは冪等に再利用(re-render で要素が増えない)、cleanup で削除

CSS(<svelte:head><style id="shumoku-node-status-css"> を注入):

/* === Device 状態 (枠) === */
g.node.status-up    .node-bg rect { stroke: #22c55e; stroke-width: 2px; }
g.node.status-down  .node-bg rect {
  stroke: #ef4444; stroke-width: 2.5px;
  filter: drop-shadow(0 0 6px ...);
  animation: shumoku-status-down-pulse 1.6s infinite alternate;
}
g.node.status-warning  .node-bg rect { stroke: #f97316; ... }
g.node.status-degraded .node-bg rect { stroke: #eab308; ... }
g.node.status-unknown  .node-bg rect { stroke: #6b7280; stroke-dasharray: 4 3; }

/* === Monitoring 健全性 (角バッジ) === */
g.node g.shumoku-monitoring-badge { pointer-events: none; }
g.node g.shumoku-monitoring-badge circle { stroke: #fff; stroke-width: 1.5; }
g.node.monitoring-healthy g.shumoku-monitoring-badge circle { fill: #22c55e; }
g.node.monitoring-failing g.shumoku-monitoring-badge circle { fill: #ef4444; }
g.node.monitoring-pending g.shumoku-monitoring-badge circle { fill: #9ca3af; }
g.node.monitoring-paused  g.shumoku-monitoring-badge circle { fill: #3b82f6; }

CSS セレクタが .node-bg rect(枠側)と g.shumoku-monitoring-badge circle (バッジ側)で完全に分離してるので、片方の rule が片方を上書きしない。

<svelte:head> 注入を使う理由は、複数の Svelte コンポーネントが 同じ rule 集合を共有するため(scoped style だと各コンポーネントで 重複定義になる)。

@media printprefers-reduced-motion も同じ block で扱う (バッジは print 時に display: none)。

プラグイン契約

プラグインが返す NodeMetrics(@shumoku/coreplugin-types.ts):

{
  status: 'up' | 'down' | 'unknown' | 'warning' | 'degraded'
  monitoring?: 'healthy' | 'failing' | 'pending' | 'paused'
  monitoringError?: string  // failing 時に "なぜ失敗したか" を残す
  lastSeen?: number
}

monitoring は optional。プラグインが対応してない場合 undefined を 返してOK(その場合はバッジが出ず、枠だけで表現される)。

unmapped は entry をそもそも作らないことで表現する (オーバーレイは entry の無いノードに対しては何もしない)。「mapped だけど不明」と「mapped されてない」を視覚的に区別できる。

6.3 Highlight(サイドカー、reactive + imperative)

用途: 検索結果や選択フィルタで、特定ノードを強調(pulse + drop-shadow)、 他を dim する。

入力:

  • svgElement
  • highlightedIds?: ReadonlySet<string> | string[](reactive)
  • attributeMatch?: { key, value }(reactive、属性ベースマッチ)
  • dimOthers?: boolean — 非マッチを暗くする
  • highlightColor?: string--highlight-color に流す
  • pulseAnimation?: boolean

imperative API(bind:this で取って使える):

  • apply(ids: Iterable<string>): void
  • applyByAttribute(key, value): void
  • clearHighlight(): void

reactive と imperative が共存してるのは、ダッシュボードからの クエリ駆動(reactive)と event 駆動(onhover callback など)の 両方に対応するため。

DOM 操作: マッチした g.node[data-id].node-highlighteddimOthers=true なら他全ての node + link-group に .node-dimmed

CSS(<svelte:head><style id="shumoku-highlight-css">):

g.node.node-highlighted {
  animation: var(--highlight-pulse, node-pulse) 0.5s ease-in-out infinite alternate;
}
g.node.node-highlighted rect,
g.node.node-highlighted circle,
g.node.node-highlighted path {
  stroke: var(--highlight-color, #f59e0b) !important;
  stroke-width: 3px !important;
  filter: drop-shadow(0 0 8px ...);
}
g.node.node-dimmed,
g.link-group.node-dimmed {
  opacity: 0.15;
  transition: opacity 0.2s ease;
}

色とアニメは --highlight-color / --highlight-pulse 変数で外から 上書き可能。SVG element 側に style.setProperty で書き込む。

6.4 Tooltip(サイドカー、DOM event)

用途: ホバー時のツールチップ。何が hover されたか(node / subgraph / link)を分類して、カスタマイズ可能な内容を浮かべる。

入力:

  • svgElement / graph / enabled / delay / maxWidth
  • content?: Snippet<{ hovered, graph }> — Svelte snippet で本体を描く
  • contentBuilder?: (hovered, graph) => string — 文字列で本体を返す関数

DOM 操作:

  • mouseover / mouseout / mousemovesvgElement に直接 attach (AbortController で cleanup)
  • ツールチップ本体は コンポーネントが直接 <div> を render(SVG 外、position: fixed)
  • DOM 改変は無し(svg 配下は読み取りのみ)

hit detection:

function classify(target: EventTarget): HoveredElement | null {
  if (target.closest('g.node[data-id]'))      return { kind: 'node', ... }
  if (target.closest('g.subgraph[data-id]'))  return { kind: 'subgraph', ... }
  if (target.closest('g.link-group[data-link-id]')) return { kind: 'link', ... }
  return null
}

closest() で renderer の class 名 / data-attribute を逆引きしてる。 これはサイドカー特有の “public DOM contract” 依存(2.3 節参照)。

6.5 Camera(関数 utility、@shumoku/renderer 提供)

用途: pan / zoom。マウスホイール = ズーム、トラックパッド 2 本指 = パン、ピンチ = ズーム、Alt+左 / 中ボタンドラッグ = パン。

API(関数1個):

const camera = attachCamera(svgElement, {
  wheelMode: 'auto',          // 'auto' | 'zoom' | 'pan'
  panFilter: ...,
  ...
})
camera.zoomBy(factor)
camera.reset()
camera.panToNode(nodeId)
camera.detach()

実装: d3-zoom + wheel-gestures。マウスホイールとトラックパッドを gesture 開始時に “sticky” に判別し、gesture 中は同じ扱いを維持 (zoom と pan が混ざらない)。詳しくは docs/ARCHITECTURE.md の Camera 節 と libs/@shumoku/renderer/src/lib/camera.ts を参照。

DOM 操作: svgElement 上の wheel / pointerdown / pointermove listener と、g.viewporttransform 属性。renderer の class 名 には依存しない(g.viewport は renderer が常に出す唯一の zoom 対象)。

TopologyViewer.svelte がこの関数を呼んで camera を attach し、 sheet 切替時に camera.reset() する。


7. Web Component 対応状況

7.1 公開 API 対応表

WC も Svelte 版も svgElement を外に出す契約。サイドカーはこの svgElement を受け取って動く。

WC (<shumoku-renderer>)Svelte (<ShumokuRenderer>)
graphel.graph = ... setterbind:nodes bind:links ...
themeel.theme = ... settertheme={...} prop
mode`el.mode = ‘view’‘edit’`
SVG 取得el.svgElement getterbind:svgElement
イベントel.onshumokuselect = fnonselect={fn}
命令的操作el.addNewNode(...)renderer.addNewNode(...)
構造化 overlay (snippet)✗ 未対応

WC 版からの利用例(他フレームワーク / 素 HTML):

<shumoku-renderer id="topo"></shumoku-renderer>
<script>
  const el = document.querySelector('#topo')
  el.graph = myGraph
  // サイドカーは同じ関数で動く
  const camera = attachCamera(el.svgElement)
  // 構造化 overlay は WC では未提供(後述)
</script>

7.2 Shadow DOM での CSS 注入問題

WC 版は Shadow DOM 内 に SVG を描く。サイドカーの DOM 操作は svgElement 配下で完結するので問題ないが、CSS 注入 は経路によって shadow に届く / 届かないが分かれる。

注入方式使用しているオーバーレイShadow DOM への到達
Svelte コンポーネントの <style>(scoped)WeathermapLinkOverlay微妙 — Svelte ランタイムが生成する <style> は document.head 行き。:global() 内の rule は shadow に届かない
<svelte:head><style> 注入NodeStatusOverlay / HighlightOverlay✗ — 常に document.head 行き、shadow に届かない
要素の style.setProperty('--var', ...)全 overlay✓ — 要素自体に書くので shadow も関係なし
attachCamera の SVG transform 操作Camera✓ — CSS 不使用

現状: Svelte 版しか使ってないので実害なし。WC 外部配布を始める タイミングで、<svelte:head> の代わりに shadow root に注入 する ヘルパーが必要:

function ensureStyleInRoot(svg: SVGSVGElement, id: string, css: string): void {
  const root = svg.getRootNode()
  const target = root instanceof ShadowRoot ? root : document.head
  if (target.querySelector(`#${id}`)) return
  const style = document.createElement('style')
  style.id = id
  style.textContent = css
  target.appendChild(style)
}

これを各 overlay の $effect 開始時に呼ぶように変える(現状 NodeStatus / Highlight の <svelte:head> 部分を置き換える)。

構造化 overlay の WC 対応: snippet は Svelte 限定なので、WC では linkOverlay 等の機能が使えない。代替案は以下のいずれか:

  • HTML <slot> で「リンクごとに複製」を実現(MutationObserver が必要)
  • imperative callback API(el.onRenderLink = (link, group) => {...})
  • 構造化 overlay は Svelte 専用と割り切り、WC は visual-only(camera
    • tooltip)で運用

8. 不変条件・注意点

8.1 共通(全オーバーレイ)

  1. 入力は受け取った参照のみで完結(svgElement / context)。 document.querySelector などグローバルアクセスは使わない
  2. renderer の DOM 属性は読むだけ、書き換えない(必要なら CSS 変数 や class 経由で上書き)
  3. unmount で必ず cleanup(class 削除 / CSS 変数削除 / listener abort / timer clear)
  4. 渡された domain object が変われば自動 reset($effect 依存追跡 に任せる)
  5. store には触らない(props 経由)

8.2 構造化 overlay

  • snippet 内で closest('g.link-group') 等の class 名逆引きは禁止 — 必要な参照は context 経由で受け取る
  • snippet の return 直前に追加要素を render するだけで済むよう設計する (DOM 並び替えは不可)

8.3 サイドカー overlay

  • renderer の public DOM contract(class 名 / data-attribute)に 依存する。ここは renderer 側の breaking change 扱い:
    • g.viewportg.node[data-id]g.link-group[data-link-id]g.subgraph[data-id]g.node-bgpath.link、port box
  • DOM event の listener は AbortController で必ず cleanup

8.4 Weathermap 固有

  • レーン path のサンプリング: 直線は fast path、曲線は法線方向に 30+ 点
  • pan/zoom 追従: g.viewport の transform に乗るので毎フレーム同期不要
  • prefers-reduced-motion は keyframe だけ止める(色 / 太さ情報は残す)

付録 A: 関連ファイル

Renderer 中核

ファイル役割
libs/@shumoku/renderer/src/components/ShumokuRenderer.svelte描画中核の Svelte コンポーネント
libs/@shumoku/renderer/src/components/svg/SvgEdge.sveltepath.link + linkOverlay snippet host
libs/@shumoku/renderer/src/components/svg/SvgNode.svelteg.node + nodeOverlay snippet host
libs/@shumoku/renderer/src/components/svg/SvgPort.svelteport box + portOverlay snippet host
libs/@shumoku/renderer/src/lib/overlays.tssnippet の型定義(LinkOverlayContext 等)
libs/@shumoku/renderer/src/lib/camera.tsattachCamera (pan/zoom)
libs/@shumoku/renderer/src/wc.svelte.ts<shumoku-renderer> Web Component ラッパー

構造化 overlay

ファイル役割
apps/server/web/src/lib/components/topology/WeathermapLinkOverlay.sveltelinkOverlay snippet 内で flow lane を描く
apps/server/web/src/lib/weathermap/index.tsweathermap の色 / duration / 幾何ヘルパー

サイドカー overlay

ファイル役割
apps/server/web/src/lib/components/topology/NodeStatusOverlay.sveltestatus-up/down/... クラスと CSS(svelte:head)
apps/server/web/src/lib/components/topology/HighlightOverlay.svelteノード強調(reactive + imperative)とその CSS
apps/server/web/src/lib/components/topology/TooltipOverlay.sveltehover ツールチップ

ホスト + サーバ側

ファイル役割
apps/server/web/src/lib/components/topology/TopologyViewer.svelterenderer mount + attachCamera + ViewerContext 出力
apps/server/api/src/api/topologies.ts (applyMappingBandwidth)mapping の override を link.bandwidth に合流させるサーバ側ロジック
libs/@shumoku/core/src/layout/link-utils.ts (getLinkWidth)bandwidth → stroke-width の校正(single source of truth)