@ds-mo/ui 13.6.1 → 13.7.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 (256) hide show
  1. package/dist/.package-ready.json +1 -1
  2. package/dist/agent-patterns.json +1 -1
  3. package/dist/agent.json +106 -368
  4. package/dist/angular/ds-card-chart.d.ts +18 -0
  5. package/dist/angular/{ds-card-data-viz.js → ds-card-chart.js} +10 -10
  6. package/dist/angular/ds-chart.d.ts +19 -0
  7. package/dist/angular/{ds-chart-donut.js → ds-chart.js} +15 -15
  8. package/dist/angular/ds-tooltip-chart.d.ts +12 -0
  9. package/dist/angular/{ds-chart-line.js → ds-tooltip-chart.js} +12 -12
  10. package/dist/angular/index.d.ts +1 -1
  11. package/dist/angular/index.js +3 -5
  12. package/dist/angular/proxies.d.ts +3 -5
  13. package/dist/angular/proxies.js +3 -5
  14. package/dist/components/ds-agent-activity.js +1 -1
  15. package/dist/components/ds-agent-response.js +1 -1
  16. package/dist/components/ds-agent-source-list.js +1 -1
  17. package/dist/components/ds-agent-tool-call.js +1 -1
  18. package/dist/components/ds-attachment-list.js +1 -1
  19. package/dist/components/ds-badge.js +1 -1
  20. package/dist/components/ds-banner.js +1 -1
  21. package/dist/components/ds-banner.js.map +1 -1
  22. package/dist/components/ds-bar-nav.js +1 -1
  23. package/dist/components/ds-bar-title.js +1 -1
  24. package/dist/components/ds-bar-workflow.js +1 -1
  25. package/dist/components/ds-breadcrumb.js +1 -1
  26. package/dist/components/ds-button-filled.js +1 -1
  27. package/dist/components/ds-button-unfilled.js +1 -1
  28. package/dist/components/{ds-chart-line.d.ts → ds-card-chart.d.ts} +4 -4
  29. package/dist/components/ds-card-chart.js +2 -0
  30. package/dist/components/ds-card-chart.js.map +1 -0
  31. package/dist/components/ds-card-overview.js +1 -1
  32. package/dist/components/ds-card-setting.js +1 -1
  33. package/dist/components/ds-card-setting.js.map +1 -1
  34. package/dist/components/ds-chart-legend.js +1 -1
  35. package/dist/components/ds-chart-legend.js.map +1 -1
  36. package/dist/components/{ds-chart-bar.d.ts → ds-chart.d.ts} +4 -4
  37. package/dist/components/ds-chart.js +2 -0
  38. package/dist/components/ds-chart.js.map +1 -0
  39. package/dist/components/ds-checkbox.js +1 -1
  40. package/dist/components/ds-chip.js +1 -1
  41. package/dist/components/ds-code-block.js +1 -1
  42. package/dist/components/ds-conversation-list-item.js +1 -1
  43. package/dist/components/ds-conversation-list-section.js +1 -1
  44. package/dist/components/ds-conversation-list.js +1 -1
  45. package/dist/components/ds-divider.js +1 -1
  46. package/dist/components/ds-empty-state.js +1 -1
  47. package/dist/components/ds-field.js +1 -1
  48. package/dist/components/ds-input.js +1 -1
  49. package/dist/components/ds-markdown.js +1 -1
  50. package/dist/components/ds-menu.js +1 -1
  51. package/dist/components/ds-message-actions.js +1 -1
  52. package/dist/components/ds-message-bubble.js +1 -1
  53. package/dist/components/ds-message-composer.js +1 -1
  54. package/dist/components/ds-message-composer.js.map +1 -1
  55. package/dist/components/ds-message-scroller.js +1 -1
  56. package/dist/components/ds-message.js +1 -1
  57. package/dist/components/ds-mobile-bar-nav.js +1 -1
  58. package/dist/components/ds-mobile-header.js +1 -1
  59. package/dist/components/ds-mobile-section-switcher.js +1 -1
  60. package/dist/components/ds-mobile-sheet-nav.js +1 -1
  61. package/dist/components/ds-modal.js +1 -1
  62. package/dist/components/ds-modal.js.map +1 -1
  63. package/dist/components/ds-panel-nav.js +1 -1
  64. package/dist/components/ds-panel-sub-nav.js +1 -1
  65. package/dist/components/ds-panel-tool-header.js +1 -1
  66. package/dist/components/ds-panel-tool-search.js +1 -1
  67. package/dist/components/ds-panel-tools.js +1 -1
  68. package/dist/components/ds-radio.js +1 -1
  69. package/dist/components/ds-scroll-overlay.js +1 -1
  70. package/dist/components/ds-scroll-overlay.js.map +1 -1
  71. package/dist/components/ds-select.js +1 -1
  72. package/dist/components/ds-select.js.map +1 -1
  73. package/dist/components/ds-shell-app.js +1 -1
  74. package/dist/components/ds-shell-page.js +1 -1
  75. package/dist/components/ds-shell-tools.js +1 -1
  76. package/dist/components/ds-skeleton.js +1 -1
  77. package/dist/components/ds-slider.js +1 -1
  78. package/dist/components/ds-swatch-picker.js +1 -1
  79. package/dist/components/ds-switch.js +1 -1
  80. package/dist/components/ds-tab-group.js +1 -1
  81. package/dist/components/ds-tag.js +1 -1
  82. package/dist/components/ds-text.js +1 -1
  83. package/dist/components/ds-toast.js +1 -1
  84. package/dist/components/ds-toast.js.map +1 -1
  85. package/dist/components/{ds-card-data-viz.d.ts → ds-tooltip-chart.d.ts} +4 -4
  86. package/dist/components/ds-tooltip-chart.js +2 -0
  87. package/dist/components/ds-tooltip.js +1 -1
  88. package/dist/components/ds-typing-indicator.js +1 -1
  89. package/dist/components/{p-IYy-jODg.js → p-1Zbh1MRy.js} +2 -2
  90. package/dist/components/{p-wOOAzVMC.js → p-BVsLc3t5.js} +2 -2
  91. package/dist/components/p-BZCUFHYa.js +2 -0
  92. package/dist/components/{p-D0bM3cKj.js.map → p-BZCUFHYa.js.map} +1 -1
  93. package/dist/components/{p-BBtjABBS.js → p-BjiRqLtB.js} +2 -2
  94. package/dist/components/p-Bk1A9jLm.js +2 -0
  95. package/dist/components/p-Bk1A9jLm.js.map +1 -0
  96. package/dist/components/{p-Bsfpxto1.js → p-BsBw-w1q.js} +2 -2
  97. package/dist/components/p-ByfXC1wu.js +2 -0
  98. package/dist/components/{p-1S9AW2MA.js.map → p-ByfXC1wu.js.map} +1 -1
  99. package/dist/components/{p-B6RrZQqw.js → p-C-ENnZzq.js} +2 -2
  100. package/dist/components/{p-BPyymeuR.js → p-C2U48kDH.js} +2 -2
  101. package/dist/components/{p-CKRJX0Y0.js → p-C7mCjS2t.js} +2 -2
  102. package/dist/components/{p-6B411hQA.js → p-CL8XL1eh.js} +2 -2
  103. package/dist/components/{p-DfvDNcoq.js → p-CMZVPEZ_.js} +2 -2
  104. package/dist/components/{p-CzraIk34.js → p-CWL61nKt.js} +2 -2
  105. package/dist/components/p-CWLrXsDQ.js +2 -0
  106. package/dist/components/{p-D2cjoj2r.js.map → p-CWLrXsDQ.js.map} +1 -1
  107. package/dist/components/{p-Di81y9Q2.js → p-CX7QIytv.js} +2 -2
  108. package/dist/components/p-CXduurfd.js +2 -0
  109. package/dist/components/p-CXduurfd.js.map +1 -0
  110. package/dist/components/p-CbAfTqlb.js +2 -0
  111. package/dist/components/{p-CuJ3J6OJ.js.map → p-CbAfTqlb.js.map} +1 -1
  112. package/dist/components/{p-DVuxzrCC.js → p-CfTThRNX.js} +2 -2
  113. package/dist/components/{p-BmNpPyzf.js → p-CkLffZGn.js} +2 -2
  114. package/dist/components/{p-B4_0Ys4w.js → p-CkyXU1YX.js} +2 -2
  115. package/dist/components/{p-iCjUWcp7.js → p-CywKWfgD.js} +2 -2
  116. package/dist/components/{p-BR_oMaas.js → p-D3UKwTEo.js} +2 -2
  117. package/dist/components/{p-Bl6A5AJq.js → p-D7z9bJ6n.js} +2 -2
  118. package/dist/components/{p-DaqFuVKZ.js → p-DAftUxvL.js} +2 -2
  119. package/dist/components/{p-BFfHIu6y.js → p-DQ94PkZA.js} +2 -2
  120. package/dist/components/{p-B-kRByho.js → p-DSpq7xQF.js} +2 -2
  121. package/dist/components/{p-og48Kh2i.js → p-DzM3TcyQ.js} +2 -2
  122. package/dist/components/p-GbTasczF.js +2 -0
  123. package/dist/components/{p-Ca6s5fLR.js → p-Mmr9b1bB.js} +2 -2
  124. package/dist/components/p-UH2AGfR0.js +2 -0
  125. package/dist/components/{p-DX4c3kZ1.js.map → p-UH2AGfR0.js.map} +1 -1
  126. package/dist/components/{p-CxgMUvqg.js → p-Z2zfShlF.js} +2 -2
  127. package/dist/components/{p-Bmo4T96l.js → p-_mClULFL.js} +2 -2
  128. package/dist/components/{p-Bb4f5SuF.js → p-jf1zCsuw.js} +2 -2
  129. package/dist/components/{p-Eep5mxtv.js → p-oiR4IoH1.js} +2 -2
  130. package/dist/components/{p-DHF5cLzr.js → p-u2OZHaLY.js} +2 -2
  131. package/dist/docs/components.json +180 -662
  132. package/dist/lib/utils/chart-grammar.d.ts +359 -0
  133. package/dist/lib/utils/index.d.ts +2 -0
  134. package/dist/lib/utils/index.js +504 -0
  135. package/dist/lib/utils/index.js.map +4 -4
  136. package/dist/lib/utils/token-defaults.d.ts +3 -0
  137. package/dist/mcp-data/registry/card-chart.json +238 -0
  138. package/dist/mcp-data/registry/card-overview.json +2 -2
  139. package/dist/mcp-data/registry/card-setting.json +2 -2
  140. package/dist/mcp-data/registry/chart-legend.json +11 -18
  141. package/dist/mcp-data/registry/chart.json +258 -0
  142. package/dist/mcp-data/registry/registry.json +105 -373
  143. package/dist/mcp-data/registry/tooltip-chart.json +253 -0
  144. package/dist/mcp-data/registry/tooltip.json +2 -2
  145. package/dist/react/components.d.ts +3 -5
  146. package/dist/react/components.js +3 -5
  147. package/dist/react/ds-card-chart.d.ts +12 -0
  148. package/dist/react/ds-card-chart.js +12 -0
  149. package/dist/react/ds-chart.d.ts +12 -0
  150. package/dist/react/ds-chart.js +12 -0
  151. package/dist/react/{ds-chart-bar.d.ts → ds-tooltip-chart.d.ts} +3 -3
  152. package/dist/react/ds-tooltip-chart.js +12 -0
  153. package/dist/types/components/{CardDataViz/CardDataViz.d.ts → CardChart/CardChart.d.ts} +9 -13
  154. package/dist/types/components/CardChart/index.d.ts +2 -0
  155. package/dist/types/components/Chart/Chart.d.ts +66 -0
  156. package/dist/types/components/Chart/index.d.ts +2 -0
  157. package/dist/types/components/ChartLegend/ChartLegend.d.ts +1 -1
  158. package/dist/types/components/{TooltipDataViz/TooltipDataViz.d.ts → TooltipChart/TooltipChart.d.ts} +15 -6
  159. package/dist/types/components/TooltipChart/index.d.ts +2 -0
  160. package/dist/types/components.d.ts +129 -294
  161. package/dist/types/utils/chart-grammar.d.ts +360 -0
  162. package/dist/types/utils/chart-scene.d.ts +128 -0
  163. package/dist/types/utils/chart-theme.d.ts +27 -0
  164. package/dist/types/utils/chart-types.d.ts +0 -13
  165. package/dist/types/utils/index.d.ts +2 -0
  166. package/dist/types/utils/token-defaults.d.ts +3 -0
  167. package/package.json +1 -1
  168. package/dist/angular/ds-card-data-viz.d.ts +0 -18
  169. package/dist/angular/ds-chart-bar.d.ts +0 -12
  170. package/dist/angular/ds-chart-bar.js +0 -38
  171. package/dist/angular/ds-chart-donut.d.ts +0 -19
  172. package/dist/angular/ds-chart-line.d.ts +0 -12
  173. package/dist/angular/ds-tooltip-data-viz.d.ts +0 -12
  174. package/dist/angular/ds-tooltip-data-viz.js +0 -38
  175. package/dist/components/ds-card-data-viz.js +0 -2
  176. package/dist/components/ds-card-data-viz.js.map +0 -1
  177. package/dist/components/ds-chart-bar.js +0 -2
  178. package/dist/components/ds-chart-bar.js.map +0 -1
  179. package/dist/components/ds-chart-donut.d.ts +0 -11
  180. package/dist/components/ds-chart-donut.js +0 -2
  181. package/dist/components/ds-chart-donut.js.map +0 -1
  182. package/dist/components/ds-chart-line.js +0 -2
  183. package/dist/components/ds-chart-line.js.map +0 -1
  184. package/dist/components/ds-tooltip-data-viz.d.ts +0 -11
  185. package/dist/components/ds-tooltip-data-viz.js +0 -2
  186. package/dist/components/p-1S9AW2MA.js +0 -2
  187. package/dist/components/p-B6Vuayt6.js +0 -2
  188. package/dist/components/p-B6Vuayt6.js.map +0 -1
  189. package/dist/components/p-BBpASejH.js +0 -2
  190. package/dist/components/p-BBpASejH.js.map +0 -1
  191. package/dist/components/p-BybqPD8K.js +0 -2
  192. package/dist/components/p-BybqPD8K.js.map +0 -1
  193. package/dist/components/p-C4uYvyq6.js +0 -2
  194. package/dist/components/p-C4uYvyq6.js.map +0 -1
  195. package/dist/components/p-CbjFUpOj.js +0 -2
  196. package/dist/components/p-CbjFUpOj.js.map +0 -1
  197. package/dist/components/p-CrCGIx8s.js +0 -2
  198. package/dist/components/p-CrCGIx8s.js.map +0 -1
  199. package/dist/components/p-CuJ3J6OJ.js +0 -2
  200. package/dist/components/p-D0bM3cKj.js +0 -2
  201. package/dist/components/p-D2cjoj2r.js +0 -2
  202. package/dist/components/p-DX4c3kZ1.js +0 -2
  203. package/dist/components/p-DXewjzkM.js +0 -2
  204. package/dist/components/p-DXewjzkM.js.map +0 -1
  205. package/dist/components/p-yhj1lIPx.js +0 -2
  206. package/dist/mcp-data/registry/card-data-viz.json +0 -241
  207. package/dist/mcp-data/registry/chart-bar.json +0 -245
  208. package/dist/mcp-data/registry/chart-donut.json +0 -344
  209. package/dist/mcp-data/registry/chart-line.json +0 -223
  210. package/dist/mcp-data/registry/tooltip-data-viz.json +0 -224
  211. package/dist/react/ds-card-data-viz.d.ts +0 -12
  212. package/dist/react/ds-card-data-viz.js +0 -12
  213. package/dist/react/ds-chart-bar.js +0 -12
  214. package/dist/react/ds-chart-donut.d.ts +0 -12
  215. package/dist/react/ds-chart-donut.js +0 -12
  216. package/dist/react/ds-chart-line.d.ts +0 -9
  217. package/dist/react/ds-chart-line.js +0 -12
  218. package/dist/react/ds-tooltip-data-viz.d.ts +0 -9
  219. package/dist/react/ds-tooltip-data-viz.js +0 -12
  220. package/dist/types/components/CardDataViz/index.d.ts +0 -2
  221. package/dist/types/components/ChartBar/ChartBar.d.ts +0 -31
  222. package/dist/types/components/ChartBar/index.d.ts +0 -3
  223. package/dist/types/components/ChartDonut/ChartDonut.d.ts +0 -72
  224. package/dist/types/components/ChartDonut/index.d.ts +0 -2
  225. package/dist/types/components/ChartLine/ChartLine.d.ts +0 -23
  226. package/dist/types/components/ChartLine/index.d.ts +0 -2
  227. package/dist/types/components/TooltipDataViz/index.d.ts +0 -2
  228. /package/dist/components/{ds-tooltip-data-viz.js.map → ds-tooltip-chart.js.map} +0 -0
  229. /package/dist/components/{p-IYy-jODg.js.map → p-1Zbh1MRy.js.map} +0 -0
  230. /package/dist/components/{p-wOOAzVMC.js.map → p-BVsLc3t5.js.map} +0 -0
  231. /package/dist/components/{p-BBtjABBS.js.map → p-BjiRqLtB.js.map} +0 -0
  232. /package/dist/components/{p-Bsfpxto1.js.map → p-BsBw-w1q.js.map} +0 -0
  233. /package/dist/components/{p-B6RrZQqw.js.map → p-C-ENnZzq.js.map} +0 -0
  234. /package/dist/components/{p-BPyymeuR.js.map → p-C2U48kDH.js.map} +0 -0
  235. /package/dist/components/{p-CKRJX0Y0.js.map → p-C7mCjS2t.js.map} +0 -0
  236. /package/dist/components/{p-6B411hQA.js.map → p-CL8XL1eh.js.map} +0 -0
  237. /package/dist/components/{p-DfvDNcoq.js.map → p-CMZVPEZ_.js.map} +0 -0
  238. /package/dist/components/{p-CzraIk34.js.map → p-CWL61nKt.js.map} +0 -0
  239. /package/dist/components/{p-Di81y9Q2.js.map → p-CX7QIytv.js.map} +0 -0
  240. /package/dist/components/{p-DVuxzrCC.js.map → p-CfTThRNX.js.map} +0 -0
  241. /package/dist/components/{p-BmNpPyzf.js.map → p-CkLffZGn.js.map} +0 -0
  242. /package/dist/components/{p-B4_0Ys4w.js.map → p-CkyXU1YX.js.map} +0 -0
  243. /package/dist/components/{p-iCjUWcp7.js.map → p-CywKWfgD.js.map} +0 -0
  244. /package/dist/components/{p-BR_oMaas.js.map → p-D3UKwTEo.js.map} +0 -0
  245. /package/dist/components/{p-Bl6A5AJq.js.map → p-D7z9bJ6n.js.map} +0 -0
  246. /package/dist/components/{p-DaqFuVKZ.js.map → p-DAftUxvL.js.map} +0 -0
  247. /package/dist/components/{p-BFfHIu6y.js.map → p-DQ94PkZA.js.map} +0 -0
  248. /package/dist/components/{p-B-kRByho.js.map → p-DSpq7xQF.js.map} +0 -0
  249. /package/dist/components/{p-og48Kh2i.js.map → p-DzM3TcyQ.js.map} +0 -0
  250. /package/dist/components/{p-yhj1lIPx.js.map → p-GbTasczF.js.map} +0 -0
  251. /package/dist/components/{p-Ca6s5fLR.js.map → p-Mmr9b1bB.js.map} +0 -0
  252. /package/dist/components/{p-CxgMUvqg.js.map → p-Z2zfShlF.js.map} +0 -0
  253. /package/dist/components/{p-Bmo4T96l.js.map → p-_mClULFL.js.map} +0 -0
  254. /package/dist/components/{p-Bb4f5SuF.js.map → p-jf1zCsuw.js.map} +0 -0
  255. /package/dist/components/{p-Eep5mxtv.js.map → p-oiR4IoH1.js.map} +0 -0
  256. /package/dist/components/{p-DHF5cLzr.js.map → p-u2OZHaLY.js.map} +0 -0
@@ -1,245 +0,0 @@
1
- {
2
- "$schema": "https://ui.shadcn.com/schema/registry-item.json",
3
- "name": "chart-bar",
4
- "title": "ChartBar",
5
- "description": "Bar chart for single-series magnitude, additive stacked totals, or normalized 0–100% composition across categories.",
6
- "type": "registry:ui",
7
- "meta": {
8
- "library": "@ds-mo/ui",
9
- "distribution": "npm",
10
- "storybook": "https://zainadeel.github.io/compomo/?path=/story/chart-bar",
11
- "source": "src/wc/components/ChartBar/ChartBar.tsx",
12
- "intentStatus": "complete",
13
- "intent": {
14
- "audience": "specialized",
15
- "status": "experimental",
16
- "summary": "Bar chart for single-series magnitude, additive stacked totals, or normalized 0–100% composition across categories.",
17
- "useWhen": [
18
- "Users need to compare one numeric measure across a small set of categories.",
19
- "Values are grouped into ordered time buckets such as hours, days, weeks, or months and equal visual spacing is appropriate.",
20
- "A data-viz card needs a compact magnitude comparison rather than a continuous trend.",
21
- "Several consistent series contribute to each category total or its normalized composition."
22
- ],
23
- "avoidWhen": [
24
- "Timestamp positions must reflect irregular elapsed time; pre-bucket the data or use a future continuous time-scale chart.",
25
- "The visualization needs grouped bars or precise comparison of each individual series across categories.",
26
- "The interaction requires selection, drill-down, brushing, or zoom.",
27
- "The data communicates a part-to-whole relationship."
28
- ],
29
- "alternatives": [
30
- {
31
- "when": "Users need to follow one or more trends across ordered categories.",
32
- "component": "component:ds-chart-line",
33
- "reason": "ChartLine connects ordered observations and supports multiple named series."
34
- },
35
- {
36
- "when": "The data communicates parts of a total.",
37
- "component": "component:ds-chart-donut",
38
- "reason": "ChartDonut provides the part-to-whole composition and center aggregate."
39
- }
40
- ],
41
- "commonlyComposedWith": [
42
- "component:ds-chart-legend",
43
- "component:ds-card-data-viz",
44
- "component:ds-tooltip-data-viz"
45
- ],
46
- "accessibility": [
47
- "Keep category labels concise and provide an adjacent textual summary when exact values are critical to completing a task.",
48
- "Do not rely on bar color alone to communicate category meaning.",
49
- "A future interactive tooltip must supplement rather than replace the chart's accessible value representation."
50
- ],
51
- "states": [
52
- "The chart is static without hover, selection, loading, or empty-state policy.",
53
- "Single mode reads data. Stacked and percentage modes read aligned series and categories.",
54
- "Stacked mode preserves raw totals; percentage mode normalizes every non-empty category to 100%.",
55
- "An empty dataset renders no bars; the owning card or product supplies meaningful empty-state messaging.",
56
- "Bars use 2px top corner radii while the lower corners touching the zero axis remain square.",
57
- "Use one consistent bar color across time buckets when every bar is the same measure at a different interval; categorical color changes are for genuinely distinct categories.",
58
- "Authored datum colors override the shared categorical palette."
59
- ],
60
- "responsiveBehavior": [
61
- "Width and height provide the standalone intrinsic viewport; container constraints are measured and reflow the plot across the available pixels.",
62
- "Responsive layout changes bar positions and plot extents without scaling SVG text, strokes, corner radii, or margins.",
63
- "Limit or pre-format long category labels so they remain legible at the chosen width.",
64
- "Time labels remain ordered, pre-formatted buckets with equal spacing rather than raw continuous timestamps."
65
- ],
66
- "frameworkCaveats": {
67
- "customElements": [
68
- "Assign data, series, and categories as JavaScript properties; arrays are not supported as HTML attributes."
69
- ],
70
- "react": [
71
- "Pass stable data or series/category arrays; width and height provide standalone intrinsic dimensions while constrained containers reflow automatically."
72
- ],
73
- "angular": [
74
- "Bind data or series/categories as properties; width and height provide standalone intrinsic dimensions while constrained containers reflow automatically."
75
- ]
76
- },
77
- "references": [
78
- {
79
- "label": "Storybook examples",
80
- "path": "src/wc/components/ChartBar/ChartBar.stories.ts"
81
- }
82
- ]
83
- },
84
- "api": {
85
- "props": {
86
- "categories": {
87
- "type": "string[]",
88
- "resolvedType": "string[]",
89
- "default": "[]",
90
- "required": false,
91
- "mutable": false,
92
- "description": "X-axis labels for stacked and percentage variants. Set as a JS property."
93
- },
94
- "data": {
95
- "type": "ChartDatum[]",
96
- "resolvedType": "ChartDatum[]",
97
- "default": "[]",
98
- "required": false,
99
- "mutable": false,
100
- "description": "Bars to render. Set as a JS property (not an HTML attribute)."
101
- },
102
- "height": {
103
- "type": "number",
104
- "resolvedType": "number",
105
- "attribute": "height",
106
- "default": "240",
107
- "required": false,
108
- "mutable": false,
109
- "description": "Standalone intrinsic height. Container constraints reflow the plot at rendered pixel size."
110
- },
111
- "series": {
112
- "type": "ChartSeries[]",
113
- "resolvedType": "ChartSeries[]",
114
- "default": "[]",
115
- "required": false,
116
- "mutable": false,
117
- "description": "Ordered stack series for stacked and percentage variants. Set as a JS property."
118
- },
119
- "variant": {
120
- "type": "ChartBarVariant",
121
- "resolvedType": "\"percentage\" | \"single\" | \"stacked\"",
122
- "attribute": "variant",
123
- "default": "'single'",
124
- "required": false,
125
- "mutable": false,
126
- "description": "Rendering model. Single uses `data`; stacked and percentage use `series` and `categories`."
127
- },
128
- "width": {
129
- "type": "number",
130
- "resolvedType": "number",
131
- "attribute": "width",
132
- "default": "480",
133
- "required": false,
134
- "mutable": false,
135
- "description": "Standalone intrinsic width. Container constraints reflow the plot at rendered pixel size."
136
- }
137
- },
138
- "events": [],
139
- "methods": [],
140
- "slots": []
141
- },
142
- "props": {
143
- "categories": {
144
- "type": "string[]",
145
- "resolvedType": "string[]",
146
- "default": "[]",
147
- "required": false,
148
- "mutable": false,
149
- "description": "X-axis labels for stacked and percentage variants. Set as a JS property."
150
- },
151
- "data": {
152
- "type": "ChartDatum[]",
153
- "resolvedType": "ChartDatum[]",
154
- "default": "[]",
155
- "required": false,
156
- "mutable": false,
157
- "description": "Bars to render. Set as a JS property (not an HTML attribute)."
158
- },
159
- "height": {
160
- "type": "number",
161
- "resolvedType": "number",
162
- "attribute": "height",
163
- "default": "240",
164
- "required": false,
165
- "mutable": false,
166
- "description": "Standalone intrinsic height. Container constraints reflow the plot at rendered pixel size."
167
- },
168
- "series": {
169
- "type": "ChartSeries[]",
170
- "resolvedType": "ChartSeries[]",
171
- "default": "[]",
172
- "required": false,
173
- "mutable": false,
174
- "description": "Ordered stack series for stacked and percentage variants. Set as a JS property."
175
- },
176
- "variant": {
177
- "type": "ChartBarVariant",
178
- "resolvedType": "\"percentage\" | \"single\" | \"stacked\"",
179
- "attribute": "variant",
180
- "default": "'single'",
181
- "required": false,
182
- "mutable": false,
183
- "description": "Rendering model. Single uses `data`; stacked and percentage use `series` and `categories`."
184
- },
185
- "width": {
186
- "type": "number",
187
- "resolvedType": "number",
188
- "attribute": "width",
189
- "default": "480",
190
- "required": false,
191
- "mutable": false,
192
- "description": "Standalone intrinsic width. Container constraints reflow the plot at rendered pixel size."
193
- }
194
- },
195
- "events": [],
196
- "methods": [],
197
- "slots": [],
198
- "exports": {
199
- "customElement": "ds-chart-bar",
200
- "react": "DsChartBar",
201
- "angular": "DsChartBar"
202
- },
203
- "consumption": {
204
- "install": "npm install @ds-mo/ui @ds-mo/tokens @ds-mo/icons",
205
- "cssSetup": "import '@ds-mo/tokens';\nimport '@ds-mo/tokens/reset';\nimport '@ds-mo/tokens/globals';",
206
- "customElements": {
207
- "import": "import '@ds-mo/ui/dist/components/ds-chart-bar.js';",
208
- "example": "<ds-chart-bar></ds-chart-bar>"
209
- },
210
- "react": {
211
- "import": "import { DsChartBar } from '@ds-mo/ui/react';",
212
- "example": "<DsChartBar />"
213
- },
214
- "angular": {
215
- "import": "import { DsChartBar } from '@ds-mo/ui/angular/ds-chart-bar';",
216
- "example": "<ds-chart-bar></ds-chart-bar>"
217
- },
218
- "complexPropertyNote": "Assign these non-primitive values as JavaScript properties: categories, data, series.",
219
- "peerDependencies": {
220
- "required": [
221
- "@ds-mo/tokens ^5.1.1",
222
- "@ds-mo/icons ^6.4.0"
223
- ],
224
- "frameworks": "Custom Elements; React 18/19 wrappers; Angular 19-22 standalone adapters."
225
- }
226
- }
227
- },
228
- "dependencies": [
229
- "@ds-mo/ui",
230
- "@ds-mo/tokens"
231
- ],
232
- "registryDependencies": [],
233
- "files": [
234
- {
235
- "path": "src/wc/components/ChartBar/ChartBar.css",
236
- "content": "@import '../../utils/forced-colors.css';\n\n:host {\n display: inline-block;\n max-width: 100%;\n font-family: var(--typography-font-family);\n}\n\n.chart-bar__svg {\n display: block;\n width: 100%;\n height: 100%;\n}\n\n.chart-bar__gridline {\n stroke: var(--color-border-tertiary);\n stroke-width: var(--dimension-stroke-width-012);\n vector-effect: non-scaling-stroke;\n}\n\n/* Matches ds-text text-caption regular (SVG cannot host ds-text). */\n.chart-bar__axis-label {\n fill: var(--color-foreground-secondary);\n font-family: var(--typography-font-family);\n font-size: var(--typography-fontsize-xs);\n font-weight: var(--typography-weight-medium);\n letter-spacing: var(--typography-letterspacing-positive);\n text-transform: uppercase;\n}\n\n.chart-bar__bar--rounded {\n rx: var(--dimension-radius-025);\n}\n\n.chart-bar__segment-shape--rounded {\n rx: var(--dimension-radius-025);\n}\n\n.chart-bar__segment-separator {\n stroke: var(--color-background-primary);\n stroke-width: var(--dimension-stroke-width-012);\n stroke-linecap: butt;\n vector-effect: non-scaling-stroke;\n}\n\n@media (forced-colors: active) {\n .chart-bar__bar,\n .chart-bar__segment-shape,\n .chart-bar__segment-square-base {\n forced-color-adjust: none;\n }\n\n .chart-bar__segment-separator {\n stroke: var(--ds-forced-color-surface);\n }\n}\n",
237
- "type": "registry:ui"
238
- },
239
- {
240
- "path": "src/wc/components/ChartBar/ChartBar.tsx",
241
- "content": "import { Component, Prop, State, Watch, h, Host } from '@stencil/core';\nimport { scaleBand, scaleLinear } from 'd3-scale';\nimport { max } from 'd3-array';\nimport { categoryColor } from '../../utils/chart-colors';\nimport { formatPercentage } from '../../utils';\nimport { resolveCartesianChartLayout } from '../../utils/cartesian-chart-layout';\nimport {\n ChartViewportController,\n type ChartViewportSize,\n} from '../../utils/chart-viewport-controller';\nimport type { ChartDatum, ChartSeries } from '../../utils/chart-types';\n\nexport type ChartBarVariant = 'single' | 'stacked' | 'percentage';\n\nconst PERCENTAGE_TICKS = [0, 25, 50, 75, 100];\n\n@Component({\n tag: 'ds-chart-bar',\n styleUrl: 'ChartBar.css',\n scoped: true,\n})\nexport class ChartBar {\n /** Rendering model. Single uses `data`; stacked and percentage use `series` and `categories`. */\n @Prop() variant: ChartBarVariant = 'single';\n /** Bars to render. Set as a JS property (not an HTML attribute). */\n @Prop() data: ChartDatum[] = [];\n /** Ordered stack series for stacked and percentage variants. Set as a JS property. */\n @Prop() series: ChartSeries[] = [];\n /** X-axis labels for stacked and percentage variants. Set as a JS property. */\n @Prop() categories: string[] = [];\n /** Standalone intrinsic width. Container constraints reflow the plot at rendered pixel size. */\n @Prop() width: number = 480;\n /** Standalone intrinsic height. Container constraints reflow the plot at rendered pixel size. */\n @Prop() height: number = 240;\n\n @State() private viewport: ChartViewportSize | undefined;\n\n private viewportController: ChartViewportController | null = null;\n private svgEl: SVGElement | null = null;\n\n componentDidLoad() {\n this.connectViewportController();\n }\n\n connectedCallback() {\n if (this.svgEl && !this.viewportController) this.connectViewportController();\n }\n\n componentDidRender() {\n if (!this.viewportController) this.connectViewportController();\n }\n\n private connectViewportController() {\n if (!this.svgEl) return;\n this.viewportController = new ChartViewportController(this.svgEl, viewport => {\n if (\n viewport.width !== this.viewport?.width ||\n viewport.height !== this.viewport?.height\n ) {\n this.viewport = viewport;\n }\n });\n this.viewportController.connect();\n }\n\n disconnectedCallback() {\n this.viewportController?.disconnect();\n this.viewportController = null;\n }\n\n @Watch('width')\n @Watch('height')\n handleIntrinsicSizeChange() {\n this.viewport = undefined;\n if (typeof requestAnimationFrame === 'function') {\n requestAnimationFrame(() => this.viewportController?.measure());\n }\n }\n\n private renderSingle(\n innerWidth: number,\n innerHeight: number,\n axisLabelGap: number,\n categoryLabelOffset: number\n ) {\n const yMax = max(this.data, d => d.value) ?? 0;\n const xScale = scaleBand()\n .domain(this.data.map(d => d.label))\n .range([0, innerWidth])\n .padding(0.35);\n const yScale = scaleLinear()\n .domain([0, yMax || 1])\n .range([innerHeight, 0])\n .nice();\n const yTicks = yScale.ticks(4);\n\n return (\n <g>\n {yTicks.map(tick => (\n <g key={`grid-${tick}`} transform={`translate(0, ${yScale(tick)})`}>\n <line class=\"chart-bar__gridline\" x1={0} x2={innerWidth} />\n <text\n class=\"chart-bar__axis-label\"\n x={-axisLabelGap}\n text-anchor=\"end\"\n dominant-baseline=\"middle\"\n >\n {tick}\n </text>\n </g>\n ))}\n\n {this.data.map((datum, index) => {\n const barWidth = xScale.bandwidth();\n const barX = xScale(datum.label) ?? 0;\n const barY = yScale(datum.value);\n const barHeight = innerHeight - barY;\n const squareBaseY = barY + barHeight / 2;\n const squareBaseHeight = barHeight / 2;\n const fill = datum.color ?? categoryColor(index);\n return (\n <g key={datum.label}>\n <rect\n class=\"chart-bar__bar chart-bar__bar--rounded\"\n x={barX}\n y={barY}\n width={barWidth}\n height={barHeight}\n fill={fill}\n />\n <rect\n class=\"chart-bar__bar chart-bar__bar--square-base\"\n x={barX}\n y={squareBaseY}\n width={barWidth}\n height={squareBaseHeight}\n fill={fill}\n />\n <text\n class=\"chart-bar__axis-label\"\n x={barX + barWidth / 2}\n y={innerHeight + categoryLabelOffset}\n text-anchor=\"middle\"\n >\n {datum.label}\n </text>\n </g>\n );\n })}\n </g>\n );\n }\n\n private valuesForCategory(categoryIndex: number) {\n return this.series.map(series => Math.max(0, series.data[categoryIndex] ?? 0));\n }\n\n private stackedAccessibleLabel() {\n return this.categories\n .map((category, categoryIndex) => {\n const values = this.valuesForCategory(categoryIndex);\n const total = values.reduce((sum, value) => sum + value, 0);\n const details = this.series\n .map((series, seriesIndex) => {\n const value = values[seriesIndex];\n const visibleValue =\n this.variant === 'percentage'\n ? formatPercentage(total ? value / total : 0, 1)\n : String(value);\n return `${series.name}: ${visibleValue}`;\n })\n .join(', ');\n return `${category}: ${details}`;\n })\n .join('; ');\n }\n\n private renderStacked(\n innerWidth: number,\n innerHeight: number,\n axisLabelGap: number,\n categoryLabelOffset: number\n ) {\n const categoryValues = this.categories.map((_, index) =>\n this.valuesForCategory(index)\n );\n const totals = categoryValues.map(values =>\n values.reduce((sum, value) => sum + value, 0)\n );\n const yMax = this.variant === 'percentage' ? 100 : Math.max(0, ...totals);\n const xScale = scaleBand()\n .domain(this.categories)\n .range([0, innerWidth])\n .padding(0.35);\n const yScale = scaleLinear()\n .domain([0, yMax || 1])\n .range([innerHeight, 0])\n .nice();\n const yTicks =\n this.variant === 'percentage' ? PERCENTAGE_TICKS : yScale.ticks(4);\n\n return (\n <g>\n {yTicks.map(tick => (\n <g key={`grid-${tick}`} transform={`translate(0, ${yScale(tick)})`}>\n <line class=\"chart-bar__gridline\" x1={0} x2={innerWidth} />\n <text\n class=\"chart-bar__axis-label\"\n x={-axisLabelGap}\n text-anchor=\"end\"\n dominant-baseline=\"middle\"\n >\n {this.variant === 'percentage' ? `${tick}%` : tick}\n </text>\n </g>\n ))}\n\n {this.categories.map((category, categoryIndex) => {\n const barWidth = xScale.bandwidth();\n const barX = xScale(category) ?? 0;\n const values = categoryValues[categoryIndex];\n const total = totals[categoryIndex];\n const topSeriesIndex = values.reduce(\n (topIndex, value, seriesIndex) =>\n value > 0 ? seriesIndex : topIndex,\n -1\n );\n let cumulativeValue = 0;\n\n return (\n <g class=\"chart-bar__stack\" data-category={category} key={category}>\n {this.series.map((series, seriesIndex) => {\n const rawValue = values[seriesIndex];\n const value =\n this.variant === 'percentage'\n ? total\n ? (rawValue / total) * 100\n : 0\n : rawValue;\n const segmentStart = cumulativeValue;\n const segmentEnd = cumulativeValue + value;\n cumulativeValue = segmentEnd;\n const segmentY = yScale(segmentEnd);\n const segmentBottom = yScale(segmentStart);\n const segmentHeight = Math.max(0, segmentBottom - segmentY);\n if (!segmentHeight) return null;\n\n const fill = series.color ?? categoryColor(seriesIndex);\n const hasRoundedTop =\n this.variant === 'stacked' && seriesIndex === topSeriesIndex;\n const squareBaseY = segmentY + segmentHeight / 2;\n const squareBaseHeight = segmentHeight / 2;\n\n return (\n <g\n class=\"chart-bar__segment\"\n data-series={series.name}\n key={series.name}\n >\n <rect\n class={{\n 'chart-bar__segment-shape': true,\n 'chart-bar__segment-shape--rounded': hasRoundedTop,\n }}\n x={barX}\n y={segmentY}\n width={barWidth}\n height={segmentHeight}\n fill={fill}\n />\n {hasRoundedTop ? (\n <rect\n class=\"chart-bar__segment-square-base\"\n x={barX}\n y={squareBaseY}\n width={barWidth}\n height={squareBaseHeight}\n fill={fill}\n />\n ) : null}\n {segmentStart > 0 ? (\n <line\n class=\"chart-bar__segment-separator\"\n x1={barX}\n x2={barX + barWidth}\n y1={segmentBottom}\n y2={segmentBottom}\n vector-effect=\"non-scaling-stroke\"\n />\n ) : null}\n </g>\n );\n })}\n <text\n class=\"chart-bar__axis-label\"\n x={barX + barWidth / 2}\n y={innerHeight + categoryLabelOffset}\n text-anchor=\"middle\"\n >\n {category}\n </text>\n </g>\n );\n })}\n </g>\n );\n }\n\n render() {\n const viewportWidth = this.viewport?.width ?? this.width;\n const viewportHeight = this.viewport?.height ?? this.height;\n const { margin, axisLabelGap, categoryLabelOffset } =\n resolveCartesianChartLayout();\n const innerWidth = Math.max(0, viewportWidth - margin.left - margin.right);\n const innerHeight = Math.max(0, viewportHeight - margin.top - margin.bottom);\n const isSingle = this.variant === 'single';\n const accessibleLabel = isSingle\n ? this.data.map(datum => `${datum.label}: ${datum.value}`).join(', ')\n : this.stackedAccessibleLabel();\n\n return (\n <Host\n class={{\n 'chart-bar': true,\n [`chart-bar--${this.variant}`]: true,\n }}\n >\n <svg\n class=\"chart-bar__svg\"\n ref={element => {\n this.svgEl = element ?? null;\n }}\n width={viewportWidth}\n height={viewportHeight}\n role=\"img\"\n aria-label={accessibleLabel}\n >\n <g transform={`translate(${margin.left}, ${margin.top})`}>\n {isSingle\n ? this.renderSingle(\n innerWidth,\n innerHeight,\n axisLabelGap,\n categoryLabelOffset\n )\n : this.renderStacked(\n innerWidth,\n innerHeight,\n axisLabelGap,\n categoryLabelOffset\n )}\n </g>\n </svg>\n </Host>\n );\n }\n}\n",
242
- "type": "registry:ui"
243
- }
244
- ]
245
- }
@@ -1,344 +0,0 @@
1
- {
2
- "$schema": "https://ui.shadcn.com/schema/registry-item.json",
3
- "name": "chart-donut",
4
- "title": "ChartDonut",
5
- "description": "Part-to-whole donut chart with a center aggregate, responsive fill sizing, local slice tooltip feedback, and externally controlled legend highlighting.",
6
- "type": "registry:ui",
7
- "meta": {
8
- "library": "@ds-mo/ui",
9
- "distribution": "npm",
10
- "storybook": "https://zainadeel.github.io/compomo/?path=/story/chart-donut",
11
- "source": "src/wc/components/ChartDonut/ChartDonut.tsx",
12
- "intentStatus": "complete",
13
- "intent": {
14
- "audience": "specialized",
15
- "status": "experimental",
16
- "summary": "Part-to-whole donut chart with a center aggregate, responsive fill sizing, local slice tooltip feedback, and externally controlled legend highlighting.",
17
- "useWhen": [
18
- "Users need to understand a small part-to-whole breakdown with a prominent total or status in the center.",
19
- "A standalone donut needs pointer and keyboard-focus value feedback through a chart-owned data-viz tooltip.",
20
- "A donut and visible ChartLegend should cross-highlight by matching stable labels."
21
- ],
22
- "avoidWhen": [
23
- "The data is a trend, time sequence, or independent magnitude comparison.",
24
- "The category count creates slices too small to understand reliably.",
25
- "Hover is intended to select, filter, or navigate rather than temporarily highlight data."
26
- ],
27
- "alternatives": [
28
- {
29
- "when": "The product needs the canonical card, filter action, visible legend, and built-in hover synchronization.",
30
- "component": "component:ds-card-data-viz",
31
- "reason": "The donut card variant owns that complete composition and suppresses redundant slice tooltips."
32
- },
33
- {
34
- "when": "Users need to compare independent category magnitudes.",
35
- "component": "component:ds-chart-bar",
36
- "reason": "Bars make category magnitude differences easier to compare."
37
- }
38
- ],
39
- "commonlyComposedWith": [
40
- "component:ds-chart-legend",
41
- "component:ds-tooltip-data-viz",
42
- "component:ds-card-data-viz"
43
- ],
44
- "accessibility": [
45
- "Every non-empty slice remains keyboard focusable and exposes its label and value without depending on the visual tooltip.",
46
- "Use a visible legend when the full set of labels, values, or percentages must remain continuously available.",
47
- "Provide localized no-data text for the neutral empty ring.",
48
- "Center text is a visual summary and must not be the only source of essential category detail."
49
- ],
50
- "states": [
51
- "Genuine local pointer or keyboard focus may show a chart-owned tooltip; externally synchronized highlight never opens it.",
52
- "When a visible legend already owns label and value detail, suppress the redundant donut tooltip. CardDataViz donut mode does this automatically.",
53
- "Local hover and external highlight remain separate while both may dim non-matching slices.",
54
- "Zero or negative totals render a neutral empty ring without interactive slices.",
55
- "Center value and caption truncate when they exceed the safe inner-circle measure."
56
- ],
57
- "responsiveBehavior": [
58
- "Prefer container-fill sizing inside data-viz cards and explicit sizing only in layouts that own a stable diameter.",
59
- "Fill sizing remains centered and constrained to the shared donut size band.",
60
- "Changing the measured diameter changes the ring radius without scaling its token-based thickness, typography, focus stroke, or corner treatment.",
61
- "The owner adapts category count and legend placement for available space."
62
- ],
63
- "frameworkCaveats": {
64
- "customElements": [
65
- "Assign data as a JavaScript property and wire slice-hover detail to a sibling legend only when not using CardDataViz donut mode."
66
- ],
67
- "react": [
68
- "Keep synchronized highlight controlled by the composition and disable slice tooltips when a sibling legend continuously shows the same details."
69
- ],
70
- "angular": [
71
- "Keep synchronized highlight controlled by the composition and disable slice tooltips when a sibling legend continuously shows the same details."
72
- ]
73
- },
74
- "references": [
75
- {
76
- "label": "Storybook examples",
77
- "path": "src/wc/components/ChartDonut/ChartDonut.stories.ts"
78
- }
79
- ]
80
- },
81
- "api": {
82
- "props": {
83
- "activeLabel": {
84
- "type": "string | null",
85
- "resolvedType": "null | string",
86
- "attribute": "active-label",
87
- "default": "null",
88
- "required": false,
89
- "mutable": false,
90
- "description": "Externally controlled highlight, matched by `label` — e.g. drive this from a sibling\n`ds-chart-legend`'s `dsItemHover` event to keep chart and legend hover in sync.\nFalls back to this component's own pointer/focus hover when unset.\nSlice hover dims peer slices and emits `dsSliceHover` for legend sync. External\nhighlight never opens the local tooltip."
91
- },
92
- "centerCaption": {
93
- "type": "string | undefined",
94
- "resolvedType": "string | undefined",
95
- "attribute": "center-caption",
96
- "required": false,
97
- "mutable": false,
98
- "description": "Secondary caption below the center value (e.g. \"Total\", \"Normal\"). Rendered uppercase."
99
- },
100
- "centerValue": {
101
- "type": "string | undefined",
102
- "resolvedType": "string | undefined",
103
- "attribute": "center-value",
104
- "required": false,
105
- "mutable": false,
106
- "description": "Primary center value; defaults to the sum of `data` values (e.g. \"187\", \"40.9%\")."
107
- },
108
- "cornerRadius": {
109
- "type": "number | string",
110
- "resolvedType": "number | string",
111
- "attribute": "corner-radius",
112
- "default": "TOKEN_DEFAULTS.radius025",
113
- "required": false,
114
- "mutable": false,
115
- "description": "Corner radius on each slice — number (px) or TokoMo length. Defaults to `--dimension-radius-025` (2px)."
116
- },
117
- "data": {
118
- "type": "ChartDatum[]",
119
- "resolvedType": "ChartDatum[]",
120
- "default": "[]",
121
- "required": false,
122
- "mutable": false,
123
- "description": "Slices to render. Set as a JS property (not an HTML attribute)."
124
- },
125
- "gap": {
126
- "type": "number",
127
- "resolvedType": "number",
128
- "attribute": "gap",
129
- "default": "1",
130
- "required": false,
131
- "mutable": false,
132
- "description": "Gap between slices, in degrees."
133
- },
134
- "locale": {
135
- "type": "string | undefined",
136
- "resolvedType": "string | undefined",
137
- "attribute": "locale",
138
- "required": false,
139
- "mutable": false
140
- },
141
- "noDataLabel": {
142
- "type": "string",
143
- "resolvedType": "string",
144
- "attribute": "no-data-label",
145
- "default": "'No data'",
146
- "required": false,
147
- "mutable": false
148
- },
149
- "showTooltip": {
150
- "type": "boolean",
151
- "resolvedType": "boolean",
152
- "attribute": "show-tooltip",
153
- "default": "true",
154
- "required": false,
155
- "mutable": false,
156
- "description": "Show a chart-owned value callout for genuine pointer or keyboard focus.\nDisable when a visible legend already owns persistent label and value detail."
157
- },
158
- "size": {
159
- "type": "number | undefined",
160
- "resolvedType": "number | undefined",
161
- "attribute": "size",
162
- "required": false,
163
- "mutable": false,
164
- "description": "Explicit diameter in px. When unset, the donut sizes to its container\n(ResizeObserver) clamped between `--dimension-size-base * 16` (128px) and\n`* 24` (192px), and stays centered in the leftover space. Prefer unset\ninside card layouts."
165
- },
166
- "thickness": {
167
- "type": "number | string",
168
- "resolvedType": "number | string",
169
- "attribute": "thickness",
170
- "default": "TOKEN_DEFAULTS.size200",
171
- "required": false,
172
- "mutable": false,
173
- "description": "Ring thickness — number (px) or TokoMo length. Defaults to `--dimension-size-200` (16px)."
174
- }
175
- },
176
- "events": [
177
- {
178
- "name": "dsSliceHover",
179
- "detail": "ChartDatum | null",
180
- "bubbles": true,
181
- "cancelable": true,
182
- "composed": true,
183
- "description": "Fires with the hovered/focused slice's datum, or `null` on leave/blur."
184
- }
185
- ],
186
- "methods": [],
187
- "slots": []
188
- },
189
- "props": {
190
- "activeLabel": {
191
- "type": "string | null",
192
- "resolvedType": "null | string",
193
- "attribute": "active-label",
194
- "default": "null",
195
- "required": false,
196
- "mutable": false,
197
- "description": "Externally controlled highlight, matched by `label` — e.g. drive this from a sibling\n`ds-chart-legend`'s `dsItemHover` event to keep chart and legend hover in sync.\nFalls back to this component's own pointer/focus hover when unset.\nSlice hover dims peer slices and emits `dsSliceHover` for legend sync. External\nhighlight never opens the local tooltip."
198
- },
199
- "centerCaption": {
200
- "type": "string | undefined",
201
- "resolvedType": "string | undefined",
202
- "attribute": "center-caption",
203
- "required": false,
204
- "mutable": false,
205
- "description": "Secondary caption below the center value (e.g. \"Total\", \"Normal\"). Rendered uppercase."
206
- },
207
- "centerValue": {
208
- "type": "string | undefined",
209
- "resolvedType": "string | undefined",
210
- "attribute": "center-value",
211
- "required": false,
212
- "mutable": false,
213
- "description": "Primary center value; defaults to the sum of `data` values (e.g. \"187\", \"40.9%\")."
214
- },
215
- "cornerRadius": {
216
- "type": "number | string",
217
- "resolvedType": "number | string",
218
- "attribute": "corner-radius",
219
- "default": "TOKEN_DEFAULTS.radius025",
220
- "required": false,
221
- "mutable": false,
222
- "description": "Corner radius on each slice — number (px) or TokoMo length. Defaults to `--dimension-radius-025` (2px)."
223
- },
224
- "data": {
225
- "type": "ChartDatum[]",
226
- "resolvedType": "ChartDatum[]",
227
- "default": "[]",
228
- "required": false,
229
- "mutable": false,
230
- "description": "Slices to render. Set as a JS property (not an HTML attribute)."
231
- },
232
- "gap": {
233
- "type": "number",
234
- "resolvedType": "number",
235
- "attribute": "gap",
236
- "default": "1",
237
- "required": false,
238
- "mutable": false,
239
- "description": "Gap between slices, in degrees."
240
- },
241
- "locale": {
242
- "type": "string | undefined",
243
- "resolvedType": "string | undefined",
244
- "attribute": "locale",
245
- "required": false,
246
- "mutable": false
247
- },
248
- "noDataLabel": {
249
- "type": "string",
250
- "resolvedType": "string",
251
- "attribute": "no-data-label",
252
- "default": "'No data'",
253
- "required": false,
254
- "mutable": false
255
- },
256
- "showTooltip": {
257
- "type": "boolean",
258
- "resolvedType": "boolean",
259
- "attribute": "show-tooltip",
260
- "default": "true",
261
- "required": false,
262
- "mutable": false,
263
- "description": "Show a chart-owned value callout for genuine pointer or keyboard focus.\nDisable when a visible legend already owns persistent label and value detail."
264
- },
265
- "size": {
266
- "type": "number | undefined",
267
- "resolvedType": "number | undefined",
268
- "attribute": "size",
269
- "required": false,
270
- "mutable": false,
271
- "description": "Explicit diameter in px. When unset, the donut sizes to its container\n(ResizeObserver) clamped between `--dimension-size-base * 16` (128px) and\n`* 24` (192px), and stays centered in the leftover space. Prefer unset\ninside card layouts."
272
- },
273
- "thickness": {
274
- "type": "number | string",
275
- "resolvedType": "number | string",
276
- "attribute": "thickness",
277
- "default": "TOKEN_DEFAULTS.size200",
278
- "required": false,
279
- "mutable": false,
280
- "description": "Ring thickness — number (px) or TokoMo length. Defaults to `--dimension-size-200` (16px)."
281
- }
282
- },
283
- "events": [
284
- {
285
- "name": "dsSliceHover",
286
- "detail": "ChartDatum | null",
287
- "bubbles": true,
288
- "cancelable": true,
289
- "composed": true,
290
- "description": "Fires with the hovered/focused slice's datum, or `null` on leave/blur."
291
- }
292
- ],
293
- "methods": [],
294
- "slots": [],
295
- "exports": {
296
- "customElement": "ds-chart-donut",
297
- "react": "DsChartDonut",
298
- "angular": "DsChartDonut"
299
- },
300
- "consumption": {
301
- "install": "npm install @ds-mo/ui @ds-mo/tokens @ds-mo/icons",
302
- "cssSetup": "import '@ds-mo/tokens';\nimport '@ds-mo/tokens/reset';\nimport '@ds-mo/tokens/globals';",
303
- "customElements": {
304
- "import": "import '@ds-mo/ui/dist/components/ds-chart-donut.js';",
305
- "example": "<ds-chart-donut></ds-chart-donut>"
306
- },
307
- "react": {
308
- "import": "import { DsChartDonut } from '@ds-mo/ui/react';",
309
- "example": "<DsChartDonut />"
310
- },
311
- "angular": {
312
- "import": "import { DsChartDonut } from '@ds-mo/ui/angular/ds-chart-donut';",
313
- "example": "<ds-chart-donut></ds-chart-donut>"
314
- },
315
- "complexPropertyNote": "Assign these non-primitive values as JavaScript properties: data.",
316
- "peerDependencies": {
317
- "required": [
318
- "@ds-mo/tokens ^5.1.1",
319
- "@ds-mo/icons ^6.4.0"
320
- ],
321
- "frameworks": "Custom Elements; React 18/19 wrappers; Angular 19-22 standalone adapters."
322
- }
323
- }
324
- },
325
- "dependencies": [
326
- "@ds-mo/ui",
327
- "@ds-mo/tokens"
328
- ],
329
- "registryDependencies": [
330
- "tooltip-data-viz"
331
- ],
332
- "files": [
333
- {
334
- "path": "src/wc/components/ChartDonut/ChartDonut.css",
335
- "content": "@import '../../utils/forced-colors.css';\n\n:host {\n display: inline-block;\n font-family: var(--typography-font-family);\n user-select: none;\n}\n\n:host(.chart-donut--fill) {\n display: block;\n width: 100%;\n height: 100%;\n min-width: 0;\n min-height: 0;\n}\n\n.chart-donut__wrapper {\n position: relative;\n overflow: visible;\n}\n\n:host(.chart-donut--fill) .chart-donut__wrapper {\n width: 100%;\n height: 100%;\n display: flex;\n align-items: center;\n justify-content: center;\n}\n\n.chart-donut__svg path {\n cursor: default;\n transition: opacity var(--effect-motion-short-2);\n}\n\n.chart-donut__slice--dimmed {\n opacity: var(--effect-opacity-medium);\n}\n\n@media (prefers-reduced-motion: reduce) {\n .chart-donut__svg path {\n transition: none;\n }\n}\n\n.chart-donut__svg path:focus-visible {\n outline: none;\n stroke: var(--color-interaction-focus);\n stroke-width: var(--dimension-stroke-width-025);\n vector-effect: non-scaling-stroke;\n}\n\n/* Matches ds-text text-title-large + emphasis (SVG cannot host ds-text). */\n.chart-donut__center-value {\n fill: var(--color-foreground-primary);\n font-family: var(--typography-font-family);\n font-size: var(--typography-fontsize-xl);\n line-height: var(--typography-lineheight-xl);\n font-weight: var(--typography-weight-semibold);\n letter-spacing: var(--typography-letterspacing-negative-double);\n cursor: default;\n pointer-events: none;\n}\n\n/* Matches ds-text text-body-medium regular. */\n.chart-donut__center-caption {\n fill: var(--color-foreground-secondary);\n font-family: var(--typography-font-family);\n font-size: var(--typography-fontsize-md);\n line-height: var(--typography-lineheight-md);\n font-weight: var(--typography-weight-regular);\n letter-spacing: var(--typography-letterspacing-negative-half);\n cursor: default;\n pointer-events: none;\n}\n\n@media (forced-colors: active) {\n .chart-donut__svg path {\n forced-color-adjust: none;\n }\n\n .chart-donut__svg path:focus-visible {\n stroke: var(--ds-forced-color-selected);\n }\n}\n",
336
- "type": "registry:ui"
337
- },
338
- {
339
- "path": "src/wc/components/ChartDonut/ChartDonut.tsx",
340
- "content": "import { Component, Prop, State, Event, EventEmitter, Element, Watch, h, Host } from '@stencil/core';\nimport { arc, pie } from 'd3-shape';\nimport { categoryColor } from '../../utils/chart-colors';\nimport { formatCompactNumber, resolveCssLengthPx, truncateSvgTextToWidth, TOKEN_DEFAULTS } from '../../utils';\nimport type { ChartDatum } from '../../utils/chart-types';\n\n/** Gap between the center value and caption line boxes. */\nconst CENTER_GAP_PX = 0;\n/** Fraction of the inner-circle diameter center text is allowed to use before truncating. */\nconst CENTER_TEXT_SAFE_WIDTH_RATIO = 0.82;\n/** Fallback when filling before the first ResizeObserver measurement (matches fill min). */\nconst FILL_FALLBACK_PX = 128;\n\n@Component({\n tag: 'ds-chart-donut',\n styleUrl: 'ChartDonut.css',\n scoped: true,\n})\nexport class ChartDonut {\n @Prop() locale: string | undefined;\n @Prop() noDataLabel: string = 'No data';\n @Element() el!: HTMLElement;\n\n /** Slices to render. Set as a JS property (not an HTML attribute). */\n @Prop() data: ChartDatum[] = [];\n /**\n * Explicit diameter in px. When unset, the donut sizes to its container\n * (ResizeObserver) clamped between `--dimension-size-base * 16` (128px) and\n * `* 24` (192px), and stays centered in the leftover space. Prefer unset\n * inside card layouts.\n */\n @Prop() size: number | undefined;\n /** Ring thickness — number (px) or TokoMo length. Defaults to `--dimension-size-200` (16px). */\n @Prop() thickness: number | string = TOKEN_DEFAULTS.size200;\n /** Corner radius on each slice — number (px) or TokoMo length. Defaults to `--dimension-radius-025` (2px). */\n @Prop() cornerRadius: number | string = TOKEN_DEFAULTS.radius025;\n /** Gap between slices, in degrees. */\n @Prop() gap: number = 1;\n /**\n * Show a chart-owned value callout for genuine pointer or keyboard focus.\n * Disable when a visible legend already owns persistent label and value detail.\n */\n @Prop() showTooltip: boolean = true;\n /** Primary center value; defaults to the sum of `data` values (e.g. \"187\", \"40.9%\"). */\n @Prop() centerValue: string | undefined;\n /** Secondary caption below the center value (e.g. \"Total\", \"Normal\"). Rendered uppercase. */\n @Prop() centerCaption: string | undefined;\n /**\n * Externally controlled highlight, matched by `label` — e.g. drive this from a sibling\n * `ds-chart-legend`'s `dsItemHover` event to keep chart and legend hover in sync.\n * Falls back to this component's own pointer/focus hover when unset.\n * Slice hover dims peer slices and emits `dsSliceHover` for legend sync. External\n * highlight never opens the local tooltip.\n */\n @Prop() activeLabel: string | null = null;\n\n @State() private hoveredLabel: string | null = null;\n /** Center text, truncated with an ellipsis if it doesn't fit inside the inner circle. */\n @State() private displayedValueText: string = '';\n @State() private displayedCaptionText: string = '';\n /** Measured fill diameter when `size` is unset. */\n @State() private measuredSize: number = FILL_FALLBACK_PX;\n @State() private tooltipX: number = 0;\n @State() private tooltipY: number = 0;\n\n /** Fires with the hovered/focused slice's datum, or `null` on leave/blur. */\n @Event() dsSliceHover!: EventEmitter<ChartDatum | null>;\n\n private valueTextEl?: SVGTextElement;\n private captionTextEl?: SVGTextElement;\n private resizeObserver: ResizeObserver | null = null;\n\n private get isFillMode(): boolean {\n return this.size == null || !Number.isFinite(this.size) || this.size <= 0;\n }\n\n private get fillMinPx(): number {\n return resolveCssLengthPx(TOKEN_DEFAULTS.donutFillMin, FILL_FALLBACK_PX);\n }\n\n private get fillMaxPx(): number {\n return resolveCssLengthPx(TOKEN_DEFAULTS.donutFillMax, 192);\n }\n\n /** Clamp available square side into the fill min/max band. */\n private clampFillSize(availablePx: number): number {\n const minPx = this.fillMinPx;\n const maxPx = this.fillMaxPx;\n if (!(availablePx > 0)) return minPx;\n return Math.round(Math.min(maxPx, Math.max(minPx, availablePx)));\n }\n\n private get resolvedSize(): number {\n if (!this.isFillMode) return this.size as number;\n return this.clampFillSize(this.measuredSize);\n }\n\n componentDidLoad() {\n this.syncResizeObserver();\n }\n\n @Watch('size')\n onSizePropChange() {\n this.syncResizeObserver();\n }\n\n disconnectedCallback() {\n this.teardownResizeObserver();\n }\n\n private syncResizeObserver() {\n this.teardownResizeObserver();\n if (!this.isFillMode) return;\n // Observe the host (which fills its parent), not the wrapper — the wrapper\n // only wraps the SVG and would collapse to the clamped size, locking fill at min.\n const target = this.el;\n if (!target || typeof ResizeObserver === 'undefined') return;\n\n this.resizeObserver = new ResizeObserver(entries => {\n const entry = entries[0];\n if (!entry) return;\n this.applyAvailableSize(entry.contentRect.width, entry.contentRect.height);\n });\n this.resizeObserver.observe(target);\n this.measureHost();\n }\n\n private measureHost() {\n const rect = this.el.getBoundingClientRect();\n this.applyAvailableSize(rect.width, rect.height);\n }\n\n private applyAvailableSize(width: number, height: number) {\n const available = Math.floor(Math.min(width, height));\n if (available > 0 && available !== this.measuredSize) {\n this.measuredSize = available;\n }\n }\n\n private teardownResizeObserver() {\n this.resizeObserver?.disconnect();\n this.resizeObserver = null;\n }\n\n private handleHover(datum: ChartDatum | null) {\n this.hoveredLabel = datum?.label ?? null;\n this.dsSliceHover.emit(datum);\n }\n\n private get tooltipDatum(): ChartDatum | null {\n if (!this.showTooltip || this.hoveredLabel == null) return null;\n return this.data.find(datum => datum.label === this.hoveredLabel) ?? null;\n }\n\n private updateTooltipFromPointer(event: MouseEvent) {\n if (!this.showTooltip) return;\n const wrapper = this.el.querySelector<HTMLElement>('.chart-donut__wrapper');\n if (!wrapper) return;\n const rect = wrapper.getBoundingClientRect();\n this.tooltipX = event.clientX - rect.left;\n this.tooltipY = event.clientY - rect.top;\n }\n\n private updateTooltipFromSvgPoint(\n path: SVGPathElement,\n svgX: number,\n svgY: number,\n viewBoxSize: number,\n ) {\n if (!this.showTooltip) return;\n const wrapper = this.el.querySelector<HTMLElement>('.chart-donut__wrapper');\n const svg = path.ownerSVGElement;\n if (!wrapper || !svg) return;\n const wrapperRect = wrapper.getBoundingClientRect();\n const svgRect = svg.getBoundingClientRect();\n this.tooltipX = svgRect.left - wrapperRect.left + svgX * (svgRect.width / viewBoxSize);\n this.tooltipY = svgRect.top - wrapperRect.top + svgY * (svgRect.height / viewBoxSize);\n }\n\n componentDidRender() {\n // Re-bind after HMR when DidLoad's observer was dropped.\n if (this.isFillMode && !this.resizeObserver) {\n this.syncResizeObserver();\n }\n\n const size = this.resolvedSize;\n const radius = size / 2;\n const thicknessPx = resolveCssLengthPx(this.thickness, TOKEN_DEFAULTS.size200);\n const innerRadius = radius - thicknessPx;\n const maxWidth = innerRadius * 2 * CENTER_TEXT_SAFE_WIDTH_RATIO;\n const total = this.data.reduce((sum, d) => sum + d.value, 0);\n\n const fullValueText = this.centerValue ?? formatCompactNumber(total, this.locale);\n if (this.valueTextEl) {\n const truncated = truncateSvgTextToWidth(this.valueTextEl, fullValueText, maxWidth);\n if (truncated !== this.displayedValueText) this.displayedValueText = truncated;\n else this.valueTextEl.textContent = truncated;\n }\n\n const fullCaptionText = this.centerCaption ?? '';\n if (this.captionTextEl && fullCaptionText) {\n const truncated = truncateSvgTextToWidth(this.captionTextEl, fullCaptionText, maxWidth);\n if (truncated !== this.displayedCaptionText) this.displayedCaptionText = truncated;\n else this.captionTextEl.textContent = truncated;\n }\n }\n\n render() {\n const size = this.resolvedSize;\n const radius = size / 2;\n const thicknessPx = resolveCssLengthPx(this.thickness, TOKEN_DEFAULTS.size200);\n const cornerRadiusPx = resolveCssLengthPx(this.cornerRadius, TOKEN_DEFAULTS.radius025);\n const innerRadius = radius - thicknessPx;\n const total = this.data.reduce((sum, d) => sum + d.value, 0);\n // d3's pie() divides each value by the total to get an angle — with an all-zero total\n // that's 0/0, producing NaN angles and broken paths. Render a flat neutral ring instead.\n const isEmpty = total <= 0;\n // Highlight/dim from either this component's own hover or an externally-synced label\n // (e.g. a sibling ds-chart-legend row).\n const highlightLabel = this.activeLabel ?? this.hoveredLabel;\n\n const arcGenerator = arc<{ startAngle: number; endAngle: number }>()\n .innerRadius(innerRadius)\n .outerRadius(radius)\n .cornerRadius(cornerRadiusPx)\n .padAngle((this.gap * Math.PI) / 180);\n\n const slices = isEmpty\n ? []\n : pie<ChartDatum>()\n .value(d => d.value)\n .sort(null)(this.data);\n\n const emptyRingPath = isEmpty\n ? arc<{ startAngle: number; endAngle: number }>()\n .innerRadius(innerRadius)\n .outerRadius(radius)({ startAngle: 0, endAngle: Math.PI * 2 })\n : null;\n\n // Precise vertical centering for the two-line center text, using the real line-height of\n // text-title-large (value) and text-body-medium (caption) rather than eyeballed em offsets.\n const valueLineHeight = resolveCssLengthPx(TOKEN_DEFAULTS.lineheightXl, 32);\n const captionLineHeight = resolveCssLengthPx(TOKEN_DEFAULTS.lineheightMd, 20);\n const centerValueY = this.centerCaption ? radius - (CENTER_GAP_PX + captionLineHeight) / 2 : radius;\n const centerCaptionY = radius + (valueLineHeight + CENTER_GAP_PX) / 2;\n\n // Show the full text before the post-render truncation pass has measured it (avoids a blank first paint).\n const fullValueText = this.centerValue ?? formatCompactNumber(total, this.locale);\n const fullCaptionText = this.centerCaption ?? '';\n const fill = this.isFillMode;\n\n return (\n <Host\n class={{\n 'chart-donut': true,\n 'chart-donut--fill': fill,\n 'chart-donut--fixed': !fill,\n }}\n style={!fill ? { width: `${size}px`, height: `${size}px` } : undefined}\n >\n <div\n class=\"chart-donut__wrapper\"\n style={!fill ? { width: `${size}px`, height: `${size}px` } : undefined}\n >\n <svg\n class=\"chart-donut__svg\"\n viewBox={`0 0 ${size} ${size}`}\n width={size}\n height={size}\n role={isEmpty ? 'img' : undefined}\n aria-label={isEmpty ? this.noDataLabel : undefined}\n >\n <g transform={`translate(${radius}, ${radius})`}>\n {isEmpty ? (\n <path d={emptyRingPath ?? undefined} fill=\"var(--color-background-faint-neutral)\" aria-hidden=\"true\" />\n ) : (\n slices.map((slice, i) => {\n const datum = this.data[i];\n const isDimmed = highlightLabel != null && datum.label !== highlightLabel;\n const [centroidX, centroidY] = arcGenerator.centroid(slice);\n return (\n <path\n key={datum.label}\n class={{ 'chart-donut__slice--dimmed': isDimmed }}\n d={arcGenerator(slice) ?? undefined}\n fill={datum.color ?? categoryColor(i)}\n tabindex={0}\n role=\"img\"\n aria-label={`${datum.label}: ${datum.value}`}\n onMouseEnter={(event: MouseEvent) => {\n this.updateTooltipFromPointer(event);\n this.handleHover(datum);\n }}\n onMouseMove={(event: MouseEvent) => this.updateTooltipFromPointer(event)}\n onMouseLeave={() => this.handleHover(null)}\n onFocus={(event: FocusEvent) => {\n this.updateTooltipFromSvgPoint(\n event.currentTarget as SVGPathElement,\n radius + centroidX,\n radius + centroidY,\n size,\n );\n this.handleHover(datum);\n }}\n onBlur={() => this.handleHover(null)}\n />\n );\n })\n )}\n </g>\n <text\n class=\"chart-donut__center-value\"\n x={radius}\n y={centerValueY}\n text-anchor=\"middle\"\n dominant-baseline=\"central\"\n ref={el => {\n this.valueTextEl = (el as SVGTextElement) ?? undefined;\n }}\n >\n {this.displayedValueText || fullValueText}\n </text>\n {fullCaptionText && (\n <text\n class=\"chart-donut__center-caption\"\n x={radius}\n y={centerCaptionY}\n text-anchor=\"middle\"\n dominant-baseline=\"central\"\n ref={el => {\n this.captionTextEl = (el as SVGTextElement) ?? undefined;\n }}\n >\n {this.displayedCaptionText || fullCaptionText}\n </text>\n )}\n </svg>\n {this.tooltipDatum && (\n <ds-tooltip-data-viz\n label={this.tooltipDatum.label}\n value={formatCompactNumber(this.tooltipDatum.value, this.locale)}\n x={this.tooltipX}\n y={this.tooltipY}\n />\n )}\n </div>\n </Host>\n );\n }\n}\n",
341
- "type": "registry:ui"
342
- }
343
- ]
344
- }