Skip to content

Commit f7ba8e4

Browse files
authored
Merge pull request #53 from m-jahn/dev
Update to v0.1.3
2 parents 3a60127 + 34c5df2 commit f7ba8e4

35 files changed

+445
-221
lines changed

DESCRIPTION

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
Package: WeightedTreemaps
22
Title: Generate and Plot Voronoi or Sunburst Treemaps from Hierarchical
33
Data
4-
Version: 0.1.2
4+
Version: 0.1.3
55
Authors@R: c(
66
person("Michael", "Jahn", , "[email protected]", role = c("aut", "cre"),
77
comment = c(ORCID = "0000-0002-3913-153X")),
@@ -54,5 +54,5 @@ VignetteBuilder:
5454
knitr
5555
Encoding: UTF-8
5656
LazyData: true
57-
RoxygenNote: 7.2.3
57+
RoxygenNote: 7.3.2
5858
SystemRequirements: C++17

NEWS.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,14 @@
1+
# WeightedTreemaps 0.1.3
2+
3+
- fixed links to external packages
4+
- several bug fixes and improvements to catch errors when parsing polygons
5+
- added option to converge with different speed
6+
17
# WeightedTreemaps 0.1.2
28

9+
- updated README
10+
- vignette: reduced example sizes to improve building time
11+
312
# WeightedTreemaps 0.1.1
413

514
- The package was prepared for release on CRAN

R/allocate.R

Lines changed: 51 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
#' @importFrom stats rnorm
22
#' @importFrom dplyr %>%
33
#' @importFrom sf st_area
4-
#'
4+
#'
55
cellError <- function(a, target) {
66
normA <- a / sum(a)
77
diff <- abs(normA - target)
@@ -14,9 +14,9 @@ breaking <- function(
1414
debug = FALSE,
1515
error_tol,
1616
prevError) {
17-
17+
1818
if (max) {
19-
19+
2020
# Stop when largest individual cell error is less than 1%
2121
# (the default)
2222
err <- cellError(a, target)
@@ -29,7 +29,7 @@ breaking <- function(
2929
prevError <- err
3030

3131
} else {
32-
32+
3333
normA <- a / sum(a)
3434
diff <- abs(normA - target)
3535
# Stop when *change* in *total* cell error is tiny
@@ -46,7 +46,7 @@ breaking <- function(
4646
))
4747
stopping <- abs(sum(diff) - prevError) < 0.001
4848
prevError <- sum(diff)
49-
49+
5050
}
5151
list(
5252
stopping = stopping,
@@ -59,16 +59,22 @@ breaking <- function(
5959
# adjust by multiple of average absolute weights
6060
# This avoids problem of getting stuck at a tiny weight
6161
# (and stabilizes the algorithm generally)
62-
# difference to original implementation: adjustment of maximal
62+
# difference to original implementation: adjustment of maximal
6363
# step change of weights to prevent crashing of algorithm
64-
adjustWeights <- function(w, a, target) {
64+
adjustWeights <- function(w, a, target, convergence) {
6565
# OPTION: avoid extreme scaling values -> squareroot function
6666
# to buffer strong difference between computed area and target
6767
# and to buffer the global weight increase
6868
# these increase stability but also computation time:
6969
normA <- a / sum(a)
7070
scaling <- ((target - normA) / target)
71-
scaling <- ifelse(scaling < -1, scaling/sqrt(abs(scaling)), scaling)
71+
if (convergence == "slow") {
72+
scaling <- ifelse(scaling < -1, -log10(abs(scaling)), scaling)
73+
} else if (convergence == "intermediate") {
74+
scaling <- ifelse(scaling < -1, -log(abs(scaling)), scaling)
75+
} else if (convergence == "fast") {
76+
scaling <- ifelse(scaling < -1, scaling/sqrt(abs(scaling)), scaling)
77+
}
7278
w + sqrt(mean(abs(w))) * scaling
7379
}
7480

@@ -115,34 +121,52 @@ shiftWeights <- function(s, w) {
115121
# just give up after 'maxIteration's
116122
allocate <- function(
117123
names, s, w, outer, target,
118-
maxIteration,
124+
maxIteration,
119125
error_tol,
120-
debug = FALSE,
126+
convergence,
127+
min_target = 0.01,
128+
debug = FALSE,
121129
debugCell = FALSE)
122130
{
123131
count <- 1
124132
prevError <- 1
125-
133+
134+
# check for extremely small cell size compared to theoretical average
135+
target_fc <- target * length(target)
136+
too_small <- target_fc < min_target
137+
if (any(too_small)) {
138+
message(paste0(
139+
"Found extremely small cell (<", round(min_target * 100, 1), "% of average size);\n",
140+
"inflating cell size to prevent failure when calculating polygons."
141+
))
142+
correction <- ifelse(
143+
too_small,
144+
min_target/length(target),
145+
-min_target/length(target) * sum(too_small)/sum(!too_small)
146+
)
147+
target <- target + correction
148+
}
149+
126150
repeat {
127-
151+
128152
# if all weights are identical the CGAL algorithm often fails
129153
# in this case we introduce a bit of random variation
130154
if (length(unique(w)) == 1) {
131155
w <- w * rnorm(length(w), mean = 1, sd = 0.01)
132156
}
133-
157+
134158
# call to awv function, the additively weighted voronoi tesselation,
135159
# wrapped within a trycatch statement to catch errors and start over
136160
k <- tryCatch(awv(s, w, outer, debug, debugCell),
137-
error = function(e) { print(e); NULL}
161+
error = function(e) { message(e); NULL}
138162
)
139163
if (is.null(k)) {
140164
return(NULL)
141165
}
142166
areas <- lapply(k, sf::st_area)
143-
167+
144168
# if debug=TRUE, every iteration is drawn to the viewport
145-
# this can be very time and resource consuming and should be used
169+
# this can be very time and resource consuming and should be used
146170
# with care. The result resembles the final treemap but is an overlay of
147171
# many iterations
148172
if (debug) {
@@ -156,7 +180,7 @@ allocate <- function(
156180
lwd = 2, col = grey(0.5),
157181
fill = grey(1, alpha=0.33)
158182
)
159-
183+
160184
info <-
161185
rbind(
162186
area = round(unlist(areas) / sum(unlist(areas)), 4),
@@ -167,31 +191,31 @@ allocate <- function(
167191
colnames(info) <- names
168192
print(info)
169193
}
170-
194+
171195
stop_cond <- breaking(
172-
unlist(areas),
173-
target,
196+
unlist(areas),
197+
target,
174198
debug = debug,
175199
error_tol = error_tol,
176200
prevError = prevError)
177-
201+
178202
# if stop condition is fulfilled, return result in form of
179203
# list of polygons and metadata
180204
if (count == maxIteration || stop_cond$stopping) {
181-
205+
182206
res <- lapply(1:length(names), function(i) {
183207
list(
184208
name = names[i], poly = k[[i]],
185-
site = c(s$x[[i]], s$y[[i]]),
186-
weight = w[i], area = unlist(areas)[i],
209+
site = c(s$x[[i]], s$y[[i]]),
210+
weight = w[i], area = unlist(areas)[i],
187211
target = target[i],
188212
count = count)
189213
}) %>% setNames(names)
190214
return(res)
191-
215+
192216
} else {
193-
194-
w <- adjustWeights(w, unlist(areas), target)
217+
218+
w <- adjustWeights(w, unlist(areas), target, convergence)
195219
s <- shiftSites(s, k)
196220
w <- shiftWeights(s, w)
197221
}

R/drawTreemap.R

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
#' Default is to use the lowest level cells for Voronoi treemaps and all levels
2424
#' for sunburst treemaps.
2525
#' @param color_palette (character) A character vector of colors used to fill cells.
26-
#' The default is to use \code{\link{rainbow_hcl}} from package \code{colorspace}
26+
#' The default is to use \code{\link[colorspace]{rainbow_hcl}} from
2727
#' @param border_level (numeric) A numeric vector representing the hierarchical level that should be
2828
#' used for drawing cell borders, or NULL to omit drawing borders, The default is
2929
#' that all borders are drawn.

R/tesselation.R

Lines changed: 30 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
# Generate one iteration of the Additively Weighted Voronoi diagram
1212
awv <- function(
1313
s, w, region, debug = FALSE,
14-
debugCell = FALSE)
14+
debugCell = FALSE)
1515
{
1616
# combine X, Y coordinates and weights as input for
1717
# C++ tesselation function
@@ -30,27 +30,29 @@ awv <- function(
3030

3131
tidyCell <-
3232
function(cell, tolerance) {
33-
33+
3434
# if cell touches the border at two points, we need to close it
3535
# this is not necessary if cell touches border at 4 points (like a stripe)
3636
if (sum(
37-
cell$border$x %in% c(4000, -4000),
37+
cell$border$x %in% c(4000, -4000),
3838
cell$border$y %in% c(4000, -4000)) == 2
3939
) {
40-
41-
closeCell(cell$border, cell$vertex, tol = tolerance)
42-
40+
poly <- closeCell(cell$border, cell$vertex, tol = tolerance)
4341
} else {
44-
4542
# return a list of the polygon
46-
list(
43+
poly <- list(
4744
x = cell$border$x,
4845
y = cell$border$y,
4946
end = "boundary"
5047
)
51-
48+
if (sf::st_is_valid(convertCell(poly[1:2]))) {
49+
return(poly)
50+
} else {
51+
message("Found invalid polygon (self-intersection)")
52+
return(NULL)
53+
}
5254
}
53-
55+
5456
}
5557

5658
# SIDES
@@ -97,7 +99,7 @@ antiSide <- function(corner) {
9799
closeClock <- function(x, y, start, end, scale = 2000) {
98100
cornerX <- c(-2 * scale, 2 * scale, 2 * scale,-2 * scale)
99101
cornerY <- c(2 * scale, 2 * scale,-2 * scale,-2 * scale)
100-
102+
101103
side <- end
102104
repeat {
103105
corner <- clockCorner(side)
@@ -116,7 +118,7 @@ closeClock <- function(x, y, start, end, scale = 2000) {
116118
closeAnti <- function(x, y, start, end, scale = 2000) {
117119
cornerX <- c(-2 * scale, 2 * scale, 2 * scale,-2 * scale)
118120
cornerY <- c(2 * scale, 2 * scale,-2 * scale,-2 * scale)
119-
121+
120122
side <- end
121123
repeat {
122124
corner <- antiCorner(side)
@@ -155,40 +157,36 @@ closeCell <- function(cell, vertex, tol, scale = 2000) {
155157
# If not, do second one (and check that vertex is "inside" that result!)
156158
x <- cell$x
157159
y <- cell$y
158-
160+
159161
# ASSUME that both first and last vertices are on boundary!
160162
N <- length(x)
161163
startSide <- side(x[1], y[1])
162164
endSide <- side(x[N], y[N])
163-
165+
164166
# exit if not both end points lie on boundary
165167
if (length(startSide) != 1 | length(endSide) != 1) {
166168
return(NULL)
167169
}
168-
170+
169171
# Start and end on same side
170172
if (startSide == endSide) {
171-
172-
cell <- list(x = x, y = y)
173+
173174
if (sp::point.in.polygon(vertex[1], vertex[2],
174-
cell$x, cell$y) == 0) {
175+
cell$x, cell$y) == 0) {
175176
boundRect <- to_sfpoly(list(
176177
x = c(-2 * scale,-2 * scale, 2 * scale, 2 * scale),
177178
y = c(-2 * scale, 2 * scale, 2 * scale,-2 * scale)
178179
))
179180
# "Subtract" smallCell from bound rect to get largeCell
180181
cellPoly <- to_sfpoly(cell)
181182
cellPoly <- sf::st_difference(boundRect, cellPoly)
182-
183-
pts <- to_coords(cellPoly)
183+
cell <- to_coords(cellPoly)
184184
if (sp::point.in.polygon(vertex[1], vertex[2],
185-
cell$x, cell$y) == 0) {
185+
cell$x, cell$y) == 0) {
186186
stop("Failed to close cell")
187187
}
188188
}
189-
190189
} else {
191-
192190
cell <- closeClock(x, y, startSide, endSide)
193191
if (sp::point.in.polygon(vertex[1], vertex[2],
194192
cell$x, cell$y) == 0) {
@@ -222,6 +220,10 @@ trimCells <- function(cells, region) {
222220
if (inherits(poly, "MULTIPOLYGON")) {
223221
poly <- suppressWarnings(sf::st_cast(poly, to = "POLYGON"))
224222
}
223+
if (inherits(poly, "GEOMETRYCOLLECTION")) {
224+
valid <- which(sapply(poly, function(x) inherits(x, "POLYGON")))[1]
225+
poly <- poly[[valid]]
226+
}
225227
poly
226228
})
227229
}
@@ -235,21 +237,21 @@ samplePoints <- function(ParentPoly, n, seed, positioning) {
235237
if (!is.null(seed)) {
236238
set.seed(seed)
237239
}
238-
239-
# This loop keeps repeating until the correct number of coordinates
240+
241+
# This loop keeps repeating until the correct number of coordinates
240242
# is sampled. The reason is that sp::spsample() does not always sample
241243
# the correct number of coordinates, but too few or too many
242244
repeat {
243-
245+
244246
sampled <- tryCatch({
245247
points <- sp::spsample(
246248
sp::Polygon(coords = ParentPoly),
247-
n = n,
249+
n = n,
248250
type = ifelse(positioning == "random", "random", "nonaligned")
249251
)
250252
points@coords}, error = function(e) NULL
251253
)
252-
254+
253255
if (is.null(sampled) || nrow(sampled) != n) {
254256
next
255257
} else {

R/voronoiTreemap.R

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,13 @@
4949
#' area. The default is 0.01 (or 1 \%) of the total parental area. Note: this
5050
#' is is different from a relative per-cell error, where 1 \% would be more
5151
#' strict.
52+
#' @param convergence (character) One of "slow", "intermediate", or "fast".
53+
#' Intermediate (default) and fast try to adjust cell weights stronger such
54+
#' that the algorithm converges faster towards the final size of the cell.
55+
#' However this comes at the price of stability, with a larger number of
56+
#' polygons possibly being misformed, e.g. by having self-intersections.
57+
#' Set convergence to "slow" if you experience problems to calculate treemaps
58+
#' with very unequal cell sizes or very large treemaps.
5259
#' @param seed (integer) The default seed is NULL, which will lead to a new
5360
#' random sampling of cell coordinates for each tesselation. If you want
5461
#' a reproducible arrangement of cells, set seed to an arbitrary number.
@@ -144,6 +151,7 @@ voronoiTreemap <- function(
144151
shape = "rectangle",
145152
maxIteration = 100,
146153
error_tol = 0.01,
154+
convergence = "intermediate",
147155
seed = NULL,
148156
positioning = "regular",
149157
verbose = FALSE,
@@ -305,6 +313,7 @@ voronoiTreemap <- function(
305313
target = weights,
306314
maxIteration = maxIteration,
307315
error_tol = error_tol,
316+
convergence = convergence,
308317
outer = sfpoly,
309318
debug = debug
310319
)

0 commit comments

Comments
 (0)