empty_plot <- function(message, height = 420L) {
  plotly::plot_ly(
    x = 0,
    y = 0,
    type = "scatter",
    mode = "markers",
    marker = list(opacity = 0),
    hoverinfo = "skip",
    showlegend = FALSE,
    height = height
  ) |>
    plotly::layout(
      xaxis = list(visible = FALSE),
      yaxis = list(visible = FALSE),
      margin = list(l = 20, r = 20, b = 20, t = 20),
      annotations = list(list(
        text = message,
        x = 0.5,
        y = 0.5,
        xref = "paper",
        yref = "paper",
        showarrow = FALSE,
        align = "center",
        font = list(size = 15, color = "#475569")
      ))
    ) |>
    plotly::config(displayModeBar = FALSE, displaylogo = FALSE, responsive = TRUE)
}

guided_plot_layout <- function(
    plot,
    x_title,
    y_title,
    x_categories = NULL,
    y_range = NULL,
    y_suffix = NULL,
    show_legend = FALSE,
    barmode = NULL) {
  x_axis <- list(
    title = list(text = x_title, standoff = 12),
    automargin = TRUE,
    tickfont = list(size = 12),
    fixedrange = TRUE
  )
  if (!is.null(x_categories)) {
    x_axis$categoryorder <- "array"
    x_axis$categoryarray <- x_categories
  }
  y_axis <- list(
    title = list(text = y_title, standoff = 10),
    automargin = TRUE,
    rangemode = "tozero",
    tickfont = list(size = 12),
    gridcolor = "#E2E8F0",
    zerolinecolor = "#94A3B8",
    fixedrange = TRUE
  )
  if (!is.null(y_range)) y_axis$range <- y_range
  if (!is.null(y_suffix)) y_axis$ticksuffix <- y_suffix

  layout_args <- list(
    xaxis = x_axis,
    yaxis = y_axis,
    hovermode = "closest",
    hoverlabel = list(
      align = "left",
      bgcolor = "#FFFFFF",
      bordercolor = "#CBD5E1",
      font = list(size = 12, color = "#17202A")
    ),
    showlegend = show_legend,
    legend = list(
      orientation = "h",
      x = 0,
      xanchor = "left",
      y = 1.08,
      yanchor = "bottom",
      font = list(size = 12),
      groupclick = "togglegroup"
    ),
    margin = list(
      l = 72,
      r = 24,
      b = 72,
      t = if (show_legend) 58 else 24,
      pad = 2
    ),
    font = list(family = "system-ui, -apple-system, Segoe UI, sans-serif", color = "#17202A")
  )
  if (!is.null(barmode)) layout_args$barmode <- barmode

  plot <- do.call(plotly::layout, c(list(p = plot), layout_args))
  plotly::config(
    plot,
    displayModeBar = FALSE,
    displaylogo = FALSE,
    responsive = TRUE,
    scrollZoom = FALSE
  )
}

comparison_dimension <- function(data, dimension) {
  data <- add_display_columns(data)
  if (dimension == "network") {
    data$comparison_label <- data$network_display
    data$comparison_hover_label <- data$network_display
    levels <- unname(network_labels)
    colors <- unname(network_colors[data$network_display])
    x_title <- "Graph family"
  } else if (dimension == "density_condition") {
    data$comparison_label <- unname(density_short_labels[data$density_condition])
    data$comparison_hover_label <- data$comparison_label
    levels <- unname(density_short_labels)
    colors <- unname(c("Sparse" = "#6A51A3", "Denser" = "#238B45")[data$density_condition])
    x_title <- "Graph density"
  } else if (dimension == "metric") {
    metric_axis_labels <- c(
      degree = "Binary degree",
      strength = "Strength",
      closeness = "Harmonic<br>closeness",
      betweenness = "Betweenness"
    )
    data$comparison_label <- unname(metric_axis_labels[data$metric])
    data$comparison_hover_label <- data$metric_display
    levels <- unname(metric_axis_labels)
    colors <- unname(c(
      degree = "#2563EB",
      strength = "#0F766E",
      closeness = "#7C3AED",
      betweenness = "#C2410C"
    )[data$metric])
    x_title <- "Centrality metric"
  } else if (dimension == "threshold") {
    data$comparison_label <- as.character(data$threshold_short)
    data$comparison_hover_label <- data$comparison_label
    levels <- unname(threshold_short_labels)
    colors <- unname(c(
      "0.10-0.20" = "#0EA5E9",
      "0.20-0.40" = "#0F766E",
      "0.40-0.60" = "#B45309",
      "0.60-0.80" = "#9F1239"
    )[data$threshold])
    x_title <- "Threshold regime"
  } else {
    stop("Unsupported comparison dimension: ", dimension)
  }
  data$comparison_color <- colors
  data$comparison_label <- factor(data$comparison_label, levels = levels)
  data <- data[order(data$comparison_label), , drop = FALSE]
  list(data = data, levels = levels, colors = data$comparison_color, x_title = x_title)
}

plot_contrast_snapshot <- function(data, dimension) {
  if (nrow(data) == 0L) return(empty_plot("No target-contrast results match this snapshot."))
  prepared <- comparison_dimension(data, dimension)
  data <- prepared$data
  data$contrast_pp <- 100 * data$mean_high_low_difference
  data$ci_lo_pp <- 100 * data$ci95_lo_high_low_difference
  data$ci_hi_pp <- 100 * data$ci95_hi_high_low_difference
  data$hover <- paste0(
    "<b>", data$comparison_hover_label, "</b><br>",
    "High–low difference: <b>", format_number(data$contrast_pp, 1, " points"), "</b><br>",
    "High > low: ", format_number(data$percent_high_greater, 1, "%")
  )

  upper <- max(data$ci_hi_pp, na.rm = TRUE)
  lower <- min(data$ci_lo_pp, 0, na.rm = TRUE)
  if (!is.finite(upper)) upper <- 1
  if (!is.finite(lower)) lower <- 0
  padding <- max(4, 0.14 * max(1, upper - lower))

  plot <- plotly::plot_ly(
    data = data,
    x = ~comparison_label,
    y = ~contrast_pp,
    type = "bar",
    text = ~format_number(contrast_pp, 1),
    textposition = "outside",
    textfont = list(size = 12, color = "#334155"),
    cliponaxis = FALSE,
    customdata = ~hover,
    hovertemplate = "%{customdata}<extra></extra>",
    marker = list(
      color = prepared$colors,
      line = list(color = "#FFFFFF", width = 1)
    ),
    error_y = list(
      type = "data",
      symmetric = FALSE,
      array = pmax(0, data$ci_hi_pp - data$contrast_pp),
      arrayminus = pmax(0, data$contrast_pp - data$ci_lo_pp),
      visible = TRUE,
      thickness = 1,
      width = 4,
      color = "#64748B"
    )
  )
  guided_plot_layout(
    plot,
    x_title = prepared$x_title,
    y_title = "High–low difference (percentage points)",
    x_categories = prepared$levels,
    y_range = c(lower, upper + padding)
  )
}

plot_multifamily_contrast_snapshot <- function(data, dimension) {
  if (nrow(data) == 0L) return(empty_plot("No target-contrast results match this snapshot."))
  prepared <- comparison_dimension(data, dimension)
  data <- prepared$data
  data$contrast_pp <- 100 * data$mean_high_low_difference
  data$ci_lo_pp <- 100 * data$ci95_lo_high_low_difference
  data$ci_hi_pp <- 100 * data$ci95_hi_high_low_difference
  data$hover <- paste0(
    "<b>", data$network_display, " · ", data$comparison_hover_label, "</b><br>",
    "High–low difference: <b>", format_number(data$contrast_pp, 1, " points"), "</b><br>",
    "High > low: ", format_number(data$percent_high_greater, 1, "%")
  )

  upper <- max(data$ci_hi_pp, na.rm = TRUE)
  lower <- min(data$ci_lo_pp, 0, na.rm = TRUE)
  if (!is.finite(upper)) upper <- 1
  if (!is.finite(lower)) lower <- 0
  padding <- max(4, 0.14 * max(1, upper - lower))

  plot <- plotly::plot_ly()
  for (network_value in names(network_labels)) {
    group_data <- data[data$network == network_value, , drop = FALSE]
    group_data <- group_data[order(group_data$comparison_label), , drop = FALSE]
    if (nrow(group_data) == 0L) next

    family_display <- unname(network_labels[network_value])
    family_color <- unname(network_colors[family_display])
    plot <- plotly::add_trace(
      plot,
      data = group_data,
      x = ~comparison_label,
      y = ~contrast_pp,
      type = "scatter",
      mode = "lines+markers",
      name = unname(network_abbreviations[network_value]),
      legendgroup = network_value,
      customdata = ~hover,
      hovertemplate = "%{customdata}<extra></extra>",
      line = list(color = family_color, width = 2.8),
      marker = list(
        color = family_color,
        symbol = unname(network_plotly_symbols[network_value]),
        size = 9
      ),
      error_y = list(
        type = "data",
        symmetric = FALSE,
        array = pmax(0, group_data$ci_hi_pp - group_data$contrast_pp),
        arrayminus = pmax(0, group_data$contrast_pp - group_data$ci_lo_pp),
        visible = TRUE,
        thickness = 1,
        width = 3,
        color = family_color
      )
    )
  }

  guided_plot_layout(
    plot,
    x_title = prepared$x_title,
    y_title = "High–low difference (percentage points)",
    x_categories = prepared$levels,
    y_range = c(lower, upper + padding),
    show_legend = TRUE
  )
}

plot_multifamily_extent_snapshot <- function(data, dimension) {
  data <- data[data$target %in% c("Low", "High"), , drop = FALSE]
  if (nrow(data) == 0L) return(empty_plot("No final-extent results match this snapshot."))
  prepared <- comparison_dimension(data, dimension)
  data <- prepared$data
  data$mean_pp <- 100 * data$mean_final_active
  data$ci_lo_pp <- 100 * data$ci95_lo_final_active
  data$ci_hi_pp <- 100 * data$ci95_hi_final_active
  data$hover <- paste0(
    "<b>", data$network_display, " · ", data$target_display, " · ", data$comparison_hover_label, "</b><br>",
    "Final activation: <b>", format_number(data$mean_pp, 1, "%"), "</b><br>",
    "95% MC interval: ", format_number(data$ci_lo_pp, 1, "%"), "–", format_number(data$ci_hi_pp, 1, "%")
  )

  plot <- plotly::plot_ly()
  for (network_value in names(network_labels)) {
    family_display <- unname(network_labels[network_value])
    family_color <- unname(network_colors[family_display])
    family_symbol <- unname(network_plotly_symbols[network_value])

    for (target_value in c("Low", "High")) {
      group_data <- data[
        data$network == network_value & data$target == target_value,
        , drop = FALSE
      ]
      group_data <- group_data[order(group_data$comparison_label), , drop = FALSE]
      if (nrow(group_data) == 0L) next

      is_high <- identical(target_value, "High")
      plot <- plotly::add_trace(
        plot,
        data = group_data,
        x = ~comparison_label,
        y = ~mean_pp,
        type = "scatter",
        mode = "lines+markers",
        name = unname(network_abbreviations[network_value]),
        legendgroup = network_value,
        showlegend = is_high,
        customdata = ~hover,
        hovertemplate = "%{customdata}<extra></extra>",
        line = list(
          color = family_color,
          width = if (is_high) 3 else 2.3,
          dash = if (is_high) "solid" else "dash"
        ),
        marker = list(
          color = family_color,
          symbol = if (is_high) family_symbol else paste0(family_symbol, "-open"),
          size = 9
        )
      )
    }
  }

  guided_plot_layout(
    plot,
    x_title = prepared$x_title,
    y_title = "Mean final activation",
    x_categories = prepared$levels,
    y_range = c(0, 100),
    y_suffix = "%",
    show_legend = TRUE
  )
}

plot_extent_snapshot <- function(data, dimension) {
  data <- data[data$target %in% c("Low", "High"), , drop = FALSE]
  if (nrow(data) == 0L) return(empty_plot("No final-extent results match this snapshot."))
  prepared <- comparison_dimension(data, dimension)
  data <- prepared$data
  data$mean_pp <- 100 * data$mean_final_active
  data$ci_lo_pp <- 100 * data$ci95_lo_final_active
  data$ci_hi_pp <- 100 * data$ci95_hi_final_active
  data$hover <- paste0(
    "<b>", data$comparison_hover_label, " · ", data$target_display, "</b><br>",
    "Final activation: <b>", format_number(data$mean_pp, 1, "%"), "</b><br>",
    "95% MC interval: ", format_number(data$ci_lo_pp, 1, "%"), "–", format_number(data$ci_hi_pp, 1, "%")
  )

  plot <- plotly::plot_ly()
  for (target_value in c("Low", "High")) {
    group_data <- data[data$target == target_value, , drop = FALSE]
    group_data <- group_data[order(group_data$comparison_label), , drop = FALSE]
    plot <- plotly::add_trace(
      plot,
      data = group_data,
      x = ~comparison_label,
      y = ~mean_pp,
      type = "scatter",
      mode = "lines+markers",
      name = unname(target_labels[target_value]),
      customdata = ~hover,
      hovertemplate = "%{customdata}<extra></extra>",
      line = list(color = unname(target_colors[target_value]), width = 3),
      marker = list(color = unname(target_colors[target_value]), size = 9),
      error_y = list(
        type = "data",
        symmetric = FALSE,
        array = pmax(0, group_data$ci_hi_pp - group_data$mean_pp),
        arrayminus = pmax(0, group_data$mean_pp - group_data$ci_lo_pp),
        visible = TRUE,
        thickness = 1,
        width = 3,
        color = unname(target_colors[target_value])
      )
    )
  }
  guided_plot_layout(
    plot,
    x_title = prepared$x_title,
    y_title = "Mean final activation",
    x_categories = prepared$levels,
    y_range = c(0, 100),
    y_suffix = "%",
    show_legend = TRUE
  )
}

plot_total_failure_snapshot <- function(data, outcome = c("completion", "steps")) {
  outcome <- match.arg(outcome)
  data <- data[data$target %in% c("Low", "High"), , drop = FALSE]
  if (nrow(data) == 0L) return(empty_plot("No total-failure results match this snapshot."))
  data <- add_display_columns(data)
  data$network_display <- factor(data$network_display, levels = unname(network_labels))

  if (outcome == "steps") {
    data <- data[!is.na(data$mean_steps_to_complete_failure), , drop = FALSE]
    if (nrow(data) == 0L) {
      return(empty_plot("No selected cascade reached total failure, so conditional updates are not estimable."))
    }
  }

  plot <- plotly::plot_ly()
  for (target_value in c("Low", "High")) {
    group_data <- data[data$target == target_value, , drop = FALSE]
    group_data <- group_data[order(group_data$network_display), , drop = FALSE]
    if (nrow(group_data) == 0L) next

    if (outcome == "completion") {
      y_values <- group_data$percent_complete_failures
      labels <- paste0(format_number(y_values, 1), "%")
      hover <- paste0(
        "<b>", group_data$network_display, " · ", group_data$target_display, "</b><br>",
        "Reached total failure: <b>", format_count(group_data$n_complete_failures), "/",
        format_count(group_data$n_replications), " (", format_number(y_values, 1, "%"), ")</b>"
      )
    } else {
      y_values <- group_data$mean_steps_to_complete_failure
      labels <- format_number(y_values, 2)
      hover <- paste0(
        "<b>", group_data$network_display, " · ", group_data$target_display, "</b><br>",
        "Mean among ", format_count(group_data$n_complete_failures), " completions: <b>",
        format_number(y_values, 2, " updates"), "</b>"
      )
    }

    plot <- plotly::add_trace(
      plot,
      x = group_data$network_display,
      y = y_values,
      type = "bar",
      name = unname(target_labels[target_value]),
      text = labels,
      textposition = "outside",
      textfont = list(size = 12),
      cliponaxis = FALSE,
      customdata = hover,
      hovertemplate = "%{customdata}<extra></extra>",
      marker = list(
        color = unname(target_colors[target_value]),
        line = list(color = "#FFFFFF", width = 1)
      )
    )
  }

  if (outcome == "completion") {
    y_title <- "Replications reaching total failure"
    y_range <- c(0, 108)
    y_suffix <- "%"
  } else {
    y_title <- "Mean synchronous updates"
    y_max <- max(data$mean_steps_to_complete_failure, na.rm = TRUE)
    y_range <- c(0, y_max + max(1, 0.15 * y_max))
    y_suffix <- NULL
  }

  guided_plot_layout(
    plot,
    x_title = "Graph family",
    y_title = y_title,
    x_categories = unname(network_labels),
    y_range = y_range,
    y_suffix = y_suffix,
    show_legend = TRUE,
    barmode = "group"
  )
}
