@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,2036 @@
|
|
|
1
|
+
import { generateSelectionsFromFormula } from "@rowsncolumns/utils";
|
|
2
|
+
import { uuid } from "@rowsncolumns/utils";
|
|
3
|
+
import { get } from "lodash-es";
|
|
4
|
+
/**
|
|
5
|
+
* Pick the data sheetId for a parsed selection. Honours the explicit
|
|
6
|
+
* sheet prefix in the chart's formula reference (cross-sheet charts);
|
|
7
|
+
* falls back to the host sheet when the formula has no prefix.
|
|
8
|
+
*/
|
|
9
|
+
function pickSheetId(sheetName, hostSheetId, resolveSheetId) {
|
|
10
|
+
if (sheetName && resolveSheetId) {
|
|
11
|
+
const resolved = resolveSheetId(sheetName);
|
|
12
|
+
if (resolved !== undefined)
|
|
13
|
+
return resolved;
|
|
14
|
+
}
|
|
15
|
+
return hostSheetId;
|
|
16
|
+
}
|
|
17
|
+
// Import from index.ts in the same directory (circular dependency but resolving symbols)
|
|
18
|
+
// Note: We need to use type import for Workbook if needed, or functions
|
|
19
|
+
// In this case, we need getChartReferenceFromAnchor which was exported from workbook.ts originally
|
|
20
|
+
// but now is in index.ts of the plugin. However, charts.ts doesn't seem to import it based on the file content I read.
|
|
21
|
+
// The search_replace failed because the import wasn't there.
|
|
22
|
+
// Let me double check the file content again.
|
|
23
|
+
// The file content I read has NO imports from "../charts" or "../workbook".
|
|
24
|
+
// It seems I was trying to fix a linter error that I *assumed* existed or was trying to match code that wasn't there.
|
|
25
|
+
// The file content shows standard imports and internal functions.
|
|
26
|
+
function getWithFallback(source, paths) {
|
|
27
|
+
for (const path of paths) {
|
|
28
|
+
const value = get(source, path);
|
|
29
|
+
if (value !== undefined) {
|
|
30
|
+
return value;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return undefined;
|
|
34
|
+
}
|
|
35
|
+
function toInt(value) {
|
|
36
|
+
const parsed = parseInt(typeof value === "number" ? String(value) : value ?? "0", 10);
|
|
37
|
+
return Number.isNaN(parsed) ? 0 : parsed;
|
|
38
|
+
}
|
|
39
|
+
export async function processChart(chartTarget, drawingPath, anchor, sheetId, zipParser, xmlParser, sheetDimensions, resolveSheetId) {
|
|
40
|
+
try {
|
|
41
|
+
// Resolve chart file path
|
|
42
|
+
let chartPath = chartTarget;
|
|
43
|
+
const drawingDir = drawingPath.substring(0, drawingPath.lastIndexOf("/"));
|
|
44
|
+
if (chartPath.startsWith("../")) {
|
|
45
|
+
chartPath = `xl/${chartPath.replace("../", "")}`;
|
|
46
|
+
}
|
|
47
|
+
else if (!chartPath.startsWith("/") && !chartPath.startsWith("xl/")) {
|
|
48
|
+
chartPath = `${drawingDir}/${chartPath}`;
|
|
49
|
+
}
|
|
50
|
+
// Read the chart file
|
|
51
|
+
const chartContent = await zipParser.getText(chartPath);
|
|
52
|
+
if (!chartContent) {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
const chartXml = xmlParser.parse(chartContent);
|
|
56
|
+
const chart = getWithFallback(chartXml, [
|
|
57
|
+
"c:chartSpace.c:chart",
|
|
58
|
+
"chartSpace.chart",
|
|
59
|
+
"chartSpace.c:chart",
|
|
60
|
+
"c:chartSpace.chart",
|
|
61
|
+
]);
|
|
62
|
+
if (!chart) {
|
|
63
|
+
console.log("Chart data not found in file:", chartPath);
|
|
64
|
+
console.log("Available keys:", Object.keys(chartXml));
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
// `<c:roundedCorners val="1"/>` is a sibling of `<c:chart>` at the
|
|
68
|
+
// chartSpace root, not inside `<c:chart>` itself. Default per
|
|
69
|
+
// OOXML §21.2.2.183 is `false`. Read both namespace-prefixed and
|
|
70
|
+
// bare forms — fast-xml-parser preserves the namespace when the
|
|
71
|
+
// schema declares it.
|
|
72
|
+
const chartSpaceRoot = getWithFallback(chartXml, [
|
|
73
|
+
"c:chartSpace",
|
|
74
|
+
"chartSpace",
|
|
75
|
+
]);
|
|
76
|
+
const roundedCornersRaw = getWithFallback(chartSpaceRoot, [
|
|
77
|
+
"c:roundedCorners",
|
|
78
|
+
"roundedCorners",
|
|
79
|
+
]);
|
|
80
|
+
const roundedCornersVal = roundedCornersRaw?.a$?.val;
|
|
81
|
+
const roundedCorners = roundedCornersVal === "1" ||
|
|
82
|
+
roundedCornersVal === "true" ||
|
|
83
|
+
roundedCornersVal === true
|
|
84
|
+
? true
|
|
85
|
+
: undefined;
|
|
86
|
+
const chartId = uuid();
|
|
87
|
+
// Extract chart title (text + formatting)
|
|
88
|
+
const title = extractChartTitle(chart);
|
|
89
|
+
const titleTextFormat = extractChartTitleTextFormat(chart);
|
|
90
|
+
// Determine chart type
|
|
91
|
+
const chartType = determineChartType(chart);
|
|
92
|
+
// Extract data
|
|
93
|
+
const plotArea = getWithFallback(chart, ["c:plotArea", "plotArea"]);
|
|
94
|
+
const series = extractChartSeries(plotArea, chartType, sheetId, resolveSheetId);
|
|
95
|
+
const domains = extractChartDomains(plotArea, chartType, sheetId, resolveSheetId);
|
|
96
|
+
const dataRange = extractChartDataRange(plotArea, chartType, sheetId, resolveSheetId);
|
|
97
|
+
// Scatter/bubble only — bubble sizes (3rd dimension) and per-point
|
|
98
|
+
// labels. Bubble charts also have a `bubble3D` flag that scatter
|
|
99
|
+
// doesn't carry.
|
|
100
|
+
const bubbleSizes = chartType === "bubble"
|
|
101
|
+
? extractChartBubbleSizes(plotArea, sheetId, resolveSheetId)
|
|
102
|
+
: undefined;
|
|
103
|
+
// Scatter only — `<c:scatterStyle val="lineMarker">` etc. controls
|
|
104
|
+
// whether points are connected and whether markers are drawn.
|
|
105
|
+
// Excel's default when omitted is "lineMarker".
|
|
106
|
+
const scatterStyle = chartType === "scatter" ? extractScatterStyle(plotArea) : undefined;
|
|
107
|
+
// Extract hidden dimension strategy from plotVisOnly
|
|
108
|
+
const hiddenDimensionStrategy = extractHiddenDimensionStrategy(chart);
|
|
109
|
+
// Extract position from anchor
|
|
110
|
+
const position = extractChartPosition(anchor, sheetId, sheetDimensions);
|
|
111
|
+
if (!position) {
|
|
112
|
+
console.log("Could not extract chart position");
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
const legend = extractLegendOptions(chart);
|
|
116
|
+
const dataLabels = extractDataLabelsOptions(plotArea, chartType);
|
|
117
|
+
const { valueAxisOptions, secondaryValueAxisOptions, categoryAxisOptions, } = extractAxisOptions(plotArea);
|
|
118
|
+
const doughnutHoleSize = chartType === "doughnut" ? extractDoughnutHoleSize(plotArea) : undefined;
|
|
119
|
+
// Pie-of-pie / bar-of-pie — <c:ofPieChart> degrades to chartType
|
|
120
|
+
// "pie" with sourceChartType "OF_PIE" plus the split fields the
|
|
121
|
+
// renderer uses to draw the composite layout.
|
|
122
|
+
const ofPieOptions = chartType === "pie" ? extractOfPieOptions(plotArea) : undefined;
|
|
123
|
+
const radarStyle = chartType === "radar" ? extractRadarStyle(plotArea) : undefined;
|
|
124
|
+
const binCount = chartType === "histogram"
|
|
125
|
+
? extractC15HistogramBinCount(plotArea)
|
|
126
|
+
: undefined;
|
|
127
|
+
// Bar/column-only — <c:grouping>, <c:overlap>, <c:gapWidth>.
|
|
128
|
+
// Excel encodes percentStacked / stacked / clustered here, and the
|
|
129
|
+
// overlap/gapWidth shape the bar layout. Without these, the
|
|
130
|
+
// Event Budget percent-stacked chart renders as clustered.
|
|
131
|
+
const barLayout = chartType === "bar" || chartType === "column"
|
|
132
|
+
? extractBarLayoutOptions(plotArea)
|
|
133
|
+
: undefined;
|
|
134
|
+
return {
|
|
135
|
+
chartId,
|
|
136
|
+
spec: {
|
|
137
|
+
title,
|
|
138
|
+
...(titleTextFormat ? { titleTextFormat } : {}),
|
|
139
|
+
chartType,
|
|
140
|
+
domains,
|
|
141
|
+
series,
|
|
142
|
+
dataRange,
|
|
143
|
+
hiddenDimensionStrategy,
|
|
144
|
+
// Scatter/bubble must conform to BubbleChartSpec which requires
|
|
145
|
+
// `bubbleLabels`. We don't currently extract per-point labels
|
|
146
|
+
// from OOXML (rare in real files); set an empty array so the
|
|
147
|
+
// renderer's `flattenChartDataValues(spec.bubbleLabels, …)`
|
|
148
|
+
// call gets a well-formed input.
|
|
149
|
+
...(chartType === "scatter" || chartType === "bubble"
|
|
150
|
+
? { bubbleLabels: [] }
|
|
151
|
+
: {}),
|
|
152
|
+
...(bubbleSizes && bubbleSizes.length ? { bubbleSizes } : {}),
|
|
153
|
+
...(scatterStyle
|
|
154
|
+
? { scatterStyle: scatterStyle }
|
|
155
|
+
: {}),
|
|
156
|
+
...(legend ? { legend } : {}),
|
|
157
|
+
...(dataLabels ? { dataLabels } : {}),
|
|
158
|
+
...(valueAxisOptions ? { valueAxisOptions } : {}),
|
|
159
|
+
...(secondaryValueAxisOptions
|
|
160
|
+
? { secondaryValueAxisOptions }
|
|
161
|
+
: {}),
|
|
162
|
+
...(categoryAxisOptions ? { categoryAxisOptions } : {}),
|
|
163
|
+
...(typeof doughnutHoleSize === "number"
|
|
164
|
+
? { doughnutHoleSize }
|
|
165
|
+
: {}),
|
|
166
|
+
...(ofPieOptions
|
|
167
|
+
? { sourceChartType: "OF_PIE", ...ofPieOptions }
|
|
168
|
+
: {}),
|
|
169
|
+
...(radarStyle ? { radarStyle } : {}),
|
|
170
|
+
...(typeof binCount === "number" ? { binCount } : {}),
|
|
171
|
+
...(barLayout?.stackedType
|
|
172
|
+
? { stackedType: barLayout.stackedType }
|
|
173
|
+
: {}),
|
|
174
|
+
...(typeof barLayout?.overlap === "number"
|
|
175
|
+
? { overlap: barLayout.overlap }
|
|
176
|
+
: {}),
|
|
177
|
+
...(typeof barLayout?.gapWidth === "number"
|
|
178
|
+
? { gapWidth: barLayout.gapWidth }
|
|
179
|
+
: {}),
|
|
180
|
+
...(roundedCorners ? { roundedCorners } : {}),
|
|
181
|
+
},
|
|
182
|
+
position,
|
|
183
|
+
id: chartId,
|
|
184
|
+
type: "chart",
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
catch (err) {
|
|
188
|
+
console.log("Error processing chart:", err);
|
|
189
|
+
return null;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
// Helper to get series elements with namespace fallbacks
|
|
193
|
+
function getSeriesElements(plotArea, chartType) {
|
|
194
|
+
switch (chartType) {
|
|
195
|
+
case "bar":
|
|
196
|
+
case "column":
|
|
197
|
+
// Phase 5.5 — c15 extension chart types emit a c:barChart body
|
|
198
|
+
// for older-Excel compatibility, so their series live in the same
|
|
199
|
+
// place as a bar/column chart. Phase 5.5b — histogram additionally
|
|
200
|
+
// wraps the barChart in mc:AlternateContent > mc:Fallback, so we
|
|
201
|
+
// also probe that path.
|
|
202
|
+
case "histogram":
|
|
203
|
+
case "boxplot":
|
|
204
|
+
case "waterfall":
|
|
205
|
+
case "funnel":
|
|
206
|
+
return getWithFallback(plotArea, [
|
|
207
|
+
"c:barChart.c:ser",
|
|
208
|
+
"barChart.ser",
|
|
209
|
+
"c:bar3DChart.c:ser",
|
|
210
|
+
"bar3DChart.ser",
|
|
211
|
+
"mc:AlternateContent.mc:Fallback.c:barChart.c:ser",
|
|
212
|
+
"AlternateContent.Fallback.barChart.ser",
|
|
213
|
+
]);
|
|
214
|
+
case "line":
|
|
215
|
+
return getWithFallback(plotArea, [
|
|
216
|
+
"c:lineChart.c:ser",
|
|
217
|
+
"lineChart.ser",
|
|
218
|
+
"c:line3DChart.c:ser",
|
|
219
|
+
"line3DChart.ser",
|
|
220
|
+
]);
|
|
221
|
+
case "pie":
|
|
222
|
+
return getWithFallback(plotArea, [
|
|
223
|
+
"c:pieChart.c:ser",
|
|
224
|
+
"pieChart.ser",
|
|
225
|
+
"c:pie3DChart.c:ser",
|
|
226
|
+
"pie3DChart.ser",
|
|
227
|
+
// pie-of-pie / bar-of-pie — determineChartType maps
|
|
228
|
+
// <c:ofPieChart> to "pie", so its series live here too.
|
|
229
|
+
"c:ofPieChart.c:ser",
|
|
230
|
+
"ofPieChart.ser",
|
|
231
|
+
]);
|
|
232
|
+
case "doughnut":
|
|
233
|
+
return getWithFallback(plotArea, [
|
|
234
|
+
"c:doughnutChart.c:ser",
|
|
235
|
+
"doughnutChart.ser",
|
|
236
|
+
]);
|
|
237
|
+
case "area":
|
|
238
|
+
return getWithFallback(plotArea, [
|
|
239
|
+
"c:areaChart.c:ser",
|
|
240
|
+
"areaChart.ser",
|
|
241
|
+
"c:area3DChart.c:ser",
|
|
242
|
+
"area3DChart.ser",
|
|
243
|
+
]);
|
|
244
|
+
case "radar":
|
|
245
|
+
return getWithFallback(plotArea, ["c:radarChart.c:ser", "radarChart.ser"]);
|
|
246
|
+
case "stock":
|
|
247
|
+
return getWithFallback(plotArea, ["c:stockChart.c:ser", "stockChart.ser"]);
|
|
248
|
+
case "scatter":
|
|
249
|
+
return getWithFallback(plotArea, [
|
|
250
|
+
"c:scatterChart.c:ser",
|
|
251
|
+
"scatterChart.ser",
|
|
252
|
+
]);
|
|
253
|
+
case "bubble":
|
|
254
|
+
return getWithFallback(plotArea, [
|
|
255
|
+
"c:bubbleChart.c:ser",
|
|
256
|
+
"bubbleChart.ser",
|
|
257
|
+
]);
|
|
258
|
+
default:
|
|
259
|
+
return getWithFallback(plotArea, ["c:barChart.c:ser", "barChart.ser"]);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
// Path tuples used by the per-series ref extractors below. We accept
|
|
263
|
+
// both namespaced and stripped forms because fast-xml-parser is
|
|
264
|
+
// configured both ways in different code paths.
|
|
265
|
+
const X_VAL_REF_PATHS = [
|
|
266
|
+
"c:xVal.c:numRef.c:f",
|
|
267
|
+
"c:xVal.c:numRef.c:f.t$",
|
|
268
|
+
"xVal.numRef.f",
|
|
269
|
+
"xVal.numRef.f.t$",
|
|
270
|
+
// scatter X axis can also be string (categorical X)
|
|
271
|
+
"c:xVal.c:strRef.c:f",
|
|
272
|
+
"c:xVal.c:strRef.c:f.t$",
|
|
273
|
+
"xVal.strRef.f",
|
|
274
|
+
"xVal.strRef.f.t$",
|
|
275
|
+
];
|
|
276
|
+
const Y_VAL_REF_PATHS = [
|
|
277
|
+
"c:yVal.c:numRef.c:f",
|
|
278
|
+
"c:yVal.c:numRef.c:f.t$",
|
|
279
|
+
"yVal.numRef.f",
|
|
280
|
+
"yVal.numRef.f.t$",
|
|
281
|
+
];
|
|
282
|
+
const BUBBLE_SIZE_REF_PATHS = [
|
|
283
|
+
"c:bubbleSize.c:numRef.c:f",
|
|
284
|
+
"c:bubbleSize.c:numRef.c:f.t$",
|
|
285
|
+
"bubbleSize.numRef.f",
|
|
286
|
+
"bubbleSize.numRef.f.t$",
|
|
287
|
+
];
|
|
288
|
+
const CAT_REF_PATHS = [
|
|
289
|
+
"c:cat.c:strRef.c:f",
|
|
290
|
+
"c:cat.c:numRef.c:f",
|
|
291
|
+
"c:cat.c:strRef.c:f.t$",
|
|
292
|
+
"c:cat.c:numRef.c:f.t$",
|
|
293
|
+
"cat.strRef.f",
|
|
294
|
+
"cat.numRef.f",
|
|
295
|
+
"cat.strRef.f.t$",
|
|
296
|
+
"cat.numRef.f.t$",
|
|
297
|
+
];
|
|
298
|
+
const VAL_REF_PATHS = [
|
|
299
|
+
"c:val.c:numRef.c:f",
|
|
300
|
+
"c:val.c:numRef.c:f.t$",
|
|
301
|
+
"val.numRef.f",
|
|
302
|
+
"val.numRef.f.t$",
|
|
303
|
+
];
|
|
304
|
+
const TX_REF_PATHS = [
|
|
305
|
+
"c:tx.c:strRef.c:f",
|
|
306
|
+
"c:tx.c:strRef.c:f.t$",
|
|
307
|
+
"tx.strRef.f",
|
|
308
|
+
"tx.strRef.f.t$",
|
|
309
|
+
];
|
|
310
|
+
const isXYChart = (chartType) => chartType === "scatter" || chartType === "bubble";
|
|
311
|
+
// Extract the overall data range for the chart
|
|
312
|
+
export function extractChartDataRange(plotArea, chartType, sheetId, resolveSheetId) {
|
|
313
|
+
try {
|
|
314
|
+
const seriesElements = getSeriesElements(plotArea, chartType);
|
|
315
|
+
if (!seriesElements)
|
|
316
|
+
return null;
|
|
317
|
+
const seriesArray = Array.isArray(seriesElements)
|
|
318
|
+
? seriesElements
|
|
319
|
+
: [seriesElements];
|
|
320
|
+
// Collect all ranges from the chart, tagged with their parsed
|
|
321
|
+
// sheet name so cross-sheet refs can be honoured.
|
|
322
|
+
const allRanges = [];
|
|
323
|
+
for (const ser of seriesArray) {
|
|
324
|
+
if (isXYChart(chartType)) {
|
|
325
|
+
const xRef = getWithFallback(ser, X_VAL_REF_PATHS);
|
|
326
|
+
const yRef = getWithFallback(ser, Y_VAL_REF_PATHS);
|
|
327
|
+
const sizeRef = chartType === "bubble"
|
|
328
|
+
? getWithFallback(ser, BUBBLE_SIZE_REF_PATHS)
|
|
329
|
+
: undefined;
|
|
330
|
+
const nameRef = getWithFallback(ser, TX_REF_PATHS);
|
|
331
|
+
if (xRef)
|
|
332
|
+
allRanges.push(xRef);
|
|
333
|
+
if (yRef)
|
|
334
|
+
allRanges.push(yRef);
|
|
335
|
+
if (sizeRef)
|
|
336
|
+
allRanges.push(sizeRef);
|
|
337
|
+
if (nameRef)
|
|
338
|
+
allRanges.push(nameRef);
|
|
339
|
+
}
|
|
340
|
+
else {
|
|
341
|
+
const catRef = getWithFallback(ser, CAT_REF_PATHS);
|
|
342
|
+
const valRef = getWithFallback(ser, VAL_REF_PATHS);
|
|
343
|
+
const nameRef = getWithFallback(ser, TX_REF_PATHS);
|
|
344
|
+
if (catRef)
|
|
345
|
+
allRanges.push(catRef);
|
|
346
|
+
if (valRef)
|
|
347
|
+
allRanges.push(valRef);
|
|
348
|
+
if (nameRef)
|
|
349
|
+
allRanges.push(nameRef);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
if (allRanges.length === 0)
|
|
353
|
+
return null;
|
|
354
|
+
// Bounding range is computed per-sheet — different ranges may live
|
|
355
|
+
// on different sheets (a chart referencing both the host sheet and
|
|
356
|
+
// a data sheet, for example). Per Excel, the chart's "data range"
|
|
357
|
+
// is logically the union of inputs; we pick the first sheet's
|
|
358
|
+
// bounding box as the reportable dataRange and pin the rest via
|
|
359
|
+
// the per-series sources.
|
|
360
|
+
const boundingRange = calculateBoundingRange(allRanges);
|
|
361
|
+
if (boundingRange) {
|
|
362
|
+
const selections = generateSelectionsFromFormula(`=${boundingRange}`);
|
|
363
|
+
if (selections.length > 0) {
|
|
364
|
+
const sel = selections[0];
|
|
365
|
+
const resolved = pickSheetId(sel.sheetName, sheetId, resolveSheetId);
|
|
366
|
+
return {
|
|
367
|
+
...sel.range,
|
|
368
|
+
sheetId: resolved,
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
return null;
|
|
373
|
+
}
|
|
374
|
+
catch (err) {
|
|
375
|
+
console.log("Error extracting chart data range:", err);
|
|
376
|
+
return null;
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
// Calculate the bounding range that encompasses all the individual ranges
|
|
380
|
+
function calculateBoundingRange(ranges) {
|
|
381
|
+
try {
|
|
382
|
+
let minRow = Infinity;
|
|
383
|
+
let maxRow = -Infinity;
|
|
384
|
+
let minCol = Infinity;
|
|
385
|
+
let maxCol = -Infinity;
|
|
386
|
+
let sheetName = "";
|
|
387
|
+
for (const range of ranges) {
|
|
388
|
+
// Parse each range (e.g., "Sheet1!$A$2:$A$20" or "Sheet1!$C$1")
|
|
389
|
+
const selections = generateSelectionsFromFormula(`=${range}`);
|
|
390
|
+
if (selections.length > 0) {
|
|
391
|
+
const sel = selections[0];
|
|
392
|
+
// Extract sheet name from the first range
|
|
393
|
+
if (!sheetName && range.includes("!")) {
|
|
394
|
+
sheetName = range.split("!")[0];
|
|
395
|
+
}
|
|
396
|
+
// Update bounding box
|
|
397
|
+
minRow = Math.min(minRow, sel.range.startRowIndex);
|
|
398
|
+
maxRow = Math.max(maxRow, sel.range.endRowIndex);
|
|
399
|
+
minCol = Math.min(minCol, sel.range.startColumnIndex);
|
|
400
|
+
maxCol = Math.max(maxCol, sel.range.endColumnIndex);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
if (minRow === Infinity || minCol === Infinity) {
|
|
404
|
+
return null;
|
|
405
|
+
}
|
|
406
|
+
// Convert back to Excel range format
|
|
407
|
+
const startCell = columnIndexToLetter(minCol) + minRow;
|
|
408
|
+
const endCell = columnIndexToLetter(maxCol) + maxRow;
|
|
409
|
+
const rangeStr = startCell === endCell ? startCell : `${startCell}:${endCell}`;
|
|
410
|
+
return sheetName ? `${sheetName}!${rangeStr}` : rangeStr;
|
|
411
|
+
}
|
|
412
|
+
catch (err) {
|
|
413
|
+
console.log("Error calculating bounding range:", err);
|
|
414
|
+
return null;
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
// Helper function to convert column index to letter (1 -> A, 2 -> B, etc.)
|
|
418
|
+
function columnIndexToLetter(index) {
|
|
419
|
+
let result = "";
|
|
420
|
+
let num = index;
|
|
421
|
+
while (num > 0) {
|
|
422
|
+
num--;
|
|
423
|
+
result = String.fromCharCode(65 + (num % 26)) + result;
|
|
424
|
+
num = Math.floor(num / 26);
|
|
425
|
+
}
|
|
426
|
+
return result;
|
|
427
|
+
}
|
|
428
|
+
/**
|
|
429
|
+
* Extract hidden dimension strategy from plotVisOnly attribute
|
|
430
|
+
* In Excel: plotVisOnly="1" means skip hidden rows/columns, "0" means show all
|
|
431
|
+
*/
|
|
432
|
+
export function extractHiddenDimensionStrategy(chart) {
|
|
433
|
+
const plotVisOnly = getWithFallback(chart, [
|
|
434
|
+
"c:plotVisOnly.a$.val",
|
|
435
|
+
"c:plotVisOnly.a$.t$",
|
|
436
|
+
"plotVisOnly.a$.val",
|
|
437
|
+
"plotVisOnly.a$.t$",
|
|
438
|
+
]);
|
|
439
|
+
if (plotVisOnly === undefined) {
|
|
440
|
+
return undefined;
|
|
441
|
+
}
|
|
442
|
+
// "1" or "true" = skip hidden cells, "0" or "false" = show all
|
|
443
|
+
const skipHidden = plotVisOnly === "1" || plotVisOnly === "true" || plotVisOnly === true;
|
|
444
|
+
return skipHidden ? "SKIP_HIDDEN_ROWS_AND_COLUMNS" : "SHOW_ALL";
|
|
445
|
+
}
|
|
446
|
+
/**
|
|
447
|
+
* Read the chart title's text formatting. Excel chart titles can be
|
|
448
|
+
* styled two ways:
|
|
449
|
+
* - via `<c:title><c:txPr>` (paragraph default, affects future
|
|
450
|
+
* content); writers use this when no inline rich text is present.
|
|
451
|
+
* - via `<c:title><c:tx><c:rich>` (the title text itself, with
|
|
452
|
+
* per-paragraph `<a:pPr><a:defRPr>` carrying the styling).
|
|
453
|
+
* We probe both, with the rich-text path winning when present (it's
|
|
454
|
+
* the more explicit signal). Returns `undefined` when neither carries
|
|
455
|
+
* formatting so the renderer falls back to its theme defaults.
|
|
456
|
+
*/
|
|
457
|
+
function extractChartTitleTextFormat(chart) {
|
|
458
|
+
const title = get(chart, "c:title") ?? get(chart, "title");
|
|
459
|
+
if (!title)
|
|
460
|
+
return undefined;
|
|
461
|
+
const richTxPr = get(title, "c:tx.c:rich") ?? get(title, "tx.rich");
|
|
462
|
+
const fromRich = extractTextProperties(richTxPr);
|
|
463
|
+
if (fromRich)
|
|
464
|
+
return fromRich;
|
|
465
|
+
const txPr = get(title, "c:txPr") ?? get(title, "txPr");
|
|
466
|
+
return extractTextProperties(txPr);
|
|
467
|
+
}
|
|
468
|
+
export function extractChartTitle(chart) {
|
|
469
|
+
// Try different possible paths for chart title
|
|
470
|
+
const title =
|
|
471
|
+
// Rich text format (your current structure)
|
|
472
|
+
get(chart, "c:title.c:tx.c:rich.a:p.a:r.a:t") ||
|
|
473
|
+
// Alternative rich text paths
|
|
474
|
+
get(chart, "c:title.c:tx.c:rich.a:p.a:r.a:t.t$") ||
|
|
475
|
+
// String reference format
|
|
476
|
+
get(chart, "c:title.c:tx.c:strRef.c:strCache.c:pt.c:v") ||
|
|
477
|
+
get(chart, "c:title.c:tx.c:strRef.c:strCache.c:pt.c:v.t$") ||
|
|
478
|
+
// Direct value format
|
|
479
|
+
get(chart, "c:title.c:tx.c:v") ||
|
|
480
|
+
get(chart, "c:title.c:tx.c:v.t$") ||
|
|
481
|
+
// Simple text format
|
|
482
|
+
get(chart, "c:title.c:tx") ||
|
|
483
|
+
// Fallback: look for any text in title
|
|
484
|
+
extractTextFromTitle(get(chart, "c:title")) ||
|
|
485
|
+
"";
|
|
486
|
+
return typeof title === "string" ? title : "";
|
|
487
|
+
}
|
|
488
|
+
// Helper function to recursively search for text in title structure
|
|
489
|
+
function extractTextFromTitle(titleObj) {
|
|
490
|
+
if (!titleObj)
|
|
491
|
+
return "";
|
|
492
|
+
// If it's already a string, return it
|
|
493
|
+
if (typeof titleObj === "string")
|
|
494
|
+
return titleObj;
|
|
495
|
+
// Look for common text properties
|
|
496
|
+
if (titleObj["a:t"])
|
|
497
|
+
return titleObj["a:t"];
|
|
498
|
+
if (titleObj.t$)
|
|
499
|
+
return titleObj.t$;
|
|
500
|
+
if (titleObj.text)
|
|
501
|
+
return titleObj.text;
|
|
502
|
+
if (titleObj.value)
|
|
503
|
+
return titleObj.value;
|
|
504
|
+
// Recursively search through nested objects
|
|
505
|
+
for (const key in titleObj) {
|
|
506
|
+
if (typeof titleObj[key] === "object") {
|
|
507
|
+
const result = extractTextFromTitle(titleObj[key]);
|
|
508
|
+
if (result)
|
|
509
|
+
return result;
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
return "";
|
|
513
|
+
}
|
|
514
|
+
export function determineChartType(chart) {
|
|
515
|
+
const plotArea = getWithFallback(chart, ["c:plotArea", "plotArea"]);
|
|
516
|
+
// c15 extension marker (Phase 5.5): if the plotArea's c:extLst tags
|
|
517
|
+
// the chart as histogram / boxplot / waterfall / funnel, that overrides
|
|
518
|
+
// the bar-chart fallback the body uses for older-Excel compatibility.
|
|
519
|
+
const c15Type = detectC15ChartType(plotArea);
|
|
520
|
+
if (c15Type)
|
|
521
|
+
return c15Type;
|
|
522
|
+
const barChart = getWithFallback(plotArea, [
|
|
523
|
+
"c:barChart",
|
|
524
|
+
"barChart",
|
|
525
|
+
"c:bar3DChart",
|
|
526
|
+
"bar3DChart",
|
|
527
|
+
]);
|
|
528
|
+
if (barChart) {
|
|
529
|
+
const direction = get(barChart, "c:barDir.a$.val") ||
|
|
530
|
+
get(barChart, "c:barDir.a$.t$") ||
|
|
531
|
+
get(barChart, "barDir.a$.val") ||
|
|
532
|
+
get(barChart, "barDir.a$.t$") ||
|
|
533
|
+
get(barChart, "c:barDir.a$.v") ||
|
|
534
|
+
get(barChart, "barDir.a$.v");
|
|
535
|
+
return direction === "bar" ? "bar" : "column";
|
|
536
|
+
}
|
|
537
|
+
if (getWithFallback(plotArea, [
|
|
538
|
+
"c:lineChart",
|
|
539
|
+
"lineChart",
|
|
540
|
+
"c:line3DChart",
|
|
541
|
+
"line3DChart",
|
|
542
|
+
]))
|
|
543
|
+
return "line";
|
|
544
|
+
// OOXML uses a dedicated <c:doughnutChart> element. Check it before
|
|
545
|
+
// <c:pieChart>/<c:ofPieChart> so the more-specific shape wins.
|
|
546
|
+
if (getWithFallback(plotArea, ["c:doughnutChart", "doughnutChart"]))
|
|
547
|
+
return "doughnut";
|
|
548
|
+
// pie-of-pie / bar-of-pie — the renderer draws the composite from a
|
|
549
|
+
// plain "pie" chartType plus sourceChartType "OF_PIE" and the of-pie
|
|
550
|
+
// split fields (extracted in extractOfPieOptions).
|
|
551
|
+
if (getWithFallback(plotArea, ["c:ofPieChart", "ofPieChart"]))
|
|
552
|
+
return "pie";
|
|
553
|
+
if (getWithFallback(plotArea, [
|
|
554
|
+
"c:pieChart",
|
|
555
|
+
"pieChart",
|
|
556
|
+
"c:pie3DChart",
|
|
557
|
+
"pie3DChart",
|
|
558
|
+
]))
|
|
559
|
+
return "pie";
|
|
560
|
+
if (getWithFallback(plotArea, [
|
|
561
|
+
"c:areaChart",
|
|
562
|
+
"areaChart",
|
|
563
|
+
"c:area3DChart",
|
|
564
|
+
"area3DChart",
|
|
565
|
+
]))
|
|
566
|
+
return "area";
|
|
567
|
+
if (getWithFallback(plotArea, ["c:radarChart", "radarChart"]))
|
|
568
|
+
return "radar";
|
|
569
|
+
if (getWithFallback(plotArea, ["c:stockChart", "stockChart"]))
|
|
570
|
+
return "stock";
|
|
571
|
+
// Scatter and bubble — both use c:xVal/c:yVal series shape (not
|
|
572
|
+
// c:cat/c:val like other charts). Bubble additionally has c:bubbleSize.
|
|
573
|
+
if (getWithFallback(plotArea, ["c:scatterChart", "scatterChart"]))
|
|
574
|
+
return "scatter";
|
|
575
|
+
if (getWithFallback(plotArea, ["c:bubbleChart", "bubbleChart"]))
|
|
576
|
+
return "bubble";
|
|
577
|
+
return "column"; // default
|
|
578
|
+
}
|
|
579
|
+
/**
|
|
580
|
+
* Phase 5.5 — detect the c15 extension marker our exporter writes on
|
|
581
|
+
* the plotArea. Returns the originating chart type (histogram /
|
|
582
|
+
* boxplot / waterfall / funnel) or null if no marker is present.
|
|
583
|
+
*
|
|
584
|
+
* The marker shape is:
|
|
585
|
+
* c:plotArea > c:extLst > c:ext (uri = ...) > c15:<chartName>Chart
|
|
586
|
+
*
|
|
587
|
+
* We accept either array or single-object shape for `c:ext` since
|
|
588
|
+
* fast-xml-parser normalizes lone children to objects.
|
|
589
|
+
*/
|
|
590
|
+
function detectC15ChartType(plotArea) {
|
|
591
|
+
if (!plotArea)
|
|
592
|
+
return null;
|
|
593
|
+
// Empty c15 elements (`<c15:histogramChart/>`) parse to "" — truthy
|
|
594
|
+
// checks skip them, so we test for key presence instead.
|
|
595
|
+
const hasKey = (obj, key) => obj != null && Object.prototype.hasOwnProperty.call(obj, key);
|
|
596
|
+
// Phase 5.5b: c15 chart wrapped in mc:AlternateContent > mc:Choice.
|
|
597
|
+
// This is the Excel-native shape — modern Excel renders the chart
|
|
598
|
+
// from here, older Excel uses mc:Fallback (a c:barChart).
|
|
599
|
+
const altContent = getWithFallback(plotArea, [
|
|
600
|
+
"mc:AlternateContent",
|
|
601
|
+
"AlternateContent",
|
|
602
|
+
]);
|
|
603
|
+
if (altContent) {
|
|
604
|
+
const choice = getWithFallback(altContent, ["mc:Choice", "Choice"]);
|
|
605
|
+
if (choice) {
|
|
606
|
+
const choices = Array.isArray(choice) ? choice : [choice];
|
|
607
|
+
for (const c of choices) {
|
|
608
|
+
if (hasKey(c, "c15:histogramChart") || hasKey(c, "histogramChart"))
|
|
609
|
+
return "histogram";
|
|
610
|
+
if (hasKey(c, "c15:boxWhiskerChart") || hasKey(c, "boxWhiskerChart"))
|
|
611
|
+
return "boxplot";
|
|
612
|
+
if (hasKey(c, "c15:waterfallChart") || hasKey(c, "waterfallChart"))
|
|
613
|
+
return "waterfall";
|
|
614
|
+
if (hasKey(c, "c15:funnelChart") || hasKey(c, "funnelChart"))
|
|
615
|
+
return "funnel";
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
// Phase 5.5 (legacy): c15 extension marker inside c:extLst > c:ext.
|
|
620
|
+
// Round-trips through our pipeline; opens as bar chart in Excel.
|
|
621
|
+
// Kept so files written by older versions of this code still parse.
|
|
622
|
+
const extLst = getWithFallback(plotArea, ["c:extLst", "extLst"]);
|
|
623
|
+
if (!extLst)
|
|
624
|
+
return null;
|
|
625
|
+
const extNode = getWithFallback(extLst, ["c:ext", "ext"]);
|
|
626
|
+
if (!extNode)
|
|
627
|
+
return null;
|
|
628
|
+
const exts = Array.isArray(extNode) ? extNode : [extNode];
|
|
629
|
+
for (const ext of exts) {
|
|
630
|
+
if (!ext)
|
|
631
|
+
continue;
|
|
632
|
+
if (hasKey(ext, "c15:histogramChart") || hasKey(ext, "histogramChart"))
|
|
633
|
+
return "histogram";
|
|
634
|
+
if (hasKey(ext, "c15:boxWhiskerChart") || hasKey(ext, "boxWhiskerChart"))
|
|
635
|
+
return "boxplot";
|
|
636
|
+
if (hasKey(ext, "c15:waterfallChart") || hasKey(ext, "waterfallChart"))
|
|
637
|
+
return "waterfall";
|
|
638
|
+
if (hasKey(ext, "c15:funnelChart") || hasKey(ext, "funnelChart"))
|
|
639
|
+
return "funnel";
|
|
640
|
+
}
|
|
641
|
+
return null;
|
|
642
|
+
}
|
|
643
|
+
/**
|
|
644
|
+
* Phase 5.5 — pull binCount out of the c15:histogramChart extension
|
|
645
|
+
* marker. Returns undefined if absent.
|
|
646
|
+
*/
|
|
647
|
+
export function extractC15HistogramBinCount(plotArea) {
|
|
648
|
+
if (!plotArea)
|
|
649
|
+
return undefined;
|
|
650
|
+
const readBinCount = (hist) => {
|
|
651
|
+
if (!hist)
|
|
652
|
+
return undefined;
|
|
653
|
+
const raw = get(hist, "c15:binCount.a$.val") ??
|
|
654
|
+
get(hist, "binCount.a$.val") ??
|
|
655
|
+
get(hist, "c15:binCount.a$.v") ??
|
|
656
|
+
get(hist, "binCount.a$.v");
|
|
657
|
+
if (raw === undefined || raw === null)
|
|
658
|
+
return undefined;
|
|
659
|
+
const num = Number(raw);
|
|
660
|
+
return Number.isFinite(num) ? num : undefined;
|
|
661
|
+
};
|
|
662
|
+
// Phase 5.5b: histogram inside mc:AlternateContent > mc:Choice.
|
|
663
|
+
const altContent = getWithFallback(plotArea, [
|
|
664
|
+
"mc:AlternateContent",
|
|
665
|
+
"AlternateContent",
|
|
666
|
+
]);
|
|
667
|
+
if (altContent) {
|
|
668
|
+
const choice = getWithFallback(altContent, ["mc:Choice", "Choice"]);
|
|
669
|
+
const choices = Array.isArray(choice)
|
|
670
|
+
? choice
|
|
671
|
+
: choice
|
|
672
|
+
? [choice]
|
|
673
|
+
: [];
|
|
674
|
+
for (const c of choices) {
|
|
675
|
+
const hist = c?.["c15:histogramChart"] ?? c?.["histogramChart"] ?? null;
|
|
676
|
+
const v = readBinCount(hist);
|
|
677
|
+
if (v !== undefined)
|
|
678
|
+
return v;
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
// Phase 5.5 (legacy): histogram inside c:extLst > c:ext marker.
|
|
682
|
+
const extLst = getWithFallback(plotArea, ["c:extLst", "extLst"]);
|
|
683
|
+
if (!extLst)
|
|
684
|
+
return undefined;
|
|
685
|
+
const extNode = getWithFallback(extLst, ["c:ext", "ext"]);
|
|
686
|
+
if (!extNode)
|
|
687
|
+
return undefined;
|
|
688
|
+
const exts = Array.isArray(extNode) ? extNode : [extNode];
|
|
689
|
+
for (const ext of exts) {
|
|
690
|
+
const hist = ext?.["c15:histogramChart"] ?? ext?.["histogramChart"] ?? null;
|
|
691
|
+
const v = readBinCount(hist);
|
|
692
|
+
if (v !== undefined)
|
|
693
|
+
return v;
|
|
694
|
+
}
|
|
695
|
+
return undefined;
|
|
696
|
+
}
|
|
697
|
+
// Chart type containers to search for series. Combo charts can mix
|
|
698
|
+
// multiple containers (e.g. column + line on one chart), so we iterate
|
|
699
|
+
// through all of them. XY charts (scatter/bubble) have a different
|
|
700
|
+
// per-series shape (xVal/yVal/bubbleSize) and require their own series
|
|
701
|
+
// extractor — they're listed here so combo charts that mix XY with
|
|
702
|
+
// non-XY series (rare but valid in OOXML) still pick up both.
|
|
703
|
+
const CHART_TYPE_CONTAINERS = [
|
|
704
|
+
{
|
|
705
|
+
paths: ["c:barChart", "barChart", "c:bar3DChart", "bar3DChart"],
|
|
706
|
+
chartType: "column",
|
|
707
|
+
isBar: true,
|
|
708
|
+
},
|
|
709
|
+
{
|
|
710
|
+
paths: ["c:lineChart", "lineChart", "c:line3DChart", "line3DChart"],
|
|
711
|
+
chartType: "line",
|
|
712
|
+
},
|
|
713
|
+
{
|
|
714
|
+
paths: ["c:areaChart", "areaChart", "c:area3DChart", "area3DChart"],
|
|
715
|
+
chartType: "area",
|
|
716
|
+
},
|
|
717
|
+
{
|
|
718
|
+
paths: ["c:scatterChart", "scatterChart"],
|
|
719
|
+
chartType: "scatter",
|
|
720
|
+
isXY: true,
|
|
721
|
+
},
|
|
722
|
+
{
|
|
723
|
+
paths: ["c:bubbleChart", "bubbleChart"],
|
|
724
|
+
chartType: "bubble",
|
|
725
|
+
isXY: true,
|
|
726
|
+
},
|
|
727
|
+
];
|
|
728
|
+
/**
|
|
729
|
+
* Resolve a chart-formula range string (e.g.
|
|
730
|
+
* `'Data for the Chart'!$A$7:$A$26`) into the sources array our chart
|
|
731
|
+
* model expects (one entry per parsed selection, with the data sheet's
|
|
732
|
+
* sheetId — not the chart's host sheet).
|
|
733
|
+
*/
|
|
734
|
+
function resolveSourcesFromRef(ref, hostSheetId, resolveSheetId) {
|
|
735
|
+
const selections = generateSelectionsFromFormula(`=${ref}`);
|
|
736
|
+
return selections.map((sel) => ({
|
|
737
|
+
...sel.range,
|
|
738
|
+
sheetId: pickSheetId(sel.sheetName, hostSheetId, resolveSheetId),
|
|
739
|
+
}));
|
|
740
|
+
}
|
|
741
|
+
/**
|
|
742
|
+
* Extract inline values from a `<c:strLit>` or `<c:numLit>` element —
|
|
743
|
+
* these appear when chart data is embedded directly in the chart XML
|
|
744
|
+
* rather than referencing the sheet (Event Budget.xlsx uses this for
|
|
745
|
+
* its `["Estimated", "Actual"]` category labels).
|
|
746
|
+
*
|
|
747
|
+
* Returns the values ordered by their `idx` attribute (Excel allows
|
|
748
|
+
* sparse indices). Returns `undefined` when no inline data is present.
|
|
749
|
+
*/
|
|
750
|
+
function extractInlineValues(node) {
|
|
751
|
+
if (!node)
|
|
752
|
+
return undefined;
|
|
753
|
+
const strLit = get(node, "c:strLit") ?? get(node, "strLit");
|
|
754
|
+
const numLit = get(node, "c:numLit") ?? get(node, "numLit");
|
|
755
|
+
const lit = strLit ?? numLit;
|
|
756
|
+
if (!lit)
|
|
757
|
+
return undefined;
|
|
758
|
+
const pts = get(lit, "c:pt") ?? get(lit, "pt");
|
|
759
|
+
if (!pts)
|
|
760
|
+
return [];
|
|
761
|
+
const ptArray = Array.isArray(pts) ? pts : [pts];
|
|
762
|
+
// Order by the `idx` attribute. Most files emit them in order but
|
|
763
|
+
// spec permits any order; we honour that to avoid mis-aligning the
|
|
764
|
+
// first column when authors hand-craft chart XML.
|
|
765
|
+
const indexed = [];
|
|
766
|
+
for (const pt of ptArray) {
|
|
767
|
+
const idxAttr = get(pt, "a$.idx");
|
|
768
|
+
const idx = idxAttr !== undefined ? Number(idxAttr) : indexed.length;
|
|
769
|
+
const rawV = get(pt, "c:v") ?? get(pt, "v");
|
|
770
|
+
let v;
|
|
771
|
+
if (typeof rawV === "number" || typeof rawV === "boolean") {
|
|
772
|
+
v = rawV;
|
|
773
|
+
}
|
|
774
|
+
else if (numLit && typeof rawV === "string" && rawV.trim() !== "") {
|
|
775
|
+
const n = Number(rawV);
|
|
776
|
+
v = Number.isFinite(n) ? n : rawV;
|
|
777
|
+
}
|
|
778
|
+
else {
|
|
779
|
+
v = typeof rawV === "string" ? rawV : String(rawV ?? "");
|
|
780
|
+
}
|
|
781
|
+
indexed.push({ idx: Number.isFinite(idx) ? idx : indexed.length, v });
|
|
782
|
+
}
|
|
783
|
+
indexed.sort((a, b) => a.idx - b.idx);
|
|
784
|
+
return indexed.map((entry) => entry.v);
|
|
785
|
+
}
|
|
786
|
+
export function extractChartSeries(plotArea, chartType, sheetId, resolveSheetId) {
|
|
787
|
+
const series = [];
|
|
788
|
+
let seriesIndex = 0;
|
|
789
|
+
// Iterate through all chart type containers to support combo charts
|
|
790
|
+
for (const container of CHART_TYPE_CONTAINERS) {
|
|
791
|
+
const chartElement = getWithFallback(plotArea, container.paths);
|
|
792
|
+
if (!chartElement)
|
|
793
|
+
continue;
|
|
794
|
+
// Handle case where chart element is an array (e.g., multiple c:lineChart elements)
|
|
795
|
+
const chartElements = Array.isArray(chartElement)
|
|
796
|
+
? chartElement
|
|
797
|
+
: [chartElement];
|
|
798
|
+
for (const singleChartElement of chartElements) {
|
|
799
|
+
// Determine the actual chart type for this container
|
|
800
|
+
let seriesChartType = container.chartType;
|
|
801
|
+
if (container.isBar) {
|
|
802
|
+
// Check barDir to distinguish bar vs column
|
|
803
|
+
const direction = get(singleChartElement, "c:barDir.a$.val") ||
|
|
804
|
+
get(singleChartElement, "c:barDir.a$.t$") ||
|
|
805
|
+
get(singleChartElement, "barDir.a$.val") ||
|
|
806
|
+
get(singleChartElement, "barDir.a$.t$");
|
|
807
|
+
seriesChartType = direction === "bar" ? "bar" : "column";
|
|
808
|
+
}
|
|
809
|
+
// Get series elements from this container
|
|
810
|
+
const seriesElements = getWithFallback(singleChartElement, [
|
|
811
|
+
"c:ser",
|
|
812
|
+
"ser",
|
|
813
|
+
]);
|
|
814
|
+
if (!seriesElements)
|
|
815
|
+
continue;
|
|
816
|
+
const seriesArray = Array.isArray(seriesElements)
|
|
817
|
+
? seriesElements
|
|
818
|
+
: [seriesElements];
|
|
819
|
+
for (let i = 0; i < seriesArray.length; i++) {
|
|
820
|
+
const ser = seriesArray[i];
|
|
821
|
+
// Scatter/bubble — "series" carries Y values (yVal). Non-XY —
|
|
822
|
+
// it carries c:val.
|
|
823
|
+
const valRef = container.isXY
|
|
824
|
+
? getWithFallback(ser, Y_VAL_REF_PATHS)
|
|
825
|
+
: getWithFallback(ser, VAL_REF_PATHS);
|
|
826
|
+
const seriesTitle = extractSeriesTitleFromSeries(ser);
|
|
827
|
+
const colorInfo = extractSeriesColorInfo(ser);
|
|
828
|
+
// Resolve the series' value source — preferring the cell-range
|
|
829
|
+
// reference and falling back to inline `<c:numLit>`/`<c:strLit>`
|
|
830
|
+
// when there's no range (rare for series values, common for
|
|
831
|
+
// tiny embedded charts).
|
|
832
|
+
let sources = [];
|
|
833
|
+
let inlineValues;
|
|
834
|
+
if (valRef) {
|
|
835
|
+
sources = resolveSourcesFromRef(valRef, sheetId, resolveSheetId);
|
|
836
|
+
}
|
|
837
|
+
if (!sources.length) {
|
|
838
|
+
const valNode = container.isXY
|
|
839
|
+
? get(ser, "c:yVal") ?? get(ser, "yVal")
|
|
840
|
+
: get(ser, "c:val") ?? get(ser, "val");
|
|
841
|
+
inlineValues = extractInlineValues(valNode);
|
|
842
|
+
}
|
|
843
|
+
if (!sources.length && (!inlineValues || !inlineValues.length)) {
|
|
844
|
+
seriesIndex++;
|
|
845
|
+
continue;
|
|
846
|
+
}
|
|
847
|
+
const trendlines = extractTrendlines(ser);
|
|
848
|
+
const errorBars = extractErrorBars(ser, sheetId, resolveSheetId);
|
|
849
|
+
const marker = extractSeriesMarker(ser);
|
|
850
|
+
const dataPointColors = extractSeriesDataPointColors(ser);
|
|
851
|
+
const nameRef = getWithFallback(ser, TX_REF_PATHS);
|
|
852
|
+
// Per-series category reference — charts whose series plot against
|
|
853
|
+
// different category ranges can't be represented by the single
|
|
854
|
+
// chart-level domain alone; the exporter prefers this when present.
|
|
855
|
+
const serCatRef = container.isXY
|
|
856
|
+
? undefined
|
|
857
|
+
: getWithFallback(ser, CAT_REF_PATHS);
|
|
858
|
+
const categorySources = serCatRef
|
|
859
|
+
? resolveSourcesFromRef(serCatRef, sheetId, resolveSheetId)
|
|
860
|
+
: [];
|
|
861
|
+
series.push({
|
|
862
|
+
dataLabel: seriesTitle || `Series ${seriesIndex + 1}`,
|
|
863
|
+
...(nameRef ? { dataLabelRef: nameRef } : {}),
|
|
864
|
+
...(categorySources.length ? { categorySources } : {}),
|
|
865
|
+
color: colorInfo.color,
|
|
866
|
+
...(colorInfo.noFill ? { noFill: true } : {}),
|
|
867
|
+
negativeColor: colorInfo.negativeColor,
|
|
868
|
+
invertIfNegative: colorInfo.invertIfNegative,
|
|
869
|
+
...(colorInfo.gradient ? { gradient: colorInfo.gradient } : {}),
|
|
870
|
+
// `seriesChartType` is only meaningful for combo charts where
|
|
871
|
+
// individual series can override the chart's overall type
|
|
872
|
+
// (bar+line, column+line, etc.). XY charts (scatter/bubble)
|
|
873
|
+
// aren't part of combo mixing — leave it unset there so the
|
|
874
|
+
// renderer falls back to the chart-level chartType.
|
|
875
|
+
...(container.isXY ? {} : { seriesChartType: seriesChartType }),
|
|
876
|
+
sources,
|
|
877
|
+
...(inlineValues && inlineValues.length ? { inlineValues } : {}),
|
|
878
|
+
...(trendlines && trendlines.length ? { trendlines } : {}),
|
|
879
|
+
...(errorBars ? { errorBars } : {}),
|
|
880
|
+
...(marker ? { marker } : {}),
|
|
881
|
+
...(dataPointColors && dataPointColors.length
|
|
882
|
+
? { dataPointColors }
|
|
883
|
+
: {}),
|
|
884
|
+
});
|
|
885
|
+
seriesIndex++;
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
// Fallback: pie/doughnut/radar/stock/ofPie/etc. aren't in
|
|
890
|
+
// CHART_TYPE_CONTAINERS; they're single-container, type-specific,
|
|
891
|
+
// and don't combo with other types. Pick up via the type-specific
|
|
892
|
+
// path-set helper.
|
|
893
|
+
if (series.length === 0) {
|
|
894
|
+
const seriesElements = getSeriesElements(plotArea, chartType);
|
|
895
|
+
if (seriesElements) {
|
|
896
|
+
const seriesArray = Array.isArray(seriesElements)
|
|
897
|
+
? seriesElements
|
|
898
|
+
: [seriesElements];
|
|
899
|
+
seriesArray.forEach((ser, index) => {
|
|
900
|
+
const valRef = isXYChart(chartType)
|
|
901
|
+
? getWithFallback(ser, Y_VAL_REF_PATHS)
|
|
902
|
+
: getWithFallback(ser, VAL_REF_PATHS);
|
|
903
|
+
const seriesTitle = extractSeriesTitleFromSeries(ser);
|
|
904
|
+
const colorInfo = extractSeriesColorInfo(ser);
|
|
905
|
+
let sources = [];
|
|
906
|
+
let inlineValues;
|
|
907
|
+
if (valRef) {
|
|
908
|
+
sources = resolveSourcesFromRef(valRef, sheetId, resolveSheetId);
|
|
909
|
+
}
|
|
910
|
+
if (!sources.length) {
|
|
911
|
+
const valNode = isXYChart(chartType)
|
|
912
|
+
? get(ser, "c:yVal") ?? get(ser, "yVal")
|
|
913
|
+
: get(ser, "c:val") ?? get(ser, "val");
|
|
914
|
+
inlineValues = extractInlineValues(valNode);
|
|
915
|
+
}
|
|
916
|
+
if (!sources.length && (!inlineValues || !inlineValues.length)) {
|
|
917
|
+
return;
|
|
918
|
+
}
|
|
919
|
+
const marker = extractSeriesMarker(ser);
|
|
920
|
+
const dataPointColors = extractSeriesDataPointColors(ser);
|
|
921
|
+
const nameRef = getWithFallback(ser, TX_REF_PATHS);
|
|
922
|
+
const serCatRef = isXYChart(chartType)
|
|
923
|
+
? undefined
|
|
924
|
+
: getWithFallback(ser, CAT_REF_PATHS);
|
|
925
|
+
const categorySources = serCatRef
|
|
926
|
+
? resolveSourcesFromRef(serCatRef, sheetId, resolveSheetId)
|
|
927
|
+
: [];
|
|
928
|
+
series.push({
|
|
929
|
+
dataLabel: seriesTitle || `Series ${index + 1}`,
|
|
930
|
+
...(nameRef ? { dataLabelRef: nameRef } : {}),
|
|
931
|
+
...(categorySources.length ? { categorySources } : {}),
|
|
932
|
+
color: colorInfo.color,
|
|
933
|
+
...(colorInfo.noFill ? { noFill: true } : {}),
|
|
934
|
+
negativeColor: colorInfo.negativeColor,
|
|
935
|
+
invertIfNegative: colorInfo.invertIfNegative,
|
|
936
|
+
...(colorInfo.gradient ? { gradient: colorInfo.gradient } : {}),
|
|
937
|
+
sources,
|
|
938
|
+
...(inlineValues && inlineValues.length ? { inlineValues } : {}),
|
|
939
|
+
...(marker ? { marker } : {}),
|
|
940
|
+
...(dataPointColors && dataPointColors.length
|
|
941
|
+
? { dataPointColors }
|
|
942
|
+
: {}),
|
|
943
|
+
});
|
|
944
|
+
});
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
return series;
|
|
948
|
+
}
|
|
949
|
+
/**
|
|
950
|
+
* Extract per-data-point color overrides — `<c:dPt>` array inside
|
|
951
|
+
* `<c:ser>`. Each entry pins a specific category index to its own
|
|
952
|
+
* fill color (Gantt charts use this to colour individual tasks; pie
|
|
953
|
+
* charts use it for per-slice colors). Returns `undefined` when there
|
|
954
|
+
* are no overrides.
|
|
955
|
+
*/
|
|
956
|
+
function extractSeriesDataPointColors(ser) {
|
|
957
|
+
const raw = get(ser, "c:dPt") ?? get(ser, "dPt");
|
|
958
|
+
if (!raw)
|
|
959
|
+
return undefined;
|
|
960
|
+
const list = Array.isArray(raw) ? raw : [raw];
|
|
961
|
+
const result = [];
|
|
962
|
+
for (const dPt of list) {
|
|
963
|
+
const idxRaw = get(dPt, "c:idx.a$.val") ?? get(dPt, "idx.a$.val");
|
|
964
|
+
const idx = idxRaw !== undefined ? Number(idxRaw) : NaN;
|
|
965
|
+
if (!Number.isFinite(idx))
|
|
966
|
+
continue;
|
|
967
|
+
const solidFill = get(dPt, "c:spPr.a:solidFill") ?? get(dPt, "spPr.a:solidFill");
|
|
968
|
+
const color = resolveDrawingColor(solidFill);
|
|
969
|
+
if (!color)
|
|
970
|
+
continue;
|
|
971
|
+
result.push({ idx, color });
|
|
972
|
+
}
|
|
973
|
+
return result.length ? result : undefined;
|
|
974
|
+
}
|
|
975
|
+
const MARKER_SYMBOL_SET = new Set([
|
|
976
|
+
"none",
|
|
977
|
+
"circle",
|
|
978
|
+
"square",
|
|
979
|
+
"diamond",
|
|
980
|
+
"triangle",
|
|
981
|
+
"x",
|
|
982
|
+
"plus",
|
|
983
|
+
"star",
|
|
984
|
+
"dash",
|
|
985
|
+
"dot",
|
|
986
|
+
"auto",
|
|
987
|
+
"picture",
|
|
988
|
+
]);
|
|
989
|
+
function extractSeriesMarker(ser) {
|
|
990
|
+
const raw = get(ser, "c:marker") ?? get(ser, "marker");
|
|
991
|
+
if (!raw)
|
|
992
|
+
return undefined;
|
|
993
|
+
const symbolRaw = get(raw, "c:symbol.a$.val") ??
|
|
994
|
+
get(raw, "symbol.a$.val") ??
|
|
995
|
+
get(raw, "c:symbol.a$.t$") ??
|
|
996
|
+
get(raw, "symbol.a$.t$");
|
|
997
|
+
const symbol = typeof symbolRaw === "string" &&
|
|
998
|
+
MARKER_SYMBOL_SET.has(symbolRaw)
|
|
999
|
+
? symbolRaw
|
|
1000
|
+
: undefined;
|
|
1001
|
+
const sizeAttr = get(raw, "c:size.a$.val") ??
|
|
1002
|
+
get(raw, "size.a$.val") ??
|
|
1003
|
+
get(raw, "c:size.a$.t$") ??
|
|
1004
|
+
get(raw, "size.a$.t$");
|
|
1005
|
+
const size = sizeAttr !== undefined && sizeAttr !== null && sizeAttr !== ""
|
|
1006
|
+
? Number(sizeAttr)
|
|
1007
|
+
: undefined;
|
|
1008
|
+
// Marker fill — used when explicit (not derived from series color).
|
|
1009
|
+
const fillRgb = get(raw, "c:spPr.a:solidFill.a:srgbClr.a$.val") ??
|
|
1010
|
+
get(raw, "spPr.solidFill.srgbClr.a$.val");
|
|
1011
|
+
const color = fillRgb ? `#${fillRgb}` : undefined;
|
|
1012
|
+
const result = {};
|
|
1013
|
+
if (symbol)
|
|
1014
|
+
result.symbol = symbol;
|
|
1015
|
+
if (typeof size === "number" && Number.isFinite(size))
|
|
1016
|
+
result.size = size;
|
|
1017
|
+
if (color)
|
|
1018
|
+
result.color = color;
|
|
1019
|
+
return Object.keys(result).length ? result : undefined;
|
|
1020
|
+
}
|
|
1021
|
+
// ─── Text property extraction (color, font) ──────────────────────────
|
|
1022
|
+
// Used by `readAxis`, `extractLegendOptions`, `extractDataLabelsOptions`,
|
|
1023
|
+
// `extractChartTitle` to pull `<c:txPr>` / `<a:rPr>` styling so axis
|
|
1024
|
+
// labels, data labels, legend entries and chart titles render with
|
|
1025
|
+
// their authored colors instead of falling back to the renderer's
|
|
1026
|
+
// theme default (the BEP.xlsx import showed all labels as
|
|
1027
|
+
// near-black).
|
|
1028
|
+
const OFFICE_THEME_FALLBACK = {
|
|
1029
|
+
// Standard Office theme color tokens. We don't try to resolve from
|
|
1030
|
+
// the workbook's actual `xl/theme/themeN.xml` here — the chart is
|
|
1031
|
+
// typically authored against the same theme palette and these
|
|
1032
|
+
// defaults match the in-app theme defaults closely enough that the
|
|
1033
|
+
// visual difference is below noise level.
|
|
1034
|
+
tx1: "#000000",
|
|
1035
|
+
tx2: "#44546A",
|
|
1036
|
+
bg1: "#FFFFFF",
|
|
1037
|
+
bg2: "#E7E6E6",
|
|
1038
|
+
dk1: "#000000",
|
|
1039
|
+
dk2: "#44546A",
|
|
1040
|
+
lt1: "#FFFFFF",
|
|
1041
|
+
lt2: "#E7E6E6",
|
|
1042
|
+
accent1: "#4472C4",
|
|
1043
|
+
accent2: "#ED7D31",
|
|
1044
|
+
accent3: "#A5A5A5",
|
|
1045
|
+
accent4: "#FFC000",
|
|
1046
|
+
accent5: "#5B9BD5",
|
|
1047
|
+
accent6: "#70AD47",
|
|
1048
|
+
hlink: "#0563C1",
|
|
1049
|
+
folHlink: "#954F72",
|
|
1050
|
+
};
|
|
1051
|
+
/**
|
|
1052
|
+
* Apply OOXML lumMod / lumOff / shade / tint modifiers to a base RGB
|
|
1053
|
+
* color. All are stored as 1000ths of a percent (75000 = 75%).
|
|
1054
|
+
*
|
|
1055
|
+
* - `lumMod` scales each channel (`c * mod`)
|
|
1056
|
+
* - `lumOff` adds toward white (`+ 255 * off`)
|
|
1057
|
+
* - `shade` darkens — interpolates toward black (`c * shade`)
|
|
1058
|
+
* - `tint` lightens — interpolates toward white (`c + (255 - c) * tint`)
|
|
1059
|
+
*
|
|
1060
|
+
* Order: shade/tint apply first (they're independent transforms), then
|
|
1061
|
+
* lumMod/lumOff. Matches Excel's behaviour closely for common patterns:
|
|
1062
|
+
* - `tx1 + lumMod=75% + lumOff=25%` → `#404040` dark gray (BEP)
|
|
1063
|
+
* - `accent1 + shade=76%` → darker accent1 (Gantt)
|
|
1064
|
+
*/
|
|
1065
|
+
const applyLumModOff = (hex, lumMod, lumOff, shade, tint) => {
|
|
1066
|
+
const m = /^#?([0-9a-fA-F]{6})$/.exec(hex);
|
|
1067
|
+
if (!m)
|
|
1068
|
+
return hex;
|
|
1069
|
+
const v = parseInt(m[1], 16);
|
|
1070
|
+
let r = (v >> 16) & 0xff;
|
|
1071
|
+
let g = (v >> 8) & 0xff;
|
|
1072
|
+
let b = v & 0xff;
|
|
1073
|
+
// shade darkens (multiply by factor < 1); tint lightens (interp
|
|
1074
|
+
// toward white). Apply before lumMod/lumOff since those mix in a
|
|
1075
|
+
// different luminance space and Excel evaluates shade/tint first.
|
|
1076
|
+
if (typeof shade === "number" && shade > 0 && shade < 1) {
|
|
1077
|
+
r = r * shade;
|
|
1078
|
+
g = g * shade;
|
|
1079
|
+
b = b * shade;
|
|
1080
|
+
}
|
|
1081
|
+
if (typeof tint === "number" && tint > 0 && tint < 1) {
|
|
1082
|
+
r = r + (255 - r) * tint;
|
|
1083
|
+
g = g + (255 - g) * tint;
|
|
1084
|
+
b = b + (255 - b) * tint;
|
|
1085
|
+
}
|
|
1086
|
+
const mod = typeof lumMod === "number" ? lumMod : 1;
|
|
1087
|
+
const off = typeof lumOff === "number" ? lumOff : 0;
|
|
1088
|
+
const apply = (c) => Math.max(0, Math.min(255, Math.round(c * mod + 255 * off)));
|
|
1089
|
+
const toHex = (c) => c.toString(16).padStart(2, "0").toUpperCase();
|
|
1090
|
+
return `#${toHex(apply(r))}${toHex(apply(g))}${toHex(apply(b))}`;
|
|
1091
|
+
};
|
|
1092
|
+
/**
|
|
1093
|
+
* Resolve a DrawingML `<a:solidFill>` (or any element that wraps an
|
|
1094
|
+
* `<a:srgbClr>` / `<a:schemeClr>`) to a hex color string. Handles
|
|
1095
|
+
* `lumMod`/`lumOff` modifiers used to derive grays from theme tokens.
|
|
1096
|
+
*/
|
|
1097
|
+
function resolveDrawingColor(node) {
|
|
1098
|
+
if (!node)
|
|
1099
|
+
return undefined;
|
|
1100
|
+
// Direct RGB.
|
|
1101
|
+
const srgb = get(node, "a:srgbClr.a$.val") ?? get(node, "srgbClr.a$.val");
|
|
1102
|
+
if (srgb) {
|
|
1103
|
+
const lumMod = readPctNumber(get(node, "a:srgbClr.a:lumMod.a$.val") ??
|
|
1104
|
+
get(node, "srgbClr.lumMod.a$.val"));
|
|
1105
|
+
const lumOff = readPctNumber(get(node, "a:srgbClr.a:lumOff.a$.val") ??
|
|
1106
|
+
get(node, "srgbClr.lumOff.a$.val"));
|
|
1107
|
+
const shade = readPctNumber(get(node, "a:srgbClr.a:shade.a$.val") ??
|
|
1108
|
+
get(node, "srgbClr.shade.a$.val"));
|
|
1109
|
+
const tint = readPctNumber(get(node, "a:srgbClr.a:tint.a$.val") ??
|
|
1110
|
+
get(node, "srgbClr.tint.a$.val"));
|
|
1111
|
+
return applyLumModOff(`#${srgb}`, lumMod, lumOff, shade, tint);
|
|
1112
|
+
}
|
|
1113
|
+
// Theme reference.
|
|
1114
|
+
const schemeName = get(node, "a:schemeClr.a$.val") ?? get(node, "schemeClr.a$.val");
|
|
1115
|
+
if (typeof schemeName === "string") {
|
|
1116
|
+
const base = OFFICE_THEME_FALLBACK[schemeName] ?? "#000000";
|
|
1117
|
+
const lumMod = readPctNumber(get(node, "a:schemeClr.a:lumMod.a$.val") ??
|
|
1118
|
+
get(node, "schemeClr.lumMod.a$.val"));
|
|
1119
|
+
const lumOff = readPctNumber(get(node, "a:schemeClr.a:lumOff.a$.val") ??
|
|
1120
|
+
get(node, "schemeClr.lumOff.a$.val"));
|
|
1121
|
+
const shade = readPctNumber(get(node, "a:schemeClr.a:shade.a$.val") ??
|
|
1122
|
+
get(node, "schemeClr.shade.a$.val"));
|
|
1123
|
+
const tint = readPctNumber(get(node, "a:schemeClr.a:tint.a$.val") ??
|
|
1124
|
+
get(node, "schemeClr.tint.a$.val"));
|
|
1125
|
+
return applyLumModOff(base, lumMod, lumOff, shade, tint);
|
|
1126
|
+
}
|
|
1127
|
+
return undefined;
|
|
1128
|
+
}
|
|
1129
|
+
/** Parse an OOXML percentage stored as 1000ths-of-a-percent → 0..1. */
|
|
1130
|
+
function readPctNumber(val) {
|
|
1131
|
+
if (val === undefined || val === null || val === "")
|
|
1132
|
+
return undefined;
|
|
1133
|
+
const n = Number(val);
|
|
1134
|
+
return Number.isFinite(n) ? n / 100000 : undefined;
|
|
1135
|
+
}
|
|
1136
|
+
/**
|
|
1137
|
+
* Extract a `ChartTextFormat` from an OOXML text-properties node. The
|
|
1138
|
+
* input is typically `<c:txPr>` or `<c:title><c:tx><c:rich>` — both
|
|
1139
|
+
* use the same `<a:p><a:pPr><a:defRPr>…</a:defRPr></a:pPr></a:p>`
|
|
1140
|
+
* shape to express paragraph-default character properties.
|
|
1141
|
+
*
|
|
1142
|
+
* Returns `undefined` when the node carries no formatting (so the
|
|
1143
|
+
* caller doesn't emit an empty `textFormat` field).
|
|
1144
|
+
*/
|
|
1145
|
+
function extractTextProperties(txPr) {
|
|
1146
|
+
if (!txPr)
|
|
1147
|
+
return undefined;
|
|
1148
|
+
// The font/color lives on `<a:p><a:pPr><a:defRPr>`. Some writers put
|
|
1149
|
+
// it directly on `<a:r><a:rPr>` for rich titles — accept both.
|
|
1150
|
+
const defRPr = get(txPr, "a:p.a:pPr.a:defRPr") ??
|
|
1151
|
+
get(txPr, "p.pPr.defRPr") ??
|
|
1152
|
+
get(txPr, "a:p.a:r.a:rPr") ??
|
|
1153
|
+
get(txPr, "p.r.rPr");
|
|
1154
|
+
if (!defRPr)
|
|
1155
|
+
return undefined;
|
|
1156
|
+
const result = {};
|
|
1157
|
+
// Font size — `sz` is in 100ths of a point (`sz="900"` → 9pt).
|
|
1158
|
+
const szAttr = get(defRPr, "a$.sz") ?? get(defRPr, "a$.t$");
|
|
1159
|
+
if (szAttr !== undefined && szAttr !== null && szAttr !== "") {
|
|
1160
|
+
const sz = Number(szAttr);
|
|
1161
|
+
if (Number.isFinite(sz) && sz > 0)
|
|
1162
|
+
result.fontSize = sz / 100;
|
|
1163
|
+
}
|
|
1164
|
+
// Bold / italic / underline / strikethrough.
|
|
1165
|
+
const bAttr = get(defRPr, "a$.b");
|
|
1166
|
+
if (bAttr === "1" || bAttr === 1 || bAttr === true)
|
|
1167
|
+
result.bold = true;
|
|
1168
|
+
const iAttr = get(defRPr, "a$.i");
|
|
1169
|
+
if (iAttr === "1" || iAttr === 1 || iAttr === true)
|
|
1170
|
+
result.italic = true;
|
|
1171
|
+
const uAttr = get(defRPr, "a$.u");
|
|
1172
|
+
if (typeof uAttr === "string" && uAttr !== "none")
|
|
1173
|
+
result.underline = true;
|
|
1174
|
+
const strikeAttr = get(defRPr, "a$.strike");
|
|
1175
|
+
if (typeof strikeAttr === "string" && strikeAttr !== "noStrike") {
|
|
1176
|
+
result.strikethrough = true;
|
|
1177
|
+
}
|
|
1178
|
+
// Font family — `<a:latin typeface="…">`.
|
|
1179
|
+
const latin = get(defRPr, "a:latin.a$.typeface") ?? get(defRPr, "latin.a$.typeface");
|
|
1180
|
+
if (typeof latin === "string" && latin)
|
|
1181
|
+
result.fontFamily = latin;
|
|
1182
|
+
// Color — `<a:solidFill>` containing `<a:srgbClr>` or `<a:schemeClr>`.
|
|
1183
|
+
const solidFill = get(defRPr, "a:solidFill") ?? get(defRPr, "solidFill");
|
|
1184
|
+
const color = resolveDrawingColor(solidFill);
|
|
1185
|
+
if (color)
|
|
1186
|
+
result.color = color;
|
|
1187
|
+
return Object.keys(result).length ? result : undefined;
|
|
1188
|
+
}
|
|
1189
|
+
/**
|
|
1190
|
+
* Read bar/column `<c:grouping>`, `<c:overlap>`, `<c:gapWidth>` —
|
|
1191
|
+
* controls how multi-series bars are arranged (clustered / stacked /
|
|
1192
|
+
* percent-stacked). Looks inside `<c:barChart>` / `<c:bar3DChart>`
|
|
1193
|
+
* containers; returns `undefined` if no settings are present so the
|
|
1194
|
+
* renderer keeps its defaults.
|
|
1195
|
+
*/
|
|
1196
|
+
function extractBarLayoutOptions(plotArea) {
|
|
1197
|
+
const bar = get(plotArea, "c:barChart") ??
|
|
1198
|
+
get(plotArea, "barChart") ??
|
|
1199
|
+
get(plotArea, "c:bar3DChart") ??
|
|
1200
|
+
get(plotArea, "bar3DChart");
|
|
1201
|
+
if (!bar)
|
|
1202
|
+
return undefined;
|
|
1203
|
+
// `<c:grouping val="clustered|stacked|percentStacked|standard"/>`.
|
|
1204
|
+
// `standard` is "side-by-side, no stacking" — same as `clustered`
|
|
1205
|
+
// semantically; map both to UNSTACKED.
|
|
1206
|
+
const groupingVal = get(bar, "c:grouping.a$.val") ??
|
|
1207
|
+
get(bar, "grouping.a$.val") ??
|
|
1208
|
+
get(bar, "c:grouping.a$.t$") ??
|
|
1209
|
+
get(bar, "grouping.a$.t$");
|
|
1210
|
+
let stackedType;
|
|
1211
|
+
switch (groupingVal) {
|
|
1212
|
+
case "stacked":
|
|
1213
|
+
stackedType = "STACKED";
|
|
1214
|
+
break;
|
|
1215
|
+
case "percentStacked":
|
|
1216
|
+
stackedType = "PERCENT_STACKED";
|
|
1217
|
+
break;
|
|
1218
|
+
case "clustered":
|
|
1219
|
+
case "standard":
|
|
1220
|
+
stackedType = "UNSTACKED";
|
|
1221
|
+
break;
|
|
1222
|
+
}
|
|
1223
|
+
const overlapVal = readNumber(get(bar, "c:overlap.a$.val") ?? get(bar, "overlap.a$.val"));
|
|
1224
|
+
const gapWidthVal = readNumber(get(bar, "c:gapWidth.a$.val") ?? get(bar, "gapWidth.a$.val"));
|
|
1225
|
+
const result = {};
|
|
1226
|
+
if (stackedType)
|
|
1227
|
+
result.stackedType = stackedType;
|
|
1228
|
+
if (typeof overlapVal === "number")
|
|
1229
|
+
result.overlap = overlapVal;
|
|
1230
|
+
if (typeof gapWidthVal === "number")
|
|
1231
|
+
result.gapWidth = gapWidthVal;
|
|
1232
|
+
return Object.keys(result).length ? result : undefined;
|
|
1233
|
+
}
|
|
1234
|
+
/**
|
|
1235
|
+
* Extract chart-level `<c:scatterStyle val=…>`. Excel default is
|
|
1236
|
+
* "lineMarker" (straight lines + markers). Returns undefined for
|
|
1237
|
+
* non-scatter charts.
|
|
1238
|
+
*/
|
|
1239
|
+
function extractScatterStyle(plotArea) {
|
|
1240
|
+
const val = get(plotArea, "c:scatterChart.c:scatterStyle.a$.val") ??
|
|
1241
|
+
get(plotArea, "scatterChart.scatterStyle.a$.val") ??
|
|
1242
|
+
get(plotArea, "c:scatterChart.c:scatterStyle.a$.t$") ??
|
|
1243
|
+
get(plotArea, "scatterChart.scatterStyle.a$.t$");
|
|
1244
|
+
return typeof val === "string" && val ? val : undefined;
|
|
1245
|
+
}
|
|
1246
|
+
function extractSeriesTitleFromSeries(ser) {
|
|
1247
|
+
// Prefer cached textual value if present
|
|
1248
|
+
const cacheEntries = get(ser, "c:tx.c:strRef.c:strCache.c:pt");
|
|
1249
|
+
if (cacheEntries) {
|
|
1250
|
+
const pts = Array.isArray(cacheEntries) ? cacheEntries : [cacheEntries];
|
|
1251
|
+
for (const pt of pts) {
|
|
1252
|
+
const value = coerceTitleText(get(pt, "c:v.t$") ||
|
|
1253
|
+
get(pt, "c:v") ||
|
|
1254
|
+
extractRichText(get(pt, "c:rich")));
|
|
1255
|
+
if (value) {
|
|
1256
|
+
return value;
|
|
1257
|
+
}
|
|
1258
|
+
}
|
|
1259
|
+
}
|
|
1260
|
+
// Fall back to literal
|
|
1261
|
+
const literal = coerceTitleText(get(ser, "c:tx.c:v.t$") ||
|
|
1262
|
+
get(ser, "c:tx.c:v") ||
|
|
1263
|
+
extractRichText(get(ser, "c:tx.c:rich")));
|
|
1264
|
+
if (literal) {
|
|
1265
|
+
return literal;
|
|
1266
|
+
}
|
|
1267
|
+
return undefined;
|
|
1268
|
+
}
|
|
1269
|
+
/**
|
|
1270
|
+
* fast-xml-parser converts numeric `<c:v>` values (e.g. a series header
|
|
1271
|
+
* like 2024) into numbers; normalize primitives to strings and drop
|
|
1272
|
+
* anything else so titles are always strings downstream.
|
|
1273
|
+
*/
|
|
1274
|
+
function coerceTitleText(value) {
|
|
1275
|
+
if (typeof value === "string")
|
|
1276
|
+
return value;
|
|
1277
|
+
if (typeof value === "number" || typeof value === "boolean") {
|
|
1278
|
+
return String(value);
|
|
1279
|
+
}
|
|
1280
|
+
return undefined;
|
|
1281
|
+
}
|
|
1282
|
+
function extractRichText(node) {
|
|
1283
|
+
if (!node) {
|
|
1284
|
+
return undefined;
|
|
1285
|
+
}
|
|
1286
|
+
const paragraphs = getWithFallback(node, ["a:p", "p"]);
|
|
1287
|
+
const paragraphArray = Array.isArray(paragraphs)
|
|
1288
|
+
? paragraphs
|
|
1289
|
+
: paragraphs
|
|
1290
|
+
? [paragraphs]
|
|
1291
|
+
: [];
|
|
1292
|
+
const fragments = [];
|
|
1293
|
+
for (const paragraph of paragraphArray) {
|
|
1294
|
+
const runs = getWithFallback(paragraph, ["a:r", "r"]);
|
|
1295
|
+
const runArray = Array.isArray(runs) ? runs : runs ? [runs] : [paragraph];
|
|
1296
|
+
for (const run of runArray) {
|
|
1297
|
+
const text = getWithFallback(run, ["a:t.t$", "a:t"]) ||
|
|
1298
|
+
getWithFallback(run, ["t$", "t"]);
|
|
1299
|
+
if (typeof text === "string") {
|
|
1300
|
+
fragments.push(text);
|
|
1301
|
+
}
|
|
1302
|
+
}
|
|
1303
|
+
}
|
|
1304
|
+
if (fragments.length > 0) {
|
|
1305
|
+
return fragments.join("");
|
|
1306
|
+
}
|
|
1307
|
+
return undefined;
|
|
1308
|
+
}
|
|
1309
|
+
function normalizeHexColor(value) {
|
|
1310
|
+
if (!value)
|
|
1311
|
+
return undefined;
|
|
1312
|
+
const trimmed = value.toString().trim().replace(/^#/, "");
|
|
1313
|
+
if (!trimmed)
|
|
1314
|
+
return undefined;
|
|
1315
|
+
const sanitized = trimmed.replace(/[^0-9a-fA-F]/g, "").toUpperCase();
|
|
1316
|
+
if (sanitized.length === 6) {
|
|
1317
|
+
return `#${sanitized}`;
|
|
1318
|
+
}
|
|
1319
|
+
if (sanitized.length === 8) {
|
|
1320
|
+
return `#${sanitized.slice(-6)}`;
|
|
1321
|
+
}
|
|
1322
|
+
return undefined;
|
|
1323
|
+
}
|
|
1324
|
+
function extractSeriesColorInfo(ser) {
|
|
1325
|
+
const result = {};
|
|
1326
|
+
// <a:noFill/> on the series shape properties — transparent series.
|
|
1327
|
+
// OOXML can express this as the singleton element with no children;
|
|
1328
|
+
// the absence of a:solidFill / a:gradFill is also a signal but
|
|
1329
|
+
// brittler. fast-xml-parser emits `noFill: ""` or `noFill: {}` for
|
|
1330
|
+
// empty elements — both falsy by-value but truthy by-presence.
|
|
1331
|
+
const noFillFill = get(ser, "c:spPr.a:noFill") ?? get(ser, "spPr.a:noFill");
|
|
1332
|
+
if (noFillFill !== undefined) {
|
|
1333
|
+
result.noFill = true;
|
|
1334
|
+
}
|
|
1335
|
+
// Try the full <a:solidFill> shape first — handles both srgbClr
|
|
1336
|
+
// and schemeClr (with lumMod/lumOff/shade/tint/alpha modifiers).
|
|
1337
|
+
const solidFillNode = get(ser, "c:spPr.a:solidFill") ?? get(ser, "spPr.a:solidFill");
|
|
1338
|
+
const resolvedFill = resolveDrawingColor(solidFillNode);
|
|
1339
|
+
// Extract main color - check both fill color (for bar/column) and line color (for line/area charts)
|
|
1340
|
+
const fillColorValue = getWithFallback(ser, [
|
|
1341
|
+
"c:spPr.a:solidFill.a:srgbClr.a$.val",
|
|
1342
|
+
"c:spPr.a:solidFill.a:srgbClr.a$.t$",
|
|
1343
|
+
"c:spPr.a:solidFill.a:srgbClr.a$.rgb",
|
|
1344
|
+
"c:spPr.solidFill.srgbClr.a$.val",
|
|
1345
|
+
"spPr.a:solidFill.a:srgbClr.a$.val",
|
|
1346
|
+
"spPr.solidFill.srgbClr.a$.val",
|
|
1347
|
+
]);
|
|
1348
|
+
// Line color for line/scatter/area charts
|
|
1349
|
+
const lineColorValue = getWithFallback(ser, [
|
|
1350
|
+
"c:spPr.a:ln.a:solidFill.a:srgbClr.a$.val",
|
|
1351
|
+
"c:spPr.a:ln.a:solidFill.a:srgbClr.a$.t$",
|
|
1352
|
+
"c:spPr.a:ln.a:solidFill.a:srgbClr.a$.rgb",
|
|
1353
|
+
"c:spPr.a:ln.solidFill.srgbClr.a$.val",
|
|
1354
|
+
"spPr.a:ln.a:solidFill.a:srgbClr.a$.val",
|
|
1355
|
+
"spPr.ln.solidFill.srgbClr.a$.val",
|
|
1356
|
+
]);
|
|
1357
|
+
// Use fill color if available, otherwise use line color.
|
|
1358
|
+
// resolvedFill handles schemeClr; the legacy normalize call handles
|
|
1359
|
+
// the explicit srgbClr path (kept as a fallback). When noFill is set
|
|
1360
|
+
// we leave `color` undefined — the renderer treats the series as
|
|
1361
|
+
// transparent.
|
|
1362
|
+
if (!result.noFill) {
|
|
1363
|
+
result.color =
|
|
1364
|
+
resolvedFill ??
|
|
1365
|
+
normalizeHexColor(fillColorValue) ??
|
|
1366
|
+
normalizeHexColor(lineColorValue);
|
|
1367
|
+
}
|
|
1368
|
+
// Gradient fill — DrawingML `<a:gradFill>` inside `<c:spPr>`. Stops
|
|
1369
|
+
// come in EMU per-mille (0..100000); positions get normalized back
|
|
1370
|
+
// to 0..1 for our shape. Linear angle is in 60000ths of a degree;
|
|
1371
|
+
// path gradients carry insets via `<a:path><a:fillToRect/>`.
|
|
1372
|
+
const gradFill = getWithFallback(ser, [
|
|
1373
|
+
"c:spPr.a:gradFill",
|
|
1374
|
+
"spPr.a:gradFill",
|
|
1375
|
+
"c:spPr.gradFill",
|
|
1376
|
+
]);
|
|
1377
|
+
if (gradFill) {
|
|
1378
|
+
const rawStops = getWithFallback(gradFill, [
|
|
1379
|
+
"a:gsLst.a:gs",
|
|
1380
|
+
"gsLst.gs",
|
|
1381
|
+
]);
|
|
1382
|
+
const stopList = rawStops
|
|
1383
|
+
? Array.isArray(rawStops)
|
|
1384
|
+
? rawStops
|
|
1385
|
+
: [rawStops]
|
|
1386
|
+
: [];
|
|
1387
|
+
const stops = [];
|
|
1388
|
+
for (const stop of stopList) {
|
|
1389
|
+
const posRaw = stop?.a$?.pos;
|
|
1390
|
+
const position = posRaw != null
|
|
1391
|
+
? Math.max(0, Math.min(1, parseFloat(posRaw) / 100000))
|
|
1392
|
+
: NaN;
|
|
1393
|
+
if (!Number.isFinite(position))
|
|
1394
|
+
continue;
|
|
1395
|
+
const hex = getWithFallback(stop, [
|
|
1396
|
+
"a:srgbClr.a$.val",
|
|
1397
|
+
"srgbClr.a$.val",
|
|
1398
|
+
]);
|
|
1399
|
+
const color = normalizeHexColor(hex);
|
|
1400
|
+
if (!color)
|
|
1401
|
+
continue;
|
|
1402
|
+
stops.push({ position, color });
|
|
1403
|
+
}
|
|
1404
|
+
if (stops.length > 0) {
|
|
1405
|
+
stops.sort((a, b) => a.position - b.position);
|
|
1406
|
+
const lin = getWithFallback(gradFill, ["a:lin.a$", "lin.a$"]);
|
|
1407
|
+
const path = getWithFallback(gradFill, ["a:path", "path"]);
|
|
1408
|
+
if (path) {
|
|
1409
|
+
const insets = getWithFallback(path, [
|
|
1410
|
+
"a:fillToRect.a$",
|
|
1411
|
+
"fillToRect.a$",
|
|
1412
|
+
]);
|
|
1413
|
+
const emuToFraction = (v) => {
|
|
1414
|
+
if (v == null)
|
|
1415
|
+
return undefined;
|
|
1416
|
+
const n = parseFloat(v);
|
|
1417
|
+
return Number.isFinite(n) ? n / 100000 : undefined;
|
|
1418
|
+
};
|
|
1419
|
+
result.gradient = {
|
|
1420
|
+
type: "path",
|
|
1421
|
+
left: emuToFraction(insets?.l),
|
|
1422
|
+
right: emuToFraction(insets?.r),
|
|
1423
|
+
top: emuToFraction(insets?.t),
|
|
1424
|
+
bottom: emuToFraction(insets?.b),
|
|
1425
|
+
stops,
|
|
1426
|
+
};
|
|
1427
|
+
}
|
|
1428
|
+
else {
|
|
1429
|
+
const ang = lin?.ang != null ? parseFloat(lin.ang) : 0;
|
|
1430
|
+
result.gradient = {
|
|
1431
|
+
type: "linear",
|
|
1432
|
+
degree: Number.isFinite(ang) ? ang / 60000 : 0,
|
|
1433
|
+
stops,
|
|
1434
|
+
};
|
|
1435
|
+
}
|
|
1436
|
+
}
|
|
1437
|
+
}
|
|
1438
|
+
// Extract invertIfNegative flag
|
|
1439
|
+
const invertIfNegativeVal = getWithFallback(ser, [
|
|
1440
|
+
"c:invertIfNegative.a$.val",
|
|
1441
|
+
"c:invertIfNegative.t$",
|
|
1442
|
+
"c:invertIfNegative",
|
|
1443
|
+
"invertIfNegative.a$.val",
|
|
1444
|
+
"invertIfNegative",
|
|
1445
|
+
]);
|
|
1446
|
+
// Value can be "1", "true", true, or 1
|
|
1447
|
+
if (invertIfNegativeVal !== undefined) {
|
|
1448
|
+
result.invertIfNegative =
|
|
1449
|
+
invertIfNegativeVal === "1" ||
|
|
1450
|
+
invertIfNegativeVal === true ||
|
|
1451
|
+
invertIfNegativeVal === 1 ||
|
|
1452
|
+
invertIfNegativeVal === "true";
|
|
1453
|
+
}
|
|
1454
|
+
// Extract negative fill color from extLst (Excel 2010+ format)
|
|
1455
|
+
// The negative color is stored in c:extLst.c:ext with specific URI
|
|
1456
|
+
const extLst = getWithFallback(ser, [
|
|
1457
|
+
"c:extLst.c:ext",
|
|
1458
|
+
"c:extLst",
|
|
1459
|
+
"extLst.ext",
|
|
1460
|
+
"extLst",
|
|
1461
|
+
]);
|
|
1462
|
+
if (extLst) {
|
|
1463
|
+
const extArray = Array.isArray(extLst) ? extLst : [extLst];
|
|
1464
|
+
for (const ext of extArray) {
|
|
1465
|
+
// Look for invertSolidFillFmt extension (negative fill format)
|
|
1466
|
+
const negFillColor = getWithFallback(ext, [
|
|
1467
|
+
"c14:invertSolidFillFmt.c14:spPr.a:solidFill.a:srgbClr.a$.val",
|
|
1468
|
+
"c14:invertSolidFillFmt.c14:spPr.a:solidFill.a:srgbClr.a$.rgb",
|
|
1469
|
+
"invertSolidFillFmt.spPr.solidFill.srgbClr.a$.val",
|
|
1470
|
+
// Also check for c15 namespace (Excel 2013+)
|
|
1471
|
+
"c15:invertSolidFillFmt.c15:spPr.a:solidFill.a:srgbClr.a$.val",
|
|
1472
|
+
]);
|
|
1473
|
+
if (negFillColor) {
|
|
1474
|
+
result.negativeColor = normalizeHexColor(negFillColor);
|
|
1475
|
+
// If we have a negative color, invertIfNegative should be true
|
|
1476
|
+
if (result.negativeColor) {
|
|
1477
|
+
result.invertIfNegative = true;
|
|
1478
|
+
}
|
|
1479
|
+
break;
|
|
1480
|
+
}
|
|
1481
|
+
}
|
|
1482
|
+
}
|
|
1483
|
+
return result;
|
|
1484
|
+
}
|
|
1485
|
+
export function extractChartDomains(plotArea, chartType, sheetId, resolveSheetId) {
|
|
1486
|
+
const domains = [];
|
|
1487
|
+
// Search all chart type containers for domains. For XY charts the
|
|
1488
|
+
// "domain" is the X values (c:xVal); for everything else it's the
|
|
1489
|
+
// category axis (c:cat). Per-container loop covers combo charts.
|
|
1490
|
+
for (const container of CHART_TYPE_CONTAINERS) {
|
|
1491
|
+
const chartElement = getWithFallback(plotArea, container.paths);
|
|
1492
|
+
if (!chartElement)
|
|
1493
|
+
continue;
|
|
1494
|
+
const chartElements = Array.isArray(chartElement)
|
|
1495
|
+
? chartElement
|
|
1496
|
+
: [chartElement];
|
|
1497
|
+
for (const singleChartElement of chartElements) {
|
|
1498
|
+
const seriesElements = getWithFallback(singleChartElement, [
|
|
1499
|
+
"c:ser",
|
|
1500
|
+
"ser",
|
|
1501
|
+
]);
|
|
1502
|
+
if (!seriesElements)
|
|
1503
|
+
continue;
|
|
1504
|
+
const seriesArray = Array.isArray(seriesElements)
|
|
1505
|
+
? seriesElements
|
|
1506
|
+
: [seriesElements];
|
|
1507
|
+
for (const ser of seriesArray) {
|
|
1508
|
+
const domainRef = container.isXY
|
|
1509
|
+
? getWithFallback(ser, X_VAL_REF_PATHS)
|
|
1510
|
+
: getWithFallback(ser, CAT_REF_PATHS);
|
|
1511
|
+
if (domainRef) {
|
|
1512
|
+
const sources = resolveSourcesFromRef(domainRef, sheetId, resolveSheetId);
|
|
1513
|
+
if (sources.length) {
|
|
1514
|
+
domains.push({ sources });
|
|
1515
|
+
// XY charts may carry per-series domains that differ — keep
|
|
1516
|
+
// the first one we encounter for the dataRange anchor, but
|
|
1517
|
+
// continue collecting so the renderer has a per-series X
|
|
1518
|
+
// when it needs one. For non-XY, first-found is enough.
|
|
1519
|
+
if (!container.isXY)
|
|
1520
|
+
return domains;
|
|
1521
|
+
}
|
|
1522
|
+
}
|
|
1523
|
+
else if (!container.isXY) {
|
|
1524
|
+
// No cell ref — check for an inline `<c:strLit>`/`<c:numLit>`
|
|
1525
|
+
// category list (e.g. `["Estimated", "Actual"]` embedded
|
|
1526
|
+
// directly in the chart XML). Same for the series val side,
|
|
1527
|
+
// but domains is by far the most common case.
|
|
1528
|
+
const catNode = get(ser, "c:cat") ?? get(ser, "cat");
|
|
1529
|
+
const inlineValues = extractInlineValues(catNode);
|
|
1530
|
+
if (inlineValues && inlineValues.length) {
|
|
1531
|
+
domains.push({ sources: [], inlineValues });
|
|
1532
|
+
return domains;
|
|
1533
|
+
}
|
|
1534
|
+
}
|
|
1535
|
+
}
|
|
1536
|
+
if (domains.length && container.isXY)
|
|
1537
|
+
return domains;
|
|
1538
|
+
}
|
|
1539
|
+
}
|
|
1540
|
+
// Fallback for chart types not in CHART_TYPE_CONTAINERS
|
|
1541
|
+
// (pie/doughnut/radar/stock/ofPie/scatter/bubble outside combos).
|
|
1542
|
+
const seriesElements = getSeriesElements(plotArea, chartType);
|
|
1543
|
+
if (seriesElements) {
|
|
1544
|
+
const seriesArray = Array.isArray(seriesElements)
|
|
1545
|
+
? seriesElements
|
|
1546
|
+
: [seriesElements];
|
|
1547
|
+
for (const ser of seriesArray) {
|
|
1548
|
+
const domainRef = isXYChart(chartType)
|
|
1549
|
+
? getWithFallback(ser, X_VAL_REF_PATHS)
|
|
1550
|
+
: getWithFallback(ser, CAT_REF_PATHS);
|
|
1551
|
+
if (domainRef) {
|
|
1552
|
+
const sources = resolveSourcesFromRef(domainRef, sheetId, resolveSheetId);
|
|
1553
|
+
if (sources.length) {
|
|
1554
|
+
domains.push({ sources });
|
|
1555
|
+
if (!isXYChart(chartType))
|
|
1556
|
+
break;
|
|
1557
|
+
}
|
|
1558
|
+
}
|
|
1559
|
+
else if (!isXYChart(chartType)) {
|
|
1560
|
+
const catNode = get(ser, "c:cat") ?? get(ser, "cat");
|
|
1561
|
+
const inlineValues = extractInlineValues(catNode);
|
|
1562
|
+
if (inlineValues && inlineValues.length) {
|
|
1563
|
+
domains.push({ sources: [], inlineValues });
|
|
1564
|
+
break;
|
|
1565
|
+
}
|
|
1566
|
+
}
|
|
1567
|
+
}
|
|
1568
|
+
}
|
|
1569
|
+
return domains;
|
|
1570
|
+
}
|
|
1571
|
+
/**
|
|
1572
|
+
* Bubble charts have a third dimension — bubble size — referenced
|
|
1573
|
+
* via `<c:bubbleSize><c:numRef><c:f>` on each series. Returns one
|
|
1574
|
+
* `ChartData` entry per series (matches the shape used by `domains` /
|
|
1575
|
+
* `series`).
|
|
1576
|
+
*/
|
|
1577
|
+
export function extractChartBubbleSizes(plotArea, sheetId, resolveSheetId) {
|
|
1578
|
+
const sizes = [];
|
|
1579
|
+
const seriesElements = getSeriesElements(plotArea, "bubble");
|
|
1580
|
+
if (!seriesElements)
|
|
1581
|
+
return sizes;
|
|
1582
|
+
const seriesArray = Array.isArray(seriesElements)
|
|
1583
|
+
? seriesElements
|
|
1584
|
+
: [seriesElements];
|
|
1585
|
+
for (const ser of seriesArray) {
|
|
1586
|
+
const sizeRef = getWithFallback(ser, BUBBLE_SIZE_REF_PATHS);
|
|
1587
|
+
if (!sizeRef)
|
|
1588
|
+
continue;
|
|
1589
|
+
const sources = resolveSourcesFromRef(sizeRef, sheetId, resolveSheetId);
|
|
1590
|
+
if (sources.length)
|
|
1591
|
+
sizes.push({ sources });
|
|
1592
|
+
}
|
|
1593
|
+
return sizes;
|
|
1594
|
+
}
|
|
1595
|
+
export function extractChartPosition(anchor, sheetId, sheetDimensions) {
|
|
1596
|
+
try {
|
|
1597
|
+
// Handle oneCellAnchor
|
|
1598
|
+
const oneCellFrom = getWithFallback(anchor, ["xdr:from", "from"]) ?? undefined;
|
|
1599
|
+
const hasTwoCellTarget = getWithFallback(anchor, ["xdr:to", "to"]) !== undefined;
|
|
1600
|
+
if (oneCellFrom && !hasTwoCellTarget) {
|
|
1601
|
+
const extNode = getWithFallback(anchor, ["xdr:ext", "ext"]);
|
|
1602
|
+
const extAttributes = (extNode && (extNode.a$ ?? extNode)) || undefined;
|
|
1603
|
+
const col = toInt(getWithFallback(oneCellFrom, ["xdr:col", "col"]));
|
|
1604
|
+
const row = toInt(getWithFallback(oneCellFrom, ["xdr:row", "row"]));
|
|
1605
|
+
const colOff = toInt(getWithFallback(oneCellFrom, ["xdr:colOff", "colOff"]));
|
|
1606
|
+
const rowOff = toInt(getWithFallback(oneCellFrom, ["xdr:rowOff", "rowOff"]));
|
|
1607
|
+
// Convert EMU to pixels (1 EMU = 1/9525 pixels)
|
|
1608
|
+
const width = extAttributes && extAttributes.cx
|
|
1609
|
+
? Math.round(parseInt(String(extAttributes.cx), 10) / 9525)
|
|
1610
|
+
: 400;
|
|
1611
|
+
const height = extAttributes && extAttributes.cy
|
|
1612
|
+
? Math.round(parseInt(String(extAttributes.cy), 10) / 9525)
|
|
1613
|
+
: 300;
|
|
1614
|
+
return {
|
|
1615
|
+
sheetId,
|
|
1616
|
+
overlayPosition: {
|
|
1617
|
+
anchorCell: {
|
|
1618
|
+
rowIndex: row + 1,
|
|
1619
|
+
columnIndex: col + 1,
|
|
1620
|
+
},
|
|
1621
|
+
offsetXPixels: Math.round(colOff / 9525),
|
|
1622
|
+
offsetYPixels: Math.round(rowOff / 9525),
|
|
1623
|
+
widthPixels: width,
|
|
1624
|
+
heightPixels: height,
|
|
1625
|
+
},
|
|
1626
|
+
};
|
|
1627
|
+
}
|
|
1628
|
+
// Handle twoCellAnchor
|
|
1629
|
+
const fromCell = getWithFallback(anchor, ["xdr:from", "from"]) ?? undefined;
|
|
1630
|
+
const toCell = getWithFallback(anchor, ["xdr:to", "to"]) ?? undefined;
|
|
1631
|
+
if (fromCell && toCell) {
|
|
1632
|
+
const col = toInt(getWithFallback(fromCell, ["xdr:col", "col"]));
|
|
1633
|
+
const row = toInt(getWithFallback(fromCell, ["xdr:row", "row"]));
|
|
1634
|
+
const colOff = toInt(getWithFallback(fromCell, ["xdr:colOff", "colOff"]));
|
|
1635
|
+
const rowOff = toInt(getWithFallback(fromCell, ["xdr:rowOff", "rowOff"]));
|
|
1636
|
+
const toCol = toInt(getWithFallback(toCell, ["xdr:col", "col"]));
|
|
1637
|
+
const toRow = toInt(getWithFallback(toCell, ["xdr:row", "row"]));
|
|
1638
|
+
const toColOff = toInt(getWithFallback(toCell, ["xdr:colOff", "colOff"]));
|
|
1639
|
+
const toRowOff = toInt(getWithFallback(toCell, ["xdr:rowOff", "rowOff"]));
|
|
1640
|
+
// Default cell dimensions in pixels
|
|
1641
|
+
const DEFAULT_COL_WIDTH_PX = sheetDimensions?.defaultColumnWidth ?? 64;
|
|
1642
|
+
const DEFAULT_ROW_HEIGHT_PX = sheetDimensions?.defaultRowHeight ?? 20;
|
|
1643
|
+
// Helper to get column width in pixels
|
|
1644
|
+
const getColWidth = (colIndex) => {
|
|
1645
|
+
return (sheetDimensions?.columnWidths?.get(colIndex + 1) ??
|
|
1646
|
+
DEFAULT_COL_WIDTH_PX);
|
|
1647
|
+
};
|
|
1648
|
+
// Helper to get row height in pixels
|
|
1649
|
+
const getRowHeight = (rowIndex) => {
|
|
1650
|
+
return (sheetDimensions?.rowHeights?.get(rowIndex + 1) ??
|
|
1651
|
+
DEFAULT_ROW_HEIGHT_PX);
|
|
1652
|
+
};
|
|
1653
|
+
// Calculate width by summing actual column widths
|
|
1654
|
+
let widthPx = 0;
|
|
1655
|
+
for (let c = col; c < toCol; c++) {
|
|
1656
|
+
widthPx += getColWidth(c);
|
|
1657
|
+
}
|
|
1658
|
+
// Subtract offset into first cell, add offset into last cell
|
|
1659
|
+
// Offsets are in EMUs, convert to pixels
|
|
1660
|
+
widthPx -= Math.round(colOff / 9525);
|
|
1661
|
+
widthPx += Math.round(toColOff / 9525);
|
|
1662
|
+
// Calculate height by summing actual row heights
|
|
1663
|
+
let heightPx = 0;
|
|
1664
|
+
for (let r = row; r < toRow; r++) {
|
|
1665
|
+
heightPx += getRowHeight(r);
|
|
1666
|
+
}
|
|
1667
|
+
// Subtract offset into first cell, add offset into last cell
|
|
1668
|
+
heightPx -= Math.round(rowOff / 9525);
|
|
1669
|
+
heightPx += Math.round(toRowOff / 9525);
|
|
1670
|
+
const width = Math.max(widthPx, 100);
|
|
1671
|
+
const height = Math.max(heightPx, 100);
|
|
1672
|
+
return {
|
|
1673
|
+
sheetId,
|
|
1674
|
+
overlayPosition: {
|
|
1675
|
+
anchorCell: {
|
|
1676
|
+
rowIndex: row + 1,
|
|
1677
|
+
columnIndex: col + 1,
|
|
1678
|
+
},
|
|
1679
|
+
offsetXPixels: Math.round(colOff / 9525),
|
|
1680
|
+
offsetYPixels: Math.round(rowOff / 9525),
|
|
1681
|
+
widthPixels: width,
|
|
1682
|
+
heightPixels: height,
|
|
1683
|
+
},
|
|
1684
|
+
};
|
|
1685
|
+
}
|
|
1686
|
+
return null;
|
|
1687
|
+
}
|
|
1688
|
+
catch (err) {
|
|
1689
|
+
console.log("Error extracting chart position:", err);
|
|
1690
|
+
return null;
|
|
1691
|
+
}
|
|
1692
|
+
}
|
|
1693
|
+
// -------------------------- Phase 5 helpers --------------------------
|
|
1694
|
+
// Read OOXML legend (<c:legend>), data labels (<c:dLbls>), and axis
|
|
1695
|
+
// formatting (<c:valAx>, <c:catAx>) elements into the engine's ChartSpec
|
|
1696
|
+
// shape. All helpers return `undefined` when nothing meaningful is set,
|
|
1697
|
+
// so callers can spread the result without polluting the spec.
|
|
1698
|
+
const readBoolean = (val) => {
|
|
1699
|
+
if (val === undefined || val === null)
|
|
1700
|
+
return undefined;
|
|
1701
|
+
return val === "1" || val === "true" || val === true;
|
|
1702
|
+
};
|
|
1703
|
+
const readNumber = (val) => {
|
|
1704
|
+
if (val === undefined || val === null)
|
|
1705
|
+
return undefined;
|
|
1706
|
+
const n = typeof val === "number" ? val : parseFloat(String(val));
|
|
1707
|
+
return Number.isFinite(n) ? n : undefined;
|
|
1708
|
+
};
|
|
1709
|
+
const mapLegendPosition = (val) => {
|
|
1710
|
+
switch (val) {
|
|
1711
|
+
case "t":
|
|
1712
|
+
return "top";
|
|
1713
|
+
case "b":
|
|
1714
|
+
return "bottom";
|
|
1715
|
+
case "l":
|
|
1716
|
+
return "left";
|
|
1717
|
+
case "r":
|
|
1718
|
+
return "right";
|
|
1719
|
+
case "tr":
|
|
1720
|
+
return "topRight";
|
|
1721
|
+
default:
|
|
1722
|
+
return undefined;
|
|
1723
|
+
}
|
|
1724
|
+
};
|
|
1725
|
+
function extractLegendOptions(chart) {
|
|
1726
|
+
// OOXML omits <c:legend> when the chart hides the legend, but
|
|
1727
|
+
// includes it (possibly empty) when the legend is shown. Some
|
|
1728
|
+
// writers add a child <c:delete val="1"/> instead — handle that too.
|
|
1729
|
+
const legend = getWithFallback(chart, ["c:legend", "legend"]);
|
|
1730
|
+
if (legend === undefined) {
|
|
1731
|
+
// Excel's default is "show". We only emit a `legend` field when the
|
|
1732
|
+
// source explicitly suppressed it, otherwise renderers fall back.
|
|
1733
|
+
return undefined;
|
|
1734
|
+
}
|
|
1735
|
+
const deleted = readBoolean(get(legend, "c:delete.a$.val") ?? get(legend, "delete.a$.val"));
|
|
1736
|
+
if (deleted) {
|
|
1737
|
+
return { position: "none" };
|
|
1738
|
+
}
|
|
1739
|
+
const posVal = get(legend, "c:legendPos.a$.val") ?? get(legend, "legendPos.a$.val");
|
|
1740
|
+
const position = mapLegendPosition(posVal);
|
|
1741
|
+
const textFormat = extractTextProperties(get(legend, "c:txPr") ?? get(legend, "txPr"));
|
|
1742
|
+
if (!position && !textFormat)
|
|
1743
|
+
return undefined;
|
|
1744
|
+
return {
|
|
1745
|
+
...(position ? { position } : {}),
|
|
1746
|
+
...(textFormat ? { textFormat } : {}),
|
|
1747
|
+
};
|
|
1748
|
+
}
|
|
1749
|
+
const mapDataLabelPosition = (val) => {
|
|
1750
|
+
switch (val) {
|
|
1751
|
+
case "ctr":
|
|
1752
|
+
return "center";
|
|
1753
|
+
case "inEnd":
|
|
1754
|
+
return "insideEnd";
|
|
1755
|
+
case "inBase":
|
|
1756
|
+
return "inside";
|
|
1757
|
+
case "outEnd":
|
|
1758
|
+
return "outsideEnd";
|
|
1759
|
+
case "bestFit":
|
|
1760
|
+
return "bestFit";
|
|
1761
|
+
default:
|
|
1762
|
+
return undefined;
|
|
1763
|
+
}
|
|
1764
|
+
};
|
|
1765
|
+
function extractDataLabelsOptions(plotArea, chartType) {
|
|
1766
|
+
// Look for <c:dLbls> inside the chartType-specific container. We pick
|
|
1767
|
+
// the first one we find — chart-level labels apply to all series.
|
|
1768
|
+
const containerKeys = {
|
|
1769
|
+
bar: ["c:barChart", "barChart"],
|
|
1770
|
+
column: ["c:barChart", "barChart"],
|
|
1771
|
+
line: ["c:lineChart", "lineChart"],
|
|
1772
|
+
pie: ["c:pieChart", "pieChart", "c:ofPieChart", "ofPieChart"],
|
|
1773
|
+
doughnut: ["c:doughnutChart", "doughnutChart"],
|
|
1774
|
+
area: ["c:areaChart", "areaChart"],
|
|
1775
|
+
};
|
|
1776
|
+
const keys = containerKeys[chartType] ?? [];
|
|
1777
|
+
let container;
|
|
1778
|
+
for (const key of keys) {
|
|
1779
|
+
container = container ?? get(plotArea, key);
|
|
1780
|
+
}
|
|
1781
|
+
const dLbls = get(container, "c:dLbls") ??
|
|
1782
|
+
get(container, "dLbls") ??
|
|
1783
|
+
get(plotArea, "c:dLbls") ??
|
|
1784
|
+
get(plotArea, "dLbls");
|
|
1785
|
+
if (!dLbls)
|
|
1786
|
+
return undefined;
|
|
1787
|
+
const result = {};
|
|
1788
|
+
const pos = get(dLbls, "c:dLblPos.a$.val") ?? get(dLbls, "dLblPos.a$.val");
|
|
1789
|
+
const position = mapDataLabelPosition(pos);
|
|
1790
|
+
if (position)
|
|
1791
|
+
result.position = position;
|
|
1792
|
+
const showVal = readBoolean(get(dLbls, "c:showVal.a$.val") ?? get(dLbls, "showVal.a$.val"));
|
|
1793
|
+
if (showVal) {
|
|
1794
|
+
result.show = true;
|
|
1795
|
+
result.showValue = true;
|
|
1796
|
+
}
|
|
1797
|
+
const showCat = readBoolean(get(dLbls, "c:showCatName.a$.val") ?? get(dLbls, "showCatName.a$.val"));
|
|
1798
|
+
if (showCat) {
|
|
1799
|
+
result.show = true;
|
|
1800
|
+
result.showCategoryName = true;
|
|
1801
|
+
}
|
|
1802
|
+
const showPct = readBoolean(get(dLbls, "c:showPercent.a$.val") ?? get(dLbls, "showPercent.a$.val"));
|
|
1803
|
+
if (showPct) {
|
|
1804
|
+
result.show = true;
|
|
1805
|
+
result.showPercent = true;
|
|
1806
|
+
}
|
|
1807
|
+
const showSer = readBoolean(get(dLbls, "c:showSerName.a$.val") ?? get(dLbls, "showSerName.a$.val"));
|
|
1808
|
+
if (showSer) {
|
|
1809
|
+
result.show = true;
|
|
1810
|
+
result.showSeriesName = true;
|
|
1811
|
+
}
|
|
1812
|
+
// If only `position` was set with no `show*` flags, treat as "show".
|
|
1813
|
+
if (position && result.show === undefined)
|
|
1814
|
+
result.show = true;
|
|
1815
|
+
// <c:txPr> on the dLbls block — affects every label in the chart.
|
|
1816
|
+
const textFormat = extractTextProperties(get(dLbls, "c:txPr") ?? get(dLbls, "txPr"));
|
|
1817
|
+
if (textFormat)
|
|
1818
|
+
result.textFormat = textFormat;
|
|
1819
|
+
// <c:numFmt formatCode="…"/> — Excel uses this to render labels as
|
|
1820
|
+
// currency / percent / etc. (`"$"#,##0`, `0.00%`). Without parsing
|
|
1821
|
+
// this, the renderer falls back to raw numbers (e.g. `1831` instead
|
|
1822
|
+
// of `$1,831`).
|
|
1823
|
+
const numFmt = get(dLbls, "c:numFmt.a$.formatCode") ??
|
|
1824
|
+
get(dLbls, "numFmt.a$.formatCode") ??
|
|
1825
|
+
get(dLbls, "c:numFmt.a$.t$") ??
|
|
1826
|
+
get(dLbls, "numFmt.a$.t$");
|
|
1827
|
+
if (typeof numFmt === "string" && numFmt) {
|
|
1828
|
+
result.numberFormatPattern = numFmt;
|
|
1829
|
+
}
|
|
1830
|
+
return Object.keys(result).length ? result : undefined;
|
|
1831
|
+
}
|
|
1832
|
+
function readAxis(axis) {
|
|
1833
|
+
if (!axis)
|
|
1834
|
+
return undefined;
|
|
1835
|
+
const result = {};
|
|
1836
|
+
// <c:scaling><c:logBase val="…"/> + <c:orientation val="minMax|maxMin"/>
|
|
1837
|
+
const logBase = readNumber(get(axis, "c:scaling.c:logBase.a$.val") ??
|
|
1838
|
+
get(axis, "scaling.logBase.a$.val"));
|
|
1839
|
+
if (typeof logBase === "number" && logBase > 0)
|
|
1840
|
+
result.logarithmic = true;
|
|
1841
|
+
const orientation = get(axis, "c:scaling.c:orientation.a$.val") ??
|
|
1842
|
+
get(axis, "scaling.orientation.a$.val");
|
|
1843
|
+
if (orientation === "maxMin")
|
|
1844
|
+
result.reverse = true;
|
|
1845
|
+
const min = readNumber(get(axis, "c:scaling.c:min.a$.val") ?? get(axis, "scaling.min.a$.val"));
|
|
1846
|
+
if (typeof min === "number")
|
|
1847
|
+
result.min = min;
|
|
1848
|
+
const max = readNumber(get(axis, "c:scaling.c:max.a$.val") ?? get(axis, "scaling.max.a$.val"));
|
|
1849
|
+
if (typeof max === "number")
|
|
1850
|
+
result.max = max;
|
|
1851
|
+
// <c:delete val="1"/> hides the axis entirely.
|
|
1852
|
+
const deleted = readBoolean(get(axis, "c:delete.a$.val") ?? get(axis, "delete.a$.val"));
|
|
1853
|
+
if (deleted)
|
|
1854
|
+
result.visible = false;
|
|
1855
|
+
// <c:majorGridlines/> presence == show. Empty element is sufficient.
|
|
1856
|
+
const majorGridlines = get(axis, "c:majorGridlines") ?? get(axis, "majorGridlines");
|
|
1857
|
+
if (majorGridlines !== undefined)
|
|
1858
|
+
result.showMajorGridlines = true;
|
|
1859
|
+
// <c:txPr> — axis tick-label font/color. Without this we'd inherit
|
|
1860
|
+
// the renderer's theme default (near-black) and lose the authored
|
|
1861
|
+
// color (e.g. BEP.xlsx X axis = #14325A, Y axis = #F0621C).
|
|
1862
|
+
const textFormat = extractTextProperties(get(axis, "c:txPr") ?? get(axis, "txPr"));
|
|
1863
|
+
if (textFormat)
|
|
1864
|
+
result.textFormat = textFormat;
|
|
1865
|
+
return Object.keys(result).length ? result : undefined;
|
|
1866
|
+
}
|
|
1867
|
+
export function extractAxisOptions(plotArea) {
|
|
1868
|
+
const valAx = get(plotArea, "c:valAx") ?? get(plotArea, "valAx");
|
|
1869
|
+
const catAx = get(plotArea, "c:catAx") ?? get(plotArea, "catAx");
|
|
1870
|
+
// Some chart types use <c:dateAx>; treat as category for our purposes.
|
|
1871
|
+
const dateAx = get(plotArea, "c:dateAx") ?? get(plotArea, "dateAx");
|
|
1872
|
+
const valAxes = Array.isArray(valAx) ? valAx : valAx ? [valAx] : [];
|
|
1873
|
+
return {
|
|
1874
|
+
valueAxisOptions: readAxis(valAxes[0]),
|
|
1875
|
+
secondaryValueAxisOptions: valAxes.length > 1 ? readAxis(valAxes[1]) : undefined,
|
|
1876
|
+
categoryAxisOptions: readAxis((Array.isArray(catAx) ? catAx[0] : catAx) ??
|
|
1877
|
+
(Array.isArray(dateAx) ? dateAx[0] : dateAx)),
|
|
1878
|
+
};
|
|
1879
|
+
}
|
|
1880
|
+
/**
|
|
1881
|
+
* Pie-of-pie / bar-of-pie — extract the of-pie layout fields from
|
|
1882
|
+
* `<c:ofPieChart>`. Returns undefined for plain pies. `splitType`
|
|
1883
|
+
* maps the OOXML tokens (`pos`/`val`/`percent`) to the spec vocabulary;
|
|
1884
|
+
* `auto` and `cust` (custom point assignment, carried in `<c:custSplit>`
|
|
1885
|
+
* which the spec cannot represent) are omitted so the renderer applies
|
|
1886
|
+
* Excel's default rule (position, last 3 values).
|
|
1887
|
+
*/
|
|
1888
|
+
export function extractOfPieOptions(plotArea) {
|
|
1889
|
+
const ofPie = getWithFallback(plotArea, ["c:ofPieChart", "ofPieChart"]);
|
|
1890
|
+
if (!ofPie)
|
|
1891
|
+
return undefined;
|
|
1892
|
+
const typeRaw = get(ofPie, "c:ofPieType.a$.val") ?? get(ofPie, "ofPieType.a$.val");
|
|
1893
|
+
const ofPieType = typeRaw === "bar" ? "bar" : "pie";
|
|
1894
|
+
const splitRaw = get(ofPie, "c:splitType.a$.val") ?? get(ofPie, "splitType.a$.val");
|
|
1895
|
+
const splitType = splitRaw === "pos"
|
|
1896
|
+
? "position"
|
|
1897
|
+
: splitRaw === "val"
|
|
1898
|
+
? "value"
|
|
1899
|
+
: splitRaw === "percent"
|
|
1900
|
+
? "percent"
|
|
1901
|
+
: undefined;
|
|
1902
|
+
const splitPos = readNumber(get(ofPie, "c:splitPos.a$.val") ?? get(ofPie, "splitPos.a$.val"));
|
|
1903
|
+
const secondPieSize = readNumber(get(ofPie, "c:secondPieSize.a$.val") ??
|
|
1904
|
+
get(ofPie, "secondPieSize.a$.val"));
|
|
1905
|
+
return {
|
|
1906
|
+
ofPieType,
|
|
1907
|
+
...(splitType ? { splitType } : {}),
|
|
1908
|
+
...(typeof splitPos === "number" ? { splitPos } : {}),
|
|
1909
|
+
...(typeof secondPieSize === "number" ? { secondPieSize } : {}),
|
|
1910
|
+
};
|
|
1911
|
+
}
|
|
1912
|
+
function extractDoughnutHoleSize(plotArea) {
|
|
1913
|
+
// <c:doughnutChart><c:holeSize val="50"/> — value is the inner radius
|
|
1914
|
+
// as a percentage of the chart radius (0-90). We normalize to a 0-1
|
|
1915
|
+
// fraction so renderers can use it without knowing the spec.
|
|
1916
|
+
const holeSize = readNumber(get(plotArea, "c:doughnutChart.c:holeSize.a$.val") ??
|
|
1917
|
+
get(plotArea, "doughnutChart.holeSize.a$.val"));
|
|
1918
|
+
if (typeof holeSize !== "number")
|
|
1919
|
+
return undefined;
|
|
1920
|
+
return Math.min(0.9, Math.max(0, holeSize / 100));
|
|
1921
|
+
}
|
|
1922
|
+
// -------------------------- Phase 5.2 helpers --------------------------
|
|
1923
|
+
const mapTrendlineType = (val) => {
|
|
1924
|
+
switch (val) {
|
|
1925
|
+
case "exp":
|
|
1926
|
+
return "exp";
|
|
1927
|
+
case "log":
|
|
1928
|
+
return "log";
|
|
1929
|
+
case "poly":
|
|
1930
|
+
return "poly";
|
|
1931
|
+
case "power":
|
|
1932
|
+
return "power";
|
|
1933
|
+
case "movingAvg":
|
|
1934
|
+
return "movingAvg";
|
|
1935
|
+
case "linear":
|
|
1936
|
+
default:
|
|
1937
|
+
return "linear";
|
|
1938
|
+
}
|
|
1939
|
+
};
|
|
1940
|
+
function extractTrendlines(ser) {
|
|
1941
|
+
// <c:trendline> can appear multiple times under a single series; the
|
|
1942
|
+
// parser may surface either an array or a single object.
|
|
1943
|
+
const raw = get(ser, "c:trendline") ?? get(ser, "trendline");
|
|
1944
|
+
if (!raw)
|
|
1945
|
+
return undefined;
|
|
1946
|
+
const list = Array.isArray(raw) ? raw : [raw];
|
|
1947
|
+
const results = [];
|
|
1948
|
+
for (const t of list) {
|
|
1949
|
+
const type = mapTrendlineType(get(t, "c:trendlineType.a$.val") ?? get(t, "trendlineType.a$.val"));
|
|
1950
|
+
const order = readNumber(get(t, "c:order.a$.val") ?? get(t, "order.a$.val"));
|
|
1951
|
+
const period = readNumber(get(t, "c:period.a$.val") ?? get(t, "period.a$.val"));
|
|
1952
|
+
const name = get(t, "c:name.t$") ?? get(t, "name.t$") ?? get(t, "c:name") ?? get(t, "name");
|
|
1953
|
+
const showEquation = readBoolean(get(t, "c:dispEq.a$.val") ?? get(t, "dispEq.a$.val"));
|
|
1954
|
+
const showRSquared = readBoolean(get(t, "c:dispRSqr.a$.val") ?? get(t, "dispRSqr.a$.val"));
|
|
1955
|
+
const forecastForward = readNumber(get(t, "c:forward.a$.val") ?? get(t, "forward.a$.val"));
|
|
1956
|
+
const forecastBackward = readNumber(get(t, "c:backward.a$.val") ?? get(t, "backward.a$.val"));
|
|
1957
|
+
const entry = { type };
|
|
1958
|
+
if (typeof order === "number")
|
|
1959
|
+
entry.order = order;
|
|
1960
|
+
if (typeof period === "number")
|
|
1961
|
+
entry.period = period;
|
|
1962
|
+
if (typeof name === "string" && name)
|
|
1963
|
+
entry.name = name;
|
|
1964
|
+
if (showEquation)
|
|
1965
|
+
entry.showEquation = true;
|
|
1966
|
+
if (showRSquared)
|
|
1967
|
+
entry.showRSquared = true;
|
|
1968
|
+
if (typeof forecastForward === "number")
|
|
1969
|
+
entry.forecastForward = forecastForward;
|
|
1970
|
+
if (typeof forecastBackward === "number")
|
|
1971
|
+
entry.forecastBackward = forecastBackward;
|
|
1972
|
+
results.push(entry);
|
|
1973
|
+
}
|
|
1974
|
+
return results;
|
|
1975
|
+
}
|
|
1976
|
+
function extractErrorBars(ser, hostSheetId, resolveSheetId) {
|
|
1977
|
+
const raw = get(ser, "c:errBars") ?? get(ser, "errBars");
|
|
1978
|
+
if (!raw)
|
|
1979
|
+
return undefined;
|
|
1980
|
+
const direction = get(raw, "c:errDir.a$.val") ?? get(raw, "errDir.a$.val");
|
|
1981
|
+
const barType = get(raw, "c:errBarType.a$.val") ?? get(raw, "errBarType.a$.val");
|
|
1982
|
+
const valTypeVal = get(raw, "c:errValType.a$.val") ?? get(raw, "errValType.a$.val");
|
|
1983
|
+
const value = readNumber(get(raw, "c:val.a$.val") ?? get(raw, "val.a$.val"));
|
|
1984
|
+
const noEndCap = readBoolean(get(raw, "c:noEndCap.a$.val") ?? get(raw, "noEndCap.a$.val"));
|
|
1985
|
+
const result = {};
|
|
1986
|
+
if (direction === "x" || direction === "y")
|
|
1987
|
+
result.direction = direction;
|
|
1988
|
+
if (barType === "both" || barType === "plus" || barType === "minus") {
|
|
1989
|
+
result.barType = barType;
|
|
1990
|
+
}
|
|
1991
|
+
if (valTypeVal === "fixedVal" ||
|
|
1992
|
+
valTypeVal === "percentage" ||
|
|
1993
|
+
valTypeVal === "stdDev" ||
|
|
1994
|
+
valTypeVal === "stdErr" ||
|
|
1995
|
+
valTypeVal === "cust") {
|
|
1996
|
+
result.valueType = valTypeVal === "cust" ? "custom" : valTypeVal;
|
|
1997
|
+
}
|
|
1998
|
+
if (typeof value === "number")
|
|
1999
|
+
result.value = value;
|
|
2000
|
+
if (noEndCap)
|
|
2001
|
+
result.noEndCap = true;
|
|
2002
|
+
// Custom error bars carry per-point magnitudes in worksheet ranges —
|
|
2003
|
+
// `<c:plus>/<c:minus>` each hold a `<c:numRef><c:f>` reference.
|
|
2004
|
+
if (result.valueType === "custom") {
|
|
2005
|
+
const plusRef = getWithFallback(raw, [
|
|
2006
|
+
"c:plus.c:numRef.c:f",
|
|
2007
|
+
"c:plus.c:numRef.c:f.t$",
|
|
2008
|
+
"plus.numRef.f",
|
|
2009
|
+
"plus.numRef.f.t$",
|
|
2010
|
+
]);
|
|
2011
|
+
const minusRef = getWithFallback(raw, [
|
|
2012
|
+
"c:minus.c:numRef.c:f",
|
|
2013
|
+
"c:minus.c:numRef.c:f.t$",
|
|
2014
|
+
"minus.numRef.f",
|
|
2015
|
+
"minus.numRef.f.t$",
|
|
2016
|
+
]);
|
|
2017
|
+
if (plusRef) {
|
|
2018
|
+
const plusSources = resolveSourcesFromRef(plusRef, hostSheetId, resolveSheetId);
|
|
2019
|
+
if (plusSources.length)
|
|
2020
|
+
result.plusRange = plusSources[0];
|
|
2021
|
+
}
|
|
2022
|
+
if (minusRef) {
|
|
2023
|
+
const minusSources = resolveSourcesFromRef(minusRef, hostSheetId, resolveSheetId);
|
|
2024
|
+
if (minusSources.length)
|
|
2025
|
+
result.minusRange = minusSources[0];
|
|
2026
|
+
}
|
|
2027
|
+
}
|
|
2028
|
+
return Object.keys(result).length ? result : undefined;
|
|
2029
|
+
}
|
|
2030
|
+
function extractRadarStyle(plotArea) {
|
|
2031
|
+
const val = get(plotArea, "c:radarChart.c:radarStyle.a$.val") ??
|
|
2032
|
+
get(plotArea, "radarChart.radarStyle.a$.val");
|
|
2033
|
+
if (val === "standard" || val === "marker" || val === "filled")
|
|
2034
|
+
return val;
|
|
2035
|
+
return undefined;
|
|
2036
|
+
}
|