Plotly: Interactive Visualizations & Dashboards

You have a chart, and the first question in every meeting is the same: “What is the exact value at that spike?” A static PNG cannot answer. Python Plotly can: hover for the number, drag to zoom, click a legend entry to hide a series. This post covers building interactive charts, saving them as one shareable HTML file, and picking the right Plotly API for the job.

“Above all else, show the data.”

Edward Tufte, The Visual Display of Quantitative Information

Last Updated: July 2026 | Tested on: Python 3.14.6, Plotly 6.8.0 | Difficulty: Advanced | Reading Time: 15 minutes

Matplotlib and Seaborn draw static images. They are perfect for a printed report or a paper, where nothing moves. But when you are exploring data yourself, or building a dashboard for people who like to poke at numbers, you want a chart you can touch. That is where Python Plotly lives. Think of a static chart as a printed photo and a Plotly chart as Google Maps: same picture to start with, except one lets you zoom in, pan around, and read the label on any point. Every Plotly figure is an interactive widget that runs in the browser, and you write zero JavaScript to get it.

Python Plotly gives you two ways in. Plotly Express is the high-level API, one function call per chart, the way Seaborn works but interactive. Graph Objects is the low-level API where you build a figure trace by trace and control every detail. Start with Express for most of your work. Drop to Graph Objects when you need custom traces, animation frames, or a layout Express cannot express.

Take Pravin, a sales analyst on our team. He built a sales review in Matplotlib once and exported 15 PNG files into a slide deck. Then he rebuilt the whole thing as one Plotly HTML file. Now stakeholders hover to read the exact value, drag to zoom into one quarter, and click a name in the legend to hide that person. The meeting went from “Can you go back to slide 7?” to “Let me look at that spike myself.” Same data, very different room.

📊 PlotlyEcosystemPlotly ExpressHigh-level APIOne-liner chartsGraph ObjectsLow-level APIFull controlDashWeb app frameworkInteractive dashboardspx.scatter, px.linepx.barpx.histogram, px.boxpx.violinpx.choroplethpx.scatter_geogo.FigureTraces + Layoutgo.Scatter, go.Bargo.HeatmapSubplotsAnimation framesdcc.GraphInteractive componentCallbacksReactive updatesPython Plotly: How Express, Graph Objects, and Dash Fit Together

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

The diagram maps Python Plotly into three tiers. Plotly Express gives you quick, high-level charts in one line. Graph Objects lets you customize every element of a figure. Dash wraps your charts in a full web app with buttons, dropdowns, and live updates. Most work starts in Express for fast exploration, drops to Graph Objects when you need exact control, and only graduates to Dash when stakeholders need a live dashboard they can click around in. This post covers the first two tiers, which is everything you need to make a chart someone can actually use.

Prerequisites

You should be comfortable with a pandas DataFrame and have seen at least one static plotting library, like our Seaborn tutorial. You do not need any JavaScript or web experience. Everything here is pure Python.

Install and Verify Python Plotly

One install line covers the whole library. Plotly ships with its own copy of plotly.js bundled in, so there is nothing else to set up.

📄 install Plotly

pip install plotly

📄 verify_plotly.py: confirm the install worked

import plotly
print("Plotly version:", plotly.__version__)

▶ Output

Plotly version: 6.8.0

What happened here: If you see a version number, you are ready. This post was tested on Plotly 6.8.0. If you instead see ModuleNotFoundError: No module named 'plotly', the install landed in a different Python than the one running your script. Run pip install plotly with the same interpreter you use to run code, or use a virtual environment so the two always match.

The Quick Win: One Interactive Chart

Here is the fastest Python Plotly path from numbers to a chart you can hover over. Six months of revenue, one line of plotting, and one line to save it.

📄 quick_win.py: from data to a shareable interactive chart

import plotly.express as px

months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun"]
revenue = [120, 135, 128, 160, 178, 195]

fig = px.line(x=months, y=revenue, markers=True,
              title="Monthly Revenue (thousands)")
fig.write_html("revenue.html")
print("Saved revenue.html")
print("Points plotted:", len(fig.data[0].x))

▶ Output

Saved revenue.html
Points plotted: 6

What happened here: px.line built a complete interactive figure from two plain Python lists. fig.write_html("revenue.html") saved it as a standalone file you can email to anyone, double-click, and open in any browser. No server, no Python needed on their end. Open that file and you can hover each point to read the exact revenue, drag a box to zoom, and double-click to reset. If you are at an interactive prompt or in a notebook, swap the save line for fig.show() and the chart opens right away.

Plotly Express: One-Line Interactive Charts

Plotly Express is the menu at a good restaurant: you point at what you want and it arrives plated. You hand it a DataFrame and tell it which column goes on x, which goes on y, and which column should set the color. It handles legends, tooltips, axis labels, and colors for you. Here are the three charts you will reach for most often, all from the same small sales table.

📄 plotly_express.py: scatter, bar, and histogram from one DataFrame

import plotly.express as px
import pandas as pd
import numpy as np

rng = np.random.default_rng(42)
df = pd.DataFrame({
    "name": ["Rahul", "Niranjan", "Viraj", "Pravin", "Sardar"] * 4,
    "quarter": ["Q1"]*5 + ["Q2"]*5 + ["Q3"]*5 + ["Q4"]*5,
    "sales": rng.integers(50, 200, 20),
    "region": rng.choice(["North", "South", "West"], 20)
})
print(df.head())

# Interactive scatter: color by region, size by sales
fig = px.scatter(df, x="sales", y="name", color="region",
                 size="sales", hover_data=["quarter"],
                 title="Sales by Person and Region")
# fig.show()                       # open in a browser
# fig.write_html("scatter.html")   # save as a standalone file
print("scatter traces:", len(fig.data))

# Interactive grouped bar chart
fig2 = px.bar(df, x="quarter", y="sales", color="name",
              barmode="group", title="Quarterly Sales by Person")
print("bar traces:", len(fig2.data))

# Interactive histogram
fig3 = px.histogram(df, x="sales", color="region", nbins=15,
                    title="Sales Distribution by Region")
print("histogram traces:", len(fig3.data))

▶ Output

       name quarter  sales region
0     Rahul      Q1     63  South
1  Niranjan      Q1    166  South
2     Viraj      Q1    148  North
3    Pravin      Q1    115   West
4    Sardar      Q1    114   West
scatter traces: 3
bar traces: 5
histogram traces: 3

What happened here: Each color= argument quietly split the data into one trace per category, which is why the scatter has 3 traces (one per region) and the bar has 5 (one per name). A trace is just one series in the chart, and each one shows up as a clickable legend entry. The rng = np.random.default_rng(42) seed means you get these exact same numbers when you run it, so your DataFrame and trace counts will match the output above. In a browser, click a region in the scatter legend and it disappears, which is the fastest way to compare two groups without writing any filter code.

Graph Objects: Full Control

Express is the menu. Graph Objects is the kitchen. You build the figure from scratch: create an empty go.Figure, add each trace yourself, then set the layout. It is more typing, but you control every line color, dash pattern, and hover behavior. Reach for it when Express cannot say exactly what you mean.

📄 graph_objects.py: two custom lines on one figure

import plotly.graph_objects as go
import numpy as np

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

fig = go.Figure()
fig.add_trace(go.Scatter(x=x, y=np.sin(x), mode="lines",
                         name="sin(x)", line=dict(color="#50fa7b", width=2)))
fig.add_trace(go.Scatter(x=x, y=np.cos(x), mode="lines",
                         name="cos(x)", line=dict(color="#ff79c6", width=2, dash="dash")))

fig.update_layout(
    title="Trigonometric Functions (Interactive)",
    xaxis_title="x (radians)",
    yaxis_title="y",
    template="plotly_dark",
    hovermode="x unified"
)
# fig.show()
print("traces:", len(fig.data))
print("trace names:", [t.name for t in fig.data])

▶ Output

traces: 2
trace names: ['sin(x)', 'cos(x)']

What happened here: We added two go.Scatter traces by hand, one solid green and one dashed pink, then styled the whole figure with update_layout. The template="plotly_dark" gives the dark background, and hovermode="x unified" is the nice touch: hover anywhere and a single tooltip shows both sin and cos at that x value, instead of two separate popups. This is the kind of control Express does not hand you directly. The rule of thumb: start in Express, and only open the Graph Objects kitchen when you hit a wall.

A Real Task: Sales by Region

Charts are most useful after you summarize. Think of a cricket scorecard: nobody reads every single ball, they just want the final total for each team. Raw rows are noisy; a manager wants the one bar chart that answers “which region sold the most?” Here we group the same sales data by region, total it, sort it, and plot the result. This is the everyday move: aggregate with pandas first, then hand the small table to Python Plotly.

📄 region_sales.py: aggregate with pandas, then plot

import plotly.express as px
import pandas as pd
import numpy as np

rng = np.random.default_rng(42)
df = pd.DataFrame({
    "name": ["Rahul", "Niranjan", "Viraj", "Pravin", "Sardar"] * 4,
    "quarter": ["Q1"]*5 + ["Q2"]*5 + ["Q3"]*5 + ["Q4"]*5,
    "sales": rng.integers(50, 200, 20),
    "region": rng.choice(["North", "South", "West"], 20)
})

# Total sales per region, highest first
by_region = (df.groupby("region")["sales"]
               .sum()
               .sort_values(ascending=False)
               .reset_index())
print(by_region)

fig = px.bar(by_region, x="region", y="sales",
             title="Total Sales by Region", text_auto=True)
fig.write_html("region_sales.html")
print("Saved region_sales.html")

▶ Output

  region  sales
0  South   1022
1  North    846
2   West    735
Saved region_sales.html

What happened here: The pandas chain did the thinking, groupby to bucket by region, sum to total each bucket, sort_values to rank them, and reset_index to turn the result back into a flat DataFrame that Plotly likes. Then px.bar drew it, and text_auto=True printed each total right on top of its bar so nobody has to hover to read the headline number. South leads at 1022, North follows at 846, West trails at 735. These are stable numbers because of the seed, so your run will show the same ranking. Pandas 2.3.3 runs Copy-on-Write by default, so the grouped result is its own independent frame and never quietly mutates the original df.

Common Mistakes

Mistake 1: A 5 MB HTML file for one tiny chart

By default, write_html packs the entire plotly.js library inside the saved file so it works completely offline. That is great for an email attachment, and surprising the first time you notice a three-point chart weighs nearly 5 MB. If the chart will live on a page that already has internet, tell Plotly to load its JavaScript from a CDN (Content Delivery Network) instead and the file shrinks to a few KB.

📄 html_size.py: inline bundle versus CDN

import plotly.express as px
import os

fig = px.line(x=[1, 2, 3], y=[4, 5, 6], title="Tiny demo")

fig.write_html("full.html")                      # bundles all of plotly.js inline
fig.write_html("cdn.html", include_plotlyjs="cdn")  # loads plotly.js from a CDN

print(f"full.html: {os.path.getsize('full.html') / 1024:.0f} KB")
print(f"cdn.html:  {os.path.getsize('cdn.html') / 1024:.0f} KB")

▶ Output

full.html: 4742 KB
cdn.html:  8 KB

What happened here: Same chart, two very different files: 4742 KB with the library baked in, 8 KB pointing at a CDN. Use the full inline file when the chart must work offline, such as an email attachment or a USB drive. Use include_plotlyjs="cdn" when the chart lives on a web page and the viewer has internet, so the heavy library downloads once and the browser caches it.

Mistake 2: Nothing shows up in Jupyter

You run fig.show() in a notebook and get a blank space instead of a chart. The usual cause is a missing helper package. Plotly needs nbformat to render inside Jupyter, and if it is absent the figure silently fails to appear.

✅ The fix

# In a terminal, install the notebook helper once:
#   pip install nbformat

# If the chart still does not show, force a specific renderer:
fig.show(renderer="notebook")   # inside Jupyter / JupyterLab
fig.show(renderer="browser")    # open in your default browser instead

# Always works as a fallback, no renderer needed:
fig.write_html("chart.html")    # then open the file by hand

Why: Plotly figures need a renderer that matches where you are running. In a fresh notebook the notebook renderer needs nbformat installed. In an editor like VS Code, the browser renderer opens the chart in a real browser tab. When you are unsure, write_html never depends on any renderer at all: it just writes a file you can open yourself, which makes it the reliable fallback.

Best Practices

  • DO start every Python Plotly project with Plotly Express. Move to Graph Objects only when Express cannot do what you need.
  • DO aggregate with pandas first (groupby, sum, sort_values), then plot the small summary table.
  • DO use write_html(..., include_plotlyjs="cdn") for charts that go on a web page, to keep file size tiny.
  • DO set hover_data=[...] so the tooltip shows the extra columns people will ask about.
  • DON’T reach for Plotly when a static image is all you need. A PNG from Matplotlib is lighter for a printed report.
  • DON’T forget to install nbformat if your charts refuse to appear in Jupyter.

Practice Exercises

  1. Exercise 1: Build a px.scatter from the sales DataFrame with x="sales", y="quarter", and color="region". Save it with write_html and open it in a browser.
  2. Exercise 2: Recreate the sin and cos figure with Graph Objects, then add a third trace for sin(x) + cos(x) in a new color.
  3. Exercise 3: Group the sales data by name instead of region, total the sales, sort them, and draw a bar chart with text_auto=True.

Conclusion

You now know how to turn plain numbers into Python Plotly charts people can actually touch. You saw the fast path with px.line, learned when Plotly Express (the menu) is enough and when Graph Objects (the kitchen) earns its extra typing, aggregated with pandas before plotting a clean summary, and saved everything as a single shareable HTML file. You also met the two traps that catch most beginners: the surprise 5 MB file and the blank chart in Jupyter. That is enough to build interactive visuals your teammates can hover, zoom, and filter on their own.

Next up, we take these charts a step further and turn a plain Python script into a live data app with Streamlit, so anyone can open it in a browser and interact without touching code. For the full path from Python basics to machine learning, head back to the Python + AI/ML tutorial series home and keep going in order.

Frequently Asked Questions

When should I use Plotly vs Matplotlib in Python?

Use plotly python for interactive exploration, dashboards, and web pages where people want to hover, zoom, and toggle series. Use Matplotlib for static, print-ready figures, machine learning (ML) training curves, and times you need pixel-level control over a saved image. Our visualization tools comparison draws the same chart in all three libraries and gives a decision guide.

Can I embed a Plotly chart in a website?

Yes. fig.write_html() creates a standalone HTML file with the chart and all its JavaScript. For a smaller file on a page that already has internet, pass include_plotlyjs=”cdn” so plotly.js loads from a CDN instead of being baked in. For full apps with dropdowns and live updates, use Dash, which is Plotly’s pure-Python web framework.

Why is my Plotly HTML file so large?

By default write_html() bundles the entire plotly.js library inside the file so it works offline, which makes even a tiny chart around 5 MB. Pass include_plotlyjs=”cdn” to load the library from a CDN and shrink the file to a few KB, as long as the viewer has internet.

Is Plotly free to use?

Yes. The Plotly Python library is open source under the MIT license and free for any use, including commercial. Plotly also sells a hosted product called Plotly Studio, but you never need it for the local work in this post. Every feature shown here is free.

What is the difference between Plotly Express and Graph Objects?

Plotly Express is the high-level API: one function call like px.scatter builds a full chart from a DataFrame. Graph Objects is the low-level API where you create a go.Figure and add each trace yourself for complete control over styling and layout. Start with Express and drop to Graph Objects only when you need custom traces, animations, or layouts Express cannot produce.

Interview Questions on Plotly

Scenario questions, not trivia: this is the form this topic takes in a real interview.

Q: What is a “trace” in Plotly, and how does the color= argument change how many you get?

A trace is one data series inside a figure, and each one shows up as a single clickable legend entry. When you pass color="region" in Plotly Express, it quietly splits the data into one trace per unique category, so a scatter colored by three regions has three traces. That is why clicking a legend entry hides exactly one group.

Q: Your colleague opens a saved revenue.html on a laptop with no internet and the chart area is blank. What likely went wrong and how do you fix it?

The file was almost certainly written with include_plotlyjs="cdn", so plotly.js is fetched from the internet the moment the page opens. With no connection the library never loads and the chart cannot render. Re-save it with the default fig.write_html("revenue.html"), which bundles the whole library inline so the file works fully offline.

Q: fig.show() renders nothing in a fresh Jupyter notebook, but write_html works fine. What do you check first?

Check that nbformat is installed, because Plotly needs it to render inline in a notebook and fails silently without it. If it is already there, force a renderer with fig.show(renderer="notebook"), or use renderer="browser" to open the chart in a real browser tab. The reliable fallback is write_html, which never depends on a renderer at all.

Q: How do you make one tooltip show every line’s value at a given x instead of a separate popup per line?

Set hovermode="x unified" inside update_layout. Hovering anywhere on the plot then shows a single combined tooltip listing each trace’s y value at that x position, which makes comparing several lines much easier. It is a layout setting, so you reach for it most naturally in the Graph Objects API.

Q: Why do the examples seed NumPy with np.random.default_rng(42)?

The seed makes the random data reproducible, so every reader generates the exact same DataFrame and the printed trace counts and totals match what the post shows. Without a fixed seed each run would produce different numbers and the output blocks would no longer line up. It is a standard trick for tutorials and tests where results must stay stable.

Q: A manager asks for a chart ranking regions by total sales. Walk through the pipeline from raw rows to the bar.

Summarize with pandas first: groupby("region")["sales"].sum() to total each region, then sort_values(ascending=False) to rank them, then reset_index() to flatten the result into a plain DataFrame. Hand that small table to px.bar with x="region", y="sales", and text_auto=True so each total is printed on its bar. Aggregating before plotting keeps the chart to the few numbers the manager actually wants.

Further reading: Plotly Python documentation is the authoritative source on this.

Previous: Seaborn: Statistical Visualization, Heatmaps, Distributions

Next: Streamlit Tutorial: Turn a Python Script into a Data App

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 *