HotXLS renderuje osie dat wykresów Excela arytmetyką kalendarzową, a nie stałą liczbą dni. Klasa TXLSChartDateAxisTransform w jednostce lxChart trzyma każdą współrzędną jako numer seryjny skoroszytu, wylicza każdy znacznik miesiąca albo roku wprost od minimum osi przez IncMonth i IncYear oraz dociąga końce miesięcy do prawdziwego kalendarza, więc seria zakotwiczona 31 stycznia dostaje znaczniki 28 lutego, 31 marca i 30 kwietnia zamiast się zjeżdżać. Ten sam transform sam wybiera jednostki dni, miesięcy albo lat i jest współdzielony przez renderery wykresów HTML, SVG i stronicowanego PDF
Błąd, któremu to zapobiega, zna każdy, kto kiedyś wykreślał zamknięcie miesiąca. Renderer traktujący miesiąc jako 30 dni zjeżdża pięć dni za wcześnie już pod koniec pierwszego roku. Renderer sprytniejszy, używający miesięcy kalendarzowych, ale wyliczający każdy kolejny znacznik z poprzedniego, zawodzi ciszej: 31 stycznia zamienia się w 28 lutego, następny krok ląduje na 28 marca i każdy znacznik po nim zostaje przyklejony do 28. Wykres wygląda wiarygodnie, etykiety są złe, a nikt nie zauważa, dopóki kontroler nie zapyta, dlaczego marcowe saldo jest narysowane trzy dni przed końcem kwartału
Dlaczego znaczniki miesięcy zjeżdżają, gdy się je akumuluje?
Znaczniki miesięcy zjeżdżają, bo dociąganie wyrzuca informację do kosza. Skoro 31 stycznia zostało dociągnięte do 28 lutego, to fakt, że seria chciała 31., przepada, a każdy krok z dociągniętej wartości dziedziczy tę stratę. TXLSChartDateAxisTransform.BuildTicks nigdy nie kroczy od poprzedniego znacznika. Znacznik i jest zawsze liczony jako AddUnits(MinValue, UnitKind, Step * i, ...), czyli min + jednostka × indeks mierzone od minimum osi. AddUnits dla jednostek dniowych dodaje liczbę wprost do numeru seryjnego, a dla jednostek miesięcznych i rocznych konwertuje numer na prawdziwą datę, woła IncMonth albo IncYear i konwertuje z powrotem. Pętla jest też ograniczona z każdej strony: tablica wynikowa ma kap 4096 wpisów, czego by wywołujący nie zażądał, Step * i jest sprawdzane względem MaxInt przed pomnożeniem, a generacja kończy się, gdy tylko znacznik przestaje rosnąć albo przekracza maksimum osi
uses
SysUtils, lxChart;
var
Ticks: TXLSChartDateValues;
MinSerial, MaxSerial: Double;
I, Count: Integer;
begin
// system dat 1900 (Dates1904 = False), seria zakotwiczona na końcu miesiąca
MinSerial := TXLSChartDateAxisTransform.DateTimeToSerial(
EncodeDate(2026, 1, 31), False);
MaxSerial := TXLSChartDateAxisTransform.DateTimeToSerial(
EncodeDate(2026, 12, 31), False);
// krok jednego miesiąca, cel: 12 znaczników, nigdy więcej niż 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;
W BuildTicks kryje się jedno uczciwe przybliżenie. Gdy żądany krok to zero, metoda szacuje rozmiar kroku, traktując miesiąc jako 30 dni i rok jako 365, po czym zaokrągla surowy krok do wielokrotności 1, 2 albo 5 potęgi dziesiątki. Ten szacunek decyduje tylko o tym, ile jednostek mieści się między znacznikami. Pozycje znaczników i tak pochodzą z IncMonth i IncYear, więc przybliżenie może zmienić gęstość znaczników, ale nigdy nie zsuwa znacznika z kalendarza
Dlaczego trzymać numery seryjne skoroszytu zamiast konwertować na TDateTime?
Oś daty w HotXLS trzyma numery seryjne skoroszytu jako swoją przestrzeń współrzędnych, bo TDateTime w Delphi nie ma slotu na numer 60 — widmo 29 lutego 1900, które system dat 1900 odziedziczył jako kwiatek dla zgodności wstecz. TrySerialToDateTime przesuwa numery poniżej 60 o jeden dzień i skleja numer 60 z 28 lutego, co wystarcza do etykietowania pojedynczego punktu, ale jest zabójcze dla geometrii: skonwertuj najpierw każdy punkt na TDateTime, a dwa dni po obu stronach widmowego dnia znajdą się o jedną jednostkę bliżej siebie, niż rysuje je Excel, więc wszystko narysowane przed marcem 1900 przesunie się względem wszystkiego, co jest po nim. TXLSChartDateAxisTransform.FormatValue ma specjalny przypadek dla tego samego slotu i etykietuje go 1900-02-29 dokładnie tak jak Excel. W skoroszycie 1904 numer 0 to 1 stycznia 1904, a transform stosuje stały offset 1462 dni w obie strony; pełną historię obu epok opisuje nasz artykuł o numerach seryjnych dat Excela, systemie 1904 i formatach liczb
Jak HotXLS sam wybiera dni, miesiące albo lata?
TXLSChartDateAxisTransform.DetectUnit wybiera najdrobniejszą jednostkę kalendarzową, jakiej przerwy w danych realnie potrzebują. Metoda najpierw wyrzuca wartości NaN i nieskończone, raz sortuje resztę w O(n log n) i pomija zduplikowane daty. Jeśli któraś sąsiednia przerwa jest krótsza niż jeden miesiąc kalendarzowy, jednostką są dni; w przeciwnym razie, jeśli któraś przerwa jest krótsza niż rok kalendarzowy — miesiące; w przeciwnym razie lata. Testem jest IncMonth(Previous, 1) > Current, a nie próg 30 dni, więc seria końców miesiąca biegnąca 31 stycznia, 28 lutego, 31 marca zostanie poprawnie rozpoznana jako dane miesięczne. Przy mniej niż dwóch różnych poprawnych datach nie ma przerwy do zmierzenia i metoda wraca do dni
ResolveUnits scala potem wykrytą jednostkę z tym, co deklaruje wykres. Jawna jednostka bazowa jest respektowana, a brakująca jednostka główna albo pomocnicza dziedziczy wykrytą, gdy ta ma grubsze ziarno niż baza. Ostatni krok to ten, który ludzie pomijają: jeśli efektywna jednostka główna albo pomocnicza jest drobniejsza niż jednostka bazowa, baza zostaje obniżona, by się dopasować. Jednostka bazowa to to, do czego punkty danych są normalizowane, zanim powstaną jakiekolwiek znaczniki — Normalize przyciąga wartość do jej dnia, pierwszego dnia jej miesiąca albo 1 stycznia, więc miesięczna baza pod dzienną jednostką główną zwinęłaby miesiąc punktów w jedno gniazdo, zanim kod znaczników w ogóle by je zobaczył. Gęstość znaczników też jest ograniczona — przez długość wykresu, zmierzoną szerokość czcionki osi, format liczb etykiet i projekcję obróconych etykiet — a każdy backend rysuje etykiety, kreski znaczników i linie siatki z jednej wspólnej, ograniczonej tablicy znaczników, przy czym znacznik pomocniczy pokrywający się z głównym jest rysowany tylko raz
Co przechowuje element dateAx i co znaczy jego pominięcie?
W ChartML każde ustawienie kalendarzowe osi dat jest opcjonalne, a pominięcie czegoś nie równa się zapisaniu jego wartości domyślnej. Typ CT_DateAx w ECMA-376 Part 1, §21.2 (DrawingML Charts) pozwala, by baseTimeUnit, majorTimeUnit, minorTimeUnit i auto każdy wystąpił albo nie, a pominięty baseTimeUnit każe Excelowi zdecydować samemu, co jest czymś innym niż jawne days. TXLSXChartAxis przechowuje więc każdą wartość obok flagi obecności: BaseTimeUnit z BaseTimeUnitSet, MajorTimeUnit z MajorTimeUnitSet, MinorTimeUnit z MinorTimeUnitSet i AutoDateAxis z AutoDateAxisSet. Przypisanie wartości ustawia jej flagę, skasowanie flagi przywraca element do stanu pominięcia, a zapisujący XLSX emituje element tylko wtedy, gdy jego flaga jest ustawiona. Zauważ, że auto znaczy automatyczne rozstrzyganie kategoria kontra data, a nie automatyczne jednostki znaczników
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; // pisze c:dateAx
Axis.BaseTimeUnit := xlsxChartTimeDays; // przy okazji ustawia BaseTimeUnitSet
Axis.MajorTimeUnit := xlsxChartTimeMonths;
Axis.MajorUnit := 1; // > 0 oznacza majorUnit jako ustawiony
Axis.NumberFormat := 'mmm yyyy';
Axis.NumberFormatSourceLinked := False;
Axis.TextStyle.Rotation := -45; // stopnie; zapisywane jako 1/60000
// AutoDateAxis nietknięte: element c:auto nie jest zapisywany
Book.SaveAs('month-end-balance.xlsx');
finally
Book.Free;
end;
end;
Etykiety i orientacja podlegają tym samym regułom świadomym obecności. numFmt osi siedzi za tytułem, a przed kreskami znaczników, jego atrybut sourceLinked ma domyślnie true, a TXLSXChartAxis trzyma kod formatu, wartość source-linked i NumberFormatSet osobno, więc pustego kodu nigdy nie pomylisz z nieobecnym elementem. Obrót tekstu jest zapisywany w 1/60000 stopnia, przy czym wartości dodatnie znaczą obrót zgodnie z ruchem wskazówek zegara; TextStyle.Rotation przyjmuje zwykłe stopnie, wyjście SVG używa kąta takim, jaki jest, a backend stronicowany, którego dodatnie kąty biegną przeciwnie do wskazówek zegara, odwraca znak na jednej wspólnej granicy. Odwrócona oś maxMin odbija lustrzanie znaczniki główne i pomocnicze, linie siatki, punkty danych, linie trendu i paski błędów razem — i nigdy samych etykiet. Gdy otwierasz skoroszyt napisany gdzie indziej, flagi mówią ci, co autor faktycznie określił
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; // zapisz jeszcze raz, a pominięte elementy pozostaną pominięte
end;
end;
Gdzie kończy się model kalendarzowy?
Model kalendarzowy kończy się na formatach, które nie umieją wyrazić jednostek kalendarzowych, i HotXLS nie udaje, że jest inaczej. ODF 1.2 oferuje chart:interval-major, czyli zwykłą liczbę bez pojęcia dni, miesięcy czy lat, więc miesięczna oś Excela zapisana jako ODS nie może nieść swojej jednostki, a HotXLS nie wymyśla niestandardowego atrybutu, żeby ją udawać. Po stronie legacy BIFF8 rekord AxcExt trzyma dziewięć stałych słów 16-bitowych, a jego flagi automatic tylko maskują pola, zamiast je usuwać; HotXLS zachowuje zamaskowane wartości minimum, maksimum, interwału, jednostki i przecięcia przy odczycie i zapisie zwrotnym, trzyma nieznane kody jednostek nietknięte w modelu Classic i mapuje na XLSX wyłącznie poprawne jednostki dat 0, 1 i 2 (dni, miesiące, lata)
Osie dat to jedna warstwa modelu wykresów, który musi też przeżywać pliki, których sam nie stworzył. Gdy skoroszyt Excela łączy linię na osi dat z serią kolumnową na osi pomocniczej, nasz artykuł o zachowanym ChartML i wykresach kombinowanych pokazuje, jak HotXLS odtwarza oryginalny XML wykresu bajt w bajt, gdy nic się nie zmieniło, i wciska w niego typowane edycje, gdy coś się zmieniło, a nasze omówienie wykresów, obrazów i rysunków obejmuje API zakotwiczania i serii użyte wyżej. Całość jest w HotXLS Delphi Component dla Delphi i C++Buildera, który czyta, zapisuje i renderuje wykresy XLS i XLSX bez automatyzacji Excela