ガントチャートGanttExperimental

リソース行と時間軸にバーを配置し、重なりや区間分割を含む予定を表示するガントチャートです。

プレビュー

21
22
23
24
6/25
26
27
28
第1ライン組立
第2ライン塗装
第3ライン検査
第4ライン梱包
第5ライン出荷前確認
保全チーム設備点検

状態とバリエーション

重なりレーン

同じ行で期間が重なるバーは自動で縦に積まれます。

21
22
23
24
6/25
26
27
28
第1ライン組立
第2ライン塗装
第3ライン検査
第4ライン梱包
第5ライン出荷前確認
保全チーム設備点検

区間分割

segments を渡すと、1本のバー内に工程や折返し区間を表現できます。

21
22
23
24
6/25
26
27
28
第1ライン組立
第2ライン塗装
第3ライン検査
第4ライン梱包
第5ライン出荷前確認
保全チーム設備点検

時間単位

resolution="hour" は1日の配車や運行表に使います。

08:00
10:00
12:00
14:00
16:00
車両 12運行日

プロパティ

表は横にスクロールできます
プロパティ初期値説明
rowsGanttRow[]-左軸のリソース行です。id、label、sublabel を渡します。
itemsGanttItem[]-時間軸に配置するバーです。rowId、start、end、label、tone、segments を持てます。
startDate / endDatestring | Date-表示する時間範囲です。
todaystring | Date-今日線として表示する日付です。SSR のため明示指定します。
resolution"day" | "hour""day"日単位または時間単位の軸を選びます。
laneHeightnumber28重なりを積む1レーンの高さです。
rowLabelWidthnumber120固定される行ラベル列の幅です。
dayWidth / hourWidthnumber44横スクロール内の軸1単位の幅です。
onSelectItem(item: GanttItem) => void-バー選択時に呼びます。

使い方

import * as React from "react";
	import {
	  Badge,
	  Gantt,
	  Sheet,
	  SheetContent,
	  SheetHeader,
	  SheetTitle,
	  type GanttItem,
	  type GanttRow,
	} from "@gunjo/ui";

const rows: GanttRow[] = [
  { id: "l1", label: "第1ライン", sublabel: "組立" },
  { id: "l2", label: "第2ライン", sublabel: "塗装" },
];

const items: GanttItem[] = [
  { id: "j1", rowId: "l1", start: "2026-06-22", end: "2026-06-24", label: "A-101", tone: "info" },
  { id: "j3", rowId: "l2", start: "2026-06-22", end: "2026-06-25", label: "B-201", tone: "success" },
  { id: "j4", rowId: "l2", start: "2026-06-23", end: "2026-06-26", label: "B-202", tone: "warning" },
];

const itemDetails = {
  j1: { status: "進行中", owner: "生産管理 A", next: "塗装前検査" },
  j3: { status: "進行中", owner: "塗装班", next: "乾燥室へ移動" },
  j4: { status: "要確認", owner: "品質保証", next: "色差チェック" },
};

const toneStyles = {
  default: { panel: "border-border bg-background" },
  primary: { panel: "border-primary bg-primary/10" },
  info: { panel: "border-info-border bg-info-subtle/55" },
  success: { panel: "border-success-border bg-success-subtle/55" },
  warning: { panel: "border-warning-border bg-warning-subtle/65" },
} as const;

export function ProductionGantt() {
  const [selectedId, setSelectedId] = React.useState(items[0].id);
  const [drawerOpen, setDrawerOpen] = React.useState(false);
  const [previewRoot, setPreviewRoot] = React.useState<HTMLDivElement | null>(null);
  const [portalContainer, setPortalContainer] = React.useState<HTMLElement | null>(null);
  const [isCompactLayout, setIsCompactLayout] = React.useState(false);
  const selectedItem = items.find((item) => item.id === selectedId) ?? items[0];
  const selectedRow = rows.find((row) => row.id === selectedItem.rowId);
  const selectedDetail = itemDetails[selectedItem.id as keyof typeof itemDetails];
  const selectedTone = toneStyles[selectedItem.tone ?? "default"];

  React.useEffect(() => {
    if (!previewRoot) return;
    const updateLayout = () => {
      const width = previewRoot.offsetWidth || previewRoot.getBoundingClientRect().width;
      setIsCompactLayout(width < 800);
    };
    updateLayout();
    const observer = new ResizeObserver(updateLayout);
    observer.observe(previewRoot);
    return () => observer.disconnect();
  }, [previewRoot]);

  const setPreviewNode = React.useCallback((node: HTMLDivElement | null) => {
    setPreviewRoot(node);
    setPortalContainer(node);
  }, []);

  return (
    <div ref={setPreviewNode} className="relative flex w-full max-w-4xl flex-col gap-4 overflow-visible rounded-lg border bg-card p-4">
      <div className="grid min-w-0 gap-4">
        <Gantt
          rows={rows}
          items={items}
          startDate="2026-06-21"
          endDate="2026-06-29"
          today="2026-06-24"
          label="生産ライン計画"
          dayWidth={52}
          onSelectItem={(item) => {
            setSelectedId(item.id);
            setDrawerOpen(true);
          }}
        />
      </div>
      <Sheet open={drawerOpen} onOpenChange={setDrawerOpen}>
        <SheetContent
          side={isCompactLayout ? "bottom" : "right"}
          portalContainer={portalContainer}
          closeLabel="閉じる"
          overlayClassName="bg-overlay/45"
          className={
            isCompactLayout
              ? "max-h-[calc(100%-2rem)] overflow-y-auto rounded-t-lg p-4"
              : "w-[min(360px,calc(100%-2rem))] max-w-sm overflow-y-auto p-4"
          }
        >
          <SheetHeader className="pr-8 text-left">
            <SheetTitle>工程詳細 / {selectedItem.label}</SheetTitle>
          </SheetHeader>
          <div className={["mt-4 rounded-lg border border-l-4 p-2 text-sm shadow-sm", selectedTone.panel].join(" ")}>
            <div className="flex justify-end">
              <Badge variant="outline" className="shrink-0 border-border bg-background text-foreground shadow-sm">
                {selectedDetail.status}
              </Badge>
            </div>
            <dl className="mt-1.5 grid gap-1 text-xs">
              <div><dt className="text-muted-foreground">担当ライン</dt><dd className="font-medium text-foreground">{selectedRow?.label}</dd></div>
              <div><dt className="text-muted-foreground">期間</dt><dd className="font-medium text-foreground">{String(selectedItem.start)} - {String(selectedItem.end)}</dd></div>
              <div><dt className="text-muted-foreground">責任者</dt><dd className="font-medium text-foreground">{selectedDetail.owner}</dd></div>
              <div><dt className="text-muted-foreground">次の作業</dt><dd className="font-medium text-foreground">{selectedDetail.next}</dd></div>
            </dl>
          </div>
        </SheetContent>
      </Sheet>
    </div>
  );
}

使用コンポーネント