I dendrogrammi circolari

Introduzione

Sono passati già parecchi mesi da quando avevo scritto una serie di articoli che trattavano lo sviluppo di dendrogrammi attraverso l’uso della libreria JavaScript D3. Comunque, recentemente ho ricevuto una serie di richieste per approfondire l’argomento, che a quanto pare sembra aver goduto di un discreto successo.

libro

A tal riguardo, ho deciso di ricominciare a scrivere articoli su questa splendida libreria, trattando uno dopo l’altro moltissimi argomenti e rappresentazioni. Per ricominciare, ho deciso di concludere l’argomento dei dendrogrammi con un quinto articolo.

Se siete interessati ad approfondire l’argomento, all’interno del libro Beginning JavaScript Charts vengono presentate varie metodologie di acquisizione dati, dall’estrazione di dati da una tabella di un database (tramite PHP), alla lettura e parsing dei dati contenuti in un file esterno. Viene inoltre spiegato dettagliatamente il formato JSON, e grazie a numerosi esempi viene mostrato come sia pratico utilizzarlo in queste occasioni.

Per chiunque volesse ripartire dagli articoli precedenti…

D3 - dendrogrammi circolari

Il codice di partenza

Partiamo dal codice dendrogram01.html presente nell’articolo Come realizzare un dendrogramma con la libreria D3 (parte 2) che produceva il seguente dendrogramma.

dendrogram_es01
Fig.1: dendrogramma

Per comodità, ripropongo qui il codice JavaScript che lo genera:

var width = 600;  
var height = 500;  
var cluster = d3.layout.cluster()       
    .size([height, width-200]);  
var diagonal = d3.svg.diagonal()       
    .projection (function(d) { return [d.y, d.x];});  
var svg = d3.select("body").append("svg")       
    .attr("width",width)       
    .attr("height",height)       
    .append("g")       
    .attr("transform","translate(100,0)");  

d3.json("dendrogram01.json", function(error, root){       
    var nodes = cluster.nodes(root);       
    var links = cluster.links(nodes);       
    var link = svg.selectAll(".link")         
        .data(links)         
        .enter().append("path")         
        .attr("class","link")         
        .attr("d", diagonal);       
    var node = svg.selectAll(".node")         
        .data(nodes)         
        .enter().append("g")         
        .attr("class","node")         
        .attr("transform", function(d) { return "translate(" + d.y + "," + d.x + ")"; });       
    node.append("circle")         
        .attr("r", 4.5);       
    node.append("text")        
        .attr("dx", function(d) { return d.children ? -8 : 8; }) 
        .attr("dy", 3)        
        .style("text-anchor", function(d) { return d.children ? "end" : "start"; })        
        .text( function(d){ return d.name;});  
});

Queste sono le definizioni CSS:

.node circle {        
     fill: #fff;       
     stroke: steelblue;       
     stroke-width: 1.5px;  
}  
.node {       
     font: 20px sans-serif;  
}  
.link {       
     fill: none;       
     stroke: #ccc;       
     stroke-width: 1.5px;  
}

e questo è il contenuto del file dendrogram02.json che consiste nella struttura dati.

{   "name": "root",   
    "children": [     
         {      "name": "parent A",      
                "children": [        
                      {"name": "child A1"},        
                      {"name": "child A2"},        
                      {"name": "child A3"}      
                 ]     
         },{      "name": "parent B",      
                  "children": [        
                      {"name": "child B1"},        
                      {"name": "child B2"}      
                 ]     
         }   ] 
}

Nel corso dell’articolo andremo a modificare questo codice al fine di ottenere un dendrogramma circolare.

Modifichiamo il codice

Dato che stiamo parlando di una rappresentazione circolare su cui distribuire radialmente i nodi del dendrogramma, sarà necessario esprimere le dimensioni della rappresentazione attraverso il diametro, invece che le classiche altezza e larghezza.

Quindi cancelliamo le variabili width e height ( nello snippet sono commentate per comodità). Definiamo quindi una variabile radius, che corrisponderà all’estensione dell’area di disegno. Inoltre definiamo la variabili margin, che definisce il margine da interporre tra l’area di disegno e il dendrogramma stesso. e la variabile angle, che rappresenta l’angolo di copertura del dendrogramma circolare. Il valore 360° esprime un angolo giro e quindi i nodi terminali del dendrogramma verranno distributi lungo l’intero perimentro della circoferenza. Un valore minore (per. esempio di 120°) porterebbee la rappresentazione del dendrogramma come un ventaglio (vedi Fig.4).

//var width = 600;  
//var height = 500;  
var radius = 350; 
var margin = 120; 
var angle = 360; 

Di conseguenza, anche la struttura dati cluster deve essere modificata di conseguenza. Allo stesso modo anche d3.svg.diagonal deve essere modificata per essere considerata radialmente.

var cluster = d3.layout.cluster()   
    //.size([height, width-200]);  
    .size([angle, radius - margin]);  
    //var diagonal = d3.svg.diagonal()  
        // .projection (function(d) { return [d.y, d.x];});  
    var diagonal = d3.svg.diagonal.radial()   
        .projection (function(d) { return [d.y, d.x / 180* Math.PI];});

Anche la definizione dell’area di disegno deve essere modificata.

var svg = d3.select("body").append("svg")   
    //.attr("width",width)   
    //.attr("height",height)   
    .attr("width",2*radius)   
    .attr("height",2*radius)   
    .append("g")   
    //.attr("transform","translate(100,0)");
    .attr("transform","translate("+radius + "," + radius + ")"); 

per quanto riguarda la rappresentazione del dendrogramma è necessario modificare solo la riga che esprime la SVG transform() dei nodi.

var node = svg.selectAll(".node")   
    .data(nodes)   
    .enter().append("g")   
    .attr("class","node")  
    .attr("transform", function(d) { return "rotate(" + (d.x - 90) + ")translate(" + d.y + ")"; });   
    //.attr("transform", function(d) { return "translate(" + d.y + "," + d.x + ")"; });

Adesso tutte le modifiche da apportare al codice sono state eseguite. Ecco il codice completo espresso qui per comodità.

var radius = 350; 
var margin = 120; 
var angle = 120; 
var cluster = d3.layout.cluster()   
    .size([angle, radius-margin]);    
var diagonal = d3.svg.diagonal.radial()   
    .projection (function(d) { return [d.y, d.x / 180* Math.PI];});  var svg = d3.select("body").append("svg")   
    .attr("width",2*radius)   
    .attr("height",2*radius)   
    .append("g")   
    .attr("transform","translate("+radius + "," + radius + ")");
   
d3.json("dendrogram02.json", function(error, root){   
    var nodes = cluster.nodes(root);   
    var links = cluster.links(nodes);   
    var link = svg.selectAll(".link")   
        .data(links)   
        .enter().append("path")   
        .attr("class","link")   
        .attr("d", diagonal);     
    var node = svg.selectAll(".node")   
        .data(nodes)   
        .enter().append("g")   
        .attr("class","node")  
        .attr("transform", function(d) { return "rotate(" + (d.x - 90) + ")translate(" + d.y + ")"; });     
    node.append("circle")   
        .attr("r", 4.5);     
    node.append("text")  
        .attr("dy", ".31em")  
        .attr("text-anchor", function(d) { return d.x < 180 ? "start" : "end"; })  
        .attr("transform", function(d) { return d.x < 180 ? "translate(8)" : "rotate(180)translate(-8)"; })  
        .text(function(d) { return d.name; });  
});

Caricando la pagina dal browser, otteniamo la rappresentazione seguente:

dendrogram too simple
Fig.2: un dendrogramma circolare troppo semplice

Non possiamo certo dire che abbiamo ottenuto un bel dendrogramma circolare. Questo perchè il dendrogramma è troppo semplice nella sua struttura. Rendiamo la struttura più complessa, andando ad aggiungere ulteriori elementi alla struttura contenuta nel file JSON che abbiamo utilizzato.

{  "name": "root",  
   "children": [  {      
        "name": "parent A",      
        "children": [        
             {"name": "child A1"},        
             {"name": "child A2"},        
             {"name": "child A3"},        
             {"name": "child A4"},        
             {"name": "child A5"},        
             {"name": "child A6"}      
         ]  
    },{      
        "name": "parent B",      
        "children": [        
             {"name": "child B1"},        
             {"name": "child B2"},        
             {"name": "child B3"},        
             {"name": "child B4"},        
             {"name": "child B5"},        
             {"name": "child B6"},        
             {"name": "child B7"},        
             {"name": "child B8"}      
          ]  
     },{     
        "name": "parent C",     
        "children": [        
             {"name": "child C1"},        
             {"name": "child C2"},        
             {"name": "child C3"},        
             {"name": "child C4"}      
          ]   
       }] 
}

Adesso se ricarichiamo nuovamente la pagina dal nostro browser, otteniamo:

first circular dendrogram
Fig.3: un dendrogramma circolare

Quindi adesso le cose sembrano funzionare egregiamente. Ora la distribuzione circolare ed uniforme delle foglie del dendrogramma lungo il perimetro circolare è abbastanza evidente.

dendrograms fan-shaped
Fig.4: Dendrogramma a ventaglio con angolo 120°

Finora abbiamo lavorato con dendrogrammi a due livelli oltre la root (n = 2). In realtà i livelli gestibili possono essere molti di più. Fin qui tutto bene, finchè il diametro del dendrogramma circolare sarà sufficientemente grande da visualizzare correttamente tutti gli elementi.

Gestire diversi livelli gerarchici

In realtà un caso limite che non abbiamo considerato finora, è per esempio quello di considerare dei dendrogrammi con dei rami che hanno più livelli rispetto ad altri.

Facciamo un esempio:

{  "name": "root",  
   "children": [  {  
         "name": "parent A",  
         "children": [  
                {"name": "child A1",   
                 "children": [          
                       {"name": "child A1-1"},          
                       {"name": "child A1-2"},          
                       {"name": "child A1-3"},          
                       {"name": "child A1-4"},          
                       {"name": "child A1-5"},          
                       {"name": "child A1-6"},          
                       {"name": "child A1-7"}      
                 ]     },     
                {"name": "child A2",      
                 "children": [           
                       {"name": "child A2-1"},           
                       {"name": "child A2-2"},           
                       {"name": "child A2-3"}      
                 ]     },     
                {"name": "child A3",       
                 "children": [           
                       {"name": "child A3-1"},           
                       {"name": "child A3-2"},           
                       {"name": "child A3-3"}      
                 ]     },     
                 {"name": "child A4",      
                 "children": [           
                       {"name": "child A4-1"},           
                       {"name": "child A4-2"},           
                       {"name": "child A4-3"}      
                 ]     },     
                 {"name": "child A5",      
                 "children": []     },     
                 {"name": "child A6",      
                 "children": [           
                       {"name": "child A6-1"},           
                       {"name": "child A6-2"},           
                       {"name": "child A6-3"}      
                 ]    }]  
        },{     
          "name": "parent B",     
          "children": [      
                {"name": "child B1"},      
                {"name": "child B2"},      
                {"name": "child B3"},      
                {"name": "child B4"},      
                {"name": "child B5"},      
                {"name": "child B6"},      
                {"name": "child B7"},      
                {"name": "child B8"}     
           ]  
         },{    
          "name": "parent C",    
          "children": [      
                {"name": "child C1"},      
                {"name": "child C2"},      
                {"name": "child C3"},      
                {"name": "child C4"}    
         ]  
}] }

Se lanciamo nuovamente la pagina, otterremo questo…

dendrogram with different levels
Fig.5: dendrogramma circolare che non rispetta le distanze tra i livelli gerarchici

Se osserviamo il dendrogramma prodotto, noteremo subito che gli elementi di un livello inferiore (n = 4) sono stati messi allo stesso livello di quelli con n = 3 lungo il perimetro esterno del dendrogramma circolare.

In rete si trovano spesso dendrogrammi espressi in questa maniera, dato che l’attenzione è centrata sugli elementi terminali del dendrogramma (foglie), indipendentemente dal loro livello gerarchico di appartenenza. In realtà, se vogliamo che l’informazione gerarchica venga mantenuta rispettando le gerarchie dei vari livelli in modo corretto, è necessario sostituire il d3.layout.cluster, con d3.layout.tree.

var tree = d3.layout.tree()   
     .size([360, diameter / 2 - 120]); 
     ... 
d3.json("dendrogram04.json", function(error, root){   
     var nodes = tree.nodes(root);   
     var links = tree.links(nodes);  
     ...  
});

Per ottenere la seguente rappresentazione circolare del dendrogramma.

dendrogram with different levels 2
Fig.6: Un dendrogramma circolare che rispetta i livelli gerarchici.

Adesso ogni livello gerarchico è distribuito ad una precisa distanza dal centro del dendrogramma (la root). Via via che il livello n cresce anche la distanza dal centro crescerà di conseguenza.

Lascia un commento