系譜グラフLineageGraphExperimental
上流と下流の関係を、複数親・複数子を含むノードリンク図として表示します。
プレビュー
状態とバリエーション
複数親
複数の上流ロットが1つの工程に合流する関係を描けます。
縦方向
direction="vertical" で上から下へ流れる図にできます。
ノード描画の差し替え
renderNode でノード内に Badge や補足情報を配置できます。
プロパティ
表は横にスクロールできます
| プロパティ | 型 | 初期値 | 説明 |
|---|---|---|---|
| nodes | LineageNode[] | - | 表示するノードです。id、label、sublabel、tone、ariaLabel を渡せます。 |
| edges | LineageEdge[] | - | 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 / nodeHeight | number | 160 / 56 | ノード箱のサイズです。 |
| layerGap / rowGap | number | 64 / 16 | 階層間、同階層内の余白です。 |
| label | ReactNode | - | グラフ全体のアクセシブル名です。 |
使い方
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>
);
}