Το HotPDF δεν διαθέτει αντικείμενο γραφήματος. Δεν υπάρχει TPDFChart, ούτε AddBarSeries, τίποτα που να παίρνει έναν πίνακα αριθμών και να επιστρέφει ένα έτοιμο γράφημα. Αυτό που σας δίνει αντ' αυτού είναι ένας καμβάς σελίδας με το ίδιο χαμηλού επιπέδου λεξιλόγιο που χρησιμοποιεί κάθε μοντέλο σχεδίασης PDF: ορθογώνια, γραμμές, κύκλους, γεμίσματα, περιγράμματα και κείμενο τοποθετημένο σε ακριβείς συντεταγμένες. Ένα γράφημα σε ένα έγγραφο HotPDF είναι επομένως κάτι που χτίζετε εσείς, όχι κάτι που ζητάτε έτοιμο. Αυτό ακούγεται σαν περισσότερη δουλειά απ' ό,τι πραγματικά είναι. Αφού γράψετε μία φορά τα μαθηματικά των συντεταγμένων, ένα γράφημα ράβδων είναι απλώς ένας βρόχος πάνω από ορθογώνια, ένα γράφημα γραμμών είναι μια πολυγραμμή (polyline), και ένα γράφημα πίτας είναι μια βεντάλια από τόξα, ενώ εσείς ελέγχετε κάθε pixel του αποτελέσματος
Αυτό έχει σημασία επειδή η εναλλακτική λύση στην οποία καταφεύγουν πρώτα οι περισσότεροι, δηλαδή η μετατροπή ενός γραφήματος οθόνης σε bitmap και η επικόλληση της εικόνας μέσα στη σελίδα, σας δίνει ένα γράφημα κλειδωμένο στην ανάλυση της οθόνης, το οποίο εκτυπώνεται θολό και διογκώνει το αρχείο. Η σχεδίαση του γραφήματος με τα διανυσματικά αρχέγονα στοιχεία του HotPDF διατηρεί την έξοδο ευκρινή σε οποιοδήποτε ζουμ και σε οποιαδήποτε ανάλυση εκτύπωσης, επειδή οι ράβδοι και οι άξονες είναι πραγματικοί τελεστές διαδρομής PDF, όχι εικονοστοιχεία (pixels). Το κόστος είναι ότι εσείς αναλαμβάνετε τη διάταξη. Ο μηχανισμός καταλήγει σε λίγες μόνο κινήσεις: η ανατροπή συντεταγμένων που μπερδεύει τους πάντες, ένα ολοκληρωμένο παράδειγμα γραφήματος ράβδων, το κόλπο της πολυγραμμής για γραφήματα γραμμών, και τα μαθηματικά τόξων για τα κομμάτια της πίτας
Το μόνο δύσκολο κομμάτι είναι το σύστημα συντεταγμένων
Τα γραφικά οθόνης τοποθετούν την αρχή στην επάνω αριστερή γωνία, με το Y να αυξάνεται προς τα κάτω. Το PDF κάνει το αντίθετο. Η αρχή βρίσκεται στην κάτω αριστερή γωνία της σελίδας και το Y αυξάνεται προς τα πάνω, μετρημένο σε σημεία (1/72 της ίντσας). Κάθε κλήση σχεδίασης στο HotPDF, TextOut, Rectangle, MoveTo, LineTo, Circle, χρησιμοποιεί αυτή τη σύμβαση κάτω-αριστερά, Y-προς-τα-πάνω. Αν μεταφέρετε τα ένστικτά σας από τα γραφικά οθόνης, το πρώτο σας γράφημα θα σχεδιαστεί ανάποδα και θα ξεφύγει από το κάτω μέρος της σελίδας
Επομένως, η πραγματική δουλειά σε κάθε γράφημα είναι μία και μοναδική αντιστοίχιση: μετατρέψτε μια τιμή δεδομένων σε συντεταγμένη Y που σέβεται τη φορά Y-προς-τα-πάνω. Αποφασίστε ένα ορθογώνιο σχεδίασης, τέσσερις αριθμούς για την περιοχή στην οποία ζει το γράφημα, και μετά αντιστοιχίστε τη μικρότερη τιμή των δεδομένων σας στο κάτω άκρο και τη μεγαλύτερη στο πάνω. Για μια ράβδο με τιμή V σε μια κλίμακα που πηγαίνει από το 0 έως το MaxValue, το πάνω άκρο της ράβδου είναι PlotBottom + (V / MaxValue) * PlotHeight, και η ράβδος μεγαλώνει προς τα πάνω από το PlotBottom. Αν πετύχετε σωστά αυτή τη μία έκφραση, όλα τα υπόλοιπα είναι απλή λογιστική. Ο παρακάτω βοηθός κρατά τη γεωμετρία του γραφήματος και κάνει τη μετατροπή, ώστε ο κώδικας σχεδίασης να μην αγγίζει ποτέ δύο φορές την ακατέργαστη αριθμητική:
type
TPlotArea = record
Left, Bottom, Width, Height: Single; // σημεία PDF, αρχή κάτω-αριστερά
MaxValue: Single; // κορυφή της κλίμακας τιμών
end;
// Αντιστοιχίζει μια τιμή δεδομένων στη συντεταγμένη Y της μέσα στο γράφημα, με το Y να αυξάνεται προς τα πάνω.
function ValueToY(const Plot: TPlotArea; V: Single): Single;
begin
Result := Plot.Bottom + (V / Plot.MaxValue) * Plot.Height;
end;
Μια κρίση κρύβεται στο MaxValue. Αν το ορίσετε ακριβώς στη μεγαλύτερη τιμή των δεδομένων, η ψηλότερη ράβδος αγγίζει το πάνω άκρο του γραφήματος και φαίνεται κομμένη. Στρογγυλοποιήστε την σε έναν καθαρό αριθμό πάνω από το μέγιστο, ας πούμε στο επόμενο πολλαπλάσιο του 10 ή του 100, ώστε το γράφημα να έχει χώρο ελευθερίας και οι ετικέτες των γραμμών πλέγματος να διαβάζονται ως στρογγυλά νούμερα αντί για ό,τι έτυχε να είναι η κορυφή των δεδομένων
Ένα γράφημα ράβδων είναι απλώς ένας βρόχος πάνω από ορθογώνια
Με την αντιστοίχιση έτοιμη, ένα γράφημα ράβδων σχεδόν γράφεται μόνο του. Διαιρέστε το πλάτος του γραφήματος σε μία θέση ανά κατηγορία, αφήστε ένα κενό ανάμεσα στις ράβδους ώστε να μην ακουμπούν, και σχεδιάστε κάθε ράβδο ως ένα γεμισμένο ορθογώνιο του οποίου το ύψος προκύπτει από το ValueToY. Το Rectangle του HotPDF δέχεται την κάτω-αριστερή γωνία συν ένα πλάτος και ένα ύψος, κάτι που ταιριάζει ακριβώς με μια ράβδο που μεγαλώνει προς τα πάνω από τη γραμμή βάσης. Ορίστε πρώτα το χρώμα γεμίσματος, στήστε τη διαδρομή και μετά καλέστε το Fill για να τη βάψετε. Η ετικέτα κατηγορίας πηγαίνει κάτω από τη γραμμή βάσης, η τιμή πάνω από τη ράβδο:
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; // κενό ενός τετάρτου θέσης σε κάθε πλευρά
BarW := SlotW - Gap;
// Γραμμή βάσης (ο άξονας X) κατά μήκος του κάτω μέρους του γραφήματος.
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, Πλάτος, Ύψος
Page.Fill;
// Ετικέτα κατηγορίας κάτω από τη γραμμή βάσης, τιμή πάνω από τη ράβδο.
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;
Δύο λεπτομέρειες αξίζει να προσέξετε. Το Gap είναι κλάσμα της θέσης και όχι σταθερός αριθμός σημείων, ώστε οι ράβδοι να παραμένουν αναλογικά τοποθετημένες είτε σχεδιάζετε τέσσερις κατηγορίες είτε σαράντα. Και η ετικέτα τιμής τοποθετείται στο ίδιο ύψος που προκύπτει από το ValueToY που χρησιμοποιεί η ράβδος, οπότε κάθεται πάντα ακριβώς πάνω από τη δική της ράβδο αντί να αιωρείται σε μια εικαζόμενη μετατόπιση. Αν θέλετε οριζόντιες γραμμές πλέγματος πίσω από τις ράβδους, σχεδιάστε τις πριν από τον βρόχο: διαλέξτε τρεις ή τέσσερις στρογγυλές τιμές, τρέξτε το ValueToY σε καθεμία, και σχεδιάστε μια αχνή γραμμή σε όλο το πλάτος του γραφήματος σε αυτό το Y. Σχεδιάζοντάς τις πρώτα, τις τοποθετείτε πίσω από τις ράβδους στο μοντέλο στοίβαξης "του ζωγράφου" που χρησιμοποιεί το PDF
Άξονες, ενδείξεις (ticks) και ετικέτες είναι απλώς περισσότερες γραμμές και κείμενο
Το γράφημα δεν είναι ολοκληρωμένο μέχρι ένας αναγνώστης να μπορεί να καταλάβει τι σημαίνουν οι ράβδοι, και αυτό είναι εξ ολοκλήρου δουλειά των αξόνων. Ο κατακόρυφος άξονας είναι μια σχεδιασμένη γραμμή στο αριστερό άκρο του γραφήματος με μερικές ενδείξεις (ticks) και τις τιμές τους. Επαναχρησιμοποιήστε το ValueToY ώστε οι ενδείξεις να προσγειώνονται στην ίδια κλίμακα που χρησιμοποιούν οι ράβδοι, αλλιώς μια ράβδος και η γραμμή πλέγματός της διαφωνούν και το γράφημα ψεύδεται σιωπηλά:
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); // μικρή ένδειξη έξω από τον άξονα
Page.LineTo(Plot.Left, TickY);
Page.Stroke;
Page.TextOut(Plot.Left - 30, TickY - 3, 0, FormatFloat('0', TickV));
end;
end;
Οι ετικέτες είναι εκεί που τα γραφήματα χαλάνε πιο συχνά στην παραγωγική χρήση, και η αστοχία είναι πάντα η ίδια: κείμενο που χωρούσε στην οθόνη ξεπερνά τον χώρο του στο PDF. Μεγάλα ονόματα κατηγοριών συγκρούονται με τα γειτονικά τους, και εντοπισμένα ονόματα μηνών όπως "septembre" ή "Dezember" είναι πιο πλατιά από το αγγλικό "Sep" με το οποίο κάνατε τις δοκιμές σας. Δεν υπάρχει αυτόματη προσαρμογή μεγέθους να σας σώσει εδώ, οπότε αφήστε πραγματικό περιθώριο κάτω από τη γραμμή βάσης, μικρύνετε τη γραμματοσειρά κατά ένα-δύο σημεία για πυκνά σύνολα κατηγοριών, και αν τα ονόματα είναι πραγματικά μεγάλα, περιστρέψτε τα. Το TextOut δέχεται μια γωνία ως τρίτο όρισμα, οπότε περνώντας 90 η ετικέτα στέκεται όρθια και σας εξασφαλίζει χώρο χωρίς επικάλυψη. Δοκιμάστε τη διάταξη με την πλατύτερη αναμενόμενη ετικέτα σας, όχι την πιο σύντομη, πριν αποσταλεί η εξαγωγή
Γραφήματα γραμμών: μία πολυγραμμή μέσα από αντιστοιχισμένα σημεία
Ένα γράφημα γραμμών επαναχρησιμοποιεί ολόκληρη την αντιστοίχιση τιμών και αλλάζει μόνο το πώς συνδέονται τα σημεία. Αντί για ένα ορθογώνιο ανά κατηγορία, διατρέχετε τα δεδομένα μία φορά, μετατρέπετε κάθε τιμή στο δικό της (X, Y) με το ValueToY, και ενώνετε τα σημεία μεταξύ τους με ένα μοναδικό MoveTo ακολουθούμενο από κλήσεις LineTo, με περίγραμμα στο τέλος. Το πρώτο σημείο ανοίγει τη διαδρομή· κάθε επόμενο σημείο την επεκτείνει:
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) // ανοίγει τη διαδρομή στο πρώτο σημείο
else
Page.LineTo(X, Y); // την επεκτείνει σε κάθε επόμενο σημείο
end;
Page.Stroke; // ένα stroke ζωγραφίζει ολόκληρη την πολυγραμμή
end;
Προσέξτε τη διαφορά στην απόσταση. Ένα γράφημα ράβδων διαιρεί το πλάτος με τον αριθμό των ράβδων, επειδή κάθε ράβδος κατέχει μία θέση. Ένα γράφημα γραμμών διαιρεί με τον αριθμό των διαστημάτων, Count - 1, επειδή το πρώτο και το τελευταίο σημείο βρίσκονται στα άκρα του γραφήματος και η γραμμή εκτείνεται στα κενά ανάμεσά τους. Η σύγχυση αυτών των δύο είναι ο συνηθέστερος λόγος για τον οποίο ένα γράφημα γραμμών παρεκκλίνει κατά μισή θέση από το γράφημα ράβδων που προορίζεται να επικαλύψει. Αν θέλετε ένα σημάδι σε κάθε σημείο δεδομένων, ρίξτε έναν μικρό Circle και Fill σε κάθε (X, Y) αφού σχεδιαστεί η πολυγραμμή
Γραφήματα πίτας: τόξα, ή σφήνες αν το κρατήσετε απλό
Τα κομμάτια πίτας είναι το μόνο σχήμα που χρειάζεται τριγωνομετρία, επειδή μια σφήνα οριοθετείται από δύο ακτίνες και ένα τόξο. Η ειλικρινής εκδοχή σαρώνει το τόξο βηματίζοντας με μικρά ευθύγραμμα τμήματα κατά μήκος της περιφέρειας, κάτι που προσεγγίζει την καμπύλη τόσο κοντά ώστε κανένας αναγνώστης να μην μπορεί να το διακρίνει. Η γωνία σάρωσης κάθε κομματιού είναι το μερίδιό του από το σύνολο, (Value / Total) * 2π, και συσσωρεύετε την τρέχουσα γωνία καθώς προχωράτε ολόγυρα:
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 μοίρες ανά τμήμα
Page.SetRGBFillColor(Colors[I]);
Page.MoveTo(CX, CY); // κορυφή σφήνας στο κέντρο
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); // κλείνει πίσω στο κέντρο
Page.Fill;
Start := Start + Sweep; // προχωρά στο επόμενο κομμάτι
end;
end;
Η διαδρομή από το κέντρο προς τα έξω, πρώτα η κορυφή, μετά το τόξο, μετά πίσω στην κορυφή, δίνει μια κλειστή σφήνα που το Fill βάφει συμπαγώς. Ο αριθμός των τμημάτων ανταλλάσσει την ομαλότητα με το μέγεθος της διαδρομής: περίπου δύο μοίρες ανά βήμα φαίνονται στρογγυλές σε οποιαδήποτε λογική ακτίνα χωρίς να παράγουν έναν τεράστιο content stream. Αν δεν χρειάζεστε έναν πραγματικό κύκλο, μπορείτε να παραλείψετε εντελώς την τριγωνομετρία και να αποδώσετε τα ίδια δεδομένα ως μία μόνο οριζόντια στοιβαγμένη ράβδο, όπου το πλάτος κάθε τμήματος είναι ανάλογο με το μερίδιό του. Αυτό είναι συχνά πιο ευανάγνωστο από μια πίτα ούτως ή άλλως, και είναι απλώς ο κώδικας της ράβδου με τα ορθογώνια απλωμένα το ένα δίπλα στο άλλο. Προτιμήστε την εκδοχή με το τόξο μόνο όταν ο σχεδιασμός απαιτεί έναν πραγματικό κύκλο
Τίποτα από όλα αυτά δεν εξαρτάται από κάποια εγκατεστημένη βιβλιοθήκη γραφημάτων, κάτι που είναι το αθόρυβο πλεονέκτημα της απευθείας σχεδίασης των αρχέγονων στοιχείων. Οι ίδιες κλήσεις σχεδίασης σε καμβά που τοποθετούν ένα λογότυπο ή ένα κουτί υπογραφής, κατασκευάζουν αυτά τα γραφήματα, και το ίδιο TextOut που βάζει ετικέτα σε ένα πεδίο φόρμας, βάζει ετικέτα και σε έναν άξονα. Βάλτε τη γεωμετρία του γραφήματος σε μια εγγραφή, αντιστοιχίστε τις τιμές στο Y μία φορά, και ένα γράφημα ράβδων, γραμμών ή πίτας γίνεται μια σύντομη ρουτίνα πάνω από τα Rectangle, LineTo και Circle που μπορείτε να ρίξετε σε οποιαδήποτε αναφορά. Οι κλήσεις Rectangle, MoveTo, LineTo, Circle, Fill, Stroke και TextOut που χρησιμοποιούνται εδώ αποτελούν μέρος του HotPDF Delphi Component για Delphi και C++Builder