Template+Embed theme + Additional JS Resources + "Manual Theme Override" Voodoo

This is kind of building on this thread (has been closed since): Importing additional JS resources to standalone html document - #2 by Bryan

Now this post is not about getting something to work, but that something I built works and I don’t know why/how! And I guarantee it’s simply knowing nothing about html/css, but if someone can explain this apparent voodoo to me it would greatly assist with my learning and understanding :slight_smile:

What I’m trying to do:

Marry the standalone_embed.py example (as referred to by Bryan in the thread above) to my “save_html_wJSResources” function which provides me/other people building bokeh applications with what is essentially an “enhanced” bokeh.plotting.save function that allows for embedding of custom themes AND additional Javascript resources (for example d3.js as demo’ed in the example below).

Now the only “gotcha” to this is that there will be guaranteed cases where the person building the app wants to “manually override” the theme for just one model. Obviously since every layout is different there will be cases where individual models will need to be altered, font sizes shrunk to fit etc.

MRE/What I’ve built

from jinja2 import Template
from bokeh.io import curdoc
from bokeh.palettes import Spectral
from bokeh.plotting import figure, show
from bokeh.themes import Theme
from bokeh.embed import components
from bokeh.resources import Resources

#make a hypothetical theme
theme = Theme(json={'attrs':{
                        'Axis':{'axis_label_text_font':'arial'
                                ,'axis_label_text_font_style':'bold'
                                ,'axis_label_text_font_size':'14pt'
                                ,'major_label_text_font':'arial'
                                ,'major_label_text_font_style':'normal'
                                ,'major_label_text_font_size':'10pt'
                                }
                        }
    }
    )

#dummy plot
x_f = [1.5, 2, 9]
y_f = [3, 3, 3.1]
p = figure(plot_width=400, plot_height=400)
p.line(x_f, y_f, line_width=3, color=Spectral[4][0])
p.axis[0].axis_label='testing'
p.axis[1].axis_label='something else'
#hard code override one of the axis label font sizes
p.axis[1].axis_label_text_font_size='25pt'

def save_html_wJSResources2(bk_obj,fname,resources_list_dict,html_title='Bokeh Plot',theme=None):
    '''function to save a bokeh figure/layout/widget but with additional JS resources imported at the top of the html
        resources_list_dict is a dict input of where to import additional JS libs/scripts so they can be utilized into CustomJS etc in bokeh work
        e.g. {'sources':['http://d3js.org/d3.v6.js'],'scripts':[var_storing_jsscript]}
        theme arg is to pass a custom theme
        '''
    if theme is None:
        script, div = components(bk_obj)
    else:
        script, div = components(bk_obj, theme=theme)
    
    template = Template('''<!DOCTYPE html>
    <html lang="en">
        <head>
            <meta charset="utf-8">
            <title>'''+html_title+'''</title>
            {{ resources }}
            {{ script }}       
        </head>
        <body>
            <div>
            {{ div }}
            </div>
        </body>
    </html>
    ''')
    
    resources = Resources().render()
    
    for r in resources_list_dict['sources']:
        resources = resources +'''\n<script type='text/javascript' src="'''+r+'''"></script>'''
    for r in resources_list_dict['scripts']:
        resources = resources +'''\n<script type='text/javascript'> '''+r+'''</script>'''  
    
    html = template.render(resources=resources,
                           script=script,
                           div=div)
    print('writing '+fname+'...')
    with open(fname, mode="w", encoding="utf-8") as f:
        f.write(html)

save_html_wJSResources2(bk_obj=p,fname='Magic.html',resources_list_dict={'sources':['http://d3js.org/d3.v6.js'],'scripts':[]}
                                    ,html_title='Magic',theme=theme)

What I Expected to Happen

Since I apply the “manual override” “first” in the code and apply the theme when I save the html “after” (during the call to save_html_wJSResources2), I expected the theme to “overwrite” my manual update (basically the two axis labels would be the same font size (14pt).

What Actually Happens

The manual override actually works!

image

So what do I think is actually happening then?

From what I can tell, there has to be some sort of “default” setting that gets written based on the applied theme… but if a particular model has been “manually overridden” by the user, a switch internally is triggered, signalling that it has been “hard coded” by the user, and therefore the default no longer applies to it, and the applied theme leaves it alone? Am I thinking correctly about this?

Follow up questions:

  • Was this sort of setup was completely intentional by the devs? and
  • Is this kind of setup the defacto standard for templating stuff on web apps?

Follow up statement from me regardless of the answers I get to these questions:

  • That is freaking clever and the devs are way smarter than me :smiley:
1 Like

Yes that is more-or-less correct. Themes provide new default values but explicitly-set property values always take precedence over default values, regardless of how those defaults came to be.

I would say that yes it is intentional but also note the the primary motivation for having to keep track of when things have or have-not been explicitly set by users is so that when things are serialized and saved or sent over a websocket, we only have to send just those user changes [1] [2]


  1. “new” defaults e.g. from themes also have to be sent, but they would still be overridden by user values. ↩︎

  2. There are extensive tests that maintain that the “default defaults” between Python Bokeh and BokehJS are always identical. ↩︎

1 Like

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.