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.
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:
- Completato il caricamento della pagina inizia la lettura delle impostazioni di Arbor.js e la costruzione del grafico;
- 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), edata
( un oggetto contentente datichiave=valore
per salvare informazioni aggiuntive per singolo nodo. - addEdge prende in ingresso
source
etarget
che rappresentano rispettivamente i riferimenti ai nodi di partenza e arrivo dell'arco, inoltre con il parametrodata
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)