HotXLS рендерира date осите на Excel графики с календарна аритметика, не с фиксиран брой дни. Класът TXLSChartDateAxisTransform в unit-а lxChart пази всяка координата като workbook serial, изчислява всеки месечен или годишен tick директно от минимума на оста чрез IncMonth и IncYear и захваща краищата на месеците към истинския календар, така че серия, закачена на 31 януари, получава ticks на 28 февруари, 31 март и 30 април вместо да плъзга. Същият transform избира автоматично единици ден, месец или година и е споделен от HTML, SVG и страничния PDF chart рендерер
Бъгът, който това предотвратява, е познат на всеки, който е чертал month-end приключване. Рендерер, който третира месеца като 30 дни, се отмества пет дни назад до края на първата година. По-умният рендерер, ползващ календарни месеци, но напредващ всеки tick от предишния, се проваля по-тихо: 31 януари става 28 февруари, следващата стъпка каца на 28 март и всеки tick след това остава закачен на 28-ото. Графиката изглежда достоверно, етикетите са грешни и никой не забелязва, докато финансов директор не попита защо мартенското салдо е начертано три дни преди края на тримесечието
Защо месечните ticks се отместват, когато се акумулират?
Месечните ticks се отместват, защото захващането изхвърля информация. Щом 31 януари е захапан на 28 февруари, фактът, че серията е искала 31-вия, е изгубен и всяка стъпка, взета от захапаната стойност, наследява загубата. TXLSChartDateAxisTransform.BuildTicks никога не стъпва от предишен tick. Tick i винаги се изчислява като AddUnits(MinValue, UnitKind, Step * i, ...), тоест min + unit × index, мерино от минимума на оста. AddUnits добавя бройката направо към serial-а за денни единици, а за месечни и годишни единици превръща serial-а в истинска дата, вика IncMonth или IncYear и превръща обратно. Цикълът е ограничен и от всяка страна: резултатният масив има таван 4096 записа, каквото и да поиска извикващият, Step * i се проверява срещу MaxInt преди умножението, а генерирането спира щом tick спре да расте или премине максимума на оста
uses
SysUtils, lxChart;
var
Ticks: TXLSChartDateValues;
MinSerial, MaxSerial: Double;
I, Count: Integer;
begin
// 1900 date система (Dates1904 = False), серия закачена на month-end
MinSerial := TXLSChartDateAxisTransform.DateTimeToSerial(
EncodeDate(2026, 1, 31), False);
MaxSerial := TXLSChartDateAxisTransform.DateTimeToSerial(
EncodeDate(2026, 12, 31), False);
// Стъпка един месец, цел 12 ticks, никога повече от 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;
В BuildTicks има една честна апроксимация. Когато исканата стъпка е нула, методът оценява размер на стъпката, третирайки месеца като 30 дни и годината като 365, после закръгля суровата стъпка до кратно на 1, 2 или 5 на степен на десетката. Тази оценка решава само колко единици се побират между ticks. Самите tick позиции продължават да идват от IncMonth и IncYear, така че апроксимацията може да смени плътността на ticks, но никога не измества tick извън календара
Защо да пазите workbook serials вместо да конвертирате към TDateTime?
Date оста в HotXLS пази workbook serials като свое координатно пространство, защото Delphi TDateTime няма слот за serial 60 — фантомните 29 февруари 1900, които 1900 date системата наследи като странност за съвместимост. TrySerialToDateTime измества serials под 60 с един ден и сгъва serial 60 върху 28 февруари, което е наред за етикетиране на една точка, но фатално за геометрията: конвертирайте първо всяка точка в TDateTime и двата дни от двете страни на фантомния ден се оказват с една единица по-близо един до друг, отколкото Excel ги чертае, така че всичко, начертано преди март 1900, се измества спрямо всичко след него. TXLSChartDateAxisTransform.FormatValue третира същия слот като специален случай и го етикетира 1900-02-29 точно както Excel. В 1904 работна книга serial 0 е 1 януари 1904, а transform-ът прилага фиксираното отместване от 1462 дни и в двете посоки; пълната история на двете епохи е в статията ни за Excel date serials, 1904 системата и number форматите
Как HotXLS избира автоматично дни, месеци или години?
TXLSChartDateAxisTransform.DetectUnit избира най-фината календарна единица, която празнините в данните реално изискват. Методът първо изхвърля NaN и безкрайните стойности, сортира останалите веднъж в O(n log n) и прескача дублирани дати. Ако някой съседен интервал е по-къс от един календарен месец, единицата е дни; иначе, ако някой интервал е по-къс от една календарна година, единицата е месеци; иначе е години. Тестът е IncMonth(Previous, 1) > Current, не праг от 30 дни, така че month-end серията 31 януари, 28 февруари, 31 март се разпознава коректно като месечни данни. При по-малко от две различни валидни дати няма интервал за измерване и методът пада обратно на дни
ResolveUnits после слива разпознатата единица с това, което графиката декларира. Изрично зададена base единица се уважава, а липсваща major или minor единица наследява разпознатата, когато тя е по-груба от base. Последната стъпка е тази, която хората прескачат: ако ефективната major или minor единица е по-фина от base единицата, base се снишава, за да се съобрази. Base единицата е това, към което точките от данните се нормализират, преди да съществува и един tick, като Normalize прищраква стойност към нейния ден, първото число на нейния месец или 1 януари, така че месечна base под дневна major единица би сринала месец точки в един слот, преди tick кодът изобщо да ги е видял. Плътността на ticks също е ограничена — от дължината на чертежа, измерената ширина на шрифта на оста, number формата на етикетите и проекцията на завъртяните етикети — и всеки backend чертае етикети, tick марки и мрежови линии от един споделен, ограничен tick масив, като minor tick, съвпадащ с major tick, се чертае само веднъж
Какво съхранява елементът dateAx и какво значи пропускането?
В ChartML всяка календарна настройка на date ос е опционална и пропускането ѝ не е същото като записването на нейната стойност по подразбиране. Типът CT_DateAx в ECMA-376 Part 1, §21.2 (DrawingML Charts) позволява baseTimeUnit, majorTimeUnit, minorTimeUnit и auto всяко да се появи или не, а пропуснат baseTimeUnit казва на Excel да реши сам, което е различно от изричен days. TXLSXChartAxis затова пази всяка стойност до presence флаг: BaseTimeUnit с BaseTimeUnitSet, MajorTimeUnit с MajorTimeUnitSet, MinorTimeUnit с MinorTimeUnitSet и AutoDateAxis с AutoDateAxisSet. Присвояването на стойност вдига флага ѝ, изчистването на флага връща елемента към пропускане, а XLSX writer-ът излъчва елемент само когато флагът му е вдигнат. Обърнете внимание, че auto значи автоматично разпознаване category срещу date, не автоматични 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; // записва c:dateAx
Axis.BaseTimeUnit := xlsxChartTimeDays; // също вдига BaseTimeUnitSet
Axis.MajorTimeUnit := xlsxChartTimeMonths;
Axis.MajorUnit := 1; // > 0 маркира majorUnit като зададен
Axis.NumberFormat := 'mmm yyyy';
Axis.NumberFormatSourceLinked := False;
Axis.TextStyle.Rotation := -45; // градуси; съхранява се като 1/60000
// AutoDateAxis недокоснат: не се записва c:auto елемент
Book.SaveAs('month-end-balance.xlsx');
finally
Book.Free;
end;
end;
Етикетите и ориентацията следват същите presence-aware правила. numFmt на оста седи след заглавието и преди tick марките, атрибутът му sourceLinked по подразбиране е true, а TXLSXChartAxis пази форматния код, source-linked стойността и NumberFormatSet поотделно, така че празен код никога не се бърка за отсъстващ елемент. Завъртането на текста се съхранява в 1/60000 от градус с положителните стойности значещи по посоката на часовниковата стрелка; TextStyle.Rotation приема обикновени градуси, SVG изходът ползва ъгъла такъв, какъвто е, а страничният backend, чийто положителни ъгли вървят обратно на часовниковата стрелка, обръща знака на една споделена граница. Обърната maxMin ос огледално отразява major и minor ticks, мрежовите линии, точките данни, trendline-ите и error bar-овете заедно, и никога само етикетите. Когато отворите работна книга, писана другаде, флаговете ви казват какво авторът действително е задал
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; // запишете пак и пропуснатите елементи остават пропуснати
end;
end;
Къде спира календарният модел?
Календарният модел спира при форматите, които не могат да изразят календарни единици, и HotXLS не се преструва другояче. ODF 1.2 предлага chart:interval-major, което е обикновено число без представа за дни, месеци или години, така че месечна Excel ос, записана като ODS, не може да носи единицата си, а HotXLS не измисля нестандартен атрибут, за да я имитира. От страната на legacy BIFF8 записът AxcExt държи девет фиксирани 16-битови думи, а автоматичните му флагове само маскират полета, вместо да ги махнат; HotXLS запазва маскираните минимум, максимум, интервал, единица и crossing стойности при четене и записване назад, пази непознатите unit кодове непокътнати в Classic модела и картира само валидните date единици 0, 1 и 2 (дни, месеци, години) към XLSX
Date осите са един слой от chart модел, който трябва да оцелее и срещу файлове, които не е създал сам. Когато Excel работна книга съчетава date-axis линия с колонна серия върху второстепенна ос, статията ни за запазен ChartML и комбинираните графики показва как HotXLS възпроизвежда оригиналния chart XML байт по байт, когато нищо не се е променило, и слива типизирани редакции в него, когато нещо се е променило, а прегледът ни на графиките, изображенията и drawings покрива anchoring и series API-ите, ползвани по-горе. Всичко това се доставя в HotXLS Delphi Component за Delphi и C++Builder, който чете, записва и рендерира XLS и XLSX графики без Excel automation