Skip to content

Commit 038ee38

Browse files
committed
1 parent 8e919e9 commit 038ee38

File tree

6 files changed

+241
-26
lines changed

6 files changed

+241
-26
lines changed

NEWS.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# tmap 4.2.0.9000 (dev version)
22

3-
- tm_text: bgcol and bgcol_alpha implemented in view mode
3+
- tm_text: halo, shadow and background rectangles implemented in plot and view mode
44

55
# tmap 4.2
66

R/tm_layers_text.R

Lines changed: 54 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -468,11 +468,19 @@ tm_labels_highlighted = function(text = tm_const(),
468468
#' @param points_only should only point geometries of the shape object (defined in [tm_shape()]) be plotted? By default `"ifany"`, which means `TRUE` in case a geometry collection is specified.
469469
#' @param point_per specification of how spatial points are mapped when the geometry is a multi line or a multi polygon. One of \code{"feature"}, \code{"segment"} or \code{"largest"}. The first generates a spatial point for every feature, the second for every segment (i.e. subfeature), the third only for the largest segment (subfeature). Note that the last two options can be significant slower.
470470
#' @param on_surface In case of polygons, centroids are computed. Should the points be on the surface? If `TRUE`, which is slower than the default `FALSE`, centroids outside the surface are replaced with points computed with [sf::st_point_on_surface()].
471-
#' @param shadow Shadow behind the text. Logical or color.
471+
#' @param shadow Shadow behind the text. Logical.
472+
#' @param shadow.col Color of the shadow.
472473
#' @param shadow.offset.x,shadow.offset.y Shadow offset in line heights
474+
#' @param halo Halo behind the text. In plot mode, it is just an outline, in view mode also a subtle glow.
475+
#' @param halo.col Color of the halo.
476+
#' @param halo.width Width (thickness) of the halo outline. In line heights
477+
#' @param halo.blur Blur radius of the halo glow (view mode only). In line heights
473478
#' @param just justification of the text relative to the point coordinates. Either one of the following values: \code{"left"} , \code{"right"}, \code{"center"}, \code{"bottom"}, and \code{"top"}, or a vector of two values where first value specifies horizontal and the second value vertical justification. Besides the mentioned values, also numeric values between 0 and 1 can be used. 0 means left justification for the first value and bottom justification for the second value. Note that in view mode, only one value is used.
474479
#' @param along_lines logical that determines whether labels are rotated along the spatial lines. Only applicable if a spatial lines shape is used.
475480
#' @param bg.padding The padding of the background in terms of line heights.
481+
#' @param bg.border Should the background have borders?
482+
#' @param bg.border.col Color of the borders
483+
#' @param bg.border.lwd Line width of the borders
476484
#' @param clustering in interactive modes (e.g. \code{"view"} mode), should clustering be applied at lower zoom levels? Either `FALSE` (default), `TRUE`, or a mode specific specification, e.g. for \code{"view"} mode \code{\link[leaflet:markerClusterOptions]{markerClusterOptions}}.
477485
#' @param point.label logical that determines whether the labels are placed automatically. By default `FALSE` for `tm_text`, and `TRUE` for `tm_labels` if the number of labels is less than 500 (otherwise it will be too slow).
478486
#' @param point.label.gap numeric that determines the gap between the point and label
@@ -484,26 +492,47 @@ opt_tm_text = function(points_only = "ifany",
484492
point_per = "feature",
485493
on_surface = FALSE,
486494
shadow = FALSE,
495+
shadow.col = NA,
487496
shadow.offset.x = 0.1,
488497
shadow.offset.y = 0.1,
498+
halo = FALSE,
499+
halo.col = NA,
500+
halo.width = 0.02,
501+
halo.blur = 0.1,
489502
just = "center",
490503
along_lines = FALSE,
491504
bg.padding = 0.4,
505+
bg.border = FALSE,
506+
bg.border.col = "black",
507+
bg.border.lwd = 1,
492508
clustering = FALSE,
493509
point.label = FALSE,
494510
point.label.gap = 0,
495511
point.label.method = "SANN",
496512
remove_overlap = FALSE) {
513+
if (!is.logical(shadow)) {
514+
cli::cli_warn("{.fun opt_tm_text} {.arg shadow} should be {.code TRUE} or {.code FALSE}. Please use shadow.col to specify a color")
515+
shadow.col = shadow
516+
shadow = TRUE
517+
}
497518
list(trans.args = list(points_only = points_only,
498519
point_per = point_per,
499520
on_surface = on_surface,
500521
along_lines = along_lines),
501522
mapping.args = list(shadow = shadow,
523+
shadow.col = shadow.col,
502524
shadow.offset.x = shadow.offset.x,
503525
shadow.offset.y = shadow.offset.y,
526+
halo = halo,
527+
halo.col = halo.col,
528+
halo.width = halo.width,
529+
halo.blur = halo.blur,
504530
just = just,
505531
along_lines = along_lines,
506532
bg.padding = bg.padding,
533+
bg.border = bg.border,
534+
bg.border.col = bg.border.col,
535+
bg.border.lwd = bg.border.lwd,
507536
clustering = clustering,
508537
point.label = point.label,
509538
point.label.gap = point.label.gap,
@@ -517,26 +546,48 @@ opt_tm_labels = function(points_only = "ifany",
517546
point_per = "feature",
518547
on_surface = FALSE,
519548
shadow = FALSE,
520-
shadow.offset.x = 0.1,
521-
shadow.offset.y = 0.1,
549+
shadow.col = NA,
550+
shadow.offset.x = 0.05,
551+
shadow.offset.y = 0.05,
552+
halo = FALSE,
553+
halo.col = NA,
554+
halo.width = 0.05,
555+
halo.blur = 0.1,
522556
just = "center",
523557
along_lines = TRUE,
524558
bg.padding = 0.4,
559+
bg.border = FALSE,
560+
bg.border.col = "black",
561+
bg.border.lwd = 1,
525562
clustering = FALSE,
526563
point.label = NA,
527564
point.label.gap = 0.4,
528565
point.label.method = "SANN",
529566
remove_overlap = FALSE) {
567+
if (!is.logical(shadow)) {
568+
cli::cli_warn("{.fun opt_tm_labels} {.arg shadow} should be {.code TRUE} or {.code FALSE}. Please use shadow.col to specify a color")
569+
shadow.col = shadow
570+
shadow = TRUE
571+
}
572+
530573
list(trans.args = list(points_only = points_only,
531574
point_per = point_per,
532575
on_surface = on_surface,
533576
along_lines = along_lines),
534577
mapping.args = list(shadow = shadow,
578+
shadow.col = shadow.col,
535579
shadow.offset.x = shadow.offset.x,
536580
shadow.offset.y = shadow.offset.y,
581+
halo = halo,
582+
halo.col = halo.col,
583+
halo.width = halo.width,
584+
halo.blur = halo.blur,
537585
just = just,
538586
along_lines = along_lines,
539587
bg.padding = bg.padding,
588+
bg.border = bg.border,
589+
bg.border.col = bg.border.col,
590+
bg.border.lwd = bg.border.lwd,
540591
clustering = clustering,
541592
point.label = point.label,
542593
point.label.gap = point.label.gap,

R/tmapGridDataPlot_text.R

Lines changed: 55 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -84,10 +84,10 @@ tmapGridDataPlot.tm_data_text = function(a, shpTM, dt, gp, bbx, facet_row, facet
8484
gp = gp_to_gpar(gp, sel = "col", o = o, type = "text")
8585

8686
with_bg = any(bgcol_alpha != 0)
87-
with_shadow = (!identical(a$shadow, FALSE))
87+
with_shadow_halo = a$shadow || a$halo
8888

8989

90-
if (with_bg || with_shadow || a$remove_overlap || a$point.label) {
90+
if (with_bg || with_shadow_halo || a$remove_overlap || a$point.label) {
9191
# grobs are processed seperately because of the order: backgrond1, shadow1, text1, background2, shadow2, text2, etc.
9292
# becaues it is less efficient when there is no background/shadow (majority of use cases), this is a separate routine
9393

@@ -97,12 +97,50 @@ tmapGridDataPlot.tm_data_text = function(a, shpTM, dt, gp, bbx, facet_row, facet
9797
grid::textGrob(x = grid::unit(x, "native"), y = grid::unit(y, "native"), label = txt, gp = gp, rot = a, just = just) #, name = paste0("text_", id))
9898
}, text, coords[,1], coords[,2], gps, angle, SIMPLIFY = FALSE, USE.NAMES = FALSE)
9999

100-
if (with_shadow) {
100+
if (with_shadow_halo) {
101101
gp_sh = gp
102-
gp_sh$col = ifelse(is_light(gp$col), "#000000", "#FFFFFF")
102+
103+
if (a$halo) {
104+
gp_sh$col = ifelse(is.na(a$halo.col), ifelse(is_light(gp$col), "#000000", "#FFFFFF"), a$halo.col)
105+
} else {
106+
gp_sh$col = ifelse(is.na(a$shadow.col), ifelse(is_light(gp$col), "#000000", "#FFFFFF"), a$shadow.col)
107+
}
103108
gps_sh = split_gp(gp_sh, n)
104109
grobTextShList = mapply(function(x, y, txt, g, ai) {
105-
grid::textGrob(x = grid::unit(x + a$shadow.offset.x * xIn * lineIn, "native"), y = grid::unit(y - a$shadow.offset.y * yIn * lineIn, "native"), label = txt, gp = g, rot = ai, just = just)
110+
if (a$halo) {
111+
offsets <- rbind(
112+
c(-1, 0),
113+
c( 1, 0),
114+
c( 0,-1),
115+
c( 0, 1)
116+
)
117+
118+
diag_offsets <- rbind(
119+
c(-1,-1),
120+
c( 1,-1),
121+
c(-1, 1),
122+
c( 1, 1)
123+
)
124+
125+
# allow smaller diagonal distance for rounder halo
126+
diag_offsets <- diag_offsets * .5 * sqrt(2)
127+
128+
offsets <- rbind(offsets, diag_offsets)
129+
130+
grid::textGrob(x = grid::unit(x + a$halo.width * xIn * lineIn * g$cex * offsets[,1], "native"),
131+
y = grid::unit(y - a$halo.width * yIn * lineIn * g$cex * offsets[,2], "native"),
132+
label = txt,
133+
gp = g,
134+
rot = ai,
135+
just = just)
136+
} else {
137+
grid::textGrob(x = grid::unit(x + a$shadow.offset.x * xIn * lineIn * g$cex, "native"),
138+
y = grid::unit(y - a$shadow.offset.y * yIn * lineIn * g$cex, "native"),
139+
label = txt,
140+
gp = g,
141+
rot = ai,
142+
just = just)
143+
}
106144
}, coords[,1], coords[,2], text, gps_sh, angle, SIMPLIFY = FALSE, USE.NAMES = FALSE)
107145
} else {
108146
grobTextShList = NULL
@@ -129,11 +167,19 @@ tmapGridDataPlot.tm_data_text = function(a, shpTM, dt, gp, bbx, facet_row, facet
129167
tGX = unit(coords[,1] + justx * tGW, "native")
130168
tGY = unit(coords[,2] + justy * tGH, "native")
131169

132-
tGH = unit(tGH + a$bg.padding * yIn * lineIn, "native")
133-
tGW = unit(tGW + a$bg.padding * xIn * lineIn, "native")
170+
tGH = unit(tGH + (0.1 + a$bg.padding) * yIn * lineIn * gp$cex, "native")
171+
tGW = unit(tGW + a$bg.padding * xIn * lineIn * gp$cex, "native")
172+
173+
if (a$bg.border) {
174+
bordercol = a$bg.border.col
175+
lwd = a$bg.border.lwd
176+
} else {
177+
bordercol = NA
178+
lwd = 0
179+
}
134180

135181
grobTextBGList = mapply(function(x, y, w, h, b, a, rot) {
136-
rect = rectGrob(x=x, y=y, width=w, height=h, gp=gpar(fill=b, alpha = a, col=NA))
182+
rect = rectGrob(x=x, y=y, width=w, height=h, gp=gpar(fill=b, alpha = a, col=bordercol, lwd = lwd))
137183
if (rot != 0) {
138184
.rectGrob2pathGrob(rect, rot, bbx)$poly
139185
} else {
@@ -181,7 +227,7 @@ tmapGridDataPlot.tm_data_text = function(a, shpTM, dt, gp, bbx, facet_row, facet
181227
}, grobTextBGList, sx, sy, SIMPLIFY = FALSE)
182228

183229

184-
if (with_shadow) {
230+
if (with_shadow_halo) {
185231
grobTextShList = mapply(function(grb, sxi, syi) {
186232
grb$x = grb$x + grid::unit(sxi, "native")
187233
grb$y = grb$y + grid::unit(syi, "native")

R/tmapLeafletDataPlot_text.R

Lines changed: 95 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -51,9 +51,15 @@ tmapLeafletDataPlot.tm_data_text = function(a, shpTM, dt, pdt, popup.format, hdt
5151
bgcol_set = unique(gp$bgcol)
5252
bgcol_alpha = unique(gp$bgcol_alpha)
5353

54-
# if (any(bgcol_set != "#00000000")) {
55-
# message("Variable bgcol and bgcol_alpha not supported by view mode")
56-
# }
54+
if (all(bgcol_set == "#00000000")) {
55+
bg_enabled = FALSE
56+
} else {
57+
bgres = split_alpha_channel(gp$bgcol, gp$bgcol_alpha)
58+
gp$bgcol = bgres$col
59+
gp$bgcol_alpha = bgres$opacity
60+
bg_enabled = TRUE
61+
}
62+
5763

5864

5965
if (length(face_set) != 1) message("Variable fontfaces not supported by view mode")
@@ -75,8 +81,22 @@ tmapLeafletDataPlot.tm_data_text = function(a, shpTM, dt, pdt, popup.format, hdt
7581
coords[,1] = coords[,1] + delta * gp$cex * gp$xmod
7682
coords[,2] = coords[,2] + delta * gp$cex * gp$ymod
7783

84+
scale_px = function(textlines, cex) {
85+
ceiling(textlines * cex * 12)
86+
}
7887

7988

89+
make_shadow = function(cex, textcol) {
90+
if (a$halo) {
91+
if (is.na(a$halo.col)) a$halo.col = ifelse(is_light(textcol), "#000000", "#FFFFFF")
92+
make_halo_css(col = a$halo.col, mode = "halo", width = scale_px(a$halo.width, cex), blur = scale_px(a$halo.blur, cex), diag_scale = sqrt(2)/2)
93+
} else if (a$shadow) {
94+
if (is.na(a$shadow.col)) a$shadow.col = ifelse(is_light(textcol), "#000000", "#FFFFFF")
95+
make_halo_css(col = a$shadow.col, mode = "shadow", offset.x = scale_px(a$shadow.offset.x, cex), offset.y = scale_px(a$shadow.offset.y, cex))
96+
} else {
97+
NULL
98+
}
99+
}
80100

81101
if (!vary) {
82102
lf = lf %>% addLabelOnlyMarkers(lng = coords[, 1], lat = coords[,2],
@@ -90,9 +110,11 @@ tmapLeafletDataPlot.tm_data_text = function(a, shpTM, dt, pdt, popup.format, hdt
90110
opacity=gp$col_alpha[1],
91111
textsize=sizeChar[1],
92112
style=list("color"=gp$col[1],
93-
"background-color" = paste0("rgba(", paste(col2rgb(gp$bgcol[1]), collapse = ", "), ",", gp$bgcol_alpha[1], ")"),
94-
# "border" = "2px solid rgba(0, 0, 0, 0.5)",
95-
"padding" = paste0(round(2 * gp$cex[1]), "px"))),
113+
"background-color" = if (bg_enabled) paste0("rgba(", paste(col2rgb(gp$bgcol[1]), collapse = ", "), ",", gp$bgcol_alpha[1], ")") else NULL,
114+
"border" = if (a$bg.border) paste0(a$bg.border.lwd, "px solid rgba(", paste(col2rgb(a$bg.border.col[1]), collapse = ", "), ",", gp$bgcol_alpha[1], ")") else NULL,
115+
"line-height" = sizeChar[1],
116+
"text-shadow" = make_shadow(gp$cex[1], gp$col[1]),
117+
"padding" = paste0(scale_px(a$bg.padding / 2, gp$cex[1]), "px"))),
96118
options = markerOptions(pane = pane),
97119
clusterOptions = clusterOpts,
98120
clusterId = cidt)
@@ -109,9 +131,11 @@ tmapLeafletDataPlot.tm_data_text = function(a, shpTM, dt, pdt, popup.format, hdt
109131
opacity=gp$col_alpha[i],
110132
textsize=sizeChar[i],
111133
style=list("color"=gp$col[i],
112-
"background-color" = paste0("rgba(", paste(col2rgb(gp$bgcol[i]), collapse = ", "), ",", gp$bgcol_alpha[i], ")"),
113-
# "border" = "2px solid rgba(0, 0, 0, 0.5)",
114-
"padding" = paste0(round(2 * gp$cex[i]), "px"))),
134+
"background-color" = if (bg_enabled) paste0("rgba(", paste(col2rgb(gp$bgcol[i]), collapse = ", "), ",", gp$bgcol_alpha[i], ")") else NULL,
135+
"border" = if (a$bg.border) paste0(a$bg.border.lwd, "px solid rgba(", paste(col2rgb(a$bg.border.col[i]), collapse = ", "), ",", gp$bgcol_alpha[i], ")") else NULL,
136+
"line-height" = sizeChar[i],
137+
"text-shadow" = make_shadow(gp$cex[i], gp$col[i]),
138+
"padding" = paste0(scale_px(a$bg.padding / 2, gp$cex[i]), "px"))),
115139
options = markerOptions(pane = pane),
116140
clusterOptions = clusterOpts,
117141
clusterId = cidt)
@@ -137,3 +161,65 @@ tmapLeafletDataPlot.tm_data_labels_highlighted = function(a, shpTM, dt, pdt, pop
137161
NextMethod()
138162
}
139163

164+
165+
166+
167+
make_halo_css <- function(
168+
col = "white",
169+
width = 1,
170+
blur = 0,
171+
diagonals = TRUE,
172+
diag_scale = 1,
173+
mode = c("halo", "shadow"),
174+
offset.x = 2,
175+
offset.y = 2
176+
) {
177+
178+
mode <- match.arg(mode)
179+
180+
# --- Simple drop shadow mode ---
181+
if (mode == "shadow") {
182+
return(sprintf(
183+
"%spx %spx %spx %s",
184+
offset.x, offset.y, blur, col
185+
))
186+
}
187+
188+
# --- Halo mode ---
189+
# Cardinal directions
190+
offsets <- rbind(
191+
c(-1, 0),
192+
c( 1, 0),
193+
c( 0,-1),
194+
c( 0, 1)
195+
)
196+
197+
# Optional diagonals
198+
if (diagonals) {
199+
diag_offsets <- rbind(
200+
c(-1,-1),
201+
c( 1,-1),
202+
c(-1, 1),
203+
c( 1, 1)
204+
)
205+
206+
# allow smaller diagonal distance for rounder halo
207+
diag_offsets <- diag_offsets * diag_scale
208+
209+
offsets <- rbind(offsets, diag_offsets)
210+
}
211+
212+
# Scale by width
213+
offsets <- offsets * width
214+
215+
parts <- apply(offsets, 1, function(x) {
216+
sprintf("%spx %spx 0 %s", x[1], x[2], col)
217+
})
218+
219+
# Optional glow
220+
if (blur > 0) {
221+
parts <- c(parts, sprintf("0 0 %spx %s", blur, col))
222+
}
223+
224+
paste(parts, collapse = ", ")
225+
}

man/tm_symbols.Rd

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)