1 |
### * None of the functions in this file is exported |
|
2 | ||
3 |
### * bezierCurve() |
|
4 | ||
5 |
#' Calculate the coordinates of a Bezier curve from control points |
|
6 |
#' |
|
7 |
#' @param x,y Coordinates of the control points |
|
8 |
#' @param n Integer, number of steps to use (larger is smoother) |
|
9 |
#' |
|
10 |
#' @return A two-column matrix with the coordinates of the Bezier curve |
|
11 |
#' |
|
12 |
#' @examples |
|
13 |
#' bezierCurve <- isotracer:::bezierCurve |
|
14 |
#' set.seed(9) |
|
15 |
#' x = runif(6) |
|
16 |
#' y = runif(6) |
|
17 |
#' plot(x, y, type = "l", col = "grey") |
|
18 |
#' lines(bezierCurve(x, y, 2), col = "cornflowerblue", lwd = 1) |
|
19 |
#' lines(bezierCurve(x, y, 4), col = "cornflowerblue", lwd = 1) |
|
20 |
#' lines(bezierCurve(x, y, 8), col = "purple", lwd = 1.5) |
|
21 |
#' lines(bezierCurve(x, y, 64), col = "indianred", lwd = 4) |
|
22 |
#' |
|
23 |
#' @keywords internal |
|
24 |
#' @noRd |
|
25 | ||
26 |
bezierCurve = function(x, y, n) { |
|
27 | 67x |
stopifnot(length(x) == length(y)) |
28 | 67x |
stopifnot(length(x) > 1) |
29 | 67x |
n = as.integer(n) |
30 | 67x |
stopifnot(n > 0) |
31 | 67x |
xOut = rep(NA, n + 2) |
32 | 67x |
yOut = rep(NA, n + 2) |
33 | 67x |
weights = seq(0, 1, length.out = n + 2) |
34 |
# Calculate coordinates |
|
35 | 67x |
for (i in seq_along(weights)) { |
36 | 2278x |
xControl = x |
37 | 2278x |
yControl = y |
38 | 2278x |
nControl = length(xControl) |
39 | 2278x |
weight = weights[i] |
40 | 2278x |
while(nControl > 1) { |
41 | 11390x |
xControl = (1 - weight) * xControl[1:(nControl - 1)] + weight * xControl[2:nControl] |
42 | 11390x |
yControl = (1 - weight) * yControl[1:(nControl - 1)] + weight * yControl[2:nControl] |
43 | 11390x |
nControl = length(xControl) |
44 |
} |
|
45 | 2278x |
stopifnot(nControl == 1) |
46 | 2278x |
xOut[i] = xControl |
47 | 2278x |
yOut[i] = yControl |
48 |
} |
|
49 |
# Return |
|
50 | 67x |
return(as.matrix(cbind(xOut, yOut))) |
51 |
} |
|
52 | ||
53 |
### * ribbonFromTrajectory() |
|
54 | ||
55 |
#' Calculate a ribbon polygon spanning a trajectory |
|
56 |
#' |
|
57 |
#' @param x Two-column object giving the coordinates defining the indicating |
|
58 |
#' trajectory. |
|
59 |
#' @param width Numeric, width of the returned segment on the x scale. |
|
60 |
#' @param constantWidth.y Boolean, if TRUE the ribbon coordinates are corrected |
|
61 |
#' for the aspect ratio of the plot so that the ribbon has a constant y |
|
62 |
#' width. |
|
63 |
#' |
|
64 |
#' @return A two-columnm matrix containing the coordinates of the ribbon, ready |
|
65 |
#' to be used with \code{\link{polygon}} |
|
66 |
#' |
|
67 |
#' @examples |
|
68 |
#' bezierCurve <- isotracer:::bezierCurve |
|
69 |
#' ribbonFromTrajectory <- isotracer:::ribbonFromTrajectory |
|
70 |
#' |
|
71 |
#' set.seed(8) |
|
72 |
#' x = runif(4) |
|
73 |
#' y = runif(4) |
|
74 |
#' b = bezierCurve(x, y, 64) |
|
75 |
#' |
|
76 |
#' plot(x, y, type = "l", col = "grey", asp = 1, main = "Plot asp = 1") |
|
77 |
#' lines(b, col = "indianred", lwd = 2) |
|
78 |
#' bvp <- gridBase::baseViewports() |
|
79 |
#' do.call(grid::pushViewport, bvp) |
|
80 |
#' polygon(ribbonFromTrajectory(b, width = 0.05), |
|
81 |
#' col = adjustcolor("cornflowerblue", alpha.f = 0.5)) |
|
82 |
#' points(ribbonFromTrajectory(b, width = 0.05), pch = 19, cex = 0.5) |
|
83 |
#' points(ribbonFromTrajectory(b, width = 0.10), pch = 19, cex = 0.5, col = "red") |
|
84 |
#' |
|
85 |
#' plot(x, y, type = "l", col = "grey", asp = 3, main = "Plot asp = 3, constant y width") |
|
86 |
#' lines(b, col = "indianred", lwd = 2) |
|
87 |
#' polygon(ribbonFromTrajectory(b, width = 0.05), |
|
88 |
#' col = adjustcolor("cornflowerblue", alpha.f = 0.5)) |
|
89 |
#' |
|
90 |
#' plot(x, y, type = "l", col = "grey", asp = 3, main = "Plot asp = 3, no constant y width") |
|
91 |
#' lines(b, col = "indianred", lwd = 2) |
|
92 |
#' polygon(ribbonFromTrajectory(b, width = 0.05, constantWidth.y = FALSE), |
|
93 |
#' col = adjustcolor("cornflowerblue", alpha.f = 0.5)) |
|
94 |
#' |
|
95 |
#' @keywords internal |
|
96 |
#' @noRd |
|
97 | ||
98 |
ribbonFromTrajectory = function(x, width, constantWidth.y = TRUE) { |
|
99 | 134x |
xIn = x |
100 | 134x |
x = xIn[,1] |
101 | 134x |
y = xIn[,2] |
102 | 134x |
n = length(x) |
103 |
# Get the plot aspect ratio (this will probably fail if no plot exists yet) |
|
104 | 134x |
if (constantWidth.y) { |
105 |
## # https://stat.ethz.ch/pipermail/r-help/2005-October/080598.html |
|
106 |
## wRes = par("pin")[1] / diff(par("usr")[c(1, 2)]) |
|
107 |
## hRes = par("pin")[2] / diff(par("usr")[c(3, 4)]) |
|
108 |
## aspectRatio = hRes / wRes |
|
109 | 134x |
aspectRatio <- grid_get_asp() |
110 |
} else { |
|
111 |
# Use default value of one |
|
112 | ! |
aspectRatio = 1 |
113 |
} |
|
114 |
# Convert x values to asp=1 space |
|
115 | 134x |
xCenter = mean(x) |
116 | 134x |
x = (x - xCenter) / aspectRatio |
117 |
# Calculate one way |
|
118 | 134x |
x0 = x |
119 | 134x |
y0 = y |
120 | 134x |
xNormal = diff(x0) |
121 | 134x |
yNormal = diff(y0) |
122 | 134x |
xyNorm = sqrt(xNormal^2 + yNormal^2) |
123 | 134x |
thetas = acos(xNormal / xyNorm) |
124 | 134x |
thetas[yNormal < 0] = - thetas[yNormal < 0] |
125 | 134x |
out = matrix(ncol = 4, nrow = n) # Columns: xleft, yleft, xright, yright |
126 | 134x |
for (i in 1:(n-1)) { |
127 | 4422x |
initLeft = c(-width/2, 0) |
128 | 4422x |
initRight = c(width/2, 0) |
129 |
# Rotation |
|
130 | 4422x |
myAngle = thetas[i] + pi/2 |
131 | 4422x |
rotationMatrix = matrix(c(cos(myAngle), - sin(myAngle), |
132 | 4422x |
sin(myAngle), cos(myAngle)), |
133 | 4422x |
ncol = 2, byrow = T) |
134 | 4422x |
rotLeft = rotationMatrix %*% initLeft |
135 | 4422x |
rotRight = rotationMatrix %*% initRight |
136 | 4422x |
left = rotLeft + c(x0[i], y0[i]) |
137 | 4422x |
right = rotRight + c(x0[i], y0[i]) |
138 | 4422x |
out[i, ] = c(left, right) |
139 |
} |
|
140 |
# Add the last segment |
|
141 | 134x |
out[n, ] = c(rotLeft + c(x0[n], y0[n]), rotRight + c(x0[n], y0[n])) |
142 |
# Calculate the other way |
|
143 | 134x |
x0 = rev(x) |
144 | 134x |
y0 = rev(y) |
145 | 134x |
xNormal = diff(x0) |
146 | 134x |
yNormal = diff(y0) |
147 | 134x |
xyNorm = sqrt(xNormal^2 + yNormal^2) |
148 | 134x |
thetas = acos(xNormal / xyNorm) |
149 | 134x |
thetas[yNormal < 0] = - thetas[yNormal < 0] |
150 | 134x |
out2 = matrix(ncol = 4, nrow = n) # Columns: xleft, yleft, xright, yright |
151 | 134x |
for (i in 1:(n-1)) { |
152 | 4422x |
initLeft = c(-width/2, 0) |
153 | 4422x |
initRight = c(width/2, 0) |
154 |
# Rotation |
|
155 | 4422x |
myAngle = thetas[i] + pi/2 |
156 | 4422x |
rotationMatrix = matrix(c(cos(myAngle), - sin(myAngle), |
157 | 4422x |
sin(myAngle), cos(myAngle)), |
158 | 4422x |
ncol = 2, byrow = T) |
159 | 4422x |
rotLeft = rotationMatrix %*% initLeft |
160 | 4422x |
rotRight = rotationMatrix %*% initRight |
161 | 4422x |
left = rotLeft + c(x0[i], y0[i]) |
162 | 4422x |
right = rotRight + c(x0[i], y0[i]) |
163 | 4422x |
out2[i, ] = c(left, right) |
164 |
} |
|
165 |
# Add the last segment |
|
166 | 134x |
out2[n, ] = c(rotLeft + c(x0[n], y0[n]), rotRight + c(x0[n], y0[n])) |
167 |
# Average both out matrices |
|
168 | 134x |
out2 = out2[nrow(out2):1, ] |
169 | 134x |
outTmp = out2 |
170 | 134x |
out2[, 1:2] = out2[, 3:4] |
171 | 134x |
out2[, 3:4] = outTmp[, 1:2] |
172 | 134x |
for (i in 1:nrow(out)) { |
173 | 4556x |
for (j in 1:ncol(out)) { |
174 | 18224x |
out2[i, j] = (out[i, j] + out2[i, j]) / 2 |
175 |
} |
|
176 |
} |
|
177 |
# Arrange format |
|
178 | 134x |
out = rbind(out2[, 1:2], out2[nrow(out2):1, 3:4]) |
179 |
# Back-convert to plot aspect ratio space |
|
180 | 134x |
out[,1] = out[,1]*aspectRatio + xCenter |
181 |
# Return |
|
182 | 134x |
return(out) |
183 |
} |
|
184 | ||
185 |
### * grid_get_asp() |
|
186 | ||
187 |
#' Get the aspect ratio of the current grid viewport |
|
188 |
#' |
|
189 |
#' @examples |
|
190 |
#' grid_get_asp <- isotracer:::grid_get_asp |
|
191 |
#' |
|
192 |
#' library(grid) |
|
193 |
#' grid.newpage() |
|
194 |
#' x <- mtcars$wt |
|
195 |
#' y <- mtcars$drat |
|
196 |
#' vp <- dataViewport(xData = x, yData = y) |
|
197 |
#' pushViewport(vp) |
|
198 |
#' grid.points(x, y) |
|
199 |
#' |
|
200 |
#' # Run this snippet several times, and resize the plot window in-between |
|
201 |
#' # The newly drawn rectangle should always be exactly square. |
|
202 |
#' width <- 1 |
|
203 |
#' asp <- grid_get_asp() |
|
204 |
#' height <- width / asp |
|
205 |
#' col <- sample(colors(), 1) |
|
206 |
#' grid.rect(width = unit(width, "native"), height = unit(height, "native")) |
|
207 |
#' |
|
208 |
#' @keywords internal |
|
209 |
#' @noRd |
|
210 | ||
211 |
grid_get_asp <- function() { |
|
212 |
# Get x and y ranges |
|
213 | 134x |
vp <- grid::current.viewport() |
214 | 134x |
xrange <- diff(vp$xscale) |
215 | 134x |
yrange <- diff(vp$yscale) |
216 | 134x |
span <- min(xrange, yrange) / 2.5 |
217 | 134x |
xmid <- mean(vp$xscale) |
218 | 134x |
ymid <- mean(vp$yscale) |
219 | 134x |
loc_mid <- grid::deviceLoc(grid::unit(xmid, "native"), |
220 | 134x |
grid::unit(ymid, "native")) |
221 | 134x |
loc_right <- grid::deviceLoc(grid::unit(xmid + span, "native"), |
222 | 134x |
grid::unit(ymid, "native")) |
223 | 134x |
loc_top <- grid::deviceLoc(grid::unit(xmid, "native"), |
224 | 134x |
grid::unit(ymid + span, "native")) |
225 | 134x |
height <- as.numeric(loc_top$y) - as.numeric(loc_mid$y) |
226 | 134x |
width <- as.numeric(loc_right$x) - as.numeric(loc_mid$x) |
227 | 134x |
return(height/width) |
228 |
} |
|
229 | ||
230 |
### * topo_has_loop() |
|
231 | ||
232 |
#' Test if a topology has a loop |
|
233 |
#' |
|
234 |
#' @param x A topology matrix or a network model (the topology of the first row |
|
235 |
#' is used in this case). |
|
236 |
#' |
|
237 |
#' @return Boolean |
|
238 |
#' |
|
239 |
#' @examples |
|
240 |
#' topo_has_loop <- isotracer:::topo_has_loop |
|
241 |
#' |
|
242 |
#' topo_has_loop(aquarium_mod) |
|
243 |
#' topo_has_loop(trini_mod) |
|
244 |
#' |
|
245 |
#' @keywords internal |
|
246 |
#' @noRd |
|
247 | ||
248 |
topo_has_loop <- function(x) { |
|
249 | 10x |
if (is(x, "networkModel")) { |
250 | ! |
x <- unique(topo(x, simplify = FALSE)) |
251 | ! |
stopifnot(length(x) == 1) |
252 | ! |
x <- x[[1]] |
253 |
} |
|
254 | 10x |
x <- unclass(x) # Remove the "topo" class to treat it as a matrix |
255 | 10x |
n_comps <- ncol(x) |
256 |
# Calculate powers of the transition matrix |
|
257 | 10x |
iters <- list() |
258 | 10x |
iters[[1]] <- x |
259 | 10x |
if (n_comps == 1) { |
260 | ! |
stop("Only one compartment present in the network topology.") |
261 |
} |
|
262 | 10x |
for (i in 2:n_comps) { |
263 | 73x |
iters[[i]] <- iters[[i-1]] %*% x |
264 |
} |
|
265 |
# Look for loops |
|
266 | 10x |
loops <- lapply(iters, diag) |
267 | 10x |
any_loop <- sapply(loops, function(l) any(l!=0)) |
268 |
# Return |
|
269 | 10x |
return(any(any_loop)) |
270 |
} |
|
271 | ||
272 |
is_dag <- function(x) { |
|
273 | 6x |
!topo_has_loop(x) |
274 |
} |
|
275 | ||
276 |
### * not_implemented_for_topology_with_loop() |
|
277 | ||
278 |
#' Check and throw an error if appropriate |
|
279 |
#' |
|
280 |
#' @param x A topology matrix or a network model (the topology of the first row |
|
281 |
#' is used in this case). |
|
282 |
#' |
|
283 |
#' @keywords internal |
|
284 |
#' @noRd |
|
285 | ||
286 |
not_implemented_for_topology_with_loop <- function(x) { |
|
287 | 4x |
if (topo_has_loop(x)) { |
288 | ! |
stop("Function not implemented for topologies containing loop(s).") |
289 |
} |
|
290 |
} |
|
291 | ||
292 |
### * sankey_get_layout() |
|
293 | ||
294 |
#' Process layout option for a given topology |
|
295 |
#' |
|
296 |
#' @param x A topology. |
|
297 |
#' @param layout String or NULL, node-placing algorithm to use from the ggraph |
|
298 |
#' package (e.g. "stress" or "sugiyama"). If NULL, a default layout is |
|
299 |
#' returned, which depends on the topology. The ggraph package itself uses |
|
300 |
#' some algoritms from the igraph package. See the Details in the help of |
|
301 |
#' \code{\link[ggraph]{layout_tbl_graph_igraph}} for available |
|
302 |
#' algorithms. The ggraph package must be installed for this argument to be |
|
303 |
#' taken into account. |
|
304 |
#' |
|
305 |
#' @return A string |
|
306 |
#' |
|
307 |
#' @keywords internal |
|
308 |
#' @noRd |
|
309 | ||
310 |
sankey_get_layout <- function(x, layout) { |
|
311 | 8x |
if (is.null(layout)) { |
312 | 6x |
if (is_dag(x)) { |
313 | ! |
layout <- "left2right" |
314 |
} else { |
|
315 | 6x |
layout <- "stress" |
316 |
} |
|
317 |
} |
|
318 | 8x |
return(layout) |
319 |
} |
|
320 | ||
321 | ||
322 |
### * sankey_calc_nodes_locations() |
|
323 | ||
324 |
#' Calculate nodes locations for a topology |
|
325 |
#' |
|
326 |
#' @param x A topology. |
|
327 |
#' @param layout String, one of "in-house" or the layout options accepted by |
|
328 |
#' \code{\link[ggraph]{create_layout}}. |
|
329 |
#' @param minimize_simple (For in-house algorithm.) Boolean, apply simple |
|
330 |
#' energy minimization by adjusting consumers location based on their |
|
331 |
#' sources y location? |
|
332 |
#' @param minimize_E Boolean, use \code{\link{sankey_minimize_energy_sim}} to |
|
333 |
#' adjust node locations using an energy minimization algorithm? |
|
334 |
#' @param k1 Numeric, stiffness for springs between nodes on the same x |
|
335 |
#' position (used if minimize_E = TRUE). |
|
336 |
#' @param k2 Numeric, sitffness for springs between connected nodes (used if |
|
337 |
#' minimize_E = TRUE). |
|
338 |
#' @param l1 Numeric, rest length for springs with k1 stiffness (used if |
|
339 |
#' minimize_E = TRUE). |
|
340 |
#' @param l2 Numeric, rest length for springs with k2 stiffness (used if |
|
341 |
#' minimize_E = TRUE). |
|
342 |
#' |
|
343 |
#' @return A tibble with the nodes and ordered x and y values. The columns are |
|
344 |
#' "comp", "x", and "y". |
|
345 |
#' |
|
346 |
#' @examples |
|
347 |
#' sankey_calc_nodes_locations <- isotracer:::sankey_calc_nodes_locations |
|
348 |
#' |
|
349 |
#' sankey_calc_nodes_locations(topo(trini_mod), "left2right") |
|
350 |
#' sankey_calc_nodes_locations(topo(aquarium_mod), "stress") |
|
351 |
#' |
|
352 |
#' @keywords internal |
|
353 |
#' @noRd |
|
354 | ||
355 |
sankey_calc_nodes_locations <- function(x, layout, |
|
356 |
minimize_simple = TRUE, |
|
357 |
minimize_E = FALSE, k1 = 1, k2 = 1, l1 = 1, l2 = 1) { |
|
358 | 8x |
if (layout %in% c("left2right", "left-to-right")) { |
359 |
# In-house algorithm for topologies without loops |
|
360 | 2x |
not_implemented_for_topology_with_loop(x) |
361 | 2x |
nodes <- sankey_calc_nodes_locations_inhouse(x, minimize_simple = minimize_simple) |
362 | 2x |
if (minimize_E) { |
363 | ! |
nodes <- sankey_minimize_energy_sim(x, nodes, k1 = k1, k2 = k2, l1 = l1, |
364 | ! |
l2 = l2) |
365 |
} |
|
366 |
} else { |
|
367 | 6x |
if (!requireNamespace("ggraph", quietly = TRUE)) { |
368 | ! |
stop("Package \"ggraph\" needed when specifying a layout argument. Please install it.", |
369 | ! |
call. = FALSE) |
370 |
} |
|
371 |
# Check that the random seed is not reset by graphlayouts functions |
|
372 | 6x |
if (exists(".Random.seed", .GlobalEnv)) { |
373 | 6x |
prev_seed <- .GlobalEnv[[".Random.seed"]] |
374 | ! |
} else { prev_seed <- NULL } |
375 | 6x |
x <- as_tbl_graph(x) |
376 | 6x |
nodes <- ggraph::create_layout(graph = x, layout = layout) |
377 | 6x |
if (exists(".Random.seed", .GlobalEnv)) { |
378 | 6x |
new_seed <- .GlobalEnv[[".Random.seed"]] |
379 | ! |
} else { new_seed <- NULL } |
380 | 6x |
if (!identical(prev_seed, new_seed)) { |
381 | ! |
stop("Random seed was reset when calling ggraph::create_layout() in sankey_calc_nodes_locations().\n", |
382 | ! |
"This is unwanted behaviour, please report it as a bug to the isotracer authors.") |
383 |
} |
|
384 | 6x |
nodes <- tibble::as_tibble(nodes[, c("name", "x", "y")]) |
385 | 6x |
names(nodes) <- c("comp", "x", "y") |
386 |
} |
|
387 | 8x |
attr(nodes, "layout") <- layout |
388 | 8x |
return(nodes) |
389 |
} |
|
390 | ||
391 |
### * sankey_calc_nodes_locations_inhouse() |
|
392 | ||
393 |
#' Calculate roughly optimized nodes locations for a topology (in-house algorithm) |
|
394 |
#' |
|
395 |
#' @param x A topology matrix or a network model (the topology of the first row |
|
396 |
#' is used in this case). |
|
397 |
#' @param minimize_simple Boolean, apply simple energy minimization by |
|
398 |
#' adjusting consumers location based on their sources y location. |
|
399 |
#' |
|
400 |
#' @return A tibble with the nodes and ordered x and y values. |
|
401 |
#' |
|
402 |
#' @keywords internal |
|
403 |
#' @noRd |
|
404 | ||
405 |
sankey_calc_nodes_locations_inhouse <- function(x, minimize_simple) { |
|
406 | 2x |
if (is(x, "networkModel")) { |
407 | ! |
x <- unique(topo(x, simplify = FALSE)) |
408 | ! |
stopifnot(length(x) == 1) |
409 | ! |
x <- x[[1]] |
410 |
} |
|
411 | 2x |
x <- unclass(x) # Remove the "topo" class to treat it as a matrix |
412 | 2x |
not_implemented_for_topology_with_loop(x) |
413 | 2x |
z <- topo_x_ordering(x) |
414 | 2x |
comps <- names(z) |
415 | 2x |
d <- tibble::tibble(comp = comps, x = z) |
416 | 2x |
nodes <- topo_y_ordering(x, d) |
417 |
# Center y locations |
|
418 | 2x |
x_ranks <- unique(nodes$x) |
419 | 2x |
for (xi in x_ranks) { |
420 | 8x |
y_shift <- mean(nodes$y[nodes$x == xi]) |
421 | 8x |
nodes$y[nodes$x == xi] <- nodes$y[nodes$x == xi] - y_shift |
422 |
} |
|
423 |
# Adjust y locations |
|
424 | 2x |
if (minimize_simple) { |
425 | 2x |
for (xi in x_ranks) { |
426 | 8x |
comps <- nodes$comp[nodes$x == xi] |
427 | 8x |
sources <- colnames(x)[apply(x[comps, , drop = FALSE], 2, sum) > 0] |
428 | 8x |
if (length(sources) > 0) { |
429 | 6x |
mean_y_sources <- mean(nodes$y[nodes$comp %in% sources]) |
430 | 6x |
nodes$y[nodes$x == xi] <- nodes$y[nodes$x == xi] + mean_y_sources |
431 |
} |
|
432 |
} |
|
433 |
} |
|
434 | 2x |
return(nodes) |
435 |
} |
|
436 | ||
437 |
### * topo_x_ordering() |
|
438 | ||
439 |
#' Order compartments from left to right |
|
440 |
#' |
|
441 |
#' @param x A topology matrix or a network model (the topology of the first row |
|
442 |
#' is used in this case). |
|
443 |
#' |
|
444 |
#' @examples |
|
445 |
#' topo_x_ordering <- isotracer:::topo_x_ordering |
|
446 |
#' |
|
447 |
#' sort(topo_x_ordering(trini_mod)) |
|
448 |
#' |
|
449 |
#' @keywords internal |
|
450 |
#' @noRd |
|
451 | ||
452 |
topo_x_ordering <- function(x) { |
|
453 | 2x |
if (is(x, "networkModel")) { |
454 | ! |
x <- unique(topo(x, simplify = FALSE)) |
455 | ! |
stopifnot(length(x) == 1) |
456 | ! |
x <- x[[1]] |
457 |
} |
|
458 | 2x |
x <- unclass(x) # Remove the "topo" class to treat it as a matrix |
459 | 2x |
n_comps <- ncol(x) |
460 | 2x |
max_iters <- 2 * n_comps |
461 | 2x |
links <- which(x > 0) |
462 | 2x |
from <- links %/% n_comps + 1 |
463 | 2x |
to <- links %% n_comps |
464 | 2x |
links <- tibble::tibble(from = from, to = to) |
465 | 2x |
for (i in seq_len(nrow(links))) { |
466 | 34x |
if (links$to[i] == 0) { |
467 | 2x |
links$from[i] <- links$from[i] - 1 |
468 | 2x |
links$to[i] <- n_comps |
469 |
} |
|
470 | 34x |
stopifnot(x[links$to[i], links$from[i]] > 0) |
471 |
} |
|
472 | 2x |
ranks <- rep(0, n_comps) |
473 | 2x |
stable <- FALSE |
474 | 2x |
iter <- 0 |
475 | 2x |
while (!stable & iter <= max_iters) { |
476 | 8x |
prev_ranks <- ranks |
477 | 8x |
for (i in seq_len(nrow(links))) { |
478 | 136x |
if (ranks[links$from[i]] >= ranks[links$to[i]]) { |
479 | 44x |
ranks[links$to[i]] <- ranks[links$to[i]] + 1 |
480 |
} |
|
481 |
} |
|
482 | 8x |
stable <- all(prev_ranks == ranks) |
483 | 8x |
iter <- iter + 1 |
484 |
} |
|
485 | 2x |
if (iter == (max_iters + 1)) { |
486 | ! |
stop("Maximum number of iterations reached.") |
487 |
} |
|
488 | 2x |
out <- setNames(ranks, colnames(x)) |
489 | 2x |
return(out) |
490 |
} |
|
491 | ||
492 |
### * topo_y_ordering() |
|
493 | ||
494 |
#' Order compartment vertically |
|
495 |
#' |
|
496 |
#' This function tries to minimize the number of edges crossings for graphical |
|
497 |
#' display of the topology. |
|
498 |
#' |
|
499 |
#' This function assumes that the x locations given in \code{nodes} have been |
|
500 |
#' established using \code{\link{topo_x_ordering}}. |
|
501 |
#' |
|
502 |
#' @param x A topology matrix or a network model (the topology of the first row |
|
503 |
#' is used in this case). |
|
504 |
#' @param nodes A tibble giving the x locations for each node. It must contain |
|
505 |
#' columns "comp" and "x". |
|
506 |
#' @param n_iters Number of iterations used to try to optimize the compartment |
|
507 |
#' ordering. |
|
508 |
#' |
|
509 |
#' @return An updated tibble similar to the \code{nodes} argument. |
|
510 |
#' |
|
511 |
#' @examples |
|
512 |
#' topo_x_ordering <- isotracer:::topo_x_ordering |
|
513 |
#' topo_y_ordering <- isotracer:::topo_y_ordering |
|
514 |
#' |
|
515 |
#' z <- topo(trini_mod) |
|
516 |
#' x <- topo_x_ordering(z) |
|
517 |
#' comps <- names(x) |
|
518 |
#' d <- tibble::tibble(comp = comps, x = x) |
|
519 |
#' nodes <- topo_y_ordering(z, d) |
|
520 |
#' |
|
521 |
#' @keywords internal |
|
522 |
#' @noRd |
|
523 | ||
524 |
topo_y_ordering <- function(x, nodes, n_iters = 2) { |
|
525 | 2x |
if (is(x, "networkModel")) { |
526 | ! |
x <- unique(topo(x, simplify = FALSE)) |
527 | ! |
stopifnot(length(x) == 1) |
528 | ! |
x <- x[[1]] |
529 |
} |
|
530 | 2x |
x <- unclass(x) # Remove the "topo" class to treat it as a matrix |
531 | 2x |
nodes$y <- seq_len(nrow(nodes)) |
532 | 2x |
nodes <- nodes[order(nodes$x, nodes$y), ] |
533 | 2x |
nodes$id <- seq_len(nrow(nodes)) |
534 | 2x |
comp_id <- setNames(nodes$id, nm = nodes$comp) |
535 | 2x |
counts <- setNames(topo_count_crossings(x, nodes), |
536 | 2x |
paste0(nodes$id, collapse = "-")) |
537 | 2x |
x_ranks <- unique(nodes$x) |
538 | 2x |
stopifnot(length(x_ranks) > 1) |
539 | 2x |
attempts <- 0 |
540 | 2x |
for (iter in seq_len(n_iters)) { |
541 |
# Forward walk |
|
542 | 4x |
for (i in 1:length(x_ranks)) { |
543 | 16x |
focal_comps <- nodes$comp[nodes$x == x_ranks[i]] |
544 | 16x |
focal_y <- nodes$y[nodes$x == x_ranks[i]] |
545 | 16x |
this_rank_counts <- vector() |
546 | 16x |
for (ci in focal_comps) { |
547 | 56x |
swaps <- insert_in_between(ci, focal_comps[focal_comps != ci]) |
548 | 56x |
for (k in seq_along(swaps)) { |
549 | 240x |
new_focal_comps <- swaps[[k]] |
550 | 240x |
nodes$y[match(new_focal_comps, nodes$comp)] <- focal_y |
551 | 240x |
count_name <- paste0(nodes$id[order(nodes$x, nodes$y)], |
552 | 240x |
collapse = "-") |
553 | 240x |
counts[count_name] <- topo_count_crossings(x, nodes) |
554 | 240x |
this_rank_counts[count_name] <- counts[count_name] |
555 |
} |
|
556 |
} |
|
557 | 16x |
this_rank_best <- names(this_rank_counts)[which.min(this_rank_counts)] |
558 | 16x |
this_rank_best_order <- as.numeric(strsplit(this_rank_best, "-")[[1]]) |
559 | 16x |
nodes <- nodes[match(this_rank_best_order, nodes$id), ] |
560 | 16x |
nodes$y <- seq_len(nrow(nodes)) |
561 |
} |
|
562 |
# Backward walk |
|
563 | 4x |
for (i in length(x_ranks):1) { |
564 | 16x |
focal_comps <- nodes$comp[nodes$x == x_ranks[i]] |
565 | 16x |
focal_y <- nodes$y[nodes$x == x_ranks[i]] |
566 | 16x |
this_rank_counts <- vector() |
567 | 16x |
for (ci in focal_comps) { |
568 | 56x |
swaps <- insert_in_between(ci, focal_comps[focal_comps != ci]) |
569 | 56x |
for (k in seq_along(swaps)) { |
570 | 240x |
new_focal_comps <- swaps[[k]] |
571 | 240x |
nodes$y[match(new_focal_comps, nodes$comp)] <- focal_y |
572 | 240x |
count_name <- paste0(nodes$id[order(nodes$x, nodes$y)], |
573 | 240x |
collapse = "-") |
574 | 240x |
counts[count_name] <- topo_count_crossings(x, nodes) |
575 | 240x |
this_rank_counts[count_name] <- counts[count_name] |
576 |
} |
|
577 |
} |
|
578 | 16x |
this_rank_best <- names(this_rank_counts)[which.min(this_rank_counts)] |
579 | 16x |
this_rank_best_order <- as.numeric(strsplit(this_rank_best, "-")[[1]]) |
580 | 16x |
nodes <- nodes[match(this_rank_best_order, nodes$id), ] |
581 | 16x |
nodes$y <- seq_len(nrow(nodes)) |
582 |
} |
|
583 |
} |
|
584 |
# Return |
|
585 | 2x |
return(nodes[, c("comp", "x", "y")]) |
586 |
} |
|
587 | ||
588 |
### * insert_in_between() |
|
589 | ||
590 |
#' Insert an element in all possible positions of another vector |
|
591 |
#' |
|
592 |
#' @param x Element to insert |
|
593 |
#' @param into Target vector |
|
594 |
#' |
|
595 |
#' @return A list of vectors with the x element inserted in all posssible |
|
596 |
#' positions of the target. |
|
597 |
#' |
|
598 |
#' @examples |
|
599 |
#' isotracer:::insert_in_between("a", 1:5) |
|
600 |
#' |
|
601 |
#' @keywords internal |
|
602 |
#' @noRd |
|
603 | ||
604 |
insert_in_between <- function(x, into) { |
|
605 | 112x |
l <- length(into) |
606 | 112x |
out <- list() |
607 | 112x |
for (i in 0:l) { |
608 | 480x |
z <- c(into[0:i], x) |
609 | 480x |
if (i+1 <= l) { |
610 | 368x |
z <- c(z, into[(i+1):l]) |
611 |
} |
|
612 | 480x |
out[[i+1]] <- z |
613 |
} |
|
614 | 112x |
return(out) |
615 |
} |
|
616 | ||
617 |
### * topo_count_crossings() |
|
618 | ||
619 |
#' Count edges crossings given a topology and a proposed node arrangement |
|
620 |
#' |
|
621 |
#' This function assumes that the x locations given in \code{nodes} have been |
|
622 |
#' established using \code{\link{topo_x_ordering}}. |
|
623 |
#' |
|
624 |
#' @param x A topology matrix or a network model (the topology of the first row |
|
625 |
#' is used in this case). |
|
626 |
#' @param nodes A tibble giving the x and y locations for each node. It must |
|
627 |
#' contain columns "comp", "x" and "y". |
|
628 |
#' |
|
629 |
#' @return Integer, the number of crossing edges when drawing the nodes |
|
630 |
#' and the connecting edges. |
|
631 |
#' |
|
632 |
#' @examples |
|
633 |
#' topo_x_ordering <- isotracer:::topo_x_ordering |
|
634 |
#' topo_count_crossings <- isotracer:::topo_count_crossings |
|
635 |
#' |
|
636 |
#' z <- topo(trini_mod) |
|
637 |
#' x <- topo_x_ordering(z) |
|
638 |
#' comps <- names(x) |
|
639 |
#' y <- 1:length(comps) |
|
640 |
#' d <- tibble::tibble(comp = comps, x = x, y = y) |
|
641 |
#' topo_count_crossings(z, d) |
|
642 |
#' |
|
643 |
#' @keywords internal |
|
644 |
#' @noRd |
|
645 | ||
646 |
topo_count_crossings <- function(x, nodes) { |
|
647 | 482x |
if (is(x, "networkModel")) { |
648 | ! |
x <- unique(topo(x, simplify = FALSE)) |
649 | ! |
stopifnot(length(x) == 1) |
650 | ! |
x <- x[[1]] |
651 |
} |
|
652 | 482x |
x <- unclass(x) # Remove the "topo" class to treat it as a matrix |
653 | 482x |
crossings <- 0 |
654 | 482x |
nodes <- nodes[order(nodes$x, nodes$y), ] |
655 | 482x |
x_ranks <- unique(nodes$x) |
656 | 482x |
stopifnot(length(x_ranks) > 1) |
657 | 482x |
for (i in 2:length(x_ranks)) { |
658 |
# Get the edges for gap i |
|
659 | 1446x |
n <- nodes[nodes$x %in% x_ranks[c(i-1, i)], ] |
660 | 1446x |
e <- x[n$comp[n$x == x_ranks[i]], n$comp[n$x == x_ranks[i-1]], drop = FALSE] |
661 | 1446x |
if (ncol(e)>1) { |
662 | 1446x |
for (j in 2:ncol(e)) { |
663 | 4338x |
k <- which(e[, j] > 0) |
664 | 4338x |
for (w in 1:(j-1)) { |
665 | 10604x |
for (ki in k) { |
666 | 11950x |
if (ki < nrow(e)) { |
667 | 7946x |
crossings <- crossings + sum(e[, w][(ki+1):nrow(e)]) |
668 |
} |
|
669 |
} |
|
670 |
} |
|
671 |
} |
|
672 |
} |
|
673 |
} |
|
674 | 482x |
return(crossings) |
675 |
} |
|
676 | ||
677 |
### * sankey_minimize_energy() |
|
678 | ||
679 |
#' Adjust nodes y positions using a spring/energy minimization model |
|
680 |
#' |
|
681 |
#' WIP: This function does not seem to working properly at the moment. |
|
682 |
#' |
|
683 |
#' @param x A topology. |
|
684 |
#' @param nodes The output of \code{\link{sankey_calc_nodes_locations}} run on |
|
685 |
#' \code{x}. |
|
686 |
#' @param k1 Numeric, stiffness for springs between nodes on the same x position. |
|
687 |
#' @param k2 Numeric, sitffness for springs between connected nodes. |
|
688 |
#' @param l1 Numeric, rest length for springs with k1 stiffness. |
|
689 |
#' @param l2 Numeric, rest length for springs with k2 stiffness. |
|
690 |
#' |
|
691 |
#' @return An updated \code{nodes} tibble. |
|
692 |
#' |
|
693 |
#' @importFrom stats optim |
|
694 |
#' |
|
695 |
#' @examples |
|
696 |
#' sankey_calc_nodes_locations <- isotracer:::sankey_calc_nodes_locations |
|
697 |
#' sankey_minimize_energy <- isotracer:::sankey_minimize_energy |
|
698 |
#' |
|
699 |
#' nodes <- sankey_calc_nodes_locations(trini_mod, layout = "left2right") |
|
700 |
#' sankey_minimize_energy(topo(trini_mod), nodes) |
|
701 |
#' |
|
702 |
#' @keywords internal |
|
703 |
#' @noRd |
|
704 | ||
705 |
sankey_minimize_energy <- function(x, nodes, k1 = 100, k2 = 10, l1 = 1, l2 = 1) { |
|
706 | ! |
n_comps <- ncol(x) |
707 | ! |
links <- which(unclass(x) > 0) |
708 | ! |
from <- links %/% n_comps + 1 |
709 | ! |
to <- links %% n_comps |
710 | ! |
links <- tibble::tibble(from = from, to = to) |
711 | ! |
for (i in seq_len(nrow(links))) { |
712 | ! |
if (links$to[i] == 0) { |
713 | ! |
links$from[i] <- links$from[i] - 1 |
714 | ! |
links$to[i] <- n_comps |
715 |
} |
|
716 | ! |
stopifnot(x[links$to[i], links$from[i]] > 0) |
717 |
} |
|
718 | ! |
links$from <- colnames(x)[links$from] |
719 | ! |
links$to <- colnames(x)[links$to] |
720 |
# The y location of the first node is fixed. |
|
721 | ! |
stopifnot(nrow(nodes) > 1) |
722 | ! |
nodes <- nodes[order(nodes$x, nodes$y), ] |
723 | ! |
links$from <- match(links$from, nodes$comp) |
724 | ! |
links$to <- match(links$to, nodes$comp) |
725 | ! |
energy <- function(...) { |
726 | ! |
nodes$y <- c(nodes$y[1], unlist(list(...))) |
727 | ! |
energy <- 0 |
728 |
# Energy due to springs between nodes on same x_rank |
|
729 | ! |
x_ranks <- unique(nodes$x) |
730 | ! |
for (xi in x_ranks) { |
731 | ! |
ni <- nodes[nodes$x == xi, ] |
732 | ! |
if (nrow(ni) > 1) { |
733 | ! |
for (i in 2:nrow(ni)) { |
734 | ! |
delta_l <- ni$y[i] - ni$y[i-1] - l1 |
735 | ! |
energy <- energy + 1/2 * k1 * delta_l^2 |
736 |
} |
|
737 |
} |
|
738 |
} |
|
739 |
# Energy due to springs along connections between nodes of different x_rank |
|
740 | ! |
for (i in 1:nrow(links)) { |
741 | ! |
delta_l <- abs(nodes$y[links$to[i]] - nodes$y[links$from[i]]) - l2 |
742 | ! |
energy <- energy + 1/2 * k2 * delta_l^2 |
743 |
} |
|
744 | ! |
return(energy) |
745 |
} |
|
746 | ! |
start_values <- nodes$y[2:nrow(nodes)] |
747 | ! |
y_locs <- optim(start_values, energy, method = "BFGS")$par |
748 | ! |
nodes$y <- c(nodes$y[1], y_locs) |
749 | ! |
return(nodes) |
750 |
} |
|
751 | ||
752 |
### * sankey_minimize_energy_sim() |
|
753 | ||
754 |
#' Adjust nodes y positions using a spring/energy minimization model |
|
755 |
#' |
|
756 |
#' @param x A topology. |
|
757 |
#' @param nodes The output of \code{\link{sankey_calc_nodes_locations}} run on |
|
758 |
#' \code{x}. |
|
759 |
#' @param k1 Numeric, stiffness for springs between nodes on the same x |
|
760 |
#' position. |
|
761 |
#' @param k2 Numeric, sitffness for springs between connected nodes. |
|
762 |
#' @param l1 Numeric, rest length for springs with k1 stiffness. |
|
763 |
#' @param l2 Numeric, rest length for springs with k2 stiffness. |
|
764 |
#' @param cd Numeric, dampening constant. |
|
765 |
#' @param debug Boolean, if TRUE return trajectories instead of the node |
|
766 |
#' tibble. |
|
767 |
#' @param return_acc Boolean, if TRUE also return velocity and acceleration |
|
768 |
#' values in the node tibble (useful for debugging). |
|
769 |
#' @param n_iters Number of iterations in the simulation loop. |
|
770 |
#' @param dt Time step used in the simulation loop. |
|
771 |
#' |
|
772 |
#' @return An updated \code{nodes} tibble. |
|
773 |
#' |
|
774 |
#' @examples |
|
775 |
#' sankey_calc_nodes_locations <- isotracer:::sankey_calc_nodes_locations |
|
776 |
#' sankey_minimize_energy_sim <- isotracer:::sankey_minimize_energy_sim |
|
777 |
#' |
|
778 |
#' nodes <- sankey_calc_nodes_locations(trini_mod, layout = "left2right") |
|
779 |
#' nodes <- nodes[nodes$x < 2, ] |
|
780 |
#' z <- sankey_minimize_energy_sim(topo(trini_mod)[nodes$comp, nodes$comp], nodes, |
|
781 |
#' n_iters = 200, cd = 0.8, dt = 0.1, debug = TRUE) |
|
782 |
#' z <- do.call(rbind, z) |
|
783 |
#' lattice::xyplot(ts(z)) |
|
784 |
#' |
|
785 |
#' (z <- sankey_minimize_energy_sim(topo(trini_mod)[nodes$comp, nodes$comp], nodes, |
|
786 |
#' n_iters = 200, cd = 0.8, dt = 0.1, return_acc = TRUE)) |
|
787 |
#' plot(z$x, z$y) |
|
788 |
#' text(z$x, z$y, z$comp) |
|
789 |
#' |
|
790 |
#' @keywords internal |
|
791 |
#' @noRd |
|
792 | ||
793 |
sankey_minimize_energy_sim <- function(x, nodes, k1 = 1, k2 = 1, l1 = 1, l2 = 1, |
|
794 |
cd = 0.8, n_iters = 25, dt = 0.5, debug = FALSE, |
|
795 |
return_acc = FALSE) { |
|
796 | ! |
n_comps <- ncol(x) |
797 | ! |
links <- which(x > 0) |
798 | ! |
from <- links %/% n_comps + 1 |
799 | ! |
to <- links %% n_comps |
800 | ! |
links <- tibble::tibble(from = from, to = to) |
801 | ! |
for (i in seq_len(nrow(links))) { |
802 | ! |
if (links$to[i] == 0) { |
803 | ! |
links$from[i] <- links$from[i] - 1 |
804 | ! |
links$to[i] <- n_comps |
805 |
} |
|
806 | ! |
stopifnot(x[links$to[i], links$from[i]] > 0) |
807 |
} |
|
808 | ! |
links$from <- colnames(x)[links$from] |
809 | ! |
links$to <- colnames(x)[links$to] |
810 | ! |
stopifnot(nrow(nodes) > 1) |
811 | ! |
nodes <- nodes[order(nodes$x, nodes$y), ] |
812 | ! |
nodes$v <- 0 |
813 | ! |
links$from <- match(links$from, nodes$comp) |
814 | ! |
links$to <- match(links$to, nodes$comp) |
815 | ! |
traj <- list() |
816 | ! |
x_ranks <- unique(nodes$x) |
817 |
# Dynamic simulation |
|
818 | ! |
for (i in seq_len(n_iters)) { |
819 | ! |
nodes$a <- 0 |
820 | ! |
traj[[i]] <- nodes$y |
821 |
# Add forces due to springs between nodes on same x_rank |
|
822 | ! |
for (xi in x_ranks) { |
823 | ! |
ni <- nodes[nodes$x == xi, ] |
824 | ! |
if (nrow(ni) > 1) { |
825 | ! |
for (i in 2:nrow(ni)) { |
826 | ! |
delta_l <- ni$y[i] - ni$y[i-1] - l1 |
827 | ! |
force <- k1 * delta_l |
828 | ! |
ni$a[i] <- ni$a[i] - force |
829 | ! |
ni$a[i-1] <- ni$a[i-1] + force |
830 |
} |
|
831 |
} |
|
832 | ! |
nodes[nodes$x == xi, ] <- ni |
833 |
} |
|
834 |
# Add forces due to springs along connections between nodes of different x_rank |
|
835 | ! |
for (i in 1:nrow(links)) { |
836 | ! |
delta_l <- abs(nodes$y[links$to[i]] - nodes$y[links$from[i]]) - l2 |
837 | ! |
force <- k2 * delta_l |
838 | ! |
if (nodes$y[links$to[i]] > nodes$y[links$from[i]]) { |
839 | ! |
nodes$a[links$to[i]] <- nodes$a[links$to[i]] - force |
840 | ! |
nodes$a[links$from[i]] <- nodes$a[links$from[i]] + force |
841 |
} else { |
|
842 | ! |
nodes$a[links$to[i]] <- nodes$a[links$to[i]] + force |
843 | ! |
nodes$a[links$from[i]] <- nodes$a[links$from[i]] - force |
844 |
} |
|
845 |
} |
|
846 |
# Dampening |
|
847 | ! |
for (i in seq_len(nrow(nodes))) { |
848 | ! |
nodes$a[i] <- nodes$a[i] - cd * nodes$v[i] |
849 |
} |
|
850 |
# Integration |
|
851 | ! |
for (i in seq_len(nrow(nodes))) { |
852 | ! |
nodes$y[i] <- nodes$y[i] + dt * nodes$v[i] |
853 | ! |
nodes$v[i] <- nodes$v[i] + dt * nodes$a[i] |
854 |
} |
|
855 |
} |
|
856 | ! |
if (debug) { |
857 | ! |
return(traj) |
858 |
} |
|
859 | ! |
if (return_acc) { |
860 | ! |
return(nodes) |
861 |
} |
|
862 | ! |
return(nodes[, c("comp", "x", "y")]) |
863 |
} |
|
864 | ||
865 |
### * sankey_draw_node() |
|
866 | ||
867 |
#' Draw a rounded rectangle with a label |
|
868 |
#' |
|
869 |
#' @param x,y Locations of the center of the rectangle. |
|
870 |
#' @param label String, label added inside the rectangle. |
|
871 |
#' @param fill Fill color. |
|
872 |
#' @param col Border color. |
|
873 |
#' @param text_col Text color. |
|
874 |
#' @param style One of "roundrect", "square". |
|
875 |
#' @param padding Padding value. |
|
876 |
#' @param padding_factor Adjustment factor for padding. |
|
877 |
#' @param r_factor Adjustment factor for rounded corner. |
|
878 |
#' @param side Vector of length 1 or 2 giving node width and height values |
|
879 |
#' (used for style = "square"). |
|
880 |
#' @param factor Adjustment factor for node dimensions (used for style = |
|
881 |
#' "square"). |
|
882 |
#' |
|
883 |
#' @keywords internal |
|
884 |
#' @noRd |
|
885 | ||
886 |
sankey_draw_node <- function(x, y, label, |
|
887 |
fill = NULL, col = "black", text_col = NULL, |
|
888 |
style = "roundrect", |
|
889 |
padding = NULL, padding_factor = 1, r_factor = 1, # For roundrect |
|
890 |
side = NULL, factor = 1) { |
|
891 |
# Convert x and y coordinates |
|
892 | ! |
x <- grid::unit(x, "native") |
893 | ! |
y <- grid::unit(y, "native") |
894 |
# Draw box |
|
895 | ! |
if (is.null(fill)) { |
896 | ! |
gp <- grid::gpar(col = col) |
897 |
} else { |
|
898 | ! |
gp <- grid::gpar(col = col, fill = fill) |
899 |
} |
|
900 | ! |
if (style == "roundrect") { |
901 | ! |
r <- grid::unit(0.1, "snpc") * r_factor |
902 | ! |
if (is.null(padding)) { |
903 | ! |
padding <- grid::unit(1, "strwidth", "o") |
904 |
} |
|
905 | ! |
width <- grid::unit(1, "strwidth", label) + 2 * padding * padding_factor |
906 | ! |
height <- grid::unit(1, "strheight", label) + 2 * padding * padding_factor |
907 | ! |
grid::grid.roundrect(x = x, y = y, width = width, height = height, |
908 | ! |
gp = gp, r = r) |
909 | ! |
} else if (style == "square") { |
910 | ! |
if (is.null(side)) { |
911 | ! |
side <- grid::unit(1, "strwidth", "toto") |
912 |
} |
|
913 | ! |
if (length(side) == 1) { |
914 | ! |
side <- grid::unit.c(side, side) |
915 |
} |
|
916 | ! |
grid::grid.rect(x = x, y = y, width = side[1] * factor, |
917 | ! |
height = side[2] * factor, gp = gp) |
918 |
} |
|
919 |
# Draw label |
|
920 | ! |
if (is.null(text_col)) { |
921 | ! |
text_col <- col |
922 |
} |
|
923 | ! |
gp <- grid::gpar(col = text_col) |
924 | ! |
grid::grid.text(label = label, x = x, y = y, gp = gp) |
925 |
} |
|
926 | ||
927 |
### * sankey_draw_edge() |
|
928 | ||
929 |
#' Draw a constant-width ribbon between nodes |
|
930 |
#' |
|
931 |
#' @param backbone Two-column table containing the x and y coordinates of the |
|
932 |
#' points making the mid-line of the edge ribbon. |
|
933 |
#' @param width Width of the edge ribbon. |
|
934 |
#' @param fill Color for ribbon fill. |
|
935 |
#' @param col Color for ribbon border. |
|
936 |
#' @param factor Adjustment factor for ribbon width. |
|
937 |
#' |
|
938 |
#' @keywords internal |
|
939 |
#' @noRd |
|
940 | ||
941 |
sankey_draw_edge <- function(backbone, width = 0.1, fill = NULL, col = "black", |
|
942 |
factor = 1) { |
|
943 | ! |
rb <- ribbonFromTrajectory(backbone, width * factor) |
944 | ! |
if (is.null(fill)) { |
945 | ! |
gp <- grid::gpar(col = col) |
946 |
} else { |
|
947 | ! |
gp <- grid::gpar(col = col, fill = fill) |
948 |
} |
|
949 | ! |
grid::grid.polygon(x = rb[, 1], y = rb[, 2], default.units = "native", gp = gp) |
950 |
} |
|
951 | ||
952 |
### * make_and_push_ortho_vp() |
|
953 | ||
954 |
#' Make an orthonormal data viewport |
|
955 |
#' |
|
956 |
#' This function must be run when a parent viewport already exists. It adds a |
|
957 |
#' container viewport, centered, with specified width and height, and fills |
|
958 |
#' this container viewport with an orthonormal data viewport (based on the |
|
959 |
#' physical dimensions of the container viewport). |
|
960 |
#' |
|
961 |
#' Note that this function creates and push two viewports on the stack (a |
|
962 |
#' container filling the parent viewport completely and a data viewport filling |
|
963 |
#' the container, with an orthonormal cartesian coordinate system), so that two |
|
964 |
#' viewports must be popped in order to come back to the parent viewport in use |
|
965 |
#' before the function call. |
|
966 |
#' |
|
967 |
#' @param width,height Numeric between 0 and 1, fraction of the parent viewport |
|
968 |
#' width and height filled by the new viewport. |
|
969 |
#' @param debug Boolean. |
|
970 |
#' |
|
971 |
#' @return An orthonormal data viewport which bounds (0, 1) on one axis. |
|
972 |
#' |
|
973 |
#' @examples |
|
974 |
#' library(grid) |
|
975 |
#' grid.newpage() |
|
976 |
#' pushViewport(viewport()) |
|
977 |
#' ortho_vp <- isotracer:::make_and_push_ortho_vp(width = 0.9, height = 0.9, |
|
978 |
#' debug = TRUE) |
|
979 |
#' |
|
980 |
#' @keywords internal |
|
981 |
#' @noRd |
|
982 | ||
983 |
make_and_push_ortho_vp <- function(width = 1, height = 1, debug = FALSE) { |
|
984 | 8x |
debug_bg_col <- "#095ba7" |
985 | 8x |
debug_line_col <- "#9fccfa" # "#baecfa" |
986 | 8x |
parent_vp <- grid::current.viewport() |
987 | 8x |
if (debug) { |
988 | 5x |
grid::grid.rect(gp = grid::gpar(col = debug_line_col, |
989 | 5x |
fill = debug_bg_col)) |
990 |
} |
|
991 | 8x |
container_vp <- grid::viewport(width = width, height = height, |
992 | 8x |
name = "ortho_container") |
993 | 8x |
grid::pushViewport(container_vp) |
994 | 8x |
if (debug) { |
995 | 5x |
grid::grid.rect(gp = grid::gpar(col = debug_line_col, |
996 | 5x |
fill = debug_bg_col)) |
997 |
} |
|
998 | 8x |
container_dims <- grid::deviceDim(container_vp$width, |
999 | 8x |
container_vp$height, |
1000 | 8x |
valueOnly = TRUE) |
1001 | 8x |
canvas_dims <- container_dims |
1002 | 8x |
if (canvas_dims$w >= canvas_dims$h) { |
1003 | 8x |
y_scale <- c(0, 1) |
1004 | 8x |
x_scale <- y_scale * canvas_dims$w / canvas_dims$h |
1005 | 8x |
x_scale <- x_scale - (x_scale[2] - 1) / 2 |
1006 |
} else { |
|
1007 | ! |
x_scale <- c(0, 1) |
1008 | ! |
y_scale <- x_scale * canvas_dims$h / canvas_dims$w |
1009 | ! |
y_scale <- y_scale - (y_scale[2] - 1) / 2 |
1010 |
} |
|
1011 | 8x |
canvas_vp <- grid::dataViewport(xscale = x_scale, yscale = y_scale, |
1012 | 8x |
name = "ortho_canvas") |
1013 | 8x |
grid::pushViewport(canvas_vp) |
1014 | 8x |
if (debug) { |
1015 | 5x |
shift <- 0.05 |
1016 | 5x |
arrow_style <- grid::arrow(length = grid::unit(0.01, "native")) |
1017 | 5x |
grid::grid.rect(width = 1, height = 1, |
1018 | 5x |
default.units = "native", |
1019 | 5x |
gp = grid::gpar(col = debug_line_col, lty = 2)) |
1020 | 5x |
grid::grid.lines(x = c(0, 0.1) + shift, y = shift, default.units = "native", |
1021 | 5x |
gp = grid::gpar(col = debug_line_col), |
1022 | 5x |
arrow = arrow_style) |
1023 | 5x |
grid::grid.lines(x = shift, y = c(0, 0.1) + shift, default.units = "native", |
1024 | 5x |
gp = grid::gpar(col = debug_line_col), |
1025 | 5x |
arrow = arrow_style) |
1026 | 5x |
grid::grid.points(x = 0.5, y = 0.5, default.units = "native", |
1027 | 5x |
pch = 3, gp = grid::gpar(col = debug_line_col)) |
1028 | 5x |
grid::grid.text("0.1 x", x = grid::unit(0.05 + shift, "native"), |
1029 | 5x |
y = grid::unit(shift, "native") + grid::unit(0.7, "strheight", "x"), |
1030 | 5x |
gp = grid::gpar(cex = 1, col = debug_line_col)) |
1031 | 5x |
grid::grid.text("0.1 y", x = grid::unit(shift, "native") + grid::unit(0.7, "strheight", "x"), |
1032 | 5x |
y = grid::unit(0.05 + shift, "native"), |
1033 | 5x |
gp = grid::gpar(cex = 1, col = debug_line_col), |
1034 | 5x |
rot = -90) |
1035 | 5x |
grid::grid.text("(0, 0)", |
1036 | 5x |
x = grid::unit(0, "native") + grid::unit(0.1, "strwidth", "x"), |
1037 | 5x |
y = grid::unit(0, "native") + grid::unit(0.4, "strheight", "x"), |
1038 | 5x |
hjust = 0, vjust = 0, |
1039 | 5x |
gp = grid::gpar(cex = 1, col = debug_line_col)) |
1040 | 5x |
grid::grid.text("(1, 1)", |
1041 | 5x |
x = grid::unit(1, "native") - grid::unit(1, "strwidth", "(1, 1)"), |
1042 | 5x |
y = grid::unit(1, "native") - grid::unit(0.4, "strheight", "x"), |
1043 | 5x |
hjust = 0, vjust = 1, |
1044 | 5x |
gp = grid::gpar(cex = 1, col = debug_line_col)) |
1045 | 5x |
topleft <- signif(c(canvas_vp$xscale[1], canvas_vp$yscale[2]), 3) |
1046 | 5x |
topleft_char <- paste0("(", topleft[1], ", ", topleft[2], ")") |
1047 | 5x |
grid::grid.text(topleft_char, |
1048 | 5x |
x = grid::unit(topleft[1], "native") + grid::unit(0.1, "strwidth", "x"), |
1049 | 5x |
y = grid::unit(topleft[2], "native") - grid::unit(0.4, "strheight", "x"), |
1050 | 5x |
hjust = 0, vjust = 1, |
1051 | 5x |
gp = grid::gpar(cex = 1, col = debug_line_col)) |
1052 | 5x |
bottomright <- signif(c(canvas_vp$xscale[2], canvas_vp$yscale[1]), 3) |
1053 | 5x |
bottomright_char <- paste0("(", bottomright[1], ", ", bottomright[2], ")") |
1054 | 5x |
grid::grid.text(bottomright_char, |
1055 | 5x |
x = grid::unit(bottomright[1], "native") - grid::unit(1, "strwidth", bottomright_char), |
1056 | 5x |
y = grid::unit(bottomright[2], "native") + grid::unit(0.4, "strheight", "x"), |
1057 | 5x |
hjust = 0, vjust = 0, |
1058 | 5x |
gp = grid::gpar(cex = 1, col = debug_line_col)) |
1059 | 5x |
grid::grid.text("(0.5, 0.5)", |
1060 | 5x |
x = grid::unit(0.5, "native") + grid::unit(0.1, "strwidth", "x"), |
1061 | 5x |
y = grid::unit(0.5, "native") + grid::unit(0.4, "strheight", "x"), |
1062 | 5x |
hjust = 0, vjust = 0, |
1063 | 5x |
gp = grid::gpar(cex = 1, col = debug_line_col)) |
1064 |
} |
|
1065 | 8x |
return(canvas_vp) |
1066 |
} |
|
1067 | ||
1068 |
### * sankey_draw_edges() |
|
1069 | ||
1070 |
#' @keywords internal |
|
1071 |
#' @noRd |
|
1072 | ||
1073 |
sankey_draw_edges <- function(edges, defaults = list(col = adjustcolor("black", alpha.f = 0.5), |
|
1074 |
fill = adjustcolor("grey", alpha.f = 0.5), |
|
1075 |
lwd = 1, lty = 1), |
|
1076 |
debug = FALSE) { |
|
1077 | 8x |
debug_bg_col <- "#095ba7" |
1078 | 8x |
debug_line_col <- "#9fccfa" # "#baecfa" |
1079 | 8x |
if (is.null(edges) || nrow(edges) == 0) { |
1080 | ! |
return(NULL) |
1081 |
} |
|
1082 | 8x |
for (property in names(defaults)) { |
1083 | 32x |
if (! property %in% colnames(edges)) { |
1084 | 32x |
edges[[property]] <- defaults[[property]] |
1085 |
} |
|
1086 |
} |
|
1087 | 8x |
for (i in seq_len(nrow(edges))) { |
1088 | 67x |
rb <- ribbonFromTrajectory(edges[["backbone"]][[i]], |
1089 | 67x |
width = edges[["width"]][i]) |
1090 | 67x |
gp <- grid::gpar(col = edges[["col"]][i], |
1091 | 67x |
fill = edges[["fill"]][i], |
1092 | 67x |
lwd = edges[["lwd"]][i], |
1093 | 67x |
lty = edges[["lty"]][i]) |
1094 | 67x |
grid::grid.polygon(x = rb[, 1], y = rb[, 2], default.units = "native", |
1095 | 67x |
gp = gp) |
1096 | 67x |
if (debug) { |
1097 | 34x |
grid::grid.lines(x = edges[["control_points"]][[i]][, 1], |
1098 | 34x |
y = edges[["control_points"]][[i]][, 2], |
1099 | 34x |
default.units = "native", |
1100 | 34x |
gp = grid::gpar(col = debug_line_col, |
1101 | 34x |
lty = 2)) |
1102 | 34x |
grid::grid.lines(x = edges[["backbone"]][[i]][, 1], |
1103 | 34x |
y = edges[["backbone"]][[i]][, 2], |
1104 | 34x |
default.units = "native", |
1105 | 34x |
gp = grid::gpar(col = debug_line_col)) |
1106 | 34x |
grid::grid.points(x = edges[["control_points"]][[i]][, 1], |
1107 | 34x |
y = edges[["control_points"]][[i]][, 2], |
1108 | 34x |
default.units = "native", |
1109 | 34x |
pch = 1, |
1110 | 34x |
gp = grid::gpar(col = debug_line_col, |
1111 | 34x |
cex = 0.75)) |
1112 | 34x |
grid::grid.points(x = edges[["backbone"]][[i]][, 1], |
1113 | 34x |
y = edges[["backbone"]][[i]][, 2], |
1114 | 34x |
default.units = "native", |
1115 | 34x |
pch = 21, |
1116 | 34x |
gp = grid::gpar(col = debug_line_col, |
1117 | 34x |
fill = debug_line_col, |
1118 | 34x |
cex = 0.5)) |
1119 |
} |
|
1120 |
} |
|
1121 | 8x |
return(NULL) |
1122 |
} |
|
1123 | ||
1124 |
### * sankey_draw_nodes() |
|
1125 | ||
1126 |
#' @keywords internal |
|
1127 |
#' @noRd |
|
1128 | ||
1129 |
sankey_draw_nodes <- function(nodes, defaults = list(label = "", |
|
1130 |
col = adjustcolor("black", alpha.f = 0.5), |
|
1131 |
fill = adjustcolor("grey", alpha.f = 0.5), |
|
1132 |
lwd = 1, lty = 1), |
|
1133 |
debug = FALSE, node_s = "default") { |
|
1134 | 8x |
debug_bg_col <- "#095ba7" |
1135 | 8x |
debug_line_col <- "#9fccfa" # "#baecfa" |
1136 | 8x |
if (is.null(nodes) || nrow(nodes) == 0) { |
1137 | ! |
return(NULL) |
1138 |
} |
|
1139 | 8x |
for (property in names(defaults)) { |
1140 | 40x |
if (! property %in% colnames(nodes)) { |
1141 | 32x |
nodes[[property]] <- defaults[[property]] |
1142 |
} |
|
1143 |
} |
|
1144 | 8x |
for (i in seq_len(nrow(nodes))) { |
1145 | 55x |
gp <- grid::gpar(col = nodes$col[i], |
1146 | 55x |
fill = nodes$fill[i], |
1147 | 55x |
lwd = nodes$lwd[i], |
1148 | 55x |
lty = nodes$lty[i]) |
1149 | 55x |
if (node_s == "roundsquare") { |
1150 | ! |
grid::grid.roundrect(x = nodes$x[i], y = nodes$y[i], |
1151 | ! |
width = nodes$width[[i]], |
1152 | ! |
height = nodes$height[[i]], |
1153 | ! |
default.units = "native", |
1154 | ! |
gp = gp) |
1155 |
} else { |
|
1156 | 55x |
grid::grid.rect(x = nodes$x[i], y = nodes$y[i], |
1157 | 55x |
width = nodes$width[[i]], |
1158 | 55x |
height = nodes$height[[i]], |
1159 | 55x |
default.units = "native", |
1160 | 55x |
gp = gp) |
1161 |
} |
|
1162 | 55x |
if (debug) { |
1163 | 29x |
gp <- grid::gpar(col = debug_line_col, |
1164 | 29x |
lty = 5) |
1165 | 29x |
if (node_s == "roundrect") { |
1166 | ! |
grid::grid.roundrect(x = nodes$x[i], y = nodes$y[i], |
1167 | ! |
width = nodes$width[[i]], |
1168 | ! |
height = nodes$height[[i]], |
1169 | ! |
default.units = "native", |
1170 | ! |
gp = gp) |
1171 |
} else { |
|
1172 | 29x |
grid::grid.rect(x = nodes$x[i], y = nodes$y[i], |
1173 | 29x |
width = nodes$width[[i]], |
1174 | 29x |
height = nodes$height[[i]], |
1175 | 29x |
default.units = "native", |
1176 | 29x |
gp = gp) |
1177 |
} |
|
1178 | 29x |
left <- nodes$x[i] - grid::convertWidth(nodes$width[[i]], unitTo = "native", |
1179 | 29x |
valueOnly = TRUE) / 2 |
1180 | 29x |
right <- nodes$x[i] + grid::convertWidth(nodes$width[[i]], unitTo = "native", |
1181 | 29x |
valueOnly = TRUE) / 2 |
1182 | 29x |
bottom <- nodes$y[i] - grid::convertHeight(nodes$height[[i]], unitTo = "native", |
1183 | 29x |
valueOnly = TRUE) / 2 |
1184 | 29x |
top <- nodes$y[i] + grid::convertHeight(nodes$height[[i]], unitTo = "native", |
1185 | 29x |
valueOnly = TRUE) / 2 |
1186 | 29x |
gp <- grid::gpar(col = debug_line_col, |
1187 | 29x |
lty = "26") |
1188 | 29x |
grid::grid.lines(x = c(left, right), y = c(top, bottom), |
1189 | 29x |
default.units = "native", gp = gp) |
1190 | 29x |
grid::grid.lines(x = c(left, right), y = c(bottom, top), |
1191 | 29x |
default.units = "native", gp = gp) |
1192 | 29x |
gp <- grid::gpar(col = debug_line_col, |
1193 | 29x |
fill = debug_bg_col) |
1194 | 29x |
grid::grid.points(x = nodes[["x"]][i], y = nodes[["y"]][i], |
1195 | 29x |
pch = 5, size = grid::unit(0.8, "char"), |
1196 | 29x |
default.units = "native", gp = gp) |
1197 | 29x |
grid::grid.text(x = nodes[["x"]][i], |
1198 | 29x |
y = grid::unit(nodes[["y"]][i], "native") + |
1199 | 29x |
grid::unit(1.4, "strheight", "x"), |
1200 | 29x |
default.units = "native", gp = gp, |
1201 | 29x |
label = nodes[["comp"]][i]) |
1202 |
} |
|
1203 |
} |
|
1204 | 8x |
return(NULL) |
1205 |
} |
|
1206 | ||
1207 |
### * sankey_draw_labels() |
|
1208 | ||
1209 |
#' @keywords internal |
|
1210 |
#' @noRd |
|
1211 | ||
1212 |
sankey_draw_labels <- function(labels, defaults = list(label = "", |
|
1213 |
cex = 1, |
|
1214 |
col = adjustcolor("black", alpha.f = 0.5), |
|
1215 |
fill = adjustcolor("grey", alpha.f = 0.5), |
|
1216 |
lwd = 1, lty = 1), |
|
1217 |
debug = FALSE) { |
|
1218 | 8x |
debug_bg_col <- "#095ba7" |
1219 | 8x |
debug_line_col <- "#9fccfa" # "#baecfa" |
1220 | 8x |
if (is.null(labels) || nrow(labels) == 0) { |
1221 | ! |
return(NULL) |
1222 |
} |
|
1223 | 8x |
for (property in names(defaults)) { |
1224 | 48x |
if (! property %in% colnames(labels)) { |
1225 | 32x |
labels[[property]] <- defaults[[property]] |
1226 |
} |
|
1227 |
} |
|
1228 | 8x |
for (i in seq_len(nrow(labels))) { |
1229 | 55x |
if (debug) { |
1230 | 29x |
gp <- grid::gpar(col = debug_line_col, |
1231 | 29x |
lwd = 0.5) |
1232 | 29x |
grid::grid.points(x = labels$label_x[i], |
1233 | 29x |
y = labels$label_y[[i]], |
1234 | 29x |
default.units = "native", |
1235 | 29x |
pch = 3, gp = gp) |
1236 | 29x |
half_height <- grid::convertHeight(grid::unit(1/2 * labels$cex[i], "strheight", labels$label[[i]]), |
1237 | 29x |
unitTo = "native", valueOnly = TRUE) |
1238 | 29x |
top <- labels$label_y[i] + half_height |
1239 | 29x |
bottom <- labels$label_y[i] - half_height |
1240 | 29x |
left <- grid::convertWidth(grid::unit(labels$label_x[i], "native") - |
1241 | 29x |
grid::unit(0.525 * labels$cex[i], "strwidth", labels$label[[i]]), |
1242 | 29x |
unitTo = "native", valueOnly = TRUE) |
1243 | 29x |
right <- grid::convertWidth(grid::unit(labels$label_x[i], "native") + |
1244 | 29x |
grid::unit(0.525 * labels$cex[i], "strwidth", labels$label[[i]]), |
1245 | 29x |
unitTo = "native", valueOnly = TRUE) |
1246 | 29x |
grid::grid.lines(c(left, right), c(top, top), |
1247 | 29x |
default.units = "native", gp = gp) |
1248 | 29x |
grid::grid.lines(c(left, right), c(bottom, bottom), |
1249 | 29x |
default.units = "native", gp = gp) |
1250 |
} |
|
1251 | 55x |
gp <- grid::gpar(col = labels$col[i], |
1252 | 55x |
cex = labels$cex[i]) |
1253 | 55x |
grid::grid.text(label = labels$label[[i]], |
1254 | 55x |
x = labels$label_x[i], |
1255 | 55x |
y = labels$label_y[[i]], |
1256 | 55x |
default.units = "native", gp = gp) |
1257 |
} |
|
1258 | 8x |
return(NULL) |
1259 |
} |
|
1260 | ||
1261 |
### * (1) sankey_place_nodes() |
|
1262 | ||
1263 |
#' Perform initial placement of the nodes |
|
1264 |
#' |
|
1265 |
#' @param topo Topology. |
|
1266 |
#' @param nodes Node tibble. |
|
1267 |
#' @param flows Flows tibble. |
|
1268 |
#' @param layout Layout string. |
|
1269 |
#' @param xlim,ylim Limits of the x and y scales. |
|
1270 |
#' |
|
1271 |
#' @examples |
|
1272 |
#' sankey_place_nodes <- isotracer:::sankey_place_nodes |
|
1273 |
#' |
|
1274 |
#' topo <- topo(trini_mod) |
|
1275 |
#' nodes <- tibble::tibble(comp = colnames(topo), size = 1, col = "red") |
|
1276 |
#' nodes$label <- letters[1:nrow(nodes)] |
|
1277 |
#' sankey_place_nodes(topo, nodes, layout = "left2right") |
|
1278 |
#' sankey_place_nodes(topo, nodes, layout = "stress") |
|
1279 |
#' |
|
1280 |
#' @keywords internal |
|
1281 |
#' @noRd |
|
1282 | ||
1283 |
sankey_place_nodes <- function(topo, nodes = NULL, flows, layout, xlim = c(0, 1), |
|
1284 |
ylim = c(0, 1)) { |
|
1285 | 8x |
nodes_arg <- nodes |
1286 |
# Calculate node locations |
|
1287 | 8x |
nodes <- sankey_calc_nodes_locations(topo, layout) |
1288 |
# Adjust node locations to fill the canvas |
|
1289 | 8x |
nodes$x <- (nodes$x - min(nodes$x)) / (max(nodes$x) - min(nodes$x)) * diff(xlim) + xlim[1] |
1290 | 8x |
nodes$y <- (nodes$y - min(nodes$y)) / (max(nodes$y) - min(nodes$y)) * diff(ylim) + ylim[1] |
1291 | 8x |
nodes$label <- nodes$comp |
1292 |
# Return |
|
1293 | 8x |
if (!is.null(nodes_arg)) { |
1294 | 8x |
if (any(colnames(nodes_arg) %in% c("x", "y"))) { |
1295 | ! |
stop("Provided `nodes` tibble cannot have `x` or `y` column.") |
1296 |
} |
|
1297 | 8x |
if (! setequal(nodes$comp, nodes_arg$comp)) { |
1298 | ! |
stop("Provided `nodes` tibble must have a `comp` column with exactly the same entries as the topology compartments.") |
1299 |
} |
|
1300 | 8x |
if ("label" %in% colnames(nodes_arg)) { |
1301 | 4x |
nodes$label <- NULL |
1302 |
} |
|
1303 | 8x |
nodes <- dplyr::left_join(nodes, nodes_arg, by = "comp") |
1304 |
} |
|
1305 | 8x |
out <- list(nodes = nodes, edges = NULL) |
1306 | 8x |
attr(out, "layout") <- layout |
1307 | 8x |
return(out) |
1308 |
} |
|
1309 | ||
1310 |
### * (2) sankey_place_edge_sockets_on_nodes() |
|
1311 | ||
1312 |
#' Determine relative location of edge sockets on nodes |
|
1313 |
#' |
|
1314 |
#' @param scene A list with a "nodes" tibble. |
|
1315 |
#' @param topo A topology. |
|
1316 |
#' @param nodes A node tibble. |
|
1317 |
#' @param flows A tibble giving the flow rates between connected nodes. |
|
1318 |
#' @param layout String specifying the plot layout. |
|
1319 |
#' |
|
1320 |
#' @examples |
|
1321 |
#' sankey_place_nodes <- isotracer:::sankey_place_nodes |
|
1322 |
#' sankey_place_edge_sockets_on_nodes <- isotracer:::sankey_place_edge_sockets_on_nodes |
|
1323 |
#' |
|
1324 |
#' topo <- topo(trini_mod) |
|
1325 |
#' flows <- flows_from_topo(topo) |
|
1326 |
#' flows$width <- 1 |
|
1327 |
#' layout <- "stress" |
|
1328 |
#' scene <- sankey_place_nodes(topo, flows = flows, layout = layout) |
|
1329 |
#' z <- sankey_place_edge_sockets_on_nodes(scene, topo, flows = flows, layout = layout) |
|
1330 |
#' |
|
1331 |
#' @keywords internal |
|
1332 |
#' @noRd |
|
1333 | ||
1334 |
sankey_place_edge_sockets_on_nodes <- function(scene, topo, nodes = NULL, flows, layout) { |
|
1335 | 8x |
nodes <- scene$nodes |
1336 | 8x |
edges <- flows |
1337 | 8x |
stopifnot(is.null(scene[["edges"]])) |
1338 | 8x |
edges$from_x <- nodes$x[match(edges$from, nodes$comp)] |
1339 | 8x |
edges$to_x <- nodes$x[match(edges$to, nodes$comp)] |
1340 | 8x |
edges$from_y <- nodes$y[match(edges$from, nodes$comp)] |
1341 | 8x |
edges$to_y <- nodes$y[match(edges$to, nodes$comp)] |
1342 | 8x |
sockets <- list() |
1343 |
# Layout left2right |
|
1344 | 8x |
if (layout == "left2right") { |
1345 | 2x |
edges <- edges[order(edges$from_x, edges$from_y), ] |
1346 | 2x |
nodes$left_accumulator <- 0 |
1347 | 2x |
nodes$right_accumulator <- 0 |
1348 | 2x |
for (i in seq_len(nrow(edges))) { |
1349 | 34x |
from <- edges$from[i] |
1350 | 34x |
to <- edges$to[i] |
1351 | 34x |
edge_id <- c(from = from, to = to) |
1352 |
# Socket from |
|
1353 | 34x |
socket_from <- list() |
1354 | 34x |
socket_from[["node_id"]] <- from |
1355 | 34x |
socket_from[["node_side"]] <- "right" |
1356 | 34x |
socket_from[["edge_id"]] <- list(edge_id) |
1357 | 34x |
socket_from[["edge_end"]] <- "from" |
1358 | 34x |
socket_from[["rel_loc"]] <- (nodes$right_accumulator[nodes$comp == from] + |
1359 | 34x |
edges$width[i]/2) |
1360 | 34x |
nodes$right_accumulator[nodes$comp == from] <- (nodes$right_accumulator[nodes$comp == from] + |
1361 | 34x |
edges$width[i]) |
1362 | 34x |
socket_from[["width"]] <- edges$width[i] |
1363 |
# Socket to |
|
1364 | 34x |
socket_to <- list() |
1365 | 34x |
socket_to[["node_id"]] <- to |
1366 | 34x |
socket_to[["node_side"]] <- "left" |
1367 | 34x |
socket_to[["edge_id"]] <- list(edge_id) |
1368 | 34x |
socket_to[["edge_end"]] <- "to" |
1369 | 34x |
socket_to[["rel_loc"]] <- (nodes$left_accumulator[nodes$comp == to] + |
1370 | 34x |
edges$width[i]/2) |
1371 | 34x |
nodes$left_accumulator[nodes$comp == to] <- (nodes$left_accumulator[nodes$comp == to] + |
1372 | 34x |
edges$width[i]) |
1373 | 34x |
socket_to[["width"]] <- edges$width[i] |
1374 |
# Store sockets |
|
1375 | 34x |
sockets[[2*i-1]] <- socket_from |
1376 | 34x |
sockets[[2*i]] <- socket_to |
1377 |
} |
|
1378 | 6x |
} else if (layout %in% c("stress", "kk", "lgl", "fr", "dh", "mds")) { |
1379 |
# Layout stress or kk or ... |
|
1380 | 6x |
edges <- edges[order(edges$from_x, edges$from_y), ] |
1381 | 6x |
sides <- c("right", "top", "left", "bottom") |
1382 |
# Place sockets (node side and angular location) |
|
1383 | 6x |
for (i in seq_len(nrow(edges))) { |
1384 | 33x |
from <- edges$from[i] |
1385 | 33x |
to <- edges$to[i] |
1386 | 33x |
edge_id <- c(from = from, to = to) |
1387 | 33x |
edge_id_char <- paste(from, to, sep = "->") |
1388 | 33x |
recip_edge_id <- paste(to, from, sep = "->") |
1389 |
# Socket from |
|
1390 | 33x |
quadrant <- sankey_stress_quadrant(y = edges$to_y[i] - edges$from_y[i], |
1391 | 33x |
x = edges$to_x[i] - edges$from_x[i]) |
1392 | 33x |
socket_from <- list() |
1393 | 33x |
socket_from[["node_id"]] <- from |
1394 | 33x |
socket_from[["node_side"]] <- sides[quadrant["q"]] |
1395 | 33x |
socket_from[["edge_id"]] <- list(edge_id) |
1396 | 33x |
socket_from[["edge_id_char"]] <- edge_id_char |
1397 | 33x |
socket_from[["recip_edge_id"]] <- recip_edge_id |
1398 | 33x |
socket_from[["edge_end"]] <- "from" |
1399 | 33x |
socket_from[["rel_quadrant_angle"]] <- quadrant["a"] |
1400 | 33x |
socket_from[["width"]] <- edges$width[i] |
1401 |
# Socket to |
|
1402 | 33x |
quadrant <- sankey_stress_quadrant(y = -(edges$to_y[i] - edges$from_y[i]), |
1403 | 33x |
x = -(edges$to_x[i] - edges$from_x[i])) |
1404 | 33x |
socket_to <- list() |
1405 | 33x |
socket_to[["node_id"]] <- to |
1406 | 33x |
socket_to[["node_side"]] <- sides[quadrant["q"]] |
1407 | 33x |
socket_to[["edge_id"]] <- list(edge_id) |
1408 | 33x |
socket_to[["edge_id_char"]] <- edge_id_char |
1409 | 33x |
socket_to[["recip_edge_id"]] <- recip_edge_id |
1410 | 33x |
socket_to[["edge_end"]] <- "to" |
1411 | 33x |
socket_to[["rel_quadrant_angle"]] <- quadrant["a"] |
1412 | 33x |
socket_to[["width"]] <- edges$width[i] |
1413 |
# Give a little nudge if a reciprocal edge exists |
|
1414 | 33x |
if (recip_edge_id %in% sapply(sockets, "[[", "edge_id_char")) { |
1415 | 9x |
socket_to[["rel_quadrant_angle"]] <- socket_to[["rel_quadrant_angle"]] - 1e-8 |
1416 |
} |
|
1417 |
# Store sockets |
|
1418 | 33x |
sockets[[2*i-1]] <- socket_from |
1419 | 33x |
sockets[[2*i]] <- socket_to |
1420 |
} |
|
1421 | 6x |
sockets <- dplyr::bind_rows(sockets) |
1422 | 6x |
sockets <- sockets[order(sockets$node_id, sockets$node_side, |
1423 | 6x |
sockets$rel_quadrant_angle), ] |
1424 | ||
1425 |
# Update the node side accumulators and sockets relative locations |
|
1426 | 6x |
nodes$left_accumulator <- 0 |
1427 | 6x |
nodes$right_accumulator <- 0 |
1428 | 6x |
nodes$top_accumulator <- 0 |
1429 | 6x |
nodes$bottom_accumulator <- 0 |
1430 | 6x |
sockets[["rel_loc"]] <- 0 |
1431 | 6x |
for (i in seq_len(nrow(sockets))) { |
1432 | 66x |
ni <- which(nodes$comp == sockets$node_id[i]) |
1433 | 66x |
side <- sockets$node_side[i] |
1434 | 66x |
if (side == "left") { |
1435 | 12x |
sockets$rel_loc[i] <- nodes$left_accumulator[ni] - sockets$width[i]/2 |
1436 | 12x |
nodes$left_accumulator[ni] <- nodes$left_accumulator[ni] - sockets$width[i] |
1437 |
} |
|
1438 | 66x |
if (side == "bottom") { |
1439 | 21x |
sockets$rel_loc[i] <- nodes$bottom_accumulator[ni] + sockets$width[i]/2 |
1440 | 21x |
nodes$bottom_accumulator[ni] <- nodes$bottom_accumulator[ni] + sockets$width[i] |
1441 |
} |
|
1442 | 66x |
if (side == "right") { |
1443 | 12x |
sockets$rel_loc[i] <- nodes$right_accumulator[ni] + sockets$width[i]/2 |
1444 | 12x |
nodes$right_accumulator[ni] <- nodes$right_accumulator[ni] + sockets$width[i] |
1445 |
} |
|
1446 | 66x |
if (side == "top") { |
1447 | 21x |
sockets$rel_loc[i] <- nodes$top_accumulator[ni] - sockets$width[i]/2 |
1448 | 21x |
nodes$top_accumulator[ni] <- nodes$top_accumulator[ni] - sockets$width[i] |
1449 |
} |
|
1450 |
} |
|
1451 | ||
1452 |
# Optimize sockets distribution around a given node |
|
1453 | 6x |
for (n in nodes$comp) { |
1454 | 27x |
node_sockets <- sockets[sockets$node_id == n, ] |
1455 | 27x |
current_imbalance <- sankey_stress_socket_imbalance(node_sockets) |
1456 | 27x |
possible_moves <- sankey_stress_socket_moves(node_sockets) |
1457 | 27x |
if (length(possible_moves) > 0) { |
1458 | 18x |
possible_imbalances <- sapply(possible_moves, |
1459 | 18x |
sankey_stress_socket_imbalance) |
1460 | 18x |
possible_moves <- possible_moves[order(possible_imbalances)] |
1461 | 18x |
possible_imbalances <- sort(possible_imbalances) |
1462 | 18x |
if (possible_imbalances[1] < current_imbalance) { |
1463 |
# Do the move |
|
1464 | 18x |
sockets[sockets$node_id == n, ] <- possible_moves[[1]] |
1465 |
} |
|
1466 |
} |
|
1467 |
} |
|
1468 | ||
1469 |
# Update nodes accumulators and sockets rel_loc |
|
1470 | 6x |
sockets <- sockets[order(sockets$node_id, sockets$node_side, |
1471 | 6x |
sockets$rel_quadrant_angle), ] |
1472 | 6x |
nodes$left_accumulator <- 0 |
1473 | 6x |
nodes$right_accumulator <- 0 |
1474 | 6x |
nodes$top_accumulator <- 0 |
1475 | 6x |
nodes$bottom_accumulator <- 0 |
1476 | 6x |
sockets[["rel_loc"]] <- 0 |
1477 | 6x |
for (i in seq_len(nrow(sockets))) { |
1478 | 66x |
ni <- which(nodes$comp == sockets$node_id[i]) |
1479 | 66x |
side <- sockets$node_side[i] |
1480 | 66x |
if (side == "left") { |
1481 | 15x |
sockets$rel_loc[i] <- nodes$left_accumulator[ni] - sockets$width[i]/2 |
1482 | 15x |
nodes$left_accumulator[ni] <- nodes$left_accumulator[ni] - sockets$width[i] |
1483 |
} |
|
1484 | 66x |
if (side == "bottom") { |
1485 | 18x |
sockets$rel_loc[i] <- nodes$bottom_accumulator[ni] + sockets$width[i]/2 |
1486 | 18x |
nodes$bottom_accumulator[ni] <- nodes$bottom_accumulator[ni] + sockets$width[i] |
1487 |
} |
|
1488 | 66x |
if (side == "right") { |
1489 | 15x |
sockets$rel_loc[i] <- nodes$right_accumulator[ni] + sockets$width[i]/2 |
1490 | 15x |
nodes$right_accumulator[ni] <- nodes$right_accumulator[ni] + sockets$width[i] |
1491 |
} |
|
1492 | 66x |
if (side == "top") { |
1493 | 18x |
sockets$rel_loc[i] <- nodes$top_accumulator[ni] - sockets$width[i]/2 |
1494 | 18x |
nodes$top_accumulator[ni] <- nodes$top_accumulator[ni] - sockets$width[i] |
1495 |
} |
|
1496 |
} |
|
1497 |
} else { |
|
1498 |
# Default sockets |
|
1499 | ! |
for (i in seq_len(nrow(edges))) { |
1500 | ! |
from <- edges$from[i] |
1501 | ! |
to <- edges$to[i] |
1502 | ! |
edge_id <- c(from = from, to = to) |
1503 |
# Socket from |
|
1504 | ! |
socket_from <- list() |
1505 | ! |
socket_from[["node_id"]] <- from |
1506 | ! |
socket_from[["node_side"]] <- "center" |
1507 | ! |
socket_from[["edge_id"]] <- list(edge_id) |
1508 | ! |
socket_from[["edge_end"]] <- "from" |
1509 | ! |
socket_from[["rel_loc"]] <- 0 |
1510 | ! |
socket_from[["width"]] <- edges$width[i] |
1511 |
# Socket to |
|
1512 | ! |
socket_to <- list() |
1513 | ! |
socket_to[["node_id"]] <- to |
1514 | ! |
socket_to[["node_side"]] <- "center" |
1515 | ! |
socket_to[["edge_id"]] <- list(edge_id) |
1516 | ! |
socket_to[["edge_end"]] <- "to" |
1517 | ! |
socket_to[["rel_loc"]] <- 0 |
1518 | ! |
socket_to[["width"]] <- edges$width[i] |
1519 |
# Store sockets |
|
1520 | ! |
sockets[[2*i-1]] <- socket_from |
1521 | ! |
sockets[[2*i]] <- socket_to |
1522 |
} |
|
1523 |
} |
|
1524 |
# Return |
|
1525 | 8x |
sockets <- dplyr::bind_rows(sockets) |
1526 | 8x |
out <- scene |
1527 | 8x |
out[["nodes"]] <- nodes |
1528 | 8x |
out[["edges"]] <- edges |
1529 | 8x |
out[["edge_sockets"]] <- sockets |
1530 | 8x |
return(out) |
1531 |
} |
|
1532 | ||
1533 |
### * (3) sankey_calc_node_shape() |
|
1534 | ||
1535 |
#' Determine node shape |
|
1536 |
#' |
|
1537 |
#' @importFrom stats aggregate |
|
1538 |
#' |
|
1539 |
#' @param scene Scene list. |
|
1540 |
#' @param topo Topology. |
|
1541 |
#' @param nodes Node tibble. |
|
1542 |
#' @param flows Flows tibble. |
|
1543 |
#' @param layout Layout string. |
|
1544 |
#' @param node_f Multiplicative factor used to adjust node size. |
|
1545 |
#' @param xlim Limits of the x scale. |
|
1546 |
#' @param node_s String defining how node size is calculated. The effect of the |
|
1547 |
#' string also depends on the chosen layout. |
|
1548 |
#' |
|
1549 |
#' @examples |
|
1550 |
#' library(magrittr) |
|
1551 |
#' |
|
1552 |
#' sankey_place_nodes <- isotracer:::sankey_place_nodes |
|
1553 |
#' sankey_place_edge_sockets_on_nodes <- isotracer:::sankey_place_edge_sockets_on_nodes |
|
1554 |
#' sankey_calc_node_shape <- isotracer:::sankey_calc_node_shape |
|
1555 |
#' |
|
1556 |
#' topo <- topo(trini_mod) |
|
1557 |
#' flows <- flows_from_topo(topo) |
|
1558 |
#' flows$width <- 1 |
|
1559 |
#' layout <- "left2right" |
|
1560 |
#' scene <- sankey_place_nodes(topo, flows = flows, layout = layout) |
|
1561 |
#' scene <- sankey_place_edge_sockets_on_nodes(scene, topo, flows = flows, layout = layout) |
|
1562 |
#' z <- sankey_calc_node_shape(scene, topo, flows = flows, layout = layout) |
|
1563 |
#' |
|
1564 |
#' topo <- topo(trini_mod) |
|
1565 |
#' flows <- flows_from_topo(topo) |
|
1566 |
#' flows$width <- 1 |
|
1567 |
#' layout <- "stress" |
|
1568 |
#' scene <- sankey_place_nodes(topo, flows = flows, layout = layout) |
|
1569 |
#' scene <- sankey_place_edge_sockets_on_nodes(scene, topo, flows = flows, layout = layout) |
|
1570 |
#' z <- sankey_calc_node_shape(scene, topo, flows = flows, layout = layout) |
|
1571 |
#' |
|
1572 |
#' y <- new_networkModel() %>% |
|
1573 |
#' set_topo(c("subs -> NH3 -> subs", |
|
1574 |
#' "NH3 -> Q, E", "E -> Q -> E", |
|
1575 |
#' "E -> D, M")) %>% |
|
1576 |
#' set_steady("subs") %>% |
|
1577 |
#' set_prop_family("normal_sd") |
|
1578 |
#' y <- topo(y) |
|
1579 |
#' nodes <- nodes_from_topo(y) |
|
1580 |
#' nodes$size <- runif(nrow(nodes), 1, 2) |
|
1581 |
#' flows <- flows_from_topo(y) |
|
1582 |
#' flows$width <- runif(nrow(flows), 0.2, 5) |
|
1583 |
#' layout <- "stress" |
|
1584 |
#' scene <- sankey_place_nodes(y, nodes = nodes, flows = flows, layout = layout) |
|
1585 |
#' scene <- sankey_place_edge_sockets_on_nodes(scene, y, flows = flows, layout = layout) |
|
1586 |
#' z <- sankey_calc_node_shape(scene, y, flows = flows, layout = layout) |
|
1587 |
#' |
|
1588 |
#' @keywords internal |
|
1589 |
#' @noRd |
|
1590 | ||
1591 |
sankey_calc_node_shape <- function(scene, topo, nodes = NULL, flows, layout, |
|
1592 |
node_f = 1, xlim = c(0, 1), node_s = "auto") { |
|
1593 | 8x |
nodes <- scene$nodes |
1594 | 8x |
sockets <- scene$edge_sockets |
1595 | 8x |
nodes$shape <- NA |
1596 | 8x |
nodes$width <- NA |
1597 | 8x |
nodes$height <- NA |
1598 | 8x |
if (!"size" %in% colnames(nodes)) { |
1599 | ! |
nodes[["size"]] <- 1 |
1600 |
} |
|
1601 |
# Layout left2right |
|
1602 | 8x |
if (layout == "left2right") { |
1603 | 2x |
for (i in seq_len(nrow(nodes))) { |
1604 | 28x |
nodes$shape[i] <- "rect" |
1605 | 28x |
nodes$height[i] <- list(grid::unit(max(nodes$left_accumulator[i], |
1606 | 28x |
nodes$right_accumulator[i]), |
1607 | 28x |
"native")) |
1608 | 28x |
nodes$width[i] <- nodes$size[i] / as.numeric(nodes$height[[i]]) |
1609 |
} |
|
1610 |
# Adjust nodes width |
|
1611 | 2x |
max_width_per_x <- aggregate(width ~ x, data = nodes, FUN = max) |
1612 | 2x |
total_width <- sum(max_width_per_x$width) |
1613 | 2x |
max_allowed_width <- diff(xlim) * 0.5 |
1614 | 2x |
adj_factor <- max_allowed_width / total_width |
1615 | 2x |
nodes$width <- lapply(nodes$width * adj_factor * node_f, |
1616 | 2x |
function(x) grid::unit(x, "native") ) |
1617 | 6x |
} else if (layout %in% c("stress", "kk", "lgl", "fr", "dh", "mds")) { |
1618 |
# Layout stress or kk or ... |
|
1619 | 6x |
min_width <- grid::convertWidth(grid::unit(1, "strwidth", "x"), unitTo = "native", |
1620 | 6x |
valueOnly = TRUE) |
1621 | 6x |
min_height <- abs(grid::convertHeight(grid::unit(1, "strheight", "x"), unitTo = "native", |
1622 | 6x |
valueOnly = TRUE)) |
1623 | 6x |
for (i in seq_len(nrow(nodes))) { |
1624 | 27x |
node_sockets <- sockets[sockets$node_id == nodes$comp[i], ] |
1625 | 27x |
side_sockets <- lapply(c("top", "left", "right", "bottom"), function(s) { |
1626 | 108x |
z <- node_sockets[node_sockets$node_side == s, ] |
1627 | 48x |
if (nrow(z) == 0) return(0) |
1628 | 60x |
return(sum(z$width)) |
1629 |
}) |
|
1630 | 27x |
names(side_sockets) <- c("top", "left", "right", "bottom") |
1631 | 27x |
nodes$shape[i] <- "rect" |
1632 | 27x |
nodes$width[i] <- list(grid::unit(max(min_width, side_sockets[["top"]], |
1633 | 27x |
side_sockets[["bottom"]]), |
1634 | 27x |
units = "native")) |
1635 | 27x |
nodes$height[i] <- list(grid::unit(max(min_height, side_sockets[["left"]], |
1636 | 27x |
side_sockets[["right"]]), |
1637 | 27x |
units = "native")) |
1638 |
} |
|
1639 | 6x |
if (node_s == "constant") { |
1640 | 2x |
max_width <- do.call(max, nodes$width) |
1641 | 2x |
max_height <- do.call(max, nodes$height) |
1642 | 2x |
nodes$width <- rep(list(max_width), nrow(nodes)) |
1643 | 2x |
nodes$height <- rep(list(max_height), nrow(nodes)) |
1644 |
} |
|
1645 | 6x |
if (node_s == "square" | node_s == "roundsquare") { |
1646 | ! |
ext_factor <- ifelse(node_s == "roundsquare", 1.10, 1.05) |
1647 | ! |
max_width <- do.call(max, nodes$width) |
1648 | ! |
max_height <- do.call(max, nodes$height) |
1649 | ! |
side_dim <- max(max_width, max_height) * ext_factor |
1650 | ! |
nodes$width <- rep(list(side_dim), nrow(nodes)) |
1651 | ! |
nodes$height <- rep(list(side_dim), nrow(nodes)) |
1652 |
} |
|
1653 | 6x |
if (node_s == "prop") { |
1654 | 2x |
nodes$min_area <- as.numeric(nodes$width) * as.numeric(nodes$height) |
1655 | 2x |
nodes$dim_adj <- nodes$size / nodes$min_area |
1656 | 2x |
nodes$dim_adj <- nodes$dim_adj / min(nodes$dim_adj) |
1657 | 2x |
for (i in seq_len(nrow(nodes))) { |
1658 | 9x |
a <- max(as.numeric(nodes$width[[i]]), as.numeric(nodes$height[[i]])) |
1659 | 9x |
b <- min(as.numeric(nodes$width[[i]]), as.numeric(nodes$height[[i]])) |
1660 | 9x |
adj <- nodes$dim_adj[i] |
1661 | 9x |
if (adj <= a/b) { |
1662 | 4x |
beta <- adj |
1663 | 4x |
alpha <- 1 |
1664 |
} else { |
|
1665 | 5x |
beta <- sqrt(adj * a/b) |
1666 | 5x |
alpha <- sqrt(adj * b/a) |
1667 |
} |
|
1668 | 9x |
if (as.numeric(nodes$width[[i]]) >= as.numeric(nodes$height[[i]])) { |
1669 | 6x |
nodes$width[[i]] <- grid::unit(alpha * as.numeric(nodes$width[[i]]), "native") |
1670 | 6x |
nodes$height[[i]] <- grid::unit(beta * as.numeric(nodes$height[[i]]), "native") |
1671 |
} else { |
|
1672 | 3x |
nodes$width[[i]] <- grid::unit(beta * as.numeric(nodes$width[[i]]), "native") |
1673 | 3x |
nodes$height[[i]] <- grid::unit(alpha * as.numeric(nodes$height[[i]]), "native") |
1674 |
} |
|
1675 |
} |
|
1676 |
} |
|
1677 |
} else { |
|
1678 |
# Default node shape |
|
1679 | ! |
width <- list(grid::unit(1, "strwidth", "toto")) |
1680 | ! |
height <- list(grid::unit(1, "strheight", "toto")) |
1681 | ! |
for (i in seq_len(nrow(nodes))) { |
1682 | ! |
nodes$shape[i] <- "rect" |
1683 | ! |
nodes$width[i] <- width |
1684 | ! |
nodes$height[i] <- height |
1685 |
} |
|
1686 |
} |
|
1687 |
# Return |
|
1688 | 8x |
out <- scene |
1689 | 8x |
out$nodes <- nodes |
1690 | 8x |
return(out) |
1691 |
} |
|
1692 | ||
1693 |
### * (4) sankey_adjust_node_locations() |
|
1694 | ||
1695 |
#' Adjust node location |
|
1696 |
#' |
|
1697 |
#' @param scene Scene list. |
|
1698 |
#' @param topo Topology. |
|
1699 |
#' @param nodes Node tibble. |
|
1700 |
#' @param flows Flows tibble. |
|
1701 |
#' @param layout Layout string. |
|
1702 |
#' |
|
1703 |
#' @examples |
|
1704 |
#' sankey_place_nodes <- isotracer:::sankey_place_nodes |
|
1705 |
#' sankey_place_edge_sockets_on_nodes <- isotracer:::sankey_place_edge_sockets_on_nodes |
|
1706 |
#' sankey_calc_node_shape <- isotracer:::sankey_calc_node_shape |
|
1707 |
#' sankey_adjust_node_locations <- isotracer:::sankey_adjust_node_locations |
|
1708 |
#' |
|
1709 |
#' topo <- topo(trini_mod) |
|
1710 |
#' flows <- flows_from_topo(topo) |
|
1711 |
#' flows$width <- 1 |
|
1712 |
#' layout <- "left2right" |
|
1713 |
#' scene <- sankey_place_nodes(topo, flows = flows, layout = layout) |
|
1714 |
#' scene <- sankey_place_edge_sockets_on_nodes(scene, topo, flows = flows, layout = layout) |
|
1715 |
#' scene <- sankey_calc_node_shape(scene, topo, flows = flows, layout = layout) |
|
1716 |
#' z <- sankey_adjust_node_locations(scene, topo, flows = flows, layout = layout) |
|
1717 |
#' |
|
1718 |
#' @keywords internal |
|
1719 |
#' @noRd |
|
1720 | ||
1721 |
sankey_adjust_node_locations <- function(scene, topo, nodes = NULL, flows, layout) { |
|
1722 |
# Layout left2right |
|
1723 | 8x |
if (layout == "left2right") { |
1724 | 2x |
nodes <- scene$nodes |
1725 | 2x |
nodes <- nodes[order(nodes$x, nodes$y), ] |
1726 | 2x |
ylim <- range(nodes$y) |
1727 | 2x |
x_locs <- sort(unique(nodes$x)) |
1728 |
# Fix collision |
|
1729 | 2x |
for (xi in x_locs) { |
1730 | 8x |
ni <- nodes[nodes$x == xi, ] |
1731 | 8x |
previous_mean_y <- mean(ni$y) |
1732 | 8x |
total_height <- sum(as.numeric(unlist(ni$height))) |
1733 | 8x |
available_height <- diff(ylim) - total_height |
1734 | 8x |
min_spacer <- min(0.08, 0.1 * total_height, available_height / (nrow(ni) + 1)) |
1735 | 8x |
if (nrow(ni) > 1) { |
1736 | 8x |
for (i in 2:nrow(ni)) { |
1737 | 20x |
top_a <- ni$y[i-1] + as.numeric(ni$height[i-1]) / 2 |
1738 | 20x |
bottom_b <- ni$y[i] - as.numeric(ni$height[i]) / 2 |
1739 | 20x |
if (bottom_b - top_a < min_spacer) { |
1740 |
# Collision or too close, shift all remaining nodes upwards |
|
1741 | 6x |
shift <- top_a - bottom_b + min_spacer |
1742 | 6x |
ni$y[i] <- ni$y[i] + shift |
1743 |
} |
|
1744 |
} |
|
1745 |
} |
|
1746 | 8x |
new_mean_y <- mean(nodes$y) |
1747 | 8x |
ni$y <- ni$y - new_mean_y + previous_mean_y |
1748 | 8x |
nodes[nodes$x == xi, ] <- ni |
1749 |
} |
|
1750 |
# Quick minimization |
|
1751 | 2x |
for (xi in x_locs) { |
1752 | 8x |
comps <- nodes$comp[nodes$x == xi] |
1753 | 8x |
sources <- colnames(topo)[apply(topo[comps,, drop = FALSE], 2, sum) > 0] |
1754 | 8x |
if (length(sources) > 0) { |
1755 | 6x |
mean_y_self <- mean(nodes$y[nodes$x == xi]) |
1756 | 6x |
mean_y_sources <- mean(nodes$y[nodes$comp %in% sources]) |
1757 | 6x |
nodes$y[nodes$x == xi] <- nodes$y[nodes$x == xi] - mean_y_self + mean_y_sources |
1758 |
} |
|
1759 |
} |
|
1760 |
# Adjust node location so that none is outside the canvas |
|
1761 | 2x |
for (xi in x_locs) { |
1762 | 8x |
ni <- nodes[nodes$x == xi, ] |
1763 | 8x |
if (nrow(ni) > 1) { |
1764 | 8x |
previous_mean_y <- mean(ni$y) |
1765 | 8x |
total_height <- sum(as.numeric(unlist(ni$height))) |
1766 | 8x |
available_height <- diff(ylim) - total_height |
1767 | 8x |
min_spacer <- min(0.08, 0.1 * total_height, available_height / (nrow(ni) + 1)) |
1768 | 8x |
lowest <- ni$y[1] - as.numeric(ni$height[1]) / 2 |
1769 | 8x |
if (lowest < ylim[1]) { |
1770 | 2x |
extra <- ylim[1] - lowest |
1771 | 2x |
total_spacing <- (ni$y[nrow(ni)] - as.numeric(ni$height[nrow(ni)]) / 2) - lowest |
1772 | 2x |
if (total_spacing > extra) { |
1773 |
# The extra height can be accommodated in the spacing |
|
1774 | 2x |
spacing_adj_factor <- 1 - extra / total_spacing |
1775 |
# Apply this factor on the nodes from top to bottom |
|
1776 | 2x |
for (i in (nrow(ni)-1):1) { |
1777 | 10x |
current_spacing <- (ni$y[i+1] - as.numeric(ni$height[i+1]) / 2 - |
1778 | 10x |
ni$y[i] + as.numeric(ni$height[i]) / 2) |
1779 | 10x |
shift <- current_spacing * (1 - spacing_adj_factor) |
1780 | 10x |
for (j in i:1) { |
1781 | 30x |
ni$y[j] <- ni$y[j] + shift |
1782 |
} |
|
1783 |
} |
|
1784 |
} |
|
1785 |
} |
|
1786 | 8x |
highest <- ni$y[nrow(ni)] + as.numeric(ni$height[nrow(ni)]) / 2 |
1787 | 8x |
if (highest > ylim[2]) { |
1788 | ! |
extra <- highest - ylim[2] |
1789 | ! |
total_spacing <- highest - ni$y[1] - as.numeric(ni$height[1]) / 2 |
1790 | ! |
if (total_spacing > extra) { |
1791 |
# The extra height can be accommodated in the spacing |
|
1792 | ! |
spacing_adj_factor <- 1 - extra / total_spacing |
1793 |
# Apply this factor on the nodes from bottom to top |
|
1794 | ! |
for (i in 2:nrow(ni)) { |
1795 | ! |
current_spacing <- (ni$y[i] - as.numeric(ni$height[i]) / 2 - |
1796 | ! |
ni$y[i-1] + as.numeric(ni$height[i-1]) / 2) |
1797 | ! |
shift <- current_spacing * (1 - spacing_adj_factor) |
1798 | ! |
for (j in i:nrow(ni)) { |
1799 | ! |
ni$y[j] <- ni$y[j] - shift |
1800 |
} |
|
1801 |
} |
|
1802 |
} |
|
1803 |
} |
|
1804 |
} |
|
1805 | 8x |
nodes[nodes$x == xi, ] <- ni |
1806 |
} |
|
1807 | 2x |
out <- scene |
1808 | 2x |
out$nodes <- nodes |
1809 |
} else { |
|
1810 |
# Default is to do nothing |
|
1811 | 6x |
out <- scene |
1812 |
} |
|
1813 | 8x |
return(out) |
1814 |
} |
|
1815 | ||
1816 |
### * (5) sankey_adjust_edge_sockets() |
|
1817 | ||
1818 |
#' Adjust edge sockets relative location on nodes |
|
1819 |
#' |
|
1820 |
#' @param scene Scene list. |
|
1821 |
#' @param topo Topology. |
|
1822 |
#' @param nodes Node tibble. |
|
1823 |
#' @param flows Flows tibble. |
|
1824 |
#' @param layout Layout string. |
|
1825 |
#' |
|
1826 |
#' @keywords internal |
|
1827 |
#' @noRd |
|
1828 | ||
1829 |
sankey_adjust_edge_sockets <- function(scene, topo, nodes = NULL, flows, layout) { |
|
1830 | 8x |
nodes <- scene$nodes |
1831 | 8x |
edges <- scene$edges |
1832 | 8x |
edge_sockets <- scene$edge_sockets |
1833 |
# Layout left2right |
|
1834 | 8x |
if (layout == "left2right") { |
1835 | 2x |
for (i in seq_len(nrow(edge_sockets))) { |
1836 |
# Slide sockets so that each node face has its group of sockets centered |
|
1837 | 68x |
node_i <- which(nodes$comp == edge_sockets$node_id[i]) |
1838 | 68x |
if (edge_sockets$edge_end[i] == "from") { |
1839 | 34x |
shift <- nodes$right_accumulator[node_i] / 2 |
1840 |
} else { |
|
1841 | 34x |
shift <- nodes$left_accumulator[node_i] / 2 |
1842 |
} |
|
1843 | 68x |
edge_sockets$rel_loc[i] <- edge_sockets$rel_loc[i] - shift |
1844 |
} |
|
1845 | 2x |
out <- scene |
1846 | 2x |
out[["edge_sockets"]] <- edge_sockets |
1847 | 6x |
} else if (layout %in% c("stress", "kk", "lgl", "fr", "dh", "mds")) { |
1848 | 6x |
for (i in seq_len(nrow(edge_sockets))) { |
1849 |
# Slide sockets so that each node face has its group of sockets centered |
|
1850 | 66x |
node_i <- which(nodes$comp == edge_sockets$node_id[i]) |
1851 | 66x |
side <- edge_sockets$node_side[i] |
1852 | 66x |
if (side == "left") { |
1853 | 15x |
shift <- nodes$left_accumulator[node_i] / 2 |
1854 | 51x |
} else if (side == "top") { |
1855 | 18x |
shift <- nodes$top_accumulator[node_i] / 2 |
1856 | 33x |
} else if (side == "right") { |
1857 | 15x |
shift <- nodes$right_accumulator[node_i] / 2 |
1858 |
} else { |
|
1859 | 18x |
shift <- nodes$bottom_accumulator[node_i] / 2 |
1860 |
} |
|
1861 | 66x |
edge_sockets$rel_loc[i] <- edge_sockets$rel_loc[i] - shift |
1862 |
} |
|
1863 | 6x |
out <- scene |
1864 | 6x |
out[["edge_sockets"]] <- edge_sockets |
1865 |
} else { |
|
1866 |
# Default is to do nothing |
|
1867 | ! |
out <- scene |
1868 |
} |
|
1869 |
# Return |
|
1870 | 8x |
return(out) |
1871 |
} |
|
1872 | ||
1873 |
### * (6) sankey_calc_edge_socket_coordinates() |
|
1874 | ||
1875 |
#' Calculate absolute coordinates of edge sockets |
|
1876 |
#' |
|
1877 |
#' This function calculates the absolute (x, y) coordinates of the center of |
|
1878 |
#' each edge socket, as well as the normal vector of each socket (which is |
|
1879 |
#' actually the normal vector of the receiving node face). |
|
1880 |
#' |
|
1881 |
#' @param scene Scene list. |
|
1882 |
#' @param topo Topology. |
|
1883 |
#' @param nodes Node tibble. |
|
1884 |
#' @param flows Flows tibble. |
|
1885 |
#' @param layout Layout string. |
|
1886 |
#' |
|
1887 |
#' @examples |
|
1888 |
#' sankey_place_nodes <- isotracer:::sankey_place_nodes |
|
1889 |
#' sankey_place_edge_sockets_on_nodes <- isotracer:::sankey_place_edge_sockets_on_nodes |
|
1890 |
#' sankey_calc_node_shape <- isotracer:::sankey_calc_node_shape |
|
1891 |
#' sankey_adjust_edge_sockets <- isotracer:::sankey_adjust_edge_sockets |
|
1892 |
#' sankey_calc_edge_socket_coordinates <- isotracer:::sankey_calc_edge_socket_coordinates |
|
1893 |
#' |
|
1894 |
#' topo <- topo(trini_mod) |
|
1895 |
#' nodes <- nodes_from_topo(topo) |
|
1896 |
#' nodes$size <- 1 |
|
1897 |
#' flows <- flows_from_topo(topo) |
|
1898 |
#' flows$width <- 1 |
|
1899 |
#' layout <- "left2right" |
|
1900 |
#' scene <- sankey_place_nodes(topo, nodes = nodes, flows = flows, layout = layout) |
|
1901 |
#' scene <- sankey_place_edge_sockets_on_nodes(scene, topo, flows = flows, layout = layout) |
|
1902 |
#' scene <- sankey_calc_node_shape(scene, topo, flows = flows, layout = layout) |
|
1903 |
#' scene <- sankey_adjust_edge_sockets(scene, topo, flows = flows, layout = layout) |
|
1904 |
#' z <- sankey_calc_edge_socket_coordinates(scene, topo, flows = flows, layout = layout) |
|
1905 |
#' |
|
1906 |
#' @keywords internal |
|
1907 |
#' @noRd |
|
1908 | ||
1909 |
sankey_calc_edge_socket_coordinates <- function(scene, topo, nodes = NULL, flows, layout) { |
|
1910 | 8x |
nodes <- scene$nodes |
1911 | 8x |
sockets <- scene$edge_sockets |
1912 | 8x |
sockets$center_x <- NA |
1913 | 8x |
sockets$center_y <- NA |
1914 | 8x |
sockets$normal <- rep(list(NA), nrow(sockets)) |
1915 |
# Process the sockets |
|
1916 | 8x |
for (i in seq_len(nrow(sockets))) { |
1917 | 134x |
side <- sockets$node_side[i] |
1918 | 134x |
ni <- which(nodes$comp == sockets$node_id[i]) |
1919 | 134x |
nw <- as.numeric(grid::convertWidth(nodes$width[[ni]], unitTo = "native")) |
1920 | 134x |
nh <- as.numeric(grid::convertHeight(nodes$height[[ni]], unitTo = "native")) |
1921 | 134x |
stopifnot(side %in% c("center", "left", "right", "top", "bottom")) |
1922 | 134x |
if (side == "center") { |
1923 | ! |
sockets$center_x[i] <- nodes$x[ni] |
1924 | ! |
sockets$center_y[i] <- nodes$y[ni] |
1925 | ! |
sockets$normal[[i]] <- c(0, 0) |
1926 | 134x |
} else if (side %in% c("left", "right")) { |
1927 | 98x |
sockets$center_x[i] <- nodes$x[ni] |
1928 | 98x |
sockets$center_y[i] <- (nodes$y[ni] + |
1929 | 98x |
sockets$rel_loc[i]) |
1930 | 98x |
if (side == "left") { |
1931 | 49x |
sockets$normal[[i]] <- c(-1, 0) |
1932 | 49x |
sockets$center_x[i] <- sockets$center_x[i] - nw / 2 |
1933 |
} else { |
|
1934 | 49x |
sockets$normal[[i]] <- c(1, 0) |
1935 | 49x |
sockets$center_x[i] <- sockets$center_x[i] + nw / 2 |
1936 |
} |
|
1937 | 36x |
} else if (side %in% c("top", "bottom")) { |
1938 | 36x |
sockets$center_x[i] <- (nodes$x[ni] + |
1939 | 36x |
sockets$rel_loc[i]) |
1940 | 36x |
sockets$center_y[i] <- nodes$y[ni] |
1941 | 36x |
if (side == "top") { |
1942 | 18x |
sockets$normal[[i]] <- c(0, 1) |
1943 | 18x |
sockets$center_y[i] <- sockets$center_y[i] + nh / 2 |
1944 |
} else { |
|
1945 | 18x |
sockets$normal[[i]] <- c(0, -1) |
1946 | 18x |
sockets$center_y[i] <- sockets$center_y[i] - nh / 2 |
1947 |
} |
|
1948 |
} |
|
1949 |
} |
|
1950 |
# Return |
|
1951 | 8x |
out <- scene |
1952 | 8x |
out[["edge_sockets"]] <- sockets |
1953 | 8x |
return(out) |
1954 |
} |
|
1955 | ||
1956 |
### * (7) sankey_place_edge_backbones() |
|
1957 | ||
1958 |
#' Calculate absolute coordinates of edge backbones |
|
1959 |
#' |
|
1960 |
#' @param scene Scene list. |
|
1961 |
#' @param topo Topology. |
|
1962 |
#' @param nodes Node tibble. |
|
1963 |
#' @param flows Flows tibble. |
|
1964 |
#' @param layout Layout string. |
|
1965 |
#' @param n Integer, number of steps used for Bézier curve interpolation. |
|
1966 |
#' |
|
1967 |
#' @examples |
|
1968 |
#' sankey_place_nodes <- isotracer:::sankey_place_nodes |
|
1969 |
#' sankey_place_edge_sockets_on_nodes <- isotracer:::sankey_place_edge_sockets_on_nodes |
|
1970 |
#' sankey_calc_node_shape <- isotracer:::sankey_calc_node_shape |
|
1971 |
#' sankey_calc_edge_socket_coordinates <- isotracer:::sankey_calc_edge_socket_coordinates |
|
1972 |
#' sankey_place_edge_backbones <- isotracer:::sankey_place_edge_backbones |
|
1973 |
#' |
|
1974 |
#' t <- topo(trini_mod) |
|
1975 |
#' nodes <- tibble::tibble(comp = colnames(t), size = 1, col = "red") |
|
1976 |
#' flows <- flows_from_topo(t) |
|
1977 |
#' flows$width <- 1 |
|
1978 |
#' layout <- "left2right" |
|
1979 |
#' scene <- sankey_place_nodes(t, nodes = nodes, flows = flows, layout = layout) |
|
1980 |
#' scene <- sankey_place_edge_sockets_on_nodes(scene, t, flows = flows, layout = layout) |
|
1981 |
#' scene <- sankey_calc_node_shape(scene, t, flows = flows, layout = layout) |
|
1982 |
#' scene <- sankey_calc_edge_socket_coordinates(scene, t, flows = flows, layout = layout) |
|
1983 |
#' z <- sankey_place_edge_backbones(scene, t, flows = flows, layout = layout) |
|
1984 |
#' |
|
1985 |
#' @keywords internal |
|
1986 |
#' @noRd |
|
1987 | ||
1988 |
sankey_place_edge_backbones <- function(scene, topo, nodes = NULL, flows, layout, |
|
1989 |
n = 32) { |
|
1990 | 8x |
nodes <- scene$nodes |
1991 | 8x |
edges <- scene$edges |
1992 | 8x |
edges$backbone <- rep(list(NA), nrow(edges)) |
1993 | 8x |
edges$control_points <- rep(list(NA), nrow(edges)) |
1994 | 8x |
edge_sockets <- scene$edge_sockets |
1995 |
# Default |
|
1996 | 8x |
for (i in seq_len(nrow(edges))) { |
1997 | 67x |
edge_id <- c(from = edges$from[i], to = edges$to[i]) |
1998 | 67x |
sockets <- edge_sockets[sapply(edge_sockets$edge_id, function(x) identical(edge_id, x)), ] |
1999 | 67x |
s_from <- as.list(sockets[sockets$edge_end == "from", ]) |
2000 | 67x |
s_to <- as.list(sockets[sockets$edge_end == "to", ]) |
2001 | 67x |
xy_start <- c(s_from$center_x, s_from$center_y) |
2002 | 67x |
xy_end <- c(s_to$center_x, s_to$center_y) |
2003 | 67x |
if (identical(s_from$normal[[1]], c(0, 0))) { |
2004 | ! |
v <- xy_end - xy_start |
2005 | ! |
v <- v / sqrt(sum(v^2)) |
2006 | ! |
s_from$normal[[1]] <- v |
2007 |
} |
|
2008 | 67x |
if (identical(s_to$normal[[1]], c(0, 0))) { |
2009 | ! |
v <- xy_start - xy_end |
2010 | ! |
v <- v / sqrt(sum(v^2)) |
2011 | ! |
s_to$normal[[1]] <- v |
2012 |
} |
|
2013 | 67x |
if (layout %in% c("left2right")) { |
2014 | 34x |
len_straight <- sqrt(sum((xy_end[1] - xy_start[1])^2)) |
2015 | 34x |
xy_start2 <- xy_start + c(s_from$normal[[1]][1], 0) * len_straight * 0.15 |
2016 | 34x |
xy_end2 <- xy_end + c(s_to$normal[[1]][1], 0) * len_straight * 0.15 |
2017 | 34x |
xy_start3 <- xy_start + c(s_from$normal[[1]][1], 0) * len_straight * 0.3 |
2018 | 34x |
xy_end3 <- xy_end + c(s_to$normal[[1]][1], 0) * len_straight * 0.3 |
2019 | 34x |
bb <- cbind(xy_start, xy_start2, xy_start3, |
2020 | 34x |
xy_end3, xy_end2, xy_end) |
2021 | 33x |
} else if (layout %in% c("stress", "kk", "lgl", "fr", "dh", "mds")) { |
2022 | 33x |
len_straight <- sqrt(sum((xy_end - xy_start)^2)) |
2023 | 33x |
xy_start2 <- xy_start + s_from$normal[[1]] * len_straight * 0.15 |
2024 | 33x |
xy_end2 <- xy_end + s_to$normal[[1]] * len_straight * 0.15 |
2025 | 33x |
xy_start3 <- xy_start + s_from$normal[[1]] * len_straight * 0.3 |
2026 | 33x |
xy_end3 <- xy_end + s_to$normal[[1]] * len_straight * 0.3 |
2027 | 33x |
bb <- cbind(xy_start, xy_start2, xy_start3, |
2028 | 33x |
xy_end3, xy_end2, xy_end) |
2029 |
} else { |
|
2030 | ! |
len_straight <- sqrt(sum((xy_end - xy_start)^2)) |
2031 | ! |
xy_start2 <- xy_start + s_from$normal[[1]] * len_straight * 0.2 |
2032 | ! |
xy_end2 <- xy_end + s_to$normal[[1]] * len_straight * 0.2 |
2033 | ! |
bb <- cbind(xy_start, xy_start2, xy_end2, xy_end) |
2034 |
} |
|
2035 | 67x |
edges$control_points[[i]] <- t(bb) |
2036 | 67x |
edges$backbone[[i]] <- bezierCurve(x = bb[1, ], y = bb[2, ], n = n) |
2037 |
} |
|
2038 |
# Return |
|
2039 | 8x |
out <- scene |
2040 | 8x |
out[["edges"]] <- edges |
2041 | 8x |
return(out) |
2042 |
} |
|
2043 | ||
2044 |
### * (8) sankey_place_labels() |
|
2045 | ||
2046 |
#' Place node labels |
|
2047 |
#' |
|
2048 |
#' @param scene Scene list. |
|
2049 |
#' @param topo Topology. |
|
2050 |
#' @param nodes Node tibble. |
|
2051 |
#' @param flows Flows tibble. |
|
2052 |
#' @param layout Layout string. |
|
2053 |
#' @param cex_lab Expansion factor for label size. |
|
2054 |
#' |
|
2055 |
#' @examples |
|
2056 |
#' sankey_place_nodes <- isotracer:::sankey_place_nodes |
|
2057 |
#' sankey_place_edge_sockets_on_nodes <- isotracer:::sankey_place_edge_sockets_on_nodes |
|
2058 |
#' sankey_calc_node_shape <- isotracer:::sankey_calc_node_shape |
|
2059 |
#' sankey_calc_edge_socket_coordinates <- isotracer:::sankey_calc_edge_socket_coordinates |
|
2060 |
#' sankey_place_edge_backbones <- isotracer:::sankey_place_edge_backbones |
|
2061 |
#' sankey_place_labels <- isotracer:::sankey_place_labels |
|
2062 |
#' |
|
2063 |
#' t <- topo(trini_mod) |
|
2064 |
#' nodes <- tibble::tibble(comp = colnames(t), size = 1, col = "red") |
|
2065 |
#' flows <- flows_from_topo(t) |
|
2066 |
#' flows$width <- 1 |
|
2067 |
#' layout <- "left2right" |
|
2068 |
#' scene <- sankey_place_nodes(t, nodes = nodes, flows = flows, layout = layout) |
|
2069 |
#' scene <- sankey_place_edge_sockets_on_nodes(scene, t, flows = flows, layout = layout) |
|
2070 |
#' scene <- sankey_calc_node_shape(scene, t, flows = flows, layout = layout) |
|
2071 |
#' scene <- sankey_calc_edge_socket_coordinates(scene, t, flows = flows, layout = layout) |
|
2072 |
#' scene <- sankey_place_edge_backbones(scene, t, flows = flows, layout = layout) |
|
2073 |
#' z <- sankey_place_labels(scene, t, flows = flows, layout = layout) |
|
2074 |
#' |
|
2075 |
#' @keywords internal |
|
2076 |
#' @noRd |
|
2077 | ||
2078 |
sankey_place_labels <- function(scene, topo, nodes, flows, layout, cex_lab = 1) { |
|
2079 | 8x |
nodes <- scene$nodes |
2080 | 8x |
labels <- nodes[, c("comp", "label", "x", "y", "width", "height")] |
2081 | 8x |
names(labels) <- c("comp", "label", "node_x", "node_y", "node_width", "node_height") |
2082 |
# Convert node dimensions to native units |
|
2083 | 8x |
labels$node_width <- sapply(labels$node_width, function(x) { |
2084 | 55x |
grid::convertWidth(x, unitTo = "native", valueOnly = TRUE) |
2085 |
}) |
|
2086 | 8x |
labels$node_height <- sapply(labels$node_height, function(x) { |
2087 | 55x |
grid::convertHeight(x, unitTo = "native", valueOnly = TRUE) |
2088 |
}) |
|
2089 |
# Calculate label coordinates |
|
2090 | 8x |
labels <- tibble::add_column(labels, cex = cex_lab, label_x = NA, label_y = NA) |
2091 | 8x |
for (i in seq_len(nrow(labels))) { |
2092 | 55x |
node_bottom <- labels$node_y[i] - labels$node_height[i]/2 |
2093 | 55x |
labels$label_x[i] <- labels$node_x[i] |
2094 | 55x |
labels$label_y[i] <- (node_bottom - |
2095 | 55x |
grid::convertHeight(grid::unit(0.5, "strheight", "X"), |
2096 | 55x |
unitTo = "native", valueOnly = TRUE) - |
2097 | 55x |
grid::convertHeight(grid::unit(cex_lab, "strheight", "X"), |
2098 | 55x |
unitTo = "native", valueOnly = TRUE) / 2) |
2099 |
} |
|
2100 |
# Return |
|
2101 | 8x |
out <- scene |
2102 | 8x |
out[["labels"]] <- labels |
2103 | 8x |
return(out) |
2104 |
} |
|
2105 | ||
2106 |
### * sankey_stress_quadrant() |
|
2107 | ||
2108 |
#' From an absolute angle, calculate the quadrant and relative angle in this quadrant |
|
2109 |
#' |
|
2110 |
#' In the "stress" layout, each node has four quadrants defined from its (x, y) |
|
2111 |
#' centre: 1 = east (-pi/4, pi/4), 2 = north (pi/4, 3pi/4), 3 = west (3pi/4, |
|
2112 |
#' 5pi/4), and 4 = south (-3pi/4, -pi/4). |
|
2113 |
#' |
|
2114 |
#' This function takes (y, x) in the same way as \code{\link{atan2}}. |
|
2115 |
#' |
|
2116 |
#' @param y,x Same as for \code{\link{atan2}}. From atan2 help: "The arc-tangent |
|
2117 |
#' of two arguments atan2(y, x) returns the angle between the x-axis and |
|
2118 |
#' the vector from the origin to (x, y), i.e., for positive arguments |
|
2119 |
#' atan2(y, x) == atan(y/x)." |
|
2120 |
#' |
|
2121 |
#' @return A vector with two elements: the quadrant and the relative |
|
2122 |
#' anti-clockwise angle from this quadrant origin. |
|
2123 |
#' |
|
2124 |
#' @examples |
|
2125 |
#' sankey_stress_quadrant <- isotracer:::sankey_stress_quadrant |
|
2126 |
#' |
|
2127 |
#' theta <- seq(- pi, pi, length.out = 128) |
|
2128 |
#' x <- cos(theta) |
|
2129 |
#' y <- sin(theta) |
|
2130 |
#' q <- sapply(seq_along(x), function(i) sankey_stress_quadrant(y[i], x[i])) |
|
2131 |
#' plot(theta, q[2,], col = c("blue", "green", "purple", "red")[q[1,]], |
|
2132 |
#' pch = 19) |
|
2133 |
#' |
|
2134 |
#' @keywords internal |
|
2135 |
#' @noRd |
|
2136 | ||
2137 |
sankey_stress_quadrant <- function(y, x) { |
|
2138 | 66x |
angle <- as.vector(atan2(y, x)) |
2139 | 66x |
stopifnot(angle <= pi & angle >= -pi) |
2140 | 66x |
if (angle >= -pi/4 & angle <= pi/4) { |
2141 |
# east |
|
2142 | 12x |
q <- 1 |
2143 | 12x |
a <- angle + pi/4 |
2144 | 54x |
} else if (angle >= pi/4 & angle <= 3*pi/4) { |
2145 |
# north |
|
2146 | 21x |
q <- 2 |
2147 | 21x |
a <- angle - pi/4 |
2148 | 33x |
} else if (angle >= 3*pi/4 | angle <= -3*pi/4 ) { |
2149 |
# west |
|
2150 | 12x |
q <- 3 |
2151 | 12x |
if (angle >= 3*pi/4) { |
2152 | 3x |
a <- angle - 3*pi/4 |
2153 |
} else { |
|
2154 | 9x |
angle <- angle + 2 * pi |
2155 | 9x |
a <- angle - 3*pi/4 |
2156 |
} |
|
2157 |
} else { |
|
2158 |
# south |
|
2159 | 21x |
q <- 4 |
2160 | 21x |
a <- angle + 3*pi/4 |
2161 |
} |
|
2162 | 66x |
return(c(q = q, a = a)) |
2163 |
} |
|
2164 | ||
2165 |
### * sankey_stress_socket_imbalance() |
|
2166 | ||
2167 |
#' Calculate an imbalance score for socket distribution around a node |
|
2168 |
#' |
|
2169 |
#' @param node_sockets A tibble containing the (at most 4) rows describing the |
|
2170 |
#' sockets related to one given node. |
|
2171 |
#' |
|
2172 |
#' @return The variance of the total socket width per node side (or NA if the |
|
2173 |
#' input has zero row). This can be used as an imbalance score to minimize |
|
2174 |
#' when optimizing the distribution of sockets around a node. |
|
2175 |
#' |
|
2176 |
#' @importFrom stats aggregate |
|
2177 |
#' @importFrom stats var |
|
2178 |
#' |
|
2179 |
#' @examples |
|
2180 |
#' sankey_stress_socket_imbalance <- isotracer:::sankey_stress_socket_imbalance |
|
2181 |
#' |
|
2182 |
#' z <- structure(list(node_id = c("arg", "arg"), node_side = c("bottom", "top"), |
|
2183 |
#' edge_id = list(c(from = "petro", to = "arg"), c(from = "tricor", to = "arg")), |
|
2184 |
#' edge_id_char = c("petro->arg", "tricor->arg"), |
|
2185 |
#' recip_edge_id = c("arg->petro", "arg->tricor"), edge_end = c("to", "to"), |
|
2186 |
#' rel_quadrant_angle = c(0.00537724841543286, 1.26282578153344), width = c(1, 1), |
|
2187 |
#' rel_loc = c(0.5, -0.5)), row.names = c(NA, -2L), |
|
2188 |
#' class = c("tbl_df", "tbl", "data.frame")) |
|
2189 |
#' sankey_stress_socket_imbalance(z) |
|
2190 |
#' |
|
2191 |
#' @keywords internal |
|
2192 |
#' @noRd |
|
2193 | ||
2194 |
sankey_stress_socket_imbalance <- function(node_sockets) { |
|
2195 | 51x |
if (nrow(node_sockets) > 0) { |
2196 | 51x |
width_per_side <- tibble::deframe(aggregate(width ~ node_side, |
2197 | 51x |
data = node_sockets, |
2198 | 51x |
FUN = sum)) |
2199 | 51x |
width_per_side <- c(width_per_side, rep(0, 4 - length(width_per_side))) |
2200 | 51x |
imbalance <- var(width_per_side) |
2201 |
} else { |
|
2202 | ! |
imbalance <- NA |
2203 |
} |
|
2204 | 51x |
return(imbalance) |
2205 |
} |
|
2206 | ||
2207 |
### * sankey_stress_socket_moves() |
|
2208 | ||
2209 |
#' Determine all the possible socket arrangements one move away from input |
|
2210 |
#' |
|
2211 |
#' @param node_sockets A tibble containing the (at most 4) rows describing the |
|
2212 |
#' sockets related to one given node. |
|
2213 |
#' |
|
2214 |
#' @keywords internal |
|
2215 |
#' @noRd |
|
2216 | ||
2217 |
sankey_stress_socket_moves <- function(node_sockets) { |
|
2218 | 27x |
if (nrow(node_sockets) == 1) { |
2219 | 9x |
return(list()) |
2220 |
} |
|
2221 | 18x |
moves <- list() |
2222 | 18x |
moves_i <- 1 |
2223 | 18x |
ref <- node_sockets |
2224 | 18x |
cclock_move <- c("top" = "left", "left" = "bottom", "bottom" = "right", |
2225 | 18x |
"right" = "top") |
2226 | 18x |
clock_move <- c("bottom" = "left", "left" = "top", "top" = "right", |
2227 | 18x |
"right" = "bottom") |
2228 | 18x |
for (side in unique(ref$node_side)) { |
2229 | 33x |
side_sockets <- ref[ref$node_side == side, ] |
2230 | 33x |
side_sockets <- side_sockets[order(side_sockets$rel_quadrant_angle), ] |
2231 | 33x |
if (nrow(side_sockets) > 1) { |
2232 | 18x |
if (side_sockets$rel_quadrant_angle[1] <= pi/4) { |
2233 |
# Clockwise move |
|
2234 | 12x |
new_side <- clock_move[side] |
2235 | 12x |
if (!any(side_sockets$node_side == new_side)) { |
2236 | 12x |
new_angle <- pi/2 - 0.01 |
2237 |
} else { |
|
2238 | ! |
new_angle <- pi/2 - (pi/2 - max(side_sockets$rel_quadrant_angle[side_sockets$node_side == new_side])) / 2 |
2239 |
} |
|
2240 |
# Update |
|
2241 | 12x |
side_sockets$node_side[1] <- new_side |
2242 | 12x |
side_sockets$rel_quadrant_angle[1] <- new_angle |
2243 | 12x |
z <- ref |
2244 | 12x |
z[z$node_side == side, ] <- side_sockets |
2245 | 12x |
moves[[moves_i]] <- z |
2246 | 12x |
moves_i <- moves_i + 1 |
2247 |
} |
|
2248 | 18x |
side_sockets <- ref[ref$node_side == side, ] |
2249 | 18x |
side_sockets <- side_sockets[order(side_sockets$rel_quadrant_angle), ] |
2250 | 18x |
if (side_sockets$rel_quadrant_angle[nrow(side_sockets)] > pi/4) { |
2251 |
# Counter-clockwise move |
|
2252 | 12x |
new_side <- cclock_move[side] |
2253 | 12x |
if (!any(side_sockets$node_side == new_side)) { |
2254 | 12x |
new_angle <- 0.01 |
2255 |
} else { |
|
2256 | ! |
new_angle <- min(side_sockets$rel_quadrant_angle[side_sockets$node_side == new_side]) / 2 |
2257 |
} |
|
2258 |
# Update |
|
2259 | 12x |
side_sockets$node_side[nrow(side_sockets)] <- new_side |
2260 | 12x |
side_sockets$rel_quadrant_angle[nrow(side_sockets)] <- new_angle |
2261 | 12x |
z <- ref |
2262 | 12x |
z[z$node_side == side, ] <- side_sockets |
2263 | 12x |
moves[[moves_i]] <- z |
2264 | 12x |
moves_i <- moves_i + 1 |
2265 |
} |
|
2266 |
} |
|
2267 |
} |
|
2268 | 18x |
return(moves) |
2269 |
} |
|
2270 | ||
2271 |
### * sankey_get_elements_lims() |
|
2272 | ||
2273 |
#' Return the xlim and ylim to contain all the graphical elements |
|
2274 |
#' |
|
2275 |
#' @param scene Scene list. |
|
2276 |
#' |
|
2277 |
#' @return A list with "xlim" and "ylim" elements. |
|
2278 |
#' |
|
2279 |
#' @keywords internal |
|
2280 |
#' @noRd |
|
2281 | ||
2282 |
sankey_get_elements_lims <- function(scene) { |
|
2283 | 8x |
nodes <- scene[["nodes"]] |
2284 | 8x |
nodes_min_x <- min(nodes$x - as.numeric(nodes$width)/2) |
2285 | 8x |
nodes_max_x <- max(nodes$x + as.numeric(nodes$width)/2) |
2286 | 8x |
nodes_min_y <- min(nodes$y - as.numeric(nodes$height)/2) |
2287 | 8x |
nodes_max_y <- max(nodes$y + as.numeric(nodes$height)/2) |
2288 | 8x |
edges <- scene[["edges"]] |
2289 | 8x |
edges$ribbon <- lapply(seq_len(nrow(edges)), function(i) { |
2290 | 67x |
ribbonFromTrajectory(edges[["backbone"]][[i]], |
2291 | 67x |
width = edges[["width"]][i]) |
2292 |
}) |
|
2293 | 8x |
ribbons <- do.call(rbind, edges$ribbon) |
2294 | 8x |
edges_min_x <- min(ribbons[, 1]) |
2295 | 8x |
edges_max_x <- max(ribbons[, 1]) |
2296 | 8x |
edges_min_y <- min(ribbons[, 2]) |
2297 | 8x |
edges_max_y <- max(ribbons[, 2]) |
2298 | 8x |
xlim <- c(min(nodes_min_x, edges_min_x), |
2299 | 8x |
max(nodes_max_x, edges_max_x)) |
2300 | 8x |
ylim <- c(min(nodes_min_y, edges_min_y), |
2301 | 8x |
max(nodes_max_y, edges_max_y)) |
2302 | 8x |
return(list(xlim = xlim, ylim = ylim)) |
2303 |
} |
1 |
### * TODO |
|
2 | ||
3 |
# Clean-up this file |
|
4 | ||
5 |
### * None of the functions in this file is exported |
|
6 | ||
7 |
### * plot_nm() |
|
8 | ||
9 |
#' Plot a network model object |
|
10 |
#' |
|
11 |
#' Reminder: A network model object is basically a tibble. Each row is one |
|
12 |
#' experimental replicate (also called "group"). The tibble might have the |
|
13 |
#' columns "observations", "prediction" and "trajectory". Those columns contain |
|
14 |
#' the data that can be plotted. |
|
15 |
#' |
|
16 |
#' @param x A network model object. Alternatively, it can also be the output |
|
17 |
#' from \code{split_to_unit_plot()} that the user has filtered themselves. |
|
18 |
#' @param facet_row,facet_column Optional, either can be "group" or |
|
19 |
#' "compartment". Define how facetting is performed. |
|
20 |
#' @param scale Define how y-scaling is performed within each panel (or |
|
21 |
#' cell). Can be one of "auto" (scaling on a per-panel basis), "all" |
|
22 |
#' (scaling shared by all panels) and "row" (scaling per row). Note that |
|
23 |
#' the x-scaling is always done for "all" (i.e. shared across all panels). |
|
24 |
#' @param type Either "prop", "size" or "both" |
|
25 |
#' @param newpage If FALSE, add the plot to the current page. |
|
26 |
#' @param xlab,ylab Labels for x and y axes. |
|
27 |
#' @param margins Figure margins. |
|
28 |
#' @param grid Boolean, draw a grid? |
|
29 |
#' @param ylab.size Y-axis label for size data. |
|
30 |
#' @param ylab.prop Y-axis label for proportion data. |
|
31 |
#' @param legend Boolean, draw legend? |
|
32 |
#' @param log Boolean, apply log transform to y axis? |
|
33 |
#' @param .colComps Colors for compartments. |
|
34 |
#' @param comps Optional, vector of compartment names giving the order in which |
|
35 |
#' they should be shown in the plot. |
|
36 |
#' @param keep Optional, vector of names of compartments to keep. |
|
37 |
#' @param drop Optional, vector of names of compartments to drop. |
|
38 |
#' |
|
39 |
#' @importFrom grDevices dev.hold |
|
40 |
#' @importFrom grDevices dev.flush |
|
41 |
#' |
|
42 |
#' @return NULL |
|
43 |
#' |
|
44 |
#' @keywords internal |
|
45 |
#' @noRd |
|
46 | ||
47 |
plot_nm <- function(x, facet_row = NULL, facet_column = NULL, scale = "auto", |
|
48 |
type = "both", newpage = TRUE, xlab = NULL, ylab = NULL, |
|
49 |
margins = c(0.5, 0.5, 1, 1), grid = TRUE, |
|
50 |
ylab.size = NULL, ylab.prop = NULL, legend = TRUE, |
|
51 |
log = FALSE, .colComps = NULL, comps, keep, drop) { |
|
52 | 90x |
if (!"group" %in% colnames(x)) { |
53 | 17x |
x[["group"]] <- rep(list(NULL), nrow(x)) |
54 |
} |
|
55 |
# Default geometries for each type of data to plot |
|
56 | 90x |
geometries <- c("observations" = "points", |
57 | 90x |
"initial" = "points", |
58 | 90x |
"prediction" = "envelope", |
59 | 90x |
"trajectory" = "line") |
60 | 90x |
colors <- c("#66C2A5", "#FC8D62", "#8DA0CB", "#E78AC3", "#A6D854", "#FFD92F", |
61 | 90x |
"#E5C494", "#B3B3B3") |
62 |
# Colors are taken from RColorBrewer::brewer.pal(8, "Set2") |
|
63 | 90x |
facet_variables <- c("group", "compartment", "type") |
64 | 90x |
nature <- type |
65 | 30x |
if (nature == "both") nature <- c("size", "prop") |
66 | 88x |
if (length(facet_row) == 0) facet_row <- NULL |
67 | 84x |
if (length(facet_column) == 0) facet_column <- NULL |
68 | 90x |
stopifnot(is.null(facet_row) | all(facet_row %in% facet_variables)) |
69 | 90x |
stopifnot(is.null(facet_column) | all(facet_column %in% facet_variables)) |
70 | 90x |
stopifnot(all(nature %in% c("prop", "size", "both"))) |
71 |
# Prepare the data |
|
72 | 90x |
if (!"ready_for_unit_plot" %in% class(x)) { |
73 | 90x |
if (log) { |
74 | 3x |
transform <- log10 |
75 |
} else { |
|
76 | 87x |
transform <- NULL |
77 |
} |
|
78 | 90x |
z <- split_to_unit_plot(x, transform) |
79 |
} else { |
|
80 | ! |
z <- x |
81 |
} |
|
82 |
# Determine what to draw |
|
83 | 90x |
if (!missing(comps)) { |
84 | ! |
z <- z[z[["compartment"]] %in% comps, ] |
85 |
} |
|
86 | 90x |
if (!missing(keep)) { |
87 | ! |
z <- z[z[["compartment"]] %in% keep, ] |
88 |
} |
|
89 | 90x |
if (!missing(drop)) { |
90 | ! |
z <- z[!z[["compartment"]] %in% drop, ] |
91 |
} |
|
92 | 90x |
if ("observations" %in% colnames(x) && !is.null(x$observations[[1]])) { |
93 | 66x |
draw_obs <- TRUE |
94 |
} else { |
|
95 | 24x |
draw_obs <- FALSE |
96 |
} |
|
97 | 90x |
if ("prediction" %in% colnames(x) && !is.null(x$prediction[[1]])) { |
98 | 39x |
draw_pred <- TRUE |
99 |
} else { |
|
100 | 51x |
draw_pred <- FALSE |
101 |
} |
|
102 |
# Process the data |
|
103 | 90x |
if (!draw_pred) { |
104 | 51x |
z <- dplyr::filter(z, type != "prediction") |
105 |
} |
|
106 | 90x |
if (!draw_obs) { |
107 | 24x |
z <- dplyr::filter(z, type != "observations") |
108 |
} |
|
109 | 90x |
if (nrow(z) == 0) { |
110 | ! |
stop("Nothing to plot") |
111 |
} |
|
112 | 90x |
z <- z[z$nature %in% nature, ] |
113 | 90x |
z$geometry <- geometries[z$type] |
114 | 90x |
z$groupLabel <- purrr::map_chr(z$group, function(x) { |
115 | 1044x |
paste0(x, collapse = ", ") |
116 |
}) |
|
117 | 90x |
groupLabels <- sort(unique(z$groupLabel)) |
118 | 90x |
compLabels <- sort(unique(z$compartment)) |
119 | 90x |
if (!missing(comps)) { |
120 | ! |
compLabels <- compLabels[order(match(compLabels, comps))] |
121 |
} |
|
122 | 90x |
colComps <- rep(colors, length(compLabels)) |
123 | 90x |
colComps <- colComps[1:length(compLabels)] |
124 | 90x |
colComps <- setNames(colComps, nm = compLabels) |
125 | 90x |
if (!is.null(.colComps)) { |
126 | 60x |
.colComps <- sort(.colComps) |
127 | 60x |
.colComps <- .colComps[names(sort(colComps))] |
128 | 60x |
stopifnot(all(.colComps == sort(colComps))) |
129 |
} |
|
130 | 30x |
if (newpage) grid::grid.newpage() |
131 | 90x |
dev.hold() |
132 | 90x |
on.exit(dev.flush()) |
133 | 90x |
if (legend) { |
134 | 30x |
labels <- c("compartment", names(x)) |
135 | 30x |
longest <- labels[which.max(nchar(labels))] |
136 | 30x |
longest <- paste0(longest, "aaaaaaaa", collapse = " ") |
137 | 30x |
lo <- grid::grid.layout(ncol = 2, |
138 | 30x |
widths = grid::unit(c(1, 1), c("null", "strwidth"), list(NULL, longest))) |
139 | 30x |
vp_top_with_legend <- grid::viewport(layout = lo) |
140 | 30x |
grid::pushViewport(vp_top_with_legend) |
141 | 30x |
vp_legend <- grid::viewport(layout.pos.col = 2) |
142 | 30x |
grid::pushViewport(vp_legend) |
143 | 30x |
draw_legend(colComps) |
144 | 30x |
grid::upViewport(1) |
145 | 30x |
vp_plot <- grid::viewport(layout.pos.col = 1) |
146 | 30x |
grid::pushViewport(vp_plot) |
147 |
} |
|
148 |
# Plot both size and proportion |
|
149 | 90x |
if (type == "both") { |
150 |
# Plot size and prop |
|
151 |
# Facetting for type |
|
152 | 30x |
facet_type <- "column" |
153 | 30x |
if ("type" %in% facet_row) { |
154 | 2x |
facet_row <- facet_row[facet_row != "type"] |
155 | 2x |
facet_type <- "row" |
156 |
} |
|
157 | 30x |
if ("type" %in% facet_column) { |
158 | ! |
facet_column <- facet_column[facet_column != "type"] |
159 | ! |
facet_type <- "column" |
160 |
} |
|
161 |
# Top vp |
|
162 | 30x |
if (facet_type == "column") { |
163 | 28x |
top_vp <- grid::viewport(layout = grid::grid.layout(ncol = 2)) |
164 |
} else { |
|
165 | 2x |
top_vp <- grid::viewport(layout = grid::grid.layout(nrow = 2)) |
166 |
} |
|
167 | 30x |
grid::pushViewport(top_vp) |
168 | 30x |
if (facet_type == "column") { |
169 | 28x |
size_vp <- grid::viewport(layout.pos.col = 1) |
170 |
} else { |
|
171 | 2x |
size_vp <- grid::viewport(layout.pos.row = 1) |
172 |
} |
|
173 | 30x |
grid::pushViewport(size_vp) |
174 | 30x |
plot_nm(x = x, facet_row = facet_row, facet_column = facet_column, |
175 | 30x |
scale = scale, type = "size", newpage = FALSE, |
176 | 30x |
xlab = xlab, ylab = ylab.size, margins = margins, grid = grid, |
177 | 30x |
.colComps = colComps, legend = FALSE, log = log, |
178 | 30x |
comps = comps, keep = keep, drop = drop) |
179 | 30x |
grid::upViewport(1) |
180 | 30x |
if (facet_type == "column") { |
181 | 28x |
prop_vp <- grid::viewport(layout.pos.col = 2) |
182 |
} else { |
|
183 | 2x |
prop_vp <- grid::viewport(layout.pos.row = 2) |
184 |
} |
|
185 | 30x |
grid::pushViewport(prop_vp) |
186 | 30x |
plot_nm(x = x, facet_row = facet_row, facet_column = facet_column, |
187 | 30x |
scale = scale, type = "prop", newpage = FALSE, |
188 | 30x |
xlab = xlab, ylab = ylab.prop, margins = margins, grid = grid, |
189 | 30x |
.colComps = colComps, legend = FALSE, log = log, |
190 | 30x |
comps = comps, keep = keep, drop = drop) |
191 | 30x |
grid::upViewport(1) |
192 | 30x |
grid::upViewport(1) |
193 | 30x |
if (legend) { |
194 | 30x |
grid::upViewport(2) |
195 |
} |
|
196 | 30x |
return(invisible(NULL)) |
197 |
} |
|
198 | 60x |
stopifnot(scale %in% c("auto", "all", "row")) |
199 | 60x |
if (is.null(xlab)) xlab <- "Time" |
200 | 60x |
if (is.null(ylab)) { |
201 | 60x |
ylab <- ifelse(nature == "size", "Size", "Proportion") |
202 |
} |
|
203 |
# Facetting |
|
204 | 60x |
if (is.null(facet_row)) { |
205 | 60x |
z$row_id <- 1 |
206 |
} else { |
|
207 | ! |
if (facet_row == "group") { z$row_id <- match(z$groupLabel, groupLabels) } |
208 | ! |
if (facet_row == "compartment") { z$row_id <- match(z$compartment, compLabels) } |
209 |
} |
|
210 | 60x |
if (is.null(facet_column)) { |
211 | 56x |
z$col_id <- 1 |
212 |
} else { |
|
213 | ! |
if (facet_column == "group") { z$col_id <- match(z$groupLabel, groupLabels) } |
214 | 4x |
if (facet_column == "compartment") { z$col_id <- match(z$compartment, compLabels) } |
215 |
} |
|
216 |
# Functions to get the scales |
|
217 | 60x |
xscaling <- function(z) { |
218 | 60x |
x <- unlist(lapply(z$data, function(x) x[["time"]])) |
219 | 60x |
return(extendrange(range(x, na.rm = TRUE))) |
220 |
} |
|
221 | 60x |
yscaling <- function(z) { |
222 | 188x |
x <- unlist(lapply(z$data, function(x) { |
223 | 1566x |
d <- colnames(x)[startsWith(colnames(x), "quantity")] |
224 | 1566x |
unlist(x[, d]) |
225 |
})) |
|
226 | 188x |
range_x <- range(x, na.rm = TRUE) |
227 | 188x |
if (range_x[1] == range_x[2]) { |
228 | ! |
delta <- 0.01 * range_x[1] |
229 | ! |
range_x <- range_x + c(-delta, delta) |
230 |
} |
|
231 | 188x |
return(extendrange(range_x)) |
232 |
} |
|
233 | 60x |
xscale <- xscaling(z) |
234 | 60x |
yscale_all <- yscaling(z) |
235 |
# Plot layout |
|
236 | 60x |
grid::pushViewport(grid::viewport(x = grid::unit(margins[2], "lines"), |
237 | 60x |
y = grid::unit(margins[1], "lines"), |
238 | 60x |
width = grid::unit(1, "npc") - grid::unit(sum(margins[c(2, 4)]), "lines"), |
239 | 60x |
height = grid::unit(1, "npc") - grid::unit(sum(margins[c(1, 3)]), "lines"), |
240 | 60x |
just =c("left", "bottom"))) |
241 |
# Top container |
|
242 | 60x |
vp_top <- grid::viewport(layout = grid::grid.layout(nrow = 2, ncol = 2, |
243 | 60x |
width = grid::unit(c(1.3, 1), c("lines", "null")), |
244 | 60x |
height = grid::unit(c(1, 1.3), c("null", "lines")))) |
245 | 60x |
grid::pushViewport(vp_top) |
246 | 60x |
vp_xlab <- grid::viewport(layout.pos.row = 2, layout.pos.col = 2) |
247 | 60x |
grid::pushViewport(vp_xlab) |
248 | 60x |
grid::grid.text(xlab, gp = grid::gpar(cex = 1)) |
249 | 60x |
grid::upViewport(1) |
250 | 60x |
vp_ylab <- grid::viewport(layout.pos.row = 1, layout.pos.col = 1) |
251 | 60x |
grid::pushViewport(vp_ylab) |
252 | 60x |
grid::grid.text(ylab, gp = grid::gpar(cex = 1), rot = 90) |
253 | 60x |
grid::upViewport(1) |
254 |
## Widths |
|
255 | 60x |
if (scale %in% c("all", "row")) { |
256 |
# One y-axis per row |
|
257 | 2x |
widths <- rep((grid::unit(1, "npc") - grid::unit(3, "lines"))*(1/max(z$col_id)), max(z$col_id)) |
258 | 2x |
widths[1] <- widths[1] + grid::unit(3, "lines") |
259 |
} else { |
|
260 |
# One y-axis per panel |
|
261 | 58x |
widths <- rep(grid::unit(1, "npc")*(1/max(z$col_id)), max(z$col_id)) |
262 |
} |
|
263 |
## Heights |
|
264 | 60x |
heights <- rep((grid::unit(1, "npc") - grid::unit(3, "lines"))*(1/max(z$row_id)), max(z$row_id)) |
265 | 60x |
heights[length(heights)] <- heights[length(heights)] + grid::unit(3, "lines") |
266 | 60x |
layout <- grid::grid.layout(nrow = max(z$row_id), ncol = max(z$col_id), |
267 | 60x |
width = widths, height = heights) |
268 | 60x |
vp_master <- grid::viewport(layout.pos.row = 1, layout.pos.col = 2, |
269 | 60x |
layout = layout) |
270 | 60x |
grid::pushViewport(vp_master) |
271 |
# Draw each panel |
|
272 | 60x |
for (i in 1:max(z$row_id)) { |
273 | 60x |
z_row <- z[z$row_id == i, ] |
274 | 60x |
yscale_row <- yscaling(z_row) |
275 | 60x |
for (j in 1:max(z$col_id)) { |
276 | 68x |
z_ij <- z[z$row_id == i & z$col_id == j, ] |
277 | 68x |
margins <- rep(0, 4) |
278 | 68x |
if (i == max(z$row_id)) margins[1] <- 3 |
279 | 64x |
if (j == 1 | scale == "auto") margins[2] <- 3 |
280 | 68x |
layout <- grid::grid.layout(nrow = 3, ncol = 2, |
281 | 68x |
widths = grid::unit(c(margins[2], 1), |
282 | 68x |
units = c("lines", "null")), |
283 | 68x |
heights = grid::unit(c(1.5, 1, margins[1]), |
284 | 68x |
units = c("lines", "null", "lines"))) |
285 | 68x |
vp_panel <- grid::viewport(layout.pos.row = i, |
286 | 68x |
layout.pos.col = j, |
287 | 68x |
layout = layout) |
288 | 68x |
grid::pushViewport(vp_panel) |
289 |
# Title |
|
290 | 68x |
vp_title <- grid::viewport(layout.pos.row = 1, |
291 | 68x |
layout.pos.col = 2) |
292 | 68x |
grid::pushViewport(vp_title) |
293 | 68x |
grid::grid.rect(gp = grid::gpar(fill = grey(0.95))) |
294 | 68x |
label <- sapply(seq_len(nrow(z_ij)), function(i) { |
295 | 522x |
if (z_ij$groupLabel[i] != "") { |
296 | 72x |
paste(z_ij$groupLabel[i]) |
297 |
} else { |
|
298 | 450x |
z_ij$compartment[i] |
299 |
} |
|
300 |
}) |
|
301 | 68x |
label <- paste(unique(label), collapse = " + ") |
302 | 68x |
grid::grid.text(label) |
303 | 68x |
grid::upViewport(1) |
304 |
# Plot |
|
305 | 68x |
vp_plot <- grid::viewport(layout.pos.row = 2, |
306 | 68x |
layout.pos.col = 2) |
307 | 68x |
grid::pushViewport(vp_plot) |
308 | 68x |
yscale <- yscaling(z_ij) |
309 | 6x |
if (scale == "all") yscale <- yscale_all |
310 | ! |
if (scale == "row") yscale <- yscale_row |
311 | 68x |
draw_nm_panel(z_ij, colors = colComps[z_ij$compartment], |
312 | 68x |
xscale = xscale, yscale = yscale, |
313 | 68x |
x.axis = i == max(z$row_id), |
314 | 68x |
y.axis = j == 1 | scale == "auto", |
315 | 68x |
grid = grid, log = log) |
316 | 68x |
grid::grid.rect(gp = grid::gpar(fill = NA)) |
317 | 68x |
grid::upViewport(1) |
318 |
# Out vp_panel |
|
319 | 68x |
grid::upViewport(1) |
320 |
} |
|
321 |
} |
|
322 |
# Up viewports |
|
323 | 60x |
grid::upViewport(1) |
324 | 60x |
grid::upViewport(1) |
325 | 60x |
grid::upViewport(1) |
326 | 60x |
if (legend) { |
327 | ! |
grid::upViewport(2) |
328 |
} |
|
329 |
# Return |
|
330 | 60x |
return(invisible(NULL)) |
331 |
} |
|
332 | ||
333 |
### * split_to_unit_plot() |
|
334 | ||
335 |
#' Separate network model object into plot chunks |
|
336 |
#' |
|
337 |
#' Function to split enriched network model object (i.e. network model that can |
|
338 |
#' have a trajectory or a prediction columns) into unit plot data. |
|
339 |
#' |
|
340 |
#' @param x A network model object, with obs/traj/prediction data. |
|
341 |
#' @param transform Optional, function to apply to all "quantities" columns |
|
342 |
#' (e.g. log). |
|
343 |
#' |
|
344 |
#' @return A tibble which can be e.g. filtered and used for plotting. |
|
345 |
#' |
|
346 |
#' @keywords internal |
|
347 |
#' @noRd |
|
348 | ||
349 |
split_to_unit_plot <- function(x, transform = NULL) { |
|
350 | 90x |
`!!` <- rlang::`!!` |
351 |
# Convert trajectory column to a light, tidy version |
|
352 | 90x |
if ("trajectory" %in% colnames(x)) { |
353 | 84x |
x$trajectory <- lapply(x$trajectory, function(z) { |
354 | 84x |
stopifnot(nrow(z) == 1) |
355 | 84x |
sizes <- tibble::as_tibble(as.data.frame(z$sizes)) |
356 | 84x |
sizes$time <- z$timepoints[[1]] |
357 | 84x |
sizes <- tidyr::pivot_longer(sizes, cols = -"time", |
358 | 84x |
names_to = "compartment", values_to = "size") |
359 | 84x |
props <- tibble::as_tibble(as.data.frame(z$proportions)) |
360 | 84x |
props$time <- z$timepoints[[1]] |
361 | 84x |
props <- tidyr::pivot_longer(props, cols = -"time", |
362 | 84x |
names_to = "compartment", values_to = "prop") |
363 | 84x |
out <- dplyr::left_join(sizes, props, by = c("time", "compartment")) |
364 | 84x |
return(out) |
365 |
}) |
|
366 |
} |
|
367 |
# For now implemented for a network model with prediction column |
|
368 | 90x |
if (!"prediction" %in% colnames(x)) { |
369 | 51x |
x$prediction <- rep(list(NULL), nrow(x)) |
370 |
} |
|
371 | 90x |
target_cols <- c("observations", "prediction", "trajectory") |
372 | 90x |
target_cols <- target_cols[target_cols %in% colnames(x)] |
373 | 90x |
if (length(target_cols) < 1) { |
374 | ! |
stop("x should have at least one of \"observations\", \"prediction\", or \"trajectory\" columns.") |
375 |
} |
|
376 | 90x |
y <- x[, c("initial", "group", target_cols)] |
377 | 90x |
y <- tidyr::pivot_longer(y, cols = target_cols, |
378 | 90x |
names_to = "type", |
379 | 90x |
values_to = "data") |
380 |
# Drop rows with NULL data |
|
381 | 90x |
y <- y[!sapply(y[["data"]], is.null), ] |
382 |
# Split by compartment |
|
383 | 90x |
z <- y |
384 | 90x |
z[["data"]] <- lapply(z[["data"]], function(x) { |
385 | 201x |
tidyr::nest(dplyr::group_by(x, `!!`(rlang::sym("compartment")))) |
386 |
}) |
|
387 | 90x |
z$row_id <- 1:nrow(z) |
388 | 90x |
for (i in seq_len(nrow(z))) { |
389 | 201x |
z$data[[i]]$row_id <- i |
390 | 201x |
z$data[[i]]$data <- as.list(z$data[[i]]$data) |
391 |
} |
|
392 | 90x |
y <- dplyr::select(z, - "data") |
393 | 90x |
y <- dplyr::full_join(y, dplyr::bind_rows(z$data), by = "row_id") |
394 | 90x |
y <- dplyr::select(y, - "row_id") |
395 |
# Separate size and proportion data |
|
396 | 90x |
y$size <- as.list(rep(NA, nrow(y))) |
397 | 90x |
y$prop <- as.list(rep(NA, nrow(y))) |
398 | 90x |
for (i in seq_len(nrow(y))) { |
399 | 531x |
d <- y$data[[i]] |
400 | 531x |
ds <- dplyr::select(d, tidyselect::starts_with("size"), "time") |
401 | 531x |
n <- names(ds) |
402 | 531x |
n <- gsub("size", "quantity", n) |
403 | 531x |
names(ds) <- n |
404 | 531x |
dp <- dplyr::select(d, tidyselect::starts_with("prop"), "time") |
405 | 531x |
n <- names(dp) |
406 | 531x |
n <- gsub("proportion", "quantity", n) |
407 | 531x |
n <- gsub("prop", "quantity", n) |
408 | 531x |
names(dp) <- n |
409 | 531x |
y$size[[i]] <- na.omit(ds) |
410 | 531x |
y$prop[[i]] <- na.omit(dp) |
411 |
} |
|
412 | 90x |
y <- dplyr::select(y, - "data") |
413 | 90x |
z <- tidyr::pivot_longer(y, cols = c("size", "prop"), |
414 | 90x |
names_to = "nature", |
415 | 90x |
values_to = "data") |
416 | 90x |
z <- dplyr::select(z, "group", "compartment", "nature", "type", "data", "initial") |
417 |
# Separate initial values |
|
418 | 90x |
init <- dplyr::select(z, "group", "compartment", "initial") |
419 | 90x |
init$size <- rep(list(NA), nrow(init)) |
420 | 90x |
init$prop <- rep(list(NA), nrow(init)) |
421 | 90x |
for (i in seq_len(nrow(init))) { |
422 | 1062x |
comp_i <- init$compartment[i] |
423 | 1062x |
init$initial[[i]] <- init$initial[[i]][init$initial[[i]][["compartment"]] == comp_i, ] |
424 | 1062x |
stopifnot(nrow(init$initial[[i]]) == 1) |
425 | 1062x |
init$size[[i]] <- tibble::tibble(quantity = init$initial[[i]]$size[1], |
426 | 1062x |
time = 0) |
427 | 1062x |
init$prop[[i]] <- tibble::tibble(quantity = init$initial[[i]]$proportion[1], |
428 | 1062x |
time = 0) |
429 |
} |
|
430 | 90x |
init$initial <- NULL |
431 | 90x |
init$type <- "initial" |
432 | 90x |
init <- tidyr::pivot_longer(init, cols = c("size", "prop"), |
433 | 90x |
names_to = "nature", |
434 | 90x |
values_to = "data") |
435 | 90x |
init <- unique(init) |
436 |
# Merge |
|
437 | 90x |
z$initial <- NULL |
438 | 90x |
z <- dplyr::bind_rows(z, init) |
439 |
# Apply transform |
|
440 | 90x |
if (!is.null(transform)) { |
441 | 3x |
for (i in seq_len(nrow(z))) { |
442 | 72x |
d <- z$data[[i]] |
443 | 72x |
cols <- colnames(d)[grepl("quantity", colnames(d))] |
444 | 72x |
for (col in cols) { |
445 | 108x |
d[[col]] <- transform(d[[col]]) |
446 |
} |
|
447 | 72x |
z$data[[i]] <- d |
448 |
} |
|
449 |
} |
|
450 |
# Return |
|
451 | 90x |
return(structure(z, class = c("ready_for_unit_plot", class(z)))) |
452 |
} |
|
453 | ||
454 |
### * draw_nm_panel() |
|
455 | ||
456 |
#' Draw a panel of the data (obs/traj/pred) from a network model |
|
457 |
#' |
|
458 |
#' @param x Output from \code{split_to_unit_plot}. |
|
459 |
#' @param colors Passed to \code{draw_nm_layer}. |
|
460 |
#' @param xscale,yscale X and y scales. |
|
461 |
#' @param x.axis,y.axis Booleans, draw the axes? |
|
462 |
#' @param grid Boolean, draw a grid? |
|
463 |
#' @param log Boolean, log-transform the y axis? |
|
464 |
#' |
|
465 |
#' @keywords internal |
|
466 |
#' @noRd |
|
467 | ||
468 |
draw_nm_panel <- function(x, colors = "grey", xscale = NULL, yscale = NULL, |
|
469 |
x.axis = FALSE, y.axis = FALSE, grid = FALSE, |
|
470 |
log = FALSE) { |
|
471 |
# Get lims |
|
472 | 68x |
x_range <- range(unlist(lapply(x$data, function(i) i[["time"]])), |
473 | 68x |
na.rm = TRUE) |
474 | 68x |
y_range <- range(unlist(lapply(x$data, function(i) { |
475 | 522x |
unlist(i[, grepl("quantity", colnames(i))]) |
476 | 68x |
})), na.rm = TRUE) |
477 |
# Create viewport |
|
478 | ! |
if (is.null(xscale)) xscale <- x_range |
479 | ! |
if (is.null(yscale)) yscale <- y_range |
480 | 68x |
vp <- grid::dataViewport(xscale = xscale, |
481 | 68x |
yscale = yscale) |
482 | 68x |
grid::pushViewport(vp) |
483 |
# Draw grid |
|
484 | 68x |
xat <- pretty(xscale) |
485 | 68x |
xat <- xat[xat >= xscale[1] & xat <= xscale[2]] |
486 | 68x |
yat <- pretty(yscale) |
487 | 68x |
if (log) { |
488 | 6x |
yat <- log10(prettyLog(yscale)) |
489 |
} |
|
490 | 68x |
yat <- yat[yat >= yscale[1] & yat <= yscale[2]] |
491 | 68x |
ylabel <- yat |
492 | 68x |
if (log) { |
493 | 6x |
ylabel <- prettyLogLabels(yscale) |
494 | 6x |
ylabel <- ylabel[log10(prettyLog(yscale)) >= yscale[1] & log10(prettyLog(yscale)) <= yscale[2]] |
495 |
} |
|
496 | 68x |
if (grid) { |
497 | 68x |
grid.col <- grey(0.95) |
498 | 68x |
for (xi in xat) { |
499 | 408x |
grid::grid.lines(x = grid::unit(c(xi, xi), "native"), |
500 | 408x |
y = grid::unit(yscale, "native"), |
501 | 408x |
gp =grid::gpar(col = grid.col)) |
502 |
} |
|
503 | 68x |
for (yi in yat) { |
504 | 412x |
grid::grid.lines(x = grid::unit(xscale, "native"), |
505 | 412x |
y = grid::unit(c(yi, yi), "native"), |
506 | 412x |
gp =grid::gpar(col = grid.col)) |
507 |
} |
|
508 |
} |
|
509 |
# Draw layers |
|
510 | 68x |
colors <- rep(colors, nrow(x)) |
511 | 68x |
for (i in seq_len(nrow(x))) { |
512 | 522x |
draw_nm_layer(x[i, ], color = colors[i]) |
513 |
} |
|
514 | 68x |
if (x.axis) grid::grid.xaxis() |
515 | 64x |
if (y.axis) grid::grid.yaxis(at = yat, |
516 | 64x |
label = ylabel) |
517 |
# Up viewports |
|
518 | 68x |
grid::upViewport(1) |
519 |
} |
|
520 | ||
521 |
### * draw_nm_layer() |
|
522 | ||
523 |
#' @importFrom grDevices adjustcolor |
|
524 |
#' |
|
525 |
#' @keywords internal |
|
526 |
#' @noRd |
|
527 | ||
528 |
draw_nm_layer <- function(x, color = "grey") { |
|
529 | 522x |
stopifnot(nrow(x) == 1) |
530 | 522x |
geometry <- x$geometry[[1]] |
531 | 522x |
stopifnot(geometry %in% c("points", "envelope", "line")) |
532 | 522x |
if (geometry == "points") { |
533 | 296x |
if (nrow(x$data[[1]]) > 0) { |
534 | 296x |
grid::grid.points(x = x$data[[1]]$time, |
535 | 296x |
y = x$data[[1]]$quantity, |
536 | 296x |
default.unit = "native", |
537 | 296x |
pch = 21, |
538 | 296x |
gp = grid::gpar(col = "black", |
539 | 296x |
fill = color)) |
540 |
} |
|
541 |
} |
|
542 | 522x |
if (geometry == "envelope") { |
543 | 82x |
if (nrow(x$data[[1]]) > 0) { |
544 | 82x |
grid::grid.polygon(x = c(x$data[[1]]$time, rev(x$data[[1]]$time)), |
545 | 82x |
y = c(x$data[[1]]$quantity_low, rev(x$data[[1]]$quantity_high)), |
546 | 82x |
default.unit = "native", |
547 | 82x |
gp = grid::gpar(col = color, |
548 | 82x |
fill = adjustcolor(color, alpha.f = 0.25))) |
549 |
} |
|
550 |
} |
|
551 | 522x |
if (geometry == "line") { |
552 | 144x |
if (nrow(x$data[[1]]) > 0) { |
553 | 144x |
grid::grid.lines(x = x$data[[1]]$time, |
554 | 144x |
y = x$data[[1]]$quantity, |
555 | 144x |
default.unit = "native", |
556 | 144x |
gp = grid::gpar(col = color)) |
557 |
} |
|
558 |
} |
|
559 |
} |
|
560 | ||
561 |
### * draw_legend() |
|
562 | ||
563 |
#' Draw a legend for plot_nm() |
|
564 |
#' |
|
565 |
#' @importFrom grDevices adjustcolor |
|
566 |
#' |
|
567 |
#' @param x A named vector of colors |
|
568 |
#' |
|
569 |
#' @keywords internal |
|
570 |
#' @noRd |
|
571 | ||
572 |
draw_legend <- function(x) { |
|
573 | 30x |
lo <- grid::grid.layout(nrow = 2, |
574 | 30x |
heights = grid::unit(c(2, 1.5 * length(x)), |
575 | 30x |
c("lines", "lines"))) |
576 | 30x |
vp_legend <- grid::viewport(layout = lo) |
577 | 30x |
grid::pushViewport(vp_legend) |
578 |
# Title |
|
579 | 30x |
vp_title <- grid::viewport(layout.pos.row = 1) |
580 | 30x |
grid::pushViewport(vp_title) |
581 | 30x |
grid::grid.text("Compartments", x = grid::unit(2, "strwidth", list("a")), |
582 | 30x |
just = "left", |
583 | 30x |
gp = grid::gpar(fontface = 2)) |
584 | 30x |
grid::upViewport(1) |
585 |
# Label stack |
|
586 | 30x |
lo <- grid::grid.layout(nrow = length(x), |
587 | 30x |
heights = grid::unit(rep(1.5, length(x)), |
588 | 30x |
rep("lines", length(x)))) |
589 | 30x |
vp_label_stack_top <- grid::viewport(layout.pos.row = 2) |
590 | 30x |
grid::pushViewport(vp_label_stack_top) |
591 | 30x |
vp_label_stack <- grid::viewport(x = grid::unit(3, "strwidth", list("a")), |
592 | 30x |
just = "left", |
593 | 30x |
layout = lo) |
594 | 30x |
grid::pushViewport(vp_label_stack) |
595 |
# Individual labels |
|
596 | 30x |
for (i in seq_along(x)) { |
597 |
# Colored square + label |
|
598 | 78x |
lo <- grid::grid.layout(ncol = 2, |
599 | 78x |
widths = grid::unit(c(0.9, 1), c("lines", "null"))) |
600 | 78x |
vp_compartment <- grid::viewport(layout.pos.row = i, |
601 | 78x |
layout = lo) |
602 | 78x |
grid::pushViewport(vp_compartment) |
603 | 78x |
vp_square_top <- grid::viewport(layout.pos.col = 1) |
604 | 78x |
grid::pushViewport(vp_square_top) |
605 | 78x |
vp_square <- grid::viewport(width = grid::unit(1.1, "lines"), |
606 | 78x |
height = grid::unit(1.1, "lines")) |
607 | 78x |
grid::pushViewport(vp_square) |
608 | 78x |
grid::grid.rect(gp = grid::gpar(col = x[i], |
609 | 78x |
fill = adjustcolor(x[i], alpha.f = 0.5))) |
610 | 78x |
grid::upViewport(2) |
611 | 78x |
vp_label <- grid::viewport(layout.pos.col = 2) |
612 | 78x |
grid::pushViewport(vp_label) |
613 | 78x |
grid::grid.text(names(x)[i], x = grid::unit(2, "strwidth", list("a")), |
614 | 78x |
just = "left") |
615 | 78x |
grid::upViewport(2) |
616 |
} |
|
617 | 30x |
grid::upViewport(3) |
618 |
} |
|
619 | ||
620 |
### * prettyLog |
|
621 | ||
622 |
#' Find pretty ticks for log scale |
|
623 |
#' |
|
624 |
#' @param range Range of the axis, in log10-transformed scale |
|
625 |
#' |
|
626 |
#' @return A vector containing the tick values in natural scale |
|
627 |
#' |
|
628 |
#' @keywords internal |
|
629 |
#' @noRd |
|
630 | ||
631 |
prettyLog <- function(range) { |
|
632 | 24x |
x <- range |
633 | 24x |
min_x <- 10^min(x) |
634 | 24x |
max_x <- 10^max(x) |
635 | 24x |
z <- list(c(1), c(1, 5), c(1, 2, 5)) |
636 | 24x |
o <- lapply(seq_along(z), function(j) { |
637 | 72x |
k <- c() |
638 | 72x |
for (i in -10:10) { |
639 | 1512x |
k <- c(k, z[[j]] * 10^i) |
640 |
} |
|
641 | 72x |
k <- k[k >= min_x & k <= max_x] |
642 | 72x |
return(k) |
643 |
}) |
|
644 | 24x |
if (length(o[[1]]) >= 3) { |
645 | 24x |
return(o[[1]]) |
646 | ! |
} else if (length(o[[2]]) >= 3) { |
647 | ! |
return(o[[2]]) |
648 |
} else { |
|
649 | ! |
return(o[[3]]) |
650 |
} |
|
651 |
} |
|
652 | ||
653 |
### * prettyLogLabels |
|
654 | ||
655 |
#' Same as prettyLog but return expression for labels |
|
656 |
#' |
|
657 |
#' TODO For now the function just returns its input. |
|
658 |
#' |
|
659 |
#' @param range Range of the axis, in log10-transformed scale |
|
660 |
#' |
|
661 |
#' @return A vector containing the tick values in natural scale |
|
662 |
#' |
|
663 |
#' @keywords internal |
|
664 |
#' @noRd |
|
665 | ||
666 |
prettyLogLabels <- function(range) { |
|
667 | 6x |
ticks <- prettyLog(range) |
668 | 6x |
labels <- ticks |
669 | 6x |
return(labels) |
670 |
} |
|
671 | ||
672 |
### * plot_traces() |
|
673 | ||
674 |
#' Plot nice traces from a mcmc list |
|
675 |
#' |
|
676 |
#' @param x A \code{coda::mcmc.list} object |
|
677 |
#' @param variables Optional, a vector specifying the ordered variables to plot |
|
678 |
#' @param regexp If TRUE (the default), strings in \code{variables} are used to |
|
679 |
#' select traces to draw by pattern matching with the variables names. |
|
680 |
#' @param loglik Boolean, if \code{TRUE} additionally plot the loglik trace. |
|
681 |
#' @param ratio Aspect ratio for the overall plot that the function thrives to |
|
682 |
#' respect (this will have an effect on the balance between numbers of rows |
|
683 |
#' and colums) |
|
684 |
#' @param transform Function to use to transform the values before plotting |
|
685 |
#' (e.g. log). The function is applied directly to \code{x}, so it should |
|
686 |
#' be able to handle \code{mcmc.list} object (and return a similar object). |
|
687 |
#' @param lty Line type for the density plots |
|
688 |
#' @param hist Boolean, if TRUE draw histograms instead of density profiles |
|
689 |
#' @param nbins Number of histogram bins, used only if \code{drawHist} is TRUE. |
|
690 |
#' @param pdf Filename to save the plot as a pdf. If \code{NULL}, the plot is |
|
691 |
#' displayed. |
|
692 |
#' @param png Filename to save the plot as a png. If \code{NULL}, the plot is |
|
693 |
#' displayed. |
|
694 |
#' @param width,height Width and height of the pdf file in inches. Also used |
|
695 |
#' for the size of the png file, using a RES value of 150. |
|
696 |
#' |
|
697 |
#' @importFrom grDevices dev.hold |
|
698 |
#' @importFrom grDevices dev.flush |
|
699 |
#' @importFrom grDevices dev.off |
|
700 |
#' |
|
701 |
#' @keywords internal |
|
702 |
#' @noRd |
|
703 | ||
704 |
plot_traces <- function(x, variables = NULL, regexp = TRUE, loglik = FALSE, |
|
705 |
ratio = 4/3, transform = NULL, lty = 1, |
|
706 |
hist = TRUE, nbins = 32, |
|
707 |
pdf = NULL, png = NULL, width = 12, height = 10) { |
|
708 | 10x |
x_original <- x |
709 | 10x |
if (is.null(transform)) { |
710 | 10x |
x <- x |
711 |
} else { |
|
712 | ! |
x <- transform(x) |
713 |
} |
|
714 | 10x |
drawHist <- hist |
715 | 10x |
nBars <- nbins |
716 |
# Parse pdf/png arguments |
|
717 | 10x |
stopifnot(is.null(pdf) | is.null(png)) |
718 | 10x |
if (!is.null(pdf)) { |
719 | ! |
pdf(file = pdf, width = width, height = height) |
720 |
} |
|
721 | 10x |
if (!is.null(png)) { |
722 | ! |
RES = 150 |
723 | ! |
png(file = png, width = width * RES, height = height * RES, res = RES, |
724 | ! |
type = "cairo-png") |
725 |
} |
|
726 | 10x |
dev.hold() |
727 | 10x |
on.exit(dev.flush()) |
728 |
# Prepare data |
|
729 | 10x |
if (loglik) { |
730 | ! |
prev_varnames <- coda::varnames(x) |
731 |
# Add loglik trace to each mcmc object |
|
732 | ! |
mcmcList <- lapply(seq_along(x), function(i) { |
733 | ! |
y <- cbind(x[[i]], attr(x, "loglik")[[i]]) |
734 | ! |
colnames(y) <- c(prev_varnames, "loglik") |
735 | ! |
chainI <- coda::mcmc(y) |
736 | ! |
return(chainI) |
737 |
}) |
|
738 | ! |
x <- coda::as.mcmc.list(mcmcList) |
739 | ! |
for (i in seq_along(x)) { |
740 | ! |
attr(x[[i]], "mcpar") <- coda::mcpar(x_original) |
741 |
} |
|
742 |
} |
|
743 | 10x |
if (regexp & !is.null(variables)) { |
744 | ! |
vars <- coda::varnames(x) |
745 | ! |
selectedVars <- vars[unlist(sapply(variables, grep, x = vars))] |
746 | ! |
selectedVars <- unique(selectedVars) |
747 | ! |
if (length(selectedVars) == 0) { |
748 | ! |
stop("No variables found matching: ", variables) |
749 |
} |
|
750 | ! |
variables <- selectedVars |
751 |
} |
|
752 | 10x |
drawTraces(mcmc.list = x, variables = variables, |
753 | 10x |
ratio = ratio, lty = lty, |
754 | 10x |
drawHist = drawHist, nBars = nBars) |
755 | 10x |
if (!is.null(pdf) | !is.null(png)) { |
756 | ! |
invisible(dev.off()) |
757 |
} |
|
758 | 10x |
return(invisible(x_original)) |
759 |
} |
|
760 | ||
761 |
### * drawTraces |
|
762 | ||
763 |
#' Draw the traces for a mcmc.list object |
|
764 |
#' |
|
765 |
#' @param mcmc.list A coda mcmc.list object |
|
766 |
#' @param drawXAxis Boolean |
|
767 |
#' @param variables A vector containing the names of the variables to include |
|
768 |
#' in the plot. If NULL, all variables are used. |
|
769 |
#' @param ratio Aspect ratio for the overall plot that the function thrives to |
|
770 |
#' respect (this will have an effect on the balance between numbers of rows |
|
771 |
#' and colums) |
|
772 |
#' @param drawHist Boolean, if TRUE draw histograms instead of density profiles |
|
773 |
#' @param nBars Number of histogram bars, used only if \code{drawHist} is TRUE. |
|
774 |
#' @param newpage Boolean, run grid.newpage() before drawing? |
|
775 |
#' @param ... Arguments passed to \code{\link{drawTraceOneVar}} |
|
776 |
#' |
|
777 |
#' @keywords internal |
|
778 |
#' @noRd |
|
779 | ||
780 |
drawTraces = function(mcmc.list, drawXAxis = FALSE, variables = NULL, |
|
781 |
ratio = 4/3, drawHist = FALSE, nBars = 64, |
|
782 |
newpage = TRUE, ...) { |
|
783 | 10x |
nchains = coda::nchain(mcmc.list) |
784 | 10x |
nvars = coda::nvar(mcmc.list) |
785 | 10x |
varsToPlot = 1:nvars |
786 | 10x |
if (!is.null(variables)) { |
787 | ! |
nvars = length(variables) |
788 | ! |
varsToPlot = variables |
789 |
} |
|
790 | 10x |
colors = c("deeppink", "green3", "darkmagenta", "dodgerblue") |
791 | 10x |
colors = rep(colors, nvars) |
792 | 10x |
mfrow = n2mfrowByRatio(nvars, ratio = ratio) |
793 |
# New page |
|
794 | 10x |
if (newpage) grid::grid.newpage() |
795 |
# Viewport for traces |
|
796 | 10x |
if (drawXAxis) { |
797 | ! |
vpTraces = grid::viewport(layout = grid::grid.layout(nrow = mfrow[1] + 1, ncol = mfrow[2], |
798 | ! |
heights = grid::unit(c(rep(1, mfrow[1]), 4), c(rep("null", mfrow[1]), "lines"))), |
799 | ! |
name = "vpTraces", clip = "on") |
800 |
} else { |
|
801 | 10x |
vpTraces = grid::viewport(layout = grid::grid.layout(nrow = mfrow[1], ncol = mfrow[2]), |
802 | 10x |
name = "vpTraces", clip = "on") |
803 |
} |
|
804 | 10x |
grid::pushViewport(vpTraces) |
805 |
# Draw panel for each parameter |
|
806 | 10x |
lapply(seq_len(nvars), function(v) { |
807 | 68x |
row = 1 + (v-1) %/% mfrow[2] |
808 | 68x |
column = 1 + (v-1) %% mfrow[2] |
809 | 68x |
vpCell = grid::viewport(layout.pos.row = row, |
810 | 68x |
layout.pos.col = column, |
811 | 68x |
name = paste("vpCell", row, column, sep = ".")) |
812 | 68x |
grid::pushViewport(vpCell) |
813 | 68x |
grid::grid.rect(gp = grid::gpar(col = grey(0.5))) |
814 | 68x |
if (drawHist) { |
815 | 60x |
drawTraceOneVarHist(mcmc.list = mcmc.list, varname = varsToPlot[v], colors = colors, |
816 | 60x |
drawXAxis = ifelse(drawXAxis, |
817 | 60x |
(row == mfrow[1] | (row == (mfrow[1] - 1) & column == mfrow[2])), |
818 | 60x |
FALSE), nBars = nBars, ...) |
819 |
} else { |
|
820 | 8x |
drawTraceOneVar(mcmc.list = mcmc.list, varname = varsToPlot[v], colors = colors, |
821 | 8x |
drawXAxis = ifelse(drawXAxis, |
822 | 8x |
(row == mfrow[1] | (row == (mfrow[1] - 1) & column == mfrow[2])), |
823 | 8x |
FALSE), ...) |
824 |
} |
|
825 | 68x |
grid::upViewport(1) |
826 |
}) |
|
827 |
# Back to parent viewport |
|
828 | 10x |
grid::upViewport(1) |
829 |
} |
|
830 | ||
831 |
### * drawTraceOneVar |
|
832 | ||
833 |
#' Draw the chain traces for one variable, using a density profile for the density panel |
|
834 |
#' |
|
835 |
#' This function is mostly useful when it is called by other functions as a |
|
836 |
#' buildign block to draw the traces of several variables. |
|
837 |
#' |
|
838 |
#' @param mcmc.list A coda mcmc.list object |
|
839 |
#' @param varname Name of the variable of which the trace is to be drawn |
|
840 |
#' @param colors Vector of colors for the chain traces |
|
841 |
#' @param drawXAxis Boolean |
|
842 |
#' @param lty Line type for the density plot |
|
843 |
#' |
|
844 |
#' @importFrom stats density |
|
845 |
#' @importFrom grDevices grey |
|
846 |
#' @importFrom grDevices extendrange |
|
847 |
#' @importFrom grDevices adjustcolor |
|
848 |
#' |
|
849 |
#' @keywords internal |
|
850 |
#' @noRd |
|
851 | ||
852 |
drawTraceOneVar = function(mcmc.list, varname, colors, drawXAxis = FALSE, lty = 1) { |
|
853 | 8x |
EXTENSION = c(0.025, 0.1) |
854 | 8x |
ALPHA_FILL = 0.2 |
855 | 8x |
ALPHA_TRACES = 0.6 |
856 | 8x |
DENSITY_MAX_RELATIVE_SHIFT = 0 # Can be set to e.g. 0.25 for slight shift in density plots |
857 | 8x |
MODE_COL = "white" |
858 | 8x |
CI_LWD = c(0.25, 0.15) # Unit: char |
859 |
# Process the data |
|
860 | 8x |
varnameOriginal <- varname |
861 | 8x |
if (is.numeric(varname)) { |
862 | 8x |
varname = coda::varnames(mcmc.list)[varname] |
863 | 8x |
if (is.null(varname)) { |
864 | ! |
varname <- "unnamed_variable" |
865 |
} |
|
866 |
} |
|
867 | 8x |
mcpars = coda::mcpar(mcmc.list[[1]]) |
868 | 8x |
nchains = coda::nchain(mcmc.list) |
869 | 8x |
if (!is.null(dim(mcmc.list[[1]]))) { |
870 | 8x |
varData = lapply(mcmc.list, function(mcmc) mcmc[, varnameOriginal]) |
871 |
} else { |
|
872 |
# There is only one variable |
|
873 | ! |
varData = lapply(mcmc.list, function(mcmc) as.vector(mcmc)) |
874 |
} |
|
875 | 8x |
xData = seq(from = mcpars[1], to = mcpars[2], by = mcpars[3]) |
876 | 8x |
yData = unlist(varData) |
877 | 8x |
densities = lapply(varData, density) |
878 | 8x |
densitiesData = unlist(lapply(densities, "[[", "y")) |
879 | 8x |
densitiesRange = range(densitiesData) |
880 | 8x |
if (nchains > 1) { |
881 | 8x |
stepDensity = diff(densitiesRange) * DENSITY_MAX_RELATIVE_SHIFT / (nchains - 1) |
882 |
} else { |
|
883 | ! |
stepDensity = 0 |
884 |
} |
|
885 | 8x |
densitiesRange[2] = densitiesRange[2] + stepDensity * (nchains - 1) + diff(densitiesRange) * 0.1 |
886 |
# Viewport for the parameter panel |
|
887 | 8x |
vpParamPanel = grid::viewport(layout = grid::grid.layout(nrow = 2, ncol = 1, |
888 | 8x |
heights = grid::unit(c(1.5, 1), c("lines", "null"))), |
889 | 8x |
name = paste("vpParamPanel", varname, sep = ".")) |
890 | 8x |
grid::pushViewport(vpParamPanel) |
891 |
# Viewport for the title |
|
892 | 8x |
vpTitle = grid::viewport(name = "vpTitle", layout.pos.row = 1) |
893 | 8x |
grid::pushViewport(vpTitle) |
894 | 8x |
grid::grid.rect(gp = grid::gpar(fill = grey(0.95), col = grey(0.5))) |
895 | 8x |
grid::grid.text(varnameToExp(varname), |
896 | 8x |
gp = grid::gpar(col = grey(0.5), cex = 1.2)) |
897 | 8x |
grid::upViewport(1) |
898 |
# Viewport for the graph |
|
899 | 8x |
vpGraph = grid::viewport(layout = grid::grid.layout(nrow = 1, ncol = 3, |
900 | 8x |
widths = grid::unit(c(3, 1, 0.25), c("lines", "null", "null"))), |
901 | 8x |
name = "vpGraph", layout.pos.row = 2, clip = ifelse(drawXAxis, "off", "on")) |
902 | 8x |
grid::pushViewport(vpGraph) |
903 |
# Viewport for the y axis |
|
904 | 8x |
vpYAxis = grid::viewport(name = "vpYAxis", layout.pos.col = 1) |
905 | 8x |
grid::pushViewport(vpYAxis) |
906 | 8x |
grid::upViewport(1) |
907 |
# Viewport for the trace plot |
|
908 | 8x |
vpTracePlot = grid::dataViewport(xData = xData, yData = yData, extension = EXTENSION, |
909 | 8x |
name = "vpTracePlot", layout.pos.col = 2) |
910 | 8x |
grid::pushViewport(vpTracePlot) |
911 |
## Draw the chains |
|
912 | 8x |
lapply(seq_len(nchains), function(i) { |
913 | 16x |
grid::grid.lines(x = xData, y = varData[[i]], default.units = "native", |
914 | 16x |
gp = grid::gpar(col = adjustcolor(colors[i], alpha.f = ALPHA_TRACES), |
915 | 16x |
lwd = 1)) |
916 |
}) |
|
917 |
## Draw axes |
|
918 | 8x |
grid::grid.yaxis(gp = grid::gpar(cex = 0.7)) |
919 | 8x |
if (drawXAxis) { |
920 |
# https://stackoverflow.com/questions/8816456/rotate-labels-in-grid-xaxis |
|
921 | ! |
grid::grid.xaxis(edits = grid::gEdit(gPath="labels", rot=90)) |
922 |
} |
|
923 | 8x |
grid::upViewport(1) |
924 |
# Viewport for the density plot |
|
925 | 8x |
vpDensityPlot = grid::dataViewport(xscale = extendrange(densitiesRange, f = 0.025), |
926 | 8x |
yData = yData, extension = EXTENSION, |
927 | 8x |
name = "vpDensityPlot", layout.pos.col = 3, |
928 | 8x |
clip = "on") |
929 | 8x |
grid::pushViewport(vpDensityPlot) |
930 | 8x |
lapply(seq_len(nchains), function(i) { |
931 | 16x |
grid::grid.polygon(x = c(densities[[i]]$y, densities[[i]]$y[1]) + stepDensity * (i - 1), |
932 | 16x |
y = c(densities[[i]]$x, densities[[i]]$x[1]), |
933 | 16x |
default.unit = "native", |
934 | 16x |
gp = grid::gpar(col = colors[i], |
935 | 16x |
fill = adjustcolor(colors[i], alpha.f = ALPHA_FILL), |
936 | 16x |
lty = lty)) |
937 |
}) |
|
938 | 8x |
grid::upViewport(1) |
939 |
# Back to vpParamPanel |