# Synthetic TLF demonstration. No patient or client data.
# Run: Rscript generate-demo.R [output-directory]
# Requires R, ggplot2, plotly and jsonlite. No network access is used.
# Question: how do two independent synthetic groups differ in a continuous score?
# Figure: individual observations plus mean and pointwise 95% t confidence interval.
# Contrast: unadjusted difference in means B - A, with Welch's 95% confidence interval.
# No covariates, repeated measures, missingness, clinical interpretation or causal claim.

library(ggplot2)
args <- commandArgs(trailingOnly = TRUE)
output_dir <- if (length(args)) args[1] else "tlf-demo"
dir.create(output_dir, recursive = TRUE, showWarnings = FALSE)
RNGkind("Mersenne-Twister", "Inversion", "Rejection")
set.seed(20260906)
data <- data.frame(
  id = sprintf("S%03d", seq_len(120)),
  group = rep(c("A", "B"), each = 60),
  score = c(rnorm(60, mean = 50, sd = 10), rnorm(60, mean = 48, sd = 12))
)
stopifnot(nrow(data) == 120L, !anyNA(data), !anyDuplicated(data$id))
summarise_group <- function(group) {
  values <- data$score[data$group == group]
  n <- length(values)
  half_width <- qt(.975, df = n - 1) * sd(values) / sqrt(n)
  data.frame(group, n, mean = mean(values), sd = sd(values),
             lower = mean(values) - half_width, upper = mean(values) + half_width)
}
summary <- do.call(rbind, lapply(c("A", "B"), summarise_group))
b <- data$score[data$group == "B"]
a <- data$score[data$group == "A"]
welch <- t.test(b, a, var.equal = FALSE, conf.level = .95)
contrast <- list(estimate = mean(b) - mean(a), lower = unname(welch$conf.int[1]),
                 upper = unname(welch$conf.int[2]), df = unname(welch$parameter))
# Independent arithmetic check of the package result and direction of the contrast.
variance_terms <- c(var(b) / length(b), var(a) / length(a))
df <- sum(variance_terms)^2 / sum(variance_terms^2 / (c(length(b), length(a)) - 1))
expected <- mean(b) - mean(a) + c(-1, 1) * qt(.975, df) * sqrt(sum(variance_terms))
stopifnot(isTRUE(all.equal(as.numeric(welch$conf.int), expected, check.attributes = FALSE)))
write.csv(data, file.path(output_dir, "synthetic-data.csv"), row.names = FALSE)
jsonlite::write_json(list(seed = 20260906, groups = summary, contrast = contrast,
                         synthetic = TRUE, generator = "R / ggplot2 / Plotly"),
                     file.path(output_dir, "summary.json"), auto_unbox = TRUE,
                     digits = 15, pretty = TRUE)

for (lang in c("es", "en")) {
  es <- lang == "es"
  data$tooltip <- paste0(if (es) "Participante sint\u00e9tico: " else "Synthetic participant: ", data$id,
                         if (es) "<br>Grupo: " else "<br>Group: ", data$group,
                         if (es) "<br>Puntuaci\u00f3n: " else "<br>Score: ", formatC(data$score, digits = 2, format = "f"))
  summary$tooltip <- paste0(if (es) "Media del grupo " else "Group mean ", summary$group, ": ",
                            formatC(summary$mean, digits = 2, format = "f"),
                            if (es) "<br>IC 95 %: " else "<br>95% CI: ",
                            formatC(summary$lower, digits = 2, format = "f"), " \u2013 ",
                            formatC(summary$upper, digits = 2, format = "f"))
  chart <- ggplot(data, aes(x = group, y = score, text = tooltip)) +
    geom_point(position = position_jitter(width = .14, height = 0, seed = 16),
               colour = "#607D8B", alpha = .85, size = 1.8) +
    geom_errorbar(data = summary, aes(y = mean, ymin = lower, ymax = upper, colour = group),
                  width = .16, linewidth = 1.1) +
    geom_point(data = summary, aes(y = mean, colour = group), shape = 18, size = 4.8) +
    scale_colour_manual(values = c(A = "#062A4E", B = "#08737A"), guide = "none") +
    scale_x_discrete(labels = if (es) c(A = "Grupo A\nn = 60", B = "Grupo B\nn = 60")
                             else c(A = "Group A\nn = 60", B = "Group B\nn = 60")) +
    labs(x = NULL, y = if (es) "Puntuaci\u00f3n sint\u00e9tica" else "Synthetic score") +
    theme_minimal(base_size = 17, base_family = "sans") +
    theme(panel.grid.minor = element_blank(), panel.grid.major.x = element_blank(),
          panel.grid.major.y = element_line(colour = "#E1EAED"),
          axis.text = element_text(colour = "#1D3646"), axis.title = element_text(colour = "#062A4E"),
          plot.margin = margin(18, 18, 12, 12), legend.position = "none")
  ggsave(file.path(output_dir, paste0("figure-", lang, ".png")), chart,
         width = 7.2, height = 6, dpi = 180, bg = "white")
  ggsave(file.path(output_dir, paste0("figure-", lang, "-mobile.png")), chart,
         width = 4.2, height = 4.6, dpi = 180, bg = "white")
  interactive <- plotly::ggplotly(chart, tooltip = "text")
  interactive <- plotly::layout(interactive, font = list(family = "Arial", size = 15),
                               margin = list(l = 65, r = 20, t = 20, b = 65))
  built <- plotly::plotly_build(interactive)
  for (axis in c("xaxis", "yaxis")) {
    built$x$layout[[axis]]$tickfont <- list(family = "Arial", size = 14, color = "#1D3646")
    built$x$layout[[axis]]$title$font <- list(family = "Arial", size = 15, color = "#062A4E")
  }
  jsonlite::write_json(list(data = built$x$data, layout = built$x$layout),
                       file.path(output_dir, paste0("figure-", lang, ".json")),
                       auto_unbox = TRUE, null = "null", digits = 15)
}
file.copy(system.file("htmlwidgets/lib/plotlyjs/plotly-latest.min.js", package = "plotly"),
          file.path(output_dir, "plotly.min.js"), overwrite = TRUE)
file.copy(system.file("htmlwidgets/lib/plotlyjs/LICENSE", package = "plotly"),
          file.path(output_dir, "plotly-LICENSE.txt"), overwrite = TRUE)
writeLines(capture.output(sessionInfo()), file.path(output_dir, "session-info.txt"))
cat("Synthetic demo generated and Welch interval verified.\n")
