Tutorial 5: Interactive Visualization with Plotly#

This tutorial demonstrates how to create interactive visualizations using the braintools.visualize module with Plotly backend. We’ll cover:

  1. Interactive spike raster plots with zoom/pan capabilities

  2. Interactive line plots with hover information

  3. Interactive heatmaps for connectivity analysis

  4. Interactive 3D scatter plots for high-dimensional data

  5. Interactive network visualizations

  6. Building comprehensive neural activity dashboards

  7. Export options for interactive plots

Interactive plots provide several advantages over static plots:

  • Zoom and Pan: Explore data at different scales

  • Hover Information: Get detailed information on data points

  • Dynamic Filtering: Show/hide data series

  • 3D Exploration: Rotate and examine 3D data from all angles

  • Export Options: Save as HTML, PNG, PDF, or SVG

Let’s start by importing the necessary modules.

# Core imports
import numpy as np

# Import braintools interactive visualization functions
import braintools

# Check if plotly is available
try:
    import plotly.graph_objects as go
    import plotly.express as px
    from plotly.subplots import make_subplots
    import plotly.offline as pyo

    # Configure plotly for Jupyter
    pyo.init_notebook_mode(connected=True)
    print("✓ Plotly is available for interactive visualizations")
except ImportError:
    print("⚠️  Plotly not available. Install with: pip install plotly")
    print("   Interactive features will not work without Plotly.")

# Set random seed for reproducible results
np.random.seed(42)
⚠️  Plotly not available. Install with: pip install plotly
   Interactive features will not work without Plotly.

1. Generate Synthetic Neural Data#

First, let’s create realistic synthetic neural data that we’ll use throughout this tutorial.

def generate_spike_data(n_neurons=50, duration=10.0, base_rate=5.0, burst_prob=0.1):
    """
    Generate realistic spike data with occasional bursts.
    
    Parameters:
    - n_neurons: Number of neurons
    - duration: Recording duration in seconds
    - base_rate: Base firing rate in Hz
    - burst_prob: Probability of burst events
    """
    spike_times = []
    neuron_ids = []

    for neuron_id in range(n_neurons):
        # Generate base Poisson spikes
        n_spikes = np.random.poisson(base_rate * duration)
        times = np.sort(np.random.uniform(0, duration, n_spikes))

        # Add occasional bursts
        for t in times:
            if np.random.random() < burst_prob:
                # Add 2-5 additional spikes in a 50ms window
                burst_size = np.random.randint(2, 6)
                burst_times = t + np.random.exponential(0.01, burst_size)
                burst_times = burst_times[burst_times < duration]
                times = np.concatenate([times, burst_times])

        times = np.sort(times)
        spike_times.extend(times)
        neuron_ids.extend([neuron_id] * len(times))

    return np.array(spike_times), np.array(neuron_ids)


def generate_lfp_data(duration=10.0, sampling_rate=1000.0, n_channels=8):
    """
    Generate synthetic Local Field Potential (LFP) data.
    """
    n_samples = int(duration * sampling_rate)
    time = np.linspace(0, duration, n_samples)

    lfp_data = np.zeros((n_samples, n_channels))

    for ch in range(n_channels):
        # Mix different frequency components
        theta = 0.5 * np.sin(2 * np.pi * 8 * time + ch * 0.3)  # 8 Hz theta
        alpha = 0.3 * np.sin(2 * np.pi * 12 * time + ch * 0.2)  # 12 Hz alpha
        beta = 0.2 * np.sin(2 * np.pi * 25 * time + ch * 0.1)  # 25 Hz beta
        gamma = 0.1 * np.sin(2 * np.pi * 60 * time + ch * 0.05)  # 60 Hz gamma

        # Add noise
        noise = 0.1 * np.random.randn(n_samples)

        lfp_data[:, ch] = theta + alpha + beta + gamma + noise

    return time, lfp_data


def generate_connectivity_matrix(n_nodes=20, connection_prob=0.3, weight_std=1.0):
    """
    Generate a random connectivity matrix with realistic properties.
    """
    # Start with random connections
    conn_matrix = np.random.binomial(1, connection_prob, (n_nodes, n_nodes))

    # Remove self-connections
    np.fill_diagonal(conn_matrix, 0)

    # Add weights
    weights = np.random.normal(0, weight_std, (n_nodes, n_nodes))
    conn_matrix = conn_matrix * weights

    return conn_matrix


# Generate data for the tutorial
print("Generating synthetic neural data...")

# Spike data
spike_times, neuron_ids = generate_spike_data(n_neurons=30, duration=5.0)
print(f"✓ Generated {len(spike_times)} spikes from {len(np.unique(neuron_ids))} neurons")

# LFP data
time_lfp, lfp_signals = generate_lfp_data(duration=5.0, n_channels=6)
print(f"✓ Generated LFP data: {lfp_signals.shape[0]} samples × {lfp_signals.shape[1]} channels")

# Connectivity data
connectivity = generate_connectivity_matrix(n_nodes=15)
print(f"✓ Generated connectivity matrix: {connectivity.shape}")

# High-dimensional data for 3D plotting
n_points = 200
high_dim_data = np.random.multivariate_normal(
    mean=[0, 0, 0],
    cov=[[2, 0.5, 0.2], [0.5, 1.5, 0.3], [0.2, 0.3, 1.0]],
    size=n_points
)
print(f"✓ Generated 3D scatter data: {high_dim_data.shape}")

print("\nData generation complete! 🎉")
Generating synthetic neural data...
✓ Generated 1040 spikes from 30 neurons
✓ Generated LFP data: 5000 samples × 6 channels
✓ Generated connectivity matrix: (15, 15)
✓ Generated 3D scatter data: (200, 3)

Data generation complete! 🎉

2. Interactive Spike Raster Plots#

Spike raster plots show when individual neurons fire. Interactive versions allow you to:

  • Zoom into specific time windows

  • Pan across the recording

  • Hover over spikes to see exact timing

  • Color-code by neuron or time

# Basic interactive spike raster plot
print("Creating basic interactive spike raster plot...")
fig1 = braintools.visualize.interactive_spike_raster(
    spike_times=spike_times,
    neuron_ids=neuron_ids,
    title="Interactive Spike Raster Plot - Zoom and Pan Enabled",
    width=900,
    height=500
)

# Show the plot
fig1.show()

print("\n📝 Interactive Features:")
print("   • Zoom: Draw a box to zoom into a region")
print("   • Pan: Click and drag to move around")
print("   • Hover: Move mouse over spikes for details")
print("   • Reset: Double-click to return to original view")
Creating basic interactive spike raster plot...
---------------------------------------------------------------------------
ImportError                               Traceback (most recent call last)
Cell In[3], line 3
      1 # Basic interactive spike raster plot
      2 print("Creating basic interactive spike raster plot...")
----> 3 fig1 = braintools.visualize.interactive_spike_raster(
      4     spike_times=spike_times,
      5     neuron_ids=neuron_ids,
      6     title="Interactive Spike Raster Plot - Zoom and Pan Enabled",
      7     width=900,
      8     height=500
      9 )
     11 # Show the plot
     12 fig1.show()

File D:\codes\projects\braintools\braintools\visualize\interactive.py:92, in interactive_spike_raster(spike_times, neuron_ids, time_range, neuron_range, color_by, title, width, height, **kwargs)
     54 def interactive_spike_raster(
     55     spike_times: Union[np.ndarray, List],
     56     neuron_ids: Optional[Union[np.ndarray, List]] = None,
   (...)
     63     **kwargs
     64 ):
     65     """
     66     Create interactive spike raster plot using Plotly.
     67     
   (...)
     90         Interactive plotly figure.
     91     """
---> 92     _check_plotly()
     94     # Convert to numpy arrays
     95     spike_times = as_numpy(spike_times)

File D:\codes\projects\braintools\braintools\visualize\interactive.py:51, in _check_plotly()
     49 """Check if plotly is available."""
     50 if not PLOTLY_AVAILABLE:
---> 51     raise ImportError("Plotly is required for interactive plots. Install with: pip install plotly")

ImportError: Plotly is required for interactive plots. Install with: pip install plotly
# Spike raster with color coding by neuron
print("Creating color-coded spike raster plot...")
fig2 = braintools.visualize.interactive_spike_raster(
    spike_times=spike_times,
    neuron_ids=neuron_ids,
    color_by='neuron',
    title="Spike Raster Plot - Colored by Neuron ID",
    width=900,
    height=500
)

fig2.show()

print("\n🎨 Color coding helps identify:")
print("   • Individual neuron firing patterns")
print("   • Synchronized activity across neurons")
print("   • Bursting behavior of specific neurons")
Creating color-coded spike raster plot...
🎨 Color coding helps identify:
   • Individual neuron firing patterns
   • Synchronized activity across neurons
   • Bursting behavior of specific neurons
# Spike raster with time-based coloring and filtering
print("Creating time-filtered spike raster plot...")

# Focus on a specific time window and neuron range
fig3 = braintools.visualize.interactive_spike_raster(
    spike_times=spike_times,
    neuron_ids=neuron_ids,
    time_range=(1.0, 4.0),  # Focus on 1-4 second window
    neuron_range=(5, 25),  # Focus on neurons 5-25
    color_by='time',
    title="Filtered Spike Raster - Time Range: 1-4s, Neurons: 5-25",
    width=900,
    height=400
)

fig3.show()

print("\n🔍 Filtering capabilities:")
print("   • Time range filtering for focused analysis")
print("   • Neuron range filtering to examine subpopulations")
print("   • Temporal color coding shows spike timing patterns")
Creating time-filtered spike raster plot...
🔍 Filtering capabilities:
   • Time range filtering for focused analysis
   • Neuron range filtering to examine subpopulations
   • Temporal color coding shows spike timing patterns

3. Interactive Line Plots with Hover Information#

Line plots are perfect for continuous signals like LFP, membrane potentials, or population activity. Interactive versions provide detailed information on hover and allow for easy comparison of multiple signals.

# Single channel LFP with hover information
print("Creating interactive LFP line plot...")

fig4 = braintools.visualize.interactive_line_plot(
    x=time_lfp,
    y=lfp_signals[:, 0],  # First channel only
    title="Interactive LFP Signal - Channel 1",
    xlabel="Time (s)",
    ylabel="Amplitude (mV)",
    width=900,
    height=400
)

fig4.show()

print("\n📊 Hover to see exact values at each time point!")
Creating interactive LFP line plot...
📊 Hover to see exact values at each time point!
# Multi-channel LFP comparison
print("Creating multi-channel LFP comparison...")

# Select a subset of channels for clarity
selected_channels = [0, 1, 2, 3]
lfp_subset = [lfp_signals[:, ch] for ch in selected_channels]
channel_labels = [f'Channel {ch + 1}' for ch in selected_channels]

fig5 = braintools.visualize.interactive_line_plot(
    x=time_lfp,
    y=lfp_subset,
    labels=channel_labels,
    title="Multi-Channel LFP Comparison",
    xlabel="Time (s)",
    ylabel="Amplitude (mV)",
    width=900,
    height=500
)

fig5.show()

print("\n🔗 Multi-trace features:")
print("   • Click legend items to show/hide traces")
print("   • Double-click legend to isolate a single trace")
print("   • Hover shows synchronized values across all traces")
Creating multi-channel LFP comparison...
🔗 Multi-trace features:
   • Click legend items to show/hide traces
   • Double-click legend to isolate a single trace
   • Hover shows synchronized values across all traces
# Population activity with spike count overlay
print("Creating population activity analysis...")

# Calculate population firing rate over time
bin_size = 0.05  # 50ms bins
time_bins = np.arange(0, 5.0, bin_size)
spike_counts, _ = np.histogram(spike_times, bins=time_bins)
firing_rate = spike_counts / (bin_size * len(np.unique(neuron_ids)))  # Hz
bin_centers = time_bins[:-1] + bin_size / 2

# Smooth the firing rate
from scipy.ndimage import gaussian_filter1d

firing_rate_smooth = gaussian_filter1d(firing_rate, sigma=2)

fig6 = braintools.visualize.interactive_line_plot(
    x=bin_centers,
    y=[firing_rate, firing_rate_smooth],
    labels=['Raw Population Rate', 'Smoothed Population Rate'],
    title="Population Firing Rate Over Time",
    xlabel="Time (s)",
    ylabel="Firing Rate (Hz)",
    width=900,
    height=400
)

fig6.show()

print("\n📈 Population activity insights:")
print(f"   • Mean firing rate: {np.mean(firing_rate):.2f} Hz")
print(f"   • Max firing rate: {np.max(firing_rate):.2f} Hz")
print(f"   • Rate variability: {np.std(firing_rate):.2f} Hz")
Creating population activity analysis...
📈 Population activity insights:
   • Mean firing rate: 6.94 Hz
   • Max firing rate: 17.33 Hz
   • Rate variability: 2.74 Hz

4. Interactive Heatmaps for Connectivity Analysis#

Heatmaps are excellent for visualizing matrices like connectivity, correlation, or cross-correlation data. Interactive versions allow detailed exploration of matrix elements.

# Basic connectivity heatmap
print("Creating interactive connectivity heatmap...")

# Create labels for nodes
node_labels = [f'N{i:02d}' for i in range(connectivity.shape[0])]

fig7 = braintools.visualize.interactive_heatmap(
    data=connectivity,
    x_labels=node_labels,
    y_labels=node_labels,
    title="Neural Connectivity Matrix",
    colorscale='RdBu_r',  # Red-Blue colorscale, reversed
    width=700,
    height=600
)

fig7.show()

print("\n🔗 Connectivity analysis:")
print(f"   • Matrix size: {connectivity.shape}")
print(f"   • Connection density: {np.mean(connectivity != 0):.1%}")
print(f"   • Weight range: [{connectivity.min():.2f}, {connectivity.max():.2f}]")
print("   • Hover over cells to see exact connection weights")
Creating interactive connectivity heatmap...
🔗 Connectivity analysis:
   • Matrix size: (15, 15)
   • Connection density: 25.3%
   • Weight range: [-2.64, 2.83]
   • Hover over cells to see exact connection weights
# Cross-correlation heatmap
print("Creating cross-correlation heatmap...")

# Calculate cross-correlation between LFP channels
n_channels = lfp_signals.shape[1]
cross_corr = np.corrcoef(lfp_signals.T)

channel_names = [f'Ch{i + 1}' for i in range(n_channels)]

fig8 = braintools.visualize.interactive_heatmap(
    data=cross_corr,
    x_labels=channel_names,
    y_labels=channel_names,
    title="LFP Channel Cross-Correlation Matrix",
    colorscale='RdBu_r',
    width=600,
    height=500
)

# Add custom colorbar range
fig8.update_traces(zmin=-1, zmax=1)

fig8.show()

print("\n📊 Cross-correlation insights:")
print(f"   • Diagonal elements are always 1.0 (self-correlation)")
print(
    f"   • Off-diagonal range: [{cross_corr[np.triu_indices_from(cross_corr, k=1)].min():.3f}, {cross_corr[np.triu_indices_from(cross_corr, k=1)].max():.3f}]")
print("   • Red = positive correlation, Blue = negative correlation")
Creating cross-correlation heatmap...
📊 Cross-correlation insights:
   • Diagonal elements are always 1.0 (self-correlation)
   • Off-diagonal range: [0.283, 0.922]
   • Red = positive correlation, Blue = negative correlation
# Time-resolved correlation heatmap
print("Creating time-resolved correlation heatmap...")

# Calculate sliding window correlation
window_size = 500  # samples
step_size = 100  # samples
n_windows = (len(time_lfp) - window_size) // step_size

# Focus on first two channels for simplicity
time_corr = np.zeros(n_windows)
window_times = np.zeros(n_windows)

for i in range(n_windows):
    start_idx = i * step_size
    end_idx = start_idx + window_size

    # Calculate correlation in this window
    corr = np.corrcoef(lfp_signals[start_idx:end_idx, 0],
                       lfp_signals[start_idx:end_idx, 1])[0, 1]
    time_corr[i] = corr
    window_times[i] = time_lfp[start_idx + window_size // 2]

# Create a 2D array for heatmap (correlation vs time)
corr_2d = time_corr.reshape(1, -1)

fig9 = braintools.visualize.interactive_heatmap(
    data=corr_2d,
    x_labels=[f'{t:.2f}s' for t in window_times[::5]],  # Every 5th time point for clarity
    y_labels=['Ch1-Ch2 Correlation'],
    title="Time-Resolved Cross-Correlation (Ch1 vs Ch2)",
    colorscale='RdBu_r',
    width=900,
    height=200
)

fig9.show()

print(f"\n⏱️  Time-resolved analysis:")
print(f"   • Window size: {window_size / 1000:.1f}s")
print(f"   • Step size: {step_size / 1000:.1f}s")
print(f"   • Correlation range: [{time_corr.min():.3f}, {time_corr.max():.3f}]")
print("   • Shows how correlation changes over time")
Creating time-resolved correlation heatmap...
⏱️  Time-resolved analysis:
   • Window size: 0.5s
   • Step size: 0.1s
   • Correlation range: [0.915, 0.929]
   • Shows how correlation changes over time

5. Interactive 3D Scatter Plots for High-Dimensional Data#

3D scatter plots are perfect for visualizing high-dimensional neural data, such as:

  • Principal component analysis (PCA) results

  • Neural state spaces

  • Dimensionality reduction visualizations

# Basic 3D scatter plot
print("Creating basic 3D scatter plot...")

fig10 = braintools.visualize.interactive_3d_scatter(
    x=high_dim_data[:, 0],
    y=high_dim_data[:, 1],
    z=high_dim_data[:, 2],
    title="3D Neural State Space",
    width=800,
    height=600
)

fig10.show()

print("\n🎮 3D Interaction controls:")
print("   • Rotate: Click and drag")
print("   • Zoom: Mouse wheel or zoom box")
print("   • Pan: Shift + click and drag")
print("   • Reset: Double-click")
Creating basic 3D scatter plot...
🎮 3D Interaction controls:
   • Rotate: Click and drag
   • Zoom: Mouse wheel or zoom box
   • Pan: Shift + click and drag
   • Reset: Double-click
# 3D scatter with color coding and size variation
print("Creating enhanced 3D scatter plot...")

# Create color values based on distance from origin
distances = np.linalg.norm(high_dim_data, axis=1)

# Create size values based on another metric
sizes = 5 + 15 * (distances - distances.min()) / (distances.max() - distances.min())

# Create labels for hover
point_labels = [f'Point {i}: d={d:.2f}' for i, d in enumerate(distances)]

fig11 = braintools.visualize.interactive_3d_scatter(
    x=high_dim_data[:, 0],
    y=high_dim_data[:, 1],
    z=high_dim_data[:, 2],
    color=distances,
    size=sizes,
    labels=point_labels,
    title="Enhanced 3D Scatter - Color by Distance, Size by Metric",
    width=800,
    height=600
)

fig11.show()

print("\n🌈 Enhanced features:")
print("   • Color coding reveals data structure")
print("   • Variable point sizes add another dimension")
print("   • Hover labels provide detailed information")
print(f"   • Distance range: [{distances.min():.2f}, {distances.max():.2f}]")
Creating enhanced 3D scatter plot...
🌈 Enhanced features:
   • Color coding reveals data structure
   • Variable point sizes add another dimension
   • Hover labels provide detailed information
   • Distance range: [0.10, 5.37]
# PCA visualization of neural data
print("Creating PCA visualization...")

# Perform PCA on LFP data
from sklearn.decomposition import PCA
from sklearn.preprocessing import StandardScaler

# Standardize the LFP data
scaler = StandardScaler()
lfp_standardized = scaler.fit_transform(lfp_signals)

# Apply PCA
pca = PCA(n_components=3)
lfp_pca = pca.fit_transform(lfp_standardized)

# Color by time
time_colors = time_lfp

fig12 = braintools.visualize.interactive_3d_scatter(
    x=lfp_pca[:, 0],
    y=lfp_pca[:, 1],
    z=lfp_pca[:, 2],
    color=time_colors,
    title=f"PCA of LFP Data - Explained Variance: {pca.explained_variance_ratio_.sum():.1%}",
    width=800,
    height=600
)

fig12.show()

print("\n📊 PCA Analysis:")
print(f"   • PC1 variance explained: {pca.explained_variance_ratio_[0]:.1%}")
print(f"   • PC2 variance explained: {pca.explained_variance_ratio_[1]:.1%}")
print(f"   • PC3 variance explained: {pca.explained_variance_ratio_[2]:.1%}")
print(f"   • Total variance explained: {pca.explained_variance_ratio_.sum():.1%}")
print("   • Colors show temporal progression")
Creating PCA visualization...
📊 PCA Analysis:
   • PC1 variance explained: 79.7%
   • PC2 variance explained: 17.0%
   • PC3 variance explained: 1.0%
   • Total variance explained: 97.6%
   • Colors show temporal progression

6. Interactive Network Visualizations#

Network plots are ideal for showing connectivity patterns, functional networks, and graph-based neural data.

# Basic network visualization
print("Creating interactive network visualization...")

# Use a subset of the connectivity matrix for clarity
n_nodes = 10
small_connectivity = connectivity[:n_nodes, :n_nodes]

# Generate node labels and colors
node_labels = [f'N{i:02d}' for i in range(n_nodes)]
node_colors = np.sum(np.abs(small_connectivity), axis=1)  # Color by total connectivity

fig13 = braintools.visualize.interactive_network(
    adjacency=small_connectivity,
    node_labels=node_labels,
    node_colors=node_colors,
    title="Interactive Neural Network - Hover over nodes and edges",
    width=700,
    height=600
)

fig13.show()

print("\n🕸️  Network features:")
print("   • Nodes represent neurons or brain regions")
print("   • Edge thickness shows connection strength")
print("   • Node colors indicate connectivity degree")
print("   • Hover for detailed connection information")
Creating interactive network visualization...
🕸️  Network features:
   • Nodes represent neurons or brain regions
   • Edge thickness shows connection strength
   • Node colors indicate connectivity degree
   • Hover for detailed connection information
# Advanced network with custom positions
print("Creating network with spatial layout...")

# Create a more structured layout (e.g., representing brain regions)
# Arrange nodes in a brain-like structure
angles = np.linspace(0, 2 * np.pi, n_nodes, endpoint=False)
radii = np.random.uniform(0.5, 1.5, n_nodes)  # Vary distances from center

positions = np.column_stack([
    radii * np.cos(angles),
    radii * np.sin(angles)
])

# Calculate node metrics
in_degree = np.sum(small_connectivity != 0, axis=0)  # Number of inputs
out_degree = np.sum(small_connectivity != 0, axis=1)  # Number of outputs
total_degree = in_degree + out_degree

fig14 = braintools.visualize.interactive_network(
    adjacency=small_connectivity,
    positions=positions,
    node_labels=[f'Region {chr(65 + i)}' for i in range(n_nodes)],  # A, B, C, etc.
    node_colors=total_degree,
    title="Brain Network - Spatial Layout with Degree Centrality",
    width=700,
    height=600
)

fig14.show()

print("\n🧠 Network analysis:")
print(f"   • Number of nodes: {n_nodes}")
print(f"   • Number of edges: {np.sum(small_connectivity != 0)}")
print(f"   • Network density: {np.sum(small_connectivity != 0) / (n_nodes * (n_nodes - 1)):.2%}")
print(f"   • Degree range: [{total_degree.min()}, {total_degree.max()}]")
Creating network with spatial layout...
🧠 Network analysis:
   • Number of nodes: 10
   • Number of edges: 28
   • Network density: 31.11%
   • Degree range: [3, 8]

7. Comprehensive Neural Activity Dashboard#

Dashboards combine multiple visualizations into a single, comprehensive view. This is perfect for real-time monitoring or detailed analysis sessions.

# Create comprehensive dashboard
print("Creating comprehensive neural activity dashboard...")

# Prepare data for dashboard
# Convert spike times to list format (one array per neuron)
spike_lists = []
unique_neurons = np.unique(neuron_ids)
for neuron in unique_neurons:
    neuron_spikes = spike_times[neuron_ids == neuron]
    spike_lists.append(neuron_spikes)

# Calculate population activity
population_activity = firing_rate_smooth

# Create the dashboard
dashboard_fig = braintools.visualize.dashboard_neural_activity(
    spike_times=spike_lists,
    population_activity=population_activity,
    time=bin_centers,
    title="Neural Activity Dashboard - Complete Analysis",
    width=1200,
    height=800
)

dashboard_fig.show()

print("\n📊 Dashboard components:")
print("   • Top left: Spike raster plot")
print("   • Top right: Inter-spike interval (ISI) distribution")
print("   • Middle left: Population activity over time")
print("   • Middle right: Individual neuron firing rate distribution")
print("   • Bottom left: Spike count over time (binned)")
print("   • Bottom right: Summary statistics table")
print("\n🎯 Use this dashboard for:")
print("   • Quick overview of neural activity patterns")
print("   • Identifying population synchronization")
print("   • Detecting bursting or irregular firing")
print("   • Comparing activity across different conditions")
Creating comprehensive neural activity dashboard...
📊 Dashboard components:
   • Top left: Spike raster plot
   • Top right: Inter-spike interval (ISI) distribution
   • Middle left: Population activity over time
   • Middle right: Individual neuron firing rate distribution
   • Bottom left: Spike count over time (binned)
   • Bottom right: Summary statistics table

🎯 Use this dashboard for:
   • Quick overview of neural activity patterns
   • Identifying population synchronization
   • Detecting bursting or irregular firing
   • Comparing activity across different conditions

8. Export Options for Interactive Plots#

Interactive plots can be saved in various formats for different purposes:

  • HTML: Preserves full interactivity

  • PNG/JPG: Static high-quality images

  • PDF: Vector graphics for publications

  • SVG: Scalable vector graphics

# Demonstration of export options
print("Demonstrating export options...")

# Create a sample plot for export
sample_fig = braintools.visualize.interactive_line_plot(
    x=time_lfp[::10],  # Subsample for smaller file size
    y=[lfp_signals[::10, 0], lfp_signals[::10, 1]],
    labels=['Channel 1', 'Channel 2'],
    title="Sample Plot for Export Demonstration",
    xlabel="Time (s)",
    ylabel="Amplitude (mV)",
    width=800,
    height=400
)

# Show the plot
sample_fig.show()

print("\n💾 Export options:")
print("")
print("1. HTML (Interactive):")
print("   fig.write_html('neural_activity.html')")
print("   • Preserves all interactive features")
print("   • Can be opened in any web browser")
print("   • Perfect for sharing with collaborators")
print("")
print("2. Static Images:")
print("   fig.write_image('plot.png', width=1200, height=800)")
print("   fig.write_image('plot.pdf')  # Vector graphics")
print("   fig.write_image('plot.svg')  # Scalable vector")
print("   • High-quality static versions")
print("   • Perfect for publications and presentations")
print("")
print("3. Programmatic Access:")
print("   fig.to_dict()     # Convert to dictionary")
print("   fig.to_json()     # Convert to JSON")
print("   • For custom processing or storage")
print("")
print("Note: Install kaleido for image export: pip install kaleido")
Demonstrating export options...
💾 Export options:

1. HTML (Interactive):
   fig.write_html('neural_activity.html')
   • Preserves all interactive features
   • Can be opened in any web browser
   • Perfect for sharing with collaborators

2. Static Images:
   fig.write_image('plot.png', width=1200, height=800)
   fig.write_image('plot.pdf')  # Vector graphics
   fig.write_image('plot.svg')  # Scalable vector
   • High-quality static versions
   • Perfect for publications and presentations

3. Programmatic Access:
   fig.to_dict()     # Convert to dictionary
   fig.to_json()     # Convert to JSON
   • For custom processing or storage

Note: Install kaleido for image export: pip install kaleido
# Example: Save dashboard as HTML (uncomment to actually save)
print("Example: Saving interactive dashboard...")

# Uncomment the line below to save the dashboard
# dashboard_fig.write_html("neural_activity_dashboard.html")

print("\n📁 File saving example:")
print('dashboard_fig.write_html("neural_activity_dashboard.html")')
print("\nThis would create an HTML file that can be:")
print("   • Opened in any web browser")
print("   • Shared via email or cloud storage")
print("   • Embedded in websites or reports")
print("   • Used offline without internet connection")

# Show file size information

dashboard_size = len(dashboard_fig.to_json()) / 1024  # KB
print(f"\n📊 Dashboard data size: ~{dashboard_size:.1f} KB")
Example: Saving interactive dashboard...

📁 File saving example:
dashboard_fig.write_html("neural_activity_dashboard.html")

This would create an HTML file that can be:
   • Opened in any web browser
   • Shared via email or cloud storage
   • Embedded in websites or reports
   • Used offline without internet connection

📊 Dashboard data size: ~46.7 KB

9. Advanced Interactive Features#

Let’s explore some advanced interactive features that make data exploration even more powerful.

# Advanced: Interactive correlation matrix with statistics
print("Creating advanced interactive correlation matrix...")

fig15 = braintools.visualize.interactive_correlation_matrix(
    data=lfp_signals,
    labels=channel_names,
    method='pearson',
    title="Interactive LFP Correlation Matrix with Values",
    width=700,
    height=600
)

fig15.show()

print("\n🔢 Advanced correlation features:")
print("   • Values displayed directly on the heatmap")
print("   • Symmetric matrix with diagonal = 1")
print("   • Color scale from -1 (anti-correlated) to +1 (correlated)")
print("   • Interactive hover shows exact correlation values")
Creating advanced interactive correlation matrix...
🔢 Advanced correlation features:
   • Values displayed directly on the heatmap
   • Symmetric matrix with diagonal = 1
   • Color scale from -1 (anti-correlated) to +1 (correlated)
   • Interactive hover shows exact correlation values
# Advanced: Interactive surface plot
print("Creating interactive 3D surface plot...")

# Create a 2D surface representing neural activity over space and time
x_space = np.linspace(-5, 5, 30)
y_time = np.linspace(0, 5, 40)
X, Y = np.meshgrid(x_space, y_time)

# Simulate neural activity wave propagation
Z = np.sin(np.sqrt(X ** 2) - 2 * Y) * np.exp(-X ** 2 / 10) * np.exp(-Y / 5)

fig16 = braintools.visualize.interactive_surface(
    z=Z,
    x=x_space,
    y=y_time,
    colorscale='Viridis',
    title="Interactive 3D Surface - Neural Activity Wave",
    width=800,
    height=600
)

# Customize the layout
fig16.update_layout(
    scene=dict(
        xaxis_title="Space (mm)",
        yaxis_title="Time (s)",
        zaxis_title="Activity Level"
    )
)

fig16.show()

print("\n🌊 3D Surface features:")
print("   • Rotate to view from different angles")
print("   • Zoom to examine specific regions")
print("   • Hover to see exact coordinates and values")
print("   • Perfect for spatio-temporal neural data")
Creating interactive 3D surface plot...
🌊 3D Surface features:
   • Rotate to view from different angles
   • Zoom to examine specific regions
   • Hover to see exact coordinates and values
   • Perfect for spatio-temporal neural data
# Advanced: Interactive histogram comparison
print("Creating interactive histogram comparison...")

# Calculate different metrics from spike data
isi_data = []
spike_count_data = []

for neuron in unique_neurons:
    neuron_spikes = spike_times[neuron_ids == neuron]
    if len(neuron_spikes) > 1:
        isis = np.diff(neuron_spikes)
        isi_data.extend(isis)
        spike_count_data.append(len(neuron_spikes))

# Create firing rate data from spike counts
firing_rates = np.array(spike_count_data) / 5.0  # Convert to Hz (5-second recording)

fig17 = braintools.visualize.interactive_histogram(
    data=[isi_data, firing_rates],
    labels=['Inter-Spike Intervals', 'Firing Rates'],
    bins=25,
    opacity=0.7,
    title="Interactive Histogram Comparison - ISI vs Firing Rates",
    xlabel="Value",
    ylabel="Count",
    width=900,
    height=500
)

# Note: This creates overlaid histograms - you can toggle them on/off
fig17.show()

print("\n📊 Histogram comparison:")
print(f"   • ISI statistics: mean={np.mean(isi_data):.3f}s, std={np.std(isi_data):.3f}s")
print(f"   • Firing rate statistics: mean={np.mean(firing_rates):.2f}Hz, std={np.std(firing_rates):.2f}Hz")
print("   • Click legend items to show/hide distributions")
print("   • Overlaid histograms allow direct comparison")
Creating interactive histogram comparison...
📊 Histogram comparison:
   • ISI statistics: mean=0.137s, std=0.182s
   • Firing rate statistics: mean=6.93Hz, std=1.41Hz
   • Click legend items to show/hide distributions
   • Overlaid histograms allow direct comparison