Welcome! This tutorial will introduce you to the basics of working with Toyplot.
Note: this tutorial was created in a Jupyter notebook and assumes that you're following-along in a notebook of your own. If you aren't using a notebook, you should read the user guide section on :ref:rendering
for some important information on how to display your figures.
To begin, we're going to import numpy (so we can create data to use for our figures), and the main toyplot module:
In [1]:
import numpy
import toyplot
For our first figure, let's create a simple set of X and Y coordinates that we can plot:
In [2]:
x = numpy.linspace(0, 10)
y = x ** 2
Now, let's put Toyplot to work ...
In [3]:
canvas = toyplot.Canvas(width=300, height=300)
axes = canvas.axes()
mark = axes.plot(x, y)
Short as it is, this example demonstrates several key features of Toyplot:
canvas <toyplot.canvas.Canvas>
- a drawing area upon which the caller adds marks. Note that the width and height of the canvas have been specified in CSS pixels, which are always equal to 1/96th of an inch, and converted to device units when the canvas is :ref:rendered <rendering>
.axes <toyplot.axes.Cartesian>
which map a data domain to a range of canvas pixels.plot <toyplot.axes.Cartesian.plot>
function adds a :class:plot <toyplot.mark.Plot>
mark using the supplied coordinates. Note that the axes have been automatically sized to the data's domain.
In [4]:
canvas = toyplot.Canvas(width=300, height=300)
axes = canvas.axes()
mark = axes.plot(x, y, style={"stroke":"blue", "stroke-dasharray":"2, 2"})
In this case, you can see that the style information is a dictionary of key-value properties that alter how a mark is rendered. To avoid reinventing the wheel, Toyplot uses Cascading Style Sheets (CSS) to specify styles. If you're familiar with web development, you already know CSS. If not, this tutorial will cover many of most useful CSS properties for Toyplot as we go, and there are many learning resources for CSS online.
Every mark you add to a figure will have at least one (and possibly more than one) set of styles that control its appearance.
Let's continue with the previous example. As a shortcut, you can omit the X coordinates when using the :func:plot <toyplot.axes.Cartesian.plot>
command, and a set of coordinates in the range $[0, M)$ will be provided (compare the following X axis with the previous two plots to see the difference):
In [5]:
canvas = toyplot.Canvas(width=300, height=300)
axes = canvas.axes()
mark = axes.plot(y)
If you add multiple plots, each automatically receives a different color:
In [6]:
x = numpy.linspace(0, 10, 100)
y1 = numpy.sin(x)
y2 = numpy.cos(x)
y3 = numpy.sin(x) + numpy.cos(x)
In [7]:
canvas = toyplot.Canvas(width=600, height=300)
axes = canvas.axes()
mark1 = axes.plot(x, y1)
mark2 = axes.plot(x, y2)
mark3 = axes.plot(x, y3)
As we've already seen, we can use the "stroke" style to override the default color of each plot; in addition, the "stroke-width" and "stroke-opacity" styles are useful properties for (de)emphasizing individual plots:
In [8]:
canvas = toyplot.Canvas(width=600, height=300)
axes = canvas.axes()
mark1 = axes.plot(x, y1, style={"stroke-width":1, "stroke-opacity":0.6})
mark2 = axes.plot(x, y2, style={"stroke-width":1, "stroke-opacity":0.6})
mark3 = axes.plot(x, y3, style={"stroke":"blue"})
Before proceeding, let's take a moment to look at how the default color for a mark is assigned. When we add multiple marks to a set of axes, each mark gets a different color. These default colors are all drawn from a :class:palette <toyplot.color.Palette>
- an ordered collection of RGBA colors. For example, here's Toyplot's default palette:
In [9]:
import toyplot.color
toyplot.color.Palette()
Out[9]:
Note: Like canvases, palettes are automatically rendered in Jupyter notebooks, in this case as a collection of color swatches.
You should observe that the order of colors in the palette match the order of the colors that were assigned to our plots as they were added to their axes. You could create a custom palette by passing a sequence of colors to the :class:toyplot.color.Palette
constructor, but Toyplot already comes with a builtin collection of high-quality palettes from Color Brewer, which we will use in the examples that follow.
For more detail on colors in Toyplot, see the :ref:color
section of the user guide.
In [10]:
numpy.random.seed(1234)
observations = numpy.random.normal(size=(50, 50))
x = numpy.linspace(0, 1, len(observations))
y1 = numpy.min(observations, axis=1)
y2 = numpy.max(observations, axis=1)
In [11]:
canvas = toyplot.Canvas(width=400, height=300)
axes = canvas.axes()
mark = axes.fill(x, y1, y2)
Use the "fill" style (not to be confused with the fill command) to control the color of the shaded region. You might also want to change the fill-opacity or add a stroke using styles:
In [12]:
canvas = toyplot.Canvas(width=400, height=300)
axes = canvas.axes()
mark = axes.fill(x, y1, y2, style={"fill":"steelblue", "fill-opacity":0.5, "stroke":toyplot.color.near_black})
If you omit one of the boundaries it will default to $y = 0$:
In [13]:
canvas = toyplot.Canvas(width=400, height=300)
axes = canvas.axes()
mark = axes.fill(x, y2)
As with plots, if you omit the X coordinates, they will default to the range $[0, M)$:
In [14]:
canvas = toyplot.Canvas(width=400, height=300)
axes = canvas.axes()
mark = axes.fill(y2)
Toyplot also makes it easy to define multiple sets of boundaries, by passing an $M \times N$ matrix as input, where $M$ is the number of observations, and $N$ is the number of boundaries:
In [15]:
boundaries = numpy.column_stack(
(numpy.min(observations, axis=1),
numpy.percentile(observations, 25, axis=1),
numpy.percentile(observations, 50, axis=1),
numpy.percentile(observations, 75, axis=1),
numpy.max(observations, axis=1)))
In [16]:
canvas = toyplot.Canvas(width=400, height=300)
axes = canvas.axes()
mark = axes.fill(boundaries)
This introduces an important new concept: you can think of fill (and other types of) marks as containers for collections of series, where in this case, $N$ boundaries define $N-1$ series.
This distinction is important because we can control the styles of individual series, not just the mark as a whole. So, if we want to override the default colors for the fill regions, we can do it using the mark's global "fill" style (with a contrasting stroke to display the boundaries between series):
In [17]:
canvas = toyplot.Canvas(width=400, height=300)
axes = canvas.axes()
mark = axes.fill(boundaries, style={"fill":"steelblue", "stroke":"white"})
... or we can do it using the "fill" argument:
In [18]:
canvas = toyplot.Canvas(width=400, height=300)
axes = canvas.axes()
mark = axes.fill(boundaries, fill="steelblue", style={"stroke":"white"})
The advantage of the latter is that the "fill" argument can specify a single color value as we've seen, or a sequence of color values, one-per-series. And, you can combine those per-series fill values with global styles in intuitive ways:
In [19]:
fill = ["red", "green", "blue", "yellow"]
In [20]:
canvas = toyplot.Canvas(width=400, height=300)
axes = canvas.axes()
mark = axes.fill(boundaries, fill=fill, style={"stroke":toyplot.color.near_black})
The "opacity" and "title" arguments can also be specified on a per-series basis (hover the mouse over the fill regions in the following figure to see the title as a popup):
In [21]:
fill = ["blue", "blue", "red", "red"]
opacity = [0.1, 0.2, 0.2, 0.1]
title = ["1st Quartile", "2nd Quartile", "3rd Quartile", "4th Quartile"]
In [22]:
canvas = toyplot.Canvas(width=400, height=300)
axes = canvas.axes()
mark = axes.fill(boundaries, fill=fill, opacity=opacity, title=title, style={"stroke":toyplot.color.near_black})
In the preceding examples you defined the fill regions by explicitly specifying their boundaries ... as an alternative, you can generate fills by specifying the magnitudes (the heights) of each region (note that in this case $N$ heights define $N$ series):
In [23]:
numpy.random.seed(1234)
heights = []
x = numpy.linspace(0, 4 * numpy.pi, 100)
for i in range(10):
heights.append(numpy.random.uniform(0.1, 1) * (2 + numpy.sin(numpy.random.normal() + (numpy.random.normal() * x))))
heights = numpy.column_stack(heights)
In [24]:
canvas = toyplot.Canvas(width=500, height=300)
axes = canvas.axes()
m = axes.fill(heights, baseline="stacked")
If you pass a sequence of scalar values instead of colors to the "fill" argument, the values will be mapped to colors using a linear mapping and a default sequential palette:
In [25]:
fill = numpy.arange(heights.shape[1])
In [26]:
canvas = toyplot.Canvas(width=500, height=300)
axes = canvas.axes()
m = axes.fill(heights, baseline="stacked", fill=fill)
Of course, you're free to supply your own palette for the mapping:
In [27]:
palette = toyplot.color.brewer("BlueRed")
In [28]:
canvas = toyplot.Canvas(width=500, height=300)
axes = canvas.axes()
m = axes.fill(heights, baseline="stacked", fill=(fill, palette))
... note that the baseline
parameter is what signals that the inputs are magnitudes instead of boundaries. You can also change the baseline parameter to create various types of streamgraph:
In [29]:
canvas = toyplot.Canvas(width=500, height=300)
axes = canvas.axes()
m = axes.fill(heights, baseline="symmetric", fill=(fill, palette))
In [30]:
canvas = toyplot.Canvas(width=500, height=300)
axes = canvas.axes()
m = axes.fill(heights, baseline="wiggle", fill=(fill, palette))
Of course, you can't have a plotting library without :meth:barplots <toyplot.axes.Cartesian.bars>
...
In [31]:
heights = numpy.linspace(1, 10, 10) ** 2
In [32]:
canvas = toyplot.Canvas(width=300, height=300)
axes = canvas.axes()
mark = axes.bars(heights)
By default the bars are centered on integer X coordinates in the range $[0, M)$ - but we can specify our own X coordinates to suit:
In [33]:
x = numpy.linspace(-2, 2, 20)
y = 5 - (x ** 2)
canvas = toyplot.Canvas(width=300, height=300)
axes = canvas.axes()
mark = axes.bars(x, y)
As a convenience, you can pass the output from :func:numpy.histogram()
directly to :meth:toyplot.axes.Cartesian.bars
:
In [34]:
numpy.random.seed(1234)
population = numpy.random.normal(size=10000)
In [35]:
canvas = toyplot.Canvas(width=300, height=300)
axes = canvas.axes()
bars = axes.bars(numpy.histogram(population, 20))
As with fill marks, Toyplot allows you to stack multiple sets of bars by passing an $M \times N$ matrix as input, where $M$ is the number of observations, and $N$ is the number of series:
In [36]:
heights1 = numpy.linspace(1, 10, 10) ** 1.1
heights2 = numpy.linspace(1, 10, 10) ** 1.3
heights3 = numpy.linspace(1, 10, 10) ** 1.4
heights4 = numpy.linspace(1, 10, 10) ** 1.5
heights = numpy.column_stack((heights1, heights2, heights3, heights4))
In [37]:
canvas = toyplot.Canvas(width=300, height=300)
axes = canvas.axes()
mark = axes.bars(heights)
As before, we can style the bars globally and use the "fill", "opacity", and "title" arguments to specify constant or per-series behavior:
In [38]:
fill = ["red", "green", "blue", "yellow"]
title = ["Series 1", "Series 2", "Series 3", "Series 4"]
style = {"stroke":toyplot.color.near_black}
In [39]:
canvas = toyplot.Canvas(width=300, height=300)
axes = canvas.axes()
bars = axes.bars(heights, fill=fill, title=title, style=style)
However, with bars we can take these concepts even further to specify per-datum quantities. That is, the fill, opacity, and title arguments can accept data that will apply to every individual bar in the plot. For the following example, we generate a per-datum set of random values to map to the fill color, and also use them as the bar titles (hover over the bars to see the titles):
In [40]:
fill = numpy.random.random(heights.shape)
colormap = toyplot.color.diverging("BlueRed")
In [41]:
canvas = toyplot.Canvas(width=300, height=300)
axes = canvas.axes()
bars = axes.bars(heights, fill=(fill, colormap), title=fill, style=style)
In [42]:
x = numpy.linspace(0, 2 * numpy.pi)
y1 = numpy.sin(x)
y2 = numpy.cos(x)
In [43]:
canvas = toyplot.Canvas(width=500, height=300)
axes = canvas.axes()
mark = axes.scatterplot(x, y1)
As you might expect, you can omit the X coordinates for a scatterplot, and they will fall in to the range $[0, M)$:
In [44]:
canvas = toyplot.Canvas(width=500, height=300)
axes = canvas.axes()
mark = axes.scatterplot(y1)
And as we've seen before, you can pass multiple series in a single call:
In [45]:
series = numpy.column_stack((y1, y2))
canvas = toyplot.Canvas(width=500, height=300)
axes = canvas.axes()
mark = axes.scatterplot(series)
And as expected, you can control attributes like fill, size, and opacity on a global, per-series, or per-datum basis (note that the size is treated as an approximation of the area of an individual datum):
In [46]:
fill = numpy.random.random(series.shape)
palette = toyplot.color.brewer("Oranges")
size = [16, 81]
canvas = toyplot.Canvas(width=500, height=300)
axes = canvas.axes()
mark = axes.scatterplot(series, fill=(fill, palette), size=size)
You can choose from a variety of marker shapes for scatterplots and line plots, also specified either globally, per-series, or per-datum, and specify styles for the markers:
In [47]:
mstyle={"stroke":toyplot.color.near_black}
canvas = toyplot.Canvas(width=500, height=300)
axes = canvas.axes()
mark = axes.scatterplot(series, size=50, marker=["^", "o"], mstyle=mstyle)
In addition to the basic marker shapes, you can create your own by adding text labels, also with their own styles:
In [48]:
marker = [{"shape":"o", "label":"1"}, {"shape":"o", "label":"2"}]
mlstyle = {"fill":"white"}
In [49]:
canvas = toyplot.Canvas(width=500, height=300)
axes = canvas.axes()
mark = axes.scatterplot(series, size=100, marker=marker, mstyle=mstyle, mlstyle=mlstyle)
See the user guide section on :ref:markers
for additional detail on the available marker shapes, styles, and how to create your own custom markers.
In [50]:
x = numpy.linspace(0, 10, 100)
y1 = numpy.sin(x)
y2 = numpy.cos(x)
y3 = numpy.sin(x) + numpy.cos(x)
series = numpy.column_stack((y1, y2, y3))
In [51]:
canvas = toyplot.Canvas(width=600, height=300)
axes = canvas.axes()
mark = axes.plot(x, series)
So far, we've created a default set of axes in each of the preceeding examples, and called methods on the axes to add marks to the canvas. The reason we explicitly create axes in Toyplot (instead of simply adding marks to the canvas directly) is that it allows us to have multiple axes on a single canvas:
In [52]:
canvas = toyplot.Canvas(600, 300)
axes = canvas.axes(grid=(1, 2, 0))
mark = axes.plot(x, y1)
axes = canvas.axes(grid=(1, 2, 1))
mark = axes.plot(x, y2)
In addition to positioning axes on the canvas, there are many properties that control their behavior. For example, Toyplot axes include builtin support for labels:
In [53]:
canvas = toyplot.Canvas(300, 300)
axes = canvas.axes(label="Toyplot User Growth", xlabel="Days", ylabel="Users")
mark = axes.plot(x, 40 + x ** 2)
Similarly, we can specify minimum and maximum values for each axis - for example, if we wanted the previous figure to include $y = 0$:
In [54]:
canvas = toyplot.Canvas(300, 300)
axes = canvas.axes(label="Toyplot User Growth", xlabel="Days", ylabel="Users", ymin=0)
mark = axes.plot(x, 40 + x ** 2)
We can also specify logarithmic scales for axes:
In [55]:
x = numpy.linspace(-1000, 1000)
In [56]:
toyplot.plot(x, x, marker="o", xscale="linear", yscale="log", width=500);
There are many more properties that control axes positioning and behavior - for more details, see :ref:canvas-layout
and :ref:cartesian-axes
in the user guide.
Toyplot can also create animated figures, by recording changes to a figure over time. Assume you've setup the following scatterplot:
In [57]:
x = numpy.random.normal(size=100)
y = numpy.random.normal(size=len(x))
In [58]:
canvas = toyplot.Canvas(300, 300)
axes = canvas.axes()
mark = axes.scatterplot(x, y, size=100)
Suppose we want to show the order in which the samples were drawn from some distribution. We could use the fill
parameter to map each sample's index to a color, but an animation can be more intuitive. We can use :meth:toyplot.canvas.Canvas.animate
to add a sequence of animation frames to the canvas. We pass the number of frames and a callback function as arguments, and the callback function will be called once per frame with a single :class:frame <toyplot.canvas.AnimationFrame>
argument. The callback uses the frame object to retrieve information about the frame and record any changes that should be made to the canvas at that frame. In the example below, we set the opacity of each scatterplot datum to 5% in the first frame, then change them back to 100% over the course of the animation:
In [59]:
canvas = toyplot.Canvas(300, 300)
axes = canvas.axes()
mark = axes.scatterplot(x, y, size=100)
def callback(frame):
if frame.index() == 0:
for i in range(len(x)):
frame.set_datum_style(mark, 0, i, style={"opacity":0.1})
else:
frame.set_datum_style(mark, 0, frame.index() - 1, style={"opacity":1.0})
canvas.animate(len(x) + 1, callback)
Let's try animating something other than a datum style - in the following example, we add a text mark to the canvas, and use it to display information about the frame:
In [60]:
canvas = toyplot.Canvas(300, 300)
axes = canvas.axes()
mark = axes.scatterplot(x, y, size=100)
text = canvas.text(150, 20, "")
def callback(frame):
frame.set_datum_text(text, 0, 0, str(frame))
if frame.index() == 0:
for i in range(len(x)):
frame.set_datum_style(mark, 0, i, style={"opacity":0.05})
else:
frame.set_datum_style(mark, 0, frame.index() - 1, style={"opacity":1.0})
canvas.animate(len(x) + 1, callback)
Note from this example that each frame has a zero-based frame index, along with begin and end times, which are measured in seconds. If you look closely, you'll see that the difference in begin and end times is 0.03 seconds for each frame, which corresponds to a default 30 frames per second. If we want to control the framerate, we can pass a (frames, framerate) tuple when we call :meth:toyplot.canvas.Canvas.animate
(note that the playback is slower, and the times for the frames are changed):
In [61]:
canvas = toyplot.Canvas(300, 300)
axes = canvas.axes()
mark = axes.scatterplot(x, y, size=100)
text = canvas.text(150, 20, "")
def callback(frame):
frame.set_datum_text(text, 0, 0, str(frame))
if frame.index() == 0:
for i in range(len(x)):
frame.set_datum_style(mark, 0, i, style={"opacity":0.05})
else:
frame.set_datum_style(mark, 0, frame.index() - 1, style={"opacity":1.0})
canvas.animate((len(x) + 1, 10), callback)
Sometimes the callback approach to animation is awkward, particularly if you simply have a one-time "event" that needs to happen in the middle of the animation. In this case, you can use :meth:toyplot.canvas.Canvas.time
to record changes for individual frames:
In [62]:
canvas = toyplot.Canvas(300, 300)
axes = canvas.axes()
mark = axes.scatterplot(x, y, size=100)
text = canvas.text(150, 20, "", style={"text-anchor":"middle"})
text2 = canvas.text(150, 50, "Halfway There!", style={"font-size":"16px", "font-weight":"bold", "fill":"blue", "text-anchor":"middle"})
def callback(frame):
frame.set_datum_text(text, 0, 0, str(frame))
if frame.index() == 0:
for i in range(len(x)):
frame.set_datum_style(mark, 0, i, style={"opacity":0.05})
else:
frame.set_datum_style(mark, 0, frame.index() - 1, style={"opacity":1.0})
canvas.animate((len(x) + 1), callback)
canvas.time(0, 0.1).set_datum_style(text2, 0, 0, {"opacity":0.0})
canvas.time(1.5, 5.1).set_datum_style(text2, 0, 0, {"opacity":1.0})
Note that when you combine :meth:toyplot.canvas.Canvas.animate
and :meth:toyplot.canvas.Canvas.time
, you don't have to force the "frames" to line-up ... you can record events in any order and at any point in time, whether there are existing frames at those times or not. In fact, you could call :meth:toyplot.canvas.Canvas.animate
multiple times, if you wanted to animate events happening at different rates:
In [63]:
canvas = toyplot.Canvas(100, 100, style={"background-color":"ivory"})
t1 = canvas.text(50, 33, "1 hz", style={"text-anchor":"middle"})
t2 = canvas.text(50, 66, "2 hz", style={"text-anchor":"middle"})
def blink(mark):
def callback(frame):
frame.set_datum_style(mark, 0, 0, {"opacity":frame.index() % 2.0})
return callback
canvas.animate((10, 2), blink(t1))
canvas.animate((20, 4), blink(t2))
Using transition properties in your styles can smooth-out the animation by avoiding instantaneous changes:
In [64]:
canvas = toyplot.Canvas(100, 100, style={"background-color":"ivory"})
t1 = canvas.text(50, 33, "1 hz", style={"text-anchor":"middle", "transition":"opacity 0.5s"})
t2 = canvas.text(50, 66, "2 hz", style={"text-anchor":"middle", "transition":"opacity 0.25s"})
def blink(mark):
def callback(frame):
frame.set_datum_style(mark, 0, 0, {"opacity":frame.index() % 2.0})
return callback
canvas.animate((10, 2), blink(t1))
canvas.animate((20, 4), blink(t2))
In [ ]: