Artigo Técnico

Editar Gráficos do Excel Sem Perder ChartML no HotXLS

Uma parte de gráfico salva por um build atual do Excel raramente para nos elementos que o schema de gráficos da ECMA-376 define. O chart space costuma abrir com um bloco mc:AlternateContent que oferece c14:style a leitores mais novos e um c:style simples aos mais velhos, cada série carrega um c:extLst com um c16:uniqueId, e rótulos de dados "Value From Cells" vivem em mais uma extensão. Um programa que interpreta só o que modela e regenera a parte no momento do save não grava nada disso de volta. Mude uma palavra no título, reabra o arquivo no Excel, e os rótulos que puxavam texto de um intervalo de células se foram

O conserto é parar de tratar um save como uma reconstrução. Guarde os bytes com que o gráfico chegou, escreva-os de volta sem mudanças quando nada modelado se mexeu, e quando algo se mexeu, mescle os elementos novos na árvore original para que as partes desconhecidas fiquem onde o Excel as pôs

Reexecute o ChartML original quando nada mudou

O HotXLS faz exatamente isso quando o TXLSXWorkbook.PreserveUnsupportedParts é True, que é o padrão em uma pasta de trabalho recém-criada. Na importação, cada gráfico guarda seus bytes UTF-8 originais junto com o comprimento e uma impressão digital de 64 bits do conteúdo que o modelo tipado representa. No save, o HotXLS regenera a parte modelada, compara comprimento e impressão digital, e se ambos batem ele grava os bytes guardados de volta verbatim: sem reordenação de elementos, sem deriva de whitespace, e sem uma segunda cópia UTF-16 de cada gráfico intocado na memória

Como a comparação roda sobre conteúdo, atribuir a uma propriedade o valor que ela já tem não muda nada. Os setters do TXLSXChart saem cedo em um valor igual, então um laço que "atualiza" cada título de gráfico a partir de uma linha de banco de dados ainda reexecuta todo gráfico cujo título já estava correto. A formatação de linha de barras de erro e trendlines é rastreada por sua própria flag de mudança, e mudá-la conta sempre como edição

Diagrama da preservação de ChartML do HotXLS em Delphi em que gráficos inalterados reproduzem os bytes originais e gráficos editados fundem nós tipados do modelo no XML preservado, enquanto uma reconstrução só do modelo perde os ramos extLst e AlternateContent
HotXLS armazena os bytes originais de ChartML na importação e os reexecuta intocados, ou mescla edições tipadas na árvore preservada quando o modelo mudou

O TXLSXChart.HasPreservedXml diz em qual caminho um gráfico está antes de você editar qualquer coisa. Ele reporta True quando o ChartML original está ligado ao gráfico, que é a pré-condição tanto para a reexecução quanto para o merge

var
  Workbook: TXLSXWorkbook;
  Chart: TXLSXChart;
begin
  Workbook := TXLSXWorkbook.Create;
  try
    // True por padrão; defina explicitamente para a intenção sobreviver a refatorações.
    // Precisa estar no lugar antes do Open, porque o Open constrói o estado
    Workbook.PreserveUnsupportedParts := True;
    Workbook.Open('quarterly-review.xlsx');

    Chart := Workbook.Sheets[1].Charts[0];   // Sheets[] tem base 1, Charts[] base 0
    if not Chart.HasPreservedXml then
      Log('chart 0 has no original ChartML attached');   // Log é a sua própria rotina

    Chart.Title := Chart.Title;                  // valor igual: ainda reexecuta
    Workbook.Sheets[1].Charts[1].Title :=
      'Revenue by region, Q3 2026';              // este gráfico segue o caminho de merge
    Workbook.SaveAs('quarterly-review-out.xlsx');
  finally
    Workbook.Free;
  end;
end;

O que sobrevive a um merge quando você edita um gráfico?

A resposta curta: tudo o que o modelo tipado não é dono. Título, séries, caches de dados, plot groups e eixos pertencem ao modelo, então a árvore recém-gerada decide eles. Atributos não modelados, declarações de namespace, estilos, listas de extensão e nós de formatação vêm da árvore original e são reinseridos ao lado dos nós correspondentes a que ficavam juntos

Apague uma série e o merge não pode ressuscitá-la do XML preservado só porque o original ainda contém um c:ser naquela posição. Tudo o que o modelo controla vem do modelo, mesmo quando a resposta do modelo é "sumiu". O mesmo vale em nível de atributo: quando você limpa negrito, itálico, sublinhado, tachado ou o tamanho da fonte de um estilo de texto conhecido, o merge lê o atributo ausente como uma limpeza e não copia b ou sz de volta do defRPr original. Atributos desconhecidos no mesmo elemento mantêm o lugar deles

Títulos seguem a mesma regra. Na importação, o HotXLS mantém os runs rich nativos do c:title, com as fontes, tamanhos e tags de idioma de cada run, e os reexecuta enquanto o texto estiver intocado. Atribua um Title novo e aquele conteúdo rich é descartado; o título é reescrito como um único run estilizado a partir do TitleTextStyle. Mesclar runs velhos em texto novo significaria adivinhar a qual run as palavras novas pertencem, e um palpite aqui produz títulos que ninguém digitou

A ordem do schema é preservada onde o Office é exigente com ela. Um extLst permanece o último filho do pai, como o schema exige. Blocos de markup-compatibility recebem tratamento próprio: o leitor pula o mc:Choice e lê o mc:Fallback, e quando esse fallback envolve um plot group que o modelo conhece, o merge grava o conteúdo tipado novo de volta na subárvore do fallback. Um merge ingênuo emitiria o plot group editado e deixaria a cópia velha dentro do ramo do fallback, então o gráfico salvo carregaria o mesmo grupo duas vezes

Desligando um gráfico com ClearPreservedXml

O TXLSXChart.ClearPreservedXml descarta o XML original e a impressão digital de um gráfico, e o próximo save reconstrói aquele gráfico puramente do modelo tipado. É uma decisão por gráfico. O PreserveUnsupportedParts continua governando se o estado de preservação é construído na importação, e ele também cobre outras partes de pacote não suportadas, não só gráficos, razão pela qual desligá-lo para obter um gráfico limpo é a alavanca errada

Limpe o gráfico de um gráfico quando descartar é o propósito da operação, por exemplo um gráfico de template que ainda carrega formatação de um tema corporativo aposentado. Deixe em paz como reflexo de depuração. Um gráfico que renderiza errado depois de uma edição costuma ser uma pergunta de modelagem, e limpar o XML preservado joga fora a única cópia do que o Excel escreveu originalmente

Um caso pede uma checagem antes. Se uma barra de erro ou uma trendline usa formatação de linha que o modelo tipado não consegue representar, a propriedade LineAppearanceSupported dela é False; um rótulo de trendline com formatação fora do modelo reporta o mesmo via LabelAppearanceSupported. O HotXLS não substitui silenciosamente nenhum dos dois por uma versão mais simples: uma vez que o gráfico não tem XML preservado para recorrer, o save para com um EArgumentException citando a aparência de linha ou rótulo não representada. Testar as duas antes de limpar leva um laço

// Helper seu, não faz parte do HotXLS: True quando toda linha de barra de erro,
// linha de trendline e rótulo de trendline podem ser escritos só do modelo tipado
function ChartRebuildable(Chart: TXLSXChart): Boolean;
var
  I, T: Integer;
  S: TXLSXChartSeries;
begin
  Result := False;
  for I := 0 to Chart.SeriesCount - 1 do    // atravessa todos os plot groups
  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      // o próximo save reconstrói este gráfico a partir do modelo
else
  Log('kept original ChartML for this chart');

Gráficos de combinação com AddPlotGroup e um eixo secundário

O TXLSXChart.AddPlotGroup anexa um plot group ordenado com sua própria família de gráfico e sua própria atribuição de eixo primário ou secundário. Isso cobre o gráfico que todo time de finanças pede: receita como colunas no eixo da esquerda, percentual de margem como linha na direita. Cada gráfico expõe ValueAxis, CategoryAxis, SecondaryValueAxis, SecondaryCategoryAxis e SeriesAxis, com configurações tipadas para posição, identificadores de eixo, cruzamento, unidades de data, intervalos de rótulos, formato numérico e visibilidade

A API plana dos gráficos de tipo único continua lá e mapeia para os grupos. ChartType e AddSeries endereçam o primeiro plot group; SeriesCount e Series[] percorrem as séries de todos os grupos em ordem, razão pela qual o helper acima enxerga a série de linha também. A ordem importa em um ponto. O TXLSXCharts.Add cria um gráfico sem plot group nenhum, e o que rodar primeiro entre AddSeries e AddPlotGroup cria o grupo um. Chame AddPlotGroup(xlsxChartLine, xlsxAxisSecondary) primeiro em um gráfico novo e a linha vira o grupo um, o tipo do gráfico troca para linha, e a série de receita que você adicionar depois cai nele. Criar os dois grupos explicitamente remove a ambiguidade, e é o que o exemplo faz

var
  Chart: TXLSXChart;
  Columns, Line: TXLSXChartPlotGroup;
begin
  // Ancorado sobre as linhas 1..15, colunas 5..12; nenhum plot group existe ainda
  Chart := Sheet.Charts.Add(xlsxChartColumn, 'Revenue and margin',
    1, 5, 15, 12);
  Columns := Chart.AddPlotGroup(xlsxChartColumn);              // grupo 1, primário
  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;                     // desenha a linha na frente das colunas

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

  // Área de plotagem como frações do gráfico: x, y, largura e altura em uma chamada
  Chart.PlotAreaLayout.SetPositionAndSize(0.08, 0.12, 0.80, 0.72);
end;
Diagrama de um gráfico de combinação do HotXLS no Delphi com colunas de receita do AddSeries no eixo de valores primário e uma linha de margem do AddPlotGroup no eixo de valores secundário, enquanto ChartType e AddSeries alcançam apenas o grupo um
Cada chamada AddPlotGroup anexa um grupo ordenado com sua própria família de gráfico e atribuição de eixo primário ou secundário, enquanto ChartType e AddSeries ainda endereçam o grupo um

O ZOrder em um plot group controla o empilhamento: o writer de gráficos ordena os grupos por ele antes de emitir os blocos c:barChart e c:lineChart, valores menores atrás. Como todo grupo ordenado é modelado, abrir e salvar de novo um gráfico de combinação mantém cada família de gráfico junto com os eixos secundário, de data e de séries dela. Referências de fórmula de gráfico e valores de série em cache sobrevivem a cópias da pasta de trabalho e ciclos de abrir/salvar, e âncoras de gráfico são casadas com as partes de gráfico pelo identificador de relationship, então um pacote cujas entradas de relationship foram reordenadas por outra ferramenta ainda põe cada gráfico na posição de planilha dele

Por que o Office ignora um layout manual?

Porque as coordenadas chegaram incompletas. O Office exige x, y, w e h como um grupo completo sempre que qualquer um deles está presente em um c:manualLayout, e ignora o elemento inteiro caso contrário. O schema marca os quatro como opcionais, então um arquivo com só x e y é válido e ainda assim não faz nada no Excel

O TXLSXChartLayout.SetPositionAndSize é a chamada a usar. Ele define os quatro valores, marca os quatro elementos de modo como presentes e habilita o layout em um passo. As propriedades individuais X, Y, Width e Height definem um valor cada, e o HotXLS grava exatamente os elementos presentes, do mesmo jeito que mantém a presença de cada um dos nove filhos opcionais quando lê um arquivo. Isso mantém um layout intocado fiel à origem. Também significa que um layout construído a partir de duas atribuições de propriedade é gravado com duas coordenadas, e o Office vai pular ele. A mesma classe TXLSXChartLayout fica atrás de PlotAreaLayout, TitleLayout, LegendLayout e do TitleLayout de cada eixo

Diagrama da regra de layout manual do HotXLS no Delphi em que x, y, w e h precisam chegar como um grupo completo ou o Office ignora o elemento manualLayout inteiro, com modos edge e factor e o layoutTarget limitado à área de plotagem
O Office aplica um layout manual só quando x, y, w e h chegam como um grupo completo, e é por isso que o SetPositionAndSize grava os quatro em uma única chamada

Dois padrões seguem o que o Excel faz em vez de tudo o que a especificação permite. Um layout novo usa edge para XMode e YMode e factor para WidthMode e HeightMode; o padrão permite as outras combinações, mas o Excel salva posições como edges e tamanhos como factors e converte qualquer outra coisa quando salva. E o layoutTarget (Target, interno ou externo) é gravado para a área de plotagem, enquanto layouts de título, título de eixo e legenda o omitem, porque as notas de implementação da Microsoft para o schema de gráficos não o permitem sob esses elementos

Encaixando isso numa ida e volta de pasta de trabalho

A preservação de gráficos é um caso de uma regra mais ampla: uma biblioteca que edita documentos criados por outros deve mudar o que lhe pediram para mudar e deixar o resto como encontrou. O HotXLS aplica o mesmo raciocínio a temas, cadeias de cálculo e extensões de planilha, cobertos em round trips sem perdas para theme, extLst e calcChain, e a projetos VBA e links externos em preservação de VBA e links externos. Para criar gráficos, posicionar imagens e desenhar formas do zero, veja gráficos, imagens e drawings no HotXLS

O HotXLS lê, edita e grava gráficos XLSX de código Delphi e C++Builder nativo sem Excel instalado na máquina, então a fidelidade de gráficos é decidida pela biblioteca e não por um servidor de automação. A referência da API de gráficos e um build de avaliação estão na página do componente de planilhas HotXLS para Delphi