Interactive Visualizations with Plotly

Everything covered in the Matplotlib and Seaborn articles produces static images. plotly interactive visualizations are genuinely different — real, interactive widgets that run in a browser. This plotly express tutorial covers Plotly Express for fast charts, Graph Objects for full control, combining charts into a dashboard, and the two beginner traps — surprise file sizes and blank Jupyter charts — that most competing guides never mention.

Why Interactive Beats Static for Exploration

Static images vs. interactive widgets

Matplotlib and Seaborn draw static images — perfect for a printed report or a paper where nothing needs to move. Plotly charts are interactive widgets that run in the browser, letting users zoom in, filter, and hover over specific points for detailed information, all without you writing a single line of JavaScript yourself.

The analogy worth leading with

A static chart is like a printed photo. A Plotly chart is like Google Maps: the same picture to start with, except one lets you zoom in, pan around, and read the label on any specific point — genuinely the same underlying data, but one format lets you actively explore it, and the other only lets you look.

What this article covers

Plotly Express for fast, high-level charts, Graph Objects for full customization control, combining multiple charts into a dashboard-style layout, and saving and sharing finished figures — including the two beginner traps worth knowing about before you hit them yourself.

Plotly Express: The Fast Path to a Chart

What Plotly Express is

Plotly Express is a high-level interface to the Plotly graphing library that lets you create complex, highly interactive visualizations with just a few lines of code — Plotly's own official recommendation is to start here for the vast majority of charts, reaching for the lower-level Graph Objects interface (covered in Section 3) only once Express's defaults genuinely aren't flexible enough.

The basic pattern
import plotly.express as px
import pandas as pd

df = pd.DataFrame({
    "month": ["Jan", "Feb", "Mar", "Jan", "Feb", "Mar"],
    "region": ["East", "East", "East", "West", "West", "West"],
    "sales": [100, 120, 115, 90, 95, 105],
})

fig = px.line(df, x="month", y="sales", color="region", title="Monthly Sales by Region")
fig.show()

This works with a pandas DataFrame directly, mapping column names to chart properties — x, y, color — exactly the same dataset-oriented philosophy covered in the earlier Seaborn articles. color="region" automatically draws a separate, distinctly colored line per region, with a legend generated automatically, without you needing to loop or filter manually.

Installation
pip install --upgrade plotly

A quick tour of the common one-liner functions

px.line(df, x="month", y="sales", color="region")       # trends over an ordered sequence
px.bar(df, x="month", y="sales", color="region")         # comparing discrete categories
px.scatter(df, x="month", y="sales", color="region")      # examining point-level relationships
px.histogram(df, x="sales")                                 # a single variable's distribution

These four cover the overwhelming majority of everyday exploratory charting needs — and notice the choice between them follows exactly the same line/bar/scatter reasoning covered in the earlier Matplotlib plot-types article: an ordered sequence calls for a line chart, discrete category comparison calls for a bar chart, and examining a relationship between two variables calls for a scatter plot. Plotly doesn't change when to use each type — it changes what you get once you've chosen: a fully interactive version, rather than a static image.

Graph Objects: Full Control for Custom Layouts

The relationship between Express and Graph Objects

This is genuinely important to understand, since it explains why both interfaces exist at all: every figure produced with Plotly Express actually uses Graph Objects underneath — Express calls graph_objects internally and returns a fully-formed chart. Graph Objects (plotly.graph_objects, commonly imported as go) is the lower-level foundation Express itself is built on, and it's the tool you reach for specifically when Express's sensible, high-level defaults genuinely aren't flexible enough for what you need.

When Graph Objects specifically earns its extra typing
  • Custom layouts — precise control over spacing, sizing, and arrangement beyond what Express's parameters expose directly.

  • Fine-grained annotations — arbitrary text, arrows, or shapes placed at exact coordinates.

  • Complex subplots — genuinely custom multi-panel arrangements beyond Express's built-in faceting.

  • Non-standard chart types — candlestick charts, treemaps, and other specialized visualizations that don't have a dedicated px. one-liner.

  • Dynamically adding or removing traces — genuinely useful for real-time or continuously updating data, where you need to add new data to an already-rendered figure rather than rebuilding it from scratch each time.

A practical example: Graph Objects vs. Express, contrasted directly
import plotly.graph_objects as go

fig = go.Figure()

fig.add_trace(go.Scatter(
    x=["Jan", "Feb", "Mar"], y=[100, 120, 115],
    mode="lines+markers", name="East"
))
fig.add_trace(go.Scatter(
    x=["Jan", "Feb", "Mar"], y=[90, 95, 105],
    mode="lines+markers", name="West"
))

fig.update_layout(title="Monthly Sales by Region")
fig.show()
# The equivalent Express version — considerably more concise
fig = px.line(df, x="month", y="sales", color="region", title="Monthly Sales by Region")

Both produce visually equivalent charts — but the Graph Objects version required manually constructing each trace individually (go.Scatter(...), once per region), while Express handled that grouping automatically from a single color="region" argument. This is precisely the trade-off worth internalizing: Graph Objects gives you explicit, granular control over every trace and layout element individually; Express trades that granularity for speed, letting you get a fully interactive chart from a single, concise line whenever its defaults already do what you need.

Combining Charts into a Dashboard and Working with Pandas

Plotly's subplot functionality
from plotly.subplots import make_subplots
import plotly.graph_objects as go

fig = make_subplots(rows=1, cols=2, subplot_titles=("Sales Trend", "Sales by Region"))

fig.add_trace(go.Scatter(x=df["month"], y=df["sales"], mode="lines"), row=1, col=1)
fig.add_trace(go.Bar(x=df["region"], y=df["sales"]), row=1, col=2)

fig.update_layout(title_text="Sales Dashboard", showlegend=False)
fig.show()

make_subplots() combines multiple chart types — here, a line chart and a bar chart — into a single, cohesive layout, exactly the natural next step once individual chart types feel comfortable on their own, mirroring the multi-panel Axes grids covered in the earlier Matplotlib article, but interactive throughout.

Aggregating with pandas before plotting
raw_sales = pd.DataFrame({
    "month": ["Jan", "Jan", "Feb", "Feb", "Mar", "Mar"] * 2,
    "region": ["East", "West"] * 6,
    "amount": [50, 40, 60, 45, 58, 47, 55, 42, 62, 48, 60, 50],
})

summary = raw_sales.groupby(["month", "region"], as_index=False)["amount"].sum()
print(summary)

fig = px.bar(summary, x="month", y="amount", color="region", barmode="group")
fig.show()

Tying directly back to the earlier pandas groupby and aggregation article: Plotly, like Seaborn, generally works best when fed already-summarized data, rather than raw, row-level records, for most standard chart types — groupby() here collapses raw transaction-level data down to one summed value per month-region combination first, and that clean summary is what actually gets plotted.

A practical combined workflow

Building a sample sales dataset with monthly figures across regions, then creating an interactive line chart and bar chart from that same underlying DataFrame side by side — exactly the pattern demonstrated above — is genuinely representative of how Plotly gets used in real analysis work: aggregate cleanly with pandas first, then hand the clean result to Plotly Express (or Graph Objects, for a custom multi-panel layout) for the actual interactive visualization.

Saving, Sharing, and Two Common Beginner Traps

Saving as a shareable HTML file
fig.write_html("chart.html")

fig.write_html() saves a finished figure as a single, self-contained HTML file — the standard way to send an interactive chart to a teammate who doesn't have Python installed at all, since the resulting file opens directly in any web browser, with full interactivity intact, no server or Python environment required on the recipient's end.

Trap 1: the surprise file size

This genuinely catches people off guard the first time: a saved HTML export can balloon to several megabytes, even for a visually simple chart with very little actual data — because, by default, the exported file bundles the entire Plotly JavaScript library directly inside it, so the file is genuinely self-contained and works even completely offline.

fig.write_html("chart.html", include_plotlyjs="cdn")

include_plotlyjs="cdn" shrinks the exported file dramatically, by loading the Plotly JavaScript library from a remote CDN (content delivery network) instead of embedding the entire thing directly in the file. The trade-off: the resulting HTML file requires an active internet connection to actually render correctly, since it now depends on fetching that JavaScript library from the CDN at open time — genuinely worth the trade-off for sharing charts via email or a wiki where recipients will have internet access, but not appropriate for a chart that specifically needs to work fully offline.

Trap 2: a blank chart in Jupyter

This is another genuinely common, confusing beginner trap: a Plotly chart rendering as a completely blank area in a Jupyter notebook, with no error message at all. This is almost always caused by a missing or outdated nbformat package, or a misconfigured notebook renderer — not a problem with the chart's underlying data or code itself.

pip install --upgrade nbformat

Upgrading nbformat resolves this in the overwhelming majority of cases. If the chart still doesn't render after that, explicitly setting the renderer can help isolate the issue:

import plotly.io as pio
pio.renderers.default = "notebook"
A quick troubleshooting checklist
  • Chart is blank in Jupyter → upgrade nbformat, then check pio.renderers.default.

  • Exported HTML file is unexpectedly huge → add include_plotlyjs="cdn" to write_html().

  • Chart works in a script but not a notebook (or vice versa) → check which renderer is actually active, since Plotly behaves slightly differently depending on the environment it detects itself running in.

A natural next step

Once static notebook output feels limiting, and you want a genuinely live, continuously-updating data application rather than a one-off exported chart, tools like Streamlit or Dash (both building on top of Plotly's underlying charting engine) let you build interactive web-based dashboards directly in Python — a natural, considerably more advanced next step once everything covered in this article feels comfortable.