Skip to content

Commit

Permalink
Add non-overlapping text labels
Browse files Browse the repository at this point in the history
  • Loading branch information
nfrerebeau committed Sep 20, 2023
1 parent d8269c9 commit 53400df
Show file tree
Hide file tree
Showing 19 changed files with 244 additions and 10 deletions.
2 changes: 2 additions & 0 deletions DESCRIPTION
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ Imports:
graphics,
grDevices,
methods,
stats,
utils
Suggests:
akima,
Expand All @@ -50,6 +51,7 @@ Collate:
'ternary_ellipse.R'
'ternary_grid.R'
'ternary_hull.R'
'ternary_labels.R'
'ternary_lines.R'
'ternary_mean.R'
'ternary_pairs.R'
Expand Down
3 changes: 3 additions & 0 deletions NAMESPACE
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ exportMethods(ternary_crosshairs)
exportMethods(ternary_density)
exportMethods(ternary_ellipse)
exportMethods(ternary_hull)
exportMethods(ternary_labels)
exportMethods(ternary_lines)
exportMethods(ternary_mean)
exportMethods(ternary_pairs)
Expand Down Expand Up @@ -56,4 +57,6 @@ importFrom(graphics,text)
importFrom(methods,.valueClassTest)
importFrom(methods,setGeneric)
importFrom(methods,setMethod)
importFrom(stats,as.dist)
importFrom(stats,optim)
importFrom(utils,combn)
3 changes: 3 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
# isopleuros 1.0.0.9000
## New classes and methods
* Add `ternary_labels()` to draw non-overlapping text labels.

## Internals
* Use **tinytest** and **tinysnapshot** instead of **testthat** and **vdiffr**.

Expand Down
32 changes: 28 additions & 4 deletions R/AllGenerics.R
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ setGeneric(
#' arguments to this function.
#' @return
#' `ternary_plot()` is called it for its side-effects: it results in a graphic
#' being displayed.
#' being displayed. Invisibly returns `x`.
#' @example inst/examples/ex-plot.R
#' @author N. Frerebeau
#' @docType methods
Expand Down Expand Up @@ -194,10 +194,10 @@ NULL
#' the column to be used as the third part of the ternary plots. If `NULL`
#' (the default), marginal compositions will be used (i.e. the geometric mean
#' of the non-selected parts).
#' @param ... Further arguments to be passed to [graphics::arrows()].
#' @param ... Further [graphical parameters][graphics::par()].
#' @return
#' `ternary_pairs()` is called it for its side-effects.
#' @seealso [graphics::arrows()]
#' `ternary_pairs()` is called it for its side-effects: it results in a graphic
#' being displayed. Invisibly returns `x`.
#' @example inst/examples/ex-pairs.R
#' @author N. Frerebeau
#' @docType methods
Expand Down Expand Up @@ -366,6 +366,30 @@ setGeneric(
def = function(x, y, z, ...) standardGeneric("ternary_text")
)

#' Non-Overlapping Text Labels
#'
#' Optimize the location of text labels to minimize overplotting text.
#' @param x,y,z A [`numeric`] vector giving the x, y and z ternary coordinates
#' of a set of points. If `y` and `z` are missing, an attempt is made to
#' interpret `x` in a suitable way (see [grDevices::xyz.coords()]).
#' @param labels A [`character`] vector or [`expression`] specifying the text
#' to be written.
#' @param ... Further graphical parameters (see [graphics::par()]) may also be
#' supplied as arguments, particularly, character expansion, `cex` and
#' color, `col`.
#' @return
#' `ternary_labels()` is called it for its side-effects.
#' @seealso [graphics::text()]
#' @example inst/examples/ex-labels.R
#' @author N. Frerebeau
#' @docType methods
#' @family geometries
#' @aliases ternary_labels-method
setGeneric(
name = "ternary_labels",
def = function(x, y, z, ...) standardGeneric("ternary_labels")
)

# Statistics ===================================================================
## Ellipse ---------------------------------------------------------------------
#' Add an Ellipse to a Ternary Plot
Expand Down
4 changes: 3 additions & 1 deletion R/isopleuros-package.R
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,11 @@
"_PACKAGE"

#' @importFrom graphics arrows layout lines par points polygon segments
#' strheight strwidth text plot plot.default plot.new plot.window rasterImage
#' strheight strwidth text par plot plot.default plot.new plot.window
#' rasterImage
#' @importFrom grDevices as.graphicsAnnot as.raster chull colorRampPalette
#' contourLines dev.flush dev.hold hcl.colors xyz.coords
#' @importFrom methods setGeneric setMethod .valueClassTest
#' @importFrom utils combn
#' @importFrom stats as.dist optim
NULL
114 changes: 114 additions & 0 deletions R/ternary_labels.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
# TERNARY LABELS
#' @include AllGenerics.R
NULL

#' @export
#' @rdname ternary_labels
#' @aliases ternary_labels,numeric,numeric,numeric-method
setMethod(
f = "ternary_labels",
signature = c(x = "numeric", y = "numeric", z = "numeric"),
definition = function(x, y, z, labels = seq_along(x), ...) {
coords <- coordinates_ternary(x, y, z)

## Compute label positions
labs <- compute_labels(x = coords$x, y = coords$y, labels = labels, ...)
xt <- labs$x
yt <- labs$y
wt <- labs$width
ht <- labs$height

shadowtext(x = xt, y = yt, labels = labels, ...)
invisible(data.frame(x = x, y = y, z = z))
}
)

#' @export
#' @rdname ternary_labels
#' @aliases ternary_labels,ANY,missing,missing-method
setMethod(
f = "ternary_labels",
signature = c(x = "ANY", y = "missing", z = "missing"),
definition = function(x, labels = seq_along(x$x), ...) {
x <- grDevices::xyz.coords(x)
methods::callGeneric(x = x$x, y = x$y, z = x$z, labels = labels, ...)
}
)

# Adapted from vegan::ordipointlabel() by Jari Oksanen
compute_labels <- function(x, y, labels, cex = graphics::par("cex"),
font = graphics::par("font")) {
xy <- cbind.data.frame(x, y)

em <- graphics::strwidth("m", cex = min(cex))
ex <- graphics::strheight("x", cex = min(cex))
ltr <- em * ex

width <- graphics::strwidth(labels, cex = cex, font = font) + em
height <- graphics::strheight(labels, cex = cex, font = font) + ex
box <- cbind.data.frame(width, height)

makeoff <- function(pos, lab) {
cbind(
c(0, 1, 0, -1, 0.9, 0.9, -0.9, -0.9)[pos] * lab[, 1] / 2,
c(1, 0, -1, 0, 0.8, -0.8, -0.8, 0.8)[pos] * lab[, 2] / 2
)
}
overlap <- function(xy1, off1, xy2, off2) {
pmax(0, pmin(xy1[, 1] + off1[, 1]/2, xy2[, 1] + off2[, 1]/2) -
pmax(xy1[, 1] - off1[, 1]/2, xy2[, 1] - off2[, 1]/2)) *
pmax(0, pmin(xy1[, 2] + off1[, 2]/2, xy2[, 2] + off2[, 2]/2) -
pmax(xy1[, 2] - off1[, 2]/2, xy2[, 2] - off2[, 2]/2))
}

n <- nrow(xy)
j <- as.vector(stats::as.dist(row(matrix(0, n, n))))
k <- as.vector(stats::as.dist(col(matrix(0, n, n))))

maylap <- overlap(xy[j, ], 2 * box[j, ], xy[k, ], 2 * box[k, ]) > 0
j <- j[maylap]
k <- k[maylap]
jk <- sort(unique(c(j, k)))

nit <- min(48 * length(jk), 10000)
pos <- rep(1, n)

## Simulated annealing
fn <- function(pos) {
off <- makeoff(pos, box)
val <- sum(overlap(xy[j, ] + off[j, ], box[j, ], xy[k, ] + off[k, ], box[k, ]))
val <- val / ltr + sum(pos > 1) * 0.1 + sum(pos > 4) * 0.1
}
gr <- function(pos) {
take <- sample(jk, 1)
pos[take] <- sample((1:8)[-pos[take]], 1)
pos
}
sol <- stats::optim(par = pos, fn = fn, gr = gr, method = "SANN",
control = list(maxit = nit))

coord <- xy + makeoff(sol$par, box)
coord$width <- width
coord$height <- height
coord
}

shadowtext <- function(x, y, labels, ...,
theta = seq(0, 2 * pi, length.out = 50),
r = 0.1,
cex = graphics::par("cex"),
col = graphics::par("fg"),
bg = graphics::par("bg"),
font = graphics::par("font"),
xpd = TRUE) {

xo <- r * graphics::strwidth("A", cex = cex, font = font, ...)
yo <- r * graphics::strheight("A", cex = cex, font = font, ...)

for (i in theta) {
graphics::text(x + cos(i) * xo, y + sin(i) * yo, labels,
col = bg, cex = cex, font = font, xpd = xpd)
}

graphics::text(x, y, labels, col = col, cex = cex, font = font, xpd = xpd)
}
4 changes: 4 additions & 0 deletions R/ternary_pairs.R
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ setMethod(
graphics::text(x = 0.5, y = 0.5, labels = part,
cex = cex.lab, col = col.lab, font = font.lab)
}

invisible(x)
}
)

Expand All @@ -73,5 +75,7 @@ setMethod(
definition = function(x, margin = NULL, ...) {
x <- data.matrix(x)
methods::callGeneric(x = x, margin = margin, ...)

invisible(x)
}
)
2 changes: 2 additions & 0 deletions R/ternary_plot.R
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,8 @@ setMethod(
panel.last = panel.last,
...
)

invisible(x)
}
)

Expand Down
11 changes: 11 additions & 0 deletions inst/examples/ex-labels.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
## Compositional data
coda <- data.frame(
X = c(41.0, 40, 39.0),
Y = c(19.5, 20, 20.5),
Z = c(39.5, 40, 40.5)
)

## Add text
ternary_plot(NULL, panel.first = ternary_grid())
ternary_points(coda)
ternary_labels(coda, labels = c("A", "B", "C"))
1 change: 1 addition & 0 deletions man/ternary_arrows.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions man/ternary_crosshairs.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

63 changes: 63 additions & 0 deletions man/ternary_labels.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions man/ternary_lines.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 3 additions & 4 deletions man/ternary_pairs.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion man/ternary_plot.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions man/ternary_points.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions man/ternary_polygon.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit 53400df

Please sign in to comment.