Un grafico di saldi a fine mese che parte dal 31 gennaio 2026 dovrebbe mettere il suo tick di aprile sul 30 aprile. Fate avanzare ogni tick dal precedente con mesi di calendario e le etichette vengono 2026-01-31, 2026-02-28, 2026-03-28, 2026-04-28, con ogni tick successivo inchiodato al 28. Fate avanzare invece di 30 giorni piatti e il tick di febbraio atterra sul 2 marzo, mentre quello di dicembre atterra il 27 dicembre. La regola che sopravvive a entrambe le trappole è semplice da enunciare: calcolate ogni tick dal minimo dell'asse, e agganciate le fine mese al calendario reale
Su un grafico di chiusura trimestrale il primo errore è quello pericoloso, perché l'immagine continua a sembrare giusta. La linea sale dove deve, le etichette sono spaziate in modo uniforme, e il saldo di marzo siede tre giorni prima della fine del trimestre. A trovarlo è chi controlla il grafico contro il libro mastro, e per allora il grafico di solito è già stato incollato in un dossier per il consiglio
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. In HotXLS, TXLSChartDateAxisTransform.BuildTicks nell'unità lxChart non parte mai da un tick precedente: il tick i è sempre AddUnits(MinValue, UnitKind, Step * i, ...), così marzo è due mesi dopo il 31 gennaio e viene 31 marzo. AddUnits aggiunge il conteggio direttamente al serial per le unità giorno; per mesi e anni converte il serial in una data vera, chiama IncMonth o IncYear, e riconverte
Il ciclo è limitato su ogni fronte. L'array risultato non supera mai le 4096 voci qualunque cosa il chiamante passi come massimo, Step * i viene controllato contro MaxInt prima che la moltiplicazione avvenga, e la generazione si ferma appena un tick fallisce nel crescere o supera il massimo dell'asse. Un workbook ostile con un intervallo di qualche milione di anni produce un array corto e ritorna
La trasformazione è pubblica, così potete far girare l'aritmetica da voi prima che esista qualsiasi grafico. Questo snippet da console costruisce un anno di tick mensili per la serie di sopra e stampa le etichette che il grafico userà
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'è esattamente un'approssimazione in BuildTicks, e siede dove un'approssimazione è innocua. Quando il passo richiesto è zero, il metodo stima un passo trattando un mese come 30 giorni e un anno come 365, poi arrotonda il passo grezzo per eccesso a un multiplo 1, 2 o 5 di una potenza di dieci. Quella stima decide solo quante unità stanno fra i tick. Le posizioni continuano a venire da IncMonth e IncYear. Una lunghezza del mese grossolana può rendere l'asse un po' più denso o più rado, ma ogni tick atterra comunque su una data prodotta da una di quelle due funzioni
I serial del workbook restano la coordinata dell'asse
Un asse date HotXLS conserva i serial del workbook come proprio spazio di coordinate perché il TDateTime di Delphi non ha uno slot per il serial 60. Nel sistema data 1900 quel serial è il 29 febbraio 1900, un giorno mai esistito che Excel conserva per compatibilità con i programmi foglio di calcolo più vecchi. TXLSChartDateAxisTransform.TrySerialToDateTime lo gestisce all'unico modo in cui può farlo un calendario reale: i serial sotto 60 slittano di un giorno e il serial 60 ripiega sul 28 febbraio. Per etichettare un singolo punto va bene. Per la geometria è sbagliato, dato che i serial 59 e 60 diventano lo stesso TDateTime, e il 28 febbraio e il 1 marzo finiscono a una unità di distanza dove Excel li disegna a due
Tre serial mostrano i due lati del problema. FormatValue tratta lo slot fantasma come caso speciale e stampa esattamente ciò che stampa Excel, mentre un round trip attraverso TDateTime lo perde
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 etichetta asse: 1900-02-28 TDateTime: 1900-02-28
// 60 etichetta asse: 1900-02-29 TDateTime: 1900-02-28
// 61 etichetta asse: 1900-03-01 TDateTime: 1900-03-01
end;
Quasi nessuno fa grafici con dati dei primi del 1900, quindi la lezione pratica riguarda il vostro codice. Se calcolate limiti dell'asse o posizioni di annotazioni, fatelo in serial e convertite con DateTimeToSerial al confine, come fa il primo snippet. Mescolate un asse basato sui serial con aritmetica TDateTime nel chiamante, e un'annotazione datata prima del 1 marzo 1900 atterra a un giorno di distanza dal suo punto. Il secondo epoch conta più spesso: in un workbook 1904 il serial 0 è il 1 gennaio 1904, e la trasformazione applica l'offset fisso di 1462 giorni in entrambe le direzioni quando l'argomento Dates1904 è vero. Entrambi gli epoch, e come i formati numerici delle celle interagiscono con loro, sono trattati in il 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. Butta i valori NaN e infiniti, ordina una copia del resto una volta sola, 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, mesi; altrimenti anni. Il test del mese è IncMonth(Previous, 1) > Current, e quella scelta conta esattamente per i dati di questo articolo. Una soglia di 30 giorni chiamerebbe il gap dal 31 gennaio al 28 febbraio "meno di un mese" e passerebbe una serie mensile ai giorni. Con meno di due date valide distinte non c'è gap da misurare, e il metodo restituisce giorni
ResolveUnits fonde l'unità rilevata con ciò che il grafico dichiara. Un'unità base esplicita vince. Un'unità major o minor assente eredita l'unità rilevata quando questa è più grossolana della base, e altrimenti ricade sulla base. L'ultima regola è quella che la gente si perde: se l'unità major o minor effettiva è più fina della base, la base scende per allinearsi. L'unità base decide come i punti vengono raggruppati prima che esista qualsiasi tick, perché Normalize aggancia un valore al suo giorno, al primo del suo mese o al 1 gennaio. Una base mensile sotto un'unità major giornaliera ripiegherebbe un intero mese di punti in uno slot prima ancora che il codice dei tick li vedesse
Quanti tick major automatici compaiono dipende da più che dall'intervallo di date. La lunghezza del disegno, la larghezza misurata del font dell'asse, il formato numerico delle etichette e la larghezza proiettata delle etichette ruotate tappano tutti il conteggio, così un grafico stretto con lunghe etichette mmmm yyyy ottiene meno tick di uno largo con mm/yy. Etichette, segni di tick e griglie vengono tutti disegnati da un unico array di tick, il che tiene d'accordo l'output HTML, SVG e PDF, e un tick minor che cade su un tick major viene 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 dice qualcosa di diverso dallo scriverne il 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. Un baseTimeUnit omesso dice a Excel di decidere al momento del rendering; un days esplicito lo blocca. TXLSXChartAxis tiene 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, riportare il flag a False riporta l'elemento all'omissione, e il writer XLSX emette un elemento solo quando il suo flag è attivo. auto si legge male facilmente: controlla il rilevamento automatico categoria-contro-data e non c'entra nulla con le unità di tick automatiche
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; // scrive c:dateAx
Axis.BaseTimeUnit := xlsxChartTimeDays; // imposta anche BaseTimeUnitSet
Axis.MajorTimeUnit := xlsxChartTimeMonths;
Axis.MajorUnit := 1; // un valore > 0 marca majorUnit come impostato
Axis.NumberFormat := 'mmm yyyy';
Axis.NumberFormatSourceLinked := False;
Axis.TextStyle.Rotation := -45; // gradi
// AutoDateAxis lasciato intatto: nessun elemento c:auto viene scritto
Book.SaveAs('month-end-balance.xlsx');
finally
Book.Free;
end;
end;
L'unità base qui è giorni di proposito. Le date di fine mese cadono su numeri di giorno diversi, e una base mensile aggancerebbe ogni punto al primo del suo mese, spostando il saldo del 31 marzo al 1 marzo. Bloccate la base sui giorni ogni volta che il giorno esatto porta un significato, e lasciate che sia l'unità major a fare la spaziatura mensile
Etichette e orientamento seguono le stesse regole di presenza. L'elemento numFmt dell'asse porta sourceLinked a true per predefinito, così TXLSXChartAxis tiene separati il codice di formato, il valore source-linked e NumberFormatSet, e un codice vuoto non viene mai scambiato per un elemento mancante. ChartML memorizza la rotazione del testo in 1/60000 di grado con valori positivi che girano in senso orario; TextStyle.Rotation prende gradi semplici, e un'etichetta a -45 pende allo stesso modo nell'output SVG e PDF. Un asse invertito (maxMin) rispecchia insieme tick major e minor, griglie, punti dati, linee di tendenza e barre di errore. La lettura funziona nel verso contrario: aprite un workbook scritto da altri e i flag vi dicono cosa il suo 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[1].Charts[0].CategoryAxis; // Sheets[] è in base 1, Charts[] è in base 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; // salvatelo di nuovo e gli elementi omessi restano omessi
end;
end;
Unità di calendario nei file ODS e XLS legacy
Il modello di calendario arriva fin dove ogni formato di file lo può esprimere. ODF 1.2 descrive la spaziatura dell'asse con chart:interval-major, un numero semplice senza nozione di giorni, mesi o anni, quindi un asse mensile di Excel salvato come ODS non può trasportare la propria unità. HotXLS lascia fuori l'unità dal file ODS anziché inventare un attributo non standard che le altre applicazioni ODF ignorerebbero comunque. Se la spaziatura mensile conta per chi apre il file, spedite XLSX
I workbook BIFF8 tengono le impostazioni dell'asse date nel record AxcExt, nove parole fisse a 16 bit. I suoi flag automatici mascherano i campi senza rimuoverli, così un record marcato "minimo automatico" porta comunque un valore di minimo. HotXLS conserva alla lettura il minimo, il massimo, l'intervallo, l'unità e i valori di incrocio mascherati e li riscrive invariati, mantiene integri i codici di unità sconosciuti nel modello a oggetti XLS, e mappa solo le unità date valide 0, 1 e 2 (giorni, mesi, anni) quando un grafico viene convertito in XLSX
Un asse date viaggia raramente da solo. Quando un workbook Excel abbina una linea su asse date a una serie a colonne su un asse secondario, il nostro articolo su ChartML preservato e grafici combinati spiega come HotXLS riproduce l'XML originale del grafico quando nulla è cambiato e vi fonde modifiche tipizzate quando qualcosa è cambiato, e la nostra panoramica su grafici, immagini e disegni copre le chiamate di ancoraggio e serie usate qui sopra. La pagina del componente HotXLS Delphi elenca l'intero set di funzionalità dei grafici per Delphi e C++Builder, compresa lettura, scrittura e rendering di grafici XLS e XLSX senza Excel installato