HotXLS малює осі дат діаграм Excel календарною арифметикою, а не фіксованою кількістю днів. Клас TXLSChartDateAxisTransform в модулі lxChart тримає кожну координату як серійне число книги, обчислює кожен поділок місяця чи року просто з мінімуму осі через IncMonth і IncYear і притискає кінці місяців до справжнього календаря, тож серія з якорем на 31 січня отримує поділки 28 лютого, 31 березня і 30 квітня замість того, щоб сповзати. Той самий transform автоматично обирає одиниці днів, місяців чи років, і ним діляться рендерери діаграм HTML, SVG і посторінкового PDF
Баг, який це попереджає, знайомий кожному, хто хоч раз креслив закриття місяця. Рендерер, який вважає місяць рівно 30 днями, до кінця першого року сповзає на п'ять днів уперед. Рендерер розумніший, що користується календарними місяцями, але рухає кожен поділок від попереднього, фейлить тихіше: 31 січня стає 28 лютим, наступний крок приземляється на 28 березня, і всі поділки далі так і сидять на 28-му. Діаграма виглядає правдоподібно, підписи неправильні, і ніхто нічого не помічає, поки контролер не спитає, чому березневий баланс намальовано за три дні до кінця кварталу
Чому поділки місяців сповзають, коли їх накопичують?
Поділки місяців сповзають, бо притискання викидає інформацію. Щойно 31 січня притиснуто до 28 лютого, факт, що серія хотіла 31-ше, зник, і будь-який крок від притиснутого значення успадковує цю втрату. TXLSChartDateAxisTransform.BuildTicks ніколи не крокує від попереднього поділку. Поділок i завжди обчислюється як AddUnits(MinValue, UnitKind, Step * i, ...) — тобто min + unit × index, відміряний від мінімуму осі. AddUnits додає кількість просто до серійного числа для одиниць днів, а для одиниць місяців і років перетворює серійне число на справжню дату, викликає IncMonth чи IncYear і перетворює назад. Цикл також обмежений з усіх боків: масив результатів закритий на 4096 елементів, скільки б викликач ні просив, Step * i звіряється з MaxInt перед множенням, а генерація зупиняється щойно поділок перестає зростати чи проходить максимум осі
uses
SysUtils, lxChart;
var
Ticks: TXLSChartDateValues;
MinSerial, MaxSerial: Double;
I, Count: Integer;
begin
// Система дат 1900 (Dates1904 = False), серія з якорем на кінці місяця
MinSerial := TXLSChartDateAxisTransform.DateTimeToSerial(
EncodeDate(2026, 1, 31), False);
MaxSerial := TXLSChartDateAxisTransform.DateTimeToSerial(
EncodeDate(2026, 12, 31), False);
// Крок один місяць, ціль — 12 поділок, ніколи не більше ніж 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 ступеня десятки. Ця оцінка вирішує лише, скільки одиниць лежить між поділками. Самі позиції поділок усе одно походять із IncMonth і IncYear, тож апроксимація може змінити щільність поділок, але ніколи не зсуває поділок із календаря
Чому тримати серійні числа книги, а не конвертувати в TDateTime?
Вісь дат у HotXLS тримає серійні числа книги як свій координатний простір, бо в Delphi TDateTime немає слота для серійного 60 — фантомного 29 лютого 1900, яке система дат 1900 успадкувала як жарт сумісності. TrySerialToDateTime зсуває серійні числа нижчі за 60 на один день і згортає серійне 60 на 28 лютого — для підпису однієї точки це ок, а для геометрії фатально: сконвертуйте всі точки в TDateTime спершу, і два дні по обидва боки фантомного дня опиняються на одиницю ближче один до одного, ніж їх малює Excel, тож усе, що намальовано до березня 1900, зсувається відносно всього, що після. TXLSChartDateAxisTransform.FormatValue спеціально обробляє той самий слот і підписує його 1900-02-29, рівно як Excel. У книзі 1904 серійне 0 — це 1 січня 1904, і transform застосовує фіксований зсув 1462 днів в обидва боки; повна історія обох епох — у нашій статті про серійні числа дат Excel, систему 1904 і числові формати
Як HotXLS автоматично обирає дні, місяці чи роки?
TXLSChartDateAxisTransform.DetectUnit обирає найтоншу календарну одиницю, яка справді потрібна проміжкам у даних. Метод спершу відкидає NaN і нескінченні значення, один раз сортує решту в O(n log n) і пропускає повторювані дати. Якщо хоч один сусідній проміжок коротший за один календарний місяць, одиниця — дні; інакше, якщо хоч один проміжок коротший за один календарний рік, одиниця — місяці; інакше це роки. Перевірка — IncMonth(Previous, 1) > Current, а не поріг у 30 днів, тож серія кінців місяців 31 січня, 28 лютого, 31 березня коректно розпізнається як місячні дані. Коли окремих валідних дат менше ніж дві, проміжка міряти немає, і метод падає назад на дні
ResolveUnits потім зливає розпізнану одиницю з тим, що оголошує діаграма. Явна базова одиниця шанується, а відсутня основна чи проміжна одиниця успадковує розпізнану, коли та грубіша за базову. Останній крок — той, який люди пропускають: якщо ефективна основна чи проміжна одиниця тонша за базову, базова знижується, щоб зійтися. Базова одиниця — це те, до чого нормалізуються точки даних, поки жодних поділок ще не існує: Normalize притискає значення до його дня, першого числа його місяця чи 1 січня, тож місячна база під денною основною одиницею згорнула б місяць точок в один слот, ще до того, як код поділок їх побачив. Щільність поділок також обмежена — довжиною області побудови, виміряною шириною шрифту осі, числовим форматом підписів і проєкцією повернутих підписів, — і кожен бекенд малює підписи, риски поділок і сітку з одного спільного обмеженого масиву поділок, причому проміжна поділка, що збігається з основною, малюється лише раз
Що зберігає елемент dateAx і що означає відсутність елементів?
У ChartML кожне календарне налаштування осі дат опціональне, і відсутність елемента — не те саме, що записане типове значення. Тип CT_DateAx у ECMA-376 Part 1, §21.2 (DrawingML Charts) дозволяє baseTimeUnit, majorTimeUnit, minorTimeUnit і auto кожному бути чи не бути, і відсутній baseTimeUnit каже Excel вирішити самому, а це інше, ніж явне days. Тому TXLSXChartAxis зберігає кожне значення поруч із прапорцем присутності: BaseTimeUnit з BaseTimeUnitSet, MajorTimeUnit з MajorTimeUnitSet, MinorTimeUnit з MinorTimeUnitSet і AutoDateAxis з AutoDateAxisSet. Присвоєння значення ставить прапорець, скидання прапорця повертає елемент до відсутності, а письменник XLSX виводить елемент лише коли прапорець стоїть. Зауважте: auto означає автоматичне розпізнавання категорія-проти-дата, а не автоматичні одиниці поділок
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;
Підписи й орієнтація йдуть за тими самими правилами з урахуванням присутності. numFmt осі стоїть після заголовка й перед рисками поділок, його атрибут sourceLinked типово true, а TXLSXChartAxis тримає код формату, значення source-linked і NumberFormatSet порізно, тож порожній код ніколи не сплутають із відсутнім елементом. Повернення тексту зберігається в 1/60000 градуса, позитивні значення означають за годинниковою стрілкою; TextStyle.Rotation бере звичайні градуси, вихід SVG використовує кут як є, а посторінковий бекенд, у якого позитивні кути йдуть проти годинникової стрілки, перевертає знак на одному спільному кордоні. Перевернута вісь maxMin дзеркалить основні й проміжні поділки, сітку, точки даних, лінії тренду і планки похибок разом — і ніколи не самі лише підписи. Коли ви відкриваєте книгу, написану десь іще, прапорці кажуть, що автор справді вказав
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 зберігає замасковані мінімум, максимум, інтервал, одиницю й точку перетину при читанні та записі назад, тримає невідомі коди одиниць недоторканими в моделі Classic і відображає на XLSX лише валідні одиниці дат 0, 1 і 2 (дні, місяці, роки)
Осі дат — це один шар моделі діаграм, яка мусить ще й переживати файли, що їх створила не вона. Коли книга Excel поєднує лінію з віссю дат і колонкову серію на вторинній осі, наша стаття про збережений ChartML і комбіновані діаграми показує, як HotXLS прокручує початковий XML діаграми байт у байт, коли нічого не змінилося, і вливає типізовані правки в нього, коли щось змінилося, а наш огляд діаграм, зображень і малюнків покриває API прив'язки та серій, використані вище. Усе це їде в HotXLS Delphi Component для Delphi та C++Builder, який читає, пише і рендерить діаграми XLS і XLSX без Excel automation