How do I use custom font in Holoviews?

I would like to change the font of my holoviews plots

  1. I would like to change my font to a “known” font like Verdana. How would I do that?
  2. I would like to use the custom font-family:OrstedSansRegular, sans;. This is what is used at in my company. I have the source files

image

Let’s take an example

import pandas as pd
import hvplot.pandas
data = {"x": [1,2], "y": [2,3]}
dataframe = pd.DataFrame(data)
dataframe.hvplot(x="x", y="y")

I would like to change the font for all text. How do I do that?

In the end I would deploy this in a Panel application.

1 Like

I can see that I can change the font via a custom Template.

In the example below I change to Comic Sans MS.

from bokeh.themes.theme import Theme
theme = Theme(
    json={
    'attrs' : {
        'Figure' : {
            'background_fill_color': ORSTED_TEXT_DIGITAL,
            'border_fill_color': ORSTED_TEXT_DIGITAL,
            'outline_line_color': ORSTED_TEXT_DIGITAL,
        },
        'Grid': {
            'grid_line_dash': [6, 4],
            'grid_line_alpha': .3,
        },
        'Text':
            {
                'text_font': 'Courier',
            },
        'Axis': {
            "major_label_text_font": "Comic Sans MS",
            "axis_label_text_font": "Comic Sans MS",
            'major_label_text_color': ORSTED_WHITE,
            'axis_label_text_color': ORSTED_WHITE,
            'major_tick_line_color': ORSTED_WHITE,
            'minor_tick_line_color': ORSTED_WHITE,
            'axis_line_color': ORSTED_WHITE,
        }
    }
})
hv.renderer('bokeh').theme = theme

I’ve tried in a notebook to change the font to a custom font using CSS. But I cannot get the holoviews plot to use it. Panel uses it nicely though.

from bokeh.themes.theme import Theme
import hvplot.pandas
import holoviews as hv
theme = Theme(
    json={
    'attrs' : {
        'Figure' : {
            'background_fill_color': "rgb(59,73,86)",
            'border_fill_color': "rgb(59,73,86)",
            'outline_line_color': "rgb(59,73,86)",
        },
        'Grid': {
            'grid_line_dash': [6, 4],
            'grid_line_alpha': .3,
        },
        'Text':
            {
                'text_font': 'OrstedSansRegular',
                'text_color': "OrstedSansRegular"
            },
        'Axis': {
            "major_label_text_font": "OrstedSansRegular",
            "axis_label_text_font": "OrstedSansRegular",
            'major_label_text_color': "white",
            'axis_label_text_color': "white",
            'major_tick_line_color': "white",
            'minor_tick_line_color': "white",
            'axis_line_color': "white",
        }
    }
})
hv.renderer('bokeh').theme = theme

import pandas as pd
import hvplot.pandas
data = {"x": [1,2], "y": [2,3]}
dataframe = pd.DataFrame(data)
dataframe.hvplot(x="x", y="y")

CSS = """
@font-face {
  font-family: "OrstedSansRegular";
  src: url("https://orstedcdn.azureedge.net/Assets_/dist/fonts/OrstedSans-Regular.eot");
  /* IE9 Compat Modes */
  src: url("https://orstedcdn.azureedge.net/Assets_/dist/fonts/OrstedSans-Regular.eot?#iefix") format("embedded-opentype"), url("https://orstedcdn.azureedge.net/Assets_/dist/fonts/OrstedSans-Regular.woff") format("woff"), url("https://orstedcdn.azureedge.net/Assets_/dist/fonts/OrstedSans-Regular.ttf") format("truetype"), url("https://orstedcdn.azureedge.net/Assets_/dist/fonts/OrstedSans-Regular.svg#52bc9fd504b621176d57f308965c98a4") format("svg");
  /* Legacy iOS */
  font-style: normal;
  font-weight: 400;
}
"""
style=f"""
<style>
{CSS}
div.orsted {{
    font-family: OrstedSansRegular
}}
</style>
<div class="orsted">Orsted</div>
"""
import panel
panel.Column(panel.pane.HTML(style), panel.Pane(dataframe.hvplot(x="x", y="y").options(title="Orsted"))).show()

And here is an extended version that does not work either. But i’ve included all the italic fonts etc. And in the title you can see how numbers look according to the Orsted font.

from bokeh.themes.theme import Theme
import hvplot.pandas
import holoviews as hv
theme = Theme(
    json={
    'attrs' : {
        'Figure' : {
            'background_fill_color': "rgb(59,73,86)",
            'border_fill_color': "rgb(59,73,86)",
            'outline_line_color': "rgb(59,73,86)",
        },
        'Grid': {
            'grid_line_dash': [6, 4],
            'grid_line_alpha': .3,
        },
        'Text':
            {
                'text_font': 'OrstedSansRegular',
                'text_color': "OrstedSansRegular"
            },
        'Axis': {
            "major_label_text_font": "OrstedSansRegular",
            "axis_label_text_font": "OrstedSansRegular",
            'major_label_text_color': "white",
            'axis_label_text_color': "white",
            'major_tick_line_color': "white",
            'minor_tick_line_color': "white",
            'axis_line_color': "white",
        }
    }
})
hv.renderer('bokeh').theme = theme

import pandas as pd
import hvplot.pandas
data = {"Orsted": [1,2], "y": [2,3]}
dataframe = pd.DataFrame(data)
plot=dataframe.hvplot(x="Orsted", y="y").options(title="Orsted")

CSS = """
@font-face {
  font-family: "OrstedSansRegular";
  src: url("https://orstedcdn.azureedge.net/Assets_/dist/fonts/OrstedSans-Regular.eot");
  /* IE9 Compat Modes */
  src: url("https://orstedcdn.azureedge.net/Assets_/dist/fonts/OrstedSans-Regular.eot?#iefix") format("embedded-opentype"), url("https://orstedcdn.azureedge.net/Assets_/dist/fonts/OrstedSans-Regular.woff") format("woff"), url("https://orstedcdn.azureedge.net/Assets_/dist/fonts/OrstedSans-Regular.ttf") format("truetype"), url("https://orstedcdn.azureedge.net/Assets_/dist/fonts/OrstedSans-Regular.svg#52bc9fd504b621176d57f308965c98a4") format("svg");
  /* Legacy iOS */
  font-style: normal;
  font-weight: 400;
}
@font-face {
  font-family: "OrstedSansLight";
  src: url("https://orstedcdn.azureedge.net/Assets_/dist/fonts/OrstedSans-Light.eot");
  /* IE9 Compat Modes */
  src: url("https://orstedcdn.azureedge.net/Assets_/dist/fonts/OrstedSans-Light.eot?#iefix") format("embedded-opentype"), url("https://orstedcdn.azureedge.net/Assets_/dist/fonts/OrstedSans-Light.woff") format("woff"), url("https://orstedcdn.azureedge.net/Assets_/dist/fonts/OrstedSans-Light.ttf") format("truetype"), url("https://orstedcdn.azureedge.net/Assets_/dist/fonts/OrstedSans-Light.svg#52bc9fd504b621176d57f308965c98a4") format("svg");
  /* Legacy iOS */
  font-style: normal;
  font-weight: 200;
}
@font-face {
  font-family: "OrstedSansBold";
  src: url("https://orstedcdn.azureedge.net/Assets_/dist/fonts/OrstedSans-Bold.eot");
  /* IE9 Compat Modes */
  src: url("https://orstedcdn.azureedge.net/Assets_/dist/fonts/OrstedSans-Bold.eot?#iefix") format("embedded-opentype"), url("https://orstedcdn.azureedge.net/Assets_/dist/fonts/OrstedSans-Bold.woff") format("woff"), url("https://orstedcdn.azureedge.net/Assets_/dist/fonts/OrstedSans-Bold.ttf") format("truetype"), url("https://orstedcdn.azureedge.net/Assets_/dist/fonts/OrstedSans-Bold.svg#52bc9fd504b621176d57f308965c98a4") format("svg");
  /* Legacy iOS */
  font-style: normal;
  font-weight: 800;
}
@font-face {
  font-family: "OrstedSansBlack";
  src: url("https://orstedcdn.azureedge.net/Assets_/dist/fonts/OrstedSans-Black.eot");
  /* IE9 Compat Modes */
  src: url("https://orstedcdn.azureedge.net/Assets_/dist/fonts/OrstedSans-Black.eot?#iefix") format("embedded-opentype"), url("https://orstedcdn.azureedge.net/Assets_/dist/fonts/OrstedSans-Black.woff") format("woff"), url("https://orstedcdn.azureedge.net/Assets_/dist/fonts/OrstedSans-Black.ttf") format("truetype"), url("https://orstedcdn.azureedge.net/Assets_/dist/fonts/OrstedSans-Black.svg#52bc9fd504b621176d57f308965c98a4") format("svg");
  /* Legacy iOS */
  font-style: normal;
  font-weight: 900;
}
@font-face {
  font-family: "OrstedSansItalic";
  src: url("https://orstedcdn.azureedge.net/Assets_/dist/fonts/OrstedSans-Italic.eot");
  /* IE9 Compat Modes */
  src: url("https://orstedcdn.azureedge.net/Assets_/dist/fonts/OrstedSans-Italic.eot?#iefix") format("embedded-opentype"), url("https://orstedcdn.azureedge.net/Assets_/dist/fonts/OrstedSans-Italic.woff") format("woff"), url("https://orstedcdn.azureedge.net/Assets_/dist/fonts/OrstedSans-Italic.ttf") format("truetype"), url("https://orstedcdn.azureedge.net/Assets_/dist/fonts/OrstedSans-Italic.svg#52bc9fd504b621176d57f308965c98a4") format("svg");
  /* Legacy iOS */
  font-style: italic;
  font-weight: 400;
}
"""
style=f"""
<style>
{CSS}
div.orsted {{
    font-family: OrstedSansRegular
}}
</style>
<div class="orsted">Orsted 1.2 1.4 1.6</div>
"""
import panel
panel.config.sizing_mode="stretch_width"
panel.Column(panel.pane.HTML(style), panel.Pane(plot)).show()

Sorry to hijack this thread, but how do you include another font in the first place? I.e. how do you discover font files/fonts installed on the computer and specify using them in holoviews?

Well i believe its about fonts available in the browser. Not nescessarily about installed fonts.

Hello, I’m very interested to see this topic revived (and that we solve this together :)). Do you experience too that by refreshing the webapp page then your holoviews plot renders with the right font?

To make things a bit clearer I provoke a redraw with a button push in the following hopefully reproducible code.

import panel as pn
import holoviews as hv
import param
from bokeh.themes.theme import Theme

theme = {
        'attrs' : {
            'Axis': {
            'major_label_text_color': 'green',
            'major_label_text_font' : 'scriptlook',
            'major_label_text_font_size': '18pt',
            'axis_label_text_color': 'red',
            'axis_label_text_font': 'scriptlook'
            },
            'Title': {
                'text_font': 'scriptlook',
                'text_color': 'blue',
                'text_alpha' : 1.0
            }
        }
}

hv.renderer('bokeh').theme = Theme(json=theme)

css = '''
@font-face {
 font-family: "scriptlook";
 src: url("http://fonts.gstatic.com/s/architectsdaughter/v11/KtkxAKiDZI_td1Lkx62xHZHDtgO_Y-bvfY5q4szgE-Q.ttf");
}

.bk.bk-btn {
    background-color: lightblue;
    font-family : "scriptlook"
}'''
        

pn.extension(raw_css=[css])


class visufont(param.Parameterized):

    action = param.Action(lambda x: x.param.trigger('action'), label='Click here!')
        
    def __init__(self, **params):
        super().__init__(**params)
        h = hv.Scatter([(0,0),(1,1)])
        h = h.opts(title='Scatter',fontsize=20,width=400,height=300,toolbar=None)
        self.view = pn.pane.HoloViews(h) 
                    
    @param.depends('action',watch=True)
    def view2(self):
        self.view.object = self.view.object
            
vf = visufont()
pn.Row(vf.param,vf.view).show() 

What we experience here is that with the first rendering, the css style applies since the colors of the letters are right. The font is right in the button, and the font is not right in the holoviews pane.
By clicking the button, it rerenders and now the font is correct (same by refreshing the page).

Is there a way to provoke this redraw? Is there a function like __init__
to put stuff that needs to happen only when all static resources are available?

I finally found a way by using the add_periodic_callback method. Setting period=10 and count=20 seems ok to minimize (but not avoid) the flash when the figure finally updates and to leave enough time for the font to be ready to be used.

vf = visufont()
v = pn.Row(vf.param,vf.view)
v.add_periodic_callback(vf.view2,period=10,count=20)
v.show()

Warning, that syntax evolves with panel 0.10 and will be pn.state.add_periodic_callback(...).

Questions : is there a way to trigger a callback when the static resources are available for use? Is there a way to avoid the FOUC (flash of unstyled content)?

1 Like

Hi @marcbernot.

I’ve also tried loading a custom font without problems.

But maybe you could look into non-Panel techniques for not rendering before the fonts have loaded. Like for example https://developer.mozilla.org/en-US/docs/Web/HTML/Preloading_content and http://www.bramstein.com/writing/preload-hints-for-web-fonts.html?

UPDATE: I just noticed, that this is only applied when browser is refreshed after initial visit.
This code imports a font (from a url) that is applied to holoview’s gridmatrix and hv.table:

import bokeh
from bokeh.themes.theme import Theme
from bokeh.sampledata.iris import flowers

import panel as pn
import holoviews as hv
from holoviews.operation import gridmatrix

FONT = "source sans pro"
theme = Theme(
    json = {
    "attrs": {
        "Axis": {
            "major_tick_line_alpha": 0,
            "major_tick_line_color": "#3C3E42",

            "minor_tick_line_alpha": .5,
            "minor_tick_line_color": "#3C3E42",

            "axis_line_alpha": 0,
            "axis_line_color": "#3C3E42",

            "major_label_text_color": "#3C3E42",
            "major_label_text_font": FONT,
            "major_label_text_font_size": "1.025em",

            "axis_label_standoff": 10,
            "axis_label_text_color": "#3C3E42",
            "axis_label_text_font": FONT,
            "axis_label_text_font_size": "1.03em",
            "axis_label_text_font_style": "normal",
            "axis_label_text_align": "left",
            "axis_label_text_baseline": "top",
        },

        "Title": {
            "text_color": "#3C3E42",
            "text_font": FONT,
            "text_font_size": "1.15em"
        }
    }
})

css = '''
@import url('https://fonts.googleapis.com/css2?family=Source+Sans+Pro&display=swap');

// html is needed for gridmatrix's diagonal plot at left-bottom position
html {
    overflow: hidden;
    font-family: 'Source Sans Pro', sans-serif !important;
}
.bk-root {
    font-family: 'Source Sans Pro', sans-serif !important;
}
'''

hv.extension('bokeh')
hv.renderer('bokeh').theme = theme
pn.extension(raw_css=[css], comms='vscode')

print('holoviews: ', hv.__version__)
print('panel:     ', pn.__version__)
print('bokeh:     ', bokeh.__version__)
# holoviews:  1.14.1
# panel:      0.10.2
# bokeh:      2.2.3

iris_ds = hv.Dataset(flowers, kdims=['sepal_length','sepal_width','petal_length','petal_width'])

grid = gridmatrix(iris_ds).opts(title='Flowers - Block #123')

table = hv.Table(iris_ds)

pn.Row((grid + table)).servable(title='Demo font loading')

Although subtle in appearance compared to default font, compare the ‘l’ in Flowers and ‘g’ in length. Source sans pro is shown in bottom portion of image.

1 Like

It seems to me that @marckassay code still has the same problem as we already had, i.e. need of a refresh.
I looked at this with the browser inspection tools and the problem is that the loading of the font of the bokeh plot starts at the beginning of the rendering of the bokeh plot and is not available in due time.
So I checked on preloading techniques as suggested by @Marc.
Here is the resulting code with no need of refresh and no FOUC.

import panel as pn
import holoviews as hv
from bokeh.themes import Theme as _BkTheme
pn.extension()

font_url = "https://fonts.gstatic.com/s/architectsdaughter/v11/KtkxAKiDZI_td1Lkx62xHZHDtgO_Y-bvTYlg4w.woff2"
font_local_name = "scriptlook"

font_preload = f'<link rel="preload" as="font" href="{font_url}" crossorigin="anonymous">'

css = f'''<style>
@font-face {{
font-family: {font_local_name};
src: url({font_url}) format("woff2");}}
</style>
'''

template = f'''
{{% extends base %}}

{{% block preamble %}}
{font_preload}
{css}
{{% endblock %}}
'''

tmpl = pn.Template(template)

font_theme = {
        'attrs' : {
            'Axis': {
            'major_label_text_color': 'green',
            'major_label_text_font' : font_local_name,
            'major_label_text_font_size': '18pt',
            'axis_label_text_color': 'red',
            'axis_label_text_font': font_local_name
            },
            'Title': {
                'text_font': font_local_name,
                'text_color': 'blue',
                'text_alpha' : 1.0
            }
        }
}

theme = _BkTheme(json=font_theme)

hvr = hv.renderer('bokeh')
hvr.theme = theme

h = hv.Scatter([(0,0),(1,1)])
h = h.opts(title='Scatter',fontsize=30,width=800,height=400,toolbar=None)

tmpl.add_panel('A', pn.pane.HTML("<h1>Custom font</h1>"))
tmpl.add_panel('B', pn.pane.HoloViews(h))
tmpl.show(title='custom font example')

This is done through a custom template.
For a cleaner code, you can put bokeh style, css and the font in an asset directory and use the asset declaration mechanism of panel.

import panel as pn
import holoviews as hv
from bokeh.themes import Theme as _BkTheme
pn.extension()

template = '''
{% extends base %}

{% block preamble %}
<link rel="preload" as="font" href="assets/ArchitectsDaughter-Regular.ttf" crossorigin="anonymous">
<link rel="preload" as="style" href="assets/font.css" >
<link rel="stylesheet" href="assets/font.css" >
{% endblock %}
'''

tmpl = pn.Template(template)

theme = _BkTheme(filename = 'assets/font_theme.yaml')
hvr = hv.renderer('bokeh')
hvr.theme = theme

h = hv.Scatter([(0,0),(1,1)])
h = h.opts(title='Scatter',fontsize=30,width=800,height=400,toolbar=None)

tmpl.add_panel('A', pn.pane.HTML("<h1>Custom font</h1>"))
tmpl.add_panel('B', pn.pane.HoloViews(h))
tmpl.show(title='custom font example',static_dirs={'assets': './assets'})

where font.css is

@font-face {
 font-family: "scriptlook";
 src: url("/assets/ArchitectsDaughter-Regular.ttf")
}

and font_theme.yaml is

attrs :
    Axis:
        major_label_text_color: 'green'
        major_label_text_font: scriptlook
        major_label_text_font_size: '18pt'
        axis_label_text_color: 'red'
        axis_label_text_font: scriptlook
    Title:
        text_font: scriptlook
        text_color: 'blue'
        text_alpha : 1.0

Notice that to make this works, you have to preload the font and the css file, AND you also have to declare that you use this preloaded css file, i.e. you need all this :

{% block preamble %}
<link rel="preload" as="font" href="assets/ArchitectsDaughter-Regular.ttf" crossorigin="anonymous">
<link rel="preload" as="style" href="assets/font.css">
<link rel="stylesheet" href="assets/font.css">
{% endblock %}
3 Likes