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:

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;

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:

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;

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);   // 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 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)        // 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;

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:

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;

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 Component za Delphi in C++Builder