mcpField
S
svelte-flow

svelte-flow

github.com/freightcognition開発442 回インストール2 週間前に更新
リポジトリ

svelte-flow について

svelte-flow は、@xyflow/svelte でノードベースの UI を構築する開発者向けガイドです。基本設定に加え、カスタムノードやエッジ、イベント、レイアウト、性能上の注意点を扱います。

主な機能

  1. 01ノードとエッジの設定
  2. 02カスタムノードとエッジ型の作成
  3. 03グラフ操作イベントの処理
  4. 04Dagre によるノード配置
  5. 05グラフ用ユーティリティとコンポーネント
  6. 06状態管理と画像のエクスポート

活用シーン

  1. 01Svelte のノードエディター構築
  2. 02独自のグラフ操作の追加
  3. 03グラフ要素の階層配置
  4. 04グラフ表示の画像エクスポート

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/freightcognition/linehaulai-claude-marketplace --skill svelte-flow

インストール後、Claude Code で/svelte-flowを入力して読み込まれたか確認します。

詳細

Svelte Flow

Svelte Flow(@xyflow/svelte)でノードベースの UI を構築するためのエキスパートガイドです。

インストール

npm install @xyflow/svelte

基本的なセットアップ

<script lang="ts">
  import { SvelteFlow, Background } from '@xyflow/svelte';
  import '@xyflow/svelte/dist/style.css';
  
  // Use $state.raw for nodes and edges (Svelte 5)
  let nodes = $state.raw([
    { id: '1', data: { label: 'Node 1' }, position: { x: 0, y: 0 } },
    { id: '2', data: { label: 'Node 2' }, position: { x: 0, y: 100 } }
  ]);
  
  let edges = $state.raw([
    { id: 'e1-2', source: '1', target: '2' }
  ]);
</script>

<SvelteFlow bind:nodes bind:edges fitView>
  <Background />
</SvelteFlow>

主な概念

ノード

各ノードには以下が必要です。

  • id:一意の識別子
  • position:{ x: number, y: number }
  • data:カスタムプロパティを持つオブジェクト(通常は label を含みます)
  • 任意:type、style、hidden、width、height、sourcePosition、targetPosition
const node = {
  id: '1',
  type: 'custom',
  position: { x: 100, y: 100 },
  data: { label: 'My Node', color: '#ff0000' },
  style: 'background: #f0f0f0',
  width: 150,
  height: 100
};
エッジ

各エッジには以下が必要です。

  • id:一意の識別子
  • source:ソースノードの ID
  • target:ターゲットノードの ID
  • 任意:type、label、animated、markerEnd、markerStart、style、hidden
const edge = {
  id: 'e1-2',
  source: '1',
  target: '2',
  type: 'smoothstep',
  label: 'connects to',
  animated: true,
  markerEnd: { type: MarkerType.ArrowClosed }
};
組み込みの型

エッジの型:

  • default:直線
  • smoothstep:滑らかな 90 度の折れ線
  • step:鋭角な 90 度の折れ線
  • straight:default の別名
  • bezier:ベジェ曲線

ノードの型:

  • default:基本的な長方形のノード
  • input:ソースハンドルのみを持つノード
  • output:ターゲットハンドルのみを持つノード

カスタムノード


<script lang="ts" module>
  import type { Node, NodeProps } from '@xyflow/svelte';
  
  export type CustomNodeType = Node<{
    label: string;
    color?: string;
  }>;
</script>

<script lang="ts">
  import { Handle, Position } from '@xyflow/svelte';
  
  let { data }: NodeProps<CustomNodeType> = $props();
</script>

<div class="custom-node" style:background={data.color}>
  <Handle type="target" position={Position.Left} />
  <div>{data.label}</div>
  <Handle type="source" position={Position.Right} />
</div>

<style>
  .custom-node {
    padding: 10px;
    border-radius: 5px;
    border: 1px solid #ddd;
  }
</style>

カスタムノードを登録します。

<script lang="ts">
  import CustomNode from './CustomNode.svelte';
  
  const nodeTypes = {
    custom: CustomNode
  };
  
  let nodes = $state.raw([
    { id: '1', type: 'custom', data: { label: 'Custom', color: '#ff7000' }, position: { x: 0, y: 0 } }
  ]);
</script>

<SvelteFlow bind:nodes {nodeTypes} bind:edges fitView />

カスタムエッジ


<script lang="ts">
  import { BaseEdge, EdgeLabel, getStraightPath, type EdgeProps } from '@xyflow/svelte';
  
  let { id, sourceX, sourceY, targetX, targetY, label }: EdgeProps = $props();
  
  let [edgePath, labelX, labelY] = $derived(
    getStraightPath({ sourceX, sourceY, targetX, targetY })
  );
</script>

<BaseEdge {id} path={edgePath} />
{#if label}
  <EdgeLabel x={labelX} y={labelY}>
    <div class="edge-label">{label}</div>
  </EdgeLabel>
{/if}

利用できるパスヘルパー:

  • getStraightPath()
  • getBezierPath()
  • getSmoothStepPath()

カスタムエッジを登録します。

<script lang="ts">
  const edgeTypes = {
    custom: CustomEdge
  };
</script>

<SvelteFlow bind:nodes bind:edges {edgeTypes} fitView />

状態の更新

重要: ノードとエッジはイミュータブルです。更新を反映するには、新しいオブジェクトを作成してください。

<script lang="ts">
  // ❌ Won't work - direct mutation
  nodes[0].position.x = 100;
  
  // ✅ Works - create new array
  nodes = nodes.map((node) => {
    if (node.id === '1') {
      return { ...node, position: { ...node.position, x: 100 } };
    }
    return node;
  });
  
  // ✅ Also works - using helper from useSvelteFlow
  import { useSvelteFlow } from '@xyflow/svelte';
  const { updateNode } = useSvelteFlow();
  
  updateNode('1', (node) => ({
    ...node,
    position: { ...node.position, x: 100 }
  }));
</script>

イベント処理

一般的なイベントハンドラー:

<SvelteFlow
  bind:nodes
  bind:edges
  onnodeclick={(event) => console.log('node clicked', event.targetNode)}
  onnodedrag={(event) => console.log('node dragging', event.targetNode)}
  onnodedragstop={(event) => console.log('drag ended', event.targetNode)}
  onedgeclick={(event) => console.log('edge clicked', event.edge)}
  onconnect={(params) => {
    edges = [...edges, { id: `e${params.source}-${params.target}`, ...params }];
  }}
/>

利用可能なイベント:

  • onnodeclick、onnodedoubleclick
  • onnodedrag、onnodedragstart、onnodedragstop
  • onedgeclick、onedgedoubleclick
  • onconnect、onconnectstart、onconnectend
  • onbeforedelete(非同期。削除を防止できます)
  • oninit、onmove、onmovestart、onmoveend

レイアウトアルゴリズム

Dagre(階層型)
import dagre from '@dagrejs/dagre';
import { Position } from '@xyflow/svelte';

function getLayoutedElements(nodes: Node[], edges: Edge[], direction = 'TB') {
  const dagreGraph = new dagre.graphlib.Graph();
  dagreGraph.setDefaultEdgeLabel(() => ({}));
  
  const isHorizontal = direction === 'LR';
  dagreGraph.setGraph({ rankdir: direction });
  
  const nodeWidth = 172;
  const nodeHeight = 36;
  
  nodes.forEach((node) => {
    dagreGraph.setNode(node.id, { width: nodeWidth, height: nodeHeight });
  });
  
  edges.forEach((edge) => {
    dagreGraph.setEdge(edge.source, edge.target);
  });
  
  dagre.layout(dagreGraph);
  
  const layoutedNodes = nodes.map((node) => {
    const nodeWithPosition = dagreGraph.node(node.id);
    return {
      ...node,
      targetPosition: isHorizontal ? Position.Left : Position.Top,
      sourcePosition: isHorizontal ? Position.Right : Position.Bottom,
      position: {
        x: nodeWithPosition.x - nodeWidth / 2,
        y: nodeWithPosition.y - nodeHeight / 2,
      },
    };
  });
  
  return { nodes: layoutedNodes, edges };
}

高度な機能

フローティングエッジ

ノード境界上の最も近い点に動的に接続するエッジです。


<script lang="ts">
  import { BaseEdge, getBezierPath, type EdgeProps } from '@xyflow/svelte';
  import { getEdgeParams } from './utils';
  
  let { id, source, target, markerEnd }: EdgeProps = $props();
  
  const { sx, sy, tx, ty, sourcePos, targetPos } = $derived(
    getEdgeParams(source, target)
  );
  
  let [edgePath] = $derived(
    getBezierPath({
      sourceX: sx,
      sourceY: sy,
      sourcePosition: sourcePos,
      targetX: tx,
      targetY: ty,
      targetPosition: targetPos,
    })
  );
</script>

<BaseEdge {id} path={edgePath} {markerEnd} />

ConnectionMode.Loose を指定して登録します。

<script lang="ts">
  import { ConnectionMode } from '@xyflow/svelte';
</script>

<SvelteFlow
  bind:nodes
  bind:edges
  edgeTypes={{ floating: FloatingEdge }}
  connectionMode={ConnectionMode.Loose}
  fitView
/>
近接接続

ノードを近づけてドラッグすると、自動的に接続します。

<script lang="ts">
  const MIN_DISTANCE = 150;
  
  function getClosestEdge(node: Node, nodes: Node[]) {
    const closestNode = nodes.reduce(
      (res, n) => {
        if (n.id !== node.id) {
          const dx = n.position.x - node.position.x;
          const dy = n.position.y - node.position.y;
          const d = Math.sqrt(dx * dx + dy * dy);
          
          if (d < res.distance && d < MIN_DISTANCE) {
            res.distance = d;
            res.node = n;
          }
        }
        return res;
      },
      { distance: Number.MAX_VALUE, node: null }
    );
    
    if (!closestNode.node) return null;
    
    const closeNodeIsSource = closestNode.node.position.x < node.position.x;
    
    return {
      id: closeNodeIsSource
        ? `${node.id}-${closestNode.node.id}`
        : `${closestNode.node.id}-${node.id}`,
      source: closeNodeIsSource ? closestNode.node.id : node.id,
      target: closeNodeIsSource ? node.id : closestNode.node.id,
      class: 'temp',
    };
  }
  
  function onNodeDrag({ targetNode: node }) {
    const closestEdge = getClosestEdge(node, nodes);
    
    edges = edges.filter(e => e.class !== 'temp');
    
    if (closestEdge && !edges.some(e => e.source === closestEdge.source && e.target === closestEdge.target)) {
      edges = [...edges, closestEdge];
    }
  }
  
  function onNodeDragStop() {
    edges = edges.map((edge) => 
      edge.class === 'temp' ? { ...edge, class: '' } : edge
    );
  }
</script>

<SvelteFlow
  bind:nodes
  bind:edges
  onnodedrag={onNodeDrag}
  onnodedragstop={onNodeDragStop}
  fitView
/>
エッジの再接続

エッジの端点を別のノードにドラッグできるようにします。


<script lang="ts">
  import {
    BaseEdge,
    EdgeReconnectAnchor,
    getBezierPath,
    type EdgeProps
  } from '@xyflow/svelte';
  
  let { sourceX, sourceY, targetX, targetY, selected, ...props }: EdgeProps = $props();
  
  const [edgePath] = $derived(
    getBezierPath({ sourceX, sourceY, targetX, targetY })
  );
  
  let reconnecting = $state(false);
</script>

{#if !reconnecting}
  <BaseEdge path={edgePath} {...props} />
{/if}

{#if selected}
  <EdgeReconnectAnchor
    bind:reconnecting
    type="source"
    position={{ x: sourceX, y: sourceY }}
  />
  <EdgeReconnectAnchor
    bind:reconnecting
    type="target"
    position={{ x: targetX, y: targetY }}
  />
{/if}
ノードを削除してエッジを再接続
<script lang="ts">
  import { getIncomers, getOutgoers, getConnectedEdges, type OnBeforeDelete } from '@xyflow/svelte';
  
  const onbeforedelete: OnBeforeDelete = async ({ nodes: deletedNodes }) => {
    let remainingNodes = [...nodes];
    
    edges = deletedNodes.reduce((acc, node) => {
      const incomers = getIncomers(node, remainingNodes, acc);
      const outgoers = getOutgoers(node, remainingNodes, acc);
      const connectedEdges = getConnectedEdges([node], acc);
      
      const remainingEdges = acc.filter((edge) => !connectedEdges.includes(edge));
      
      const createdEdges = incomers.flatMap(({ id: source }) =>
        outgoers.map(({ id: target }) => ({
          id: `${source}->${target}`,
          source,
          target,
        }))
      );
      
      remainingNodes = remainingNodes.filter((rn) => rn.id !== node.id);
      
      return [...remainingEdges, ...createdEdges];
    }, edges);
    
    nodes = remainingNodes;
    
    return true;
  };
</script>

<SvelteFlow bind:nodes bind:edges {onbeforedelete} fitView />

コンポーネント

コントロール
<script lang="ts">
  import { SvelteFlow, Controls } from '@xyflow/svelte';
</script>

<SvelteFlow bind:nodes bind:edges fitView>
  <Controls showLock={false} showFitView={true} showZoom={true} />
</SvelteFlow>
ミニマップ
<script lang="ts">
  import { SvelteFlow, MiniMap } from '@xyflow/svelte';
</script>

<SvelteFlow bind:nodes bind:edges fitView>
  <MiniMap />
</SvelteFlow>
背景
<script lang="ts">
  import { SvelteFlow, Background, BackgroundVariant } from '@xyflow/svelte';
</script>

<SvelteFlow bind:nodes bind:edges fitView>
  <Background variant={BackgroundVariant.Dots} gap={12} size={1} />
</SvelteFlow>

バリエーション:Dots、Lines、Cross

パネル(カスタムコントロール)
<script lang="ts">
  import { SvelteFlow, Panel } from '@xyflow/svelte';
</script>

<SvelteFlow bind:nodes bind:edges fitView>
  <Panel position="top-left">
    <button>Custom Button</button>
  </Panel>
</SvelteFlow>

位置:top-left、top-center、top-right、bottom-left、bottom-center、bottom-right

ユーティリティ関数

import {
  getConnectedEdges,
  getIncomers,
  getOutgoers,
  addEdge,
  applyEdgeChanges,
  applyNodeChanges
} from '@xyflow/svelte';

// Get edges connected to nodes
const connectedEdges = getConnectedEdges(nodes, edges);

// Get nodes that connect TO a node
const incomers = getIncomers(targetNode, nodes, edges);

// Get nodes that a node connects TO
const outgoers = getOutgoers(sourceNode, nodes, edges);

// Add edge with validation
edges = addEdge(connection, edges);

ストア管理(代替手段)

状態を一元管理する場合:

// store.svelte.js
let nodes = $state.raw([...]);
let edges = $state.raw([...]);

export const getNodes = () => nodes;
export const getEdges = () => edges;
export const setNodes = (newNodes) => nodes = newNodes;
export const setEdges = (newEdges) => edges = newEdges;

<script>
  import { getNodes, getEdges, setNodes, setEdges } from './store.svelte.js';
</script>

<SvelteFlow bind:nodes={getNodes, setNodes} bind:edges={getEdges, setEdges} />

画像へのエクスポート

<script lang="ts">
  import { useSvelteFlow } from '@xyflow/svelte';
  
  const { toObject, getViewport } = useSvelteFlow();
  
  async function downloadImage() {
    const { toPng } = await import('html-to-image');
    const dataUrl = await toPng(document.querySelector('.svelte-flow'));
    const a = document.createElement('a');
    a.setAttribute('download', 'flow.png');
    a.setAttribute('href', dataUrl);
    a.click();
  }
</script>

パフォーマンスのヒント

  • ノードとエッジには $state.raw を使用します(深いリアクティビティを防ぎます)
  • 大規模なグラフ(ノード 1000 個以上)では、elevateNodesOnSelect={false} と elevateEdgesOnSelect={false} を設定します
  • minZoom と maxZoom を使用して、ズームレベルを制限します
  • 非常に大規模なグラフでは、仮想化を検討してください

よく使う Props

<SvelteFlow
  bind:nodes
  bind:edges
  nodeTypes={customNodeTypes}
  edgeTypes={customEdgeTypes}
  defaultEdgeOptions={{ animated: true, type: 'smoothstep' }}
  connectionLineType={ConnectionLineType.Straight}
  connectionMode={ConnectionMode.Loose}
  fitView
  minZoom={0.1}
  maxZoom={2}
  snapToGrid={true}
  snapGrid={[15, 15]}
  elevateNodesOnSelect={true}
  elevateEdgesOnSelect={true}
  deleteKeyCode="Delete"
  selectionKeyCode="Shift"
  multiSelectionKeyCode="Meta"
  panOnScroll={true}
  zoomOnScroll={true}
  zoomOnDoubleClick={true}
  zoomOnPinch={true}
  panOnDrag={true}
/>

コメント