Articolo tecnico

Asse date dei grafici in HotXLS: tick fedeli al calendario

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

Due modi in cui un asse date dei grafici HotXLS può piazzare tick mensili dopo un ancoraggio al 31 gennaio 2026: avanzare ogni tick dal precedente valore agganciato resta inchiodato al 28, mentre BuildTicks calcola AddUnits(MinValue, xcduMonths, Step times i) dal minimo dell'asse così i tick atterrano su 28 febbraio, 31 marzo e 30 aprile
L'aggancio butta via informazione, così TXLSChartDateAxisTransform non parte mai da un tick precedente e ricalcola ogni posizione dal minimo dell'asse, agganciando le fine mese al calendario reale

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

Perché un asse date HotXLS conserva i serial del workbook invece di convertire a TDateTime: il serial 60 è il fantomatico 29 febbraio 1900, TrySerialToDateTime sposta i serial sotto 60 e ripiega il 60 sul 28 febbraio così i giorni ai lati finiscono un'unità più vicini di come li disegna Excel, mentre FormatValue etichetta il serial 60 come 1900-02-29
Il ripiegamento va bene per etichettare un singolo punto ma è fatale per la geometria, perché tutto ciò che è tracciato prima di marzo 1900 si sposta contro tutto ciò che viene dopo; un workbook 1904 applica invece l'offset fisso di 1462 giorni

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

Come HotXLS sceglie automaticamente le unità dell'asse date: DetectUnit butta i valori NaN e infiniti, ordina una volta sola e misura i gap di calendario con IncMonth invece che con una soglia di 30 giorni, così una serie 31 gennaio, 28 febbraio, 31 marzo viene rilevata come mensile, e ResolveUnits rispetta una base esplicita abbassandola quando l'unità major è più fina
Normalize aggancia ogni valore al suo giorno, al primo del suo mese o al 1 gennaio prima che esista qualsiasi tick, perché una base mensile sotto un'unità major giornaliera collasserebbe un mese di punti in uno slot

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