Wykres sald na koniec miesiąca startujący 31 stycznia 2026 powinien wsadzić znacznik kwietnia na 30 kwietnia. Dociągaj każdy znacznik od poprzedniego miesiącami kalendarzowymi, a etykiety wyjdą jako 2026-01-31, 2026-02-28, 2026-03-28, 2026-04-28, z każdym dalszym znacznikiem przyklejonym do 28. Dociągaj płaskimi 30 dniami zamiast tego, a znacznik lutego wyląduje na 2 marca, podczas gdy grudniowy wyląduje na 27 grudnia. Reguła, która przeżywa obie pułapki, da się łatwo powiedzieć: licz każdy znacznik od minimum osi i zaciśkaj końce miesięcy do prawdziwego kalendarza
Na wykresie zamknięcia kwartału pierwsza pomyłka jest niebezpieczna, bo obraz nadal wygląda dobrze. Linia rośnie tam, gdzie powinna, etykiety są równo rozłożone, a marcowe saldo siedzi trzy dni przed końcem kwartału. Kontroler porównujący wykres z księgą to człowiek, który to znajdzie, a do tamtej pory wykres jest zwykle już wklejony do pakietu dla zarządu
Dlaczego znaczniki miesięcy dryfują, gdy są akumulowane?
Znaczniki miesięcy dryfują, bo zaciśnięcie wyrzuca informację. Gdy 31 stycznia zostało zaciśnięte do 28 lutego, fakt, że seria chciała 31, znika, a każdy krok z zaciśniętej wartości dziedziczy stratę. W HotXLS TXLSChartDateAxisTransform.BuildTicks w jednostce lxChart nigdy nie robi kroku od poprzedniego znacznika: znacznik i jest zawsze AddUnits(MinValue, UnitKind, Step * i, ...), więc marzec to dwa miesiące po 31 stycznia i wychodzi jako 31 marca. AddUnits dodaje liczbę wprost do numeru seryjnego dla jednostek dni; dla jednostek miesięcy i lat zamienia numer seryjny na prawdziwą datę, woła IncMonth albo IncYear i zamienia z powrotem
Pętla jest ograniczona z każdej strony. Tablica wynikowa nigdy nie przekroczy 4096 wpisów, cokolwiek wołający poda jako maksimum, Step * i jest sprawdzane wobec MaxInt przed wykonaniem mnożenia, a generacja staje, jak tylko znacznik przestanie rosnąć albo przeskoczy maksimum osi. Wrogi skoroszyt z rozpiętością paru milionów lat produkuje krótką tablicę i wraca
Transformacja jest publiczna, więc możesz przegonić tę arytmetykę samodzielnie, zanim istnieje jakikolwiek wykres. Snippet konsolowy niżej buduje rok znaczników miesięcznych dla serii z wyżej i wypisuje etykiety, których użyje wykres
uses
SysUtils, lxChart;
var
Ticks: TXLSChartDateValues;
MinSerial, MaxSerial: Double;
I, Count: Integer;
begin
// System dat 1900 (Dates1904 = False), seria zakotwiczona na koniecach miesięcy
MinSerial := TXLSChartDateAxisTransform.DateTimeToSerial(
EncodeDate(2026, 1, 31), False);
MaxSerial := TXLSChartDateAxisTransform.DateTimeToSerial(
EncodeDate(2026, 12, 31), False);
// Krok jednomiesięczny, celuj w 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;
Jest dokładnie jedno przybliżenie w BuildTicks i siedzi tam, gdzie przybliżenie jest nieszkodliwe. Gdy żądany krok wynosi zero, metoda szacuje krok traktując miesiąc jako 30 dni, a rok jako 365, po czym zaokrągla surowy krok w górę do wielokrotności 1, 2 albo 5 potęgi dziesięciu. Tamten szacunek rozstrzyga tylko, ile jednostek mieści się między znacznikami. Pozycje nadal pochodzą z IncMonth i IncYear. Zgrubna długość miesiąca może zrobić oś odrobinę gęstszą albo rzadszą, ale każdy znacznik nadal ląduje na dacie, którą wyprodukowała jedna z tych dwóch funkcji
Numery seryjne skoroszytu zostają współrzędną osi
Oś daty HotXLS trzyma numery seryjne skoroszytu jako swoją przestrzeń współrzędnych, bo TDateTime Delphi nie ma slotu na numer seryjny 60. W systemie dat 1900 tamten numer to 29 lutego 1900, dzień, który nigdy nie istniał i który Excel trzyma dla zgodności ze starszymi programami arkuszowymi. TXLSChartDateAxisTransform.TrySerialToDateTime obsługuje go jedyną drogą, na jaką stać prawdziwy kalendarz: numery poniżej 60 przesuwają się o jeden dzień, a numer 60 składa się na 28 lutego. Dla etykietowania pojedynczego punktu to w porządku. Dla geometrii to zło, bo numery 59 i 60 stają się tym samym TDateTime i 28 lutego oraz 1 marca kończą jedną jednostkę od siebie, tam gdzie Excel rysuje je dwie od siebie
Trzy numery seryjne pokazują obie strony problemu. FormatValue ma specjalny przypadek dla slotu fantomowego i wypisuje dokładnie to, co wypisuje Excel, podczas gdy round-trip przez TDateTime go gubi
var
S: Integer;
D: TDateTime;
begin
for S := 59 to 61 do
begin
TXLSChartDateAxisTransform.TrySerialToDateTime(S, False, D);
Writeln(S, ' axis label: ',
TXLSChartDateAxisTransform.FormatValue(S, False),
' TDateTime: ', FormatDateTime('yyyy-mm-dd', D));
end;
// 59 axis label: 1900-02-28 TDateTime: 1900-02-28
// 60 axis label: 1900-02-29 TDateTime: 1900-02-28
// 61 axis label: 1900-03-01 TDateTime: 1900-03-01
end;
Prawie nikt nie wykresuje danych z początku 1900, więc praktyczna lekcja dotyczy twojego własnego kodu. Jeśli liczysz granice osi albo pozycje adnotacji, rób to na numerach seryjnych i konwertuj przez DateTimeToSerial na krawędzi, tak jak robi to pierwszy snippet. Wymieszaj oś opartą na numerach seryjnych z arytmetyką TDateTime po stronie wołającego, a adnotacja datowana przed 1 marca 1900 wyląduje jeden dzień od swojego punktu. Druga epoka znaczy częściej: w skoroszycie 1904 numer seryjny 0 to 1 stycznia 1904, a transformacja nakłada stały offset 1462 dni w obie strony, gdy argument Dates1904 jest prawdziwy. Obie epoki i to, jak formaty liczbowe komórek z nimi współgrają, opisuje nasz artykuł o numerach seryjnych dat Excela, systemie 1904 i formatach liczbowych
Jak HotXLS wybiera dni, miesiące albo lata automatycznie?
TXLSChartDateAxisTransform.DetectUnit wybiera najdrobniejszą jednostkę kalendarzową, której faktycznie wymagają odstępy w danych. Wyrzuca wartości NaN i nieskończone, sortuje kopię reszty raz i pomija zduplikowane daty. Jeśli jakikolwiek sąsiedni odstęp jest krótszy niż miesiąc kalendarzowy, jednostką są dni; w przeciwnym razie, jeśli jakikolwiek odstęp jest krótszy niż rok kalendarzowy — miesiące; w przeciwnym razie lata. Testem miesiąca jest IncMonth(Previous, 1) > Current i ten wybór ma znaczenie dokładnie dla danych z tego artykułu. Próg 30 dni nazwałby odstęp od 31 stycznia do 28 lutego "mniej niż miesiąc" i przełączyłby miesięczną serię na dni. Przy mniej niż dwóch odrębnych poprawnych datach nie ma odstępu do zmierzenia i metoda zwraca dni
ResolveUnits łączy wykrytą jednostkę z tą, którą deklaruje wykres. Jawna jednostka bazowa wygrywa. Nieobecna jednostka główna albo pomocnicza dziedziczy wykrytą jednostkę, gdy ta jest grubsza od bazowej, a inaczej spada z powrotem na bazową. Ostatnia reguła to ta, którą ludzie gubią: jeśli efektywna jednostka główna albo pomocnicza jest drobniejsza od bazowej, bazowa spada, by się dopasować. Jednostka bazowa rozstrzyga, jak punkty są grupowane, zanim istnieje jakikolwiek znacznik, bo Normalize przypina wartość do jej dnia, pierwszego dnia jej miesiąca albo 1 stycznia. Miesięczna baza pod dzienną jednostką główną zwinęłaby cały miesiąc punktów w jeden slot, zanim kod znaczników je w ogóle zobaczył
Ile automatycznych znaczników głównych się pojawi, zależy od czegoś więcej niż zakresu dat. Długość plotu, zmierzona szerokość fontu osi, format liczbowy etykiet i projektowana szerokość etykiet obróconych wszystko capują liczbę, więc wąski wykres z długimi etykietami mmmm yyyy dostaje mniej znaczników niż szeroki z mm/yy. Etykiety, znaczniki i linie siatki są wszystkie rysowane z jednej tablicy znaczników, co trzyma wyjścia HTML, SVG i PDF w zgodzie, a znacznik pomocniczy wpadający na znacznik główny jest rysowany raz
Co element dateAx zapisuje i co znaczy pominięcie?
W ChartML każde ustawienie kalendarzowe na osi daty jest opcjonalne i pominięcie go mówi coś innego niż zapisanie 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żde wystąpiło albo nie. Pominięty baseTimeUnit każe Excelowi rozstrzygnąć w chwili renderowania; jawny days przypina. TXLSXChartAxis trzyma każdą wartość obok flagi obecności: BaseTimeUnit z BaseTimeUnitSet, MajorTimeUnit z MajorTimeUnitSet, MinorTimeUnit z MinorTimeUnitSet oraz AutoDateAxis z AutoDateAxisSet. Przypisanie wartości ustawia jej flagę, ustawienie flagi na False zwraca element do pominięcia, a pisarz XLSX emituje element tylko, gdy jego flaga jest włączona. auto łatwo źle odczytać: steruje automatyczną detekcją kategoria-przeciwko-dacie i nie ma nic wspólnego z automatycznymi jednostkami znaczników
uses
SysUtils, DateUtils, lxHandleX;
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));
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; // zapisuje c:dateAx
Axis.BaseTimeUnit := xlsxChartTimeDays; // przy okazji ustawia BaseTimeUnitSet
Axis.MajorTimeUnit := xlsxChartTimeMonths;
Axis.MajorUnit := 1; // wartość > 0 znaczy majorUnit jako ustawione
Axis.NumberFormat := 'mmm yyyy';
Axis.NumberFormatSourceLinked := False;
Axis.TextStyle.Rotation := -45; // stopnie
// AutoDateAxis nietknięte: element c:auto nie jest zapisywany
Book.SaveAs('month-end-balance.xlsx');
finally
Book.Free;
end;
end;
Jednostka bazowa tutaj to dni celowo. Daty końców miesięcy wypadają na różne numery dni, a miesięczna baza przypnęłaby każdy punkt do pierwszego dnia jego miesiąca, co przenosi saldo 31 marca na 1 marca. Przypnij bazę do dni, ilekroć dokładny dzień niesie znaczenie, a miesięczne rozstawianie zostaw jednostce głównej
Etykiety i orientacja idą tymi samymi regułami obecności. Element numFmt osi domyślnie ma sourceLinked na true, więc TXLSXChartAxis trzyma kod formatu, wartość source-linked i NumberFormatSet osobno, a pusty kod nigdy nie zostanie wzięty za brakujący element. ChartML trzyma obrót tekstu w 1/60000 stopnia z wartościami dodatnimi biegnącymi zgodnie z ruchem wskazówek zegara; TextStyle.Rotation bierze gołe stopnie i etykieta ustawiona na -45 wychyla się tak samo w wyjściu SVG i PDF. Odwrócona (maxMin) oś zwierciadli znaczniki główne i pomocnicze, linie siatki, punkty danych, linie trendu i słupki błędów razem. Czytanie działa w drugą stronę: otwórz skoroszyt napisany gdzie indziej, a flagi powiedzą ci, co jego 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[1].Charts[0].CategoryAxis; // Sheets[] numeruje od 1, Charts[] od 0
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 ponownie, a pominięte elementy pozostaną pominięte
end;
end;
Jednostki kalendarzowe w plikach ODS i starszych XLS
Model kalendarza sięga tak daleko, jak daleko każdy format pliku potrafi go wyrazić. ODF 1.2 opisuje rozstawienie osi przez chart:interval-major, gołą liczbę bez pojęcia dni, miesięcy ani lat, więc Excelowa miesięczna oś zapisana jako ODS nie zaniesie swojej jednostki. HotXLS zostawia jednostkę poza plikiem ODS, zamiast wymyślać niestandardowy atrybut, który inne aplikacje ODF i tak ignorowałyby. Jeśli miesięczne rozstawienie ma znaczenie dla ludzi otwierających plik, wysyłaj XLSX
Skoroszyty BIFF8 trzymają ustawienia osi daty w rekordzie AxcExt, dziewięć stałych słów 16-bitowych. Jego flagi automatyczne maskują pola bez ich usuwania, więc rekord oznaczony "automatyczne minimum" nadal niesie wartość minimum. HotXLS trzyma zamaskowane wartości minimum, maksimum, interwału, jednostki i przecięcia przy odczycie i zapisuje je z powrotem niezmienione, trzyma nieznane kody jednostek nietknięte w modelu obiektowym XLS i mapuje wyłącznie poprawne jednostki dat 0, 1 i 2 (dni, miesiące, lata), gdy wykres jest konwertowany na XLSX
Oś daty rzadko podróżuje sama. Gdy skoroszyt Excela paruje linię z osią dat z serią kolumnową na osi wtórnej, nasz artykuł o zachowanym ChartML i wykresach kombinowanych wyjaśnia, jak HotXLS odtwarza oryginalny XML wykresu, gdy nic się nie zmieniło, i wtapia typowane edycje w niego, gdy coś się zmieniło, a nasz przegląd wykresów, obrazów i rysunków pokrywa kotwiczenie i wywołania serii użyte wyżej. Strona HotXLS Delphi Component wypisuje pełny zestaw funkcji wykresów dla Delphi i C++Builder, włącznie z czytaniem, zapisem i renderowaniem wykresów XLS i XLSX bez zainstalowanego Excela