5

I keep asking this question to myself: how to combine easily different plots with matplotlib, in a modular way?

Let's say for instance that I wrote a function displaying the positions of the nodes of a graph. In parallel, I made another function which plots some polygons. Now, what is the right way of combining the outputs, so the nodes appear to be inside the polygons? What about the possibility to change the transparency, positions, etc, of the entire individual plots? What should be the structure of the two initial functions?

Is there a smart and general way of doing this?

Benares
  • 1,186
  • 1
  • 7
  • 13
  • 1
    Just be sure your function takes an axes argument, traditionally called `ax`. Use `ax` for all your plotting and drawing. My preference is to also return `ax`, but that's not necessary in Python: you can just pass it twice. All else equal, the order in which you call your functions will determine what's "on top" in the drawing. – Alan Mar 24 '16 at 16:17
  • Ok, so the idea is to create an axis, input it in the first function to plot the first things, get the axis again as output, input it in the second function to plot the second things, get it as output, and show/save it? – Benares Mar 24 '16 at 18:33

1 Answers1

5

Just to elaborate on what @Alan said, you'd typically structure your plotting functions somewhat similar to this:

import numpy as np
import matplotlib.pyplot as plt

def main():
    data = [np.random.random((2, 3)) for _ in range(5)]
    fig, ax = plt.subplots()
    plot_polygons(data, alpha=0.5, ax=ax)
    plot_verts(data, marker='^', color='black', ax=ax)
    plt.show()


def plot_polygons(data, ax=None, **kwargs):
    if ax is None:
        ax = plt.gca()

    artists = [ax.fill(x, y, **kwargs) for x, y in data]
    return artists

def plot_verts(data, ax=None, **kwargs):
    if ax is None:
        ax = plt.gca()

    artists = [ax.scatter(x, y, **kwargs) for x, y in data]
    return artists

main()

enter image description here

The advantage of this approach is that you could implicitly use the current figure and/or automatically create one. By doing something similar to ax = plt.gca() if ax is None else ax inside your plotting function, you can mix in the pyplot state-machine style if you'd like:

def main():
    data = [np.random.random((2, 3)) for _ in range(5)]
    plot_polygons(data, alpha=0.5)
    plot_verts(data, marker='^', color='black')
    plt.show()

Or you can explicitly specify the Axes instance (which is a better approach in general). This allows you to plot on specific axes in different ways:

data = [np.random.random((2, 3)) for _ in range(5)]

fig, axes = plt.subplots(nrows=2, sharex=True)

axes[0].set(title='Simple Plot', ylabel='Y-label')
plot_verts(data, marker='o', ax=axes[0])

axes[1].set(title='More complex', xlabel='X-label')
plot_polygons(data, ax=axes[1], alpha=0.5, color='gray')
plot_verts(data, ax=axes[1], color='red', marker='s', s=200)

plt.show()

enter image description here


Note that I'm returning the artists that are created, but I haven't used them in any example yet. However, it's a good idea to return the artists, as it allows you to modify their properties later if you need to.

For example, let's put together a simple interactive example that will hide the polygons when you click. I'll redefine the functions from earlier to make this a complete example that you can copy-paste and run:

import numpy as np
import matplotlib.pyplot as plt

def main():
    data = [np.random.random((2, 3)) for _ in range(5)]
    fig, ax = plt.subplots()
    polygons = plot_polygons(data, alpha=0.5, ax=ax, color='gray')
    verts = plot_verts(data, marker='s', color='red', ax=ax, s=200)

    def on_click(event):
        visible = polygons[0][0].get_visible()
        plt.setp(polygons, visible=not visible)
        plt.setp(verts, color=np.random.random(3))
        plt.draw()
    fig.canvas.mpl_connect('button_press_event', on_click)

    ax.set(title='Click on plot to change')
    plt.show()


def plot_polygons(data, ax=None, **kwargs):
    if ax is None:
        ax = plt.gca()

    artists = [ax.fill(x, y, **kwargs) for x, y in data]
    return artists

def plot_verts(data, ax=None, **kwargs):
    if ax is None:
        ax = plt.gca()

    artists = [ax.scatter(x, y, **kwargs) for x, y in data]
    return artists

main()
Joe Kington
  • 275,208
  • 71
  • 604
  • 463
  • Waoh, that's an amazing answer! Your second piece of code concerning the pyplot state-machine style is very useful in my opinion for quick and dirty combined plots with jupyter notebooks, but I can see why it is cleaner to explicitly use the axes. The last part is very interesting too, I never used this `mpl_connect` method, I will definitely have a look. Many thanks. – Benares Mar 29 '16 at 11:51