#################################################################### ## ## ## FUNCTION FOR DETERMINING LABEL POSITIONS ## ## ## #################################################################### ### The following function tries to position the labels in ### a nice way. ### (1) First we calculate the centroids of all the ### top level polygons. Based on the median x-coordinate we ### decide which polygons will have a RHS label and which ### will have a LHS label. Our starting y coordinates for ### the labels are taken as the y coordinates of the polygon ### centroids (converted into npc, which are the units on ### the left and right viewports). ### (2) We form a series of temporary text grobs that are ### located at the starting coordinates - these are used in ### the calculation of label widths and heights, which is ### done immediately after. The bottom and top y coordinates ### of the text grobs are calculated based on these values. ### (3) Based on a specified value of text padding, we check ### for overlaps. If a pair of labels (with their padding) ### overlap, then they are shifted apart. The direction of ### shift is determined by the bottom y-coordinate of the top ### element of the overlapping pair. If this value is more ### than 0.5, then the top element is shifted up, otherwise ### the bottom element is shifted down. A maximum number of ### iterations are set for this part to avoid infinite loops ### in case something goes wrong (this may not be necessary?) ### (4) After this adjustment, it is possible that labels may ### have been shifted above the top of the device or below the ### bottom of the device. To address this, we calculate the ### maximum allowable spare space between labels (obeying the ### padding requirement). Then we shift labels vertically by ### eliminating the spare space, in an attempt to get all the ### labels back within the edges of the device. ### (5) The final step is to 'centre' the 'block of labels' on ### each side, if the block height is more than 1npc. If that ### is the case, the labels are shifted vertically with a bit ### more emphasis placed on keeping spare room at the bottom, ### since we may have additional text underneath our labels. ### The returned values are used for our 'real' text grobs ### that will appear on our image. positionLabels = function(topPgons, polyVars, cpiData, text.padding.mm = 18) { ## format the labels (split onto multiple lines if long) formatted.strings = formatLabels(cpiData) n.groups = polyVars$n.groups temp.labels = gList() panel = character(n.groups) y.npc = numeric(n.groups) x.main = numeric(n.groups) y.main = numeric(n.groups) for (i in 1:n.groups) { pgonPts = topPgons[[i]]@pts[[1]] centre = TT.polygon.centroids(pgonPts$x, pgonPts$y) x.main[i] = centre["x"] y.main[i] = centre["y"] } middle.x = median(x.main) for (i in 1:n.groups) { x = x.main[i] y = y.main[i] ## need to decide where the lines go (which side) ## want roughly equal number of labels each side panel[i] = ifelse(x > middle.x, "right", "left") y.npc[i] = 0.5 + y / 2000 labelName = paste("label", cpiData$groupNames[i], sep = ".") temp.labels[[labelName]] = makeLabel(formatted.strings[i], panel[i], labelName, unit(y.npc[i], "npc"), temp = TRUE) } text.padding = unit(text.padding.mm, "mm") labels.y.bot = numeric(n.groups) labels.y.top = numeric(n.groups) labels.width.npc = numeric(n.groups) seekViewport("left") for (i in 1:n.groups) { y = unit(y.npc[i], "npc") labels.y.bot[i] = convertHeight(y - 0.5 * text.padding, "npc", valueOnly = TRUE) labels.y.top[i] = convertHeight(y + 0.5 * text.padding + grobHeight(temp.labels[[i]]), "npc", valueOnly = TRUE) labels.width.npc[i] = convertWidth(grobWidth(temp.labels[[i]]), "npc", valueOnly = TRUE) } orig.labels.y.bot = labels.y.bot orig.labels.y.top = labels.y.top ######################################## ### now we test for any overlaps, according to text.padding ### we adjust the labels up and down to get something that ### (roughly) obeys our text padding requirement max.iter = 30 iter = 0 while (TRUE) { overlap = logical(n.groups) is.overlap = vector("list", n.groups) for (i in 1:n.groups) { same.place = labels.y.top[i] == labels.y.top same.place[i] = FALSE bot.below.tops = labels.y.bot[i] - labels.y.top < -1e-6 bot.below.tops[i] = FALSE top.above.tops = labels.y.top[i] - labels.y.top > 1e-6 top.above.tops[i] = FALSE same.panels = panel[i] == panel same.panels[i] = FALSE is.overlap[[i]] = ((bot.below.tops & top.above.tops) | same.place) & same.panels overlap[i] = any(is.overlap[[i]]) } if (any(overlap)) { which.adjust = which(overlap)[1] ## which is the bottom of the two overlapping labels: overlaps.with = which(is.overlap[[which.adjust]])[1] overlaps.by = labels.y.top[overlaps.with] - labels.y.bot[which.adjust] ## shift them apart if (labels.y.bot[which.adjust] > 0.5) { ## shift upwards top.mult = 1 bot.mult = 0 } else { ## shift downwards top.mult = 0 bot.mult = -1 } labels.y.bot[which.adjust] = labels.y.bot[which.adjust] + overlaps.by * top.mult labels.y.top[which.adjust] = labels.y.top[which.adjust] + overlaps.by * top.mult labels.y.bot[overlaps.with] = labels.y.bot[overlaps.with] + overlaps.by * bot.mult labels.y.top[overlaps.with] = labels.y.top[overlaps.with] + overlaps.by * bot.mult ## update iteration number iter = iter + 1 if (iter >= max.iter) break } else { break } ## useful for debugging # print(data.frame(labels.y.bot, labels.y.top, panel)) # cat("upper was:", which.adjust, ", lower was:", # overlaps.with, "\n\n\n") } ### now check labels haven't been bumped down/up off the page ### If they have, we attempt to shift them up/down df = data.frame(labels.y.bot, labels.y.top, panel) for (s in c("left", "right")) { dfi = subset(df, panel == s) dfi = dfi[order(dfi[,1], decreasing = TRUE), ] i.side = as.numeric(rownames(dfi)) nS = nrow(dfi) adjust.down = FALSE adjust.up = FALSE if (dfi[nS, 1] < 0) adjust.up = TRUE if (dfi[1, 2] > 1) { adjust.up = FALSE adjust.down = TRUE } ## check if we are pushed up too far. Push things down if we are if (adjust.down) { extra = dfi[1, 2] - 1 spare.space = numeric(nS) for (i in 1:(nS-1)) spare.space[i] = with(dfi, labels.y.bot[i] - labels.y.top[i + 1]) spare.space[nS] = max(0, dfi$labels.y.bot[nS]) w = which(spare.space > .01) for (i in seq(along = w)) { labels.y.top[i.side[1:w[i]]] = labels.y.top[i.side[1:w[i]]] - min(spare.space[w[i]], extra) labels.y.bot[i.side[1:w[i]]] = labels.y.bot[i.side[1:w[i]]] - min(spare.space[w[i]], extra) extra = extra - spare.space[w[i]] if (extra <= 0 && dfi[nS, 1] < 0) break } } ## check if we are pushed down too far (only if adjust.down ## is false). Push things up if we are if (adjust.up) { extra = -dfi[nS, 1] spare.space = numeric(nS) for (i in nS:2) spare.space[i] = with(dfi, labels.y.bot[i - 1] - labels.y.top[i]) spare.space[1] = max(0, 1 - dfi$labels.y.top[1]) w = which(spare.space > .01) for (i in seq_along(w)) { labels.y.top[i.side[w[i]:nS]] = labels.y.top[i.side[w[i]:nS]] + min(spare.space[w[i]], extra) labels.y.bot[i.side[w[i]:nS]] = labels.y.bot[i.side[w[i]:nS]] + min(spare.space[w[i]], extra) extra = extra - spare.space[w[i]] if (extra <= 0) break } } ## if the vertical range is over 1 npc, take up more of the top ## margin rather than the bottom margin. minmax = range(labels.y.bot[panel == s], labels.y.top[panel == s]) r = diff(minmax) if (r > 1) { newtop = 0.52 * (1 + r) adj = newtop - minmax[2] labels.y.top[i.side] = labels.y.top[i.side] + adj labels.y.bot[i.side] = labels.y.bot[i.side] + adj } if (FALSE) { ## Check if some lines don't need to be bent, and if they don't, ## make them straight. tmp.df = data.frame(labels.y.bot, labels.y.top, panel, y.npc, sideY = y.npc + labels.y.bot - orig.labels.y.bot) tmp2.df = tmp.df[panel == s,] tmp2.df = tmp2.df[order(tmp2.df$sideY),] gapsize = numeric(nrow(tmp2.df)) gapsize[1] = max(0, tmp2.df$labels.y.bot[1]) for (i in 2:nrow(tmp2.df)) gapsize[i] = tmp2.df[i, 1] - tmp2.df[i-1, 2] bendheight = tmp2.df$sideY - tmp2.df$y.npc for (i in seq_along(gapsize)) { if (gapsize[i] > .001 & bendheight[i] > 0) { index = rev(i.side)[i] labels.y.top[index] = labels.y.top[i.side[index]] - bendheight[i] labels.y.bot[index] = labels.y.bot[i.side[index]] - bendheight[i] } } } } list(formatted.strings = formatted.strings, origLabBot = orig.labels.y.bot, origLabTop = orig.labels.y.top, labBot = labels.y.bot, labTop = labels.y.top, centroidX = x.main, centroidY = y.main, centroidYnpc = y.npc, panel = panel) } ################################################################ ## ## ## FUNCTION FOR FORMATTING LABELS ## ## ## ################################################################ ### Due to limited width in the left and right viewports, we need ### to check for long labels and split them onto multiple lines ### if necessary. This function does that, and returns the new ### strings, with '\n' characters inserted where necessary. formatLabels = function(cpiData, max.width.mm = 52) { # label.strings = paste(cpiData$groupNames, # gettextf("%.1f%%", cpiData$cpi.groups)) label.strings = cpiData$groupNames n = length(label.strings) strw = convertWidth(stringWidth(label.strings), "mm", valueOnly = TRUE) for (i in 1:n) { if (strw[i] > max.width.mm) { a = gregexpr(" ", label.strings[i])[[1]] attributes(a) = NULL spaces.mm = a * strw[i] / nchar(label.strings[i]) j = which.max(spaces.mm[spaces.mm <= max.width.mm])[1] ## case where the only space is after max width if (is.na(j) & length(a) > 0) j = a[1] parts = unlist(strsplit(label.strings[i], " ")) parts[j] = paste(parts[j], "\n", parts[j + 1], sep = "") parts = parts[-(j + 1)] label.strings[i] = paste(parts, collapse = " ") } } label.strings }