HotXLS écrit les graphiques Waterfall et Treemap Excel comme des parties cx:chartSpace autonomes — xl/charts/chartExN.xml portant le type de contenu application/vnd.ms-office.chartex+xml — et les ancre via un wrapper mc:AlternateContent pour qu'Excel 2016 et ultérieur rende le graphique pendant que les lecteurs plus anciens reçoivent une forme de repli. Choisissez xlsxChartWaterfall ou xlsxChartTreemap et tout le pipeline de parties suit
La façon dont ce problème s'annonce habituellement, c'est un ticket de support. Un client envoie un classeur que son équipe finance a construit dans Excel 2019, votre outil de reporting l'ouvre, l'enregistre, et le graphique Waterfall a disparu. Pas d'exception, pas d'avertissement, rien dans le log — le graphique n'est simplement pas dans la sortie. Si vous partez le chercher dans le chemin de code des graphiques classiques, vous chercherez longtemps, parce que le graphique n'y a jamais été
Pourquoi les graphiques Waterfall et Treemap ne sont-ils pas dans c:chartSpace ?
Parce que Microsoft ne pouvait pas les y mettre. Le schéma de graphiques DrawingML d'ECMA-376 Partie 1 §21.2 énumère ses éléments de graphique — c:barChart, c:lineChart, c:pieChart et les autres — comme un choix fermé, donc ajouter waterfall, treemap, sunburst et funnel dans Excel 2016 aurait signifié casser un schéma dont dépend chaque lecteur existant. Les nouveaux types sont allés dans un document parallèle à la place : une partie cx:chartSpace dans l'espace de noms chartex de 2014, http://schemas.microsoft.com/office/drawing/2014/chartex, stockée à côté des graphiques classiques mais structurellement sans lien avec eux. Le côté dessin relie les deux mondes avec mc:AlternateContent, le mécanisme de compatibilité d'ECMA-376 Partie 3 : la branche mc:Choice déclare Requires="cx1" et contient un xdr:graphicFrame dont le a:graphicData/@uri est l'espace de noms chartex et dont le contenu est une référence <cx:chart r:id="rIdN"/> nue, tandis que mc:Fallback contient un rectangle avec l'avis qu'Excel lui-même y écrit. Un lecteur qui ne comprend pas cx1 est tenu par le contrat de la Partie 3 de prendre le repli
Le type de relation est l'autre indice. Un graphique classique pend de la partie dessin avec la relation de graphique OOXML standard ; une partie chartEx utilise http://schemas.microsoft.com/office/2014/relationships/chartEx à la place, documentée dans [MS-ODRAWXML]. HotXLS expose les deux mondes comme constantes — XlsxRtChartEx pour cette relation, XlsxCtChartEx pour le type de contenu, XlsxNsMsChartEx et XlsxNsMsChartEx2015 pour les deux espaces de noms dont l'ancre a besoin — et le prédicat XlsxChartIsChartEx est ce qui achemine un graphique vers le chemin étendu à la sauvegarde. Les graphiques combinés et le XML de graphique classique préservé passent par l'autre, décrit dans l'article sur le XML de graphique préservé et les graphiques combinés
Quatre règles de schéma qui décident si Excel ouvre le fichier
Le schéma chartex est plus strict que le schéma classique à quatre endroits précis, et chacun d'eux produit le même invite de réparation inutile quand vous vous trompez. BuildChartExKnownXml et BuildChartExRelsXml dans HotXLS intègrent les quatre, mais elles valent la peine d'être connues si vous inspectez le XML à la main un jour
- Ordre. Le
cx:chartDatadoit venir avant lecx:chartdans le document. C'est l'inverse de l'intuition que vous construisez avec les graphiques classiques, où les caches vivent dans les séries dans la zone de traçage ; dans une partie chartEx, les données sont en avant dans leur propre bloc et le corps du graphique n'y fait que référence par id - Cardinalité. Le
cx:plotAreapeut contenir exactement uncx:plotAreaRegion. Ni zéro, ni deux. Pas d'histoire de graphique combiné ici — une région, et les séries dedans - Emplacement du type. Le type de graphique n'est pas du tout un nom d'élément. C'est l'attribut
layoutIdsurcx:series, prenant un jeton de l'énumérationST_SeriesLayoutdans le schéma chartex de Microsoft.XlsxChartExLayoutIdprojette l'énumération HotXLS sur ce jeton et retourne'waterfall'ou'treemap';XlsxChartExLayoutIdToTypeexécute la projection en sens inverse à la lecture - La relation dont les graphiques classiques n'ont jamais besoin. Le
cx:chartDatacontient un élément<cx:externalData r:id="rId1"/>, et cette relation doit exister dansxl/charts/_rels/chartExN.xml.relsen pointant vers la feuille de calcul propriétaire des données, parce qu'Excel l'utilise pour résoudre les formules de plagescx:f. Omettez la partie rels et le graphique s'ouvre vide sans rien qui explique pourquoi
Comment ajouter un graphique Waterfall ou Treemap en Delphi ?
De la même façon que vous ajoutez n'importe quel autre graphique dans HotXLS — les types étendus sont membres de TXLSXChartType, donc TXLSXWorksheet.AddChart les accepte directement et tout ce qui suit se branche sur la valeur de l'énumération. Il n'y a pas d'API chartEx séparée à apprendre
var
Book: TXLSXWorkbook;
Sheet: TXLSXWorksheet;
Chart: TXLSXChart;
begin
Book := TXLSXWorkbook.Create;
try
Book.Open('cashflow.xlsx');
Sheet := Book.Sheets[0];
// Rectangle d'ancrage en coordonnées ligne/colonne, comme un graphique classique
Chart := Sheet.AddChart(xlsxChartWaterfall, 'Q3 Cash Flow',
2, 5, 20, 12);
Chart.AddSeries('Movement', 'Sheet1!$A$2:$A$9', 'Sheet1!$B$2:$B$9');
Book.SaveAs('cashflow-waterfall.xlsx');
finally
Book.Free;
end;
end;
À la sauvegarde, HotXLS voit XlsxChartIsChartEx retourner True, alloue un nom de partie depuis un compteur chartEx séparé, et écrit xl/charts/chartEx1.xml, son fichier rels, l'override du type de contenu, et l'ancre mc:AlternateContent produite par XlsxChartExAnchorPayloadXml. Le compteur indépendant compte : un classeur avec deux graphiques classiques et un Waterfall obtient chart1.xml, chart2.xml et chartEx1.xml, pas chartEx3.xml. Les Treemap diffèrent ensuite des Waterfall sur deux détails, tous deux gérés par le même builder. La dimension numérique change de type — cx:numDim type="val" pour un Waterfall devient type="size" pour un Treemap, parce qu'un rectangle de treemap encode la magnitude comme surface plutôt que comme valeur tracée — et les Treemap veulent d'habitude une hiérarchie de catégories, que le modèle exprime via TXLSXChartSeries.CategoriesMultiLevel. Posez ce drapeau et S.ResolveCategoryLevels lit la plage dans la feuille de calcul au moment de l'écriture, la découpe en un TXLSXChartDataCache par niveau derrière S.CategoryLevelCount et S.CategoryLevels[], et BuildChartExLevelXml émet un cx:lvl par niveau dans un seul cx:strDim, niveau feuille en premier — le même ordre physique qu'utilise ECMA-376 Partie 1 §21.2 pour c:multiLvlStrCache, donc cette convention survit même si aucun des balisages ne survit
Chart := Sheet.AddChart(xlsxChartTreemap, 'Revenue by Region',
2, 8, 24, 16);
S := Chart.AddSeries('Revenue',
'Sheet1!$A$2:$B$40', // deux colonnes : libellé de feuille, puis groupe parent
'Sheet1!$C$2:$C$40');
S.CategoriesMultiLevel := True;
La forme de cache qui casse toutes les hypothèses des graphiques classiques
Si vous avez écrit un lecteur de cache de graphique classique, trois habitudes vous trahiront sur une partie chartEx, et les trois viennent d'une seule décision de conception : chartex stocke dans des attributs et du contenu d'élément ce que DrawingML stocke dans des éléments enfants
- Un point en cache est
<cx:pt idx="0">42</cx:pt>. La valeur est le contenu de l'élément. Il n'y a pas d'enfantc:vdans lequel descendre, donc un parser qui marche en cherchant des nœudsvne trouve rien et rapporte un cache vide ptCountetformatCodesont des attributs surcx:lvl. Dans unc:numCacheclassique, les deux sont des éléments enfants- Plusieurs niveaux de hiérarchie, ce sont plusieurs éléments
cx:lvldans un seulcx:strDim. Il n'y a pas d'élément de cache multi-niveaux séparé commec:multiLvlStrCachel'est dec:strCache
BuildChartExLevelXml écrit cette forme et ParseChartExXml la relit, en faisant passer chaque valeur par XlsxEscapeText à la sortie pour qu'un libellé de catégorie contenant une esperluette ou un chevron survive à l'aller-retour. À noter ce que fait le builder quand un niveau numérique ne porte pas de code de format : il écrit formatCode="General" plutôt que de laisser tomber l'attribut, parce qu'ici l'attribut n'est pas optionnel comme l'est son homologue classique
Le préfixe d'espace de noms qui faisait échouer la relecture du type
La première version du parser chartEx de HotXLS écrivait des fichiers parfaitement bons et relisait chacun d'eux comme un graphique en colonnes. La cause racine tenait en une ligne, et la leçon se généralise à n'importe quel parser OOXML que vous écrirez : TXMLReader.Name retourne le nom tel qu'écrit dans le document, préfixe compris. Dans une partie de graphique classique, la plupart des éléments arrivent sans préfixe, donc SameText(Reader.Name, 'series') marche et vous cessez d'y penser. Dans une partie chartEx, chaque élément est préfixé — le lecteur vous tend 'cx:series', la comparaison échoue silencieusement sur tous, et vous n'avez ni exception ni XML malformé, juste une liste de séries vide et un type de graphique qui retombe sur le défaut. Le correctif consiste à comparer sur le nom local, à un seul endroit plutôt qu'à chaque site d'appel
// Reader.Name porte son préfixe d'espace de noms : 'cx:series', pas 'series'
function IsEl(const LocalName: string): Boolean;
var
NodeName: string;
begin
NodeName := String(Reader.Name);
Result := SameText(NodeName, LocalName) or
EndsText(':' + LocalName, NodeName);
end;
ParseChartExXml utilise cette aide pour chaque test d'élément et une aide équivalente AttrVal pour les attributs, puisque cx:autoUpdate et compagnie sont préfixés aussi. Une fois les éléments de séries réellement vus, le type revient du premier layoutId rencontré, et chaque cx:series est câblée à son bloc cx:data en faisant correspondre cx:dataId/@val avec cx:data/@id. Les graphiques ouverts mais jamais modifiés prennent l'ancien chemin à la place : les octets d'origine rejouent depuis FRawChartXml, donc un fichier client intact ressort comme il est entré — la même discipline d'écrivain conservateur qui régit les extensions au niveau de la feuille dans l'article sur les extensions sparkline
Ce que HotXLS ne fait délibérément pas avec ces graphiques
Deux limites valent la peine d'être dites clairement, parce que les deux sont des choix plutôt que des trous en attente d'être comblés. Les rendus PDF et SVG intégrés ne dessinent pas les séries Waterfall ou Treemap : les types ne correspondent à aucune branche de dessin, donc le rendu des séries est sauté et le reste de la feuille s'exporte normalement — simuler un treemap crédible dans un PDF exporté serait pire que ne rien dessiner, puisque cela serait en désaccord silencieux avec ce qu'Excel montre pour le même fichier. Et xlsxChartWaterfall et xlsxChartTreemap sont les deux seules mises en page étendues que HotXLS construit depuis le modèle ; ST_SeriesLayout définit aussi sunburst, funnel, box-and-whisker et histogramme, et un classeur contenant n'importe lequel d'entre eux fait toujours l'aller-retour par le chemin de rejeu du XML brut, vous ne pouvez simplement pas en créer un depuis du code Delphi aujourd'hui. Le rendu des types de graphiques classiques est inchangé et couvert dans l'article sur les graphiques, les images et les dessins, et le pipeline chartEx est livré à côté dans le HotXLS Delphi Excel Component pour Delphi et C++Builder