@rowsncolumns/toolkit 24.4.4 → 24.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (113) hide show
  1. package/dist/cjs/autofilter-condition.d.ts +45 -1
  2. package/dist/cjs/autofilter-condition.d.ts.map +1 -1
  3. package/dist/cjs/autofilter-condition.js +135 -12
  4. package/dist/cjs/excel-parser/helpers.d.ts +15 -1
  5. package/dist/cjs/excel-parser/helpers.d.ts.map +1 -1
  6. package/dist/cjs/excel-parser/helpers.js +119 -5
  7. package/dist/cjs/excel-parser/interface.d.ts +3 -1
  8. package/dist/cjs/excel-parser/interface.d.ts.map +1 -1
  9. package/dist/cjs/excel-parser/plugins/ods/index.d.ts.map +1 -1
  10. package/dist/cjs/excel-parser/plugins/ods/index.js +186 -97
  11. package/dist/cjs/excel-parser/plugins/ods/styles.d.ts +15 -0
  12. package/dist/cjs/excel-parser/plugins/ods/styles.d.ts.map +1 -1
  13. package/dist/cjs/excel-parser/plugins/ods/styles.js +80 -6
  14. package/dist/cjs/excel-parser/plugins/xlsb/index.d.ts.map +1 -1
  15. package/dist/cjs/excel-parser/plugins/xlsb/index.js +107 -103
  16. package/dist/cjs/excel-parser/plugins/xlsx/charts.d.ts +77 -1
  17. package/dist/cjs/excel-parser/plugins/xlsx/charts.d.ts.map +1 -1
  18. package/dist/cjs/excel-parser/plugins/xlsx/charts.js +920 -122
  19. package/dist/cjs/excel-parser/plugins/xlsx/drawing.d.ts +7 -1
  20. package/dist/cjs/excel-parser/plugins/xlsx/drawing.d.ts.map +1 -1
  21. package/dist/cjs/excel-parser/plugins/xlsx/drawing.js +24 -9
  22. package/dist/cjs/excel-parser/plugins/xlsx/form-controls.d.ts +24 -3
  23. package/dist/cjs/excel-parser/plugins/xlsx/form-controls.d.ts.map +1 -1
  24. package/dist/cjs/excel-parser/plugins/xlsx/form-controls.js +25 -3
  25. package/dist/cjs/excel-parser/plugins/xlsx/index.d.ts +24 -1
  26. package/dist/cjs/excel-parser/plugins/xlsx/index.d.ts.map +1 -1
  27. package/dist/cjs/excel-parser/plugins/xlsx/index.js +561 -394
  28. package/dist/cjs/excel-parser/plugins/xlsx/pivots.d.ts +8 -2
  29. package/dist/cjs/excel-parser/plugins/xlsx/pivots.d.ts.map +1 -1
  30. package/dist/cjs/excel-parser/plugins/xlsx/pivots.js +312 -21
  31. package/dist/cjs/excel-parser/plugins/xlsx/relationships.d.ts +6 -0
  32. package/dist/cjs/excel-parser/plugins/xlsx/relationships.d.ts.map +1 -1
  33. package/dist/cjs/excel-parser/plugins/xlsx/relationships.js +9 -0
  34. package/dist/cjs/excel-parser/plugins/xlsx/theme.d.ts +1 -1
  35. package/dist/cjs/excel-parser/types.d.ts +9 -1
  36. package/dist/cjs/excel-parser/types.d.ts.map +1 -1
  37. package/dist/cjs/excel-parser/workbook.d.ts +1 -0
  38. package/dist/cjs/excel-parser/workbook.d.ts.map +1 -1
  39. package/dist/cjs/excel-parser/workbook.js +5 -0
  40. package/dist/cjs/excel-parser/worker.js +2 -0
  41. package/dist/cjs/exporter/ods/ods-export.d.ts.map +1 -1
  42. package/dist/cjs/exporter/ods/ods-export.js +255 -80
  43. package/dist/cjs/exporter/xlsx/chart-builder.d.ts +7 -1
  44. package/dist/cjs/exporter/xlsx/chart-builder.d.ts.map +1 -1
  45. package/dist/cjs/exporter/xlsx/chart-builder.js +902 -119
  46. package/dist/cjs/exporter/xlsx/form-controls-builder.d.ts +16 -3
  47. package/dist/cjs/exporter/xlsx/form-controls-builder.d.ts.map +1 -1
  48. package/dist/cjs/exporter/xlsx/form-controls-builder.js +30 -8
  49. package/dist/cjs/exporter/xlsx/pivot-builder.d.ts.map +1 -1
  50. package/dist/cjs/exporter/xlsx/pivot-builder.js +206 -162
  51. package/dist/cjs/exporter/xlsx/style-builder.d.ts +8 -2
  52. package/dist/cjs/exporter/xlsx/style-builder.d.ts.map +1 -1
  53. package/dist/cjs/exporter/xlsx/style-builder.js +25 -0
  54. package/dist/cjs/exporter/xlsx/xlsx-export.d.ts +2 -2
  55. package/dist/cjs/exporter/xlsx/xlsx-export.d.ts.map +1 -1
  56. package/dist/cjs/exporter/xlsx/xlsx-export.js +728 -452
  57. package/dist/esm/autofilter-condition.d.ts +45 -1
  58. package/dist/esm/autofilter-condition.d.ts.map +1 -1
  59. package/dist/esm/autofilter-condition.js +131 -11
  60. package/dist/esm/excel-parser/helpers.d.ts +15 -1
  61. package/dist/esm/excel-parser/helpers.d.ts.map +1 -1
  62. package/dist/esm/excel-parser/helpers.js +94 -4
  63. package/dist/esm/excel-parser/interface.d.ts +3 -1
  64. package/dist/esm/excel-parser/interface.d.ts.map +1 -1
  65. package/dist/esm/excel-parser/plugins/ods/index.d.ts.map +1 -1
  66. package/dist/esm/excel-parser/plugins/ods/index.js +105 -22
  67. package/dist/esm/excel-parser/plugins/ods/styles.d.ts +15 -0
  68. package/dist/esm/excel-parser/plugins/ods/styles.d.ts.map +1 -1
  69. package/dist/esm/excel-parser/plugins/ods/styles.js +79 -5
  70. package/dist/esm/excel-parser/plugins/xlsb/index.d.ts.map +1 -1
  71. package/dist/esm/excel-parser/plugins/xlsb/index.js +15 -7
  72. package/dist/esm/excel-parser/plugins/xlsx/charts.d.ts +77 -1
  73. package/dist/esm/excel-parser/plugins/xlsx/charts.d.ts.map +1 -1
  74. package/dist/esm/excel-parser/plugins/xlsx/charts.js +708 -16
  75. package/dist/esm/excel-parser/plugins/xlsx/drawing.d.ts +7 -1
  76. package/dist/esm/excel-parser/plugins/xlsx/drawing.d.ts.map +1 -1
  77. package/dist/esm/excel-parser/plugins/xlsx/drawing.js +23 -0
  78. package/dist/esm/excel-parser/plugins/xlsx/form-controls.d.ts +24 -3
  79. package/dist/esm/excel-parser/plugins/xlsx/form-controls.d.ts.map +1 -1
  80. package/dist/esm/excel-parser/plugins/xlsx/form-controls.js +21 -3
  81. package/dist/esm/excel-parser/plugins/xlsx/index.d.ts +24 -1
  82. package/dist/esm/excel-parser/plugins/xlsx/index.d.ts.map +1 -1
  83. package/dist/esm/excel-parser/plugins/xlsx/index.js +192 -54
  84. package/dist/esm/excel-parser/plugins/xlsx/pivots.d.ts +8 -2
  85. package/dist/esm/excel-parser/plugins/xlsx/pivots.d.ts.map +1 -1
  86. package/dist/esm/excel-parser/plugins/xlsx/pivots.js +220 -4
  87. package/dist/esm/excel-parser/plugins/xlsx/relationships.d.ts +6 -0
  88. package/dist/esm/excel-parser/plugins/xlsx/relationships.d.ts.map +1 -1
  89. package/dist/esm/excel-parser/plugins/xlsx/relationships.js +9 -0
  90. package/dist/esm/excel-parser/plugins/xlsx/theme.d.ts +1 -1
  91. package/dist/esm/excel-parser/types.d.ts +9 -1
  92. package/dist/esm/excel-parser/types.d.ts.map +1 -1
  93. package/dist/esm/excel-parser/workbook.d.ts +1 -0
  94. package/dist/esm/excel-parser/workbook.d.ts.map +1 -1
  95. package/dist/esm/excel-parser/workbook.js +4 -0
  96. package/dist/esm/excel-parser/worker.js +2 -0
  97. package/dist/esm/exporter/ods/ods-export.d.ts.map +1 -1
  98. package/dist/esm/exporter/ods/ods-export.js +238 -72
  99. package/dist/esm/exporter/xlsx/chart-builder.d.ts +7 -1
  100. package/dist/esm/exporter/xlsx/chart-builder.d.ts.map +1 -1
  101. package/dist/esm/exporter/xlsx/chart-builder.js +674 -58
  102. package/dist/esm/exporter/xlsx/form-controls-builder.d.ts +16 -3
  103. package/dist/esm/exporter/xlsx/form-controls-builder.d.ts.map +1 -1
  104. package/dist/esm/exporter/xlsx/form-controls-builder.js +29 -8
  105. package/dist/esm/exporter/xlsx/pivot-builder.d.ts.map +1 -1
  106. package/dist/esm/exporter/xlsx/pivot-builder.js +187 -139
  107. package/dist/esm/exporter/xlsx/style-builder.d.ts +8 -2
  108. package/dist/esm/exporter/xlsx/style-builder.d.ts.map +1 -1
  109. package/dist/esm/exporter/xlsx/style-builder.js +28 -0
  110. package/dist/esm/exporter/xlsx/xlsx-export.d.ts +2 -2
  111. package/dist/esm/exporter/xlsx/xlsx-export.d.ts.map +1 -1
  112. package/dist/esm/exporter/xlsx/xlsx-export.js +451 -197
  113. package/package.json +11 -4
@@ -1,8 +1,8 @@
1
1
  import { cellRangeToAddress, cellToAddress, escapeCharacters, getCellEffectiveFormat, getCellEffectiveValue, getCellFormattedValue, getCellUserEnteredFormat, getCellUserEnteredValue, getExtendedValueBool, getExtendedValueError, getExtendedValueFormula, getExtendedValueNumber, getExtendedValueString, isNil, sortSheetsByIndex, } from "@rowsncolumns/utils";
2
2
  import { zipSync } from "fflate";
3
3
  import { omitInvalidDataValidationError } from "../invalid-data-validation";
4
- import { buildAutoFilterConditionXml } from "../../autofilter-condition";
5
- import { buildControlElementXml, buildControlsXml, buildCtrlPropXml, buildFormControlVmlShape, buildVmlDrawingXml, CTRL_PROP_CONTENT_TYPE, CTRL_PROP_REL_TYPE, FORM_CONTROL_VML_SHAPETYPE, } from "./form-controls-builder";
4
+ import { buildAutoFilterCriteriaXml } from "../../autofilter-condition";
5
+ import { buildControlElementXml, buildControlsXml, buildCtrlPropXml, buildFormControlVmlShape, controlAnchorFlags, buildVmlDrawingXml, CTRL_PROP_CONTENT_TYPE, CTRL_PROP_REL_TYPE, FORM_CONTROL_VML_SHAPETYPE, } from "./form-controls-builder";
6
6
  import { buildChartExGraphicFrameXml, buildChartExXml, CHART_EX_CONTENT_TYPE, CHART_EX_REL_TYPE, isChartExType, } from "./chartex-builder";
7
7
  import { applyPowerQueryParts } from "./power-query-builder";
8
8
  import { encodeImplicitIntersection } from "../../helpers/implicit-intersection";
@@ -19,7 +19,7 @@ import { buildChartXml } from "./chart-builder";
19
19
  import { buildPivotCacheDefinitionXml, buildPivotTableRelsXml, buildPivotTableXml, calculatedFieldReferences, } from "./pivot-builder";
20
20
  import { buildSlicerCacheXml, buildSlicerXml } from "./slicer-builder";
21
21
  import { buildSparklineGroupsXml } from "./sparkline-builder";
22
- import { StyleBuilder } from "./style-builder";
22
+ import { StyleBuilder, tableStyleElementsFromBanding, } from "./style-builder";
23
23
  const BLANK_LABEL = "(Blanks)";
24
24
  const isStyleReference = (value) => !!value && typeof value === "object" && "sid" in value;
25
25
  const DEFAULT_COLUMN_WIDTH_PX = 100;
@@ -38,9 +38,15 @@ export const createExcelFile = async ({ conditionalFormats: _conditionalFormats,
38
38
  if (namedCellStyles && namedCellStyles.length > 0) {
39
39
  styleBuilder.setNamedCellStyles(namedCellStyles);
40
40
  }
41
- if (customTableStyles && customTableStyles.length > 0) {
42
- styleBuilder.setCustomTableStyles(customTableStyles);
41
+ // The document facet carries `{ name, bandedRange }` (#668 T-103); a host may still pass the
42
+ // parser's element records. Both become `<tableStyle>` elements.
43
+ const customTableStyleRecords = (customTableStyles ?? []).map((style) => "bandedRange" in style
44
+ ? { name: style.name, elements: tableStyleElementsFromBanding(style.bandedRange) }
45
+ : style);
46
+ if (customTableStyleRecords.length > 0) {
47
+ styleBuilder.setCustomTableStyles(customTableStyleRecords);
43
48
  }
49
+ const customTableStyleNames = new Set(customTableStyleRecords.map((s) => s.name));
44
50
  // Drop tables with invalid coords or overlapping ranges — Excel rejects them
45
51
  // and would trigger a recovery prompt. Filter once so the id remap and the
46
52
  // prepared output stay in lockstep.
@@ -52,10 +58,13 @@ export const createExcelFile = async ({ conditionalFormats: _conditionalFormats,
52
58
  tableIdMap.set(String(table.id), index + 1);
53
59
  });
54
60
  const preparedTables = prepareTables(exportableTables);
55
- const embedsBySheet = groupEmbedsBySheet(embeds ?? []);
56
- const textBoxesBySheet = groupTextBoxesBySheet(embeds ?? []);
57
- const shapesBySheet = groupShapesBySheet(embeds ?? []);
58
- const formControlsBySheet = groupFormControlsBySheet(embeds ?? []);
61
+ // Charts and embeds share one z-order per sheet (`position.zOrder`, else
62
+ // creation order: charts below embeds) — the drawing anchors follow it.
63
+ const chartCount = charts?.length ?? 0;
64
+ const embedsBySheet = groupEmbedsBySheet(embeds ?? [], chartCount);
65
+ const textBoxesBySheet = groupTextBoxesBySheet(embeds ?? [], chartCount);
66
+ const shapesBySheet = groupShapesBySheet(embeds ?? [], chartCount);
67
+ const formControlsBySheet = groupFormControlsBySheet(embeds ?? [], chartCount);
59
68
  const ctrlProps = { parts: [] };
60
69
  const chartsBySheet = groupChartsBySheet(charts ?? []);
61
70
  // Remap slicer tableIds to match prepared table IDs
@@ -100,6 +109,22 @@ export const createExcelFile = async ({ conditionalFormats: _conditionalFormats,
100
109
  const pivotCacheRelsEntries = [];
101
110
  const pivotTableEntries = [];
102
111
  const pivotTableRelsEntries = [];
112
+ // `<c:pivotSource><c:name>` for PivotCharts: the exported pivot's name on
113
+ // its target sheet (`pivot-builder` names an unnamed pivot the same way).
114
+ // Only pivots the pivot writer emits (source + pivotRange) are named, so
115
+ // a chart never points Excel at a pivot part that is not in the file.
116
+ const pivotSourceNameById = new Map();
117
+ for (const p of pivotTables) {
118
+ const targetSheet = sortedSheets.find((s) => s.sheetId === p.targetPosition?.sheetId);
119
+ if (!targetSheet || !p.source || !p.pivotRange)
120
+ continue;
121
+ const sheetTitle = targetSheet.title ?? `Sheet${targetSheet.sheetId}`;
122
+ const quoted = /^[A-Za-z_][A-Za-z0-9_.]*$/.test(sheetTitle)
123
+ ? sheetTitle
124
+ : `'${sheetTitle.replace(/'/g, "''")}'`;
125
+ pivotSourceNameById.set(String(p.pivotId), `[Workbook]${quoted}!${p.name ?? `PivotTable${p.pivotId}`}`);
126
+ }
127
+ const resolvePivotSourceName = (pivotTableId) => pivotSourceNameById.get(String(pivotTableId));
103
128
  // Group pivots by the sheet they render on (targetPosition.sheetId).
104
129
  const pivotsByTargetSheet = new Map();
105
130
  for (const p of pivotTables) {
@@ -115,7 +140,15 @@ export const createExcelFile = async ({ conditionalFormats: _conditionalFormats,
115
140
  // Records pivot caches that need a workbook-level entry (added
116
141
  // after the sheet loop closes). Each entry: { cacheIndex, rIdOnWorkbook }
117
142
  const workbookPivotCacheRefs = [];
143
+ // Worksheet and chartsheet parts are numbered 1..N within their own folder
144
+ // (`xl/worksheets/sheetN.xml` / `xl/chartsheets/sheetN.xml`), as Excel does;
145
+ // drawings stay keyed by the sheet's position in `sortedSheets`.
146
+ let worksheetCount = 0;
147
+ let chartsheetCount = 0;
118
148
  const worksheetEntries = sortedSheets.map((sheet, index) => {
149
+ const isChartsheet = sheet.type === "chartsheet";
150
+ const partIndex = isChartsheet ? ++chartsheetCount : ++worksheetCount;
151
+ const partFolder = isChartsheet ? "chartsheets" : "worksheets";
119
152
  const rows = getRowsForSheet(sheetData, sheet.sheetId);
120
153
  const sheetRelationships = new RelationshipsBuilder();
121
154
  const sheetTables = preparedTables.filter((table) => table.sheetId === sheet.sheetId);
@@ -165,6 +198,7 @@ export const createExcelFile = async ({ conditionalFormats: _conditionalFormats,
165
198
  offsetY: placement.offsetY,
166
199
  width: placement.width,
167
200
  height: placement.height,
201
+ overlay: slicer.position?.overlayPosition,
168
202
  });
169
203
  globalSlicerIndex += 1;
170
204
  }
@@ -233,35 +267,42 @@ export const createExcelFile = async ({ conditionalFormats: _conditionalFormats,
233
267
  workbookPivotCacheRefs.push({ cacheIndex });
234
268
  globalPivotIndex += 1;
235
269
  }
236
- const result = buildWorksheetXml({
237
- sheet,
238
- rows,
239
- sharedStrings,
240
- sourceSharedStrings,
241
- cellXfs,
242
- styleBuilder,
243
- tables: sheetTables,
244
- relationships: sheetRelationships,
245
- drawingContext,
246
- conditionalFormats: sheetConditionalFormats,
247
- dataValidationsById,
248
- dataValidations: _dataValidations,
249
- ignoreFormulaPatterns,
250
- citations: sheetCitations,
251
- comments,
252
- personRegistry,
253
- slicerRelIds: worksheetSlicerRelIds,
254
- sheetData,
255
- sheets: sortedSheets,
256
- locale,
257
- protectedRanges,
258
- formControls: sheetFormControls,
259
- ctrlProps,
260
- });
270
+ const result = isChartsheet
271
+ ? buildChartsheetXml({
272
+ sheet,
273
+ relationships: sheetRelationships,
274
+ drawingContext,
275
+ })
276
+ : buildWorksheetXml({
277
+ sheet,
278
+ rows,
279
+ sharedStrings,
280
+ sourceSharedStrings,
281
+ cellXfs,
282
+ styleBuilder,
283
+ tables: sheetTables,
284
+ relationships: sheetRelationships,
285
+ drawingContext,
286
+ conditionalFormats: sheetConditionalFormats,
287
+ dataValidationsById,
288
+ dataValidations: _dataValidations,
289
+ ignoreFormulaPatterns,
290
+ citations: sheetCitations,
291
+ comments,
292
+ personRegistry,
293
+ slicerRelIds: worksheetSlicerRelIds,
294
+ sheetData,
295
+ sheets: sortedSheets,
296
+ locale,
297
+ protectedRanges,
298
+ formControls: sheetFormControls,
299
+ ctrlProps,
300
+ });
261
301
  return {
262
302
  index,
263
- path: `xl/worksheets/sheet${index + 1}.xml`,
264
- relationshipsPath: `xl/worksheets/_rels/sheet${index + 1}.xml.rels`,
303
+ isChartsheet,
304
+ path: `xl/${partFolder}/sheet${partIndex}.xml`,
305
+ relationshipsPath: `xl/${partFolder}/_rels/sheet${partIndex}.xml.rels`,
265
306
  content: result.xml,
266
307
  relationshipsXml: result.relationshipsXml,
267
308
  drawingPath: `xl/drawings/drawing${index + 1}.xml`,
@@ -278,12 +319,25 @@ export const createExcelFile = async ({ conditionalFormats: _conditionalFormats,
278
319
  threadedCommentsXml: result.threadedCommentsXml,
279
320
  };
280
321
  });
281
- const { xml: workbookRelationshipsXml, sheetRelationshipIds, slicerCacheRelationshipIds, pivotCacheRelationshipIds, externalLinkRelationshipIds, } = buildWorkbookRelationships(worksheetEntries.length, slicerCacheEntries.length, externalLinks.length, pivotCacheEntries.length, !personRegistry.isEmpty());
322
+ const { xml: workbookRelationshipsXml, sheetRelationshipIds, slicerCacheRelationshipIds, pivotCacheRelationshipIds, externalLinkRelationshipIds, } = buildWorkbookRelationships(worksheetEntries.map((entry) => ({
323
+ chartsheet: entry.isChartsheet,
324
+ target: entry.path.replace(/^xl\//, ""),
325
+ })), slicerCacheEntries.length, externalLinks.length, pivotCacheEntries.length, !personRegistry.isEmpty());
282
326
  const workbookXml = buildWorkbookXml(sortedSheets, sheetRelationshipIds, workbookSheetNames, namedRanges, iterativeCalculationOptions, slicerCacheRelationshipIds, use1904, externalLinkRelationshipIds, pivotCacheRelationshipIds);
283
327
  const sharedStringsXml = buildSharedStringsXml(sharedStrings);
284
328
  const rootRelationshipsXml = buildRootRelationshipsXml();
285
- const appXml = buildAppXml(workbookSheetNames, properties);
329
+ const appXml = buildAppXml(workbookSheetNames, sortedSheets.map((sheet) => sheet.type === "chartsheet"), properties);
286
330
  const coreXml = buildCoreXml(properties);
331
+ // Table parts register dxfs (colour filters, colour sort levels), so they
332
+ // are built before styles.xml is serialized.
333
+ const tablePartFiles = {};
334
+ for (const table of preparedTables) {
335
+ const rows = getRowsForSheet(sheetData, table.sheetId);
336
+ const tableXml = buildTableXml(table, rows, sourceSharedStrings, styleBuilder, customTableStyleNames);
337
+ if (tableXml) {
338
+ tablePartFiles[table.path] = encodeXml(tableXml);
339
+ }
340
+ }
287
341
  const stylesXml = styleBuilder.toXml();
288
342
  const themeXml = theme ? generateThemeXml(theme) : theme1Xml;
289
343
  const files = {
@@ -317,13 +371,7 @@ export const createExcelFile = async ({ conditionalFormats: _conditionalFormats,
317
371
  if (personsXml) {
318
372
  files["xl/persons/person.xml"] = encodeXml(personsXml);
319
373
  }
320
- for (const table of preparedTables) {
321
- const rows = getRowsForSheet(sheetData, table.sheetId);
322
- const tableXml = buildTableXml(table, rows, sourceSharedStrings, styleBuilder);
323
- if (tableXml) {
324
- files[table.path] = encodeXml(tableXml);
325
- }
326
- }
374
+ Object.assign(files, tablePartFiles);
327
375
  let globalImageIndex = 1;
328
376
  let globalChartIndex = 1;
329
377
  const drawingSheets = [];
@@ -356,6 +404,7 @@ export const createExcelFile = async ({ conditionalFormats: _conditionalFormats,
356
404
  charts: chartsForSheet,
357
405
  slicers: slicersForSheet,
358
406
  sheetNameById,
407
+ resolvePivotSourceName,
359
408
  });
360
409
  if (!drawingParts)
361
410
  continue;
@@ -425,7 +474,8 @@ export const createExcelFile = async ({ conditionalFormats: _conditionalFormats,
425
474
  .filter((entry) => entry.threadedCommentsXml)
426
475
  .map((entry) => entry.sheet.sheetId ?? entry.index + 1);
427
476
  const contentTypesXml = buildContentTypesXml({
428
- sheetCount: worksheetEntries.length,
477
+ worksheetCount,
478
+ chartsheetCount,
429
479
  tableCount: preparedTables.length,
430
480
  drawingSheets,
431
481
  mediaExtensions: Array.from(imageExtensions),
@@ -929,6 +979,27 @@ const buildWorksheetXml = ({ sheet, rows, sharedStrings, sourceSharedStrings, ce
929
979
  threadedCommentsXml,
930
980
  };
931
981
  };
982
+ /**
983
+ * `CT_Chartsheet` — a sheet tab whose whole body is one chart and that has no
984
+ * cells (#668 C-17). Children in schema order: `sheetPr` (tab colour only),
985
+ * `sheetViews`, `pageMargins`, `drawing`. The chart itself rides the sheet's
986
+ * drawing part as an `absoluteAnchor`, like every other chart.
987
+ */
988
+ const buildChartsheetXml = ({ sheet, relationships, drawingContext, }) => {
989
+ const tabColor = serializeColorXml(sheet.tabColor ?? undefined);
990
+ const sheetPrXml = tabColor ? `<sheetPr><tabColor ${tabColor}/></sheetPr>` : "";
991
+ const drawingXml = drawingContext.hasDrawing()
992
+ ? `<drawing r:id="${drawingContext.relId}"/>`
993
+ : "";
994
+ return {
995
+ xml: `${XML_DECLARATION}<chartsheet xmlns="${WORKSHEET_NS}" xmlns:r="${REL_NS}">${sheetPrXml}<sheetViews><sheetView workbookViewId="0" zoomToFit="1"/></sheetViews>${buildPageMarginsXml(sheet.printSettings)}${drawingXml}</chartsheet>`,
996
+ relationshipsXml: relationships.toXml(),
997
+ images: [],
998
+ commentsXml: undefined,
999
+ vmlDrawingXml: undefined,
1000
+ threadedCommentsXml: undefined,
1001
+ };
1002
+ };
932
1003
  /**
933
1004
  * Build combined extLst XML for worksheet (sparklines + slicers)
934
1005
  */
@@ -2360,7 +2431,7 @@ const resolveTableColumnTotals = (column, totalsCell) => {
2360
2431
  const label = getExtendedValueString(getCellUserEnteredValue(totalsCell));
2361
2432
  return label && label.trim() !== "" ? { totalsRowLabel: label } : undefined;
2362
2433
  };
2363
- const buildTableXml = (table, rows, sourceSharedStrings, styleBuilder) => {
2434
+ const buildTableXml = (table, rows, sourceSharedStrings, styleBuilder, customTableStyleNames) => {
2364
2435
  const rangeRef = table.ref;
2365
2436
  const columnCount = Math.max(table.columns?.length ?? 0, (table.range.endColumnIndex ?? table.range.startColumnIndex) -
2366
2437
  table.range.startColumnIndex +
@@ -2434,17 +2505,71 @@ const buildTableXml = (table, rows, sourceSharedStrings, styleBuilder) => {
2434
2505
  })
2435
2506
  .join("")}</tableColumns>`;
2436
2507
  const isBuiltInStyle = table.theme && /^TableStyle(Light|Medium|Dark)\d+$/.test(table.theme);
2437
- const tableStyleName = isBuiltInStyle ? table.theme : "TableStyleMedium2";
2508
+ // A custom style written to `<tableStyles>` keeps its name here (#668 T-103).
2509
+ const isCustomStyle = !!table.theme && (customTableStyleNames?.has(table.theme) ?? false);
2510
+ const tableStyleName = isBuiltInStyle || isCustomStyle ? table.theme : "TableStyleMedium2";
2438
2511
  const tableStyleInfo = `<tableStyleInfo name="${tableStyleName}" showFirstColumn="${table.showFirstColumn ? 1 : 0}" showLastColumn="${table.showLastColumn ? 1 : 0}" showRowStripes="${table.showRowStripes ? 1 : 0}" showColumnStripes="${table.showColumnStripes ? 1 : 0}"/>`;
2512
+ // Excel's per-column "filter button" toggle: `<filterColumn hiddenButton="1">`
2513
+ // (the table-level toggle is the presence of `<autoFilter>` itself).
2514
+ const hiddenButtonColumns = new Set(columnNames
2515
+ .map((_, idx) => idx)
2516
+ .filter((idx) => table.columns?.[idx]?.filterButton === false));
2439
2517
  const autoFilter = headerRowCount > 0 && table.filterButton !== false
2440
2518
  ? (() => {
2441
- const filterColumns = buildFilterColumnsXml(table.filterSpecs ?? [], table.range, rows, table.headerRow ?? true, table.totalRow ?? false, styleBuilder);
2519
+ const filterColumns = buildFilterColumnsXml(table.filterSpecs ?? [], table.range, rows, table.headerRow ?? true, table.totalRow ?? false, styleBuilder, hiddenButtonColumns);
2442
2520
  return filterColumns
2443
2521
  ? `<autoFilter ref="${rangeRef}">${filterColumns}</autoFilter>`
2444
2522
  : `<autoFilter ref="${rangeRef}"/>`;
2445
2523
  })()
2446
2524
  : "";
2447
- return `${XML_DECLARATION}<table xmlns="${WORKSHEET_NS}" id="${table.id}" name="${escapeXmlAttr(table.name)}" displayName="${escapeXmlAttr(table.displayName)}" ref="${rangeRef}" totalsRowCount="${totalsRowCount}" headerRowCount="${headerRowCount}">${autoFilter}${tableColumns}${tableStyleInfo}</table>`;
2525
+ const sortState = buildTableSortStateXml(table, headerRowCount, totalsRowCount, styleBuilder);
2526
+ return `${XML_DECLARATION}<table xmlns="${WORKSHEET_NS}" id="${table.id}" name="${escapeXmlAttr(table.name)}" displayName="${escapeXmlAttr(table.displayName)}" ref="${rangeRef}" totalsRowCount="${totalsRowCount}" headerRowCount="${headerRowCount}">${autoFilter}${sortState}${tableColumns}${tableStyleInfo}</table>`;
2527
+ };
2528
+ /**
2529
+ * A table's persisted sort levels as `<sortState ref="<body>">` with one
2530
+ * `<sortCondition>` per level (`CT_SortCondition`: `ref` is the level's
2531
+ * column over the body rows, `descending`, `sortBy` + `dxfId` for a colour
2532
+ * level, `sortBy="icon"` + `iconSet`/`iconId`, and Excel's comma-joined
2533
+ * `customList`). Sits between `<autoFilter>` and `<tableColumns>` per
2534
+ * `CT_Table`. The inverse of the parser's `createSortSpecsFromSortState`.
2535
+ */
2536
+ const buildTableSortStateXml = (table, headerRowCount, totalsRowCount, styleBuilder) => {
2537
+ const sortSpecs = table.sortSpecs ?? [];
2538
+ const bodyStartRow = table.range.startRowIndex + headerRowCount;
2539
+ const bodyEndRow = table.range.endRowIndex - totalsRowCount;
2540
+ if (!sortSpecs.length || bodyEndRow < bodyStartRow)
2541
+ return "";
2542
+ const bodyRef = (startColumnIndex, endColumnIndex) => cellRangeToAddress(undefined, {
2543
+ startRowIndex: bodyStartRow,
2544
+ endRowIndex: bodyEndRow,
2545
+ startColumnIndex,
2546
+ endColumnIndex,
2547
+ });
2548
+ const conditions = sortSpecs.map((spec) => {
2549
+ const columnIndex = table.range.startColumnIndex + spec.dimensionIndex;
2550
+ const attrs = [`ref="${bodyRef(columnIndex, columnIndex)}"`];
2551
+ if (spec.sortOrder === "DESCENDING")
2552
+ attrs.push(`descending="1"`);
2553
+ const sortBy = spec.sortBy ?? "value";
2554
+ if (sortBy === "cellColor" || sortBy === "fontColor") {
2555
+ attrs.push(`sortBy="${sortBy}"`);
2556
+ const dxfId = spec.sortColor && styleBuilder
2557
+ ? styleBuilder.registerDifferentialFormat(sortBy === "cellColor"
2558
+ ? { backgroundColor: spec.sortColor }
2559
+ : { textFormat: { color: spec.sortColor } })
2560
+ : undefined;
2561
+ if (typeof dxfId === "number")
2562
+ attrs.push(`dxfId="${dxfId}"`);
2563
+ }
2564
+ else if (sortBy === "icon" && spec.sortIcon) {
2565
+ attrs.push(`sortBy="icon"`, `iconSet="${escapeXmlAttr(spec.sortIcon.iconSet)}"`, `iconId="${spec.sortIcon.iconId ?? 0}"`);
2566
+ }
2567
+ if (spec.customList?.length) {
2568
+ attrs.push(`customList="${escapeXmlAttr(spec.customList.join(","))}"`);
2569
+ }
2570
+ return `<sortCondition ${attrs.join(" ")}/>`;
2571
+ });
2572
+ return `<sortState ref="${bodyRef(table.range.startColumnIndex, table.range.endColumnIndex)}">${conditions.join("")}</sortState>`;
2448
2573
  };
2449
2574
  const getFilterVisibleValues = (spec, range, rows, headerRow = true, totalRow = false) => {
2450
2575
  const values = new Set();
@@ -2615,6 +2740,12 @@ const xmlNumAttr = (name, value) => typeof value === "number" && Number.isFinite
2615
2740
  * mandatory in practice (Excel always writes it), so the default margins go
2616
2741
  * out when the sheet has none of its own.
2617
2742
  */
2743
+ const buildPageMarginsXml = (print) => {
2744
+ const margins = print?.pageMargins;
2745
+ return margins && Object.values(margins).some((v) => typeof v === "number")
2746
+ ? `<pageMargins left="${margins.left ?? 0.7}" right="${margins.right ?? 0.7}" top="${margins.top ?? 0.75}" bottom="${margins.bottom ?? 0.75}" header="${margins.header ?? 0.3}" footer="${margins.footer ?? 0.3}"/>`
2747
+ : DEFAULT_PAGE_MARGINS;
2748
+ };
2618
2749
  const buildPrintSettingsXml = (print) => {
2619
2750
  const parts = [];
2620
2751
  const options = print?.printOptions;
@@ -2624,11 +2755,7 @@ const buildPrintSettingsXml = (print) => {
2624
2755
  xmlBoolAttr("gridLines", options?.gridLines);
2625
2756
  if (optionAttrs)
2626
2757
  parts.push(`<printOptions${optionAttrs}/>`);
2627
- const margins = print?.pageMargins;
2628
- parts.push(margins &&
2629
- Object.values(margins).some((v) => typeof v === "number")
2630
- ? `<pageMargins left="${margins.left ?? 0.7}" right="${margins.right ?? 0.7}" top="${margins.top ?? 0.75}" bottom="${margins.bottom ?? 0.75}" header="${margins.header ?? 0.3}" footer="${margins.footer ?? 0.3}"/>`
2631
- : DEFAULT_PAGE_MARGINS);
2758
+ parts.push(buildPageMarginsXml(print));
2632
2759
  const setup = print?.pageSetup;
2633
2760
  if (setup) {
2634
2761
  // Excel keeps `firstPageNumber` as a dormant value until the user picks
@@ -2693,13 +2820,27 @@ const buildPrintSettingsXml = (print) => {
2693
2820
  }
2694
2821
  return parts.join("");
2695
2822
  };
2696
- const buildFilterColumnsXml = (filterSpecs, range, rows, headerRow = true, totalRow = false, styleBuilder) => {
2697
- if (!filterSpecs?.length)
2698
- return "";
2699
- const entries = filterSpecs
2700
- .map((spec) => buildFilterColumnXml(spec, range, rows, headerRow, totalRow, styleBuilder))
2701
- .filter((entry) => Boolean(entry));
2702
- return entries.join("");
2823
+ const buildFilterColumnsXml = (filterSpecs, range, rows, headerRow = true, totalRow = false, styleBuilder,
2824
+ /** Table-relative column ids whose header hides its filter button. */
2825
+ hiddenButtonColumns) => {
2826
+ const entries = new Map();
2827
+ for (const spec of filterSpecs ?? []) {
2828
+ const xml = buildFilterColumnXml(spec, range, rows, headerRow, totalRow, styleBuilder);
2829
+ if (xml)
2830
+ entries.set(spec.columnIndex, xml);
2831
+ }
2832
+ // `hiddenButton="1"` rides the column's criteria element when it has one,
2833
+ // else a bare `<filterColumn/>` carries it.
2834
+ hiddenButtonColumns?.forEach((colId) => {
2835
+ const existing = entries.get(colId);
2836
+ entries.set(colId, existing
2837
+ ? existing.replace(`<filterColumn colId="${colId}">`, `<filterColumn colId="${colId}" hiddenButton="1">`)
2838
+ : `<filterColumn colId="${colId}" hiddenButton="1"/>`);
2839
+ });
2840
+ return [...entries.entries()]
2841
+ .sort(([a], [b]) => a - b)
2842
+ .map(([, xml]) => xml)
2843
+ .join("");
2703
2844
  };
2704
2845
  const buildFilterColumnXml = (spec, range, rows, headerRow = true, totalRow = false, styleBuilder) => {
2705
2846
  const parts = [];
@@ -2728,7 +2869,9 @@ const buildFilterColumnXml = (spec, range, rows, headerRow = true, totalRow = fa
2728
2869
  // above-average …) takes precedence over a value-list and is emitted as
2729
2870
  // <customFilters> or <dynamicFilter>. Falls back to the value list when the
2730
2871
  // condition has no AutoFilter representation.
2731
- const conditionXml = buildAutoFilterConditionXml(spec.filterCriteria?.condition);
2872
+ const conditionXml = spec.filterCriteria
2873
+ ? buildAutoFilterCriteriaXml(spec.filterCriteria)
2874
+ : null;
2732
2875
  if (conditionXml) {
2733
2876
  parts.push(conditionXml);
2734
2877
  }
@@ -2758,9 +2901,9 @@ const gridRangeToRef = (range) => {
2758
2901
  return undefined;
2759
2902
  }
2760
2903
  };
2761
- const groupEmbedsBySheet = (embeds) => {
2904
+ const groupEmbedsBySheet = (embeds, chartCount) => {
2762
2905
  const result = new Map();
2763
- (embeds ?? []).forEach((embed, order) => {
2906
+ (embeds ?? []).forEach((embed, index) => {
2764
2907
  if (!embed.position?.overlayPosition)
2765
2908
  return;
2766
2909
  if (embed.spec?.type !== "image")
@@ -2779,13 +2922,14 @@ const groupEmbedsBySheet = (embeds) => {
2779
2922
  adjustments: Object.values(adjustments).some((v) => v !== undefined)
2780
2923
  ? adjustments
2781
2924
  : undefined,
2782
- order,
2925
+ order: embed.position.zOrder ?? chartCount + index,
2783
2926
  rowIndex: overlay.anchorCell.rowIndex,
2784
2927
  columnIndex: overlay.anchorCell.columnIndex,
2785
2928
  offsetX: overlay.offsetXPixels ?? 0,
2786
2929
  offsetY: overlay.offsetYPixels ?? 0,
2787
2930
  width: overlay.widthPixels ?? 120,
2788
2931
  height: overlay.heightPixels ?? 120,
2932
+ overlay,
2789
2933
  };
2790
2934
  const list = result.get(embed.position.sheetId) ?? [];
2791
2935
  list.push(placement);
@@ -2793,9 +2937,9 @@ const groupEmbedsBySheet = (embeds) => {
2793
2937
  });
2794
2938
  return result;
2795
2939
  };
2796
- const groupTextBoxesBySheet = (embeds) => {
2940
+ const groupTextBoxesBySheet = (embeds, chartCount) => {
2797
2941
  const result = new Map();
2798
- (embeds ?? []).forEach((embed, order) => {
2942
+ (embeds ?? []).forEach((embed, index) => {
2799
2943
  if (!embed.position?.overlayPosition)
2800
2944
  return;
2801
2945
  if (embed.spec?.type !== "textbox")
@@ -2803,7 +2947,7 @@ const groupTextBoxesBySheet = (embeds) => {
2803
2947
  const overlay = embed.position.overlayPosition;
2804
2948
  const placement = {
2805
2949
  textBox: embed,
2806
- order,
2950
+ order: embed.position.zOrder ?? chartCount + index,
2807
2951
  rowIndex: overlay.anchorCell.rowIndex,
2808
2952
  columnIndex: overlay.anchorCell.columnIndex,
2809
2953
  offsetX: overlay.offsetXPixels ?? 0,
@@ -2818,9 +2962,9 @@ const groupTextBoxesBySheet = (embeds) => {
2818
2962
  return result;
2819
2963
  };
2820
2964
  /** Form-control embeds (`spec.type === "formControl"`) by sheet, in host order. */
2821
- const groupFormControlsBySheet = (embeds) => {
2965
+ const groupFormControlsBySheet = (embeds, chartCount) => {
2822
2966
  const result = new Map();
2823
- (embeds ?? []).forEach((embed, order) => {
2967
+ (embeds ?? []).forEach((embed, index) => {
2824
2968
  if (!embed.position?.overlayPosition)
2825
2969
  return;
2826
2970
  if (embed.spec?.type !== "formControl")
@@ -2828,7 +2972,7 @@ const groupFormControlsBySheet = (embeds) => {
2828
2972
  const overlay = embed.position.overlayPosition;
2829
2973
  const placement = {
2830
2974
  shape: embed,
2831
- order,
2975
+ order: embed.position.zOrder ?? chartCount + index,
2832
2976
  rowIndex: overlay.anchorCell.rowIndex,
2833
2977
  columnIndex: overlay.anchorCell.columnIndex,
2834
2978
  offsetX: overlay.offsetXPixels ?? 0,
@@ -2883,18 +3027,33 @@ const buildSheetFormControls = ({ sheet, controls, relationships, ctrlProps, zIn
2883
3027
  const heightPx = Math.max(5, placement.height ?? 20);
2884
3028
  const columnSize = (index) => getColumnWidthPx(sheet, index, true);
2885
3029
  const rowSize = (index) => getRowHeightPx(sheet, index, true);
2886
- const toColumn = resolveAnchorEndpoint({
2887
- startIndex: fromColumn,
2888
- startOffset: offsetX,
2889
- span: widthPx,
2890
- getSize: columnSize,
2891
- });
2892
- const toRow = resolveAnchorEndpoint({
2893
- startIndex: fromRow,
2894
- startOffset: offsetY,
2895
- span: heightPx,
2896
- getSize: rowSize,
2897
- });
3030
+ // A "Move and size" control keeps its persisted second anchor (#668 C-46)
3031
+ const overlay = placement.shape.position?.overlayPosition;
3032
+ const persistedTo = controlAnchorFlags(spec, overlay).sizeWithCells && overlay?.toCell
3033
+ ? overlay.toCell
3034
+ : undefined;
3035
+ const toColumn = persistedTo
3036
+ ? {
3037
+ index: Math.max(0, persistedTo.columnIndex - 1),
3038
+ offset: Math.max(0, overlay?.toOffsetXPixels ?? 0),
3039
+ }
3040
+ : resolveAnchorEndpoint({
3041
+ startIndex: fromColumn,
3042
+ startOffset: offsetX,
3043
+ span: widthPx,
3044
+ getSize: columnSize,
3045
+ });
3046
+ const toRow = persistedTo
3047
+ ? {
3048
+ index: Math.max(0, persistedTo.rowIndex - 1),
3049
+ offset: Math.max(0, overlay?.toOffsetYPixels ?? 0),
3050
+ }
3051
+ : resolveAnchorEndpoint({
3052
+ startIndex: fromRow,
3053
+ startOffset: offsetY,
3054
+ span: heightPx,
3055
+ getSize: rowSize,
3056
+ });
2898
3057
  const partIndex = ctrlProps.parts.length + 1;
2899
3058
  const path = `xl/ctrlProps/ctrlProp${partIndex}.xml`;
2900
3059
  ctrlProps.parts.push({ path, xml: buildCtrlPropXml(spec) });
@@ -2916,11 +3075,13 @@ const buildSheetFormControls = ({ sheet, controls, relationships, ctrlProps, zIn
2916
3075
  row: toRow.index,
2917
3076
  rowOff: toRow.offset * EMUS_PER_PIXEL,
2918
3077
  },
3078
+ overlay,
2919
3079
  }));
2920
3080
  vmlShapes.push(buildFormControlVmlShape({
2921
3081
  spec,
2922
3082
  shapeId,
2923
3083
  zIndex: zIndexStart + position + 1,
3084
+ overlay,
2924
3085
  geometry: {
2925
3086
  leftPx: sumSizes(fromColumn, columnSize) + offsetX,
2926
3087
  topPx: sumSizes(fromRow, rowSize) + offsetY,
@@ -2941,9 +3102,9 @@ const buildSheetFormControls = ({ sheet, controls, relationships, ctrlProps, zIn
2941
3102
  });
2942
3103
  return { controlsXml: buildControlsXml(controlElements), vmlShapes };
2943
3104
  };
2944
- const groupShapesBySheet = (embeds) => {
3105
+ const groupShapesBySheet = (embeds, chartCount) => {
2945
3106
  const result = new Map();
2946
- (embeds ?? []).forEach((embed, order) => {
3107
+ (embeds ?? []).forEach((embed, index) => {
2947
3108
  if (!embed.position?.overlayPosition)
2948
3109
  return;
2949
3110
  if (embed.spec?.type !== "shape")
@@ -2951,7 +3112,7 @@ const groupShapesBySheet = (embeds) => {
2951
3112
  const overlay = embed.position.overlayPosition;
2952
3113
  const placement = {
2953
3114
  shape: embed,
2954
- order,
3115
+ order: embed.position.zOrder ?? chartCount + index,
2955
3116
  rowIndex: overlay.anchorCell.rowIndex,
2956
3117
  columnIndex: overlay.anchorCell.columnIndex,
2957
3118
  offsetX: overlay.offsetXPixels ?? 0,
@@ -2967,12 +3128,13 @@ const groupShapesBySheet = (embeds) => {
2967
3128
  };
2968
3129
  const groupChartsBySheet = (charts) => {
2969
3130
  const result = new Map();
2970
- for (const chart of charts ?? []) {
3131
+ (charts ?? []).forEach((chart, index) => {
2971
3132
  const overlay = chart.position?.overlayPosition;
2972
3133
  if (!overlay)
2973
- continue;
3134
+ return;
2974
3135
  const placement = {
2975
3136
  chart,
3137
+ order: chart.position.zOrder ?? index,
2976
3138
  rowIndex: overlay.anchorCell.rowIndex,
2977
3139
  columnIndex: overlay.anchorCell.columnIndex,
2978
3140
  offsetX: overlay.offsetXPixels ?? 0,
@@ -2983,7 +3145,7 @@ const groupChartsBySheet = (charts) => {
2983
3145
  const list = result.get(chart.position.sheetId) ?? [];
2984
3146
  list.push(placement);
2985
3147
  result.set(chart.position.sheetId, list);
2986
- }
3148
+ });
2987
3149
  return result;
2988
3150
  };
2989
3151
  const groupSlicersBySheet = (slicers) => {
@@ -3554,7 +3716,7 @@ const buildConditionalFormattingRuleXml = ({ rule, styleBuilder, priority, baseC
3554
3716
  rules.push(dataBarXml);
3555
3717
  return rules.length ? rules : undefined;
3556
3718
  };
3557
- const buildDrawingParts = async ({ placements, textBoxes, shapes = [], startImageIndex, startChartIndex, startChartExIndex = 1, charts, slicers, sheet, sheetNameById, }) => {
3719
+ const buildDrawingParts = async ({ placements, textBoxes, shapes = [], startImageIndex, startChartIndex, startChartExIndex = 1, charts, slicers, sheet, sheetNameById, resolvePivotSourceName, }) => {
3558
3720
  if (!placements.length &&
3559
3721
  !textBoxes.length &&
3560
3722
  !shapes.length &&
@@ -3571,6 +3733,12 @@ const buildDrawingParts = async ({ placements, textBoxes, shapes = [], startImag
3571
3733
  let frameId = 1;
3572
3734
  const chartEntries = [];
3573
3735
  const chartExEntries = [];
3736
+ // Charts and embeds (text boxes, shapes, images) are written back-to-front
3737
+ // by their shared z-order — anchor order IS Excel's z-order, so a picture
3738
+ // sent behind a chart must be emitted before it regardless of kind.
3739
+ // Placements without an order (in-cell images) go last, in their own order.
3740
+ const orderedAnchors = [];
3741
+ const orderOf = (order) => order ?? Number.MAX_SAFE_INTEGER;
3574
3742
  for (const placement of charts) {
3575
3743
  if (isChartExType(placement.chart.spec.chartType)) {
3576
3744
  // Treemap / sunburst live in a chartex part with an mc:AlternateContent anchor.
@@ -3578,7 +3746,16 @@ const buildDrawingParts = async ({ placements, textBoxes, shapes = [], startImag
3578
3746
  if (!chartExXml)
3579
3747
  continue;
3580
3748
  const relId = relationships.add(CHART_EX_REL_TYPE, `../charts/chartEx${chartExIndex}.xml`);
3581
- anchors.push(createChartAnchor({ sheet, placement, relId, frameId, chartEx: true }));
3749
+ orderedAnchors.push({
3750
+ order: placement.order,
3751
+ xml: createChartAnchor({
3752
+ sheet,
3753
+ placement,
3754
+ relId,
3755
+ frameId,
3756
+ chartEx: true,
3757
+ }),
3758
+ });
3582
3759
  chartExEntries.push({
3583
3760
  path: `xl/charts/chartEx${chartExIndex}.xml`,
3584
3761
  xml: chartExXml,
@@ -3587,16 +3764,19 @@ const buildDrawingParts = async ({ placements, textBoxes, shapes = [], startImag
3587
3764
  frameId += 1;
3588
3765
  continue;
3589
3766
  }
3590
- const chartXml = buildChartXml(placement.chart, sheetNameById);
3767
+ const chartXml = buildChartXml(placement.chart, sheetNameById, resolvePivotSourceName);
3591
3768
  if (!chartXml)
3592
3769
  continue;
3593
3770
  const relId = relationships.add(CHART_REL_TYPE, `../charts/chart${chartIndex}.xml`);
3594
- anchors.push(createChartAnchor({
3595
- sheet,
3596
- placement,
3597
- relId,
3598
- frameId,
3599
- }));
3771
+ orderedAnchors.push({
3772
+ order: placement.order,
3773
+ xml: createChartAnchor({
3774
+ sheet,
3775
+ placement,
3776
+ relId,
3777
+ frameId,
3778
+ }),
3779
+ });
3600
3780
  chartEntries.push({
3601
3781
  path: `xl/charts/chart${chartIndex}.xml`,
3602
3782
  xml: chartXml,
@@ -3612,14 +3792,8 @@ const buildDrawingParts = async ({ placements, textBoxes, shapes = [], startImag
3612
3792
  }));
3613
3793
  frameId += 1;
3614
3794
  }
3615
- // Embeds (text boxes, shapes, images) keep the host's `embeds` order in the
3616
- // drawing part — anchor order IS Excel's z-order, so an image drawn over a
3617
- // shape must be emitted after it regardless of kind. Placements without an
3618
- // order (in-cell images) go last, in their own order.
3619
- const embedAnchors = [];
3620
- const orderOf = (order) => order ?? Number.MAX_SAFE_INTEGER;
3621
3795
  for (const placement of textBoxes) {
3622
- embedAnchors.push({
3796
+ orderedAnchors.push({
3623
3797
  order: orderOf(placement.order),
3624
3798
  xml: createTextBoxAnchor({
3625
3799
  sheet,
@@ -3630,7 +3804,7 @@ const buildDrawingParts = async ({ placements, textBoxes, shapes = [], startImag
3630
3804
  frameId += 1;
3631
3805
  }
3632
3806
  for (const placement of shapes) {
3633
- embedAnchors.push({
3807
+ orderedAnchors.push({
3634
3808
  order: orderOf(placement.order),
3635
3809
  xml: createShapeAnchor({
3636
3810
  sheet,
@@ -3654,7 +3828,7 @@ const buildDrawingParts = async ({ placements, textBoxes, shapes = [], startImag
3654
3828
  extension: buffer.extension,
3655
3829
  });
3656
3830
  const relId = relationships.add(IMAGE_REL_TYPE, `../media/image${imageIndex}.${buffer.extension}`);
3657
- embedAnchors.push({
3831
+ orderedAnchors.push({
3658
3832
  order: orderOf(placement.order),
3659
3833
  xml: createTwoCellAnchor({
3660
3834
  sheet,
@@ -3666,8 +3840,8 @@ const buildDrawingParts = async ({ placements, textBoxes, shapes = [], startImag
3666
3840
  imageIndex += 1;
3667
3841
  pictureId += 1;
3668
3842
  }
3669
- embedAnchors.sort((a, b) => a.order - b.order);
3670
- for (const entry of embedAnchors)
3843
+ orderedAnchors.sort((a, b) => a.order - b.order);
3844
+ for (const entry of orderedAnchors)
3671
3845
  anchors.push(entry.xml);
3672
3846
  const drawingXml = `${XML_DECLARATION}<xdr:wsDr xmlns:xdr="http://schemas.openxmlformats.org/drawingml/2006/spreadsheetDrawing" xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:r="${REL_NS}">${anchors.join("")}</xdr:wsDr>`;
3673
3847
  return {
@@ -3690,25 +3864,21 @@ const createTwoCellAnchor = ({ placement, relId, pictureId, sheet, }) => {
3690
3864
  const defaultHeight = placement.kind === "cell" ? getRowHeightPx(sheet, fromRow) : 120;
3691
3865
  const widthPx = Math.max(5, placement.width ?? defaultWidth);
3692
3866
  const heightPx = Math.max(5, placement.height ?? defaultHeight);
3693
- const toColumn = resolveAnchorEndpoint({
3694
- startIndex: fromColumn,
3695
- startOffset: offsetX,
3696
- span: widthPx,
3697
- getSize: (index) => getColumnWidthPx(sheet, index, true),
3698
- });
3699
- const toRow = resolveAnchorEndpoint({
3700
- startIndex: fromRow,
3701
- startOffset: offsetY,
3702
- span: heightPx,
3703
- getSize: (index) => getRowHeightPx(sheet, index, true),
3704
- });
3705
- const fromXml = `<xdr:from><xdr:col>${fromColumn}</xdr:col><xdr:colOff>${Math.round(offsetX * EMUS_PER_PIXEL)}</xdr:colOff><xdr:row>${fromRow}</xdr:row><xdr:rowOff>${Math.round(offsetY * EMUS_PER_PIXEL)}</xdr:rowOff></xdr:from>`;
3706
- const toXml = `<xdr:to><xdr:col>${toColumn.index}</xdr:col><xdr:colOff>${Math.round(toColumn.offset * EMUS_PER_PIXEL)}</xdr:colOff><xdr:row>${toRow.index}</xdr:row><xdr:rowOff>${Math.round(toRow.offset * EMUS_PER_PIXEL)}</xdr:rowOff></xdr:to>`;
3707
3867
  const { blipChildren, srcRectXml } = buildPictureAdjustmentsXml(placement.adjustments);
3708
3868
  const blipXml = blipChildren
3709
3869
  ? `<a:blip r:embed="${relId}">${blipChildren}</a:blip>`
3710
3870
  : `<a:blip r:embed="${relId}"/>`;
3711
- return `<xdr:twoCellAnchor editAs="oneCell">${fromXml}${toXml}<xdr:pic><xdr:nvPicPr><xdr:cNvPr id="${pictureId}" name="Picture ${pictureId}"/><xdr:cNvPicPr/></xdr:nvPicPr><xdr:blipFill>${blipXml}${srcRectXml}<a:stretch><a:fillRect/></a:stretch></xdr:blipFill><xdr:spPr><a:xfrm><a:off x="0" y="0"/><a:ext cx="${Math.round(widthPx * EMUS_PER_PIXEL)}" cy="${Math.round(heightPx * EMUS_PER_PIXEL)}"/></a:xfrm><a:prstGeom prst="rect"><a:avLst/></a:prstGeom></xdr:spPr></xdr:pic><xdr:clientData/></xdr:twoCellAnchor>`;
3871
+ return buildDrawingAnchorXml({
3872
+ sheet,
3873
+ overlay: placement.overlay,
3874
+ fromRow,
3875
+ fromColumn,
3876
+ offsetX,
3877
+ offsetY,
3878
+ widthPx,
3879
+ heightPx,
3880
+ body: `<xdr:pic><xdr:nvPicPr><xdr:cNvPr id="${pictureId}" name="Picture ${pictureId}"/><xdr:cNvPicPr/></xdr:nvPicPr><xdr:blipFill>${blipXml}${srcRectXml}<a:stretch><a:fillRect/></a:stretch></xdr:blipFill><xdr:spPr><a:xfrm><a:off x="0" y="0"/><a:ext cx="${Math.round(widthPx * EMUS_PER_PIXEL)}" cy="${Math.round(heightPx * EMUS_PER_PIXEL)}"/></a:xfrm><a:prstGeom prst="rect"><a:avLst/></a:prstGeom></xdr:spPr></xdr:pic>`,
3881
+ });
3712
3882
  };
3713
3883
  /** Fraction (0…1) → DrawingML per-mille percent (100000 = 100%). */
3714
3884
  const fractionToPct = (value) => Math.round(Math.max(-1, Math.min(1, value)) * 100000);
@@ -3759,29 +3929,30 @@ const createChartAnchor = ({ placement, relId, frameId, sheet, chartEx = false,
3759
3929
  const offsetY = Math.max(0, placement.offsetY ?? 0);
3760
3930
  const widthPx = Math.max(5, placement.width ?? 300);
3761
3931
  const heightPx = Math.max(5, placement.height ?? 200);
3762
- const toColumn = resolveAnchorEndpoint({
3763
- startIndex: fromColumn,
3764
- startOffset: offsetX,
3765
- span: widthPx,
3766
- getSize: (index) => getColumnWidthPx(sheet, index, true),
3767
- });
3768
- const toRow = resolveAnchorEndpoint({
3769
- startIndex: fromRow,
3770
- startOffset: offsetY,
3771
- span: heightPx,
3772
- getSize: (index) => getRowHeightPx(sheet, index, true),
3773
- });
3774
- const fromXml = `<xdr:from><xdr:col>${fromColumn}</xdr:col><xdr:colOff>${Math.round(offsetX * EMUS_PER_PIXEL)}</xdr:colOff><xdr:row>${fromRow}</xdr:row><xdr:rowOff>${Math.round(offsetY * EMUS_PER_PIXEL)}</xdr:rowOff></xdr:from>`;
3775
- const toXml = `<xdr:to><xdr:col>${toColumn.index}</xdr:col><xdr:colOff>${Math.round(toColumn.offset * EMUS_PER_PIXEL)}</xdr:colOff><xdr:row>${toRow.index}</xdr:row><xdr:rowOff>${Math.round(toRow.offset * EMUS_PER_PIXEL)}</xdr:rowOff></xdr:to>`;
3776
- if (chartEx) {
3777
- return `<xdr:twoCellAnchor>${fromXml}${toXml}${buildChartExGraphicFrameXml({
3932
+ const body = chartEx
3933
+ ? buildChartExGraphicFrameXml({
3778
3934
  relId,
3779
3935
  frameId,
3780
3936
  widthEmu: Math.round(widthPx * EMUS_PER_PIXEL),
3781
3937
  heightEmu: Math.round(heightPx * EMUS_PER_PIXEL),
3782
- })}<xdr:clientData/></xdr:twoCellAnchor>`;
3783
- }
3784
- return `<xdr:twoCellAnchor>${fromXml}${toXml}<xdr:graphicFrame><xdr:nvGraphicFramePr><xdr:cNvPr id="${frameId}" name="Chart ${frameId}"/><xdr:cNvGraphicFramePr/></xdr:nvGraphicFramePr><xdr:xfrm><a:off x="0" y="0"/><a:ext cx="${Math.round(widthPx * EMUS_PER_PIXEL)}" cy="${Math.round(heightPx * EMUS_PER_PIXEL)}"/></xdr:xfrm><a:graphic><a:graphicData uri="http://schemas.openxmlformats.org/drawingml/2006/chart"><c:chart xmlns:c="http://schemas.openxmlformats.org/drawingml/2006/chart" xmlns:r="${REL_NS}" r:id="${relId}"/></a:graphicData></a:graphic></xdr:graphicFrame><xdr:clientData/></xdr:twoCellAnchor>`;
3938
+ })
3939
+ : `<xdr:graphicFrame><xdr:nvGraphicFramePr><xdr:cNvPr id="${frameId}" name="Chart ${frameId}"/><xdr:cNvGraphicFramePr/></xdr:nvGraphicFramePr><xdr:xfrm><a:off x="0" y="0"/><a:ext cx="${Math.round(widthPx * EMUS_PER_PIXEL)}" cy="${Math.round(heightPx * EMUS_PER_PIXEL)}"/></xdr:xfrm><a:graphic><a:graphicData uri="http://schemas.openxmlformats.org/drawingml/2006/chart"><c:chart xmlns:c="http://schemas.openxmlformats.org/drawingml/2006/chart" xmlns:r="${REL_NS}" r:id="${relId}"/></a:graphicData></a:graphic></xdr:graphicFrame>`;
3940
+ const overlay = placement.chart.position?.overlayPosition;
3941
+ return buildDrawingAnchorXml({
3942
+ sheet,
3943
+ // A chartsheet has no cells to anchor to: Excel only reads an
3944
+ // `absoluteAnchor` there, whatever placement the chart carries.
3945
+ overlay: sheet.type === "chartsheet" && overlay
3946
+ ? { ...overlay, placement: "absolute" }
3947
+ : overlay,
3948
+ fromRow,
3949
+ fromColumn,
3950
+ offsetX,
3951
+ offsetY,
3952
+ widthPx,
3953
+ heightPx,
3954
+ body,
3955
+ });
3785
3956
  };
3786
3957
  const createSlicerAnchor = ({ placement, frameId, sheet, }) => {
3787
3958
  const fromRow = Math.max(0, placement.rowIndex - 1);
@@ -3790,21 +3961,42 @@ const createSlicerAnchor = ({ placement, frameId, sheet, }) => {
3790
3961
  const offsetY = Math.max(0, placement.offsetY ?? 0);
3791
3962
  const widthPx = Math.max(50, placement.width ?? 200);
3792
3963
  const heightPx = Math.max(50, placement.height ?? 200);
3793
- const toColumn = resolveAnchorEndpoint({
3794
- startIndex: fromColumn,
3795
- startOffset: offsetX,
3796
- span: widthPx,
3797
- getSize: (index) => getColumnWidthPx(sheet, index, true),
3798
- });
3799
- const toRow = resolveAnchorEndpoint({
3800
- startIndex: fromRow,
3801
- startOffset: offsetY,
3802
- span: heightPx,
3803
- getSize: (index) => getRowHeightPx(sheet, index, true),
3804
- });
3964
+ // Excel keeps a slicer in a `twoCellAnchor` whatever its placement and
3965
+ // spells the other two modes as `editAs` (#668 C-46); a "Move and size"
3966
+ // slicer keeps its persisted second anchor.
3967
+ const overlay = placement.overlay;
3968
+ const mode = overlay?.placement ?? "moveAndSize";
3969
+ const persistedTo = mode === "moveAndSize" ? overlay?.toCell : undefined;
3970
+ const editAs = mode === "moveOnly"
3971
+ ? ' editAs="oneCell"'
3972
+ : mode === "absolute"
3973
+ ? ' editAs="absolute"'
3974
+ : "";
3975
+ const toColumn = persistedTo
3976
+ ? {
3977
+ index: Math.max(0, persistedTo.columnIndex - 1),
3978
+ offset: Math.max(0, overlay?.toOffsetXPixels ?? 0),
3979
+ }
3980
+ : resolveAnchorEndpoint({
3981
+ startIndex: fromColumn,
3982
+ startOffset: offsetX,
3983
+ span: widthPx,
3984
+ getSize: (index) => getColumnWidthPx(sheet, index, true),
3985
+ });
3986
+ const toRow = persistedTo
3987
+ ? {
3988
+ index: Math.max(0, persistedTo.rowIndex - 1),
3989
+ offset: Math.max(0, overlay?.toOffsetYPixels ?? 0),
3990
+ }
3991
+ : resolveAnchorEndpoint({
3992
+ startIndex: fromRow,
3993
+ startOffset: offsetY,
3994
+ span: heightPx,
3995
+ getSize: (index) => getRowHeightPx(sheet, index, true),
3996
+ });
3805
3997
  const fromXml = `<xdr:from><xdr:col>${fromColumn}</xdr:col><xdr:colOff>${Math.round(offsetX * EMUS_PER_PIXEL)}</xdr:colOff><xdr:row>${fromRow}</xdr:row><xdr:rowOff>${Math.round(offsetY * EMUS_PER_PIXEL)}</xdr:rowOff></xdr:from>`;
3806
3998
  const toXml = `<xdr:to><xdr:col>${toColumn.index}</xdr:col><xdr:colOff>${Math.round(toColumn.offset * EMUS_PER_PIXEL)}</xdr:colOff><xdr:row>${toRow.index}</xdr:row><xdr:rowOff>${Math.round(toRow.offset * EMUS_PER_PIXEL)}</xdr:rowOff></xdr:to>`;
3807
- return `<xdr:twoCellAnchor editAs="absolute">${fromXml}${toXml}<mc:AlternateContent xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"><mc:Choice xmlns:sle15="http://schemas.microsoft.com/office/drawing/2012/slicer" Requires="sle15"><xdr:graphicFrame macro=""><xdr:nvGraphicFramePr><xdr:cNvPr id="${frameId}" name="${escapeXmlAttr(placement.title)}"/><xdr:cNvGraphicFramePr/></xdr:nvGraphicFramePr><xdr:xfrm><a:off x="0" y="0"/><a:ext cx="${Math.round(widthPx * EMUS_PER_PIXEL)}" cy="${Math.round(heightPx * EMUS_PER_PIXEL)}"/></xdr:xfrm><a:graphic><a:graphicData uri="http://schemas.microsoft.com/office/drawing/2010/slicer"><sle:slicer xmlns:sle="http://schemas.microsoft.com/office/drawing/2010/slicer" name="${escapeXmlAttr(placement.title)}"/></a:graphicData></a:graphic></xdr:graphicFrame></mc:Choice></mc:AlternateContent><xdr:clientData/></xdr:twoCellAnchor>`;
3999
+ return `<xdr:twoCellAnchor${editAs}>${fromXml}${toXml}<mc:AlternateContent xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"><mc:Choice xmlns:sle15="http://schemas.microsoft.com/office/drawing/2012/slicer" Requires="sle15"><xdr:graphicFrame macro=""><xdr:nvGraphicFramePr><xdr:cNvPr id="${frameId}" name="${escapeXmlAttr(placement.title)}"/><xdr:cNvGraphicFramePr/></xdr:nvGraphicFramePr><xdr:xfrm><a:off x="0" y="0"/><a:ext cx="${Math.round(widthPx * EMUS_PER_PIXEL)}" cy="${Math.round(heightPx * EMUS_PER_PIXEL)}"/></xdr:xfrm><a:graphic><a:graphicData uri="http://schemas.microsoft.com/office/drawing/2010/slicer"><sle:slicer xmlns:sle="http://schemas.microsoft.com/office/drawing/2010/slicer" name="${escapeXmlAttr(placement.title)}"/></a:graphicData></a:graphic></xdr:graphicFrame></mc:Choice></mc:AlternateContent><xdr:clientData/></xdr:twoCellAnchor>`;
3808
4000
  };
3809
4001
  const createTextBoxAnchor = ({ placement, frameId, sheet, }) => {
3810
4002
  const fromRow = Math.max(0, placement.rowIndex - 1);
@@ -3813,24 +4005,20 @@ const createTextBoxAnchor = ({ placement, frameId, sheet, }) => {
3813
4005
  const offsetY = Math.max(0, placement.offsetY ?? 0);
3814
4006
  const widthPx = Math.max(50, placement.width ?? 200);
3815
4007
  const heightPx = Math.max(50, placement.height ?? 60);
3816
- const toColumn = resolveAnchorEndpoint({
3817
- startIndex: fromColumn,
3818
- startOffset: offsetX,
3819
- span: widthPx,
3820
- getSize: (index) => getColumnWidthPx(sheet, index, true),
3821
- });
3822
- const toRow = resolveAnchorEndpoint({
3823
- startIndex: fromRow,
3824
- startOffset: offsetY,
3825
- span: heightPx,
3826
- getSize: (index) => getRowHeightPx(sheet, index, true),
3827
- });
3828
- const fromXml = `<xdr:from><xdr:col>${fromColumn}</xdr:col><xdr:colOff>${Math.round(offsetX * EMUS_PER_PIXEL)}</xdr:colOff><xdr:row>${fromRow}</xdr:row><xdr:rowOff>${Math.round(offsetY * EMUS_PER_PIXEL)}</xdr:rowOff></xdr:from>`;
3829
- const toXml = `<xdr:to><xdr:col>${toColumn.index}</xdr:col><xdr:colOff>${Math.round(toColumn.offset * EMUS_PER_PIXEL)}</xdr:colOff><xdr:row>${toRow.index}</xdr:row><xdr:rowOff>${Math.round(toRow.offset * EMUS_PER_PIXEL)}</xdr:rowOff></xdr:to>`;
3830
4008
  const spec = placement.textBox.spec;
3831
4009
  const textBodyXml = buildTextBodyXml(spec);
3832
4010
  const shapePropertiesXml = buildShapePropertiesXml(spec, widthPx, heightPx);
3833
- return `<xdr:twoCellAnchor editAs="oneCell">${fromXml}${toXml}<xdr:sp><xdr:nvSpPr><xdr:cNvPr id="${frameId}" name="TextBox ${frameId}"/><xdr:cNvSpPr txBox="1"/></xdr:nvSpPr>${shapePropertiesXml}${textBodyXml}</xdr:sp><xdr:clientData/></xdr:twoCellAnchor>`;
4011
+ return buildDrawingAnchorXml({
4012
+ sheet,
4013
+ overlay: placement.textBox.position?.overlayPosition,
4014
+ fromRow,
4015
+ fromColumn,
4016
+ offsetX,
4017
+ offsetY,
4018
+ widthPx,
4019
+ heightPx,
4020
+ body: `<xdr:sp><xdr:nvSpPr><xdr:cNvPr id="${frameId}" name="TextBox ${frameId}"/><xdr:cNvSpPr txBox="1"/></xdr:nvSpPr>${shapePropertiesXml}${textBodyXml}</xdr:sp>`,
4021
+ });
3834
4022
  };
3835
4023
  /** ColorIndex slots → canonical DrawingML scheme names (inverse of the importer's map). */
3836
4024
  const THEME_INDEX_TO_SCHEME = [
@@ -3948,20 +4136,6 @@ const createShapeAnchor = ({ placement, frameId, sheet, }) => {
3948
4136
  const offsetY = Math.max(0, placement.offsetY ?? 0);
3949
4137
  const widthPx = Math.max(10, placement.width ?? 120);
3950
4138
  const heightPx = Math.max(10, placement.height ?? 60);
3951
- const toColumn = resolveAnchorEndpoint({
3952
- startIndex: fromColumn,
3953
- startOffset: offsetX,
3954
- span: widthPx,
3955
- getSize: (index) => getColumnWidthPx(sheet, index, true),
3956
- });
3957
- const toRow = resolveAnchorEndpoint({
3958
- startIndex: fromRow,
3959
- startOffset: offsetY,
3960
- span: heightPx,
3961
- getSize: (index) => getRowHeightPx(sheet, index, true),
3962
- });
3963
- const fromXml = `<xdr:from><xdr:col>${fromColumn}</xdr:col><xdr:colOff>${Math.round(offsetX * EMUS_PER_PIXEL)}</xdr:colOff><xdr:row>${fromRow}</xdr:row><xdr:rowOff>${Math.round(offsetY * EMUS_PER_PIXEL)}</xdr:rowOff></xdr:from>`;
3964
- const toXml = `<xdr:to><xdr:col>${toColumn.index}</xdr:col><xdr:colOff>${Math.round(toColumn.offset * EMUS_PER_PIXEL)}</xdr:colOff><xdr:row>${toRow.index}</xdr:row><xdr:rowOff>${Math.round(toRow.offset * EMUS_PER_PIXEL)}</xdr:rowOff></xdr:to>`;
3965
4139
  const xfrmAttrs = [
3966
4140
  placement.shape.rotation
3967
4141
  ? ` rot="${Math.round(placement.shape.rotation * 60000)}"`
@@ -3988,7 +4162,17 @@ const createShapeAnchor = ({ placement, frameId, sheet, }) => {
3988
4162
  ? buildTextBodyXml(spec.textBody)
3989
4163
  : `<xdr:txBody><a:bodyPr vertOverflow="clip" horzOverflow="clip" rtlCol="0" anchor="t"/><a:lstStyle/><a:p><a:endParaRPr lang="en-US"/></a:p></xdr:txBody>`;
3990
4164
  const name = escapeXmlAttr(spec.name ?? `Shape ${frameId}`);
3991
- return `<xdr:twoCellAnchor editAs="oneCell">${fromXml}${toXml}<xdr:sp><xdr:nvSpPr><xdr:cNvPr id="${frameId}" name="${name}"/><xdr:cNvSpPr/></xdr:nvSpPr>${spPr}${styleXml}${textBodyXml}</xdr:sp><xdr:clientData/></xdr:twoCellAnchor>`;
4165
+ return buildDrawingAnchorXml({
4166
+ sheet,
4167
+ overlay: placement.shape.position?.overlayPosition,
4168
+ fromRow,
4169
+ fromColumn,
4170
+ offsetX,
4171
+ offsetY,
4172
+ widthPx,
4173
+ heightPx,
4174
+ body: `<xdr:sp><xdr:nvSpPr><xdr:cNvPr id="${frameId}" name="${name}"/><xdr:cNvSpPr/></xdr:nvSpPr>${spPr}${styleXml}${textBodyXml}</xdr:sp>`,
4175
+ });
3992
4176
  };
3993
4177
  const buildTextWithTabs = (text, rPrXml) => {
3994
4178
  const parts = text.split("\t");
@@ -4239,6 +4423,60 @@ const buildShapePropertiesXml = (spec, widthPx, heightPx) => {
4239
4423
  }
4240
4424
  return `<xdr:spPr><a:xfrm><a:off x="0" y="0"/><a:ext cx="${Math.round(widthPx * EMUS_PER_PIXEL)}" cy="${Math.round(heightPx * EMUS_PER_PIXEL)}"/></a:xfrm><a:prstGeom prst="rect"><a:avLst/></a:prstGeom>${fillXml}${outlineXml}</xdr:spPr>`;
4241
4425
  };
4426
+ /**
4427
+ * Wrap a drawing object's body in the anchor its placement mode calls for
4428
+ * (#668 C-06). "Move and size with cells" (Excel's default) is a
4429
+ * `twoCellAnchor` whose `to` is the object's persisted second anchor — or,
4430
+ * for an object that has none yet, the cell its pixel extent reaches over
4431
+ * this sheet's row / column sizes. "Move but don't size" is a
4432
+ * `oneCellAnchor` (`from` + `ext`); "Don't move or size" is an
4433
+ * `absoluteAnchor` (`pos` + `ext`). The importers read all three back into
4434
+ * the same placement.
4435
+ */
4436
+ const buildDrawingAnchorXml = ({ sheet, overlay, fromRow, fromColumn, offsetX, offsetY, widthPx, heightPx, body, }) => {
4437
+ const emu = (px) => Math.round(px * EMUS_PER_PIXEL);
4438
+ const fromXml = `<xdr:from><xdr:col>${fromColumn}</xdr:col><xdr:colOff>${emu(offsetX)}</xdr:colOff><xdr:row>${fromRow}</xdr:row><xdr:rowOff>${emu(offsetY)}</xdr:rowOff></xdr:from>`;
4439
+ const extXml = `<xdr:ext cx="${emu(widthPx)}" cy="${emu(heightPx)}"/>`;
4440
+ const placement = overlay?.placement ?? "moveAndSize";
4441
+ if (placement === "moveOnly") {
4442
+ return `<xdr:oneCellAnchor>${fromXml}${extXml}${body}<xdr:clientData/></xdr:oneCellAnchor>`;
4443
+ }
4444
+ if (placement === "absolute") {
4445
+ let x = offsetX;
4446
+ for (let index = 0; index < fromColumn; index++) {
4447
+ x += getColumnWidthPx(sheet, index, true);
4448
+ }
4449
+ let y = offsetY;
4450
+ for (let index = 0; index < fromRow; index++) {
4451
+ y += getRowHeightPx(sheet, index, true);
4452
+ }
4453
+ return `<xdr:absoluteAnchor><xdr:pos x="${emu(x)}" y="${emu(y)}"/>${extXml}${body}<xdr:clientData/></xdr:absoluteAnchor>`;
4454
+ }
4455
+ const toColumn = overlay?.toCell
4456
+ ? {
4457
+ index: Math.max(0, overlay.toCell.columnIndex - 1),
4458
+ offset: Math.max(0, overlay.toOffsetXPixels ?? 0),
4459
+ }
4460
+ : resolveAnchorEndpoint({
4461
+ startIndex: fromColumn,
4462
+ startOffset: offsetX,
4463
+ span: widthPx,
4464
+ getSize: (index) => getColumnWidthPx(sheet, index, true),
4465
+ });
4466
+ const toRow = overlay?.toCell
4467
+ ? {
4468
+ index: Math.max(0, overlay.toCell.rowIndex - 1),
4469
+ offset: Math.max(0, overlay.toOffsetYPixels ?? 0),
4470
+ }
4471
+ : resolveAnchorEndpoint({
4472
+ startIndex: fromRow,
4473
+ startOffset: offsetY,
4474
+ span: heightPx,
4475
+ getSize: (index) => getRowHeightPx(sheet, index, true),
4476
+ });
4477
+ const toXml = `<xdr:to><xdr:col>${toColumn.index}</xdr:col><xdr:colOff>${emu(toColumn.offset)}</xdr:colOff><xdr:row>${toRow.index}</xdr:row><xdr:rowOff>${emu(toRow.offset)}</xdr:rowOff></xdr:to>`;
4478
+ return `<xdr:twoCellAnchor>${fromXml}${toXml}${body}<xdr:clientData/></xdr:twoCellAnchor>`;
4479
+ };
4242
4480
  const resolveAnchorEndpoint = ({ startIndex, startOffset, span, getSize, }) => {
4243
4481
  let index = startIndex;
4244
4482
  let offset = Math.max(0, startOffset);
@@ -4548,15 +4786,18 @@ const buildWorkbookSlicerCachesXml = (slicerCacheRelIds) => {
4548
4786
  .join("");
4549
4787
  return `<extLst><ext uri="{46BE6895-7355-4a93-B00E-2C351335B9C9}" xmlns:x15="http://schemas.microsoft.com/office/spreadsheetml/2010/11/main"><x15:slicerCaches xmlns:x14="http://schemas.microsoft.com/office/spreadsheetml/2009/9/main">${cacheEntries}</x15:slicerCaches></ext></extLst>`;
4550
4788
  };
4551
- const buildWorkbookRelationships = (sheetCount, slicerCacheCount = 0, externalLinkCount = 0, pivotCacheCount = 0, hasPersons = false) => {
4789
+ const buildWorkbookRelationships = (
4790
+ /** One entry per sheet in `<sheets>` order; `target` is relative to `xl/`. */
4791
+ sheetParts, slicerCacheCount = 0, externalLinkCount = 0, pivotCacheCount = 0, hasPersons = false) => {
4552
4792
  const relationships = [];
4553
4793
  const sheetRelIds = [];
4554
4794
  const slicerCacheRelIds = [];
4555
- for (let i = 0; i < sheetCount; i++) {
4795
+ const sheetCount = sheetParts.length;
4796
+ sheetParts.forEach((part, i) => {
4556
4797
  const relId = `rId${i + 1}`;
4557
4798
  sheetRelIds.push(relId);
4558
- relationships.push(`<Relationship Id="${relId}" Type="${REL_NS}/worksheet" Target="worksheets/sheet${i + 1}.xml"/>`);
4559
- }
4799
+ relationships.push(`<Relationship Id="${relId}" Type="${REL_NS}/${part.chartsheet ? "chartsheet" : "worksheet"}" Target="${part.target}"/>`);
4800
+ });
4560
4801
  const stylesId = `rId${sheetCount + 1}`;
4561
4802
  const themeId = `rId${sheetCount + 2}`;
4562
4803
  const sharedStringsId = `rId${sheetCount + 3}`;
@@ -4600,10 +4841,11 @@ const buildWorkbookRelationships = (sheetCount, slicerCacheCount = 0, externalLi
4600
4841
  slicerCacheRelationshipIds: slicerCacheRelIds,
4601
4842
  };
4602
4843
  };
4603
- const buildContentTypesXml = ({ sheetCount, tableCount, drawingSheets, mediaExtensions, chartCount, chartExCount = 0, slicerCount = 0, slicerCacheCount = 0, pivotTableCount = 0, pivotCacheCount = 0, commentsSheets = [], threadedCommentsSheets = [], hasPersons = false, externalLinkCount = 0, hasVmlDrawings = false, ctrlPropCount = 0, }) => {
4604
- const sheetOverrides = Array.from({ length: sheetCount }, (_, index) => {
4844
+ const buildContentTypesXml = ({ worksheetCount, chartsheetCount = 0, tableCount, drawingSheets, mediaExtensions, chartCount, chartExCount = 0, slicerCount = 0, slicerCacheCount = 0, pivotTableCount = 0, pivotCacheCount = 0, commentsSheets = [], threadedCommentsSheets = [], hasPersons = false, externalLinkCount = 0, hasVmlDrawings = false, ctrlPropCount = 0, }) => {
4845
+ const sheetOverrides = Array.from({ length: worksheetCount }, (_, index) => {
4605
4846
  return `<Override PartName="/xl/worksheets/sheet${index + 1}.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml"/>`;
4606
4847
  }).join("");
4848
+ const chartsheetOverrides = Array.from({ length: chartsheetCount }, (_, index) => `<Override PartName="/xl/chartsheets/sheet${index + 1}.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.chartsheet+xml"/>`).join("");
4607
4849
  const tableOverrides = Array.from({ length: tableCount }, (_, index) => {
4608
4850
  return `<Override PartName="/xl/tables/table${index + 1}.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.table+xml"/>`;
4609
4851
  }).join("");
@@ -4644,13 +4886,25 @@ const buildContentTypesXml = ({ sheetCount, tableCount, drawingSheets, mediaExte
4644
4886
  ? `<Default Extension="vml" ContentType="application/vnd.openxmlformats-officedocument.vmlDrawing"/>`
4645
4887
  : "";
4646
4888
  const ctrlPropOverrides = Array.from({ length: ctrlPropCount }, (_, index) => `<Override PartName="/xl/ctrlProps/ctrlProp${index + 1}.xml" ContentType="${CTRL_PROP_CONTENT_TYPE}"/>`).join("");
4647
- return `${XML_DECLARATION}<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types"><Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/><Default Extension="xml" ContentType="application/xml"/>${vmlDefault}${mediaDefaults}<Override PartName="/xl/workbook.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml"/>${sheetOverrides}${tableOverrides}${drawingOverrides}${chartOverrides}${chartExOverrides}${slicerOverrides}${slicerCacheOverrides}${pivotTableOverrides}${pivotCacheOverrides}${externalLinkOverrides}${ctrlPropOverrides}${commentsOverrides}${threadedCommentsOverrides}${personOverride}<Override PartName="/xl/theme/theme1.xml" ContentType="application/vnd.openxmlformats-officedocument.theme+xml"/><Override PartName="/xl/styles.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml"/><Override PartName="/xl/sharedStrings.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.sharedStrings+xml"/><Override PartName="/docProps/core.xml" ContentType="application/vnd.openxmlformats-package.core-properties+xml"/><Override PartName="/docProps/app.xml" ContentType="application/vnd.openxmlformats-officedocument.extended-properties+xml"/></Types>`;
4889
+ return `${XML_DECLARATION}<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types"><Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/><Default Extension="xml" ContentType="application/xml"/>${vmlDefault}${mediaDefaults}<Override PartName="/xl/workbook.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml"/>${sheetOverrides}${chartsheetOverrides}${tableOverrides}${drawingOverrides}${chartOverrides}${chartExOverrides}${slicerOverrides}${slicerCacheOverrides}${pivotTableOverrides}${pivotCacheOverrides}${externalLinkOverrides}${ctrlPropOverrides}${commentsOverrides}${threadedCommentsOverrides}${personOverride}<Override PartName="/xl/theme/theme1.xml" ContentType="application/vnd.openxmlformats-officedocument.theme+xml"/><Override PartName="/xl/styles.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml"/><Override PartName="/xl/sharedStrings.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.sharedStrings+xml"/><Override PartName="/docProps/core.xml" ContentType="application/vnd.openxmlformats-package.core-properties+xml"/><Override PartName="/docProps/app.xml" ContentType="application/vnd.openxmlformats-officedocument.extended-properties+xml"/></Types>`;
4648
4890
  };
4649
4891
  const buildRootRelationshipsXml = () => {
4650
4892
  return `${XML_DECLARATION}<Relationships xmlns="${PACKAGE_REL_NS}"><Relationship Id="rId1" Type="${REL_NS}/officeDocument" Target="xl/workbook.xml"/><Relationship Id="rId2" Type="${REL_NS}/extended-properties" Target="docProps/app.xml"/><Relationship Id="rId3" Type="${REL_NS}/core-properties" Target="docProps/core.xml"/></Relationships>`;
4651
4893
  };
4652
- const buildAppXml = (sheetNames, properties) => {
4653
- const titles = sheetNames
4894
+ const buildAppXml = (sheetNames, isChartsheet, properties) => {
4895
+ // Excel groups the tabs by kind: `HeadingPairs` counts `Worksheets` then
4896
+ // `Charts` (a pair is only written for a non-empty kind) and `TitlesOfParts`
4897
+ // lists the worksheet names followed by the chartsheet names.
4898
+ const worksheetNames = sheetNames.filter((_, index) => !isChartsheet[index]);
4899
+ const chartsheetNames = sheetNames.filter((_, index) => isChartsheet[index]);
4900
+ const headingPairs = [
4901
+ ["Worksheets", worksheetNames.length],
4902
+ ["Charts", chartsheetNames.length],
4903
+ ].filter(([, count]) => count > 0);
4904
+ const headingPairsXml = headingPairs
4905
+ .map(([kind, count]) => `<vt:variant><vt:lpstr>${kind}</vt:lpstr></vt:variant><vt:variant><vt:i4>${count}</vt:i4></vt:variant>`)
4906
+ .join("");
4907
+ const titles = [...worksheetNames, ...chartsheetNames]
4654
4908
  .map((name) => `<vt:lpstr>${escapeXmlText(name)}</vt:lpstr>`)
4655
4909
  .join("");
4656
4910
  const application = properties?.application ?? CREATOR;
@@ -4660,7 +4914,7 @@ const buildAppXml = (sheetNames, properties) => {
4660
4914
  const managerTag = properties?.manager
4661
4915
  ? `<Manager>${escapeXmlText(properties.manager)}</Manager>`
4662
4916
  : "";
4663
- return `${XML_DECLARATION}<Properties xmlns="http://schemas.openxmlformats.org/officeDocument/2006/extended-properties" xmlns:vt="http://schemas.openxmlformats.org/officeDocument/2006/docPropsVTypes"><Application>${escapeXmlText(application)}</Application><DocSecurity>0</DocSecurity><ScaleCrop>false</ScaleCrop><HeadingPairs><vt:vector size="2" baseType="variant"><vt:variant><vt:lpstr>Worksheets</vt:lpstr></vt:variant><vt:variant><vt:i4>${sheetNames.length}</vt:i4></vt:variant></vt:vector></HeadingPairs><TitlesOfParts><vt:vector size="${sheetNames.length}" baseType="lpstr">${titles}</vt:vector></TitlesOfParts>${companyTag}${managerTag}</Properties>`;
4917
+ return `${XML_DECLARATION}<Properties xmlns="http://schemas.openxmlformats.org/officeDocument/2006/extended-properties" xmlns:vt="http://schemas.openxmlformats.org/officeDocument/2006/docPropsVTypes"><Application>${escapeXmlText(application)}</Application><DocSecurity>0</DocSecurity><ScaleCrop>false</ScaleCrop><HeadingPairs><vt:vector size="${headingPairs.length * 2}" baseType="variant">${headingPairsXml}</vt:vector></HeadingPairs><TitlesOfParts><vt:vector size="${sheetNames.length}" baseType="lpstr">${titles}</vt:vector></TitlesOfParts>${companyTag}${managerTag}</Properties>`;
4664
4918
  };
4665
4919
  const buildCoreXml = (properties) => {
4666
4920
  const now = new Date().toISOString();