{"spec_id":"curve-dose-response","library":"muix","language":"javascript","code":"// anyplot.ai\n// curve-dose-response: Pharmacological Dose-Response Curve\n// Library: muix 7.29.1 | JavaScript 22.22.3\n// Quality: 89/100 | Created: 2026-06-24\n//# anyplot-orientation: landscape\n// anyplot.ai\n// curve-dose-response: Pharmacological Dose-Response Curve\n// Library: MUI X Charts | React | Node 22\n// License: @mui/x-charts — MIT (community). Pro/Premium are out of scope.\n// Quality: pending | Created: 2026-06-24\n\nimport { ChartContainer } from \"@mui/x-charts/ChartContainer\";\nimport { LinePlot } from \"@mui/x-charts/LineChart\";\nimport { ScatterPlot } from \"@mui/x-charts/ScatterChart\";\nimport { ChartsXAxis } from \"@mui/x-charts/ChartsXAxis\";\nimport { ChartsYAxis } from \"@mui/x-charts/ChartsYAxis\";\nimport { ChartsGrid } from \"@mui/x-charts/ChartsGrid\";\nimport { ChartsLegend } from \"@mui/x-charts/ChartsLegend\";\nimport { ChartsReferenceLine } from \"@mui/x-charts/ChartsReferenceLine\";\nimport { useXScale, useYScale } from \"@mui/x-charts/hooks\";\n\nconst t = window.ANYPLOT_TOKENS;\nconst { width, height } = window.ANYPLOT_SIZE;\n\n// Four-parameter logistic model (4PL)\nfunction fourPL(c, bottom, top, ec50, hill) {\n  return bottom + (top - bottom) / (1 + Math.pow(ec50 / c, hill));\n}\n\n// Reproducible LCG (seed 42) — no Math.random() in browser harness context\nlet seed = 42;\nfunction rng() {\n  seed = (1664525 * seed + 1013904223) >>> 0;\n  return seed / 4294967296;\n}\n\n// Unicode superscript formatter for log10 tick labels (e.g. 10⁻⁹)\nconst SUPERSCRIPT = { '0': '⁰', '1': '¹', '2': '²', '3': '³', '4': '⁴', '5': '⁵', '6': '⁶', '7': '⁷', '8': '⁸', '9': '⁹', '-': '⁻' };\nfunction toSuperscript(n) {\n  return String(n).split('').map(c => SUPERSCRIPT[c] || c).join('');\n}\n\n// Compound pharmacological parameters\nconst compA = { label: \"Compound A (EC₅₀ = 100 nM)\", bottom: 2, top: 98, ec50: 1e-7, hill: 1.8 };\nconst compB = { label: \"Compound B (EC₅₀ = 800 nM)\", bottom: 5, top: 85, ec50: 8e-7, hill: 1.1 };\n\n// Ten measured concentrations per compound spanning 1 nM – 100 µM\nconst measConcs = [1e-9, 3.16e-9, 1e-8, 3.16e-8, 1e-7, 3.16e-7, 1e-6, 3.16e-6, 1e-5, 1e-4];\n\nfunction makeMeasured(comp, idPrefix) {\n  return measConcs.map((c, i) => ({\n    x: c,\n    y: Math.max(0, Math.min(100, fourPL(c, comp.bottom, comp.top, comp.ec50, comp.hill) + (rng() - 0.5) * 9)),\n    sem: 2.5 + rng() * 4,\n    id: `${idPrefix}-${i}`,\n  }));\n}\n\nconst measA = makeMeasured(compA, \"A\");\nconst measB = makeMeasured(compB, \"B\");\n\n// Dense log-spaced x-axis for smooth fitted curves (80 points)\nconst N = 80;\nconst logMin = Math.log10(3e-10);\nconst logMax = Math.log10(3e-3);\nconst curveXs = Array.from({ length: N }, (_, i) =>\n  Math.pow(10, logMin + (i / (N - 1)) * (logMax - logMin))\n);\n\nconst curveAY = curveXs.map(x => fourPL(x, compA.bottom, compA.top, compA.ec50, compA.hill));\nconst curveBY = curveXs.map(x => fourPL(x, compB.bottom, compB.top, compB.ec50, compB.hill));\n\n// 95% CI band for Compound A (±5 pp approximation)\nconst ciUpper = curveAY.map(y => Math.min(100, y + 5));\nconst ciLower = curveAY.map(y => Math.max(0, y - 5));\n\n// Y-midpoint for the 50% response reference line\nconst midA = (compA.bottom + compA.top) / 2;\n\n// Custom 95% CI band polygon rendered via MUI X axis scale hooks\nfunction CIBand() {\n  const xScale = useXScale();\n  const yScale = useYScale();\n  if (!xScale || !yScale) return null;\n\n  const upper = curveXs.map((x, i) => [xScale(x), yScale(ciUpper[i])]);\n  const lower = curveXs.map((x, i) => [xScale(x), yScale(ciLower[i])]);\n  const pts = [...upper, ...lower.slice().reverse()];\n  const d =\n    pts.map((p, i) => `${i === 0 ? \"M\" : \"L\"}${p[0].toFixed(1)},${p[1].toFixed(1)}`).join(\" \") + \" Z\";\n\n  return <path d={d} fill={t.palette[0]} fillOpacity={0.20} stroke=\"none\" />;\n}\n\n// SEM error bars rendered directly as SVG lines\nfunction ErrorBars({ data, color }) {\n  const xScale = useXScale();\n  const yScale = useYScale();\n  if (!xScale || !yScale) return null;\n\n  const cap = 8;\n  return (\n    <g>\n      {data.map(d => {\n        const cx = xScale(d.x);\n        const ytop = yScale(d.y + d.sem);\n        const ybot = yScale(d.y - d.sem);\n        if (isNaN(cx) || isNaN(ytop) || isNaN(ybot)) return null;\n        return (\n          <g key={d.id}>\n            <line x1={cx} y1={ytop} x2={cx} y2={ybot} stroke={color} strokeWidth={2.5} />\n            <line x1={cx - cap} y1={ytop} x2={cx + cap} y2={ytop} stroke={color} strokeWidth={2.5} />\n            <line x1={cx - cap} y1={ybot} x2={cx + cap} y2={ybot} stroke={color} strokeWidth={2.5} />\n          </g>\n        );\n      })}\n    </g>\n  );\n}\n\nconst TITLE = \"curve-dose-response · javascript · muix · anyplot.ai\";\n\nexport default function Chart() {\n  return (\n    <ChartContainer\n      width={width}\n      height={height}\n      margin={{ top: 62, right: 56, bottom: 88, left: 96 }}\n      sx={{ '& .MuiChartsAxis-top, & .MuiChartsAxis-right': { display: 'none' } }}\n      series={[\n        {\n          type: \"line\",\n          id: \"curveA\",\n          data: curveAY,\n          label: compA.label,\n          color: t.palette[0],\n          showMark: false,\n          curve: \"monotoneX\",\n          xAxisId: \"concentration\",\n        },\n        {\n          type: \"line\",\n          id: \"curveB\",\n          data: curveBY,\n          label: compB.label,\n          color: t.palette[1],\n          showMark: false,\n          curve: \"monotoneX\",\n          xAxisId: \"concentration\",\n        },\n        {\n          type: \"scatter\",\n          id: \"dataA\",\n          data: measA,\n          color: t.palette[0],\n          markerSize: 9,\n          xAxisId: \"concentration\",\n        },\n        {\n          type: \"scatter\",\n          id: \"dataB\",\n          data: measB,\n          color: t.palette[1],\n          markerSize: 9,\n          xAxisId: \"concentration\",\n        },\n      ]}\n      xAxis={[{\n        id: \"concentration\",\n        scaleType: \"log\",\n        data: curveXs,\n        min: 3e-10,\n        max: 3e-3,\n        label: \"Concentration (M)\",\n        tickInterval: [1e-9, 1e-8, 1e-7, 1e-6, 1e-5, 1e-4, 1e-3],\n        valueFormatter: v => `10${toSuperscript(Math.round(Math.log10(v)))}`,\n        tickLabelStyle: { fontSize: 14, fill: t.inkSoft },\n        labelStyle: { fontSize: 16, fill: t.ink },\n      }]}\n      yAxis={[{\n        id: \"response\",\n        min: -5,\n        max: 108,\n        label: \"Response (%)\",\n        tickLabelStyle: { fontSize: 14, fill: t.inkSoft },\n        labelStyle: { fontSize: 16, fill: t.ink },\n      }]}\n    >\n      <ChartsGrid horizontal />\n      <CIBand />\n      <LinePlot skipAnimation />\n      <ScatterPlot skipAnimation />\n      <ErrorBars data={measA} color={t.palette[0]} />\n      <ErrorBars data={measB} color={t.palette[1]} />\n      <ChartsXAxis\n        axisId=\"concentration\"\n        tickLabelStyle={{ fontSize: 14, fill: t.inkSoft }}\n        labelStyle={{ fontSize: 16, fill: t.ink }}\n      />\n      <ChartsYAxis\n        axisId=\"response\"\n        tickLabelStyle={{ fontSize: 14, fill: t.inkSoft }}\n        labelStyle={{ fontSize: 16, fill: t.ink }}\n      />\n      <ChartsReferenceLine\n        x={compA.ec50}\n        axisId=\"concentration\"\n        label=\"EC₅₀-A\"\n        labelAlign=\"start\"\n        labelStyle={{ fill: t.palette[0], fontSize: 13, fontWeight: \"bold\" }}\n        lineStyle={{ stroke: t.palette[0], strokeDasharray: \"8,5\", strokeWidth: 1.5 }}\n      />\n      <ChartsReferenceLine\n        x={compB.ec50}\n        axisId=\"concentration\"\n        label=\"EC₅₀-B\"\n        labelAlign=\"start\"\n        labelStyle={{ fill: t.palette[1], fontSize: 13, fontWeight: \"bold\" }}\n        lineStyle={{ stroke: t.palette[1], strokeDasharray: \"8,5\", strokeWidth: 1.5 }}\n      />\n      <ChartsReferenceLine\n        y={midA}\n        axisId=\"response\"\n        label=\"50%\"\n        labelAlign=\"end\"\n        labelStyle={{ fill: t.inkSoft, fontSize: 12 }}\n        lineStyle={{ stroke: t.inkSoft, strokeDasharray: \"4,4\", strokeWidth: 1, opacity: 0.55 }}\n      />\n      {/* Top asymptotes — Compound A (98%) and Compound B (85%) */}\n      <ChartsReferenceLine\n        y={compA.top}\n        axisId=\"response\"\n        label={`${compA.top}%`}\n        labelAlign=\"start\"\n        labelStyle={{ fill: t.palette[0], fontSize: 11 }}\n        lineStyle={{ stroke: t.palette[0], strokeDasharray: \"3,5\", strokeWidth: 1, opacity: 0.45 }}\n      />\n      <ChartsReferenceLine\n        y={compB.top}\n        axisId=\"response\"\n        label={`${compB.top}%`}\n        labelAlign=\"start\"\n        labelStyle={{ fill: t.palette[1], fontSize: 11 }}\n        lineStyle={{ stroke: t.palette[1], strokeDasharray: \"3,5\", strokeWidth: 1, opacity: 0.45 }}\n      />\n      {/* Bottom asymptotes — Compound A (2%) and Compound B (5%) */}\n      <ChartsReferenceLine\n        y={compA.bottom}\n        axisId=\"response\"\n        label={`${compA.bottom}%`}\n        labelAlign=\"start\"\n        labelStyle={{ fill: t.palette[0], fontSize: 11 }}\n        lineStyle={{ stroke: t.palette[0], strokeDasharray: \"3,5\", strokeWidth: 1, opacity: 0.45 }}\n      />\n      <ChartsReferenceLine\n        y={compB.bottom}\n        axisId=\"response\"\n        label={`${compB.bottom}%`}\n        labelAlign=\"start\"\n        labelStyle={{ fill: t.palette[1], fontSize: 11 }}\n        lineStyle={{ stroke: t.palette[1], strokeDasharray: \"3,5\", strokeWidth: 1, opacity: 0.45 }}\n      />\n      <ChartsLegend\n        position={{ vertical: \"top\", horizontal: \"right\" }}\n        slotProps={{\n          legend: {\n            itemMarkWidth: 22,\n            itemMarkHeight: 4,\n            markGap: 8,\n            itemGap: 30,\n            labelStyle: { fontSize: 14, fill: t.ink },\n          },\n        }}\n      />\n      <text\n        x={width / 2}\n        y={30}\n        textAnchor=\"middle\"\n        fontSize={22}\n        fontWeight={600}\n        fill={t.ink}\n      >\n        {TITLE}\n      </text>\n    </ChartContainer>\n  );\n}\n"}