HotPDF har ikke noget diagramobjekt. Der er ingen TPDFChart, ingen AddBarSeries, intet, der tager et array af tal og leverer en gengivet graf tilbage. Hvad det giver dig i stedet er et sidelærred med det samme ordforråd på lavt niveau, som enhver PDF-tegnemodel bruger: rektangler, linjer, cirkler, fyld, streger og tekst placeret ved nøjagtige koordinater. Et diagram i et HotPDF-dokument er derfor noget, du bygger, ikke noget, du anmoder om. Det lyder som mere arbejde, end det er. Når du først har skrevet koordinatmatematikken én gang, er et søjlediagram en løkke over rektangler, et linjediagram er en polylinje, og et cirkeldiagram er en vifte af buer, og du styrer hver pixel af resultatet
Dette har betydning, fordi det alternativ, folk rækker ud efter først – at rasterisere en skærmdiagramkontrol til et bitmap og indsætte billedet på siden – giver dig et diagram låst til skærmopløsning, som udskrives sløret og puster filen op. At tegne diagrammet med HotPDFs vektorprimitiver holder outputtet skarpt ved enhver zoom og enhver udskrifts-DPI, fordi søjlerne og akserne er rigtige PDF-stioperatorer, ikke pixels. Omkostningen er, at du ejer layoutet. Mekanikken koger ned til nogle få træk: den ene koordinatvending, der fanger alle, et gennemarbejdet søjlediagram, polylinje-tricket til linjediagrammer og buematematikken til cirkelskiver
Den eneste svære del er koordinatsystemet
Skærmgrafik placerer origo øverst til venstre, hvor Y vokser nedad. PDF gør det modsatte. Origo sidder i nederste venstre hjørne af siden, og Y vokser opad, målt i punkter (1/72 tomme). Hvert tegnekald i HotPDF – TextOut, Rectangle, MoveTo, LineTo, Circle – bruger den nederste venstre, Y-op-konvention. Hvis du overfører skærmgrafik-instinkter, tegner dit første diagram på hovedet og løber ud over bunden af siden
Så det virkelige arbejde i ethvert diagram er én kortlægning: omsæt en dataværdi til en Y-koordinat, der respekterer Y-op. Beslut dig for et plotrektangel, fire tal for det område diagrammet lever i, og kortlæg derefter den mindste værdi i dine data til den nederste kant og den største til toppen. For en søjle med værdien V på en skala, der går fra 0 til MaxValue, er søjlens øverste kant PlotBottom + (V / MaxValue) * PlotHeight, og søjlen vokser op fra PlotBottom. Få det ene udtryk rigtigt, og alt andet er bogholderi. Hjælperen nedenfor indeholder plotgeometrien og foretager konverteringen, så tegnekoden aldrig rører ved rå aritmetik to gange:
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;
Én skønsmæssig vurdering gemmer sig i MaxValue. Hvis du sætter den til det nøjagtige største datapunkt, rører den højeste søjle den øverste kant af plottet og ser afskåret ud. Rund den op til et rent tal over maksimum, f.eks. det næste multiplum af 10 eller 100, så diagrammet har plads over sig, og gitterlinjeetiketterne læses som runde tal i stedet for det, dataene tilfældigvis toppede ved
Et søjlediagram er en løkke over rektangler
Når kortlægningen er på plads, skriver et søjlediagram sig selv. Opdel plotbredden i én plads pr. kategori, efterlad et mellemrum mellem søjlerne, så de ikke rører hinanden, og tegn hver søjle som et udfyldt rektangel, hvis højde kommer fra ValueToY. HotPDFs Rectangle tager det nederste venstre hjørne plus en bredde og en højde, hvilket passer nøjagtigt med en søjle, der vokser opad fra basislinjen. Indstil fyldfarven først, læg stien ned, og kald derefter Fill for at male den. Kategorietiketten går under basislinjen, værdien over søjlen:
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;
To detaljer tjener deres berettigelse. Gap er en brøkdel af pladsen i stedet for et fast punkttal, så søjlerne forbliver proportionelt fordelt, uanset om du plotter fire kategorier eller fyrre. Og værdi-etiketten er placeret med den samme ValueToY-afledte højde, som søjlen bruger, så den altid sidder lige over sin egen søjle i stedet for at flyde med en gættet forskydning. Hvis du vil have vandrette gitterlinjer bag søjlerne, skal du tegne dem før løkken: vælg tre eller fire runde værdier, kør ValueToY på hver, og streg en svag linje over plottet ved den Y-værdi. At tegne dem først placerer dem bag søjlerne i den maler-model-stabling (painter's-model stacking), som PDF bruger
Akser, mærker og etiketter er bare flere linjer og tekst
Diagrammet er ikke færdigt, før en læser kan se, hvad søjlerne betyder, og det er udelukkende akse-arbejde. Den lodrette akse er én streget linje op ad den venstre kant af plottet med en håndfuld mærker og deres værdier. Genbrug ValueToY, så mærkerne lander på den samme skala, som søjlerne bruger, ellers er en søjle og dens gitterlinje uenige, og diagrammet lyver i stilhed:
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;
Etiketter er dér, hvor diagrammer oftest går i stykker i produktion, og fejlen er altid den samme: tekst, der passede på skærmen, overskrider sin plads i PDF'en. Lange kategorinavne kolliderer med deres naboer, og lokaliserede månedsnavne som "septembre" eller "Dezember" er bredere end det engelske "Sep", du testede med. Der er ingen auto-størrelse (autosize) til at redde dig her, så efterlad reel margen under basislinjen, formindsk skrifttypen et punkt eller to for tætte kategorisæt, og hvis navnene er ægte lange, skal du rotere dem. TextOut tager en vinkel som sit tredje argument, så at passere 90 stiller etiketten på højkant og køber dig plads uden overlap. Test layoutet med din bredeste forventede etiket, ikke din korteste, før eksporten sendes afsted
Linjediagrammer: én polylinje gennem kortlagte punkter
Et linjediagram genbruger hele værdikortlægningen og ændrer kun, hvordan punkterne forbindes. I stedet for et rektangel pr. kategori gennemløber du dataene én gang, konverterer hver værdi til dens (X, Y) med ValueToY og syr punkterne sammen med et enkelt MoveTo efterfulgt af LineTo-kald, streget til sidst. Det første punkt åbner stien; hvert senere punkt forlænger den:
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;
Bemærk forskellen i afstandsfordeling. Et søjlediagram deler bredden med antallet af søjler, fordi hver søjle ejer en plads. Et linjediagram deler med antallet af intervaller, Count - 1, fordi det første og sidste punkt sidder på plotkanterne, og linjen spænder over hullerne mellem dem. At blande disse to sammen er den sædvanlige grund til, at et linjediagram driver en halv plads væk fra det søjlediagram, det er beregnet til at overlejre. Hvis du vil have en markør ved hvert datapunkt, skal du slippe en lille Circle og Fill ved hver (X, Y), efter polylinjen er streget
Cirkeldiagrammer: buer, eller kiler, hvis du holder det simpelt
Cirkelskiver er den ene form, der har brug for trigonometri, fordi en kile er begrænset af to radier og en bue. Den ærlige version fejer buen ved at lade små linjesegmenter træde langs omkredsen, hvilket tilnærmer kurven tæt nok til, at ingen læser kan se det. Hver skives fejevinkel er dens andel af det samlede, (Value / Total) * 2π, og du akkumulerer den løbende vinkel, mens du går rundt:
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;
Center-ud-stien, spids (apex) først, derefter buen, derefter tilbage til spidsen, giver en lukket kile, som Fill maler solidt. Segmentantallet bytter glathed ud med stistørrelse: cirka to grader pr. trin ser rundt ud ved en hvilken som helst fornuftig radius uden at producere en enorm indholdsstrøm. Hvis du ikke har brug for en ægte cirkel, kan du springe trigonometrien helt over og gengive de samme data som en enkelt vandret stablet søjle, hvor hvert segments bredde er proportional med dens andel. Det er ofte mere læseligt end et cirkeldiagram alligevel, og det er bare søjlekoden med rektanglerne lagt ende til ende. Grib kun efter bueversionen, når designet kræver en rigtig cirkel
None of this depends on any chart library being installed, which is the quiet advantage of drawing the primitives directly. The same canvas drawing calls that place a logo or a signature box build these charts, and the same TextOut that labels a form field labels an axis. Put the plot geometry in a record, map values to Y once, and a bar, line, or pie chart is a short routine over Rectangle, LineTo, and Circle that you can drop into any report. The Rectangle, MoveTo, LineTo, Circle, Fill, Stroke, and TextOut calls used here are part of the HotPDF-komponent for Delphi and C++Builder