@rho-workspace/rho 0.5.6 → 0.5.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent-connect.d.ts +36 -2
- package/dist/agent-connect.js +64 -11
- package/dist/answer-board.js +3 -3
- package/dist/bridge.js +6 -1
- package/dist/browser-access.d.ts +1 -0
- package/dist/browser-access.js +5 -3
- package/dist/capabilities.d.ts +21 -5
- package/dist/capabilities.js +6 -4
- package/dist/cli.js +76 -11
- package/dist/composition.d.ts +1 -1
- package/dist/composition.js +13 -3
- package/dist/connect-cli.d.ts +50 -0
- package/dist/connect-cli.js +128 -0
- package/dist/contract.generated.d.ts +1191 -278
- package/dist/contract.generated.js +1 -1
- package/dist/contracts/owners.generated.js +845 -47
- package/dist/conversation-presentation.d.ts +9 -2
- package/dist/conversation-presentation.js +3 -1
- package/dist/conversations.d.ts +154 -8
- package/dist/conversations.js +147 -24
- package/dist/discovery.js +2 -2
- package/dist/extension-contract.d.ts +19 -0
- package/dist/extension-contract.js +15 -6
- package/dist/extension-markets.d.ts +74 -0
- package/dist/extension-markets.js +166 -0
- package/dist/extensions.d.ts +29 -1
- package/dist/extensions.js +13 -4
- package/dist/external-work.d.ts +22 -0
- package/dist/external-work.js +91 -0
- package/dist/plugin-cli.js +75 -3
- package/dist/plugin-inventory.d.ts +56 -0
- package/dist/plugin-inventory.js +41 -0
- package/dist/plugin-inventory.json +1581 -102
- package/dist/plugin-presentation.d.ts +105 -2
- package/dist/plugin-presentation.js +59 -5
- package/dist/plugins.d.ts +14 -3
- package/dist/plugins.js +12 -6
- package/dist/project-notes.js +1 -1
- package/dist/protocol.d.ts +6 -0
- package/dist/protocol.js +30 -12
- package/dist/replay.js +4 -0
- package/dist/response-contracts.js +26 -11
- package/dist/running.d.ts +10 -0
- package/dist/running.js +45 -0
- package/dist/server.d.ts +2 -0
- package/dist/server.js +71 -8
- package/dist/state.d.ts +8 -0
- package/dist/state.js +8 -0
- package/dist/studio-layout.js +3 -3
- package/dist/task-frame.d.ts +1 -1
- package/dist/timeline-events.d.ts +1 -0
- package/dist/timeline-events.js +8 -1
- package/dist/ui-context.d.ts +73 -0
- package/dist/ui-context.js +98 -0
- package/dist/ui-contract.d.ts +23 -0
- package/dist/web/.vite/manifest.json +133 -74
- package/dist/web/assets/answer-board-CeYS8k8y.js +1 -0
- package/dist/web/assets/choices-D8vGwsl2.js +1 -0
- package/dist/web/assets/{console-Trqc0swD.js → console-Dyuo1q4N.js} +4 -4
- package/dist/web/assets/{dist-pF902jHA.js → dist-CSiCyBml.js} +1 -1
- package/dist/web/assets/{dist-DpIHRk3B.js → dist-D9FLb9Hm.js} +1 -1
- package/dist/web/assets/{dist-eMx6b8N1.js → dist-FXbTBwML.js} +1 -1
- package/dist/web/assets/{dist-DZmszrjG.js → dist-l3DUhbw5.js} +1 -1
- package/dist/web/assets/{explore-in-BCHqjJ7B.js → explore-in-BfSFGRh2.js} +1 -1
- package/dist/web/assets/graph-BU2_o5uA.js +2 -0
- package/dist/web/assets/index-C16mXVvk.css +1 -0
- package/dist/web/assets/index-CWg9XYXx.js +952 -0
- package/dist/web/assets/markdown.reader-Ct2-e2Zt.png +0 -0
- package/dist/web/assets/{notes-meaning-DcVGUBBY.js → notes-meaning-T2m5zQfv.js} +1 -1
- package/dist/web/assets/overview-PzfLJCo-.js +6 -0
- package/dist/web/assets/preview-BguEdsza.js +1 -0
- package/dist/web/assets/previews-pWNik-2g.js +3 -0
- package/dist/web/assets/{review-DYP0aCXb.js → review-Dy_85lcH.js} +1 -1
- package/dist/web/assets/rho.editor-CCJeyHma.png +0 -0
- package/dist/web/assets/rho.flow-WkMj6fRS.png +0 -0
- package/dist/web/assets/rho.media-Cc6amodO.png +0 -0
- package/dist/web/assets/rho.notebook-6vsZKGDq.png +0 -0
- package/dist/web/assets/rho.object-BGoeXjte.png +0 -0
- package/dist/web/assets/rho.plots-yIiISCkb.png +0 -0
- package/dist/web/assets/rho.scatter-DwHxesCs.png +0 -0
- package/dist/web/assets/rho.servers-DKf_mnNq.png +0 -0
- package/dist/web/assets/rho.table-DWOGaWDa.png +0 -0
- package/dist/web/assets/rho.variables-CKcWczhQ.png +0 -0
- package/dist/web/assets/{skills-1ec0Z-jx.js → skills-Dyz7mBlc.js} +1 -1
- package/dist/web/assets/{skills-work-DNexV4OJ.js → skills-work-BEGqlCkx.js} +1 -1
- package/dist/web/assets/studio-process-D-ykbC4D.js +2 -0
- package/dist/web/assets/timeline-events-DKTho9fU.js +1 -0
- package/dist/web/assets/work-turns-DSabCogR.js +1 -0
- package/dist/web/assets/{workspace-COmCbGV3.js → workspace-DfjBL--r.js} +2 -2
- package/dist/web/composition.json +138 -83
- package/dist/web/index.html +3 -3
- package/dist/web/renderer.json +1 -1
- package/dist/web-assets.js +2 -2
- package/dist/work-graph.d.ts +11 -1
- package/dist/work-graph.js +16 -5
- package/dist/workspace.d.ts +2 -2
- package/dist/workspace.js +8 -7
- package/docs/FRONTEND.md +53 -11
- package/docs/HEADLESS.md +104 -11
- package/docs/THIRD-PARTY-PLUGINS.md +75 -3
- package/node_modules/@rho-workspace/plugin-agent/README.md +5 -0
- package/node_modules/@rho-workspace/plugin-agent/dist/acp-command.d.ts +31 -0
- package/node_modules/@rho-workspace/plugin-agent/dist/acp-command.js +45 -10
- package/node_modules/@rho-workspace/plugin-agent/dist/acp-conversation.js +19 -3
- package/node_modules/@rho-workspace/plugin-agent/dist/external-session-contract.js +3 -2
- package/node_modules/@rho-workspace/plugin-agent/dist/external-session.d.ts +1 -1
- package/node_modules/@rho-workspace/plugin-agent/dist/index.d.ts +8 -0
- package/node_modules/@rho-workspace/plugin-agent/dist/index.js +3 -2
- package/node_modules/@rho-workspace/plugin-agent/dist/mcp-entry.d.ts +13 -0
- package/node_modules/@rho-workspace/plugin-agent/dist/mcp-entry.js +13 -0
- package/node_modules/@rho-workspace/plugin-agent/dist/mcp.js +16 -8
- package/node_modules/@rho-workspace/plugin-agent/dist/native-sessions.d.ts +1 -1
- package/node_modules/@rho-workspace/plugin-agent/package.json +1 -1
- package/node_modules/@rho-workspace/plugin-figures/LICENSE +661 -0
- package/node_modules/@rho-workspace/plugin-figures/README.md +191 -0
- package/node_modules/@rho-workspace/plugin-figures/dist/index.d.ts +236 -0
- package/node_modules/@rho-workspace/plugin-figures/dist/index.js +165 -0
- package/node_modules/@rho-workspace/plugin-figures/dist/operations.d.ts +44 -0
- package/node_modules/@rho-workspace/plugin-figures/dist/operations.js +39 -0
- package/node_modules/@rho-workspace/plugin-figures/dist/presentation.d.ts +19 -0
- package/node_modules/@rho-workspace/plugin-figures/dist/presentation.js +26 -0
- package/node_modules/@rho-workspace/plugin-figures/dist/record.d.ts +188 -0
- package/node_modules/@rho-workspace/plugin-figures/dist/record.js +143 -0
- package/node_modules/@rho-workspace/plugin-figures/dist/ui-styles.d.ts +1 -0
- package/node_modules/@rho-workspace/plugin-figures/dist/ui-styles.js +68 -0
- package/node_modules/@rho-workspace/plugin-figures/dist/ui.d.ts +56 -0
- package/node_modules/@rho-workspace/plugin-figures/dist/ui.js +363 -0
- package/node_modules/@rho-workspace/plugin-figures/package.json +41 -0
- package/node_modules/@rho-workspace/plugin-figures/py/figures.py +856 -0
- package/node_modules/@rho-workspace/plugin-figures/r/figures.R +775 -0
- package/node_modules/@rho-workspace/plugin-kernel/README.md +27 -3
- package/node_modules/@rho-workspace/plugin-kernel/dist/contract.d.ts +12 -0
- package/node_modules/@rho-workspace/plugin-kernel/dist/contract.js +8 -1
- package/node_modules/@rho-workspace/plugin-kernel/dist/index.d.ts +61 -0
- package/node_modules/@rho-workspace/plugin-kernel/dist/index.js +125 -9
- package/node_modules/@rho-workspace/plugin-kernel/dist/session-scope.d.ts +14 -0
- package/node_modules/@rho-workspace/plugin-kernel/dist/session-scope.js +9 -0
- package/node_modules/@rho-workspace/plugin-kernel/dist/ui-base.d.ts +23 -5
- package/node_modules/@rho-workspace/plugin-kernel/dist/ui-notebook-work.js +1 -1
- package/node_modules/@rho-workspace/plugin-kernel/dist/ui-notebook.d.ts +6 -4
- package/node_modules/@rho-workspace/plugin-kernel/dist/ui-notebook.js +44 -17
- package/node_modules/@rho-workspace/plugin-kernel/dist/ui-styles.js +6 -0
- package/node_modules/@rho-workspace/plugin-kernel/package.json +1 -1
- package/node_modules/@rho-workspace/plugin-kernel/runners/runner.R +144 -4
- package/node_modules/@rho-workspace/plugin-kernel/runners/runner.py +106 -1
- package/node_modules/@rho-workspace/plugin-tables/README.md +5 -0
- package/node_modules/@rho-workspace/plugin-tables/dist/index.d.ts +1 -0
- package/node_modules/@rho-workspace/plugin-tables/dist/index.js +19 -3
- package/node_modules/@rho-workspace/plugin-tables/dist/ui.js +1 -1
- package/node_modules/@rho-workspace/plugin-tables/package.json +1 -1
- package/node_modules/jose/dist/webapi/jwe/general/decrypt.js +3 -2
- package/node_modules/jose/dist/webapi/jwks/remote.js +1 -1
- package/node_modules/jose/dist/webapi/jws/general/verify.js +2 -8
- package/node_modules/jose/dist/webapi/lib/jwe_decrypt.js +11 -8
- package/node_modules/jose/dist/webapi/lib/key_management.js +0 -1
- package/node_modules/jose/package.json +1 -1
- package/package.json +6 -4
- package/protocol/index.json +91 -55
- package/protocol/operations/agent.sessions.list.json +13 -5
- package/protocol/operations/agent.sessions.read.json +13 -5
- package/protocol/operations/capability.list.json +382 -6
- package/protocol/operations/composition.apply.json +10 -2
- package/protocol/operations/composition.delete.json +845 -0
- package/protocol/operations/composition.read.json +10 -2
- package/protocol/operations/composition.undo.json +10 -2
- package/protocol/operations/conversation.agents.add.json +346 -0
- package/protocol/operations/conversation.agents.remove.json +340 -0
- package/protocol/operations/conversation.decide.json +196 -0
- package/protocol/operations/conversation.permission.json +196 -0
- package/protocol/operations/conversation.read.json +196 -0
- package/protocol/operations/conversation.send.json +265 -0
- package/protocol/operations/conversation.settings.check.json +213 -495
- package/protocol/operations/conversation.settings.read.json +211 -490
- package/protocol/operations/conversation.settings.update.json +235 -554
- package/protocol/operations/conversation.start.json +267 -5
- package/protocol/operations/conversation.stop.json +196 -0
- package/protocol/operations/extension.describe.json +39 -0
- package/protocol/operations/extension.install.json +62 -0
- package/protocol/operations/extension.list.json +164 -1
- package/protocol/operations/extension.market.change.json +210 -0
- package/protocol/operations/figure.read.json +265 -0
- package/protocol/operations/figure.select.json +157 -0
- package/protocol/operations/kernel.interrupt.json +119 -0
- package/protocol/operations/kernel.read.json +119 -0
- package/protocol/operations/kernel.run.json +119 -0
- package/protocol/operations/observation.snapshot.json +416 -8
- package/protocol/operations/project.openFile.json +10 -2
- package/protocol/operations/session.grant.json +4 -0
- package/protocol/operations/shared.json +1 -0
- package/protocol/operations/view.close.json +10 -2
- package/protocol/operations/view.protection.set.json +10 -2
- package/vendor/composition.json +177 -112
- package/vendor/rho-workspace-plugin-agent-0.4.33.tgz +0 -0
- package/vendor/rho-workspace-plugin-figures-0.5.1.tgz +0 -0
- package/vendor/rho-workspace-plugin-kernel-0.5.13.tgz +0 -0
- package/vendor/rho-workspace-plugin-tables-0.5.2.tgz +0 -0
- package/web/theme.css +5 -0
- package/dist/web/assets/answer-board-CBkGbSLD.js +0 -1
- package/dist/web/assets/canvas-D8rDdfDz.js +0 -1
- package/dist/web/assets/choices-D-YfnFP6.js +0 -1
- package/dist/web/assets/graph-lENgfQNH.js +0 -2
- package/dist/web/assets/index-B3_OVy91.css +0 -1
- package/dist/web/assets/index-BpL_Kl_s.js +0 -892
- package/dist/web/assets/overview-CDnVKlOt.js +0 -6
- package/dist/web/assets/preview-BEC1E9MK.js +0 -1
- package/dist/web/assets/studio-process-EYQgikxq.js +0 -2
- package/dist/web/assets/timeline-events-Chj2Gbx7.js +0 -1
- package/dist/web/assets/work-turns-CT9RVcWY.js +0 -1
- package/vendor/rho-workspace-plugin-agent-0.4.30.tgz +0 -0
- package/vendor/rho-workspace-plugin-kernel-0.5.5.tgz +0 -0
- package/vendor/rho-workspace-plugin-tables-0.5.0.tgz +0 -0
|
@@ -0,0 +1,775 @@
|
|
|
1
|
+
# Semantic figures for a live R session. The Notebook's runner sources this file into an
|
|
2
|
+
# environment of its own and calls three functions:
|
|
3
|
+
#
|
|
4
|
+
# page_object() when a grid page starts on the session's recording device: the plot object
|
|
5
|
+
# being drawn there, if a describer knows how to find it (NULL otherwise);
|
|
6
|
+
# saved_page() when a page starts on another device: the plot, the file it is being saved to
|
|
7
|
+
# and the page's size in inches, if a describer knows them (NULL otherwise). A
|
|
8
|
+
# page drawn on a file the code opened itself (png(), pdf()) is found by the
|
|
9
|
+
# runner, which asks page_object() when that page starts;
|
|
10
|
+
# describe_page(object, file, width, height, into, recorded)
|
|
11
|
+
# after that page's picture is written to `file`: writes the figure record
|
|
12
|
+
# (by default beside it, <file without .png>.figure.json) and returns a short
|
|
13
|
+
# summary. `object` is what page_object() named, or the recorded page itself
|
|
14
|
+
# when it named nothing; `recorded` is the page as recordPlot() kept it.
|
|
15
|
+
#
|
|
16
|
+
# A describer recognizes one kind of plot and binds what it draws to the data behind it. ggplot2's
|
|
17
|
+
# describes any plot built on ggplot2 layers (ggtree, enrichplot, Seurat, ggpubr, survminer and the
|
|
18
|
+
# rest) from its own layers and data, with no package-specific code. A page drawn with base
|
|
19
|
+
# graphics is described from what it drew: each point, label, bar or line keeps the values it was
|
|
20
|
+
# drawn at, and is bound to a table of the session only where those values are the table's own.
|
|
21
|
+
# A ComplexHeatmap is described from its laid-out heatmap: each cell names its row, its column
|
|
22
|
+
# and its value. Nothing here guesses a role from pixels.
|
|
23
|
+
#
|
|
24
|
+
# A describer never changes the session: printed output, warnings and messages are discarded,
|
|
25
|
+
# the random number stream is restored, and a failure leaves the page as a picture with the
|
|
26
|
+
# reason. The protected resources are the session's time and the state folder: a description
|
|
27
|
+
# stops after maxSeconds, refuses more than maxMarks drawn marks and writes at most maxBytes.
|
|
28
|
+
# A volcano of every gene with a p-value (22,415 marks with its labels) took 2.8 s and 7.8 MB, and
|
|
29
|
+
# an MA plot of every gene with a count (33,469 points, written with png()) 2.5 s and 8.5 MB.
|
|
30
|
+
limits <- list(maxMarks = 50000L, maxColumns = 40L, maxCellChars = 160L, maxSeconds = 20, maxBytes = 16L * 1024L * 1024L,
|
|
31
|
+
maxTables = 64L, maxTableCells = 5e6, maxComparedColumns = 64L)
|
|
32
|
+
schema <- "rho.figure/1"
|
|
33
|
+
`%||%` <- function(a, b) if (is.null(a)) b else a
|
|
34
|
+
|
|
35
|
+
# ggplot2 ----------------------------------------------------------------------------------------
|
|
36
|
+
# print() of a ggplot names the plot as the last one and then starts the page, so the plot being
|
|
37
|
+
# drawn is the `x` of the print method's frame on the stack when the page starts.
|
|
38
|
+
ggplot_print_methods <- function() {
|
|
39
|
+
if (!isNamespaceLoaded("ggplot2")) return(list())
|
|
40
|
+
Filter(Negate(is.null), list(
|
|
41
|
+
utils::getS3method("print", "ggplot2::ggplot", optional = TRUE),
|
|
42
|
+
utils::getS3method("print", "ggplot", optional = TRUE),
|
|
43
|
+
if (isNamespaceLoaded("patchwork")) utils::getS3method("print", "patchwork", optional = TRUE)))
|
|
44
|
+
}
|
|
45
|
+
ggplot_object <- function() {
|
|
46
|
+
methods <- ggplot_print_methods()
|
|
47
|
+
if (!length(methods)) return(NULL)
|
|
48
|
+
n <- sys.nframe()
|
|
49
|
+
for (i in rev(seq_len(n))) {
|
|
50
|
+
f <- sys.function(i)
|
|
51
|
+
for (m in methods) if (identical(f, m)) {
|
|
52
|
+
x <- tryCatch(get("x", envir = sys.frame(i), inherits = FALSE), error = function(e) NULL)
|
|
53
|
+
if (inherits(x, "ggplot")) return(x)
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
NULL
|
|
57
|
+
}
|
|
58
|
+
# ggsave() opens a device for its file and draws the plot there: the page's plot is found as above,
|
|
59
|
+
# its file is ggsave's own (with its path already joined) and its size is the device's, whatever
|
|
60
|
+
# units, scale or dpi it was given.
|
|
61
|
+
saved_page <- function() {
|
|
62
|
+
object <- ggplot_object()
|
|
63
|
+
if (is.null(object)) return(NULL)
|
|
64
|
+
save <- utils::getFromNamespace("ggsave", "ggplot2")
|
|
65
|
+
for (i in rev(seq_len(sys.nframe()))) if (identical(sys.function(i), save)) {
|
|
66
|
+
file <- get0("filename", envir = sys.frame(i), inherits = FALSE)
|
|
67
|
+
if (!is.character(file) || length(file) != 1L || !nzchar(file)) return(NULL)
|
|
68
|
+
size <- grDevices::dev.size("in")
|
|
69
|
+
return(list(object = object, file = file, width = size[1], height = size[2]))
|
|
70
|
+
}
|
|
71
|
+
NULL
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
# A grid page names a ggplot being printed, or else a ComplexHeatmap being drawn (see below). A
|
|
75
|
+
# page drawn with base graphics names nothing: it is described from its recording.
|
|
76
|
+
page_object <- function() ggplot_object() %||% heatmap_frame()
|
|
77
|
+
|
|
78
|
+
# Each layer's geom is wrapped so that what it draws in each panel carries the plot, the layer, the
|
|
79
|
+
# panel and the built rows it drew: a built row is numbered as it reaches the geom (handle_na sees
|
|
80
|
+
# the layer's whole built data), so this holds whoever builds the plot, ggplot2 or patchwork.
|
|
81
|
+
# ggproto objects are environments: layers and geoms are cloned, never changed, so the person's
|
|
82
|
+
# own plot object stays as it was.
|
|
83
|
+
tag_layer <- function(layer, plot, index) {
|
|
84
|
+
force(plot); force(index); original <- layer$geom
|
|
85
|
+
geom <- ggplot2::ggproto(NULL, original,
|
|
86
|
+
handle_na = function(self, data, params) {
|
|
87
|
+
data$.rho_mark <- seq_len(nrow(data))
|
|
88
|
+
ggplot2::ggproto_parent(original, self)$handle_na(data, params)
|
|
89
|
+
},
|
|
90
|
+
draw_layer = function(self, data, params, layout, coord) {
|
|
91
|
+
grobs <- ggplot2::ggproto_parent(original, self)$draw_layer(data, params, layout, coord)
|
|
92
|
+
panels <- if (nlevels(as.factor(data$PANEL)) > 1L) split(data, data$PANEL) else list(data)
|
|
93
|
+
for (k in seq_along(grobs)) {
|
|
94
|
+
d <- panels[[k]]
|
|
95
|
+
if (is.null(d) || !nrow(d) || is.null(grobs[[k]]) || inherits(grobs[[k]], "zeroGrob") || is.null(d$.rho_mark)) next
|
|
96
|
+
grobs[[k]]$rho_tag <- list(plot = plot, layer = index, panel = as.integer(as.character(d$PANEL[1])), mark = d$.rho_mark, group = d$group %||% rep(-1L, nrow(d)),
|
|
97
|
+
label = if (!is.null(d$label) && !is.list(d$label)) as.character(d$label) else NULL)
|
|
98
|
+
}
|
|
99
|
+
grobs
|
|
100
|
+
})
|
|
101
|
+
out <- ggplot2::ggproto(NULL, layer); out$geom <- geom
|
|
102
|
+
out
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
# Every layer whose data is a table gets a row number as an extra aesthetic. A stat that keeps
|
|
106
|
+
# its input rows (identity, ggtree's, most label stats) carries it through to the drawn marks;
|
|
107
|
+
# one that computes new rows (bins, densities, boxes) drops it, and those marks name the
|
|
108
|
+
# computed values instead.
|
|
109
|
+
prepare <- function(p, plot) {
|
|
110
|
+
sources <- list(); keys <- character(length(p$layers))
|
|
111
|
+
for (i in seq_along(p$layers)) {
|
|
112
|
+
original <- p$layers[[i]]
|
|
113
|
+
layer <- tag_layer(original, plot, i)
|
|
114
|
+
d <- tryCatch(original$layer_data(p$data), error = function(e) NULL)
|
|
115
|
+
if (is.data.frame(d) && nrow(d)) {
|
|
116
|
+
# layer_data() drops row names (cell barcodes, gene IDs); the rows keep their order.
|
|
117
|
+
raw <- if (inherits(original$data, "waiver")) p$data else if (is.function(original$data)) tryCatch(original$data(p$data), error = function(e) NULL) else original$data
|
|
118
|
+
if (is.data.frame(raw) && nrow(raw) == nrow(d) && .row_names_info(raw) > 0L) rownames(d) <- rownames(raw)
|
|
119
|
+
own <- !inherits(original$data, "waiver")
|
|
120
|
+
key <- paste0(if (plot > 1L) paste0("plot", plot, ".") else "", if (own) paste0("layer", i) else "data")
|
|
121
|
+
if (is.null(sources[[key]])) sources[[key]] <- d
|
|
122
|
+
d$.rho_row <- seq_len(nrow(d))
|
|
123
|
+
layer$data <- d
|
|
124
|
+
m <- if (is.null(original$mapping)) ggplot2::aes() else original$mapping
|
|
125
|
+
m[[".rho_row"]] <- ggplot2::aes(.rho_row = .rho_row)[[1]]
|
|
126
|
+
layer$mapping <- m
|
|
127
|
+
keys[i] <- key
|
|
128
|
+
}
|
|
129
|
+
p$layers[[i]] <- layer
|
|
130
|
+
}
|
|
131
|
+
list(plot = p, sources = sources, keys = keys)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
# One unit is a set of drawn elements that stand for some built rows of one layer in one panel:
|
|
135
|
+
# each element one row (points, rectangles, labels, segments), or one group (a line, a polygon,
|
|
136
|
+
# a box). What cannot be matched one to one is left drawn and reported, never guessed.
|
|
137
|
+
primitive_count <- function(x) {
|
|
138
|
+
if (inherits(x, "text")) return(length(x$label))
|
|
139
|
+
if (inherits(x, "segments")) return(length(x$x0))
|
|
140
|
+
if (inherits(x, c("points", "rect", "circle"))) return(max(length(x$x), length(x$y)))
|
|
141
|
+
NA_integer_
|
|
142
|
+
}
|
|
143
|
+
texts_in <- function(g) if (inherits(g, "text")) as.character(g$label) else if (inherits(g, "gTree")) unlist(lapply(g$children, texts_in), use.names = FALSE) else character()
|
|
144
|
+
units_of <- function(grob) {
|
|
145
|
+
tag <- grob$rho_tag
|
|
146
|
+
groups <- sort(unique(tag$group)); n <- length(tag$mark)
|
|
147
|
+
of_group <- function(g) tag$mark[tag$group == g]
|
|
148
|
+
k <- primitive_count(grob)
|
|
149
|
+
if (!is.na(k)) {
|
|
150
|
+
if (k == n) return(list(list(grob = grob, marks = as.list(tag$mark), each = TRUE)))
|
|
151
|
+
return(list(list(grob = grob, reason = sprintf("draws %d elements for %d rows", k, n))))
|
|
152
|
+
}
|
|
153
|
+
if (inherits(grob, c("polyline", "polygon", "pathgrob", "lines"))) {
|
|
154
|
+
ids <- grob$id %||% grob$pathId
|
|
155
|
+
if (!is.null(ids) && length(unique(ids)) == length(groups)) return(list(list(grob = grob, marks = lapply(groups, of_group), each = TRUE)))
|
|
156
|
+
if (length(groups) == 1L) return(list(list(grob = grob, marks = list(tag$mark), each = TRUE)))
|
|
157
|
+
return(list(list(grob = grob, reason = sprintf("draws one shape for %d groups", length(groups)))))
|
|
158
|
+
}
|
|
159
|
+
if (inherits(grob, "gTree") && length(grob$children)) {
|
|
160
|
+
kids <- length(grob$children)
|
|
161
|
+
# Labels drawn with halos or leader lines (ggrepel) draw several parts per row: a part whose
|
|
162
|
+
# text is one row's label stands for that row; the lines between stay drawn, unbound.
|
|
163
|
+
if (!is.null(tag$label) && length(tag$label) == n && !anyDuplicated(tag$label)) {
|
|
164
|
+
owner <- vapply(grob$children, function(ch) { t <- unique(texts_in(ch)); if (length(t) == 1L && t %in% tag$label) t else NA_character_ }, "")
|
|
165
|
+
if (all(tag$label %in% owner)) return(lapply(seq_len(kids), function(j) if (is.na(owner[j])) list(grob = grob$children[[j]], quiet = TRUE, child = TRUE)
|
|
166
|
+
else list(grob = grob$children[[j]], marks = list(tag$mark[match(owner[j], tag$label)]), each = TRUE, child = TRUE)))
|
|
167
|
+
}
|
|
168
|
+
if (kids == n && length(groups) != n) return(lapply(seq_len(kids), function(j) list(grob = grob$children[[j]], marks = list(tag$mark[j]), each = TRUE, child = TRUE)))
|
|
169
|
+
if (kids %% length(groups) == 0L) {
|
|
170
|
+
per <- kids / length(groups)
|
|
171
|
+
return(lapply(seq_len(kids), function(j) list(grob = grob$children[[j]], marks = list(of_group(groups[(j - 1L) %/% per + 1L])), each = TRUE, child = TRUE)))
|
|
172
|
+
}
|
|
173
|
+
return(list(list(grob = grob, reason = sprintf("draws %d parts for %d groups", kids, length(groups)))))
|
|
174
|
+
}
|
|
175
|
+
list(list(grob = grob, reason = sprintf("draws a %s", class(grob)[1])))
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
# `gt` is the page as a grob, or a function that draws it and returns it; `tag` marks the parts of
|
|
179
|
+
# the forced page that stand for marks.
|
|
180
|
+
render <- function(gt, width, height, tag = identity) {
|
|
181
|
+
capture <- svglite::svgstring(width, height, standalone = TRUE, web_fonts = list())
|
|
182
|
+
open <- TRUE
|
|
183
|
+
on.exit(if (open) grDevices::dev.off(), add = TRUE)
|
|
184
|
+
grob <- tag(grid::grid.force(if (is.function(gt)) gt() else gt))
|
|
185
|
+
units <- list(); unbound <- list()
|
|
186
|
+
wrap <- function(x) {
|
|
187
|
+
if (!is.null(x$rho_tag)) {
|
|
188
|
+
parts <- units_of(x)
|
|
189
|
+
made <- lapply(parts, function(u) {
|
|
190
|
+
if (is.null(u$marks) && isTRUE(u$quiet)) return(u$grob)
|
|
191
|
+
if (is.null(u$marks)) { unbound[[length(unbound) + 1L]] <<- list(plot = x$rho_tag$plot, layer = x$rho_tag$layer, panel = x$rho_tag$panel, reason = u$reason); return(u$grob) }
|
|
192
|
+
units[[length(units) + 1L]] <<- list(plot = x$rho_tag$plot, layer = x$rho_tag$layer, panel = x$rho_tag$panel, marks = u$marks)
|
|
193
|
+
g <- u$grob; vp <- g$vp; g$vp <- NULL
|
|
194
|
+
grid::groupGrob(g, vp = vp, name = paste0("rho-unit-", length(units)))
|
|
195
|
+
})
|
|
196
|
+
if (!isTRUE(parts[[1]]$child)) return(made[[1]])
|
|
197
|
+
return(grid::setChildren(x, do.call(grid::gList, made)))
|
|
198
|
+
}
|
|
199
|
+
if (inherits(x, "gTree") && length(x$children)) {
|
|
200
|
+
order <- match(x$childrenOrder, names(x$children))
|
|
201
|
+
if (anyNA(order)) stop("A drawn part's children cannot be put in their drawing order.")
|
|
202
|
+
x$children <- do.call(grid::gList, lapply(x$children[order], wrap))
|
|
203
|
+
x$childrenOrder <- names(x$children)
|
|
204
|
+
}
|
|
205
|
+
x
|
|
206
|
+
}
|
|
207
|
+
grid::grid.newpage(); grid::grid.draw(wrap(grob))
|
|
208
|
+
grDevices::dev.off(); open <- FALSE
|
|
209
|
+
list(svg = as.character(utils::tail(capture(), 1L)), units = units, unbound = unbound)
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
drawn <- ".//*[self::circle or self::rect or self::polygon or self::polyline or self::path or self::line or self::text]"
|
|
213
|
+
# Each element of a unit gets data-rho="unit.element" (both counted from 0), and the units' groups
|
|
214
|
+
# are put back where the device referenced them, keeping its transforms and clipping.
|
|
215
|
+
bind <- function(svg, units) {
|
|
216
|
+
doc <- xml2::read_xml(svg); xml2::xml_ns_strip(doc)
|
|
217
|
+
defs <- xml2::xml_find_all(doc, ".//defs/g[starts-with(@id, 'group-')]")
|
|
218
|
+
problems <- character()
|
|
219
|
+
if (length(defs) != length(units)) stop(sprintf("The picture has %d groups for %d bound units.", length(defs), length(units)))
|
|
220
|
+
kept <- logical(length(units))
|
|
221
|
+
for (i in seq_along(units)) {
|
|
222
|
+
u <- units[[i]]; els <- xml2::xml_find_all(defs[[i]], drawn)
|
|
223
|
+
n <- length(u$marks)
|
|
224
|
+
if (!length(els) || length(els) %% n != 0L) { problems <- c(problems, sprintf("Layer %d drew %d elements for %d marks; they are not bound.", u$layer, length(els), n)); next }
|
|
225
|
+
per <- length(els) / n
|
|
226
|
+
for (j in seq_along(els)) xml2::xml_set_attr(els[[j]], "data-rho", paste0(i - 1L, ".", (j - 1L) %/% per))
|
|
227
|
+
kept[i] <- TRUE
|
|
228
|
+
}
|
|
229
|
+
for (use in xml2::xml_find_all(doc, ".//use")) {
|
|
230
|
+
ref <- sub("^#", "", xml2::xml_attr(use, "href") %||% xml2::xml_attr(use, "xlink:href"))
|
|
231
|
+
idx <- which(xml2::xml_attr(defs, "id") == ref)
|
|
232
|
+
if (length(idx) != 1L) next
|
|
233
|
+
node <- xml2::xml_add_sibling(use, defs[[idx]], .copy = TRUE)
|
|
234
|
+
xml2::xml_set_attr(node, "id", NA_character_)
|
|
235
|
+
for (key in names(xml2::xml_attrs(use))) if (!key %in% c("href", "xlink:href")) xml2::xml_set_attr(node, key, xml2::xml_attr(use, key))
|
|
236
|
+
xml2::xml_remove(use)
|
|
237
|
+
}
|
|
238
|
+
xml2::xml_remove(defs)
|
|
239
|
+
# Namespaces were stripped to search the document; the picture is SVG again.
|
|
240
|
+
xml2::xml_set_attr(xml2::xml_root(doc), "xmlns", "http://www.w3.org/2000/svg")
|
|
241
|
+
list(svg = as.character(doc), kept = kept, problems = problems)
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
clip <- function(s, n = limits$maxCellChars) ifelse(!is.na(s) & nchar(s) > n, paste0(substr(s, 1, n - 1L), "…"), s)
|
|
245
|
+
# A column as JSON-ready values: numbers stay numbers, everything else is its printed text.
|
|
246
|
+
column <- function(v, rows) {
|
|
247
|
+
v <- v[rows]
|
|
248
|
+
if (is.factor(v)) v <- as.character(v)
|
|
249
|
+
if (inherits(v, c("Date", "POSIXt"))) v <- format(v)
|
|
250
|
+
if (is.list(v)) return(list(type = "list", values = I(vapply(v, function(e) paste0("<", class(e)[1], ">"), ""))))
|
|
251
|
+
if (is.logical(v)) return(list(type = "logical", values = I(v)))
|
|
252
|
+
if (is.numeric(v)) return(list(type = if (is.integer(v)) "integer" else "number", values = I(ifelse(is.finite(v), signif(v, 7), NA))))
|
|
253
|
+
list(type = "text", values = I(clip(as.character(v))))
|
|
254
|
+
}
|
|
255
|
+
# A column whose values name each row once can follow a row across versions of the data.
|
|
256
|
+
key_column <- function(d) {
|
|
257
|
+
if (.row_names_info(d) > 0L) return(".rownames")
|
|
258
|
+
unique_ <- function(v) !anyNA(v) && !anyDuplicated(v)
|
|
259
|
+
for (name in names(d)) if ((is.character(d[[name]]) || is.factor(d[[name]])) && unique_(d[[name]])) return(name)
|
|
260
|
+
for (name in names(d)) if (is.numeric(d[[name]]) && grepl("^(node|id|.*[._]id|ID)$", name) && unique_(d[[name]])) return(name)
|
|
261
|
+
NULL
|
|
262
|
+
}
|
|
263
|
+
table_of <- function(d, rows) {
|
|
264
|
+
names_ <- utils::head(names(d)[!startsWith(names(d), ".rho")], limits$maxColumns)
|
|
265
|
+
cols <- lapply(names_, function(n) c(list(name = n), column(d[[n]], rows)))
|
|
266
|
+
if (.row_names_info(d) > 0L) cols <- c(list(c(list(name = ".rownames"), column(rownames(d), rows))), cols)
|
|
267
|
+
list(rows = nrow(d), columnsTotal = ncol(d) - sum(startsWith(names(d), ".rho")), key = key_column(d), index = I(as.integer(rows) - 1L), columns = cols)
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
label_text <- function(x) if (is.null(x) || inherits(x, "waiver")) NULL else if (is.character(x)) paste(x, collapse = " ") else tryCatch(paste(deparse(x), collapse = " "), error = function(e) NULL)
|
|
271
|
+
mapping_text <- function(m) if (is.null(m) || !length(m)) list() else as.list(vapply(m, function(q) tryCatch(rlang::as_label(q), error = function(e) "?"), ""))
|
|
272
|
+
geom_name <- function(layer) sub("^Geom", "", class(layer$geom)[1])
|
|
273
|
+
stat_name <- function(layer) sub("^Stat", "", class(layer$stat)[1])
|
|
274
|
+
|
|
275
|
+
# A ggplot is one plot; a patchwork is its parts, each in turn, then itself, the last plot. A part
|
|
276
|
+
# may be a patchwork of its own (Seurat returns each plot as one), and one that is not a ggplot
|
|
277
|
+
# (a wrapped grob, a spacer) is drawn as it is, with nothing bound.
|
|
278
|
+
parts_of <- function(p) {
|
|
279
|
+
if (!inherits(p, "patchwork")) return(list(p))
|
|
280
|
+
c(unlist(lapply(p$patches$plots, function(x) if (inherits(x, "ggplot")) parts_of(x) else list()), recursive = FALSE), list(p))
|
|
281
|
+
}
|
|
282
|
+
# The same composition with each ggplot part replaced, in the order parts_of() named them.
|
|
283
|
+
with_parts <- function(p, parts) {
|
|
284
|
+
k <- 0L
|
|
285
|
+
swap <- function(x) {
|
|
286
|
+
if (!inherits(x, "ggplot")) return(x)
|
|
287
|
+
if (inherits(x, "patchwork")) for (j in seq_along(x$patches$plots)) x$patches$plots[[j]] <- swap(x$patches$plots[[j]])
|
|
288
|
+
k <<- k + 1L
|
|
289
|
+
main <- parts[[k]]
|
|
290
|
+
if (inherits(x, "patchwork")) { main$patches <- x$patches }
|
|
291
|
+
main
|
|
292
|
+
}
|
|
293
|
+
swap(p)
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
# Built positions are on the scale's transformed axis (log10 for scale_y_log10); a reader wants
|
|
297
|
+
# the data's own values, so positions are put back through the scale's inverse.
|
|
298
|
+
position_columns <- list(x = c("x", "xmin", "xmax", "xend", "xintercept", "xmin_final", "xmax_final", "xlower", "xmiddle", "xupper"),
|
|
299
|
+
y = c("y", "ymin", "ymax", "yend", "yintercept", "lower", "middle", "upper", "ymin_final", "ymax_final", "notchupper", "notchlower"))
|
|
300
|
+
style_columns <- "^(colour|color|fill|size|alpha|shape|linetype|linewidth|stroke|fontface|family|fontsize|hjust|vjust|angle|lineheight|width|weight|point[.]size|point[.]colour|segment[.].*|label[.].*|bg[.].*|box[.]padding|point[.]padding|min[.]segment[.]length|arrow[.].*)$"
|
|
301
|
+
transform_of <- function(sc) {
|
|
302
|
+
tr <- tryCatch(sc$get_transformation(), error = function(e) tryCatch(sc$trans, error = function(e) NULL))
|
|
303
|
+
if (is.null(tr) || identical(tr$name, "identity") || !is.function(tr$inverse)) NULL else tr
|
|
304
|
+
}
|
|
305
|
+
describe_plot <- function(p, b, prep, plot, units, unbound, used) {
|
|
306
|
+
scales_xy <- list(x = tryCatch(b$layout$panel_scales_x[[1]], error = function(e) NULL), y = tryCatch(b$layout$panel_scales_y[[1]], error = function(e) NULL))
|
|
307
|
+
transforms <- lapply(scales_xy, function(sc) if (is.null(sc) || isTRUE(tryCatch(sc$is_discrete(), error = function(e) TRUE))) NULL else transform_of(sc))
|
|
308
|
+
labels <- utils::modifyList(as.list(b$plot$labels %||% list()), as.list(p$labels %||% list()))
|
|
309
|
+
layers <- list(); marks <- list()
|
|
310
|
+
# A plot without layers (a spacer) is built with a blank one of ggplot2's own; it draws nothing.
|
|
311
|
+
for (i in seq_len(min(length(b$data), length(p$layers)))) {
|
|
312
|
+
d <- b$data[[i]]; layer <- p$layers[[i]]
|
|
313
|
+
for (axis in c("x", "y")) if (!is.null(transforms[[axis]])) for (cn in intersect(names(d), position_columns[[axis]])) if (is.numeric(d[[cn]])) d[[cn]] <- suppressWarnings(transforms[[axis]]$inverse(d[[cn]]))
|
|
314
|
+
linked <- !is.null(d$.rho_row) && is.numeric(d$.rho_row) && !anyNA(d$.rho_row)
|
|
315
|
+
source <- if (linked && nzchar(prep$keys[i])) prep$keys[i] else NULL
|
|
316
|
+
if (!is.null(source)) used[[source]] <- sort(unique(c(used[[source]], d$.rho_row)))
|
|
317
|
+
keep <- setdiff(names(d), c(".rho_row", ".rho_mark", "PANEL", "group", "flipped_aes"))
|
|
318
|
+
keep <- keep[vapply(d[keep], function(v) !all(is.na(v)) && !is.list(v), TRUE)]
|
|
319
|
+
# Only a style the layer draws every mark in (one colour, one size) is told once for the layer;
|
|
320
|
+
# a position or a computed value (a count, a density) stays with each mark even when equal.
|
|
321
|
+
constant <- keep[grepl(style_columns, keep) & vapply(d[keep], function(v) nrow(d) > 1L && length(unique(v)) == 1L, TRUE)]
|
|
322
|
+
varying <- utils::head(setdiff(keep, constant), limits$maxColumns)
|
|
323
|
+
marks[[i]] <- list(panel = I(as.integer(as.character(d$PANEL))), group = I(as.integer(d$group %||% rep(-1L, nrow(d)))), row = if (!is.null(source)) I(as.integer(d$.rho_row) - 1L) else NULL,
|
|
324
|
+
columns = lapply(varying, function(n) c(list(name = n), column(d[[n]], seq_len(nrow(d))))))
|
|
325
|
+
mine <- vapply(units, function(u) u$plot == plot && u$layer == i, TRUE)
|
|
326
|
+
lost <- Filter(function(u) u$plot == plot && u$layer == i, unbound)
|
|
327
|
+
layers[[i]] <- list(plot = plot, layer = i, geom = geom_name(layer), stat = stat_name(layer), position = sub("^Position", "", class(layer$position)[1]),
|
|
328
|
+
mapping = mapping_text(if (isTRUE(layer$inherit.aes)) utils::modifyList(as.list(p$mapping), as.list(layer$mapping %||% list())) else layer$mapping),
|
|
329
|
+
constants = lapply(d[1, constant, drop = FALSE], function(v) if (is.numeric(v)) signif(v, 7) else as.character(v)),
|
|
330
|
+
source = source, marks = nrow(d), units = sum(mine),
|
|
331
|
+
bound = if (!any(mine)) "none" else if (all(vapply(units[mine], function(u) all(lengths(u$marks) == 1L), TRUE))) "mark" else "group",
|
|
332
|
+
unbound = if (length(lost)) I(unique(vapply(lost, `[[`, "", "reason"))) else NULL)
|
|
333
|
+
}
|
|
334
|
+
facets <- b$layout$layout
|
|
335
|
+
facet_vars <- setdiff(names(facets), c("PANEL", "ROW", "COL", "SCALE_X", "SCALE_Y", "COORD"))
|
|
336
|
+
scales <- tryCatch(Filter(Negate(is.null), lapply(b$plot$scales$scales, function(sc) {
|
|
337
|
+
aes <- sc$aesthetics
|
|
338
|
+
if (any(aes %in% c("x", "y", "xmin", "xmax", "ymin", "ymax", "xend", "yend"))) return(NULL)
|
|
339
|
+
br <- sc$get_breaks(); if (is.null(br) || !length(br) || all(is.na(br))) return(NULL)
|
|
340
|
+
list(aesthetic = aes[1], title = label_text(labels[[aes[1]]]) %||% aes[1], breaks = I(as.character(sc$get_labels(br))), values = I(as.character(sc$map(br))))
|
|
341
|
+
})), error = function(e) list())
|
|
342
|
+
positions <- lapply(c(x = "x", y = "y"), function(a) tryCatch({
|
|
343
|
+
sc <- if (a == "x") b$layout$panel_scales_x[[1]] else b$layout$panel_scales_y[[1]]
|
|
344
|
+
tr <- transforms[[a]]
|
|
345
|
+
if (sc$is_discrete()) list(discrete = TRUE, levels = I(as.character(sc$get_limits())))
|
|
346
|
+
else c(list(discrete = FALSE, range = I(signif(if (is.null(tr)) sc$get_limits() else tr$inverse(sc$get_limits()), 7))), if (!is.null(tr)) list(transform = tr$name))
|
|
347
|
+
}, error = function(e) NULL))
|
|
348
|
+
list(used = used, layers = layers, marks = marks,
|
|
349
|
+
plot = list(plot = plot, title = label_text(labels$title), subtitle = label_text(labels$subtitle), caption = label_text(labels$caption),
|
|
350
|
+
labels = Filter(Negate(is.null), lapply(labels[setdiff(names(labels), c("title", "subtitle", "caption", "alt"))], label_text)),
|
|
351
|
+
panels = lapply(seq_len(nrow(facets)), function(k) list(panel = as.integer(as.character(facets$PANEL[k])), facets = lapply(facets[k, facet_vars, drop = FALSE], as.character))),
|
|
352
|
+
facet = sub("^Facet", "", class(b$layout$facet)[1]), coord = sub("^Coord", "", class(b$layout$coord)[1]), scales = scales, positions = positions))
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
describe_ggplot <- function(p, width, height, ...) {
|
|
356
|
+
parts <- parts_of(p)
|
|
357
|
+
preps <- lapply(seq_along(parts), function(k) prepare(parts[[k]], k))
|
|
358
|
+
builds <- lapply(preps, function(x) ggplot2::ggplot_build(x$plot))
|
|
359
|
+
total <- sum(vapply(builds, function(b) sum(vapply(b$data, nrow, 0L)), 0L))
|
|
360
|
+
if (total > limits$maxMarks) stop(sprintf("The plot draws %d marks, more than the %d a figure binds.", total, limits$maxMarks))
|
|
361
|
+
gt <- if (inherits(p, "patchwork")) patchwork::patchworkGrob(with_parts(p, lapply(preps, `[[`, "plot")))
|
|
362
|
+
else ggplot2::ggplot_gtable(builds[[1]])
|
|
363
|
+
r <- render(gt, width, height)
|
|
364
|
+
s <- bind(r$svg, r$units)
|
|
365
|
+
units <- r$units[s$kept]
|
|
366
|
+
used <- list(); layers <- list(); marks <- list(); plots <- list(); sources <- list()
|
|
367
|
+
for (k in seq_along(parts)) {
|
|
368
|
+
one <- describe_plot(parts[[k]], builds[[k]], preps[[k]], k, units, r$unbound, used)
|
|
369
|
+
used <- one$used; layers <- c(layers, one$layers); marks <- c(marks, one$marks); plots[[k]] <- one$plot
|
|
370
|
+
for (key in names(preps[[k]]$sources)) sources[[length(sources) + 1L]] <- c(list(id = key), table_of(preps[[k]]$sources[[key]], used[[key]] %||% integer()))
|
|
371
|
+
}
|
|
372
|
+
index <- function(plot, layer) which(vapply(layers, function(l) l$plot == plot && l$layer == layer, TRUE)) - 1L
|
|
373
|
+
title <- if (inherits(p, "patchwork")) label_text(p$patches$annotation$title) else NULL
|
|
374
|
+
list(svg = s$svg, problems = I(s$problems),
|
|
375
|
+
units = lapply(units, function(u) list(layer = index(u$plot, u$layer), panel = u$panel, marks = lapply(u$marks, function(m) I(as.integer(m) - 1L)))),
|
|
376
|
+
figure = list(kind = if (inherits(p, "patchwork")) "patchwork" else "ggplot", title = title %||% plots[[length(plots)]]$title, plots = plots, layers = layers),
|
|
377
|
+
marks = marks, sources = sources)
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
# Drawings ---------------------------------------------------------------------------------------
|
|
381
|
+
# Leaves of a forced page, in drawing order, each replaced by what f returns for it.
|
|
382
|
+
each_leaf <- function(x, f) {
|
|
383
|
+
if (inherits(x, "gTree") && length(x$children)) {
|
|
384
|
+
order <- match(x$childrenOrder, names(x$children))
|
|
385
|
+
if (anyNA(order)) stop("A drawn part's children cannot be put in their drawing order.")
|
|
386
|
+
x$children <- do.call(grid::gList, lapply(x$children[order], each_leaf, f))
|
|
387
|
+
x$childrenOrder <- names(x$children)
|
|
388
|
+
return(x)
|
|
389
|
+
}
|
|
390
|
+
f(x)
|
|
391
|
+
}
|
|
392
|
+
# The same drawn part with only the elements in `keep` (a point whose position is missing is not
|
|
393
|
+
# drawn, so it is left out rather than counted).
|
|
394
|
+
keep_elements <- function(g, keep) {
|
|
395
|
+
n <- length(keep)
|
|
396
|
+
for (field in c("x", "y", "x0", "y0", "x1", "y1", "width", "height", "label", "pch", "size", "r", "rot", "hjust", "vjust"))
|
|
397
|
+
if (!is.null(g[[field]]) && length(g[[field]]) == n) g[[field]] <- g[[field]][keep]
|
|
398
|
+
for (field in names(g$gp)) if (length(g$gp[[field]]) == n) g$gp[[field]] <- g$gp[[field]][keep]
|
|
399
|
+
g
|
|
400
|
+
}
|
|
401
|
+
native <- function(u, log = FALSE) {
|
|
402
|
+
if (is.null(u) || !length(u) || !all(grid::unitType(u) == "native")) return(NULL)
|
|
403
|
+
v <- as.numeric(u)
|
|
404
|
+
if (log) 10^v else v
|
|
405
|
+
}
|
|
406
|
+
justification <- function(g) {
|
|
407
|
+
j <- g$just %||% "centre"
|
|
408
|
+
side <- function(value, given, low, high) if (!is.null(given)) given else if (is.numeric(j)) j[value] else switch(j[value], left = , bottom = 0, right = , top = 1, 0.5)
|
|
409
|
+
list(h = side(1L, g$hjust), v = side(length(j), g$vjust))
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
# Base graphics: gridGraphics makes the recorded page into grobs named after what drew them
|
|
413
|
+
# (graphics-plot-2-points-1 is the first points() of the second plot). Each kind below is one
|
|
414
|
+
# layer, and each element it drew one mark; axes, boxes and titles are drawn and not bound.
|
|
415
|
+
graphics_kinds <- c(points = "points", text = "text", rect = "rect", "image-rect" = "image", segments = "segments", arrows = "arrows",
|
|
416
|
+
"abline-h" = "abline", "abline-v" = "abline", abline = "abline", lines = "lines", polygon = "polygon", path = "path", "symbols-circle" = "circles")
|
|
417
|
+
grouped_kinds <- c("lines", "polygon", "path")
|
|
418
|
+
# What each plot's window was: its limits, which axes are on a log scale, and its titles, read
|
|
419
|
+
# from the page's display list in the order the plots were started.
|
|
420
|
+
graphics_plots <- function(recorded) {
|
|
421
|
+
plots <- list(); k <- 0L
|
|
422
|
+
for (e in recorded[[1]]) {
|
|
423
|
+
name <- tryCatch(e[[2]][[1]]$name, error = function(err) "")
|
|
424
|
+
if (!is.character(name) || length(name) != 1L) next
|
|
425
|
+
args <- as.list(e[[2]])[-1]
|
|
426
|
+
if (name == "C_plot_new") { k <- k + 1L; plots[[k]] <- list(xlim = NULL, ylim = NULL, log = "", labels = list()) }
|
|
427
|
+
else if (k > 0L && name == "C_plot_window" && length(args) >= 3L) {
|
|
428
|
+
plots[[k]]$xlim <- args[[1]]; plots[[k]]$ylim <- args[[2]]
|
|
429
|
+
plots[[k]]$log <- if (is.character(args[[3]]) && length(args[[3]]) == 1L) args[[3]] else ""
|
|
430
|
+
} else if (k > 0L && name == "C_title" && length(args) >= 4L) {
|
|
431
|
+
for (j in 1:4) { t <- label_text(args[[j]]); if (!is.null(t) && nzchar(t)) plots[[k]]$labels[[c("title", "subtitle", "x", "y")[j]]] <- t }
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
plots
|
|
435
|
+
}
|
|
436
|
+
is_graphics_page <- function(x) inherits(x, "recordedplot") && any(vapply(x[[1]], function(e) identical(tryCatch(e[[2]][[1]]$name, error = function(err) ""), "C_plot_new"), TRUE))
|
|
437
|
+
|
|
438
|
+
# The values one drawn part was drawn at, in the data's units, one per element (a vertex of a line
|
|
439
|
+
# or polygon). A position not drawn in the plot's own units (a line across the whole plot) is left
|
|
440
|
+
# out; a bar is named by the axis label drawn at its middle.
|
|
441
|
+
graphics_columns <- function(g, kind, logx, logy, ticks) {
|
|
442
|
+
X <- function(u) native(u, logx); Y <- function(u) native(u, logy)
|
|
443
|
+
cols <- switch(kind,
|
|
444
|
+
points = , "symbols-circle" = list(x = X(g$x), y = Y(g$y)),
|
|
445
|
+
text = list(x = X(g$x), y = Y(g$y), label = as.character(g$label)),
|
|
446
|
+
rect = , "image-rect" = {
|
|
447
|
+
x <- native(g$x); y <- native(g$y); w <- native(g$width); h <- native(g$height); j <- justification(g)
|
|
448
|
+
if (is.null(x) || is.null(y) || is.null(w) || is.null(h)) list() else {
|
|
449
|
+
left <- x - j$h * w; bottom <- y - j$v * h
|
|
450
|
+
out <- list(xmin = if (logx) 10^left else left, xmax = if (logx) 10^(left + w) else left + w,
|
|
451
|
+
ymin = if (logy) 10^bottom else bottom, ymax = if (logy) 10^(bottom + h) else bottom + h)
|
|
452
|
+
for (side in names(ticks)) {
|
|
453
|
+
at <- ticks[[side]]$at; middle <- if (side == "bottom") left + w / 2 else bottom + h / 2
|
|
454
|
+
hit <- vapply(middle, function(m) { k <- which(abs(at - m) <= 1e-9 * pmax(1, abs(m))); if (length(k) == 1L) k else NA_integer_ }, 0L)
|
|
455
|
+
if (any(!is.na(hit))) { out$label <- ticks[[side]]$labels[hit]; break }
|
|
456
|
+
}
|
|
457
|
+
out
|
|
458
|
+
}
|
|
459
|
+
},
|
|
460
|
+
segments = , arrows = list(x = X(g$x0), y = Y(g$y0), xend = X(g$x1), yend = Y(g$y1)),
|
|
461
|
+
"abline-h" = list(yintercept = Y(g$y0)),
|
|
462
|
+
"abline-v" = list(xintercept = X(g$x0)),
|
|
463
|
+
lines = , polygon = , path = list(x = X(g$x), y = Y(g$y)),
|
|
464
|
+
list())
|
|
465
|
+
cols <- Filter(Negate(is.null), cols)
|
|
466
|
+
n <- if (length(cols)) max(lengths(cols)) else 0L
|
|
467
|
+
style <- Filter(function(v) !is.null(v) && length(v), list(colour = g$gp$col, fill = g$gp$fill, shape = if (kind == "points") g$pch,
|
|
468
|
+
size = if (kind == "points" && !is.null(g$size)) as.numeric(g$size), radius = if (kind == "symbols-circle") native(g$r)))
|
|
469
|
+
c(lapply(cols, rep_len, n), lapply(style, function(v) rep_len(if (is.factor(v)) as.character(v) else v, n)))
|
|
470
|
+
}
|
|
471
|
+
axis_ticks <- function(forced) {
|
|
472
|
+
ticks <- list()
|
|
473
|
+
each_leaf(forced, function(g) {
|
|
474
|
+
m <- regmatches(g$name, regexec("graphics-plot-(\\d+)-(bottom|left)-axis-labels-\\d+$", g$name))[[1]]
|
|
475
|
+
if (length(m) && inherits(g, "text")) {
|
|
476
|
+
at <- native(if (m[3] == "bottom") g$x else g$y)
|
|
477
|
+
if (!is.null(at) && length(at) == length(g$label)) ticks[[m[2]]][[m[3]]] <<- list(at = at, labels = as.character(g$label))
|
|
478
|
+
}
|
|
479
|
+
g
|
|
480
|
+
})
|
|
481
|
+
ticks
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
# A drawn part is bound to a table of the session (a data frame, a Bioconductor DataFrame, a
|
|
485
|
+
# numeric matrix or a named vector) when its x and y are two of the table's columns, value for
|
|
486
|
+
# value, over the table's rows in order; rows that were not drawn (missing values, rows filtered
|
|
487
|
+
# out first) are skipped. A position drawn through a common transform of a column (-log10, log10,
|
|
488
|
+
# log2, log2(x + 1), log1p) is matched through it, and the layer's mapping says so
|
|
489
|
+
# (y: -log10(padj)); columns drawn as they are come first. A part drawn from other computed values
|
|
490
|
+
# matches no table and keeps only the values it was drawn at. The protected resource is the
|
|
491
|
+
# session's time: at most maxTables tables of at most maxTableCells cells each, with fewer rows
|
|
492
|
+
# than the marks skipped, and at most maxComparedColumns numeric columns of each are compared.
|
|
493
|
+
transforms <- list(`%s` = function(v) v, `-log10(%s)` = function(v) -log10(v), `log10(%s)` = log10, `log2(%s)` = log2,
|
|
494
|
+
`log2(%s + 1)` = function(v) log2(v + 1), `log1p(%s)` = log1p)
|
|
495
|
+
# Equal, or within 1e-9 of each other relative to their size. A value that is not finite matches
|
|
496
|
+
# only itself: -log10(0) is Inf, and Inf is not near every number.
|
|
497
|
+
near <- function(a, b) !is.na(a) & !is.na(b) & (a == b | (is.finite(a) & is.finite(b) & abs(a - b) <= 1e-9 * pmax(abs(a), abs(b))))
|
|
498
|
+
rows_of <- function(drawn, column, sorted) {
|
|
499
|
+
j <- findInterval(column, sorted)
|
|
500
|
+
member <- near(column, sorted[pmax(j, 1L)]) | near(column, sorted[pmin(j + 1L, length(sorted))])
|
|
501
|
+
member[is.na(member)] <- FALSE
|
|
502
|
+
kept <- which(member)
|
|
503
|
+
if (length(kept) < length(drawn)) return(NULL)
|
|
504
|
+
if (length(kept) == length(drawn)) return(if (all(near(column[kept], drawn))) kept else NULL)
|
|
505
|
+
out <- integer(length(drawn)); k <- 1L
|
|
506
|
+
for (r in kept) { if (near(column[r], drawn[k])) { out[k] <- r; k <- k + 1L; if (k > length(drawn)) break } }
|
|
507
|
+
if (k <= length(drawn)) NULL else out
|
|
508
|
+
}
|
|
509
|
+
session_tables <- function() {
|
|
510
|
+
out <- list()
|
|
511
|
+
for (name in ls(globalenv())) {
|
|
512
|
+
if (length(out) >= limits$maxTables) break
|
|
513
|
+
x <- get(name, envir = globalenv())
|
|
514
|
+
size <- if (is.null(dim(x))) length(x) else prod(dim(x))
|
|
515
|
+
if (!is.numeric(size) || size > limits$maxTableCells) next
|
|
516
|
+
d <- tryCatch(if (is.data.frame(x)) x
|
|
517
|
+
else if (isS4(x) && methods::is(x, "DataFrame")) as.data.frame(x)
|
|
518
|
+
else if (is.matrix(x) && is.numeric(x)) as.data.frame(x)
|
|
519
|
+
else if (is.numeric(x) && is.null(dim(x)) && !is.null(names(x)) && !anyDuplicated(names(x))) data.frame(value = unname(x), row.names = names(x))
|
|
520
|
+
else NULL, error = function(e) NULL)
|
|
521
|
+
if (is.data.frame(d) && nrow(d) > 0L && prod(dim(d)) <= limits$maxTableCells) out[[name]] <- d
|
|
522
|
+
}
|
|
523
|
+
out
|
|
524
|
+
}
|
|
525
|
+
match_table <- function(cols, tables) {
|
|
526
|
+
if (!is.numeric(cols$x) || !is.numeric(cols$y)) return(NULL)
|
|
527
|
+
n <- length(cols$x); best <- NULL
|
|
528
|
+
sorted <- lapply(cols[c("x", "y")], function(v) sort(unique(v)))
|
|
529
|
+
through <- function(f, v) suppressWarnings(f(v))
|
|
530
|
+
for (allowed in list(transforms[1], transforms)) {
|
|
531
|
+
for (name in names(tables)) {
|
|
532
|
+
d <- tables[[name]]
|
|
533
|
+
# A table with fewer rows than the marks drawn cannot hold them.
|
|
534
|
+
if (nrow(d) < n) next
|
|
535
|
+
numeric_ <- utils::head(names(d)[vapply(d, is.numeric, TRUE)], limits$maxComparedColumns)
|
|
536
|
+
for (a in c("x", "y")) for (cn in numeric_) for (form in names(allowed)) {
|
|
537
|
+
if (!is.null(best) && best$differ == 0L) break
|
|
538
|
+
rows <- rows_of(cols[[a]], through(allowed[[form]], d[[cn]]), sorted[[a]]); if (is.null(rows)) next
|
|
539
|
+
b <- setdiff(c("x", "y"), a)
|
|
540
|
+
fit <- 0; how <- NULL
|
|
541
|
+
for (c2 in numeric_) {
|
|
542
|
+
for (form2 in names(transforms)) {
|
|
543
|
+
k <- sum(near(through(transforms[[form2]], d[[c2]][rows]), cols[[b]]))
|
|
544
|
+
if (k > fit) { fit <- k; how <- c(c2, form2) }
|
|
545
|
+
if (fit == n) break
|
|
546
|
+
}
|
|
547
|
+
if (fit == n) break
|
|
548
|
+
}
|
|
549
|
+
if (fit < 0.99 * n) next
|
|
550
|
+
found <- list(table = name, rows = rows, mapping = stats::setNames(list(sprintf(form, cn), sprintf(how[2], how[1])), c(a, b)), other = b, differ = n - fit,
|
|
551
|
+
how = stats::setNames(list(c(cn, form), how), c(a, b)))
|
|
552
|
+
found$mapping <- found$mapping[c("x", "y")]
|
|
553
|
+
if (is.null(best) || found$differ < best$differ || (found$differ == best$differ && is.null(key_column(tables[[best$table]])) && !is.null(key_column(d)))) best <- found
|
|
554
|
+
}
|
|
555
|
+
if (!is.null(best) && best$differ == 0L) break
|
|
556
|
+
}
|
|
557
|
+
if (!is.null(best)) break
|
|
558
|
+
}
|
|
559
|
+
best
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
describe_graphics <- function(p, width, height, ...) {
|
|
563
|
+
plots <- graphics_plots(p)
|
|
564
|
+
page <- gridGraphics::echoGrob(p, name = "rho")
|
|
565
|
+
layers <- list(); columns <- list(); total <- 0L
|
|
566
|
+
tag <- function(forced) {
|
|
567
|
+
ticks <- axis_ticks(forced)
|
|
568
|
+
each_leaf(forced, function(g) {
|
|
569
|
+
m <- regmatches(g$name, regexec("graphics-plot-(\\d+)-(.+)-\\d+$", g$name))[[1]]
|
|
570
|
+
if (!length(m) || !m[3] %in% names(graphics_kinds)) return(g)
|
|
571
|
+
k <- as.integer(m[2]); kind <- m[3]; info <- if (k <= length(plots)) plots[[k]] else list(log = "")
|
|
572
|
+
cols <- graphics_columns(g, kind, grepl("x", info$log), grepl("y", info$log), ticks[[as.character(k)]] %||% list())
|
|
573
|
+
positions <- intersect(names(cols), c("x", "y", "xmin", "xmax", "ymin", "ymax", "xend", "yend", "xintercept", "yintercept"))
|
|
574
|
+
if (!length(positions)) return(g)
|
|
575
|
+
n <- length(cols[[1]])
|
|
576
|
+
if (!kind %in% grouped_kinds) {
|
|
577
|
+
keep <- Reduce(`&`, lapply(cols[positions], is.finite))
|
|
578
|
+
if (!all(keep)) { g <- keep_elements(g, keep); cols <- lapply(cols, `[`, keep); n <- sum(keep) }
|
|
579
|
+
}
|
|
580
|
+
if (!n) return(g)
|
|
581
|
+
total <<- total + n
|
|
582
|
+
if (total > limits$maxMarks) stop(sprintf("The plot draws more than the %d marks a figure binds.", limits$maxMarks))
|
|
583
|
+
L <- length(layers) + 1L
|
|
584
|
+
layers[[L]] <<- list(plot = k, kind = kind, n = n)
|
|
585
|
+
columns[[L]] <<- cols
|
|
586
|
+
g$rho_tag <- list(plot = k, layer = L, panel = 1L, mark = seq_len(n), group = if (kind %in% grouped_kinds) as.integer(g$id %||% rep(1L, n)) else rep(-1L, n),
|
|
587
|
+
label = if (kind == "text") as.character(g$label) else NULL)
|
|
588
|
+
g
|
|
589
|
+
})
|
|
590
|
+
}
|
|
591
|
+
r <- render(page, width, height, tag)
|
|
592
|
+
s <- bind(r$svg, r$units)
|
|
593
|
+
units <- r$units[s$kept]
|
|
594
|
+
tables <- NULL; used <- list(); sources <- list(); hows <- list(); bound_to <- list()
|
|
595
|
+
out_layers <- list(); out_marks <- list()
|
|
596
|
+
for (L in seq_along(layers)) {
|
|
597
|
+
l <- layers[[L]]; n <- l$n
|
|
598
|
+
cols <- Filter(function(v) !all(is.na(v)), columns[[L]])
|
|
599
|
+
source <- NULL; mapping <- list(); notes <- NULL; rows <- NULL
|
|
600
|
+
if (l$kind %in% c("points", "text", "symbols-circle", "lines")) {
|
|
601
|
+
if (is.null(tables)) tables <- session_tables()
|
|
602
|
+
found <- match_table(cols, tables)
|
|
603
|
+
if (!is.null(found)) {
|
|
604
|
+
source <- found$table; rows <- found$rows; mapping <- found$mapping
|
|
605
|
+
used[[source]] <- sort(unique(c(used[[source]], rows)))
|
|
606
|
+
if (found$differ > 0L) notes <- sprintf("%d of %d marks are drawn at a %s other than their %s.", found$differ, n, found$other, mapping[[found$other]])
|
|
607
|
+
# A column an earlier layer of this plot draws on the same axis through another transform:
|
|
608
|
+
# the two layers place their marks on different scales.
|
|
609
|
+
for (M in rev(seq_along(hows))) if (!is.null(hows[[M]]) && layers[[M]]$plot == l$plot && identical(bound_to[[M]], source)) for (axis in c("x", "y")) {
|
|
610
|
+
mine <- found$how[[axis]]; theirs <- hows[[M]][[axis]]
|
|
611
|
+
if (mine[1] == theirs[1] && mine[2] != theirs[2]) notes <- c(notes, sprintf("Its %s is %s, while layer %d of this plot draws %s as %s on the same axis.", axis, sprintf(mine[2], mine[1]), M - 1L, axis, sprintf(theirs[2], theirs[1])))
|
|
612
|
+
}
|
|
613
|
+
hows[[L]] <- found$how; bound_to[[L]] <- source
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
constant <- names(cols)[grepl(style_columns, names(cols)) & vapply(cols, function(v) n > 1L && length(unique(v)) == 1L, TRUE)]
|
|
617
|
+
varying <- setdiff(names(cols), constant)
|
|
618
|
+
mine <- vapply(units, function(u) u$layer == L, TRUE)
|
|
619
|
+
lost <- Filter(function(u) u$layer == L, r$unbound)
|
|
620
|
+
out_marks[[L]] <- list(panel = I(rep(1L, n)), group = I(rep(if (l$kind %in% grouped_kinds) 1L else -1L, n)), row = if (!is.null(rows)) I(as.integer(rows) - 1L) else NULL,
|
|
621
|
+
columns = lapply(varying, function(v) c(list(name = v), column(cols[[v]], seq_len(n)))))
|
|
622
|
+
out_layers[[L]] <- list(plot = l$plot, layer = L, geom = graphics_kinds[[l$kind]], stat = "identity", position = "identity", mapping = mapping,
|
|
623
|
+
constants = lapply(cols[constant], function(v) if (is.numeric(v)) signif(v[1], 7) else as.character(v[1])),
|
|
624
|
+
source = source, marks = n, units = sum(mine),
|
|
625
|
+
bound = if (!any(mine)) "none" else if (all(vapply(units[mine], function(u) all(lengths(u$marks) == 1L), TRUE))) "mark" else "group",
|
|
626
|
+
unbound = if (length(lost)) I(unique(vapply(lost, `[[`, "", "reason"))) else NULL, notes = if (length(notes)) I(notes) else NULL)
|
|
627
|
+
}
|
|
628
|
+
for (name in names(used)) sources[[length(sources) + 1L]] <- c(list(id = name), table_of(tables[[name]], used[[name]]))
|
|
629
|
+
out_plots <- lapply(seq_along(plots), function(k) {
|
|
630
|
+
p <- plots[[k]]
|
|
631
|
+
axis <- function(lim, log) if (is.numeric(lim) && length(lim) == 2L) c(list(discrete = FALSE, range = I(signif(lim, 7))), if (log) list(transform = "log-10"))
|
|
632
|
+
list(plot = k, title = p$labels$title, subtitle = p$labels$subtitle, caption = NULL, labels = p$labels[intersect(names(p$labels), c("x", "y"))],
|
|
633
|
+
panels = list(list(panel = 1L, facets = structure(list(), names = character()))), facet = "Null", coord = "cartesian", scales = list(),
|
|
634
|
+
positions = Filter(Negate(is.null), list(x = axis(p$xlim, grepl("x", p$log)), y = axis(p$ylim, grepl("y", p$log)))))
|
|
635
|
+
})
|
|
636
|
+
list(svg = s$svg, problems = I(s$problems),
|
|
637
|
+
units = lapply(units, function(u) list(layer = u$layer - 1L, panel = u$panel, marks = lapply(u$marks, function(m) I(as.integer(m) - 1L)))),
|
|
638
|
+
figure = list(kind = "graphics", title = if (length(out_plots)) out_plots[[1]]$title else NULL, plots = out_plots, layers = out_layers),
|
|
639
|
+
marks = out_marks, sources = sources)
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
# ComplexHeatmap -----------------------------------------------------------------------------------
|
|
643
|
+
# draw() starts the page before it lays the heatmaps out, in the frame of its own method, so that
|
|
644
|
+
# frame is kept when the page starts and its laid-out heatmap list is read when the page is
|
|
645
|
+
# described: the rows and columns in the order and slices drawn, and the matrix. The page is drawn
|
|
646
|
+
# again from its recording, and each cell rectangle of a heatmap body is one mark. Annotations,
|
|
647
|
+
# dendrograms and names are drawn and not bound.
|
|
648
|
+
heatmap_frame <- function() {
|
|
649
|
+
if (!isNamespaceLoaded("ComplexHeatmap")) return(NULL)
|
|
650
|
+
for (i in rev(seq_len(sys.nframe()))) {
|
|
651
|
+
env <- sys.frame(i)
|
|
652
|
+
if (inherits(get0("object", envir = env, inherits = FALSE), "HeatmapList")) return(structure(list(frame = env), class = "rho_heatmap"))
|
|
653
|
+
}
|
|
654
|
+
NULL
|
|
655
|
+
}
|
|
656
|
+
# A grabbed page names each part's viewport by its path, and grid matches a path as a regular
|
|
657
|
+
# expression: a heatmap named "z-score (VST)" would not find its own viewports. Paths are made
|
|
658
|
+
# literal before the page is drawn again.
|
|
659
|
+
literal <- function(s) gsub("([][{}()+*^$|\\\\?.])", "\\\\\\1", s)
|
|
660
|
+
literal_paths <- function(x) {
|
|
661
|
+
if (inherits(x$vp, "vpPath")) x$vp$path <- literal(x$vp$path)
|
|
662
|
+
if (inherits(x, "gTree") && length(x$children)) for (n in names(x$children)) x$children[[n]] <- literal_paths(x$children[[n]])
|
|
663
|
+
x
|
|
664
|
+
}
|
|
665
|
+
viewport_name <- function(vp) if (inherits(vp, "viewport") || inherits(vp, "vpPath")) vp$name else if (inherits(vp, "vpStack") && length(vp)) viewport_name(vp[[length(vp)]]) else NULL
|
|
666
|
+
describe_heatmap <- function(h, width, height, recorded = NULL, ...) {
|
|
667
|
+
o <- get0("object", envir = h$frame, inherits = FALSE)
|
|
668
|
+
if (!inherits(o, "HeatmapList") || !isTRUE(o@layout$initialized)) stop("The heatmap was not laid out when its page was drawn; it stays a picture.")
|
|
669
|
+
if (is.null(recorded)) stop("The heatmap's page was not recorded; it stays a picture.")
|
|
670
|
+
maps <- Filter(function(x) inherits(x, "Heatmap"), o@ht_list)
|
|
671
|
+
shape <- lapply(maps, function(m) list(rows = m@row_order_list, cols = m@column_order_list))
|
|
672
|
+
total <- sum(vapply(shape, function(s) length(unlist(s$rows)) * length(unlist(s$cols)), 0))
|
|
673
|
+
if (total > limits$maxMarks) stop(sprintf("The heatmap draws %d cells, more than the %d a figure binds.", total, limits$maxMarks))
|
|
674
|
+
bodies <- paste0("^(", paste(vapply(maps, function(m) literal(m@name), ""), collapse = "|"), ")_heatmap_body_(\\d+)_(\\d+)$")
|
|
675
|
+
reasons <- list()
|
|
676
|
+
tag <- function(forced) each_leaf(forced, function(g) {
|
|
677
|
+
m <- regmatches(viewport_name(g$vp) %||% "", regexec(bodies, viewport_name(g$vp) %||% ""))[[1]]
|
|
678
|
+
if (!length(m)) return(g)
|
|
679
|
+
k <- match(m[2], vapply(maps, function(x) x@name, "")); i <- as.integer(m[3]); j <- as.integer(m[4])
|
|
680
|
+
s <- shape[[k]]; rows <- s$rows[[i]]; cols <- s$cols[[j]]
|
|
681
|
+
if (inherits(g, "rastergrob")) { reasons[[length(reasons) + 1L]] <<- list(layer = k, reason = "draws its cells as one image (use_raster)"); return(g) }
|
|
682
|
+
if (!inherits(g, "rect") || max(length(g$x), length(g$y)) != length(rows) * length(cols)) return(g)
|
|
683
|
+
x <- as.numeric(g$x); y <- as.numeric(g$y)
|
|
684
|
+
column <- match(x, sort(unique(x))); row <- match(y, sort(unique(y), decreasing = TRUE))
|
|
685
|
+
if (max(column) != length(cols) || max(row) != length(rows) || anyDuplicated(paste(row, column))) { reasons[[length(reasons) + 1L]] <<- list(layer = k, reason = "draws cells that are not in a grid of its rows and columns"); return(g) }
|
|
686
|
+
before_rows <- sum(lengths(s$rows[seq_len(i - 1L)])); before_cols <- sum(lengths(s$cols[seq_len(j - 1L)]))
|
|
687
|
+
g$rho_tag <- list(plot = k, layer = k, panel = (i - 1L) * length(s$cols) + j, mark = (before_rows + row - 1L) * length(unlist(s$cols)) + before_cols + column, group = rep(-1L, length(x)))
|
|
688
|
+
g
|
|
689
|
+
})
|
|
690
|
+
r <- render(function() { grDevices::replayPlot(recorded); literal_paths(grid::grid.grab()) }, width, height, tag)
|
|
691
|
+
s <- bind(r$svg, r$units)
|
|
692
|
+
units <- r$units[s$kept]
|
|
693
|
+
layers <- list(); marks <- list(); plots <- list(); sources <- list()
|
|
694
|
+
for (k in seq_along(maps)) {
|
|
695
|
+
hm <- maps[[k]]; rows <- shape[[k]]$rows; cols <- shape[[k]]$cols; m <- hm@matrix
|
|
696
|
+
r_all <- unlist(rows, use.names = FALSE); c_all <- unlist(cols, use.names = FALSE)
|
|
697
|
+
a <- rep(seq_along(r_all), each = length(c_all)); b <- rep(seq_along(c_all), times = length(r_all))
|
|
698
|
+
ri <- r_all[a]; ci <- c_all[b]
|
|
699
|
+
name_of <- function(names_, i) if (is.null(names_)) i else names_[i]
|
|
700
|
+
slice <- function(parts, at) rep(seq_along(parts), lengths(parts))[at]
|
|
701
|
+
built <- list(row = name_of(rownames(m), ri), column = name_of(colnames(m), ci), value = m[cbind(ri, ci)])
|
|
702
|
+
if (length(rows) > 1L) built[["row slice"]] <- (names(rows) %||% as.character(seq_along(rows)))[slice(rows, a)]
|
|
703
|
+
if (length(cols) > 1L) built[["column slice"]] <- (names(cols) %||% as.character(seq_along(cols)))[slice(cols, b)]
|
|
704
|
+
panel <- (slice(rows, a) - 1L) * length(cols) + slice(cols, b)
|
|
705
|
+
source <- NULL
|
|
706
|
+
for (n in ls(globalenv())) { x <- get(n, envir = globalenv()); if (is.matrix(x) && identical(dim(x), dim(m)) && identical(unname(x), unname(m)) && identical(dimnames(x), dimnames(m))) { source <- n; break } }
|
|
707
|
+
source <- source %||% hm@name
|
|
708
|
+
sources[[length(sources) + 1L]] <- c(list(id = source), table_of(as.data.frame(m), sort(unique(r_all))))
|
|
709
|
+
marks[[k]] <- list(panel = I(as.integer(panel)), group = I(rep(-1L, length(a))), row = I(as.integer(ri) - 1L),
|
|
710
|
+
columns = lapply(names(built), function(v) c(list(name = v), column(built[[v]], seq_along(a)))))
|
|
711
|
+
mine <- vapply(units, function(u) u$layer == k, TRUE)
|
|
712
|
+
lost <- c(Filter(function(u) u$layer == k, r$unbound), Filter(function(u) u$layer == k, reasons))
|
|
713
|
+
layers[[k]] <- list(plot = k, layer = k, geom = "heatmap", stat = "identity", position = "identity", mapping = list(x = "column", y = "row", fill = "value"),
|
|
714
|
+
constants = structure(list(), names = character()), source = source, marks = length(a), units = sum(mine), bound = if (any(mine)) "mark" else "none",
|
|
715
|
+
unbound = if (length(lost)) I(unique(vapply(lost, `[[`, "", "reason"))) else if (!any(mine)) I("draws its cells with functions of its own") else NULL)
|
|
716
|
+
cm <- hm@matrix_color_mapping
|
|
717
|
+
scale <- tryCatch(list(list(aesthetic = "fill", title = cm@name, breaks = I(as.character(signif(as.numeric(cm@levels), 4))), values = I(as.character(cm@colors)))), error = function(e) list())
|
|
718
|
+
title <- tryCatch(paste(hm@column_title, collapse = " "), error = function(e) "")
|
|
719
|
+
panels <- list()
|
|
720
|
+
for (i in seq_along(rows)) for (j in seq_along(cols)) panels[[length(panels) + 1L]] <- list(panel = (i - 1L) * length(cols) + j,
|
|
721
|
+
facets = c(if (length(rows) > 1L) list(`row slice` = (names(rows) %||% as.character(seq_along(rows)))[i]), if (length(cols) > 1L) list(`column slice` = (names(cols) %||% as.character(seq_along(cols)))[j])) %||% structure(list(), names = character()))
|
|
722
|
+
plots[[k]] <- list(plot = k, title = if (nzchar(title)) title else NULL, subtitle = NULL, caption = NULL, labels = list(fill = cm@name),
|
|
723
|
+
panels = panels, facet = if (length(panels) > 1L) "slices" else "Null", coord = "heatmap", scales = scale,
|
|
724
|
+
positions = list(x = list(discrete = TRUE, levels = I(as.character(name_of(colnames(m), c_all)))), y = list(discrete = TRUE, levels = I(as.character(name_of(rownames(m), r_all))))))
|
|
725
|
+
}
|
|
726
|
+
list(svg = s$svg, problems = I(s$problems),
|
|
727
|
+
units = lapply(units, function(u) list(layer = u$layer - 1L, panel = u$panel, marks = lapply(u$marks, function(m) I(as.integer(m) - 1L)))),
|
|
728
|
+
figure = list(kind = "ComplexHeatmap", title = plots[[1]]$title, plots = plots, layers = layers), marks = marks, sources = sources)
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
# The page's describer. Other plot systems join with a recognizer and a describe function of their
|
|
732
|
+
# own.
|
|
733
|
+
describers <- list(
|
|
734
|
+
ggplot = list(accepts = function(x) inherits(x, "ggplot"), describe = describe_ggplot, packages = c("ggplot2", "svglite", "xml2", "jsonlite")),
|
|
735
|
+
ComplexHeatmap = list(accepts = function(x) inherits(x, "rho_heatmap"), describe = describe_heatmap, packages = c("ComplexHeatmap", "svglite", "xml2", "jsonlite")),
|
|
736
|
+
graphics = list(accepts = is_graphics_page, describe = describe_graphics, packages = c("gridGraphics", "svglite", "xml2", "jsonlite")))
|
|
737
|
+
|
|
738
|
+
describe_page <- function(object, file, width, height, into = sub("\\.png$", ".figure.json", file), recorded = NULL) {
|
|
739
|
+
name <- names(Filter(function(d) d$accepts(object), describers))[1]
|
|
740
|
+
if (is.na(name)) return(NULL)
|
|
741
|
+
d <- describers[[name]]
|
|
742
|
+
missing <- d$packages[!vapply(d$packages, requireNamespace, TRUE, quietly = TRUE)]
|
|
743
|
+
if (length(missing)) return(list(problem = paste0("Install ", paste(missing, collapse = ", "), " in this session's library to make plots like this one selectable.")))
|
|
744
|
+
had <- exists(".Random.seed", envir = globalenv(), inherits = FALSE)
|
|
745
|
+
if (had) seed <- get(".Random.seed", envir = globalenv())
|
|
746
|
+
on.exit({
|
|
747
|
+
if (had) assign(".Random.seed", seed, envir = globalenv())
|
|
748
|
+
else if (exists(".Random.seed", envir = globalenv(), inherits = FALSE)) rm(".Random.seed", envir = globalenv())
|
|
749
|
+
}, add = TRUE)
|
|
750
|
+
started <- Sys.time()
|
|
751
|
+
result <- NULL; problem <- NULL
|
|
752
|
+
utils::capture.output(withCallingHandlers(
|
|
753
|
+
tryCatch({
|
|
754
|
+
setTimeLimit(elapsed = limits$maxSeconds, transient = TRUE)
|
|
755
|
+
on.exit(setTimeLimit(elapsed = Inf), add = TRUE)
|
|
756
|
+
result <- d$describe(object, width, height, recorded = recorded)
|
|
757
|
+
}, error = function(e) problem <<- conditionMessage(e)),
|
|
758
|
+
warning = function(w) invokeRestart("muffleWarning"), message = function(m) invokeRestart("muffleMessage")))
|
|
759
|
+
setTimeLimit(elapsed = Inf)
|
|
760
|
+
if (!is.null(problem)) return(list(problem = if (grepl("time limit", problem)) sprintf("Describing the plot took longer than %d s; it stays a picture.", limits$maxSeconds) else problem))
|
|
761
|
+
versions <- vapply(setdiff(d$packages, c("xml2", "jsonlite")), function(p) as.character(utils::packageVersion(p)), "")
|
|
762
|
+
record <- c(list(schema = schema, describer = name, size = list(width = width, height = height, units = "in"),
|
|
763
|
+
versions = as.list(c(R = as.character(getRversion()), versions)), seconds = round(as.numeric(Sys.time() - started, units = "secs"), 2)), result)
|
|
764
|
+
json <- jsonlite::toJSON(record, auto_unbox = TRUE, null = "null", na = "null", digits = NA)
|
|
765
|
+
if (nchar(json, type = "bytes") > limits$maxBytes) return(list(problem = sprintf("The figure record would take %.1f MB, more than %d MB.", nchar(json, type = "bytes") / 1048576, limits$maxBytes / 1048576)))
|
|
766
|
+
out <- into
|
|
767
|
+
tmp <- paste0(out, ".tmp")
|
|
768
|
+
writeLines(json, tmp, useBytes = TRUE)
|
|
769
|
+
file.rename(tmp, out)
|
|
770
|
+
f <- result$figure
|
|
771
|
+
Filter(Negate(is.null), list(describer = name, kind = f$kind, title = f$title, layers = length(f$layers),
|
|
772
|
+
marks = sum(vapply(f$layers, `[[`, 0L, "marks")),
|
|
773
|
+
bound = length(unique(unlist(lapply(result$units, function(u) paste(u$layer, unlist(u$marks)))))),
|
|
774
|
+
geoms = I(vapply(f$layers, `[[`, "", "geom")), file = basename(out)))
|
|
775
|
+
}
|