iEEG Visualization Toolkit

Four modules for interactive visualization of iEEG recorded from a 2D electrode grid.


channel_grid.py — ChannelGrid

Pure selection logic. No display code. Knows the grid shape and computes which channels are selected based on the current mode.

Modes

  • row — all channels in one AP row

  • column — all channels in one ML column

  • sparse — strided subgrid covering the full extent (stride=2 on 16×16 → 64 evenly spaced channels)

  • neighborhood — filled Chebyshev square around a center electrode (radius=2 → 5×5)

  • manual — individual cell toggling

Key outputs

  • grid.selectedfrozenset of (ap, ml) int tuples. Watch this downstream.

  • grid.flat_indiceslist[int], row-major: ap * n_ml + ml

  • grid.as_array(n_ap, n_ml) bool mask

grid = ChannelGrid(n_ap=16, n_ml=16)
grid.select_row(3)
grid.select_sparse(stride=2, offset=0)
grid.select_neighborhood(ap=8, ml=8, radius=2)
grid.toggle_manual(3, 5)

grid.param.watch(lambda e: print(e.new), "selected")

Coordinate convention: all indices are integers 0..N-1. Normalize physical AP/ML coords first with normalize_coords_to_index().


channel_grid_widget.py — ChannelGridWidget

Bokeh heatmap of the electrode grid plus Panel mode controls. Wraps a ChannelGrid — clicking cells updates the grid, grid state changes redraw the widget.

Optionally accepts a cell_values array (e.g. per-channel RMS) to show signal amplitude as background brightness on unselected cells, and an atlas_image for anatomical context.

rms = sig.std(dim="time").transpose("AP", "ML").values   # (n_ap, n_ml)
w   = ChannelGridWidget.from_grid(grid, cell_values=rms)
w.panel()   # returns pn.Column
w.grid      # the ChannelGrid inside

multichannel_viewer.py — MultichannelViewer

Stacked trace viewer. Takes numpy directly — no xarray, no grid awareness. Call show_channels() to update which channels are displayed; the plot updates in place without rebuilding.

Uses a fixed-size hv.NdOverlay internally: always max_channels slots, inactive ones are empty with alpha=0. This prevents HoloViews from leaving ghost traces when the selection changes.

viewer = MultichannelViewer(sig_z, t_vals, ch_labels, max_channels=32)
viewer.show_channels([0, 1, 2, 3])
viewer.panel().servable()

ieeg_viewer.py — ieeg_viewer / IEEGViewer

Thin integration layer. Accepts an xarray DataArray, handles z-scoring and Dask materialisation, builds a MultichannelViewer, and optionally wires a ChannelGrid so selection changes drive the viewer. Includes an Apply button to batch rapid selection changes into a single render.

# Standalone
viewer = ieeg_viewer(sig_tc)
viewer.panel().servable()

# Grid-wired
viewer = ieeg_viewer(sig_tc, channel_grid=grid, n_ml=16)
pn.Row(w.panel(), viewer.panel()).servable()

Full example

import panel as pn
from cogpy.plot.hv.xarray_hv import normalize_coords_to_index
from cogpy.datasets.tensor import example_ieeg
from cogpy.plot.hv.channel_grid import ChannelGrid
from cogpy.plot.hv.channel_grid_widget import ChannelGridWidget
from cogpy.plot.hv.ieeg_viewer import ieeg_viewer

pn.extension("bokeh")

sig      = example_ieeg()                                      # (time, ML, AP)
sig_norm = normalize_coords_to_index(sig, ("AP", "ML"))
sig_tc   = sig_norm.transpose("time", "AP", "ML").stack(channel=("AP", "ML"))
n_ap, n_ml = sig_norm.sizes["AP"], sig_norm.sizes["ML"]

rms  = sig_norm.std(dim="time").transpose("AP", "ML").values
grid = ChannelGrid(n_ap=n_ap, n_ml=n_ml)
w    = ChannelGridWidget.from_grid(grid, cell_values=rms)

viewer = ieeg_viewer(sig_tc, channel_grid=grid, n_ml=n_ml, initial_window_s=5)

pn.Row(w.panel(), viewer.panel()).servable()

Demo apps (servable entrypoints)

These are thin “glue” apps intended for GUI development and manual testing.

iEEG grid + traces

import panel as pn
from cogpy.plot.hv.ieeg_toolkit import ieeg_toolkit_app

pn.extension("bokeh")
ieeg_toolkit_app(mode="small", seed=0).servable()

4D spectrogram + bursts navigator

import panel as pn
from cogpy.plot._legacy.spectrogram_bursts_app import spectrogram_bursts_app

pn.extension()
spectrogram_bursts_app(mode="small", seed=0, kind="toy").servable()

Future directions

  • Signal-driven selection — top-N by variance in the current window, or most correlated with a seed channel

  • Sorting — reorder selected channels by AP, ML, variance, or correlation

  • Atlas placement — proper asymmetric AP extent for atlas_mode="full"

  • Bad channel detection — flag high-kurtosis electrodes on the grid widget

  • Linked views — shared time cursor between trace viewer, spectrogram, and AP×ML topomap

  • Lazy loading — materialise only the visible time window for long recordings


Atlas overlay (typed)

ChannelGridWidget supports passing an AtlasImageOverlay (image + extents) so the placement metadata travels with the image.

import numpy as np
from PIL import Image

from cogpy.datasets.schemas import AtlasImageOverlay
from cogpy.plot.hv.channel_grid import ChannelGrid
from cogpy.plot.hv.channel_grid_widget import ChannelGridWidget

atlas = np.array(Image.open(\"docs/assets/atlas/dorsal-cortex.png\").convert(\"RGBA\"), dtype=np.uint8)
overlay = AtlasImageOverlay(
    image=atlas,
    ap_extent=(-4.0, 1.0),
    ml_extent=(-4.0, 4.0),
    bl_distance=7.5,
)

grid = ChannelGrid(n_ap=16, n_ml=16)
w = ChannelGridWidget.from_grid(
    grid,
    ap_coords=np.linspace(-4, 1, 16),
    ml_coords=np.linspace(-4, 4, 16),
    atlas_overlay=overlay,
)
w.panel().servable()