Accessing self when using python plotly/dash during app.callback

Viewed 707

I am creating a live updating graph using plotly/dash and python. Passing the variable self in python is unfortunately throwing errors. I have made a minmimal example below to outline the problem.

import dash
from dash.dependencies import Output, Input
import dash_core_components as dcc
import dash_html_components as html
import plotly
import random
import plotly.graph_objs as go
from collections import deque
import time
import numpy as np


class PlotlyDashboard:
    _X = deque(maxlen = 20)
    _X.append(1)
    
    _Y = deque(maxlen = 20)
    _Y.append(1)
    
    _app = dash.Dash(__name__)

    def __init__(self):
        self._app.layout = html.Div(
        [
            dcc.Graph(id = 'live-graph', animate = True),
            dcc.Interval(
                id = 'graph-update',
                interval = 1000,
                n_intervals = 0
            ),
        ]
        )

    @_app.callback(
        Output('live-graph', 'figure'),
        [Input('graph-update', 'n_intervals') ]
    )
    def update_graph_scatter(self, n):
        data = plotly.graph_objs.Scatter(
                x=list(self._X),
                y=list(self._Y),
                name='Scatter',
                mode= 'lines+markers'
        )
    
        return {'data': [data],
                'layout' : go.Layout(xaxis=dict(range=[min(self._X),max(self._X)]),yaxis = dict(range = [min(self._Y),max(self._Y)]),)}

    def update(self, y):
        self._X.append(self._X[-1] + 1)
        self._Y.append(y)
    
    def start(self):
        self._app.run_server(port=8050)

live_plotter = PlotlyDashboard()

live_plotter.start()
while True:
    live_plotter.update(np.random.normal(0,1))
    time.sleep(1)

The graph updates once a second with new data that is provided via the update function. The update function updates the data queues stored in self. During the @app.callback which also happens every second it consults self to get the current state of the data before plotting, but I get the below error when doing so

output_value = func(*func_args, **func_kwargs)  # %% callback invoked %%
TypeError: update_graph_scatter() missing 1 required positional argument: 'n'

The error goes away if I remove self as an arg in the update_graph_scatter method, but then the internals of the method cannot access self which causes a different error. Any help that can be provided on how I can access self or an equivalent common store that both the update method and @app.callback tagged method can consult will be much appreciated.

Edit: By using rob's solution and a bit of threading I was able to solve the issue. Please see my solution below on how to instantiate the class in one thread (must be done because app_server is blocking) and then update the data the class is plotting using the main thread.

import dash
from dash.dependencies import Output, Input
import dash_core_components as dcc
import dash_html_components as html
import plotly
import random
import plotly.graph_objs as go
from collections import deque
import time
import numpy as np
from threading import Thread
class PlotlyDashboard:
    _X = deque(maxlen = 20)
    _X.append(1)
    
    _Y = deque(maxlen = 20)
    _Y.append(1)
    
    _app = dash.Dash(__name__)

    def __init__(self):
        self._app.layout = html.Div(
        [
            dcc.Graph(id = 'live-graph', animate = True),
            dcc.Interval(
                id = 'graph-update',
                interval = 1000,
                n_intervals = 0
            ),
        ]
        )
        if self._app is not None and hasattr(self, "callbacks"):
            self.callbacks(self._app)

    def callbacks(self, _app):
        @_app.callback(
            Output("live-graph", "figure"), [Input("graph-update", "n_intervals")]
        )
        def update_graph_scatter(n):
            # let's update data here to show class callbacks are working
            data = plotly.graph_objs.Scatter(
                x=list(self._X), y=list(self._Y), name="Scatter", mode="lines+markers"
            )

            return {
                "data": [data],
                "layout": go.Layout(
                    xaxis=dict(range=[min(self._X), max(self._X)]),
                    yaxis=dict(range=[min(self._Y), max(self._Y)]),
                ),
            }
    def update(self, y):
        self._X.append(self._X[-1] + 1)
        self._Y.append(y)
    
    def start(self):
        self._app.run_server(port=8050)

live_plotter = PlotlyDashboard()

def starter():
    live_plotter.start()

plot_thread = Thread(target=starter)
plot_thread.start()
while True:
        live_plotter.update(np.random.normal(0,1))
        time.sleep(1)
1 Answers
  • I have answered the scope of your question. How to have callbacks as class methods
  • fundamentally scope them as static methods within a class method.
  • you have assumed that run_server() is non blocking, i.e. returns control immediately after server has been started back to controlling process. It is not - hence I have put call to self.update() into callback
import dash
from dash.dependencies import Output, Input
import dash_core_components as dcc
import dash_html_components as html
import plotly
import random
import plotly.graph_objs as go
from collections import deque
import time

class PlotlyDashboard:
    _X = deque(maxlen=20)
    _X.append(1)

    _Y = deque(maxlen=20)
    _Y.append(1)

    _app = dash.Dash(__name__)

    def __init__(self):
        self._app.layout = html.Div(
            [
                dcc.Graph(id="live-graph", animate=True),
                dcc.Interval(id="graph-update", interval=1000, n_intervals=0),
            ]
        )

        if self._app is not None and hasattr(self, "callbacks"):
            self.callbacks(self._app)

    def callbacks(self, _app):
        @_app.callback(
            Output("live-graph", "figure"), [Input("graph-update", "n_intervals")]
        )
        def update_graph_scatter(n):
            # let's update data here to show class callbacks are working
            self.update()
            data = plotly.graph_objs.Scatter(
                x=list(self._X), y=list(self._Y), name="Scatter", mode="lines+markers"
            )

            return {
                "data": [data],
                "layout": go.Layout(
                    xaxis=dict(range=[min(self._X), max(self._X)]),
                    yaxis=dict(range=[min(self._Y), max(self._Y)]),
                ),
            }

    def update(self):
        self._X.append(self._X[-1] + 1)
        self._Y.append(self._Y[-1] + self._Y[-1] * random.uniform(-0.1, 0.1))

    def start(self):
        print("starting")
        self._app.run_server(port=8050)
        # this will never be seen !!!!
        print("started")


live_plotter = PlotlyDashboard()


live_plotter.start()
# this code will never run as run_server() is a blocking not a sub-process
while True:
    live_plotter.update()
    time.sleep(1)
Related