HotPDF har ikke noe diagramobjekt. Det finnes ingen TPDFChart, ingen AddBarSeries, ingenting som tar et array med tall og gir deg et ferdig tegnet diagram tilbake. Det du får i stedet, er et sidelerret med det samme lavnivåvokabularet som enhver PDF-tegnemodell bruker: rektangler, linjer, sirkler, fyll, streker og tekst plassert i eksakte koordinater. Et diagram i et HotPDF-dokument er derfor noe du bygger, ikke noe du ber om. Det høres ut som mer arbeid enn det er. Når du først har skrevet koordinatmatematikken én gang, er et stolpediagram en løkke over rektangler, et linjediagram en polylinje og et sektordiagram en vifte av buer, og du styrer hver piksel av resultatet
Dette betyr noe fordi alternativet folk griper til først, å rastrere et diagramelement fra skjermen til et punktbilde og lime bildet inn på siden, gir deg et diagram låst til skjermoppløsning som skrives ut uskarpt og blåser opp filen. Å tegne diagrammet med vektorprimitivene i HotPDF holder resultatet skarpt ved enhver zoom og enhver utskrifts-DPI, fordi stolpene og aksene er ekte PDF-baneoperatorer, ikke piksler. Prisen er at du eier layouten. Mekanikken koker ned til noen få grep: den ene koordinatvendingen som tar alle, et gjennomarbeidet stolpediagram, polylinjetrikset for linjediagrammer og buematematikken for sektorer
Det eneste vanskelige er koordinatsystemet
Skjermgrafikk legger origo øverst til venstre med Y som vokser nedover. PDF gjør det motsatte. Origo ligger nederst til venstre på siden, og Y vokser oppover, målt i punkter (1/72 tomme). Hvert tegnekall i HotPDF, TextOut, Rectangle, MoveTo, LineTo, Circle, bruker den konvensjonen med origo nederst til venstre og Y oppover. Tar du med deg instinktene fra skjermgrafikk, tegnes det første diagrammet ditt opp ned og renner ut nederst på siden
Så det egentlige arbeidet i ethvert diagram er én avbildning: å gjøre en dataverdi om til en Y-koordinat som respekterer at Y vokser oppover. Bestem deg for et plottrektangel, fire tall for området diagrammet lever i, og avbild så den minste verdien i dataene dine til nedre kant og den største til øvre. For en stolpe med verdien V på en skala som går fra 0 til MaxValue er stolpens øvre kant PlotBottom + (V / MaxValue) * PlotHeight, og stolpen vokser oppover fra PlotBottom. Få det ene uttrykket riktig, og alt annet er bokføring. Hjelperen nedenfor holder plottgeometrien og gjør omregningen, så tegnekoden aldri rører rå aritmetikk to ganger:
type
TPlotArea = record
Left, Bottom, Width, Height: Single; // PDF-punkter, origo nederst til venstre
MaxValue: Single; // toppen av verdiskalaen
end;
// Avbild en dataverdi til Y-koordinaten inne i plottet, med Y oppover.
function ValueToY(const Plot: TPlotArea; V: Single): Single;
begin
Result := Plot.Bottom + (V / Plot.MaxValue) * Plot.Height;
end;
Én skjønnsvurdering gjemmer seg i MaxValue. Setter du den til nøyaktig det største datapunktet, berører den høyeste stolpen øvre kant av plottet og ser avklippet ut. Rund den opp til et rent tall over maksimum, si neste multiplum av 10 eller 100, så diagrammet får luft og etikettene på rutenettlinjene leses som runde tall i stedet for det dataene tilfeldigvis toppet seg på
Et stolpediagram er en løkke over rektangler
Med avbildningen på plass skriver stolpediagrammet seg selv. Del plottbredden i ett spor per kategori, la det være et mellomrom mellom stolpene så de ikke berører hverandre, og tegn hver stolpe som et fylt rektangel med høyde fra ValueToY. Rectangle i HotPDF tar nedre venstre hjørne pluss en bredde og en høyde, noe som passer nøyaktig med en stolpe som vokser oppover fra grunnlinjen. Sett fyllfargen først, legg ned banen, og kall så Fill for å male den. Kategorietiketten går under grunnlinjen, verdien over stolpen:
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; // en kvart spor i mellomrom på hver side
BarW := SlotW - Gap;
// Grunnlinjen (X-aksen) langs bunnen av plottet.
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, bredde, høyde
Page.Fill;
// Kategorietikett under grunnlinjen, verdi over stolpen.
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 gjør nytte for seg. Gap er en brøkdel av sporet i stedet for et fast antall punkter, så stolpene holder proporsjonal avstand enten du plotter fire kategorier eller førti. Og verdietiketten plasseres med den samme høyden fra ValueToY som stolpen bruker, så den ligger alltid rett over sin egen stolpe i stedet for å flyte på en gjettet forskyvning. Vil du ha vannrette rutenettlinjer bak stolpene, tegner du dem før løkken: velg tre eller fire runde verdier, kjør ValueToY på hver av dem, og strek en svak linje tvers over plottet ved den Y-en. Å tegne dem først legger dem bak stolpene i den malermodellen PDF bruker for stabling
Akser, aksemerker og etiketter er bare flere linjer og mer tekst
Diagrammet er ikke ferdig før en leser kan se hva stolpene betyr, og det er utelukkende aksearbeid. Den loddrette aksen er én streket linje opp langs venstre kant av plottet med en håndfull aksemerker og verdiene deres. Gjenbruk ValueToY så merkene lander på den samme skalaen stolpene bruker, ellers er en stolpe og rutenettlinjen dens uenige, og diagrammet lyver stille:
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); // kort merke utenfor aksen
Page.LineTo(Plot.Left, TickY);
Page.Stroke;
Page.TextOut(Plot.Left - 30, TickY - 3, 0, FormatFloat('0', TickV));
end;
end;
Etiketter er der diagrammer oftest brekker i produksjon, og feilen er alltid den samme: tekst som fikk plass på skjermen, går ut over plassen sin i PDF-en. Lange kategorinavn kolliderer med naboene, og lokaliserte månedsnavn som "septembre" eller "Dezember" er bredere enn den engelske "Sep" du testet med. Det finnes ingen automatisk størrelsestilpasning som redder deg her, så la det være god margin under grunnlinjen, krymp skriften ett eller to punkter for tette kategorisett, og roter navnene hvis de virkelig er lange. TextOut tar en vinkel som tredje argument, så å sende inn 90 stiller etiketten på høykant og gir deg plass uten overlapp. Test layouten med den bredeste etiketten du venter deg, ikke den korteste, før eksporten går ut
Linjediagrammer: én polylinje gjennom avbildede punkter
Et linjediagram gjenbruker hele verdiavbildningen og endrer bare hvordan punktene kobles sammen. I stedet for ett rektangel per kategori går du gjennom dataene én gang, konverterer hver verdi til sin (X, Y) med ValueToY, og syr punktene sammen med én MoveTo etterfulgt av LineTo-kall, streket til slutt. Det første punktet åpner banen; hvert senere punkt forlenger 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) // åpne banen i det første punktet
else
Page.LineTo(X, Y); // forleng den gjennom hvert senere punkt
end;
Page.Stroke; // én strek maler hele polylinjen
end;
Merk forskjellen i avstand. Et stolpediagram deler bredden på antall stolper, fordi hver stolpe eier et spor. Et linjediagram deler på antall intervaller, Count - 1, fordi det første og det siste punktet ligger på plottkantene og linjen spenner over mellomrommene mellom dem. Å blande de to er den vanlige grunnen til at et linjediagram driver et halvt spor bort fra stolpediagrammet det skal ligge oppå. Vil du ha en markør i hvert datapunkt, slipper du en liten Circle og Fill i hver (X, Y) etter at polylinjen er streket
Sektordiagrammer: buer, eller kiler hvis du holder det enkelt
Sektorer er den ene formen som trenger trigonometri, fordi en kile avgrenses av to radier og en bue. Den ærlige varianten sveiper buen ved å legge små linjesegmenter steg for steg langs omkretsen, noe som tilnærmer kurven så tett at ingen leser ser det. Sveipevinkelen til hver sektor er dens andel av totalen, (Value / Total) * 2π, og du akkumulerer den løpende vinkelen 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; // ca. to grader per segment
Page.SetRGBFillColor(Colors[I]);
Page.MoveTo(CX, CY); // kilens spiss i sentrum
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); // lukk tilbake til sentrum
Page.Fill;
Start := Start + Sweep; // gå videre til neste sektor
end;
end;
Banen fra sentrum og ut, spissen først, så buen, så tilbake til spissen, gir en lukket kile som Fill maler solid. Antall segmenter bytter jevnhet mot banestørrelse: rundt to grader per steg ser rundt ut ved enhver fornuftig radius uten å gi en enorm innholdsstrøm. Trenger du ingen ekte sirkel, kan du hoppe over trigonometrien helt og gjengi de samme dataene som én vannrett stablet stolpe, der bredden på hvert segment er proporsjonal med andelen sin. Det er ofte mer lesbart enn en sektor uansett, og det er bare stolpekoden med rektanglene lagt ende mot ende. Grip til buevarianten bare når designet krever en faktisk sirkel
Ingenting av dette avhenger av at et diagrambibliotek er installert, som er den stille fordelen ved å tegne primitivene direkte. De samme tegnekallene på lerretet som plasserer en logo eller en signaturboks, bygger disse diagrammene, og den samme TextOut som setter etikett på et skjemafelt, setter etikett på en akse. Legg plottgeometrien i en record, avbild verdier til Y én gang, og et stolpe-, linje- eller sektordiagram er en kort rutine over Rectangle, LineTo og Circle som du kan slippe inn i en hvilken som helst rapport. Kallene Rectangle, MoveTo, LineTo, Circle, Fill, Stroke og TextOut som er brukt her, er en del av HotPDF Delphi Component for Delphi og C++Builder