HotXLS escribe los gráficos waterfall y treemap de Excel como partes cx:chartSpace independientes — xl/charts/chartExN.xml con el content type application/vnd.ms-office.chartex+xml — y los ancla a través de un wrapper mc:AlternateContent para que Excel 2016 y posteriores rendericen el gráfico mientras los lectores antiguos reciben una forma de respaldo. Elige xlsxChartWaterfall o xlsxChartTreemap y toda la pipeline de partes sigue sola
La forma en que este problema suele anunciarse es un ticket de soporte. Un cliente envía un workbook que su equipo de finanzas montó en Excel 2019, tu herramienta de informes lo abre, lo guarda, y el gráfico waterfall ha desaparecido. Sin excepción, sin aviso, nada en el log — el gráfico simplemente no está en la salida. Si vas a buscarlo por el code path clásico de gráficos, buscarás mucho rato, porque el gráfico nunca estuvo ahí desde el principio
¿Por qué los gráficos waterfall y treemap no están en c:chartSpace?
Porque Microsoft no podía meterlos ahí. El esquema de gráficos DrawingML de ECMA-376 Parte 1 §21.2 enumera sus elementos de gráfico — c:barChart, c:lineChart, c:pieChart y el resto — como una choice cerrada, así que añadir waterfall, treemap, sunburst y funnel en Excel 2016 habría significado romper un esquema del que depende todo lector existente. Los tipos nuevos fueron a un documento paralelo: una parte cx:chartSpace en el namespace chartex de 2014, http://schemas.microsoft.com/office/drawing/2014/chartex, guardada junto a los gráficos clásicos pero estructuralmente ajena a ellos. El lado del dibujo cose los dos mundos con mc:AlternateContent, el mecanismo de compatibilidad de la Parte 3 de ECMA-376: la rama mc:Choice declara Requires="cx1" y contiene un xdr:graphicFrame cuyo a:graphicData/@uri es el namespace chartex y cuyo payload es una referencia desnuda <cx:chart r:id="rIdN"/>, mientras que mc:Fallback contiene un rectángulo con el aviso que el propio Excel escribe ahí. Un lector que no entienda cx1 está obligado por el contrato de la Parte 3 a tomar el fallback
El tipo de relación es la otra pista. Un gráfico clásico cuelga de la parte de dibujo con la relación OOXML estándar de gráficos; una parte chartEx usa http://schemas.microsoft.com/office/2014/relationships/chartEx en su lugar, documentada en [MS-ODRAWXML]. HotXLS expone los dos mundos como constantes — XlsxRtChartEx para esa relación, XlsxCtChartEx para el content type, XlsxNsMsChartEx y XlsxNsMsChartEx2015 para los dos namespaces que necesita el ancla — y el predicado XlsxChartIsChartEx es lo que encamina un gráfico por el path extendido al guardar. Los combination charts y el XML clásico de gráficos preservado van por el otro, descrito en el artículo sobre XML de gráficos preservado y combination charts
Cuatro reglas del esquema que deciden si Excel abre el archivo
El esquema chartex es más estricto que el clásico en cuatro puntos concretos, y todos producen el mismo aviso de reparación poco útil cuando te equivocas. BuildChartExKnownXml y BuildChartExRelsXml en HotXLS ya incorporan las cuatro, pero merece la pena conocerlas si alguna vez inspeccionas el XML a mano
- Orden.
cx:chartDatadebe ir antes quecx:charten el documento. Es lo contrario de la intuición que construyes con los gráficos clásicos, donde los caches viven dentro de la serie dentro del plot area; en una parte chartEx los datos van delante en su propio bloque y el cuerpo del gráfico solo los referencia por id - Cardinalidad.
cx:plotAreapuede contener exactamente uncx:plotAreaRegion. Ni cero ni dos. Aquí no hay historia de combination chart — una región, y las series dentro - Ubicación del tipo. El tipo de gráfico no es en absoluto un nombre de elemento. Es el atributo
layoutIdencx:series, que toma un token de la enumeraciónST_SeriesLayoutdel esquema chartex de Microsoft.XlsxChartExLayoutIdmapea el enum de HotXLS a ese token y devuelve'waterfall'o'treemap';XlsxChartExLayoutIdToTypecorre el mapeo hacia atrás al leer - La relación que los gráficos clásicos nunca necesitan.
cx:chartDatacontiene un elemento<cx:externalData r:id="rId1"/>, y esa relación tiene que existir enxl/charts/_rels/chartExN.xml.relsapuntando de vuelta a la worksheet que posee los datos, porque Excel la usa para resolver las fórmulas de rangocx:f. Omite la parte rels y el gráfico se abre vacío sin nada que explique por qué
¿Cómo añades un gráfico waterfall o treemap en Delphi?
Igual que añades cualquier otro gráfico en HotXLS — los tipos extendidos son miembros de TXLSXChartType, así que TXLSXWorksheet.AddChart los acepta directamente y todo lo que viene después depende del valor del enum. No hay una API chartEx aparte que aprender
var
Book: TXLSXWorkbook;
Sheet: TXLSXWorksheet;
Chart: TXLSXChart;
begin
Book := TXLSXWorkbook.Create;
try
Book.Open('cashflow.xlsx');
Sheet := Book.Sheets[0];
// Rectángulo de anclaje en coordenadas de fila/columna, igual que un gráfico clásico
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;
Al guardar, HotXLS ve que XlsxChartIsChartEx devuelve True, asigna un nombre de parte de un contador chartEx separado, y escribe xl/charts/chartEx1.xml, su archivo de rels, el override del content type y el ancla mc:AlternateContent que produce XlsxChartExAnchorPayloadXml. El contador independiente importa: un workbook con dos gráficos clásicos y un waterfall obtiene chart1.xml, chart2.xml y chartEx1.xml, no chartEx3.xml. Los treemaps se diferencian de los waterfalls en dos detalles, ambos manejados por el mismo builder. La dimensión numérica cambia de tipo — cx:numDim type="val" para un waterfall se convierte en type="size" para un treemap, porque un rectángulo treemap codifica la magnitud como área y no como valor representado — y los treemaps normalmente quieren una jerarquía de categorías, que el modelo expresa con TXLSXChartSeries.CategoriesMultiLevel. Activa ese flag y S.ResolveCategoryLevels lee el rango de la worksheet en el momento de escribir, lo divide en un TXLSXChartDataCache por nivel detrás de S.CategoryLevelCount y S.CategoryLevels[], y BuildChartExLevelXml emite un cx:lvl por nivel dentro de un solo cx:strDim, con el nivel hoja primero — el mismo orden físico que ECMA-376 Parte 1 §21.2 usa para c:multiLvlStrCache, así que esa convención se conserva aunque no lo haga ningún markup
Chart := Sheet.AddChart(xlsxChartTreemap, 'Revenue by Region',
2, 8, 24, 16);
S := Chart.AddSeries('Revenue',
'Sheet1!$A$2:$B$40', // dos columnas: etiqueta de hoja y luego el grupo padre
'Sheet1!$C$2:$C$40');
S.CategoriesMultiLevel := True;
La forma del cache que rompe todas las suposiciones de los gráficos clásicos
Si has escrito un lector de caches de gráficos clásicos, tres hábitos te van a traicionar con una parte chartEx, y los tres vienen de una única decisión de diseño: chartex guarda en atributos y contenido de elemento lo que DrawingML guarda en elementos hijo
- Un punto cacheado es
<cx:pt idx="0">42</cx:pt>. El valor es el contenido del elemento. No hay un hijoc:val que bajar, así que un parser que recorra buscando nodosvno encuentra nada y reporta un cache vacío ptCountyformatCodeson atributos sobrecx:lvl. En unc:numCacheclásico ambos son elementos hijo- Varios niveles de jerarquía son varios elementos
cx:lvldentro de un solocx:strDim. No hay un elemento de cache multi-nivel separado comoc:multiLvlStrCachelo está dec:strCache
BuildChartExLevelXml escribe esa forma y ParseChartExXml la lee de vuelta, pasando cada valor por XlsxEscapeText a la salida para que una etiqueta de categoría con un ampersand o un corchete angular sobreviva al round trip. Merece la pena anotar qué hace el builder cuando un nivel numérico no lleva format code: escribe formatCode="General" en vez de descartar el atributo, porque aquí el atributo no es opcional como lo es su contrapartida clásica
El prefijo de namespace que hacía fallar la lectura del tipo
La primera versión del parser chartEx de HotXLS escribía archivos perfectamente válidos y los leía todos de vuelta como un gráfico de columnas. La causa raíz era una línea, y la lección se generaliza a cualquier parser OOXML que vayas a escribir: TXMLReader.Name devuelve el nombre tal como está escrito en el documento, prefijo incluido. En una parte de gráfico clásica la mayoría de los elementos llegan sin prefijo, así que SameText(Reader.Name, 'series') funciona y dejas de pensar en ello. En una parte chartEx todo elemento está prefijado — el reader te entrega 'cx:series', la comparación falla en silencio con todos, y no obtienes ni excepción ni XML malformado, solo una lista de series vacía y un tipo de gráfico que cae al default. El arreglo es comparar por el nombre local, en un solo sitio y no en cada punto de llamada
// Reader.Name lleva su prefijo de namespace: 'cx:series', no '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 usa ese helper para cada test de elemento y un helper equivalente AttrVal para los atributos, porque cx:autoUpdate y compañía también van prefijados. Una vez que los elementos de serie se ven de verdad, el tipo vuelve del primer layoutId encontrado, y cada cx:series se conecta a su bloque cx:data casando cx:dataId/@val contra cx:data/@id. Los gráficos abiertos pero nunca modificados toman el path antiguo: los bytes originales se reproducen desde FRawChartXml, así que un archivo de cliente intacto sale como entró — la misma disciplina de writer conservador que gobierna las extensiones a nivel de hoja en el artículo de la extensión de sparklines
Lo que HotXLS deliberadamente no hace con estos gráficos
Hay dos límites que merece la pena decir con claridad, porque ambos son decisiones y no huecos esperando a llenarse. Los renderers PDF y SVG integrados no dibujan series waterfall ni treemap: los tipos no casan con ninguna rama de dibujo, así que el renderizado de series se salta y el resto de la hoja se exporta con normalidad — fingir un treemap con aspecto plausible en un PDF exportado sería peor que no dibujar nada, porque discreparía en silencio de lo que Excel muestra para el mismo archivo. Y xlsxChartWaterfall y xlsxChartTreemap son los únicos dos layouts extendidos que HotXLS construye desde el modelo; ST_SeriesLayout también define sunburst, funnel, box-and-whisker e histogram, y un workbook que contenga cualquiera de ellos sigue haciendo round trip por el path de replay de XML crudo, solo que no puedes crearlo desde código Delphi a día de hoy. El renderizado de los tipos de gráfico clásicos no cambia y está cubierto en el artículo sobre gráficos, imágenes y dibujos, y la pipeline chartEx viaja con él en el HotXLS Delphi Excel Component para Delphi y C++Builder