简体   繁体   中英

plotting multiple lines of streaming data in a bokeh server application

I'm trying to build a bokeh application with streaming data that tracks multiple "strategies" as they are generated in a prisoners-dilemma agent based model. I've run into a problem trying to get my line plots NOT to connect all the data points in one line. I put together this little demo script that replicates the issue. I've read lots of documentation on line and multi_line rendering in bokeh plots, but I just haven't found something that seems to match my simple case. You can run this code & it will automatically open a bokeh server at localhost:5004...

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):

    # Create a data source
    data_source = ColumnDataSource({'step': [], 'strategy': [], 'ncount': []})

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

    # Create a figure
    fig = figure(title='Streaming Line Plot',
                 plot_width=800, plot_height=400)
    fig.line(x='step', y='ncount', source=data_source)
    global step
    step = 0

    def button1_run():
        global 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():
        global step
        step = step+1
        for i in range(len(strategies)):
            new = {'step': [step],
                   'strategy': [strategies[i]],
                   'ncount': [random.choice(range(1,100))]}
            fig.line(x='step', y='ncount', source=new)
            data_source.stream(new)


    # 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)

    doc.add_root(column(fig, button1, button2))
    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()

My hope was that by looping thru the 4 "strategies" in the example (after clicking button2), I could stream the new data coming out of the simulation into a line plot for that one strategy and step only. But what I get is one line with all four values connected vertically, then one of them connected to the first one at the next step. Here's what it looks like after a few steps: 在此处输入图像描述

I noticed that if I move data_source.stream(new) out of the for loop, I get a nice single line plot, but of course it is only for the last strategy coming out of the loop.

In all the bokeh multiple line plotting examples I've studied (not the multi_line glyph, which I can't figure out and which seems to have some issues with the Hover tool), the instructions seem pretty clear: if you want to render a second line, you add another fig.line renderer to an existing figure , and it draws a line with the data provided in source=data_source for this line. But even though my for-loop collects and adds data separately for each strategy, I don't get 4 line plots, I get only one.

Hoping I'm missing something obvious. Thanks in advance.

Seems like you need a line per strategy, not a line per step. If so, here's how I would do it:

import random

from bokeh.application import Application
from bokeh.application.handlers.function import FunctionHandler
from bokeh.layouts import column
from bokeh.models import Button
from bokeh.palettes import Dark2
from bokeh.plotting import figure, ColumnDataSource
from bokeh.server.server import Server

STRATEGIES = ['DD', 'DC', 'CD', 'CCDD']


def make_document(doc):
    step = 0

    def new_step_data():
        nonlocal step
        result = [dict(step=[step],
                       ncount=[random.choice(range(1, 100))])
                  for _ in STRATEGIES]
        step += 1
        return result

    fig = figure(title='Streaming Line Plot', plot_width=800, plot_height=400)
    sources = []
    for s, d, c in zip(STRATEGIES, new_step_data(), Dark2[4]):
        # Generate the very first step right away
        # to avoid having a completely empty plot.
        ds = ColumnDataSource(d)
        sources.append(ds)
        fig.line(x='step', y='ncount', source=ds, color=c)

    callback_obj = None

    def button1_run():
        nonlocal callback_obj
        if callback_obj is None:
            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():
        for src, data in zip(sources, new_step_data()):
            src.stream(data)

    # 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)

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


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

server = Server(apps, port=5004)

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

Thank you, Eugene. Your solution got me back on the right track. I played around with it a bit more and ended up with the following:

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
    # 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=800, 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)        


    # 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)

    doc.add_root(column(fig, button1, button2))
    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()

Result is just what I was looking for... 在此处输入图像描述

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM