Техническая статья

Редактирование диаграмм Excel без потери ChartML в HotXLS

HotXLS сохраняет исходный ChartML импортированной диаграммы Excel и побайтовно воспроизводит его, когда в типизированной модели ничего не изменилось, поэтому открытие и пересохранение книги не молча обрезает те части диаграммы, которые объектная модель не представляет. Измените заголовок, ряд или ось, и HotXLS структурно вмержит новые типизированные узлы в исходное дерево, а не перегенерирует диаграмму с нуля

Именно это различие — воспроизведение для нетронутого и слияние для изменённого — и делает Delphi-приложение безопасным, чтобы натравить его на диаграммы, собранные аналитиком в Excel. Перегенерация диаграммы из типизированной модели всегда теряет то, чего модель не знает, а диаграммы Excel полны тем, чего библиотека не знает: нестандартные стили, списки расширений, ветви альтернативного содержимого, форматирование с квалификатором пространства имён из надстроек

Почему пересохранение книги уплощает её диаграммы?

Потому что обычная реализация читает то, что понимает, и пишет то, что понимает. Всё, что между ними — extLst из более нового сборки Excel, ветвь mc:AlternateContent, эффект фигуры, для которого в модели нет свойства, — существует в файле, не имеет дома в объектной модели и исчезает при сохранении. Пользователь видит диаграмму, потерявшую градиент, пользовательские подписи данных или 3D-эффект, и книга не может объяснить почему

С PreserveUnsupportedParts, включённым на книге, HotXLS хранит исходные UTF-8 байты диаграммы при импорте вместе с длиной и 64-битным отпечатком смоделированного содержимого. Если к моменту сохранения типизированная модель не изменилась, он записывает исходные байты обратно. Без переупорядочивания XML, без дрейфа пробелов, без резидентной UTF-16 копии диаграммы, висящей в памяти для диаграммы, которую никто не правил

Диаграмма сохранения ChartML в HotXLS в Delphi: неизменённые диаграммы проигрывают исходные байты, отредактированные вливают типизированные узлы модели в сохранённый XML, тогда как пересборка только из модели теряет ветки extLst и AlternateContent
HotXLS хранит исходные байты ChartML при импорте и переигрывает их нетронутыми или вливает типизированные правки в сохранённое дерево, когда модель изменилась
var
  Workbook: TXLSXWorkbook;
  Chart: TXLSXChart;
begin
  Workbook := TXLSXWorkbook.Create;
  try
    Workbook.PreserveUnsupportedParts := True;   // установить перед Open
    Workbook.Open('quarterly-review.xlsx');
    Chart := Workbook.Sheets[1].Charts[0];   // Sheets[] отсчитывается от 1
    if Chart.HasPreservedXml then
      Log('original ChartML retained; untouched charts replay exactly');
    Chart.Title := 'Revenue by region, Q3 2026';  // теперь запускается путь объединения
    Workbook.SaveAs('quarterly-review-out.xlsx');
  finally
    Workbook.Free;
  end;
end;

Что слияние сохраняет и чем владеет модель

Правило — владение. Публичные узлы модели — заголовок, ряды, кэши, группы графиков, оси — решаются заново построенным деревом. Не моделированные поддеревья переносятся из оригинала. Эта асимметрия намерена и требует момента на осмысление: удаление ряда не должно воскрешать старый ряд из сохранённого XML, поэтому всё, чем владеет модель, берётся из модели, даже когда модель говорит «исчезло»

Не принадлежащее содержимое повторно вставляется рядом с matched-узлами, рядом с которыми оно стояло, а extLst остаётся последним внутри своего родителя, потому что именно там требует его схема. Очистка известного свойства — снятие флага «жирный», удаление цвета — трактуется как очистка, а не как пробел, который надо заполнить из исходного дерева. Неизвестные атрибуты в той же окрестности сохраняют своё место

Содержимое markup-compatibility получает особую обработку. При разборе HotXLS пропускает mc:Choice и читает mc:Fallback; если эта ветвь fallback оборачивает группу графиков, известную модели, слияние пишет новое типизированное содержимое обратно в поддерево fallback. Без этого шага сохранение выпустило бы новую группу графиков и оставило старую внутри fallback, и диаграмма несла бы две

Отключение одной диаграммы

ClearPreservedXml сбрасывает исходный XML и отпечаток для одной диаграммы, поэтому следующее сохранение перестраивает эту диаграмму чисто из типизированной модели. Это решение на уровне диаграммы; книжный PreserveUnsupportedParts по-прежнему определяет, строится ли состояние сохранения при импорте

Обращайтесь к этому, когда вам нужна чистая диаграмма, а не верная — например, когда диаграмма-шаблон несёт форматирование из старой корпоративной темы и смысл операции в том, чтобы его отбросить. Не обращайтесь к этому как к рефлексу отладки: диаграмма, ошибочно рендерящаяся после правки, чаще вопрос моделирования, а не сохранения, а очистка сохранённого XML разрушает улику

Комбинированные диаграммы и вторая ось

AddPlotGroup строит упорядоченные комбинированные диаграммы, где каждая группа графиков сохраняет своё семейство диаграммы и своё назначение первичной или вторичной оси. Это то, что нужно вездесущей деловой диаграмме: выручка столбцами по левой оси, маржа в процентах линией по правой

Диаграммы предоставляют первичную и вторичную оси категорий и значений плюс ось рядов с типизированными настройками категории, значения, даты и оси рядов, покрывающими позиции, идентификаторы, пересечения, единицы дат, интервалы меток и видимость. Прежний плоский API — ChartType, AddSeries, плоская коллекция Series — по-прежнему адресует первичную группу графиков, поэтому существующий код продолжает работать, тогда как модель под ним держит каждую упорядоченную группу

Диаграмма комбинированной диаграммы HotXLS в Delphi: столбцы выручки из AddSeries на главной оси значений и линия маржи из AddPlotGroup на вторичной оси значений, тогда как прежний плоский API достаёт только до группы один
Каждый вызов AddPlotGroup дописывает упорядоченную группу со своим семейством диаграмм и назначением главной или вторичной оси, тогда как плоский API по-прежнему адресует первую группу
var
  Chart: TXLSXChart;
  Line: TXLSXChartPlotGroup;
begin
  // Столбчатая диаграмма, закреплённая над строками 1..15, колонками 5..12
  Chart := Sheet.Charts.Add(xlsxChartColumn, 'Revenue and margin',
    1, 5, 15, 12);
  Chart.AddSeries('Revenue', 'Data!$A$2:$A$13', 'Data!$B$2:$B$13');
  // Вторая группа построения: линия, читаемая по вторичной оси значений
  Line := Chart.AddPlotGroup(xlsxChartLine, xlsxAxisSecondary);
  Line.AddSeries('Margin %', 'Data!$A$2:$A$13', 'Data!$C$2:$C$13');
end;

До появления этой модели открытие и пересохранение комбинированной диаграммы схлопывало её в одно семейство и теряло вторичную, дату и ось рядов. Ссылки в формулах диаграмм и кэшированные значения рядов теперь также переживают копии книг и циклы открыть/сохранить, а привязки диаграмм сопоставляются с их частями по идентификатору связи, поэтому приложение, переупорядочивающее записи связей, больше не меняет содержимое диаграмм между позициями листа

Ловушка ручной разметки

Если вы позиционируете область построения, заголовок, подпись оси или легенду вручную, записывайте все четыре координаты. Office требует x, y, w и h как полную группу, когда присутствует хоть одна из них, иначе игнорирует весь manualLayout. HotXLS поэтому всегда выпускает полный прямоугольник, а SetPositionAndSize задаёт и включает его одним вызовом

Диаграмма правила ручной компоновки HotXLS в Delphi: x, y, w и h должны прийти полной группой, иначе Office игнорирует весь элемент manualLayout, с режимами edge и factor и layoutTarget, ограниченным областью построения
Office применяет ручную раскладку, только когда x, y, w и h приходят полной группой — вот почему HotXLS выдаёт полный прямоугольник

Ещё две детали следуют Excel, а не полной свободе спецификации. Позиция записывается с xMode и yMode в edge, тогда как размер использует factor для wMode и hMode, потому что именно так пишет Excel и во что он конвертирует прочие комбинации. А layoutTarget выпускается только для области построения, никогда для заголовков или легенд, где заметки реализации Microsoft это запрещают

Как это встаёт в цикл книги

Сохранение диаграмм — один из случаев общей политики: библиотека, редактирующая документы, авторизованные в другом месте, должна менять то, о чём её попросили, и ничего более. Та же логика движет HotXLS на темах, цепочках вычислений и расширениях листа, описанная в заметках о циклах без потерь для темы, extLst и calcChain и о проектах VBA и внешних ссылках — в сохранении VBA и внешних ссылок. Саму авторскую поверхность диаграмм — создание диаграмм, размещение изображений и графики из Delphi — см. в разборе диаграмм, изображений и графики в HotXLS

HotXLS читает, редактирует и пишет диаграммы XLSX из нативного кода Delphi и C++Builder без установленного на машине Excel, что и делает корректность проблемой библиотеки, а не автоматизации — на странице компонента HotXLS есть список возможностей диаграмм и пробная загрузка