HotPDF hat kein Diagramm-Objekt. Es gibt kein TPDFChart, kein AddBarSeries, nichts, was ein Array von Zahlen entgegennimmt und ein gerendertes Diagramm zurückgibt. Was Sie stattdessen erhalten, ist eine Seitenleinwand mit demselben Low-Level-Vokabular, das jedes PDF-Zeichenmodell verwendet: Rechtecke, Linien, Kreise, Füllungen, Konturen und an exakten Koordinaten platzierter Text. Ein Diagramm in einem HotPDF-Dokument ist daher etwas, das Sie aufbauen, nicht etwas, das Sie anfordern. Das klingt nach mehr Arbeit, als es ist. Wenn Sie die Koordinatenberechnung einmal geschrieben haben, ist ein Balkendiagramm eine Schleife über Rechtecke, ein Liniendiagramm eine Polylinie und ein Kreisdiagramm ein Fächer von Bögen, und Sie kontrollieren jedes Pixel des Ergebnisses
Dies ist wichtig, da die Alternative, nach der die Leute zuerst greifen – das Rastern eines Bildschirm-Diagrammsteuerelements in ein Bitmap und das Einfügen des Bildes in die Seite –, ein an die Bildschirmauflösung gebundenes Diagramm liefert, das unscharf gedruckt wird und die Datei aufbläht. Das Zeichnen des Diagramms mit den Vektorprimitiven von HotPDF hält die Ausgabe bei jedem Zoom und jeder Druckauflösung gestochen scharf, da die Balken und Achsen echte PDF-Pfad-Operatoren und keine Pixel sind. Der Preis dafür ist, dass Sie für das Layout verantwortlich sind. Die Mechanik läuft auf ein paar wenige Schritte hinaus: der eine Koordinatenwechsel, der jeden erwischt, ein ausgearbeitetes Balkendiagramm, der Polylinien-Trick für Liniendiagramme und die Bogenberechnung für Kuchenstücke
Der einzige schwierige Teil ist das Koordinatensystem
Bei Bildschirmgrafiken liegt der Ursprung oben links, wobei Y nach unten wächst. PDF macht es umgekehrt. Der Ursprung befindet sich in der unteren linken Ecke der Seite und Y wächst nach oben, gemessen in Punkten (1/72 Zoll). Jeder Zeichenaufruf in HotPDF, TextOut, Rectangle, MoveTo, LineTo, Circle, verwendet diese Konvention von unten links und Y-nach-oben. Wenn Sie Instinkte von Bildschirmgrafiken übernehmen, wird Ihr erstes Diagramm auf dem Kopf stehend gezeichnet und verläuft über den unteren Rand der Seite hinaus
Die eigentliche Arbeit bei jedem Diagramm ist also eine Abbildung: Verwandeln Sie einen Datenwert in eine Y-Koordinate, die Y-nach-oben respektiert. Legen Sie ein Plot-Rechteck fest, vier Zahlen für den Bereich, in dem das Diagramm lebt, und bilden Sie dann den kleinsten Wert in Ihren Daten auf den unteren Rand und den größten auf den oberen Rand ab. Für einen Balken mit dem Wert V auf einer Skala, die von 0 bis MaxValue reicht, ist die Oberkante des Balkens PlotBottom + (V / MaxValue) * PlotHeight, und der Balken wächst von PlotBottom nach oben. Wenn Sie diesen einen Ausdruck richtig hinbekommen, ist alles andere Buchhaltung. Der Helfer unten enthält die Plot-Geometrie und führt die Konvertierung durch, sodass der Zeichencode nie zweimal rohe Arithmetik berührt:
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;
Eine Ermessensentscheidung verbirgt sich in MaxValue. Wenn Sie es auf den exakten größten Datenpunkt setzen, berührt der höchste Balken die Oberkante des Plots und sieht abgeschnitten aus. Runden Sie ihn auf eine saubere Zahl über dem Maximum auf, sagen wir das nächste Vielfache von 10 oder 100, damit das Diagramm Spielraum hat und die Gitternetzlinien-Beschriftungen als runde Zahlen gelesen werden und nicht als das, was die Daten zufällig erreicht haben
Ein Balkendiagramm ist eine Schleife über Rechtecke
Wenn die Abbildung festgelegt ist, schreibt sich ein Balkendiagramm von selbst. Teilen Sie die Plot-Breite in einen Slot pro Kategorie ein, lassen Sie eine Lücke zwischen den Balken, damit sie sich nicht berühren, und zeichnen Sie jeden Balken als gefülltes Rechteck, dessen Höhe von ValueToY stammt. HotPDFs Rectangle nimmt die untere linke Ecke plus eine Breite und eine Höhe, was genau mit einem Balken übereinstimmt, der von der Grundlinie nach oben wächst. Legen Sie zuerst die Füllfarbe fest, legen Sie den Pfad fest und rufen Sie dann Fill auf, um ihn zu malen. Die Kategorie-Beschriftung kommt unter die Grundlinie, der Wert über den Balken:
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;
Zwei Details lohnen sich. Die Lücke (Gap) ist ein Bruchteil des Slots und keine feste Punktzahl, sodass die Balken proportional im Abstand bleiben, egal ob Sie vier oder vierzig Kategorien plotten. Und die Wert-Beschriftung wird mit derselben von ValueToY abgeleiteten Höhe positioniert, die der Balken verwendet, sodass sie immer genau über dem eigenen Balken sitzt, anstatt in einem geschätzten Versatz zu schweben. Wenn Sie horizontale Gitternetzlinien hinter den Balken wünschen, zeichnen Sie diese vor der Schleife: Wählen Sie drei oder vier runde Werte, führen Sie ValueToY auf jedem aus und streichen Sie bei diesem Y eine schwache Linie quer über den Plot. Das erste Zeichnen platziert sie hinter den Balken im Painter-Modell-Stapel, den PDF verwendet
Achsen, Teilstriche und Beschriftungen sind nur weitere Linien und Text
Das Diagramm ist nicht fertig, bis ein Leser sagen kann, was die Balken bedeuten, und das ist reine Achsenarbeit. Die vertikale Achse ist eine gestrichene Linie am linken Rand des Plots nach oben mit einer Handvoll Teilstrichen und deren Werten. Verwenden Sie ValueToY wieder, damit die Teilstriche in derselben Skala landen, die die Balken verwenden, sonst stimmen ein Balken und seine Gitternetzlinie nicht überein und das Diagramm lügt stillschweigend:
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;
Beschriftungen sind der Punkt, an dem Diagramme in der Produktion am häufigsten kaputt gehen, und der Fehler ist immer derselbe: Text, der auf den Bildschirm passte, sprengt seinen Platz im PDF. Lange Kategorienamen kollidieren mit ihren Nachbarn, und lokalisierte Monatsnamen wie "septembre" oder "Dezember" sind breiter als das englische "Sep", mit dem Sie getestet haben. Hier gibt es keine automatische Größenanpassung, um Sie zu retten. Lassen Sie also einen echten Rand unter der Grundlinie, verkleinern Sie die Schriftart um einen oder zwei Punkte für dichte Kategoriensätze, und wenn Namen wirklich lang sind, drehen Sie sie. TextOut nimmt einen Winkel als drittes Argument an, sodass die Übergabe von 90 das Etikett aufrecht stellt und Ihnen Platz ohne Überlappung verschafft. Testen Sie das Layout mit Ihrem längsten erwarteten Etikett, nicht mit Ihrem kürzesten, bevor der Export ausgeliefert wird
Liniendiagramme: eine Polylinie durch abgebildete Punkte
Ein Liniendiagramm verwendet die gesamte Wertabbildung wieder und ändert nur, wie die Punkte verbunden werden. Anstelle eines Rechtecks pro Kategorie durchlaufen Sie die Daten einmal, wandeln jeden Wert mit ValueToY in sein (X, Y) um und verbinden die Punkte mit einem einzigen MoveTo, gefolgt von LineTo-Aufrufen, die am Ende gestrichen werden. Der erste Punkt öffnet den Pfad; jeder weitere Punkt verlängert ihn:
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;
Beachten Sie den Unterschied im Abstand. Ein Balkendiagramm teilt die Breite durch die Anzahl der Balken, da jeder Balken einen Slot besitzt. Ein Liniendiagramm teilt durch die Anzahl der Intervalle, Count - 1, da die ersten und letzten Punkte an den Rändern des Plots liegen und die Linie die Lücken dazwischen überspannt. Das Verwechseln dieser beiden ist der übliche Grund, warum ein Liniendiagramm um einen halben Slot von dem Balkendiagramm abweicht, das es überlagern soll. Wenn Sie an jedem Datenpunkt eine Markierung wünschen, setzen Sie einen kleinen Circle und Fill an jedem (X, Y) ab, nachdem die Polylinie gestrichen wurde
Kreisdiagramme: Bögen oder Keile, wenn Sie es einfach halten
Kuchenstücke sind die einzige Form, die Trigonometrie erfordert, da ein Keil von zwei Radien und einem Bogen begrenzt wird. Die ehrliche Version fegt den Bogen, indem sie kleine Liniensegmente entlang des Umfangs setzt, was die Kurve so genau annähert, dass kein Leser den Unterschied erkennen kann. Der Überstreichungswinkel jedes Stücks ist sein Anteil am Ganzen, (Value / Total) * 2π, und Sie akkumulieren den laufenden Winkel, während Sie herumgehen:
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;
Der Weg von der Mitte nach außen, zuerst die Spitze, dann der Bogen, dann zurück zur Spitze, ergibt einen geschlossenen Keil, den Fill solide ausmalt. Die Segmentanzahl tauscht Glätte gegen Pfadgröße: Ungefähr zwei Grad pro Schritt sehen bei jedem vernünftigen Radius rund aus, ohne einen riesigen Inhaltsstrom zu erzeugen. Wenn Sie keinen echten Kreis benötigen, können Sie die Trigonometrie komplett überspringen und dieselben Daten als einzelnen horizontalen gestapelten Balken rendern, wobei die Breite jedes Segments proportional zu seinem Anteil ist. Das ist ohnehin oft lesbarer als ein Kreis, und es ist nur der Balkencode mit aneinandergereihten Rechtecken. Greifen Sie nur dann zur Bogenversion, wenn das Design einen tatsächlichen Kreis erfordert
Nichts davon hängt von der Installation einer Diagrammbibliothek ab, was der stille Vorteil des direkten Zeichnens der Primitive ist. Dieselben Aufrufe zum Zeichnen auf der Leinwand, die ein Logo oder ein Unterschriftenfeld platzieren, erstellen diese Diagramme, und dasselbe TextOut, das ein Formularfeld beschriftet, beschriftet eine Achse. Legen Sie die Plot-Geometrie in einem Record ab, bilden Sie die Werte einmal auf Y ab, und ein Balken-, Linien- oder Kreisdiagramm ist eine kurze Routine über Rectangle, LineTo und Circle, die Sie in jeden Bericht einfügen können. Die hier verwendeten Aufrufe Rectangle, MoveTo, LineTo, Circle, Fill, Stroke und TextOut sind Teil der HotPDF-Komponente für Delphi und C++Builder