HotXLS rendert Datumsachsen in Excel-Charts mit Kalenderarithmetik, nicht mit fixen Tageszählungen. Die Klasse TXLSChartDateAxisTransform in der Unit lxChart hält jede Koordinate als Workbook-Serial, berechnet jeden Monats- oder Jahrestick direkt vom Achsenminimum aus über IncMonth und IncYear und klemmt Monatsenden an den realen Kalender, sodass eine am 31. Januar verankerte Serie Ticks am 28. Februar, 31. März und 30. April bekommt, statt wegzurutschen. Dasselbe Transform wählt Tages-, Monats- oder Jahreseinheiten automatisch und wird von den HTML-, SVG- und seitenbasierten PDF-Chart-Renderern geteilt
Der Bug, den das verhindert, ist jedem vertraut, der schon einmal ein Monatsende gechartet hat. Ein Renderer, der einen Monat als 30 Tage behandelt, driftet bis Ende des ersten Jahres fünf Tage zu früh. Ein Renderer, der es schlauer macht und Kalendermonate benutzt, aber jeden Tick vom vorherigen weiterschreitet, versagt leiser: Aus dem 31. Januar wird der 28. Februar, der nächste Schritt landet auf dem 28. März, und jeder Tick danach bleibt am 28. festgenagelt. Das Chart wirkt plausibel, die Labels sind falsch, und niemand merkt es, bis ein Controller fragt, warum die März-Bilanz drei Tage vor dem Quartalsende eingezeichnet ist
Warum driften Monatsticks, wenn sie akkumuliert werden?
Monatsticks driften, weil Klemmen Information wegwirft. Sobald der 31. Januar auf den 28. Februar geklemmt wurde, ist die Tatsache verloren, dass die Serie den 31. wollte, und jeder Schritt vom geklemmten Wert erbt den Verlust. TXLSChartDateAxisTransform.BuildTicks schreitet nie von einem vorherigen Tick weiter. Tick i wird immer als AddUnits(MinValue, UnitKind, Step * i, ...) berechnet, also min + Einheit × Index, gemessen vom Achsenminimum. AddUnits addiert die Anzahl bei Tageseinheiten direkt auf den Serial und konvertiert bei Monats- und Jahreseinheiten den Serial in ein echtes Datum, ruft IncMonth oder IncYear auf und rechnet zurück. Die Schleife ist außerdem auf jeder Seite begrenzt: Das Ergebnis-Array ist auf 4096 Einträge gedeckelt, was auch immer der Aufrufer verlangt, Step * i wird vor der Multiplikation gegen MaxInt geprüft, und die Erzeugung stoppt, sobald ein Tick nicht mehr steigt oder das Achsenmaximum passiert
uses
SysUtils, lxChart;
var
Ticks: TXLSChartDateValues;
MinSerial, MaxSerial: Double;
I, Count: Integer;
begin
// 1900-Datumssystem (Dates1904 = False), am Monatsende verankerte Serie
MinSerial := TXLSChartDateAxisTransform.DateTimeToSerial(
EncodeDate(2026, 1, 31), False);
MaxSerial := TXLSChartDateAxisTransform.DateTimeToSerial(
EncodeDate(2026, 12, 31), False);
// Ein-Monats-Schritt, Ziel 12 Ticks, nie mehr als 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;
Eine ehrliche Näherung steckt in BuildTicks. Ist der angeforderte Schritt null, schätzt die Methode eine Schrittweite, indem sie einen Monat als 30 Tage und ein Jahr als 365 behandelt, und rundet dann den rohen Schritt auf ein 1-, 2- oder 5-Faches einer Zehnerpotenz. Diese Schätzung entscheidet nur, wie viele Einheiten zwischen Ticks liegen. Die Tick-Positionen selbst kommen weiterhin von IncMonth und IncYear, die Näherung kann also die Tick-Dichte ändern, aber nie einen Tick vom Kalender wegrücken
Warum Workbook-Serials behalten, statt zu TDateTime zu konvertieren?
Eine Datumsachse in HotXLS behält Workbook-Serials als ihren Koordinatenraum, weil Delphis TDateTime keinen Platz für Serial 60 hat – den Phantom-29. Februar 1900, den das 1900er-Datumssystem als Kompatibilitätsquirk geerbt hat. TrySerialToDateTime verschiebt Serials unter 60 um einen Tag und faltet Serial 60 auf den 28. Februar, was für das Labeln eines einzelnen Punkts passt, für Geometrie aber fatal ist: Konvertiert man jeden Punkt zuerst nach TDateTime, liegen die beiden Tage neben dem Phantomtag am Ende eine Einheit näher beieinander, als Excel sie zeichnet, sodass alles, was vor März 1900 geplottet ist, relativ zu allem danach verschoben wird. TXLSChartDateAxisTransform.FormatValue behandelt denselben Slot als Sonderfall und labelt ihn 1900-02-29, genau wie Excel. In einer 1904er-Arbeitsmappe ist Serial 0 der 1. Januar 1904, und das Transform wendet den festen 1462-Tage-Offset in beide Richtungen an; die ganze Geschichte beider Epochen erzählt unser Artikel zu Excel-Datumsserials, dem 1904er-System und Zahlenformaten
Wie wählt HotXLS Tage, Monate oder Jahre automatisch?
TXLSChartDateAxisTransform.DetectUnit wählt die feinste Kalendereinheit, die die Lücken in den Daten tatsächlich brauchen. Die Methode wirft zuerst NaN- und unendliche Werte weg, sortiert den Rest einmal in O(n log n) und überspringt doppelte Daten. Ist eine benachbarte Lücke kürzer als ein Kalendermonat, ist die Einheit Tage; andernfalls, ist eine Lücke kürzer als ein Kalenderjahr, ist die Einheit Monate; andernfalls Jahre. Der Test lautet IncMonth(Previous, 1) > Current, nicht eine 30-Tage-Schwelle, sodass eine Monatsserie über den 31. Januar, 28. Februar, 31. März korrekt als monatliche Daten erkannt wird. Mit weniger als zwei verschiedenen gültigen Daten gibt es keine Lücke zu messen, und die Methode fällt auf Tage zurück
ResolveUnits verschmilzt dann die erkannte Einheit mit dem, was das Chart deklariert. Eine explizite Basiseinheit wird respektiert, und eine fehlende Major- oder Minor-Einheit erbt die erkannte Einheit, wenn diese gröber als die Basis ist. Der letzte Schritt ist der, den Leute übersehen: Ist die effektive Major- oder Minor-Einheit feiner als die Basiseinheit, wird die Basis abgesenkt. Die Basiseinheit ist das, worauf Datenpunkte normalisiert werden, bevor es überhaupt Ticks gibt, wobei Normalize einen Wert auf seinen Tag, den Ersten seines Monats oder den 1. Januar schnappt – eine monatliche Basis unter einer täglichen Major-Einheit würde also einen Monat voller Punkte in einen Slot kollabieren, bevor der Tick-Code sie je zu Gesicht bekommt. Die Tick-Dichte ist ebenfalls begrenzt, durch die Plot-Länge, die gemessene Breite der Achsenschrift, das Zahlenformat der Labels und die Projektion rotierter Labels, und jedes Backend zeichnet Labels, Tick-Marks und Gitterlinien aus einem gemeinsamen, begrenzten Tick-Array, wobei ein Minor-Tick, der auf einen Major-Tick fällt, nur einmal gezeichnet wird
Was speichert das dateAx-Element, und was bedeutet Auslassen?
In ChartML ist jede Kalendereinstellung einer Datumsachse optional, und ein Auslassen ist nicht dasselbe wie das Schreiben ihres Default-Werts. Der Typ CT_DateAx in ECMA-376 Part 1, §21.2 (DrawingML Charts) lässt baseTimeUnit, majorTimeUnit, minorTimeUnit und auto jeweils erscheinen oder nicht, und ein ausgelassenes baseTimeUnit sagt Excel, dass es selbst entscheiden soll – was von einem expliziten days verschieden ist. TXLSXChartAxis legt daher jeden Wert neben einem Presence-Flag ab: BaseTimeUnit mit BaseTimeUnitSet, MajorTimeUnit mit MajorTimeUnitSet, MinorTimeUnit mit MinorTimeUnitSet und AutoDateAxis mit AutoDateAxisSet. Eine Wertzuweisung setzt das Flag, das Löschen des Flags bringt das Element zurück ins Auslassen, und der XLSX-Writer gibt ein Element nur aus, wenn sein Flag gesetzt ist. Beachten Sie, dass auto die automatische Kategorie-oder-Datum-Erkennung meint, nicht automatische Tick-Einheiten
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; // schreibt c:dateAx
Axis.BaseTimeUnit := xlsxChartTimeDays; // setzt auch BaseTimeUnitSet
Axis.MajorTimeUnit := xlsxChartTimeMonths;
Axis.MajorUnit := 1; // > 0 markiert majorUnit als gesetzt
Axis.NumberFormat := 'mmm yyyy';
Axis.NumberFormatSourceLinked := False;
Axis.TextStyle.Rotation := -45; // Grad; gespeichert als 1/60000
// AutoDateAxis unangetastet gelassen: kein c:auto-Element wird geschrieben
Book.SaveAs('month-end-balance.xlsx');
finally
Book.Free;
end;
end;
Labels und Orientierung folgen denselben presence-bewussten Regeln. Das Achsen-numFmt sitzt nach dem Titel und vor den Tick-Marks, sein sourceLinked-Attribut ist per Default true, und TXLSXChartAxis hält den Formatcode, den Source-Linked-Wert und NumberFormatSet getrennt, sodass ein leerer Code nie für ein fehlendes Element gehalten wird. Die Textrotation wird in 1/60000 Grad gespeichert, positive Werte bedeuten im Uhrzeigersinn; TextStyle.Rotation nimmt schlichte Grad, die SVG-Ausgabe benutzt den Winkel, wie er ist, und das seitenbasierte Backend, dessen positive Winkel gegen den Uhrzeigersinn laufen, dreht das Vorzeichen an einer gemeinsamen Grenzstelle um. Eine umgekehrte maxMin-Achse spiegelt Major- und Minor-Ticks, Gitterlinien, Datenpunkte, Trendlinien und Fehlerbalken zusammen – und niemals nur die Labels. Wenn Sie eine anderswo geschriebene Arbeitsmappe öffnen, sagen Ihnen die Flags, was der Autor tatsächlich angegeben hat
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; // nochmals speichern, und die ausgelassenen Elemente bleiben ausgelassen
end;
end;
Wo endet das Kalendermodell?
Das Kalendermodell endet bei Formaten, die keine Kalendereinheiten ausdrücken können, und HotXLS tut nicht so, als wäre es anders. ODF 1.2 bietet chart:interval-major, eine schlichte Zahl ohne Begriff von Tagen, Monaten oder Jahren, sodass eine als ODS gespeicherte Excel-Monatsachse ihre Einheit nicht tragen kann und HotXLS kein nicht standardisiertes Attribut erfindet, um eine vorzutäuschen. Auf der Legacy-BIFF8-Seite hält der AxcExt-Record neun feste 16-Bit-Wörter, und seine automatischen Flags maskieren Felder nur, statt sie zu entfernen; HotXLS bewahrt das maskierte Minimum, Maximum, Intervall, die Einheit und die Schnittpunktwerte beim Lesen und Zurückschreiben, hält unbekannte Unit-Codes im Classic-Modell unangetastet und mappt nur die gültigen Datumseinheiten 0, 1 und 2 (Tage, Monate, Jahre) nach XLSX
Datumsachsen sind eine Schicht eines Chart-Modells, das auch Dateien überleben muss, die es nicht selbst erzeugt hat. Kombiniert eine Excel-Arbeitsmappe eine Datumsachsen-Linie mit einer Column-Serie auf einer Sekundärachse, zeigt unser Artikel zu bewahrtem ChartML und Kombinations-Charts, wie HotXLS das originale Chart-XML Byte für Byte zurückspielt, wenn nichts geändert wurde, und typisierte Edits hineinmergt, wenn doch, und unser Überblick über Charts, Bilder und Drawings deckt die oben benutzten Anker- und Serien-APIs ab. Das alles liefert die HotXLS Delphi Component für Delphi und C++Builder mit, die XLS- und XLSX-Charts ohne Excel-Automatisierung liest, schreibt und rendert