A HotPDF nem rendelkezik diagram objektummal (chart object). Nincs TPDFChart, nincs AddBarSeries, semmi olyan, ami felvesz egy számtömböt, és visszaad egy renderelt (rendered) grafikont. Amit ehelyett ad Önnek, az egy oldalvászon (page canvas) ugyanazzal az alacsony szintű (low-level) szótárral (vocabulary), amelyet minden PDF rajzolási modell (drawing model) használ: téglalapok (rectangles), vonalak (lines), körök (circles), kitöltések (fills), körvonalak (strokes) és pontos koordinátákon elhelyezett szövegek. Egy diagram a HotPDF dokumentumban tehát olyasmi, amit Ön felépít (build), nem pedig olyasmi, amit kér (request). Ez több munkának hangzik, mint amennyi valójában. Miután egyszer megírta a koordinátamatematikát, egy oszlopdiagram (bar chart) egy ciklus (loop) a téglalapokon, egy vonaldiagram (line chart) egy vonallánc (polyline), egy tortadiagram (pie chart) pedig ívek legyezője (fan of arcs), és az eredmény minden egyes pixelét (pixel) Ön irányítja
Ez azért számít (matters), mert az az alternatíva (alternative), amihez az emberek először nyúlnak, vagyis egy képernyődiagram (screen chart) vezérlő (control) bittérképpé (bitmap) történő raszterizálása és a kép beillesztése az oldalra (pasting the image into the page), egy olyan diagramot ad, amely a képernyőfelbontáshoz (screen resolution) van rögzítve, elmosódottan (fuzzy) nyomtatódik, és felfújja (bloats) a fájlt. Ha a diagramot a HotPDF vektoros primitívjeivel (vector primitives) rajzolja meg, a kimenet éles (crisp) marad bármilyen nagyításnál (zoom) és bármilyen nyomtatási DPI-nél, mert a sávok (bars) és a tengelyek (axes) valódi PDF útvonal-operátorok (path operators), nem pedig pixelek. Az ára (cost) az, hogy Ön birtokolja (own) az elrendezést (layout). A mechanika (mechanics) néhány lépésre (moves) vezethető vissza: az egyetlen koordináta-átfordításra (coordinate flip), amely mindenkit megfog, egy kidolgozott (worked) oszlopdiagramra, a vonallánc trükkre (polyline trick) a vonaldiagramokhoz, és az ívmatematikára (arc math) a tortaszeletekhez (pie slices)
Az egyetlen nehéz rész a koordináta-rendszer
A képernyőgrafika (Screen graphics) az origót (origin) a bal felső sarokba (top-left) teszi, az Y pedig lefelé növekszik (growing downward). A PDF pont az ellenkezőjét teszi. Az origó az oldal bal alsó (bottom-left) sarkában helyezkedik el, és az Y felfelé (upward) növekszik, pontokban (points) mérve (1/72 hüvelyk). A HotPDF minden rajzolási hívása, a TextOut, Rectangle, MoveTo, LineTo, Circle ezt a bal alsó, Y-felfelé konvenciót használja. Ha Ön áthozza a képernyőgrafikai (screen-graphics) ösztöneit, az első diagramja fejjel lefelé (upside down) rajzolódik ki, és lefut (runs off) az oldal aljáról
Tehát a valódi munka minden diagramban egyetlen leképzés (mapping): alakítson át egy adatértéket (data value) egy olyan Y koordinátává, amely tiszteletben tartja (respects) az Y-felfelé elvet. Döntsön egy ábrázolási téglalapról (plot rectangle), négy számról, amely azon területet határozza meg, amelyen a diagram él, majd képezze le (map) az adatainak legkisebb értékét az alsó élre (bottom edge), a legnagyobbat pedig a felsőre (top). Egy 0-tól MaxValue-ig terjedő (runs) skálán egy V értékű sáv (bar) felső éle (top edge) PlotBottom + (V / MaxValue) * PlotHeight, és a sáv (bar) a PlotBottom-tól növekszik felfelé (grows up). Ha ez az egy kifejezés rendben van, minden más már csak könyvelés (bookkeeping). Az alábbi segéd (helper) tartalmazza az ábrázolási geometriát (plot geometry) és elvégzi a konverziót, így a rajzoló kód (drawing code) soha nem érinti kétszer a nyers aritmetikát (raw arithmetic):
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;
Egy mérlegelési döntés (judgement call) bújik meg a MaxValue-ban. Ha a pontosan legnagyobb adatpontra (largest data point) állítja be, a legmagasabb sáv (tallest bar) megérinti az ábra (plot) felső szélét (top edge), és levágottnak (clipped) tűnik. Kerekítse fel (Round it up) egy tiszta számra a maximum fölé, mondjuk a következő 10-es vagy 100-as többszörösre, hogy a diagramnak legyen egy kis extra magassága (headroom), és a rácsvonalcímkék (gridline labels) kerek számokként (round figures) olvashatók legyenek, nem pedig akármiként, amiben az adat éppen tetőzött (happened to peak at)
Az oszlopdiagram egy ciklus a téglalapokon
A leképzés rendezésével (With the mapping settled) az oszlopdiagram magát írja meg. Ossza fel a plot szélességét kategóriánként egy rekeszre (slot), hagyjon egy rést (gap) a sávok (bars) között, hogy ne érjenek egymáshoz, és rajzoljon meg minden sávot kitöltött téglalapként (filled rectangle), melynek magassága a ValueToY-ból származik. A HotPDF Rectangle-je felveszi a bal alsó sarkot (lower-left corner), plusz egy szélességet és egy magasságot, ami pontosan egy vonalba esik (lines up) egy olyan sávval, amely az alapvonaltól (baseline) felfelé nő. Először állítsa be a kitöltési színt (fill color), fektesse le az útvonalat (path), majd hívja meg a Fill-t a megfestéséhez (paint). A kategóriacímke (category label) az alapvonal alá kerül, az érték pedig a sáv (bar) fölé:
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;
Két részlet megéri az árát (earn their keep). A Gap a rekesz (slot) egy töredéke, nem pedig egy rögzített (fixed) pontszám, így a sávok (bars) arányosan elosztva (proportionally spaced) maradnak, függetlenül attól, hogy négy vagy negyven kategóriát ábrázol (plot). Az értékcímkét (value label) pedig ugyanazzal a ValueToY-ból származtatott (derived) magassággal pozicionáljuk, amit a sáv is használ, így mindig pontosan a saját sávja (bar) felett ül (sits), ahelyett, hogy egy megtippelt eltoláson (guessed offset) lebegne (floating). Ha vízszintes rácsvonalakat (horizontal gridlines) szeretne a sávok mögé, rajzolja meg őket a ciklus előtt: válasszon ki három vagy négy kerek értéket, futtassa le mindegyiken a ValueToY-t, és húzzon egy halvány vonalat (faint line) az ábrán (plot) annál az Y-nál. Ha ezeket rajzolja meg először, akkor a sávok mögé kerülnek a PDF által használt festőmodell-rétegzésben (painter's-model stacking)
A tengelyek, a beosztások és a címkék csupán további vonalak és szövegek
A diagram nincs kész, amíg egy olvasó meg nem tudja mondani, mit jelentenek az oszlopok (bars), és ez teljes egészében a tengelyek (axis) munkája. A függőleges tengely egy megrajzolt (stroked) vonal a plot bal szélén felfelé (up the left edge), egy maréknyi (handful) beosztásjelöléssel (tick marks) és azok értékeivel. Használja újra a ValueToY-t, hogy a beosztások (ticks) ugyanarra a skálára érkezzenek, amit a sávok (bars) használnak, különben egy sáv és a rácsvonala nem fognak egyezni (disagree), és a diagram csendben hazudni fog (lies quietly):
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;
A címkék (Labels) azok, ahol a diagramok a leggyakrabban eltörnek (break) a termelésben (production), és a hiba mindig ugyanaz: a képernyőre illeszkedő szöveg túllóg (overruns) a helyén a PDF-ben. A hosszú kategórianevek ütköznek (collide) a szomszédjaikkal, a lokalizált (localized) hónapnevek (month names), mint a "septembre" vagy a "Dezember" szélesebbek, mint az angol "Sep", amivel tesztelt. Itt nincs automatikus méretezés (autosize), ami megmenthetné, ezért hagyjon valódi margót az alapvonal alatt, zsugorítsa (shrink) a betűtípust egy-két ponttal a sűrű kategóriahalmazoknál, és ha a nevek valóban hosszúak, forgassa (rotate) el őket. A TextOut harmadik argumentumként egy szöget (angle) vár, így 90 átadásával a címke feláll (stands on end), és helyet nyer (buys you room) átfedés (overlap) nélkül. Tesztelje (Test) az elrendezést (layout) a várható legszélesebb (widest) címkével, ne a legrövidebbel, mielőtt az export kikerül a termelésbe (ships)
Vonaldiagramok: egy vonallánc leképzett pontokon keresztül
Egy vonaldiagram (line chart) újra felhasználja a teljes értékleképzést (value mapping), és csak azt változtatja meg, ahogyan a pontok összekapcsolódnak. Ahelyett, hogy kategóriánként egy téglalap (rectangle per category) lenne, egyszer végigmegy (walk) az adatokon, minden értéket átalakít a saját (X, Y) koordinátájára a ValueToY segítségével, és összevarrja (stitch) a pontokat egyetlen MoveTo hívással, amit LineTo hívások követnek, majd a végén körvonalazza (stroked) azokat. Az első pont megnyitja (opens) az útvonalat (path); minden későbbi (later) pont kiterjeszti (extends) azt:
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;
Figyelje meg a térközökbeli (spacing) különbséget. Egy oszlopdiagram a szélességet a sávok (bars) számával osztja el, mert minden sáv egy rekeszt (slot) birtokol. A vonaldiagram az intervallumok (intervals) számával oszt, Count - 1, mivel az első és az utolsó pont (first and last points) a plot szélein (plot edges) ül, és a vonal átfogja (spans) a köztük lévő hézagokat (gaps). E kettő összekeverése (Mixing those two up) a szokásos (usual) oka annak, hogy egy vonaldiagram (line chart) egy fél rekesszel (half-slot) elsodródik (drifts) attól az oszlopdiagramtól, amelyet át kellene fednie (meant to overlay). Ha minden adatponthoz (data point) jelölőt (marker) szeretne, dobjon egy kis Circle-t és Fill-t minden (X, Y) koordinátára, miután a vonallánc (polyline) megrajzolásra került
Tortadiagramok: ívek, vagy ékek, ha egyszerűre veszi
A tortaszeletek (Pie slices) jelentik azt az egyetlen alakzatot, amelyhez trigonometria (trigonometry) szükséges, mivel egy éket (wedge) két sugár (radii) és egy ív (arc) határol. Az őszinte (honest) verzió úgy söpri végig (sweeps) az ívet, hogy kis vonalszakaszokat (line segments) léptet a kerület (circumference) mentén, ami elég közelíti a görbét (curve) ahhoz, hogy az olvasó ne vegye észre (no reader can tell). Minden szelet söprési szöge (sweep angle) az ő részesedése az összesből, (Value / Total) * 2π, és Ön felhalmozza (accumulate) a futó szöget (running angle), ahogy körbemegy:
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;
A középpontból kifelé induló útvonal (center-out path), először a csúcs (apex), majd az ív (arc), majd vissza a csúcshoz, egy zárt éket (closed wedge) ad, amelyet a Fill tömörré (solidly) fest. A szegmensszám (segment count) elcseréli a simaságot (smoothness) az útvonal méretére (path size): lépésenként (step) nagyjából két fok (two degrees) kereknek tűnik (looks round) bármilyen ésszerű (sensible) sugár (radius) esetén anélkül, hogy egy hatalmas tartalomfolyamot (content stream) hozna létre. Ha nincs szüksége valódi körre (true circle), a trigonometriát teljesen kihagyhatja, és ugyanazt az adatot egyetlen vízszintes halmozott sávként (horizontal stacked bar) renderelheti, ahol minden szegmens szélessége (width) arányos (proportional) a részesedésével. Ez amúgy is gyakran olvashatóbb, mint egy torta (pie), és ez csak a vonalkód egymás végéhez fektetett téglalapokkal. Csak akkor nyúljon az ív verzióhoz, amikor a dizájn valódi kört (actual circle) kíván
Mindez (None of this) nem függ attól, hogy bármilyen diagramkönyvtár (chart library) telepítve van-e, ami a primitívek (primitives) közvetlen rajzolásának csendes előnye (quiet advantage). Ugyanazok a vászon rajzolási hívások (canvas drawing calls) építik fel (build) ezeket a diagramokat (charts), amelyek egy logót (logo) vagy egy aláírási dobozt (signature box) helyeznek el, és ugyanaz a TextOut feliratoz egy tengelyt, amely feliratoz egy űrlapmezőt (form field) is. Tegye a plot geometriáját (plot geometry) egy rekordba (record), képezze le az értékeket az Y-ra egyszer, és egy oszlop- (bar), vonal- (line) vagy tortadiagram (pie chart) csupán egy rövid rutin (short routine) a Rectangle, LineTo és Circle felett, amelyet bármilyen jelentésbe bedobhat (drop into). Az itt használt Rectangle, MoveTo, LineTo, Circle, Fill, Stroke és TextOut hívások a HotPDF Component részei Delphi és C++Builder esetében