Artigo Técnico

Grupos de eixos secundários BIFF8 no HotXLS

O HotXLS escreve grupos de eixos secundários de gráficos BIFF8 emitindo um segundo bloco AxisParent, não acrescentando um segundo grupo de gráficos depois dos eixos. No substream de gráficos Classic XLS, cada grupo de gráficos — ChartFormat, o registo do tipo de gráfico e CrtLink — vive dentro do seu próprio bloco de grupo de eixos, e cada série liga-se a um deles através de SerToCrt. Inverta esse aninhamento e deixa de existir um segundo grupo de gráficos ao qual uma série se possa ligar, por muitos registos de eixos que emita

Porque é que um segundo grupo de gráficos depois dos eixos não liga nada?

A gramática é toda a resposta, e está numa linha de ABNF. A regra CHARTFOMATS em [MS-XLS] 2.1.7.20.1 diz AxesUsed 1*2AXISPARENT e depois explicita AXISPARENT = AxisParent Begin Pos [AXES] 1*4CRT End. Leia essas duas produções em conjunto e a forma torna-se evidente: os grupos de gráficos são filhos de um grupo de eixos, não seus irmãos. Um substream de gráficos com dois pares de eixos e um grupo de gráficos final não é um gráfico de dois eixos com uma peculiaridade de layout; é um gráfico com um grupo de gráficos e um conjunto de registos de eixos órfãos. Isto importa porque SerToCrt ($1045), que fica dentro do bloco SERIESFORMAT, transporta um índice de grupo de gráficos baseado em zero, não um índice de eixos. Escrever crt = 1 quando só existe um bloco CRT aponta uma série para um grupo de gráficos que nunca foi emitido. A intuição que confunde as pessoas vem do nome do registo: AXESUSED ($1046) parece contar eixos, pelo que o passo seguinte natural é emitir mais eixos. Conta grupos de eixos, e cada grupo de eixos arrasta consigo uma área de plotagem e um grupo de gráficos completos

Marcar uma série para o grupo de eixos secundário

Do lado do HotXLS, isto reduz-se a um Boolean. TXLSChartSeriesInfo tem um campo SecondaryAxis, e defini-lo em qualquer série do array entregue a TXLSWorksheets.AddChartSheet muda todo o builder para o modo de dois grupos. Não existe uma chamada separada para "ativar eixo secundário" nem um parâmetro de contagem de eixos, porque a contagem pode ser derivada: se alguma 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';
    // ... preencher A1:C12 com categorias, receita e margem ...

    Series[0] := Default(TXLSChartSeriesInfo);   // nunca faça FillChar deste 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 passa a 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 geridos (os nomes WideString, o array dinâmico de trendline e os arrays de error bars) com membros Boolean simples e o Delphi só garante que os campos geridos são limpos por si. Deixe SecondaryAxis sem inicializar 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 a partir de um host de consola e um gráfico de dois eixos sob o test runner. Entretanto, os intervalos de categorias e valores são resolvidos através da tabela EXTERNSHEET do workbook antes de o builder sequer os ver — a mesma maquinaria de índices abordada em como o HotXLS classifica ligações externas BIFF SupBook e XTI — pelo que um intervalo que nomeie uma sheet desconhecida degrada para um placeholder BRAI vazio em vez de fazer o build falhar

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

O emitter muda de forma, não apenas de valor. Sem séries secundárias, AXESUSED contém 1 (ou 0 para pie e pie 3D, que não têm grupos de eixos) e segue-se um bloco AxisParent. Com uma, AXESUSED contém 2 e o builder executa o bloco duas vezes, com iax — a primeira palavra 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 predefinido, depois ChartFormat ($1014), o registo do tipo de gráfico, CrtLink ($1022) e dois marcadores End para fechar o grupo de gráficos e o grupo de eixos. A série secundária liga-se depois com SerToCrt crt = 1 e a primária mantém crt = 0. Um registo não é duplicado deliberadamente: a legenda é emitida apenas no primeiro grupo, porque o Excel dá a um gráfico uma legenda independentemente do número de grupos de eixos que transporta. Vale a pena declarar claramente outras duas propriedades. Reestruturar o emitter para colocar o grupo de gráficos dentro do bloco axis-parent não alterou a saída dos gráficos normais — sem séries secundárias, o substream é byte a byte idêntico à versão anterior, já que parametrizar AddAxisParent com iax = 0 é exatamente o antigo percurso de código. E o builder continua a emitir um par de eixos completo por grupo, pelo que um grupo secundário chega sempre com o seu próprio eixo de categorias mesmo quando só se interessa pela sua escala de valores

Como recupera a inspeção de gráficos a ligação ao grupo de eixos?

A leitura faz-se em duas passagens sobre a lista de registos e tem de ser assim porque AXESUSED chega antes dos blocos que descreve. A primeira passagem procura apenas $1046 e lê a sua primeira palavra como contagem de grupos de eixos. Esse valor começa em 1 e só sobe, nunca desce: o HotXLS usa o máximo entre a contagem atual e a declarada, pelo que um AXESUSED malformado ou duplicado não pode fazer regredir um gráfico que já tenha sido visto a declarar dois grupos. A segunda passagem acompanha o grupo de eixos atual, atualizando-o em cada AxisParent, e carimba esse índice em cada registo Axis que encontra até aparecer o AxisParent seguinte

var
  Model: TXLSChartModel;
  i: Integer;
begin
  // Sheets[1] é a worksheet de dados, Sheets[2] é 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 a pena nomear dois limites. TXLSChartModel.AxisGroupCount comunica o que o ficheiro declara, não quantos blocos AxisParent foram realmente encontrados; um ficheiro que diga 2 e forneça um bloco comunicará 2, e é em AxisCount que se nota. E TXLSChartAxis.AxisGroup é um carimbo posicional: regista em que bloco um eixo foi lido, que é a única coisa que o formato lhe diz. Do lado das séries, a descodificação de SerToCrt é condicionada por estar dentro de um bloco Series, porque o mesmo ID de registo aparece em contextos onde não é uma ligação de série e um decoder sem essa guarda substituiria alegremente a série errada

Verificar eixos secundários sem um ficheiro Excel real

A verificação aqui não precisou de um ficheiro Excel com um eixo secundário e essa é a parte útil da história. A descodificação estrutural é uma propriedade da sequência de registos, pelo que uma sequência sintetizada prova-a tão bem como uma capturada. A regressão constrói AXESUSED com um payload de 2, depois dois blocos AxisParent, cada um a envolver um eixo de categorias e um eixo de valores, e afirma que o modelo regressa com AxisGroupCount = 2, quatro eixos carimbados 0, 0, 1, 1 e os tipos de eixos esperados no segundo par

// Verificação estrutural sem envolver qualquer ficheiro Excel
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;

Dois apontamentos práticos se sintetizar registos por sua conta. TXLSCustomChart.AddData(RecID, Len, nil) desreferencia o payload quando Len é diferente de zero, pelo que os marcadores Begin ($1033) e End ($1034) têm de ser adicionados com comprimento zero, não com um blob nil e um comprimento obsoleto. E uma sequência sintética prova o decoder, nunca a aceitação do resultado pelo Excel — o lado da escrita foi estruturado a partir da ABNF e depois verificado fazendo round-trip por GetChartModel, com asserções dos carimbos por eixo 0/0/1/1 e dos grupos de gráficos por série 0/1, usando o percurso sem secundário byte a byte idêntico como rede de segurança. É a mesma postura conservadora em que assenta o resto do suporte de gráficos, imagens e drawings HotXLS para Delphi: descodificar o que os registos dizem e recusar adivinhar um layout binário que não tenha lido na especificação. A suite Delphi completa executou 1650 de 1650 em Win32 e Win64 depois de o lado da escrita entrar

Parâmetros de cena Chart3d e a armadilha fAuto

Dois detalhes adjacentes mordem quem vai além do gráfico predefinido. O primeiro é Chart3d ($103A, [MS-XLS] 2.4.46), um payload plano de 14 bytes emitido dentro do grupo de gráficos para variantes 3D: anRot (rotação, 0 a 360), anElev (elevação, com sinal, -90 a 90), pcDist (distância de perspetiva, 0 a 100, ignorada a menos que fPerspective esteja definido), pcHeight e pcDepth (percentagem 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 registo não imporá por si: num gráfico de barras transposto, anRot e anElev não podem exceder 44 e, num gráfico circular, anElev não pode ser negativo

O segundo é o bit fAuto e é ele que produz o relatório de bug "my colors were ignored". LineFormat ($1007), AreaFormat ($100A) e MarkerFormat ($1009) transportam todos fAuto no bit 0 do seu grbit e, quando esse bit está definido, o Excel aplica o seu estilo automático e trata os valores RGB explícitos, o estilo de linha, a forma do marcador e o tamanho do marcador junto dele como decoração. Qualquer emitter que escreva um estilo de série personalizado tem de limpar o bit 0; os emitters predefinidos mantêm-no definido precisamente para que o Excel escolha a paleta. Se estiver a editar um workbook existente em vez de construir um, as regras de preservação voltam a ser diferentes e estão cobertas em editar gráficos Excel sem perder ChartML preservado

Os grupos de eixos secundários, a ligação SerToCrt e o modelo de gráficos tipado aqui mostrado são distribuídos no componente de folhas de cálculo HotXLS para Delphi para Delphi e C++Builder, que lê e escreve gráficos BIFF8 sem o Excel instalado; a página do produto contém a referência completa dos registos de gráficos e a lista de overloads de AddChartSheet