Line Plots and Scatter Plots

1

Line Plots and Scatter Plots

▶

Line plots and scatter plots are the two workhorses of data visualization in science and engineering. A line plot connects data points with line segments to emphasize trends, continuity, or the evolution of a quantity over an ordered axis such as time or position. A scatter plot, by contrast, renders each observation as an independent marker with no connecting line, making it ideal for revealing correlations, clusters, or outliers in relational data. Matplotlib's pyplot interface — typically imported as import matplotlib.pyplot as plt — provides a concise, state-based API that makes both chart types fast to produce and deeply customizable. The following sections walk through creation and customization of each type in detail.

Creating a Basic Line Plot

The entry point for line plots is plt.plot(). At its simplest, you pass a single sequence and Matplotlib treats the index (0, 1, 2, …) as the x-axis. More commonly you supply two sequences — x values then y values. The function is deliberately flexible about the container type: NumPy arrays, plain Python lists, and pandas Series objects are all accepted without any explicit conversion.

import numpy as np
import matplotlib.pyplot as plt

x = np.linspace(0, 2 * np.pi, 200)   # NumPy array
y = np.sin(x)

plt.plot(x, y)
plt.show()

The same call works with a Python list:

x_list = [0, 1, 2, 3, 4]
y_list = [0, 1, 4, 9, 16]
plt.plot(x_list, y_list)
plt.show()

And with a pandas Series, where the Series index automatically becomes the x-axis:

import pandas as pd

temperature = pd.Series([22.1, 23.4, 25.0, 24.3, 21.8],
                        index=[8, 9, 10, 11, 12])   # hours of the day
plt.plot(temperature)
plt.show()

Multiple lines on the same axes are added by calling plt.plot() more than once before plt.show(). Each call appends a new line to the current axes object. Matplotlib automatically assigns a different color from its default color cycle so the lines are visually distinct even if you do nothing extra.

x = np.linspace(0, 2 * np.pi, 200)

plt.plot(x, np.sin(x))
plt.plot(x, np.cos(x))
plt.plot(x, np.sin(2 * x))
plt.show()

Format strings let you specify line style, color, and marker shape compactly in a single positional argument placed immediately after the y-data. The format string is parsed as color + line style + marker in any order. Common color codes include 'r' (red), 'g' (green), 'b' (blue), 'k' (black), and 'c' (cyan). Line styles include '-' (solid), '--' (dashed), '-.' (dash-dot), and ':' (dotted). Marker codes include 'o' (circle), 's' (square), '^' (triangle), '+' (plus), and 'x' (cross).

x = np.linspace(0, 10, 30)

plt.plot(x, np.sin(x), 'r--o')    # red dashed line with circle markers
plt.plot(x, np.cos(x), 'b-.s')    # blue dash-dot line with square markers
plt.plot(x, x / 10,   'k:^')      # black dotted line with triangle markers
plt.show()

For finer control you can always use keyword arguments instead of or in addition to the format string. linewidth (or lw), markersize (or ms), markerfacecolor, and alpha (transparency, 0–1) are frequently useful.

plt.plot(x, np.sin(x), color='steelblue', linestyle='--',
         marker='o', linewidth=2, markersize=6, alpha=0.8)
plt.show()

Customizing Line Plots

A plot without labeled axes and a title is incomplete for any audience beyond a quick personal check. Matplotlib provides straightforward functions for all standard annotations.

plt.xlabel() and plt.ylabel() attach text labels to the horizontal and vertical axes respectively. Both accept an optional fontsize keyword. Including the physical unit in the label — for example "Time (s)" or "Voltage (mV)" — is a professional standard that prevents misinterpretation.

x = np.linspace(0, 4 * np.pi, 300)

plt.plot(x, np.sin(x))
plt.xlabel('Angle (radians)', fontsize=13)
plt.ylabel('Amplitude', fontsize=13)
plt.show()

plt.title() places a title above the axes. For publications you might leave the title blank and use a figure caption instead, but for exploratory work a descriptive title speeds up interpretation.

plt.plot(x, np.sin(x))
plt.title('Sine Wave — One Full Cycle', fontsize=15)
plt.show()

plt.legend() renders a legend box that maps line styles to labels. It only produces useful output when each plt.plot() call includes a label keyword argument; plt.legend() then collects all labeled artists automatically. The loc keyword controls placement: 'upper right', 'lower left', 'center', and so on. Passing loc='best' lets Matplotlib find the position that overlaps the least data.

x = np.linspace(0, 2 * np.pi, 200)

plt.plot(x, np.sin(x),     label='sin(x)',    color='royalblue')
plt.plot(x, np.cos(x),     label='cos(x)',    color='tomato')
plt.plot(x, np.sin(2 * x), label='sin(2x)',   color='seagreen', linestyle='--')

plt.xlabel('x (radians)')
plt.ylabel('f(x)')
plt.title('Trigonometric Functions')
plt.legend(loc='upper right', fontsize=11)
plt.show()

A common mistake is calling plt.legend() without having passed label= to any plot call — the result is either an empty legend or a legend with generic, unhelpful entries. Always pair label= in plt.plot() with plt.legend().

Additional quality-of-life customizations worth knowing: plt.grid(True) overlays a background grid that makes reading values easier; plt.tight_layout() automatically adjusts spacing to prevent axis labels from being clipped; and plt.savefig('filename.png', dpi=150) exports the figure to disk instead of displaying it interactively.

Creating a Scatter Plot

plt.scatter(x, y) is the dedicated scatter plot function. Unlike plt.plot(x, y, 'o') which also places markers at data points, plt.scatter() is designed to encode additional variables through marker size and color, making it much more expressive for multivariate data.

rng = np.random.default_rng(42)
x = rng.uniform(0, 10, 80)
y = 2.5 * x + rng.normal(0, 3, 80)

plt.scatter(x, y)
plt.xlabel('Feature X')
plt.ylabel('Response Y')
plt.title('Basic Scatter Plot')
plt.show()

The s parameter sets the marker size in points² (not points — the unit is area). A scalar value applies the same size to every point. Passing an array whose length matches the data allows each point to have its own size, which encodes a third variable visually through marker area. For example, if a dataset records city locations and population, the population can drive the size of each city's dot.

population = rng.uniform(10, 500, 80)    # a third variable (e.g., city population)

plt.scatter(x, y, s=population)          # larger population → larger dot
plt.xlabel('Longitude (relative)')
plt.ylabel('Latitude (relative)')
plt.title('Marker Size Encodes Population')
plt.show()

Because s is in units of area, the visual radius scales as the square root of the value. If you want the radius to be proportional to your variable, pass the square of that variable to s. Conversely, to represent a magnitude that spans orders of magnitude, you may want to pass the log of the variable.

The c parameter maps a continuous or categorical variable to marker color. When you pass an array of numbers to c, Matplotlib maps those numbers through a colormap (specified with the cmap keyword) to produce a gradient of colors. This is one of the most powerful techniques for revealing a third dimension in a 2-D scatter plot.

depth = rng.uniform(0, 100, 80)          # a third variable (e.g., ocean depth)

scatter = plt.scatter(x, y, c=depth, cmap='plasma', s=60, alpha=0.8)
plt.xlabel('X')
plt.ylabel('Y')
plt.title('Color Encodes Depth')
plt.show()

When a continuous variable is mapped to color, it is essential to add a color scale so the viewer can read off actual values. plt.colorbar() creates a vertical (or horizontal) color bar adjacent to the axes. You should pass the return value of plt.scatter() as its argument so Matplotlib knows which mappable to reference, and you can add a label with the label keyword or by calling .set_label() on the returned colorbar object.

fig, ax = plt.subplots()

sc = ax.scatter(x, y, c=depth, cmap='viridis', s=population * 0.5, alpha=0.75)

cbar = fig.colorbar(sc, ax=ax)
cbar.set_label('Depth (m)', fontsize=12)

ax.set_xlabel('X')
ax.set_ylabel('Y')
ax.set_title('Scatter: Size = Population, Color = Depth')
plt.show()

Notice the switch here to the object-oriented API (fig, ax = plt.subplots()). This is preferable for any plot more complex than a quick exploratory sketch because it gives you explicit handles to the figure and axes, preventing ambiguity when multiple subplots are involved. ax.scatter(), ax.set_xlabel(), ax.set_ylabel(), and ax.set_title() are the object-oriented equivalents of the plt.* convenience functions.

Popular colormaps for continuous data include 'viridis' and 'plasma' (perceptually uniform, colorblind-friendly), 'coolwarm' (diverging, useful when data has a meaningful midpoint), and 'gray' (for monochrome output). Avoid 'jet' in scientific work — it is perceptually non-uniform and can create false visual boundaries.

Axis Limits and Scaling

By default Matplotlib automatically determines axis limits based on the range of the data, adding a small margin. In many situations you need explicit control — to zoom in on a region of interest, to match limits across multiple panels, or to exclude outliers that distort the view.

plt.xlim(min, max) and plt.ylim(min, max) set the visible range of the x- and y-axes respectively. You can pass just one bound by using None for the other (e.g., plt.xlim(0, None) forces the left limit to zero while letting Matplotlib choose the right limit automatically).

x = np.linspace(-10, 10, 400)
y = np.sin(x) / x    # sinc-like function; undefined at x=0 but NumPy handles it

plt.plot(x, y)
plt.xlim(-8, 8)       # zoom in, exclude the noisy tails near ±10
plt.ylim(-0.3, 1.1)   # show the full main lobe plus a little negative excursion
plt.xlabel('x')
plt.ylabel('sin(x)/x')
plt.title('Windowed Sinc Function')
plt.show()

plt.xscale('log') and plt.yscale('log') switch an axis to a base-10 logarithmic scale. Logarithmic axes are indispensable whenever data spans several orders of magnitude — frequency response plots (Bode plots), power spectral densities, population sizes, financial returns, and many physical phenomena all benefit from log scaling. On a log axis, equal distances represent equal ratios, not equal differences, so exponential relationships appear as straight lines.

frequency = np.logspace(1, 5, 200)    # 10 Hz to 100 kHz, evenly spaced in log
gain = 1 / np.sqrt(1 + (frequency / 1000) ** 2)   # simple RC low-pass filter

plt.plot(frequency, gain)
plt.xscale('log')
plt.yscale('log')
plt.xlabel('Frequency (Hz)')
plt.ylabel('Gain')
plt.title('Bode Magnitude Plot — RC Low-Pass Filter')
plt.grid(True, which='both', linestyle='--', alpha=0.5)  # grid on major & minor ticks
plt.show()

The keyword which='both' in plt.grid() draws grid lines at both major and minor tick marks, which is standard practice for log-scale plots and greatly aids reading values.

plt.axis('equal') forces the same physical scale on both axes, meaning one unit in x occupies the same number of pixels as one unit in y. This is critical for spatial or geometric data where distortion would be misleading. For instance, a circle plotted with unequal axis scaling appears as an ellipse — plt.axis('equal') corrects this.

theta = np.linspace(0, 2 * np.pi, 300)
x_circle = np.cos(theta)
y_circle = np.sin(theta)

plt.plot(x_circle, y_circle, 'b-')
plt.axis('equal')            # without this, the circle would look like an ellipse
plt.xlabel('x')
plt.ylabel('y')
plt.title('Unit Circle — Equal Aspect Ratio')
plt.show()

Related options for plt.axis() include 'tight' (removes extra margin around data), 'square' (makes the axes box square without necessarily equalizing scales), and 'off' (hides all axis decorations — useful for image display). You can also call ax.set_aspect('equal') in the object-oriented API for the same effect as plt.axis('equal').

Together, line plots and scatter plots — augmented with careful axis labels, legends, limits, and scaling — form the foundation of nearly all quantitative visualization. Mastering their options in Matplotlib gives you a toolkit capable of communicating everything from a simple trend to a rich four-variable relationship rendered through position, size, color, and transparency simultaneously.

NotesConsider demonstrating plt.subplots() for multi-panel figures in a follow-up topic so learners see how the object-oriented API scales beyond single plots. The Bode plot example pairs naturally with a signal-processing or controls engineering context.