{"spec_id":"network-transport-static","library":"muix","language":"javascript","code":"// anyplot.ai\n// network-transport-static: Static Transport Network Diagram\n// Library: muix 7.29.1 | JavaScript 22.23.2\n// Quality: 87/100 | Created: 2026-09-02\n//# anyplot-orientation: square\n// anyplot.ai\n// network-transport-static: Static Transport Network Diagram\n// Library: MUI X Charts | React | Node 22\n// License: @mui/x-charts — MIT (community). Pro/Premium are out of scope.\n// Quality: pending | Created: 2026-09-02\n\nimport { ChartContainer } from \"@mui/x-charts/ChartContainer\";\nimport { useXScale, useYScale } from \"@mui/x-charts/hooks\";\n\nconst t = window.ANYPLOT_TOKENS;\nconst FONT = \"Inter, system-ui, -apple-system, sans-serif\";\n\n// --- Data: a regional commuter-rail network — four branches radiating from a\n// Central interchange, plus a limited-stop express to the coast. --------------\nconst STATIONS = [\n  { id: \"CTR\", label: \"Central\", x: 0, y: 0 },\n  { id: \"NJC\", label: \"North Jct.\", x: 0, y: 2.4 },\n  { id: \"UPT\", label: \"Uptown\", x: 0.9, y: 4.6 },\n  { id: \"EPT\", label: \"Eastport\", x: 2.8, y: 0 },\n  { id: \"HBS\", label: \"Harborside\", x: 5.6, y: 0 },\n  { id: \"WFD\", label: \"Westfield\", x: -2.8, y: 0 },\n  { id: \"WBK\", label: \"Westbrook\", x: -5.4, y: 0.9 },\n  { id: \"SGT\", label: \"Southgate\", x: 0, y: -2.6 },\n  { id: \"SPK\", label: \"Southpark\", x: 0.7, y: -5.0 },\n];\n\nconst TYPES = [\"Regional\", \"Local\", \"Express\"];\nconst TYPE_COLOR = {\n  Regional: t.palette[0],\n  Local: t.palette[1],\n  Express: t.palette[2],\n};\n\n// Trunk segments (touching the Central interchange) run both directions.\n// Branch-tip segments run both directions too. The Harborside express skips\n// Eastport entirely, so it is drawn as a bowed arc rather than a straight edge.\nconst ROUTES = [\n  { source_id: \"CTR\", target_id: \"NJC\", route_id: \"R10\", departure_time: \"07:05\", arrival_time: \"07:12\", type: \"Regional\" },\n  { source_id: \"NJC\", target_id: \"CTR\", route_id: \"R11\", departure_time: \"07:20\", arrival_time: \"07:27\", type: \"Regional\" },\n  { source_id: \"CTR\", target_id: \"EPT\", route_id: \"R20\", departure_time: \"07:00\", arrival_time: \"07:14\", type: \"Regional\" },\n  { source_id: \"EPT\", target_id: \"CTR\", route_id: \"R21\", departure_time: \"07:20\", arrival_time: \"07:34\", type: \"Regional\" },\n  { source_id: \"CTR\", target_id: \"WFD\", route_id: \"R30\", departure_time: \"07:05\", arrival_time: \"07:17\", type: \"Regional\" },\n  { source_id: \"WFD\", target_id: \"CTR\", route_id: \"R31\", departure_time: \"07:25\", arrival_time: \"07:37\", type: \"Regional\" },\n  { source_id: \"CTR\", target_id: \"SGT\", route_id: \"R40\", departure_time: \"07:10\", arrival_time: \"07:19\", type: \"Regional\" },\n  { source_id: \"SGT\", target_id: \"CTR\", route_id: \"R41\", departure_time: \"07:25\", arrival_time: \"07:34\", type: \"Regional\" },\n  { source_id: \"NJC\", target_id: \"UPT\", route_id: \"L12\", departure_time: \"07:15\", arrival_time: \"07:26\", type: \"Local\" },\n  { source_id: \"UPT\", target_id: \"NJC\", route_id: \"L13\", departure_time: \"07:35\", arrival_time: \"07:46\", type: \"Local\" },\n  { source_id: \"EPT\", target_id: \"HBS\", route_id: \"L22\", departure_time: \"07:18\", arrival_time: \"07:33\", type: \"Local\" },\n  { source_id: \"HBS\", target_id: \"EPT\", route_id: \"L23\", departure_time: \"07:40\", arrival_time: \"07:55\", type: \"Local\" },\n  { source_id: \"WFD\", target_id: \"WBK\", route_id: \"L32\", departure_time: \"07:20\", arrival_time: \"07:34\", type: \"Local\" },\n  { source_id: \"WBK\", target_id: \"WFD\", route_id: \"L33\", departure_time: \"07:42\", arrival_time: \"07:56\", type: \"Local\" },\n  { source_id: \"SGT\", target_id: \"SPK\", route_id: \"L42\", departure_time: \"07:22\", arrival_time: \"07:38\", type: \"Local\" },\n  { source_id: \"SPK\", target_id: \"SGT\", route_id: \"L43\", departure_time: \"07:45\", arrival_time: \"08:01\", type: \"Local\" },\n  { source_id: \"CTR\", target_id: \"HBS\", route_id: \"EX1\", departure_time: \"07:00\", arrival_time: \"07:28\", type: \"Express\" },\n  { source_id: \"HBS\", target_id: \"CTR\", route_id: \"EX2\", departure_time: \"07:35\", arrival_time: \"08:03\", type: \"Express\" },\n];\n\nconst DEGREE = {};\nSTATIONS.forEach((s) => (DEGREE[s.id] = 0));\nROUTES.forEach((r) => {\n  DEGREE[r.source_id] += 1;\n  DEGREE[r.target_id] += 1;\n});\n\nfunction nodeRadius(id) {\n  // The Central interchange is drawn larger, with its full name inside the\n  // circle instead of an external label — see Stations().\n  if (id === \"CTR\") return 46;\n  return 13 + DEGREE[id] * 1.4;\n}\n\n// --- Square-friendly domain: bounding box of station coordinates, padded ----\nconst xs = STATIONS.map((s) => s.x);\nconst ys = STATIONS.map((s) => s.y);\nconst minX = Math.min(...xs);\nconst maxX = Math.max(...xs);\nconst minY = Math.min(...ys);\nconst maxY = Math.max(...ys);\nconst centerX = (minX + maxX) / 2;\nconst centerY = (minY + maxY) / 2;\nconst halfSpan = (Math.max(maxX - minX, maxY - minY) / 2) * 1.3;\nconst X_DOMAIN = [centerX - halfSpan, centerX + halfSpan];\nconst Y_DOMAIN = [centerY - halfSpan, centerY + halfSpan];\n\nconst stationById = Object.fromEntries(STATIONS.map((s) => [s.id, s]));\n\n// --- Custom marks — drawn on the MUI X coordinate system --------------------\n// Directed edges as quadratic-bezier curves: opposite-direction services on\n// the same corridor bow to opposite sides (parallel offset), and the\n// skip-stop express bows well clear of the Eastport node it passes over.\nfunction RouteEdges() {\n  const xScale = useXScale();\n  const yScale = useYScale();\n\n  return (\n    <g fontFamily={FONT}>\n      <defs>\n        {TYPES.map((type) => (\n          <marker\n            key={type}\n            id={`arrow-${type}`}\n            markerWidth=\"9\"\n            markerHeight=\"9\"\n            refX=\"6.5\"\n            refY=\"3\"\n            orient=\"auto\"\n          >\n            <path d=\"M0,0 L7,3 L0,6 Z\" fill={TYPE_COLOR[type]} />\n          </marker>\n        ))}\n      </defs>\n      {ROUTES.map((route) => {\n        const source = stationById[route.source_id];\n        const target = stationById[route.target_id];\n        const sx = xScale(source.x);\n        const sy = yScale(source.y);\n        const tx = xScale(target.x);\n        const ty = yScale(target.y);\n        const dx = tx - sx;\n        const dy = ty - sy;\n        const length = Math.hypot(dx, dy) || 1;\n\n        // Perpendicular is derived from a *canonical* (sorted) pair direction,\n        // not the route's own source→target direction — otherwise reversing\n        // the route also flips the perpendicular, cancelling out the side\n        // assignment below and stacking both directions on the same side.\n        const [aId, bId] = route.source_id < route.target_id ? [route.source_id, route.target_id] : [route.target_id, route.source_id];\n        const aPoint = { x: xScale(stationById[aId].x), y: yScale(stationById[aId].y) };\n        const bPoint = { x: xScale(stationById[bId].x), y: yScale(stationById[bId].y) };\n        const canonLength = Math.hypot(bPoint.x - aPoint.x, bPoint.y - aPoint.y) || 1;\n        const perpX = -(bPoint.y - aPoint.y) / canonLength;\n        const perpY = (bPoint.x - aPoint.x) / canonLength;\n        const sign = route.source_id === aId ? -1 : 1;\n\n        const isExpress = route.type === \"Express\";\n        const offset = isExpress ? length * 0.22 : 17;\n        const midX = (sx + tx) / 2 + perpX * offset * sign;\n        const midY = (sy + ty) / 2 + perpY * offset * sign;\n\n        // Pull the endpoints back to the node rims so arrowheads land on the\n        // circle edge instead of under the fill.\n        const sourceR = nodeRadius(route.source_id);\n        const targetR = nodeRadius(route.target_id);\n        const startX = sx + (dx / length) * (sourceR + 2);\n        const startY = sy + (dy / length) * (sourceR + 2);\n        const endX = tx - (dx / length) * (targetR + 5);\n        const endY = ty - (dy / length) * (targetR + 5);\n\n        const d = `M ${startX} ${startY} Q ${midX} ${midY} ${endX} ${endY}`;\n\n        // Label sits ~60% along the curve, nudged further out — this keeps\n        // it beside its own line, not on top of it.\n        const lt = 0.62;\n        const labelX =\n          (1 - lt) ** 2 * sx + 2 * (1 - lt) * lt * midX + lt ** 2 * tx + perpX * 24 * sign;\n        const labelY =\n          (1 - lt) ** 2 * sy + 2 * (1 - lt) * lt * midY + lt ** 2 * ty + perpY * 24 * sign;\n        const text = `${route.route_id} ${route.departure_time}→${route.arrival_time}`;\n        const boxW = text.length * 7.3 + 10;\n        const tooltip = `${route.route_id}: ${source.label} → ${target.label} (${route.departure_time} → ${route.arrival_time}, ${route.type})`;\n\n        return (\n          <g key={route.route_id}>\n            <path\n              d={d}\n              fill=\"none\"\n              stroke={TYPE_COLOR[route.type]}\n              strokeWidth={isExpress ? 2.5 : 2}\n              strokeDasharray={isExpress ? \"9 6\" : undefined}\n              markerEnd={`url(#arrow-${route.type})`}\n            >\n              <title>{tooltip}</title>\n            </path>\n            <rect\n              x={labelX - boxW / 2}\n              y={labelY - 11}\n              width={boxW}\n              height={20}\n              rx={3}\n              fill={t.elevatedBg}\n              opacity={0.94}\n            >\n              <title>{tooltip}</title>\n            </rect>\n            <text x={labelX} y={labelY} fontSize={14} fill={t.ink} textAnchor=\"middle\" dominantBaseline=\"middle\">\n              {text}\n            </text>\n          </g>\n        );\n      })}\n    </g>\n  );\n}\n\n// Each station's neighbors, deduplicated across both route directions — used\n// to find the widest angular gap for label placement below.\nconst NEIGHBORS_BY_STATION = Object.fromEntries(\n  STATIONS.map((station) => {\n    const ids = new Set();\n    ROUTES.forEach((route) => {\n      if (route.source_id === station.id) ids.add(route.target_id);\n      if (route.target_id === station.id) ids.add(route.source_id);\n    });\n    return [station.id, Array.from(ids)];\n  }),\n);\n\nfunction Stations() {\n  const xScale = useXScale();\n  const yScale = useYScale();\n\n  return (\n    <g fontFamily={FONT}>\n      {STATIONS.map((station) => {\n        const isHub = station.id === \"CTR\";\n        const cx = xScale(station.x);\n        const cy = yScale(station.y);\n        const r = nodeRadius(station.id);\n        const fill = isHub ? t.ink : t.elevatedBg;\n        const codeColor = isHub ? t.pageBg : t.ink;\n\n        // The hub has 4 branches converging, so every direction around it is\n        // already claimed by a route label — instead of fighting for an\n        // external spot, the interchange is drawn larger with its full name\n        // set inside the circle, the way transit maps mark major interchanges.\n        if (isHub) {\n          return (\n            <g key={station.id}>\n              <circle cx={cx} cy={cy} r={r} fill={fill} stroke={t.ink} strokeWidth={2}>\n                <title>{`${station.label} — major interchange, ${DEGREE[station.id]} connecting routes`}</title>\n              </circle>\n              <text x={cx} y={cy} fontSize={19} fontWeight={600} fill={codeColor} textAnchor=\"middle\" dominantBaseline=\"middle\">\n                {station.label}\n              </text>\n            </g>\n          );\n        }\n\n        // Place the label in the widest angular gap between this station's\n        // incident edges, so it never sits on top of a route line or label.\n        const edgeAngles = NEIGHBORS_BY_STATION[station.id].map((nid) => {\n          const npos = { x: xScale(stationById[nid].x), y: yScale(stationById[nid].y) };\n          return Math.atan2(npos.y - cy, npos.x - cx);\n        });\n        let labelAngle = Math.PI / 2;\n        if (edgeAngles.length > 0) {\n          edgeAngles.sort((a, b) => a - b);\n          let bestGap = -1;\n          for (let k = 0; k < edgeAngles.length; k += 1) {\n            const start = edgeAngles[k];\n            const end = edgeAngles[(k + 1) % edgeAngles.length] + (k === edgeAngles.length - 1 ? 2 * Math.PI : 0);\n            const gap = end - start;\n            if (gap > bestGap) {\n              bestGap = gap;\n              labelAngle = start + gap / 2;\n            }\n          }\n        }\n        // Anchor the label away from the node along the gap direction — a\n        // fixed \"middle\" anchor would let long names like \"Harborside\" swing\n        // back over the node when the gap points nearly horizontal.\n        const cosA = Math.cos(labelAngle);\n        const textAnchor = cosA > 0.35 ? \"start\" : cosA < -0.35 ? \"end\" : \"middle\";\n        const labelDist = r + (textAnchor === \"middle\" ? 20 : 13);\n        const lx = cx + Math.cos(labelAngle) * labelDist;\n        const ly = cy + Math.sin(labelAngle) * labelDist;\n\n        return (\n          <g key={station.id}>\n            <circle cx={cx} cy={cy} r={r} fill={fill} stroke={t.ink} strokeWidth={2}>\n              <title>{`${station.label} (${station.id}) — ${DEGREE[station.id]} connecting route${DEGREE[station.id] === 1 ? \"\" : \"s\"}`}</title>\n            </circle>\n            <text x={cx} y={cy} fontSize={Math.max(10, r * 0.55)} fontWeight={600} fill={codeColor} textAnchor=\"middle\" dominantBaseline=\"middle\">\n              {station.id}\n            </text>\n            <text x={lx} y={ly} fontSize={15} fontWeight={400} fill={t.ink} textAnchor={textAnchor} dominantBaseline=\"middle\">\n              {station.label}\n            </text>\n          </g>\n        );\n      })}\n    </g>\n  );\n}\n\nfunction Legend() {\n  return (\n    <g transform=\"translate(28, 26)\" fontFamily={FONT}>\n      {TYPES.map((type, i) => (\n        <g key={type} transform={`translate(0, ${i * 28})`}>\n          <line x1={0} y1={8} x2={26} y2={8} stroke={TYPE_COLOR[type]} strokeWidth={3} strokeDasharray={type === \"Express\" ? \"9 6\" : undefined} />\n          <text x={34} y={13} fontSize={14} fill={t.ink}>\n            {type}\n          </text>\n        </g>\n      ))}\n    </g>\n  );\n}\n\nconst TITLE_H = 62;\nconst CAPTION_H = 30;\n\n// --- Chart (default-exported component — the harness mounts it) -------------\nexport default function Chart() {\n  const W = window.ANYPLOT_SIZE.width;\n  const H = window.ANYPLOT_SIZE.height;\n  const chartH = H - TITLE_H - CAPTION_H;\n\n  return (\n    <div\n      style={{\n        width: W,\n        height: H,\n        background: t.pageBg,\n        fontFamily: FONT,\n        display: \"flex\",\n        flexDirection: \"column\",\n      }}\n    >\n      <div style={{ height: TITLE_H, display: \"flex\", alignItems: \"center\", justifyContent: \"center\" }}>\n        <span style={{ fontSize: 22, fontWeight: 600, color: t.ink }}>\n          network-transport-static · javascript · muix · anyplot.ai\n        </span>\n      </div>\n      <ChartContainer\n        width={W}\n        height={chartH}\n        skipAnimation\n        series={[]}\n        margin={{ top: 20, right: 30, bottom: 20, left: 30 }}\n        xAxis={[{ min: X_DOMAIN[0], max: X_DOMAIN[1], scaleType: \"linear\" }]}\n        yAxis={[{ min: Y_DOMAIN[0], max: Y_DOMAIN[1], scaleType: \"linear\" }]}\n      >\n        <RouteEdges />\n        <Stations />\n        <Legend />\n      </ChartContainer>\n      <div style={{ height: CAPTION_H, display: \"flex\", alignItems: \"center\", justifyContent: \"center\" }}>\n        <span style={{ fontSize: 13, color: t.inkSoft }}>\n          Arrows show travel direction · labels show route ID and scheduled departure {\"→\"} arrival · larger circle marks the Central interchange\n        </span>\n      </div>\n    </div>\n  );\n}\n"}