Artículo técnico

Editar gráficos de Excel sin perder ChartML con HotXLS

Una parte de gráfico guardada por una build actual de Excel rara vez se queda en los elementos que el esquema de gráficos de ECMA-376 define. El chart space suele abrir con un bloque mc:AlternateContent que ofrece c14:style a los lectores nuevos y un c:style llano a los viejos, cada serie lleva un c:extLst con un c16:uniqueId, y las etiquetas de datos "Value From Cells" viven en otra extensión más. Un programa que parsea solo lo que modela y regenera la parte al guardar no devuelve nada de eso. Cambia una palabra del título, reabre el archivo en Excel, y las etiquetas que sacaban el texto de un rango de celdas se han ido

El arreglo es dejar de tratar un guardado como una reconstrucción. Conserva los bytes con que llegó el gráfico, escríbelos de vuelta sin cambios cuando nada modelado se haya movido, y cuando algo se haya movido, fusiona los elementos nuevos en el árbol original para que las partes desconocidas se queden donde Excel las puso

Reproducir el ChartML original cuando nada cambió

HotXLS hace exactamente eso cuando TXLSXWorkbook.PreserveUnsupportedParts es True, que es el valor por defecto en un libro recién creado. Al importar, cada gráfico conserva sus bytes UTF-8 originales junto con la longitud y una huella de 64 bits del contenido que el modelo tipado representa. Al guardar, HotXLS regenera la parte modelada, compara longitud y huella, y si ambas coinciden escribe los bytes guardados de vuelta tal cual: sin reordenación de elementos, sin deriva de espacios en blanco, y sin una segunda copia UTF-16 de cada gráfico sin tocar retenida en memoria

Como la comparación corre sobre contenido, asignar a una propiedad el valor que ya tiene no cambia nada. Los setters de TXLSXChart salen pronto ante un valor igual, así que un bucle que «refresca» el título de cada gráfico desde una fila de base de datos sigue reproduciendo cada gráfico cuyo título ya era el correcto. El formato de línea en barras de error y líneas de tendencia se rastrea con su propio flag de cambio, y cambiarlo cuenta siempre como una edición

Diagrama de la preservación ChartML de HotXLS en Delphi donde los gráficos sin cambios reproducen los bytes originales y los gráficos editados fusionan nodos tipados del modelo en el XML preservado, mientras una reconstrucción solo desde el modelo pierde las ramas extLst y AlternateContent
HotXLS almacena los bytes ChartML originales en la importación y los reproduce intactos, o fusiona ediciones tipadas en el árbol preservado cuando el modelo cambió

TXLSXChart.HasPreservedXml te dice qué ruta lleva un gráfico antes de que edites nada. Reporta True cuando hay ChartML original atado al gráfico, que es la precondición tanto de la reproducción como de la fusión

var
  Workbook: TXLSXWorkbook;
  Chart: TXLSXChart;
begin
  Workbook := TXLSXWorkbook.Create;
  try
    // True por defecto; ponlo explícitamente para que la intención sobreviva a refactorizaciones.
    // Tiene que estar puesto antes de Open, porque Open construye el estado
    Workbook.PreserveUnsupportedParts := True;
    Workbook.Open('quarterly-review.xlsx');

    Chart := Workbook.Sheets[1].Charts[0];   // Sheets[] es base 1, Charts[] base 0
    if not Chart.HasPreservedXml then
      Log('chart 0 has no original ChartML attached');   // Log es tu propia rutina

    Chart.Title := Chart.Title;                  // valor igual: sigue reproduciendo
    Workbook.Sheets[1].Charts[1].Title :=
      'Revenue by region, Q3 2026';              // este gráfico toma la ruta de fusión
    Workbook.SaveAs('quarterly-review-out.xlsx');
  finally
    Workbook.Free;
  end;
end;

¿Qué sobrevive a una fusión cuando editas un gráfico?

La respuesta corta: todo de lo que el modelo tipado no es dueño. Título, series, cachés de datos, grupos de trazado y ejes son propiedad del modelo, así que el árbol recién generado los decide. Los atributos no modelados, las declaraciones de espacios de nombres, los estilos, las listas de extensión y los nodos de formato vienen del árbol original y se reinserzan al lado de los nodos coincidentes junto a los que estaban

Borra una serie y la fusión no debe resucitarla desde el XML preservado solo porque el original todavía contenga un c:ser en esa posición. Todo lo que es del modelo se toma del modelo incluso cuando su respuesta es "se ha ido". Lo mismo aplica a nivel de atributo: cuando limpias negrita, cursiva, subrayado, tachado o el tamaño de fuente de un estilo de texto conocido, la fusión lee el atributo ausente como una limpieza y no copia b ni sz de vuelta desde el defRPr original. Los atributos desconocidos del mismo elemento conservan su sitio

Los títulos siguen la misma regla. Al importar, HotXLS conserva los runs rich nativos del c:title, con sus fuentes, tamaños y etiquetas de idioma por run, y los reproduce mientras el texto no se toque. Asigna un Title nuevo y ese contenido rich se descarta; el título se reescribe como un único run estilado desde TitleTextStyle. Fusionar los runs viejos en el texto nuevo sería adivinar a qué run pertenecen las palabras nuevas, y una adivinanza aquí produce títulos que nadie escribió

El orden del esquema se conserva donde Office es quisquilloso. Un extLst se queda como último hijo de su padre, como exige el esquema. Los bloques de compatibilidad de marcado reciben tratamiento propio: el lector se salta mc:Choice y lee mc:Fallback, y cuando ese respaldo envuelve un grupo de trazado que el modelo conoce, la fusión escribe el nuevo contenido tipado de vuelta en el subárbol de respaldo. Una fusión ingenua emitiría el grupo de trazado editado y dejaría la copia vieja dentro de la rama de respaldo, así que el gráfico guardado llevaría el mismo grupo dos veces

Excluir un gráfico con ClearPreservedXml

TXLSXChart.ClearPreservedXml suelta el XML original y la huella de un gráfico, y el próximo guardado reconstruye ese gráfico puramente desde el modelo tipado. Es una decisión por gráfico. PreserveUnsupportedParts sigue gobernando si el estado de preservación se construye al importar, y cubre también otras partes del paquete sin soporte, no solo gráficos, razón por la que apagarla para conseguir un gráfico limpio es la palanca equivocada

Limpia un gráfico cuando descartar es el objetivo de la operación, por ejemplo un gráfico de plantilla que todavía lleva formato de un tema corporativo retirado. No lo uses como reflejo de depuración. Un gráfico que se renderiza mal tras una edición suele ser una cuestión de modelado, y limpiar el XML preservado tira la única copia de lo que Excel escribió originalmente

Un caso necesita comprobación previa. Si una barra de error o una línea de tendencia usa formato de línea que el modelo tipado no puede representar, su propiedad LineAppearanceSupported es False; una etiqueta de línea de tendencia con formato fuera del modelo reporta lo mismo vía LabelAppearanceSupported. HotXLS no reemplazará silenciosamente ninguna de las dos por una versión más plana: en cuanto el gráfico no tiene XML preservado al que recurrir, el guardado se detiene con una EArgumentException que nombra la apariencia de línea o etiqueta no representada. Comprobar ambas antes de limpiar lleva un bucle

// Tu propio helper, no forma parte de HotXLS: True cuando cada línea de barra de error,
// línea de trendline y etiqueta de trendline se pueden escribir solo desde el modelo tipado
function ChartRebuildable(Chart: TXLSXChart): Boolean;
var
  I, T: Integer;
  S: TXLSXChartSeries;
begin
  Result := False;
  for I := 0 to Chart.SeriesCount - 1 do    // recorre cada grupo de trazado
  begin
    S := Chart.Series[I];
    if (S.ErrBarX <> nil) and not S.ErrBarX.LineAppearanceSupported then Exit;
    if (S.ErrBarY <> nil) and not S.ErrBarY.LineAppearanceSupported then Exit;
    for T := 0 to S.TrendlineCount - 1 do
    begin
      if not S.Trendlines[T].LineAppearanceSupported then Exit;
      if not S.Trendlines[T].LabelAppearanceSupported then Exit;
    end;
  end;
  Result := True;
end;

// ...
Chart := Workbook.Sheets[1].Charts.FindByName('Old theme chart');
if (Chart <> nil) and ChartRebuildable(Chart) then
  Chart.ClearPreservedXml      // el próximo guardado reconstruye este gráfico desde el modelo
else
  Log('kept original ChartML for this chart');

Gráficos combinados con AddPlotGroup y un eje secundario

TXLSXChart.AddPlotGroup añade un grupo de trazado ordenado con su propia familia de gráfico y su propia asignación a eje primario o secundario. Eso cubre el gráfico que pide todo equipo de finanzas: ingresos como columnas en el eje izquierdo, porcentaje de margen como línea en el derecho. Cada gráfico expone ValueAxis, CategoryAxis, SecondaryValueAxis, SecondaryCategoryAxis y SeriesAxis, con ajustes tipados de posición, identificadores de eje, cruce, unidades de fecha, intervalos de etiqueta, formato numérico y visibilidad

La API plana de los gráficos de un solo tipo sigue ahí y se mapea sobre los grupos. ChartType y AddSeries se dirigen al primer grupo de trazado; SeriesCount y Series[] recorren las series de todos los grupos en orden, razón por la que el helper de arriba ve también las series de línea. El orden importa en un sitio. TXLSXCharts.Add crea un gráfico sin grupo de trazado alguno, y la primera de AddSeries o AddPlotGroup que corra crea el grupo uno. Llama a AddPlotGroup(xlsxChartLine, xlsxAxisSecondary) primero en un gráfico fresco y la línea se convierte en el grupo uno, el tipo de gráfico cambia a línea, y la serie de ingresos que añadas después cae en él. Crear ambos grupos explícitamente elimina la ambigüedad, así que el ejemplo lo hace

var
  Chart: TXLSXChart;
  Columns, Line: TXLSXChartPlotGroup;
begin
  // Anclado sobre las filas 1..15, columnas 5..12; aún no existe ningún grupo de trazado
  Chart := Sheet.Charts.Add(xlsxChartColumn, 'Revenue and margin',
    1, 5, 15, 12);
  Columns := Chart.AddPlotGroup(xlsxChartColumn);              // grupo 1, primario
  Columns.AddSeries('Revenue', 'Data!$A$2:$A$13', 'Data!$B$2:$B$13');

  Line := Chart.AddPlotGroup(xlsxChartLine, xlsxAxisSecondary); // grupo 2
  Line.AddSeries('Margin %', 'Data!$A$2:$A$13', 'Data!$C$2:$C$13');
  Line.ZOrder := 1;                     // dibuja la línea delante de las columnas

  Chart.SecondaryValueAxis.NumberFormat := '0%';
  Chart.SecondaryValueAxis.NumberFormatSourceLinked := False;
  Chart.SecondaryValueAxis.MinValue := 0;
  Chart.SecondaryValueAxis.MaxValue := 0.5;

  // Área de trazado como fracciones del gráfico: x, y, ancho, alto en una llamada
  Chart.PlotAreaLayout.SetPositionAndSize(0.08, 0.12, 0.80, 0.72);
end;
Diagrama de un gráfico combinado HotXLS en Delphi con columnas de ingresos de AddSeries en el eje de valores primario y una línea de margen de AddPlotGroup en el eje de valores secundario, mientras la API plana antigua solo alcanza el grupo uno
Cada llamada AddPlotGroup añade un grupo ordenado con su propia familia de gráficos y asignación de eje primario o secundario mientras que la API plana sigue direccionando al grupo uno

ZOrder en un grupo de trazado controla el apilado: el escritor de gráficos ordena los grupos por él antes de emitir los bloques c:barChart y c:lineChart, los valores bajos detrás. Como cada grupo ordenado está modelado, abrir y volver a guardar un gráfico combinado mantiene cada familia de gráfico junto con sus ejes secundario, de fecha y de series. Las referencias a fórmulas de gráfico y los valores de serie en caché sobreviven a copias del libro y a ciclos de abrir y guardar, y los anclajes de gráfico se emparejan con sus partes de gráfico por identificador de relación, así que un paquete cuyas entradas de relación reordenó otra herramienta sigue poniendo cada gráfico en su posición de hoja de cálculo

¿Por qué Office ignora un layout manual?

Porque las coordenadas llegaron incompletas. Office exige x, y, w y h como grupo completo siempre que cualquiera de ellas esté presente en un c:manualLayout, y si no ignora el elemento entero. El esquema marca las cuatro como opcionales, así que un archivo con solo x e y es válido y aun así no hace nada en Excel

TXLSXChartLayout.SetPositionAndSize es la llamada a usar. Fija los cuatro valores, marca los cuatro elementos de modo como presentes y habilita el layout en un paso. Las propiedades individuales X, Y, Width y Height fijan cada una un valor, y HotXLS escribe exactamente los elementos presentes, igual que conserva la presencia de cada uno de los nueve hijos opcionales cuando lee un archivo. Eso mantiene un layout sin tocar fiel a su fuente. También significa que un layout construido desde dos asignaciones de propiedades se escribe con dos coordenadas, y Office lo saltará. La misma clase TXLSXChartLayout está detrás de PlotAreaLayout, TitleLayout, LegendLayout y el TitleLayout de cada eje

Diagrama de la regla de disposición manual de HotXLS en Delphi donde x, y, w y h deben llegar como grupo completo o Office ignora todo el elemento manualLayout, con modos edge y factor y layoutTarget limitado al área de trazado
Office aplica un layout manual solo cuando x, y, w y h llegan como un grupo completo, razón por la que SetPositionAndSize escribe los cuatro en una llamada

Dos valores por defecto siguen lo que hace Excel en lugar de todo lo que la especificación permite. Un layout nuevo usa edge para XMode y YMode y factor para WidthMode y HeightMode; el estándar permite las otras combinaciones, pero Excel guarda posiciones como bordes y tamaños como factores y convierte cualquier otra cosa al guardar. Y layoutTarget (Target, interior o exterior) se escribe para el área de trazado, mientras que los layouts de título, título de eje y leyenda lo omiten, porque las notas de implementación de Microsoft para el esquema de gráficos no lo permiten bajo esos elementos

Encajar esto en un ida y vuelta del libro

La preservación de gráficos es un caso de una regla más amplia: una biblioteca que edita documentos creados en otro sitio debería cambiar lo que se le pidió cambiar y dejar el resto como lo encontró. HotXLS aplica el mismo razonamiento a temas, cadenas de cálculo y extensiones de hoja, cubierto en los ida y vuelta sin pérdida de tema, extLst y calcChain, y a proyectos VBA y enlaces externos en preservar VBA y enlaces externos. Para crear gráficos, colocar imágenes y dibujar formas desde cero, mira gráficos, imágenes y dibujos en HotXLS

HotXLS lee, edita y escribe gráficos XLSX desde código nativo de Delphi y C++Builder sin Excel instalado en la máquina, así que la fidelidad de los gráficos la decide la biblioteca y no un servidor de automatización. La referencia de la API de gráficos y una build de prueba están en la página del componente de hojas de cálculo para Delphi de HotXLS