Article technique

Graphiques Waterfall et Treemap Excel en Delphi avec HotXLS

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é

L'ancre mc:AlternateContent que HotXLS écrit pour un graphique chartEx : la branche mc:Choice exige cx1 et contient un xdr:graphicFrame dont le a:graphicData référence l'espace de noms chartex avec une relation cx:chart nue, tandis que mc:Fallback porte un rectangle d'avis que les lecteurs plus anciens dessinent à la place
L'ancre mc:AlternateContent achemine un graphique chartEx entre Excel 2016 et les lecteurs plus anciens : la branche Choice exige cx1 et porte le cadre chartex, tandis que le Fallback contient le rectangle d'avis que les lecteurs plus anciens doivent dessiner

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:chartData doit venir avant le cx:chart dans 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:plotArea peut contenir exactement un cx: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 layoutId sur cx:series, prenant un jeton de l'énumération ST_SeriesLayout dans le schéma chartex de Microsoft. XlsxChartExLayoutId projette l'énumération HotXLS sur ce jeton et retourne 'waterfall' ou 'treemap' ; XlsxChartExLayoutIdToType exécute la projection en sens inverse à la lecture
  • La relation dont les graphiques classiques n'ont jamais besoin. Le cx:chartData contient un élément <cx:externalData r:id="rId1"/>, et cette relation doit exister dans xl/charts/_rels/chartExN.xml.rels en pointant vers la feuille de calcul propriétaire des données, parce qu'Excel l'utilise pour résoudre les formules de plages cx: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;
Le pipeline de sauvegarde de HotXLS pour les graphiques Excel étendus : AddChart avec xlsxChartWaterfall ou xlsxChartTreemap atteint XlsxChartIsChartEx, qui achemine les graphiques classiques vers un compteur partagé et les graphiques chartEx vers un compteur indépendant qui écrit chartEx1.xml, son fichier rels pointant vers la feuille de calcul, l'override du type de contenu chartex et le contenu de l'ancre AlternateContent
Un seul modèle de graphique et un seul prédicat d'acheminement alimentent deux compteurs à la sauvegarde : les graphiques classiques partagent chart1.xml et chart2.xml, tandis que le Waterfall atterrit en chartEx1.xml parce que le compteur chartEx est indépendant de celui des classiques

À 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'enfant c:v dans lequel descendre, donc un parser qui marche en cherchant des nœuds v ne trouve rien et rapporte un cache vide
  • ptCount et formatCode sont des attributs sur cx:lvl. Dans un c:numCache classique, les deux sont des éléments enfants
  • Plusieurs niveaux de hiérarchie, ce sont plusieurs éléments cx:lvl dans un seul cx:strDim. Il n'y a pas d'élément de cache multi-niveaux séparé comme c:multiLvlStrCache l'est de c: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

Formes de cache côte à côte dans les parties chartEx de HotXLS : un c:numCache classique stocke ptCount et formatCode comme éléments enfants avec les valeurs dans c:v, tandis que cx:numDim les stocke comme attributs sur cx:lvl avec la valeur comme contenu d'élément, et CategoriesMultiLevel émet un cx:lvl par niveau de catégorie, feuille en premier, dans un seul cx:strDim
Chartex stocke dans des attributs et du contenu d'élément ce que DrawingML stocke dans des éléments enfants : la valeur siège directement dans cx:pt, ptCount et formatCode deviennent des attributs sur cx:lvl, et une hiérarchie de catégories est plusieurs éléments cx:lvl dans un seul cx:strDim

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