X and Y axis settings

This section documents features used for modifying x and y axis settings, including axis scales, tick locations, and tick label formatting. It also documents a handy “dual axes” feature.

Axis tick locations

Axis locators are used to select tick locations based on the axis data limits. ProPlot lets you easily specify axis locators with format (keyword args xlocator, ylocator, xminorlocator, and yminorlocator, or their aliases xticks, yticks, xminorticks, and yminorticks).

Pass a number to tick every N data values, lookup a builtin matplotlib ticker with a string key name, or pass a list of numbers to tick specific locations. I recommend using ProPlot’s arange function to generate lists of ticks – it’s like numpy’s arange, but is endpoint-inclusive, which is usually what you’ll want in this context. See format and Locator for details.

[1]:
import proplot as plot
import numpy as np
plot.rc.facecolor = plot.shade('powder blue', 1.15) # shade makes it a bit brighter, multiplies luminance channel by this much!
plot.rc.update(linewidth=1, small=10, large=12, color='dark blue', suptitlecolor='dark blue')
f, axs = plot.subplots(nrows=5, axwidth=5, aspect=(8,1), share=0, hspace=0.3)
# Manual locations
axs[0].format(xlim=(0,200), xminorlocator=10, xlocator=30, suptitle='Declaring tick locations with ProPlot')
axs[1].format(xlim=(0,10), xlocator=[0,0.3,0.8,1.6,4.4,8,8.8,10], xminorlocator=0.1)
# Locator classes
state = np.random.RandomState(51423)
axs[3].format(xlim=(1,10), xlocator=('maxn',20)) # specify the approx number of ticks you want, but not locations
axs[2].format(xlim=(1,100), xlocator='log', xminorlocator='logminor') # automatically applied for log scale plots
axs[4].plot(np.arange(10)-5, state.rand(10), alpha=0) # index locator, something must be plotted for it to work
axs[4].format(xlim=(0,6), xlocator='index', xformatter=[r'$\alpha$', r'$\beta$', r'$\gamma$', r'$\delta$', r'$\epsilon$', r'$\zeta$', r'$\eta$'])
plot.rc.reset()
_images/axis_4_0.svg

Axis tick labels

Axis formatters are used to convert float numbers to tick label strings. ProPlot lets you easily change the axis formatter with format (keyword args xformatter and yformatter, or their aliases xticklabels and yticklabels). The builtin matplotlib formatters can be referenced by string name, and several new formatters have been introduced – for example, you can now easily label your axes as fractions or as geographic coordinates. You can also just pass a list of strings or a % style format directive. See format and Formatter for details.

[2]:
import proplot as plot
import numpy as np
plot.rc.update(linewidth=1.2, small=10, large=12, facecolor='gray8', figurefacecolor='gray8',
               suptitlecolor='w', gridcolor='w', color='w')
f, axs = plot.subplots(nrows=6, axwidth=5, aspect=(8,1), share=0, hspace=0.3)
# Fraction formatters
axs[0].format(xlim=(0,4*np.pi), xlocator=plot.arange(0, 4, 0.25)*np.pi, xformatter='pi')
axs[1].format(xlim=(0,2*np.e), xlocator=plot.arange(0, 2, 0.5)*np.e, xticklabels='e')
# Geographic formatter
axs[2].format(xlim=(-90,90), xlocator=plot.arange(-90, 90, 30), xformatter='deglat')
# User input labels
axs[3].format(xlim=(-1.01,1), xlocator=0.5, xticklabels=['a', 'b', 'c', 'd', 'e'])
# Custom style labels
axs[4].format(xlim=(0, 0.001), xlocator=0.0001, xformatter='%.E')
axs[5].format(xlim=(0,100), xtickminor=False, xlocator=20, xformatter='{x:.1f}')
axs.format(ylocator='null', suptitle='Setting tick styles with ProPlot')
plot.rc.reset()
_images/axis_7_0.svg

ProPlot also changes the default axis formatter. The new formatter trims trailing zeros by default, and can be used to filter tick labels within some data range, as demonstrated below. See AutoFormatter for details.

[3]:
import proplot as plot
plot.rc.linewidth = 2
plot.rc.small = plot.rc.large = 11
locator = [0, 0.25, 0.5, 0.75, 1]
f, axs = plot.subplots([[1,1,2,2],[0,3,3,0]], axwidth=1.5, share=0)
axs[0].format(xformatter='scalar', yformatter='scalar', title='Matplotlib formatter')
axs[1].format(yticklabelloc='both', title='ProPlot formatter')
axs[:2].format(xlocator=locator, ylocator=locator)
axs[2].format(title='Limiting the tick range', ticklen=5, xlim=(0,5), ylim=(0,5),
              xtickrange=(0,2), ytickrange=(0,2), xlocator=1, ylocator=1)
axs.format(ytickloc='both', yticklabelloc='both', titleweight='bold', titlepad='0.5em')
plot.rc.reset()
_images/axis_9_0.svg

Axis scales

The axis scale can now be changed with format (keyword args xscale and yscale). You can also configure the 'log' and 'symlog' axis scales with the more sensible base, linthresh, linscale, and subs keyword args (i.e. you can omit the x and y). See format, Scale, LogScale and SymmetricalLogScale for details.

[4]:
import proplot as plot
import numpy as np
N = 200
lw = 3
plot.rc.update({'linewidth':1, 'ticklabelweight':'bold', 'axeslabelweight':'bold'})
f, axs = plot.subplots(ncols=2, nrows=2, axwidth=1.8, share=0)
axs.format(suptitle='Changing the axis scale')
# Linear and log scales
axs[0].format(yscale='linear', ylabel='linear scale')
axs[1].format(ylim=(1e-3, 1e3), yscale='log', yscale_kw={'subs':np.arange(1,10)}, ylabel='log scale')
axs[:2].plot(np.linspace(0,1,N), np.linspace(0,1000,N), lw=lw)
# Symlog and logit scales
ax = axs[2]
ax.format(yscale='symlog', yscale_kw={'linthresh':1}, ylabel='symlog scale')
ax.plot(np.linspace(0,1,N), np.linspace(-1000,1000,N), lw=lw)
ax = axs[3]
ax.format(yscale='logit', ylabel='logit scale')
ax.plot(np.linspace(0,1,N), np.linspace(0.01,0.99,N), lw=lw)
plot.rc.reset()
_images/axis_12_0.svg

ProPlot also adds several new axis scales. The 'cutoff' scale is great when you have weirdly distributed data (see CutoffScale). The 'sine' scale scales the axis as the sine of the coordinate, resulting in an “area-weighted” spherical latitude coordinate. The 'inverse' scale is perfect for labeling spectral coordinates (this is more useful with the dualx and dualy commands; see Dual unit axes).

[5]:
import proplot as plot
import numpy as np
f, axs = plot.subplots(width=6, nrows=4, aspect=(5,1), sharex=False)
ax = axs[0]
x = np.linspace(0,4*np.pi,100)
dy = np.linspace(-1,1,5)
y1 = np.sin(x)
y2 = np.cos(x)
state = np.random.RandomState(51423)
data = state.rand(len(dy)-1, len(x)-1)
scales = [(3, np.pi), (0.3, 3*np.pi), (np.inf, np.pi, 2*np.pi), (5, np.pi, 2*np.pi)]
titles = ('Zoom out of left', 'Zoom into left', 'Discrete cutoff', 'Fast jump')
locators = [np.pi/3, np.pi/3, *([x*np.pi for x in plot.arange(0, 4, 0.25) if not (1 < x <= 2)] for i in range(2))]
for ax,scale,title,locator in zip(axs,scales,titles,locators):
    ax.pcolormesh(x, dy, data, cmap='grays', cmap_kw={'right': 0.8})
    for y,color in zip((y1,y2), ('coral','sky blue')):
        ax.plot(x, y, lw=4, color=color)
    ax.format(xscale=('cutoff', *scale), title=title,
              xlim=(0,4*np.pi), ylabel='wave amplitude', # note since 'spanning labels' turned on by default, only one label is drawn
              xformatter='pi', xlocator=locator,
              xtickminor=False, xgrid=True, ygrid=False, suptitle='Demo of cutoff scales')
plot.rc.reset()
_images/axis_14_0.svg
[6]:
import proplot as plot
import numpy as np
plot.rc.reset()
f, axs = plot.subplots(nrows=3, ncols=2, axwidth=1.5, share=0)
axs.format(rowlabels=['Power\nscales', 'Exponential\nscales', 'Geographic\nscales'], suptitle='Demo of esoteric axis scales')
x = np.linspace(0,1,50)
y = 10*x
state = np.random.RandomState(51423)
data = state.rand(len(y)-1, len(x)-1)
# Power scales
colors = ('coral','sky blue')
for ax,power,color in zip(axs[:2],(2,1/4),colors):
    ax.pcolormesh(x, y, data, cmap='grays', cmap_kw={'right': 0.8})
    ax.plot(x, y, lw=4, color=color)
    ax.format(ylim=(0.1,10), yscale=('power',power),
              title=f'$x^{{{power}}}$')
# Exp scales
for ax,a,c,color in zip(axs[2:4],(np.e,2),(0.5,-1),colors):
    ax.pcolormesh(x, y, data, cmap='grays', cmap_kw={'right': 0.8})
    ax.plot(x, y, lw=4, color=color)
    ax.format(ylim=(0.1,10), yscale=('exp',a,c),
              title=f'${(a,"e")[a==np.e]}^{{{(c,"-")[c==-1]}x}}$')
# Geographic scales
n = 20
x = np.linspace(-180,180,n)
y = np.linspace(-85,85,n) # note sine just truncated values not in [-90,90], but Mercator transformation can reflect them
y2 = np.linspace(-85,85,n) # for pcolor
data = state.rand(len(x), len(y2))
for ax,scale,color in zip(axs[4:],('sine','mercator'),('coral','sky blue')):
    ax.plot(x, y, '-', color=color, lw=4)
    ax.pcolormesh(x, y2, data, cmap='grays', cmap_kw={'right': 0.8}) # use 'right' to trim the colormap from 0-1 color range to 0-0.8 color range
    ax.format(title=scale.title() + ' y-axis', yscale=scale,
              ytickloc='left',
              yformatter='deglat', grid=False, ylocator=20,
              xscale='linear', xlim=None, ylim=(-85,85))
_images/axis_15_0.svg

Datetime axes

Labeling datetime axes is incredibly easy with ProPlot. Pass a unit string as the locator argument, and the axis will be ticked at those units. Pass a (unit, interval) tuple to tick every interval units. Use the formatter argument for % style formatting of datetime. Again, see format, Locator, and Formatter for details.

[7]:
import proplot as plot
import numpy as np
plot.rc.update(linewidth=1.2, small=10, large=12, ticklenratio=0.7)
plot.rc.update(figurefacecolor='w', facecolor=plot.shade('C0', 2.7))
f, axs = plot.subplots(nrows=6, axwidth=6, aspect=(8,1), share=0)
# Default date locator enabled if you plot datetime data or set datetime limits
axs[0].format(xlim=(np.datetime64('2000-01-01'), np.datetime64('2001-01-02')),
              xrotation=0)
# Concise date formatter introduced in matplotlib 3.1
axs[1].format(xlim=(np.datetime64('2000-01-01'), np.datetime64('2001-01-01')),
              xformatter='concise', xrotation=0)
# Minor ticks every year, major every 10 years
axs[2].format(xlim=(np.datetime64('2000-01-01'), np.datetime64('2050-01-01')), xrotation=0,
              xlocator=('year', 10), xformatter='\'%y')
# Minor ticks every 10 minutes, major every 2 minutes
axs[3].format(xlim=(np.datetime64('2000-01-01T00:00:00'), np.datetime64('2000-01-01T12:00:00')), xrotation=0,
              xlocator=('hour',range(0,24,2)), xminorlocator=('minute',range(0,60,10)), xformatter='T%H:%M:%S')
# Month and year labels, with default tick label rotation
axs[4].format(xlim=(np.datetime64('2000-01-01'), np.datetime64('2008-01-01')),
              xlocator='year', xminorlocator='month', xformatter='%b %Y') # minor ticks every month
axs[5].format(xlim=(np.datetime64('2000-01-01'), np.datetime64('2001-01-01')),
              xgridminor=True, xgrid=False,
              xlocator='month', xminorlocator='weekday', xformatter='%B') # minor ticks every Monday, major every month
axs.format(ylocator='null', suptitle='Tick locators and formatters with time axes in ProPlot')
plot.rc.reset()
_images/axis_18_0.svg

Dual unit axes

The new dualx and dualy methods build duplicate x and y axes meant to represent alternate units in the same coordinate range as the “parent” axis. For simple transformations, just use the offset and scale keyword args. For more complex transformations, pass the name of any registered “axis scale” to the xscale or yscale keyword args (see below).

[8]:
import proplot as plot
# Arbitrary scaling and offsets
plot.rc.update({'grid.alpha':0.4, 'linewidth':1, 'grid.linewidth':1})
f, axs = plot.subplots(ncols=2, share=0, aspect=2.2, axwidth=3)
ax = axs[0]
c1, c2 = plot.shade('cerulean', 0.5), plot.shade('red', 0.5)
ax.format(yformatter='null', xlabel='meters', xlocator=1000, xlim=(0,5000),
          xcolor=c2, gridcolor=c2,
          suptitle='Duplicate x-axes with simple, custom transformations', ylocator=[], # locator=[] has same result as locator='null'
          )
ax.dualx(lambda x: x*1e-3, label='kilometers', grid=True, color=c1)
ax = axs[1]
ax.format(yformatter='null', xlabel='temperature (K)', title='', xlim=(200,300), ylocator='null',
         xcolor=c2, gridcolor=c2)
ax.dualx(lambda x: x - 273.15, label='temperature (\N{DEGREE SIGN}C)', grid=True, color=c1)
plot.rc.reset()
_images/axis_21_0.svg
[9]:
# Special scalings for atmospheric scientists, assumed scale height is 7km
plot.rc.update({'grid.alpha':0.4, 'linewidth':1, 'grid.linewidth':1})
f, axs = plot.subplots(ncols=2, share=0, aspect=0.4, axwidth=1.8)
ax = axs[0]
c1, c2 = plot.shade('cerulean', 0.5), plot.shade('red', 0.5)
ax.format(xformatter='null', ylabel='pressure (hPa)', ylim=(1000,10), xlocator=[], ycolor=c1)
ax.dualy('height', label='height (km)', ticks=2.5, color=c2, grid=True)
ax = axs[1] # span
ax.format(xformatter='null', ylabel='height (km)', ylim=(0,20), xlocator='null', gridcolor=c2, ycolor=c2,
          suptitle='Duplicate y-axes with special transformations', grid=True)
ax.dualy('pressure', label='pressure (hPa)', locator=100, color=c1, grid=True)
plot.rc.reset()
_images/axis_22_0.svg
[10]:
# Inverse scaling, useful for frequency analysis
import proplot as plot
import numpy as np
plot.rc['axes.ymargin'] = 0
cutoff = 1/5
c1, c2 = plot.shade('cerulean', 0.5), plot.shade('red', 0.5)
x = np.linspace(0.01,0.5,1000) # in wavenumber days
response = (np.tanh(-((x - cutoff)/0.03)) + 1)/2 # imgarinary response function
f, ax = plot.subplots(aspect=(3,1), width=6)
ax.plot(x, response, color='gray7', lw=2)
ax.axvline(cutoff, lw=2, ls='-', color=c2)
ax.fill_between([cutoff - 0.03, cutoff + 0.03], 0, 1, color=c2, alpha=0.3)
ax.format(xlabel='wavenumber (days$^{-1}$)', ylabel='response', gridminor=True)
ax = ax.dualx('inverse', locator=np.array([20, 10, 5, 2, 1, 0.5, 0.2, 0.1, 0.05]), label='period (days)')
ax.format(title='Imaginary response function', suptitle='Duplicate x-axes with wavenumber and period')
plot.rc.reset()
_images/axis_23_0.svg