How to use `Viewer` and `Param` classes with non-blocking loading pages (long time consuming components)

Hi, I would like to create a more sophisticated dashboard. However, I’m having trouble combining the param.Parametrized and pn.viewable.Viewer classes with non-blocking loading of data and, effectively, the entire page.

  • I want to display the page before the data and components are loaded. Instead of waiting, I’d like to show a LoadingSpinner to indicate that the data is being fetched. Only when the components are created should they be progressively displayed.
  • I also don’t quite understand how, in this scenario, to decouple the data from the date_range and Viewer components.
  • I would like to add a DateRangeSlider to complement the DateRangePicker, but I’m not sure how to use pn.rx for this.
  • I don’t even know how to structure the project in this case.

Here is my current (blocking) code draft, I have no Idea how to change it to make it non-blocking. Tutorials usually use functions instead of classes.

import param
import panel as pn
import datetime as dt
import time
import hvplot.polars
import polars as pl
import numpy as np


## === Data manipulation ===
def get_data_1(date_from: str, date_to: str) -> pl.DataFrame:
    """Some data from SQL db (time consuming function), Returns polars df. For simplicity generate simple random data."""
    time.sleep(3)
    date_series = pl.date_range(
        start=pl.lit(date_from),
        end=pl.lit(date_to),
        eager=True,
    )
    n = len(date_series)
    x = 50 + np.random.randn(n).cumsum()
    d = np.random.randn(n)
    df = pl.DataFrame(
        {
            "dt": date_series,
            "x": x,
            "d": d,
        }
    )
    return df


def get_data_1_available_date_range() -> tuple[dt.date, dt.date]:
    """SQL query to db."""
    time.sleep(0.5)
    # use to bound date_range and picker/slider
    first_date = dt.date(2025, 1, 1)
    last_date = dt.date(2025, 3, 31)
    return (first_date, last_date)


def postprocess_data_data_1(df: pl.DataFrame):
    some_stats = df.select(
        pl.min("dt").alias("first"),
        pl.max("dt").alias("last"),
        pl.mean("x"),
        pl.mean("d"),
    )
    return some_stats


def get_data_2(date_from: str, date_to: str):
    """Another data source - time consuming source (REST API). For simplicity generate simple random data."""
    time.sleep(5)
    df = pl.DataFrame(
        {
            "i": list(range(100)),
            "perc": np.random.randint(0, 100, size=100),
        }
    )
    return df


## === Charts ===
def create_sophisticate_charts(df: pl.DataFrame):
    """Another time consuming task (chart generation)."""
    time.sleep(1)
    chart1 = df.hvplot.line(x="dt", y="x")
    distribution_chart = df.hvplot.hist("d")
    chart_layout = chart1 + distribution_chart
    return chart_layout


def create_chart_2(df: pl.DataFrame):
    """This is another sophisticated chart with consumed a lot of time."""
    time.sleep(3)
    barchart = df.hvplot.bar(x="i", y="perc")
    return barchart


## === Components ===


class Component1(pn.viewable.Viewer):
    """Chart + Stats info."""

    def __init__(self, date_from: str, date_to: str):
        # super.__init__(self, *args, **kwargs)
        self.df = get_data_1(date_from, date_to)
        self.stats_df = postprocess_data_data_1(self.df)

    def __panel__(self):
        return pn.Column(
            pn.Card(
                pn.pane.Markdown(repr(self.stats_df)),
                title="Total stats",
                width=400,
            ),
            create_sophisticate_charts(self.df),
        )


class Component2(pn.viewable.Viewer):
    """Sophisticated chart."""

    def __init__(self, date_from: str, date_to: str):
        self.df = get_data_2(date_from, date_to)

    def __panel__(self):
        return pn.panel(create_chart_2(self.df))


## === Pages ===

class PageContent1(pn.viewable.Viewer):
    date_range = param.DateRange(default=(dt.date(2025, 1, 1), dt.date(2025, 2, 10)))

    def __init__(self, **params):
        super().__init__(**params)
        # set bounds for self.date_range and this date_range_picker
        # after time-consuming operation get_data_1_available_date_range()
        date_from, date_to = get_data_1_available_date_range()
        self.date_from = date_from
        self.date_to = date_to
        self.param.date_range.bounds = (date_from, date_to)

        self.date_range_picker = pn.widgets.DateRangePicker(
            name="Date Range Picker",
            value=(dt.date(2025, 1, 1), dt.date(2025, 2, 10)),
            start=date_from,
            end=date_to,
        )

        # Link the picker to the parameter
        self.date_range_picker.link(self, value="date_range")

        # Initialize components with default values
        self.component1 = Component1("2025-01-01", "2025-02-10")
        self.component2 = Component2("2025-01-01", "2025-02-10")

    @param.depends("date_range", watch=True)
    def _update_charts(self):
        # Convert date range to strings - use getattr for safety
        date_range_value = getattr(
            self, "date_range", (dt.date(2025, 1, 1), dt.date(2025, 2, 10))
        )
        start_date, end_date = date_range_value
        date_from_str = start_date.strftime("%Y-%m-%d")
        date_to_str = end_date.strftime("%Y-%m-%d")
        self.component1 = Component1(date_from_str, date_to_str)
        self.component2 = Component2(date_from_str, date_to_str)

    def __panel__(self):
        return pn.Column(
            self.date_range_picker, pn.FlexBox(self.component1, self.component2)
        )

# dashboard
p = PageContent1()

dashboard = pn.template.BootstrapTemplate(
    title="My Dashboard",
    main=[p],
    sidebar_width=250,
    sidebar=[
        "## Navigation",
    ],
)

Can you help me?