HotXLS écrit les groupes d’axes secondaires des graphiques BIFF8 en émettant un second bloc AxisParent, et non en ajoutant un second groupe de graphique après les axes. Dans le sous-flux de graphique XLS classique, chaque groupe de graphique — ChartFormat, l’enregistrement de type de graphique et CrtLink — vit dans son propre bloc de groupe d’axes, et chaque série s’y rattache par SerToCrt. Inversez cet emboîtement et il n’existe aucun second groupe de graphique auquel une série puisse se rattacher, quel que soit le nombre d’enregistrements d’axes émis
Pourquoi un second groupe de graphique après les axes ne rattache-t-il rien ?
La grammaire est toute la réponse, et elle tient en une ligne d’ABNF. La règle CHARTFOMATS de [MS-XLS] 2.1.7.20.1 indique AxesUsed 1*2AXISPARENT, puis explicite AXISPARENT = AxisParent Begin Pos [AXES] 1*4CRT End. Lisez ces deux productions ensemble et la forme apparaît : les groupes de graphique sont des enfants d’un groupe d’axes, pas ses frères. Un sous-flux de graphique avec deux paires d’axes et un groupe de graphique final n’est pas un graphique à deux axes avec une bizarrerie de mise en page ; c’est un graphique avec un seul groupe de graphique et un ensemble d’enregistrements d’axes orphelins. Cela compte parce que SerToCrt ($1045), placé dans le bloc SERIESFORMAT, porte un index de groupe de graphique commençant à zéro, pas un index d’axe. Écrire crt = 1 alors qu’un seul bloc CRT existe dirige une série vers un groupe de graphique qui n’a jamais été émis. L’intuition qui piège tout le monde vient du nom de l’enregistrement : AXESUSED ($1046) semble compter les axes, donc le réflexe naturel est d’en émettre davantage. Il compte les groupes d’axes, et chaque groupe d’axes entraîne avec lui une zone de tracé complète et un groupe de graphique
Marquer une série pour le groupe d’axes secondaire
Du côté HotXLS, cela se résume à un booléen. TXLSChartSeriesInfo porte un champ SecondaryAxis, et le définir sur une série quelconque du tableau remis à TXLSWorksheets.AddChartSheet fait passer tout le générateur en mode à deux groupes. Il n’existe ni appel distinct « activer l’axe secondaire » ni paramètre de nombre d’axes, car ce nombre se déduit : si une série quelconque veut le groupe secondaire, le graphique en nécessite deux
var
Wb: TXLSWorkbook;
Series: array [0..1] of TXLSChartSeriesInfo;
begin
Wb := TXLSWorkbook.Create;
try
Wb.Sheets.Add.Name := 'Data';
// ... remplir A1:C12 avec les catégories, le chiffre d’affaires et la marge ...
Series[0] := Default(TXLSChartSeriesInfo); // ne jamais appliquer FillChar à cet enregistrement
Series[0].Name := 'Revenue';
Series[0].Categories := 'Data!$A$1:$A$12';
Series[0].Values := 'Data!$B$1:$B$12';
Series[1] := Default(TXLSChartSeriesInfo);
Series[1].Name := 'Margin';
Series[1].Categories := 'Data!$A$1:$A$12';
Series[1].Values := 'Data!$C$1:$C$12';
Series[1].SecondaryAxis := True; // AXESUSED passe à 2
Wb.Sheets.AddChartSheet('Dual Axis', xlsChartTypeLine,
'Revenue vs Margin', '', '', Series);
Wb.SaveAs('dual-axis.xls', xlExcel97);
finally
Wb.Free;
end;
end;
La ligne Default(TXLSChartSeriesInfo) n’est pas décorative. TXLSChartSeriesInfo mélange des champs gérés (les noms WideString, les tableaux dynamiques de courbe de tendance et de barres d’erreur) avec des membres Boolean simples, et Delphi ne garantit la remise à zéro que des champs gérés. Laisser SecondaryAxis non initialisé signifie qu’il prend la valeur présente sur la pile ; en pratique, le même binaire produit un graphique à un axe depuis un hôte console et un graphique à deux axes sous le test runner. Les plages de catégories et de valeurs, quant à elles, sont résolues via la table EXTERNSHEET du classeur avant même que le générateur ne les voie — le même mécanisme d’index que celui décrit dans la classification des liens externes BIFF SupBook et XTI par HotXLS — de sorte qu’une plage nommant une feuille inconnue se dégrade en placeholder BRAI vide plutôt que de faire échouer le build
Ce que HotXLS émet lorsqu’une série est secondaire
L’émetteur change de forme, pas seulement de valeur. Sans série secondaire, AXESUSED vaut 1 (ou 0 pour les graphiques en secteurs et les graphiques en secteurs 3D, qui n’ont aucun groupe d’axes) et un bloc AxisParent suit. Avec une série secondaire, AXESUSED vaut 2 et le générateur exécute le bloc deux fois, avec iax — le premier mot de la charge utile de 18 octets de AxisParent ($1041) — défini successivement à 0 puis à 1. Chaque passe émet Pos, l’axe des catégories et l’axe des valeurs (Axis, $101D), le marqueur PlotArea ($1035), un Frame par défaut, puis ChartFormat ($1014), l’enregistrement de type de graphique, CrtLink ($1022) et deux marqueurs End pour fermer le groupe de graphique et le groupe d’axes. La série secondaire se rattache alors avec SerToCrt crt = 1 et la série primaire conserve crt = 0. Un enregistrement n’est volontairement pas dupliqué : la légende n’est émise que dans le premier groupe, car Excel donne un seul légende à un graphique, quel que soit le nombre de groupes d’axes qu’il porte. Deux autres propriétés méritent d’être énoncées clairement. Restructurer l’émetteur pour placer le groupe de graphique dans le bloc parent d’axes n’a pas changé la sortie des graphiques ordinaires — sans série secondaire, le sous-flux est identique octet par octet à la version précédente, puisque paramétrer AddAxisParent avec iax = 0 est exactement l’ancien chemin. Le générateur émet toujours une paire complète d’axes par groupe, si bien qu’un groupe secondaire arrive avec son propre axe de catégories même si vous ne vous intéressez qu’à son échelle de valeurs
Comment l’inspection d’un graphique retrouve-t-elle l’association au groupe d’axes ?
La lecture se fait en deux passes sur la liste d’enregistrements, et c’est nécessaire parce que AXESUSED arrive avant les blocs qu’il décrit. La première passe ne cherche que $1046 et lit son premier mot comme nombre de groupes d’axes. Cette valeur commence à 1 et ne peut qu’augmenter : HotXLS prend le maximum entre le nombre courant et la valeur déclarée, de sorte qu’un AXESUSED malformé ou dupliqué ne peut pas faire régresser un graphique déjà observé comme déclarant deux groupes. La seconde passe suit le groupe d’axes courant, le met à jour à chaque AxisParent et appose cet index sur chaque enregistrement Axis rencontré jusqu’à l’apparition du AxisParent suivant
var
Model: TXLSChartModel;
i: Integer;
begin
// Sheets[1] est la feuille de données, Sheets[2] la feuille de graphique
Model := Wb.Sheets[2]._Chart.GetChartModel;
try
if Model.AxisGroupCount = 2 then
Writeln('AXESUSED declares a secondary axis group');
for i := 0 to Model.AxisCount - 1 do
Writeln('axis ', i, ' group ', Model.GetAxis(i).AxisGroup);
for i := 0 to Model.SeriesCount - 1 do
Writeln('series ', i, ' chart group ', Model.GetSeries(i).ChartGroup);
finally
Model.Free;
end;
end;
Deux limites méritent d’être nommées. TXLSChartModel.AxisGroupCount indique ce que le fichier déclare, pas le nombre de blocs AxisParent réellement trouvés ; un fichier qui annonce 2 mais n’en fournit qu’un renverra 2, et c’est AxisCount qui vous le fera remarquer. Ensuite, TXLSChartAxis.AxisGroup est une marque positionnelle : elle indique dans quel bloc l’axe a été lu, seule information que le format fournisse. Du côté des séries, le décodage de SerToCrt est conditionné par la présence dans un bloc Series, car le même identifiant d’enregistrement apparaît dans des contextes qui ne sont pas des associations de séries et un décodeur non conditionné écraserait volontiers la mauvaise série
Vérifier les axes secondaires sans vrai fichier Excel
La vérification n’a nécessité aucun fichier Excel avec axe secondaire, ce qui est la partie utile de l’histoire. Le décodage structurel est une propriété de la séquence d’enregistrements ; une séquence synthétisée la prouve donc aussi exactement qu’une séquence capturée. La régression construit AXESUSED avec une charge utile de 2, puis deux blocs AxisParent contenant chacun un axe de catégories et un axe de valeurs, et vérifie que le modèle revient avec AxisGroupCount = 2, quatre axes marqués 0, 0, 1, 1 et les types d’axes attendus pour la seconde paire
// Vérification structurelle sans aucun fichier Excel
Chart := TXLSCustomChart.Create(nil, $0600);
try
AddWordRecord($1046, [2]); // AXESUSED : deux groupes d’axes
AddAxisParentGroup(0); // AxisParent iax=0 + Begin + 2 Axis + End
AddAxisParentGroup(1); // AxisParent iax=1 + Begin + 2 Axis + End
Model := Chart.GetChartModel;
Assert.AreEqual(2, Model.AxisGroupCount);
Assert.AreEqual(4, Model.AxisCount);
Assert.AreEqual(0, Model.GetAxis(1).AxisGroup);
Assert.AreEqual(1, Model.GetAxis(2).AxisGroup);
finally
Model.Free;
Chart.Free;
end;
Deux notes pratiques si vous synthétisez vous-même les enregistrements. TXLSCustomChart.AddData(RecID, Len, nil) déréférence la charge utile lorsque Len est non nul ; les marqueurs Begin ($1033) et End ($1034) doivent donc être ajoutés avec une longueur nulle, et non avec un blob nil et une longueur obsolète. Une séquence synthétique prouve le décodeur, jamais l’acceptation de votre sortie par Excel — le chemin d’écriture a été structuré à partir de l’ABNF puis contrôlé par round-trip via GetChartModel, en vérifiant les marques par axe 0/0/1/1 et les groupes de graphique par série 0/1, avec le chemin sans secondaire identique octet par octet comme filet de sécurité. C’est la même posture prudente que celle sur laquelle repose le reste de la prise en charge des graphiques, images et dessins HotXLS pour Delphi : décoder ce que disent les enregistrements et refuser de deviner une disposition binaire que vous n’avez pas lue dans la spécification. La suite Delphi complète a exécuté 1650 tests sur 1650 sous Win32 et Win64 après l’arrivée du chemin d’écriture
Paramètres de scène Chart3d et piège de fAuto
Deux détails voisins piègent ceux qui dépassent le graphique par défaut. Le premier est Chart3d ($103A, [MS-XLS] 2.4.46), une charge utile plate de 14 octets émise dans le groupe de graphique pour les variantes 3D : anRot (rotation, de 0 à 360), anElev (élévation, signée, de -90 à 90), pcDist (distance de perspective, de 0 à 100, ignorée sauf si fPerspective est défini), pcHeight et pcDepth (pourcentage de la largeur du graphique, de 5 à 500), pcGap (de 0 à 500) et un grbit dont les bits sont fPerspective $0001, fCluster $0002, fAutoscale $0004, f3DScaling $0010 et f2DWalls $0020. La spécification ajoute des contraintes que la disposition de l’enregistrement ne fera pas respecter pour vous : sur un graphique à barres transposé, anRot et anElev ne doivent pas dépasser 44, et sur un graphique en secteurs anElev ne doit pas être négatif
Le second est le bit fAuto, celui qui produit le ticket « mes couleurs ont été ignorées ». LineFormat ($1007), AreaFormat ($100A) et MarkerFormat ($1009) portent tous fAuto dans le bit 0 de leur grbit ; lorsque ce bit est défini, Excel applique son style automatique et traite les valeurs RGB explicites, le style de ligne, la forme du marqueur et sa taille placés juste à côté comme de la décoration. Tout émetteur qui écrit un style de série personnalisé doit effacer le bit 0 ; les émetteurs par défaut le laissent activé précisément pour qu’Excel choisisse la palette. Si vous modifiez un classeur existant plutôt que d’en construire un, les règles de préservation diffèrent encore et sont décrites dans la modification de graphiques Excel sans perdre le ChartML préservé
Les groupes d’axes secondaires, l’association SerToCrt et le modèle de graphique typé présentés ici sont livrés dans le composant tableur HotXLS pour Delphi pour Delphi et C++Builder, qui lit et écrit les graphiques BIFF8 sans Excel installé ; la page produit contient la référence complète des enregistrements de graphiques et la liste des surcharges de AddChartSheet