HotPDF nu are un obiect de tip grafic. Nu există niciun TPDFChart, niciun AddBarSeries, nimic care să primească un tablou de numere și să înapoieze un grafic randat. În schimb, vă dă o pânză de pagină cu același vocabular de nivel jos pe care îl folosește orice model de desen PDF: dreptunghiuri, linii, cercuri, umpleri, contururi și text plasat la coordonate exacte. Un grafic într-un document HotPDF este, prin urmare, ceva ce construiți, nu ceva ce cereți. Sună a mai multă muncă decât este. Odată ce ați scris o dată calculul de coordonate, un grafic cu bare este o buclă peste dreptunghiuri, un grafic cu linii este o polilinie, iar un grafic circular este un evantai de arce, și controlați fiecare pixel al rezultatului
Asta contează pentru că alternativa la care lumea apelează prima dată, rasterizarea unui control de grafic de pe ecran într-un bitmap și lipirea imaginii în pagină, vă dă un grafic legat de rezoluția ecranului, care se tipărește neclar și umflă fișierul. Desenarea graficului cu primitivele vectoriale din HotPDF păstrează ieșirea clară la orice mărire și la orice DPI de tipărire, pentru că barele și axele sunt operatori de traseu PDF adevărați, nu pixeli. Costul este că aranjamentul vă aparține. Mecanica se reduce la câteva mișcări: singura inversare de coordonate care prinde pe toată lumea, un grafic cu bare lucrat în detaliu, trucul poliliniei pentru graficele cu linii și calculul de arce pentru feliile de plăcintă
Singura parte grea este sistemul de coordonate
Grafica de ecran pune originea în stânga sus, cu Y crescând în jos. PDF face invers. Originea stă în colțul din stânga jos al paginii, iar Y crește în sus, măsurat în puncte (1/72 dintr-un inch). Fiecare apel de desenare din HotPDF, TextOut, Rectangle, MoveTo, LineTo, Circle, folosește acea convenție cu originea în stânga jos și Y în sus. Dacă vă aduceți cu dumneavoastră instinctele din grafica de ecran, primul grafic se desenează cu susul în jos și fuge dincolo de marginea de jos a paginii
Așadar, munca reală din orice grafic este o singură mapare: transformarea unei valori de date într-o coordonată Y care respectă orientarea Y în sus. Hotărâți un dreptunghi de reprezentare, patru numere pentru zona în care trăiește graficul, apoi mapați cea mai mică valoare din date pe marginea de jos și pe cea mai mare pe marginea de sus. Pentru o bară cu valoarea V pe o scară care merge de la 0 la MaxValue, marginea de sus a barei este PlotBottom + (V / MaxValue) * PlotHeight, iar bara crește în sus de la PlotBottom. Nimeriți acea singură expresie și tot restul este contabilitate. Funcția ajutătoare de mai jos ține geometria reprezentării și face conversia, așa că nicăieri în codul de desenare nu se atinge de aritmetica brută de două ori:
type
TPlotArea = record
Left, Bottom, Width, Height: Single; // puncte PDF, origine în stânga jos
MaxValue: Single; // capătul de sus al scării de valori
end;
// Mapează o valoare de date la coordonata ei Y în interiorul reprezentării, cu Y în sus.
function ValueToY(const Plot: TPlotArea; V: Single): Single;
begin
Result := Plot.Bottom + (V / Plot.MaxValue) * Plot.Height;
end;
O decizie de judecată se ascunde în MaxValue. Dacă o setați exact pe cea mai mare valoare din date, bara cea mai înaltă atinge marginea de sus a reprezentării și pare tăiată. Rotunjiți-o în sus la un număr curat, peste maxim, să zicem următorul multiplu de 10 sau de 100, ca graficul să aibă spațiu deasupra, iar etichetele caroiajului să se citească drept cifre rotunde, nu drept orice s-a întâmplat să fie vârful datelor
Un grafic cu bare este o buclă peste dreptunghiuri
Cu maparea rezolvată, un grafic cu bare se scrie singur. Împărțiți lățimea reprezentării într-un slot pentru fiecare categorie, lăsați un spațiu între bare ca să nu se atingă și desenați fiecare bară ca dreptunghi umplut a cărui înălțime vine din ValueToY. Metoda Rectangle din HotPDF primește colțul din stânga jos plus o lățime și o înălțime, ceea ce se potrivește exact cu o bară care crește în sus de la linia de bază. Setați întâi culoarea de umplere, așezați traseul, apoi apelați Fill ca să îl pictați. Eticheta de categorie merge sub linia de bază, valoarea deasupra barei:
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; // spațiu de un sfert de slot pe fiecare parte
BarW := SlotW - Gap;
// Linia de bază (axa X) de-a lungul părții de jos a reprezentării.
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;
// Eticheta de categorie sub linia de bază, valoarea deasupra barei.
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;
Două detalii își merită locul. Gap este o fracțiune din slot, nu un număr fix de puncte, așa că barele rămân distanțate proporțional fie că reprezentați patru categorii, fie patruzeci. Iar eticheta de valoare este poziționată cu aceeași înălțime derivată din ValueToY pe care o folosește bara, deci stă întotdeauna chiar deasupra barei ei, în loc să plutească la un decalaj ghicit. Dacă vreți linii de caroiaj orizontale în spatele barelor, desenați-le înaintea buclei: alegeți trei sau patru valori rotunde, rulați ValueToY pe fiecare și trasați o linie estompată de-a latul reprezentării la acel Y. Desenându-le primele, ele ajung în spatele barelor, în stivuirea de tip model al pictorului pe care o folosește PDF
Axele, gradațiile și etichetele sunt doar alte linii și text
Graficul nu este terminat până când un cititor nu poate spune ce înseamnă barele, iar asta ține în întregime de munca la axe. Axa verticală este o singură linie conturată pe marginea din stânga a reprezentării, cu câteva gradații și valorile lor. Refolosiți ValueToY, ca gradațiile să aterizeze pe aceeași scară pe care o folosesc barele, altfel o bară și linia ei de caroiaj se contrazic, iar graficul minte în tăcere:
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); // gradație scurtă în afara axei
Page.LineTo(Plot.Left, TickY);
Page.Stroke;
Page.TextOut(Plot.Left - 30, TickY - 3, 0, FormatFloat('0', TickV));
end;
end;
Etichetele sunt locul în care graficele se strică cel mai des în producție, iar eșecul este întotdeauna același: text care încăpea pe ecran își depășește spațiul în PDF. Numele lungi de categorii se ciocnesc de vecinii lor, iar numele de luni localizate, precum „septembre” sau „Dezember”, sunt mai late decât „Sep”-ul englezesc cu care ați testat. Aici nu există nicio redimensionare automată care să vă salveze, așa că lăsați o margine reală sub linia de bază, micșorați fontul cu un punct sau două pentru seturile dense de categorii, iar dacă numele sunt cu adevărat lungi, rotiți-le. TextOut primește un unghi ca al treilea argument, deci transmiterea valorii 90 pune eticheta pe verticală și vă cumpără spațiu fără suprapunere. Testați aranjamentul cu cea mai lată etichetă pe care o așteptați, nu cu cea mai scurtă, înainte ca exportul să plece la drum
Grafice cu linii: o polilinie prin punctele mapate
Un grafic cu linii refolosește întreaga mapare de valori și schimbă doar felul în care se leagă punctele. În loc de un dreptunghi pentru fiecare categorie, parcurgeți datele o dată, convertiți fiecare valoare în perechea ei (X, Y) cu ValueToY și coaseți punctele împreună cu un singur MoveTo urmat de apeluri LineTo, conturate la final. Primul punct deschide traseul; fiecare punct ulterior îl prelungește:
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) // deschide traseul la primul punct
else
Page.LineTo(X, Y); // îl prelungește prin fiecare punct ulterior
end;
Page.Stroke; // un singur contur pictează toată polilinia
end;
Observați diferența de distanțare. Un grafic cu bare împarte lățimea la numărul de bare, pentru că fiecare bară deține un slot. Un grafic cu linii împarte la numărul de intervale, Count - 1, pentru că primul și ultimul punct stau pe marginile reprezentării, iar linia acoperă spațiile dintre ele. Confundarea celor două este motivul obișnuit pentru care un grafic cu linii se deplasează cu jumătate de slot față de graficul cu bare peste care ar trebui să se suprapună. Dacă vreți un marcaj la fiecare punct de date, puneți un mic Circle și un Fill la fiecare (X, Y) după ce polilinia a fost conturată
Grafice circulare: arce, sau pene dacă păstrați lucrurile simple
Feliile de plăcintă sunt singura formă care are nevoie de trigonometrie, pentru că o pană este mărginită de două raze și de un arc. Varianta cinstită mătură arcul pășind cu segmente mici de-a lungul circumferinței, ceea ce aproximează curba suficient de bine încât niciun cititor să nu observe. Unghiul de baleiaj al fiecărei felii este cota ei din total, (Value / Total) * 2π, iar unghiul curent se acumulează pe măsură ce înaintați în jurul cercului:
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 grade pe segment
Page.SetRGBFillColor(Colors[I]);
Page.MoveTo(CX, CY); // vârful penei în centru
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); // se închide înapoi spre centru
Page.Fill;
Start := Start + Sweep; // avansează la felia următoare
end;
end;
Traseul dinspre centru spre exterior, întâi vârful, apoi arcul, apoi înapoi la vârf, dă o pană închisă pe care Fill o pictează plin. Numărul de segmente face un schimb între netezime și dimensiunea traseului: aproximativ două grade pe pas arată rotund la orice rază rezonabilă, fără să producă un flux de conținut enorm. Dacă nu aveți nevoie de un cerc adevărat, puteți sări complet peste trigonometrie și puteți reda aceleași date ca o singură bară orizontală stivuită, cu lățimea fiecărui segment proporțională cu cota lui. Oricum este adesea mai lizibil decât o plăcintă și este doar codul cu bare, cu dreptunghiurile așezate cap la cap. Apelați la varianta cu arce doar când designul cere un cerc adevărat
Nimic din toate acestea nu depinde de instalarea vreunei biblioteci de grafice, ceea ce este avantajul discret al desenării directe a primitivelor. Aceleași apeluri de desenare pe pânză care plasează un logo sau o casetă de semnătură construiesc aceste grafice, iar același TextOut care etichetează un câmp de formular etichetează o axă. Puneți geometria reprezentării într-o înregistrare, mapați valorile la Y o singură dată, iar un grafic cu bare, cu linii sau circular devine o rutină scurtă peste Rectangle, LineTo și Circle, pe care o puteți lăsa în orice raport. Apelurile Rectangle, MoveTo, LineTo, Circle, Fill, Stroke și TextOut folosite aici fac parte din HotPDF Delphi Component pentru Delphi și C++Builder