-
Notifications
You must be signed in to change notification settings - Fork 6
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #65 from holoviz-topics/eeg-responsive-sizing
Add responsive sizing and better spacing to eeg viewer plots
- Loading branch information
Showing
2 changed files
with
236 additions
and
1,352 deletions.
There are no files selected for viewing
219 changes: 219 additions & 0 deletions
219
workflows/eeg-viewer/dev/230725_responsive-sizing_better-spacing.ipynb
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
Oops, something went wrong.