{"spec_id":"curve-dose-response","library":"echarts","language":"javascript","code":"// anyplot.ai\n// curve-dose-response: Pharmacological Dose-Response Curve\n// Library: echarts 5.5.1 | JavaScript 22.23.0\n// Quality: 90/100 | Created: 2026-06-24\n\nconst t = window.ANYPLOT_TOKENS;\n\n// 4-parameter logistic (4PL) model\nfunction pl4(c, bot, top, ec50, hill) {\n  return bot + (top - bot) / (1 + Math.pow(ec50 / c, hill));\n}\n\n// Deterministic LCG RNG\nlet _s = 0;\nfunction srand(n) { _s = n >>> 0; }\nfunction rand() {\n  _s = (_s * 1664525 + 1013904223) & 0xffffffff;\n  return (_s >>> 0) / 4294967295;\n}\n\n// Kinase inhibitor study: two compounds with different potencies\nconst cmpA = { name: 'Inhibitor A', bot: 3,  top: 96, ec50: 8e-8, hill: 1.2 };\nconst cmpB = { name: 'Inhibitor B', bot: 5,  top: 89, ec50: 7e-7, hill: 0.9 };\n\n// Experimental concentration points (log-spaced, 1 nM – 100 µM)\nconst concs = [1e-9, 3e-9, 1e-8, 3e-8, 1e-7, 3e-7, 1e-6, 3e-6, 1e-5, 3e-5, 1e-4];\n\nfunction makeExpData(cmp, seed) {\n  srand(seed);\n  return concs.map(c => [\n    c,\n    Math.max(0, Math.min(100, pl4(c, cmp.bot, cmp.top, cmp.ec50, cmp.hill) + (rand() - 0.5) * 9)),\n    1.2 + rand() * 2.4,\n  ]);\n}\n\nconst expA = makeExpData(cmpA, 42);\nconst expB = makeExpData(cmpB, 137);\n\n// Smooth fitted curves (150 log-spaced evaluation points)\nconst xCurve = Array.from({ length: 151 }, (_, i) => Math.pow(10, -9.5 + i * 6.5 / 150));\nconst curveA = xCurve.map(c => [c, pl4(c, cmpA.bot, cmpA.top, cmpA.ec50, cmpA.hill)]);\nconst curveB = xCurve.map(c => [c, pl4(c, cmpB.bot, cmpB.top, cmpB.ec50, cmpB.hill)]);\n\n// 95% CI band around Inhibitor A (±4.5 response units, stacked area technique)\nconst CI = 4.5;\nconst ciLow  = xCurve.map(c => [c, pl4(c, cmpA.bot, cmpA.top, cmpA.ec50, cmpA.hill) - CI]);\nconst ciDiff = xCurve.map(c => [c, CI * 2]);\n\n// EC50 half-maximal response (midpoint of 4PL = (bot + top) / 2)\nconst midA = (cmpA.bot + cmpA.top) / 2;\nconst midB = (cmpB.bot + cmpB.top) / 2;\n\n// Custom renderItem for scatter points with ±1.96 SEM error bars\nfunction makeEBSeries(data, color, name) {\n  return {\n    type: 'custom',\n    name,\n    legendHoverLink: false,\n    renderItem(_params, api) {\n      const [px, py]  = api.coord([api.value(0), api.value(1)]);\n      const [, ytop]  = api.coord([api.value(0), api.value(1) + api.value(2) * 1.96]);\n      const [, ybot]  = api.coord([api.value(0), api.value(1) - api.value(2) * 1.96]);\n      const cap = 5;\n      return {\n        type: 'group',\n        children: [\n          { type: 'line', shape: { x1: px, y1: ytop, x2: px, y2: ybot },\n            style: { stroke: color, lineWidth: 1.5 } },\n          { type: 'line', shape: { x1: px - cap, y1: ytop, x2: px + cap, y2: ytop },\n            style: { stroke: color, lineWidth: 1.5 } },\n          { type: 'line', shape: { x1: px - cap, y1: ybot, x2: px + cap, y2: ybot },\n            style: { stroke: color, lineWidth: 1.5 } },\n          { type: 'circle', shape: { cx: px, cy: py, r: 7 },\n            style: { fill: color, stroke: t.pageBg, lineWidth: 2 } },\n        ],\n      };\n    },\n    data,\n    z: 10,\n  };\n}\n\nconst chart = echarts.init(document.getElementById('container'));\n\nchart.setOption({\n  animation: false,\n  backgroundColor: 'transparent',\n  color: t.palette,\n\n  title: {\n    text: 'curve-dose-response · javascript · echarts · anyplot.ai',\n    left: 'center',\n    top: 18,\n    textStyle: { color: t.ink, fontSize: 22, fontWeight: 'bold' },\n  },\n\n  legend: {\n    data: [cmpA.name, cmpB.name, '95% CI (A)'],\n    orient: 'vertical',\n    right: 24,\n    top: 'middle',\n    textStyle: { color: t.inkSoft, fontSize: 14 },\n    itemGap: 18,\n    itemWidth: 20,\n    itemHeight: 12,\n  },\n\n  grid: { left: 110, right: 160, top: 85, bottom: 85 },\n\n  xAxis: {\n    type: 'log',\n    name: 'Concentration (M)',\n    nameLocation: 'middle',\n    nameGap: 44,\n    nameTextStyle: { color: t.ink, fontSize: 16 },\n    min: 3e-10,\n    max: 3e-4,\n    axisLabel: {\n      color: t.inkSoft,\n      fontSize: 13,\n      formatter(v) {\n        const logV = Math.log10(v);\n        const e = Math.round(logV);\n        return Math.abs(logV - e) < 0.01 ? '10^' + e : '';\n      },\n    },\n    axisLine: { lineStyle: { color: t.inkSoft } },\n    splitLine: { lineStyle: { color: t.grid } },\n    minorSplitLine: { show: true, lineStyle: { color: t.grid, opacity: 0.45 } },\n    minorTick: { show: true },\n  },\n\n  yAxis: {\n    type: 'value',\n    name: 'Response (%)',\n    nameLocation: 'middle',\n    nameGap: 60,\n    nameTextStyle: { color: t.ink, fontSize: 16 },\n    min: -8,\n    max: 110,\n    interval: 20,\n    axisLabel: { color: t.inkSoft, fontSize: 13 },\n    axisLine: { lineStyle: { color: t.inkSoft } },\n    splitLine: { lineStyle: { color: t.grid } },\n  },\n\n  series: [\n    // CI band lower boundary (invisible baseline for stacking)\n    {\n      type: 'line',\n      name: '_ci_base',\n      data: ciLow,\n      lineStyle: { opacity: 0 },\n      areaStyle: { opacity: 0 },\n      stack: 'ci-a',\n      symbol: 'none',\n      silent: true,\n    },\n    // CI band upper (stacked on base — fills the band between lower and upper)\n    {\n      type: 'line',\n      name: '95% CI (A)',\n      data: ciDiff,\n      lineStyle: { opacity: 0 },\n      areaStyle: { color: t.palette[0], opacity: 0.13 },\n      stack: 'ci-a',\n      symbol: 'none',\n      color: t.palette[0],\n      silent: true,\n    },\n\n    // Fitted curve — Inhibitor A with EC50 + asymptote reference lines\n    {\n      type: 'line',\n      name: cmpA.name,\n      data: curveA,\n      smooth: false,\n      symbol: 'none',\n      lineStyle: { color: t.palette[0], width: 2.5 },\n      color: t.palette[0],\n      markLine: {\n        silent: true,\n        symbol: 'none',\n        label: { show: false },\n        data: [\n          {\n            yAxis: midA,\n            lineStyle: { color: t.palette[0], type: 'dashed', opacity: 0.8, width: 1.5 },\n            label: { show: true, formatter: 'EC₅₀', position: 'insideEndTop',\n                     color: t.inkSoft, fontSize: 12, distance: [4, 4] },\n          },\n          {\n            xAxis: cmpA.ec50,\n            lineStyle: { color: t.palette[0], type: 'dashed', opacity: 0.8, width: 1.5 },\n          },\n          {\n            yAxis: cmpA.top,\n            lineStyle: { color: t.palette[0], type: 'dashed', opacity: 0.32, width: 1 },\n            label: { show: true, formatter: 'Top', position: 'insideEndTop',\n                     color: t.inkSoft, fontSize: 11, distance: [4, 4] },\n          },\n          {\n            yAxis: cmpA.bot,\n            lineStyle: { color: t.palette[0], type: 'dashed', opacity: 0.32, width: 1 },\n            label: { show: true, formatter: 'Bottom', position: 'insideEndTop',\n                     color: t.inkSoft, fontSize: 11, distance: [4, 4] },\n          },\n        ],\n      },\n    },\n\n    // Fitted curve — Inhibitor B with EC50 + asymptote reference lines\n    {\n      type: 'line',\n      name: cmpB.name,\n      data: curveB,\n      smooth: false,\n      symbol: 'none',\n      lineStyle: { color: t.palette[1], width: 2.5 },\n      color: t.palette[1],\n      markLine: {\n        silent: true,\n        symbol: 'none',\n        label: { show: false },\n        data: [\n          {\n            yAxis: midB,\n            lineStyle: { color: t.palette[1], type: 'dashed', opacity: 0.8, width: 1.5 },\n            label: { show: true, formatter: 'EC₅₀', position: 'insideStartTop',\n                     color: t.inkSoft, fontSize: 12, distance: [4, 4] },\n          },\n          {\n            xAxis: cmpB.ec50,\n            lineStyle: { color: t.palette[1], type: 'dashed', opacity: 0.8, width: 1.5 },\n          },\n          {\n            yAxis: cmpB.top,\n            lineStyle: { color: t.palette[1], type: 'dashed', opacity: 0.32, width: 1 },\n          },\n          {\n            yAxis: cmpB.bot,\n            lineStyle: { color: t.palette[1], type: 'dashed', opacity: 0.32, width: 1 },\n          },\n        ],\n      },\n    },\n\n    // Scatter + error bars for Inhibitor A\n    makeEBSeries(expA, t.palette[0], '_eb_a'),\n    // Scatter + error bars for Inhibitor B\n    makeEBSeries(expB, t.palette[1], '_eb_b'),\n  ],\n});\n"}