@rowsncolumns/charts 27.0.30

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 (281) hide show
  1. package/LICENSE.md +100 -0
  2. package/Readme.md +81 -0
  3. package/dist/cjs/atoms/editing-chart.atom.d.ts +13 -0
  4. package/dist/cjs/atoms/editing-chart.atom.d.ts.map +1 -0
  5. package/dist/cjs/atoms/editing-chart.atom.js +26 -0
  6. package/dist/cjs/basic-chart/basic-chart.d.ts +5 -0
  7. package/dist/cjs/basic-chart/basic-chart.d.ts.map +1 -0
  8. package/dist/cjs/basic-chart/basic-chart.js +2250 -0
  9. package/dist/cjs/basic-chart/helpers.d.ts +12 -0
  10. package/dist/cjs/basic-chart/helpers.d.ts.map +1 -0
  11. package/dist/cjs/basic-chart/helpers.js +105 -0
  12. package/dist/cjs/basic-chart/index.d.ts +5 -0
  13. package/dist/cjs/basic-chart/index.d.ts.map +1 -0
  14. package/dist/cjs/basic-chart/index.js +12 -0
  15. package/dist/cjs/components/dialogs/chart-editor-dialog.d.ts +5 -0
  16. package/dist/cjs/components/dialogs/chart-editor-dialog.d.ts.map +1 -0
  17. package/dist/cjs/components/dialogs/chart-editor-dialog.js +25 -0
  18. package/dist/cjs/components/editors/chart-editor.d.ts +10 -0
  19. package/dist/cjs/components/editors/chart-editor.d.ts.map +1 -0
  20. package/dist/cjs/components/editors/chart-editor.js +631 -0
  21. package/dist/cjs/components/editors/chart-range-input.d.ts +17 -0
  22. package/dist/cjs/components/editors/chart-range-input.d.ts.map +1 -0
  23. package/dist/cjs/components/editors/chart-range-input.js +16 -0
  24. package/dist/cjs/config.d.ts +19 -0
  25. package/dist/cjs/config.d.ts.map +1 -0
  26. package/dist/cjs/config.js +196 -0
  27. package/dist/cjs/hooks/use-chart-editor.d.ts +8 -0
  28. package/dist/cjs/hooks/use-chart-editor.d.ts.map +1 -0
  29. package/dist/cjs/hooks/use-chart-editor.js +29 -0
  30. package/dist/cjs/hooks/use-charts.d.ts +35 -0
  31. package/dist/cjs/hooks/use-charts.d.ts.map +1 -0
  32. package/dist/cjs/hooks/use-charts.js +67 -0
  33. package/dist/cjs/hooks/use-on-create-chart.d.ts +13 -0
  34. package/dist/cjs/hooks/use-on-create-chart.d.ts.map +1 -0
  35. package/dist/cjs/hooks/use-on-create-chart.js +24 -0
  36. package/dist/cjs/hooks/use-on-delete-chart.d.ts +8 -0
  37. package/dist/cjs/hooks/use-on-delete-chart.d.ts.map +1 -0
  38. package/dist/cjs/hooks/use-on-delete-chart.js +19 -0
  39. package/dist/cjs/hooks/use-on-move-chart.d.ts +8 -0
  40. package/dist/cjs/hooks/use-on-move-chart.d.ts.map +1 -0
  41. package/dist/cjs/hooks/use-on-move-chart.js +15 -0
  42. package/dist/cjs/hooks/use-on-resize-chart.d.ts +8 -0
  43. package/dist/cjs/hooks/use-on-resize-chart.d.ts.map +1 -0
  44. package/dist/cjs/hooks/use-on-resize-chart.js +15 -0
  45. package/dist/cjs/hooks/use-on-update-chart.d.ts +9 -0
  46. package/dist/cjs/hooks/use-on-update-chart.d.ts.map +1 -0
  47. package/dist/cjs/hooks/use-on-update-chart.js +15 -0
  48. package/dist/cjs/index.d.ts +11 -0
  49. package/dist/cjs/index.d.ts.map +1 -0
  50. package/dist/cjs/index.js +18 -0
  51. package/dist/cjs/interface/create-chart.d.ts +9 -0
  52. package/dist/cjs/interface/create-chart.d.ts.map +1 -0
  53. package/dist/cjs/interface/create-chart.js +110 -0
  54. package/dist/cjs/interface/delete-chart.d.ts +4 -0
  55. package/dist/cjs/interface/delete-chart.d.ts.map +1 -0
  56. package/dist/cjs/interface/delete-chart.js +26 -0
  57. package/dist/cjs/interface/move-chart.d.ts +5 -0
  58. package/dist/cjs/interface/move-chart.d.ts.map +1 -0
  59. package/dist/cjs/interface/move-chart.js +28 -0
  60. package/dist/cjs/interface/resize-chart.d.ts +5 -0
  61. package/dist/cjs/interface/resize-chart.d.ts.map +1 -0
  62. package/dist/cjs/interface/resize-chart.js +30 -0
  63. package/dist/cjs/interface/update-chart.d.ts +4 -0
  64. package/dist/cjs/interface/update-chart.d.ts.map +1 -0
  65. package/dist/cjs/interface/update-chart.js +29 -0
  66. package/dist/cjs/plotly/basic-chart/basic-chart.d.ts +5 -0
  67. package/dist/cjs/plotly/basic-chart/basic-chart.d.ts.map +1 -0
  68. package/dist/cjs/plotly/basic-chart/basic-chart.js +1586 -0
  69. package/dist/cjs/plotly/basic-chart/helpers.d.ts +20 -0
  70. package/dist/cjs/plotly/basic-chart/helpers.d.ts.map +1 -0
  71. package/dist/cjs/plotly/basic-chart/helpers.js +114 -0
  72. package/dist/cjs/plotly/basic-chart/index.d.ts +5 -0
  73. package/dist/cjs/plotly/basic-chart/index.d.ts.map +1 -0
  74. package/dist/cjs/plotly/basic-chart/index.js +12 -0
  75. package/dist/cjs/tsconfig.cjs.tsbuildinfo +1 -0
  76. package/dist/cjs/types.d.ts +27 -0
  77. package/dist/cjs/types.d.ts.map +1 -0
  78. package/dist/cjs/types.js +2 -0
  79. package/dist/cjs/utils/aggregation.d.ts +6 -0
  80. package/dist/cjs/utils/aggregation.d.ts.map +1 -0
  81. package/dist/cjs/utils/aggregation.js +127 -0
  82. package/dist/cjs/utils/bar-layout.d.ts +57 -0
  83. package/dist/cjs/utils/bar-layout.d.ts.map +1 -0
  84. package/dist/cjs/utils/bar-layout.js +81 -0
  85. package/dist/cjs/utils/boxplot.d.ts +62 -0
  86. package/dist/cjs/utils/boxplot.d.ts.map +1 -0
  87. package/dist/cjs/utils/boxplot.js +105 -0
  88. package/dist/cjs/utils/bubble.d.ts +52 -0
  89. package/dist/cjs/utils/bubble.d.ts.map +1 -0
  90. package/dist/cjs/utils/bubble.js +139 -0
  91. package/dist/cjs/utils/chart-spec-utils.d.ts +67 -0
  92. package/dist/cjs/utils/chart-spec-utils.d.ts.map +1 -0
  93. package/dist/cjs/utils/chart-spec-utils.js +355 -0
  94. package/dist/cjs/utils/echarts-gl-loader.d.ts +11 -0
  95. package/dist/cjs/utils/echarts-gl-loader.d.ts.map +1 -0
  96. package/dist/cjs/utils/echarts-gl-loader.js +59 -0
  97. package/dist/cjs/utils/error-bars.d.ts +58 -0
  98. package/dist/cjs/utils/error-bars.d.ts.map +1 -0
  99. package/dist/cjs/utils/error-bars.js +154 -0
  100. package/dist/cjs/utils/gradient.d.ts +52 -0
  101. package/dist/cjs/utils/gradient.d.ts.map +1 -0
  102. package/dist/cjs/utils/gradient.js +93 -0
  103. package/dist/cjs/utils/hierarchy.d.ts +28 -0
  104. package/dist/cjs/utils/hierarchy.d.ts.map +1 -0
  105. package/dist/cjs/utils/hierarchy.js +186 -0
  106. package/dist/cjs/utils/histogram.d.ts +46 -0
  107. package/dist/cjs/utils/histogram.d.ts.map +1 -0
  108. package/dist/cjs/utils/histogram.js +125 -0
  109. package/dist/cjs/utils/of-pie.d.ts +101 -0
  110. package/dist/cjs/utils/of-pie.d.ts.map +1 -0
  111. package/dist/cjs/utils/of-pie.js +159 -0
  112. package/dist/cjs/utils/overrides.d.ts +12 -0
  113. package/dist/cjs/utils/overrides.d.ts.map +1 -0
  114. package/dist/cjs/utils/overrides.js +171 -0
  115. package/dist/cjs/utils/radar.d.ts +37 -0
  116. package/dist/cjs/utils/radar.d.ts.map +1 -0
  117. package/dist/cjs/utils/radar.js +70 -0
  118. package/dist/cjs/utils/region-map-loader.d.ts +22 -0
  119. package/dist/cjs/utils/region-map-loader.d.ts.map +1 -0
  120. package/dist/cjs/utils/region-map-loader.js +174 -0
  121. package/dist/cjs/utils/region-map.d.ts +100 -0
  122. package/dist/cjs/utils/region-map.d.ts.map +1 -0
  123. package/dist/cjs/utils/region-map.js +327 -0
  124. package/dist/cjs/utils/stock.d.ts +64 -0
  125. package/dist/cjs/utils/stock.d.ts.map +1 -0
  126. package/dist/cjs/utils/stock.js +153 -0
  127. package/dist/cjs/utils/three-d.d.ts +96 -0
  128. package/dist/cjs/utils/three-d.d.ts.map +1 -0
  129. package/dist/cjs/utils/three-d.js +191 -0
  130. package/dist/cjs/utils/trendline.d.ts +41 -0
  131. package/dist/cjs/utils/trendline.d.ts.map +1 -0
  132. package/dist/cjs/utils/trendline.js +303 -0
  133. package/dist/cjs/utils/waterfall.d.ts +24 -0
  134. package/dist/cjs/utils/waterfall.d.ts.map +1 -0
  135. package/dist/cjs/utils/waterfall.js +61 -0
  136. package/dist/cjs/utils/webgl.d.ts +12 -0
  137. package/dist/cjs/utils/webgl.d.ts.map +1 -0
  138. package/dist/cjs/utils/webgl.js +36 -0
  139. package/dist/cjs/vitest.config.d.ts +3 -0
  140. package/dist/cjs/vitest.config.d.ts.map +1 -0
  141. package/dist/cjs/vitest.config.js +9 -0
  142. package/dist/esm/atoms/editing-chart.atom.d.ts +13 -0
  143. package/dist/esm/atoms/editing-chart.atom.d.ts.map +1 -0
  144. package/dist/esm/atoms/editing-chart.atom.js +12 -0
  145. package/dist/esm/basic-chart/basic-chart.d.ts +5 -0
  146. package/dist/esm/basic-chart/basic-chart.d.ts.map +1 -0
  147. package/dist/esm/basic-chart/basic-chart.js +2343 -0
  148. package/dist/esm/basic-chart/helpers.d.ts +12 -0
  149. package/dist/esm/basic-chart/helpers.d.ts.map +1 -0
  150. package/dist/esm/basic-chart/helpers.js +98 -0
  151. package/dist/esm/basic-chart/index.d.ts +5 -0
  152. package/dist/esm/basic-chart/index.d.ts.map +1 -0
  153. package/dist/esm/basic-chart/index.js +7 -0
  154. package/dist/esm/components/dialogs/chart-editor-dialog.d.ts +5 -0
  155. package/dist/esm/components/dialogs/chart-editor-dialog.d.ts.map +1 -0
  156. package/dist/esm/components/dialogs/chart-editor-dialog.js +19 -0
  157. package/dist/esm/components/editors/chart-editor.d.ts +10 -0
  158. package/dist/esm/components/editors/chart-editor.d.ts.map +1 -0
  159. package/dist/esm/components/editors/chart-editor.js +550 -0
  160. package/dist/esm/components/editors/chart-range-input.d.ts +17 -0
  161. package/dist/esm/components/editors/chart-range-input.d.ts.map +1 -0
  162. package/dist/esm/components/editors/chart-range-input.js +10 -0
  163. package/dist/esm/config.d.ts +19 -0
  164. package/dist/esm/config.d.ts.map +1 -0
  165. package/dist/esm/config.js +161 -0
  166. package/dist/esm/hooks/use-chart-editor.d.ts +8 -0
  167. package/dist/esm/hooks/use-chart-editor.d.ts.map +1 -0
  168. package/dist/esm/hooks/use-chart-editor.js +32 -0
  169. package/dist/esm/hooks/use-charts.d.ts +35 -0
  170. package/dist/esm/hooks/use-charts.d.ts.map +1 -0
  171. package/dist/esm/hooks/use-charts.js +62 -0
  172. package/dist/esm/hooks/use-on-create-chart.d.ts +13 -0
  173. package/dist/esm/hooks/use-on-create-chart.d.ts.map +1 -0
  174. package/dist/esm/hooks/use-on-create-chart.js +19 -0
  175. package/dist/esm/hooks/use-on-delete-chart.d.ts +8 -0
  176. package/dist/esm/hooks/use-on-delete-chart.d.ts.map +1 -0
  177. package/dist/esm/hooks/use-on-delete-chart.js +14 -0
  178. package/dist/esm/hooks/use-on-move-chart.d.ts +8 -0
  179. package/dist/esm/hooks/use-on-move-chart.d.ts.map +1 -0
  180. package/dist/esm/hooks/use-on-move-chart.js +10 -0
  181. package/dist/esm/hooks/use-on-resize-chart.d.ts +8 -0
  182. package/dist/esm/hooks/use-on-resize-chart.d.ts.map +1 -0
  183. package/dist/esm/hooks/use-on-resize-chart.js +10 -0
  184. package/dist/esm/hooks/use-on-update-chart.d.ts +9 -0
  185. package/dist/esm/hooks/use-on-update-chart.d.ts.map +1 -0
  186. package/dist/esm/hooks/use-on-update-chart.js +10 -0
  187. package/dist/esm/index.d.ts +11 -0
  188. package/dist/esm/index.d.ts.map +1 -0
  189. package/dist/esm/index.js +13 -0
  190. package/dist/esm/interface/create-chart.d.ts +9 -0
  191. package/dist/esm/interface/create-chart.d.ts.map +1 -0
  192. package/dist/esm/interface/create-chart.js +126 -0
  193. package/dist/esm/interface/delete-chart.d.ts +4 -0
  194. package/dist/esm/interface/delete-chart.d.ts.map +1 -0
  195. package/dist/esm/interface/delete-chart.js +20 -0
  196. package/dist/esm/interface/move-chart.d.ts +5 -0
  197. package/dist/esm/interface/move-chart.d.ts.map +1 -0
  198. package/dist/esm/interface/move-chart.js +22 -0
  199. package/dist/esm/interface/resize-chart.d.ts +5 -0
  200. package/dist/esm/interface/resize-chart.d.ts.map +1 -0
  201. package/dist/esm/interface/resize-chart.js +24 -0
  202. package/dist/esm/interface/update-chart.d.ts +4 -0
  203. package/dist/esm/interface/update-chart.d.ts.map +1 -0
  204. package/dist/esm/interface/update-chart.js +26 -0
  205. package/dist/esm/plotly/basic-chart/basic-chart.d.ts +5 -0
  206. package/dist/esm/plotly/basic-chart/basic-chart.d.ts.map +1 -0
  207. package/dist/esm/plotly/basic-chart/basic-chart.js +1682 -0
  208. package/dist/esm/plotly/basic-chart/helpers.d.ts +20 -0
  209. package/dist/esm/plotly/basic-chart/helpers.d.ts.map +1 -0
  210. package/dist/esm/plotly/basic-chart/helpers.js +107 -0
  211. package/dist/esm/plotly/basic-chart/index.d.ts +5 -0
  212. package/dist/esm/plotly/basic-chart/index.d.ts.map +1 -0
  213. package/dist/esm/plotly/basic-chart/index.js +7 -0
  214. package/dist/esm/tsconfig.esm.tsbuildinfo +1 -0
  215. package/dist/esm/types.d.ts +27 -0
  216. package/dist/esm/types.d.ts.map +1 -0
  217. package/dist/esm/types.js +1 -0
  218. package/dist/esm/utils/aggregation.d.ts +6 -0
  219. package/dist/esm/utils/aggregation.d.ts.map +1 -0
  220. package/dist/esm/utils/aggregation.js +96 -0
  221. package/dist/esm/utils/bar-layout.d.ts +57 -0
  222. package/dist/esm/utils/bar-layout.d.ts.map +1 -0
  223. package/dist/esm/utils/bar-layout.js +70 -0
  224. package/dist/esm/utils/boxplot.d.ts +62 -0
  225. package/dist/esm/utils/boxplot.d.ts.map +1 -0
  226. package/dist/esm/utils/boxplot.js +91 -0
  227. package/dist/esm/utils/bubble.d.ts +52 -0
  228. package/dist/esm/utils/bubble.d.ts.map +1 -0
  229. package/dist/esm/utils/bubble.js +122 -0
  230. package/dist/esm/utils/chart-spec-utils.d.ts +67 -0
  231. package/dist/esm/utils/chart-spec-utils.d.ts.map +1 -0
  232. package/dist/esm/utils/chart-spec-utils.js +270 -0
  233. package/dist/esm/utils/echarts-gl-loader.d.ts +11 -0
  234. package/dist/esm/utils/echarts-gl-loader.d.ts.map +1 -0
  235. package/dist/esm/utils/echarts-gl-loader.js +51 -0
  236. package/dist/esm/utils/error-bars.d.ts +58 -0
  237. package/dist/esm/utils/error-bars.d.ts.map +1 -0
  238. package/dist/esm/utils/error-bars.js +134 -0
  239. package/dist/esm/utils/gradient.d.ts +52 -0
  240. package/dist/esm/utils/gradient.d.ts.map +1 -0
  241. package/dist/esm/utils/gradient.js +84 -0
  242. package/dist/esm/utils/hierarchy.d.ts +28 -0
  243. package/dist/esm/utils/hierarchy.d.ts.map +1 -0
  244. package/dist/esm/utils/hierarchy.js +150 -0
  245. package/dist/esm/utils/histogram.d.ts +46 -0
  246. package/dist/esm/utils/histogram.d.ts.map +1 -0
  247. package/dist/esm/utils/histogram.js +95 -0
  248. package/dist/esm/utils/of-pie.d.ts +101 -0
  249. package/dist/esm/utils/of-pie.d.ts.map +1 -0
  250. package/dist/esm/utils/of-pie.js +141 -0
  251. package/dist/esm/utils/overrides.d.ts +12 -0
  252. package/dist/esm/utils/overrides.d.ts.map +1 -0
  253. package/dist/esm/utils/overrides.js +159 -0
  254. package/dist/esm/utils/radar.d.ts +37 -0
  255. package/dist/esm/utils/radar.d.ts.map +1 -0
  256. package/dist/esm/utils/radar.js +49 -0
  257. package/dist/esm/utils/region-map-loader.d.ts +22 -0
  258. package/dist/esm/utils/region-map-loader.d.ts.map +1 -0
  259. package/dist/esm/utils/region-map-loader.js +141 -0
  260. package/dist/esm/utils/region-map.d.ts +100 -0
  261. package/dist/esm/utils/region-map.d.ts.map +1 -0
  262. package/dist/esm/utils/region-map.js +266 -0
  263. package/dist/esm/utils/stock.d.ts +64 -0
  264. package/dist/esm/utils/stock.d.ts.map +1 -0
  265. package/dist/esm/utils/stock.js +123 -0
  266. package/dist/esm/utils/three-d.d.ts +96 -0
  267. package/dist/esm/utils/three-d.d.ts.map +1 -0
  268. package/dist/esm/utils/three-d.js +162 -0
  269. package/dist/esm/utils/trendline.d.ts +41 -0
  270. package/dist/esm/utils/trendline.d.ts.map +1 -0
  271. package/dist/esm/utils/trendline.js +257 -0
  272. package/dist/esm/utils/waterfall.d.ts +24 -0
  273. package/dist/esm/utils/waterfall.d.ts.map +1 -0
  274. package/dist/esm/utils/waterfall.js +55 -0
  275. package/dist/esm/utils/webgl.d.ts +12 -0
  276. package/dist/esm/utils/webgl.d.ts.map +1 -0
  277. package/dist/esm/utils/webgl.js +30 -0
  278. package/dist/esm/vitest.config.d.ts +3 -0
  279. package/dist/esm/vitest.config.d.ts.map +1 -0
  280. package/dist/esm/vitest.config.js +7 -0
  281. package/package.json +96 -0
@@ -0,0 +1,2343 @@
1
+ import { ChevronRightIcon, DotsVerticalIcon } from "@rowsncolumns/icons";
2
+ import { getStringifiedColor, useFocusSheet } from "@rowsncolumns/spreadsheet";
3
+ import { useSubscribeToMultipleRanges } from "@rowsncolumns/spreadsheet-state";
4
+ import { createDownloadableFile } from "@rowsncolumns/toolkit";
5
+ import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuPortal, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuTrigger, DropdownRightSlot, IconButton, useIsomorphicLayoutEffect, useOnVisibilityChange, } from "@rowsncolumns/ui";
6
+ import { debug, isAFormula, isNil } from "@rowsncolumns/utils";
7
+ import { BarChart, BoxplotChart, CandlestickChart, CustomChart, FunnelChart, LineChart, PieChart, RadarChart as EchartsRadarChart, ScatterChart, SunburstChart, TreemapChart as EchartsTreemapChart, } from "echarts/charts";
8
+ import { DatasetComponent, GridComponent, LegendComponent, RadarComponent, TitleComponent, TooltipComponent, } from "echarts/components";
9
+ import * as echarts from "echarts/core";
10
+ import { CanvasRenderer } from "echarts/renderers";
11
+ import React, { useCallback, useMemo, useRef, useState } from "react";
12
+ import { aggregateSeriesByDomain, getDomainValuesForAxis, sanitizeDomainValues, shouldAggregateSeries, } from "../utils/aggregation";
13
+ import { gapWidthToBarCategoryGap, overlapToBarGap, } from "../utils/bar-layout";
14
+ import { computeBoxplotSummary } from "../utils/boxplot";
15
+ import { createBubbleSizeScale, prepareBubbleSeriesData, } from "../utils/bubble";
16
+ import { coerceCalculatedLabel, getChartDataRanges, } from "../utils/chart-spec-utils";
17
+ import { getEchartsGlState, loadEchartsGl, } from "../utils/echarts-gl-loader";
18
+ import { computeErrorBars, resolveCustomErrorBarValues, } from "../utils/error-bars";
19
+ import { gradientToEcharts } from "../utils/gradient";
20
+ import { buildHierarchyTree, extractHierarchyRows, getHierarchyHeaderTitle, } from "../utils/hierarchy";
21
+ import { binHistogram, computePareto, isParetoSpec, } from "../utils/histogram";
22
+ import { clampSecondPieSize, isOfPieChartSpec, otherSliceHalfAngle, resolveOfPieType, splitOfPie, } from "../utils/of-pie";
23
+ import { applyAxisOverrides, applyLegendOverrides, buildSeriesLabelOption, } from "../utils/overrides";
24
+ import { buildRadarIndicators, buildRadarSeriesData } from "../utils/radar";
25
+ import { buildRegionMapData, buildSequentialColorRamp, isRegionMapSpec, resolveGeoScope, warnOnceForUnmatchedRegions, } from "../utils/region-map";
26
+ import { loadRegionMapGeo } from "../utils/region-map-loader";
27
+ import { toCandlestick, toLineSeries, toVolumeSeries, } from "../utils/stock";
28
+ import { buildBar3DData, buildLine3DSeries, buildSurfaceData, getValueExtent, resolve3DRenderKind, SURFACE_COLOR_RAMP, } from "../utils/three-d";
29
+ import { computeTrendline } from "../utils/trendline";
30
+ import { computeWaterfall } from "../utils/waterfall";
31
+ import { isWebGLAvailable } from "../utils/webgl";
32
+ import { chartTypeToEchartsType, getEchartsSeriesStyle, mapMarkerSymbolToEcharts, showXAxis, } from "./helpers";
33
+ // Excel error values that should be treated as null in charts
34
+ const EXCEL_ERROR_VALUES = new Set([
35
+ "#N/A",
36
+ "#REF!",
37
+ "#VALUE!",
38
+ "#DIV/0!",
39
+ "#NAME?",
40
+ "#NULL!",
41
+ "#NUM!",
42
+ "#ERROR!",
43
+ "#GETTING_DATA",
44
+ ]);
45
+ /**
46
+ * Check if a value is an Excel error string
47
+ */
48
+ const isErrorValue = (value) => {
49
+ if (typeof value !== "string")
50
+ return false;
51
+ return EXCEL_ERROR_VALUES.has(value.toUpperCase());
52
+ };
53
+ /**
54
+ * Sanitize a value for chart display - convert errors to undefined
55
+ */
56
+ const sanitizeChartValue = (value) => {
57
+ if (value === null || value === undefined)
58
+ return undefined;
59
+ if (isErrorValue(value))
60
+ return undefined;
61
+ if (typeof value === "number" && !isNaN(value))
62
+ return value;
63
+ if (typeof value === "string") {
64
+ const num = parseFloat(value);
65
+ return isNaN(num) ? undefined : num;
66
+ }
67
+ return undefined;
68
+ };
69
+ const BASIC_CHART_TYPES = new Set([
70
+ "bar",
71
+ "column",
72
+ "line",
73
+ "pie",
74
+ "area",
75
+ "doughnut",
76
+ "radar",
77
+ "stock",
78
+ "histogram",
79
+ "boxplot",
80
+ "waterfall",
81
+ "funnel",
82
+ "combo",
83
+ "regionMap",
84
+ ]);
85
+ const BUBBLE_CHART_TYPES = new Set([
86
+ "bubble",
87
+ "scatter",
88
+ ]);
89
+ const HIERARCHY_CHART_TYPES = new Set([
90
+ "treemap",
91
+ "sunburst",
92
+ ]);
93
+ const isBasicChartSpec = (spec) => BASIC_CHART_TYPES.has(spec.chartType);
94
+ const isBubbleChartSpec = (spec) => BUBBLE_CHART_TYPES.has(spec.chartType);
95
+ const isHierarchyChartSpec = (spec) => HIERARCHY_CHART_TYPES.has(spec.chartType);
96
+ const HIERARCHY_LEVEL_STYLES = [
97
+ {
98
+ itemStyle: {
99
+ gapWidth: 1,
100
+ borderColorSaturation: 0.6,
101
+ },
102
+ upperLabel: { show: false },
103
+ },
104
+ {
105
+ colorSaturation: [0.35, 0.65],
106
+ itemStyle: {
107
+ gapWidth: 1,
108
+ borderColorSaturation: 0.6,
109
+ },
110
+ },
111
+ {
112
+ colorSaturation: [0.45, 0.75],
113
+ itemStyle: {
114
+ gapWidth: 1,
115
+ borderColorSaturation: 0.6,
116
+ },
117
+ },
118
+ ];
119
+ const getHierarchyDepth = (nodes, depth = 1) => {
120
+ let maxDepth = depth;
121
+ nodes.forEach((node) => {
122
+ if (node.children && node.children.length > 0) {
123
+ maxDepth = Math.max(maxDepth, getHierarchyDepth(node.children, depth + 1));
124
+ }
125
+ });
126
+ return maxDepth;
127
+ };
128
+ const flattenChartDataValues = (chartData, extractor, hiddenStrategy, skipFirstRow = false) => {
129
+ if (!chartData || chartData.length === 0)
130
+ return [];
131
+ const values = [];
132
+ chartData.forEach((entry) => {
133
+ // Inline values (`<c:strLit>` / `<c:numLit>` embedded in the chart
134
+ // XML) take precedence over cell-range sources — by spec only one
135
+ // is populated, but if both happen to exist the inline value is
136
+ // the authored "ground truth".
137
+ const inline = entry
138
+ .inlineValues;
139
+ if (inline && inline.length) {
140
+ const trimmed = skipFirstRow ? inline.slice(1) : inline;
141
+ values.push(...trimmed);
142
+ return;
143
+ }
144
+ entry.sources.forEach((source) => {
145
+ const extracted = extractor(source, hiddenStrategy) ?? [];
146
+ const trimmed = skipFirstRow && extracted.length > 0 ? extracted.slice(1) : extracted;
147
+ values.push(...trimmed);
148
+ });
149
+ });
150
+ return values;
151
+ };
152
+ const buildHierarchyLevels = (maxDepth) => {
153
+ if (maxDepth <= 0)
154
+ return undefined;
155
+ const levels = [];
156
+ for (let i = 0; i < maxDepth; i++) {
157
+ const baseStyle = HIERARCHY_LEVEL_STYLES[Math.min(i, HIERARCHY_LEVEL_STYLES.length - 1)];
158
+ levels.push({
159
+ ...baseStyle,
160
+ itemStyle: {
161
+ ...baseStyle.itemStyle,
162
+ },
163
+ });
164
+ }
165
+ return levels;
166
+ };
167
+ const defaultLegendConfig = {
168
+ padding: 10,
169
+ itemWidth: 10,
170
+ itemHeight: 4,
171
+ top: 20,
172
+ };
173
+ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, readonly, getDomainValuesFromRange, getSeriesValuesFromRange, onDeleteChart, onRequestEdit, onRequestCalculate, isSelected, }) => {
174
+ const spec = chart.spec;
175
+ const explicitChartTitle = chart.spec.title ?? undefined;
176
+ const hierarchyTitleRef = useRef(explicitChartTitle);
177
+ const title = explicitChartTitle ?? hierarchyTitleRef.current;
178
+ const el = useRef(null);
179
+ const chartRef = useRef(undefined);
180
+ const focusSheet = useFocusSheet();
181
+ const shouldFocusSheetRef = useRef(false);
182
+ const previousChartType = useRef(undefined);
183
+ const previousSeriesCount = useRef(undefined);
184
+ // 3-D pipeline state. echarts-gl is lazy-loaded the first time a 3-D
185
+ // spec renders; the tick re-runs updateChart when the module lands so
186
+ // the chart upgrades from the immediate 2-D fallback to true 3-D.
187
+ const renderedIn3D = useRef(false);
188
+ const glUpgradeRequested = useRef(false);
189
+ const [glUpgradeTick, setGlUpgradeTick] = useState(0);
190
+ // Region-map (choropleth) pipeline state. The map option (geo +
191
+ // visualMap) doesn't merge back into a cartesian one, so the instance
192
+ // is re-initialized when entering/leaving the region-map rendering.
193
+ const renderedAsRegionMap = useRef(false);
194
+ // Initialize line chart
195
+ const initializeChart = () => {
196
+ if (el.current && !chartRef.current) {
197
+ chartRef.current = echarts.init(el.current);
198
+ chartRef.current.setOption({
199
+ title: {
200
+ left: "top",
201
+ padding: 10,
202
+ textStyle: {
203
+ fontSize: 16,
204
+ fontWeight: "normal",
205
+ fontFamily: "Untitled Sans, apple-system, sans-serif",
206
+ },
207
+ },
208
+ tooltip: {
209
+ textStyle: {
210
+ fontSize: 12,
211
+ },
212
+ trigger: "axis",
213
+ },
214
+ legend: defaultLegendConfig,
215
+ grid: {
216
+ left: "3%",
217
+ right: "4%",
218
+ bottom: "3%",
219
+ containLabel: true,
220
+ },
221
+ xAxis: [
222
+ {
223
+ type: "category",
224
+ name: spec.horizontalAxisTitle ?? "", // Set your x-axis title here
225
+ nameLocation: "middle",
226
+ nameGap: 30,
227
+ },
228
+ ],
229
+ yAxis: [
230
+ {
231
+ type: "value",
232
+ name: spec.verticalAxisTitle ?? "", // Set your y-axis title here
233
+ nameLocation: "middle",
234
+ nameGap: 50,
235
+ },
236
+ ],
237
+ series: [],
238
+ });
239
+ }
240
+ };
241
+ const getSeriesData = useCallback((spec, sheetRange, options) => {
242
+ const rawSeriesValues = getSeriesValuesFromRange(sheetRange, spec.hiddenDimensionStrategy);
243
+ // Sanitize values - convert error strings like #N/A to null
244
+ const seriesValues = rawSeriesValues.map(sanitizeChartValue);
245
+ const { aggregateEnabled, aggregateType, rawDomainValues, orderedDomainValues, } = options;
246
+ const canAggregate = aggregateEnabled &&
247
+ !!aggregateType &&
248
+ rawDomainValues.length > 0 &&
249
+ orderedDomainValues.length > 0 &&
250
+ rawDomainValues.length === seriesValues.length;
251
+ if (canAggregate) {
252
+ const aggregatedValues = aggregateSeriesByDomain(rawDomainValues, seriesValues, aggregateType, orderedDomainValues);
253
+ if (spec.chartType === "pie") {
254
+ return orderedDomainValues.map((name, idx) => ({
255
+ value: aggregatedValues[idx] ?? 0,
256
+ name,
257
+ }));
258
+ }
259
+ return aggregatedValues;
260
+ }
261
+ const numericValues = seriesValues;
262
+ if (spec.chartType === "pie") {
263
+ const domainSheetRange = spec.domains[0]?.sources[0];
264
+ if (!domainSheetRange) {
265
+ return [];
266
+ }
267
+ const domainValues = getDomainValuesFromRange(domainSheetRange, spec.hiddenDimensionStrategy);
268
+ return domainValues.map((name, idx) => ({
269
+ value: numericValues[idx],
270
+ name,
271
+ }));
272
+ }
273
+ return numericValues;
274
+ }, [getSeriesValuesFromRange, getDomainValuesFromRange]);
275
+ // Resize a chart
276
+ const resizeChart = () => {
277
+ chartRef.current?.resize();
278
+ };
279
+ /**
280
+ * Triggers a chart update
281
+ */
282
+ const updateChart = async () => {
283
+ debug("Updating chart", chart);
284
+ // ---------------------------- 3-D gating ----------------------------
285
+ // 3-D / surface payloads degrade to a 2-D fallback `chartType` with the
286
+ // authored class preserved as `sourceChartType` (BAR_3D, LINE_3D,
287
+ // PIE_3D, AREA_3D, SURFACE, SURFACE_3D). When WebGL is available and
288
+ // the lazily loaded echarts-gl module is ready, the dedicated 3-D
289
+ // branch below renders a true 3-D plot; in every other case (no WebGL,
290
+ // SSR, import failure, PIE_3D) the 2-D fallback renders untouched.
291
+ // The 3-D and region-map gates key off disjoint sourceChartType
292
+ // tokens, so a spec can never satisfy both.
293
+ const threeDRenderKind = resolve3DRenderKind(spec.sourceChartType);
294
+ const canRender3D = threeDRenderKind !== null && isWebGLAvailable();
295
+ const willRender3D = canRender3D && getEchartsGlState() === "ready";
296
+ if (canRender3D && !willRender3D && getEchartsGlState() !== "failed") {
297
+ // Kick off the echarts-gl load in the background (once). The 2-D
298
+ // fallback renders immediately; the tick re-runs updateChart to
299
+ // upgrade to 3-D when the module resolves. A failed load latches
300
+ // and the chart simply stays 2-D.
301
+ if (!glUpgradeRequested.current) {
302
+ glUpgradeRequested.current = true;
303
+ loadEchartsGl().then((loaded) => {
304
+ if (loaded) {
305
+ setGlUpgradeTick((tick) => tick + 1);
306
+ }
307
+ });
308
+ }
309
+ }
310
+ if (renderedIn3D.current && !willRender3D) {
311
+ // Leaving 3-D (spec edit) — the grid3D option doesn't merge back
312
+ // into a cartesian one, so re-initialize from a clean slate and
313
+ // let the normal 2-D flow below do a full structural render.
314
+ chartRef.current?.dispose();
315
+ chartRef.current = undefined;
316
+ renderedIn3D.current = false;
317
+ previousChartType.current = undefined;
318
+ previousSeriesCount.current = undefined;
319
+ initializeChart();
320
+ applyThemeOptions();
321
+ }
322
+ // ------------------------- Region map -------------------------
323
+ // Filled map / choropleth: `chartType: "regionMap"`, or a payload
324
+ // degraded to another basic `chartType` that preserves the authored
325
+ // class as `sourceChartType: "REGION_MAP"`. Category labels are the
326
+ // region names and the first series carries the values. The geo
327
+ // assets (echarts map module + world GeoJSON) are lazy-loaded on
328
+ // first use, so non-map charts never pay for them.
329
+ const isRegionMap = isRegionMapSpec(spec);
330
+ if (renderedAsRegionMap.current && !isRegionMap) {
331
+ // Leaving the region map (spec edit) — the geo/visualMap option
332
+ // doesn't merge back into a cartesian one, so re-initialize from
333
+ // a clean slate and let the normal flow below do a full
334
+ // structural render.
335
+ chartRef.current?.dispose();
336
+ chartRef.current = undefined;
337
+ renderedAsRegionMap.current = false;
338
+ previousChartType.current = undefined;
339
+ previousSeriesCount.current = undefined;
340
+ initializeChart();
341
+ applyThemeOptions();
342
+ }
343
+ if (isRegionMap) {
344
+ const regionSpec = spec;
345
+ const geoScope = resolveGeoScope(regionSpec.geoScope);
346
+ const geo = await loadRegionMapGeo(geoScope);
347
+ // Entering the region map from a cartesian render: the previous
348
+ // grid/axis option doesn't merge into the map one, so start from
349
+ // a bare instance (theme options are re-applied after setOption).
350
+ if (!renderedAsRegionMap.current && chartRef.current) {
351
+ chartRef.current.dispose();
352
+ chartRef.current = undefined;
353
+ }
354
+ if (el.current && !chartRef.current) {
355
+ chartRef.current = echarts.init(el.current);
356
+ }
357
+ if (!geo) {
358
+ // Map asset failed to load (or unknown scope) — the loader
359
+ // already warned once; render an empty chart, never crash.
360
+ chartRef.current?.setOption({ series: [] }, { replaceMerge: ["series", "visualMap"] });
361
+ renderedAsRegionMap.current = true;
362
+ applyThemeOptions();
363
+ previousChartType.current = spec.chartType;
364
+ previousSeriesCount.current = 0;
365
+ return;
366
+ }
367
+ const labels = flattenChartDataValues(regionSpec.domains, getDomainValuesFromRange, regionSpec.hiddenDimensionStrategy, false);
368
+ const seriesEntry = regionSpec.series?.[0];
369
+ const values = seriesEntry
370
+ ? flattenChartDataValues([seriesEntry], getSeriesValuesFromRange, regionSpec.hiddenDimensionStrategy)
371
+ : [];
372
+ const { data, unmatched, min, max } = buildRegionMapData(labels, values, geo.regionNames);
373
+ warnOnceForUnmatchedRegions(geoScope, unmatched);
374
+ const textColor = getStringifiedColor({ theme: 1, tint: isDarkMode ? 1 : 0 }, theme);
375
+ // Sequential single-hue value ramp derived from the theme accent;
376
+ // regions without data keep a neutral fill so "no data" reads
377
+ // differently from "small value".
378
+ const ramp = buildSequentialColorRamp(theme.themeColors.accent1, !!isDarkMode);
379
+ const noDataColor = isDarkMode
380
+ ? "rgba(255,255,255,0.08)"
381
+ : "rgba(0,0,0,0.06)";
382
+ const borderColor = isDarkMode ? "rgba(255,255,255,0.3)" : "#FFFFFF";
383
+ const regionSeriesLabel = buildSeriesLabelOption(spec.dataLabels);
384
+ const seriesName = seriesEntry?.dataLabel && !isAFormula(seriesEntry.dataLabel)
385
+ ? seriesEntry.dataLabel
386
+ : "";
387
+ chartRef.current?.setOption({
388
+ // The visualMap color scale is the value legend; the series
389
+ // legend stays hidden unless an authored override turns it on.
390
+ legend: applyLegendOverrides({ ...defaultLegendConfig, show: false }, spec.legend),
391
+ tooltip: {
392
+ textStyle: { fontSize: 12 },
393
+ trigger: "item",
394
+ formatter: (params) => {
395
+ const value = Array.isArray(params?.value)
396
+ ? params.value[params.value.length - 1]
397
+ : params?.value;
398
+ return typeof value === "number" && Number.isFinite(value)
399
+ ? `${params?.name}: ${value}`
400
+ : `${params?.name ?? ""}`;
401
+ },
402
+ },
403
+ visualMap: [
404
+ {
405
+ type: "continuous",
406
+ min,
407
+ max,
408
+ left: "left",
409
+ bottom: 10,
410
+ inRange: { color: ramp },
411
+ textStyle: { color: textColor, fontSize: 12 },
412
+ },
413
+ ],
414
+ series: [
415
+ {
416
+ type: "map",
417
+ map: geoScope,
418
+ name: seriesName,
419
+ data,
420
+ roam: false,
421
+ // Region names label the shapes only when the authored
422
+ // dataLabels override asks for them.
423
+ label: { show: false, ...(regionSeriesLabel ?? {}) },
424
+ emphasis: { label: { show: false } },
425
+ select: { disabled: true },
426
+ itemStyle: {
427
+ areaColor: noDataColor,
428
+ borderColor,
429
+ borderWidth: 0.5,
430
+ },
431
+ },
432
+ ],
433
+ }, { replaceMerge: ["series", "visualMap", "legend", "tooltip"] });
434
+ // Re-apply theme-derived title / palette / tooltip colors — lost
435
+ // when the instance was re-initialized on entry.
436
+ renderedAsRegionMap.current = true;
437
+ applyThemeOptions();
438
+ previousChartType.current = spec.chartType;
439
+ previousSeriesCount.current = 1;
440
+ return;
441
+ }
442
+ const chartDomains = [];
443
+ const chartSeries = [];
444
+ const legend = { ...defaultLegendConfig, data: [] };
445
+ const chartType = chartTypeToEchartsType(spec?.chartType);
446
+ const position = chart.position;
447
+ const showAxis = showXAxis(spec?.chartType);
448
+ const isBubble = isBubbleChartSpec(spec);
449
+ const isHierarchy = isHierarchyChartSpec(spec);
450
+ if (isBubble) {
451
+ hierarchyTitleRef.current = explicitChartTitle;
452
+ const skipHeaders = spec.useFirstRowAsHeaders !== false;
453
+ const labelValues = flattenChartDataValues(spec.bubbleLabels, getDomainValuesFromRange, spec.hiddenDimensionStrategy, skipHeaders);
454
+ // Per-series X / size — scatter (always) and bubble (often) carry
455
+ // a separate xVal/bubbleSize per series in OOXML. Earlier code
456
+ // flattened them into a single shared array which broke when
457
+ // series had different ranges.
458
+ const perSeriesX = (spec.domains ?? []).map((dom) => flattenChartDataValues([dom], getSeriesValuesFromRange, spec.hiddenDimensionStrategy, skipHeaders));
459
+ const perSeriesSize = (spec.bubbleSizes ?? []).map((sz) => flattenChartDataValues([sz], getSeriesValuesFromRange, spec.hiddenDimensionStrategy, skipHeaders));
460
+ const seriesInputs = [];
461
+ for (let i = 0; i < spec.series.length; i++) {
462
+ const seriesEntry = spec.series[i];
463
+ let resolvedLabel = seriesEntry.dataLabel ?? "";
464
+ if (resolvedLabel && isAFormula(resolvedLabel)) {
465
+ resolvedLabel = coerceCalculatedLabel(await onRequestCalculate(resolvedLabel, position.sheetId ?? sheetId, position.overlayPosition.anchorCell.rowIndex, position.overlayPosition.anchorCell.columnIndex));
466
+ }
467
+ const xValues = perSeriesX[i] ?? perSeriesX[0];
468
+ const sizeValues = perSeriesSize[i] ?? perSeriesSize[0];
469
+ seriesInputs.push({
470
+ name: resolvedLabel,
471
+ values: flattenChartDataValues([seriesEntry], getSeriesValuesFromRange, spec.hiddenDimensionStrategy, skipHeaders),
472
+ ...(xValues ? { xValues } : {}),
473
+ ...(sizeValues ? { sizeValues } : {}),
474
+ });
475
+ }
476
+ const sharedXValues = perSeriesX[0] ?? [];
477
+ const sharedSizeValues = perSeriesSize[0] ?? [];
478
+ const { categories, series: preparedSeries, sizeValues, } = prepareBubbleSeriesData({
479
+ xValues: sharedXValues,
480
+ labelValues,
481
+ sizeValues: sharedSizeValues,
482
+ series: seriesInputs,
483
+ });
484
+ const sizeScale = createBubbleSizeScale(sizeValues);
485
+ const xAxisConfig = categories.length > 0
486
+ ? {
487
+ type: "category",
488
+ data: categories,
489
+ name: spec.horizontalAxisTitle ?? "",
490
+ nameLocation: "middle",
491
+ nameGap: 30,
492
+ }
493
+ : {
494
+ type: "value",
495
+ name: spec.horizontalAxisTitle ?? "",
496
+ nameLocation: "middle",
497
+ nameGap: 30,
498
+ };
499
+ const tooltipFormatter = (params) => {
500
+ const [rawX, y, size] = (params.value ?? []);
501
+ const label = categories.length > 0
502
+ ? params.data?.xLabel ?? categories[Math.round(rawX)] ?? rawX
503
+ : rawX;
504
+ const bubbleName = params.name ? `${params.name}<br/>` : "";
505
+ const seriesName = params.seriesName ? `${params.seriesName}<br/>` : "";
506
+ return `${seriesName}${bubbleName}X: ${label ?? ""}<br/>Y: ${y ?? ""}<br/>Size: ${size ?? ""}`;
507
+ };
508
+ // Spec-level overrides — axis min/max/visibility/textFormat, legend
509
+ // position/visibility, dataLabel toggles. Same helpers as the
510
+ // non-bubble layout below.
511
+ const bubbleSeriesLabel = buildSeriesLabelOption(spec.dataLabels);
512
+ chartRef.current?.setOption({
513
+ xAxis: [
514
+ applyAxisOverrides({ ...xAxisConfig }, spec.categoryAxisOptions),
515
+ ],
516
+ yAxis: [
517
+ applyAxisOverrides({
518
+ type: "value",
519
+ name: spec.verticalAxisTitle ?? "",
520
+ nameLocation: "middle",
521
+ nameGap: 45,
522
+ }, spec.valueAxisOptions),
523
+ ],
524
+ legend: applyLegendOverrides({
525
+ ...legend,
526
+ show: preparedSeries.length > 1,
527
+ data: preparedSeries.map((entry) => entry.name),
528
+ }, spec.legend),
529
+ grid: {
530
+ left: "3%",
531
+ right: "4%",
532
+ bottom: "3%",
533
+ containLabel: true,
534
+ },
535
+ tooltip: {
536
+ trigger: "item",
537
+ formatter: tooltipFormatter,
538
+ },
539
+ // Scatter vs bubble — scatter uses small fixed markers
540
+ // (optionally connected with a line per scatterStyle); bubble
541
+ // applies a size scale on the 3rd dimension. Pre-fix scatter
542
+ // routed through the bubble code, producing Y-scaled giant
543
+ // circles instead of Excel's small dots.
544
+ series: preparedSeries.map((entry, index) => {
545
+ const isScatter = spec.chartType === "scatter";
546
+ const scatterStyle = spec.scatterStyle ??
547
+ (isScatter ? "lineMarker" : undefined);
548
+ const seriesEntry = spec.series?.[index];
549
+ const seriesColor = seriesEntry?.color;
550
+ const perMarker = seriesEntry?.marker;
551
+ if (isScatter) {
552
+ const symbol = perMarker?.symbol;
553
+ const noMarkers = symbol === "none";
554
+ const wantsLine = scatterStyle === "line" ||
555
+ scatterStyle === "lineMarker" ||
556
+ scatterStyle === "smooth" ||
557
+ scatterStyle === "smoothMarker";
558
+ const wantsMarkers = !noMarkers &&
559
+ (scatterStyle === "marker" ||
560
+ scatterStyle === "lineMarker" ||
561
+ scatterStyle === "smoothMarker" ||
562
+ !scatterStyle);
563
+ const smooth = scatterStyle === "smooth" || scatterStyle === "smoothMarker";
564
+ return wantsLine
565
+ ? {
566
+ type: "line",
567
+ name: entry.name,
568
+ data: entry.points,
569
+ showSymbol: wantsMarkers,
570
+ smooth,
571
+ symbol: mapMarkerSymbolToEcharts(symbol) ?? "circle",
572
+ symbolSize: perMarker?.size ?? 6,
573
+ ...(seriesColor
574
+ ? {
575
+ itemStyle: { color: seriesColor },
576
+ lineStyle: { color: seriesColor },
577
+ }
578
+ : {}),
579
+ }
580
+ : {
581
+ type: "scatter",
582
+ name: entry.name,
583
+ data: entry.points,
584
+ symbol: mapMarkerSymbolToEcharts(symbol) ?? "circle",
585
+ symbolSize: perMarker?.size ?? 6,
586
+ ...(seriesColor
587
+ ? { itemStyle: { color: seriesColor } }
588
+ : {}),
589
+ };
590
+ }
591
+ return {
592
+ type: chartType,
593
+ name: entry.name,
594
+ data: entry.points,
595
+ symbolSize: (data) => sizeScale(data?.[2] ?? 0),
596
+ label: {
597
+ show: true,
598
+ formatter: (params) => params.name ?? "",
599
+ },
600
+ ...(seriesColor ? { itemStyle: { color: seriesColor } } : {}),
601
+ };
602
+ }).map((s) => bubbleSeriesLabel
603
+ ? { ...s, label: { ...(s.label ?? {}), ...bubbleSeriesLabel } }
604
+ : s),
605
+ }, {
606
+ replaceMerge: [
607
+ "series",
608
+ "xAxis",
609
+ "yAxis",
610
+ "legend",
611
+ "grid",
612
+ "tooltip",
613
+ ],
614
+ });
615
+ previousChartType.current = spec.chartType;
616
+ previousSeriesCount.current = preparedSeries.length;
617
+ return;
618
+ }
619
+ if (isHierarchy) {
620
+ const hierarchyRows = extractHierarchyRows(spec, {
621
+ getDomainValuesFromRange,
622
+ getSeriesValuesFromRange,
623
+ hiddenDimensionStrategy: spec.hiddenDimensionStrategy,
624
+ });
625
+ const headerTitle = getHierarchyHeaderTitle(spec, {
626
+ getDomainValuesFromRange,
627
+ getSeriesValuesFromRange,
628
+ hiddenDimensionStrategy: spec.hiddenDimensionStrategy,
629
+ });
630
+ hierarchyTitleRef.current = explicitChartTitle ?? headerTitle;
631
+ const hierarchyData = buildHierarchyTree(hierarchyRows);
632
+ const rootName = hierarchyData.length === 1 ? hierarchyData[0]?.name ?? "" : "";
633
+ const seriesDisplayName = explicitChartTitle ?? headerTitle ?? rootName ?? "";
634
+ const hierarchyDepth = getHierarchyDepth(hierarchyData);
635
+ const levels = buildHierarchyLevels(hierarchyDepth);
636
+ const tooltipFormatter = (params) => {
637
+ const value = Array.isArray(params.value)
638
+ ? params.value[params.value.length - 1]
639
+ : params.value ?? "";
640
+ return `${params.name}: ${value}`;
641
+ };
642
+ // Spec-level overrides — treemap/sunburst default to no legend,
643
+ // but an authored legend (visibility/position) still wins.
644
+ const hierarchySeriesLabel = buildSeriesLabelOption(spec.dataLabels);
645
+ chartRef.current?.setOption({
646
+ xAxis: [
647
+ {
648
+ type: "category",
649
+ show: false,
650
+ },
651
+ ],
652
+ yAxis: [
653
+ {
654
+ type: "value",
655
+ show: false,
656
+ },
657
+ ],
658
+ legend: applyLegendOverrides({ ...defaultLegendConfig, show: false }, spec.legend),
659
+ grid: {
660
+ left: 0,
661
+ right: 0,
662
+ top: 0,
663
+ bottom: 0,
664
+ },
665
+ tooltip: {
666
+ trigger: "item",
667
+ formatter: tooltipFormatter,
668
+ },
669
+ series: [
670
+ {
671
+ type: chartType,
672
+ name: seriesDisplayName,
673
+ rootName: rootName || seriesDisplayName || "",
674
+ data: hierarchyData,
675
+ label: {
676
+ show: true,
677
+ formatter: "{b}",
678
+ ...(hierarchySeriesLabel ?? {}),
679
+ },
680
+ upperLabel: spec.chartType === "treemap"
681
+ ? {
682
+ show: true,
683
+ padding: [0, 0, 0, 4],
684
+ }
685
+ : undefined,
686
+ levels,
687
+ colorMappingBy: "index",
688
+ },
689
+ ],
690
+ }, {
691
+ replaceMerge: [
692
+ "series",
693
+ "xAxis",
694
+ "yAxis",
695
+ "legend",
696
+ "grid",
697
+ "tooltip",
698
+ ],
699
+ });
700
+ previousChartType.current = spec.chartType;
701
+ previousSeriesCount.current = 1;
702
+ return;
703
+ }
704
+ else {
705
+ hierarchyTitleRef.current = explicitChartTitle;
706
+ }
707
+ if (!isBasicChartSpec(spec)) {
708
+ debug("Unsupported chart type", spec.chartType);
709
+ return;
710
+ }
711
+ // ----------------------------- 3-D -----------------------------
712
+ // True 3-D rendering for degraded Excel 3-D / surface payloads,
713
+ // keyed off `sourceChartType` (see the gating block above):
714
+ // - BAR_3D -> bar3D (x = category, y = series row, z = value)
715
+ // - LINE_3D -> line3D (one polyline per series at its depth row)
716
+ // - AREA_3D -> line3D (echarts-gl has no area3D)
717
+ // - SURFACE / SURFACE_3D -> surface (values grid as z-heights)
718
+ // - PIE_3D never reaches here (echarts-gl has no pie3D; the 2-D pie
719
+ // fallback renders instead — matching what Excel-degraded payloads
720
+ // did before this pipeline existed).
721
+ if (willRender3D && threeDRenderKind) {
722
+ const categories = flattenChartDataValues(spec.domains, getDomainValuesFromRange, spec.hiddenDimensionStrategy, false).map((v) => String(v ?? ""));
723
+ const threeDSeriesInputs = [];
724
+ for (const seriesEntry of spec.series ?? []) {
725
+ const dataLabel = !isNil(seriesEntry.dataLabel) && isAFormula(seriesEntry.dataLabel)
726
+ ? coerceCalculatedLabel(await onRequestCalculate(seriesEntry.dataLabel, position.sheetId ?? sheetId, position.overlayPosition.anchorCell.rowIndex, position.overlayPosition.anchorCell.columnIndex))
727
+ : seriesEntry.dataLabel ?? "";
728
+ const values = flattenChartDataValues([seriesEntry], getSeriesValuesFromRange, spec.hiddenDimensionStrategy).map(sanitizeChartValue);
729
+ threeDSeriesInputs.push({ name: dataLabel, values });
730
+ }
731
+ const seriesNames = threeDSeriesInputs.map((entry, index) => entry.name || `Series ${index + 1}`);
732
+ const threeDPalette = [
733
+ theme.themeColors.accent1,
734
+ theme.themeColors.accent2,
735
+ theme.themeColors.accent3,
736
+ theme.themeColors.accent4,
737
+ theme.themeColors.accent5,
738
+ theme.themeColors.accent6,
739
+ ];
740
+ const seriesColorAt = (index) => spec.series?.[index]?.color ??
741
+ threeDPalette[index % threeDPalette.length];
742
+ // Entering 3-D from a 2-D render: the cartesian option doesn't
743
+ // merge into a grid3D one, so re-initialize the echarts instance
744
+ // from scratch (theme options are re-applied after setOption).
745
+ if (!renderedIn3D.current && chartRef.current) {
746
+ chartRef.current.dispose();
747
+ chartRef.current = undefined;
748
+ }
749
+ if (el.current && !chartRef.current) {
750
+ chartRef.current = echarts.init(el.current);
751
+ }
752
+ // Value axes label integer positions with the category / series
753
+ // name they map to; fractional ticks stay blank.
754
+ const categoryAxisLabel = (value) => Number.isInteger(value) ? categories[value] ?? "" : "";
755
+ const seriesAxisLabel = (value) => Number.isInteger(value) ? seriesNames[value] ?? "" : "";
756
+ const threeDTooltipFormatter = (params) => {
757
+ const value = (params?.value ?? []);
758
+ const categoryLabel = categories[Math.round(value[0] ?? 0)] ?? String(value[0] ?? "");
759
+ const seriesLabel = seriesNames[Math.round(value[1] ?? 0)] ?? params?.seriesName ?? "";
760
+ return `${seriesLabel ? `${seriesLabel}<br/>` : ""}${categoryLabel}: ${value[2] ?? ""}`;
761
+ };
762
+ let threeDSeries;
763
+ let visualMap;
764
+ let xAxis3D;
765
+ let yAxis3D;
766
+ if (threeDRenderKind === "bar3D") {
767
+ xAxis3D = { type: "category", data: categories };
768
+ yAxis3D = { type: "category", data: seriesNames };
769
+ threeDSeries = [
770
+ {
771
+ type: "bar3D",
772
+ shading: "lambert",
773
+ data: buildBar3DData(threeDSeriesInputs).map(([x, y, z]) => ({
774
+ value: [x, y, z],
775
+ itemStyle: { color: seriesColorAt(y) },
776
+ })),
777
+ },
778
+ ];
779
+ }
780
+ else if (threeDRenderKind === "line3D") {
781
+ xAxis3D = {
782
+ type: "value",
783
+ axisLabel: { formatter: categoryAxisLabel },
784
+ min: 0,
785
+ max: Math.max(categories.length - 1, 1),
786
+ };
787
+ yAxis3D = {
788
+ type: "value",
789
+ axisLabel: { formatter: seriesAxisLabel },
790
+ min: 0,
791
+ max: Math.max(seriesNames.length - 1, 1),
792
+ };
793
+ threeDSeries = buildLine3DSeries(threeDSeriesInputs).map((entry, index) => ({
794
+ type: "line3D",
795
+ name: seriesNames[index],
796
+ data: entry.data,
797
+ lineStyle: { width: 3, color: seriesColorAt(index) },
798
+ }));
799
+ }
800
+ else {
801
+ const surfacePoints = buildSurfaceData(threeDSeriesInputs);
802
+ const [zMin, zMax] = getValueExtent(surfacePoints);
803
+ xAxis3D = {
804
+ type: "value",
805
+ axisLabel: { formatter: categoryAxisLabel },
806
+ min: 0,
807
+ max: Math.max(categories.length - 1, 1),
808
+ };
809
+ yAxis3D = {
810
+ type: "value",
811
+ axisLabel: { formatter: seriesAxisLabel },
812
+ min: 0,
813
+ max: Math.max(seriesNames.length - 1, 1),
814
+ };
815
+ threeDSeries = [
816
+ {
817
+ type: "surface",
818
+ data: surfacePoints,
819
+ wireframe: { show: true },
820
+ shading: "color",
821
+ },
822
+ ];
823
+ visualMap = {
824
+ show: false,
825
+ dimension: 2,
826
+ min: zMin,
827
+ max: zMax,
828
+ seriesIndex: 0,
829
+ inRange: { color: [...SURFACE_COLOR_RAMP] },
830
+ };
831
+ }
832
+ chartRef.current?.setOption({
833
+ tooltip: {
834
+ textStyle: { fontSize: 12 },
835
+ formatter: threeDTooltipFormatter,
836
+ },
837
+ legend: threeDRenderKind === "line3D"
838
+ ? applyLegendOverrides({
839
+ ...defaultLegendConfig,
840
+ show: seriesNames.length > 1,
841
+ data: seriesNames,
842
+ }, spec.legend)
843
+ : { show: false },
844
+ xAxis3D: { ...xAxis3D, name: spec.horizontalAxisTitle ?? "" },
845
+ yAxis3D: { ...yAxis3D, name: "" },
846
+ zAxis3D: { type: "value", name: spec.verticalAxisTitle ?? "" },
847
+ grid3D: {
848
+ boxDepth: Math.max(20, Math.min(100, seriesNames.length * 20)),
849
+ viewControl: { autoRotate: false },
850
+ },
851
+ ...(visualMap ? { visualMap: [visualMap] } : {}),
852
+ series: threeDSeries,
853
+ }, {
854
+ replaceMerge: [
855
+ "series",
856
+ "xAxis3D",
857
+ "yAxis3D",
858
+ "zAxis3D",
859
+ "visualMap",
860
+ "legend",
861
+ "tooltip",
862
+ ],
863
+ });
864
+ // Re-apply theme-derived title / palette / tooltip colors — lost
865
+ // when the instance was re-initialized on entry into 3-D.
866
+ renderedIn3D.current = true;
867
+ applyThemeOptions();
868
+ previousChartType.current = spec.chartType;
869
+ previousSeriesCount.current = threeDSeries.length;
870
+ return;
871
+ }
872
+ // ---------------------------- Radar ----------------------------
873
+ // Radar is polar: a top-level `radar.indicator` list (one axis per
874
+ // category) plus one `radar`-type series carrying every data series'
875
+ // value vector. radarStyle controls markers / filled area.
876
+ if (spec.chartType === "radar") {
877
+ const skipHeaders = false;
878
+ const categories = flattenChartDataValues(spec.domains, getDomainValuesFromRange, spec.hiddenDimensionStrategy, skipHeaders);
879
+ const radarSeriesInputs = [];
880
+ for (const seriesEntry of spec.series ?? []) {
881
+ const label = seriesEntry.dataLabel && !isAFormula(seriesEntry.dataLabel)
882
+ ? seriesEntry.dataLabel
883
+ : "";
884
+ const values = flattenChartDataValues([seriesEntry], getSeriesValuesFromRange, spec.hiddenDimensionStrategy).map((v) => (typeof v === "number" ? v : Number(v)));
885
+ radarSeriesInputs.push({ name: label, values });
886
+ }
887
+ const indicators = buildRadarIndicators(categories, radarSeriesInputs);
888
+ const radarData = buildRadarSeriesData(radarSeriesInputs, indicators.length);
889
+ const radarStyle = spec.radarStyle ?? "standard";
890
+ const radarColors = radarSeriesInputs.map((_, i) => spec.series?.[i]?.color);
891
+ // Spec-level overrides — radar is polar (no cartesian axes), so
892
+ // only legend + dataLabel overrides apply here.
893
+ const radarSeriesLabel = buildSeriesLabelOption(spec.dataLabels);
894
+ chartRef.current?.setOption({
895
+ radar: {
896
+ indicator: indicators.length
897
+ ? indicators
898
+ : [{ name: "", max: 1 }],
899
+ },
900
+ legend: applyLegendOverrides({
901
+ ...legend,
902
+ show: radarData.length > 1,
903
+ data: radarData.map((d) => d.name),
904
+ }, spec.legend),
905
+ tooltip: { trigger: "item" },
906
+ series: [
907
+ {
908
+ type: "radar",
909
+ symbol: radarStyle === "standard" ? "none" : "circle",
910
+ ...(radarSeriesLabel ? { label: radarSeriesLabel } : {}),
911
+ data: radarData.map((d, i) => ({
912
+ value: d.value,
913
+ name: d.name,
914
+ ...(radarStyle === "filled" ? { areaStyle: {} } : {}),
915
+ ...(radarColors[i]
916
+ ? {
917
+ itemStyle: { color: radarColors[i] },
918
+ lineStyle: { color: radarColors[i] },
919
+ }
920
+ : {}),
921
+ })),
922
+ },
923
+ ],
924
+ }, {
925
+ replaceMerge: [
926
+ "series",
927
+ "xAxis",
928
+ "yAxis",
929
+ "radar",
930
+ "legend",
931
+ "grid",
932
+ "tooltip",
933
+ ],
934
+ });
935
+ previousChartType.current = spec.chartType;
936
+ previousSeriesCount.current = radarData.length;
937
+ return;
938
+ }
939
+ // ---------------------------- Stock ----------------------------
940
+ // Line-derived stock: fan the price-role series (open/high/low/close)
941
+ // out into separate line series over the category (date) domain. The
942
+ // V-variants additionally get a volume column sub-pane below the
943
+ // price plot.
944
+ if (spec.chartType === "stock") {
945
+ const variant = (spec.stockVariant ?? "HLC");
946
+ const categories = flattenChartDataValues(spec.domains, getDomainValuesFromRange, spec.hiddenDimensionStrategy, false).map((v) => String(v ?? ""));
947
+ const seriesValues = (spec.series ?? []).map((seriesEntry) => flattenChartDataValues([seriesEntry], getSeriesValuesFromRange, spec.hiddenDimensionStrategy).map((v) => (typeof v === "number" ? v : Number(v))));
948
+ // Spec-level overrides — stock is cartesian, so axis min/max/
949
+ // visibility apply alongside legend + dataLabel toggles. They
950
+ // target the price plot's axes: the category-axis overrides land
951
+ // on the shared base x-axis (so both panes stay in sync when a
952
+ // volume pane exists), the value-axis overrides on the price
953
+ // pane's y-axis only — the volume pane keeps its own hidden-label
954
+ // value axis.
955
+ const stockSeriesLabel = buildSeriesLabelOption(spec.dataLabels);
956
+ // V-variants (VHLC / VOHLC) carry a volume series that renders as
957
+ // a column sub-pane under the price plot: two grids stacked
958
+ // vertically (price ~70% of the plot, volume below on its own
959
+ // value axis) with the category axes linked through axisPointer.
960
+ // Up days color like the candlestick body, down days like its
961
+ // shadow, so the panes read as one chart.
962
+ const upColor = theme.themeColors.accent1;
963
+ const downColor = theme.themeColors.accent2;
964
+ const volumePoints = toVolumeSeries(seriesValues, variant);
965
+ const hasVolume = (volumePoints?.length ?? 0) > 0;
966
+ const stockGrid = hasVolume
967
+ ? [
968
+ { left: "10%", right: "4%", top: "8%", height: "56%" },
969
+ { left: "10%", right: "4%", top: "74%", height: "18%" },
970
+ ]
971
+ : [{ left: "3%", right: "4%", bottom: "3%", containLabel: true }];
972
+ const baseStockXAxis = applyAxisOverrides({
973
+ type: "category",
974
+ data: categories,
975
+ show: true,
976
+ axisLine: { show: true },
977
+ axisTick: { show: true },
978
+ splitLine: { show: false },
979
+ name: spec.horizontalAxisTitle ?? "",
980
+ nameLocation: "middle",
981
+ nameGap: 30,
982
+ }, spec.categoryAxisOptions);
983
+ const stockXAxis = hasVolume
984
+ ? [
985
+ // Price pane keeps its axis line but the shared category
986
+ // labels + title live on the volume pane below. Spread after
987
+ // the overridden base so the pane's labels stay hidden even
988
+ // when the overrides carry an axisLabel text style.
989
+ {
990
+ ...baseStockXAxis,
991
+ gridIndex: 0,
992
+ axisLabel: { show: false },
993
+ axisTick: { show: false },
994
+ name: "",
995
+ },
996
+ { ...baseStockXAxis, gridIndex: 1 },
997
+ ]
998
+ : [baseStockXAxis];
999
+ const volumeYAxis = {
1000
+ type: "value",
1001
+ gridIndex: 1,
1002
+ scale: true,
1003
+ splitNumber: 2,
1004
+ axisLabel: { show: false },
1005
+ axisLine: { show: false },
1006
+ axisTick: { show: false },
1007
+ splitLine: { show: false },
1008
+ };
1009
+ const volumeSeries = hasVolume
1010
+ ? [
1011
+ {
1012
+ type: "bar",
1013
+ name: "Volume",
1014
+ xAxisIndex: 1,
1015
+ yAxisIndex: 1,
1016
+ data: (volumePoints ?? []).map((p) => ({
1017
+ value: p.value,
1018
+ itemStyle: { color: p.up ? upColor : downColor },
1019
+ })),
1020
+ },
1021
+ ]
1022
+ : [];
1023
+ // Link the panes' category axes so hovering either pane raises the
1024
+ // axis pointer (and tooltip) across both. Cleared when no volume.
1025
+ const stockAxisPointer = {
1026
+ link: hasVolume ? [{ xAxisIndex: "all" }] : [],
1027
+ };
1028
+ // OHLC / VOHLC variants carry an `open`, so they render as a
1029
+ // candlestick (Excel's open-high-low-close stock chart). HLC / VHLC
1030
+ // (no open) fall through to the line-derived rendering below.
1031
+ if (variant === "OHLC" || variant === "VOHLC") {
1032
+ const candleData = toCandlestick(seriesValues, variant);
1033
+ chartRef.current?.setOption({
1034
+ xAxis: stockXAxis,
1035
+ yAxis: [
1036
+ applyAxisOverrides({
1037
+ type: "value",
1038
+ show: true,
1039
+ scale: true,
1040
+ name: spec.verticalAxisTitle ?? "",
1041
+ nameLocation: "middle",
1042
+ nameGap: 50,
1043
+ gridIndex: 0,
1044
+ }, spec.valueAxisOptions),
1045
+ ...(hasVolume ? [volumeYAxis] : []),
1046
+ ],
1047
+ legend: applyLegendOverrides({ ...legend, show: false }, spec.legend),
1048
+ grid: stockGrid,
1049
+ tooltip: { trigger: "axis" },
1050
+ axisPointer: stockAxisPointer,
1051
+ series: [
1052
+ {
1053
+ type: "candlestick",
1054
+ name: spec.title || "Stock",
1055
+ data: candleData,
1056
+ itemStyle: {
1057
+ color: upColor,
1058
+ color0: downColor,
1059
+ borderColor: upColor,
1060
+ borderColor0: downColor,
1061
+ },
1062
+ },
1063
+ ...volumeSeries,
1064
+ ],
1065
+ }, {
1066
+ replaceMerge: [
1067
+ "series",
1068
+ "xAxis",
1069
+ "yAxis",
1070
+ "legend",
1071
+ "grid",
1072
+ "tooltip",
1073
+ ],
1074
+ });
1075
+ previousChartType.current = spec.chartType;
1076
+ previousSeriesCount.current = 1 + volumeSeries.length;
1077
+ return;
1078
+ }
1079
+ const lineSeries = toLineSeries(seriesValues, variant);
1080
+ const stockColors = [
1081
+ theme.themeColors.accent1,
1082
+ theme.themeColors.accent2,
1083
+ theme.themeColors.accent3,
1084
+ theme.themeColors.accent4,
1085
+ ];
1086
+ chartRef.current?.setOption({
1087
+ xAxis: stockXAxis,
1088
+ yAxis: [
1089
+ applyAxisOverrides({
1090
+ type: "value",
1091
+ show: true,
1092
+ name: spec.verticalAxisTitle ?? "",
1093
+ nameLocation: "middle",
1094
+ nameGap: 50,
1095
+ gridIndex: 0,
1096
+ }, spec.valueAxisOptions),
1097
+ ...(hasVolume ? [volumeYAxis] : []),
1098
+ ],
1099
+ legend: applyLegendOverrides({
1100
+ ...legend,
1101
+ show: lineSeries.length > 1,
1102
+ data: lineSeries.map((s) => s.name),
1103
+ }, spec.legend),
1104
+ grid: stockGrid,
1105
+ tooltip: { trigger: "axis" },
1106
+ axisPointer: stockAxisPointer,
1107
+ series: [
1108
+ ...lineSeries.map((s, i) => ({
1109
+ type: "line",
1110
+ name: s.name,
1111
+ data: s.values,
1112
+ showSymbol: false,
1113
+ itemStyle: { color: stockColors[i % stockColors.length] },
1114
+ lineStyle: { color: stockColors[i % stockColors.length] },
1115
+ ...(stockSeriesLabel ? { label: stockSeriesLabel } : {}),
1116
+ })),
1117
+ ...volumeSeries,
1118
+ ],
1119
+ }, {
1120
+ replaceMerge: [
1121
+ "series",
1122
+ "xAxis",
1123
+ "yAxis",
1124
+ "legend",
1125
+ "grid",
1126
+ "tooltip",
1127
+ ],
1128
+ });
1129
+ previousChartType.current = spec.chartType;
1130
+ previousSeriesCount.current = lineSeries.length + volumeSeries.length;
1131
+ return;
1132
+ }
1133
+ const isStacked = spec.stackedType && spec.stackedType !== "UNSTACKED";
1134
+ const isCircularChart = spec.chartType === "pie" || spec.chartType === "doughnut";
1135
+ // Pie-of-pie / bar-of-pie composite. The of-pie branch below fills
1136
+ // `ofPieLayout` so the grid/axis assembly at the bottom can add the
1137
+ // secondary-bar grid and the 0-1 value axes the connector series
1138
+ // needs.
1139
+ const isOfPie = isCircularChart && isOfPieChartSpec(spec);
1140
+ let ofPieLayout = null;
1141
+ const isHorizontalBarChart = spec.chartType === "bar";
1142
+ const shouldAggregate = shouldAggregateSeries(spec.series);
1143
+ const primaryDomainSource = spec?.domains?.[0]?.sources?.[0];
1144
+ const rawPrimaryDomainValues = primaryDomainSource
1145
+ ? sanitizeDomainValues(getDomainValuesFromRange(primaryDomainSource, spec.hiddenDimensionStrategy) ?? [])
1146
+ : [];
1147
+ const aggregateDomainValues = getDomainValuesForAxis(rawPrimaryDomainValues, shouldAggregate);
1148
+ spec.domains?.forEach((domain, domainIndex) => {
1149
+ domain.sources.forEach((sheetRange, sourceIndex) => {
1150
+ let axisData = [];
1151
+ if (domainIndex === 0 && sourceIndex === 0) {
1152
+ axisData = aggregateDomainValues;
1153
+ }
1154
+ else {
1155
+ const rawValues = sanitizeDomainValues(getDomainValuesFromRange(sheetRange, spec.hiddenDimensionStrategy) ?? []);
1156
+ axisData = getDomainValuesForAxis(rawValues, shouldAggregate);
1157
+ }
1158
+ const axisTitle = domainIndex === 0 && sourceIndex === 0 && !isCircularChart
1159
+ ? isHorizontalBarChart
1160
+ ? spec.verticalAxisTitle ?? ""
1161
+ : spec.horizontalAxisTitle ?? ""
1162
+ : undefined;
1163
+ chartDomains.push({
1164
+ type: "category",
1165
+ data: axisData,
1166
+ show: showAxis && !isCircularChart,
1167
+ axisLine: { show: !isCircularChart },
1168
+ axisTick: { show: !isCircularChart },
1169
+ splitLine: { show: !isCircularChart },
1170
+ name: axisTitle,
1171
+ nameLocation: "middle",
1172
+ nameGap: 30,
1173
+ });
1174
+ });
1175
+ });
1176
+ if (chartDomains.length === 0 && !isCircularChart) {
1177
+ chartDomains.push({
1178
+ type: "category",
1179
+ show: showAxis,
1180
+ axisLine: { show: true },
1181
+ axisTick: { show: true },
1182
+ splitLine: { show: true },
1183
+ gridIndex: 0,
1184
+ name: isHorizontalBarChart
1185
+ ? spec.verticalAxisTitle ?? ""
1186
+ : spec.horizontalAxisTitle ?? "",
1187
+ nameLocation: "middle",
1188
+ nameGap: 30,
1189
+ });
1190
+ }
1191
+ // Trendline + error-bar overlays are extra series appended after the
1192
+ // base series. The redraw/series-count accounting below derives from
1193
+ // the final `chartSeries.length`, so these just push onto the array.
1194
+ /**
1195
+ * Append trendline and error-bar overlay series for one data series.
1196
+ * `data` is the resolved numeric value array (may be number[] or
1197
+ * objects with a `value` field for pie — overlays are skipped for
1198
+ * non-numeric shapes).
1199
+ */
1200
+ const pushSeriesOverlays = (series, data, dataLabel) => {
1201
+ const numericValues = Array.isArray(data)
1202
+ ? data.map((v) => typeof v === "number"
1203
+ ? v
1204
+ : v && typeof v === "object" && "value" in v
1205
+ ? v.value
1206
+ : Number(v))
1207
+ : [];
1208
+ if (numericValues.length === 0)
1209
+ return;
1210
+ const seriesColor = series.color ?? theme.themeColors.accent1;
1211
+ const useSecondary = series.useSecondaryAxis ? { yAxisIndex: 1 } : {};
1212
+ // Trendlines — one overlay line series per spec.
1213
+ for (const tl of series.trendlines ?? []) {
1214
+ const result = computeTrendline(numericValues, {
1215
+ type: tl.type,
1216
+ order: tl.order,
1217
+ period: tl.period,
1218
+ forecastForward: tl.forecastForward,
1219
+ forecastBackward: tl.forecastBackward,
1220
+ });
1221
+ if (!result || result.points.length < 2)
1222
+ continue;
1223
+ const tlName = tl.name ??
1224
+ `${dataLabel ? dataLabel + " " : ""}${tl.type ?? "linear"} trend`;
1225
+ chartSeries.push({
1226
+ type: "line",
1227
+ name: tlName,
1228
+ // Index x lets the trendline overlay the category axis even
1229
+ // when forecasting extends beyond the data range.
1230
+ data: result.points.map((p) => [p.x, p.y]),
1231
+ showSymbol: false,
1232
+ smooth: tl.type === "poly",
1233
+ lineStyle: {
1234
+ color: tl.color ?? seriesColor,
1235
+ type: "dashed",
1236
+ width: 2,
1237
+ },
1238
+ itemStyle: { color: tl.color ?? seriesColor },
1239
+ tooltip: { show: false },
1240
+ z: 5,
1241
+ ...useSecondary,
1242
+ });
1243
+ }
1244
+ // Error bars — drawn as a custom series of vertical/horizontal
1245
+ // whiskers. valueType "custom" reads per-point magnitudes from the
1246
+ // spec's plus/minus ranges, resolved through the same extractor
1247
+ // the series values use.
1248
+ const magnitudes = computeErrorBars(numericValues, resolveCustomErrorBarValues(series.errorBars, (range) => getSeriesValuesFromRange(range, spec.hiddenDimensionStrategy)));
1249
+ if (magnitudes) {
1250
+ const direction = series.errorBars?.direction ?? "y";
1251
+ const noEndCap = series.errorBars?.noEndCap ?? false;
1252
+ const errorData = numericValues.map((v, i) => [
1253
+ i,
1254
+ typeof v === "number" ? v : 0,
1255
+ magnitudes.plus[i] ?? 0,
1256
+ magnitudes.minus[i] ?? 0,
1257
+ ]);
1258
+ chartSeries.push({
1259
+ type: "custom",
1260
+ name: `${dataLabel ? dataLabel + " " : ""}error`,
1261
+ data: errorData,
1262
+ z: 6,
1263
+ tooltip: { show: false },
1264
+ ...useSecondary,
1265
+ renderItem: (_params, api) => {
1266
+ const value = api.value(1);
1267
+ const plus = api.value(2);
1268
+ const minus = api.value(3);
1269
+ const capHalf = 5;
1270
+ if (direction === "x") {
1271
+ const center = api.coord([value, api.value(0)]);
1272
+ const hi = api.coord([value + plus, api.value(0)]);
1273
+ const lo = api.coord([value - minus, api.value(0)]);
1274
+ const children = [
1275
+ {
1276
+ type: "line",
1277
+ shape: { x1: lo[0], y1: center[1], x2: hi[0], y2: center[1] },
1278
+ style: { stroke: seriesColor, lineWidth: 1 },
1279
+ },
1280
+ ];
1281
+ if (!noEndCap) {
1282
+ children.push({
1283
+ type: "line",
1284
+ shape: {
1285
+ x1: hi[0],
1286
+ y1: center[1] - capHalf,
1287
+ x2: hi[0],
1288
+ y2: center[1] + capHalf,
1289
+ },
1290
+ style: { stroke: seriesColor, lineWidth: 1 },
1291
+ }, {
1292
+ type: "line",
1293
+ shape: {
1294
+ x1: lo[0],
1295
+ y1: center[1] - capHalf,
1296
+ x2: lo[0],
1297
+ y2: center[1] + capHalf,
1298
+ },
1299
+ style: { stroke: seriesColor, lineWidth: 1 },
1300
+ });
1301
+ }
1302
+ return { type: "group", children };
1303
+ }
1304
+ const center = api.coord([api.value(0), value]);
1305
+ const hi = api.coord([api.value(0), value + plus]);
1306
+ const lo = api.coord([api.value(0), value - minus]);
1307
+ const children = [
1308
+ {
1309
+ type: "line",
1310
+ shape: { x1: center[0], y1: lo[1], x2: center[0], y2: hi[1] },
1311
+ style: { stroke: seriesColor, lineWidth: 1 },
1312
+ },
1313
+ ];
1314
+ if (!noEndCap) {
1315
+ children.push({
1316
+ type: "line",
1317
+ shape: {
1318
+ x1: center[0] - capHalf,
1319
+ y1: hi[1],
1320
+ x2: center[0] + capHalf,
1321
+ y2: hi[1],
1322
+ },
1323
+ style: { stroke: seriesColor, lineWidth: 1 },
1324
+ }, {
1325
+ type: "line",
1326
+ shape: {
1327
+ x1: center[0] - capHalf,
1328
+ y1: lo[1],
1329
+ x2: center[0] + capHalf,
1330
+ y2: lo[1],
1331
+ },
1332
+ style: { stroke: seriesColor, lineWidth: 1 },
1333
+ });
1334
+ }
1335
+ return { type: "group", children };
1336
+ },
1337
+ });
1338
+ }
1339
+ };
1340
+ // Excel `<c:overlap>` / `<c:gapWidth>` → echarts barGap /
1341
+ // barCategoryGap (see utils/bar-layout.ts for the exact mapping).
1342
+ // The category-gap conversion needs the number of side-by-side bars
1343
+ // per category slot: 1 when stacked (all series share the slot),
1344
+ // otherwise the count of rendered bar/column series.
1345
+ const sideBySideBarCount = isStacked
1346
+ ? 1
1347
+ : (spec.series ?? []).reduce((count, s) => {
1348
+ const seriesType = s.seriesChartType ?? spec.chartType;
1349
+ return seriesType === "bar" || seriesType === "column"
1350
+ ? count + Math.max(1, s.sources?.length ?? 0)
1351
+ : count;
1352
+ }, 0);
1353
+ const specBarGap = overlapToBarGap(spec.overlap);
1354
+ const specBarCategoryGap = gapWidthToBarCategoryGap(spec.gapWidth, {
1355
+ seriesCount: sideBySideBarCount,
1356
+ overlap: isStacked ? 0 : spec.overlap,
1357
+ });
1358
+ for (const series of spec.series ?? []) {
1359
+ const dataLabel = !isNil(series.dataLabel) && isAFormula(series.dataLabel)
1360
+ ? coerceCalculatedLabel(await onRequestCalculate(series.dataLabel, position.sheetId ?? sheetId, position.overlayPosition.anchorCell.rowIndex, position.overlayPosition.anchorCell.columnIndex))
1361
+ : series.dataLabel ?? "";
1362
+ // Label
1363
+ if (dataLabel) {
1364
+ legend.data?.push(dataLabel);
1365
+ }
1366
+ for (const sheetRange of series.sources) {
1367
+ const data = getSeriesData(spec, sheetRange, {
1368
+ aggregateEnabled: shouldAggregate,
1369
+ aggregateType: series.aggregateType,
1370
+ rawDomainValues: rawPrimaryDomainValues,
1371
+ orderedDomainValues: aggregateDomainValues,
1372
+ });
1373
+ // Support per-series chart type for combo charts
1374
+ const effectiveChartType = series.seriesChartType ?? spec.chartType;
1375
+ const seriesEchartsType = chartTypeToEchartsType(effectiveChartType);
1376
+ const seriesStyleAttrs = getEchartsSeriesStyle(effectiveChartType);
1377
+ const seriesEntry = {
1378
+ type: seriesEchartsType,
1379
+ name: dataLabel,
1380
+ data,
1381
+ stack: isStacked ? "dataLabel" : undefined,
1382
+ smooth: spec.smooth ?? undefined,
1383
+ radius: spec.radius ?? (isCircularChart ? [0, "75%"] : undefined),
1384
+ ...seriesStyleAttrs,
1385
+ };
1386
+ // Plot this series against a secondary value axis. The 2nd
1387
+ // yAxis element is appended below (when any series opts in);
1388
+ // here we point the series at index 1.
1389
+ if (series.useSecondaryAxis) {
1390
+ seriesEntry.yAxisIndex = 1;
1391
+ }
1392
+ // Handle invertIfNegative for bar/column charts
1393
+ const isBarOrColumn = effectiveChartType === "bar" || effectiveChartType === "column";
1394
+ // Bar layout (overlap/gapWidth) — echarts reads barGap /
1395
+ // barCategoryGap from bar series (the last bar series wins for
1396
+ // the whole coordinate system), so apply them to every
1397
+ // bar/column series. Set before the gradient/noFill branches so
1398
+ // every push path carries them.
1399
+ if (isBarOrColumn) {
1400
+ if (specBarGap !== undefined) {
1401
+ seriesEntry.barGap = specBarGap;
1402
+ }
1403
+ if (specBarCategoryGap !== undefined) {
1404
+ seriesEntry.barCategoryGap = specBarCategoryGap;
1405
+ }
1406
+ }
1407
+ const seriesAny = series;
1408
+ // Gradient fill — overrides solid color when present. echarts
1409
+ // paints it natively via a LinearGradient on itemStyle.color
1410
+ // (and areaStyle for area series / lineStyle for lines).
1411
+ const gradientArgs = gradientToEcharts(series.gradient);
1412
+ if (gradientArgs && !seriesAny.noFill) {
1413
+ const gradient = new echarts.graphic.LinearGradient(gradientArgs.x, gradientArgs.y, gradientArgs.x2, gradientArgs.y2, gradientArgs.colorStops);
1414
+ seriesEntry.itemStyle = {
1415
+ ...(seriesEntry.itemStyle ?? {}),
1416
+ color: gradient,
1417
+ };
1418
+ if (effectiveChartType === "area") {
1419
+ seriesEntry.areaStyle = {
1420
+ ...(seriesEntry.areaStyle ?? {}),
1421
+ color: gradient,
1422
+ };
1423
+ }
1424
+ chartSeries.push(seriesEntry);
1425
+ pushSeriesOverlays(series, data, dataLabel);
1426
+ continue;
1427
+ }
1428
+ if (seriesAny.noFill) {
1429
+ // Transparent series (Gantt lag offset, etc.) — fill it
1430
+ // with `transparent` so layout is preserved but nothing
1431
+ // paints. Legend entries are also suppressed via `name: ""`
1432
+ // since the series carries no semantic information.
1433
+ seriesEntry.itemStyle = {
1434
+ ...(seriesEntry.itemStyle ?? {}),
1435
+ color: "transparent",
1436
+ borderColor: "transparent",
1437
+ };
1438
+ }
1439
+ else if (isBarOrColumn &&
1440
+ Array.isArray(seriesAny.dataPointColors) &&
1441
+ seriesAny.dataPointColors.length) {
1442
+ // Per-data-point color overrides — Gantt-style charts colour
1443
+ // each task differently via `<c:dPt>`. Echarts accepts an
1444
+ // `itemStyle.color` function that receives the data index;
1445
+ // we look up the override or fall back to the series color.
1446
+ const overrides = new Map();
1447
+ for (const dp of seriesAny.dataPointColors) {
1448
+ if (typeof dp.idx === "number" && typeof dp.color === "string") {
1449
+ overrides.set(dp.idx, dp.color);
1450
+ }
1451
+ }
1452
+ const fallback = series.color ?? theme.themeColors.accent1;
1453
+ seriesEntry.itemStyle = {
1454
+ ...(seriesEntry.itemStyle ?? {}),
1455
+ color: (params) => overrides.get(params.dataIndex) ?? fallback,
1456
+ };
1457
+ }
1458
+ else if (isBarOrColumn && series.invertIfNegative) {
1459
+ const positiveColor = series.color ?? theme.themeColors.accent1;
1460
+ const negativeColor = series.negativeColor ?? "#FF0000";
1461
+ // Use a color function that returns different colors based on value
1462
+ seriesEntry.itemStyle = {
1463
+ ...(seriesEntry.itemStyle ?? {}),
1464
+ color: (params) => {
1465
+ const value = Array.isArray(params.data)
1466
+ ? params.data[1]
1467
+ : params.data?.value ?? params.data;
1468
+ return typeof value === "number" && value < 0
1469
+ ? negativeColor
1470
+ : positiveColor;
1471
+ },
1472
+ };
1473
+ }
1474
+ else if (series.color) {
1475
+ seriesEntry.itemStyle = {
1476
+ ...(seriesEntry.itemStyle ?? {}),
1477
+ color: series.color,
1478
+ };
1479
+ seriesEntry.lineStyle = {
1480
+ ...(seriesEntry.lineStyle ?? {}),
1481
+ color: series.color,
1482
+ };
1483
+ if (effectiveChartType === "area") {
1484
+ seriesEntry.areaStyle = {
1485
+ ...(seriesEntry.areaStyle ?? {}),
1486
+ color: series.color,
1487
+ };
1488
+ }
1489
+ }
1490
+ chartSeries.push(seriesEntry);
1491
+ pushSeriesOverlays(series, data, dataLabel);
1492
+ }
1493
+ }
1494
+ if (isOfPie) {
1495
+ // Pie-of-pie / bar-of-pie — Excel splits the minor slices into a
1496
+ // secondary pie or stacked bar beside the main pie, replaces them
1497
+ // with an aggregated "Other" slice, and joins the two plots with
1498
+ // connector lines. The split rule comes from splitType/splitPos
1499
+ // (default: last 3 categories) and the secondary plot's size from
1500
+ // secondPieSize (percent of the main pie, default 75).
1501
+ const labels = rawPrimaryDomainValues.map((v) => String(v ?? ""));
1502
+ const seriesEntry = spec.series?.[0];
1503
+ const rawValues = seriesEntry
1504
+ ? flattenChartDataValues([seriesEntry], getSeriesValuesFromRange, spec.hiddenDimensionStrategy).map((v) => (typeof v === "number" ? v : Number(v)))
1505
+ : [];
1506
+ const { main, secondary, otherTotal, total } = splitOfPie(rawValues, labels, {
1507
+ splitType: spec.splitType,
1508
+ splitPos: spec.splitPos,
1509
+ });
1510
+ const ofPieType = resolveOfPieType(spec.ofPieType);
1511
+ const secondScale = clampSecondPieSize(spec.secondPieSize) / 100;
1512
+ const halfAngle = otherSliceHalfAngle(otherTotal, total);
1513
+ const hasSecondary = secondary.length > 0;
1514
+ // Layout, as fractions of the chart container. The main pie sits
1515
+ // left-of-center; the secondary plot on the right. Pie radii are
1516
+ // echarts-style percentages of min(width, height) / 2.
1517
+ const MAIN_CX = 0.3;
1518
+ const CY = 0.5;
1519
+ const MAIN_RADIUS_PCT = 55;
1520
+ const SECONDARY_CX = 0.78;
1521
+ const secondaryRadiusPct = Math.min(80, MAIN_RADIUS_PCT * secondScale);
1522
+ const BAR_LEFT = 0.68;
1523
+ const BAR_RIGHT = 0.88;
1524
+ // Bar-of-pie: the stacked bar's height tracks secondPieSize the
1525
+ // same way the secondary pie's diameter does.
1526
+ const barHalfFrac = Math.min(0.45, Math.max(0.08, 0.28 * secondScale));
1527
+ ofPieLayout = {
1528
+ ofPieType,
1529
+ hasSecondary,
1530
+ barGrid: ofPieType === "bar" && hasSecondary
1531
+ ? {
1532
+ left: `${BAR_LEFT * 100}%`,
1533
+ right: `${(1 - BAR_RIGHT) * 100}%`,
1534
+ top: `${(CY - barHalfFrac) * 100}%`,
1535
+ bottom: `${(1 - CY - barHalfFrac) * 100}%`,
1536
+ }
1537
+ : undefined,
1538
+ };
1539
+ chartDomains.length = 0;
1540
+ chartSeries.length = 0;
1541
+ // Palette continues from the main plot into the secondary one so
1542
+ // slices keep distinct colors across both plots (Excel behavior).
1543
+ const ofPiePalette = [
1544
+ theme.themeColors.accent1,
1545
+ theme.themeColors.accent2,
1546
+ theme.themeColors.accent3,
1547
+ theme.themeColors.accent4,
1548
+ theme.themeColors.accent5,
1549
+ theme.themeColors.accent6,
1550
+ ];
1551
+ const paletteAt = (index) => ofPiePalette[index % ofPiePalette.length];
1552
+ chartSeries.push({
1553
+ type: "pie",
1554
+ name: seriesEntry?.dataLabel ?? "",
1555
+ center: [`${MAIN_CX * 100}%`, `${CY * 100}%`],
1556
+ radius: [0, `${MAIN_RADIUS_PCT}%`],
1557
+ // Rotate the pie so the "Other" slice's angular midpoint faces
1558
+ // the secondary plot (Excel's layout). Slices sweep clockwise
1559
+ // and Other is last, ending the sweep at startAngle; putting
1560
+ // the midpoint at 0° (3 o'clock) needs startAngle 360 - 180f.
1561
+ startAngle: 360 - (halfAngle * 180) / Math.PI,
1562
+ data: main.map((s, i) => ({
1563
+ name: s.label,
1564
+ value: s.value,
1565
+ itemStyle: { color: paletteAt(i) },
1566
+ })),
1567
+ });
1568
+ if (hasSecondary) {
1569
+ if (ofPieType === "pie") {
1570
+ chartSeries.push({
1571
+ type: "pie",
1572
+ name: seriesEntry?.dataLabel ?? "",
1573
+ center: [`${SECONDARY_CX * 100}%`, `${CY * 100}%`],
1574
+ radius: [0, `${secondaryRadiusPct}%`],
1575
+ data: secondary.map((s, i) => ({
1576
+ name: s.label,
1577
+ value: s.value,
1578
+ itemStyle: { color: paletteAt(main.length + i) },
1579
+ })),
1580
+ });
1581
+ }
1582
+ else {
1583
+ // Bar-of-pie — a single-category stacked bar on the second
1584
+ // grid, one series per point so each segment keeps its own
1585
+ // color, legend entry and tooltip.
1586
+ secondary.forEach((s, i) => {
1587
+ chartSeries.push({
1588
+ type: "bar",
1589
+ name: s.label,
1590
+ stack: "rnc-of-pie-secondary",
1591
+ xAxisIndex: 1,
1592
+ yAxisIndex: 1,
1593
+ barWidth: "75%",
1594
+ data: [s.value],
1595
+ itemStyle: { color: paletteAt(main.length + i) },
1596
+ tooltip: { formatter: "{a}: {c}" },
1597
+ });
1598
+ });
1599
+ }
1600
+ // Connector lines from the Other slice's edges to the secondary
1601
+ // plot, drawn by a custom series over hidden 0-1 value axes so
1602
+ // they re-render on resize. Geometry mirrors the fractions used
1603
+ // for the pie centers/radii above.
1604
+ const connectorColor = isDarkMode
1605
+ ? "rgba(255,255,255,0.45)"
1606
+ : "#999999";
1607
+ chartSeries.push({
1608
+ type: "custom",
1609
+ name: "",
1610
+ silent: true,
1611
+ animation: false,
1612
+ tooltip: { show: false },
1613
+ z: 1,
1614
+ data: [[0, 0]],
1615
+ renderItem: (params) => {
1616
+ const cs = params?.coordSys ?? {};
1617
+ const width = cs.width ?? 0;
1618
+ const height = cs.height ?? 0;
1619
+ if (!(width > 0) || !(height > 0)) {
1620
+ return { type: "group", children: [] };
1621
+ }
1622
+ const x0 = cs.x ?? 0;
1623
+ const y0 = cs.y ?? 0;
1624
+ const r = ((MAIN_RADIUS_PCT / 100) * Math.min(width, height)) / 2;
1625
+ const cx = x0 + MAIN_CX * width;
1626
+ const cy = y0 + CY * height;
1627
+ // Boundary points of the Other slice on the main circle —
1628
+ // ±halfAngle around 3 o'clock (screen y grows downwards).
1629
+ const px = cx + r * Math.cos(halfAngle);
1630
+ const pyTop = cy - r * Math.sin(halfAngle);
1631
+ const pyBottom = cy + r * Math.sin(halfAngle);
1632
+ let tx;
1633
+ let tyTop;
1634
+ let tyBottom;
1635
+ if (ofPieType === "pie") {
1636
+ const r2 = ((secondaryRadiusPct / 100) * Math.min(width, height)) / 2;
1637
+ tx = x0 + SECONDARY_CX * width;
1638
+ tyTop = cy - r2;
1639
+ tyBottom = cy + r2;
1640
+ }
1641
+ else {
1642
+ tx = x0 + BAR_LEFT * width;
1643
+ tyTop = y0 + (CY - barHalfFrac) * height;
1644
+ tyBottom = y0 + (CY + barHalfFrac) * height;
1645
+ }
1646
+ const style = { stroke: connectorColor, lineWidth: 1 };
1647
+ return {
1648
+ type: "group",
1649
+ children: [
1650
+ {
1651
+ type: "line",
1652
+ shape: { x1: px, y1: pyTop, x2: tx, y2: tyTop },
1653
+ style,
1654
+ },
1655
+ {
1656
+ type: "line",
1657
+ shape: { x1: px, y1: pyBottom, x2: tx, y2: tyBottom },
1658
+ style,
1659
+ },
1660
+ ],
1661
+ };
1662
+ },
1663
+ });
1664
+ }
1665
+ // Legend lists every category (main + secondary); the Other slice
1666
+ // only exists in the main pie.
1667
+ legend.data = [
1668
+ ...main.map((s) => s.label),
1669
+ ...secondary.map((s) => s.label),
1670
+ ];
1671
+ }
1672
+ else if (spec.chartType === "waterfall") {
1673
+ const primaryDomainSource = spec?.domains?.[0]?.sources?.[0];
1674
+ const rawDomainValues = primaryDomainSource
1675
+ ? sanitizeDomainValues(getDomainValuesFromRange(primaryDomainSource, spec.hiddenDimensionStrategy) ?? [])
1676
+ : [];
1677
+ const labels = rawDomainValues.map((v) => String(v ?? ""));
1678
+ const seriesEntry = spec.series?.[0];
1679
+ const rawValues = seriesEntry
1680
+ ? flattenChartDataValues([seriesEntry], getSeriesValuesFromRange, spec.hiddenDimensionStrategy).map((v) => (typeof v === "number" ? v : Number(v)))
1681
+ : [];
1682
+ const totalIndices = new Set(spec.totalIndices ?? []);
1683
+ const steps = computeWaterfall(rawValues, labels, totalIndices);
1684
+ chartDomains.length = 0;
1685
+ chartDomains.push({
1686
+ type: "category",
1687
+ data: steps.map((s) => s.label),
1688
+ show: showAxis,
1689
+ axisLine: { show: true },
1690
+ axisTick: { show: true },
1691
+ splitLine: { show: false },
1692
+ name: spec.horizontalAxisTitle ?? "",
1693
+ nameLocation: "middle",
1694
+ nameGap: 30,
1695
+ });
1696
+ chartSeries.length = 0;
1697
+ chartSeries.push({
1698
+ type: "bar",
1699
+ stack: "waterfall",
1700
+ itemStyle: { color: "transparent" },
1701
+ emphasis: { itemStyle: { color: "transparent" } },
1702
+ data: steps.map((s) => s.placeholder),
1703
+ silent: true,
1704
+ });
1705
+ const accent = seriesEntry?.color ?? theme.themeColors.accent1;
1706
+ const decreaseColor = seriesEntry?.negativeColor ?? theme.themeColors.accent2 ?? accent;
1707
+ const totalColor = theme.themeColors.accent3 ?? accent;
1708
+ // Excel-style connector lines: a horizontal segment at the running
1709
+ // total after bar i, spanning across to bar i+1. Within the bars
1710
+ // the segment overlaps their top/bottom edges, so only the gap
1711
+ // between bars is visually distinct.
1712
+ const connectors = steps.slice(0, -1).map((s, i) => [
1713
+ { coord: [i, s.cumulative] },
1714
+ { coord: [i + 1, s.cumulative] },
1715
+ ]);
1716
+ chartSeries.push({
1717
+ type: "bar",
1718
+ stack: "waterfall",
1719
+ name: seriesEntry?.dataLabel ?? "",
1720
+ data: steps.map((s, i) => ({
1721
+ value: s.value,
1722
+ itemStyle: {
1723
+ color: s.isTotal
1724
+ ? totalColor
1725
+ : rawValues[i] < 0
1726
+ ? decreaseColor
1727
+ : accent,
1728
+ },
1729
+ })),
1730
+ markLine: {
1731
+ silent: true,
1732
+ symbol: "none",
1733
+ animation: false,
1734
+ label: { show: false },
1735
+ lineStyle: {
1736
+ color: isDarkMode ? "rgba(255,255,255,0.45)" : "#999999",
1737
+ width: 1,
1738
+ type: "solid",
1739
+ },
1740
+ data: connectors,
1741
+ },
1742
+ });
1743
+ legend.data = [];
1744
+ }
1745
+ else if (spec.chartType === "funnel") {
1746
+ const primaryDomainSource = spec?.domains?.[0]?.sources?.[0];
1747
+ const rawDomainValues = primaryDomainSource
1748
+ ? sanitizeDomainValues(getDomainValuesFromRange(primaryDomainSource, spec.hiddenDimensionStrategy) ?? [])
1749
+ : [];
1750
+ const labels = rawDomainValues.map((v) => String(v ?? ""));
1751
+ const seriesEntry = spec.series?.[0];
1752
+ const rawValues = seriesEntry
1753
+ ? flattenChartDataValues([seriesEntry], getSeriesValuesFromRange, spec.hiddenDimensionStrategy).map((v) => (typeof v === "number" ? v : 0))
1754
+ : [];
1755
+ chartDomains.length = 0;
1756
+ chartSeries.length = 0;
1757
+ chartSeries.push({
1758
+ type: "funnel",
1759
+ name: seriesEntry?.dataLabel ?? "",
1760
+ data: rawValues.map((value, i) => ({
1761
+ name: labels[i] ?? String(i),
1762
+ value,
1763
+ })),
1764
+ label: { show: true, position: "inside" },
1765
+ });
1766
+ legend.data = [];
1767
+ }
1768
+ else if (spec.chartType === "boxplot") {
1769
+ const quartileMethod = spec.quartileMethod;
1770
+ const showOutliers = spec.showOutliers;
1771
+ const showMeanMarker = spec.showMeanMarker === true;
1772
+ const boxStats = [];
1773
+ const boxLabels = [];
1774
+ // [categoryIndex, value] pairs for the outlier / mean overlay
1775
+ // scatter series (only populated when the options are on).
1776
+ const outlierPoints = [];
1777
+ const meanPoints = [];
1778
+ for (const seriesEntry of spec.series ?? []) {
1779
+ const label = seriesEntry.dataLabel && !isAFormula(seriesEntry.dataLabel)
1780
+ ? seriesEntry.dataLabel
1781
+ : "";
1782
+ const rawValues = flattenChartDataValues([seriesEntry], getSeriesValuesFromRange, spec.hiddenDimensionStrategy).map((v) => (typeof v === "number" ? v : Number(v)));
1783
+ const summary = computeBoxplotSummary(rawValues, { quartileMethod });
1784
+ if (summary) {
1785
+ const boxIndex = boxStats.length;
1786
+ // `showOutliers === true` clips the whiskers to the most
1787
+ // extreme data points inside the 1.5×IQR fences and plots
1788
+ // the points beyond them (Excel's "show outlier points").
1789
+ // Otherwise the whiskers span the full min/max — today's
1790
+ // rendering.
1791
+ const lo = showOutliers === true ? summary.lowerWhisker : summary.min;
1792
+ const hi = showOutliers === true ? summary.upperWhisker : summary.max;
1793
+ boxStats.push([lo, summary.q1, summary.median, summary.q3, hi]);
1794
+ boxLabels.push(label);
1795
+ if (showOutliers === true) {
1796
+ for (const outlier of summary.outliers) {
1797
+ outlierPoints.push([boxIndex, outlier]);
1798
+ }
1799
+ }
1800
+ if (showMeanMarker) {
1801
+ meanPoints.push([boxIndex, summary.mean]);
1802
+ }
1803
+ }
1804
+ }
1805
+ chartDomains.length = 0;
1806
+ chartDomains.push({
1807
+ type: "category",
1808
+ data: boxLabels,
1809
+ show: showAxis,
1810
+ axisLine: { show: true },
1811
+ axisTick: { show: true },
1812
+ splitLine: { show: false },
1813
+ name: spec.horizontalAxisTitle ?? "",
1814
+ nameLocation: "middle",
1815
+ nameGap: 30,
1816
+ });
1817
+ chartSeries.length = 0;
1818
+ const boxSeries = {
1819
+ type: "boxplot",
1820
+ name: "",
1821
+ data: boxStats,
1822
+ };
1823
+ const boxColor = spec.series?.[0]?.color;
1824
+ if (boxColor) {
1825
+ boxSeries.itemStyle = { color: boxColor, borderColor: boxColor };
1826
+ }
1827
+ chartSeries.push(boxSeries);
1828
+ if (outlierPoints.length > 0) {
1829
+ chartSeries.push({
1830
+ type: "scatter",
1831
+ name: "",
1832
+ data: outlierPoints,
1833
+ symbolSize: 6,
1834
+ ...(boxColor ? { itemStyle: { color: boxColor } } : {}),
1835
+ tooltip: { show: false },
1836
+ });
1837
+ }
1838
+ if (meanPoints.length > 0) {
1839
+ chartSeries.push({
1840
+ type: "scatter",
1841
+ name: "",
1842
+ data: meanPoints,
1843
+ symbol: "diamond",
1844
+ symbolSize: 9,
1845
+ ...(boxColor ? { itemStyle: { color: boxColor } } : {}),
1846
+ tooltip: { show: false },
1847
+ });
1848
+ }
1849
+ legend.data = [];
1850
+ }
1851
+ else if (spec.chartType === "histogram") {
1852
+ const rawValues = flattenChartDataValues(spec.series, getSeriesValuesFromRange, spec.hiddenDimensionStrategy).map((v) => (typeof v === "number" ? v : Number(v)));
1853
+ let { labels, counts } = binHistogram(rawValues, spec.binCount);
1854
+ // Pareto — a histogram whose bins are sorted descending with a
1855
+ // cumulative-% line overlaid on a secondary 0-100% axis. The
1856
+ // authored type arrives as `sourceChartType: "PARETO"` on an
1857
+ // otherwise plain histogram spec.
1858
+ const isPareto = isParetoSpec(spec);
1859
+ let cumulativePercents = [];
1860
+ if (isPareto) {
1861
+ const pareto = computePareto({ labels, counts });
1862
+ labels = pareto.labels;
1863
+ counts = pareto.counts;
1864
+ cumulativePercents = pareto.cumulativePercents;
1865
+ }
1866
+ chartDomains.length = 0;
1867
+ chartDomains.push({
1868
+ type: "category",
1869
+ data: labels,
1870
+ show: showAxis,
1871
+ axisLine: { show: true },
1872
+ axisTick: { show: true },
1873
+ splitLine: { show: false },
1874
+ name: spec.horizontalAxisTitle ?? "",
1875
+ nameLocation: "middle",
1876
+ nameGap: 30,
1877
+ });
1878
+ chartSeries.length = 0;
1879
+ const histogramSeries = {
1880
+ type: "bar",
1881
+ name: spec.series?.[0]?.dataLabel ?? "",
1882
+ data: counts,
1883
+ barCategoryGap: "1%",
1884
+ };
1885
+ const histogramColor = spec.series?.[0]?.color;
1886
+ if (histogramColor) {
1887
+ histogramSeries.itemStyle = { color: histogramColor };
1888
+ }
1889
+ chartSeries.push(histogramSeries);
1890
+ if (isPareto) {
1891
+ // Cumulative-% overlay on the secondary axis (yAxisIndex 1);
1892
+ // the matching 0-100% axis element is appended with the other
1893
+ // value axes below.
1894
+ chartSeries.push({
1895
+ type: "line",
1896
+ name: "Cumulative %",
1897
+ data: cumulativePercents,
1898
+ yAxisIndex: 1,
1899
+ symbol: "circle",
1900
+ symbolSize: 6,
1901
+ });
1902
+ }
1903
+ legend.data = [];
1904
+ }
1905
+ // Only redraw chart
1906
+ // if chart type has changed
1907
+ // if series count has changed. Use the rendered series count
1908
+ // (`chartSeries.length`) rather than `spec.series.length` because
1909
+ // trendline / error-bar overlays add extra series; comparing the
1910
+ // raw spec count would miss those and skip the structural redraw.
1911
+ const renderedSeriesCount = chartSeries.length;
1912
+ const replaceMerge = previousChartType.current !== spec.chartType ||
1913
+ previousSeriesCount.current !== renderedSeriesCount
1914
+ ? ["series", "xAxis", "yAxis", "legend", "grid", "tooltip"]
1915
+ : [];
1916
+ // Of-pie uses a full-container grid (for the connector overlay's
1917
+ // hidden 0-1 axes) plus, for bar-of-pie, a second grid hosting the
1918
+ // stacked secondary bar.
1919
+ const gridConfig = isOfPie
1920
+ ? [
1921
+ { left: 0, right: 0, top: 0, bottom: 0 },
1922
+ ...(ofPieLayout?.barGrid ? [ofPieLayout.barGrid] : []),
1923
+ ]
1924
+ : isCircularChart
1925
+ ? {
1926
+ left: 0,
1927
+ right: 0,
1928
+ top: 0,
1929
+ bottom: 0,
1930
+ }
1931
+ : {
1932
+ left: "3%",
1933
+ right: "4%",
1934
+ bottom: "3%",
1935
+ containLabel: true,
1936
+ };
1937
+ const valueAxisBase = {
1938
+ type: "value",
1939
+ show: !isCircularChart,
1940
+ axisLine: { show: !isCircularChart },
1941
+ axisTick: { show: !isCircularChart },
1942
+ splitLine: { show: !isCircularChart },
1943
+ nameLocation: "middle",
1944
+ nameGap: 50,
1945
+ };
1946
+ // Secondary (right-hand) value axis. Series with useSecondaryAxis
1947
+ // were pointed at yAxisIndex 1 in the loop above; the matching axis
1948
+ // element is appended here. Pareto histograms always carry one for
1949
+ // the cumulative-% line, clamped to 0-100%.
1950
+ const isPareto = spec.chartType === "histogram" && isParetoSpec(spec);
1951
+ const hasSecondaryAxis = !isCircularChart &&
1952
+ !isHorizontalBarChart &&
1953
+ ((spec.series ?? []).some((s) => s.useSecondaryAxis) || isPareto);
1954
+ const tooltipConfig = {
1955
+ textStyle: {
1956
+ fontSize: 12,
1957
+ },
1958
+ trigger: isCircularChart ? "item" : "axis",
1959
+ formatter: isCircularChart ? "{b}: {c} ({d}%)" : undefined,
1960
+ };
1961
+ // Phase 5.1: apply legend / axis / dataLabels / doughnut overrides.
1962
+ const finalLegend = applyLegendOverrides(legend, spec.legend);
1963
+ const seriesLabelOption = buildSeriesLabelOption(spec.dataLabels);
1964
+ // For doughnut, surface the inner-radius hole on each pie series.
1965
+ // Excel persists `holeSize` as a 0-90 percentage; we normalize to a
1966
+ // 0-1 fraction (doughnutHoleSize). Default to 0.5 when unset.
1967
+ const isDoughnut = spec.chartType === "doughnut";
1968
+ const doughnutHoleSize = spec.doughnutHoleSize;
1969
+ // PERCENT_STACKED — echarts has no built-in `barnorm: 'percent'`,
1970
+ // so we normalize per category here: each cell becomes
1971
+ // (value / category_total) * 100, while the original value is
1972
+ // tucked into a sibling field so the label formatter can still
1973
+ // surface the dollar amount. Touches only bar/column series in
1974
+ // the percent-stacked spec; other charts pass through unchanged.
1975
+ if (spec.stackedType === "PERCENT_STACKED" &&
1976
+ (spec.chartType === "bar" || spec.chartType === "column")) {
1977
+ const barSeries = chartSeries.filter((s) => s?.type === "bar");
1978
+ // Length of the value axis = max length across stacked series.
1979
+ const len = barSeries.reduce((m, s) => Math.max(m, Array.isArray(s.data) ? s.data.length : 0), 0);
1980
+ const categoryTotals = new Array(len).fill(0);
1981
+ for (const s of barSeries) {
1982
+ if (!Array.isArray(s.data))
1983
+ continue;
1984
+ for (let i = 0; i < s.data.length; i++) {
1985
+ const raw = s.data[i];
1986
+ const n = typeof raw === "number" ? raw : Number(raw);
1987
+ if (Number.isFinite(n))
1988
+ categoryTotals[i] += n;
1989
+ }
1990
+ }
1991
+ for (const s of barSeries) {
1992
+ if (!Array.isArray(s.data))
1993
+ continue;
1994
+ s.data = s.data.map((raw, i) => {
1995
+ const n = typeof raw === "number" ? raw : Number(raw);
1996
+ const total = categoryTotals[i];
1997
+ if (!Number.isFinite(n) || total === 0) {
1998
+ return { value: 0, originalValue: raw };
1999
+ }
2000
+ return { value: (n / total) * 100, originalValue: n };
2001
+ });
2002
+ }
2003
+ }
2004
+ const seriesWithLabels = chartSeries.map((s) => {
2005
+ const next = { ...s };
2006
+ if (seriesLabelOption) {
2007
+ next.label = { ...(s?.label ?? {}), ...seriesLabelOption };
2008
+ }
2009
+ if (isDoughnut && next.type === "pie") {
2010
+ const inner = typeof doughnutHoleSize === "number"
2011
+ ? Math.round(Math.max(0, Math.min(0.9, doughnutHoleSize)) * 100)
2012
+ : 50;
2013
+ next.radius = [`${inner}%`, "75%"];
2014
+ }
2015
+ return next;
2016
+ });
2017
+ // Update charts
2018
+ chartRef.current?.setOption({
2019
+ xAxis: (() => {
2020
+ if (isOfPie) {
2021
+ // Hidden 0-1 value axis on the full-container grid for the
2022
+ // connector overlay, plus a hidden single-category axis on
2023
+ // the secondary grid for bar-of-pie.
2024
+ const axes = [
2025
+ {
2026
+ type: "value",
2027
+ min: 0,
2028
+ max: 1,
2029
+ show: false,
2030
+ axisLine: { show: false },
2031
+ axisTick: { show: false },
2032
+ splitLine: { show: false },
2033
+ },
2034
+ ];
2035
+ if (ofPieLayout?.barGrid) {
2036
+ axes.push({
2037
+ type: "category",
2038
+ data: [""],
2039
+ gridIndex: 1,
2040
+ show: false,
2041
+ axisLine: { show: false },
2042
+ axisTick: { show: false },
2043
+ splitLine: { show: false },
2044
+ });
2045
+ }
2046
+ return axes;
2047
+ }
2048
+ if (isCircularChart) {
2049
+ return [
2050
+ {
2051
+ type: "category",
2052
+ show: false,
2053
+ axisLine: { show: false },
2054
+ axisTick: { show: false },
2055
+ splitLine: { show: false },
2056
+ },
2057
+ ];
2058
+ }
2059
+ if (isHorizontalBarChart) {
2060
+ return [
2061
+ applyAxisOverrides({
2062
+ ...valueAxisBase,
2063
+ name: spec.horizontalAxisTitle ?? "",
2064
+ }, spec.valueAxisOptions),
2065
+ ];
2066
+ }
2067
+ const base = chartDomains.length > 0
2068
+ ? chartDomains
2069
+ : [
2070
+ {
2071
+ type: "category",
2072
+ show: showAxis,
2073
+ axisLine: { show: true },
2074
+ axisTick: { show: true },
2075
+ splitLine: { show: true },
2076
+ name: spec.horizontalAxisTitle ?? "",
2077
+ nameLocation: "middle",
2078
+ nameGap: 30,
2079
+ },
2080
+ ];
2081
+ return base.map((axis) => applyAxisOverrides(axis, spec.categoryAxisOptions));
2082
+ })(),
2083
+ yAxis: (() => {
2084
+ if (isOfPie) {
2085
+ const axes = [
2086
+ {
2087
+ ...valueAxisBase,
2088
+ min: 0,
2089
+ max: 1,
2090
+ show: false,
2091
+ axisLine: { show: false },
2092
+ axisTick: { show: false },
2093
+ splitLine: { show: false },
2094
+ name: undefined,
2095
+ },
2096
+ ];
2097
+ if (ofPieLayout?.barGrid) {
2098
+ axes.push({
2099
+ type: "value",
2100
+ gridIndex: 1,
2101
+ show: false,
2102
+ axisLine: { show: false },
2103
+ axisTick: { show: false },
2104
+ splitLine: { show: false },
2105
+ });
2106
+ }
2107
+ return axes;
2108
+ }
2109
+ if (isCircularChart) {
2110
+ return [
2111
+ {
2112
+ ...valueAxisBase,
2113
+ show: false,
2114
+ axisLine: { show: false },
2115
+ axisTick: { show: false },
2116
+ splitLine: { show: false },
2117
+ name: undefined,
2118
+ },
2119
+ ];
2120
+ }
2121
+ if (isHorizontalBarChart) {
2122
+ const base = chartDomains.length > 0
2123
+ ? chartDomains
2124
+ : [
2125
+ {
2126
+ type: "category",
2127
+ show: showAxis,
2128
+ axisLine: { show: true },
2129
+ axisTick: { show: true },
2130
+ splitLine: { show: true },
2131
+ name: spec.verticalAxisTitle ?? "",
2132
+ nameLocation: "middle",
2133
+ nameGap: 30,
2134
+ },
2135
+ ];
2136
+ return base.map((axis) => applyAxisOverrides(axis, spec.categoryAxisOptions));
2137
+ }
2138
+ const axes = [
2139
+ applyAxisOverrides({ ...valueAxisBase, name: spec.verticalAxisTitle ?? "" }, spec.valueAxisOptions),
2140
+ ];
2141
+ if (hasSecondaryAxis) {
2142
+ axes.push(applyAxisOverrides({
2143
+ ...valueAxisBase,
2144
+ name: "",
2145
+ splitLine: { show: false },
2146
+ // Pareto — the cumulative-% axis is clamped 0-100%.
2147
+ ...(isPareto
2148
+ ? {
2149
+ min: 0,
2150
+ max: 100,
2151
+ axisLabel: { formatter: "{value}%" },
2152
+ }
2153
+ : {}),
2154
+ }, spec.secondaryValueAxisOptions));
2155
+ }
2156
+ return axes;
2157
+ })(),
2158
+ series: seriesWithLabels,
2159
+ legend: finalLegend,
2160
+ grid: gridConfig,
2161
+ tooltip: tooltipConfig,
2162
+ }, { replaceMerge, lazyUpdate: true });
2163
+ previousChartType.current = spec.chartType;
2164
+ previousSeriesCount.current = renderedSeriesCount;
2165
+ };
2166
+ useIsomorphicLayoutEffect(() => {
2167
+ echarts.use([
2168
+ TitleComponent,
2169
+ TooltipComponent,
2170
+ GridComponent,
2171
+ LegendComponent,
2172
+ RadarComponent,
2173
+ BarChart,
2174
+ BoxplotChart,
2175
+ CandlestickChart,
2176
+ CustomChart,
2177
+ FunnelChart,
2178
+ LineChart,
2179
+ PieChart,
2180
+ EchartsRadarChart,
2181
+ ScatterChart,
2182
+ EchartsTreemapChart,
2183
+ SunburstChart,
2184
+ CanvasRenderer,
2185
+ DatasetComponent,
2186
+ ]);
2187
+ initializeChart();
2188
+ }, []);
2189
+ // Subscribe to all chart data sources (domains, series, etc.)
2190
+ const chartDataRanges = useMemo(() => getChartDataRanges(chart.spec), [chart.spec]);
2191
+ useSubscribeToMultipleRanges(chart.chartId, chartDataRanges, updateChart);
2192
+ // Theme-derived chart properties (title/legend/tooltip colors, series
2193
+ // palette). Shared by the theme effect below and the 3-D / region-map
2194
+ // pipelines, which re-initialize the echarts instance when switching
2195
+ // between the cartesian 2-D option and the grid3D / map one and must
2196
+ // re-apply these.
2197
+ const applyThemeOptions = () => {
2198
+ if (chartRef.current) {
2199
+ const textColor = getStringifiedColor({
2200
+ theme: 1,
2201
+ tint: isDarkMode ? 1 : 0,
2202
+ }, theme);
2203
+ const bgColor = getStringifiedColor({ theme: 0, tint: isDarkMode ? -1 : 1 }, theme);
2204
+ const showAxis = showXAxis(chart.spec.chartType);
2205
+ // Authored chart-level text formats override the theme-derived
2206
+ // defaults. Title, legend, and data-label colors flow through
2207
+ // here; per-axis label colors land on each axis via
2208
+ // `applyAxisOverrides` later in the layout build.
2209
+ const authoredTitleFmt = chart.spec.titleTextFormat;
2210
+ const authoredLegendFmt = chart.spec.legend?.textFormat;
2211
+ const authoredDataLabelFmt = chart.spec.dataLabels?.textFormat;
2212
+ const options = {
2213
+ title: {
2214
+ text: title ?? undefined,
2215
+ textStyle: {
2216
+ color: authoredTitleFmt?.color ?? textColor,
2217
+ ...(authoredTitleFmt?.fontFamily
2218
+ ? { fontFamily: authoredTitleFmt.fontFamily }
2219
+ : {}),
2220
+ ...(typeof authoredTitleFmt?.fontSize === "number"
2221
+ ? { fontSize: authoredTitleFmt.fontSize }
2222
+ : {}),
2223
+ ...(authoredTitleFmt?.bold ? { fontWeight: "bold" } : {}),
2224
+ ...(authoredTitleFmt?.italic ? { fontStyle: "italic" } : {}),
2225
+ },
2226
+ },
2227
+ legend: {
2228
+ textStyle: {
2229
+ color: authoredLegendFmt?.color ?? textColor,
2230
+ ...(authoredLegendFmt?.fontFamily
2231
+ ? { fontFamily: authoredLegendFmt.fontFamily }
2232
+ : {}),
2233
+ ...(typeof authoredLegendFmt?.fontSize === "number"
2234
+ ? { fontSize: authoredLegendFmt.fontSize }
2235
+ : {}),
2236
+ },
2237
+ },
2238
+ axisLabel: {
2239
+ color: authoredDataLabelFmt?.color ?? textColor,
2240
+ },
2241
+ tooltip: {
2242
+ backgroundColor: bgColor,
2243
+ textStyle: {
2244
+ color: textColor,
2245
+ },
2246
+ },
2247
+ color: [
2248
+ theme.themeColors.accent1,
2249
+ theme.themeColors.accent2,
2250
+ theme.themeColors.accent3,
2251
+ theme.themeColors.accent4,
2252
+ theme.themeColors.accent5,
2253
+ theme.themeColors.accent6,
2254
+ ],
2255
+ };
2256
+ // Skip when rendering 3-D or a region map: merging a cartesian
2257
+ // yAxis into a grid3D / map option would create a stray empty
2258
+ // 2-D axis over the WebGL scene / choropleth.
2259
+ if (showAxis &&
2260
+ !renderedIn3D.current &&
2261
+ !renderedAsRegionMap.current) {
2262
+ options.yAxis = {
2263
+ splitLine: {
2264
+ lineStyle: {
2265
+ color: isDarkMode ? "rgba(255,255,255,0.3)" : "#CCCCCC",
2266
+ },
2267
+ },
2268
+ };
2269
+ }
2270
+ chartRef.current.setOption(options);
2271
+ }
2272
+ };
2273
+ // Update chart properties
2274
+ useIsomorphicLayoutEffect(() => {
2275
+ applyThemeOptions();
2276
+ }, [theme, title, isDarkMode, chart.spec.chartType]);
2277
+ // Draw chart. `glUpgradeTick` re-runs the draw when the lazily loaded
2278
+ // echarts-gl module resolves, upgrading a 3-D spec's immediate 2-D
2279
+ // fallback to the true 3-D rendering.
2280
+ useIsomorphicLayoutEffect(() => {
2281
+ updateChart();
2282
+ }, [
2283
+ chart.spec,
2284
+ getDomainValuesFromRange,
2285
+ getSeriesValuesFromRange,
2286
+ glUpgradeTick,
2287
+ ]);
2288
+ // Resize chart
2289
+ useIsomorphicLayoutEffect(() => {
2290
+ resizeChart();
2291
+ }, [
2292
+ chart.position.overlayPosition.widthPixels,
2293
+ chart.position.overlayPosition.heightPixels,
2294
+ ]);
2295
+ // Chrome bug
2296
+ useOnVisibilityChange((state) => state === "visible" ? resizeChart() : null);
2297
+ useIsomorphicLayoutEffect(() => {
2298
+ chartRef.current?.resize({ silent: true });
2299
+ }, [width, height]);
2300
+ const onDownloadChart = () => {
2301
+ if (chartRef.current) {
2302
+ const url = chartRef.current.getDataURL({
2303
+ type: "png",
2304
+ backgroundColor: getStringifiedColor({ theme: 0, tint: isDarkMode ? -1 : 1 }, theme),
2305
+ });
2306
+ createDownloadableFile(chart.spec.title ?? "Chart", url);
2307
+ }
2308
+ };
2309
+ // Excel's "Rounded Corners" chart style (`<c:roundedCorners val="1"/>`
2310
+ // at the chartSpace root). Default per OOXML §21.2.2.183 is square,
2311
+ // so only opt in when the workbook explicitly set the flag — otherwise
2312
+ // every imported chart would silently gain a radius.
2313
+ const roundedCornersStyle = spec.roundedCorners
2314
+ ? { borderRadius: 8, overflow: "hidden" }
2315
+ : undefined;
2316
+ return (React.createElement("div", { className: "flex flex-1", style: roundedCornersStyle },
2317
+ isSelected ? (React.createElement(DropdownMenu, null,
2318
+ React.createElement(DropdownMenuTrigger, { asChild: true },
2319
+ React.createElement(IconButton, { className: "absolute right-1 top-1 z-10" },
2320
+ React.createElement(DotsVerticalIcon, null))),
2321
+ React.createElement(DropdownMenuPortal, null,
2322
+ React.createElement(DropdownMenuContent, { align: "start", onCloseAutoFocus: (event) => {
2323
+ event.preventDefault();
2324
+ if (shouldFocusSheetRef.current) {
2325
+ focusSheet?.();
2326
+ }
2327
+ }, onInteractOutside: () => {
2328
+ shouldFocusSheetRef.current = false;
2329
+ } },
2330
+ readonly ? null : (React.createElement(DropdownMenuItem, { onClick: () => onRequestEdit?.(chart) }, "Edit chart")),
2331
+ readonly ? null : (React.createElement(DropdownMenuItem, { onClick: () => {
2332
+ shouldFocusSheetRef.current = true;
2333
+ onDeleteChart?.(chart.chartId);
2334
+ } }, "Delete chart")),
2335
+ React.createElement(DropdownMenuSub, null,
2336
+ React.createElement(DropdownMenuSubTrigger, null,
2337
+ "Download chart",
2338
+ React.createElement(DropdownRightSlot, null,
2339
+ React.createElement(ChevronRightIcon, null))),
2340
+ React.createElement(DropdownMenuSubContent, null,
2341
+ React.createElement(DropdownMenuItem, { onClick: () => onDownloadChart?.() }, "PNG Image (.png)"))))))) : null,
2342
+ React.createElement("div", { className: "flex-1", ref: el })));
2343
+ };