_images/toyplot.png

Labels and Legends

Of course, most figures must be properly labelled before they can be of value, so Toyplot provides several mechanisms to help:

Coordinate System Labels

First, Cartesian Coordinates, Numberline Coordinates, and Table Coordinates provide labels that can be specified when they are created. In all cases the label parameter provides a top-level label for the coordinate system:

[1]:
import numpy
import toyplot

canvas = toyplot.Canvas(width=600, height=600)
canvas.cartesian(grid=(2,2,0), label="Cartesian Coordinates").plot(numpy.linspace(0, 1)**2)
canvas.numberline(grid=(2,2,1), label="Numberline Coordinates").scatterplot(numpy.random.normal(size=100))
canvas.table(grid=(2,2,2), label="Table Coordinates", data = numpy.random.random((4, 3)));
010203040500.00.51.0Cartesian Coordinates-2024Numberline CoordinatesTable Coordinates0120.4208520.7975110.3791020.7150570.4363080.9273740.6563930.785020.988810.5112330.8948840.150652

Naturally, some coordinate systems - such as Cartesian - allow you to specify additional, axis-specific labels:

[2]:
canvas = toyplot.Canvas(width=300, height=300)
axes = canvas.cartesian(label="Cartesian Coordinates", xlabel="Days", ylabel="Users")
axes.plot(numpy.linspace(0, 1)**2);
01020304050Days0.00.51.0UsersCartesian Coordinates

Coordinate System Text

Another option for labelling a figure is to insert text using the same domain as the data. For example, we can label individual series in a plot:

[3]:
def series(x):
    return numpy.cumsum(numpy.random.normal(loc=0.05, size=len(x)))

numpy.random.seed(1234)
x = numpy.arange(100)
y = numpy.column_stack([series(x) for i in range(5)])
[4]:
label_style = {"text-anchor":"start", "-toyplot-anchor-shift":"5px"}
canvas, axes, mark = toyplot.plot(x, y)
for i in range(y.shape[1]):
    axes.text(x[-1], y[-1,i], "Series %s" % i, style=label_style)
Series 0Series 1Series 2Series 3Series 4050100-1001020

Note that we are using the last point in each series as the anchor for the corresponding label - by default, Toyplot renders text centered on its anchor, so in this case we’ve chosen a text style that left-aligns the text and offsets it slightly to avoid overlapping the data.

Canvas Text

When adding text to axes, you specify the text coordinates using the same domain as your data. Naturally, this limits the added text to the bounds defined by the axes. For the ultimate in labeling flexibility, you can add text to the canvas directly, using canvas units, outside and/or overlapping coordinate systems:

[5]:
label_style={"font-size":"18px", "font-weight":"bold"}

canvas = toyplot.Canvas(width=600, height=300)
canvas.cartesian(grid=(1,2,0)).plot(numpy.linspace(1, 0)**2)
canvas.cartesian(grid=(1,2,1), yshow=False).plot(numpy.linspace(0, 1)**2)
canvas.text(300, 120, "This label overlaps two sets of axes!", style=label_style);
010203040500.00.51.001020304050This label overlaps two sets of axes!

… remember when placing labels directly on the canvas that, unlike Cartesian coordinates, canvas coordinates increase from top-to-bottom.

Coordinate System Color Scales

Since we often use color in visualization to add an additional dimension to our plots, we need a way to help viewers map between colors and values. For this case, Toyplot allows a color scale to be added to a set of Cartesian Coordinates:

[6]:
data = toyplot.data.cars()

colormap = toyplot.color.brewer.map(
    name="BlueGreenBrown",
    reverse=True,
    domain_min = data["MPG"].min(),
    domain_max = data["MPG"].max(),
)
canvas = toyplot.Canvas(width=600, height=400)
axes = canvas.cartesian(xlabel="Year", ylabel="Horsepower", margin=75)
axes.scatterplot(
    data["Year"],
    data["Horsepower"],
    color=(data["MPG"], colormap),
    size=8,
    mstyle={"stroke":"black", "stroke-opacity":0.3}
)
axes.color_scale(colormap, label="MPG");
70747882Year50100150200250Horsepower1020304050MPG

Note that a colormap must be explicitly specified when creating a color scale - this is necessary to avoid ambiguity when a single coordinate system contains multiple visualizations or data series.

Canvas Color Scales

For situations where displaying a vertical color scale with a single set of Cartesian axes is too limiting, you can add horizontal color scales directly to a canvas using any Canvas Layout that makes sense. For example, the following figure uses a single horizontal color scale to display a colormap that is shared between two coordinate systems:

[7]:
canvas = toyplot.Canvas(width=600, height=400)

axes = canvas.cartesian(bounds=("10%", "45%", "10%", "65%"), xlabel="Year", ylabel="Horsepower")
axes.scatterplot(
    data["Year"],
    data["Horsepower"],
    color=(data["MPG"], colormap),
    size=8,
    mstyle={"stroke":"black", "stroke-opacity":0.3}
)

axes = canvas.cartesian(bounds=("-45%", "-10%", "10%", "65%"), xlabel="Weight", ylabel="Horsepower")
axes.y.spine.position="high"
axes.scatterplot(
    data["Weight"],
    data["Horsepower"],
    color=(data["MPG"], colormap),
    size=8,
    mstyle={"stroke":"black", "stroke-opacity":0.3}
)

canvas.color_scale(colormap, bounds=("10%", "-10%", "75%", "90%"), label="MPG");

70747882Year50100150200250Horsepower2000300040005000Weight50100150200250Horsepower1020304050MPG

Note that when manually adding a color scale to a canvas, you can orient it any way you like (including diagonally!) by explicitly specifying the endpoints in canvas coordinates:

[8]:
colormap = toyplot.color.brewer.map(
    name="Spectral",
    domain_min=0,
    domain_max=1,
)
canvas = toyplot.Canvas(width=400)
canvas.color_scale(colormap, x1=50, y1=-50, x2=50, y2=50, label="Bottom to Top")
canvas.color_scale(colormap, x1=150, y1=50, x2=150, y2=-50, label="Top to Bottom")
canvas.color_scale(colormap, x1=200, y1=150, x2=350, y2=150, label="Left to Right")
canvas.color_scale(colormap, x1=350, y1=250, x2=200, y2=250, label="Right to Left");
0.00.51.0Bottom to Top0.00.51.0Top to Bottom0.00.51.0Left to Right0.00.51.0Right to Left

Canvas Legends

Last-but-not-least, Toyplot provides basic support for graphical legends:

[18]:
observations = numpy.random.power(2, size=(50, 50))

x = numpy.arange(len(observations))

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

color = ["blue", "blue", "red", "red"]
opacity = [0.1, 0.2, 0.2, 0.1]

canvas = toyplot.Canvas(600, 400)
axes = canvas.cartesian(bounds=(50, -50, 100, -50))
fill = axes.fill(x, boundaries, color=color, opacity=opacity)
mean = axes.plot(x, numpy.mean(observations, axis=1), color="blue")

canvas.legend([
    ("Mean", mean),
    ("Quartiles", fill),
    ],
    corner=("top", 30, 100, 50),
    );

010203040500.00.51.0Mean Quartiles

The call to toyplot.canvas.Canvas.legend() always includes an explicit list of entries to add to the legend, plus a Canvas Layout specification of where the layout should appear on the canvas. Currently, each entry to be displayed in a legend must be one of the following:

  • A (label, mark) tuple, which will get its appearance from the mark, or:
  • A (label, marker) tuple, which can be used to specify an arbitrary Marker.

Of course, label is the human-readable text to be displayed next to an item in the legend, while mark is a mark that has been added to the canvas. However, not all marks can map cleanly to a single entry in the legend - note in the example above that the fill mark added multiple markers to the “Quartiles” entry in the legend, one for each data series. While the interpretation is reasonably clear in this case, there will be occasions when there isn’t a sensible one-to-one mapping between a mark and an entry in the legend. For example, the meaning of multiple series may not be clear, or you may be plotting categorical information using custom markers in a line or scatter plot. In these cases use the second form of legend entry to specify as many explicit Markers as needed.

There are some subtleties here worth noting, many of which are driven by Toyplot’s deliberate embrace of the philosophy that explicit is better than implicit:

  • You can have as many or as few legends on your canvas as you like.
  • Callers explicitly specify the order and contents of each legend.
  • There is no relationship between axes and legends - you can combine marks from multiple axes in a single legend.

Here’s an example with all these ideas at work. Note that the legend overlaps two coordinate systems and its first entry derives directly from the mark in the first coordinate system, while the second and third entries document the individual series in the second mark:

[10]:
x = numpy.linspace(0, 1)
y1 = (1 - x) ** 2
y2 = numpy.column_stack((1 - (x ** 2), x ** 2))

canvas = toyplot.Canvas(width=600, height=300)
m1 = canvas.cartesian(grid=(1,2,0), margin=25).scatterplot(x, y1, marker="o", color="rgb(255,0,0)")
m2 = canvas.cartesian(grid=(1,2,1), margin=25, yshow=False).scatterplot(x, y2, marker="s", color=["green", "blue"])

if True:
    canvas.legend([
        ("Experiment 1", m1),
        ("Experiment 2", m2.markers[0]),
        ("Experiment 3", m2.markers[1]),
        ],
        corner=("top", 100, 100, 70),
        );
0.00.51.00.00.51.00.00.51.0Experiment 1Experiment 2Experiment 3