I didn’t really understand a dataset until I plotted it — that’s just been true for me across every analysis project I’ve worked on. Summary statistics tell you numbers; a chart tells you a story, and often reveals patterns, outliers, or errors that numbers alone hide completely. Over the years I’ve settled into using three main Python visualization libraries depending on the job: Matplotlib for full control, Seaborn for fast statistical plots, and Plotly for interactive, shareable visuals. This guide walks through all three, when to reach for each, and how they actually work under the hood.
Setting Up
pip install matplotlib seaborn plotly --break-system-packages
import matplotlib.pyplot as plt
import seaborn as sns
import plotly.express as px
import pandas as pd
import numpy as np
Matplotlib: The Foundation Everything Else Is Built On
Matplotlib is the oldest and most foundational of the three — both Seaborn and, to a lesser extent, other plotting libraries build on top of its rendering engine. I think of Matplotlib as the low-level tool that gives me total control at the cost of more verbose code.
import matplotlib.pyplot as plt
x = [1, 2, 3, 4, 5]
y = [2, 4, 6, 8, 10]
plt.figure(figsize=(8, 5))
plt.plot(x, y, marker='o', color='steelblue', linewidth=2)
plt.title('Simple Line Plot')
plt.xlabel('X Axis')
plt.ylabel('Y Axis')
plt.grid(True, alpha=0.3)
plt.savefig('line_plot.png', dpi=150, bbox_inches='tight')
plt.show()
Understanding Matplotlib’s Figure and Axes Architecture
This is the concept that took me the longest to internalize, and understanding it made every subsequent Matplotlib plot easier to write. Every Matplotlib plot has two layers: a Figure (the overall canvas/window) and one or more Axes (the individual plot areas within that figure, despite the confusingly plural name — one Axes object is one subplot).
import matplotlib.pyplot as plt
import numpy as np
fig, axes = plt.subplots(1, 2, figsize=(12, 5))
x = np.linspace(0, 10, 100)
axes[0].plot(x, np.sin(x), color='crimson')
axes[0].set_title('Sine Wave')
axes[1].plot(x, np.cos(x), color='darkgreen')
axes[1].set_title('Cosine Wave')
plt.tight_layout()
plt.savefig('subplots.png', dpi=150)
I use the explicit fig, ax = plt.subplots() pattern (the “object-oriented” API) rather than the simpler plt.plot() shortcut (the “pyplot” or “state-machine” API) for anything beyond a single throwaway chart, because it gives explicit control over exactly which axes each command applies to — this matters enormously once I’m building multi-panel figures.
Common Matplotlib Chart Types
import matplotlib.pyplot as plt
import numpy as np
categories = ['A', 'B', 'C', 'D']
values = [23, 45, 12, 38]
fig, axes = plt.subplots(2, 2, figsize=(10, 8))
axes[0, 0].bar(categories, values, color='teal')
axes[0, 0].set_title('Bar Chart')
axes[0, 1].scatter(np.random.rand(50), np.random.rand(50), alpha=0.6)
axes[0, 1].set_title('Scatter Plot')
axes[1, 0].hist(np.random.normal(0, 1, 1000), bins=30, color='coral')
axes[1, 0].set_title('Histogram')
axes[1, 1].pie(values, labels=categories, autopct='%1.1f%%')
axes[1, 1].set_title('Pie Chart')
plt.tight_layout()
plt.savefig('chart_types.png', dpi=150)
Seaborn: Statistical Plotting Built on Matplotlib
Seaborn is built directly on top of Matplotlib, but it provides a much higher-level interface specifically designed for statistical visualization, and it works especially well directly with Pandas DataFrames. When I’m doing exploratory data analysis, Seaborn gets me to a useful chart in far fewer lines than raw Matplotlib.
import seaborn as sns
import pandas as pd
import matplotlib.pyplot as plt
df = pd.DataFrame({
'category': ['A', 'B', 'C', 'A', 'B', 'C'] * 20,
'value': np.random.normal(50, 15, 120),
'group': ['X', 'Y'] * 60
})
sns.set_theme(style='whitegrid')
plt.figure(figsize=(8, 5))
sns.boxplot(data=df, x='category', y='value', hue='group')
plt.title('Distribution by Category and Group')
plt.savefig('seaborn_boxplot.png', dpi=150)
Seaborn’s Statistical Convenience Functions
What sold me on Seaborn permanently was how it handles common statistical visualizations that would take considerably more manual code in raw Matplotlib.
import seaborn as sns
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
df = pd.DataFrame({
'x': np.random.normal(0, 1, 200),
'y': np.random.normal(0, 1, 200)
})
df['y'] = df['x'] * 0.7 + df['y'] * 0.5
fig, axes = plt.subplots(1, 3, figsize=(15, 4))
sns.scatterplot(data=df, x='x', y='y', ax=axes[0])
axes[0].set_title('Scatter')
sns.regplot(data=df, x='x', y='y', ax=axes[1]) # scatter + regression line + confidence band
axes[1].set_title('Regression Fit')
sns.kdeplot(data=df, x='x', y='y', ax=axes[2], fill=True) # density estimate
axes[2].set_title('KDE Density')
plt.tight_layout()
plt.savefig('seaborn_stats.png', dpi=150)
regplot() fitting a regression line with a confidence interval in a single function call is the kind of thing that would take me many more lines of manual statistics code with raw Matplotlib — Seaborn handles the underlying calculation internally.
Correlation Heatmaps
A chart type I use constantly during initial data exploration:
import seaborn as sns
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
df = pd.DataFrame(np.random.randn(100, 5), columns=['A', 'B', 'C', 'D', 'E'])
df['B'] = df['A'] * 0.8 + np.random.randn(100) * 0.3
correlation_matrix = df.corr()
plt.figure(figsize=(7, 6))
sns.heatmap(correlation_matrix, annot=True, cmap='coolwarm', center=0, fmt='.2f')
plt.title('Correlation Heatmap')
plt.savefig('heatmap.png', dpi=150)
Plotly: Interactive, Shareable Visualizations
Where Matplotlib and Seaborn produce static images, Plotly generates interactive charts — hover tooltips, zoom, pan — rendered as HTML/JavaScript rather than a static image file. I reach for Plotly specifically when a visualization needs to be explored interactively, shared as a standalone web page, or embedded in a dashboard.
import plotly.express as px
import pandas as pd
import numpy as np
df = pd.DataFrame({
'date': pd.date_range('2026-01-01', periods=100),
'revenue': np.cumsum(np.random.normal(1000, 200, 100)),
'category': np.random.choice(['Product A', 'Product B'], 100)
})
fig = px.line(df, x='date', y='revenue', color='category', title='Revenue Over Time')
fig.update_layout(hovermode='x unified')
fig.write_html('interactive_revenue.html')
fig.show()
The resulting chart lets a viewer hover over any point to see exact values, zoom into specific date ranges, and toggle categories on/off by clicking the legend — none of which is possible with a static Matplotlib PNG.
Plotly for 3D and Complex Interactive Charts
import plotly.express as px
import pandas as pd
import numpy as np
df = pd.DataFrame({
'x': np.random.randn(200),
'y': np.random.randn(200),
'z': np.random.randn(200),
'category': np.random.choice(['Type1', 'Type2', 'Type3'], 200)
})
fig = px.scatter_3d(df, x='x', y='y', z='z', color='category', opacity=0.7)
fig.write_html('scatter_3d.html')
3D interactive rotation is something that’s genuinely difficult to convey meaningfully in a static image, but works naturally in Plotly since the viewer can drag to rotate the plot themselves.
Internal Working: Why These Libraries Behave Differently
Matplotlib renders directly to a raster or vector image using its own internal drawing backend (Agg for raster PNGs, and others for PDF/SVG output) — it builds up a tree of Artist objects (lines, text, patches) and renders them pixel-by-pixel or as vector paths when you call savefig() or show(). This is why Matplotlib output is static: once rendered, it’s just pixels or fixed vector paths with no retained interactivity.
Plotly, in contrast, generates a JSON specification describing the chart’s data and layout, which gets handed to a JavaScript rendering library (Plotly.js) running in a browser or notebook environment. The actual interactivity — hovering, zooming, panning — happens entirely in the browser via JavaScript, reacting to the underlying data structure Plotly.py generated. This is fundamentally why Plotly charts can be interactive while Matplotlib charts (in their default output) cannot: they’re rendered by two completely different technology stacks.
Seaborn doesn’t have its own rendering engine at all — every Seaborn function ultimately calls Matplotlib functions internally, computing the necessary statistics (regression coefficients, kernel density estimates, aggregations) in Python/NumPy/pandas first, then handing the results to Matplotlib to actually draw. This is why a Seaborn figure object is still a genuine Matplotlib figure, and why you can freely mix sns. calls with plt. customization calls on the same chart.
Choosing the Right Library for the Job
I’ve settled into a rough decision process over time:
- Matplotlib: when I need precise, publication-quality control over every visual element, or need to embed plots in a larger application with fine-grained customization.
- Seaborn: for fast exploratory data analysis directly from a DataFrame, especially anything involving statistical relationships, distributions, or categorical comparisons.
- Plotly: when the output needs to be interactive, shared as a standalone web artifact, or embedded in a dashboard where users explore the data themselves.
Performance Considerations
For very large datasets (hundreds of thousands to millions of points), rendering performance differs meaningfully between these libraries. Matplotlib’s scatter plots can become slow to render and interact with beyond a few tens of thousands of points because each point is drawn individually as a vector object by default. Plotly can also slow down significantly with huge datasets since all the data gets embedded into the HTML/JSON payload sent to the browser, which can produce enormous file sizes.
import matplotlib.pyplot as plt
import numpy as np
# For large datasets, use rasterized=True to render points as a bitmap
# rather than individual vector objects, dramatically speeding up rendering
x = np.random.randn(500000)
y = np.random.randn(500000)
plt.figure(figsize=(8, 6))
plt.scatter(x, y, s=1, alpha=0.3, rasterized=True)
plt.savefig('large_scatter.png', dpi=150)
For genuinely massive datasets, I downsample or aggregate (e.g., using 2D histograms/hexbin plots instead of raw scatter points) rather than plotting every single data point directly.
Common Mistakes I’ve Made
- Forgetting
plt.show()orsavefig(), and wondering why nothing appeared. - Reusing the same figure across multiple plots without calling
plt.figure()orplt.clf(), causing charts to overlap unexpectedly. - Plotting massive datasets directly as scatter points without downsampling, leading to painfully slow rendering.
- Using Plotly for static reports meant to be printed or embedded as plain images, when a simpler Matplotlib PNG would have been more appropriate and far smaller in file size.
- Not setting
figsize, ending up with default-sized charts that look cramped once labels and titles are added.
Real-World Use Cases
- Exploratory data analysis — quickly visualizing distributions and relationships in a new dataset with Seaborn.
- Publication-ready figures for reports or papers, using Matplotlib’s fine-grained control over fonts, sizing, and layout.
- Interactive dashboards built with Plotly (often paired with Dash) for stakeholders to explore data themselves.
- Automated reporting pipelines generating standardized charts on a schedule, saved as image files.
FAQs
Do I need to learn Matplotlib if I only plan to use Seaborn? Yes, at least the basics — Seaborn returns Matplotlib objects, and customizing titles, labels, and layout beyond Seaborn’s defaults requires Matplotlib commands.
Can Plotly charts be saved as static images? Yes, using fig.write_image() (which requires the kaleido package), though this loses the interactivity that’s Plotly’s main advantage.
Which library is fastest for very large datasets? None of the three handle millions of raw points gracefully without downsampling or aggregation; specialized libraries like Datashader exist specifically for that scale.
Is Seaborn slower than Matplotlib since it’s built on top of it? For equivalent charts, the overhead is generally small since Seaborn’s extra work is mostly statistical computation (which is fast via NumPy/pandas), not rendering overhead.
Can I combine Matplotlib and Seaborn code in the same figure? Yes — since Seaborn functions operate on and return real Matplotlib Axes objects, you can freely mix sns. plotting calls with plt. or ax. customization calls.
Summary
Matplotlib, Seaborn, and Plotly each solve a different part of the data visualization problem: Matplotlib gives granular control and static, publication-ready output; Seaborn accelerates statistical exploration directly from DataFrames; Plotly delivers genuine browser-based interactivity through its JavaScript rendering layer. Understanding that Seaborn is built on Matplotlib, and that Plotly’s interactivity comes from a fundamentally different rendering approach, has helped me pick the right tool immediately rather than fighting the wrong library to do something it wasn’t designed for.
References
- Matplotlib Official Documentation: https://matplotlib.org/stable/
- Seaborn Official Documentation: https://seaborn.pydata.org/
- Plotly Python Official Documentation: https://plotly.com/python/
- Python Official Documentation: Data Science and Scientific Computing resources
