Artigo Técnico

Grupos de eixos secundários BIFF8 no Delphi

O HotXLS grava grupos de eixo secundário de gráficos BIFF8 emitindo um segundo bloco AxisParent, não anexando um segundo grupo de gráfico depois dos eixos. No substream de gráficos do XLS clássico, cada grupo de gráfico — ChartFormat, o registro de tipo do gráfico e CrtLink — vive dentro do seu próprio bloco de grupo de eixos, e cada série se associa a um deles por meio de SerToCrt. Inverta esse aninhamento e não haverá um segundo grupo de gráfico para uma série usar, por mais registros de eixo que você emita

Por que um segundo grupo de gráfico depois dos eixos não associa nada?

A gramática é toda a resposta, e ela está em uma linha de ABNF. A regra CHARTFOMATS em [MS-XLS] 2.1.7.20.1 diz AxesUsed 1*2AXISPARENT e depois detalha AXISPARENT = AxisParent Begin Pos [AXES] 1*4CRT End. Leia essas duas produções juntas e o formato aparece: os grupos de gráfico são filhos de um grupo de eixos, não irmãos dele. Um substream de gráfico com dois pares de eixos e um grupo de gráfico no final não é um gráfico de dois eixos com uma peculiaridade de layout; é um gráfico com um grupo de gráfico e um conjunto de registros de eixo órfãos. Isso importa porque SerToCrt ($1045), que fica dentro do bloco SERIESFORMAT, carrega um índice baseado em zero do grupo de gráfico, não um índice de eixo. Gravar crt = 1 quando só existe um bloco CRT aponta uma série para um grupo de gráfico que nunca foi emitido. A intuição que confunde as pessoas está no nome do registro: AXESUSED ($1046) parece contar eixos, então o próximo passo natural é emitir mais eixos. Ele conta grupos de eixos, e cada grupo de eixos traz consigo uma área de plotagem completa e um grupo de gráfico

Marcando uma série para o grupo de eixo secundário

No lado do HotXLS, isso se reduz a um Boolean. TXLSChartSeriesInfo carrega um campo SecondaryAxis, e defini-lo em qualquer série do array passado a TXLSWorksheets.AddChartSheet muda todo o builder para o modo de dois grupos. Não existe uma chamada separada de "enable secondary axis" nem um parâmetro de contagem de eixos, porque a contagem pode ser derivada: se qualquer série quiser o grupo secundário, o gráfico precisa de dois

var
  Wb: TXLSWorkbook;
  Series: array [0..1] of TXLSChartSeriesInfo;
begin
  Wb := TXLSWorkbook.Create;
  try
    Wb.Sheets.Add.Name := 'Data';
    // ... preencha A1:C12 com categorias, receita e margem ...

    Series[0] := Default(TXLSChartSeriesInfo);   // nunca use FillChar neste record
    Series[0].Name := 'Revenue';
    Series[0].Categories := 'Data!$A$1:$A$12';
    Series[0].Values := 'Data!$B$1:$B$12';

    Series[1] := Default(TXLSChartSeriesInfo);
    Series[1].Name := 'Margin';
    Series[1].Categories := 'Data!$A$1:$A$12';
    Series[1].Values := 'Data!$C$1:$C$12';
    Series[1].SecondaryAxis := True;             // AXESUSED vira 2

    Wb.Sheets.AddChartSheet('Dual Axis', xlsChartTypeLine,
      'Revenue vs Margin', '', '', Series);
    Wb.SaveAs('dual-axis.xls', xlExcel97);
  finally
    Wb.Free;
  end;
end;

A linha Default(TXLSChartSeriesInfo) não é decoração. TXLSChartSeriesInfo mistura campos gerenciados (os nomes WideString, os arrays dinâmicos de trendline e error bar) com membros Boolean simples, e o Delphi só garante que os campos gerenciados sejam limpos por você. Deixe SecondaryAxis sem inicialização e ele terá o que estava na stack, o que na prática significa que o mesmo binário produz um gráfico de um eixo quando executado por um host de console e um gráfico de dois eixos no test runner. Enquanto isso, os ranges de categorias e valores são resolvidos pela tabela EXTERNSHEET do workbook antes de o builder sequer vê-los — a mesma maquinaria de índices coberta em como o HotXLS classifica links externos BIFF SupBook e XTI — então um range que nomeia uma sheet desconhecida degrada para um placeholder BRAI vazio, em vez de falhar o build

O que o HotXLS emite quando uma série é secundária

O emitter muda de forma, não apenas de valor. Sem série secundária, AXESUSED contém 1 (ou 0 para pizza e pizza 3D, que não têm grupos de eixos) e um bloco AxisParent vem depois. Com uma, AXESUSED contém 2 e o builder executa o bloco duas vezes, com iax — a primeira word do payload de 18 bytes de AxisParent ($1041) — definida primeiro como 0 e depois como 1. Cada passagem emite Pos, o eixo de categorias e o eixo de valores (Axis, $101D), o marcador PlotArea ($1035), um Frame padrão, depois ChartFormat ($1014), o registro de tipo do gráfico, CrtLink ($1022) e dois marcadores End para fechar o grupo de gráfico e o grupo de eixos. A série secundária então se associa com SerToCrt crt = 1, e a primária mantém crt = 0. Um registro deliberadamente não é duplicado: a legenda é emitida somente no primeiro grupo, porque o Excel dá a um gráfico uma única legenda independentemente de quantos grupos de eixos ele contenha. Duas outras propriedades merecem ser ditas claramente. Reestruturar o emitter para pôr o grupo de gráfico dentro do bloco axis-parent não mudou a saída de gráficos comuns — sem série secundária, o substream é byte a byte idêntico à versão anterior, pois parametrizar AddAxisParent com iax = 0 é exatamente o caminho antigo. E o builder continua emitindo um par completo de eixos por grupo, então um grupo secundário sempre chega com seu próprio eixo de categorias mesmo quando você só se importa com sua escala de valores

Como a inspeção do gráfico recupera a associação do grupo de eixos?

A leitura ocorre em duas passagens sobre a lista de registros, e precisa ser assim porque AXESUSED chega antes dos blocos que descreve. A primeira passagem procura apenas $1046 e lê sua primeira word como a contagem de grupos de eixos. Esse valor começa em 1 e só aumenta, nunca diminui: o HotXLS usa o máximo entre a contagem atual e a declarada, então um AXESUSED malformado ou duplicado não consegue fazer um gráfico que já foi visto declarando dois grupos regredir. A segunda passagem acompanha o grupo de eixos atual, atualizando-o a cada AxisParent, e grava esse índice em cada registro Axis encontrado até aparecer o próximo AxisParent

var
  Model: TXLSChartModel;
  i: Integer;
begin
  // Sheets[1] e a worksheet de dados, Sheets[2] e a chart sheet
  Model := Wb.Sheets[2]._Chart.GetChartModel;
  try
    if Model.AxisGroupCount = 2 then
      Writeln('AXESUSED declares a secondary axis group');
    for i := 0 to Model.AxisCount - 1 do
      Writeln('axis ', i, ' group ', Model.GetAxis(i).AxisGroup);
    for i := 0 to Model.SeriesCount - 1 do
      Writeln('series ', i, ' chart group ', Model.GetSeries(i).ChartGroup);
  finally
    Model.Free;
  end;
end;

Vale nomear dois limites. TXLSChartModel.AxisGroupCount informa o que o arquivo declara, não quantos blocos AxisParent foram realmente encontrados; um arquivo que diz 2 e entrega um bloco reportará 2, e AxisCount é onde você percebe isso. E TXLSChartAxis.AxisGroup é uma marca posicional: registra em qual bloco um eixo foi lido, a única informação que o formato fornece. No lado das séries, a decodificação de SerToCrt é condicionada a estar dentro de um bloco Series, porque o mesmo id de registro aparece em contextos onde não é uma associação de série, e um decoder sem essa condição sobrescreveria alegremente a série errada

Verificando eixos secundários sem um arquivo Excel real

A verificação aqui não precisou de um arquivo Excel com eixo secundário, e essa é a parte útil da história. A decodificação estrutural é uma propriedade da sequência de registros, então uma sequência sintetizada a prova tão precisamente quanto uma capturada. A regressão constrói AXESUSED com payload 2, depois dois blocos AxisParent, cada um envolvendo um eixo de categorias e um eixo de valores, e afirma que o modelo volta com AxisGroupCount = 2, quatro eixos marcados como 0, 0, 1, 1 e os tipos de eixo esperados no segundo par

// Verificacao estrutural sem envolver arquivo Excel algum
Chart := TXLSCustomChart.Create(nil, $0600);
try
  AddWordRecord($1046, [2]);   // AXESUSED: dois grupos de eixos
  AddAxisParentGroup(0);       // AxisParent iax=0 + Begin + 2 Axis + End
  AddAxisParentGroup(1);       // AxisParent iax=1 + Begin + 2 Axis + End

  Model := Chart.GetChartModel;
  Assert.AreEqual(2, Model.AxisGroupCount);
  Assert.AreEqual(4, Model.AxisCount);
  Assert.AreEqual(0, Model.GetAxis(1).AxisGroup);
  Assert.AreEqual(1, Model.GetAxis(2).AxisGroup);
finally
  Model.Free;
  Chart.Free;
end;

Duas notas práticas se você sintetizar registros por conta própria. TXLSCustomChart.AddData(RecID, Len, nil) desreferencia o payload quando Len não é zero, então marcadores Begin ($1033) e End ($1034) precisam ser adicionados com comprimento zero, não com um blob nil e um tamanho antigo. E uma sequência sintética prova o decoder, nunca a aceitação do output pelo Excel — o lado da escrita foi estruturado a partir da ABNF e depois verificado com round-trip por GetChartModel, afirmando as marcas por eixo 0/0/1/1 e os grupos de gráfico por série 0/1, com o caminho byte a byte idêntico sem secundário como rede de segurança. É a mesma postura conservadora sobre a qual se apoia o restante do suporte a gráficos, imagens e drawings HotXLS para Delphi: decodifique o que os registros dizem e recuse adivinhar um layout binário que você não leu na especificação. A suíte Delphi completa executou 1650 de 1650 no Win32 e no Win64 depois que o lado da escrita foi concluído

Parâmetros de cena Chart3d e a armadilha fAuto

Dois detalhes adjacentes atingem quem vai além do gráfico padrão. O primeiro é Chart3d ($103A, [MS-XLS] 2.4.46), um payload plano de 14 bytes emitido dentro do grupo de gráfico para variantes 3D: anRot (rotação, 0 a 360), anElev (elevação, com sinal, -90 a 90), pcDist (distância de perspectiva, 0 a 100, ignorada a menos que fPerspective esteja definida), pcHeight e pcDepth (percentual da largura do gráfico, 5 a 500), pcGap (0 a 500) e um grbit cujos bits são fPerspective $0001, fCluster $0002, fAutoscale $0004, f3DScaling $0010 e f2DWalls $0020. A especificação acrescenta restrições que o layout do registro não impõe por você: em um gráfico de barras transposto, anRot e anElev não podem exceder 44, e em um gráfico de pizza anElev não pode ser negativo

O segundo é o bit fAuto, e é ele que produz o relatório de bug "minhas cores foram ignoradas". LineFormat ($1007), AreaFormat ($100A) e MarkerFormat ($1009) carregam fAuto no bit 0 de seu grbit, e quando esse bit está definido o Excel aplica seu estilo automático e trata os valores RGB explícitos, o estilo da linha, a forma do marcador e o tamanho do marcador ao lado dele como decoração. Qualquer emitter que grave um estilo de série customizado precisa limpar o bit 0; os emitters padrão o mantêm definido precisamente para que o Excel escolha a paleta. Se você estiver editando um workbook existente, em vez de construí-lo, as regras de preservação mudam novamente e estão cobertas em editar gráficos Excel sem perder ChartML preservado

Grupos de eixo secundário, associação SerToCrt e o modelo tipado de gráfico mostrado aqui fazem parte do componente de planilhas Delphi HotXLS para Delphi e C++Builder, que lê e grava gráficos BIFF8 sem Excel instalado; a página do produto traz a referência completa dos registros de gráfico e a lista de overloads de AddChartSheet