## DEV Community is a community of 556,550 amazing developers

We're a place where coders share, stay up-to-date and grow their careers.

# "Artist" in Matplotlib - something I wanted to know before spending tremendous hours on googling how-tos.

skotaro Updated on ・18 min read

It's true that matplotlib is a fantastic visualizing tool in Python. But it's also true that tweaking details in matplotlib is a real pain. You may easily lose hours to find out how to change a small part of your plot. Sometimes you even don't know the name of the part, which makes googling it harder. Even if you find a hint on Stack Overflow, you may spend another couple of hours to make it fit your case. These non-rewarding tasks can be avoided by knowing what a figure in matplotlib consists of and what you can do with them. Like most of you, I think, I have overcome my plotting problems by reading lots of answers by matplotlib gurus on Stack Overflow. Recently, I noticed that an official tutorial about Artist objects is very informative and helpful to understand what is going on when we plot with matplotlib and to reduce a large amount of time spent for tweaking1. In this post, I would like to share some basic knowledge about Artist objects in matplotlib which would prevent you from spending hours for tweaking.

# Purpose of this post

I'm not going to write about how-tos like "do like this when you want to do this", but a basic concept of Artist in matplotlib which helps you choose suitable search queries and arrange a solution for a similar problem as yours. After reading this, you'll probably understand those huge amount of recipes on the Internet more clearly. This also applies to those who use seaborn and plotting features of pandas which are wrappers of matplotlib.

# Contents

This post is basically an English version of the original article I wrote in Japanese, and is mostly based on Artist tutorial and Usage Guide (2.1.1 at the time of publication of the original one)

# Who is this for?

Matplotlib users who

• are able to make plots if needed, but often struggle to make them appropriate for publication or presentation (and are irritated by "the last one mile" to what you really want).
• have successfully found an exact solution on Stack Overflow but are still hazy about how it works and cannot apply it to other problems.
• found multiple hints for a problem but are not sure which to follow.

# Environment

• Python 3.6
• matplotlib 2.2
%matplotlib inline
import matplotlib.pyplot as plt
import numpy as np


plt.show() is omitted in this article because I use Jupyter notebook's inline plot.

# Two plotting styles you should be aware of

Before looking into Artist objects, I would like to mention the difference between plt.plot and ax.plot, or Pyplot and object-oriented API. While object-oriented API style is officially recommended, there are still lots of examples and code snippets using Pyplot style, including official docs. Some are even mixing both styles meaninglessly, which causes unnecessary confusion for beginners. Since official doc has good notes about them such as A note on the Object-Oriented API vs Pyplot and Coding Styles, here I only make some comments on them. If you look for introduction for them, I recommend official tutorials.

## Object-oriented API interface

This is the recommended style which often starts with fig, ax = plt.subplots() or other equivalents followed by ax.plot, ax.imshow etc. fig and ax are, in fact, Artists. Here are some simplest examples.

fig, ax = plt.subplots()
ax.plot(x,y)

fig = plt.figure()
ax.plot(x, y)


Some tutorials use fig = plt.gcf() and ax = plt.gca(). These should be used when you switch from Pyplot interface to OO interface, but some Pyplot-based codes include, for example, pointless ax = plt.gca() which is apparently copied from OO-based code without understanding. Using plt.gcf() or plt.gca() isn't a bad thing at all if one switch the interface intentionally. Considering implicit switching can be a cause of confusion for beginners, using plt.subplots or fig.add_subplot from the beginning would be the best practice for most of the cases if they are publicly available.

## Pyplot interface

This is a MATLAB-user-friendly style in which everything is done with plt.***.

# https://matplotlib.org/tutorials/introductory/pyplot.html
def f(t):
return np.exp(-t) * np.cos(2*np.pi*t)

t1 = np.arange(0.0, 5.0, 0.1)
t2 = np.arange(0.0, 5.0, 0.02)

plt.figure(1)
plt.subplot(211)
plt.plot(t1, f(t1), 'bo', t2, f(t2), 'k')

plt.subplot(212)
plt.plot(t2, np.cos(2*np.pi*t2), 'r--')
plt.show()


At first, it seems very simple because there is no need to think about which objects you are handling. You only need to know which "state" you are in, which is why this style is also called "stateful interface". Here, a "state" means which figure and subplot you are currently in. As you see in Pyplot tutorial, it gives a nice figure if your plot is not so complicated. Although Pyplot interface offers lots of functions to change plot settings, you may reach its limit within a couple of hours, days, months (or never if you are lucky enough) depending on what you want to do. At this stage, you need to switch to OO interface. That is why I recommend to use OO interface from the beginning. But Pyplot is still useful for quick checks or any occasions where you need rough plots.

# The hierarchy in matplotlib

After googling several times, you will notice that matplotlib has a hierarchical structure consisting of something often called fig and ax. Old doc for matplotlib 1.5 has a nice image explaining this.

Actually, these three components are special Artists called "containers" (and there is the fourth container Tick) which we will see later. This hierarchy makes simple examples above even clearer.

fig, ax = plt.subplots() # make Figure and Axes which belongs to 'fig'

fig = plt.figure() # make Figure
ax = fig.add_subplot(1,1,1) # make Axes belonging to fig


Taking a further look at attributes of fig and ax helps you understand the hierarchy more.

fig = plt.figure()
ax = fig.add_subplot(1,1,1) # make a blank plotting area
print('fig.axes:', fig.axes)
print('ax.figure:', ax.figure)
print('ax.xaxis:', ax.xaxis)
print('ax.yaxis:', ax.yaxis)
print('ax.xaxis.axes:', ax.xaxis.axes)
print('ax.yaxis.axes:', ax.yaxis.axes)
print('ax.xaxis.figure:', ax.xaxis.figure)
print('ax.yaxis.figure:', ax.yaxis.figure)
print('fig.xaxis:', fig.xaxis)

fig.axes: [<matplotlib.axes._subplots.AxesSubplot object at 0x1167b0630>]
ax.figure: Figure(432x288)
ax.xaxis: XAxis(54.000000,36.000000)
ax.yaxis: YAxis(54.000000,36.000000)
ax.xaxis.axes: AxesSubplot(0.125,0.125;0.775x0.755)
ax.yaxis.axes: AxesSubplot(0.125,0.125;0.775x0.755)
ax.xaxis.figure: Figure(432x288)
ax.yaxis.figure: Figure(432x288)
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-21-b9f2d5d9fe09> in <module>()
9 print('ax.xaxis.figure:', ax.xaxis.figure)
10 print('ax.yaxis.figure:', ax.yaxis.figure)
---> 11 print('fig.xaxis:', fig.xaxis)

AttributeError: 'Figure' object has no attribute 'xaxis'


From these results, we can expect following rules about the hierarchy of Figure, Axes, and Axis.

• Figure knows Axes but not Axis.
• Axes knows Figure and Axis both.
• Axis knows Axes and Figure both.
• Figure can contain multiple Axes because fig.axes is a list of Axes.
• Axes can belong to only single Figure because ax.figure is not a list.
• Axes can have one XAxis and YAxis respectively for similar reason.
• XAxis and YAxis can belong to single Axes and, accordingly, single Figure.

# Everything in your plot is an Artist

Instead of a figure explaining the hierarchical structure, Usage Guide in the current doc has "Anatomy of a figure"2 explaining all components in a figure which is also informative3.

From lines and points which represent data to minor ticks and text labels for them on X axis, every single component in a figure is an Artist object4. There are two types of Artist, containers and primitives. As I wrote in the previous section, three components in matplotlib's hierarchy, Figure, Axes, and Axis are containers which can contain lower containers and multiple primitives such as Line2D made by ax.plot, PathCollection by ax.scatter, or Text by ax.annotate. Even tick lines and labels are in fact Line2D and Text which belong to the fourth container Tick.

Containers have many "boxes" (Python lists, technicaly) for each type of primitives. For example, an Axes object, ax, just after instantation has an empty list ax.lines. Frequently-used command ax.plot adds a Line2D object in the list and does other accompanying settings silently.

x = np.linspace(0, 2*np.pi, 100)

fig = plt.figure()
print('ax.lines before plot:\n', ax.lines) # empty
line1, = ax.plot(x, np.sin(x), label='1st plot') # add Line2D in ax.lines
print('ax.lines after 1st plot:\n', ax.lines)
line2, = ax.plot(x, np.sin(x+np.pi/8), label='2nd plot') # add another Line2D
print('ax.lines after 2nd plot:\n', ax.lines)
ax.legend()
print('line1:', line1)
print('line2:', line2)

ax.lines before plot:
[]
ax.lines after 1st plot:
[<matplotlib.lines.Line2D object at 0x1171ca748>]
ax.lines after 2nd plot:
[<matplotlib.lines.Line2D object at 0x1171ca748>, <matplotlib.lines.Line2D object at 0x117430550>]
line1: Line2D(1st plot)
line2: Line2D(2nd plot)


Following sections summarize four containers. Tables are copied from Artist tutorial.

## Figure

Figure attributes description
fig.axes A list of Axes instances (includes Subplot)
fig.patch The Rectangle background
fig.images A list of FigureImages patches - useful for raw pixel display
fig.legends A list of Figure Legend instances (different from Axes.legends)
fig.lines A list of Figure Line2D instances (rarely used, see Axes.lines)
fig.patches A list of Figure patches (rarely used, see Axes.patches)
fig.texts A list Figure Text instances

Attributes with a plural name are lists and those with a singular name represent a single object. It is worth noting that Artists belonging to Figure use Figure coordinate by default. This can be converted to Axes or data coordinates by using Transforms, which is outside the scope of this post.

### fig.legend and ax.legend

fig.legends is a "box" for legends added by fig.lenged method. You may think "What's that for? We have ax.legend." The difference is a scope of each method. While ax.legend only collects labels from Artists belonging to ax, fig.legend gathers labels from all Axes under fig. This is useful, for instance, when you make a plot using ax.twinx. Simply using ax.legend twice makes two legends, which is not desirable in general.

x = np.linspace(0, 2*np.pi, 100)

fig = plt.figure()
ax.plot(x, np.sin(x), label='sin(x)')
ax1 = ax.twinx()
ax1.plot(x, 2*np.cos(x), c='C1', label='2*cos(x)')
# cf. 'CN' notation
# https://matplotlib.org/tutorials/colors/colors.html#cn-color-selection

ax.legend()
ax1.legend()


A famous recipe to put them together is to combine legend handlers and labels of both Axes.

# Executing this part in a different notebook cell shows an updated figure.
handler, label = ax.get_legend_handles_labels()
handler1, label1 = ax1.get_legend_handles_labels()
ax.legend(handler+handler1, label+label1, loc='upper center', title='ax.legend')
# Legend made by ax1.legend remains
fig


This can be easily done by fig.legend without arguments which was introduced in ver 2.15. By default, the position is specified with Figure coordinate which is not useful when you want to put it in a plotting frame. You can change it to Axes coordinate with bbox_transform keyword.

fig.legend(loc='upper right', bbox_to_anchor=(1,1), bbox_transform=ax.transAxes, title='fig.legend\nax.transAxes')
fig


## Axes

The matplotlib.axes.Axes is the center of the matplotlib universe

This is a quote from Artist tutorial. This is very true because important parts of data visualization in matplotlib all come from Axes methods.

Axes attributes description
ax.artists A list of Artist instances
ax.patch Rectangle instance for Axes background
ax.collections A list of Collection instances
ax.images A list of AxesImage
ax.legends A list of Legend instances
ax.lines A list of Line2D instances
ax.patches A list of Patch instances
ax.texts A list of Text instances
ax.xaxis matplotlib.axis.XAxis instance
ax.yaxis matplotlib.axis.YAxis instance

Frequently-used commands such as ax.plot and ax.scatter are called "helper methods" which add corresponding Artists in appropriate containers and do other miscellaneous jobs.

Helper methods Artist Container
ax.annotate Annotate ax.texts
ax.bar Rectangle ax.patches
ax.errorbar Line2D & Rectangle ax.lines & ax.patches
ax.fill Polygon ax.patches
ax.hist Rectangle ax.patches
ax.imshow AxesImage ax.images
ax.legend Legend ax.legends
ax.plot Line2D ax.lines
ax.scatter PathCollection ax.collections
ax.text Text ax.texts

This example shows ax.plot and ax.scatter add Line2D and PathCollection objects in corresponding lists.

x = np.linspace(0, 2*np.pi, 100)

fig = plt.figure()
print('ax.lines before plot:\n', ax.lines) # empty Axes.lines
line1, = ax.plot(x, np.sin(x), label='1st plot') # add Line2D in Axes.lines
print('ax.lines after 1st plot:\n', ax.lines)
line2, = ax.plot(x, np.sin(x+np.pi/8), label='2nd plot') # add another Line2D
print('ax.lines after 2nd plot:\n', ax.lines)

print('ax.collections before scatter:\n', ax.collections)
scat = ax.scatter(x, np.random.rand(len(x)), label='scatter') # add PathCollection in Axes.collections
print('ax.collections after scatter:\n', ax.collections)
ax.legend()
print('line1:', line1)
print('line2:', line2)
print('scat:', scat)
ax.set_xlabel('x value')
ax.set_ylabel('y value')

ax.lines before plot:
[]
ax.lines after 1st plot:
[<matplotlib.lines.Line2D object at 0x1181d16d8>]
ax.lines after 2nd plot:
[<matplotlib.lines.Line2D object at 0x1181d16d8>, <matplotlib.lines.Line2D object at 0x1181d1e10>]
ax.collections before scatter:
[]
ax.collections after scatter:
[<matplotlib.collections.PathCollection object at 0x1181d74a8>]
line1: Line2D(1st plot)
line2: Line2D(2nd plot)
scat: <matplotlib.collections.PathCollection object at 0x1181d74a8>


### Reusing a plotted object is not recommended

After knowing that plotted objects are contained in lists, you probably come up with an idea to reuse those objects in an Axes.lines by appending it to another Axes.lines list for faster plotting. Artist tutorial clearly states this is not recommended because helper methods do many things other than creating an Artist. A quick test tells this is not a good idea.

x = np.linspace(0, 2*np.pi, 100)

fig = plt.figure()
ax1 = fig.add_subplot(2,1,1) # upper subplot
line, = ax1.plot(x, np.sin(x), label='ax1 line') # create a Line2D object
ax1.legend()

ax2 = fig.add_subplot(2,1,2) # lower subplot
ax2.lines.append(line) # try to reuse same Line2D object in another Axes


Even add_line method doesn't work.

ax2.add_line(line)

ValueError: Can not reset the axes.  You are probably trying to re-use an artist in more than one Axes which is not supported


This error message indicates that an Artist, container or primitive, cannot be contained in multiple containers, which is consistent with the fact that each Artist holds the parent container as a bare object, not in a list.

print('fig:', id(fig))
print('ax1:', id(ax1))
print('line.fig:', id(line.figure))
print('line.axes:', id(line.axes))

fig: 4707121584
ax1: 4707121136
line.fig: 4707121584
line.axes: 4707121136


It might be possible if you do all the necessary stuff in a proper way, but this is far from the first idea to just append an object to a list and is enough reason not to do it.

## Axis

While Axis, appeared as XAxis or YAxis, only contains Artists related to ticks and labels, it often requires some googling for minor tweaking, occasionally for an hour. I hope this section helps you get the job done quick.

As Artist tutorial doesn't have a table like other containers, I made a similar table.

Axis attributes description
Axis.label A Text instance for axis label
Axis.majorTicks A list of Tick instances for major ticks.
Axis.minorTicks A list of Tick instances for minor ticks.

We used ax.set_xlabel and ax.set_ylabel in the example for Axes container. You might think these methods change X and Y label of Axes instance (ax), but in fact they change label attributes of XAxis and YAxis, ax.xaxis.label and ax.yaxis.label, respectively.

xax = ax.xaxis
print('xax.label:', xax.label)
print('xax.majorTicks:\n', xax.majorTicks) # seven major ticks (from 0 to 6) and two invisible ticks locating outside of the figure
print('xax.minorTicks:\n', xax.minorTicks) # two ticks outside the figure

xax.label: Text(0.5,17.2,'x value')
xax.majorTicks:
[<matplotlib.axis.XTick object at 0x117ae4400>, <matplotlib.axis.XTick object at 0x117941128>, <matplotlib.axis.XTick object at 0x11732c940>, <matplotlib.axis.XTick object at 0x1177d0470>, <matplotlib.axis.XTick object at 0x1177d0390>, <matplotlib.axis.XTick object at 0x1175058d0>, <matplotlib.axis.XTick object at 0x1175050b8>, <matplotlib.axis.XTick object at 0x117bf65c0>, <matplotlib.axis.XTick object at 0x117bf6b00>]
xax.minorTicks:
[<matplotlib.axis.XTick object at 0x117ab5940>, <matplotlib.axis.XTick object at 0x117b540f0>]


### ax.set_*** methods are ad hoc

Axes has many "set_***" helper methods to modify attributes and values of Axis and Tick instances. They are so handy that a large portion of problems matplotlib beginners encounter can be solved with some of them. It's worth noting that these "set_***" methods are static. Changes made with them are not updated when something changed. For example, if you change X ticks with ax.set_xticks to make them look nice with the first plot and the second plot exceeds the X range of the first plot, the result doesn't look like what it should be.

x = np.linspace(0, 2*np.pi, 100)

fig = plt.figure()
line1, = ax.plot(x, np.sin(x), label='') # X range: 0 to 2pi
ax.set_xticks([0, 0.5*np.pi, np.pi, 1.5*np.pi, 2*np.pi])
line2, = ax.plot(1.5*x, np.sin(x), label='') # X range: 0 to 3pi


### Ticker does it for you

If you don't change tick-related settings with "set_***" methods, ticks and tick labels are automatically updated for each new plot accordingly. This is done by Ticker, more specifically, formatter and locator. Although they are quite essential for tick-related settings, you might know little about them if you have solved your problems by copying and pasting Stack Overflow answers6. Let's see what's going on in the previous example.

xax = ax.xaxis
yax = ax.yaxis
print('xax.get_major_formatter()', xax.get_major_formatter())
print('yax.get_major_formatter()', yax.get_major_formatter())
print('xax.get_major_locator():',  xax.get_major_locator())
print('yax.get_major_locator():',  yax.get_major_locator())

xax.get_major_formatter() <matplotlib.ticker.ScalarFormatter object at 0x118af4d68>
yax.get_major_formatter() <matplotlib.ticker.ScalarFormatter object at 0x118862be0>
xax.get_major_locator(): <matplotlib.ticker.FixedLocator object at 0x1188d5908>
yax.get_major_locator(): <matplotlib.ticker.AutoLocator object at 0x118aed1d0>


ScalarFormatter is set for both X and Y axes, because it is a default formatter and we didn't change it. On the other hand, while default AutoLocator is set for Y axis, FixedLocator is set for X axis which we changed tick positions by using ax.set_xticks method. As you can imagine from its name, FixedLocator fixes tick positions and does not update them even if the plotting range is changed.

Let's change Ticker in the previous example instead of ax.set_xticks.

import matplotlib.ticker as ticker # this is required to used Ticker
ax.xaxis.set_major_locator(ticker.MultipleLocator(0.5*np.pi)) # locate ticks at every 0.5*pi
fig # display the figure again with new locator.


@ticker.FuncFormatter # FuncFormatter can be used as a decorator
return '{}$\pi$'.format(x/np.pi) # probably not the best way to show radian tick labels

fig


Well, there might still be something you would like to tweak, but I guess it's clear enough.

Gallery > Tick formatters
Gallery > Tick locators

### xunits keyword for ax.plot

For your information, ax.plot has xunits keyword which is not described in the doc for the moment. I have never tried to use this option, but you can see an example in Gallery > Radian ticks and learn more about matplotlib.units.ConversionInterface here.

import numpy as np
from basic_units import radians, degrees, cos
from matplotlib.pyplot import figure, show

x = [val*radians for val in np.arange(0, 15, 0.01)]

fig = figure()

line2, = ax.plot(x, cos(x), xunits=degrees)


## Tick

Finally, we reached at the bottom of the matplotlib hierarchy. Tick is a small container mainly for a short line for a tick itself and a text for a tick label.

Tick attributes description
Tick.tick1line Line2D instance
Tick.tick2line Line2D instance
Tick.gridline Line2D instance for grid
Tick.label1 Text instance
Tick.label2 Text instance
Tick.gridOn boolean which determines whether to draw the gridline
Tick.tick1On boolean which determines whether to draw the 1st tickline
Tick.tick2On boolean which determines whether to draw the 2nd tickline
Tick.label1On boolean which determines whether to draw the 1st tick label
Tick.label2On boolean which determines whether to draw the 2nd tick label

As we see in Axis, Tick also appears as XTick or YTick. 1st and 2nd indicate ticks at lower and upper sides for XTick, and those at left and right sides for YTick. The latter ticks are not visible by default.

xmajortick = ax.xaxis.get_major_ticks()[2] # tick at 0.5 pi in the previous figure
print('xmajortick', xmajortick)
print('xmajortick.tick1line', xmajortick.tick1line)
print('xmajortick.tick2line', xmajortick.tick2line)
print('xmajortick.gridline', xmajortick.gridline)
print('xmajortick.label1', xmajortick.label1)
print('xmajortick.label2', xmajortick.label2)
print('xmajortick.gridOn', xmajortick.gridOn)
print('xmajortick.tick1On', xmajortick.tick1On)
print('xmajortick.tick2On', xmajortick.tick2On)
print('xmajortick.label1On', xmajortick.label1On)
print('xmajortick.label2On', xmajortick.label2On)

xmajortick <matplotlib.axis.XTick object at 0x11eec0710>
xmajortick.tick1line Line2D((1.5708,0))
xmajortick.tick2line Line2D()
xmajortick.gridline Line2D((0,0),(0,1))
xmajortick.label1 Text(1.5708,0,'0.5$\\pi$')
xmajortick.label2 Text(0,1,'0.5$\\pi$')
xmajortick.gridOn False
xmajortick.tick1On True
xmajortick.tick2On False
xmajortick.label1On True
xmajortick.label2On False


We hardly need to handle Tick directly thanks to many helper methods, Ticker, and Axes.tick_params.

# It's time to custom your default style

Take a look at a list of parameters for the default style.
Tutorials > Customizing matplotlib > A sample matplotlibrc file
I guess now you can figure out not only what a parameter is for, but also on which Artist a parameter have an effect actually, which lets you save time for googling7. You can also customize the default style without making matplotlibrc file by just typing like this at the beginning of the code.

plt.rcParams['lines.linewidth'] = 2


# Go and see docs (again)

Some of you might have negative impression on matplotlib's doc. I agree that it was hard to find an appropriate example for your problem from a long list. But it has been improved significantly since version 2.1.08. This is obvious if you compare corresponding pages before and after the improvement.

I recommend to take a look at the latest gallery and the tutorial which are now pretty neat.

Thanks for reading. Enjoy plotting with matplotlib (and googling) 📈🤗📊

Cover Photo by Caleb Salomons on Unsplash

1. Yes, tutorials are always informative and helpful if you are not lazy enough to read them before using. Actually, I might have tried to read a doc about Artist once when I started to plot with matplotlib a few years ago, but I'm pretty sure that I thought "OK, this is not for me" at that time. (Maybe it was not the current tutorial.)

2. Here is a sample code for this figure https://matplotlib.org/gallery/showcase/anatomy.html

3. Of course there are other Artists. This page is a good entry for those who want a big picture. You can click Artist names for further explanation.

4. Technically speaking, Artists draw your beautiful data on a canvas in matplotlib. What a lovely rhetoric.

5. fig.legend had been not as useful as the current version because it required legend handles and labels according to the doc for ver. 2.0.2

6. You'll often come across recipes using them when you google tick-related settings one step further from "set_***" methods and give up arranging them for your own problem. (Yes, that's me several months ago.)

7. Or you can dig deeper using the saved time, like I do.

8. Here is a good read to know how difficult to improve the document. Matplotlib Lead Dev on Why He Can't Fix the Docs | NumFOCUS