Artículo técnico

Embeber objetos de chart en hojas de cálculo con HotXLS

HotXLS puede colocar un chart directamente sobre una hoja de cálculo, anclado a un rango de celdas, en lugar de ponerlo en una hoja de chart dedicada. En términos BIFF8 eso significa escribir un drawing shape con un record OBJ de tipo 5 y estacionar el substream del chart al final del record stream de la hoja, que es exactamente el layout que Excel produce y exactamente donde el lector espera encontrarlo

La distinción le importa a cualquiera que genere reportes operativos. Una hoja de chart es un buen hogar para un visual principal único. Un desglose regional mensual quiere el chart al lado de los números que resume, en la misma hoja, dimensionado al bloque de celdas al que pertenece, de modo que quien lee haga scroll una vez en lugar de cambiar de pestaña y perder el contexto

Leer ya existía, escribir no

La asimetría vale la pena nombrarla porque moldea el trabajo. HotXLS ya podía leer charts embebidos: cuando el record stream de la hoja contiene un BOF marcado como substream de chart, el parser cambia de contexto, junta los records del chart, y en el EOF de cierre se los entrega de vuelta al drawing shape que el record OBJ introdujo. Esa ruta había sido ejercitada por cada workbook escrito en Excel que la biblioteca haya abierto jamás

Lo que faltaba era el lado de autoría, y la consecuencia útil es que el nuevo writer tenía una especificación precisa que alcanzar: producir el layout de bytes que el lector existente ya reattacha. No hay mejor criterio de aceptación para una función de formato binario que un lector escrito de forma independiente que usted no pueda modificar

De qué está hecho un chart embebido

Tres piezas tienen que concordar. La capa de dibujo aporta un shape de control host, la capa de objetos aporta un record OBJ cuyos common object data declaran el tipo de objeto 5, y el record stream aporta el substream del chart mismo. Los option flags del record OBJ son los que Excel escribe para un chart frame: positioned, locked, automatic line y automatic fill, que es lo que hace que el chart embebido se comporte como uno nativo cuando un usuario hace clic en él

HotXLS ancla un substream de chart BIFF8 a una hoja Delphi a través de tres piezas que concuerdan: el shape de control host de la capa de dibujo, el record OBJ cuyos common object data declaran el tipo de objeto 5, y la cadena de records del chart estacionada al final del record stream de la hoja, donde un BOF de chart cambia el contexto del parser y el EOF de cierre reattacha los records
Tres capas cargan un chart embebido: el drawing shape lo ancla, el record OBJ lo tipa como host de chart, y el substream del chart al final del stream de la hoja provee los records que el lector reattacha

El ancla merece una nota porque es una fuente común de bugs de off-by-one. La API de HotXLS toma números de fila y columna base uno, en línea con el resto de la biblioteca, y el client anchor escrito en el archivo es base cero. La conversión ocurre dentro de AddChartObject, así que quienes llaman se quedan en el sistema de coordenadas que usan en todo lo demás, pero cualquiera que compare un hex dump contra su propia llamada necesita recordar de qué lado de esa frontera está leyendo

var
  Book: TXLSWorkbook;
  Sheet: TXLSWorksheet;
  Series: array[0..1] of TXLSChartSeriesInfo;
begin
  Book := TXLSWorkbook.Create(nil);
  try
    Book.LoadFromFile('regional-sales.xls');
    Sheet := Book.Sheets[0];

    FillChar(Series, SizeOf(Series), 0);
    Series[0].Name := 'Actual';
    Series[0].Categories := 'Data!$A$2:$A$13';
    Series[0].Values := 'Data!$B$2:$B$13';
    Series[0].DataLabels.ShowValue := True;
    Series[0].HasDataLabels := True;

    Series[1].Name := 'Target';
    Series[1].Categories := 'Data!$A$2:$A$13';
    Series[1].Values := 'Data!$C$2:$C$13';
    Series[1].SecondaryAxis := True;

    // Anclado a E2:M20 en esta hoja, base uno
    Sheet.AddChartObject(xlsChartTypeColumn, 'Regional sales',
      'Month', 'Amount', Series, 2, 5, 20, 13);

    Book.SaveToFile('regional-sales-charted.xls');
  finally
    Book.Free;
  end;
end;

El FillChar sobre el arreglo de series no es decoración. TXLSChartSeriesInfo carga varios sub-records opcionales, data labels, estilo por serie, trendlines y error bars, cada uno con una puerta booleana, y un record parcialmente inicializado en el stack le entregará al emitter flags que nadie fijó. Ponga el arreglo en ceros y luego fije los campos que le importan

¿Qué referencias de serie acepta la ruta embebida?

Rangos planos estilo A1 dentro del mismo workbook, y esa restricción es deliberada, no un descuido. Cada referencia se resuelve contra la lista de hojas del workbook y se convierte en el índice de external reference que los records del chart necesitan. Un named range o una referencia a un workbook externo cae a un placeholder con una expresión parseada de longitud cero, así que el chart se escribe limpio pero esa serie en particular se queda sin fuente de datos hasta que usted la apunte a un rango

Aceptación de referencias de serie de HotXLS en la ruta de chart BIFF8 embebido: rangos estilo A1 como Data!$B$2:$B$13 dentro del mismo workbook se resuelven contra la lista de hojas hacia el índice de external reference que los records del chart necesitan, mientras que named ranges y referencias a workbooks externos caen a un placeholder con expresión parseada de longitud cero, con ambas cubiertas por AddChartSheet
Solo los rangos planos estilo A1 dentro del mismo workbook compilan a referencias de serie de chart; todo lo demás se escribe limpio como placeholder hasta ser re-apuntado, y la ruta completa vive en AddChartSheet

La razón es un trade de ingeniería de los derechos. La ruta completa de compilación de referencias existe en la ruta de hoja de chart, envuelta en la capa de colección de hojas, y extraerla limpiamente significaría duplicar cien líneas de lógica de resolución para un caso poco común en la práctica. Un chart embebido casi siempre grafica celdas de su propia hoja o de una hoja de datos hermana. Las referencias nombradas y externas quedan cubiertas en la ruta de hoja de chart vía AddChartSheet, así que nada está indisponible, solo se alcanza desde otro punto de entrada

Todo lo demás en el modelo de series funciona idéntico en ambas rutas. El binding de eje secundario, los estilos de línea, fill y marker por serie, las trendlines, los error bars y los data labels son parte de TXLSChartSeriesInfo y todos se emiten igual, así que una definición de chart puede moverse entre objeto embebido y hoja de chart cambiando solo la llamada. La mecánica de axis groups detrás del flag de eje secundario está cubierta en grupos de eje secundario en escritura BIFF

¿Por qué el título del chart se leía como dos caracteres?

Porque se pasó un conteo de caracteres donde se esperaba un conteo de bytes, y los strings Unicode de BIFF hacen que ese error sea fácil de escribir y difícil de ver. Un string Unicode BIFF corto empieza con un conteo de caracteres y un byte de flags, y el byte de flags carga el bit de high byte que dice si el payload es de un byte por carácter o de dos. Lea un payload de 16 bits con el conteo de caracteres como si fuera una longitud en bytes y obtiene exactamente la mitad del string: una serie llamada Sales regresa como Sa, y un título de chart se trunca igual porque títulos y etiquetas de serie comparten la ruta de decodificación

Lo que hace notable este defecto es que recurió tres veces en la misma familia de records, una vez en nombres de trendline, una vez en nombres de pivot chart, y una vez en títulos de chart. Cada ocurrencia parecía un bug nuevo de una función nueva. Las tres eran el mismo multiply faltante. La regla que finalmente lo cerró es mecánica y debería aplicarse sin juicio: cada vez que lea uno de estos strings, consulte primero el flag de high byte y multiplique el conteo de caracteres por el ancho del payload antes de tocar el buffer. Los detalles a nivel de record están en decodificar conteos de caracteres de XLUnicodeString y el flag de high byte

// El chart embebido comparte la capa de dibujo con imágenes y shapes,
// así que un dibujo existente en la hoja se preserva. AddChartObject
// devuelve el índice del objeto creado
var
  ObjIndex: Integer;
begin
  ObjIndex := Sheet.AddChartObject(xlsChartTypeLine, 'Trend',
    'Week', 'Units', Series, 2, 8, 18, 16);
  if ObjIndex < 0 then
    raise Exception.Create('chart object was not created');
end;

Dónde encajan los charts embebidos frente a las alternativas

Existen tres rutas y responden preguntas distintas. Un objeto de chart embebido pertenece junto a sus datos en una hoja de cálculo y es lo que la mayoría de los reportes quieren. Una hoja de chart conviene para un visual de presentación único y le da la ruta completa de compilación de referencias. Preservar un chart existente de un archivo cargado, intacto, es la respuesta correcta cuando el workbook vino de Excel con formato que nadie quiere que una biblioteca reinterprete; ese comportamiento de pass-through está descrito en ChartML preservado y charts combinados

Como el chart embebido viaja sobre la capa de dibujo, coexiste con imágenes y shapes en la misma hoja en lugar de reemplazarlos, y el modelo general de esa capa está cubierto en charts, imágenes y dibujos en HotXLS. Las tres rutas vienen en el HotXLS Delphi spreadsheet component, así que la elección va de cómo debe verse el reporte y no de qué puede expresar la biblioteca

El punto metodológico es el que vale conservar. Cuando una función de formato binario tiene un lector existente, construya el writer contra el lector y no contra su lectura de la especificación. El lector codifica años de contacto con archivos que aplicaciones reales produjeron de verdad, incluidas las partes que la especificación enuncia con laxitud, y un writer que lo satisface tiene muchas más probabilidades de satisfacer también a Excel