How do I get my Holoviews plot(s) to be responsive in my Panel app?

I’m trying to create a dashboard with some settings on the left and a plot layout with two subplots in one column.

My requirements are

  • The settings should be on the left with a fixed size of 300px.
  • The upper price plot should be
    • a Curve plot.
    • responsive horizontally and vertically.
  • The lower delta plot should be
    • a Bar chart
    • responsive vertically
    • fixed horizontally at 300px.
    • It should be clear to the user if the delta is positive or negative.
  • The date x-axis of the two plots should be linked.
  • The value and delta y-axis of the two plots should not be linked.
  • Preferably I would like one common toolbar on the top of the plots.

The best approximation so far is shown below but it does not satisfy my requirements as

  • The upper price plot is not responsive vertically
  • There are two toolbars instead of one.
  • The lower delta plot is a step plot.

I can get some of the missing things working but then some of the requirements already satisfied no longer are satisfied.

How do I solve this problem?

Best Approximation So Far

import holoviews as hv
import hvplot.pandas
import pandas as pd
import panel as pn
from bokeh.models.formatters import DatetimeTickFormatter
from holoviews import opts

pn.config.sizing_mode = "stretch_width"

N = 365
START = pd.Timestamp("2020-01-01")
OFFSET = pd.Timedelta(days=1)
DATES = [START + n * OFFSET for n in range(0, N)]
PRICES = [20 + n / N for n in range(0, N)]
FORECASTS = [20 + 1.1 * n / N + 0.5 for n in range(0, N)]
DELTA = [0.1 * n / N for n in range(0, N)]

MARKET_DF = pd.DataFrame({"date": DATES, "value": PRICES,})
FORECASTS_DF = pd.DataFrame({"date": DATES, "value": FORECASTS,})
DELTA_DF = pd.DataFrame({"date": DATES, "delta": DELTA,})

DATETIME_TICK_FORMATTER = DatetimeTickFormatter(days="%d/%m", months="%d/%m/%Y", years="%d/%m/%Y",)
PRICE_OPTS = opts.Curve(line_width=6, responsive=True, show_grid=True)
DELTA_OPTS = opts.Curve(
    height=300, line_width=4, responsive=True, color="lightblue", show_grid=True
)
DELTA_LINE_OPTS = opts.HLine(color="black", line_width=1, alpha=0.6)


def get_app():
    market_plot = (
        MARKET_DF.hvplot(x="date", y="value", c="green", label="market")
        .opts(xformatter=DATETIME_TICK_FORMATTER)
        .opts(PRICE_OPTS)
        .opts(color="red")
    )
    forecasts_plot = (
        FORECASTS_DF.hvplot(x="date", y="value", c="red", label="fumo")
        .opts(xformatter=DATETIME_TICK_FORMATTER)
        .opts(PRICE_OPTS)
        .opts(color="blue")
    )
    price_plot = market_plot * forecasts_plot

    hline_plot = (
        hv.HLine(0, kdims=[hv.Dimension("date"), hv.Dimension("delta")])
        .opts(xformatter=DATETIME_TICK_FORMATTER)
        .opts(DELTA_LINE_OPTS)
    )
    delta_plot = (
        DELTA_DF.hvplot(x="date", y="delta", kind="step")
        .opts(xformatter=DATETIME_TICK_FORMATTER)
        .opts(DELTA_OPTS)
    )
    delta_plot = delta_plot * hline_plot

    plot = pn.Column(
        price_plot.opts(height=600, responsive=True),
        delta_plot.opts(height=300, responsive=True),
        sizing_mode="stretch_both",
    )

    settings_data = pn.Spacer(background="gray", height=300, width=300, sizing_mode="fixed")
    settings_plot = pn.Spacer(background="gray", height=300, width=300, sizing_mode="fixed")
    plot_view = pn.Column(plot)

    return pn.Row(
        pn.Column(
            pn.Row("# Data", margin=(0, 0, 0, 10), aligh="center", sizing_mode="stretch_width"),
            settings_data,
            pn.Row("# Plot", margin=(0, 0, 0, 10), align="center", sizing_mode="stretch_width"),
            settings_plot,
            sizing_mode="stretch_height",
        ),
        plot_view,
        pn.layout.VSpacer(margin=0),  # Hack to stretch Row to max height
        sizing_mode="stretch_both",
        background="lightgray",
    )


get_app().servable()

I’ve filed an issue here https://github.com/holoviz/panel/issues/1394

I’ve replied to the issue. My recommendation in general is to set options using hvPlot rather than .opts where possible and only to fall back on HoloViews options if there is no other way. In this case that looks like this:

def get_app():
    market_plot = MARKET_DF.hvplot(
        x="date", y="value", color="red", label="market", line_width=6,
         responsive=True, grid=True, xformatter=DATETIME_TICK_FORMATTER
    )
    delta_plot = DELTA_DF.hvplot(
        x="date", y="delta", kind="area", label="delta", responsive=True,
        color="lightblue", grid=True, xformatter=DATETIME_TICK_FORMATTER
    )
    return pn.pane.HoloViews(
        (market_plot+delta_plot).cols(1),
        sizing_mode="stretch_both",
        background="lightgray"
    )

Thanks @philippjfr. It works perfectly.

One question: Why is it better to set options in hvplot than in opts?

(I thought the whole point of hvplot was to be able to quickly explore and create. But also end up with HoloViews objects that can be further refined to get the last details in order or reused for other purposes.)

import holoviews as hv
import hvplot.pandas
import pandas as pd
import panel as pn
from bokeh.models.formatters import DatetimeTickFormatter
from holoviews import opts

pn.config.sizing_mode = "stretch_width"

N = 20
START = pd.Timestamp("2020-01-01")
OFFSET = pd.Timedelta(days=1)
DATES = [START + n * OFFSET for n in range(0, N)]
PRICES = [20 + n / N for n in range(0, N)]
DELTA = [0.1 * n / N for n in range(0, N)]

MARKET_DF = pd.DataFrame({"date": DATES, "value": PRICES,})
DELTA_DF = pd.DataFrame({"date": DATES, "delta": DELTA,})

DATETIME_TICK_FORMATTER = DatetimeTickFormatter(days="%d/%m", months="%d/%m/%Y", years="%d/%m/%Y",)
PRICE_OPTS = opts.Curve(line_width=6, responsive=True, show_grid=True, color="red", xformatter=DATETIME_TICK_FORMATTER)
DELTA_OPTS = opts.Area(
    responsive=True, color="lightblue", show_grid=True, xformatter=DATETIME_TICK_FORMATTER
)


def get_app():
    market_plot = MARKET_DF.hvplot(
        x="date", y="value", color="red", label="market", line_width=6,
         responsive=True, grid=True, xformatter=DATETIME_TICK_FORMATTER
    )
    delta_plot = DELTA_DF.hvplot(
        x="date", y="delta", kind="area", label="delta", responsive=True, height=300,
        color="lightblue", grid=True, xformatter=DATETIME_TICK_FORMATTER
    )
    return pn.pane.HoloViews(
        (market_plot+delta_plot).cols(1),
        sizing_mode="stretch_both",
        background="lightgray"
    )


get_app().servable()

Regarding the Bar Chart I think there is a trick described here Workaround for date-based histogram tick labels?. The tick labels can be reduced by using a Histogram.

Whenever I try setting responsive=True within the hvplot, I get the following error:

2020-06-18 15:53:18,276 responsive mode could not be enabled because fixed width and height were specified.

It says that even if there is no fixed width/height.

ticker = pn.widgets.Select(name='Ticker', options=['BTC_USDT','LTC_USDT'])
exchange = pn.widgets.Select(name='Exchange', options=['Binance','Kraken','Bitmex'])

def plot(data):
    h = data.hvplot(datashade=False,logz=False,cmap="hot",
                 grid=False).opts(active_tools=['wheel_zoom'],yaxis = 'right',
                                  responsive=True,
#                                   xlim=(data.date[0].values,data.date[-1].values)
                                 )
    return h

h = rasterize(hv.DynamicMap(plot,streams=[pipe]))
p = price.hvplot.line(line_width=1,responsive=True)
app = h*p

# Panel Layout
p1 = pn.Column(pn.Column(ticker,exchange),app)
p1.add_periodic_callback(update, int(60e3))
p1.servable()