import math import numpy as np import matplotlib import matplotlib.pyplot as plt import matplotlib.ticker as tckr import matplotlib.transforms as mtransforms import matplotlib.mlab as mlab # An alpha version of the Talbot, Lin, Hanrahan tick mark generator for matplotlib. # Described in "An Extension of Wilkinson's Algorithm for Positioning Tick Labels on Axes" # by Justin Talbot, Sharon Lin, and Pat Hanrahan, InfoVis 2010. # Implementation by Justin Talbot # This implementation is in the public domain. # Report bugs to jtalbot@stanford.edu # A shortcoming: # The weights used in the paper were designed for static plots where the extent of # the tick marks unioned with the extent of the data defines the extent of the plot. # In a plot where the extent of the plot is defined by the user (e.g. an interactive # plot supporting panning and zooming), the weights don't work as well. In particular, # you would want to retune them assuming that the tick labels must be inside # the provided view range. You probably want higher weighting on simplicity and lower # on coverage and possibly density. But I haven't experimented in any detail with this. # # If you do intend on using this for static plots in matplotlib, you should set # only_inside to False in the call to Extended.extended. And then you should # manually set your view extent to include the min and max ticks if they are outside # the data range. This should produce the same results as the paper. class Extended(tckr.Locator): # density is labels per inch def __init__(self, density = 1, steps = None, figure = None, which = 1): """ Keyword args: """ self._density = density self._figure = figure self._which = which if steps is None: self._steps = [1, 5, 2, 2.5, 4, 3] else: self._steps = steps def coverage(self, dmin, dmax, lmin, lmax): range = dmax-dmin return 1 - 0.5 * (math.pow(dmax-lmax, 2)+math.pow(dmin-lmin, 2)) / math.pow(0.1 * range, 2) def coverage_max(self, dmin, dmax, span): range = dmax-dmin if span > range: half = (span-range)/2.0 return 1 - math.pow(half, 2) / math.pow(0.1*range, 2) else: return 1 def density(self, k, m, dmin, dmax, lmin, lmax): r = (k-1.0) / (lmax-lmin) rt = (m-1.0) / (max(lmax, dmax) - min(lmin, dmin)) return 2 - max( r/rt, rt/r ) def density_max(self, k, m): if k >= m: return 2 - (k-1.0)/(m-1.0) else: return 1 def simplicity(self, q, Q, j, lmin, lmax, lstep): eps = 1e-10 n = len(Q) i = Q.index(q)+1 v = 1 if ((lmin % lstep < eps or (lstep - lmin % lstep) < eps) and lmin <= 0 and lmax >= 0) else 0 return (n-i)/(n-1.0) + v - j def simplicity_max(self, q, Q, j): n = len(Q) i = Q.index(q)+1 v = 1 return (n-i)/(n-1.0) + v - j def legibility(self, lmin, lmax, lstep): return 1 def legibility_max(self, lmin, lmax, lstep): return 1 def extended (self, dmin, dmax, m, Q=[1,5,2,2.5,4,3], only_inside=False, w=[0.25,0.2,0.5,0.05]): n = len(Q) best_score = -2.0 j = 1.0 while j < float('infinity'): for q in Q: sm = self.simplicity_max(q, Q, j) if w[0] * sm + w[1] + w[2] + w[3] < best_score: j = float('infinity') break k = 2.0 while k < float('infinity'): dm = self.density_max(k, m) if w[0] * sm + w[1] + w[2] * dm + w[3] < best_score: break delta = (dmax-dmin)/(k+1.0)/j/q z = math.ceil(math.log(delta, 10)) while z < float('infinity'): step = j*q*math.pow(10,z) cm = self.coverage_max(dmin, dmax, step*(k-1.0)) if w[0] * sm + w[1] * cm + w[2] * dm + w[3] < best_score: break min_start = math.floor(dmax/step)*j - (k-1.0)*j max_start = math.ceil(dmin/step)*j if min_start > max_start: z = z+1 break for start in range(int(min_start), int(max_start)+1): lmin = start * (step/j) lmax = lmin + step*(k-1.0) lstep = step s = self.simplicity(q, Q, j, lmin, lmax, lstep) c = self.coverage(dmin, dmax, lmin, lmax) d = self.density(k, m, dmin, dmax, lmin, lmax) l = self.legibility(lmin, lmax, lstep) score = w[0] * s + w[1] * c + w[2] * d + w[3] * l if score > best_score and (not only_inside or (lmin >= dmin and lmax <= dmax)): best_score = score best = (lmin, lmax, lstep, q, k) z = z+1 k = k+1 j = j+1 return best def __call__(self): vmin, vmax = self.axis.get_view_interval() size = self._figure.get_size_inches()[self._which] # density * size gives target number of intervals, # density * size + 1 gives target number of tick marks, # the density function converts this back to a density in data units (not inches) # should probably make this cleaner. best = self.extended(vmin, vmax, self._density * size + 1.0, only_inside=True, w=[0.25, 0.2, 0.5, 0.05]) locs = np.arange(best[4]) * best[2] + best[0] return locs fig = plt.figure() ax = fig.add_subplot(111) ax.plot(10*np.random.randn(100), 10*np.random.randn(100), 'o') xmin, xmax = ax.xaxis.get_data_interval() xrange = xmax-xmin xmin, xmax = (xmin - xrange * 0.05, xmax + xrange * 0.05) ymin, ymax = ax.yaxis.get_data_interval() yrange = ymax-ymin ymin, ymax = (ymin - yrange * 0.05, ymax + yrange * 0.05) ax.xaxis.set_view_interval(xmin, xmax, ignore=True) ax.yaxis.set_view_interval(ymin, ymax, ignore=True) ax.xaxis.set_major_locator(Extended(density=0.5, figure=fig, which=0)) ax.yaxis.set_major_locator(Extended(density=0.5, figure=fig, which=1)) ax.set_title('Talbot, Lin, Hanrahan 2010') plt.show()