Tehnički članak

Crtanje grafikona u PDF-u pomocu HotPDF primitiva

HotPDF nema objekat grafikona. Ne postoji TPDFChart, niti AddBarSeries, nista sto prima niz brojeva i vraca renderovani grafikon. Umesto toga, daje vam platno stranice s istim niskonivojskim recnikom koji koristi svaki PDF model crtanja: pravougaonici, linije, krugovi, popune, konture i tekst smesten na tackne koordinate. Grafikon u HotPDF dokumentu je, dakle, nesto sto vi gradite, a ne nesto sto zahtevate. To zvuci kao vise posla nego sto zaista jeste. Kada jednom napiste matematiku koordinata, stubicasti grafikon je petlja nad pravougaonicima, linijski grafikon je poliliniija, a krozni grafikon je lepeza lukova, i vi kontrolisete svaki piksel rezultata

Ovo je vazno jer alternativa kojoj ljudi prvo posegnu, rasterizovanje graficke kontrole ekrana u bitmap i lepljenje slike na stranicu, daje grafikon zakljiucan na rezoluciju ekrana koji se zamagljuje pri stampanju i nadima fajl. Crtanje grafikona pomocu vektorskih primitiva HotPDF-a cuva izlaz ostrim pri svakom zumu i svakom DPI-ju stampaca, jer stubici i ose su pravi PDF operatori putanje, a ne pikseli. Cena je da vi posedujete raspored. Mehanika se svodi na nekoliko poteza: jedino preokretanje koordinata koje svakoga uhvati, obradeni stubicasti grafikon, trik s polilinijom za linijske grafikone i matematika luka za segmente kroznog grafikona

Jedini tezan deo je koordinatni sistem

Ekranska grafika postavlja ishodiste u gornji levi ugao sa Y koji raste nadole. PDF radi suprotno. Ishodiste se nalazi u donjem levom uglu stranice i Y raste nagore, meren u tackama (1/72 inca). Svaki poziv crtanja u HotPDF-u, TextOut, Rectangle, MoveTo, LineTo, Circle, koristi taj donji levi, Y-gore konvenciju. Ako prenesete instinkte iz ekranske grafike, vas prvi grafikon ce biti nacrtan naopako i pobeknuti s dna stranice

Dakle, pravi posao u bilo kom grafikonu je jedno mapiranje: pretvorite vrednost podatka u Y koordinatu koja postuje Y-gore. Odlucite se za pravougaonik grafikona, cetiri broja za oblast gde grafikon zivi, zatim mapujte najmanju vrednost u vasim podacima na donju ivicu, a najvecu na gornju. Za stubic vrednosti V na skali od 0 do MaxValue, gornja ivica stubica je PlotBottom + (V / MaxValue) * PlotHeight, a stubic raste od PlotBottom nagore. Dobro definisajte taj jedan izraz i sve ostalo je racunovodstvo. Pomocna funkcija ispod cuva geometriju grafikona i vrsi konverziju, tako da kod za crtanje nikada ne dodiruje sirovu aritmetiku dvaput:

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;

Jedna odluka se krije u MaxValue. Ako ga postavite na tacno najvecu tacku podataka, najvisi stubic dodiruje gornju ivicu grafikona i izgleda iseceno. Zaokruzite ga na cist broj iznad maksimuma, recimo sledeci visekratnik od 10 ili 100, tako da grafikon ima prostora za dah i oznake linije mreye citaju kao okrugle cifre, a ne ono sto se podaci desili da dostignu kao vrh

Stubicasti grafikon je petlja nad pravougaonicima

Sa uspostavljenim mapiranjem, stubicasti grafikon se pise sam od sebe. Podelite sirinu grafikona na jedan slot po kategoriji, ostavite razmak izmedzu stubica kako se ne bi dodirivali, i nacrtajte svaki stubic kao ispunjeni pravougaonik cijja visina dolazi od ValueToY. HotPDF-ov Rectangle uzima donji levi ugao plus sirinu i visinu, sto se tacno uklapa u stubic koji raste nagore od bazne linije. Postavite boju popune prve, postavite putanju, zatim pozovite Fill da je iscrtate. Oznaka kategorije ide ispod bazne linije, a vrednost iznad stubica:

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;

Dva detalja zasluzuju paznju. Gap je deo slota, a ne fiksni broj tacaka, pa stubici ostaju proporcionalno rasporedeni bez obzira crtate li cetiri kategorije ili cetrdeset. I oznaka vrednosti je pozicionirana s istom visinom izvedenom iz ValueToY koju koristi stubic, tako da uvek sedi tacno iznad svog stubica umesto da pluta na pogodjenoj visini. Ako zelite horizontalne linije mreze iza stubica, nacrtajte ih pre petlje: odaberite tri ili cetiri okrugle vrednosti, pokrenite ValueToY na svakoj, i povucite blagu liniju kroz grafikon na toj Y koordinati. Crtanjem prvim postavlja ih iza stubica u slaganju po modelu slikara koje PDF koristi

Ose, nozice i oznake su samo jos linije i tekst

Grafikon nije zavrsen dok citaoci ne mogu da shvate sta stubici znace, a to je u potpunosti posao osa. Vertikalna osa je jedna vucena linija uz levu ivicu grafikona s sakom nozica i njihovim vrednostima. Ponovo koristite ValueToY tako da nozice padaju na istu skalu koju koriste stubici, inace se stubic i njegova linija mreze ne slazu i grafikon tiho laze:

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;

Oznake su mesto gde grafikoni najcesce pucaju u produkciji, i kvar je uvek isti: tekst koji je stao na ekranu prelazi granice u PDF-u. Dugacka imena kategorija sudare se s komsiijama, a lokalizovana imena meseci kao sto su "septembre" ili "Dezember" su sira od engleskog "Sep" s kojim ste testirali. Nema automatskog prilagodjavanja velicine koji ce vas spasiti, pa ostavite pravu marginu ispod bazne linije, smanjite font za tacku-dve za guste skupove kategorija, i ako su nazivi zaista dugi, rotirajte ih. TextOut uzima ugao kao treci argument, pa prosledjivanjem 90 postavlja oznaku uspravno i dobijate prostor bez preklapanja. Testirajte raspored s vasim najsirim ocekivanim oznakom, a ne s najkracim, pre nego sto se izvoz isporucuje

Linijski grafikoni: jedna poliliniija kroz mapovane tacke

Linijski grafikon ponovo koristi celokupno mapiranje vrednosti i menja samo nacin na koji se tacke povezuju. Umesto pravougaonika po kategoriji, jednom prolazite kroz podatke, konvertujete svaku vrednost u njene (X, Y) koordinate pomocu ValueToY, i spajate tacke zajedno jednim MoveTo pracenim pozivima LineTo, iscrtanim na kraju. Prva tacka otvara putanju; svaka kasnija je prosiruje:

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;

Primetite razliku u razmaku. Stubicasti grafikon deli sirinu brojem stubica, jer svaki stubic poseduje slot. Linijski grafikon deli brojem intervala, Count - 1, jer prva i poslednja tacka sede na ivicama grafikona i linija premostava razmake izmedzu njih. Mesan ova dva je uobicajen razlog zasto linijski grafikon drifuje za pola slota od stubicasnog grafikona koji treba da preklopi. Ako zelite marker na svakoj tacki podataka, postavite mali Circle i Fill na svakom (X, Y) nakon sto je poliliniija iscrtana

Krozni grafikoni: lukovi, ili klinovi ako je jednostavno

Isecci kroznog grafikona su jedini oblik koji zahteva trigonometriju, jer je klin ogranicen dvema poluprecnicima i lukom. Prava verzija premetace luk koracanjem malih segmenata linija duz obima, sto aproksimira krivu dovoljno blisko da nijedan citaoc ne moze da kaze. Ugao prelaska svakog isecka je njegov udeo ukupnog iznosa, (Value / Total) * 2*Pi, i akumulirate tekuci ugao kako idete naokolo:

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;

Putanja iz centra prema spolja, vrh prvo, zatim luk, zatim nazad do vrha, daje zatvoren klin koji Fill pune iscrtava. Broj segmenata trguje glatkocom za veliicnu putanje: otprilike dva stepena po koraku izgleda zaobljeno pri svakom razumnom poluprecniku bez stvaranja ogromnog toka sadrzaja. Ako vam ne treba pravi krug, mozete u potpunosti preskociti trigonometriju i prikazati iste podatke kao jednu horizontalnu naredanu traku, sirina svakog segmenta proporcionalna svom udelu. To je cesto preglednije od kroznog grafikona ionako, i to je samo kod trake s pravougaonicima slozenim jedan za drugim. Dosegnite do verzije s lukom samo kada dizajn zahteva pravi krug

Nista od ovoga ne zavisi od instaliranog ikakvog grafikonskog paketa, sto je tiha prednost direktnog crtanja primitiva. Isti pozivi za crtanje po platnu koji postavljaju logo ili okvir za potpis grade ove grafikone, a isti TextOut koji oznacava polje obrasca oznacava i osu. Smestite geometriju grafikona u slog, jednom mapirajte vrednosti na Y, i stubicasti, linijski ili krozni grafikon je kratka rutina nad Rectangle, LineTo i Circle koju mozete ubaciti u bilo koji izvestaj. Pozivi Rectangle, MoveTo, LineTo, Circle, Fill, Stroke i TextOut koji se ovde koriste deo su HotPDF Component za Delphi i C++Builder