HotXLS escribe los gráficos de cascada 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 todo el pipeline de partes sigue solo
La forma en que este problema suele anunciarse es un ticket de soporte. Un cliente manda un workbook que su equipo de finanzas armó en Excel 2019, tu herramienta de reportes lo abre, lo guarda, y el gráfico de cascada desapareció. Sin excepción, sin advertencia, nada en el log — el gráfico simplemente no está en la salida. Si vas a buscarlo por la ruta de código de los gráficos clásicos vas a buscar un buen rato, porque el gráfico nunca estuvo ahí para empezar
¿Por qué los gráficos de cascada y treemap no están en c:chartSpace?
Porque Microsoft no podía ponerlos ahí. El esquema de gráficos DrawingML en ECMA-376 Parte 1 §21.2 enumera sus elementos de gráfico — c:barChart, c:lineChart, c:pieChart y el resto — como una elección cerrada, así que añadir cascada, treemap, sunburst y embudo 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 sin relación con ellos. El lado del dibujo cose los dos mundos con mc:AlternateContent, el mecanismo de compatibilidad de ECMA-376 Parte 3: 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 <cx:chart r:id="rIdN"/> a secas, mientras que mc:Fallback guarda 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 de gráfico estándar; una parte chartEx usa http://schemas.microsoft.com/office/2014/relationships/chartEx en su lugar, documentada en [MS-ODRAWXML]. HotXLS expone ambos 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 enruta un gráfico por el camino extendido al guardar. Los gráficos combinados y el XML de gráfico clásico preservado van por el otro, descrito en el artículo sobre XML de gráfico preservado y gráficos combinados
Cuatro reglas de esquema que deciden si Excel abre el archivo
El esquema chartex es más estricto que el clásico en cuatro lugares concretos, y cada uno produce el mismo aviso de reparación inútil cuando te equivocas. BuildChartExKnownXml y BuildChartExRelsXml en HotXLS los incorporan a los cuatro, pero conviene conocerlos 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 armas con los gráficos clásicos, donde las cachés viven dentro de las series dentro del área de trazado; en una parte chartEx los datos van al frente 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 gráficos combinados — una región, y las series adentro - Ubicación del tipo. El tipo de gráfico no es para nada un nombre de elemento. Es el atributo
layoutIdsobrecx: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 al revé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 hoja que es dueña de 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 agregas un gráfico de cascada o treemap en Delphi?
De la misma manera que agregas 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, como 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 desde un contador chartEx separado, y escribe xl/charts/chartEx1.xml, su archivo de rels, el override de content type y el ancla mc:AlternateContent producida por XlsxChartExAnchorPayloadXml. El contador independiente importa: un workbook con dos gráficos clásicos y una cascada recibe chart1.xml, chart2.xml y chartEx1.xml, no chartEx3.xml. Los treemap entonces difieren de las cascadas en dos detalles, ambos manejados por el mismo builder. La dimensión numérica cambia de tipo — cx:numDim type="val" para una cascada se vuelve type="size" para un treemap, porque un rectángulo de treemap codifica la magnitud como área y no como un valor graficado — y los treemap suelen querer una jerarquía de categorías, que el modelo expresa con TXLSXChartSeries.CategoriesMultiLevel. Activa ese flag y S.ResolveCategoryLevels lee el rango de la hoja 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, primero el nivel hoja — el mismo orden físico que ECMA-376 Parte 1 §21.2 usa para c:multiLvlStrCache, así que esa convención sobrevive aunque ninguno de los markup lo haga
Chart := Sheet.AddChart(xlsxChartTreemap, 'Revenue by Region',
2, 8, 24, 16);
S := Chart.AddSeries('Revenue',
'Sheet1!$A$2:$B$40', // dos columnas: etiqueta hoja, luego grupo padre
'Sheet1!$C$2:$C$40');
S.CategoriesMultiLevel := True;
La forma de la caché que rompe todos los supuestos de los gráficos clásicos
Si has escrito un lector de caché de gráficos clásicos, tres hábitos te van a traicionar con una parte chartEx, y los tres vienen de una sola decisión de diseño: chartex guarda en atributos y en el contenido del elemento lo que DrawingML guarda en elementos hijos
- 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 recorre buscando nodosvno encuentra nada e informa una caché vacía ptCountyformatCodeson atributos sobrecx:lvl. En unc:numCacheclásico ambos son elementos hijos- Varios niveles de jerarquía son varios elementos
cx:lvldentro de un solocx:strDim. No existe un elemento de caché multinivel separado, de la forma en quec:multiLvlStrCachees distinto 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. Vale la pena notar qué hace el builder cuando un nivel numérico no trae formato: escribe formatCode="General" en lugar de tirar el atributo, porque aquí el atributo no es opcional como su contraparte 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 leía cada uno 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 cada elemento está prefijado — el reader te entrega 'cx:series', la comparación falla en silencio para todos, y no tienes ni excepción ni XML malformado, solo una lista de series vacía y un tipo de gráfico que cae al predeterminado. La corrección es comparar por el nombre local, en un solo lugar en vez de en cada call site
// Reader.Name trae 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 prueba de elemento y un helper AttrVal equivalente para los atributos, porque cx:autoUpdate y compañía también vienen prefijados. Una vez que los elementos de series se ven de verdad, el tipo vuelve desde el primer layoutId encontrado, y cada cx:series se conecta con su bloque cx:data haciendo coincidir cx:dataId/@val contra cx:data/@id. Los gráficos abiertos pero nunca modificados van por el camino viejo: los bytes originales se reproducen desde FRawChartXml, así que un archivo de cliente sin tocar sale tal como entró — la misma disciplina de escritor conservador que rige las extensiones a nivel de hoja en el artículo sobre la extensión de sparklines
Lo que HotXLS deliberadamente no hace con estos gráficos
Vale la pena decir dos límites con claridad, porque ambos son decisiones y no huecos esperando a ser llenados. Los renderizadores PDF y SVG integrados no dibujan series de cascada ni treemap: los tipos no coinciden con ninguna rama de dibujo, así que el renderizado de la serie se salta y el resto de la hoja se exporta normal — falsificar un treemap de aspecto plausible en un PDF exportado sería peor que no dibujar nada, porque discreparía en silencio con 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 igual hace round trip por el camino de reproducción del XML crudo, solo que no puedes crear uno desde código Delphi 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 el pipeline chartEx viaja junto a él en el HotXLS Delphi Excel Component para Delphi y C++Builder