Is it possible to save the bokeh sessions?

Hi, Assuming the an interactive document has been created by a user using a bokeh server, is it possible to save the session on the sever (or client) so that the user can reload (from a new browser window) the document to get the previous plots and widget settings? Thanks

I don’t think there is a built-in way to do that. But it’s possible with some work; here I have a hacky code where I load a previous document that was saved as a json string:

https://bitbucket.org/rocheseb/bokeh/src/default/applications/save_and_load/

It can become messy and difficult depending of how interactive your app is, as you need to reattribute callbacks to widgets, and you also need to change the document of all loaded elements to the current document.

A smarter way is to have your document construct itself based on a data structure. So it would read in e.g. from a .npy or netcdf file and make all necessary plots and widgets based on that data, so you can make ‘saving’ and ‘loading’ a session just about your data structure.

forgott o mention that I didn’t test past bokeh 0.12.10

···

Le jeudi 22 février 2018 14:57:37 UTC-5, Sébastien Roche a écrit :

I don’t think there is a built-in way to do that. But it’s possible with some work; here I have a hacky code where I load a previous document that was saved as a json string:

https://bitbucket.org/rocheseb/bokeh/src/default/applications/save_and_load/

It can become messy and difficult depending of how interactive your app is, as you need to reattribute callbacks to widgets, and you also need to change the document of all loaded elements to the current document.

A smarter way is to have your document construct itself based on a data structure. So it would read in e.g. from a .npy or netcdf file and make all necessary plots and widgets based on that data, so you can make ‘saving’ and ‘loading’ a session just about your data structure.

Thanks Sébastien for your reply. I think it would be a nice feature to save/load the session from json file straight forward. As you said it becomes messy and difficult to manage by re attributing the callbacks and reattaching them to the current document.

···

On Thursday, February 22, 2018 at 8:05:47 PM UTC, Sébastien Roche wrote:

forgott o mention that I didn’t test past bokeh 0.12.10

Le jeudi 22 février 2018 14:57:37 UTC-5, Sébastien Roche a écrit :

I don’t think there is a built-in way to do that. But it’s possible with some work; here I have a hacky code where I load a previous document that was saved as a json string:

https://bitbucket.org/rocheseb/bokeh/src/default/applications/save_and_load/

It can become messy and difficult depending of how interactive your app is, as you need to reattribute callbacks to widgets, and you also need to change the document of all loaded elements to the current document.

A smarter way is to have your document construct itself based on a data structure. So it would read in e.g. from a .npy or netcdf file and make all necessary plots and widgets based on that data, so you can make ‘saving’ and ‘loading’ a session just about your data structure.

To give you a simple example of the second option I mentioned:

This is for a folder app, you will need to place it in a directory that also has a subdirectory named “saved_sessions”

from bokeh.io import curdoc

from bokeh.plotting import figure

from bokeh.models import Button, ColumnDataSource, Select, TextInput, RadioButtonGroup

from bokeh.layouts import widgetbox, gridplot

from random import random

import os

import numpy as np

app_path = os.path.dirname(file) # the app should be in /linefit/lft145/lft_app

save_path = os.path.join(app_path,‘saved_sessions’)

data = {0:{‘x’:range(10),‘y’:range(10)}}

def save_session():

global data

save_name = curdoc().select_one({'name':'save_input'}).value

np.save(os.path.join(save_path,save_name+'.npy'), data)

curdoc().select_one({'name':'load_input'}).options = [' ']+os.listdir(save_path)

def load_session(attr,old,new):

global data

load_name = curdoc().select_one({'name':'load_input'}).value

data = np.load(os.path.join(save_path,load_name)).item()

doc_maker()

def add_stuff():

global data

new_key = max(data.keys())+1

data[new_key] = {'x':[random() for i in range(10)],'y':[random() for i in range(10)]}

curdoc().select_one({'name':'button_group'}).labels = [str(i) for i in sorted(data.keys())]

def doc_maker():

curdoc().clear()

save_input = TextInput(title='Save name:',name='save_input')

save_button = Button(label='Save')

save_button.on_click(save_session)

load_input = Select(value =' ',options=[' ']+os.listdir(save_path),name='load_input')

load_input.on_change('value',load_session)

add_button = Button(label='add stuff',name='add_button')

add_button.on_click(add_stuff)

button_group = RadioButtonGroup(labels=[str(i) for i in sorted(data.keys())],active=0,name='button_group')

button_group.on_change('active',update)

fig = figure()

source = ColumnDataSource(data=data[0])

fig.scatter('x','y',source=source,name='fig_scatter')

grid = gridplot([[button_group],[fig,widgetbox(load_input,save_input,save_button,add_button)]])

curdoc().add_root(grid)

def update(attr,old,new):

global data

curdoc().select_one({'name':'fig_scatter'}).data_source.data.update(data[new])

curdoc().title = “save_load”

doc_maker()

``

In this example the doc_maker() and clear() are not really needed, but this gives you the general idea. When I have multiple plots with multiples glyphrenderers I usually have a function update_doc() at the end of doc_maker() that will fill all the different ColumnDataSources from the structure of the dictionnary"data".

Hi,

I never tried, but maybe something like pickle.dump(curdoc, ‘session.txt’) / curdoc = pickle.load('session.txt) could work out of the box?

Just an idea

Daniel

···

2018-02-24 4:01 GMT+01:00 Sébastien Roche [email protected]:

To give you a simple example of the second option I mentioned:

This is for a folder app, you will need to place it in a directory that also has a subdirectory named “saved_sessions”

from bokeh.io import curdoc

from bokeh.plotting import figure

from bokeh.models import Button, ColumnDataSource, Select, TextInput, RadioButtonGroup

from bokeh.layouts import widgetbox, gridplot

from random import random

import os

import numpy as np

app_path = os.path.dirname(file) # the app should be in /linefit/lft145/lft_app

save_path = os.path.join(app_path,‘saved_sessions’)

data = {0:{‘x’:range(10),‘y’:range(10)}}

def save_session():

global data

save_name = curdoc().select_one({‘name’:‘save_input’}).value

np.save(os.path.join(save_path,save_name+‘.npy’), data)

curdoc().select_one({‘name’:‘load_input’}).options = [’ ']+os.listdir(save_path)

def load_session(attr,old,new):

global data

load_name = curdoc().select_one({‘name’:‘load_input’}).value

data = np.load(os.path.join(save_path,load_name)).item()

doc_maker()

def add_stuff():

global data

new_key = max(data.keys())+1

data[new_key] = {‘x’:[random() for i in range(10)],‘y’:[random() for i in range(10)]}

curdoc().select_one({‘name’:‘button_group’}).labels = [str(i) for i in sorted(data.keys())]

def doc_maker():

curdoc().clear()

save_input = TextInput(title=‘Save name:’,name=‘save_input’)

save_button = Button(label=‘Save’)

save_button.on_click(save_session)

load_input = Select(value =’ ‘,options=[’ ']+os.listdir(save_path),name=‘load_input’)

load_input.on_change(‘value’,load_session)

add_button = Button(label=‘add stuff’,name=‘add_button’)

add_button.on_click(add_stuff)

button_group = RadioButtonGroup(labels=[str(i) for i in sorted(data.keys())],active=0,name=‘button_group’)

button_group.on_change(‘active’,update)

fig = figure()

source = ColumnDataSource(data=data[0])

fig.scatter(‘x’,‘y’,source=source,name=‘fig_scatter’)

grid = gridplot([[button_group],[fig,widgetbox(load_input,save_input,save_button,add_button)]])

curdoc().add_root(grid)

def update(attr,old,new):

global data

curdoc().select_one({‘name’:‘fig_scatter’}).data_source.data.update(data[new])

curdoc().title = “save_load”

doc_maker()

``

In this example the doc_maker() and clear() are not really needed, but this gives you the general idea. When I have multiple plots with multiples glyphrenderers I usually have a function update_doc() at the end of doc_maker() that will fill all the different ColumnDataSources from the structure of the dictionnary"data".

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/72b194b0-d826-40ca-b0f9-23c2d3ed38d0%40continuum.io.

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

Hi,

Just to add a little context. This is definitely an area where Bokeh is agnostic. Any solution we might implement would almost certainly not be sufficient for a lot of users/use cases (restore based on user? On origin? Reset-able? After some timeout? Or other condition?) Yet any solution we make would add both complexity and additional package dependencies. Since the app itself can execute pretty much whatever arbitrary python code it wants to access files, databases, other APIS, etc., it definitely seems best to stay out of people's way and not try to dictate policy.

Thanks,

Bryan

···

On Feb 23, 2018, at 23:10, 'Daniel Krause' via Bokeh Discussion - Public <[email protected]> wrote:

Hi,

I never tried, but maybe something like pickle.dump(curdoc, 'session.txt') / curdoc = pickle.load('session.txt) could work out of the box?

Just an idea
Daniel

2018-02-24 4:01 GMT+01:00 Sébastien Roche <[email protected]>:
To give you a simple example of the second option I mentioned:

This is for a folder app, you will need to place it in a directory that also has a subdirectory named "saved_sessions"

from bokeh.io import curdoc
from bokeh.plotting import figure
from bokeh.models import Button, ColumnDataSource, Select, TextInput, RadioButtonGroup
from bokeh.layouts import widgetbox, gridplot
from random import random

import os
import numpy as np

app_path = os.path.dirname(__file__) # the app should be in /linefit/lft145/lft_app
save_path = os.path.join(app_path,'saved_sessions')

data = {0:{'x':range(10),'y':range(10)}}

def save_session():

  global data

  save_name = curdoc().select_one({'name':'save_input'}).value

  np.save(os.path.join(save_path,save_name+'.npy'), data)

  curdoc().select_one({'name':'load_input'}).options = [' ']+os.listdir(save_path)

def load_session(attr,old,new):

  global data

  load_name = curdoc().select_one({'name':'load_input'}).value

  data = np.load(os.path.join(save_path,load_name)).item()

  doc_maker()

def add_stuff():

  global data

  new_key = max(data.keys())+1

  data[new_key] = {'x':[random() for i in range(10)],'y':[random() for i in range(10)]}

  curdoc().select_one({'name':'button_group'}).labels = [str(i) for i in sorted(data.keys())]

def doc_maker():

  curdoc().clear()

  save_input = TextInput(title='Save name:',name='save_input')
  save_button = Button(label='Save')
  save_button.on_click(save_session)

  load_input = Select(value =' ',options=[' ']+os.listdir(save_path),name='load_input')
  load_input.on_change('value',load_session)

  add_button = Button(label='add stuff',name='add_button')
  add_button.on_click(add_stuff)

  button_group = RadioButtonGroup(labels=[str(i) for i in sorted(data.keys())],active=0,name='button_group')
  button_group.on_change('active',update)

  fig = figure()
  source = ColumnDataSource(data=data[0])
  fig.scatter('x','y',source=source,name='fig_scatter')

  grid = gridplot([[button_group],[fig,widgetbox(load_input,save_input,save_button,add_button)]])

  curdoc().add_root(grid)

def update(attr,old,new):

  global data

  curdoc().select_one({'name':'fig_scatter'}).data_source.data.update(data[new])

curdoc().title = "save_load"

doc_maker()

In this example the doc_maker() and clear() are not really needed, but this gives you the general idea. When I have multiple plots with multiples glyphrenderers I usually have a function update_doc() at the end of doc_maker() that will fill all the different ColumnDataSources from the structure of the dictionnary"data".

--
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/72b194b0-d826-40ca-b0f9-23c2d3ed38d0%40continuum.io\.

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

--
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/CAMiLiKgfw%3DRJuPXWd5dEFwqOPr8Qe6yveGHVXpXqu3oFwgBNeQ%40mail.gmail.com\.
For more options, visit https://groups.google.com/a/continuum.io/d/optout\.

Unfortunately it is not quite as simple as pickle.dump then pickle.load the curdoc(). There are internal functions (e.g. lambda) in bokeh that cannot pickeled. I tried it and fails as follows:

PicklingError(“Can’t pickle <function ColumnDataSource. at 0x7f98a8def840>: attribute lookup ColumnDataSource. on bokeh.models.sources failed”,)

···

On Saturday, February 24, 2018 at 7:10:25 AM UTC, Daniel Krause wrote:

Hi,

I never tried, but maybe something like pickle.dump(curdoc, ‘session.txt’) / curdoc = pickle.load('session.txt) could work out of the box?

Just an idea

Daniel

2018-02-24 4:01 GMT+01:00 Sébastien Roche [email protected]:

To give you a simple example of the second option I mentioned:

This is for a folder app, you will need to place it in a directory that also has a subdirectory named “saved_sessions”

from bokeh.io import curdoc

from bokeh.plotting import figure

from bokeh.models import Button, ColumnDataSource, Select, TextInput, RadioButtonGroup

from bokeh.layouts import widgetbox, gridplot

from random import random

import os

import numpy as np

app_path = os.path.dirname(file) # the app should be in /linefit/lft145/lft_app

save_path = os.path.join(app_path,‘saved_sessions’)

data = {0:{‘x’:range(10),‘y’:range(10)}}

def save_session():

global data
save_name = curdoc().select_one({'name':'save_input'}).value
np.save(os.path.join(save_path,save_name+'.npy'), data)
curdoc().select_one({'name':'load_input'}).options = [' ']+os.listdir(save_path)

def load_session(attr,old,new):

global data
load_name = curdoc().select_one({'name':'load_input'}).value
data = np.load(os.path.join(save_path,load_name)).item()
doc_maker()

def add_stuff():

global data
new_key = max(data.keys())+1
data[new_key] = {'x':[random() for i in range(10)],'y':[random() for i in range(10)]}
curdoc().select_one({'name':'button_group'}).labels = [str(i) for i in sorted(data.keys())]

def doc_maker():

curdoc().clear()
save_input = TextInput(title='Save name:',name='save_input')
save_button = Button(label='Save')
save_button.on_click(save_session)
load_input = Select(value =' ',options=[' ']+os.listdir(save_path),name='load_input')
load_input.on_change('value',load_session)
add_button = Button(label='add stuff',name='add_button')
add_button.on_click(add_stuff)
button_group = RadioButtonGroup(labels=[str(i) for i in sorted(data.keys())],active=0,name='button_group')
button_group.on_change('active',update)
fig = figure()
source = ColumnDataSource(data=data[0])
fig.scatter('x','y',source=source,name='fig_scatter')
grid = gridplot([[button_group],[fig,widgetbox(load_input,save_input,save_button,add_button)]])
curdoc().add_root(grid)

def update(attr,old,new):

global data
curdoc().select_one({'name':'fig_scatter'}).data_source.data.update(data[new])

curdoc().title = “save_load”

doc_maker()

``

In this example the doc_maker() and clear() are not really needed, but this gives you the general idea. When I have multiple plots with multiples glyphrenderers I usually have a function update_doc() at the end of doc_maker() that will fill all the different ColumnDataSources from the structure of the dictionnary"data".

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/72b194b0-d826-40ca-b0f9-23c2d3ed38d0%40continuum.io.

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

Hi Bryan,

I think it make a lot sense to be able restore the current status of curdoc() of session. A save on the server functionality, with failure upon the mismatch between current code and the saved session (e.g. when a curdoc’s component is missing, renamed) and restore upon the exact match between two, will be great feature.

Regards

···

On Monday, February 26, 2018 at 8:13:03 PM UTC, Bryan Van de ven wrote:

Hi,

Just to add a little context. This is definitely an area where Bokeh is agnostic. Any solution we might implement would almost certainly not be sufficient for a lot of users/use cases (restore based on user? On origin? Reset-able? After some timeout? Or other condition?) Yet any solution we make would add both complexity and additional package dependencies. Since the app itself can execute pretty much whatever arbitrary python code it wants to access files, databases, other APIS, etc., it definitely seems best to stay out of people’s way and not try to dictate policy.

Thanks,

Bryan

On Feb 23, 2018, at 23:10, ‘Daniel Krause’ via Bokeh Discussion - Public [email protected] wrote:

Hi,

I never tried, but maybe something like pickle.dump(curdoc, ‘session.txt’) / curdoc = pickle.load('session.txt) could work out of the box?

Just an idea

Daniel

2018-02-24 4:01 GMT+01:00 Sébastien Roche [email protected]:

To give you a simple example of the second option I mentioned:

This is for a folder app, you will need to place it in a directory that also has a subdirectory named “saved_sessions”

from bokeh.io import curdoc

from bokeh.plotting import figure

from bokeh.models import Button, ColumnDataSource, Select, TextInput, RadioButtonGroup

from bokeh.layouts import widgetbox, gridplot

from random import random

import os

import numpy as np

app_path = os.path.dirname(file) # the app should be in /linefit/lft145/lft_app

save_path = os.path.join(app_path,‘saved_sessions’)

data = {0:{‘x’:range(10),‘y’:range(10)}}

def save_session():

    global data
    save_name = curdoc().select_one({'name':'save_input'}).value
    np.save(os.path.join(save_path,save_name+'.npy'), data)
    curdoc().select_one({'name':'load_input'}).options = [' ']+os.listdir(save_path)

def load_session(attr,old,new):

    global data
    load_name = curdoc().select_one({'name':'load_input'}).value
    data = np.load(os.path.join(save_path,load_name)).item()
    doc_maker()

def add_stuff():

    global data
    new_key = max(data.keys())+1
    data[new_key] = {'x':[random() for i in range(10)],'y':[random() for i in range(10)]}
    curdoc().select_one({'name':'button_group'}).labels = [str(i) for i in sorted(data.keys())]

def doc_maker():

    curdoc().clear()
    save_input = TextInput(title='Save name:',name='save_input')
    save_button = Button(label='Save')
    save_button.on_click(save_session)
    load_input = Select(value =' ',options=[' ']+os.listdir(save_path),name='load_input')
    load_input.on_change('value',load_session)
    add_button = Button(label='add stuff',name='add_button')
    add_button.on_click(add_stuff)
    button_group = RadioButtonGroup(labels=[str(i) for i in sorted(data.keys())],active=0,name='button_group')
    button_group.on_change('active',update)
    fig = figure()
    source = ColumnDataSource(data=data[0])
    fig.scatter('x','y',source=source,name='fig_scatter')
    grid = gridplot([[button_group],[fig,widgetbox(load_input,save_input,save_button,add_button)]])
    curdoc().add_root(grid)

def update(attr,old,new):

    global data

    curdoc().select_one({'name':'fig_scatter'}).data_source.data.update(data[new])

curdoc().title = “save_load”

doc_maker()

In this example the doc_maker() and clear() are not really needed, but this gives you the general idea. When I have multiple plots with multiples glyphrenderers I usually have a function update_doc() at the end of doc_maker() that will fill all the different ColumnDataSources from the structure of the dictionnary"data".


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/72b194b0-d826-40ca-b0f9-23c2d3ed38d0%40continuum.io.

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


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/CAMiLiKgfw%3DRJuPXWd5dEFwqOPr8Qe6yveGHVXpXqu3oFwgBNeQ%40mail.gmail.com.

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