@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
@@ -1484,6 +1484,18 @@ const buildNumberStyleXml = (name, format) => {
1484
1484
  if (!format)
1485
1485
  return "";
1486
1486
  const type = format.type;
1487
+ // The authored pattern decides the symbol, its side, the decimals and the
1488
+ // thousands separator (the same numeric grammar the chart axes convert);
1489
+ // the per-type defaults below only cover a format without a pattern.
1490
+ if (format.pattern &&
1491
+ (type === "PERCENT" ||
1492
+ type === "CURRENCY" ||
1493
+ type === "ACCOUNTING" ||
1494
+ type === "NUMBER")) {
1495
+ const fromPattern = numericPatternToOdsStyle(name, format.pattern);
1496
+ if (fromPattern)
1497
+ return fromPattern;
1498
+ }
1487
1499
  if (type === "PERCENT") {
1488
1500
  return `<number:percentage-style style:name="${name}"><number:number number:min-integer-digits="1" number:decimal-places="2"/></number:percentage-style>`;
1489
1501
  }
@@ -2073,27 +2085,56 @@ const buildFilterXml = (filterSpecs, range, rows, headerRow = true, totalRow = f
2073
2085
  if (!filterSpecs?.length)
2074
2086
  return "";
2075
2087
  const conditions = [];
2088
+ const filterConditionXml = (columnIndex, mapped) => {
2089
+ const attrs = [
2090
+ `table:field-number="${columnIndex}"`,
2091
+ `table:operator="${mapped.operator}"`,
2092
+ ];
2093
+ if (mapped.value !== undefined) {
2094
+ attrs.push(`table:value="${escapeXml(mapped.value)}"`);
2095
+ }
2096
+ if (mapped.value2 !== undefined) {
2097
+ attrs.push(`table:value2="${escapeXml(mapped.value2)}"`);
2098
+ }
2099
+ return `<table:filter-condition ${attrs.join(" ")}/>`;
2100
+ };
2076
2101
  for (const spec of filterSpecs) {
2077
2102
  const criteria = spec.filterCriteria;
2078
- // A condition filter (greater-than / contains / between / blank / …) takes
2079
- // precedence over a value list, and is emitted as a single operator-based
2080
- // <table:filter-condition>. Otherwise fall back to value-list entries.
2103
+ // A condition filter (greater-than / contains / between / blank / Top 10
2104
+ // / …) takes precedence over a value list, and is emitted as an
2105
+ // operator-based <table:filter-condition>. Excel's Custom AutoFilter
2106
+ // second clause joins it inside <table:filter-or> (Or) or beside it in
2107
+ // the enclosing <table:filter-and> (And). Otherwise fall back to
2108
+ // value-list entries.
2081
2109
  const condition = criteria?.condition;
2082
2110
  const mapped = condition?.type
2083
2111
  ? mapConditionToFilterCondition(condition.type, condition.values ?? [])
2084
2112
  : undefined;
2085
2113
  if (mapped) {
2086
- const attrs = [
2087
- `table:field-number="${spec.columnIndex}"`,
2088
- `table:operator="${mapped.operator}"`,
2089
- ];
2090
- if (mapped.value !== undefined) {
2091
- attrs.push(`table:value="${escapeXml(mapped.value)}"`);
2114
+ const first = filterConditionXml(spec.columnIndex, mapped);
2115
+ const condition2 = criteria?.condition2;
2116
+ const mapped2 = condition2?.type
2117
+ ? mapConditionToFilterCondition(condition2.type, condition2.values ?? [])
2118
+ : undefined;
2119
+ if (mapped2) {
2120
+ const second = filterConditionXml(spec.columnIndex, mapped2);
2121
+ conditions.push(criteria?.conditionJoin === "OR"
2122
+ ? `<table:filter-or>${first}${second}</table:filter-or>`
2123
+ : `${first}${second}`);
2092
2124
  }
2093
- if (mapped.value2 !== undefined) {
2094
- attrs.push(`table:value2="${escapeXml(mapped.value2)}"`);
2125
+ else {
2126
+ if (condition2?.type) {
2127
+ conditions.push(unsupportedConditionComment(spec.columnIndex, condition2.type));
2128
+ }
2129
+ conditions.push(first);
2095
2130
  }
2096
- conditions.push(`<table:filter-condition ${attrs.join(" ")}/>`);
2131
+ continue;
2132
+ }
2133
+ if (condition?.type) {
2134
+ // A criterion ODF cannot express (relative-date periods, above /
2135
+ // below average, custom formulas): degrade explicitly instead of
2136
+ // flattening the column into an all-values list.
2137
+ conditions.push(unsupportedConditionComment(spec.columnIndex, condition.type));
2097
2138
  continue;
2098
2139
  }
2099
2140
  const visibleValues = criteria?.visibleValues ??
@@ -2110,6 +2151,12 @@ const buildFilterXml = (filterSpecs, range, rows, headerRow = true, totalRow = f
2110
2151
  return "";
2111
2152
  return `<table:filter><table:filter-and>${conditions.join("")}</table:filter-and></table:filter>`;
2112
2153
  };
2154
+ /**
2155
+ * An XML comment recording a criterion ODF has no shape for (a dynamic
2156
+ * period such as DATE_THIS_MONTH, above/below average, a custom formula).
2157
+ * Readers ignore it; the column exports with no criterion on that clause.
2158
+ */
2159
+ const unsupportedConditionComment = (columnIndex, type) => `<!-- rnc: ${type.replace(/-/g, "")} filter on field ${columnIndex} has no ODF equivalent; exported without that criterion -->`;
2113
2160
  const buildSortXml = (sortSpecs) => {
2114
2161
  if (!sortSpecs?.length)
2115
2162
  return "";
@@ -2167,6 +2214,16 @@ const mapConditionToFilterCondition = (type, values) => {
2167
2214
  return { operator: "non-empty" };
2168
2215
  case "BOOLEAN":
2169
2216
  return { operator: "=", value: "TRUE" };
2217
+ // Excel's "Top 10…" AutoFilter — ODF 1.2 `table:operator` has native
2218
+ // top / bottom values / percent forms.
2219
+ case "NUMBER_TOP_N":
2220
+ return { operator: "top values", value: first ?? "" };
2221
+ case "NUMBER_BOTTOM_N":
2222
+ return { operator: "bottom values", value: first ?? "" };
2223
+ case "NUMBER_TOP_PERCENT":
2224
+ return { operator: "top percent", value: first ?? "" };
2225
+ case "NUMBER_BOTTOM_PERCENT":
2226
+ return { operator: "bottom percent", value: first ?? "" };
2170
2227
  default:
2171
2228
  return undefined;
2172
2229
  }
@@ -2796,6 +2853,7 @@ const groupEmbedsBySheet = (embeds) => {
2796
2853
  offsetY: overlay.offsetYPixels ?? 0,
2797
2854
  width: overlay.widthPixels ?? 120,
2798
2855
  height: overlay.heightPixels ?? 120,
2856
+ overlay,
2799
2857
  };
2800
2858
  const list = result.get(embed.position.sheetId) ?? [];
2801
2859
  list.push(placement);
@@ -2858,29 +2916,17 @@ const buildEmbedFramesXml = async ({ embeds, mediaEntries, sheetName, sheet, })
2858
2916
  const anchorAttr = anchorCell
2859
2917
  ? ` table:anchor-cell-address="${escapeXmlAddress(anchorCell)}"`
2860
2918
  : "";
2861
- const endColumn = resolveAnchorEndpoint({
2862
- startIndex: placement.columnIndex,
2863
- startOffset: offsetXPx,
2864
- span: widthPx,
2865
- getSize: (index) => getColumnWidthPx(sheet, index),
2866
- });
2867
- const endRow = resolveAnchorEndpoint({
2868
- startIndex: placement.rowIndex,
2869
- startOffset: offsetYPx,
2870
- span: heightPx,
2871
- getSize: (index) => getRowHeightPx(sheet, index),
2872
- });
2873
- const endRef = cellToAddress({
2874
- rowIndex: endRow.index,
2875
- columnIndex: endColumn.index,
2919
+ const endAttrs = buildFrameEndCellAttrs({
2920
+ sheet,
2921
+ overlay: placement.overlay,
2922
+ rowIndex: placement.rowIndex,
2923
+ columnIndex: placement.columnIndex,
2924
+ offsetXPx,
2925
+ offsetYPx,
2926
+ widthPx,
2927
+ heightPx,
2876
2928
  });
2877
- const endCell = endRef ?? undefined;
2878
- const endCellAttr = endCell
2879
- ? ` table:end-cell-address="${escapeXmlAddress(endCell)}"`
2880
- : "";
2881
- const endX = pxToCm(endColumn.offset).toFixed(2);
2882
- const endY = pxToCm(endRow.offset).toFixed(2);
2883
- frames.push(`<draw:frame draw:name="Image${mediaEntries.length}" svg:width="${width}cm" svg:height="${height}cm" svg:x="${offsetX}cm" svg:y="${offsetY}cm" table:anchor-type="cell"${anchorAttr}${endCellAttr} table:end-x="${endX}cm" table:end-y="${endY}cm"><draw:image xlink:href="${mediaPath}" xlink:type="simple" xlink:show="embed" xlink:actuate="onLoad"/></draw:frame>`);
2929
+ frames.push(`<draw:frame draw:name="Image${mediaEntries.length}" svg:width="${width}cm" svg:height="${height}cm" svg:x="${offsetX}cm" svg:y="${offsetY}cm" table:anchor-type="cell"${anchorAttr}${endAttrs}><draw:image xlink:href="${mediaPath}" xlink:type="simple" xlink:show="embed" xlink:actuate="onLoad"/></draw:frame>`);
2884
2930
  }
2885
2931
  return frames;
2886
2932
  };
@@ -2937,6 +2983,44 @@ const getCellOriginPx = (sheet, rowIndex, columnIndex) => {
2937
2983
  }
2938
2984
  return { x, y };
2939
2985
  };
2986
+ /**
2987
+ * `table:end-cell-address` + `table:end-x/y` for a frame — ODS's shape of
2988
+ * Excel's "Move and size with cells": the persisted second anchor when the
2989
+ * object carries one, else the cell its pixel extent reaches. ODS has no
2990
+ * placement mode, so "Move but don't size" / "Don't move or size" frames
2991
+ * degrade to a fixed-size cell-anchored frame (no end cell) — the closest
2992
+ * ODS reading; the mode itself is not round-tripped through ODS (#668 C-06).
2993
+ */
2994
+ const buildFrameEndCellAttrs = ({ sheet, overlay, rowIndex, columnIndex, offsetXPx, offsetYPx, widthPx, heightPx, }) => {
2995
+ if ((overlay?.placement ?? "moveAndSize") !== "moveAndSize")
2996
+ return "";
2997
+ const endColumn = overlay?.toCell
2998
+ ? { index: overlay.toCell.columnIndex, offset: overlay.toOffsetXPixels ?? 0 }
2999
+ : resolveAnchorEndpoint({
3000
+ startIndex: columnIndex,
3001
+ startOffset: offsetXPx,
3002
+ span: widthPx,
3003
+ getSize: (index) => getColumnWidthPx(sheet, index),
3004
+ });
3005
+ const endRow = overlay?.toCell
3006
+ ? { index: overlay.toCell.rowIndex, offset: overlay.toOffsetYPixels ?? 0 }
3007
+ : resolveAnchorEndpoint({
3008
+ startIndex: rowIndex,
3009
+ startOffset: offsetYPx,
3010
+ span: heightPx,
3011
+ getSize: (index) => getRowHeightPx(sheet, index),
3012
+ });
3013
+ const endRef = cellToAddress({
3014
+ rowIndex: endRow.index,
3015
+ columnIndex: endColumn.index,
3016
+ });
3017
+ const endCellAttr = endRef
3018
+ ? ` table:end-cell-address="${escapeXmlAddress(endRef)}"`
3019
+ : "";
3020
+ const endX = pxToCm(endColumn.offset).toFixed(2);
3021
+ const endY = pxToCm(endRow.offset).toFixed(2);
3022
+ return `${endCellAttr} table:end-x="${endX}cm" table:end-y="${endY}cm"`;
3023
+ };
2940
3024
  const resolveAnchorEndpoint = ({ startIndex, startOffset, span, getSize, }) => {
2941
3025
  let remaining = Math.max(0, span);
2942
3026
  let index = Math.max(1, startIndex);
@@ -2956,6 +3040,12 @@ const resolveAnchorEndpoint = ({ startIndex, startOffset, span, getSize, }) => {
2956
3040
  }
2957
3041
  return { index, offset };
2958
3042
  };
3043
+ /** `ChartSpec.emptyCells` → ODF `chart:treat-empty-cells`. */
3044
+ const ODS_TREAT_EMPTY_CELLS = {
3045
+ gaps: "leave-gap",
3046
+ zero: "use-zero",
3047
+ connect: "ignore",
3048
+ };
2959
3049
  const buildChartObjectXml = (chart, sheetNameById) => {
2960
3050
  const spec = chart.spec;
2961
3051
  const series = spec?.series ?? [];
@@ -2976,6 +3066,7 @@ const buildChartObjectXml = (chart, sheetNameById) => {
2976
3066
  column: "chart:bar",
2977
3067
  line: "chart:line",
2978
3068
  pie: "chart:circle",
3069
+ doughnut: "chart:ring",
2979
3070
  scatter: "chart:scatter",
2980
3071
  area: "chart:area",
2981
3072
  };
@@ -2996,20 +3087,52 @@ const buildChartObjectXml = (chart, sheetNameById) => {
2996
3087
  const chartClass = isSurface
2997
3088
  ? "chart:surface"
2998
3089
  : (typeMap[spec.chartType] ?? "chart:bar");
2999
- const plotStyleProps = threeDimensional
3000
- ? `<style:chart-properties chart:three-dimensional="true"/>`
3090
+ // Pie and doughnut (ODF `chart:ring`) share the slice model — no axes,
3091
+ // walls or legend block, and the per-series slice offset.
3092
+ const isCircular = chartClass === "chart:circle" || chartClass === "chart:ring";
3093
+ // Excel's "Show empty cells as" has a native ODF shape on the plot-area
3094
+ // style (`chart:treat-empty-cells`: leave-gap / use-zero / ignore, the
3095
+ // last being "continue the line"); "Show #N/A as an empty cell" has none
3096
+ // and degrades on ODS export.
3097
+ const treatEmptyCells = ODS_TREAT_EMPTY_CELLS[String(spec.emptyCells ?? "")];
3098
+ const plotChartProps = [
3099
+ threeDimensional ? `chart:three-dimensional="true"` : "",
3100
+ treatEmptyCells ? `chart:treat-empty-cells="${treatEmptyCells}"` : "",
3101
+ ].filter(Boolean);
3102
+ const plotStyleProps = plotChartProps.length
3103
+ ? `<style:chart-properties ${plotChartProps.join(" ")}/>`
3001
3104
  : "";
3002
3105
  const title = spec.title
3003
3106
  ? `<chart:title><text:p>${escapeXml(String(spec.title))}</text:p></chart:title>`
3004
3107
  : "";
3005
- const seriesPalette = [
3006
- "#4285f4",
3007
- "#ea4335",
3008
- "#fbbc04",
3009
- "#34a853",
3010
- "#ff6d01",
3011
- "#46bdc6",
3012
- ];
3108
+ // Axis tick-label formats. Excel's explicit `<c:numFmt sourceLinked="0">`
3109
+ // is an ODF data style on the axis style with the link to the source
3110
+ // severed (`chart:link-data-style-to-source="false"`); without one the
3111
+ // axis keeps ODF's default and stays linked to its source cells.
3112
+ const axisStyle = (name, options) => {
3113
+ const dataStyleName = `${name}N`;
3114
+ const pattern = options?.numberFormatPattern;
3115
+ const numberStyle = typeof pattern === "string" && pattern.trim()
3116
+ ? buildAxisNumberStyleXml(dataStyleName, pattern)
3117
+ : undefined;
3118
+ const dataStyleAttr = numberStyle
3119
+ ? ` style:data-style-name="${dataStyleName}"`
3120
+ : "";
3121
+ const linkAttr = numberStyle
3122
+ ? ` chart:link-data-style-to-source="false"`
3123
+ : "";
3124
+ return {
3125
+ numberStyle: numberStyle ?? "",
3126
+ style: `<style:style style:name="${name}" style:family="chart"${dataStyleAttr}><style:chart-properties chart:display-label="true" chart:tick-marks-major-inner="false" chart:tick-marks-major-outer="true"${linkAttr}/><style:graphic-properties svg:stroke-color="#b3b3b3"/><style:text-properties fo:font-size="9pt"/></style:style>`,
3127
+ };
3128
+ };
3129
+ const axisX = axisStyle("axisx", spec.categoryAxisOptions);
3130
+ const axisY = axisStyle("axisy", spec.valueAxisOptions);
3131
+ // Change Colors palette when set (ODF has no chart-style id — it
3132
+ // degrades to the resolved series colours); else the default cycle.
3133
+ const seriesPalette = Array.isArray(spec.palette) && spec.palette.length > 0
3134
+ ? spec.palette
3135
+ : ["#4285f4", "#ea4335", "#fbbc04", "#34a853", "#ff6d01", "#46bdc6"];
3013
3136
  // Series chart styles + (optional) gradient defs. When a series
3014
3137
  // carries an explicit gradient we emit a `<draw:gradient>` def
3015
3138
  // alongside it and reference the def via `draw:fill-gradient-name`
@@ -3017,6 +3140,14 @@ const buildChartObjectXml = (chart, sheetNameById) => {
3017
3140
  // for `draw:angle` (so a 90° XLSX gradient is `900` here) and
3018
3141
  // `linear` / `radial` for the two flavors we round-trip.
3019
3142
  const gradientDefs = [];
3143
+ // Exploded pie / doughnut — ODF keeps the slice offset on the series
3144
+ // style (`chart:pie-offset`, a percentage of the radius) like OOXML's
3145
+ // per-series `<c:explosion>`.
3146
+ const explosion = isCircular &&
3147
+ typeof spec.explosion === "number" &&
3148
+ spec.explosion > 0
3149
+ ? `<style:chart-properties chart:pie-offset="${Math.round(spec.explosion)}"/>`
3150
+ : "";
3020
3151
  const seriesStyles = seriesRanges
3021
3152
  .map((_range, index) => {
3022
3153
  const color = seriesPalette[index % seriesPalette.length];
@@ -3042,10 +3173,10 @@ const buildChartObjectXml = (chart, sheetNameById) => {
3042
3173
  const angle = Math.round((gradient.degree ?? 0) * 10);
3043
3174
  gradientDefs.push(`<draw:gradient draw:name="${gradName}" draw:style="linear" draw:start-color="${startColor}" draw:end-color="${endColor}" draw:start-intensity="100%" draw:end-intensity="100%" draw:angle="${angle}"/>`);
3044
3175
  }
3045
- return `<style:style style:name="series${index}" style:family="chart"><style:graphic-properties svg:stroke-color="${startColor}" svg:stroke-width="0.08cm" draw:fill="gradient" draw:fill-gradient-name="${gradName}"/></style:style>`;
3176
+ return `<style:style style:name="series${index}" style:family="chart">${explosion}<style:graphic-properties svg:stroke-color="${startColor}" svg:stroke-width="0.08cm" draw:fill="gradient" draw:fill-gradient-name="${gradName}"/></style:style>`;
3046
3177
  }
3047
3178
  const fillColor = seriesData?.color || color;
3048
- return `<style:style style:name="series${index}" style:family="chart"><style:graphic-properties svg:stroke-color="${fillColor}" svg:stroke-width="0.08cm" draw:fill="none"/></style:style>`;
3179
+ return `<style:style style:name="series${index}" style:family="chart">${explosion}<style:graphic-properties svg:stroke-color="${fillColor}" svg:stroke-width="0.08cm" draw:fill="none"/></style:style>`;
3049
3180
  })
3050
3181
  .join("");
3051
3182
  const gradientDefsXml = gradientDefs.join("");
@@ -3067,7 +3198,7 @@ const buildChartObjectXml = (chart, sheetNameById) => {
3067
3198
  return `<chart:series chart:class="${chartClass}" chart:style-name="series${index}" chart:values-cell-range-address="${escapeXmlAddress(range)}"${labelAttr}>${pointsXml}</chart:series>`;
3068
3199
  })
3069
3200
  .join("");
3070
- const axisXml = chartClass === "chart:circle"
3201
+ const axisXml = isCircular
3071
3202
  ? ""
3072
3203
  : `<chart:axis chart:dimension="x" chart:name="primary-x" chart:style-name="axisx"><chart:title/></chart:axis><chart:axis chart:dimension="y" chart:name="primary-y" chart:style-name="axisy"><chart:title/></chart:axis>`;
3073
3204
  const categoriesAttr = domainRange
@@ -3075,14 +3206,61 @@ const buildChartObjectXml = (chart, sheetNameById) => {
3075
3206
  : "";
3076
3207
  const labelMode = domainRange ? "both" : hasSeriesLabels ? "row" : "none";
3077
3208
  const plotAreaAttrs = ` chart:data-source-has-labels="${labelMode}"${categoriesAttr}`;
3078
- const wallFloor = chartClass === "chart:circle"
3209
+ const wallFloor = isCircular
3079
3210
  ? ""
3080
3211
  : `<chart:wall chart:style-name="wall1"/><chart:floor chart:style-name="floor1"/>`;
3081
- const legendXml = chartClass === "chart:circle"
3212
+ const legendXml = isCircular
3082
3213
  ? ""
3083
3214
  : `<chart:legend chart:legend-position="end" chart:legend-align="center" chart:style-name="legend1"/>`;
3084
- return `${XML_DECLARATION}<office:document-content xmlns:office="${ODS_NS.office}" xmlns:chart="${ODS_NS.chart}" xmlns:table="${ODS_NS.table}" xmlns:text="${ODS_NS.text}" xmlns:style="${ODS_NS.style}" xmlns:draw="${ODS_NS.draw}" xmlns:svg="${ODS_NS.svg}" xmlns:fo="${ODS_NS.fo}" office:version="1.2"><office:automatic-styles>${gradientDefsXml}<style:style style:name="ch1" style:family="chart"><style:graphic-properties draw:fill="solid" draw:fill-color="#ffffff"/></style:style><style:style style:name="plot1" style:family="chart">${plotStyleProps}<style:graphic-properties draw:fill="solid" draw:fill-color="#ffffff" svg:stroke-color="#b3b3b3"/></style:style><style:style style:name="wall1" style:family="chart"><style:graphic-properties draw:fill="solid" draw:fill-color="#ffffff" svg:stroke-color="#b3b3b3" svg:stroke-width="0.01cm"/></style:style><style:style style:name="floor1" style:family="chart"><style:graphic-properties draw:fill="none" svg:stroke-color="#b3b3b3"/></style:style><style:style style:name="legend1" style:family="chart"><style:graphic-properties draw:fill="none" svg:stroke-color="#b3b3b3"/><style:text-properties fo:font-size="10pt"/></style:style><style:style style:name="axisx" style:family="chart"><style:chart-properties chart:display-label="true" chart:tick-marks-major-inner="false" chart:tick-marks-major-outer="true"/><style:graphic-properties svg:stroke-color="#b3b3b3"/><style:text-properties fo:font-size="9pt"/></style:style><style:style style:name="axisy" style:family="chart"><style:chart-properties chart:display-label="true" chart:tick-marks-major-inner="false" chart:tick-marks-major-outer="true"/><style:graphic-properties svg:stroke-color="#b3b3b3"/><style:text-properties fo:font-size="9pt"/></style:style>${seriesStyles}</office:automatic-styles><office:body><office:chart><chart:chart chart:class="${chartClass}" chart:style-name="ch1">${title}${legendXml}<chart:plot-area chart:style-name="plot1"${plotAreaAttrs}>${axisXml}${seriesXml}${wallFloor}</chart:plot-area></chart:chart></office:chart></office:body></office:document-content>`;
3215
+ return `${XML_DECLARATION}<office:document-content xmlns:office="${ODS_NS.office}" xmlns:chart="${ODS_NS.chart}" xmlns:table="${ODS_NS.table}" xmlns:text="${ODS_NS.text}" xmlns:style="${ODS_NS.style}" xmlns:draw="${ODS_NS.draw}" xmlns:svg="${ODS_NS.svg}" xmlns:fo="${ODS_NS.fo}" xmlns:number="${ODS_NS.number}" office:version="1.2"><office:automatic-styles>${axisX.numberStyle}${axisY.numberStyle}${gradientDefsXml}<style:style style:name="ch1" style:family="chart"><style:graphic-properties draw:fill="solid" draw:fill-color="#ffffff"/></style:style><style:style style:name="plot1" style:family="chart">${plotStyleProps}<style:graphic-properties draw:fill="solid" draw:fill-color="#ffffff" svg:stroke-color="#b3b3b3"/></style:style><style:style style:name="wall1" style:family="chart"><style:graphic-properties draw:fill="solid" draw:fill-color="#ffffff" svg:stroke-color="#b3b3b3" svg:stroke-width="0.01cm"/></style:style><style:style style:name="floor1" style:family="chart"><style:graphic-properties draw:fill="none" svg:stroke-color="#b3b3b3"/></style:style><style:style style:name="legend1" style:family="chart"><style:graphic-properties draw:fill="none" svg:stroke-color="#b3b3b3"/><style:text-properties fo:font-size="10pt"/></style:style>${axisX.style}${axisY.style}${seriesStyles}</office:automatic-styles><office:body><office:chart><chart:chart chart:class="${chartClass}" chart:style-name="ch1">${title}${legendXml}<chart:plot-area chart:style-name="plot1"${plotAreaAttrs}>${axisXml}${seriesXml}${wallFloor}</chart:plot-area></chart:chart></office:chart></office:body></office:document-content>`;
3216
+ };
3217
+ /**
3218
+ * A numeric format pattern as an ODF data style — shared by cell number
3219
+ * formats and the chart axes' tick-label patterns. Only the numeric grammar
3220
+ * converts — digit placeholders, grouping, decimals, `%`, and a quoted /
3221
+ * bare currency literal before or after the digits (as a
3222
+ * `number:currency-style`; whitespace between the symbol and the digits is
3223
+ * kept as a `number:text` so `€ #,##0.00` and `€#,##0.00` stay distinct).
3224
+ * Dates, fractions, scientific and text patterns have no shape the ODS
3225
+ * parser reads back, so they return `undefined` and the caller degrades
3226
+ * (the axis stays linked to its source, a cell falls back to its type's
3227
+ * default style — both stated in the compatibility table).
3228
+ */
3229
+ const numericPatternToOdsStyle = (name, pattern) => {
3230
+ // The first (positive) section; a `[$€-2]` currency token keeps its symbol
3231
+ // as a literal, the other bracket tokens (colours, conditions) drop.
3232
+ const section = pattern
3233
+ .split(";")[0]
3234
+ .replace(/\[\$([^\]-]*)(?:-[^\]]*)?\]/g, '"$1"')
3235
+ .replace(/\[[^\]]*\]/g, "")
3236
+ .trim();
3237
+ const match = section.match(/^((?:"[^"]*"|\\.|[^#0?.,%"\\])*)([#0?][#0?,]*(?:\.[#0?]+)?)((?:"[^"]*"|\\.|[^#0?"\\])*)$/);
3238
+ if (!match)
3239
+ return undefined;
3240
+ const unquote = (literal) => literal.replace(/"([^"]*)"/g, "$1").replace(/\\(.)/g, "$1");
3241
+ const rawPrefix = unquote(match[1]);
3242
+ const rawSuffix = unquote(match[3]);
3243
+ const prefix = rawPrefix.trim();
3244
+ const digits = match[2];
3245
+ const suffix = rawSuffix.trim();
3246
+ const [integerPart = "", fractionPart = ""] = digits.split(".");
3247
+ const numberXml = `<number:number number:min-integer-digits="${(integerPart.match(/0/g) ?? []).length}" number:decimal-places="${fractionPart.length}" number:grouping="${integerPart.includes(",")}"/>`;
3248
+ if (suffix.includes("%")) {
3249
+ return `<number:percentage-style style:name="${name}">${numberXml}<number:text>%</number:text></number:percentage-style>`;
3250
+ }
3251
+ const gap = (literal) => /\s$/.test(literal) || /^\s/.test(literal)
3252
+ ? "<number:text> </number:text>"
3253
+ : "";
3254
+ if (prefix) {
3255
+ return `<number:currency-style style:name="${name}"><number:currency-symbol>${escapeXml(prefix)}</number:currency-symbol>${gap(rawPrefix)}${numberXml}</number:currency-style>`;
3256
+ }
3257
+ if (suffix) {
3258
+ return `<number:currency-style style:name="${name}">${numberXml}${gap(rawSuffix)}<number:currency-symbol>${escapeXml(suffix)}</number:currency-symbol></number:currency-style>`;
3259
+ }
3260
+ return `<number:number-style style:name="${name}">${numberXml}</number:number-style>`;
3085
3261
  };
3262
+ /** A chart axis' explicit tick-label pattern as an ODF data style (see `numericPatternToOdsStyle`). */
3263
+ const buildAxisNumberStyleXml = numericPatternToOdsStyle;
3086
3264
  const buildChartRangeAddress = (range, sheetNameById) => {
3087
3265
  const sheetName = sheetNameById.get(range.sheetId);
3088
3266
  if (!sheetName)
@@ -3163,29 +3341,17 @@ const buildChartFramesXml = async ({ charts, sheet, sheetName, sheetNameById, st
3163
3341
  const anchorAttr = anchorCell
3164
3342
  ? ` table:anchor-cell-address="${escapeXmlAddress(anchorCell)}"`
3165
3343
  : "";
3166
- const endColumn = resolveAnchorEndpoint({
3167
- startIndex: placement.columnIndex,
3168
- startOffset: offsetXPx,
3169
- span: widthPx,
3170
- getSize: (index) => getColumnWidthPx(sheet, index),
3171
- });
3172
- const endRow = resolveAnchorEndpoint({
3173
- startIndex: placement.rowIndex,
3174
- startOffset: offsetYPx,
3175
- span: heightPx,
3176
- getSize: (index) => getRowHeightPx(sheet, index),
3177
- });
3178
- const endRef = cellToAddress({
3179
- rowIndex: endRow.index,
3180
- columnIndex: endColumn.index,
3344
+ const endAttrs = buildFrameEndCellAttrs({
3345
+ sheet,
3346
+ overlay: placement.chart.position?.overlayPosition,
3347
+ rowIndex: placement.rowIndex,
3348
+ columnIndex: placement.columnIndex,
3349
+ offsetXPx,
3350
+ offsetYPx,
3351
+ widthPx,
3352
+ heightPx,
3181
3353
  });
3182
- const endCell = endRef ?? undefined;
3183
- const endCellAttr = endCell
3184
- ? ` table:end-cell-address="${escapeXmlAddress(endCell)}"`
3185
- : "";
3186
- const endX = pxToCm(endColumn.offset).toFixed(2);
3187
- const endY = pxToCm(endRow.offset).toFixed(2);
3188
- frames.push(`<draw:frame draw:name="Chart${index}" svg:width="${width}cm" svg:height="${height}cm" svg:x="${offsetX}cm" svg:y="${offsetY}cm" table:anchor-type="cell"${anchorAttr}${endCellAttr} table:end-x="${endX}cm" table:end-y="${endY}cm"><draw:object xlink:href="${folder}" xlink:type="simple"/></draw:frame>`);
3354
+ frames.push(`<draw:frame draw:name="Chart${index}" svg:width="${width}cm" svg:height="${height}cm" svg:x="${offsetX}cm" svg:y="${offsetY}cm" table:anchor-type="cell"${anchorAttr}${endAttrs}><draw:object xlink:href="${folder}" xlink:type="simple"/></draw:frame>`);
3189
3355
  index += 1;
3190
3356
  }
3191
3357
  return { frames, chartObjects, nextIndex: index };
@@ -1,3 +1,9 @@
1
1
  import type { EmbeddedChart } from "@rowsncolumns/spreadsheet";
2
- export declare const buildChartXml: (chart: EmbeddedChart, sheetNameById: Map<number, string>) => string | undefined;
2
+ /**
3
+ * Resolves a chart's `pivotTableId` to the `<c:pivotSource><c:name>` Excel
4
+ * expects (`[Workbook]Sheet!PivotTable1`); `undefined` when the pivot is
5
+ * not exported, in which case the chart is written as a plain chart.
6
+ */
7
+ export type ResolvePivotSourceName = (pivotTableId: string | number) => string | undefined;
8
+ export declare const buildChartXml: (input: EmbeddedChart, sheetNameById: Map<number, string>, resolvePivotSourceName?: ResolvePivotSourceName) => string | undefined;
3
9
  //# sourceMappingURL=chart-builder.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"chart-builder.d.ts","sourceRoot":"","sources":["../../../../exporter/xlsx/chart-builder.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAGV,aAAa,EAEd,MAAM,2BAA2B,CAAC;AAovCnC,eAAO,MAAM,aAAa,GACxB,OAAO,aAAa,EACpB,eAAe,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,KACjC,MAAM,GAAG,SA+lBX,CAAC"}
1
+ {"version":3,"file":"chart-builder.d.ts","sourceRoot":"","sources":["../../../../exporter/xlsx/chart-builder.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAIV,aAAa,EAEd,MAAM,2BAA2B,CAAC;AA63CnC;;;;GAIG;AACH,MAAM,MAAM,sBAAsB,GAAG,CACnC,YAAY,EAAE,MAAM,GAAG,MAAM,KAC1B,MAAM,GAAG,SAAS,CAAC;AAExB,eAAO,MAAM,aAAa,GACxB,OAAO,aAAa,EACpB,eAAe,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,EAClC,yBAAyB,sBAAsB,KAC9C,MAAM,GAAG,SAqqBX,CAAC"}