{"spec_id":"campbell-basic","library":"d3","language":"javascript","code":"// anyplot.ai\n// campbell-basic: Campbell Diagram\n// Library: d3 7.9.0 | JavaScript 22.23.2\n// Quality: 90/100 | Created: 2026-08-24\n\n//# anyplot-orientation: landscape\nconst t = window.ANYPLOT_TOKENS;\nconst { width, height } = window.ANYPLOT_SIZE;\nconst margin = { top: 90, right: 250, bottom: 90, left: 90 };\nconst iw = width - margin.left - margin.right;\nconst ih = height - margin.top - margin.bottom;\n\n// --- Data (in-memory, deterministic) ----------------------------------------\nconst maxSpeed = 6000; // RPM\nconst maxFreq = 180; // Hz — visible plot ceiling\nconst nPoints = 81;\nconst speeds = d3.range(nPoints).map((i) => (i * maxSpeed) / (nPoints - 1));\n\n// Natural frequency modes: base value at zero speed + linear drift from\n// gyroscopic stiffening/softening (some modes rise, some fall with speed).\nconst modes = [\n  { label: \"1st Bending\", base: 25, delta: 10 },\n  { label: \"2nd Bending\", base: 60, delta: -8 },\n  { label: \"1st Torsional\", base: 110, delta: 12 },\n  { label: \"Axial\", base: 155, delta: 5 },\n];\nconst modeCurves = modes.map((m, i) => ({\n  label: m.label,\n  color: t.palette[i],\n  points: speeds.map((s) => ({ speed: s, freq: m.base + (m.delta * s) / maxSpeed })),\n}));\n\n// Engine order excitation lines: frequency = order * (speed / 60 s/min).\nconst engineOrders = [1, 2, 3].map((order) => ({\n  order,\n  label: `${order}x`,\n  points: speeds.map((s) => ({ speed: s, freq: (order * s) / 60 })),\n}));\n\n// Critical speeds: numeric root-finding for mode/order-line intersections.\nfunction findIntersections(modeCurve, orderLine) {\n  const hits = [];\n  for (let i = 0; i < speeds.length - 1; i++) {\n    const d0 = modeCurve.points[i].freq - orderLine.points[i].freq;\n    const d1 = modeCurve.points[i + 1].freq - orderLine.points[i + 1].freq;\n    if (d0 === 0 || d0 * d1 < 0) {\n      const frac = d0 === 0 ? 0 : d0 / (d0 - d1);\n      const speed = speeds[i] + frac * (speeds[i + 1] - speeds[i]);\n      const freq = modeCurve.points[i].freq + frac * (modeCurve.points[i + 1].freq - modeCurve.points[i].freq);\n      if (freq <= maxFreq) hits.push({ speed, freq });\n    }\n  }\n  return hits;\n}\nconst criticalSpeeds = modeCurves.flatMap((mc) =>\n  engineOrders.flatMap((eo) => findIntersections(mc, eo).map((hit) => ({ ...hit, mode: mc.label, order: eo.label }))),\n);\n\n// --- Scales -------------------------------------------------------------------\nconst x = d3.scaleLinear().domain([0, maxSpeed]).range([0, iw]);\nconst y = d3.scaleLinear().domain([0, maxFreq]).range([ih, 0]);\nconst line = d3.line().x((d) => x(d.speed)).y((d) => y(d.freq));\n\n// --- SVG mount ------------------------------------------------------------\nconst svg = d3.select(\"#container\").append(\"svg\").attr(\"width\", width).attr(\"height\", height);\nconst g = svg.append(\"g\").attr(\"transform\", `translate(${margin.left},${margin.top})`);\n\nsvg.append(\"clipPath\").attr(\"id\", \"plot-area\").append(\"rect\").attr(\"width\", iw).attr(\"height\", ih);\nconst plot = g.append(\"g\").attr(\"clip-path\", \"url(#plot-area)\");\n\n// --- Y grid (subtle, per style guide) --------------------------------------\nplot\n  .append(\"g\")\n  .selectAll(\"line\")\n  .data(y.ticks(6))\n  .join(\"line\")\n  .attr(\"x1\", 0)\n  .attr(\"x2\", iw)\n  .attr(\"y1\", (d) => y(d))\n  .attr(\"y2\", (d) => y(d))\n  .attr(\"stroke\", t.grid)\n  .attr(\"stroke-width\", 1);\n\n// --- Engine order excitation lines (dashed, neutral reference lines) -------\nplot\n  .selectAll(\".order-line\")\n  .data(engineOrders)\n  .join(\"path\")\n  .attr(\"class\", \"order-line\")\n  .attr(\"d\", (d) => line(d.points))\n  .attr(\"fill\", \"none\")\n  .attr(\"stroke\", t.inkSoft)\n  .attr(\"stroke-width\", 2)\n  .attr(\"stroke-dasharray\", \"8,6\");\n\n// End labels for the engine order lines, placed where each line exits the\n// visible frame (right edge if it stays under maxFreq, top edge otherwise).\nconst orderExits = engineOrders.map((eo) => {\n  const freqAt6000 = (eo.order * maxSpeed) / 60;\n  return freqAt6000 <= maxFreq\n    ? { ...eo, speed: maxSpeed, freq: freqAt6000, anchor: \"start\", dx: 8, dy: 4 }\n    : { ...eo, speed: (maxFreq * 60) / eo.order, freq: maxFreq, anchor: \"middle\", dx: 0, dy: -10 };\n});\ng.selectAll(\".order-label\")\n  .data(orderExits)\n  .join(\"text\")\n  .attr(\"class\", \"order-label\")\n  .attr(\"x\", (d) => x(d.speed) + d.dx)\n  .attr(\"y\", (d) => y(d.freq) + d.dy)\n  .attr(\"text-anchor\", (d) => d.anchor)\n  .attr(\"fill\", t.inkSoft)\n  .style(\"font-size\", \"14px\")\n  .style(\"font-weight\", \"600\")\n  .text((d) => d.label);\n\n// --- Natural frequency curves ------------------------------------------------\nplot\n  .selectAll(\".mode-line\")\n  .data(modeCurves)\n  .join(\"path\")\n  .attr(\"class\", \"mode-line\")\n  .attr(\"d\", (d) => line(d.points))\n  .attr(\"fill\", \"none\")\n  .attr(\"stroke\", (d) => d.color)\n  .attr(\"stroke-width\", 3.5);\n\n// --- Critical speed markers (deferred semantic-red anchor, per style guide) --\nconst diamond = d3.symbol().type(d3.symbolDiamond).size(260);\nplot\n  .selectAll(\".critical-marker\")\n  .data(criticalSpeeds)\n  .join(\"path\")\n  .attr(\"class\", \"critical-marker\")\n  .attr(\"d\", diamond)\n  .attr(\"transform\", (d) => `translate(${x(d.speed)},${y(d.freq)})`)\n  .attr(\"fill\", t.palette[4])\n  .attr(\"stroke\", t.pageBg)\n  .attr(\"stroke-width\", 1.5);\n\n// --- Axes -----------------------------------------------------------------\nconst xAxis = g.append(\"g\").attr(\"transform\", `translate(0,${ih})`).call(d3.axisBottom(x).ticks(6));\nconst yAxis = g.append(\"g\").call(d3.axisLeft(y).ticks(6));\nfor (const axis of [xAxis, yAxis]) {\n  axis.selectAll(\"text\").attr(\"fill\", t.inkSoft).style(\"font-size\", \"14px\");\n  axis.selectAll(\"line\").attr(\"stroke\", t.inkSoft);\n  axis.select(\".domain\").attr(\"stroke\", t.inkSoft);\n}\n\ng.append(\"text\")\n  .attr(\"x\", iw / 2)\n  .attr(\"y\", ih + 60)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"16px\")\n  .text(\"Rotational Speed (RPM)\");\n\ng.append(\"text\")\n  .attr(\"transform\", \"rotate(-90)\")\n  .attr(\"x\", -ih / 2)\n  .attr(\"y\", -60)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"16px\")\n  .text(\"Natural Frequency (Hz)\");\n\n// --- Title ------------------------------------------------------------------\nsvg\n  .append(\"text\")\n  .attr(\"x\", width / 2)\n  .attr(\"y\", 44)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"22px\")\n  .style(\"font-weight\", \"600\")\n  .text(\"campbell-basic · javascript · d3 · anyplot.ai\");\n\n// --- Legend -----------------------------------------------------------------\nconst legendItems = [\n  ...modeCurves.map((d) => ({ label: d.label, kind: \"line\", color: d.color, dash: null })),\n  { label: \"Engine order excitation\", kind: \"line\", color: t.inkSoft, dash: \"8,6\" },\n  { label: \"Critical speed\", kind: \"marker\", color: t.palette[4] },\n];\nconst legend = g.append(\"g\").attr(\"transform\", `translate(${iw + 40}, 0)`);\nconst legendRows = legend\n  .selectAll(\".legend-row\")\n  .data(legendItems)\n  .join(\"g\")\n  .attr(\"class\", \"legend-row\")\n  .attr(\"transform\", (_, i) => `translate(0, ${i * 34})`);\n\nlegendRows\n  .filter((d) => d.kind === \"line\")\n  .append(\"line\")\n  .attr(\"x1\", 0)\n  .attr(\"x2\", 28)\n  .attr(\"y1\", 0)\n  .attr(\"y2\", 0)\n  .attr(\"stroke\", (d) => d.color)\n  .attr(\"stroke-width\", 3.5)\n  .attr(\"stroke-dasharray\", (d) => d.dash);\n\nlegendRows\n  .filter((d) => d.kind === \"marker\")\n  .append(\"path\")\n  .attr(\"d\", d3.symbol().type(d3.symbolDiamond).size(220))\n  .attr(\"transform\", \"translate(14,0)\")\n  .attr(\"fill\", (d) => d.color)\n  .attr(\"stroke\", t.pageBg)\n  .attr(\"stroke-width\", 1.5);\n\nlegendRows\n  .append(\"text\")\n  .attr(\"x\", 38)\n  .attr(\"y\", 5)\n  .attr(\"fill\", t.inkSoft)\n  .style(\"font-size\", \"14px\")\n  .text((d) => d.label);\n"}