Exemple de création et d'intégration d'un graphe D3JS : social graph

Last modified by Aurelie Bertrand on 2023/04/12 17:26

Section valable pour les versions antérieures à 2022R2

Cet exemple est proposé afin d'expliciter à l'aide d'un cas concret la configuration d'un graphe de type D3.JS. 

Dans cet exemple, nous revenons sur la création d'un social graph à l'aide de D3.JS

Javascript

Définition des variables utilisées dans le code

// initialisation de variables nécessaires à la construction du graphe
var scale = {x: 0.3, y: 0.3};
var margin = {top: 50, right: 0, bottom: 0, left: -100};
var svg = null;
var links = [], arcLines = [], tabl=[];
var swoosh = null;
var path = null;
var sky = null;
var proj = null;
var width = 960, height = 500;
var world = null;
var place = null;
var data = null;
var nodes = null;
var index = null;

// Initialisation des mesures en rapport avec la configuration du graphe dans le studio.
const IDX_MEASURE_KPI_VALUE_FROM = 0;
const IDX_MEASURE_KPI_VALUE_TO = 1;
const IDX_MEASURE_AVG_INFLUENCE_WGT_FROM = 2;
const IDX_MEASURE_POSITIVE_INFLUENCE_FROM = 3;
const IDX_MEASURE_NEGATIVE_INFLUENCE_FROM = 4;
const IDX_MEASURE_AVG_INFLUENCE_WGT_TO = 5;
const IDX_MEASURE_POSITIVE_INFLUENCE_TO = 6;
const IDX_MEASURE_NEGATIVE_INFLUENCE_TO = 7;
const IDX_MEASURE_DELTA = 8;

const IDX_DIMENSION_BAR = 1;
const IDX_DIMENSION_GRPT = 2;

Récupération des membres de dimension et des valeurs des mesures associées

// Fonction permettant de récupérer l'attribut d'un membre (par défaut son identifiant)
function getMember(axisArray, position, getAttribute = item => item.id) {
   var item = axisArray.find(item => item.pos == position);
   if (item != null) {
       return getAttribute.call(this, item);
    }
   return null;
}

// Construction du jeu de données à partir des objets Digdash
function socialGraphPluginDD(ddjson, ddobj){
   var links = [],
    arcLines = [],
    tabl = [];
// on vérifie dans un premier temps que les valeurs ne soient pas vides.
   if (ddobj.values && ddobj.values.values && Object.keys(ddobj.values.values).length !== 0) {
       // the level of the hierarchy that is displayed on the current social graph
       const LEVEL_DISPLAYED = ddobj.dmsel.dimsToExplore[0].lPos;
       let tabForNodes = [];
       let tabForLinks = [];
       //let tabForLinks = [];
       let jsonFinalNodes = [];
       let tabToEmployee = [];
       var transform = d3.zoomIdentity;
       let positiveInfluencesFrom = [];
       let negativeInfluencesFrom = [];
       let positiveInfluencesTo = [];
       let negativeInfluencesTo = [];
       let deltasTab = [];
       let kpiNameFromContext = null;
       let kpiNameFromFrame = null;
        kpiNameFromContext = window.top.getDimensionFilter('KPI_Name') ? window.top.getDimensionFilter('KPI_Name')[0] : null;

       if (window.top.isCurrentPage('KPI_PulseCorp_What-If')) {
           var tabFrame = ddobj.frameId.split('_');
            kpiNameFromFrame = tabFrame[tabFrame.length - 3];
        }

       // DEBUT : parcours des mesures
       for (let v in ddobj.values.values) {
           // split sur chaque membre
           var tab = v.split("-");
           // récupération index mesure
           var idx_measure = Number(tab[0]);
           // récupération index membre dimension bar
           var idx_dimBar = tab[IDX_DIMENSION_BAR];
           // récupération index membre dimension groupement
           var idx_dimGpt = tab[IDX_DIMENSION_GRPT];

           // vérification nature de la mesure
           switch (idx_measure) {
               // en fonction de la mesure, affectation d'un objet spécifique
           case IDX_MEASURE_KPI_VALUE_FROM:
               if (!tabForNodes[getMember(ddobj.values.axis[IDX_DIMENSION_GRPT], idx_dimGpt)]) {
                    tabForNodes[getMember(ddobj.values.axis[IDX_DIMENSION_GRPT], idx_dimGpt)] = [];
                }
                tabForNodes[getMember(ddobj.values.axis[2], idx_dimGpt])].push(ddobj.values.values[v]);
               break;
           case IDX_MEASURE_KPI_VALUE_TO:
               if (!tabToEmployee[getMember(ddobj.values.axis[IDX_DIMENSION_BAR], idx_dimBar)]) {
                    tabToEmployee[getMember(ddobj.values.axis[IDX_DIMENSION_BAR], idx_dimBar)] = [];
                }
                tabToEmployee[getMember(ddobj.values.axis[IDX_DIMENSION_BAR], idx_dimBar)].push(ddobj.values.values[v]);
               break;
           case IDX_MEASURE_AVG_INFLUENCE_WGT_FROM:
                tabForLinks.push({
                   "from": getMember(ddobj.values.axis[IDX_DIMENSION_GRPT], idx_dimGpt),
                   "to": getMember(ddobj.values.axis[IDX_DIMENSION_BAR], idx_dimBar),
                   "influence": ddobj.values.values[v],
                   "tag": 'F'
                });
               break;
           case IDX_MEASURE_AVG_INFLUENCE_WGT_TO:
                tabForLinks.push({
                   "from": getMember(ddobj.values.axis[IDX_DIMENSION_BAR], idx_dimBar),
                   "to": getMember(ddobj.values.axis[IDX_DIMENSION_GRPT], idx_dimGpt),
                   "influence": ddobj.values.values[v],
                   "tag": 'T'
                });
               break;
               // continuer le switch pour couvrir toutes les mesures
           default:
                console.log("échec");
               break;
            }
        }
 }
}

Construction du graphe

Une fois les données récupérées, il est possible de construire le graphe avec D3.JS. 

Pour ce faire, il faut appeler les fonctions de la librairie, toujours dans cette même fonction définissant le plugin.

// On applique une logique si besoin pour obtenir le calcul souhaité
for (var key in tabForNodes) {
   let values = tabForNodes[key];
   let sum = values.reduce((previous, current) => current += previous);
   let avg = Math.round((sum / values.length) * 100) / 100;
   let name = retrieveCaptionForMemberId(key);
    jsonFinalNodes.push({
       "id": key,
       "name": name,
       "KPI_value": avg,
       "flag": 'F'
    });
}

for (var key in tabToEmployee) {
   let values = tabToEmployee[key];
   let sum = values.reduce((previous, current) => current += previous);
   let avg = Math.round((sum / values.length) * 100) / 100;
   if (!tabForNodes[key]) {
       let name = retrieveCaptionForMemberId(key);
        jsonFinalNodes.push({
           "id": key,
           "name": name,
           "KPI_value": avg,
           "flag": 'T'
        });
    }
}

// Le code ci-dessous est le code original du graphique construit sous D3.JS
// Pour le passer sous DigDash, il faut commenter cet appel de sélection D3.JS du cadre SVG, comme montré ci-dessous.
/*const svg = d3.select("body").append("svg")
.attr("viewBox", [0, 0, width, height]);*/

// si besoin, on peut sélectionner ici l'élèment racine du svg.
// cela peut être utile si on a besoin d'afficher des choses en dehors du graphe en lui-même
svg_base = d3.select('svg');
// sélection de l'élèment #D3DD généré par DD dans le SVG.
// c'est principalement sur cet élément que l'on appliquera toutes les fonctions D3 pour construire le graphe.
svg = d3.select("svg #D3DD")
    .attr("transform", "scale(" + 1 * scale.x + "," + 1 * scale.y + ")" + "translate(" + margin.left + "," + margin.top + ")");

// on crée les objets "finaux" en les mappant depuis les objets précédemment crées.
// ce seront ces objets qui seront utilisés par la librairie D3
var nodes = jsonFinalNodes.map(d => {
   return Object.assign(Object.create(d), {
        id: d.id
    })
});
var nodeById = d3.map(nodes, function (d) {
   return d.id;
});
var links = tabForLinks.map(d => {
   return Object.assign(Object.create(d), {
        source: d.from,
        target: d.to,
        influence: d.influence
    });
});

// Ici on initie un graphe de poids pondéré qui peut aussi être utilisé comme un social graph
const simulation = d3.forceSimulation()
    .force("link", d3.forceLink().id(d => d.id).distance(400).strength(1.5))
    .force("charge", d3.forceManyBody().strength(-500))
    .force("center", d3.forceCenter(width / 2, height / 2));

// On construit les liens du graphe qui relieront les noeuds
const link = svg.append("g")
    .attr("stroke-opacity", 0.6)
    .selectAll(".link")
    .data(linksData).enter().append("path")
   // on peut colorer les liens en fonction de la valeur prise par un attribut
   .attr("class", "link").attr("class", function (d) {
   if (d.influence < -2) {
       return "link link-low";
    } else if (d.influence < 3) {
       return "link link-middle";
    } else {
       return "link link-high";
    }
});

// on peut également affecter un tooltip au lien, qui s'affichera au passage de la souris.
link.append("title")
.text(d => {
   if (retrieveInfluence('positive', d.source, d.target, d.tag)) {
       return "Positive influence : " + Math.round(retrieveInfluence('positive', d.source, d.target, d.tag)) + "\nNegative influence : " + Math.round(retrieveInfluence('negative', d.source, d.target, d.tag));
    } else {
       return "";
    }
});

// On construit les noeuds
const node = svg.selectAll("foo")
    .data(nodes).enter()
    .append("g");

// Puis on les "décore"
node.append("circle")
.attr('r', '100')
.attr('fill', colorForNode)
.attr("stroke", "#fff")
.attr("stroke-width", 1.5);

// On peut également appliquer un texte au noeud
node.append("text")
.attr("text-anchor", "middle")
.attr("dy", ".35em")
.attr("class", function (d) {
   if (LEVEL_DISPLAYED == LEVEL_COMPANY || LEVEL_DISPLAYED == LEVEL_EMPLOYEE || rowSetAgg.indexOf(d.id) > -1) {
       return "text-in-node clickable";
    } else {
       return "text-in-node not-clickable";
    }
})
.attr("font-size", "15pt")
.attr("fill", "#ffffff")
.text(function (d) {
   return d.name;
});

Configuration dans le studio

Dans le Studio, pour appeler cette configuration codée précédemment en Javascript, il convient de suivre quelques règles. 

Les captures d'écran présentées ci-après sont des captures du Studio avec une visualisation sans assistant visuel. Ce mode de visualisation du studio est plus pratique dans le cas où un nombre important de mesures sont utilisées dans la configuration du graphe.

Cette visualisation s'obtient en cliquant sur le bouton en haut à droite : 

switch_view.png

  1. Créer un graphique de type D3.JS, en choisissant le template précédemment créé et configuré
  2. Respecter l'ordre des mesures (dans le cas où l'on décide d'afficher plusieurs mesures)
    • Cet ordre est indiqué par les constantes "IDX_MEASURE_NOM_MEASURE" : il varie de 0 à N-1, N étant le nombre de mesures configurées dans le studio.
    • Cet ordre est à reproduire dans la configuration des mesures dans le studio (le plus haut dans la liste des mesures est l'index 0, le plus bas l'index N-1)
      order_measure.png
       
  3. Respecter le placement des dimensions.
    • Cet ordre est cette fois indiqué par les constantes "IDX_DIMENSION_TYPE_OF_DIMENSION".
    • order_dimension.png