Monday, September 29th, 2025¶

Figure and axes objects¶

In [2]:
import matplotlib.pyplot as plt
import numpy as np

Suppose we want to draw the unit circle (i.e. the circle of radius $1$ centered at the origin). Recall from MTH 142 that we can describe the unit circle by the parametric equation $$x = \cos t \qquad\qquad y = \sin t \qquad \qquad 0 \leq t \leq 2\pi.$$

Exercise: Use np.linspace, np.pi, np.sin, and np.cos to plot the unit circle using the parametrization above.

In [3]:
t = np.linspace(0, 2*np.pi, 1000)

x = np.cos(t)
y = np.sin(t)

plt.plot(x,y)
Out[3]:
[<matplotlib.lines.Line2D at 0x28cffe6ed50>]
No description has been provided for this image

Notice: the graph does not look circular. We need to adjust the aspect ratio to correct this. First, a little more information on the structure of figures in matplotlib. When plotting with matplotlib.pyplot:

  • Each figure starts with a figure object.
  • Within that figure object, we can place an axes object (or several).
  • On an axes object, we can draw things (lines, points, etc.).

When we call plt.plot (in the absence of other commands), matplotlib automatically generates a figure and axes object. We can also take control of that process ourselves:

  • Calling plt.figure() will generate a figure object.

Exercise: Create two figures, one which graphs $x$ vs $y$ (as in the previous exercise), and another that graphs both $x(t)$ and $y(t)$ vs $t$.

In [6]:
plt.figure()
plt.plot(x,y, label='$x(t)$ vs $y(t)$')
plt.legend()

plt.figure()
plt.plot(t, x, label='$x(t)$ vs $t$')
plt.plot(t, y, label='$y(t)$ vs $t$')

plt.legend()
Out[6]:
<matplotlib.legend.Legend at 0x28c81d1cf50>
No description has been provided for this image
No description has been provided for this image

By taking control of defining the figure object, we can include numerous optional arguments to change the default behavior. For example, the optional argument figsize=(<horizontal size>, <vertical size>) can be used to change the size of the figure. Let's graph the circle again, but this time specify a figure size that is square:

In [7]:
plt.figure(figsize=(3,3))
plt.plot(x,y)
Out[7]:
[<matplotlib.lines.Line2D at 0x28c81f25bd0>]
No description has been provided for this image

Now suppose we want to graph a second circle centered at $(3,0)$.

In [14]:
plt.figure(figsize=(3,3))
plt.plot(x,y)
plt.plot(3 + x, y)
Out[14]:
[<matplotlib.lines.Line2D at 0x28c819ec190>]
No description has been provided for this image

We see that neither circle is circular. Unfortunately, using the figure size is not a robust way to set aspect ratios. Instead, we can take control of the aspect ratio through the axes object. To do so, we will need to add an axes object ourselves. Calling the plt.subplot function is one mechanism for adding an axes object to a figure.

In [16]:
plt.figure(figsize=(3,3))
plt.subplot()

plt.plot(x,y)
plt.plot(3+x, y)
Out[16]:
[<matplotlib.lines.Line2D at 0x28c81e351d0>]
No description has been provided for this image

The plt.subplot function can take in an optional argument, aspect. We can use aspect='equal' to force an equal aspect ratio.

Note: by default, Pyplot will shrink the figure to (approximately) the extents of the data. As a result, when forcing an equal aspect ratio, the resulting figure may end up smaller than the specified figsize.

In [17]:
plt.figure(figsize=(3,3))
plt.subplot(aspect='equal')

plt.plot(x,y)
plt.plot(3+x, y)
Out[17]:
[<matplotlib.lines.Line2D at 0x28c81e9d310>]
No description has been provided for this image
In [18]:
plt.figure(figsize=(3,3))
plt.subplot(aspect='equal')

plt.plot(x,y)
plt.plot(3+x, y)

plt.ylim(-3,3)
Out[18]:
(-3.0, 3.0)
No description has been provided for this image

Subplots¶

We can call plt.subplot several times to create several axes objects within the same figure. In particular: plt.subplot(r, c, n) will create an axes object positioned within an r by c grid of subplots and insert the axes object in the nth position. These subplot positions are counted starting in the top-left corner, then proceeding left-to-right, then top-to-bottom.

Note: Because matplotlib.pyplot is an implementation of plotting functions from MATLAB, which is not a 0-based indexing language, the subplot positions are counted starting from 1 rather than 0.

In [23]:
plt.figure(figsize=(5,5))

plt.subplot(2,2,1, aspect='equal')
plt.plot(x,y)

plt.subplot(2,2,4)
plt.plot(x,y)
plt.plot(3+x,y)
Out[23]:
[<matplotlib.lines.Line2D at 0x28c8329b250>]
No description has been provided for this image

Exercise: Create a $3\times 4$ grid of subplots, where the $n^\text{th}$ subplot graphs $y = \cos(nt)$ over the interval $0 \leq t \leq 2\pi$ for $n = 1, 2, \dots, 12$. Add a title to each subplot identifying which function is being graphed.

In [27]:
plt.figure(figsize=(6,6))

for n in range(1,13):
    plt.subplot(3,4,n)
    plt.plot(t, np.cos(n*t))
    plt.title('$y = \cos {}t$'.format(n))
No description has been provided for this image

The plt.suptitle function can be used to add a title to a figure with several subplots. The plt.tight_layout function can be called to have Pyplot automatically adjust the subplot spacing to remove extraneous white space.

In [31]:
plt.figure(figsize=(6,6))

for n in range(1,13):
    plt.subplot(3,4,n)
    plt.plot(t, np.cos(n*t))
    plt.title('$y = \cos {}t$'.format(n))

plt.suptitle('Subplots')
plt.tight_layout()
No description has been provided for this image

The arrangment of subplots can be quite flexible. For example, we can add subplots as parts of different grid layouts. Suppose we want to add three subplots to a figure, where the first two subplots are side by side in the first at the top of the figure, and the third subplot sits below the two upper subplots and spans the width of the figure.

We can accomplish this by adding the first two subplots as part of a $2 \times 2$ grid in the first and second positions, then adding the third subplot as part of a $2 \times 1$ grid in the second position.

In [33]:
plt.figure(figsize=(5,5))

plt.subplot(2,2,1, aspect='equal')
plt.plot(x,y)

plt.subplot(2,2,2, aspect='equal')
plt.plot(x,y)
plt.plot(3 + x, y)

plt.subplot(2,1,2)
plt.plot(t,x)
plt.plot(t,y)
Out[33]:
[<matplotlib.lines.Line2D at 0x28c87fd4690>]
No description has been provided for this image
In [34]:
plt.figure(figsize=(5,5))

plt.subplot(3,1,1)
plt.plot(t, t**2)

plt.subplot(3,2,3, aspect='equal')
plt.plot(x,y)

plt.subplot(3,2,4, aspect='equal')
plt.plot(x,y)
plt.plot(3 + x, y)

plt.subplot(3,1,3)
plt.plot(t,x)
plt.plot(t,y)
Out[34]:
[<matplotlib.lines.Line2D at 0x28c880a8550>]
No description has been provided for this image

When adding a subplot with plt.subplot, the created axes object becomes "active", meaning that all plotting commands (e.g. plt.plot, plt.title, plt.legend, etc.) are directed toward the active axes. Sometimes we may want to go back and activate an earlier axes object to make additional changes. As long as we've stored the desired axes object (e.g. ax = plt.subplot(...)), then we can use the plt.axes function to re-activate the axes object and continue plotting to it.

In [37]:
plt.figure(figsize=(5,5))

top_subplot = plt.subplot(3,1,1)
plt.plot(t, t**2)

middle_left_subplot = plt.subplot(3,2,3, aspect='equal')
plt.plot(x,y)

middle_right_subplot = plt.subplot(3,2,4, aspect='equal')
plt.plot(x,y)
plt.plot(3 + x, y)

bottom_subplot = plt.subplot(3,1,3)
plt.plot(t,x)
plt.plot(t,y)

plt.axes(middle_left_subplot)
plt.title('Circle plot')

plt.axes(middle_right_subplot)
plt.title('Plot with two circles')

plt.tight_layout()
No description has been provided for this image

The plt.axes function can also be used to insert an axes object with arbitrary position and size. To use plt.axes in this way, we supply a list input [x_left, y_bottom, width, height] where

  • x_left is the left edge of the axes object (measured between 0 and 1 where 0 is the at the far left and 1 is at the far right)
  • y_bottom is the bottom edge of the axes object (measured between 0 and 1 where 0 is at the bottom and 1 is at the top)
  • width is the width of the axes object (measured between 0 and 1 as a proportion of the full figure width)
  • height is the height of the axes object (measured between 0 and 1 as a proportion of the full figure height)
In [39]:
plt.figure(figsize=(5,5))

top_subplot = plt.subplot(3,1,1)
plt.plot(t, t**2)

middle_left_subplot = plt.subplot(3,2,3, aspect='equal')
plt.plot(x,y)

middle_right_subplot = plt.subplot(3,2,4, aspect='equal')
plt.plot(x,y)
plt.plot(3 + x, y)

bottom_subplot = plt.subplot(3,1,3)
plt.plot(t,x)
plt.plot(t,y)

plt.axes(middle_left_subplot)
plt.title('Circle plot')

plt.axes(middle_right_subplot)
plt.title('Plot with two circles')

plt.tight_layout()

plt.axes([.3,.1, .5, .4])
plt.plot(t, np.sin(2*t))
Out[39]:
[<matplotlib.lines.Line2D at 0x28c86ae7250>]
No description has been provided for this image

Multi-dimensional arrays¶

So far, we've been working with 1-dimensional arrays (essentially lists of numbers).

In [40]:
np.arange(10)
Out[40]:
array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])

We can also create multi-dimensional NumPy arrays. For example, we can reshape a 1D array into a compatible 2D shape using the .reshape method. The .reshape method takes in a sequence of integers that define the shape of the output array. For example, calling .reshape(2,5) on an array of length 10 will create an array containing 2 rows and 5 columns.

In [41]:
np.arange(10).reshape(2,5)
Out[41]:
array([[0, 1, 2, 3, 4],
       [5, 6, 7, 8, 9]])

Just like with 1D arrays, we can perform arithmetic operations elementwise on 2D arrays:

In [42]:
my_array = np.arange(30).reshape(5,6)
print(my_array)
[[ 0  1  2  3  4  5]
 [ 6  7  8  9 10 11]
 [12 13 14 15 16 17]
 [18 19 20 21 22 23]
 [24 25 26 27 28 29]]
In [43]:
3*my_array
Out[43]:
array([[ 0,  3,  6,  9, 12, 15],
       [18, 21, 24, 27, 30, 33],
       [36, 39, 42, 45, 48, 51],
       [54, 57, 60, 63, 66, 69],
       [72, 75, 78, 81, 84, 87]])
In [44]:
my_array**2
Out[44]:
array([[  0,   1,   4,   9,  16,  25],
       [ 36,  49,  64,  81, 100, 121],
       [144, 169, 196, 225, 256, 289],
       [324, 361, 400, 441, 484, 529],
       [576, 625, 676, 729, 784, 841]])
In [45]:
2**my_array
Out[45]:
array([[        1,         2,         4,         8,        16,        32],
       [       64,       128,       256,       512,      1024,      2048],
       [     4096,      8192,     16384,     32768,     65536,    131072],
       [   262144,    524288,   1048576,   2097152,   4194304,   8388608],
       [ 16777216,  33554432,  67108864, 134217728, 268435456, 536870912]])

Each array has an attribute .shape that stores the shape of the array:

In [46]:
my_array.shape
Out[46]:
(5, 6)

We can also use np.ones or np.zeros to generate multi-dimensional arrays:

In [48]:
np.ones((5,6))
Out[48]:
array([[1., 1., 1., 1., 1., 1.],
       [1., 1., 1., 1., 1., 1.],
       [1., 1., 1., 1., 1., 1.],
       [1., 1., 1., 1., 1., 1.],
       [1., 1., 1., 1., 1., 1.]])
In [49]:
np.zeros((3,4))
Out[49]:
array([[0., 0., 0., 0.],
       [0., 0., 0., 0.],
       [0., 0., 0., 0.]])

We can use plt.array to convert a list of lists into an array. This only works if each "inner" list has the same length. For example, consider the a list of Pythagorean triples:

In [50]:
ptriples = [[3, 4, 5],
[4, 3, 5],
[5, 12, 13],
[6, 8, 10],
[8, 6, 10],
[8, 15, 17],
[9, 12, 15],
[12, 5, 13],
[12, 9, 15],
[12, 16, 20],
[15, 8, 17],
[15, 20, 25],
[16, 12, 20],
[20, 15, 25]]

ptriples_array = np.array(ptriples)
print(ptriples_array)
[[ 3  4  5]
 [ 4  3  5]
 [ 5 12 13]
 [ 6  8 10]
 [ 8  6 10]
 [ 8 15 17]
 [ 9 12 15]
 [12  5 13]
 [12  9 15]
 [12 16 20]
 [15  8 17]
 [15 20 25]
 [16 12 20]
 [20 15 25]]
In [51]:
ptriples_array.shape
Out[51]:
(14, 3)

With multi-dimensional arrays, we can slice across any axis of the array. This works just like regular list/array slicing, except that we can separately slice through a row, columm, or any other axis. For example, taking [:,0] will produce a slice consisting of elements from every row, but only the first column (e.g. the index-0 column).

In [53]:
print(ptriples_array[:,0])
[ 3  4  5  6  8  8  9 12 12 12 15 15 16 20]
In [61]:
a = ptriples_array[:,0]
b = ptriples_array[:,1]
c = ptriples_array[:,2]

plt.figure(figsize=(3,3))

plt.subplot(aspect='equal')
plt.plot(a,b,'o')
plt.xlabel('$a$')
plt.ylabel('$b$')
plt.title('Pythagorean doubles')

plt.xlim(0,21)
plt.ylim(0,21)
Out[61]:
(0.0, 21.0)
No description has been provided for this image

Back to Project 2: Pythagorean triples¶

It will be useful for the project to consider what are called, "primitive Pythagorean triples". We say that a Pythagorean triple $(a,b,c)$ is primitive if the greatest common divisor of $a$, $b$, and $c$ is $1$.

Math exercise: A Pythagorean triple $(a,b,c)$ is primitive if and only if the greatest common divisor of $a$ and $b$ is $1$.

We will call a Pythagorean double $(a,b)$ primitive if $a$ and $b$ have greatest common divisor $1$. It will be useful for the project to also plot only the primitive Pythagorean doubles. To that end, can we calculated greatest common divisors? Let's write our own greatest_common_divisor function:

In [64]:
def greatest_common_divisor(a,b):
    gcd = 1
    for n in range(2, min(a,b) + 1):
        if a % n == 0 and b % n == 0:
            gcd = n
    return gcd
In [67]:
greatest_common_divisor(10,15)
Out[67]:
5
In [68]:
greatest_common_divisor(2**3 * 3**4, 2**2 * 3**2)
Out[68]:
36

Another way of calculating greatest common divisors is called "the Euclidean algorithm." The algorithm works as follows:

  • Start with integers $a$ and $b$, and assume that $a \leq b$
  • Subtract the smaller $a$ from the larger $b$, and replace the larger $b$ with the difference $b - a$
  • Repeat step 2 until one of the numbers is $0$. The other will be the greatest common divisor

Exercise: Write a function to implement the Euclidean algorithm

In [69]:
def Euclidean_algorithm(a,b):
    while a != 0 and b != 0:
        if a > b:
            a,b = b,a
        b = b - a
    return a
In [70]:
Euclidean_algorithm(15,10)
Out[70]:
5
In [71]:
Euclidean_algorithm(2**3 * 3**4, 2**2 * 3**2)
Out[71]:
36