Grafici in HTML con PHP

di Antonio Bonifati <antonio.bonifati@libero.it>

Introduzione

In questo articolo vedremo com'è semplice generare dei semplici grafici a barre, sia orizzontali che verticali usando un pacchetto di funzioni PHP che semplifica molto il lavoro. Presuppongo una conoscenza almeno elementare di PHP. Se non sapete nulla di PHP potete trovare un mio tutorial introduttivo qui:

http://monitor.deis.unical.it/ant/it/php/

Il linguaggio di scripting PHP può utilizzare la libreria grafica GD. E' sufficiente compilare PHP col supporto per GD (che deve essere installata prima ovviamente), oppure in alcune distribuzioni (ad es. la Mandrake 7.2) basta installare il pacchetto php-gd e sarete in grado di creare "dinamicamente" immagini in formato PNG da script PHP. In particolare con le primitive di questa libreria potete creare grafici di ottima fattura. Tuttavia per alcune semplici applicazioni, come quelle che descriverò in questo articolo, non è necessario ricorrere a GD: anche solo con l'utilizzo dell'HTML si possono produrre grafici a barre esteticamente belli. Di certo non potete disegnare grafici 3D o a torta, cosa che invece potete fare con la GD, tuttavia vale la pena lo stesso di esplorare questa possibilità.

Installazione di HTML_Graphs

Iniziate ad installare HTML_Graphs che è un pacchetto di funzioni per generare con facilità grafici in HTML, scritto da Phil Davis, ed è free software distribuito secondo la licenza GPL (GNU General Public License). Potete scaricarlo da qui e nel contempo dare un'occhiata di quello che è in grado di fare:

http://www.webguys.com/pdavis/Programs/html_graphs/

L'installazione è molto semplice, basta scompattarlo al di sotto della document root. Ad es. nel mio PC con Linux la document root è /var/www/html/ e per scompattare i file zip utilizzo l'utility unzip. Per ordine ho deciso di porre tutti i file dentro la directory web "/HTML_Graphs":

# unzip HTML_Graphs.zip -d /var/www/html/HTML_Graphs

Per far funzionare l'esempio in un sistema UNIX, dove i nomi dei file sono case-sensitive, occorre che cambiate la riga

require("./html_graphs.php");

in HTML_Graphs_Example.php (dovrebbe essere la linea 79) in:

require("./HTML_Graphs.php");

o viceversa cambiate nome al file. Fatto questo con un browser andate all'URL:

http://localhost/HTML_Graphs/HTML_Graphs_Example.php

Un esempio

Supponete di voler realizzare un semplice sondaggio online sul vostro sito, di quelli che vado molto di moda su alcuni portali ultimamente. Si tratta di porre ai visitatori una domanda con risposte multiple da scegliere. Volete che dopo aver votato il visitatore ritorni alla stessa pagina in cui però al posto del questionario appaiano le statistiche e che queste possano essere visionate anche senza dover votare tramite apposito link.

Ecco ad es. una domanda in cui si può dare una sola risposta:

Quale sistema operativo usi di più?

Windows 95/98/Me
Windows NT/2000
MacOS
Linux
altro Unix

Il codice HTML relativo è piuttosto semplice (il file l'ho chiamato index.php):

<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 3.2//EN">
<html>
<head>
<meta name="generator" content="HTML Tidy, see www.w3.org">
<title>questionario</title>
</head>
<body>
<form method="post" action="index.php">
<p>Quale sistema operativo usi di pi&ugrave;?</p>

<table border="0">
<tr>
<td>Windows 95/98/Me</td>
<td><input type="radio" name="voto" value="Windows 95/98/Me"></td>
</tr>

<tr>
<td>Windows NT/2000</td>
<td><input type="radio" name="voto" value="Windows NT/2000"></td>
</tr>

<tr>
<td>MacOS</td>
<td><input type="radio" name="voto" value="MacOS"></td>
</tr>

<tr>
<td>Linux</td>
<td><input type="radio" name="voto" value="Linux"></td>
</tr>

<tr>
<td>altro Unix</td>
<td><input type="radio" name="voto" value="altro Unix"></td>
</tr>
</table>

<p><input type="submit" value="vota"></p>
</form>
</body>
</html>

Dobbiamo ora scrivere il codice PHP che effettua il conteggio delle scelte, che ovviamente deve essere salvato in qualche formato su disco. Una scelta conveniente è usare queste due funzioni, disponibili sin da PHP 3.0.5:

string serialize (mixed value);
mixed unserialize (string str);

Come risulta dal codice HTML visto prima, allo script viene passata una variabile $voto la quale indica univocamente mediante una etichetta (ad es. "Windows 95/98/Me") quale voto è stato espresso. Per memorizzare tutta la statistica, una struttura dati conveniente è un array associativo (spesso detto anche "hash") in cui l'indice è l'etichetta e il valore corrispondente a quell'indice indica il numero di voti espressi. Ho chiamato questo array $voti. Verso la fine dello script, $voti viene serializzato, ossia trasformato in una stringa che rappresenta valori e tipi della variabile, mediante la funzione serialize. La stringa viene poi scritta su disco in un file chiamato "voti.ser" (a cui potete dare il nome che vi pare comunque). All'inizio il programma recupera il valore precedente dell'hash voti leggendo il file voti.ser e deserializzandolo tramite la funzione unserialize.

Grazie a queste due funzioni e ad un semplice file di testo siamo quindi in grado di mantenere sul server lo stato dell'array voti tra una richiesta e l'altra. Notate anche che non occorre conoscere il formato di memorizzazione usato da serialize, anche se può essere interessante darci un'occhiata.

Ecco quindi index.php con l'aggiunta del codice che effettua la memorizzazione dei dati (tutto il codice aggiunto è evidenziato in neretto):

<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 3.2//EN">
<html>
<head>
<meta name="generator" content="HTML Tidy, see www.w3.org">
<title>questionario</title>
</head>
<body>

<?

if (!empty($voto))
{
   # Cambia qui il nome del file di dati.
   # Se il file non esiste va creato vuoto in modo
   # che almeno l'utente web (di solito detto
   # apache o nobody) possa leggerlo e scriverlo.
   # Usa i comandi:
   #    # touch nome_file
   #    # chown apache.apache nome_file
   #    # chmod 600 nome_file
   define('DFN', 'voti.ser');

   # deserializza i dati da un file su disco
   $fp=fopen(DFN, 'r+b');
   # se il file può essere più grosso di 4096 bytes, aumentare ;)
   $voti=unserialize(fread($fp, 4096));

   # conteggia questo voto
   $voti[$voto]++;

   # serializza i dati sullo stesso file
   rewind($fp);
   fwrite($fp, serialize($voti));
   fclose($fp);

?>

<p>Grazie per aver votato. Torna alla <a href="/">home</a>.</p><?
}
else
{
?>
<form method="post" action="index.php">
<p>Quale sistema operativo usi di pi&ugrave;?</p>

<table border="0">
<tr>
<td>Windows 95/98/Me</td>
<td><input type="radio" name="voto" value="Windows 95/98/Me"></td>
</tr>

<tr>
<td>Windows NT/2000</td>
<td><input type="radio" name="voto" value="Windows NT/2000"></td>
</tr>

<tr>
<td>MacOS</td>
<td><input type="radio" name="voto" value="MacOS"></td>
</tr>

<tr>
<td>Linux</td>
<td><input type="radio" name="voto" value="Linux"></td>
</tr>

<tr>
<td>altro Unix</td>
<td><input type="radio" name="voto" value="altro Unix"></td>
</tr>
</table>

<p><input type="submit" value="vota"></p>
</form>

<?
}
?>
</body>
</html>

Lucchetti e semafori

Cosa avranno mai a che fare i lucchetti e addirittura i semafori con l'argomento di questo tutorial? L'algoritmo visto prima non funziona correttamente in ambiente multitasking, ma per mia fortuna non è da buttar via. Invece basta solo aggiungere un'istruzione per risolvere il problema, di cui ho già parlato una volta nel mio tutorial su PHP. Subito dopo la fopen aggiungete:

flock($fp, 2);

Se già conoscete il motivo di questa istruzione, potete passare al paragrafo successivo, altrimenti vale forse la pena di capirci qualcosa di più, anche se esula dall'argomento di questo tutorial. Vi propino una spiegazione semplice di un argomento secondo me non tanto facile da capire.

Considerate un file contenente dei dati che possono o devono essere modificati. Ammettete poi di essere in un ambiente multitasking (dove l'esecuzione di un processo viene sospesa per un certo tempo per continuare quella di un'altro precedentemente sospeso, poi viene sospeso il secondo e il primo viene ripreso ecc...) e che ci siano più processi che accedono a quel file in lettura e/o scrittura. In questa situazione si possono verificare dei problemi. Nel caso dell'esempio precedente siamo in ambiente multitasking, perché ci possono essere più copie dello stesso script index.php che accedono allo stesso file voti.ser create dal server web che utilizzate per ottimizzare in modo notevole le prestazioni (pensate a come lento sarebbe un server web che non può gestire un'altra richiesta se prima non è terminata la precedente). Apache ad esempio, il web server più usato su internet e ancora più diffuso in ambiente Unix, è un server multiprocesso e consiste in un processo genitore che coordina una serie di processi figli che poi sono quelli che servono le pagine.

Esemplifico un genere di problemi che si può verificare. Come detto prima, in un server multiprocesso come Apache, più persone possono accedere alle vostre pagine web praticamente nello stesso tempo. Quindi ci possono essere due o più processi che eseguono l'interprete PHP sul file index.php e che leggeranno e poi scriveranno nel file voti.ser. Detto in altri termini ci sono più persone che stanno esprimendo il loro voto contemporaneamente (o quasi). Per semplicità supponete siano solo due persone (a maggior ragione il problema si potrà verificare se sono di più). Quindi ci sono due persone che attivano l'esecuzione del programma index.php. Ogni programma index.php apre in lettura il file voti.sar. Si può essere così sfortunati che può verificarsi ad es. una cosa del genere:

  1. il programma 1 incrementa il contatore di Linux dal valore 43,768 a 43,769 e poi scrive il nuovo valore sul file.
  2. il programma 2, quasi allo stesso tempo apre il file e legge il valoredel contatore 43,768. Supponendo che anche l'altro utente abbia votato per il pinguino, quello che può succedere è che il programma 2 incrementa il contatore di Linux e scriva il valore 43,769.

L'errore che si verifica è che avete perso un voto. Non è una grande tragedia, tuttavia significa che il nostro sistema di votazione online non è accurato e che più il sito è trafficato e meno accurato diventa, perché maggiore è la probabilità che si verifichino problemi come quello descritto sopra. Questo genere di problemi vengono detti in gergo "race condition".

Potete immaginare una soluzione per questo problema della race condition? Se ci pensate bene la soluzione dovrebbe essere ovvia:

ogni volta che volete leggere e scrivere in un file che potrebbe essere modificato da un altro processo, dovreste bloccare l'altro processo, impedendogli di modificare il file mentre il vostro processo lo vuole modificare ("file locking", dall'inglese to lock che significa bloccare, chiudere a chiave, mentre lock è la serratura).

In pratica occorre un meccanismo con cui il primo processo, quello che per primo ha messo le mani sul file, comunichi al secondo processo che sta tentando di aprire un file già aperto dal primo un messaggio che gli indichi che deve aspettare finché l'altro processo ha finito di usare il file. Chi prima arriva prima alloggia... e mette il lucchetto alla risorsa :)

Un modo per comunicare questo messaggio, usando un proprio meccanismo di locking, potrebbe consistere nel convenire di utilizzare un'altro file che in qualche modo, ad es. semplicemente attraverso la sua esistenza, comunica che il file voti.ser è bloccato. Questo file viene spesso chiamato "semaforo" (in inglese semaphore), perché analogamente ai semafori stradali che segnalano quando la strada è libera o occupata, questo file segnala quando una risorsa di sistema (nel nostro esempio l'altro file voti.ser) è disponibile. Il codice seguente implementa in PHP questo tipo di file-locking usando un proprio file di lock:

while (file_exists('voti.ser.lock'))
{
   usleep(1000);
   clearstatcache();
}
$lock_fp=fopen('voti.ser.lock','w');
...
# codice che legge e/o scrive voti.ser, come sopra
...
fclose($lock_fp);
unlink('voti.ser.lock');

Questo codice come vedremo purtroppo è da buttare, comunque è molto istruttivo descriverne il funzionamento. Con la funzione file_exists controlliamo se il file di lock esiste. Se esiste vuol dire che un altro processo sta usando il file voti.ser. Se siamo in questo caso col while precedente il nostro processo aspetterà finché il file di lock, voti.ser.lock non esiste più. Aspetterà anche all'infinito, ma questo è un altro problema che possiamo supporre che in condizioni normali non si verifichi. Tra uno sguardo e l'altro al semaforo attendiamo un tempo definito dal parametro della funzione usleep, per non appesantire il sistema con una raffica di chiamate a file_exists. Usiamo usleep perché la funzione sleep() accetta un numero intero di secondi come unità di ritardo, che per un lock è un tempo molto lungo di attesa. Considerando la notevole velocità dei calcolatori, il tempo necessario ad uno sblocco dovrebbe essere mediamente solo di alcuni microsecondi. L'argomento della funzione usleep è il numero di microsecondi di attesa (la u è una stilizzazione della lettera greca mu spesso usata per indicare un milionesimo).

Quando il file di lock non esiste più, il programma sa che può creare il suo file di lock e cominciare a modificare il file voti.ser. Perciò crea un file di lock con la funzione fopen. Con questo comando, il programma dice a tutti gli altri programmi che sta per modificare il file voti.ser e lo blocca, come quando si mette il lucchetto a qualcosa. Quando il programma ha finito di riscrivere il file voti.ser, chiude il file di lock (voti.ser.lock) e poi usa la funzione unlink per cancellare il lock file. Dopo che il lock file è stato cancellato, un altro programma che stava aspettando su quel semaforo può reiniziare il processo che abbiamo descritto.

Il file di lock che abbiamo usato non è un file speciale; è solo un nome di file comune usato da ogni processo che vuole modificare il file voti.ser. Per convenzione ho scelto di utilizzare il nome del file da bloccare più l'ulteriore estensione .lock come nome del file di lock, ma è solo una buona convenzione. Il file di lock viene creato da una comune chiamata a fopen e cancellato tramite il comando unlink. Quando un file di lock esiste, ogni processo sa che deve aspettare prima di modificare il file bloccato (voti.ser nel nostro caso).

Note:

  1. La funzione usleep non è supportata dai sistemi Windows, perciò siete costretti ad usare sleep(1). Naturalmente sovradimensionare il tempo di ritardo non produce errori, solo riduce le prestazioni, similmente ad avere un tempo di ritardo molto più basso del tempo medio di sblocco o del tutto nullo. Inoltre la funzione unlink può non funzionare sui sistemi Windows. Avete capito quindi quale sistema conviene usare con PHP ;)
  2. La ragione della chiamata della funzione clearstatcache è dovuta al fatto che l'interprete PHP, allo scopo di aumentare le prestazioni si tiene in una cache i risultati dell'ultima chiamata a molte funzioni che hanno a che fare col filesystem, tra cui file_exists. Se la prossima chiamata a file_exists viene fatta con lo stesso nome di file come argomento, l'interprete restituisce il valore messo nella cache, senza rifare alcun controllo dello stato attuale del file. Poiché nel nostro caso lo stato del file voti.ser.lock viene controllato molte volte e può cambiare repentinamente (il file stesso può scompare perché un altro processo toglie il lock al file), occorre chiamare la funzione clearstatcache per svuotare la cache prima di fare un'altra chiamata a file_exists, forzando così file_exists ad effettuare un vero e proprio controllo sullo stato del file tramite le funzioni del sistema operativo. Devo anche precisare che ogni singola richiesta usa una cache separata per le funzioni che operano sul filesystem che viene rilasciata al termine della richiesta stessa.

Se siete acuti avrete già notato cosa c'è che non va nel programma precedente. Il problema è rappresentato dal multitasking. Chi vi assicura che subito dopo essere usciti dal ciclo while e prima che l'azione della fopen sia portata a completamento, il sistema operativo non decida di interrompere l'esecuzione e passare ad un altro processo index.php? In questo caso il primo processo, quando se ne riprende l'esecuzione, considera il file voti.ser sbloccato, e questo potrebbe non esserlo, con la conseguenza che si verifica di nuovo una "race condition". Anche se le probabilità sono minori rispetto al caso in cui non c'è alcun controllo sull'accesso contemponeraneo alla risorsa, il baco rimane.

Per risolvere questo problema occorrerebbe assicurare che le due operazioni di verifica dell'esistenza del lock e creazione di un nuovo lock siano "atomiche", vale a dire non interrompibili. Non c'è in PHP alcuna funzione per assicurare questo, mentre in assembly sì. Tuttavia in PHP non avete bisogno di programmarvi un meccanismo di locking. Poiché il file locking è spesso necessario, PHP vi mette a disposizione una funzione sicura e semplice da usare per effettuare questo task che si appoggia a sua volta ad una system call del vostro sistema operativo.

Quindi invece di usare un vostro meccanismo di locking, dovreste usare uno dei meccanismi di locking di sistema, che garantiscono il funzionamento corretto della procedura di locking in tutti i casi e sono accessibili dal PHP tramite la funzione flock, sia su piattaforma Unix che Windows.

La funzione flock (che sta per "file lock") ha la seguente sintassi:

bool flock (int fp, int operation [, int wouldblock])

Per il tipo di locking che ci serve a noi (lock esclusivo, che può essere posseduto solo da un processo per volta), settate operation uguale a 2 oppure a LOCK_EX (se avete PHP >=4.0.1). fp è l'handle del file da lockkare ritornato da fopen. Se passate il valore 3 (o LOCK_UN per PHP>=4.0.1) il lock viene rilasciato. Comunque un lock viene rilasciato all'atto della chiusura del file su cui vige tramite fclose, quindi possiamo risparmiarci di chiamare prima la flock.

Definendo un lock esclusivo (exclusive lock) sul file, quando un altro processo tenta di accedere al file bloccato, chiamando a sua volta la flock, questa chiamata della flock lo pone in attesa finché il file non viene sbloccato dall'altro processo e appena viene sbloccato stabilisce un nuovo lock sul file. Il codice eseguito dalla flock è simile a quello del nostro meccanismo di locking, ma garantisce l'atomicità della sequenza di operazioni di verifica e acquisizione del lock. Oltretutto la flock è anche più semplice da usare: apri il file, poi passi il filehandle a flock. Se un altro processo cerca di usare il file si bloccherà al comando flock() finché il primo processo ha finito col file. Non c'è altro da fare.

$fp=fopen(DFN, 'r+b');
flock($fp, 2);
...
# codice che legge e/o scrive voti.ser, come sopra
...
fclose($fp);

Naturalmente questo meccanismo di locking è consultivo (advisory lock), perché la flock non ha il potere di bloccare l'accesso fisico al file: se un processo non chiama la flock e quindi non attende sulla flock lo sbloccaggio, si può verificare lo stesso la condizione di gara. Assicuratevi perciò che tutti i processi che accedano al file comune usino lock. Nel caso precedente avevamo tutti processi identici copie di un unico codice, quello di index.php e quindi ogni copia per il fatto di essere identica alla prima usa flock.

Il primo grafico

Torniamo all'argomento principale di questo tutorial. Nella prima versione del nostro semplice questionario online, anziché presentare le statistiche viene stampato il messaggio "Grazie per aver votato. Torna alla home". Si tratta adesso solo di sostituire quel messaggio con la stampa del grafico delle statistiche. I dati, compreso l'ultimo voto conteggiato, sono già disponibili nell'array $voti. Naturalmente, essendo in un contesto multitasking, potrebbe accadere che subito dopo aver registrato un voto e prima che venga emesso il grafico, il processo venga sospeso per registrare un altro voto e quando si ritorna al primo utente, questo si ritrova stampata una statistica falsata per mancanza di un voto. Pensavate di esservene liberati eppure troviamo di nuovo qui il problema del locking. La cosa non è qui così grave, poiché i voti vengono comunque registrati correttamente grazie al file locking, ma è così semplice porre rimedio al problema che non ho resistito. Basta ritardare la chiusura del file puntato da $fp a dopo la stampa del grafico, in modo che il lock sul file rimanga anche durante la stampa del grafico stesso. In pratica lettura, aggiornamento del file voti.ser e stampa della statistica sono operazioni che ogni processo index.php deve portare a termine, prima che possa iniziarle un altro processo.

Veniamo ora finalmente alla generazione del grafico con HTML_Graphs. La prima cosa è includere la libreria. Ho messo index.php nella directory web /quest e lì dentro ho creato dei link simbolici con lo stesso nome ad HTML_Graphs.php (che sta in /HTML_Graphs) e alle immagini contenute nella sottodirectory images di /HTML_Graphs che useremo tra poco usando il comando Unix "ls -s nomefile nomelink". In questo modo scrivo semplicemente:

require('HTML_Graphs.php');

Per creare un grafico a barre orizzontali è sufficiente chiamare la funzione html_graph che genera il codice HTML necessario. I parametri che è obbligatorio passare sono 4:

  1. un array contenente le etichette delle barre. Nel nostro caso passiamo semplicemente l'array formato dalle chiavi di $voti, cosa che rende il nostro codice più generico e più semplice. Per ottenere la lista delle chiavi di un array associativo sotto forma di un array scalare basta chiamare la funzione array_keys sull'array associativo.
  2. l'array dei valori di ogni barra ordinati in modo corrispondente alle etichette delle barre indicate nell'array precedente.
  3. l'array dei corrispondenti codici di colore delle barre. Un codice di colore va preceduto da # come in HTML, altrimenti si intende il percorso di un file di immagine che verrà usato come tile per riempire la barra. Con i grafici verticali si possono usare solo i file di immagine.
  4. un array associativo che definisce lo stile dell'intero grafico, documentato con precisione in HTML_Graphs.php. Tutte le chiavi hanno associati dei valori di default. L'unico valore che nel nostro caso è necessario cambiare è il fattore di scala che per default vale 1.

Un problema è rappresentato dal fatto che il nostro codice ordina gli elementi dell'array $voti a seconda di come gli vengono i primi voti. Per far sì che gli elementi dell'array $voti siano nello stesso ordine in cui sono presentati nella form di votazione, basta creare un array $voti in cui gli elementi sono in quest'ordine ed i voti sono tutti zero e scriverlo come valore iniziale nel file voti.ser. La cosa è banale, comunque se avete intenzione di usare lo script sul vostro sito per proporre periodicamente dei sondaggi, conviene farsi uno scriptino facilmente riutilizzabile per inizializzare il file voti.ser. Ho chiamato questo script reset.php. Dovete semplicemente inizializzare l'array $voti (che stavolta è un semplice array) e lanciare lo script da web oppure da riga di comando se avete installato la versione CGI di PHP. Dopodiché eliminate lo script dalla directory web in modo che nessun utente web abbia la possibilità di invocarlo e di resettarvi le statistiche.

<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 3.2//EN">
<html>
<head>
<meta name="generator" content="HTML Tidy, see www.w3.org">
<title>reset del questionario</title>
</head>
<body>

<?
   # se avete installato PHP da linea di comando potete
   # lanciare questo script da linea di comando con:
   #    # php reset.php
   # altrimenti lanciatelo da web

   # Cambia qui il nome del file di dati.
   # Se il file non esiste va creato vuoto in modo
   # che almeno l'utente web (di solito detto
   # apache o nobody) possa leggerlo e scriverlo.
   # Usa i comandi:
   #    # touch nome_file
   #    # chown apache.apache nome_file
   #    # chmod 600 nome_file
   define('DFN', 'voti.ser');

   # definisci qui i voti iniziali
   # nell'ordine desiderato per il grafico
   # attenzione: le chiavi devono corrispondere
   # ai valori degli attributi value nei
   # tag <input type="radio"> di index.php
   $voti = array(
     'Windows 95/98/Me' => 0,
         'Windows NT/2000'      => 0,
         'MacOS'                        => 0,
         'Linux'                        => 0,
         'altro Unix'           => 0
   );

   # serializza voti tutti nulli
   $fp=fopen(DFN, 'wb');
   fwrite($fp, serialize($voti));
   fclose($fp);
?>

<p>Fine dello script. Ricordarsi di rimuoverlo o spostarlo in modo
che non possa essere pi&ugrave; invocato via web.</p>
</body>
</html>

Ecco quindi la prossima versione di index.php che finalmente genera il grafico delle statistiche dopo la votazione:

<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 3.2//EN">
<html>
<head>
<meta name="generator" content="HTML Tidy, see www.w3.org">
<title>questionario</title>
</head>
<body>

<?

if (!empty($voto))
{
   # Cambia qui il nome del file di dati.
   # Questo file va creato usando lo script reset.php
   define('DFN', 'voti.ser');

   # deserializza i dati da un file su disco
   $fp=fopen(DFN, 'r+b');
   flock($fp, 2);
   # se il file può essere più grosso di 4096 bytes, aumentare ;)
   $voti=unserialize(fread($fp, 4096));

   # conteggia questo voto
   $voti[$voto]++;

   # stampa il grafico con HTML_Graphs
   require('HTML_Graphs.php');

   $names = array_keys($voti);
   $values = array_values($voti);
   $largest = max($values);
   # alcuni colori per le barre: blu, verde, fucsia, rosso, ciano
   $bars = array('#0000ff', '#00ff00', '#ff00ff', '#ff0000', '#00ffff');
   # scale = fattore che moltiplicato ai valori fornisce la
   # dimensione della barra. Il valore maggiore $largest avrà
   # una barra lunga 150 pixel settando scale a 150/$largest
   $graph_vals = array('scale'=>150/$largest);

   html_graph($names, $values, $bars, $graph_vals);
   # serializza i dati sullo stesso file
   rewind($fp);
   fwrite($fp, serialize($voti));
   fclose($fp);
}
else
{
?>

<form method="post" action="index.php">
<p>Quale sistema operativo usi di pi&ugrave;?</p>

<table border="0">
<tr>
<td>Windows 95/98/Me</td>
<td><input type="radio" name="voto" value="Windows 95/98/Me"></td>
</tr>

<tr>
<td>Windows NT/2000</td>
<td><input type="radio" name="voto" value="Windows NT/2000"></td>
</tr>

<tr>
<td>MacOS</td>
<td><input type="radio" name="voto" value="MacOS"></td>
</tr>

<tr>
<td>Linux</td>
<td><input type="radio" name="voto" value="Linux"></td>
</tr>

<tr>
<td>altro Unix</td>
<td><input type="radio" name="voto" value="altro Unix"></td>
</tr>
</table>

<p><input type="submit" value="vota"></p>
</form>

<?
}
?>

</body>
</html>

Ecco poi un esempio dell'aspetto che ha il grafico sul mio browser Mozilla:

primo esempio di grafico

Le barre sono state generata da HTML_Graphs come delle celle di tabella contenenti un "nonbreaking space" (&nbsp;), un trucco noto un po' a tutti i webmaster per evitare che alcuni browser non disegnino proprio delle celle di tabella che si vogliono vuote. Possiamo migliorare il grafico, aggiustando vari parametri. Prima di tutto mettiamo più spazio tra le barre ed inseriamo un titolo. Per fare questo basta aggiungere qualche parametro nell'hash $graph_vals:

   $graph_vals = array(
     'scale'        =>  150/$largest,
     'hlabel'       =>  '# di voti',
     #'border'       =>  1,
     'cellpadding'  =>  4
   );

border specifica la dimensione del bordo della tabella più esterna ed è utile in fase di sviluppo per rendervi conto di com'è la struttura tabellare con cui è costruito il grafico. cellpadding imposta lo spazio tra le celle ed il contenuto per questa tabella principale. Il risultato della modifica è mostrato in figura:

Il grafico migliorato

A questo punto provvedere un link per visualizzare il grafico senza registrare alcun voto è piuttosto semplice: passiamo un parametro flag (stat=on) per indicare che vogliamo le statistiche e non vogliamo alcuna votazione. Un paio di if e il gioco è fatto.

Per evitare che un valore zero venga comunque rappresentato con uno spazio, potete usare delle immagini che verrano "stirate" per costituire le barre. Per far questo basta modificare l'array $bars ad es. come segue:

   $bars = array(
     'images/hbar_blue.gif',
     'images/hbar_green.gif',
     'images/hbar_orange.gif',
     'images/hbar_red.gif',
     'images/hbar_aqua.gif'
   );

Quando ci sono le immagini potete trasformare un grafico a barre orizzontali in uno a barre verticali in uno snap. Il grafico orizzontale è del tipo 0 (predefinito), quello verticale del tipo 1, quindi aggiungete questo elemento all'array $graph_vals:

'type' => 1

Download esempio

La versione finale del nostro programma la potete scaricare qui. Come al solito licenza GPL:)

<quest.tar.gz>

E' tutto per questo articolo. Alcuni miglioramenti sarebbero possibili, come ad es. utilizzare il meccanismo dei cookie per limitare votazioni a raffica dallo stesso client. "The rest is up to you".

                .----.
             _.'__    `. 
         .--(#)(##)---/#\
       .' @          /###\
       :         ,   #####
        `-..__.-' _.-\###/  
              `;_:    `"'
            .'"""""`. 
           /,  ANT  ,\
          //  COOL!  \\
          `-._______.-'
          ___`. | .'___ 
         (______|______)

Documento creato con

get amaya!

Torna alla Home Page di Antonio