@rowsncolumns/toolkit 24.4.1
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/LICENSE.md +100 -0
- package/Readme.md +27 -0
- package/dist/cjs/autofilter-condition.d.ts +43 -0
- package/dist/cjs/autofilter-condition.d.ts.map +1 -0
- package/dist/cjs/autofilter-condition.js +155 -0
- package/dist/cjs/csv-export.d.ts +49 -0
- package/dist/cjs/csv-export.d.ts.map +1 -0
- package/dist/cjs/csv-export.js +142 -0
- package/dist/cjs/csv-import-worker.d.ts +16 -0
- package/dist/cjs/csv-import-worker.d.ts.map +1 -0
- package/dist/cjs/csv-import-worker.js +60 -0
- package/dist/cjs/csv-import.d.ts +38 -0
- package/dist/cjs/csv-import.d.ts.map +1 -0
- package/dist/cjs/csv-import.js +166 -0
- package/dist/cjs/csv-parser/helpers.d.ts +14 -0
- package/dist/cjs/csv-parser/helpers.d.ts.map +1 -0
- package/dist/cjs/csv-parser/helpers.js +145 -0
- package/dist/cjs/csv-parser/types.d.ts +66 -0
- package/dist/cjs/csv-parser/types.d.ts.map +1 -0
- package/dist/cjs/csv-parser/types.js +14 -0
- package/dist/cjs/csv-parser/worker.d.ts +2 -0
- package/dist/cjs/csv-parser/worker.d.ts.map +1 -0
- package/dist/cjs/csv-parser/worker.js +200 -0
- package/dist/cjs/excel-export.d.ts +11 -0
- package/dist/cjs/excel-export.d.ts.map +1 -0
- package/dist/cjs/excel-export.js +22 -0
- package/dist/cjs/excel-import-session.d.ts +51 -0
- package/dist/cjs/excel-import-session.d.ts.map +1 -0
- package/dist/cjs/excel-import-session.js +307 -0
- package/dist/cjs/excel-import-worker.d.ts +7 -0
- package/dist/cjs/excel-import-worker.d.ts.map +1 -0
- package/dist/cjs/excel-import-worker.js +119 -0
- package/dist/cjs/excel-parser/builtin-numfmts.d.ts +24 -0
- package/dist/cjs/excel-parser/builtin-numfmts.d.ts.map +1 -0
- package/dist/cjs/excel-parser/builtin-numfmts.js +63 -0
- package/dist/cjs/excel-parser/cfb-decryptor.d.ts +53 -0
- package/dist/cjs/excel-parser/cfb-decryptor.d.ts.map +1 -0
- package/dist/cjs/excel-parser/cfb-decryptor.js +553 -0
- package/dist/cjs/excel-parser/cfb-encryptor.d.ts +23 -0
- package/dist/cjs/excel-parser/cfb-encryptor.d.ts.map +1 -0
- package/dist/cjs/excel-parser/cfb-encryptor.js +200 -0
- package/dist/cjs/excel-parser/dv-cell-stamp.d.ts +29 -0
- package/dist/cjs/excel-parser/dv-cell-stamp.d.ts.map +1 -0
- package/dist/cjs/excel-parser/dv-cell-stamp.js +239 -0
- package/dist/cjs/excel-parser/helpers.d.ts +28 -0
- package/dist/cjs/excel-parser/helpers.d.ts.map +1 -0
- package/dist/cjs/excel-parser/helpers.js +188 -0
- package/dist/cjs/excel-parser/interface.d.ts +179 -0
- package/dist/cjs/excel-parser/interface.d.ts.map +1 -0
- package/dist/cjs/excel-parser/interface.js +1 -0
- package/dist/cjs/excel-parser/plugins/base/index.d.ts +31 -0
- package/dist/cjs/excel-parser/plugins/base/index.d.ts.map +1 -0
- package/dist/cjs/excel-parser/plugins/base/index.js +101 -0
- package/dist/cjs/excel-parser/plugins/ods/conditional-formatting.d.ts +16 -0
- package/dist/cjs/excel-parser/plugins/ods/conditional-formatting.d.ts.map +1 -0
- package/dist/cjs/excel-parser/plugins/ods/conditional-formatting.js +488 -0
- package/dist/cjs/excel-parser/plugins/ods/data-validations.d.ts +15 -0
- package/dist/cjs/excel-parser/plugins/ods/data-validations.d.ts.map +1 -0
- package/dist/cjs/excel-parser/plugins/ods/data-validations.js +353 -0
- package/dist/cjs/excel-parser/plugins/ods/formula.d.ts +8 -0
- package/dist/cjs/excel-parser/plugins/ods/formula.d.ts.map +1 -0
- package/dist/cjs/excel-parser/plugins/ods/formula.js +82 -0
- package/dist/cjs/excel-parser/plugins/ods/index.d.ts +122 -0
- package/dist/cjs/excel-parser/plugins/ods/index.d.ts.map +1 -0
- package/dist/cjs/excel-parser/plugins/ods/index.js +3642 -0
- package/dist/cjs/excel-parser/plugins/ods/sparklines.d.ts +25 -0
- package/dist/cjs/excel-parser/plugins/ods/sparklines.d.ts.map +1 -0
- package/dist/cjs/excel-parser/plugins/ods/sparklines.js +221 -0
- package/dist/cjs/excel-parser/plugins/ods/styles.d.ts +60 -0
- package/dist/cjs/excel-parser/plugins/ods/styles.d.ts.map +1 -0
- package/dist/cjs/excel-parser/plugins/ods/styles.js +808 -0
- package/dist/cjs/excel-parser/plugins/ods/types.d.ts +7 -0
- package/dist/cjs/excel-parser/plugins/ods/types.d.ts.map +1 -0
- package/dist/cjs/excel-parser/plugins/ods/types.js +1 -0
- package/dist/cjs/excel-parser/plugins/xls/index.d.ts +5 -0
- package/dist/cjs/excel-parser/plugins/xls/index.d.ts.map +1 -0
- package/dist/cjs/excel-parser/plugins/xls/index.js +19 -0
- package/dist/cjs/excel-parser/plugins/xlsb/biff-record-reader.d.ts +13 -0
- package/dist/cjs/excel-parser/plugins/xlsb/biff-record-reader.d.ts.map +1 -0
- package/dist/cjs/excel-parser/plugins/xlsb/biff-record-reader.js +49 -0
- package/dist/cjs/excel-parser/plugins/xlsb/index.d.ts +117 -0
- package/dist/cjs/excel-parser/plugins/xlsb/index.d.ts.map +1 -0
- package/dist/cjs/excel-parser/plugins/xlsb/index.js +2972 -0
- package/dist/cjs/excel-parser/plugins/xlsb/rgce-parser.d.ts +18 -0
- package/dist/cjs/excel-parser/plugins/xlsb/rgce-parser.d.ts.map +1 -0
- package/dist/cjs/excel-parser/plugins/xlsb/rgce-parser.js +527 -0
- package/dist/cjs/excel-parser/plugins/xlsm/index.d.ts +5 -0
- package/dist/cjs/excel-parser/plugins/xlsm/index.d.ts.map +1 -0
- package/dist/cjs/excel-parser/plugins/xlsm/index.js +10 -0
- package/dist/cjs/excel-parser/plugins/xlsx/charts.d.ts +60 -0
- package/dist/cjs/excel-parser/plugins/xlsx/charts.d.ts.map +1 -0
- package/dist/cjs/excel-parser/plugins/xlsx/charts.js +2178 -0
- package/dist/cjs/excel-parser/plugins/xlsx/conditional-formatting.d.ts +18 -0
- package/dist/cjs/excel-parser/plugins/xlsx/conditional-formatting.d.ts.map +1 -0
- package/dist/cjs/excel-parser/plugins/xlsx/conditional-formatting.js +620 -0
- package/dist/cjs/excel-parser/plugins/xlsx/data-validations.d.ts +4 -0
- package/dist/cjs/excel-parser/plugins/xlsx/data-validations.d.ts.map +1 -0
- package/dist/cjs/excel-parser/plugins/xlsx/data-validations.js +515 -0
- package/dist/cjs/excel-parser/plugins/xlsx/drawing.d.ts +39 -0
- package/dist/cjs/excel-parser/plugins/xlsx/drawing.d.ts.map +1 -0
- package/dist/cjs/excel-parser/plugins/xlsx/drawing.js +941 -0
- package/dist/cjs/excel-parser/plugins/xlsx/index.d.ts +320 -0
- package/dist/cjs/excel-parser/plugins/xlsx/index.d.ts.map +1 -0
- package/dist/cjs/excel-parser/plugins/xlsx/index.js +6056 -0
- package/dist/cjs/excel-parser/plugins/xlsx/numfmt.d.ts +2 -0
- package/dist/cjs/excel-parser/plugins/xlsx/numfmt.d.ts.map +1 -0
- package/dist/cjs/excel-parser/plugins/xlsx/numfmt.js +3 -0
- package/dist/cjs/excel-parser/plugins/xlsx/pivots.d.ts +40 -0
- package/dist/cjs/excel-parser/plugins/xlsx/pivots.d.ts.map +1 -0
- package/dist/cjs/excel-parser/plugins/xlsx/pivots.js +342 -0
- package/dist/cjs/excel-parser/plugins/xlsx/relationships.d.ts +8 -0
- package/dist/cjs/excel-parser/plugins/xlsx/relationships.d.ts.map +1 -0
- package/dist/cjs/excel-parser/plugins/xlsx/relationships.js +26 -0
- package/dist/cjs/excel-parser/plugins/xlsx/sheet-views.d.ts +26 -0
- package/dist/cjs/excel-parser/plugins/xlsx/sheet-views.d.ts.map +1 -0
- package/dist/cjs/excel-parser/plugins/xlsx/sheet-views.js +59 -0
- package/dist/cjs/excel-parser/plugins/xlsx/styles.d.ts +44 -0
- package/dist/cjs/excel-parser/plugins/xlsx/styles.d.ts.map +1 -0
- package/dist/cjs/excel-parser/plugins/xlsx/styles.js +1377 -0
- package/dist/cjs/excel-parser/plugins/xlsx/theme.d.ts +50 -0
- package/dist/cjs/excel-parser/plugins/xlsx/theme.d.ts.map +1 -0
- package/dist/cjs/excel-parser/plugins/xlsx/theme.js +150 -0
- package/dist/cjs/excel-parser/plugins/xlsx/threaded-comments.d.ts +47 -0
- package/dist/cjs/excel-parser/plugins/xlsx/threaded-comments.d.ts.map +1 -0
- package/dist/cjs/excel-parser/plugins/xlsx/threaded-comments.js +141 -0
- package/dist/cjs/excel-parser/plugins/xlsx/value-type-detection.d.ts +44 -0
- package/dist/cjs/excel-parser/plugins/xlsx/value-type-detection.d.ts.map +1 -0
- package/dist/cjs/excel-parser/plugins/xlsx/value-type-detection.js +407 -0
- package/dist/cjs/excel-parser/registry.d.ts +5 -0
- package/dist/cjs/excel-parser/registry.d.ts.map +1 -0
- package/dist/cjs/excel-parser/registry.js +21 -0
- package/dist/cjs/excel-parser/style-registry.d.ts +51 -0
- package/dist/cjs/excel-parser/style-registry.d.ts.map +1 -0
- package/dist/cjs/excel-parser/style-registry.js +175 -0
- package/dist/cjs/excel-parser/theme-default.d.ts +3 -0
- package/dist/cjs/excel-parser/theme-default.d.ts.map +1 -0
- package/dist/cjs/excel-parser/theme-default.js +19 -0
- package/dist/cjs/excel-parser/types.d.ts +365 -0
- package/dist/cjs/excel-parser/types.d.ts.map +1 -0
- package/dist/cjs/excel-parser/types.js +59 -0
- package/dist/cjs/excel-parser/utils.d.ts +6 -0
- package/dist/cjs/excel-parser/utils.d.ts.map +1 -0
- package/dist/cjs/excel-parser/utils.js +30 -0
- package/dist/cjs/excel-parser/workbook.d.ts +51 -0
- package/dist/cjs/excel-parser/workbook.d.ts.map +1 -0
- package/dist/cjs/excel-parser/workbook.js +317 -0
- package/dist/cjs/excel-parser/worker.d.ts +2 -0
- package/dist/cjs/excel-parser/worker.d.ts.map +1 -0
- package/dist/cjs/excel-parser/worker.js +1149 -0
- package/dist/cjs/excel-parser/zip-parser-streaming.d.ts +13 -0
- package/dist/cjs/excel-parser/zip-parser-streaming.d.ts.map +1 -0
- package/dist/cjs/excel-parser/zip-parser-streaming.js +200 -0
- package/dist/cjs/excel-parser/zip-parser.d.ts +11 -0
- package/dist/cjs/excel-parser/zip-parser.d.ts.map +1 -0
- package/dist/cjs/excel-parser/zip-parser.js +49 -0
- package/dist/cjs/exporter/invalid-data-validation.d.ts +19 -0
- package/dist/cjs/exporter/invalid-data-validation.d.ts.map +1 -0
- package/dist/cjs/exporter/invalid-data-validation.js +39 -0
- package/dist/cjs/exporter/ods/ods-export.d.ts +26 -0
- package/dist/cjs/exporter/ods/ods-export.d.ts.map +1 -0
- package/dist/cjs/exporter/ods/ods-export.js +3714 -0
- package/dist/cjs/exporter/ods/sparkline-builder.d.ts +33 -0
- package/dist/cjs/exporter/ods/sparkline-builder.d.ts.map +1 -0
- package/dist/cjs/exporter/ods/sparkline-builder.js +221 -0
- package/dist/cjs/exporter/xlsx/chart-builder.d.ts +3 -0
- package/dist/cjs/exporter/xlsx/chart-builder.d.ts.map +1 -0
- package/dist/cjs/exporter/xlsx/chart-builder.js +1901 -0
- package/dist/cjs/exporter/xlsx/chart-templates.d.ts +1781 -0
- package/dist/cjs/exporter/xlsx/chart-templates.d.ts.map +1 -0
- package/dist/cjs/exporter/xlsx/chart-templates.js +1798 -0
- package/dist/cjs/exporter/xlsx/future-functions.d.ts +10 -0
- package/dist/cjs/exporter/xlsx/future-functions.d.ts.map +1 -0
- package/dist/cjs/exporter/xlsx/future-functions.js +264 -0
- package/dist/cjs/exporter/xlsx/pivot-builder.d.ts +58 -0
- package/dist/cjs/exporter/xlsx/pivot-builder.d.ts.map +1 -0
- package/dist/cjs/exporter/xlsx/pivot-builder.js +277 -0
- package/dist/cjs/exporter/xlsx/power-query-builder.d.ts +26 -0
- package/dist/cjs/exporter/xlsx/power-query-builder.d.ts.map +1 -0
- package/dist/cjs/exporter/xlsx/power-query-builder.js +306 -0
- package/dist/cjs/exporter/xlsx/slicer-builder.d.ts +11 -0
- package/dist/cjs/exporter/xlsx/slicer-builder.d.ts.map +1 -0
- package/dist/cjs/exporter/xlsx/slicer-builder.js +87 -0
- package/dist/cjs/exporter/xlsx/sparkline-builder.d.ts +61 -0
- package/dist/cjs/exporter/xlsx/sparkline-builder.d.ts.map +1 -0
- package/dist/cjs/exporter/xlsx/sparkline-builder.js +489 -0
- package/dist/cjs/exporter/xlsx/style-builder.d.ts +93 -0
- package/dist/cjs/exporter/xlsx/style-builder.d.ts.map +1 -0
- package/dist/cjs/exporter/xlsx/style-builder.js +813 -0
- package/dist/cjs/exporter/xlsx/threaded-comments-builder.d.ts +58 -0
- package/dist/cjs/exporter/xlsx/threaded-comments-builder.d.ts.map +1 -0
- package/dist/cjs/exporter/xlsx/threaded-comments-builder.js +189 -0
- package/dist/cjs/exporter/xlsx/xlsx-export.d.ts +121 -0
- package/dist/cjs/exporter/xlsx/xlsx-export.d.ts.map +1 -0
- package/dist/cjs/exporter/xlsx/xlsx-export.js +4596 -0
- package/dist/cjs/helpers/theme-xml-generator.d.ts +8 -0
- package/dist/cjs/helpers/theme-xml-generator.d.ts.map +1 -0
- package/dist/cjs/helpers/theme-xml-generator.js +33 -0
- package/dist/cjs/helpers.d.ts +34 -0
- package/dist/cjs/helpers.d.ts.map +1 -0
- package/dist/cjs/helpers.js +189 -0
- package/dist/cjs/index.d.ts +16 -0
- package/dist/cjs/index.d.ts.map +1 -0
- package/dist/cjs/index.js +15 -0
- package/dist/cjs/indexed-colors.d.ts +2 -0
- package/dist/cjs/indexed-colors.d.ts.map +1 -0
- package/dist/cjs/indexed-colors.js +75 -0
- package/dist/cjs/ods-export.d.ts +24 -0
- package/dist/cjs/ods-export.d.ts.map +1 -0
- package/dist/cjs/ods-export.js +23 -0
- package/dist/cjs/parse.d.ts +24 -0
- package/dist/cjs/parse.d.ts.map +1 -0
- package/dist/cjs/parse.js +68 -0
- package/dist/cjs/power-query.d.ts +41 -0
- package/dist/cjs/power-query.d.ts.map +1 -0
- package/dist/cjs/power-query.js +22 -0
- package/dist/cjs/server.d.ts +9 -0
- package/dist/cjs/server.d.ts.map +1 -0
- package/dist/cjs/server.js +8 -0
- package/dist/cjs/theme1.d.ts +3 -0
- package/dist/cjs/theme1.d.ts.map +1 -0
- package/dist/cjs/theme1.js +2 -0
- package/dist/cjs/tsconfig.cjs.tsbuildinfo +1 -0
- package/dist/cjs/types.d.ts +14 -0
- package/dist/cjs/types.d.ts.map +1 -0
- package/dist/cjs/types.js +14 -0
- package/dist/cjs/vitest.config.d.ts +3 -0
- package/dist/cjs/vitest.config.d.ts.map +1 -0
- package/dist/cjs/vitest.config.js +16 -0
- package/dist/esm/autofilter-condition.d.ts +43 -0
- package/dist/esm/autofilter-condition.d.ts.map +1 -0
- package/dist/esm/autofilter-condition.js +145 -0
- package/dist/esm/csv-export.d.ts +49 -0
- package/dist/esm/csv-export.d.ts.map +1 -0
- package/dist/esm/csv-export.js +126 -0
- package/dist/esm/csv-import-worker.d.ts +16 -0
- package/dist/esm/csv-import-worker.d.ts.map +1 -0
- package/dist/esm/csv-import-worker.js +60 -0
- package/dist/esm/csv-import.d.ts +38 -0
- package/dist/esm/csv-import.d.ts.map +1 -0
- package/dist/esm/csv-import.js +132 -0
- package/dist/esm/csv-parser/helpers.d.ts +14 -0
- package/dist/esm/csv-parser/helpers.d.ts.map +1 -0
- package/dist/esm/csv-parser/helpers.js +0 -0
- package/dist/esm/csv-parser/types.d.ts +66 -0
- package/dist/esm/csv-parser/types.d.ts.map +1 -0
- package/dist/esm/csv-parser/types.js +14 -0
- package/dist/esm/csv-parser/worker.d.ts +2 -0
- package/dist/esm/csv-parser/worker.d.ts.map +1 -0
- package/dist/esm/csv-parser/worker.js +154 -0
- package/dist/esm/excel-export.d.ts +11 -0
- package/dist/esm/excel-export.d.ts.map +1 -0
- package/dist/esm/excel-export.js +13 -0
- package/dist/esm/excel-import-session.d.ts +51 -0
- package/dist/esm/excel-import-session.d.ts.map +1 -0
- package/dist/esm/excel-import-session.js +255 -0
- package/dist/esm/excel-import-worker.d.ts +7 -0
- package/dist/esm/excel-import-worker.d.ts.map +1 -0
- package/dist/esm/excel-import-worker.js +116 -0
- package/dist/esm/excel-parser/builtin-numfmts.d.ts +24 -0
- package/dist/esm/excel-parser/builtin-numfmts.d.ts.map +1 -0
- package/dist/esm/excel-parser/builtin-numfmts.js +59 -0
- package/dist/esm/excel-parser/cfb-decryptor.d.ts +53 -0
- package/dist/esm/excel-parser/cfb-decryptor.d.ts.map +1 -0
- package/dist/esm/excel-parser/cfb-decryptor.js +493 -0
- package/dist/esm/excel-parser/cfb-encryptor.d.ts +23 -0
- package/dist/esm/excel-parser/cfb-encryptor.d.ts.map +1 -0
- package/dist/esm/excel-parser/cfb-encryptor.js +181 -0
- package/dist/esm/excel-parser/dv-cell-stamp.d.ts +29 -0
- package/dist/esm/excel-parser/dv-cell-stamp.d.ts.map +1 -0
- package/dist/esm/excel-parser/dv-cell-stamp.js +155 -0
- package/dist/esm/excel-parser/helpers.d.ts +28 -0
- package/dist/esm/excel-parser/helpers.d.ts.map +1 -0
- package/dist/esm/excel-parser/helpers.js +160 -0
- package/dist/esm/excel-parser/interface.d.ts +179 -0
- package/dist/esm/excel-parser/interface.d.ts.map +1 -0
- package/dist/esm/excel-parser/interface.js +1 -0
- package/dist/esm/excel-parser/plugins/base/index.d.ts +31 -0
- package/dist/esm/excel-parser/plugins/base/index.d.ts.map +1 -0
- package/dist/esm/excel-parser/plugins/base/index.js +64 -0
- package/dist/esm/excel-parser/plugins/ods/conditional-formatting.d.ts +16 -0
- package/dist/esm/excel-parser/plugins/ods/conditional-formatting.d.ts.map +1 -0
- package/dist/esm/excel-parser/plugins/ods/conditional-formatting.js +405 -0
- package/dist/esm/excel-parser/plugins/ods/data-validations.d.ts +15 -0
- package/dist/esm/excel-parser/plugins/ods/data-validations.d.ts.map +1 -0
- package/dist/esm/excel-parser/plugins/ods/data-validations.js +331 -0
- package/dist/esm/excel-parser/plugins/ods/formula.d.ts +8 -0
- package/dist/esm/excel-parser/plugins/ods/formula.d.ts.map +1 -0
- package/dist/esm/excel-parser/plugins/ods/formula.js +82 -0
- package/dist/esm/excel-parser/plugins/ods/index.d.ts +122 -0
- package/dist/esm/excel-parser/plugins/ods/index.d.ts.map +1 -0
- package/dist/esm/excel-parser/plugins/ods/index.js +2896 -0
- package/dist/esm/excel-parser/plugins/ods/sparklines.d.ts +25 -0
- package/dist/esm/excel-parser/plugins/ods/sparklines.d.ts.map +1 -0
- package/dist/esm/excel-parser/plugins/ods/sparklines.js +188 -0
- package/dist/esm/excel-parser/plugins/ods/styles.d.ts +60 -0
- package/dist/esm/excel-parser/plugins/ods/styles.d.ts.map +1 -0
- package/dist/esm/excel-parser/plugins/ods/styles.js +732 -0
- package/dist/esm/excel-parser/plugins/ods/types.d.ts +7 -0
- package/dist/esm/excel-parser/plugins/ods/types.d.ts.map +1 -0
- package/dist/esm/excel-parser/plugins/ods/types.js +1 -0
- package/dist/esm/excel-parser/plugins/xls/index.d.ts +5 -0
- package/dist/esm/excel-parser/plugins/xls/index.d.ts.map +1 -0
- package/dist/esm/excel-parser/plugins/xls/index.js +7 -0
- package/dist/esm/excel-parser/plugins/xlsb/biff-record-reader.d.ts +13 -0
- package/dist/esm/excel-parser/plugins/xlsb/biff-record-reader.d.ts.map +1 -0
- package/dist/esm/excel-parser/plugins/xlsb/biff-record-reader.js +48 -0
- package/dist/esm/excel-parser/plugins/xlsb/index.d.ts +117 -0
- package/dist/esm/excel-parser/plugins/xlsb/index.d.ts.map +1 -0
- package/dist/esm/excel-parser/plugins/xlsb/index.js +2339 -0
- package/dist/esm/excel-parser/plugins/xlsb/rgce-parser.d.ts +18 -0
- package/dist/esm/excel-parser/plugins/xlsb/rgce-parser.d.ts.map +1 -0
- package/dist/esm/excel-parser/plugins/xlsb/rgce-parser.js +522 -0
- package/dist/esm/excel-parser/plugins/xlsm/index.d.ts +5 -0
- package/dist/esm/excel-parser/plugins/xlsm/index.d.ts.map +1 -0
- package/dist/esm/excel-parser/plugins/xlsm/index.js +6 -0
- package/dist/esm/excel-parser/plugins/xlsx/charts.d.ts +60 -0
- package/dist/esm/excel-parser/plugins/xlsx/charts.d.ts.map +1 -0
- package/dist/esm/excel-parser/plugins/xlsx/charts.js +2036 -0
- package/dist/esm/excel-parser/plugins/xlsx/conditional-formatting.d.ts +18 -0
- package/dist/esm/excel-parser/plugins/xlsx/conditional-formatting.d.ts.map +1 -0
- package/dist/esm/excel-parser/plugins/xlsx/conditional-formatting.js +621 -0
- package/dist/esm/excel-parser/plugins/xlsx/data-validations.d.ts +4 -0
- package/dist/esm/excel-parser/plugins/xlsx/data-validations.d.ts.map +1 -0
- package/dist/esm/excel-parser/plugins/xlsx/data-validations.js +493 -0
- package/dist/esm/excel-parser/plugins/xlsx/drawing.d.ts +39 -0
- package/dist/esm/excel-parser/plugins/xlsx/drawing.d.ts.map +1 -0
- package/dist/esm/excel-parser/plugins/xlsx/drawing.js +905 -0
- package/dist/esm/excel-parser/plugins/xlsx/index.d.ts +320 -0
- package/dist/esm/excel-parser/plugins/xlsx/index.d.ts.map +1 -0
- package/dist/esm/excel-parser/plugins/xlsx/index.js +4654 -0
- package/dist/esm/excel-parser/plugins/xlsx/numfmt.d.ts +2 -0
- package/dist/esm/excel-parser/plugins/xlsx/numfmt.d.ts.map +1 -0
- package/dist/esm/excel-parser/plugins/xlsx/numfmt.js +3 -0
- package/dist/esm/excel-parser/plugins/xlsx/pivots.d.ts +40 -0
- package/dist/esm/excel-parser/plugins/xlsx/pivots.d.ts.map +1 -0
- package/dist/esm/excel-parser/plugins/xlsx/pivots.js +254 -0
- package/dist/esm/excel-parser/plugins/xlsx/relationships.d.ts +8 -0
- package/dist/esm/excel-parser/plugins/xlsx/relationships.d.ts.map +1 -0
- package/dist/esm/excel-parser/plugins/xlsx/relationships.js +13 -0
- package/dist/esm/excel-parser/plugins/xlsx/sheet-views.d.ts +26 -0
- package/dist/esm/excel-parser/plugins/xlsx/sheet-views.d.ts.map +1 -0
- package/dist/esm/excel-parser/plugins/xlsx/sheet-views.js +64 -0
- package/dist/esm/excel-parser/plugins/xlsx/styles.d.ts +44 -0
- package/dist/esm/excel-parser/plugins/xlsx/styles.d.ts.map +1 -0
- package/dist/esm/excel-parser/plugins/xlsx/styles.js +1365 -0
- package/dist/esm/excel-parser/plugins/xlsx/theme.d.ts +50 -0
- package/dist/esm/excel-parser/plugins/xlsx/theme.d.ts.map +1 -0
- package/dist/esm/excel-parser/plugins/xlsx/theme.js +127 -0
- package/dist/esm/excel-parser/plugins/xlsx/threaded-comments.d.ts +47 -0
- package/dist/esm/excel-parser/plugins/xlsx/threaded-comments.d.ts.map +1 -0
- package/dist/esm/excel-parser/plugins/xlsx/threaded-comments.js +103 -0
- package/dist/esm/excel-parser/plugins/xlsx/value-type-detection.d.ts +44 -0
- package/dist/esm/excel-parser/plugins/xlsx/value-type-detection.d.ts.map +1 -0
- package/dist/esm/excel-parser/plugins/xlsx/value-type-detection.js +399 -0
- package/dist/esm/excel-parser/registry.d.ts +5 -0
- package/dist/esm/excel-parser/registry.d.ts.map +1 -0
- package/dist/esm/excel-parser/registry.js +20 -0
- package/dist/esm/excel-parser/style-registry.d.ts +51 -0
- package/dist/esm/excel-parser/style-registry.d.ts.map +1 -0
- package/dist/esm/excel-parser/style-registry.js +163 -0
- package/dist/esm/excel-parser/theme-default.d.ts +3 -0
- package/dist/esm/excel-parser/theme-default.d.ts.map +1 -0
- package/dist/esm/excel-parser/theme-default.js +19 -0
- package/dist/esm/excel-parser/types.d.ts +365 -0
- package/dist/esm/excel-parser/types.d.ts.map +1 -0
- package/dist/esm/excel-parser/types.js +59 -0
- package/dist/esm/excel-parser/utils.d.ts +6 -0
- package/dist/esm/excel-parser/utils.d.ts.map +1 -0
- package/dist/esm/excel-parser/utils.js +25 -0
- package/dist/esm/excel-parser/workbook.d.ts +51 -0
- package/dist/esm/excel-parser/workbook.d.ts.map +1 -0
- package/dist/esm/excel-parser/workbook.js +192 -0
- package/dist/esm/excel-parser/worker.d.ts +2 -0
- package/dist/esm/excel-parser/worker.d.ts.map +1 -0
- package/dist/esm/excel-parser/worker.js +862 -0
- package/dist/esm/excel-parser/zip-parser-streaming.d.ts +13 -0
- package/dist/esm/excel-parser/zip-parser-streaming.d.ts.map +1 -0
- package/dist/esm/excel-parser/zip-parser-streaming.js +119 -0
- package/dist/esm/excel-parser/zip-parser.d.ts +11 -0
- package/dist/esm/excel-parser/zip-parser.d.ts.map +1 -0
- package/dist/esm/excel-parser/zip-parser.js +43 -0
- package/dist/esm/exporter/invalid-data-validation.d.ts +19 -0
- package/dist/esm/exporter/invalid-data-validation.d.ts.map +1 -0
- package/dist/esm/exporter/invalid-data-validation.js +37 -0
- package/dist/esm/exporter/ods/ods-export.d.ts +26 -0
- package/dist/esm/exporter/ods/ods-export.d.ts.map +1 -0
- package/dist/esm/exporter/ods/ods-export.js +3238 -0
- package/dist/esm/exporter/ods/sparkline-builder.d.ts +33 -0
- package/dist/esm/exporter/ods/sparkline-builder.d.ts.map +1 -0
- package/dist/esm/exporter/ods/sparkline-builder.js +165 -0
- package/dist/esm/exporter/xlsx/chart-builder.d.ts +3 -0
- package/dist/esm/exporter/xlsx/chart-builder.d.ts.map +1 -0
- package/dist/esm/exporter/xlsx/chart-builder.js +1805 -0
- package/dist/esm/exporter/xlsx/chart-templates.d.ts +1781 -0
- package/dist/esm/exporter/xlsx/chart-templates.d.ts.map +1 -0
- package/dist/esm/exporter/xlsx/chart-templates.js +1798 -0
- package/dist/esm/exporter/xlsx/future-functions.d.ts +10 -0
- package/dist/esm/exporter/xlsx/future-functions.d.ts.map +1 -0
- package/dist/esm/exporter/xlsx/future-functions.js +264 -0
- package/dist/esm/exporter/xlsx/pivot-builder.d.ts +58 -0
- package/dist/esm/exporter/xlsx/pivot-builder.d.ts.map +1 -0
- package/dist/esm/exporter/xlsx/pivot-builder.js +230 -0
- package/dist/esm/exporter/xlsx/power-query-builder.d.ts +26 -0
- package/dist/esm/exporter/xlsx/power-query-builder.d.ts.map +1 -0
- package/dist/esm/exporter/xlsx/power-query-builder.js +257 -0
- package/dist/esm/exporter/xlsx/slicer-builder.d.ts +11 -0
- package/dist/esm/exporter/xlsx/slicer-builder.d.ts.map +1 -0
- package/dist/esm/exporter/xlsx/slicer-builder.js +84 -0
- package/dist/esm/exporter/xlsx/sparkline-builder.d.ts +61 -0
- package/dist/esm/exporter/xlsx/sparkline-builder.d.ts.map +1 -0
- package/dist/esm/exporter/xlsx/sparkline-builder.js +437 -0
- package/dist/esm/exporter/xlsx/style-builder.d.ts +93 -0
- package/dist/esm/exporter/xlsx/style-builder.d.ts.map +1 -0
- package/dist/esm/exporter/xlsx/style-builder.js +795 -0
- package/dist/esm/exporter/xlsx/threaded-comments-builder.d.ts +58 -0
- package/dist/esm/exporter/xlsx/threaded-comments-builder.d.ts.map +1 -0
- package/dist/esm/exporter/xlsx/threaded-comments-builder.js +175 -0
- package/dist/esm/exporter/xlsx/xlsx-export.d.ts +121 -0
- package/dist/esm/exporter/xlsx/xlsx-export.d.ts.map +1 -0
- package/dist/esm/exporter/xlsx/xlsx-export.js +4062 -0
- package/dist/esm/helpers/theme-xml-generator.d.ts +8 -0
- package/dist/esm/helpers/theme-xml-generator.d.ts.map +1 -0
- package/dist/esm/helpers/theme-xml-generator.js +33 -0
- package/dist/esm/helpers.d.ts +34 -0
- package/dist/esm/helpers.d.ts.map +1 -0
- package/dist/esm/helpers.js +182 -0
- package/dist/esm/index.d.ts +16 -0
- package/dist/esm/index.d.ts.map +1 -0
- package/dist/esm/index.js +15 -0
- package/dist/esm/indexed-colors.d.ts +2 -0
- package/dist/esm/indexed-colors.d.ts.map +1 -0
- package/dist/esm/indexed-colors.js +75 -0
- package/dist/esm/ods-export.d.ts +24 -0
- package/dist/esm/ods-export.d.ts.map +1 -0
- package/dist/esm/ods-export.js +12 -0
- package/dist/esm/parse.d.ts +24 -0
- package/dist/esm/parse.d.ts.map +1 -0
- package/dist/esm/parse.js +55 -0
- package/dist/esm/power-query.d.ts +41 -0
- package/dist/esm/power-query.d.ts.map +1 -0
- package/dist/esm/power-query.js +19 -0
- package/dist/esm/server.d.ts +9 -0
- package/dist/esm/server.d.ts.map +1 -0
- package/dist/esm/server.js +8 -0
- package/dist/esm/theme1.d.ts +3 -0
- package/dist/esm/theme1.d.ts.map +1 -0
- package/dist/esm/theme1.js +2 -0
- package/dist/esm/tsconfig.esm.tsbuildinfo +1 -0
- package/dist/esm/types.d.ts +14 -0
- package/dist/esm/types.d.ts.map +1 -0
- package/dist/esm/types.js +14 -0
- package/dist/esm/vitest.config.d.ts +3 -0
- package/dist/esm/vitest.config.d.ts.map +1 -0
- package/dist/esm/vitest.config.js +16 -0
- package/package.json +62 -0
- package/xlsx.d.ts +52 -0
|
@@ -0,0 +1,1805 @@
|
|
|
1
|
+
import { cellRangeToAddress, sanitizeSheetName } from "@rowsncolumns/utils";
|
|
2
|
+
import { XMLBuilder } from "fast-xml-parser";
|
|
3
|
+
import { area_series, bubble_series, chart_template, column_json, column_series, donut_json, line_series, scatter_json, scatter_series, static_title, } from "./chart-templates";
|
|
4
|
+
const XML_DECLARATION = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>`;
|
|
5
|
+
const INVALID_XML_CHAR_REGEX = /[\u0000-\u0008\u000B\u000C\u000E-\u001F]/g;
|
|
6
|
+
const chartBuilder = new XMLBuilder({
|
|
7
|
+
ignoreAttributes: false,
|
|
8
|
+
attributesGroupName: "a$",
|
|
9
|
+
textNodeName: "t$",
|
|
10
|
+
suppressEmptyNode: true,
|
|
11
|
+
});
|
|
12
|
+
const sanitizeXmlText = (value) => value ? value.replace(INVALID_XML_CHAR_REGEX, "") : value;
|
|
13
|
+
const cloneTemplate = (template) => JSON.parse(JSON.stringify(template));
|
|
14
|
+
const removeProp = (obj, key) => {
|
|
15
|
+
delete obj[key];
|
|
16
|
+
};
|
|
17
|
+
const normalizeFormula = (formula) => {
|
|
18
|
+
const trimmed = formula.startsWith("=") ? formula.slice(1) : formula;
|
|
19
|
+
return sanitizeXmlText(trimmed).replace(/\r\n|\r|\n/g, " ");
|
|
20
|
+
};
|
|
21
|
+
const hasPropertyReference = (formula) => {
|
|
22
|
+
if (!formula)
|
|
23
|
+
return false;
|
|
24
|
+
// Detects cell references with property access: A1.property, $A$1.nested.value, etc.
|
|
25
|
+
const propertyReferencePattern = /[$]?[A-Za-z]{1,3}[$]?[1-9][0-9]*\./;
|
|
26
|
+
return propertyReferencePattern.test(formula);
|
|
27
|
+
};
|
|
28
|
+
const formatRangeRef = (range, sheetNameById) => {
|
|
29
|
+
if (!range)
|
|
30
|
+
return undefined;
|
|
31
|
+
const sheetName = sheetNameById.get(range.sheetId);
|
|
32
|
+
return cellRangeToAddress(sheetName, range);
|
|
33
|
+
};
|
|
34
|
+
/**
|
|
35
|
+
* A1 reference over EVERY range of a (possibly multi-area) source list,
|
|
36
|
+
* joined with the union operator — `'Sheet'!$E$19,'Sheet'!$E$26,…`. The old
|
|
37
|
+
* first-range-only behavior silently truncated discontiguous series.
|
|
38
|
+
*/
|
|
39
|
+
const formatSourcesRef = (sources, sheetNameById) => {
|
|
40
|
+
if (!sources?.length)
|
|
41
|
+
return undefined;
|
|
42
|
+
const parts = sources
|
|
43
|
+
.map((range) => formatRangeRef(range, sheetNameById))
|
|
44
|
+
.filter((ref) => Boolean(ref));
|
|
45
|
+
return parts.length ? parts.join(",") : undefined;
|
|
46
|
+
};
|
|
47
|
+
const seriesCategoryRef = (entry, sheetNameById) => formatSourcesRef(entry?.categorySources, sheetNameById);
|
|
48
|
+
const buildSeriesLabel = (label, sheetName, labelRef) => {
|
|
49
|
+
// A captured source reference (`<c:tx><c:strRef>`) wins over the literal:
|
|
50
|
+
// emitting the ref without a cache lets Excel resolve the live series name
|
|
51
|
+
// on open, instead of freezing a possibly-generic literal.
|
|
52
|
+
if (labelRef) {
|
|
53
|
+
return {
|
|
54
|
+
"c:tx": {
|
|
55
|
+
"c:strRef": {
|
|
56
|
+
"c:f": labelRef,
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
if (label === undefined || label === null || label === "")
|
|
62
|
+
return undefined;
|
|
63
|
+
// Numeric series headers (e.g. a year like 2024) can reach us as numbers
|
|
64
|
+
const text = typeof label === "string" ? label : String(label);
|
|
65
|
+
if (text.startsWith("=")) {
|
|
66
|
+
// If label contains property references (e.g., =A1.propertyName),
|
|
67
|
+
// treat it as static text instead of formula reference
|
|
68
|
+
if (hasPropertyReference(text)) {
|
|
69
|
+
return {
|
|
70
|
+
"c:tx": {
|
|
71
|
+
"c:v": sanitizeXmlText(text),
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
const formula = normalizeFormula(text);
|
|
76
|
+
const resolved = sheetName && !formula.includes("!")
|
|
77
|
+
? `${sanitizeSheetName(sheetName)}!${formula}`
|
|
78
|
+
: formula;
|
|
79
|
+
return {
|
|
80
|
+
"c:tx": {
|
|
81
|
+
"c:strRef": {
|
|
82
|
+
"c:f": resolved,
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
return {
|
|
88
|
+
"c:tx": {
|
|
89
|
+
"c:v": sanitizeXmlText(text),
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
};
|
|
93
|
+
const buildColorNode = (color) => {
|
|
94
|
+
const normalized = color.startsWith("#") ? color.slice(1) : color;
|
|
95
|
+
const hex = normalized.length === 6
|
|
96
|
+
? normalized
|
|
97
|
+
: normalized.slice(normalized.length - 6);
|
|
98
|
+
return {
|
|
99
|
+
"a:solidFill": {
|
|
100
|
+
"a:srgbClr": {
|
|
101
|
+
a$: {
|
|
102
|
+
val: hex,
|
|
103
|
+
},
|
|
104
|
+
},
|
|
105
|
+
},
|
|
106
|
+
};
|
|
107
|
+
};
|
|
108
|
+
const normalizeHex = (color) => {
|
|
109
|
+
const stripped = color.startsWith("#") ? color.slice(1) : color;
|
|
110
|
+
return stripped.length === 6
|
|
111
|
+
? stripped
|
|
112
|
+
: stripped.slice(stripped.length - 6);
|
|
113
|
+
};
|
|
114
|
+
/**
|
|
115
|
+
* Build an OOXML `<a:gradFill>` node from a series-level gradient
|
|
116
|
+
* descriptor. Matches the DrawingML schema:
|
|
117
|
+
*
|
|
118
|
+
* <a:gradFill flip="none" rotWithShape="1">
|
|
119
|
+
* <a:gsLst>
|
|
120
|
+
* <a:gs pos="0"><a:srgbClr val="FF0000"/></a:gs>
|
|
121
|
+
* <a:gs pos="100000"><a:srgbClr val="0000FF"/></a:gs>
|
|
122
|
+
* </a:gsLst>
|
|
123
|
+
* <a:lin ang="5400000" scaled="0"/> <!-- or <a:path path="rect">…</a:path> -->
|
|
124
|
+
* </a:gradFill>
|
|
125
|
+
*
|
|
126
|
+
* Positions are EMU per-mille (0..100000). Linear angle is in 60000ths
|
|
127
|
+
* of a degree (90° == 5400000) — Excel's chart angle convention runs
|
|
128
|
+
* clockwise from 12 o'clock, so the conversion is `deg * 60000`.
|
|
129
|
+
*
|
|
130
|
+
* Path gradients use `<a:path path="rect">` with a `<a:fillToRect>`
|
|
131
|
+
* child carrying the inset of the inner stop in EMU per-mille.
|
|
132
|
+
*/
|
|
133
|
+
const buildGradFillNode = (gradient) => {
|
|
134
|
+
if (!gradient?.stops?.length)
|
|
135
|
+
return null;
|
|
136
|
+
const gsList = gradient.stops.map((stop) => ({
|
|
137
|
+
a$: { pos: String(Math.round(Math.max(0, Math.min(1, stop.position)) * 100000)) },
|
|
138
|
+
"a:srgbClr": { a$: { val: normalizeHex(stop.color) } },
|
|
139
|
+
}));
|
|
140
|
+
const direction = {};
|
|
141
|
+
if (gradient.type === "path") {
|
|
142
|
+
const toEmu = (v) => v != null ? String(Math.round(v * 100000)) : "0";
|
|
143
|
+
direction["a:path"] = {
|
|
144
|
+
a$: { path: "rect" },
|
|
145
|
+
"a:fillToRect": {
|
|
146
|
+
a$: {
|
|
147
|
+
l: toEmu(gradient.left),
|
|
148
|
+
r: toEmu(gradient.right),
|
|
149
|
+
t: toEmu(gradient.top),
|
|
150
|
+
b: toEmu(gradient.bottom),
|
|
151
|
+
},
|
|
152
|
+
},
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
else {
|
|
156
|
+
const deg = ((gradient.degree ?? 0) % 360 + 360) % 360;
|
|
157
|
+
direction["a:lin"] = {
|
|
158
|
+
a$: {
|
|
159
|
+
ang: String(Math.round(deg * 60000)),
|
|
160
|
+
scaled: "0",
|
|
161
|
+
},
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
return {
|
|
165
|
+
a$: { flip: "none", rotWithShape: "1" },
|
|
166
|
+
"a:gsLst": { "a:gs": gsList },
|
|
167
|
+
...direction,
|
|
168
|
+
};
|
|
169
|
+
};
|
|
170
|
+
/**
|
|
171
|
+
* Replace the series' shape-properties fill with a gradient when one
|
|
172
|
+
* is supplied. Strips any pre-existing solid/scheme fill so the
|
|
173
|
+
* gradient doesn't double up.
|
|
174
|
+
*/
|
|
175
|
+
const applySeriesGradient = (seriesNode, gradient) => {
|
|
176
|
+
if (!gradient)
|
|
177
|
+
return false;
|
|
178
|
+
const gradFill = buildGradFillNode(gradient);
|
|
179
|
+
if (!gradFill)
|
|
180
|
+
return false;
|
|
181
|
+
const spPr = seriesNode["c:spPr"];
|
|
182
|
+
if (!spPr)
|
|
183
|
+
return false;
|
|
184
|
+
delete spPr["a:solidFill"];
|
|
185
|
+
spPr["a:gradFill"] = gradFill;
|
|
186
|
+
return true;
|
|
187
|
+
};
|
|
188
|
+
/**
|
|
189
|
+
* Build the extension list for a series with invertIfNegative support.
|
|
190
|
+
* This adds the c14:invertSolidFillFmt extension for negative fill color.
|
|
191
|
+
*/
|
|
192
|
+
const buildSeriesExtLst = (negativeColor, uniqueId) => {
|
|
193
|
+
const extensions = [];
|
|
194
|
+
// Add unique ID extension (required by Excel)
|
|
195
|
+
extensions.push({
|
|
196
|
+
a$: {
|
|
197
|
+
uri: "{C3380CC4-5D6E-409C-BE32-E72D297353CC}",
|
|
198
|
+
"xmlns:c16": "http://schemas.microsoft.com/office/drawing/2014/chart",
|
|
199
|
+
},
|
|
200
|
+
"c16:uniqueId": {
|
|
201
|
+
a$: {
|
|
202
|
+
val: uniqueId,
|
|
203
|
+
},
|
|
204
|
+
},
|
|
205
|
+
});
|
|
206
|
+
// Add negative fill color extension if specified
|
|
207
|
+
if (negativeColor) {
|
|
208
|
+
const normalizedColor = negativeColor.startsWith("#")
|
|
209
|
+
? negativeColor.slice(1)
|
|
210
|
+
: negativeColor;
|
|
211
|
+
const hex = normalizedColor.length === 6
|
|
212
|
+
? normalizedColor
|
|
213
|
+
: normalizedColor.slice(normalizedColor.length - 6);
|
|
214
|
+
extensions.push({
|
|
215
|
+
a$: {
|
|
216
|
+
uri: "{6F2FDCE9-48DA-4B69-8628-5D25D57E5C99}",
|
|
217
|
+
"xmlns:c14": "http://schemas.microsoft.com/office/drawing/2007/8/2/chart",
|
|
218
|
+
},
|
|
219
|
+
"c14:invertSolidFillFmt": {
|
|
220
|
+
"c14:spPr": {
|
|
221
|
+
"a:solidFill": {
|
|
222
|
+
"a:srgbClr": {
|
|
223
|
+
a$: {
|
|
224
|
+
val: hex,
|
|
225
|
+
},
|
|
226
|
+
},
|
|
227
|
+
},
|
|
228
|
+
"a:ln": {
|
|
229
|
+
"a:noFill": {},
|
|
230
|
+
},
|
|
231
|
+
"a:effectLst": {},
|
|
232
|
+
},
|
|
233
|
+
},
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
return {
|
|
237
|
+
"c:ext": extensions.length === 1 ? extensions[0] : extensions,
|
|
238
|
+
};
|
|
239
|
+
};
|
|
240
|
+
// ─── Round-trip helpers (textFormat, marker, dPt, inline values) ─────
|
|
241
|
+
/**
|
|
242
|
+
* Build a `<c:txPr>` XML node from a ChartTextFormat. Returns
|
|
243
|
+
* `undefined` when the format has no settable properties so callers
|
|
244
|
+
* can skip emission. Used by axis, legend, title, and data-label
|
|
245
|
+
* blocks to preserve authored color/font on round-trip.
|
|
246
|
+
*/
|
|
247
|
+
const buildTxPrNode = (textFormat) => {
|
|
248
|
+
if (!textFormat)
|
|
249
|
+
return undefined;
|
|
250
|
+
const defRPr = { a$: {} };
|
|
251
|
+
if (typeof textFormat.fontSize === "number") {
|
|
252
|
+
// ChartTextFormat stores points; OOXML stores 100ths of a point.
|
|
253
|
+
defRPr.a$.sz = String(Math.round(textFormat.fontSize * 100));
|
|
254
|
+
}
|
|
255
|
+
if (textFormat.bold)
|
|
256
|
+
defRPr.a$.b = "1";
|
|
257
|
+
if (textFormat.italic)
|
|
258
|
+
defRPr.a$.i = "1";
|
|
259
|
+
if (textFormat.underline)
|
|
260
|
+
defRPr.a$.u = "sng";
|
|
261
|
+
if (textFormat.strikethrough)
|
|
262
|
+
defRPr.a$.strike = "sngStrike";
|
|
263
|
+
if (textFormat.color) {
|
|
264
|
+
const hex = String(textFormat.color).replace(/^#/, "").toUpperCase();
|
|
265
|
+
defRPr["a:solidFill"] = {
|
|
266
|
+
"a:srgbClr": { a$: { val: hex } },
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
if (textFormat.fontFamily) {
|
|
270
|
+
defRPr["a:latin"] = { a$: { typeface: textFormat.fontFamily } };
|
|
271
|
+
}
|
|
272
|
+
// Only emit when we wrote something useful — otherwise Excel rejects
|
|
273
|
+
// an empty defRPr.
|
|
274
|
+
const hasAttrs = Object.keys(defRPr.a$).length > 0;
|
|
275
|
+
const hasChildren = Object.keys(defRPr).some((k) => k !== "a$");
|
|
276
|
+
if (!hasAttrs && !hasChildren)
|
|
277
|
+
return undefined;
|
|
278
|
+
return {
|
|
279
|
+
"a:bodyPr": {
|
|
280
|
+
a$: {
|
|
281
|
+
rot: "0",
|
|
282
|
+
spcFirstLastPara: "1",
|
|
283
|
+
vertOverflow: "ellipsis",
|
|
284
|
+
wrap: "square",
|
|
285
|
+
anchor: "ctr",
|
|
286
|
+
anchorCtr: "1",
|
|
287
|
+
},
|
|
288
|
+
},
|
|
289
|
+
"a:lstStyle": {},
|
|
290
|
+
"a:p": {
|
|
291
|
+
"a:pPr": { "a:defRPr": defRPr },
|
|
292
|
+
"a:endParaRPr": { a$: { lang: "en-US" } },
|
|
293
|
+
},
|
|
294
|
+
};
|
|
295
|
+
};
|
|
296
|
+
/**
|
|
297
|
+
* Build a `<c:strLit>` inline category block from an array of values.
|
|
298
|
+
* Used when a chart's category axis is embedded literally in the chart
|
|
299
|
+
* XML (e.g. Event Budget's `["Estimated", "Actual"]`) instead of
|
|
300
|
+
* referencing the sheet.
|
|
301
|
+
*/
|
|
302
|
+
const buildInlineStrLit = (values) => ({
|
|
303
|
+
"c:strLit": {
|
|
304
|
+
"c:ptCount": { a$: { val: String(values.length) } },
|
|
305
|
+
"c:pt": values.map((v, i) => ({
|
|
306
|
+
a$: { idx: String(i) },
|
|
307
|
+
"c:v": sanitizeXmlText(String(v)),
|
|
308
|
+
})),
|
|
309
|
+
},
|
|
310
|
+
});
|
|
311
|
+
/**
|
|
312
|
+
* Build a `<c:numLit>` inline numeric block from values. Used for
|
|
313
|
+
* inline-value series (rare but legal in OOXML).
|
|
314
|
+
*/
|
|
315
|
+
const buildInlineNumLit = (values) => ({
|
|
316
|
+
"c:numLit": {
|
|
317
|
+
"c:formatCode": "General",
|
|
318
|
+
"c:ptCount": { a$: { val: String(values.length) } },
|
|
319
|
+
"c:pt": values.map((v, i) => ({
|
|
320
|
+
a$: { idx: String(i) },
|
|
321
|
+
"c:v": typeof v === "number" ? String(v) : Number(v).toString(),
|
|
322
|
+
})),
|
|
323
|
+
},
|
|
324
|
+
});
|
|
325
|
+
/**
|
|
326
|
+
* Build a `<c:marker>` node from a ChartData.marker spec. Returns
|
|
327
|
+
* `undefined` when the marker carries no information.
|
|
328
|
+
*/
|
|
329
|
+
const buildMarkerNode = (marker) => {
|
|
330
|
+
if (!marker)
|
|
331
|
+
return undefined;
|
|
332
|
+
const node = {};
|
|
333
|
+
if (marker.symbol) {
|
|
334
|
+
node["c:symbol"] = { a$: { val: marker.symbol } };
|
|
335
|
+
}
|
|
336
|
+
if (typeof marker.size === "number") {
|
|
337
|
+
node["c:size"] = { a$: { val: String(Math.round(marker.size)) } };
|
|
338
|
+
}
|
|
339
|
+
if (marker.color) {
|
|
340
|
+
node["c:spPr"] = {
|
|
341
|
+
"a:solidFill": buildColorNode(marker.color)["a:solidFill"],
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
return Object.keys(node).length ? node : undefined;
|
|
345
|
+
};
|
|
346
|
+
/**
|
|
347
|
+
* Build a `<c:dPt>` array from per-point color overrides. Each entry
|
|
348
|
+
* pins a category index to a different fill — Gantt-style charts use
|
|
349
|
+
* this to colour each task individually.
|
|
350
|
+
*/
|
|
351
|
+
const buildDataPointNodes = (dataPointColors) => {
|
|
352
|
+
if (!dataPointColors || !dataPointColors.length)
|
|
353
|
+
return undefined;
|
|
354
|
+
return dataPointColors.map((dp) => ({
|
|
355
|
+
"c:idx": { a$: { val: String(dp.idx) } },
|
|
356
|
+
"c:invertIfNegative": { a$: { val: "0" } },
|
|
357
|
+
"c:bubble3D": { a$: { val: "0" } },
|
|
358
|
+
"c:spPr": {
|
|
359
|
+
"a:solidFill": buildColorNode(dp.color)["a:solidFill"],
|
|
360
|
+
"a:ln": { "a:noFill": {} },
|
|
361
|
+
},
|
|
362
|
+
}));
|
|
363
|
+
};
|
|
364
|
+
/**
|
|
365
|
+
* Apply transparent fill to a series spPr node. Mirrors the
|
|
366
|
+
* `<a:noFill/>` form Excel uses for invisible "offset" bars (Gantt
|
|
367
|
+
* lag series). Replaces any existing solidFill/gradFill.
|
|
368
|
+
*/
|
|
369
|
+
const applyNoFillToSeriesSpPr = (seriesNode) => {
|
|
370
|
+
const spPr = (seriesNode["c:spPr"] = seriesNode["c:spPr"] ?? {});
|
|
371
|
+
removeProp(spPr, "a:solidFill");
|
|
372
|
+
removeProp(spPr, "a:gradFill");
|
|
373
|
+
spPr["a:noFill"] = {};
|
|
374
|
+
};
|
|
375
|
+
/**
|
|
376
|
+
* Build the subtitle `<a:p>` paragraph node (fast-xml-parser object form)
|
|
377
|
+
* appended below the chart title inside the same `<c:rich>` block.
|
|
378
|
+
*
|
|
379
|
+
* Mirrors the legacy JS post-patcher (`chart-subtitle-patcher.ts`): the
|
|
380
|
+
* subtitle renders at ~70% of the title font size (9pt floor) in a muted
|
|
381
|
+
* `tx1` scheme colour. `titleSize` is in OOXML hundredths-of-a-point.
|
|
382
|
+
*/
|
|
383
|
+
const buildSubtitleParagraphNode = (subtitle, titleSize) => {
|
|
384
|
+
const subtitleSize = Math.max(900, Math.round(titleSize * 0.7));
|
|
385
|
+
return {
|
|
386
|
+
"a:pPr": {
|
|
387
|
+
"a:defRPr": {
|
|
388
|
+
a$: {
|
|
389
|
+
sz: String(subtitleSize),
|
|
390
|
+
b: "0",
|
|
391
|
+
i: "0",
|
|
392
|
+
u: "none",
|
|
393
|
+
strike: "noStrike",
|
|
394
|
+
kern: "1200",
|
|
395
|
+
spc: "0",
|
|
396
|
+
baseline: "0",
|
|
397
|
+
},
|
|
398
|
+
"a:solidFill": {
|
|
399
|
+
"a:schemeClr": {
|
|
400
|
+
a$: { val: "tx1" },
|
|
401
|
+
"a:lumMod": { a$: { val: "65000" } },
|
|
402
|
+
"a:lumOff": { a$: { val: "35000" } },
|
|
403
|
+
},
|
|
404
|
+
},
|
|
405
|
+
"a:latin": { a$: { typeface: "+mn-lt" } },
|
|
406
|
+
"a:ea": { a$: { typeface: "+mn-ea" } },
|
|
407
|
+
"a:cs": { a$: { typeface: "+mn-cs" } },
|
|
408
|
+
},
|
|
409
|
+
},
|
|
410
|
+
"a:r": {
|
|
411
|
+
"a:rPr": { a$: { lang: "en-US", sz: String(subtitleSize), b: "0", i: "0" } },
|
|
412
|
+
"a:t": sanitizeXmlText(subtitle),
|
|
413
|
+
},
|
|
414
|
+
};
|
|
415
|
+
};
|
|
416
|
+
const applyChartTitle = (chartNode, title, titleTextFormat, subtitle) => {
|
|
417
|
+
if (!title)
|
|
418
|
+
return;
|
|
419
|
+
const titleBlock = cloneTemplate(static_title);
|
|
420
|
+
titleBlock["c:tx"]["c:rich"]["a:p"]["a:r"]["a:rPr"].a$.lang = "en-US";
|
|
421
|
+
titleBlock["c:tx"]["c:rich"]["a:p"]["a:r"]["a:t"] = sanitizeXmlText(title);
|
|
422
|
+
// Apply authored text format on the rich run's `a:rPr` so Excel
|
|
423
|
+
// preserves color/font on round-trip. We mutate the rPr directly
|
|
424
|
+
// (it's already inside the template's a:p > a:r).
|
|
425
|
+
if (titleTextFormat) {
|
|
426
|
+
const rPr = titleBlock["c:tx"]["c:rich"]["a:p"]["a:r"]["a:rPr"];
|
|
427
|
+
if (typeof titleTextFormat.fontSize === "number") {
|
|
428
|
+
rPr.a$.sz = String(Math.round(titleTextFormat.fontSize * 100));
|
|
429
|
+
}
|
|
430
|
+
if (titleTextFormat.bold)
|
|
431
|
+
rPr.a$.b = "1";
|
|
432
|
+
if (titleTextFormat.italic)
|
|
433
|
+
rPr.a$.i = "1";
|
|
434
|
+
if (titleTextFormat.color) {
|
|
435
|
+
const hex = String(titleTextFormat.color).replace(/^#/, "").toUpperCase();
|
|
436
|
+
rPr["a:solidFill"] = { "a:srgbClr": { a$: { val: hex } } };
|
|
437
|
+
}
|
|
438
|
+
if (titleTextFormat.fontFamily) {
|
|
439
|
+
rPr["a:latin"] = { a$: { typeface: titleTextFormat.fontFamily } };
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
// Append the subtitle as a SECOND `<a:p>` inside the title's `<c:rich>`,
|
|
443
|
+
// matching what the legacy JS post-patcher spliced in. Emitted natively
|
|
444
|
+
// here so no post-processing pass is needed. Scale relative to the
|
|
445
|
+
// authored title size (defaults to the template's 14pt → sz="1400").
|
|
446
|
+
if (subtitle && subtitle.trim().length > 0) {
|
|
447
|
+
const rich = titleBlock["c:tx"]["c:rich"];
|
|
448
|
+
const titlePara = rich["a:p"];
|
|
449
|
+
const titleSizeStr = (titlePara?.["a:pPr"]?.["a:defRPr"]?.a$?.sz ??
|
|
450
|
+
titlePara?.["a:r"]?.["a:rPr"]?.a$?.sz);
|
|
451
|
+
const titleSize = titleSizeStr && Number.isFinite(parseInt(titleSizeStr, 10))
|
|
452
|
+
? parseInt(titleSizeStr, 10)
|
|
453
|
+
: 1400;
|
|
454
|
+
rich["a:p"] = [titlePara, buildSubtitleParagraphNode(subtitle, titleSize)];
|
|
455
|
+
}
|
|
456
|
+
chartNode["c:title"] = titleBlock;
|
|
457
|
+
};
|
|
458
|
+
const getPrimarySource = (entry) => entry?.sources?.[0];
|
|
459
|
+
/**
|
|
460
|
+
* c15 extension namespace (Excel 2012+) — histogram / boxplot /
|
|
461
|
+
* waterfall / funnel charts are emitted with a `c:barChart` body
|
|
462
|
+
* inside `mc:Fallback` (so older Excel still renders something) plus a
|
|
463
|
+
* full c15 chart element inside `mc:Choice` (so Excel 2016+ renders
|
|
464
|
+
* the chart natively). See `buildC15AlternateContent` and the per-type
|
|
465
|
+
* builders below.
|
|
466
|
+
*/
|
|
467
|
+
const C15_NS = "http://schemas.microsoft.com/office/drawing/2012/chart";
|
|
468
|
+
const C15_ELEMENT_NAMES = {
|
|
469
|
+
histogram: "c15:histogramChart",
|
|
470
|
+
boxplot: "c15:boxWhiskerChart",
|
|
471
|
+
waterfall: "c15:waterfallChart",
|
|
472
|
+
funnel: "c15:funnelChart",
|
|
473
|
+
};
|
|
474
|
+
const isC15ChartType = (t) => t === "histogram" || t === "boxplot" || t === "waterfall" || t === "funnel";
|
|
475
|
+
const MC_NS = "http://schemas.openxmlformats.org/markup-compatibility/2006";
|
|
476
|
+
/**
|
|
477
|
+
* Phase 5.5b — build the full c15:histogramChart body that Excel 2016+
|
|
478
|
+
* uses to render histograms natively. Lives inside `mc:Choice` next to
|
|
479
|
+
* the older `c:barChart` fallback (in `mc:Fallback`). Both share the
|
|
480
|
+
* `c:catAx`/`c:valAx` declared at `c:plotArea` level — c15 axId values
|
|
481
|
+
* are picked to match the bar chart's axId so Excel can resolve them.
|
|
482
|
+
*/
|
|
483
|
+
const buildC15HistogramChartNode = (spec, seriesRefs, series, domainRef, axIds) => {
|
|
484
|
+
const c15SeriesEntries = [];
|
|
485
|
+
seriesRefs.forEach((ref, index) => {
|
|
486
|
+
if (!ref)
|
|
487
|
+
return;
|
|
488
|
+
const seriesNode = {
|
|
489
|
+
"c15:idx": { a$: { val: String(index) } },
|
|
490
|
+
"c15:order": { a$: { val: String(index) } },
|
|
491
|
+
};
|
|
492
|
+
// Series title — only if the consumer set a non-formula dataLabel.
|
|
493
|
+
const dataLabel = series[index]?.dataLabel;
|
|
494
|
+
if (dataLabel && !dataLabel.startsWith("=")) {
|
|
495
|
+
seriesNode["c15:tx"] = {
|
|
496
|
+
"c15:v": dataLabel,
|
|
497
|
+
};
|
|
498
|
+
}
|
|
499
|
+
seriesNode["c15:val"] = {
|
|
500
|
+
"c15:numRef": {
|
|
501
|
+
"c15:f": ref,
|
|
502
|
+
},
|
|
503
|
+
};
|
|
504
|
+
c15SeriesEntries.push(seriesNode);
|
|
505
|
+
});
|
|
506
|
+
const histogramChart = {
|
|
507
|
+
"c15:varyColors": { a$: { val: "0" } },
|
|
508
|
+
"c15:ser": c15SeriesEntries.length === 1 ? c15SeriesEntries[0] : c15SeriesEntries,
|
|
509
|
+
"c15:axId": [
|
|
510
|
+
{ a$: { val: axIds.catAxId } },
|
|
511
|
+
{ a$: { val: axIds.valAxId } },
|
|
512
|
+
],
|
|
513
|
+
};
|
|
514
|
+
// Bin configuration: explicit binCount when set, otherwise let Excel
|
|
515
|
+
// auto-bin (Sturges-like).
|
|
516
|
+
if (typeof spec?.binCount === "number" && spec.binCount > 0) {
|
|
517
|
+
histogramChart["c15:binCount"] = { a$: { val: String(spec.binCount) } };
|
|
518
|
+
}
|
|
519
|
+
else {
|
|
520
|
+
histogramChart["c15:binsAuto"] = { a$: { val: "1" } };
|
|
521
|
+
}
|
|
522
|
+
return histogramChart;
|
|
523
|
+
};
|
|
524
|
+
/**
|
|
525
|
+
* Phase 5.5b — c15:boxWhiskerChart body. Excel 2016+ renders box & whisker
|
|
526
|
+
* natively from this. Each series carries its raw values; Excel computes
|
|
527
|
+
* the quartiles + median + outliers from `c15:quartileMethod`.
|
|
528
|
+
*/
|
|
529
|
+
const buildC15BoxplotChartNode = (_spec, seriesRefs, series, domainRef, axIds) => {
|
|
530
|
+
const c15SeriesEntries = [];
|
|
531
|
+
seriesRefs.forEach((ref, index) => {
|
|
532
|
+
if (!ref)
|
|
533
|
+
return;
|
|
534
|
+
const seriesNode = {
|
|
535
|
+
"c15:idx": { a$: { val: String(index) } },
|
|
536
|
+
"c15:order": { a$: { val: String(index) } },
|
|
537
|
+
"c15:invertIfNegative": { a$: { val: "0" } },
|
|
538
|
+
"c15:bubble3D": { a$: { val: "0" } },
|
|
539
|
+
};
|
|
540
|
+
const dataLabel = series[index]?.dataLabel;
|
|
541
|
+
if (dataLabel && !dataLabel.startsWith("=")) {
|
|
542
|
+
seriesNode["c15:tx"] = { "c15:v": dataLabel };
|
|
543
|
+
}
|
|
544
|
+
if (domainRef) {
|
|
545
|
+
seriesNode["c15:cat"] = {
|
|
546
|
+
"c15:strRef": { "c15:f": domainRef },
|
|
547
|
+
};
|
|
548
|
+
}
|
|
549
|
+
seriesNode["c15:val"] = {
|
|
550
|
+
"c15:numRef": { "c15:f": ref },
|
|
551
|
+
};
|
|
552
|
+
c15SeriesEntries.push(seriesNode);
|
|
553
|
+
});
|
|
554
|
+
return {
|
|
555
|
+
"c15:varyColors": { a$: { val: "0" } },
|
|
556
|
+
"c15:ser": c15SeriesEntries.length === 1 ? c15SeriesEntries[0] : c15SeriesEntries,
|
|
557
|
+
"c15:showInnerPoints": { a$: { val: "0" } },
|
|
558
|
+
"c15:showOutlierPoints": { a$: { val: "1" } },
|
|
559
|
+
"c15:showMeanMarker": { a$: { val: "1" } },
|
|
560
|
+
"c15:showMeanLine": { a$: { val: "0" } },
|
|
561
|
+
"c15:quartileMethod": { a$: { val: "exclusive" } },
|
|
562
|
+
"c15:axId": [
|
|
563
|
+
{ a$: { val: axIds.catAxId } },
|
|
564
|
+
{ a$: { val: axIds.valAxId } },
|
|
565
|
+
],
|
|
566
|
+
};
|
|
567
|
+
};
|
|
568
|
+
/**
|
|
569
|
+
* Phase 5.5b — c15:waterfallChart body. Each c15:ser carries one data
|
|
570
|
+
* point per category. Subtotals are marked at the series level via
|
|
571
|
+
* c15:dPt with c15:subTotal set; without these, Excel treats every
|
|
572
|
+
* point as a delta and the running cumulative is implicit.
|
|
573
|
+
*/
|
|
574
|
+
const buildC15WaterfallChartNode = (_spec, seriesRefs, series, domainRef, axIds) => {
|
|
575
|
+
const c15SeriesEntries = [];
|
|
576
|
+
seriesRefs.forEach((ref, index) => {
|
|
577
|
+
if (!ref)
|
|
578
|
+
return;
|
|
579
|
+
const seriesNode = {
|
|
580
|
+
"c15:idx": { a$: { val: String(index) } },
|
|
581
|
+
"c15:order": { a$: { val: String(index) } },
|
|
582
|
+
};
|
|
583
|
+
const dataLabel = series[index]?.dataLabel;
|
|
584
|
+
if (dataLabel && !dataLabel.startsWith("=")) {
|
|
585
|
+
seriesNode["c15:tx"] = { "c15:v": dataLabel };
|
|
586
|
+
}
|
|
587
|
+
if (domainRef) {
|
|
588
|
+
seriesNode["c15:cat"] = {
|
|
589
|
+
"c15:strRef": { "c15:f": domainRef },
|
|
590
|
+
};
|
|
591
|
+
}
|
|
592
|
+
seriesNode["c15:val"] = {
|
|
593
|
+
"c15:numRef": { "c15:f": ref },
|
|
594
|
+
};
|
|
595
|
+
c15SeriesEntries.push(seriesNode);
|
|
596
|
+
});
|
|
597
|
+
return {
|
|
598
|
+
"c15:varyColors": { a$: { val: "0" } },
|
|
599
|
+
"c15:ser": c15SeriesEntries.length === 1 ? c15SeriesEntries[0] : c15SeriesEntries,
|
|
600
|
+
"c15:axId": [
|
|
601
|
+
{ a$: { val: axIds.catAxId } },
|
|
602
|
+
{ a$: { val: axIds.valAxId } },
|
|
603
|
+
],
|
|
604
|
+
};
|
|
605
|
+
};
|
|
606
|
+
/**
|
|
607
|
+
* Phase 5.5b — c15:funnelChart body. Categories label each funnel
|
|
608
|
+
* stage; values size the stages. No subtotals, no quartile config.
|
|
609
|
+
*/
|
|
610
|
+
const buildC15FunnelChartNode = (_spec, seriesRefs, series, domainRef, axIds) => {
|
|
611
|
+
const c15SeriesEntries = [];
|
|
612
|
+
seriesRefs.forEach((ref, index) => {
|
|
613
|
+
if (!ref)
|
|
614
|
+
return;
|
|
615
|
+
const seriesNode = {
|
|
616
|
+
"c15:idx": { a$: { val: String(index) } },
|
|
617
|
+
"c15:order": { a$: { val: String(index) } },
|
|
618
|
+
};
|
|
619
|
+
const dataLabel = series[index]?.dataLabel;
|
|
620
|
+
if (dataLabel && !dataLabel.startsWith("=")) {
|
|
621
|
+
seriesNode["c15:tx"] = { "c15:v": dataLabel };
|
|
622
|
+
}
|
|
623
|
+
if (domainRef) {
|
|
624
|
+
seriesNode["c15:cat"] = {
|
|
625
|
+
"c15:strRef": { "c15:f": domainRef },
|
|
626
|
+
};
|
|
627
|
+
}
|
|
628
|
+
seriesNode["c15:val"] = {
|
|
629
|
+
"c15:numRef": { "c15:f": ref },
|
|
630
|
+
};
|
|
631
|
+
c15SeriesEntries.push(seriesNode);
|
|
632
|
+
});
|
|
633
|
+
return {
|
|
634
|
+
"c15:varyColors": { a$: { val: "0" } },
|
|
635
|
+
"c15:ser": c15SeriesEntries.length === 1 ? c15SeriesEntries[0] : c15SeriesEntries,
|
|
636
|
+
"c15:axId": [
|
|
637
|
+
{ a$: { val: axIds.catAxId } },
|
|
638
|
+
{ a$: { val: axIds.valAxId } },
|
|
639
|
+
],
|
|
640
|
+
};
|
|
641
|
+
};
|
|
642
|
+
/**
|
|
643
|
+
* Phase 5.5b — wrap a c15 chart body in `mc:AlternateContent` so Excel
|
|
644
|
+
* 2016+ uses the c15 chart and older Excel falls back to the bar chart.
|
|
645
|
+
* Returns the AlternateContent node ready to splice into c:plotArea.
|
|
646
|
+
*/
|
|
647
|
+
const buildC15AlternateContent = (chartType, c15ChartBody, fallbackBarChart) => {
|
|
648
|
+
const choiceElementName = C15_ELEMENT_NAMES[chartType];
|
|
649
|
+
return {
|
|
650
|
+
a$: { "xmlns:mc": MC_NS },
|
|
651
|
+
"mc:Choice": {
|
|
652
|
+
a$: {
|
|
653
|
+
Requires: "c15",
|
|
654
|
+
"xmlns:c15": C15_NS,
|
|
655
|
+
},
|
|
656
|
+
[choiceElementName]: c15ChartBody,
|
|
657
|
+
},
|
|
658
|
+
"mc:Fallback": {
|
|
659
|
+
"c:barChart": fallbackBarChart,
|
|
660
|
+
},
|
|
661
|
+
};
|
|
662
|
+
};
|
|
663
|
+
/**
|
|
664
|
+
* Group series by their effective chart type for combo chart support.
|
|
665
|
+
* Returns a map of chart type to array of {index, ref, seriesData}.
|
|
666
|
+
*/
|
|
667
|
+
const groupSeriesByChartType = (series, seriesRefs, defaultChartType) => {
|
|
668
|
+
const groups = new Map();
|
|
669
|
+
seriesRefs.forEach((ref, index) => {
|
|
670
|
+
if (!ref)
|
|
671
|
+
return;
|
|
672
|
+
const seriesData = series[index];
|
|
673
|
+
const effectiveType = (seriesData?.seriesChartType ?? defaultChartType);
|
|
674
|
+
if (!groups.has(effectiveType)) {
|
|
675
|
+
groups.set(effectiveType, []);
|
|
676
|
+
}
|
|
677
|
+
groups.get(effectiveType).push({ index, ref, data: seriesData });
|
|
678
|
+
});
|
|
679
|
+
return groups;
|
|
680
|
+
};
|
|
681
|
+
/**
|
|
682
|
+
* Check if a chart has multiple series types (combo chart).
|
|
683
|
+
*/
|
|
684
|
+
const isComboChart = (series, defaultChartType) => {
|
|
685
|
+
const types = new Set();
|
|
686
|
+
for (const s of series) {
|
|
687
|
+
types.add(s.seriesChartType ?? defaultChartType);
|
|
688
|
+
}
|
|
689
|
+
return types.size > 1;
|
|
690
|
+
};
|
|
691
|
+
/**
|
|
692
|
+
* Build a bar/column series node.
|
|
693
|
+
*/
|
|
694
|
+
const buildBarSeriesNode = (index, ref, seriesData, domainRef, sheetName, isFirstInGroup) => {
|
|
695
|
+
const seriesNode = cloneTemplate(column_series);
|
|
696
|
+
seriesNode["c:idx"] = { a$: { val: index.toString() } };
|
|
697
|
+
seriesNode["c:order"] = { a$: { val: index.toString() } };
|
|
698
|
+
const seriesColor = seriesData?.color;
|
|
699
|
+
const anyData = seriesData;
|
|
700
|
+
// noFill takes precedence over any other fill — Gantt-style
|
|
701
|
+
// invisible offset series. Must run BEFORE solidFill/gradient so it
|
|
702
|
+
// can wipe the template's default fill.
|
|
703
|
+
if (anyData?.noFill) {
|
|
704
|
+
applyNoFillToSeriesSpPr(seriesNode);
|
|
705
|
+
}
|
|
706
|
+
else if (applySeriesGradient(seriesNode, seriesData?.gradient)) {
|
|
707
|
+
// Gradient wins — solid/scheme already cleared.
|
|
708
|
+
}
|
|
709
|
+
else if (seriesColor) {
|
|
710
|
+
seriesNode["c:spPr"]["a:solidFill"] =
|
|
711
|
+
buildColorNode(seriesColor)["a:solidFill"];
|
|
712
|
+
}
|
|
713
|
+
else if (index < 6) {
|
|
714
|
+
seriesNode["c:spPr"]["a:solidFill"]["a:schemeClr"].a$.val = `accent${index + 1}`;
|
|
715
|
+
}
|
|
716
|
+
// Per-data-point colors — emit `<c:dPt>` array after the series-
|
|
717
|
+
// level fill. Each entry overrides the fill for one category index.
|
|
718
|
+
const dPtNodes = buildDataPointNodes(anyData?.dataPointColors);
|
|
719
|
+
if (dPtNodes) {
|
|
720
|
+
seriesNode["c:dPt"] = dPtNodes;
|
|
721
|
+
}
|
|
722
|
+
// Handle invertIfNegative for negative bar coloring
|
|
723
|
+
const invertIfNegative = seriesData?.invertIfNegative;
|
|
724
|
+
const negativeColor = seriesData?.negativeColor;
|
|
725
|
+
if (invertIfNegative) {
|
|
726
|
+
seriesNode["c:invertIfNegative"] = { a$: { val: "1" } };
|
|
727
|
+
const uniqueId = `{${crypto.randomUUID().toUpperCase()}}`;
|
|
728
|
+
seriesNode["c:extLst"] = buildSeriesExtLst(negativeColor ?? undefined, uniqueId);
|
|
729
|
+
}
|
|
730
|
+
// Excel writes the category reference on every series, not just the first
|
|
731
|
+
// in a group — readers treat a cat-less series as having no categories.
|
|
732
|
+
if (domainRef) {
|
|
733
|
+
seriesNode["c:cat"] = {
|
|
734
|
+
"c:strRef": {
|
|
735
|
+
"c:f": domainRef,
|
|
736
|
+
},
|
|
737
|
+
};
|
|
738
|
+
}
|
|
739
|
+
else if (seriesData?.inlineDomainValues) {
|
|
740
|
+
// Inline category labels (`<c:strLit>`) — used when categories are
|
|
741
|
+
// embedded in the chart XML rather than from a sheet range.
|
|
742
|
+
seriesNode["c:cat"] = buildInlineStrLit(seriesData.inlineDomainValues);
|
|
743
|
+
}
|
|
744
|
+
seriesNode["c:val"]["c:numRef"]["c:f"] = ref;
|
|
745
|
+
// Inline series values (`<c:numLit>`) — when the series carries its
|
|
746
|
+
// own values rather than a cell-range. Rare for bars but valid.
|
|
747
|
+
const inlineValues = anyData?.inlineValues;
|
|
748
|
+
if (!ref && Array.isArray(inlineValues) && inlineValues.length) {
|
|
749
|
+
removeProp(seriesNode, "c:val");
|
|
750
|
+
seriesNode["c:val"] = buildInlineNumLit(inlineValues);
|
|
751
|
+
}
|
|
752
|
+
const labelNode = buildSeriesLabel(seriesData?.dataLabel ?? undefined, sheetName, seriesData?.dataLabelRef);
|
|
753
|
+
if (labelNode) {
|
|
754
|
+
seriesNode["c:tx"] = labelNode["c:tx"];
|
|
755
|
+
}
|
|
756
|
+
// Per-series marker — typically scatter/bubble but bar/line/area
|
|
757
|
+
// charts can carry one too via `<c:marker>`.
|
|
758
|
+
const markerNode = buildMarkerNode(anyData?.marker);
|
|
759
|
+
if (markerNode) {
|
|
760
|
+
seriesNode["c:marker"] = markerNode;
|
|
761
|
+
}
|
|
762
|
+
return seriesNode;
|
|
763
|
+
};
|
|
764
|
+
/**
|
|
765
|
+
* Build a line series node.
|
|
766
|
+
*/
|
|
767
|
+
const buildLineSeriesNode = (index, ref, seriesData, domainRef, sheetName, smooth) => {
|
|
768
|
+
const seriesNode = cloneTemplate(line_series);
|
|
769
|
+
seriesNode["c:idx"] = { a$: { val: index.toString() } };
|
|
770
|
+
seriesNode["c:order"] = { a$: { val: index.toString() } };
|
|
771
|
+
const seriesColor = seriesData?.color;
|
|
772
|
+
// Line series carry the stroke fill on `c:spPr/a:ln`. A gradient on
|
|
773
|
+
// a line stroke is unusual (Excel renders a flat-color stroke even
|
|
774
|
+
// when the underlying spec carries a gradient on `a:ln`), so we
|
|
775
|
+
// prefer a solid stroke and only apply the gradient to the bg-style
|
|
776
|
+
// `c:spPr/a:solidFill` slot when present. If the caller explicitly
|
|
777
|
+
// requested a gradient, we replace the stroke's solid color with
|
|
778
|
+
// the first stop's hex — best-effort fallback.
|
|
779
|
+
if (seriesData?.gradient && seriesData.gradient.stops?.length) {
|
|
780
|
+
const first = seriesData.gradient.stops[0].color;
|
|
781
|
+
seriesNode["c:spPr"]["a:ln"]["a:solidFill"] =
|
|
782
|
+
buildColorNode(first)["a:solidFill"];
|
|
783
|
+
}
|
|
784
|
+
else if (seriesColor) {
|
|
785
|
+
seriesNode["c:spPr"]["a:ln"]["a:solidFill"] =
|
|
786
|
+
buildColorNode(seriesColor)["a:solidFill"];
|
|
787
|
+
}
|
|
788
|
+
else if (index < 6) {
|
|
789
|
+
seriesNode["c:spPr"]["a:ln"]["a:solidFill"]["a:schemeClr"].a$.val = `accent${index + 1}`;
|
|
790
|
+
}
|
|
791
|
+
if (domainRef) {
|
|
792
|
+
seriesNode["c:cat"]["c:strRef"]["c:f"] = domainRef;
|
|
793
|
+
}
|
|
794
|
+
else {
|
|
795
|
+
removeProp(seriesNode, "c:cat");
|
|
796
|
+
}
|
|
797
|
+
seriesNode["c:val"]["c:numRef"]["c:f"] = ref;
|
|
798
|
+
if (smooth) {
|
|
799
|
+
seriesNode["c:smooth"].a$.val = "1";
|
|
800
|
+
}
|
|
801
|
+
const labelNode = buildSeriesLabel(seriesData?.dataLabel ?? undefined, sheetName, seriesData?.dataLabelRef);
|
|
802
|
+
if (labelNode) {
|
|
803
|
+
seriesNode["c:tx"] = labelNode["c:tx"];
|
|
804
|
+
}
|
|
805
|
+
return seriesNode;
|
|
806
|
+
};
|
|
807
|
+
/**
|
|
808
|
+
* Build an area series node.
|
|
809
|
+
*/
|
|
810
|
+
const buildAreaSeriesNode = (index, ref, seriesData, domainRef, sheetName) => {
|
|
811
|
+
const seriesNode = cloneTemplate(area_series);
|
|
812
|
+
seriesNode["c:idx"] = { a$: { val: index.toString() } };
|
|
813
|
+
seriesNode["c:order"] = { a$: { val: index.toString() } };
|
|
814
|
+
const seriesColor = seriesData?.color;
|
|
815
|
+
if (applySeriesGradient(seriesNode, seriesData?.gradient)) {
|
|
816
|
+
// Gradient applied to the fill area. Stroke stays solid using
|
|
817
|
+
// the first stop's color so the outline is visible.
|
|
818
|
+
if (seriesData?.gradient?.stops?.length) {
|
|
819
|
+
seriesNode["c:spPr"]["a:ln"]["a:solidFill"] =
|
|
820
|
+
buildColorNode(seriesData.gradient.stops[0].color)["a:solidFill"];
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
else if (seriesColor) {
|
|
824
|
+
seriesNode["c:spPr"]["a:solidFill"] =
|
|
825
|
+
buildColorNode(seriesColor)["a:solidFill"];
|
|
826
|
+
seriesNode["c:spPr"]["a:ln"]["a:solidFill"] =
|
|
827
|
+
buildColorNode(seriesColor)["a:solidFill"];
|
|
828
|
+
}
|
|
829
|
+
else if (index < 6) {
|
|
830
|
+
seriesNode["c:spPr"]["a:solidFill"]["a:schemeClr"].a$.val = `accent${index + 1}`;
|
|
831
|
+
seriesNode["c:spPr"]["a:ln"]["a:solidFill"]["a:schemeClr"].a$.val = `accent${index + 1}`;
|
|
832
|
+
}
|
|
833
|
+
if (domainRef) {
|
|
834
|
+
seriesNode["c:cat"]["c:strRef"]["c:f"] = domainRef;
|
|
835
|
+
}
|
|
836
|
+
else {
|
|
837
|
+
removeProp(seriesNode, "c:cat");
|
|
838
|
+
}
|
|
839
|
+
seriesNode["c:val"]["c:numRef"]["c:f"] = ref;
|
|
840
|
+
const labelNode = buildSeriesLabel(seriesData?.dataLabel ?? undefined, sheetName, seriesData?.dataLabelRef);
|
|
841
|
+
if (labelNode) {
|
|
842
|
+
seriesNode["c:tx"] = labelNode["c:tx"];
|
|
843
|
+
}
|
|
844
|
+
return seriesNode;
|
|
845
|
+
};
|
|
846
|
+
/**
|
|
847
|
+
* Pie/doughnut templates carry exactly one `c:ser`; a source chart with
|
|
848
|
+
* several series (e.g. seven single-cell slices) silently lost all but the
|
|
849
|
+
* first. Convert the template's ser into an array and append one node per
|
|
850
|
+
* remaining series, each with its own idx/order, name, category and value
|
|
851
|
+
* reference.
|
|
852
|
+
*/
|
|
853
|
+
const appendExtraCircularSeries = (chartNode, groupKey, seriesRefs, seriesCatRefs, domainRef, series, sheetName) => {
|
|
854
|
+
const group = chartNode["c:plotArea"]?.[groupKey];
|
|
855
|
+
if (!group)
|
|
856
|
+
return;
|
|
857
|
+
const extra = seriesRefs
|
|
858
|
+
.map((ref, index) => ({ ref, index }))
|
|
859
|
+
.filter(({ ref, index }) => index > 0 && Boolean(ref));
|
|
860
|
+
if (!extra.length)
|
|
861
|
+
return;
|
|
862
|
+
const firstSer = group["c:ser"];
|
|
863
|
+
const sers = Array.isArray(firstSer) ? firstSer : [firstSer];
|
|
864
|
+
// The template's single ser is series 0; give it its per-series cat when
|
|
865
|
+
// one was captured.
|
|
866
|
+
const firstCat = seriesCatRefs[0];
|
|
867
|
+
if (firstCat && sers[0]?.["c:cat"]?.["c:strRef"]) {
|
|
868
|
+
sers[0]["c:cat"]["c:strRef"]["c:f"] = firstCat;
|
|
869
|
+
}
|
|
870
|
+
for (const { ref, index } of extra) {
|
|
871
|
+
const node = {
|
|
872
|
+
"c:idx": { a$: { val: String(index) } },
|
|
873
|
+
"c:order": { a$: { val: String(index) } },
|
|
874
|
+
};
|
|
875
|
+
const labelNode = buildSeriesLabel(series[index]?.dataLabel ?? undefined, sheetName, series[index]?.dataLabelRef);
|
|
876
|
+
if (labelNode) {
|
|
877
|
+
node["c:tx"] = labelNode["c:tx"];
|
|
878
|
+
}
|
|
879
|
+
const catRef = seriesCatRefs[index] ?? domainRef;
|
|
880
|
+
if (catRef) {
|
|
881
|
+
node["c:cat"] = { "c:strRef": { "c:f": catRef } };
|
|
882
|
+
}
|
|
883
|
+
node["c:val"] = { "c:numRef": { "c:f": ref } };
|
|
884
|
+
sers.push(node);
|
|
885
|
+
}
|
|
886
|
+
group["c:ser"] = sers;
|
|
887
|
+
};
|
|
888
|
+
const buildPieChart = (domainRef, valueRef, title, subtitle) => {
|
|
889
|
+
const pieChart = cloneTemplate(donut_json);
|
|
890
|
+
applyChartTitle(pieChart, title, undefined, subtitle);
|
|
891
|
+
const plotArea = pieChart["c:plotArea"];
|
|
892
|
+
const donut = plotArea["c:doughnutChart"];
|
|
893
|
+
removeProp(donut, "c:holeSize");
|
|
894
|
+
plotArea["c:pieChart"] = donut;
|
|
895
|
+
removeProp(plotArea, "c:doughnutChart");
|
|
896
|
+
const seriesNode = plotArea["c:pieChart"]["c:ser"];
|
|
897
|
+
if (domainRef) {
|
|
898
|
+
seriesNode["c:cat"]["c:strRef"]["c:f"] = domainRef;
|
|
899
|
+
}
|
|
900
|
+
if (valueRef) {
|
|
901
|
+
seriesNode["c:val"]["c:numRef"]["c:f"] = valueRef;
|
|
902
|
+
}
|
|
903
|
+
return pieChart;
|
|
904
|
+
};
|
|
905
|
+
/**
|
|
906
|
+
* Pie-of-pie / bar-of-pie (`<c:ofPieChart>`). Reuses the donut template
|
|
907
|
+
* like buildPieChart but rebuilds the chart-group node so the of-pie
|
|
908
|
+
* elements land in the CT_OfPieChart sequence order: ofPieType,
|
|
909
|
+
* varyColors, ser, dLbls, gapWidth, splitType, splitPos, secondPieSize,
|
|
910
|
+
* serLines. splitType maps the spec vocabulary back to the OOXML
|
|
911
|
+
* tokens (`pos`/`val`/`percent`); `<c:serLines/>` turns on Excel's
|
|
912
|
+
* connector lines between the two plots.
|
|
913
|
+
*/
|
|
914
|
+
const buildOfPieChart = (domainRef, valueRef, title, spec, subtitle) => {
|
|
915
|
+
const chartObj = cloneTemplate(donut_json);
|
|
916
|
+
applyChartTitle(chartObj, title, undefined, subtitle);
|
|
917
|
+
const plotArea = chartObj["c:plotArea"];
|
|
918
|
+
const donut = plotArea["c:doughnutChart"];
|
|
919
|
+
// holeSize and firstSliceAng are pie/doughnut-only — invalid inside
|
|
920
|
+
// <c:ofPieChart>.
|
|
921
|
+
removeProp(donut, "c:holeSize");
|
|
922
|
+
removeProp(donut, "c:firstSliceAng");
|
|
923
|
+
const splitTypeVal = spec?.splitType === "position"
|
|
924
|
+
? "pos"
|
|
925
|
+
: spec?.splitType === "value"
|
|
926
|
+
? "val"
|
|
927
|
+
: spec?.splitType === "percent"
|
|
928
|
+
? "percent"
|
|
929
|
+
: undefined;
|
|
930
|
+
const ofPieNode = {
|
|
931
|
+
"c:ofPieType": { a$: { val: spec?.ofPieType === "bar" ? "bar" : "pie" } },
|
|
932
|
+
"c:varyColors": donut["c:varyColors"],
|
|
933
|
+
"c:ser": donut["c:ser"],
|
|
934
|
+
"c:dLbls": donut["c:dLbls"],
|
|
935
|
+
// Excel's default gap between the two plots.
|
|
936
|
+
"c:gapWidth": { a$: { val: "100" } },
|
|
937
|
+
...(splitTypeVal ? { "c:splitType": { a$: { val: splitTypeVal } } } : {}),
|
|
938
|
+
...(typeof spec?.splitPos === "number" && Number.isFinite(spec.splitPos)
|
|
939
|
+
? { "c:splitPos": { a$: { val: String(spec.splitPos) } } }
|
|
940
|
+
: {}),
|
|
941
|
+
...(typeof spec?.secondPieSize === "number" &&
|
|
942
|
+
Number.isFinite(spec.secondPieSize)
|
|
943
|
+
? { "c:secondPieSize": { a$: { val: String(Math.round(spec.secondPieSize)) } } }
|
|
944
|
+
: {}),
|
|
945
|
+
"c:serLines": {},
|
|
946
|
+
};
|
|
947
|
+
plotArea["c:ofPieChart"] = ofPieNode;
|
|
948
|
+
removeProp(plotArea, "c:doughnutChart");
|
|
949
|
+
const seriesNode = ofPieNode["c:ser"];
|
|
950
|
+
if (domainRef) {
|
|
951
|
+
seriesNode["c:cat"]["c:strRef"]["c:f"] = domainRef;
|
|
952
|
+
}
|
|
953
|
+
if (valueRef) {
|
|
954
|
+
seriesNode["c:val"]["c:numRef"]["c:f"] = valueRef;
|
|
955
|
+
}
|
|
956
|
+
return chartObj;
|
|
957
|
+
};
|
|
958
|
+
/**
|
|
959
|
+
* Minimal builder for chart types that follow the line-style template
|
|
960
|
+
* (radar, stock, etc.) — single chart-type container with a flat series
|
|
961
|
+
* list. Reuses the column template and rewrites its barChart block to
|
|
962
|
+
* the target chart-type element. The renderer/Excel handles the rest.
|
|
963
|
+
*/
|
|
964
|
+
const buildRadarLikeChart = (chartElementKey, domainRef, seriesRefs, title, radarStyle, subtitle) => {
|
|
965
|
+
const chartObj = cloneTemplate(column_json);
|
|
966
|
+
applyChartTitle(chartObj, title, undefined, subtitle);
|
|
967
|
+
const plotArea = chartObj["c:plotArea"];
|
|
968
|
+
const barChart = plotArea["c:barChart"];
|
|
969
|
+
const axisIds = barChart?.["c:axId"] ?? [];
|
|
970
|
+
const containerNode = {
|
|
971
|
+
"c:varyColors": { a$: { val: "0" } },
|
|
972
|
+
"c:ser": [],
|
|
973
|
+
"c:axId": axisIds,
|
|
974
|
+
};
|
|
975
|
+
if (chartElementKey === "c:radarChart") {
|
|
976
|
+
containerNode["c:radarStyle"] = {
|
|
977
|
+
a$: { val: radarStyle ?? "standard" },
|
|
978
|
+
};
|
|
979
|
+
}
|
|
980
|
+
removeProp(plotArea, "c:barChart");
|
|
981
|
+
plotArea[chartElementKey] = containerNode;
|
|
982
|
+
const seriesContainer = containerNode["c:ser"];
|
|
983
|
+
seriesRefs.forEach((ref, index) => {
|
|
984
|
+
if (!ref)
|
|
985
|
+
return;
|
|
986
|
+
const seriesNode = {
|
|
987
|
+
"c:idx": { a$: { val: String(index) } },
|
|
988
|
+
"c:order": { a$: { val: String(index) } },
|
|
989
|
+
"c:cat": domainRef
|
|
990
|
+
? { "c:strRef": { "c:f": domainRef } }
|
|
991
|
+
: undefined,
|
|
992
|
+
"c:val": { "c:numRef": { "c:f": ref } },
|
|
993
|
+
};
|
|
994
|
+
if (!domainRef)
|
|
995
|
+
delete seriesNode["c:cat"];
|
|
996
|
+
seriesContainer.push(seriesNode);
|
|
997
|
+
});
|
|
998
|
+
return chartObj;
|
|
999
|
+
};
|
|
1000
|
+
const buildDoughnutChart = (domainRef, valueRef, title, subtitle) => {
|
|
1001
|
+
// Doughnut uses the same donut_json template as pie but keeps the
|
|
1002
|
+
// <c:doughnutChart> wrapper (and the holeSize, which we set later
|
|
1003
|
+
// via applyDoughnutHole when the spec carries one).
|
|
1004
|
+
const chartObj = cloneTemplate(donut_json);
|
|
1005
|
+
applyChartTitle(chartObj, title, undefined, subtitle);
|
|
1006
|
+
const plotArea = chartObj["c:plotArea"];
|
|
1007
|
+
const seriesNode = plotArea["c:doughnutChart"]["c:ser"];
|
|
1008
|
+
if (domainRef) {
|
|
1009
|
+
seriesNode["c:cat"]["c:strRef"]["c:f"] = domainRef;
|
|
1010
|
+
}
|
|
1011
|
+
if (valueRef) {
|
|
1012
|
+
seriesNode["c:val"]["c:numRef"]["c:f"] = valueRef;
|
|
1013
|
+
}
|
|
1014
|
+
return chartObj;
|
|
1015
|
+
};
|
|
1016
|
+
export const buildChartXml = (chart, sheetNameById) => {
|
|
1017
|
+
const series = chart.spec?.series ?? [];
|
|
1018
|
+
if (!series.length)
|
|
1019
|
+
return undefined;
|
|
1020
|
+
const domainRef = formatSourcesRef(chart.spec.domains?.[0]?.sources, sheetNameById);
|
|
1021
|
+
const seriesRefs = series.map((entry) => formatSourcesRef(entry?.sources, sheetNameById));
|
|
1022
|
+
const seriesCatRefs = series.map((entry) => seriesCategoryRef(entry, sheetNameById));
|
|
1023
|
+
if (!seriesRefs.some(Boolean))
|
|
1024
|
+
return undefined;
|
|
1025
|
+
const chartType = chart.spec.chartType;
|
|
1026
|
+
const chartTitle = chart.spec.title ?? undefined;
|
|
1027
|
+
// Subtitle line rendered under the title (e.g. a "Total Value to
|
|
1028
|
+
// Paid-In" caption under "Net MOIC by Fund Vintage"). Emitted as a
|
|
1029
|
+
// second `<a:p>` in the title's rich block — see `applyChartTitle`.
|
|
1030
|
+
const chartSubtitle = chart.spec.subtitle ?? undefined;
|
|
1031
|
+
// Authored title text formatting (color/font/bold) — round-tripped
|
|
1032
|
+
// back so Excel keeps the styled title we read on import.
|
|
1033
|
+
const chartTitleFmt = chart.spec.titleTextFormat ?? undefined;
|
|
1034
|
+
const smooth = "smooth" in chart.spec ? chart.spec.smooth : undefined;
|
|
1035
|
+
const chartSpace = cloneTemplate(chart_template);
|
|
1036
|
+
const sheetName = sheetNameById.get(chart.position.sheetId);
|
|
1037
|
+
// Check for combo chart (multiple series chart types)
|
|
1038
|
+
if (isComboChart(series, chartType)) {
|
|
1039
|
+
const groups = groupSeriesByChartType(series, seriesRefs, chartType);
|
|
1040
|
+
// Build combo chart with multiple chart type containers
|
|
1041
|
+
const comboChart = cloneTemplate(column_json);
|
|
1042
|
+
applyChartTitle(comboChart, chartTitle, chartTitleFmt, chartSubtitle);
|
|
1043
|
+
const plotArea = comboChart["c:plotArea"];
|
|
1044
|
+
const baseBarChart = plotArea["c:barChart"];
|
|
1045
|
+
const axisIds = baseBarChart?.["c:axId"] ?? [];
|
|
1046
|
+
const dLbls = baseBarChart?.["c:dLbls"] ?? {};
|
|
1047
|
+
// Remove the default bar chart - we'll add specific chart types
|
|
1048
|
+
removeProp(plotArea, "c:barChart");
|
|
1049
|
+
// Add chart type containers for each group
|
|
1050
|
+
for (const [groupType, groupSeries] of groups) {
|
|
1051
|
+
if (groupType === "bar" || groupType === "column") {
|
|
1052
|
+
const barChartNode = {
|
|
1053
|
+
"c:barDir": { a$: { val: groupType === "bar" ? "bar" : "col" } },
|
|
1054
|
+
"c:grouping": { a$: { val: "clustered" } },
|
|
1055
|
+
"c:varyColors": { a$: { val: "0" } },
|
|
1056
|
+
"c:ser": [],
|
|
1057
|
+
"c:dLbls": dLbls,
|
|
1058
|
+
"c:axId": axisIds,
|
|
1059
|
+
};
|
|
1060
|
+
for (let i = 0; i < groupSeries.length; i++) {
|
|
1061
|
+
const { index, ref, data } = groupSeries[i];
|
|
1062
|
+
const seriesNode = buildBarSeriesNode(index, ref, data, seriesCatRefs[index] ?? domainRef, sheetName, i === 0);
|
|
1063
|
+
barChartNode["c:ser"].push(seriesNode);
|
|
1064
|
+
}
|
|
1065
|
+
plotArea["c:barChart"] = barChartNode;
|
|
1066
|
+
}
|
|
1067
|
+
else if (groupType === "line") {
|
|
1068
|
+
const lineChartNode = {
|
|
1069
|
+
"c:grouping": { a$: { val: "standard" } },
|
|
1070
|
+
"c:varyColors": { a$: { val: "0" } },
|
|
1071
|
+
"c:ser": [],
|
|
1072
|
+
"c:dLbls": dLbls,
|
|
1073
|
+
"c:axId": axisIds,
|
|
1074
|
+
};
|
|
1075
|
+
for (const { index, ref, data } of groupSeries) {
|
|
1076
|
+
const seriesNode = buildLineSeriesNode(index, ref, data, seriesCatRefs[index] ?? domainRef, sheetName, smooth);
|
|
1077
|
+
lineChartNode["c:ser"].push(seriesNode);
|
|
1078
|
+
}
|
|
1079
|
+
plotArea["c:lineChart"] = lineChartNode;
|
|
1080
|
+
}
|
|
1081
|
+
else if (groupType === "area") {
|
|
1082
|
+
const areaChartNode = {
|
|
1083
|
+
"c:grouping": { a$: { val: "standard" } },
|
|
1084
|
+
"c:varyColors": { a$: { val: "0" } },
|
|
1085
|
+
"c:ser": [],
|
|
1086
|
+
"c:dLbls": dLbls,
|
|
1087
|
+
"c:axId": axisIds,
|
|
1088
|
+
};
|
|
1089
|
+
for (const { index, ref, data } of groupSeries) {
|
|
1090
|
+
const seriesNode = buildAreaSeriesNode(index, ref, data, seriesCatRefs[index] ?? domainRef, sheetName);
|
|
1091
|
+
areaChartNode["c:ser"].push(seriesNode);
|
|
1092
|
+
}
|
|
1093
|
+
plotArea["c:areaChart"] = areaChartNode;
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
chartSpace["c:chartSpace"]["c:chart"] = comboChart;
|
|
1097
|
+
const xml = chartBuilder.build(chartSpace);
|
|
1098
|
+
return `${XML_DECLARATION}${xml}`;
|
|
1099
|
+
}
|
|
1100
|
+
// Handle non-combo charts (original logic)
|
|
1101
|
+
if (isC15ChartType(chartType)) {
|
|
1102
|
+
// Histogram / boxplot / waterfall / funnel emit as a barChart for
|
|
1103
|
+
// older Excel compatibility. For histogram (Phase 5.5b), the
|
|
1104
|
+
// barChart is wrapped in mc:AlternateContent with a c15:histogramChart
|
|
1105
|
+
// sibling in mc:Choice — Excel 2016+ uses the c15 chart, older
|
|
1106
|
+
// versions fall back to bar. For boxplot/waterfall/funnel we still
|
|
1107
|
+
// use the c15 extension marker (round-trips in our pipeline; opens
|
|
1108
|
+
// as bar chart in Excel) until those types get the same upgrade.
|
|
1109
|
+
const barChartDoc = cloneTemplate(column_json);
|
|
1110
|
+
applyChartTitle(barChartDoc, chartTitle, chartTitleFmt, chartSubtitle);
|
|
1111
|
+
const bar = barChartDoc["c:plotArea"]["c:barChart"];
|
|
1112
|
+
bar["c:barDir"].a$.val = "col";
|
|
1113
|
+
const seriesContainer = bar["c:ser"];
|
|
1114
|
+
seriesRefs.forEach((ref, index) => {
|
|
1115
|
+
if (!ref)
|
|
1116
|
+
return;
|
|
1117
|
+
const seriesNode = cloneTemplate(column_series);
|
|
1118
|
+
seriesNode["c:idx"] = { a$: { val: index.toString() } };
|
|
1119
|
+
seriesNode["c:order"] = { a$: { val: index.toString() } };
|
|
1120
|
+
const seriesColor = series[index]?.color;
|
|
1121
|
+
if (applySeriesGradient(seriesNode, series[index]?.gradient)) {
|
|
1122
|
+
// Gradient applied; nothing else.
|
|
1123
|
+
}
|
|
1124
|
+
else if (seriesColor) {
|
|
1125
|
+
seriesNode["c:spPr"]["a:solidFill"] =
|
|
1126
|
+
buildColorNode(seriesColor)["a:solidFill"];
|
|
1127
|
+
}
|
|
1128
|
+
else if (index < 6) {
|
|
1129
|
+
seriesNode["c:spPr"]["a:solidFill"]["a:schemeClr"].a$.val = `accent${index + 1}`;
|
|
1130
|
+
}
|
|
1131
|
+
if (!index && domainRef) {
|
|
1132
|
+
seriesNode["c:cat"] = {
|
|
1133
|
+
"c:strRef": {
|
|
1134
|
+
"c:f": domainRef,
|
|
1135
|
+
},
|
|
1136
|
+
};
|
|
1137
|
+
}
|
|
1138
|
+
seriesNode["c:val"]["c:numRef"]["c:f"] = ref;
|
|
1139
|
+
const labelNode = buildSeriesLabel(series[index]?.dataLabel ?? undefined, sheetNameById.get(chart.position.sheetId), series[index]?.dataLabelRef);
|
|
1140
|
+
if (labelNode) {
|
|
1141
|
+
seriesNode["c:tx"] = labelNode["c:tx"];
|
|
1142
|
+
}
|
|
1143
|
+
seriesContainer.push(seriesNode);
|
|
1144
|
+
});
|
|
1145
|
+
// Phase 5.5b: wrap the bar chart in mc:AlternateContent and emit
|
|
1146
|
+
// a full c15:* chart in mc:Choice so Excel 2016+ renders natively.
|
|
1147
|
+
// c:catAx / c:valAx stay at c:plotArea level — both Choice and
|
|
1148
|
+
// Fallback reference them through matching axId values.
|
|
1149
|
+
const axIdNodes = bar["c:axId"];
|
|
1150
|
+
const catAxId = axIdNodes?.[0]?.a$?.val ?? "1";
|
|
1151
|
+
const valAxId = axIdNodes?.[1]?.a$?.val ?? "2";
|
|
1152
|
+
const c15Args = {
|
|
1153
|
+
seriesRefs,
|
|
1154
|
+
series,
|
|
1155
|
+
domainRef,
|
|
1156
|
+
axIds: { catAxId, valAxId },
|
|
1157
|
+
};
|
|
1158
|
+
let c15Chart;
|
|
1159
|
+
if (chartType === "histogram") {
|
|
1160
|
+
c15Chart = buildC15HistogramChartNode(chart.spec, c15Args.seriesRefs, c15Args.series, c15Args.domainRef, c15Args.axIds);
|
|
1161
|
+
}
|
|
1162
|
+
else if (chartType === "boxplot") {
|
|
1163
|
+
c15Chart = buildC15BoxplotChartNode(chart.spec, c15Args.seriesRefs, c15Args.series, c15Args.domainRef, c15Args.axIds);
|
|
1164
|
+
}
|
|
1165
|
+
else if (chartType === "waterfall") {
|
|
1166
|
+
c15Chart = buildC15WaterfallChartNode(chart.spec, c15Args.seriesRefs, c15Args.series, c15Args.domainRef, c15Args.axIds);
|
|
1167
|
+
}
|
|
1168
|
+
else {
|
|
1169
|
+
c15Chart = buildC15FunnelChartNode(chart.spec, c15Args.seriesRefs, c15Args.series, c15Args.domainRef, c15Args.axIds);
|
|
1170
|
+
}
|
|
1171
|
+
const altContent = buildC15AlternateContent(chartType, c15Chart, bar);
|
|
1172
|
+
removeProp(barChartDoc["c:plotArea"], "c:barChart");
|
|
1173
|
+
barChartDoc["c:plotArea"]["mc:AlternateContent"] = altContent;
|
|
1174
|
+
chartSpace["c:chartSpace"]["c:chart"] = barChartDoc;
|
|
1175
|
+
}
|
|
1176
|
+
else if (chartType === "bar" || chartType === "column") {
|
|
1177
|
+
const barChart = cloneTemplate(column_json);
|
|
1178
|
+
applyChartTitle(barChart, chartTitle, chartTitleFmt, chartSubtitle);
|
|
1179
|
+
const bar = barChart["c:plotArea"]["c:barChart"];
|
|
1180
|
+
bar["c:barDir"].a$.val = chartType === "bar" ? "bar" : "col";
|
|
1181
|
+
// Grouping / overlap / gapWidth — Excel's stacked / percent-stacked
|
|
1182
|
+
// bars need these set. Excel's defaults are `clustered` + overlap=0
|
|
1183
|
+
// + gapWidth=150; we only emit when the spec actually carries a
|
|
1184
|
+
// value to avoid bloating the file.
|
|
1185
|
+
const stackedType = chart.spec.stackedType;
|
|
1186
|
+
if (stackedType === "STACKED") {
|
|
1187
|
+
bar["c:grouping"] = { a$: { val: "stacked" } };
|
|
1188
|
+
}
|
|
1189
|
+
else if (stackedType === "PERCENT_STACKED") {
|
|
1190
|
+
bar["c:grouping"] = { a$: { val: "percentStacked" } };
|
|
1191
|
+
}
|
|
1192
|
+
else if (stackedType === "UNSTACKED") {
|
|
1193
|
+
bar["c:grouping"] = { a$: { val: "clustered" } };
|
|
1194
|
+
}
|
|
1195
|
+
const overlap = chart.spec.overlap;
|
|
1196
|
+
if (typeof overlap === "number") {
|
|
1197
|
+
bar["c:overlap"] = { a$: { val: String(Math.round(overlap)) } };
|
|
1198
|
+
}
|
|
1199
|
+
const gapWidth = chart.spec.gapWidth;
|
|
1200
|
+
if (typeof gapWidth === "number") {
|
|
1201
|
+
bar["c:gapWidth"] = { a$: { val: String(Math.round(gapWidth)) } };
|
|
1202
|
+
}
|
|
1203
|
+
const seriesContainer = bar["c:ser"];
|
|
1204
|
+
// Inline categories — when no cell-range, use the first domain's
|
|
1205
|
+
// inlineValues directly on the first series' c:cat.
|
|
1206
|
+
const inlineDomainValues = chart.spec.domains?.[0]
|
|
1207
|
+
?.inlineValues;
|
|
1208
|
+
seriesRefs.forEach((ref, index) => {
|
|
1209
|
+
if (!ref && !series[index]?.inlineValues)
|
|
1210
|
+
return;
|
|
1211
|
+
// Decorate the spec entry with the chart-level inline domain so
|
|
1212
|
+
// `buildBarSeriesNode` can emit `<c:strLit>` on the first series.
|
|
1213
|
+
const enrichedSeries = index === 0 && !domainRef && inlineDomainValues?.length
|
|
1214
|
+
? {
|
|
1215
|
+
...series[index],
|
|
1216
|
+
inlineDomainValues,
|
|
1217
|
+
}
|
|
1218
|
+
: series[index];
|
|
1219
|
+
const seriesNode = buildBarSeriesNode(index, ref ?? "", enrichedSeries, seriesCatRefs[index] ?? domainRef, sheetNameById.get(chart.position.sheetId), index === 0);
|
|
1220
|
+
seriesContainer.push(seriesNode);
|
|
1221
|
+
});
|
|
1222
|
+
chartSpace["c:chartSpace"]["c:chart"] = barChart;
|
|
1223
|
+
}
|
|
1224
|
+
else if (chartType === "pie") {
|
|
1225
|
+
const valueRef = seriesRefs.find(Boolean);
|
|
1226
|
+
const specAny = chart.spec;
|
|
1227
|
+
// Pie-of-pie / bar-of-pie round-trips as <c:ofPieChart> when the
|
|
1228
|
+
// spec carries the OF_PIE marker (or an explicit ofPieType).
|
|
1229
|
+
const isOfPie = String(specAny?.sourceChartType ?? "").toUpperCase() === "OF_PIE" ||
|
|
1230
|
+
specAny?.ofPieType === "pie" ||
|
|
1231
|
+
specAny?.ofPieType === "bar";
|
|
1232
|
+
const pieChart = isOfPie
|
|
1233
|
+
? buildOfPieChart(domainRef, valueRef, chartTitle, specAny, chartSubtitle)
|
|
1234
|
+
: buildPieChart(domainRef, valueRef, chartTitle, chartSubtitle);
|
|
1235
|
+
if (!isOfPie) {
|
|
1236
|
+
appendExtraCircularSeries(pieChart, "c:pieChart", seriesRefs, seriesCatRefs, domainRef, series, sheetNameById.get(chart.position.sheetId));
|
|
1237
|
+
}
|
|
1238
|
+
chartSpace["c:chartSpace"]["c:chart"] = pieChart;
|
|
1239
|
+
}
|
|
1240
|
+
else if (chartType === "doughnut") {
|
|
1241
|
+
const valueRef = seriesRefs.find(Boolean);
|
|
1242
|
+
const doughnutChart = buildDoughnutChart(domainRef, valueRef, chartTitle, chartSubtitle);
|
|
1243
|
+
appendExtraCircularSeries(doughnutChart, "c:doughnutChart", seriesRefs, seriesCatRefs, domainRef, series, sheetNameById.get(chart.position.sheetId));
|
|
1244
|
+
chartSpace["c:chartSpace"]["c:chart"] = doughnutChart;
|
|
1245
|
+
}
|
|
1246
|
+
else if (chartType === "radar") {
|
|
1247
|
+
const radarChart = buildRadarLikeChart("c:radarChart", domainRef, seriesRefs, chartTitle, chart.spec.radarStyle ?? "standard", chartSubtitle);
|
|
1248
|
+
chartSpace["c:chartSpace"]["c:chart"] = radarChart;
|
|
1249
|
+
}
|
|
1250
|
+
else if (chartType === "stock") {
|
|
1251
|
+
const stockChart = buildRadarLikeChart("c:stockChart", domainRef, seriesRefs, chartTitle, undefined, chartSubtitle);
|
|
1252
|
+
chartSpace["c:chartSpace"]["c:chart"] = stockChart;
|
|
1253
|
+
}
|
|
1254
|
+
else if (chartType === "line") {
|
|
1255
|
+
const lineChart = cloneTemplate(column_json);
|
|
1256
|
+
applyChartTitle(lineChart, chartTitle, chartTitleFmt, chartSubtitle);
|
|
1257
|
+
const plotArea = lineChart["c:plotArea"];
|
|
1258
|
+
const barChart = plotArea["c:barChart"];
|
|
1259
|
+
const lineChartNode = {
|
|
1260
|
+
"c:grouping": {
|
|
1261
|
+
a$: {
|
|
1262
|
+
val: "standard",
|
|
1263
|
+
},
|
|
1264
|
+
},
|
|
1265
|
+
"c:varyColors": {
|
|
1266
|
+
a$: {
|
|
1267
|
+
val: "0",
|
|
1268
|
+
},
|
|
1269
|
+
},
|
|
1270
|
+
"c:ser": [],
|
|
1271
|
+
"c:dLbls": barChart?.["c:dLbls"] ?? {},
|
|
1272
|
+
"c:axId": barChart?.["c:axId"] ?? [],
|
|
1273
|
+
};
|
|
1274
|
+
removeProp(plotArea, "c:barChart");
|
|
1275
|
+
plotArea["c:lineChart"] = lineChartNode;
|
|
1276
|
+
const seriesContainer = lineChartNode["c:ser"];
|
|
1277
|
+
seriesRefs.forEach((ref, index) => {
|
|
1278
|
+
if (!ref)
|
|
1279
|
+
return;
|
|
1280
|
+
const seriesNode = cloneTemplate(line_series);
|
|
1281
|
+
seriesNode["c:idx"] = { a$: { val: index.toString() } };
|
|
1282
|
+
seriesNode["c:order"] = { a$: { val: index.toString() } };
|
|
1283
|
+
const seriesColor = series[index]?.color;
|
|
1284
|
+
if (seriesColor) {
|
|
1285
|
+
seriesNode["c:spPr"]["a:ln"]["a:solidFill"] =
|
|
1286
|
+
buildColorNode(seriesColor)["a:solidFill"];
|
|
1287
|
+
}
|
|
1288
|
+
else if (index < 6) {
|
|
1289
|
+
seriesNode["c:spPr"]["a:ln"]["a:solidFill"]["a:schemeClr"].a$.val = `accent${index + 1}`;
|
|
1290
|
+
}
|
|
1291
|
+
// Category axis is optional - Excel will use numeric indices if not provided
|
|
1292
|
+
const catRef = seriesCatRefs[index] ?? domainRef;
|
|
1293
|
+
if (catRef) {
|
|
1294
|
+
seriesNode["c:cat"]["c:strRef"]["c:f"] = catRef;
|
|
1295
|
+
}
|
|
1296
|
+
else {
|
|
1297
|
+
// Remove the category reference if no domain
|
|
1298
|
+
removeProp(seriesNode, "c:cat");
|
|
1299
|
+
}
|
|
1300
|
+
seriesNode["c:val"]["c:numRef"]["c:f"] = ref;
|
|
1301
|
+
if (smooth) {
|
|
1302
|
+
seriesNode["c:smooth"].a$.val = "1";
|
|
1303
|
+
}
|
|
1304
|
+
const labelNode = buildSeriesLabel(series[index]?.dataLabel ?? undefined, sheetNameById.get(chart.position.sheetId), series[index]?.dataLabelRef);
|
|
1305
|
+
if (labelNode) {
|
|
1306
|
+
seriesNode["c:tx"] = labelNode["c:tx"];
|
|
1307
|
+
}
|
|
1308
|
+
seriesContainer.push(seriesNode);
|
|
1309
|
+
});
|
|
1310
|
+
chartSpace["c:chartSpace"]["c:chart"] = lineChart;
|
|
1311
|
+
}
|
|
1312
|
+
else if (chartType === "area") {
|
|
1313
|
+
const areaChart = cloneTemplate(column_json);
|
|
1314
|
+
applyChartTitle(areaChart, chartTitle, chartTitleFmt, chartSubtitle);
|
|
1315
|
+
const plotArea = areaChart["c:plotArea"];
|
|
1316
|
+
const barChart = plotArea["c:barChart"];
|
|
1317
|
+
const areaChartNode = {
|
|
1318
|
+
"c:grouping": {
|
|
1319
|
+
a$: {
|
|
1320
|
+
val: "standard",
|
|
1321
|
+
},
|
|
1322
|
+
},
|
|
1323
|
+
"c:varyColors": {
|
|
1324
|
+
a$: {
|
|
1325
|
+
val: "0",
|
|
1326
|
+
},
|
|
1327
|
+
},
|
|
1328
|
+
"c:ser": [],
|
|
1329
|
+
"c:dLbls": barChart?.["c:dLbls"] ?? {},
|
|
1330
|
+
"c:axId": barChart?.["c:axId"] ?? [],
|
|
1331
|
+
};
|
|
1332
|
+
removeProp(plotArea, "c:barChart");
|
|
1333
|
+
plotArea["c:areaChart"] = areaChartNode;
|
|
1334
|
+
const seriesContainer = areaChartNode["c:ser"];
|
|
1335
|
+
seriesRefs.forEach((ref, index) => {
|
|
1336
|
+
if (!ref)
|
|
1337
|
+
return;
|
|
1338
|
+
const seriesNode = cloneTemplate(area_series);
|
|
1339
|
+
seriesNode["c:idx"] = { a$: { val: index.toString() } };
|
|
1340
|
+
seriesNode["c:order"] = { a$: { val: index.toString() } };
|
|
1341
|
+
const seriesColor = series[index]?.color;
|
|
1342
|
+
if (seriesColor) {
|
|
1343
|
+
seriesNode["c:spPr"]["a:solidFill"] =
|
|
1344
|
+
buildColorNode(seriesColor)["a:solidFill"];
|
|
1345
|
+
seriesNode["c:spPr"]["a:ln"]["a:solidFill"] =
|
|
1346
|
+
buildColorNode(seriesColor)["a:solidFill"];
|
|
1347
|
+
}
|
|
1348
|
+
else if (index < 6) {
|
|
1349
|
+
seriesNode["c:spPr"]["a:solidFill"]["a:schemeClr"].a$.val = `accent${index + 1}`;
|
|
1350
|
+
seriesNode["c:spPr"]["a:ln"]["a:solidFill"]["a:schemeClr"].a$.val = `accent${index + 1}`;
|
|
1351
|
+
}
|
|
1352
|
+
// Category axis is optional - Excel will use numeric indices if not provided
|
|
1353
|
+
const catRef = seriesCatRefs[index] ?? domainRef;
|
|
1354
|
+
if (catRef) {
|
|
1355
|
+
seriesNode["c:cat"]["c:strRef"]["c:f"] = catRef;
|
|
1356
|
+
}
|
|
1357
|
+
else {
|
|
1358
|
+
removeProp(seriesNode, "c:cat");
|
|
1359
|
+
}
|
|
1360
|
+
seriesNode["c:val"]["c:numRef"]["c:f"] = ref;
|
|
1361
|
+
const labelNode = buildSeriesLabel(series[index]?.dataLabel ?? undefined, sheetNameById.get(chart.position.sheetId), series[index]?.dataLabelRef);
|
|
1362
|
+
if (labelNode) {
|
|
1363
|
+
seriesNode["c:tx"] = labelNode["c:tx"];
|
|
1364
|
+
}
|
|
1365
|
+
seriesContainer.push(seriesNode);
|
|
1366
|
+
});
|
|
1367
|
+
chartSpace["c:chartSpace"]["c:chart"] = areaChart;
|
|
1368
|
+
}
|
|
1369
|
+
else if (chartType === "scatter" || chartType === "bubble") {
|
|
1370
|
+
const bubbleSpec = chart.spec;
|
|
1371
|
+
const domainEntries = bubbleSpec.domains ?? [];
|
|
1372
|
+
const seriesEntries = bubbleSpec.series ?? [];
|
|
1373
|
+
const bubbleSizeEntries = bubbleSpec.bubbleSizes ?? [];
|
|
1374
|
+
const bubbleLabelEntries = bubbleSpec.bubbleLabels ?? [];
|
|
1375
|
+
const seriesContainer = [];
|
|
1376
|
+
const seriesTemplate = chartType === "bubble" ? bubble_series : scatter_series;
|
|
1377
|
+
seriesEntries.forEach((entry, index) => {
|
|
1378
|
+
const domainRef = formatRangeRef(getPrimarySource(domainEntries[index]) ??
|
|
1379
|
+
getPrimarySource(domainEntries[0]), sheetNameById);
|
|
1380
|
+
const valueRef = formatRangeRef(getPrimarySource(entry), sheetNameById);
|
|
1381
|
+
if (!domainRef || !valueRef)
|
|
1382
|
+
return;
|
|
1383
|
+
const seriesNode = cloneTemplate(seriesTemplate);
|
|
1384
|
+
seriesNode["c:idx"] = { a$: { val: index.toString() } };
|
|
1385
|
+
seriesNode["c:order"] = { a$: { val: index.toString() } };
|
|
1386
|
+
const seriesColor = entry?.color;
|
|
1387
|
+
if (seriesColor) {
|
|
1388
|
+
if (chartType === "bubble") {
|
|
1389
|
+
seriesNode["c:spPr"]["a:solidFill"] =
|
|
1390
|
+
buildColorNode(seriesColor)["a:solidFill"];
|
|
1391
|
+
}
|
|
1392
|
+
else {
|
|
1393
|
+
seriesNode["c:spPr"]["a:ln"]["a:solidFill"] =
|
|
1394
|
+
buildColorNode(seriesColor)["a:solidFill"];
|
|
1395
|
+
}
|
|
1396
|
+
}
|
|
1397
|
+
else if (index < 6) {
|
|
1398
|
+
const colorNode = chartType === "bubble"
|
|
1399
|
+
? seriesNode["c:spPr"]["a:solidFill"]["a:schemeClr"]
|
|
1400
|
+
: seriesNode["c:spPr"]["a:ln"]["a:solidFill"]["a:schemeClr"];
|
|
1401
|
+
colorNode.a$.val = `accent${index + 1}`;
|
|
1402
|
+
}
|
|
1403
|
+
seriesNode["c:xVal"]["c:numRef"]["c:f"] = domainRef;
|
|
1404
|
+
seriesNode["c:yVal"]["c:numRef"]["c:f"] = valueRef;
|
|
1405
|
+
if (chartType === "bubble") {
|
|
1406
|
+
const sizeRef = formatRangeRef(getPrimarySource(bubbleSizeEntries[index]) ??
|
|
1407
|
+
getPrimarySource(bubbleSizeEntries[0]) ??
|
|
1408
|
+
getPrimarySource(entry), sheetNameById);
|
|
1409
|
+
if (sizeRef) {
|
|
1410
|
+
seriesNode["c:bubbleSize"]["c:numRef"]["c:f"] = sizeRef;
|
|
1411
|
+
}
|
|
1412
|
+
}
|
|
1413
|
+
else if (smooth) {
|
|
1414
|
+
seriesNode["c:smooth"].a$.val = "1";
|
|
1415
|
+
}
|
|
1416
|
+
const labelNode = buildSeriesLabel(entry?.dataLabel ?? bubbleLabelEntries[index]?.dataLabel ?? undefined, sheetNameById.get(chart.position.sheetId), entry?.dataLabelRef);
|
|
1417
|
+
if (labelNode) {
|
|
1418
|
+
seriesNode["c:tx"] = labelNode["c:tx"];
|
|
1419
|
+
}
|
|
1420
|
+
seriesContainer.push(seriesNode);
|
|
1421
|
+
});
|
|
1422
|
+
if (!seriesContainer.length)
|
|
1423
|
+
return undefined;
|
|
1424
|
+
const chartDoc = cloneTemplate(scatter_json);
|
|
1425
|
+
applyChartTitle(chartDoc, chartTitle, chartTitleFmt, chartSubtitle);
|
|
1426
|
+
const plotArea = chartDoc["c:plotArea"];
|
|
1427
|
+
const scatterChart = plotArea["c:scatterChart"];
|
|
1428
|
+
if (chartType === "bubble") {
|
|
1429
|
+
removeProp(plotArea, "c:scatterChart");
|
|
1430
|
+
removeProp(scatterChart, "c:scatterStyle");
|
|
1431
|
+
scatterChart["c:bubbleScale"] = { a$: { val: "100" } };
|
|
1432
|
+
scatterChart["c:showNegBubbles"] = { a$: { val: "0" } };
|
|
1433
|
+
scatterChart["c:sizeRepresents"] = { a$: { val: "area" } };
|
|
1434
|
+
plotArea["c:bubbleChart"] = scatterChart;
|
|
1435
|
+
}
|
|
1436
|
+
else {
|
|
1437
|
+
// <c:scatterStyle> — Excel default is "lineMarker". Round-trip
|
|
1438
|
+
// whatever the spec carries so the connecting line / marker mix
|
|
1439
|
+
// survives re-saving.
|
|
1440
|
+
const scatterStyle = chart.spec.scatterStyle;
|
|
1441
|
+
if (scatterStyle && typeof scatterStyle === "string") {
|
|
1442
|
+
scatterChart["c:scatterStyle"] = { a$: { val: scatterStyle } };
|
|
1443
|
+
}
|
|
1444
|
+
}
|
|
1445
|
+
// Per-series marker — append to each series node that doesn't yet
|
|
1446
|
+
// carry one (scatter/bubble series templates already include a
|
|
1447
|
+
// marker block in many cases; only add when authored).
|
|
1448
|
+
seriesContainer.forEach((node, i) => {
|
|
1449
|
+
const markerSpec = chart.spec.series?.[i]?.marker;
|
|
1450
|
+
const m = buildMarkerNode(markerSpec);
|
|
1451
|
+
if (m) {
|
|
1452
|
+
node["c:marker"] = m;
|
|
1453
|
+
}
|
|
1454
|
+
});
|
|
1455
|
+
const targetChart = chartType === "bubble" ? plotArea["c:bubbleChart"] : scatterChart;
|
|
1456
|
+
targetChart["c:ser"] = seriesContainer;
|
|
1457
|
+
chartSpace["c:chartSpace"]["c:chart"] = chartDoc;
|
|
1458
|
+
}
|
|
1459
|
+
else if (chartType === "treemap" || chartType === "sunburst") {
|
|
1460
|
+
const treeSpec = chart.spec;
|
|
1461
|
+
const labelRef = formatRangeRef(getPrimarySource(treeSpec.labels?.[0]), sheetNameById);
|
|
1462
|
+
const sizeRef = formatRangeRef(getPrimarySource(treeSpec.sizeData?.[0]), sheetNameById);
|
|
1463
|
+
if (!labelRef || !sizeRef)
|
|
1464
|
+
return undefined;
|
|
1465
|
+
if (chartType === "sunburst") {
|
|
1466
|
+
const pieChart = buildPieChart(labelRef, sizeRef, chartTitle, chartSubtitle);
|
|
1467
|
+
chartSpace["c:chartSpace"]["c:chart"] = pieChart;
|
|
1468
|
+
}
|
|
1469
|
+
else {
|
|
1470
|
+
const columnChart = cloneTemplate(column_json);
|
|
1471
|
+
applyChartTitle(columnChart, chartTitle, chartTitleFmt, chartSubtitle);
|
|
1472
|
+
const bar = columnChart["c:plotArea"]["c:barChart"];
|
|
1473
|
+
const seriesContainer = bar["c:ser"];
|
|
1474
|
+
const seriesNode = cloneTemplate(column_series);
|
|
1475
|
+
seriesNode["c:idx"] = { a$: { val: "0" } };
|
|
1476
|
+
seriesNode["c:order"] = { a$: { val: "0" } };
|
|
1477
|
+
seriesNode["c:cat"] = {
|
|
1478
|
+
"c:strRef": {
|
|
1479
|
+
"c:f": labelRef,
|
|
1480
|
+
},
|
|
1481
|
+
};
|
|
1482
|
+
seriesNode["c:val"]["c:numRef"]["c:f"] = sizeRef;
|
|
1483
|
+
seriesContainer.push(seriesNode);
|
|
1484
|
+
chartSpace["c:chartSpace"]["c:chart"] = columnChart;
|
|
1485
|
+
}
|
|
1486
|
+
}
|
|
1487
|
+
else {
|
|
1488
|
+
return undefined;
|
|
1489
|
+
}
|
|
1490
|
+
// Apply Phase 5.1 formatting (legend, data labels, axis options) to the
|
|
1491
|
+
// assembled chart. These mutate the chart node in place — they're
|
|
1492
|
+
// chartType-agnostic and intentionally run last so per-type branches
|
|
1493
|
+
// don't have to know about them.
|
|
1494
|
+
applyChartFormatting(chartSpace, chart.spec, sheetNameById);
|
|
1495
|
+
// `<c:roundedCorners val="1"/>` lives at the chartSpace root (sibling
|
|
1496
|
+
// of `<c:chart>`). Default per OOXML §21.2.2.183 is `false`; only
|
|
1497
|
+
// emit when the spec opted in so files imported without the element
|
|
1498
|
+
// round-trip cleanly without gaining a spurious one.
|
|
1499
|
+
if (chart.spec.roundedCorners) {
|
|
1500
|
+
chartSpace["c:chartSpace"]["c:roundedCorners"] = { a$: { val: "1" } };
|
|
1501
|
+
}
|
|
1502
|
+
const xml = chartBuilder.build(chartSpace);
|
|
1503
|
+
return `${XML_DECLARATION}${xml}`;
|
|
1504
|
+
};
|
|
1505
|
+
// -------------------------- Phase 5 helpers --------------------------
|
|
1506
|
+
const LEGEND_POS = {
|
|
1507
|
+
top: "t",
|
|
1508
|
+
bottom: "b",
|
|
1509
|
+
left: "l",
|
|
1510
|
+
right: "r",
|
|
1511
|
+
topRight: "tr",
|
|
1512
|
+
};
|
|
1513
|
+
const DLBL_POS = {
|
|
1514
|
+
center: "ctr",
|
|
1515
|
+
inside: "inBase",
|
|
1516
|
+
outside: "outEnd",
|
|
1517
|
+
insideEnd: "inEnd",
|
|
1518
|
+
outsideEnd: "outEnd",
|
|
1519
|
+
bestFit: "bestFit",
|
|
1520
|
+
};
|
|
1521
|
+
const applyChartFormatting = (chartSpace, spec, sheetNameById) => {
|
|
1522
|
+
const chartNode = chartSpace?.["c:chartSpace"]?.["c:chart"];
|
|
1523
|
+
if (!chartNode)
|
|
1524
|
+
return;
|
|
1525
|
+
applyLegend(chartNode, spec?.legend);
|
|
1526
|
+
const plotArea = chartNode["c:plotArea"];
|
|
1527
|
+
if (!plotArea)
|
|
1528
|
+
return;
|
|
1529
|
+
applyDataLabels(plotArea, spec?.dataLabels);
|
|
1530
|
+
applyAxisOptions(plotArea, spec?.valueAxisOptions, "value");
|
|
1531
|
+
applyAxisOptions(plotArea, spec?.categoryAxisOptions, "category");
|
|
1532
|
+
applyDoughnutHole(plotArea, spec?.doughnutHoleSize);
|
|
1533
|
+
applySeriesOverlays(plotArea, spec?.series, sheetNameById);
|
|
1534
|
+
applySecondaryAxis(plotArea, spec?.secondaryValueAxisOptions);
|
|
1535
|
+
};
|
|
1536
|
+
const SERIES_CONTAINER_KEYS = [
|
|
1537
|
+
"c:barChart",
|
|
1538
|
+
"c:lineChart",
|
|
1539
|
+
"c:pieChart",
|
|
1540
|
+
"c:doughnutChart",
|
|
1541
|
+
"c:areaChart",
|
|
1542
|
+
"c:scatterChart",
|
|
1543
|
+
"c:bubbleChart",
|
|
1544
|
+
];
|
|
1545
|
+
/**
|
|
1546
|
+
* Walk the per-chart-type containers in plotArea and apply trendlines
|
|
1547
|
+
* / errBars to each emitted `<c:ser>`. The series order in spec.series
|
|
1548
|
+
* matches the order emitted by the builder, so we index by position.
|
|
1549
|
+
*/
|
|
1550
|
+
const applySeriesOverlays = (plotArea, series, sheetNameById) => {
|
|
1551
|
+
if (!Array.isArray(series) || !series.length)
|
|
1552
|
+
return;
|
|
1553
|
+
let seriesIndex = 0;
|
|
1554
|
+
for (const key of SERIES_CONTAINER_KEYS) {
|
|
1555
|
+
const container = plotArea[key];
|
|
1556
|
+
if (!container)
|
|
1557
|
+
continue;
|
|
1558
|
+
const ser = container["c:ser"];
|
|
1559
|
+
if (!ser)
|
|
1560
|
+
continue;
|
|
1561
|
+
const list = Array.isArray(ser) ? ser : [ser];
|
|
1562
|
+
for (const node of list) {
|
|
1563
|
+
const data = series[seriesIndex++];
|
|
1564
|
+
if (!data)
|
|
1565
|
+
continue;
|
|
1566
|
+
if (Array.isArray(data.trendlines) && data.trendlines.length) {
|
|
1567
|
+
const built = data.trendlines
|
|
1568
|
+
.map(buildTrendlineNode)
|
|
1569
|
+
.filter(Boolean);
|
|
1570
|
+
// OOXML allows multiple <c:trendline> on a series; preserve the
|
|
1571
|
+
// order from the spec.
|
|
1572
|
+
node["c:trendline"] = built.length === 1 ? built[0] : built;
|
|
1573
|
+
}
|
|
1574
|
+
if (data.errorBars) {
|
|
1575
|
+
const built = buildErrorBarsNode(data.errorBars, sheetNameById);
|
|
1576
|
+
if (built)
|
|
1577
|
+
node["c:errBars"] = built;
|
|
1578
|
+
}
|
|
1579
|
+
}
|
|
1580
|
+
}
|
|
1581
|
+
};
|
|
1582
|
+
const buildTrendlineNode = (t) => {
|
|
1583
|
+
if (!t)
|
|
1584
|
+
return undefined;
|
|
1585
|
+
const node = {};
|
|
1586
|
+
if (t.name) {
|
|
1587
|
+
node["c:name"] = { t$: String(t.name) };
|
|
1588
|
+
}
|
|
1589
|
+
// <c:trendlineType val="linear|exp|log|poly|power|movingAvg"/>
|
|
1590
|
+
node["c:trendlineType"] = {
|
|
1591
|
+
a$: { val: t.type ?? "linear" },
|
|
1592
|
+
};
|
|
1593
|
+
if (t.type === "poly" && typeof t.order === "number") {
|
|
1594
|
+
node["c:order"] = { a$: { val: String(t.order) } };
|
|
1595
|
+
}
|
|
1596
|
+
if (t.type === "movingAvg" && typeof t.period === "number") {
|
|
1597
|
+
node["c:period"] = { a$: { val: String(t.period) } };
|
|
1598
|
+
}
|
|
1599
|
+
if (typeof t.forecastForward === "number") {
|
|
1600
|
+
node["c:forward"] = { a$: { val: String(t.forecastForward) } };
|
|
1601
|
+
}
|
|
1602
|
+
if (typeof t.forecastBackward === "number") {
|
|
1603
|
+
node["c:backward"] = { a$: { val: String(t.forecastBackward) } };
|
|
1604
|
+
}
|
|
1605
|
+
if (t.showEquation) {
|
|
1606
|
+
node["c:dispEq"] = { a$: { val: "1" } };
|
|
1607
|
+
}
|
|
1608
|
+
if (t.showRSquared) {
|
|
1609
|
+
node["c:dispRSqr"] = { a$: { val: "1" } };
|
|
1610
|
+
}
|
|
1611
|
+
return node;
|
|
1612
|
+
};
|
|
1613
|
+
const buildErrorBarsNode = (eb, sheetNameById) => {
|
|
1614
|
+
if (!eb)
|
|
1615
|
+
return undefined;
|
|
1616
|
+
const node = {};
|
|
1617
|
+
if (eb.direction === "x" || eb.direction === "y") {
|
|
1618
|
+
node["c:errDir"] = { a$: { val: eb.direction } };
|
|
1619
|
+
}
|
|
1620
|
+
node["c:errBarType"] = { a$: { val: eb.barType ?? "both" } };
|
|
1621
|
+
const valueType = eb.valueType === "custom" ? "cust" : eb.valueType;
|
|
1622
|
+
node["c:errValType"] = { a$: { val: valueType ?? "fixedVal" } };
|
|
1623
|
+
if (eb.noEndCap) {
|
|
1624
|
+
node["c:noEndCap"] = { a$: { val: "1" } };
|
|
1625
|
+
}
|
|
1626
|
+
// Custom error bars — per-point magnitudes come from worksheet
|
|
1627
|
+
// ranges emitted as `<c:plus>/<c:minus>` numRefs. Schema order puts
|
|
1628
|
+
// them after noEndCap and before `<c:val>`.
|
|
1629
|
+
if (valueType === "cust") {
|
|
1630
|
+
const plusRef = formatRangeRef(eb.plusRange ?? undefined, sheetNameById);
|
|
1631
|
+
const minusRef = formatRangeRef(eb.minusRange ?? undefined, sheetNameById);
|
|
1632
|
+
if (plusRef) {
|
|
1633
|
+
node["c:plus"] = { "c:numRef": { "c:f": plusRef } };
|
|
1634
|
+
}
|
|
1635
|
+
if (minusRef) {
|
|
1636
|
+
node["c:minus"] = { "c:numRef": { "c:f": minusRef } };
|
|
1637
|
+
}
|
|
1638
|
+
}
|
|
1639
|
+
if (typeof eb.value === "number") {
|
|
1640
|
+
node["c:val"] = { a$: { val: String(eb.value) } };
|
|
1641
|
+
}
|
|
1642
|
+
return node;
|
|
1643
|
+
};
|
|
1644
|
+
/**
|
|
1645
|
+
* Add a secondary `<c:valAx>` next to the primary one when the spec
|
|
1646
|
+
* carries `secondaryValueAxisOptions`. We give it a distinct axId
|
|
1647
|
+
* (existing primary axId + 100) — series-level routing to the new axis
|
|
1648
|
+
* is intentionally not wired up here (most consumers of "secondary
|
|
1649
|
+
* axis" only care about the data round-trip; full charting fidelity
|
|
1650
|
+
* lands in a follow-up).
|
|
1651
|
+
*/
|
|
1652
|
+
const applySecondaryAxis = (plotArea, opts) => {
|
|
1653
|
+
if (!opts)
|
|
1654
|
+
return;
|
|
1655
|
+
const existing = plotArea["c:valAx"];
|
|
1656
|
+
if (!existing)
|
|
1657
|
+
return;
|
|
1658
|
+
const existingArr = Array.isArray(existing) ? existing : [existing];
|
|
1659
|
+
if (existingArr.length >= 2)
|
|
1660
|
+
return; // already has secondary
|
|
1661
|
+
const primary = existingArr[0];
|
|
1662
|
+
const primaryAxId = parseInt((primary?.["c:axId"]?.a$?.val ?? "1000"), 10);
|
|
1663
|
+
const secondary = {
|
|
1664
|
+
"c:axId": { a$: { val: String(primaryAxId + 100) } },
|
|
1665
|
+
"c:scaling": { "c:orientation": { a$: { val: "minMax" } } },
|
|
1666
|
+
"c:delete": { a$: { val: "0" } },
|
|
1667
|
+
"c:axPos": { a$: { val: "r" } },
|
|
1668
|
+
"c:crossAx": { a$: { val: String(primaryAxId) } },
|
|
1669
|
+
};
|
|
1670
|
+
// Apply the same logarithmic / reverse / min / max / gridlines etc.
|
|
1671
|
+
applyAxisOptions({ "c:valAx": secondary }, opts, "value");
|
|
1672
|
+
plotArea["c:valAx"] = [...existingArr, secondary];
|
|
1673
|
+
};
|
|
1674
|
+
const applyLegend = (chartNode, legend) => {
|
|
1675
|
+
if (!legend)
|
|
1676
|
+
return;
|
|
1677
|
+
if (legend.position === "none") {
|
|
1678
|
+
// Excel omits <c:legend> entirely when hidden; some readers expect
|
|
1679
|
+
// an explicit <c:legend><c:delete val="1"/> instead. Use the
|
|
1680
|
+
// explicit form so the suppression survives ANY reader.
|
|
1681
|
+
chartNode["c:legend"] = {
|
|
1682
|
+
"c:delete": { a$: { val: "1" } },
|
|
1683
|
+
};
|
|
1684
|
+
return;
|
|
1685
|
+
}
|
|
1686
|
+
const posCode = LEGEND_POS[legend.position];
|
|
1687
|
+
const txPr = buildTxPrNode(legend.textFormat);
|
|
1688
|
+
if (!posCode) {
|
|
1689
|
+
// No explicit position — just ensure the legend element exists,
|
|
1690
|
+
// attach text formatting if authored.
|
|
1691
|
+
const existing = chartNode["c:legend"] ?? {};
|
|
1692
|
+
if (txPr)
|
|
1693
|
+
existing["c:txPr"] = txPr;
|
|
1694
|
+
chartNode["c:legend"] = existing;
|
|
1695
|
+
return;
|
|
1696
|
+
}
|
|
1697
|
+
chartNode["c:legend"] = {
|
|
1698
|
+
"c:legendPos": { a$: { val: posCode } },
|
|
1699
|
+
"c:overlay": { a$: { val: "0" } },
|
|
1700
|
+
...(txPr ? { "c:txPr": txPr } : {}),
|
|
1701
|
+
};
|
|
1702
|
+
};
|
|
1703
|
+
const applyDataLabels = (plotArea, dataLabels) => {
|
|
1704
|
+
if (!dataLabels?.show)
|
|
1705
|
+
return;
|
|
1706
|
+
// Find the chart-type container inside <c:plotArea>. We attach <c:dLbls>
|
|
1707
|
+
// to whichever exists.
|
|
1708
|
+
const containerKey = [
|
|
1709
|
+
"c:barChart",
|
|
1710
|
+
"c:lineChart",
|
|
1711
|
+
"c:pieChart",
|
|
1712
|
+
"c:doughnutChart",
|
|
1713
|
+
"c:areaChart",
|
|
1714
|
+
].find((k) => plotArea[k]);
|
|
1715
|
+
if (!containerKey)
|
|
1716
|
+
return;
|
|
1717
|
+
const container = plotArea[containerKey];
|
|
1718
|
+
const posCode = dataLabels.position && DLBL_POS[dataLabels.position];
|
|
1719
|
+
const dLbls = {};
|
|
1720
|
+
if (posCode)
|
|
1721
|
+
dLbls["c:dLblPos"] = { a$: { val: posCode } };
|
|
1722
|
+
// <c:numFmt> — emit before the show* flags. Excel reads this to
|
|
1723
|
+
// format the label value (e.g. `"$"#,##0` → "$1,831").
|
|
1724
|
+
if (dataLabels.numberFormatPattern) {
|
|
1725
|
+
dLbls["c:numFmt"] = {
|
|
1726
|
+
a$: {
|
|
1727
|
+
formatCode: dataLabels.numberFormatPattern,
|
|
1728
|
+
sourceLinked: "0",
|
|
1729
|
+
},
|
|
1730
|
+
};
|
|
1731
|
+
}
|
|
1732
|
+
// <c:txPr> — authored color / font / bold for the label text.
|
|
1733
|
+
const txPr = buildTxPrNode(dataLabels.textFormat);
|
|
1734
|
+
if (txPr)
|
|
1735
|
+
dLbls["c:txPr"] = txPr;
|
|
1736
|
+
// OOXML requires explicit boolean flags. When none of show* are set,
|
|
1737
|
+
// default to showVal=1 since that's Excel's default for "Show Labels".
|
|
1738
|
+
const showValue = dataLabels.showValue === undefined
|
|
1739
|
+
? !dataLabels.showCategoryName &&
|
|
1740
|
+
!dataLabels.showPercent &&
|
|
1741
|
+
!dataLabels.showSeriesName
|
|
1742
|
+
: !!dataLabels.showValue;
|
|
1743
|
+
dLbls["c:showLegendKey"] = { a$: { val: "0" } };
|
|
1744
|
+
dLbls["c:showVal"] = { a$: { val: showValue ? "1" : "0" } };
|
|
1745
|
+
dLbls["c:showCatName"] = {
|
|
1746
|
+
a$: { val: dataLabels.showCategoryName ? "1" : "0" },
|
|
1747
|
+
};
|
|
1748
|
+
dLbls["c:showSerName"] = {
|
|
1749
|
+
a$: { val: dataLabels.showSeriesName ? "1" : "0" },
|
|
1750
|
+
};
|
|
1751
|
+
dLbls["c:showPercent"] = {
|
|
1752
|
+
a$: { val: dataLabels.showPercent ? "1" : "0" },
|
|
1753
|
+
};
|
|
1754
|
+
dLbls["c:showBubbleSize"] = { a$: { val: "0" } };
|
|
1755
|
+
container["c:dLbls"] = dLbls;
|
|
1756
|
+
};
|
|
1757
|
+
const applyAxisOptions = (plotArea, options, kind) => {
|
|
1758
|
+
if (!options)
|
|
1759
|
+
return;
|
|
1760
|
+
const axisKey = kind === "value" ? "c:valAx" : "c:catAx";
|
|
1761
|
+
const ax = plotArea[axisKey];
|
|
1762
|
+
if (!ax)
|
|
1763
|
+
return;
|
|
1764
|
+
// OOXML can carry multiple axes (e.g. primary + secondary). Target the
|
|
1765
|
+
// first one for now — secondary axes ship in Phase 5.2.
|
|
1766
|
+
const node = Array.isArray(ax) ? ax[0] : ax;
|
|
1767
|
+
const scaling = (node["c:scaling"] = node["c:scaling"] ?? {});
|
|
1768
|
+
if (options.logarithmic) {
|
|
1769
|
+
scaling["c:logBase"] = { a$: { val: "10" } };
|
|
1770
|
+
}
|
|
1771
|
+
if (options.reverse) {
|
|
1772
|
+
scaling["c:orientation"] = { a$: { val: "maxMin" } };
|
|
1773
|
+
}
|
|
1774
|
+
if (typeof options.min === "number") {
|
|
1775
|
+
scaling["c:min"] = { a$: { val: String(options.min) } };
|
|
1776
|
+
}
|
|
1777
|
+
if (typeof options.max === "number") {
|
|
1778
|
+
scaling["c:max"] = { a$: { val: String(options.max) } };
|
|
1779
|
+
}
|
|
1780
|
+
if (options.visible === false) {
|
|
1781
|
+
node["c:delete"] = { a$: { val: "1" } };
|
|
1782
|
+
}
|
|
1783
|
+
if (options.showMajorGridlines === true && !node["c:majorGridlines"]) {
|
|
1784
|
+
node["c:majorGridlines"] = {};
|
|
1785
|
+
}
|
|
1786
|
+
if (options.showMajorGridlines === false && node["c:majorGridlines"]) {
|
|
1787
|
+
delete node["c:majorGridlines"];
|
|
1788
|
+
}
|
|
1789
|
+
// <c:txPr> — axis tick-label color/font. Without this, exported
|
|
1790
|
+
// axes would render with the default theme color instead of the
|
|
1791
|
+
// authored one (BEP.xlsx axis colors #14325A / #F0621C).
|
|
1792
|
+
const txPr = buildTxPrNode(options.textFormat);
|
|
1793
|
+
if (txPr)
|
|
1794
|
+
node["c:txPr"] = txPr;
|
|
1795
|
+
};
|
|
1796
|
+
const applyDoughnutHole = (plotArea, holeSize) => {
|
|
1797
|
+
if (typeof holeSize !== "number")
|
|
1798
|
+
return;
|
|
1799
|
+
const doughnut = plotArea["c:doughnutChart"];
|
|
1800
|
+
if (!doughnut)
|
|
1801
|
+
return;
|
|
1802
|
+
// Convert 0-1 fraction back to OOXML's 0-90 percentage.
|
|
1803
|
+
const pct = Math.round(Math.max(0, Math.min(0.9, holeSize)) * 100);
|
|
1804
|
+
doughnut["c:holeSize"] = { a$: { val: String(pct) } };
|
|
1805
|
+
};
|