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; // body PDF, počátek vlevo dole
MaxValue: Single; // horní hranice škály hodnot
end;
// Namapujte datovou hodnotu na její Y souřadnici uvnitř výkresu; Y roste vzhůru.
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; // čtvrtslotová mezera na každé straně
BarW := SlotW - Gap;
// Základní linka (osa X) podél spodní hrany výkresu.
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;
// Popisek kategorie pod základní linkou, hodnota nad sloupcem.
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); // krátká značka vně osy
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) // otevřete cestu v prvním bodě
else
Page.LineTo(X, Y); // prodlužte ji přes každý další bod
end;
Page.Stroke; // jeden tah vykreslí celou lomenou čáru
end;
Všimněte si rozdílu v rozestupech. Sloupcový graf dělí šířku počtem sloupců, protože každý sloupec vlastní jeden slot. Čárový graf naopak dělí počtem intervalů, Count - 1, protože první a poslední bod sedí na okrajích zobrazení a čára překlenuje mezery mezi nimi. Popletení těchto dvou variant je nejčastějším důvodem, proč čárový graf o půl slotu uskočí od sloupcového grafu, který má překrývat. Pokud chcete na každém datovém bodě značku, položte po vytažení lomené čáry na každou souřadnici (X, Y) malou Circle a aplikujte Fill
Koláčové grafy: oblouky nebo výseče (wedges), pokud to ponecháte jednoduché
Výseče koláčového grafu jsou jediným tvarem, který vyžaduje trigonometrii, protože klín je ohraničen dvěma poloměry a obloukem. Poctivá varianta opisuje oblouk drobnými úsečkami kráčejícími podél obvodu, což aproximuje křivku dostatečně věrně, aby ji od oka nepoznal žádný čtenář. Úhel oblouku každé výseče je její podíl z celku, (Value / Total) * 2π, a průběžný úhel kumulujete, jak se kolem dokola posouváte:
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 stupně na segment
Page.SetRGBFillColor(Colors[I]);
Page.MoveTo(CX, CY); // hrot výseče uprostřed
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); // uzavřete zpět do středu
Page.Fill;
Start := Start + Sweep; // přejděte k další výseči
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 toho nezávisí na žádné nainstalované knihovně pro grafy, což je tichá výhoda přímého kreslení primitiv. Stejná volání kreslení na plátno, která umísťují logo nebo box pro podpis, postaví i tyto grafy a stejný TextOut, který popisuje formulářové pole, popíše i osu. Uložte geometrii zobrazení do záznamu, namapujte hodnoty na Y jednou a sloupcový, čárový i koláčový graf je krátká rutina nad Rectangle, LineTo a Circle, kterou můžete vložit do libovolného reportu. Volání Rectangle, MoveTo, LineTo, Circle, Fill, Stroke a TextOut použité v tomto článku jsou součástí HotPDF Delphi Component pro Delphi a C++Builder