{"spec_id":"nyquist-basic","library":"echarts","language":"javascript","code":"// anyplot.ai\n// nyquist-basic: Nyquist Plot for Control Systems\n// Library: echarts 5.5.1 | JavaScript 22.22.3\n// Quality: 90/100 | Created: 2026-06-17\n//# anyplot-orientation: square\n\nconst t = window.ANYPLOT_TOKENS;\n\n// --- Data (deterministic Nyquist curve for a 3rd-order system) ---------------\n// Transfer function: G(s) = 1 / (s(s+1)(s+2))\n// G(jw) = 1 / (jw * (jw+1) * (jw+2))\n// Denominator: D = jw*(1+jw)*(2+jw)\n//   jw*(1+jw) = -w^2 + jw\n//   (-w^2+jw)*(2+jw) = -3w^2 + j(2w - w^3)\n// G(jw) = 1/D => multiply by conjugate: Re = -3w^2 / |D|^2, Im = -(2w-w^3)/|D|^2\n\nfunction logspace(start, end, n) {\n  const result = [];\n  const logStart = Math.log10(start);\n  const logEnd = Math.log10(end);\n  for (let i = 0; i < n; i++) {\n    result.push(Math.pow(10, logStart + (i / (n - 1)) * (logEnd - logStart)));\n  }\n  return result;\n}\n\nconst freqs = logspace(0.01, 100, 500);\nconst realParts = [];\nconst imagParts = [];\n\nfor (let i = 0; i < freqs.length; i++) {\n  const w = freqs[i];\n  const denom = Math.pow(3 * w * w, 2) + Math.pow(2 * w - w * w * w, 2);\n  realParts.push((-3 * w * w) / denom);\n  imagParts.push(-(2 * w - w * w * w) / denom);\n}\n\nconst curveData = realParts.map((r, i) => [r, imagParts[i]]);\n\n// Unit circle (101 points)\nconst unitCircle = [];\nfor (let i = 0; i <= 100; i++) {\n  const angle = (i / 100) * 2 * Math.PI;\n  unitCircle.push([Math.cos(angle), Math.sin(angle)]);\n}\n\n// Frequency annotation points at key frequencies\nconst annotFreqs = [0.3, 0.5, 1.0, 2.0, 5.0];\nconst annotPoints = annotFreqs.map((fw) => {\n  let bestIdx = 0;\n  let bestDiff = Infinity;\n  for (let i = 0; i < freqs.length; i++) {\n    const diff = Math.abs(freqs[i] - fw);\n    if (diff < bestDiff) { bestDiff = diff; bestIdx = i; }\n  }\n  return { freq: fw, re: realParts[bestIdx], im: imagParts[bestIdx] };\n});\n\n// Arrow direction indices (evenly spaced along visible curve segment)\nconst arrowIndices = [60, 150, 250];\n\n// --- Init -------------------------------------------------------------------\nconst chart = echarts.init(document.getElementById(\"container\"));\n\n// --- Option -----------------------------------------------------------------\nchart.setOption({\n  animation: false,\n  color: t.palette,\n  backgroundColor: \"transparent\",\n\n  title: {\n    text: \"nyquist-basic · javascript · echarts · anyplot.ai\",\n    left: \"center\",\n    top: 20,\n    textStyle: { color: t.ink, fontSize: 22, fontWeight: \"bold\" }\n  },\n\n  legend: {\n    data: [\"Nyquist Curve\", \"Unit Circle\"],\n    top: 60,\n    left: \"center\",\n    textStyle: { color: t.ink, fontSize: 14 },\n    itemWidth: 22,\n    itemHeight: 3,\n  },\n\n  grid: { left: 130, right: 80, top: 110, bottom: 110 },\n\n  xAxis: {\n    type: \"value\",\n    name: \"Real\",\n    nameLocation: \"middle\",\n    nameGap: 45,\n    nameTextStyle: { color: t.ink, fontSize: 16, fontWeight: \"bold\" },\n    axisLabel: { color: t.inkSoft, fontSize: 13 },\n    axisLine: { show: false },\n    splitLine: { lineStyle: { color: t.grid } },\n    min: -2.0,\n    max: 1.5,\n  },\n\n  yAxis: {\n    type: \"value\",\n    name: \"Imaginary\",\n    nameLocation: \"middle\",\n    nameGap: 65,\n    nameTextStyle: { color: t.ink, fontSize: 16, fontWeight: \"bold\" },\n    axisLabel: { color: t.inkSoft, fontSize: 13 },\n    axisLine: { show: false },\n    splitLine: { lineStyle: { color: t.grid } },\n    min: -1.75,\n    max: 1.75,\n  },\n\n  tooltip: {\n    trigger: \"item\",\n    formatter: (params) => {\n      if (params.seriesName === \"Nyquist Curve\") {\n        return `Re: ${params.data[0].toFixed(4)}<br/>Im: ${params.data[1].toFixed(4)}`;\n      }\n      return \"\";\n    },\n    backgroundColor: t.elevatedBg,\n    borderColor: t.grid,\n    textStyle: { color: t.ink, fontSize: 13 }\n  },\n\n  series: [\n    // Main Nyquist curve\n    {\n      name: \"Nyquist Curve\",\n      type: \"line\",\n      data: curveData,\n      smooth: false,\n      symbol: \"none\",\n      lineStyle: { color: t.palette[0], width: 2.5 },\n      emphasis: { disabled: true },\n    },\n\n    // Unit circle reference\n    {\n      name: \"Unit Circle\",\n      type: \"line\",\n      data: unitCircle,\n      smooth: false,\n      symbol: \"none\",\n      lineStyle: { color: t.inkSoft, width: 1.5, type: \"dashed\", opacity: 0.55 },\n      emphasis: { disabled: true },\n    },\n\n    // Critical point (-1, 0) — red diamond marker with label\n    {\n      name: \"Critical Point\",\n      type: \"scatter\",\n      data: [[-1, 0]],\n      symbol: \"diamond\",\n      symbolSize: 18,\n      itemStyle: { color: t.palette[4], borderColor: t.palette[4], borderWidth: 2 },\n      label: {\n        show: true,\n        formatter: \"(-1, 0)\",\n        position: \"right\",\n        color: t.palette[4],\n        fontSize: 14,\n        fontWeight: \"bold\",\n        offset: [8, 0],\n      },\n      emphasis: { disabled: true },\n    },\n\n    // Frequency annotation markers with labels\n    {\n      name: \"Frequency Markers\",\n      type: \"scatter\",\n      data: annotPoints.map((pt, i) => ({\n        value: [pt.re, pt.im],\n        label: { position: i === 4 ? \"bottom\" : \"top\", offset: [0, i === 4 ? 4 : -4] },\n      })),\n      symbol: \"circle\",\n      symbolSize: 9,\n      itemStyle: { color: t.palette[2], borderColor: t.pageBg, borderWidth: 1.5 },\n      label: {\n        show: true,\n        formatter: (params) => `${annotPoints[params.dataIndex].freq} rad/s`,\n        color: t.inkSoft,\n        fontSize: 12,\n      },\n      emphasis: { disabled: true },\n    },\n\n    // Direction arrows showing increasing frequency\n    {\n      name: \"Direction\",\n      type: \"scatter\",\n      data: arrowIndices.map((idx) => curveData[idx]),\n      symbol: \"arrow\",\n      symbolSize: 13,\n      symbolRotate: (value, params) => {\n        const idx = arrowIndices[params.dataIndex];\n        const dx = realParts[Math.min(idx + 2, freqs.length - 1)] - realParts[Math.max(idx - 2, 0)];\n        const dy = imagParts[Math.min(idx + 2, freqs.length - 1)] - imagParts[Math.max(idx - 2, 0)];\n        return (Math.atan2(dy, dx) * 180) / Math.PI;\n      },\n      itemStyle: { color: t.palette[0], opacity: 0.9 },\n      emphasis: { disabled: true },\n    },\n  ],\n});\n"}