HotXLS rend les axes de dates des graphiques Excel avec de l'arithmétique calendaire, pas avec un compte de jours fixe. La classe TXLSChartDateAxisTransform de l'unité lxChart garde chaque coordonnée sous forme de serial du classeur, calcule chaque graduation de mois ou d'année directement depuis le minimum de l'axe via IncMonth et IncYear, et ramène les fins de mois au calendrier réel, si bien qu'une série ancrée au 31 janvier obtient des graduations au 28 février, au 31 mars et au 30 avril au lieu de glisser. La même transformation choisit automatiquement les unités jour, mois ou année et est partagée par les moteurs de rendu de graphiques HTML, SVG et PDF paginé
Le bug que cela prévient est familier à quiconque a graphié une clôture de fin de mois. Un moteur qui traite un mois comme 30 jours dérive de cinq jours trop tôt à la fin de la première année. Un moteur plus malin qui utilise les mois du calendrier mais fait avancer chaque graduation depuis la précédente échoue plus silencieusement : le 31 janvier devient le 28 février, l'étape suivante tombe le 28 mars, et chaque graduation après cela reste scotchée au 28. Le graphique a l'air plausible, les libellés sont faux, et personne ne le remarque jusqu'à ce qu'un contrôleur de gestion demande pourquoi le solde de mars est tracé trois jours avant la fin du trimestre
Pourquoi les graduations de mois dérivent-elles quand on les accumule ?
Les graduations de mois dérivent parce que le clamp jette de l'information. Une fois que le 31 janvier a été clampé au 28 février, le fait que la série voulait le 31 est perdu, et toute étape prise depuis la valeur clampée hérite de cette perte. TXLSChartDateAxisTransform.BuildTicks ne fait jamais d'étape depuis une graduation précédente. La graduation i est toujours calculée comme AddUnits(MinValue, UnitKind, Step * i, ...), c'est-à-dire min + unité × index mesuré depuis le minimum de l'axe. AddUnits ajoute le compte directement au serial pour les unités de jours et, pour les unités de mois et d'années, convertit le serial en vraie date, appelle IncMonth ou IncYear, puis reconvertit. La boucle est aussi bornée de tous les côtés : le tableau résultat est plafonné à 4096 entrées quoi que demande l'appelant, Step * i est contrôlé contre MaxInt avant d'être multiplié, et la génération s'arrête dès qu'une graduation échoue à croître ou dépasse le maximum de l'axe
uses
SysUtils, lxChart;
var
Ticks: TXLSChartDateValues;
MinSerial, MaxSerial: Double;
I, Count: Integer;
begin
// système de dates 1900 (Dates1904 = False), série ancrée en fin de mois
MinSerial := TXLSChartDateAxisTransform.DateTimeToSerial(
EncodeDate(2026, 1, 31), False);
MaxSerial := TXLSChartDateAxisTransform.DateTimeToSerial(
EncodeDate(2026, 12, 31), False);
// pas d'un mois, viser 12 graduations, jamais plus 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;
Il y a une approximation honnête dans BuildTicks. Quand le pas demandé est zéro, la méthode estime une taille de pas en traitant un mois comme 30 jours et une année comme 365, puis arrondit le pas brut à un multiple de 1, 2 ou 5 d'une puissance de dix. Cette estimation ne décide que du nombre d'unités entre les graduations. Les positions des graduations elles-mêmes viennent toujours de IncMonth et IncYear, si bien que l'approximation peut changer la densité des graduations mais ne déplace jamais une graduation hors du calendrier
Pourquoi garder les serials du classeur au lieu de convertir en TDateTime ?
Un axe de dates dans HotXLS garde les serials du classeur comme espace de coordonnées parce que le TDateTime de Delphi n'a pas d'emplacement pour le serial 60, le fantôme 29 février 1900 que le système de dates 1900 a hérité comme bizarrerie de compatibilité. TrySerialToDateTime décale les serials inférieurs à 60 d'un jour et replie le serial 60 sur le 28 février, ce qui va pour étiqueter un point isolé mais est fatal pour la géométrie : convertissez d'abord chaque point en TDateTime et les deux jours de part et d'autre du jour fantôme se retrouvent une unité plus proches qu'Excel ne les dessine, si bien que tout ce qui est tracé avant mars 1900 se décale par rapport à tout ce qui vient après. TXLSChartDateAxisTransform.FormatValue traite le même emplacement comme cas particulier et l'étiquette 1900-02-29 exactement comme Excel. Dans un classeur 1904, le serial 0 est le 1er janvier 1904, et la transformation applique le décalage fixe de 1462 jours dans les deux sens ; toute l'histoire des deux époques est dans notre article sur les serials de dates Excel, le système 1904 et les formats de nombre
Comment HotXLS choisit-il automatiquement jours, mois ou années ?
TXLSChartDateAxisTransform.DetectUnit choisit l'unité calendaire la plus fine que les écarts dans les données exigent réellement. La méthode écarte d'abord les valeurs NaN et infinies, trie le reste une fois en O(n log n), et saute les dates dupliquées. Si un écart adjacent quelconque est plus court qu'un mois calendaire, l'unité est les jours ; sinon, si un écart quelconque est plus court qu'une année calendaire, l'unité est les mois ; sinon c'est les années. Le test est IncMonth(Previous, 1) > Current, pas un seuil de 30 jours, si bien qu'une série de fins de mois déroulant 31 janvier, 28 février, 31 mars est correctement détectée comme des données mensuelles. Avec moins de deux dates valides distinctes, il n'y a aucun écart à mesurer, et la méthode retombe sur les jours
ResolveUnits fusionne ensuite l'unité détectée avec ce que le graphique déclare. Une unité de base explicite est honorée, et une unité majeure ou mineure manquante hérite de l'unité détectée quand celle-ci est plus grossière que la base. La dernière étape est celle que les gens ratent : si l'unité majeure ou mineure effective est plus fine que l'unité de base, la base est abaissée pour s'aligner. L'unité de base est ce à quoi les points de données sont normalisés avant qu'aucune graduation n'existe, avec Normalize accrochant une valeur à son jour, au premier de son mois ou au 1er janvier, si bien qu'une base mensuelle sous une unité majeure journalière réduirait un mois de points à un seul emplacement avant même que le code de graduations ne les voie. La densité de graduations est bornée elle aussi, par la longueur du tracé, la largeur mesurée de la police de l'axe, le format de nombre des libellés et la projection des libellés pivotés, et chaque backend dessine libellés, marques de graduation et lignes de grille depuis un tableau de graduations partagé et borné, une graduation mineure qui coïncide avec une majeure n'étant dessinée qu'une fois
Que stocke l'élément dateAx, et que signifie l'omission ?
Dans ChartML chaque réglage calendaire d'un axe de dates est optionnel, et omettre l'un d'eux n'est pas la même chose qu'écrire sa valeur par défaut. Le type CT_DateAx dans ECMA-376 Partie 1, §21.2 (DrawingML Charts) laisse baseTimeUnit, majorTimeUnit, minorTimeUnit et auto apparaître ou non, et un baseTimeUnit omis dit à Excel de décider lui-même, ce qui diffère d'un days explicite. TXLSXChartAxis stocke donc chaque valeur à côté d'un drapeau de présence : BaseTimeUnit avec BaseTimeUnitSet, MajorTimeUnit avec MajorTimeUnitSet, MinorTimeUnit avec MinorTimeUnitSet, et AutoDateAxis avec AutoDateAxisSet. Affecter une valeur positionne son drapeau, effacer le drapeau ramène l'élément à l'omission, et l'écrivain XLSX n'émet un élément que lorsque son drapeau est positionné. Notez que auto signifie la détection automatique catégorie contre date, pas des unités de graduation automatiques
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; // écrit c:dateAx
Axis.BaseTimeUnit := xlsxChartTimeDays; // positionne aussi BaseTimeUnitSet
Axis.MajorTimeUnit := xlsxChartTimeMonths;
Axis.MajorUnit := 1; // > 0 marque majorUnit comme positionné
Axis.NumberFormat := 'mmm yyyy';
Axis.NumberFormatSourceLinked := False;
Axis.TextStyle.Rotation := -45; // degrés ; stocké en 1/60000
// AutoDateAxis laissé intact : aucun élément c:auto n'est écrit
Book.SaveAs('month-end-balance.xlsx');
finally
Book.Free;
end;
end;
Libellés et orientation suivent les mêmes règles sensibles à la présence. Le numFmt de l'axe se situe après le titre et avant les marques de graduation, son attribut sourceLinked vaut true par défaut, et TXLSXChartAxis garde séparément le code de format, la valeur source-linked et NumberFormatSet, si bien qu'un code vide n'est jamais confondu avec un élément absent. La rotation du texte est stockée en 1/60000 de degré, les valeurs positives signifiant horaire ; TextStyle.Rotation prend des degrés ordinaires, la sortie SVG utilise l'angle tel quel, et le backend paginé, dont les angles positifs tournent anti-horaire, inverse le signe à une frontière partagée unique. Un axe maxMin inversé met en miroir les graduations majeures et mineures, les lignes de grille, les points de données, les courbes de tendance et les barres d'erreur ensemble, et jamais les seuls libellés. Quand vous ouvrez un classeur écrit ailleurs, les drapeaux vous disent ce que l'auteur a réellement spécifié
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; // sauvegardez à nouveau et les éléments omis restent omis
end;
end;
Où le modèle calendaire s'arrête-t-il ?
Le modèle calendaire s'arrête aux formats qui ne peuvent pas exprimer d'unités calendaires, et HotXLS ne fait pas semblant du contraire. ODF 1.2 propose chart:interval-major, qui est un simple nombre sans notion de jours, de mois ou d'années, si bien qu'un axe mensuel Excel sauvegardé en ODS ne peut pas transporter son unité et HotXLS n'invente pas d'attribut non standard pour en simuler une. Du côté BIFF8 historique, l'enregistrement AxcExt contient neuf mots 16 bits fixes, et ses drapeaux automatiques ne font que masquer des champs au lieu de les supprimer ; HotXLS préserve les valeurs masquées de minimum, maximum, intervalle, unité et croisement à la lecture et à la réécriture, garde les codes d'unité inconnus intacts dans le modèle Classic, et ne mappe que les unités de dates valides 0, 1 et 2 (jours, mois, années) vers XLSX
Les axes de dates sont une couche d'un modèle de graphique qui doit aussi survivre à des fichiers qu'il n'a pas créés. Quand un classeur Excel combine une courbe à axe de dates avec une série de colonnes sur un axe secondaire, notre article sur le ChartML préservé et les graphiques combinés montre comment HotXLS rejoue le XML du graphique d'origine octet pour octet quand rien n'a changé et y fusionne des modifications typées quand quelque chose a changé, et notre panorama des graphiques, images et drawings couvre les API d'ancrage et de séries utilisées plus haut. Le tout est livré dans le HotXLS Delphi Component pour Delphi et C++Builder, qui lit, écrit et rend les graphiques XLS et XLSX sans automation Excel