Cas d'études - Tutoriel


Diagramme Polygones avec Tableau et Python

Diagramme Polygones avec Tableau et Python



ParJulien Godenir

...

16 Juillet 2019

Comment j'ai réalisé le rapport d'activité du CNRS avec Tableau

Bonjour à tous, récemment j'ai été contacté par le CNRS afin de réaliser un dashboard interactif web qui présenterait le rapport d'activité 2018 du CNRS.

Une des demandes du client était de réaliser un dashboard clair notamment sur la répartition du budget. Afin de pouvoir visualiser correctement la diversité des sources de budget du CNRS, il était demandé de scinder le diagramme circulaire en deux comme montré ci-dessous:

Malheureusement, je ne sentais pas possible de réaliser un effet joli avec l'outil de diagramme circulaire de Tableau. J'ai donc pensé que ce serai un bon exercice pour utiliser les diagrammes Polygones de Tableau afin de dessiner très exactement ce que je souhaitais. L'idée était donc de tracer à l'aide de polygones le dessin exact souhaité.

Etape 1 : Le code Python

Nous allons devoir écrire pas mal de code, je travaille régulièrement en Python pour l'écriture de script rapide et non réutilisés. C'est un langage simple et complet qui se prête bien à ce genre d'usage. 

Nous avons besoin de la librairie math ainsi que pandas et csv pour charger nos données d'un fichier Excel.

 # -*- coding: utf-8 -*-
import pandas as pd
import math, csv, os, codecs
from math import sqrt
from operator import itemgetter

Les premières fonctions dont nous allons avoir besoin permettent de transcrire des coordonnées polaires en coordonnées cartésiennes:

 # conversion de coordonnées polaires en coordonnées X, Y
def getXY(r, rad, offset_x=0, offset_y=0):
x = r * math.cos(rad) + offset_x
y = r * math.sin(rad) + offset_y
return x, y

# Conversion d'un pourcentage de graphe circulaire en radians
def percToRad(perc):
return math.pi * 2 * perc

Ensuite, on s'appuie sur ces fonctions de base pour construire une fonction qui renvoie une liste de point entre deux angles à un rayon donné

 # Retourne une liste de coordonnées entre un angle de départ sur un pourcentage donné
def listOfCoordinates(start_rad, perc, r, offset_x=0, offset_y=0, end_x=None, end_y=None):
end_rad = DIRECTION * percToRad(perc) + start_rad
rad = start_rad
output = []
while DIRECTION * rad < DIRECTION * end_rad:
output.append(getXY(r, rad, offset_x, offset_y))
rad += DIRECTION * INCREMENT
output.append(getXY(r, end_rad, offset_x, offset_y))
if end_x!=None:
output[-1] = [end_x, end_y]
return output, rad-DIRECTION * INCREMENT

J'ai choisi d'utiliser un digramme de type "donught" plutôt qu'un diagramme circulaire classique. Il me faut donc tracer une portion de diagramme en revenant en arrière avec un rayon légèrement plus petit (j'ai pris 60% plus petit mais vous pouvez régler cela comme vous le souhaitez).

# Pour une liste de points, retourne les points dans l'ordre formant un diagramme circulaire
# de type "donught"
def getPoints(_list, start_rad, r, offset_x, offset_y):
output = []
small_r = 0.6*r
end_x, end_y = getXY(r, start_rad, offset_x, offset_y)
small_end_x, small_end_y = getXY(small_r, start_rad, offset_x, offset_y)
sec = [[end_x, end_y]]
for i in range(len(_list)):
row = _list[i]
[x1, y1] = [end_x, end_y] if (i == len(_list) - 1) else [None, None]
[small_x1, small_y1] = [small_end_x, small_end_y] if (i == len(_list) - 1) else [None, None]
old_rad = start_rad
lst, start_rad = listOfCoordinates(start_rad, row[2], r, offset_x=offset_x, offset_y=offset_y, end_x=x1, end_y=y1)
small_lst, old_rad = listOfCoordinates(old_rad, row[2], small_r, offset_x=offset_x, offset_y=offset_y, end_x=small_x1, end_y=small_y1)
sec = lst
lst = [[row[0], x[0], x[1], row[1], row[2]] for x in lst]
small_lst = [[row[0], x[0], x[1], row[1], row[2]] for x in small_lst]
output+=lst
output+=small_lst[::-1]
return output, sec[1:-1]

Le but étant d'orienter le regard de l'utilisateur vers le diagramme circulaire de droite, j'ai volontairement choisi d'agrandir démesurément le diagramme de droite. Pour cela, je place le premier diagramme en X=-4 et Y=0 avec un rayon de 2. Le deuxième diagramme sera placé en X=5, Y=-5 et R=5. Le premier diagramme commencera en pi/8 et le deuxième en pi/2.

Voici ce à quoi ressemble le code dans son ensemble

# -*- coding: utf-8 -*-
import pandas as pd
import math, csv, os, codecs
from math import sqrt
from operator import itemgetter

INCREMENT = 0.05
DIRECTION = 1
R = 2
OFFSET_X = -1
OFFSET_Y = 0
START_RAD = math.pi /4
FILE = "output.csv"

def loadData():
df = pd.read_excel(io="./votre/fichier.xlsx", sheet_name="Feuille 1", encoding="unicode")
rows = df.values
lst = []
for row in rows:
r =[]
refuse = False
for c in row:
try:
if math.isnan(c):
refuse = True
except Exception as e:
pass
r.append(c)
if not refuse:
lst.append(r)
return lst[:2], lst[3:]

# conversion de coordonnées polaires en coordonnées X, Y
def getXY(r, rad, offset_x=0, offset_y=0):
x = r * math.cos(rad) + offset_x
y = r * math.sin(rad) + offset_y
return x, y

# Conversion d'un pourcentage de graphe circulaire en radians
def percToRad(perc):
return math.pi * 2 * perc

# Retourne une liste de coordonnées entre un angle de départ sur un pourcentage donné
def listOfCoordinates(start_rad, perc, r, offset_x=0, offset_y=0, end_x=None, end_y=None):
end_rad = DIRECTION * percToRad(perc) + start_rad
rad = start_rad
output = []
while DIRECTION * rad < DIRECTION * end_rad:
output.append(getXY(r, rad, offset_x, offset_y))
rad += DIRECTION * INCREMENT
output.append(getXY(r, end_rad, offset_x, offset_y))
if end_x!=None:
output[-1] = [end_x, end_y]
return output, rad-DIRECTION * INCREMENT

# Pour une liste de points, retourne les points dans l'ordre formant un diagramme circulaire
# de type "donught"
def getPoints(_list, start_rad, r, offset_x, offset_y):
output = []
small_r = 0.6*r
end_x, end_y = getXY(r, start_rad, offset_x, offset_y)
small_end_x, small_end_y = getXY(small_r, start_rad, offset_x, offset_y)
sec = [[end_x, end_y]]
for i in range(len(_list)):
row = _list[i]
[x1, y1] = [end_x, end_y] if (i == len(_list) - 1) else [None, None]
[small_x1, small_y1] = [small_end_x, small_end_y] if (i == len(_list) - 1) else [None, None]
old_rad = start_rad
lst, start_rad = listOfCoordinates(start_rad, row[2], r, offset_x=offset_x, offset_y=offset_y, end_x=x1, end_y=y1)
small_lst, old_rad = listOfCoordinates(old_rad, row[2], small_r, offset_x=offset_x, offset_y=offset_y, end_x=small_x1, end_y=small_y1)
sec = lst
lst = [[row[0], x[0], x[1], row[1], row[2]] for x in lst]
small_lst = [[row[0], x[0], x[1], row[1], row[2]] for x in small_lst]
output+=lst
output+=small_lst[::-1]
return output, sec[1:-1]

if __name__ == "__main__":
first_list, second_list = loadData()
output = []

#
# 1er graphe circulaire
#
DIRECTION=-1
R1 = 2
X1=-4
Y1=0
output, sec = getPoints(first_list, math.pi / 8 , R1, X1, Y1)

#
# 2e graphe circulaire
#
DIRECTION=-1
R2 = 5
X2 = 5
Y2 = -5
output2, sec2 = getPoints(sorted(second_list, key=itemgetter(2))[::-1], math.pi/2, R2, X2, Y2)

output+=output2

with codecs.open(FILE, 'a', encoding="utf8") as my_csv:
for i in range(len(output)):
row = output[i]
_str = u",".join(str(row))
my_csv.write(_str+","+str(u+i+1)+"\n")

A ce stade, j'ai donc des listes de points ordonnées qui tracent chaque portion de mes deux diagrammes circulaires. Il me faudra simplement rajouter un champ incrémenté automatiquement que nous spécifierons à Tableau pour qu'il suive le tracé! En fait, Tableau se contentera de joindre les points entre eux en suivant les numéros, comme les enfants le font avec leurs jeux éducationnels.
https://www.onlykidsonly.com

Etape 2 : Utiliser les polygones dans Tableau 

Le gros du travail est fait désormais! Vous pouvez importer les données dans Tableau. Glissez X sur les colonnes et Y sur les lignes. N'oubliez pas de décocher l'agrégation des mesures pour la vue que nous allons faire dans Analyse -> Agréger les mesures

Changer agrégation dans Tableau

Changez le type de graphe pour polygone. Vous devriez avoir quelque chose d'assez brouillon car Tableau essaiera de lier les points entre eux sans forcément le faire dans l'ordre:

Polygones dans Tableau

Glissez ID (ou votre champ incrémenté) sur le chemin. Vous devriez voir un dessin beaucoup plus propre ainsi:

Chemin de polygones dans Tableau

Terminons par remplir les polygones de couleurs. On s'appuiera des couleurs pour appuyer la correspondance entre chaque partie de chaque diagramme ainsi : 

Polygones avec couleur dans Tableau

Et voilà le résultat !