Customize theme-dependent plot colors using panel/holoviews

When using for example pmui.Page together with HoloViews (Bokeh backend), the plot appearance automatically changes when toggling between light and dark mode.

I now want to override some of the colors that HoloViews/Bokeh uses when the theme changes, i.e. define custom colors for light and dark mode, maybe use another background_fill_color etc.

What is the recommended or supported way in Panel / panel_material_ui to achieve this? Using plot hooks for light and dark mode separately, based on the theme toggle of the Page? Access the theme parameter of pn.pane.HoloViews and use something like pn.bind(update_plot, page.param.dark_theme) (see How to adjust theme on the fly)?

MRE basis:

import holoviews as hv
import numpy as np
import panel as pn
import panel_material_ui as pmui

hv.extension('bokeh')
pn.extension()

x = np.arange(100)
y = np.cumsum(np.random.randn(100))

curve = hv.Curve((x, y)).opts(
    responsive=True,
    height=500
)

hv_pane = pn.pane.HoloViews(curve)

page = pmui.Page(main=[hv_pane])

page.preview()

Not entirely sure, but maybe this can generate some insights

If that doesn’t work, feel free to raise an issue on panel / pmui

1 Like

I used the holoviz-skills branded apps example (url inside the example code below) when I started testing PMUI in my dashboards.

import holoviews as hv
import numpy as np
import panel as pn
import panel_material_ui as pmui

pn.extension()
hv.extension("bokeh")

THEME = {
    "light": {
        "palette": {
            "primary": {"main": "#4099da"},
            "secondary": {"main": "#644c76"},
        },
        "typography": {
            "fontFamily": "Montserrat, sans-serif",
            "fontSize": 14,
        },
        "shape": {"borderRadius": 8},
    },
    "dark": {
        "palette": {
            "primary": {"main": "#FF4F00"},
            "secondary": {"main": "#9575cd"},
        },
        "typography": {
            "fontFamily": "Montserrat, sans-serif",
            "fontSize": 14,
        },
        "shape": {"borderRadius": 8},
    },
}

# Custom HoloViews/Bokeh plot colours per mode (keyed by dark_theme bool).
PLOT_COLORS = {
    True: {  # dark
        "bg": "#11111a",
        "border": "#11111a",
        "line": "#FF4F00",
        "grid": "#33333a",
        "text": "#e0e0e0",
    },
    False: {  # light
        "bg": "#ffffff",
        "border": "#ffffff",
        "line": "#4099da",
        "grid": "#e0e0e0",
        "text": "#222222",
    },
}

x = np.arange(100)
y = np.cumsum(np.random.randn(100))


def _style_hook(colors):
    """Return a HoloViews hook that paints the Bokeh figure with ``colors``."""

    def hook(plot, _element):
        fig = plot.handles["plot"]
        fig.background_fill_color = colors["bg"]
        fig.border_fill_color = colors["border"]
        for axis in (fig.xaxis, fig.yaxis):
            axis.axis_label_text_color = colors["text"]
            axis.major_label_text_color = colors["text"]
            axis.axis_line_color = colors["grid"]
            axis.major_tick_line_color = colors["grid"]
            axis.minor_tick_line_color = colors["grid"]
        for grid in (fig.xgrid, fig.ygrid):
            grid.grid_line_color = colors["grid"]

    return hook


def plot(dark_theme: bool):
    """Curve restyled to match the active (light/dark) Page theme."""
    colors = PLOT_COLORS[bool(dark_theme)]
    return hv.Curve((x, y)).opts(
        responsive=True,
        height=500,
        color=colors["line"],
        line_width=2,
        bgcolor=colors["bg"],
        hooks=[_style_hook(colors)],
    )


page = pmui.Page(
    title="Branded App",
    theme_config=THEME,
    sidebar=[
        pmui.Button(label="Action", icon="bolt", color="primary"),
        pmui.Button(
            label="Branding docs",
            icon="link",
            color="primary",
            variant="outlined",
            href="https://holoviz-dev.github.io/holoviz-skills/developing-with-holoviz/panel/branding-material-ui/",
            target="_blank",
        ),
    ],
)
# Bind the plot to the Page's theme toggle so its colours follow light/dark.
page.main = [
    pmui.Typography("Welcome", variant="h4"),
    pn.panel(pn.bind(plot, page.param.dark_theme)),
]
page.servable()

Hopefully this helps :slightly_smiling_face:

2 Likes

Thanks to both of you, those are great approaches.

The idea using a bind method and custom plot options or hooks is exactly what I needed in principle.
However, when I run your example, only some of the colors change as expected. For example, the background_fill_color is not applied…
Is this a problem on my end?

I’m using pmui 0.12.0 and panel 1.9.3

Okay so I had a bit of a play around and came across something interesting (for me).
The following version adds a DynamicMap callback that listens to page.dark_theme changes.
I have commented the additional code and also made the background colours more distinct.
Is there an explanation why, in the first “classic” plot, the background doesn’t change when the theme is toggled, whereas in the second plot/dmap it only changes after the first toggle?

import holoviews as hv
import numpy as np
import panel as pn
import panel_material_ui as pmui

pn.extension()
hv.extension("bokeh")

THEME = {
    "light": {
        "palette": {
            "primary": {"main": "#4099da"},
            "secondary": {"main": "#644c76"},
        },
        "typography": {
            "fontFamily": "Montserrat, sans-serif",
            "fontSize": 14,
        },
        "shape": {"borderRadius": 8},
    },
    "dark": {
        "palette": {
            "primary": {"main": "#FF4F00"},
            "secondary": {"main": "#9575cd"},
        },
        "typography": {
            "fontFamily": "Montserrat, sans-serif",
            "fontSize": 14,
        },
        "shape": {"borderRadius": 8},
    },
}

# Custom HoloViews/Bokeh plot colours per mode (keyed by dark_theme bool).
PLOT_COLORS = {
    True: {  # dark
        "bg": "#3e3ebd",        # adapted to blue
        "border": "#11111a",
        "line": "#FF4F00",
        "grid": "#33333a",
        "text": "#e0e0e0",
    },
    False: {  # light
        "bg": "#f98a8a",        # adapted to red
        "border": "#ffffff",
        "line": "#4099da",
        "grid": "#e0e0e0",
        "text": "#222222",
    },
}

x = np.arange(100)
y = np.cumsum(np.random.randn(100))


def _style_hook(colors):
    """Return a HoloViews hook that paints the Bokeh figure with ``colors``."""

    def hook(plot, _element):
        fig = plot.handles["plot"]
        fig.background_fill_color = colors["bg"]
        fig.border_fill_color = colors["border"]
        for axis in (fig.xaxis, fig.yaxis):
            axis.axis_label_text_color = colors["text"]
            axis.major_label_text_color = colors["text"]
            axis.axis_line_color = colors["grid"]
            axis.major_tick_line_color = colors["grid"]
            axis.minor_tick_line_color = colors["grid"]
        for grid in (fig.xgrid, fig.ygrid):
            grid.grid_line_color = colors["grid"]

    return hook


def plot(dark_theme: bool):
    """Curve restyled to match the active (light/dark) Page theme."""
    colors = PLOT_COLORS[bool(dark_theme)]
    return hv.Curve((x, y)).opts(
        responsive=True,
        height=400,
        color=colors["line"],
        line_width=2,
        bgcolor=colors["bg"],
        hooks=[_style_hook(colors)],
    )


page = pmui.Page(
    title="Branded App",
    theme_config=THEME,
    sidebar=[
        pmui.Button(label="Action", icon="bolt", color="primary"),
        pmui.Button(
            label="Branding docs",
            icon="link",
            color="primary",
            variant="outlined",
            href="https://holoviz-dev.github.io/holoviz-skills/developing-with-holoviz/panel/branding-material-ui/",
            target="_blank",
        ),
    ],
)


# NOTE: additional code starts here

curve_for_dmap = hv.Curve((x, y))

def plot_dmap(dark_theme: bool):
    colors = PLOT_COLORS[bool(dark_theme)]
    
    return curve_for_dmap.opts(
        responsive=True,
        height=400,
        color=colors["line"],
        line_width=2,
        bgcolor=colors["bg"],
        hooks=[_style_hook(colors)],
    )
    
theme_stream = hv.streams.Stream.define("Theme", dark_theme=False)()

dmap = hv.DynamicMap(plot_dmap, streams=[theme_stream])

page.param.watch(lambda _: theme_stream.event(dark_theme=bool(page.dark_theme)), "dark_theme")



# Bind the plot to the Page's theme toggle so its colours follow light/dark.
page.main = [
    pmui.Typography("Welcome", variant="h4"),
    pn.panel(pn.bind(plot, page.param.dark_theme)),
    dmap
]

page.servable()

There were some changes in the latest pmui Theme Plotting Libraries — Panel Material UI v0.12.0

Perhaps it’s a bug? **Bug: Bokeh figure axis lines, tick marks, and outline border invisible inside `mui.Page()`** · Issue #644 · panel-extensions/panel-material-ui · GitHub

Having the same problem with previous pmui versions.
Could it possibly be that MaterialUI is overriding certain CSS classes? Or do I need to set a trigger once the page has loaded that kind of “activates” my plot?
I’ll keep playing around with this… anyway, appreciate other impulses!