8/ Interface Graphique

Résumé

L’interface graphique n’est pas à délaisser. La mise en forme est primordiale. Les images ont une influence puissante dans la compréhension de système complexe. Ces représentations sont nécessaires pour une prise de décision éclairée.

Le menu permet d’interagir avec les paramètres de la méthode pour effectuer de nouvelles comparaisons d’impacts. L’affichage du Donut donne une visualisation puissante pour comprendre les impacts d’un système.


1/ Une représentation visuelle: le Donut

Les images sont très puissantes pour expliquer un concept inconnu, elles s’emparent de la première couche, de la Tabula Rasa. - soutenait Paul Samuelson en 1948.

Cette phrase de Samuelson nous rappelle une chose fondamentale: les images sont instantanément absorbées sans aucune médiation car les spectateurs ne sont généralement pas appelés à les analyser ou à les déconstruire comme c’est le cas quand il s’agit d’un message verbal [1]. La perception d’une image entraine une réaction émotionnelle qui associe l’information contenu dans la représentation à une émotion. Bref, l’image est un vecteur puissant et encore mal maîtrisé pour transmettre de l’information.

C’est pourquoi la représentation économique du Donut de Kate Raworth[2] est si intéressante pour représenter des impacts.

Donut de Kate Raworth

Image 1: Le Donut de Kate Raworth

L’espace sûre et juste, souvent en vert, est représenté comme un cercle. Les impacts environnementaux vers l’extérieur, et les dégradations sociales à l’intérieur.

L’application du Donut a fait le choix de cette représentation pour illustrer les impacts du projet. Cependant, il convient d’y appliquer quelques ajustements pour le mettre au goût des capabilités.

Donut de Kate Raworth

Image 2: Le Donut des capabilités

Le donut des capabilités représentent les impacts relative d’un projet, entre un scénario et une référence. Si un impact sur un Endpoint du scénario est supérieur à celui de la référence (c’est à dire si la valeur d’impact est >1) alors l’impact est représenté en bordure du Donut. Si c’est l’impact de la référence qui dépasse celui du scénario (c’est à dire si la valeur d’impact est <1) alors l’impact est représenté dans le Donut. Cette double direction d’impact met en évidence les points faibles du projet (là ou l’impact sort du Donut), mais aussi les points forts (là ou le projet améliore des capabilités).

2/ Accessibilité et expérience utilisateur

Le développement de l’interface telle qu’elle est aujourd’hui (Aout 2023) est le fruit d’un compromis. La volonté de l’équipe est de construire une application ergonomique et facile d’utilisation. Cependant, au stade du projet, il n’est pas pertinent de passer autant de temps sur l’ajout de nombreuses fonctionnalités graphiques, stylistiques… C’est pourquoi, nous avons souhaitez gardez le développement de l’interface au juste minimum: une utilisation qui ne demande pas de compétences informatiques complexes.

En effet, en suivant le tutoriel d’installation, il vous suffit ensuite de cliquer sur le fichier run.sh pour lancer l’interface graphique. Il n’y a besoin de passer en ligne de commande (celles qui font peur aux non-initiés). Dans cette volonté d’appropriation se cache néanmoins une éthique de compostabilité du code puisqu’il est possible d’installer l’application par de nombreuses autres manière: via Gitlab, avec un IDE, en ligne de commande. Libre à chacun d’utiliser le niveau qui lui correspond le mieux.

3/ Application Donut: Création de l’interface graphique

3.2/ Class Parameters()

L’usage de ce menu à paramètre dans le fichier main.py demande plusieurs variables pour stocker les choix utilisateurs. Or, ces variables sont utilisés dans plusieurs fonctions et actualisé dans plusieurs fichier. Afin de permettre cela, tous les paramètres sont stocké dans une classe: Parameters().

> Graph.py

class Parameters:  
   def __init__(self):  
        self.mode = 0  
        self.ref_name = 0  
        self.unity = ''  
        self.type = 0  
        self.stakeholder = 'Local_Community'  
        self.scenario_name = 0  
        self.List_name = ['']  
        self.error_shown = 0

La classe Parameters() ne possède aucune méthode, seulement les attributs ci-dessus. Voir la documentation de la classe pour plus de détails.

3.3/ Graphique avec Matplotlib

Le tracé des graphique Donut se fait avec la librairie python mathplotlib, ainsi que numpy. Tout le code correspondant est écrit dans le fichier Graph.py.

Graph.py contient la classe Parameters ainsi que trois fonctions:

  • plot_donut: utilisé pour tracer un donut

  • plot_type_1: Utilisé pour créer la figure qui recevra le Donut dans le cas Specific stakeholder

  • plot_type_2: Utilisé pour créer la figure et les Donuts dans le cas Multiple stakeholder

Fonction plot_donut()

plotdonut

Image 4: Graphique d'un Donut par plot_donut()

Cette fonction prend en paramètre une figure, un axe, des limites min et max pour l'axe, un titre, et le nom du scenario et de la partie prenante qui doit être tracé. La fonction génère un diagramme en barres avec les données qui lui sont fournies.

Ce code va charger les paramètres et modifier le style de l’axe afin de le rendre moins encombrant:

> Graph.py

# Layout parameters  
ax.xaxis.grid(False)  
ax.yaxis.grid(False)  
ax.spines["start"].set_color("none")  
ax.set_title(name, fontsize=20, ha="center", va="center")  
COLOR_OUT_DONUT = "#08A04B"  
COLOR_BAR_M = "#CD7F32"  
COLOR_BAR_L = "#C04000"  
COLOR_DONUT = "#3A5F0B"  
  
# Recover labels IN and OUT for the donut  
df = df.loc[df['Stakeholders'] == stakeholder]  
value_scenario_out = df.loc[df['Category_ID'] == 'OUT', scenario].values  
value_scenario_in = df.loc[df['Category_ID'] == 'IN', scenario].values  
  
label_out_ = df.loc[df['Category_ID'] == 'OUT', 'Name'].values  
label_in_ = df.loc[df['Category_ID'] == 'IN', 'Name'].values  
  
error_out = df.loc[df['Category_ID'] == 'OUT', 'Errorbar'].values  
error_in = df.loc[df['Category_ID'] == 'IN', 'Errorbar'].values  
  
# Security if there is no values to plot.  
if len(label_out_) == 0 or len(label_in_) == 0:  
    return  
    
# Definition of the geometrical parameters, in MAJUSCULE.  
DONUT = 2  
ANGLES_OUT = np.linspace(0, 2 * np.pi, len(label_out_), endpoint=False)  
ANGLES_IN = np.linspace(0, 2 * np.pi, len(label_in_), endpoint=False)  
WIDTH_OUT = 2 * np.pi / len(label_out_)  # Determine the width of each bar.  
OFFSET_OUT = np.pi / 2  # Determines where to place the first bar.  
WIDTH_IN = 2 * np.pi / len(label_in_)  # Determine the width of each bar.

Ensuite, on s’occupe d’afficher les noms des Endpoints (sous forme de label à l’exterieur et de texte à l’intérieur):

> Graph.py

# a/ Labels  
# Add Labels (OUT) and text (IN)  
# Note the 'wrap()' function. The '10' means we want at most 10 consecutive letters in a word,  
# but the 'break_long_words' means we don't want to break words longer than 10 characters.  
for v in range(0, len(label_out_)): label_out_[v] = str(label_out_[v])  
for v in range(0, len(label_in_)): label_in_[v] = str(label_in_[v])  
label_out = ["\n".join(wrap(r, 10, break_long_words=False)) for r in label_out_]  
label_in = ["\n".join(wrap(r, 10, break_long_words=False)) for r in label_in_]  
  
# Add space for labels:  
XTICKS = ax.xaxis.get_major_ticks()  
for tick in XTICKS: tick.set_pad(6)  
# Add labels OUT  
ax.set_xticks(ANGLES_OUT)  
ax.set_xticklabels(label_out, size=6)  
  
# Add text for IN  
for i in range(0, len(label_in)):  
    texte = label_in[i]  
    angle = ANGLES_IN[i]  
    ax.text(angle, -(DONUT / 2) - 2, texte, fontsize=6, ha="center", va="center")

La partie suivante vient imposer les limites min et max de l’axe, et créé des masques en fonction de la valeur de l’Endpoint. Cela permet de séparer, par exemple, les valeurs qui sont à coté intérieur l’intérieur du donut (mask_IN_in) de celles qui sont coté intérieur à l’exterieur du Donut (mask_IN_out).

> Graph.py

# b/ Style and limits  
ax.set_ylim(-lim_in, lim_out)  
ax.set_theta_offset(OFFSET_OUT)  # Specify offset  
  
# Mask to differentiate values for outer circle  
mask_OUT_low = value_scenario_out < 0  
mask_OUT_in = (value_scenario_out >= 0) & (value_scenario_out < DONUT / 2)  
mask_OUT_out = (value_scenario_out >= DONUT / 2) & (value_scenario_out < lim_out)  
mask_OUT_high = value_scenario_out >= lim_out  
  
# Mask to differentiate values for inner circle  
mask_IN_low = value_scenario_in < 0  
mask_IN_in = (value_scenario_in >= 0) & (value_scenario_in < DONUT / 2)  
mask_IN_out = (value_scenario_in >= DONUT / 2) & (value_scenario_in < lim_in)  
mask_IN_high = value_scenario_in >= lim_in

Enfin, on plot les barres et les barres d’erreurs de chacun des masques, en leur donnant la couleur qui leur correspond. La liste Error contient un booléen indiquant si oui on non l’on doit afficher les barres d’erreurs. Ce paramètre se règle grâce à l’interactivité dans plot_type_1().

> Graph.py

# d/ Plotting  
  
# Donut plotting  
ax.bar(0, DONUT / 2, color=COLOR_DONUT, alpha=1, width=2 * np.pi, zorder=0)  
ax.bar(0, -DONUT / 2, color=COLOR_DONUT, alpha=1, width=2 * np.pi, zorder=0)  
  
# Outer circle  
# Plotting of values outside the donut  
  
ax.bar(ANGLES_OUT[mask_OUT_high], value_scenario_out[mask_OUT_high],  
       color=COLOR_BAR_L, alpha=0.6, width=WIDTH_OUT, zorder=-1)  
ax.bar(ANGLES_OUT[mask_OUT_out], value_scenario_out[mask_OUT_out],  
       color=COLOR_BAR_M, alpha=0.6, width=WIDTH_OUT, zorder=-1)  
  
# Plotting of values inside the donut  
ax.bar(ANGLES_OUT[mask_OUT_in], -value_scenario_out[mask_OUT_in], yerr=error_out[mask_OUT_in],  
       color=COLOR_OUT_DONUT, alpha=1, width=WIDTH_OUT, zorder=1, bottom=DONUT / 2)  
ax.bar(ANGLES_OUT[mask_OUT_low], -DONUT / 2, yerr=error_out[mask_OUT_low],  
       color=COLOR_OUT_DONUT, alpha=1, width=WIDTH_OUT, zorder=1, bottom=DONUT / 2)  
  
# Plotting error bar  
if error[1]:  
    ax.errorbar(ANGLES_OUT[mask_OUT_out], value_scenario_out[mask_OUT_out], yerr=error_out[mask_OUT_out],  
                capsize=4, capthick=4, color="red", linestyle='')  
    ax.errorbar(ANGLES_OUT[mask_OUT_in], -value_scenario_out[mask_OUT_in], yerr=error_out[mask_OUT_in],  
                capsize=4, capthick=4, color="green", linestyle='')  
  
# Inner circle  
# Plotting of values inside the donut  
ax.bar(ANGLES_IN[mask_IN_high], -lim_in, yerr=error_in[mask_IN_high],  
       color=COLOR_BAR_L, alpha=0.6, width=WIDTH_IN, zorder=-1)  
ax.bar(ANGLES_IN[mask_IN_out], -value_scenario_in[mask_IN_out], yerr=error_in[mask_IN_out],  
       color=COLOR_BAR_M, alpha=0.6, width=WIDTH_IN, zorder=-1)  
  
# Plotting of values outside the donut  
ax.bar(ANGLES_IN[mask_IN_in], value_scenario_in[mask_IN_in], yerr=error_in[mask_IN_in],  
       color=COLOR_OUT_DONUT, alpha=1, width=WIDTH_IN, zorder=1, bottom=-DONUT / 2)  
ax.bar(ANGLES_IN[mask_IN_low], DONUT / 2, yerr=error_in[mask_IN_low],  
       color=COLOR_OUT_DONUT, alpha=1, width=WIDTH_IN, zorder=1, bottom=-DONUT / 2)  
  
# Plotting error bar  
if error[0]:  
    ax.errorbar(ANGLES_IN[mask_IN_out], -value_scenario_in[mask_IN_out], yerr=error_in[mask_IN_out],  
                capsize=4, capthick=4, color="red", linestyle='')  
    ax.errorbar(ANGLES_IN[mask_IN_in], value_scenario_in[mask_IN_in], yerr=error_in[mask_IN_in],  
                capsize=4, capthick=4, color="green", linestyle='')

Pour plus de détails, voir la documentation de plot_donut().

Fonction plot_type_1()

Cette figure matplotlib est divisée en deux parties:

> Graph.py

fig, (legend, ax) = plt.subplots(1, 2, figsize=(15, 10), subplot_kw={"projection": "polar"}, label='Donut Tool')

La diagramme de barres issu de la fonction plot_donut() est courbé grâce au choix de projection polaire pour la figure.

donut_plot_type_1

Image 5: Affichage de l'interface Donut de plot_type_1()

La figure comprend:

  • Sur sa partie droite le donut tracé.

  • Sur sa partie gauche, au milieu, un tableau contenant les valeurs des Endpoints.

  • Sur sa partie gauche, en bas, des boutons pour modifier les paramètres d’affichages suivant:

    • « Go Back » pour fermer la figure.

    • « Refresh » pour réactualiser la figure.

    • « Limit IN et Limit OUT » permettent de régler l’échelle du Donut, ainsi on peut agrandir ou diminuer la taille du trou, et de la partie verte du Donut.

    • « ErrorBar In ? et ErrorBar OUT ? » permettent d’afficher ou de cacher les barres d’erreurs intérieures et extérieures.

Fonction plot_type_2()

donut_plot_type_2

Image 6: Affichage de l'interface Donut de plot_type_2()

Cette fois ci, la figure est en 2x3, comme une boite d’œuf. Toutes les zones comprennent un Donut d’un des stakeholders sauf la zone en bas à droite, qui contient un bouton « Go back » pour fermer la figure.

> Graph.py

def plot_type_2(database_output, scenario_name):
	...  
	stakeholders = ['Local_Community', 'Society', 'Consumers', 'Workers', 'Chain_Value_Actors']  
	############################################################  
	'''2.a/ Figure creation'''  
	fig, axes = plt.subplots(2, 3, figsize=(25, 10), subplot_kw={"projection": "polar"}, label='Donut Tool')  
	fig.tight_layout(pad=10.0)  
	  
	############################################################  
	'''2.b/ Buttons and interactivity'''  
	ax_button = plt.axes([0.8, 0.05, 0.10, 0.03], label='Go Back')  
	button_goback = Button(ax_button, 'Go Back', color=col)  
	  
	############################################################  
	'''2.c/ Update functions'''  
	def back(val):  
	    plt.close(fig)  
	  
	############################################################  
	'''2.d/ Drawing'''  
	for i in range(0, 2):  
	    for j in range(0, 3):  
	        ax = axes[i, j]  
	        if 3*i+j < 5:  
	            plot_donut(database_output, ax, scenario_name, stakeholders[3*i+j], 10, 10, stakeholders[3*i+j])  
	        else:  
	            ax.grid(False)  
	            ax.axis('off')  
  
	button_goback.on_clicked(back)  
	plt.show()