Un graphique de soldes de fin de mois qui démarre le 31 janvier 2026 devrait poser sa graduation d'avril le 30 avril. Avancez chaque graduation depuis la précédente avec des mois de calendrier et les étiquettes sortent en 2026-01-31, 2026-02-28, 2026-03-28, 2026-04-28, chaque graduation ultérieure coincée au 28. Avancez d'un plat 30 jours à la place et la graduation de février atterrit le 2 mars, tandis que celle de décembre atterrit le 27 décembre. La règle qui survit aux deux pièges est simple à énoncer : calculez chaque graduation depuis le minimum de l'axe, et bornez les fins de mois au vrai calendrier
Sur un graphique de clôture trimestrielle la première erreur est la dangereuse, parce que l'image paraît encore juste. La ligne monte où elle doit, les étiquettes sont régulièrement espacées, et le solde de mars siège trois jours avant la fin du trimestre. C'est la contrôleur de gestion qui compare le graphique au grand livre qui le trouve, et à ce moment le graphique a d'ordinaire déjà été collé dans un dossier de conseil d'administration
Pourquoi les graduations mensuelles dérivent-elles quand on les accumule ?
Les graduations mensuelles dérivent parce que le bornage jette de l'information. Une fois le 31 janvier borné au 28 février, le fait que la série voulait le 31 a disparu, et tout pas pris depuis la valeur bornée hérite de la perte. Dans HotXLS, TXLSChartDateAxisTransform.BuildTicks dans l'unité lxChart ne fait jamais de pas depuis une graduation précédente : la graduation i est toujours AddUnits(MinValue, UnitKind, Step * i, ...), donc mars vaut deux mois après le 31 janvier et sort en 31 mars. AddUnits ajoute le compte tout droit au serial pour les unités de jours ; pour les unités de mois et d'années il convertit le serial en vraie date, appelle IncMonth ou IncYear, et reconvertit
La boucle est bornée de tous les côtés. Le tableau résultat ne dépasse jamais 4096 entrées quoi que passe l'appelant comme maximum, Step * i se contrôle contre MaxInt avant que la multiplication n'arrive, et la génération s'arrête dès qu'une graduation échoue à croître ou dépasse le maximum de l'axe. Un classeur hostile avec une étendue de quelques millions d'années produit un tableau court et rend
La transformation est publique, donc vous pouvez faire tourner l'arithmétique vous-même avant qu'aucun graphique n'existe. Ce snippet console bâtit une année de graduations mensuelles pour la série ci-dessus et imprime les étiquettes qu'emploiera le graphique
uses
SysUtils, lxChart;
var
Ticks: TXLSChartDateValues;
MinSerial, MaxSerial: Double;
I, Count: Integer;
begin
// Système de dates 1900 (Dates1904 = False), série ancrée 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 exactement une approximation dans BuildTicks, et elle siège là où une approximation est sans danger. Quand le pas demandé est zéro, la méthode estime un pas en traitant un mois comme 30 jours et une année comme 365, puis arrondit le pas brut vers un multiple 1, 2 ou 5 d'une puissance de dix. Cette estimation ne décide que du nombre d'unités entre graduations. Les positions viennent toujours d'IncMonth et IncYear. Une longueur de mois approximative peut rendre l'axe un peu plus dense ou plus clairsemé, mais chaque graduation atterrit toujours sur une date que l'une de ces deux fonctions a produite
Les serials du classeur restent la coordonnée de l'axe
Un axe de dates HotXLS garde les serials du classeur comme espace de coordonnées parce que le TDateTime de Delphi n'a pas de case pour le serial 60. Dans le système de dates 1900 ce serial est le 29 février 1900, un jour qui n'a jamais existé et qu'Excel garde pour compatibilité avec d'anciens programmes tableurs. TXLSChartDateAxisTransform.TrySerialToDateTime le gère de la seule façon qu'un vrai calendrier peut : les serials sous 60 se décalent d'un jour et le serial 60 se replie sur le 28 février. Pour étiqueter un point isolé ça va. Pour la géométrie c'est faux, car les serials 59 et 60 deviennent le même TDateTime, et le 28 février et le 1er mars finissent à une unité de distance là où Excel les dessine à deux
Trois serials montrent les deux versants du problème. FormatValue traite spécialement la case fantôme et imprime exactement ce qu'Excel imprime, tandis qu'un aller-retour par TDateTime la perd
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 axis label: 1900-02-28 TDateTime: 1900-02-28
// 60 axis label: 1900-02-29 TDateTime: 1900-02-28
// 61 axis label: 1900-03-01 TDateTime: 1900-03-01
end;
Presque personne ne met en graphique des données du début 1900, donc la leçon pratique concerne votre propre code. Si vous calculez des bornes d'axe ou des positions d'annotations, faites-le en serials et convertissez avec DateTimeToSerial au bord, comme le fait le premier snippet. Mélangez un axe à base de serials avec de l'arithmétique TDateTime dans l'appelant, et une annotation datée avant le 1er mars 1900 atterrit à un jour de son point. La seconde époque compte plus souvent : 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 quand l'argument Dates1904 est vrai. Les deux époques, et comment les formats de nombres de cellules s'y mêlent, sont couverts dans notre article sur les serials de dates Excel, le système 1904 et les formats de nombres
Comment HotXLS choisit-il jours, mois ou années automatiquement ?
TXLSChartDateAxisTransform.DetectUnit choisit l'unité de calendrier la plus fine que les écarts dans les données réclament réellement. Il jette les valeurs NaN et infinies, trie une copie du reste une fois, et saute les dates dupliquées. Si un écart adjacent quelconque est plus court qu'un mois de calendrier, l'unité est les jours ; sinon, si un écart quelconque est plus court qu'une année de calendrier, les mois ; sinon les années. Le test de mois est IncMonth(Previous, 1) > Current, et ce choix compte pour exactement les données de cet article. Un seuil de 30 jours appellerait l'écart du 31 janvier au 28 février « moins d'un mois » et basculerait une série mensuelle en jours. Avec moins de deux dates valides distinctes il n'y a pas d'écart à mesurer, et la méthode rend les jours
ResolveUnits fusionne l'unité détectée avec ce que le graphique déclare. Une unité de base explicite gagne. Une unité majeure ou mineure manquante hérite de l'unité détectée quand celle-ci est plus grossière que la base, et sinon retombe sur la base. La dernière règle est celle que le monde rate : si l'unité majeure ou mineure effective est plus fine que la base, la base descend pour s'accorder. L'unité de base décide comment les points se groupent avant qu'aucune graduation n'existe, car Normalize accroche une valeur à son jour, au premier de son mois ou au 1er janvier. Une base mensuelle sous une unité majeure quotidienne replierait un mois entier de points dans une case avant que le code de graduations ne les voie jamais
Combien de graduations majeures automatiques apparaissent dépend de plus que la plage de dates. La longueur du tracé, la largeur mesurée de la police de l'axe, le format de nombre des étiquettes et la largeur projetée des étiquettes pivotées plafonnent tous le compte, donc un graphique étroit avec de longues étiquettes mmmm yyyy obtient moins de graduations qu'un large avec mm/yy. Étiquettes, marques de graduations et quadrillages se tracent tous depuis un seul tableau de graduations, ce qui garde les sorties HTML, SVG et PDF en accord, et une graduation mineure qui tombe sur une majeure se trace une fois
Que range l'élément dateAx, et que signifie l'omission ?
Dans ChartML chaque réglage de calendrier sur un axe de dates est optionnel, et en omettre un dit quelque chose de différent que d'écrire son défaut. Le type CT_DateAx d'ECMA-376 Part 1, §21.2 (DrawingML Charts) laisse baseTimeUnit, majorTimeUnit, minorTimeUnit et auto apparaître chacun ou non. Un baseTimeUnit omis dit à Excel de décider au rendu ; un days explicite l'épingle. TXLSXChartAxis garde chaque valeur à côté d'un drapeau de présence : BaseTimeUnit avec BaseTimeUnitSet, MajorTimeUnit avec MajorTimeUnitSet, MinorTimeUnit avec MinorTimeUnitSet, et AutoDateAxis avec AutoDateAxisSet. Assigner une valeur pose son drapeau, poser le drapeau à False rend l'élément à l'omission, et le rédacteur XLSX n'émet un élément que quand son drapeau est allumé. auto se lit mal aisément : il contrôle la détection automatique catégories-contre-dates et n'a rien à voir avec les unités de graduations automatiques
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; // écrit c:dateAx
Axis.BaseTimeUnit := xlsxChartTimeDays; // pose aussi BaseTimeUnitSet
Axis.MajorTimeUnit := xlsxChartTimeMonths;
Axis.MajorUnit := 1; // une valeur > 0 marque majorUnit comme posé
Axis.NumberFormat := 'mmm yyyy';
Axis.NumberFormatSourceLinked := False;
Axis.TextStyle.Rotation := -45; // degrés
// AutoDateAxis laissé tranquille : aucun élément c:auto n'est écrit
Book.SaveAs('month-end-balance.xlsx');
finally
Book.Free;
end;
end;
L'unité de base ici est les jours exprès. Les dates de fin de mois tombent sur des numéros de jours différents, et une base mensuelle accrocherait chaque point au premier de son mois, ce qui déplace le solde du 31 mars au 1er mars. Épinglez la base aux jours chaque fois que le jour exact porte du sens, et laissez l'unité majeure faire l'espacement mensuel
Étiquettes et orientation suivent les mêmes règles de présence. L'élément numFmt de l'axe met sourceLinked à vrai par défaut, donc TXLSXChartAxis garde séparés le code de format, la valeur liée à la source et NumberFormatSet, et un code vide n'est jamais pris pour un élément manquant. ChartML range la rotation du texte en 1/60000 de degré avec les valeurs positives tournant horaire ; TextStyle.Rotation prend des degrés simples, et une étiquette à -45 penche de la même façon en sorties SVG et PDF. Un axe inversé (maxMin) reflète ensemble graduations majeures et mineures, quadrillages, points de données, courbes de tendance et barres d'erreur. La lecture marche dans l'autre sens : ouvrez un classeur écrit ailleurs et les drapeaux vous disent ce que son 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[1].Charts[0].CategoryAxis; // Sheets[] est à base 1, Charts[] à 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; // sauvegardez-le encore et les éléments omis restent omis
end;
end;
Unités de calendrier dans les fichiers ODS et XLS historiques
Le modèle de calendrier va aussi loin que chaque format de fichier peut l'exprimer. ODF 1.2 décrit l'espacement d'axe avec chart:interval-major, un nombre simple sans notion de jours, mois ou années, donc un axe mensuel Excel sauvegardé en ODS ne peut pas porter son unité. HotXLS laisse l'unité dehors du fichier ODS plutôt que d'inventer un attribut non standard que les autres applications ODF ignoreraient de toute façon. Si l'espacement mensuel compte pour les gens qui ouvrent le fichier, livrez du XLSX
Les classeurs BIFF8 gardent les réglages d'axe de dates dans l'enregistrement AxcExt, neuf mots 16 bits fixes. Ses drapeaux automatiques masquent des champs sans les retirer, donc un enregistrement marqué « minimum automatique » porte quand même une valeur de minimum. HotXLS garde les valeurs masquées de minimum, maximum, intervalle, unité et croisement à la lecture et les réécrit telles quelles, garde les codes d'unités inconnus intacts dans le modèle objet XLS, et ne mappe que les unités de dates valides 0, 1 et 2 (jours, mois, années) quand un graphique se convertit vers XLSX
Un axe de dates voyage rarement seul. Quand un classeur Excel apparie une ligne à 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 explique comment HotXLS rejoue le XML de graphique d'origine quand rien n'a changé et fusionne des éditions typées dedans quand quelque chose a changé, et notre vue d'ensemble des graphiques, images et drawings couvre les appels d'ancrage et de séries employés ci-dessus. La page du composant Delphi HotXLS liste le jeu complet de fonctionnalités de graphiques pour Delphi et C++Builder, y compris la lecture, l'écriture et le rendu de graphiques XLS et XLSX sans Excel installé