[Matrix-SIG] algorithm for intelligent axis scaling?

David Ascher da@ski.org
Thu, 18 Mar 1999 13:13:35 -0800 (Pacific Standard Time)


> This is not directly a NumPy question, but I figured that the best answer
> might come from this group. :) I'm looking for a simple algorithm for
> generating intelligent axis scalings for a graph. Basically what I need is
> the axis minimum, axis maximum, and step length between ticks given the
> minimum data value, maximum data value, and suggested number of ticks.
> By intelligent, I mean reasonably intuitive rounding for the axis labels!

I've found it surprisingly difficult.  The routine I use in Snow
(http://starship.python.net:9673/crew/da/Snow) is:

def format(v):
    if int(v) == v:
	return `int(v)`
    else:
	return `v`

def myticks(self, Min, Max, log, fitMin=1, fitMax=1):
	r = [] ; l = []
	# what's the tick seperation: if min is .2, then it's .1
	# if it's .002, then it's 0.001.
	if log:
	    startscale = floor(log10(Min))
	    endscale = floor(log10(Max))

	    candidates = []
	    for x in arange(startscale, endscale+1):
		candidates = candidates + (arange(1,10)*power(10,x)).tolist()

	    valid = nonzero(logical_and(greater(candidates, Min),
					less(candidates, Max)))
	    doable = candidates[valid[0]-1:valid[-1]+2]
	    for v in doable:
		if v/power(10,floor(log10(v))) == 1.0:
		    r.append((v,0))
		elif v/power(10,floor(log10(v))) == 5.0:
		    r.append((v,1))
		else:
		    r.append((v,2))
		if  (v/power(10, floor(log10(v))) in (1.0, 5.0)): 
		    l.append((v, None, format(v)))

		# enable following for ticks at startat and endat
		if 0 and (doable[0], doable[-1]):
		    l.append((v, None, format(v)))
	    return doable[0], r, l, doable[-1]
	else:  # linear case
	    d = abs(Max - Min)
	    epsilon = 1e-10
	    s = power(10,floor(log10(d))-1) 
	    if fitMin:
		startat = Min - (Min % (s * 10))
	    else:
		startat = Min
	    overmax = Max + (s * 10)
	    if fitMax:
		endat = overmax - (overmax % (s * 10))
	    else:
		endat = Max
	    if d / (10*s) > 5.2:  # magic!
		             # small tickmarks at unit factors
		s = s * 10   # we need to upshift the scale
		maj_mod = 10 # major tick marks at factors of 10
		int_mod = 2  # intermediate tick marks at factors of 2
	    else: 
		maj_mod = 10 # major tick marks at factors of 10
		int_mod = 5  # minors at factors of 5
		             # smalls at factors of 1
	    r = [] ; l = []
	    nummajors = 0 # for debugging
	    numints = 0
	    for x in arange(startat, endat+epsilon, s):
		if (abs((x / s) % maj_mod) < epsilon): 
                    x = round(x, 4)
		    r.append((x,0))
		    l.append((x, None, format(x)))
		    nummajors = nummajors + 1
		elif (abs((x / s) % int_mod) < epsilon): 
                    x = round(x, 4)
		    r.append((x,1))
		    l.append((x, None, format(x)))
		    numints = numints + 1
		else:
                    x = round(x, 4)
		    r.append((x,2))
	    return startat, r, l, endat


It's a hack, but it's neater than the code in gist. =) It's also highly
undocumented.  Playing with it however should make it relatively clear
what the return values are.

--david