Interface utilisateur à programmation schématisée avec Plotly Dash

Par: Jeffery Zhang, Statistique Canada

Introduction

Les scientifiques des données créent souvent des modèles qui sont mis en œuvre en R ou en Python. Si ces modèles sont destinés à la production, ils devront être accessibles aux utilisateurs non spécialisés.

Pour rendre les modèles de données accessibles aux utilisateurs non spécialisés dans la phase de production, l'un des principaux problèmes réside dans les aléas de la création d'interfaces utilisateurs accessibles. Bien qu'il soit acceptable qu'un prototype de recherche soit exécuté à partir d'une ligne de commande, toutes les complexités que ce type d'interface présente peuvent grandement décourager les utilisateurs non spécialisés.

La plupart des scientifiques des données manquent d'expérience dans la conception d'interfaces utilisateurs, et la plupart des projets ne disposent pas du budget nécessaire pour l'embauche d'un développeur spécialiste des interfaces utilisateurs. Dans le présent article, nous présentons un outil qui permet aux personnes qui ne sont pas des spécialistes de ce type d'interface de créer rapidement une interface utilisateur satisfaisante en langage Python.

En quoi consiste Plotly Dash?

Plotly est une bibliothèque de visualisation des données à code source ouvert. Dash est un cadre à programmation schématisée pour la conception d'applications de données à code source ouvert qui s'appuie sur Plotly. Plotly Dash offre une solution au problème des interfaces utilisateurs de données. Avec Plotly Dash, les scientifiques des données qui ne sont pas spécialisés dans les interfaces utilisateurs peuvent en quelques jours en concevoir une qui sera satisfaisante pour une application de données. Dans la plupart des projets, un investissement de deux à cinq jours de travail supplémentaires pour la conception d'une interface utilisateur graphique interactive en vaut la peine.

Comment fonctionne Plotly Dash?

Plotly et Dash peuvent être considérés comme des langages dédiés. Plotly est un langage dédié permettant de décrire des graphiques. L'objet central de Plotly est une figure, qui décrit tous les aspects d'un graphique tels que les axes, ainsi que les composants graphiques comme les barres, les lignes ou les secteurs. Nous utilisons Plotly pour créer les objets de la figure et avons ensuite recours à l'un des moteurs de rendu disponibles pour le rendre sur un dispositif de sortie cible, tel qu'un navigateur Web.

Figure 1 - Exemple de figure Plotly.
Description - Figure 1 : Exemple d'une figure Plotly

Voici un exemple d’une figure générée par Plotly. Il s’agit d’un diagramme à barres interactif qui permet à l’utilisateur de passer la souris sur chaque barre pour voir les valeurs des données associées à cette barre.

Dash fournit deux langages dédiés et un moteur de rendu Web pour les objets Figure de Plotly.

Le premier langage dédié de Dash sert à décrire la structure d'une interface utilisateur Web. Il comprend des composants pour les éléments HTML tels que div et p, ainsi que des contrôles d'interface utilisateur tels que Slider et DropDown. L'un des éléments clés du langage dédié Web de Dash est le composant Graph, qui nous permet d'intégrer une figure Plotly dans l'interface utilisateur Web de Dash.

Voici un exemple d'une application Dash simple.

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

df = pd.read_csv('https://raw.githubusercontent.com/plotly/datasets/master/gapminder_unfiltered.csv')

app = Dash(__name__)

app.layout = html.Div([
    html.H1(children='Title of Dash App', style={'textAlign':'center'}),
    dcc.Dropdown(df.country.unique(), 'Canada', id='dropdown-selection'),
    dcc.Graph(id='graph-content')
])

if __name__ == '__main__':
    app.run_server(debug=True)

Voici à quoi cela ressemble dans un navigateur Web.

Figure 2 - Application Dash simple affichée dans un navigateur Web.
Description - Figure 2 : Application simple affichée dans un navigateur

Voici un exemple d’une application minimale créée avec Plotly Dash. Cette application d’échantillons permet de visualiser la croissance de la population canadienne de 1950 jusqu’à la date actuelle à l’aide d’un graphique linéaire. La visualisation est interactive et l’utilisateur peut passer la souris sur les points de la ligne bleue pour voir les valeurs de données associées à ce point.

Le deuxième langage dédié de Dash permet de décrire des flux de données réactifs. Cela nous permet d'ajouter de l'interactivité à l'application de données en décrivant de quelle façon les données passent des composants d'entrée de l'utilisateur au modèle de données, puis reviennent à l'interface utilisateur.

L'ajout du code suivant à l'exemple ci-dessus crée un flux de données réactif entre le composant d'entrée dropdown-selection, la fonction update_graph et le graphique de sortie. Dès que la valeur du composant d'entrée dropdown-selection change, la fonction update_graph est lancée avec la nouvelle valeur de dropdown-selection, et la valeur de retour de update-graph est envoyée à la propriété figure de l'objet graph-content. Cette opération met à jour le graphique en fonction de la sélection de l'utilisateur dans le composant déroulant.

@callback(
    Output('graph-content', 'figure'),
    Input('dropdown-selection', 'value')
)
def update_graph(value):
    dff = df[df.country==value]
    return px.line(dff, x='year', y='pop')

Fonctions utiles de Dash

Vous trouverez ci-dessous quelques scénarios courants d'applications de données; nous indiquons également de quelle façon les fonctions de Dash prennent en charge ces scénarios.

Longs délais de traitement

Il arrive que l'exécution d'un modèle de données prenne beaucoup de temps. Par conséquent, il est judicieux de fournir à l'utilisateur une rétroaction au cours de ce processus afin de l'informer que le modèle de données est en cours d'exécution et que l'application n'est pas tombée en panne. Il serait encore plus utile de fournir une mise à jour de l'état d'avancement afin que l'utilisateur sache approximativement la part du travail qui a été accomplie et celle qui reste à faire.

Nous pouvons également nous rendre compte que nous avons commis une erreur lors de la définition des paramètres d'un travail à longue exécution; dans ce cas, nous voudrions annuler le travail en cours et le recommencer après y avoir apporté des corrections. La fonction de Dash permettant la mise en œuvre de ces scénarios s'appelle « Background callbacks » (en anglais seulement).

Voici un exemple d'une application Dash simple qui présente un travail à longue exécution et montre la barre de progression et le bouton d'annulation.


Figure 3 - Exemple d'une application Dash simple avec la barre de progression et le bouton d'annulation.
Description - Figure 3 : Travail à longue exécution avec la barre de progression et le bouton d'annulation

Voici un exemple d’une application de Plotly Dash impliquant une tâche d’exécution longue et une barre de progression pour afficher l’état d’avancement de la tâche. Cette application a deux boutons. Le bouton « Run Job » (exécuter la tâche) est activé initialement. Si vous cliquez sur ce bouton, la tâche sera lancée et la barre de progression sera créée. Lorsque la tâche est en cours d’exécution, le bouton « Run Job » (exécuter la tâche) est désactivé et le bouton « Cancel Running Job » (annuler la tâche en cours) est activé pendant que la tâche est en cours d’exécution. Si vous cliquez sur ce bouton avant la fin de la tâche, celle-ci sera annulée.

Rappels multiples

Normalement, la valeur d'une sortie est déterminée de manière unique par un rappel. Si plusieurs rappels mettent à jour la même sortie, nous serons confrontés à un scénario dans lequel la sortie aura plusieurs valeurs en même temps et nous ne saurons pas laquelle est la bonne.

Cependant, nous pourrons parfois prendre le risque de lier plusieurs rappels à la même sortie pour simplifier les choses. Dash nous permet de le faire en indiquant expressément que nous autorisons les sorties multiples. Cette fonction s'active lorsque nous fixons la valeur du paramètre allow_duplicate de Output à True. Voici un exemple :

app.layout = html.Div([
    html.Button('Draw Graph', id='draw-2'),
    html.Button('Reset Graph', id='reset-2'),
    dcc.Graph(id='duplicate-output-graph')
])

@app.callback(
    Output('duplicate-output-graph', 'figure', allow_duplicate=True),
    Input('draw-2', 'n_clicks'),
    prevent_initial_call=True
)
def draw_graph(n_clicks):
    df = px.data.iris()
    return px.scatter(df, x=df.columns[0], y=df.columns[1])

@app.callback(
    Output('duplicate-output-graph', 'figure'),
    Input('reset-2', 'n_clicks'),
)
def reset_graph(input):
    return go.Figure()

app.run_server(debug=True)
Figure 4 - Exemple d'une application Dash qui utilise des rappels multiples
Description - Figure 4 : Graphique mis à jour par deux boutons différents

Voici un exemple d’une application Plotly Dash qui utilise des rappels multiples. Elle comporte deux boutons qui ciblent tous les deux les mêmes données de sortie, soit le graphique ci-dessous. Un clic sur le bouton « Draw Graph » (dessiner le graphique) produit le graphique, tandis que le bouton « Reset Graph » (réinitialiser le graphique) efface le graphique. Étant donné que les deux boutons ciblent les mêmes données de sortie, ce scénario nécessite la fonction de rappels multiples de Plotly Dash.

Dans ce cas, nous disposons de deux boutons pour mettre à jour un graphique : Draw (dessiner) et Reset (réinitialiser). Le graphique sera mis à jour par le dernier bouton utilisé. Bien que cela soit pratique, la conception d'une interface utilisateur de cette manière comporte un risque. Dans un ordinateur pourvu d'un seul pointeur de souris, on peut supposer qu'un seul clic de bouton est possible à un moment donné. Par contre, dans le cas d'un écran tactile multipoint comme celui d'un téléphone intelligent ou d'une tablette, il est possible de cliquer sur deux boutons en même temps. En général, dès que nous autorisons des rappels multiples, la sortie devient potentiellement indéterminée, ce qui peut entraîner certains bogues très difficiles à reproduire.

Cette fonctionnalité est à la fois pratique et potentiellement dangereuse. Par conséquent, son utilisation est à vos risques et périls!

Composants personnalisés

Parfois, l'ensemble des composants fournis avec Dash n'est pas suffisant. L'interface utilisateur Web de Dash est créée avec React; Dash fournit un outil pratique pour intégrer des composants React personnalisés dans Dash. Cet article ne traite pas en détail de React ni de l'intégration Dash-React. Cependant, vous pouvez en savoir plus à ce sujet en consultant la page « Build your own components » (en anglais seulement).

Affichage des erreurs

Durant les calculs, il arrive qu'une erreur se produise en raison de problèmes liés aux données, au code ou à une erreur de l'utilisateur. Au lieu d'interrompre l'application, nous pourrions vouloir afficher l'erreur pour l'utilisateur et lui fournir quelques renseignements sur ce qu'il est possible de faire pour la corriger.

Deux fonctions de Dash sont utilisées pour ce scénario : multiple outputs et dash.no_update.

La fonction multiple outputs autorise les rappels et retourne plusieurs sorties sous la forme d'un uplet.

Quant à la fonction dash.no_update, elle prend une valeur et peut la retourner dans un emplacement de sortie pour indiquer qu'il n'y a pas de changement dans cette sortie.

Voici un exemple qui utilise ces deux fonctions pour mettre en œuvre l'affichage d'une erreur :

@app.callback(
    Output('out', 'text'),
    Output('err', 'text'),
    Input('num', 'value')
)
def validate_num(num):
    if validate(num):
        return "OK", ""
    else:
        return dash.no_update, "Error"

Mises à jour partielles

Les calculs de rappel Dash étant effectués sur le serveur, il faut, pour afficher les résultats à l'intention du client, rassembler toutes les valeurs de retour du rappel et les lui envoyer à chaque mise à jour.

Ces mises à jour concernent parfois des objets de figure très volumineux, qui consomment beaucoup de bande passante et ralentissent le processus de mise à jour. Cela aura une incidence négative sur l'expérience d'utilisateur. Une manière simple de réaliser des mises à jour par rappel consiste à effectuer des mises à jour monolithiques sur de grandes structures de données telles que des figures, même si seule une petite partie, comme le titre, a été modifiée.

Pour optimiser l'utilisation de la bande passante et améliorer l'expérience d'utilisateur, Dash dispose d'une fonction appelée « Partial Update » (mise à jour partielle). Cette fonctionnalité introduit un nouveau type de valeur de retour pour les rappels appelé Patch. Patch désigne les sous-composants d'une structure de données plus large qui doivent être mis à jour, ce qui nous permet d'éviter d'envoyer une structure de données entière dans l'ensemble du réseau lorsque seule une partie de celle-ci doit être mise à jour.

Voici un exemple de mise à jour partielle qui ne sert à modifier que la couleur de la police du titre de la figure, au lieu de la figure entière :

From dash import Dash, html, dcc, Input, Output, Patch
import plotly.express as px
import random

app = Dash(__name__)

df = px.data.iris()
fig = px.scatter(
    df, x="sepal_length", y="sepal_width", color="species", title="Updating Title Color"
)

app.layout = html.Div(
    [
        html.Button("Update Graph Color", id="update-color-button-2"),
        dcc.Graph(figure=fig, id="my-fig"),
    ]
)

@app.callback(Output("my-fig", "figure"), Input("update-color-button-2", "n_clicks"))

def my_callback(n_clicks):
    # Defining a new random color
    red = random.randint(0, 255)
    green = random.randint(0, 255)
    blue = random.randint(0, 255)
    new_color = f"rgb({red}, {green}, {blue})"

    # Creating a Patch object
    patched_figure = Patch()
    patched_figure["layout"]["title"]["font"]["color"] = new_color
    return patched_figure

if __name__ == "__main__":
    app.run_server(debug=True)

Interface utilisateur dynamique et filtrage de rappels

Parfois, il n'est pas possible de définir statiquement le flux de données. Si, par exemple, nous voulons créer une pile de filtres qui permet à l'utilisateur d'ajouter des filtres de façon flexible, nous ne saurons pas à l'avance quels filtres ce dernier ajoutera. C'est statiquement impossible de définir des flux de données comportant des composants d'entrée que l'utilisateur ajoute au moment de l'exécution.

Voici un exemple de pile dynamique de filtres à laquelle l'utilisateur peut en ajouter de nouveaux en cliquant sur le bouton ADD FILTER. L'utilisateur peut ensuite sélectionner la valeur du filtre à l'aide de la liste déroulante qui s'ajoute dynamiquement.

Figure 5 - Exemple d'interface utilisateur dynamique dans Dash
Description - Figure 5 : Pile de filtres dynamique

Voici un exemple d’une application Plotly Dash qui utilise une interface utilisateur dynamique et un filtrage de rappels. En cliquant sur le bouton « Add Filter » (ajouter un filtre), vous ajoutez une liste déroulante supplémentaire. Étant donné que les cases déroulantes sont ajoutées de manière dynamique, nous ne pouvons pas les lier aux rappels à l’avance. L’utilisation de la fonction de filtrage de rappels de Dash nous permet de lier des éléments de l’interface utilisateur créés dynamiquement à des rappels à l’aide d’un prédicat de filtrage.

Dash prend en charge ce scénario en nous permettant de lier des rappels à des sources de données de manière dynamique grâce à un mécanisme de filtrage.

Le code suivant met en œuvre l'interface utilisateur ci-dessus :

From dash import Dash, dcc, html, Input, Output, ALL, Patch

app = Dash(__name__)

app.layout = html.Div(
    [
        html.Button("Add Filter", id="add-filter-btn", n_clicks=0),
        html.Div(id="dropdown-container-div", children=[]),
        html.Div(id="dropdown-container-output-div"),
    ]
)


@app.callback(
    Output("dropdown-container-div", "children"), Input("add-filter-btn", "n_clicks")
)
def display_dropdowns(n_clicks):
    patched_children = Patch()
    new_dropdown = dcc.Dropdown(
        ["NYC", "MTL", "LA", "TOKYO"],
        id={"type": "city-filter-dropdown", "index": n_clicks},
    )
    patched_children.append(new_dropdown)
    return patched_children


@app.callback(
    Output("dropdown-container-output-div", "children"),
    Input({"type": "city-filter-dropdown", "index": ALL}, "value"),
)
def display_output(values):
    return html.Div(
        [html.Div(f"Dropdown {i + 1} = {value}") for (i, value) in enumerate(values)]
    )


if __name__ == "__main__":
    app.run_server(debug=True)

Au lieu de définir les composants DropDown de manière statique, nous créons dropdown-container-div, où seront stockés tous les composants DropDown que l'utilisateur créera. Si nous créons les composants DropDown dans display_dropdowns, chaque nouveau composant DropDown sera doté d'un id (identifiant). En règle générale, cette valeur id aurait la forme d'une chaîne de caractères; cependant, pour activer le filtrage des rappels, Dash permet également que id soit un dictionnaire. Il peut s'agir d'un dictionnaire arbitraire, de sorte que les clés de l'exemple ci-dessus ne sont pas des valeurs spéciales. Si id est un dictionnaire, nous pouvons définir des filtres détaillés dont l'appariement est effectué avec chaque clé du dictionnaire.

Dans l'exemple ci-dessus, lorsque l'utilisateur ajoute de nouveaux composants DropDown, les identifiants (id) des composants dynamiques DropDown sont marqués par des identifiants séquentiels comme les suivants :

  1. '{"type": "city-filter-dropdown", "index": 1}
  2. '{"type": "city-filter-dropdown", "index": 2}
  3. '{"type": "city-filter-dropdown", "index": 3}

Ensuite, dans les métadonnées du rappel display_output, nous définissons son entrée comme Input({"type" : « city-filter-dropdown », « index" : ALL}, « value »), qui s'apparie alors à tous les composants dont l'id a un type égal à city-filter-dropdown. En indiquant "index": ALL, nous précisons que l'appariement s'applique à toutes les valeurs de l'indice (index).

Outre ALL, Dash prend également en charge d'autres critères de filtrage tels que MATCH et ALLSMALLER. Pour en savoir davantage sur cette fonctionnalité, consultez la page « Pattern Matching Callbacks » (en anglais seulement).

Exemples

Voici quelques exemples d'applications créées avec Dash :

Figure 6 - Application Dash pour la détection d'objets.
Description - Figure 6 : Détection d'objets

Voici un exemple d’une application Plotly Dash utilisée pour la détection d’objets. Elle permet de visualiser les boîtes de délimitation des objets détectés dans une scène.

Figure 7 - Dash a créé un tableau de bord pour les données éoliennes.
Description - Figure 7 : Tableau de bord

Voici un exemple d’une application du tableau de bord Plotly Dash. Cette application permet de visualiser les données relatives à la vitesse et à la direction du vent.

Figure 8 - Application Dash de visualisation des trajets Uber dans la ville de New York.
Description - Figure 8 :Trajets Uber

Voici un exemple d’une application du tableau de bord Plotly Dash. Elle permet de visualiser la répartition temporelle et spatiale des trajets Uber à Manhattan.

Figure 9 - Tableau de bord Dash des données américaines sur les opioïdes.
Description - Figure 9 : Carte des opioïdes

Voici un exemple d’une application du tableau de bord Plotly Dash. Elle visualise la répartition spatiale des décès attribuables à la toxicité des opioïdes aux États-Unis, selon le comté.

Figure 10 - Interface utilisateur Dash pour la visualisation de nuages de points.
Description - Figure 10 : Nuage de points

Voici un exemple d’une application de visualisation 3D développée à l’aide de Plotly Dash. Cette application permet de visualiser les nuages de points 3D recueillis par un LIDAR dans une voiture.

Figure 11 - Interface utilisateur Dash avec un composant pour la visualisation de maillages 3D
Description - Figure 11 : Maillage 3D

Voici un exemple d’une application de visualisation de maillages 3D développée à l’aide de Plotly Dash. Cette application permet de visualiser la reconstruction du cerveau à partir des données de l’IRM.

D'autres exemples figurent à la page « Dash Enterprise App Gallery » (en anglais seulement).

Conclusion

Une bonne interface utilisateur peut ajouter de la valeur aux projets en rendant les produits livrables plus présentables et utilisables. Pour les systèmes de production qui seront utilisés pendant longtemps, l'investissement préalable dans l'interface utilisateur peut se révéler rentable au fil du temps en réduisant la courbe d'apprentissage, en diminuant la confusion chez les utilisateurs et en améliorant leur productivité. Plotly Dash contribue à réduire considérablement le coût de conception d'interfaces utilisateurs pour les applications de données, ce qui peut augmenter le rendement sur l'investissement dans la conception de telles interfaces.

Rencontre avec le scientifique des données

Inscrivez-vous à la présentation Rencontre avec le scientifique des données

Si vous avez des questions à propos de mon article ou si vous souhaitez en discuter davantage, je vous invite à une Rencontre avec le scientifique des données, un événement au cours duquel les auteurs rencontrent les lecteurs, présentent leur sujet et discutent de leurs résultats.

Jeudi, le 15 juin
De 13 00 h à 16 00 h, HE
MS Teams – le lien sera fourni aux participants par courriel

Inscrivez-vous à la présentation Rencontre avec le scientifique des données.
À bientôt!

Abonnez-vous au bulletin d'information du Réseau de la science des données pour la fonction publique fédérale pour rester au fait des dernières nouvelles de la science des données.

Références

  1. Plotly: Low-Code Data App Development (en anglais seulement)
  2. Rappels en arrière-plan : Plotly - Background Callbacks (en anglais seulement)
  3. Composants personnalisés : Plotly - Build Your Own Components (en anglais seulement)
  4. Rappels de filtrage : Plotly - Pattern-Matching Callbacks (en anglais seulement)
Date de modification :