FWIW, I've attached the R code that I'm using to choose ticks and tick
labels. The algorithms are sound, but the lack of context means that
I have to make assumptions about the text size and available space.
Examples:
> source('axis.R')
> axis.ticks.generate(1:100)
[1] 0 10 20 30 40 50 60 70 80 90 100
> axis.ticks.generate(1:100, log=TRUE)
[1] 1.0 1.8 3.0 6.0 10.0 18.0 32.0 56.0 100.0
> axis.labels.select(axis.ticks.generate(1:100))
[1] "0" "10" "20" "30" "40" "50" "60" "70" "80" "90" "100"
> axis.labels.select(axis.ticks.generate(1:100), available.space=20)
[1] "0" "" "20" "" "40" "" "60" "" "80" "" "100"
Perhaps this code will be useful to someone.
Neal
make.multiple.of <- function(x, multiple,
towards.zero=FALSE,
away.from.zero=FALSE,
towards.positive.infinity=FALSE,
towards.negative.infinity=FALSE) {
if (sum(sapply(list(towards.zero,
away.from.zero,
towards.positive.infinity,
towards.negative.infinity),
function (x) identical(x, FALSE)))
!= 3) {
browser()
stop("You must set exactly one of towards.zero, away.from.zero,
towards.positive.infinity or towards.negative.infinity to TRUE")
}
if (towards.zero)
return (sign(x) * (abs(x) - abs(x) %% multiple))
if (away.from.zero)
return (sign(x) * (abs(x) + (-abs(x) %% multiple)))
if (towards.positive.infinity) {
mask = x < 0
} else {
stopifnot(towards.negative.infinity)
mask = x > 0
}
x[mask] = make.multiple.of(x[mask], multiple=multiple, towards.zero=TRUE)
x[!mask] = make.multiple.of(x[!mask], multiple=multiple, away.from.zero=TRUE)
return (x)
}
axis.ticks.generate <- function(values,
log=FALSE,
ticks=10,
verbose=FALSE) {
if (log)
values = base::log(values, 10)
# Compute the difference between the extremes.
min.value = min(values)
max.value = max(values)
value.range = max.value - min.value
if (value.range > 10 && log) {
min.value = floor(min.value)
max.value = ceiling(max.value)
value.range = max.value - min.value
}
orders.of.magnitude = max(floor(log(abs(value.range), base=10)))
if (verbose)
cat(sprintf("%g - %g => width of %g (spans %d orders of magnitude)\n",
min.value, max.value, value.range, orders.of.magnitude))
generate.ticks <- function(step.size.multiple) {
step.size = step.size.multiple * 10^(orders.of.magnitude - 1)
min.axis = make.multiple.of(min.value, step.size,
towards.negative.infinity=TRUE)
max.axis = make.multiple.of(max.value, step.size,
towards.positive.infinity=TRUE)
ticks = seq(min.axis, max.axis, step.size)
if (verbose)
cat(sprintf("Step size of %g (multiple: %g) results in %d ticks:\n %s\n",
step.size, step.size.multiple, length(ticks),
paste(sprintf("%g", ticks), collapse=", ")))
# Trim the ticks, if appropriate.
# In steps.
max.margin = 0.5
separation = (min.value - ticks[1]) / step.size
if (separation > max.margin) {
if (verbose)
cat(sprintf("trimming left: (%g of a step >= %g of a step)\n",
separation, max.margin))
ticks[1] = min.value
}
separation = (ticks[length(ticks)] - max.value) / step.size
if (separation > max.margin) {
if (verbose)
cat(sprintf("trimming right: (%g of a step >= %g of a step)\n",
separation, max.margin))
ticks[length(ticks)] = max.value
}
return (ticks)
}
# Choose the step size that is as close to 10 ticks as possible.
step.size.multiples = c(1, 1.25, 2.5, 5, 10)
# We prefer multiples of 10.
step.size.penalties = c(0, 2, 1.5, 1, 0)
step.size.multiple = step.size.multiples[
which.min(abs(ticks
- (sapply(step.size.multiples,
function (m) {
length(generate.ticks(m))
})
+ step.size.penalties)))]
if (verbose)
cat(sprintf("Optimal step size multiple for %d ticks is %g\n",
ticks, step.size.multiple))
ticks = generate.ticks(step.size.multiple)
if (log) {
ticks = 10^ticks
if (10^value.range < 2)
ticks = signif(ticks, 2)
else {
small = abs(ticks) < 2
large = abs(ticks) >= 100
medium = !(small | large)
ticks[small] = round(ticks[small], 1)
ticks[medium] = round(ticks[medium])
ticks[large] = signif(ticks[large], 2)
ticks = unique(ticks)
}
}
return (ticks)
}
axis.labels.select <- function(ticks, labels=NULL,
axis='x',
available.space=NULL,
log=FALSE, log.base=NULL,
verbose=FALSE) {
stopifnot(is.numeric(ticks))
if (length(ticks) <= 1)
return (as.character(ticks))
if (is.null(labels))
labels = ticks
if (is.numeric(labels)) {
if (max(abs(ticks)) >= 10)
# We have single and double digit numbers. Make the single
# digit numbers single digit and the wider numbers significant
# to 2 places.
labels = ifelse(abs(ticks) >= 10, signif(ticks, 2), round(ticks))
else
# Use 2 significant digits, except for the last tick.
labels = signif(ticks, 2)
labels[length(labels)] = ticks[length(ticks)]
labels = sprintf("%g", labels)
}
stopifnot(length(ticks) == length(labels))
stopifnot(is.character(labels))
if (log) {
if (is.null(log.base))
log = base::log
else {
log = function(x) { base::log(x, base=log.base) }
exp = function(x) { log.base^x }
}
} else {
log = identity
exp = identity
}
stopifnot(axis %in% c('x', 'y'))
# Assuming labels are evenly spaced:
#
# If all labels are 2 characters wide, we can have 11 labels without
# overlap. That's 22 characters plus 10 inter-label spaces.
#
# If all labels are 3 characters, then we can have 8 labels
# without overlap. That's 24 characters plus 7 interlabel spaces.
#
# If all labels are 4 characters, then we can have 6 labels
# without overlap. That's 24 characters plus 5 interlabel spaces.
#
# Thus, we have space for about 37 characters (digits or spaces).
#
# This means that we can have a density of one character every
# 1/37th of the x-axis.
if (is.null(available.space))
available.space = if (axis == 'x') 37 else 20
# Here's our approach: for each pair of adjacent, non-empty
# labels, we compute the number of characters from the middle of
# the left label to the middle of the right label and the amount
# of available space. If the character density exceeds the
# threshold, then we kill one of the labels.
# Compute the position of each tick on a linear scale from 0 to 1.
pos = (log(ticks) - log(ticks[1])) / (log(ticks[length(ticks)]) -
log(ticks[1]))
label.space <- function(label) {
if (axis == 'y')
# XXX: It's hard to compute the height. Most likely, it is just
# one, however.
return (1)
label = gsub("[\\][a-zA-Z]*", "x", label)
label = gsub("$", "", label, fixed=TRUE)
return (nchar(label)
# Count punctuation as less than a character.
# - .25 * sapply(strsplit(label, '[.,]'), length)
)
}
stopifnot(length(ticks) >= 2)
for (i in 2:(length(ticks))) {
# Find the first label (largest x, such that x <= i - 1) that
# is not the empty string.
not.empty = which(labels[(i - 1):1] != "")
if (length(not.empty) == 0)
next
left = ((i - 1):1)[not.empty[1]]
right = i
if (labels[right] == "")
next
used.chars =
(sum(label.space(labels[c(left, right)])) / 2
# The interlabel space.
+ 1)
space = pos[right] - pos[left]
available.chars = available.space * space
if (verbose)
cat(sprintf("Considering %s: space: %.2g => %.1f characters; have %g
characters.\n",
paste(sprintf("'%s'", labels[c(left, right)]),
collapse=", "),
space, available.chars, used.chars))
if (used.chars > available.chars) {
# Kill the current label, unless this is the last label
# (which we always want to keep).
kill = if (right == length(ticks)) left else right
if (verbose)
cat(sprintf("Killing '%s' (index: %d)\n", labels[kill], kill))
labels[kill] = ""
}
}
labels
}
------------------------------------------------------------------------------
Everyone hates slow websites. So do we.
Make your web apps faster with AppDynamics
Download AppDynamics Lite for free today:
http://p.sf.net/sfu/appdyn_d2d_mar
_______________________________________________
Pgfplots-features mailing list
[email protected]
https://lists.sourceforge.net/lists/listinfo/pgfplots-features