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