Skip to content

Commit

Permalink
Merge pull request #65 from holoviz-topics/eeg-responsive-sizing
Browse files Browse the repository at this point in the history
Add responsive sizing and better spacing to eeg viewer plots
  • Loading branch information
droumis authored Jul 25, 2023
2 parents 4d98aa5 + d6d478f commit b29301a
Show file tree
Hide file tree
Showing 2 changed files with 236 additions and 1,352 deletions.
219 changes: 219 additions & 0 deletions workflows/eeg-viewer/dev/230725_responsive-sizing_better-spacing.ipynb
Original file line number Diff line number Diff line change
@@ -0,0 +1,219 @@
{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# EEG Viewer\n",
"![status](https://img.shields.io/badge/status-in%20progress-orange)\n",
"\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"<div style=\"text-align: center;\">\n",
" <img src=\"./assets/230524_eeg-viewer.png\" alt=\"eeg viewer preview\" width=\"450\"/>\n",
"</div>"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Summary\n",
"\n",
"This workflow is intended to demonstrate the visualization of a set of 1D EEG timeseries with HoloViz and Bokeh tools.\n",
"\n",
"For details specific to this workflow, such as goals, specifications, and bottlenecks, please see this workflow's [readme](./readme_eeg-viewer.md).\n",
"\n",
"For a summary of EEG research, data, and software, please see [neuro/wiki/EEG-notes](https://github.com/holoviz-topics/neuro/wiki/EEG-notes)."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Imports and config"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"<div class=\"admonition alert alert-info\">\n",
" <p class=\"admonition-title\" style=\"font-weight:bold\">Requirements</p>\n",
" <p>This workflow notebook requires the <a href=\"./environment.yml\">environment</a> specified in this workflow directory.</p>\n",
"</div>\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"import numpy as np\n",
"import holoviews as hv; hv.extension('bokeh')\n",
"from holoviews.plotting.links import RangeToolLink\n",
"from neurodatagen.eeg import generate_eeg_powerlaw\n",
"from scipy.stats import zscore\n",
"import panel as pn; pn.extension()"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Generate simulated data\n",
"\n",
"The `generate_eeg_powerlaw` function synthesizes EEG data as high-pass filtered pink noise power law time series by default. The function returns a 2D numpy array of synthetic EEG data (in microvolts) shaped as (number of channels, total samples), a 1D time array (in seconds), and a list of channel names. Parameters such as the high-pass filter factor (in Hz) and an amplitude scaling factor allow customization of the generated data."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"n_channels = 25\n",
"n_seconds = 30\n",
"fs = 512\n",
"\n",
"data, time, channels = generate_eeg_powerlaw(n_channels, n_seconds, fs)\n",
"\n",
"print(f'shape: {data.shape} (n_channels, samples) ')\n",
"data"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Visualize synthetic EEG data with minimap"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# Set vertical spacing for EEG traces to avoid visual overlap\n",
"spacing = 1.2\n",
"offset = np.max(np.abs(data)) * spacing\n",
"\n",
"# Create a hv.Curve element per chan.\n",
"# Note: alternative is to call hv.Path once on offset-adjusted data, but \n",
"# then we couldn't independently apply formating to the channels (which \n",
"# we aren't doing yet, but we likely will in the future)\n",
"channel_curves = []\n",
"for i, channel_data in enumerate(data):\n",
" channel_curves.append(\n",
" hv.Curve((time, channel_data + (i * offset)), \"Time\").opts(\n",
" color=\"black\", line_width=1, tools=[\"hover\"], shared_axes=False))\n",
"\n",
"# Create mapping from yaxis location to ytick for each channel\n",
"# so we can have categorical-style labeling on a continuous axis.\n",
"# Note: this would/should change when we implement independent \n",
"# coordinates.\n",
"yticks = [(i * offset, ich) for i, ich in enumerate(channels)]\n",
"\n",
"# create a hook to set a predefined selection of RangeToolLink\n",
"max_ch_disp = 10\n",
"max_t_disp = 5\n",
"range_opts = []\n",
"\n",
"def xrange_hook(plot, element):\n",
" plot.handles['x_range'].end = max_t_disp\n",
"\n",
"def yrange_hook(plot, element):\n",
" plot.handles['y_range'].end = np.max(data[max_ch_disp-1,:] + ((max_ch_disp-1) * offset))\n",
" \n",
"if time.max() > max_t_disp:\n",
" range_opts.append(xrange_hook)\n",
"\n",
"if len(channel_curves) > max_ch_disp:\n",
" range_opts.append(yrange_hook)\n",
"\n",
"# Create an overlay of curves\n",
"eeg_viewer = hv.Overlay(channel_curves, kdims=\"Channel\").opts(\n",
" padding=0, xlabel=\"Time (s)\", ylabel=\"Channel\", #default_tools=['hover', 'pan', 'box_zoom', 'save', 'reset'],\n",
" yticks=yticks, show_legend=False, hooks=range_opts, aspect=1.5, responsive=True,\n",
" shared_axes=False)\n",
"\n",
"# Get the y positions of the yticks to use as yaxis of minimap image\n",
"y_positions, _ = zip(*yticks)\n",
"\n",
"# Compute z-scores across time for each channel\n",
"z_data = zscore(data, axis=1)\n",
"\n",
"# Generate the zscored image for the minimap using the y tick positions from the eeg_viewer\n",
"minimap = hv.Image((time, y_positions, z_data), [\"Time (s)\", \"Channel\"], \"Amplitude (uV)\")\n",
"\n",
"# Style the minimap \n",
"minimap = minimap.opts(\n",
" cmap=\"RdBu_r\", colorbar=False, xlabel='', alpha=.5, yticks=[yticks[0], yticks[-1]],\n",
" height=100, responsive=True, default_tools=[], shared_axes=False)\n",
"\n",
"# Create RangeToolLink between the minimap and the main EEG viewer \n",
"# (quirk: apply to just one eeg trace and it will apply to all. see HoloViews #4472)\n",
"RangeToolLink(minimap, list(eeg_viewer.values())[0], axes=[\"x\", \"y\"])\n",
"\n",
"# Display vertically\n",
"# layout = (eeg_viewer + minimap).cols(1).opts(shared_axes=False, merge_tools=False)\n",
"# eeg_app = pn.Row(layout).servable() # too much spacing between plots in served app\n",
"eeg_app = pn.Column(pn.Row(eeg_viewer, min_height=500), minimap).servable() # BUG Panel #5315: rangetool is variably active in the bokeh toolbar on eeg viewer plot.. not respecting shared_axes=False\n",
"eeg_app"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n",
"<div class=\"alert alert-info\">\n",
" <p class=\"admonition-title\" style=\"font-weight:bold\">Tip:</p>\n",
"Hover near any border of the minimap, then click and drag to adjust the visible bounds in the EEG plot\n",
"\n",
"</div>"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Load and plot real data"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": []
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3 (ipykernel)",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.9.16"
}
},
"nbformat": 4,
"nbformat_minor": 4
}
Loading

0 comments on commit b29301a

Please sign in to comment.