Technický článek

Vykreslení grafů v PDF pomocí primitiv HotPDF

HotPDF neobsahuje žádný objekt pro grafy. Neexistuje tu žádný TPDFChart, žádné volání typu AddBarSeries, prostě nic, co by přijalo pole čísel a vydalo vyrenderovaný graf. To, co namísto toho dostáváte, je stránka s kreslicím plátnem se stejným nízkoúrovňovým slovníkem, který používá každý kreslicí model v PDF: obdélníky, čáry, kruhy, výplně, tahy a text umísťovaný na naprosto přesné souřadnice. Graf v dokumentu HotPDF je tak něco, co musíte sami sestavit, ne něco, o co si zkrátka zažádáte. Zní to složitěji, než jaké to ve skutečnosti je. Jakmile si jednou napíšete tu správnou souřadnicovou matematiku, sloupcový graf je jen smyčka přes obdélníky, čárový graf je lomená čára (polyline) a koláčový graf je vějíř složený z oblouků, přičemž vy sami kontrolujete u takového výsledku úplně každý pixel

Záleží na tom proto, že alternativa, po které lidé sáhnou jako první (rastrovaný ovládací prvek z obrazovky uložený do bitmapy a vložený coby obrázek na stránku), vám dá graf uzamčený v obrazovkovém rozlišení, který se vytiskne rozmazaně a zbytečně soubor nafoukne. Kreslení grafu pomocí vektorových primitiv HotPDF udržuje výstup ostrý v libovolném přiblížení i v jakémkoli tiskovém DPI, protože sloupce a osy jsou pak ve skutečnosti čisté operátory cesty (path operators) v PDF a nikoli jen pixely. Cenou za to však je, že musíte mít plně ve svých rukou rozvržení stránky (layout). Samotná mechanika k tomu se scvrkává jen na hrstku kroků: jedno překlopení souřadnic, na kterém se každý nachytá, odpracovaný sloupcový graf, trik s lomenou čárou u čárových grafů a nakonec s matematikou pro výpočet kruhových výsečí (plátků koláče)

Jediná těžká část leží u souřadnicového systému

Počítačová grafika klade počátek do levého horního rohu a Y se zvětšuje směrem dolů. PDF dělá pravý opak. Počátek sedí v levém dolním rohu stránky a Y roste vzhůru, přičemž se měří v bodech (1/72 palce). Každé volání pro vykreslení v HotPDF – TextOut, Rectangle, MoveTo, LineTo i Circle – využívá tuto konvenci se spodem vlevo a narůstajícím Y vzhůru. Pokud sem přenesete instinkty z grafiky pro obrazovky, váš první graf se nakreslí vzhůru nohama a vyteče mimo spodní okraj stránky

Tím pádem leží ta skutečná námaha u každého grafu v jednom mapování: přeměnit hodnotu dat na Y souřadnici, která respektuje nárůst Y vzhůru. Stanovte si obdélník pro zobrazení grafu (plot rectangle), neboli čtyři čísla reprezentující plochu, ve které bude graf žít, a poté namapujte nejmenší datovou hodnotu na spodní hranu a tu nejvyšší na horní. Pro sloupec s hodnotou V na škále, která probíhá od 0 po MaxValue, platí, že vrchní hrana sloupce bude PlotBottom + (V / MaxValue) * PlotHeight, a samotný sloupec vyroste od PlotBottom. Zvládněte tento jeden jediný matematický výraz správně a veškerý ten zbytek se stane už jen obyčejnou účtařinou (bookkeeping). Pomocná funkce (helper) níže drží geometrii zobrazení a provádí konverzi tak, aby se kreslicí kód už podruhé nikdy nemusel dotýkat surové 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;

Jeden z odhadů se skrývá v hodnotě MaxValue. Pokud ji nastavíte přesně na největší datový bod, nejvyšší sloupec se dotkne horní hrany grafu a bude vypadat oříznutě. Zaokrouhlete ji nahoru na čisté číslo nad daným maximem, řekněme na další násobek deseti nebo sta, takže graf získá rezervu (headroom) a štítky s hodnotami mřížky budou čitelné v celých (kulatých) číslech, namísto těch, na kterých data zrovna dosáhla svého maxima

Sloupcový graf je smyčka přes obdélníky

Jakmile je mapování vyřešeno, sloupcový graf se napíše v podstatě sám. Rozdělte šířku grafu na jeden slot (okénko) na kategorii, ponechte mezi sloupci určitou mezeru tak, aby se nedotýkaly, a nakreslete každý sloupec jako vyplněný obdélník, jehož výška pochází z metody ValueToY. Metoda Rectangle komponenty HotPDF přijímá levý dolní roh spolu se šířkou a výškou, což se dokonale sladí se sloupcem, který roste směrem vzhůru od své základní linky (baseline). Nastavte barvu výplně, sestavte cestu a následně ji vykreslete zavoláním Fill. Označení kategorie se nachází pod základní linkou a samotná hodnota naopak leží nad sloupcem:

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 z toho si plně zaslouží svou pozornost. Parametr Gap (mezera) tvoří zlomek ze slotu (okénka) a nikoliv fixně daný počet bodů, takže sloupce si zachovávají proporcionální odstupy bez ohledu na to, zda zakreslujete kategorie čtyři, nebo čtyřicet. Dále je popisek s hodnotou umístěn u výšky odvozené ze stejné funkce ValueToY, kterou používá i samotný sloupec, díky čemuž pak bude popisek ukotven těsně nad svým vlastním sloupcem, místo aby jen volně plul s využitím zkusmo odhadnutého offsetu. Pokud si přejete vodorovné linie (mřížku) za sloupci, vykreslete je před samotnou smyčkou: vyberte si tři nebo čtyři kulaté hodnoty, na každou uplatněte ValueToY a danou Y souřadnicí veďte přes graf matnou linku. Kreslením čar jako prvních se tyto ocitnou ve skládání (stackingu), jež v PDF funguje na základě malířského modelu (painter's-model), bezpečně v pozadí (za sloupci)

Osy, značky (ticks) a štítky jsou prostě jen další linky a text

Graf není hotov, dokud čtenář nedokáže jasně poznat, co dané sloupce znamenají, a to je plně v kompetenci práce u os. Svislou osu tvoří jediná čára tažená nahoru podél levého okraje zobrazení, doplněná několika značkami (ticks) a jim odpovídajícími hodnotami. Opět využijte funkce ValueToY, aby značky spadaly do stejného měřítka, které využívají sloupce; v opačném případě pak mezi sebou sloupec a jeho odpovídající mřížka začnou hrubě nesouhlasit, čímž bude graf v tichosti lhát:

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;

Zádrhely spojené s popisky jsou to, na čem grafy v produkci selhávají nejčastěji, a princip selhání je pokaždé stejný: text, který se bez problémů vešel na obrazovku, přeteče vymezené místo v PDF dokumentu. Dlouhá jména kategorií začnou narážet do svých sousedů a lokalizované názvy měsíců jako „septembre“ nebo „Dezember“ vyžadují více prostoru do šířky než vámi testované kratší anglické „Sep“. Před touto hrozbou vás tu ovšem nezachrání žádná funkce autosize, proto ponechte pod základní linií dostatečný reálný okraj. U hustých kategorií smrskněte písmo o bod nebo dva dolů, a v případech, kdy jsou názvy opravdu dlouhé, text rovnou zrotujte. Příkaz TextOut přijímá jako třetí argument úhel, takže předání úhlu 90 postaví popisek na konec a získá vám tolik potřebný prostor i bez překryvů (overlap). Před odevzdáním finálního exportu do produkce otestujte rozvržení se svým absolutně nejširším očekávaným popiskem, nikoliv s tím nejkratším

Čárové grafy: jedna složená lomená čára (polyline) napříč mapovanými body

Čárový graf opakovaně využívá celé mapování hodnot a mění se pouze to, jak se body (points) spojují. Místo abyste vykreslovali obdélník pro každou kategorii, projdete data pouze jednou, zkonvertujete každou hodnotu na její podobu (X, Y) s funkcí ValueToY, a tyto body sešijete dohromady pomocí jednoho MoveTo s následným voláním LineTo, a úplně nakonec to celé vykreslíte jedním tahem (stroked). První bod začne s budováním u cesty a každý pozdější bod u trasy ji nadále prodlužuje (extends):

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šimněte si rozdílů u mezer. Sloupcový graf dělí šířku na počet sloupců, protože každý ze sloupců vlastní jeden slot. Čárový graf naopak dělí dle počtu intervalů Count - 1, a to z toho důvodu, že ten úplně první a úplně poslední bod sedí na obou okrajích zobrazení grafu a čára tak (pouze) překlenuje ony zbylé mezery mezi nimi. Pokud si to ve výpočtu náhodou popletete a obě varianty nechtěně promícháte, je to většinou ta hlavní příčina toho, proč čárový graf v PDF dokumentu v tichosti ustřelí a o půl slotu se u vizualizace grafů (čárového) „rozejde“ mimo zarovnání (drifts) vůči případnému sloupcovému (bar chart) grafu, jež by přitom svými hodnotami měl na chlup přesně jakožto krycí křivka bez odchylek vizuálně u přesahů překrývat (overlay). Pokud u zobrazení zatoužíte po značce (markeru) na každém bodu s hodnotou, upusťte po provedení finálního tahu lomené čáry přes graf jednoduše objekt s malým kruhem Circle a vyplňte ho výplní z metody Fill (vždy nad přesně každý od zapsaných a odpočítaných párovaných křižovatek s parametry (X, Y) z vašeho datového cyklu bodů z polí s grafickými datovými úkony)

Koláčové grafy: oblouky nebo výseče (wedges), pokud to ponecháte jednoduché

Výseče (plátky) koláčového grafu jsou na rozdíl od všech předchozích tím jediným tvarem z obrazců u grafů, který vyžaduje nasazení trigonometrie, protože klínek od výseče k podílu je u obálky ohraničen vždy svými dvěma poloměry na osách od středu a zvenčí jedním zaobleným obloukem. Ta nejpoctivější varianta ke kódování simuluje vnější oblouk drobnými, těsně k sobě poskládanými liniovými segmenty krokovaně podél obvodu (kružnice), což se po okrajích ve stavbě kruhového lomení (PDF dokumentů od HotPDF z kódů po Delphi testování pro reportní zkušební tabulkové grafové vizualizace pro kódy v programování) těsně přiblíží oné pravé zvažované přesné hladké oblé křivce s poloměrem na dotek skutečně natolik, že tu drobnou zástupnou rovnou chytrou lež ohledně tvaru křivek (od zaoblení na okrajích a lomech do tvarů pro koláčové grafické komponentní modely) prostě od oka téměř žádný čtenář neprohlédne (no reader can tell). Úhel z rozsahu u oblouku od jednotlivé ukrojené porce tvoří pochopitelně její vlastní datový dílek zastoupení podílu u rozložení v podílu vůči od celku z dat ((Value / Total) * 2π) a s dalším posunem kolem okraje grafu si k výseči vy v celém kódování už vždy jen načítáte ten celkový stoupající aktuální sčítaný průběžný sbíhající obvodový propočítaný úhel pro nárůsty v celkových proporcích od úhlů (running angle as you go around):

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;

Středová cesta, hrot nejprve, pak oblouk, pak zpět na hrot, dává uzavřenou výseč, kterou Fill (výplň) nakreslí celistvě. Počet segmentů vyměňuje hladkost za velikost cesty: zhruba dva stupně na jeden krok vypadají u jakéhokoli smysluplného poloměru kulatě, aniž by vytvořily obrovský proud obsahu. Pokud nepotřebujete skutečný kruh, můžete trigonometrii zcela vynechat a stejná data vykreslit jako jeden vodorovný skládaný sloupkový graf (horizontal stacked bar), šířka každého segmentu pak odpovídá úměrně jeho podílu. To je často beztak čitelnější než koláčový graf, a jedná se pouze o sloupcový graf (bar code) s obdélníky položenými koncem k sobě. Pro verzi s obloukem sáhněte pouze tehdy, když design vyžaduje skutečný kruh

Nic z tohoto nezávisí na žádné instalované knihovně pro grafy, což je přesně tou tichou výhodou z přímého kreslení primitiv. Ta samá volání přes kreslicí plátno, přes která se usazuje logo či box pro podpis, vybudují také tyto grafy, a stejně tak TextOut, jenž v reportu nadepíše formulářové pole, spolehlivě nadepíše a popíše (oštítkuje) i jakoukoliv osu v grafu. Uzavřete si geometrii ke zobrazení (plot geometry) do datového proměnného strukturovaného záznamu u record, namapujte hodnoty na Y souřadnici pouze jedinkrát hned ze startu, a následně (pak) jakýkoli ze sloupcových, čárových či koláčových grafů začne reprezentovat v kódu pouze obyčejnou velice odlehčenou snadnou úspornou programovou smyčkovou sekvenci (rutinu) s opakováním z volání nad rozhraními přes prvky obálek u volaných hodnot k proměnným za metody Rectangle, LineTo a Circle, kterou z kódu tak s hravým uvolněním pak následně bleskově plynule a s jistotou snadně volně vnoříte s generátorem rovnou libovolně vůbec a zcela do úplně každého a naprosto kteréhokoli libovolného z reportů ("drop into any report"). Uvedená volání od Rectangle, MoveTo, LineTo a Circle, spouštěcí plnicí operace Fill, dokončená obrysová čára s okrajovou stopou u Stroke a finálně sázecí metoda pro formátování znaků TextOut tvoří začleněnou součást obsáhlé PDF výbavy HotPDF Component pro Delphi a C++Builder