@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.
- package/dist/cjs/autofilter-condition.d.ts +45 -1
- package/dist/cjs/autofilter-condition.d.ts.map +1 -1
- package/dist/cjs/autofilter-condition.js +135 -12
- package/dist/cjs/excel-parser/helpers.d.ts +15 -1
- package/dist/cjs/excel-parser/helpers.d.ts.map +1 -1
- package/dist/cjs/excel-parser/helpers.js +119 -5
- package/dist/cjs/excel-parser/interface.d.ts +3 -1
- package/dist/cjs/excel-parser/interface.d.ts.map +1 -1
- package/dist/cjs/excel-parser/plugins/ods/index.d.ts.map +1 -1
- package/dist/cjs/excel-parser/plugins/ods/index.js +186 -97
- package/dist/cjs/excel-parser/plugins/ods/styles.d.ts +15 -0
- package/dist/cjs/excel-parser/plugins/ods/styles.d.ts.map +1 -1
- package/dist/cjs/excel-parser/plugins/ods/styles.js +80 -6
- package/dist/cjs/excel-parser/plugins/xlsb/index.d.ts.map +1 -1
- package/dist/cjs/excel-parser/plugins/xlsb/index.js +107 -103
- package/dist/cjs/excel-parser/plugins/xlsx/charts.d.ts +77 -1
- package/dist/cjs/excel-parser/plugins/xlsx/charts.d.ts.map +1 -1
- package/dist/cjs/excel-parser/plugins/xlsx/charts.js +920 -122
- package/dist/cjs/excel-parser/plugins/xlsx/drawing.d.ts +7 -1
- package/dist/cjs/excel-parser/plugins/xlsx/drawing.d.ts.map +1 -1
- package/dist/cjs/excel-parser/plugins/xlsx/drawing.js +24 -9
- package/dist/cjs/excel-parser/plugins/xlsx/form-controls.d.ts +24 -3
- package/dist/cjs/excel-parser/plugins/xlsx/form-controls.d.ts.map +1 -1
- package/dist/cjs/excel-parser/plugins/xlsx/form-controls.js +25 -3
- package/dist/cjs/excel-parser/plugins/xlsx/index.d.ts +24 -1
- package/dist/cjs/excel-parser/plugins/xlsx/index.d.ts.map +1 -1
- package/dist/cjs/excel-parser/plugins/xlsx/index.js +561 -394
- package/dist/cjs/excel-parser/plugins/xlsx/pivots.d.ts +8 -2
- package/dist/cjs/excel-parser/plugins/xlsx/pivots.d.ts.map +1 -1
- package/dist/cjs/excel-parser/plugins/xlsx/pivots.js +312 -21
- package/dist/cjs/excel-parser/plugins/xlsx/relationships.d.ts +6 -0
- package/dist/cjs/excel-parser/plugins/xlsx/relationships.d.ts.map +1 -1
- package/dist/cjs/excel-parser/plugins/xlsx/relationships.js +9 -0
- package/dist/cjs/excel-parser/plugins/xlsx/theme.d.ts +1 -1
- package/dist/cjs/excel-parser/types.d.ts +9 -1
- package/dist/cjs/excel-parser/types.d.ts.map +1 -1
- package/dist/cjs/excel-parser/workbook.d.ts +1 -0
- package/dist/cjs/excel-parser/workbook.d.ts.map +1 -1
- package/dist/cjs/excel-parser/workbook.js +5 -0
- package/dist/cjs/excel-parser/worker.js +2 -0
- package/dist/cjs/exporter/ods/ods-export.d.ts.map +1 -1
- package/dist/cjs/exporter/ods/ods-export.js +255 -80
- package/dist/cjs/exporter/xlsx/chart-builder.d.ts +7 -1
- package/dist/cjs/exporter/xlsx/chart-builder.d.ts.map +1 -1
- package/dist/cjs/exporter/xlsx/chart-builder.js +902 -119
- package/dist/cjs/exporter/xlsx/form-controls-builder.d.ts +16 -3
- package/dist/cjs/exporter/xlsx/form-controls-builder.d.ts.map +1 -1
- package/dist/cjs/exporter/xlsx/form-controls-builder.js +30 -8
- package/dist/cjs/exporter/xlsx/pivot-builder.d.ts.map +1 -1
- package/dist/cjs/exporter/xlsx/pivot-builder.js +206 -162
- package/dist/cjs/exporter/xlsx/style-builder.d.ts +8 -2
- package/dist/cjs/exporter/xlsx/style-builder.d.ts.map +1 -1
- package/dist/cjs/exporter/xlsx/style-builder.js +25 -0
- package/dist/cjs/exporter/xlsx/xlsx-export.d.ts +2 -2
- package/dist/cjs/exporter/xlsx/xlsx-export.d.ts.map +1 -1
- package/dist/cjs/exporter/xlsx/xlsx-export.js +728 -452
- package/dist/esm/autofilter-condition.d.ts +45 -1
- package/dist/esm/autofilter-condition.d.ts.map +1 -1
- package/dist/esm/autofilter-condition.js +131 -11
- package/dist/esm/excel-parser/helpers.d.ts +15 -1
- package/dist/esm/excel-parser/helpers.d.ts.map +1 -1
- package/dist/esm/excel-parser/helpers.js +94 -4
- package/dist/esm/excel-parser/interface.d.ts +3 -1
- package/dist/esm/excel-parser/interface.d.ts.map +1 -1
- package/dist/esm/excel-parser/plugins/ods/index.d.ts.map +1 -1
- package/dist/esm/excel-parser/plugins/ods/index.js +105 -22
- package/dist/esm/excel-parser/plugins/ods/styles.d.ts +15 -0
- package/dist/esm/excel-parser/plugins/ods/styles.d.ts.map +1 -1
- package/dist/esm/excel-parser/plugins/ods/styles.js +79 -5
- package/dist/esm/excel-parser/plugins/xlsb/index.d.ts.map +1 -1
- package/dist/esm/excel-parser/plugins/xlsb/index.js +15 -7
- package/dist/esm/excel-parser/plugins/xlsx/charts.d.ts +77 -1
- package/dist/esm/excel-parser/plugins/xlsx/charts.d.ts.map +1 -1
- package/dist/esm/excel-parser/plugins/xlsx/charts.js +708 -16
- package/dist/esm/excel-parser/plugins/xlsx/drawing.d.ts +7 -1
- package/dist/esm/excel-parser/plugins/xlsx/drawing.d.ts.map +1 -1
- package/dist/esm/excel-parser/plugins/xlsx/drawing.js +23 -0
- package/dist/esm/excel-parser/plugins/xlsx/form-controls.d.ts +24 -3
- package/dist/esm/excel-parser/plugins/xlsx/form-controls.d.ts.map +1 -1
- package/dist/esm/excel-parser/plugins/xlsx/form-controls.js +21 -3
- package/dist/esm/excel-parser/plugins/xlsx/index.d.ts +24 -1
- package/dist/esm/excel-parser/plugins/xlsx/index.d.ts.map +1 -1
- package/dist/esm/excel-parser/plugins/xlsx/index.js +192 -54
- package/dist/esm/excel-parser/plugins/xlsx/pivots.d.ts +8 -2
- package/dist/esm/excel-parser/plugins/xlsx/pivots.d.ts.map +1 -1
- package/dist/esm/excel-parser/plugins/xlsx/pivots.js +220 -4
- package/dist/esm/excel-parser/plugins/xlsx/relationships.d.ts +6 -0
- package/dist/esm/excel-parser/plugins/xlsx/relationships.d.ts.map +1 -1
- package/dist/esm/excel-parser/plugins/xlsx/relationships.js +9 -0
- package/dist/esm/excel-parser/plugins/xlsx/theme.d.ts +1 -1
- package/dist/esm/excel-parser/types.d.ts +9 -1
- package/dist/esm/excel-parser/types.d.ts.map +1 -1
- package/dist/esm/excel-parser/workbook.d.ts +1 -0
- package/dist/esm/excel-parser/workbook.d.ts.map +1 -1
- package/dist/esm/excel-parser/workbook.js +4 -0
- package/dist/esm/excel-parser/worker.js +2 -0
- package/dist/esm/exporter/ods/ods-export.d.ts.map +1 -1
- package/dist/esm/exporter/ods/ods-export.js +238 -72
- package/dist/esm/exporter/xlsx/chart-builder.d.ts +7 -1
- package/dist/esm/exporter/xlsx/chart-builder.d.ts.map +1 -1
- package/dist/esm/exporter/xlsx/chart-builder.js +674 -58
- package/dist/esm/exporter/xlsx/form-controls-builder.d.ts +16 -3
- package/dist/esm/exporter/xlsx/form-controls-builder.d.ts.map +1 -1
- package/dist/esm/exporter/xlsx/form-controls-builder.js +29 -8
- package/dist/esm/exporter/xlsx/pivot-builder.d.ts.map +1 -1
- package/dist/esm/exporter/xlsx/pivot-builder.js +187 -139
- package/dist/esm/exporter/xlsx/style-builder.d.ts +8 -2
- package/dist/esm/exporter/xlsx/style-builder.d.ts.map +1 -1
- package/dist/esm/exporter/xlsx/style-builder.js +28 -0
- package/dist/esm/exporter/xlsx/xlsx-export.d.ts +2 -2
- package/dist/esm/exporter/xlsx/xlsx-export.d.ts.map +1 -1
- package/dist/esm/exporter/xlsx/xlsx-export.js +451 -197
- 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 /
|
|
2079
|
-
// precedence over a value list, and is emitted as
|
|
2080
|
-
// <table:filter-condition>.
|
|
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
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
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
|
-
|
|
2094
|
-
|
|
2125
|
+
else {
|
|
2126
|
+
if (condition2?.type) {
|
|
2127
|
+
conditions.push(unsupportedConditionComment(spec.columnIndex, condition2.type));
|
|
2128
|
+
}
|
|
2129
|
+
conditions.push(first);
|
|
2095
2130
|
}
|
|
2096
|
-
|
|
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
|
|
2862
|
-
|
|
2863
|
-
|
|
2864
|
-
|
|
2865
|
-
|
|
2866
|
-
|
|
2867
|
-
|
|
2868
|
-
|
|
2869
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3000
|
-
|
|
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
|
-
|
|
3006
|
-
|
|
3007
|
-
|
|
3008
|
-
|
|
3009
|
-
|
|
3010
|
-
|
|
3011
|
-
|
|
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"
|
|
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"
|
|
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 =
|
|
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 =
|
|
3209
|
+
const wallFloor = isCircular
|
|
3079
3210
|
? ""
|
|
3080
3211
|
: `<chart:wall chart:style-name="wall1"/><chart:floor chart:style-name="floor1"/>`;
|
|
3081
|
-
const legendXml =
|
|
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
|
|
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
|
|
3167
|
-
|
|
3168
|
-
|
|
3169
|
-
|
|
3170
|
-
|
|
3171
|
-
|
|
3172
|
-
|
|
3173
|
-
|
|
3174
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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"}
|