@rho-workspace/rho 0.5.5 → 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.
Files changed (250) hide show
  1. package/dist/agent-connect.d.ts +42 -0
  2. package/dist/agent-connect.js +68 -0
  3. package/dist/answer-board.js +3 -3
  4. package/dist/bridge.js +6 -1
  5. package/dist/browser-access.d.ts +1 -0
  6. package/dist/browser-access.js +5 -3
  7. package/dist/capabilities.d.ts +21 -5
  8. package/dist/capabilities.js +6 -4
  9. package/dist/cli.js +113 -18
  10. package/dist/composition.d.ts +1 -1
  11. package/dist/composition.js +13 -3
  12. package/dist/connect-cli.d.ts +50 -0
  13. package/dist/connect-cli.js +128 -0
  14. package/dist/contract.generated.d.ts +1437 -381
  15. package/dist/contract.generated.js +1 -1
  16. package/dist/contracts/owners.generated.js +1160 -267
  17. package/dist/conversation-presentation.d.ts +9 -2
  18. package/dist/conversation-presentation.js +3 -1
  19. package/dist/conversations.d.ts +306 -7
  20. package/dist/conversations.js +208 -22
  21. package/dist/discovery.js +3 -3
  22. package/dist/extension-contract.d.ts +19 -0
  23. package/dist/extension-contract.js +15 -6
  24. package/dist/extension-markets.d.ts +74 -0
  25. package/dist/extension-markets.js +166 -0
  26. package/dist/extensions.d.ts +29 -1
  27. package/dist/extensions.js +13 -4
  28. package/dist/external-work.d.ts +22 -0
  29. package/dist/external-work.js +91 -0
  30. package/dist/plugin-cli.js +75 -3
  31. package/dist/plugin-inventory.d.ts +56 -0
  32. package/dist/plugin-inventory.js +41 -0
  33. package/dist/plugin-inventory.json +1581 -102
  34. package/dist/plugin-presentation.d.ts +105 -2
  35. package/dist/plugin-presentation.js +59 -5
  36. package/dist/plugins.d.ts +14 -3
  37. package/dist/plugins.js +12 -6
  38. package/dist/project-notes.js +1 -1
  39. package/dist/protocol.d.ts +7 -0
  40. package/dist/protocol.js +40 -15
  41. package/dist/replay.js +4 -0
  42. package/dist/response-contracts.js +26 -11
  43. package/dist/running.d.ts +10 -0
  44. package/dist/running.js +45 -0
  45. package/dist/server.d.ts +4 -10
  46. package/dist/server.js +140 -14
  47. package/dist/state.d.ts +8 -0
  48. package/dist/state.js +8 -0
  49. package/dist/studio-layout.js +3 -3
  50. package/dist/task-frame.d.ts +1 -1
  51. package/dist/timeline-events.d.ts +1 -0
  52. package/dist/timeline-events.js +8 -1
  53. package/dist/ui-context.d.ts +73 -0
  54. package/dist/ui-context.js +98 -0
  55. package/dist/ui-contract.d.ts +23 -0
  56. package/dist/web/.vite/manifest.json +135 -76
  57. package/dist/web/assets/answer-board-CeYS8k8y.js +1 -0
  58. package/dist/web/assets/choices-D8vGwsl2.js +1 -0
  59. package/dist/web/assets/{console-BSjLGe0T.js → console-Dyuo1q4N.js} +4 -4
  60. package/dist/web/assets/{dist-Tzoe_zm-.js → dist-CSiCyBml.js} +1 -1
  61. package/dist/web/assets/{dist-Cw8hRIlT.js → dist-D9FLb9Hm.js} +1 -1
  62. package/dist/web/assets/{dist-foH9IKc6.js → dist-FXbTBwML.js} +1 -1
  63. package/dist/web/assets/{dist-Buk0lwzI.js → dist-l3DUhbw5.js} +1 -1
  64. package/dist/web/assets/{explore-in-BVUy3ZW_.js → explore-in-BfSFGRh2.js} +1 -1
  65. package/dist/web/assets/graph-BU2_o5uA.js +2 -0
  66. package/dist/web/assets/index-C16mXVvk.css +1 -0
  67. package/dist/web/assets/index-CWg9XYXx.js +952 -0
  68. package/dist/web/assets/markdown.reader-Ct2-e2Zt.png +0 -0
  69. package/dist/web/assets/{notes-meaning-pVbbQUGP.js → notes-meaning-T2m5zQfv.js} +1 -1
  70. package/dist/web/assets/overview-PzfLJCo-.js +6 -0
  71. package/dist/web/assets/preview-BguEdsza.js +1 -0
  72. package/dist/web/assets/previews-pWNik-2g.js +3 -0
  73. package/dist/web/assets/{review-CMxQlQ-C.js → review-Dy_85lcH.js} +1 -1
  74. package/dist/web/assets/rho.editor-CCJeyHma.png +0 -0
  75. package/dist/web/assets/rho.flow-WkMj6fRS.png +0 -0
  76. package/dist/web/assets/rho.media-Cc6amodO.png +0 -0
  77. package/dist/web/assets/rho.notebook-6vsZKGDq.png +0 -0
  78. package/dist/web/assets/rho.object-BGoeXjte.png +0 -0
  79. package/dist/web/assets/rho.plots-yIiISCkb.png +0 -0
  80. package/dist/web/assets/rho.scatter-DwHxesCs.png +0 -0
  81. package/dist/web/assets/rho.servers-DKf_mnNq.png +0 -0
  82. package/dist/web/assets/rho.table-DWOGaWDa.png +0 -0
  83. package/dist/web/assets/rho.variables-CKcWczhQ.png +0 -0
  84. package/dist/web/assets/{skills-BFNSyrnD.js → skills-Dyz7mBlc.js} +1 -1
  85. package/dist/web/assets/{skills-work-BGPSnF_I.js → skills-work-BEGqlCkx.js} +1 -1
  86. package/dist/web/assets/studio-process-D-ykbC4D.js +2 -0
  87. package/dist/web/assets/timeline-events-DKTho9fU.js +1 -0
  88. package/dist/web/assets/work-turns-DSabCogR.js +1 -0
  89. package/dist/web/assets/{workspace-CWlkjLNx.js → workspace-DfjBL--r.js} +2 -2
  90. package/dist/web/composition.json +135 -80
  91. package/dist/web/index.html +3 -3
  92. package/dist/web/renderer.json +1 -1
  93. package/dist/web-assets.js +2 -2
  94. package/dist/work-graph.d.ts +11 -1
  95. package/dist/work-graph.js +16 -5
  96. package/dist/workspace.d.ts +2 -2
  97. package/dist/workspace.js +8 -7
  98. package/docs/FRONTEND.md +71 -21
  99. package/docs/HEADLESS.md +127 -24
  100. package/docs/THIRD-PARTY-PLUGINS.md +75 -3
  101. package/node_modules/@rho-workspace/plugin-agent/README.md +34 -0
  102. package/node_modules/@rho-workspace/plugin-agent/dist/acp-command.d.ts +32 -0
  103. package/node_modules/@rho-workspace/plugin-agent/dist/acp-command.js +54 -11
  104. package/node_modules/@rho-workspace/plugin-agent/dist/acp-conversation.d.ts +1 -0
  105. package/node_modules/@rho-workspace/plugin-agent/dist/acp-conversation.js +29 -3
  106. package/node_modules/@rho-workspace/plugin-agent/dist/external-session-contract.js +11 -5
  107. package/node_modules/@rho-workspace/plugin-agent/dist/external-session.d.ts +12 -1
  108. package/node_modules/@rho-workspace/plugin-agent/dist/index.d.ts +8 -0
  109. package/node_modules/@rho-workspace/plugin-agent/dist/index.js +3 -2
  110. package/node_modules/@rho-workspace/plugin-agent/dist/mcp-entry.d.ts +13 -0
  111. package/node_modules/@rho-workspace/plugin-agent/dist/mcp-entry.js +13 -0
  112. package/node_modules/@rho-workspace/plugin-agent/dist/mcp.js +16 -8
  113. package/node_modules/@rho-workspace/plugin-agent/dist/native-sessions.d.ts +70 -0
  114. package/node_modules/@rho-workspace/plugin-agent/dist/native-sessions.js +538 -0
  115. package/node_modules/@rho-workspace/plugin-agent/package.json +2 -1
  116. package/node_modules/@rho-workspace/plugin-figures/LICENSE +661 -0
  117. package/node_modules/@rho-workspace/plugin-figures/README.md +191 -0
  118. package/node_modules/@rho-workspace/plugin-figures/dist/index.d.ts +236 -0
  119. package/node_modules/@rho-workspace/plugin-figures/dist/index.js +165 -0
  120. package/node_modules/@rho-workspace/plugin-figures/dist/operations.d.ts +44 -0
  121. package/node_modules/@rho-workspace/plugin-figures/dist/operations.js +39 -0
  122. package/node_modules/@rho-workspace/plugin-figures/dist/presentation.d.ts +19 -0
  123. package/node_modules/@rho-workspace/plugin-figures/dist/presentation.js +26 -0
  124. package/node_modules/@rho-workspace/plugin-figures/dist/record.d.ts +188 -0
  125. package/node_modules/@rho-workspace/plugin-figures/dist/record.js +143 -0
  126. package/node_modules/@rho-workspace/plugin-figures/dist/ui-styles.d.ts +1 -0
  127. package/node_modules/@rho-workspace/plugin-figures/dist/ui-styles.js +68 -0
  128. package/node_modules/@rho-workspace/plugin-figures/dist/ui.d.ts +56 -0
  129. package/node_modules/@rho-workspace/plugin-figures/dist/ui.js +363 -0
  130. package/node_modules/@rho-workspace/plugin-figures/package.json +41 -0
  131. package/node_modules/@rho-workspace/plugin-figures/py/figures.py +856 -0
  132. package/node_modules/@rho-workspace/plugin-figures/r/figures.R +775 -0
  133. package/node_modules/@rho-workspace/plugin-kernel/README.md +27 -3
  134. package/node_modules/@rho-workspace/plugin-kernel/dist/contract.d.ts +12 -0
  135. package/node_modules/@rho-workspace/plugin-kernel/dist/contract.js +8 -1
  136. package/node_modules/@rho-workspace/plugin-kernel/dist/index.d.ts +61 -0
  137. package/node_modules/@rho-workspace/plugin-kernel/dist/index.js +125 -9
  138. package/node_modules/@rho-workspace/plugin-kernel/dist/session-scope.d.ts +14 -0
  139. package/node_modules/@rho-workspace/plugin-kernel/dist/session-scope.js +9 -0
  140. package/node_modules/@rho-workspace/plugin-kernel/dist/ui-base.d.ts +23 -5
  141. package/node_modules/@rho-workspace/plugin-kernel/dist/ui-notebook-work.js +1 -1
  142. package/node_modules/@rho-workspace/plugin-kernel/dist/ui-notebook.d.ts +6 -4
  143. package/node_modules/@rho-workspace/plugin-kernel/dist/ui-notebook.js +44 -17
  144. package/node_modules/@rho-workspace/plugin-kernel/dist/ui-styles.js +6 -0
  145. package/node_modules/@rho-workspace/plugin-kernel/package.json +1 -1
  146. package/node_modules/@rho-workspace/plugin-kernel/runners/runner.R +144 -4
  147. package/node_modules/@rho-workspace/plugin-kernel/runners/runner.py +106 -1
  148. package/node_modules/@rho-workspace/plugin-tables/README.md +5 -0
  149. package/node_modules/@rho-workspace/plugin-tables/dist/index.d.ts +1 -0
  150. package/node_modules/@rho-workspace/plugin-tables/dist/index.js +19 -3
  151. package/node_modules/@rho-workspace/plugin-tables/dist/ui.js +1 -1
  152. package/node_modules/@rho-workspace/plugin-tables/package.json +1 -1
  153. package/node_modules/hono/dist/adapter/aws-lambda/handler.js +1 -1
  154. package/node_modules/hono/dist/cjs/adapter/aws-lambda/handler.js +1 -1
  155. package/node_modules/hono/dist/cjs/adapter/bun/serve-static.js +1 -1
  156. package/node_modules/hono/dist/cjs/adapter/bun/websocket.js +1 -1
  157. package/node_modules/hono/dist/cjs/client/fetch-result-please.js +1 -1
  158. package/node_modules/hono/dist/cjs/helper/accepts/accepts.js +2 -1
  159. package/node_modules/hono/dist/cjs/helper/ssg/ssg.js +5 -3
  160. package/node_modules/hono/dist/cjs/jsx/base.js +49 -30
  161. package/node_modules/hono/dist/cjs/jsx/components.js +2 -2
  162. package/node_modules/hono/dist/cjs/jsx/constants.js +2 -0
  163. package/node_modules/hono/dist/cjs/jsx/jsx-runtime.js +13 -17
  164. package/node_modules/hono/dist/cjs/middleware/etag/index.js +3 -1
  165. package/node_modules/hono/dist/cjs/middleware/pretty-json/index.js +1 -0
  166. package/node_modules/hono/dist/cjs/middleware/timing/timing.js +4 -3
  167. package/node_modules/hono/dist/cjs/utils/cookie.js +2 -2
  168. package/node_modules/hono/dist/cjs/utils/mime.js +1 -1
  169. package/node_modules/hono/dist/cjs/utils/url.js +1 -1
  170. package/node_modules/hono/dist/client/fetch-result-please.js +1 -1
  171. package/node_modules/hono/dist/helper/accepts/accepts.js +2 -1
  172. package/node_modules/hono/dist/helper/ssg/ssg.js +5 -3
  173. package/node_modules/hono/dist/jsx/base.js +49 -32
  174. package/node_modules/hono/dist/jsx/components.js +2 -2
  175. package/node_modules/hono/dist/jsx/constants.js +2 -1
  176. package/node_modules/hono/dist/jsx/jsx-runtime.js +15 -19
  177. package/node_modules/hono/dist/middleware/etag/index.js +3 -1
  178. package/node_modules/hono/dist/middleware/language/language.js +1 -1
  179. package/node_modules/hono/dist/middleware/pretty-json/index.js +1 -0
  180. package/node_modules/hono/dist/middleware/timing/timing.js +4 -3
  181. package/node_modules/hono/dist/utils/cookie.js +2 -2
  182. package/node_modules/hono/dist/utils/mime.js +1 -1
  183. package/node_modules/hono/dist/utils/url.js +1 -1
  184. package/node_modules/hono/package.json +1 -1
  185. package/node_modules/jose/dist/webapi/jwe/general/decrypt.js +3 -2
  186. package/node_modules/jose/dist/webapi/jwks/remote.js +1 -1
  187. package/node_modules/jose/dist/webapi/jws/general/verify.js +2 -8
  188. package/node_modules/jose/dist/webapi/lib/jwe_decrypt.js +11 -8
  189. package/node_modules/jose/dist/webapi/lib/key_management.js +0 -1
  190. package/node_modules/jose/package.json +1 -1
  191. package/package.json +6 -4
  192. package/protocol/index.json +106 -64
  193. package/protocol/operations/agent.sessions.list.json +57 -3
  194. package/protocol/operations/agent.sessions.read.json +61 -4
  195. package/protocol/operations/capability.list.json +382 -6
  196. package/protocol/operations/composition.apply.json +10 -2
  197. package/protocol/operations/composition.delete.json +845 -0
  198. package/protocol/operations/composition.read.json +10 -2
  199. package/protocol/operations/composition.undo.json +10 -2
  200. package/protocol/operations/conversation.agents.add.json +346 -0
  201. package/protocol/operations/conversation.agents.remove.json +340 -0
  202. package/protocol/operations/conversation.configure.json +29 -0
  203. package/protocol/operations/conversation.decide.json +225 -0
  204. package/protocol/operations/conversation.list.json +29 -0
  205. package/protocol/operations/conversation.permission.json +225 -0
  206. package/protocol/operations/conversation.read.json +225 -0
  207. package/protocol/operations/conversation.restore.json +29 -0
  208. package/protocol/operations/conversation.send.json +294 -0
  209. package/protocol/operations/conversation.settings.check.json +226 -416
  210. package/protocol/operations/conversation.settings.read.json +224 -411
  211. package/protocol/operations/conversation.settings.update.json +248 -475
  212. package/protocol/operations/conversation.start.json +313 -6
  213. package/protocol/operations/conversation.stop.json +225 -0
  214. package/protocol/operations/conversation.trash.json +29 -0
  215. package/protocol/operations/extension.describe.json +39 -0
  216. package/protocol/operations/extension.install.json +62 -0
  217. package/protocol/operations/extension.list.json +164 -1
  218. package/protocol/operations/extension.market.change.json +210 -0
  219. package/protocol/operations/figure.read.json +265 -0
  220. package/protocol/operations/figure.select.json +157 -0
  221. package/protocol/operations/kernel.interrupt.json +119 -0
  222. package/protocol/operations/kernel.read.json +119 -0
  223. package/protocol/operations/kernel.run.json +119 -0
  224. package/protocol/operations/observation.snapshot.json +445 -8
  225. package/protocol/operations/project.openFile.json +10 -2
  226. package/protocol/operations/session.grant.json +4 -0
  227. package/protocol/operations/session.instructions.json +43 -0
  228. package/protocol/operations/shared.json +1 -0
  229. package/protocol/operations/view.close.json +10 -2
  230. package/protocol/operations/view.protection.set.json +10 -2
  231. package/vendor/composition.json +174 -109
  232. package/vendor/rho-workspace-plugin-agent-0.4.33.tgz +0 -0
  233. package/vendor/rho-workspace-plugin-figures-0.5.1.tgz +0 -0
  234. package/vendor/rho-workspace-plugin-kernel-0.5.13.tgz +0 -0
  235. package/vendor/rho-workspace-plugin-tables-0.5.2.tgz +0 -0
  236. package/web/theme.css +17 -0
  237. package/dist/web/assets/answer-board-CBkGbSLD.js +0 -1
  238. package/dist/web/assets/canvas-D8rDdfDz.js +0 -1
  239. package/dist/web/assets/choices-b0XhrGQV.js +0 -1
  240. package/dist/web/assets/graph-1OdIJ9l7.js +0 -2
  241. package/dist/web/assets/index-CsC3sTRq.css +0 -1
  242. package/dist/web/assets/index-CxmItXqa.js +0 -891
  243. package/dist/web/assets/overview-B9UtVJ32.js +0 -6
  244. package/dist/web/assets/preview-CksD_5KS.js +0 -1
  245. package/dist/web/assets/studio-process-CsRoTHOh.js +0 -2
  246. package/dist/web/assets/timeline-events-BbuuFWHV.js +0 -1
  247. package/dist/web/assets/work-turns-BvJlIMmA.js +0 -1
  248. package/vendor/rho-workspace-plugin-agent-0.4.29.tgz +0 -0
  249. package/vendor/rho-workspace-plugin-kernel-0.5.5.tgz +0 -0
  250. 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
+ }