Fix: Plotly Not Working — Figure Not Showing, Kaleido Export Errors, and Subplot Issues
Quick Answer
How to fix Plotly errors — figure not rendering in Jupyter, ValueError must install Kaleido, static image export slow or crashing, subplot trace placement, update_layout not working, and offline vs online mode confusion.
The Error
You run fig.show() in Jupyter and the plot doesn’t appear — just a blank output or raw HTML:
import plotly.express as px
fig = px.scatter(df, x="x", y="y")
fig.show()
# Nothing visible. Or raw HTML dumped to output.Or you try to save a figure as PNG and Plotly complains about Kaleido:
ValueError:
Image export using the "kaleido" engine requires the kaleido package,
which can be installed using pip:
$ pip install -U kaleidoOr the PNG export takes 30+ seconds or crashes with a Chrome error:
ValueError: Transform failed with error code 525: Input/output error
RuntimeError: Failed to create transportOr you add traces to subplots and they all end up in the first panel:
fig = make_subplots(rows=1, cols=2)
fig.add_trace(go.Scatter(...)) # Goes to row=1, col=1 by default
fig.add_trace(go.Scatter(...)) # Also goes to row=1, col=1!Plotly is a JavaScript-based library with Python bindings — figures are HTML/JavaScript that render in a browser, not raw image files. The static export path (PNG/PDF) uses a separate engine (Kaleido) that spawns headless Chrome. Both the rendering and export paths have distinct failure modes.
Why This Happens
Plotly figures are JSON specifications that a JavaScript renderer converts to SVG in the browser. When you call fig.show(), Plotly either renders inline in Jupyter (via the notebook’s JavaScript engine) or opens a browser tab. Neither works in plain Python scripts without extra setup. Static image export requires Kaleido, which downloads a headless Chrome binary — on restricted networks or in minimal Docker images, this fails silently.
Subplots use a different API than single figures. Traces must explicitly specify which row and column they belong to, or they all stack in the first subplot.
Fix 1: Figure Not Showing in Jupyter
fig.show()
# Blank output or raw HTML stringJupyter Notebook / JupyterLab — most common cause is missing JupyterLab extension:
pip install plotly>=5.0
pip install "jupyterlab>=3" "ipywidgets>=7.6"JupyterLab 3+ auto-discovers Plotly. Earlier versions needed manual extension install:
# JupyterLab 2.x only
jupyter labextension install jupyterlab-plotlyRestart the JupyterLab server (not just the kernel) after installing. The browser frontend needs to reload.
Force a renderer explicitly:
import plotly.io as pio
# List available renderers
print(pio.renderers)
# ['notebook', 'jupyterlab', 'colab', 'browser', 'png', 'svg', 'iframe', ...]
# Set the default renderer
pio.renderers.default = "notebook" # Classic Jupyter
pio.renderers.default = "jupyterlab" # JupyterLab 3+
pio.renderers.default = "colab" # Google Colab
pio.renderers.default = "browser" # Opens default browser
pio.renderers.default = "iframe" # Saves to file and loads iframeIn VS Code notebooks — install the Jupyter extension, which includes Plotly support:
# In VS Code, search for "Jupyter" extension and install
# Then restart VS CodeFor Jupyter-specific display problems, see Jupyter not working.
Fix 2: Figure Not Showing in Scripts
Running a Python script with fig.show() opens the figure in a browser tab by default — but in headless environments (Docker, CI, SSH without X forwarding), this fails.
Solution 1: Save to HTML and open it manually:
import plotly.express as px
fig = px.scatter(df, x="x", y="y")
# Save as standalone HTML file
fig.write_html("plot.html")
# Open plot.html in a browser to viewSolution 2: Export to static image for servers/CI:
fig.write_image("plot.png")
fig.write_image("plot.pdf")
fig.write_image("plot.svg")This requires Kaleido (covered in Fix 3).
Solution 3: In Jupyter via fig.show("png") — renders a static PNG inline, which works even without the JavaScript extension:
fig.show("png") # Rasters the figure as PNG in the output cellPro Tip: When writing reusable functions that return figures, don’t call fig.show() inside them. Return the figure object and let the caller decide how to display it. This makes the same code work in Jupyter (where the caller can inline-render), in scripts (where they can save to HTML), and in Dash apps (where they assign figure=fig to dcc.Graph).
Fix 3: Kaleido Installation and Image Export
ValueError: Image export using the "kaleido" engine requires the kaleido packageKaleido is Plotly’s static export engine — it uses a bundled headless Chrome to render figures to PNG/PDF/SVG.
Install Kaleido:
pip install -U kaleidoBasic export:
import plotly.express as px
fig = px.scatter(df, x="x", y="y")
fig.write_image("plot.png", width=1200, height=800, scale=2)
# scale=2 doubles the resolution (like Retina)Format options:
fig.write_image("plot.png") # Raster PNG
fig.write_image("plot.jpg") # Raster JPEG
fig.write_image("plot.svg") # Vector SVG
fig.write_image("plot.pdf") # Vector PDF
fig.write_image("plot.eps") # Vector EPS
# Export with specific dimensions and resolution
fig.write_image(
"plot.png",
width=1920, # Pixels
height=1080,
scale=2, # DPI multiplier
engine="kaleido",
)Kaleido hangs or crashes — common on Linux/Docker when Chrome dependencies are missing:
# Debian/Ubuntu — install Chrome dependencies
sudo apt update && sudo apt install -y \
libgbm1 \
libnss3 \
libxkbcommon0 \
libxcomposite1 \
libxdamage1 \
libxrandr2 \
libgtk-3-0 \
libasound2 \
libx11-xcb1
# Docker — add to Dockerfile
RUN apt-get update && apt-get install -y \
libnss3 libatk-bridge2.0-0 libxkbcommon0 libgbm1 \
libxcomposite1 libxdamage1 libxrandr2 libgtk-3-0 libasound2 \
&& rm -rf /var/lib/apt/lists/*Timeout errors on large figures:
# Kaleido has a 30-second default timeout; raise it for complex figures
import plotly.io as pio
pio.kaleido.scope.default_width = 1920
pio.kaleido.scope.default_height = 1080
# For v0.2+ of Kaleido
fig.write_image("plot.png", engine="kaleido", format="png")Alternative: use Orca (the older engine) if Kaleido fails:
conda install -c plotly plotly-orcafig.write_image("plot.png", engine="orca")Orca is deprecated but still works. Prefer Kaleido for new code.
Fix 4: Subplot Traces Go to the Wrong Panel
from plotly.subplots import make_subplots
import plotly.graph_objects as go
fig = make_subplots(rows=1, cols=2)
fig.add_trace(go.Scatter(x=[1, 2, 3], y=[1, 4, 2], name="A"))
fig.add_trace(go.Scatter(x=[1, 2, 3], y=[3, 1, 4], name="B"))
# Both traces render in the first subplot!add_trace requires explicit row/column arguments when the figure has subplots:
fig = make_subplots(rows=1, cols=2, subplot_titles=("Left Panel", "Right Panel"))
fig.add_trace(
go.Scatter(x=[1, 2, 3], y=[1, 4, 2], name="A"),
row=1, col=1, # Explicit placement
)
fig.add_trace(
go.Scatter(x=[1, 2, 3], y=[3, 1, 4], name="B"),
row=1, col=2, # Different panel
)
fig.update_layout(height=400, title_text="Two Panels")
fig.show()Shared axes across subplots:
fig = make_subplots(
rows=2, cols=2,
shared_xaxes=True,
shared_yaxes=True,
vertical_spacing=0.1,
horizontal_spacing=0.1,
subplot_titles=("Q1", "Q2", "Q3", "Q4"),
)Mixed plot types in subplots:
fig = make_subplots(
rows=1, cols=2,
specs=[[{"type": "scatter"}, {"type": "pie"}]],
)
fig.add_trace(go.Scatter(x=[1, 2, 3], y=[1, 4, 2]), row=1, col=1)
fig.add_trace(go.Pie(values=[30, 50, 20], labels=["A", "B", "C"]), row=1, col=2)3D and specialized subplots need matching specs:
fig = make_subplots(
rows=1, cols=2,
specs=[[{"type": "scene"}, {"type": "scatter"}]], # 3D on left
)
fig.add_trace(
go.Scatter3d(x=[1,2,3], y=[1,2,3], z=[1,2,3]),
row=1, col=1,
)Updating subplot axis properties:
# Each subplot has its own axis — reference by position
fig.update_xaxes(title_text="Time", row=1, col=1)
fig.update_yaxes(title_text="Value", row=1, col=1)
fig.update_xaxes(type="log", row=1, col=2) # Log scale on right panel onlyFix 5: update_layout vs update_traces
These do different things — confusing them causes “why isn’t my change showing up” issues.
update_layout — modifies figure-level properties (title, axes, legend, size):
fig.update_layout(
title="My Chart",
title_x=0.5, # Center title
xaxis_title="X Axis",
yaxis_title="Y Axis",
width=1000,
height=600,
showlegend=True,
legend=dict(x=0.01, y=0.99, bgcolor="rgba(255,255,255,0.8)"),
margin=dict(l=50, r=50, t=80, b=50),
paper_bgcolor="white",
plot_bgcolor="rgba(240,240,240,0.5)",
font=dict(family="Arial", size=14),
)update_traces — modifies trace-level properties (line color, marker size, hover text):
fig.update_traces(
marker=dict(size=10, color="steelblue", line=dict(width=1, color="DarkSlateGrey")),
selector=dict(type="scatter"), # Only apply to Scatter traces
)
fig.update_traces(
line=dict(width=3),
selector=dict(name="Series A"), # Only for the trace named "Series A"
)Selectors let you target specific traces:
# By type
fig.update_traces(marker_size=12, selector=dict(type="scatter"))
# By name
fig.update_traces(line_color="red", selector=dict(name="Predicted"))
# By mode
fig.update_traces(marker_symbol="diamond", selector=dict(mode="markers"))Common Mistake: Using update_layout to change line colors or marker sizes. Those are trace properties — update_layout silently does nothing for them. Likewise, using update_traces to change the figure title doesn’t work — title is a layout property.
Fix 6: Plotly Express vs Graph Objects
# Plotly Express — high-level, DataFrame-oriented
import plotly.express as px
fig = px.scatter(df, x="total_bill", y="tip", color="smoker")
# Graph Objects — low-level, manual construction
import plotly.graph_objects as go
fig = go.Figure()
fig.add_trace(go.Scatter(x=df["total_bill"], y=df["tip"], mode="markers"))When to use which:
| Use Plotly Express | Use Graph Objects |
|---|---|
| Quick exploration | Complex custom layouts |
| Single DataFrame → standard chart | Combining multiple unrelated traces |
Need faceting (facet_col, facet_row) | Need precise control over every trace |
| Want nice defaults | Need mixed plot types |
Converting Express figures to Graph Objects (Express figures are Graph Objects — you can modify them):
import plotly.express as px
# Create with Express
fig = px.scatter(df, x="x", y="y", color="category")
# Modify with Graph Objects methods
fig.add_hline(y=0, line_dash="dash", line_color="gray")
fig.update_traces(marker=dict(size=12, line=dict(width=2)))
fig.update_layout(template="plotly_dark")Add reference lines and shapes:
import plotly.graph_objects as go
fig = px.scatter(df, x="x", y="y")
# Horizontal line
fig.add_hline(y=0, line_dash="dash", line_color="red", annotation_text="Baseline")
# Vertical line
fig.add_vline(x=5, line_color="blue")
# Rectangle region
fig.add_vrect(x0=2, x1=4, fillcolor="LightGreen", opacity=0.3, layer="below", line_width=0)
# Annotation
fig.add_annotation(
x=5, y=10,
text="Peak",
showarrow=True,
arrowhead=2,
ax=30, ay=-30,
)Fix 7: Hover Text, Tooltips, and Formatting
import plotly.express as px
fig = px.scatter(
df,
x="total_bill",
y="tip",
hover_data=["day", "time", "size"], # Add these columns to hover
hover_name="name", # Bold title in hover
labels={"total_bill": "Bill ($)", "tip": "Tip ($)"}, # Rename labels
)Custom hover templates:
import plotly.graph_objects as go
fig = go.Figure(data=go.Scatter(
x=df["x"],
y=df["y"],
mode="markers",
marker=dict(size=10, color=df["value"], colorscale="Viridis", showscale=True),
customdata=df[["category", "region"]],
hovertemplate=(
"<b>%{customdata[0]}</b><br>"
"Region: %{customdata[1]}<br>"
"X: %{x:.2f}<br>"
"Y: %{y:.2f}<br>"
"<extra></extra>" # Remove the trace name box
),
))Format numbers, dates, percentages:
fig.update_layout(
xaxis_tickformat=",", # 1,000 instead of 1000
yaxis_tickformat=".1%", # 0.25 → 25.0%
xaxis=dict(tickformat="%Y-%m-%d"), # Date format
)Fix 8: Exporting for Different Contexts
Standalone HTML — shareable file that works anywhere:
fig.write_html(
"plot.html",
include_plotlyjs="cdn", # Load Plotly.js from CDN (smaller file)
# include_plotlyjs=True, # Bundle Plotly.js (~3MB but offline-capable)
# include_plotlyjs="directory", # Separate .js file for reuse
full_html=True, # Complete HTML document
auto_open=False,
)Embed in a web page:
# Just the <div> — paste into an existing HTML page
html_fragment = fig.to_html(
include_plotlyjs=False, # Assume Plotly.js is loaded elsewhere
full_html=False, # Only the div, no <html> wrapper
div_id="my-plot-123",
)JSON export for web API responses (Plotly.js consumes this directly):
import json
# To JSON string
plot_json = fig.to_json()
# To dict for FastAPI/Flask JSON response
plot_dict = fig.to_dict()
return {"plot": plot_dict} # Frontend passes to Plotly.newPlot()Still Not Working?
Dash Integration
When embedding Plotly in a Dash web app, the figure goes into a dcc.Graph component:
from dash import Dash, dcc, html
import plotly.express as px
app = Dash(__name__)
fig = px.scatter(df, x="x", y="y")
app.layout = html.Div([
dcc.Graph(id="scatter", figure=fig)
])For Dash-specific callback and layout issues, see Dash not working.
Streamlit Integration
Streamlit displays Plotly figures with st.plotly_chart:
import streamlit as st
import plotly.express as px
fig = px.scatter(df, x="x", y="y")
st.plotly_chart(fig, use_container_width=True)For Streamlit-specific issues around figure caching and component state, see Streamlit not working.
Pandas DataFrame Integration
Plotly Express accepts a DataFrame directly and treats columns by name. Long-format DataFrames are preferred (like Seaborn):
fig = px.line(df_long, x="date", y="value", color="category", facet_col="region")For pandas data manipulation and reshape patterns needed before plotting, see pandas SettingWithCopyWarning and Seaborn not working for similar long-format requirements.
Performance with Large Datasets
Plotly gets slow above ~50k points per trace. Strategies:
- WebGL traces for scatter and line:
go.Scatterglinstead ofgo.Scatter(10x+ faster) - Downsample before plotting:
df.sample(10000)for exploration - Aggregate: plot summary statistics instead of raw points for very large data
# WebGL scatter — handles 100k+ points smoothly
import plotly.graph_objects as go
fig = go.Figure(data=go.Scattergl( # Note: Scattergl, not Scatter
x=df["x"],
y=df["y"],
mode="markers",
marker=dict(size=3, opacity=0.5),
))Theme and Template System
Plotly includes several built-in templates. Apply globally or per-figure:
import plotly.io as pio
# Global default
pio.templates.default = "plotly_dark" # or "plotly_white", "ggplot2", "seaborn", "simple_white"
# Per-figure override
fig.update_layout(template="plotly_white")
# List available templates
print(pio.templates)Build a custom template once and reuse across reports:
import plotly.graph_objects as go
pio.templates["company_brand"] = go.layout.Template(
layout=dict(
font=dict(family="Inter, Arial", size=13, color="#333"),
colorway=["#0066CC", "#FF6633", "#00AA66", "#FFAA00"],
paper_bgcolor="white",
plot_bgcolor="#F8F9FA",
)
)
pio.templates.default = "company_brand"Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.
Was this article helpful?
Related Articles
Fix: Dash Not Working — Callback Errors, Pattern Matching, and State Management
How to fix Dash errors — circular dependency in callbacks, pattern matching callback not firing, missing attribute clientside_callback, DataTable filtering not working, clientside JavaScript errors, Input Output State confusion, and async callback delays.
Fix: Matplotlib Not Working — Plots Not Showing, Blank Output, and Figure Layout Problems
How to fix Matplotlib errors — plot not displaying, blank figure, RuntimeError main thread not in main loop, tight_layout UserWarning, overlapping subplots, savefig saving blank image, backend errors, and figure/axes confusion.
Fix: Seaborn Not Working — FutureWarning, FacetGrid Errors, and Figure-Level Confusion
How to fix Seaborn errors — FutureWarning use of palette without hue, figure-level vs axes-level function confusion, FacetGrid layout issues, tight_layout with seaborn, seaborn 0.13 breaking changes, and ci parameter deprecated.
Fix: Apache Airflow Not Working — DAG Not Found, Task Failures, and Scheduler Issues
How to fix Apache Airflow errors — DAG not appearing in UI, ImportError preventing DAG load, task stuck in running or queued, scheduler not scheduling, XCom too large, connection not found, and database migration errors.