{"spec_id":"curve-dose-response","library":"ggplot2","language":"r","code":"#' anyplot.ai\n#' curve-dose-response: Pharmacological Dose-Response Curve\n#' Library: ggplot2 3.5.1 | R 4.4.1\n#' Quality: 94/100 | Created: 2026-06-24\n\nlibrary(ggplot2)\nlibrary(dplyr)\nlibrary(tibble)\nlibrary(ragg)\n\nset.seed(42)\n\n# Theme tokens\nTHEME       <- Sys.getenv(\"ANYPLOT_THEME\", \"light\")\nPAGE_BG     <- if (THEME == \"light\") \"#FAF8F1\" else \"#1A1A17\"\nELEVATED_BG <- if (THEME == \"light\") \"#FFFDF6\" else \"#242420\"\nINK         <- if (THEME == \"light\") \"#1A1A17\" else \"#F0EFE8\"\nINK_SOFT    <- if (THEME == \"light\") \"#4A4A44\" else \"#B8B7B0\"\nINK_MUTED   <- if (THEME == \"light\") \"#6B6A63\" else \"#A8A79F\"\n\nIMPRINT_PALETTE <- c(\n    \"#009E73\", \"#C475FD\", \"#4467A3\", \"#BD8233\",\n    \"#AE3030\", \"#2ABCCD\", \"#954477\", \"#99B314\"\n)\n\n# Data — concentrations in nM, 10 points per compound spanning 6 decades\nconc_obs_nM <- 10^seq(-1, 5, length.out = 10)\n\nresp_a_true <- 4 + (94 - 4) / (1 + (20  / conc_obs_nM)^1.8)\nresp_b_true <- 6 + (88 - 6) / (1 + (800 / conc_obs_nM)^1.1)\n\ndf <- tibble(\n    conc     = rep(conc_obs_nM, 2),\n    response = c(\n        pmax(0, pmin(100, resp_a_true + rnorm(10, 0, 4))),\n        pmax(0, pmin(100, resp_b_true + rnorm(10, 0, 4)))\n    ),\n    sem      = c(runif(10, 1.5, 3.5), runif(10, 1.5, 3.5)),\n    compound = rep(c(\"Compound A\", \"Compound B\"), each = 10)\n)\n\n# Fit 4PL models via nonlinear least squares\nfit_a <- nls(\n    response ~ Bottom + (Top - Bottom) / (1 + (EC50 / conc)^Hill),\n    data  = filter(df, compound == \"Compound A\"),\n    start = list(Bottom = 4, Top = 94, EC50 = 20, Hill = 1.5)\n)\n\nfit_b <- nls(\n    response ~ Bottom + (Top - Bottom) / (1 + (EC50 / conc)^Hill),\n    data  = filter(df, compound == \"Compound B\"),\n    start = list(Bottom = 6, Top = 88, EC50 = 800, Hill = 1.0)\n)\n\n# Fine concentration grid for smooth curves\nconc_fine <- 10^seq(-1.5, 5.5, length.out = 400)\n\ncoef_a <- coef(fit_a)\ncoef_b <- coef(fit_b)\nvcov_a <- vcov(fit_a)\n\npred_a <- predict(fit_a, newdata = data.frame(conc = conc_fine))\npred_b <- predict(fit_b, newdata = data.frame(conc = conc_fine))\n\n# 95% confidence band for Compound A via delta method (gradient of 4PL w.r.t. params)\nse_a <- sapply(conc_fine, function(x) {\n    B <- coef_a[\"Bottom\"]; Tp <- coef_a[\"Top\"]\n    E <- coef_a[\"EC50\"];   H  <- coef_a[\"Hill\"]\n    r <- (E / x)^H; denom <- 1 + r\n    g <- c(r / denom, 1 / denom,\n           -(Tp - B) * H * r / (E * denom^2),\n           -(Tp - B) * r * log(E / x) / denom^2)\n    sqrt(as.numeric(t(g) %*% vcov_a %*% g))\n})\n\ndf_curves <- tibble(\n    conc     = rep(conc_fine, 2),\n    response = c(pred_a, pred_b),\n    compound = rep(c(\"Compound A\", \"Compound B\"), each = 400)\n)\n\ndf_ci_a <- tibble(\n    conc = conc_fine,\n    ymin = pred_a - 1.96 * se_a,\n    ymax = pred_a + 1.96 * se_a\n)\n\n# EC50 and half-response values for crosshair annotations\nec50_a <- coef_a[\"EC50\"]\nec50_b <- coef_b[\"EC50\"]\nhmid_a <- (coef_a[\"Bottom\"] + coef_a[\"Top\"]) / 2\nhmid_b <- (coef_b[\"Bottom\"] + coef_b[\"Top\"]) / 2\nx_left  <- 10^(-1.5)\nx_right <- 10^5.5\n\ncomp_colors <- c(\n    \"Compound A\" = IMPRINT_PALETTE[1],\n    \"Compound B\" = IMPRINT_PALETTE[2]\n)\n\n# Plot\np <- ggplot() +\n    # 95% CI ribbon — Compound A only\n    geom_ribbon(\n        data  = df_ci_a,\n        aes(x = conc, ymin = ymin, ymax = ymax),\n        fill  = IMPRINT_PALETTE[1],\n        alpha = 0.18\n    ) +\n    # Top and bottom asymptote dashed lines (Compound A)\n    geom_hline(\n        yintercept = coef_a[\"Top\"],\n        linetype = \"dashed\", color = INK_SOFT, linewidth = 0.4\n    ) +\n    geom_hline(\n        yintercept = coef_a[\"Bottom\"],\n        linetype = \"dashed\", color = INK_SOFT, linewidth = 0.4\n    ) +\n    # Asymptote labels at right margin\n    annotate(\n        \"text\",\n        x = x_right * 0.88, y = coef_a[\"Top\"] + 3.5,\n        label = \"Top\", color = INK_MUTED, size = 2.6, hjust = 1, fontface = \"italic\"\n    ) +\n    annotate(\n        \"text\",\n        x = x_right * 0.88, y = coef_a[\"Bottom\"] + 3.5,\n        label = \"Bottom\", color = INK_MUTED, size = 2.6, hjust = 1, fontface = \"italic\"\n    ) +\n    # EC50 crosshair — Compound A\n    annotate(\n        \"segment\",\n        x = ec50_a, xend = ec50_a, y = -3, yend = hmid_a,\n        linetype = \"dashed\", color = IMPRINT_PALETTE[1], linewidth = 0.7\n    ) +\n    annotate(\n        \"segment\",\n        x = x_left, xend = ec50_a, y = hmid_a, yend = hmid_a,\n        linetype = \"dashed\", color = IMPRINT_PALETTE[1], linewidth = 0.7\n    ) +\n    # EC50 crosshair — Compound B\n    annotate(\n        \"segment\",\n        x = ec50_b, xend = ec50_b, y = -3, yend = hmid_b,\n        linetype = \"dashed\", color = IMPRINT_PALETTE[2], linewidth = 0.7\n    ) +\n    annotate(\n        \"segment\",\n        x = x_left, xend = ec50_b, y = hmid_b, yend = hmid_b,\n        linetype = \"dashed\", color = IMPRINT_PALETTE[2], linewidth = 0.7\n    ) +\n    # Fitted 4PL curves\n    geom_line(\n        data      = df_curves,\n        aes(x = conc, y = response, color = compound),\n        linewidth = 1.4\n    ) +\n    # Observed data: error bars then points (bars behind points)\n    geom_errorbar(\n        data      = df,\n        aes(x = conc, ymin = response - sem, ymax = response + sem, color = compound),\n        width     = 0.08,\n        linewidth = 0.7\n    ) +\n    geom_point(\n        data   = df,\n        aes(x = conc, y = response, color = compound),\n        size   = 2.5,\n        shape  = 21,\n        fill   = PAGE_BG,\n        stroke = 1.2\n    ) +\n    # EC50 label annotations with background box for visual weight\n    annotate(\n        \"label\",\n        x = ec50_a * 2.0, y = hmid_a - 10,\n        label        = sprintf(\"EC50 = %.0f nM\", ec50_a),\n        color        = IMPRINT_PALETTE[1],\n        fill         = ELEVATED_BG,\n        label.size   = 0.15,\n        label.padding = unit(0.12, \"lines\"),\n        size = 3.0, hjust = 0, fontface = \"italic\"\n    ) +\n    annotate(\n        \"label\",\n        x = ec50_b * 1.8, y = hmid_b + 8,\n        label        = sprintf(\"EC50 = %.0f nM\", ec50_b),\n        color        = IMPRINT_PALETTE[2],\n        fill         = ELEVATED_BG,\n        label.size   = 0.15,\n        label.padding = unit(0.12, \"lines\"),\n        size = 3.0, hjust = 0, fontface = \"italic\"\n    ) +\n    # Potency ratio annotation\n    annotate(\n        \"text\",\n        x = 10^4, y = 30,\n        label = sprintf(\"Potency ratio (B/A): %.0f×\", ec50_b / ec50_a),\n        color = INK_MUTED, size = 2.8, hjust = 0.5, fontface = \"italic\"\n    ) +\n    # Axis scales\n    scale_x_log10(\n        breaks = c(0.1, 1, 10, 100, 1000, 10000, 100000),\n        labels = c(\"0.1\", \"1\", \"10\", \"100\", \"1k\", \"10k\", \"100k\"),\n        limits = c(x_left, x_right),\n        name   = \"Concentration (nM)\"\n    ) +\n    scale_y_continuous(\n        breaks = seq(0, 100, 20),\n        limits = c(-5, 108),\n        name   = \"Response (%)\"\n    ) +\n    scale_color_manual(values = comp_colors, name = NULL) +\n    labs(title = \"curve-dose-response · r · ggplot2 · anyplot.ai\") +\n    theme_minimal(base_size = 8) +\n    theme(\n        plot.background   = element_rect(fill = PAGE_BG, color = PAGE_BG),\n        panel.background  = element_rect(fill = PAGE_BG, color = NA),\n        panel.grid.major  = element_line(color = INK_MUTED, linewidth = 0.15),\n        panel.grid.minor  = element_blank(),\n        panel.border      = element_blank(),\n        axis.line.x       = element_line(color = INK_SOFT, linewidth = 0.4),\n        axis.line.y       = element_line(color = INK_SOFT, linewidth = 0.4),\n        axis.ticks        = element_line(color = INK_SOFT, linewidth = 0.3),\n        axis.title        = element_text(color = INK,      size = 10),\n        axis.text         = element_text(color = INK_SOFT, size = 8),\n        plot.title        = element_text(\n            color = INK, size = 12, hjust = 0.5,\n            margin = margin(b = 8)\n        ),\n        legend.background = element_rect(\n            fill = ELEVATED_BG, color = INK_MUTED, linewidth = 0.25\n        ),\n        legend.text       = element_text(color = INK_SOFT, size = 8),\n        legend.key.size        = unit(1.2, \"lines\"),\n        legend.position        = \"inside\",\n        legend.position.inside = c(0.13, 0.78),\n        plot.margin            = margin(t = 10, r = 15, b = 10, l = 10)\n    )\n\n# Save\nggsave(\n    filename = sprintf(\"plot-%s.png\", THEME),\n    plot     = p,\n    device   = ragg::agg_png,\n    width    = 8,\n    height   = 4.5,\n    units    = \"in\",\n    dpi      = 400\n)\n"}