{ "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 }