{ "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 from different data groups in a single plot using `subcoordinate_y`.\n", "2. Normalize the timeseries per data group.\n", "3. 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) and position 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": "4d81eadd-95c7-4474-9f59-deb2c0cf3297", "metadata": {}, "outputs": [], "source": [ "import pandas as pd\n", "import numpy as np\n", "import pandas as pd\n", "import holoviews as hv\n", "from holoviews.operation.normalization import subcoordinate_group_ranges\n", "from holoviews.operation.datashader import rasterize\n", "from holoviews.plotting.links import RangeToolLink\n", "import colorcet as cc\n", "from scipy.stats import zscore\n", "hv.extension('bokeh')" ] }, { "cell_type": "markdown", "id": "1c95f241-2314-42b0-b6cb-2c0baf332686", "metadata": {}, "source": [ "## Generating data\n", "\n", "Let's start by `EEG` and position (`POS`) data. We'll create a timeseries for each EEG channel using sine waves with varying frequencies, and random data for three position channels. We'll set these two data groups to have different amplitudes and units." ] }, { "cell_type": "code", "execution_count": null, "id": "6ac3812a", "metadata": {}, "outputs": [], "source": [ "GROUP_EEG = 'EEG'\n", "GROUP_POS = 'Position'\n", "N_CHANNELS_EEG = 10\n", "N_CHANNELS_POS = 3\n", "N_SECONDS = 5\n", "SAMPLING_RATE_EEG = 200\n", "SAMPLING_RATE_POS = 25\n", "INIT_FREQ = 2 # Initial frequency in Hz\n", "FREQ_INC = 5 # Frequency increment\n", "AMPLITUDE_EEG = 1000 # EEG amplitude multiplier\n", "AMPLITUDE_POS = 10 # Position amplitude multiplier\n", "\n", "# Generate time for EEG and position data\n", "total_samples_eeg = N_SECONDS * SAMPLING_RATE_EEG\n", "total_samples_pos = N_SECONDS * SAMPLING_RATE_POS\n", "time_eeg = np.linspace(0, N_SECONDS, total_samples_eeg)\n", "time_pos = np.linspace(0, N_SECONDS, total_samples_pos)\n", "\n", "# Generate EEG timeseries data\n", "def generate_eeg_data(index):\n", " return AMPLITUDE_EEG * np.sin(2 * np.pi * (INIT_FREQ + index * FREQ_INC) * time_eeg)\n", "\n", "eeg_channels = [str(i) for i in np.arange(N_CHANNELS_EEG)]\n", "eeg_data = np.array([generate_eeg_data(i) for i in np.arange(N_CHANNELS_EEG)])\n", "eeg_df = pd.DataFrame(eeg_data.T, index=time_eeg, columns=eeg_channels)\n", "eeg_df.index.name = 'Time'\n", "\n", "# Generate position data\n", "pos_channels = ['X', 'Y', 'Z'] # avoid lowercase 'x' and 'y' as channel/dimension names\n", "pos_data = AMPLITUDE_POS * np.random.randn(N_CHANNELS_POS, total_samples_pos).cumsum(axis=1)\n", "pos_df = pd.DataFrame(pos_data.T, index=time_pos, columns=pos_channels)\n", "pos_df.index.name = 'Time'" ] }, { "cell_type": "markdown", "id": "ec9e71b8-a995-4c0f-bdbb-5d148d8fa138", "metadata": {}, "source": [ "## Visualizing EEG Data\n", "\n", "Next, let's dive into visualizing the data. We construct each timeseries using a `Curve` element, assigning it a `group`, a `label` and setting `subcoordinate_y=True`. All these curves are then aggregated into a list per data group, 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 about the group, channel, time, and amplitude value when you hover over any of the curves." ] }, { "cell_type": "code", "execution_count": null, "id": "9476769f-3935-4236-b010-1511d1a1e77f", "metadata": {}, "outputs": [], "source": [ "# Create a Curve per data series\n", "def df_to_curves(df, kdim, vdim, color='black', group='EEG', ):\n", " curves = []\n", " for i, (channel, channel_data) in enumerate(df.items()):\n", " ds = hv.Dataset((channel_data.index, channel_data), [kdim, vdim])\n", " curve = hv.Curve(ds, kdim, vdim, group=group, label=str(channel))\n", " curve.opts(\n", " subcoordinate_y=True, color=color if isinstance(color, str) else color[i], line_width=1, \n", " hover_tooltips=hover_tooltips, tools=['xwheel_zoom'], line_alpha=.8,\n", " )\n", " curves.append(curve)\n", " return curves\n", "\n", "hover_tooltips = [(\"Group\", \"$group\"), (\"Channel\", \"$label\"), (\"Time\"), (\"Value\")]\n", "\n", "vdim_EEG = hv.Dimension(\"Value\", unit=\"µV\")\n", "vdim_POS = hv.Dimension(\"Value\", unit=\"cm\")\n", "time_dim = hv.Dimension(\"Time\", unit=\"s\")\n", "\n", "eeg_curves = df_to_curves(eeg_df, time_dim, vdim_EEG, color='black', group='EEG')\n", "pos_curves = df_to_curves(pos_df, time_dim, vdim_POS, color=cc.glasbey_cool, group='POS')\n", "\n", "# Combine EEG and POS curves into an Overlay\n", "eeg_curves_overlay = hv.Overlay(eeg_curves, \"Channel\")\n", "pos_curves_overlay = hv.Overlay(pos_curves, \"Channel\")\n", "curves_overlay = (eeg_curves_overlay * pos_curves_overlay).opts(\n", " xlabel=time_dim.pprint_label, ylabel=\"Channel\", show_legend=False, aspect=3, responsive=True,\n", ")\n", "curves_overlay" ] }, { "cell_type": "markdown", "id": "983e1f84-6006-4d64-9144-4aba0ad93946", "metadata": {}, "source": [ "Note that the overlay above has a single y-axis wheel-zoom tool in the toolbar which has been configured specifically for grouped subcoordinate_y overlays. When this tool is enabled and your mouse intersects horizontally with a curve, scrolling will scale all the curves that belong to the same group. The second wheel zoom tool controls the X-axis scale of all the curves together.\n", "\n", "
Important
\n", "\n",
" Ensure that the cursor is within a curve's data range to activate the Y-axis wheel zoom tool for that curve's group.\n",
" \n",
"