JavaScript VML Line Chart

Se ritieni utile questo articolo, considera la possibilità di effettuare una donazione (il cui importo è a tua completa discrezione) tramite PayPal. Grazie.

Creare grafici lineari (line chart) utilizzando JavaScript e DHTML non è una cosa semplicissima: l'unico modo è quello di disegnare degli elementi (tipicamente dei div) con dimensioni di 1 pixel x 1 pixel e disporli con posizionamento assoluto in un contenitore. Oltre a non essere facile, calcolare la posizione di ogni pixel ed aggiungerlo al DOM della pagina, è un'operazione molto onerosa per il client, specie quando le linee diventano molte e complesse. Una soluzione alternativa è sfruttare la capacità di Internet Explorer di gestire informazioni in formato VML (Vector Markup Language), uno standard grafico basato su XML e riconosciuto dal W3C (Vector Markup Language)

Una presentazione esaustiva di VML esula dagli obiettivi di questo articolo; per maggiori informazioni su VML è possibile consultare Introduction to Vector Markup Language (VML) (MSDN)

Una libreria per generare grafici

Prima di formalizzare la funzione di generazione del grafico vero e proprio definiamo la struttura dei dati (valori) che desideriamo rappresentare.

Struttura dei dati

Un diagramma che mostri un andamento in forma lineare può essere visto come una tabella, dove:

  • le colonne rappresentano l'asse delle ascisse (X), ovvero l'andamento progressivo (ad esempio il tempo) comune ad ogni serie di valori

  • le righe rappresentano le ordinate (Y), ovvero i singoli valori di ogni serie

Per maggior chiarezza possiamo assimilire questa forma di accorpamento dei dati a quella utilizzata in un foglio di calcolo, come Microsoft Excel (e che lo stesso Excel è in grado di mostrare in forma grafica)

Se per le ascisse può essere sufficiente un array di stringhe (le etichette dell'asse, mentre i valori sono costituiti dalla loro posizione dell'array), le righe necessitano di qualche informazione in più, indispensabile per il rendering:

function Row(linecolor, linesize, valuelist)
{
    this.color = linecolor;
    this.size = linesize;
    this.values = valuelist;
}

Ogni serie è quindi definita da:

  • linecolor: il colore della serie nel grafico

  • linesize: la dimensione della linea nel grafico

  • valuelist: i valori veri e propri, ricevuti come array

Interfaccia della funzione di disegno

Dopo aver formalizzato la struttura dati da rappresentare, analizziamo l'interfaccia del metodo di rendering:

function DrawLineChart(chartid, labels, rowlist, gridcolor)

che richiede:

  • chartid: l'ID del contenitore entro cui disegnare il grafico (tipicamente un DIV)

  • labels: l'array delle etichette (colonne)

  • rowlist: l'array delle linee da rappresentare (array di Row)

  • gridcolor: il colore della griglia del grafico

Operazioni preliminari

Le prime operazioni eseguite dalla funzione DrawLineChart preparano gli elementi del DOM ad ospitare il grafico e mostrano un messaggio di attesa all'utente durante il caricamento del grafico (in condizioni normali non risulta però nemmeno visibile, data la rapidità con cui il grafico viene visualizzato)

// get chart container
var chart = document.getElementById(chartid);
var chartSize = getElementSize(chart);
// reset chart    
chart.innerHTML = "";    // reset
chart.style.visibility = "hidden";

// show waiting message
var loadingmessage = document.createElement("div");
loadingmessage.className = "loadingmessage";
loadingmessage.innerText = "Loading...";
loadingmessage.style.position = "absolute";
chart.parentElement.appendChild(loadingmessage);
loadingmessage.style.top = (getElementPosition(chart).top + chartSize.height / 2) - getElementSize(loadingmessage).height / 2;
loadingmessage.style.left = (getElementPosition(chart).left + chartSize.width / 2) - getElementSize(loadingmessage).width / 2;

var chartSize = getElementSize(chart);
var mingridspace = 20;    // pixels
var xlabelheight = 0;

Caclolo del valore massimo e minimo

Il codice successivo esegue un ciclo sui valori da rappresentare al fine di individuare il valore massimo e quello minimo per determinare le scale da utilizzare:

// get Max and Min values
var maxvalue = null;
var minvalue = null;
for(var y = 0; y < rowlist.length; y++)
{
    for(var x = 0; x < rowlist[y].values.length; x++)
    {
        maxvalue = (maxvalue == null || rowlist[y].values[x] > maxvalue) ? rowlist[y].values[x] : maxvalue;
        minvalue = (minvalue == null || rowlist[y].values[x] < minvalue) ? rowlist[y].values[x] : minvalue;
    }
}
maxvalue = (maxvalue == null || isNaN(maxvalue)) ? 0 : maxvalue;
maxvalue += Math.round(maxvalue * 0.05);    // add 5% for top margin
minvalue = (minvalue == null || isNaN(minvalue)) ? 0 : minvalue;

Griglia delle ascisse

Utilizzando l'array delle etichette viene disegnato l'asse e la relativa griglia:

// get X coords
var xcoords = new Array();
for(var x = 0; x < labels.length; x++)
    xcoords[x] = Math.round(chartSize.width * x / labels.length);
// draw X grid
var steps = 1;
var deltax = Math.round(chartSize.width / labels.length);
if(deltax < 1)    // prevent dived by zero
    deltax = 1;
while(deltax * steps < mingridspace)
    steps++;
for(var x = steps; x < xcoords.length; x += steps)
{
    // x lines
    var l = document.createElement("v:line");
    l.strokeweight = "0.75pt";
    l.strokecolor = gridcolor;
    var s = document.createElement("v:stroke");
    s.dashstyle = "dot";
    l.appendChild(s);
    l.from = xcoords[x] + "," + 0 + "";
    l.to = xcoords[x] + "," + chartSize.height + "";
    chart.appendChild(l);
    // x labels
    var r = document.createElement("div");
    r.className = "labelx";
    r.innerText = labels[x];
    chart.appendChild(r);
    r.style.position = "absolute";
    rsize = getElementSize(r);
    r.style.top = getElementPosition(chart).top + chartSize.height - rsize.height;
    r.style.left = getElementPosition(chart).left + xcoords[x];
    xlabelheight = (rsize.height > xlabelheight) ? rsize.height : xlabelheight;    
}

Si noti la semplicità con cui è stata disegnata la griglia: è stato sufficiente aggiungere al nostro contenitore (chart) dei semplici nodi XML (line e stroke con i relativi attributi). Sarà poi il nostro browser ad interpretare questi "tag" e rappresentarli in forma grafica.

Griglia delle ordinate

Ripetiamo quanto visto per le ascisse anche per le ordinate:

// draw Y grid
var deltavalues = maxvalue - minvalue;    
var deltay = Math.round((chartSize.height - xlabelheight) / deltavalues);
if(deltay < 1)    // prevent dived by zero
    deltay = 1;
while(deltay < mingridspace)
    deltay = deltay * 2;    
for(var y = chartSize.height - xlabelheight; y > mingridspace; y -= deltay)
{
    // y lines
    var l = document.createElement("v:line");
    l.strokeweight = "0.75pt";
    l.strokecolor = gridcolor;
    var s = document.createElement("v:stroke");
    s.dashstyle = "dot";
    l.appendChild(s);
    l.from = 0 + "," + y + "";
    l.to = chartSize.width + "," + y + "";
    chart.appendChild(l);
    // y labels
    var lbl = Math.round((chartSize.height - xlabelheight - y) * deltavalues / (chartSize.height - xlabelheight));
    var r = document.createElement("div");
    r.className = "labely";
    r.innerText = lbl;
    chart.appendChild(r);
    r.style.position = "absolute";
    rsize = getElementSize(r);
    r.style.top = y + getElementPosition(chart).top - rsize.height;
    r.style.left = getElementPosition(chart).left;
    var r1 = r.cloneNode();
    r1.style.left = getElementPosition(chart).left + chartSize.width - rsize.width;
    r1.innerText = lbl;
    chart.appendChild(r1);
}

Rappresentazione della serie

Dopo aver preparato la struttura del grafico, aggiungiamo ogni singola serie da rappresentare:

// draw lines
for(var y = 0; y < rowlist.length; y++)
{
    var lastX = null;
    var lastY = null;
    for(var x = 0; x < rowlist[y].values.length; x++)
    {
        var ycoord = Math.round((chartSize.height - xlabelheight) - ((chartSize.height - xlabelheight) * rowlist[y].values[x] / maxvalue))
        if(lastX != null && lastY != null)
            _drawLine(chart, rowlist[y].color, rowlist[y].size, lastX, lastY, xcoords[x], ycoord);
        lastX = xcoords[x];
        lastY = ycoord;
    }
}

Per una maggior comprensione dello script si è spostato in una funzione d'appoggio il rendering di ogni singola linea: _drawLine

function _drawLine(p, color, width, fx, fy, tx, ty)
{
    var l = document.createElement("v:line");
    l.strokeweight = width + "pt";
    l.strokecolor = color;
    l.from = fx + "," + fy + "";
    l.to = tx + "," + ty + "";
    var s = document.createElement("v:stroke");
    s.endarrow = "diamond";
    l.appendChild(s);
    p.appendChild(l);
}

In pratica viene disegnata una linea (che termina con un segnaposto a forma di diamante) che congiunge ogni valore della serie con il successivo.

Operazioni conclusive

Per completare il codice della funzione di disegno non ci resta che nascondere il messaggio di attesa ("Loading...") e visualizzare il grafico:

// hide waiting message and show the chart
chart.parentElement.removeChild(loadingmessage);
chart.style.visibility = "visible";

Funzioni di utilità interne

Per il calcolo delle posizioni e delle dimensioni degli elementi sono state utilizzate due funzioni JavaScript, getElementPosition e getElementSize per le quali si rimanda all'articolo JavaScript Base Library.
Il download del sorgente e della demo allegato a questo articolo contiene una copia delle suddette funzioni.

Utilizzare lo script nella pagina

Oltre ad includere la libreria JavaScript e disporre un contenitore per il grafico (DIV), nella pagina è necessario:

  • includere la libreria JavaScript "vmlchart.js":

    <script type="text/javascript" src="vmlchart.js"></script>
  • disporre l'elemento che conterrà il grafico:

    <div id="myChart" style="width:600px; height:400px; border: 1px solid #000; background-color: #eee;"></div>
  • specificare l'utilizzo di VML:

    <xml:namespace ns="urn:schemas-microsoft-com:vml" prefix="v"/>
  • definire lo style (CSS) per la rappresentazione del grafico:

    v\:* { behavior:url(#default#VML); antialias: true; margin: auto; }
  • definire via JavaScript le etichette, le serie di valori e richiamare la funzione di rendering (il codice che segue è puramente indicativo):

    var labels = new Array("L", "M", "M", "G", "V", "S", "D");
    var s1 = new Row("#ff0000", 1, new Array(1, 2, 3, 4, 5, 6, 7));
    var s2 = new Row("#0000ff", 1, new Array(5, 7, 3, 1, 8, 0, 9));
    var rows = new Array(s1, s2);
    DrawLineChart("myChart", labels, rows, "#aaaaaa");

Demo

Per visualizzare una dimostrazione pratica di utilizzo della libreria, impostate il numero di linee da visualizzare ed il numero di campionature desiderate, quindi premete il tasto "Visualizza grafico".

Note:

  • I dati visualizzati sono generati in modo casuale

  • Lo script richiede l'utilizzo di Microsoft Internet Explorer 5.5 o successivo.

Impostazioni    

Non solo VML

Oltre a VML esiste un'altro (e forse più affermato) formato di grafica vettoriale basato su XML: SVG (Scalable Vector Graphics). SVG è utilizzabile su qualunque browser tramite l'installazione di un plug-in (sia Adobe che Corel ne distribuiscono uno gratuito); per maggiori informazioni su SVG si veda:

Essendo sia VML che SVG basati su XML non è particolarmente complesso effettuare il porting della libreria presentata in questo articolo affiché supporti il formato SVG: le logiche di calcolo della coordinate (praticamente il 99% di quello che fa la libreria!) rimangono infatti invariate.