Ajouter un graphique - D3

Modifié par Aurelie Bertrand le 2024/07/04 16:21

Interface d'édition de flux D3

L'interface graphique d'édition de flux D3 a été pensée afin d'une part de couvrir un champ de conception de modèles D3 le plus large possible, et d'autre part de permettre aux développeurs de travailler le code de leurs modèles directement en face de la prévisualisation.

Dans un flux D3, il n'y a pas de notions d'axes : les dimensions et mesures issues du modèle de données sont déposées selon des colonnes, comme c'est le cas dans un flux de type tableau.

d3chart1.png

Les modèles D3 disponibles sont sélectionnables dans la partie Visualisation / Style, en haut à droite.

d3chart2.PNG

L'onglet D3 donne accès aux copies temporaires de tous les fichiers de ressources associés au modèle D3 courant. Ces fichiers sont directement modifiables via l'éditeur et enregistrables temporairement. Il n'est pas possible d'ajouter, de supprimer ou de renommer des fichiers - de même, il n'est pas possible de configurer le nom du modèle ou de sa fonction de plugin.

d3chart4.PNG

Un fichier inexistant (supprimé du serveur D3templates ou renommé) sera marqué d'une icône rouge.

d3tab.PNG

La prévisualisation se base sur ces fichiers temporaires. Le graphe est construit sur une feuille html dont le body est vierge.

Les paramétrages de chacun des axes (tris, hiérarchies, filtres, formats, agrégations) sont bien pris en compte dans l'alimentation en données de la visualisation.

d3chart3.PNG

Au moment de valider l'édition du flux, si au moins un fichier de ressources a été temporairement modifié, vous avez la possibilité :

  • de sauvegarder ces modifications sur le serveur D3templates
  • de ne pas les sauvegarder
  • de générer un nouveau modèle D3 en enregistrant les modifications sous de nouvelles ressources

d3save1.PNG

En sélectionnant "Générer un nouveau modèle", vous serez sur le point de dupliquer le modèle D3 avec la possibilité de dupliquer les fichiers correspondants, en particulier les fichiers qui ont été temporairement modifiés lors de l'édition du flux.

d3save2.PNG

Rappel :

Un fichier propre à un modèle devrait respecter la convention suivante :

{nomDuModèle}_{rôleDuFichier}.{extension}

Exemples : textReport_plugin.jscirclePacking_style.css

Un fichier commun à plusieurs modèles devrait respecter la convention suivante :

{nomDuGroupeCommun}_{rôleDuFichier}.{extension}

Exemple : common_tooltip.js

Ainsi les fichiers d'un même modèle sont alphabétiquement regroupés dans le répertoire D3templates.

La fonction de plugin

La fonction de plugin est le mécanisme fondamental d'un modèle D3 DigDash permettant de récupérer les données d'un modèle de rendu pour alimenter la visualisation d3.js proprement dite.

Codée en javascript, elle doit être disponible dans un des fichiers de ressources du modèle D3. Elle prend en entrée trois paramètres :

  • ddjson : l'objet contenant les informations du modèle de rendu (datamodel-sélection, n-uplets dimensionnels, mesures, palette de couleur)
  • ddobj (déprécié) : un objet généré par la logique DigDash contenant la totalité des informations du flux en cours d'édition
  • exportCallback : la fonction de callback a exécuter pour l'intégration du flux dans une fabrique de document / pour l'export
// Plugin function for a DigDash D3 template which plugin-function field is called "myTemplatePlugin"
function myTemplatePlugin(ddjson, ddobj, exportCallback) {
    // Browse ddjson to prepare a dataset
    console.log(ddjson);

    // Feed your dataviz and build it
}

Depuis la version DigDash 2022R2, ddobj devrait être délaissé au profit de ddjson.

L'objet ddjson

Afficher ddjson dans la console du navigateur à la prévisualisation d'un flux D3 est un excellent moyen de comprendre le contenu et les méthodes fournies par cet objet.

d3chart5.PNG

AttributDescription

children

Deprecated Tableau d'objets représentant les n-uplets de membres (utilisé avant 2022R2)

colorsTableau de la palette de couleurs du flux en cours d'édition. Chaque couleur est un objet à quatre attributs.
dimensionsTableau des dimensions déposées en entrée du flux, en tant qu'objets DimExplorer.
dimensionsByColumnsTableau des listes de dimensions (objets DimExplorer) par index de colonnes.
interactionsContient les fonctions d'interaction doDrillDown et doFilter
keychildren

Deprecated (utilisé avant 2022R2)

lines

Tableau des n-uplets de membres dimensionnels générés par les dimensions déposées en entrée du flux, respectant l'ordre spécifié par chacun des axes.

Chaque n-uplet est une chaîne de caractères de la forme "m0-m1-...-mp" où chaque mk correspond à l'index de membre de la k-ième dimension.

measuresTableau des objets mesures déposés en entrée du flux (index, identifiant, nom).
selInfoObjet SelectionInfo contenant toutes les informations du flux en cours d'édition.
specificFeaturesListe des fonctionnalités avancées du flux, sous la forme de paires clés-valeurs.
valuesByMembersMap / dictionnaire des points de mesures par n-uplets de membres dimensionnels
variablesTableau des variables du modèle de données en tant qu'objets DDVar
MéthodeDescription
getMemberByIndex(memberIndex, colIndex)

Renvoie un objet membre connaissant l'index de l'axe dimensionnel (l'index de colonne en entrée du flux) et son index en tant que membre au sein de cette dimension.

Paramètres :

  • memberIndex : l'index d'un membre dans sa dimension correspondante
  • colIndex : l'index de la colonne (son numéro de colonne dimensionnelle en entrée du flux, moins un)

Ex : getMemberByIndex(3, 1) renverra le troisième membre de la dimension déposée en colonne 2.

getMemberByKey(memberKey, colIndex)

A partir d'un n-uplet de membres, renvoie un objet membre connaissant l'index de la colonne en entrée du flux.

Paramètres :

  • memberKey : n-uplet de membres en tant que chaîne de caractères, sous la forme "q0-q1-...-qn"
  • colIndex : l'index de la colonne (son numéro de colonne dimensionnelle en entrée du flux, moins un)

Ex : getMemberByKey("1-3-4", 1) renverra le troisième membre de la dimension déposée en colonne 2. C'est équivalent à getMemberByIndex(3, 1).

getValue(memberKey, measureIndex, formated=false)

Renvoie la valeur éventuellement formatée de la measureIndex - ième mesure du n-uplet de membres défini par memberKey.

Paramètres :

  • memberKey : n-uplet de membres en tant que chaîne de caractères, sous la forme "q0-q1-...-qn"
  • measureIndex : l'index de la mesure (son numéro de colonne de mesure en entrée du flux, moins un)
  • formated (optional) : booléen, indique si la valeur doit être formatée selon le paramétrage de l'axe correspondant

Ex : getValue("1-3-4", 1) renverra la valeur brute de la deuxième mesure déposée pour le n-uplet de membres "1-3-4"

interactions.doChangeDimension(dimExplorer, memberId)

Change de dimension conformément au chemin de navigation associé à la dimension explorée, si ce chemin est activé dans la configuration du flux (onglet "Interactions").

Paramètres :

  • dimExplorer : l'objet DimExplorer correspondant à la dimension explorée
  • memberId : l'identifiant du membre sur lequel est effectué la navigation
interactions.doDrillDown(dimExplorer, memberId, memberFormated)

Fonction simplifiée de ddCtrl.drillDown(portlet, dimension, hiérarchie, niveau, membre,membreFormaté)

Explore d'un niveau descendant un chemin de navigation (hiérarchique ou changement de dimension), si ce chemin est activé dans la configuration du flux (onglet "Interactions").

Paramètres :

  • dimExplorer : l'objet DimExplorer correspondant à la dimension explorée
  • memberId : l'identifiant du membre sur lequel est effectué la navigation
  • memberFormated (optional) : la valeur formatée du membre selon le format affecté à la hiérarchie explorée. Par défaut, l'identifiant du membre.

interactions.doFilter(dimExplorer, memberId)

Fonction équivalente à ddCtrl.filter (dimension, membre) ou ddCtrl.filter (oFilterOperand)

Filtre sur une dimension en prenant en compte les hiérarchies.

Paramètres :

  • dimExplorer : l'objet DimExplorer correspondant à la dimension filtrée
  • memberId : l'identifiant du membre sur lequel est effectué le filtrage

En déposant dimensions et mesures sur les colonnes du graphique, cela va générer autant de lignes que de n-uplets de membres issu du produit cartésien des dimensions déposées. A chacune de ces lignes correspond autant de points de mesures que de mesures déposées.

Un modèle simple à reproduire

Voici un modèle D3 très simple constitué d'un fichier javascript et d'un CSS. Il s'agit grosso modo d'un rapport html.

La fonction de plugin consiste à parcourir les n-uplets dimensionnels, et afficher pour chacun d'entre eux toutes les données disponibles : nom de dimensions, nom de membres, noms des mesures et valeurs des mesures.

/*
* A basic plugin to show data in Digdash using D3
* It accepts an undefined number of measures and dimensions
*/

function textReportPlugin(ddjson, ddobj, exportCallback) {
    makingReport(ddjson);
   if (exportCallback)
        setTimeout(exportCallback, 1000); // Triggers export with nodeJS
}

function makingReport(ddjson) {
    console.log(" >> DigDash d3.js text report");
    console.log(ddjson);

   // Starting straigth on document's body
   var dataDiv = d3.select("body");
   
   // Browsing members set to generate some text lines
   // For each line of members in ddjson we build a paragraph describing the data
   dataDiv.selectAll('p')
    .data(ddjson.lines)
    .join(enter => {
        enter.insert('p')
        .html(tuple => {
           let ret = "";
           // each tuple is a string of member's indexes separated with '-'
           // like "7-10-2"
           for (let dimAxisIndex=0; dimAxisIndex < ddjson.dimensionsByColumns.length; dimAxisIndex++) {
               // Building axis header
               // There can be several dimensions per axis
               var dimensionsOnAxis = ddjson.dimensionsByColumns[dimAxisIndex];
               var dimensionsNames = dimensionsOnAxis.map(dimExplorer => {
                   var dimName = dimExplorer.dim.name;
                   // Getting a more precise dimension name if a hierarchy is set on it
                   var currentLevel = dimExplorer.lPos;
                   if (currentLevel >= 0) {
                        dimName += " (" + dimExplorer.hierarchy.levels[currentLevel].name + ")";
                    }
                   return dimName;
                });
                ret += `<span class="pDimension">${dimensionsNames.join('-')}</span>`;
                ret += ' - ';
               // Retrieving the member
               var member = ddjson.getMemberByKey(tuple, dimAxisIndex);
                ret += `<span class="pMember">${member.formated}</span><br/>`;
            }
           // Getting every measuring point for each tuple of members
           var values = ddjson.valuesByMembers[tuple];
           var rawValues = values.rawValues; // raw values (non-formated)
           var formatedValues = values.formatedValues; // formated values
           for (let k=0; k < formatedValues.length; k++) {
               var measureName = ddjson.measures[k].id; // or f for formated
               var measureValue = formatedValues[k];
                ret += `<span class="pMeasure">${measureName}</span>`;
                ret += ' : ';
                ret += `<span class="pValue">${measureValue}</span><br/>`;
            }
           return ret;
        })
        .classed("pData", true);
    });
}

Le CSS associé permettant de styliser le rendu :

.pData {
   font: bold 14px sans-serif;
   color: navy;
}
.pDimension {
   color: navy;
}
.pMember {
   color: mediumblue;
}
.pMeasure {
   color: saddlebrown;
}
.pValue {
   color: darkorange;
}

Remarquez que la bibliothèque d3.js pourrait ne pas être exploitée ici (moyennant plus de code javascript bien entendu).

Voici finalement ce que peut donner ce modèle d'une soixantaine de lignes :

d3chart6.PNG

Chaque paragraphe correspond effectivement à un n-uplet de membres pour lesquels il existe autant de valeurs de mesures que de mesures déposées sur les colonnes.

Stratégie d'implémentation d'un modèle et d'un flux

  • Avant toute chose, il faut avoir une idée du type de graphe à développer. Même si d3.js permet de générer des formes HTML et SVG "à la main", il est souvent préférable de choisir comme base de départ un template D3 existant. Par exemple, le site https://observablehq.com/@d3 propose de nombreux templates dont la conception est parfois même expliquée dans les détails.

Le développement peut se poursuivre ainsi :

  • Le template candidat doit être fonctionnel sur une simple page html avec un jeu de données simple et dont la structure est bien définie (cela peut être un simple tableau comme une structure hiérarchique arborescente, selon ce que le template cherche à représenter).
  • A partir de là, on peut créer un modèle D3 DigDash avec :
    • un fichier javascript pour la fonction de plugin
    • un fichier javascript pour la construction du graphe d3.js le plus indépendant possible de la fonction de plugin (cette construction prend la forme d'une fonction qui retourne un nœud document ou un nœud d3)
    • éventuellement un fichier de version d3 si elle n'est pas chargée dans la fonction de plugin
    • éventuellement des fichiers CSS
    • éventuellement d'autres fichiers JS utilitaires (par exemple pour la définition d'une info-bulle)
  • Ensuite il faut développer la fonction de plugin de telle sorte
    • qu'elle transforme les données issues de ddjson en jeu de données attendu par le graphe
    • qu'elle appelle la fonction de construction en lui passant les données préparées et qu'elle insère le graphe retourné dans le DOM

Une fois ceci fait, il est possible de créer un flux avec ce modèle.

Bien entendu, le passage des colonnes en entrée du flux (combien de dimensions attendues, de mesures attendues et de quels types) doit être connu de celui qui édite le flux, ou bien anticipé par le développeur du modèle, en documentant la fonction de plugin.

La fiche résolution Exemple de création et d'intégration d'un graphe D3JS : heatmap (histogramme 2D) décrit étapes par étapes la construction d'un modèle D3 à partir d'un template d3.js.

Charger une version d3.js ou autre ressource via CDN

Il est possible de charger une ressource distante nécessaire à la construction de votre visualisation (bibliothèque javascript, feuille CSS...).

Vous devez programmatiquement insérer une balise script dans le DOM et poursuivre l'exécution sur onload. Ainsi on peut charger une version de d3.js via CDN plutôt que d'utiliser un fichier du serveur D3templates :

function textReportPlugin(ddjson, ddobj, exportCallback) {
    // Loading d3.js V7 from CDN
    var d3cdn = document.createElement('script');
    d3cdn.type = 'text/javascript';
    d3cdn.src = 'https://cdn.jsdelivr.net/npm/d3@7';
    document.head.appendChild(d3cdn);
    d3cdn.onload = function() {
        // Creating text report as soon as d3 is loaded
        makingReport(ddjson);
    }
    if (exportCallback)
        setTimeout(exportCallback, 1000); // Triggers export with nodeJS
}

Exploiter une source de données distante (Fetch avec d3.js)

L'API Fetch de d3.js (https://github.com/d3/d3-fetch/tree/v3.0.1) permet de récolter des données distantes (notamment lorsqu'elles sont au format JSON ou CSV).

Cela permet de tester un modèle D3 en créant des flux sans utiliser de modèles de données DigDash (un modèle de données quelconque sera tout de même nécessaire pour instancier le flux).

Par exemple :

// No DigDash datamodel used here
d3.csv(
  'https://raw.githubusercontent.com/bumbeishvili/sample-data/main/org.csv'
).then(data => {
   // Build something from data
   d3.select("body")
       .selectAll("p")
       .data(data)
       .join("p")
       .text(d => JSON.stringify(d))
});

Paramétrage spécifique à un flux - fonctionnalités avancées

Un modèle D3 correspond à un modèle de visualisation.

En changeant le code des fichiers du modèle, tous les flux qui utilisent ce modèle seront impactés.

Or, on peut avoir besoin de paramétrer différemment plusieurs flux basés sur un même modèle.

Les fonctionnalités avancées du flux (specific features) répondent à ce besoin.

Exemple : modèle organigramme (Organization Chart)

Le modèle D3 organigramme (Organization Chart) est capable de générer des organigrammes paramétrés avec plusieurs options.

Les options disponibles en tant que fonctionnalités spécifiques sont :

  • compact : la compacité de l'organigramme : "compact" ou "flat" 
  • expandLevel : le niveau étendu initial
  • initialZoomLevel : le niveau de zoom initial
  • layout : l'orientation de l'organigramme : "top", "bottom", "left", "right"
  • templateIndex : l'index de template de noeud parmi quatre templates disponibles

Voici comment sont déclarées les fonctionnalités spécifiques dans le fichier de plugin :

// Default specific features (flow parameters)
const SPECIFIC_FEATURES = {
    compact: "true",
    expandLevel: "1",
    initialZoomLevel: "0.8",
    layout: "top",
    templateIndex: "0"
};
// Current specific features
var flowSpecificFeatures = SPECIFIC_FEATURES;
var compact;
var expandLevel;
var initialZoomLevel;
var layout;
var templateIndex;
/**
 * Templates :
 * 0 : skyTemplate
 * 1 : cleanTemplate
 * 2 : circlesTemplate
 * 3 : blueTemplate
 */

La constante SPECIFIC_FEATURES correspond aux valeurs des fonctionnalités spécifiques par défaut.

Les valeurs courantes de ces fonctionnalités spécifiques peuvent ensuite être déclarées, au besoin, dans l'interface d'édition du flux, onglet Paramètres de visualisation. Par exemple, ici on passe le paramètre "compact" à "false", le paramètre "layout" à "left" et le paramètre "templateIndex" à "2" :

d3chart7.png

Les fonctionnalités spécifiques ainsi ajoutées doivent être correctement traitées dans le code de la fonction de plugin afin de paramétrer le graphe D3 comme il se doit. Pour éviter toute ambigüité, il est conseillé de traiter les paramètres reçus comme des chaînes de caractères.

La fonction utilitaire loadSpecificFeatures(ddjson, defaultSpecificFeatures) doit être chargée grâce au fichier common_specificFeaturesUtil.js disponible dans les ressources du CMDR et utilisée comme suit :

// -------------------------------------------------------------
    // Loading specific features
    flowSpecificFeatures = loadSpecificFeatures(ddjson, SPECIFIC_FEATURES);
    compact = (String(flowSpecificFeatures.compact) === 'true');
    expandLevel = parseInt(flowSpecificFeatures.expandLevel);
    initialZoomLevel = Number(flowSpecificFeatures.initialZoomLevel);
    layout = String(flowSpecificFeatures.layout);
    templateIndex = parseInt(flowSpecificFeatures.templateIndex);
// -------------------------------------------------------------

Ainsi, nous voyons qu'un même modèle d3.js d'organigramme est capable de générer des flux ayant leurs propres spécificités.

Ce concept peut être généralisé à tous les modèles D3 si le besoin se présente, et moyennant une bonne conception du modèle en question.

Interactions sur un flux D3

L'éventail des possibilités offertes par d3.js permet aux développeurs de créer leurs propres mécanismes d'effets graphiques (transitions) et d'interactions (notamment le zoom : https://github.com/d3/d3-zoom), voire même d'ajouter des contrôles propres au flux (via des éléments de formulaires pour des réglages divers comme la variation d'un paramètre numérique ou le choix d'un thème CSS).

Il n'y a pas d'affectation d'une interaction par colonne depuis le Studio, comme c'est le cas sur les flux DigDash de type tableau.

En revanche, l'utilisateur peut toujours exploiter les fonctions DigDash du Dashboard API (ddCtrl) notamment pour le filtrage et la navigation de données, ou les versions simplifiées fournies avec ddjson.interactions.

Ces fonctions sont à intégrer aux mécanismes d'évènements propres à d3.js : https://github.com/d3/d3/blob/main/API.md#handling-events

Exemple :

element.on("click", (event, i) => ddCtrl.openFlowInWindow('9452509', 600, 400));

D'autres exemples sont disponibles dans la fiche résolution création d'un graphe D3JS.

Activer des chemins de navigation

Par défaut, les hiérarchies et chemins de navigations ne sont pas activés dans les flux de type D3.

Dans l'onglet "Interactions" de l'éditeur de flux, vous pouvez activer la navigation (synchronisée ou non) d'une ou plusieurs hiérarchies affectées aux dimensions passées en entrée du flux.

d3chart8.png

L'activation de la navigation entraîne l'apparition du fil d'Ariane (breadcrumb) dans le rendu final. Cela correspond à la génération d'objet de type exploration (DrillObject) pour les hiérarchies et changement de dimension (ChangeDimObject) pour les chemins de dimensions.

Pour exploiter la navigation en tant qu'interaction dans le code du modèle, vous pouvez appeler les fonctions DigDash du Dashboard API (ddCtrl.drillDown) ou sa version simplifiée disponible avec ddjson.interactions qui prend en paramètre un objet DimensionExplorer et un identifiant de membre:

onClick: (event, d) => {
         // assuming datum d handles enough values, dimExplorers and members informations
         console.log(d);
         
         let colIndex = 0;
         let dimIndex = 0;
         let dimExplorers = ddjson.dimensionsByColumns[colIndex];
         let dimExplorer = dimExplorers[dimIndex];
         let dimensionId = dimExplorer.dim.id;
         let memberId = d.members[colIndex].id;
         
         // performed interaction can be specified from Specific Features
         //ddjson.interactions.doFilter(dimExplorer, memberId);
         ddjson.interactions.doDrillDown(dimExplorer, memberId);  
      }

Export de flux D3 - intégration dans les fabriques de documents

Un flux D3 peut être intégré dans les fabriques PDF, PPT et HTML.

Pour cela, il vous faut d'abord activer NodeJS dans les options d'export.

Le processus consiste à générer une image du graphe.

Vous aurez donc par ailleurs besoin, si nécessaire, d'adapter la fonction de plugin du modèle D3 et le code de construction du graphe afin de désactiver certains effets comme les transitions, de définir des dimensions spécifiques et de déclencher la capture une fois le graphique prêt.

function horizontalBarPlugin(ddjson, ddobj, exportCallBack) {
   // Transition parameters
   var duration = 3000;
   var delay = 200;

   // for export we need no transition
   if (ddCtrl && ddCtrl.__nodejs) {
        duration = 0;
        delay = 0;
 
       // Other corrections
   }

   // Data preparation
   // ...

   // Chart building
   // ...
   
   // Once chart ready, trigger export callback
   setTimeout(exportCallback, 1000);
}

Afin d'être certain que la capture du flux dans le document final s'effectue correctement, vous devez appeler de manière asynchrone la fonction exportCallback qui est passée en paramètre de la fonction de plugin.

Une possibilité est de la déclencher après un délai fixe en millisecondes avec un setTimeout (délai qui peut être défini à titre indicatif, ou passé en tant que fonctionnalité spécifique dans le cas où certains flux mettraient plus de temps à se charger que d'autres). D'autres stratégies peuvent être envisagées, par exemple en utilisant le concept de Promise javascript.

Legacy 2022R2

Jusqu'à la version DigDash 2022R1, les flux de type D3 étaient construits sur un document SVG pré-rempli avec certains éléments (notamment une balise svg d'identifiant "D3DD"). La fiche résolution suivante explicitait la marche à suivre pour développer un modèle de graphe D3.

A partir de DigDash 2022R2, les flux sont construits sur un document HTML dont le <body> est vierge. Il est ainsi plus simple d'intégrer n'importe quel template d3.js faisant appel aux technologies html, svg et css.

Pour qu'un modèle développé avant 2022R2 soit fonctionnel, il vous faudra le retoucher afin de reconstituer le DOM qui servait de base dans les versions précédentes.

Par exemple ajouter la ligne suivante en début de fonction de plugin :

d3.select("body").append("svg").attr("id", "D3DD");

En ce qui concerne la fonction de plugin, il n'y a pas de conflit étant donné qu'elle conserve les mêmes paramètres.

Idem pour l'objet ddjson 2022R2 qui étend l'ancien ddjson en lui ajoutant des attributs et méthodes supplémentaires.