plotly

Use when creating interactive Python visualizations with Plotly, including pan, zoom, hover charts, dashboard figures, interactive HTML output, or 3D plots. Trigger terms include plotly, interactive c

SKILL.md

GitHub

Plotly

Overview

Plotly is Python's main library for interactive visualization. Every figure is a JSON-backed object rendered by plotly.js in the browser, so charts pan, zoom, and show hover tooltips out of the box, and can be embedded in HTML pages, Jupyter notebooks, or Dash apps. Two APIs sit on the same figure model:

  • plotly.express (px) — high-level, one call per chart, tidy-DataFrame oriented. Returns a fully-built go.Figure. Start here for almost everything.
  • plotly.graph_objects (go) — low-level. You assemble a figure from traces (the data: go.Scatter, go.Bar, ...) and a layout (axes, legend, title). Use when you need mixed trace types, secondary axes, or fine control px does not expose.

Because px returns a go.Figure, the normal workflow is "build with px, then refine with go methods" (update_layout, update_traces, add_trace).

Installation

uv add plotly           # or: pip install plotly
# Static image export (PNG/PDF/SVG). Plotly 6.1.1+ uses Kaleido v1:
uv add kaleido
plotly_get_chrome       # one-time: installs the Chrome that Kaleido v1 drives
import plotly.express as px
import plotly.graph_objects as go
from plotly.subplots import make_subplots
import plotly.io as pio      # renderers, templates, export helpers

px bundles sample datasets: df = px.data.iris(), px.data.gapminder(), px.data.tips().

Core workflow

plotly.express (high-level)

df = px.data.gapminder().query("year == 2007")

# Scatter with color/size/hover encodings — px maps DataFrame columns to visuals
fig = px.scatter(
    df, x="gdpPercap", y="lifeExp",
    color="continent", size="pop", size_max=60,
    hover_name="country", log_x=True,
    labels={"gdpPercap": "GDP per capita", "lifeExp": "Life expectancy"},
    title="Wealth vs. health, 2007",
)
fig.show()   # opens interactive chart (browser / inline in notebook)

Common chart types share the same signature — swap the function:

px.line(px.data.stocks(), x="date", y="GOOG")                 # line / time series
px.bar(df, x="continent", y="pop", color="continent")          # bar (barmode="group"|"stack")
px.box(px.data.tips(), x="day", y="total_bill", color="smoker")# box
px.violin(px.data.tips(), y="total_bill", x="day", box=True)   # violin
px.histogram(px.data.tips(), x="total_bill", nbins=30)         # histogram
px.imshow(df.corr(numeric_only=True), text_auto=".2f",         # heatmap (matrix)
          color_continuous_scale="RdBu", zmin=-1, zmax=1)
px.scatter_3d(px.data.iris(), x="sepal_length", y="sepal_width",
              z="petal_width", color="species")                # 3D scatter

Faceting (small multiples) is a keyword, not a separate API:

fig = px.scatter(px.data.tips(), x="total_bill", y="tip",
                 color="sex", facet_col="time", facet_row="smoker")

graph_objects (low-level)

Use go when a single px call cannot express the figure — e.g. overlaying a bar and a line series, or a secondary y-axis:

fig = go.Figure()
fig.add_trace(go.Bar(x=months, y=revenue, name="Revenue"))
fig.add_trace(go.Scatter(x=months, y=margin, name="Margin %",
                         yaxis="y2", mode="lines+markers"))
fig.update_layout(
    title="Revenue and margin",
    yaxis=dict(title="Revenue ($)"),
    yaxis2=dict(title="Margin (%)", overlaying="y", side="right"),
    template="plotly_white",
)

Subplots

make_subplots builds a grid; add each trace to its row/col. Mixed types (e.g. a 3D scene next to a 2D chart) need a specs grid:

fig = make_subplots(rows=1, cols=2, subplot_titles=("Scatter", "Surface"),
                    specs=[[{"type": "xy"}, {"type": "scene"}]])
fig.add_trace(go.Scatter(x=x, y=y, mode="markers"), row=1, col=1)
fig.add_trace(go.Surface(z=z), row=1, col=2)
fig.update_layout(height=500, showlegend=False)

Updating layout, traces, axes

fig.update_layout(title="...", template="plotly_dark",
                  legend=dict(orientation="h", yanchor="bottom", y=1.02, x=0),
                  margin=dict(l=40, r=20, t=60, b=40))
fig.update_traces(marker=dict(size=8, line=dict(width=1, color="white")),
                  selector=dict(type="scatter"))   # target specific traces
fig.update_xaxes(title="Time", showgrid=False, rangeslider_visible=True)
fig.update_yaxes(type="log")

Hover and legends

# px: pick tooltip fields
px.scatter(df, x="x", y="y", hover_name="id", hover_data={"pop": ":,.0f", "x": False})

# go / any figure: custom hover template + unified hover across traces
fig.update_traces(hovertemplate="<b>%{x}</b><br>value: %{y:.1f}<extra></extra>")
fig.update_layout(hovermode="x unified")          # or "closest"
fig.update_layout(legend=dict(title="Series"), showlegend=True)

Export

fig.write_html("chart.html")                 # self-contained interactive page
fig.write_html("chart.html", include_plotlyjs="cdn")  # smaller file, needs internet
fig.write_image("chart.png", scale=2, width=1000, height=600)  # static; needs Kaleido
fig.write_image("chart.pdf")                 # also svg, jpeg, webp
html_str = fig.to_html(full_html=False)      # snippet to embed in a larger page

Gotchas / best practices

  • Static export needs Kaleido + Chrome. write_image / to_image require Kaleido. On Plotly 6.1.1+ (Kaleido v1) Chrome is not bundled — run plotly_get_chrome (or plotly.io.get_chrome()) once, or you get a "Chrome not found" error. Do not pass the engine= argument: it is deprecated in Plotly 6.2. For interactive output prefer write_html, which needs neither Kaleido nor Chrome.
  • px vs go — pick by shape of the work. Reach for px first: it wires up color/size legends, faceting, and hover from a tidy DataFrame in one line. Drop to go only for mixed trace types, secondary axes, or per-trace control. Never rebuild a px chart by hand — build with px, then .update_layout() / .update_traces() to refine.
  • Large data → WebGL. SVG traces (go.Scatter) bog down past ~10k points. Use the GL-backed equivalents (go.Scattergl, go.Scatter3d) or px.scatter(..., render_mode="webgl"). For very dense scatter/heatmap data use px.density_heatmap / go.Histogram2dContour to bin instead of drawing every point. Downsample time series before plotting.
  • Tidy (long-form) DataFrames make px effortless: one column per variable, one row per observation, then map columns to x/y/color/facet_*. Reshape wide data with df.melt(...) first.
  • Themes via template= ("plotly", "plotly_white", "plotly_dark", "simple_white", "presentation", ...). Set a default once: pio.templates.default = "plotly_white".
  • Rendering context. If fig.show() shows nothing, set the renderer explicitly: pio.renderers.default = "browser" (or "notebook", "vscode", "png").
  • Dash for apps. Turn figures into an interactive web app with callbacks: pip install dash; put figures in dcc.Graph(figure=fig), wire inputs/outputs with @callback, and run with app.run(debug=True) (Dash 3.x; the old app.run_server is deprecated). Dash is a separate deployment concern — build and test the figure with plotly first, then embed it.

Use this vs related skills

Use plotly for interactive/hover/dashboard charts and interactive HTML; use matplotlib/seaborn for static publication figures (PNG/PDF/SVG); use scientific-visualization for the broader figure-strategy and journal-formatting skill.