Bokeh: Unexpected (broken?) zoom behavior on line plots

210 views Asked by At

I'm building a live plotting utility using Bokeh+tornado, and I'm experiencing a strange behaviour where the plot autoscaling does not behave as expected, in particular, there are three puzzling issues. Any idea of what could go wrong and what could I look into?

List of issues with pictures to show the behaviour:

  1. The line plot auto-scales only for positive Y-axis values (the expected figure shows random values between -50 and 50 but the issue is that the Y-axis doesn't scale below 0):

positive only scaling

  1. The rolling live plot suddenly stops following the data at a specific time moment, has a crazy zoom and flipped X-axis, then resumes after some time (this always happens once and at t=10, both plots suddenly change and become like this. Then resume normal plotting after few seconds):

sudden X-axis zoom and inversion

  1. The line plot autoscale only depending on one line and forgets the rest (here the upward line will be "followed" by the autoscaling, which increases the minimum Y, to the point the other lines will no longer be visible):

bad scaling

This is the code to reproduce the behaviour:

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 tornado import gen
from tornado.ioloop import IOLoop
from multiprocessing import Process, Pipe


class LivePlotApp(object):
    parent_conn, child_conn = Pipe()

    def __init__(self, data_format, app_name='myapp', update_step_ms=1,
                 stream_rollover=50):
        self.app_name = app_name
        self.data_format = data_format
        self.update_step_ms = update_step_ms
        self.stream_rollover = stream_rollover
        self.figs = []
        self.lines = []
        self.figs_n = 0

    def start_process(self):
        self.process =\
            Process(target=self.start_io_loop,
                    args=(self.child_conn,),
                    daemon=True)
        self.process.start()

    # Safe update
    @gen.coroutine
    def locked_update(self):
        try:
            x = self.cc.recv()  # This is blocking
        except EOFError:
            return
        self.source.stream(x, rollover=self.stream_rollover)

    def start_io_loop(self, child_conn):
        self.io_loop = IOLoop.current()
        self.server = Server(applications={
            '/'+self.app_name: Application(FunctionHandler(self.make_document))
            }, io_loop=self.io_loop, port=5001)
        self.server.start()
        self.server.show('/'+self.app_name)
        self.cc = child_conn
        self.io_loop.start()

    def make_document(self, doc):
        # Define data source
        self.source = ColumnDataSource(self.data_format)

        # Add figures and lines to plots to document
        figs = []
        for f in self.figs:
            fig = figure(**f)
            figs.append(fig)
        for line in self.lines:
            for fig_n, params in line.items():
                figs[fig_n].line(*params[0], **params[1], source=self.source)
        for f in figs:
            doc.add_root(f)
        ###################################################################
        # If you don't need/want to use add_figure, or add_line_to_figure
        # you can just make a fixed layout
        ###################################################################
        # fig1 = figure(title="FIG 1")
        # fig1.line("t", "x", line_color="pink", source=self.source)
        # fig1.line("t", "y", line_color="red", source=self.source)
        # fig1.line("t", "z", line_color="blue", line_width=2,
        #           source=self.source)
        # doc.add_root(fig1)
        # fig2 = figure(title="FIG 2")
        # fig2.line('t', 'other', source=self.source)
        # doc.add_root(fig2)
        ###################################################################

        doc.add_periodic_callback(self.locked_update, self.update_step_ms)

    def add_figure(self, **kwargs):
        self.figs.append(kwargs)
        self.figs_n += 1
        return self.figs_n-1

    def add_line_to_figure(self, f, *args, **kwargs):
        self.lines.append({f: (args, kwargs)})


if __name__ == '__main__':
    # Example usage
    import time
    import random
    import math
    dict_data = {'t': [], 'x': [], 'y': [], 'z': [], 'other': []}
    app = LivePlotApp(data_format=dict_data, app_name="Demo_app")
    f1 = app.add_figure(title="Fig 1", plot_width=300, plot_height=300)
    app.add_line_to_figure(f1, "t", "y", line_color="pink")
    app.add_line_to_figure(f1, "t", "z", line_color="red", line_width=2)
    app.add_line_to_figure(f1, "t", "x", line_color="blue")
    app.add_line_to_figure(f1, "t", "x")
    f2 = app.add_figure(title="Fig 2", plot_width=300, plot_height=300)
    app.add_line_to_figure(f2, "t", "other", line_color="blue")

    app.start_process()
    i = 0
    dt = 0.01  # Send data at 100Hz
    t_sent = time.perf_counter()

    while True:
        time.sleep(0.1)
        t = time.perf_counter()
        dict_data['other'] = [random.randint(-50, 50)]
        dict_data['t'] = [time.perf_counter()],
        dict_data['x'] = [7 if i < 110 else 15]
        dict_data['y'] = [i % 100]
        dict_data['z'] = [10*math.sin(t*2*math.pi)]
        if (t-t_sent > dt):
            t_sent = time.perf_counter()
            app.parent_conn.send(dict_data)
            i += 1
1

There are 1 answers

2
Eugene Pakhomov On BEST ANSWER

You have a trailing comma in this line:

dict_data['t'] = [time.perf_counter()],

It creates a tuple with a single list, and it messes things up.