Les éléments à mettre en œuvre pour ce prototype sont :
Les éléments qui ne sont pas traités (mais qui sont traitables avec plus de temps dans une version postérieure au prototype) :
Selon Wikipédia, "le diagramme de Gantt est un outil utilisé en gestion de projet permettant de visualiser dans le temps les diverses tâches composant un projet. Il s'agit d'une représentation d'un graphe connexe, valué et orienté, qui permet de représenter graphiquement l'avancement du projet. (...) Le concept a été nommé d'après Henry L. Gantt, ingénieur américain qui a publié la description du diagramme en 1910".
Je n'ai jamais eu l'occasion de mettre en pratique le développement d'une Power Apps avec un affichage de type "diagramme de Gantt" pour le compte d'un client.
Naturellement, j'ai fait une recherche sur internet afin de voir si certains avaient déjà eu l'occasion de faire ce genre d'application. Bingo 🥳 On trouve un article de 2021 d'un certain Ron Larsen : Gantt-like Display Using Standard Power Apps Controls.
L'article de Ron permet donc de valider que l'idée d'une telle application est réalisable avec Power Apps.
Datant de 2 ans, j'avais cependant envie de mettre en pratique par moi-même. Et peut-être que les nouveaux composants modernes introduits fin 2023 peuvent améliorer l'ergonomie 🤔
Également, j'avais envie d'estimer la charge de travail nécessaire pour mettre en place un prototype fonctionnel.
Et comme je n'avais pas envie de garder ce projet pour moi, je le partage ici, en français et sur LinkedIn !
Depuis un an, impossible d'échapper à ChatGPT. Mais là encore, je n'avais jamais eu l'occasion de l'utiliser sérieusement dans un "vrai" projet afin de gagner du temps.
Pour cette réalisation, je me suis donc servi de ChatGPT afin de générer un jeu de données propre. Ne voulant pas utiliser une source de données externe pour ce prototype, une structure statique en JSON était suffisante. Afin de valider que l'affichage reste propre et réactif avec de nombreux projets, j'ai donc utilisé ChatGPT pour générer la structure attendue.
Cette première requête était presque suffisante. Les projets avaient cependant des intitulés incrémentaux (Projet 1, Projet 2, etc.). Je lui ai donc demandé d'utiliser des noms de fruits pour nommer les projets et d'ajouter en plus une description qui explique les bienfaits du fruit 🍉
De même, le JSON retourné avait des double-quotes sur le nom des propriétés. Cela posait problème à Power Apps, je lui ai donc demandé de mettre les propriétés sans double-quote, même si cela ne respecte pas les standards.
Et voilà le résultat obtenu (extrait) :
[{
Title: "Cerise",
StartDate: "2023-01-25",
EndDate: "2023-02-01",
Tag: "Red",
Progress: 10,
Owner: "Lucas Dubois",
Description: "Les cerises sont remplies d'antioxydants et aident à réduire l'inflammation."
},
{
Title: "Ananas",
StartDate: "2023-02-10",
EndDate: "2023-02-17",
Tag: "Yellow",
Progress: 55,
Owner: "Léo Lefebvre",
Description: "L'ananas est riche en bromélaïne, bénéfique pour la digestion."
},
{
Title: "Kiwi",
StartDate: "2023-02-18",
EndDate: "2023-02-25",
Tag: "Red",
Progress: 20,
Owner: "Julie Rousseau",
Description: "Les kiwis sont chargés de vitamine K, essentielle pour la coagulation sanguine."
}]
Dans Power Apps Studio, afin de mettre en place un début de mode responsive, il convient de désactiver l'option « Scale to fit » dans les options de l'application. Cela permettra de profiter de toute la place disponible en fonction de la taille de l'écran. Dès lors, sur chaque composant à déposer à l'écran, plutôt que de mettre ou de laisser des tailles en dur (comme 640 x 480) on utilisera systématiquement Parent.Width et Parent.Height afin d'affecter dynamiquement la taille du composant en fonction de la fenêtre.
Pour faire un affichage de type « diagramme de Gantt », l'astuce telle que décrite dans l'article original de Ron, est d'utiliser deux galeries superposées à mettre sur un même écran :
La galerie verticale doit être alimentée par la collection de projets déclarée dans le OnStart (variable Projects). Il suffit de modifier la carte de la galerie en ajoutant le titre, la date de début et de fin.
J'ai profité de la construction de la collection (Projects) pour ajouter une colonne calculée afin d'obtenir le nombre de jours entre la date de début et la date de fin :
ClearCollect(Projects, AddColumns(ProjectsRaw, "Duration", DateDiff(ThisRecord.StartDate, ThisRecord.EndDate) + 1));
J'ai opté pour l'affichage de ce nombre de jour sous la forme d'un badge (composant moderne) dans un rond qui reprend la couleur du projet.
Ici la couleur est associée à un projet. On aurait pu associer la couleur au statut du projet (non démarré, en retard, terminé, etc.). C'est un choix tout à fait possible. L'objectif de la couleur est de différencier les projets en entre eux. En pratique, cela peut signifier un type de projet, un département, une direction, un métier, etc.
Pour la progression, c'est la couleur de fond de la cellule qui donne une indication sous la forme d'un thermomètre horizontal 🌡️ avec un effet de transparence :
Width = (ProjectWidth - rectangleSelection.Width) * ThisItem.Progress / 100
Fill = ColorFade(ColorValue(ThisItem.Tag), 80%)
La galerie horizontale permet d'afficher des jours sous forme de colonne. Chaque colonne correspond donc à une journée avec une largeur volontairement réduite.
Si on considère que le planning démarre au 1er janvier 2023, la première colonne sera donc le 1er janvier 2023. La deuxième colonne sera le jour suivant, etc. Ainsi, il suffit d'incrémenter de 1 la valeur de chaque cellule et d'afficher la date correspondante.
La date de départ doit être déclarée dans le OnStart de l'application, de même que le nombre de jours total à afficher dans le planning. Par exemple :
Set(CurrentStartDate, "2023-01-01");
Set(MaxDays, 365);
La propriétés Items de la galerie horizontale doit être une séquence numérique commençant à 0 jusqu'au nombre maximal de jour (MaxDays).
Pour un premier test, autant rester sur une seule année (365 jours). La formule permettant de générer le tableau est donc :
Items = Sequence(MaxDays, 0)
Chaque cellule possède ainsi une valeur numérique (ThisItem.Value).
Pour convertir cette valeur en date, il suffit d'appliquer les formules suivantes afin d'incrémenter la date de départ :
Text(DateAdd(CurrentStartDate, ThisItem.Value), "ddd")
Text(DateAdd(CurrentStartDate, ThisItem.Value), "dd")
Petit à petit, on peut donc construire un affichage propre avec l'intitulé du jour et son numéro. On peut ajouter également un style grisé pour signaler les week-ends (samedi et dimanche).
Fill = If(Text(DateAdd(CurrentStartDate, ThisItem.Value), "ddd") = "Sat" || Text(DateAdd(CurrentStartDate, ThisItem.Value), "ddd") = "Sun", Color.LightGray, Color.White))
Enfin, à chaque 1er du mois, on affiche le libellé complet du mois et l'année (mmm yyyy).
Text = If(Text(DateAdd(CurrentStartDate, ThisItem.Value), "dd") = "01", "📅 " & Text(DateAdd(CurrentStartDate, ThisItem.Value), "mmmm yyyy"), "")
Au final, le résultat est le suivant :
La grille du calendrier est désormais générée. L'écran est constitué de la sorte :
Il faut alors positionner les différents projets aux bonnes dates. Sachant que la première cellule correspond au 01/01/2023, si un projet démarre au 05/01/2023, il suffit de laisser 4 cellules vides et de commencer à la 5e cellule. Si le projet dure 3 jours, on remplit alors un rectangle de couleur d'une largeur équivalente à 3 jours.
Il est donc primordial de connaitre la largeur d'une journée. Pour mon prototype, j'ai choisi une largeur fixe de 90 pixels. Cette valeur est stockée dans une variable déclarée dans le OnStart : ColSize.
Set(ColSize, 90);
Il faut aussi connaitre la largeur de la « première colonne » qui correspond à l'affichage des projets dans la galerie verticale (la partie gauche). Dans mon cas, cette valeur est fixée à 240 pixels. D'un point de vue responsive, il aurait sûrement fallu faire un ratio de l'affichage (par exemple 1/5 de la largeur disponible avec une largeur minimale) :
Set(ProjectWidth, 240);
La valeur du X (position sur l'abscisse) pour la date de début d'un projet peut donc se calculer de cette manière :
X = ProjectWidth + DateDiff(CurrentStartDate, ThisItem.StartDate) * ColSize
La largeur du rectangle (Width) correspond donc au nombre de jours de la tâche, avec un minimum de 1 jour pour les dates qui tiennent en une seule journée, multiplié par la largeur d'une journée :
Width = (DateDiff(CurrentStartDate, ThisItem.EndDate) - DateDiff(CurrentDateYear, ThisItem.StartDate) + 1) * ColSize
A ce stade, la tâche est bien positionnée avec un rectangle qui correspond à la date de début et de fin du projet. Cependant, l'affichage se limite à la largeur de l'écran et il est impossible d'avoir tout le planning sur un seul écran.
Il va donc falloir faire un déplacement horizontal pour avancer ou reculer dans le temps. Ainsi la première cellule du planning ne sera pas forcément le 01/01/2023 mais le 15/01/2023 ou le 01/08/2023, selon le déplacement de l'utilisateur.
Il faut donc ajouter une deuxième translation sur le rectangle afin de calculer le décalage entre la première cellule (01/01/2023) du planning et la première cellule réellement affichée à l'écran.
Avant de présenter la solution technique, il faut permettre à l'utilisateur de changer la date d'affichage. J'ai mis en place quatre mécaniques complémentaire :
Afin que tous ces composants soient synchronisés entre eux, chacun calcule le delta entre la première date du planning (01/01/2023) et la nouvelle date. Ce décalage est stocké dans une variable nommée Offset (décalage).
Par défaut à 0, l'offset change donc à la manipulation de ces boutons. Par exemple, si j'avance d'une semaine, l'Offset vaut -7 car pour avancer dans le temps, on pousse vers la gauche le planning.
Pour le slider, on affecte cette formule :
OnChange = Set(Offset, Self.Value * -1);
Pour le date picker, on affecte cette formule :
OnChange = Set(Offset, DateDiff(datePicker.SelectedDate, CurrentStartDate));
Pour le bouton "semaine suivante", on affecte cette formule (OffsetStep est une variable déclarée dans le OnStart dont la valeur vaut 7) :
OnSelect = Set(Offset, Offset - OffsetStep);
La nouvelle formule de la position X du rectangle qui correspond à chaque projet doit donc prendre en compte cette nouvelle variable Offset :
X = ProjectWidth + DateDiff(CurrentStartDate, ThisItem.StartDate) * ColSize + Offset * ColSize
Egalement, au clic sur un projet, le planning se positionne automatiquement à la date de démarrage. Il faut donc modifier la valeur du Offset avec cette formule :
OnSelect = Set(Offset, DateDiff(ThisItem.StartDate, CurrentStartDate))
La sélection d'un projet permet de se positionner à la date de démarrage. Mais cela affiche également sur la droite un panneau d'information avec le détail du projet, dont la description et l'avancement sous forme d'un thermomètre horizontal.
Ce panneau est masqué si la largeur de l'écran est insuffisante. Pour cela, il faut ajouter dans le OnVisible de l'écran, une variable qui va tester la largeur de l'écran par rapport à des constantes prédéfinies (smartphone, tablette, ordinateur) :
Set(LargeScreen, Self.Size >= ScreenSize.Large);
Ensuite, sur la panneau de droite, il faut conditionner la valeur de Visible :
Visible = LargeScreen
Même sans le panneau de droite, l'affichage sur mobile (mode responsive) n'est pas optimal. C'est une limite connue et acceptable pour un prototype. En revanche, sur un écran très large, on bénéficie d'un planning très étendu !
De manière très classique, j'ai ajouté dans l'entête une zone de texte afin de filtrer les données affichées. La propriété Items de la galerie des projets se charge de faire un filtre sur l'ensemble des propriétés du projet (titre, progression, description, etc.).
La valeur du filtre est mis dans une variable déclarée dans le OnStart (Filter). Cette valeur est ensuite utilisée pour filter les données sur la galerie des projets :
Items = If(Filter <> "", Filter(Projects, Filter in Title || Filter in Tag || Filter in Progress || Filter in Owner), Projects)
Pour les tris, des boutons ont été ajoutés au dessus des projets. Deux types de boutons sont présents :
Il y avait probablement mieux à faire d'un point de vue ergonomie et expérience utilisateur ! J'attends vos retours dans les commentaires 📑
Items = If(SortByDays, Sort(Projects, Duration, If(SortDirAsc, SortOrder.Ascending, SortOrder.Descending)), If(SortByDates, ...
A ce stage, l'écran n'affiche que des projets.
En réalité, dans un diagramme de Gantt on trouve des projets et un ensemble de tâches affectées à ce projet. C'est là qu'intervient la notion de prérequis : certaines tâches ne peuvent démarrées que si la précédente est terminée. La gestion d'un projet n'est pas le thème de cet article, mais la visualisation de ce comportement est cependant primordial et doit faire parti du prototype.
J'ai donc fait évoluer la structure JSON du jeu de données pour introduire la notion de projet parent. Bien sûr, il aurait fallu utiliser des identifiants internes ou des codes uniques pour chaque projet. Dans cet exemple, c'est le nom du projet qui sert de clé.
Exemple (extrait) :
Set(ProjectsRaw, [
{
Title: "Power Apps Gantt",
StartDate: "2023-01-02",
EndDate: "2023-03-10",
Tag: "Green",
Progress: 0,
Owner: "Julien Leture",
Description: "Try to create a Gantt application with Power Apps Canvas.",
ParentProject: ""
},
{
Title: "Pre sales",
StartDate: "2023-01-02",
EndDate: "2023-01-12",
Tag: "Green",
Progress: 100,
Owner: "Julien Leture",
Description: "",
ParentProject: "Power Apps Gantt"
},
{
Title: "Prototype",
StartDate: "2023-01-18",
EndDate: "2023-01-20",
Tag: "Green",
Progress: 80,
Owner: "Julien Leture",
Description: "",
ParentProject: "Power Apps Gantt"
},
{
Title: "Specifications",
StartDate: "2023-01-18",
EndDate: "2023-02-03",
Tag: "Green",
Progress: 10,
Owner: "Pierre Frambois",
Description: "",
ParentProject: "Power Apps Gantt"
}]
);
Par défaut, la galerie n'affiche que les projets, sans leurs tâches. Les projets ayant des enfants se voient attribuer un nouveau bouton à bascule (toggle 🔘) afin de pouvoir déplier, et donc afficher, les tâches associées.
Ce bouton est conditionné par la présence d'enfants, donc de projets qui ont pour parent le projet courant. Un LookUp permet de le vérifier et de conditionner la propriété Visible :
Visible = ThisItem.ParentProject = "" && Not(IsBlank(LookUp(Projects, ParentProject = ThisItem.Title)))
Pour les sous-tâches d'un projet, j'ai ajouté une petite barre horizontale sur la gauche pour montrer la filiation avec le projet parent.
Ce prototype est aussi l'occasion d'utiliser les nouveaux composants modernes de Power Apps Canvas introduits début 2023 en preview. Pour information, on peut combiner les anciens et les nouveaux composants, y compris dans la carte d'une galerie.
Egalement, les composants modernes n'étant sont pas encore en GA (General Availability) aujourd'hui, il faut donc les activer dans les options de l'application : Modern controls and themes.
Un des ajouts les plus appréciables est l'arrivée des thèmes de couleur. En changeant de thème, toutes les couleurs (et leur dégradé) s'adaptent. Plus besoin de repasser sur chaque composant pour modifier la couleur de fond, de survol, de la bordure !
Pour le moment, seuls les thèmes natifs sont utilisables, mais il est évident que l'on pourra bientôt créer ses propres thèmes pour respecter la charte du client et la partager au sein de plusieurs applications.
Pour utiliser la couleur du thème dans son projet, il suffit d'utiliser les propriétés accessible par App.Theme.Colors. Outre Primary qui est la couleur de base, on trouve les nuances sombres (de Dark10 à Dark80) et claires (de Lighter10 à Lighter80).
App.Theme.Colors.Primary App.Theme.Colors.Dark10 App.Theme.Colors.Lighter10
Un des nouveaux composants proposés est l'entête (header) qui permet d'afficher un logo et un titre sur la gauche et le profil de l'utilisateur connecté sur la droite. Tout cela en déposant le composant sur un écran sans rien configurer de plus ! La couleur étant bien sûr reprise du thème.
Outre l'objectif de faisabilité qui était validé avant même de commencer grâce à l'article publié en 2021 sur un forum par un certain Ron, ce prototype devait aussi servir à estimer le temps de réalisation.
Ma consommation sur le projet est la suivante :
Plus spécifiquement, pour des diagrammes de Gantt, la collision entre les projets et les règles de gestion sur les dépendances ne sont pas traitées : ce prototype ne sert qu'à mettre en place l'affichage et le filtrage des données.
En plus de la vue Gantt, j'ai créé trois autres écrans :
Réaliser un diagramme de Gantt avec Power Apps, c'est possible ! Avec deux galeries (l'une pour les projets et l'autre pour le planning), le tour est joué. Pour faciliter la construction de cette application, toutes les variables doivent être déclarées dans le OnStart de l'application. Notamment :
Il ne faut pas tomber dans le piège de mettre en dur certaines valeurs comme la largeur de chaque jour ou le décalage initial sur la gauche. Avec ces variables il devient très facile de modifier profondément l'affichage.
Vous voulez faire un zoom sur le planning ? Il suffit d'ajouter un bouton qui modifie la valeur de ColWidth et tout se recalcule automatiquement. Vous voulez changer la date de début ? Il suffit de modifier CurrentStartDate (d'où le Current dans le nom de la variable, l'idée étant de pouvoir paginer par année le diagramme).
A vous de low-coder ! 👨🏫
#powerapps #gantt #powerplatform #lowcode