{"spec_id":"curve-dose-response","library":"highcharts","language":"javascript","code":"// anyplot.ai\n// curve-dose-response: Pharmacological Dose-Response Curve\n// Library: highcharts 12.6.0 | JavaScript 22.22.3\n// Quality: 87/100 | Created: 2026-06-24\n//# anyplot-orientation: landscape\n\nconst t = window.ANYPLOT_TOKENS;\n\n// --- Data --------------------------------------------------------------------\n\n// 4-parameter logistic (4PL) sigmoid model\nfunction fourPL(c, bottom, top, ec50, hill) {\n    return bottom + (top - bottom) / (1 + Math.pow(ec50 / c, hill));\n}\n\n// Generate smooth fitted curve across 200 log-spaced points (1e-9 to 1e-4 M)\nfunction smoothCurve(p) {\n    const pts = [];\n    for (let i = 0; i <= 200; i++) {\n        const logC = -9 + 5 * i / 200;\n        const c = Math.pow(10, logC);\n        pts.push([c, fourPL(c, p.bottom, p.top, p.ec50, p.hill)]);\n    }\n    return pts;\n}\n\n// Deterministic LCG pseudo-random generator (reproducible, no seed API in browser)\nfunction makeLCG(seed) {\n    let s = seed >>> 0;\n    return function () {\n        s = (Math.imul(s, 1664525) + 1013904223) >>> 0;\n        return s / 4294967296;\n    };\n}\nfunction boxMuller(rng) {\n    const u1 = rng() + 1e-10, u2 = rng();\n    return Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2);\n}\n\n// Compound parameters — pharmacology drug-potency scenario\nconst compA = { name: 'Compound A', bottom: 2, top: 98, ec50: 1e-7, hill: 1.5 };\nconst compB = { name: 'Compound B', bottom: 5, top: 85, ec50: 5e-7, hill: 0.8 };\n\n// Measurement concentrations: 10 log-spaced points from 1 nM to 100 µM\nconst measConc = [1e-9, 3e-9, 1e-8, 3e-8, 1e-7, 3e-7, 1e-6, 3e-6, 1e-5, 1e-4];\n\nfunction genMeasured(p, seedVal) {\n    const rng = makeLCG(seedVal);\n    return measConc.map(c => {\n        const ideal = fourPL(c, p.bottom, p.top, p.ec50, p.hill);\n        const sem = 2.5 + Math.abs(boxMuller(rng)) * 1.5;\n        const noise = boxMuller(rng) * sem;\n        return { x: c, y: Math.max(0, Math.min(100, ideal + noise)), sem };\n    });\n}\n\nconst dataA = genMeasured(compA, 42);\nconst dataB = genMeasured(compB, 137);\n\nconst curveA = smoothCurve(compA);\nconst curveB = smoothCurve(compB);\n\n// 95% CI band for Compound A: ±8 response units around fitted curve\nconst CI_W = 8;\nconst ciUpperA = curveA.map(([x, y]) => [x, Math.min(100, y + CI_W)]);\n\n// Half-maximal response thresholds for EC50 reference lines\nconst halfA = (compA.bottom + compA.top) / 2;  // 50 %\nconst halfB = (compB.bottom + compB.top) / 2;  // 45 %\n\n// RGBA for CI band: #009E73 = rgb(0,158,115) at 15% opacity\nconst ciColorA = 'rgba(0,158,115,0.15)';\n\n// --- Chart -------------------------------------------------------------------\n\nHighcharts.chart('container', {\n    chart: {\n        backgroundColor: 'transparent',\n        animation: false,\n        style: { fontFamily: 'inherit' },\n        marginTop: 90,\n        marginRight: 50,\n        marginBottom: 90,\n        marginLeft: 80,\n        events: {\n            // Draw error bars via SVG renderer after each render\n            render: function () {\n                const c = this;\n                if (c._errorBars) c._errorBars.forEach(el => el.destroy());\n                c._errorBars = [];\n\n                // Series indices 3 (Compound A data) and 4 (Compound B data)\n                [[3, t.palette[0]], [4, t.palette[1]]].forEach(([si, color]) => {\n                    const s = c.series[si];\n                    if (!s || !s.points) return;\n                    s.points.forEach(pt => {\n                        if (pt.plotX === undefined || pt.plotY === undefined) return;\n                        const sem = (pt.options && pt.options.sem) || 3;\n                        const px = pt.plotX + c.plotLeft;\n                        const topY = c.yAxis[0].toPixels(pt.y + sem, false);\n                        const botY = c.yAxis[0].toPixels(pt.y - sem, false);\n                        const cap = 5;\n                        const path = c.renderer.path([\n                            'M', px, topY, 'L', px, botY,\n                            'M', px - cap, topY, 'L', px + cap, topY,\n                            'M', px - cap, botY, 'L', px + cap, botY\n                        ]).attr({\n                            stroke: color,\n                            'stroke-width': 2,\n                            zIndex: 10,\n                            'stroke-linecap': 'round'\n                        }).add();\n                        c._errorBars.push(path);\n                    });\n                });\n            }\n        }\n    },\n    credits: { enabled: false },\n    colors: t.palette,\n\n    title: {\n        text: 'curve-dose-response · javascript · highcharts · anyplot.ai',\n        style: { color: t.ink, fontSize: '22px', fontWeight: '600' }\n    },\n    subtitle: {\n        text: 'Shaded band: 95% CI for Compound A  |  Dashed lines: EC₅₀ and asymptote references',\n        style: { color: t.inkSoft, fontSize: '13px' }\n    },\n\n    xAxis: {\n        type: 'logarithmic',\n        title: {\n            text: 'Concentration (M)',\n            style: { color: t.inkSoft, fontSize: '16px' }\n        },\n        lineColor: t.inkSoft,\n        tickColor: t.inkSoft,\n        gridLineColor: t.grid,\n        gridLineWidth: 0,\n        labels: {\n            style: { color: t.inkSoft, fontSize: '14px' },\n            formatter: function () {\n                const exp = Math.round(Math.log10(this.value));\n                return '10<sup>' + exp + '</sup>';\n            },\n            useHTML: true\n        },\n        plotLines: [\n            {\n                value: compA.ec50,\n                color: t.palette[0],\n                dashStyle: 'ShortDash',\n                width: 1.5,\n                zIndex: 5,\n                label: {\n                    text: 'EC₅₀ A',\n                    style: { color: t.palette[0], fontSize: '12px' },\n                    align: 'right',\n                    y: -6\n                }\n            },\n            {\n                value: compB.ec50,\n                color: t.palette[1],\n                dashStyle: 'ShortDash',\n                width: 1.5,\n                zIndex: 5,\n                label: {\n                    text: 'EC₅₀ B',\n                    style: { color: t.palette[1], fontSize: '12px' },\n                    align: 'right',\n                    y: -6\n                }\n            }\n        ]\n    },\n\n    yAxis: {\n        title: {\n            text: 'Response (%)',\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: {\n            style: { color: t.inkSoft, fontSize: '14px' },\n            format: '{value}%'\n        },\n        min: -5,\n        max: 110,\n        plotLines: [\n            // EC50 half-max horizontal reference lines\n            { value: halfA, color: t.palette[0], dashStyle: 'ShortDash', width: 1.5, zIndex: 5 },\n            { value: halfB, color: t.palette[1], dashStyle: 'ShortDash', width: 1.5, zIndex: 5 },\n            // Top asymptotes\n            { value: compA.top, color: t.palette[0], dashStyle: 'LongDash', width: 1, zIndex: 4 },\n            { value: compB.top, color: t.palette[1], dashStyle: 'LongDash', width: 1, zIndex: 4 },\n            // Bottom asymptote (shared)\n            { value: 3, color: t.inkSoft, dashStyle: 'LongDash', width: 1.5, zIndex: 4 }\n        ]\n    },\n\n    legend: {\n        enabled: true,\n        align: 'right',\n        verticalAlign: 'middle',\n        layout: 'vertical',\n        itemStyle: { color: t.inkSoft, fontSize: '14px' },\n        itemHoverStyle: { color: t.ink },\n        backgroundColor: t.elevatedBg,\n        borderColor: t.grid,\n        borderWidth: 1,\n        borderRadius: 4,\n        padding: 12,\n        symbolWidth: 24\n    },\n\n    plotOptions: {\n        series: { animation: false },\n        spline: {\n            lineWidth: 3,\n            marker: { enabled: false }\n        },\n        scatter: {\n            marker: { radius: 6 }\n        }\n    },\n\n    series: [\n        // Compound A fitted curve (show first so it appears first in legend)\n        {\n            type: 'spline',\n            id: 'curveA',\n            name: 'Compound A (fit)',\n            data: curveA,\n            color: t.palette[0],\n            lineWidth: 3,\n            zIndex: 5,\n            showInLegend: true\n        },\n        // Compound A 95% CI band — upper envelope fill\n        {\n            type: 'area',\n            name: 'Compound A 95% CI',\n            data: ciUpperA,\n            color: t.palette[0],\n            fillColor: ciColorA,\n            lineWidth: 0,\n            threshold: null,\n            showInLegend: true,\n            enableMouseTracking: false,\n            marker: { enabled: false },\n            zIndex: 1\n        },\n        // Compound B fitted curve\n        {\n            type: 'spline',\n            id: 'curveB',\n            name: 'Compound B (fit)',\n            data: curveB,\n            color: t.palette[1],\n            lineWidth: 3,\n            zIndex: 5,\n            showInLegend: true\n        },\n        // Compound A measured data points (linked to curveA for legend toggling)\n        {\n            type: 'scatter',\n            name: 'Compound A (data)',\n            data: dataA.map(d => ({ x: d.x, y: d.y, sem: d.sem })),\n            color: t.palette[0],\n            marker: {\n                radius: 6,\n                symbol: 'circle',\n                lineWidth: 1.5,\n                lineColor: t.pageBg\n            },\n            zIndex: 8,\n            linkedTo: 'curveA',\n            showInLegend: false,\n            enableMouseTracking: false\n        },\n        // Compound B measured data points (linked to curveB for legend toggling)\n        {\n            type: 'scatter',\n            name: 'Compound B (data)',\n            data: dataB.map(d => ({ x: d.x, y: d.y, sem: d.sem })),\n            color: t.palette[1],\n            marker: {\n                radius: 6,\n                symbol: 'square',\n                lineWidth: 1.5,\n                lineColor: t.pageBg\n            },\n            zIndex: 8,\n            linkedTo: 'curveB',\n            showInLegend: false,\n            enableMouseTracking: false\n        }\n    ]\n});\n"}