Tehnički članak

Crtanje grafikona u PDF-u s HotPDF osnovnim oblicima

HotPDF nema objekt grafikona. Ovdje ne postoji TPDFChart, kao ni funkcija AddBarSeries ili išta slično što jednostavno preuzima polje brojeva i vraća iscrtani grafikon. Ono što umjesto toga dobivate jest radna površina stranice s istim vokabularom niske razine koji koristi svaki model crtanja PDF-a: pravokutnici, linije, krugovi, ispune, potezi i tekst smješteni na točnim koordinatama. Grafikon u HotPDF dokumentu je, stoga, nešto što sami gradite, a ne nešto što tražite. To zvuči kao više posla nego što zaista jest. Nakon što jednom napišete matematiku koordinata, stupčasti grafikon postaje samo petlja nad pravokutnicima, linijski grafikon je polilinija, a tortni grafikon lepeza lukova, dok vi u potpunosti kontrolirate svaki piksel rezultata

Ovo je važno jer prva alternativa za kojom ljudi posežu, rasteriziranje kontrole zaslonskog grafikona u bitmapu i lijepljenje slike na stranicu, rezultira grafikonom koji je zaključan na zaslonsku rezoluciju, mutan pri ispisu i nepotrebno povećava veličinu datoteke. Crtanje grafikona s pomoću HotPDF vektorskih osnovnih oblika zadržava izlaz jasnim pri bilo kojem povećanju i svakoj DPI rezoluciji ispisa jer su stupci i osi pravi PDF operatori staza, a ne pikseli. Cijena toga jest da vi upravljate rasporedom. Mehanika se svodi na nekoliko osnovnih poteza: jedan obrat koordinata s kojim se svi bore, razrađeni stupčasti grafikon, trik s polilinijom za linijske grafikone i matematika luka za kriške torte

Jedini težak dio je koordinatni sustav

Zaslonska grafika postavlja ishodište u gornji lijevi kut s Y koordinatom koja raste prema dolje. PDF radi upravo suprotno. Ishodište leži u donjem lijevom kutu stranice, a Y koordinata raste prema gore i mjeri se u točkama (1/72 inča). Svaki poziv za crtanje u HotPDF-u, TextOut, Rectangle, MoveTo, LineTo, Circle, koristi tu donju-lijevu, Y-prema-gore konvenciju. Ako prenesete instinkte zaslonske grafike, vaš prvi grafikon bit će nacrtan naopako i izaći će s dna stranice

Pravi zadatak u bilo kojem grafikonu je jedno mapiranje: pretvaranje podatkovne vrijednosti u Y koordinatu koja poštuje to pravilo. Odlučite se za pravokutnik crteža (četiri broja za područje unutar kojeg će grafikon biti), a zatim mapirajte najmanju vrijednost vaših podataka na donji rub, a najveću na vrh. Za stupac vrijednosti V na ljestvici koja ide od 0 do MaxValue, gornji rub stupca bit će PlotBottom + (V / MaxValue) * PlotHeight, dok stupac raste prema gore od PlotBottom. Pogodite taj jedan izraz i sve ostalo bit će samo jednostavno bilježenje. Sljedeći pomoćnik sadrži geometriju grafa i vrši konverziju kako kôd za crtanje ne bi dvaput dodirivao istu aritmetiku:

HotPDF: zaslonske koordinate postavljaju ishodište gore-lijevo s Y rastućim prema dolje dok PDF koordinate počinju dolje-lijevo s Y rastućim prema gore, a ValueToY preslikava podatkovnu vrijednost u taj prostor s Y prema gore
PDF okreće poznati okvir ekrana: ishodište sjedi dolje lijevo, Y raste prema gore, a jedan ValueToY izraz preslikava svaku podatkovnu vrijednost u taj prostor
type
  TPlotArea = record
    Left, Bottom, Width, Height: Single;  // PDF točke, ishodište donji lijevi kut
    MaxValue: Single;                     // vrh skale vrijednosti
  end;

// Mapiraj vrijednost podatka na njenu Y koordinatu unutar grafa, Y raste prema gore.
function ValueToY(const Plot: TPlotArea; V: Single): Single;
begin
  Result := Plot.Bottom + (V / Plot.MaxValue) * Plot.Height;
end;

Jedna sitnica krije se u MaxValue. Ako ga postavite na točno najveću točku u podacima, najviši stupac dodirnut će gornji rub grafa i izgledat će odrezano. Zaokružite ga na čisti broj iznad samog maksimuma, na primjer, na sljedeći višekratnik od 10 ili 100, tako da grafikon ima prostora i oznake rešetki izgledaju kao zaokružene brojke, a ne kao nasumične brojke ovisne o najvećoj vrijednosti

Stupčasti grafikon je petlja nad pravokutnicima

Nakon što riješite mapiranje, stupčasti se grafikon piše sam. Podijelite širinu grafa u jedan utor po kategoriji, ostavite razmak između stupaca kako se ne bi dodirivali i nacrtajte svaki stupac kao ispunjeni pravokutnik čija visina dolazi iz funkcije ValueToY. HotPDF funkcija Rectangle uzima donji lijevi kut zajedno sa širinom i visinom, što se točno podudara sa stupcem koji raste prema gore od osnovne linije. Prvo postavite boju ispune, položite stazu, a zatim pozovite Fill kako biste je obojili. Oznaka kategorije ide ispod osnovne linije, a vrijednost iznad stupca:

HotPDF: anatomija stupčanog grafikona koja prikazuje slotove podijeljene iz širine grafa s razmacima među stupcima, svaki stupac ispunjeni Rectangle koji raste gore od Plot.Bottom, oznake kategorija ispod baze, oznake vrijednosti uz visinu stupca i crtice osi koje ponovno koriste ValueToY
Svaki stupac jedan je ispunjen Rectangle koji raste gore od Plot.Bottom unutar svog utora, jaz četvrtine utora skalira se sa širinom, a osovinske crtice ponovno koriste ValueToY tako da se stupci i mrežne linije slažu
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;          // razmak od četvrtine utora sa svake strane
  BarW := SlotW - Gap;

  // Osnovna linija (os X) duž dna grafa.
  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, širina, visina
    Page.Fill;

    // Oznaka kategorije ispod osnovne linije, vrijednost iznad stupca.
    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 ovdje opravdavaju svoje postojanje. Razmak (Gap) je udio utora, a ne fiksni broj točaka, tako da stupci ostaju proporcionalno raspoređeni bilo da crtate četiri kategorije ili njih četrdeset. A oznaka vrijednosti smještena je s istom visinom izvedenom iz ValueToY koju koristi stupac, pa uvijek sjedi točno iznad vlastitog stupca umjesto da lebdi u obliku nagađanog pomaka. Ako želite horizontalne mrežne linije iza stupaca, nacrtajte ih prije petlje: odaberite tri ili četiri okrugle vrijednosti, pokrenite ValueToY za svaku i povucite blagu crtu preko grafa na tom Y-u. Njihovo crtanje na početku postavlja ih iza stupaca unutar slikarskog modela slaganja koji koristi PDF

Osi, kvačice i oznake samo su dodatne linije i tekst

Grafikon nije završen sve dok čitatelj ne bude razumio što stupci znače, a to je u potpunosti zadatak osi. Vertikalna os je jedna nacrtana linija uz lijevi rub grafa uz pregršt kvačica i njihovih vrijednosti. Ponovno iskoristite ValueToY kako bi kvačice sletjele na isto mjerilo kao i stupci, jer se u suprotnom stupac i njegova rešetka neće slagati, i grafikon će tiho prikazivati pogreške:

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);   // kratka crtica izvan osi
    Page.LineTo(Plot.Left, TickY);
    Page.Stroke;
    Page.TextOut(Plot.Left - 30, TickY - 3, 0, FormatFloat('0', TickV));
  end;
end;

Oznake su mjesto gdje grafikoni u produkciji najčešće zakazuju, a pogreška je uvijek ista: tekst koji je dobro pristajao na zaslonu sada preuzima cijeli prostor u PDF-u. Dugi nazivi kategorija sudarat će se sa svojim susjedima, a lokalizirani mjeseci poput "septembre" ili "Dezember" bit će puno širi od engleskog "Sep" s kojim ste testirali. Ovdje ne postoji automatsko skaliranje kako biste se spasili, stoga ostavite dovoljnu marginu ispod osnovne linije, smanjite font za veličinu ili dvije za gušće skupove kategorija i ako su nazivi doista dugi, jednostavno ih rotirajte. TextOut funkcija uzima kut kao svoj treći argument, stoga ako unesete 90 to će podići oznaku i osigurati prostor bez preklapanja. Testirajte raspored sa svojom najširom očekivanom oznakom, a ne najkraćom, i to učinite prije nego što projekt ide u izvoz

Linijski grafikoni: jedna polilinija preko mapiranih točaka

Linijski grafikon koristi čitavo to isto mapiranje vrijednosti te samo mijenja način povezivanja. Umjesto pravokutnika po kategoriji, vi prolazite kroz podatke jednom, pretvarate svaku vrijednost u (X, Y) s ValueToY funkcijom i spajate ih s MoveTo pozivima popraćenim s LineTo pozivima. Prva točka otvara stazu, a svaka naknadna ju dodatno proširuje:

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)        // otvori putanju na prvoj točki
    else
      Page.LineTo(X, Y);       // proširi je kroz svaku sljedeću točku
  end;
  Page.Stroke;                 // jedan potez iscrtava cijelu izlomljenu liniju
end;

Zabilježite razliku u proredu. Stupčasti grafikon dijeli širinu po broju stupaca, iz razloga što svaki stupac preuzima jedan dio utora. Linijski grafikon se dijeli putem broja samih intervala, Count - 1, to je stoga jer prva i zadnja točka uvijek stoje na granicama grafa. Ukoliko to pomiješate, linijski grafikon vrlo će lako otići napola ukoso prema rubovima u odnosu na grafičke oznake na kojem bi trebao sjediti. Ako vam je potreban marker pri svakoj podatkovnoj točki, stavite manji Circle i Fill pri samom (X, Y) dijelu i to nakon što se polilinija nacrta

Tortni grafikoni: kružni lukovi ili kriške

Kriške tortnog grafikona predstavljaju jedini ovakav oblik u kojem je nužno poznavanje trigonometrije. Razlog tomu jest taj da je sam rub ograničen s dva radijusa kruga te lukom. Iskreniji i direktniji prikaz prebrisat će sami luk tako što povuče mali stupnjeviti polilinijski segment uzduž okvira linije, uz adekvatnu aproksimaciju kružnice. Svaki presjek kuta iznosit će otprilike (Vrijednost / Total) * 2π, pa ćete ukupan obliži kut dobiti ovisno o tom krugu:

HotPDF: konstrukcija isječka pite počinje s MoveTo u središtu kruga, koraci malih LineTo segmenata otprilike dva stupnja razmaknutih duž luka preko zahvaćenog kuta svakog isječka, pa zatvaranje prema središtu tako da Fill boji puni klin
Klin počinje s MoveTo u središtu, prolazi malim LineTo koracima razmaknutima oko dva stupnja duž svog dijela opsega i zatvara se natrag tako da ga Fill čvrsto naslika
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 stupnja po segmentu

    Page.SetRGBFillColor(Colors[I]);
    Page.MoveTo(CX, CY);                     // vrh isječka u središtu
    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);                      // zatvori natrag u središte
    Page.Fill;

    Start := Start + Sweep;                   // pomakni se na sljedeći isječak
  end;
end;

Putanja od centra prema van, gdje vrh ide prvi, pa onda luk i potom nazad do centra stvara zatvoreni klin kojeg Fill oslikava do pune linije. Računanje po segmentima mijenja samu glatkoću veličine polja. Grubo oko dva stupnja na jedan korak, na bilo kojem boljem i adekvatnijem omjeru dat će dobar ispis, a neće nužno otežati graf. Ako nemate potrebe za cijelim i pravim krugom radije preskočite gornju trigonometriju pa renderirajte taj dio u jedan okrugli sloj. Biti će puno jednostavnije pri pogledu te izgleda čisto naspram nepotrebnih detalja na bazi krugova

Ništa od ovoga nužno ne ovisi o bilo kojem posebnom alatu ni obliku iscrtanog grafikona za dodatno ispisivanje, tu i je pretežita prednost koja izvlači bazu primitiva pri usmjeravanju. Isto sučelje za crtanje baze iscrtava graf i logotip te ih oblikuje redom u grafikon, dok će funkcija s TextOut isto tako izvesti i oznaku polja za sami axis tj osi unosa. Postavite pravila okvira direktno u formu, izvedite putanje od Y, pa odradite jedan kratki prolaz koji objedinjuje parametre Rectangle, LineTo, kao i opciju Circle kod novog izvještaja. Funkcije poput Rectangle, MoveTo, LineTo, Circle, Fill, Stroke, kao i TextOut sastavni su dio HotPDF Delphi komponente za Delphi i C++Builder