HotXLS renderizza gli assi date dei grafici Excel con aritmetica di calendario, non con conteggi fissi di giorni. La classe TXLSChartDateAxisTransform nell'unit lxChart tiene ogni coordinata come serial del workbook, calcola ogni tick mensile o annuale direttamente dal minimo dell'asse tramite IncMonth e IncYear, e aggancia le fine mese al calendario reale, così una serie ancorata al 31 gennaio riceve tick su 28 febbraio, 31 marzo e 30 aprile invece di scivolare. La stessa transform sceglie automaticamente le unità giorno, mese o anno ed è condivisa dai renderer dei grafici HTML, SVG e PDF paginato
Il bug che questo previene è familiare a chiunque abbia graficato una chiusura di fine mese. Un renderer che tratta un mese come 30 giorni deriva di cinque giorni in anticipo entro la fine del primo anno. Un renderer più furbo che usa mesi di calendario, ma avanza ogni tick dal precedente, fallisce più silenziosamente: il 31 gennaio diventa 28 febbraio, il passo successivo atterra sul 28 marzo, e ogni tick dopo quello resta inchiodato al 28. Il grafico sembra plausibile, le etichette sono sbagliate, e nessuno se ne accorge finché un controller non chiede perché il saldo di marzo è tracciato tre giorni prima della fine del trimestre
Perché i tick mensili derivano quando vengono accumulati?
I tick mensili derivano perché l'aggancio butta via informazione. Una volta che il 31 gennaio è stato agganciato al 28 febbraio, il fatto che la serie volesse il 31 è perduto, e ogni passo compiuto dal valore agganciato eredita la perdita. TXLSChartDateAxisTransform.BuildTicks non parte mai da un tick precedente. Il tick i è sempre calcolato come AddUnits(MinValue, UnitKind, Step * i, ...), cioè min + unità × indice misurato dal minimo dell'asse. AddUnits aggiunge il conteggio direttamente al serial per le unità giorno e, per le unità mese e anno, converte il serial in una data reale, chiama IncMonth o IncYear, e riconverte. Il loop è anche limitato su ogni fronte: l'array risultato è tappato a 4096 voci qualunque cosa chieda il chiamante, Step * i viene verificato contro MaxInt prima di essere moltiplicato, e la generazione si ferma appena un tick non riesce a crescere o supera il massimo dell'asse
uses
SysUtils, lxChart;
var
Ticks: TXLSChartDateValues;
MinSerial, MaxSerial: Double;
I, Count: Integer;
begin
// Sistema data 1900 (Dates1904 = False), serie ancorata a fine mese
MinSerial := TXLSChartDateAxisTransform.DateTimeToSerial(
EncodeDate(2026, 1, 31), False);
MaxSerial := TXLSChartDateAxisTransform.DateTimeToSerial(
EncodeDate(2026, 12, 31), False);
// Passo di un mese, obiettivo 12 tick, mai più di 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;
C'è una sola approssimazione onesta in BuildTicks. Quando il passo richiesto è zero, il metodo stima una dimensione del passo trattando un mese come 30 giorni e un anno come 365, poi arrotonda il passo grezzo a un multiplo 1, 2 o 5 di una potenza di dieci. Quella stima decide solo quante unità stanno tra i tick. Le posizioni dei tick continuano comunque a venire da IncMonth e IncYear, quindi l'approssimazione può cambiare la densità dei tick ma non sposta mai un tick fuori dal calendario
Perché conservare i serial del workbook invece di convertire a TDateTime?
Un asse date in HotXLS conserva i serial del workbook come spazio di coordinate perché il TDateTime di Delphi non ha uno slot per il serial 60, il fantomatico 29 febbraio 1900 che il sistema data 1900 ha ereditato come stranezza di compatibilità. TrySerialToDateTime sposta di un giorno i serial sotto 60 e ripiega il serial 60 sul 28 febbraio, il che va bene per etichettare un singolo punto ma è fatale per la geometria: convertite prima ogni punto a TDateTime e i due giorni ai lati del giorno fantasma finiscono un'unità più vicini di quanto Excel li disegni, così tutto ciò che è tracciato prima di marzo 1900 si sposta rispetto a tutto ciò che viene dopo. TXLSChartDateAxisTransform.FormatValue tratta lo stesso slot come caso speciale e lo etichetta 1900-02-29 esattamente come fa Excel. In un workbook 1904, il serial 0 è il 1 gennaio 1904, e la transform applica l'offset fisso di 1462 giorni in entrambe le direzioni; la storia completa delle due epoche è nel nostro articolo sui serial data di Excel, il sistema 1904 e i formati numerici
Come sceglie HotXLS automaticamente giorni, mesi o anni?
TXLSChartDateAxisTransform.DetectUnit sceglie l'unità di calendario più fina che i gap nei dati richiedono davvero. Il metodo butta prima i valori NaN e infiniti, ordina il resto una volta sola in O(n log n), e salta le date duplicate. Se un gap adiacente è più corto di un mese di calendario, l'unità è giorni; altrimenti, se un gap è più corto di un anno di calendario, l'unità è mesi; altrimenti sono anni. Il test è IncMonth(Previous, 1) > Current, non una soglia di 30 giorni, così una serie di fine mese che corre 31 gennaio, 28 febbraio, 31 marzo viene rilevata correttamente come dati mensili. Con meno di due date valide distinte non c'è gap da misurare, e il metodo ricade sui giorni
ResolveUnits poi fonde l'unità rilevata con quella che il grafico dichiara. Un'unità base esplicita viene rispettata, e un'unità major o minor assente eredita l'unità rilevata quando questa è più grossolana della base. L'ultimo passo è quello che la gente si perde: se l'unità major o minor effettiva è più fina dell'unità base, la base viene abbassata per allinearsi. L'unità base è ciò a cui i punti dati vengono normalizzati prima che esista qualsiasi tick, con Normalize che aggancia un valore al suo giorno, al primo del suo mese o al 1 gennaio, così una base mensile sotto un'unità major giornaliera collasserebbe un mese di punti in uno slot prima ancora che il codice dei tick li vedesse. Anche la densità dei tick è limitata, dalla lunghezza del plot, dalla larghezza misurata del font dell'asse, dal formato numerico delle etichette e dalla proiezione delle etichette ruotate, e ogni backend disegna etichette, segni di tick e griglie da un unico array di tick condiviso e limitato, con un tick minor che coincide con uno major disegnato una volta sola
Che cosa memorizza l'elemento dateAx, e cosa significa l'omissione?
In ChartML ogni impostazione di calendario su un asse date è opzionale, e ometterne una non è la stessa cosa che scriverne il valore predefinito. Il tipo CT_DateAx in ECMA-376 Part 1, §21.2 (DrawingML Charts) lascia che baseTimeUnit, majorTimeUnit, minorTimeUnit e auto compaiano o no ciascuno, e un baseTimeUnit omesso dice a Excel di decidere da sé, il che è diverso da un days esplicito. TXLSXChartAxis quindi memorizza ogni valore accanto a un flag di presenza: BaseTimeUnit con BaseTimeUnitSet, MajorTimeUnit con MajorTimeUnitSet, MinorTimeUnit con MinorTimeUnitSet, e AutoDateAxis con AutoDateAxisSet. Assegnare un valore imposta il suo flag, azzerare il flag riporta l'elemento all'omissione, e il writer XLSX emette un elemento solo quando il suo flag è impostato. Notate che auto significa rilevamento automatico categoria-contro-data, non unità dei tick automatiche
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; // scrive c:dateAx
Axis.BaseTimeUnit := xlsxChartTimeDays; // imposta anche BaseTimeUnitSet
Axis.MajorTimeUnit := xlsxChartTimeMonths;
Axis.MajorUnit := 1; // > 0 segna majorUnit come impostato
Axis.NumberFormat := 'mmm yyyy';
Axis.NumberFormatSourceLinked := False;
Axis.TextStyle.Rotation := -45; // gradi; memorizzato come 1/60000
// AutoDateAxis lasciato intatto: nessun elemento c:auto viene scritto
Book.SaveAs('month-end-balance.xlsx');
finally
Book.Free;
end;
end;
Etichette e orientamento seguono le stesse regole consapevoli della presenza. Il numFmt dell'asse sta dopo il titolo e prima dei segni di tick, il suo attributo sourceLinked vale true per default, e TXLSXChartAxis tiene separati il codice di formato, il valore source-linked e NumberFormatSet, così un codice vuoto non viene mai scambiato per un elemento assente. La rotazione del testo è memorizzata in 1/60000 di grado con i valori positivi che significano senso orario; TextStyle.Rotation prende gradi semplici, l'output SVG usa l'angolo così com'è, e il backend paginato, i cui angoli positivi girano in senso antiorario, inverte il segno a un unico confine condiviso. Un asse maxMin rovesciato specchia insieme i tick major e minor, le griglie, i punti dati, le linee di tendenza e le barre di errore, e mai solo le etichette. Quando aprite un workbook scritto altrove, i flag vi dicono che cosa l'autore ha davvero specificato
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; // salvate di nuovo e gli elementi omessi restano omessi
end;
end;
Dove si ferma il modello di calendario?
Il modello di calendario si ferma ai formati che non possono esprimere unità di calendario, e HotXLS non finge il contrario. ODF 1.2 offre chart:interval-major, che è un numero nudo e crudo senza nozione di giorni, mesi o anni, quindi un asse mensile di Excel salvato come ODS non può trasportare la sua unità e HotXLS non inventa un attributo non standard per fingerne uno. Sul fronte legacy BIFF8, il record AxcExt contiene nove word fisse a 16 bit, e i suoi flag automatici mascherano i campi invece di rimuoverli; HotXLS preserva minimo, massimo, intervallo, unità e valori di attraversamento mascherati in lettura e in riscrittura, tiene intatti i codici unità sconosciuti nel modello Classic, e mappa su XLSX solo le unità data valide 0, 1 e 2 (giorni, mesi, anni)
Gli assi date sono uno strato di un modello di grafici che deve anche sopravvivere a file che non ha creato lui. Quando un workbook Excel combina una linea su asse date con una serie a colonne su un asse secondario, il nostro articolo su ChartML preservato e grafici combinati mostra come HotXLS riproduce l'XML originale del grafico byte per byte quando nulla è cambiato e vi fonde le modifiche tipizzate quando qualcosa è cambiato, e la nostra panoramica su grafici, immagini e drawing copre le API di ancoraggio e serie usate sopra. Il tutto viaggia nel componente HotXLS per Delphi per Delphi e C++Builder, che legge, scrive e renderizza grafici XLS e XLSX senza automazione di Excel