@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,1682 @@
1
+ import { DotsVerticalIcon } from "@rowsncolumns/icons";
2
+ import { getStringifiedColor, useFocusSheet } from "@rowsncolumns/spreadsheet";
3
+ import { useSubscribeToSheet } from "@rowsncolumns/spreadsheet-state";
4
+ import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuPortal, DropdownMenuTrigger, IconButton, useIsomorphicLayoutEffect, useOnVisibilityChange, } from "@rowsncolumns/ui";
5
+ import { debug, isAFormula, isNil, ssfFormat } from "@rowsncolumns/utils";
6
+ import React, { useCallback, useRef, useState } from "react";
7
+ import Plot from "react-plotly.js";
8
+ import { aggregateSeriesByDomain, getDomainValuesForAxis, sanitizeDomainValues, shouldAggregateSeries, } from "../../utils/aggregation";
9
+ import { computeBoxplotSummary } from "../../utils/boxplot";
10
+ import { createBubbleSizeScale, prepareBubbleSeriesData, toLabelString, } from "../../utils/bubble";
11
+ import { coerceCalculatedLabel } from "../../utils/chart-spec-utils";
12
+ import { computeErrorBars, resolveCustomErrorBarValues, } from "../../utils/error-bars";
13
+ import { gradientToPlotly } from "../../utils/gradient";
14
+ import { extractHierarchyRows, getHierarchyHeaderTitle, toPlotlyHierarchy, } from "../../utils/hierarchy";
15
+ import { binHistogram, computePareto, isParetoSpec, } from "../../utils/histogram";
16
+ import { clampSecondPieSize, isOfPieChartSpec, otherSliceHalfAngle, resolveOfPieType, splitOfPie, } from "../../utils/of-pie";
17
+ import { buildRadarIndicators } from "../../utils/radar";
18
+ import { buildSequentialColorRamp, isRegionMapSpec, resolveGeoScope, WORLD_GEO_SCOPE, } from "../../utils/region-map";
19
+ import { assignStockRoles, toLineSeries, toVolumeSeries, } from "../../utils/stock";
20
+ import { computeTrendline } from "../../utils/trendline";
21
+ import { chartTypeToPlotlyType, getPlotlyTraceStyle, mapMarkerSymbolToPlotly, showXAxis, } from "./helpers";
22
+ const BASIC_CHART_TYPES = new Set([
23
+ "bar",
24
+ "column",
25
+ "line",
26
+ "pie",
27
+ "doughnut",
28
+ "area",
29
+ "radar",
30
+ "stock",
31
+ "histogram",
32
+ "boxplot",
33
+ "waterfall",
34
+ "funnel",
35
+ "combo",
36
+ "regionMap",
37
+ ]);
38
+ // Plotly ships its own world geometry; these are the `layout.geo.scope`
39
+ // values it accepts. Any other authored geoScope falls back to "world".
40
+ const PLOTLY_GEO_SCOPES = new Set([
41
+ "world",
42
+ "usa",
43
+ "europe",
44
+ "asia",
45
+ "africa",
46
+ "north america",
47
+ "south america",
48
+ ]);
49
+ // -------------------------- Phase 5.1 helpers --------------------------
50
+ /** Merge a parsed `ChartTextFormat` onto a plotly font object. */
51
+ const applyTextFormatToFont = (font, fmt) => {
52
+ if (!fmt)
53
+ return font;
54
+ const next = { ...(font ?? {}) };
55
+ if (fmt.color)
56
+ next.color = fmt.color;
57
+ if (fmt.fontFamily)
58
+ next.family = fmt.fontFamily;
59
+ if (typeof fmt.fontSize === "number")
60
+ next.size = fmt.fontSize;
61
+ return next;
62
+ };
63
+ const applyLegendOverridesPlotly = (legend, opts) => {
64
+ if (!opts)
65
+ return { legend, showlegend: undefined };
66
+ if (opts.position === "none") {
67
+ return { legend, showlegend: false };
68
+ }
69
+ const next = { ...(legend ?? {}) };
70
+ switch (opts.position) {
71
+ case "top":
72
+ next.orientation = "h";
73
+ next.x = 0.5;
74
+ next.xanchor = "center";
75
+ next.y = 1.1;
76
+ break;
77
+ case "bottom":
78
+ next.orientation = "h";
79
+ next.x = 0.5;
80
+ next.xanchor = "center";
81
+ next.y = -0.2;
82
+ break;
83
+ case "left":
84
+ next.orientation = "v";
85
+ next.x = -0.2;
86
+ next.y = 0.5;
87
+ next.yanchor = "middle";
88
+ break;
89
+ case "right":
90
+ next.orientation = "v";
91
+ next.x = 1.1;
92
+ next.y = 0.5;
93
+ next.yanchor = "middle";
94
+ break;
95
+ case "topRight":
96
+ next.orientation = "v";
97
+ next.x = 1;
98
+ next.y = 1;
99
+ next.xanchor = "right";
100
+ next.yanchor = "top";
101
+ break;
102
+ }
103
+ // Authored legend `<c:txPr>` overrides the theme-derived default.
104
+ next.font = applyTextFormatToFont(next.font, opts.textFormat);
105
+ return { legend: next, showlegend: true };
106
+ };
107
+ const applyAxisOverridesPlotly = (axis, opts) => {
108
+ if (!opts)
109
+ return axis;
110
+ const next = { ...(axis ?? {}) };
111
+ if (opts.logarithmic)
112
+ next.type = "log";
113
+ if (opts.reverse)
114
+ next.autorange = "reversed";
115
+ if (typeof opts.min === "number" || typeof opts.max === "number") {
116
+ next.range = [opts.min ?? null, opts.max ?? null];
117
+ }
118
+ if (opts.visible === false)
119
+ next.visible = false;
120
+ if (opts.showMajorGridlines === false)
121
+ next.showgrid = false;
122
+ if (opts.showMajorGridlines === true)
123
+ next.showgrid = true;
124
+ // Authored axis-label `<c:txPr>` — affects tick labels (`tickfont`)
125
+ // and the axis title font (`title.font`).
126
+ if (opts.textFormat) {
127
+ next.tickfont = applyTextFormatToFont(next.tickfont, opts.textFormat);
128
+ next.title = {
129
+ ...(next.title ?? {}),
130
+ font: applyTextFormatToFont(next.title?.font, opts.textFormat),
131
+ };
132
+ }
133
+ return next;
134
+ };
135
+ const PLOTLY_DLBL_POS_PIE = {
136
+ center: "inside",
137
+ inside: "inside",
138
+ outside: "outside",
139
+ insideEnd: "inside",
140
+ outsideEnd: "outside",
141
+ bestFit: "outside",
142
+ };
143
+ const PLOTLY_DLBL_POS_BAR = {
144
+ center: "inside",
145
+ inside: "inside",
146
+ outside: "outside",
147
+ insideEnd: "inside",
148
+ outsideEnd: "outside",
149
+ bestFit: "outside",
150
+ };
151
+ const applyDataLabelsToTracePlotly = (trace, opts, chartType) => {
152
+ if (!opts?.show)
153
+ return trace;
154
+ const next = { ...trace };
155
+ const isCircular = chartType === "pie" || chartType === "doughnut";
156
+ const posMap = isCircular ? PLOTLY_DLBL_POS_PIE : PLOTLY_DLBL_POS_BAR;
157
+ const position = opts.position ? posMap[opts.position] ?? "inside" : "inside";
158
+ if (isCircular) {
159
+ // Plotly's pie trace uses `textinfo` to control which fields render.
160
+ const parts = [];
161
+ if (opts.showCategoryName !== false && opts.showCategoryName)
162
+ parts.push("label");
163
+ if (opts.showValue !== false)
164
+ parts.push("value");
165
+ if (opts.showPercent)
166
+ parts.push("percent");
167
+ next.textinfo = parts.length ? parts.join("+") : "value";
168
+ next.textposition = position;
169
+ }
170
+ else {
171
+ // <c:dLbls><c:numFmt formatCode=…> — when authored, run each value
172
+ // through the same numfmt grammar the spreadsheet uses (`"$"#,##0`
173
+ // → `$1,831`). Plotly supports this natively via `texttemplate`
174
+ // with the d3-format mini-language, but our patterns are
175
+ // SpreadsheetML — easier to pre-format the strings.
176
+ const pattern = opts.numberFormatPattern;
177
+ const yVals = Array.isArray(next.y) ? next.y : [];
178
+ next.text = pattern
179
+ ? yVals.map((v) => (v === null || v === undefined ? "" : ssfFormat(pattern, v)))
180
+ : next.y;
181
+ next.textposition = position;
182
+ }
183
+ return next;
184
+ };
185
+ const BUBBLE_CHART_TYPES = new Set([
186
+ "bubble",
187
+ "scatter",
188
+ ]);
189
+ const HIERARCHY_CHART_TYPES = new Set([
190
+ "treemap",
191
+ "sunburst",
192
+ ]);
193
+ const isBasicChartSpec = (spec) => BASIC_CHART_TYPES.has(spec.chartType);
194
+ const isBubbleChartSpec = (spec) => BUBBLE_CHART_TYPES.has(spec.chartType);
195
+ const isHierarchyChartSpec = (spec) => HIERARCHY_CHART_TYPES.has(spec.chartType);
196
+ const flattenChartDataValues = (chartData, extractor, hiddenStrategy, skipFirstRow = false) => {
197
+ if (!chartData || chartData.length === 0)
198
+ return [];
199
+ const values = [];
200
+ chartData.forEach((entry) => {
201
+ // Inline values (`<c:strLit>` / `<c:numLit>`) take precedence over
202
+ // cell-range sources — only one is populated by spec.
203
+ const inline = entry
204
+ .inlineValues;
205
+ if (inline && inline.length) {
206
+ const trimmed = skipFirstRow ? inline.slice(1) : inline;
207
+ values.push(...trimmed);
208
+ return;
209
+ }
210
+ entry.sources.forEach((source) => {
211
+ const extracted = extractor(source, hiddenStrategy) ?? [];
212
+ const trimmed = skipFirstRow && extracted.length > 0 ? extracted.slice(1) : extracted;
213
+ values.push(...trimmed);
214
+ });
215
+ });
216
+ return values;
217
+ };
218
+ // Excel error values that should be treated as null in charts
219
+ const EXCEL_ERROR_VALUES = new Set([
220
+ "#N/A",
221
+ "#REF!",
222
+ "#VALUE!",
223
+ "#DIV/0!",
224
+ "#NAME?",
225
+ "#NULL!",
226
+ "#NUM!",
227
+ "#ERROR!",
228
+ "#GETTING_DATA",
229
+ ]);
230
+ /**
231
+ * Check if a value is an Excel error string
232
+ */
233
+ const isErrorValue = (value) => {
234
+ if (typeof value !== "string")
235
+ return false;
236
+ return EXCEL_ERROR_VALUES.has(value.toUpperCase());
237
+ };
238
+ /**
239
+ * Sanitize a value for chart display - convert errors to undefined
240
+ */
241
+ const sanitizeChartValue = (value) => {
242
+ if (value === null || value === undefined)
243
+ return undefined;
244
+ if (isErrorValue(value))
245
+ return undefined;
246
+ if (typeof value === "number" && !isNaN(value))
247
+ return value;
248
+ if (typeof value === "string") {
249
+ const num = parseFloat(value);
250
+ return isNaN(num) ? undefined : num;
251
+ }
252
+ return undefined;
253
+ };
254
+ /**
255
+ * Add alpha transparency to a color (hex or rgb)
256
+ */
257
+ const addAlphaToColor = (color, alpha) => {
258
+ if (!color)
259
+ return `rgba(0, 0, 0, ${alpha})`;
260
+ // Handle hex colors
261
+ if (color.startsWith("#")) {
262
+ const hex = color.slice(1);
263
+ const r = parseInt(hex.slice(0, 2), 16);
264
+ const g = parseInt(hex.slice(2, 4), 16);
265
+ const b = parseInt(hex.slice(4, 6), 16);
266
+ return `rgba(${r}, ${g}, ${b}, ${alpha})`;
267
+ }
268
+ // Handle rgb/rgba colors
269
+ if (color.startsWith("rgb")) {
270
+ const match = color.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/);
271
+ if (match) {
272
+ return `rgba(${match[1]}, ${match[2]}, ${match[3]}, ${alpha})`;
273
+ }
274
+ }
275
+ return color;
276
+ };
277
+ export const BasicChart = ({ width, height, chart, theme, isDarkMode, sheetId, readonly, getDomainValuesFromRange, getSeriesValuesFromRange, onDeleteChart, onRequestEdit, onRequestCalculate, isSelected, }) => {
278
+ const spec = chart.spec;
279
+ const hierarchyTitleRef = useRef(undefined);
280
+ const explicitChartTitle = chart.spec.title ?? undefined;
281
+ const title = explicitChartTitle ?? hierarchyTitleRef.current;
282
+ const plotRef = useRef(null);
283
+ const focusSheet = useFocusSheet();
284
+ const shouldFocusSheetRef = useRef(false);
285
+ const [plotData, setPlotData] = useState([]);
286
+ const [layout, setLayout] = useState({});
287
+ const getSeriesData = useCallback((spec, sheetRange, options) => {
288
+ const rawSeriesValues = getSeriesValuesFromRange(sheetRange, spec.hiddenDimensionStrategy);
289
+ // Sanitize values - convert error strings like #N/A to null
290
+ const sanitizedValues = rawSeriesValues.map(sanitizeChartValue);
291
+ const { aggregateEnabled, aggregateType, rawDomainValues, orderedDomainValues, } = options;
292
+ const canAggregate = aggregateEnabled &&
293
+ !!aggregateType &&
294
+ rawDomainValues.length > 0 &&
295
+ orderedDomainValues.length > 0 &&
296
+ rawDomainValues.length === sanitizedValues.length;
297
+ if (canAggregate) {
298
+ const aggregatedValues = aggregateSeriesByDomain(rawDomainValues, sanitizedValues, aggregateType, orderedDomainValues).map((value) => value ?? 0);
299
+ if ((spec.chartType === "pie" || spec.chartType === "doughnut")) {
300
+ return {
301
+ values: aggregatedValues,
302
+ labels: orderedDomainValues,
303
+ };
304
+ }
305
+ return aggregatedValues;
306
+ }
307
+ // Use sanitized values - errors like #N/A become undefined
308
+ const seriesValues = sanitizedValues;
309
+ if ((spec.chartType === "pie" || spec.chartType === "doughnut")) {
310
+ const domainSheetRange = spec.domains[0]?.sources[0];
311
+ if (!domainSheetRange) {
312
+ return { values: [], labels: [] };
313
+ }
314
+ const domainValues = getDomainValuesFromRange(domainSheetRange, spec.hiddenDimensionStrategy);
315
+ return {
316
+ values: seriesValues,
317
+ labels: domainValues,
318
+ };
319
+ }
320
+ return seriesValues;
321
+ }, [getSeriesValuesFromRange, getDomainValuesFromRange]);
322
+ /**
323
+ * Triggers a chart update
324
+ */
325
+ const updateChart = async () => {
326
+ debug("Updating chart", chart);
327
+ const traces = [];
328
+ const chartType = chartTypeToPlotlyType(spec?.chartType);
329
+ const position = chart.position;
330
+ const isBubble = isBubbleChartSpec(spec);
331
+ const isHierarchy = isHierarchyChartSpec(spec);
332
+ const textColor = getStringifiedColor({
333
+ theme: 1,
334
+ tint: isDarkMode ? 1 : 0,
335
+ }, theme);
336
+ const bgColor = getStringifiedColor({ theme: 0, tint: isDarkMode ? -1 : 1 }, theme);
337
+ const gridColor = isDarkMode ? "rgba(255,255,255,0.3)" : "#CCCCCC";
338
+ if (isBubble) {
339
+ const skipHeaders = spec.useFirstRowAsHeaders !== false;
340
+ const labels = flattenChartDataValues(spec.bubbleLabels, getDomainValuesFromRange, spec.hiddenDimensionStrategy, skipHeaders).map(toLabelString);
341
+ // Per-series resolved values. Scatter (always) and bubble (often)
342
+ // carry one xVal/bubbleSize per series in OOXML — the parser
343
+ // produces `spec.domains` and `spec.bubbleSizes` with one entry
344
+ // per series. Earlier code flattened all of them into a single
345
+ // shared X, which broke when series had different ranges. Now
346
+ // each series picks its own slot; we still expose a shared
347
+ // `xValues` for callers that pass a single domain (back-compat).
348
+ const perSeriesX = (spec.domains ?? []).map((dom) => flattenChartDataValues([dom], getSeriesValuesFromRange, spec.hiddenDimensionStrategy, skipHeaders));
349
+ const perSeriesSize = (spec.bubbleSizes ?? []).map((sz) => flattenChartDataValues([sz], getSeriesValuesFromRange, spec.hiddenDimensionStrategy, skipHeaders));
350
+ const seriesInputs = [];
351
+ for (let i = 0; i < spec.series.length; i++) {
352
+ const seriesEntry = spec.series[i];
353
+ let resolvedLabel = seriesEntry.dataLabel ?? "";
354
+ if (resolvedLabel && isAFormula(resolvedLabel)) {
355
+ resolvedLabel = coerceCalculatedLabel(await onRequestCalculate(resolvedLabel, chart.position.sheetId ?? sheetId, chart.position.overlayPosition.anchorCell.rowIndex, chart.position.overlayPosition.anchorCell.columnIndex));
356
+ }
357
+ // Per-series X, with fallback to the first domain so single-X
358
+ // bubble charts still work.
359
+ const xValues = perSeriesX[i] ?? perSeriesX[0];
360
+ const sizeValues = perSeriesSize[i] ?? perSeriesSize[0];
361
+ seriesInputs.push({
362
+ name: resolvedLabel,
363
+ values: flattenChartDataValues([seriesEntry], getSeriesValuesFromRange, spec.hiddenDimensionStrategy, skipHeaders),
364
+ ...(xValues ? { xValues } : {}),
365
+ ...(sizeValues ? { sizeValues } : {}),
366
+ });
367
+ }
368
+ // Shared fallbacks (legacy bubble charts with a single domain).
369
+ const sharedXValues = perSeriesX[0] ?? [];
370
+ const sharedSizeValues = perSeriesSize[0] ?? [];
371
+ const { categories, series: preparedSeries, sizeValues, } = prepareBubbleSeriesData({
372
+ xValues: sharedXValues,
373
+ labelValues: labels,
374
+ sizeValues: sharedSizeValues,
375
+ series: seriesInputs,
376
+ });
377
+ const sizeScale = createBubbleSizeScale(sizeValues);
378
+ const hasStringX = categories.length > 0;
379
+ const palette = [
380
+ theme.themeColors.accent1,
381
+ theme.themeColors.accent2,
382
+ theme.themeColors.accent3,
383
+ theme.themeColors.accent4,
384
+ theme.themeColors.accent5,
385
+ theme.themeColors.accent6,
386
+ ];
387
+ // Scatter and bubble share the same "BubbleChartSpec" shape but
388
+ // render very differently. Scatter is small fixed-size markers
389
+ // (optionally connected by a line per `scatterStyle`); bubble
390
+ // applies a size scale so the third dimension shows as bubble
391
+ // diameter. Treating scatter through the bubble path produced
392
+ // huge bubbles sized by Y values (the original mismatch with
393
+ // Excel's scatter chart).
394
+ const isScatter = spec.chartType === "scatter";
395
+ const scatterStyle = spec.scatterStyle ?? (isScatter ? "lineMarker" : undefined);
396
+ const plotlyModeForScatterStyle = (style) => {
397
+ switch (style) {
398
+ case "line":
399
+ return "lines";
400
+ case "smooth":
401
+ return "lines";
402
+ case "marker":
403
+ return "markers";
404
+ case "smoothMarker":
405
+ return "lines+markers";
406
+ case "lineMarker":
407
+ default:
408
+ return "lines+markers";
409
+ }
410
+ };
411
+ const lineShapeFor = (style) => style === "smooth" || style === "smoothMarker"
412
+ ? "spline"
413
+ : "linear";
414
+ const traces = preparedSeries.map((entry, index) => {
415
+ const xValues = entry.points.map((point) => hasStringX
416
+ ? point.xLabel ??
417
+ categories[Math.round(point.value[0])] ??
418
+ point.value[0]
419
+ : point.value[0]);
420
+ const yValues = entry.points.map((point) => point.value[1]);
421
+ const labelValuesForSeries = entry.points.map((point) => point.name);
422
+ const seriesColor = spec.series[index]?.color ?? palette[index % palette.length];
423
+ const perSeriesMarker = spec.series[index]?.marker ?? undefined;
424
+ if (isScatter) {
425
+ // Per-series marker.symbol = "none" → pure line (no markers).
426
+ // Per-series marker.size overrides the default. Falls back to
427
+ // chart-level scatterStyle for the line/marker mix.
428
+ const symbol = perSeriesMarker?.symbol;
429
+ const noMarkers = symbol === "none";
430
+ const mode = noMarkers
431
+ ? "lines"
432
+ : plotlyModeForScatterStyle(scatterStyle);
433
+ const markerSize = perSeriesMarker?.size ?? 6;
434
+ const markerColor = perSeriesMarker?.color ?? seriesColor;
435
+ return {
436
+ type: "scatter",
437
+ mode,
438
+ x: xValues,
439
+ y: yValues,
440
+ text: labelValuesForSeries,
441
+ name: entry.name,
442
+ hovertemplate: "%{text}<br>X: %{x}<br>Y: %{y}<extra></extra>",
443
+ line: {
444
+ shape: lineShapeFor(scatterStyle),
445
+ color: seriesColor,
446
+ width: 2,
447
+ },
448
+ ...(noMarkers
449
+ ? {}
450
+ : {
451
+ marker: {
452
+ size: markerSize,
453
+ color: markerColor,
454
+ symbol: mapMarkerSymbolToPlotly(symbol),
455
+ },
456
+ }),
457
+ };
458
+ }
459
+ // Bubble — third dimension scaled into marker diameter.
460
+ const rawSizes = entry.points.map((point) => point.value[2]);
461
+ const scaledSizes = rawSizes.map((value) => sizeScale(value));
462
+ return {
463
+ type: "scatter",
464
+ mode: "markers",
465
+ x: xValues,
466
+ y: yValues,
467
+ text: labelValuesForSeries,
468
+ customdata: rawSizes,
469
+ name: entry.name,
470
+ hovertemplate: "%{text}<br>X: %{x}<br>Y: %{y}<br>Size: %{customdata}<extra></extra>",
471
+ marker: {
472
+ size: scaledSizes,
473
+ sizemode: "diameter",
474
+ color: seriesColor,
475
+ opacity: 0.7,
476
+ },
477
+ };
478
+ });
479
+ // Resolve text formats: chart-authored values (via OOXML <c:txPr>)
480
+ // override the theme-derived defaults. `spec.titleTextFormat`,
481
+ // `spec.categoryAxisOptions.textFormat`, etc. all land here.
482
+ const titleFmt = spec.titleTextFormat;
483
+ const xAxisFmt = spec.categoryAxisOptions?.textFormat;
484
+ const yAxisFmt = spec.valueAxisOptions?.textFormat;
485
+ const legendFmt = spec.legend?.textFormat;
486
+ const bubbleLayout = {
487
+ title: {
488
+ text: title ?? undefined,
489
+ font: {
490
+ family: titleFmt?.fontFamily ?? "Untitled Sans, apple-system, sans-serif",
491
+ size: titleFmt?.fontSize ?? 16,
492
+ color: titleFmt?.color ?? textColor,
493
+ },
494
+ x: 0.02,
495
+ xanchor: "left",
496
+ },
497
+ xaxis: {
498
+ title: {
499
+ text: spec.horizontalAxisTitle ?? "",
500
+ font: { color: xAxisFmt?.color ?? textColor },
501
+ },
502
+ gridcolor: gridColor,
503
+ tickfont: {
504
+ color: xAxisFmt?.color ?? textColor,
505
+ ...(xAxisFmt?.fontFamily ? { family: xAxisFmt.fontFamily } : {}),
506
+ ...(xAxisFmt?.fontSize ? { size: xAxisFmt.fontSize } : {}),
507
+ },
508
+ type: hasStringX ? "category" : "linear",
509
+ },
510
+ yaxis: {
511
+ title: {
512
+ text: spec.verticalAxisTitle ?? "",
513
+ font: { color: yAxisFmt?.color ?? textColor },
514
+ },
515
+ gridcolor: gridColor,
516
+ tickfont: {
517
+ color: yAxisFmt?.color ?? textColor,
518
+ ...(yAxisFmt?.fontFamily ? { family: yAxisFmt.fontFamily } : {}),
519
+ ...(yAxisFmt?.fontSize ? { size: yAxisFmt.fontSize } : {}),
520
+ },
521
+ },
522
+ legend: {
523
+ font: {
524
+ color: legendFmt?.color ?? textColor,
525
+ ...(legendFmt?.fontFamily ? { family: legendFmt.fontFamily } : {}),
526
+ ...(legendFmt?.fontSize ? { size: legendFmt.fontSize } : {}),
527
+ },
528
+ },
529
+ paper_bgcolor: bgColor,
530
+ plot_bgcolor: bgColor,
531
+ showlegend: preparedSeries.length > 1,
532
+ margin: { l: 60, r: 20, t: 50, b: 50 },
533
+ };
534
+ setPlotData(traces);
535
+ setLayout(bubbleLayout);
536
+ return;
537
+ }
538
+ if (isHierarchy) {
539
+ const hierarchyRows = extractHierarchyRows(spec, {
540
+ getDomainValuesFromRange,
541
+ getSeriesValuesFromRange,
542
+ hiddenDimensionStrategy: spec.hiddenDimensionStrategy,
543
+ });
544
+ const headerTitle = getHierarchyHeaderTitle(spec, {
545
+ getDomainValuesFromRange,
546
+ getSeriesValuesFromRange,
547
+ hiddenDimensionStrategy: spec.hiddenDimensionStrategy,
548
+ });
549
+ hierarchyTitleRef.current = explicitChartTitle ?? headerTitle;
550
+ const hierarchyData = toPlotlyHierarchy(hierarchyRows);
551
+ const seriesName = hierarchyData.labels.length === 1
552
+ ? hierarchyData.labels[0] ?? ""
553
+ : explicitChartTitle ?? headerTitle ?? "";
554
+ const hierarchyTrace = {
555
+ type: chartType,
556
+ labels: hierarchyData.labels,
557
+ parents: hierarchyData.parents,
558
+ values: hierarchyData.values,
559
+ name: seriesName,
560
+ textinfo: "label+value",
561
+ branchvalues: "total",
562
+ };
563
+ const layoutForHierarchy = {
564
+ title: {
565
+ text: title ?? undefined,
566
+ font: {
567
+ family: "Untitled Sans, apple-system, sans-serif",
568
+ size: 16,
569
+ color: textColor,
570
+ },
571
+ x: 0.02,
572
+ xanchor: "left",
573
+ },
574
+ font: {
575
+ family: "Untitled Sans, apple-system, sans-serif",
576
+ size: 12,
577
+ color: textColor,
578
+ },
579
+ paper_bgcolor: bgColor,
580
+ plot_bgcolor: bgColor,
581
+ showlegend: false,
582
+ margin: { l: 20, r: 20, t: 50, b: 20 },
583
+ };
584
+ setPlotData([hierarchyTrace]);
585
+ setLayout(layoutForHierarchy);
586
+ return;
587
+ }
588
+ hierarchyTitleRef.current = explicitChartTitle;
589
+ if (!isBasicChartSpec(spec)) {
590
+ debug("Unsupported chart type", spec.chartType);
591
+ return;
592
+ }
593
+ // ------------------------- Region map -------------------------
594
+ // Filled map / choropleth (`chartType: "regionMap"`, or a payload
595
+ // degraded to another basic type carrying `sourceChartType:
596
+ // "REGION_MAP"`). Plotly ships its own world geometry, so this maps
597
+ // straight onto the built-in `choropleth` trace with
598
+ // `locationmode: "country names"` — plotly does its own (lenient)
599
+ // country-name matching and silently skips labels it can't place.
600
+ if (isRegionMapSpec(spec)) {
601
+ const labels = flattenChartDataValues(spec.domains, getDomainValuesFromRange, spec.hiddenDimensionStrategy, false).map((v) => String(v ?? "").trim());
602
+ const seriesEntry = spec.series?.[0];
603
+ const rawValues = seriesEntry
604
+ ? flattenChartDataValues([seriesEntry], getSeriesValuesFromRange, spec.hiddenDimensionStrategy).map((v) => (typeof v === "number" ? v : Number(v)))
605
+ : [];
606
+ const locations = [];
607
+ const z = [];
608
+ labels.forEach((label, i) => {
609
+ const value = rawValues[i];
610
+ if (label && typeof value === "number" && Number.isFinite(value)) {
611
+ locations.push(label);
612
+ z.push(value);
613
+ }
614
+ });
615
+ // Sequential single-hue ramp from the theme accent (same helper
616
+ // the echarts renderer uses), expressed as a plotly colorscale.
617
+ const ramp = buildSequentialColorRamp(theme.themeColors.accent1, !!isDarkMode);
618
+ const colorscale = ramp.map((color, i) => [i / (ramp.length - 1), color]);
619
+ const geoScope = resolveGeoScope(spec.geoScope);
620
+ const regionTrace = {
621
+ type: "choropleth",
622
+ locations,
623
+ z,
624
+ locationmode: "country names",
625
+ colorscale,
626
+ marker: {
627
+ line: {
628
+ color: isDarkMode ? "rgba(255,255,255,0.3)" : "#FFFFFF",
629
+ width: 0.5,
630
+ },
631
+ },
632
+ colorbar: {
633
+ thickness: 10,
634
+ outlinewidth: 0,
635
+ tickfont: { color: textColor, size: 12 },
636
+ },
637
+ hovertemplate: "%{location}: %{z}<extra></extra>",
638
+ };
639
+ setPlotData([regionTrace]);
640
+ setLayout({
641
+ title: { text: title ?? undefined },
642
+ font: { color: textColor },
643
+ paper_bgcolor: bgColor,
644
+ plot_bgcolor: bgColor,
645
+ showlegend: false,
646
+ geo: {
647
+ scope: (PLOTLY_GEO_SCOPES.has(geoScope)
648
+ ? geoScope
649
+ : WORLD_GEO_SCOPE),
650
+ bgcolor: bgColor,
651
+ showframe: false,
652
+ showcoastlines: false,
653
+ // Regions without data keep a neutral land fill so "no data"
654
+ // reads differently from "small value".
655
+ showland: true,
656
+ landcolor: isDarkMode
657
+ ? "rgba(255,255,255,0.08)"
658
+ : "rgba(0,0,0,0.06)",
659
+ },
660
+ margin: { l: 20, r: 20, t: 50, b: 20 },
661
+ autosize: true,
662
+ });
663
+ return;
664
+ }
665
+ // ---------------------------- Radar ----------------------------
666
+ // Plotly renders radar via `scatterpolar` traces: r = the values,
667
+ // theta = the category names (closed by appending the first point).
668
+ if (spec.chartType === "radar") {
669
+ const categories = flattenChartDataValues(spec.domains, getDomainValuesFromRange, spec.hiddenDimensionStrategy, false).map((v) => String(v ?? ""));
670
+ const radarStyle = spec.radarStyle ?? "standard";
671
+ const radarTraces = [];
672
+ const radarInputs = [];
673
+ for (const seriesEntry of spec.series ?? []) {
674
+ const label = seriesEntry.dataLabel && !isAFormula(seriesEntry.dataLabel)
675
+ ? seriesEntry.dataLabel
676
+ : "";
677
+ const values = flattenChartDataValues([seriesEntry], getSeriesValuesFromRange, spec.hiddenDimensionStrategy).map((v) => (typeof v === "number" ? v : Number(v)));
678
+ radarInputs.push({ name: label, values });
679
+ }
680
+ // buildRadarIndicators keeps category/value alignment consistent
681
+ // with the echarts renderer (and validates we have axes).
682
+ const indicators = buildRadarIndicators(categories, radarInputs);
683
+ for (let i = 0; i < radarInputs.length; i++) {
684
+ const input = radarInputs[i];
685
+ const seriesColor = spec.series?.[i]?.color ?? undefined;
686
+ const r = indicators.map((_, idx) => input.values[idx] ?? 0);
687
+ const theta = indicators.map((ind) => ind.name);
688
+ radarTraces.push({
689
+ type: "scatterpolar",
690
+ r: r.length ? [...r, r[0]] : r,
691
+ theta: theta.length ? [...theta, theta[0]] : theta,
692
+ name: input.name,
693
+ mode: radarStyle === "standard" ? "lines" : "lines+markers",
694
+ ...(radarStyle === "filled" ? { fill: "toself" } : {}),
695
+ ...(seriesColor ? { line: { color: seriesColor } } : {}),
696
+ });
697
+ }
698
+ const maxR = indicators.reduce((m, ind) => Math.max(m, ind.max), 0);
699
+ setPlotData(radarTraces);
700
+ setLayout({
701
+ title: { text: title ?? undefined },
702
+ font: { color: textColor },
703
+ paper_bgcolor: bgColor,
704
+ plot_bgcolor: bgColor,
705
+ showlegend: radarTraces.length > 1,
706
+ polar: {
707
+ radialaxis: {
708
+ visible: true,
709
+ range: [0, maxR > 0 ? maxR : 1],
710
+ },
711
+ },
712
+ margin: { l: 50, r: 50, t: 50, b: 50 },
713
+ autosize: true,
714
+ });
715
+ return;
716
+ }
717
+ // ---------------------------- Stock ----------------------------
718
+ // Line-derived stock: one scatter line trace per price role. The
719
+ // V-variants additionally get a volume column sub-pane below the
720
+ // price plot.
721
+ if (spec.chartType === "stock") {
722
+ const variant = (spec.stockVariant ?? "HLC");
723
+ const categories = flattenChartDataValues(spec.domains, getDomainValuesFromRange, spec.hiddenDimensionStrategy, false).map((v) => String(v ?? ""));
724
+ const seriesValues = (spec.series ?? []).map((seriesEntry) => flattenChartDataValues([seriesEntry], getSeriesValuesFromRange, spec.hiddenDimensionStrategy).map((v) => (typeof v === "number" ? v : Number(v))));
725
+ // V-variants (VHLC / VOHLC) carry a volume series drawn as a
726
+ // column sub-pane under the price plot: the shared x-axis is
727
+ // split into two stacked y-axis domains (price ~70%, volume
728
+ // ~22%) and the columns are colored by up/down day to match the
729
+ // price plot.
730
+ const volumePoints = toVolumeSeries(seriesValues, variant);
731
+ const hasVolume = (volumePoints?.length ?? 0) > 0;
732
+ const buildVolumeTrace = (xs) => ({
733
+ type: "bar",
734
+ x: xs,
735
+ y: (volumePoints ?? []).map((p) => p.value),
736
+ name: "Volume",
737
+ yaxis: "y2",
738
+ showlegend: false,
739
+ marker: {
740
+ color: (volumePoints ?? []).map((p) => p.up ? theme.themeColors.accent1 : theme.themeColors.accent2),
741
+ },
742
+ });
743
+ const volumeLayout = hasVolume
744
+ ? {
745
+ yaxis2: {
746
+ domain: [0, 0.22],
747
+ automargin: true,
748
+ showgrid: false,
749
+ showticklabels: false,
750
+ },
751
+ }
752
+ : {};
753
+ // The price y-axis keeps the top of the plot when a volume pane
754
+ // sits below it.
755
+ const priceYAxisDomain = hasVolume ? { domain: [0.3, 1] } : {};
756
+ // OHLC / VOHLC variants render as a native candlestick; HLC / VHLC
757
+ // (no open) fall through to the line-derived rendering below.
758
+ if (variant === "OHLC" || variant === "VOHLC") {
759
+ const roles = assignStockRoles(seriesValues, variant);
760
+ const xs = categories.length > 0
761
+ ? categories
762
+ : (roles.close ?? []).map((_, i) => i + 1);
763
+ const candleTrace = {
764
+ type: "candlestick",
765
+ x: xs,
766
+ open: (roles.open ?? []).map((v) => v ?? null),
767
+ high: (roles.high ?? []).map((v) => v ?? null),
768
+ low: (roles.low ?? []).map((v) => v ?? null),
769
+ close: (roles.close ?? []).map((v) => v ?? null),
770
+ increasing: { line: { color: theme.themeColors.accent1 } },
771
+ decreasing: { line: { color: theme.themeColors.accent2 } },
772
+ };
773
+ setPlotData(hasVolume ? [candleTrace, buildVolumeTrace(xs)] : [candleTrace]);
774
+ setLayout({
775
+ title: { text: title ?? undefined },
776
+ font: { color: textColor },
777
+ paper_bgcolor: bgColor,
778
+ plot_bgcolor: bgColor,
779
+ showlegend: false,
780
+ xaxis: {
781
+ title: { text: spec.horizontalAxisTitle ?? "" },
782
+ automargin: true,
783
+ showgrid: true,
784
+ gridcolor: gridColor,
785
+ rangeslider: { visible: false },
786
+ },
787
+ yaxis: {
788
+ title: { text: spec.verticalAxisTitle ?? "" },
789
+ automargin: true,
790
+ showgrid: true,
791
+ gridcolor: gridColor,
792
+ ...priceYAxisDomain,
793
+ },
794
+ ...volumeLayout,
795
+ margin: { l: 50, r: 50, t: 50, b: 50 },
796
+ autosize: true,
797
+ });
798
+ return;
799
+ }
800
+ const lineSeries = toLineSeries(seriesValues, variant);
801
+ const stockPalette = [
802
+ theme.themeColors.accent1,
803
+ theme.themeColors.accent2,
804
+ theme.themeColors.accent3,
805
+ theme.themeColors.accent4,
806
+ ];
807
+ const xs = categories.length > 0
808
+ ? categories
809
+ : (lineSeries[0]?.values ?? []).map((_, i) => i + 1);
810
+ const stockTraces = lineSeries.map((s, i) => ({
811
+ type: "scatter",
812
+ mode: "lines",
813
+ x: xs,
814
+ y: s.values.map((v) => (v ?? null)),
815
+ name: s.name,
816
+ line: { color: stockPalette[i % stockPalette.length] },
817
+ }));
818
+ setPlotData(hasVolume ? [...stockTraces, buildVolumeTrace(xs)] : stockTraces);
819
+ setLayout({
820
+ title: { text: title ?? undefined },
821
+ font: { color: textColor },
822
+ paper_bgcolor: bgColor,
823
+ plot_bgcolor: bgColor,
824
+ showlegend: stockTraces.length > 1,
825
+ xaxis: {
826
+ title: { text: spec.horizontalAxisTitle ?? "" },
827
+ automargin: true,
828
+ showgrid: true,
829
+ gridcolor: gridColor,
830
+ },
831
+ yaxis: {
832
+ title: { text: spec.verticalAxisTitle ?? "" },
833
+ automargin: true,
834
+ showgrid: true,
835
+ gridcolor: gridColor,
836
+ ...priceYAxisDomain,
837
+ },
838
+ ...volumeLayout,
839
+ margin: { l: 50, r: 50, t: 50, b: 50 },
840
+ autosize: true,
841
+ });
842
+ return;
843
+ }
844
+ const isStacked = spec.stackedType && spec.stackedType !== "UNSTACKED";
845
+ const isHorizontalBarChart = spec.chartType === "bar";
846
+ // Get domain data (x-axis)
847
+ const shouldAggregate = shouldAggregateSeries(spec.series);
848
+ const primaryDomainSource = spec?.domains?.[0]?.sources?.[0];
849
+ const rawPrimaryDomainValues = primaryDomainSource
850
+ ? sanitizeDomainValues(getDomainValuesFromRange(primaryDomainSource, spec.hiddenDimensionStrategy) ?? [])
851
+ : [];
852
+ let domainData = getDomainValuesForAxis(rawPrimaryDomainValues, shouldAggregate);
853
+ spec.domains?.forEach((domain, domainIndex) => {
854
+ domain.sources.forEach((sheetRange, sourceIndex) => {
855
+ if (domainIndex === 0 && sourceIndex === 0) {
856
+ domainData = getDomainValuesForAxis(rawPrimaryDomainValues, shouldAggregate);
857
+ }
858
+ else {
859
+ const rawValues = sanitizeDomainValues(getDomainValuesFromRange(sheetRange, spec.hiddenDimensionStrategy) ?? []);
860
+ domainData = getDomainValuesForAxis(rawValues, shouldAggregate);
861
+ }
862
+ });
863
+ });
864
+ const assignCartesianData = (categories, values) => {
865
+ // Convert undefined to null for Plotly (Datum type accepts null but not undefined)
866
+ const plotlyValues = values.map((v) => v ?? null);
867
+ // When categories is empty but we have values, generate index-based x values
868
+ // so Plotly can render the chart
869
+ const effectiveCategories = categories.length === 0 && plotlyValues.length > 0
870
+ ? Array.from({ length: plotlyValues.length }, (_, i) => i + 1)
871
+ : categories;
872
+ if (isHorizontalBarChart) {
873
+ return {
874
+ x: plotlyValues,
875
+ y: effectiveCategories,
876
+ orientation: "h",
877
+ };
878
+ }
879
+ return { x: effectiveCategories, y: plotlyValues, orientation: undefined };
880
+ };
881
+ // Theme color palette for fallback
882
+ const themeColorPalette = [
883
+ theme.themeColors.accent1,
884
+ theme.themeColors.accent2,
885
+ theme.themeColors.accent3,
886
+ theme.themeColors.accent4,
887
+ theme.themeColors.accent5,
888
+ theme.themeColors.accent6,
889
+ ];
890
+ const applyTraceColor = (trace, seriesColor, options) => {
891
+ const target = trace;
892
+ const chartTypeForStyling = options?.effectiveChartType ?? spec.chartType;
893
+ const isBarOrColumn = chartTypeForStyling === "bar" || chartTypeForStyling === "column";
894
+ // Transparent series (Gantt lag offset). Set marker color to
895
+ // `rgba(0,0,0,0)` so the bar layout is preserved but invisible.
896
+ if (options?.noFill) {
897
+ target.marker = {
898
+ ...(target.marker ?? {}),
899
+ color: "rgba(0,0,0,0)",
900
+ line: {
901
+ ...(target.marker?.line ?? {}),
902
+ color: "rgba(0,0,0,0)",
903
+ width: 0,
904
+ },
905
+ };
906
+ return trace;
907
+ }
908
+ // Get effective color - gradient fallback wins, then series color,
909
+ // then theme palette. Plotly cannot paint the real gradient, so the
910
+ // median stop stands in for the whole ramp.
911
+ const effectiveColor = options?.gradientColor ??
912
+ seriesColor ??
913
+ (options?.seriesIndex !== undefined
914
+ ? themeColorPalette[options.seriesIndex % themeColorPalette.length]
915
+ : undefined);
916
+ // Per-data-point color overrides — Gantt charts use `<c:dPt>` to
917
+ // give every task its own color. Build a per-point color array
918
+ // by category index, falling back to the series color for points
919
+ // without an override.
920
+ if (isBarOrColumn &&
921
+ Array.isArray(options?.dataPointColors) &&
922
+ options?.dataPointColors.length) {
923
+ const overrides = new Map();
924
+ for (const dp of options.dataPointColors) {
925
+ if (typeof dp.idx === "number" && typeof dp.color === "string") {
926
+ overrides.set(dp.idx, dp.color);
927
+ }
928
+ }
929
+ const fallback = effectiveColor ?? "#4285F4";
930
+ const yArray = Array.isArray(target.y)
931
+ ? target.y
932
+ : Array.isArray(target.x)
933
+ ? target.x
934
+ : [];
935
+ const colors = yArray.map((_, i) => overrides.get(i) ?? fallback);
936
+ target.marker = { ...(target.marker ?? {}), color: colors };
937
+ return trace;
938
+ }
939
+ // Handle invertIfNegative for bar/column charts
940
+ if (isBarOrColumn && options?.invertIfNegative && options?.values) {
941
+ const positiveColor = effectiveColor ?? "#4285F4";
942
+ const negColor = options.negativeColor ?? "#FF0000";
943
+ // Create array of colors based on value sign
944
+ const colors = options.values.map((v) => typeof v === "number" && v < 0 ? negColor : positiveColor);
945
+ target.marker = { ...(target.marker ?? {}), color: colors };
946
+ return trace;
947
+ }
948
+ if (!effectiveColor) {
949
+ return trace;
950
+ }
951
+ if (chartTypeForStyling === "pie") {
952
+ // Don't clobber the distinct per-slice palette the trace was built
953
+ // with (accent1..accent6) with a single repeated color. Only repeat
954
+ // a uniform color when the user explicitly authored a series color;
955
+ // otherwise keep the multi-color palette so each slice stays distinct.
956
+ const existingColors = target.marker?.colors;
957
+ if (!seriesColor &&
958
+ Array.isArray(existingColors) &&
959
+ existingColors.length > 1) {
960
+ return trace;
961
+ }
962
+ const labelCount = Array.isArray(target.labels)
963
+ ? target.labels.length
964
+ : Array.isArray(target.y)
965
+ ? target.y.length
966
+ : 0;
967
+ target.marker = {
968
+ ...(target.marker ?? {}),
969
+ colors: labelCount > 0
970
+ ? Array.from({ length: labelCount }, () => effectiveColor)
971
+ : target.marker?.colors,
972
+ color: effectiveColor,
973
+ };
974
+ return trace;
975
+ }
976
+ if (isBarOrColumn) {
977
+ target.marker = { ...(target.marker ?? {}), color: effectiveColor };
978
+ return trace;
979
+ }
980
+ target.marker = { ...(target.marker ?? {}), color: effectiveColor };
981
+ target.line = { ...(target.line ?? {}), color: effectiveColor };
982
+ if (target.fill && target.fill !== "none") {
983
+ // Add transparency to area fills so they don't cover other traces
984
+ target.fillcolor = addAlphaToColor(effectiveColor, 0.5);
985
+ }
986
+ return trace;
987
+ };
988
+ // Track whether any series opts into the secondary value axis; the
989
+ // yaxis2 layout block is added below when true.
990
+ let hasSecondaryAxis = false;
991
+ // Pareto — a histogram whose bins are sorted descending with a
992
+ // cumulative-% line overlaid on a secondary 0-100% axis. Arrives
993
+ // as a plain histogram spec carrying `sourceChartType: "PARETO"`.
994
+ const isPareto = spec.chartType === "histogram" && isParetoSpec(spec);
995
+ // Pie-of-pie / bar-of-pie composite. The pie branch below fills
996
+ // `ofPieExtras` with connector-line shapes and (for bar-of-pie) the
997
+ // hidden secondary axes + stack mode merged into the layout later.
998
+ const isOfPie = spec.chartType === "pie" && isOfPieChartSpec(spec);
999
+ let ofPieHandled = false;
1000
+ let ofPieExtras = null;
1001
+ /**
1002
+ * Attach cartesian-only features to a freshly-built line/bar/area
1003
+ * trace: native error bars, secondary-axis assignment, and trendline
1004
+ * overlay traces (computed by the shared utils/trendline regression).
1005
+ */
1006
+ const decorateCartesianTrace = (trace, series, values, dataLabel) => {
1007
+ const target = trace;
1008
+ // Secondary value axis. Plotly overlays a second y-axis named
1009
+ // "y2" and the trace points at it via `yaxis: "y2"`.
1010
+ if (series.useSecondaryAxis) {
1011
+ target.yaxis = "y2";
1012
+ hasSecondaryAxis = true;
1013
+ }
1014
+ // Error bars — native plotly `error_y` / `error_x`. valueType
1015
+ // "custom" reads per-point magnitudes from the spec's plus/minus
1016
+ // ranges, resolved through the same extractor the series values
1017
+ // use.
1018
+ const magnitudes = computeErrorBars(values, resolveCustomErrorBarValues(series.errorBars, (range) => getSeriesValuesFromRange(range, spec.hiddenDimensionStrategy)));
1019
+ if (magnitudes) {
1020
+ const direction = series.errorBars?.direction ?? "y";
1021
+ const noEndCap = series.errorBars?.noEndCap ?? false;
1022
+ const symmetric = magnitudes.plus.every((p, i) => p === magnitudes.minus[i]);
1023
+ const errorConfig = {
1024
+ type: "data",
1025
+ symmetric,
1026
+ array: magnitudes.plus,
1027
+ ...(symmetric ? {} : { arrayminus: magnitudes.minus }),
1028
+ color: series.color ?? undefined,
1029
+ ...(noEndCap ? { width: 0 } : {}),
1030
+ };
1031
+ if (direction === "x") {
1032
+ target.error_x = errorConfig;
1033
+ }
1034
+ else {
1035
+ target.error_y = errorConfig;
1036
+ }
1037
+ }
1038
+ // Trendlines — one overlay scatter trace per spec, reusing the
1039
+ // shared regression util.
1040
+ const seriesColor = series.color ?? theme.themeColors.accent1;
1041
+ for (const tl of series.trendlines ?? []) {
1042
+ const result = computeTrendline(values, {
1043
+ type: tl.type,
1044
+ order: tl.order,
1045
+ period: tl.period,
1046
+ forecastForward: tl.forecastForward,
1047
+ forecastBackward: tl.forecastBackward,
1048
+ });
1049
+ if (!result || result.points.length < 2)
1050
+ continue;
1051
+ const tlName = tl.name ??
1052
+ `${dataLabel ? dataLabel + " " : ""}${tl.type ?? "linear"} trend`;
1053
+ // Map the fitted x (category index) back onto the category
1054
+ // labels where possible so the line aligns with the data.
1055
+ const xs = result.points.map((p) => p.x >= 0 && p.x < domainData.length ? domainData[p.x] : p.x);
1056
+ const ys = result.points.map((p) => p.y);
1057
+ const overlay = {
1058
+ type: "scatter",
1059
+ mode: "lines",
1060
+ name: tlName,
1061
+ line: {
1062
+ color: tl.color ?? seriesColor,
1063
+ dash: "dash",
1064
+ width: 2,
1065
+ },
1066
+ hoverinfo: "skip",
1067
+ ...(series.useSecondaryAxis ? { yaxis: "y2" } : {}),
1068
+ };
1069
+ if (isHorizontalBarChart) {
1070
+ overlay.x = ys;
1071
+ overlay.y = xs;
1072
+ overlay.orientation = "h";
1073
+ }
1074
+ else {
1075
+ overlay.x = xs;
1076
+ overlay.y = ys;
1077
+ }
1078
+ traces.push(overlay);
1079
+ }
1080
+ };
1081
+ // Create traces for each series
1082
+ let seriesIndex = 0;
1083
+ for (const series of spec.series ?? []) {
1084
+ const currentSeriesIndex = seriesIndex++;
1085
+ const dataLabel = !isNil(series.dataLabel) && isAFormula(series.dataLabel)
1086
+ ? coerceCalculatedLabel(await onRequestCalculate(series.dataLabel, position.sheetId ?? sheetId, position.overlayPosition.anchorCell.rowIndex, position.overlayPosition.anchorCell.columnIndex))
1087
+ : series.dataLabel ?? "";
1088
+ for (const sheetRange of series.sources) {
1089
+ const seriesData = getSeriesData(spec, sheetRange, {
1090
+ aggregateEnabled: shouldAggregate,
1091
+ aggregateType: series.aggregateType,
1092
+ rawDomainValues: rawPrimaryDomainValues,
1093
+ orderedDomainValues: domainData,
1094
+ });
1095
+ // Support per-series chart type for combo charts
1096
+ const effectiveChartType = series.seriesChartType ?? spec.chartType;
1097
+ const seriesPlotlyType = chartTypeToPlotlyType(effectiveChartType);
1098
+ const seriesTraceStyle = getPlotlyTraceStyle(effectiveChartType);
1099
+ if (effectiveChartType === "histogram") {
1100
+ const rawValues = "values" in seriesData
1101
+ ? seriesData.values
1102
+ : seriesData;
1103
+ const binCount = spec.binCount;
1104
+ if (isPareto) {
1105
+ // Plotly's native histogram trace bins in axis order and
1106
+ // can't reorder bins, so Pareto pre-bins with the shared
1107
+ // util, sorts descending, and renders a bar trace plus the
1108
+ // cumulative-% line pointed at the y2 overlay axis.
1109
+ const { labels, counts, cumulativePercents } = computePareto(binHistogram(rawValues, binCount));
1110
+ const barTrace = {
1111
+ type: "bar",
1112
+ x: labels,
1113
+ y: counts,
1114
+ name: dataLabel,
1115
+ };
1116
+ applyTraceColor(barTrace, series.color ?? undefined, {
1117
+ noFill: series.noFill,
1118
+ dataPointColors: series.dataPointColors,
1119
+ seriesIndex: currentSeriesIndex,
1120
+ effectiveChartType,
1121
+ });
1122
+ traces.push(barTrace);
1123
+ traces.push({
1124
+ type: "scatter",
1125
+ mode: "lines+markers",
1126
+ x: labels,
1127
+ y: cumulativePercents,
1128
+ name: "Cumulative %",
1129
+ yaxis: "y2",
1130
+ });
1131
+ hasSecondaryAxis = true;
1132
+ }
1133
+ else {
1134
+ const trace = {
1135
+ type: "histogram",
1136
+ x: rawValues.map((v) => (v ?? null)),
1137
+ name: dataLabel,
1138
+ ...(typeof binCount === "number" && binCount > 0
1139
+ ? { nbinsx: binCount }
1140
+ : {}),
1141
+ };
1142
+ applyTraceColor(trace, series.color ?? undefined, {
1143
+ noFill: series.noFill,
1144
+ dataPointColors: series.dataPointColors,
1145
+ seriesIndex: currentSeriesIndex,
1146
+ effectiveChartType,
1147
+ });
1148
+ traces.push(trace);
1149
+ }
1150
+ }
1151
+ else if (effectiveChartType === "boxplot") {
1152
+ const rawValues = "values" in seriesData
1153
+ ? seriesData.values
1154
+ : seriesData;
1155
+ const quartileMethod = spec.quartileMethod;
1156
+ const showOutliers = spec.showOutliers;
1157
+ const showMeanMarker = spec.showMeanMarker === true;
1158
+ // Plotly's box trace computes quartiles/fences itself from
1159
+ // raw values, so a spec-pinned quartile method (or Excel's
1160
+ // "no outliers → whiskers span min/max" semantics) requires
1161
+ // switching to plotly's precomputed-statistics signature
1162
+ // (q1/median/q3/lowerfence/upperfence arrays). When neither
1163
+ // option is set the raw-values trace is kept unchanged.
1164
+ const usePrecomputed = quartileMethod != null || showOutliers === false;
1165
+ let trace;
1166
+ if (usePrecomputed) {
1167
+ const summary = computeBoxplotSummary(rawValues, {
1168
+ quartileMethod,
1169
+ });
1170
+ if (!summary)
1171
+ continue;
1172
+ const hideOutliers = showOutliers === false;
1173
+ trace = {
1174
+ type: "box",
1175
+ name: dataLabel,
1176
+ // Precomputed boxes need an explicit category so each
1177
+ // series lands on its own slot (raw mode derives it from
1178
+ // the trace name).
1179
+ x: [dataLabel || `trace ${currentSeriesIndex}`],
1180
+ q1: [summary.q1],
1181
+ median: [summary.median],
1182
+ q3: [summary.q3],
1183
+ // Excel: hiding outlier points extends the whiskers to
1184
+ // the full min/max; with outliers shown they stop at the
1185
+ // most extreme points inside the 1.5×IQR fences.
1186
+ lowerfence: [hideOutliers ? summary.min : summary.lowerWhisker],
1187
+ upperfence: [hideOutliers ? summary.max : summary.upperWhisker],
1188
+ ...(hideOutliers
1189
+ ? { boxpoints: false }
1190
+ : {
1191
+ // Sample points ride along so plotly can still draw
1192
+ // the outlier dots against the precomputed fences.
1193
+ y: [rawValues.filter((v) => v != null)],
1194
+ boxpoints: "outliers",
1195
+ }),
1196
+ ...(showMeanMarker
1197
+ ? { mean: [summary.mean], boxmean: true }
1198
+ : {}),
1199
+ };
1200
+ }
1201
+ else {
1202
+ trace = {
1203
+ type: "box",
1204
+ y: rawValues.map((v) => (v ?? null)),
1205
+ name: dataLabel,
1206
+ // Raw mode is used when outliers are visible (today's
1207
+ // default and `showOutliers: true` alike).
1208
+ boxpoints: "outliers",
1209
+ ...(showMeanMarker ? { boxmean: true } : {}),
1210
+ };
1211
+ }
1212
+ applyTraceColor(trace, series.color ?? undefined, {
1213
+ noFill: series.noFill,
1214
+ dataPointColors: series.dataPointColors,
1215
+ seriesIndex: currentSeriesIndex,
1216
+ effectiveChartType,
1217
+ });
1218
+ traces.push(trace);
1219
+ }
1220
+ else if (effectiveChartType === "waterfall") {
1221
+ const rawValues = "values" in seriesData
1222
+ ? seriesData.values
1223
+ : seriesData;
1224
+ // Plotly's native waterfall draws connector lines itself and
1225
+ // renders "total" measures from the zero baseline with its
1226
+ // own distinct fill.
1227
+ const totalIndices = new Set(spec.totalIndices ?? []);
1228
+ const trace = {
1229
+ type: "waterfall",
1230
+ x: domainData,
1231
+ y: rawValues.map((v) => (v ?? null)),
1232
+ measure: rawValues.map((_, i) => totalIndices.has(i) ? "total" : "relative"),
1233
+ name: dataLabel,
1234
+ };
1235
+ applyTraceColor(trace, series.color ?? undefined, {
1236
+ noFill: series.noFill,
1237
+ dataPointColors: series.dataPointColors,
1238
+ seriesIndex: currentSeriesIndex,
1239
+ effectiveChartType,
1240
+ });
1241
+ traces.push(trace);
1242
+ }
1243
+ else if (effectiveChartType === "funnel") {
1244
+ const rawValues = "values" in seriesData
1245
+ ? seriesData.values
1246
+ : seriesData;
1247
+ const trace = {
1248
+ type: "funnel",
1249
+ y: domainData,
1250
+ x: rawValues.map((v) => (v ?? null)),
1251
+ name: dataLabel,
1252
+ };
1253
+ applyTraceColor(trace, series.color ?? undefined, {
1254
+ noFill: series.noFill,
1255
+ dataPointColors: series.dataPointColors,
1256
+ seriesIndex: currentSeriesIndex,
1257
+ effectiveChartType,
1258
+ });
1259
+ traces.push(trace);
1260
+ }
1261
+ else if (effectiveChartType === "pie" && isOfPie) {
1262
+ // Pie-of-pie / bar-of-pie — split the minor slices into a
1263
+ // secondary pie or stacked bar beside the main pie, replace
1264
+ // them with an aggregated "Other" slice, and join the two
1265
+ // plots with connector-line shapes. Only the first pie series
1266
+ // participates (of-pie is a single-series chart in Excel).
1267
+ if (ofPieHandled)
1268
+ continue;
1269
+ ofPieHandled = true;
1270
+ const pieData = seriesData;
1271
+ const { main, secondary, otherTotal, total } = splitOfPie(pieData.values, pieData.labels, {
1272
+ splitType: spec.splitType,
1273
+ splitPos: spec.splitPos,
1274
+ });
1275
+ const ofPieType = resolveOfPieType(spec.ofPieType);
1276
+ const secondScale = clampSecondPieSize(spec.secondPieSize) / 100;
1277
+ const halfAngle = otherSliceHalfAngle(otherTotal, total);
1278
+ const paletteAt = (index) => themeColorPalette[index % themeColorPalette.length];
1279
+ // Layout in paper coordinates (0-1 across the plot area
1280
+ // inside the margins). Main pie on the left, secondary plot
1281
+ // on the right.
1282
+ const MAIN_DOMAIN_X = [0, 0.55];
1283
+ const CY = 0.5;
1284
+ const SECONDARY_CX = 0.78;
1285
+ const BAR_LEFT = 0.68;
1286
+ const BAR_RIGHT = 0.88;
1287
+ traces.push({
1288
+ type: "pie",
1289
+ values: main.map((s) => s.value),
1290
+ labels: main.map((s) => s.label),
1291
+ name: dataLabel,
1292
+ domain: { x: MAIN_DOMAIN_X, y: [0, 1] },
1293
+ sort: false,
1294
+ direction: "clockwise",
1295
+ // Rotate so the "Other" slice's angular midpoint faces the
1296
+ // secondary plot (Excel's layout). Other is the last slice
1297
+ // of a clockwise sweep starting at `rotation`° from 12
1298
+ // o'clock, so its midpoint lands at 3 o'clock when the
1299
+ // sweep starts 90° + halfAngle past the top.
1300
+ rotation: (90 + (halfAngle * 180) / Math.PI) % 360,
1301
+ marker: { colors: main.map((_, i) => paletteAt(i)) },
1302
+ });
1303
+ if (secondary.length > 0) {
1304
+ // Paper coords are relative while the pie is circular in
1305
+ // pixels — convert the main pie's pixel radius into
1306
+ // separate x/y paper fractions to compensate for the
1307
+ // aspect ratio. Falls back to fixed fractions when the
1308
+ // container size isn't known yet.
1309
+ const plotW = typeof width === "number" ? Math.max(1, width - 102) : 0;
1310
+ const plotH = typeof height === "number" ? Math.max(1, height - 102) : 0;
1311
+ const hasGeometry = plotW > 1 && plotH > 1;
1312
+ const r = hasGeometry
1313
+ ? Math.min((MAIN_DOMAIN_X[1] - MAIN_DOMAIN_X[0]) * plotW, plotH) / 2
1314
+ : 0;
1315
+ const r2 = r * secondScale;
1316
+ const clampFrac = (v, min, max) => Math.min(max, Math.max(min, v));
1317
+ const rx2 = clampFrac(hasGeometry ? r2 / plotW : 0.14 * secondScale, 0.03, 0.19);
1318
+ const ry2 = clampFrac(hasGeometry ? r2 / plotH : 0.28 * secondScale, 0.05, 0.45);
1319
+ const barHalf = ry2;
1320
+ if (ofPieType === "pie") {
1321
+ traces.push({
1322
+ type: "pie",
1323
+ values: secondary.map((s) => s.value),
1324
+ labels: secondary.map((s) => s.label),
1325
+ name: dataLabel,
1326
+ domain: {
1327
+ x: [SECONDARY_CX - rx2, SECONDARY_CX + rx2],
1328
+ y: [CY - ry2, CY + ry2],
1329
+ },
1330
+ sort: false,
1331
+ direction: "clockwise",
1332
+ marker: {
1333
+ colors: secondary.map((_, i) => paletteAt(main.length + i)),
1334
+ },
1335
+ });
1336
+ }
1337
+ else {
1338
+ // Bar-of-pie — a single-category stacked bar on hidden
1339
+ // secondary axes; one trace per point so each segment
1340
+ // keeps its own color and legend entry.
1341
+ secondary.forEach((s, i) => {
1342
+ traces.push({
1343
+ type: "bar",
1344
+ x: [""],
1345
+ y: [s.value],
1346
+ name: s.label,
1347
+ xaxis: "x2",
1348
+ yaxis: "y2",
1349
+ marker: { color: paletteAt(main.length + i) },
1350
+ });
1351
+ });
1352
+ }
1353
+ // Connector lines from the Other slice's edges to the top
1354
+ // and bottom of the secondary plot. Skipped (gracefully)
1355
+ // when the container size isn't known — pure paper-coord
1356
+ // geometry can't compensate for the pixel aspect ratio.
1357
+ const shapes = [];
1358
+ if (hasGeometry) {
1359
+ const connectorColor = isDarkMode
1360
+ ? "rgba(255,255,255,0.45)"
1361
+ : "#999999";
1362
+ const mainCx = (MAIN_DOMAIN_X[0] + MAIN_DOMAIN_X[1]) / 2;
1363
+ const px = mainCx + (r * Math.cos(halfAngle)) / plotW;
1364
+ const pyTop = CY + (r * Math.sin(halfAngle)) / plotH;
1365
+ const pyBottom = CY - (r * Math.sin(halfAngle)) / plotH;
1366
+ const tx = ofPieType === "pie" ? SECONDARY_CX : BAR_LEFT;
1367
+ const half = ofPieType === "pie" ? ry2 : barHalf;
1368
+ const line = {
1369
+ type: "line",
1370
+ xref: "paper",
1371
+ yref: "paper",
1372
+ layer: "below",
1373
+ line: { color: connectorColor, width: 1 },
1374
+ };
1375
+ shapes.push({ ...line, x0: px, y0: pyTop, x1: tx, y1: CY + half }, { ...line, x0: px, y0: pyBottom, x1: tx, y1: CY - half });
1376
+ }
1377
+ ofPieExtras = {
1378
+ shapes,
1379
+ ...(ofPieType === "bar"
1380
+ ? {
1381
+ barmode: "stack",
1382
+ xaxis2: {
1383
+ domain: [BAR_LEFT, BAR_RIGHT],
1384
+ anchor: "y2",
1385
+ visible: false,
1386
+ fixedrange: true,
1387
+ },
1388
+ yaxis2: {
1389
+ domain: [CY - barHalf, CY + barHalf],
1390
+ anchor: "x2",
1391
+ visible: false,
1392
+ fixedrange: true,
1393
+ },
1394
+ }
1395
+ : {}),
1396
+ };
1397
+ }
1398
+ }
1399
+ else if (effectiveChartType === "pie") {
1400
+ const pieData = seriesData;
1401
+ const trace = {
1402
+ type: seriesPlotlyType,
1403
+ values: pieData.values,
1404
+ labels: pieData.labels,
1405
+ name: dataLabel,
1406
+ marker: {
1407
+ colors: [
1408
+ theme.themeColors.accent1,
1409
+ theme.themeColors.accent2,
1410
+ theme.themeColors.accent3,
1411
+ theme.themeColors.accent4,
1412
+ theme.themeColors.accent5,
1413
+ theme.themeColors.accent6,
1414
+ ],
1415
+ },
1416
+ };
1417
+ applyTraceColor(trace, series.color ?? undefined, {
1418
+ noFill: series.noFill,
1419
+ dataPointColors: series.dataPointColors,
1420
+ seriesIndex: currentSeriesIndex,
1421
+ effectiveChartType,
1422
+ });
1423
+ traces.push(trace);
1424
+ }
1425
+ else if ("values" in seriesData && "labels" in seriesData) {
1426
+ const { x, y, orientation } = assignCartesianData(domainData, seriesData.values);
1427
+ const trace = {
1428
+ type: seriesPlotlyType,
1429
+ x,
1430
+ y,
1431
+ orientation,
1432
+ name: dataLabel,
1433
+ stackgroup: isStacked ? "one" : undefined,
1434
+ ...seriesTraceStyle,
1435
+ };
1436
+ applyTraceColor(trace, series.color ?? undefined, {
1437
+ noFill: series.noFill,
1438
+ dataPointColors: series.dataPointColors,
1439
+ invertIfNegative: series.invertIfNegative,
1440
+ negativeColor: series.negativeColor,
1441
+ values: seriesData.values,
1442
+ seriesIndex: currentSeriesIndex,
1443
+ effectiveChartType,
1444
+ gradientColor: gradientToPlotly(series.gradient),
1445
+ });
1446
+ decorateCartesianTrace(trace, series, seriesData.values, dataLabel);
1447
+ traces.push(trace);
1448
+ }
1449
+ else {
1450
+ const numericValues = seriesData;
1451
+ const { x, y, orientation } = assignCartesianData(domainData, numericValues);
1452
+ const trace = {
1453
+ type: seriesPlotlyType,
1454
+ x,
1455
+ y,
1456
+ orientation,
1457
+ name: dataLabel,
1458
+ stackgroup: isStacked ? "one" : undefined,
1459
+ ...seriesTraceStyle,
1460
+ };
1461
+ applyTraceColor(trace, series.color ?? undefined, {
1462
+ noFill: series.noFill,
1463
+ dataPointColors: series.dataPointColors,
1464
+ invertIfNegative: series.invertIfNegative,
1465
+ negativeColor: series.negativeColor,
1466
+ values: numericValues,
1467
+ seriesIndex: currentSeriesIndex,
1468
+ effectiveChartType,
1469
+ gradientColor: gradientToPlotly(series.gradient),
1470
+ });
1471
+ decorateCartesianTrace(trace, series, numericValues, dataLabel);
1472
+ traces.push(trace);
1473
+ }
1474
+ }
1475
+ }
1476
+ // Authored title formatting (parsed from <c:title><c:txPr> or
1477
+ // <c:title><c:tx><c:rich>) overrides the theme-derived default.
1478
+ const authoredTitleFmt = spec.titleTextFormat;
1479
+ const newLayout = {
1480
+ title: {
1481
+ text: title ?? undefined,
1482
+ font: applyTextFormatToFont({
1483
+ family: "Untitled Sans, apple-system, sans-serif",
1484
+ size: 16,
1485
+ color: textColor,
1486
+ }, authoredTitleFmt),
1487
+ x: 0.02, // Small left margin (0.02 = 2% from left edge)
1488
+ xanchor: "left",
1489
+ },
1490
+ font: {
1491
+ family: "Untitled Sans, apple-system, sans-serif",
1492
+ size: 12,
1493
+ color: textColor,
1494
+ },
1495
+ paper_bgcolor: bgColor,
1496
+ plot_bgcolor: bgColor,
1497
+ xaxis: applyAxisOverridesPlotly({
1498
+ title: {
1499
+ text: spec.horizontalAxisTitle ?? "",
1500
+ font: { color: textColor },
1501
+ },
1502
+ automargin: true,
1503
+ showgrid: true,
1504
+ gridcolor: gridColor,
1505
+ visible: showXAxis(spec?.chartType),
1506
+ type: isHorizontalBarChart ? "linear" : undefined,
1507
+ },
1508
+ // For horizontal bar, OOXML's value axis is the X axis.
1509
+ isHorizontalBarChart
1510
+ ? spec.valueAxisOptions
1511
+ : spec.categoryAxisOptions),
1512
+ yaxis: applyAxisOverridesPlotly({
1513
+ title: {
1514
+ text: spec.verticalAxisTitle ?? "",
1515
+ font: { color: textColor },
1516
+ },
1517
+ showgrid: !isHorizontalBarChart,
1518
+ gridcolor: gridColor,
1519
+ automargin: true,
1520
+ type: isHorizontalBarChart ? "category" : undefined,
1521
+ }, isHorizontalBarChart
1522
+ ? spec.categoryAxisOptions
1523
+ : spec.valueAxisOptions),
1524
+ // Secondary value axis — a second y-axis overlaying the first on
1525
+ // the right. Series with `useSecondaryAxis` were pointed at "y2".
1526
+ ...(hasSecondaryAxis
1527
+ ? {
1528
+ yaxis2: applyAxisOverridesPlotly({
1529
+ title: { font: { color: textColor } },
1530
+ overlaying: "y",
1531
+ side: "right",
1532
+ showgrid: false,
1533
+ automargin: true,
1534
+ // Pareto — the cumulative-% axis is clamped 0-100%.
1535
+ ...(isPareto
1536
+ ? { range: [0, 100], ticksuffix: "%" }
1537
+ : {}),
1538
+ }, spec.secondaryValueAxisOptions),
1539
+ }
1540
+ : {}),
1541
+ ...(() => {
1542
+ const { legend: legendCfg, showlegend } = applyLegendOverridesPlotly({ font: { color: textColor } }, spec.legend);
1543
+ return {
1544
+ showlegend: showlegend === undefined ? true : showlegend,
1545
+ legend: legendCfg,
1546
+ };
1547
+ })(),
1548
+ margin: {
1549
+ l: 50,
1550
+ r: 50,
1551
+ t: 50,
1552
+ b: 50,
1553
+ },
1554
+ autosize: true,
1555
+ // Bar/column stacking. Plotly's `barmode: "stack"` stacks bars
1556
+ // at the same X (or Y for horizontal). `barnorm: "percent"`
1557
+ // normalises each category's stack to 100% — Excel's
1558
+ // `percentStacked` grouping. Default is grouped (clustered).
1559
+ ...((spec.chartType === "bar" || spec.chartType === "column") && spec.stackedType
1560
+ ? {
1561
+ barmode: spec.stackedType === "STACKED" ||
1562
+ spec.stackedType === "PERCENT_STACKED"
1563
+ ? "stack"
1564
+ : "group",
1565
+ ...(spec.stackedType === "PERCENT_STACKED"
1566
+ ? { barnorm: "percent" }
1567
+ : {}),
1568
+ }
1569
+ : {}),
1570
+ // gapWidth — Excel stores 0..500 as a percentage of bar width.
1571
+ // Plotly's `bargap` is a fraction of the slot (0..1). Approx:
1572
+ // gapWidth=150 (default) → ~0.4 bargap.
1573
+ ...(typeof spec.gapWidth === "number"
1574
+ ? { bargap: Math.max(0, Math.min(0.95, spec.gapWidth / 400)) }
1575
+ : {}),
1576
+ // Of-pie composite — connector-line shapes plus (bar-of-pie) the
1577
+ // hidden secondary axes and stacked bar mode.
1578
+ ...(ofPieExtras
1579
+ ? {
1580
+ shapes: ofPieExtras.shapes,
1581
+ ...(ofPieExtras.barmode ? { barmode: ofPieExtras.barmode } : {}),
1582
+ ...(ofPieExtras.xaxis2 ? { xaxis2: ofPieExtras.xaxis2 } : {}),
1583
+ ...(ofPieExtras.yaxis2 ? { yaxis2: ofPieExtras.yaxis2 } : {}),
1584
+ }
1585
+ : {}),
1586
+ };
1587
+ // Phase 5.1: apply data labels per trace, and set the doughnut hole
1588
+ // when chartType === "doughnut".
1589
+ const dlOpts = spec.dataLabels;
1590
+ const doughnutHole = spec.doughnutHoleSize;
1591
+ const formattedTraces = traces.map((trace) => {
1592
+ let next = applyDataLabelsToTracePlotly(trace, dlOpts, spec.chartType);
1593
+ // Doughnut is a pie trace with `hole` set.
1594
+ if (spec.chartType === "doughnut" && next.type === "pie") {
1595
+ next = {
1596
+ ...next,
1597
+ hole: typeof doughnutHole === "number" ? doughnutHole : 0.5,
1598
+ };
1599
+ }
1600
+ return next;
1601
+ });
1602
+ // Sort traces so area charts (with fill) render first (behind other traces)
1603
+ const sortedTraces = [...formattedTraces].sort((a, b) => {
1604
+ const aHasFill = a.fill && a.fill !== "none";
1605
+ const bHasFill = b.fill && b.fill !== "none";
1606
+ if (aHasFill && !bHasFill)
1607
+ return -1;
1608
+ if (!aHasFill && bHasFill)
1609
+ return 1;
1610
+ return 0;
1611
+ });
1612
+ setPlotData(sortedTraces);
1613
+ setLayout(newLayout);
1614
+ };
1615
+ // Subscribe to datarange
1616
+ useSubscribeToSheet(chart.chartId, chart.spec.dataRange, updateChart);
1617
+ // Update chart on spec changes
1618
+ useIsomorphicLayoutEffect(() => {
1619
+ updateChart();
1620
+ }, [chart.spec, getDomainValuesFromRange, getSeriesValuesFromRange]);
1621
+ // Update theme
1622
+ useIsomorphicLayoutEffect(() => {
1623
+ updateChart();
1624
+ }, [theme, title, isDarkMode]);
1625
+ // Resize chart on width/height changes
1626
+ useIsomorphicLayoutEffect(() => {
1627
+ setLayout((prev) => ({
1628
+ ...prev,
1629
+ width: width - 2,
1630
+ height: height - 2,
1631
+ }));
1632
+ // Of-pie composites derive their connector-line shapes and
1633
+ // secondary-plot domains from the container's pixel aspect ratio —
1634
+ // rebuild them for the new size.
1635
+ if (spec.chartType === "pie" && isOfPieChartSpec(spec)) {
1636
+ updateChart();
1637
+ }
1638
+ }, [width, height]);
1639
+ // Chrome bug - redraw chart when visibility changes
1640
+ useOnVisibilityChange((state) => {
1641
+ if (state === "visible") {
1642
+ updateChart();
1643
+ }
1644
+ });
1645
+ const config = {
1646
+ responsive: true,
1647
+ // Static so a body drag falls through to the EmbedDraggable move handler
1648
+ // (Excel/Sheets behavior) instead of triggering Plotly's zoom-box.
1649
+ // staticPlot also drops the modebar and the `.plot-container .draglayer`
1650
+ // element the move's cancel selector keys off of.
1651
+ displayModeBar: false,
1652
+ displaylogo: false,
1653
+ staticPlot: true,
1654
+ };
1655
+ // Excel's "Rounded Corners" chart frame (`<c:roundedCorners val="1"/>`
1656
+ // at the chartSpace root). Default per OOXML §21.2.2.183 is square,
1657
+ // so only opt in when the workbook explicitly set the flag.
1658
+ const roundedCornersStyle = spec.roundedCorners
1659
+ ? { borderRadius: 8, overflow: "hidden" }
1660
+ : undefined;
1661
+ return (React.createElement("div", { className: "flex flex-1", style: roundedCornersStyle },
1662
+ isSelected ? (React.createElement(DropdownMenu, null,
1663
+ React.createElement(DropdownMenuTrigger, { asChild: true },
1664
+ React.createElement(IconButton, { className: "absolute right-1 top-1 z-10" },
1665
+ React.createElement(DotsVerticalIcon, null))),
1666
+ React.createElement(DropdownMenuPortal, null,
1667
+ React.createElement(DropdownMenuContent, { align: "start", onCloseAutoFocus: (event) => {
1668
+ event.preventDefault();
1669
+ if (shouldFocusSheetRef.current) {
1670
+ focusSheet?.();
1671
+ }
1672
+ }, onInteractOutside: () => {
1673
+ shouldFocusSheetRef.current = false;
1674
+ } },
1675
+ readonly ? null : (React.createElement(DropdownMenuItem, { onClick: () => onRequestEdit?.(chart) }, "Edit chart")),
1676
+ readonly ? null : (React.createElement(DropdownMenuItem, { onClick: () => {
1677
+ shouldFocusSheetRef.current = true;
1678
+ onDeleteChart?.(chart.chartId);
1679
+ } }, "Delete chart")))))) : null,
1680
+ React.createElement("div", { className: "flex-1 rnc-plotly-chart-container" },
1681
+ React.createElement(Plot, { ref: plotRef, data: plotData, layout: layout, config: config, style: { width: "100%", height: "100%" } }))));
1682
+ };