_images/toyplot.png

The Toyplot Tutorial

Getting Started

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 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:

import numpy
import toyplot

For our first figure, let’s create a simple set of X and Y coordinates that we can plot:

x = numpy.linspace(0, 10)
y = x ** 2

Now, let’s put Toyplot to work ...

canvas = toyplot.Canvas(width=300, height=300)
axes = canvas.cartesian()
mark = axes.plot(x, y)
0510050100

Short as it is, this example demonstrates several key features of Toyplot:

  • Toyplot figures are beautiful, scalable, embeddable, and interactive. For example:
    • Click or tap anywhere in the figure, and note the coordinates that update along each axis.
    • Carefully place the cursor on top of the plotted line, and open a context menu - in the popup that appears, you can choose to save the figure data to a CSV file from your browser.
    • Note that Toyplot produces a clean, aesthetically pleasing figure that is crisp at any scale and free of chartjunk “out-of-the-box”.
  • Every Toyplot figure begins with a 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 rendered.
  • A Toyplot figure typically contains one-or-more coordinate systems which map a data domain to a range of canvas pixels.
  • Marks are added to coordinate systems using the factory functions they provide. In this example, the plot function adds a plot mark using the supplied coordinates. Note that the cartesian axes been automatically sized to the data’s domain.

Styles

Let’s say that you wanted to alter the above figure to make the plotted line blue and dashed. To do so, simply override the default style information when creating the plot:

canvas = toyplot.Canvas(width=300, height=300)
axes = canvas.cartesian()
mark = axes.plot(x, y, style={"stroke":"blue", "stroke-dasharray":"2, 2"})
0510050100

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.

Plotting

Let’s continue with the previous example. As a shortcut, you can omit the X coordinates when using the 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):

canvas = toyplot.Canvas(width=300, height=300)
axes = canvas.cartesian()
mark = axes.plot(y)
01020304050050100

If you add multiple plots to a set of axes, each automatically receives a different color:

x = numpy.linspace(0, 10, 100)
y1 = numpy.sin(x)
y2 = numpy.cos(x)
y3 = numpy.sin(x) + numpy.cos(x)
canvas = toyplot.Canvas(width=600, height=300)
axes = canvas.cartesian()
mark1 = axes.plot(x, y1)
mark2 = axes.plot(x, y2)
mark3 = axes.plot(x, y3)
0510-101

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:

canvas = toyplot.Canvas(width=600, height=300)
axes = canvas.cartesian()
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"})
0510-101

Palettes

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 palette - an ordered collection of RGBA colors. For example, here’s Toyplot’s default palette:

import toyplot.color
toyplot.color.Palette()

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 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 Colors section of the user guide.

Filled Regions

You can use fill to display a region bounded by two sets of Y coordinates. This can be a handy way to visualize data distributions:

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)
canvas = toyplot.Canvas(width=400, height=300)
axes = canvas.cartesian()
mark = axes.fill(x, y1, y2)
0.00.51.0-4-2024

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:

style={"fill":"steelblue", "fill-opacity":0.5, "stroke":toyplot.color.black}
canvas = toyplot.Canvas(width=400, height=300)
axes = canvas.cartesian()
mark = axes.fill(x, y1, y2, style=style)
0.00.51.0-4-2024

If you omit one of the boundaries it will default to \(y = 0\):

canvas = toyplot.Canvas(width=400, height=300)
axes = canvas.cartesian()
mark = axes.fill(x, y2)
0.00.51.00123

As with plots, if you omit the X coordinates, they will default to the range \([0, M)\):

canvas = toyplot.Canvas(width=400, height=300)
axes = canvas.cartesian()
mark = axes.fill(y2)
010203040500123

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:

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)))
canvas = toyplot.Canvas(width=400, height=300)
axes = canvas.cartesian()
mark = axes.fill(boundaries)
01020304050-4-2024

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):

canvas = toyplot.Canvas(width=400, height=300)
axes = canvas.cartesian()
mark = axes.fill(boundaries, style={"fill":"steelblue", "stroke":"white"})
01020304050-4-2024

... or we can do it using the “color” argument:

canvas = toyplot.Canvas(width=400, height=300)
axes = canvas.cartesian()
mark = axes.fill(boundaries, color="steelblue", style={"stroke":"white"})
01020304050-4-2024

The advantage of the latter is that the “color” 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 color values with global styles in intuitive ways:

color = ["red", "green", "blue", "yellow"]
canvas = toyplot.Canvas(width=400, height=300)
axes = canvas.cartesian()
mark = axes.fill(boundaries, color=color, style={"stroke":toyplot.color.black})
01020304050-4-2024

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):

color = ["blue", "blue", "red", "red"]
opacity = [0.1, 0.2, 0.2, 0.1]
title = ["1st Quartile", "2nd Quartile", "3rd Quartile", "4th Quartile"]
style={"stroke":toyplot.color.black}
canvas = toyplot.Canvas(width=400, height=300)
axes = canvas.cartesian()
mark = axes.fill(boundaries, color=color, opacity=opacity, title=title, style=style)
1st Quartile2nd Quartile3rd Quartile4th Quartile01020304050-4-2024

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):

numpy.random.seed(1234)

samples = numpy.linspace(0, 4 * numpy.pi, 100)
frequency = lambda: numpy.random.normal()
phase = lambda: numpy.random.normal()
amplitude = lambda: numpy.random.uniform(0.1, 1)
wave = lambda: numpy.sin(phase() + (frequency() * samples))
signal = lambda: amplitude() * (2 + wave())
heights = numpy.column_stack([signal() for i in range(10)])
canvas = toyplot.Canvas(width=500, height=300)
axes = canvas.cartesian()
m = axes.fill(heights, baseline="stacked")
05010004812

If you pass a sequence of scalar values instead of colors to the “color” argument, the values will be mapped to colors using a linear mapping and a default diverging colormap:

color = numpy.arange(heights.shape[1])
canvas = toyplot.Canvas(width=500, height=300)
axes = canvas.cartesian()
m = axes.fill(heights, baseline="stacked", color=color)
05010004812

Of course, you’re free to supply your own colormap instead:

colormap = toyplot.color.brewer.map("BlueGreenBrown")
canvas = toyplot.Canvas(width=500, height=300)
axes = canvas.cartesian()
m = axes.fill(heights, baseline="stacked", color=(color, colormap))
05010004812

... 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:

canvas = toyplot.Canvas(width=500, height=300)
axes = canvas.cartesian()
m = axes.fill(heights, baseline="symmetric", color=(color, colormap))
050100-505
canvas = toyplot.Canvas(width=500, height=300)
axes = canvas.cartesian()
m = axes.fill(heights, baseline="wiggle", color=(color, colormap))
050100-4048

Barplots

Of course, you can’t have a plotting library without barplots ...

heights = numpy.linspace(1, 10, 10) ** 2
canvas = toyplot.Canvas(width=300, height=300)
axes = canvas.cartesian()
mark = axes.bars(heights)
0510050100

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:

x = numpy.linspace(-2, 2, 20)
y = 5 - (x ** 2)

canvas = toyplot.Canvas(width=300, height=300)
axes = canvas.cartesian()
mark = axes.bars(x, y)
-2-1012012345

As a convenience, you can pass the output from numpy.histogram() directly to toyplot.coordinates.Cartesian.bars():

numpy.random.seed(1234)
population = numpy.random.normal(size=10000)
canvas = toyplot.Canvas(width=300, height=300)
axes = canvas.cartesian()
bars = axes.bars(numpy.histogram(population, 20))
-4-2024050010001500

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:

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))
canvas = toyplot.Canvas(width=300, height=300)
axes = canvas.cartesian()
mark = axes.bars(heights)
05100306090

As before, we can style the bars globally and use the “color”, “opacity”, and “title” arguments to specify constant or per-series behavior:

color = ["red", "green", "blue", "yellow"]
title = ["Series 1", "Series 2", "Series 3", "Series 4"]
style = {"stroke":toyplot.color.black}
canvas = toyplot.Canvas(width=300, height=300)
axes = canvas.cartesian()
bars = axes.bars(heights, color=color, title=title, style=style)
Series 1Series 1Series 1Series 1Series 1Series 1Series 1Series 1Series 1Series 1Series 2Series 2Series 2Series 2Series 2Series 2Series 2Series 2Series 2Series 2Series 3Series 3Series 3Series 3Series 3Series 3Series 3Series 3Series 3Series 3Series 4Series 4Series 4Series 4Series 4Series 4Series 4Series 4Series 4Series 405100306090

However, with bars we can take these concepts even further to specify per-datum quantities. That is, the color, 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 color, and also use them as the bar titles (hover over the bars to see the titles):

color = numpy.random.random(heights.shape)
colormap = toyplot.color.diverging.map("BlueRed")
canvas = toyplot.Canvas(width=300, height=300)
axes = canvas.cartesian()
bars = axes.bars(heights, color=(color, colormap), title=color, style=style)
0.671180366490.1335326337740.5541205739780.2517039739570.9205897480110.7759288665580.1347204372830.4825836850710.08188993451240.3926659033060.8883088878670.9901663250710.9124180775430.1981534929710.9520176342640.8283304454050.3773021026340.1720444573530.002482950217810.4628787394180.5424529654880.9359602075270.6052180652360.163638000510.5212729044720.02115913445260.7154891795510.5053269667880.9288410165950.06828732321960.9450110629430.3526752535930.4021094467720.8678650687380.5422079096990.7801711435650.9609649388790.1872860863710.6640783903610.35655134502805100306090

Scatterplots

Next on our whirlwind tour of marks is the scatterplot:

x = numpy.linspace(0, 2 * numpy.pi)
y1 = numpy.sin(x)
y2 = numpy.cos(x)
canvas = toyplot.Canvas(width=500, height=300)
axes = canvas.cartesian()
mark = axes.scatterplot(x, y1)
0246-1.0-0.50.00.51.0