Skip to content

Plotly Visualization Guide

Overview

Plotly is optional — a jaxfne addition for creating interactive HTML visualizations. Install it when needed for this feature:

pip install plotly

This guide describes standard practices for generating reproducible, portable HTML figures from jaxfne simulations.

Installation

Add to your environment:

pip install plotly>=5.0

Or include in a project requirements.txt:

jaxfne>=0.2.14
plotly>=5.0
numpy
scipy

Output directory structure

Use this standard layout for all runs:

outputs/
  <run_name>/
    manifest.json              # Full experiment metadata
    probe_report.json          # Probe status and contracts
    metrics.json               # Numerical results summary
    validation_report.json     # NaN/Inf/admissibility checks
    asset_hashes.json          # SHA256 hashes of all figures
    figures/
      lfp_proxy_trace.html
      csd_proxy_heatmap.html
      raster_depth.html
      spectrolaminar_summary.html

Create directories automatically:

from pathlib import Path
run_name = "myrun_20260520_1200"
figures_dir = Path(f"outputs/{run_name}/figures")
figures_dir.mkdir(parents=True, exist_ok=True)

Code examples

LFP-proxy trace (1D time series)

import plotly.graph_objects as go
from pathlib import Path

# Assume: lfp_proxy shape [T, C] (time × contacts)
lfp = readout["lfp_proxy"]
T, C = lfp.shape

fig = go.Figure()
for contact in range(C):
    fig.add_trace(go.Scatter(
        y=lfp[:, contact],
        mode="lines",
        name=f"Contact {contact}",
        opacity=0.7
    ))

fig.update_layout(
    title="LFP-proxy across laminar contacts",
    xaxis_title="Time (ms)",
    yaxis_title="Potential (proxy units)",
    template="plotly_white",
    height=500,
    width=1000
)

out = Path("outputs/myrun/figures")
out.mkdir(parents=True, exist_ok=True)
fig.write_html(
    out / "lfp_proxy_trace.html",
    include_plotlyjs="cdn",  # Use CDN for optimal file size
    full_html=True
)

CSD-proxy heatmap (2D space-time)

import plotly.graph_objects as go
import numpy as np

# Assume: csd_proxy shape [T, C] (time × contacts)
csd = readout["csd_proxy"]

fig = go.Figure(data=go.Heatmap(
    z=csd.T,  # Transpose for space on y-axis
    x=np.arange(csd.shape[0]),
    y=np.arange(csd.shape[1]),
    colorscale="RdBu",  # Red/blue for positive/negative
    colorbar=dict(title="CSD (proxy)")
))

fig.update_layout(
    title="CSD-proxy heatmap (laminar profile)",
    xaxis_title="Time (ms)",
    yaxis_title="Contact depth",
    template="plotly_white",
    height=500,
    width=1000
)

out.mkdir(parents=True, exist_ok=True)
fig.write_html(
    out / "csd_proxy_heatmap.html",
    include_plotlyjs="cdn",
    full_html=True
)

Raster plot (spikes by depth)

import plotly.graph_objects as go

# Assume: spikes shape [T, N] or spike times + cell IDs
spikes = readout["spikes"]  # [T, N]
T, N = spikes.shape

# Find spike times
spike_times = []
cell_ids = []
for n in range(N):
    spike_indices = np.where(spikes[:, n] > 0)[0]
    if len(spike_indices) > 0:
        spike_times.extend(spike_indices)
        cell_ids.extend([n] * len(spike_indices))

fig = go.Figure(data=go.Scatter(
    x=spike_times,
    y=cell_ids,
    mode="markers",
    marker=dict(size=4, color="black"),
    name="Spikes"
))

fig.update_layout(
    title="Spike raster",
    xaxis_title="Time (ms)",
    yaxis_title="Neuron ID",
    template="plotly_white",
    height=600,
    width=1200
)

fig.write_html(
    out / "raster_depth.html",
    include_plotlyjs="cdn",
    full_html=True
)

Spectrolaminar summary (multiple panels)

from plotly.subplots import make_subplots

# Create 2x2 subplot grid
fig = make_subplots(
    rows=2, cols=2,
    subplot_titles=("LFP power", "CSD profile", "Spike rate", "Source norm")
)

# Placeholder: add traces as needed
# fig.add_trace(go.Scatter(...), row=1, col=1)
# etc.

fig.update_layout(height=800, width=1200, title="Spectrolaminar summary")
fig.write_html(
    out / "spectrolaminar_summary.html",
    include_plotlyjs="cdn",
    full_html=True
)

Best practices

1. Use CDN for optimal file size

Always use include_plotlyjs="cdn":

fig.write_html(
    "figure.html",
    include_plotlyjs="cdn",  # ~10 KB file size
    full_html=True
)

Embedded Plotly library inflates files to 3–5 MB each. CDN-linked files stay ~10–100 KB and load the library once.

2. File size and performance

  • Per-figure size: 10–200 KB (with CDN)
  • Heatmaps: Can be large if data is very high-dimensional; consider decimation
  • Raster plots: Sparse data (many empty time points) may benefit from downsampling

3. Declarative metadata

Store figure provenance in asset_hashes.json:

import hashlib
import json

def file_sha256(path):
    """Compute SHA256 hash of file."""
    hash_obj = hashlib.sha256()
    with open(path, "rb") as f:
        for chunk in iter(lambda: f.read(4096), b""):
            hash_obj.update(chunk)
    return hash_obj.hexdigest()

# After all figures are written:
figures_dir = Path("outputs/myrun/figures")
asset_hashes = {
    f.name: file_sha256(f)
    for f in figures_dir.glob("*.html")
}

with open(Path("outputs/myrun") / "asset_hashes.json", "w") as fp:
    json.dump(asset_hashes, fp, indent=2)

Artifact hygiene

Exclude generated HTML from version control

Add to .gitignore:

outputs/
site/

Generated HTML figures, build artifacts, and site/ from mkdocs build should never be committed.

Do commit manifests and metadata

Keep version control of: - manifest.json (experiment metadata) - probe_report.json (operator contracts) - metrics.json (numerical summaries) - validation_report.json (NaN/Inf checks) - asset_hashes.json (reproducibility checksums)

These are small (KB scale) and document the run without storing large arrays.

Output bundle example

import json
from jaxfne.io import json_safe

# After simulation and probing:
outputs = {
    "manifest": model.manifest(signals, readout),
    "probe_report": readout,  # Already JSON-safe
    "metrics": {
        "spike_rate_mean": float(spike_count / duration_ms),
        "lfp_power_mean": float(lfp.std(axis=0).mean()),
    },
    "validation": {
        "lfp_finite": bool(np.isfinite(lfp).all()),
        "csd_finite": bool(np.isfinite(csd).all()),
    }
}

# Write manifests (keep)
out_dir = Path(f"outputs/{run_name}")
out_dir.mkdir(parents=True, exist_ok=True)
with open(out_dir / "manifest.json", "w") as f:
    json.dump(json_safe(outputs["manifest"]), f, indent=2, allow_nan=False)
with open(out_dir / "metrics.json", "w") as f:
    json.dump(json_safe(outputs["metrics"]), f, indent=2, allow_nan=False)

# Write figures (don't commit)
figs_dir = out_dir / "figures"
figs_dir.mkdir(exist_ok=True)
fig.write_html(figs_dir / "lfp_proxy_trace.html", include_plotlyjs="cdn", full_html=True)

Common mistakes

1. Embedded Plotly library

INEFFICIENT:

fig.write_html("figure.html", include_plotlyjs=True)  # ~3 MB file

RECOMMENDED:

fig.write_html("figure.html", include_plotlyjs="cdn")  # ~100 KB

2. Correctly labeling proxy-scale readouts

INCORRECT:

# Avoid stating physical amplitude:
fig.update_layout(title="Real LFP recorded from V1 cortex")  # Use proxy-scale label

CORRECT:

fig.update_layout(title="LFP-proxy from proxy field operator (proxy-scale units)")

3. Version control for outputs/

AVOID:

git add outputs/myrun/figures/*.html  # Exclude generated figures
git push

CORRECT:

git add outputs/myrun/manifest.json outputs/myrun/asset_hashes.json  # Metadata only
git push

4. Ensure directories exist

INCOMPLETE:

fig.write_html("outputs/myrun/figures/trace.html")  # Missing directory check

CORRECT:

Path("outputs/myrun/figures").mkdir(parents=True, exist_ok=True)
fig.write_html("outputs/myrun/figures/trace.html")

See also


Status: v0.2.14
Last updated: 2026-05-20
Plotly: Optional jaxfne addition for interactive visualizations