The previous article covered the Figure/Axes foundation every Matplotlib plot type builds on. This one covers matplotlib line bar scatter plots — the three most common building blocks — framed around when to use each, not just the syntax, plus combining them with pandas groupby data from earlier in this series.
Three Plot Types, Three Different Jobs
The building blocks of visualization
Basic plots like line, bar, and scatter plots are the building blocks of data visualization — a simple yet powerful way to understand data at a glance, whether comparing values over time, visualizing distributions, or identifying correlations between variables.
The core framing worth leading with
Choosing the wrong plot type for a given question is a genuinely common beginner mistake — a bar chart forcing continuous time-series data into discrete categories, or a line chart implying a trend between unrelated categories that have no natural order. The fix is simple to state, if not always simple to remember under pressure: line plots are perfect for showing trends over time. Bar plots are great for comparing discrete categories or groups. Scatter plots are ideal for exploring relationships between two numerical variables. The shape of your actual question — trend, comparison, or relationship — should determine the plot type, not personal preference or habit.
What this article covers
All three plot types, built using the object-oriented fig, ax pattern from the previous article throughout, working with both plain lists and real pandas data — tying directly back to the pandas articles earlier in this series.
Line Plots: Showing Trends Over Time
Basic syntax
import matplotlib.pyplot as plt
x = [1, 2, 3, 4, 5]
y = [20, 22, 19, 25, 27]
fig, ax = plt.subplots()
ax.plot(x, y)
plt.show()ax.plot(x, y) connects data points with a line — the natural choice whenever the x-axis represents an ordered sequence, like time or a sequential index position, where the connecting line meaningfully implies "this is how the value changed as we moved forward."
Plotting directly from a pandas DataFrame
import pandas as pd
df = pd.DataFrame({
"Weight": [70, 71, 70.5, 69, 71.5],
}, index=pd.date_range("2026-01-01", periods=5, freq="D"))
fig, ax = plt.subplots()
ax.plot(df.index, df["Weight"])
ax.set_xlabel("Date")
ax.set_ylabel("Weight (kg)")
plt.show()This ties directly back to the earlier datetime article: plotting a DatetimeIndex directly against a numeric column is exactly the shape of data a line plot is built for — genuinely time-indexed data, where Matplotlib automatically handles formatting the date labels along the x-axis sensibly.
Multiple lines on the same Axes
fig, ax = plt.subplots()
ax.plot(x, [20, 22, 19, 25, 27], label="Location A")
ax.plot(x, [18, 20, 21, 23, 24], label="Location B")
ax.set_xlabel("Day")
ax.set_ylabel("Temperature")
ax.legend()
plt.show()Calling .plot() more than once on the same ax adds each line to that same Axes, rather than replacing the previous one. Giving each call a distinct label= argument, then calling ax.legend() once, is the standard pattern for comparing several trends on a single chart — genuinely the most common way multi-line comparisons get built in practice.
Bar Plots: Comparing Discrete Categories
Basic syntax
categories = ["Setosa", "Versicolor", "Virginica"]
values = [5.0, 5.9, 6.6]
fig, ax = plt.subplots()
ax.bar(categories, values)
plt.show()ax.bar(categories, values) draws vertical bars — the natural choice for comparing values across discrete, unordered (or at least non-time-based) categories.
fig, ax = plt.subplots()
ax.barh(categories, values)
plt.show()ax.barh() draws horizontal bars instead — genuinely useful when category labels are long enough that they'd otherwise overlap or need awkward rotation along the x-axis; horizontal bars give long text labels room to read naturally along the y-axis instead.
The pattern that matters most: aggregate first, then plot
iris_data = pd.DataFrame({
"Species": ["Setosa", "Setosa", "Versicolor", "Versicolor", "Virginica", "Virginica"],
"PetalLength": [1.4, 1.5, 4.5, 4.2, 5.9, 6.1],
})
summary = iris_data.groupby("Species")["PetalLength"].mean()
print(summary)
fig, ax = plt.subplots()
ax.bar(summary.index, summary.values)
ax.set_ylabel("Average Petal Length")
plt.show()This is genuinely the standard, most common pattern for real bar charts: bar charts almost always plot summarized data, rather than raw individual records — tying directly back to the earlier groupby and aggregation article, .groupby("Species")["PetalLength"].mean() first collapses raw, row-level data down into one summary value per category, and that summary is what actually gets plotted, since plotting hundreds of raw individual data points as separate bars would be both unreadable and not actually answer the "compare across categories" question a bar chart exists to answer.
Customization: value labels and grouped bars
fig, ax = plt.subplots()
bars = ax.bar(summary.index, summary.values)
for bar in bars:
height = bar.get_height()
ax.text(bar.get_x() + bar.get_width() / 2, height, f"{height:.1f}",
ha="center", va="bottom")
plt.show()Adding value labels directly above each bar — looping over the returned bar objects and using ax.text() to place a formatted number just above each one — genuinely improves readability, letting a viewer read exact values at a glance rather than estimating them from bar height alone.
For comparing multiple categories side by side within each group (say, petal length and petal width per species), grouped or stacked bars — adjusting each bar's width and x-position slightly to sit alongside its neighbors, rather than directly on top of the same x-position — handle that comparison cleanly, though the details are involved enough to deserve their own dedicated treatment beyond this introductory article.
Scatter Plots: Exploring Relationships Between Variables
Basic syntax
import numpy as np
rng = np.random.default_rng(42)
x = rng.uniform(0, 100, 50)
y = x * 1.5 + rng.normal(0, 10, 50)
fig, ax = plt.subplots()
ax.scatter(x, y)
plt.show()ax.scatter(x, y) plots individual points, without connecting them — the right choice for visualizing the relationship between two continuous variables (population vs. GDP, weight vs. height), letting you spot correlations, clusters, or outliers at a glance in a way a connected line (implying an ordered sequence that doesn't actually exist here) genuinely wouldn't.
Customization parameters, demonstrated together
sizes = rng.uniform(20, 200, 50) # imagine this encodes a third variable
fig, ax = plt.subplots()
ax.scatter(x, y, s=sizes, color="steelblue", edgecolor="black", alpha=0.6)
ax.set_xlabel("Variable X")
ax.set_ylabel("Variable Y")
plt.show()s— marker size. Genuinely useful for encoding a third variable directly in the plot — larger points representing a larger value of something else entirely, letting a single 2D scatter plot convey three dimensions of information at once.color/edgecolor— controls the fill and outline color of each marker, useful for visual distinction, especially when combined with multiple scatter calls representing different categories on the same Axes.alpha— controls transparency, genuinely essential when points overlap heavily — without some transparency, a dense cluster of overlapping points can look like a single solid blob, hiding the actual density and structure of the data; a partially transparent marker lets overlapping regions appear visibly darker, revealing where points are genuinely concentrated.
Overlaying a regression line
coefficients = np.polyfit(x, y, deg=1)
trend_line = np.poly1d(coefficients)
fig, ax = plt.subplots()
ax.scatter(x, y, alpha=0.6)
x_sorted = np.sort(x)
ax.plot(x_sorted, trend_line(x_sorted), color="red", linewidth=2, label="Trend line")
ax.set_xlabel("Variable X")
ax.set_ylabel("Variable Y")
ax.legend()
plt.show()np.polyfit(x, y, deg=1) fits a simple linear regression (degree 1, a straight line) to the scattered points, and np.poly1d() turns those fitted coefficients into a callable function you can evaluate at any x-value. Overlaying this fitted line directly on top of the raw scatter points — a genuinely publication-quality touch — gives the plot both the raw underlying relationship and a clear, fitted summary of its overall trend, in one combined figure.
Choosing the Right Plot and Customization That Ties Them Together
A practical decision recap
Line plot — when the x-axis is genuinely ordered: time, a sequential index, anything where "what came before" meaningfully connects to "what comes next."
Bar plot — when comparing values across discrete categories or groups, typically after aggregating raw data first, exactly as shown in Section 3.
Scatter plot — when examining the relationship between two numeric variables, where you want to see the actual shape of that relationship — correlation, clustering, outliers — rather than a summarized trend or a categorical comparison.
The shape of the underlying question should determine the plot type, not personal preference or which one happens to look more polished — a line plot connecting unordered categories, or a bar chart forcing continuous time-series data into artificial buckets, actively misleads a viewer about the actual structure of the data, regardless of how clean the resulting chart looks.
Shared customization across all three
fig, ax = plt.subplots()
ax.plot(x, y) # or .bar(), or .scatter() — everything below applies identically
ax.set_title("Chart Title")
ax.set_xlabel("X Label")
ax.set_ylabel("Y Label")
ax.set_xlim(0, 100)
ax.set_ylim(0, 150)
ax.grid(True)
plt.show()ax.set_title(), ax.set_xlabel()/ax.set_ylabel(), ax.set_xlim()/ax.set_ylim() (controlling exactly which range of values is visible along each axis), and ax.grid(True) (adding light gridlines for easier value reading) all apply identically regardless of which of the three plot types you're actually using — since, as covered in the previous article, they're all methods on the same underlying ax object, not specific to any one plot type.
A closing practical note
Annotations, grid lines, and value labels — small, easy-to-add customizations — make any of these three plot types meaningfully more informative at a glance, regardless of which one you've chosen for a given question. A chart that requires a viewer to squint at unlabeled axes or guess at approximate values from bar height alone is doing less than it could with only a few extra lines of code — the plot-type decision covered throughout this article matters most, but these smaller finishing touches are what separate a merely correct chart from a genuinely clear, communicative one.