0

I've been working on a project using bokeh visualization to display results from an agent based model (ABM) simulation. In a recent post, I got help getting my data to stream properly in a very simplified version of my simulation. My next task, which I thought would be a no-brainer, was to add a "reset" button to my layout so I could take my figure back to its initial state and run the simulation from "step 0" again. Surprisingly, there does not seem to be a simple way to do this. I have tried several different things, including re-initializing all my data and re-populating my ColumnDataSources, but I can't get the data from previous runs of the simulation to disappear. Here's a standalone code sample that illustrates the problem:

import colorcet as cc
from bokeh.server.server import Server
from bokeh.application import Application
from bokeh.application.handlers.function import FunctionHandler
from bokeh.plotting import figure, ColumnDataSource
from bokeh.models import Button
from bokeh.layouts import column
import random

def make_document(doc):

    # make a list of groups
    strategies = ['DD', 'DC', 'CD', 'CCDD']

    # initialize some vars
    step = 0
    callback_obj = None  
    colors = cc.glasbey_dark
    #num_colors = len(colors)
    # create a list to hold all CDSs for active strategies in next step
    sources = []

    # Create a figure container
    fig = figure(title='Streaming Line Plot - Step 0', plot_width=1400, plot_height=400)

    # get step 0 data for initial strategies
    for i in range(len(strategies)):
        step_data = dict(step=[step], 
                        strategy = [strategies[i]],
                        ncount=[random.choice(range(1, 100))])
        data_source = ColumnDataSource(step_data)
        color = colors[i]
        # this will create one fig.line renderer for each strategy & its data for this step
        fig.line(x='step', y='ncount', source=data_source, color=color, line_width=2)
        # add this CDS to the sources list
        sources.append(data_source)

    def button1_run():
        nonlocal callback_obj
        if button1.label == 'Run':
            button1.label = 'Stop'
            button1.button_type='danger'
            callback_obj = doc.add_periodic_callback(button2_step, 100)
        else:
            button1.label = 'Run'
            button1.button_type = 'success'
            doc.remove_periodic_callback(callback_obj)

    def button2_step():
        nonlocal step
        data = []
        step += 1
        fig.title.text = 'Streaming Line Plot - Step '+str(step)
        for i in range(len(strategies)):
            step_data = dict(step=[step], 
                            strategy = [strategies[i]],
                            ncount=[random.choice(range(1, 100))])
            data.append(step_data)
        for source, data in zip(sources, data):
            source.stream(data)        

    def button3_reset():
        step = 0
        fig.title.text = 'Streaming Line Plot - Step '+str(step)

        for i in range(len(strategies)):
            init_data = dict(step=[step], 
                            strategy = [strategies[i]],
                            ncount=[random.choice(range(1, 100))])
            reset_source = ColumnDataSource(init_data)
            print(init_data)
            color = colors[i]
            # this will create one fig.line renderer for each strategy & its data for this step
            fig.line(x='step', y='ncount', source=reset_source, color=color, line_width=2)
            # add this CDS to the sources list
            sources.append(reset_source)


    # add on_click callback for button widget
    button1 = Button(label="Run", button_type='success', width=390)
    button1.on_click(button1_run)
    button2 = Button(label="Step", button_type='primary', width=390)
    button2.on_click(button2_step)
    button3 = Button(label="Reset", button_type='warning', width=390)
    button3.on_click(button3_reset)

    doc.add_root(column(fig, button1, button2, button3))
    doc.title = "Now with live updating!"

apps = {'/': Application(FunctionHandler(make_document))}

server = Server(apps, port=5004)
server.start()

if __name__ == '__main__':
    server.io_loop.add_callback(server.show, "/")
    server.io_loop.start()

What I'm trying to do in my button3_reset code is basically repeat the initialization at the top of the make_document function. But even though that code works identically (apparent from a print output stuck in the middle of the button3 step), I can't get the figure to reset to its initial empty state. I've read thru a lot of stack overflow posts and other bokeh documentation and haven't found a simple answer to what I thought was a simple question: how do you reset a bokeh line plot back to its original state so you can run the data stream again from its starting point?

I am using bokeh 1.4.0 (anaconda won't let me update), python 3.7.6, spyder 4.0.1, and both Chrome & Brave browsers for visualizing.

steve---g
  • 365
  • 2
  • 11
  • You could look at the implementation of the "official" reset button, https://demo.bokeh.org/crossfilter – Joe Apr 24 '20 at 05:16
  • Does this answer your question? [What's the command to "reset" a bokeh plot?](https://stackoverflow.com/questions/39278110/whats-the-command-to-reset-a-bokeh-plot) – Joe Apr 24 '20 at 05:19
  • 1
    https://github.com/bokeh/bokeh/issues/5071#issuecomment-378811501 – Joe Apr 24 '20 at 05:19

2 Answers2

0

Your button3_reset code does not clean up anything - it just adds new stuff on top of the existing stuff.

Instead, you should just iterate over the sources list and set the data attribute of each source to the initial value used in the very first loop in your code. Meaning, you will have to preserve that data somewhere as well.

Eugene Pakhomov
  • 9,309
  • 3
  • 27
  • 53
  • Right. That's why my code has me stumped. I thought I was doing exactly that. In the button3 for-loop, init_data gives me the dict of the data I want to feed to the figure with the reset (just like in the initial definition). I thought resetting the CDS with that data as its source (in the fig.line renderer) would clear the old data. I would be fine with literally creating a new figure to replace the old one. Each sim is a separate run and has no connection to previous runs. But if I redo the fig = figure definition, it just adds a second figure to the page, doesn't replace the first. – steve---g Apr 24 '20 at 17:42
  • Yes, that's exactly why you should not redo _any_ of the Bokeh models. Just replace the data, that's it. Update: Ah, I see that you did exactly that in your new function - nice! – Eugene Pakhomov Apr 25 '20 at 04:40
0

As is usually the case, I greatly overcomplicated things. Here's code for button3_reset that does the job.

 def button3_reset():
        nonlocal step
        step = 0
        data = []
        fig.title.text = 'Streaming Line Plot - Step '+str(step)

        for i in range(len(strategies)):
            init_data = dict(step=[step], 
                            strategy = [strategies[i]],
                            ncount=[random.choice(range(1, 100))])
            data.append(init_data)

        for source, data in zip(sources, data):
            source.data = data

What I was doing before was generating new CDSs, but the old ones were still embedded in the figure. Once again thanks to Eugene's tip, I realized I only needed to reassign the .data attribute of the existing CDSs, not create new ones. Then, to tidy things up, I had to update the title back to 'Step 0' and then make step a nonlocal variable so button2_step would know to restart step numbering at 1. With that, the reset what it's supposed to do. Thanks again for the responses.

steve---g
  • 365
  • 2
  • 11