@keyfunc/keybricks-ds 1.0.0

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 (418) hide show
  1. package/DEPLOY.md +204 -0
  2. package/DESIGN.md +269 -0
  3. package/INTEGRATION.md +417 -0
  4. package/SKILL.md +32 -0
  5. package/_adherence.oxlintrc.json +1469 -0
  6. package/_scripts/build-index.mjs +80 -0
  7. package/_scripts/build-tokens.mjs +135 -0
  8. package/assets/icons/icon-index.js +1 -0
  9. package/assets/icons/icon-index.json +1 -0
  10. package/assets/icons/unicons-index.js +1 -0
  11. package/assets/icons/unicons-index.json +1 -0
  12. package/assets/keybricks-logo.png +0 -0
  13. package/components/_cards/accordion.card.html +32 -0
  14. package/components/_cards/action-chip.card.html +22 -0
  15. package/components/_cards/activity.card.html +34 -0
  16. package/components/_cards/alert.card.html +32 -0
  17. package/components/_cards/app-chrome.card.html +36 -0
  18. package/components/_cards/banner.card.html +21 -0
  19. package/components/_cards/brand-visual.card.html +43 -0
  20. package/components/_cards/breadcrumb.card.html +23 -0
  21. package/components/_cards/button-1-base.card.html +39 -0
  22. package/components/_cards/button-2-states.card.html +56 -0
  23. package/components/_cards/button-3-sizes-icons.card.html +32 -0
  24. package/components/_cards/button-4-group.card.html +47 -0
  25. package/components/_cards/canvas-zoom-controls.card.html +19 -0
  26. package/components/_cards/cards-indicators.card.html +59 -0
  27. package/components/_cards/charts-1-sparkline.card.html +55 -0
  28. package/components/_cards/charts-10-gauge.card.html +31 -0
  29. package/components/_cards/charts-11-funnel.card.html +28 -0
  30. package/components/_cards/charts-12-treemap.card.html +23 -0
  31. package/components/_cards/charts-14-radar.card.html +31 -0
  32. package/components/_cards/charts-15-distribution.card.html +32 -0
  33. package/components/_cards/charts-16-pareto.card.html +25 -0
  34. package/components/_cards/charts-17-candlestick.card.html +34 -0
  35. package/components/_cards/charts-18-sankey.card.html +29 -0
  36. package/components/_cards/charts-19-chord.card.html +31 -0
  37. package/components/_cards/charts-2-line.card.html +46 -0
  38. package/components/_cards/charts-20-sunburst.card.html +38 -0
  39. package/components/_cards/charts-21-worldmap.card.html +33 -0
  40. package/components/_cards/charts-22-gantt.card.html +37 -0
  41. package/components/_cards/charts-23-bar3d.card.html +26 -0
  42. package/components/_cards/charts-24-pie3d.card.html +32 -0
  43. package/components/_cards/charts-25-surface3d.card.html +24 -0
  44. package/components/_cards/charts-26-scatter3d.card.html +24 -0
  45. package/components/_cards/charts-27-ribbon3d.card.html +23 -0
  46. package/components/_cards/charts-3-bar.card.html +71 -0
  47. package/components/_cards/charts-4-barlist.card.html +30 -0
  48. package/components/_cards/charts-5-donut.card.html +35 -0
  49. package/components/_cards/charts-6-scatter.card.html +26 -0
  50. package/components/_cards/charts-7-heatmap.card.html +28 -0
  51. package/components/_cards/charts-8-waterfall.card.html +25 -0
  52. package/components/_cards/charts-9-bullet.card.html +31 -0
  53. package/components/_cards/charts-99-fullpage.card.html +194 -0
  54. package/components/_cards/checkbox.card.html +34 -0
  55. package/components/_cards/codeblock.card.html +19 -0
  56. package/components/_cards/combobox.card.html +25 -0
  57. package/components/_cards/context-menu.card.html +21 -0
  58. package/components/_cards/datatable.card.html +48 -0
  59. package/components/_cards/date-picker.card.html +31 -0
  60. package/components/_cards/diffvalue.card.html +21 -0
  61. package/components/_cards/divider.card.html +24 -0
  62. package/components/_cards/drawer.card.html +30 -0
  63. package/components/_cards/dropdown-menu.card.html +38 -0
  64. package/components/_cards/dropzone.card.html +22 -0
  65. package/components/_cards/empty-state.card.html +29 -0
  66. package/components/_cards/filter-bar.card.html +34 -0
  67. package/components/_cards/icon-button.card.html +27 -0
  68. package/components/_cards/icons-1-core.card.html +49 -0
  69. package/components/_cards/icons-2-feather.card.html +39 -0
  70. package/components/_cards/icons-3-unicons.card.html +44 -0
  71. package/components/_cards/input.card.html +25 -0
  72. package/components/_cards/kbd.card.html +20 -0
  73. package/components/_cards/keyvalue.card.html +23 -0
  74. package/components/_cards/listrow.card.html +30 -0
  75. package/components/_cards/loading.card.html +33 -0
  76. package/components/_cards/log-viewer.card.html +23 -0
  77. package/components/_cards/modal.card.html +35 -0
  78. package/components/_cards/multi-select.card.html +26 -0
  79. package/components/_cards/number-stepper.card.html +24 -0
  80. package/components/_cards/pagination.card.html +25 -0
  81. package/components/_cards/people.card.html +46 -0
  82. package/components/_cards/pipeline-node.card.html +76 -0
  83. package/components/_cards/pipeline-palette.card.html +54 -0
  84. package/components/_cards/search.card.html +23 -0
  85. package/components/_cards/select.card.html +23 -0
  86. package/components/_cards/slider.card.html +25 -0
  87. package/components/_cards/smart-table.card.html +37 -0
  88. package/components/_cards/spreadsheet.card.html +31 -0
  89. package/components/_cards/stepper.card.html +25 -0
  90. package/components/_cards/tabs.card.html +25 -0
  91. package/components/_cards/tag.card.html +26 -0
  92. package/components/_cards/textarea.card.html +27 -0
  93. package/components/_cards/timeline.card.html +32 -0
  94. package/components/_cards/toast.card.html +33 -0
  95. package/components/_cards/toggles.card.html +68 -0
  96. package/components/_cards/tooltip.card.html +22 -0
  97. package/components/_cards/tree.card.html +150 -0
  98. package/components/_shared/frame-theme.js +16 -0
  99. package/components/_shared/src-brand.js +1 -0
  100. package/components/_shared/src-buttons.js +1 -0
  101. package/components/_shared/src-charts.js +1 -0
  102. package/components/_shared/src-data.js +1 -0
  103. package/components/_shared/src-display.js +1 -0
  104. package/components/_shared/src-feedback.js +1 -0
  105. package/components/_shared/src-forms.js +1 -0
  106. package/components/_shared/src-icons.js +1 -0
  107. package/components/_shared/src-navigation.js +1 -0
  108. package/components/_shared/src-pipeline.js +1 -0
  109. package/components/_shared/src-tabs.js +1 -0
  110. package/components/_shared/svg-download.js +19 -0
  111. package/components/_shared/view-source.js +185 -0
  112. package/components/_vue/index.mjs +114 -0
  113. package/components/brand/BrandVisual.d.ts +17 -0
  114. package/components/brand/BrandVisual.jsx +29 -0
  115. package/components/brand/BrandVisual.vue +34 -0
  116. package/components/brand/_dir.card.html +13 -0
  117. package/components/brand/brandMotifs.js +210 -0
  118. package/components/buttons/ActionChip.d.ts +10 -0
  119. package/components/buttons/ActionChip.jsx +20 -0
  120. package/components/buttons/ActionChip.vue +18 -0
  121. package/components/buttons/Button.d.ts +40 -0
  122. package/components/buttons/Button.jsx +101 -0
  123. package/components/buttons/Button.vue +87 -0
  124. package/components/buttons/IconButton.d.ts +26 -0
  125. package/components/buttons/IconButton.jsx +28 -0
  126. package/components/buttons/IconButton.vue +21 -0
  127. package/components/buttons/_dir.card.html +13 -0
  128. package/components/buttons/buttons.prompt.md +19 -0
  129. package/components/charts/Bar3D.d.ts +15 -0
  130. package/components/charts/Bar3D.jsx +93 -0
  131. package/components/charts/Bar3D.vue +79 -0
  132. package/components/charts/BarChart.d.ts +23 -0
  133. package/components/charts/BarChart.jsx +135 -0
  134. package/components/charts/BarChart.vue +139 -0
  135. package/components/charts/BarList.d.ts +13 -0
  136. package/components/charts/BarList.jsx +31 -0
  137. package/components/charts/BarList.vue +25 -0
  138. package/components/charts/BoxPlot.d.ts +18 -0
  139. package/components/charts/BoxPlot.jsx +62 -0
  140. package/components/charts/BoxPlot.vue +58 -0
  141. package/components/charts/BulletChart.d.ts +12 -0
  142. package/components/charts/BulletChart.jsx +41 -0
  143. package/components/charts/BulletChart.vue +33 -0
  144. package/components/charts/CandlestickChart.d.ts +15 -0
  145. package/components/charts/CandlestickChart.jsx +63 -0
  146. package/components/charts/CandlestickChart.vue +62 -0
  147. package/components/charts/ChartKit.d.ts +8 -0
  148. package/components/charts/ChartKit.jsx +76 -0
  149. package/components/charts/ChartKit.vue +47 -0
  150. package/components/charts/ChordChart.d.ts +13 -0
  151. package/components/charts/ChordChart.jsx +55 -0
  152. package/components/charts/ChordChart.vue +54 -0
  153. package/components/charts/DonutChart.d.ts +15 -0
  154. package/components/charts/DonutChart.jsx +73 -0
  155. package/components/charts/DonutChart.vue +61 -0
  156. package/components/charts/FunnelChart.d.ts +11 -0
  157. package/components/charts/FunnelChart.jsx +37 -0
  158. package/components/charts/FunnelChart.vue +29 -0
  159. package/components/charts/GanttChart.d.ts +20 -0
  160. package/components/charts/GanttChart.jsx +80 -0
  161. package/components/charts/GanttChart.vue +60 -0
  162. package/components/charts/GaugeChart.d.ts +13 -0
  163. package/components/charts/GaugeChart.jsx +33 -0
  164. package/components/charts/GaugeChart.vue +29 -0
  165. package/components/charts/HeatmapChart.d.ts +13 -0
  166. package/components/charts/HeatmapChart.jsx +62 -0
  167. package/components/charts/HeatmapChart.vue +38 -0
  168. package/components/charts/Histogram.d.ts +16 -0
  169. package/components/charts/Histogram.jsx +56 -0
  170. package/components/charts/Histogram.vue +53 -0
  171. package/components/charts/LineChart.d.ts +17 -0
  172. package/components/charts/LineChart.jsx +84 -0
  173. package/components/charts/LineChart.vue +86 -0
  174. package/components/charts/ParetoChart.d.ts +13 -0
  175. package/components/charts/ParetoChart.jsx +54 -0
  176. package/components/charts/ParetoChart.vue +55 -0
  177. package/components/charts/Pie3D.d.ts +17 -0
  178. package/components/charts/Pie3D.jsx +145 -0
  179. package/components/charts/Pie3D.vue +115 -0
  180. package/components/charts/RadarChart.d.ts +14 -0
  181. package/components/charts/RadarChart.jsx +79 -0
  182. package/components/charts/RadarChart.vue +73 -0
  183. package/components/charts/Ribbon3D.d.ts +13 -0
  184. package/components/charts/Ribbon3D.jsx +89 -0
  185. package/components/charts/Ribbon3D.vue +79 -0
  186. package/components/charts/SankeyChart.d.ts +15 -0
  187. package/components/charts/SankeyChart.jsx +59 -0
  188. package/components/charts/SankeyChart.vue +53 -0
  189. package/components/charts/Scatter3D.d.ts +12 -0
  190. package/components/charts/Scatter3D.jsx +69 -0
  191. package/components/charts/Scatter3D.vue +59 -0
  192. package/components/charts/ScatterChart.d.ts +12 -0
  193. package/components/charts/ScatterChart.jsx +53 -0
  194. package/components/charts/ScatterChart.vue +57 -0
  195. package/components/charts/Sparkline.d.ts +19 -0
  196. package/components/charts/Sparkline.jsx +63 -0
  197. package/components/charts/Sparkline.vue +54 -0
  198. package/components/charts/SunburstChart.d.ts +12 -0
  199. package/components/charts/SunburstChart.jsx +61 -0
  200. package/components/charts/SunburstChart.vue +60 -0
  201. package/components/charts/Surface3D.d.ts +13 -0
  202. package/components/charts/Surface3D.jsx +70 -0
  203. package/components/charts/Surface3D.vue +63 -0
  204. package/components/charts/TreemapChart.d.ts +11 -0
  205. package/components/charts/TreemapChart.jsx +35 -0
  206. package/components/charts/TreemapChart.vue +27 -0
  207. package/components/charts/WaterfallChart.d.ts +11 -0
  208. package/components/charts/WaterfallChart.jsx +59 -0
  209. package/components/charts/WaterfallChart.vue +46 -0
  210. package/components/charts/WorldMap.d.ts +21 -0
  211. package/components/charts/WorldMap.jsx +107 -0
  212. package/components/charts/WorldMap.vue +73 -0
  213. package/components/charts/_dir.card.html +7 -0
  214. package/components/charts/chart-3d.js +84 -0
  215. package/components/charts/chart-geometry.js +223 -0
  216. package/components/charts/charts.prompt.md +24 -0
  217. package/components/data/CodeBlock.d.ts +9 -0
  218. package/components/data/CodeBlock.jsx +19 -0
  219. package/components/data/CodeBlock.vue +22 -0
  220. package/components/data/DataTable.d.ts +28 -0
  221. package/components/data/DataTable.jsx +32 -0
  222. package/components/data/DataTable.vue +25 -0
  223. package/components/data/DiffValue.d.ts +11 -0
  224. package/components/data/DiffValue.jsx +15 -0
  225. package/components/data/DiffValue.vue +13 -0
  226. package/components/data/FilterBar.d.ts +16 -0
  227. package/components/data/FilterBar.jsx +22 -0
  228. package/components/data/FilterBar.vue +23 -0
  229. package/components/data/KeyValue.d.ts +16 -0
  230. package/components/data/KeyValue.jsx +25 -0
  231. package/components/data/KeyValue.vue +26 -0
  232. package/components/data/ListRow.d.ts +14 -0
  233. package/components/data/ListRow.jsx +25 -0
  234. package/components/data/ListRow.vue +25 -0
  235. package/components/data/Pagination.d.ts +12 -0
  236. package/components/data/Pagination.jsx +27 -0
  237. package/components/data/Pagination.vue +24 -0
  238. package/components/data/SmartTable.d.ts +30 -0
  239. package/components/data/SmartTable.jsx +73 -0
  240. package/components/data/SmartTable.vue +74 -0
  241. package/components/data/Spreadsheet.d.ts +33 -0
  242. package/components/data/Spreadsheet.jsx +110 -0
  243. package/components/data/Spreadsheet.vue +95 -0
  244. package/components/data/_dir.card.html +13 -0
  245. package/components/data/data.prompt.md +33 -0
  246. package/components/display/Avatar.d.ts +21 -0
  247. package/components/display/Avatar.jsx +31 -0
  248. package/components/display/Avatar.vue +34 -0
  249. package/components/display/Badge.d.ts +27 -0
  250. package/components/display/Badge.jsx +40 -0
  251. package/components/display/Badge.vue +47 -0
  252. package/components/display/Card.d.ts +27 -0
  253. package/components/display/Card.jsx +27 -0
  254. package/components/display/Card.vue +28 -0
  255. package/components/display/Divider.d.ts +8 -0
  256. package/components/display/Divider.jsx +14 -0
  257. package/components/display/Divider.vue +13 -0
  258. package/components/display/Kbd.d.ts +7 -0
  259. package/components/display/Kbd.jsx +12 -0
  260. package/components/display/Kbd.vue +9 -0
  261. package/components/display/ProgressBar.d.ts +42 -0
  262. package/components/display/ProgressBar.jsx +70 -0
  263. package/components/display/ProgressBar.vue +69 -0
  264. package/components/display/Tag.d.ts +9 -0
  265. package/components/display/Tag.jsx +15 -0
  266. package/components/display/Tag.vue +15 -0
  267. package/components/display/_dir.card.html +13 -0
  268. package/components/display/display.prompt.md +30 -0
  269. package/components/feedback/Accordion.d.ts +13 -0
  270. package/components/feedback/Accordion.jsx +21 -0
  271. package/components/feedback/Accordion.vue +22 -0
  272. package/components/feedback/ActivityItem.d.ts +25 -0
  273. package/components/feedback/ActivityItem.jsx +39 -0
  274. package/components/feedback/ActivityItem.vue +46 -0
  275. package/components/feedback/Alert.d.ts +36 -0
  276. package/components/feedback/Alert.jsx +48 -0
  277. package/components/feedback/Alert.vue +59 -0
  278. package/components/feedback/Banner.d.ts +15 -0
  279. package/components/feedback/Banner.jsx +25 -0
  280. package/components/feedback/Banner.vue +25 -0
  281. package/components/feedback/ContextMenu.d.ts +8 -0
  282. package/components/feedback/ContextMenu.jsx +18 -0
  283. package/components/feedback/ContextMenu.vue +19 -0
  284. package/components/feedback/Drawer.d.ts +18 -0
  285. package/components/feedback/Drawer.jsx +26 -0
  286. package/components/feedback/Drawer.vue +24 -0
  287. package/components/feedback/Menu.d.ts +10 -0
  288. package/components/feedback/Menu.jsx +24 -0
  289. package/components/feedback/Menu.vue +21 -0
  290. package/components/feedback/Modal.d.ts +38 -0
  291. package/components/feedback/Modal.jsx +69 -0
  292. package/components/feedback/Modal.vue +65 -0
  293. package/components/feedback/Stepper.d.ts +9 -0
  294. package/components/feedback/Stepper.jsx +24 -0
  295. package/components/feedback/Stepper.vue +18 -0
  296. package/components/feedback/Timeline.d.ts +15 -0
  297. package/components/feedback/Timeline.jsx +27 -0
  298. package/components/feedback/Timeline.vue +21 -0
  299. package/components/feedback/_dir.card.html +13 -0
  300. package/components/feedback/feedback.prompt.md +35 -0
  301. package/components/forms/Checkbox.d.ts +13 -0
  302. package/components/forms/Checkbox.jsx +17 -0
  303. package/components/forms/Checkbox.vue +18 -0
  304. package/components/forms/Combobox.d.ts +13 -0
  305. package/components/forms/Combobox.jsx +34 -0
  306. package/components/forms/Combobox.vue +35 -0
  307. package/components/forms/DatePicker.d.ts +12 -0
  308. package/components/forms/DatePicker.jsx +48 -0
  309. package/components/forms/DatePicker.vue +46 -0
  310. package/components/forms/Dropzone.d.ts +20 -0
  311. package/components/forms/Dropzone.jsx +36 -0
  312. package/components/forms/Dropzone.vue +28 -0
  313. package/components/forms/Field.d.ts +13 -0
  314. package/components/forms/Field.jsx +13 -0
  315. package/components/forms/Field.vue +12 -0
  316. package/components/forms/Input.d.ts +18 -0
  317. package/components/forms/Input.jsx +35 -0
  318. package/components/forms/Input.vue +38 -0
  319. package/components/forms/MultiSelect.d.ts +14 -0
  320. package/components/forms/MultiSelect.jsx +33 -0
  321. package/components/forms/MultiSelect.vue +36 -0
  322. package/components/forms/NumberStepper.d.ts +13 -0
  323. package/components/forms/NumberStepper.jsx +15 -0
  324. package/components/forms/NumberStepper.vue +17 -0
  325. package/components/forms/Radio.vue +16 -0
  326. package/components/forms/RadioGroup.vue +11 -0
  327. package/components/forms/SearchInput.d.ts +12 -0
  328. package/components/forms/SearchInput.jsx +17 -0
  329. package/components/forms/SearchInput.vue +14 -0
  330. package/components/forms/Select.d.ts +14 -0
  331. package/components/forms/Select.jsx +17 -0
  332. package/components/forms/Select.vue +16 -0
  333. package/components/forms/Slider.d.ts +15 -0
  334. package/components/forms/Slider.jsx +21 -0
  335. package/components/forms/Slider.vue +23 -0
  336. package/components/forms/Switch.d.ts +31 -0
  337. package/components/forms/Switch.jsx +40 -0
  338. package/components/forms/Switch.vue +16 -0
  339. package/components/forms/_dir.card.html +13 -0
  340. package/components/forms/forms.prompt.md +23 -0
  341. package/components/icons/Icon.d.ts +19 -0
  342. package/components/icons/Icon.jsx +93 -0
  343. package/components/icons/Icon.prompt.md +8 -0
  344. package/components/icons/Icon.vue +12 -0
  345. package/components/icons/Unicon.d.ts +12 -0
  346. package/components/icons/Unicon.jsx +16 -0
  347. package/components/icons/Unicon.vue +15 -0
  348. package/components/icons/_dir.card.html +13 -0
  349. package/components/icons/feather-paths.js +2 -0
  350. package/components/icons/icon-paths.js +73 -0
  351. package/components/index.d.ts +87 -0
  352. package/components/index.js +110 -0
  353. package/components/navigation/Breadcrumb.d.ts +24 -0
  354. package/components/navigation/Breadcrumb.jsx +120 -0
  355. package/components/navigation/Breadcrumb.vue +94 -0
  356. package/components/navigation/DocTree.d.ts +18 -0
  357. package/components/navigation/DocTree.jsx +37 -0
  358. package/components/navigation/DocTree.vue +25 -0
  359. package/components/navigation/Sidebar.d.ts +29 -0
  360. package/components/navigation/Sidebar.jsx +75 -0
  361. package/components/navigation/Sidebar.vue +54 -0
  362. package/components/navigation/TopBar.d.ts +27 -0
  363. package/components/navigation/TopBar.jsx +44 -0
  364. package/components/navigation/TopBar.vue +51 -0
  365. package/components/navigation/Tree.d.ts +83 -0
  366. package/components/navigation/Tree.jsx +347 -0
  367. package/components/navigation/Tree.vue +258 -0
  368. package/components/navigation/_dir.card.html +13 -0
  369. package/components/navigation/navigation.prompt.md +22 -0
  370. package/components/pipeline/ConfigPanel.d.ts +18 -0
  371. package/components/pipeline/ConfigPanel.jsx +30 -0
  372. package/components/pipeline/ConfigPanel.vue +32 -0
  373. package/components/pipeline/DataPreview.d.ts +14 -0
  374. package/components/pipeline/DataPreview.jsx +24 -0
  375. package/components/pipeline/DataPreview.vue +21 -0
  376. package/components/pipeline/NodeCard.d.ts +47 -0
  377. package/components/pipeline/NodeCard.jsx +97 -0
  378. package/components/pipeline/NodeCard.vue +75 -0
  379. package/components/pipeline/PaletteItem.vue +15 -0
  380. package/components/pipeline/PaletteListItem.vue +15 -0
  381. package/components/pipeline/PalettePanel.d.ts +20 -0
  382. package/components/pipeline/PalettePanel.jsx +74 -0
  383. package/components/pipeline/PalettePanel.vue +45 -0
  384. package/components/pipeline/PipelineCanvas.d.ts +25 -0
  385. package/components/pipeline/PipelineCanvas.jsx +94 -0
  386. package/components/pipeline/PipelineCanvas.vue +73 -0
  387. package/components/pipeline/ZoomControls.d.ts +9 -0
  388. package/components/pipeline/ZoomControls.jsx +15 -0
  389. package/components/pipeline/ZoomControls.vue +15 -0
  390. package/components/pipeline/_dir.card.html +13 -0
  391. package/components/pipeline/pipeline.prompt.md +22 -0
  392. package/components/tabs/SegmentedControl.d.ts +11 -0
  393. package/components/tabs/SegmentedControl.jsx +21 -0
  394. package/components/tabs/SegmentedControl.vue +15 -0
  395. package/components/tabs/Tabs.d.ts +13 -0
  396. package/components/tabs/Tabs.jsx +52 -0
  397. package/components/tabs/Tabs.vue +39 -0
  398. package/components/tabs/ViewToggle.d.ts +8 -0
  399. package/components/tabs/ViewToggle.jsx +12 -0
  400. package/components/tabs/ViewToggle.vue +12 -0
  401. package/components/tabs/_dir.card.html +13 -0
  402. package/components/tabs/tabs.prompt.md +14 -0
  403. package/package.json +76 -0
  404. package/readme.md +150 -0
  405. package/styles.css +10 -0
  406. package/tokens/alpha.css +27 -0
  407. package/tokens/charts.css +55 -0
  408. package/tokens/colors-extended.css +40 -0
  409. package/tokens/colors.css +39 -0
  410. package/tokens/dark.css +96 -0
  411. package/tokens/fonts.css +4 -0
  412. package/tokens/interaction.css +48 -0
  413. package/tokens/layout.css +30 -0
  414. package/tokens/motion.css +22 -0
  415. package/tokens/tokens.d.ts +680 -0
  416. package/tokens/tokens.js +1018 -0
  417. package/tokens/tokens.json +2928 -0
  418. package/tokens/typography.css +22 -0
@@ -0,0 +1 @@
1
+ window.__KB_SRC__=Object.assign(window.__KB_SRC__||{},{"charts/chart-geometry.js":"// Framework-neutral chart math shared by the React and Vue chart components.\n// Everything here is pure: values in, geometry out. No DOM, no tokens.\n\nexport const SERIES = [\"var(--series-1)\",\"var(--series-2)\",\"var(--series-3)\",\"var(--series-4)\",\"var(--series-5)\",\"var(--series-6)\",\"var(--series-7)\",\"var(--series-8)\",\"var(--series-9)\",\"var(--series-10)\"];\nexport const seriesColor = (i) => SERIES[i % SERIES.length];\n\n/** Compact number label: 1.24M · 843K · 12,400 → 12.4K · 0.82 */\nexport function fmtNum(v, opts = {}) {\n if (v == null || isNaN(v)) return \"–\";\n const abs = Math.abs(v);\n const sign = v < 0 ? \"-\" : \"\";\n const f = (n, d) => n.toFixed(d).replace(/\\.0+$|(\\.\\d*[1-9])0+$/, \"$1\");\n if (opts.pct) return f(v, abs < 10 ? 1 : 0) + \"%\";\n if (abs >= 1e9) return sign + f(abs / 1e9, 2) + \"B\";\n if (abs >= 1e6) return sign + f(abs / 1e6, 2) + \"M\";\n if (abs >= 1e4) return sign + f(abs / 1e3, 1) + \"K\";\n if (abs >= 1000) return sign + abs.toLocaleString(\"en-US\");\n return sign + f(abs, abs < 1 && abs > 0 ? 2 : abs < 100 && abs % 1 ? 1 : 0);\n}\n\n/** Nice axis ticks covering [min,max]. Returns {ticks, lo, hi}. */\nexport function niceTicks(min, max, count = 4) {\n if (min === max) { max = min + (min === 0 ? 1 : Math.abs(min) * 0.1); }\n const span = max - min;\n const step0 = span / Math.max(1, count);\n const mag = Math.pow(10, Math.floor(Math.log10(step0)));\n const step = [1, 2, 2.5, 5, 10].map((m) => m * mag).find((s) => span / s <= count + 0.5) || 10 * mag;\n const lo = Math.floor(min / step) * step;\n const hi = Math.ceil(max / step) * step;\n const ticks = [];\n for (let t = lo; t <= hi + step / 1e6; t += step) ticks.push(Math.abs(t) < step / 1e6 ? 0 : +t.toPrecision(12));\n return { ticks, lo, hi };\n}\n\nexport const scaleLin = (lo, hi, r0, r1) => (v) => r0 + ((v - lo) / (hi - lo || 1)) * (r1 - r0);\n\n/** Extent over arrays of numbers (ignores null). */\nexport function extent(arrs, zero = true) {\n let min = Infinity, max = -Infinity;\n arrs.forEach((a) => a.forEach((v) => { if (v == null) return; if (v < min) min = v; if (v > max) max = v; }));\n if (min === Infinity) { min = 0; max = 1; }\n if (zero) { min = Math.min(0, min); max = Math.max(0, max); }\n return [min, max];\n}\n\nexport const linePath = (pts) => pts.length ? \"M\" + pts.map((p) => p[0].toFixed(1) + \" \" + p[1].toFixed(1)).join(\" L\") : \"\";\nexport const areaPath = (pts, y0) => pts.length ? linePath(pts) + \" L\" + pts[pts.length - 1][0].toFixed(1) + \" \" + y0.toFixed(1) + \" L\" + pts[0][0].toFixed(1) + \" \" + y0.toFixed(1) + \" Z\" : \"\";\n\n/** Stack rows of series values → per-series [y0,y1] bands (positive stacking). */\nexport function stack(seriesValues) {\n const n = seriesValues[0] ? seriesValues[0].length : 0;\n const acc = new Array(n).fill(0);\n return seriesValues.map((vals) => vals.map((v, i) => { const y0 = acc[i]; acc[i] += v || 0; return [y0, acc[i]]; }));\n}\n\n/** Polar arc path (annular sector) for donut/gauge. Angles in degrees, 0 = 12 o'clock, clockwise. */\nexport function arcPath(cx, cy, rOuter, rInner, a0, a1) {\n const rad = (a) => ((a - 90) * Math.PI) / 180;\n const p = (r, a) => [cx + r * Math.cos(rad(a)), cy + r * Math.sin(rad(a))];\n const large = a1 - a0 > 180 ? 1 : 0;\n const [x0, y0] = p(rOuter, a0), [x1, y1] = p(rOuter, a1), [x2, y2] = p(rInner, a1), [x3, y3] = p(rInner, a0);\n return `M${x0.toFixed(2)} ${y0.toFixed(2)} A${rOuter} ${rOuter} 0 ${large} 1 ${x1.toFixed(2)} ${y1.toFixed(2)} L${x2.toFixed(2)} ${y2.toFixed(2)} A${rInner} ${rInner} 0 ${large} 0 ${x3.toFixed(2)} ${y3.toFixed(2)} Z`;\n}\n\n/** Waterfall: steps [{label,value,total}] → bars [{label,y0,y1,kind}] with running sum. kind: pos|neg|total */\nexport function waterfall(steps) {\n let run = 0;\n return steps.map((s) => {\n if (s.total) { const b = { label: s.label, y0: 0, y1: run, kind: \"total\", value: run }; return b; }\n const y0 = run; run += s.value;\n return { label: s.label, y0: Math.min(y0, run), y1: Math.max(y0, run), kind: s.value >= 0 ? \"pos\" : \"neg\", value: s.value, from: y0, to: run };\n });\n}\n\n/** Squarified treemap. items [{value,...}] → [{x,y,w,h,item}] in a w×h box. */\nexport function treemap(items, w, h) {\n const total = items.reduce((s, it) => s + it.value, 0) || 1;\n const scaled = items.map((it) => ({ it, a: (it.value / total) * w * h })).sort((a, b) => b.a - a.a);\n const out = [];\n let x = 0, y = 0, rw = w, rh = h, row = [];\n const worst = (row, side) => {\n const s = row.reduce((t, r) => t + r.a, 0);\n let mx = 0;\n row.forEach((r) => { const ratio = Math.max((side * side * r.a) / (s * s), (s * s) / (side * side * r.a)); if (ratio > mx) mx = ratio; });\n return mx;\n };\n const layoutRow = (row) => {\n const s = row.reduce((t, r) => t + r.a, 0);\n const horiz = rw >= rh;\n const side = horiz ? rh : rw;\n const thick = s / side;\n let off = 0;\n row.forEach((r) => {\n const len = r.a / thick;\n out.push(horiz ? { x, y: y + off, w: thick, h: len, item: r.it } : { x: x + off, y, w: len, h: thick, item: r.it });\n off += len;\n });\n if (horiz) { x += thick; rw -= thick; } else { y += thick; rh -= thick; }\n };\n scaled.forEach((r) => {\n const side = rw >= rh ? rh : rw;\n if (row.length && worst([...row, r], side) > worst(row, side)) { layoutRow(row); row = [r]; } else row.push(r);\n });\n if (row.length) layoutRow(row);\n return out;\n}\n\n/** 5-step sequential heat color index for v within [lo,hi]. */\nexport function heatStep(v, lo, hi) {\n const t = (v - lo) / (hi - lo || 1);\n return Math.max(0, Math.min(4, Math.floor(t * 5 - 1e-9)));\n}\n\n/** Monotone cubic path through points — smooth without overshooting real values. */\nexport function curvePath(pts) {\n const n = pts.length;\n if (n < 3) return linePath(pts);\n const xs = pts.map((p) => p[0]), ys = pts.map((p) => p[1]);\n const dx = [], dy = [], m = [];\n for (let i = 0; i < n - 1; i++) { dx.push(xs[i + 1] - xs[i]); dy.push(ys[i + 1] - ys[i]); m.push(dy[i] / (dx[i] || 1)); }\n const t = [m[0]];\n for (let i = 1; i < n - 1; i++) t.push(m[i - 1] * m[i] <= 0 ? 0 : (m[i - 1] + m[i]) / 2);\n t.push(m[n - 2]);\n let d = \"M\" + xs[0].toFixed(1) + \" \" + ys[0].toFixed(1);\n for (let i = 0; i < n - 1; i++) {\n const c = dx[i] / 3;\n d += \" C\" + (xs[i] + c).toFixed(1) + \" \" + (ys[i] + t[i] * c).toFixed(1) + \",\" + (xs[i + 1] - c).toFixed(1) + \" \" + (ys[i + 1] - t[i + 1] * c).toFixed(1) + \",\" + xs[i + 1].toFixed(1) + \" \" + ys[i + 1].toFixed(1);\n }\n return d;\n}\n/** Step path (stairs) — for values that hold until the next reading. */\nexport function stepPath(pts) {\n if (!pts.length) return \"\";\n let d = \"M\" + pts[0][0].toFixed(1) + \" \" + pts[0][1].toFixed(1);\n for (let i = 1; i < pts.length; i++) { const mx = (pts[i - 1][0] + pts[i][0]) / 2; d += \" L\" + mx.toFixed(1) + \" \" + pts[i - 1][1].toFixed(1) + \" L\" + mx.toFixed(1) + \" \" + pts[i][1].toFixed(1) + \" L\" + pts[i][0].toFixed(1) + \" \" + pts[i][1].toFixed(1); }\n return d;\n}\n/** linear | smooth | step in one call. */\nexport const shapePath = (pts, curve) => (curve === \"smooth\" ? curvePath(pts) : curve === \"step\" ? stepPath(pts) : linePath(pts));\nexport const closeToBaseline = (d, pts, y0) => (d ? d + \" L\" + pts[pts.length - 1][0].toFixed(1) + \" \" + y0.toFixed(1) + \" L\" + pts[0][0].toFixed(1) + \" \" + y0.toFixed(1) + \" Z\" : \"\");\n\n/** Sankey layout. nodes:[{id,label}], links:[{source,target,value}] — depth by longest path. */\nexport function sankey(nodes, links, w, h, nodeW, pad) {\n nodeW = nodeW || 14; pad = pad == null ? 14 : pad;\n const byId = {};\n nodes.forEach((n) => { byId[n.id] = Object.assign({}, n, { in: [], out: [], depth: 0, value: 0 }); });\n links.forEach((l) => { byId[l.source].out.push(l); byId[l.target].in.push(l); });\n let changed = true, guard = 0;\n while (changed && guard++ < 60) {\n changed = false;\n links.forEach((l) => { const d = byId[l.source].depth + 1; if (d > byId[l.target].depth) { byId[l.target].depth = d; changed = true; } });\n }\n const list = Object.keys(byId).map((k) => byId[k]);\n list.forEach((n) => { n.value = Math.max(n.in.reduce((s, l) => s + l.value, 0), n.out.reduce((s, l) => s + l.value, 0)); });\n const maxDepth = Math.max.apply(null, list.map((n) => n.depth));\n /* Sinks hug the right edge so the diagram reads left-to-right even with uneven path lengths. */\n list.forEach((n) => { if (!n.out.length) n.depth = maxDepth; });\n const cols = [];\n list.forEach((n) => { (cols[n.depth] = cols[n.depth] || []).push(n); });\n const colX = maxDepth ? (w - nodeW) / maxDepth : 0;\n const scale = Math.min.apply(null, cols.map((c) => (h - pad * (c.length - 1)) / (c.reduce((s, n) => s + n.value, 0) || 1)));\n cols.forEach((col, ci) => {\n let y = 0;\n col.forEach((n) => { n.x = ci * colX; n.w = nodeW; n.y = y; n.h = Math.max(2, n.value * scale); y += n.h + pad; });\n const extra = (h - (y - pad)) / 2;\n col.forEach((n) => { n.y += extra; });\n });\n const outAt = {}, inAt = {};\n const ribbons = links.map((l) => {\n const s = byId[l.source], t = byId[l.target];\n const so = (outAt[l.source] = outAt[l.source] || 0), ti = (inAt[l.target] = inAt[l.target] || 0);\n const th = Math.max(1, l.value * scale);\n outAt[l.source] += th; inAt[l.target] += th;\n const x0 = s.x + s.w, x1 = t.x, cx = (x1 - x0) / 2;\n const y0 = s.y + so, y1 = t.y + ti;\n const d = \"M\" + x0 + \" \" + y0 + \" C\" + (x0 + cx) + \" \" + y0 + \", \" + (x1 - cx) + \" \" + y1 + \", \" + x1 + \" \" + y1\n + \" L\" + x1 + \" \" + (y1 + th) + \" C\" + (x1 - cx) + \" \" + (y1 + th) + \", \" + (x0 + cx) + \" \" + (y0 + th) + \", \" + x0 + \" \" + (y0 + th) + \" Z\";\n return { link: l, thickness: th, d: d, source: s, target: t };\n });\n return { nodes: list, ribbons: ribbons, maxDepth: maxDepth };\n}\n\n/** Chord (circular sankey) layout from a square matrix of flows. */\nexport function chord(matrix, gapDeg) {\n gapDeg = gapDeg == null ? 2 : gapDeg;\n const n = matrix.length;\n const totals = matrix.map((row, i) => row.reduce((s, v) => s + v, 0) + matrix.reduce((s, r2, j) => s + (j === i ? 0 : r2[i]), 0));\n const grand = totals.reduce((s, v) => s + v, 0) || 1;\n const span = 360 - gapDeg * n;\n const arcs = []; let a = 0;\n totals.forEach((v, i) => { const sweep = (v / grand) * span; arcs.push({ i: i, a0: a, a1: a + sweep, value: v }); a += sweep + gapDeg; });\n const cursor = arcs.map((x) => x.a0);\n const ribbons = [];\n for (let i = 0; i < n; i++) for (let j = 0; j < n; j++) {\n const v = matrix[i][j];\n if (!v || i === j) continue;\n const sweep = (v / grand) * span;\n const a0 = cursor[i], a1 = a0 + sweep; cursor[i] = a1;\n const b0 = cursor[j], b1 = b0 + sweep; cursor[j] = b1;\n ribbons.push({ i: i, j: j, value: v, a0: a0, a1: a1, b0: b0, b1: b1 });\n }\n return { arcs: arcs, ribbons: ribbons, totals: totals, grand: grand };\n}\nexport const polar = (cx, cy, r, deg) => [cx + r * Math.cos(((deg - 90) * Math.PI) / 180), cy + r * Math.sin(((deg - 90) * Math.PI) / 180)];\n/** Ribbon between two arc spans, bowed through the circle centre. */\nexport function chordRibbon(cx, cy, r, a0, a1, b0, b1) {\n const p = (deg) => polar(cx, cy, r, deg);\n const s0 = p(a0), s1 = p(a1), t0 = p(b0), t1 = p(b1);\n const big = (x, y) => (Math.abs(y - x) > 180 ? 1 : 0);\n return \"M\" + s0[0].toFixed(1) + \" \" + s0[1].toFixed(1)\n + \" A\" + r + \" \" + r + \" 0 \" + big(a0, a1) + \" 1 \" + s1[0].toFixed(1) + \" \" + s1[1].toFixed(1)\n + \" Q\" + cx + \" \" + cy + \" \" + t0[0].toFixed(1) + \" \" + t0[1].toFixed(1)\n + \" A\" + r + \" \" + r + \" 0 \" + big(b0, b1) + \" 1 \" + t1[0].toFixed(1) + \" \" + t1[1].toFixed(1)\n + \" Q\" + cx + \" \" + cy + \" \" + s0[0].toFixed(1) + \" \" + s0[1].toFixed(1) + \" Z\";\n}\n/** Quantile buckets for choropleth shading — returns a 0..steps-1 index fn. */\nexport function quantize(values, steps) {\n const sorted = values.filter((v) => v != null).slice().sort((a, b) => a - b);\n if (!sorted.length) return () => 0;\n const cuts = [];\n for (let i = 1; i < steps; i++) cuts.push(sorted[Math.floor((i / steps) * sorted.length)]);\n return (v) => { if (v == null) return -1; let k = 0; while (k < cuts.length && v >= cuts[k]) k++; return k; };\n}\n","charts/ChartKit.jsx":"import React from \"react\";\nimport { fmtNum, seriesColor } from \"./chart-geometry.js\";\n\nconst sans = \"var(--font-sans)\", mono = \"var(--font-mono)\";\n\n/** Legend row. items: [{label,color}] — with onToggle, clicking mutes a series. */\nexport function ChartLegend({ items = [], hidden = [], onToggle, align = \"left\", style }) {\n return (\n <div style={{ display: \"flex\", flexWrap: \"wrap\", gap: \"4px 14px\", justifyContent: align === \"right\" ? \"flex-end\" : \"flex-start\", lineHeight: \"normal\", ...style }}>\n {items.map((it, i) => {\n const off = hidden.includes(it.label);\n return (\n <span key={it.label} role={onToggle ? \"button\" : undefined} tabIndex={onToggle ? 0 : undefined}\n onClick={onToggle ? () => onToggle(it.label) : undefined}\n style={{ display: \"inline-flex\", alignItems: \"center\", gap: 6, cursor: onToggle ? \"pointer\" : \"default\", opacity: off ? 0.4 : 1, transition: \"var(--t-hover)\" }}>\n <span style={{ width: 8, height: 8, borderRadius: 2, background: it.color || seriesColor(i), flexShrink: 0 }} />\n <span style={{ fontFamily: sans, fontSize: \"var(--text-xs)\", color: \"var(--text-secondary)\", textDecoration: off ? \"line-through\" : \"none\" }}>{it.label}</span>\n </span>\n );\n })}\n </div>\n );\n}\n\n/** Floating tooltip. rows: [{label,value,color}]; pins near (x,y) inside the chart wrapper. */\nexport function ChartTooltip({ x, y, title, rows = [], flip }) {\n return (\n <div style={{ position: \"absolute\", left: x, top: y, transform: `translate(${flip ? \"calc(-100% - 10px)\" : \"10px\"}, -50%)`, pointerEvents: \"none\", zIndex: 5, background: \"var(--bg-surface)\", border: \"1px solid var(--border-card)\", borderRadius: \"var(--radius-md)\", boxShadow: \"var(--shadow-menu)\", padding: \"7px 10px\", minWidth: 90, lineHeight: \"normal\", whiteSpace: \"nowrap\" }}>\n {title != null ? <div style={{ fontFamily: sans, fontSize: \"var(--text-2xs)\", color: \"var(--text-muted)\", paddingBottom: rows.length ? 4 : 0, whiteSpace: \"nowrap\" }}>{title}</div> : null}\n {rows.map((r, i) => (\n <div key={i} style={{ display: \"flex\", alignItems: \"center\", gap: 6, padding: \"1px 0\" }}>\n {r.color ? <span style={{ width: 7, height: 7, borderRadius: 2, background: r.color, flexShrink: 0 }} /> : null}\n <span style={{ fontFamily: sans, fontSize: \"var(--text-xs)\", color: \"var(--text-secondary)\", whiteSpace: \"nowrap\" }}>{r.label}</span>\n <span style={{ fontFamily: mono, fontSize: \"var(--text-xs)\", color: \"var(--text-heading)\", marginLeft: \"auto\", paddingLeft: 10 }}>{r.value}</span>\n </div>\n ))}\n </div>\n );\n}\n\n/** y-axis tick labels + horizontal gridlines, x category labels. Shared plot furniture. */\nexport function PlotGrid({ w, h, pad, ticks, sy, xLabels = [], sxLabel, showAxisLine = false, yFormat = fmtNum }) {\n return (\n <g>\n {ticks.map((t) => (\n <g key={t}>\n <line x1={pad.l} x2={w - pad.r} y1={sy(t)} y2={sy(t)} stroke={t === 0 ? \"var(--chart-axis)\" : \"var(--chart-grid)\"} strokeWidth=\"1\" />\n <text x={pad.l - 8} y={sy(t) + 3} textAnchor=\"end\" style={{ fontFamily: mono, fontSize: 10, fill: \"var(--chart-label)\" }}>{yFormat(t)}</text>\n </g>\n ))}\n {showAxisLine ? <line x1={pad.l} x2={pad.l} y1={pad.t} y2={h - pad.b} stroke=\"var(--chart-axis)\" strokeWidth=\"1\" /> : null}\n {xLabels.map((lb, i) => (lb == null ? null : (\n <text key={i} x={sxLabel(i)} y={h - pad.b + 16} textAnchor=\"middle\" style={{ fontFamily: sans, fontSize: 10, fill: \"var(--chart-label)\" }}>{lb}</text>\n )))}\n </g>\n );\n}\n\n/** Thin x labels only (sm tier / no y axis). */\nexport function XLabels({ h, pad, xLabels = [], sxLabel }) {\n return <g>{xLabels.map((lb, i) => (lb == null ? null : <text key={i} x={sxLabel(i)} y={h - pad.b + 16} textAnchor=\"middle\" style={{ fontFamily: sans, fontSize: 10, fill: \"var(--chart-label)\" }}>{lb}</text>))}</g>;\n}\n\n/** Skip x labels so they never collide: keeps ~every nth. */\nexport function thinLabels(labels, maxShown = 8) {\n const n = labels.length;\n if (n <= maxShown) return labels;\n const step = Math.ceil(n / maxShown);\n return labels.map((l, i) => (i % step === 0 || i === n - 1 ? l : null));\n}\n\n/** Wrapper div a chart mounts into: relative for tooltips, block-level svg. */\nexport const wrapStyle = { position: \"relative\", display: \"inline-block\", verticalAlign: \"top\", lineHeight: 0 };\n\n/** Namespace handle so the kit can be imported as one symbol. */\nexport const ChartKit = { ChartLegend, ChartTooltip, PlotGrid, XLabels, thinLabels, wrapStyle };\n","charts/Sparkline.jsx":"import React from \"react\";\nimport { extent, scaleLin, shapePath, closeToBaseline, fmtNum } from \"./chart-geometry.js\";\n\n/**\n * Micro line/area/bar chart for stat tiles, table cells and list rows. No axes, no labels.\n * mode: \"line\" | \"area\" | \"bars\". curve: \"linear\" | \"smooth\" | \"step\".\n * tone: \"default\" (series-1) | \"auto\" (green up / red down) | \"pos\" | \"neg\" | \"muted\".\n * Interactive by default: hovering snaps to the nearest point and shows a compact value chip.\n */\nexport function Sparkline({ values = [], mode = \"line\", curve = \"linear\", tone = \"default\", width = 96, height = 28, strokeWidth = 1.5, dot = true, baseline, interactive = true, labels, valueFormat = fmtNum, style }) {\n const [hover, setHover] = React.useState(null);\n const uid = React.useId ? React.useId().replace(/:/g, \"\") : \"kbspark\";\n const [lo, hi] = extent([values], mode === \"bars\");\n const pad = strokeWidth + 1.5;\n const sx = scaleLin(0, values.length - 1 || 1, pad, width - pad);\n const sy = scaleLin(lo, hi, height - pad, pad);\n const up = values.length > 1 && values[values.length - 1] >= values[0];\n const color = tone === \"auto\" ? (up ? \"var(--chart-pos)\" : \"var(--chart-neg)\")\n : tone === \"pos\" ? \"var(--chart-pos)\" : tone === \"neg\" ? \"var(--chart-neg)\"\n : tone === \"muted\" ? \"var(--chart-guide)\" : \"var(--series-1)\";\n const pts = values.map((v, i) => [sx(i), sy(v)]);\n const d = shapePath(pts, curve);\n const bw = Math.max(2, (width - pad * 2) / (values.length || 1) - 2);\n const move = (e) => {\n if (!interactive) return;\n const r = e.currentTarget.getBoundingClientRect();\n const i = Math.round(((e.clientX - r.left - pad) / (width - pad * 2)) * (values.length - 1 || 1));\n setHover(Math.max(0, Math.min(values.length - 1, i)));\n };\n const chip = hover != null && interactive ? (\n <span style={{ position: \"absolute\", left: Math.min(Math.max(sx(hover), 18), width - 18), top: -4, transform: \"translate(-50%,-100%)\", pointerEvents: \"none\", whiteSpace: \"nowrap\", background: \"var(--bg-surface)\", border: \"1px solid var(--border-card)\", borderRadius: \"var(--radius-sm)\", boxShadow: \"var(--shadow-menu)\", padding: \"2px 6px\", fontFamily: \"var(--font-mono)\", fontSize: 10, lineHeight: \"14px\", color: \"var(--text-heading)\", zIndex: 5 }}>\n {labels && labels[hover] ? <span style={{ color: \"var(--text-muted)\", paddingRight: 5 }}>{labels[hover]}</span> : null}\n {valueFormat(values[hover])}\n </span>\n ) : null;\n return (\n <span style={{ position: \"relative\", display: \"inline-block\", verticalAlign: \"middle\", lineHeight: 0, ...style }}>\n <svg width={width} height={height} onMouseMove={move} onMouseLeave={() => setHover(null)} role=\"img\" style={{ display: \"block\", overflow: \"visible\" }}>\n {mode === \"area\" ? (\n <defs>\n <linearGradient id={uid} x1=\"0\" y1=\"0\" x2=\"0\" y2=\"1\">\n <stop offset=\"0%\" stopColor={color} stopOpacity=\"0.38\" />\n <stop offset=\"100%\" stopColor={color} stopOpacity=\"0.02\" />\n </linearGradient>\n </defs>\n ) : null}\n {mode === \"bars\" ? values.map((v, i) => {\n const y = sy(Math.max(0, v)), y0 = sy(Math.min(0, v));\n return <rect key={i} x={sx(i) - bw / 2} y={y} width={bw} height={Math.max(1.5, y0 - y)} rx=\"1\" fill={v < 0 ? \"var(--chart-neg)\" : color} opacity={hover === i ? 1 : hover != null ? 0.4 : i === values.length - 1 ? 1 : 0.55} />;\n }) : (\n <>\n {baseline != null ? <line x1={pad} x2={width - pad} y1={sy(baseline)} y2={sy(baseline)} stroke=\"var(--chart-guide)\" strokeWidth=\"1\" strokeDasharray=\"2 3\" /> : null}\n {mode === \"area\" ? <path d={closeToBaseline(d, pts, height - pad)} fill={\"url(#\" + uid + \")\"} /> : null}\n <path d={d} fill=\"none\" stroke={color} strokeWidth={strokeWidth} strokeLinejoin=\"round\" strokeLinecap=\"round\" />\n {dot && pts.length ? <circle cx={pts[pts.length - 1][0]} cy={pts[pts.length - 1][1]} r={strokeWidth + 0.8} fill={color} /> : null}\n {hover != null && interactive ? <circle cx={sx(hover)} cy={sy(values[hover])} r={strokeWidth + 1.4} fill={color} stroke=\"var(--bg-surface)\" strokeWidth=\"1.5\" /> : null}\n </>\n )}\n </svg>\n {chip}\n </span>\n );\n}\n","charts/Sparkline.vue":"<script setup>\nimport { computed, ref } from 'vue'\nimport { extent, scaleLin, shapePath, closeToBaseline, fmtNum } from './chart-geometry.js'\n// Micro line/area/bar trend — no axes, no labels. tone \"auto\" = green up / red down.\nconst props = defineProps({ values:{type:Array,default:()=>[]}, mode:{type:String,default:'line'}, curve:{type:String,default:'linear'}, tone:{type:String,default:'default'}, width:{type:Number,default:96}, height:{type:Number,default:28}, strokeWidth:{type:Number,default:1.5}, dot:{type:Boolean,default:true}, baseline:Number, interactive:{type:Boolean,default:true}, labels:Array, valueFormat:{type:Function,default:fmtNum}, style:Object })\nconst hover = ref(null)\nconst uid = 'kbspark-' + Math.random().toString(36).slice(2, 8)\nfunction move(e){\n if (!props.interactive) return\n const r = e.currentTarget.getBoundingClientRect()\n const pad = props.strokeWidth + 1.5\n const i = Math.round(((e.clientX-r.left-pad)/(props.width-pad*2))*(props.values.length-1||1))\n hover.value = Math.max(0, Math.min(props.values.length-1, i))\n}\nconst g = computed(() => {\n const [lo,hi] = extent([props.values], props.mode==='bars')\n const pad = props.strokeWidth + 1.5\n const sx = scaleLin(0, props.values.length-1||1, pad, props.width-pad)\n const sy = scaleLin(lo, hi, props.height-pad, pad)\n const up = props.values.length>1 && props.values[props.values.length-1] >= props.values[0]\n const color = props.tone==='auto' ? (up?'var(--chart-pos)':'var(--chart-neg)') : props.tone==='pos'?'var(--chart-pos)' : props.tone==='neg'?'var(--chart-neg)' : props.tone==='muted'?'var(--chart-guide)':'var(--series-1)'\n const pts = props.values.map((v,i)=>[sx(i),sy(v)])\n const bw = Math.max(2,(props.width-pad*2)/props.values.length-2)\n return { pad, sx, sy, color, pts, bw,\n line: shapePath(pts, props.curve), area: closeToBaseline(shapePath(pts, props.curve), pts, props.height-pad),\n baseY: props.baseline!=null ? sy(props.baseline) : null,\n bars: props.values.map((v,i)=>({x:sx(i)-bw/2,y:sy(Math.max(0,v)),h:Math.max(1.5,sy(Math.min(0,v))-sy(Math.max(0,v))),neg:v<0,last:i===props.values.length-1})) }\n})\n</script>\n<template>\n <span :style=\"{position:'relative',display:'inline-block',verticalAlign:'middle',lineHeight:0,...style}\">\n <svg :width=\"width\" :height=\"height\" @mousemove=\"move\" @mouseleave=\"hover=null\" role=\"img\" style=\"display:block;overflow:visible\">\n <defs v-if=\"mode==='area'\">\n <linearGradient :id=\"uid\" x1=\"0\" y1=\"0\" x2=\"0\" y2=\"1\">\n <stop offset=\"0%\" :stop-color=\"g.color\" stop-opacity=\"0.38\"/>\n <stop offset=\"100%\" :stop-color=\"g.color\" stop-opacity=\"0.02\"/>\n </linearGradient>\n </defs>\n <template v-if=\"mode==='bars'\">\n <rect v-for=\"(b,i) in g.bars\" :key=\"i\" :x=\"b.x\" :y=\"b.y\" :width=\"g.bw\" :height=\"b.h\" rx=\"1\" :fill=\"b.neg?'var(--chart-neg)':g.color\" :opacity=\"hover===i?1:(hover!=null?.4:(b.last?1:.55))\"/>\n </template>\n <template v-else>\n <line v-if=\"g.baseY!=null\" :x1=\"g.pad\" :x2=\"width-g.pad\" :y1=\"g.baseY\" :y2=\"g.baseY\" stroke=\"var(--chart-guide)\" stroke-width=\"1\" stroke-dasharray=\"2 3\"/>\n <path v-if=\"mode==='area'\" :d=\"g.area\" :fill=\"'url(#'+uid+')'\"/>\n <path :d=\"g.line\" fill=\"none\" :stroke=\"g.color\" :stroke-width=\"strokeWidth\" stroke-linejoin=\"round\" stroke-linecap=\"round\"/>\n <circle v-if=\"dot && g.pts.length\" :cx=\"g.pts[g.pts.length-1][0]\" :cy=\"g.pts[g.pts.length-1][1]\" :r=\"strokeWidth+0.8\" :fill=\"g.color\"/>\n <circle v-if=\"hover!=null && interactive\" :cx=\"g.sx(hover)\" :cy=\"g.sy(values[hover])\" :r=\"strokeWidth+1.4\" :fill=\"g.color\" stroke=\"var(--bg-surface)\" stroke-width=\"1.5\"/>\n </template>\n </svg>\n <span v-if=\"hover!=null && interactive\" :style=\"{position:'absolute',left:Math.min(Math.max(g.sx(hover),18),width-18)+'px',top:'-4px',transform:'translate(-50%,-100%)',pointerEvents:'none',whiteSpace:'nowrap',background:'var(--bg-surface)',border:'1px solid var(--border-card)',borderRadius:'var(--radius-sm)',boxShadow:'var(--shadow-menu)',padding:'2px 6px',fontFamily:'var(--font-mono)',fontSize:'10px',lineHeight:'14px',color:'var(--text-heading)',zIndex:5}\">\n <span v-if=\"labels && labels[hover]\" style=\"color:var(--text-muted);padding-right:5px\">{{ labels[hover] }}</span>{{ valueFormat(values[hover]) }}\n </span>\n </span>\n</template>\n","charts/LineChart.jsx":"import React from \"react\";\nimport { fmtNum, extent, scaleLin, shapePath, closeToBaseline, stack, niceTicks, seriesColor } from \"./chart-geometry.js\";\nimport { ChartLegend, ChartTooltip, PlotGrid, thinLabels, wrapStyle } from \"./ChartKit.jsx\";\n\n/**\n * Line / area / stacked-area chart. series: [{name, values, color?}] over shared xLabels.\n * curve: \"linear\" (default) | \"smooth\" (monotone cubic, never overshoots a real value) | \"step\".\n * Interactive by default at every size: hover guideline + tooltip, click legend to mute a series.\n * sm tier: axes={false} legend={false} for a tile-sized trend — the tooltip stays.\n */\nexport function LineChart({ series = [], xLabels = [], width = 560, height = 240, area = false, stacked = false, curve = \"linear\", axes = true, legend = true, interactive = true, yFormat = fmtNum, target, targetLabel, style }) {\n const [hidden, setHidden] = React.useState([]);\n const [hover, setHover] = React.useState(null);\n const shown = series.filter((s) => !hidden.includes(s.name));\n const pad = { l: axes ? 44 : 4, r: 8, t: 10, b: axes ? 24 : 4 };\n const vals = shown.map((s) => s.values);\n const bands = stacked && shown.length ? stack(vals) : null;\n const [lo0, hi0] = stacked ? [0, Math.max(1, ...(bands[bands.length - 1] || []).map((b) => b[1]))] : extent(vals.concat(target != null ? [[target]] : []));\n const { ticks, lo, hi } = niceTicks(lo0, hi0, 4);\n const n = Math.max(...series.map((s) => s.values.length), 2);\n const sx = scaleLin(0, n - 1, pad.l + 4, width - pad.r - 4);\n const sy = scaleLin(lo, hi, height - pad.b, pad.t);\n const colorOf = (s) => s.color || seriesColor(series.indexOf(s));\n const base = sy(Math.max(lo, 0));\n const move = (e) => {\n if (!interactive) return;\n const r = e.currentTarget.getBoundingClientRect();\n const x = e.clientX - r.left;\n setHover(Math.max(0, Math.min(n - 1, Math.round(((x - pad.l - 4) / (width - pad.l - pad.r - 8)) * (n - 1)))));\n };\n const gid = React.useId ? React.useId().replace(/:/g, \"\") : \"kbline\";\n /* Stacked bands are closed against the band below, not the baseline: overlapping\n translucent fills stack up into mud. Each band gets its own gradient, scaled to that\n band's own box (objectBoundingBox), so the fade reads the same at any thickness. */\n const layers = shown.map((s, si) => {\n const pts = s.values.map((v, i) => [sx(i), sy(stacked ? bands[si][i][1] : v)]);\n const d = shapePath(pts, curve);\n const under = stacked\n ? [...s.values.keys()].reverse().map((i) => sx(i).toFixed(1) + \" \" + sy(bands[si][i][0]).toFixed(1)).join(\" L\")\n : null;\n return { s, si, pts, d, color: colorOf(s), fill: stacked ? d + \" L\" + under + \" Z\" : closeToBaseline(d, pts, base) };\n });\n const painted = layers;\n return (\n <div style={{ ...wrapStyle, ...style }}>\n <svg width={width} height={height} onMouseMove={move} onMouseLeave={() => setHover(null)} role=\"img\">\n <defs>\n {series.map((s, i) => {\n const c = s.color || seriesColor(i);\n return (\n <linearGradient key={i} id={gid + \"-\" + i} x1=\"0\" y1=\"0\" x2=\"0\" y2=\"1\">\n <stop offset=\"0%\" stopColor={c} stopOpacity={stacked ? \"0.30\" : \"0.34\"} />\n <stop offset=\"100%\" stopColor={c} stopOpacity={stacked ? \"0.10\" : \"0.02\"} />\n </linearGradient>\n );\n })}\n </defs>\n {axes ? <PlotGrid w={width} h={height} pad={pad} ticks={ticks} sy={sy} xLabels={thinLabels(xLabels)} sxLabel={sx} yFormat={yFormat} /> : null}\n {target != null ? <g>\n <line x1={pad.l} x2={width - pad.r} y1={sy(target)} y2={sy(target)} stroke=\"var(--chart-guide)\" strokeWidth=\"1\" strokeDasharray=\"3 3\" />\n <text x={width - pad.r} y={sy(target) - 4} textAnchor=\"end\" style={{ fontFamily: \"var(--font-sans)\", fontSize: 10, fill: \"var(--chart-label)\" }}>{targetLabel || \"Target \" + yFormat(target)}</text>\n </g> : null}\n {painted.map((L) => (\n <g key={L.s.name}>\n {(area || stacked) ? <path d={L.fill} fill={\"url(#\" + gid + \"-\" + series.indexOf(L.s) + \")\"} /> : null}\n <path d={L.d} fill=\"none\" stroke={L.color} strokeWidth=\"1.5\" strokeLinejoin=\"round\" strokeLinecap=\"round\" />\n </g>\n ))}\n {hover != null && interactive ? <line x1={sx(hover)} x2={sx(hover)} y1={pad.t} y2={height - pad.b} stroke=\"var(--chart-guide)\" strokeWidth=\"1\" strokeDasharray=\"2 3\" /> : null}\n {hover != null ? layers.map((L) => (L.s.values[hover] == null ? null : (\n <circle key={L.s.name} cx={sx(hover)} cy={sy(stacked ? bands[L.si][hover][1] : L.s.values[hover])} r=\"3\" fill={L.color} stroke=\"var(--bg-surface)\" strokeWidth=\"1.5\" />\n ))) : null}\n </svg>\n {hover != null && interactive ? (\n <ChartTooltip x={sx(hover)} y={height / 2 - 10} flip={sx(hover) > width * 0.62} title={xLabels[hover]}\n rows={shown.map((s) => ({ label: s.name, value: yFormat(s.values[hover]), color: colorOf(s) }))} />\n ) : null}\n {legend && series.length > 1 ? (\n <ChartLegend style={{ paddingTop: 8 }} items={series.map((s) => ({ label: s.name, color: colorOf(s) }))} hidden={hidden}\n onToggle={(l) => setHidden(hidden.includes(l) ? hidden.filter((x) => x !== l) : hidden.length + 1 < series.length ? [...hidden, l] : hidden)} />\n ) : null}\n </div>\n );\n}\n","charts/LineChart.vue":"<script setup>\nimport { ref, computed } from 'vue'\nimport { fmtNum, extent, scaleLin, shapePath, closeToBaseline, stack, niceTicks, SERIES } from './chart-geometry.js'\n// Line / area / stacked-area over shared x labels. Hover guideline + tooltip; legend click mutes.\nconst props = defineProps({ series:{type:Array,default:()=>[]}, xLabels:{type:Array,default:()=>[]}, width:{type:Number,default:560}, height:{type:Number,default:240}, area:Boolean, stacked:Boolean, axes:{type:Boolean,default:true}, legend:{type:Boolean,default:true}, interactive:{type:Boolean,default:true}, yFormat:{type:Function,default:fmtNum}, target:Number, targetLabel:String, style:Object })\nconst hidden = ref([]); const hover = ref(null)\nconst uid = 'kbline-' + Math.random().toString(36).slice(2, 8)\nconst g = computed(() => {\n const shown = props.series.filter(s=>!hidden.value.includes(s.name))\n const pad = { l: props.axes?44:4, r:8, t:10, b: props.axes?24:4 }\n const vals = shown.map(s=>s.values)\n const bands = props.stacked && shown.length ? stack(vals) : null\n const [lo0,hi0] = props.stacked ? [0, Math.max(1,...(bands[bands.length-1]||[]).map(b=>b[1]))] : extent(vals.concat(props.target!=null?[[props.target]]:[]))\n const { ticks, lo, hi } = niceTicks(lo0, hi0, 4)\n const n = Math.max(...props.series.map(s=>s.values.length), 2)\n const sx = scaleLin(0, n-1, pad.l+4, props.width-pad.r-4)\n const sy = scaleLin(lo, hi, props.height-pad.b, pad.t)\n const colorOf = s => s.color || SERIES[props.series.indexOf(s)%SERIES.length]\n /* Stacked bands close against the band below, not the baseline — overlapping\n translucent fills stack up into mud. */\n const base = sy(Math.max(lo, 0))\n const lines = shown.map((s,si)=>{\n const pts = s.values.map((v,i)=>[sx(i), sy(props.stacked?bands[si][i][1]:v)])\n const d = shapePath(pts, props.curve)\n const fill = props.stacked\n ? d + ' L' + [...s.values.keys()].reverse().map(i=>sx(i).toFixed(1)+' '+sy(bands[si][i][0]).toFixed(1)).join(' L') + ' Z'\n : closeToBaseline(d, pts, base)\n return { s, color: colorOf(s), path: d, fill, pts, at:(i)=>[sx(i), sy(props.stacked?bands[si][i][1]:s.values[i])] }\n })\n return { pad, ticks, sx, sy, n, lines, shown, colorOf }\n})\nfunction move(e){\n if (!props.interactive) return\n const r = e.currentTarget.getBoundingClientRect()\n const gg = g.value\n hover.value = Math.max(0, Math.min(gg.n-1, Math.round(((e.clientX-r.left-gg.pad.l-4)/(props.width-gg.pad.l-gg.pad.r-8))*(gg.n-1))))\n}\nfunction toggle(name){\n if (hidden.value.includes(name)) hidden.value = hidden.value.filter(x=>x!==name)\n else if (hidden.value.length+1 < props.series.length) hidden.value = [...hidden.value, name]\n}\n</script>\n<template>\n <div :style=\"{position:'relative',display:'inline-block',lineHeight:0,...style}\">\n <svg :width=\"width\" :height=\"height\" @mousemove=\"move\" @mouseleave=\"hover=null\" role=\"img\">\n <defs>\n <linearGradient v-for=\"(s,i) in series\" :key=\"'grad'+i\" :id=\"uid+'-'+i\" x1=\"0\" y1=\"0\" x2=\"0\" y2=\"1\">\n <stop offset=\"0%\" :stop-color=\"s.color||SERIES[i%SERIES.length]\" :stop-opacity=\"stacked ? 0.30 : 0.34\"/>\n <stop offset=\"100%\" :stop-color=\"s.color||SERIES[i%SERIES.length]\" :stop-opacity=\"stacked ? 0.10 : 0.02\"/>\n </linearGradient>\n </defs>\n <template v-if=\"axes\">\n <g v-for=\"t in g.ticks\" :key=\"t\">\n <line :x1=\"g.pad.l\" :x2=\"width-g.pad.r\" :y1=\"g.sy(t)\" :y2=\"g.sy(t)\" :stroke=\"t===0?'var(--chart-axis)':'var(--chart-grid)'\" stroke-width=\"1\"/>\n <text :x=\"g.pad.l-8\" :y=\"g.sy(t)+3\" text-anchor=\"end\" style=\"font-family:var(--font-mono);font-size:10px;fill:var(--chart-label)\">{{ yFormat(t) }}</text>\n </g>\n <text v-for=\"(lb,i) in xLabels\" :key=\"'x'+i\" :x=\"g.sx(i)\" :y=\"height-g.pad.b+16\" text-anchor=\"middle\" style=\"font-family:var(--font-sans);font-size:10px;fill:var(--chart-label)\">{{ lb }}</text>\n </template>\n <template v-if=\"target!=null\">\n <line :x1=\"g.pad.l\" :x2=\"width-g.pad.r\" :y1=\"g.sy(target)\" :y2=\"g.sy(target)\" stroke=\"var(--chart-guide)\" stroke-width=\"1\" stroke-dasharray=\"3 3\"/>\n <text :x=\"width-g.pad.r\" :y=\"g.sy(target)-4\" text-anchor=\"end\" style=\"font-family:var(--font-sans);font-size:10px;fill:var(--chart-label)\">{{ targetLabel || 'Target '+yFormat(target) }}</text>\n </template>\n <g v-for=\"ln in g.lines\" :key=\"ln.s.name\">\n <path v-if=\"area||stacked\" :d=\"ln.fill\" :fill=\"'url(#'+uid+'-'+series.indexOf(ln.s)+')'\"/>\n <path :d=\"ln.path\" fill=\"none\" :stroke=\"ln.color\" stroke-width=\"1.5\" stroke-linejoin=\"round\"/>\n <circle v-if=\"hover!=null && ln.s.values[hover]!=null\" :cx=\"ln.at(hover)[0]\" :cy=\"ln.at(hover)[1]\" r=\"3\" :fill=\"ln.color\" stroke=\"var(--bg-surface)\" stroke-width=\"1.5\"/>\n </g>\n <line v-if=\"hover!=null && interactive\" :x1=\"g.sx(hover)\" :x2=\"g.sx(hover)\" :y1=\"g.pad.t\" :y2=\"height-g.pad.b\" stroke=\"var(--chart-guide)\" stroke-width=\"1\" stroke-dasharray=\"2 3\"/>\n </svg>\n <div v-if=\"hover!=null && interactive\" :style=\"{position:'absolute',left:g.sx(hover)+'px',top:(height/2-10)+'px',transform:g.sx(hover)>width*0.62?'translate(calc(-100% - 10px),-50%)':'translate(10px,-50%)',pointerEvents:'none',zIndex:5,background:'var(--bg-surface)',border:'1px solid var(--border-card)',borderRadius:'var(--radius-md)',boxShadow:'var(--shadow-menu)',padding:'7px 10px',minWidth:'90px',lineHeight:'normal'}\">\n <div style=\"font-family:var(--font-sans);font-size:var(--text-2xs);color:var(--text-muted);padding-bottom:4px;white-space:nowrap\">{{ xLabels[hover] }}</div>\n <div v-for=\"ln in g.lines\" :key=\"ln.s.name\" style=\"display:flex;align-items:center;gap:6px;padding:1px 0\">\n <span :style=\"{width:'7px',height:'7px',borderRadius:'2px',background:ln.color,flexShrink:0}\"/>\n <span style=\"font-family:var(--font-sans);font-size:var(--text-xs);color:var(--text-secondary);white-space:nowrap\">{{ ln.s.name }}</span>\n <span style=\"font-family:var(--font-mono);font-size:var(--text-xs);color:var(--text-heading);margin-left:auto;padding-left:10px\">{{ yFormat(ln.s.values[hover]) }}</span>\n </div>\n </div>\n <div v-if=\"legend && series.length>1\" style=\"display:flex;flex-wrap:wrap;gap:4px 14px;padding-top:8px;line-height:normal\">\n <span v-for=\"(s,i) in series\" :key=\"s.name\" role=\"button\" tabindex=\"0\" @click=\"toggle(s.name)\"\n :style=\"{display:'inline-flex',alignItems:'center',gap:'6px',cursor:'pointer',opacity:hidden.includes(s.name)?0.4:1}\">\n <span :style=\"{width:'8px',height:'8px',borderRadius:'2px',background:s.color||SERIES[i%SERIES.length],flexShrink:0}\"/>\n <span :style=\"{fontFamily:'var(--font-sans)',fontSize:'var(--text-xs)',color:'var(--text-secondary)',textDecoration:hidden.includes(s.name)?'line-through':'none'}\">{{ s.name }}</span>\n </span>\n </div>\n </div>\n</template>\n","charts/BarChart.jsx":"import React from \"react\";\nimport { fmtNum, extent, scaleLin, stack, niceTicks, seriesColor, linePath } from \"./chart-geometry.js\";\nimport { ChartLegend, ChartTooltip, PlotGrid, thinLabels, wrapStyle } from \"./ChartKit.jsx\";\n\n/** Bar with only its outer end rounded — a stack must read as one bar, not stacked pills. */\nfunction barPath(x, y, w, h, r, side) {\n const rr = Math.max(0, Math.min(r, w / 2, h / 2));\n if (!rr) return `M${x} ${y} h${w} v${h} h${-w} Z`;\n if (side === \"top\") return `M${x} ${y + h} V${y + rr} a${rr} ${rr} 0 0 1 ${rr} ${-rr} h${w - rr * 2} a${rr} ${rr} 0 0 1 ${rr} ${rr} V${y + h} Z`;\n if (side === \"bottom\") return `M${x} ${y} V${y + h - rr} a${rr} ${rr} 0 0 0 ${rr} ${rr} h${w - rr * 2} a${rr} ${rr} 0 0 0 ${rr} ${-rr} V${y} Z`;\n if (side === \"right\") return `M${x} ${y} h${w - rr} a${rr} ${rr} 0 0 1 ${rr} ${rr} v${h - rr * 2} a${rr} ${rr} 0 0 1 ${-rr} ${rr} h${-(w - rr)} Z`;\n if (side === \"left\") return `M${x + w} ${y} h${-(w - rr)} a${rr} ${rr} 0 0 0 ${-rr} ${rr} v${h - rr * 2} a${rr} ${rr} 0 0 0 ${rr} ${rr} h${w - rr} Z`;\n return `M${x} ${y} h${w} v${h} h${-w} Z`;\n}\n\n/**\n * Bar chart. series over shared categories; mode \"grouped\" | \"stacked\" | \"stacked100\"\n * (share of each category) | \"diverging\" (single series, positive --chart-pos / negative\n * --chart-neg — variance vs. plan). orientation=\"horizontal\" swaps the axes for long\n * category names and ranked comparisons. overlay draws a line on its own secondary axis\n * (vertical only). Inside a stack only the outermost segment is rounded, so joins are flush.\n */\nexport function BarChart({ series = [], categories = [], mode = \"grouped\", orientation = \"vertical\", overlay, width = 560, height = 240, axes = true, legend = true, interactive = true, showValues = false, radius = 2, yFormat = fmtNum, overlayFormat = (v) => fmtNum(v, { pct: true }), style }) {\n const [hidden, setHidden] = React.useState([]);\n const [hover, setHover] = React.useState(null);\n const horiz = orientation === \"horizontal\";\n const shown = series.filter((s) => !hidden.includes(s.name));\n const n = categories.length || Math.max(...series.map((s) => s.values.length));\n const pct = mode === \"stacked100\";\n const stacked = mode === \"stacked\" || pct;\n\n const norm = pct\n ? shown.map((s) => s.values.map((v, i) => {\n const tot = shown.reduce((t, o) => t + (o.values[i] || 0), 0) || 1;\n return ((v || 0) / tot) * 100;\n }))\n : shown.map((s) => s.values);\n const bands = stacked && shown.length ? stack(norm) : null;\n const [lo0, hi0] = pct ? [0, 100] : stacked ? extent([(bands[bands.length - 1] || []).map((b) => b[1])]) : extent(norm);\n const { ticks, lo, hi } = niceTicks(lo0, hi0, 4);\n const labelPad = horiz ? Math.min(140, Math.max(...categories.map((c) => String(c).length)) * 6.4 + 12) : 44;\n const pad = horiz\n ? { l: axes ? labelPad : 4, r: 12, t: 6, b: axes ? 22 : 4 }\n : { l: axes ? 44 : 4, r: overlay ? 40 : 8, t: 10, b: axes ? 24 : 4 };\n\n const sv = horiz ? scaleLin(lo, hi, pad.l, width - pad.r) : scaleLin(lo, hi, height - pad.b, pad.t);\n const band = ((horiz ? height - pad.t - pad.b : width - pad.l - pad.r) / n);\n const mid = (i) => (horiz ? pad.t : pad.l) + band * i + band / 2;\n const groupW = Math.min(band * 0.66, mode === \"grouped\" ? 20 * Math.max(1, shown.length) : 30);\n const bw = mode === \"grouped\" ? groupW / Math.max(1, shown.length) : groupW;\n const colorOf = (s) => s.color || seriesColor(series.indexOf(s));\n const zero = sv(Math.max(lo, 0));\n\n const oScale = overlay && !horiz ? (() => { const [ol, oh] = extent([overlay.values]); const t = niceTicks(ol, oh, 4); return { sy: scaleLin(t.lo, t.hi, height - pad.b, pad.t), ticks: t.ticks }; })() : null;\n const move = (e) => {\n if (!interactive) return;\n const r = e.currentTarget.getBoundingClientRect();\n const off = horiz ? e.clientY - r.top - pad.t : e.clientX - r.left - pad.l;\n const i = Math.floor(off / band);\n setHover(i >= 0 && i < n ? i : null);\n };\n const fmtVal = (s, i) => (pct ? norm[shown.indexOf(s)][i].toFixed(1) + \"%\" : yFormat(s.values[i]));\n\n const rects = [];\n shown.forEach((s, si) => (pct ? norm[si] : s.values).forEach((v, i) => {\n if (v == null) return;\n let fill = mode === \"diverging\" ? (v >= 0 ? \"var(--chart-pos)\" : \"var(--chart-neg)\") : colorOf(s);\n const outer = !shown.slice(si + 1).some((o) => ((pct ? norm[shown.indexOf(o)][i] : o.values[i]) || 0) > 0);\n let x, y, w, h, side;\n if (stacked) {\n const a = sv(bands[si][i][0]), b = sv(bands[si][i][1]);\n if (horiz) { x = Math.min(a, b); w = Math.max(1, Math.abs(b - a)); y = mid(i) - bw / 2; h = bw; side = outer ? \"right\" : null; }\n else { y = Math.min(a, b); h = Math.max(1, Math.abs(a - b)); x = mid(i) - bw / 2; w = bw; side = outer ? \"top\" : null; }\n } else if (horiz) {\n const e = sv(v);\n x = Math.min(zero, e); w = Math.max(1, Math.abs(e - zero)); y = mid(i) - groupW / 2 + si * bw + 0.5; h = Math.max(1, bw - 1);\n side = v >= 0 ? \"right\" : \"left\";\n } else {\n const e = sv(v);\n y = Math.min(zero, e); h = Math.max(1, Math.abs(e - zero)); x = mid(i) - groupW / 2 + si * bw + 0.5; w = Math.max(1, bw - 1);\n side = v >= 0 ? \"top\" : \"bottom\";\n }\n rects.push({ key: s.name + i, d: barPath(x, y, w, h, radius, side), fill, x, y, w, h, v, s, i });\n }));\n\n return (\n <div style={{ ...wrapStyle, ...style }}>\n <svg width={width} height={height} onMouseMove={move} onMouseLeave={() => setHover(null)} role=\"img\">\n {axes && !horiz ? <PlotGrid w={width} h={height} pad={pad} ticks={ticks} sy={sv} xLabels={thinLabels(categories)} sxLabel={mid} yFormat={pct ? (v) => v + \"%\" : yFormat} /> : null}\n {axes && horiz ? (\n <g>\n {ticks.map((t) => (\n <g key={t}>\n <line x1={sv(t)} x2={sv(t)} y1={pad.t} y2={height - pad.b} stroke={t === 0 ? \"var(--chart-axis)\" : \"var(--chart-grid)\"} strokeWidth=\"1\" />\n <text x={sv(t)} y={height - pad.b + 15} textAnchor=\"middle\" style={{ fontFamily: \"var(--font-mono)\", fontSize: 10, fill: \"var(--chart-label)\" }}>{pct ? t + \"%\" : yFormat(t)}</text>\n </g>\n ))}\n {categories.map((c, i) => (\n <text key={i} x={pad.l - 8} y={mid(i) + 3.5} textAnchor=\"end\" style={{ fontFamily: \"var(--font-sans)\", fontSize: 10.5, fill: hover === i ? \"var(--text-heading)\" : \"var(--chart-label)\" }}>{c}</text>\n ))}\n </g>\n ) : null}\n {hover != null ? (horiz\n ? <rect x={pad.l} y={pad.t + band * hover} width={width - pad.l - pad.r} height={band} fill=\"var(--bg-hover)\" />\n : <rect x={pad.l + band * hover} y={pad.t} width={band} height={height - pad.t - pad.b} fill=\"var(--bg-hover)\" />) : null}\n {rects.map((r) => <path key={r.key} d={r.d} fill={r.fill} opacity={hover == null || hover === r.i ? 1 : 0.55} style={{ transition: \"opacity var(--dur-fast) var(--ease-out)\" }} />)}\n {showValues ? rects.map((r) => (\n (horiz ? r.w : r.h) < 22 ? null : (\n <text key={\"v\" + r.key} x={horiz ? r.x + r.w - 5 : r.x + r.w / 2} y={horiz ? r.y + r.h / 2 + 3.5 : r.y + 12}\n textAnchor={horiz ? \"end\" : \"middle\"} style={{ fontFamily: \"var(--font-mono)\", fontSize: 10, fill: \"var(--on-accent)\" }}>{fmtVal(r.s, r.i)}</text>\n )\n )) : null}\n {oScale ? <g>\n <path d={linePath(overlay.values.map((v, i) => [mid(i), oScale.sy(v)]))} fill=\"none\" stroke={overlay.color || \"var(--chart-total)\"} strokeWidth=\"1.5\" />\n {overlay.values.map((v, i) => <circle key={i} cx={mid(i)} cy={oScale.sy(v)} r=\"2.5\" fill={overlay.color || \"var(--chart-total)\"} />)}\n {axes ? oScale.ticks.map((t) => <text key={t} x={width - pad.r + 6} y={oScale.sy(t) + 3} style={{ fontFamily: \"var(--font-mono)\", fontSize: 10, fill: \"var(--chart-label)\" }}>{overlayFormat(t)}</text>) : null}\n </g> : null}\n </svg>\n {hover != null && interactive ? (\n <ChartTooltip\n x={horiz ? Math.min(width - 20, pad.l + (width - pad.l - pad.r) * 0.5) : mid(hover)}\n y={horiz ? pad.t + band * hover + band / 2 : height / 2 - 10}\n flip={horiz ? true : mid(hover) > width * 0.62} title={categories[hover]}\n rows={shown.map((s) => ({ label: s.name, value: fmtVal(s, hover), color: mode === \"diverging\" ? (s.values[hover] >= 0 ? \"var(--chart-pos)\" : \"var(--chart-neg)\") : colorOf(s) }))\n .concat(overlay ? [{ label: overlay.name, value: overlayFormat(overlay.values[hover]), color: overlay.color || \"var(--chart-total)\" }] : [])} />\n ) : null}\n {legend && (series.length > 1 || overlay) ? (\n <ChartLegend style={{ paddingTop: 8 }}\n items={series.map((s) => ({ label: s.name, color: colorOf(s) })).concat(overlay ? [{ label: overlay.name, color: overlay.color || \"var(--chart-total)\" }] : [])}\n hidden={hidden}\n onToggle={(l) => series.some((s) => s.name === l) && setHidden(hidden.includes(l) ? hidden.filter((x) => x !== l) : hidden.length + 1 < series.length ? [...hidden, l] : hidden)} />\n ) : null}\n </div>\n );\n}\n","charts/BarChart.vue":"<script setup>\nimport { ref, computed } from 'vue'\nimport { fmtNum, extent, scaleLin, stack, niceTicks, SERIES, linePath } from './chart-geometry.js'\n/**\n * Bar chart. mode \"grouped\" | \"stacked\" | \"stacked100\" | \"diverging\";\n * orientation=\"horizontal\" swaps the axes. Only the outermost stack segment is rounded,\n * so colour joins inside a stack stay flush. overlay = line on a secondary axis (vertical).\n */\nconst props = defineProps({ series:{type:Array,default:()=>[]}, categories:{type:Array,default:()=>[]}, mode:{type:String,default:'grouped'}, orientation:{type:String,default:'vertical'}, overlay:Object, width:{type:Number,default:560}, height:{type:Number,default:240}, axes:{type:Boolean,default:true}, legend:{type:Boolean,default:true}, interactive:{type:Boolean,default:true}, showValues:Boolean, radius:{type:Number,default:2}, yFormat:{type:Function,default:fmtNum}, overlayFormat:{type:Function,default:(v)=>fmtNum(v,{pct:true})}, style:Object })\nconst hidden = ref([]); const hover = ref(null)\nfunction barPath(x,y,w,h,r,side){\n const rr = Math.max(0, Math.min(r, w/2, h/2))\n if (!rr) return `M${x} ${y} h${w} v${h} h${-w} Z`\n if (side==='top') return `M${x} ${y+h} V${y+rr} a${rr} ${rr} 0 0 1 ${rr} ${-rr} h${w-rr*2} a${rr} ${rr} 0 0 1 ${rr} ${rr} V${y+h} Z`\n if (side==='bottom') return `M${x} ${y} V${y+h-rr} a${rr} ${rr} 0 0 0 ${rr} ${rr} h${w-rr*2} a${rr} ${rr} 0 0 0 ${rr} ${-rr} V${y} Z`\n if (side==='right') return `M${x} ${y} h${w-rr} a${rr} ${rr} 0 0 1 ${rr} ${rr} v${h-rr*2} a${rr} ${rr} 0 0 1 ${-rr} ${rr} h${-(w-rr)} Z`\n if (side==='left') return `M${x+w} ${y} h${-(w-rr)} a${rr} ${rr} 0 0 0 ${-rr} ${rr} v${h-rr*2} a${rr} ${rr} 0 0 0 ${rr} ${rr} h${w-rr} Z`\n return `M${x} ${y} h${w} v${h} h${-w} Z`\n}\nconst g = computed(() => {\n const horiz = props.orientation === 'horizontal'\n const shown = props.series.filter(s=>!hidden.value.includes(s.name))\n const n = props.categories.length || Math.max(...props.series.map(s=>s.values.length))\n const pct = props.mode === 'stacked100'\n const stacked = props.mode === 'stacked' || pct\n const norm = pct\n ? shown.map(s=>s.values.map((v,i)=>{ const tot = shown.reduce((t,o)=>t+(o.values[i]||0),0)||1; return ((v||0)/tot)*100 }))\n : shown.map(s=>s.values)\n const bands = stacked && shown.length ? stack(norm) : null\n const ext = pct ? [0,100] : (stacked ? extent([(bands[bands.length-1]||[]).map(b=>b[1])]) : extent(norm))\n const { ticks, lo, hi } = niceTicks(ext[0], ext[1], 4)\n const labelPad = horiz ? Math.min(140, Math.max(...props.categories.map(c=>String(c).length))*6.4+12) : 44\n const pad = horiz ? { l: props.axes?labelPad:4, r:12, t:6, b: props.axes?22:4 }\n : { l: props.axes?44:4, r: props.overlay?40:8, t:10, b: props.axes?24:4 }\n const sv = horiz ? scaleLin(lo,hi,pad.l,props.width-pad.r) : scaleLin(lo,hi,props.height-pad.b,pad.t)\n const band = (horiz ? props.height-pad.t-pad.b : props.width-pad.l-pad.r)/n\n const mid = i => (horiz?pad.t:pad.l) + band*i + band/2\n const groupW = Math.min(band*0.66, props.mode==='grouped' ? 20*Math.max(1,shown.length) : 30)\n const bw = props.mode==='grouped' ? groupW/Math.max(1,shown.length) : groupW\n const colorOf = s => s.color || SERIES[props.series.indexOf(s)%SERIES.length]\n const zero = sv(Math.max(lo,0))\n const rects = []\n shown.forEach((s,si)=>(pct?norm[si]:s.values).forEach((v,i)=>{\n if (v==null) return\n const fill = props.mode==='diverging' ? (v>=0?'var(--chart-pos)':'var(--chart-neg)') : colorOf(s)\n const outer = !shown.slice(si+1).some(o=>((pct?norm[shown.indexOf(o)][i]:o.values[i])||0)>0)\n let x,y,w,h,side\n if (stacked) {\n const a = sv(bands[si][i][0]), b = sv(bands[si][i][1])\n if (horiz) { x=Math.min(a,b); w=Math.max(1,Math.abs(b-a)); y=mid(i)-bw/2; h=bw; side=outer?'right':null }\n else { y=Math.min(a,b); h=Math.max(1,Math.abs(a-b)); x=mid(i)-bw/2; w=bw; side=outer?'top':null }\n } else if (horiz) {\n const e = sv(v); x=Math.min(zero,e); w=Math.max(1,Math.abs(e-zero)); y=mid(i)-groupW/2+si*bw+0.5; h=Math.max(1,bw-1); side=v>=0?'right':'left'\n } else {\n const e = sv(v); y=Math.min(zero,e); h=Math.max(1,Math.abs(e-zero)); x=mid(i)-groupW/2+si*bw+0.5; w=Math.max(1,bw-1); side=v>=0?'top':'bottom'\n }\n rects.push({ key:s.name+i, d:barPath(x,y,w,h,props.radius,side), fill, x, y, w, h, s, i,\n text: pct ? norm[si][i].toFixed(1)+'%' : props.yFormat(s.values[i]) })\n }))\n let ov = null\n if (props.overlay && !horiz) {\n const e2 = extent([props.overlay.values]); const t = niceTicks(e2[0],e2[1],4)\n const osy = scaleLin(t.lo,t.hi,props.height-pad.b,pad.t)\n ov = { path: linePath(props.overlay.values.map((v,i)=>[mid(i),osy(v)])), dots: props.overlay.values.map((v,i)=>[mid(i),osy(v)]), ticks:t.ticks, osy, color: props.overlay.color||'var(--chart-total)' }\n }\n return { horiz, shown, pct, stacked, ticks, sv, band, mid, pad, rects, ov, n, colorOf,\n fmtAxis: pct ? (v=>v+'%') : props.yFormat }\n})\nfunction move(e){\n if(!props.interactive) return\n const r = e.currentTarget.getBoundingClientRect()\n const off = g.value.horiz ? e.clientY-r.top-g.value.pad.t : e.clientX-r.left-g.value.pad.l\n const i = Math.floor(off/g.value.band)\n hover.value = i>=0 && i<g.value.n ? i : null\n}\nfunction toggle(name){\n if (hidden.value.includes(name)) hidden.value = hidden.value.filter(x=>x!==name)\n else if (hidden.value.length+1 < props.series.length) hidden.value = [...hidden.value, name]\n}\nfunction rowVal(s){\n const i = hover.value\n return g.value.pct ? (g.value.rects.find(r=>r.s===s && r.i===i)||{}).text : props.yFormat(s.values[i])\n}\n</script>\n<template>\n <div :style=\"{position:'relative',display:'inline-block',lineHeight:0,...style}\">\n <svg :width=\"width\" :height=\"height\" @mousemove=\"move\" @mouseleave=\"hover=null\" role=\"img\">\n <template v-if=\"axes\">\n <g v-for=\"t in g.ticks\" :key=\"t\">\n <line v-if=\"g.horiz\" :x1=\"g.sv(t)\" :x2=\"g.sv(t)\" :y1=\"g.pad.t\" :y2=\"height-g.pad.b\" :stroke=\"t===0?'var(--chart-axis)':'var(--chart-grid)'\" stroke-width=\"1\"/>\n <line v-else :x1=\"g.pad.l\" :x2=\"width-g.pad.r\" :y1=\"g.sv(t)\" :y2=\"g.sv(t)\" :stroke=\"t===0?'var(--chart-axis)':'var(--chart-grid)'\" stroke-width=\"1\"/>\n <text v-if=\"g.horiz\" :x=\"g.sv(t)\" :y=\"height-g.pad.b+15\" text-anchor=\"middle\" style=\"font-family:var(--font-mono);font-size:10px;fill:var(--chart-label)\">{{ g.fmtAxis(t) }}</text>\n <text v-else :x=\"g.pad.l-8\" :y=\"g.sv(t)+3\" text-anchor=\"end\" style=\"font-family:var(--font-mono);font-size:10px;fill:var(--chart-label)\">{{ g.fmtAxis(t) }}</text>\n </g>\n <text v-for=\"(c,i) in categories\" :key=\"'c'+i\"\n :x=\"g.horiz ? g.pad.l-8 : g.mid(i)\" :y=\"g.horiz ? g.mid(i)+3.5 : height-g.pad.b+16\"\n :text-anchor=\"g.horiz ? 'end' : 'middle'\"\n :style=\"{fontFamily:'var(--font-sans)',fontSize:g.horiz?'10.5px':'10px',fill:hover===i?'var(--text-heading)':'var(--chart-label)'}\">{{ c }}</text>\n </template>\n <rect v-if=\"hover!=null && g.horiz\" :x=\"g.pad.l\" :y=\"g.pad.t+g.band*hover\" :width=\"width-g.pad.l-g.pad.r\" :height=\"g.band\" fill=\"var(--bg-hover)\"/>\n <rect v-else-if=\"hover!=null\" :x=\"g.pad.l+g.band*hover\" :y=\"g.pad.t\" :width=\"g.band\" :height=\"height-g.pad.t-g.pad.b\" fill=\"var(--bg-hover)\"/>\n <path v-for=\"r in g.rects\" :key=\"r.key\" :d=\"r.d\" :fill=\"r.fill\" :opacity=\"hover==null||hover===r.i?1:0.55\" style=\"transition:opacity var(--dur-fast) var(--ease-out)\"/>\n <template v-if=\"showValues\">\n <text v-for=\"r in g.rects.filter(r=>(g.horiz?r.w:r.h)>=22)\" :key=\"'v'+r.key\"\n :x=\"g.horiz ? r.x+r.w-5 : r.x+r.w/2\" :y=\"g.horiz ? r.y+r.h/2+3.5 : r.y+12\"\n :text-anchor=\"g.horiz ? 'end' : 'middle'\" style=\"font-family:var(--font-mono);font-size:10px;fill:var(--on-accent)\">{{ r.text }}</text>\n </template>\n <template v-if=\"g.ov\">\n <path :d=\"g.ov.path\" fill=\"none\" :stroke=\"g.ov.color\" stroke-width=\"1.5\"/>\n <circle v-for=\"(d,i) in g.ov.dots\" :key=\"'o'+i\" :cx=\"d[0]\" :cy=\"d[1]\" r=\"2.5\" :fill=\"g.ov.color\"/>\n <text v-if=\"axes\" v-for=\"t in g.ov.ticks\" :key=\"'ot'+t\" :x=\"width-g.pad.r+6\" :y=\"g.ov.osy(t)+3\" style=\"font-family:var(--font-mono);font-size:10px;fill:var(--chart-label)\">{{ overlayFormat(t) }}</text>\n </template>\n </svg>\n <div v-if=\"hover!=null && interactive\" :style=\"{position:'absolute',left:(g.horiz ? Math.min(width-20, g.pad.l+(width-g.pad.l-g.pad.r)*0.5) : g.mid(hover))+'px',top:(g.horiz ? g.pad.t+g.band*hover+g.band/2 : height/2-10)+'px',transform:(g.horiz || g.mid(hover)>width*0.62)?'translate(calc(-100% - 10px),-50%)':'translate(10px,-50%)',pointerEvents:'none',zIndex:5,background:'var(--bg-surface)',border:'1px solid var(--border-card)',borderRadius:'var(--radius-md)',boxShadow:'var(--shadow-menu)',padding:'7px 10px',lineHeight:'normal',whiteSpace:'nowrap'}\">\n <div style=\"font-family:var(--font-sans);font-size:var(--text-2xs);color:var(--text-muted);padding-bottom:4px\">{{ categories[hover] }}</div>\n <div v-for=\"s in g.shown\" :key=\"s.name\" style=\"display:flex;align-items:center;gap:6px;padding:1px 0\">\n <span :style=\"{width:'7px',height:'7px',borderRadius:'2px',background:mode==='diverging'?(s.values[hover]>=0?'var(--chart-pos)':'var(--chart-neg)'):g.colorOf(s),flexShrink:0}\"/>\n <span style=\"font-family:var(--font-sans);font-size:var(--text-xs);color:var(--text-secondary)\">{{ s.name }}</span>\n <span style=\"font-family:var(--font-mono);font-size:var(--text-xs);color:var(--text-heading);margin-left:auto;padding-left:10px\">{{ rowVal(s) }}</span>\n </div>\n <div v-if=\"overlay\" style=\"display:flex;align-items:center;gap:6px;padding:1px 0\">\n <span :style=\"{width:'7px',height:'7px',borderRadius:'2px',background:overlay.color||'var(--chart-total)'}\"/>\n <span style=\"font-family:var(--font-sans);font-size:var(--text-xs);color:var(--text-secondary)\">{{ overlay.name }}</span>\n <span style=\"font-family:var(--font-mono);font-size:var(--text-xs);color:var(--text-heading);margin-left:auto;padding-left:10px\">{{ overlayFormat(overlay.values[hover]) }}</span>\n </div>\n </div>\n <div v-if=\"legend && (series.length>1 || overlay)\" style=\"display:flex;flex-wrap:wrap;gap:4px 14px;padding-top:8px;line-height:normal\">\n <span v-for=\"(s,i) in series\" :key=\"s.name\" role=\"button\" tabindex=\"0\" @click=\"toggle(s.name)\"\n :style=\"{display:'inline-flex',alignItems:'center',gap:'6px',cursor:'pointer',opacity:hidden.includes(s.name)?0.4:1}\">\n <span :style=\"{width:'8px',height:'8px',borderRadius:'2px',background:s.color||SERIES[i%SERIES.length]}\"/>\n <span :style=\"{fontFamily:'var(--font-sans)',fontSize:'var(--text-xs)',color:'var(--text-secondary)',textDecoration:hidden.includes(s.name)?'line-through':'none'}\">{{ s.name }}</span>\n </span>\n <span v-if=\"overlay\" style=\"display:inline-flex;align-items:center;gap:6px\">\n <span :style=\"{width:'8px',height:'8px',borderRadius:'2px',background:overlay.color||'var(--chart-total)'}\"/>\n <span style=\"font-family:var(--font-sans);font-size:var(--text-xs);color:var(--text-secondary)\">{{ overlay.name }}</span>\n </span>\n </div>\n </div>\n</template>\n","charts/BarList.jsx":"import React from \"react\";\nimport { fmtNum } from \"./chart-geometry.js\";\n\n/**\n * Horizontal labeled bars — top-N rankings, budget consumption, category comparison.\n * rows: [{label, value, color?, delta?}] — delta renders a signed % at the right.\n */\nexport function BarList({ rows = [], max, width = 360, valueFormat = fmtNum, showTrack = true, barHeight = 8, interactive = true, onSelect, style }) {\n const [hover, setHover] = React.useState(null);\n const top = max != null ? max : Math.max(1, ...rows.map((r) => r.value));\n const total = rows.reduce((s, r) => s + r.value, 0) || 1;\n return (\n <div style={{ display: \"flex\", flexDirection: \"column\", gap: 10, width, ...style }}>\n {rows.map((r) => (\n <div key={r.label} onMouseEnter={interactive ? () => setHover(r.label) : undefined} onMouseLeave={interactive ? () => setHover(null) : undefined}\n onClick={onSelect ? () => onSelect(r) : undefined}\n style={{ opacity: hover == null || hover === r.label ? 1 : 0.5, cursor: onSelect ? \"pointer\" : \"default\", transition: \"var(--t-hover)\" }}>\n <div style={{ display: \"flex\", alignItems: \"baseline\", gap: 8, paddingBottom: 4 }}>\n <span style={{ fontFamily: \"var(--font-sans)\", fontSize: \"var(--text-sm)\", color: \"var(--text-body)\", whiteSpace: \"nowrap\", overflow: \"hidden\", textOverflow: \"ellipsis\" }}>{r.label}</span>\n <span style={{ marginLeft: \"auto\", fontFamily: \"var(--font-mono)\", fontSize: \"var(--text-xs)\", color: \"var(--text-heading)\" }}>{valueFormat(r.value)}</span>\n {hover === r.label && r.delta == null ? <span style={{ fontFamily: \"var(--font-mono)\", fontSize: \"var(--text-2xs)\", color: \"var(--text-muted)\", paddingLeft: 6 }}>{((r.value / total) * 100).toFixed(1)}%</span> : null}\n {r.delta != null ? <span style={{ fontFamily: \"var(--font-mono)\", fontSize: \"var(--text-2xs)\", color: r.delta >= 0 ? \"var(--chart-pos)\" : \"var(--chart-neg)\", minWidth: 38, textAlign: \"right\" }}>{(r.delta >= 0 ? \"+\" : \"\") + r.delta.toFixed(1)}%</span> : null}\n </div>\n <div style={{ height: barHeight, borderRadius: \"var(--radius-full)\", background: showTrack ? \"var(--chart-track)\" : \"transparent\", overflow: \"hidden\" }}>\n <div style={{ width: Math.max(1, (r.value / top) * 100) + \"%\", height: \"100%\", borderRadius: \"var(--radius-full)\", background: r.color || \"var(--series-1)\", filter: hover === r.label ? \"brightness(1.08)\" : \"none\", transition: \"width var(--dur) var(--ease-out), filter var(--dur-fast) var(--ease-out)\" }} />\n </div>\n </div>\n ))}\n </div>\n );\n}\n","charts/BarList.vue":"<script setup>\nimport { computed, ref } from 'vue'\nimport { fmtNum } from './chart-geometry.js'\n// Horizontal labeled bars — top-N rankings, budget consumption.\nconst props = defineProps({ rows:{type:Array,default:()=>[]}, max:Number, width:{type:Number,default:360}, barHeight:{type:Number,default:8}, showTrack:{type:Boolean,default:true}, valueFormat:{type:Function,default:fmtNum}, interactive:{type:Boolean,default:true}, style:Object })\nconst top = computed(() => props.max!=null ? props.max : Math.max(1,...props.rows.map(r=>r.value)))\nconst total = computed(() => props.rows.reduce((s,r)=>s+r.value,0)||1)\nconst hover = ref(null)\n</script>\n<template>\n <div :style=\"{display:'flex',flexDirection:'column',gap:'10px',width:width+'px',...style}\">\n <div v-for=\"r in rows\" :key=\"r.label\" @mouseenter=\"interactive && (hover=r.label)\" @mouseleave=\"interactive && (hover=null)\"\n :style=\"{opacity:hover==null||hover===r.label?1:0.5,transition:'var(--t-hover)'}\">\n <div style=\"display:flex;align-items:baseline;gap:8px;padding-bottom:4px\">\n <span style=\"font-family:var(--font-sans);font-size:var(--text-sm);color:var(--text-body);white-space:nowrap;overflow:hidden;text-overflow:ellipsis\">{{ r.label }}</span>\n <span style=\"margin-left:auto;font-family:var(--font-mono);font-size:var(--text-xs);color:var(--text-heading)\">{{ valueFormat(r.value) }}</span>\n <span v-if=\"hover===r.label && r.delta==null\" style=\"font-family:var(--font-mono);font-size:var(--text-2xs);color:var(--text-muted);padding-left:6px\">{{ ((r.value/total)*100).toFixed(1) }}%</span>\n <span v-if=\"r.delta!=null\" :style=\"{fontFamily:'var(--font-mono)',fontSize:'var(--text-2xs)',color:r.delta>=0?'var(--chart-pos)':'var(--chart-neg)',minWidth:'38px',textAlign:'right'}\">{{ (r.delta>=0?'+':'')+r.delta.toFixed(1) }}%</span>\n </div>\n <div :style=\"{height:barHeight+'px',borderRadius:'var(--radius-full)',background:showTrack?'var(--chart-track)':'transparent',overflow:'hidden'}\">\n <div :style=\"{width:Math.max(1,(r.value/top)*100)+'%',height:'100%',borderRadius:'var(--radius-full)',background:r.color||'var(--series-1)',filter:hover===r.label?'brightness(1.08)':'none',transition:'width var(--dur) var(--ease-out), filter var(--dur-fast) var(--ease-out)'}\"/>\n </div>\n </div>\n </div>\n</template>\n","charts/DonutChart.jsx":"import React from \"react\";\nimport { fmtNum, arcPath, polar, seriesColor } from \"./chart-geometry.js\";\nimport { ChartTooltip, wrapStyle } from \"./ChartKit.jsx\";\n\n/**\n * Donut / pie. segments: [{label, value, color?}]. The donut keeps a KPI center\n * (total by default, or a fixed centerValue); pie={true} fills to the middle and\n * relies on the tooltip. Hovering lifts a segment and pins a tooltip at its arc.\n */\nexport function DonutChart({ segments = [], size = 180, thickness = 22, pie = false, centerLabel, centerValue, legend = true, interactive = true, valueFormat = fmtNum, style }) {\n const [hover, setHover] = React.useState(null);\n const total = segments.reduce((s, x) => s + x.value, 0) || 1;\n const cx = size / 2, cy = size / 2;\n const rO = size / 2 - 4, rI = pie ? 0 : rO - thickness;\n let a = 0;\n const arcs = segments.map((s, i) => {\n const a0 = a, a1 = a + (s.value / total) * 360;\n a = a1;\n return { s, i, a0, a1, mid: (a0 + a1) / 2 };\n });\n const colorOf = (s, i) => s.color || seriesColor(i);\n const h = hover != null ? arcs[hover] : null;\n /* A pinned centerValue is a stat, not a readout: swapping it on hover made the tile\n look broken, so the tooltip carries per-segment detail and the center stays put. */\n const swapCenter = centerValue == null;\n const tipAt = h ? polar(cx, cy, pie ? rO * 0.62 : (rO + rI) / 2, h.mid) : null;\n return (\n <div style={{ ...wrapStyle, ...style }}>\n <div style={{ display: \"flex\", alignItems: \"center\", gap: 18 }}>\n <div style={{ position: \"relative\", width: size, height: size, flexShrink: 0 }}>\n <svg width={size} height={size} role=\"img\">\n {arcs.map(({ s, i, a0, a1, mid }) => {\n const lift = interactive && hover === i ? 3 : 0;\n const [lx, ly] = polar(0, 0, lift, mid);\n return (\n <path key={i} d={arcPath(cx + lx, cy + ly, rO, rI, a0 + 0.6, Math.max(a0 + 0.8, a1 - 0.6))}\n fill={colorOf(s, i)} opacity={hover == null || hover === i ? 1 : 0.35}\n onMouseEnter={interactive ? () => setHover(i) : undefined} onMouseLeave={interactive ? () => setHover(null) : undefined}\n style={{ transition: \"opacity var(--dur-fast) var(--ease-out)\", cursor: interactive ? \"pointer\" : \"default\" }} />\n );\n })}\n </svg>\n {!pie ? (\n <div style={{ position: \"absolute\", inset: 0, display: \"flex\", flexDirection: \"column\", alignItems: \"center\", justifyContent: \"center\", pointerEvents: \"none\", lineHeight: \"normal\", gap: 3 }}>\n <span style={{ fontFamily: \"var(--font-sans)\", fontWeight: \"var(--weight-light)\", fontSize: Math.round(size / 6.4), lineHeight: 1, color: \"var(--text-heading)\" }}>\n {h && swapCenter ? valueFormat(h.s.value) : centerValue != null ? centerValue : valueFormat(total)}\n </span>\n <span style={{ fontFamily: \"var(--font-sans)\", fontSize: \"var(--text-2xs)\", lineHeight: 1.2, color: \"var(--text-muted)\", maxWidth: rI * 1.8, textAlign: \"center\", whiteSpace: \"nowrap\", overflow: \"hidden\", textOverflow: \"ellipsis\" }}>\n {h && swapCenter ? h.s.label : centerLabel || \"Total\"}\n </span>\n </div>\n ) : null}\n {h && interactive ? (\n <ChartTooltip x={tipAt[0]} y={tipAt[1]} flip={tipAt[0] > size * 0.55} title={h.s.label}\n rows={[{ label: \"value\", value: valueFormat(h.s.value), color: colorOf(h.s, h.i) }, { label: \"share\", value: ((h.s.value / total) * 100).toFixed(1) + \"%\" }]} />\n ) : null}\n </div>\n {legend ? (\n <div style={{ display: \"flex\", flexDirection: \"column\", gap: 6, lineHeight: \"normal\" }}>\n {segments.map((s, i) => (\n <span key={s.label} onMouseEnter={interactive ? () => setHover(i) : undefined} onMouseLeave={interactive ? () => setHover(null) : undefined}\n style={{ display: \"inline-flex\", alignItems: \"center\", gap: 7, cursor: interactive ? \"default\" : undefined, opacity: hover == null || hover === i ? 1 : 0.45, transition: \"var(--t-hover)\" }}>\n <span style={{ width: 8, height: 8, borderRadius: 2, background: colorOf(s, i), flexShrink: 0 }} />\n <span style={{ fontFamily: \"var(--font-sans)\", fontSize: \"var(--text-xs)\", color: \"var(--text-secondary)\" }}>{s.label}</span>\n <span style={{ fontFamily: \"var(--font-mono)\", fontSize: \"var(--text-xs)\", color: \"var(--text-heading)\", marginLeft: 6 }}>{((s.value / total) * 100).toFixed(0)}%</span>\n </span>\n ))}\n </div>\n ) : null}\n </div>\n </div>\n );\n}\n","charts/DonutChart.vue":"<script setup>\nimport { ref, computed } from 'vue'\nimport { fmtNum, arcPath, polar, SERIES } from './chart-geometry.js'\n// Donut with a KPI center (or pie). Hovering a segment or its legend row lifts it.\nconst props = defineProps({ segments:{type:Array,default:()=>[]}, size:{type:Number,default:180}, thickness:{type:Number,default:22}, pie:Boolean, centerLabel:String, centerValue:[String,Number], legend:{type:Boolean,default:true}, interactive:{type:Boolean,default:true}, valueFormat:{type:Function,default:fmtNum}, style:Object })\nconst hover = ref(null)\nconst total = computed(() => props.segments.reduce((s,x)=>s+x.value,0) || 1)\nconst arcs = computed(() => {\n const cx=props.size/2, cy=props.size/2, rO=props.size/2-4, rI=props.pie?0:rO-props.thickness\n let a=0\n return props.segments.map((s,i)=>{\n const a0=a, a1=a+(s.value/total.value)*360; a=a1\n const mid=(a0+a1)/2, rad=((mid-90)*Math.PI)/180\n const lift = props.interactive && hover.value===i ? 3 : 0\n return { s, i, mid, color: s.color||SERIES[i%SERIES.length],\n d: arcPath(cx+lift*Math.cos(rad), cy+lift*Math.sin(rad), rO, rI, a0+0.6, Math.max(a0+0.8,a1-0.6)) }\n })\n})\nconst rI = computed(() => props.size/2-4-props.thickness)\nconst tipStyle = computed(() => {\n const a = arcs.value[hover.value]\n const r = props.pie ? (props.size/2-4)*0.62 : (props.size/2-4+rI.value)/2\n const p = polar(props.size/2, props.size/2, r, a.mid)\n return { position:'absolute', left:p[0]+'px', top:p[1]+'px',\n transform: p[0] > props.size*0.55 ? 'translate(calc(-100% - 10px),-50%)' : 'translate(10px,-50%)',\n pointerEvents:'none', zIndex:5, background:'var(--bg-surface)', border:'1px solid var(--border-card)',\n borderRadius:'var(--radius-md)', boxShadow:'var(--shadow-menu)', padding:'7px 10px', lineHeight:'normal' }\n})\n</script>\n<template>\n <div :style=\"{display:'inline-block',...style}\">\n <div style=\"display:flex;align-items:center;gap:18px\">\n <div :style=\"{position:'relative',width:size+'px',height:size+'px',flexShrink:0}\">\n <svg :width=\"size\" :height=\"size\" role=\"img\">\n <path v-for=\"a in arcs\" :key=\"a.i\" :d=\"a.d\" :fill=\"a.color\" :opacity=\"hover==null||hover===a.i?1:.35\"\n @mouseenter=\"interactive && (hover=a.i)\" @mouseleave=\"interactive && (hover=null)\"\n :style=\"{transition:'opacity var(--dur-fast) var(--ease-out)',cursor:interactive?'pointer':'default'}\"/>\n </svg>\n <!-- A pinned centerValue is a stat, not a readout: the tooltip carries per-segment detail. -->\n <div v-if=\"!pie\" style=\"position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center;pointer-events:none;line-height:normal;gap:3px\">\n <span :style=\"{fontFamily:'var(--font-sans)',fontWeight:'var(--weight-light)',fontSize:Math.round(size/6.4)+'px',lineHeight:1,color:'var(--text-heading)'}\">\n {{ hover!=null && centerValue==null ? valueFormat(segments[hover].value) : (centerValue!=null ? centerValue : valueFormat(total)) }}</span>\n <span :style=\"{fontFamily:'var(--font-sans)',fontSize:'var(--text-2xs)',lineHeight:1.2,color:'var(--text-muted)',maxWidth:rI*1.8+'px',textAlign:'center',whiteSpace:'nowrap',overflow:'hidden',textOverflow:'ellipsis'}\">\n {{ hover!=null && centerValue==null ? segments[hover].label : (centerLabel||'Total') }}</span>\n </div>\n <div v-if=\"hover!=null && interactive\" :style=\"tipStyle\" >\n <div style=\"font-family:var(--font-sans);font-size:var(--text-2xs);color:var(--text-muted);padding-bottom:3px;white-space:nowrap\">{{ segments[hover].label }}</div>\n <div style=\"font-family:var(--font-mono);font-size:var(--text-xs);color:var(--text-heading);white-space:nowrap\">{{ valueFormat(segments[hover].value) }} · {{ ((segments[hover].value/total)*100).toFixed(1) }}%</div>\n </div>\n </div>\n <div v-if=\"legend\" style=\"display:flex;flex-direction:column;gap:6px\">\n <span v-for=\"a in arcs\" :key=\"a.s.label\" @mouseenter=\"interactive && (hover=a.i)\" @mouseleave=\"interactive && (hover=null)\"\n :style=\"{display:'inline-flex',alignItems:'center',gap:'7px',opacity:hover==null||hover===a.i?1:.45,transition:'var(--t-hover)'}\">\n <span :style=\"{width:'8px',height:'8px',borderRadius:'2px',background:a.color,flexShrink:0}\"/>\n <span style=\"font-family:var(--font-sans);font-size:var(--text-xs);color:var(--text-secondary)\">{{ a.s.label }}</span>\n <span style=\"font-family:var(--font-mono);font-size:var(--text-xs);color:var(--text-heading);margin-left:6px\">{{ ((a.s.value/total)*100).toFixed(0) }}%</span>\n </span>\n </div>\n </div>\n </div>\n</template>\n","charts/ScatterChart.jsx":"import React from \"react\";\nimport { fmtNum, extent, scaleLin, niceTicks, seriesColor } from \"./chart-geometry.js\";\nimport { ChartLegend, ChartTooltip, PlotGrid, wrapStyle } from \"./ChartKit.jsx\";\n\n/**\n * Scatter / bubble. points: [{x, y, r?, label?, series?}] — r in data units scaled to\n * 3–14px bubbles. Hover pins a tooltip to the nearest point.\n */\nexport function ScatterChart({ points = [], width = 560, height = 260, xFormat = fmtNum, yFormat = fmtNum, xLabel, yLabel, axes = true, legend = true, interactive = true, style }) {\n const [hover, setHover] = React.useState(null);\n const names = [...new Set(points.map((p) => p.series).filter(Boolean))];\n const pad = { l: axes ? 44 : 4, r: 10, t: 10, b: axes ? 26 : 4 };\n const [xl, xh] = extent([points.map((p) => p.x)], false);\n const [yl, yh] = extent([points.map((p) => p.y)], false);\n const tx = niceTicks(xl, xh, 5), ty = niceTicks(yl, yh, 4);\n const sx = scaleLin(tx.lo, tx.hi, pad.l + 6, width - pad.r - 6);\n const sy = scaleLin(ty.lo, ty.hi, height - pad.b - 6, pad.t + 6);\n const rs = points.map((p) => p.r != null ? p.r : 1);\n const [rl, rh] = extent([rs], false);\n const sr = (r) => 3 + ((r - rl) / (rh - rl || 1)) * 11;\n const colorOf = (p) => p.color || seriesColor(p.series ? names.indexOf(p.series) : 0);\n const move = (e) => {\n if (!interactive) return;\n const rc = e.currentTarget.getBoundingClientRect();\n const mx = e.clientX - rc.left, my = e.clientY - rc.top;\n let best = null, bd = 24 * 24;\n points.forEach((p, i) => { const d = (sx(p.x) - mx) ** 2 + (sy(p.y) - my) ** 2; if (d < bd) { bd = d; best = i; } });\n setHover(best);\n };\n const h = hover != null ? points[hover] : null;\n return (\n <div style={{ ...wrapStyle, ...style }}>\n <svg width={width} height={height} onMouseMove={move} onMouseLeave={() => setHover(null)} role=\"img\">\n {axes ? <g>\n <PlotGrid w={width} h={height} pad={pad} ticks={ty.ticks} sy={sy} xLabels={[]} sxLabel={() => 0} yFormat={yFormat} />\n {tx.ticks.map((t) => <text key={t} x={sx(t)} y={height - pad.b + 16} textAnchor=\"middle\" style={{ fontFamily: \"var(--font-mono)\", fontSize: 10, fill: \"var(--chart-label)\" }}>{xFormat(t)}</text>)}\n {xLabel ? <text x={width - pad.r} y={height - 2} textAnchor=\"end\" style={{ fontFamily: \"var(--font-sans)\", fontSize: 10, fill: \"var(--chart-label)\" }}>{xLabel}</text> : null}\n {yLabel ? <text x={pad.l} y={pad.t - 1} style={{ fontFamily: \"var(--font-sans)\", fontSize: 10, fill: \"var(--chart-label)\" }}>{yLabel}</text> : null}\n </g> : null}\n {points.map((p, i) => (\n <circle key={i} cx={sx(p.x)} cy={sy(p.y)} r={sr(p.r != null ? p.r : 1)} fill={colorOf(p)}\n opacity={hover == null ? 0.55 : hover === i ? 0.9 : 0.2} stroke={hover === i ? \"var(--bg-surface)\" : \"none\"} strokeWidth=\"1.5\"\n style={{ transition: \"opacity var(--dur-fast) var(--ease-out)\" }} />\n ))}\n </svg>\n {h && interactive ? (\n <ChartTooltip x={sx(h.x)} y={sy(h.y)} flip={sx(h.x) > width * 0.62} title={h.label || h.series}\n rows={[{ label: xLabel || \"x\", value: xFormat(h.x) }, { label: yLabel || \"y\", value: yFormat(h.y) }].concat(h.r != null ? [{ label: \"size\", value: fmtNum(h.r) }] : [])} />\n ) : null}\n {legend && names.length > 1 ? <ChartLegend style={{ paddingTop: 8 }} items={names.map((nm, i) => ({ label: nm, color: seriesColor(i) }))} /> : null}\n </div>\n );\n}\n","charts/ScatterChart.vue":"<script setup>\nimport { ref, computed } from 'vue'\nimport { fmtNum, extent, scaleLin, niceTicks, SERIES } from './chart-geometry.js'\n// Scatter / bubble with nearest-point tooltip.\nconst props = defineProps({ points:{type:Array,default:()=>[]}, width:{type:Number,default:560}, height:{type:Number,default:260}, xFormat:{type:Function,default:fmtNum}, yFormat:{type:Function,default:fmtNum}, xLabel:String, yLabel:String, axes:{type:Boolean,default:true}, legend:{type:Boolean,default:true}, interactive:{type:Boolean,default:true}, style:Object })\nconst hover = ref(null)\nconst g = computed(() => {\n const names = [...new Set(props.points.map(p=>p.series).filter(Boolean))]\n const pad = { l: props.axes?44:4, r:10, t:10, b: props.axes?26:4 }\n const [xl,xh] = extent([props.points.map(p=>p.x)], false)\n const [yl,yh] = extent([props.points.map(p=>p.y)], false)\n const tx = niceTicks(xl,xh,5), ty = niceTicks(yl,yh,4)\n const sx = scaleLin(tx.lo,tx.hi,pad.l+6,props.width-pad.r-6)\n const sy = scaleLin(ty.lo,ty.hi,props.height-pad.b-6,pad.t+6)\n const rs = props.points.map(p=>p.r!=null?p.r:1)\n const [rl,rh] = extent([rs], false)\n return { names, pad, tx, ty, sx, sy,\n dots: props.points.map((p,i)=>({ p, i, cx:sx(p.x), cy:sy(p.y), r: 3+(((p.r!=null?p.r:1)-rl)/(rh-rl||1))*11,\n color: p.color || SERIES[(p.series?names.indexOf(p.series):0)%SERIES.length] })) }\n})\nfunction move(e){\n if (!props.interactive) return\n const rc = e.currentTarget.getBoundingClientRect()\n const mx = e.clientX-rc.left, my = e.clientY-rc.top\n let best=null, bd=576\n g.value.dots.forEach(d=>{ const dd=(d.cx-mx)**2+(d.cy-my)**2; if(dd<bd){bd=dd;best=d.i} })\n hover.value = best\n}\n</script>\n<template>\n <div :style=\"{position:'relative',display:'inline-block',lineHeight:0,...style}\">\n <svg :width=\"width\" :height=\"height\" @mousemove=\"move\" @mouseleave=\"hover=null\" role=\"img\">\n <template v-if=\"axes\">\n <g v-for=\"t in g.ty.ticks\" :key=\"t\">\n <line :x1=\"g.pad.l\" :x2=\"width-g.pad.r\" :y1=\"g.sy(t)\" :y2=\"g.sy(t)\" :stroke=\"t===0?'var(--chart-axis)':'var(--chart-grid)'\" stroke-width=\"1\"/>\n <text :x=\"g.pad.l-8\" :y=\"g.sy(t)+3\" text-anchor=\"end\" style=\"font-family:var(--font-mono);font-size:10px;fill:var(--chart-label)\">{{ yFormat(t) }}</text>\n </g>\n <text v-for=\"t in g.tx.ticks\" :key=\"'x'+t\" :x=\"g.sx(t)\" :y=\"height-g.pad.b+16\" text-anchor=\"middle\" style=\"font-family:var(--font-mono);font-size:10px;fill:var(--chart-label)\">{{ xFormat(t) }}</text>\n <text v-if=\"xLabel\" :x=\"width-g.pad.r\" :y=\"height-2\" text-anchor=\"end\" style=\"font-family:var(--font-sans);font-size:10px;fill:var(--chart-label)\">{{ xLabel }}</text>\n <text v-if=\"yLabel\" :x=\"g.pad.l\" :y=\"g.pad.t-1\" style=\"font-family:var(--font-sans);font-size:10px;fill:var(--chart-label)\">{{ yLabel }}</text>\n </template>\n <circle v-for=\"d in g.dots\" :key=\"d.i\" :cx=\"d.cx\" :cy=\"d.cy\" :r=\"d.r\" :fill=\"d.color\"\n :opacity=\"hover==null?.55:hover===d.i?.9:.2\" :stroke=\"hover===d.i?'var(--bg-surface)':'none'\" stroke-width=\"1.5\"\n style=\"transition:opacity var(--dur-fast) var(--ease-out)\"/>\n </svg>\n <div v-if=\"hover!=null && interactive\" :style=\"{position:'absolute',left:g.dots[hover].cx+'px',top:g.dots[hover].cy+'px',transform:g.dots[hover].cx>width*0.62?'translate(calc(-100% - 10px),-50%)':'translate(10px,-50%)',pointerEvents:'none',zIndex:5,background:'var(--bg-surface)',border:'1px solid var(--border-card)',borderRadius:'var(--radius-md)',boxShadow:'var(--shadow-menu)',padding:'7px 10px',lineHeight:'normal'}\">\n <div style=\"font-family:var(--font-sans);font-size:var(--text-2xs);color:var(--text-muted);padding-bottom:4px;white-space:nowrap\">{{ points[hover].label || points[hover].series }}</div>\n <div style=\"font-family:var(--font-mono);font-size:var(--text-xs);color:var(--text-heading);white-space:nowrap\">{{ (xLabel||'x')+' '+xFormat(points[hover].x) }} · {{ (yLabel||'y')+' '+yFormat(points[hover].y) }}</div>\n </div>\n <div v-if=\"legend && g.names.length>1\" style=\"display:flex;gap:14px;padding-top:8px;line-height:normal\">\n <span v-for=\"(nm,i) in g.names\" :key=\"nm\" style=\"display:inline-flex;align-items:center;gap:6px\">\n <span :style=\"{width:'8px',height:'8px',borderRadius:'2px',background:SERIES[i%SERIES.length]}\"/>\n <span style=\"font-family:var(--font-sans);font-size:var(--text-xs);color:var(--text-secondary)\">{{ nm }}</span>\n </span>\n </div>\n </div>\n</template>\n","charts/HeatmapChart.jsx":"import React from \"react\";\nimport { fmtNum, extent, heatStep } from \"./chart-geometry.js\";\nimport { ChartTooltip, wrapStyle } from \"./ChartKit.jsx\";\n\nconst HEAT = [\"var(--chart-heat-1)\",\"var(--chart-heat-2)\",\"var(--chart-heat-3)\",\"var(--chart-heat-4)\",\"var(--chart-heat-5)\"];\n/**\n * Matrix heatmap — entity × month, hour × weekday. values[row][col].\n * scale \"blue\" (sequential) or \"diverging\" (negative red / positive green, for variance).\n * showValues prints the number in each cell (md+ sizes).\n */\nexport function HeatmapChart({ xLabels = [], yLabels = [], values = [], width = 560, cellHeight = 26, gap = 2, scale = \"blue\", showValues = false, valueFormat = fmtNum, interactive = true, style }) {\n const [hover, setHover] = React.useState(null);\n const labelW = 86;\n const cols = xLabels.length, rows = yLabels.length;\n const cw = (width - labelW) / cols - gap;\n const [lo, hi] = extent(values, false);\n const amax = Math.max(Math.abs(lo), Math.abs(hi)) || 1;\n const cellFill = (v) => {\n if (v == null) return \"var(--chart-track)\";\n if (scale === \"diverging\") {\n const t = Math.min(1, Math.abs(v) / amax);\n const a = (0.15 + t * 0.85).toFixed(2);\n return v >= 0 ? `color-mix(in srgb, var(--chart-pos) ${Math.round(a * 100)}%, var(--bg-surface))` : `color-mix(in srgb, var(--chart-neg) ${Math.round(a * 100)}%, var(--bg-surface))`;\n }\n return HEAT[heatStep(v, lo, hi)];\n };\n const textOn = (v) => {\n if (v == null) return \"var(--text-muted)\";\n if (scale === \"diverging\") return Math.abs(v) / amax > 0.55 ? \"var(--on-accent)\" : \"var(--text-heading)\";\n return heatStep(v, lo, hi) >= 3 ? \"var(--chart-heat-ink)\" : \"var(--text-heading)\";\n };\n const h = hover ? { v: values[hover.r] && values[hover.r][hover.c] } : null;\n return (\n <div style={{ ...wrapStyle, width, ...style }}>\n <div style={{ display: \"grid\", gridTemplateColumns: `${labelW}px repeat(${cols}, 1fr)`, gap: gap + \"px\", lineHeight: \"normal\" }}>\n <span />\n {xLabels.map((x) => <span key={x} style={{ fontFamily: \"var(--font-sans)\", fontSize: 10, color: \"var(--chart-label)\", textAlign: \"center\", paddingBottom: 2 }}>{x}</span>)}\n {yLabels.map((y, r) => (\n <React.Fragment key={y}>\n <span style={{ fontFamily: \"var(--font-sans)\", fontSize: \"var(--text-xs)\", color: \"var(--text-secondary)\", display: \"flex\", alignItems: \"center\", whiteSpace: \"nowrap\", overflow: \"hidden\", textOverflow: \"ellipsis\", paddingRight: 6 }}>{y}</span>\n {xLabels.map((x, c) => {\n const v = values[r] ? values[r][c] : null;\n const on = hover && hover.r === r && hover.c === c;\n return (\n <span key={c}\n onMouseEnter={interactive ? () => setHover({ r, c }) : undefined} onMouseLeave={interactive ? () => setHover(null) : undefined}\n style={{ height: cellHeight, borderRadius: 4, background: cellFill(v), display: \"flex\", alignItems: \"center\", justifyContent: \"center\",\n outline: on ? \"2px solid var(--focus-border)\" : \"none\", outlineOffset: -1, transition: \"var(--t-hover)\", cursor: interactive ? \"default\" : undefined }}>\n {showValues && v != null ? <span style={{ fontFamily: \"var(--font-mono)\", fontSize: 10, color: textOn(v) }}>{valueFormat(v)}</span> : null}\n </span>\n );\n })}\n </React.Fragment>\n ))}\n </div>\n {hover && interactive ? (\n <ChartTooltip x={labelW + (hover.c + 0.5) * (cw + gap)} y={20 + (hover.r + 0.5) * (cellHeight + gap)} flip={hover.c > cols * 0.62}\n title={yLabels[hover.r] + \" · \" + xLabels[hover.c]} rows={[{ label: \"value\", value: valueFormat(h.v) }]} />\n ) : null}\n </div>\n );\n}\n","charts/HeatmapChart.vue":"<script setup>\nimport { ref, computed } from 'vue'\nimport { fmtNum, extent, heatStep } from './chart-geometry.js'\n// Matrix heatmap. scale \"blue\" (sequential) or \"diverging\" (red/green variance).\nconst HEAT = ['var(--chart-heat-1)','var(--chart-heat-2)','var(--chart-heat-3)','var(--chart-heat-4)','var(--chart-heat-5)']\nconst props = defineProps({ xLabels:{type:Array,default:()=>[]}, yLabels:{type:Array,default:()=>[]}, values:{type:Array,default:()=>[]}, width:{type:Number,default:560}, cellHeight:{type:Number,default:26}, gap:{type:Number,default:2}, scale:{type:String,default:'blue'}, showValues:Boolean, valueFormat:{type:Function,default:fmtNum}, interactive:{type:Boolean,default:true}, style:Object })\nconst hover = ref(null)\nconst dom = computed(() => { const [lo,hi] = extent(props.values,false); return { lo, hi, amax: Math.max(Math.abs(lo),Math.abs(hi))||1 } })\nfunction fill(v){\n if (v==null) return 'var(--chart-track)'\n if (props.scale==='diverging'){\n const a = Math.round((0.15+Math.min(1,Math.abs(v)/dom.value.amax)*0.85)*100)\n return (v>=0?`color-mix(in srgb, var(--chart-pos) ${a}%, var(--bg-surface))`:`color-mix(in srgb, var(--chart-neg) ${a}%, var(--bg-surface))`)\n }\n return HEAT[heatStep(v, dom.value.lo, dom.value.hi)]\n}\nfunction ink(v){\n if (v==null) return 'var(--text-muted)'\n if (props.scale==='diverging') return Math.abs(v)/dom.value.amax>0.55 ? 'var(--on-accent)' : 'var(--text-heading)'\n return heatStep(v, dom.value.lo, dom.value.hi)>=3 ? 'var(--chart-heat-ink)' : 'var(--text-heading)'\n}\n</script>\n<template>\n <div :style=\"{width:width+'px',...style}\">\n <div :style=\"{display:'grid',gridTemplateColumns:'86px repeat('+xLabels.length+', 1fr)',gap:gap+'px'}\">\n <span/>\n <span v-for=\"x in xLabels\" :key=\"x\" style=\"font-family:var(--font-sans);font-size:10px;color:var(--chart-label);text-align:center;padding-bottom:2px\">{{ x }}</span>\n <template v-for=\"(y,r) in yLabels\" :key=\"y\">\n <span style=\"font-family:var(--font-sans);font-size:var(--text-xs);color:var(--text-secondary);display:flex;align-items:center;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;padding-right:6px\">{{ y }}</span>\n <span v-for=\"(x,c) in xLabels\" :key=\"c\" :title=\"y+' · '+x+(values[r]&&values[r][c]!=null?' = '+valueFormat(values[r][c]):'')\"\n @mouseenter=\"interactive && (hover={r,c})\" @mouseleave=\"interactive && (hover=null)\"\n :style=\"{height:cellHeight+'px',borderRadius:'4px',background:fill(values[r]?values[r][c]:null),display:'flex',alignItems:'center',justifyContent:'center',outline:hover&&hover.r===r&&hover.c===c?'2px solid var(--focus-border)':'none',outlineOffset:'-1px',transition:'var(--t-hover)'}\">\n <span v-if=\"showValues && values[r] && values[r][c]!=null\" :style=\"{fontFamily:'var(--font-mono)',fontSize:'10px',color:ink(values[r][c])}\">{{ valueFormat(values[r][c]) }}</span>\n </span>\n </template>\n </div>\n </div>\n</template>\n","charts/WaterfallChart.jsx":"import React from \"react\";\nimport { fmtNum, waterfall, niceTicks, scaleLin } from \"./chart-geometry.js\";\nimport { ChartTooltip, PlotGrid, wrapStyle } from \"./ChartKit.jsx\";\n\n/**\n * Waterfall bridge — plan → actual, revenue → net income. steps: [{label, value}] plus\n * {label, total:true} rows that re-baseline (subtotals/result). Positive --chart-pos,\n * negative --chart-neg, totals --chart-total; dashed connectors carry the running sum.\n * Hovering a bar dims the rest and reports the delta and the running total.\n */\nexport function WaterfallChart({ steps = [], width = 560, height = 260, axes = true, valueFormat = fmtNum, showValues = true, interactive = true, style }) {\n const [hover, setHover] = React.useState(null);\n const bars = waterfall(steps);\n const pad = { l: axes ? 44 : 4, r: 8, t: showValues ? 22 : 10, b: 40 };\n const [lo0, hi0] = [Math.min(0, ...bars.map((b) => b.y0)), Math.max(0, ...bars.map((b) => b.y1))];\n const { ticks, lo, hi } = niceTicks(lo0, hi0, 4);\n const sy = scaleLin(lo, hi, height - pad.b, pad.t);\n const slot = (width - pad.l - pad.r) / bars.length;\n const bw = Math.min(slot * 0.58, 40);\n const fill = { pos: \"var(--chart-pos)\", neg: \"var(--chart-neg)\", total: \"var(--chart-total)\" };\n const move = (e) => {\n if (!interactive) return;\n const r = e.currentTarget.getBoundingClientRect();\n const i = Math.floor((e.clientX - r.left - pad.l) / slot);\n setHover(i >= 0 && i < bars.length ? i : null);\n };\n const h = hover != null ? bars[hover] : null;\n return (\n <div style={{ ...wrapStyle, ...style }}>\n <svg width={width} height={height} onMouseMove={move} onMouseLeave={() => setHover(null)} role=\"img\">\n {axes ? <PlotGrid w={width} h={height} pad={pad} ticks={ticks} sy={sy} xLabels={[]} sxLabel={() => 0} yFormat={valueFormat} /> : null}\n {hover != null ? <rect x={pad.l + slot * hover} y={pad.t} width={slot} height={height - pad.t - pad.b} fill=\"var(--bg-hover)\" /> : null}\n {bars.map((b, i) => {\n const x = pad.l + slot * i + slot / 2;\n const y = sy(b.y1), hgt = Math.max(1.5, sy(b.y0) - y);\n const runEnd = b.kind === \"total\" ? b.y1 : b.to;\n return (\n <g key={i} opacity={hover == null || hover === i ? 1 : 0.45} style={{ transition: \"opacity var(--dur-fast) var(--ease-out)\" }}>\n <rect x={x - bw / 2} y={y} width={bw} height={hgt} rx=\"2\" fill={fill[b.kind]} />\n {i < bars.length - 1 ? <line x1={x + bw / 2} x2={pad.l + slot * (i + 1) + slot / 2 - bw / 2} y1={sy(runEnd)} y2={sy(runEnd)} stroke=\"var(--chart-guide)\" strokeWidth=\"1\" strokeDasharray=\"2 3\" /> : null}\n {showValues ? <text x={x} y={y - 5} textAnchor=\"middle\" style={{ fontFamily: \"var(--font-mono)\", fontSize: 10, fill: b.kind === \"neg\" ? \"var(--chart-neg)\" : \"var(--text-heading)\" }}>{(b.kind === \"pos\" ? \"+\" : \"\") + valueFormat(b.value)}</text> : null}\n <text x={x} y={height - pad.b + 15} textAnchor=\"middle\" style={{ fontFamily: \"var(--font-sans)\", fontSize: 10, fill: \"var(--chart-label)\" }}>\n {String(b.label).split(\"\\n\").map((ln, li) => <tspan key={li} x={x} dy={li ? 11 : 0}>{ln}</tspan>)}\n </text>\n </g>\n );\n })}\n </svg>\n {h && interactive ? (\n <ChartTooltip x={pad.l + slot * hover + slot / 2} y={height / 2 - 10} flip={pad.l + slot * hover > width * 0.62}\n title={String(h.label).replace(\"\\n\", \" \")}\n rows={h.kind === \"total\"\n ? [{ label: \"running total\", value: valueFormat(h.value), color: fill.total }]\n : [{ label: h.kind === \"pos\" ? \"gain\" : \"loss\", value: (h.kind === \"pos\" ? \"+\" : \"\") + valueFormat(h.value), color: fill[h.kind] },\n { label: \"running total\", value: valueFormat(h.to) }]} />\n ) : null}\n </div>\n );\n}\n","charts/WaterfallChart.vue":"<script setup>\nimport { computed, ref } from 'vue'\nimport { fmtNum, waterfall, niceTicks, scaleLin } from './chart-geometry.js'\n// Finance bridge: green gains, red losses, neutral totals, dashed running-sum connectors.\nconst props = defineProps({ steps:{type:Array,default:()=>[]}, width:{type:Number,default:560}, height:{type:Number,default:260}, axes:{type:Boolean,default:true}, showValues:{type:Boolean,default:true}, valueFormat:{type:Function,default:fmtNum}, interactive:{type:Boolean,default:true}, style:Object })\nconst hover = ref(null)\nfunction move(e){ if(!props.interactive) return; const r=e.currentTarget.getBoundingClientRect(); const slot=(props.width-g.value.pad.l-g.value.pad.r)/g.value.out.length; const i=Math.floor((e.clientX-r.left-g.value.pad.l)/slot); hover.value = i>=0 && i<g.value.out.length ? i : null }\nconst FILL = { pos:'var(--chart-pos)', neg:'var(--chart-neg)', total:'var(--chart-total)' }\nconst g = computed(() => {\n const bars = waterfall(props.steps)\n const pad = { l: props.axes?44:4, r:8, t: props.showValues?22:10, b:40 }\n const { ticks, lo, hi } = niceTicks(Math.min(0,...bars.map(b=>b.y0)), Math.max(0,...bars.map(b=>b.y1)), 4)\n const sy = scaleLin(lo, hi, props.height-pad.b, pad.t)\n const slot = (props.width-pad.l-pad.r)/bars.length\n const bw = Math.min(slot*0.58, 40)\n return { pad, ticks, sy, out: bars.map((b,i)=>{\n const x = pad.l + slot*i + slot/2\n return { ...b, x, bw, y: sy(b.y1), h: Math.max(1.5, sy(b.y0)-sy(b.y1)), fill: FILL[b.kind],\n run: sy(b.kind==='total'?b.y1:b.to), nextX: pad.l + slot*(i+1) + slot/2 - bw/2, last: i===bars.length-1,\n lines: String(b.label).split('\\\\n') } }) }\n})\n</script>\n<template>\n <div :style=\"{position:'relative',display:'inline-block',lineHeight:0,...style}\">\n <svg :width=\"width\" :height=\"height\" @mousemove=\"move\" @mouseleave=\"hover=null\" role=\"img\">\n <template v-if=\"axes\">\n <g v-for=\"t in g.ticks\" :key=\"t\">\n <line :x1=\"g.pad.l\" :x2=\"width-g.pad.r\" :y1=\"g.sy(t)\" :y2=\"g.sy(t)\" :stroke=\"t===0?'var(--chart-axis)':'var(--chart-grid)'\" stroke-width=\"1\"/>\n <text :x=\"g.pad.l-8\" :y=\"g.sy(t)+3\" text-anchor=\"end\" style=\"font-family:var(--font-mono);font-size:10px;fill:var(--chart-label)\">{{ valueFormat(t) }}</text>\n </g>\n </template>\n <g v-for=\"(b,i) in g.out\" :key=\"i\" :opacity=\"hover==null||hover===i?1:0.45\" style=\"transition:opacity var(--dur-fast) var(--ease-out)\">\n <rect :x=\"b.x-b.bw/2\" :y=\"b.y\" :width=\"b.bw\" :height=\"b.h\" rx=\"2\" :fill=\"b.fill\"/>\n <line v-if=\"!b.last\" :x1=\"b.x+b.bw/2\" :x2=\"b.nextX\" :y1=\"b.run\" :y2=\"b.run\" stroke=\"var(--chart-guide)\" stroke-width=\"1\" stroke-dasharray=\"2 3\"/>\n <text v-if=\"showValues\" :x=\"b.x\" :y=\"b.y-5\" text-anchor=\"middle\" :style=\"{fontFamily:'var(--font-mono)',fontSize:'10px',fill:b.kind==='neg'?'var(--chart-neg)':'var(--text-heading)'}\">{{ (b.kind==='pos'?'+':'')+valueFormat(b.value) }}</text>\n <text :x=\"b.x\" :y=\"height-g.pad.b+15\" text-anchor=\"middle\" style=\"font-family:var(--font-sans);font-size:10px;fill:var(--chart-label)\">\n <tspan v-for=\"(ln,li) in b.lines\" :key=\"li\" :x=\"b.x\" :dy=\"li?11:0\">{{ ln }}</tspan>\n </text>\n </g>\n </svg>\n <div v-if=\"hover!=null && interactive\" :style=\"{position:'absolute',left:g.out[hover].x+'px',top:(height/2-10)+'px',transform:g.out[hover].x>width*0.62?'translate(calc(-100% - 10px),-50%)':'translate(10px,-50%)',pointerEvents:'none',zIndex:5,background:'var(--bg-surface)',border:'1px solid var(--border-card)',borderRadius:'var(--radius-md)',boxShadow:'var(--shadow-menu)',padding:'7px 10px',lineHeight:'normal',whiteSpace:'nowrap'}\">\n <div style=\"font-family:var(--font-sans);font-size:var(--text-2xs);color:var(--text-muted);padding-bottom:3px\">{{ String(g.out[hover].label).replace('\\n',' ') }}</div>\n <div style=\"font-family:var(--font-mono);font-size:var(--text-xs);color:var(--text-heading)\">{{ g.out[hover].kind==='total' ? valueFormat(g.out[hover].value) : ((g.out[hover].kind==='pos'?'+':'')+valueFormat(g.out[hover].value)+' → '+valueFormat(g.out[hover].to)) }}</div>\n </div>\n </div>\n</template>\n","charts/BulletChart.jsx":"import React from \"react\";\nimport { fmtNum } from \"./chart-geometry.js\";\n\n/**\n * Bullet rows — actual vs. target on a qualitative track. rows:\n * [{label, value, target, max?, ranges?: [poor, ok] }] — ranges tint the track;\n * the bar is --series-1 (or --chart-neg when value misses target with tone=\"auto\").\n */\nexport function BulletChart({ rows = [], width = 360, valueFormat = fmtNum, tone = \"default\", interactive = true, style }) {\n const [hover, setHover] = React.useState(null);\n return (\n <div style={{ display: \"flex\", flexDirection: \"column\", gap: 14, width, ...style }}>\n {rows.map((r) => {\n const max = r.max != null ? r.max : Math.max(r.value, r.target) * 1.15;\n const pc = (v) => Math.min(100, (v / max) * 100) + \"%\";\n const missed = r.value < r.target;\n const bar = tone === \"auto\" ? (missed ? \"var(--chart-neg)\" : \"var(--chart-pos)\") : \"var(--series-1)\";\n const on = hover === r.label;\n return (\n <div key={r.label} onMouseEnter={interactive ? () => setHover(r.label) : undefined} onMouseLeave={interactive ? () => setHover(null) : undefined}\n style={{ opacity: hover == null || on ? 1 : 0.55, transition: \"var(--t-hover)\" }}>\n <div style={{ display: \"flex\", alignItems: \"baseline\", paddingBottom: 4 }}>\n <span style={{ fontFamily: \"var(--font-sans)\", fontSize: \"var(--text-sm)\", color: \"var(--text-body)\" }}>{r.label}</span>\n <span style={{ marginLeft: \"auto\", fontFamily: \"var(--font-mono)\", fontSize: \"var(--text-xs)\", color: \"var(--text-heading)\" }}>{valueFormat(r.value)}</span>\n <span style={{ fontFamily: \"var(--font-mono)\", fontSize: \"var(--text-2xs)\", color: \"var(--text-muted)\", paddingLeft: 6 }}>/ {valueFormat(r.target)}</span>\n {on ? <span style={{ fontFamily: \"var(--font-mono)\", fontSize: \"var(--text-2xs)\", color: missed ? \"var(--chart-neg)\" : \"var(--chart-pos)\", paddingLeft: 8 }}>{(missed ? \"\" : \"+\") + (((r.value - r.target) / (r.target || 1)) * 100).toFixed(1)}%</span> : null}\n </div>\n <div style={{ position: \"relative\", height: 12, borderRadius: 3, background: \"var(--chart-track)\", overflow: \"hidden\" }}>\n {r.ranges ? <>\n <div style={{ position: \"absolute\", left: 0, top: 0, bottom: 0, width: pc(r.ranges[0]), background: \"var(--alpha-heavy, rgba(0,0,0,.12))\", opacity: 0.5 }} />\n <div style={{ position: \"absolute\", left: 0, top: 0, bottom: 0, width: pc(r.ranges[1]), background: \"var(--alpha-heavy, rgba(0,0,0,.12))\", opacity: 0.25 }} />\n </> : null}\n <div style={{ position: \"absolute\", left: 0, top: 3, bottom: 3, width: pc(r.value), borderRadius: 2, background: bar, transition: \"width var(--dur) var(--ease-out)\" }} />\n <div style={{ position: \"absolute\", left: pc(r.target), top: 0, bottom: 0, width: 2, background: \"var(--chart-total)\" }} />\n </div>\n </div>\n );\n })}\n </div>\n );\n}\n","charts/BulletChart.vue":"<script setup>\nimport { ref } from 'vue'\nimport { fmtNum } from './chart-geometry.js'\n// Actual vs. target rows with a target tick. tone \"auto\" colors misses red.\nconst props = defineProps({ rows:{type:Array,default:()=>[]}, width:{type:Number,default:360}, tone:{type:String,default:'default'}, valueFormat:{type:Function,default:fmtNum}, interactive:{type:Boolean,default:true}, style:Object })\nconst hover = ref(null)\nfunction geom(r){\n const max = r.max!=null ? r.max : Math.max(r.value,r.target)*1.15\n const pc = v => Math.min(100,(v/max)*100)+'%'\n return { pc, bar: props.tone==='auto' ? (r.value<r.target?'var(--chart-neg)':'var(--chart-pos)') : 'var(--series-1)' }\n}\n</script>\n<template>\n <div :style=\"{display:'flex',flexDirection:'column',gap:'14px',width:width+'px',...style}\">\n <div v-for=\"r in rows\" :key=\"r.label\" @mouseenter=\"interactive && (hover=r.label)\" @mouseleave=\"interactive && (hover=null)\"\n :style=\"{opacity:hover==null||hover===r.label?1:0.55,transition:'var(--t-hover)'}\">\n <div style=\"display:flex;align-items:baseline;padding-bottom:4px\">\n <span style=\"font-family:var(--font-sans);font-size:var(--text-sm);color:var(--text-body)\">{{ r.label }}</span>\n <span style=\"margin-left:auto;font-family:var(--font-mono);font-size:var(--text-xs);color:var(--text-heading)\">{{ valueFormat(r.value) }}</span>\n <span style=\"font-family:var(--font-mono);font-size:var(--text-2xs);color:var(--text-muted);padding-left:6px\">/ {{ valueFormat(r.target) }}</span>\n <span v-if=\"hover===r.label\" :style=\"{fontFamily:'var(--font-mono)',fontSize:'var(--text-2xs)',paddingLeft:'8px',color:r.value<r.target?'var(--chart-neg)':'var(--chart-pos)'}\">{{ (r.value<r.target?'':'+')+(((r.value-r.target)/(r.target||1))*100).toFixed(1) }}%</span>\n </div>\n <div style=\"position:relative;height:12px;border-radius:3px;background:var(--chart-track);overflow:hidden\">\n <template v-if=\"r.ranges\">\n <div :style=\"{position:'absolute',left:0,top:0,bottom:0,width:geom(r).pc(r.ranges[0]),background:'rgba(0,0,0,.12)',opacity:.5}\"/>\n <div :style=\"{position:'absolute',left:0,top:0,bottom:0,width:geom(r).pc(r.ranges[1]),background:'rgba(0,0,0,.12)',opacity:.25}\"/>\n </template>\n <div :style=\"{position:'absolute',left:0,top:'3px',bottom:'3px',width:geom(r).pc(r.value),borderRadius:'2px',background:geom(r).bar,transition:'width var(--dur) var(--ease-out)'}\"/>\n <div :style=\"{position:'absolute',left:geom(r).pc(r.target),top:0,bottom:0,width:'2px',background:'var(--chart-total)'}\"/>\n </div>\n </div>\n </div>\n</template>\n","charts/GaugeChart.jsx":"import React from \"react\";\nimport { fmtNum, arcPath } from \"./chart-geometry.js\";\n\n/**\n * Semi-circular gauge KPI — utilization, data quality, SLA. Flat arc fill over a track\n * (no needle). bands: [{upTo, color}] recolor the fill by threshold; big light numeral.\n */\nexport function GaugeChart({ value = 0, max = 100, size = 180, thickness = 14, label, valueFormat = (v) => fmtNum(v, { pct: true }), bands, interactive = true, style }) {\n const [hover, setHover] = React.useState(false);\n const rO = size / 2 - 4, rI = rO - thickness;\n const cx = size / 2, cy = rO + 4;\n const H = cy + 18;\n const t = Math.max(0, Math.min(1, value / max));\n const band = bands && bands.find((b) => value <= b.upTo);\n const color = (band && band.color) || \"var(--series-1)\";\n const A0 = -90;\n return (\n <div style={{ position: \"relative\", width: size, height: H, display: \"inline-block\", ...style }}>\n <svg width={size} height={H} role=\"img\">\n <path d={arcPath(cx, cy, rO, rI, A0, 90)} fill=\"var(--chart-track)\" />\n {t > 0 ? <path d={arcPath(cx, cy, rO, rI, A0, A0 + t * 180)} fill={color}\n onMouseEnter={interactive ? () => setHover(true) : undefined} onMouseLeave={interactive ? () => setHover(false) : undefined}\n style={{ filter: hover ? \"brightness(1.1)\" : \"none\", transition: \"filter var(--dur-fast) var(--ease-out)\" }} /> : null}\n <text x={cx - rO + thickness / 2} y={cy + 13} textAnchor=\"middle\" style={{ fontFamily: \"var(--font-mono)\", fontSize: 9, fill: \"var(--chart-label)\" }}>0</text>\n <text x={cx + rO - thickness / 2} y={cy + 13} textAnchor=\"middle\" style={{ fontFamily: \"var(--font-mono)\", fontSize: 9, fill: \"var(--chart-label)\" }}>{fmtNum(max)}</text>\n </svg>\n <div style={{ position: \"absolute\", left: 0, right: 0, top: cy - size / 7, textAlign: \"center\", pointerEvents: \"none\" }}>\n <div style={{ fontFamily: \"var(--font-sans)\", fontWeight: \"var(--weight-light)\", fontSize: size / 7, lineHeight: 1, color: \"var(--text-heading)\" }}>{valueFormat(value)}</div>\n {label ? <div style={{ fontFamily: \"var(--font-sans)\", fontSize: \"var(--text-2xs)\", color: \"var(--text-muted)\", paddingTop: 3 }}>{hover ? Math.round(t * 100) + \"% of \" + fmtNum(max) : label}</div> : null}\n </div>\n </div>\n );\n}\n","charts/GaugeChart.vue":"<script setup>\nimport { computed, ref } from 'vue'\nimport { fmtNum, arcPath } from './chart-geometry.js'\n// Semi-circular KPI gauge — flat arc fill, no needle. bands recolor by threshold.\nconst props = defineProps({ value:{type:Number,default:0}, max:{type:Number,default:100}, size:{type:Number,default:180}, thickness:{type:Number,default:14}, label:String, valueFormat:{type:Function,default:(v)=>fmtNum(v,{pct:true})}, bands:Array, interactive:{type:Boolean,default:true}, style:Object })\nconst hover = ref(false)\nconst g = computed(() => {\n const rO = props.size/2-4, rI = rO-props.thickness, cx = props.size/2, cy = rO+4, H = cy+18\n const t = Math.max(0, Math.min(1, props.value/props.max))\n const band = props.bands && props.bands.find(b=>props.value<=b.upTo)\n return { rO, rI, cx, cy, H, t, color:(band&&band.color)||'var(--series-1)',\n track: arcPath(cx,cy,rO,rI,-90,90), fill: t>0 ? arcPath(cx,cy,rO,rI,-90,-90+t*180) : null }\n})\n</script>\n<template>\n <div :style=\"{position:'relative',width:size+'px',height:g.H+'px',display:'inline-block',...style}\">\n <svg :width=\"size\" :height=\"g.H\" role=\"img\">\n <path :d=\"g.track\" fill=\"var(--chart-track)\"/>\n <path v-if=\"g.fill\" :d=\"g.fill\" :fill=\"g.color\" @mouseenter=\"interactive && (hover=true)\" @mouseleave=\"interactive && (hover=false)\"\n :style=\"{filter:hover?'brightness(1.1)':'none',transition:'filter var(--dur-fast) var(--ease-out)'}\"/>\n <text :x=\"g.cx-g.rO+thickness/2\" :y=\"g.cy+13\" text-anchor=\"middle\" style=\"font-family:var(--font-mono);font-size:9px;fill:var(--chart-label)\">0</text>\n <text :x=\"g.cx+g.rO-thickness/2\" :y=\"g.cy+13\" text-anchor=\"middle\" style=\"font-family:var(--font-mono);font-size:9px;fill:var(--chart-label)\">{{ fmtNum(max) }}</text>\n </svg>\n <div :style=\"{position:'absolute',left:0,right:0,top:(g.cy-size/7)+'px',textAlign:'center',pointerEvents:'none'}\">\n <div :style=\"{fontFamily:'var(--font-sans)',fontWeight:'var(--weight-light)',fontSize:size/7+'px',lineHeight:1,color:'var(--text-heading)'}\">{{ valueFormat(value) }}</div>\n <div v-if=\"label\" style=\"font-family:var(--font-sans);font-size:var(--text-2xs);color:var(--text-muted);padding-top:3px\">{{ hover ? Math.round(g.t*100)+'% of '+max : label }}</div>\n </div>\n </div>\n</template>\n","charts/FunnelChart.jsx":"import React from \"react\";\nimport { fmtNum } from \"./chart-geometry.js\";\n\n/**\n * Conversion funnel — onboarding, approval chains, migration stages.\n * stages: [{label, value}]; each band shows its share of stage 1 and the\n * step-to-step conversion underneath.\n */\nexport function FunnelChart({ stages = [], width = 420, bandHeight = 34, gap = 14, valueFormat = fmtNum, interactive = true, style }) {\n const [hover, setHover] = React.useState(null);\n const top = stages[0] ? stages[0].value : 1;\n return (\n <div style={{ width, ...style }}>\n {stages.map((s, i) => {\n const share = s.value / (top || 1);\n const prev = i > 0 ? s.value / (stages[i - 1].value || 1) : null;\n return (\n <div key={s.label} onMouseEnter={interactive ? () => setHover(i) : undefined} onMouseLeave={interactive ? () => setHover(null) : undefined}\n style={{ paddingBottom: i < stages.length - 1 ? gap : 0, opacity: hover == null || hover === i ? 1 : 0.55, transition: \"var(--t-hover)\" }}>\n <div style={{ display: \"flex\", justifyContent: \"center\" }}>\n <div style={{ width: Math.max(12, share * 100) + \"%\", height: bandHeight, borderRadius: \"var(--radius-sm)\", background: \"var(--series-1)\", opacity: 1 - i * (0.55 / Math.max(1, stages.length - 1)), display: \"flex\", alignItems: \"center\", justifyContent: \"center\", gap: 8, minWidth: 150, transition: \"width var(--dur) var(--ease-out)\" }}>\n <span style={{ fontFamily: \"var(--font-sans)\", fontSize: \"var(--text-xs)\", color: \"var(--on-accent)\", whiteSpace: \"nowrap\" }}>{s.label}</span>\n <span style={{ fontFamily: \"var(--font-mono)\", fontSize: \"var(--text-xs)\", color: \"var(--on-accent)\" }}>{valueFormat(s.value)}</span>\n {hover === i ? <span style={{ fontFamily: \"var(--font-mono)\", fontSize: \"var(--text-2xs)\", color: \"var(--on-accent)\", opacity: 0.8 }}>{((share) * 100).toFixed(0)}% of top</span> : null}\n </div>\n </div>\n {prev != null ? (\n <div style={{ textAlign: \"center\", paddingTop: 3, fontFamily: \"var(--font-mono)\", fontSize: \"var(--text-2xs)\", color: prev < 0.5 ? \"var(--chart-neg)\" : \"var(--text-muted)\" }}>\n ↓ {(prev * 100).toFixed(0)}%\n </div>\n ) : null}\n </div>\n );\n })}\n </div>\n );\n}\n","charts/FunnelChart.vue":"<script setup>\nimport { computed, ref } from 'vue'\nimport { fmtNum } from './chart-geometry.js'\n// Centered conversion funnel with step-to-step % under each band.\nconst props = defineProps({ stages:{type:Array,default:()=>[]}, width:{type:Number,default:420}, bandHeight:{type:Number,default:34}, gap:{type:Number,default:14}, valueFormat:{type:Function,default:fmtNum}, interactive:{type:Boolean,default:true}, style:Object })\nconst hover = ref(null)\nconst rows = computed(() => {\n const top = props.stages[0] ? props.stages[0].value : 1\n return props.stages.map((s,i)=>({ ...s,\n share: Math.max(12, (s.value/(top||1))*100),\n conv: i>0 ? s.value/(props.stages[i-1].value||1) : null,\n opacity: 1 - i*(0.55/Math.max(1,props.stages.length-1)) }))\n})\n</script>\n<template>\n <div :style=\"{width:width+'px',...style}\">\n <div v-for=\"(s,i) in rows\" :key=\"s.label\" @mouseenter=\"interactive && (hover=i)\" @mouseleave=\"interactive && (hover=null)\"\n :style=\"{paddingBottom:i<rows.length-1?gap+'px':0,opacity:hover==null||hover===i?1:0.55,transition:'var(--t-hover)'}\">\n <div style=\"display:flex;justify-content:center\">\n <div :style=\"{width:s.share+'%',height:bandHeight+'px',borderRadius:'var(--radius-sm)',background:'var(--series-1)',opacity:s.opacity,display:'flex',alignItems:'center',justifyContent:'center',gap:'8px',minWidth:'150px'}\">\n <span style=\"font-family:var(--font-sans);font-size:var(--text-xs);color:var(--on-accent);white-space:nowrap\">{{ s.label }}</span>\n <span style=\"font-family:var(--font-mono);font-size:var(--text-xs);color:var(--on-accent)\">{{ valueFormat(s.value) }}</span>\n <span v-if=\"hover===i\" style=\"font-family:var(--font-mono);font-size:var(--text-2xs);color:var(--on-accent);opacity:.8\">{{ s.share.toFixed(0) }}% of top</span>\n </div>\n </div>\n <div v-if=\"s.conv!=null\" :style=\"{textAlign:'center',paddingTop:'3px',fontFamily:'var(--font-mono)',fontSize:'var(--text-2xs)',color:s.conv<0.5?'var(--chart-neg)':'var(--text-muted)'}\">↓ {{ (s.conv*100).toFixed(0) }}%</div>\n </div>\n </div>\n</template>\n","charts/TreemapChart.jsx":"import React from \"react\";\nimport { fmtNum, treemap, seriesColor } from \"./chart-geometry.js\";\nimport { ChartTooltip, wrapStyle } from \"./ChartKit.jsx\";\n\n/**\n * Squarified treemap — composition at a glance (cost base, storage by dataset).\n * items: [{label, value, color?}]. Labels render only where they fit.\n */\nexport function TreemapChart({ items = [], width = 560, height = 260, valueFormat = fmtNum, interactive = true, style }) {\n const [hover, setHover] = React.useState(null);\n const cells = treemap(items, width, height);\n const total = items.reduce((s, i) => s + i.value, 0) || 1;\n const colorOf = (it) => it.color || seriesColor(items.indexOf(it));\n const h = hover != null ? cells[hover] : null;\n return (\n <div style={{ ...wrapStyle, ...style }}>\n <svg width={width} height={height} role=\"img\">\n {cells.map((c, i) => (\n <g key={i} onMouseEnter={interactive ? () => setHover(i) : undefined} onMouseLeave={interactive ? () => setHover(null) : undefined} style={{ cursor: interactive ? \"default\" : undefined }}>\n <rect x={c.x + 1} y={c.y + 1} width={Math.max(1, c.w - 2)} height={Math.max(1, c.h - 2)} rx=\"3\" fill={colorOf(c.item)}\n opacity={hover == null ? 0.85 : hover === i ? 1 : 0.35} style={{ transition: \"opacity var(--dur-fast) var(--ease-out)\" }} />\n {c.w > 74 && c.h > 34 ? <>\n <text x={c.x + 9} y={c.y + 18} style={{ fontFamily: \"var(--font-sans)\", fontSize: 11, fill: \"var(--on-accent)\" }}>{c.item.label}</text>\n <text x={c.x + 9} y={c.y + 32} style={{ fontFamily: \"var(--font-mono)\", fontSize: 10, fill: \"var(--on-accent)\", opacity: 0.85 }}>{valueFormat(c.item.value)}</text>\n </> : null}\n </g>\n ))}\n </svg>\n {h && interactive ? (\n <ChartTooltip x={h.x + h.w / 2} y={h.y + h.h / 2} flip={h.x + h.w / 2 > width * 0.62} title={h.item.label}\n rows={[{ label: \"value\", value: valueFormat(h.item.value) }, { label: \"share\", value: ((h.item.value / total) * 100).toFixed(1) + \"%\" }]} />\n ) : null}\n </div>\n );\n}\n","charts/TreemapChart.vue":"<script setup>\nimport { ref, computed } from 'vue'\nimport { fmtNum, treemap, SERIES } from './chart-geometry.js'\n// Squarified treemap; labels render only where they fit, hover dims the rest.\nconst props = defineProps({ items:{type:Array,default:()=>[]}, width:{type:Number,default:560}, height:{type:Number,default:260}, valueFormat:{type:Function,default:fmtNum}, interactive:{type:Boolean,default:true}, style:Object })\nconst hover = ref(null)\nconst cells = computed(() => treemap(props.items, props.width, props.height).map(c => ({ ...c, color: c.item.color || SERIES[props.items.indexOf(c.item)%SERIES.length] })))\nconst total = computed(() => props.items.reduce((s,i)=>s+i.value,0)||1)\n</script>\n<template>\n <div :style=\"{position:'relative',display:'inline-block',lineHeight:0,...style}\">\n <svg :width=\"width\" :height=\"height\" role=\"img\">\n <g v-for=\"(c,i) in cells\" :key=\"i\" @mouseenter=\"interactive && (hover=i)\" @mouseleave=\"interactive && (hover=null)\">\n <rect :x=\"c.x+1\" :y=\"c.y+1\" :width=\"Math.max(1,c.w-2)\" :height=\"Math.max(1,c.h-2)\" rx=\"3\" :fill=\"c.color\"\n :opacity=\"hover==null?.85:hover===i?1:.35\" style=\"transition:opacity var(--dur-fast) var(--ease-out)\"/>\n <template v-if=\"c.w>74 && c.h>34\">\n <text :x=\"c.x+9\" :y=\"c.y+18\" style=\"font-family:var(--font-sans);font-size:11px;fill:var(--on-accent)\">{{ c.item.label }}</text>\n <text :x=\"c.x+9\" :y=\"c.y+32\" style=\"font-family:var(--font-mono);font-size:10px;fill:var(--on-accent);opacity:.85\">{{ valueFormat(c.item.value) }}</text>\n </template>\n </g>\n </svg>\n <div v-if=\"hover!=null && interactive\" :style=\"{position:'absolute',left:(cells[hover].x+cells[hover].w/2)+'px',top:(cells[hover].y+cells[hover].h/2)+'px',transform:(cells[hover].x+cells[hover].w/2)>width*0.62?'translate(calc(-100% - 10px),-50%)':'translate(10px,-50%)',pointerEvents:'none',zIndex:5,background:'var(--bg-surface)',border:'1px solid var(--border-card)',borderRadius:'var(--radius-md)',boxShadow:'var(--shadow-menu)',padding:'7px 10px',lineHeight:'normal'}\">\n <div style=\"font-family:var(--font-sans);font-size:var(--text-2xs);color:var(--text-muted);white-space:nowrap;padding-bottom:2px\">{{ cells[hover].item.label }}</div>\n <div style=\"font-family:var(--font-mono);font-size:var(--text-xs);color:var(--text-heading);white-space:nowrap\">{{ valueFormat(cells[hover].item.value) }} · {{ ((cells[hover].item.value/total)*100).toFixed(1) }}%</div>\n </div>\n </div>\n</template>\n","charts/RadarChart.jsx":"import React from \"react\";\nimport { fmtNum, niceTicks, polar, seriesColor } from \"./chart-geometry.js\";\nimport { ChartLegend, ChartTooltip, wrapStyle } from \"./ChartKit.jsx\";\n\n/**\n * Radar (spider) chart — compare a few entities across the same 3–10 measures.\n * axes: [{label, max?}]; series: [{name, values, color?}] aligned to axes.\n * Hovering an axis spoke highlights it and reports every series at that measure.\n */\nexport function RadarChart({ axes = [], series = [], size = 260, levels = 4, legend = true, interactive = true, valueFormat = fmtNum, style }) {\n const [hover, setHover] = React.useState(null);\n const [hidden, setHidden] = React.useState([]);\n const shown = series.filter((s) => !hidden.includes(s.name));\n /* Axis labels sit outside the outer ring, so the plot radius has to leave room for the\n longest one — otherwise \"Accuracy\" gets clipped at the SVG edge. */\n const cx = size / 2, cy = size / 2, r = size / 2 - 42;\n const n = axes.length || 1;\n const step = 360 / n;\n const max = axes.map((a, i) => a.max != null ? a.max : niceTicks(0, Math.max(...series.map((s) => s.values[i] || 0)), 3).hi);\n const at = (si, i) => {\n const v = (shown[si].values[i] || 0) / (max[i] || 1);\n return polar(cx, cy, Math.max(0, Math.min(1, v)) * r, i * step);\n };\n const colorOf = (s) => s.color || seriesColor(series.indexOf(s));\n const move = (e) => {\n if (!interactive) return;\n const rc = e.currentTarget.getBoundingClientRect();\n const dx = e.clientX - rc.left - cx, dy = e.clientY - rc.top - cy;\n if (Math.hypot(dx, dy) > r + 22) return setHover(null);\n let deg = (Math.atan2(dy, dx) * 180) / Math.PI + 90;\n if (deg < 0) deg += 360;\n setHover(Math.round(deg / step) % n);\n };\n const tip = hover != null ? polar(cx, cy, r * 0.72, hover * step) : null;\n return (\n <div style={{ ...wrapStyle, ...style }}>\n <svg width={size} height={size} onMouseMove={move} onMouseLeave={() => setHover(null)} role=\"img\" style={{ overflow: \"visible\" }}>\n {[...Array(levels)].map((_, li) => {\n const rr = (r * (li + 1)) / levels;\n const pts = [...Array(n)].map((__, i) => polar(cx, cy, rr, i * step).map((v) => v.toFixed(1)).join(\" \")).join(\" L\");\n return <path key={li} d={\"M\" + pts + \" Z\"} fill=\"none\" stroke=\"var(--chart-grid)\" strokeWidth=\"1\" />;\n })}\n {axes.map((a, i) => {\n const [x, y] = polar(cx, cy, r, i * step);\n const [lx, ly] = polar(cx, cy, r + 14, i * step);\n /* An anchor=\"start\" label grows rightward, so near-vertical spokes must center\n and the right column must not sit so close to the edge that it clips. */\n const anchor = Math.abs(lx - cx) < 12 ? \"middle\" : lx > cx ? \"start\" : \"end\";\n const on = hover === i;\n return (\n <g key={a.label}>\n <line x1={cx} y1={cy} x2={x} y2={y} stroke={on ? \"var(--chart-guide)\" : \"var(--chart-axis)\"} strokeWidth=\"1\" />\n <text x={lx} y={ly + 3} textAnchor={anchor}\n style={{ fontFamily: \"var(--font-sans)\", fontSize: 10, fill: on ? \"var(--text-heading)\" : \"var(--chart-label)\" }}>{a.label}</text>\n </g>\n );\n })}\n {shown.map((s, si) => {\n const pts = axes.map((_, i) => at(si, i));\n const d = \"M\" + pts.map((p) => p[0].toFixed(1) + \" \" + p[1].toFixed(1)).join(\" L\") + \" Z\";\n return (\n <g key={s.name}>\n <path d={d} fill={colorOf(s)} fillOpacity=\"0.14\" stroke={colorOf(s)} strokeWidth=\"1.5\" strokeLinejoin=\"round\" />\n {pts.map((p, i) => <circle key={i} cx={p[0]} cy={p[1]} r={hover === i ? 3.4 : 2.2} fill={colorOf(s)} stroke=\"var(--bg-surface)\" strokeWidth={hover === i ? 1.5 : 0} />)}\n </g>\n );\n })}\n </svg>\n {hover != null && interactive ? (\n <ChartTooltip x={tip[0]} y={tip[1]} flip={tip[0] > size * 0.58} title={axes[hover].label}\n rows={shown.map((s) => ({ label: s.name, value: valueFormat(s.values[hover]), color: colorOf(s) }))} />\n ) : null}\n {legend && series.length > 1 ? (\n <ChartLegend style={{ paddingTop: 6 }} items={series.map((s) => ({ label: s.name, color: colorOf(s) }))} hidden={hidden}\n onToggle={(l) => setHidden(hidden.includes(l) ? hidden.filter((x) => x !== l) : hidden.length + 1 < series.length ? [...hidden, l] : hidden)} />\n ) : null}\n </div>\n );\n}\n","charts/RadarChart.vue":"<script setup>\nimport { ref, computed } from 'vue'\nimport { fmtNum, niceTicks, polar, SERIES } from './chart-geometry.js'\n// Spider chart — a few entities across the same 3–10 measures. Hovering a spoke reports all series.\nconst props = defineProps({ axes:{type:Array,default:()=>[]}, series:{type:Array,default:()=>[]}, size:{type:Number,default:260}, levels:{type:Number,default:4}, legend:{type:Boolean,default:true}, interactive:{type:Boolean,default:true}, valueFormat:{type:Function,default:fmtNum}, style:Object })\nconst hover = ref(null); const hidden = ref([])\nconst g = computed(() => {\n const cx = props.size/2, cy = props.size/2, r = props.size/2-42\n const n = props.axes.length||1, step = 360/n\n const shown = props.series.filter(s=>!hidden.value.includes(s.name))\n const max = props.axes.map((a,i)=> a.max!=null ? a.max : niceTicks(0, Math.max(...props.series.map(s=>s.values[i]||0)), 3).hi)\n const rings = [...Array(props.levels)].map((_,li)=>{\n const rr = (r*(li+1))/props.levels\n return 'M' + [...Array(n)].map((__,i)=>polar(cx,cy,rr,i*step).map(v=>v.toFixed(1)).join(' ')).join(' L') + ' Z'\n })\n const spokes = props.axes.map((a,i)=>{\n const p = polar(cx,cy,r,i*step), l = polar(cx,cy,r+14,i*step)\n // An anchor=\"start\" label grows rightward, so near-vertical spokes must center.\n return { label:a.label, x:p[0], y:p[1], lx:l[0], ly:l[1],\n anchor: Math.abs(l[0]-cx)<12 ? 'middle' : (l[0]>cx ? 'start' : 'end') }\n })\n const shapes = shown.map(s=>{\n const pts = props.axes.map((_,i)=>polar(cx,cy,Math.max(0,Math.min(1,(s.values[i]||0)/(max[i]||1)))*r, i*step))\n return { s, color: s.color||SERIES[props.series.indexOf(s)%SERIES.length], pts,\n d:'M'+pts.map(p=>p[0].toFixed(1)+' '+p[1].toFixed(1)).join(' L')+' Z' }\n })\n return { cx, cy, r, n, step, rings, spokes, shapes, shown }\n})\nfunction move(e){\n if(!props.interactive) return\n const rc = e.currentTarget.getBoundingClientRect()\n const dx = e.clientX-rc.left-g.value.cx, dy = e.clientY-rc.top-g.value.cy\n if (Math.hypot(dx,dy) > g.value.r+22) { hover.value=null; return }\n let deg = (Math.atan2(dy,dx)*180)/Math.PI + 90\n if (deg<0) deg += 360\n hover.value = Math.round(deg/g.value.step) % g.value.n\n}\nconst tip = computed(()=> hover.value==null ? null : polar(g.value.cx, g.value.cy, g.value.r*0.72, hover.value*g.value.step))\nfunction toggle(name){\n if (hidden.value.includes(name)) hidden.value = hidden.value.filter(x=>x!==name)\n else if (hidden.value.length+1 < props.series.length) hidden.value = [...hidden.value, name]\n}\n</script>\n<template>\n <div :style=\"{position:'relative',display:'inline-block',lineHeight:0,...style}\">\n <svg :width=\"size\" :height=\"size\" @mousemove=\"move\" @mouseleave=\"hover=null\" role=\"img\" style=\"overflow:visible\">\n <path v-for=\"(d,i) in g.rings\" :key=\"'r'+i\" :d=\"d\" fill=\"none\" stroke=\"var(--chart-grid)\" stroke-width=\"1\"/>\n <g v-for=\"(sp,i) in g.spokes\" :key=\"sp.label\">\n <line :x1=\"g.cx\" :y1=\"g.cy\" :x2=\"sp.x\" :y2=\"sp.y\" :stroke=\"hover===i?'var(--chart-guide)':'var(--chart-axis)'\" stroke-width=\"1\"/>\n <text :x=\"sp.lx\" :y=\"sp.ly+3\" :text-anchor=\"sp.anchor\" :style=\"{fontFamily:'var(--font-sans)',fontSize:'10px',fill:hover===i?'var(--text-heading)':'var(--chart-label)'}\">{{ sp.label }}</text>\n </g>\n <g v-for=\"sh in g.shapes\" :key=\"sh.s.name\">\n <path :d=\"sh.d\" :fill=\"sh.color\" fill-opacity=\"0.14\" :stroke=\"sh.color\" stroke-width=\"1.5\" stroke-linejoin=\"round\"/>\n <circle v-for=\"(p,i) in sh.pts\" :key=\"i\" :cx=\"p[0]\" :cy=\"p[1]\" :r=\"hover===i?3.4:2.2\" :fill=\"sh.color\" stroke=\"var(--bg-surface)\" :stroke-width=\"hover===i?1.5:0\"/>\n </g>\n </svg>\n <div v-if=\"hover!=null && interactive\" :style=\"{position:'absolute',left:tip[0]+'px',top:tip[1]+'px',transform:tip[0]>size*0.58?'translate(calc(-100% - 10px),-50%)':'translate(10px,-50%)',pointerEvents:'none',zIndex:5,background:'var(--bg-surface)',border:'1px solid var(--border-card)',borderRadius:'var(--radius-md)',boxShadow:'var(--shadow-menu)',padding:'7px 10px',lineHeight:'normal',whiteSpace:'nowrap'}\">\n <div style=\"font-family:var(--font-sans);font-size:var(--text-2xs);color:var(--text-muted);padding-bottom:3px\">{{ axes[hover].label }}</div>\n <div v-for=\"sh in g.shapes\" :key=\"sh.s.name\" style=\"display:flex;align-items:center;gap:6px\">\n <span :style=\"{width:'7px',height:'7px',borderRadius:'2px',background:sh.color}\"/>\n <span style=\"font-family:var(--font-sans);font-size:var(--text-xs);color:var(--text-secondary)\">{{ sh.s.name }}</span>\n <span style=\"font-family:var(--font-mono);font-size:var(--text-xs);color:var(--text-heading);margin-left:auto;padding-left:10px\">{{ valueFormat(sh.s.values[hover]) }}</span>\n </div>\n </div>\n <div v-if=\"legend && series.length>1\" style=\"display:flex;flex-wrap:wrap;gap:4px 14px;padding-top:6px;line-height:normal\">\n <span v-for=\"(s,i) in series\" :key=\"s.name\" role=\"button\" tabindex=\"0\" @click=\"toggle(s.name)\"\n :style=\"{display:'inline-flex',alignItems:'center',gap:'6px',cursor:'pointer',opacity:hidden.includes(s.name)?0.4:1}\">\n <span :style=\"{width:'8px',height:'8px',borderRadius:'2px',background:s.color||SERIES[i%SERIES.length]}\"/>\n <span style=\"font-family:var(--font-sans);font-size:var(--text-xs);color:var(--text-secondary)\">{{ s.name }}</span>\n </span>\n </div>\n </div>\n</template>\n","charts/Histogram.jsx":"import React from \"react\";\nimport { fmtNum, niceTicks, scaleLin } from \"./chart-geometry.js\";\nimport { ChartTooltip, PlotGrid, wrapStyle } from \"./ChartKit.jsx\";\n\nconst bucketize = (values, bins) => {\n const lo = Math.min(...values), hi = Math.max(...values);\n const w = (hi - lo) / bins || 1;\n const out = [...Array(bins)].map((_, i) => ({ from: lo + i * w, to: lo + (i + 1) * w, n: 0 }));\n values.forEach((v) => { out[Math.max(0, Math.min(bins - 1, Math.floor((v - lo) / w)))].n++; });\n return out;\n};\n\n/**\n * Histogram — the distribution of one measure. Pass raw `values` (auto-bucketed into\n * `bins`) or pre-computed `buckets: [{from, to, n}]`. mean/median guides are optional.\n */\nexport function Histogram({ values = [], buckets, bins = 12, width = 560, height = 240, axes = true, showMean = false, valueFormat = fmtNum, countFormat = fmtNum, interactive = true, color = \"var(--series-1)\", style }) {\n const [hover, setHover] = React.useState(null);\n const bkt = buckets || bucketize(values, bins);\n const pad = { l: axes ? 40 : 4, r: 8, t: showMean ? 22 : 10, b: axes ? 26 : 4 };\n const { ticks, lo, hi } = niceTicks(0, Math.max(1, ...bkt.map((b) => b.n)), 4);\n const sy = scaleLin(lo, hi, height - pad.b, pad.t);\n const slot = (width - pad.l - pad.r) / bkt.length;\n const mean = showMean && values.length ? values.reduce((s, v) => s + v, 0) / values.length : null;\n const sxVal = scaleLin(bkt[0].from, bkt[bkt.length - 1].to, pad.l, width - pad.r);\n const move = (e) => {\n if (!interactive) return;\n const r = e.currentTarget.getBoundingClientRect();\n const i = Math.floor((e.clientX - r.left - pad.l) / slot);\n setHover(i >= 0 && i < bkt.length ? i : null);\n };\n const total = bkt.reduce((s, b) => s + b.n, 0) || 1;\n return (\n <div style={{ ...wrapStyle, ...style }}>\n <svg width={width} height={height} onMouseMove={move} onMouseLeave={() => setHover(null)} role=\"img\">\n {axes ? <PlotGrid w={width} h={height} pad={pad} ticks={ticks} sy={sy} xLabels={[]} sxLabel={() => 0} yFormat={countFormat} /> : null}\n {bkt.map((b, i) => (\n <rect key={i} x={pad.l + slot * i + 0.5} y={sy(b.n)} width={Math.max(1, slot - 1.5)} height={Math.max(1, sy(0) - sy(b.n))} rx=\"2\"\n fill={color} opacity={hover == null ? 0.9 : hover === i ? 1 : 0.4} style={{ transition: \"opacity var(--dur-fast) var(--ease-out)\" }} />\n ))}\n {mean != null ? <g>\n <line x1={sxVal(mean)} x2={sxVal(mean)} y1={pad.t} y2={height - pad.b} stroke=\"var(--chart-guide)\" strokeWidth=\"1\" strokeDasharray=\"3 3\" />\n <text x={Math.min(sxVal(mean) + 4, width - 60)} y={pad.t - 8} style={{ fontFamily: \"var(--font-mono)\", fontSize: 10, fill: \"var(--chart-label)\" }}>mean {valueFormat(mean)}</text>\n </g> : null}\n {axes ? [0, Math.floor(bkt.length / 2), bkt.length - 1].map((i) => (\n <text key={i} x={pad.l + slot * i + slot / 2} y={height - pad.b + 16} textAnchor=\"middle\" style={{ fontFamily: \"var(--font-mono)\", fontSize: 10, fill: \"var(--chart-label)\" }}>{valueFormat(bkt[i].from)}</text>\n )) : null}\n </svg>\n {hover != null && interactive ? (\n <ChartTooltip x={pad.l + slot * hover + slot / 2} y={height / 2 - 10} flip={pad.l + slot * hover > width * 0.62}\n title={valueFormat(bkt[hover].from) + \" – \" + valueFormat(bkt[hover].to)}\n rows={[{ label: \"count\", value: countFormat(bkt[hover].n), color }, { label: \"share\", value: ((bkt[hover].n / total) * 100).toFixed(1) + \"%\" }]} />\n ) : null}\n </div>\n );\n}\n","charts/Histogram.vue":"<script setup>\nimport { ref, computed } from 'vue'\nimport { fmtNum, niceTicks, scaleLin } from './chart-geometry.js'\n// Distribution of one measure — raw values are bucketed for you.\nconst props = defineProps({ values:{type:Array,default:()=>[]}, buckets:Array, bins:{type:Number,default:12}, width:{type:Number,default:560}, height:{type:Number,default:240}, axes:{type:Boolean,default:true}, showMean:Boolean, valueFormat:{type:Function,default:fmtNum}, countFormat:{type:Function,default:fmtNum}, interactive:{type:Boolean,default:true}, color:{type:String,default:'var(--series-1)'}, style:Object })\nconst hover = ref(null)\nconst g = computed(() => {\n let bkt = props.buckets\n if (!bkt) {\n const lo = Math.min(...props.values), hi = Math.max(...props.values)\n const w = (hi-lo)/props.bins || 1\n bkt = [...Array(props.bins)].map((_,i)=>({ from: lo+i*w, to: lo+(i+1)*w, n: 0 }))\n props.values.forEach(v=>{ bkt[Math.max(0,Math.min(props.bins-1,Math.floor((v-lo)/w)))].n++ })\n }\n const pad = { l: props.axes?40:4, r:8, t: props.showMean?22:10, b: props.axes?26:4 }\n const { ticks, lo, hi } = niceTicks(0, Math.max(1,...bkt.map(b=>b.n)), 4)\n const sy = scaleLin(lo, hi, props.height-pad.b, pad.t)\n const slot = (props.width-pad.l-pad.r)/bkt.length\n const mean = props.showMean && props.values.length ? props.values.reduce((s,v)=>s+v,0)/props.values.length : null\n const sxVal = scaleLin(bkt[0].from, bkt[bkt.length-1].to, pad.l, props.width-pad.r)\n return { bkt, pad, ticks, sy, slot, mean, meanX: mean!=null ? Math.min(sxVal(mean)+4, props.width-60) : 0, meanLine: mean!=null ? sxVal(mean) : 0,\n total: bkt.reduce((s,b)=>s+b.n,0)||1, labels:[0, Math.floor(bkt.length/2), bkt.length-1] }\n})\nfunction move(e){\n if(!props.interactive) return\n const r = e.currentTarget.getBoundingClientRect()\n const i = Math.floor((e.clientX-r.left-g.value.pad.l)/g.value.slot)\n hover.value = i>=0 && i<g.value.bkt.length ? i : null\n}\n</script>\n<template>\n <div :style=\"{position:'relative',display:'inline-block',lineHeight:0,...style}\">\n <svg :width=\"width\" :height=\"height\" @mousemove=\"move\" @mouseleave=\"hover=null\" role=\"img\">\n <template v-if=\"axes\">\n <g v-for=\"t in g.ticks\" :key=\"t\">\n <line :x1=\"g.pad.l\" :x2=\"width-g.pad.r\" :y1=\"g.sy(t)\" :y2=\"g.sy(t)\" :stroke=\"t===0?'var(--chart-axis)':'var(--chart-grid)'\" stroke-width=\"1\"/>\n <text :x=\"g.pad.l-8\" :y=\"g.sy(t)+3\" text-anchor=\"end\" style=\"font-family:var(--font-mono);font-size:10px;fill:var(--chart-label)\">{{ countFormat(t) }}</text>\n </g>\n </template>\n <rect v-for=\"(b,i) in g.bkt\" :key=\"i\" :x=\"g.pad.l+g.slot*i+0.5\" :y=\"g.sy(b.n)\" :width=\"Math.max(1,g.slot-1.5)\" :height=\"Math.max(1,g.sy(0)-g.sy(b.n))\" rx=\"2\"\n :fill=\"color\" :opacity=\"hover==null?0.9:(hover===i?1:0.4)\" style=\"transition:opacity var(--dur-fast) var(--ease-out)\"/>\n <template v-if=\"g.mean!=null\">\n <line :x1=\"g.meanLine\" :x2=\"g.meanLine\" :y1=\"g.pad.t\" :y2=\"height-g.pad.b\" stroke=\"var(--chart-guide)\" stroke-width=\"1\" stroke-dasharray=\"3 3\"/>\n <text :x=\"g.meanX\" :y=\"g.pad.t-8\" style=\"font-family:var(--font-mono);font-size:10px;fill:var(--chart-label)\">mean {{ valueFormat(g.mean) }}</text>\n </template>\n <text v-if=\"axes\" v-for=\"i in g.labels\" :key=\"'l'+i\" :x=\"g.pad.l+g.slot*i+g.slot/2\" :y=\"height-g.pad.b+16\" text-anchor=\"middle\" style=\"font-family:var(--font-mono);font-size:10px;fill:var(--chart-label)\">{{ valueFormat(g.bkt[i].from) }}</text>\n </svg>\n <div v-if=\"hover!=null && interactive\" :style=\"{position:'absolute',left:(g.pad.l+g.slot*hover+g.slot/2)+'px',top:(height/2-10)+'px',transform:(g.pad.l+g.slot*hover)>width*0.62?'translate(calc(-100% - 10px),-50%)':'translate(10px,-50%)',pointerEvents:'none',zIndex:5,background:'var(--bg-surface)',border:'1px solid var(--border-card)',borderRadius:'var(--radius-md)',boxShadow:'var(--shadow-menu)',padding:'7px 10px',lineHeight:'normal',whiteSpace:'nowrap'}\">\n <div style=\"font-family:var(--font-sans);font-size:var(--text-2xs);color:var(--text-muted);padding-bottom:3px\">{{ valueFormat(g.bkt[hover].from) }} – {{ valueFormat(g.bkt[hover].to) }}</div>\n <div style=\"font-family:var(--font-mono);font-size:var(--text-xs);color:var(--text-heading)\">{{ countFormat(g.bkt[hover].n) }} · {{ ((g.bkt[hover].n/g.total)*100).toFixed(1) }}%</div>\n </div>\n </div>\n</template>\n","charts/BoxPlot.jsx":"import React from \"react\";\nimport { fmtNum, niceTicks, scaleLin, seriesColor } from \"./chart-geometry.js\";\nimport { ChartTooltip, PlotGrid, wrapStyle } from \"./ChartKit.jsx\";\n\n/** Five-number summary from raw values (Tukey fences for outliers). */\nexport function summarize(values) {\n const v = values.slice().sort((a, b) => a - b);\n const q = (p) => { const i = (v.length - 1) * p, lo = Math.floor(i), hi = Math.ceil(i); return v[lo] + (v[hi] - v[lo]) * (i - lo); };\n const q1 = q(0.25), med = q(0.5), q3 = q(0.75), iqr = q3 - q1;\n const lo = Math.max(v[0], q1 - 1.5 * iqr), hi = Math.min(v[v.length - 1], q3 + 1.5 * iqr);\n return { q1, med, q3, lo, hi, outliers: v.filter((x) => x < lo || x > hi) };\n}\n\n/**\n * Box & whisker — spread and outliers per group. groups: [{label, values}] (summarized\n * for you) or [{label, q1, med, q3, lo, hi, outliers}] when you have the stats already.\n */\nexport function BoxPlot({ groups = [], width = 560, height = 260, axes = true, valueFormat = fmtNum, interactive = true, style }) {\n const [hover, setHover] = React.useState(null);\n const boxes = groups.map((g) => (g.values ? { label: g.label, color: g.color, ...summarize(g.values) } : g));\n const pad = { l: axes ? 44 : 4, r: 8, t: 12, b: axes ? 26 : 4 };\n const all = boxes.flatMap((b) => [b.lo, b.hi, ...(b.outliers || [])]);\n const { ticks, lo, hi } = niceTicks(Math.min(...all), Math.max(...all), 4);\n const sy = scaleLin(lo, hi, height - pad.b, pad.t);\n const slot = (width - pad.l - pad.r) / boxes.length;\n const bw = Math.min(slot * 0.5, 38);\n const move = (e) => {\n if (!interactive) return;\n const r = e.currentTarget.getBoundingClientRect();\n const i = Math.floor((e.clientX - r.left - pad.l) / slot);\n setHover(i >= 0 && i < boxes.length ? i : null);\n };\n const h = hover != null ? boxes[hover] : null;\n return (\n <div style={{ ...wrapStyle, ...style }}>\n <svg width={width} height={height} onMouseMove={move} onMouseLeave={() => setHover(null)} role=\"img\">\n {axes ? <PlotGrid w={width} h={height} pad={pad} ticks={ticks} sy={sy} xLabels={[]} sxLabel={() => 0} yFormat={valueFormat} /> : null}\n {boxes.map((b, i) => {\n const x = pad.l + slot * i + slot / 2;\n const c = b.color || seriesColor(i);\n return (\n <g key={b.label} opacity={hover == null || hover === i ? 1 : 0.45} style={{ transition: \"opacity var(--dur-fast) var(--ease-out)\" }}>\n <line x1={x} x2={x} y1={sy(b.hi)} y2={sy(b.q3)} stroke=\"var(--chart-axis)\" strokeWidth=\"1\" />\n <line x1={x} x2={x} y1={sy(b.q1)} y2={sy(b.lo)} stroke=\"var(--chart-axis)\" strokeWidth=\"1\" />\n <line x1={x - bw / 3} x2={x + bw / 3} y1={sy(b.hi)} y2={sy(b.hi)} stroke=\"var(--chart-axis)\" strokeWidth=\"1\" />\n <line x1={x - bw / 3} x2={x + bw / 3} y1={sy(b.lo)} y2={sy(b.lo)} stroke=\"var(--chart-axis)\" strokeWidth=\"1\" />\n <rect x={x - bw / 2} y={sy(b.q3)} width={bw} height={Math.max(1, sy(b.q1) - sy(b.q3))} rx=\"2\" fill={c} fillOpacity=\"0.28\" stroke={c} strokeWidth=\"1.2\" />\n <line x1={x - bw / 2} x2={x + bw / 2} y1={sy(b.med)} y2={sy(b.med)} stroke={c} strokeWidth=\"2\" />\n {(b.outliers || []).map((o, oi) => <circle key={oi} cx={x} cy={sy(o)} r=\"2.2\" fill=\"none\" stroke={c} strokeWidth=\"1\" />)}\n <text x={x} y={height - pad.b + 16} textAnchor=\"middle\" style={{ fontFamily: \"var(--font-sans)\", fontSize: 10, fill: \"var(--chart-label)\" }}>{b.label}</text>\n </g>\n );\n })}\n </svg>\n {h && interactive ? (\n <ChartTooltip x={pad.l + slot * hover + slot / 2} y={height / 2 - 10} flip={pad.l + slot * hover > width * 0.62} title={h.label}\n rows={[{ label: \"max\", value: valueFormat(h.hi) }, { label: \"Q3\", value: valueFormat(h.q3) }, { label: \"median\", value: valueFormat(h.med), color: h.color || seriesColor(hover) }, { label: \"Q1\", value: valueFormat(h.q1) }, { label: \"min\", value: valueFormat(h.lo) }]\n .concat((h.outliers || []).length ? [{ label: \"outliers\", value: String(h.outliers.length) }] : [])} />\n ) : null}\n </div>\n );\n}\n","charts/BoxPlot.vue":"<script setup>\nimport { ref, computed } from 'vue'\nimport { fmtNum, niceTicks, scaleLin, SERIES } from './chart-geometry.js'\n// Box & whisker with Tukey-fence outliers. Pass raw values or the five-number stats.\nconst props = defineProps({ groups:{type:Array,default:()=>[]}, width:{type:Number,default:560}, height:{type:Number,default:260}, axes:{type:Boolean,default:true}, valueFormat:{type:Function,default:fmtNum}, interactive:{type:Boolean,default:true}, style:Object })\nconst hover = ref(null)\nfunction summarize(values){\n const v = values.slice().sort((a,b)=>a-b)\n const q = p => { const i=(v.length-1)*p, lo=Math.floor(i), hi=Math.ceil(i); return v[lo]+(v[hi]-v[lo])*(i-lo) }\n const q1=q(0.25), med=q(0.5), q3=q(0.75), iqr=q3-q1\n const lo=Math.max(v[0], q1-1.5*iqr), hi=Math.min(v[v.length-1], q3+1.5*iqr)\n return { q1, med, q3, lo, hi, outliers: v.filter(x=>x<lo||x>hi) }\n}\nconst g = computed(() => {\n const boxes = props.groups.map((x,i)=> Object.assign({ label:x.label, color:x.color||SERIES[i%SERIES.length] }, x.values ? summarize(x.values) : x))\n const pad = { l: props.axes?44:4, r:8, t:12, b: props.axes?26:4 }\n const all = boxes.flatMap(b=>[b.lo, b.hi, ...(b.outliers||[])])\n const { ticks, lo, hi } = niceTicks(Math.min(...all), Math.max(...all), 4)\n const sy = scaleLin(lo, hi, props.height-pad.b, pad.t)\n const slot = (props.width-pad.l-pad.r)/boxes.length\n const bw = Math.min(slot*0.5, 38)\n return { boxes: boxes.map((b,i)=>Object.assign({}, b, { x: pad.l+slot*i+slot/2, bw })), pad, ticks, sy, slot }\n})\nfunction move(e){\n if(!props.interactive) return\n const r = e.currentTarget.getBoundingClientRect()\n const i = Math.floor((e.clientX-r.left-g.value.pad.l)/g.value.slot)\n hover.value = i>=0 && i<g.value.boxes.length ? i : null\n}\n</script>\n<template>\n <div :style=\"{position:'relative',display:'inline-block',lineHeight:0,...style}\">\n <svg :width=\"width\" :height=\"height\" @mousemove=\"move\" @mouseleave=\"hover=null\" role=\"img\">\n <template v-if=\"axes\">\n <g v-for=\"t in g.ticks\" :key=\"t\">\n <line :x1=\"g.pad.l\" :x2=\"width-g.pad.r\" :y1=\"g.sy(t)\" :y2=\"g.sy(t)\" :stroke=\"t===0?'var(--chart-axis)':'var(--chart-grid)'\" stroke-width=\"1\"/>\n <text :x=\"g.pad.l-8\" :y=\"g.sy(t)+3\" text-anchor=\"end\" style=\"font-family:var(--font-mono);font-size:10px;fill:var(--chart-label)\">{{ valueFormat(t) }}</text>\n </g>\n </template>\n <g v-for=\"(b,i) in g.boxes\" :key=\"b.label\" :opacity=\"hover==null||hover===i?1:0.45\" style=\"transition:opacity var(--dur-fast) var(--ease-out)\">\n <line :x1=\"b.x\" :x2=\"b.x\" :y1=\"g.sy(b.hi)\" :y2=\"g.sy(b.q3)\" stroke=\"var(--chart-axis)\" stroke-width=\"1\"/>\n <line :x1=\"b.x\" :x2=\"b.x\" :y1=\"g.sy(b.q1)\" :y2=\"g.sy(b.lo)\" stroke=\"var(--chart-axis)\" stroke-width=\"1\"/>\n <line :x1=\"b.x-b.bw/3\" :x2=\"b.x+b.bw/3\" :y1=\"g.sy(b.hi)\" :y2=\"g.sy(b.hi)\" stroke=\"var(--chart-axis)\" stroke-width=\"1\"/>\n <line :x1=\"b.x-b.bw/3\" :x2=\"b.x+b.bw/3\" :y1=\"g.sy(b.lo)\" :y2=\"g.sy(b.lo)\" stroke=\"var(--chart-axis)\" stroke-width=\"1\"/>\n <rect :x=\"b.x-b.bw/2\" :y=\"g.sy(b.q3)\" :width=\"b.bw\" :height=\"Math.max(1,g.sy(b.q1)-g.sy(b.q3))\" rx=\"2\" :fill=\"b.color\" fill-opacity=\"0.28\" :stroke=\"b.color\" stroke-width=\"1.2\"/>\n <line :x1=\"b.x-b.bw/2\" :x2=\"b.x+b.bw/2\" :y1=\"g.sy(b.med)\" :y2=\"g.sy(b.med)\" :stroke=\"b.color\" stroke-width=\"2\"/>\n <circle v-for=\"(o,oi) in (b.outliers||[])\" :key=\"oi\" :cx=\"b.x\" :cy=\"g.sy(o)\" r=\"2.2\" fill=\"none\" :stroke=\"b.color\" stroke-width=\"1\"/>\n <text :x=\"b.x\" :y=\"height-g.pad.b+16\" text-anchor=\"middle\" style=\"font-family:var(--font-sans);font-size:10px;fill:var(--chart-label)\">{{ b.label }}</text>\n </g>\n </svg>\n <div v-if=\"hover!=null && interactive\" :style=\"{position:'absolute',left:g.boxes[hover].x+'px',top:(height/2-10)+'px',transform:g.boxes[hover].x>width*0.62?'translate(calc(-100% - 10px),-50%)':'translate(10px,-50%)',pointerEvents:'none',zIndex:5,background:'var(--bg-surface)',border:'1px solid var(--border-card)',borderRadius:'var(--radius-md)',boxShadow:'var(--shadow-menu)',padding:'7px 10px',lineHeight:'normal',whiteSpace:'nowrap'}\">\n <div style=\"font-family:var(--font-sans);font-size:var(--text-2xs);color:var(--text-muted);padding-bottom:3px\">{{ g.boxes[hover].label }}</div>\n <div style=\"font-family:var(--font-mono);font-size:var(--text-xs);color:var(--text-heading)\">\n {{ valueFormat(g.boxes[hover].lo) }} · Q1 {{ valueFormat(g.boxes[hover].q1) }} · med {{ valueFormat(g.boxes[hover].med) }} · Q3 {{ valueFormat(g.boxes[hover].q3) }} · {{ valueFormat(g.boxes[hover].hi) }}\n </div>\n </div>\n </div>\n</template>\n","charts/ParetoChart.jsx":"import React from \"react\";\nimport { fmtNum, niceTicks, scaleLin, linePath } from \"./chart-geometry.js\";\nimport { ChartTooltip, PlotGrid, wrapStyle } from \"./ChartKit.jsx\";\n\n/**\n * Pareto — bars sorted descending with a cumulative % line on the right axis, plus the\n * 80% guide. items: [{label, value}]; the component sorts and accumulates for you.\n */\nexport function ParetoChart({ items = [], width = 560, height = 260, axes = true, threshold = 80, valueFormat = fmtNum, interactive = true, style }) {\n const [hover, setHover] = React.useState(null);\n const sorted = items.slice().sort((a, b) => b.value - a.value);\n const total = sorted.reduce((s, i) => s + i.value, 0) || 1;\n let run = 0;\n const rows = sorted.map((it) => { run += it.value; return { ...it, cum: (run / total) * 100 }; });\n const pad = { l: axes ? 44 : 4, r: axes ? 38 : 4, t: 12, b: 30 };\n const { ticks, lo, hi } = niceTicks(0, Math.max(...rows.map((r) => r.value)), 4);\n const sy = scaleLin(lo, hi, height - pad.b, pad.t);\n const sc = scaleLin(0, 100, height - pad.b, pad.t);\n const slot = (width - pad.l - pad.r) / rows.length;\n const bw = Math.min(slot * 0.62, 40);\n const cx = (i) => pad.l + slot * i + slot / 2;\n const move = (e) => {\n if (!interactive) return;\n const r = e.currentTarget.getBoundingClientRect();\n const i = Math.floor((e.clientX - r.left - pad.l) / slot);\n setHover(i >= 0 && i < rows.length ? i : null);\n };\n const vital = rows.findIndex((r) => r.cum >= threshold);\n return (\n <div style={{ ...wrapStyle, ...style }}>\n <svg width={width} height={height} onMouseMove={move} onMouseLeave={() => setHover(null)} role=\"img\">\n {axes ? <PlotGrid w={width} h={height} pad={pad} ticks={ticks} sy={sy} xLabels={[]} sxLabel={() => 0} yFormat={valueFormat} /> : null}\n {hover != null ? <rect x={pad.l + slot * hover} y={pad.t} width={slot} height={height - pad.t - pad.b} fill=\"var(--bg-hover)\" /> : null}\n <line x1={pad.l} x2={width - pad.r} y1={sc(threshold)} y2={sc(threshold)} stroke=\"var(--chart-guide)\" strokeWidth=\"1\" strokeDasharray=\"3 3\" />\n <text x={width - pad.r - 2} y={sc(threshold) - 4} textAnchor=\"end\" style={{ fontFamily: \"var(--font-sans)\", fontSize: 10, fill: \"var(--chart-label)\" }}>{threshold}%</text>\n {rows.map((r, i) => (\n <rect key={r.label} x={cx(i) - bw / 2} y={sy(r.value)} width={bw} height={Math.max(1, sy(0) - sy(r.value))} rx=\"2\"\n fill={i <= vital ? \"var(--series-1)\" : \"var(--chart-track)\"} stroke={i > vital ? \"var(--border-control)\" : \"none\"}\n opacity={hover == null || hover === i ? 1 : 0.5} style={{ transition: \"opacity var(--dur-fast) var(--ease-out)\" }} />\n ))}\n <path d={linePath(rows.map((r, i) => [cx(i), sc(r.cum)]))} fill=\"none\" stroke=\"var(--chart-total)\" strokeWidth=\"1.5\" />\n {rows.map((r, i) => <circle key={i} cx={cx(i)} cy={sc(r.cum)} r={hover === i ? 3.4 : 2.4} fill=\"var(--chart-total)\" />)}\n {axes ? [0, 25, 50, 75, 100].map((t) => <text key={t} x={width - pad.r + 6} y={sc(t) + 3} style={{ fontFamily: \"var(--font-mono)\", fontSize: 10, fill: \"var(--chart-label)\" }}>{t}%</text>) : null}\n {rows.map((r, i) => <text key={\"l\" + i} x={cx(i)} y={height - pad.b + 15} textAnchor=\"middle\" style={{ fontFamily: \"var(--font-sans)\", fontSize: 10, fill: \"var(--chart-label)\" }}>{r.label}</text>)}\n </svg>\n {hover != null && interactive ? (\n <ChartTooltip x={cx(hover)} y={height / 2 - 10} flip={cx(hover) > width * 0.62} title={rows[hover].label}\n rows={[{ label: \"value\", value: valueFormat(rows[hover].value), color: \"var(--series-1)\" },\n { label: \"share\", value: ((rows[hover].value / total) * 100).toFixed(1) + \"%\" },\n { label: \"cumulative\", value: rows[hover].cum.toFixed(1) + \"%\", color: \"var(--chart-total)\" }]} />\n ) : null}\n </div>\n );\n}\n","charts/ParetoChart.vue":"<script setup>\nimport { ref, computed } from 'vue'\nimport { fmtNum, niceTicks, scaleLin, linePath } from './chart-geometry.js'\n// Sorted bars + cumulative % line and the 80% guide — the \"vital few\" chart.\nconst props = defineProps({ items:{type:Array,default:()=>[]}, width:{type:Number,default:560}, height:{type:Number,default:260}, axes:{type:Boolean,default:true}, threshold:{type:Number,default:80}, valueFormat:{type:Function,default:fmtNum}, interactive:{type:Boolean,default:true}, style:Object })\nconst hover = ref(null)\nconst g = computed(() => {\n const sorted = props.items.slice().sort((a,b)=>b.value-a.value)\n const total = sorted.reduce((s,i)=>s+i.value,0)||1\n let run = 0\n const rows = sorted.map(it=>{ run+=it.value; return Object.assign({}, it, { cum:(run/total)*100 }) })\n const pad = { l: props.axes?44:4, r: props.axes?38:4, t:12, b:30 }\n const { ticks, lo, hi } = niceTicks(0, Math.max(...rows.map(r=>r.value)), 4)\n const sy = scaleLin(lo, hi, props.height-pad.b, pad.t)\n const sc = scaleLin(0, 100, props.height-pad.b, pad.t)\n const slot = (props.width-pad.l-pad.r)/rows.length\n const bw = Math.min(slot*0.62, 40)\n const cx = i => pad.l+slot*i+slot/2\n const vital = rows.findIndex(r=>r.cum>=props.threshold)\n return { rows, total, pad, ticks, sy, sc, slot, bw, cx, vital,\n line: linePath(rows.map((r,i)=>[cx(i), sc(r.cum)])), pct:[0,25,50,75,100] }\n})\nfunction move(e){\n if(!props.interactive) return\n const r = e.currentTarget.getBoundingClientRect()\n const i = Math.floor((e.clientX-r.left-g.value.pad.l)/g.value.slot)\n hover.value = i>=0 && i<g.value.rows.length ? i : null\n}\n</script>\n<template>\n <div :style=\"{position:'relative',display:'inline-block',lineHeight:0,...style}\">\n <svg :width=\"width\" :height=\"height\" @mousemove=\"move\" @mouseleave=\"hover=null\" role=\"img\">\n <template v-if=\"axes\">\n <g v-for=\"t in g.ticks\" :key=\"t\">\n <line :x1=\"g.pad.l\" :x2=\"width-g.pad.r\" :y1=\"g.sy(t)\" :y2=\"g.sy(t)\" :stroke=\"t===0?'var(--chart-axis)':'var(--chart-grid)'\" stroke-width=\"1\"/>\n <text :x=\"g.pad.l-8\" :y=\"g.sy(t)+3\" text-anchor=\"end\" style=\"font-family:var(--font-mono);font-size:10px;fill:var(--chart-label)\">{{ valueFormat(t) }}</text>\n </g>\n </template>\n <rect v-if=\"hover!=null\" :x=\"g.pad.l+g.slot*hover\" :y=\"g.pad.t\" :width=\"g.slot\" :height=\"height-g.pad.t-g.pad.b\" fill=\"var(--bg-hover)\"/>\n <line :x1=\"g.pad.l\" :x2=\"width-g.pad.r\" :y1=\"g.sc(threshold)\" :y2=\"g.sc(threshold)\" stroke=\"var(--chart-guide)\" stroke-width=\"1\" stroke-dasharray=\"3 3\"/>\n <text :x=\"width-g.pad.r-2\" :y=\"g.sc(threshold)-4\" text-anchor=\"end\" style=\"font-family:var(--font-sans);font-size:10px;fill:var(--chart-label)\">{{ threshold }}%</text>\n <rect v-for=\"(r,i) in g.rows\" :key=\"r.label\" :x=\"g.cx(i)-g.bw/2\" :y=\"g.sy(r.value)\" :width=\"g.bw\" :height=\"Math.max(1,g.sy(0)-g.sy(r.value))\" rx=\"2\"\n :fill=\"i<=g.vital?'var(--series-1)':'var(--chart-track)'\" :stroke=\"i>g.vital?'var(--border-control)':'none'\"\n :opacity=\"hover==null||hover===i?1:0.5\" style=\"transition:opacity var(--dur-fast) var(--ease-out)\"/>\n <path :d=\"g.line\" fill=\"none\" stroke=\"var(--chart-total)\" stroke-width=\"1.5\"/>\n <circle v-for=\"(r,i) in g.rows\" :key=\"'c'+i\" :cx=\"g.cx(i)\" :cy=\"g.sc(r.cum)\" :r=\"hover===i?3.4:2.4\" fill=\"var(--chart-total)\"/>\n <text v-if=\"axes\" v-for=\"t in g.pct\" :key=\"'p'+t\" :x=\"width-g.pad.r+6\" :y=\"g.sc(t)+3\" style=\"font-family:var(--font-mono);font-size:10px;fill:var(--chart-label)\">{{ t }}%</text>\n <text v-for=\"(r,i) in g.rows\" :key=\"'l'+i\" :x=\"g.cx(i)\" :y=\"height-g.pad.b+15\" text-anchor=\"middle\" style=\"font-family:var(--font-sans);font-size:10px;fill:var(--chart-label)\">{{ r.label }}</text>\n </svg>\n <div v-if=\"hover!=null && interactive\" :style=\"{position:'absolute',left:g.cx(hover)+'px',top:(height/2-10)+'px',transform:g.cx(hover)>width*0.62?'translate(calc(-100% - 10px),-50%)':'translate(10px,-50%)',pointerEvents:'none',zIndex:5,background:'var(--bg-surface)',border:'1px solid var(--border-card)',borderRadius:'var(--radius-md)',boxShadow:'var(--shadow-menu)',padding:'7px 10px',lineHeight:'normal',whiteSpace:'nowrap'}\">\n <div style=\"font-family:var(--font-sans);font-size:var(--text-2xs);color:var(--text-muted);padding-bottom:3px\">{{ g.rows[hover].label }}</div>\n <div style=\"font-family:var(--font-mono);font-size:var(--text-xs);color:var(--text-heading)\">{{ valueFormat(g.rows[hover].value) }} · {{ ((g.rows[hover].value/g.total)*100).toFixed(1) }}% · cum {{ g.rows[hover].cum.toFixed(1) }}%</div>\n </div>\n </div>\n</template>\n","charts/CandlestickChart.jsx":"import React from \"react\";\nimport { fmtNum, niceTicks, scaleLin } from \"./chart-geometry.js\";\nimport { ChartTooltip, PlotGrid, wrapStyle } from \"./ChartKit.jsx\";\n\n/**\n * Candlestick (K-line) — OHLC price action with an optional volume band and moving average.\n * candles: [{t, o, h, l, c, v?}]. Up candles are --chart-pos, down --chart-neg; a hovered\n * candle pins the full OHLC readout. Set hollowUp for the East-Asian hollow-body convention.\n */\nexport function CandlestickChart({ candles = [], width = 620, height = 300, axes = true, volume = true, ma = 0, hollowUp = false, valueFormat = fmtNum, interactive = true, style }) {\n const [hover, setHover] = React.useState(null);\n const volH = volume ? Math.round(height * 0.22) : 0;\n const pad = { l: axes ? 46 : 4, r: 8, t: 10, b: (axes ? 22 : 4) + volH };\n const { ticks, lo, hi } = niceTicks(Math.min(...candles.map((c) => c.l)), Math.max(...candles.map((c) => c.h)), 4);\n const sy = scaleLin(lo, hi, height - pad.b, pad.t);\n const slot = (width - pad.l - pad.r) / (candles.length || 1);\n const bw = Math.max(2, Math.min(slot * 0.62, 14));\n const cx = (i) => pad.l + slot * i + slot / 2;\n const maxV = Math.max(1, ...candles.map((c) => c.v || 0));\n const vy = (v) => height - (axes ? 22 : 4) - (v / maxV) * (volH - 6);\n const avg = ma > 1 ? candles.map((_, i) => (i < ma - 1 ? null : candles.slice(i - ma + 1, i + 1).reduce((s, c) => s + c.c, 0) / ma)) : null;\n const move = (e) => {\n if (!interactive) return;\n const r = e.currentTarget.getBoundingClientRect();\n const i = Math.floor((e.clientX - r.left - pad.l) / slot);\n setHover(i >= 0 && i < candles.length ? i : null);\n };\n const h = hover != null ? candles[hover] : null;\n const up = (c) => c.c >= c.o;\n return (\n <div style={{ ...wrapStyle, ...style }}>\n <svg width={width} height={height} onMouseMove={move} onMouseLeave={() => setHover(null)} role=\"img\">\n {axes ? <PlotGrid w={width} h={height} pad={pad} ticks={ticks} sy={sy} xLabels={[]} sxLabel={() => 0} yFormat={valueFormat} /> : null}\n {hover != null ? <rect x={pad.l + slot * hover} y={pad.t} width={slot} height={height - pad.t - (axes ? 22 : 4)} fill=\"var(--bg-hover)\" /> : null}\n {avg ? <path d={\"M\" + avg.map((v, i) => (v == null ? null : cx(i).toFixed(1) + \" \" + sy(v).toFixed(1))).filter(Boolean).join(\" L\")} fill=\"none\" stroke=\"var(--series-8)\" strokeWidth=\"1.3\" /> : null}\n {candles.map((c, i) => {\n const col = up(c) ? \"var(--chart-pos)\" : \"var(--chart-neg)\";\n const yO = sy(c.o), yC = sy(c.c);\n const top = Math.min(yO, yC), body = Math.max(1.5, Math.abs(yC - yO));\n const solid = !hollowUp || !up(c);\n return (\n <g key={i} opacity={hover == null || hover === i ? 1 : 0.5} style={{ transition: \"opacity var(--dur-fast) var(--ease-out)\" }}>\n <line x1={cx(i)} x2={cx(i)} y1={sy(c.h)} y2={sy(c.l)} stroke={col} strokeWidth=\"1\" />\n <rect x={cx(i) - bw / 2} y={top} width={bw} height={body} fill={solid ? col : \"var(--bg-surface)\"} stroke={col} strokeWidth=\"1\" />\n {volume && c.v != null ? <rect x={cx(i) - bw / 2} y={vy(c.v)} width={bw} height={Math.max(1, height - (axes ? 22 : 4) - vy(c.v))} fill={col} opacity=\"0.35\" /> : null}\n </g>\n );\n })}\n {volume ? <line x1={pad.l} x2={width - pad.r} y1={height - (axes ? 22 : 4)} y2={height - (axes ? 22 : 4)} stroke=\"var(--chart-axis)\" strokeWidth=\"1\" /> : null}\n {axes ? candles.map((c, i) => (i % Math.ceil(candles.length / 6) ? null : (\n <text key={\"t\" + i} x={cx(i)} y={height - 6} textAnchor=\"middle\" style={{ fontFamily: \"var(--font-sans)\", fontSize: 10, fill: \"var(--chart-label)\" }}>{c.t}</text>\n ))) : null}\n </svg>\n {h && interactive ? (\n <ChartTooltip x={cx(hover)} y={height / 2 - 20} flip={cx(hover) > width * 0.62} title={h.t}\n rows={[{ label: \"open\", value: valueFormat(h.o) }, { label: \"high\", value: valueFormat(h.h) }, { label: \"low\", value: valueFormat(h.l) },\n { label: \"close\", value: valueFormat(h.c), color: up(h) ? \"var(--chart-pos)\" : \"var(--chart-neg)\" },\n { label: \"change\", value: ((h.c - h.o) >= 0 ? \"+\" : \"\") + (((h.c - h.o) / (h.o || 1)) * 100).toFixed(2) + \"%\" }]\n .concat(h.v != null ? [{ label: \"volume\", value: fmtNum(h.v) }] : [])} />\n ) : null}\n </div>\n );\n}\n","charts/CandlestickChart.vue":"<script setup>\nimport { ref, computed } from 'vue'\nimport { fmtNum, niceTicks, scaleLin } from './chart-geometry.js'\n// OHLC K-line with optional volume band and moving average.\nconst props = defineProps({ candles:{type:Array,default:()=>[]}, width:{type:Number,default:620}, height:{type:Number,default:300}, axes:{type:Boolean,default:true}, volume:{type:Boolean,default:true}, ma:{type:Number,default:0}, hollowUp:Boolean, valueFormat:{type:Function,default:fmtNum}, interactive:{type:Boolean,default:true}, style:Object })\nconst hover = ref(null)\nconst g = computed(() => {\n const volH = props.volume ? Math.round(props.height*0.22) : 0\n const axisB = props.axes ? 22 : 4\n const pad = { l: props.axes?46:4, r:8, t:10, b: axisB+volH }\n const { ticks, lo, hi } = niceTicks(Math.min(...props.candles.map(c=>c.l)), Math.max(...props.candles.map(c=>c.h)), 4)\n const sy = scaleLin(lo, hi, props.height-pad.b, pad.t)\n const slot = (props.width-pad.l-pad.r)/(props.candles.length||1)\n const bw = Math.max(2, Math.min(slot*0.62, 14))\n const cx = i => pad.l+slot*i+slot/2\n const maxV = Math.max(1, ...props.candles.map(c=>c.v||0))\n const vy = v => props.height-axisB-(v/maxV)*(volH-6)\n const avg = props.ma>1 ? props.candles.map((_,i)=> i<props.ma-1 ? null : props.candles.slice(i-props.ma+1,i+1).reduce((s,c)=>s+c.c,0)/props.ma) : null\n const bars = props.candles.map((c,i)=>{\n const up = c.c>=c.o, col = up?'var(--chart-pos)':'var(--chart-neg)'\n const yO=sy(c.o), yC=sy(c.c)\n return { c, i, up, col, x:cx(i), bw, top:Math.min(yO,yC), body:Math.max(1.5,Math.abs(yC-yO)), yH:sy(c.h), yL:sy(c.l),\n solid: !props.hollowUp || !up, vY: c.v!=null?vy(c.v):null, vH: c.v!=null?Math.max(1, props.height-axisB-vy(c.v)):0 }\n })\n const maPath = avg ? 'M'+avg.map((v,i)=> v==null?null:cx(i).toFixed(1)+' '+sy(v).toFixed(1)).filter(Boolean).join(' L') : null\n return { pad, ticks, sy, slot, cx, bars, maPath, axisB, every: Math.ceil(props.candles.length/6) }\n})\nfunction move(e){\n if(!props.interactive) return\n const r = e.currentTarget.getBoundingClientRect()\n const i = Math.floor((e.clientX-r.left-g.value.pad.l)/g.value.slot)\n hover.value = i>=0 && i<props.candles.length ? i : null\n}\n</script>\n<template>\n <div :style=\"{position:'relative',display:'inline-block',lineHeight:0,...style}\">\n <svg :width=\"width\" :height=\"height\" @mousemove=\"move\" @mouseleave=\"hover=null\" role=\"img\">\n <template v-if=\"axes\">\n <g v-for=\"t in g.ticks\" :key=\"t\">\n <line :x1=\"g.pad.l\" :x2=\"width-g.pad.r\" :y1=\"g.sy(t)\" :y2=\"g.sy(t)\" :stroke=\"t===0?'var(--chart-axis)':'var(--chart-grid)'\" stroke-width=\"1\"/>\n <text :x=\"g.pad.l-8\" :y=\"g.sy(t)+3\" text-anchor=\"end\" style=\"font-family:var(--font-mono);font-size:10px;fill:var(--chart-label)\">{{ valueFormat(t) }}</text>\n </g>\n </template>\n <rect v-if=\"hover!=null\" :x=\"g.pad.l+g.slot*hover\" :y=\"g.pad.t\" :width=\"g.slot\" :height=\"height-g.pad.t-g.axisB\" fill=\"var(--bg-hover)\"/>\n <path v-if=\"g.maPath\" :d=\"g.maPath\" fill=\"none\" stroke=\"var(--series-8)\" stroke-width=\"1.3\"/>\n <g v-for=\"b in g.bars\" :key=\"b.i\" :opacity=\"hover==null||hover===b.i?1:0.5\" style=\"transition:opacity var(--dur-fast) var(--ease-out)\">\n <line :x1=\"b.x\" :x2=\"b.x\" :y1=\"b.yH\" :y2=\"b.yL\" :stroke=\"b.col\" stroke-width=\"1\"/>\n <rect :x=\"b.x-b.bw/2\" :y=\"b.top\" :width=\"b.bw\" :height=\"b.body\" :fill=\"b.solid?b.col:'var(--bg-surface)'\" :stroke=\"b.col\" stroke-width=\"1\"/>\n <rect v-if=\"volume && b.vY!=null\" :x=\"b.x-b.bw/2\" :y=\"b.vY\" :width=\"b.bw\" :height=\"b.vH\" :fill=\"b.col\" opacity=\"0.35\"/>\n </g>\n <line v-if=\"volume\" :x1=\"g.pad.l\" :x2=\"width-g.pad.r\" :y1=\"height-g.axisB\" :y2=\"height-g.axisB\" stroke=\"var(--chart-axis)\" stroke-width=\"1\"/>\n <text v-if=\"axes\" v-for=\"b in g.bars.filter(b=>b.i%g.every===0)\" :key=\"'t'+b.i\" :x=\"b.x\" :y=\"height-6\" text-anchor=\"middle\" style=\"font-family:var(--font-sans);font-size:10px;fill:var(--chart-label)\">{{ b.c.t }}</text>\n </svg>\n <div v-if=\"hover!=null && interactive\" :style=\"{position:'absolute',left:g.cx(hover)+'px',top:(height/2-20)+'px',transform:g.cx(hover)>width*0.62?'translate(calc(-100% - 10px),-50%)':'translate(10px,-50%)',pointerEvents:'none',zIndex:5,background:'var(--bg-surface)',border:'1px solid var(--border-card)',borderRadius:'var(--radius-md)',boxShadow:'var(--shadow-menu)',padding:'7px 10px',lineHeight:'normal',whiteSpace:'nowrap'}\">\n <div style=\"font-family:var(--font-sans);font-size:var(--text-2xs);color:var(--text-muted);padding-bottom:3px\">{{ candles[hover].t }}</div>\n <div style=\"font-family:var(--font-mono);font-size:var(--text-xs);color:var(--text-heading)\">O {{ valueFormat(candles[hover].o) }} · H {{ valueFormat(candles[hover].h) }} · L {{ valueFormat(candles[hover].l) }} · C {{ valueFormat(candles[hover].c) }}</div>\n <div :style=\"{fontFamily:'var(--font-mono)',fontSize:'var(--text-2xs)',color:candles[hover].c>=candles[hover].o?'var(--chart-pos)':'var(--chart-neg)'}\">\n {{ (candles[hover].c-candles[hover].o>=0?'+':'')+(((candles[hover].c-candles[hover].o)/(candles[hover].o||1))*100).toFixed(2) }}%<span v-if=\"candles[hover].v!=null\" style=\"color:var(--text-muted)\"> · vol {{ fmtNum(candles[hover].v) }}</span>\n </div>\n </div>\n </div>\n</template>\n","charts/SankeyChart.jsx":"import React from \"react\";\nimport { fmtNum, sankey, seriesColor } from \"./chart-geometry.js\";\nimport { ChartTooltip, wrapStyle } from \"./ChartKit.jsx\";\n\n/**\n * Sankey flow diagram — budget allocation, headcount movement, data lineage.\n * nodes: [{id, label, color?}]; links: [{source, target, value}]. Depth (column) is\n * derived from the longest path, so you never position nodes by hand. Hovering a\n * ribbon or a node isolates that flow.\n */\nexport function SankeyChart({ nodes = [], links = [], width = 620, height = 320, nodeWidth = 13, nodePadding = 16, valueFormat = fmtNum, interactive = true, labelWidth = 96, style }) {\n const [hover, setHover] = React.useState(null);\n const pad = { l: 6, r: labelWidth, t: 8, b: 8 };\n const inner = { w: width - pad.l - pad.r, h: height - pad.t - pad.b };\n const { nodes: laid, ribbons } = React.useMemo(\n () => sankey(nodes, links, inner.w, inner.h, nodeWidth, nodePadding),\n [nodes, links, inner.w, inner.h, nodeWidth, nodePadding]\n );\n const byId = Object.fromEntries(laid.map((n) => [n.id, n]));\n const colorOf = (n) => n.color || seriesColor(laid.indexOf(n));\n const litRibbon = (r) => hover == null ? 0.42\n : hover.kind === \"ribbon\" ? (hover.i === ribbons.indexOf(r) ? 0.78 : 0.12)\n : (r.link.source === hover.id || r.link.target === hover.id ? 0.7 : 0.1);\n const h = hover;\n const tip = h ? (h.kind === \"ribbon\"\n ? { x: (ribbons[h.i].source.x + ribbons[h.i].target.x) / 2 + pad.l, y: 0, title: byId[ribbons[h.i].link.source].label + \" → \" + byId[ribbons[h.i].link.target].label,\n rows: [{ label: \"flow\", value: valueFormat(ribbons[h.i].link.value), color: colorOf(byId[ribbons[h.i].link.source]) }] }\n : { x: byId[h.id].x + pad.l + nodeWidth, y: byId[h.id].y + pad.t + byId[h.id].h / 2, title: byId[h.id].label,\n rows: [{ label: \"total\", value: valueFormat(byId[h.id].value), color: colorOf(byId[h.id]) },\n { label: \"links\", value: String(byId[h.id].in.length + byId[h.id].out.length) }] }) : null;\n return (\n <div style={{ ...wrapStyle, ...style }}>\n <svg width={width} height={height} role=\"img\">\n <g transform={\"translate(\" + pad.l + \",\" + pad.t + \")\"}>\n {ribbons.map((r, i) => (\n <path key={i} d={r.d} fill={colorOf(byId[r.link.source])} opacity={litRibbon(r)}\n onMouseEnter={interactive ? () => setHover({ kind: \"ribbon\", i }) : undefined} onMouseLeave={interactive ? () => setHover(null) : undefined}\n style={{ transition: \"opacity var(--dur-fast) var(--ease-out)\", cursor: interactive ? \"default\" : undefined }} />\n ))}\n {laid.map((n) => {\n const c = colorOf(n);\n const rightSide = n.x > inner.w * 0.55;\n return (\n <g key={n.id} onMouseEnter={interactive ? () => setHover({ kind: \"node\", id: n.id }) : undefined} onMouseLeave={interactive ? () => setHover(null) : undefined}\n style={{ cursor: interactive ? \"default\" : undefined }}>\n <rect x={n.x} y={n.y} width={n.w} height={n.h} rx=\"2\" fill={c} opacity={hover == null || (hover.kind === \"node\" && hover.id === n.id) ? 1 : 0.55} />\n <text x={rightSide ? n.x + n.w + 6 : n.x + n.w + 6} y={n.y + n.h / 2 + 3.5}\n style={{ fontFamily: \"var(--font-sans)\", fontSize: 11, fill: \"var(--text-heading)\" }}>{n.label}</text>\n <text x={n.x + n.w + 6} y={n.y + n.h / 2 + 15}\n style={{ fontFamily: \"var(--font-mono)\", fontSize: 9.5, fill: \"var(--chart-label)\" }}>{valueFormat(n.value)}</text>\n </g>\n );\n })}\n </g>\n </svg>\n {tip && interactive ? <ChartTooltip x={tip.x} y={tip.y || height / 2} flip={tip.x > width * 0.6} title={tip.title} rows={tip.rows} /> : null}\n </div>\n );\n}\n","charts/SankeyChart.vue":"<script setup>\nimport { ref, computed } from 'vue'\nimport { fmtNum, sankey, SERIES } from './chart-geometry.js'\n// Flow diagram; node columns are derived from the longest path, never positioned by hand.\nconst props = defineProps({ nodes:{type:Array,default:()=>[]}, links:{type:Array,default:()=>[]}, width:{type:Number,default:620}, height:{type:Number,default:320}, nodeWidth:{type:Number,default:13}, nodePadding:{type:Number,default:16}, labelWidth:{type:Number,default:96}, valueFormat:{type:Function,default:fmtNum}, interactive:{type:Boolean,default:true}, style:Object })\nconst hover = ref(null)\nconst g = computed(() => {\n const pad = { l:6, r:props.labelWidth, t:8, b:8 }\n const inner = { w: props.width-pad.l-pad.r, h: props.height-pad.t-pad.b }\n const laid = sankey(props.nodes, props.links, inner.w, inner.h, props.nodeWidth, props.nodePadding)\n const byId = {}; laid.nodes.forEach(n=>{ byId[n.id]=n })\n const color = n => n.color || SERIES[laid.nodes.indexOf(n)%SERIES.length]\n return { pad, inner, laid, byId, color,\n ribbons: laid.ribbons.map((r,i)=>Object.assign({}, r, { i, fill: color(byId[r.link.source]) })) }\n})\nfunction ribbonOpacity(r){\n const h = hover.value\n if (!h) return 0.42\n if (h.kind==='ribbon') return h.i===r.i ? 0.78 : 0.12\n return (r.link.source===h.id || r.link.target===h.id) ? 0.7 : 0.1\n}\nconst tip = computed(()=>{\n const h = hover.value\n if (!h) return null\n if (h.kind==='ribbon') {\n const r = g.value.ribbons[h.i]\n return { x:(r.source.x+r.target.x)/2+g.value.pad.l, y:props.height/2,\n title: g.value.byId[r.link.source].label+' → '+g.value.byId[r.link.target].label, value: props.valueFormat(r.link.value) }\n }\n const n = g.value.byId[h.id]\n return { x:n.x+g.value.pad.l+props.nodeWidth, y:n.y+g.value.pad.t+n.h/2, title:n.label, value: props.valueFormat(n.value) }\n})\n</script>\n<template>\n <div :style=\"{position:'relative',display:'inline-block',lineHeight:0,...style}\">\n <svg :width=\"width\" :height=\"height\" role=\"img\">\n <g :transform=\"'translate('+g.pad.l+','+g.pad.t+')'\">\n <path v-for=\"r in g.ribbons\" :key=\"r.i\" :d=\"r.d\" :fill=\"r.fill\" :opacity=\"ribbonOpacity(r)\"\n @mouseenter=\"interactive && (hover={kind:'ribbon',i:r.i})\" @mouseleave=\"interactive && (hover=null)\"\n style=\"transition:opacity var(--dur-fast) var(--ease-out)\"/>\n <g v-for=\"n in g.laid.nodes\" :key=\"n.id\" @mouseenter=\"interactive && (hover={kind:'node',id:n.id})\" @mouseleave=\"interactive && (hover=null)\">\n <rect :x=\"n.x\" :y=\"n.y\" :width=\"n.w\" :height=\"n.h\" rx=\"2\" :fill=\"g.color(n)\" :opacity=\"hover==null||(hover.kind==='node'&&hover.id===n.id)?1:0.55\"/>\n <text :x=\"n.x+n.w+6\" :y=\"n.y+n.h/2+3.5\" style=\"font-family:var(--font-sans);font-size:11px;fill:var(--text-heading)\">{{ n.label }}</text>\n <text :x=\"n.x+n.w+6\" :y=\"n.y+n.h/2+15\" style=\"font-family:var(--font-mono);font-size:9.5px;fill:var(--chart-label)\">{{ valueFormat(n.value) }}</text>\n </g>\n </g>\n </svg>\n <div v-if=\"tip && interactive\" :style=\"{position:'absolute',left:tip.x+'px',top:tip.y+'px',transform:tip.x>width*0.6?'translate(calc(-100% - 10px),-50%)':'translate(10px,-50%)',pointerEvents:'none',zIndex:5,background:'var(--bg-surface)',border:'1px solid var(--border-card)',borderRadius:'var(--radius-md)',boxShadow:'var(--shadow-menu)',padding:'7px 10px',lineHeight:'normal',whiteSpace:'nowrap'}\">\n <div style=\"font-family:var(--font-sans);font-size:var(--text-2xs);color:var(--text-muted);padding-bottom:3px\">{{ tip.title }}</div>\n <div style=\"font-family:var(--font-mono);font-size:var(--text-xs);color:var(--text-heading)\">{{ tip.value }}</div>\n </div>\n </div>\n</template>\n","charts/ChordChart.jsx":"import React from \"react\";\nimport { fmtNum, chord, chordRibbon, arcPath, polar, seriesColor } from \"./chart-geometry.js\";\nimport { ChartTooltip, wrapStyle } from \"./ChartKit.jsx\";\n\n/**\n * Chord diagram — the circular Sankey. A square `matrix` of flows between `labels`\n * (matrix[i][j] = i → j). Use it when flows are many-to-many and cyclical, where a\n * left-to-right Sankey would cross itself. Hovering an arc isolates that node's flows.\n */\nexport function ChordChart({ labels = [], matrix = [], size = 380, thickness = 14, gap = 2, valueFormat = fmtNum, interactive = true, style }) {\n const [hover, setHover] = React.useState(null);\n const cx = size / 2, cy = size / 2;\n const rO = size / 2 - 58, rI = rO - thickness;\n const { arcs, ribbons, totals, grand } = React.useMemo(() => chord(matrix, gap), [matrix, gap]);\n const colorOf = (i) => seriesColor(i);\n const lit = (r) => hover == null ? 0.42 : hover.kind === \"ribbon\" ? (hover.k === r.k ? 0.8 : 0.08) : (r.i === hover.i || r.j === hover.i ? 0.72 : 0.07);\n const keyed = ribbons.map((r, k) => ({ ...r, k }));\n const h = hover;\n const tipRows = () => {\n if (h.kind === \"ribbon\") {\n const r = keyed[h.k];\n return { title: labels[r.i] + \" → \" + labels[r.j], rows: [{ label: \"flow\", value: valueFormat(r.value), color: colorOf(r.i) }, { label: \"share\", value: ((r.value / grand) * 100).toFixed(1) + \"%\" }] };\n }\n const out = matrix[h.i].reduce((s, v, j) => s + (j === h.i ? 0 : v), 0);\n const inc = matrix.reduce((s, row, j) => s + (j === h.i ? 0 : row[h.i]), 0);\n return { title: labels[h.i], rows: [{ label: \"outgoing\", value: valueFormat(out), color: colorOf(h.i) }, { label: \"incoming\", value: valueFormat(inc) }, { label: \"total\", value: valueFormat(totals[h.i]) }] };\n };\n const tipAt = h ? polar(cx, cy, rI * 0.55, h.kind === \"ribbon\" ? (keyed[h.k].a0 + keyed[h.k].b1) / 2 : (arcs[h.i].a0 + arcs[h.i].a1) / 2) : null;\n return (\n <div style={{ ...wrapStyle, ...style }}>\n <svg width={size} height={size} role=\"img\">\n {keyed.map((r) => (\n <path key={r.k} d={chordRibbon(cx, cy, rI - 1, r.a0, r.a1, r.b0, r.b1)} fill={colorOf(r.i)} opacity={lit(r)}\n onMouseEnter={interactive ? () => setHover({ kind: \"ribbon\", k: r.k }) : undefined} onMouseLeave={interactive ? () => setHover(null) : undefined}\n style={{ transition: \"opacity var(--dur-fast) var(--ease-out)\" }} />\n ))}\n {arcs.map((a) => {\n const mid = (a.a0 + a.a1) / 2;\n const [lx, ly] = polar(cx, cy, rO + 12, mid);\n const on = hover && hover.kind === \"node\" && hover.i === a.i;\n const anchor = Math.abs(lx - cx) < 10 ? \"middle\" : lx > cx ? \"start\" : \"end\";\n return (\n <g key={a.i} onMouseEnter={interactive ? () => setHover({ kind: \"node\", i: a.i }) : undefined} onMouseLeave={interactive ? () => setHover(null) : undefined}\n style={{ cursor: interactive ? \"default\" : undefined }}>\n <path d={arcPath(cx, cy, rO, rI, a.a0, a.a1)} fill={colorOf(a.i)} opacity={hover == null || on ? 1 : 0.4} style={{ transition: \"opacity var(--dur-fast) var(--ease-out)\" }} />\n <text x={lx} y={ly + 3} textAnchor={anchor} style={{ fontFamily: \"var(--font-sans)\", fontSize: 10.5, fill: on ? \"var(--text-heading)\" : \"var(--text-secondary)\" }}>{labels[a.i]}</text>\n <text x={lx} y={ly + 14} textAnchor={anchor} style={{ fontFamily: \"var(--font-mono)\", fontSize: 9, fill: \"var(--chart-label)\" }}>{valueFormat(a.value)}</text>\n </g>\n );\n })}\n </svg>\n {h && interactive ? <ChartTooltip x={tipAt[0]} y={tipAt[1]} flip={tipAt[0] > size * 0.55} {...tipRows()} /> : null}\n </div>\n );\n}\n","charts/ChordChart.vue":"<script setup>\nimport { ref, computed } from 'vue'\nimport { fmtNum, chord, chordRibbon, arcPath, polar, SERIES } from './chart-geometry.js'\n// Circular Sankey — many-to-many, cyclical flows. matrix[i][j] = flow from i to j.\nconst props = defineProps({ labels:{type:Array,default:()=>[]}, matrix:{type:Array,default:()=>[]}, size:{type:Number,default:380}, thickness:{type:Number,default:14}, gap:{type:Number,default:2}, valueFormat:{type:Function,default:fmtNum}, interactive:{type:Boolean,default:true}, style:Object })\nconst hover = ref(null)\nconst g = computed(() => {\n const cx = props.size/2, cy = props.size/2\n const rO = props.size/2-58, rI = rO-props.thickness\n const { arcs, ribbons, totals, grand } = chord(props.matrix, props.gap)\n return { cx, cy, rO, rI, grand, totals,\n arcs: arcs.map(a=>{\n const mid=(a.a0+a.a1)/2, l=polar(cx,cy,rO+12,mid)\n return Object.assign({}, a, { mid, d: arcPath(cx,cy,rO,rI,a.a0,a.a1), color: SERIES[a.i%SERIES.length],\n lx:l[0], ly:l[1], anchor: Math.abs(l[0]-cx)<10 ? 'middle' : (l[0]>cx?'start':'end') })\n }),\n ribbons: ribbons.map((r,k)=>Object.assign({}, r, { k, d: chordRibbon(cx,cy,rI-1,r.a0,r.a1,r.b0,r.b1), color: SERIES[r.i%SERIES.length] })) }\n})\nfunction op(r){\n const h = hover.value\n if (!h) return 0.42\n return h.kind==='ribbon' ? (h.k===r.k?0.8:0.08) : ((r.i===h.i||r.j===h.i)?0.72:0.07)\n}\nconst tip = computed(()=>{\n const h = hover.value\n if (!h) return null\n if (h.kind==='ribbon') {\n const r = g.value.ribbons[h.k], p = polar(g.value.cx,g.value.cy,g.value.rI*0.55,(r.a0+r.b1)/2)\n return { x:p[0], y:p[1], title: props.labels[r.i]+' → '+props.labels[r.j], value: props.valueFormat(r.value)+' · '+((r.value/g.value.grand)*100).toFixed(1)+'%' }\n }\n const a = g.value.arcs[h.i], p = polar(g.value.cx,g.value.cy,g.value.rI*0.55,a.mid)\n const out = props.matrix[h.i].reduce((s,v,j)=>s+(j===h.i?0:v),0)\n const inc = props.matrix.reduce((s,row,j)=>s+(j===h.i?0:row[h.i]),0)\n return { x:p[0], y:p[1], title: props.labels[h.i], value: 'out '+props.valueFormat(out)+' · in '+props.valueFormat(inc) }\n})\n</script>\n<template>\n <div :style=\"{position:'relative',display:'inline-block',lineHeight:0,...style}\">\n <svg :width=\"size\" :height=\"size\" role=\"img\">\n <path v-for=\"r in g.ribbons\" :key=\"r.k\" :d=\"r.d\" :fill=\"r.color\" :opacity=\"op(r)\"\n @mouseenter=\"interactive && (hover={kind:'ribbon',k:r.k})\" @mouseleave=\"interactive && (hover=null)\"\n style=\"transition:opacity var(--dur-fast) var(--ease-out)\"/>\n <g v-for=\"a in g.arcs\" :key=\"a.i\" @mouseenter=\"interactive && (hover={kind:'node',i:a.i})\" @mouseleave=\"interactive && (hover=null)\">\n <path :d=\"a.d\" :fill=\"a.color\" :opacity=\"hover==null||(hover.kind==='node'&&hover.i===a.i)?1:0.4\" style=\"transition:opacity var(--dur-fast) var(--ease-out)\"/>\n <text :x=\"a.lx\" :y=\"a.ly+3\" :text-anchor=\"a.anchor\" style=\"font-family:var(--font-sans);font-size:10.5px;fill:var(--text-secondary)\">{{ labels[a.i] }}</text>\n <text :x=\"a.lx\" :y=\"a.ly+14\" :text-anchor=\"a.anchor\" style=\"font-family:var(--font-mono);font-size:9px;fill:var(--chart-label)\">{{ valueFormat(a.value) }}</text>\n </g>\n </svg>\n <div v-if=\"tip && interactive\" :style=\"{position:'absolute',left:tip.x+'px',top:tip.y+'px',transform:tip.x>size*0.55?'translate(calc(-100% - 10px),-50%)':'translate(10px,-50%)',pointerEvents:'none',zIndex:5,background:'var(--bg-surface)',border:'1px solid var(--border-card)',borderRadius:'var(--radius-md)',boxShadow:'var(--shadow-menu)',padding:'7px 10px',lineHeight:'normal',whiteSpace:'nowrap'}\">\n <div style=\"font-family:var(--font-sans);font-size:var(--text-2xs);color:var(--text-muted);padding-bottom:3px\">{{ tip.title }}</div>\n <div style=\"font-family:var(--font-mono);font-size:var(--text-xs);color:var(--text-heading)\">{{ tip.value }}</div>\n </div>\n </div>\n</template>\n","charts/SunburstChart.jsx":"import React from \"react\";\nimport { fmtNum, arcPath, polar, seriesColor } from \"./chart-geometry.js\";\nimport { ChartTooltip, wrapStyle } from \"./ChartKit.jsx\";\n\n/**\n * Sunburst — a hierarchy as concentric rings (the radial treemap). data: a root\n * {label, children:[{label, value, children?}]} tree; leaf values roll up. Clicking a\n * ring zooms into that branch, and the center returns to the parent.\n */\nexport function SunburstChart({ data, size = 320, ringWidth, valueFormat = fmtNum, interactive = true, style }) {\n const [zoom, setZoom] = React.useState([]);\n const [hover, setHover] = React.useState(null);\n const roll = (n) => (n.children && n.children.length ? { ...n, value: n.children.reduce((s, c) => s + roll(c).value, 0), children: n.children.map(roll) } : { ...n, value: n.value || 0 });\n const rooted = data ? roll(data) : { label: \"\", value: 0, children: [] };\n const focus = zoom.reduce((n, i) => (n.children ? n.children[i] : n), rooted);\n const depthOf = (n) => (n.children && n.children.length ? 1 + Math.max(...n.children.map(depthOf)) : 0);\n const rings = Math.max(1, depthOf(focus));\n const rInner = size * 0.17;\n const rw = ringWidth || (size / 2 - 6 - rInner) / rings;\n const segs = [];\n const walk = (node, depth, a0, a1, path, colorIdx) => {\n (node.children || []).forEach((c, ci) => {\n const span = (c.value / (node.value || 1)) * (a1 - a0);\n const idx = depth === 0 ? ci : colorIdx;\n segs.push({ node: c, depth, a0, a1: a0 + span, path: [...path, ci], colorIdx: idx });\n walk(c, depth + 1, a0, a0 + span, [...path, ci], idx);\n a0 += span;\n });\n };\n walk(focus, 0, 0, 360, [], 0);\n const h = hover != null ? segs[hover] : null;\n const tipAt = h ? polar(size / 2, size / 2, rInner + rw * (h.depth + 0.5), (h.a0 + h.a1) / 2) : null;\n return (\n <div style={{ ...wrapStyle, ...style }}>\n <div style={{ position: \"relative\", width: size, height: size }}>\n <svg width={size} height={size} role=\"img\">\n {segs.map((s, i) => (\n <path key={i} d={arcPath(size / 2, size / 2, rInner + rw * (s.depth + 1) - 1.5, rInner + rw * s.depth, s.a0 + 0.35, Math.max(s.a0 + 0.5, s.a1 - 0.35))}\n fill={seriesColor(s.colorIdx)} opacity={hover == null ? 0.92 - s.depth * 0.16 : hover === i ? 1 : 0.28}\n onMouseEnter={interactive ? () => setHover(i) : undefined} onMouseLeave={interactive ? () => setHover(null) : undefined}\n onClick={interactive && s.node.children ? () => { setZoom([...zoom, ...s.path]); setHover(null); } : undefined}\n style={{ transition: \"opacity var(--dur-fast) var(--ease-out)\", cursor: interactive && s.node.children ? \"pointer\" : \"default\" }} />\n ))}\n <circle cx={size / 2} cy={size / 2} r={rInner - 2} fill=\"var(--bg-surface)\" stroke=\"var(--border-subtle)\" strokeWidth=\"1\"\n onClick={interactive && zoom.length ? () => setZoom(zoom.slice(0, -1)) : undefined} style={{ cursor: interactive && zoom.length ? \"pointer\" : \"default\" }} />\n </svg>\n <div style={{ position: \"absolute\", inset: 0, display: \"flex\", flexDirection: \"column\", alignItems: \"center\", justifyContent: \"center\", pointerEvents: \"none\", lineHeight: \"normal\", gap: 2 }}>\n <span style={{ fontFamily: \"var(--font-sans)\", fontWeight: \"var(--weight-light)\", fontSize: Math.round(size / 13), color: \"var(--text-heading)\" }}>{valueFormat(focus.value)}</span>\n <span style={{ fontFamily: \"var(--font-sans)\", fontSize: \"var(--text-2xs)\", color: \"var(--text-muted)\", maxWidth: rInner * 1.7, textAlign: \"center\", whiteSpace: \"nowrap\", overflow: \"hidden\", textOverflow: \"ellipsis\" }}>\n {zoom.length ? focus.label : focus.label || \"Total\"}\n </span>\n {zoom.length ? <span style={{ fontFamily: \"var(--font-sans)\", fontSize: 9, color: \"var(--action-primary)\" }}>↑ back</span> : null}\n </div>\n {h && interactive ? (\n <ChartTooltip x={tipAt[0]} y={tipAt[1]} flip={tipAt[0] > size * 0.55} title={h.node.label}\n rows={[{ label: \"value\", value: valueFormat(h.node.value), color: seriesColor(h.colorIdx) }, { label: \"of parent\", value: ((h.node.value / (focus.value || 1)) * 100).toFixed(1) + \"%\" }]} />\n ) : null}\n </div>\n </div>\n );\n}\n","charts/SunburstChart.vue":"<script setup>\nimport { ref, computed } from 'vue'\nimport { fmtNum, arcPath, polar, SERIES } from './chart-geometry.js'\n// Hierarchy as concentric rings; click to zoom a branch, click the center to go back.\nconst props = defineProps({ data:Object, size:{type:Number,default:320}, ringWidth:Number, valueFormat:{type:Function,default:fmtNum}, interactive:{type:Boolean,default:true}, style:Object })\nconst zoom = ref([]); const hover = ref(null)\nfunction roll(n){ return n.children && n.children.length ? Object.assign({}, n, { value: n.children.reduce((s,c)=>s+roll(c).value,0), children: n.children.map(roll) }) : Object.assign({}, n, { value: n.value||0 }) }\nconst g = computed(() => {\n const rooted = props.data ? roll(props.data) : { label:'', value:0, children:[] }\n const focus = zoom.value.reduce((n,i)=> n.children ? n.children[i] : n, rooted)\n const depthOf = n => n.children && n.children.length ? 1+Math.max(...n.children.map(depthOf)) : 0\n const rings = Math.max(1, depthOf(focus))\n const rInner = props.size*0.17\n const rw = props.ringWidth || (props.size/2-6-rInner)/rings\n const segs = []\n const walk = (node, depth, a0, a1, path, colorIdx) => {\n (node.children||[]).forEach((c,ci)=>{\n const span = (c.value/(node.value||1))*(a1-a0)\n const idx = depth===0 ? ci : colorIdx\n segs.push({ node:c, depth, a0, a1:a0+span, path:[...path,ci], colorIdx:idx,\n d: arcPath(props.size/2, props.size/2, rInner+rw*(depth+1)-1.5, rInner+rw*depth, a0+0.35, Math.max(a0+0.5, a0+span-0.35)),\n color: SERIES[idx%SERIES.length] })\n walk(c, depth+1, a0, a0+span, [...path,ci], idx)\n a0 += span\n })\n }\n walk(focus, 0, 0, 360, [], 0)\n return { focus, rInner, rw, segs }\n})\nconst tip = computed(()=>{\n if (hover.value==null) return null\n const s = g.value.segs[hover.value]\n const p = polar(props.size/2, props.size/2, g.value.rInner+g.value.rw*(s.depth+0.5), (s.a0+s.a1)/2)\n return { x:p[0], y:p[1], title:s.node.label, value: props.valueFormat(s.node.value)+' · '+((s.node.value/(g.value.focus.value||1))*100).toFixed(1)+'%' }\n})\n</script>\n<template>\n <div :style=\"{position:'relative',display:'inline-block',lineHeight:0,...style}\">\n <div :style=\"{position:'relative',width:size+'px',height:size+'px'}\">\n <svg :width=\"size\" :height=\"size\" role=\"img\">\n <path v-for=\"(s,i) in g.segs\" :key=\"i\" :d=\"s.d\" :fill=\"s.color\"\n :opacity=\"hover==null ? 0.92-s.depth*0.16 : (hover===i?1:0.28)\"\n @mouseenter=\"interactive && (hover=i)\" @mouseleave=\"interactive && (hover=null)\"\n @click=\"interactive && s.node.children && (zoom=[...zoom,...s.path], hover=null)\"\n :style=\"{transition:'opacity var(--dur-fast) var(--ease-out)',cursor:interactive&&s.node.children?'pointer':'default'}\"/>\n <circle :cx=\"size/2\" :cy=\"size/2\" :r=\"g.rInner-2\" fill=\"var(--bg-surface)\" stroke=\"var(--border-subtle)\" stroke-width=\"1\"\n @click=\"interactive && zoom.length && (zoom=zoom.slice(0,-1))\" :style=\"{cursor:interactive&&zoom.length?'pointer':'default'}\"/>\n </svg>\n <div style=\"position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center;pointer-events:none;line-height:normal;gap:2px\">\n <span :style=\"{fontFamily:'var(--font-sans)',fontWeight:'var(--weight-light)',fontSize:Math.round(size/13)+'px',color:'var(--text-heading)'}\">{{ valueFormat(g.focus.value) }}</span>\n <span :style=\"{fontFamily:'var(--font-sans)',fontSize:'var(--text-2xs)',color:'var(--text-muted)',maxWidth:g.rInner*1.7+'px',textAlign:'center',whiteSpace:'nowrap',overflow:'hidden',textOverflow:'ellipsis'}\">{{ g.focus.label || 'Total' }}</span>\n <span v-if=\"zoom.length\" style=\"font-family:var(--font-sans);font-size:9px;color:var(--action-primary)\">↑ back</span>\n </div>\n <div v-if=\"tip && interactive\" :style=\"{position:'absolute',left:tip.x+'px',top:tip.y+'px',transform:tip.x>size*0.55?'translate(calc(-100% - 10px),-50%)':'translate(10px,-50%)',pointerEvents:'none',zIndex:5,background:'var(--bg-surface)',border:'1px solid var(--border-card)',borderRadius:'var(--radius-md)',boxShadow:'var(--shadow-menu)',padding:'7px 10px',lineHeight:'normal',whiteSpace:'nowrap'}\">\n <div style=\"font-family:var(--font-sans);font-size:var(--text-2xs);color:var(--text-muted);padding-bottom:3px\">{{ tip.title }}</div>\n <div style=\"font-family:var(--font-mono);font-size:var(--text-xs);color:var(--text-heading)\">{{ tip.value }}</div>\n </div>\n </div>\n </div>\n</template>\n","charts/WorldMap.jsx":"import React from \"react\";\nimport { fmtNum, quantize } from \"./chart-geometry.js\";\nimport { ChartTooltip, wrapStyle } from \"./ChartKit.jsx\";\n\nconst HEAT = [\"var(--chart-heat-1)\", \"var(--chart-heat-2)\", \"var(--chart-heat-3)\", \"var(--chart-heat-4)\", \"var(--chart-heat-5)\"];\nconst TOPO = \"https://cdn.jsdelivr.net/npm/world-atlas@2.0.2/countries-110m.json\";\n/* Natural Earth spells several countries formally; accept the everyday name too so\n callers can write { \"United States\": 1 } instead of \"United States of America\". */\nconst ALIAS = {\n \"united states\": \"United States of America\", usa: \"United States of America\", us: \"United States of America\",\n uk: \"United Kingdom\", \"great britain\": \"United Kingdom\", britain: \"United Kingdom\",\n \"south korea\": \"South Korea\", korea: \"South Korea\", \"czech republic\": \"Czechia\",\n uae: \"United Arab Emirates\", drc: \"Dem. Rep. Congo\", \"democratic republic of the congo\": \"Dem. Rep. Congo\",\n \"bosnia and herzegovina\": \"Bosnia and Herz.\", \"dominican republic\": \"Dominican Rep.\",\n \"central african republic\": \"Central African Rep.\", \"south sudan\": \"S. Sudan\", \"ivory coast\": \"Côte d'Ivoire\",\n};\n\n/**\n * World choropleth. Real Natural Earth geometry (world-atlas 110m) projected with d3-geo —\n * never hand-drawn outlines. `values` maps country name → number (\"United States\",\n * \"Germany\"…); unmatched countries render as empty land. Hover highlights a country and\n * pins its value; click reports it.\n *\n * The host page must load the pinned d3 + topojson-client tags (see the card source);\n * the component reads them off `window` and fetches the pinned topology itself.\n */\nexport function WorldMap({ values = {}, width = 660, height = 340, projection = \"naturalEarth\", scale = \"blue\", valueFormat = fmtNum, legend = true, unit, interactive = true, onSelect, style }) {\n const [geo, setGeo] = React.useState(null);\n const [err, setErr] = React.useState(null);\n const [hover, setHover] = React.useState(null);\n\n React.useEffect(() => {\n let dead = false;\n const d3 = window.d3, topojson = window.topojson;\n if (!d3 || !topojson) { setErr(\"d3 + topojson-client are required on the page\"); return; }\n fetch(TOPO).then((r) => r.json()).then((topo) => {\n if (dead) return;\n const fc = topojson.feature(topo, topo.objects.countries);\n setGeo(fc);\n }).catch(() => !dead && setErr(\"could not load country geometry\"));\n return () => { dead = true; };\n }, []);\n\n const paths = React.useMemo(() => {\n if (!geo || !window.d3) return null;\n const d3 = window.d3;\n const proj = (projection === \"mercator\" ? d3.geoMercator() : projection === \"equalEarth\" && d3.geoEqualEarth ? d3.geoEqualEarth() : d3.geoNaturalEarth1())\n .fitSize([width, height - (legend ? 22 : 0)], geo);\n const gp = d3.geoPath(proj);\n return geo.features.map((f) => ({ name: f.properties.name, d: gp(f), c: gp.centroid(f) }));\n }, [geo, width, height, projection, legend]);\n\n /* Resolve caller keys to Natural Earth feature names once, so both the scale and every\n lookup agree on the same key set. */\n const resolved = React.useMemo(() => {\n const out = {};\n Object.keys(values).forEach((k) => {\n const canon = ALIAS[k.toLowerCase()] || k;\n out[canon] = values[k];\n });\n return out;\n }, [values]);\n const nums = Object.values(resolved).filter((v) => typeof v === \"number\");\n const bucket = quantize(nums, 5);\n const lo = Math.min(...nums), hi = Math.max(...nums);\n const fillOf = (name) => {\n const v = resolved[name];\n if (v == null) return \"var(--chart-track)\";\n if (scale === \"diverging\") {\n const amax = Math.max(Math.abs(lo), Math.abs(hi)) || 1;\n const a = Math.round((0.18 + Math.min(1, Math.abs(v) / amax) * 0.82) * 100);\n return \"color-mix(in srgb, var(--chart-\" + (v >= 0 ? \"pos\" : \"neg\") + \") \" + a + \"%, var(--bg-surface))\";\n }\n return HEAT[Math.max(0, bucket(v))];\n };\n\n if (err) return <div style={{ width, height, display: \"flex\", alignItems: \"center\", justifyContent: \"center\", fontFamily: \"var(--font-mono)\", fontSize: \"var(--text-xs)\", color: \"var(--text-muted)\", background: \"var(--bg-inset)\", borderRadius: \"var(--radius-lg)\", ...style }}>{err}</div>;\n if (!paths) return <div style={{ width, height, display: \"flex\", alignItems: \"center\", justifyContent: \"center\", fontFamily: \"var(--font-mono)\", fontSize: \"var(--text-xs)\", color: \"var(--text-muted)\", ...style }}>loading map…</div>;\n\n const h = hover != null ? paths[hover] : null;\n return (\n <div style={{ ...wrapStyle, ...style }}>\n <svg width={width} height={height - (legend ? 22 : 0)} role=\"img\">\n {paths.map((p, i) => (\n <path key={p.name + i} d={p.d} fill={fillOf(p.name)} stroke=\"var(--bg-surface)\" strokeWidth=\"0.5\"\n onMouseEnter={interactive ? () => setHover(i) : undefined} onMouseLeave={interactive ? () => setHover(null) : undefined}\n onClick={onSelect ? () => onSelect(p.name, resolved[p.name]) : undefined}\n style={{ cursor: onSelect ? \"pointer\" : \"default\", outline: \"none\", opacity: hover == null || hover === i ? 1 : 0.75, transition: \"opacity var(--dur-fast) var(--ease-out)\" }} />\n ))}\n {h ? <path d={h.d} fill=\"none\" stroke=\"var(--action-primary)\" strokeWidth=\"1.4\" pointerEvents=\"none\" /> : null}\n </svg>\n {legend ? (\n <div style={{ display: \"flex\", alignItems: \"center\", gap: 6, paddingTop: 6, lineHeight: \"normal\" }}>\n <span style={{ fontFamily: \"var(--font-mono)\", fontSize: 10, color: \"var(--chart-label)\" }}>{valueFormat(lo)}</span>\n {(scale === \"diverging\" ? [\"var(--chart-neg)\", \"color-mix(in srgb, var(--chart-neg) 40%, var(--bg-surface))\", \"var(--chart-track)\", \"color-mix(in srgb, var(--chart-pos) 40%, var(--bg-surface))\", \"var(--chart-pos)\"] : HEAT).map((c, i) => (\n <span key={i} style={{ width: 22, height: 8, borderRadius: 2, background: c }} />\n ))}\n <span style={{ fontFamily: \"var(--font-mono)\", fontSize: 10, color: \"var(--chart-label)\" }}>{valueFormat(hi)}{unit ? \" \" + unit : \"\"}</span>\n </div>\n ) : null}\n {h && interactive ? (\n <ChartTooltip x={h.c[0]} y={h.c[1]} flip={h.c[0] > width * 0.62} title={h.name}\n rows={[{ label: unit || \"value\", value: resolved[h.name] == null ? \"no data\" : valueFormat(resolved[h.name]), color: resolved[h.name] == null ? undefined : fillOf(h.name) }]} />\n ) : null}\n </div>\n );\n}\n","charts/WorldMap.vue":"<script setup>\nimport { ref, computed, onMounted } from 'vue'\nimport { fmtNum, quantize } from './chart-geometry.js'\n/**\n * World choropleth on real Natural Earth geometry (world-atlas 110m) projected with d3-geo.\n * The host page MUST load the pinned d3 + topojson-client tags; this reads them off window.\n */\nconst HEAT = ['var(--chart-heat-1)','var(--chart-heat-2)','var(--chart-heat-3)','var(--chart-heat-4)','var(--chart-heat-5)']\nconst TOPO = 'https://cdn.jsdelivr.net/npm/world-atlas@2.0.2/countries-110m.json'\nconst ALIAS = { 'united states':'United States of America', usa:'United States of America', us:'United States of America',\n uk:'United Kingdom', britain:'United Kingdom', 'great britain':'United Kingdom', 'czech republic':'Czechia',\n uae:'United Arab Emirates', 'south sudan':'S. Sudan', 'dominican republic':'Dominican Rep.',\n 'bosnia and herzegovina':'Bosnia and Herz.', 'central african republic':'Central African Rep.', 'ivory coast':\"Côte d'Ivoire\" }\nconst props = defineProps({ values:{type:Object,default:()=>({})}, width:{type:Number,default:660}, height:{type:Number,default:340}, projection:{type:String,default:'naturalEarth'}, scale:{type:String,default:'blue'}, valueFormat:{type:Function,default:fmtNum}, legend:{type:Boolean,default:true}, unit:String, interactive:{type:Boolean,default:true}, style:Object })\nconst emit = defineEmits(['select'])\nconst geo = ref(null); const err = ref(null); const hover = ref(null)\nonMounted(() => {\n const d3 = window.d3, topojson = window.topojson\n if (!d3 || !topojson) { err.value = 'd3 + topojson-client are required on the page'; return }\n fetch(TOPO).then(r=>r.json()).then(topo=>{ geo.value = topojson.feature(topo, topo.objects.countries) })\n .catch(()=>{ err.value = 'could not load country geometry' })\n})\nconst resolved = computed(() => {\n const out = {}\n Object.keys(props.values).forEach(k=>{ out[ALIAS[k.toLowerCase()]||k] = props.values[k] })\n return out\n})\nconst plotH = computed(() => props.height - (props.legend ? 22 : 0))\nconst paths = computed(() => {\n if (!geo.value || !window.d3) return null\n const d3 = window.d3\n const proj = (props.projection==='mercator' ? d3.geoMercator() : (props.projection==='equalEarth' && d3.geoEqualEarth ? d3.geoEqualEarth() : d3.geoNaturalEarth1())).fitSize([props.width, plotH.value], geo.value)\n const gp = d3.geoPath(proj)\n return geo.value.features.map(f=>({ name:f.properties.name, d:gp(f), c:gp.centroid(f) }))\n})\nconst nums = computed(() => Object.values(resolved.value).filter(v=>typeof v==='number'))\nconst lo = computed(() => Math.min(...nums.value)); const hi = computed(() => Math.max(...nums.value))\nconst bucket = computed(() => quantize(nums.value, 5))\nfunction fill(name){\n const v = resolved.value[name]\n if (v==null) return 'var(--chart-track)'\n if (props.scale==='diverging') {\n const amax = Math.max(Math.abs(lo.value), Math.abs(hi.value))||1\n const a = Math.round((0.18+Math.min(1,Math.abs(v)/amax)*0.82)*100)\n return 'color-mix(in srgb, var(--chart-'+(v>=0?'pos':'neg')+') '+a+'%, var(--bg-surface))'\n }\n return HEAT[Math.max(0, bucket.value(v))]\n}\nconst swatches = computed(() => props.scale==='diverging'\n ? ['var(--chart-neg)','color-mix(in srgb, var(--chart-neg) 40%, var(--bg-surface))','var(--chart-track)','color-mix(in srgb, var(--chart-pos) 40%, var(--bg-surface))','var(--chart-pos)']\n : HEAT)\n</script>\n<template>\n <div v-if=\"err\" :style=\"{width:width+'px',height:height+'px',display:'flex',alignItems:'center',justifyContent:'center',fontFamily:'var(--font-mono)',fontSize:'var(--text-xs)',color:'var(--text-muted)',background:'var(--bg-inset)',borderRadius:'var(--radius-lg)',...style}\">{{ err }}</div>\n <div v-else-if=\"!paths\" :style=\"{width:width+'px',height:height+'px',display:'flex',alignItems:'center',justifyContent:'center',fontFamily:'var(--font-mono)',fontSize:'var(--text-xs)',color:'var(--text-muted)',...style}\">loading map…</div>\n <div v-else :style=\"{position:'relative',display:'inline-block',lineHeight:0,...style}\">\n <svg :width=\"width\" :height=\"plotH\" role=\"img\">\n <path v-for=\"(p,i) in paths\" :key=\"p.name+i\" :d=\"p.d\" :fill=\"fill(p.name)\" stroke=\"var(--bg-surface)\" stroke-width=\"0.5\"\n @mouseenter=\"interactive && (hover=i)\" @mouseleave=\"interactive && (hover=null)\" @click=\"emit('select', p.name, resolved[p.name])\"\n :style=\"{opacity:hover==null||hover===i?1:0.75,transition:'opacity var(--dur-fast) var(--ease-out)'}\"/>\n <path v-if=\"hover!=null\" :d=\"paths[hover].d\" fill=\"none\" stroke=\"var(--action-primary)\" stroke-width=\"1.4\" pointer-events=\"none\"/>\n </svg>\n <div v-if=\"legend\" style=\"display:flex;align-items:center;gap:6px;padding-top:6px;line-height:normal\">\n <span style=\"font-family:var(--font-mono);font-size:10px;color:var(--chart-label)\">{{ valueFormat(lo) }}</span>\n <span v-for=\"(c,i) in swatches\" :key=\"i\" :style=\"{width:'22px',height:'8px',borderRadius:'2px',background:c}\"/>\n <span style=\"font-family:var(--font-mono);font-size:10px;color:var(--chart-label)\">{{ valueFormat(hi) }}{{ unit ? ' '+unit : '' }}</span>\n </div>\n <div v-if=\"hover!=null && interactive\" :style=\"{position:'absolute',left:paths[hover].c[0]+'px',top:paths[hover].c[1]+'px',transform:paths[hover].c[0]>width*0.62?'translate(calc(-100% - 10px),-50%)':'translate(10px,-50%)',pointerEvents:'none',zIndex:5,background:'var(--bg-surface)',border:'1px solid var(--border-card)',borderRadius:'var(--radius-md)',boxShadow:'var(--shadow-menu)',padding:'7px 10px',lineHeight:'normal',whiteSpace:'nowrap'}\">\n <div style=\"font-family:var(--font-sans);font-size:var(--text-2xs);color:var(--text-muted);padding-bottom:3px\">{{ paths[hover].name }}</div>\n <div style=\"font-family:var(--font-mono);font-size:var(--text-xs);color:var(--text-heading)\">{{ resolved[paths[hover].name]==null ? 'no data' : valueFormat(resolved[paths[hover].name]) }}</div>\n </div>\n </div>\n</template>\n","charts/GanttChart.jsx":"import React from \"react\";\nimport { seriesColor } from \"./chart-geometry.js\";\nimport { ChartTooltip, wrapStyle } from \"./ChartKit.jsx\";\n\nconst DAY = 86400000;\nconst parse = (d) => (d instanceof Date ? d : new Date(d));\n\n/**\n * Gantt timeline — close calendars, migration waves, release plans.\n * tasks: [{label, start, end, progress?, lane?, color?, milestone?}] with ISO dates.\n * Weekend columns are tinted, today draws a live marker, and hovering a bar reports\n * its window, duration and progress.\n */\nexport function GanttChart({ tasks = [], width = 660, rowHeight = 26, today, unit = \"day\", interactive = true, style }) {\n const [hover, setHover] = React.useState(null);\n const labelW = 150;\n const t0 = new Date(Math.min(...tasks.map((t) => parse(t.start))));\n const t1 = new Date(Math.max(...tasks.map((t) => parse(t.end || t.start))));\n const days = Math.max(1, Math.round((t1 - t0) / DAY) + 1);\n const plotW = width - labelW - 8;\n const x = (d) => labelW + ((parse(d) - t0) / (days * DAY)) * plotW;\n const height = tasks.length * rowHeight + 26;\n /* One tick per ~52px of plot: enough room for \"Mar 12\" without labels touching. */\n const ticks = [];\n const perLabel = 52;\n const stepDays = Math.max(unit === \"week\" ? 7 : 1, Math.ceil(days / Math.max(2, Math.floor(plotW / perLabel))));\n for (let i = 0; i <= days; i += stepDays) ticks.push(new Date(+t0 + i * DAY));\n const fmt = (d) => d.toLocaleDateString(\"en-US\", { month: \"short\", day: \"numeric\" });\n const h = hover != null ? tasks[hover] : null;\n return (\n <div style={{ ...wrapStyle, ...style }}>\n <svg width={width} height={height} role=\"img\">\n {ticks.map((d, i) => (\n <g key={i}>\n <line x1={x(d)} x2={x(d)} y1={20} y2={height - 4} stroke=\"var(--chart-grid)\" strokeWidth=\"1\" />\n <text x={x(d)} y={12} textAnchor=\"middle\" style={{ fontFamily: \"var(--font-sans)\", fontSize: 9.5, fill: \"var(--chart-label)\" }}>{fmt(d)}</text>\n </g>\n ))}\n {[...Array(days)].map((_, i) => {\n const d = new Date(+t0 + i * DAY);\n const wk = d.getDay() === 0 || d.getDay() === 6;\n return wk ? <rect key={i} x={x(d)} y={20} width={plotW / days} height={height - 24} fill=\"var(--bg-inset)\" opacity=\"0.6\" /> : null;\n })}\n {tasks.map((t, i) => {\n const y = 24 + i * rowHeight;\n const c = t.color || seriesColor(t.lane != null ? t.lane : i);\n const bx = x(t.start), bw = Math.max(4, x(t.end || t.start) - bx + plotW / days);\n const on = hover === i;\n return (\n <g key={t.label + i} onMouseEnter={interactive ? () => setHover(i) : undefined} onMouseLeave={interactive ? () => setHover(null) : undefined}\n opacity={hover == null || on ? 1 : 0.55} style={{ transition: \"opacity var(--dur-fast) var(--ease-out)\", cursor: interactive ? \"default\" : undefined }}>\n <text x={0} y={y + rowHeight / 2 + 3} style={{ fontFamily: \"var(--font-sans)\", fontSize: 11, fill: on ? \"var(--text-heading)\" : \"var(--text-secondary)\" }}>\n {t.label.length > 22 ? t.label.slice(0, 21) + \"…\" : t.label}\n </text>\n {t.milestone ? (\n <path d={\"M\" + bx + \" \" + (y + rowHeight / 2 - 6) + \" L\" + (bx + 6) + \" \" + (y + rowHeight / 2) + \" L\" + bx + \" \" + (y + rowHeight / 2 + 6) + \" L\" + (bx - 6) + \" \" + (y + rowHeight / 2) + \" Z\"} fill={c} />\n ) : (\n <>\n <rect x={bx} y={y + 4} width={bw} height={rowHeight - 12} rx=\"3\" fill={c} opacity=\"0.32\" />\n {t.progress != null ? <rect x={bx} y={y + 4} width={Math.max(2, bw * Math.min(1, t.progress))} height={rowHeight - 12} rx=\"3\" fill={c} /> : null}\n </>\n )}\n </g>\n );\n })}\n {today ? <g>\n <line x1={x(today)} x2={x(today)} y1={18} y2={height - 4} stroke=\"var(--action-primary)\" strokeWidth=\"1.2\" />\n <circle cx={x(today)} cy={18} r=\"2.6\" fill=\"var(--action-primary)\" />\n </g> : null}\n </svg>\n {h && interactive ? (\n <ChartTooltip x={x(h.start)} y={24 + hover * rowHeight + rowHeight / 2} flip={x(h.start) > width * 0.6} title={h.label}\n rows={[{ label: \"start\", value: fmt(parse(h.start)), color: h.color || seriesColor(h.lane != null ? h.lane : hover) },\n { label: \"end\", value: fmt(parse(h.end || h.start)) },\n { label: \"duration\", value: Math.max(1, Math.round((parse(h.end || h.start) - parse(h.start)) / DAY) + 1) + \"d\" }]\n .concat(h.progress != null ? [{ label: \"progress\", value: Math.round(h.progress * 100) + \"%\" }] : [])} />\n ) : null}\n </div>\n );\n}\n","charts/GanttChart.vue":"<script setup>\nimport { ref, computed } from 'vue'\nimport { SERIES } from './chart-geometry.js'\n// Timeline of tasks with weekend tinting, progress fill, today marker and milestones.\nconst DAY = 86400000\nconst props = defineProps({ tasks:{type:Array,default:()=>[]}, width:{type:Number,default:660}, rowHeight:{type:Number,default:26}, today:[String,Object], unit:{type:String,default:'day'}, interactive:{type:Boolean,default:true}, style:Object })\nconst hover = ref(null)\nconst parse = d => d instanceof Date ? d : new Date(d)\nconst fmt = d => parse(d).toLocaleDateString('en-US',{month:'short',day:'numeric'})\nconst g = computed(() => {\n const labelW = 150\n const t0 = new Date(Math.min(...props.tasks.map(t=>parse(t.start))))\n const t1 = new Date(Math.max(...props.tasks.map(t=>parse(t.end||t.start))))\n const days = Math.max(1, Math.round((t1-t0)/DAY)+1)\n const plotW = props.width-labelW-8\n const x = d => labelW + ((parse(d)-t0)/(days*DAY))*plotW\n const height = props.tasks.length*props.rowHeight+26\n // One tick per ~52px of plot: enough room for \"Mar 12\" without labels touching.\n const stepDays = Math.max(props.unit==='week'?7:1, Math.ceil(days/Math.max(2, Math.floor(plotW/52))))\n const ticks = []\n for (let i=0;i<=days;i+=stepDays) ticks.push(new Date(+t0+i*DAY))\n const weekends = [...Array(days)].map((_,i)=>new Date(+t0+i*DAY)).filter(d=>d.getDay()===0||d.getDay()===6)\n return { labelW, t0, days, plotW, x, height, ticks, weekends, colW: plotW/days,\n rows: props.tasks.map((t,i)=>{\n const bx = x(t.start)\n return { t, i, y: 24+i*props.rowHeight, color: t.color||SERIES[(t.lane!=null?t.lane:i)%SERIES.length],\n bx, bw: Math.max(4, x(t.end||t.start)-bx+plotW/days),\n label: t.label.length>22 ? t.label.slice(0,21)+'…' : t.label,\n dur: Math.max(1, Math.round((parse(t.end||t.start)-parse(t.start))/DAY)+1) }\n }) }\n})\n</script>\n<template>\n <div :style=\"{position:'relative',display:'inline-block',lineHeight:0,...style}\">\n <svg :width=\"width\" :height=\"g.height\" role=\"img\">\n <g v-for=\"(d,i) in g.ticks\" :key=\"'t'+i\">\n <line :x1=\"g.x(d)\" :x2=\"g.x(d)\" :y1=\"20\" :y2=\"g.height-4\" stroke=\"var(--chart-grid)\" stroke-width=\"1\"/>\n <text :x=\"g.x(d)\" :y=\"12\" text-anchor=\"middle\" style=\"font-family:var(--font-sans);font-size:9.5px;fill:var(--chart-label)\">{{ fmt(d) }}</text>\n </g>\n <rect v-for=\"(d,i) in g.weekends\" :key=\"'w'+i\" :x=\"g.x(d)\" :y=\"20\" :width=\"g.colW\" :height=\"g.height-24\" fill=\"var(--bg-inset)\" opacity=\"0.6\"/>\n <g v-for=\"r in g.rows\" :key=\"r.t.label+r.i\" @mouseenter=\"interactive && (hover=r.i)\" @mouseleave=\"interactive && (hover=null)\"\n :opacity=\"hover==null||hover===r.i?1:0.55\" style=\"transition:opacity var(--dur-fast) var(--ease-out)\">\n <text :x=\"0\" :y=\"r.y+rowHeight/2+3\" :style=\"{fontFamily:'var(--font-sans)',fontSize:'11px',fill:hover===r.i?'var(--text-heading)':'var(--text-secondary)'}\">{{ r.label }}</text>\n <path v-if=\"r.t.milestone\" :d=\"'M'+r.bx+' '+(r.y+rowHeight/2-6)+' L'+(r.bx+6)+' '+(r.y+rowHeight/2)+' L'+r.bx+' '+(r.y+rowHeight/2+6)+' L'+(r.bx-6)+' '+(r.y+rowHeight/2)+' Z'\" :fill=\"r.color\"/>\n <template v-else>\n <rect :x=\"r.bx\" :y=\"r.y+4\" :width=\"r.bw\" :height=\"rowHeight-12\" rx=\"3\" :fill=\"r.color\" opacity=\"0.32\"/>\n <rect v-if=\"r.t.progress!=null\" :x=\"r.bx\" :y=\"r.y+4\" :width=\"Math.max(2,r.bw*Math.min(1,r.t.progress))\" :height=\"rowHeight-12\" rx=\"3\" :fill=\"r.color\"/>\n </template>\n </g>\n <g v-if=\"today\">\n <line :x1=\"g.x(today)\" :x2=\"g.x(today)\" :y1=\"18\" :y2=\"g.height-4\" stroke=\"var(--action-primary)\" stroke-width=\"1.2\"/>\n <circle :cx=\"g.x(today)\" :cy=\"18\" r=\"2.6\" fill=\"var(--action-primary)\"/>\n </g>\n </svg>\n <div v-if=\"hover!=null && interactive\" :style=\"{position:'absolute',left:g.rows[hover].bx+'px',top:(g.rows[hover].y+rowHeight/2)+'px',transform:g.rows[hover].bx>width*0.6?'translate(calc(-100% - 10px),-50%)':'translate(10px,-50%)',pointerEvents:'none',zIndex:5,background:'var(--bg-surface)',border:'1px solid var(--border-card)',borderRadius:'var(--radius-md)',boxShadow:'var(--shadow-menu)',padding:'7px 10px',lineHeight:'normal',whiteSpace:'nowrap'}\">\n <div style=\"font-family:var(--font-sans);font-size:var(--text-2xs);color:var(--text-muted);padding-bottom:3px\">{{ tasks[hover].label }}</div>\n <div style=\"font-family:var(--font-mono);font-size:var(--text-xs);color:var(--text-heading)\">{{ fmt(tasks[hover].start) }} → {{ fmt(tasks[hover].end||tasks[hover].start) }} · {{ g.rows[hover].dur }}d<span v-if=\"tasks[hover].progress!=null\"> · {{ Math.round(tasks[hover].progress*100) }}%</span></div>\n </div>\n </div>\n</template>\n","charts/chart-3d.js":"// Shared 3D chart math: yaw/pitch rotation, orthographic projection, depth sorting,\n// face shading. Pure functions — no DOM, no tokens.\n\n/** Rotate (x,y,z) by yaw (around Y) then pitch (around X); degrees. */\nexport function rotate3(x, y, z, yaw, pitch) {\n const a = (yaw * Math.PI) / 180, b = (pitch * Math.PI) / 180;\n const x1 = x * Math.cos(a) - z * Math.sin(a);\n const z1 = x * Math.sin(a) + z * Math.cos(a);\n const y1 = y * Math.cos(b) - z1 * Math.sin(b);\n const z2 = y * Math.sin(b) + z1 * Math.cos(b);\n return [x1, y1, z2];\n}\n\n/** Projector for a unit-ish box: world coords in, screen [x,y] + depth out. */\nexport function makeProj(cx, cy, scale, yaw, pitch) {\n return (x, y, z) => {\n const [rx, ry, rz] = rotate3(x, -y, z, yaw, pitch);\n return { x: cx + rx * scale, y: cy + ry * scale, depth: rz };\n };\n}\n\nexport const facePath = (pts) => \"M\" + pts.map((p) => p.x.toFixed(1) + \" \" + p.y.toFixed(1)).join(\" L\") + \" Z\";\nexport const faceDepth = (pts) => pts.reduce((s, p) => s + p.depth, 0) / pts.length;\n\n/** Lambert-ish shade for a quad: mixes the base color toward white/black by orientation. */\nexport function shade(color, amount) {\n const pct = Math.round(Math.abs(amount) * 100);\n return amount >= 0\n ? `color-mix(in srgb, ${color} ${100 - pct}%, #fff)`\n : `color-mix(in srgb, ${color} ${100 - pct}%, #000)`;\n}\n\n/** Normal-based light factor for a quad given 3 projected-space corners (screen z). */\nexport function lightOf(p0, p1, p2) {\n const ux = p1.x - p0.x, uy = p1.y - p0.y, uz = p1.depth - p0.depth;\n const vx = p2.x - p0.x, vy = p2.y - p0.y, vz = p2.depth - p0.depth;\n const nx = uy * vz - uz * vy, ny = uz * vx - ux * vz, nz = ux * vy - uy * vx;\n const len = Math.hypot(nx, ny, nz) || 1;\n // Light from upper-left-front\n return (nx * -0.35 + ny * -0.6 + nz * -0.72) / len;\n}\n\n/** The 6 faces of a cuboid spanning [x0,x1]×[y0,y1]×[z0,z1], projected + depth-sorted. */\nexport function cuboidFaces(P, x0, x1, y0, y1, z0, z1) {\n const c = [\n P(x0, y0, z0), P(x1, y0, z0), P(x1, y1, z0), P(x0, y1, z0),\n P(x0, y0, z1), P(x1, y0, z1), P(x1, y1, z1), P(x0, y1, z1),\n ];\n const F = [\n { pts: [c[0], c[1], c[2], c[3]], kind: \"front\" },\n { pts: [c[4], c[5], c[6], c[7]], kind: \"back\" },\n { pts: [c[3], c[2], c[6], c[7]], kind: \"top\" },\n { pts: [c[0], c[1], c[5], c[4]], kind: \"bottom\" },\n { pts: [c[0], c[3], c[7], c[4]], kind: \"left\" },\n { pts: [c[1], c[2], c[6], c[5]], kind: \"right\" },\n ];\n F.forEach((f) => { f.depth = faceDepth(f.pts); f.d = facePath(f.pts); });\n return F.sort((a, b) => b.depth - a.depth);\n}\n\n/** Drag-to-rotate pointer handlers: returns props to spread on the svg.\n Drag state lives on the DOM node — a closure variable would be reset by every\n React re-render mid-drag, which silently kills the rotation after one frame. */\nexport function dragRotate(get, set) {\n return {\n onPointerDown: (e) => { e.currentTarget.__kbDrag = { x: e.clientX, y: e.clientY, ...get() }; e.currentTarget.setPointerCapture && e.currentTarget.setPointerCapture(e.pointerId); },\n onPointerMove: (e) => {\n const s = e.currentTarget.__kbDrag;\n if (!s) return;\n set({\n yaw: s.yaw + (e.clientX - s.x) * 0.45,\n pitch: Math.max(8, Math.min(80, s.pitch + (e.clientY - s.y) * 0.35)),\n });\n },\n onPointerUp: (e) => { e.currentTarget.__kbDrag = null; },\n onPointerLeave: (e) => { e.currentTarget.__kbDrag = null; },\n };\n}\n\n/** Sequential heat color for t∈[0,1] on the chart heat ramp (surface shading). */\nexport function heatColor(t) {\n const stops = [\"var(--chart-heat-1)\", \"var(--chart-heat-2)\", \"var(--chart-heat-3)\", \"var(--chart-heat-4)\", \"var(--chart-heat-5)\"];\n return stops[Math.max(0, Math.min(4, Math.floor(t * 5 - 1e-9)))];\n}\n","charts/Surface3D.jsx":"import React from \"react\";\nimport { fmtNum, niceTicks } from \"./chart-geometry.js\";\nimport { makeProj, facePath, faceDepth, shade, lightOf, dragRotate, heatColor } from \"./chart-3d.js\";\nimport { ChartTooltip, wrapStyle } from \"./ChartKit.jsx\";\n\n/**\n * 3D surface — z = f(x, y) over a grid. values[row][col]; drag to orbit. Cells shade on\n * the sequential heat ramp by height with light from the upper left, so the shape reads\n * in both themes without a legend.\n */\nexport function Surface3D({ values = [], width = 420, height = 300, yaw: yaw0 = 38, pitch: pitch0 = 32, zScale = 0.55, wire = true, xLabel = \"x\", yLabel = \"y\", interactive = true, valueFormat = fmtNum, style }) {\n const [rot, setRot] = React.useState({ yaw: yaw0, pitch: pitch0 });\n const [hover, setHover] = React.useState(null);\n const rows = values.length, cols = values[0] ? values[0].length : 0;\n let lo = Infinity, hi = -Infinity;\n values.forEach((r) => r.forEach((v) => { if (v < lo) lo = v; if (v > hi) hi = v; }));\n const span = hi - lo || 1;\n const scale = Math.min(width, height) * 0.42;\n const P = makeProj(width / 2, height / 2 + height * 0.05, scale, rot.yaw, rot.pitch);\n const pt = (r, c) => P((c / (cols - 1)) - 0.5, (((values[r][c] - lo) / span) - 0.5) * zScale * 2, (r / (rows - 1)) - 0.5);\n const quads = [];\n for (let r = 0; r < rows - 1; r++) for (let c = 0; c < cols - 1; c++) {\n const pts = [pt(r, c), pt(r, c + 1), pt(r + 1, c + 1), pt(r + 1, c)];\n const avg = (values[r][c] + values[r][c + 1] + values[r + 1][c] + values[r + 1][c + 1]) / 4;\n const t = (avg - lo) / span;\n quads.push({ pts, t, avg, r, c, depth: faceDepth(pts), light: lightOf(pts[0], pts[1], pts[3]) });\n }\n quads.sort((a, b) => b.depth - a.depth);\n /* Base frame + three axes meeting at the front-left corner: X along the columns,\n Y along the rows (depth), Z vertical with real ticks. */\n const yB = -zScale - 0.06;\n const base = [P(-0.5, yB, -0.5), P(0.5, yB, -0.5), P(0.5, yB, 0.5), P(-0.5, yB, 0.5)];\n const basePath = \"M\" + base.map((p) => p.x.toFixed(1) + \" \" + p.y.toFixed(1)).join(\" L\") + \" Z\";\n const corner = P(-0.5, yB, 0.5);\n const axes3 = [\n { a: corner, b: P(0.5, yB, 0.5), label: xLabel, lp: P(0, yB - 0.16, 0.5), anchor: \"middle\" },\n { a: corner, b: P(-0.5, yB, -0.5), label: yLabel, lp: P(-0.62, yB - 0.12, 0), anchor: \"end\" },\n { a: corner, b: P(-0.5, zScale, 0.5) },\n ];\n const zt = niceTicks(lo, hi, 3);\n const zTicks = zt.ticks.filter((t) => t >= lo && t <= hi).map((t) => ({\n p: P(-0.5, ((t - lo) / span - 0.5) * zScale * 2, 0.5), v: valueFormat(t),\n }));\n const handlers = interactive ? dragRotate(() => rot, setRot) : {};\n return (\n <div style={{ ...wrapStyle, ...style }}>\n <svg width={width} height={height} {...handlers} role=\"img\" style={{ cursor: interactive ? \"grab\" : \"default\", touchAction: \"none\" }}>\n <path d={basePath} fill=\"var(--bg-inset)\" stroke=\"var(--chart-axis)\" strokeWidth=\"1\" />\n {axes3.map((ax, i) => <line key={\"ae\" + i} x1={ax.a.x} y1={ax.a.y} x2={ax.b.x} y2={ax.b.y} stroke=\"var(--chart-guide)\" strokeWidth=\"1\" />)}\n {axes3.map((ax, i) => ax.label ? <text key={\"al\" + i} x={ax.lp.x} y={ax.lp.y + 3} textAnchor={ax.anchor} style={{ fontFamily: \"var(--font-sans)\", fontSize: 10, fill: \"var(--text-secondary)\" }}>{ax.label}</text> : null)}\n {zTicks.map((t, i) => (\n <g key={\"zt\" + i}>\n <line x1={t.p.x - 4} y1={t.p.y} x2={t.p.x} y2={t.p.y} stroke=\"var(--chart-guide)\" strokeWidth=\"1\" />\n <text x={t.p.x - 7} y={t.p.y + 3} textAnchor=\"end\" style={{ fontFamily: \"var(--font-mono)\", fontSize: 9, fill: \"var(--chart-label)\" }}>{t.v}</text>\n </g>\n ))}\n {quads.map((q, i) => (\n <path key={i} d={facePath(q.pts)} fill={shade(heatColor(q.t), Math.max(-0.35, Math.min(0.3, q.light * 0.4)) + (hover && hover.i === i ? 0.18 : 0))}\n stroke={wire ? \"var(--bg-surface)\" : \"none\"} strokeWidth=\"0.5\"\n onMouseEnter={interactive ? () => setHover({ i, q, x: q.pts.reduce((s, p) => s + p.x, 0) / 4, y: q.pts.reduce((s, p) => s + p.y, 0) / 4 }) : undefined}\n onMouseLeave={interactive ? () => setHover(null) : undefined} />\n ))}\n </svg>\n {hover && interactive ? (\n <ChartTooltip x={hover.x} y={hover.y} flip={hover.x > width * 0.6} title={\"row \" + (hover.q.r + 1) + \" · col \" + (hover.q.c + 1)}\n rows={[{ label: \"value\", value: valueFormat(hover.q.avg), color: heatColor(hover.q.t) }]} />\n ) : null}\n </div>\n );\n}\n","charts/Scatter3D.jsx":"import React from \"react\";\nimport { fmtNum } from \"./chart-geometry.js\";\nimport { makeProj, dragRotate } from \"./chart-3d.js\";\nimport { ChartTooltip, wrapStyle } from \"./ChartKit.jsx\";\n\n/**\n * 3D scatter — points in x/y/z with group coloring; drag to orbit. Depth-sorted circles\n * with radius attenuated by distance so nearer points read closer.\n */\nexport function Scatter3D({ points = [], width = 420, height = 300, yaw: yaw0 = 35, pitch: pitch0 = 28, r = 4, xLabel = \"x\", yLabel = \"y\", zLabel = \"z\", legend = true, interactive = true, valueFormat = fmtNum, style }) {\n const [rot, setRot] = React.useState({ yaw: yaw0, pitch: pitch0 });\n const [hover, setHover] = React.useState(null);\n const names = [...new Set(points.map((p) => p.series).filter(Boolean))];\n const norm = (arr) => { const l = Math.min(...arr), h = Math.max(...arr), s = h - l || 1; return (v) => (v - l) / s - 0.5; };\n const nx = norm(points.map((p) => p.x)), ny = norm(points.map((p) => p.y)), nz = norm(points.map((p) => p.z));\n const scale = Math.min(width, height) * 0.44;\n const P = makeProj(width / 2, height / 2, scale, rot.yaw, rot.pitch);\n const dots = points.map((p, i) => ({ p, i, ...P(nx(p.x), ny(p.y), nz(p.z)) })).sort((a, b) => b.depth - a.depth);\n // Cube edges for orientation\n const C = [-0.5, 0.5];\n const edges = [];\n C.forEach((a) => C.forEach((b) => { edges.push([P(a, b, -0.5), P(a, b, 0.5)], [P(a, -0.5, b), P(a, 0.5, b)], [P(-0.5, a, b), P(0.5, a, b)]); }));\n const colorOf = (p) => p.color || `var(--series-${(names.indexOf(p.series) % 10) + 1})`;\n /* Three axis edges meet at visible cube corners; names sit outside the cube along the\n screen-space outward direction, so they never land inside the plot at any rotation. */\n const axes3 = [\n { a: P(-0.5, -0.5, 0.5), b: P(0.5, -0.5, 0.5), name: xLabel },\n { a: P(-0.5, -0.5, 0.5), b: P(-0.5, 0.5, 0.5), name: yLabel },\n { a: P(0.5, -0.5, 0.5), b: P(0.5, -0.5, -0.5), name: zLabel },\n ].map((ax) => {\n const mx = (ax.a.x + ax.b.x) / 2, my = (ax.a.y + ax.b.y) / 2;\n const dx = mx - width / 2, dy = my - height / 2;\n const len = Math.hypot(dx, dy) || 1;\n return { ...ax, nx: mx + (dx / len) * 18, ny: my + (dy / len) * 18 };\n });\n const handlers = interactive ? dragRotate(() => rot, setRot) : {};\n return (\n <div style={{ ...wrapStyle, ...style }}>\n <svg width={width} height={height} {...handlers} role=\"img\" style={{ cursor: interactive ? \"grab\" : \"default\", touchAction: \"none\" }}>\n {edges.map(([a, b], i) => <line key={i} x1={a.x} y1={a.y} x2={b.x} y2={b.y} stroke=\"var(--chart-axis)\" strokeWidth=\"1\" />)}\n {axes3.map((ax, i) => <line key={\"ae\" + i} x1={ax.a.x} y1={ax.a.y} x2={ax.b.x} y2={ax.b.y} stroke=\"var(--chart-guide)\" strokeWidth=\"1\" />)}\n {axes3.map((ax, i) => (\n <text key={\"at\" + i} x={ax.nx} y={ax.ny + 3} textAnchor=\"middle\" style={{ fontFamily: \"var(--font-sans)\", fontSize: 10, fill: \"var(--text-secondary)\" }}>{ax.name}</text>\n ))}\n {dots.map((d) => (\n <circle key={d.i} cx={d.x} cy={d.y} r={r * (1.18 - (d.depth + 0.75) * 0.35) * (hover === d.i ? 1.5 : 1)}\n fill={colorOf(d.p)} opacity={hover === d.i ? 1 : (0.85 - (d.depth + 0.75) * 0.28) * (hover == null ? 1 : 0.45)}\n stroke={hover === d.i ? \"var(--bg-surface)\" : \"none\"} strokeWidth=\"1.5\"\n onMouseEnter={interactive ? () => setHover(d.i) : undefined} onMouseLeave={interactive ? () => setHover(null) : undefined} />\n ))}\n </svg>\n {hover != null && interactive ? (() => {\n const d = dots.find((x) => x.i === hover), p = d.p;\n return <ChartTooltip x={d.x} y={d.y} flip={d.x > width * 0.6} title={p.label || p.series}\n rows={[{ label: \"x\", value: valueFormat(p.x) }, { label: \"y\", value: valueFormat(p.y) }, { label: \"z\", value: valueFormat(p.z) }]} />;\n })() : null}\n {legend && names.length > 1 ? (\n <div style={{ display: \"flex\", gap: 14, paddingTop: 6, lineHeight: \"normal\" }}>\n {names.map((nm, i) => (\n <span key={nm} style={{ display: \"inline-flex\", alignItems: \"center\", gap: 6 }}>\n <span style={{ width: 8, height: 8, borderRadius: 4, background: `var(--series-${(i % 10) + 1})` }} />\n <span style={{ fontFamily: \"var(--font-sans)\", fontSize: \"var(--text-xs)\", color: \"var(--text-secondary)\" }}>{nm}</span>\n </span>\n ))}\n </div>\n ) : null}\n </div>\n );\n}\n","charts/Bar3D.jsx":"import React from \"react\";\nimport { fmtNum, seriesColor, stack, niceTicks } from \"./chart-geometry.js\";\nimport { makeProj, cuboidFaces, shade, dragRotate, facePath } from \"./chart-3d.js\";\nimport { ChartTooltip, wrapStyle } from \"./ChartKit.jsx\";\n\nconst FACE_SHADE = { top: 0.22, front: 0, right: -0.22, left: -0.3, back: -0.1, bottom: -0.4 };\n\n/**\n * 3D bars — categories along x, series as depth rows (or stacked in place). Drag to orbit.\n * mode \"grouped\" (one depth row per series) | \"stacked\" (segments pile up in one row).\n */\nexport function Bar3D({ series = [], categories = [], width = 460, height = 300, yaw: yaw0 = 30, pitch: pitch0 = 26, mode = \"grouped\", gap = 0.35, interactive = true, valueFormat = fmtNum, style }) {\n const [rot, setRot] = React.useState({ yaw: yaw0, pitch: pitch0 });\n const [hover, setHover] = React.useState(null);\n const n = categories.length || Math.max(...series.map((s) => s.values.length));\n const m = mode === \"grouped\" ? series.length : 1;\n const hi = mode === \"stacked\"\n ? Math.max(...[...Array(n)].map((_, i) => series.reduce((t, s) => t + (s.values[i] || 0), 0)))\n : Math.max(...series.flatMap((s) => s.values));\n const bands = mode === \"stacked\" ? stack(series.map((s) => s.values)) : null;\n const scale = Math.min(width, height) * 0.42;\n const P = makeProj(width / 2, height / 2 + height * 0.06, scale, rot.yaw, rot.pitch);\n const bw = (1 - gap) / n, bd = (1 - gap) / Math.max(m, 2);\n const bars = [];\n series.forEach((s, si) => s.values.forEach((v, i) => {\n if (v == null) return;\n const x0 = i / n - 0.5 + (gap / n) / 2, x1 = x0 + bw / (n / (n)) * (1 / 1) * (bw > 0 ? 1 : 1) * 1;\n const cx0 = i / n - 0.5 + (1 / n) * (gap / 2), cx1 = cx0 + (1 / n) * (1 - gap);\n const zi = mode === \"grouped\" ? si : 0;\n const cz0 = zi / Math.max(m, 1) - 0.5 + (1 / Math.max(m, 2)) * (gap / 2), cz1 = cz0 + (1 / Math.max(m, 2)) * (1 - gap);\n const y0 = mode === \"stacked\" ? (bands[si][i][0] / hi) : 0;\n const y1 = mode === \"stacked\" ? (bands[si][i][1] / hi) : v / hi;\n bars.push({ s, si, i, v, x0: cx0, x1: cx1, z0: cz0, z1: cz1, y0: (y0 - 0.5) * 0.9, y1: (y1 - 0.5) * 0.9 });\n }));\n // draw far-to-near: sort by projected center depth\n const drawn = bars.map((b, key) => {\n const faces = cuboidFaces(P, b.x0, b.x1, b.y0, b.y1, b.z0, b.z1);\n const depth = faces.reduce((s2, f) => s2 + f.depth, 0) / faces.length;\n const top = P((b.x0 + b.x1) / 2, b.y1, (b.z0 + b.z1) / 2);\n return { ...b, key, faces, depth, top, color: b.s.color || seriesColor(b.si) };\n }).sort((a, b2) => b2.depth - a.depth);\n const base = [P(-0.5, -0.46, -0.5), P(0.5, -0.46, -0.5), P(0.5, -0.46, 0.5), P(-0.5, -0.46, 0.5)];\n /* Vertical value axis with real ticks at the front-left corner. */\n const vt = niceTicks(0, hi, 3);\n const vTicks = vt.ticks.filter((t) => t <= hi * 1.001).map((t) => ({ p: P(-0.5, (t / hi - 0.5) * 0.9, 0.5), v: valueFormat(t) }));\n const vAxis = [P(-0.5, -0.45, 0.5), P(-0.5, 0.45, 0.5)];\n const handlers = interactive ? dragRotate(() => rot, setRot) : {};\n return (\n <div style={{ ...wrapStyle, ...style }}>\n <svg width={width} height={height} {...handlers} role=\"img\" style={{ cursor: interactive ? \"grab\" : \"default\", touchAction: \"none\" }}>\n <path d={facePath(base)} fill=\"var(--bg-inset)\" stroke=\"var(--chart-axis)\" strokeWidth=\"1\" />\n <line x1={vAxis[0].x} y1={vAxis[0].y} x2={vAxis[1].x} y2={vAxis[1].y} stroke=\"var(--chart-guide)\" strokeWidth=\"1\" />\n {vTicks.map((t, i) => (\n <g key={\"vt\" + i}>\n <line x1={t.p.x - 4} y1={t.p.y} x2={t.p.x} y2={t.p.y} stroke=\"var(--chart-guide)\" strokeWidth=\"1\" />\n <text x={t.p.x - 7} y={t.p.y + 3} textAnchor=\"end\" style={{ fontFamily: \"var(--font-mono)\", fontSize: 9, fill: \"var(--chart-label)\" }}>{t.v}</text>\n </g>\n ))}\n {categories.map((c, i) => {\n /* Labels follow whichever base edge currently faces the viewer (larger screen y),\n sit outside it, and paint BEFORE the bars, so they never cover a column. */\n const zE = P(0, -0.46, 0.5).y >= P(0, -0.46, -0.5).y ? 0.5 : -0.5;\n const p0 = P(i / n - 0.5 + 0.5 / n, -0.46, zE);\n const ctr = P(0, -0.46, 0);\n const dx = p0.x - ctr.x, dy = p0.y - ctr.y, len = Math.hypot(dx, dy) || 1;\n return <text key={\"c\" + i} x={p0.x + (dx / len) * 18} y={p0.y + (dy / len) * 18 + 3} textAnchor=\"middle\" style={{ fontFamily: \"var(--font-sans)\", fontSize: 10, fill: \"var(--chart-label)\" }}>{c}</text>;\n })}\n {drawn.map((b) => (\n <g key={b.key} opacity={hover == null || hover === b.key ? 1 : 0.45}\n onMouseEnter={interactive ? () => setHover(b.key) : undefined} onMouseLeave={interactive ? () => setHover(null) : undefined}\n style={{ transition: \"opacity var(--dur-fast) var(--ease-out)\" }}>\n {b.faces.map((f, fi) => (\n <path key={fi} d={f.d} fill={shade(b.color, FACE_SHADE[f.kind])} stroke=\"var(--bg-surface)\" strokeWidth=\"0.4\" />\n ))}\n </g>\n ))}\n </svg>\n {hover != null && interactive ? (() => {\n const b = drawn.find((x) => x.key === hover);\n return <ChartTooltip x={b.top.x} y={b.top.y} flip={b.top.x > width * 0.6} title={categories[b.i]}\n rows={[{ label: b.s.name, value: valueFormat(b.v), color: b.color }]} />;\n })() : null}\n <div style={{ display: \"flex\", gap: 14, paddingTop: 6, lineHeight: \"normal\" }}>\n {series.map((s, i) => (\n <span key={s.name} style={{ display: \"inline-flex\", alignItems: \"center\", gap: 6 }}>\n <span style={{ width: 8, height: 8, borderRadius: 2, background: s.color || seriesColor(i) }} />\n <span style={{ fontFamily: \"var(--font-sans)\", fontSize: \"var(--text-xs)\", color: \"var(--text-secondary)\" }}>{s.name}</span>\n </span>\n ))}\n </div>\n </div>\n );\n}\n","charts/Pie3D.jsx":"import React from \"react\";\nimport { fmtNum, seriesColor } from \"./chart-geometry.js\";\nimport { shade } from \"./chart-3d.js\";\nimport { ChartTooltip, wrapStyle } from \"./ChartKit.jsx\";\n\n/* Intersect the arc [a0,a1] (degrees, a1 > a0) with the window [w0,w1]; returns visible\n sub-arcs. Handles arcs that spin past 360 by testing the window at ±360. */\nfunction clipArc(a0, a1, w0, w1) {\n const A0 = ((a0 % 360) + 360) % 360, len = a1 - a0, A1 = A0 + len;\n const out = [];\n for (const off of [-360, 0, 360]) {\n const s = Math.max(A0, w0 + off), e = Math.min(A1, w1 + off);\n if (e - s > 0.4) out.push([s, e]);\n }\n return out;\n}\n\n/**\n * 3D pie / donut — extruded slices. Drag horizontally to spin in place (tilt stays fixed:\n * free orbit only distorts an extruded pie). stairs={true} extrudes each slice to its own\n * height by value — the stepped presentation pie. Hover lifts a slice and pins a tooltip.\n */\nexport function Pie3D({ segments = [], size = 300, thickness = 26, innerRatio = 0, tilt = 0.45, explode = 6, stairs = false, spin: spin0 = 0, valueFormat = fmtNum, legend = true, interactive = true, style }) {\n const [hover, setHover] = React.useState(null);\n const [spin, setSpin] = React.useState(spin0);\n const drag = React.useRef(null);\n const total = segments.reduce((s, x) => s + x.value, 0) || 1;\n const vmax = Math.max(...segments.map((s) => s.value), 1);\n const maxTh = stairs ? Math.round(thickness * 2.1) : thickness;\n const cx = size / 2, cy = size / 2 - maxTh / 2 + 6;\n const rO = size / 2 - 10, rI = rO * innerRatio;\n const pt = (r, deg, dz = 0) => [cx + r * Math.cos(((deg - 90) * Math.PI) / 180), cy + r * Math.sin(((deg - 90) * Math.PI) / 180) * tilt + dz];\n let a = 0;\n const slices = segments.map((s, i) => {\n const a0 = a + spin, a1 = a + (s.value / total) * 360 + spin; a += (s.value / total) * 360;\n const mid = ((((a0 + a1) / 2) % 360) + 360) % 360;\n /* Stairs: bottoms share one plane, each top rises by value — the stepped pie. */\n const h = stairs ? maxTh * (0.35 + 0.65 * (s.value / vmax)) : thickness;\n return { s, i, a0, a1, mid, h, color: s.color || seriesColor(i) };\n });\n const arcPts = (r, a0, a1, dz) => {\n const steps = Math.max(2, Math.ceil(Math.abs(a1 - a0) / 6));\n return [...Array(steps + 1)].map((_, k) => pt(r, a0 + ((a1 - a0) * k) / steps, dz));\n };\n const path = (pts) => \"M\" + pts.map((p) => p[0].toFixed(1) + \" \" + p[1].toFixed(1)).join(\" L\") + \" Z\";\n const off = (sl) => {\n if (hover !== sl.i || !interactive) return [0, 0];\n return [Math.cos(((sl.mid - 90) * Math.PI) / 180) * explode, Math.sin(((sl.mid - 90) * Math.PI) / 180) * explode * tilt];\n };\n const hoverProps = (sl) => interactive ? {\n onMouseEnter: () => !drag.current && setHover(sl.i),\n onMouseLeave: () => setHover(null),\n } : {};\n const dim = (sl) => ({ opacity: hover == null || hover === sl.i ? 1 : 0.55, style: { transition: \"opacity var(--dur-fast) var(--ease-out)\" } });\n /* deg 180 is the ellipse edge nearest the viewer. An outer wall is visible only where\n its arc crosses the near window (90–270); an inner wall only where it crosses the far\n window, seen through the hole. Clipping each wall to its visible window means the\n visible pieces never overlap in angle — paint order between them stops mattering,\n and no slice can ever show a gap between its top and its wall. */\n const nearness = (sl) => -Math.cos((sl.mid * Math.PI) / 180);\n const sorted = [...slices].sort((p, q) => nearness(p) - nearness(q));\n /* Bottoms first: any sliver the walls miss reads as dark interior, never as a hole in\n the render. */\n const bottoms = slices.map((sl) => {\n const [dx, dy] = off(sl);\n const d = rI > 0\n ? path([...arcPts(rO, sl.a0, sl.a1, maxTh), ...arcPts(rI, sl.a1, sl.a0, maxTh)].map(([x, y]) => [x + dx, y + dy]))\n : path([[cx + dx, cy + dy + maxTh], ...arcPts(rO, sl.a0, sl.a1, maxTh).map(([x, y]) => [x + dx, y + dy])]);\n return <path key={\"b\" + sl.i} d={d} fill={shade(sl.color, -0.45)} {...hoverProps(sl)} {...dim(sl)} />;\n });\n /* Each slice is a closed solid: two radial faces, then its full outer wall. Solids paint\n far-to-near, so a taller near slice covers the interior faces behind it and the side\n of the pie is never open at any spin angle. */\n const solids = sorted.map((sl) => {\n const [dx, dy] = off(sl);\n const zTop = maxTh - sl.h;\n const radial = (deg, k) => {\n const pts = [pt(rI || 0.01, deg, zTop), pt(rO, deg, zTop), pt(rO, deg, maxTh), pt(rI || 0.01, deg, maxTh)].map(([x, y]) => [x + dx, y + dy]);\n return <path key={\"r\" + k + sl.i} d={path(pts)} fill={shade(sl.color, -0.18)} />;\n };\n const top = arcPts(rO, sl.a0, sl.a1, zTop).map(([x, y]) => [x + dx, y + dy]);\n const bot = arcPts(rO, sl.a0, sl.a1, maxTh).map(([x, y]) => [x + dx, y + dy]);\n return (\n <g key={\"solid\" + sl.i} {...hoverProps(sl)} {...dim(sl)}>\n {radial(sl.a0, \"s\")}\n {radial(sl.a1, \"e\")}\n <path d={path([...top, ...bot.reverse()])} fill={shade(sl.color, -0.28)} />\n </g>\n );\n });\n const innerWalls = rI ? slices.flatMap((sl) => clipArc(sl.a0, sl.a1, -90, 90).map(([s, e], k) => {\n const [dx, dy] = off(sl);\n const top = arcPts(rI, s, e, maxTh - sl.h).map(([x, y]) => [x + dx, y + dy]);\n const bot = arcPts(rI, e, s, maxTh).map(([x, y]) => [x + dx, y + dy]);\n return <path key={\"iw\" + sl.i + \"-\" + k} d={path([...top, ...bot])} fill={shade(sl.color, -0.38)} {...hoverProps(sl)} {...dim(sl)} />;\n })) : [];\n const tops = sorted.map((sl) => {\n const [dx, dy] = off(sl);\n const zTop = maxTh - sl.h;\n const d = rI > 0\n ? path([...arcPts(rO, sl.a0, sl.a1, zTop), ...arcPts(rI, sl.a1, sl.a0, zTop)].map(([x, y]) => [x + dx, y + dy]))\n : path([[cx + dx, cy + dy + zTop], ...arcPts(rO, sl.a0, sl.a1, zTop).map(([x, y]) => [x + dx, y + dy])]);\n return <path key={\"t\" + sl.i} d={d} fill={sl.color} stroke=\"var(--bg-surface)\" strokeWidth=\"1\" {...hoverProps(sl)} {...dim(sl)} />;\n });\n const spinHandlers = interactive ? {\n onPointerDown: (e) => { drag.current = { x: e.clientX, s: spin }; e.currentTarget.setPointerCapture && e.currentTarget.setPointerCapture(e.pointerId); },\n onPointerMove: (e) => { if (drag.current) setSpin(drag.current.s + (e.clientX - drag.current.x) * 0.5); },\n onPointerUp: () => { drag.current = null; },\n onPointerLeave: () => { drag.current = null; },\n } : {};\n const h = hover != null ? slices.find((sl) => sl.i === hover) : null;\n const tipAt = h ? pt(rO * 0.8, (h.a0 + h.a1) / 2, (maxTh - h.h) / 2) : null;\n return (\n <div style={{ ...wrapStyle, ...style }}>\n <div style={{ display: \"flex\", alignItems: \"center\", gap: 16 }}>\n <div style={{ position: \"relative\" }}>\n <svg width={size} height={size * 0.72 + maxTh} {...spinHandlers} role=\"img\" style={{ cursor: interactive ? \"grab\" : \"default\", touchAction: \"none\" }}>\n {bottoms}\n {innerWalls}\n {solids}\n {tops}\n </svg>\n {h && interactive ? (\n <ChartTooltip x={tipAt[0]} y={tipAt[1]} flip={tipAt[0] > size * 0.55} title={h.s.label}\n rows={[{ label: \"value\", value: valueFormat(h.s.value), color: h.color }, { label: \"share\", value: ((h.s.value / total) * 100).toFixed(1) + \"%\" }]} />\n ) : null}\n </div>\n {legend ? (\n <div style={{ display: \"flex\", flexDirection: \"column\", gap: 6, lineHeight: \"normal\" }}>\n {slices.map(({ s, i, color }) => (\n <span key={s.label} onMouseEnter={interactive ? () => setHover(i) : undefined} onMouseLeave={interactive ? () => setHover(null) : undefined}\n style={{ display: \"inline-flex\", alignItems: \"center\", gap: 7, opacity: hover == null || hover === i ? 1 : 0.45 }}>\n <span style={{ width: 8, height: 8, borderRadius: 2, background: color, flexShrink: 0 }} />\n <span style={{ fontFamily: \"var(--font-sans)\", fontSize: \"var(--text-xs)\", color: \"var(--text-secondary)\" }}>{s.label}</span>\n <span style={{ fontFamily: \"var(--font-mono)\", fontSize: \"var(--text-xs)\", color: \"var(--text-heading)\", marginLeft: 6 }}>\n {hover === i ? valueFormat(s.value) : ((s.value / total) * 100).toFixed(0) + \"%\"}\n </span>\n </span>\n ))}\n </div>\n ) : null}\n </div>\n </div>\n );\n}\n","charts/Ribbon3D.jsx":"import React from \"react\";\nimport { fmtNum, seriesColor, niceTicks } from \"./chart-geometry.js\";\nimport { makeProj, facePath, faceDepth, shade, dragRotate } from \"./chart-3d.js\";\nimport { ChartTooltip, wrapStyle } from \"./ChartKit.jsx\";\n\n/**\n * 3D ribbon series — each series is an extruded band at its own depth row (the classic\n * \"3D area\" presentation). Drag to orbit.\n */\nexport function Ribbon3D({ series = [], xLabels = [], width = 460, height = 300, yaw: yaw0 = 34, pitch: pitch0 = 30, ribbon = 0.55, interactive = true, valueFormat = fmtNum, style }) {\n const [rot, setRot] = React.useState({ yaw: yaw0, pitch: pitch0 });\n const [hover, setHover] = React.useState(null);\n const n = Math.max(...series.map((s) => s.values.length), 2);\n const hi = Math.max(...series.flatMap((s) => s.values)) || 1;\n const m = series.length;\n const scale = Math.min(width, height) * 0.42;\n const P = makeProj(width / 2, height / 2 + height * 0.05, scale, rot.yaw, rot.pitch);\n const base = [P(-0.5, -0.45, -0.5), P(0.5, -0.45, -0.5), P(0.5, -0.45, 0.5), P(-0.5, -0.45, 0.5)];\n /* Vertical value axis with real ticks at the front-left corner. */\n const vt = niceTicks(0, hi, 3);\n const vTicks = vt.ticks.filter((t) => t <= hi * 1.001).map((t) => ({ p: P(-0.5, (t / hi - 0.5) * 0.9, 0.5), v: fmtNum(t) }));\n const vAxis = [P(-0.5, -0.45, 0.5), P(-0.5, 0.45, 0.5)];\n const rows = series.map((s, si) => {\n const z0 = si / m - 0.5 + (1 / m) * (0.5 - ribbon / 2), z1 = z0 + (ribbon / m);\n const y = (i) => ((s.values[i] || 0) / hi - 0.5) * 0.9;\n const quads = [];\n for (let i = 0; i < n - 1; i++) {\n const x0 = i / (n - 1) - 0.5, x1 = (i + 1) / (n - 1) - 0.5;\n const top = [P(x0, y(i), z0), P(x1, y(i + 1), z0), P(x1, y(i + 1), z1), P(x0, y(i), z1)];\n const front = [P(x0, -0.45, z1), P(x1, -0.45, z1), P(x1, y(i + 1), z1), P(x0, y(i), z1)];\n const back = [P(x0, -0.45, z0), P(x1, -0.45, z0), P(x1, y(i + 1), z0), P(x0, y(i), z0)];\n quads.push({ pts: back, kind: \"back\" }, { pts: top, kind: \"top\" }, { pts: front, kind: \"front\" });\n }\n const end = [P(0.5, -0.45, z0), P(0.5, -0.45, z1), P(0.5, y(n - 1), z1), P(0.5, y(n - 1), z0)];\n const start = [P(-0.5, -0.45, z0), P(-0.5, -0.45, z1), P(-0.5, y(0), z1), P(-0.5, y(0), z0)];\n quads.push({ pts: end, kind: \"end\" }, { pts: start, kind: \"end\" });\n quads.forEach((q) => { q.depth = faceDepth(q.pts); });\n const rowDepth = quads.reduce((t, q) => t + q.depth, 0) / quads.length;\n return { s, si, quads, rowDepth, color: s.color || seriesColor(si) };\n }).sort((a, b) => b.rowDepth - a.rowDepth);\n const handlers = interactive ? dragRotate(() => rot, setRot) : {};\n return (\n <div style={{ ...wrapStyle, ...style }}>\n <svg width={width} height={height} {...handlers} role=\"img\" style={{ cursor: interactive ? \"grab\" : \"default\", touchAction: \"none\" }}>\n <path d={facePath(base)} fill=\"var(--bg-inset)\" stroke=\"var(--chart-axis)\" strokeWidth=\"1\" />\n <line x1={vAxis[0].x} y1={vAxis[0].y} x2={vAxis[1].x} y2={vAxis[1].y} stroke=\"var(--chart-guide)\" strokeWidth=\"1\" />\n {vTicks.map((t, i) => (\n <g key={\"vt\" + i}>\n <line x1={t.p.x - 4} y1={t.p.y} x2={t.p.x} y2={t.p.y} stroke=\"var(--chart-guide)\" strokeWidth=\"1\" />\n <text x={t.p.x - 7} y={t.p.y + 3} textAnchor=\"end\" style={{ fontFamily: \"var(--font-mono)\", fontSize: 9, fill: \"var(--chart-label)\" }}>{t.v}</text>\n </g>\n ))}\n {xLabels.map((lb, i) => {\n if (lb == null || (xLabels.length > 8 && i % Math.ceil(xLabels.length / 8) && i !== xLabels.length - 1)) return null;\n /* Follow whichever base edge faces the viewer; outside it, painted before the bands. */\n const zE = P(0, -0.45, 0.5).y >= P(0, -0.45, -0.5).y ? 0.62 : -0.62;\n const p0 = P(i / (n - 1) - 0.5, -0.45, zE);\n const ctr = P(0, -0.45, 0);\n const dx = p0.x - ctr.x, dy = p0.y - ctr.y, len = Math.hypot(dx, dy) || 1;\n return <text key={\"x\" + i} x={p0.x + (dx / len) * 14} y={p0.y + (dy / len) * 14 + 3} textAnchor=\"middle\" style={{ fontFamily: \"var(--font-sans)\", fontSize: 10, fill: \"var(--chart-label)\" }}>{lb}</text>;\n })}\n {rows.map((r) => (\n <g key={r.s.name} opacity={hover == null || hover === r.si ? 1 : 0.4}\n onMouseEnter={interactive ? () => setHover(r.si) : undefined} onMouseLeave={interactive ? () => setHover(null) : undefined}\n style={{ transition: \"opacity var(--dur-fast) var(--ease-out)\" }}>\n {r.quads.sort((a, b) => b.depth - a.depth).map((q, qi) => (\n <path key={qi} d={facePath(q.pts)} fill={shade(r.color, q.kind === \"top\" ? 0.14 : q.kind === \"front\" ? -0.12 : q.kind === \"back\" ? -0.2 : -0.28)} stroke=\"var(--bg-surface)\" strokeWidth=\"0.4\" />\n ))}\n </g>\n ))}\n </svg>\n {hover != null && interactive ? (() => {\n const r = rows.find((x) => x.si === hover);\n const peak = Math.max(...r.s.values);\n return <ChartTooltip x={width / 2} y={height * 0.28} flip={false} title={r.s.name}\n rows={[{ label: \"peak\", value: valueFormat(peak), color: r.color }, { label: \"latest\", value: valueFormat(r.s.values[r.s.values.length - 1]) }]} />;\n })() : null}\n <div style={{ display: \"flex\", gap: 14, paddingTop: 6, lineHeight: \"normal\" }}>\n {series.map((s, i) => (\n <span key={s.name} onMouseEnter={interactive ? () => setHover(i) : undefined} onMouseLeave={interactive ? () => setHover(null) : undefined}\n style={{ display: \"inline-flex\", alignItems: \"center\", gap: 6, opacity: hover == null || hover === i ? 1 : 0.45, transition: \"var(--t-hover)\" }}>\n <span style={{ width: 8, height: 8, borderRadius: 2, background: s.color || seriesColor(i) }} />\n <span style={{ fontFamily: \"var(--font-sans)\", fontSize: \"var(--text-xs)\", color: \"var(--text-secondary)\" }}>{s.name}</span>\n </span>\n ))}\n </div>\n </div>\n );\n}\n","charts/Pie3D.vue":"<script setup>\nimport { ref, computed } from 'vue'\nimport { fmtNum, SERIES } from './chart-geometry.js'\nimport { shade } from './chart-3d.js'\n/**\n * 3D pie / donut — extruded slices. Drag horizontally to spin in place (tilt stays fixed:\n * free orbit only distorts an extruded pie). stairs extrudes each slice to its own height\n * by value — the stepped presentation pie. Hover lifts a slice and pins a tooltip.\n */\nconst props = defineProps({ segments:{type:Array,default:()=>[]}, size:{type:Number,default:300}, thickness:{type:Number,default:26}, innerRatio:{type:Number,default:0}, tilt:{type:Number,default:0.45}, explode:{type:Number,default:6}, stairs:Boolean, spin:{type:Number,default:0}, valueFormat:{type:Function,default:fmtNum}, legend:{type:Boolean,default:true}, interactive:{type:Boolean,default:true}, style:Object })\nconst hover = ref(null)\nconst spinNow = ref(props.spin)\nconst drag = ref(null)\nfunction down(e){ drag.value = { x:e.clientX, s:spinNow.value }; e.currentTarget.setPointerCapture && e.currentTarget.setPointerCapture(e.pointerId) }\nfunction move(e){ if (drag.value) spinNow.value = drag.value.s + (e.clientX-drag.value.x)*0.5 }\nfunction up(){ drag.value = null }\nconst g = computed(() => {\n const total = props.segments.reduce((s2,x)=>s2+x.value,0) || 1\n const vmax = Math.max(...props.segments.map(s2=>s2.value), 1)\n const maxTh = props.stairs ? Math.round(props.thickness*2.1) : props.thickness\n const cx = props.size/2, cy = props.size/2 - maxTh/2 + 6\n const rO = props.size/2-10, rI = rO*props.innerRatio\n const pt = (r,deg,dz=0) => [cx + r*Math.cos(((deg-90)*Math.PI)/180), cy + r*Math.sin(((deg-90)*Math.PI)/180)*props.tilt + dz]\n const arcPts = (r,a0,a1,dz) => { const steps = Math.max(2, Math.ceil(Math.abs(a1-a0)/6)); return [...Array(steps+1)].map((_,k)=>pt(r, a0+((a1-a0)*k)/steps, dz)) }\n const path = pts => 'M' + pts.map(p=>p[0].toFixed(1)+' '+p[1].toFixed(1)).join(' L') + ' Z'\n let a = 0\n const slices = props.segments.map((s2,i)=>{\n const a0 = a + spinNow.value, a1 = a + (s2.value/total)*360 + spinNow.value; a += (s2.value/total)*360\n const mid = ((((a0+a1)/2)%360)+360)%360\n // Stairs: bottoms share one plane, each top rises by value.\n const h = props.stairs ? maxTh*(0.35+0.65*(s2.value/vmax)) : props.thickness\n return { s:s2, i, a0, a1, mid, h, color: s2.color || SERIES[i%SERIES.length] }\n })\n const off = sl => (hover.value===sl.i && props.interactive)\n ? [Math.cos(((sl.mid-90)*Math.PI)/180)*props.explode, Math.sin(((sl.mid-90)*Math.PI)/180)*props.explode*props.tilt]\n : [0,0]\n const nearness = sl => -Math.cos((sl.mid*Math.PI)/180)\n const sorted = [...slices].sort((p,q)=>nearness(p)-nearness(q))\n // Bottoms first: any sliver the walls miss reads as dark interior, never a hole.\n const bottoms = slices.map(sl=>{\n const [dx,dy] = off(sl)\n const d = rI>0\n ? path([...arcPts(rO,sl.a0,sl.a1,maxTh), ...arcPts(rI,sl.a1,sl.a0,maxTh)].map(([x,y])=>[x+dx,y+dy]))\n : path([[cx+dx, cy+dy+maxTh], ...arcPts(rO,sl.a0,sl.a1,maxTh).map(([x,y])=>[x+dx,y+dy])])\n return { key:'b'+sl.i, d, fill: shade(sl.color,-0.45), i:sl.i }\n })\n const innerWalls = rI ? sorted.map(sl=>{\n const [dx,dy] = off(sl)\n const top = arcPts(rI,sl.a0,sl.a1,maxTh-sl.h).map(([x,y])=>[x+dx,y+dy])\n const bot = arcPts(rI,sl.a1,sl.a0,maxTh).map(([x,y])=>[x+dx,y+dy])\n return { key:'iw'+sl.i, d: path([...top,...bot]), fill: shade(sl.color,-0.38), i:sl.i }\n }) : []\n /* Each slice is a closed solid: two radial faces plus its full outer wall, painted\n far-to-near, so the side of the pie is never open at any spin angle. */\n const solids = sorted.map(sl=>{\n const [dx,dy] = off(sl)\n const zTop = maxTh - sl.h\n const radial = deg => path([pt(rI||0.01,deg,zTop), pt(rO,deg,zTop), pt(rO,deg,maxTh), pt(rI||0.01,deg,maxTh)].map(([x,y])=>[x+dx,y+dy]))\n const top = arcPts(rO,sl.a0,sl.a1,zTop).map(([x,y])=>[x+dx,y+dy])\n const bot = arcPts(rO,sl.a0,sl.a1,maxTh).map(([x,y])=>[x+dx,y+dy])\n return { key:'sd'+sl.i, i:sl.i, r0: radial(sl.a0), r1: radial(sl.a1),\n wall: path([...top,...bot.reverse()]), side: shade(sl.color,-0.18), wallFill: shade(sl.color,-0.28) }\n })\n const tops = sorted.map(sl=>{\n const [dx,dy] = off(sl)\n const zTop = maxTh - sl.h\n const d = rI>0\n ? path([...arcPts(rO,sl.a0,sl.a1,zTop), ...arcPts(rI,sl.a1,sl.a0,zTop)].map(([x,y])=>[x+dx,y+dy]))\n : path([[cx+dx, cy+dy+zTop], ...arcPts(rO,sl.a0,sl.a1,zTop).map(([x,y])=>[x+dx,y+dy])])\n return { key:'t'+sl.i, d, fill: sl.color, i:sl.i }\n })\n const h = hover.value!=null ? slices.find(sl=>sl.i===hover.value) : null\n return { total, maxTh, slices, bottoms, innerWalls, solids, tops,\n tip: h ? { p: pt(rO*0.8, (h.a0+h.a1)/2, (maxTh-h.h)/2), s:h.s, color:h.color } : null }\n})\nfunction dim(i){ return hover.value==null || hover.value===i ? 1 : 0.55 }\nfunction enter(i){ if (props.interactive && !drag.value) hover.value = i }\n</script>\n<template>\n <div :style=\"{display:'inline-block',lineHeight:0,...style}\">\n <div style=\"display:flex;align-items:center;gap:16px\">\n <div style=\"position:relative\">\n <svg :width=\"size\" :height=\"size*0.72+g.maxTh\" @pointerdown=\"down\" @pointermove=\"move\" @pointerup=\"up\" @pointerleave=\"up\"\n role=\"img\" :style=\"{cursor:interactive?'grab':'default',touchAction:'none'}\">\n <path v-for=\"b in g.bottoms\" :key=\"b.key\" :d=\"b.d\" :fill=\"b.fill\" :opacity=\"dim(b.i)\"\n @mouseenter=\"enter(b.i)\" @mouseleave=\"interactive && (hover=null)\"/>\n <path v-for=\"w in g.innerWalls\" :key=\"w.key\" :d=\"w.d\" :fill=\"w.fill\" :opacity=\"dim(w.i)\"\n @mouseenter=\"enter(w.i)\" @mouseleave=\"interactive && (hover=null)\"/>\n <g v-for=\"sd in g.solids\" :key=\"sd.key\" :opacity=\"dim(sd.i)\"\n @mouseenter=\"enter(sd.i)\" @mouseleave=\"interactive && (hover=null)\"\n style=\"transition:opacity var(--dur-fast) var(--ease-out)\">\n <path :d=\"sd.r0\" :fill=\"sd.side\"/>\n <path :d=\"sd.r1\" :fill=\"sd.side\"/>\n <path :d=\"sd.wall\" :fill=\"sd.wallFill\"/>\n </g>\n <path v-for=\"t in g.tops\" :key=\"t.key\" :d=\"t.d\" :fill=\"t.fill\" stroke=\"var(--bg-surface)\" stroke-width=\"1\" :opacity=\"dim(t.i)\"\n @mouseenter=\"enter(t.i)\" @mouseleave=\"interactive && (hover=null)\"/>\n </svg>\n <div v-if=\"g.tip && interactive\" :style=\"{position:'absolute',left:g.tip.p[0]+'px',top:g.tip.p[1]+'px',transform:g.tip.p[0]>size*0.55?'translate(calc(-100% - 10px),-50%)':'translate(10px,-50%)',pointerEvents:'none',zIndex:5,background:'var(--bg-surface)',border:'1px solid var(--border-card)',borderRadius:'var(--radius-md)',boxShadow:'var(--shadow-menu)',padding:'7px 10px',lineHeight:'normal',whiteSpace:'nowrap'}\">\n <div style=\"font-family:var(--font-sans);font-size:var(--text-2xs);color:var(--text-muted);padding-bottom:3px\">{{ g.tip.s.label }}</div>\n <div style=\"font-family:var(--font-mono);font-size:var(--text-xs);color:var(--text-heading)\">{{ valueFormat(g.tip.s.value) }} · {{ ((g.tip.s.value/g.total)*100).toFixed(1) }}%</div>\n </div>\n </div>\n <div v-if=\"legend\" style=\"display:flex;flex-direction:column;gap:6px;line-height:normal\">\n <span v-for=\"sl in g.slices\" :key=\"sl.s.label\" @mouseenter=\"interactive && (hover=sl.i)\" @mouseleave=\"interactive && (hover=null)\"\n :style=\"{display:'inline-flex',alignItems:'center',gap:'7px',opacity:dim(sl.i)}\">\n <span :style=\"{width:'8px',height:'8px',borderRadius:'2px',background:sl.color,flexShrink:0}\"/>\n <span style=\"font-family:var(--font-sans);font-size:var(--text-xs);color:var(--text-secondary)\">{{ sl.s.label }}</span>\n <span style=\"font-family:var(--font-mono);font-size:var(--text-xs);color:var(--text-heading);margin-left:6px\">\n {{ hover===sl.i ? valueFormat(sl.s.value) : ((sl.s.value/g.total)*100).toFixed(0)+'%' }}</span>\n </span>\n </div>\n </div>\n </div>\n</template>\n","charts/ChartKit.vue":"<script setup>\nimport { computed } from 'vue'\nimport { SERIES } from './chart-geometry.js'\n/**\n * Shared chart furniture for the Vue charts — one component, two shapes via `kind`\n * (\"legend\" | \"tooltip\"), so a single import covers both. The React side exposes these\n * as ChartLegend / ChartTooltip.\n */\nconst props = defineProps({\n kind: { type: String, default: 'legend' },\n items: { type: Array, default: () => [] },\n hidden: { type: Array, default: () => [] },\n align: { type: String, default: 'left' },\n x: Number,\n y: Number,\n title: String,\n rows: { type: Array, default: () => [] },\n flip: Boolean,\n style: Object,\n})\nconst emit = defineEmits(['toggle'])\nconst tipStyle = computed(() => ({\n position: 'absolute', left: props.x + 'px', top: props.y + 'px',\n transform: props.flip ? 'translate(calc(-100% - 10px),-50%)' : 'translate(10px,-50%)',\n pointerEvents: 'none', zIndex: 5, background: 'var(--bg-surface)',\n border: '1px solid var(--border-card)', borderRadius: 'var(--radius-md)',\n boxShadow: 'var(--shadow-menu)', padding: '7px 10px', minWidth: '90px',\n lineHeight: 'normal', whiteSpace: 'nowrap',\n}))\n</script>\n<template>\n <div v-if=\"kind === 'tooltip'\" :style=\"tipStyle\">\n <div v-if=\"title != null\" style=\"font-family:var(--font-sans);font-size:var(--text-2xs);color:var(--text-muted);padding-bottom:4px\">{{ title }}</div>\n <div v-for=\"(r,i) in rows\" :key=\"i\" style=\"display:flex;align-items:center;gap:6px;padding:1px 0\">\n <span v-if=\"r.color\" :style=\"{width:'7px',height:'7px',borderRadius:'2px',background:r.color,flexShrink:0}\"/>\n <span style=\"font-family:var(--font-sans);font-size:var(--text-xs);color:var(--text-secondary)\">{{ r.label }}</span>\n <span style=\"font-family:var(--font-mono);font-size:var(--text-xs);color:var(--text-heading);margin-left:auto;padding-left:10px\">{{ r.value }}</span>\n </div>\n </div>\n <div v-else :style=\"{display:'flex',flexWrap:'wrap',gap:'4px 14px',justifyContent:align==='right'?'flex-end':'flex-start',lineHeight:'normal',...style}\">\n <span v-for=\"(it,i) in items\" :key=\"it.label\" role=\"button\" tabindex=\"0\" @click=\"emit('toggle', it.label)\"\n :style=\"{display:'inline-flex',alignItems:'center',gap:'6px',cursor:'pointer',opacity:hidden.includes(it.label)?0.4:1,transition:'var(--t-hover)'}\">\n <span :style=\"{width:'8px',height:'8px',borderRadius:'2px',background:it.color||SERIES[i%SERIES.length],flexShrink:0}\"/>\n <span :style=\"{fontFamily:'var(--font-sans)',fontSize:'var(--text-xs)',color:'var(--text-secondary)',textDecoration:hidden.includes(it.label)?'line-through':'none'}\">{{ it.label }}</span>\n </span>\n </div>\n</template>\n","charts/Surface3D.vue":"<script setup>\nimport { ref, computed } from 'vue'\nimport { fmtNum, niceTicks } from './chart-geometry.js'\nimport { makeProj, facePath, faceDepth, shade, lightOf, heatColor } from './chart-3d.js'\n// 3D surface z = f(x,y); drag to orbit. Cells shade on the heat ramp by height.\nconst props = defineProps({ values:{type:Array,default:()=>[]}, width:{type:Number,default:420}, height:{type:Number,default:300}, yaw:{type:Number,default:38}, pitch:{type:Number,default:32}, zScale:{type:Number,default:0.55}, wire:{type:Boolean,default:true}, xLabel:{type:String,default:'x'}, yLabel:{type:String,default:'y'}, interactive:{type:Boolean,default:true}, valueFormat:{type:Function,default:fmtNum}, style:Object })\nconst rot = ref({ yaw: props.yaw, pitch: props.pitch })\nconst hover = ref(null)\n// Drag state on the DOM node: a closure var is reset by re-render mid-drag.\nfunction down(e){ e.currentTarget.__kbDrag = { x:e.clientX, y:e.clientY, ...rot.value }; e.currentTarget.setPointerCapture && e.currentTarget.setPointerCapture(e.pointerId) }\nfunction move(e){ const s = e.currentTarget.__kbDrag; if(!s) return; rot.value = { yaw: s.yaw+(e.clientX-s.x)*0.45, pitch: Math.max(8, Math.min(80, s.pitch+(e.clientY-s.y)*0.35)) } }\nfunction up(e){ e.currentTarget.__kbDrag = null }\nconst g = computed(() => {\n const rows = props.values.length, cols = props.values[0] ? props.values[0].length : 0\n let lo = Infinity, hi = -Infinity\n props.values.forEach(r=>r.forEach(v=>{ if(v<lo) lo=v; if(v>hi) hi=v }))\n const span = hi-lo || 1\n const scale = Math.min(props.width, props.height)*0.42\n const P = makeProj(props.width/2, props.height/2 + props.height*0.05, scale, rot.value.yaw, rot.value.pitch)\n const pt = (r,c) => P((c/(cols-1))-0.5, (((props.values[r][c]-lo)/span)-0.5)*props.zScale*2, (r/(rows-1))-0.5)\n const quads = []\n for (let r=0;r<rows-1;r++) for (let c=0;c<cols-1;c++) {\n const pts = [pt(r,c), pt(r,c+1), pt(r+1,c+1), pt(r+1,c)]\n const avg = (props.values[r][c]+props.values[r][c+1]+props.values[r+1][c]+props.values[r+1][c+1])/4\n quads.push({ d: facePath(pts), t:(avg-lo)/span, avg, r, c, depth: faceDepth(pts), light: lightOf(pts[0],pts[1],pts[3]),\n cx: pts.reduce((s,p)=>s+p.x,0)/4, cy: pts.reduce((s,p)=>s+p.y,0)/4 })\n }\n quads.sort((a,b)=>b.depth-a.depth)\n const yB = -props.zScale-0.06\n const base = [P(-0.5,yB,-0.5), P(0.5,yB,-0.5), P(0.5,yB,0.5), P(-0.5,yB,0.5)]\n const corner = P(-0.5,yB,0.5)\n const axes3 = [\n { a:corner, b:P(0.5,yB,0.5), label:props.xLabel, lp:P(0,yB-0.16,0.5), anchor:'middle' },\n { a:corner, b:P(-0.5,yB,-0.5), label:props.yLabel, lp:P(-0.62,yB-0.12,0), anchor:'end' },\n { a:corner, b:P(-0.5,props.zScale,0.5) },\n ]\n const zt = niceTicks(lo,hi,3)\n const zTicks = zt.ticks.filter(t=>t>=lo&&t<=hi).map(t=>({ p:P(-0.5, ((t-lo)/span-0.5)*props.zScale*2, 0.5), v:props.valueFormat(t) }))\n return { quads, basePath: facePath(base), axes3, zTicks }\n})\nfunction fill(q){ return shade(heatColor(q.t), Math.max(-0.35, Math.min(0.3, q.light*0.4)) + (hover.value===q ? 0.18 : 0)) }\n</script>\n<template>\n <div :style=\"{position:'relative',display:'inline-block',lineHeight:0,...style}\">\n <svg :width=\"width\" :height=\"height\" @pointerdown=\"down\" @pointermove=\"move\" @pointerup=\"up\" @pointerleave=\"up\"\n role=\"img\" :style=\"{cursor:interactive?'grab':'default',touchAction:'none'}\">\n <path :d=\"g.basePath\" fill=\"var(--bg-inset)\" stroke=\"var(--chart-axis)\" stroke-width=\"1\"/>\n <line v-for=\"(ax,i) in g.axes3\" :key=\"'ae'+i\" :x1=\"ax.a.x\" :y1=\"ax.a.y\" :x2=\"ax.b.x\" :y2=\"ax.b.y\" stroke=\"var(--chart-guide)\" stroke-width=\"1\"/>\n <text v-for=\"(ax,i) in g.axes3.filter(a=>a.label)\" :key=\"'al'+i\" :x=\"ax.lp.x\" :y=\"ax.lp.y+3\" :text-anchor=\"ax.anchor\"\n style=\"font-family:var(--font-sans);font-size:10px;fill:var(--text-secondary)\">{{ ax.label }}</text>\n <g v-for=\"(t,i) in g.zTicks\" :key=\"'zt'+i\">\n <line :x1=\"t.p.x-4\" :y1=\"t.p.y\" :x2=\"t.p.x\" :y2=\"t.p.y\" stroke=\"var(--chart-guide)\" stroke-width=\"1\"/>\n <text :x=\"t.p.x-7\" :y=\"t.p.y+3\" text-anchor=\"end\" style=\"font-family:var(--font-mono);font-size:9px;fill:var(--chart-label)\">{{ t.v }}</text>\n </g>\n <path v-for=\"(q,i) in g.quads\" :key=\"i\" :d=\"q.d\" :fill=\"fill(q)\" :stroke=\"wire?'var(--bg-surface)':'none'\" stroke-width=\"0.5\"\n @mouseenter=\"interactive && (hover=q)\" @mouseleave=\"interactive && (hover=null)\"/>\n </svg>\n <div v-if=\"hover && interactive\" :style=\"{position:'absolute',left:hover.cx+'px',top:hover.cy+'px',transform:hover.cx>width*0.6?'translate(calc(-100% - 10px),-50%)':'translate(10px,-50%)',pointerEvents:'none',zIndex:5,background:'var(--bg-surface)',border:'1px solid var(--border-card)',borderRadius:'var(--radius-md)',boxShadow:'var(--shadow-menu)',padding:'7px 10px',lineHeight:'normal',whiteSpace:'nowrap'}\">\n <div style=\"font-family:var(--font-sans);font-size:var(--text-2xs);color:var(--text-muted);padding-bottom:3px\">row {{ hover.r+1 }} · col {{ hover.c+1 }}</div>\n <div style=\"font-family:var(--font-mono);font-size:var(--text-xs);color:var(--text-heading)\">{{ valueFormat(hover.avg) }}</div>\n </div>\n </div>\n</template>\n","charts/Scatter3D.vue":"<script setup>\nimport { ref, computed } from 'vue'\nimport { fmtNum, SERIES } from './chart-geometry.js'\nimport { makeProj } from './chart-3d.js'\n// 3D scatter in an orientation cube; drag to orbit. Depth attenuates radius and opacity.\nconst props = defineProps({ points:{type:Array,default:()=>[]}, width:{type:Number,default:420}, height:{type:Number,default:300}, yaw:{type:Number,default:35}, pitch:{type:Number,default:28}, r:{type:Number,default:4}, xLabel:{type:String,default:'x'}, yLabel:{type:String,default:'y'}, zLabel:{type:String,default:'z'}, legend:{type:Boolean,default:true}, interactive:{type:Boolean,default:true}, valueFormat:{type:Function,default:fmtNum}, style:Object })\nconst rot = ref({ yaw: props.yaw, pitch: props.pitch })\nconst hover = ref(null)\nfunction down(e){ e.currentTarget.__kbDrag = { x:e.clientX, y:e.clientY, ...rot.value }; e.currentTarget.setPointerCapture && e.currentTarget.setPointerCapture(e.pointerId) }\nfunction move(e){ const s = e.currentTarget.__kbDrag; if(!s) return; rot.value = { yaw: s.yaw+(e.clientX-s.x)*0.45, pitch: Math.max(8, Math.min(80, s.pitch+(e.clientY-s.y)*0.35)) } }\nfunction up(e){ e.currentTarget.__kbDrag = null }\nconst g = computed(() => {\n const names = [...new Set(props.points.map(p=>p.series).filter(Boolean))]\n const norm = arr => { const l=Math.min(...arr), h=Math.max(...arr), s=h-l||1; return v=>(v-l)/s-0.5 }\n const nx = norm(props.points.map(p=>p.x)), ny = norm(props.points.map(p=>p.y)), nz = norm(props.points.map(p=>p.z))\n const scale = Math.min(props.width, props.height)*0.44\n const P = makeProj(props.width/2, props.height/2, scale, rot.value.yaw, rot.value.pitch)\n const dots = props.points.map((p,i)=>({ p, i, ...P(nx(p.x), ny(p.y), nz(p.z)),\n color: p.color || SERIES[(p.series?names.indexOf(p.series):0)%SERIES.length] })).sort((a,b)=>b.depth-a.depth)\n const C = [-0.5,0.5]; const edges = []\n C.forEach(a=>C.forEach(b=>{ edges.push([P(a,b,-0.5),P(a,b,0.5)],[P(a,-0.5,b),P(a,0.5,b)],[P(-0.5,a,b),P(0.5,a,b)]) }))\n // Axis names sit outside the cube along the screen-space outward direction.\n const axes3 = [\n { a:P(-0.5,-0.5,0.5), b:P(0.5,-0.5,0.5), name:props.xLabel },\n { a:P(-0.5,-0.5,0.5), b:P(-0.5,0.5,0.5), name:props.yLabel },\n { a:P(0.5,-0.5,0.5), b:P(0.5,-0.5,-0.5), name:props.zLabel },\n ].map(ax=>{\n const mx=(ax.a.x+ax.b.x)/2, my=(ax.a.y+ax.b.y)/2\n const dx=mx-props.width/2, dy=my-props.height/2, len=Math.hypot(dx,dy)||1\n return { ...ax, nx: mx+(dx/len)*18, ny: my+(dy/len)*18 }\n })\n return { names, dots, edges, axes3 }\n})\n</script>\n<template>\n <div :style=\"{position:'relative',display:'inline-block',lineHeight:0,...style}\">\n <svg :width=\"width\" :height=\"height\" @pointerdown=\"down\" @pointermove=\"move\" @pointerup=\"up\" @pointerleave=\"up\"\n role=\"img\" :style=\"{cursor:interactive?'grab':'default',touchAction:'none'}\">\n <line v-for=\"(e,i) in g.edges\" :key=\"'e'+i\" :x1=\"e[0].x\" :y1=\"e[0].y\" :x2=\"e[1].x\" :y2=\"e[1].y\" stroke=\"var(--chart-axis)\" stroke-width=\"1\"/>\n <line v-for=\"(ax,i) in g.axes3\" :key=\"'ae'+i\" :x1=\"ax.a.x\" :y1=\"ax.a.y\" :x2=\"ax.b.x\" :y2=\"ax.b.y\" stroke=\"var(--chart-guide)\" stroke-width=\"1\"/>\n <text v-for=\"(ax,i) in g.axes3\" :key=\"'at'+i\" :x=\"ax.nx\" :y=\"ax.ny+3\" text-anchor=\"middle\" style=\"font-family:var(--font-sans);font-size:10px;fill:var(--text-secondary)\">{{ ax.name }}</text>\n <circle v-for=\"d in g.dots\" :key=\"d.i\" :cx=\"d.x\" :cy=\"d.y\"\n :r=\"r*(1.18-(d.depth+0.75)*0.35)*(hover===d.i?1.5:1)\" :fill=\"d.color\"\n :opacity=\"hover===d.i ? 1 : (0.85-(d.depth+0.75)*0.28)*(hover==null?1:0.45)\"\n :stroke=\"hover===d.i?'var(--bg-surface)':'none'\" stroke-width=\"1.5\"\n @mouseenter=\"interactive && (hover=d.i)\" @mouseleave=\"interactive && (hover=null)\"/>\n </svg>\n <div v-if=\"hover!=null && interactive\" :style=\"{position:'absolute',left:g.dots.find(d=>d.i===hover).x+'px',top:g.dots.find(d=>d.i===hover).y+'px',transform:g.dots.find(d=>d.i===hover).x>width*0.6?'translate(calc(-100% - 10px),-50%)':'translate(10px,-50%)',pointerEvents:'none',zIndex:5,background:'var(--bg-surface)',border:'1px solid var(--border-card)',borderRadius:'var(--radius-md)',boxShadow:'var(--shadow-menu)',padding:'7px 10px',lineHeight:'normal',whiteSpace:'nowrap'}\">\n <div style=\"font-family:var(--font-sans);font-size:var(--text-2xs);color:var(--text-muted);padding-bottom:3px\">{{ points[hover].label || points[hover].series }}</div>\n <div style=\"font-family:var(--font-mono);font-size:var(--text-xs);color:var(--text-heading)\">{{ xLabel }} {{ valueFormat(points[hover].x) }} · {{ yLabel }} {{ valueFormat(points[hover].y) }} · {{ zLabel }} {{ valueFormat(points[hover].z) }}</div>\n </div>\n <div v-if=\"legend && g.names.length>1\" style=\"display:flex;gap:14px;padding-top:6px;line-height:normal\">\n <span v-for=\"(nm,i) in g.names\" :key=\"nm\" style=\"display:inline-flex;align-items:center;gap:6px\">\n <span :style=\"{width:'8px',height:'8px',borderRadius:'4px',background:SERIES[i%SERIES.length]}\"/>\n <span style=\"font-family:var(--font-sans);font-size:var(--text-xs);color:var(--text-secondary)\">{{ nm }}</span>\n </span>\n </div>\n </div>\n</template>\n","charts/Bar3D.vue":"<script setup>\nimport { ref, computed } from 'vue'\nimport { fmtNum, SERIES, stack, niceTicks } from './chart-geometry.js'\nimport { makeProj, cuboidFaces, shade, facePath } from './chart-3d.js'\n// 3D cuboid bars — categories on x, series as depth rows or stacked; drag to orbit.\nconst FACE_SHADE = { top:0.22, front:0, right:-0.22, left:-0.3, back:-0.1, bottom:-0.4 }\nconst props = defineProps({ series:{type:Array,default:()=>[]}, categories:{type:Array,default:()=>[]}, mode:{type:String,default:'grouped'}, width:{type:Number,default:460}, height:{type:Number,default:300}, yaw:{type:Number,default:30}, pitch:{type:Number,default:26}, gap:{type:Number,default:0.35}, interactive:{type:Boolean,default:true}, valueFormat:{type:Function,default:fmtNum}, style:Object })\nconst rot = ref({ yaw: props.yaw, pitch: props.pitch })\nconst hover = ref(null)\nfunction down(e){ e.currentTarget.__kbDrag = { x:e.clientX, y:e.clientY, ...rot.value }; e.currentTarget.setPointerCapture && e.currentTarget.setPointerCapture(e.pointerId) }\nfunction move(e){ const s = e.currentTarget.__kbDrag; if(!s) return; rot.value = { yaw: s.yaw+(e.clientX-s.x)*0.45, pitch: Math.max(8, Math.min(80, s.pitch+(e.clientY-s.y)*0.35)) } }\nfunction up(e){ e.currentTarget.__kbDrag = null }\nconst g = computed(() => {\n const n = props.categories.length || Math.max(...props.series.map(s=>s.values.length))\n const m = props.mode==='grouped' ? props.series.length : 1\n const hi = props.mode==='stacked'\n ? Math.max(...[...Array(n)].map((_,i)=>props.series.reduce((t,s)=>t+(s.values[i]||0),0)))\n : Math.max(...props.series.flatMap(s=>s.values))\n const bands = props.mode==='stacked' ? stack(props.series.map(s=>s.values)) : null\n const scale = Math.min(props.width, props.height)*0.42\n const P = makeProj(props.width/2, props.height/2 + props.height*0.06, scale, rot.value.yaw, rot.value.pitch)\n const bars = []\n props.series.forEach((s,si)=>s.values.forEach((v,i)=>{\n if (v==null) return\n const cx0 = i/n-0.5+(1/n)*(props.gap/2), cx1 = cx0+(1/n)*(1-props.gap)\n const zi = props.mode==='grouped' ? si : 0\n const cz0 = zi/Math.max(m,1)-0.5+(1/Math.max(m,2))*(props.gap/2), cz1 = cz0+(1/Math.max(m,2))*(1-props.gap)\n const y0 = props.mode==='stacked' ? (bands[si][i][0]/hi) : 0\n const y1 = props.mode==='stacked' ? (bands[si][i][1]/hi) : v/hi\n bars.push({ s, si, i, v, x0:cx0, x1:cx1, z0:cz0, z1:cz1, y0:(y0-0.5)*0.9, y1:(y1-0.5)*0.9 })\n }))\n const drawn = bars.map((b,key)=>{\n const faces = cuboidFaces(P, b.x0, b.x1, b.y0, b.y1, b.z0, b.z1)\n return { ...b, key, faces, depth: faces.reduce((s2,f)=>s2+f.depth,0)/faces.length,\n top: P((b.x0+b.x1)/2, b.y1, (b.z0+b.z1)/2), color: b.s.color || SERIES[b.si%SERIES.length] }\n }).sort((a,b2)=>b2.depth-a.depth)\n const base = [P(-0.5,-0.46,-0.5), P(0.5,-0.46,-0.5), P(0.5,-0.46,0.5), P(-0.5,-0.46,0.5)]\n const vt = niceTicks(0, hi, 3)\n const vTicks = vt.ticks.filter(t=>t<=hi*1.001).map(t=>({ p:P(-0.5,(t/hi-0.5)*0.9,0.5), v:props.valueFormat(t) }))\n // Labels follow whichever base edge faces the viewer, outside it.\n const zE = P(0,-0.46,0.5).y >= P(0,-0.46,-0.5).y ? 0.5 : -0.5\n const ctr = P(0,-0.46,0)\n const labels = props.categories.map((c,i)=>{\n const p0 = P(i/n-0.5+0.5/n, -0.46, zE)\n const dx = p0.x-ctr.x, dy = p0.y-ctr.y, len = Math.hypot(dx,dy)||1\n return { c, x: p0.x+(dx/len)*18, y: p0.y+(dy/len)*18+3 }\n })\n return { drawn, basePath: facePath(base), vTicks, vAxis:[P(-0.5,-0.45,0.5), P(-0.5,0.45,0.5)], labels, FACE_SHADE }\n})\n</script>\n<template>\n <div :style=\"{position:'relative',display:'inline-block',lineHeight:0,...style}\">\n <svg :width=\"width\" :height=\"height\" @pointerdown=\"down\" @pointermove=\"move\" @pointerup=\"up\" @pointerleave=\"up\"\n role=\"img\" :style=\"{cursor:interactive?'grab':'default',touchAction:'none'}\">\n <path :d=\"g.basePath\" fill=\"var(--bg-inset)\" stroke=\"var(--chart-axis)\" stroke-width=\"1\"/>\n <line :x1=\"g.vAxis[0].x\" :y1=\"g.vAxis[0].y\" :x2=\"g.vAxis[1].x\" :y2=\"g.vAxis[1].y\" stroke=\"var(--chart-guide)\" stroke-width=\"1\"/>\n <g v-for=\"(t,i) in g.vTicks\" :key=\"'vt'+i\">\n <line :x1=\"t.p.x-4\" :y1=\"t.p.y\" :x2=\"t.p.x\" :y2=\"t.p.y\" stroke=\"var(--chart-guide)\" stroke-width=\"1\"/>\n <text :x=\"t.p.x-7\" :y=\"t.p.y+3\" text-anchor=\"end\" style=\"font-family:var(--font-mono);font-size:9px;fill:var(--chart-label)\">{{ t.v }}</text>\n </g>\n <text v-for=\"(l,i) in g.labels\" :key=\"'c'+i\" :x=\"l.x\" :y=\"l.y\" text-anchor=\"middle\" style=\"font-family:var(--font-sans);font-size:10px;fill:var(--chart-label)\">{{ l.c }}</text>\n <g v-for=\"b in g.drawn\" :key=\"b.key\" :opacity=\"hover==null||hover===b.key?1:0.45\"\n @mouseenter=\"interactive && (hover=b.key)\" @mouseleave=\"interactive && (hover=null)\"\n style=\"transition:opacity var(--dur-fast) var(--ease-out)\">\n <path v-for=\"(f,fi) in b.faces\" :key=\"fi\" :d=\"f.d\" :fill=\"shade(b.color, g.FACE_SHADE[f.kind])\" stroke=\"var(--bg-surface)\" stroke-width=\"0.4\"/>\n </g>\n </svg>\n <div v-if=\"hover!=null && interactive\" :style=\"{position:'absolute',left:g.drawn.find(b=>b.key===hover).top.x+'px',top:g.drawn.find(b=>b.key===hover).top.y+'px',transform:g.drawn.find(b=>b.key===hover).top.x>width*0.6?'translate(calc(-100% - 10px),-50%)':'translate(10px,-50%)',pointerEvents:'none',zIndex:5,background:'var(--bg-surface)',border:'1px solid var(--border-card)',borderRadius:'var(--radius-md)',boxShadow:'var(--shadow-menu)',padding:'7px 10px',lineHeight:'normal',whiteSpace:'nowrap'}\">\n <div style=\"font-family:var(--font-sans);font-size:var(--text-2xs);color:var(--text-muted);padding-bottom:3px\">{{ categories[g.drawn.find(b=>b.key===hover).i] }}</div>\n <div style=\"font-family:var(--font-mono);font-size:var(--text-xs);color:var(--text-heading)\">{{ g.drawn.find(b=>b.key===hover).s.name }} · {{ valueFormat(g.drawn.find(b=>b.key===hover).v) }}</div>\n </div>\n <div style=\"display:flex;gap:14px;padding-top:6px;line-height:normal\">\n <span v-for=\"(s,i) in series\" :key=\"s.name\" style=\"display:inline-flex;align-items:center;gap:6px\">\n <span :style=\"{width:'8px',height:'8px',borderRadius:'2px',background:s.color||SERIES[i%SERIES.length]}\"/>\n <span style=\"font-family:var(--font-sans);font-size:var(--text-xs);color:var(--text-secondary)\">{{ s.name }}</span>\n </span>\n </div>\n </div>\n</template>\n","charts/Ribbon3D.vue":"<script setup>\nimport { ref, computed } from 'vue'\nimport { fmtNum, SERIES, niceTicks } from './chart-geometry.js'\nimport { makeProj, facePath, faceDepth, shade } from './chart-3d.js'\n// Each series as an extruded band at its own depth row (the classic 3D area); drag to orbit.\nconst props = defineProps({ series:{type:Array,default:()=>[]}, xLabels:{type:Array,default:()=>[]}, width:{type:Number,default:460}, height:{type:Number,default:300}, yaw:{type:Number,default:34}, pitch:{type:Number,default:30}, ribbon:{type:Number,default:0.55}, interactive:{type:Boolean,default:true}, valueFormat:{type:Function,default:fmtNum}, style:Object })\nconst rot = ref({ yaw: props.yaw, pitch: props.pitch })\nconst hover = ref(null)\nfunction down(e){ e.currentTarget.__kbDrag = { x:e.clientX, y:e.clientY, ...rot.value }; e.currentTarget.setPointerCapture && e.currentTarget.setPointerCapture(e.pointerId) }\nfunction move(e){ const s = e.currentTarget.__kbDrag; if(!s) return; rot.value = { yaw: s.yaw+(e.clientX-s.x)*0.45, pitch: Math.max(8, Math.min(80, s.pitch+(e.clientY-s.y)*0.35)) } }\nfunction up(e){ e.currentTarget.__kbDrag = null }\nconst SHADE = { top:0.14, front:-0.12, back:-0.2, end:-0.28 }\nconst g = computed(() => {\n const n = Math.max(...props.series.map(s=>s.values.length), 2)\n const hi = Math.max(...props.series.flatMap(s=>s.values)) || 1\n const m = props.series.length\n const scale = Math.min(props.width, props.height)*0.42\n const P = makeProj(props.width/2, props.height/2 + props.height*0.05, scale, rot.value.yaw, rot.value.pitch)\n const base = [P(-0.5,-0.45,-0.5), P(0.5,-0.45,-0.5), P(0.5,-0.45,0.5), P(-0.5,-0.45,0.5)]\n const rows = props.series.map((s,si)=>{\n const z0 = si/m-0.5+(1/m)*(0.5-props.ribbon/2), z1 = z0+(props.ribbon/m)\n const y = i => ((s.values[i]||0)/hi-0.5)*0.9\n const quads = []\n for (let i=0;i<n-1;i++) {\n const x0 = i/(n-1)-0.5, x1 = (i+1)/(n-1)-0.5\n quads.push({ pts:[P(x0,-0.45,z0),P(x1,-0.45,z0),P(x1,y(i+1),z0),P(x0,y(i),z0)], kind:'back' })\n quads.push({ pts:[P(x0,y(i),z0),P(x1,y(i+1),z0),P(x1,y(i+1),z1),P(x0,y(i),z1)], kind:'top' })\n quads.push({ pts:[P(x0,-0.45,z1),P(x1,-0.45,z1),P(x1,y(i+1),z1),P(x0,y(i),z1)], kind:'front' })\n }\n quads.push({ pts:[P(0.5,-0.45,z0),P(0.5,-0.45,z1),P(0.5,y(n-1),z1),P(0.5,y(n-1),z0)], kind:'end' })\n quads.push({ pts:[P(-0.5,-0.45,z0),P(-0.5,-0.45,z1),P(-0.5,y(0),z1),P(-0.5,y(0),z0)], kind:'end' })\n quads.forEach(q=>{ q.depth = faceDepth(q.pts); q.d = facePath(q.pts) })\n quads.sort((a,b)=>b.depth-a.depth)\n return { s, si, quads, rowDepth: quads.reduce((t,q)=>t+q.depth,0)/quads.length, color: s.color || SERIES[si%SERIES.length] }\n }).sort((a,b)=>b.rowDepth-a.rowDepth)\n const vt = niceTicks(0, hi, 3)\n const vTicks = vt.ticks.filter(t=>t<=hi*1.001).map(t=>({ p:P(-0.5,(t/hi-0.5)*0.9,0.5), v:fmtNum(t) }))\n const zE = P(0,-0.45,0.5).y >= P(0,-0.45,-0.5).y ? 0.62 : -0.62\n const ctr = P(0,-0.45,0)\n const every = props.xLabels.length > 8 ? Math.ceil(props.xLabels.length/8) : 1\n const labels = props.xLabels.map((lb,i)=>{\n if (lb==null || (every>1 && i%every && i!==props.xLabels.length-1)) return null\n const p0 = P(i/(n-1)-0.5, -0.45, zE)\n const dx = p0.x-ctr.x, dy = p0.y-ctr.y, len = Math.hypot(dx,dy)||1\n return { lb, x: p0.x+(dx/len)*14, y: p0.y+(dy/len)*14+3 }\n }).filter(Boolean)\n return { rows, basePath: facePath(base), vTicks, vAxis:[P(-0.5,-0.45,0.5), P(-0.5,0.45,0.5)], labels, SHADE }\n})\n</script>\n<template>\n <div :style=\"{position:'relative',display:'inline-block',lineHeight:0,...style}\">\n <svg :width=\"width\" :height=\"height\" @pointerdown=\"down\" @pointermove=\"move\" @pointerup=\"up\" @pointerleave=\"up\"\n role=\"img\" :style=\"{cursor:interactive?'grab':'default',touchAction:'none'}\">\n <path :d=\"g.basePath\" fill=\"var(--bg-inset)\" stroke=\"var(--chart-axis)\" stroke-width=\"1\"/>\n <line :x1=\"g.vAxis[0].x\" :y1=\"g.vAxis[0].y\" :x2=\"g.vAxis[1].x\" :y2=\"g.vAxis[1].y\" stroke=\"var(--chart-guide)\" stroke-width=\"1\"/>\n <g v-for=\"(t,i) in g.vTicks\" :key=\"'vt'+i\">\n <line :x1=\"t.p.x-4\" :y1=\"t.p.y\" :x2=\"t.p.x\" :y2=\"t.p.y\" stroke=\"var(--chart-guide)\" stroke-width=\"1\"/>\n <text :x=\"t.p.x-7\" :y=\"t.p.y+3\" text-anchor=\"end\" style=\"font-family:var(--font-mono);font-size:9px;fill:var(--chart-label)\">{{ t.v }}</text>\n </g>\n <text v-for=\"(l,i) in g.labels\" :key=\"'x'+i\" :x=\"l.x\" :y=\"l.y\" text-anchor=\"middle\" style=\"font-family:var(--font-sans);font-size:10px;fill:var(--chart-label)\">{{ l.lb }}</text>\n <g v-for=\"r in g.rows\" :key=\"r.s.name\" :opacity=\"hover==null||hover===r.si?1:0.4\"\n @mouseenter=\"interactive && (hover=r.si)\" @mouseleave=\"interactive && (hover=null)\"\n style=\"transition:opacity var(--dur-fast) var(--ease-out)\">\n <path v-for=\"(q,qi) in r.quads\" :key=\"qi\" :d=\"q.d\" :fill=\"shade(r.color, g.SHADE[q.kind])\" stroke=\"var(--bg-surface)\" stroke-width=\"0.4\"/>\n </g>\n </svg>\n <div v-if=\"hover!=null && interactive\" :style=\"{position:'absolute',left:(width/2)+'px',top:(height*0.28)+'px',transform:'translate(10px,-50%)',pointerEvents:'none',zIndex:5,background:'var(--bg-surface)',border:'1px solid var(--border-card)',borderRadius:'var(--radius-md)',boxShadow:'var(--shadow-menu)',padding:'7px 10px',lineHeight:'normal',whiteSpace:'nowrap'}\">\n <div style=\"font-family:var(--font-sans);font-size:var(--text-2xs);color:var(--text-muted);padding-bottom:3px\">{{ series[hover].name }}</div>\n <div style=\"font-family:var(--font-mono);font-size:var(--text-xs);color:var(--text-heading)\">peak {{ valueFormat(Math.max(...series[hover].values)) }} · latest {{ valueFormat(series[hover].values[series[hover].values.length-1]) }}</div>\n </div>\n <div style=\"display:flex;gap:14px;padding-top:6px;line-height:normal\">\n <span v-for=\"(s,i) in series\" :key=\"s.name\" @mouseenter=\"interactive && (hover=i)\" @mouseleave=\"interactive && (hover=null)\"\n :style=\"{display:'inline-flex',alignItems:'center',gap:'6px',opacity:hover==null||hover===i?1:0.45,transition:'var(--t-hover)'}\">\n <span :style=\"{width:'8px',height:'8px',borderRadius:'2px',background:s.color||SERIES[i%SERIES.length]}\"/>\n <span style=\"font-family:var(--font-sans);font-size:var(--text-xs);color:var(--text-secondary)\">{{ s.name }}</span>\n </span>\n </div>\n </div>\n</template>\n"});