HotPDF har inget diagramobjekt. Det finns ingen TPDFChart, ingen AddBarSeries, ingenting som tar en array med siffror och returnerar en renderad graf. Vad det ger dig i stället är en sidduk med samma lågnivåvokabulär som varje PDF-ritningsmodell använder: rektanglar, linjer, cirklar, fyllningar, streck och text placerad på exakta koordinater. Ett diagram i ett HotPDF-dokument är därför något du bygger, inte något du begär. Det låter som mer arbete än vad det är. När du väl har skrivit koordinatmatematiken en gång är ett stapeldiagram en slinga över rektanglar, ett linjediagram är en polylinje och ett cirkeldiagram är en solfjäder av bågar, och du kontrollerar varje pixel i resultatet
Detta spelar roll eftersom alternativet folk först sträcker sig efter, att rastrera en skärmdiagramkontroll till en bitmapp och klistra in bilden på sidan, ger dig ett diagram låst till skärmupplösning som skrivs ut suddigt och gör filen onödigt stor. Att rita diagrammet med HotPDF:s vektorprimitiver håller utmatningen skarp vid vilken zoomnivå som helst och vilken utskrifts-DPI som helst, eftersom staplarna och axlarna är riktiga PDF-sökvägsoperatörer, inte pixlar. Kostnaden är att du äger layouten. Mekaniken kokar ner till några få drag: den enda koordinatvändningen som lurar alla, ett bearbetat stapeldiagram, polylinjetricket för linjediagram och bågmatematiken för tårtbitar
Den enda svåra delen är koordinatsystemet
Skärmgrafik placerar origo längst upp till vänster med Y växande nedåt. PDF gör tvärtom. Origo sitter i det nedre vänstra hörnet av sidan och Y växer uppåt, mätt i punkter (1/72 tum). Varje ritningsanrop i HotPDF, TextOut, Rectangle, MoveTo, LineTo, Circle, använder den nedre vänstra konventionen där Y pekar uppåt. Om du tar med dig instinkter från skärmgrafik ritar ditt första diagram upp och ner och hamnar utanför sidans nederkant
Så det verkliga arbetet i alla diagram är en mappning: förvandla ett datavärde till en Y-koordinat som respekterar Y-upp. Bestäm dig för en diagramrektangel, fyra siffror för området diagrammet lever i, mappa sedan det minsta värdet i dina data till underkanten och det största till överkanten. För en stapel med värdet V på en skala som löper från 0 till MaxValue, är stapelns överkant PlotBottom + (V / MaxValue) * PlotHeight, och stapeln växer uppåt från PlotBottom. Få det enda uttrycket rätt och allt annat är bokföring. Hjälpfunktionen nedan håller reda på diagrammets geometri och gör konverteringen, så ritningskoden aldrig rör rå aritmetik två gånger:
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;
En bedömningsfråga gömmer sig i MaxValue. Om du sätter den till den exakta största datapunkten, rör den högsta stapeln den övre kanten av diagrammet och ser avklippt ut. Avrunda det uppåt till ett jämnt tal över maximum, till exempel nästa multipel av 10 eller 100, så diagrammet har utrymme och rutnätsetiketterna läses som jämna siffror i stället för vad data råkade toppa på
Ett stapeldiagram är en slinga över rektanglar
Med mappningen avklarad skriver ett stapeldiagram sig självt. Dela upp diagrammets bredd i en plats per kategori, lämna ett mellanrum mellan staplarna så att de inte rör varandra, och rita varje stapel som en fylld rektangel vars höjd kommer från ValueToY. HotPDF:s Rectangle tar det nedre vänstra hörnet plus en bredd och en höjd, vilket stämmer exakt överens med en stapel som växer uppåt från baslinjen. Ställ in fyllningsfärgen först, lägg ner sökvägen (path) och anropa sedan Fill för att måla den. Kategorietiketten hamnar under baslinjen, värdet över stapeln:
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;
Två detaljer gör skäl för sig. Gap är en bråkdel av platsen i stället för ett fast antal punkter, så staplarna hålls proportionellt åtskilda oavsett om du ritar fyra kategorier eller fyrtio. Och värdeetiketten är placerad med samma höjd härledd från ValueToY som stapeln använder, så den sitter alltid precis ovanför sin egen stapel i stället för att sväva vid en gissad förskjutning. Om du vill ha horisontella rutnätslinjer bakom staplarna, rita dem innan slingan: välj tre eller fyra jämna värden, kör ValueToY på varje och dra en svag linje tvärs över diagrammet vid det Y-värdet. Att rita dem först lägger dem bakom staplarna i den målarmodell-stapling som PDF använder
Axlar, bockmarkeringar och etiketter är bara fler linjer och text
Diagrammet är inte färdigt förrän en läsare kan avgöra vad staplarna betyder, och det är helt och hållet axelarbete. Den vertikala axeln är en dragen linje upp längs den vänstra kanten av diagrammet med en handfull bockmarkeringar och deras värden. Återanvänd ValueToY så att bockarna hamnar på samma skala som staplarna använder, annars är en stapel och dess rutnätslinje oense och diagrammet ljuger i tysthet:
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 är där diagram oftast går sönder i produktion, och felet är alltid detsamma: text som fick plats på skärmen överskrider sitt utrymme i PDF:en. Långa kategorinamn kolliderar med sina grannar, och lokaliserade månadsnamn som "septembre" eller "Dezember" är bredare än det engelska "Sep" som du testade med. Det finns ingen automatisk storleksanpassning för att rädda dig här, så lämna en riktig marginal under baslinjen, krymp teckensnittet en punkt eller två för täta kategoriuppsättningar, och om namnen genuint är långa, rotera dem. TextOut tar en vinkel som sitt tredje argument, så att skicka med 90 ställer etiketten på ände och ger dig utrymme utan överlappning. Testa layouten med din förväntade bredaste etikett, inte din kortaste, innan exporten levereras
Linjediagram: en polylinje genom mappade punkter
Ett linjediagram återanvänder hela värdemappningen och ändrar bara hur punkterna ansluts. I stället för en rektangel per kategori går du igenom datan en gång, konverterar varje värde till sitt (X, Y) med ValueToY och syr ihop punkterna med ett enda MoveTo följt av LineTo-anrop, med Stroke i slutet. Den första punkten öppnar sökvägen (path); varje senare punkt förlä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;
Notera avståndsskillnaden. Ett stapeldiagram delar bredden med antalet staplar, eftersom varje stapel äger en plats. Ett linjediagram delar med antalet intervaller, Count - 1, eftersom den första och sista punkten sitter på diagrammets kanter och linjen spänner över mellanrummen mellan dem. Att blanda ihop dessa två är den vanliga anledningen till att ett linjediagram driver ett halvt platsteg bort från det stapeldiagram som det är tänkt att ligga över. Om du vill ha en markör vid varje datapunkt, släpp en liten Circle och Fill vid varje (X, Y) efter att polylinjen har ritats (stroked)
Cirkeldiagram: bågar, eller tårtbitar om du håller det enkelt
Tårtbitar är den enda formen som kräver trigonometri, eftersom en bit avgränsas av två radier och en båge. Den ärliga versionen sveper bågen genom att stega små linjesegment längs omkretsen, vilket approximerar kurvan tillräckligt nära för att ingen läsare ska märka det. Varje bits svepvinkel är dess andel av totalen, (Value / Total) * 2π, och du ackumulerar den löpande vinkeln när du går runt:
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;
Vägen från mitten och utåt, spetsen först, sedan bågen, sedan tillbaka till spetsen, ger en stängd kil som Fill målar solitt. Antalet segment handlar om att byta jämnhet mot sökvägens storlek: ungefär två grader per steg ser runt ut vid vilken vettig radie som helst utan att producera en enorm innehållsström. Om du inte behöver en riktig cirkel kan du hoppa över trigonometrin helt och rendera samma data som en enda horisontell staplad stapel, varje segments bredd proportionell mot dess andel. Det är ofta mer lättläst än en cirkel ändå, och det är bara stapelkoden med rektanglarna lagda ände mot ände. Sträck dig efter bågversionen först när designen kräver en verklig cirkel
Inget av detta är beroende av att något diagrambibliotek installeras, vilket är den tysta fördelen med att rita primitiverna direkt. Samma canvas-ritningsanrop som placerar en logotyp eller en signaturruta bygger dessa diagram, och samma TextOut som sätter etikett på ett formulärfält gör det för en axel. Lägg diagramgeometrin i en post (record), mappa värden till Y en gång, och ett stapel-, linje- eller cirkeldiagram är en kort rutin över Rectangle, LineTo och Circle som du kan släppa in i valfri rapport. Rectangle, MoveTo, LineTo, Circle, Fill, Stroke och TextOut-anropen som används här är en del av HotPDF Component för Delphi och C++Builder