系譜グラフLineageGraphExperimental

上流と下流の関係を、複数親・複数子を含むノードリンク図として表示します。

プレビュー

状態とバリエーション

複数親

複数の上流ロットが1つの工程に合流する関係を描けます。

縦方向

direction="vertical" で上から下へ流れる図にできます。

ノード描画の差し替え

renderNode でノード内に Badge や補足情報を配置できます。

プロパティ

表は横にスクロールできます
プロパティ初期値説明
nodesLineageNode[]-表示するノードです。id、label、sublabel、tone、ariaLabel を渡せます。
edgesLineageEdge[]-from から to への有向エッジです。複数親・複数子を扱えます。
direction"horizontal" | "vertical""horizontal"左から右、または上から下の流れを選びます。
renderNode(node: LineageNode) => ReactNode-ノード本体の描画を差し替えます。関数propのため Client Component からのみ渡すこと(Server Component から渡すと next build が落ちる)。JSX を返すため serializable な代替は無く、RSC からは "use client" ラッパーで包む。(#338)
onSelectNode(node: LineageNode) => void-渡すとノードが選択可能なボタンになります。
nodeWidth / nodeHeightnumber160 / 56ノード箱のサイズです。
layerGap / rowGapnumber64 / 16階層間、同階層内の余白です。
labelReactNode-グラフ全体のアクセシブル名です。

使い方

import * as React from "react";
import {
  Badge,
  LineageGraph,
  Sheet,
  SheetContent,
  SheetDescription,
  SheetHeader,
  SheetTitle,
  type LineageEdge,
  type LineageNode,
} from "@gunjo/ui";

const nodes: LineageNode[] = [
  { id: "rm-01", label: "原料ロット RM-01", sublabel: "仕入 2026/06/01", tone: "info" },
  { id: "rm-02", label: "原料ロット RM-02", sublabel: "仕入 2026/06/02", tone: "info" },
  { id: "mix-01", label: "混合ロット MX-01", sublabel: "配合工程", tone: "warning" },
  { id: "pl-2405", label: "製品ロット PL-2405", sublabel: "出荷対象", tone: "primary" },
];

const edges: LineageEdge[] = [
  { from: "rm-01", to: "mix-01" },
  { from: "rm-02", to: "mix-01" },
  { from: "mix-01", to: "pl-2405" },
];

export function LotLineage() {
  const rootRef = React.useRef<HTMLDivElement | null>(null);
  const [portalContainer, setPortalContainer] = React.useState<HTMLElement | null>(null);
  const [selectedId, setSelectedId] = React.useState("pl-2405");
  const [open, setOpen] = React.useState(false);
  const [isLargeViewport, setIsLargeViewport] = React.useState(false);
  const selectedNode = nodes.find((node) => node.id === selectedId) ?? nodes[0];
  const upstreamCount = edges.filter((edge) => edge.to === selectedNode.id).length;
  const downstreamCount = edges.filter((edge) => edge.from === selectedNode.id).length;

  React.useEffect(() => {
    setPortalContainer(rootRef.current?.closest<HTMLElement>("[data-doc-component-preview-surface]") ?? rootRef.current);
  }, []);

  React.useEffect(() => {
    const media = window.matchMedia("(min-width: 1024px)");
    const update = () => setIsLargeViewport(media.matches);
    update();
    media.addEventListener("change", update);
    return () => media.removeEventListener("change", update);
  }, []);

  return (
    <div ref={rootRef} className="relative grid w-full max-w-4xl gap-4 overflow-hidden rounded-lg border bg-card p-4">
      <LineageGraph
        nodes={nodes}
        edges={edges}
        label="ロット系譜"
        onSelectNode={(node) => {
          setSelectedId(node.id);
          setOpen(true);
        }}
      />
      <Sheet open={open} onOpenChange={setOpen} modal={false}>
        <SheetContent
          side={isLargeViewport ? "right" : "bottom"}
          portalContainer={portalContainer}
          overlayClassName="rounded-md"
          closeLabel="閉じる"
          className={isLargeViewport ? "grid w-[300px] max-w-[calc(100%-2rem)] p-4" : "max-h-[72%] overflow-y-auto p-4"}
        >
          <SheetHeader>
            <SheetTitle>対象ロット</SheetTitle>
            <SheetDescription>{selectedNode.sublabel}</SheetDescription>
          </SheetHeader>
          <div className="grid gap-3 rounded-lg border border-primary-border bg-primary-subtle/70 p-3">
            <p className="text-sm font-semibold text-foreground">{selectedNode.label}</p>
            <Badge variant="outline" className="w-fit">{selectedNode.tone}</Badge>
            <dl className="grid grid-cols-2 gap-2 text-xs">
              <div className="rounded-md bg-background/60 p-2">
                <dt className="text-muted-foreground">上流</dt>
                <dd className="text-sm font-semibold text-foreground">{upstreamCount}</dd>
              </div>
              <div className="rounded-md bg-background/60 p-2">
                <dt className="text-muted-foreground">下流</dt>
                <dd className="text-sm font-semibold text-foreground">{downstreamCount}</dd>
              </div>
            </dl>
          </div>
        </SheetContent>
      </Sheet>
    </div>
  );
}

使用コンポーネント