In questo articolo tecnico presenterò il prototipo funzionante di un mio piccolo framework PHP utile per la creazione di immagini e grafici da inserire nelle proprie applicazioni, web o client che siano.
Tale framework, allo stato attuale, è in grado di eseguire il rendering di grafici a torta(in inglese PieChart) ed istogrammi verticali ed orizzontali.
Per avere un'idea di ciò che questa classe può generare è possibile dare uno sguardo a questi grafici PHP di esempio.
Noterai per prima cosa che sono presenti sia grafici a torte sia istogrammi, poi che i valori espressi negli istogrammi si auto-ridimensionano automaticamente e che i possibili colori delle etichette, delle barre e delle fette di torta sono praticamente infiniti.
Introduzione alla classe Picasso
La suddetta classe, che è il cuore del framework in questione, è in grado di generare immagini in molti formati tra i quali GIF, PNG e JPG/JPEG. Questo è possibile perchè Picasso utilizza i metodi esposti dalle librerie grafiche GDLib.
Effettivamente, la classe incapsula alcuni metodi delle suddette librerie ed esegue una serie di calcoli per normalizzare le dimensioni delle barre, per distribuire i vari oggetti nello spazio, per calcolare l'ampiezza degli angoli delle fette di torta e le dimensioni dell'etichette.
I metodi delle GDLib che vengono incapsulati ed utilizzati all'interno dei membri della classe Picasso sono:
- imagestring(image,font,x,y,text,lineColor): aggiunge del testo orizzontale all'immagine;
- imagestringup(image,font,x,y,text,lineColor): aggiunge del testo verticale all'immagine;
- imagerectangle(image,x,y,X,Y,lineColor): disegna un rettangolo nell'immagine;
- imagefilledrectangle(image,x,y,X,Y,lineColor): disegna un rettangolo colorato nell'immagine;
- imagefilledarc(image,x,y,width,height,start,end,lineColor,IMG_ARC_PIE): disegna una fetta di torta colorata nell'immagine;
Prima di esporre ed analizzare il codice sorgente della classe è necessario spiegare il funzionamento del framework.
Il framework e le classi accessorie
Per funzionare esso ha bisogno, oltre alla classe Picasso, di un'altra classe per rappresentare le barre degli istogrammi, o le fette di pie charts, e di una pagina per gestire i valori da rappresentare nei grafici, istanziare l'oggetto Picasso ed inviare al client le immagini generate.
Grazie a questo framework, per scatenare la creazione delle immagini e dei grafici in una pagina HTML basterà utilizzare un semplice tag IMG come il seguente:
<img src="imageViewer.php?
width=150&height=150&red=255&green=255&blu=255&
type=pie&
items=2&
label1=IndPres&value1=100&r1=0&g1=225&b1=0&
label2=IndNonPres&value2=0&r2=255&g2=0&b2=0" />
I dati in querystring
Che significato hanno i parametri utilizzati in querystring per il trasporto dei dati?
- width ed height indicano le dimensioni, in pixel, dell'immagine da creare;
- red,green e blu sono le percentuali, in formato RGB, per determinarne il colore di sfondo dell'immagine;
- type indica che tipo di grafico si vuole ottenere, in questo caso una torta;
- items indica quanti valori si vogliono rappresentare, in questo caso 2;
- label1,value1,r1,g1,b1 sono le proprietà del primo dato da rappresentare graficamente, e si incrementano per ogni dato successivo;
Il gestore delle richieste: imageViewer.php
Entriamo nel vivo del codice analizzando cosa fa lo script PHP presente in imageViewer.php. Come al solito spiattello il codice sorgente commentando abbondantemente i passaggi chiave nel codice stesso:
<?php
include_once 'CPicasso.php';
include_once 'CBar.php';
//Valori di default per l'immagine
$w = 500;
$h = 300;
$red = 200;
$green = 100;
$blu = 300;
//Ricezione dei parametri in querystring
if($_GET['width'])
$w = $_GET['width'];
if($_GET['height'])
$h = $_GET['height'];
if($_GET['red'])
$red = $_GET['red'];
if($_GET['green'])
$green = $_GET['green'];
if($_GET['blu'])
$blu = $_GET['blu'];
/**
* Creo una istanza della classe Picasso:
* il costruttore della classe riceve in input:
* 1)La larghezza che deve avere l'immagine;
* 2)L'altezza che deve avere l'immagine;
* 3)La percentuale di Rosso che deve avere lo sfondo;
* 4)La percentuale di Verde che deve avere lo sfondo;
* 5)La percentuale di Blue che deve avere lo sfondo;
*/
$img = new Picasso($w,$h,$red,$green,$blu);
//Ricevo il numero di items da rappresentare graficamente
if($_GET['items']){
$items = $_GET['items'];
//Array di Bar che Picasso disegnerà nel grafico
$barArr = array();
for($i=0;$i<$items; $i++){
$param = $i+1;
$lbl = "label$param";
$value = "value$param";
$r = "r$param";
$g = "g$param";
$b = "b$param";
/**
* Istanzio un oggetto Bar per ogni items ricevuto in querystring
* il costruttore della classe riceve in input:
* 1)L'etichetta che deve indicare l'elemento;
* 2)Il valore che deve esprimere l'elemento;
* 3)La percentuale di Rosso che deve avere lo sfondo;
* 4)La percentuale di Verde che deve avere lo sfondo;
* 5)La percentuale di Blue che deve avere lo sfondo;
*/
$barArr[] =
new Bar($_GET[$lbl],$_GET[$value],$_GET[$r],$_GET[$g],$_GET[$b]);
}
/**
* Gestione della tipologia di grafico da creare:
* type = "hbars" -> istogramma con barre orizzontali.
*/
if($_GET['type'] && $_GET['type'] == 'hbars')
$img->addHorizontalBars($barArr);
//type = "vbars" -> istogramma con barre verticali
else if($_GET['type'] && $_GET['type'] == 'vbars')
$img->addVerticalBars($barArr);
//type = "pie" -> PieChart
else if($_GET['type'] && $_GET['type'] == 'pie')
$img->addPieChart($barArr);
}
/**
* Setto il content-type per quello che voglio
* inviare al browser cliente: in questo caso
* si tratta di un'immagine PNG.
*/
header('Content-Type: image/png');
//Invio l'immagine al client
imagepng($img->getImage());
//Distruggo l'immagine
unset($img);
?>
Riassumendo: imageViewer.php crea un array di istanze della classe Bar che l'oggetto Picasso successivamente disegnerà nel grafico scelto.
Inoltre, analizzando il codice dello script PHP presente in imageViewer.php, si è potuto vedere con che facilità è possibile scegliere quali tipologie di grafici far disegnare dalla classe Picasso, in particolare:
- Istogrammi con barre verticali;
- Istogrammi con barre orizzontali;
- PieChart, ovvero grafici a Torta;
In realtà sarà dimostrato, successivamente analizzandone il codice, che la classe Picasso espone altri metodi per disegnare rettangoli colorati e con bordo, testo e archi di circonferenze.
Passiamo ora all'analisi della classe Bar, ovvero il componente di model che rappresenta una barra, una fetta di torta oppure un semplice rettangolo:
La classe Bar, ovvero un item del grafico
<?php
/**
* Rappresenta un oggetto, o item, rappresentabile
* all'interno di qualunque tipologia di grafico
* generato dal componente Picasso.
*/
class Bar{
//Proprietà Private
private $label = 'label';
private $value = 0;
private $r = 0;
private $g = 0;
private $b = 0;
/**
* Costruttore:
* Riceve: etichetta,valore,%rosso,%verde,%blue
*/
public function __construct($lbl,$value,$r,$g,$b){
$this->value = $value;
$this->label = $lbl;
$this->r = $r;
$this->g = $g;
$this->b = $b;
}
/**
* Metodi Setter & Getter Pubblici
*/
public function getLabel(){
return $this->label;
}
public function getValue(){
return $this->value;
}
public function getRed(){
return $this->r;
}
public function getGreen(){
return $this->g;
}
public function getBlu(){
return $this->b;
}
}
?>
Il componente è molto semplice e non ha bisogno di alcun commento supplementare a ciò che è già presente nel codice sorgente, in ambito Java definirei questa classe un semplice bean.
Ciò che è importante è che Bar rappresenta la tipologia utilizzata da Picasso per effettuare il rendering grafico di barre e fette di torta all'interno dei propri grafici.
Ora, manca solo l'esposizione e l'analisi del codice sorgente base del nocciolo che
caratterizza il mio framework: ovvero la classe Picasso.
<?php
class Picasso{
/** Dimensione del Margine tra ogni Barra */
const BARS_MARGIN = 4;
/** Margine interno dell'immagine */
const IMAGE_MARGIN = 4;
/** Dimensione del font di default */
const DEFAULT_FONT_SIZE = 6;
/**
* Coefficiente di default per ricavare
* il colore del testo in base a quello del
* proprio contenitore
*/
const DEFAULT_FONT_COLOR_COEFF = 4;
/** Immagine Padre di tutti i grafici */
private $image = null;
/** Larghezza dell'immagine Padre */
private $width = 0;
/** Altezza dell'immagine Padre */
private $height = 0;
/** Colore di sfondo dell'immagine Padre */
private $bgColor = '';
/**
* Costruttore:
* Riceve: larghezza,altezza,%rosso,%verde,%blue
*/
function __construct($w,$h,$red,$green,$blu){
$this->width = $w;
$this->height = $h;
$this->image = imagecreate($w,$h);
$this->bgColor = $this->setColor($red,$green,$blu);
}
/** Aggiunge del testo alle coordinate x,y */
public function addText($font,$x,$y,$text
,$linered,$linegreen,$lineblu){
$lineColor = $this->setColor($linered,$linegreen,$lineblu);
imagestring($this->image,$font,$x,$y,$text,$lineColor);
}
/** Aggiunge del testo verticale alle coordinate x,y */
public function addVerticalText($font,$x,$y,$text
,$linered,$linegreen,$lineblu){
$lineColor = $this->setColor($linered,$linegreen,$lineblu);
imagestringup($this->image,$font,$x,$y,$text,$lineColor);
}
/**
* Crea un rettangolo colorato con l'angolo
* alto-sinistro alle coordinate x,y
* e con l'angolo basso-destro alle coordinate X,Y
*/
public function addFilledRectangle($x,$y,$X,$Y
,$linered,$linegreen,$lineblu){
$lineColor = $this->setColor($linered,$linegreen,$lineblu);
imagefilledrectangle($this->image,$x,$y,$X,$Y,$lineColor);
}
/**
* Crea i bordi di un rettangolo con l'angolo
* alto-sinistro alle coordinate x,y
* e con l'angolo basso-destro alle coordinate X,Y
*/
public function addEmptyRectangle($x,$y,$X,$Y
,$linered,$linegreen,$lineblu){
$lineColor = $this->setColor($linered,$linegreen,$lineblu);
imagerectangle($this->image,$x,$y,$X,$Y,$lineColor);
}
/**
* Crea una fetta di Torta colorata
* con centro alle coordinate x,y
* con raggio di lunghezza width,
* con un angolo rappresentato da start e end
*/
public function addFilledPieceOfPie($x,$y,$width,$height
,$start,$end,$red,$green,$blu){
$lineColor = $this->setColor($red,$green,$blu);
imagefilledarc ($this->image,$x,$y,$width,$height
,$start,$end,$lineColor, IMG_ARC_PIE);
}
/**
* Crea una grafico a barre orizzontali:
* riceve un array di barre con cui
* riempire il grafico (class CBar)
*/
public function addHorizontalBars($barArray){
/**
* Coefficiente per la rappresentazione delle barre:
* Se le barre sono piu' lunghe della dimensione dell'immagine
* $scala viene incrementato fino a che tutte le barre possano
* essere disegnate nell'immagine.
*/
$scala = $this->normalizeBarsLength($barArray,$this->width);
$nBar = count($barArray);
//Altezza delle barre
$h = $this->calculateBarsDimension($this->height,$nBar);
$tempHeight = Picasso::BARS_MARGIN;
$newY = $h + $tempHeight;
foreach($barArray as $bar){
try{
$this->addFilledRectangle(Picasso::IMAGE_MARGIN
,$tempHeight,($bar->getValue()/$scala),$newY
,$bar->getRed(),$bar->getGreen(),$bar->getBlu());
$fontSize = $this->calculateHorizontalFontSize($h);
$this->addText($fontSize
,(Picasso::IMAGE_MARGIN*2)
, $this->centerHorizontalText($fontSize,$h,$newY)
,$bar->getLabel() . ": " . $bar->getValue()
,$bar->getRed()/Picasso::DEFAULT_FONT_COLOR_COEFF
,$bar->getGreen()/Picasso::DEFAULT_FONT_COLOR_COEFF
,$bar->getBlu()/Picasso::DEFAULT_FONT_COLOR_COEFF);
$tempHeight = $tempHeight + Picasso::BARS_MARGIN + $h;
$newY = $tempHeight + $h;
}
catch (Exception $e){
die('<br />Eccezzione: '
. $e->getMessage() . '<br />');
}
}
}
/**
* Crea una grafico a barre verticali:
* riceve un array di barre con cui riempire
* il grafico (class CBar).
*/
public function addVerticalBars($barArray){
/**
* Coefficiente per la rappresentazione delle barre:
* Se le barre sono piu' lunghe della dimensione dell'immagine
* $scala viene incrementato fino a che tutte le barre possano
* essere disegnate nell'immagine.
*/
$scala = $this->normalizeBarsLength($barArray,$this->height);
$nBar = count($barArray);
//larghezza delle barre
$h = $this->calculateBarsDimension($this->width,$nBar);
$tempWidth = Picasso::BARS_MARGIN;
$newX = $h + $tempWidth;
foreach($barArray as $bar){
try{
$this->addFilledRectangle($tempWidth
,($this->height - Picasso::IMAGE_MARGIN
- $bar->getValue()/$scala)
,$newX
,($this->height - Picasso::IMAGE_MARGIN)
,$bar->getRed(),$bar->getGreen(),$bar->getBlu());
$text = $bar->getLabel()."(".$bar->getValue().")";
$fontSize = $this->calculateVBarFontSize($h,$text);
if($fontSize < 1){
/**
* Dimensione dello Spazio che rimane vuoto
* tra l'immagine e la barra
*/
$emptySpace = $this->height - Picasso::IMAGE_MARGIN
- $bar->getValue()/$scala;
/**
* Ricalcolo la dimensione del font
* in base all'altezza della barra
*/
$fontSize = $this->calculateVBarFontSize(
$this->height - $emptySpace,$text);
$this->addVerticalText($fontSize
,$this->centerVerticalVBarText
($fontSize,$h,$tempWidth,$text)
,($this->height - Picasso::IMAGE_MARGIN
- imagefontwidth($fontSize))
,$text
,$bar->getRed()/Picasso::DEFAULT_FONT_COLOR_COEFF
,$bar->getGreen()/Picasso::DEFAULT_FONT_COLOR_COEFF
,$bar->getBlu()/Picasso::DEFAULT_FONT_COLOR_COEFF);
}
else{
$this->addText($fontSize
,$this->centerVBarText($fontSize,$h,$tempWidth,$text)
,($this->height - Picasso::IMAGE_MARGIN
- imagefontheight($fontSize))
,$text
,$bar->getRed()/Picasso::DEFAULT_FONT_COLOR_COEFF
,$bar->getGreen()/Picasso::DEFAULT_FONT_COLOR_COEFF
,$bar->getBlu()/Picasso::DEFAULT_FONT_COLOR_COEFF);
}
$tempWidth = $tempWidth + Picasso::BARS_MARGIN + $h;
$newX = $tempWidth + $h;
}
catch (Exception $e){
die('<br />Eccezzione: '
. $e->getMessage() . '<br />');
}
}
}
function addPieChart($barArray){
$coeff = $this->getPieCoeff($barArray);
$raggio = (imagesx($this->image) <= imagesy($this->image))
? imagesx($this->image) : imagesy($this->image);
$raggio = $raggio - Picasso::IMAGE_MARGIN*2;
$x = imagesx($this->image)/2;
$y = imagesy($this->image)/2;
$start = 0;
$end = 0;
foreach($barArray as $bar){
$end += $coeff * $bar->getValue();
$this->addFilledPieceOfPie($x,$y,$raggio,$raggio
, $start, $end, $bar->getRed()
, $bar->getGreen(),$bar->getBlu());
$start = $end;
}
}
/** Restituisce l'immagine Padre. */
public function getImage(){
return $this->image;
}
/**
* Calcola lo spessore di ogni barra dividendo
* l'altezza o la larghezza dell'immagine
* per il numero di barre considerando uno spazio
* di 4px per distanziarle.
*
* Spessore: imgHeight = 4 + nBar(x + 4);
* ==> x = (imgHeight - 4 - 4*nBar)/nBar;
*/
private function calculateBarsDimension($dimensionSide,$barsNumber){
return ($dimensionSide - Picasso::BARS_MARGIN -
(Picasso::BARS_MARGIN*$barsNumber))/$barsNumber;
}
/**
* Restituisce un Colore da usare per l'immagine
* o un oggetto creato nell'immagine
*/
private function setColor($red,$green,$blu){
return imagecolorallocate($this->image,$red,$green,$blu);
}
/** Calcola il punto per centrare il testo nell'immagine */
private function centerHorizontalText($font,$dimension,$position){
$fHeight = imagefontheight($font);
return $position - ($dimension/2 + $fHeight/2);
}
/**
* Calcola il punto per centrare un testo orizzontale
* in una Vertical Bar.
*/
private function centerVBarText($font,$dimension,$position,$txt){
$fWidth = imagefontwidth($font);
return $position + ($dimension/2 - ($fWidth * strlen($txt))/2);
}
/**
* Calcola il punto per centrare un testo verticale
* in una Vertical Bar
*/
private function centerVerticalVBarText
($font,$dimension,$position,$txt){
$fWidth = imagefontheight($font);
return $position +
($dimension/2 - ($fWidth * strlen($txt[1]))/2);
}
/**
* Calcola il font da utilizzare in base alla dimensione
* del lato da considerare nell'immagine
*/
private function calculateHorizontalFontSize($dimension){
$font = Picasso::DEFAULT_FONT_SIZE;
while(imagefontheight($font) >
($dimension - Picasso::BARS_MARGIN)
&& $font >0){
$font--;
}
return $font;
}
/**
* Calcola la dimensione del font da utilizzare
* nelle barre verticali in base alla dimensione
* del lato da considerare nell'immagine e alla
* lunghezza del testo.
*/
private function calculateVBarFontSize($dimension,$txt){
$font = Picasso::DEFAULT_FONT_SIZE;
while((imagefontwidth($font)*strlen($txt) >
$dimension - Picasso::BARS_MARGIN)
&& $font >0){
$font--;
}
return $font;
}
/**
* Trova il coefficiente con cui ridurre in scala
* la lunghezza delle barre nei grafici
* a barre orizzontali e verticali.
*/
private function normalizeBarsLength($arr,$dimension){
$coeff = 1;
//Rapresenta il valore piu' grande da rappresentare
$maxLength = 0;
$coeffIng = 1;
foreach($arr as $bar){
$temp = $bar->getValue();
$maxLength = ($temp > $maxLength)
? $temp : $maxLength;
while( ($temp/$coeff) >=
($dimension - Picasso::IMAGE_MARGIN)){
$coeff++;
}
}
//Se $coeff == 1 allora provo ad ingrandire la scala
if($coeff == 1){
$temp = $coeffIng + 1;
while( ($maxLength*$temp) <
($dimension - Picasso::IMAGE_MARGIN)){
$coeffIng++;
$temp = $coeffIng + 1;
}
$coeff = 1/$coeffIng;
}
return $coeff;
}
/**
* restituisce il coefficiente per trasformare il valore
* di ogni oggetto Bar in una fetta di torta
*/
function getPieCoeff($arr){
$tot = 0;
foreach($arr as $bar){
$tot += $bar->getValue();
}
return 360/$tot;
}
}
?>
La classe è un po' complessa, sopratutto nelle fasi di calcolo per determinare le dimensioni e le distanze degli oggetti, per il resto il modo in cui vengono create le immagini e come sono posizionate nell'immagine padre risulta essere molto semplice e trasparente.
Come premesso, questo framework è un prototipo funzionante dal quale poter facilmente creare grafici sempre più complessi e raffinati. Direi però che è una buona base da cui iniziare.
Alla prossima,
MA.