HotPDF heeft geen grafiekobject. Er is geen TPDFChart, geen AddBarSeries, niets dat een array getallen aanneemt en een gerenderde grafiek teruggeeft. Wat het u in plaats daarvan geeft, is een paginacanvas met dezelfde laagniveauwoordenschat die elk PDF-tekenmodel gebruikt: rechthoeken, lijnen, cirkels, vullingen, lijnstreken en tekst geplaatst op exacte coördinaten. Een grafiek in een HotPDF-document is daarom iets wat u bouwt, niet iets wat u opvraagt. Dat klinkt als meer werk dan het is. Zodra u de coördinatenrekenkunde één keer hebt geschreven, is een staafdiagram een lus over rechthoeken, is een lijndiagram een polylijn en is een taartdiagram een waaier van bogen, en beheerst u elke pixel van het resultaat
Dit doet ertoe omdat het alternatief waar mensen als eerste naar grijpen, een schermgrafiek naar een bitmap rasteren en die afbeelding in de pagina plakken, u een grafiek geeft die vastzit aan schermresolutie, wazig afdrukt en het bestand opblaast. De grafiek met de vectorprimitieven van HotPDF tekenen houdt de uitvoer scherp bij elke zoom en elke afdruk-DPI, omdat de staven en assen echte PDF-padoperatoren zijn en geen pixels. De prijs is dat de opmaak van u is. De mechaniek komt neer op een paar zetten: de ene coördinatenomkering waar iedereen over struikelt, een uitgewerkt staafdiagram, de polylijntruc voor lijndiagrammen, en de boogrekenkunde voor taartpunten
Het enige lastige is het coördinatenstelsel
Schermgraphics leggen de oorsprong linksboven met Y die naar beneden groeit. PDF doet het omgekeerde. De oorsprong ligt in de linkeronderhoek van de pagina en Y groeit omhoog, gemeten in punten (1/72 inch). Elke tekenaanroep in HotPDF, TextOut, Rectangle, MoveTo, LineTo, Circle, gebruikt die conventie van linksonder met Y omhoog. Neemt u uw instincten uit schermgraphics mee, dan tekent uw eerste grafiek ondersteboven en loopt hij van de onderkant van de pagina af
Het echte werk in elke grafiek is dus één afbeelding: een datawaarde omzetten in een Y-coördinaat die Y-omhoog respecteert. Kies een plotrechthoek, vier getallen voor het gebied waarin de grafiek woont, en map dan de kleinste waarde in uw data op de onderrand en de grootste op de bovenrand. Voor een staaf met waarde V op een schaal die van 0 tot MaxValue loopt, is de bovenrand van de staaf PlotBottom + (V / MaxValue) * PlotHeight, en de staaf groeit omhoog vanaf PlotBottom. Krijg die ene uitdrukking goed en al het overige is boekhouding. De helper hieronder houdt de plotgeometrie vast en doet de omzetting, zodat de tekencode nooit twee keer ruwe rekenkunde aanraakt:
type
TPlotArea = record
Left, Bottom, Width, Height: Single; // PDF-punten, oorsprong linksonder
MaxValue: Single; // bovenkant van de waardeschaal
end;
// Map een datawaarde op haar Y-coördinaat in de plot, met Y omhoog groeiend.
function ValueToY(const Plot: TPlotArea; V: Single): Single;
begin
Result := Plot.Bottom + (V / Plot.MaxValue) * Plot.Height;
end;
Er schuilt één beoordelingskwestie in MaxValue. Zet u die op precies het grootste datapunt, dan raakt de hoogste staaf de bovenrand van de plot en oogt hij afgeknipt. Rond hem af naar een net getal boven het maximum, bijvoorbeeld het eerstvolgende veelvoud van 10 of 100, zodat de grafiek kopruimte heeft en de labels bij de rasterlijnen als ronde getallen lezen in plaats van als de toevallige piek van de data
Een staafdiagram is een lus over rechthoeken
Met de afbeelding vastgelegd schrijft een staafdiagram zichzelf. Verdeel de plotbreedte in één vak per categorie, laat een tussenruimte tussen de staven zodat ze elkaar niet raken, en teken elke staaf als een gevulde rechthoek waarvan de hoogte uit ValueToY komt. De Rectangle van HotPDF neemt de linkeronderhoek plus een breedte en een hoogte, wat precies aansluit bij een staaf die vanaf de basislijn omhoog groeit. Stel eerst de vulkleur in, leg het pad neer, en roep dan Fill aan om het te schilderen. Het categorielabel gaat onder de basislijn, de waarde boven de staaf:
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; // tussenruimte van een kwart vak aan elke kant
BarW := SlotW - Gap;
// Basislijn (de X-as) langs de onderkant van de 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, breedte, hoogte
Page.Fill;
// Categorielabel onder de basislijn, waarde boven de staaf.
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;
Twee details verdienen hun plek. De Gap is een fractie van het vak in plaats van een vast aantal punten, dus de staven blijven proportioneel gespreid of u nu vier categorieën plot of veertig. En het waardelabel wordt gepositioneerd met dezelfde uit ValueToY afgeleide hoogte die de staaf gebruikt, zodat het altijd net boven zijn eigen staaf zit in plaats van op een geraden offset te zweven. Wilt u horizontale rasterlijnen achter de staven, teken die dan vóór de lus: kies drie of vier ronde waarden, draai ValueToY op elk ervan, en trek een vage lijn dwars over de plot op die Y. Ze eerst tekenen zet ze achter de staven in de schildersmodelstapeling die PDF gebruikt
Assen, streepjes en labels zijn gewoon meer lijnen en tekst
De grafiek is niet af totdat een lezer kan zien wat de staven betekenen, en dat is volledig aswerk. De verticale as is één getrokken lijn langs de linkerrand van de plot met een handvol streepjes en hun waarden. Hergebruik ValueToY zodat de streepjes op dezelfde schaal landen die de staven gebruiken, anders spreken een staaf en zijn rasterlijn elkaar tegen en liegt de grafiek in stilte:
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 streepje buiten de as
Page.LineTo(Plot.Left, TickY);
Page.Stroke;
Page.TextOut(Plot.Left - 30, TickY - 3, 0, FormatFloat('0', TickV));
end;
end;
Labels zijn de plek waar grafieken in productie het vaakst stukgaan, en het falen is altijd hetzelfde: tekst die op het scherm paste, loopt in de PDF over haar ruimte heen. Lange categorienamen botsen met hun buren, en gelokaliseerde maandnamen als "septembre" of "Dezember" zijn breder dan het Engelse "Sep" waarmee u hebt getest. Er is hier geen autosize die u redt, dus laat echte marge onder de basislijn, verklein het lettertype een punt of twee voor dichte categoriesets, en draai de namen als ze werkelijk lang zijn. TextOut neemt een hoek als derde argument, dus 90 doorgeven zet het label rechtop en levert u ruimte zonder overlap. Test de opmaak met uw breedste verwachte label, niet met uw kortste, voordat de export de deur uitgaat
Lijndiagrammen: één polylijn door gemapte punten
Een lijndiagram hergebruikt de hele waardeafbeelding en verandert alleen hoe de punten verbinden. In plaats van een rechthoek per categorie loopt u de data één keer door, zet elke waarde met ValueToY om naar haar (X, Y), en rijgt u de punten aaneen met één MoveTo gevolgd door LineTo-aanroepen, aan het eind getrokken. Het eerste punt opent het pad; elk later punt verlengt het:
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 het pad bij het eerste punt
else
Page.LineTo(X, Y); // verleng het door elk later punt
end;
Page.Stroke; // één streek schildert de hele polylijn
end;
Let op het verschil in verdeling. Een staafdiagram deelt de breedte door het aantal staven, omdat elke staaf een vak bezit. Een lijndiagram deelt door het aantal intervallen, Count - 1, omdat het eerste en het laatste punt op de plotranden liggen en de lijn de tussenruimtes overspant. Die twee door elkaar halen is de gebruikelijke reden dat een lijndiagram een half vak afwijkt van het staafdiagram waarop het moet liggen. Wilt u een markering bij elk datapunt, zet dan na het trekken van de polylijn een kleine Circle met Fill op elke (X, Y)
Taartdiagrammen: bogen, of punten als u het simpel houdt
Taartpunten zijn de ene vorm die trigonometrie vereist, want een punt wordt begrensd door twee stralen en een boog. De eerlijke versie doorloopt de boog door kleine lijnsegmenten langs de omtrek te stappen, wat de kromming zo dicht benadert dat geen lezer het merkt. De veeghoek van elke punt is haar aandeel in het totaal, (Value / Total) * 2π, en u telt de lopende hoek op terwijl u de cirkel rondgaat:
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 graden per segment
Page.SetRGBFillColor(Colors[I]);
Page.MoveTo(CX, CY); // top van de punt in het midden
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); // sluit terug naar het midden
Page.Fill;
Start := Start + Sweep; // ga door naar de volgende punt
end;
end;
Het pad van het midden naar buiten, eerst de top, dan de boog, dan terug naar de top, levert een gesloten punt op die Fill massief schildert. Het aantal segmenten ruilt gladheid tegen padgrootte: ongeveer twee graden per stap oogt rond bij elke redelijke straal zonder een enorme contentstream te produceren. Hebt u geen echte cirkel nodig, dan kunt u de trigonometrie helemaal overslaan en dezelfde data renderen als één horizontale gestapelde staaf, waarbij de breedte van elk segment evenredig is aan zijn aandeel. Dat is vaak toch leesbaarder dan een taart, en het is gewoon de staafcode met de rechthoeken kop aan staart gelegd. Grijp alleen naar de boogversie wanneer het ontwerp om een echte cirkel vraagt
Niets hiervan hangt af van een geïnstalleerde grafiekbibliotheek, wat het stille voordeel is van de primitieven rechtstreeks tekenen. Dezelfde canvas-tekenaanroepen die een logo of een handtekeningvak plaatsen, bouwen deze grafieken, en dezelfde TextOut die een formulierveld labelt, labelt een as. Zet de plotgeometrie in een record, map waarden één keer op Y, en een staaf-, lijn- of taartdiagram is een korte routine over Rectangle, LineTo en Circle die u in elk rapport kunt laten vallen. De hier gebruikte aanroepen Rectangle, MoveTo, LineTo, Circle, Fill, Stroke en TextOut maken deel uit van het HotPDF Delphi Component voor Delphi en C++Builder