{"spec_id":"campbell-basic","library":"muix","language":"javascript","code":"// anyplot.ai\n// campbell-basic: Campbell Diagram\n// Library: muix 7.29.1 | JavaScript 22.23.2\n// Quality: 94/100 | Created: 2026-08-24\nimport { ChartContainer } from \"@mui/x-charts/ChartContainer\";\nimport { LinePlot } from \"@mui/x-charts/LineChart\";\nimport { ScatterPlot } from \"@mui/x-charts/ScatterChart\";\nimport { ChartsXAxis } from \"@mui/x-charts/ChartsXAxis\";\nimport { ChartsYAxis } from \"@mui/x-charts/ChartsYAxis\";\nimport { ChartsGrid } from \"@mui/x-charts/ChartsGrid\";\nimport { ChartsLegend } from \"@mui/x-charts/ChartsLegend\";\n\nconst t = window.ANYPLOT_TOKENS;\nconst { width, height } = window.ANYPLOT_SIZE;\nconst TITLE = \"campbell-basic · javascript · muix · anyplot.ai\";\n\n// --- Data (in-memory, deterministic): turbocompressor rotor train -----------\nconst SPEED_STEP = 100;\nconst SPEED_MAX = 6000;\nconst speeds = Array.from(\n  { length: SPEED_MAX / SPEED_STEP + 1 },\n  (_, i) => i * SPEED_STEP,\n);\n\n// Natural frequency curves (Hz): gyroscopic effects stiffen forward-whirl\n// modes and soften backward-whirl modes as rotational speed rises.\nconst modes = [\n  { id: \"bend1\", label: \"1st Bending\", freq: (s) => 42 + 0.0027 * s },\n  { id: \"bend2\", label: \"2nd Bending\", freq: (s) => 125 - 0.0025 * s },\n  { id: \"tors1\", label: \"1st Torsional\", freq: (s) => 88 + 0.0008 * s },\n  { id: \"axial\", label: \"Axial\", freq: (s) => 152 - 0.0012 * s },\n];\nconst modeCurves = modes.map((m) => ({ ...m, data: speeds.map(m.freq) }));\n\n// Engine order excitation lines: frequency (Hz) = order x speed (RPM) / 60.\nconst orders = [\n  { id: \"order1\", order: 1, label: \"1x\", dash: \"12 6\", opacity: 0.85 },\n  { id: \"order2\", order: 2, label: \"2x\", dash: \"6 4\", opacity: 0.6 },\n  { id: \"order3\", order: 3, label: \"3x\", dash: \"2 3\", opacity: 0.4 },\n];\nconst orderLines = orders.map((o) => ({\n  ...o,\n  data: speeds.map((s) => (o.order * s) / 60),\n}));\n\n// Critical speeds: RPM where an engine-order line crosses a natural-frequency\n// curve, located by linear interpolation across sign changes on the shared\n// speed grid (both families are sampled on the same `speeds` array).\nconst criticalSpeeds = [];\nmodeCurves.forEach((mode) => {\n  orderLines.forEach((order) => {\n    for (let i = 0; i < speeds.length - 1; i += 1) {\n      const diffA = mode.data[i] - order.data[i];\n      const diffB = mode.data[i + 1] - order.data[i + 1];\n      if (diffA === 0 || diffA * diffB < 0) {\n        const frac = diffA / (diffA - diffB);\n        criticalSpeeds.push({\n          x: speeds[i] + frac * SPEED_STEP,\n          y: mode.data[i] + frac * (mode.data[i + 1] - mode.data[i]),\n          id: `${mode.id}-${order.id}`,\n        });\n      }\n    }\n  });\n});\n\nconst allFrequencies = [\n  ...modeCurves.flatMap((m) => m.data),\n  ...orderLines.flatMap((o) => o.data),\n];\nconst FREQ_MAX = Math.ceil(Math.max(...allFrequencies) / 20) * 20 + 20;\n\nexport default function Chart() {\n  return (\n    <ChartContainer\n      width={width}\n      height={height}\n      skipAnimation\n      margin={{ top: 70, right: 250, bottom: 90, left: 135 }}\n      sx={{\n        // Mode curves (the primary data) get a heavier stroke so they read as\n        // the \"real\" data; engine-order reference lines stay thin, dashed,\n        // and get a per-order opacity step so 1x/2x/3x read as a fading\n        // sequence in the plot body.\n        ...Object.fromEntries(\n          modeCurves.map((m) => [\n            `& .MuiLineElement-series-${m.id}`,\n            { strokeWidth: 3 },\n          ]),\n        ),\n        ...Object.fromEntries(\n          orders.map((o) => [\n            `& .MuiLineElement-series-${o.id}`,\n            { strokeDasharray: o.dash, opacity: o.opacity, strokeWidth: 1.5 },\n          ]),\n        ),\n        // The legend mark is a solid filled <rect> (no stroke, so\n        // strokeDasharray never reaches it) — mirror the same per-order\n        // opacity step there so 1x/2x/3x stay visually distinct in the\n        // legend, not just in the plot body.\n        ...Object.fromEntries(\n          orders.map((o) => [\n            `& .MuiChartsLegend-series-${o.id} .MuiChartsLegend-mark`,\n            { opacity: o.opacity },\n          ]),\n        ),\n      }}\n      series={[\n        ...modeCurves.map((m, i) => ({\n          type: \"line\",\n          id: m.id,\n          data: m.data,\n          label: m.label,\n          color: t.palette[i],\n          showMark: false,\n          curve: \"linear\",\n        })),\n        ...orderLines.map((o) => ({\n          type: \"line\",\n          id: o.id,\n          data: o.data,\n          label: o.label,\n          color: t.ink,\n          showMark: false,\n          curve: \"linear\",\n        })),\n        {\n          type: \"scatter\",\n          id: \"critical\",\n          data: criticalSpeeds,\n          label: \"Critical Speeds\",\n          color: t.palette[4],\n          markerSize: 13,\n        },\n      ]}\n      xAxis={[\n        {\n          data: speeds,\n          scaleType: \"linear\",\n          min: 0,\n          max: SPEED_MAX,\n          tickNumber: 7,\n          label: \"Rotational Speed (RPM)\",\n          labelStyle: { fontSize: 16, fill: t.ink },\n          tickLabelStyle: { fontSize: 14, fill: t.inkSoft },\n        },\n      ]}\n      yAxis={[\n        {\n          min: 0,\n          max: FREQ_MAX,\n          label: \"Natural Frequency (Hz)\",\n          labelStyle: { fontSize: 16, fill: t.ink },\n          // tickFontSize only widens the label's clearance from the tick\n          // text (see ChartsYAxis labelRefPoint offset) — the *rendered*\n          // tick size still comes from tickLabelStyle.fontSize below.\n          tickFontSize: 38,\n          tickLabelStyle: { fontSize: 14, fill: t.inkSoft },\n        },\n      ]}\n    >\n      <ChartsGrid horizontal />\n      <LinePlot />\n      <ScatterPlot />\n      <ChartsXAxis />\n      <ChartsYAxis />\n      <ChartsLegend\n        position={{ vertical: \"middle\", horizontal: \"right\" }}\n        direction=\"column\"\n        slotProps={{\n          legend: {\n            // Widened from 20px so the per-order opacity step (see sx above)\n            // reads as a clear light/medium/dark gradient across the three\n            // swatches instead of a barely-visible sliver.\n            itemMarkWidth: 36,\n            itemMarkHeight: 4,\n            markGap: 8,\n            itemGap: 16,\n            labelStyle: { fontSize: 14, fill: t.ink },\n          },\n        }}\n      />\n      <text\n        x={width / 2}\n        y={36}\n        textAnchor=\"middle\"\n        fontSize={22}\n        fontWeight={600}\n        fill={t.ink}\n      >\n        {TITLE}\n      </text>\n    </ChartContainer>\n  );\n}\n"}