# C[omp]ute

Welcome to my blog, which was once a mailing list of the same name and is still generated by mail. Please reply via the "comment" links.

Always interested in offers/projects/new ideas. Eclectic experience in fields like: numerical computing; Python web; Java enterprise; functional languages; GPGPU; SQL databases; etc. Based in Santiago, Chile; telecommute worldwide. CV; email.

© 2006-2015 Andrew Cooke (site) / post authors (content).

## Auto-Scaling Date Axes in Python

From: andrew cooke <andrew@...>

Date: Wed, 28 Jul 2010 10:16:53 -0400

There's a nice algorithm for auto-scaling axes, called the "nice number
algorithm", written by Paul Heckbert and published in "Graphics Gems" -

The routines below implement this, but are parameterised over the number base
used, so can also be used for axes based on units that repeat over multiples
of 12, 60, or any other value.

from calendar import timegm
from math import floor, log, log10, ceil
from time import gmtime

# These allow the use with base 10, 12 and 60:
LIM10 = (10, [(1.5, 1), (3, 2), (7, 5)], [1, 2, 5])
LIM12 = (12, [(1.5, 1), (3, 2), (8, 6)], [1, 2, 6])
LIM60 = (60, [(1.5, 1), (20, 15), (40, 30)], [1, 15, 40])

def heckbert_d(lo, hi, ntick=5, limits=None):
'''
Calculate the step size.
'''
if limits is None:
limits = LIM10
(base, rfs, fs) = limits
def nicenum(x, round):
step = base ** floor(log(x)/log(base))
f = float(x) / step
nf = base
if round:
for (a, b) in rfs:
if f < a:
nf = b
break
else:
for a in fs:
if f <= a:
nf = a
break
return nf * step
delta = nicenum(hi-lo, False)
return nicenum(delta / (ntick-1), True)

def heckbert(lo, hi, ntick=5, limits=None):
'''
Calculate the axes lables.
'''
def _heckbert():
d = heckbert_d(lo, hi, ntick=ntick, limits=limits)
graphlo = floor(lo / d) * d
graphhi = ceil(hi / d) * d
fmt = '%' + '.%df' %  max(-floor(log10(d)), 0)
value = graphlo
while value < graphhi + 0.5*d:
yield fmt % value
value += d
return list(_heckbert())

This can then be used with a range of seconds as follows:

def autoscale_time(start, end):
'''
Yields a sequence of epochs that are nicely spaced.

start and end are Unix epochs.
'''
time_chunks = [('days', 3 * 24 * 60 * 60, 24 * 60 * 60, 2, None),
('hours', 3 * 60 * 60, 60 * 60, 3, LIM12),
('minutes', 3 * 60, 60, 4, LIM60),
('seconds', 0, 1, 5, LIM60)]
for (name, limit, secs, sindex, limits) in self.time_chunks:
if (end - start) > limit:
break
d = heckbert_d(start / secs, end / secs, limits=limits)

# zero out lower steps, so that we get a starting date that's an
# integral number of units
stime = list(gmtime(start))
for i in range(sindex, 9):
stime[i] = 0

# generate a sequence of epochs (cannot use the usual heckbert routine
# because formatting will be different)
value = timegm(stime)
while value <= end:
if value >= start:
yield value
value += d * secs

This could be extended further by:

- having different formats in the time_chunks parameter, so that different
intervals are formatted differently

- adding months etc.  This would require changing the "secs" increment to be a
timedelta and working with datetime instances rather than epochs (because
months are not all equally sized).

NOTE: The code above is cut + pasted from some working code and is not tested
in its existing form; I may have introduced a bug somewhere, but hopefully
this illustrates the idea.

Andrew