HotXLS пишет Excel-диаграммы waterfall и treemap как отдельные части cx:chartSpace — xl/charts/chartExN.xml с content type application/vnd.ms-office.chartex+xml — и якорит их через обёртку mc:AlternateContent, чтобы Excel 2016 и новее рендерили диаграмму, а старые читатели получали fallback-фигуру. Выберите xlsxChartWaterfall или xlsxChartTreemap — и весь пайплайн частей поедет сам
Обычно эта проблема объявляет о себе тикетом в поддержку. Клиент присылает книгу, которую их финкоманда собрала в Excel 2019, ваш отчётный инструмент открывает её, сохраняет — и waterfall-диаграмма исчезла. Ни исключения, ни предупреждения, ничего в логе: диаграммы просто нет в выводе. Если пойдёте искать её в классическом кодовом пути диаграмм, искать придётся долго, потому что диаграммы там никогда и не было
Почему waterfall и treemap-диаграмм нет в c:chartSpace?
Потому что Microsoft не могла положить их туда. Схема диаграмм DrawingML в ECMA-376 Part 1 §21.2 перечисляет свои элементы диаграмм — c:barChart, c:lineChart, c:pieChart и остальные — как закрытый choice, так что добавление waterfall, treemap, sunburst и funnel в Excel 2016 означало бы слом схемы, на которую завязан каждый существующий читатель. Новые типы ушли в параллельный документ: часть cx:chartSpace в namespace chartex 2014 года, http://schemas.microsoft.com/office/drawing/2014/chartex, лежит рядом с классическими диаграммами, но структурно с ними не связана. Рисовальная сторона сшивает два мира через mc:AlternateContent — механизм совместимости из ECMA-376 Part 3: ветка mc:Choice объявляет Requires="cx1" и держит xdr:graphicFrame, чей a:graphicData/@uri — это namespace chartex, а полезная нагрузка — голая ссылка <cx:chart r:id="rIdN"/>, тогда как mc:Fallback держит прямоугольник с уведомлением, которое туда пишет сам Excel. Читатель, не понимающий cx1, по контракту Part 3 обязан взять fallback
Второй маркер — тип связи. Классическая диаграмма висит на рисовальной части со стандартной OOXML chart-связью; chartEx-часть использует вместо неё http://schemas.microsoft.com/office/2014/relationships/chartEx, задокументированный в [MS-ODRAWXML]. HotXLS выставляет оба мира константами — XlsxRtChartEx для этой связи, XlsxCtChartEx для content type, XlsxNsMsChartEx и XlsxNsMsChartEx2015 для двух namespace, которые нужны якорю, — а предикат XlsxChartIsChartEx решает, что диаграмма на сохранении уйдёт в расширенный путь. Комбинированные диаграммы и сохранённый классический chart XML идут другим, описанным в статье про сохранённый chart XML и комбинированные диаграммы
Четыре правила схемы, которые решают, откроет ли Excel файл
Схема chartex строже классической в четырёх конкретных местах, и каждое из них при ошибке даёт один и тот же бесполезный запрос на восстановление. BuildChartExKnownXml и BuildChartExRelsXml в HotXLS зашивают все четыре, но их стоит знать, если вы когда-нибудь будете смотреть XML руками
- Порядок.
cx:chartDataобязан стоять в документе раньшеcx:chart. Это противоречит интуиции, выработанной на классических диаграммах, где кэши живут внутри серии внутри области графика; в chartEx-части данные лежат впереди своим отдельным блоком, а тело диаграммы только ссылается на них по id - Кардинальность.
cx:plotAreaможет содержать ровно одинcx:plotAreaRegion. Не ноль и не два. Никаких комбинированных диаграмм здесь нет — одна область, и серии внутри неё - Размещение типа. Тип диаграммы вообще не имя элемента. Это атрибут
layoutIdнаcx:series, принимающий токен из перечисленияST_SeriesLayoutв схеме chartex от Microsoft.XlsxChartExLayoutIdотображает перечисление HotXLS на этот токен и возвращает'waterfall'или'treemap';XlsxChartExLayoutIdToTypeгонит отображение в обратную сторону на чтении - Связь, которая классическим диаграммам не нужна.
cx:chartDataдержит элемент<cx:externalData r:id="rId1"/>, и эта связь обязана существовать вxl/charts/_rels/chartExN.xml.rels, указывая обратно на лист-владельца данных, потому что Excel использует её для разрешения формул диапазоновcx:f. Пропустите rels-часть — диаграмма откроется пустой, и ничто не объяснит почему
Как добавить waterfall или treemap-диаграмму в Delphi?
Точно так же, как любую другую диаграмму в HotXLS: расширенные типы — члены TXLSXChartType, так что TXLSXWorksheet.AddChart принимает их напрямую, и всё ниже по потоку завязано на значение перечисления. Никакого отдельного chartEx API учить не надо
var
Book: TXLSXWorkbook;
Sheet: TXLSXWorksheet;
Chart: TXLSXChart;
begin
Book := TXLSXWorkbook.Create;
try
Book.Open('cashflow.xlsx');
Sheet := Book.Sheets[0];
// Прямоугольник якоря в координатах строк/колонок, как у классической диаграммы
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;
На сохранении HotXLS видит, что XlsxChartIsChartEx вернул True, выделяет имя части из отдельного chartEx-счётчика и пишет xl/charts/chartEx1.xml, его rels-файл, override content type и якорь mc:AlternateContent, который производит XlsxChartExAnchorPayloadXml. Независимый счётчик важен: книга с двумя классическими диаграммами и одним waterfall получает chart1.xml, chart2.xml и chartEx1.xml, а не chartEx3.xml. Дальше treemap отличается от waterfall в двух деталях, обе обрабатывает тот же билдер. Числовое измерение меняет тип — cx:numDim type="val" у waterfall становится type="size" у treemap, потому что прямоугольник treemap кодирует величину площадью, а не отложенным значением, — а treemap обычно хочет иерархию категорий, которую модель выражает через TXLSXChartSeries.CategoriesMultiLevel. Поставьте этот флаг — и S.ResolveCategoryLevels прочитает диапазон из листа в момент записи, разобьёт его на один TXLSXChartDataCache на уровень за S.CategoryLevelCount и S.CategoryLevels[], а BuildChartExLevelXml выпустит по одному cx:lvl на уровень внутри единого cx:strDim, листовой уровень первым, — тот же физический порядок, что ECMA-376 Part 1 §21.2 использует для c:multiLvlStrCache, так что эта конвенция переносится, хотя разметка — никакая
Chart := Sheet.AddChart(xlsxChartTreemap, 'Revenue by Region',
2, 8, 24, 16);
S := Chart.AddSeries('Revenue',
'Sheet1!$A$2:$B$40', // две колонки: подпись листа, затем родительская группа
'Sheet1!$C$2:$C$40');
S.CategoriesMultiLevel := True;
Форма кэша, которая ломает все допущения классических диаграмм
Если вы писали читатель кэша классических диаграмм, три привычки подведут вас на chartEx-части, и все три вытекают из одного дизайн-решения: chartex хранит в атрибутах и содержимом элементов то, что DrawingML хранит в дочерних элементах
- Кэшированная точка — это
<cx:pt idx="0">42</cx:pt>. Значение — содержимое элемента. Никакого дочернегоc:v, в который можно спуститься, нет, так что парсер, идущий в поисках узловv, не находит ничего и сообщает о пустом кэше ptCountиformatCode— атрибуты наcx:lvl. В классическомc:numCacheоба — дочерние элементы- Несколько уровней иерархии — это несколько элементов
cx:lvlвнутри одногоcx:strDim. Отдельного многоуровневого элемента кэша нет — в отличие от того, какc:multiLvlStrCacheотделён отc:strCache
BuildChartExLevelXml пишет эту форму, а ParseChartExXml читает её обратно, прогоняя каждое значение через XlsxEscapeText на выходе, чтобы подпись категории с амперсандом или угловой скобкой пережила round trip. Стоит отметить, что делает билдер, когда числовой уровень не несёт format code: он пишет formatCode="General" вместо того, чтобы выбросить атрибут, потому что здесь атрибут не опционален, как его классический аналог
Namespace-префикс, из-за которого ломалось чтение типа
Первая версия chartEx-парсера HotXLS писала совершенно нормальные файлы и читала каждый из них обратно как столбчатую диаграмму. Корень — одна строка, и урок обобщается на любой OOXML-парсер, который вы когда-нибудь напишете: TXMLReader.Name возвращает имя как написано в документе, включая префикс. В классической chart-части большинство элементов приходит без префикса, так что SameText(Reader.Name, 'series') работает, и вы перестаёте об этом думать. В chartEx-части префикс есть у каждого элемента: ридер отдаёт вам 'cx:series', сравнение молча не срабатывает на всех, и вы получаете ни исключения, ни битого XML — просто пустой список серий и тип диаграммы, откатывающийся к значению по умолчанию. Фикс — сравнивать по локальному имени, в одном месте, а не на каждом месте вызова
// Reader.Name несёт namespace-префикс: 'cx:series', а не '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 использует этот хелпер для каждой проверки элемента и эквивалентный хелпер AttrVal для атрибутов, потому что cx:autoUpdate и его товарищи тоже с префиксами. Как только элементы серий реально начинают видеться, тип возвращается из первого встреченного layoutId, а каждая cx:series привязывается к своему блоку cx:data сопоставлением cx:dataId/@val с cx:data/@id. Диаграммы, открытые, но не изменённые, идут старшим путём: исходные байты воспроизводятся из FRawChartXml, так что нетронутый файл клиента выходит таким же, каким вошёл, — та же дисциплина консервативного писателя, что управляет расширениями уровня листа в статье про расширение sparkline
Чего HotXLS намеренно не делает с этими диаграммами
Две границы стоит назвать прямо, потому что обе — выборы, а не дыры, ждущие заполнения. Встроенные рендереры PDF и SVG не рисуют серии waterfall и treemap: типы не совпадают ни с одной веткой отрисовки, так что рендер серий пропускается, а остальное листа экспортируется как обычно, — подделать правдоподобный treemap в экспортированном PDF было бы хуже, чем не рисовать ничего, потому что он молча разошёлся бы с тем, что Excel показывает для того же файла. И xlsxChartWaterfall с xlsxChartTreemap — единственные два расширенных layout, которые HotXLS строит из модели; ST_SeriesLayout определяет ещё sunburst, funnel, box-and-whisker и histogram, и книга с любым из них по-прежнему переживает round trip через путь воспроизведения сырого XML, просто сочинить такой из Delphi-кода сегодня нельзя. Рендер классических типов диаграмм не менялся и описан в статье про диаграммы, изображения и рисунки, а chartEx-пайплайн едет рядом в HotXLS Delphi Excel Component для Delphi и C++Builder