Custom CSS with app on new Bokeh server

I’ve been learning how to use the new Bokeh server (and loving it), and I’m trying to figure out how to apply some custom css to style the app I create. For example, if I implement the example here:

And deploy it in the recommended way:

bokeh serve --show myapp.py

I get an app at:

http://localhost:5006/myapp

How can I specify the CSS for the resulting page? In the old bokeh server, I could point bokeh_app to custom jinja templates, but bokeh_app is no longer used. I believe there are things coming with phosphorJS, etc. to address some of this, but is there anything I can use in the meantime?

Hi Schaun,

Thanks for the kind words. Right now you should be able to embed a Bokeh server document in another page that you control and create. It was pointed out today that this section in the docs needs updating, but you can follow this example:

  https://github.com/bokeh/bokeh/blob/master/examples/embed/widget.py

to hopefully see how it could be done.

Thanks,

Bryan

···

On Jan 8, 2016, at 9:09 AM, [email protected] wrote:

I've been learning how to use the new Bokeh server (and loving it), and I'm trying to figure out how to apply some custom css to style the app I create. For example, if I implement the example here:

Bokeh server — Bokeh 3.3.2 Documentation

And deploy it in the recommended way:

bokeh serve --show myapp.py

I get an app at:

http://localhost:5006/myapp

How can I specify the CSS for the resulting page? In the old bokeh server, I could point bokeh_app to custom jinja templates, but bokeh_app is no longer used. I believe there are things coming with phosphorJS, etc. to address some of this, but is there anything I can use in the meantime?

--
You received this message because you are subscribed to the Google Groups "Bokeh Discussion - Public" group.
To unsubscribe from this group and stop receiving emails from it, send an email to [email protected].
To post to this group, send email to [email protected].
To view this discussion on the web visit https://groups.google.com/a/continuum.io/d/msgid/bokeh/d9147505-228a-49b1-b012-aabeef147fa6%40continuum.io\.
For more options, visit https://groups.google.com/a/continuum.io/d/optout\.

I implemented an app using the widget.py example, and it allows me to customize the CSS just fine, but it only allows a single, shared state - if I open the link in two browsers, a change in one creates changes in the other. My use case will involve many people accessing an app at a single time. I know that’s what the bokeh serve method for deploying an app was created for, but I can’t figure out how to customize the CSS in that case. Does bokeh serve use a template to create the web page when the app is called? Is there a way to point it to a different template, the way I could with the former bokeh server? Or is there a way to deploy using autoload_server like in the widget.py example, but to allow multiple users to interact with the visualization independently at the same time?

Thanks,

Schaun

···

On Friday, January 8, 2016 at 10:22:49 AM UTC-5, Bryan Van de ven wrote:

Hi Schaun,

Thanks for the kind words. Right now you should be able to embed a Bokeh server document in another page that you control and create. It was pointed out today that this section in the docs needs updating, but you can follow this example:

    [https://github.com/bokeh/bokeh/blob/master/examples/embed/widget.py](https://github.com/bokeh/bokeh/blob/master/examples/embed/widget.py)

to hopefully see how it could be done.

Thanks,

Bryan

On Jan 8, 2016, at 9:09 AM, [email protected] wrote:

I’ve been learning how to use the new Bokeh server (and loving it), and I’m trying to figure out how to apply some custom css to style the app I create. For example, if I implement the example here:

http://bokeh.pydata.org/en/latest/docs/user_guide/server.html#single-module-format

And deploy it in the recommended way:

bokeh serve --show myapp.py

I get an app at:

http://localhost:5006/myapp

How can I specify the CSS for the resulting page? In the old bokeh server, I could point bokeh_app to custom jinja templates, but bokeh_app is no longer used. I believe there are things coming with phosphorJS, etc. to address some of this, but is there anything I can use in the meantime?


You received this message because you are subscribed to the Google Groups “Bokeh Discussion - Public” group.

To unsubscribe from this group and stop receiving emails from it, send an email to [email protected].

To post to this group, send email to [email protected].

To view this discussion on the web visit https://groups.google.com/a/continuum.io/d/msgid/bokeh/d9147505-228a-49b1-b012-aabeef147fa6%40continuum.io.

For more options, visit https://groups.google.com/a/continuum.io/d/optout.

Hi,

···

On Wed, Jan 13, 2016 at 12:55 PM, [email protected] wrote:

I implemented an app using the widget.py example, and it allows me to customize the CSS just fine, but it only allows a single, shared state - if I open the link in two browsers, a change in one creates changes in the other. My use case will involve many people accessing an app at a single time. I know that’s what the bokeh serve method for deploying an app was created for, but I can’t figure out how to customize the CSS in that case. Does bokeh serve use a template to create the web page when the app is called? Is there a way to point it to a different template, the way I could with the former bokeh server? Or is there a way to deploy using autoload_server like in the widget.py example, but to allow multiple users to interact with the visualization independently at the same time?

You can use autoload_server with a bokeh serve app instead of using push_session, and set session_id=None when you call autoload_server to get a fresh session. But you do still need your own process that builds the page containing the autoload_server html.

bokeh serve does use a template but there’s no current way to change it. I think what we should have is essentially a curdoc().add_css() kind of thing, but that feature doesn’t exist yet. Then from a bokeh serve app you could add CSS. And in fact in an app directory, we could have a static/ subdir and if there’s css in there we auto-add it to the document. This is a pretty straightforward feature to add if anyone is feeling ambitious :slight_smile:

Another possibility is to make a custom server process; instead of bokeh serve do new Server() in your own main(). It’s also possible to add extra tornado routes when doing this. But I think it may be overkill here. Still, it could be what you want depending on what’s important to you.

Havoc

You can use autoload_server with a bokeh serve app instead of using push_session, and set session_id=None when you call autoload_server to get a fresh session. But you do still need your own process that builds the page containing the autoload_server html.

Do you have an example of what that looks like? When I set session_id=None, I got: “ValueError: A specific model was passed to autoload_server() but no session_id; this doesn’t work because the server will generate a fresh session which won’t have the model in it.”

Also, if I don’t use push_session, how do I call session.loop_until_closed() at the end of the script?

···

On Wednesday, January 13, 2016 at 3:15:32 PM UTC-5, Havoc Pennington wrote:

Hi,

On Wed, Jan 13, 2016 at 12:55 PM, [email protected] wrote:

I implemented an app using the widget.py example, and it allows me to customize the CSS just fine, but it only allows a single, shared state - if I open the link in two browsers, a change in one creates changes in the other. My use case will involve many people accessing an app at a single time. I know that’s what the bokeh serve method for deploying an app was created for, but I can’t figure out how to customize the CSS in that case. Does bokeh serve use a template to create the web page when the app is called? Is there a way to point it to a different template, the way I could with the former bokeh server? Or is there a way to deploy using autoload_server like in the widget.py example, but to allow multiple users to interact with the visualization independently at the same time?

You can use autoload_server with a bokeh serve app instead of using push_session, and set session_id=None when you call autoload_server to get a fresh session. But you do still need your own process that builds the page containing the autoload_server html.

bokeh serve does use a template but there’s no current way to change it. I think what we should have is essentially a curdoc().add_css() kind of thing, but that feature doesn’t exist yet. Then from a bokeh serve app you could add CSS. And in fact in an app directory, we could have a static/ subdir and if there’s css in there we auto-add it to the document. This is a pretty straightforward feature to add if anyone is feeling ambitious :slight_smile:

Another possibility is to make a custom server process; instead of bokeh serve do new Server() in your own main(). It’s also possible to add extra tornado routes when doing this. But I think it may be overkill here. Still, it could be what you want depending on what’s important to you.

Havoc

And, now that I think about it, would you be able to give me a brief, high-level sketch of what a curdoc().add_css() method might look like? I may be able to work up a pull request if I had some pointers.

···

On Wednesday, January 13, 2016 at 5:42:47 PM UTC-5, [email protected] wrote:

You can use autoload_server with a bokeh serve app instead of using push_session, and set session_id=None when you call autoload_server to get a fresh session. But you do still need your own process that builds the page containing the autoload_server html.

Do you have an example of what that looks like? When I set session_id=None, I got: “ValueError: A specific model was passed to autoload_server() but no session_id; this doesn’t work because the server will generate a fresh session which won’t have the model in it.”

Also, if I don’t use push_session, how do I call session.loop_until_closed() at the end of the script?

On Wednesday, January 13, 2016 at 3:15:32 PM UTC-5, Havoc Pennington wrote:

Hi,

On Wed, Jan 13, 2016 at 12:55 PM, [email protected] wrote:

I implemented an app using the widget.py example, and it allows me to customize the CSS just fine, but it only allows a single, shared state - if I open the link in two browsers, a change in one creates changes in the other. My use case will involve many people accessing an app at a single time. I know that’s what the bokeh serve method for deploying an app was created for, but I can’t figure out how to customize the CSS in that case. Does bokeh serve use a template to create the web page when the app is called? Is there a way to point it to a different template, the way I could with the former bokeh server? Or is there a way to deploy using autoload_server like in the widget.py example, but to allow multiple users to interact with the visualization independently at the same time?

You can use autoload_server with a bokeh serve app instead of using push_session, and set session_id=None when you call autoload_server to get a fresh session. But you do still need your own process that builds the page containing the autoload_server html.

bokeh serve does use a template but there’s no current way to change it. I think what we should have is essentially a curdoc().add_css() kind of thing, but that feature doesn’t exist yet. Then from a bokeh serve app you could add CSS. And in fact in an app directory, we could have a static/ subdir and if there’s css in there we auto-add it to the document. This is a pretty straightforward feature to add if anyone is feeling ambitious :slight_smile:

Another possibility is to make a custom server process; instead of bokeh serve do new Server() in your own main(). It’s also possible to add extra tornado routes when doing this. But I think it may be overkill here. Still, it could be what you want depending on what’s important to you.

Havoc

You can use autoload_server with a `bokeh serve` app instead of using

push_session, and set session_id=None when you call autoload_server to get
a fresh session. But you do still need your own process that builds the
page containing the autoload_server html.

Do you have an example of what that looks like? When I set
session_id=None, I got: "ValueError: A specific model was passed to
autoload_server() but no session_id; this doesn't work because the server
will generate a fresh session which won't have the model in it."

To do session_id=None you have to also pass in model=None (to load the
entire document) because you don't have the model ID that will be in the
new session. Each session has its own Document and its own models.

A future patch idea could be to allow providing a model _name_, the server
could then set model.name="foo" for the appropriate model in each session.
I just thought of that now so obviously haven't thought it through, but
it'd be a matter of passing model_name in to autoload_server then passing
that down through embed.py so it ends up in the "render items" and then
embed.coffee would do a get_model_by_name() instead of its current lookup
by ID, if it gets a name instead of an ID. Could be handy for this sort of
scenario.

A workaround in the meantime could be to have multiple server apps (you can
load multiple in one invocation of bokeh serve) and put different plots in
each, since you can give autoload_server the app_path.

Also, if I don't use push_session, how do I call

`session.loop_until_closed()` at the end of the script?

If you fill in the document contents on the server side, you probably want
to pull_session() in the client to get the doc.

I don't know if there's a pull_session example or not but search for
pull_session in the examples directory. It works exactly like push_session
except you get the server's document as session.document, instead of
sending the server a document. Whether you start with a push or a pull,
once the session is open either side can make changes and the other side
will see them.

You may not need to open a session at all, depending on what your app does.
You don't have to open a session in order to autoload_server, only if you
want to track or modify the session document from a client.

Regarding the add_css() thing, here's roughly how I'd approach it I think,
though of course Bryan or others may have different ideas or you may
discover I'm wrong if you dig into the implementation.

This already-applied PR adding Document.title illustrates how to add a new
thing to Document:

The property has to be added to the Document in both Python and Coffee,
then an event to track changes to it, and code to sync the changes
client-server.

In that approach, I guess you would add a Document.css property with a list
of css strings in it, to both the python-side and coffee-side Document
classes. On the client side when we render the doc (see

) we would also render the css props ... they would go to <style> elements
and be placed in the page head. When the css list changed, it would be
necessary to add/remove/update any existing style elements.

To support CSS in an application directory, it would work similarly to the
theme.yaml support. See here where we load theme.yaml:

and here we add it to each Document as we create sessions:

So similarly you'd load up CSS files and add them to the document. It would
be necessary to adopt a convention, either css/*.css or *.css or
static/*.css or I don't know, and the DirectoryHandler would use that
convention to auto-add the CSS.

Two alternative design ideas:

* instead of a special Document.css property, have a CSS Model, and do
`curdoc().add_root(CSS(css_string))`; in embed.coffee, we'd have to be
smart enough to add the view for a "CSS" model to the <head> instead of to
the usual element. Advantage: no custom CSSChangedEvent, no custom
property, etc. Disadvantage: this model is "weird" and not quite like other
models (but does that really matter?).

* instead of inline CSS strings, add Tornado routes for each chunk of CSS
and include them as links (any advantage to this? not sure, seems like it's
probably harder to do it this way)

The internals docs at
http://bokeh.pydata.org/en/latest/docs/dev_guide/server.html could be
helpful.

Havoc

···

On Wed, Jan 13, 2016 at 5:42 PM, <[email protected]> wrote:

So similarly you’d load up CSS files and add them to the document. It would be necessary to adopt a convention, either css/*.css or .css or static/.css or I don’t know, and the DirectoryHandler would use that convention to auto-add the CSS.

Has this been done? I see an example where the directory structure is organized with static/css/*.css, which was added after this thread, but the *.css files doesn’t seem to be served automatically (with Bokeh 0.12.3).

···

On Wednesday, January 13, 2016 at 9:11:39 PM UTC-5, Havoc Pennington wrote:

On Wed, Jan 13, 2016 at 5:42 PM, [email protected] wrote:

You can use autoload_server with a bokeh serve app instead of using push_session, and set session_id=None when you call autoload_server to get a fresh session. But you do still need your own process that builds the page containing the autoload_server html.

Do you have an example of what that looks like? When I set session_id=None, I got: “ValueError: A specific model was passed to autoload_server() but no session_id; this doesn’t work because the server will generate a fresh session which won’t have the model in it.”

To do session_id=None you have to also pass in model=None (to load the entire document) because you don’t have the model ID that will be in the new session. Each session has its own Document and its own models.

A future patch idea could be to allow providing a model name, the server could then set model.name=“foo” for the appropriate model in each session. I just thought of that now so obviously haven’t thought it through, but it’d be a matter of passing model_name in to autoload_server then passing that down through embed.py so it ends up in the “render items” and then embed.coffee would do a get_model_by_name() instead of its current lookup by ID, if it gets a name instead of an ID. Could be handy for this sort of scenario.

A workaround in the meantime could be to have multiple server apps (you can load multiple in one invocation of bokeh serve) and put different plots in each, since you can give autoload_server the app_path.

Also, if I don’t use push_session, how do I call session.loop_until_closed() at the end of the script?

If you fill in the document contents on the server side, you probably want to pull_session() in the client to get the doc.

I don’t know if there’s a pull_session example or not but search for pull_session in the examples directory. It works exactly like push_session except you get the server’s document as session.document, instead of sending the server a document. Whether you start with a push or a pull, once the session is open either side can make changes and the other side will see them.

You may not need to open a session at all, depending on what your app does. You don’t have to open a session in order to autoload_server, only if you want to track or modify the session document from a client.

Regarding the add_css() thing, here’s roughly how I’d approach it I think, though of course Bryan or others may have different ideas or you may discover I’m wrong if you dig into the implementation.

This already-applied PR adding Document.title illustrates how to add a new thing to Document:

https://github.com/bokeh/bokeh/pull/3090

The property has to be added to the Document in both Python and Coffee, then an event to track changes to it, and code to sync the changes client-server.

In that approach, I guess you would add a Document.css property with a list of css strings in it, to both the python-side and coffee-side Document classes. On the client side when we render the doc (see https://github.com/bokeh/bokeh/blob/a0a419dd67f068ad074555ee554ad9a3e0354b5b/bokehjs/src/coffee/common/embed.coffee#L45-L73 ) we would also render the css props … they would go to elements and be placed in the page head. When the css list changed, it would be necessary to add/remove/update any existing style elements.

To support CSS in an application directory, it would work similarly to the theme.yaml support. See here where we load theme.yaml:

https://github.com/bokeh/bokeh/blob/a0a419dd67f068ad074555ee554ad9a3e0354b5b/bokeh/application/handlers/directory.py#L32-L36

and here we add it to each Document as we create sessions:

https://github.com/bokeh/bokeh/blob/a0a419dd67f068ad074555ee554ad9a3e0354b5b/bokeh/application/handlers/directory.py#L50-L51

So similarly you’d load up CSS files and add them to the document. It would be necessary to adopt a convention, either css/*.css or .css or static/.css or I don’t know, and the DirectoryHandler would use that convention to auto-add the CSS.

Two alternative design ideas:

  • instead of a special Document.css property, have a CSS Model, and do curdoc().add_root(CSS(css_string)); in embed.coffee, we’d have to be smart enough to add the view for a “CSS” model to the instead of to the usual element. Advantage: no custom CSSChangedEvent, no custom property, etc. Disadvantage: this model is “weird” and not quite like other models (but does that really matter?).
  • instead of inline CSS strings, add Tornado routes for each chunk of CSS and include them as links (any advantage to this? not sure, seems like it’s probably harder to do it this way)

The internals docs at http://bokeh.pydata.org/en/latest/docs/dev_guide/server.html could be helpful.

Havoc

I know this is a bit dated, but any arbitrary custom css can be added directly to the document from a JS callback, without having to use an external css file. To do that you can use two dummy widgets in a dummy widgetbox that is hidden after a timeout callback and adds the custom style.

Here is an example that prevents all the hovertools to display stacked tooltips even when many data points are hit by the mouse:

from random import random

import time

from bokeh.io import curdoc
from bokeh.plotting import figure
from bokeh.models import Div, CustomJS, TextInput, HoverTool, ColumnDataSource
from bokeh.layouts import gridplot, widgetbox

dum_div = Div(text=‘hidden’) # just used to find the dummy widgetbox via its textContent
dum_input = TextInput() # used to trigger arbitrary JS

source = ColumnDataSource(data={‘x’:range(10000),‘y’:[random() for i in range(10000)]})

fig = figure(tools=‘hover,wheel_zoom’)
fig.scatter(x=‘x’,y=‘y’,hover_color=‘red’,source=source)

def dum_hide():
dum_input.value = str(time.time())

dum_input_code="""
// hide the dummy widgetbox
widgetboxes = document.getElementsByClassName(“bk-widget-box”);

for(i=0;i<widgetboxes.length;i++){
if(widgetboxes[i].textContent.includes(‘hidden’)){widgetboxes[i].style.display=“none”}
}

// prevents hovertools from stacking tooltips on clumped data
var css = document.createElement(“style”);
css.type = “text/css”;
css.innerHTML = “.bk-tooltip>div:not(:first-child) { display: none }”;
document.body.appendChild(css);
“”"
dum_input.js_on_change(‘value’, CustomJS(code=dum_input_code))

dum_box = widgetbox(dum_div,dum_input,width=200)

grid = gridplot([[fig],[dum_box]])

curdoc().add_root(grid)
curdoc().add_timeout_callback(dum_hide,1000)

``

1 Like