HotXLS zapisuje wykresy wodospadowe i treemap w Excelu jako samodzielne części cx:chartSpace — xl/charts/chartExN.xml z typem treści application/vnd.ms-office.chartex+xml — i kotwiczy je przez wrapper mc:AlternateContent, żeby Excel 2016 i nowszy renderował wykres, a starsze czytniki dostały kształt zastępczy. Wybierasz xlsxChartWaterfall albo xlsxChartTreemap i cały pipeline części jedzie za tym
Ten problem zwykle ogłasza się ticketem wsparcia. Klient przysyła skoroszyt, który ich zespół finansowy zbudował w Excelu 2019, twoje narzędzie raportowe go otwiera, zapisuje, a wykres wodospadowy znika. Żadnego wyjątku, żadnego ostrzeżenia, nic w logu — wykres po prostu nie ma go w wyjściu. Jeśli pójdziesz go szukać w klasycznej ścieżce kodowej wykresów, będziesz szukał długo, bo wykres nigdy tam nie był
Dlaczego wykresów wodospadowych i treemap nie ma w c:chartSpace?
Bo Microsoft nie mógł ich tam dodać. Schemat wykresów DrawingML w ECMA-376 Part 1 §21.2 wylicza swoje elementy wykresów — c:barChart, c:lineChart, c:pieChart i resztę — jako zamknięty wybór, więc dodanie wodospadu, treemap, sunburst i funnel w Excelu 2016 oznaczałoby złamanie schematu, na którym polega każdy istniejący czytnik. Nowe typy trafiły zamiast tego do równoległego dokumentu: części cx:chartSpace w przestrzeni nazw chartex z 2014, http://schemas.microsoft.com/office/drawing/2014/chartex, składowanej obok klasycznych wykresów, ale strukturalnie z nimi niepowiązanej. Warstwa rysowania zszywa oba światy przez mc:AlternateContent, mechanizm kompatybilności z ECMA-376 Part 3: gałąź mc:Choice deklaruje Requires="cx1" i trzyma xdr:graphicFrame, którego a:graphicData/@uri to przestrzeń nazw chartex, a payload to gołe odwołanie <cx:chart r:id="rIdN"/>, podczas gdy mc:Fallback trzyma prostokąt z informacją, którą Excel sam tam zapisuje. Czytnik, który nie rozumie cx1, jest zobowiązany kontraktem Part 3 do wzięcia fallbacku
Typ relacji to druga wskazówka. Klasyczny wykres wisi na części rysunkowej ze standardową relacją wykresu OOXML; część chartEx używa zamiast tego http://schemas.microsoft.com/office/2014/relationships/chartEx, udokumentowanego w [MS-ODRAWXML]. HotXLS wystawia oba światy jako stałe — XlsxRtChartEx dla tej relacji, XlsxCtChartEx dla typu treści, XlsxNsMsChartEx i XlsxNsMsChartEx2015 dla dwóch przestrzeni nazw, których potrzebuje kotwica — a predykat XlsxChartIsChartEx kieruje wykres w rozszerzoną ścieżkę przy zapisie. Wykresy kombinowane i zachowany klasyczny XML wykresów idą tą drugą, opisaną w artykule o zachowanym XML wykresów i wykresach kombinowanych
Cztery reguły schematu, które decydują, czy Excel otworzy plik
Schemat chartex jest ostrzejszy niż klasyczny w czterech konkretnych miejscach i każde z nich daje ten sam niepomocny komunikat o naprawie, gdy coś zepsujesz. BuildChartExKnownXml i BuildChartExRelsXml w HotXLS mają wszystkie cztery wpisane w kod, ale warto je znać, jeśli kiedykolwiek oglądasz XML ręcznie
- Kolejność.
cx:chartDatamusi być w dokumencie przedcx:chart. To odwrotność intuicji, którą budujesz na klasycznych wykresach, gdzie cache mieszkają w serii wewnątrz obszaru kreślenia; w części chartEx dane siedzą z przodu we własnym bloku, a ciało wykresu odwołuje się do nich tylko po id - Liczność.
cx:plotAreamoże zawierać dokładnie jedencx:plotAreaRegion. Nie zero, nie dwa. Nie ma tu historii wykresu kombinowanego — jeden region i serie w środku - Umiejscowienie typu. Typ wykresu w ogóle nie jest nazwą elementu. To atrybut
layoutIdnacx:series, przyjmujący token z wyliczeniaST_SeriesLayoutw schemacie chartex Microsoftu.XlsxChartExLayoutIdmapuje enum HotXLS na ten token i zwraca'waterfall'lub'treemap';XlsxChartExLayoutIdToTypeodpala mapowanie wstecz przy czytaniu - Relacja, której klasyczne wykresy nigdy nie potrzebują.
cx:chartDatatrzyma element<cx:externalData r:id="rId1"/>i ta relacja musi istnieć wxl/charts/_rels/chartExN.xml.rels, wskazując z powrotem na arkusz, który jest właścicielem danych, bo Excel używa jej do rozwiązania formuł zakresówcx:f. Pomiń część rels, a wykres otworzy się pusty bez niczego, co tłumaczyłoby dlaczego
Jak dodać wykres wodospadowy albo treemap w Delphi?
Tak samo, jak dodajesz każdy inny wykres w HotXLS — typy rozszerzone są członkami TXLSXChartType, więc TXLSXWorksheet.AddChart przyjmuje je bezpośrednio, a wszystko niżej w pipeline kluczuje po wartości enuma. Nie ma osobnego API chartEx do nauki
var
Book: TXLSXWorkbook;
Sheet: TXLSXWorksheet;
Chart: TXLSXChart;
begin
Book := TXLSXWorkbook.Create;
try
Book.Open('cashflow.xlsx');
Sheet := Book.Sheets[0];
// Prostokąt kotwiczący we współrzędnych wiersz/kolumna, jak w klasycznym wykresie
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;
Przy zapisie HotXLS widzi, że XlsxChartIsChartEx zwraca True, przydziela nazwę części z osobnego licznika chartEx i zapisuje xl/charts/chartEx1.xml, jego plik rels, nadpisanie typu treści i kotwicę mc:AlternateContent wyprodukowaną przez XlsxChartExAnchorPayloadXml. Niezależny licznik ma znaczenie: skoroszyt z dwoma klasycznymi wykresami i jednym wodospadem dostaje chart1.xml, chart2.xml i chartEx1.xml, nie chartEx3.xml. Treemapy różnią się potem od wodospadów dwoma szczegółami, oba obsługiwane przez ten sam builder. Wymiar numeryczny zmienia typ — cx:numDim type="val" dla wodospadu staje się type="size" dla treemap, bo prostokąt treemap koduje wielkość jako powierzchnię, nie jako wykreśloną wartość — a treemapy zwykle chcą hierarchii kategorii, którą model wyraża przez TXLSXChartSeries.CategoriesMultiLevel. Ustaw ten flag i S.ResolveCategoryLevels czyta zakres z arkusza w czasie zapisu, dzieli go na jeden TXLSXChartDataCache na poziom za S.CategoryLevelCount i S.CategoryLevels[], a BuildChartExLevelXml emituje jeden cx:lvl na poziom wewnątrz jednego cx:strDim, najpierw poziom liścia — ta sama fizyczna kolejność, której ECMA-376 Part 1 §21.2 używa dla c:multiLvlStrCache, więc ta konwencja przenosi się dalej, nawet jeśli żadna część znaczników się nie przenosi
Chart := Sheet.AddChart(xlsxChartTreemap, 'Revenue by Region',
2, 8, 24, 16);
S := Chart.AddSeries('Revenue',
'Sheet1!$A$2:$B$40', // dwie kolumny: etykieta liścia, potem grupa nadrzędna
'Sheet1!$C$2:$C$40');
S.CategoriesMultiLevel := True;
Kształt cache, który łamie każde założenie z klasycznych wykresów
Jeśli pisałeś czytnik cache klasycznych wykresów, trzy nawyki zdradzą cię na części chartEx, a wszystkie trzy biorą się z jednej decyzji projektowej: chartex przechowuje w atrybutach i treści elementów to, co DrawingML przechowuje w elementach potomnych
- Zacache'owany punkt to
<cx:pt idx="0">42</cx:pt>. Wartością jest treść elementu. Nie ma elementu potomnegoc:v, do którego można zejść, więc parser chodzący w poszukiwaniu węzłówvniczego nie znajduje i raportuje pusty cache ptCountiformatCodeto atrybuty nacx:lvl. W klasycznymc:numCacheoba są elementami potomnymi- Wiele poziomów hierarchii to wiele elementów
cx:lvlwewnątrz jednegocx:strDim. Nie ma osobnego elementu cache wielopoziomowego w taki sposób, w jakic:multiLvlStrCachejest oddzielone odc:strCache
BuildChartExLevelXml zapisuje ten kształt, a ParseChartExXml czyta go z powrotem, przepuszczając każdą wartość przez XlsxEscapeText przy wyjściu, żeby etykieta kategorii z ampersandem albo nawiasem ostrym przeżyła round trip. Warto odnotować, co builder robi, gdy poziom numeryczny nie niesie kodu formatu: zapisuje formatCode="General" zamiast upuszczać atrybut, bo tu atrybut nie jest opcjonalny tak, jak jego klasyczny odpowiednik
Prefiks przestrzeni nazw, który psuł odczyt typu
Pierwsza wersja parsera chartEx w HotXLS zapisywała całkiem dobre pliki i czytała każdy z nich z powrotem jako wykres kolumnowy. Sednem była jedna linia, a lekcja uogólnia się na każdy parser OOXML, jaki kiedykolwiek napiszesz: TXMLReader.Name zwraca nazwę tak, jak jest zapisana w dokumencie, z prefiksem. W klasycznej części wykresu większość elementów przychodzi bez prefiksu, więc SameText(Reader.Name, 'series') działa i przestajesz o tym myśleć. W części chartEx każdy element ma prefiks — czytnik podaje ci 'cx:series', porównanie po cichu zawodzi na wszystkich, i nie dostajesz żadnego wyjątku ani zepsutego XML, tylko pustą listę serii i typ wykresu spadający do domyślnego. Poprawka to porównywanie po nazwie lokalnej, w jednym miejscu zamiast przy każdym wywołaniu
// Reader.Name zawiera prefiks przestrzeni nazw: 'cx:series', a nie '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 używa tego helpera do każdego testu elementu i równoważnego helpera AttrVal do atrybutów, bo cx:autoUpdate i spółka też mają prefiksy. Gdy elementy serii są w końcu widziane, typ wraca z pierwszego napotkanego layoutId, a każdy cx:series jest podpinany do swojego bloku cx:data przez dopasowanie cx:dataId/@val do cx:data/@id. Wykresy otwarte, ale nigdy niezmodyfikowane, idą starszą ścieżką: oryginalne bajty wracają z FRawChartXml, więc nietknięty plik klienta wychodzi tak, jak wszedł — ta sama dyscyplina konserwatywnego zapisu, która rządzi rozszerzeniami na poziomie arkusza w artykule o rozszerzeniu sparklines
Czego HotXLS celowo nie robi z tymi wykresami
Dwie granice warto nazwać wprost, bo obie to wybory, nie luki czekające na uzupełnienie. Wbudowane renderery PDF i SVG nie rysują serii wodospadowych ani treemap: te typy nie pasują do żadnej gałęzi rysowania, więc renderowanie serii jest pomijane, a reszta arkusza eksportuje się normalnie — sfałszowanie wiarygodnie wyglądającego treemap w eksportowanym PDF byłoby gorsze niż narysowanie niczego, bo po cichu zaprzeczałoby temu, co Excel pokazuje dla tego samego pliku. A xlsxChartWaterfall i xlsxChartTreemap to jedyne dwa rozszerzone układy, które HotXLS buduje z modelu; ST_SeriesLayout definiuje też sunburst, funnel, box-and-whisker i histogram, a skoroszyt zawierający którekolwiek z nich wciąż przechodzi round trip ścieżką replayu surowego XML — po prostu nie dasz rady dziś utworzyć takiego z kodu Delphi. Renderowanie klasycznych typów wykresów jest niezmienione i opisane w artykule o wykresach, obrazach i rysunkach, a pipeline chartEx jedzie razem z nim w HotXLS Delphi Excel Component dla Delphi i C++Builder