Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement support for subcoordinate systems in the y-axis #5840

Merged
merged 38 commits into from
Sep 26, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
7a1f5ff
Implement support for subcoordinate systems in the y-axis
philippjfr Aug 2, 2023
d690a2e
Allow links at the OverlayPlot level
philippjfr Aug 4, 2023
f91ecb3
Compute subcoordinate ranges at the OverlayPlot level
philippjfr Aug 4, 2023
daa1dd5
Check if subplots is available
hoxbro Aug 8, 2023
def2c73
Add support for automatically distributing subcoordinates
philippjfr Aug 30, 2023
2d1347f
Merge branch 'main' into subcoordinate
maximlt Sep 4, 2023
0f524ba
Merge branch 'main' into subcoordinate
maximlt Sep 6, 2023
3b19d42
support no label set
maximlt Sep 12, 2023
313dd7e
add basic tests
maximlt Sep 12, 2023
3692edf
test scale and tighten accepted range
maximlt Sep 12, 2023
951c891
add support for range
maximlt Sep 12, 2023
224ef93
Merge branch 'main' into subcoordinate
maximlt Sep 12, 2023
ddcbfc9
Revert "Allow links at the OverlayPlot level"
maximlt Sep 12, 2023
2fba7d8
inherit from TestBokehPlot
maximlt Sep 12, 2023
ec3a5b8
fix tests
maximlt Sep 12, 2023
e0280f0
Merge branch 'main' into subcoordinate
maximlt Sep 16, 2023
e1d204f
reduce upper padding
maximlt Sep 19, 2023
bba18d6
allow to plot an element alone
maximlt Sep 19, 2023
147b37c
error when label isn't set
maximlt Sep 19, 2023
19beea1
update docs
maximlt Sep 19, 2023
8ad0b4f
fix code in docs
maximlt Sep 19, 2023
4b6791a
Revise the eeg_viewer demo data and text
droumis Sep 20, 2023
f0f9502
Revise subcoord UG for label and scale
droumis Sep 20, 2023
86a1815
add comment on computing the upper bound
maximlt Sep 21, 2023
c0d91e8
clean up enumerate
maximlt Sep 21, 2023
fd61b68
apply code suggestion
maximlt Sep 21, 2023
175d8c7
apply micro-opt suggestion
maximlt Sep 21, 2023
bb7c3ce
link to the RangeToolLink doc
maximlt Sep 21, 2023
acea701
refactor categorical handling
maximlt Sep 21, 2023
58a2126
add comment about subcoordinate_y as a tuple
maximlt Sep 21, 2023
5061e8b
clean up unused auto trace
maximlt Sep 22, 2023
9307662
fix the position of a condition in the if block
maximlt Sep 22, 2023
ab348b4
add _subcoord_overlaid property
maximlt Sep 22, 2023
f2dbd37
remove seemingly non required guard
maximlt Sep 22, 2023
1f476d6
Merge branch 'main' into subcoordinate
maximlt Sep 25, 2023
59a8ec7
add error when multi_y is set together with subcoordinate_y
maximlt Sep 25, 2023
90206fa
add error when labels are not unique, update no label error
maximlt Sep 25, 2023
23277bc
only check labels are provided on elements set with subcoordinate_y
maximlt Sep 26, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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."
hoxbro marked this conversation as resolved.
Show resolved Hide resolved
]
},
{
"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