HotPDF heeft geen grafiekobject. Er is geen TPDFChart, geen AddBarSeries, niets dat een array met getallen accepteert en een gerenderde grafiek teruggeeft. In plaats daarvan biedt het u een paginacanvas met hetzelfde low-level vocabulaire dat elk PDF-tekenmodel gebruikt: rechthoeken, lijnen, cirkels, vullingen, lijndiktes en tekst geplaatst op exacte coördinaten. Een grafiek in een HotPDF-document is daarom iets dat u zelf bouwt, niet iets dat u simpelweg aanvraagt. Dat klinkt als meer werk dan het is. Zodra u de coördinatenberekening eenmaal hebt geschreven, is een staafdiagram een lus over rechthoeken, een lijndiagram een polylijn en een cirkeldiagram een reeks bogen, waarbij u elke pixel van het resultaat zelf in de hand hebt.
Dit is belangrijk omdat het alternatief waar men meestal als eerste naar grijpt (het rasteren van een grafiekcomponent van het scherm naar een bitmap en de afbeelding in de pagina plakken) een grafiek oplevert die vastzit aan de schermresolutie. Dit drukt wazig af en maakt het bestand onnodig groot. Door de grafiek te tekenen met de vectorprimitieven van HotPDF blijft de uitvoer haarscherp bij elke zoomfactor en elke printresolutie, omdat de staven en assen echte PDF-padoperatoren zijn en geen pixels. De prijs hiervoor is dat u zelf verantwoordelijk bent voor de lay-out. De mechanica komt neer op een paar stappen: de coördinatenomkering waar iedereen in het begin tegenaan loopt, een uitgewerkt staafdiagram, de polylijnmethode voor lijndiagrammen en de boogberekening voor cirkelsegmenten.
Het enige lastige deel is het coördinatensysteem
Schermafbeeldingen plaatsen de oorsprong linksboven, waarbij Y naar beneden toe groeit. PDF doet het tegenovergestelde. De oorsprong bevindt zich in de linkerbenedenhoek van de pagina en Y groeit omhoog, gemeten in punten (1/72 inch). Elke tekenfunctie in HotPDF, zoals TextOut, Rectangle, MoveTo, LineTo en Circle, maakt gebruik van die conventie met de oorsprong linksonder en Y omhoog. Als u uw schermgrafische instincten volgt, wordt uw eerste grafiek ondersteboven getekend en loopt deze van de onderkant van de pagina af.
Het echte werk bij elke grafiek is dus een kwestie van mapping: een gegevenswaarde omzetten in een Y-coördinaat die rekening houdt met de opwaartse Y-as. Bepaal een plotrechthoek (vier getallen voor het gebied waarin de grafiek zich bevindt) en map vervolgens de kleinste waarde in uw gegevens naar de onderrand en de grootste naar de bovenrand. Voor een staaf met waarde V op een schaal van 0 tot MaxValue is de bovenrand van de staaf PlotBottom + (V / MaxValue) * PlotHeight, waarbij de staaf omhoog groeit vanaf PlotBottom. Als u die ene formule goed hebt, is de rest slechts administratie. De onderstaande helperfunctie bevat de plotgeometrie en voert de conversie uit, zodat de tekencode de ruwe berekeningen nooit twee keer hoeft uit te voeren:
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;
Er zit een subtiel detail verborgen in MaxValue. Als u deze instelt op exact het grootste datapunt, raakt de hoogste staaf de bovenrand van de plot en ziet dat er afgesneden uit. Rond de waarde af naar boven op een mooi rond getal, bijvoorbeeld het volgende veelvoud van 10 of 100, zodat de grafiek wat hoofdruimte heeft en de rasterlijnen als ronde getallen worden weergegeven in plaats van de toevallige piekwaarde van de gegevens.
Een staafdiagram is een lus over rechthoeken
Zodra de mapping geregeld is, schrijft een staafdiagram zichzelf. Verdeel de breedte van de plot in één vak per categorie, laat een opening tussen de staven zodat ze elkaar niet raken, en teken elke staaf als een gevulde rechthoek waarvan de hoogte afkomstig is van ValueToY. De functie Rectangle van HotPDF verwacht de linkerbenedenhoek plus een breedte en een hoogte, wat precies overeenkomt met een staaf die vanaf de basislijn omhoog groeit. Stel eerst de vulkleur in, definieer het pad en roep vervolgens Fill aan om het te tekenen. Het categorielabel komt 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; // 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;
Twee details bewijzen hier hun nut. De Gap is een fractie van het vak in plaats van een vast aantal punten, zodat de staven proportioneel verdeeld blijven, of u nu vier of veertig categorieën plot. Daarnaast wordt het waardelabel gepositioneerd met dezelfde van ValueToY afgeleide hoogte die de staaf gebruikt, zodat deze altijd net boven zijn eigen staaf staat in plaats van te zweven op een geschatte afstand. Als u horizontale hulplijnen achter de staven wilt, teken deze dan vóór de lus: kies drie of vier ronde waarden, voer ValueToY uit op elke waarde en teken een dunne lijn over de plot op die Y-hoogte. Door ze eerst te tekenen, komen ze achter de staven te staan volgens het schildersmodel dat PDF gebruikt voor het stapelen van elementen.
Assen, maatstreepjes en labels zijn gewoon meer lijnen en tekst
De grafiek is pas compleet als een lezer kan zien wat de staven betekenen, en dat is volledig het werk van de assen. De verticale as is een enkele lijn langs de linkerrand van de plot met een aantal maatstreepjes en hun waarden. Gebruik ValueToY opnieuw zodat de streepjes op dezelfde schaal terechtkomen als de staven, anders komen een staaf en zijn hulplijn niet overeen en klopt er niets van de grafiek:
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;
Labels zijn de plek waar grafieken in productie het vaakst kapotgaan, en het probleem is altijd hetzelfde: tekst die op het scherm paste, overschrijdt de beschikbare ruimte in de PDF. Lange categorienamen overlappen elkaar en gelokaliseerde maandnamen zoals "septembre" of "Dezember" zijn breder dan het Engelse "Sep" waarmee u hebt getest. Er is hier geen automatische groottebepaling om u te redden, dus laat voldoende marge onder de basislijn over, maak het lettertype een punt of twee kleiner bij veel categorieën en roteer de namen als ze echt lang zijn. De functie TextOut accepteert een hoek als derde argument; door 90 door te geven wordt het label verticaal geplaatst, wat ruimte oplevert zonder overlap. Test de lay-out met uw breedst verwachte label, niet het kortste, voordat u de exportfunctie oplevert.
Lijndiagrammen: één polylijn door gemapte punten
Een lijndiagram hergebruikt de volledige waardemapping en verandert alleen de manier waarop de punten met elkaar worden verbonden. In plaats van een rechthoek per categorie doorloopt u de gegevens eenmalig, converteert u elke waarde naar (X, Y) met ValueToY en verbindt u de punten met een enkele MoveTo gevolgd door LineTo-aanroepen, waarna u de lijn ten slotte tekent met een stroke. Het eerste punt opent het pad; elk volgend punt breidt het uit:
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;
Let op het verschil in afstand. Een staafdiagram verdeelt de breedte door het aantal staven, omdat elke staaf een eigen vak heeft. Een lijndiagram deelt door het aantal intervallen, Count - 1, omdat de eerste en laatste punten zich op de randen van de plot bevinden en de lijn de tussenruimten overbrugt. Het door elkaar halen van deze twee is de meest voorkomende reden dat een lijndiagram een half vak verschuift ten opzichte van het staafdiagram dat het zou moeten overlappen. Als u een marker bij elk datapunt wilt, plaats dan een kleine Circle met Fill op elke (X, Y) nadat de polylijn is getekend.
Cirkeldiagrammen: bogen, of taartpunten als u het eenvoudig houdt
Cirkelsegmenten zijn de enige vorm waarvoor goniometrie nodig is, omdat een taartpunt wordt begrensd door twee radii en een boog. Een realistische benadering tekent de boog door kleine lijnsegmenten langs de omtrek te stappen, wat de curve zo dicht benadert dat geen lezer het verschil ziet. De hoek van elk segment is zijn aandeel in het totaal, (Value / Total) * 2π, en u telt de lopende hoek op naarmate u 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 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;
Het pad van binnen naar buiten (eerst de punt, dan de boog, dan terug naar de punt) levert een gesloten taartpunt op die met Fill egaal kan worden ingekleurd. Het aantal segmenten zoekt een balans tussen vloeiendheid en padgrootte: ongeveer twee graden per stap ziet er op elke redelijke straal rond uit zonder een enorme datastroom te genereren. Als u geen echte cirkel nodig hebt, kunt u de goniometrie volledig overslaan en dezelfde gegevens weergeven als een enkele horizontale gestapelde staaf, waarbij de breedte van elk segment evenredig is met het aandeel ervan. Dat is vaak sowieso beter leesbaar dan een cirkeldiagram, en het maakt gebruik van dezelfde staafcode waarbij de rechthoeken achter elkaar worden geplaatst. Grijp pas naar de boogversie als het ontwerp specifiek om een echte cirkel vraagt.
Dit alles werkt zonder dat er een grafische bibliotheek geïnstalleerd hoeft te zijn, wat het grote voordeel is van het rechtstreeks tekenen met primitieven. Dezelfde tekenfuncties op het canvas die een logo of een handtekeningvak plaatsen, bouwen deze grafieken op, en dezelfde TextOut die een formulierveld labelt, labelt een as. Plaats de plotgeometrie in een record, map waarden eenmalig naar Y, en een staaf-, lijn- of cirkeldiagram is een korte routine met Rectangle, LineTo en Circle die u in elk rapport kunt invoegen. De functies Rectangle, MoveTo, LineTo, Circle, Fill, Stroke en TextOut die hier worden gebruikt, maken deel uit van de HotPDF Component voor Delphi en C++Builder.