Atmos CEE Masthead

Plotting 201: Intro to Fancy Plotting in Python ("Axes" and "Themes")

Warning

Typos are Legion

Introduction

This is a deep dive with a lot of repeative plotting to demonstrate how features are added to a graphical object and the impact each one makes to the entire product. The goal with this stragegy is that it will let us slowly explore how the more advanced graphics in Python work, albiet at the cost of a much longer Python notebook.

In this "Let's Play" session, we will explore the more complex aspects of plotting graphics beyond the simple way that we've been doing it through the semester.

One reason for this is that when one goes to the "online 'help,'" the examples that are often provided for "how to make a plot" for example tends to expect a user to jump in and intuit what is happening.

Personally for me, this is an engraved invitation to rage quit, which I did -- more than once -- when transitioning to Python. Therefore, in this session we are going to slow-roll the process of making "Fancy Plots" one command at a time.

Prerequisite Skills

To play along with this deep-dive you should have the following

Parts of a Python Graph

Before we start though, we should address the basic parts of a chart in "Pythonese." And mind you there is going to be some frustrating uses of nearly identical terms that mean very different things!

So far we have been working with just the matplotlib.pyplot interface. This does a lot of things automatically "under the hood" but we'll be needing to dig our fingers into these back workings to access some of the plotting features if we indeed wish to be fancy.

A sketch of the main big-picture parts of a Python figure is shown below and is taken from an earlier version of the Matplotlib documentation.

Python Plot Components

This is a somewhat busy plot, and there are worse ones out there in the newer versions of Matplotlib documentation but for now this is going to keep us more than busy.

If we look at the plot's features going from the higher-order objects representing the whole figure space down to the lower-level ones that reflect the basic details.

In short, each of these three elements can be broken down into parallel indivisual components.

Your document can have more than one Figure, each with its own goodies.

Your figure can have more than one ~Axes~ subplots or pannel, each with its own items and data, and can even have a map in one subplot, an x-y plot in another, a polar or ternary (triangular) plot in a third, and 3-D plot, in the last one.

And finally, any given ~Axes~ subplot, can have its own $x$, $y$, $z$, $r$, $\theta$ or $\phi$ axes, each with their own units, labels, tickmarks, etc.

So far we have been only been woring with one "Axes" in our Figures, even when we overlayed multiple lines or scatter-spots on it. As we play here, we will be digging into making more than one "Axes" (subplots) and also digging into specific elements of our graphics.

Libraries

For this exercize we are going to keep things restricted to just graphics. We'll need a few math operations but otherwise we are not going to make too much of a mess.

Against my better judgement I am adding plotting "themes" to the tutorial. Some "basic" examples will have this as part of the activity even though it isn't moving the plot. This will include one new library:

Grounding Ourselves with a Simple Plot

To start things off, let's establish a simple function representing a wave system:

$$f(t)=1 + e^{-t/100} \space \sin{t}$$

where $t$ goes from 0.1 to 10 using our numpy.linspace() function that we've been using in this class. The sine, numpy.sin(), and exponential, numpy.exp() functions are, likewise, basic numpy math functions. The "plus one" is there so we can use a log axis (or two log axissessess).

The Devil We Know

Let's make a couple simple, but street-legal plot of our function using matplotlib.pyplot.plot and it's three log-friends, matplotlib.pyplot.loglog, matplotlib.pyplot.semilogx(), and matplotlib.pyplot.semilogy().

Also notice the trick where you can slide in LaTeX equations into the text by preceeding any string with just a plain letter r. Also, make a mental note that exponents in the default-sized graphs in Jupyter Notebooks may be tiny.

These are decent basic plots but there may be some things you may want to do with them.

For example,

But to do most of these operations we will need to depart from our confort zone and start getting into the weeds of the Figure, Axes, and Axis operators.

Polar Plots

You also can make polar plots for certain applications. Polar on a single "non Axes" quick-n-easy case is pretty easy, instead of an x and y coordiante system it is a angle and radius system. You can access the tool for a polar plot with matplotlib.pyplot.polar().

This will get more complicated if we start making multiple plots on a given panel.

Fancy Tick-Fu and Making our First Non-"Basic" Plot

Let's start by just taking the linear plot and begin making small modifications bit by bit so you can see how using the more advanced fatures in matplotlib bring thing together.

The Creating the Figure and Axes

To start we will be making a new fresh figure. The command to best do this is the matplotlib.pyplot.subplots() function.

This will acutally create TWO output "variables." One is the figure object (we're calling it "fig"). The other is the Axes object which is typically called "ax."

Exciting!

OK it's not exciting, but this represents the dropping of a "blank" graph.

Adding a simple x-y plot.

Don't like the size? While the Python Notebook will try to scale the graph if you make it too large, you can make it bigger. (We'll do that latter.. we also can make it it have multiple "panels.")

Now let's now create, in our Figure, a simple plot...

Ooops. It's not going into the Figure. It's going into our one and only "Axes" (still strangly singular, but hopefully this madness will show its method as we go.)

We're just going to place a traditonal line plot w/o any frills into the plot area.

This command will work and behave just like the one we've been using with the plt.plot command, but it sits under the "Axes" area in our Matplotlib library: https://matplotlib.org/stable/api/axes_api.html#plotting and it mostly works just like our old friend.

(Notice that I am not dropping the closing plt.show() command. I'm saving that for the end.

Titles and AxIs Labels and Legends (Ohhhh Myyyyyy!)

Uh oh. Your TA with the red pen is passive-agressively gazing over your shoulder. Better put some labels on the plot.

And here is where we start diverging from what you may be expecting.

First we can have TWO titles. One for the Figure and one for each Axes ("thinking the subplots with a capital-A helps me cope with the homonymns." This is the "supertitle" or matplotlib.Figure.suptitle().

You can still have an individual title and legend for each subplot with these tools:

https://matplotlib.org/stable/api/axes_api.html#axis-labels-title-and-legend ...

...but many of the functions that said ".ylabel" or ".title" will be changed to ".set_title", ".set_xlabel" etc. (".legend" stays the same!)

~Micromanaging~ Customizing finer elements on the x- and y-Axe... Axi?

Axis Major Ticks

Now things are going to get strange. If you want to start manipulating the x- and y- axes you will have dig a little deeper and it's not all that easy. (In fact I only do this when I absolutely have to.)

The various tick commands, objects and operators are here: https://matplotlib.org/stable/api/axes_api.html#ticks-and-tick-labels

Here I am going to recommend keeping it as simple as possible (if you have to at all!).

There are two ways you can do this:

... the other way which isn't as scary as it looks is to just say, "gimme a major tick every x number of values." This uses two new ones. matplotlib.axis.set_major_locator and to specify where things go, use the function matplotlib.ticker.MultipleLocator()

Customizing values and formats for major ticks. (This is overkill even for me on most days!)

Before moving to the minor ticks, that x axis is representing cycles of the sine wave. It is sometimes the convention to do this in intervals of pi, You acutally can be [too] clever here by rescaling your x axis values and then changing the format.

This is done with a similar operator to the above: matplotlib.axis.get_major_formatter

The downer here (and one reason why I don't go this far on even a bad day), is the means of formatting. There's some explanations of this in the code documetation comments in treh code block below.

Multi-Panel Plots

Probably the hardest part in the case above was customizing the ticks which is something that I don't do if absolutely necessary. (I've grown fond of minimalist graph design that focuses on the data and what it represents rather than using every plotting feature at my disposal.)

But the next step will be putting more than one plot on a display.

This is also something that I often don't do for single "one-off" "do-and-done" plots, in cases like that I just say "#@*$-it!" make them separately and arrange them in Powerpoint, Viso, Adobe, or Keynote.

But if I am making something that will be done over and over, or if I am making an operational product that's generated automatically or repeatedly, I gulp (but not as much as I used to) and create multipanel plots.

There are a couple clever ways to do this but I am going to just demonstate a basic two-fisted way to do it.

From one Axes to multiple Axessesessesses

To do this we will again use the matplotlib.pyplot.subplots() command. This time though we can chose the number of rows and/or columns we want.

Let's do an example here.

Whoa! That's a little cramped. As I said earlier you can make the graphs bigger. Notice the example below using the figsize operator.

The units, by the way? Inches!!! In Jupyter it doesn't mean much but there are ways to export scripts into PDFs and Postscript files where the units actually do matter.

Now we can better see what we'd be putting inside each subplot or "Axes."

Each individual Axes is numbered by row and column in most uses.

Before moving on, how to we determine which box is which and how to access them? We can illustrate that here by just making some headers for each "Axes".

And now we can try loading each axes as if it were its own graph.

Now a couple things about this plot.

First it looks cluttered in that the top x-titles are running into the bottom upper titles.

Plus you'll note that the above example isn't including the polar plot example. Working with multiple panels or any of the more advanced plotting tools with plots in multiple coordinate systems, e.g., we will have to roll up our sleeves. An example of a plot with a "cartesian" and "polar" plot in the same figure canvas will be shown at the end of this particilar deep-dive.

Also I set it up on purpose so that when you go across the rows, you have the same kind of y-axis. And if you go down each column, you have the stake ame kind of x-axis.

There are three ways we can manage this

  1. We can limit which plots are labeling their axis'es. In the matplotlib.pyplot.subplots() statement you can set two parameters, sharex, and sharey. You can specify whether you share a given axis (x or y) across rows, columns, or both.

  2. We can also trim the titles and place the titles inside the plot.

  3. You can use a figure-level command, [plot.tight_layout()],(https://matplotlib.org/stable/api/_as_gen/matplotlib.pyplot.tight_layout.html) which will modify the various Axeses. (An example of this will be used shortly.)

Sharing Axis parameters across Axes.

Let's do (1) first. This is using the sharex and sharey modifiers to the matplotlib.pyplot.subplots().

That's a little better, and we can also clean things up by removing the axes on the "inside of the plots" (the x axis for the top row, and the right columns).

I'm ust going to do this by "commenting out" the undesired labels.

Don't like the big empty? There is an option to make the layout tight: matplotlib.tight_layout(). Ironically, this will help with plots that are too loose, or too tight.

Adding Annotations

I am literally doing this just for you because I REALLY hate doing this. This is another thing I do "in post" with Powerpoint, Keynote, Adobe or Keynote. You can add text objects to your plots. This can make them look more cluttered. In this case I am trying to do this to remove the over-titles for each Axes graph.

This is using the function matplotlib.Axes.text and it is rather high-maintenance (hence me not wanting to do this).

First you need to pick a coordinate pair to drop your graph. This also means that you can chose coordinates relative to your figure or your specific Axis, or the acutal x-y's of your plot.

To do the latter and if you are not asking to use your graphs x- and y-axis coordinates, you will need to use a "transform" operator. (You have to do this when you use maps.)

You also can chose the alignment of what corner or side of your textbox your x-and-y of your text coordinate will go.

Finally for any text (axis'es, titles, etc.) you can also select a relative text size: 'xx-small', 'x-small', 'small', 'medium', 'large', 'x-large', 'xx-large'

The first annotation for Axes 0-0 has comments to describe them if you REALLY REALLY one to use it.

Plotting Multiple Projections/Transforms in the Same Figure

While you can make more than one kind of cartessian plot on the same figure "canvas" (e.g., a log-x, log-y and log-log plot). Changing to a very different transform (e.g., plotting a linear plot along side a polar plot takes a little more work.

Axes Subplots by panel

So far we have declared all of our subplots in a figure at once. That works so far and will probably serve you well in most uses. But one problem with this setup is that you can either declare all of your plots up front with a single "transform" or "projection." Otherwise you have to do them one at a time.

When I say projection and transform, I mean the "mapping" in your plotting coordinate space. For example cartesian is just x and y. Polar projections are mapped to angle and radius. Map projections are mapped to a map east and north distance from a given point but you can "transform" a plot statement to be in lat-lon space.

If you want one graph a plain cartesian plot, another in Merator map coordinates, a third in polar coordinates, and a fourth in polar sterographic map coordinates. You need to overwrite each subplot with its own new subplot. It's not that hard but it looks wierd when you do it.

Here we take the command matplotlib.pyplot.subplot() (singular!)

This will drop ONE Axes workspace for you.

BUT. The coordinates arguments are wierd. Here is the syntax we'll use below to place our polar plot in the second Axes position.

ax[1] = plt.subplot(1,  # put me in row-positon 1 where 1 is the starting value)
                    2,  # put me in column-positon 2 where 1 is the starting)
                    2,  # The full space of work will be in position 2
                        #    starting at 1.  You acutally can use this third
                        #    position to take control of more than one Axes
                        #    space for one graph.  I haven't tried this nor
                        #    do I plan to in the near future because this
                        #    is messy enough!
                    projection='polar') # OK this part is where things make 
                                        # make sense again!  Here is your
                                        # projection in which you will
                                        # warp your cartesian space on the
                                        # 2-d workspace into your new coordinate
                                        # system.

When plotting the default system will match the system here.

Seaborn "Themes"

Unfortunately, some online examples that will sometimes include a "theme" from the Seaborn. Themes are simply a number of preset graphic options that are designed to add some style beyond the boring default fonts and plot layouts that we've done above. It's really a matter of personal tastes.

To activate "themes" you can use the seaborn.set_theme()

There are three basic themes, "darkgrid" (the default), "dark", "white" (my favorite), "whitegrid", and "ticks."

The rotations below will give you a taste of the look-n-feel of these themes.

To "unset" use the seaborn.reset_orig() function.

Conclusion

This sesison has hopefully been able to help you navigate some of the stranger elements of starting to get fancy in Python plotting as well as some of the tutorials where they drop everything down at once.

This session will also hopefully get you ready for other plotting procedures such as geospatial mapping (which will be on another module).

Version Information

There was once a very handy tool that would print version information of the Python version, operating system, and the versions of the Python packages you are running. It leveraged a "magic" command in IPython (which is what lies beneath Jupyter and JuptyerLab notebooks).

The developer has moved on to other things but the resource still has a narrow but ~militant~ ernest fan base (myself included). The original doesn't work with versions of Python above 3.7. So a couple of us wrote some patches to fix it. You can access my version below following these instructions.

  1. If you don't have GIT on your rig yet, you can fetch it via conda
!conda install -y -v git
  1. You can install it using the following command.
!pip install git+https://github.com/wjcapehart/version_information

JupyterLab Caveat

For people using Jupyter Lab, the interface does not play well with this "magic" function. It basically exports it as a JSON object with clicky expander buttons. However if you "Export" your notebook as an HTML or PDF file you should get a reasonable looking exported document.