Ein Monatsende-Saldo-Chart, das am 31. Januar 2026 beginnt, sollte seinen April-Tick auf den 30. April legen. Jeden Tick vom vorherigen mit Kalendermonaten weiterschreiten lassen, und die Labels kommen als 2026-01-31, 2026-02-28, 2026-03-28, 2026-04-28 heraus, mit jedem späteren Tick festgebacken auf dem 28. Stattdessen um flache 30 Tage weiterschreiten, und der Februar-Tick landet auf dem 2. März, während der Dezember-Tick auf dem 27. Dezember landet. Die Regel, die beide Fallen überlebt, lässt sich kurz sagen: Jeden Tick aus dem Achsen-Minimum berechnen und Monatsenden auf den echten Kalender klemmen
Bei einem Quartalsabschluss-Chart ist der erste Fehler der gefährliche, denn das Bild sieht trotzdem richtig aus. Die Linie steigt, wo sie soll, die Labels stehen gleichmäßig verteilt, und der März-Saldo sitzt drei Tage vor dem Quartalsende. Ein Controller, der das Chart mit der Kontenführung vergleicht, ist die Person, die es findet, und bis dahin ist das Chart meist schon in ein Vorstandspaket geklebt
Warum driften Monats-Ticks, wenn sie akkumuliert werden?
Monats-Ticks driften, weil Clamping Information wegwirft. Sobald der 31. Januar auf den 28. Februar geklemmt wurde, ist die Tatsache, dass die Serie den 31. wollte, verloren, und jeder Schritt vom geklemmten Wert erbt den Verlust. In HotXLS schreitet TXLSChartDateAxisTransform.BuildTicks in der Unit lxChart nie von einem vorherigen Tick fort: Tick i ist immer AddUnits(MinValue, UnitKind, Step * i, ...), März ist also zwei Monate nach dem 31. Januar und kommt als 31. März heraus. AddUnits addiert die Zahl bei Tageseinheiten direkt auf die Serial; bei Monats- und Jahreseinheiten wandelt es die Serial in ein echtes Datum, ruft IncMonth oder IncYear und wandelt zurück
Die Schleife ist auf jeder Seite begrenzt. Das Ergebnis-Array übersteigt nie 4096 Einträge, was auch immer der Aufrufer als Maximum übergibt, Step * i wird vor der Multiplikation gegen MaxInt geprüft, und die Erzeugung stoppt, sobald ein Tick nicht mehr steigt oder das Achsen-Maximum passiert. Ein feindliches Workbook mit einer Spanne von ein paar Millionen Jahren produziert ein kurzes Array und kehrt zurück
Die Transform ist öffentlich, Sie können die Arithmetik also selbst laufen lassen, bevor irgendein Chart existiert. Dieses Console-Snippet baut ein Jahr Monats-Ticks für die Serie oben und druckt die Labels, die das Chart benutzen wird
uses
SysUtils, lxChart;
var
Ticks: TXLSChartDateValues;
MinSerial, MaxSerial: Double;
I, Count: Integer;
begin
// 1900-Datumssystem (Dates1904 = False), Monatsende-verankerte Serie
MinSerial := TXLSChartDateAxisTransform.DateTimeToSerial(
EncodeDate(2026, 1, 31), False);
MaxSerial := TXLSChartDateAxisTransform.DateTimeToSerial(
EncodeDate(2026, 12, 31), False);
// Ein-Monat-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;
Es gibt genau eine Näherung in BuildTicks, und sie sitzt dort, wo eine Näherung harmlos ist. Wenn der angeforderte Schritt null ist, schätzt die Methode einen Schritt, 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 auf. Diese Schätzung entscheidet nur, wie viele Einheiten zwischen Ticks liegen. Positionen kommen weiterhin aus IncMonth und IncYear. Eine grobe Monatslänge kann die Achse etwas dichter oder dünner machen, aber jeder Tick landet weiterhin auf einem Datum, das eine der beiden Funktionen produziert hat
Workbook-Serials bleiben die Achsen-Koordinate
Eine HotXLS-Datumsachse behält Workbook-Serials als Koordinatenraum, weil Delphis TDateTime keinen Slot für Serial 60 hat. Im 1900-Datumssystem ist diese Serial der 29. Februar 1900, ein Tag, der nie existierte und den Excel zur Kompatibilität mit älteren Tabellenprogrammen hält. TXLSChartDateAxisTransform.TrySerialToDateTime behandelt ihn auf die einzige Weise, die ein echter Kalender kann: Serials unter 60 verschieben sich um einen Tag, und Serial 60 legt sich auf den 28. Februar. Zum Beschriften eines einzelnen Punkts ist das in Ordnung. Für Geometrie ist es falsch, denn Serials 59 und 60 werden dasselbe TDateTime, und der 28. Februar und der 1. März liegen am Ende eine Einheit auseinander, wo Excel sie zwei auseinander zeichnet
Drei Serials zeigen beide Seiten des Problems. FormatValue nimmt den Phantom-Slot gesondert und druckt exakt das, was Excel druckt, während ein Round-Trip über TDateTime ihn verliert
var
S: Integer;
D: TDateTime;
begin
for S := 59 to 61 do
begin
TXLSChartDateAxisTransform.TrySerialToDateTime(S, False, D);
Writeln(S, ' axis label: ',
TXLSChartDateAxisTransform.FormatValue(S, False),
' TDateTime: ', FormatDateTime('yyyy-mm-dd', D));
end;
// 59 axis label: 1900-02-28 TDateTime: 1900-02-28
// 60 axis label: 1900-02-29 TDateTime: 1900-02-28
// 61 axis label: 1900-03-01 TDateTime: 1900-03-01
end;
Kaum jemand chartet Daten von Anfang 1900, die praktische Lehre betrifft also Ihren eigenen Code. Wenn Sie Achsengrenzen oder Annotations-Positionen berechnen, rechnen Sie in Serials und konvertieren am Rand mit DateTimeToSerial, wie das erste Snippet. Mischt man eine serial-basierte Achse mit TDateTime-Arithmetik im Aufrufer, landet eine Annotation mit Datum vor dem 1. März 1900 einen Tag neben ihrem Punkt. Die zweite Epoche zählt öfter: In einem 1904-Workbook ist Serial 0 der 1. Januar 1904, und die Transform wendet den festen 1462-Tage-Offset in beide Richtungen an, wenn das Dates1904-Argument true ist. Beide Epochen und wie Zellen-Zahlenformate mit ihnen interagieren, behandelt 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. Sie verwirft NaN- und unendliche Werte, sortiert eine Kopie des Rests einmal und überspringt doppelte Daten. Ist irgendeine benachbarte Lücke kürzer als ein Kalendermonat, ist die Einheit Tage; sonst, ist irgendeine Lücke kürzer als ein Kalenderjahr, Monate; sonst Jahre. Der Monatstest ist IncMonth(Previous, 1) > Current, und diese Wahl zählt ausgerechnet für die Daten dieses Artikels. Ein 30-Tage-Schwellwert würde die Lücke vom 31. Januar zum 28. Februar "weniger als ein Monat" nennen und eine monatliche Serie auf Tage umschalten. Bei weniger als zwei verschiedenen gültigen Daten gibt es keine messbare Lücke, und die Methode liefert Tage
ResolveUnits verschmilzt die erkannte Einheit mit dem, was das Chart deklariert. Eine explizite Basiseinheit gewinnt. Eine fehlende Major- oder Minor-Einheit erbt die erkannte Einheit, wenn diese gröber als die Basis ist, und fällt sonst auf die Basis zurück. Die letzte Regel ist die, die Leute verpassen: Ist die effektive Major- oder Minor-Einheit feiner als die Basis, senkt sich die Basis entsprechend. Die Basiseinheit entscheidet, wie Punkte gruppiert werden, bevor irgendein Tick existiert, denn Normalize schnappt einen Wert auf seinen Tag, den ersten seines Monats oder den 1. Januar. Eine monatliche Basis unter einer täglichen Major-Einheit würde einen ganzen Monat Punkte in einen Slot falten, bevor der Tick-Code sie je sah
Wie viele automatische Major-Ticks erscheinen, hängt von mehr ab als dem Datenbereich. Die Plot-Länge, die gemessene Breite der Achsen-Schrift, das Label-Zahlenformat und die projizierte Breite rotierter Labels deckeln alle die Zahl, ein schmales Chart mit langen mmmm yyyy-Labels bekommt also weniger Ticks als ein breites mit mm/yy. Labels, Tick-Marks und Gitterlinien zeichnen alle aus einem Tick-Array, was HTML-, SVG- und PDF-Ausgaben im Einklang hält, und ein Minor-Tick, der auf einen Major-Tick fällt, wird einmal gezeichnet
Was speichert das dateAx-Element, und was bedeutet Auslassen?
In ChartML ist jede Kalender-Einstellung an einer Datumsachse optional, und wegzulassen sagt etwas anderes als den Default zu schreiben. Der CT_DateAx-Typ in ECMA-376 Part 1, §21.2 (DrawingML Charts) lässt baseTimeUnit, majorTimeUnit, minorTimeUnit und auto je erscheinen oder nicht. Ein ausgelassenes baseTimeUnit sagt Excel, beim Rendern selbst zu entscheiden; ein explizites days pinnt es. TXLSXChartAxis hält jeden Wert neben einem Presence-Flag: BaseTimeUnit mit BaseTimeUnitSet, MajorTimeUnit mit MajorTimeUnitSet, MinorTimeUnit mit MinorTimeUnitSet und AutoDateAxis mit AutoDateAxisSet. Ein Wert zuweisen setzt sein Flag, das Flag auf False setzen bringt das Element zur Auslassung zurück, und der XLSX-Writer emittiert ein Element nur, wenn sein Flag an ist. auto wird leicht missverstanden: Es steuert die automatische Kategorie-gegen-Datum-Erkennung und hat nichts mit automatischen Tick-Einheiten zu tun
uses
SysUtils, DateUtils, lxHandleX;
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));
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; // ein Wert > 0 markiert majorUnit als gesetzt
Axis.NumberFormat := 'mmm yyyy';
Axis.NumberFormatSourceLinked := False;
Axis.TextStyle.Rotation := -45; // Grad
// AutoDateAxis unangetastet gelassen: kein c:auto-Element wird geschrieben
Book.SaveAs('month-end-balance.xlsx');
finally
Book.Free;
end;
end;
Die Basiseinheit hier ist mit Absicht Tage. Monatsende-Daten fallen auf verschiedene Tageszahlen, und eine monatliche Basis würde jeden Punkt auf den ersten seines Monats schnappen, was den 31.-März-Saldo auf den 1. März verschiebt. Pinnt die Basis auf Tage, wann immer der exakte Tag Bedeutung trägt, und lasst die Major-Einheit den monatlichen Abstand machen
Labels und Orientierung folgen denselben Presence-Regeln. Das numFmt-Element der Achse defaultet sourceLinked auf true, TXLSXChartAxis hält also den Format-Code, den Source-Linked-Wert und NumberFormatSet auseinander, und ein leerer Code wird nie für ein fehlendes Element gehalten. ChartML speichert Text-Rotation in 1/60000 Grad mit positiven Werten im Uhrzeigersinn; TextStyle.Rotation nimmt schlichte Grad, und ein Label auf -45 lehnt sich in SVG- und PDF-Ausgabe gleich. Eine umgekehrte (maxMin)-Achse spiegelt Major- und Minor-Ticks, Gitterlinien, Datenpunkte, Trendlinien und Fehlerbalken gemeinsam. Das Lesen läuft andersherum: Ein woanders geschriebenes Workbook öffnen, und die Flags verraten, was sein Autor tatsächlich spezifiziert 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[1].Charts[0].CategoryAxis; // Sheets[] ist 1-basiert, Charts[] ist 0-basiert
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; // nochmal speichern, und die ausgelassenen Elemente bleiben ausgelassen
end;
end;
Kalender-Einheiten in ODS und älteren XLS-Dateien
Das Kalender-Modell reicht so weit, wie jedes Dateiformat es ausdrücken kann. ODF 1.2 beschreibt Achsen-Abstand mit chart:interval-major, einer schlichten Zahl ohne Begriff von Tagen, Monaten oder Jahren, eine als ODS gespeicherte Excel-Monatsachse kann ihre Einheit also nicht tragen. HotXLS lässt die Einheit aus der ODS-Datei weg, statt ein nicht standardmäßiges Attribut zu erfinden, das andere ODF-Anwendungen ohnehin ignorieren würden. Wenn der monatliche Abstand den Menschen wichtig ist, die die Datei öffnen, dann XLSX ausliefern
BIFF8-Workbooks halten Datumsachsen-Einstellungen im AxcExt-Record, neun feste 16-Bit-Wörter. Seine Automatisch-Flags maskieren Felder, ohne sie zu entfernen, ein als "automatisches Minimum" markierter Record trägt also weiterhin einen Minimalwert. HotXLS behält das maskierte Minimum, Maximum, Intervall, die Einheit und die Kreuzungswerte beim Lesen und schreibt sie unverändert zurück, hält unbekannte Einheiten-Codes intakt im XLS-Objektmodell und mappt nur die gültigen Datumseinheiten 0, 1 und 2 (Tage, Monate, Jahre), wenn ein Chart nach XLSX konvertiert wird
Eine Datumsachse reist selten allein. Wenn ein Excel-Workbook eine Datumsachsen-Linie mit einer Säulenserie auf einer Sekundärachse paart, erklärt unser Artikel zu bewahrtem ChartML und Kombi-Charts, wie HotXLS das originale Chart-XML abspielt, wenn nichts geändert hat, und typisierte Edits hineinmergt, wenn doch, und unser Überblick über Charts, Bilder und Drawings behandelt die oben benutzten Anker- und Serien-Aufrufe. Die Seite HotXLS Delphi Component listet den vollen Chart-Feature-Satz für Delphi und C++Builder, inklusive XLS- und XLSX-Chart-Lesen, -Schreiben und -Rendern ohne installiertes Excel