| 1 |
# render.R — HTML rendering and display |
|
| 2 | ||
| 3 |
# ── Internal: locate and read the bundled JS ─────────────────────────────────── |
|
| 4 | ||
| 5 |
.bundle_js <- function() {
|
|
| 6 | 21x |
path <- system.file("qeviz.umd.js", package = "qeviz")
|
| 7 | 21x |
if (!nzchar(path) || !file.exists(path)) |
| 8 | ! |
stop("qeviz.umd.js not found in package inst/. Re-install the package.")
|
| 9 | 21x |
paste(readLines(path, warn = FALSE), collapse = "\n") |
| 10 |
} |
|
| 11 | ||
| 12 |
# ── Internal: accept either a raw rENA set, a pre-extracted ModelData list, |
|
| 13 |
# or a qe_plot object ──────────────────────────────────────────── |
|
| 14 |
# |
|
| 15 |
# • ena.set objects → qe_extract() with any forwarded args |
|
| 16 |
# • qe_plot objects → return the model slot directly |
|
| 17 |
# • anything else → assumed to be a ModelData list already |
|
| 18 |
.to_model <- function(x, ...) {
|
|
| 19 | 1x |
if (inherits(x, "qe_plot")) return(x$model) |
| 20 | ! |
if (inherits(x, "ena.set")) return(qe_extract(x, ...)) |
| 21 | 22x |
x |
| 22 |
} |
|
| 23 | ||
| 24 |
# ── Internal: resolve graph_opts from a qe_plot object or plain list ────────── |
|
| 25 |
# When x is a qe_plot, its stored opts are used as the base; the caller can |
|
| 26 |
# still pass graph_opts to override individual keys. |
|
| 27 |
.resolve_opts <- function(x, graph_opts = list()) {
|
|
| 28 | 21x |
base <- if (inherits(x, "qe_plot")) x$graph_opts else list() |
| 29 | 21x |
utils::modifyList(base, graph_opts) |
| 30 |
} |
|
| 31 | ||
| 32 |
# ── Internal: resolve title ─────────────────────────────────────────────────── |
|
| 33 |
.resolve_title <- function(x, title) {
|
|
| 34 | 1x |
if (!is.null(title)) return(title) |
| 35 | 1x |
if (inherits(x, "qe_plot")) x$title else "qeviz" |
| 36 |
} |
|
| 37 | ||
| 38 |
# ── Internal: build the <qe-graph> element with child layer elements ────────── |
|
| 39 |
# |
|
| 40 |
# Each declarative child element maps directly to a rendering layer: |
|
| 41 |
# <qe-nodes> — code-node labels (always rendered; controls label mode) |
|
| 42 |
# <qe-means> — group mean markers + CI boxes (omit to hide) |
|
| 43 |
# <qe-points> — unit scatter dots (omit to hide) |
|
| 44 |
# <qe-edges> — edge network (omit to hide) |
|
| 45 |
# <qe-trajectories> — ordered unit paths (omit to hide) |
|
| 46 |
# |
|
| 47 |
# Presence of a child = the layer renders. |
|
| 48 |
# Absence = the layer is hidden. |
|
| 49 |
# DOM order = SVG render order (later children draw on top). |
|
| 50 | ||
| 51 |
.build_graph_element <- function(opts) {
|
|
| 52 | 21x |
w <- opts[["width"]] %||% "100%" |
| 53 | 21x |
h <- opts[["height"]] %||% "440" |
| 54 | 21x |
children <- character(0) |
| 55 | ||
| 56 |
# ── <qe-nodes> — always emitted; controls node label mode ──────────────── |
|
| 57 | 21x |
node_label <- opts[["label-nodes"]] %||% "on" |
| 58 | 21x |
children <- c(children, |
| 59 | 21x |
sprintf(' <qe-nodes label="%s"></qe-nodes>', node_label))
|
| 60 | ||
| 61 |
# ── <qe-means> — show group means + optional CI boxes ──────────────────── |
|
| 62 |
# Omit entirely only if explicitly suppressed (not currently exposed in API; |
|
| 63 |
# defaults to present so `qe_plot(set)` always shows group means). |
|
| 64 | 21x |
means_parts <- character(0) |
| 65 | 21x |
if (!is.null(opts[["groups"]])) |
| 66 | ! |
means_parts <- c(means_parts, sprintf('groups="%s"', opts[["groups"]]))
|
| 67 | 21x |
if (!isFALSE(opts[["confidence"]])) |
| 68 | 21x |
means_parts <- c(means_parts, "confidence") |
| 69 | 21x |
means_label <- opts[["label-means"]] %||% "on" |
| 70 | 21x |
means_parts <- c(means_parts, sprintf('label="%s"', means_label))
|
| 71 | 21x |
children <- c(children, |
| 72 | 21x |
sprintf(' <qe-means %s></qe-means>', paste(means_parts, collapse = " ")))
|
| 73 | ||
| 74 |
# ── <qe-points> — unit scatter dots (only when points = TRUE) ──────────── |
|
| 75 | 21x |
if (isTRUE(opts[["points"]])) {
|
| 76 | ! |
pt_label <- opts[["label-points"]] %||% "auto" |
| 77 | ! |
children <- c(children, |
| 78 | ! |
sprintf(' <qe-points label="%s"></qe-points>', pt_label))
|
| 79 |
} |
|
| 80 | ||
| 81 |
# ── <qe-edges> — edge network (omit when no primary target or suppressed) ─ |
|
| 82 |
# Emitted only when at least one of group/unit is set (or compare/also are, |
|
| 83 |
# though those require a primary). The R chain functions validate this. |
|
| 84 | 21x |
if (!identical(opts[["edges"]], "false")) {
|
| 85 | 21x |
edge_parts <- character(0) |
| 86 | 21x |
if (!is.null(opts[["group"]])) |
| 87 | ! |
edge_parts <- c(edge_parts, sprintf('group="%s"', opts[["group"]]))
|
| 88 | 21x |
if (!is.null(opts[["unit"]])) |
| 89 | ! |
edge_parts <- c(edge_parts, sprintf('unit="%s"', opts[["unit"]]))
|
| 90 | 21x |
if (!is.null(opts[["compare"]])) |
| 91 | ! |
edge_parts <- c(edge_parts, sprintf('compare="%s"', opts[["compare"]]))
|
| 92 | 21x |
if (!is.null(opts[["also"]])) |
| 93 | ! |
edge_parts <- c(edge_parts, sprintf('also="%s"', opts[["also"]]))
|
| 94 | 21x |
if (length(edge_parts) > 0) {
|
| 95 | ! |
children <- c(children, |
| 96 | ! |
sprintf(' <qe-edges %s></qe-edges>', paste(edge_parts, collapse = " ")))
|
| 97 |
} |
|
| 98 |
} |
|
| 99 | ||
| 100 |
# ── <qe-trajectories> — ordered unit trajectories ─────────────────────── |
|
| 101 | 21x |
if (isTRUE(opts[["trajectories"]])) {
|
| 102 | 2x |
traj_parts <- character(0) |
| 103 | 2x |
if (!is.null(opts[["trajectory-units"]])) |
| 104 | 2x |
traj_parts <- c(traj_parts, sprintf('units="%s"', opts[["trajectory-units"]]))
|
| 105 | 2x |
traj_parts <- c(traj_parts, |
| 106 | 2x |
sprintf('smooth="%s"', opts[["trajectory-smooth"]] %||% "none"),
|
| 107 | 2x |
sprintf('max-degree="%s"', opts[["trajectory-max-degree"]] %||% "3"),
|
| 108 | 2x |
sprintf('degree-selection="%s"', opts[["trajectory-degree-selection"]] %||% "loocv_2d"),
|
| 109 | 2x |
sprintf('points="%s"', if (isFALSE(opts[["trajectory-points"]])) "false" else "true"),
|
| 110 | 2x |
sprintf('tips="%s"', if (isFALSE(opts[["trajectory-tips"]])) "false" else "true")
|
| 111 |
) |
|
| 112 | 2x |
if (!is.null(opts[["trajectory-playhead"]])) |
| 113 | ! |
traj_parts <- c(traj_parts, sprintf('playhead="%s"', opts[["trajectory-playhead"]]))
|
| 114 | 2x |
traj_parts <- c(traj_parts, |
| 115 | 2x |
sprintf('line-style="%s"', opts[["trajectory-line-style"]] %||% "uniform"),
|
| 116 | 2x |
sprintf('stroke-width="%s"', opts[["trajectory-stroke-width"]] %||% "2"),
|
| 117 | 2x |
sprintf('stroke-width-range="%s"', opts[["trajectory-stroke-width-range"]] %||% "1,6"),
|
| 118 | 2x |
sprintf('opacity-range="%s"', opts[["trajectory-opacity-range"]] %||% "0.3,1")
|
| 119 |
) |
|
| 120 | 2x |
children <- c(children, |
| 121 | 2x |
sprintf(' <qe-trajectories %s></qe-trajectories>', paste(traj_parts, collapse = " ")))
|
| 122 |
} |
|
| 123 | ||
| 124 | 21x |
children_str <- paste(children, collapse = "\n") |
| 125 | 21x |
sprintf('<qe-graph width="%s" height="%s">\n%s\n </qe-graph>', w, h, children_str)
|
| 126 |
} |
|
| 127 | ||
| 128 |
# ── Internal: build the full HTML document string ───────────────────────────── |
|
| 129 | ||
| 130 |
.build_html <- function(model_json, title = "qeviz", graph_opts = list()) {
|
|
| 131 | 13x |
bundle <- .bundle_js() |
| 132 | 13x |
graph_elem <- .build_graph_element(graph_opts) |
| 133 | ||
| 134 | 13x |
sprintf('<!DOCTYPE html>
|
| 135 | 13x |
<html lang="en"> |
| 136 | 13x |
<head> |
| 137 | 13x |
<meta charset="UTF-8"> |
| 138 | 13x |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| 139 | 13x |
<title>%s</title> |
| 140 | 13x |
<style> |
| 141 | 13x |
* { box-sizing: border-box; }
|
| 142 | 13x |
body { font-family: system-ui, sans-serif; margin: 0; padding: 0; background: #fff; }
|
| 143 | 13x |
qe-visual { display: block; width: 100%%; height: 480px; }
|
| 144 | 13x |
qe-graph { display: block; width: 100%%; height: 100%%; }
|
| 145 | 13x |
</style> |
| 146 | 13x |
</head> |
| 147 | 13x |
<body> |
| 148 | 13x |
<qe-visual id="vis"> |
| 149 | 13x |
%s |
| 150 | 13x |
</qe-visual> |
| 151 | 13x |
<script>%s</script> |
| 152 | 13x |
<script> |
| 153 | 13x |
const model = %s; |
| 154 | 13x |
document.getElementById("vis").setModelData(model);
|
| 155 | 13x |
</script> |
| 156 | 13x |
</body> |
| 157 | 13x |
</html>', title, graph_elem, bundle, model_json) |
| 158 |
} |
|
| 159 | ||
| 160 |
# ── NULL-coalescing helper ───────────────────────────────────────────────────── |
|
| 161 | 106x |
`%||%` <- function(a, b) if (!is.null(a)) a else b |
| 162 | ||
| 163 | ||
| 164 |
# ══ qe_plot object ════════════════════════════════════════════════════════════ |
|
| 165 | ||
| 166 |
#' Create a qeviz plot object |
|
| 167 |
#' |
|
| 168 |
#' Returns a `qe_plot` object that can be customised with chained `qe_*()` |
|
| 169 |
#' functions and rendered by printing, [qe_export_html()], or [qe_html()]. |
|
| 170 |
#' |
|
| 171 |
#' `width` and `height` set the plot dimensions and can be supplied here rather |
|
| 172 |
#' than in a separate sizing call. |
|
| 173 |
#' |
|
| 174 |
#' @param x Either a raw rENA / tma / ona set object **or** a `ModelData` |
|
| 175 |
#' list from [qe_model_data()] / [qe_extract()]. |
|
| 176 |
#' @param title Window / tab title used when the plot is saved or opened. |
|
| 177 |
#' @param width CSS width string. Default `"100%"`. |
|
| 178 |
#' @param height Plot height in pixels (numeric) or any CSS string. Default `440`. |
|
| 179 |
#' @param ... Additional arguments forwarded to [qe_extract()] when `x` is a |
|
| 180 |
#' raw set (e.g. `group_col = "Condition"`). Directionality is detected |
|
| 181 |
#' automatically from the set class and rarely needs to be overridden. |
|
| 182 |
#' |
|
| 183 |
#' @return An object of class `qe_plot`. |
|
| 184 |
#' |
|
| 185 |
#' @examples |
|
| 186 |
#' \dontrun{
|
|
| 187 |
#' qe_plot(set) |
|
| 188 |
#' qe_plot(set, height = 600) |> qe_group("FirstGame")
|
|
| 189 |
#' } |
|
| 190 |
#' |
|
| 191 |
#' @export |
|
| 192 |
qe_plot <- function(x, title = "qeviz", width = "100%", height = 440, ...) {
|
|
| 193 | 2x |
p <- structure( |
| 194 | 2x |
list( |
| 195 | 2x |
model = .to_model(x, ...), |
| 196 | 2x |
title = title, |
| 197 | 2x |
graph_opts = list() |
| 198 |
), |
|
| 199 | 2x |
class = "qe_plot" |
| 200 |
) |
|
| 201 | 2x |
p$graph_opts$width <- width |
| 202 | 2x |
p$graph_opts$height <- as.character(height) |
| 203 | 2x |
p |
| 204 |
} |
|
| 205 | ||
| 206 |
#' Render a qe_plot object in the viewer |
|
| 207 |
#' |
|
| 208 |
#' Called automatically when a `qe_plot` object is returned at the top level |
|
| 209 |
#' of the console. Writes to a temp file and opens the RStudio viewer or |
|
| 210 |
#' the system browser. |
|
| 211 |
#' |
|
| 212 |
#' @param x A `qe_plot` object. |
|
| 213 |
#' @param ... Ignored. |
|
| 214 |
#' @export |
|
| 215 |
print.qe_plot <- function(x, ...) {
|
|
| 216 | ! |
tmp <- tempfile(fileext = ".html") |
| 217 | ! |
qe_export_html(x, tmp, open = TRUE) |
| 218 | ! |
invisible(x) |
| 219 |
} |
|
| 220 | ||
| 221 | ||
| 222 |
# ══ Chain functions ════════════════════════════════════════════════════════════ |
|
| 223 | ||
| 224 |
#' Select which group means to display |
|
| 225 |
#' |
|
| 226 |
#' Controls which groups' mean markers and CI boxes appear in the plot. This |
|
| 227 |
#' is independent of edge rendering — use [qe_edges()] to select whose network |
|
| 228 |
#' to draw. |
|
| 229 |
#' |
|
| 230 |
#' @param p A `qe_plot` object. |
|
| 231 |
#' @param ... Group names to show means for. Supply one or more strings, |
|
| 232 |
#' each matching a value in the model's group column. Omit entirely to show |
|
| 233 |
#' means for **all** groups in the model (the default). |
|
| 234 |
#' @param by The metadata column used for grouping (e.g. `"Condition"`). |
|
| 235 |
#' Optional — used to validate against the column baked into the model at |
|
| 236 |
#' extraction time. An error is raised if the column name does not match. |
|
| 237 |
#' @param intervals Logical. Show 95% CI box(es) around group mean(s). |
|
| 238 |
#' Default `TRUE`. |
|
| 239 |
#' |
|
| 240 |
#' @return The modified `qe_plot` object. |
|
| 241 |
#' |
|
| 242 |
#' @examples |
|
| 243 |
#' \dontrun{
|
|
| 244 |
#' # Show means for all groups (default) |
|
| 245 |
#' qe_plot(set) |> qe_group() |
|
| 246 |
#' |
|
| 247 |
#' # Show means for specific groups only |
|
| 248 |
#' qe_plot(set) |> qe_group("FirstGame", "SecondGame")
|
|
| 249 |
#' |
|
| 250 |
#' # Explicit column name — validates against model |
|
| 251 |
#' qe_plot(set) |> qe_group("FirstGame", by = "Condition")
|
|
| 252 |
#' } |
|
| 253 |
#' |
|
| 254 |
#' @export |
|
| 255 |
qe_group <- function(p, ..., by = NULL, intervals = TRUE) {
|
|
| 256 | ! |
requested <- c(...) # character vector of group names, or length-0 |
| 257 | ||
| 258 |
# Validate `by` against the model's group_col |
|
| 259 | ! |
if (!is.null(by)) {
|
| 260 | ! |
model_gcol <- if (inherits(p, "qe_plot")) p$model$group_col else p$group_col |
| 261 | ! |
if (!is.null(model_gcol) && !identical(by, model_gcol)) {
|
| 262 | ! |
stop(sprintf( |
| 263 | ! |
"qe_group(): by = %s does not match the model's group column (%s).\n", |
| 264 | ! |
dQuote(by), dQuote(model_gcol) |
| 265 | ! |
), call. = FALSE) |
| 266 |
} |
|
| 267 |
} |
|
| 268 | ||
| 269 |
# Validate requested group names against model |
|
| 270 | ! |
if (length(requested) > 0) {
|
| 271 | ! |
available <- qe_groups(p) |
| 272 | ! |
bad <- requested[!requested %in% available] |
| 273 | ! |
if (length(bad) > 0) {
|
| 274 | ! |
stop(sprintf( |
| 275 | ! |
"qe_group(): group(s) not found in model: %s\nAvailable: %s", |
| 276 | ! |
paste(dQuote(bad), collapse = ", "), |
| 277 | ! |
paste(dQuote(available), collapse = ", ") |
| 278 | ! |
), call. = FALSE) |
| 279 |
} |
|
| 280 |
# Comma-separated list → `groups` attribute (means filter) |
|
| 281 | ! |
p$graph_opts$groups <- paste(requested, collapse = ",") |
| 282 |
} else {
|
|
| 283 |
# No names → show all groups; remove any previous filter |
|
| 284 | ! |
p$graph_opts$groups <- NULL |
| 285 |
} |
|
| 286 | ||
| 287 | ! |
p$graph_opts$confidence <- intervals |
| 288 | ! |
p |
| 289 |
} |
|
| 290 | ||
| 291 |
#' Select which edge network to draw |
|
| 292 |
#' |
|
| 293 |
#' Controls edge rendering. Supply **either** a group name (`group`) **or** a |
|
| 294 |
#' specific unit ID (`unit`) as the primary edge source; `compare` or `also` |
|
| 295 |
#' optionally bring in a second group for subtraction or overlay. Omitting both |
|
| 296 |
#' `group` and `unit` (or calling `qe_edges(show = FALSE)`) suppresses the edge |
|
| 297 |
#' network entirely, leaving only group means and unit points visible. |
|
| 298 |
#' |
|
| 299 |
#' Use [qe_units()] to inspect the unit IDs present in the model. |
|
| 300 |
#' |
|
| 301 |
#' @param p A `qe_plot` object. |
|
| 302 |
#' @param group Name of the primary group whose **mean** edge network to draw. |
|
| 303 |
#' Must match a value in the model's group column. Mutually exclusive with |
|
| 304 |
#' `unit`. |
|
| 305 |
#' @param unit ID of a single unit whose **individual** edge network to draw |
|
| 306 |
#' (e.g. `"FirstGame::steven z"`). Mutually exclusive with `group`. |
|
| 307 |
#' `compare` and `also` are not supported when `unit` is supplied. |
|
| 308 |
#' @param compare Name of a second group to subtract (`group − compare`). |
|
| 309 |
#' Positive differences use the primary colour; negative use the secondary. |
|
| 310 |
#' Cannot be combined with `unit`. |
|
| 311 |
#' @param also Name of a second group to overlay alongside `group`. |
|
| 312 |
#' Cannot be combined with `unit`. |
|
| 313 |
#' @param show Logical. `FALSE` suppresses all edge rendering regardless of |
|
| 314 |
#' other arguments. Default `TRUE`. |
|
| 315 |
#' |
|
| 316 |
#' @return The modified `qe_plot` object. |
|
| 317 |
#' |
|
| 318 |
#' @examples |
|
| 319 |
#' \dontrun{
|
|
| 320 |
#' # Single group mean network |
|
| 321 |
#' qe_plot(set) |> qe_edges("FirstGame")
|
|
| 322 |
#' |
|
| 323 |
#' # Individual unit network |
|
| 324 |
#' qe_plot(set) |> qe_edges(unit = "FirstGame::steven z") |
|
| 325 |
#' |
|
| 326 |
#' # Individual unit + show all group means |
|
| 327 |
#' qe_plot(set) |> qe_group() |> qe_edges(unit = "FirstGame::steven z") |
|
| 328 |
#' |
|
| 329 |
#' # Subtraction |
|
| 330 |
#' qe_plot(set) |> qe_edges("FirstGame", compare = "SecondGame")
|
|
| 331 |
#' |
|
| 332 |
#' # Overlay |
|
| 333 |
#' qe_plot(set) |> qe_edges("FirstGame", also = "SecondGame")
|
|
| 334 |
#' |
|
| 335 |
#' # No edges (means and/or points only) |
|
| 336 |
#' qe_plot(set) |> qe_group() |> qe_edges(show = FALSE) |
|
| 337 |
#' } |
|
| 338 |
#' |
|
| 339 |
#' @export |
|
| 340 |
qe_edges <- function(p, group = NULL, unit = NULL, |
|
| 341 |
compare = NULL, also = NULL, show = TRUE) {
|
|
| 342 | ! |
if (!is.null(group) && !is.null(unit)) |
| 343 | ! |
stop("qe_edges(): supply either 'group' or 'unit', not both.", call. = FALSE)
|
| 344 | ||
| 345 | ! |
if (!show) {
|
| 346 |
# TypeScript reads getAttribute('edges') !== 'false', so we must emit the
|
|
| 347 |
# string "false" — a logical FALSE would be omitted and edges would stay on. |
|
| 348 | ! |
p$graph_opts$edges <- "false" |
| 349 | ! |
p$graph_opts$group <- NULL |
| 350 | ! |
p$graph_opts$unit <- NULL |
| 351 | ! |
p$graph_opts$compare <- NULL |
| 352 | ! |
p$graph_opts$also <- NULL |
| 353 | ! |
return(p) |
| 354 |
} |
|
| 355 | ||
| 356 | ! |
available_groups <- qe_groups(p) |
| 357 | ! |
available_units <- qe_units(p) |
| 358 | ||
| 359 |
# Validate group-based targets |
|
| 360 | ! |
.check_group <- function(name, arg) {
|
| 361 | ! |
if (!is.null(name) && !name %in% available_groups) {
|
| 362 | ! |
stop(sprintf( |
| 363 | ! |
"qe_edges(): %s = %s not found in model.\nAvailable groups: %s", |
| 364 | ! |
arg, dQuote(name), |
| 365 | ! |
paste(dQuote(available_groups), collapse = ", ") |
| 366 | ! |
), call. = FALSE) |
| 367 |
} |
|
| 368 |
} |
|
| 369 | ! |
.check_group(group, "group") |
| 370 | ! |
.check_group(compare, "compare") |
| 371 | ! |
.check_group(also, "also") |
| 372 | ||
| 373 |
# Validate unit ID |
|
| 374 | ! |
if (!is.null(unit) && !unit %in% available_units) {
|
| 375 | ! |
stop(sprintf( |
| 376 | ! |
"qe_edges(): unit = %s not found in model.\nUse qe_units() to see available unit IDs.", |
| 377 | ! |
dQuote(unit) |
| 378 | ! |
), call. = FALSE) |
| 379 |
} |
|
| 380 | ||
| 381 | ! |
if (!is.null(compare) && !is.null(also)) |
| 382 | ! |
stop("qe_edges(): supply either 'compare' or 'also', not both.", call. = FALSE)
|
| 383 | ||
| 384 | ! |
if (!is.null(unit) && (!is.null(compare) || !is.null(also))) |
| 385 | ! |
stop("qe_edges(): 'compare' and 'also' cannot be combined with 'unit'.", call. = FALSE)
|
| 386 | ||
| 387 | ! |
p$graph_opts$edges <- TRUE |
| 388 | ! |
p$graph_opts$group <- group |
| 389 | ! |
p$graph_opts$unit <- unit |
| 390 | ! |
p$graph_opts$compare <- compare |
| 391 | ! |
p$graph_opts$also <- also |
| 392 | ! |
p |
| 393 |
} |
|
| 394 | ||
| 395 |
#' Show or hide unit points |
|
| 396 |
#' |
|
| 397 |
#' @param p A `qe_plot` object. |
|
| 398 |
#' @param show Logical. Default `TRUE`. |
|
| 399 |
#' @return The modified `qe_plot` object. |
|
| 400 |
#' @export |
|
| 401 |
qe_points <- function(p, show = TRUE) {
|
|
| 402 | ! |
p$graph_opts$points <- show |
| 403 | ! |
p |
| 404 |
} |
|
| 405 | ||
| 406 |
#' Show or hide trajectory paths |
|
| 407 |
#' |
|
| 408 |
#' Adds a `<qe-trajectories>` layer to the rendered graph. QEviz only renders |
|
| 409 |
#' supplied trajectory rows; accumulation, dooring, model selection, and |
|
| 410 |
#' inference remain the caller's responsibility. |
|
| 411 |
#' |
|
| 412 |
#' @param p A `qe_plot` object. |
|
| 413 |
#' @param units Optional character vector of unit IDs to show. Omit to show all |
|
| 414 |
#' units present in the model's `trajectories` frame. |
|
| 415 |
#' @param smooth Path treatment: `"none"` for observed steps or `"poly"` for |
|
| 416 |
#' polynomial smoothing. |
|
| 417 |
#' @param max_degree Upper bound on polynomial degree when `smooth = "poly"`. |
|
| 418 |
#' With `degree_selection = "loocv_2d"` the actual degree is chosen in |
|
| 419 |
#' `1..max_degree`; with `"fixed"` this degree is always used. |
|
| 420 |
#' @param degree_selection Degree selection when `smooth = "poly"`. `"loocv_2d"` |
|
| 421 |
#' (default) picks the degree by joint 2D leave-one-out cross-validation; |
|
| 422 |
#' `"fixed"` always uses `max_degree`. Near-origin points are excluded from the |
|
| 423 |
#' fit (still drawn), matching `ena.plot.trajectory()`. |
|
| 424 |
#' @param points Logical. Show observed state dots. |
|
| 425 |
#' @param tips Logical. Show the leading current-state marker. |
|
| 426 |
#' @param playhead Optional numeric time value. Rows after this value are hidden. |
|
| 427 |
#' @param line_style `"uniform"` or `"tapered"`. |
|
| 428 |
#' @param stroke_width Uniform stroke width in display pixels. |
|
| 429 |
#' @param stroke_width_range Numeric length-2 vector for tapered paths. |
|
| 430 |
#' @param opacity_range Numeric length-2 vector for tapered paths. |
|
| 431 |
#' @param show Logical. `FALSE` suppresses trajectory rendering. |
|
| 432 |
#' |
|
| 433 |
#' @return The modified `qe_plot` object. |
|
| 434 |
#' |
|
| 435 |
#' @export |
|
| 436 |
qe_trajectories <- function(p, units = NULL, smooth = c("none", "poly"),
|
|
| 437 |
max_degree = 3L, |
|
| 438 |
degree_selection = c("loocv_2d", "fixed"),
|
|
| 439 |
points = TRUE, tips = TRUE, |
|
| 440 |
playhead = NULL, |
|
| 441 |
line_style = c("uniform", "tapered"),
|
|
| 442 |
stroke_width = 2, |
|
| 443 |
stroke_width_range = c(1, 6), |
|
| 444 |
opacity_range = c(0.3, 1), |
|
| 445 |
show = TRUE) {
|
|
| 446 | 3x |
if (!show) {
|
| 447 | ! |
p$graph_opts$trajectories <- FALSE |
| 448 | ! |
return(p) |
| 449 |
} |
|
| 450 | ||
| 451 | 3x |
smooth <- match.arg(smooth) |
| 452 | 3x |
degree_selection <- match.arg(degree_selection) |
| 453 | 2x |
line_style <- match.arg(line_style) |
| 454 | ||
| 455 | 2x |
if (!is.null(units)) {
|
| 456 | 2x |
available <- qe_trajectory_units(p) |
| 457 | 2x |
bad <- units[!units %in% available] |
| 458 | 2x |
if (length(bad) > 0) {
|
| 459 | 1x |
stop(sprintf( |
| 460 | 1x |
"qe_trajectories(): unit(s) not found in model: %s\nAvailable: %s", |
| 461 | 1x |
paste(dQuote(bad), collapse = ", "), |
| 462 | 1x |
paste(dQuote(available), collapse = ", ") |
| 463 | 1x |
), call. = FALSE) |
| 464 |
} |
|
| 465 |
} |
|
| 466 | ||
| 467 | 1x |
p$graph_opts$trajectories <- TRUE |
| 468 | 1x |
p$graph_opts[["trajectory-units"]] <- if (is.null(units)) NULL else paste(units, collapse = ",") |
| 469 | 1x |
p$graph_opts[["trajectory-smooth"]] <- smooth |
| 470 | 1x |
p$graph_opts[["trajectory-max-degree"]] <- as.integer(max_degree) |
| 471 | 1x |
p$graph_opts[["trajectory-degree-selection"]] <- degree_selection |
| 472 | 1x |
p$graph_opts[["trajectory-points"]] <- isTRUE(points) |
| 473 | 1x |
p$graph_opts[["trajectory-tips"]] <- isTRUE(tips) |
| 474 | 1x |
p$graph_opts[["trajectory-playhead"]] <- playhead |
| 475 | 1x |
p$graph_opts[["trajectory-line-style"]] <- line_style |
| 476 | 1x |
p$graph_opts[["trajectory-stroke-width"]] <- stroke_width |
| 477 | 1x |
p$graph_opts[["trajectory-stroke-width-range"]] <- paste(stroke_width_range, collapse = ",") |
| 478 | 1x |
p$graph_opts[["trajectory-opacity-range"]] <- paste(opacity_range, collapse = ",") |
| 479 | 1x |
p |
| 480 |
} |
|
| 481 | ||
| 482 |
#' Control label visibility |
|
| 483 |
#' |
|
| 484 |
#' @param p A `qe_plot` object. |
|
| 485 |
#' @param nodes Label mode for code nodes. One of `"on"`, `"off"`, |
|
| 486 |
#' `"click"`, `"auto"`. Default `"on"`. |
|
| 487 |
#' @param means Label mode for group means. Default `"on"`. |
|
| 488 |
#' @param points Label mode for unit points. Default `"auto"`. |
|
| 489 |
#' @return The modified `qe_plot` object. |
|
| 490 |
#' @export |
|
| 491 |
qe_labels <- function(p, nodes = "on", means = "on", points = "auto") {
|
|
| 492 | ! |
p$graph_opts[["label-nodes"]] <- nodes |
| 493 | ! |
p$graph_opts[["label-means"]] <- means |
| 494 | ! |
p$graph_opts[["label-points"]] <- points |
| 495 | ! |
p |
| 496 |
} |
|
| 497 | ||
| 498 | ||
| 499 |
# ══ Output functions ═══════════════════════════════════════════════════════════ |
|
| 500 |
# All three accept a qe_plot object, a raw ena.set, or a plain ModelData list. |
|
| 501 | ||
| 502 |
#' Return a qeviz plot as an HTML string |
|
| 503 |
#' |
|
| 504 |
#' @param x A `qe_plot` object, a raw rENA / tma / ona set, or a |
|
| 505 |
#' `ModelData` list from [qe_model_data()] / [qe_extract()]. |
|
| 506 |
#' @param title `<title>` text. Defaults to the title stored in the |
|
| 507 |
#' `qe_plot` object when one is supplied. |
|
| 508 |
#' @param graph_opts Named list of `<qe-graph>` attribute overrides. When `x` |
|
| 509 |
#' is a `qe_plot` object the stored opts are used as the base; keys supplied |
|
| 510 |
#' here take precedence. |
|
| 511 |
#' @param ... Additional arguments forwarded to [qe_extract()] when `x` |
|
| 512 |
#' is a raw set. |
|
| 513 |
#' @return A single character string containing a complete HTML document. |
|
| 514 |
#' @examples |
|
| 515 |
#' \dontrun{
|
|
| 516 |
#' writeLines(qe_html(set), "plot.html") |
|
| 517 |
#' writeLines(qe_html(qe_plot(set) |> qe_group("FirstGame")), "plot.html")
|
|
| 518 |
#' } |
|
| 519 |
#' @export |
|
| 520 |
qe_html <- function(x, title = NULL, graph_opts = list(), ...) {
|
|
| 521 | 13x |
model <- .to_model(x, ...) |
| 522 | 13x |
opts <- .resolve_opts(x, graph_opts) |
| 523 | 13x |
ttl <- .resolve_title(x, title) |
| 524 | 13x |
json <- jsonlite::toJSON(model, auto_unbox = TRUE, digits = 6, null = "null") |
| 525 | 13x |
.build_html(json, title = ttl, graph_opts = opts) |
| 526 |
} |
|
| 527 | ||
| 528 |
#' Write a self-contained qeviz HTML file |
|
| 529 |
#' |
|
| 530 |
#' @param x A `qe_plot` object, a raw rENA / tma / ona set, or a |
|
| 531 |
#' `ModelData` list. |
|
| 532 |
#' @param file Output file path (should end in `.html`). |
|
| 533 |
#' @param title `<title>` text (defaults to stored title for `qe_plot` objects). |
|
| 534 |
#' @param graph_opts Named list of `<qe-graph>` attribute overrides. |
|
| 535 |
#' @param open If `TRUE` (default when interactive), open after writing. |
|
| 536 |
#' @param ... Additional arguments forwarded to [qe_extract()] when `x` |
|
| 537 |
#' is a raw set. |
|
| 538 |
#' @return The normalised `file` path, invisibly. |
|
| 539 |
#' @examples |
|
| 540 |
#' \dontrun{
|
|
| 541 |
#' qe_export_html(set, "ena_plot.html") |
|
| 542 |
#' qe_export_html(qe_plot(set) |> qe_group("FirstGame"), "fg.html")
|
|
| 543 |
#' } |
|
| 544 |
#' @export |
|
| 545 |
qe_export_html <- function(x, file, title = NULL, graph_opts = list(), |
|
| 546 |
open = interactive(), ...) {
|
|
| 547 | 4x |
html <- qe_html(x, title = title, graph_opts = graph_opts, ...) |
| 548 | 4x |
writeLines(html, file) |
| 549 | ! |
if (open) .open_viewer(file) |
| 550 | 4x |
invisible(normalizePath(file)) |
| 551 |
} |
|
| 552 | ||
| 553 |
#' Return a qeviz plot as an embeddable HTML fragment |
|
| 554 |
#' |
|
| 555 |
#' Returns a `<div>` with the inlined bundle and a `<qe-visual>` / `<qe-graph>` |
|
| 556 |
#' pair — no `<html>` wrapper, suitable for R Markdown / Quarto via |
|
| 557 |
#' `htmltools::HTML()`. |
|
| 558 |
#' |
|
| 559 |
#' @param x A `qe_plot` object, a raw rENA / tma / ona set, or a |
|
| 560 |
#' `ModelData` list. |
|
| 561 |
#' @param height CSS height of the containing `<div>`. Default `"480px"`. |
|
| 562 |
#' @param graph_opts Named list of `<qe-graph>` attribute overrides. |
|
| 563 |
#' @param ... Additional arguments forwarded to [qe_extract()] when `x` |
|
| 564 |
#' is a raw set. |
|
| 565 |
#' @return A character string of class `"html"`. |
|
| 566 |
#' @examples |
|
| 567 |
#' \dontrun{
|
|
| 568 |
#' htmltools::HTML(qe_fragment(set)) |
|
| 569 |
#' htmltools::HTML(qe_fragment(qe_plot(set) |> qe_group("FirstGame")))
|
|
| 570 |
#' } |
|
| 571 |
#' @export |
|
| 572 |
qe_fragment <- function(x, height = "480px", graph_opts = list(), ...) {
|
|
| 573 | 8x |
model <- .to_model(x, ...) |
| 574 | 8x |
opts <- .resolve_opts(x, graph_opts) |
| 575 | 8x |
json <- jsonlite::toJSON(model, auto_unbox = TRUE, digits = 6, null = "null") |
| 576 | 8x |
bundle <- .bundle_js() |
| 577 | 8x |
graph_elem <- .build_graph_element(opts) |
| 578 | 8x |
uid <- paste0("qeviz", as.integer(proc.time()[["elapsed"]] * 1e4) %% 1e9L)
|
| 579 | ||
| 580 | 8x |
html <- sprintf( |
| 581 | 8x |
'<div style="width:100%%;height:%s">\n<script>%s</script>\n<qe-visual id="%s">\n %s\n</qe-visual>\n<script>\ndocument.getElementById("%s").setModelData(%s);\n</script>\n</div>',
|
| 582 | 8x |
height, bundle, uid, graph_elem, uid, json |
| 583 |
) |
|
| 584 | 8x |
structure(html, class = c("html", "character"))
|
| 585 |
} |
|
| 586 | ||
| 587 |
# ── Internal: open a file in RStudio viewer or browser ──────────────────────── |
|
| 588 | ||
| 589 |
.open_viewer <- function(path) {
|
|
| 590 | ! |
path <- normalizePath(path) |
| 591 | ! |
if (requireNamespace("rstudioapi", quietly = TRUE) &&
|
| 592 | ! |
rstudioapi::isAvailable() && |
| 593 | ! |
rstudioapi::hasFun("viewer")) {
|
| 594 | ! |
rstudioapi::viewer(path) |
| 595 |
} else {
|
|
| 596 | ! |
utils::browseURL(paste0("file://", path))
|
| 597 |
} |
|
| 598 | ! |
invisible(path) |
| 599 |
} |
| 1 |
# frames.R — low-level helpers for building QEFrame structures |
|
| 2 |
# |
|
| 3 |
# A QEFrame is the wire format qeviz expects for every data table: |
|
| 4 |
# { data: [ {col: val, ...}, ... ], types: { col: "character"|"numeric"|"integer"|"logical" } }
|
|
| 5 | ||
| 6 |
# ── Internal helpers ─────────────────────────────────────────────────────────── |
|
| 7 | ||
| 8 |
#' Convert a data.frame to a QEFrame list |
|
| 9 |
#' |
|
| 10 |
#' Serialises a data.frame into the `{ data, types }` structure expected by
|
|
| 11 |
#' qeviz's `ModelData` interface. Factors are coerced to character. |
|
| 12 |
#' |
|
| 13 |
#' @param df A `data.frame` (or object coercible via `as.data.frame()`). |
|
| 14 |
#' @return A named list with elements `data` (list of row lists) and `types` |
|
| 15 |
#' (named character vector of column types). |
|
| 16 |
#' @keywords internal |
|
| 17 |
qe_frame <- function(df) {
|
|
| 18 | 80x |
df <- as.data.frame(df) |
| 19 | 80x |
types <- vapply(df, function(col) {
|
| 20 | 123x |
if (is.character(col) || is.factor(col)) "character" |
| 21 | 1x |
else if (is.logical(col)) "logical" |
| 22 | 5x |
else if (is.integer(col)) "integer" |
| 23 | 309x |
else "numeric" |
| 24 | 80x |
}, character(1L)) |
| 25 | 80x |
rows <- lapply(seq_len(nrow(df)), function(i) {
|
| 26 | 571x |
r <- as.list(df[i, , drop = FALSE]) |
| 27 | 571x |
lapply(r, function(v) if (is.factor(v)) as.character(v) else v) |
| 28 |
}) |
|
| 29 | 80x |
list(data = rows, types = as.list(types)) |
| 30 |
} |
|
| 31 | ||
| 32 |
#' Return the names of the first two numeric columns |
|
| 33 |
#' |
|
| 34 |
#' Used to locate the rotated dimension axes (e.g. `SVD1`, `SVD2`) in a |
|
| 35 |
#' nodes or points data.frame without hardcoding column names. |
|
| 36 |
#' |
|
| 37 |
#' @param df A `data.frame`. |
|
| 38 |
#' @return Character vector of length 2. |
|
| 39 |
#' @keywords internal |
|
| 40 |
find_dims <- function(df) {
|
|
| 41 | 10x |
nm <- names(df)[vapply(df, is.numeric, logical(1L))] |
| 42 | 10x |
if (length(nm) < 2L) |
| 43 | 1x |
stop("Expected at least two numeric dimension columns in data.frame.")
|
| 44 | 9x |
nm[1:2] |
| 45 |
} |
|
| 46 | ||
| 47 |
#' Compute per-group means and 95% CI bounds |
|
| 48 |
#' |
|
| 49 |
#' For each group, computes the mean x/y position and a rectangular 95% |
|
| 50 |
#' confidence interval using `t(df = n-1, 0.975) * SE` on each axis |
|
| 51 |
#' independently. This matches the CI convention used by rENA / Web ENA. |
|
| 52 |
#' |
|
| 53 |
#' @param pts A `data.frame` with at least the columns named by `x_col`, |
|
| 54 |
#' `y_col`, and `group_col`. |
|
| 55 |
#' @param x_col Name of the x-coordinate column. |
|
| 56 |
#' @param y_col Name of the y-coordinate column. |
|
| 57 |
#' @param group_col Name of the grouping column. |
|
| 58 |
#' @return A `data.frame` with columns `group`, `x`, `y`, `x.low`, `x.high`, |
|
| 59 |
#' `y.low`, `y.high`. |
|
| 60 |
#' @keywords internal |
|
| 61 |
group_summary <- function(pts, x_col, y_col, group_col) {
|
|
| 62 | 22x |
groups <- sort(unique(pts[[group_col]])) |
| 63 | 22x |
do.call(rbind, lapply(groups, function(g) {
|
| 64 | 43x |
sub <- pts[pts[[group_col]] == g, ] |
| 65 | 43x |
n <- nrow(sub) |
| 66 | 43x |
t_v <- stats::qt(0.975, df = max(1L, n - 1L)) |
| 67 | 43x |
x <- sub[[x_col]]; y <- sub[[y_col]] |
| 68 | 43x |
mx <- mean(x, na.rm = TRUE); my <- mean(y, na.rm = TRUE) |
| 69 | 43x |
se_x <- stats::sd(x, na.rm = TRUE) / sqrt(n) |
| 70 | 43x |
se_y <- stats::sd(y, na.rm = TRUE) / sqrt(n) |
| 71 | 43x |
row <- data.frame( |
| 72 | 43x |
x = mx, y = my, |
| 73 | 43x |
x.low = mx - t_v * se_x, x.high = mx + t_v * se_x, |
| 74 | 43x |
y.low = my - t_v * se_y, y.high = my + t_v * se_y, |
| 75 | 43x |
stringsAsFactors = FALSE |
| 76 |
) |
|
| 77 | 43x |
row[[group_col]] <- g |
| 78 | 43x |
row[, c(group_col, "x", "y", "x.low", "x.high", "y.low", "y.high")] |
| 79 |
})) |
|
| 80 |
} |
| 1 |
# model.R — build ModelData structures from data.frames or rENA/tma set objects |
|
| 2 | ||
| 3 |
#' Build a qeviz ModelData list from data.frames |
|
| 4 |
#' |
|
| 5 |
#' Assembles the `ModelData` structure consumed by `<qe-visual>` from |
|
| 6 |
#' pre-prepared data.frames. All data.frames are converted to QEFrames |
|
| 7 |
#' (see [qe_frame()]). |
|
| 8 |
#' |
|
| 9 |
#' @param nodes `data.frame` of code-node positions. Must contain columns |
|
| 10 |
#' `node_id_col`, `x_col`, `y_col`. |
|
| 11 |
#' @param edges `data.frame` of edge weights. Must contain `id_col` plus |
|
| 12 |
#' one column per edge pair (e.g. `Data.Collaboration`). |
|
| 13 |
#' @param points `data.frame` of unit positions. Must contain `id_col`, |
|
| 14 |
#' `group_col`, `x_col`, `y_col`. |
|
| 15 |
#' @param trajectories Optional ordered observations for one or more units. |
|
| 16 |
#' Must contain `id_col`, `x_col`, `y_col`, and ideally `time`, `RowID`, or |
|
| 17 |
#' `ActivityNumber` for playhead ordering. |
|
| 18 |
#' @param groups `data.frame` of group summary statistics as returned by |
|
| 19 |
#' [group_summary()], or `NULL` to compute automatically from `points`. |
|
| 20 |
#' @param directed Logical. `TRUE` for ONA (ordered/directed) models. |
|
| 21 |
#' @param id_col Name of the unit-ID column in `edges` and `points`. |
|
| 22 |
#' @param node_id_col Name of the code-ID column in `nodes`. |
|
| 23 |
#' @param x_col Name of the x-coordinate column. |
|
| 24 |
#' @param y_col Name of the y-coordinate column. |
|
| 25 |
#' @param group_col Name of the group column in `points`. |
|
| 26 |
#' |
|
| 27 |
#' @return A named list suitable for serialisation with [jsonlite::toJSON()] |
|
| 28 |
#' and passing to `qe_export_html()` or `qe_plot()`. |
|
| 29 |
#' |
|
| 30 |
#' @examples |
|
| 31 |
#' \dontrun{
|
|
| 32 |
#' model <- qe_model_data(nodes_df, edges_df, points_df, directed = FALSE) |
|
| 33 |
#' qe_plot(model) |
|
| 34 |
#' } |
|
| 35 |
#' |
|
| 36 |
#' @export |
|
| 37 |
qe_model_data <- function( |
|
| 38 |
nodes, |
|
| 39 |
edges, |
|
| 40 |
points = NULL, |
|
| 41 |
trajectories = NULL, |
|
| 42 |
groups = NULL, |
|
| 43 |
directed = FALSE, |
|
| 44 |
id_col = "ENA_UNIT", |
|
| 45 |
node_id_col = "code", |
|
| 46 |
x_col = "x", |
|
| 47 |
y_col = "y", |
|
| 48 |
group_col = "Condition" |
|
| 49 |
) {
|
|
| 50 | 21x |
if (!is.null(points) && is.null(groups)) {
|
| 51 | 8x |
groups <- group_summary(as.data.frame(points), x_col, y_col, group_col) |
| 52 |
} |
|
| 53 | 21x |
result <- list( |
| 54 | 21x |
directed = directed, |
| 55 | 21x |
updated = as.numeric(Sys.time()) * 1000L, |
| 56 | 21x |
id_col = id_col, |
| 57 | 21x |
node_id_col = node_id_col, |
| 58 | 21x |
x_col = x_col, |
| 59 | 21x |
y_col = y_col, |
| 60 | 21x |
group_col = group_col, |
| 61 | 21x |
nodes = qe_frame(nodes), |
| 62 | 21x |
edges = qe_frame(edges) |
| 63 |
) |
|
| 64 | 15x |
if (!is.null(points)) result$points <- qe_frame(points) |
| 65 | 3x |
if (!is.null(trajectories)) result$trajectories <- qe_frame(trajectories) |
| 66 | 15x |
if (!is.null(groups)) result$groups <- qe_frame(groups) |
| 67 | 21x |
result |
| 68 |
} |
|
| 69 | ||
| 70 |
#' Extract a qeviz ModelData list from an rENA / tma model object |
|
| 71 |
#' |
|
| 72 |
#' Reads nodes, points, edges, and group membership from a set object |
|
| 73 |
#' produced by `rENA::model()` (tma pipeline) and returns a `ModelData` |
|
| 74 |
#' list ready for rendering. |
|
| 75 |
#' |
|
| 76 |
#' The set object must expose: |
|
| 77 |
#' * `set$rotation$nodes` — code positions data.frame with a `code` column |
|
| 78 |
#' * `set$points` — unit positions data.frame with `ENA_UNIT`, |
|
| 79 |
#' `Condition`, and rotated-dimension columns |
|
| 80 |
#' * `set$line.weights` — edge-weight columns (one per code pair) |
|
| 81 |
#' * `set$meta.data` — unit metadata including `ENA_UNIT`, `Condition` |
|
| 82 |
#' |
|
| 83 |
#' Edge column names are normalised: `" & "` separators are replaced with |
|
| 84 |
#' `"."` so qeviz's edge parser can resolve them (e.g. `"Data & Collab"` |
|
| 85 |
#' becomes `"Data.Collab"`). |
|
| 86 |
#' |
|
| 87 |
#' @param set A model set object from `rENA::model()` or `ona::model()`. |
|
| 88 |
#' @param directed Logical, or `NULL` (default). When `NULL`, directionality |
|
| 89 |
#' is detected automatically: sets with class `"ena.ordered.set"` (ONA) are |
|
| 90 |
#' treated as directed; standard ENA sets are treated as undirected. Pass |
|
| 91 |
#' `TRUE` or `FALSE` explicitly only if you need to override the default. |
|
| 92 |
#' @param id_col Unit-ID column name in the set's meta/points frames. |
|
| 93 |
#' @param node_id_col Code-ID column name in the nodes frame. |
|
| 94 |
#' @param group_col Group column name. |
|
| 95 |
#' |
|
| 96 |
#' @return A `ModelData` list (see [qe_model_data()]). |
|
| 97 |
#' |
|
| 98 |
#' @examples |
|
| 99 |
#' \dontrun{
|
|
| 100 |
#' library(tma); library(rENA) |
|
| 101 |
#' data(RS.data) |
|
| 102 |
#' result <- tma::accumulate(tensor, codes = codes, |
|
| 103 |
#' context_model = ctx, ordered = FALSE) |
|
| 104 |
#' ena_set <- rENA::model(result) |
|
| 105 |
#' ena_model <- qe_extract(ena_set, directed = FALSE) |
|
| 106 |
#' qe_plot(ena_model) |
|
| 107 |
#' } |
|
| 108 |
#' |
|
| 109 |
#' @export |
|
| 110 |
qe_extract <- function( |
|
| 111 |
set, |
|
| 112 |
directed = NULL, |
|
| 113 |
id_col = "ENA_UNIT", |
|
| 114 |
node_id_col = "code", |
|
| 115 |
group_col = "Condition" |
|
| 116 |
) {
|
|
| 117 |
# ── Directionality ───────────────────────────────────────────────────────── |
|
| 118 |
# Ordered (ONA) sets carry class "ena.ordered.set", which means the |
|
| 119 |
# line.weights matrix is asymmetric (A→B and B→A are distinct columns). |
|
| 120 |
# Auto-detect from the set class so callers don't have to specify it. |
|
| 121 |
# An explicit directed = TRUE/FALSE overrides the auto-detection. |
|
| 122 | 7x |
if (is.null(directed)) {
|
| 123 | 4x |
directed <- inherits(set, "ena.ordered.set") |
| 124 |
} |
|
| 125 | ||
| 126 |
# ── Nodes ────────────────────────────────────────────────────────────────── |
|
| 127 | 7x |
nodes_raw <- as.data.frame(set$rotation$nodes) |
| 128 | 7x |
dims <- find_dims(nodes_raw) |
| 129 | 7x |
nodes_df <- nodes_raw[, c(node_id_col, dims), drop = FALSE] |
| 130 | 7x |
names(nodes_df)[2:3] <- c("x", "y")
|
| 131 | ||
| 132 |
# ── Points ───────────────────────────────────────────────────────────────── |
|
| 133 | 7x |
pts_raw <- as.data.frame(set$points) |
| 134 | 7x |
points_df <- pts_raw[, c(id_col, group_col, dims), drop = FALSE] |
| 135 | 7x |
names(points_df)[3:4] <- c("x", "y")
|
| 136 | ||
| 137 |
# ── Edges ────────────────────────────────────────────────────────────────── |
|
| 138 | 7x |
meta_df <- as.data.frame(set$meta.data)[, c(id_col, group_col), drop = FALSE] |
| 139 | 7x |
edges_raw <- as.data.frame(set$line.weights) |
| 140 | 7x |
edges_df <- cbind(meta_df, edges_raw) |
| 141 |
# Normalise separator: rENA uses " & ", qeviz expects "." |
|
| 142 | 7x |
names(edges_df) <- gsub(" & ", ".", names(edges_df), fixed = TRUE)
|
| 143 | ||
| 144 |
# ── Groups (means + 95% CI) ──────────────────────────────────────────────── |
|
| 145 | 7x |
groups_df <- group_summary(points_df, "x", "y", group_col) |
| 146 | ||
| 147 | 7x |
qe_model_data( |
| 148 | 7x |
nodes = nodes_df, |
| 149 | 7x |
edges = edges_df, |
| 150 | 7x |
points = points_df, |
| 151 | 7x |
groups = groups_df, |
| 152 | 7x |
directed = directed, |
| 153 | 7x |
id_col = id_col, |
| 154 | 7x |
node_id_col = node_id_col, |
| 155 | 7x |
x_col = "x", |
| 156 | 7x |
y_col = "y", |
| 157 | 7x |
group_col = group_col |
| 158 |
) |
|
| 159 |
} |
|
| 160 | ||
| 161 |
#' List the group names in a model |
|
| 162 |
#' |
|
| 163 |
#' Returns the unique group names present in a `ModelData` list or `qe_plot` |
|
| 164 |
#' object. Useful when a dataset has more than two groups and you want to |
|
| 165 |
#' iterate over them or validate a group name before passing it to |
|
| 166 |
#' [qe_group()]. |
|
| 167 |
#' |
|
| 168 |
#' @param x A `qe_plot` object or a `ModelData` list from [qe_model_data()] / |
|
| 169 |
#' [qe_extract()]. |
|
| 170 |
#' |
|
| 171 |
#' @return A character vector of group names, in the order they appear in the |
|
| 172 |
#' model's group summary table. |
|
| 173 |
#' |
|
| 174 |
#' @examples |
|
| 175 |
#' \dontrun{
|
|
| 176 |
#' model <- qe_extract(set) |
|
| 177 |
#' qe_groups(model) |
|
| 178 |
#' #> [1] "FirstGame" "SecondGame" |
|
| 179 |
#' |
|
| 180 |
#' # Useful for iterating over all groups: |
|
| 181 |
#' lapply(qe_groups(model), function(g) {
|
|
| 182 |
#' qe_plot(model) |> qe_group(g) |> qe_export_html(paste0(g, ".html")) |
|
| 183 |
#' }) |
|
| 184 |
#' } |
|
| 185 |
#' |
|
| 186 |
#' @export |
|
| 187 |
qe_groups <- function(x) {
|
|
| 188 | ! |
model <- if (inherits(x, "qe_plot")) x$model |
| 189 | ! |
else if (inherits(x, "ena.set")) qe_extract(x) |
| 190 | ! |
else x |
| 191 | ! |
gcol <- model$group_col %||% "Condition" |
| 192 | ! |
vapply(model$groups$data, function(row) {
|
| 193 | ! |
v <- row[[gcol]] |
| 194 | ! |
if (is.null(v)) NA_character_ else as.character(v) |
| 195 | ! |
}, character(1L)) |
| 196 |
} |
|
| 197 | ||
| 198 |
#' List the unit IDs in a model |
|
| 199 |
#' |
|
| 200 |
#' Returns the unit IDs present in a `ModelData` list or `qe_plot` object. |
|
| 201 |
#' Unit IDs are the values in the model's `id_col` column (default |
|
| 202 |
#' `"ENA_UNIT"`), typically in compound form `"Group::Name"` as produced by |
|
| 203 |
#' rENA / tma. Pass any of these strings to [qe_edges()] as the `unit` |
|
| 204 |
#' argument to plot an individual unit's edge network. |
|
| 205 |
#' |
|
| 206 |
#' @param x A `qe_plot` object or a `ModelData` list from [qe_model_data()] / |
|
| 207 |
#' [qe_extract()]. |
|
| 208 |
#' |
|
| 209 |
#' @return A character vector of unit IDs, in the order they appear in the |
|
| 210 |
#' model's points table. Returns `character(0)` if the model has no points |
|
| 211 |
#' frame. |
|
| 212 |
#' |
|
| 213 |
#' @examples |
|
| 214 |
#' \dontrun{
|
|
| 215 |
#' model <- qe_extract(set) |
|
| 216 |
#' qe_units(model) |
|
| 217 |
#' #> [1] "FirstGame::steven z" "FirstGame::akash v" |
|
| 218 |
#' #> [3] "SecondGame::steven z" ... |
|
| 219 |
#' |
|
| 220 |
#' # Plot one unit's edge network alongside both group means: |
|
| 221 |
#' qe_plot(set) |> |
|
| 222 |
#' qe_group() |> |
|
| 223 |
#' qe_points() |> |
|
| 224 |
#' qe_edges(unit = "FirstGame::steven z") |
|
| 225 |
#' } |
|
| 226 |
#' |
|
| 227 |
#' @export |
|
| 228 |
qe_units <- function(x) {
|
|
| 229 | ! |
model <- if (inherits(x, "qe_plot")) x$model |
| 230 | ! |
else if (inherits(x, "ena.set")) qe_extract(x) |
| 231 | ! |
else x |
| 232 | ! |
id_col <- model$id_col %||% "ENA_UNIT" |
| 233 | ! |
if (is.null(model$points)) return(character(0L)) |
| 234 | ! |
vapply(model$points$data, function(row) {
|
| 235 | ! |
v <- row[[id_col]] |
| 236 | ! |
if (is.null(v)) NA_character_ else as.character(v) |
| 237 | ! |
}, character(1L)) |
| 238 |
} |
|
| 239 | ||
| 240 |
#' List the trajectory unit IDs in a model |
|
| 241 |
#' |
|
| 242 |
#' Returns the unit IDs present in the model's `trajectories` frame. If no |
|
| 243 |
#' separate trajectories frame is present, falls back to [qe_units()] because |
|
| 244 |
#' qeviz can use point rows as trajectory states. |
|
| 245 |
#' |
|
| 246 |
#' @param x A `qe_plot` object or a `ModelData` list from [qe_model_data()] / |
|
| 247 |
#' [qe_extract()]. |
|
| 248 |
#' |
|
| 249 |
#' @return A character vector of trajectory unit IDs. |
|
| 250 |
#' |
|
| 251 |
#' @export |
|
| 252 |
qe_trajectory_units <- function(x) {
|
|
| 253 | 4x |
model <- if (inherits(x, "qe_plot")) x$model |
| 254 | 4x |
else if (inherits(x, "ena.set")) qe_extract(x) |
| 255 | 4x |
else x |
| 256 | 4x |
id_col <- model$id_col %||% "ENA_UNIT" |
| 257 | 4x |
frame <- model$trajectories %||% model$points |
| 258 | ! |
if (is.null(frame)) return(character(0L)) |
| 259 | 4x |
unique(vapply(frame$data, function(row) {
|
| 260 | 80x |
v <- row[[id_col]] |
| 261 | 80x |
if (is.null(v)) NA_character_ else as.character(v) |
| 262 | 4x |
}, character(1L))) |
| 263 |
} |