Putting a dash instance inside a class?

Forewarning, I have three strikes against me: new-ish to python, very newt to dash and just wrapping my head around classes.

I have a process at work and I want to create a gui for it. The user will input some key process variables, click “Go”, and the process will start, outputting some data of interest via a table and plot live. My dash code got a bit long so I started looking into putting it into a class. This is likely wrong, but the gist of my structure:

class Gui:

    def __init__(self):

        self.params = {'dur': 5, 'freq': 10}
        self.app = dash.Dash()
        self.app.layout = self.build_layout


    def generate_inputs(self, params):

        dur = [html.Label(html.Strong('duration to sample, sec')),
                dcc.Input(type='number', value=params['dur'], id='dur')]
    
        freq = [html.Label(html.Strong('sample frequency, n/sec')),
                  dcc.Input(type='number', value=params['freq'], id='freq')]

        run_test = [html.Label(html.Strong('ready?')),
                    html.Button('run test!', id='run', n_clicks=0)]
    
        input_list = [dur, freq, run_test]
        widgets = list()    
        for sublist in input_list:
            sublist.append(html.H1(''))
            for item in sublist:
                widgets.append(item)
            
        return widgets


    def build_layout(self):

        layout = html.Div([
            html.H2('a dashboard'),
            html.Div(self.generate_inputs(self.params),
                     style={'width' : '15%', 'float' : 'left' }),
            html.Div(id='plot'),
        
        ]) # app.layout

        return layout


    def generate_plot(data):

        plot = dcc.Graph(
            id='live-data',
            figure={
                'data': [go.Scatter(name='plot',
                                    x=data['i'],
                                    y=data['mean'])],
                'layout': go.Layout(title= 'Live test data')#,
            },
            
            style={'width' : '80%', 'float' : 'right' }
        ) # dcc.Graph
    
        return plot


if __name__ == '__main__':

    gui = Gui()
    gui.app.run_server(debug=False)

This works, but I get stumped when it comes to integrating any sort of reactivity. For example, let’s say I add this to build_layout():

html.Div(dcc.Interval(id='refresh', interval=5000))

This would call my plot function, but I don’t know how to put it in the class. I get NameError: name 'app' is not defined as shown, and Dash.app.callback, dash.app.callback, and self.app.callback don’t work either (total guesses).

    @app.callback(
        dash.dependencies.Output('plot', 'children'),
        events=[dash.dependencies.Event('refresh', 'interval')])
    def updates(self):
        return generate_plot(self.data)

I’m open to any feedback on this approach. What is the recommended way to integrate dash with more complicated things? My sense in the examples I see is that dash is sort of in the spotlight, with other things somehow worked into it. What if dash is only a component of what you’re doing (the gui/visualization side) and you have a lot of other complicated stuff to do elsewhere?

If it’s helpful here’s a flattened (no class) version of the above that works. In parallel with dash I’d be using a library to talk to some equipment, send a command, get back some data, and plot it. In my real example, the code just gets so darn long and ugly I was trying to simplify it a bit and ran into issues.

Many thanks!

1 Like

I think that the answer in How to integrate dash with another process that might be blocking should be helpful here. I’m personally not a big fan of classes for stuff like this - I prefer working in multiple files and importing the underling app around. That’s what I do for the multi-page dash userguide: https://github.com/plotly/dash-docs.

I “solved” the problem with a hybrid approach.

I have a class called Web_App, which holds information such as my database name and session, etc.

The dash app is instantiated at module global level (necessary as the @app.callback) doesn’t work on a class method.

My callbacks then call the web instance method, passing the input parameter and returning the results.

#! the Web_App class will set this singleton instance when starting the server
#! this is required as Dash event handling (with @app.callback) doesn't
#! work with class methods, so must live at the global module level.
#! the callbacks will then call the class methods via this instance.
web_app : object = None


#! instantiate the dash app
dash_app = Dash( name=__name__, external_stylesheets=[ dbc.themes.BOOTSTRAP ] )

#! dash callbacks call the web app methods
@dash_app.callback(
    Output( 'content', 'children' ), [ Input( 'main-tabs', 'active_tab' ) ]
)
def tab_content( active_tab ) :
    return web_app.tab_content( active_tab )


class Web_App :
    """
    Web Application class.
    """

    def __init__( self ) -> None :
        """Initialise the attributes."""
        ...

    def run( self ) -> None :
        """Run the app."""

        #! set global module singleton instance of the web app
        global web_app
        assert web_app is None, "Cannot instantiate more than one web app !!"
        web_app = self

        ...

        auth = dash_auth.BasicAuth(
            dash_app,
            AUTH_USERNAME_PASSWORD_PAIRS,
        )

        #! run the dash app server
        dash_app.run_server()

    #!------------------------------------------------------------------------

    def tab_content( self, active_tab ) :
       """Return content based on selected tab."""
        content = ...
        return content
2 Likes

Another approach might be to decorate callback class methods with @staticmethod, which do not have a self argument.

However, if you need to access any attributes in a class instance, then you will need to call an instance of the class and method (e.g. web_app.callback_method( self, ...).

So effectively the same as my previous answer, except that:

  • the dash callbacks are within the class as static methods (instead of at global module scope)
  • if an instance method needs to be called then it will have to have a different name.

It still doesn’t get around the single dash instance per module issue, but sometimes you can’t have everything :-/

1 Like

Found a solution without using the hybrid method here: StackOverflow

Basically, do what the decorators do for you manually within the class.

Example:

class MakeStuff:
    def __init__(self, ..., **optional):
        ...
        self.app = dash.Dash(...)
        self.app.callback(dash.dependencies.Output('some-id', 'some-value'),
            dash.dependencies.Input('some-other-id', 'some-other-value'))(self.make_update)
        ...

    def make_update(self, input):
        return input

It seems like the above might be frowned upon… but IMO it makes writing the app much cleaner/reusable without the stray class instance above the app then the class definition below. Plus you do not need to jump through hoops to connect your app with a class that maintains state.

4 Likes

I have a very similar implementation and I wanted to share my solution. My approach was to make a new decorator that stores all of the stuff to pass to app.callback in a dictionary. Then, at init-time, I can iterate over the dictionary of callbacks and apply them.

NOTE: below is pseudo code. You can’t just plug it in and expect it to work!

my_dico = dict()  # is there a better place to store this besides a global?  Perhaps <thinking emoji>
def callback(*args, **kwargs):
    """args and kwargs are the arguments for the app.callback function"""
    def wrapped(func):
        global my_dico
        my_dico[func.__name__] = (*args **kwargs)
        return func
    return wrapped

class WrappedDash:
    def __init__(self):
        self.app = dash.Dash()
        self.app.layout = self.build_layout()
        self.apply_callbacks()

    def apply_callbacks(self):
        """Here, we apply the callbacks to the Dash instance"""
        for func_name, (args, kwargs) in my_dico.items():
            func = getattr(self, func_name)
            self.app.callback(*args, **kwargs)(func)

    @callback(Output('my-output-component'), Input('my-input-component'))
    def some_callback_func(self, input_component):
        return "My input was {}".format(input_component)
        
1 Like