#| '!! shinylive warning !!': |
#| shinylive does not work in self-contained HTML documents.
#| Please set `embed-resources: false` in your metadata.
#| standalone: true
library(shiny)
library(pracma)
ui <- fluidPage(
tags$head(
tags$style(HTML("
html, body { height: 100%; overflow-x: hidden; }
.container-fluid { max-width: 1400px; }
.pk-toolbar { display: flex; justify-content: flex-end; margin: .5rem 0 1rem 0; gap: .5rem; }
.table-wrap { overflow-x: auto; }
#concPlot { width: 100%; }
@media (max-width: 768px) {
.form-group, .shiny-input-container { margin-bottom: .5rem; }
}
"))
),
titlePanel('\U0001F4CA PK Simulator: One-Compartment Model'),
sidebarLayout(
sidebarPanel(
selectInput("route", "Route of Administration:",
choices = c("IV Bolus" = "iv",
"Oral (1st-order absorption)" = "oral",
"IV Infusion (zero-order input)" = "infusion")),
numericInput("dose", "Dose (mg):", value = 500),
numericInput("vd", "Volume of Distribution (L):", value = 50),
numericInput("cl", "Clearance (L/hr):", value = 5),
numericInput("weight", "Body Weight (kg):", value = 70),
conditionalPanel(condition = "input.route == 'oral'", numericInput("ka", "Ka (1/hr):", value = 1.0)),
conditionalPanel(condition = "input.route == 'infusion'", numericInput("tinf","Infusion Duration (hr):", value = 1)),
numericInput("tau", "Dosing Interval (hr):", value = 8),
checkboxInput("simulate_ss", "Simulate Until Steady State", value = FALSE),
numericInput("n_doses", "Number of Doses:", value = 5),
conditionalPanel(condition = "input.route == 'oral'", numericInput("f", "Bioavailability (F):", value = 1)),
numericInput("duration", "Simulation Duration (hours):", value = 24),
checkboxInput("show_doses", "Show Dose Markers", value = TRUE),
checkboxInput("show_cmax", "Show Cmax & Tmax", value = FALSE),
checkboxInput("show_auc", "Show AUC (shaded)", value = FALSE),
checkboxInput("show_fluctuation", "Show Fluctuation Index", value = FALSE),
checkboxInput("show_time_above_mec", "Show Time > MEC", value = FALSE),
numericInput("mec", "Minimum Effective Concentration (MEC):", value = 5),
checkboxInput("show_ptr", "Show Peak-to-Trough Ratio (PTR)", value = FALSE),
checkboxInput("show_tss", "Show Time to Steady State (Tss)", value = FALSE),
checkboxInput("show_vd_kg", "Show Vd/kg", value = FALSE),
checkboxInput("show_rac", "Show Accumulation Ratio (Rac)", value = FALSE)
),
mainPanel(
plotOutput("concPlot", height = "70vh"),
br(),
checkboxInput("show_table", "Show Simulation Table", value = FALSE),
conditionalPanel(condition = "input.show_table",
div(class = "table-wrap", tableOutput("conc_table"))),
br(),
h4("\U0001F4CC PK Summary Parameters"),
div(class = "table-wrap", tableOutput("summary_table"))
)
)
)
server <- function(input, output, session) {
observeEvent({input$n_doses; input$tau}, {
if (!input$simulate_ss) updateNumericInput(session, "duration", value = input$n_doses * input$tau)
})
observeEvent(input$simulate_ss, {
if (input$simulate_ss) {
kel <- input$cl / input$vd
t_half <- log(2) / kel
tss <- 5 * t_half
new_duration <- ceiling(tss + input$tau * 3)
updateNumericInput(session, "duration", value = new_duration)
}
})
pk_data <- reactive({
kel <- input$cl / input$vd
time <- seq(0, input$duration, by = 0.1)
conc <- rep(0, length(time))
n_doses <- if (input$simulate_ss) ceiling(input$duration / input$tau) else input$n_doses
for (i in 0:(n_doses - 1)) {
t_dose <- i * input$tau
if (input$route == "iv") {
conc <- conc + ifelse(time >= t_dose, (input$dose / input$vd) * exp(-kel * (time - t_dose)), 0)
} else if (input$route == "oral") {
ka <- input$ka; f <- input$f
conc <- conc + ifelse(time >= t_dose,
(f * input$dose * ka / (input$vd * (ka - kel))) *
(exp(-kel * (time - t_dose)) - exp(-ka * (time - t_dose))), 0)
} else if (input$route == "infusion") {
tinf <- input$tinf; rate <- input$dose / tinf
conc <- conc + ifelse(time >= t_dose & time <= (t_dose + tinf),
(rate / input$cl) * (1 - exp(-kel * (time - t_dose))),
ifelse(time > (t_dose + tinf),
(rate / input$cl) * (1 - exp(-kel * tinf)) * exp(-kel * (time - t_dose - tinf)), 0))
}
}
data.frame(Time_hr = round(time, 2), Conc_mg_per_L = round(conc, 3))
})
output$summary_table <- renderTable({
df <- pk_data(); conc <- df$Conc_mg_per_L; time <- df$Time_hr
kel <- input$cl / input$vd; t_half <- log(2) / kel
c_max <- max(conc, na.rm = TRUE); c_min <- min(conc, na.rm = TRUE)
auc <- pracma::trapz(time, conc); c_avg <- auc / (max(time) - min(time))
fi <- if (input$show_fluctuation && c_avg > 0) ((c_max - c_min) / c_avg) * 100 else NA
time_above <- if (input$show_time_above_mec) sum(conc > input$mec) * 0.1 else NA
ptr <- if (input$show_ptr && c_min > 0) c_max / c_min else NA
tss <- if (input$show_tss) 5 * t_half else NA
vd_kg <- if (input$show_vd_kg) input$vd / input$weight else NA
rac <- if (input$show_rac) 1 / (1 - exp(-kel * input$tau)) else NA
data.frame(
Parameter = c("Half-life (t½)", "Cmax", "Cmin", "Cavg", "AUC", "Fluctuation Index",
"Time > MEC", "Peak-to-Trough Ratio", "Time to Steady State (Tss)",
"Vd/kg", "Accumulation Ratio (Rac)"),
Value = round(c(t_half, c_max, c_min, c_avg, auc, fi, time_above, ptr, tss, vd_kg, rac), 2),
Units = c("hr", "mg/L", "mg/L", "mg/L", "mg·hr/L", "%", "hours", "none", "hours", "L/kg", "none")
)
})
output$concPlot <- renderPlot({
df <- pk_data(); time <- df$Time_hr; conc <- df$Conc_mg_per_L
plot(time, conc, type = "l", lwd = 2, col = "blue",
xlab = "Time (hours)", ylab = "Plasma Concentration (mg/L)",
main = paste("PK Profile -",
switch(input$route, "iv"="IV Bolus", "oral"="Oral Dosing", "infusion"="IV Infusion"),
"(Multiple Doses)"),
ylim = c(0, max(conc) * 1.2))
if (input$show_doses) {
dose_times <- seq(0, by = input$tau, length.out = if (input$simulate_ss) ceiling(input$duration / input$tau) else input$n_doses)
abline(v = dose_times, col = "gray40", lty = 2)
text(x = dose_times, y = rep(max(conc, na.rm = TRUE) * 0.9, length(dose_times)),
labels = paste0("Dose ", seq_along(dose_times)), col = "gray30", cex = 0.8, pos = 3)
}
if (input$show_cmax) {
cmax <- max(conc, na.rm = TRUE); tmax <- time[which.max(conc)]
points(tmax, cmax, col = "red", pch = 19)
text(tmax, cmax * 1.1, paste0("Cmax = ", round(cmax, 2), "\nTmax = ", round(tmax, 2)),
col = "red", cex = 0.9)
}
if (input$show_auc) {
polygon(c(time, rev(time)), c(rep(0, length(time)), rev(conc)),
col = adjustcolor("skyblue", alpha.f = 0.3), border = NA)
}
if (input$show_tss) {
kel <- input$cl / input$vd; tss <- 5 * log(2) / kel
abline(v = tss, col = "darkgreen", lty = 3, lwd = 2)
text(tss, max(conc) * 1.05, labels = "Steady State", col = "darkgreen", pos = 4, cex = 0.9)
}
})
output$conc_table <- renderTable({ pk_data() })
}
shinyApp(ui = ui, server = server)
Pharmacokinetic Simulator (PK-Simulator)
PK-Simulator is an interactive tool that models drug concentration over time using a one-compartment pharmacokinetic approach. Users can select the route of administration—IV bolus, oral, or infusion—and adjust parameters such as dose, clearance, volume of distribution, and dosing interval.
The app dynamically generates concentration–time curves and provides key pharmacokinetic metrics including Cmax, Tmax, AUC, and half-life. Designed for learners, educators, and healthcare professionals alike, the simulator offers an intuitive way to explore how different variables influence drug behavior in the body.