A HotXLS escreve gráficos waterfall e treemap do Excel como parts cx:chartSpace autônomas — xl/charts/chartExN.xml carregando o content type application/vnd.ms-office.chartex+xml — e os ancora por um wrapper mc:AlternateContent para que o Excel 2016 em diante renderize o gráfico enquanto leitores antigos recebem uma shape de fallback. Escolha xlsxChartWaterfall ou xlsxChartTreemap e o pipeline de parts inteiro segue junto
O jeito como esse problema costuma se anunciar é um ticket de suporte. Um cliente manda uma pasta de trabalho que o time financeiro dele montou no Excel 2019, sua ferramenta de relatório abre, salva, e o gráfico waterfall sumiu. Sem exceção, sem aviso, nada no log — o gráfico simplesmente não está na saída. Se você for procurá-lo no caminho clássico de gráficos, vai procurar por muito tempo, porque o gráfico nunca esteve lá para começar
Por que os gráficos waterfall e treemap não ficam em c:chartSpace?
Porque a Microsoft não tinha onde colocá-los. O schema de gráficos DrawingML na ECMA-376 Parte 1 §21.2 enumera os elementos de gráfico dele — c:barChart, c:lineChart, c:pieChart e o resto — como uma choice fechada, então adicionar waterfall, treemap, sunburst e funnel no Excel 2016 teria significado quebrar um schema do qual todo leitor existente depende. Os tipos novos foram para um documento paralelo: uma part cx:chartSpace no namespace chartex de 2014, http://schemas.microsoft.com/office/drawing/2014/chartex, guardada ao lado dos gráficos clássicos mas estruturalmente sem relação com eles. O lado do drawing costura os dois mundos com mc:AlternateContent, o mecanismo de compatibilidade da ECMA-376 Parte 3: o ramo mc:Choice declara Requires="cx1" e guarda um xdr:graphicFrame cujo a:graphicData/@uri é o namespace chartex e cujo payload é uma referência <cx:chart r:id="rIdN"/> pura, enquanto mc:Fallback guarda um retângulo com o aviso que o próprio Excel escreve ali. Um leitor que não entende cx1 é obrigado pelo contrato da Parte 3 a seguir o fallback
O tipo de relacionamento é o outro sinal. Um gráfico clássico pende da part de drawing com o relacionamento OOXML padrão de gráfico; uma part chartEx usa http://schemas.microsoft.com/office/2014/relationships/chartEx no lugar, documentado em [MS-ODRAWXML]. A HotXLS expõe os dois mundos como constantes — XlsxRtChartEx para esse relacionamento, XlsxCtChartEx para o content type, XlsxNsMsChartEx e XlsxNsMsChartEx2015 para os dois namespaces que a âncora precisa — e o predicado XlsxChartIsChartEx é o que roteia um gráfico pelo caminho estendido no save. Gráficos combinados e XML de gráfico clássico preservado vão pelo outro, descrito no artigo sobre XML de gráfico preservado e gráficos combinados
Quatro regras de schema que decidem se o Excel abre o arquivo
O schema chartex é mais estrito que o clássico em quatro lugares específicos, e cada um deles produz o mesmo prompt de reparo inútil quando você erra. BuildChartExKnownXml e BuildChartExRelsXml na HotXLS assam os quatro, mas vale conhecê-los se você algum dia inspecionar o XML na mão
- Ordem.
cx:chartDatatem que vir antes decx:chartno documento. É o inverso da intuição que você constrói com gráficos clássicos, onde os caches vivem dentro da série dentro da plot area; numa part chartEx os dados ficam na frente num bloco próprio e o corpo do gráfico só os referencia por id - Cardinalidade.
cx:plotAreapode conter exatamente umcx:plotAreaRegion. Nem zero, nem dois. Não existe história de gráfico combinado aqui — uma região, e as séries dentro dela - Colocação do tipo. O tipo de gráfico não é nome de elemento nenhum. É o atributo
layoutIdemcx:series, tomando um token da enumeraçãoST_SeriesLayoutno schema chartex da Microsoft.XlsxChartExLayoutIdmapeia a enum da HotXLS nesse token e retorna'waterfall'ou'treemap';XlsxChartExLayoutIdToTyperoda o mapeamento ao contrário na leitura - O relacionamento que gráficos clássicos nunca precisam.
cx:chartDataguarda um elemento<cx:externalData r:id="rId1"/>, e esse relacionamento tem que existir emxl/charts/_rels/chartExN.xml.relsapontando de volta para a worksheet que é dona dos dados, porque o Excel o usa para resolver as fórmulas de rangecx:f. Omita a part de rels e o gráfico abre vazio sem nada explicando o porquê
Como você adiciona um gráfico waterfall ou treemap em Delphi?
Do mesmo jeito que você adiciona qualquer outro gráfico na HotXLS — os tipos estendidos são membros de TXLSXChartType, então TXLSXWorksheet.AddChart os aceita direto e tudo a jusante se baseia no valor da enum. Não existe uma API chartEx separada para aprender
var
Book: TXLSXWorkbook;
Sheet: TXLSXWorksheet;
Chart: TXLSXChart;
begin
Book := TXLSXWorkbook.Create;
try
Book.Open('cashflow.xlsx');
Sheet := Book.Sheets[0];
// Retângulo de âncora em coordenadas de linha/coluna, igual a um gráfico clássico
Chart := Sheet.AddChart(xlsxChartWaterfall, 'Q3 Cash Flow',
2, 5, 20, 12);
Chart.AddSeries('Movement', 'Sheet1!$A$2:$A$9', 'Sheet1!$B$2:$B$9');
Book.SaveAs('cashflow-waterfall.xlsx');
finally
Book.Free;
end;
end;
No save, a HotXLS vê XlsxChartIsChartEx retornar True, aloca um nome de part de um contador chartEx separado, e escreve xl/charts/chartEx1.xml, o arquivo de rels dele, o override de content type, e a âncora mc:AlternateContent produzida por XlsxChartExAnchorPayloadXml. O contador independente importa: uma pasta de trabalho com dois gráficos clássicos e um waterfall fica com chart1.xml, chart2.xml e chartEx1.xml, não chartEx3.xml. Treemaps então diferem de waterfalls em dois detalhes, os dois tratados pelo mesmo builder. A dimensão numérica muda de tipo — cx:numDim type="val" para um waterfall vira type="size" para um treemap, porque um retângulo de treemap codifica magnitude como área em vez de como valor plotado — e treemaps normalmente querem uma hierarquia de categorias, que o modelo expressa por TXLSXChartSeries.CategoriesMultiLevel. Ative essa flag e S.ResolveCategoryLevels lê o range da worksheet no momento da escrita, divide-o em um TXLSXChartDataCache por nível atrás de S.CategoryLevelCount e S.CategoryLevels[], e BuildChartExLevelXml emite um cx:lvl por nível dentro de um único cx:strDim, nível folha primeiro — a mesma ordenação física que a ECMA-376 Parte 1 §21.2 usa para c:multiLvlStrCache, então essa convenção sobrevive mesmo que nenhum markup sobreviva
Chart := Sheet.AddChart(xlsxChartTreemap, 'Revenue by Region',
2, 8, 24, 16);
S := Chart.AddSeries('Revenue',
'Sheet1!$A$2:$B$40', // duas colunas: rótulo da folha, depois o grupo pai
'Sheet1!$C$2:$C$40');
S.CategoriesMultiLevel := True;
A forma de cache que quebra toda suposição vinda de gráficos clássicos
Se você já escreveu um leitor de cache de gráfico clássico, três hábitos vão te trair numa part chartEx, e os três vêm de uma decisão de design: chartex guarda em atributos e no conteúdo de elementos o que DrawingML guarda em elementos filhos
- Um ponto cacheado é
<cx:pt idx="0">42</cx:pt>. O valor é o conteúdo do elemento. Não existe filhoc:vpara descer, então um parser que percorre procurando nósvnão acha nada e reporta um cache vazio ptCounteformatCodesão atributos emcx:lvl. Numc:numCacheclássico os dois são elementos filhos- Múltiplos níveis de hierarquia são múltiplos elementos
cx:lvldentro de umcx:strDim. Não existe um elemento de cache multinível separado do jeito quec:multiLvlStrCacheé separado dec:strCache
BuildChartExLevelXml escreve essa forma e ParseChartExXml a lê de volta, passando todo valor por XlsxEscapeText na saída para que um rótulo de categoria contendo um ampersand ou um colchete angular sobreviva ao round trip. Vale notar o que o builder faz quando um nível numérico não carrega format code: ele escreve formatCode="General" em vez de descartar o atributo, porque aqui o atributo não é opcional do jeito que o equivalente clássico é
O prefixo de namespace que fez a leitura do tipo falhar
A primeira versão do parser chartEx da HotXLS escrevia arquivos perfeitamente bons e lia todos de volta como gráfico de colunas. A causa raiz era uma linha, e a lição generaliza para qualquer parser OOXML que você for escrever: TXMLReader.Name retorna o nome como escrito no documento, prefixo incluído. Numa part de gráfico clássico a maioria dos elementos chega sem prefixo, então SameText(Reader.Name, 'series') funciona e você para de pensar nisso. Numa part chartEx todo elemento é prefixado — o reader te entrega 'cx:series', a comparação falha silenciosamente em todos, e você não ganha exceção nem XML malformado, só uma lista de séries vazia e um tipo de gráfico que cai no default. A correção é comparar no local name, num lugar só em vez de em todo call site
// Reader.Name carrega o prefixo de namespace: 'cx:series', não 'series'
function IsEl(const LocalName: string): Boolean;
var
NodeName: string;
begin
NodeName := String(Reader.Name);
Result := SameText(NodeName, LocalName) or
EndsText(':' + LocalName, NodeName);
end;
ParseChartExXml usa esse helper em todo teste de elemento e um helper AttrVal equivalente para atributos, porque cx:autoUpdate e companhia também são prefixados. Uma vez que os elementos de série estão sendo vistos de verdade, o tipo volta do primeiro layoutId encontrado, e cada cx:series é ligado ao bloco cx:data dele casando cx:dataId/@val com cx:data/@id. Gráficos abertos mas nunca modificados tomam o caminho antigo no lugar: os bytes originais são reproduzidos de FRawChartXml, então um arquivo de cliente intocado sai como entrou — a mesma disciplina de writer conservador que rege extensões de nível de planilha no artigo sobre a extensão de sparklines
O que a HotXLS deliberadamente não faz com esses gráficos
Dois limites valem ser ditos claramente, porque os dois são escolhas em vez de lacunas esperando preenchimento. Os renderizadores PDF e SVG embutidos não desenham séries waterfall nem treemap: os tipos não casam com nenhum ramo de drawing, então a renderização de série é pulada e o resto da planilha exporta normalmente — fingir um treemap plausível num PDF exportado seria pior que não desenhar nada, porque ele discordaria silenciosamente do que o Excel mostra para o mesmo arquivo. E xlsxChartWaterfall e xlsxChartTreemap são os únicos dois layouts estendidos que a HotXLS constrói a partir do modelo; ST_SeriesLayout também define sunburst, funnel, box-and-whisker e histogram, e uma pasta de trabalho contendo qualquer um deles ainda faz round trip pelo caminho de replay do XML cru, só que você não consegue criar um a partir de código Delphi hoje. A renderização dos tipos clássicos de gráfico está inalterada e é coberta no artigo sobre gráficos, imagens e drawings, e o pipeline chartEx acompanha ela no HotXLS Delphi Excel Component para Delphi e C++Builder