{"spec_id":"bode-basic","library":"highcharts","language":"javascript","code":"// anyplot.ai\n// bode-basic: Bode Plot for Frequency Response\n// Library: highcharts 12.6.0 | JavaScript 22.22.3\n// Quality: 89/100 | Created: 2026-06-17\n\n//# anyplot-orientation: landscape\n\nconst t = window.ANYPLOT_TOKENS;\n\n// --- Transfer function data --------------------------------------------------\n// Three-pole low-pass: H(s) = K / ((s/w1+1)(s/w2+1)(s/w3+1))\n// K=10, pole frequencies: f1=1Hz, f2=10Hz, f3=100Hz\n// Gives ~54deg phase margin and ~21dB gain margin — a well-tuned stable system\nconst K = 10, f1 = 1, f2 = 10, f3 = 100;\nconst N = 300, fMin = 0.05, fMax = 5000;\nconst magData = [], phaseData = [];\n\nfor (let i = 0; i < N; i++) {\n    const f = fMin * Math.pow(10, i * Math.log10(fMax / fMin) / (N - 1));\n    const denom = Math.sqrt(1 + (f / f1) ** 2) * Math.sqrt(1 + (f / f2) ** 2) * Math.sqrt(1 + (f / f3) ** 2);\n    const magDb = 20 * Math.log10(K / denom);\n    const phaseDeg = -(Math.atan(f / f1) + Math.atan(f / f2) + Math.atan(f / f3)) * (180 / Math.PI);\n    magData.push([f, +magDb.toFixed(4)]);\n    phaseData.push([f, +phaseDeg.toFixed(4)]);\n}\n\n// --- Stability margin computation (log-linear interpolation) -----------------\n// Gain crossover: first frequency where magnitude crosses 0 dB from above\nlet fGc = null, phaseAtGc = null;\nfor (let i = 1; i < magData.length; i++) {\n    if (magData[i - 1][1] >= 0 && magData[i][1] < 0) {\n        const r = magData[i - 1][1] / (magData[i - 1][1] - magData[i][1]);\n        fGc = magData[i - 1][0] * Math.pow(magData[i][0] / magData[i - 1][0], r);\n        phaseAtGc = phaseData[i - 1][1] + (phaseData[i][1] - phaseData[i - 1][1]) * r;\n        break;\n    }\n}\n\n// Phase crossover: first frequency where phase crosses -180 degrees\nlet fPc = null, magAtPc = null;\nfor (let i = 1; i < phaseData.length; i++) {\n    if (phaseData[i - 1][1] >= -180 && phaseData[i][1] < -180) {\n        const r = (-180 - phaseData[i - 1][1]) / (phaseData[i][1] - phaseData[i - 1][1]);\n        fPc = phaseData[i - 1][0] * Math.pow(phaseData[i][0] / phaseData[i - 1][0], r);\n        magAtPc = magData[i - 1][1] + (magData[i][1] - magData[i - 1][1]) * r;\n        break;\n    }\n}\n\nconst phaseMarginStr = phaseAtGc !== null ? (180 + phaseAtGc).toFixed(1) : null;\nconst gainMarginStr  = magAtPc   !== null ? (-magAtPc).toFixed(1) : null;\n\n// Vertical marker lines at crossover frequencies\nconst xPlotLines = [];\nif (fGc !== null) {\n    xPlotLines.push({\n        value: fGc,\n        color: t.palette[3],       // ochre — gain crossover\n        dashStyle: 'ShortDash',\n        width: 1.5,\n        zIndex: 3,\n        label: {\n            text: 'PM = ' + phaseMarginStr + '°',\n            style: { color: t.palette[3], fontSize: '12px', fontWeight: '600' },\n            rotation: 0,\n            verticalAlign: 'top',\n            y: 16,\n            x: 5\n        }\n    });\n}\nif (fPc !== null) {\n    xPlotLines.push({\n        value: fPc,\n        color: t.palette[4],       // matte red — phase crossover\n        dashStyle: 'ShortDash',\n        width: 1.5,\n        zIndex: 3,\n        label: {\n            text: 'GM = ' + gainMarginStr + ' dB',\n            style: { color: t.palette[4], fontSize: '12px', fontWeight: '600' },\n            rotation: 0,\n            verticalAlign: 'bottom',\n            y: -16,\n            x: 5\n        }\n    });\n}\n\n// --- Chart ------------------------------------------------------------------\nHighcharts.chart('container', {\n    chart: {\n        type: 'line',\n        backgroundColor: 'transparent',\n        animation: false,\n        style: { fontFamily: 'inherit' },\n        marginLeft: 85,\n        marginRight: 40,\n        marginBottom: 70,\n        marginTop: 65\n    },\n    credits: { enabled: false },\n    colors: t.palette,\n    title: {\n        text: 'bode-basic · javascript · highcharts · anyplot.ai',\n        style: { color: t.ink, fontSize: '22px', fontWeight: '600' }\n    },\n    subtitle: {\n        text: 'Three-pole system — K = 10,  f₁ = 1 Hz,  f₂ = 10 Hz,  f₃ = 100 Hz',\n        style: { color: t.inkSoft, fontSize: '13px' }\n    },\n    legend: {\n        enabled: true,\n        verticalAlign: 'top',\n        align: 'right',\n        y: 6,\n        itemStyle: { color: t.inkSoft, fontSize: '14px' },\n        itemHoverStyle: { color: t.ink }\n    },\n    xAxis: {\n        type: 'logarithmic',\n        min: fMin,\n        max: fMax,\n        title: {\n            text: 'Frequency (Hz)',\n            style: { color: t.inkSoft, fontSize: '16px' }\n        },\n        lineColor: t.inkSoft,\n        tickColor: t.inkSoft,\n        gridLineColor: t.grid,\n        gridLineWidth: 1,\n        labels: { style: { color: t.inkSoft, fontSize: '14px' } },\n        plotLines: xPlotLines\n    },\n    yAxis: [\n        {\n            // Top panel — Magnitude\n            title: {\n                text: 'Magnitude (dB)',\n                style: { color: t.inkSoft, fontSize: '16px' }\n            },\n            top: '3%',\n            height: '43%',\n            offset: 0,\n            lineColor: t.inkSoft,\n            gridLineColor: t.grid,\n            labels: { style: { color: t.inkSoft, fontSize: '14px' } },\n            plotLines: [{\n                value: 0,\n                color: t.inkSoft,\n                dashStyle: 'Dot',\n                width: 1.5,\n                zIndex: 2,\n                label: {\n                    text: '0 dB',\n                    style: { color: t.inkSoft, fontSize: '12px' },\n                    align: 'right',\n                    x: -6,\n                    y: -6\n                }\n            }]\n        },\n        {\n            // Bottom panel — Phase\n            title: {\n                text: 'Phase (°)',\n                style: { color: t.inkSoft, fontSize: '16px' }\n            },\n            top: '54%',\n            height: '43%',\n            offset: 0,\n            lineColor: t.inkSoft,\n            gridLineColor: t.grid,\n            labels: { style: { color: t.inkSoft, fontSize: '14px' } },\n            plotLines: [{\n                value: -180,\n                color: t.inkSoft,\n                dashStyle: 'Dot',\n                width: 1.5,\n                zIndex: 2,\n                label: {\n                    text: '−180°',\n                    style: { color: t.inkSoft, fontSize: '12px' },\n                    align: 'right',\n                    x: -6,\n                    y: -6\n                }\n            }]\n        }\n    ],\n    plotOptions: {\n        series: {\n            animation: false,\n            lineWidth: 2.5,\n            marker: { enabled: false }\n        }\n    },\n    series: [\n        {\n            name: 'Magnitude',\n            data: magData,\n            yAxis: 0,\n            color: t.palette[0]    // #009E73 brand green — always first series\n        },\n        {\n            name: 'Phase',\n            data: phaseData,\n            yAxis: 1,\n            color: t.palette[1]    // #C475FD lavender — canonical second series\n        }\n    ]\n});\n"}