Python Decorator as Callback in Dash Using Dash Object That is an Instance Variable - Fails
Asked Answered
C

3

18

I'm updating some code to use Dash and plotly. The main code for graphing is defined within a class. I replaced some Bokeh widgets with Dash controls, and ended up with a callback that looks like this:

class MakeStuff:
    def __init__(self, ..., **optional):
        ...
        self.app = dash.Dash(...)
        ...

    @self.app.callback(
    dash.dependencies.Output('indicator-graphic', 'figure'),
        [dash.dependencies.Input('start-time-slider', 'value'),
         dash.dependencies.Input('graph-width-slider', 'value')]
        )
    def update_graphs(self,range_start,graph_width):
        print(...)

I am following some examples from the Dash website. I was able to run the examples, including callbacks. In my code, without the decorator, the code runs without error, producing the graphics and controls as I expected it to. (Of course, the code is incomplete, but there is no error.) When I include the decorator, I get this error:

NameError: name 'self' is not defined

I tired it this way, first, just mimicking the code examples:

class MakeStuff:
    def __init__(self, ..., **optional):
        ...
        app = dash.Dash(...)
        ...

    @app.callback(
    dash.dependencies.Output('indicator-graphic', 'figure'),
    [dash.dependencies.Input('start-time-slider', 'value'),
     dash.dependencies.Input('graph-width-slider', 'value')]
    )
    def update_graphs(self,range_start,graph_width):
        print(...)

Of course, the variable "app" is only know within the scope of the init function, so it's no surprise that that doesn't work, giving the similar error:

NameError: name 'app' is not defined

Is there a straightforward way to set up this decorator to work while still keeping my code within a class definition? I am guessing some pre-processing is going on with the decorator, but I don't understand it well enough to come up with a solution.

Cleancut answered 17/2, 2019 at 2:15 Comment(0)
R
23

You could call the callback function not as a decorator, as shown in this answer. This should work from within your __init__ function:

class MakeStuff:
    def __init__(self, ..., **optional):
        ...
        self.app = dash.Dash(...)
        self.app.callback(
            dash.dependencies.Output('indicator-graphic', 'figure'),
            [dash.dependencies.Input('start-time-slider', 'value'),
             dash.dependencies.Input('graph-width-slider', 'value')]
            )(self.update_graphs)
        ...

    def update_graphs(self,range_start,graph_width):
        print(...)

I've never tried it with a class instance before, but see no reason for it not to work.

Rusert answered 17/2, 2019 at 9:1 Comment(0)
V
5

ned2 provides a solution here, he uses the following structure to set up the decorators within a class definition.

class BaseBlock:
def __init__(self, app=None):
    self.app = app

    if self.app is not None and hasattr(self, 'callbacks'):
        self.callbacks(self.app)

class MyBlock(BaseBlock):
    layout = html.Div('layout for this "block".')

    def callbacks(self, app):

        @app.callback(Output('foo', 'figure'), [Input('bar')])
        def do_things(bar):
            return SOME_DATA

        @app.callback(Output('baz', 'figure'), [Input('boop')])
        def do_things(boop):
            return OTHER_DATA

# creating a new MyBlock will register all callbacks
block = MyBlock(app=app)

# now insert this component into the app's layout 
app.layout['slot'] = block.layout
Vender answered 28/3, 2021 at 21:24 Comment(0)
M
0

I have slightly different version from tisalvadores that doesn't have to pass the app as a parameter. It's an OO version of the Controls and Callbacks example in the "Dash 20 minute tutorial" (https://dash.plotly.com/tutorial) so you can compare to the original.

Base class:

# Import packages
from dash import Dash, html

class DashBaseClass:
    app:Dash

    def __init__(self, simple_run = False) -> None:
        print("DashBaseClass.init()")
        self.app = Dash(__name__)
        if simple_run:
            self.initialize()
            self.setup_layout()
            self.setup_callbacks()
            self.run()

    def initialize(self) -> None:
        pass

    def setup_callbacks(self) ->None:
        pass

    def setup_layout(self) -> None:
        self.app.layout = html.Div([
            html.Div(children='Hello World Base')
        ])

    def run(self, debug = True) -> None:
        self.app.run_server(debug=debug)

if __name__ == "__main__":
    dbc = DashBaseClass(True)

Inherited class:

from dash import Dash, html, dash_table, dcc, callback, Output, Input
import pandas as pd
import plotly.express as px

from DashBaseClass import DashBaseClass

class DashCallbackHello(DashBaseClass):
    df:pd.DataFrame

    def initialize(self) -> None:
        self.df = pd.read_csv('https://raw.githubusercontent.com/plotly/datasets/master/gapminder2007.csv')

    def setup_layout(self) -> None:
        self.app.layout = html.Div([
            html.Div(children='My First App with Data, Graph, and Controls'),
            html.Hr(),
            dcc.RadioItems(options=['pop', 'lifeExp', 'gdpPercap'], value='lifeExp', id='controls-and-radio-item'),
            dash_table.DataTable(data = self.df.to_dict('records'), page_size=6),
            dcc.Graph(figure={}, id='controls-and-graph')
        ])

    def setup_callbacks(self) -> None:
        @self.app.callback(
            Output(component_id='controls-and-graph', component_property='figure'),
            Input(component_id='controls-and-radio-item', component_property='value')
        )
        def update_graph(col_chosen):
            fig = px.histogram(self.df, x='continent', y=col_chosen, histfunc='avg')
            return fig

if __name__ == "__main__":
    dch = DashCallbackHello(True)

Running the full app can be done by calling DashCallbackHello(True)

Note that the callback decorator has the class designation (self.app.callback), while the callback method (def update_graph(col_chosen)) does not:

Screenshot of running code: Running Dash code with interactive plots

Montano answered 10/5 at 14:16 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.