french_pipeline.jpg D'autres graphiques impressionnants sur cette magnifique galerie.

Principe

Le principe est très simple: Vous générez un fichier texte contenant les informations du graphe.

Vous donnez ensuite ce fichier à un des outils de Graphviz (principalement dot) qui génère ainsi une image de votre graphe. :hehe:

Un premier exemple, simple

Installer graphviz pour votre OS, lancez un terminal et c'est parti! :grenadelauncher:

Le langage dot est très simple et il s'apprend quasiment uniquement par la pratique.

Voici un exemple que je vais détailler:

digraph g {
	graph [
		rankdir = LR
		bgcolor= grey50
	]
	node [
		fontsize = "10"
		shape = box
		style = "rounded,filled"
	]
 
	myNode1 [
		label = "Node One"
		color = lightsalmon
		fillcolor = grey75
	]
	myNode2 [
		label = "Node Two"
		color = lightcoral
		fillcolor = grey75
	]
	myNode3 [
		label = "Node Three"
		color = lightpink
		fillcolor = grey75
	]
	myNode4 [
		label = "Node Four"
		color = orange
		fillcolor = grey75
	]
 
	myNode1 -> myNode2 [ penwidth = 1, fontsize = 8, label = "Connection One" ]
	myNode2 -> myNode3 [ penwidth = 1, fontsize = 8, label = "Connection Two" ]
	myNode1 -> myNode4 [ penwidth = 1, fontsize = 8, label = "Connection Three" ]
 
	subgraph cluster_sg1 {
		label = "subGraph One"
		style = rounded
		color = springgreen4
		myNode1
		myNode4
	}
 
	subgraph cluster_sg2 {
		label = "subGraph Two"
		style = rounded
		color = violetred3
		myNode2
		myNode3
	}
}

Copiez tout ça dans un fichier monFichier.dot puis faites:

dot -Tpng monFichier.dot -o monGraphe.png

Ici, -Tpng sert à dire à dot de créer un graph au format png. (Il y en a d'autres comme -Tsvg. La liste complète est disponible dans la doc de dot, bas de la première page).

Une fois généré, vous devriez avoir ça:

myGraph001.png

Et voilà le travail! :hehe:

Notez qu'on a pas besoin de réfléchir au positionnement des nodes. On déclare, les nodes, les connexions et il génère le graphe. Cette "puissance" peut être bloquante quand justement, on essaye d'organiser un peu les graphes qu'on sort... :reflechi:

Et maintenant les explications.

Vous allez voir que Graphviz peut être très flexible quand vous décrivez vos graphes. Il n'y a pas une seule façon de déclarer un graphe, d’où l’intérêt d’expérimenter un maximum. :idee:

C'est aussi la porte ouverte au n'importe quoi. C'est à vous de faire attention. :papi:

digraph g { ... }

Il ne doit y en avoir qu'un seul et c'est celui qui contient votre graphe. C'est un peu la racine.

:longBar:

graph [
	rankdir = LR
	bgcolor= grey50
]

Contient tout les attributs par défaut du graphe (cf dotguide, page 33).

  • rankdir: Permet de définir le sens du graphe (ici, Left to Right)
  • bgcolor: La couleur du fond du graphe, à choisir parmi les nombreuses couleurs disponibles.

:longBar:

node [
	fontsize = "10"
	shape = box
	style = "rounded,filled"
]

Contient tout les attributs par défaut des nodes (cf dotguide, page 30).

  • fontsize: La taille de la police de caractère utilisée pour les nodes.
  • shape: La forme des nodes (cf dotguide, page 38).
  • style: La "mise en page" des nodes. Ici, bords arrondis (j'aime bien :seSentCon: ) et bg coloré.

:longBar:

myNode1 [
	label = "Node One"
	color = lightsalmon
	fillcolor = grey75
]

S'en suit maintenant la description de chaque node.

Par défaut, les valeurs des attributs sont celles définies dans la déclaration node, juste au dessus.

Ensuite, chaque paramètre vient s'ajouter:

  • label: Ce qui est écrit dans le node (nous verrons plus loin quand on souhaite faire de la mise en forme plus complexe)
  • color: La couleur des bords du node, à choisir parmi les nombreuses couleurs disponibles.
  • fillcolor: La couleur du fond du node.

On déclare plusieurs nodes comme ça puis on attaque les connections.

:longBar:

myNode1 -> myNode2 [ penwidth = 1, fontsize = 8, label = "Connection One" ]

Les attributs et leurs valeurs par défauts sont dispos sur le dotguide, page 32.

  • penwidth: La taille du tracé de la connexion (Inutile dans ce cas là car, comme vous pouvez le constater dans la doc, la taille par défaut est déjà à 1). :baffed:
  • fontsize: La taille du texte de la connexion.
  • label: Ce qui est écrit dans la connection (notez que j’écris rarement un texte sur une connexion).

:longBar:

Viennent ensuite les subgraphs qui sont tout à fait optionnel mais peuvent servir à donner un peu d'ordre à vos graphes:

subgraph cluster_sg1 {
	label = "subGraph One"
	style = rounded
	color = springgreen4
	myNode1
	myNode4
}
  • label: Le nom du subgraph qui s'affichera.
  • style: Le "style" du subgraph (comme les nodes).
  • color: La couleur des contours du subgraph.

Viennent ensuite la liste des nodes contenu dans le subgraph.

Important: Les identifiants des subgraphs doivent toujours commencer par "cluster" sinon ils ne s'afficheront pas. Je m'en suis fait des bosses à me taper la tête contre les murs avec ça. :aupoil:

Nous avons donc fini avec le premier exemple. Nous allons maintenant prendre un cas plus concret. :enerve:

Un graphe de dépendance de révision

Comme je ne m'en suis servi que dans le cadre d'une production CG, je placerai mon exemple dans ce contexte. Peut être existe t'il mille façons d'utiliser dot et que la mienne est bien la dernière, mais après tout, c'est ce qui nous intéresse non? :dentcasse:

Nous allons prendre l'exemple d'un graphe de dépendance de révision. L'avantage de ce genre de graph, c'est qu'il est souvent de type tree.

Avant propos, Python est ton amis

Je vais passer brièvement sur le "comment" de la génération des fichiers .dot.

Pour générer un tel fichier, je me suis servi des classes en Python auquel je rajoutais des attributs.

Chaque nœud du graphe est en fait un objet Python de type RevNode contenant tous ses attributs ainsi qu'une liste de tous ses enfants.

L'idée est de créer une liste de RevNode, chacun ayant ces propres attributs et les autres Nodes auxquels il se connecte.

Bien entendu, vous récupérez automatiquement ces informations dans la base de donnée de votre pipeline. :sourit:

Vous partez donc d'une révision que vous choisissez, puis vous la parcourez en arbre via une fonction récursive.

class RevNode() :
 
 
	def __init__( self ) :
 
		self.nodeName = ""	# Ici, un identifiant unique du node, c'est à vous de voir
		self.assetName = ""	# "chaussette"
		self.assetType = ""	# anim, model, etc...
		self.rev = ""	# "Last", "Validated", "5", etc...
		self.childrenList = []	# List de tout les RevNode enfant de celui ci
 
	def getAllRevNode( self ) :
		"""Renvoit ce node plus ces enfants"""
 
		revNodeList = []
 
		revNodeList.append( self )	# on ajoute ce node
 
		for revNode in self.childrenList :
 
			revNodeList += revNode.getAllAssetNode()
 
		return assetNodeList
Oui je sais, les fonctions récursives en Python, caymal :baffed:

Arrivé à ce stade, vous avez une liste de RevNode.

Il faut ensuite générer le fichier .dot contenant tout ça. Nous allons voir que l'on peut "formater" le contenu d'un node de manière plus personnalisé grâce à une possibilité intéressante de dot qui permet de lui donner un label qui sera interprété en HTML (cf dotguide, page 9).

HTML dans dot

Il faut préciser à dot que le label que vous lui donnez devra être interprété en HTML.

Pour ça, rien de plus simple, il faut simplement placer le contenu HTML entre < ... > :

label = < maDescriptionHTML >

Le plus dur étant de justement avoir de l'HTML valide. :sourit:

Un exemple:

<TABLE BORDER="0" CELLBORDER="0" CELLSPACING="0">
<TR><TD ALIGN="right" ><FONT COLOR="deeppink">Name:</FONT></TD><TD ALIGN="left" >Chausset</TD></TR>
<TR><TD ALIGN="right" ><FONT COLOR="blue4">Type:</FONT></TD><TD ALIGN="left" >Model</TD></TR>
<TR><TD ALIGN="right" ><FONT COLOR="darkorchid4">Rev:</FONT></TD ALIGN="left" ><TD>5</TD></TR>
</TABLE>

Mettez tout ça entre < ... >.

Expérimentez! :sourit:

Let's go!

Voici un graphe qui pourrait ressembler à un graphe de dépendance:

digraph g {
	graph [
		rankdir = RL
		bgcolor= grey50
	]
	node [
		fontsize = "10"
		shape = box
		style = "rounded,filled"
	]
	edge [
		fontsize = "8"
	]
 
	myNodeSet1 [
		label = <<TABLE BORDER="0" CELLBORDER="0" CELLSPACING="0">
<TR><TD ALIGN="right" ><FONT COLOR="blue4">Name:</FONT></TD><TD ALIGN="left" >Etagere</TD></TR>
<TR><TD ALIGN="right" ><FONT COLOR="blue4">Type:</FONT></TD><TD ALIGN="left" >Set</TD></TR>
<TR><TD ALIGN="right" ><FONT COLOR="darkorchid4">Rev:</FONT></TD><TD ALIGN="left" >7</TD></TR>
</TABLE>>
		color = blue4
		fillcolor = deepskyblue1
	]
	myNodeModel1 [
		label = <<TABLE BORDER="0" CELLBORDER="0" CELLSPACING="0">
<TR><TD ALIGN="right" ><FONT COLOR="darkgreen">Name:</FONT></TD><TD ALIGN="left" >Chaussette</TD></TR>
<TR><TD ALIGN="right" ><FONT COLOR="blue4">Type:</FONT></TD><TD ALIGN="left" >Model</TD></TR>
<TR><TD ALIGN="right" ><FONT COLOR="darkorchid4">Rev:</FONT></TD><TD ALIGN="left" >5</TD></TR>
</TABLE>>
		color = darkgreen
		fillcolor = darkolivegreen4
	]
	myNodeCache1 [
		label = <<TABLE BORDER="0" CELLBORDER="0" CELLSPACING="0">
<TR><TD ALIGN="right" ><FONT COLOR="deeppink">Name:</FONT></TD><TD ALIGN="left" >Chaussette</TD></TR>
<TR><TD ALIGN="right" ><FONT COLOR="blue4">Type:</FONT></TD><TD ALIGN="left" >Cache</TD></TR>
<TR><TD ALIGN="right" ><FONT COLOR="darkorchid4">Rev:</FONT></TD><TD ALIGN="left" >5</TD></TR>
</TABLE>>
		color = violetred3
		fillcolor = coral
	]
	myNodeModel2 [
		label = <<TABLE BORDER="0" CELLBORDER="0" CELLSPACING="0">
<TR><TD ALIGN="right" ><FONT COLOR="darkgreen">Name:</FONT></TD><TD ALIGN="left" >Chemise</TD></TR>
<TR><TD ALIGN="right" ><FONT COLOR="blue4">Type:</FONT></TD><TD ALIGN="left" >Model</TD></TR>
<TR><TD ALIGN="right" ><FONT COLOR="darkorchid4">Rev:</FONT></TD><TD ALIGN="left" >12</TD></TR>
</TABLE>>
		color = darkgreen
		fillcolor = darkolivegreen4
	]
	myNodeCache2 [
		label = <<TABLE BORDER="0" CELLBORDER="0" CELLSPACING="0">
<TR><TD ALIGN="right" ><FONT COLOR="deeppink">Name:</FONT></TD><TD ALIGN="left" >Chemise</TD></TR>
<TR><TD ALIGN="right" ><FONT COLOR="blue4">Type:</FONT></TD><TD ALIGN="left" >Cache</TD></TR>
<TR><TD ALIGN="right" ><FONT COLOR="darkorchid4">Rev:</FONT></TD><TD ALIGN="left" >12</TD></TR>
</TABLE>>
		color = violetred3
		fillcolor = coral
	]
 
	myNodeModel1 -> myNodeCache1
	myNodeModel2 -> myNodeCache2
	myNodeCache1 -> myNodeSet1 [label = "used in"]
	myNodeModel2 -> myNodeSet1 [label = "used in"]
 
	subgraph cluster_model1 {
		label = "Models"
		style = "rounded, filled"
		color = darkgreen
		fillcolor = darkolivegreen3
		myNodeModel1
		myNodeModel2
	}
 
	subgraph cluster_cache1 {
		label = "Caches"
		style = "rounded, filled"
		color = violetred3
		fillcolor = darksalmon
		myNodeCache1
		myNodeCache2
	}
}

Le résultat:

myGraph002.png

L'objectif est donc de faire un script python capable de générer un tel fichier, de l’exécuter à la volé et de l'afficher à l'utilisateur. :jdicajdirien:

Ce graphe est minuscule. Ça commence à devenir intéressant quand les informations abondent (dans le cas d'une "vraie" prod, les infos, il en pleut à torrent! :hehe: ).

Liens

N'oubliez pas de passer en revue les exemples proposés dans la galerie qui sont tous disponibles avec leur code dot.

Le guide contient aussi pas mal d'infos.

Sans oublier internet qui regorge d'exemples. :hehe:

Conclusion

Et voilà! J'espère que ça vous aura donné envie d'essayer de faire des graphes quand la situation l'exige. :laClasse:

Je pense qu'on ne se rend compte de l’intérêt d'un tel truc qu'une fois qu'on en a vu un qui correspond à nos propres besoins.

A bientôt!

Dorian

:marioCours: