HotXLS zapisuje vodopádové a treemap grafy Excelu jako samostatné části cx:chartSpace — xl/charts/chartExN.xml s content type application/vnd.ms-office.chartex+xml — a kotví je přes obal mc:AlternateContent, takže Excel 2016 a novější graf vykreslí, zatímco starší čtečky dostanou fallback tvar. Zvolte xlsxChartWaterfall nebo xlsxChartTreemap a celá pipeline částí běží sama
Takhle se tenhle problém obvykle ohlásí — support ticketem. Zákazník pošle workbook, který jeho finanční tým postavil v Excelu 2019, váš reportovací nástroj ho otevře, uloží a vodopádový graf je pryč. Žádná výjimka, žádné varování, nic v logu — graf prostě není ve výstupu. Když ho půjdete hledat do klasické cesty chart kódu, budete hledat dlouho, protože tam nikdy nebyl
Proč vodopádové a treemap grafy nejsou v c:chartSpace?
Protože je tam Microsoft nemohl dát. Schéma DrawingML chart v ECMA-376 Part 1 §21.2 vyjmenovává prvky grafu — c:barChart, c:lineChart, c:pieChart a ostatní — jako uzavřenou volbu, takže přidat v Excelu 2016 waterfall, treemap, sunburst a funnel by znamenalo rozbít schéma, na kterém závisí každá existující čtečka. Nové typy šly místo toho do paralelního dokumentu: část cx:chartSpace v namespace chartex z roku 2014, http://schemas.microsoft.com/office/drawing/2014/chartex, uložená vedle klasických grafů, ale strukturálně s nimi nesouvisející. Kreslicí strana sešívá oba světy přes mc:AlternateContent, kompatibilní mechanismus z ECMA-376 Part 3: větev mc:Choice deklaruje Requires="cx1" a drží xdr:graphicFrame, jehož a:graphicData/@uri je namespace chartex a jehož payload je holý odkaz <cx:chart r:id="rIdN"/>, zatímco mc:Fallback drží obdélník s upozorněním, které tam Excel sám píše. Čtečka, která nerozumí cx1, je kontraktem Part 3 povinna vzít fallback
Typ vztahu je druhé prozrazení. Klasický graf visí na kreslicí části se standardním OOXML chart vztahem; část chartEx místo toho používá http://schemas.microsoft.com/office/2014/relationships/chartEx, dokumentovaný v [MS-ODRAWXML]. HotXLS vystavuje oba světy jako konstanty — XlsxRtChartEx pro ten vztah, XlsxCtChartEx pro content type, XlsxNsMsChartEx a XlsxNsMsChartEx2015 pro dva namespace, které kotva potřebuje — a predikát XlsxChartIsChartEx je to, co při uložení nasměruje graf do rozšířené cesty. Kombinační grafy a zachovaná klasická chart XML jdou tou druhou, popsanou v článku o zachované chart XML a kombinačních grafech
Čtyři pravidla schématu, která rozhodují, jestli Excel soubor otevře
Schéma chartex je na čtyřech konkrétních místech přísnější než klasické a každé z nich při chybě vyprodukuje stejnou nepoužitelnou výzvu k opravě. BuildChartExKnownXml a BuildChartExRelsXml v HotXLS mají všechny čtyři zapracované, ale stojí za to je znát, pokud XML někdy zkoumáte ručně
- Pořadí.
cx:chartDatamusí být v dokumentu předcx:chart. To je opak intuice, kterou si stavíte z klasických grafů, kde cache žijí uvnitř série uvnitř plot area; v části chartEx sedí data vpředu ve vlastním bloku a tělo grafu se na ně jen odkazuje id - Kardinalita.
cx:plotAreamůže obsahovat přesně jedencx:plotAreaRegion. Ne nula, ne dva. Žádný příběh kombinačního grafu tady není — jedna oblast a série uvnitř ní - Umístění typu. Typ grafu vůbec není jméno elementu. Je to atribut
layoutIdnacx:series, přijímající token z enumeraceST_SeriesLayoutv Microsoft chartex schématu.XlsxChartExLayoutIdmapuje HotXLS enum na ten token a vrací'waterfall'nebo'treemap';XlsxChartExLayoutIdToTypespouští mapování pozpátku při čtení - Vztah, který klasické grafy nikdy nepotřebují.
cx:chartDatadrží element<cx:externalData r:id="rId1"/>a ten vztah musí existovat vxl/charts/_rels/chartExN.xml.relsa ukazovat zpět na worksheet, který data vlastní, protože Excel ho používá k rozlišení range formulícx:f. Vynechejte rels část a graf se otevře prázdný a nic nevysvětlí proč
Jak přidáte vodopádový nebo treemap graf v Delphi?
Stejně jako přidáváte jakýkoli jiný graf v HotXLS — rozšířené typy jsou členy TXLSXChartType, takže je TXLSXWorksheet.AddChart přijme přímo a všechno níže se odvíjí od hodnoty enumu. Žádné samostatné chartEx API se učit nemusíte
var
Book: TXLSXWorkbook;
Sheet: TXLSXWorksheet;
Chart: TXLSXChart;
begin
Book := TXLSXWorkbook.Create;
try
Book.Open('cashflow.xlsx');
Sheet := Book.Sheets[0];
// Kotva v řádkových a sloupcových souřadnicích, stejně jako klasický graf
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;
Při uložení HotXLS vidí XlsxChartIsChartEx vracet True, alokuje jméno části z odděleného chartEx čítače a zapíše xl/charts/chartEx1.xml, jeho rels soubor, content-type override a kotvu mc:AlternateContent produkovanou funkcí XlsxChartExAnchorPayloadXml. Ten nezávislý čítač je důležitý: workbook se dvěma klasickými grafy a jedním waterfall dostane chart1.xml, chart2.xml a chartEx1.xml, ne chartEx3.xml. Treemapy se pak od waterfall liší ve dvou detailech, oba obslouží stejný builder. Číselná dimenze mění typ — cx:numDim type="val" pro waterfall se pro treemap mění na type="size", protože obdélník treemapu kóduje velikost jako plochu, ne jako vynesenou hodnotu — a treemapy obvykle chtějí hierarchii kategorií, kterou model vyjadřuje přes TXLSXChartSeries.CategoriesMultiLevel. Nastavte ten příznak a S.ResolveCategoryLevels při zápisu přečte range z worksheetu, rozdělí ho na jeden TXLSXChartDataCache na úroveň za S.CategoryLevelCount a S.CategoryLevels[], a BuildChartExLevelXml emituje jeden cx:lvl na úroveň uvnitř jediného cx:strDim, nejdřív úroveň listu — stejné fyzické pořadí, jaké ECMA-376 Part 1 §21.2 používá pro c:multiLvlStrCache, takže se konvence přenáší, i když se žádný markup nepřenáší
Chart := Sheet.AddChart(xlsxChartTreemap, 'Revenue by Region',
2, 8, 24, 16);
S := Chart.AddSeries('Revenue',
'Sheet1!$A$2:$B$40', // dva sloupce: popisek listu, pak nadřazená skupina
'Sheet1!$C$2:$C$40');
S.CategoriesMultiLevel := True;
Tvar cache, který rozbije každý předpoklad z klasických grafů
Pokud jste psali čtečku cache klasických grafů, tři návyky vás na části chartEx zradí a všechny tři pramení z jednoho rozhodnutí: chartex ukládá do atributů a obsahu elementu to, co DrawingML ukládá do child elementů
- Cache bod je
<cx:pt idx="0">42</cx:pt>. Hodnota je obsah elementu. Není tu žádný childc:v, do kterého by se sestoupilo, takže parser chodící za uzlyvnic nenajde a ohlásí prázdnou cache ptCountaformatCodejsou atributy nacx:lvl. V klasickémc:numCachejsou oba child elementy- Více úrovní hierarchie je více elementů
cx:lvluvnitř jednohocx:strDim. Neexistuje žádný samostatný element multi-level cache tak, jak jec:multiLvlStrCacheoddělený odc:strCache
BuildChartExLevelXml zapisuje ten tvar a ParseChartExXml ho čte zpět, přičemž každou hodnotu na cestě ven prohání přes XlsxEscapeText, takže popisek kategorie obsahující ampersand nebo špičatou závorku přežije round trip. Stojí za zmínku, co builder dělá, když číselná úroveň nemá format code: zapíše formatCode="General" místo vynechání atributu, protože tady atribut není volitelný tak, jak je jeho klasický protějšek
Namespace prefix, který rozbil zpětné čtení typu
První verze chartEx parseru HotXLS psala naprosto správné soubory a každý z nich přečetla zpět jako sloupcový graf. Kořenová příčina byl jeden řádek a poučení zobecňuje na každý OOXML parser, který kdy napíšete: TXMLReader.Name vrací jméno tak, jak je zapsané v dokumentu, včetně prefixu. V klasické chart části přichází většina elementů bez prefixu, takže SameText(Reader.Name, 'series') funguje a přestanete o tom přemýšlet. V chartEx části má prefix každý element — čtečka vám předá 'cx:series', porovnání tiše selže na všech a nedostanete žádnou výjimku ani špatně utvořené XML, jen prázdný seznam sérií a typ grafu padající na default. Oprava spočívá v porovnání lokálního jména, na jednom místě, ne na každém call site
// Reader.Name nese svůj namespace prefix: 'cx:series', ne '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 používá ten helper pro každý test elementu a ekvivalentní helper AttrVal pro atributy, protože i cx:autoUpdate a spol. mají prefix. Jakmile se elementy sérií skutečně vidí, typ se vrátí z prvního nalezeného layoutId a každá cx:series se propojí se svým blokem cx:data spárováním cx:dataId/@val s cx:data/@id. Grafy otevřené, ale nikdy nezměněné jdou místo toho starší cestou: původní bajty se přehrají z FRawChartXml, takže nedotčený soubor zákazníka vyjde ven tak, jak vešel — stejná disciplína konzervativního zapisovače, která řídí rozšíření na úrovni sheetu v článku o sparkline rozšíření
Co HotXLS s těmito grafy záměrně nedělá
Dvě hranice stojí za to říct jasně, protože obě jsou volby, ne mezery čekající na zaplnění. Vestavěné PDF a SVG renderery nekreslí vodopádové ani treemap série: typy neodpovídají žádné kreslicí větvi, takže vykreslení série se přeskočí a zbytek sheetu se exportuje normálně — předstírat věrohodně vypadající treemap v exportovaném PDF by bylo horší než nekreslit nic, protože by to tiše nesouhlasilo s tím, co Excel ukazuje pro stejný soubor. A xlsxChartWaterfall a xlsxChartTreemap jsou jediné dva rozšířené layouty, které HotXLS staví z modelu; ST_SeriesLayout definuje taky sunburst, funnel, box-and-whisker a histogram, a workbook obsahující kterýkoli z nich pořád projde round trip cestou přehrávání raw XML, jen ho dnes z Delphi kódu nenapíšete. Vykreslování klasických typů grafů se nemění a je pokryté v článku o grafech, obrázcích a kresbách a chartEx pipeline se dodává vedle něj v HotXLS Delphi Excel Component pro Delphi a C++Builder