HotXLS vykresluje date axis Excel grafů kalendářní aritmetikou, ne pevnými počty dnů. Třída TXLSChartDateAxisTransform v unitu lxChart drží každou souřadnici jako sešitový serial, počítá každý měsíční nebo roční tick přímo z minima osy přes IncMonth a IncYear a upíná konce měsíců na reálný kalendář, takže série ukotvená na 31. ledna dostane ticky na 28. února, 31. března a 30. dubna místo toho, aby klouzala. Tentýž transform si den, měsíc nebo rok jako jednotku vybere automaticky a sdílejí ho renderery grafů do HTML, SVG i stránkovaného PDF
Bug, kterému tohle brání, zná každý, kdo někdy grafem kreslil month-end close. Renderer, který bere měsíc jako 30 dnů, sklouzne do konce prvního roku o pět dní dopředu. Renderer chytřejší, který používá kalendářní měsíce, ale odvozuje každý tick z předchozího, selhává potichuji: 31. ledna se stane 28. únor, další krok dopadne na 28. března a každý další tick už zůstane přilepený na 28. Graf vypadá věrohodně, popisky jsou špatně a nikdo si toho nevšimne, dokud kontrolor nepozeptá, proč je březní zůstatek vykreslený tři dny před koncem kvartálu
Proč měsíční ticky driftují, když se akumulují?
Měsíční ticky driftují, protože upnutí informaci vyhodí. Jakmile se 31. ledna upne na 28. února, fakt, že série chtěla 31., je pryč a každý krok odvozený z upnuté hodnoty ztrátu dědí. TXLSChartDateAxisTransform.BuildTicks nikdy neodstupuje z předchozího ticku. Tick i se vždy počítá jako AddUnits(MinValue, UnitKind, Step * i, ...), tedy min + jednotka × index měřeno od minima osy. AddUnits přičítá počet rovnou k serialu u denních jednotek a u měsíčních a ročních jednotek serial převede na reálné datum, zavolá IncMonth nebo IncYear a převede zpět. Smyčka je taky ohraničená ze všech stran: výsledné pole má strop 4096 položek, ať si volající přeje cokoli, Step * i se zkontroluje proti MaxInt před násobením a generace skončí, jakmile tick přestane růst nebo přeletí maximum osy
uses
SysUtils, lxChart;
var
Ticks: TXLSChartDateValues;
MinSerial, MaxSerial: Double;
I, Count: Integer;
begin
// Datový systém 1900 (Dates1904 = False), série ukotvená na konec měsíce
MinSerial := TXLSChartDateAxisTransform.DateTimeToSerial(
EncodeDate(2026, 1, 31), False);
MaxSerial := TXLSChartDateAxisTransform.DateTimeToSerial(
EncodeDate(2026, 12, 31), False);
// Krok jednoho měsíce, cíl 12 ticků, nikdy víc než 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;
V BuildTicks je jedna poctivá aproximace. Když je požadovaný krok nula, metoda odhadne velikost kroku s měsícem jako 30 dnů a rokem jako 365 a pak zaokrouhlí syrový krok na násobek 1, 2 nebo 5 mocniny deseti. Tenhle odhad rozhoduje jen o tom, kolik jednotek leží mezi ticky. Pozice ticků samotných dál přicházejí z IncMonth a IncYear, takže aproximace může změnit hustotu ticků, ale nikdy nesune tick z kalendáře
Proč držet sešitové serialy místo konverze na TDateTime?
Date axis v HotXLS drží jako souřadnicový prostor sešitové serialy, protože TDateTime v Delphi nemá slot pro serial 60, fiktivní 29. únor 1900, který datový systém 1900 zdědil jako kompatibilní kuriozitu. TrySerialToDateTime posune serialy pod 60 o jeden den a slepí serial 60 na 28. února, což na popisek jediného bodu stačí, ale pro geometrii je fatální: převeďte nejdřív všechny body na TDateTime a dva dny kolem fiktivního dne skončí o jednotku blíž u sebe, než je Excel kreslí, takže všechno vykreslené před březnem 1900 se posune vůči tomu po něm. TXLSChartDateAxisTransform.FormatValue má pro tentýž slot speciální případ a nalepí mu 1900-02-29 přesně jako Excel. V sešitu 1904 je serial 0 roven 1. lednu 1904 a transform aplikuje pevný offset 1462 dní oběma směry; celý příběh obou epoch rozebírá náš článek o Excel date serialech, systému 1904 a number formátech
Jak si HotXLS automaticky vybere dny, měsíce nebo roky?
TXLSChartDateAxisTransform.DetectUnit vybere nejjemnější kalendářní jednotku, kterou mezery v datech doopravdy potřebují. Metoda nejdřív zahodí NaN a nekonečné hodnoty, jednou setřídí zbytek v O(n log n) a přeskočí duplicitní data. Pokud je jakákoli sousední mezera kratší než jeden kalendářní měsíc, jednotkou jsou dny; jinak, pokud je jakákoli mezera kratší než jeden kalendářní rok, jednotkou jsou měsíce; jinak roky. Test je IncMonth(Previous, 1) > Current, ne práh 30 dnů, takže month-end série 31. ledna, 28. února, 31. března se správně detekuje jako měsíční data. S méně než dvěma různými platnými daty není co měřit a metoda spadne na dny
ResolveUnits pak sloučí detekovanou jednotku s tím, co deklaruje graf. Explicitní base jednotka se respektuje a chybějící major nebo minor jednotka dědí detekovanou, když je ta hrubší než base. Poslední krok je ten, který lidé přehlížejí: pokud je efektivní major nebo minor jednotka jemnější než base jednotka, base se sníží, aby seděla. Base jednotka je to, na co se normalizují datové body, než vůbec existují ticky, s Normalize přichytávajícím hodnotu na její den, první den jejího měsíce nebo 1. ledna, takže měsíční base pod denní major jednotkou by slepila měsíc bodů do jednoho slotu, dřív než je tickový kód vůbec uvidí. Hustota ticků má taky strop — řídí ji délka plotu, změřená šířka písma osy, number formát popisků a projekce otočených popisků — a každý backend kreslí popisky, tick značky a mřížku z jednoho sdíleného, ohraničeného pole ticků, přičemž minor tick splývající s major tickem se kreslí jen jednou
Co element dateAx ukládá a co znamená jeho vynechání?
V ChartML je každé kalendářní nastavení na date axis volitelné a vynechání není totéž jako zápis výchozí hodnoty. Typ CT_DateAx v ECMA-376 Part 1, §21.2 (DrawingML Charts) nechá baseTimeUnit, majorTimeUnit, minorTimeUnit i auto každý objevit se nebo ne, a vynechaný baseTimeUnit říká Excelu, ať se rozhodne sám, což je něco jiného než explicitní days. TXLSXChartAxis proto ukládá každou hodnotu vedle presence flagu: BaseTimeUnit s BaseTimeUnitSet, MajorTimeUnit s MajorTimeUnitSet, MinorTimeUnit s MinorTimeUnitSet a AutoDateAxis s AutoDateAxisSet. Přiřazení hodnoty nastaví její flag, smazání flagu vrátí element do stavu vynechání a XLSX writer emituje element jen tehdy, když je jeho flag nastavený. Pozor, auto znamená automatickou detekci category versus date, ne automatické jednotky ticků
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; // zapíše c:dateAx
Axis.BaseTimeUnit := xlsxChartTimeDays; // zároveň nastaví BaseTimeUnitSet
Axis.MajorTimeUnit := xlsxChartTimeMonths;
Axis.MajorUnit := 1; // > 0 označí majorUnit jako nastavený
Axis.NumberFormat := 'mmm yyyy';
Axis.NumberFormatSourceLinked := False;
Axis.TextStyle.Rotation := -45; // stupně; ukládá se jako 1/60000
// AutoDateAxis zůstává nedotčené: element c:auto se nezapisuje
Book.SaveAs('month-end-balance.xlsx');
finally
Book.Free;
end;
end;
Popisky a orientace následují stejná presence-aware pravidla. numFmt osy sedí za titulkem a před tick značkami, jeho atribut sourceLinked defaultně stojí na true a TXLSXChartAxis drží formátový kód, hodnotu source-linked a NumberFormatSet odděleně, takže prázdný kód se nikdy nezamění za chybějící element. Otočení textu se ukládá v 1/60000 stupně, přičemž kladné hodnoty znamenají po směru hodinových ručiček; TextStyle.Rotation bere obyčejné stupně, SVG výstup použije úhel tak, jak je, a stránkovaný backend, jehož kladné úhly běží proti směru hodinových ručiček, překlápí znaménko na jedné sdílené hranici. Obrácená osa maxMin zrcadlí major i minor ticky, mřížku, datové body, trend lines a error bary dohromady, a nikdy jen popisky. Když otevřete sešit napsaný jinde, flagy vám poví, co autor doopravdy zadal
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; // uložte znovu a vynechané elementy zůstanou vynechané
end;
end;
Kde model kalendáře končí?
Model kalendáře končí u formátů, které kalendářní jednotky vyjádřit neumí, a HotXLS to nepředstírá jinak. ODF 1.2 nabízí chart:interval-major, což je holé číslo bez pojmu dnů, měsíců nebo roků, takže měsíční osa Excelu uložená jako ODS nemůže svou jednotku nést a HotXLS si nevynalézá nestandardní atribut, aby jednotku předstíral. Na legacy straně BIFF8 drží záznam AxcExt devět pevných 16bitových slov a jeho automatické flagy pole jen maskují, ne odstraňují; HotXLS zachovává maskované minimum, maximum, interval, jednotku a průsečíky při čtení i zpětném zápisu, v Classic modelu nechává neznámé kódy jednotek nedotčené a do XLSX mapuje jen platné date jednotky 0, 1 a 2 (dny, měsíce, roky)
Date axis jsou jedna vrstva modelu grafu, který musí přežít i soubory, které sám nevytvořil. Když Excel sešit zkombinuje linku s date axis se sloupcovou sérií na sekundární ose, článek o zachovávaném ChartML a combination grafech ukazuje, jak HotXLS přehraje původní chart XML bajt po bajtu, když se nic nezměnilo, a smísí do něj typované editace, když se změnilo něco, a přehled grafů, obrázků a kreseb pokrývá API pro ukotvení a série použité výše. Všechno dohromady dodává HotXLS Delphi Component pro Delphi a C++Builder, která čte, zapisuje a vykresluje XLS a XLSX grafy bez Excel automation