Dash Crush, part 6: serialization

Post Contents

Welcome to Dash Crush, your walk-through tutorial to advanced interactive Data Viz dashboards using Dash Plotly.
In the previous parts, we learned how to create dynamic layouts and retrieve the state from them.

In this chapter, we will expand the topic with serialization.

TL;DR

In this chapter:

  1. How to save the state of the app to a file
  2. How to download the file from the server

Introduction

Dash is a fantastic tool for creating interactive data-centric apps. However, in its free form, it is not a tool for collaboration and sharing your results with the audience or co-workers. The Enterprise version of Dash implements the snapshot mechanism, allowing preserving and sharing the state of the app. Nevertheless, it is a paid solution. (See https://plotly.com/dash/snapshot-engine/ for details).

In this chapter, we will learn how to implement preserving the state of your app (or serialization) using JSON files.

The code

We will reuse the code from the previous chapter and add a serialization mechanism on top of it.

Install packages

Create and activate your virtual environment, and install the following packages:

pip install dash pandas dash-bootstrap-components colorlover

Layout

Our layout remains mostly unchanged. We will add only a JSON export button to the sidebar.

Sidebar

We will add a "Download config" button, dcc.Store for the intermediate storage of config, and dcc.Download component for getting a file to a local machine from a server. The code will be located in src/layout/sidebar.py

# src/layout/sidebar.py

from dash import html, dcc
import dash_bootstrap_components as dbc
from src.layout.grid import spacer

def sidebar_menu():
    """Sidebar menu"""
    return html.Div(
        [
            spacer(10),
            _buttons(),
        ]
    )

def _buttons():
    return html.Div(
        [
            dbc.Button(
                [html.I(className="fi-save"), " Download config"],
                id="download_button",
                color="primary",
                outline=True,
            ),
            dbc.Button("Plot", id="plot_button", color="primary"),
            dcc.Store(id="save_config_store", data={}),
            dcc.Download(id="save_config_download"),
        ],
        className="d-grid gap-2",
    )

More details about downloading files can be found in the Dash documentation: https://dash.plotly.com/dash-core-components/download

Callbacks

In the usual case, not all components require serialization. Some may depend on others and be set in a callback, so it may be better to serialize only a subset of a component that is sufficient to restore the state of the app. Of course, how to proceed will depend on your use case. Let's define the config structure for easier tracking of which components we want to serialize.

Default config

Let's add the following content to src/utils/config.py:

# src/utils/config.py

from src.layout.dropdowns import BULKS

JSON_VERSION = 1

DEFAULT_OPTIONS = {
    "substrate_value": "GaAs",
    "substrate_options": BULKS,
}

DEFAULT_CONFIG = {
    "version": JSON_VERSION,
    "cards": [],
    "options": deepcopy(DEFAULT_OPTIONS),
}

In our case, we would like to preserve the substrate dropdown and layer cards' state.
Additionally, as a rule of thumb, I add a version field to each config I ever create. If you bring your dashboard to production, share it with users, and foresee it may evolve, the version field may save you from hours of debugging later on.

Serialization

Let's create a callback that will serialize the state of our app to config dict. The code will be placed in src/callbacks/serializer.py

# src/callbacks/serializer.py

from copy import deepcopy
import json
from dash import dcc, Input, Output, State, ALL
from src.utils.config import build_layer_config, DEFAULT_OPTIONS, JSON_VERSION

def create_callbacks(app):
    """Create serialization callback"""

    @app.callback(
        Output("save_config_store", "data"),
        Input("download_button", "n_clicks"),
        State("bulk_material", "value"),
        State("bulk_material", "options"),
        State({"role": "material_dropdown", "index": ALL}, "value"),
        State({"role": "material_dropdown", "index": ALL}, "id"),
        State({"role": "param", "index": ALL, "name": ALL, "material": ALL}, "value"),
        State({"role": "param", "index": ALL, "name": ALL, "material": ALL}, "id"),
        prevent_initial_call=True,
    )
    def _serialize(
        _, substrate, substrate_options, dropdowns, dropdown_ids, params, param_ids
    ):  # pylint: disable=too-many-arguments
        def _list(list_of_dicts, field="value"):
            return [entry[field] for entry in list_of_dicts]

        def _dict(list_of_dicts):
            return {entry["value"]: entry["label"] for entry in list_of_dicts}

        options = deepcopy(DEFAULT_OPTIONS)
        options["substrate_value"] = substrate
        options["substrate_options"] = _list(substrate_options)

        config = {
            "version": JSON_VERSION,
            "cards": build_layer_config(dropdowns, dropdown_ids, params, param_ids),
            "options": options,
        }

        return dcc.send_string(json.dumps(config, indent=4), "dc-config.json")

We reuse build_layer_config defined in the previous chapter to create layer config. Additionally, we store value and options of the bulk card dropdown.

We have implemented two nested helper methods: _list and _dict for converting lists of dictionaries to plain list or dictionary respectively.

The final step is downloading config to your local machine. We use dcc.send_string for that.

Using callback

Finally, we need to create all the callbacks. In dashboard.py modify callback creation with:

# dashboard.py
import src.callbacks.serializer

and in create_callbacks(app):

    # Append at the end of the function
    src.callbacks.serializer.create_callbacks(app)

Starting the app:

Start the app with python dashboard.py. You should see an indication of a running app in your console. The dashboard is available under http://localhost:8050.

Summary:

Quick recap:

  1. For serialization, collect all relevant properties in the dictionary
  2. Use dcc.Download to download the config to your local machine
  3. Config versioning may save you hours of debugging

The complete code from this chapter can be found on GitHub:
https://github.com/ptrhbt/dash-crush/tree/6-serialization

There is also the requirements.txt file which lists all packages used. Just install it to your virtual env with pip install -r requirements.txt.

I would be happy to hear your opinions about Dash Crush.

See you in the next chapter of Dash Crush! We will implement complementary functionality, namely deserialization app state from the config file.

Share this Post:

Related posts