Skip to content

Commit

Permalink
Implement support for subcoordinate systems in the y-axis (#5840)
Browse files Browse the repository at this point in the history
Co-authored-by: Simon Høxbro Hansen <[email protected]>
Co-authored-by: maximlt <[email protected]>
Co-authored-by: Demetris Roumis <[email protected]>
  • Loading branch information
4 people committed Sep 26, 2023
1 parent 9d208fb commit 38e0b39
Show file tree
Hide file tree
Showing 4 changed files with 515 additions and 19 deletions.
172 changes: 172 additions & 0 deletions examples/gallery/demos/bokeh/eeg_viewer.ipynb
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "549b47a4",
"metadata": {},
"source": [
"This example demonstrates advanced visualization techniques using HoloViews with the Bokeh plotting backend. You'll learn how to:\n",
"\n",
"1. Display multiple timeseries in a single plot using `subcoordinate_y`.\n",
"2. Create and link a minimap to the main plot with `RangeToolLink`.\n",
"\n",
"Specifically, we'll simulate [Electroencephalography](https://en.wikipedia.org/wiki/Electroencephalography) (EEG) data, plot it, and then create a minimap based on the [z-score](https://en.wikipedia.org/wiki/Standard_score) of the data for easier navigation."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "8109537b-5fba-4f07-aba4-91a56f7e95c7",
"metadata": {},
"outputs": [],
"source": [
"import numpy as np\n",
"import holoviews as hv\n",
"from bokeh.models import HoverTool\n",
"from holoviews.plotting.links import RangeToolLink\n",
"from scipy.stats import zscore\n",
"\n",
"hv.extension('bokeh')"
]
},
{
"cell_type": "markdown",
"id": "1c95f241-2314-42b0-b6cb-2c0baf332686",
"metadata": {},
"source": [
"## Generating EEG data\n",
"\n",
"Let's start by simulating some EEG data. We'll create a timeseries for each channel using sine waves with varying frequencies."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "5f4a9dbe",
"metadata": {},
"outputs": [],
"source": [
"\n",
"N_CHANNELS = 10\n",
"N_SECONDS = 5\n",
"SAMPLING_RATE = 200\n",
"INIT_FREQ = 2 # Initial frequency in Hz\n",
"FREQ_INC = 5 # Frequency increment\n",
"AMPLITUDE = 1\n",
"\n",
"# Generate time and channel labels\n",
"total_samples = N_SECONDS * SAMPLING_RATE\n",
"time = np.linspace(0, N_SECONDS, total_samples)\n",
"channels = [f'EEG {i}' for i in range(N_CHANNELS)]\n",
"\n",
"# Generate sine wave data\n",
"data = np.array([AMPLITUDE * np.sin(2 * np.pi * (INIT_FREQ + i * FREQ_INC) * time)\n",
" for i in range(N_CHANNELS)])"
]
},
{
"cell_type": "markdown",
"id": "ec9e71b8-a995-4c0f-bdbb-5d148d8fa138",
"metadata": {},
"source": [
"## Visualizing EEG Data\n",
"\n",
"Next, let's dive into visualizing the EEG data. We construct each timeseries using a `Curve` element, assigning it a `label` and setting `subcoordinate_y=True`. All these curves are then aggregated into a list, which serves as the input for an `Overlay` element. Rendering this `Overlay` produces a plot where the timeseries are stacked vertically.\n",
"\n",
"Additionally, we'll enhance user interaction by implementing a custom hover tool. This will display key information—channel, time, and amplitude—when you hover over any of the curves."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "9476769f-3935-4236-b010-1511d1a1e77f",
"metadata": {},
"outputs": [],
"source": [
"hover = HoverTool(tooltips=[\n",
" (\"Channel\", \"@channel\"),\n",
" (\"Time\", \"$x s\"),\n",
" (\"Amplitude\", \"$y µV\")\n",
"])\n",
"\n",
"channel_curves = []\n",
"for channel, channel_data in zip(channels, data):\n",
" ds = hv.Dataset((time, channel_data, channel), [\"Time\", \"Amplitude\", \"channel\"])\n",
" curve = hv.Curve(ds, \"Time\", [\"Amplitude\", \"channel\"], label=channel)\n",
" curve.opts(\n",
" subcoordinate_y=True, color=\"black\", line_width=1, tools=[hover],\n",
" )\n",
" channel_curves.append(curve)\n",
"\n",
"eeg = hv.Overlay(channel_curves, kdims=\"Channel\").opts(\n",
" xlabel=\"Time (s)\", ylabel=\"Channel\", show_legend=False, aspect=3, responsive=True,\n",
")\n",
"eeg"
]
},
{
"cell_type": "markdown",
"id": "b4f603e2-039d-421a-ba9a-ed9e77efab99",
"metadata": {},
"source": [
"## Creating the Minimap\n",
"\n",
"A minimap can provide a quick overview of the data and help you navigate through it. We'll compute the z-score for each channel and represent it as an image; the z-score will normalize the data and bring out the patterns more clearly. To enable linking in the next step between the EEG `Overlay` and the minimap `Image`, we ensure they share the same y-axis range."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "40fa2198-c3b5-41e1-944f-f8b812612168",
"metadata": {},
"outputs": [],
"source": [
"y_positions = range(N_CHANNELS)\n",
"yticks = [(i , ich) for i, ich in enumerate(channels)]\n",
"\n",
"z_data = zscore(data, axis=1)\n",
"\n",
"minimap = hv.Image((time, y_positions , z_data), [\"Time (s)\", \"Channel\"], \"Amplitude (uV)\")\n",
"minimap = minimap.opts(\n",
" cmap=\"RdBu_r\", xlabel='Time (s)', alpha=.5, yticks=[yticks[0], yticks[-1]],\n",
" height=150, responsive=True, default_tools=[], clim=(-z_data.std(), z_data.std())\n",
")\n",
"minimap"
]
},
{
"cell_type": "markdown",
"id": "a5b77970-342f-4428-bd1c-4dbef1e6a2b5",
"metadata": {},
"source": [
"## Building the dashboard\n",
"\n",
"Finally, we use [`RangeToolLink`](../../../user_guide/Linking_Plots.ipynb) to connect the minimap `Image` and the EEG `Overlay`, setting bounds for the initial viewable area. Once the plots are linked and assembled into a unified dashboard, you can interact with it. Experiment by dragging the selection box on the minimap or resizing it by clicking and dragging its edges."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "260489eb-2dbf-4c88-ba83-dd1cba0e547b",
"metadata": {},
"outputs": [],
"source": [
"RangeToolLink(\n",
" minimap, eeg, axes=[\"x\", \"y\"],\n",
" boundsx=(None, 2), boundsy=(None, 6.5)\n",
")\n",
"\n",
"dashboard = (eeg + minimap).opts(merge_tools=False).cols(1)\n",
"dashboard"
]
}
],
"metadata": {
"language_info": {
"name": "python",
"pygments_lexer": "ipython3"
}
},
"nbformat": 4,
"nbformat_minor": 5
}
28 changes: 28 additions & 0 deletions examples/user_guide/Customizing_Plots.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -745,6 +745,34 @@
"\n",
"Note that as of HoloViews 1.17.0, `multi_y` does not have streaming plot support, extra axis labels are not dynamic and only the `RangeXY` linked stream is aware of additional y-axes."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Subcoordinate y-axis\n",
"*(Available in HoloViews >= 1.18)*\n",
"\n",
"HoloViews enables you to create overlays where each element has its own distinct y-axis subcoordinate system. To activate this feature, set the `subcoordinate_y` keyword to True for **each** overlay element; the default is False. When using `subcoordinate_y=True`, setting a `label` for each element is required for proper rendering and identification.This will automatically distribute overlay elements along the y-axis.\n",
"\n",
"For more fine-grained control over y-axis positioning, you can specify a numerical 2-tuple for subcoordinate_y with values ranging from 0 to 1. Additionally, the `subcoordinate_scale` keyword, which defaults to 1, allows you to adjust the vertical scale of each element. This option is only applicable when `subcoordinate_y=True`. For example, setting a single Curve's `subcoordinate_scale` to 2 will result in it overlapping 50% with its adjacent elements."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"x = np.linspace(0, 10*np.pi)\n",
"\n",
"curves = [\n",
" hv.Curve((x + i*np.pi/2, np.sin(x)), label=f'Line {i}').opts(subcoordinate_y=True, subcoordinate_scale=1.2)\n",
" for i in range(3)\n",
"]\n",
"\n",
"hv.Overlay(curves).opts(show_legend=False)"
]
}
],
"metadata": {
Expand Down
Loading

0 comments on commit 38e0b39

Please sign in to comment.