HotPDF non dispone di un oggetto grafico. Non esiste un TPDFChart, né un AddBarSeries, nulla che prenda un array di numeri e restituisca un grafico renderizzato. Ciò che offre invece è un canvas di pagina con lo stesso vocabolario a basso livello utilizzato da ogni modello di disegno PDF: rettangoli, linee, cerchi, riempimenti, tratti e testo posizionati a coordinate esatte. Un grafico in un documento HotPDF è quindi qualcosa che si costruisce, non che si richiede. Può sembrare più faticoso di quanto non sia in realtà. Una volta scritta la matematica delle coordinate, un grafico a barre diventa un semplice ciclo sui rettangoli, un grafico a linee una polilinea e un grafico a torta un ventaglio di archi, lasciandovi il controllo su ogni singolo pixel del risultato.
Questo è importante perché l'alternativa a cui spesso si ricorre inizialmente, ovvero rasterizzare un controllo grafico a schermo in una bitmap e incollare l'immagine nella pagina, fornisce un grafico bloccato alla risoluzione dello schermo che risulterà sfuocato in stampa e aumenterà le dimensioni del file. Disegnare il grafico con le primitive vettoriali di HotPDF mantiene l'output nitido a qualsiasi livello di zoom e per qualsiasi DPI di stampa, in quanto le barre e gli assi sono veri operatori di percorso PDF e non pixel. Il prezzo da pagare è che dovrete gestire direttamente il layout. I meccanismi operativi si riducono a pochi passaggi: l'inversione delle coordinate che spesso trae in inganno, un grafico a barre completo, il trucco della polilinea per i grafici a linee e la matematica degli archi per gli spicchi della torta.
L'unica parte difficile è il sistema di coordinate
La grafica a schermo posiziona l'origine in alto a sinistra con la Y che cresce verso il basso. Il formato PDF fa il contrario. L'origine si trova nell'angolo in basso a sinistra della pagina e la Y cresce verso l'alto, misurata in punti (1/72 di pollice). Ogni chiamata di disegno in HotPDF, come TextOut, Rectangle, MoveTo, LineTo e Circle, utilizza questa convenzione con origine in basso a sinistra e Y verso l'alto. Se si applicano i principi della grafica a schermo, il primo grafico risulterà capovolto e finirà oltre il bordo inferiore della pagina.
Quindi, il vero lavoro in qualsiasi grafico consiste in una mappatura: trasformare un valore di dati in una coordinata Y che rispetti la crescita verso l'alto. Definite un rettangolo di tracciamento, ovvero quattro numeri per l'area in cui risiede il grafico, quindi mappate il valore più piccolo dei vostri dati sul bordo inferiore e quello più grande sul bordo superiore. Per una barra con valore V su una scala da 0 a MaxValue, il bordo superiore della barra sarà PlotBottom + (V / MaxValue) * PlotHeight, e la barra crescerà verso l'alto a partire da PlotBottom. Una volta impostata correttamente questa singola espressione, tutto il resto è solo gestione dei dettagli. La struttura di supporto indicata di seguito memorizza la geometria del tracciato ed effettua la conversione, evitando che il codice di disegno debba ripetere i calcoli aritmetici:
type
TPlotArea = record
Left, Bottom, Width, Height: Single; // PDF points, bottom-left origin
MaxValue: Single; // top of the value scale
end;
// Map a data value to its Y coordinate inside the plot, Y growing upward.
function ValueToY(const Plot: TPlotArea; V: Single): Single;
begin
Result := Plot.Bottom + (V / Plot.MaxValue) * Plot.Height;
end;
Una decisione di progettazione riguarda la variabile MaxValue. Se la impostate esattamente sul valore massimo dei dati, la barra più alta toccherà il bordo superiore dell'area di tracciamento, sembrando tagliata. È consigliabile arrotondarla a una cifra tonda superiore al massimo, ad esempio al successivo multiplo di 10 o 100, in modo che il grafico abbia un margine superiore e le etichette delle griglie mostrino cifre tonde anziché lo specifico valore di picco raggiunto dai dati.
Un grafico a barre è un ciclo sui rettangoli
Una volta definita la mappatura, la creazione di un grafico a barre è immediata. Dividete la larghezza del tracciato in uno spazio per ciascuna categoria, lasciate un intervallo tra le barre in modo che non si tocchino e disegnate ogni barra come un rettangolo riempito la cui altezza è calcolata tramite ValueToY. La funzione Rectangle di HotPDF accetta l'angolo inferiore sinistro più larghezza e altezza, allineandosi perfettamente con una barra che cresce verso l'alto a partire dalla linea di base. Impostate prima il colore di riempimento, create il percorso e infine chiamate Fill per colorarlo. L'etichetta della categoria viene posizionata sotto la linea di base, mentre il valore sopra la barra:
procedure DrawBarChart(Page: THPDFPage; const Plot: TPlotArea;
const Values: array of Single; const Labels: array of string);
var
I, Count: Integer;
SlotW, BarW, BarX, BarH, Gap: Single;
begin
Count := Length(Values);
SlotW := Plot.Width / Count;
Gap := SlotW * 0.25; // quarter-slot gap on each side
BarW := SlotW - Gap;
// Baseline (the X axis) along the bottom of the plot.
Page.SetLineWidth(1.0);
Page.MoveTo(Plot.Left, Plot.Bottom);
Page.LineTo(Plot.Left + Plot.Width, Plot.Bottom);
Page.Stroke;
Page.SetFont('Arial', [], 9);
for I := 0 to Count - 1 do
begin
BarX := Plot.Left + I * SlotW + Gap / 2;
BarH := ValueToY(Plot, Values[I]) - Plot.Bottom;
Page.SetRGBFillColor(RGB(56, 110, 219));
Page.Rectangle(BarX, Plot.Bottom, BarW, BarH); // X, Y, Width, Height
Page.Fill;
// Category label below the baseline, value above the bar.
Page.SetRGBFillColor(clBlack);
Page.TextOut(BarX, Plot.Bottom - 14, 0, Labels[I]);
Page.TextOut(BarX, Plot.Bottom + BarH + 4, 0,
FormatFloat('0', Values[I]));
end;
end;
Due accorgimenti fanno la differenza. L'intervallo Gap è una frazione dello spazio a disposizione anziché un valore fisso in punti, permettendo alle barre di mantenere una spaziatura proporzionale sia che si traccino quattro categorie o quaranta. Inoltre, l'etichetta del valore è posizionata usando la stessa altezza derivata da ValueToY usata per la barra, trovandosi così sempre subito sopra la barra stessa invece di fluttuare a una distanza approssimativa. Se desiderate griglie orizzontali dietro le barre, disegnatele prima del ciclo: scegliete tre o quattro valori arrotondati, applicate ValueToY su ciascuno e tracciate una linea sottile lungo il tracciato a quella coordinata Y. Disegnandole per prime, le linee si troveranno dietro le barre secondo il modello di sovrapposizione utilizzato dal motore PDF.
Assi, tacche ed etichette sono solo altre linee e testo
Il grafico non può dirsi completo finché il lettore non è in grado di comprendere il significato delle barre, ed è qui che entrano in gioco gli assi. L'asse verticale è una semplice linea tracciata lungo il bordo sinistro del tracciato, con alcune tacche e i rispettivi valori. Riutilizzate ValueToY affinché le tacche corrispondano alla stessa scala usata per le barre; in caso contrario, la corrispondenza tra barre e griglia salterà e il grafico riporterà informazioni errate senza segnalarlo:
procedure DrawValueAxis(Page: THPDFPage; const Plot: TPlotArea;
TickCount: Integer);
var
I: Integer;
TickV, TickY: Single;
begin
Page.SetLineWidth(1.0);
Page.MoveTo(Plot.Left, Plot.Bottom);
Page.LineTo(Plot.Left, Plot.Bottom + Plot.Height);
Page.Stroke;
Page.SetFont('Arial', [], 8);
for I := 0 to TickCount do
begin
TickV := (Plot.MaxValue / TickCount) * I;
TickY := ValueToY(Plot, TickV);
Page.MoveTo(Plot.Left - 4, TickY); // short tick outside the axis
Page.LineTo(Plot.Left, TickY);
Page.Stroke;
Page.TextOut(Plot.Left - 30, TickY - 3, 0, FormatFloat('0', TickV));
end;
end;
Le etichette sono l'elemento in cui i grafici falliscono più spesso in produzione, e l'errore è sempre lo stesso: il testo che appariva corretto a schermo supera lo spazio disponibile nel PDF. I nomi lunghi delle categorie si sovrappongono e i nomi dei mesi localizzati come "septembre" o "Dezember" risultano più larghi della versione inglese "Sep" usata nei test. In questo caso non c'è una funzione di ridimensionamento automatico, quindi lasciate un margine adeguato sotto la linea di base, riducete il carattere di un punto o due per insiemi di categorie densi e, se i nomi sono molto lunghi, ruotateli. La funzione TextOut accetta un angolo come terzo parametro: passando 90, l'etichetta verrà ruotata in verticale, recuperando spazio ed evitando sovrapposizioni. Prima di avviare l'esportazione definitiva, verificate il layout con l'etichetta più lunga prevista, non con la più corta.
Grafici a linee: una polilinea attraverso i punti mappati
Un grafico a linee riutilizza l'intera mappatura dei valori e cambia solo il modo in cui i punti sono collegati. Invece di avere un rettangolo per ciascuna categoria, si scorrono i dati una volta, si converte ogni valore nelle rispettive coordinate (X, Y) con ValueToY e si uniscono i punti tramite una singola chiamata a MoveTo seguita da chiamate a LineTo, per poi tracciare la linea alla fine. Il primo punto apre il percorso, mentre ogni punto successivo lo estende:
procedure DrawLineChart(Page: THPDFPage; const Plot: TPlotArea;
const Values: array of Single);
var
I, Count: Integer;
StepX, X, Y: Single;
begin
Count := Length(Values);
if Count < 2 then Exit;
StepX := Plot.Width / (Count - 1);
Page.SetLineWidth(1.5);
Page.SetRGBStrokeColor(RGB(214, 92, 36));
for I := 0 to Count - 1 do
begin
X := Plot.Left + I * StepX;
Y := ValueToY(Plot, Values[I]);
if I = 0 then
Page.MoveTo(X, Y) // open the path at the first point
else
Page.LineTo(X, Y); // extend it through every later point
end;
Page.Stroke; // one stroke paints the whole polyline
end;
Notate la differenza nella spaziatura. Un grafico a barre divide la larghezza per il numero di barre, poiché ogni barra ha una propria colonna. Un grafico a linee divide per il numero di intervalli, ossia Count - 1, poiché il primo e l'ultimo punto si trovano sui bordi del tracciato e la linea attraversa gli spazi tra di essi. Confondere questi due approcci è il motivo più comune per cui un grafico a linee si sposta di mezza colonna rispetto al grafico a barre su cui dovrebbe sovrapporsi. Se volete inserire un indicatore per ciascun punto dati, disegnate un piccolo Circle ed eseguite un Fill su ogni coordinata (X, Y) dopo aver tracciato la polilinea.
Grafici a torta: archi, o spicchi per mantenere le cose semplici
Gli spicchi della torta sono l'unica forma che richiede l'uso della trigonometria, poiché uno spicchio è delimitato da due raggi e da un arco. Il metodo più corretto disegna l'arco tracciando piccoli segmenti di linea lungo la circonferenza, approssimando la curva in modo tale che sia impossibile notare imperfezioni visive. L'angolo di apertura di ciascuno spicchio è la sua quota rispetto al totale, ossia (Value / Total) * 2π, accumulando l'angolo corrente man mano che si procede lungo il cerchio:
procedure DrawPieChart(Page: THPDFPage; CX, CY, Radius: Single;
const Values: array of Single; const Colors: array of TColor);
var
I, Step, Steps: Integer;
Total, Start, Sweep, A: Single;
begin
Total := 0;
for I := 0 to High(Values) do Total := Total + Values[I];
Start := 0;
for I := 0 to High(Values) do
begin
Sweep := (Values[I] / Total) * 2 * Pi;
Steps := Round(Sweep / (Pi / 90)) + 1; // ~2 degrees per segment
Page.SetRGBFillColor(Colors[I]);
Page.MoveTo(CX, CY); // wedge apex at the center
for Step := 0 to Steps do
begin
A := Start + Sweep * (Step / Steps);
Page.LineTo(CX + Radius * Cos(A), CY + Radius * Sin(A));
end;
Page.LineTo(CX, CY); // close back to the center
Page.Fill;
Start := Start + Sweep; // advance to the next slice
end;
end;
Il percorso che parte dal centro, prosegue con l'apice, l'arco e torna all'apice, crea uno spicchio chiuso che Fill colora in modo uniforme. Il numero di segmenti rappresenta un compromesso tra fluidità e dimensione del tracciato: circa due gradi per ogni passo garantiscono una forma rotonda per qualsiasi raggio ragionevole senza appesantire il flusso di contenuti del file. Se non necessitate di un cerchio perfetto, potete evitare del tutto la trigonometria e visualizzare gli stessi dati sotto forma di una singola barra orizzontale in cui la larghezza di ogni segmento è proporzionale alla sua quota. Questa soluzione è spesso più leggibile rispetto a un grafico a torta, e si ottiene semplicemente disponendo i rettangoli uno di seguito all'altro. Ricorrete alla versione con l'arco solo quando il design richiede espressamente un cerchio.
Nulla di tutto questo richiede l'installazione di librerie grafiche esterne, il che costituisce il vantaggio silenzioso del disegno diretto delle primitive. Le stesse chiamate di disegno sul canvas utilizzate per posizionare un logo o una firma permettono di costruire questi grafici, e la stessa chiamata TextOut che etichetta un campo di un modulo viene impiegata per definire gli assi. Memorizzate la geometria del tracciato in una struttura dati, mappate i valori sulla coordinata Y ed avrete un grafico a barre, a linee o a torta tramite semplici routine basate su Rectangle, LineTo e Circle da inserire in qualsiasi rapporto. Le chiamate a Rectangle, MoveTo, LineTo, Circle, Fill, Stroke e TextOut qui descritte fanno parte di HotPDF Component per Delphi e C++Builder.