Pharmacological Dose-Response Curve — Apache ECharts

A sigmoidal dose-response curve that plots biological response against drug concentration on a logarithmic x-axis, fitted using a four-parameter logistic (4PL) model. This visualization is essential for determining drug potency metrics such as EC50 (half-maximal effective concentration) or IC50 (half-maximal inhibitory concentration), Hill slope steepness, and upper/lower response asymptotes. It enables rapid visual comparison of compound efficacy and is a standard tool in pharmacological analysis.

Pharmacological Dose-Response Curve rendered with Apache ECharts

Renders

JavaScript source (Apache ECharts)

// anyplot.ai
// curve-dose-response: Pharmacological Dose-Response Curve
// Library: echarts 5.5.1 | JavaScript 22.23.0
// Quality: 90/100 | Created: 2026-06-24

const t = window.ANYPLOT_TOKENS;

// 4-parameter logistic (4PL) model
function pl4(c, bot, top, ec50, hill) {
  return bot + (top - bot) / (1 + Math.pow(ec50 / c, hill));
}

// Deterministic LCG RNG
let _s = 0;
function srand(n) { _s = n >>> 0; }
function rand() {
  _s = (_s * 1664525 + 1013904223) & 0xffffffff;
  return (_s >>> 0) / 4294967295;
}

// Kinase inhibitor study: two compounds with different potencies
const cmpA = { name: 'Inhibitor A', bot: 3,  top: 96, ec50: 8e-8, hill: 1.2 };
const cmpB = { name: 'Inhibitor B', bot: 5,  top: 89, ec50: 7e-7, hill: 0.9 };

// Experimental concentration points (log-spaced, 1 nM – 100 µM)
const concs = [1e-9, 3e-9, 1e-8, 3e-8, 1e-7, 3e-7, 1e-6, 3e-6, 1e-5, 3e-5, 1e-4];

function makeExpData(cmp, seed) {
  srand(seed);
  return concs.map(c => [
    c,
    Math.max(0, Math.min(100, pl4(c, cmp.bot, cmp.top, cmp.ec50, cmp.hill) + (rand() - 0.5) * 9)),
    1.2 + rand() * 2.4,
  ]);
}

const expA = makeExpData(cmpA, 42);
const expB = makeExpData(cmpB, 137);

// Smooth fitted curves (150 log-spaced evaluation points)
const xCurve = Array.from({ length: 151 }, (_, i) => Math.pow(10, -9.5 + i * 6.5 / 150));
const curveA = xCurve.map(c => [c, pl4(c, cmpA.bot, cmpA.top, cmpA.ec50, cmpA.hill)]);
const curveB = xCurve.map(c => [c, pl4(c, cmpB.bot, cmpB.top, cmpB.ec50, cmpB.hill)]);

// 95% CI band around Inhibitor A (±4.5 response units, stacked area technique)
const CI = 4.5;
const ciLow  = xCurve.map(c => [c, pl4(c, cmpA.bot, cmpA.top, cmpA.ec50, cmpA.hill) - CI]);
const ciDiff = xCurve.map(c => [c, CI * 2]);

// EC50 half-maximal response (midpoint of 4PL = (bot + top) / 2)
const midA = (cmpA.bot + cmpA.top) / 2;
const midB = (cmpB.bot + cmpB.top) / 2;

// Custom renderItem for scatter points with ±1.96 SEM error bars
function makeEBSeries(data, color, name) {
  return {
    type: 'custom',
    name,
    legendHoverLink: false,
    renderItem(_params, api) {
      const [px, py]  = api.coord([api.value(0), api.value(1)]);
      const [, ytop]  = api.coord([api.value(0), api.value(1) + api.value(2) * 1.96]);
      const [, ybot]  = api.coord([api.value(0), api.value(1) - api.value(2) * 1.96]);
      const cap = 5;
      return {
        type: 'group',
        children: [
          { type: 'line', shape: { x1: px, y1: ytop, x2: px, y2: ybot },
            style: { stroke: color, lineWidth: 1.5 } },
          { type: 'line', shape: { x1: px - cap, y1: ytop, x2: px + cap, y2: ytop },
            style: { stroke: color, lineWidth: 1.5 } },
          { type: 'line', shape: { x1: px - cap, y1: ybot, x2: px + cap, y2: ybot },
            style: { stroke: color, lineWidth: 1.5 } },
          { type: 'circle', shape: { cx: px, cy: py, r: 7 },
            style: { fill: color, stroke: t.pageBg, lineWidth: 2 } },
        ],
      };
    },
    data,
    z: 10,
  };
}

const chart = echarts.init(document.getElementById('container'));

chart.setOption({
  animation: false,
  backgroundColor: 'transparent',
  color: t.palette,

  title: {
    text: 'curve-dose-response · javascript · echarts · anyplot.ai',
    left: 'center',
    top: 18,
    textStyle: { color: t.ink, fontSize: 22, fontWeight: 'bold' },
  },

  legend: {
    data: [cmpA.name, cmpB.name, '95% CI (A)'],
    orient: 'vertical',
    right: 24,
    top: 'middle',
    textStyle: { color: t.inkSoft, fontSize: 14 },
    itemGap: 18,
    itemWidth: 20,
    itemHeight: 12,
  },

  grid: { left: 110, right: 160, top: 85, bottom: 85 },

  xAxis: {
    type: 'log',
    name: 'Concentration (M)',
    nameLocation: 'middle',
    nameGap: 44,
    nameTextStyle: { color: t.ink, fontSize: 16 },
    min: 3e-10,
    max: 3e-4,
    axisLabel: {
      color: t.inkSoft,
      fontSize: 13,
      formatter(v) {
        const logV = Math.log10(v);
        const e = Math.round(logV);
        return Math.abs(logV - e) < 0.01 ? '10^' + e : '';
      },
    },
    axisLine: { lineStyle: { color: t.inkSoft } },
    splitLine: { lineStyle: { color: t.grid } },
    minorSplitLine: { show: true, lineStyle: { color: t.grid, opacity: 0.45 } },
    minorTick: { show: true },
  },

  yAxis: {
    type: 'value',
    name: 'Response (%)',
    nameLocation: 'middle',
    nameGap: 60,
    nameTextStyle: { color: t.ink, fontSize: 16 },
    min: -8,
    max: 110,
    interval: 20,
    axisLabel: { color: t.inkSoft, fontSize: 13 },
    axisLine: { lineStyle: { color: t.inkSoft } },
    splitLine: { lineStyle: { color: t.grid } },
  },

  series: [
    // CI band lower boundary (invisible baseline for stacking)
    {
      type: 'line',
      name: '_ci_base',
      data: ciLow,
      lineStyle: { opacity: 0 },
      areaStyle: { opacity: 0 },
      stack: 'ci-a',
      symbol: 'none',
      silent: true,
    },
    // CI band upper (stacked on base — fills the band between lower and upper)
    {
      type: 'line',
      name: '95% CI (A)',
      data: ciDiff,
      lineStyle: { opacity: 0 },
      areaStyle: { color: t.palette[0], opacity: 0.13 },
      stack: 'ci-a',
      symbol: 'none',
      color: t.palette[0],
      silent: true,
    },

    // Fitted curve — Inhibitor A with EC50 + asymptote reference lines
    {
      type: 'line',
      name: cmpA.name,
      data: curveA,
      smooth: false,
      symbol: 'none',
      lineStyle: { color: t.palette[0], width: 2.5 },
      color: t.palette[0],
      markLine: {
        silent: true,
        symbol: 'none',
        label: { show: false },
        data: [
          {
            yAxis: midA,
            lineStyle: { color: t.palette[0], type: 'dashed', opacity: 0.8, width: 1.5 },
            label: { show: true, formatter: 'EC₅₀', position: 'insideEndTop',
                     color: t.inkSoft, fontSize: 12, distance: [4, 4] },
          },
          {
            xAxis: cmpA.ec50,
            lineStyle: { color: t.palette[0], type: 'dashed', opacity: 0.8, width: 1.5 },
          },
          {
            yAxis: cmpA.top,
            lineStyle: { color: t.palette[0], type: 'dashed', opacity: 0.32, width: 1 },
            label: { show: true, formatter: 'Top', position: 'insideEndTop',
                     color: t.inkSoft, fontSize: 11, distance: [4, 4] },
          },
          {
            yAxis: cmpA.bot,
            lineStyle: { color: t.palette[0], type: 'dashed', opacity: 0.32, width: 1 },
            label: { show: true, formatter: 'Bottom', position: 'insideEndTop',
                     color: t.inkSoft, fontSize: 11, distance: [4, 4] },
          },
        ],
      },
    },

    // Fitted curve — Inhibitor B with EC50 + asymptote reference lines
    {
      type: 'line',
      name: cmpB.name,
      data: curveB,
      smooth: false,
      symbol: 'none',
      lineStyle: { color: t.palette[1], width: 2.5 },
      color: t.palette[1],
      markLine: {
        silent: true,
        symbol: 'none',
        label: { show: false },
        data: [
          {
            yAxis: midB,
            lineStyle: { color: t.palette[1], type: 'dashed', opacity: 0.8, width: 1.5 },
            label: { show: true, formatter: 'EC₅₀', position: 'insideStartTop',
                     color: t.inkSoft, fontSize: 12, distance: [4, 4] },
          },
          {
            xAxis: cmpB.ec50,
            lineStyle: { color: t.palette[1], type: 'dashed', opacity: 0.8, width: 1.5 },
          },
          {
            yAxis: cmpB.top,
            lineStyle: { color: t.palette[1], type: 'dashed', opacity: 0.32, width: 1 },
          },
          {
            yAxis: cmpB.bot,
            lineStyle: { color: t.palette[1], type: 'dashed', opacity: 0.32, width: 1 },
          },
        ],
      },
    },

    // Scatter + error bars for Inhibitor A
    makeEBSeries(expA, t.palette[0], '_eb_a'),
    // Scatter + error bars for Inhibitor B
    makeEBSeries(expB, t.palette[1], '_eb_b'),
  ],
});

Part of Pharmacological Dose-Response Curve on anyplot.ai.

Other implementations