1

I am using Jupyter via Anaconda on Windows 10.

I would like to have a plot, on which a new random value is plotted each second. So far, I have this code:

import plotly.graph_objs as go
import numpy as np
import sched, time
import random
from ipywidgets import widgets

xs = np.linspace(0, 10, 100)
ys = np.zeros(100)

fig = go.FigureWidget()
fig.add_trace(go.Scatter(x=xs, y=ys,
                    mode='lines',
                    name='Random'))
fig.update_xaxes(range=[0, 10])
fig.update_yaxes(range=[0, 10])

s = sched.scheduler(time.time, time.sleep)
yi = 0
def tick_func(sc):
    global ys, yi
    ys[yi] = random.random() * 10
    yi = (yi+1)%100
    fig.data[0].y = ys
    s.enterabs(time.time()+1, 1, tick_func, (sc,))
#s.enterabs(time.time()+2, 1, tick_func, (s,))
s.run()

widgets.VBox([fig])

The output of this code, as is, is shown on this screenshot:

jupyter_tick_start

... which is the correct starting/initial plot, as intended. The idea then is, as soon as the code has started, I would get a new random value along the x-axis, each second.

However, as soon as I enable/uncomment the s.enterabs(time.time()+2, 1, tick_func, (s,)) line - which actually triggers the timer function to start looping - then there is simply no output (no graph drawn)! No errors, but no output either!

So, how can I get a timer function running inside a Jupyter notebook, with effects shown on a plot (here Plotly)?

I found the recommendation for sched here: What is the best way to repeatedly execute a function every x seconds in Python? - but maybe it interferes with some of the implementation in Jupyter (or Plotly, or both?), so maybe there is something else I should use?

( possibly related, though I couldn't find much that helps me with this specific problem: Interactive Timer in Jupyter Notebook )

sdbbs
  • 4,270
  • 5
  • 32
  • 87

2 Answers2

2

Ok, got somewhere, thanks to these:

First of all, here is a reworked simple example from the link above, which shows the use of IPython.lib.backgroundjobs.BackgroundJobManager:

#https://stackoverflow.com/questions/32081926/a-new-thread-for-running-a-cell-in-ipython-jupyter-notebook
##github.com/jupyter/ngcm-tutorial/blob/master/Day-1/IPython%20Kernel/Background%20Jobs.ipynb
import sched, time # NOTE: without this import, we'll get "Dead jobs:" instead of "Running jobs:" - exception at time.sleep will not be reported as such!
from IPython.lib import backgroundjobs as bg
jobs = bg.BackgroundJobManager()

def printfunc(interval=1, reps=5):
    for n in range(reps):
        time.sleep(interval)
        print('In the background... %i' % n)
        #sys.stdout.flush() # flush breaks the thread in Jupyter, after first printout!
    print('All done!')
    #sys.stdout.flush()

#jobs.new('printfunc(1,3)') # works; prints: <BackgroundJob #0: printfunc(1,3)>
jobs.new(printfunc, 1, 3) # works; prints: <BackgroundJob #0: <function printfunc at 0x0000017120038730>>
jobs.status() # prints: "Running jobs:" "0 : <function printfunc at 0x00000171200389D8>" or "0 : printfunc(1,3)"

Here is how it looks like in Jupyter notebook:

jupyter_background_jobs_01.gif

That lead finally to getting the OP example to work - simply, by having the scheduler.run be called through jobs.new:

import plotly.graph_objs as go
import numpy as np
import sched, time
import random
from ipywidgets import widgets

from IPython.lib import backgroundjobs as bg
jobs = bg.BackgroundJobManager()

xs = np.linspace(0, 10, 100)
ys = np.zeros(100)

fig = go.FigureWidget()
fig.add_trace(go.Scatter(x=xs, y=ys,
                    mode='lines',
                    name='Random'))
fig.update_xaxes(range=[0, 10])
fig.update_yaxes(range=[0, 10])

s = sched.scheduler(time.time, time.sleep)
yi = 0
do_run_tick_func = True
def tick_func(sc):
    global ys, yi
    if do_run_tick_func:
        ys[yi] = random.random() * 10
        yi = (yi+1)%100
        fig.data[0].y = ys
        s.enterabs(time.time()+1, 1, tick_func, (sc,))
    else:
        print("Exiting tick_func (not recheduling)") # this does not print on page
#s.enterabs(time.time()+15, 1, tick_func, (s,))
#s.run() # kills widget

#s.run()
#jobs.new(s.enterabs, time.time()+2, 1, tick_func, (s,)) # Does not run in loop, but says: Completed jobs: 0 : <bound method scheduler.enterabs of <sched.scheduler object at 0x000001E01CF12978>>

#jobs.new(tick_func, s) # runs once, does not loop: Allo 0; Completed jobs: 0 : <function tick_func at 0x0000017120038400>

s.enterabs(time.time()+1, 1, tick_func, (s,))
jobs.new(s.run) # runs in loop! Running jobs: 0 : <bound method scheduler.run of <sched.scheduler object at 0x0000017120058390>>
#jobs.status() # prints: "Running jobs:" ...

# https://ipywidgets.readthedocs.io/en/stable/examples/Widget%20Events.html
output = widgets.Output()
mybuttonstop = widgets.Button(description="Stop Background Thread")
mybuttonstart = widgets.Button(description="Start Background Thread")

def on_button_stop_clicked(b):
    global do_run_tick_func
    print("Button clicked - stopping background thread...") # this does print on page
    do_run_tick_func = False
    with output:
        print("print, with output?") # this does not print on page

def on_button_start_clicked(b):
    global do_run_tick_func
    print("Button clicked - starting background thread...") # this does print on page
    do_run_tick_func = True
    s.enterabs(time.time()+1, 1, tick_func, (s,))
    jobs.new(s.run)

mybuttonstop.on_click(on_button_stop_clicked)
mybuttonstart.on_click(on_button_start_clicked)

myhbox = widgets.HBox([mybuttonstart, mybuttonstop])
widgets.VBox([myhbox, fig])

Here is how this looks like in Jupyter notebook:

jupyter_background_jobs_02.gif

sdbbs
  • 4,270
  • 5
  • 32
  • 87
1

The basic problem is that the code cell which displays a widget must finish executing before the widget will display.

What you need is a way for the widget to call back to the Python kernel after it has initialized in order to request an update. You could implement this using proxy widgets like this.

enter image description here

You can also combine proxy widgets with other widget types using HBox, etcetera and use the proxy widget for timing purposes as shown above.

Please see https://github.com/AaronWatters/jp_proxy_widget and the discussion of asynchronicity of widgets in the Tutorial notebook.

Aaron Watters
  • 2,784
  • 3
  • 23
  • 37
  • Thanks for this - as far as I can see, here JavaScript takes over controlling the timer function (even if the timer function code is defined in Python); in my answer below, it is Python that still controls the timing. Many thanks for the pointer to the asynchronicity of widgets discussion, was not aware of that! – sdbbs Sep 05 '19 at 07:44