Panel Matplotlib rendering slow?

Hello everyone,

I’m trying to make my first panel app. Its supposed to be a dashboard for a study dataset to quickly skim over the data and check everything looks right for every participant and session. I use 2 pn.widgets.Select to select the participant and the session you want to look at and there are overviews for each level (so without participant selected, with participant but no session selected yet and the last one details for a session of a participant).

When switching the screens between participants and the dataset overview everything loads very fast but when selecting a session, there is like a 2.5 seconds delay and I cant figure out why and if I can reduce that. Some notes: the details page are just 4 plots which by themselves are computed within like 0.1s each. The code is down below.

I’m still very new to panel so just let me know if I’m doing something wrong and why the switching to the details page is so slow?
Thank you in advance for your help!

import matplotlib
import panel as pn
import numpy as np

from pathlib import Path

from motion_utils import plot_accelorometer_from_tsv
from edf_utils import plot_body_temp_from_edf, plot_eeg_spectrogram_from_edf



pn.extension(loading_spinner='dots')
matplotlib.use('agg')

DATASET_ROOT = Path("../BIDS/")


def get_participants():
    return sorted(
        p.name for p in DATASET_ROOT.glob("sub-*")
        if p.is_dir()
    )

def get_sessions(participant):
    if participant is None:
        return []
    return sorted(
        p.name for p in (DATASET_ROOT / participant).glob("ses-*")
        if p.is_dir()
    )



participant_select = pn.widgets.Select(
    name="Participant",
    options=[None] + get_participants(),
    value=None
)

session_select = pn.widgets.Select(
    name="Session",
    options=[],
    value=None
)

session_select.options = pn.bind(lambda p: [None] + get_sessions(p), participant_select)
session_select.disabled = pn.bind(lambda p: p is None, participant_select)

def dataset_overview():
    return pn.Column(
        f"## General Overview Page",
        f"Number of participants: {len(get_participants())}"
    )

def participant_overview(participant):
    sessions = get_sessions(participant)
    return pn.Column(
        f"## Participant {participant}",
        f"Number of sessions: {len(sessions)}",
    )

def session_overview(participant, session):
    session_path = DATASET_ROOT / participant / session

    files = []
    for path in session_path.rglob("*"):
        if path.is_file():
            files.append(path.relative_to(session_path))
    
    edf_status = get_edf_status(session_path)
    motion_status = get_motion_status(session_path)
    

    return pn.Column(
        f"## {participant} / {session}",
        f"Files in session: {len(files)}",
        pn.pane.Markdown("\n".join(f"- {f}" for f in files)),
        edf_status,
        motion_status,
    )



def get_edf_status(session_path):
    edf_status = pn.pane.Markdown("## Edf Data is missing")
    edf_files = []
    for path in session_path.rglob("*_eeg.edf"):
        if path.is_file():
            edf_files.append(path)
    if len(edf_files) > 0:
        eeg_left_plot = plot_eeg_spectrogram_from_edf(edf_files[0], channel_name="EEG_LEFT")
        eeg_right_plot = plot_eeg_spectrogram_from_edf(edf_files[0], channel_name="EEG_RIGHT")
        body_temp_plot = plot_body_temp_from_edf(edf_files[0])
        edf_status = pn.Column(
            pn.pane.Matplotlib(eeg_left_plot, dpi=144, sizing_mode='stretch_width'),
            pn.pane.Matplotlib(eeg_right_plot, dpi=144, sizing_mode='stretch_width'),
            pn.pane.Matplotlib(body_temp_plot, dpi=144, sizing_mode='stretch_width')
        )
    return edf_status



def get_motion_status(session_path):
    motion_status = pn.pane.Markdown("## Accelerometer Data is missing")
    motion_files = []
    for path in session_path.rglob("*_motion.tsv"):
        if path.is_file():
            motion_files.append(path)

    if len(motion_files) > 0:
        motion_plot = plot_accelorometer_from_tsv(motion_files[0])
        motion_status = pn.pane.Matplotlib(motion_plot, dpi=144, sizing_mode='stretch_width')
    return motion_status


def main_view(participant, session):
    if participant is None:
        return dataset_overview()

    if session is None:
        return participant_overview(participant)

    return session_overview(participant, session)

main = pn.panel(
    pn.bind(main_view, participant_select, session_select),
    loading_indicator=True
)


template = pn.template.FastListTemplate(
    title="Dataset Explorer",
    sidebar=[
        pn.pane.Markdown("### Navigation"),
        participant_select,
        session_select,
    ],
    main=[
        main
    ],
)

template.servable()

Also when doing larger plots (which take 4 seconds to compute) the app takes several minutes to render them.

@christof without any data it’s a little hard to help debug your codes performance, plus I don’t have all the imports.

But here is a couple of pointers which I would look to implement.

  1. Don’t loop over file paths inside functions which produce plots. Do this once at the start for each file type then store in a dict.
  2. Cache data in the pn.state.cache so that the user only needs to load the data for each specific session once. Subsequent loads will be much faster.
  3. W8401: Use a list comprehension instead of a for-loop (use-list-comprehension) (lines 55, 74 & 92)
  4. If the data set is large try and implement datashader.
1 Like