Matplotlib Basics: Line, Bar, Scatter, Histogram

Matplotlib basics come down to a handful of chart types, and this tutorial walks you through creating each one in Python: line plots, bar charts, scatter plots, histograms, and pie charts. You will also learn how to customize colors, labels, titles, and legends using the pyplot interface, with every example run on real code.

“The purpose of visualization is insight, not pictures.”

Ben Shneiderman

Last Updated: July 2026 | Tested on: Python 3.14.6, Matplotlib 3.11 | Difficulty: Intermediate | Reading Time: 13 minutes

You are staring at a spreadsheet with 500 rows of numbers and your boss asks, “So, are sales going up or down?” Squinting at a column of figures will not answer that in a meeting. A chart will, in one glance. That is the gap the matplotlib basics in this post fill. Matplotlib is Python’s original plotting library. It has been around since 2003, and even though newer tools have arrived, every data scientist still reaches for it. Seaborn, Pandas plotting, and parts of Plotly are all built on top of it. When you need full control over every pixel in a chart, Matplotlib is the tool you pick up.

Think of a chart like a map. The raw numbers are the GPS coordinates, technically complete but useless when you are lost. The chart is the actual map that shows you where to go. A table tells you the facts. A chart tells you the story those facts add up to.

Matplotlib gives you two ways to draw. There is the pyplot quick-start interface (plt.plot()), handy for a fast one-off chart, and the object-oriented interface (fig, ax = plt.subplots()), which you reach for in real work. We will use both in this post, but lean on the object-oriented style for anything past a quick look, because it hands you explicit control over the figure and its axes instead of relying on hidden global state.

Here is why this matters in practice. A data analyst named Pravin once plotted customer spend against satisfaction and instantly spotted a cluster of high-spending but unhappy customers sitting in the top-left corner. That one picture kicked off a retention campaign aimed at exactly those people. The same data sat in a table for months and nobody noticed. The chart made it obvious in seconds.

TrendComparisonRelationshipDistributionCompositionWhat story doesyour data tell?Line Chartax.plot()Bar Chartax.bar()Scatter Plotax.scatter()Area chart forcumulative trendsGrouped bars forsubcategoriesBubble chartfor 3rd variableHistogramax.hist()Pie Chartax.pie()Use < 6 slices only!Box plot tocompare groupsViolin fordistribution shapePython Matplotlib: Choosing the Right Chart from Your Data Story

Tip: click the diagram to open it full screen, then use the zoom and pan controls for a closer look.

This matplotlib chart selection guide starts with one question: what story does your data tell? Follow the branch that fits. A trend over time points you to a line chart with ax.plot(). Comparing categories points to a bar chart with ax.bar(). A relationship between two variables means a scatter plot with ax.scatter(). The shape of one variable calls for a histogram with ax.hist(). Parts of a whole suggest a pie chart with ax.pie(), and keep that one under six slices or it turns into confetti. Pick the chart by the question first, then write the code. That habit alone fixes most “which chart do I use” paralysis.

Prerequisites

Work through the correlation and regression tutorial first, since a few examples here plot relationships you met there. You also need Python 3.14.6 and a couple of libraries. Install them with pip install matplotlib numpy. NumPy comes along because we use it to generate sample data.

Matplotlib Basics: Install & Quick Win

One command pulls in everything, then one line confirms it landed.

📄 Terminal: install and confirm the version

pip install matplotlib numpy
python -c "import matplotlib; print(matplotlib.__version__)"

▶ Output

3.11.0

If a version number prints, you are ready for the matplotlib basics ahead. This post was tested on Matplotlib 3.11.0 (latest stable at the time of writing). Your patch number might be slightly higher, and that is fine. Now the fun part. A short script turns a plain list of sales numbers into a chart you could drop straight into a slide deck.

📄 quick_win.py: your first chart

import matplotlib.pyplot as plt

months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun"]
sales = [12000, 15000, 13500, 18000, 21000, 19500]

plt.plot(months, sales, marker="o", color="#50fa7b", linewidth=2)
plt.title("Monthly Sales 2026")
plt.xlabel("Month")
plt.ylabel("Sales ($)")
plt.grid(True, alpha=0.3)
plt.tight_layout()
plt.savefig("sales_chart.png", dpi=150)
plt.show()
print("Chart saved to sales_chart.png")

▶ Output

Chart saved to sales_chart.png

What happened here: The chart itself opens in a window and also lands on disk as sales_chart.png, so the only text you see in the terminal is that one confirmation line. plt.plot() drew the line, marker="o" dropped a dot on each month, and the title, xlabel, and ylabel calls labeled it so a stranger can read it without you in the room. plt.savefig(..., dpi=150) writes a crisp PNG (Portable Network Graphics) file, and plt.show() pops the window. One quick note: call savefig before show, because after you close the window the figure is cleared and you would save a blank image.

Core Chart Types

Four chart types sit at the heart of matplotlib basics: line for trends, bar for comparisons, scatter for relationships, and histogram for the shape of a single column. The script below draws all four at once on a 2 by 2 grid so you can see them side by side. We seed the random data with default_rng(42) so your numbers come out exactly like the ones here instead of shifting on every run. The salary data in the histogram panel is sampled from a normal distribution, the same bell curve covered in the probability distributions tutorial.

📄 chart_types.py: line, bar, scatter, histogram

import matplotlib.pyplot as plt
import numpy as np

rng = np.random.default_rng(42)
fig, axes = plt.subplots(2, 2, figsize=(12, 10))

# 1. Line chart for trends over time
months = np.arange(1, 13)
revenue = [45, 52, 48, 61, 55, 67, 72, 68, 75, 81, 77, 89]
axes[0, 0].plot(months, revenue, marker="o", color="#bd93f9", linewidth=2)
axes[0, 0].set_title("Line: Monthly Revenue")
axes[0, 0].set_xlabel("Month")
axes[0, 0].set_ylabel("Revenue ($K)")
axes[0, 0].grid(True, alpha=0.3)

# 2. Bar chart for comparing categories
departments = ["Eng", "DS", "Mkt", "Sales", "HR"]
headcount = [45, 22, 18, 30, 12]
colors = ["#50fa7b", "#8be9fd", "#ffb86c", "#ff79c6", "#bd93f9"]
axes[0, 1].bar(departments, headcount, color=colors)
axes[0, 1].set_title("Bar: Department Headcount")
axes[0, 1].set_ylabel("Employees")

# 3. Scatter plot for relationships between variables
study_hours = rng.uniform(1, 10, 50)
exam_scores = 50 + 4 * study_hours + rng.normal(0, 5, 50)
axes[1, 0].scatter(study_hours, exam_scores, alpha=0.6, color="#ff79c6", edgecolors="white")
axes[1, 0].set_title("Scatter: Study Hours vs Exam Score")
axes[1, 0].set_xlabel("Study Hours")
axes[1, 0].set_ylabel("Exam Score")

# 4. Histogram for data distribution
salaries = rng.normal(75000, 12000, 500)
axes[1, 1].hist(salaries, bins=25, color="#8be9fd", edgecolor="white", alpha=0.7)
axes[1, 1].axvline(salaries.mean(), color="#ff5555", linestyle="--", label=f"Mean: ${salaries.mean():,.0f}")
axes[1, 1].set_title("Histogram: Salary Distribution")
axes[1, 1].set_xlabel("Salary ($)")
axes[1, 1].set_ylabel("Frequency")
axes[1, 1].legend()

plt.tight_layout()
plt.savefig("chart_types.png", dpi=150)
plt.show()
print("All 4 chart types saved!")

▶ Output

All 4 chart types saved!

What happened here: plt.subplots(2, 2) hands back a figure and a 2 by 2 grid of axes, and you address each cell like a spreadsheet: axes[0, 0] is top-left, axes[1, 1] is bottom-right. Each cell gets its own chart, its own title, and its own labels, all kept separate by the object-oriented style. The red dashed line on the histogram is axvline marking the average salary, which with the fixed seed comes out to exactly $74,842 every time you run it, so the label is reproducible rather than random. tight_layout() nudges the spacing so the titles do not crash into each other.

The terminal shows just the one confirmation line, because the four charts live in the saved image, not in the text output.

Customization: Colors, Styles, Labels

A default chart is fine for you. A publication-ready chart is fine for everyone else. The difference is a handful of touches: a clean style, bold readable labels, a legend, and an arrow pointing at the thing you want people to notice. The next script applies a built-in style and annotates the peak of a sine wave.

📄 customization.py: make charts publication-ready

import matplotlib.pyplot as plt
import numpy as np

# Use a style
plt.style.use("seaborn-v0_8-darkgrid")

fig, ax = plt.subplots(figsize=(10, 6))

x = np.linspace(0, 10, 100)
ax.plot(x, np.sin(x), label="sin(x)", linewidth=2, color="#50fa7b")
ax.plot(x, np.cos(x), label="cos(x)", linewidth=2, color="#ff79c6", linestyle="--")
ax.fill_between(x, np.sin(x), alpha=0.1, color="#50fa7b")

ax.set_title("Trigonometric Functions", fontsize=16, fontweight="bold")
ax.set_xlabel("x (radians)", fontsize=12)
ax.set_ylabel("y", fontsize=12)
ax.legend(fontsize=12, loc="upper right")
ax.set_xlim(0, 10)
ax.set_ylim(-1.5, 1.5)

# Annotate a point
ax.annotate("Peak", xy=(np.pi/2, 1), xytext=(3, 1.3),
            arrowprops=dict(arrowstyle="->", color="#ffb86c"),
            fontsize=11, color="#ffb86c")

plt.tight_layout()
plt.savefig("custom_chart.png", dpi=150, bbox_inches="tight")
plt.show()

What happened here: This script prints nothing to the terminal on purpose, since all the output is the saved image custom_chart.png. plt.style.use("seaborn-v0_8-darkgrid") swaps in a softer grid theme in one line, no manual restyling needed. That exact style name is still shipped and valid in Matplotlib 3.11.0, so it runs without warnings. fill_between shades the area under the sine curve, legend() reads the label text off each line so you do not write the legend by hand, and annotate draws the “Peak” arrow at the top of the wave.

Think of annotate like a sticky note you slap on the one data point that matters, so the reader’s eye goes straight to it. The bbox_inches="tight" in savefig trims the white border so the chart fills the image instead of floating in a sea of margin.

Chart Selection Guide

Picking a chart is like reaching into a toolbox: you would not use a hammer to drive a screw, and a pie chart will not show a trend over time. Picking a chart is not about taste, it is about the question. Match the story you want to tell to the row below, then call the function in the last column. Keep this table next to you for your first few weeks and the choice stops feeling like a guess.

Data StoryChart TypeFunction
Trend over timeLine chartax.plot()
Compare categoriesBar chartax.bar()
Relationship between 2 variablesScatter plotax.scatter()
Distribution of one variableHistogramax.hist()
Composition (parts of whole)Pie chartax.pie()
Compare distributionsBox plotax.boxplot()
Correlation matrixHeatmapax.imshow()

Common Mistakes

Mistake 1: Using the plt interface for multi-panel figures

The plt functions track a “current” plot behind the scenes, like a TV remote that only controls whichever channel happens to be on. That works for one chart. The moment you have several panels, you lose track of which one you are talking to. The object-oriented version hands you a named handle for each panel, so there is never any doubt.

📄 subplots_two_ways.py: state-based vs object-oriented

import matplotlib.pyplot as plt

# BAD: plt interface, state-based, easy to lose track of for subplots
plt.subplot(121)
plt.plot([1, 2, 3])
plt.subplot(122)
plt.plot([3, 2, 1])

# GOOD: object-oriented, explicit, scalable
fig, (ax1, ax2) = plt.subplots(1, 2)
ax1.plot([1, 2, 3])
ax2.plot([3, 2, 1])

What happened here: Both blocks draw two side-by-side panels and run without error, so the code is not “wrong” in the crash sense. The difference is readability. In the BAD block, plt.plot([3, 2, 1]) lands on the second panel only because plt.subplot(122) ran just before it and quietly made that panel “current.” Move the lines around and the chart silently breaks. In the GOOD block, ax1 and ax2 are explicit handles, so ax1.plot(...) always targets the left panel no matter where the line sits. Once you have more than one panel, always reach for fig, axes = plt.subplots(...).

Practice Exercises

  1. Exercise 1: Plot y = x squared for x from 1 to 10, then add a title and axis labels.
  2. Exercise 2: Recreate the four chart types (line, bar, scatter, histogram) on a single 2 by 2 grid using the object-oriented interface.
  3. Exercise 3: Build a six-panel dashboard with shared axes, a peak annotation, and a custom style.

Conclusion

You now have the four charts that cover most day-to-day plotting: a line chart for trends, a bar chart for comparisons, a scatter plot for relationships, and a histogram for the shape of a single column. You also learned the difference between the quick plt interface and the object-oriented fig, ax style, how to customize colors, labels, legends, and annotations, and how to pick the right chart by starting from the question your data answers. Save your work with savefig before show, and reach for the object-oriented style the moment you have more than one panel. Those matplotlib basics carry you through most of the plotting a real job asks for.

Next up, the Matplotlib Advanced tutorial builds on this with multi-panel dashboards, shared axes, and deeper styling. For the full path from Python basics to machine learning, browse the Python + AI/ML tutorial series home.

Frequently Asked Questions

What is the difference between the plt and ax interfaces in this matplotlib tutorial?

plt (pyplot) is a state-based interface that quietly manages the current figure for you. ax (the Axes object) is the object-oriented interface where you control each plot through an explicit handle. Use plt for a quick throwaway chart. Use ax for anything else, because it is clearer, more flexible, and required once you have more than one panel.

How do I save Matplotlib charts as high-quality images?

Use plt.savefig(‘chart.png’, dpi=300, bbox_inches=’tight’). A dpi of 300 is publication quality, and bbox_inches=’tight’ trims the extra white border. For a scalable vector format that never blurs when resized, save as SVG or PDF instead, for example plt.savefig(‘chart.svg’).

Should I use Matplotlib or Seaborn?

Once the matplotlib basics are in your hands, use Matplotlib for full control and custom plots. Use Seaborn when you want common statistical plots in far less code, since Seaborn is built on top of Matplotlib. For most exploratory data analysis, Seaborn is faster to write. For custom or publication charts, Matplotlib gives you control over every pixel.

How do I make Matplotlib charts look modern?

Call plt.style.use() with a built-in style such as ‘seaborn-v0_8-darkgrid’, ‘ggplot’, or ‘fivethirtyeight’. Remove clutter like the top and right spines, use muted colors, and leave breathing room around the plot. The Matplotlib subplots tutorial linked below covers advanced styling in detail.

Interview Questions on Matplotlib

These come from real screens and onsites. Practice answering before you read each answer.

Q: What is the difference between a figure and an axes in Matplotlib?

A figure is the whole canvas or window, and an axes is a single plot region drawn inside it, complete with its own x-axis, y-axis, title, and data. One figure can hold many axes, which is exactly what happens when you call plt.subplots(2, 2) and get a grid of four axes back. When people say “axis” they usually mean the axes object, not the single x or y line.

Q: When would you choose the object-oriented interface over the pyplot state-based interface?

Use the object-oriented style (fig, ax = plt.subplots()) for anything beyond a quick throwaway chart, and always once you have more than one panel. It hands you an explicit handle for each axes, so ax1.plot(...) targets exactly the panel you mean instead of relying on whichever plot pyplot last made “current.” That explicitness makes the code readable and safe to rearrange.

Q: Which chart type fits a trend over time versus a distribution of one variable?

A trend over time calls for a line chart with ax.plot(), since the connected line shows direction and momentum at a glance. The distribution of a single variable calls for a histogram with ax.hist(), which buckets the values into bins so you can see the shape, spread, and any skew. Picking the chart from the question you are answering avoids most “which chart do I use” confusion.

Q: Why should you call savefig before show?

Because plt.show() opens the interactive window and, once you close it, the current figure is cleared. If you call savefig after that, you save a blank image. Always write the file first with plt.savefig("chart.png", dpi=150), then call plt.show() to display it.

Q: Scenario: you run a script on a headless server and plt.show() throws an error or hangs. What do you check first?

A headless server has no display, so the default interactive backend cannot open a window. Switch to a non-interactive backend before importing pyplot, for example import matplotlib; matplotlib.use("Agg"), and then only call savefig rather than show. The Agg backend renders straight to an image file, which is exactly what you want in a batch job or CI pipeline.

Q: Scenario: a teammate says Anvi signs off on a report where the pie chart has 11 slices and nobody can read it. How do you fix it?

Pie charts fall apart past five or six slices because the human eye cannot compare thin wedges by angle. Replace it with a bar chart using ax.bar(), which lines the categories up on a shared baseline so differences are obvious. If the goal is parts of a whole, keep the top few slices and group the rest into a single “Other” category before plotting.

Q: How do you export a chart that stays sharp when scaled up for a print poster?

Save it as a vector format such as SVG or PDF, for example plt.savefig("chart.svg"), because vectors redraw at any size without blurring. If you must use a raster format like PNG, bump the resolution with dpi=300 and add bbox_inches="tight" to trim the surrounding white border. Vector output is the safer default for anything headed to print.

Further reading: for the full reference, see Matplotlib documentation.

Previous: Statistics: Correlation & Regression Analysis

Next: Matplotlib Advanced: Subplots, Annotations, Styles

Series Home: Python + AI/ML Tutorial Series

RahulAuthor posts

Avatar for Rahul

Rahul is a passionate IT professional who loves to sharing his knowledge with others and inspiring them to expand their technical knowledge. Rahul's current objective is to write informative and easy-to-understand articles to help people avoid day-to-day technical issues altogether. Follow Rahul's blog to stay informed on the latest trends in IT and gain insights into how to tackle complex technical issues. Whether you're a beginner or an expert in the field, Rahul's articles are sure to leave you feeling inspired and informed.

No comment

Leave a Reply

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