運行図表StringlineExperimental

停車地点と時刻を結ぶ運行図表を表示し、計画と実績、上下方向、現在時刻を重ねて確認できます。

プレビュー

712T

下り+8分

新宿 12番線

新宿 7:12 → 八王子 8:14

計画
新宿 7:12 → 八王子 8:06
運行担当
中央線運行指令

状態とバリエーション

計画と実績

actual を渡すと計画線を破線、実績を実線で重ねます。

712T

下り+8分

新宿 12番線

新宿 7:12 → 八王子 8:14

計画
新宿 7:12 → 八王子 8:06
運行担当
中央線運行指令

選択中の運行

selectedRunId で1本を強調し、他を淡色化します。

712T

下り+8分

新宿 12番線

新宿 7:12 → 八王子 8:14

計画
新宿 7:12 → 八王子 8:06
運行担当
中央線運行指令

短い区間

停車地点や運行本数を絞ったプレビューにも使えます。

712T

下り+8分

新宿 12番線

新宿 7:12 → 八王子 8:14

計画
新宿 7:12 → 八王子 8:06
運行担当
中央線運行指令

プロパティ

表は横にスクロールできます
プロパティ初期値説明
stopsStringlineStop[]-距離軸の停車地点です。distance が縦位置を決めます。
runsStringlineRun[]-時刻と停車地点を結ぶ運行線です。
startTime / endTimenumber-時間軸の範囲です。既定単位は0時からの分です。
actualStringlineRunPoint[]-実績点です。計画は破線、実績は実線で重ねます。
nownumber-現在時刻の縦線です。SSR安全のため値を渡します。
runLabel / directionLabelsstring / object-選択可能な運行の読み上げ文言をローカライズします。
selectedRunIdstring | number-1本の運行を強調し、他を淡色化します。
heightnumber320プロット高さです。
formatTime(time: number) => string-軸ラベルの時刻を整形します(既定は0時からの分を HH:MM)。関数propのため Client Component からのみ渡すこと(Server Component から渡すと next build が落ちる)。ドメイン値のため serializable な代替は無く、RSC からは "use client" ラッパーで包む。(#338)

使い方

import * as React from "react";
import { Badge, Stringline, type StringlineRun, type StringlineStop } from "@gunjo/ui";

const stops: StringlineStop[] = [
  { id: "sjk", label: "新宿", distance: 0 },
  { id: "mtk", label: "三鷹", distance: 14.3 },
  { id: "ttc", label: "立川", distance: 27.2 },
  { id: "hco", label: "八王子", distance: 37.1 },
];
const runs: StringlineRun[] = [
  {
    id: "712T",
    label: "712T",
    direction: "down",
    tone: "primary",
    points: [
      { stopId: "sjk", time: 432 },
      { stopId: "mtk", time: 451 },
      { stopId: "ttc", time: 469 },
      { stopId: "hco", time: 486 },
    ],
    actual: [
      { stopId: "sjk", time: 432 },
      { stopId: "mtk", time: 455 },
      { stopId: "ttc", time: 477 },
      { stopId: "hco", time: 494 },
    ],
  },
  {
    id: "809M",
    label: "809M",
    direction: "up",
    tone: "info",
    points: [
      { stopId: "hco", time: 438 },
      { stopId: "ttc", time: 456 },
      { stopId: "mtk", time: 474 },
      { stopId: "sjk", time: 494 },
    ],
  },
  {
    id: "615A",
    label: "615A",
    direction: "down",
    tone: "warning",
    points: [
      { stopId: "sjk", time: 450 },
      { stopId: "mtk", time: 466 },
      { stopId: "ttc", time: 490 },
      { stopId: "hco", time: 510 },
    ],
  },
];

const details = {
  "712T": {
    direction: "下り",
    planned: "新宿 7:12 → 八王子 8:06",
    actual: "新宿 7:12 → 八王子 8:14",
    delay: "+8分",
    platform: "新宿 12番線",
    crew: "中央線運行指令",
  },
  "809M": {
    direction: "上り",
    planned: "八王子 7:18 → 新宿 8:14",
    actual: "計画通り",
    delay: "定時",
    platform: "八王子 3番線",
    crew: "中央線運行指令",
  },
  "615A": {
    direction: "下り",
    planned: "新宿 7:30 → 八王子 8:30",
    actual: "立川で調整中",
    delay: "+12分見込み",
    platform: "新宿 10番線",
    crew: "運転整理担当",
  },
};

export function TrainDiagram() {
  const [selectedRunId, setSelectedRunId] = React.useState("712T");
  const selectableRuns = runs.map((run) => ({ ...run, onSelect: () => setSelectedRunId(String(run.id)) }));
  const selectedRun = selectableRuns.find((run) => run.id === selectedRunId)!;
  const detail = details[selectedRunId as keyof typeof details];

  return (
    <div className="flex w-full max-w-4xl flex-col gap-3 rounded-lg border bg-card p-4">
      <Stringline
        stops={stops}
        runs={selectableRuns}
        startTime={420}
        endTime={520}
        now={462}
        tickInterval={20}
        height={300}
        selectedRunId={selectedRunId}
        ariaLabel="中央線の運行図表"
      />
      <section
        className="grid min-w-0 gap-3 rounded-md border bg-background p-3"
        aria-live="polite"
        aria-label="選択中運行の詳細"
      >
        <div className="min-w-0 space-y-1.5">
          <div className="flex flex-wrap items-center gap-2">
            <h3 className="text-sm font-semibold text-foreground">{selectedRun.label}</h3>
            <Badge variant="secondary">{detail.direction}</Badge>
            <Badge variant={detail.delay.includes("+") ? "destructive" : "secondary"}>{detail.delay}</Badge>
          </div>
          <p className="text-xs text-muted-foreground">{detail.platform}</p>
          <p className="text-sm text-foreground">{detail.actual}</p>
        </div>
        <dl className="grid min-w-0 gap-2 rounded-md bg-muted/40 p-3 text-xs">
          <div>
            <dt className="text-muted-foreground">計画</dt>
            <dd className="font-medium text-foreground">
              {detail.planned}
            </dd>
          </div>
          <div>
            <dt className="text-muted-foreground">運行担当</dt>
            <dd className="font-medium text-foreground">
              {detail.crew}
            </dd>
          </div>
        </dl>
      </section>
    </div>
  );
}

使用コンポーネント