HotXLS renderer Excel-diagrammers datoakser med kalenderaritmetik, ikke faste dagstællinger. Klassen TXLSChartDateAxisTransform i lxChart-uniten holder hver koordinat som en workbook-serial, beregner hvert måned- eller årstick direkte fra akse-minimum gennem IncMonth og IncYear og clamper månedsslutninger til den rigtige kalender, så en serie forankret den 31. januar får ticks den 28. februar, 31. marts og 30. april i stedet for at glide. Samme transform vælger dag-, måned- eller årsenheder automatisk og deles af HTML-, SVG- og sideopdelte PDF-chart-renderere
Fejlen, dette forhindrer, er kendt af alle, der har plottet en månedsslut-afslutning. En renderer, der behandler en måned som 30 dage, driver fem dage for tidligt inden udgangen af det første år. En renderer, der er klogere og bruger kalendermåneder, men skrider frem fra det forrige tick, fejler mere lydløst: 31. januar bliver til 28. februar, næste skridt lander på 28. marts, og hvert tick derefter forbliver naglet til den 28. Diagrammet ser plausibelt ud, etiketterne er forkerte, og ingen lægger mærke til det, før en controller spørger, hvorfor marts-saldoen er plottet tre dage før kvartalsslut
Hvorfor driver månedsticks, når de akkumuleres?
Månedsticks driver, fordi clamping smider information væk. Når 31. januar først er clampet til 28. februar, er det væk, at serien ønskede den 31., og ethvert skridt taget fra den clampede værdi arver tabet. TXLSChartDateAxisTransform.BuildTicks skrider aldrig frem fra et tidligere tick. Tick i beregnes altid som AddUnits(MinValue, UnitKind, Step * i, ...), hvilket er min + enhed × indeks målt fra akse-minimum. AddUnits lægger tællingen direkte til serialen for dagenheder og konverterer for måned- og årsenheder serialen til en rigtig dato, kalder IncMonth eller IncYear og konverterer tilbage. Løkken er også begrænset i alle retninger: resultat-arrayet er cap'et til 4096 entries, uanset hvad kalderen beder om, Step * i tjekkes mod MaxInt, før det ganges, og genereringen stopper, så snart et tick holder op med at stige eller passerer akse-maksimum
uses
SysUtils, lxChart;
var
Ticks: TXLSChartDateValues;
MinSerial, MaxSerial: Double;
I, Count: Integer;
begin
// 1900-datosystem (Dates1904 = False), serie forankret på månedsslut
MinSerial := TXLSChartDateAxisTransform.DateTimeToSerial(
EncodeDate(2026, 1, 31), False);
MaxSerial := TXLSChartDateAxisTransform.DateTimeToSerial(
EncodeDate(2026, 12, 31), False);
// Ét måneds skridt, sigt efter 12 ticks, aldrig flere end 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;
Der er én ærlig approksimation i BuildTicks. Når det ønskede skridt er nul, estimerer metoden en skridtstørrelse ved at behandle en måned som 30 dage og et år som 365 og runder derefter det rå skridt til et 1-, 2- eller 5-multiplum af en titalspotens. Det estimat beslutter kun, hvor mange enheder der ligger mellem ticks. Tick-positionerne selv kommer stadig fra IncMonth og IncYear, så approksimationen kan ændre tick-tætheden, men flytter aldrig et tick væk fra kalenderen
Hvorfor beholde workbook-serials i stedet for at konvertere til TDateTime?
En datoakse i HotXLS beholder workbook-serials som sit koordinatrum, fordi TDateTime i Delphi ikke har nogen plads til serial 60, den spøgelsesagtige 29. februar 1900, som 1900-datosystemet arvede som et kompatibilitetskværk. TrySerialToDateTime flytter serials under 60 én dag og folder serial 60 sammen med 28. februar, hvilket er fint til at etikettere et enkelt punkt, men fatalt for geometrien: konvertér alle punkter til TDateTime først, og de to dage på hver side af spøgelsesdagen ender én enhed tættere på hinanden, end Excel tegner dem, så alt, hvad der er plottet før marts 1900, flytter sig relativt til alt efter det. TXLSChartDateAxisTransform.FormatValue special-caser samme plads og etiketterer den 1900-02-29, præcis som Excel gør. I en 1904-arbejdsbog er serial 0 den 1. januar 1904, og transformen anvender det faste 1462-dages offset i begge retninger; hele historien om begge epoker står i vores artikel om Excel-datoserials, 1904-systemet og talformater
Hvordan vælger HotXLS dage, måneder eller år automatisk?
TXLSChartDateAxisTransform.DetectUnit vælger den fineste kalenderenhed, som hullerne i dataene reelt kræver. Metoden kasserer NaN- og uendelige værdier først, sorterer resten én gang i O(n log n) og springer duplikerede datoer over. Er et hvilket som helst nabohul kortere end én kalendermåned, er enheden dage; er et hul ellers kortere end ét kalenderår, er enheden måneder; ellers er det år. Testen er IncMonth(Previous, 1) > Current, ikke en 30-dages tærskel, så en månedsslut-serie, der løber 31. januar, 28. februar, 31. marts, detekteres korrekt som månedlige data. Med færre end to forskellige gyldige datoer er der intet hul at måle, og metoden falder tilbage til dage
ResolveUnits fletter derefter den detekterede enhed med, hvad diagrammet erklærer. En eksplicit basisenhed respekteres, og en manglende major- eller minorenhed arver den detekterede enhed, når den er grovere end basen. Sidste skridt er det, folk overser: er den effektive major- eller minorenhed finere end basisenheden, sænkes basen til at matche. Basisenheden er det, datapunkter normaliseres til, før nogen ticks findes, med Normalize, der snapper en værdi til sin dag, den første i sin måned eller 1. januar, så en månedlig base under en daglig majorenhed ville kollapse en måneds punkter til én plads, før tick-koden nogensinde så dem. Tick-tætheden er også begrænset, af plotlængden, den målte bredde af akse-fonten, etiketternes talformat og projiceringen af roterede etiketter, og alle backends tegner etiketter, tick-mærker og gitterlinjer fra ét delt, begrænset tick-array, med et minortick, der falder sammen med et majortick, kun tegnet én gang
Hvad gemmer dateAx-elementet, og hvad betyder udeladelse?
I ChartML er enhver kalenderindstilling på en datoakse valgfri, og at udelade én er ikke det samme som at skrive dens standardværdi. Typen CT_DateAx i ECMA-376 Part 1, §21.2 (DrawingML Charts) lader baseTimeUnit, majorTimeUnit, minorTimeUnit og auto hver især optræde eller ej, og en udeladt baseTimeUnit beder Excel om selv at beslutte, hvilket er anderledes end en eksplicit days. TXLSXChartAxis gemmer derfor hver værdi ved siden af en tilstede-flag: BaseTimeUnit med BaseTimeUnitSet, MajorTimeUnit med MajorTimeUnitSet, MinorTimeUnit med MinorTimeUnitSet og AutoDateAxis med AutoDateAxisSet. Tildeler man en værdi, sættes dens flag, klarer man flaget, vender elementet tilbage til udeladelse, og XLSX-writeren emitterer kun et element, når dets flag er sat. Bemærk, at auto betyder automatisk kategori-versus-dato-detektion, ikke automatiske tick-enheder
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; // skriver c:dateAx
Axis.BaseTimeUnit := xlsxChartTimeDays; // sætter også BaseTimeUnitSet
Axis.MajorTimeUnit := xlsxChartTimeMonths;
Axis.MajorUnit := 1; // > 0 markerer majorUnit som sat
Axis.NumberFormat := 'mmm yyyy';
Axis.NumberFormatSourceLinked := False;
Axis.TextStyle.Rotation := -45; // grader; gemmes som 1/60000
// AutoDateAxis røres ikke: der skrives intet c:auto-element
Book.SaveAs('month-end-balance.xlsx');
finally
Book.Free;
end;
end;
Etiketter og orientering følger samme presence-aware regler. Aksens numFmt ligger efter titlen og før tick-mærkerne, dens sourceLinked-attribut er som standard true, og TXLSXChartAxis holder formatkoden, sourceLinked-værdien og NumberFormatSet adskilt, så en tom kode aldrig forveksles med et fraværende element. Tekstrotation gemmes i 1/60000 grad med positive værdier betydende med uret; TextStyle.Rotation tager rene grader, SVG-outputtet bruger vinklen, som den er, og den sideopdelte backend, hvis positive vinkler løber mod uret, vender fortegnet ved én delt grænseflade. En vendt maxMin-akse spejler major- og minorticks, gitterlinjer, datapunkter, trendlinjer og fejlbjælker sammen og aldrig kun etiketterne. Åbner man en arbejdsbog skrevet et andet sted fra, fortæller flagene én, hvad forfatteren faktisk specificerede
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; // gem den igen, og de udeladte elementer forbliver udeladt
end;
end;
Hvor stopper kalendermodellen?
Kalendermodellen stopper ved formater, der ikke kan udtrykke kalenderenheder, og HotXLS foregiver ikke andet. ODF 1.2 tilbyder chart:interval-major, som er et almindeligt tal uden forestilling om dage, måneder eller år, så en månedlig Excel-akse gemt som ODS ikke kan bære sin enhed, og HotXLS opfinder ikke en ikke-standard-attribut for at narre én frem. På den gamle BIFF8-side holder AxcExt-recorden ni faste 16-bit ord, og dens automatiske flags maskerer kun felter frem for at fjerne dem; HotXLS bevarer de maskerede minimums-, maksimum-, interval-, enheds- og krydsningsværdier ved læsning og tilbagekrivning, holder ukendte enhedskoder intakte i Classic-modellen og mapper kun de gyldige datoenheder 0, 1 og 2 (dage, måneder, år) til XLSX
Datoakser er ét lag af en diagrammodel, der også skal overleve filer, den ikke selv har lavet. Kombinerer en Excel-arbejdsbog en datoakse-linje med en kolonneserie på en sekundær akse, viser vores artikel om bevaret ChartML og kombinationsdiagrammer, hvordan HotXLS afspiller det originale diagram-XML byte for byte, når intet er ændret, og fletter typede redigeringer ind i det, når noget er, og vores oversigt over diagrammer, billeder og drawings dækker anker- og serie-API'erne brugt ovenfor. Det hele leveres i HotXLS Delphi Component til Delphi og C++Builder, som læser, skriver og renderer XLS- og XLSX-diagrammer uden Excel-automatisering