HotXLS randează axele de date ale graficelor Excel cu aritmetică de calendar, nu cu numărători fixe de zile. Clasa TXLSChartDateAxisTransform din unit-ul lxChart ține fiecare coordonată ca serial de registru de lucru, calculează fiecare tics de lună sau de an direct din minimul axei prin IncMonth și IncYear și clampă sfârșiturile de lună la calendarul real, astfel încât o serie ancorată pe 31 ianuarie primește ticsuri pe 28 februarie, 31 martie și 30 aprilie în loc să alunece. Aceeași transformare alege automat unități de zi, de lună sau de an și este partajată de randerele de grafice HTML, SVG și PDF paginat
Bug-ul pe care asta îl previne este familiar oricui a desenat grafic o închidere de sfârșit de lună. Un renderer care tratează o lună ca 30 de zile alunecă cu cinci zile mai devreme până la capătul primului an. Un renderer mai deștept, care folosește luni de calendar, dar avansează fiecare tics din cel anterior, eșuează mai silențios: 31 ianuarie devine 28 februarie, pasul următor aterizează pe 28 martie, iar fiecare tics de după rămâne în cui pe 28. Graficul arată plauzibil, etichetele sunt greșite, iar nimeni nu observă până când un contabil nu întreabă de ce soldul din martie este desenat cu trei zile înainte de sfârșitul trimestrului
De ce alunecă ticsurile de lună când sunt acumulate?
Ticsurile de lună alunecă pentru că clamparea aruncă informații. Odată ce 31 ianuarie a fost clampat la 28 februarie, faptul că seria voia 31 a dispărut, iar orice pas făcut din valoarea clampată moștenește pierderea. TXLSChartDateAxisTransform.BuildTicks nu face niciodată un pas dintr-un tics anterior. Tics-ul i este mereu calculat ca AddUnits(MinValue, UnitKind, Step * i, ...), adică min + unitate × index, măsurat din minimul axei. AddUnits adaugă numărul direct la serial pentru unitățile de zi, iar pentru unitățile de lună și de an convertește serialul într-o dată reală, apelează IncMonth sau IncYear și convertește înapoi. Bucla este și ea mărginită pe toate laturile: tabloul rezultat este plafonat la 4096 de intrări indiferent ce cere apelantul, Step * i este verificat față de MaxInt înainte de înmulțire, iar generarea se oprește imediat ce un tics nu mai crește sau trece de maximul axei
uses
SysUtils, lxChart;
var
Ticks: TXLSChartDateValues;
MinSerial, MaxSerial: Double;
I, Count: Integer;
begin
// sistem de date 1900 (Dates1904 = False), serie ancorată la sfârșit de lună
MinSerial := TXLSChartDateAxisTransform.DateTimeToSerial(
EncodeDate(2026, 1, 31), False);
MaxSerial := TXLSChartDateAxisTransform.DateTimeToSerial(
EncodeDate(2026, 12, 31), False);
// pas de o lună, ținta este 12 ticsuri, niciodată mai mult de 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;
Există o singură aproximare onestă în BuildTicks. Când pasul cerut este zero, metoda estimează o mărime de pas tratând o lună ca 30 de zile și un an ca 365, apoi rotunjește pasul brut la un multiplu de 1, 2 sau 5 al unei puteri a lui zece. Estimarea aceea decide doar câte unități stau între ticsuri. Pozițiile ticsurilor în sine vin tot din IncMonth și IncYear, deci aproximarea poate schimba densitatea ticsurilor, dar niciodată nu scoate un tics din calendar
De ce păstrezi serialurile registrului de lucru în loc să convertești la TDateTime?
O axă de date în HotXLS păstrează serialurile registrului de lucru ca spațiu de coordonate pentru că TDateTime-ul din Delphi nu are niciun slot pentru serialul 60, 29 februarie 1900 fantomă, pe care sistemul de date 1900 l-a moștenit ca ciudățenie de compatibilitate. TrySerialToDateTime deplasează serialurile sub 60 cu o zi și pliază serialul 60 pe 28 februarie, ceea ce este acceptabil pentru etichetarea unui singur punct, dar fatal pentru geometrie: convertiți întâi fiecare punct la TDateTime și cele două zile de o parte și de alta a zilei fantomă ajung cu o unitate mai apropiate decât le desenează Excel, deci tot ce este desenat înainte de martie 1900 se deplasează relativ la tot ce urmează. TXLSChartDateAxisTransform.FormatValue tratează special același slot și îl etichetează 1900-02-29 exact ca Excel. Într-un registru de lucru 1904, serialul 0 este 1 ianuarie 1904, iar transformarea aplică offset-ul fix de 1462 de zile în ambele direcții; povestea completă a celor două epoci este în articolul nostru despre serialurile de dată Excel, sistemul 1904 și formatele de numere
Cum alege HotXLS automat zile, luni sau ani?
TXLSChartDateAxisTransform.DetectUnit alege cea mai fină unitate de calendar de care au nevoie efectiv golurile din date. Metoda aruncă întâi valorile NaN și infinite, sortează restul o singură dată în O(n log n) și sărite datele duplicate. Dacă vreun gol adiacent este mai scurt decât o lună de calendar, unitatea este zile; altfel, dacă vreun gol este mai scurt decât un an de calendar, unitatea este luni; altfel este ani. Testul este IncMonth(Previous, 1) > Current, nu un prag de 30 de zile, astfel încât o serie de sfârșit de lună care rulează 31 ianuarie, 28 februarie, 31 martie este detectată corect ca date lunare. Cu mai puțin de două date valide distincte nu există gol de măsurat, iar metoda cade pe zile
ResolveUnits combină apoi unitatea detectată cu orice declară graficul. O unitate de bază explicită este respectată, iar o unitate majoră sau minoră lipsă moștenește unitatea detectată când aceasta este mai grosieră decât baza. Ultimul pas este cel pe care oamenii îl ratează: dacă unitatea efectivă majoră sau minoră este mai fină decât unitatea de bază, baza este coborâtă să se potrivească. Unitatea de bază este la ce sunt normalizate punctele de date înainte să existe vreun tics, cu Normalize prinzând o valoare pe ziua ei, pe prima a lunii ei sau pe 1 ianuarie, astfel încât o bază lunară sub o unitate majoră zilnică ar prăbuși o lună de puncte într-un singur slot înainte ca codul de ticsuri să le vadă vreodată. Densitatea ticsurilor este și ea mărginită, de lungimea planului de desen, de lățimea măsurată a fontului axei, de formatul numeric al etichetelor și de proiecția etichetelor rotite, iar fiecare backend desenează etichete, semne de tics și linii de grilă dintr-un singur tablou partajat și mărginit de ticsuri, cu un tics minor care coincide cu unul major desenat o singură dată
Ce stochează elementul dateAx și ce înseamnă omisiunea?
În ChartML fiecare setare de calendar pe o axă de date este opțională, iar omiterea uneia nu înseamnă același lucru cu scrierea valorii ei implicite. Tipul CT_DateAx din ECMA-376 Partea 1, §21.2 (DrawingML Charts) lasă baseTimeUnit, majorTimeUnit, minorTimeUnit și auto să apară sau nu fiecare, iar un baseTimeUnit omis îi spune Excelului să decidă singur, ceea ce diferă de un days explicit. TXLSXChartAxis stochează prin urmare fiecare valoare lângă un flag de prezență: BaseTimeUnit cu BaseTimeUnitSet, MajorTimeUnit cu MajorTimeUnitSet, MinorTimeUnit cu MinorTimeUnitSet și AutoDateAxis cu AutoDateAxisSet. Atribuirea unei valori îi setează flag-ul, curățarea flag-ului întoarce elementul la omisiune, iar scriitorul XLSX emite un element doar când flag-ul lui este setat. Rețineți că auto înseamnă detecție automată categorie-sau-dată, nu unități de tics automate
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; // scrie c:dateAx
Axis.BaseTimeUnit := xlsxChartTimeDays; // setează și BaseTimeUnitSet
Axis.MajorTimeUnit := xlsxChartTimeMonths;
Axis.MajorUnit := 1; // > 0 marchează majorUnit ca setat
Axis.NumberFormat := 'mmm yyyy';
Axis.NumberFormatSourceLinked := False;
Axis.TextStyle.Rotation := -45; // grade; stocat ca 1/60000
// AutoDateAxis lăsat neatins: niciun element c:auto nu este scris
Book.SaveAs('month-end-balance.xlsx');
finally
Book.Free;
end;
end;
Etichetele și orientarea urmează aceleași reguli conștiente de prezență. numFmt-ul axei stă după titlu și înaintea semnelor de tics, atributul lui sourceLinked are implicit valoarea true, iar TXLSXChartAxis ține separat codul de format, valoarea de legare la sursă și NumberFormatSet, astfel încât un cod gol nu este confundat niciodată cu un element absent. Rotația textului este stocată în 1/60000 de grad, cu valorile pozitive însemnând sens orar; TextStyle.Rotation primește grade simple, ieșirea SVG folosește unghiul ca atare, iar backend-ul paginat, ale cărui unghiuri pozitive merg în sens antiorar, inversează semnul la o singură graniță partajată. O axă maxMin inversată oglindește împreună ticsurile majore și minore, liniile de grilă, punctele de date, liniile de tendință și barele de eroare, și niciodată doar etichetele. Când deschideți un registru de lucru scris altăunde, flag-urile vă spun ce a specificat efectiv autorul
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; // salvați-l din nou și elementele omise rămân omise
end;
end;
Unde se oprește modelul de calendar?
Modelul de calendar se oprește la formatele care nu pot exprima unități de calendar, iar HotXLS nu se dă drept altceva. ODF 1.2 oferă chart:interval-major, care este un număr simplu fără nicio noțiune de zile, luni sau ani, deci o axă lunară Excel salvată ca ODS nu își poate duce unitatea cu ea, iar HotXLS nu inventează un atribut nestandard ca să mimeze una. Pe partea de BIFF8 legacy, înregistrarea AxcExt ține nouă cuvinte fixe pe 16 biți, iar flag-urile ei automate doar maschează câmpuri în loc să le elimine; HotXLS păstrează la citire și la rescriere valorile mascate de minim, maxim, interval, unitate și intersecție, ține codurile de unitate necunoscute intacte în modelul Classic și mapează spre XLSX doar unitățile de dată valide 0, 1 și 2 (zile, luni, ani)
Axele de date sunt un singur strat dintr-un model de grafice care trebuie să supraviețuiască și fișierelor pe care nu le-a creat el însuși. Când un registru de lucru Excel combină o linie cu axă de date cu o serie de coloane pe o axă secundară, articolul nostru despre ChartML păstrat și grafice de combinație arată cum HotXLS redă XML-ul original al graficului byte cu byte când nimic nu s-a schimbat și îmbină editările tipizate în el când ceva s-a schimbat, iar privirea noastră de ansamblu asupra graficelor, imaginilor și desenelor acoperă API-urile de ancorare și de serie folosite mai sus. Totul se livrează în HotXLS Delphi Component pentru Delphi și C++Builder, care citește, scrie și randează grafice XLS și XLSX fără automatizare Excel