Monday, September 29th, 2025¶
Figure and axes objects¶
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.
t = np.linspace(0, 2*np.pi, 1000)
x = np.cos(t)
y = np.sin(t)
plt.plot(x,y)
[<matplotlib.lines.Line2D at 0x28cffe6ed50>]
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$.
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()
<matplotlib.legend.Legend at 0x28c81d1cf50>
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:
plt.figure(figsize=(3,3))
plt.plot(x,y)
[<matplotlib.lines.Line2D at 0x28c81f25bd0>]
Now suppose we want to graph a second circle centered at $(3,0)$.
plt.figure(figsize=(3,3))
plt.plot(x,y)
plt.plot(3 + x, y)
[<matplotlib.lines.Line2D at 0x28c819ec190>]
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.
plt.figure(figsize=(3,3))
plt.subplot()
plt.plot(x,y)
plt.plot(3+x, y)
[<matplotlib.lines.Line2D at 0x28c81e351d0>]
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
.
plt.figure(figsize=(3,3))
plt.subplot(aspect='equal')
plt.plot(x,y)
plt.plot(3+x, y)
[<matplotlib.lines.Line2D at 0x28c81e9d310>]
plt.figure(figsize=(3,3))
plt.subplot(aspect='equal')
plt.plot(x,y)
plt.plot(3+x, y)
plt.ylim(-3,3)
(-3.0, 3.0)
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 n
th 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
.
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)
[<matplotlib.lines.Line2D at 0x28c8329b250>]
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.
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))
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.
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()
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.
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)
[<matplotlib.lines.Line2D at 0x28c87fd4690>]
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)
[<matplotlib.lines.Line2D at 0x28c880a8550>]
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.
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()
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 between0
and1
where0
is the at the far left and1
is at the far right)y_bottom
is the bottom edge of the axes object (measured between0
and1
where0
is at the bottom and1
is at the top)width
is the width of the axes object (measured between0
and1
as a proportion of the full figure width)height
is the height of the axes object (measured between0
and1
as a proportion of the full figure height)
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))
[<matplotlib.lines.Line2D at 0x28c86ae7250>]
Multi-dimensional arrays¶
So far, we've been working with 1-dimensional arrays (essentially lists of numbers).
np.arange(10)
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.
np.arange(10).reshape(2,5)
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:
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]]
3*my_array
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]])
my_array**2
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]])
2**my_array
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:
my_array.shape
(5, 6)
We can also use np.ones
or np.zeros
to generate multi-dimensional arrays:
np.ones((5,6))
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.]])
np.zeros((3,4))
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:
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]]
ptriples_array.shape
(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).
print(ptriples_array[:,0])
[ 3 4 5 6 8 8 9 12 12 12 15 15 16 20]
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)
(0.0, 21.0)
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:
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
greatest_common_divisor(10,15)
5
greatest_common_divisor(2**3 * 3**4, 2**2 * 3**2)
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
def Euclidean_algorithm(a,b):
while a != 0 and b != 0:
if a > b:
a,b = b,a
b = b - a
return a
Euclidean_algorithm(15,10)
5
Euclidean_algorithm(2**3 * 3**4, 2**2 * 3**2)
36