SciAgent-Skills plotly-interactive-visualization

Interactive visualization with Plotly. 40+ chart types (scatter, line, bar, heatmap, 3D, statistical, geographic) with hover, zoom, and pan. Use for exploratory analysis, dashboards, and presentations. Two APIs: Plotly Express (quick, DataFrame-oriented) and Graph Objects (fine-grained control). For static publication figures use matplotlib; for statistical grammar use seaborn.

install
source · Clone the upstream repo
git clone https://github.com/jaechang-hits/SciAgent-Skills
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/jaechang-hits/SciAgent-Skills "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data-visualization/plotly-interactive-visualization" ~/.claude/skills/jaechang-hits-sciagent-skills-plotly-interactive-visualization && rm -rf "$T"
manifest: skills/data-visualization/plotly-interactive-visualization/SKILL.md
source content

Plotly — Interactive Scientific Visualization

Overview

Plotly is a Python graphing library for interactive, web-embeddable visualizations with 40+ chart types. It provides two APIs: Plotly Express (high-level, pandas-native) for quick plots and Graph Objects (low-level) for full customization. Output to interactive HTML, static PNG/PDF/SVG, or Dash web apps.

When to Use

  • Creating interactive charts with hover tooltips, zoom, and pan
  • Building multi-panel exploratory dashboards for data analysis
  • Visualizing 3D data (surfaces, scatter3d, mesh, volume)
  • Making geographic/map visualizations (choropleth, scatter_geo)
  • Presenting data in web-embeddable HTML format
  • Statistical distribution comparison (violin, box, histogram with marginals)
  • Time series with range sliders and animation frames
  • For static publication-quality figures (journal submissions), use
    matplotlib
    instead
  • For statistical grammar-of-graphics style, use
    seaborn
    instead

Prerequisites

  • Python packages:
    plotly
    ,
    pandas
    ,
    numpy
  • For static export:
    kaleido
    (PNG/PDF/SVG rendering)
  • For web apps:
    dash
    (optional)
pip install plotly kaleido

Quick Start

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

# Sample data
np.random.seed(42)
df = pd.DataFrame({
    "x": np.random.randn(200),
    "y": np.random.randn(200),
    "group": np.random.choice(["A", "B", "C"], 200),
    "size": np.random.uniform(5, 20, 200),
})

fig = px.scatter(df, x="x", y="y", color="group", size="size",
                 title="Interactive Scatter Plot", hover_data=["group"])
fig.write_html("scatter.html")
fig.write_image("scatter.png", width=800, height=500, scale=2)
print("Saved scatter.html and scatter.png")

Core API

1. Plotly Express (High-Level API)

Quick, one-line charts from pandas DataFrames. Returns

go.Figure
objects that can be further customized.

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

np.random.seed(42)
df = pd.DataFrame({
    "temperature": np.linspace(20, 80, 50),
    "yield": 50 + 0.8 * np.linspace(20, 80, 50) + np.random.randn(50) * 5,
    "catalyst": np.random.choice(["Pd", "Pt", "Rh"], 50),
})

# Scatter with trendline
fig = px.scatter(df, x="temperature", y="yield", color="catalyst",
                 trendline="ols", title="Temperature vs Yield")
fig.write_image("scatter_trend.png", width=700, height=450)
print("Saved scatter_trend.png")

# Bar chart
summary = df.groupby("catalyst")["yield"].mean().reset_index()
fig = px.bar(summary, x="catalyst", y="yield", color="catalyst",
             title="Mean Yield by Catalyst")
fig.write_image("bar_catalyst.png", width=600, height=400)
print("Saved bar_catalyst.png")
# Heatmap from correlation matrix
import plotly.express as px
import pandas as pd
import numpy as np

np.random.seed(42)
data = pd.DataFrame(np.random.randn(100, 5), columns=["Gene_A", "Gene_B", "Gene_C", "Gene_D", "Gene_E"])
corr = data.corr()

fig = px.imshow(corr, text_auto=".2f", color_continuous_scale="RdBu_r",
                zmin=-1, zmax=1, title="Gene Expression Correlation")
fig.write_image("heatmap.png", width=600, height=500)
print("Saved heatmap.png")

2. Graph Objects (Low-Level API)

Full control over individual traces, layouts, and annotations.

import plotly.graph_objects as go
import numpy as np

# 3D surface plot
x = np.linspace(-5, 5, 50)
y = np.linspace(-5, 5, 50)
X, Y = np.meshgrid(x, y)
Z = np.sin(np.sqrt(X**2 + Y**2))

fig = go.Figure(data=[go.Surface(z=Z, x=X[0], y=y, colorscale="Viridis")])
fig.update_layout(title="3D Surface Plot",
                  scene=dict(xaxis_title="X", yaxis_title="Y", zaxis_title="Z"))
fig.write_image("surface_3d.png", width=700, height=500)
print("Saved surface_3d.png")
# Multi-trace figure with custom styling
import plotly.graph_objects as go
import numpy as np

np.random.seed(42)
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="blue", width=2)))
fig.add_trace(go.Scatter(x=x, y=np.cos(x), mode="lines", name="cos(x)",
                         line=dict(color="red", width=2, dash="dash")))
fig.add_hline(y=0, line_dash="dot", line_color="gray", opacity=0.5)
fig.add_annotation(x=np.pi/2, y=1, text="sin peak", showarrow=True, arrowhead=2)

fig.update_layout(template="plotly_white", title="Trigonometric Functions",
                  xaxis_title="x", yaxis_title="f(x)")
fig.write_image("multi_trace.png", width=700, height=400)
print("Saved multi_trace.png")

3. Subplots and Multi-Panel Layouts

Create figure grids with shared or independent axes.

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

np.random.seed(42)
data = np.random.randn(500)

fig = make_subplots(
    rows=2, cols=2,
    subplot_titles=("Histogram", "Box Plot", "Scatter", "Violin"),
    specs=[[{"type": "histogram"}, {"type": "box"}],
           [{"type": "scatter"}, {"type": "violin"}]],
)

fig.add_trace(go.Histogram(x=data, nbinsx=30, name="Hist"), row=1, col=1)
fig.add_trace(go.Box(y=data, name="Box"), row=1, col=2)
fig.add_trace(go.Scatter(x=data[:100], y=data[100:200], mode="markers", name="Scatter"), row=2, col=1)
fig.add_trace(go.Violin(y=data, name="Violin", box_visible=True), row=2, col=2)

fig.update_layout(height=700, width=800, title_text="Multi-Panel Dashboard", showlegend=False)
fig.write_image("subplots.png", width=800, height=700)
print("Saved subplots.png")

4. Statistical Charts

Distribution comparison, error bars, and statistical annotations.

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

np.random.seed(42)
df = pd.DataFrame({
    "value": np.concatenate([np.random.normal(0, 1, 100), np.random.normal(2, 1.5, 100)]),
    "group": ["Control"] * 100 + ["Treatment"] * 100,
})

# Histogram with marginal box plot
fig = px.histogram(df, x="value", color="group", marginal="box",
                   nbins=30, barmode="overlay", opacity=0.7,
                   title="Distribution Comparison")
fig.write_image("stat_hist.png", width=700, height=450)
print("Saved stat_hist.png")

# Violin plot with individual points
fig = px.violin(df, x="group", y="value", box=True, points="all",
                title="Treatment Effect (Violin + Points)")
fig.write_image("violin.png", width=500, height=450)
print("Saved violin.png")
# Error bars
import plotly.graph_objects as go
import numpy as np

conditions = ["Control", "Low Dose", "Med Dose", "High Dose"]
means = [5.2, 7.1, 9.8, 11.3]
sems = [0.4, 0.6, 0.5, 0.8]

fig = go.Figure(data=[go.Bar(
    x=conditions, y=means,
    error_y=dict(type="data", array=sems, visible=True),
    marker_color=["#636EFA", "#EF553B", "#00CC96", "#AB63FA"],
)])
fig.update_layout(title="Dose Response (mean ± SEM)", yaxis_title="Response",
                  template="plotly_white")
fig.write_image("error_bars.png", width=600, height=400)
print("Saved error_bars.png")

5. Export and Rendering

Save to interactive HTML, static images, or embed in notebooks.

import plotly.express as px
import pandas as pd

df = px.data.iris()
fig = px.scatter(df, x="sepal_width", y="sepal_length", color="species")

# Interactive HTML (full standalone)
fig.write_html("interactive.html")
# HTML with CDN (smaller file, needs internet)
fig.write_html("interactive_cdn.html", include_plotlyjs="cdn")

# Static images (requires kaleido)
fig.write_image("plot.png", width=800, height=500, scale=2)  # 2x resolution
fig.write_image("plot.pdf")  # Vector PDF
fig.write_image("plot.svg")  # Vector SVG

# Get image as bytes (for embedding)
img_bytes = fig.to_image(format="png", width=600, height=400)
print(f"PNG bytes: {len(img_bytes)}")

6. Interactivity Features

Customize hover, animations, buttons, and range sliders.

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

# Custom hover template
np.random.seed(42)
df = pd.DataFrame({
    "date": pd.date_range("2024-01-01", periods=100),
    "price": 100 + np.cumsum(np.random.randn(100) * 2),
    "volume": np.random.randint(1000, 5000, 100),
})

fig = px.line(df, x="date", y="price", title="Stock Price",
              hover_data={"volume": True, "price": ":.2f"})
fig.update_traces(hovertemplate="<b>%{x|%Y-%m-%d}</b><br>Price: $%{y:.2f}<br>Volume: %{customdata[0]:,}<extra></extra>")
fig.update_xaxes(rangeslider_visible=True)
fig.write_html("timeseries.html")
print("Saved timeseries.html with range slider")

Common Workflows

Workflow 1: Exploratory Data Analysis Dashboard

Goal: Create a multi-panel interactive dashboard for dataset exploration.

import plotly.express as px
import plotly.graph_objects as go
from plotly.subplots import make_subplots
import pandas as pd
import numpy as np

# Sample dataset
np.random.seed(42)
n = 300
df = pd.DataFrame({
    "gene_expression": np.random.lognormal(2, 1, n),
    "protein_level": np.random.lognormal(1.5, 0.8, n),
    "cell_type": np.random.choice(["Neuron", "Astrocyte", "Microglia"], n),
    "treatment": np.random.choice(["Control", "Drug_A", "Drug_B"], n),
    "viability": np.random.uniform(0.3, 1.0, n),
})

fig = make_subplots(rows=2, cols=2,
                    subplot_titles=("Expression vs Protein", "Expression by Cell Type",
                                    "Viability by Treatment", "Expression Distribution"))

# Panel 1: Scatter
for ct in df["cell_type"].unique():
    sub = df[df["cell_type"] == ct]
    fig.add_trace(go.Scatter(x=sub["gene_expression"], y=sub["protein_level"],
                             mode="markers", name=ct, opacity=0.6), row=1, col=1)

# Panel 2: Box
for ct in df["cell_type"].unique():
    fig.add_trace(go.Box(y=df[df["cell_type"]==ct]["gene_expression"],
                         name=ct, showlegend=False), row=1, col=2)

# Panel 3: Violin
for tx in df["treatment"].unique():
    fig.add_trace(go.Violin(y=df[df["treatment"]==tx]["viability"],
                            name=tx, showlegend=False, box_visible=True), row=2, col=1)

# Panel 4: Histogram
fig.add_trace(go.Histogram(x=df["gene_expression"], nbinsx=30,
                           name="Expression", showlegend=False), row=2, col=2)

fig.update_layout(height=800, width=1000, title="Exploratory Data Analysis")
fig.write_html("eda_dashboard.html")
fig.write_image("eda_dashboard.png", width=1000, height=800)
print("Saved eda_dashboard.html and eda_dashboard.png")

Workflow 2: Publication Figure with Annotations

Goal: Create a polished, annotated figure suitable for supplementary materials or presentations.

import plotly.graph_objects as go
import numpy as np

np.random.seed(42)
x = np.linspace(0, 24, 100)
control = 50 + 10 * np.sin(x * np.pi / 12) + np.random.randn(100) * 3
treatment = 70 + 15 * np.sin(x * np.pi / 12 + 0.5) + np.random.randn(100) * 4

fig = go.Figure()
fig.add_trace(go.Scatter(x=x, y=control, mode="lines", name="Control",
                         line=dict(color="#636EFA", width=2)))
fig.add_trace(go.Scatter(x=x, y=treatment, mode="lines", name="Treatment",
                         line=dict(color="#EF553B", width=2)))

# Add shaded region for treatment window
fig.add_vrect(x0=6, x1=18, fillcolor="yellow", opacity=0.1, line_width=0,
              annotation_text="Treatment Window", annotation_position="top left")

# Add annotation at peak difference
fig.add_annotation(x=12, y=85, text="Peak difference<br>p < 0.001",
                   showarrow=True, arrowhead=2, font=dict(size=11))

fig.update_layout(
    template="plotly_white",
    title="Circadian Response to Treatment",
    xaxis_title="Time (hours)", yaxis_title="Response (AU)",
    font=dict(family="Arial", size=12),
    legend=dict(x=0.02, y=0.98),
    width=700, height=450,
)
fig.write_image("publication_figure.png", width=700, height=450, scale=3)
print("Saved publication_figure.png (3x resolution)")

Key Parameters

ParameterModuleDefaultRange / OptionsEffect
template
update_layout
"plotly"
"plotly_white"
,
"plotly_dark"
,
"ggplot2"
,
"seaborn"
,
"simple_white"
Global figure styling theme
color_continuous_scale
px
/
go
varies
"Viridis"
,
"Plasma"
,
"RdBu"
,
"RdBu_r"
,
"Blues"
Color scale for continuous data
barmode
px.histogram
"relative"
"group"
,
"overlay"
,
"relative"
,
"stack"
How multiple histograms are arranged
trendline
px.scatter
None
None
,
"ols"
,
"lowess"
,
"expanding"
Regression line overlay
marginal
px.scatter/histogram
None
None
,
"rug"
,
"box"
,
"violin"
,
"histogram"
Marginal distribution display
scale
write_image
1
1
5
Image resolution multiplier (2=retina, 3=print)
include_plotlyjs
write_html
True
True
,
"cdn"
,
"directory"
,
False
How Plotly.js is bundled in HTML
opacity
most trace types
1.0
0.0
1.0
Trace transparency for overlapping data

Best Practices

  1. Start with Plotly Express, customize with Graph Objects:

    px
    functions return
    go.Figure
    objects, so you can always add Graph Objects methods after. Don't start with
    go
    unless
    px
    genuinely cannot express what you need.

    fig = px.scatter(df, x="x", y="y", color="group")
    fig.update_layout(template="plotly_white")  # go method on px figure
    fig.add_hline(y=threshold)
    
  2. Use

    plotly_white
    template for publication figures: The default
    plotly
    template has a gray background that looks unprofessional in papers.
    plotly_white
    or
    simple_white
    gives a clean look.

  3. Always set explicit width/height for static export: Without dimensions,

    write_image
    uses the default viewport size which may not match your target (journal column width, slide dimensions).

  4. Use

    scale=2
    or higher for print-quality images: Default
    scale=1
    produces 72 DPI equivalent. For publications, use
    scale=3
    (216 DPI effective).

  5. Prefer HTML export for interactive data sharing: HTML files are self-contained and can be opened in any browser without Python. Use

    include_plotlyjs="cdn"
    to reduce file size.

Common Recipes

Recipe: Correlation Matrix Heatmap

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

np.random.seed(42)
df = pd.DataFrame(np.random.randn(100, 6), columns=[f"Var_{i}" for i in range(6)])
corr = df.corr()

# Mask upper triangle
mask = np.triu(np.ones_like(corr, dtype=bool), k=1)
corr_masked = corr.where(~mask)

fig = px.imshow(corr_masked, text_auto=".2f", color_continuous_scale="RdBu_r",
                zmin=-1, zmax=1, title="Correlation Matrix")
fig.write_image("correlation.png", width=600, height=500, scale=2)
print("Saved correlation.png")

Recipe: Animated Scatter Plot

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

np.random.seed(42)
frames = []
for year in range(2010, 2025):
    n = 50
    frames.append(pd.DataFrame({
        "x": np.random.randn(n) * (year - 2009),
        "y": np.random.randn(n) * (year - 2009),
        "size": np.random.uniform(5, 20, n),
        "year": year,
    }))
df = pd.concat(frames)

fig = px.scatter(df, x="x", y="y", size="size", animation_frame="year",
                 range_x=[-30, 30], range_y=[-30, 30], title="Animated Scatter")
fig.write_html("animated.html")
print("Saved animated.html")

Recipe: Geographic Choropleth Map

import plotly.express as px

# Built-in gapminder dataset
df = px.data.gapminder().query("year == 2007")
fig = px.choropleth(df, locations="iso_alpha", color="gdpPercap",
                    hover_name="country", color_continuous_scale="Plasma",
                    title="GDP per Capita (2007)")
fig.write_image("choropleth.png", width=900, height=500, scale=2)
print("Saved choropleth.png")

Troubleshooting

ProblemCauseSolution
write_image
fails with
ValueError
kaleido
not installed
pip install kaleido
Blank/white image from
write_image
Plotly version mismatch with kaleidoUpdate both:
pip install --upgrade plotly kaleido
HTML file very large (>10 MB)Full Plotly.js bundledUse
fig.write_html(path, include_plotlyjs="cdn")
Hover data not showingColumn not in DataFrame or wrong nameCheck
hover_data
parameter matches DataFrame columns exactly
Subplot traces appear in wrong panelIncorrect
row
/
col
in
add_trace
Verify
row=
and
col=
match your
make_subplots
grid (1-indexed)
Colors don't match between px and goDifferent default color sequencesSet explicitly:
fig.update_layout(colorway=px.colors.qualitative.Plotly)
Animation slow/choppyToo many points per frameReduce data points or use
px.scatter
with
render_mode="webgl"
for large datasets

Related Skills

  • matplotlib — static, publication-quality figures for journal submissions (more control over typography and layout)
  • seaborn — statistical visualization with grammar-of-graphics approach (built on matplotlib)
  • scientific-visualization — general principles of scientific figure design and color theory

References