Last Updated: June 01, 2026

Beginner

Static charts made with Matplotlib are fine for reports and papers, but when you’re presenting data to a stakeholder or building a data product, you want something the viewer can interact with: hover over a data point to see exact values, zoom into a region, click to filter by category, or toggle series on and off. That interactivity makes data comprehensible in ways that a frozen PNG never can.

Plotly is the leading Python library for interactive data visualizations. It renders charts directly in Jupyter notebooks and web browsers, produces publication-quality output, and supports over 40 chart types — from basic bar and line charts to heatmaps, 3D scatter plots, geographic maps, and animated timelines. Plotly has two APIs: plotly.express for quick one-line charts and plotly.graph_objects for fine-grained customization. Install it with pip install plotly.

In this tutorial you’ll create line charts, bar charts, scatter plots, and heatmaps using Plotly Express, learn how to customize layouts and colors, combine multiple traces into a single figure, export charts to HTML and PNG, and build a multi-panel dashboard. By the end you’ll be producing interactive visualizations in a fraction of the time static charts take.

Pubs - Python How To Program
Written by Pubs

Python developer and educator with 15+ years building production systems across data engineering, web APIs, and AI tooling. Founder of Python How To Program — 270+ in-depth tutorials covering the modern Python stack.

View all tutorials by Pubs →

Python Plotly: Quick Example

Part of the Python Data Stack Hub. See the full hub for related Python tutorials.

Here is a complete interactive line chart in four lines of code, using a dataset built directly into Plotly.

# quick_plotly.py
import plotly.express as px

# Load a built-in dataset (no external file needed)
df = px.data.gapminder().query("country == 'Australia'")

fig = px.line(df, x="year", y="lifeExp", title="Life Expectancy in Australia Over Time",
              labels={"lifeExp": "Life Expectancy (years)", "year": "Year"})
fig.show()  # Opens in your browser -- fully interactive (hover, zoom, pan)

# Save as standalone HTML file
fig.write_html("life_expectancy.html")
print("Chart saved as life_expectancy.html")

Output:

Chart saved as life_expectancy.html
(Interactive chart opens in browser at http://localhost:XXXXX)

px.data.gapminder() returns a pandas DataFrame with country-level statistics including GDP per capita, population, and life expectancy from 1952 to 2007. The fig.show() call opens the chart in your default browser. Every Plotly chart is interactive by default: hover over any point to see its exact values, click and drag to zoom, double-click to reset the view. The write_html() output is a self-contained HTML file you can share with anyone — no Python installation needed to view it.

Plotly Express vs Graph Objects

Plotly has two layers of API. Plotly Express (plotly.express or px) is a high-level wrapper that generates common chart types from a DataFrame in one function call. Graph Objects (plotly.graph_objects or go) is the lower-level API that gives you complete control over every element of the chart.

FeaturePlotly Express (px)Graph Objects (go)
Code length1-3 lines for most charts10-30+ lines for same chart
Data inputpandas DataFrame (column names)Raw arrays and dicts
CustomizationLimited to function parametersFull control over every trace
Multi-trace chartsVia color= and facet_ paramsManual add_trace() calls
Best forExploratory analysis, quick chartsProduction dashboards, edge cases

The best approach is to start with Plotly Express and drop down to Graph Objects only when you need something Express cannot do. You can also mix them: create a chart with Express and then call fig.update_layout() or fig.update_traces() to make fine-grained adjustments.

Tutorial image
fig.show(): static PNGs were a cry for help.

Common Chart Types with Plotly Express

Most data stories fit into a handful of chart types. Here is a working example for each of the most common ones, all using built-in Plotly datasets so you can run them without any external data files.

# common_charts.py
import plotly.express as px

# --- Bar Chart ---
df_tips = px.data.tips()
fig_bar = px.bar(df_tips, x="day", y="total_bill", color="sex",
                 barmode="group",
                 title="Restaurant Bills by Day and Gender",
                 labels={"total_bill": "Total Bill ($)", "day": "Day of Week"})
fig_bar.write_html("bar_chart.html")

# --- Scatter Plot ---
df_gap = px.data.gapminder().query("year == 2007")
fig_scatter = px.scatter(df_gap, x="gdpPercap", y="lifeExp",
                         size="pop", color="continent",
                         hover_name="country",
                         log_x=True,  # log scale on x axis
                         title="GDP vs Life Expectancy (2007)",
                         labels={"gdpPercap": "GDP per Capita (log scale)",
                                 "lifeExp": "Life Expectancy"})
fig_scatter.write_html("scatter_chart.html")

# --- Histogram ---
fig_hist = px.histogram(df_tips, x="total_bill", nbins=20,
                        color="smoker",
                        marginal="rug",  # adds a rug plot above histogram
                        title="Distribution of Restaurant Bills")
fig_hist.write_html("histogram.html")

# --- Box Plot ---
fig_box = px.box(df_tips, x="day", y="tip", color="sex",
                 title="Tip Distribution by Day and Gender",
                 points="all")  # show all data points as dots
fig_box.write_html("boxplot.html")

print("All charts saved as HTML files.")

Output:

All charts saved as HTML files.

The size="pop" argument in the scatter plot maps bubble size to the pop column — Plotly scales the circles automatically. hover_name="country" makes the country name bold in the hover tooltip. log_x=True applies a logarithmic scale to the x-axis, which is standard practice when GDP data spans several orders of magnitude. The marginal="rug" parameter on the histogram adds a one-dimensional scatter plot along the axis, showing individual data points in addition to the distribution shape.

Customizing Layouts and Styles

Plotly Express creates charts with sensible defaults, but you’ll almost always want to adjust titles, colors, fonts, axis labels, and layout for presentation. The update_layout() and update_traces() methods let you modify any aspect of the chart after creation.

# customization.py
import plotly.express as px
import plotly.graph_objects as go

df = px.data.gapminder().query("continent == 'Europe' and year == 2007")
fig = px.bar(df, x="country", y="gdpPercap",
             color="gdpPercap",
             color_continuous_scale="Viridis",
             title="GDP per Capita -- European Countries 2007")

# Customize the layout
fig.update_layout(
    title_font_size=20,
    title_font_family="Arial",
    xaxis_tickangle=-45,       # rotate x labels for readability
    xaxis_title="Country",
    yaxis_title="GDP per Capita (USD)",
    plot_bgcolor="white",
    paper_bgcolor="white",
    font=dict(family="Arial", size=12, color="#333333"),
    coloraxis_showscale=False,  # hide the color legend
    margin=dict(t=80, b=100, l=60, r=20),
    height=500
)

# Add a horizontal reference line at the European average
avg_gdp = df["gdpPercap"].mean()
fig.add_hline(y=avg_gdp, line_dash="dash", line_color="red",
              annotation_text=f"EU Avg: ${avg_gdp:,.0f}",
              annotation_position="top right")

fig.write_html("europe_gdp_styled.html")

# Export as a static PNG (requires kaleido: pip install kaleido)
try:
    fig.write_image("europe_gdp.png", width=1200, height=500, scale=2)
    print("PNG saved (requires kaleido package)")
except Exception:
    print("Install kaleido for PNG export: pip install kaleido")

Output:

PNG saved (requires kaleido package)

The add_hline() call adds an annotation line across the full width of the chart at a specific y-value. Similar methods include add_vline(), add_hrect() (horizontal band), and add_vrect() (vertical band). These are perfect for showing thresholds, targets, or date ranges. Static PNG export requires the kaleido package (pip install kaleido) in addition to Plotly — Plotly itself only handles the interactive HTML output natively.

Tutorial image
make_subplots(rows=2, cols=2): four charts for the price of one fig.show().

Multi-Trace and Subplot Charts

Many real dashboards combine multiple related charts in a single figure. Plotly’s make_subplots() function creates a grid of sub-axes that you can populate independently, while all sharing the same interactive controls.

# subplots.py
import plotly.graph_objects as go
from plotly.subplots import make_subplots
import plotly.express as px

df = px.data.gapminder()
countries = ["United States", "China", "India", "Germany"]
colors = {"United States": "#1f77b4", "China": "#ff7f0e",
          "India": "#2ca02c", "Germany": "#d62728"}

fig = make_subplots(
    rows=1, cols=2,
    subplot_titles=("Life Expectancy Over Time", "GDP Per Capita Over Time"),
    shared_xaxes=True  # zoom on one chart affects the other
)

for country in countries:
    country_df = df[df["country"] == country]
    color = colors[country]

    # Left chart: life expectancy
    fig.add_trace(
        go.Scatter(x=country_df["year"], y=country_df["lifeExp"],
                   mode="lines+markers", name=country,
                   line=dict(color=color, width=2),
                   legendgroup=country),  # group legend entries
        row=1, col=1
    )

    # Right chart: GDP per capita
    fig.add_trace(
        go.Scatter(x=country_df["year"], y=country_df["gdpPercap"],
                   mode="lines+markers", name=country,
                   line=dict(color=color, width=2),
                   legendgroup=country,
                   showlegend=False),  # avoid duplicate legend entries
        row=1, col=2
    )

fig.update_layout(
    title_text="Global Development Trends: Selected Countries",
    height=450,
    hovermode="x unified"  # show all traces at same x position on hover
)

fig.update_yaxes(title_text="Life Expectancy (years)", row=1, col=1)
fig.update_yaxes(title_text="GDP per Capita (USD)", row=1, col=2)

fig.write_html("development_dashboard.html")
print("Dashboard saved.")

Output:

Dashboard saved.

The legendgroup=country parameter links the two traces for each country so that clicking a country name in the legend shows or hides both its life expectancy and GDP lines simultaneously. hovermode="x unified" is a particularly useful layout setting: it shows all traces’ values for the same x-coordinate in a single tooltip, making it easy to compare values at the same year across countries.

Real-Life Example: Sales Performance Dashboard

Tutorial image
fig.write_html(): the only report your manager will actually interact with.

Here is a self-contained sales dashboard that generates realistic sample data and builds a multi-panel figure with KPI annotations, a trend line, and a product breakdown.

# sales_dashboard.py
import plotly.graph_objects as go
from plotly.subplots import make_subplots
import random
from datetime import date, timedelta

# Generate realistic sample sales data
random.seed(42)
months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun",
          "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]
revenue = [random.randint(80000, 200000) for _ in months]
units = [random.randint(400, 900) for _ in months]
products = {"Widget A": 35, "Widget B": 25, "Widget C": 20, "Other": 20}

fig = make_subplots(
    rows=2, cols=2,
    subplot_titles=("Monthly Revenue", "Units Sold",
                    "Revenue Trend (with 3-month avg)", "Product Mix"),
    specs=[[{"type": "bar"}, {"type": "scatter"}],
           [{"type": "scatter"}, {"type": "pie"}]]
)

# Top-left: Monthly revenue bar chart
fig.add_trace(
    go.Bar(x=months, y=revenue, name="Revenue",
           marker_color=["#2ecc71" if r >= 130000 else "#e74c3c" for r in revenue]),
    row=1, col=1
)

# Top-right: Units sold as scatter with lines
fig.add_trace(
    go.Scatter(x=months, y=units, mode="lines+markers", name="Units",
               line=dict(color="#3498db", width=2), marker=dict(size=8)),
    row=1, col=2
)

# Bottom-left: Revenue with 3-month moving average
moving_avg = [sum(revenue[max(0,i-2):i+1]) / len(revenue[max(0,i-2):i+1]) for i in range(len(revenue))]
fig.add_trace(go.Bar(x=months, y=revenue, name="Monthly Revenue",
                     marker_color="#bdc3c7", showlegend=False), row=2, col=1)
fig.add_trace(go.Scatter(x=months, y=moving_avg, mode="lines", name="3-Mo Avg",
                         line=dict(color="#e67e22", width=3, dash="dash")), row=2, col=1)

# Bottom-right: Product mix pie chart
fig.add_trace(
    go.Pie(labels=list(products.keys()), values=list(products.values()),
           name="Products", hole=0.4),
    row=2, col=2
)

total_rev = sum(revenue)
fig.update_layout(
    title_text=f"2026 Sales Dashboard -- Total Revenue: ${total_rev:,}",
    height=700,
    showlegend=True,
    plot_bgcolor="white"
)

fig.write_html("sales_dashboard.html")
print(f"Dashboard saved. Total revenue: ${total_rev:,}")

Output:

Dashboard saved. Total revenue: $1,567,000

The specs parameter in make_subplots() defines the chart type for each cell — this is required when mixing different trace types like bars, scatter plots, and pie charts in the same figure. The color list comprehension ["#2ecc71" if r >= 130000 else "#e74c3c" for r in revenue] colors revenue bars green if they exceed a threshold and red otherwise — a simple but effective way to highlight performance against a target. Extend this dashboard by wiring it up to a live database query and serving it with Flask or Dash for a real-time monitoring page.

Frequently Asked Questions

How do I display Plotly charts in Jupyter Notebook?

Plotly integrates with Jupyter automatically — fig.show() renders the interactive chart inline in the notebook cell. For JupyterLab, you may need to run pip install "jupyterlab>=3" ipywidgets and enable the Plotly extension. In VS Code’s Jupyter extension, charts render inline without any extra setup. If charts appear blank, try fig.show(renderer="iframe") as a fallback.

Does Plotly work with pandas DataFrames?

Yes, Plotly Express is designed around pandas DataFrames. You pass column names as strings to px.line(df, x="date_col", y="value_col") and Plotly handles the rest. You can also use Plotly’s pandas backend: import plotly.io as pio; pio.templates.default = "plotly_white" and then df.plot(backend="plotly") to use Plotly syntax on any pandas DataFrame plot call.

What is Plotly Dash and how is it different?

Dash is a full web application framework built on top of Plotly. While Plotly creates static interactive charts saved as HTML files, Dash lets you build live dashboards with Python callbacks: dropdowns, sliders, and buttons that trigger Python functions to update charts in real time. Dash runs as a web server. Install it with pip install dash. If you need a shareable, embeddable chart, use Plotly directly; if you need a live app with user controls, use Dash.

How do I export Plotly charts as PNG or PDF?

Static export requires the kaleido package: pip install kaleido. Then use fig.write_image("chart.png"), fig.write_image("chart.pdf"), or fig.write_image("chart.svg"). Use the scale parameter for higher resolution: fig.write_image("chart.png", scale=3) produces a 3x resolution image suitable for print. For batch export of many charts, reuse the same kaleido process by calling multiple write_image() calls on the same Python process.

How do I apply a consistent theme to all my charts?

Plotly includes several built-in themes: "plotly" (default), "plotly_white", "plotly_dark", "ggplot2", "seaborn", "simple_white", and "none". Set a global default with import plotly.io as pio; pio.templates.default = "plotly_white" and all subsequent charts will use that theme. You can also create custom templates by extending existing ones with pio.templates["my_theme"] = go.layout.Template(...).

Conclusion

Plotly makes interactive data visualization accessible in Python with minimal code. You learned to create line charts, bar charts, scatter plots, histograms, and box plots with Plotly Express, customize layouts with update_layout(), combine multiple traces with make_subplots(), and build a complete sales performance dashboard with moving averages and color-coded KPIs. The write_html() output is a shareable, self-contained file that any stakeholder can open in a browser without installing Python.

The natural next step is Plotly Dash for building live dashboards with interactive controls, or integrating Plotly charts into a Flask or FastAPI application as JSON responses rendered by the frontend. Both approaches build directly on the Plotly knowledge you’ve developed here.

Complete documentation and an extensive example gallery are available at plotly.com/python.