{"spec_id":"campbell-basic","library":"highcharts","language":"javascript","code":"// anyplot.ai\n// campbell-basic: Campbell Diagram\n// Library: highcharts 12.6.0 | JavaScript 22.23.2\n// Quality: 89/100 | Created: 2026-08-24\n\nconst t = window.ANYPLOT_TOKENS;\n\n// --- Data (in-memory, deterministic) ----------------------------------------\n// Natural frequency modes: base Hz at zero speed, Hz at max speed (linear\n// gyroscopic trend — forward whirl stiffens, backward whirl softens, higher\n// modes drift only slightly).\nconst MAX_SPEED = 6000; // RPM\nconst POINTS = 61; // 100 RPM steps\n\nconst modes = [\n  { name: \"1st Bending (Forward)\", baseHz: 20, endHz: 27 },\n  { name: \"1st Bending (Backward)\", baseHz: 20, endHz: 14 },\n  { name: \"2nd Bending\", baseHz: 52, endHz: 57 },\n  { name: \"1st Torsional\", baseHz: 88, endHz: 90 },\n];\n\nconst modeFreq = (mode, speed) =>\n  mode.baseHz + (mode.endHz - mode.baseHz) * (speed / MAX_SPEED);\n\nconst modeSeries = modes.map((mode) => ({\n  name: mode.name,\n  type: \"line\",\n  data: Array.from({ length: POINTS }, (_, i) => {\n    const speed = (i * MAX_SPEED) / (POINTS - 1);\n    return [speed, modeFreq(mode, speed)];\n  }),\n  lineWidth: 3,\n  marker: { enabled: false },\n}));\n\n// Engine order excitation lines: frequency = order * speed / 60 (RPM -> Hz)\nconst orders = [1, 2, 3];\nconst orderFreq = (order, speed) => (order * speed) / 60;\n\nconst orderSeries = orders.map((order) => ({\n  name: `${order}x order`,\n  type: \"line\",\n  data: [\n    [0, 0],\n    [MAX_SPEED, orderFreq(order, MAX_SPEED)],\n  ],\n  color: t.ink,\n  dashStyle: \"Dash\",\n  lineWidth: 2,\n  marker: { enabled: false },\n  enableMouseTracking: false,\n}));\n\n// Critical speeds: where an engine order line crosses a natural frequency\n// curve. Found numerically via sign change of (orderFreq - modeFreq) across\n// a fine RPM sweep, then refined with linear interpolation.\nconst criticalSpeeds = [];\nconst SCAN_STEP = 5; // RPM\n\norders.forEach((order) => {\n  modes.forEach((mode) => {\n    let prevSpeed = 0;\n    let prevDiff = orderFreq(order, 0) - modeFreq(mode, 0);\n    for (let speed = SCAN_STEP; speed <= MAX_SPEED; speed += SCAN_STEP) {\n      const diff = orderFreq(order, speed) - modeFreq(mode, speed);\n      if (prevDiff === 0 || prevDiff * diff < 0) {\n        const crossSpeed =\n          prevSpeed + ((speed - prevSpeed) * -prevDiff) / (diff - prevDiff);\n        criticalSpeeds.push([crossSpeed, orderFreq(order, crossSpeed)]);\n      }\n      prevSpeed = speed;\n      prevDiff = diff;\n    }\n  });\n});\n\nconst criticalSeries = {\n  name: \"Critical Speeds\",\n  type: \"scatter\",\n  data: criticalSpeeds,\n  color: t.palette[4], // matte red — semantic anchor for critical/error\n  marker: {\n    enabled: true,\n    symbol: \"diamond\",\n    radius: 5,\n    lineColor: t.pageBg,\n    lineWidth: 1,\n  },\n};\n\n// Group crossings that sit close together in both RPM and Hz (visually\n// overlapping diamonds) into a shaded \"critical zone\" — a distinctive\n// Highcharts plotBands touch that also satisfies the spec's optional\n// zone-highlighting suggestion.\nconst sortedCritical = [...criticalSpeeds].sort((a, b) => a[0] - b[0]);\nconst ZONE_RPM_TOL = 60;\nconst ZONE_HZ_TOL = 3;\nconst clusters = [];\nsortedCritical.forEach(([speed, freq]) => {\n  const last = clusters[clusters.length - 1];\n  const [lastSpeed, lastFreq] = last ? last[last.length - 1] : [];\n  if (last && speed - lastSpeed < ZONE_RPM_TOL && Math.abs(freq - lastFreq) < ZONE_HZ_TOL) {\n    last.push([speed, freq]);\n  } else {\n    clusters.push([[speed, freq]]);\n  }\n});\n\nconst ZONE_MARGIN = 30; // RPM padding around a cluster's plot band\nconst criticalZones = clusters\n  .filter((cluster) => cluster.length > 1)\n  .map((cluster) => {\n    const speeds = cluster.map(([speed]) => speed);\n    return {\n      from: Math.min(...speeds) - ZONE_MARGIN,\n      to: Math.max(...speeds) + ZONE_MARGIN,\n      color: \"rgba(174, 48, 48, 0.08)\", // matte red at low alpha\n      zIndex: 0,\n    };\n  });\n\n// --- Chart -------------------------------------------------------------------\nconst FONT_FAMILY = '\"Helvetica Neue\", Arial, sans-serif';\n\nHighcharts.chart(\"container\", {\n  chart: {\n    type: \"line\",\n    backgroundColor: \"transparent\",\n    animation: false,\n    style: { fontFamily: FONT_FAMILY },\n  },\n  credits: { enabled: false },\n  colors: t.palette,\n  title: {\n    text: \"campbell-basic · javascript · highcharts · anyplot.ai\",\n    style: { color: t.ink, fontSize: \"22px\", fontWeight: \"600\" },\n  },\n  xAxis: {\n    title: {\n      text: \"Rotational Speed (RPM)\",\n      style: { color: t.inkSoft, fontSize: \"16px\" },\n    },\n    min: 0,\n    max: MAX_SPEED,\n    lineColor: t.inkSoft,\n    tickColor: t.inkSoft,\n    gridLineColor: t.grid,\n    labels: { style: { color: t.inkSoft, fontSize: \"14px\" } },\n    plotBands: criticalZones,\n  },\n  yAxis: {\n    title: {\n      text: \"Natural Frequency (Hz)\",\n      style: { color: t.inkSoft, fontSize: \"16px\" },\n    },\n    min: 0,\n    max: 100,\n    gridLineColor: t.grid,\n    labels: { style: { color: t.inkSoft, fontSize: \"14px\" } },\n  },\n  legend: {\n    itemStyle: { color: t.inkSoft, fontSize: \"14px\" },\n    itemHoverStyle: { color: t.ink },\n  },\n  tooltip: { enabled: false },\n  plotOptions: {\n    series: { animation: false },\n  },\n  series: [...modeSeries, ...orderSeries, criticalSeries],\n});\n"}