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 | 12x |
sockets$rel_loc[i] <- nodes$left_accumulator[ni] - sockets$width[i]/2 |
1482 | 12x |
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 | 18x |
sockets$rel_loc[i] <- nodes$right_accumulator[ni] + sockets$width[i]/2 |
1490 | 18x |
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 | 51x |
if (nrow(z) == 0) return(0) |
1628 | 57x |
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 | 7x |
nodes$width[[i]] <- grid::unit(alpha * as.numeric(nodes$width[[i]]), "native") |
1670 | 7x |
nodes$height[[i]] <- grid::unit(beta * as.numeric(nodes$height[[i]]), "native") |
1671 |
} else { |
|
1672 | 2x |
nodes$width[[i]] <- grid::unit(beta * as.numeric(nodes$width[[i]]), "native") |
1673 | 2x |
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 | 8x |
shift <- top_a - bottom_b + min_spacer |
1742 | 8x |
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 | 2x |
extra <- highest - ylim[2] |
1789 | 2x |
total_spacing <- highest - ni$y[1] - as.numeric(ni$height[1]) / 2 |
1790 | 2x |
if (total_spacing > extra) { |
1791 |
# The extra height can be accommodated in the spacing |
|
1792 | 2x |
spacing_adj_factor <- 1 - extra / total_spacing |
1793 |
# Apply this factor on the nodes from bottom to top |
|
1794 | 2x |
for (i in 2:nrow(ni)) { |
1795 | 10x |
current_spacing <- (ni$y[i] - as.numeric(ni$height[i]) / 2 - |
1796 | 10x |
ni$y[i-1] + as.numeric(ni$height[i-1]) / 2) |
1797 | 10x |
shift <- current_spacing * (1 - spacing_adj_factor) |
1798 | 10x |
for (j in i:nrow(ni)) { |
1799 | 30x |
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 | 12x |
shift <- nodes$left_accumulator[node_i] / 2 |
1854 | 54x |
} else if (side == "top") { |
1855 | 18x |
shift <- nodes$top_accumulator[node_i] / 2 |
1856 | 36x |
} else if (side == "right") { |
1857 | 18x |
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 | 46x |
sockets$normal[[i]] <- c(-1, 0) |
1932 | 46x |
sockets$center_x[i] <- sockets$center_x[i] - nw / 2 |
1933 |
} else { |
|
1934 | 52x |
sockets$normal[[i]] <- c(1, 0) |
1935 | 52x |
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 |
### * None of the functions in this file is exported |
|
2 | ||
3 |
### * describe_z_eta() |
|
4 | ||
5 |
#' Print a message describing the role of eta or zeta in a distribution family |
|
6 |
#' |
|
7 |
#' @param param_name Name of the parameter (e.g. "eta" or "zeta"). |
|
8 |
#' @param family Family string. |
|
9 |
#' @param prefix,suffix Strings appended to the message. |
|
10 |
#' |
|
11 |
#' @keywords internal |
|
12 |
#' @noRd |
|
13 | ||
14 |
describe_z_eta <- function(param_name, family, prefix = " (", suffix = ")") { |
|
15 | ! |
msg <- list( |
16 | ! |
"gamma_cv" = " is the coefficient of variation of gamma distributions.", |
17 | ! |
"normal_cv" = " is the coefficient of variation of normal distributions.", |
18 | ! |
"normal_sd" = " is the standard deviation of normal distributions.", |
19 | ! |
"beta_phi" = " is the precision (phi) of beta distributions.") |
20 | ! |
if (!family %in% names(msg)) { |
21 | ! |
stop("The provided value for the family argument is not allowed.") |
22 |
} |
|
23 | ! |
message(prefix, param_name, msg[[family]], suffix, sep = "") |
24 |
} |
|
25 | ||
26 |
### * valid_prior_tbl() |
|
27 | ||
28 |
#' Test if the input is a valid prior tibble |
|
29 |
#' |
|
30 |
#' @param x Some input to test. |
|
31 |
#' |
|
32 |
#' @return Boolean. |
|
33 |
#' |
|
34 |
#' @examples |
|
35 |
#' valid_prior_tbl(priors(aquarium_mod)) |
|
36 |
#' valid_prior_tbl(mtcars) |
|
37 |
#' |
|
38 |
#' @keywords internal |
|
39 |
#' @noRd |
|
40 | ||
41 |
valid_prior_tbl <- function(x) { |
|
42 |
# Is tibble? |
|
43 | ! |
if (!is(x, "tbl_df")) return(FALSE) |
44 |
# Has at least columns "in_model" and "prior"? |
|
45 | ! |
if (!all(c("in_model", "prior") %in% colnames(x))) return(FALSE) |
46 |
# "in_model" is a character vector? |
|
47 | ! |
if (!is(x[["in_model"]], "character")) return(FALSE) |
48 |
# "prior" is a list? |
|
49 | ! |
if (!is(x[["prior"]], "list")) return(FALSE) |
50 |
# Are all the entries in the "prior" column NULL or priors? |
|
51 | ! |
non_null <- x[["prior"]][!is.null(x[["prior"]])] |
52 | ! |
if (!all(sapply(non_null, is, "prior"))) return(FALSE) |
53 |
# End of tests |
|
54 | ! |
return(TRUE) |
55 |
} |
1 |
### * All functions in this file are exported |
|
2 | ||
3 |
### * new_networkModel() |
|
4 | ||
5 |
#' Create an empty network model |
|
6 |
#' |
|
7 |
#' The first step in building a network model is to create a new, empty |
|
8 |
#' \code{networkModel} object. This model can then be completed using functions |
|
9 |
#' such as \code{set_topo()}, \code{set_init()}, etc... |
|
10 |
#' |
|
11 |
#' @param quiet Boolean, if \code{FALSE} print a message indicating which |
|
12 |
#' distribution family is used for proportions. |
|
13 |
#' |
|
14 |
#' @return An empty \code{networkModel} object. It is basically a zero-row |
|
15 |
#' tibble with the appropriate columns. |
|
16 |
#' |
|
17 |
#' @examples |
|
18 |
#' m <- new_networkModel() |
|
19 |
#' m |
|
20 |
#' class(m) |
|
21 |
#' |
|
22 |
#' @export |
|
23 | ||
24 |
new_networkModel <- function(quiet = FALSE) { |
|
25 | 38x |
verbose <- !quiet |
26 | 38x |
x <- tibble::tibble(topology = list(), |
27 | 38x |
initial = list(), |
28 | 38x |
observations = list()) |
29 | 38x |
attr(x, "prop_family") <- "gamma_cv" |
30 | 38x |
if (verbose) { |
31 | ! |
message("Using default distribution family for proportions (\"gamma_cv\").") |
32 | ! |
describe_z_eta("eta", attr(x, "prop_family")) |
33 |
} |
|
34 | 38x |
attr(x, "size_family") <- "normal_cv" |
35 | 38x |
if (verbose) { |
36 | ! |
message("Using default distribution family for sizes (\"normal_cv\").") |
37 | ! |
describe_z_eta("zeta", attr(x, "size_family")) |
38 |
} |
|
39 | 38x |
attr(x, "size_zeta_per_compartment") <- FALSE |
40 | 38x |
x <- structure(x, class = c("networkModel", class(x))) |
41 | 38x |
return(x) |
42 |
} |
|
43 | ||
44 |
### * set_prop_family() |
|
45 | ||
46 |
#' Set the distribution family for observed proportions |
|
47 |
#' |
|
48 |
#' @param nm A \code{networkModel} object (output from |
|
49 |
#' \code{\link{new_networkModel}}). |
|
50 |
#' @param family Allowed values are "gamma_cv", "beta_phi", "normal_cv", and |
|
51 |
#' "normal_sd". |
|
52 |
#' @param quiet Boolean, if \code{FALSE} print a message indicating which |
|
53 |
#' distribution family is used for proportions. |
|
54 |
#' |
|
55 |
#' @return A \code{networkModel} object. |
|
56 |
#' |
|
57 |
#' @examples |
|
58 |
#' library(magrittr) |
|
59 |
#' |
|
60 |
#' m <- new_networkModel() %>% |
|
61 |
#' set_topo(links = "NH4, NO3 -> epi -> pseph, tricor") |
|
62 |
#' m <- m %>% set_prop_family("beta_phi") |
|
63 |
#' m |
|
64 |
#' attr(m, "prop_family") |
|
65 |
#' |
|
66 |
#' @export |
|
67 | ||
68 |
set_prop_family <- function(nm, family, quiet = FALSE) { |
|
69 | 5x |
verbose <- !quiet |
70 | 5x |
known_families <- c("gamma_cv" = 1, "normal_cv" = 2, "normal_sd" = 3, |
71 | 5x |
"beta_phi" = 4) |
72 | 5x |
if (!family %in% names(known_families)) { |
73 | ! |
stop("Unknown distribution family for proportions. Got value: \"", |
74 | ! |
family, "\"\n", |
75 | ! |
"Allowed values are: ", paste0(sapply(names(known_families), function(x) paste0("\"", x, "\"")), |
76 | ! |
collapse = ", ")) |
77 |
} |
|
78 | 5x |
attr(nm, "prop_family") <- family |
79 | 5x |
if (verbose) { |
80 | ! |
message("Using distribution family for proportions: \"", family, "\".", |
81 | ! |
sep = "") |
82 | ! |
describe_z_eta("eta", family) |
83 |
} |
|
84 | 5x |
return(nm) |
85 |
} |
|
86 | ||
87 |
### * set_size_family() |
|
88 | ||
89 |
#' Set the distribution family for observed sizes |
|
90 |
#' |
|
91 |
#' @param nm A \code{networkModel} object (output from |
|
92 |
#' \code{\link{new_networkModel}}). |
|
93 |
#' @param family Allowed values are "normal_cv" and "normal_sd". |
|
94 |
#' @param by_compartment Boolean, if TRUE then zeta is compartment-specific. |
|
95 |
#' @param quiet Boolean, if \code{FALSE} print a message indicating which |
|
96 |
#' distribution family is used for proportions. |
|
97 |
#' @param quiet_reset Boolean, write a message when model parameters (and |
|
98 |
#' covariates and priors) are reset? |
|
99 |
#' |
|
100 |
#' @return A \code{networkModel} object. |
|
101 |
#' |
|
102 |
#' @examples |
|
103 |
#' library(magrittr) |
|
104 |
#' |
|
105 |
#' m <- new_networkModel() %>% |
|
106 |
#' set_topo(links = "NH4, NO3 -> epi -> pseph, tricor") |
|
107 |
#' m <- m %>% set_size_family("normal_sd") |
|
108 |
#' m |
|
109 |
#' attr(m, "size_family") |
|
110 |
#' |
|
111 |
#' m <- m %>% set_size_family(by_compartment = TRUE) |
|
112 |
#' attr(m, "size_zeta_per_compartment") |
|
113 |
#' |
|
114 |
#' @export |
|
115 | ||
116 |
set_size_family <- function(nm, family, by_compartment, quiet = FALSE, |
|
117 |
quiet_reset = FALSE) { |
|
118 | ! |
verbose <- !quiet |
119 | ! |
verbose_reset <- !quiet_reset |
120 | ! |
if (!missing(family)) { |
121 | ! |
known_families <- c("normal_cv" = 1, "normal_sd" = 2) |
122 | ! |
if (!family %in% names(known_families)) { |
123 | ! |
stop("Unknown distribution family for proportions. Got value: \"", |
124 | ! |
family, "\"\n", |
125 | ! |
"Allowed values are: ", paste0(sapply(names(known_families), function(x) paste0("\"", x, "\"")), |
126 | ! |
collapse = ", ")) |
127 |
} |
|
128 | ! |
attr(nm, "size_family") <- family |
129 | ! |
if (verbose) { |
130 | ! |
message("Using distribution family for sizes: \"", family, "\".", |
131 | ! |
sep = "") |
132 | ! |
describe_z_eta("zeta", family) |
133 |
} |
|
134 |
} |
|
135 | ! |
if (!missing(by_compartment)) { |
136 | ! |
stopifnot(by_compartment %in% c(FALSE, TRUE)) |
137 | ! |
attr(nm, "size_zeta_per_compartment") <- by_compartment |
138 | ! |
if(verbose) { |
139 | ! |
message("Compartment-specific zeta set to ", by_compartment, ".", |
140 | ! |
sep = "") |
141 |
} |
|
142 | ! |
if (!is.null(nm[["parameters"]])) { |
143 |
# nm already had parameters |
|
144 |
# Reset to update the zeta parameters |
|
145 | ! |
nm <- add_param_mapping(nm) |
146 | ! |
if (verbose_reset) { |
147 | ! |
message("Parameters priors and covariates were reset because `by_compartment` was specified.") |
148 | ! |
message("(This means that no priors are set and no covariates are used in the returned networkModel.)") |
149 |
} |
|
150 |
} |
|
151 |
} |
|
152 | ! |
return(nm) |
153 |
} |
|
154 | ||
155 |
### * set_topo() |
|
156 | ||
157 |
### ** Doc |
|
158 | ||
159 |
#' Set the topology in a network model. |
|
160 |
#' |
|
161 |
#' @param nm A \code{networkModel} object (output from |
|
162 |
#' \code{\link{new_networkModel}}). |
|
163 |
#' @param ... One or more strings describing the links defining the network |
|
164 |
#' topology. Optionally, links can be given as a data frame. See the |
|
165 |
#' examples for more details about acceptable input formats. |
|
166 |
#' @param from Optional, string containing the column name for sources if |
|
167 |
#' links are provided as a data frame. |
|
168 |
#' @param to Optional, string containing the column name for destinations if |
|
169 |
#' links are provided as a data frame. |
|
170 |
#' |
|
171 |
#' @return A \code{networkModel} object. |
|
172 |
#' |
|
173 |
#' @examples |
|
174 |
#' # A single string can describe several links in one go. |
|
175 |
#' m <- new_networkModel() %>% |
|
176 |
#' set_topo("NH4, NO3 -> epi -> pseph, tricor") |
|
177 |
#' m |
|
178 |
#' topo(m) |
|
179 |
#' |
|
180 |
#' # Several strings can be given as distinct arguments. |
|
181 |
#' m2 <- new_networkModel() %>% |
|
182 |
#' set_topo("NH4, NO3 -> epi -> pseph, tricor", |
|
183 |
#' "NH4 -> FBOM, CBOM", "CBOM <- NO3") |
|
184 |
#' m2 |
|
185 |
#' topo(m2) |
|
186 |
#' |
|
187 |
#' # Multiple strings can be also be combined into a single argument with `c()`. |
|
188 |
#' links <- c("NH4, NO3 -> epi -> pseph, tricor", "NH4 -> FBOM, CBOM", |
|
189 |
#' "CBOM <- NO3") |
|
190 |
#' m3 <- new_networkModel() %>% |
|
191 |
#' set_topo(links) |
|
192 |
#' m3 |
|
193 |
#' topo(m3) |
|
194 |
#' |
|
195 |
#' # A data frame can be used to specify the links. |
|
196 |
#' links <- data.frame(source = c("NH4", "NO3", "epi"), |
|
197 |
#' consumer = c("epi", "epi", "petro")) |
|
198 |
#' links |
|
199 |
#' m4 <- new_networkModel() %>% |
|
200 |
#' set_topo(links, from = "source", to = "consumer") |
|
201 |
#' m4 |
|
202 |
#' m4$topology[[1]] |
|
203 |
#' |
|
204 |
#' @export |
|
205 | ||
206 |
### ** Code |
|
207 | ||
208 |
set_topo <- function(nm, ..., from = NULL, to = NULL) { |
|
209 |
# Parse the ... argument |
|
210 | 38x |
args <- list(...) |
211 | 38x |
if (length(args) > 1) { |
212 | 2x |
if (!all(sapply(args, is.character))) { |
213 | 1x |
stop("Only strings can be used when providing multiple link arguments.\n", |
214 | 1x |
"(Maybe you provided both string(s) and data frame(s)?)") |
215 |
} |
|
216 | 1x |
links <- unlist(args) |
217 |
} else { |
|
218 | 36x |
links <- args[[1]] |
219 |
} |
|
220 |
# Build the topology object |
|
221 | 37x |
topology <- make_topology(links = links, from = from, to = to) |
222 |
# If the network model is empty, create a row of NULLs |
|
223 | 37x |
message_reset <- TRUE |
224 | 37x |
if (nrow(nm) == 0) { |
225 | 36x |
nm[1, 1] <- NA |
226 | 36x |
message_reset <- FALSE |
227 |
} |
|
228 |
# Assign all the cells in the "topology" column |
|
229 | 37x |
for (i in seq_len(nrow(nm))) { |
230 | 37x |
nm$topology[[i]] <- topology |
231 |
} |
|
232 |
# Add parameter mapping |
|
233 | 37x |
nm <- add_param_mapping(nm) |
234 | 37x |
if (message_reset) { |
235 | 1x |
msg <- c("`set_topo()` was called on a non-empty networkModel object.", |
236 | 1x |
"As a result, the parameter mapping and the priors of the model were reset.") |
237 | 1x |
message(paste0(msg, collapse = " ")) |
238 |
} |
|
239 | 37x |
return(nm) |
240 |
} |
|
241 | ||
242 |
### * set_steady() |
|
243 | ||
244 |
#' Flag some network compartments as being in a steady state |
|
245 |
#' |
|
246 |
#' @param nm A \code{networkModel} object. |
|
247 |
#' @param comps Vector of strings, names of the compartments to set steady. |
|
248 |
#' @param which Vector of integers giving the nm rows to update. Default is to |
|
249 |
#' update all rows. |
|
250 |
#' |
|
251 |
#' @return A \code{networkModel} object. |
|
252 |
#' |
|
253 |
#' @examples |
|
254 |
#' library(magrittr) |
|
255 |
#' x <- new_networkModel() %>% |
|
256 |
#' set_topo("NH4 -> algae -> daphnia") %>% |
|
257 |
#' set_steady("NH4") |
|
258 |
#' topo(x) |
|
259 |
#' |
|
260 |
#' @export |
|
261 | ||
262 |
set_steady <- function(nm, comps = NULL, which = NULL) { |
|
263 | 6x |
if (is.null(which)) { |
264 | 6x |
which <- seq_len(nrow(nm)) |
265 |
} |
|
266 | 6x |
stopifnot(all(which %in% seq_len(nrow(nm)))) |
267 |
# Update row by row |
|
268 | 6x |
for (i in which) { |
269 | 7x |
topo <- nm[["topology"]][[i]] |
270 | 7x |
steady <- attr(topo, "steadyState") |
271 | 7x |
for (comp in comps) { |
272 | 8x |
stopifnot(comp %in% colnames(topo)) |
273 | 8x |
steady <- c(steady, comp) |
274 |
} |
|
275 | 7x |
attr(nm[["topology"]][[i]], "steadyState") <- steady |
276 |
} |
|
277 |
# Return |
|
278 | 6x |
return(nm) |
279 |
} |
|
280 | ||
281 |
### * set_split() |
|
282 | ||
283 |
#' Flag some network compartments as being split compartments |
|
284 |
#' |
|
285 |
#' This function automatically adds a default prior (uniform on [0,1]) for the |
|
286 |
#' active portion of split compartments. |
|
287 |
#' |
|
288 |
#' @param nm A \code{networkModel} object. |
|
289 |
#' @param comps Vector of strings, the names of the compartments to set split. |
|
290 |
#' @param which Vector of integers giving the nm rows to update. Default is to |
|
291 |
#' update all rows. |
|
292 |
#' |
|
293 |
#' @return A \code{networkModel} object. |
|
294 |
#' |
|
295 |
#' @examples |
|
296 |
#' library(magrittr) |
|
297 |
#' x <- new_networkModel() %>% |
|
298 |
#' set_topo("NH4 -> algae -> daphnia") %>% |
|
299 |
#' set_split("algae") |
|
300 |
#' topo(x) |
|
301 |
#' |
|
302 |
#' @export |
|
303 | ||
304 |
set_split <- function(nm, comps = NULL, which = NULL) { |
|
305 | 6x |
if (is.null(which)) { |
306 | 6x |
which <- seq_len(nrow(nm)) |
307 |
} |
|
308 | 6x |
stopifnot(all(which %in% seq_len(nrow(nm)))) |
309 |
# Update row by row |
|
310 | 6x |
for (i in which) { |
311 | 6x |
topo <- nm[["topology"]][[i]] |
312 | 6x |
split <- attr(topo, "split") |
313 | 6x |
for (comp in comps) { |
314 | 6x |
stopifnot(comp %in% colnames(topo)) |
315 | 6x |
split <- c(split, comp) |
316 |
} |
|
317 | 6x |
attr(nm[["topology"]][[i]], "split") <- split |
318 |
# Add the corresponding parameters |
|
319 | 6x |
params <- nm[["parameters"]][[i]] |
320 | 6x |
for (comp in comps) { |
321 | 6x |
paramName <- paste0("portion.act_", comp) |
322 | 6x |
params <- tibble::add_row(params, in_replicate = paramName, |
323 | 6x |
in_model = paramName) |
324 |
} |
|
325 | 6x |
nm[["parameters"]][[i]] <- params |
326 |
} |
|
327 |
# Update priors |
|
328 | 6x |
for (comp in comps) { |
329 | 6x |
paramName <- paste0("portion.act_", comp) |
330 | 6x |
attr(nm, "priors") <- tibble::add_row(attr(nm, "priors"), |
331 | 6x |
in_model = paramName, |
332 | 6x |
prior = list(uniform_p(0, 1))) |
333 |
} |
|
334 | 6x |
return(nm) |
335 |
} |
|
336 | ||
337 |
### * set_half_life() |
|
338 | ||
339 |
#' Set the half-life for radioactive tracers |
|
340 |
#' |
|
341 |
#' Indicating a non-zero value for half-life will add a decay to the marked |
|
342 |
#' portion of the tracer element. The decay constant is calculated from the |
|
343 |
#' half-life value as: |
|
344 |
#' |
|
345 |
#' lambda_decay = log(2) / half_life |
|
346 |
#' |
|
347 |
#' Note that for correct calculations the half-life value should be given in |
|
348 |
#' the same time unit (e.g. hour, day) that the time unit used for |
|
349 |
#' observations. |
|
350 |
#' |
|
351 |
#' @param nm A \code{networkModel} object. |
|
352 |
#' @param hl Half-life value, in the same time unit as the observations are (or |
|
353 |
#' will be) given. Setting half-life to zero is equivalent to using a |
|
354 |
#' stable isotope (no decay used in the model). |
|
355 |
#' @param quiet Boolean for verbosity. |
|
356 |
#' |
|
357 |
#' @return A \code{networkModel} object. |
|
358 |
#' |
|
359 |
#' @examples |
|
360 |
#' library(magrittr) |
|
361 |
#' x <- new_networkModel() %>% |
|
362 |
#' set_topo("32P -> root -> leaf") %>% |
|
363 |
#' set_half_life(hl = 14.268) |
|
364 |
#' x |
|
365 |
#' |
|
366 |
#' @export |
|
367 | ||
368 |
set_half_life <- function(nm, hl, quiet = FALSE) { |
|
369 | ! |
verbose <- !quiet |
370 | ! |
if (hl == 0) { |
371 | ! |
if (verbose) { |
372 | ! |
message("Half-life set to zero; the model will assume stable isotopes.") |
373 |
} |
|
374 | ! |
attr(nm, "lambda_hl") <- NULL |
375 |
} else { |
|
376 | ! |
lambda_hl <- log(2) / hl |
377 | ! |
if (verbose) { |
378 | ! |
message("Half-life set to ", hl, " time units.\n", |
379 | ! |
"(equivalent to a decay rate of ", signif(lambda_hl, 3), " per time unit).") |
380 |
} |
|
381 | ! |
attr(nm, "lambda_hl") <- lambda_hl |
382 |
} |
|
383 | ! |
return(nm) |
384 |
} |
|
385 | ||
386 |
### * add_pulse_event() |
|
387 | ||
388 |
#' Register a pulse event on one of the compartment of a topology |
|
389 |
#' |
|
390 |
#' When applied to a steady-state compartment, this is equivalent to changing |
|
391 |
#' the steady state. Negative values are allowed, so one can add a "pulse" to a |
|
392 |
#' steady-state compartment and then later add a similar but negative "pulse" |
|
393 |
#' to simulate a drip in a stream for example. |
|
394 |
#' |
|
395 |
#' @param nm A \code{networkModel} object. |
|
396 |
#' @param time Numeric, time at which the pulse is happening. |
|
397 |
#' @param comp One compartment name only. |
|
398 |
#' @param unmarked Numeric, quantity of unmarked marker added. |
|
399 |
#' @param marked Numeric, quantity of marked marker added. |
|
400 |
#' @param which Vector of integers giving the nm rows to update. Default is to |
|
401 |
#' update all rows. |
|
402 |
#' @param pulses Optionally, a tibble containing the pulse information in |
|
403 |
#' columns. If provided, `comp`, `time`, `unmarked` and `marked` must be |
|
404 |
#' strings giving the corresponding column names. |
|
405 |
#' |
|
406 |
#' @return A \code{networkModel} object. |
|
407 |
#' |
|
408 |
#' @examples |
|
409 |
#' m <- trini_mod |
|
410 |
#' m$events <- NULL |
|
411 |
#' pulses <- tibble::tribble( |
|
412 |
#' ~ stream, ~ transect, ~ comp, ~ time, ~ qty_14N, ~ qty_15N, |
|
413 |
#' "UL", "transect.1", "NH4", 11, 0, -0.00569, |
|
414 |
#' "UL", "transect.2", "NH4", 11, 0, -0.00264, |
|
415 |
#' "UL", "transect.3", "NH4", 11, 0, -0.000726, |
|
416 |
#' "UL", "transect.1", "NO3", 11, 0, -0.00851, |
|
417 |
#' "UL", "transect.2", "NO3", 11, 0, -0.01118, |
|
418 |
#' "UL", "transect.3", "NO3", 11, 0, -0.01244, |
|
419 |
#' ) |
|
420 |
#' m <- add_pulse_event(m, pulses = pulses, comp = "comp", time = "time", |
|
421 |
#' unmarked = "qty_14N", marked = "qty_15N") |
|
422 |
#' m |
|
423 |
#' |
|
424 |
#' |
|
425 |
#' @export |
|
426 | ||
427 |
add_pulse_event <- function(nm, time, comp = NULL, unmarked, marked, |
|
428 |
which = NULL, pulses) { |
|
429 | 2x |
if (!missing(pulses)) { |
430 |
# Table syntax |
|
431 | ! |
if (is.null(groups(nm))) { |
432 |
# No groups |
|
433 | ! |
for (i in seq_len(nrow(pulses))) { |
434 | ! |
args <- list(nm = nm, |
435 | ! |
time = pulses[[time]][i], |
436 | ! |
comp = pulses[[comp]][i], |
437 | ! |
unmarked = pulses[[unmarked]][i], |
438 | ! |
marked = pulses[[marked]][i]) |
439 | ! |
nm <- do.call(add_pulse_event, args) |
440 |
} |
|
441 | ! |
return(nm) |
442 |
} else { |
|
443 |
# Groups |
|
444 | ! |
grps <- groups(nm) |
445 | ! |
gp_vars <- colnames(grps) |
446 | ! |
if (!all(gp_vars %in% colnames(pulses))) { |
447 | ! |
gp_vars_str <- paste0("\"", gp_vars, "\"") |
448 | ! |
stop("The `pulses` argument must have columns specifying the group variables of the network model.\n", |
449 | ! |
" (Model grouped by: ", paste(gp_vars_str, collapse = ", "), ")\n", |
450 | ! |
" (Missing column(s) in `pulses` table: ", paste(gp_vars_str[!gp_vars %in% colnames(pulses)], collapse = ", "), ")\n") |
451 |
} |
|
452 | ! |
pulses_labels <- apply(as.matrix(pulses[, gp_vars]), 1, paste, collapse = " ") |
453 | ! |
for (i in seq_len(nrow(pulses))) { |
454 | ! |
grps <- groups(nm) |
455 | ! |
grps_labels <- apply(as.matrix(grps), 1, paste, collapse = " ") |
456 | ! |
row_i <- which(grps_labels == pulses_labels[i]) |
457 | ! |
stopifnot(length(row_i) == 1) |
458 | ! |
args <- list(nm = nm, |
459 | ! |
time = pulses[[time]][i], |
460 | ! |
comp = pulses[[comp]][i], |
461 | ! |
unmarked = pulses[[unmarked]][i], |
462 | ! |
marked = pulses[[marked]][i], |
463 | ! |
which = row_i) |
464 | ! |
nm <- do.call(add_pulse_event, args) |
465 |
} |
|
466 | ! |
return(nm) |
467 |
} |
|
468 |
} |
|
469 |
# Single pulse |
|
470 | 2x |
if (is.null(which)) { |
471 | 2x |
which <- seq_len(nrow(nm)) |
472 |
} |
|
473 | 2x |
stopifnot(all(which %in% seq_len(nrow(nm)))) |
474 |
# Create an empty "events" column if needed |
|
475 | 2x |
if (!"events" %in% colnames(nm)) { |
476 | 1x |
nm[["events"]] <- rep(list(NULL), nrow(nm)) |
477 |
} |
|
478 |
# Update row by row |
|
479 | 2x |
for (i in which) { |
480 | 4x |
topo <- nm[["topology"]][[i]] |
481 | 4x |
stopifnot(comp %in% colnames(topo)) |
482 | 4x |
this_event <- tibble::tibble(event = "pulse", |
483 | 4x |
time = time, |
484 | 4x |
compartment = comp, |
485 | 4x |
characteristics = list(list(unmarked = unmarked, |
486 | 4x |
marked = marked))) |
487 | 4x |
nm[["events"]][[i]] <- dplyr::bind_rows(nm[["events"]][[i]], |
488 | 4x |
this_event) |
489 | 4x |
nm[["events"]][[i]] <- nm[["events"]][[i]][order(nm[["events"]][[i]][["time"]], |
490 | 4x |
nm[["events"]][[i]][["compartment"]]), ] |
491 |
} |
|
492 |
# Return |
|
493 | 2x |
return(nm) |
494 |
} |
|
495 | ||
496 |
### * set_init() |
|
497 | ||
498 |
#' Set initial conditions in a network model |
|
499 |
#' |
|
500 |
#' @param nm A \code{networkModel} object (e.g. output from |
|
501 |
#' \code{\link{new_networkModel}}) |
|
502 |
#' @param data A tibble containing the initial conditions |
|
503 |
#' @param comp String, name of the \code{data} column with the compartment names |
|
504 |
#' @param size String, name of the \code{data} column with the compartment sizes |
|
505 |
#' @param prop String, name of the \code{data} column with the compartment |
|
506 |
#' proportions of marked tracer |
|
507 |
#' @param group_by Optional vector of string giving the names of the columns to |
|
508 |
#' use for grouping the data into replicates |
|
509 |
#' |
|
510 |
#' @return A \code{networkModel} object. |
|
511 |
#' |
|
512 |
#' @examples |
|
513 |
#' # Using the topology from the Trinidad case study |
|
514 |
#' m <- new_networkModel() %>% |
|
515 |
#' set_topo("NH4, NO3 -> epi, FBOM", "epi -> petro, pseph", |
|
516 |
#' "FBOM -> tricor", "petro, tricor -> arg") |
|
517 |
#' |
|
518 |
#' # Taking initial condtions from the 'lalaja' dataset at t=0 |
|
519 |
#' inits <- lalaja[lalaja[["time.days"]] == 0, ] |
|
520 |
#' inits |
|
521 |
#' m <- set_init(m, inits, comp = "compartment", size = "mgN.per.m2", |
|
522 |
#' prop = "prop15N", group_by = "transect") |
|
523 |
#' m |
|
524 |
#' |
|
525 |
#' @export |
|
526 | ||
527 |
set_init <- function(nm, data, comp, size, prop, group_by = NULL) { |
|
528 |
# Trim extra compartments |
|
529 | 29x |
topo_comps <- unique(unlist(comps(nm))) |
530 | 29x |
init_comps <- unique(data[[comp]]) |
531 | 29x |
extra_comps <- init_comps[!init_comps %in% topo_comps] |
532 | 29x |
if (length(extra_comps) > 0) { |
533 | ! |
message(paste0(strwrap("Some compartments are not present in the network topology and are removed from the initial data:"), |
534 | ! |
collapse = "\n"), "\n", |
535 | ! |
" - ", paste0(extra_comps, collapse = "\n - "), "\n") |
536 | ! |
data <- data[data[[comp]] %in% topo_comps, ] |
537 |
} |
|
538 |
# Build init |
|
539 | 29x |
grouped_init <- make_init(data = data, comp = comp, |
540 | 29x |
size = size, prop = prop, |
541 | 29x |
group_by = group_by) |
542 | 29x |
out <- merge_nm_by_group(nm = nm, tib = grouped_init, |
543 | 29x |
destination = "initial", |
544 | 29x |
tib_name = "initial conditions") |
545 | 29x |
attr(out, "prop_family") <- attr(nm, "prop_family") |
546 | 29x |
attr(out, "default_columns") <- list(comp = comp, |
547 | 29x |
size = size, |
548 | 29x |
prop = prop, |
549 | 29x |
group_by = group_by) |
550 |
# Check that each compartment gets exactly one initial condition |
|
551 | 29x |
for (i in seq_len(nrow(out))) { |
552 | 47x |
z <- out$initial[[i]] |
553 | 47x |
if (nrow(z) != nrow(na.omit(z))) { |
554 | ! |
stop("Some NAs are present in the initial conditions.") |
555 |
} |
|
556 | 47x |
if (!all(table(z$compartment) == 1)) { |
557 | ! |
stop("Some compartments are present twice in the initial conditions data.") |
558 |
} |
|
559 | 47x |
if (!all(topo_comps %in% z[["compartment"]])) { |
560 | ! |
stop("Some compartments are present in the topology but do not have initial conditions.") |
561 |
} |
|
562 |
} |
|
563 | 29x |
return(out) |
564 |
} |
|
565 | ||
566 |
### * set_obs() |
|
567 | ||
568 |
#' Set observations in a network model |
|
569 |
#' |
|
570 |
#' @param nm A \code{networkModel} object (e.g. output from |
|
571 |
#' \code{\link{new_networkModel}}) |
|
572 |
#' @param data A tibble containing the observations. If NULL, remove |
|
573 |
#' observations from the model. |
|
574 |
#' @param comp String, name of the \code{data} column with the compartment |
|
575 |
#' names |
|
576 |
#' @param size String, name of the \code{data} column with the compartment |
|
577 |
#' sizes |
|
578 |
#' @param prop String, name of the \code{data} column with the compartment |
|
579 |
#' proportions of heavy tracer |
|
580 |
#' @param time String, name of the \code{data} column with the sampling times |
|
581 |
#' @param group_by Optional vector of string giving the names of the columns to |
|
582 |
#' use for grouping the data into replicates |
|
583 |
#' |
|
584 |
#' @return A \code{networkModel} object. |
|
585 |
#' |
|
586 |
#' @examples |
|
587 |
#' # Using the topology from the Trinidad case study |
|
588 |
#' m <- new_networkModel() %>% |
|
589 |
#' set_topo("NH4, NO3 -> epi, FBOM", "epi -> petro, pseph", |
|
590 |
#' "FBOM -> tricor", "petro, tricor -> arg") |
|
591 |
#' |
|
592 |
#' # Taking initial condtions from the 'lalaja' dataset at t=0 |
|
593 |
#' inits <- lalaja[lalaja[["time.days"]] == 0, ] |
|
594 |
#' inits |
|
595 |
#' m <- set_init(m, inits, comp = "compartment", size = "mgN.per.m2", |
|
596 |
#' prop = "prop15N", group_by = "transect") |
|
597 |
#' m |
|
598 |
#' |
|
599 |
#' # Taking observations from 'lalaja' |
|
600 |
#' m <- set_obs(m, lalaja[lalaja[["time.days"]] > 0, ], time = "time.days") |
|
601 |
#' m |
|
602 |
#' plot(m) |
|
603 |
#' |
|
604 |
#' @export |
|
605 | ||
606 |
set_obs <- function(nm, data, comp, size, prop, time, group_by) { |
|
607 | 25x |
if (is.null(data)) { |
608 | ! |
nm[["observations"]] <- rep(list(NULL), nrow(nm)) |
609 | ! |
return(nm) |
610 |
} |
|
611 |
# Get default columns |
|
612 | 25x |
default_columns <- attr(nm, "default_columns") |
613 | 25x |
columns_from_attr <- c() |
614 | 25x |
if (missing(comp)) { |
615 | 2x |
comp <- default_columns[["comp"]] |
616 | 2x |
if (!is.null(comp)) { |
617 | 2x |
columns_from_attr <- c(columns_from_attr, "comp") |
618 |
} |
|
619 |
} |
|
620 | 25x |
if (missing(size)) { |
621 | 2x |
size <- default_columns[["size"]] |
622 | 2x |
if (!is.null(size)) { |
623 | 2x |
columns_from_attr <- c(columns_from_attr, "size") |
624 |
} |
|
625 |
} |
|
626 | 25x |
if (missing(prop)) { |
627 | 2x |
prop <- default_columns[["prop"]] |
628 | 2x |
if (!is.null(prop)) { |
629 | 2x |
columns_from_attr <- c(columns_from_attr, "prop") |
630 |
} |
|
631 |
} |
|
632 | 25x |
if (missing(group_by)) { |
633 | 18x |
group_by <- default_columns[["group_by"]] |
634 | 18x |
if (!is.null(group_by)) { |
635 | 5x |
columns_from_attr <- c(columns_from_attr, "group_by") |
636 |
} |
|
637 |
} |
|
638 | 25x |
if (length(columns_from_attr) > 0) { |
639 | 5x |
msg <- "Using the same columns by default as the ones used with `set_init()`:\n" |
640 | 5x |
for (i in columns_from_attr) { |
641 | 11x |
msg <- c(msg, paste0(" ", i, " = \"", default_columns[[i]], "\"\n")) |
642 |
} |
|
643 | 5x |
msg <- paste0(msg, collapse = "") |
644 | 5x |
message(msg) |
645 |
} |
|
646 |
# Trim extra compartments |
|
647 | 25x |
topo_comps <- unique(unlist(comps(nm))) |
648 | 25x |
obs_comps <- unique(data[[comp]]) |
649 | 25x |
extra_comps <- obs_comps[!obs_comps %in% topo_comps] |
650 | 25x |
if (length(extra_comps) > 0) { |
651 | ! |
message(paste0(strwrap("Some compartments are not present in the network topology and are removed from the observed data:"), |
652 | ! |
collapse = "\n"), "\n", |
653 | ! |
" - ", paste0(extra_comps, collapse = "\n - "), "\n") |
654 | ! |
data <- data[data[[comp]] %in% topo_comps, ] |
655 |
} |
|
656 |
# Build observations |
|
657 | 25x |
grouped_obs <- make_obs(data = data, comp = comp, size = size, prop = prop, |
658 | 25x |
time = time, group_by = group_by) |
659 | 25x |
out <- merge_nm_by_group(nm = nm, tib = grouped_obs, |
660 | 25x |
destination = "observations", |
661 | 25x |
tib_name = "observations") |
662 | 25x |
default_columns[["time"]] <- time |
663 | 25x |
attr(out, "default_columns") <- default_columns |
664 | 25x |
attr(out, "prop_family") <- attr(nm, "prop_family") |
665 | 25x |
return(out) |
666 |
} |
|
667 | ||
668 |
### * set_params() |
|
669 | ||
670 |
#' Set the parameters in a network model |
|
671 |
#' |
|
672 |
#' @param nm A \code{networkModel} object. |
|
673 |
#' @param params A named vector or a tibble with columns c("parameter", |
|
674 |
#' "value") containing the (global) parameter values. |
|
675 |
#' @param force Boolean, if FALSE will not overwrite already set parameters. |
|
676 |
#' @param quick Boolean, if TRUE take some shortcuts for faster parameter |
|
677 |
#' settings when called by another function. This should usually be left to |
|
678 |
#' the default (FALSE) by a regular package user. |
|
679 |
#' |
|
680 |
#' @return A \code{networkModel} object. |
|
681 |
#' |
|
682 |
#' @examples |
|
683 |
#' m <- aquarium_mod |
|
684 |
#' p <- sample_params(m) |
|
685 |
#' m2 <- set_params(m, p) |
|
686 |
#' m2$parameters |
|
687 |
#' |
|
688 |
#' @export |
|
689 | ||
690 |
set_params <- function(nm, params, force = TRUE, quick = FALSE) { |
|
691 | 579x |
if (is.vector(params)) { |
692 | 579x |
params <- data.frame(value = params, parameter = names(params), |
693 | 579x |
stringsAsFactors = FALSE) |
694 |
} |
|
695 | 579x |
prev_params <- attr(nm, "parameterValues") |
696 | 579x |
if (!is.null(prev_params) & !force) { |
697 | ! |
kept_indices <- !(prev_params$parameter %in% params$parameter) |
698 | ! |
kept_params <- prev_params[kept_indices, ] |
699 | ! |
new_params <- dplyr::bind_rows(kept_params, params) |
700 |
} else { |
|
701 | 579x |
new_params <- params |
702 |
} |
|
703 | 579x |
if (!quick) { |
704 | 299x |
new_params <- new_params[order(new_params[["parameter"]]), ] |
705 | 299x |
attr(nm, "parameterValues") <- tibble::as_tibble(new_params) |
706 |
} else { |
|
707 | 280x |
attr(nm, "parameterValues") <- new_params |
708 |
} |
|
709 | 579x |
for (i in seq_len(nrow(nm))) { |
710 | 581x |
nm[["parameters"]][[i]]$value <- new_params[["value"]][match(nm[["parameters"]][[i]]$in_model, |
711 | 581x |
new_params[["parameter"]])] |
712 |
} |
|
713 | 579x |
return(nm) |
714 |
} |
|
715 | ||
716 |
### * set_prior() | set_priors() |
|
717 | ||
718 |
#' Set prior(s) for a network model |
|
719 |
#' |
|
720 |
#' @param x A \code{networkModel} object. |
|
721 |
#' @param prior A prior built with e.g. uniform_p() or hcauchy_p(). Call |
|
722 |
#' \code{available_priors()} to see a table of implemented |
|
723 |
#' priors. Alternatively, if \code{prior} is a tibble, the function will |
|
724 |
#' try to use it to set parameter priors. The format of such an argument is |
|
725 |
#' the same as the format of the output of the getter function |
|
726 |
#' \code{priors()} (see examples). Note that if `prior` is given as a |
|
727 |
#' tibble, all other arguments (except `x`) are disregarded. |
|
728 |
#' @param param String, target parameter or regexp to target several |
|
729 |
#' parameters. Default is the empty string \code{""}, which will match all |
|
730 |
#' parameters. |
|
731 |
#' @param use_regexp Boolean, if \code{TRUE} (the default) then \code{param} is |
|
732 |
#' used as a regular expression to match one or several parameter names. |
|
733 |
#' @param quiet Boolean, if \code{FALSE} print a message indicating which |
|
734 |
#' parameters had their prior modified. |
|
735 |
#' |
|
736 |
#' @return A \code{networkModel} object. |
|
737 |
#' |
|
738 |
#' @examples |
|
739 |
#' # Copy `aquarium_mod` |
|
740 |
#' m <- aquarium_mod |
|
741 |
#' priors(m) |
|
742 |
#' |
|
743 |
#' # Modify the priors of `m` |
|
744 |
#' m <- set_priors(m, exponential_p(0.5), "lambda") |
|
745 |
#' priors(m) |
|
746 |
#' |
|
747 |
#' # Re-apply priors from the original `aquarium_mod` |
|
748 |
#' prev_priors <- priors(aquarium_mod) |
|
749 |
#' prev_priors |
|
750 |
#' m <- set_priors(m, prev_priors) |
|
751 |
#' priors(m) |
|
752 |
#' |
|
753 |
#' @export |
|
754 | ||
755 |
set_prior <- function(x, prior, param = "", use_regexp = TRUE, quiet = FALSE) { |
|
756 | 33x |
verbose <- !quiet |
757 | 33x |
params <- params(x, simplify = TRUE) |
758 | 33x |
priors <- priors(x) |
759 | 33x |
stopifnot(setequal(params, priors[["in_model"]])) |
760 | 33x |
params <- priors[["in_model"]] |
761 |
# If `prior` is a tibble |
|
762 | 33x |
if (is(prior, "tbl_df")) { |
763 | ! |
if (!valid_prior_tbl(prior)) { |
764 | ! |
stop("`prior` is not a valid prior tibble.") |
765 |
} |
|
766 | ! |
prior <- prior[, c("in_model", "prior")] |
767 | ! |
extra_params <- which(!prior[["in_model"]] %in% params) |
768 | ! |
if (length(extra_params) > 0) { |
769 | ! |
message("Some parameter names in `prior` are not used in the network model and will be ignored.\n", |
770 | ! |
paste0(paste0("\"", prior[["in_model"]][extra_params], "\""), collapse = ", ")) |
771 |
} |
|
772 | ! |
prior <- prior[which(prior[["in_model"]] %in% params), ] |
773 | ! |
param_indices <- match(prior[["in_model"]], params) |
774 | ! |
for (i in seq_along(param_indices)) { |
775 | ! |
priors[["prior"]][[param_indices[i]]] <- prior[["prior"]][[i]] |
776 |
} |
|
777 | ! |
if (verbose) { |
778 | ! |
message("Prior modified for parameter(s): \n - ", |
779 | ! |
paste0(prior[["in_model"]], collapse = "\n - ")) |
780 |
} |
|
781 | ! |
attr(x, "priors") <- priors |
782 | ! |
return(x) |
783 |
} |
|
784 |
# If `prior` is not a tibble |
|
785 | 33x |
if (!is(prior, "prior")) { |
786 | ! |
stop("`prior` must be a valid prior. See `available_priors()`.") |
787 |
} |
|
788 | 33x |
if (use_regexp) { |
789 | 32x |
param_indices <- which(grepl(pattern = param, x = params)) |
790 |
} else { |
|
791 | 1x |
param_indices <- which(params == param) |
792 |
} |
|
793 | 33x |
if (length(param_indices) == 0) { |
794 | ! |
stop("No matching parameter found for: ", param, ".\n", |
795 | ! |
"Available parameters are: \n- ", |
796 | ! |
paste0(params, collapse = "\n- ")) |
797 |
} |
|
798 | 33x |
for (i in param_indices) { |
799 | 225x |
priors[["prior"]][[i]] <- prior |
800 |
} |
|
801 | 33x |
if (verbose) { |
802 | 17x |
message("Prior modified for parameter(s): \n - ", |
803 | 17x |
paste0(params[param_indices], collapse = "\n - ")) |
804 |
} |
|
805 | 33x |
attr(x, "priors") <- priors |
806 | 33x |
return(x) |
807 |
} |
|
808 | ||
809 |
#' @rdname set_prior |
|
810 |
#' @export |
|
811 | ||
812 |
set_priors <- set_prior |
|
813 |
|
|
814 |
### * add_covariates() |
|
815 | ||
816 |
#' Add fixed effects of one or several covariates to some parameters. |
|
817 |
#' |
|
818 |
#' Note that new global parameters are not given any default prior. |
|
819 |
#' |
|
820 |
#' @param nm A \code{networkModel} object. |
|
821 |
#' @param ... One or several formulas defining the covariates. |
|
822 |
#' @param use_regexpr Boolean, use regular expression to match the parameters |
|
823 |
#' affected by the formulas? |
|
824 |
#' |
|
825 |
#' @return A \code{networkModel} object. |
|
826 |
#' |
|
827 |
#' @examples |
|
828 |
#' # Using a subset of the topology from the Trinidad case study |
|
829 |
#' m <- new_networkModel() %>% |
|
830 |
#' set_topo("NH4, NO3 -> epi, FBOM", "epi -> petro, pseph") |
|
831 |
#' |
|
832 |
#' # Taking initial condtions from the 'lalaja' dataset at t=0 |
|
833 |
#' # Grouping by transect id |
|
834 |
#' inits <- lalaja[lalaja[["time.days"]] == 0, ] |
|
835 |
#' inits |
|
836 |
#' m <- set_init(m, inits, comp = "compartment", size = "mgN.per.m2", |
|
837 |
#' prop = "prop15N", group_by = "transect") |
|
838 |
#' m |
|
839 |
#' |
|
840 |
#' # Default model |
|
841 |
#' params(m, simplify = TRUE) |
|
842 |
#' |
|
843 |
#' # Adding an effect of the "transect" covariate on some parameters |
|
844 |
#' m <- add_covariates(m, upsilon_epi_to_pseph ~ transect) |
|
845 |
#' params(m, simplify = TRUE) |
|
846 |
#' |
|
847 |
#' @export |
|
848 | ||
849 |
add_covariates <- function(nm, ..., use_regexpr = TRUE) { |
|
850 | 5x |
formula <- list(...) |
851 |
# Apply formula |
|
852 | 5x |
for (eachFormula in formula) { |
853 | 5x |
nm <- refresh_param_mapping(nm, eachFormula, use_regexpr = use_regexpr) |
854 |
} |
|
855 |
# Refresh priors |
|
856 | 5x |
current_priors <- priors(nm) |
857 | 5x |
global_params <- params(nm, simplify = TRUE) |
858 | 5x |
default_priors <- tibble::tibble(in_model = global_params) |
859 | 5x |
default_priors$prior <- rep(list(NULL), nrow(default_priors)) |
860 | 5x |
kept_priors <- current_priors[current_priors$in_model %in% global_params, ] |
861 | 5x |
new_priors <- default_priors[!default_priors$in_model %in% |
862 | 5x |
current_priors$in_model, ] |
863 | 5x |
priors <- dplyr::bind_rows(kept_priors, new_priors) |
864 | 5x |
priors <- priors[order(priors$in_model), ] |
865 |
# Return |
|
866 | 5x |
attr(nm, "priors") <- priors |
867 | 5x |
return(nm) |
868 |
} |
|
869 | ||
870 |
### * print.networkModel() |
|
871 | ||
872 |
#' Print method for \code{networkModel} objects |
|
873 |
#' |
|
874 |
#' @param x A \code{networkModel} object. |
|
875 |
#' @param ... Passsed to the next method. |
|
876 |
#' |
|
877 |
#' @return Called for the side effect of printing a network model object. |
|
878 |
#' |
|
879 |
#' @method print networkModel |
|
880 |
#' |
|
881 |
#' @export |
|
882 |
#' |
|
883 | ||
884 |
print.networkModel <- function(x, ...) { |
|
885 | ! |
NextMethod() |
886 | ! |
lambda_hl <- attr(x, "lambda_hl") |
887 | ! |
if (!is.null(lambda_hl)) { |
888 | ! |
hl <- signif(log(2) / lambda_hl, 5) |
889 | ! |
cat("# Half-life of marked tracer:", hl, "time units.\n") |
890 |
} |
|
891 |
} |
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_plo |