HotXLS rendert de datumassen van Excel-charts met kalenderrekenkunde, niet met vaste dagtellingen. De klasse TXLSChartDateAxisTransform in de unit lxChart houdt elke coördinaat vast als workbook-serial, berekent elke maand- of jaartick rechtstreeks vanuit het minimum van de as via IncMonth en IncYear, en klem maandeeinden op de echte kalender, zodat een serie die op 31 januari verankerd is ticks krijgt op 28 februari, 31 maart en 30 april in plaats van te verschuiven. Dezelfde transform kiest dag-, maand- of jaareenheden automatisch en wordt gedeeld door de HTML-, SVG- en gepagineerde PDF-chartrenderers
De bug die dit voorkomt is iedereen bekend die ooit een maandafsluiting heeft geplot. Een renderer die een maand als 30 dagen behandelt loopt aan het einde van het eerste jaar al vijf dagen vroeger. Een renderer die het slimmer aanpakt en kalendermaanden gebruikt, maar elke tick vanuit de vorige laat verderlopen, faalt stiller: 31 januari wordt 28 februari, de volgende stap landt op 28 maart, en elke tick daarna blijft aan de 28e vastgekleefd. De chart ziet er plausibel uit, de labels zijn fout, en niemand merkt het totdat een controller vraagt waarom het saldo van maart drie dagen vóór het kwartaaleinde staat geplot
Waarom schuiven maandticks als ze worden opgestapeld?
Maandticks schuiven omdat afkappen informatie weggooit. Zodra 31 januari naar 28 februari is afgekapt, is het feit dat de serie de 31ste wilde verdwenen, en elke stap die vanuit de afgekapte waarde wordt gezet erft dat verlies. TXLSChartDateAxisTransform.BuildTicks stapt nooit vanuit een eerdere tick. Tick i wordt altijd berekend als AddUnits(MinValue, UnitKind, Step * i, ...), dus min + eenheid × index gemeten vanaf het minimum van de as. AddUnits telt het aantal voor dagunits rechtstreeks bij de serial op en converteert voor maand- en jaareenheden de serial naar een echte datum, roept IncMonth of IncYear aan en rekent terug. De lus is ook aan alle kanten begrensd: de resultarray is gekapt op 4096 invoeren wat de aanvrager ook vraagt, Step * i wordt tegen MaxInt gecontroleerd vóórdat er wordt vermenigvuldigd, en de generatie stopt zodra een tick niet meer toeneemt of het maximum van de as passeert
uses
SysUtils, lxChart;
var
Ticks: TXLSChartDateValues;
MinSerial, MaxSerial: Double;
I, Count: Integer;
begin
// 1900-datumsysteem (Dates1904 = False), serie verankerd op maandeeinde
MinSerial := TXLSChartDateAxisTransform.DateTimeToSerial(
EncodeDate(2026, 1, 31), False);
MaxSerial := TXLSChartDateAxisTransform.DateTimeToSerial(
EncodeDate(2026, 12, 31), False);
// Stap van één maand, mik op 12 ticks, nooit meer dan 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;
Er zit één eerlijke benadering in BuildTicks. Als de gevraagde stap nul is, schat de methode een stapgrootte door een maand als 30 dagen en een jaar als 365 te tellen, en rondt vervolgens de ruwe stap af op een veelvoud van 1, 2 of 5 keer een macht van tien. Die schatting bepaalt alleen hoeveel eenheden er tussen ticks liggen. De tickposities zelf komen nog steeds uit IncMonth en IncYear, dus de benadering kan de tickdichtheid veranderen maar zet nooit een tick van de kalender af
Waarom workbook-serials aanhouden in plaats van te converteren naar TDateTime?
Een datumas in HotXLS houdt workbook-serials als coördinatenruimte aan, omdat de TDateTime van Delphi geen plek heeft voor serial 60, de fantoom-29-februari-1900 die het 1900-datumsysteem als compatibiliteitsquirk heeft geërfd. TrySerialToDateTime schuift serials onder 60 één dag op en vouwt serial 60 samen op 28 februari, wat prima is voor het labelen van één punt maar fataal voor geometrie: converteer eerst elk punt naar TDateTime en de twee dagen rond de fantoomdag komen één eenheid dichter bij elkaar te liggen dan Excel ze tekent, dus alles wat vóór maart 1900 is geplot schuift ten opzichte van alles erna. TXLSChartDateAxisTransform.FormatValue behandelt dezelfde plek als speciaal geval en labelt hem 1900-02-29, precies zoals Excel. In een 1904-werkboek is serial 0 op 1 januari 1904, en past de transform de vaste offset van 1462 dagen in beide richtingen toe; het volledige verhaal van beide tijdperken staat in ons artikel over Excel-datumserials, het 1904-systeem en getalnotaties
Hoe kiest HotXLS automatisch dagen, maanden of jaren?
TXLSChartDateAxisTransform.DetectUnit kiest de fijnste kalendereenheid die de gaten in de data echt nodig hebben. De methode gooit eerst NaN- en oneindige waarden weg, sorteert de rest één keer in O(n log n) en slaat dubbele datums over. Is een of ander aangrenzend gat korter dan één kalendermaand, dan is de eenheid dagen; anders, als een gat korter is dan één kalenderjaar, is de eenheid maanden; anders jaren. De test is IncMonth(Previous, 1) > Current, geen drempel van 30 dagen, dus een maandeeindserie die loopt van 31 januari, 28 februari, 31 maart wordt terecht als maanddata gedetecteerd. Met minder dan twee verschillende geldige datums is er geen gat te meten, en valt de methode terug op dagen
ResolveUnits voegt daarna de gedetecteerde eenheid samen met wat de chart verklaart. Een expliciete basiseenheid wordt gerespecteerd, en een ontbrekende major- of minor-unit erft de gedetecteerde eenheid wanneer die grover is dan de basis. De laatste stap is degene die mensen missen: is de effectieve major- of minor-unit fijner dan de basiseenheid, dan wordt de basis verlaagd om mee te komen. De basiseenheid is waarnaar datapunten worden genormaliseerd voordat er ticks bestaan, met Normalize dat een waarde vastzet op zijn dag, de eerste van zijn maand of 1 januari, dus een maandelijkse basis onder een dagelijkse major-unit zou een maand vol punten in één plek laten samenvallen voordat de tickcode ze ooit zag. De tickdichtheid is ook begrensd, door de plotlengte, de gemeten breedte van het aslettertype, het getalnotatieformaat van de labels en de projectie van gedraaide labels, en elke backend tekent labels, tickmarks en rasterlijnen vanuit één gedeelde, begrensde tickarray, waarbij een minortick die samenvalt met een majortick maar één keer wordt getekend
Wat slaat het dateAx-element op, en wat betekent weglaten?
In ChartML is elke kalenderinstelling op een datumas optioneel, en een instelling weglaten is niet hetzelfde als de standaardwaarde opschrijven. Het type CT_DateAx in ECMA-376 Part 1, §21.2 (DrawingML Charts) laat baseTimeUnit, majorTimeUnit, minorTimeUnit en auto elk verschijnen of niet, en een weggelaten baseTimeUnit zegt tegen Excel dat hij het zelf mag uitzoeken, wat anders is dan een expliciete days. TXLSXChartAxis bewaart elke waarde daarom naast een aanwezigheidsvlag: BaseTimeUnit met BaseTimeUnitSet, MajorTimeUnit met MajorTimeUnitSet, MinorTimeUnit met MinorTimeUnitSet en AutoDateAxis met AutoDateAxisSet. Een waarde toewijzen zet de vlag, de vlag wissen brengt het element terug naar weglaten, en de XLSX-writer schrijft een element alleen als zijn vlag is gezet. Let op: auto betekent automatische detectie van categorie versus datum, niet automatische tickunits
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; // schrijft c:dateAx
Axis.BaseTimeUnit := xlsxChartTimeDays; // zet ook BaseTimeUnitSet
Axis.MajorTimeUnit := xlsxChartTimeMonths;
Axis.MajorUnit := 1; // > 0 markeert majorUnit als gezet
Axis.NumberFormat := 'mmm yyyy';
Axis.NumberFormatSourceLinked := False;
Axis.TextStyle.Rotation := -45; // graden; opgeslagen als 1/60000
// AutoDateAxis onaangeroerd gelaten: er wordt geen c:auto-element geschreven
Book.SaveAs('month-end-balance.xlsx');
finally
Book.Free;
end;
end;
Labels en oriëntatie volgen dezelfde aanwezigheidsbewuste regels. De numFmt van de as zit na de titel en vóór de tickmarks, het attribuut sourceLinked staat standaard op true, en TXLSXChartAxis houdt de formatcode, de sourceLinked-waarde en NumberFormatSet apart, zodat een lege code nooit wordt aangezien voor een afwezig element. Textrotatie wordt opgeslagen in 1/60000 van een graad, waarbij positieve waarden met de klok mee betekenen; TextStyle.Rotation neemt gewone graden, de SVG-uitvoer gebruikt de hoek zoals hij is, en de gepagineerde backend, waar positieve hoeken tegen de klok in lopen, klapt het teken om op één gedeelde grens. Een omgekeerde maxMin-as spiegelt de major- en minorticks, rasterlijnen, datapunten, trendlijnen en foutbalken tegelijk, en nooit alleen de labels. Open u een werkboek dat elders is geschreven, dan vertellen de vlaggen u wat de auteur echt heeft gespecificeerd
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; // sla het opnieuw op en de weggelaten elementen blijven weg
end;
end;
Waar houdt het kalendermodel op?
Het kalendermodel houdt op bij formaten die kalendereenheden niet kunnen uitdrukken, en HotXLS doet alsof niet. ODF 1.2 biedt chart:interval-major, een kaal getal zonder notie van dagen, maanden of jaren, dus een maandelijkse Excel-as die als ODS wordt opgeslagen kan zijn eenheid niet meedragen en HotXLS verzint geen niet-standaard attribuut om er één te veinzen. Aan de verouderde BIFF8-kant bevat het AxcExt-record negen vaste 16-bit woorden, en zijn automatische vlaggen maskeren velden slechts in plaats van ze te verwijderen; HotXLS behoudt de gemaskeerde waarden voor minimum, maximum, interval, eenheid en snijpunt bij lezen en terugschrijven, houdt onbekende unitcodes intact in het Classic-model en mapt alleen de geldige dateenheden 0, 1 en 2 (dagen, maanden, jaren) naar XLSX
Datumassen zijn één laag van een chartmodel dat ook nog moet overleven in bestanden die het zelf niet maakte. Combineert een Excel-werkboek een datumas-lijn met een kolomserie op een secundaire as, dan laat ons artikel over bewaarde ChartML en combinatiecharts zien hoe HotXLS de oorspronkelijke chart-XML byte voor byte afspeelt zolang er niets veranderde en getypeerde bewerkingen samenvoegt zodra er wel iets veranderde, en ons overzicht van charts, afbeeldingen en drawings behandelt de anker- en serie-API's die hierboven werden gebruikt. Dit alles zit in de HotXLS Delphi Component voor Delphi en C++Builder, die XLS- en XLSX-charts leest, schrijft en rendert zonder Excel-automatisering