How to create basic plots with matplotlib.pyplot.plot in Python

How to create basic plots with matplotlib.pyplot.plot in Python

Let’s get one thing straight from the outset: matplotlib.pyplot is not a simple drawing library. It’s a carefully constructed interface to a complex, state-based rendering machine. When you call a function like pyplot.plot(), you aren’t just telling the computer “draw a line.” You are manipulating a hidden state, nudging a complex apparatus into a configuration that will, eventually, produce an image. Understanding this state machine is the key to moving from frustrating, unpredictable results to having the machine bend to your will. Forget everything you think you know about direct-mode graphics; this is a different beast entirely.

Consider the most basic incantation to draw a line:

import matplotlib.pyplot as plt

plt.plot([1, 2, 4, 8, 16])
plt.show()

Simple enough. You pass a list of numbers, and you get a line. But what actually happened? A whole lot more than you might think. When you first called plt.plot(), pyplot peeked behind the curtain and saw there was no active drawing surface-no Figure, in its parlance. So, it created one for you. Think of a Figure as the entire window or the page of a document. It’s the top-level container for everything.

But a Figure is just a blank canvas. You can’t draw data directly onto it. You need a coordinate system, a set of axes. So, after creating a Figure, pyplot then created an Axes object inside it. This Axes object is where the actual plotting happens; it’s the familiar grid with x and y axes, ticks, and labels. Both this new Figure and new Axes were then set as the current Figure and current Axes in pyplot’s internal state machine.

Only then did plt.plot() actually perform its main job. It looked at the data you provided, [1, 2, 4, 8, 16]. Seeing only one sequence of numbers, it assumed these were the y-values and generated the x-values for you-a simple integer sequence starting from 0, so [0, 1, 2, 3, 4]. It then plotted these (x, y) pairs on the current Axes. The subsequent call to plt.show() looks at the current state, finds all the “active” Figure objects, and renders them to the screen.

This implicit, stateful behavior is what allows for the concise scripting style pyplot is known for. Every function call you make, by default, operates on the “current” Figure and Axes. You don’t have to pass around object references constantly. For instance, you can add a label to the x-axis after plotting:

import matplotlib.pyplot as plt

# Pyplot creates a Figure and Axes behind the scenes here
plt.plot([0, 1, 2, 3, 4], [1, 2, 4, 8, 16]) 

# This call knows to target the Axes created by the plot() call above
plt.xlabel("Time (seconds)")

# And this one targets the same Axes
plt.ylabel("Value (units)")

plt.show()

The calls to xlabel() and ylabel() didn’t need to be told which plot to modify. They simply queried the state machine for the current Axes object and performed their work on it. This is powerful, but it’s also a trap for the unwary. If you don’t keep a mental model of the current state, you can easily end up drawing on a plot you didn’t intend to. The machine maintains its state until you explicitly change it or clear it. For example, if you make another call to plot(), it will simply draw another line on the same Axes.

import matplotlib.pyplot as plt

plt.plot([0, 1, 2, 3], [10, 20, 30, 40], label="Series A")
plt.plot([0, 1, 2, 3], [45, 35, 25, 15], label="Series B")

plt.legend()
plt.show()

Here, the second plot() call didn’t create a new Figure or Axes. It found the existing ones from the first call and added a new line to them. The subsequent legend() call also operated on this same Axes, collecting the labels from both plot commands to build its key. This stacking of commands, all implicitly targeting the same object, is the core of the pyplot interface. Wrangling this state is the first, and most critical, step.

Moving from raw pixels to meaningful information

A plot with two lines is a start, but it’s still just pixels on a screen. The default blue and orange lines don’t inherently mean anything. We’ve added labels and a legend, which map the lines to concepts, but the lines themselves are visually identical beyond their color. To truly convey information, we need to control the visual properties of the plot elements. This is where we begin to move from simply telling the machine *what* to draw to telling it *how* to draw it in a way that communicates meaning to a human observer.

Every element you see is customizable. Let’s start with the most basic annotations. We’ve already seen xlabel() and ylabel(). Unsurprisingly, there’s a title() function that works in the exact same way, targeting the current Axes:

import matplotlib.pyplot as plt

plt.plot([0, 1, 2, 3], [10, 20, 30, 40], label="Series A")
plt.plot([0, 1, 2, 3], [45, 35, 25, 15], label="Series B")

plt.xlabel("Time")
plt.ylabel("Measurement")
plt.title("Measurement over Time")
plt.legend()

plt.show()

This adds context, but the data representation is still primitive. To make the lines themselves more expressive, we can pass additional keyword arguments to the plot() function. For instance, we can specify the color, the style of the line, and add markers to indicate the actual data points. A line connecting points can be misleading; showing the discrete points it was generated from is often critical for honest data representation.

import matplotlib.pyplot as plt

# Let's make Series A a green, dashed line with circle markers
plt.plot(
    [0, 1, 2, 3], 
    [10, 20, 30, 40], 
    color='green', 
    linestyle='dashed', 
    marker='o', 
    label="Series A"
)

# And Series B a red, dotted line with square markers
plt.plot(
    [0, 1, 2, 3], 
    [45, 35, 25, 15], 
    color='#FF0000',      # Colors can be hex codes
    linestyle=':',        # Dotted line shorthand
    marker='s',           # Square marker
    label="Series B"
)

plt.title("Detailed Measurement over Time")
plt.legend()
plt.show()

This is far more descriptive. A viewer can now distinguish the series by color, line style, and marker shape, adding multiple layers of visual encoding. But typing out color, linestyle, and marker every time is verbose. The designers of matplotlib, coming from a MATLAB background, anticipated this and provided a powerful, compact syntax called a format string. The format string can be passed as an optional third positional argument to combine color, marker, and line style settings into a single short string.

Let’s rewrite the previous example using this shorthand. The format string is structured as '[color][marker][line]'.

import matplotlib.pyplot as plt

# 'g' for green, 'o' for circle marker, '--' for dashed line
plt.plot([0, 1, 2, 3], [10, 20, 30, 40], 'go--', label="Series A")

# 'r' for red, 's' for square marker, ':' for dotted line
plt.plot([0, 1, 2, 3], [45, 35, 25, 15], 'rs:', label="Series B")

plt.title("Compact Syntax Example")
plt.legend()
plt.show()

The result is identical, but the code is far more concise. This isn’t just a gimmick; it’s a fundamental part of the library’s design philosophy, encouraging rapid, script-based exploration of data. Mastering these format strings is a significant step toward fluent plotting. You can combine any of the single-character color codes (like ‘r’, ‘g’, ‘b’, ‘k’ for black, ‘y’ for yellow), with any of the marker symbols (‘o’, ‘s’, ‘^’, ‘x’, ‘+’), and any of the line styles (‘-‘ for solid, ‘–‘ for dashed, ‘:’ for dotted, ‘-.’ for dash-dot). If you omit a line style, no line will be drawn between the markers; if you omit a marker, you get a continuous line. This gives you fine-grained control over the visual encoding of your data with minimal keystrokes. Beyond simple lines and markers, you can control every conceivable property, from the width of the line to the fill color of the markers.

Bending the rendering engine to your will

The pyplot interface, with its format strings and stateful function calls, is a brilliant piece of engineering for interactive, notebook-style data exploration. It lets you get from data to visualization with a minimum of cognitive overhead. But it’s a facade. A useful one, to be sure, but a facade nonetheless. Behind the simple plt.plot() and plt.title() calls lies the true machinery: an object-oriented hierarchy of graphics primitives. The stateful interface is just a convenient front desk that finds the right object for you and passes your message along. To truly bend the rendering engine to your will, you must bypass the front desk and start manipulating these objects directly. This is the difference between asking a chauffeur to “go downtown” and having the steering wheel in your own hands.

The key is to explicitly create and hold onto the Figure and Axes objects. The most direct way to do this is with the plt.subplots() function. This is the canonical entry point to the object-oriented style. It creates a Figure and a set of subplots (Axes) within it, returning both objects for you to manipulate.

import matplotlib.pyplot as plt

# The magic incantation: create a figure and a single axes object
fig, ax = plt.subplots()

# Now, instead of calling plt.plot(), we call the plot() method of our axes object
ax.plot([0, 1, 2, 3], [45, 35, 25, 15], 'rs:', label="Series B")

# Notice the function names change slightly: plt.xlabel() becomes ax.set_xlabel()
ax.set_xlabel("Time (s)")
ax.set_ylabel("Value")
ax.set_title("Object-Oriented Plotting")
ax.legend()

plt.show()

At first glance, this seems more verbose. Why type ax.set_xlabel() when plt.xlabel() works? Because this code has no ambiguity. It is not dependent on some hidden global state. The ax.plot() call will always draw on that specific axes object, ax, regardless of what other plots may have been created. This makes your code more robust, more readable, and far easier to debug. You are no longer at the mercy of the state machine’s internal pointer to the “current” axes; you are holding the pointer yourself.

The real power becomes apparent when you want more than one plot. The state machine becomes clumsy when managing multiple distinct coordinate systems, but with the object-oriented interface, it’s trivial. You simply ask subplots() for the grid of axes you need.

import matplotlib.pyplot as plt
import numpy as np

# Create a figure and a 2x1 grid of Axes objects
# sharex=True links the x-axes of the two plots
fig, axs = plt.subplots(2, 1, sharex=True)

# axs is now a numpy array containing our two axes objects
# Plot a sine wave on the first axes
x = np.linspace(0, 2 * np.pi, 400)
y1 = np.sin(x**2)
axs[0].plot(x, y1)
axs[0].set_title('Plot 1: sin(x^2)')
axs[0].set_ylabel('Amplitude')
axs[0].grid(True) # We can turn on a grid for just this subplot

# Plot a cosine wave on the second axes
y2 = np.cos(x)
axs[1].plot(x, y2, 'r-')
axs[1].set_title('Plot 2: cos(x)')
axs[1].set_xlabel('Time (s)')
axs[1].set_ylabel('Amplitude')

# This is a figure-level adjustment that cleans up the layout
fig.tight_layout()

plt.show()

This level of control is impossible to achieve cleanly with the stateful interface. Here, we have two distinct plots, each with its own data and labels, but their x-axes are linked. Zooming or panning on one will automatically update the other. We’ve explicitly targeted each drawing command to a specific canvas, axs[0] or axs[1]. We can go further, manipulating the axis limits, tick marks, and even adding arbitrary text or shapes. For example, we can annotate a specific point on the first plot. The ax.text() method lets you place text at a specific coordinate in data space.

import matplotlib.pyplot as plt
import numpy as np

fig, ax = plt.subplots()

x = np.linspace(0, 10, 100)
y = np.sqrt(x)

ax.plot(x, y)
ax.set_title("Annotating Data")

# Add text at data coordinates (x=4, y=2), which is on the line
ax.text(4, 2.1, "Important Point!", color='red')

# We can also add arrows and more complex annotations
ax.annotate(
    'Peak', 
    xy=(9, 3), 
    xytext=(7, 3.5),
    arrowprops=dict(facecolor='black', shrink=0.05),
)

# And force the aspect ratio to be equal
ax.set_aspect('equal')
ax.grid(True)
plt.show()

The annotate method is even more powerful, allowing you to create an arrow that points from a piece of text (at xytext) to a specific data point (at xy). This is the essence of bending the engine to your will: you are no longer just dumping data into a black box but are precisely controlling the final rendered output, element by element. You can control the coordinate system used for placing text, switching between data coordinates, axes-fractional coordinates, or even figure-fractional coordinates, giving you complete control over the placement of every single element on the canvas. For instance, placing a watermark or a global information box is often done using figure coordinates so that it remains fixed even if the data axes are panned or zoomed. The default coordinate system for ax.text is ‘data’, which is why the text “Important Point!” scrolls with the data if you pan the plot interactively.

To place text relative to the axes viewport itself, you would use the transform argument. By setting transform=ax.transAxes, the coordinates (0,0) represent the bottom-left corner of the axes box and (1,1) represents the top-right. This is invaluable for adding labels that should not move with the data, such as indicating a quadrant or stamping a “Draft” watermark across the plot. The renderer’s transformation pipeline is complex, but exposing it through arguments like this gives you the power to hook into it at the right stage to achieve almost any effect. For example, if you wanted to place a copyright notice in the bottom-right corner of the plot, you could use ax.text(0.98, 0.02, '© My Corp', transform=ax.transAxes, ha='right').

The ha argument, for horizontal alignment, ensures the text’s right edge aligns with the specified x-coordinate. It’s this deep, layered control that separates a basic plot from a publication-quality figure. You can even create your own custom transform objects for truly exotic effects, though that involves diving into the transformation framework itself, which is a world of affine transformations, non-affine transformations, and blended transformation pipelines that map data coordinates through scales and projections to final display coordinates. It’s turtles all the way down, but each layer gives you more precise control. By mastering the object hierarchy and the transform system, you move from a user of matplotlib to a true operator of the rendering machine, capable of crafting visualizations that are not just informative, but precisely engineered. This direct manipulation is critical when building complex dashboards or embedding plots within application GUIs, where the state machine’s global nature becomes a liability.

The explicit fig, ax pattern ensures that your plotting code is modular, reusable, and free from side effects. You can pass an Axes object to a function, have that function draw on it, and be confident that it hasn’t accidentally modified some other plot somewhere else in your application. This is how robust graphical applications are built. The next step is to understand how the artists themselves-the lines, patches, and text objects-are structured. Each call to ax.plot(), for instance, returns a list of Line2D objects. You can hold onto these objects and manipulate their properties directly long after they’ve been created, perhaps in response to a user event like a mouse click. You could, for example, change the color of a line when the user’s mouse hovers over it.

This requires connecting to the event handling system of the figure canvas, which listens for backend-specific GUI events and dispatches them to any callback functions you’ve registered. The event object passed to your callback contains information like the mouse position in both pixel and data coordinates, allowing you to determine which data element the user is interacting with. This opens up the possibility of creating fully interactive plots where the user can directly manipulate the data on screen. For instance, a callback for a ‘button_press_event’ could identify the closest data point to the click, and a subsequent ‘motion_notify_event’ could drag that point to a new location, updating the plot in real-time. This level of interactivity goes far beyond simple plotting and enters the realm of building full-fledged data analysis tools.

The architecture is all there, exposed through the object-oriented API, waiting for the programmer willing to dig in and understand the machinery. The stateful pyplot interface gets you started, but the object-oriented approach is where the real power lies. It’s the path to complete control over the final image, turning the renderer from an unpredictable black box into a deterministic tool that executes your will precisely. The key is to stop thinking about pyplot as a command-line interface and start thinking of it as a factory for creating and connecting the underlying graphical objects that you will then manipulate directly. Every component, from the figure’s background patch to the individual tick labels, is an accessible, mutable object.

Comments

No comments yet. Why don’t you start the discussion?

Leave a Reply

Your email address will not be published. Required fields are marked *