.. _tick-locators:
When you create a figure in Toyplot, you begin by creating a :class:canvas<toyplot.canvas.Canvas>
, add :mod:axes<toyplot.axes>
, and add data to the axes in the form of marks. The axes map data from its domain to a range on the canvas, generating ticks in domain-space with tick labels as an integral part of the process.
To generate tick locations and tick labels, the axes delegate to the tick locator classes in the :mod:toyplot.locator
module. Each tick locator class is responsible for generating a collection of tick locations from the range of values in an axis domain, and there are several different classes available that implement different strategies for generating "good" tick locations. If you don't specify any tick locators when creating axes, sensible defaults will be chosen for you. For example:
In [1]:
import numpy
x = numpy.arange(20)
y = numpy.linspace(0, 1, len(x)) ** 2
In [2]:
import toyplot
canvas, axes, mark = toyplot.plot(x, y, width=300)
Note that the Y axis in this plot has sensible ticks that cover the full data domain $[0, 1]$, while the X axis also has sensible ticks that include "round" numbers like 20 even though the X values only cover $[0, 19]$. The algorithm for identifying "good" "round" tick values is provided by Toyplot's :class:toyplot.locator.Extended
locator.
However, let's say that we prefer to always have ticks that include the exact minimum and maximum data domain values, and evenly divide the rest of the domain. In this case, we can override the default choice of locator with the :class:toyplot.locator.Basic
tick locator:
In [3]:
canvas, axes, mark = toyplot.plot(x, y, width=300)
axes.x.ticks.locator = toyplot.locator.Basic(count=5)
We can also override the default formatting string used to generate the locator labels:
In [4]:
canvas, axes, mark = toyplot.plot(x, y, width=300)
axes.x.ticks.locator = toyplot.locator.Basic(count=5, format="{:.2f}")
Anytime you use log scale axes in a plot, Toyplot automatically uses the :class:toyplot.locator.Log
locator to provide ticks that are evenly-spaced :
In [5]:
canvas, axes, mark = toyplot.plot(x, y, xscale="log10", width=300)
If you don't like the "superscript" notation that the Log locator produces, you could replace it with your own locator and custom format:
In [6]:
canvas, axes, mark = toyplot.plot(x, y, xscale="log10", width=300)
axes.x.ticks.locator = toyplot.locator.Log(base=10, format="{base}^{exponent}")
Or even display raw tick values:
In [7]:
canvas, axes, mark = toyplot.plot(x, y, xscale="log2", width=300)
axes.x.ticks.locator = toyplot.locator.Log(base=2, format="{:.0f}")
Although you might not think of :ref:table-axes
as needing tick locators, when you use :func:toyplot.matrix
or :meth:toyplot.canvas.Canvas.matrix
to visualize a matrix of values, it generates a table visualization and uses :class:toyplot.locator.Integer
locators to generate row and column labels:
In [8]:
numpy.random.seed(1234)
canvas, table = toyplot.matrix(numpy.random.random((5, 5)), width=300)
By default the Integer locator generates a tick/label for every integer in the range $[0, N)$ ... as you visualize larger matrices, you'll find that a label for every row and column becomes crowded, in which case you can override the default step
parameter to space-out the labels:
In [9]:
canvas, table = toyplot.matrix(numpy.random.random((50, 50)), width=400, step=5)
For the ultimate flexibility in positioning tick locations and labels, you can use the :class:toyplot.locator.Explicit
locator. With it, you can specify an explicit set of labels, and a set of $[0, N)$ integer locations will be created to match. This is particularly useful if you are working with categorical data:
In [10]:
fruits = ["Apples", "Oranges", "Kiwi", "Miracle Fruit", "Durian"]
counts = [452, 347, 67, 21, 5]
canvas, axes, mark = toyplot.bars(counts, width=400, height=300)
axes.x.ticks.locator = toyplot.locator.Explicit(labels=fruits)
Note that in the above example the implicit $[0, N)$ tick locations match the implicit $[0, N)$ X coordinates that are generated for each bar when you don't supply any X coordinates of your own. This is by design!
You can also use Explicit locators with a list of tick locations, and a set of tick labels will be generated using a format string. For example:
In [11]:
x = numpy.linspace(0, 2 * numpy.pi)
y = numpy.sin(x)
locations=[0, numpy.pi/2, numpy.pi, 3*numpy.pi/2, 2*numpy.pi]
canvas, axes, mark = toyplot.plot(x, y, width=500, height=300)
axes.x.ticks.locator = toyplot.locator.Explicit(locations=locations, format="{:.2f}")
Finally, you can supply both locations and labels to an Explicit locator:
In [12]:
labels = ["0", u"\u03c0 / 2", u"\u03c0", u"3\u03c0 / 2", u"2\u03c0"]
canvas, axes, mark = toyplot.plot(x, y, width=500, height=300)
axes.x.ticks.locator = toyplot.locator.Explicit(locations=locations, labels=labels)
Explicit locators are also a good way to handle timeseries using timestamps or Python datetime objects. For this example, we will create a series "x" that contains datetime objects, and a series "y" containing values:
In [13]:
import datetime
import toyplot.data
data = toyplot.data.read_csv("commute-obd.csv")
observations = numpy.logical_and(data["name"] == "Vehicle Speed", data["value"] != "NODATA")
timestamps = data["timestamp"][observations]
x = [datetime.datetime.strptime(timestamp, "%Y-%m-%d %H:%M:%S.%f") for timestamp in timestamps]
y = data["value"][observations]
Now, we can extract a set of locations and labels, formatting the datetime objects to suit:
In [14]:
locations = []
labels = []
for index, timestamp in enumerate(x):
if timestamp.minute % 5 == 0 and 0 < timestamp.second < 5:
locations.append(index)
labels.append(timestamp.strftime("%H:%M"))
canvas, axes, mark = toyplot.plot(y, label="Vehicle Speed", xlabel="Time", ylabel="km/h", width=600, height=300)
axes.x.ticks.locator = toyplot.locator.Explicit(locations=locations, labels=labels)
axes.x.ticks.show = True
Because timestamp labels tend to be fairly long, this is often a case where you'll want to adjust the orientation of the labels so they don't overlap (note in the following example that we had to make the canvas larger and position the axes manually on the canvas to make room for the vertical labels - and we manually placed the x axis label to avoid overlap):
In [15]:
locations = []
labels = []
for index, timestamp in enumerate(x):
if timestamp.minute % 5 == 0 and 0 < timestamp.second < 5:
locations.append(index)
labels.append(timestamp.strftime("%Y-%m-%d %H:%M"))
canvas = toyplot.Canvas(width=600, height=400)
canvas.text(300, 360, "Time", style={"font-weight":"bold"})
axes = canvas.axes(label="Vehicle Speed", ylabel="km/h", bounds=(50, -50, 50, -150))
axes.x.ticks.locator = toyplot.locator.Explicit(locations=locations, labels=labels)
axes.x.ticks.show = True
axes.x.ticks.labels.angle = 90
axes.x.ticks.labels.style = {"baseline-shift":0, "text-anchor":"end", "-toyplot-anchor-shift":"-6px"}
mark = axes.plot(y)