Nessun risultato. Prova con un altro termine.
Guide
Notizie
Software
Tutorial

Grafi dinamici con in Canvas con Arbor.js

Plugin jQuery per realizzare grafi dinamici e interattivi
Plugin jQuery per realizzare grafi dinamici e interattivi
Link copiato negli appunti

In questo articolo ci serviremo di un semplice esempio per mostrare alcune delle caratteristiche di Arbor.js. Si tratta di un plugin per jQuery che sfrutta in features HTML5 come i Web workers per creare grafi animati e interattivi.

Grazie a Arbor.js possiamo creare un grafo a partire da un insieme di dati, e una volta ottenuto il rendering del grafo, possiamo spostare i nodi del grafo a piacimento. Per questo possiamo impostare anche alcune caratteristiche di archi e nodi, come l'elasticità o la resistenza al movimento.

Il disegno vero e proprio degli elementi è demandato al programmatore, che può sfruttare tecnologie come Canvas, SVG o semplice HTML. Questo ci consente di preoccuparci solo dell'aspetto del grafo, demandando alla libreria la gestione della "fisica" del sistema.

Gli usi possibili di una simile libreria sono diversi, dalla rappresentazione delle distanze tra città alle mappe mentali, dalla creazione di infografiche interattive fino alla visualizzazione di sistemi di particelle.

Figura 1. Esempio di grafo con arbor.js
Esempio di grafo

Installazione di Arbor.js

Per iniziare, come per tutti i plugin per jQuery, abbiamo bisogno di caricare la libreria di base (jQuery appunto) e il file della nostra estensione:

<script src="path/to/jquery.min.js"></script>
<script src="path/to/arbor.js"></script>

La documentazione ci consiglia di includere anche il file arbor-tween.js, che aggiungerà alcuni effetti di movimento all'oggetto ParticleSystem che utilizzeremo.

<script src="path/to/arbor-tween.js"></script>

Definire nodi e vertici del grafo

Creiamo ora il nostro primo grafo. Anzitutto è necessario organizzare i dati secondo una precisa sintassi, che definendo i nodi (o vertici) e gli archi che collegano tra loro questi elementi. Creiamo una semplice pagina che carichi dati JSON di vertici e archi via Ajax secondo questo standard:

{
         "nodes": [
         {"name": "node_1"}, {"name": "node_2"},
         {"name": "node_3"}, {"name": "node_4"},
         {"name": "node_5"}, {"name": "node_6"},
         {"name": "node_7"}, {"name": "node_8"},
         {"name": "node_9"}, {"name": "node_10"}
         ],
         "edges": [
         {"src": "node_3", "dest": "node_2"},
         {"src": "node_5", "dest": "node_3"},
         {"src": "node_8", "dest": "node_7"},
         {"src ":"node_1", "dest": "node_4"},
         {"src": "node_7", "dest": "node_5"},
         {"src": "node_3", "dest": "node_9"},
         {"src": "node_2", "dest": "node_4"},
         {"src": "node_6", "dest": "node_5"},
         {"src": "node_9", "dest": "node_1"},
         {"src": "node_10", "dest": "node_2"},
         {"src": "node_1", "dest": "node_10"}
         ]
        }

Nell'elemento nodes vengono specificati i nodi del grafo, mentre in edges sono registrate gli archi, ovvero le relazioni tra i nodi, grazie alle chiavi src e dest.

L'oggetto ParticleSystem

Come abbiamo detto, possiamo scegliere la tecnologia di rappresentazione che più ci è utile, per questo esempio quindi scegliamo il Canvas e aggiungiamo il nostro elemento contenitore all'interno della pagina:

<canvas id="viewport" width="800" height="600"></canvas>

Nell'esempio in allegato possiamo trovare tutto il codice necessario a compiere l'animazione, qui limitiamoci a esaminare in astratto il ciclo di vita della creazione del grafo:

  1. Completato il caricamento della pagina inizia la lettura delle impostazioni di Arbor.js e la costruzione del grafico;
  2. Durante il rendering di ogni vertice e nodo si verificano alcuni eventi e vengono chiamati i relativi handler, questo meccanismo ci permette di personalizzare look e comportamenti del grafo.

Vediamo nel dettaglio alcune delle istruzioni invocate nel rendering degli elementi: anzitutto è necessario creare un oggetto ParticleSystem, il sistema di particelle in cui memorizzare archi e vertici, e in cui tenere aggiornate le loro coordinate. Il costruttore del sistema può essere richiamato con:

arbor.ParticleSystem(repulsion, stiffness, friction, gravity, fps, dt, precision)

ecco il dettaglio dei parametri:

Parametro Default Descrizione
repulsion 1000 la forza repulsiva tra i nodi
stiffness 600 la rigidità degli spigoli
friction 0.5 la quantità di frizione del sistema
gravity false forza supplementare per attrarre i nodi alla posizione originale
fps 55 frames al secondo
dt 0.02 tempo di refresh della simulazione
precision 0.6 Precisione contro velocità nei calcoli della forza (0 è veloce ma nervoso, 1 è lento, ma incice sulla CPU)

I valori di default saranno utilizzati per tutti gli argomenti omessi. Per semplificare, le seguenti chiamate sono tutte equivalenti:

arbor.ParticleSystem()
arbor.ParticleSystem(600)
arbor.ParticleSystem(600, 1000, .5, 55, .02, false)
arbor.ParticleSystem({friction:.5, stiffness:600, repulsion:1000})

Un'altra modalità è creare l'oggetto ParticleSystem, e valorizzare i parametri col metodo parameters():

var sys = arbor.ParticleSystem()
sys.parameters({gravity:true, dt:0.005})

Il disegno del grafo

Ora dobbiamo impostare il disegno vero e proprio del grafo e lo possiamo fare creando un oggetto Renderer, grazie al quale definire:

  • lo stato iniziale dell'animazione (implementare il metodo init)
  • il modo di ridisegnare ogni frame (implementare il metodo redraw)

Una volta definite queste funzioni, passiamo il nostro oggetto come attributo di rendering del sistema di particelle:

sys.renderer = myRenderer;

In altre parole il metodo init sarà invocato una volta sola, all'inizio dell'animazione, mentre redraw sarà invocato ogni volta che lo schermo deve essere aggiornato, per esempio quando un nodo viene spostato.

Nel codice che segue vediamo i primi comandi che vengono eseguiti dall'applicazione al momento del caricamento della pagina:

$(document).ready(function() {
  sys = arbor.ParticleSystem(1000);    // Crea un sistema di particelle
  sys.parameters({gravity:true});      // Include la gravità
  sys.renderer = Renderer("#viewport") // Inizia a disegnare nella viewport
  $.getJSON("data.json",               // Ottiene i dati dal server
            function(data){
			  $.each(data.nodes, function(i,node){
			    sys.addNode(node.name);  // Aggiunge nodi ...
			  });
			  $.each(data.edges, function(i,edge) {
			    sys.addEdge(sys.getNode(edge.src),sys.getNode(edge.dest)); // ... e archi
			  });
			});
})

Come si vede, per aggiungere vertici e archi, invochiamo rispettivamente i metodi addNode e
addEdge, esaminiamone i parametri:

  • addNode prende name (identificatore che sarà utilizzato dal sistema), e data ( un oggetto contentente dati chiave=valore per salvare informazioni aggiuntive per singolo nodo.
  • addEdge prende in ingresso source e target che rappresentano rispettivamente i riferimenti ai nodi di partenza e arrivo dell'arco, inoltre con il parametro data possiamo impostare etichette aggiuntive all'arco.

Naturalmente il primo metodo ritornerà un oggetto di tipo Node, mentre il secondo di tipo Edge.

Vediamo ora in dettaglio il metodo init:

init:function(system){
    //INIZIALIZZAZIONE
    particleSystem = system;
    particleSystem.screenSize(canvas.width, canvas.height);
    particleSystem.screenPadding(80);
    that.initMouseHandling();
},

Questo metodo viene invocato solo una volta per impostare le proprietà di default del sistema come le dimensioni del Canvas (metodo screenSize) e abilitare la gestione degli eventi del mouse sull'oggetto.

Non rimane che analizzare rapidamente anche il metodo redraw:

redraw:function(){
    // Effettua il refresh
    ctx.fillStyle = "white";
    ctx.fillRect(0,0, canvas.width, canvas.height); // Riempie l'area intera
    particleSystem.eachEdge( // Per ogni bordo
     function(edge, pt1, pt2){ //LAVORERÀ CON TUTTI I BORDI E PUNTI DI INIZIO E FINE
      ctx.strokeStyle = "rgba(0,0,0, .333)"; //LE FACCE SARANNO NERE CON UNA SFUMATURA
      ctx.lineWidth = 1; //1 PIXEL
      ctx.beginPath();  //INIZIO DEL DISEGNO
      ctx.moveTo(pt1.x, pt1.y); //DAL PUNTO A
      ctx.lineTo(pt2.x, pt2.y); //AL PUNTO B
      ctx.stroke();
    });
    particleSystem.eachNode( //OGNI VERTICE
     function(node, pt){  //OTTENGO IL TOP E IL PUNTO
      var w = 10;   //LARGHEZZA
      ctx.fillStyle = "orange"; //COLORE CHIARO
      ctx.fillRect(pt.x-w/2, pt.y-w/2, w,w); //DISEGNO
      ctx.fillStyle = "black"; //COLORE DEL FRONT
      ctx.font = 'italic 13px sans-serif'; //CARATTERE
      ctx.fillText (node.name, pt.x+8, pt.y+8); //SCRIVO IL NOME DI OGNI NODO
    });
},

A parte le impostazioni del Canvas, i metodi che più ci interessano sono eachEdge e eachNode che permettono di iterare tra tutti gli oggetti presenti nel sistema.

Il sistema, ovviamente, parte in automatico non appena tutti gli oggetti sono pronti alla visualizzazione, ma la libreria ci mette a disposizione comunque dei metodi per gestire l'avvio e lo stop l'animazione, con start e stop.

È possibile visualizzare tutto il codice nella prossima pagina.

Link utili

Il codice completo dell'esempio

(function($){
 var Renderer = function(canvas)
 {
  var canvas = $(canvas).get(0);
  var ctx = canvas.getContext("2d");
  var particleSystem;
  var that = {
   init:function(system){
    //INIZIALIZZAZIONE
    particleSystem = system;
    particleSystem.screenSize(canvas.width, canvas.height);
    particleSystem.screenPadding(80);
    that.initMouseHandling();
   },
   redraw:function(){
    //IN FASE DI REDRAW, EFFETTUO IL REFRESH
    ctx.fillStyle = "white";
    ctx.fillRect(0,0, canvas.width, canvas.height); //COLORO L'AREA INTERA
    particleSystem.eachEdge( //PER OGNI BORDO
     function(edge, pt1, pt2){ //LAVORERÀ CON TUTTI I BORDI E PUNTI DI INIZIO E FINE
      ctx.strokeStyle = "rgba(0,0,0, .333)"; //LE FACCE SARANNO NERE CON UNA SFUMATURA
      ctx.lineWidth = 1; //1 PIXEL
      ctx.beginPath();  //INIZIO DEL DISEGNO
      ctx.moveTo(pt1.x, pt1.y); //DAL PUNTO A
      ctx.lineTo(pt2.x, pt2.y); //AL PUNTO B
      ctx.stroke();
    });
    particleSystem.eachNode( //OGNI VERTICE
     function(node, pt){  //OTTENGO IL TOP E IL PUNTO
      var w = 10;   //LARGHEZZA
      ctx.fillStyle = "orange"; //COLORE CHIARO
      ctx.fillRect(pt.x-w/2, pt.y-w/2, w,w); //DISEGNO
      ctx.fillStyle = "black"; //COLORE DEL FRONT
      ctx.font = 'italic 13px sans-serif'; //CARATTERE
      ctx.fillText (node.name, pt.x+8, pt.y+8); //SCRIVO IL NOME DI OGNI NODO
    });
   },
   initMouseHandling:function(){ //EVENTO DEL MOUSE
    var dragged = null;   //IL VERTICE CHE SI MUOVE
    var handler = {
     clicked:function(e){ //PREMUTO
      var pos = $(canvas).offset(); //OTTENGO LA POSIZIONE DEL CANVAS
      _mouseP = arbor.Point(e.pageX-pos.left, e.pageY-pos.top); //E LA POSIZIONE RELATIVA AL PUNTO PREMUTO SUL CANVAS
      dragged = particleSystem.nearest(_mouseP); //DETERMINA IL VERTICA PIU' VICINO
      if (dragged && dragged.node !== null){
       dragged.node.fixed = true; //RILASCIO
      }
      $(canvas).bind('mousemove', handler.dragged); //HANDLER DEL MOUSE MOVE
      $(window).bind('mouseup', handler.dropped);  //E DEL MOUSE UP
      return false;
     },
     dragged:function(e){ //RITORNO AL TOP
      var pos = $(canvas).offset();
      var s = arbor.Point(e.pageX-pos.left, e.pageY-pos.top);
      if (dragged && dragged.node !== null){
       var p = particleSystem.fromScreen(s);
       dragged.node.p = p; //PUNTARE LA PARTE BASSA DEL MOUSE
      }
      return false;
     },
     dropped:function(e){ //RELASCIO
      if (dragged===null || dragged.node===undefined) return; //SE NON SI MUOVE, NULL
      if (dragged.node !== null) dragged.node.fixed = false; //SE SI MUOVE, INIZIA
      dragged = null; //PULISCO TUTTO
      $(canvas).unbind('mousemove', handler.dragged); //FERMO TUTTO GLI EVENTI
      $(window).unbind('mouseup', handler.dropped);
      _mouseP = null;
      return false;
     }
    }
    // ASCOLTO GLI EVENTI DEL MOUSE
    $(canvas).mousedown(handler.clicked);
   },
  }
  return that;
 }    
 $(document).ready(function(){
  sys = arbor.ParticleSystem(1000); // CREO UN SISTEMA DI PARTICELLE
  sys.parameters({gravity:true}); // INCLUDO LA GRAVITA'
  sys.renderer = Renderer("#viewport") //INIZIO A DISEGNARE NEL VIEWPORT
  $.getJSON("data.json", //OTTENGO I DATI DAL SERVER
   function(data){
    $.each(data.nodes, function(i,node){
     sys.addNode(node.name); //AGGIUNGO UN VERTICE
    });
    $.each(data.edges, function(i,edge){
     sys.addEdge(sys.getNode(edge.src),sys.getNode(edge.dest)); //E I NODI
    });
  });
 });
})(this.jQuery)

Ti consigliamo anche