Tehnični članak

Risanje grafikonov v PDF s HotPDF gradniki

HotPDF nima objekta grafikona. Ni razreda TPDFChart, ni metode AddBarSeries in ničesar, kar bi sprejelo polje številk in vrnilo izrisan grafikon. Namesto tega ponuja platno strani z istim nizkonivojskim besednjakom, ki ga uporablja vsak model risanja PDF: pravokotniki, črte, krogi, polnila, poteze in besedilo na natančnih koordinatah. Grafikon v dokumentu HotPDF je torej nekaj, kar zgradite sami, ne pa nekaj, kar le zahtevate. To se sliši kot več dela, kot je v resnici. Ko enkrat napišete koordinatno matematiko, je stolpčni grafikon le zanka čez pravokotnike, črtni grafikon je lomljena črta, tortni grafikon pa pahljača lokov, pri čemer nadzorujete vsako piko rezultata

To je pomembno, ker alternativa, po katero ljudje najpogosteje segajo najprej - rasterizacija zaslonskega grafikona v bitno sliko in lepljenje te slike na stran - da grafikon, zaklenjen na ločljivost zaslona, ki se natisne zamegljeno in poveča datoteko. Risanje grafikona z vektorskimi gradniki HotPDF ohrani izhod oster pri kateri koli povečavi in kateri koli ločljivosti tiskanja, ker so stolpci in osi dejanski operatorji poti v PDF-ju in ne piksli. Cena za to je, da upravljate postavitev sami. Mehanika se zvede na nekaj korakov: edina zamenjava koordinat, ki zmede vsakogar, primer stolpčnega grafikona, trik z lomljeno črto za črtne grafikone in matematika loka za tortne izseke

Edini težak del je koordinatni sistem

Zaslonska grafika postavi izhodišče v zgornji levi kot, Y pa narašča navzdol. PDF dela nasprotno. Izhodišče leži v spodnjem levem kotu strani, Y narašča navzgor, merjeno v točkah (1/72 palca). Vsak risalni klic v HotPDF - TextOut, Rectangle, MoveTo, LineTo, Circle - uporablja to pravilo spodaj-levo in Y-navzgor. Če prenesete zaslonske grafične reflekse, se bo vaš prvi grafikon izrisal obrnjen na glavo in bo izginil pod spodnjim robom strani

Dejansko delo pri vsakem grafikonu je torej ena preslikava: pretvorba vrednosti podatkov v koordinato Y, ki upošteva naraščanje Y navzgor. Določite pravokotnik izrisa - štiri številke za območje, kjer bo grafikon živel - nato pa najmanjšo vrednost podatkov preslikajte na spodnji rob in največjo na vrh. Za stolpec z vrednostjo V na lestvici od 0 do MaxValue je zgornji rob stolpca PlotBottom + (V / MaxValue) * PlotHeight, stolpec pa raste navzgor od PlotBottom. Ko pravilno nastavite ta en izraz, je vse ostalo le rutinsko delo. Spodnji pomočnik vsebuje geometrijo izrisa in izvaja pretvorbo, tako da risalna koda nikoli ne uporablja surove aritmetike dvakrat:

HotPDF: zaslonske koordinate postavijo izhodišče zgoraj levo z Y, ki raste navzdol, koordinate PDF pa se začnejo spodaj levo z Y, ki raste navzgor, ValueToY pa preslika podatkovno vrednost v ta prostor z Y navzgor
PDF prevrne znani zaslobski okvir: izhodišče leži spodaj levo in Y raste navzgor, en izraz ValueToY pa preslika vsako podatkovno vrednost v ta prostor
type
  TPlotArea = record
    Left, Bottom, Width, Height: Single;  // točke PDF, izhodišče spodaj levo
    MaxValue: Single;                     // vrh lestvice vrednosti
  end;

// Preslikaj podatkovno vrednost v njeno koordinato Y znotraj izrisa, Y narašča navzgor.
function ValueToY(const Plot: TPlotArea; V: Single): Single;
begin
  Result := Plot.Bottom + (V / Plot.MaxValue) * Plot.Height;
end;

V MaxValue se skriva ena strokovna presoja. Če jo nastavite na točno največjo vrednost podatkov, se bo najvišji stolpec dotikal zgornjega roba izrisa in bo videti odrezan. Zaokrožite jo navzgor na čisto število nad maksimumom - recimo na naslednji večkratnik 10 ali 100 - da ima grafikon prostor na vrhu in se oznake mrežnih črt berejo kot zaokrožene vrednosti namesto kot naključni vrhovi podatkov

Stolpčni grafikon je zanka čez pravokotnike

Ko je preslikava urejena, se stolpčni grafikon napiše sam. Širino izrisa razdelite na eno režo na kategorijo, med stolpci pustite vrzel, da se ne stikajo, in vsak stolpec narišite kot zapolnjen pravokotnik, katerega višina izhaja iz ValueToY. Metoda Rectangle v HotPDF sprejme spodnji levi kot ter širino in višino, kar se natančno ujema s stolpcem, ki raste navzgor od izhodiščne črte. Najprej nastavite barvo polnila, določite pot in nato pokličite Fill za izris. Oznaka kategorije gre pod izhodiščno črto, vrednost pa nad stolpec:

HotPDF: anatomija stolpčnega diagrama: reže razdeljene iz širine risalnega prostora s presledki med stolpci, vsak stolpec napolnjen Rectangle, ki raste navzgor od Plot.Bottom, oznake kategorij pod osnovno črto, oznake vrednosti na višini stolpca ter klerki osi, ki znova uporabijo ValueToY
Vsak stolpec je en polnjen Rectangle, ki raste navzgor od Plot.Bottom znotraj svoje reže, razmik četrtinske reže se meri s širino, črtice osi pa ponovno uporabijo ValueToY, tako da se stolpci in mrežne črte ujemajo
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;          // vrzel četrtine reže na vsaki strani
  BarW := SlotW - Gap;

  // Izhodiščna črta (os X) vzdolž dna izrisa.
  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, višina
    Page.Fill;

    // Oznaka kategorije pod izhodiščno črto, vrednost nad stolpcem.
    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;

Dve podrobnosti sta zlasti koristni. Gap je delež reže in ne fiksno število točk, zato so stolpci sorazmerno razporejeni, ne glede na to, ali rišete štiri kategorije ali štirideset. Oznaka vrednosti pa je postavljena z isto višino, izpeljano iz ValueToY, ki jo uporablja stolpec, tako da vedno sedi tik nad svojim stolpcem namesto pri ugibanjem odmiku. Če želite vodoravne mrežne črte za stolpci, jih narišite pred zanko: izberite tri ali štiri zaokrožene vrednosti, za vsako zaženite ValueToY in čez izris narišite tanko črto pri tistem Y. Ker jih narišete prve, bodo ležale za stolpci v skladu s slikarskim modelom sklada, ki ga PDF uporablja

Osi, črtice in oznake so le dodatne črte in besedilo

Grafikon ni končan, dokler bralec ne razbere pomena stolpcev, kar je v celoti delo z osmi. Navpična os je ena potegnjena črta vzdolž levega roba izrisa z nekaj črticami in njihovimi vrednostmi. Znova uporabite ValueToY, da črtice pristanejo na isti lestvici, ki jo uporabljajo stolpci - sicer se stolpec in njegova mrežna črta ne ujemata in grafikon tiho laže:

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

Oznake so tisto mesto, kjer grafikoni v produkciji najpogosteje spodletijo, in napaka je vedno enaka: besedilo, ki je ustrezalo zaslonu, preseže prostor v PDF-ju. Dolga imena kategorij trčijo s sosednjimi, lokalizirana imena mesecev, kot sta 'septembre' ali 'Dezember', pa so širša od angleške kratice 'Sep', s katero ste testirali. Tu ni samodejnega prilagajanja velikosti, ki bi vas rešilo, zato pustite dovolj prostora pod izhodiščno črto, za gosta nabora kategorij zmanjšajte pisavo za točko ali dve, in če so imena res dolga, jih zavrtite. TextOut sprejme kot kot tretji argument, zato vrednost 90 postavi oznako navpično in vam pridobi prostor brez prekrivanja. Pred pošiljanjem preizkusite postavitev z najdaljšo pričakovano oznako in ne z najkrajšo

Črtni grafikoni: ena lomljena črta skozi preslikane točke

Črtni grafikon znova uporabi celotno preslikavo vrednosti in spremeni le način, kako se točke povežejo. Namesto pravokotnika na kategorijo se enkrat sprehodite skozi podatke, vsako vrednost pretvorite v njen (X, Y) z ValueToY in točke povežete z enim samim klicem MoveTo, ki mu sledijo klici LineTo, na koncu pa Stroke. Prva točka odpre pot, vsaka naslednja jo podaljša:

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)        // odpri pot pri prvi točki
    else
      Page.LineTo(X, Y);       // podaljšaj jo skozi vsako naslednjo točko
  end;
  Page.Stroke;                 // en Stroke pobarva celotno polilinijo
end;

Upoštevajte razliko v razmiku. Stolpčni grafikon širino deli s številom stolpcev, ker vsak stolpec zaseda svojo režo. Črtni grafikon jo deli s številom intervalov, Count - 1, ker prva in zadnja točka sedita na robovih izrisa in črta premošča vrzeli med njima. Zamenjava teh dveh pristopov je pogost razlog, da se črtni grafikon premakne za pol reže glede na stolpčni grafikon, ki naj bi ga prekrival. Če želite označevalnik pri vsaki podatkovni točki, narišite majhen krog s Circle in Fill na vsakem (X, Y) po izrisu lomljene črte

Tortni grafikoni: loki ali izseki, če se odločite za preprosto pot

Tortni izseki so edina oblika, ki zahteva trigonometrijo, ker je izsek omejen z dvema polmeroma in lokom. Natančna različica loka se izrisuje s postopanjem majhnih segmentov črt vzdolž obsega, kar krivuljo aproksimira dovolj dobro, da je bralec ne opazi. Kot zasuka posameznega izseka je njegov delež v celoti, (Value / Total) * 2π, tekoči kot pa seštevate med kroženjem:

HotPDF: izgradnja rezine pogače: začetek z MoveTo v središču kroga, majhni odseki LineTo približno dve stopinji narazen po loku čez zasuk vsake rezine, nato zaprtje v središče, tako da Fill nariše poln klin
Klin začne z MoveTo v središču, koraka z majhnimi koraki LineTo približno dve stopinji narazen vzdolž svojega prehoda po obodu in se zapre nazaj, tako da Fill naslika trdno
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 stopinji na segment

    Page.SetRGBFillColor(Colors[I]);
    Page.MoveTo(CX, CY);                     // vrh izseka v središču
    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);                      // zapri nazaj v središče
    Page.Fill;

    Start := Start + Sweep;                   // premakni se na naslednji izsek
  end;
end;

Pot od središča navzven - vrh izseka, lok in nazaj v vrh - ustvari zaprt izsek, ki ga Fill trdno zapolni. Število segmentov uravnava gladkost in velikost poti: približno dve stopinji na korak dajeta zaokrožen videz pri poljubnem razumnem polmeru, ne da bi ustvarile ogromno vsebinsko tokovanje. Če ne potrebujete pravega kroga, lahko trigonometrijo v celoti preskočite in iste podatke prikažete kot en sam vodoravni naloženi stolpec, kjer je širina vsakega segmenta sorazmerna z njegovim deležem. To je pogosto bolj berljivo kot tortni grafikon in je dejansko le koda za stolpce s pravokotniki, položenimi eden za drugim. Po različici z lokom posegite le, ko zasnova zahteva dejanski krog

Nič od tega ni odvisno od nameščene knjižnice za grafikone, kar je tiha prednost neposrednega risanja gradnikov. Isti klici za risanje na platno, ki postavijo logotip ali polje za podpis, gradijo te grafikone, in isti TextOut, ki označuje polje obrazca, označuje tudi os. Postavite geometrijo izrisa v zapis, vrednosti preslikajte v Y enkrat, in stolpčni, črtni ali tortni grafikon postane kratka rutina z Rectangle, LineTo in Circle, ki jo lahko vstavite v katero koli poročilo. Klici Rectangle, MoveTo, LineTo, Circle, Fill, Stroke in TextOut, ki so tu prikazani, so del komponente HotPDF Delphi Component za Delphi in C++Builder