mrkeyoor.com_
Wed 05 Aug 05:00 UTC
PyPIDataupdated 05 Aug 2026

plotly

Interactive charting for Python, built on the plotly.js JavaScript library. Every figure renders as a live browser object with zoom, pan, hover tooltips, and legend toggling for free, across 30+ chart types including 3D, maps, statistical, and financial charts. There are two APIs: plotly.express for one-line charts straight from dataframes, and graph_objects for full control. Figures work in Jupyter and marimo notebooks, export to standalone HTML, and are the chart layer of the Dash web-app framework, all under an MIT license maintained by Plotly the company.

Verdict

The easiest way to get genuinely interactive charts out of Python dataframes, and plotly.express is a real productivity win. Accept the heavy HTML output, the kaleido dance for static export, and the two-API split; if your end product is a PNG in a paper, matplotlib remains the better tool.

API stability4/5The express and graph_objects APIs have been stable for years; v6 (2025) did shift internals to narwhals and changed the default notebook renderer to anywidget, which broke some environments on upgrade
Docs4/5plotly.com/python has hundreds of example-driven pages and a full figure reference; weaker at conceptual depth, so nontrivial customization becomes hovertemplate and update_layout trial and error
Maintenance4/5Corporate-backed with pushes days before this review and steady releases (6.9.0 current); around 774 open issues and priorities that track the commercial Dash product
Ecosystem4/516M weekly downloads, the Dash framework on top, kaleido and plotly-geo satellites, and a large community forum; smaller academic footprint than matplotlib, which most other Python viz tools target

Use it if

  • You want interactive charts (hover, zoom, toggle) in notebooks or HTML reports without writing any JavaScript
  • You live in dataframes: plotly.express turns a pandas or polars frame into a faceted, colored, tooltipped figure in one call
  • You need chart types matplotlib makes painful: 3D surfaces, choropleth and tile maps, sunbursts, candlesticks, animated frames
  • You plan to build a Dash app later; the figures carry over unchanged
Skip it if

Setup reality

pip install plotly works and fig.show() in a browser or classic script is instant. The friction is everything around it: Jupyter widget rendering needs the separate anywidget package, static image export (write_image) needs kaleido, which pulls a headless Chromium and fails in odd ways on slim Docker images and CI, and county-level geo features need a separate plotly-geo package. Since v6, dataframe interop goes through narwhals, so pandas is no longer a hard dependency but most examples still assume it. Expect to spend time in update_layout margins, template, and hovertemplate syntax to make default charts presentation-ready.

Patterns

One-line chart with plotly.expressquick-chart

import plotly.express as px

fig = px.bar(x=["a", "b", "c"], y=[1, 3, 2])
fig.show()

px is the high-level API; start here and drop to graph_objects only when you need trace-level control.

Scatter from a dataframe with color and sizedataframe-scatter

import plotly.express as px

df = px.data.gapminder().query("year == 2007")
fig = px.scatter(
    df, x="gdpPercap", y="lifeExp",
    color="continent", size="pop",
    hover_name="country", log_x=True,
)
fig.show()

Column names map straight to visual channels; since v6 polars and other frames work too via narwhals.

Multi-series line chartline-chart

fig = px.line(df, x="date", y="value", color="series",
              markers=True)
fig.show()

px.line needs long-format (tidy) data; wide dataframes should go through df.melt() first.

Titles, size, and templatestyle-layout

fig.update_layout(
    title="Revenue by month",
    template="plotly_white",
    width=800, height=450,
    margin=dict(l=40, r=20, t=60, b=40),
    legend=dict(orientation="h", y=-0.2),
)

Nearly all styling questions end at update_layout; template='plotly_white' is the usual first fix for the default grey look.

Control the hover tooltiphover-tooltip

fig = px.scatter(df, x="gdpPercap", y="lifeExp",
                 hover_data={"pop": ":,", "gdpPercap": ":.0f"})

# full control on a trace:
fig.update_traces(
    hovertemplate="%{customdata[0]}<br>GDP: %{x:$,.0f}<extra></extra>"
)

The <extra></extra> tag removes the secondary trace-name box; hovertemplate uses d3-format strings, not Python format specs.

Multiple charts in one figuresubplots

from plotly.subplots import make_subplots
import plotly.graph_objects as go

fig = make_subplots(rows=1, cols=2, subplot_titles=("A", "B"))
fig.add_trace(go.Scatter(x=x, y=y1), row=1, col=1)
fig.add_trace(go.Bar(x=x, y=y2), row=1, col=2)
fig.show()

make_subplots only accepts graph_objects traces; you cannot drop a px figure into a cell, though you can copy its fig.data traces over.

Build a figure trace by tracegraph-objects

import plotly.graph_objects as go

fig = go.Figure()
fig.add_trace(go.Scatter(x=x, y=actual, name="actual", mode="lines"))
fig.add_trace(go.Scatter(x=x, y=forecast, name="forecast",
                         mode="lines", line=dict(dash="dot")))
fig.show()

graph_objects is verbose but explicit; use it when express keyword arguments run out.

Save an interactive HTML fileexport-html

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

The default embeds all of plotly.js (several MB) in every file; include_plotlyjs='cdn' keeps files small but requires internet to view.

Export a static PNG or PDFexport-png

# pip install -U kaleido
fig.write_image("chart.png", scale=2)
fig.write_image("chart.pdf")

Requires the separate kaleido package, which runs a headless Chromium; on slim Docker images install its browser deps or this fails at runtime.

Plot large datasets with WebGLlarge-data-webgl

fig = px.scatter(df, x="x", y="y", render_mode="webgl")

# or explicitly:
fig = go.Figure(go.Scattergl(x=x, y=y, mode="markers"))

Default SVG rendering degrades past a few thousand points; WebGL handles far more but loses some styling options like dashed lines.

Small multiples with facetsfacet-grid

fig = px.scatter(df, x="gdpPercap", y="lifeExp",
                 facet_col="continent", facet_col_wrap=3)
fig.for_each_annotation(lambda a: a.update(text=a.text.split("=")[-1]))

Facet titles render as 'column=value'; the for_each_annotation line is the standard trick to strip the prefix.

Animate over a variableanimation

fig = px.scatter(px.data.gapminder(),
                 x="gdpPercap", y="lifeExp", size="pop",
                 color="continent", animation_frame="year",
                 log_x=True, range_y=[20, 90])
fig.show()

Set explicit range_x/range_y or the axes rescale every frame; animations do not survive static image export.

Alternatives

PackageRegistryPick it when
matplotlibPyPIPublication-quality static figures, print output, or the massive ecosystem built on it (seaborn, pandas .plot)
altairPyPIYou prefer a declarative Vega-Lite grammar and mostly make standard statistical charts
bokehPyPIInteractive plots with Python server callbacks and streaming data, without adopting Dash