6.2. Figures and Axes#

Hide code cell source

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt

6.2.1. Object-Oriented Plotting with plt.subplots()#

Note

But what’s the difference between a figure and an axes?

Figure

Axes

Analogy

The full canvas/window

One plot region on that canvas

What it is

The top-level container

The area where data is drawn

Contains

One or more Axes

X/Y axis, ticks, lines, labels, title, legend

Commonly created by

plt.figure() or plt.subplots()

returned by plt.subplots() or added to a figure

A good mental model is:

Figure
└── Axes (a.k.a. one plot)
    ├── XAxis
    ├── YAxis
    ├── Title
    ├── Lines / Bars / etc.
    └── Legend

Here we will learn two OO workflows:

  • plt.subplots() for the standard, everyday workflow

  • plt.figure() + fig.add_axes() for manual placement and inset-style layouts

6.2.1.1. plt.subplots()#

plt.subplots() creates the figure and axes objects in one step and returns them as a tuple.

  • If you create a single subplot, you typically get one Axes object.

  • If you create multiple subplots, you get a NumPy array of Axes objects.

Typical syntax:

fig, ax = plt.subplots()

The example below shows the standard one-axes case.

month = np.arange(1, 7)                               ### months 1 to 6 (Jan to Jun)
revenue = np.array([110, 104, 118, 121, 125, 119])    ### monthly revenue in $K

# fig, ax = plt.subplots()              ### single Axes case
# fig, axes = plt.subplots(1, 2)        ### multiple Axes case

fig, ax = plt.subplots(figsize=(4,3))   ### unpacking in one step
ax.plot(month, revenue, 'g--')          ### plot on that axes

### common axes methods
ax.set_xlabel('Month')
ax.set_ylabel('Revenue ($K)')
ax.set_title('Monthly Revenue')
plt.show()
../../_images/1b6ea23bd498e1427100cec159b000afc0433f8df214497512185a5895c9b222.png

With subplots(), you can specify the number of rows and columns using the nrows= and ncols= parameters when creating the subplots() object. The example below creates an empty canvas of 1-row by 2-cols subplots.

fig, axes = plt.subplots(nrows=1, ncols=2, figsize=(6, 2))  ### similar to plt.subplot(1, 2, ith axes)
# fig, axes = plt.subplots(1, 2)            ### same as above
../../_images/614fc49c948c7302f3de9adbaf9c4ea5f621c8c709bba7749d34db33ab474e84.png

In this example above, axes is an array variable containing two Axes objects. Looping over that array is a common pattern when you want to apply the same formatting to each subplot.

print(axes)
[<Axes: > <Axes: >]

Now we have access over the two Axes individually using indexing ([0] and [1]). We may plot them separately. Plotting functions from pandas and Seaborn (Chapter 07) also accept an ax= argument, so you can draw their charts into one of these panels, for example df.plot(ax=axes[0]).

month = np.arange(1, 7)
store_a = np.array([62, 58, 66, 70, 73, 69])          ### monthly sales in $K
store_b = np.array([48, 51, 47, 55, 58, 60])

fig, axes = plt.subplots(nrows=1, ncols=2, figsize=(6, 2))

axes[0].plot(month, store_a, 'r--')    ### plot on the first axes
axes[0].set_title('Store A')           ### set title for the first axes

axes[1].plot(month, store_b, 'g*-')    ### plot on the second axes
axes[1].set_title('Store B')           ### set title for the second axes
plt.show()
../../_images/62193c22670ebf3eb297d416bbc12209a737c3168b75fbfc2f109bf78e2703d6.png

It’s not uncommon to loop through the Axes object then fine tune the plots. zip() pairs each axes with its data and title, so one loop formats both charts.

month = np.arange(1, 7)
store_a = np.array([62, 58, 66, 70, 73, 69])          ### monthly sales in $K
store_b = np.array([48, 51, 47, 55, 58, 60])

fig, axes = plt.subplots(nrows=1, ncols=2, figsize=(6, 2))

for ax, sales, name in zip(axes, [store_a, store_b], ['Store A', 'Store B']):
    ax.plot(month, sales, 'b')
    ax.set_xlabel('Month')
    ax.set_ylabel('Sales ($K)')
    ax.set_title(name)

plt.show()
../../_images/18c79d0e5b5273e58a2c46b9d6436eba8b6dfbf0a1f60b7195f774fb5b5b3920.png

A common issue in Matplotlib is overlapping subplot labels or titles. plt.tight_layout() can automatically adjust spacing so the figure is easier to read. (compare with the plots above)

month = np.arange(1, 7)
store_a = np.array([62, 58, 66, 70, 73, 69])          ### monthly sales in $K
store_b = np.array([48, 51, 47, 55, 58, 60])

fig, axes = plt.subplots(nrows=1, ncols=2, figsize=(6, 2))

for ax, sales, name in zip(axes, [store_a, store_b], ['Store A', 'Store B']):
    ax.plot(month, sales, 'g--')
    ax.set_xlabel('Month')
    ax.set_ylabel('Sales ($K)')
    ax.set_title(name)

plt.tight_layout()                     ### fix overlapping labels and titles
../../_images/a34160ec3c49aede3c8bb98bd615ca727871aefac04faf97ca5fc26d669d067c.png

Each ax.set_title() names one axes. To give the whole figure one title, use fig.suptitle(), which places the title above all the subplots. This is common in dashboards, where several charts support one message. The x values here are quarter labels rather than numbers; Matplotlib places text labels along the axis in the order given. sharey=True gives every subplot the same y-axis range. Without it, each subplot scales to its own data, and a small region can look as large as a big one.

quarters = ["Q1", "Q2", "Q3", "Q4"]
north = [150, 162, 158, 171]
south = [120, 118, 131, 127]

fig, axes = plt.subplots(1, 2, figsize=(8, 3), sharey=True)   ### sharey: same y scale for a fair comparison

axes[0].plot(quarters, north, marker='o')
axes[0].set_title('North')

axes[1].plot(quarters, south, marker='o')
axes[1].set_title('South')

fig.suptitle('Quarterly Revenue by Region ($K)')   ### one title for the whole figure
plt.tight_layout()
../../_images/c8fe787d7bec2d58f1a4d47974875f0df120c20e7523d70f0af48f436cdf8e71.png
### Exercise: Side-by-Side Subplots with plt.subplots()
#   1. Create month = np.arange(1, 7),
#      east = np.array([95, 99, 104, 101, 110, 116]), and
#      west = np.array([70, 68, 74, 79, 77, 83]).
#   2. Use plt.subplots(1, 2, figsize=(8, 3)) to create a figure and two axes.
#   3. On axes[0], plot month vs east with 'b-', title "East",
#      x-label "Month", and y-label "Revenue ($K)".
#   4. On axes[1], plot month vs west with 'r--', title "West",
#      x-label "Month", and y-label "Revenue ($K)".
#   5. Call plt.tight_layout() at the end.
### Your code starts here.




### Your code ends here.

Hide code cell source

### Solution
import numpy as np
import matplotlib.pyplot as plt

month = np.arange(1, 7)
east = np.array([95, 99, 104, 101, 110, 116])
west = np.array([70, 68, 74, 79, 77, 83])

fig, axes = plt.subplots(1, 2, figsize=(8, 3))
axes[0].plot(month, east, 'b-')
axes[0].set_title('East')
axes[0].set_xlabel('Month')
axes[0].set_ylabel('Revenue ($K)')
axes[1].plot(month, west, 'r--')
axes[1].set_title('West')
axes[1].set_xlabel('Month')
axes[1].set_ylabel('Revenue ($K)')
plt.tight_layout()
plt.show()
../../_images/81345ca6e41f2b90dfb394fa8a0ac6d120144820a8abf2b543747e3fa92adff4.png

6.2.2. Manual Placement with add_axes()#

fig.add_axes() is a more manual OO technique than plt.subplots(). It is most useful when you want precise axes placement, such as inset plots or custom layouts.

The process of using add_axes() to create plots is:

  1. Create a Figure object.

  2. Add one or more Axes using normalized figure coordinates [left, bottom, width, height].

  3. Plot and format each axes separately.

add_axes() uses normalized figure coordinates. For example, [0.1, 0.1, 0.8, 0.8] means:

  • left edge at 10% of the figure width

  • bottom edge at 10% of the figure height

  • axes width equal to 80% of the figure width

  • axes height equal to 80% of the figure height

fig = plt.figure(figsize=(4,3))             ### 1. create an empty figure
ax = fig.add_axes([0.1, 0.1, 0.8, 0.8])     ### 2. add one axes using normalized coordinates
../../_images/31d5c5ac541539a430387124ad0057757782e1d3902fd1ea62945a29cf206322.png

While slightly more involved, add_axes() gives you precise control over where each axes sits in the figure. That makes it a good choice for inset plots and manually arranged layouts.

day = np.arange(1, 31)                  ### days in June
daily_sales = np.array([41, 39, 43, 40, 42, 44, 38, 41, 45, 43,
                        40, 42, 44, 46, 61, 68, 72, 75, 70, 66,
                        63, 47, 44, 42, 45, 43, 41, 44, 46, 42])   ### $K; promotion on days 15-21

fig = plt.figure(figsize=(4,3))         ### create empty canvas on the figure (object)

### add two axes to the figure
axes1 = fig.add_axes([0.1, 0.1, 0.8, 0.8])     ### main axes
axes2 = fig.add_axes([0.2, 0.5, 0.25, 0.3])    ### inset axes, over the flat early days

### main axes: the whole month
axes1.plot(day, daily_sales, 'b')
axes1.set_xlabel('Day')
axes1.set_ylabel('Sales ($K)')
axes1.set_title('Daily Sales in June')

### inset axes: zoom in on the promotion week
axes2.plot(day[14:21], daily_sales[14:21], 'r')
axes2.set_title('Promotion Week')
plt.show()
../../_images/888a14aa6b5c388f3fe1d212a428bb9892b40bc2fd956922531be191ec26a456.png
### Exercise: Figure with Main Plot and Inset using plt.figure()
#   1. Create month = np.arange(1, 7),
#      units = np.array([540, 510, 580, 600, 620, 590]), and
#      returns = np.array([22, 25, 19, 18, 21, 17]).
#   2. Create a figure with figsize=(4, 3).
#   3. Add a main axes at [0.1, 0.1, 0.8, 0.8] and plot month vs units in blue.
#      Set the title to "Units Sold".
#   4. Add an inset axes at [0.55, 0.15, 0.3, 0.3] and plot month vs returns in red.
#      Set the title to "Returns".
### Your code starts here.




### Your code ends here.

Hide code cell source

### Solution
import numpy as np
import matplotlib.pyplot as plt

month = np.arange(1, 7)
units = np.array([540, 510, 580, 600, 620, 590])
returns = np.array([22, 25, 19, 18, 21, 17])

fig = plt.figure(figsize=(4, 3))
main_ax = fig.add_axes([0.1, 0.1, 0.8, 0.8])
main_ax.plot(month, units, color='blue')
main_ax.set_title('Units Sold')
inset_ax = fig.add_axes([0.55, 0.15, 0.3, 0.3])
inset_ax.plot(month, returns, color='red')
inset_ax.set_title('Returns')
plt.show()
../../_images/4c4572485125d6b0135503ccd2a730a51d29989009f094652c44684490566164.png

6.2.3. Figure Size, Resolution, and Saving#

Three figure-level settings decide how a chart looks outside the notebook: its size in inches, its resolution in dots per inch, and the file it is saved to. These matter most when a chart goes into a report or slide deck.

6.2.3.1. figsize=#

Figure size and DPI are two of the most common figure-level controls.

  • figsize=(width, height) uses inches.

This can be set with plt.figure(...) or plt.subplots(...).

month = np.arange(1, 7)                               ### months 1 to 6 (Jan to Jun)
revenue = np.array([110, 104, 118, 121, 125, 119])    ### monthly revenue in $K

fig, axes = plt.subplots(figsize=(4,3))

### peripherals for the axes object
axes.plot(month, revenue, 'r')
axes.set_xlabel('Month')
axes.set_ylabel('Revenue ($K)')
axes.set_title('Monthly Revenue')
plt.show()
../../_images/5c0958b06c4f1faccdcc7fd794bd8ce5b10b5f86d1e7dcf658d28cb34a6acaab.png

6.2.3.2. DPI#

  • dpi sets dots per inch, which affects rendered resolution.

month = np.arange(1, 7)                               ### months 1 to 6 (Jan to Jun)
revenue = np.array([110, 104, 118, 121, 125, 119])    ### monthly revenue in $K

fig = plt.figure(figsize=(3,1), dpi=50)          ### low resolution
axes = fig.add_axes([0.1, 0.1, 0.8, 0.8])
axes.plot(month, revenue, 'r')
axes.set_xlabel('Month')
axes.set_ylabel('$K')
axes.set_title('Monthly Revenue')
plt.show()
../../_images/13d1458565421033bcad4b5567d9d11071de40c95552e8890f5fa973a835f6b5.png
month = np.arange(1, 7)                               ### months 1 to 6 (Jan to Jun)
revenue = np.array([110, 104, 118, 121, 125, 119])    ### monthly revenue in $K

fig = plt.figure(figsize=(3,1), dpi=100)         ### same size, twice the resolution
axes = fig.add_axes([0.1, 0.1, 0.8, 0.8])
axes.plot(month, revenue, 'r')
axes.set_xlabel('Month')
axes.set_ylabel('$K')
axes.set_title('Monthly Revenue')
plt.show()
../../_images/7556753511f14395d4ad8cdb66bad2bb8f7c3e0fd224df5a2e1dab96d504befd.png
### Exercise: Setting Figure Size and DPI
#   1. Create month = np.arange(1, 7) and
#      revenue = np.array([110, 104, 118, 121, 125, 119]).
#   2. Create a figure with figsize=(4, 3) and dpi=150.
#   3. Add axes using fig.add_axes([0.1, 0.1, 0.8, 0.8]).
#   4. Plot month vs revenue with a green line.
#   5. Set the title to "Monthly Revenue".
### Your code starts here.




### Your code ends here.

Hide code cell source

### Solution
import numpy as np
import matplotlib.pyplot as plt

month = np.arange(1, 7)
revenue = np.array([110, 104, 118, 121, 125, 119])

fig = plt.figure(figsize=(4, 3), dpi=150)
ax = fig.add_axes([0.1, 0.1, 0.8, 0.8])
ax.plot(month, revenue, color='green')
ax.set_title('Monthly Revenue')
plt.show()
../../_images/a871e59f029e914efb07ec0c06931822cf431a05c7fd5803c3c64d770272f64c.png

6.2.3.3. Saving Figures#

Matplotlib can save high-quality output in formats such as PNG, JPG, SVG, and PDF.

Use the figure’s .savefig() method. The file type follows the extension, such as .png, .pdf, or .svg. Use dpi= to set the resolution of the saved file: the default is about 100 dots per inch, which looks fine on screen, while 200 to 300 is typical for printed reports. In the example below, we create a small figure explicitly before saving it:

import os
output_dir = '../../_build/generated'
os.makedirs(output_dir, exist_ok=True)
output_file = os.path.join(output_dir, 'monthly_revenue.png')

month = np.arange(1, 7)                               ### months 1 to 6 (Jan to Jun)
revenue = np.array([110, 104, 118, 121, 125, 119])    ### monthly revenue in $K

fig, ax = plt.subplots(figsize=(4, 3))
ax.plot(month, revenue)
ax.set_title('Monthly Revenue')

fig.savefig(output_file, dpi=200)     ### dpi= sets the resolution of the saved file
print(f"Saved: {output_file}")
Saved: ../../_build/generated/monthly_revenue.png
../../_images/744c58e7bd376394789b489f4d5b379f7883f38c50bbfb4ff40ef0739829466f.png
### Exercise: Save a Chart for a Report
#   1. Create quarters = ["Q1", "Q2", "Q3", "Q4"] and revenue = [120, 135, 128, 150].
#   2. Use fig, ax = plt.subplots(figsize=(6, 3)) and plot quarters vs revenue
#      with circle markers (marker='o').
#   3. Set the title "Quarterly Revenue".
#   4. Save the figure as "quarterly_revenue.png" with dpi=150.
### Your code starts here.




### Your code ends here.

Hide code cell source

### Solution
quarters = ["Q1", "Q2", "Q3", "Q4"]
revenue = [120, 135, 128, 150]

fig, ax = plt.subplots(figsize=(6, 3))
ax.plot(quarters, revenue, marker='o')
ax.set_title("Quarterly Revenue")
fig.savefig("quarterly_revenue.png", dpi=150)
../../_images/337e3c8fffb09b988d790531d0dec83089cc9740dc7035cb844ebe54702ecebb.png

6.2.4. Summary#

  • A figure is the whole canvas; an axes is one chart on it. Most formatting is done with axes methods such as ax.set_title(), ax.set_xlabel(), and ax.set_ylabel().

  • fig, ax = plt.subplots() creates one chart. fig, axes = plt.subplots(nrows, ncols) returns an array of axes that you can index (axes[0]) or loop over with zip().

  • plt.tight_layout() fixes overlapping labels, fig.suptitle() titles the whole figure, and sharey=True puts subplots on the same scale so they compare fairly.

  • fig.add_axes([left, bottom, width, height]) places an axes by fractions of the figure, which is how inset charts are made.

  • figsize= sets size in inches and dpi= sets resolution. fig.savefig("file.png", dpi=200) saves a chart for a report; the file extension sets the format.

6.2.5. Further Reading#