Skip to content

Commit 36ae315

Browse files
committed
Added dotted lines and position() with relative moves
Closes issue #76
1 parent d938f0b commit 36ae315

31 files changed

Lines changed: 742 additions & 21 deletions

.Rbuildignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,3 +28,4 @@ Gmisc/inst/extdata/Full_test_suite_files/*
2828
^docs/
2929
^README\.Rmd$
3030
^README\.html$
31+
^Rplots.pdf

NAMESPACE

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,9 @@ S3method(phaseLabel,default)
2626
S3method(plot,box)
2727
S3method(plot,connect_boxes)
2828
S3method(plot,connect_boxes_list)
29+
S3method(position,box)
30+
S3method(position,coords)
31+
S3method(position,default)
2932
S3method(prCalculateConnector,ConnectorStrategy)
3033
S3method(prCalculateConnector,ManyToOneFanInCenterConnectorStrategy)
3134
S3method(prCalculateConnector,ManyToOneFanInTopConnectorStrategy)
@@ -110,6 +113,7 @@ export(move)
110113
export(moveBox)
111114
export(pathJoin)
112115
export(phaseLabel)
116+
export(position)
113117
export(rack_box_fn)
114118
export(retrieve)
115119
export(server_box_fn)

NEWS.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,9 @@ NEWS for the Gmisc package
1111
- Improved interactive example ergonomics by pausing between graph pages in multi-plot examples.
1212
- Added `box_fn_args` parameter to `boxGrob()` for passing extra arguments directly to the box drawing function. The default is now `list(r = unit(5, "pt"))`, giving every box a fixed 5 pt corner radius (approximately equivalent to the widely-used 5 px CSS `border-radius`) regardless of box size — solving the issue where larger boxes had visibly rounder corners than smaller ones. Override per box or globally via `options(boxGrobFnArgs = list(...))`.
1313
- Added `equalizeHeights()` for flowchart box lists, mirroring the existing `equalizeWidths()`. It sets selected boxes to a shared height (defaulting to the tallest among the selection) and supports the same `subelement` path syntax as `equalizeWidths()`.
14-
14+
- Added `position()` with relative moves.
15+
- Added dotted arrows lines for flowcharts
16+
- Added regex selector for box lists
1517

1618
## Changes for 3.3.0
1719

R/boxGrobs_connect.R

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,14 @@
8282
#' A [grid::unit()] or numeric (interpreted as millimeters). Default `unit(3, "mm")`.
8383
#' @param side For `type = "side"`, which side of the start box to exit from.
8484
#' `"auto"` (default) picks the side that faces the end box.
85+
#' @param end_side For `type = "side"`, which side of the end box to enter.
86+
#' `"auto"` (default) picks the side that faces the start box.
87+
#' @param side_fan_in_route For grouped `connect(..., type = "side")` calls in
88+
#' the S3 flowchart API, where to draw the shared vertical return path.
89+
#' `"outside"` offsets it away from the source boxes; `"edge"` draws it on
90+
#' the source box edge.
91+
#' @param side_fan_in_offset Offset used when `side_fan_in_route = "outside"`.
92+
#' Numeric values are interpreted as millimeters.
8593
#' @param label Optional text label for one-to-one connectors (e.g. `"yes"` / `"no"`).
8694
#' Only supported when both `start` and `end` are single boxes.
8795
#' @param label_gp A [grid::gpar()] controlling label appearance.
@@ -114,6 +122,9 @@ connectGrob <- function(
114122
smooth = FALSE,
115123
corner_radius = unit(3, "mm"),
116124
side = c("auto", "left", "right"),
125+
end_side = c("auto", "left", "right"),
126+
side_fan_in_route = c("outside", "edge"),
127+
side_fan_in_offset = unit(5, "mm"),
117128
label = NULL,
118129
label_gp = grid::gpar(cex = 0.9),
119130
label_bg_gp = grid::gpar(fill = "white", col = NA),
@@ -124,6 +135,8 @@ connectGrob <- function(
124135
type <- match.arg(type)
125136
label_pos <- match.arg(label_pos)
126137
side <- match.arg(side)
138+
end_side <- match.arg(end_side)
139+
side_fan_in_route <- match.arg(side_fan_in_route)
127140

128141
if (!is.null(arrow_size)) {
129142
ends_map <- c("1" = "first", "2" = "last", "3" = "both")
@@ -216,6 +229,9 @@ connectGrob <- function(
216229
smooth = smooth,
217230
corner_radius = corner_radius,
218231
side = side,
232+
end_side = end_side,
233+
side_fan_in_route = side_fan_in_route,
234+
side_fan_in_offset = side_fan_in_offset,
219235
label = label,
220236
label_gp = label_gp,
221237
label_bg_gp = label_bg_gp,

R/boxGrobs_connect_pr_single_boxes.R

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,9 @@ prConnect1 <- function(
9595
smooth = FALSE,
9696
corner_radius = unit(3, "mm"),
9797
side = c("auto", "left", "right"),
98+
end_side = c("auto", "left", "right"),
99+
side_fan_in_route = c("outside", "edge"),
100+
side_fan_in_offset = unit(5, "mm"),
98101
label = NULL,
99102
label_gp = grid::gpar(cex = 0.9),
100103
label_bg_gp = grid::gpar(fill = "white", col = NA),
@@ -110,6 +113,8 @@ prConnect1 <- function(
110113

111114
label_pos <- match.arg(label_pos)
112115
side <- match.arg(side)
116+
end_side <- match.arg(end_side)
117+
side_fan_in_route <- match.arg(side_fan_in_route)
113118

114119
start <- coords(start)
115120
end <- coords(end)
@@ -194,20 +199,23 @@ prConnect1 <- function(
194199
line$y <- unit.c(start$bottom, end$top)
195200
}
196201
} else if (type == "side") {
197-
# Horizontal-first exit: leave from start side, travel horizontally,
198-
# then drop vertically to the end box.
202+
# Horizontal-first exit: leave from a selected start side, travel vertically
203+
# outside/alongside the flow, then enter the selected side of the end box.
199204
end_is_right <- prConvertWidthToMm(getX4elmnt(end, "x")) > prConvertWidthToMm(start$x)
200205
exit_x <- if (side == "right" || (side == "auto" && end_is_right)) {
201206
start$right
202207
} else {
203208
start$left
204209
}
205-
line$x <- unit.c(exit_x, getX4elmnt(end, "x"), getX4elmnt(end, "x"))
206-
if (prConvertHeightToMm(start$y) > prConvertHeightToMm(end$y)) {
207-
line$y <- unit.c(start$y, start$y, end$top)
210+
211+
start_is_left <- prConvertWidthToMm(start$x) < prConvertWidthToMm(getX4elmnt(end, "x"))
212+
entry_x <- if (end_side == "left" || (end_side == "auto" && start_is_left)) {
213+
end$left
208214
} else {
209-
line$y <- unit.c(start$y, start$y, end$bottom)
215+
end$right
210216
}
217+
line$x <- unit.c(exit_x, exit_x, entry_x)
218+
line$y <- unit.c(start$y, end$y, end$y)
211219
} else { # horizontal
212220
line$y <- unit.c(start$y, end$y)
213221
if (prConvertWidthToMm(getX4elmnt(start, "x")) < prConvertWidthToMm(getX4elmnt(end, "x"))) {

R/boxGrobs_connect_strategies.R

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,9 @@ prCalculateConnector.ConnectorStrategy <- function(strategy, ...) {
104104
smooth = args$smooth,
105105
corner_radius = args$corner_radius,
106106
side = args$side,
107+
end_side = args$end_side,
108+
side_fan_in_route = args$side_fan_in_route,
109+
side_fan_in_offset = args$side_fan_in_offset,
107110
label = args$label,
108111
label_gp = args$label_gp,
109112
label_bg_gp = args$label_bg_gp,

R/boxGrobs_coords.R

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,26 @@
33
#' Retrieves the boxes \code{"coords"} attribute.
44
#'
55
#' @param box The \code{\link{boxGrob}} or \code{\link{boxPropGrob}}
6-
#' @return A list with the coordinates
6+
#' @return A list with grid unit coordinates. Standard boxes include:
7+
#' \describe{
8+
#' \item{\code{x}, \code{y}}{The box center.}
9+
#' \item{\code{left}, \code{right}}{The horizontal edges.}
10+
#' \item{\code{top}, \code{bottom}}{The vertical edges.}
11+
#' \item{\code{width}, \code{height}}{The full box dimensions.}
12+
#' \item{\code{half_width}, \code{half_height}}{Half of the box dimensions.}
13+
#' }
14+
#' Split boxes such as \code{\link{boxPropGrob}} may add extra coordinates,
15+
#' for example \code{left_x}, \code{right_x}, and \code{prop_x}.
716
#'
817
#' @importFrom checkmate assert_class
918
#' @family flowchart components
1019
#' @export
1120
#' @examples
1221
#' box <- boxGrob("A test box")
1322
#' coords(box)
23+
#'
24+
#' # Extract a single position as a unit
25+
#' position(box, position = "left", type = "x")
1426
coords <- function(box) {
1527
# Check if not already a coordinate element
1628
if (inherits(box, "coords")) {
@@ -19,4 +31,4 @@ coords <- function(box) {
1931

2032
assert_class(box, "box")
2133
attr(box, "coords")
22-
}
34+
}

R/boxGrobs_move.R

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,9 @@ moveBox <- function(element,
3737
to_unit <- function(u) if (is.unit(u) || is.null(u)) u else unit(u, "npc")
3838

3939
if (is.list(element) && !inherits(element, "box")) {
40+
x <- prResolvePositionValue(x, element)
41+
y <- prResolvePositionValue(y, element)
42+
4043
if (is.null(x) && is.null(y)) {
4144
stop("You have to specify at least x or y move parameters")
4245
}

R/boxGrobs_position.R

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
#' Reference a flowchart box position
2+
#'
3+
#' Creates a position reference that can be used anywhere a `move()`/`moveBox()`
4+
#' coordinate accepts a `grid::unit()`. When `reference` is a path, the position
5+
#' is resolved against the current box list at move time.
6+
#'
7+
#' @param reference A box, a `coords()` object, a list of boxes, or a path into
8+
#' a flowchart list such as `"groups"` or `c("groups", 1)`.
9+
#' @param position Which position to extract. For `type = "x"` use `"center"`,
10+
#' `"left"`, or `"right"`. For `type = "y"` use `"center"`, `"top"`, or
11+
#' `"bottom"`.
12+
#' @param type Axis to extract: `"x"` or `"y"`.
13+
#'
14+
#' @return A `grid::unit()` when `reference` is already a box/coords object, or
15+
#' a deferred position reference when `reference` is a flowchart path.
16+
#' @export
17+
#' @family flowchart components
18+
#' @examples
19+
#' fc <- flowchart(groups = list("A", "B"), ex = list("X", "Y")) |>
20+
#' spread(axis = "x", subelement = "groups")
21+
#'
22+
#' fc |>
23+
#' move(subelement = c("ex", 1),
24+
#' x = position(c("groups", 1), position = "center", type = "x") +
25+
#' grid::unit(5, "mm"))
26+
position <- function(reference, position = "center", type = c("x", "y")) {
27+
UseMethod("position")
28+
}
29+
30+
#' @export
31+
#' @rdname position
32+
position.default <- function(reference, position = "center", type = c("x", "y")) {
33+
type <- match.arg(type)
34+
position <- match.arg(position, c("center", "left", "right", "top", "bottom"))
35+
prValidatePositionAxis(position = position, type = type)
36+
37+
if (is.list(reference) && !is.null(reference) && length(reference) > 0 &&
38+
all(vapply(reference, function(x) inherits(x, "box") || inherits(x, "coords") || is.list(x), logical(1)))) {
39+
return(prSelectPosition(prConvert2Coords(reference), position = position, type = type))
40+
}
41+
42+
ref <- structure(
43+
list(reference = reference, position = position, type = type),
44+
class = "Gmisc_position_ref"
45+
)
46+
structure(list(list(0, ref, 0L)), class = c("unit", "unit_v2"))
47+
}
48+
49+
#' @export
50+
#' @rdname position
51+
position.box <- function(reference, position = "center", type = c("x", "y")) {
52+
type <- match.arg(type)
53+
position <- match.arg(position, c("center", "left", "right", "top", "bottom"))
54+
prValidatePositionAxis(position = position, type = type)
55+
prSelectPosition(coords(reference), position = position, type = type)
56+
}
57+
58+
#' @export
59+
#' @rdname position
60+
position.coords <- function(reference, position = "center", type = c("x", "y")) {
61+
type <- match.arg(type)
62+
position <- match.arg(position, c("center", "left", "right", "top", "bottom"))
63+
prValidatePositionAxis(position = position, type = type)
64+
prSelectPosition(reference, position = position, type = type)
65+
}
66+
67+
prValidatePositionAxis <- function(position, type) {
68+
if (type == "x" && !position %in% c("center", "left", "right")) {
69+
stop("For type = 'x', `position` must be 'center', 'left', or 'right'.", call. = FALSE)
70+
}
71+
if (type == "y" && !position %in% c("center", "top", "bottom")) {
72+
stop("For type = 'y', `position` must be 'center', 'top', or 'bottom'.", call. = FALSE)
73+
}
74+
}
75+
76+
prSelectPosition <- function(coords, position, type) {
77+
if (position == "center") {
78+
return(coords[[type]])
79+
}
80+
coords[[position]]
81+
}
82+
83+
prUnitAsExpressionPart <- function(unit_value) {
84+
raw <- unclass(unit_value)
85+
if (is.list(raw)) {
86+
if (length(raw) != 1) {
87+
stop("Expected a length-1 unit value for position reference resolution.", call. = FALSE)
88+
}
89+
return(raw[[1]])
90+
}
91+
list(as.numeric(raw), NULL, as.integer(attr(raw, "unit")))
92+
}
93+
94+
prResolvePositionValue <- function(value, element) {
95+
if (!inherits(value, "unit")) {
96+
return(value)
97+
}
98+
99+
resolve_ref <- function(ref) {
100+
target <- if (inherits(ref$reference, "box") || inherits(ref$reference, "coords")) {
101+
ref$reference
102+
} else {
103+
get_list_element_by_path(element, ref$reference)
104+
}
105+
106+
if (is.null(target)) {
107+
stop(
108+
"The position reference '",
109+
paste(ref$reference, collapse = " -> "),
110+
"' was not found in the provided boxes.",
111+
call. = FALSE
112+
)
113+
}
114+
115+
prSelectPosition(prConvert2Coords(target), position = ref$position, type = ref$type)
116+
}
117+
118+
resolve_unit <- function(u) {
119+
if (!inherits(u, "unit_v2")) {
120+
return(u)
121+
}
122+
123+
parts <- unclass(u)
124+
if (!is.list(parts)) {
125+
return(u)
126+
}
127+
changed <- FALSE
128+
129+
parts <- lapply(parts, function(part) {
130+
data <- part[[2]]
131+
if (inherits(data, "Gmisc_position_ref")) {
132+
changed <<- TRUE
133+
return(prUnitAsExpressionPart(resolve_ref(data)))
134+
}
135+
if (inherits(data, "unit")) {
136+
part[[2]] <- resolve_unit(data)
137+
changed <<- TRUE
138+
}
139+
part
140+
})
141+
142+
if (!changed) {
143+
return(u)
144+
}
145+
structure(parts, class = c("unit", "unit_v2"))
146+
}
147+
148+
resolve_unit(value)
149+
}

0 commit comments

Comments
 (0)