Το HotXLS αποδίδει τους αξόνες ημερομηνιών των charts Excel με αριθμητική ημερολογίου, όχι με σταθερές μετρήσεις ημερών. Η κλάση TXLSChartDateAxisTransform στη μονάδα lxChart κρατάει κάθε συντεταγμένη ως serial του workbook, υπολογίζει κάθε tick μήνα ή έτους απευθείας από το ελάχιστο του άξονα μέσω IncMonth και IncYear, και σφηνώνει τα τέλη μήνων στο πραγματικό ημερολόγιο, ώστε μια σειρά αγκυρωμένη στις 31 Ιανουαρίου να παίρνει ticks στις 28 Φεβρουαρίου, 31 Μαρτίου και 30 Απριλίου αντί να γλιστράει. Ο ίδιος transform διαλέγει αυτόματα μονάδες ημέρας, μήνα ή έτους και μοιράζεται από τους renderers chart HTML, SVG και σελιδοποιημένου PDF
Το ελάττωμα που αυτό προλαβαίνει είναι οικείο σε όποιον έχει σχεδιάσει chart με κλείσιμο τέλους μήνα. Ένας renderer που αντιμετωπίζει τον μήνα ως 30 ημέρες γλιστράει πέντε ημέρες νωρίτερα ως το τέλος της πρώτης χρονιάς. Ένας renderer πιο έξυπνος που χρησιμοποιεί μήνες ημερολογίου, αλλά προωθεί κάθε tick από το προηγούμενο, αποτυγχάνει πιο σιωπηλά: η 31 Ιανουαρίου γίνεται 28 Φεβρουαρίου, το επόμενο βήμα προσγειώνεται στις 28 Μαρτίου, και κάθε tick μετά εκείνο μένει καρφωμένο στο 28. Το chart φαίνεται πειστικό, οι ετικέτες είναι λάθος, και δεν το προσέχει κανείς μέχρι ένας λογιστής να ρωτήσει γιατί το υπόλοιπο Μαρτίου σχεδιάζεται τρεις ημέρες πριν το τέλος του τριμήνου
Γιατί τα ticks μήνα γλιστράνε όταν αθροίζονται;
Τα ticks μήνα γλιστράνε επειδή η σφήνωση πετάει πληροφορία. Μόλις η 31 Ιανουαρίου σφηνωθεί στις 28 Φεβρουαρίου, το γεγονός ότι η σειρά ήθελε την 31η έχει χαθεί, και κάθε βήμα από τη σφηνωμένη τιμή κληρονομεί την απώλεια. Το TXLSChartDateAxisTransform.BuildTicks δεν πατάει ποτέ πάνω σε προηγούμενο tick. Το tick i υπολογίζεται πάντα ως AddUnits(MinValue, UnitKind, Step * i, ...), δηλαδή min + μονάδα × δείκτης μετρούμενος από το ελάχιστο του άξονα. Το AddUnits προσθέτει το πλήθος κατευθείαν στο serial για μονάδες ημερών, και για μονάδες μηνών και ετών μετατρέπει τον serial σε πραγματική ημερομηνία, καλεί IncMonth ή IncYear, και μετατρέπει πίσω. Ο βρόχος είναι επίσης φραγμένος από κάθε πλευρά: το array αποτελεσμάτων έχει πλαφόν 4096 εγγραφές ό,τι κι αν ζητήσει ο καλών, το Step * i ελέγχεται απέναντι στο MaxInt πριν πολλαπλασιαστεί, και η παραγωγή σταματά μόλις ένα tick αρνηθεί να μεγαλώσει ή περάσει το μέγιστο του άξονα
uses
SysUtils, lxChart;
var
Ticks: TXLSChartDateValues;
MinSerial, MaxSerial: Double;
I, Count: Integer;
begin
// Σύστημα ημερομηνιών 1900 (Dates1904 = False), σειρά αγκυρωμένη σε τέλος μήνα
MinSerial := TXLSChartDateAxisTransform.DateTimeToSerial(
EncodeDate(2026, 1, 31), False);
MaxSerial := TXLSChartDateAxisTransform.DateTimeToSerial(
EncodeDate(2026, 12, 31), False);
// Βήμα ενός μήνα, στόχος 12 ticks, ποτέ πάνω από 64
Count := TXLSChartDateAxisTransform.BuildTicks(MinSerial, MaxSerial,
1, xcduMonths, False, 12, 64, Ticks);
for I := 0 to Count - 1 do
Writeln(TXLSChartDateAxisTransform.FormatValue(Ticks[I], False));
// 2026-01-31, 2026-02-28, 2026-03-31, 2026-04-30 ... 2026-12-31
end;
Υπάρχει μία ειλικρινής προσέγγιση μέσα στο BuildTicks. Όταν το ζητούμενο βήμα είναι μηδέν, η μέθοδος εκτιμά ένα μέγεθος βήματος αντιμετωπίζοντας τον μήνα ως 30 ημέρες και το έτος ως 365, και μετά στρογγυλοποιεί το ακατέργαστο βήμα σε πολλαπλάσιο 1, 2 ή 5 μιας δύναμης του δέκα. Αυτή η εκτίμηση αποφασίζει μόνο πόσες μονάδες μεσολαβούν ανάμεσα στα ticks. Οι ίδιες οι θέσεις των ticks εξακολουθούν να βγαίνουν από IncMonth και IncYear, οπότε η προσέγγιση μπορεί να αλλάξει την πυκνότητα των ticks αλλά δεν μετακινεί ποτέ ένα tick έξω από το ημερολόγιο
Γιατί κρατάμε serials workbook αντί να μετατρέπουμε σε TDateTime;
Ένας άξονας ημερομηνιών στο HotXLS κρατάει serials workbook ως χώρο συντεταγμένων επειδή το TDateTime του Delphi δεν έχει θέση για τον serial 60, τη φανταστική 29 Φεβρουαρίου 1900 που το σύστημα ημερομηνιών 1900 κληρονόμησε ως ιδιορρυθμία συμβατότητας. Το TrySerialToDateTime μετατοπίζει τους serials κάτω από 60 κατά μία ημέρα και διπλώνει τον serial 60 πάνω στις 28 Φεβρουαρίου, που είναι μια χαρά για να ετικετοποιήσεις ένα μεμονωμένο σημείο αλλά μοιραίο για γεωμετρία: αν μετατρέψεις πρώτα κάθε σημείο σε TDateTime, οι δύο ημέρες εκατέρωθεν της φανταστικής ημέρας καταλήγουν μία μονάδα πιο κοντά μεταξύ τους απ' όσο τις σχεδιάζει το Excel, οπότε ό,τι έχει σχεδιαστεί πριν τον Μάρτιο του 1900 μετατοπίζεται σε σχέση με ό,τι έρχεται μετά. Το TXLSChartDateAxisTransform.FormatValue αντιμετωπίζει την ίδια θέση ως ειδική περίπτωση και την ετικετοποιεί 1900-02-29 ακριβώς όπως κάνει το Excel. Σε ένα workbook 1904, ο serial 0 είναι η 1 Ιανουαρίου 1904, και ο transform εφαρμόζει τη σταθερή μετατόπιση 1462 ημερών και προς τις δύο κατευθύνσεις· όλη η ιστορία των δύο εποχών βρίσκεται στο άρθρο μας για τα serials ημερομηνιών του Excel, το σύστημα 1904 και τις μορφές αριθμών
Πώς διαλέγει το HotXLS αυτόματα ημέρες, μήνες ή έτη;
Το TXLSChartDateAxisTransform.DetectUnit διαλέγει την πιο λεπτή μονάδα ημερολογίου που χρειάζονται πραγματικά τα κενά στα δεδομένα. Η μέθοδος πετάει πρώτα τις τιμές NaN και τις άπειρες, ταξινομεί τις υπόλοιπες μία φορά σε O(n log n), και παραλείπει διπλές ημερομηνίες. Αν οποιοδήποτε διαδοχικό κενό είναι πιο σύντομο από έναν ημερολογιακό μήνα, η μονάδα είναι ημέρες· αλλιώς, αν κάποιο κενό είναι πιο σύντομο από ένα ημερολογιακό έτος, η μονάδα είναι μήνες· αλλιώς είναι έτη. Ο έλεγχος είναι IncMonth(Previous, 1) > Current, όχι όριο 30 ημερών, ώστε μια σειρά τέλους μήνα με 31 Ιανουαρίου, 28 Φεβρουαρίου, 31 Μαρτίου να ανιχνεύεται σωστά ως μηνιαία δεδομένα. Με λιγότερες από δύο διακριτές έγκυρες ημερομηνίες δεν υπάρχει κενό για μέτρηση, και η μέθοδος επιστρέφει σε ημέρες
Το ResolveUnits συγχωνεύει μετά την ανιχνευμένη μονάδα με ό,τι δηλώνει το chart. Μια ρητή βασική μονάδα γίνεται σεβαστή, και μια απόν major ή minor μονάδα κληρονομεί την ανιχνευμένη όταν αυτή είναι πιο χοντρή από τη βασική. Το τελευταίο βήμα είναι αυτό που διαφεύγει: αν η ενεργή major ή minor μονάδα είναι πιο λεπτή από τη βασική, η βασική κατεβαίνει να συμβαδίσει. Η βασική μονάδα είναι αυτή στην οποία κανονικοποιούνται τα σημεία δεδομένων πριν υπάρξει οποιοδήποτε tick, με το Normalize να προσγειώνει μια τιμή στην ημέρα της, την πρώτη του μήνα της ή την 1 Ιανουαρίου, ώστε μια μηνιαία βάση κάτω από μια ημερήσια major μονάδα θα συμπίεζε έναν ολόκληρο μήνα σημείων σε μία θέση προτού τα δει καθόλου ο κώδικας των ticks. Η πυκνότητα των ticks είναι και αυτή φραγμένη, από το μήκος της σχεδίασης, το μετρημένο πλάτος της γραμματοσειράς του άξονα, τη μορφή αριθμών των ετικετών και την προβολή των περιστραμμένων ετικετών, και κάθε backend ζωγραφίζει ετικέτες, tick marks και πλέγματα από έναν κοινό, φραγμένο array ticks, με ένα minor tick που συμπίπτει με major tick να ζωγραφίζεται μόνο μία φορά
Τι αποθηκεύει το στοιχείο dateAx, και τι σημαίνει η παράλειψη;
Στο ChartML κάθε ρύθμιση ημερολογίου σε έναν άξονα ημερομηνιών είναι προαιρετική, και η παράλειψή της δεν είναι το ίδιο με το να γράψεις την προεπιλογή της. Ο τύπος CT_DateAx στο ECMA-376 Part 1, §21.2 (DrawingML Charts) αφήνει τα baseTimeUnit, majorTimeUnit, minorTimeUnit και auto το καθένα να εμφανίζεται ή όχι, και ένα παραλειπόμενο baseTimeUnit λέει στο Excel να αποφασίσει μόνο του, που είναι διαφορετικό από ένα ρητό days. Το TXLSXChartAxis αποθηκεύει επομένως κάθε τιμή δίπλα σε μια σημαία παρουσίας: BaseTimeUnit με BaseTimeUnitSet, MajorTimeUnit με MajorTimeUnitSet, MinorTimeUnit με MinorTimeUnitSet, και AutoDateAxis με AutoDateAxisSet. Το να αναθέσεις τιμή θέτει τη σημαία της, το να καθαρίσεις τη σημαία επιστρέφει το στοιχείο σε παράλειψη, και ο writer XLSX εκπέμπει ένα στοιχείο μόνο όταν η σημαία του είναι τεθειμένη. Προσέξτε ότι το auto σημαίνει αυτόματη ανίχνευση category-ή-ημερομηνίας, όχι αυτόματες μονάδες ticks
var
Book: TXLSXWorkbook;
Sheet: TXLSXWorksheet;
Chart: TXLSXChart;
Axis: TXLSXChartAxis;
M: Integer;
begin
Book := TXLSXWorkbook.Create;
try
Sheet := Book.Sheets.Add('Close');
Sheet.Cells[1, 1].Value := 'Month end';
Sheet.Cells[1, 2].Value := 'Balance';
for M := 1 to 12 do
begin
Sheet.Cells[M + 1, 1].Value :=
EncodeDate(2026, M, DaysInAMonth(2026, M)); // DateUtils
Sheet.Cells[M + 1, 2].Value := 1000 + M * 75;
end;
Chart := Sheet.AddLineChart('Month-end balance',
'Close!$A$2:$A$13', 'Close!$B$2:$B$13', 15, 1, 32, 9);
Axis := Chart.CategoryAxis;
Axis.Kind := xlsxAxisDate; // γράφει c:dateAx
Axis.BaseTimeUnit := xlsxChartTimeDays; // θέτει και το BaseTimeUnitSet
Axis.MajorTimeUnit := xlsxChartTimeMonths;
Axis.MajorUnit := 1; // > 0 σηματοδοτεί το majorUnit ως τεθειμένο
Axis.NumberFormat := 'mmm yyyy';
Axis.NumberFormatSourceLinked := False;
Axis.TextStyle.Rotation := -45; // μοίρες· αποθηκεύεται ως 1/60000
// Το AutoDateAxis μένει ανέγγιχτο: δεν γράφεται στοιχείο c:auto
Book.SaveAs('month-end-balance.xlsx');
finally
Book.Free;
end;
end;
Οι ετικέτες και ο προσανατολισμός ακολουθούν τους ίδιους κανόνες με επίγνωση παρουσίας. Το numFmt του άξονα κάθεται μετά τον τίτλο και πριν από τα tick marks, το attribute του sourceLinked έχει προεπιλογή true, και το TXLSXChartAxis κρατάει τον κωδικό μορφής, την τιμή source-linked και το NumberFormatSet ξεχωριστά, ώστε ένας κενός κωδικός να μην ξεγελιέται ποτέ για απόν στοιχείο. Η περιστροφή κειμένου αποθηκεύεται σε 1/60000 της μοίρας με θετικές τιμές να σημαίνουν δεξιόστροφα· το TextStyle.Rotation παίρνει σκέτες μοίρες, η έξοδος SVG χρησιμοποιεί τη γωνία ως έχει, και το σελιδοποιημένο backend, του οποίου οι θετικές γωνίες τρέχουν αριστερόστροφα, αντιστρέφει το πρόσημο σε ένα κοινό όριο. Ένας αντιστραμένος άξονας maxMin κατοπτρίζει μαζί τα major και minor ticks, τα πλέγματα, τα σημεία δεδομένων, τις γραμμές τάσης και τις μπάρες σφάλματος, και ποτέ μόνο τις ετικέτες. Όταν ανοίγετε ένα workbook γραμμένο αλλού, οι σημαίες σας λένε τι όρισε πραγματικά ο συγγραφέας
var
Book: TXLSXWorkbook;
Axis: TXLSXChartAxis;
begin
Book := TXLSXWorkbook.Create;
try
if Book.Open('forecast.xlsx') <> 1 then
raise Exception.Create('Cannot open workbook');
Axis := Book.Sheets[0].Charts[0].CategoryAxis;
if Axis.Kind = xlsxAxisDate then
begin
if Axis.BaseTimeUnitSet then
Writeln('baseTimeUnit = ', Ord(Axis.BaseTimeUnit))
else
Writeln('baseTimeUnit omitted: Excel chooses at render time');
if Axis.AutoDateAxisSet then
Writeln('auto = ', Axis.AutoDateAxis);
end;
finally
Book.Free; // αποθηκεύστε το ξανά και όσα στοιχεία παραλείφθηκαν μένουν παραλειμμένα
end;
end;
Πού σταματά το μοντέλο ημερολογίου;
Το μοντέλο ημερολογίου σταματά στις μορφές που δεν μπορούν να εκφράσουν ημερολογιακές μονάδες, και το HotXLS δεν προσποιείται το αντίθετο. Το ODF 1.2 προσφέρει το chart:interval-major, που είναι ένας σκέτος αριθμός χωρίς έννοια ημερών, μηνών ή ετών, οπότε ένας μηνιαίος άξονας Excel αποθηκευμένος ως ODS δεν μπορεί να κουβαλήσει τη μονάδα του και το HotXLS δεν εφευρίσκει μη πρότυπο attribute για να προσποιηθεί ότι τη φέρνει. Στην πλευρά του legacy BIFF8, το record AxcExt κρατάει εννιά σταθερές λέξεις 16-bit, και οι σημαίες αυτόματου μόνο μασκάρουν πεδία αντί να τα αφαιρούν· το HotXLS διατηρεί την masked ελάχιστη και μέγιστη τιμή, το διάστημα, τη μονάδα και τις τιμές διασταύρωσης σε ανάγνωση και επανεγγραφή, κρατάει άγνωστους κωδικούς μονάδων ανέπαφους στο Classic μοντέλο, και αντιστοιχίζει μόνο τις έγκυρες μονάδες ημερομηνιών 0, 1 και 2 (ημέρες, μήνες, έτη) σε XLSX
Οι άξονες ημερομηνιών είναι ένα επίπεδο ενός μοντέλου chart που πρέπει επίσης να επιβιώνει από αρχεία που δεν δημιούργησε ο ίδιος. Όταν ένα workbook Excel συνδυάζει μια γραμμή με άξονα ημερομηνιών με μια σειρά στηλών σε δευτερεύοντα άξονα, το άρθρο μας για τα διατηρημένα ChartML και τα combination charts δείχνει πώς το HotXLS ξαναπαίζει το αρχικό chart XML byte προς byte όταν τίποτα δεν άλλαξε και συγχωνεύει τυποποιημένες επεξεργασίες μέσα του όταν κάτι άλλαξε, και η επισκόπησή μας για charts, εικόνες και drawings καλύπτει τα API αγκύρωσης και σειρών που χρησιμοποιήθηκαν παραπάνω. Όλα αυτά έρχονται μέσα στο HotXLS Delphi Component για Delphi και C++Builder, που διαβάζει, γράφει και αποδίδει charts XLS και XLSX χωρίς Excel automation