Un grafico copiato da un controllo grafico a schermo come bitmap a 96 DPI sembra a posto al 100% di zoom in un visualizzatore PDF. Zoom al 400%, o mandate la pagina a una stampante laser a 600 DPI, e i bordi delle barre diventano morbidi, le etichette degli assi sfocano in sbavature grigie, e il file porta qualche centinaio di kilobyte di pixel per quella che in realtà è una dozzina di rettangoli. Disegnato invece come percorsi vettoriali, lo stesso grafico resta nitido a ogni livello di zoom e risoluzione di stampa, perché barre, assi ed etichette sono operatori di percorso e testo PDF che il visualizzatore ri-renderizza alla scala che gli serve
L'output vettoriale sposta il lavoro di layout nel vostro codice. Qualcosa deve calcolare dove atterra ogni barra, tacca ed etichetta, e per grafici a barre, linee e torta quel calcolo è una mappatura di coordinate più qualche ciclo su rettangoli, segmenti di linea e archi. Azzeccata la mappatura una volta, i cicli sono corti
DrawChart o primitive disegnate a mano in HotPDF?
HotPDF include un renderer di grafici già fatto, e per un grafico da report standard è la prima cosa da provare. Costruite un THPDFChart, aggiungete serie con AddSeries (tipi hckLine, hckBar, hckArea, hckPie e hckScatter), riempitele con AddPoint, e chiamate THPDFPage.DrawChart con un rettangolo di destinazione. Il renderer ricava da solo intervallo degli assi, griglia, etichette delle tacche, legenda e colori del tema, e l'overload che prende un structure parent scrive il grafico come figura taggata con testo alternativo costruito dai dati, cosa che conta se il documento deve passare un controllo di accessibilità
var
Chart: THPDFChart;
Sales: THPDFChartSeries;
begin
Chart := THPDFChart.Create;
try
Chart.Title := 'Quarterly sales';
Chart.LegendPosition := hclBottom;
Sales := Chart.AddSeries(hckBar, 'Sales');
Sales.AddPoint(1, 420, 'Q1');
Sales.AddPoint(2, 515, 'Q2');
Sales.AddPoint(3, 388, 'Q3');
Sales.AddPoint(4, 610, 'Q4');
// Rettangolo in unità di pagina: left, top, width, height
HotPDF1.CurrentPage.DrawChart(Chart, 60, 120, 470, 280);
finally
Chart.Free;
end;
end;
Il renderer però ha un vocabolario fisso. Diverse serie a barre stanno fianco a fianco in ogni categoria, un grafico a torta deve essere l'unica serie del suo grafico, e l'aspetto viene da tre temi più i colori per serie. Barre impilate, una banda obiettivo sfumata dietro le barre, un colore di marca diverso per ogni barra di una stessa serie, una torta che parte dalle dodici e gira in senso orario: appena un design chiede una di queste cose, disegnare il grafico da soli con il canvas di pagina è meno lavoro che piegare il renderer. Quelle primitive sono Rectangle, MoveTo, LineTo, Pie, Fill, Stroke e TextOut, le stesse chiamate trattate nella procedura guidata del disegno su canvas
Perché il mio grafico a barre esce sottosopra?
Le coordinate di pagina HotPDF mettono l'origine nell'angolo in alto a sinistra con la Y che cresce verso il basso, lo stesso sistema di riferimento di un canvas VCL. Lo spazio utente PDF in sé corre dall'altra parte (origine in basso a sinistra, Y verso l'alto, ISO 32000-1 §8.3.2.3), e HotPDF capovolge ogni coordinata in quello spazio mentre scrive il content stream. I valori di un grafico, d'altra parte, crescono verso l'alto. Passate un valore scalato dritto come Y e la barra più alta pende dalla cima del tracciato come un ghiacciolo
La correzione è un'espressione. Scegliete un rettangolo di tracciato, mappate lo zero sul suo bordo inferiore e la cima della scala sul suo bordo superiore, e fate passare ogni valore per quella mappatura prima che tocchi la pagina. Per un valore V su una scala da 0 a MaxValue, la Y di pagina è Plot.Bottom - (V / Plot.MaxValue) * Plot.Height, dove Plot.Bottom è la baseline. Il codice del grafico a barre più avanti avvolge quella formula in un piccolo helper ValueToY e ogni altra routine lo riusa
In quella formula si nascondono due scelte. MaxValue dovrebbe essere un numero tondo sopra il punto dati più grande, il successivo multiplo di 10 o 100, così la barra più alta non tocca il bordo superiore e le etichette delle linee di griglia leggono 0, 200, 400 anziché 0, 203.5, 407. E la scala parte da zero apposta. La lunghezza della barra è il valore, quindi un asse dei valori che parte da 350 fa sembrare una barra da 388 a malapena più della metà di una da 420 quando il divario vero è sotto l'8%. Il renderer integrato tira lo zero nell'intervallo per le serie a barre e area esattamente per questo motivo. Tenete la stessa regola nel codice disegnato a mano
Le unità sono l'altra metà della mappatura. Con il Resolution predefinito di 72, un'unità di pagina è un punto PDF (1/72 di pollice). Cambiate la risoluzione e coordinate, larghezze di linea e misure del testo passano tutti alla nuova unità, cosa che l'articolo su Resolution e unità di disegno tratta nel dettaglio. DrawChart prende il proprio rettangolo di destinazione in unità di pagina ma impagina margini, dimensioni dei font e larghezze di linea in punti a qualunque risoluzione
Un grafico a barre è un ciclo sui rettangoli
Con la mappatura al posto, un grafico a barre è un ciclo. Dividete la larghezza del tracciato in uno slot per categoria, lasciate un varco così le barre vicine non si toccano, e disegnate ogni barra come rettangolo riempito dalla sua cima ValueToY fino alla baseline. Rectangle prende l'angolo in alto a sinistra, una larghezza e un'altezza, il che combacia direttamente con questa mappatura:
type
TPlotArea = record
Left, Bottom, Width, Height: Single; // unità di pagina; Bottom è la Y della baseline
MaxValue: Single; // cima della scala dei valori
end;
// Un vostro helper: un valore più grande sta più in alto, il che su una
// pagina con Y verso il basso significa una Y più piccola
function ValueToY(const Plot: TPlotArea; V: Single): Single;
begin
Result := Plot.Bottom - (V / Plot.MaxValue) * Plot.Height;
end;
procedure DrawBarChart(Page: THPDFPage; const Plot: TPlotArea;
const Values: array of Single; const Labels: array of string;
const BarColors: array of TColor);
var
I, Count: Integer;
SlotW, BarW, BarX, BarTop, Gap, GridV, GridY: Single;
begin
Count := Length(Values);
SlotW := Plot.Width / Count;
Gap := SlotW * 0.25; // un quarto dello slot, diviso a sinistra e a destra
BarW := SlotW - Gap;
// Prima le linee di griglia tratteggiate, così le barre dipingono sopra
Page.SetLineWidth(0.5);
Page.SetRGBStrokeColor(RGB(200, 200, 200));
Page.SetDash([2, 2], 0);
GridV := Plot.MaxValue / 4;
while GridV <= Plot.MaxValue do
begin
GridY := ValueToY(Plot, GridV);
Page.MoveTo(Plot.Left, GridY);
Page.LineTo(Plot.Left + Plot.Width, GridY);
Page.Stroke;
GridV := GridV + Plot.MaxValue / 4;
end;
Page.NoDash;
Page.SetFont('Arial', [], 9);
for I := 0 to Count - 1 do
begin
BarX := Plot.Left + I * SlotW + Gap / 2;
BarTop := ValueToY(Plot, Values[I]);
Page.SetRGBFillColor(BarColors[I mod Length(BarColors)]);
Page.Rectangle(BarX, BarTop, BarW, Plot.Bottom - BarTop);
Page.Fill;
// TextOut piazza la cima del testo a Y
Page.SetRGBFillColor(clBlack);
Page.TextOut(BarX, Plot.Bottom + 4, 0, Labels[I]);
Page.TextOut(BarX, BarTop - 12, 0, FormatFloat('0', Values[I]));
end;
// La baseline per ultima, così sta sopra i fondi delle barre
Page.SetLineWidth(1.0);
Page.SetRGBStrokeColor(clBlack);
Page.MoveTo(Plot.Left, Plot.Bottom);
Page.LineTo(Plot.Left + Plot.Width, Plot.Bottom);
Page.Stroke;
end;
L'ordine è tutto il modello di stratificazione qui. PDF usa un modello del pittore, in cui gli operatori successivi nel content stream dipingono sopra i precedenti, quindi le linee di griglia vanno per prime, le barre sopra, la baseline per ultima. Scambiate il blocco delle linee di griglia sotto il ciclo e le linee tratteggiate corrono dritte attraverso ogni barra
Il varco è una frazione dello slot invece di un numero fisso di punti. Con quattro categorie su un tracciato da 470 punti ogni slot è largo 117.5 punti e le barre prendono circa 29 punti d'aria tra loro; con quaranta categorie lo stesso codice lascia meno di 3 punti, che si leggono ancora come barre separate. Un varco fisso da 12 punti inghiottirebbe 480 dei 470 punti disponibili a quaranta categorie e la larghezza della barra diventerebbe negativa
Etichette degli assi che non collidono
Le etichette sono dove i grafici disegnati a mano si rompono per primi, e quasi sempre allo stesso modo. Un testo che stava a schermo sfora il suo spazio sulla pagina, perché lo sviluppatore ha provato con "Sep" e il report tedesco stampa "September" o "Dezember" in un font da 9 punti. L'asse dei valori ha una variante più silenziosa dello stesso problema: etichette delle tacche allineate a sinistra a un offset X fisso sembrano arruffiate appena i numeri cambiano lunghezza, e un'etichetta "1,200" corre contro la linea dell'asse che "800" rispettava
Misurate invece di tirare a indovinare. TextWidth restituisce la larghezza di una stringa nel font corrente, in unità di pagina, e TextHeight restituisce l'ascent del font alla dimensione corrente. Allineate a destra ogni etichetta di tacca contro l'asse e centratala sulla sua tacca:
procedure DrawValueAxis(Page: THPDFPage; const Plot: TPlotArea;
TickCount: Integer);
var
I: Integer;
TickV, TickY: Single;
S: string;
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); // breve tacca fuori dall'asse
Page.LineTo(Plot.Left, TickY);
Page.Stroke;
S := FormatFloat('#,##0', TickV);
Page.TextOut(Plot.Left - 7 - Page.TextWidth(AnsiString(S)),
TickY - Page.TextHeight(AnsiString(S)) / 2, 0, S);
end;
end;
Per le etichette di categoria la stessa misura vi dice quando arrendersi sul testo orizzontale. Confrontate l'etichetta più larga con la larghezza dello slot prima di disegnare qualcosa. Se non ci sta, calate il font di un punto o due; se ancora non ci sta, passate 90 come argomento angolo di TextOut e mettete le etichette in verticale, poi lasciate margine a sufficienza sotto la baseline per la più lunga. Provate con l'etichetta più larga che il report può davvero produrre, che per i nomi dei mesi in una build localizzata è raramente quella inglese
I grafici a linee riusano la stessa mappatura
Un grafico a linee cambia solo il modo in cui i punti si collegano. Percorrete i dati una volta, convertite ogni valore con ValueToY, e cucite i punti in un solo percorso: MoveTo per il primo punto, LineTo per ogni successivo, un singolo Stroke alla fine. Se avete già i punti in un array, Polyline prende un array of THPDFCurrPoint, apre un percorso fresco e lascia lo stroke a voi; solleva un'eccezione con meno di due punti, quindi una serie con una singola lettura vuole un marcatore invece di una linea
La spaziatura è dove un grafico a linee devia da un grafico a barre disegnato sullo stesso tracciato. Le barre dividono la larghezza per il numero di categorie perché ogni barra possiede uno slot. Una linea lo divide per il numero di intervalli, Count - 1, perché il primo e l'ultimo punto stanno sui bordi del tracciato. Sovrapponete i due e i punti della linea atterrano sui confini degli slot invece che sui centri delle barre. Quando una linea cavalca le barre, piazzate i suoi punti al centro di ogni slot, Plot.Left + I * SlotW + SlotW / 2, e eliminate del tutto la spaziatura a Count - 1. Per un marcatore su ogni punto, un piccolo Circle seguito da Fill dopo che la linea è tracciata fa il lavoro
Spicchi di torta con Pie, in senso orario dalle dodici
Uno spicchio di torta è due raggi e un arco. Il modo artigianale di costruirne uno parte dal centro, avanza con brevi segmenti LineTo a un paio di gradi di distanza lungo il bordo, e richiude verso il centro, che è la costruzione nella figura sotto. Funziona, ma uno spicchio di 270 gradi diventa ben oltre un centinaio di segmenti di percorso e il bordo resta comunque un poligono sotto un forte zoom
Usate invece Pie. THPDFPage.Pie segue la convenzione GDI: un rettangolo di delimitazione, un angolo iniziale e uno finale in gradi, 0 che punta a est, 90 a nord, angoli positivi in senso antiorario. Costruisce l'arco da segmenti Bézier di al più 90 gradi ciascuno, disegna la linea verso il centro e chiude il percorso, così il bordo resta una curva vera e uno spicchio intero costa una manciata di operatori. Le torte di business di solito partono dalle dodici e girano in senso orario, che in termini GDI significa partire da 90 e scendere con l'angolo:
procedure DrawPieChart(Page: THPDFPage; CX, CY, Radius: Single;
const Values: array of Double; const Colors: array of TColor);
var
I: Integer;
Total, Sweep, Angle: Double;
begin
Total := 0;
for I := 0 to High(Values) do
if Values[I] > 0 then
Total := Total + Values[I];
if Total <= 0 then
Exit;
Angle := 90; // le dodici
Page.SetRGBStrokeColor(clWhite);
Page.SetLineWidth(0.8);
for I := 0 to High(Values) do
begin
if Values[I] <= 0 then
Continue; // vedi sotto: non passare mai uno sweep zero o negativo
Sweep := Values[I] / Total * 360;
Page.SetRGBFillColor(Colors[I mod Length(Colors)]);
// In senso antiorario da Angle - Sweep ad Angle c'è la fetta che
// sta in senso orario rispetto alla posizione corrente
Page.Pie(CX - Radius, CY - Radius, CX + Radius, CY + Radius,
Angle - Sweep, Angle);
Page.FillAndStroke; // il tratto bianco separa le fette
Angle := Angle - Sweep;
end;
end;
La guardia Continue è la riga che conta di più. Pie tratta un angolo finale più piccolo di quello iniziale come uno sweep che avvolge oltre i 360, la lettura GDI, ed è ciò che permette a uno spicchio di attraversare la linea degli 0 gradi. Una riga di rimborso o una voce di correzione nei dati sorgente dà un valore negativo, l'angolo finale calcolato atterra sotto quello iniziale, e lo spicchio avvolge in un disco quasi pieno dipinto sopra tutto ciò che era stato disegnato prima. Un valore zero è più mite ma pur sempre visibile: il percorso chiuso collassa in un raggio, e il tratto bianco di separazione lo disegna come una linea dispersa dal bordo al centro. Filtrate entrambi prima del ciclo, e decidete a parte come il report mostra gli importi negativi, perché una torta non li può mostrare affatto
Quando il design non insiste sul cerchio, una singola barra impilata orizzontale mostra le stesse quote con nient'altro che chiamate Rectangle disposte una dietro l'altra, e i lettori confrontano le lunghezze più accuratamente degli angoli. Per una figura parte-del-tutto con più di cinque o sei categorie è il grafico migliore, e il codice del grafico a barre qui sopra fa già la maggior parte del lavoro
Ogni grafico in questo articolo è costruito con le chiamate del canvas di pagina dello HotPDF Delphi Component, con DrawChart disponibile per i layout standard a linea, barre, area, torta e scatter, e il tutto gira da Delphi o C++Builder senza una libreria di grafici installata sulla macchina di build