Odborný článok

Kreslenie grafov do PDF pomocou primitív HotPDF

HotPDF nemá žiadny objekt grafu. Neexistuje tu žiadne TPDFChart, žiadne AddBarSeries, nič, čo by prijalo pole čísel a odovzdalo späť vykreslený graf. Namiesto toho vám poskytne plátno stránky s rovnakým nízkoúrovňovým slovníkom, aký používa každý model kreslenia PDF: obdĺžniky, čiary, kruhy, výplne, ťahy (čiary) a text umiestnený na presných súradniciach. Graf v dokumente HotPDF je teda niečo, čo vytvoríte, a nie niečo, čo si vyžiadate. Znie to ako viac práce, než to v skutočnosti je. Keď už raz napíšete súradnicovú matematiku, stĺpcový graf je cyklus prechádzajúci cez obdĺžniky, čiarový graf je polyline (lomená čiara) a koláčový graf je vejár oblúkov, pričom ovládate každý jeden pixel výsledku

Záleží na tom preto, lebo alternatíva, po ktorej ľudia siahnu najprv – rastrovanie ovládacieho prvku grafu na obrazovke do bitmapy a prilepenie obrázka na stránku – vám dá graf uzamknutý na rozlíšenie obrazovky, ktorý sa vytlačí rozmazane a neprimerane zväčší súbor. Kreslenie grafu pomocou vektorových primitív HotPDF udržuje výstup ostrý pri každom priblížení a akomkoľvek DPI pri tlači, pretože stĺpce a osi sú skutočnými operátormi dráhy PDF, a nie pixelmi. Cenou za to je, že ste zodpovední za rozloženie. Mechanizmus sa scvrkáva na niekoľko krokov: jediné otočenie súradníc, ktoré dostane každého, vypracovaný stĺpcový graf, trik s lomenou čiarou pre čiarové grafy a oblúková matematika pre dieliky koláča

Jedinou ťažkou časťou je súradnicový systém

Grafika obrazovky kladie počiatok do ľavého horného rohu, pričom Y rastie smerom nadol. PDF robí opak. Počiatok sedí v ľavom dolnom rohu stránky a Y rastie smerom nahor, pričom sa meria v bodoch (points, 1/72 palca). Každé jedno volanie na kreslenie v HotPDF – TextOut, Rectangle, MoveTo, LineTo, Circle – používa túto konvenciu, kedy je začiatok vľavo dole a Y ide nahor. Ak si prinesiete inštinkty z grafiky obrazovky, váš prvý graf sa nakreslí hore nohami a utečie mimo spodný okraj stránky

Takže skutočnou prácou v akomkoľvek grafe je jedno jediné mapovanie: zmeňte údajovú hodnotu na súradnicu Y, ktorá rešpektuje to, že Y rastie nahor. Rozhodnite sa pre obdĺžnik grafu, štyri čísla pre oblasť, v ktorej bude graf žiť, potom namapujte najmenšiu hodnotu z vašich dát na spodný okraj a najväčšiu na horný. Pre stĺpec s hodnotou V na mierke, ktorá beží od 0 do MaxValue, je horný okraj stĺpca PlotBottom + (V / MaxValue) * PlotHeight a stĺpec rastie nahor z PlotBottom. Dajte tento jeden výraz dokopy správne a všetko ostatné je už len papierovanie. Nižšie uvedený pomocník obsahuje geometriu grafu a vykonáva túto konverziu, takže kód kreslenia sa už nikdy dvakrát nedotkne surovej aritmetiky:

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;

Jedno voľné rozhodnutie sa skrýva v MaxValue. Ak ho nastavíte presne na najväčší dátový bod, najvyšší stĺpec sa dotkne horného okraja grafu a bude vyzerať orezane. Zaokrúhlite ho smerom nahor na čisté (okrúhle) číslo nad maximom, napríklad na ďalší násobok 10 alebo 100, aby mal graf rezervu a štítky s mriežkou sa čítali ako okrúhle čísla, a nie ako nejaké čísla, na ktorých sa náhodou zastavili vrcholy údajov

Stĺpcový graf je cyklus cez obdĺžniky

Keď je mapovanie vyriešené, stĺpcový graf sa napíše sám. Rozdeľte šírku grafu do jedného slotu na kategóriu, nechajte medzi stĺpcami medzeru, aby sa nedotýkali, a každý stĺpec nakreslite ako vyplnený obdĺžnik, ktorého výška pochádza z ValueToY. Objekt Rectangle z knižnice HotPDF zaberie ľavý dolný roh a k tomu šírku aj výšku, čo je presne v súlade so stĺpcom, ktorý rastie od základnej línie smerom nahor. Najprv nastavte farbu výplne, vyznačte si cestu a potom zavolajte operátor Fill, aby ju vykreslil. Značka/štítok kategórie ide pod základnú líniu a samotná hodnota nad stĺpec:

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 detaily si svoje miesto v kóde obhája. Gap (medzera) je skôr zlomkom slotu, než nejaký pevný počet bodov, takže stĺpce si zachovávajú proporcionálne rovnaké odstupy bez ohľadu na to, či vykreslíte štyri kategórie, alebo hneď štyridsať. A štítok hodnoty sa umiestni pomocou rovnakej výšky odvodenej z ValueToY, akú používa stĺpec, takže bude sedieť priamo nad ním namiesto toho, aby visel vo vzduchu len v nejakom hrubom (uhádnutom) ofsete. Pokiaľ budete vyžadovať nakresliť horizontálne čiary mriežky poza stĺpce grafu, nakreslite ich ešte pred cyklom: zoberte si tri alebo štyri okrúhle hodnoty, na každú z nich aplikujte ValueToY a naprieč grafom na danej súradnici Y nakreslite tenkú čiaru. Ich skorším nakreslením ich presuniete v stohovacom modeli (ktorý PDF používa) do pozadia priamo poza stĺpce

Osi, značky (ticks) a štítky sú len ďalšie čiary a text

Graf nie je dokončený, kým čitateľ nedokáže rozoznať, čo stĺpce znamenajú, a toto celé je prácou osi. Vertikálna os je jedna nakreslená čiara po ľavom okraji ohraničenia grafu s niekoľkými priradenými značkami a ich hodnotami. Znovu použite ValueToY, aby značky pristáli presne v rovnakej mierke akú používajú stĺpce, inak by stĺpec a jeho čiara mriežky nesúhlasili, čím by graf potichu klamal:

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;

Štítky (názvy) sú tým miestom, kde produkčné grafy zlyhávajú najčastejšie, pričom chyba býva vždy tá istá: text, ktorý sa zmestil na obrazovku, po príchode do PDF pretečie cez hranice svojho priestoru. Dlhé názvy kategórií sa zrazia so svojimi susedmi a lokalizované názvy mesiacov (napríklad francúzsky "septembre" alebo nemecký "Dezember") sú širšie ako anglický "Sep", s ktorým ste ich testovali. Neexistuje tu žiadne automatické prispôsobenie veľkosti (autosize), ktoré by vás zachránilo, preto pod základnou líniou nechajte skutočnú rezervu. Ak sú názvy naozaj dlhé, natočte ich. TextOut prijíma uhol ako svoj tretí argument v zápise, vďaka čomu vám hodnota 90 otočí text vertikálne (nastojato) a ušetrí vám priestor bez prekrývania. Pred vydaním exportu do sveta vždy otestujte rozloženie s vaším najširším očakávaným štítkom, nie tým najkratším

Čiarové grafy: jedna lomená čiara (polyline) cez mapované body

Čiarový graf znovu využíva celé to mapovanie hodnôt a mení v skutočnosti iba spôsob, ako sa dané body vzájomne prepoja. Namiesto obdĺžnika na jednotlivú kategóriu, vy raz prejdete dáta (od začiatku), prevediete každú hodnotu na jej (X, Y) pomocou ValueToY a pospájate body pomocou jediného volania MoveTo, nasledovaného volaniami LineTo, a na konci nakreslíte celú cestu cez Stroke. Prvý bod cestu otvára, a každý ďalší bod ju rozš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)        // 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;

Všimnite si rozdiel v rozostupoch. Stĺpcový graf vydelí šírku počtom stĺpcov, pretože každý stĺpec vlastní svoj slot. Čiarový graf delí na základe počtu intervalov (čiže Count - 1), pretože prvý a posledný bod sedia na okrajoch grafu a čiara pokrýva medzery medzi nimi. Zmätok v týchto dvoch postupoch je zvyčajne hlavným dôvodom, prečo je čiarový graf od stĺpcového grafu (s ktorým sa má prekrývať) posunutý bokom o polovicu slotu. Ak pri každom údajovom bode chcete nejakú značku, za nakreslením lomenej čiary pridajte ku každému bodu (X, Y) malý Circle a Fill

Koláčové grafy: oblúky alebo kliny (výseče), ak to chcete udržať jednoduché

Kúsky koláča (koláčové výseče) sú tým jediným tvarom, kde naozaj potrebujete trigonometriu, pretože klin (výseč) je ohraničený dvoma polomermi a oblúkom. Poctivá verzia prechádza oblúkom tak, že po obvode krok za krokom prikladá malé čiarové segmenty, čo túto krivku aproximuje tak tesne, že žiaden čitateľ nedokáže rozoznať rozdiel. Smerujúci uhol pre jeden kúsok je jeho podiel z celku, (Value / Total) * 2π, a tento postupný uhol hromadíte počas toho, ako idete dookola:

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;

Cesta zo stredu von, najprv vrchol (špička výseče), potom oblúk, a späť k vrcholu, poskytuje uzavretý klin (výseč), ktorý Fill vyplní jednoliato. Počet segmentov vymieňa hladkosť za veľkosť trasy: zhruba 2 stupne na jeden krok vyzerajú okrúhlo pri akomkoľvek rozumnom polomere a zároveň nevyprodukujú obrovský dátový prúd. Pokiaľ nepotrebujete skutočný kruh, môžete úplne preskočiť celú trigonometriu a nakresliť tieto isté dáta ako jediný stĺpec poskladaný horizontálne, pričom šírka každého segmentu v ňom bude priamo úmerná na základe svojho podielu. Bežne to napokon býva pre používateľa čitateľnejšie než nejaký koláč, a pre vás je to len zrecyklovaný kód zo stĺpcového grafu, kde sa obdĺžniky iba pospájali/položili za sebou. Pre verziu obsahujúcu kruh a oblúky siahnite iba s úmyslom, pri ktorom dizajn vyžaduje aplikovať skutočný kruh

Nič z tohto nezávisí od inštalácie akejkoľvek knižnice pre grafy, čo je tichou výhodou priameho kreslenia primitív. Rovnaké volania na kreslenie plátna, aké umiestňujú logo alebo pole pre podpis, stavajú aj tieto grafy a ten istý TextOut, akým sa popisuje pole formulára, tu pomenúva os. Uložte (vložte) geometriu grafu do záznamu (typu record), namapujte si hodnoty na súradnicu Y a urobte to len raz; a potom stĺpcový, čiarový alebo koláčový graf je v tomto kontexte už len drobnou krátkou rutinou s Rectangle, LineTo a Circle, ktorú môžete začleniť/vložiť/umiestniť do akéhokoľvek reportu. Tu použité volania Rectangle, MoveTo, LineTo, Circle, Fill, Stroke a TextOut sú neoddeliteľnou súčasťou komponentu HotPDF určeného pre Delphi a C++Builder