@blukavantui/charts-community 0.0.0-stage → 1.2.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 (262) hide show
  1. package/LICENSE.txt +103 -0
  2. package/README.md +98 -2
  3. package/THIRD_PARTY_NOTICES.txt +288 -0
  4. package/dist/accessibility.d.ts +53 -0
  5. package/dist/async.d.ts +2 -0
  6. package/dist/bar.css +1 -0
  7. package/dist/bar.d.ts +5 -0
  8. package/dist/bar.js +1 -0
  9. package/dist/browser/charts-community.css +1 -0
  10. package/dist/browser/charts-community.js +52 -0
  11. package/dist/build-audit.json +209 -0
  12. package/dist/builders/bar.d.ts +6 -0
  13. package/dist/builders/doughnut.d.ts +6 -0
  14. package/dist/builders/heatmap.d.ts +16 -0
  15. package/dist/builders/line.d.ts +6 -0
  16. package/dist/builders/pie.d.ts +6 -0
  17. package/dist/builders/radar.d.ts +6 -0
  18. package/dist/builders/scatter.d.ts +6 -0
  19. package/dist/catalogue-model.d.ts +11 -0
  20. package/dist/catalogue-runtime.d.ts +23 -0
  21. package/dist/catalogue-types.d.ts +167 -0
  22. package/dist/chart-state.d.ts +18 -0
  23. package/dist/chunk-2GUWWKHW.js +1 -0
  24. package/dist/chunk-3PITHTEO.js +1 -0
  25. package/dist/chunk-4B5MQ6GZ.js +1 -0
  26. package/dist/chunk-4EQMOBBN.js +1 -0
  27. package/dist/chunk-4WDZUJDS.js +1 -0
  28. package/dist/chunk-5NUOOMHV.js +1 -0
  29. package/dist/chunk-7OCMRHIH.js +1 -0
  30. package/dist/chunk-CF5Q2DDP.js +1 -0
  31. package/dist/chunk-EITBQBN2.js +1 -0
  32. package/dist/chunk-FXNKAHRG.js +1 -0
  33. package/dist/chunk-IDMAFTWV.js +1 -0
  34. package/dist/chunk-ILTENI35.js +1 -0
  35. package/dist/chunk-KRMM7EWI.js +1 -0
  36. package/dist/chunk-LUUPBBD4.js +1 -0
  37. package/dist/chunk-MR5VITXX.js +1 -0
  38. package/dist/chunk-NASBRHIW.js +1 -0
  39. package/dist/chunk-NXDBBMF5.js +1 -0
  40. package/dist/chunk-OQ6MP7PY.js +1 -0
  41. package/dist/chunk-RC2TCMJF.js +1 -0
  42. package/dist/chunk-RXOGAN23.js +1 -0
  43. package/dist/chunk-UGXWOWAG.js +2 -0
  44. package/dist/chunk-UO2KNHTU.js +2 -0
  45. package/dist/chunk-UQMMBG26.js +3 -0
  46. package/dist/chunk-VTJ4XPRV.js +1 -0
  47. package/dist/chunk-WJ3UHH6J.js +1 -0
  48. package/dist/chunk-XLJUSNGV.js +1 -0
  49. package/dist/colors.d.ts +15 -0
  50. package/dist/community.d.ts +11 -0
  51. package/dist/doughnut.css +1 -0
  52. package/dist/doughnut.d.ts +5 -0
  53. package/dist/doughnut.js +1 -0
  54. package/dist/export.d.ts +5 -0
  55. package/dist/fonts/dm-sans-LICENSE.txt +93 -0
  56. package/dist/fonts/dm-sans-latin-ext-wght-italic.woff2 +0 -0
  57. package/dist/fonts/dm-sans-latin-ext-wght-normal.woff2 +0 -0
  58. package/dist/fonts/dm-sans-latin-wght-italic.woff2 +0 -0
  59. package/dist/fonts/dm-sans-latin-wght-normal.woff2 +0 -0
  60. package/dist/fonts/ibm-plex-mono-LICENSE.txt +93 -0
  61. package/dist/fonts/ibm-plex-mono-latin-100-italic.woff2 +0 -0
  62. package/dist/fonts/ibm-plex-mono-latin-100-normal.woff2 +0 -0
  63. package/dist/fonts/ibm-plex-mono-latin-200-italic.woff2 +0 -0
  64. package/dist/fonts/ibm-plex-mono-latin-200-normal.woff2 +0 -0
  65. package/dist/fonts/ibm-plex-mono-latin-300-italic.woff2 +0 -0
  66. package/dist/fonts/ibm-plex-mono-latin-300-normal.woff2 +0 -0
  67. package/dist/fonts/ibm-plex-mono-latin-400-italic.woff2 +0 -0
  68. package/dist/fonts/ibm-plex-mono-latin-400-normal.woff2 +0 -0
  69. package/dist/fonts/ibm-plex-mono-latin-500-italic.woff2 +0 -0
  70. package/dist/fonts/ibm-plex-mono-latin-500-normal.woff2 +0 -0
  71. package/dist/fonts/ibm-plex-mono-latin-600-italic.woff2 +0 -0
  72. package/dist/fonts/ibm-plex-mono-latin-600-normal.woff2 +0 -0
  73. package/dist/fonts/ibm-plex-mono-latin-700-italic.woff2 +0 -0
  74. package/dist/fonts/ibm-plex-mono-latin-700-normal.woff2 +0 -0
  75. package/dist/fonts/ibm-plex-mono-latin-ext-100-italic.woff2 +0 -0
  76. package/dist/fonts/ibm-plex-mono-latin-ext-100-normal.woff2 +0 -0
  77. package/dist/fonts/ibm-plex-mono-latin-ext-200-italic.woff2 +0 -0
  78. package/dist/fonts/ibm-plex-mono-latin-ext-200-normal.woff2 +0 -0
  79. package/dist/fonts/ibm-plex-mono-latin-ext-300-italic.woff2 +0 -0
  80. package/dist/fonts/ibm-plex-mono-latin-ext-300-normal.woff2 +0 -0
  81. package/dist/fonts/ibm-plex-mono-latin-ext-400-italic.woff2 +0 -0
  82. package/dist/fonts/ibm-plex-mono-latin-ext-400-normal.woff2 +0 -0
  83. package/dist/fonts/ibm-plex-mono-latin-ext-500-italic.woff2 +0 -0
  84. package/dist/fonts/ibm-plex-mono-latin-ext-500-normal.woff2 +0 -0
  85. package/dist/fonts/ibm-plex-mono-latin-ext-600-italic.woff2 +0 -0
  86. package/dist/fonts/ibm-plex-mono-latin-ext-600-normal.woff2 +0 -0
  87. package/dist/fonts/ibm-plex-mono-latin-ext-700-italic.woff2 +0 -0
  88. package/dist/fonts/ibm-plex-mono-latin-ext-700-normal.woff2 +0 -0
  89. package/dist/fonts/inter-LICENSE.txt +93 -0
  90. package/dist/fonts/inter-latin-ext-wght-italic.woff2 +0 -0
  91. package/dist/fonts/inter-latin-ext-wght-normal.woff2 +0 -0
  92. package/dist/fonts/inter-latin-wght-italic.woff2 +0 -0
  93. package/dist/fonts/inter-latin-wght-normal.woff2 +0 -0
  94. package/dist/fonts/jetbrains-mono-LICENSE.txt +93 -0
  95. package/dist/fonts/jetbrains-mono-latin-ext-wght-italic.woff2 +0 -0
  96. package/dist/fonts/jetbrains-mono-latin-ext-wght-normal.woff2 +0 -0
  97. package/dist/fonts/jetbrains-mono-latin-wght-italic.woff2 +0 -0
  98. package/dist/fonts/jetbrains-mono-latin-wght-normal.woff2 +0 -0
  99. package/dist/fonts/lora-LICENSE.txt +93 -0
  100. package/dist/fonts/lora-latin-ext-wght-italic.woff2 +0 -0
  101. package/dist/fonts/lora-latin-ext-wght-normal.woff2 +0 -0
  102. package/dist/fonts/lora-latin-wght-italic.woff2 +0 -0
  103. package/dist/fonts/lora-latin-wght-normal.woff2 +0 -0
  104. package/dist/fonts/manifest.json +618 -0
  105. package/dist/fonts/manrope-LICENSE.txt +93 -0
  106. package/dist/fonts/manrope-latin-ext-wght-normal.woff2 +0 -0
  107. package/dist/fonts/manrope-latin-wght-normal.woff2 +0 -0
  108. package/dist/fonts/merriweather-LICENSE.txt +93 -0
  109. package/dist/fonts/merriweather-latin-ext-wght-italic.woff2 +0 -0
  110. package/dist/fonts/merriweather-latin-ext-wght-normal.woff2 +0 -0
  111. package/dist/fonts/merriweather-latin-wght-italic.woff2 +0 -0
  112. package/dist/fonts/merriweather-latin-wght-normal.woff2 +0 -0
  113. package/dist/fonts/montserrat-LICENSE.txt +93 -0
  114. package/dist/fonts/montserrat-latin-ext-wght-italic.woff2 +0 -0
  115. package/dist/fonts/montserrat-latin-ext-wght-normal.woff2 +0 -0
  116. package/dist/fonts/montserrat-latin-wght-italic.woff2 +0 -0
  117. package/dist/fonts/montserrat-latin-wght-normal.woff2 +0 -0
  118. package/dist/fonts/playfair-display-LICENSE.txt +93 -0
  119. package/dist/fonts/playfair-display-latin-ext-wght-italic.woff2 +0 -0
  120. package/dist/fonts/playfair-display-latin-ext-wght-normal.woff2 +0 -0
  121. package/dist/fonts/playfair-display-latin-wght-italic.woff2 +0 -0
  122. package/dist/fonts/playfair-display-latin-wght-normal.woff2 +0 -0
  123. package/dist/fonts/roboto-LICENSE.txt +93 -0
  124. package/dist/fonts/roboto-latin-ext-wght-italic.woff2 +0 -0
  125. package/dist/fonts/roboto-latin-ext-wght-normal.woff2 +0 -0
  126. package/dist/fonts/roboto-latin-wght-italic.woff2 +0 -0
  127. package/dist/fonts/roboto-latin-wght-normal.woff2 +0 -0
  128. package/dist/fonts/source-sans-3-LICENSE.txt +93 -0
  129. package/dist/fonts/source-sans-3-latin-ext-wght-italic.woff2 +0 -0
  130. package/dist/fonts/source-sans-3-latin-ext-wght-normal.woff2 +0 -0
  131. package/dist/fonts/source-sans-3-latin-wght-italic.woff2 +0 -0
  132. package/dist/fonts/source-sans-3-latin-wght-normal.woff2 +0 -0
  133. package/dist/heatmap.css +1 -0
  134. package/dist/heatmap.d.ts +3 -0
  135. package/dist/heatmap.js +1 -0
  136. package/dist/index.css +1 -0
  137. package/dist/index.d.ts +11 -0
  138. package/dist/index.js +1 -0
  139. package/dist/legend-export.d.ts +9 -0
  140. package/dist/legend-model.d.ts +39 -0
  141. package/dist/legend.d.ts +19 -0
  142. package/dist/limits.d.ts +16 -0
  143. package/dist/line.css +1 -0
  144. package/dist/line.d.ts +5 -0
  145. package/dist/line.js +1 -0
  146. package/dist/loading-state.d.ts +6 -0
  147. package/dist/map-assets.d.ts +4 -0
  148. package/dist/map-resources.d.ts +24 -0
  149. package/dist/messages.d.ts +3 -0
  150. package/dist/model.d.ts +41 -0
  151. package/dist/motion/bar.css +1 -0
  152. package/dist/motion/bar.d.ts +8 -0
  153. package/dist/motion/bar.js +1 -0
  154. package/dist/motion/doughnut.css +1 -0
  155. package/dist/motion/doughnut.d.ts +8 -0
  156. package/dist/motion/doughnut.js +1 -0
  157. package/dist/motion/heatmap.css +1 -0
  158. package/dist/motion/heatmap.d.ts +7 -0
  159. package/dist/motion/heatmap.js +1 -0
  160. package/dist/motion/line.css +1 -0
  161. package/dist/motion/line.d.ts +8 -0
  162. package/dist/motion/line.js +1 -0
  163. package/dist/motion/pie.css +1 -0
  164. package/dist/motion/pie.d.ts +8 -0
  165. package/dist/motion/pie.js +1 -0
  166. package/dist/motion/radar.css +1 -0
  167. package/dist/motion/radar.d.ts +8 -0
  168. package/dist/motion/radar.js +1 -0
  169. package/dist/motion/scatter.css +1 -0
  170. package/dist/motion/scatter.d.ts +8 -0
  171. package/dist/motion/scatter.js +1 -0
  172. package/dist/motion/sparkline.d.ts +7 -0
  173. package/dist/motion/sparkline.js +1 -0
  174. package/dist/motion-collection.d.ts +7 -0
  175. package/dist/motion-community.d.ts +40 -0
  176. package/dist/motion-policy.d.ts +14 -0
  177. package/dist/motion.css +1 -0
  178. package/dist/motion.d.ts +40 -0
  179. package/dist/motion.js +1 -0
  180. package/dist/options.d.ts +11 -0
  181. package/dist/pie.css +1 -0
  182. package/dist/pie.d.ts +5 -0
  183. package/dist/pie.js +1 -0
  184. package/dist/radar.css +1 -0
  185. package/dist/radar.d.ts +4 -0
  186. package/dist/radar.js +1 -0
  187. package/dist/runtime.d.ts +4 -0
  188. package/dist/scatter.css +1 -0
  189. package/dist/scatter.d.ts +5 -0
  190. package/dist/scatter.js +1 -0
  191. package/dist/sparkline-runtime/runtime.d.ts +10 -0
  192. package/dist/sparkline-validation.d.ts +3 -0
  193. package/dist/sparkline.css +25 -0
  194. package/dist/sparkline.d.ts +44 -0
  195. package/dist/sparkline.js +1 -0
  196. package/dist/state-announcement.d.ts +6 -0
  197. package/dist/styles.css +496 -0
  198. package/dist/theme.css +1391 -0
  199. package/dist/types.d.ts +137 -0
  200. package/dist/validation.d.ts +8 -0
  201. package/dist/web-api.json +4305 -0
  202. package/dist/web-motion-sparkline.d.ts +4 -0
  203. package/edition.json +48 -0
  204. package/package.json +103 -4
  205. package/src/accessibility.ts +81 -0
  206. package/src/assets.d.ts +1 -0
  207. package/src/async.ts +12 -0
  208. package/src/bar.ts +13 -0
  209. package/src/builders/bar.ts +39 -0
  210. package/src/builders/doughnut.ts +66 -0
  211. package/src/builders/heatmap.ts +60 -0
  212. package/src/builders/line.ts +39 -0
  213. package/src/builders/pie.ts +66 -0
  214. package/src/builders/radar.ts +65 -0
  215. package/src/builders/scatter.ts +34 -0
  216. package/src/catalogue-model.ts +235 -0
  217. package/src/catalogue-runtime.ts +244 -0
  218. package/src/catalogue-types.ts +163 -0
  219. package/src/chart-state.ts +35 -0
  220. package/src/charts.css +135 -0
  221. package/src/colors.ts +277 -0
  222. package/src/community.ts +11 -0
  223. package/src/doughnut.ts +12 -0
  224. package/src/export.ts +50 -0
  225. package/src/heatmap.ts +11 -0
  226. package/src/index.ts +11 -0
  227. package/src/legend-export.ts +36 -0
  228. package/src/legend-model.ts +76 -0
  229. package/src/legend.css +64 -0
  230. package/src/legend.ts +90 -0
  231. package/src/limits.ts +7 -0
  232. package/src/line.ts +13 -0
  233. package/src/loading-state.ts +39 -0
  234. package/src/map-assets.ts +1 -0
  235. package/src/map-resources.ts +46 -0
  236. package/src/messages.ts +5 -0
  237. package/src/model.ts +32 -0
  238. package/src/motion/bar.ts +15 -0
  239. package/src/motion/doughnut.ts +14 -0
  240. package/src/motion/heatmap.ts +14 -0
  241. package/src/motion/line.ts +15 -0
  242. package/src/motion/pie.ts +14 -0
  243. package/src/motion/radar.ts +15 -0
  244. package/src/motion/scatter.ts +15 -0
  245. package/src/motion/sparkline.ts +10 -0
  246. package/src/motion-collection.ts +20 -0
  247. package/src/motion-community.ts +22 -0
  248. package/src/motion-policy.ts +36 -0
  249. package/src/options.ts +24 -0
  250. package/src/pie.ts +12 -0
  251. package/src/radar.ts +12 -0
  252. package/src/runtime.ts +7 -0
  253. package/src/scatter.ts +13 -0
  254. package/src/sparkline-runtime/runtime.d.ts +10 -0
  255. package/src/sparkline-runtime/runtime.js +263 -0
  256. package/src/sparkline-validation.ts +19 -0
  257. package/src/sparkline.css +25 -0
  258. package/src/sparkline.ts +68 -0
  259. package/src/state-announcement.ts +17 -0
  260. package/src/types.ts +104 -0
  261. package/src/validation.ts +75 -0
  262. package/src/web-motion-sparkline.ts +37 -0
@@ -0,0 +1,4 @@
1
+ /** Local opt-in adapter. The compact SVG runtime and its ordinary defaults stay intact. */
2
+ import { type SparklineOptions, type SparklineEvents } from './sparkline.ts';
3
+ import type { ChartPresentation } from './catalogue-runtime.ts';
4
+ export declare function createMotionSparkline(host: HTMLElement, options: SparklineOptions, events: SparklineEvents, policy: ChartPresentation): import("./sparkline.ts").SparklineInstance;
package/edition.json ADDED
@@ -0,0 +1,48 @@
1
+ {
2
+ "edition": "community",
3
+ "version": "1.2.0",
4
+ "platform": "web",
5
+ "charts": [
6
+ {
7
+ "slug": "sparkline",
8
+ "edition": "community",
9
+ "factory": "createSparkline"
10
+ },
11
+ {
12
+ "slug": "line",
13
+ "edition": "community",
14
+ "factory": "createLineChart"
15
+ },
16
+ {
17
+ "slug": "bar",
18
+ "edition": "community",
19
+ "factory": "createBarChart"
20
+ },
21
+ {
22
+ "slug": "pie",
23
+ "edition": "community",
24
+ "factory": "createPieChart"
25
+ },
26
+ {
27
+ "slug": "doughnut",
28
+ "edition": "community",
29
+ "factory": "createDoughnutChart"
30
+ },
31
+ {
32
+ "slug": "scatter",
33
+ "edition": "community",
34
+ "factory": "createScatterChart"
35
+ },
36
+ {
37
+ "slug": "radar",
38
+ "edition": "community",
39
+ "factory": "createRadarChart"
40
+ },
41
+ {
42
+ "slug": "heatmap",
43
+ "edition": "community",
44
+ "factory": "createHeatmap"
45
+ }
46
+ ],
47
+ "licensing": "BluKavantUI Community license."
48
+ }
package/package.json CHANGED
@@ -1,6 +1,105 @@
1
1
  {
2
2
  "name": "@blukavantui/charts-community",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "1.2.0",
4
+ "description": "BluKavantUI Charts Community: 8 modular web charts. A separate chart bundle.",
5
+ "type": "module",
6
+ "license": "SEE LICENSE IN LICENSE.txt",
7
+ "publishConfig": {
8
+ "access": "public"
9
+ },
10
+ "main": "./dist/index.js",
11
+ "types": "./dist/index.d.ts",
12
+ "exports": {
13
+ "./motion": {
14
+ "types": "./dist/motion.d.ts",
15
+ "import": "./dist/motion.js"
16
+ },
17
+ "./motion/sparkline": {
18
+ "types": "./dist/motion/sparkline.d.ts",
19
+ "import": "./dist/motion/sparkline.js"
20
+ },
21
+ "./motion/line": {
22
+ "types": "./dist/motion/line.d.ts",
23
+ "import": "./dist/motion/line.js"
24
+ },
25
+ "./motion/bar": {
26
+ "types": "./dist/motion/bar.d.ts",
27
+ "import": "./dist/motion/bar.js"
28
+ },
29
+ "./motion/pie": {
30
+ "types": "./dist/motion/pie.d.ts",
31
+ "import": "./dist/motion/pie.js"
32
+ },
33
+ "./motion/doughnut": {
34
+ "types": "./dist/motion/doughnut.d.ts",
35
+ "import": "./dist/motion/doughnut.js"
36
+ },
37
+ "./motion/scatter": {
38
+ "types": "./dist/motion/scatter.d.ts",
39
+ "import": "./dist/motion/scatter.js"
40
+ },
41
+ "./motion/radar": {
42
+ "types": "./dist/motion/radar.d.ts",
43
+ "import": "./dist/motion/radar.js"
44
+ },
45
+ "./motion/heatmap": {
46
+ "types": "./dist/motion/heatmap.d.ts",
47
+ "import": "./dist/motion/heatmap.js"
48
+ },
49
+ ".": {
50
+ "types": "./dist/index.d.ts",
51
+ "import": "./dist/index.js"
52
+ },
53
+ "./sparkline": {
54
+ "types": "./dist/sparkline.d.ts",
55
+ "import": "./dist/sparkline.js"
56
+ },
57
+ "./line": {
58
+ "types": "./dist/line.d.ts",
59
+ "import": "./dist/line.js"
60
+ },
61
+ "./bar": {
62
+ "types": "./dist/bar.d.ts",
63
+ "import": "./dist/bar.js"
64
+ },
65
+ "./pie": {
66
+ "types": "./dist/pie.d.ts",
67
+ "import": "./dist/pie.js"
68
+ },
69
+ "./doughnut": {
70
+ "types": "./dist/doughnut.d.ts",
71
+ "import": "./dist/doughnut.js"
72
+ },
73
+ "./scatter": {
74
+ "types": "./dist/scatter.d.ts",
75
+ "import": "./dist/scatter.js"
76
+ },
77
+ "./radar": {
78
+ "types": "./dist/radar.d.ts",
79
+ "import": "./dist/radar.js"
80
+ },
81
+ "./heatmap": {
82
+ "types": "./dist/heatmap.d.ts",
83
+ "import": "./dist/heatmap.js"
84
+ },
85
+ "./styles.css": "./dist/styles.css",
86
+ "./theme.css": "./dist/theme.css",
87
+ "./browser/charts-community.js": "./dist/browser/charts-community.js",
88
+ "./sparkline.css": "./dist/sparkline.css"
89
+ },
90
+ "files": [
91
+ "dist",
92
+ "src",
93
+ "README.md",
94
+ "LICENSE.txt",
95
+ "THIRD_PARTY_NOTICES.txt",
96
+ "edition.json"
97
+ ],
98
+ "sideEffects": [
99
+ "**/*.css"
100
+ ],
101
+ "dependencies": {
102
+ "@blukavantui/community": "^1.2.0",
103
+ "echarts": "6.1.0"
104
+ }
105
+ }
@@ -0,0 +1,81 @@
1
+ import { contrastText, interpolateColor } from './colors.ts';
2
+
3
+ // Rendering policy only: input colors and stable data keys are never rewritten.
4
+ // Use a margin above 3:1 so channel rounding cannot turn a pass into a failure.
5
+ export const MARK_CONTRAST = 3.2;
6
+ function rgba(value: string): number[] {
7
+ if (value.startsWith('#')) {
8
+ let h = value.slice(1);
9
+ if (h.length <= 4) h = h.split('').map(c => c + c).join('');
10
+ return [0, 2, 4].map(i => parseInt(h.slice(i, i + 2), 16)).concat(h.length === 8 ? parseInt(h.slice(6), 16) / 255 : 1);
11
+ }
12
+ const channels = value.match(/[\d.]+/g)?.map(Number);
13
+ return channels && channels.length >= 3 ? [...channels.slice(0, 3), channels[3] ?? 1] : [24, 34, 48, 1];
14
+ }
15
+ function rgb(channels: number[]) { return `rgb(${channels.slice(0, 3).map(v => Math.round(v)).join(',')})`; }
16
+ export function opaqueColor(value: string, surface: string): string {
17
+ const c = rgba(value), bg = rgba(surface);
18
+ return rgb(c.slice(0, 3).map((v, i) => v * c[3] + bg[i] * (1 - c[3])));
19
+ }
20
+ function luminance(value: string): number {
21
+ const c = rgba(value).slice(0, 3).map(v => { const n = v / 255; return n <= .04045 ? n / 12.92 : ((n + .055) / 1.055) ** 2.4; });
22
+ return c[0] * .2126 + c[1] * .7152 + c[2] * .0722;
23
+ }
24
+ export function contrastRatio(a: string, b: string): number {
25
+ const x = luminance(a), y = luminance(b);
26
+ return (Math.max(x, y) + .05) / (Math.min(x, y) + .05);
27
+ }
28
+ export function markColor(value: string, surface: string, minimum = MARK_CONTRAST): string {
29
+ const original = opaqueColor(value, surface);
30
+ if (contrastRatio(original, surface) >= minimum) return original;
31
+ const end = contrastRatio('#000', surface) >= contrastRatio('#fff', surface) ? '#000' : '#fff';
32
+ const target = Math.min(minimum, contrastRatio(end, surface));
33
+ let lo = 0, hi = 1;
34
+ for (let i = 0; i < 20; i++) {
35
+ const mid = (lo + hi) / 2;
36
+ if (contrastRatio(interpolateColor([original, end], mid), surface) >= target) hi = mid;
37
+ else lo = mid;
38
+ }
39
+ return interpolateColor([original, end], hi);
40
+ }
41
+ /** A single-hue luminance ramp also works when hues cannot be distinguished. */
42
+ export function sequentialScale(value: string, surface: string): string[] {
43
+ const end = markColor(value, surface, 9);
44
+ return [markColor(interpolateColor([surface, end], .12), surface), end];
45
+ }
46
+ export const seriesSymbol = (index: number) => ['circle', 'diamond', 'triangle', 'rect', 'roundRect', 'pin', 'arrow', 'emptyCircle', 'emptyDiamond', 'emptyTriangle', 'emptyRect', 'emptyRoundRect'][index % 12];
47
+ export const seriesDash = (index: number) => (['solid', 'dashed', 'dotted'] as const)[(index + Math.floor(index / 12)) % 3];
48
+ export function seriesDecal(index: number, fill: string) {
49
+ const variants = [
50
+ { symbol: 'rect', symbolSize: 0, dashArrayX: [1, 0], dashArrayY: [1, 8], rotation: 0 },
51
+ { symbol: 'rect', symbolSize: 1, dashArrayX: [1, 0], dashArrayY: [1, 8], rotation: Math.PI / 4 },
52
+ { symbol: 'circle', symbolSize: 1, dashArrayX: [2, 7], dashArrayY: [2, 7], rotation: 0 },
53
+ { symbol: 'rect', symbolSize: 1, dashArrayX: [1, 0], dashArrayY: [1, 8], rotation: -Math.PI / 4 },
54
+ { symbol: 'rect', symbolSize: 1, dashArrayX: [1, 0], dashArrayY: [1, 6], rotation: 0 },
55
+ { symbol: 'rect', symbolSize: 1, dashArrayX: [3, 5], dashArrayY: [3, 5], rotation: Math.PI / 4 },
56
+ { symbol: 'triangle', symbolSize: 1, dashArrayX: [4, 6], dashArrayY: [4, 6], rotation: 0 },
57
+ { symbol: 'diamond', symbolSize: 1, dashArrayX: [4, 6], dashArrayY: [4, 6], rotation: 0 },
58
+ { symbol: 'rect', symbolSize: 1, dashArrayX: [1, 0], dashArrayY: [1, 3, 1, 7], rotation: Math.PI / 4 },
59
+ { symbol: 'circle', symbolSize: 1, dashArrayX: [2, 3], dashArrayY: [2, 3], rotation: 0 },
60
+ { symbol: 'rect', symbolSize: 1, dashArrayX: [1, 0], dashArrayY: [3, 4], rotation: -Math.PI / 4 },
61
+ { symbol: 'rect', symbolSize: 1, dashArrayX: [4, 4], dashArrayY: [1, 5], rotation: 0 },
62
+ ];
63
+ return { ...variants[index % variants.length], color: contrastText(fill), backgroundColor: 'none' };
64
+ }
65
+ /** Selection and pointer emphasis share a quiet, solid contrast cue. */
66
+ export function highlightStyle(fill: string) {
67
+ return { color: fill, borderColor: contrastText(fill), borderWidth: 2, borderType: 'solid' as const };
68
+ }
69
+ export function markStyle(fill: string, surface: string, selected = false, pattern?: number) {
70
+ return { color: fill, opacity: 1, borderColor: surface, borderWidth: 1.5, borderType: 'solid' as const, ...(selected ? highlightStyle(fill) : {}),
71
+ ...(pattern === undefined ? {} : { decal: seriesDecal(pattern, fill) }) };
72
+ }
73
+
74
+ /** Keep callouts inside the plot and out of its key; the HTML table retains full text. */
75
+ export function boundedLabels(width: number, height: number, top = 8) {
76
+ return ({ labelRect: r }: { labelRect: { x: number; y: number; width: number; height: number } }) => ({
77
+ hideOverlap: true,
78
+ dx: Math.max(0, 8 - r.x) + Math.min(0, width - 8 - r.x - r.width),
79
+ dy: Math.max(0, top - r.y) + Math.min(0, height - 8 - r.y - r.height),
80
+ });
81
+ }
@@ -0,0 +1 @@
1
+ declare module '*.css';
package/src/async.ts ADDED
@@ -0,0 +1,12 @@
1
+ /** Settle even if the underlying browser API ignores cancellation. */
2
+ export async function boundedAwait<T>(work:Promise<T>,signal:AbortSignal|undefined,milliseconds:number,timeoutMessage:string):Promise<T> {
3
+ if(signal?.aborted){void work.catch(()=>{});signal.throwIfAborted();}
4
+ let timer:ReturnType<typeof setTimeout>|undefined,abort:()=>void=()=>{};
5
+ try {
6
+ return await Promise.race([work,new Promise<never>((_,reject)=>{
7
+ abort=()=>reject(signal?.reason??new DOMException('Operation aborted.','AbortError'));
8
+ signal?.addEventListener('abort',abort,{once:true});
9
+ timer=setTimeout(()=>reject(new Error(timeoutMessage)),milliseconds);
10
+ })]);
11
+ } finally {clearTimeout(timer);signal?.removeEventListener('abort',abort);}
12
+ }
package/src/bar.ts ADDED
@@ -0,0 +1,13 @@
1
+ import { buildOption } from './builders/bar.ts';
2
+ import { use } from 'echarts/core';
3
+ import { BarChart } from 'echarts/charts';
4
+ import { GridComponent } from 'echarts/components';
5
+ import { createCatalogueChart } from './catalogue-runtime.ts';
6
+ import type { OptionsByKind, ChartPoint } from './catalogue-types.ts';
7
+ import type { ChartEvents } from './types.ts';
8
+ export type { OptionsByKind, ChartPoint } from './catalogue-types.ts';
9
+ export type BarChartOptions = OptionsByKind['bar'];
10
+ export function createBarChart(host: HTMLElement, options: BarChartOptions, events: ChartEvents<ChartPoint> = {}) {
11
+ use([BarChart, GridComponent]);
12
+ return createCatalogueChart(host, 'bar', options, events, buildOption);
13
+ }
@@ -0,0 +1,39 @@
1
+ import { format, type EChartsCoreOption } from 'echarts/core';
2
+ import type { CatalogueKind, CatalogueOptions, CartesianOptions, RadarOptions, CircularOptions, ScatterOptions, TreemapOptions, SankeyOptions, FunnelOptions, GeoMapOptions, CalendarHeatmapOptions, ChartPoint, TreeNode } from '../catalogue-types.ts';
3
+ import { cataloguePoints, treeValue } from '../catalogue-model.ts';
4
+ import { resolveChartColor, contrastText, interpolateColor } from '../colors.ts';
5
+ import { formatValue } from '../model.ts';
6
+ import { escapeText } from '../validation.ts';
7
+ import { markColor, markStyle, highlightStyle, sequentialScale, seriesSymbol, seriesDash, seriesDecal, boundedLabels } from '../accessibility.ts';
8
+ import type { themeTokens } from '../options.ts';
9
+ type Tokens = ReturnType<typeof themeTokens>;
10
+ export function buildOption(options: CatalogueOptions, t: Tokens, mapName?: string, reducedMotion = false, width = 640): EChartsCoreOption {
11
+ const kind = 'bar' as CatalogueKind;
12
+ const o = options as any, points = cataloguePoints(kind, options);
13
+ const decal = (index: number, fill: string) => o.ShowPatterns ? seriesDecal(index, fill) : undefined;
14
+ const colors = t.palette;
15
+ const color = (c: any, i: number) => markColor(t.forcedColors ? t.text : resolveChartColor(c, colors[i % colors.length], t.dark, t.read), t.surface, kind === 'line' || kind === 'radar' || kind === 'polar' && o.PolarType === 'Line' ? 4.5 : 3.2);
16
+ const style = (key: string, c: any, i: number) => markStyle(color(c, i), t.surface, o.SelectedKey === key);
17
+ const point = (key: string, seriesKey?: string) => points.find(p => p.Key === key && (!seriesKey || p.SeriesKey === seriesKey));
18
+ const namesForLegend: string[] = ['line','radar','bar','polar'].includes(kind) ? o.Series.map((s: any) => s.Label) : kind === 'scatter' ? [...new Set<string>(o.Points.map((p: any) => p.Group || 'Observations'))] : ['pie','doughnut','nightingale','funnel'].includes(kind) ? [...new Set<string>(o.Items.map((i: any) => i.Label))] : [];
19
+ const legend = { data: namesForLegend, show: o.ShowLegend, type: 'scroll', top: 6, left: 10, right: 10, itemWidth: 24, itemHeight: 12, selectedMode: false, pageTextStyle: { color: t.muted }, pageIconColor: t.text, textStyle: { color: t.muted, fontSize: 11 } };
20
+ const label = { show: o.ShowLabels, color: contrastText(t.surface), fontSize: 11, backgroundColor: t.surface, padding: [2, 3], borderRadius: 2 };
21
+ const tooltip = { show: o.ShowTooltip, confine: true, enterable: true, backgroundColor: t.surface, borderColor: t.border, textStyle: { color: t.text, fontSize: 12 }, formatter: (p: any) => {
22
+ const d = p.data?.point as ChartPoint | undefined;
23
+ if (!d)
24
+ return `${escapeText(p.name || '')}${p.value == null || Number.isNaN(p.value) ? ' · No data' : ''}`;
25
+ const extra = d.Date ? `<br/>${escapeText(d.Date)}<br/><strong>${escapeText(formatValue(d.Value, o))}</strong>` : d.X !== undefined ? `<br/>${escapeText(o.XLabel || 'X')}: ${d.X}<br/>${escapeText(o.YLabel || 'Y')}: ${d.Y}${d.Size === undefined ? '' : `<br/>Size: ${d.Size}`}` : `<br/><strong>${escapeText(formatValue(d.Value, o))}</strong>`;
26
+ return `${escapeText(d.SeriesName ? `${d.SeriesName} · ${d.Label}` : d.Label)}${extra}${p.data?.conversion ? `<br/>${escapeText(p.data.conversion)}` : ''}`;
27
+ } };
28
+ const base = { backgroundColor: t.surface, color: colors.map((c, i) => color(c, i)), animation: !!o.EnableAnimation && !reducedMotion, animationDuration: 300, animationDurationUpdate: 150, textStyle: { fontFamily: 'Inter,system-ui,sans-serif', color: t.text }, aria: { enabled: true, description: `${o.Title}. ${o.Description || ''} Exact values and keyboard selection are in the data table.` }, tooltip };
29
+ const axis = { axisLine: { show: false }, axisTick: { show: false }, axisLabel: { color: t.muted, fontSize: 10, hideOverlap: true }, splitLine: { lineStyle: { color: t.border, type: 'dashed' } } };
30
+
31
+ const c = options as CartesianOptions;
32
+ const percent = c.Stacking === 'Percent';
33
+ const cat = { ...axis, type: 'category', data: c.Categories.map(x => x.Label), axisLabel: { ...axis.axisLabel, width: Math.min(kind === 'polar' ? 100 : 140, width * .2), overflow: 'truncate' }, splitLine: { show: false } };
34
+ const numeric = { ...axis, type: 'value', max: percent ? 1 : undefined, axisLabel: { ...axis.axisLabel, formatter: (v: number) => percent ? `${Math.round(v * 100)}%` : formatValue(v, o, true) } };
35
+ const insideBars = (kind === 'bar' || (kind === 'polar' && c.PolarType !== 'Line')) && (kind === 'polar' || (!!c.Stacking && c.Stacking !== 'None'));
36
+ const series = c.Series.map((s, si) => ({ id: s.Key, name: s.Label, type: kind === 'line' || (kind === 'polar' && c.PolarType === 'Line') ? 'line' : 'bar', coordinateSystem: kind === 'polar' ? 'polar' : 'cartesian2d', stack: c.Stacking && c.Stacking !== 'None' ? 'total' : undefined, smooth: c.LineStyle === 'Smooth', step: c.LineStyle === 'Step' ? 'middle' : false, connectNulls: false, areaStyle: c.ShowArea ? { opacity: .10 } : undefined, barMaxWidth: 38, symbol: seriesSymbol(si), symbolSize: 8, itemStyle: { ...markStyle(color(s.Color, si), t.surface), decal: decal(si, color(s.Color, si)) }, lineStyle: { width: 2.5, color: color(s.Color, si), type: seriesDash(si) }, label: { ...label, color: insideBars ? contrastText(color(s.Color, si)) : contrastText(t.surface), backgroundColor: insideBars ? color(s.Color, si) : t.surface, position: insideBars ? 'inside' : kind === 'bar' && c.Orientation === 'Horizontal' ? 'right' : 'top', formatter: (p: any) => percent ? `${Math.round(Number(p.value) * 100)}%` : formatValue(p.data.point.Value, o, true) }, labelLayout: { hideOverlap: true }, emphasis: { focus: 'none', scale: false }, blur: { itemStyle: { opacity: 1 }, lineStyle: { opacity: 1 }, label: { opacity: 1 } }, data: s.Values.map((v, i) => ({ value: v === null ? null : percent ? v / (c.Series.reduce((sum, s) => sum + (s.Values[i] || 0), 0) || 1) : v, point: point(c.Categories[i].Key, s.Key), itemStyle: { ...style(c.Categories[i].Key, s.Color, si), decal: decal(si, color(s.Color, si)) }, emphasis: { itemStyle: highlightStyle(color(s.Color, si)) } })) }));
37
+ return kind === 'polar' ? { ...base, legend, polar: { radius: ['12%', '72%'], center: ['50%', '55%'] }, angleAxis: { ...cat, startAngle: 90 }, radiusAxis: numeric, series } : { ...base, legend, grid: { left: 14, right: 22, top: o.ShowLegend ? 48 : 20, bottom: 16, containLabel: true }, xAxis: c.Orientation === 'Horizontal' ? numeric : cat, yAxis: c.Orientation === 'Horizontal' ? { ...cat, inverse: true } : numeric, series };
38
+
39
+ }
@@ -0,0 +1,66 @@
1
+ import { format, type EChartsCoreOption } from 'echarts/core';
2
+ import type { CatalogueKind, CatalogueOptions, CartesianOptions, RadarOptions, CircularOptions, ScatterOptions, TreemapOptions, SankeyOptions, FunnelOptions, GeoMapOptions, CalendarHeatmapOptions, ChartPoint, TreeNode } from '../catalogue-types.ts';
3
+ import { cataloguePoints, treeValue } from '../catalogue-model.ts';
4
+ import { resolveChartColor, contrastText, interpolateColor } from '../colors.ts';
5
+ import { formatValue } from '../model.ts';
6
+ import { escapeText } from '../validation.ts';
7
+ import { markColor, markStyle, highlightStyle, sequentialScale, seriesSymbol, seriesDash, seriesDecal, boundedLabels } from '../accessibility.ts';
8
+ import type { themeTokens } from '../options.ts';
9
+ type Tokens = ReturnType<typeof themeTokens>;
10
+ export function buildOption(options: CatalogueOptions, t: Tokens, mapName?: string, reducedMotion = false, width = 640): EChartsCoreOption {
11
+ const kind = 'doughnut' as CatalogueKind;
12
+ const o = options as any, points = cataloguePoints(kind, options);
13
+ const decal = (index: number, fill: string) => o.ShowPatterns ? seriesDecal(index, fill) : undefined;
14
+ const colors = t.palette;
15
+ const color = (c: any, i: number) => markColor(t.forcedColors ? t.text : resolveChartColor(c, colors[i % colors.length], t.dark, t.read), t.surface, kind === 'line' || kind === 'radar' || kind === 'polar' && o.PolarType === 'Line' ? 4.5 : 3.2);
16
+ const style = (key: string, c: any, i: number) => markStyle(color(c, i), t.surface, o.SelectedKey === key);
17
+ const point = (key: string, seriesKey?: string) => points.find(p => p.Key === key && (!seriesKey || p.SeriesKey === seriesKey));
18
+ const namesForLegend: string[] = ['line','radar','bar','polar'].includes(kind) ? o.Series.map((s: any) => s.Label) : kind === 'scatter' ? [...new Set<string>(o.Points.map((p: any) => p.Group || 'Observations'))] : ['pie','doughnut','nightingale','funnel'].includes(kind) ? [...new Set<string>(o.Items.map((i: any) => i.Label))] : [];
19
+ const legend = { data: namesForLegend, show: o.ShowLegend, type: 'scroll', top: 6, left: 10, right: 10, itemWidth: 24, itemHeight: 12, selectedMode: false, pageTextStyle: { color: t.muted }, pageIconColor: t.text, textStyle: { color: t.muted, fontSize: 11 } };
20
+ const label = { show: o.ShowLabels, color: contrastText(t.surface), fontSize: 11, backgroundColor: t.surface, padding: [2, 3], borderRadius: 2 };
21
+ const tooltip = { show: o.ShowTooltip, confine: true, enterable: true, backgroundColor: t.surface, borderColor: t.border, textStyle: { color: t.text, fontSize: 12 }, formatter: (p: any) => {
22
+ const d = p.data?.point as ChartPoint | undefined;
23
+ if (!d)
24
+ return `${escapeText(p.name || '')}${p.value == null || Number.isNaN(p.value) ? ' · No data' : ''}`;
25
+ const extra = d.Date ? `<br/>${escapeText(d.Date)}<br/><strong>${escapeText(formatValue(d.Value, o))}</strong>` : d.X !== undefined ? `<br/>${escapeText(o.XLabel || 'X')}: ${d.X}<br/>${escapeText(o.YLabel || 'Y')}: ${d.Y}${d.Size === undefined ? '' : `<br/>Size: ${d.Size}`}` : `<br/><strong>${escapeText(formatValue(d.Value, o))}</strong>`;
26
+ return `${escapeText(d.SeriesName ? `${d.SeriesName} · ${d.Label}` : d.Label)}${extra}${p.data?.conversion ? `<br/>${escapeText(p.data.conversion)}` : ''}`;
27
+ } };
28
+ const base = { backgroundColor: t.surface, color: colors.map((c, i) => color(c, i)), animation: !!o.EnableAnimation && !reducedMotion, animationDuration: 300, animationDurationUpdate: 150, textStyle: { fontFamily: 'Inter,system-ui,sans-serif', color: t.text }, aria: { enabled: true, description: `${o.Title}. ${o.Description || ''} Exact values and keyboard selection are in the data table.` }, tooltip };
29
+ const axis = { axisLine: { show: false }, axisTick: { show: false }, axisLabel: { color: t.muted, fontSize: 10, hideOverlap: true }, splitLine: { lineStyle: { color: t.border, type: 'dashed' } } };
30
+
31
+ const c = options as CircularOptions;
32
+ const rings = c.Nested ? c.Rings! : [{ Key: '', Label: '' }];
33
+ const half = c.Shape === 'Half', nested = !!c.Nested && rings.length > 1;
34
+ const inner = kind === 'pie' ? 0 : c.InnerRadius ?? (kind === 'doughnut' ? 48 : 18);
35
+ const outside = c.LabelPosition !== 'Inside';
36
+ const outer = outside ? 66 : 76;
37
+ const names = [...new Set(c.Items.map(item => item.Label))];
38
+ const series = rings.map((ring, ri) => {
39
+ // Match ECharts' nested-pie composition: a solid core, a visible gap, then the detail ring.
40
+ const radius = nested ? ri === 0 ? [0, 32] : [44 + (outer - 44) * (ri - 1) / (rings.length - 1), 44 + (outer - 44) * ri / (rings.length - 1) - (ri === rings.length - 1 ? 0 : 2)] : [inner, Math.max(outer, inner + 5)];
41
+ const inside = !outside || (nested && ri < rings.length - 1);
42
+ return {
43
+ type: 'pie', id: ring.Key || kind, name: ring.Label, top: o.ShowLegend ? 28 : 0, bottom: 2,
44
+ center: ['50%', half ? '72%' : '55%'], radius: radius.map(r => `${r}%`),
45
+ startAngle: half ? 180 : 90, endAngle: half ? 360 : 'auto',
46
+ roseType: kind === 'nightingale' ? (c.RoseMode === 'Radius' ? 'radius' : 'area') : false,
47
+ avoidLabelOverlap: true, stillShowZeroSum: false,
48
+ label: { ...label, position: inside ? 'inside' : 'outside', backgroundColor: inside ? undefined : t.surface,
49
+ alignTo: 'none', bleedMargin: 6, distanceToLabelLine: 4,
50
+ formatter: '{b}\n{d}%', lineHeight: 15 },
51
+ labelLine: { show: !!o.ShowLabels && !inside, length: 12, length2: 8, smooth: .15 },
52
+ labelLayout: { hideOverlap: true }, emphasis: { scale: true },
53
+ data: c.Items.filter(i => !c.Nested || i.RingKey === ring.Key).map(i => {
54
+ const index = names.indexOf(i.Label), fill = color(i.Color, index);
55
+ return { id: i.Key, name: i.Label, value: i.Value, point: point(i.Key),
56
+ label: { color: contrastText(inside ? fill : t.surface), backgroundColor: inside ? fill : t.surface },
57
+ labelLine: { lineStyle: { color: fill, width: 1.25 } },
58
+ itemStyle: { ...style(i.Key, i.Color, index), decal: decal(index, fill), borderRadius: 3 }, emphasis: { itemStyle: highlightStyle(fill) } };
59
+ })
60
+ };
61
+ });
62
+ const selected = points.find(p => p.Key === o.SelectedKey);
63
+ const total = selected?.Value ?? c.Items.reduce((n, i) => n + (i.Value || 0), 0);
64
+ return { ...base, legend, series, media: outside ? [{ query: { maxWidth: 420 }, option: { series: series.map(s => ({ id: s.id, radius: s.radius.map(r => `${parseFloat(r) * .84}%`) })) } }] : [], graphic: kind === 'doughnut' && !nested && inner >= 35 ? [{ type: 'text', left: 'center', top: half ? '63%' : '48%', silent: true, style: { text: `${formatValue(total, o, true)}\n${selected?.Label || c.CenterLabel || 'Total'}`, textAlign: 'center', fill: t.text, font: '600 16px Inter,system-ui', lineHeight: 24, width: Math.max(24, Math.min(width, o.Height || 320) * inner / 100 - 12), overflow: 'truncate' } }] : [] };
65
+
66
+ }
@@ -0,0 +1,60 @@
1
+ import type { EChartsCoreOption } from 'echarts/core';
2
+ import type { MixedChartOptions, HeatmapOptions, ChartOptions } from '../types.ts';
3
+ import { formatValue, heatmapRange, heatmapPoint } from '../model.ts';
4
+ import { automaticPalette, componentPalette, isComponentColor, resolveChartColor, contrastText, interpolateColor } from '../colors.ts';
5
+ import { markColor, markStyle, highlightStyle, sequentialScale, seriesSymbol, seriesDash, seriesDecal } from '../accessibility.ts';
6
+ import { escapeText } from '../validation.ts';
7
+
8
+ export interface ChartTheme { surface: string; text: string; muted: string; border: string; dark: boolean; forcedColors?: boolean; read: (name: string) => string; palette: string[]; }
9
+ export function themeTokens(host: HTMLElement): ChartTheme {
10
+ const style = getComputedStyle(host);
11
+ const read = (name: string, fallback: string) => style.getPropertyValue(name).trim() || fallback;
12
+ const dark = document.documentElement.dataset.bluuiTheme === 'dark';
13
+ const forcedColors = matchMedia('(forced-colors: active)').matches;
14
+ if (forcedColors) {
15
+ const probe = document.createElement('span');
16
+ probe.style.cssText = 'position:absolute;visibility:hidden;color:Canvas;background-color:CanvasText;forced-color-adjust:none';
17
+ host.append(probe);
18
+ const computed = getComputedStyle(probe), surface = computed.color, text = computed.backgroundColor;
19
+ probe.remove();
20
+ return { surface, text, muted: text, border: text, dark, forcedColors, read: () => '', palette: Array(8).fill(text) };
21
+ }
22
+ return { forcedColors, surface: read('--bluui-surface', '#fff'), text: read('--bluui-text', '#182230'), muted: read('--bluui-text-muted', '#667085'), border: read('--bluui-border', '#e3e5ed'), dark, read: (name: string) => style.getPropertyValue(name),
23
+ palette: automaticPalette(dark) };
24
+ }
25
+ type Tokens = ReturnType<typeof themeTokens>;
26
+ function base(o: ChartOptions, t: Tokens): EChartsCoreOption {
27
+ return {
28
+ backgroundColor: t.surface,
29
+ animation: o.EnableAnimation && !matchMedia('(prefers-reduced-motion: reduce)').matches,
30
+ animationDuration: 350, animationDurationUpdate: 180,
31
+ textStyle: { fontFamily: 'Inter, system-ui, sans-serif', color: t.text },
32
+ aria: { enabled: true, description: `${o.Title}. ${o.Description || ''} Exact values and keyboard selections are available in the chart data table.` },
33
+ tooltip: { show: o.ShowTooltip, confine: true, enterable: true, backgroundColor: t.surface, borderColor: t.border, textStyle: { color: t.text, fontSize: 12 }, extraCssText: 'border-radius:8px;box-shadow:0 8px 28px #0002;' },
34
+ };
35
+ }
36
+ function axis(t: Tokens) { return { axisLine: { show: false }, axisTick: { show: false }, axisLabel: { color: t.muted, fontSize: 11 } }; }
37
+ export function heatmapOption(o: HeatmapOptions, t: Tokens): EChartsCoreOption {
38
+ const { min, max } = heatmapRange(o);
39
+ const palette = componentPalette(isComponentColor(o.ColorScale) ? o.ColorScale : 'Purple', t.dark, t.read);
40
+ const scaleColor = t.forcedColors ? t.text : o.ColorScale === 'Automatic' ? t.palette[0] : o.ColorScale === 'Teal' ? '#126e5b' : palette.solid;
41
+ // Diverging keeps a neutral midpoint; signed values remain available in the table.
42
+ const colors = o.ColorScale === 'Diverging' && !t.forcedColors ? [markColor('#ba5943', t.surface, 9), markColor(t.surface, t.surface), markColor('#6850bf', t.surface, 9)] : sequentialScale(scaleColor, t.surface);
43
+ return {
44
+ ...base(o, t),
45
+ grid: { left: 12, right: 12, top: 16, bottom: o.ShowLegend ? 62 : 12, containLabel: true },
46
+ tooltip: { ...(base(o, t).tooltip as object), formatter: (params: unknown) => { const point = heatmapPoint(o, (params as { dataIndex: number }).dataIndex)!; return `${escapeText(point.YLabel)} · ${escapeText(point.XLabel)}<br/><strong>${escapeText(formatValue(point.Value, o))}</strong>`; } },
47
+ xAxis: { ...axis(t), type: 'category', data: o.XCategories.map(c => c.Label), axisLabel: { ...axis(t).axisLabel, hideOverlap: true, width: 120, overflow: 'truncate' } },
48
+ yAxis: { ...axis(t), type: 'category', inverse: true, data: o.YCategories.map(c => c.Label), axisLabel: { ...axis(t).axisLabel, width: 80, overflow: 'truncate' } },
49
+ visualMap: { show: o.ShowLegend, min, max, calculable: false, orient: 'horizontal', left: 'center', bottom: 6, itemWidth: 9, itemHeight: 130, text: ['Higher', 'Lower'], textGap: 9, textStyle: { color: t.muted, fontSize: 10 }, inRange: { color: colors } },
50
+ series: [{ type: 'heatmap',
51
+ data: o.Cells.map(c => ({ name: c.Key, value: [o.XCategories.findIndex(x => x.Key === c.XKey), o.YCategories.findIndex(y => y.Key === c.YKey), c.Value ?? '-'],
52
+ itemStyle: { ...markStyle(interpolateColor(colors, ((c.Value ?? min) - min) / (max - min)), t.surface, o.SelectedKey === c.Key), borderRadius: 4 },
53
+ label: { color: contrastText(interpolateColor(colors, ((c.Value ?? min) - min) / (max - min))) },
54
+ emphasis: { itemStyle: highlightStyle(interpolateColor(colors, ((c.Value ?? min) - min) / (max - min))) },
55
+ })),
56
+ label: { show: o.ShowLabels, fontSize: 10, formatter: (p: { dataIndex: number }) => formatValue(o.Cells[p.dataIndex].Value, o, true) },
57
+ emphasis: { itemStyle: { borderColor: t.text, borderWidth: 2, borderType: 'solid' } },
58
+ }],
59
+ };
60
+ }
@@ -0,0 +1,39 @@
1
+ import { format, type EChartsCoreOption } from 'echarts/core';
2
+ import type { CatalogueKind, CatalogueOptions, CartesianOptions, RadarOptions, CircularOptions, ScatterOptions, TreemapOptions, SankeyOptions, FunnelOptions, GeoMapOptions, CalendarHeatmapOptions, ChartPoint, TreeNode } from '../catalogue-types.ts';
3
+ import { cataloguePoints, treeValue } from '../catalogue-model.ts';
4
+ import { resolveChartColor, contrastText, interpolateColor } from '../colors.ts';
5
+ import { formatValue } from '../model.ts';
6
+ import { escapeText } from '../validation.ts';
7
+ import { markColor, markStyle, highlightStyle, sequentialScale, seriesSymbol, seriesDash, seriesDecal, boundedLabels } from '../accessibility.ts';
8
+ import type { themeTokens } from '../options.ts';
9
+ type Tokens = ReturnType<typeof themeTokens>;
10
+ export function buildOption(options: CatalogueOptions, t: Tokens, mapName?: string, reducedMotion = false, width = 640): EChartsCoreOption {
11
+ const kind = 'line' as CatalogueKind;
12
+ const o = options as any, points = cataloguePoints(kind, options);
13
+ const decal = (index: number, fill: string) => o.ShowPatterns ? seriesDecal(index, fill) : undefined;
14
+ const colors = t.palette;
15
+ const color = (c: any, i: number) => markColor(t.forcedColors ? t.text : resolveChartColor(c, colors[i % colors.length], t.dark, t.read), t.surface, kind === 'line' || kind === 'radar' || kind === 'polar' && o.PolarType === 'Line' ? 4.5 : 3.2);
16
+ const style = (key: string, c: any, i: number) => markStyle(color(c, i), t.surface, o.SelectedKey === key);
17
+ const point = (key: string, seriesKey?: string) => points.find(p => p.Key === key && (!seriesKey || p.SeriesKey === seriesKey));
18
+ const namesForLegend: string[] = ['line','radar','bar','polar'].includes(kind) ? o.Series.map((s: any) => s.Label) : kind === 'scatter' ? [...new Set<string>(o.Points.map((p: any) => p.Group || 'Observations'))] : ['pie','doughnut','nightingale','funnel'].includes(kind) ? [...new Set<string>(o.Items.map((i: any) => i.Label))] : [];
19
+ const legend = { data: namesForLegend, show: o.ShowLegend, type: 'scroll', top: 6, left: 10, right: 10, itemWidth: 24, itemHeight: 12, selectedMode: false, pageTextStyle: { color: t.muted }, pageIconColor: t.text, textStyle: { color: t.muted, fontSize: 11 } };
20
+ const label = { show: o.ShowLabels, color: contrastText(t.surface), fontSize: 11, backgroundColor: t.surface, padding: [2, 3], borderRadius: 2 };
21
+ const tooltip = { show: o.ShowTooltip, confine: true, enterable: true, backgroundColor: t.surface, borderColor: t.border, textStyle: { color: t.text, fontSize: 12 }, formatter: (p: any) => {
22
+ const d = p.data?.point as ChartPoint | undefined;
23
+ if (!d)
24
+ return `${escapeText(p.name || '')}${p.value == null || Number.isNaN(p.value) ? ' · No data' : ''}`;
25
+ const extra = d.Date ? `<br/>${escapeText(d.Date)}<br/><strong>${escapeText(formatValue(d.Value, o))}</strong>` : d.X !== undefined ? `<br/>${escapeText(o.XLabel || 'X')}: ${d.X}<br/>${escapeText(o.YLabel || 'Y')}: ${d.Y}${d.Size === undefined ? '' : `<br/>Size: ${d.Size}`}` : `<br/><strong>${escapeText(formatValue(d.Value, o))}</strong>`;
26
+ return `${escapeText(d.SeriesName ? `${d.SeriesName} · ${d.Label}` : d.Label)}${extra}${p.data?.conversion ? `<br/>${escapeText(p.data.conversion)}` : ''}`;
27
+ } };
28
+ const base = { backgroundColor: t.surface, color: colors.map((c, i) => color(c, i)), animation: !!o.EnableAnimation && !reducedMotion, animationDuration: 300, animationDurationUpdate: 150, textStyle: { fontFamily: 'Inter,system-ui,sans-serif', color: t.text }, aria: { enabled: true, description: `${o.Title}. ${o.Description || ''} Exact values and keyboard selection are in the data table.` }, tooltip };
29
+ const axis = { axisLine: { show: false }, axisTick: { show: false }, axisLabel: { color: t.muted, fontSize: 10, hideOverlap: true }, splitLine: { lineStyle: { color: t.border, type: 'dashed' } } };
30
+
31
+ const c = options as CartesianOptions;
32
+ const percent = c.Stacking === 'Percent';
33
+ const cat = { ...axis, type: 'category', data: c.Categories.map(x => x.Label), axisLabel: { ...axis.axisLabel, width: Math.min(kind === 'polar' ? 100 : 140, width * .2), overflow: 'truncate' }, splitLine: { show: false } };
34
+ const numeric = { ...axis, type: 'value', max: percent ? 1 : undefined, axisLabel: { ...axis.axisLabel, formatter: (v: number) => percent ? `${Math.round(v * 100)}%` : formatValue(v, o, true) } };
35
+ const insideBars = (kind === 'bar' || (kind === 'polar' && c.PolarType !== 'Line')) && (kind === 'polar' || (!!c.Stacking && c.Stacking !== 'None'));
36
+ const series = c.Series.map((s, si) => ({ id: s.Key, name: s.Label, type: kind === 'line' || (kind === 'polar' && c.PolarType === 'Line') ? 'line' : 'bar', coordinateSystem: kind === 'polar' ? 'polar' : 'cartesian2d', stack: c.Stacking && c.Stacking !== 'None' ? 'total' : undefined, smooth: c.LineStyle === 'Smooth', step: c.LineStyle === 'Step' ? 'middle' : false, connectNulls: false, areaStyle: c.ShowArea ? { opacity: .10 } : undefined, barMaxWidth: 38, symbol: seriesSymbol(si), symbolSize: 8, itemStyle: { ...markStyle(color(s.Color, si), t.surface), decal: decal(si, color(s.Color, si)) }, lineStyle: { width: 2.5, color: color(s.Color, si), type: seriesDash(si) }, label: { ...label, color: insideBars ? contrastText(color(s.Color, si)) : contrastText(t.surface), backgroundColor: insideBars ? color(s.Color, si) : t.surface, position: insideBars ? 'inside' : kind === 'bar' && c.Orientation === 'Horizontal' ? 'right' : 'top', formatter: (p: any) => percent ? `${Math.round(Number(p.value) * 100)}%` : formatValue(p.data.point.Value, o, true) }, labelLayout: { hideOverlap: true }, emphasis: { focus: 'none', scale: false }, blur: { itemStyle: { opacity: 1 }, lineStyle: { opacity: 1 }, label: { opacity: 1 } }, data: s.Values.map((v, i) => ({ value: v === null ? null : percent ? v / (c.Series.reduce((sum, s) => sum + (s.Values[i] || 0), 0) || 1) : v, point: point(c.Categories[i].Key, s.Key), itemStyle: { ...style(c.Categories[i].Key, s.Color, si), decal: decal(si, color(s.Color, si)) }, emphasis: { itemStyle: highlightStyle(color(s.Color, si)) } })) }));
37
+ return kind === 'polar' ? { ...base, legend, polar: { radius: ['12%', '72%'], center: ['50%', '55%'] }, angleAxis: { ...cat, startAngle: 90 }, radiusAxis: numeric, series } : { ...base, legend, grid: { left: 14, right: 22, top: o.ShowLegend ? 48 : 20, bottom: 16, containLabel: true }, xAxis: c.Orientation === 'Horizontal' ? numeric : cat, yAxis: c.Orientation === 'Horizontal' ? { ...cat, inverse: true } : numeric, series };
38
+
39
+ }
@@ -0,0 +1,66 @@
1
+ import { format, type EChartsCoreOption } from 'echarts/core';
2
+ import type { CatalogueKind, CatalogueOptions, CartesianOptions, RadarOptions, CircularOptions, ScatterOptions, TreemapOptions, SankeyOptions, FunnelOptions, GeoMapOptions, CalendarHeatmapOptions, ChartPoint, TreeNode } from '../catalogue-types.ts';
3
+ import { cataloguePoints, treeValue } from '../catalogue-model.ts';
4
+ import { resolveChartColor, contrastText, interpolateColor } from '../colors.ts';
5
+ import { formatValue } from '../model.ts';
6
+ import { escapeText } from '../validation.ts';
7
+ import { markColor, markStyle, highlightStyle, sequentialScale, seriesSymbol, seriesDash, seriesDecal, boundedLabels } from '../accessibility.ts';
8
+ import type { themeTokens } from '../options.ts';
9
+ type Tokens = ReturnType<typeof themeTokens>;
10
+ export function buildOption(options: CatalogueOptions, t: Tokens, mapName?: string, reducedMotion = false, width = 640): EChartsCoreOption {
11
+ const kind = 'pie' as CatalogueKind;
12
+ const o = options as any, points = cataloguePoints(kind, options);
13
+ const decal = (index: number, fill: string) => o.ShowPatterns ? seriesDecal(index, fill) : undefined;
14
+ const colors = t.palette;
15
+ const color = (c: any, i: number) => markColor(t.forcedColors ? t.text : resolveChartColor(c, colors[i % colors.length], t.dark, t.read), t.surface, kind === 'line' || kind === 'radar' || kind === 'polar' && o.PolarType === 'Line' ? 4.5 : 3.2);
16
+ const style = (key: string, c: any, i: number) => markStyle(color(c, i), t.surface, o.SelectedKey === key);
17
+ const point = (key: string, seriesKey?: string) => points.find(p => p.Key === key && (!seriesKey || p.SeriesKey === seriesKey));
18
+ const namesForLegend: string[] = ['line','radar','bar','polar'].includes(kind) ? o.Series.map((s: any) => s.Label) : kind === 'scatter' ? [...new Set<string>(o.Points.map((p: any) => p.Group || 'Observations'))] : ['pie','doughnut','nightingale','funnel'].includes(kind) ? [...new Set<string>(o.Items.map((i: any) => i.Label))] : [];
19
+ const legend = { data: namesForLegend, show: o.ShowLegend, type: 'scroll', top: 6, left: 10, right: 10, itemWidth: 24, itemHeight: 12, selectedMode: false, pageTextStyle: { color: t.muted }, pageIconColor: t.text, textStyle: { color: t.muted, fontSize: 11 } };
20
+ const label = { show: o.ShowLabels, color: contrastText(t.surface), fontSize: 11, backgroundColor: t.surface, padding: [2, 3], borderRadius: 2 };
21
+ const tooltip = { show: o.ShowTooltip, confine: true, enterable: true, backgroundColor: t.surface, borderColor: t.border, textStyle: { color: t.text, fontSize: 12 }, formatter: (p: any) => {
22
+ const d = p.data?.point as ChartPoint | undefined;
23
+ if (!d)
24
+ return `${escapeText(p.name || '')}${p.value == null || Number.isNaN(p.value) ? ' · No data' : ''}`;
25
+ const extra = d.Date ? `<br/>${escapeText(d.Date)}<br/><strong>${escapeText(formatValue(d.Value, o))}</strong>` : d.X !== undefined ? `<br/>${escapeText(o.XLabel || 'X')}: ${d.X}<br/>${escapeText(o.YLabel || 'Y')}: ${d.Y}${d.Size === undefined ? '' : `<br/>Size: ${d.Size}`}` : `<br/><strong>${escapeText(formatValue(d.Value, o))}</strong>`;
26
+ return `${escapeText(d.SeriesName ? `${d.SeriesName} · ${d.Label}` : d.Label)}${extra}${p.data?.conversion ? `<br/>${escapeText(p.data.conversion)}` : ''}`;
27
+ } };
28
+ const base = { backgroundColor: t.surface, color: colors.map((c, i) => color(c, i)), animation: !!o.EnableAnimation && !reducedMotion, animationDuration: 300, animationDurationUpdate: 150, textStyle: { fontFamily: 'Inter,system-ui,sans-serif', color: t.text }, aria: { enabled: true, description: `${o.Title}. ${o.Description || ''} Exact values and keyboard selection are in the data table.` }, tooltip };
29
+ const axis = { axisLine: { show: false }, axisTick: { show: false }, axisLabel: { color: t.muted, fontSize: 10, hideOverlap: true }, splitLine: { lineStyle: { color: t.border, type: 'dashed' } } };
30
+
31
+ const c = options as CircularOptions;
32
+ const rings = c.Nested ? c.Rings! : [{ Key: '', Label: '' }];
33
+ const half = c.Shape === 'Half', nested = !!c.Nested && rings.length > 1;
34
+ const inner = kind === 'pie' ? 0 : c.InnerRadius ?? (kind === 'doughnut' ? 48 : 18);
35
+ const outside = c.LabelPosition !== 'Inside';
36
+ const outer = outside ? 66 : 76;
37
+ const names = [...new Set(c.Items.map(item => item.Label))];
38
+ const series = rings.map((ring, ri) => {
39
+ // Match ECharts' nested-pie composition: a solid core, a visible gap, then the detail ring.
40
+ const radius = nested ? ri === 0 ? [0, 32] : [44 + (outer - 44) * (ri - 1) / (rings.length - 1), 44 + (outer - 44) * ri / (rings.length - 1) - (ri === rings.length - 1 ? 0 : 2)] : [inner, Math.max(outer, inner + 5)];
41
+ const inside = !outside || (nested && ri < rings.length - 1);
42
+ return {
43
+ type: 'pie', id: ring.Key || kind, name: ring.Label, top: o.ShowLegend ? 28 : 0, bottom: 2,
44
+ center: ['50%', half ? '72%' : '55%'], radius: radius.map(r => `${r}%`),
45
+ startAngle: half ? 180 : 90, endAngle: half ? 360 : 'auto',
46
+ roseType: kind === 'nightingale' ? (c.RoseMode === 'Radius' ? 'radius' : 'area') : false,
47
+ avoidLabelOverlap: true, stillShowZeroSum: false,
48
+ label: { ...label, position: inside ? 'inside' : 'outside', backgroundColor: inside ? undefined : t.surface,
49
+ alignTo: 'none', bleedMargin: 6, distanceToLabelLine: 4,
50
+ formatter: '{b}\n{d}%', lineHeight: 15 },
51
+ labelLine: { show: !!o.ShowLabels && !inside, length: 12, length2: 8, smooth: .15 },
52
+ labelLayout: { hideOverlap: true }, emphasis: { scale: true },
53
+ data: c.Items.filter(i => !c.Nested || i.RingKey === ring.Key).map(i => {
54
+ const index = names.indexOf(i.Label), fill = color(i.Color, index);
55
+ return { id: i.Key, name: i.Label, value: i.Value, point: point(i.Key),
56
+ label: { color: contrastText(inside ? fill : t.surface), backgroundColor: inside ? fill : t.surface },
57
+ labelLine: { lineStyle: { color: fill, width: 1.25 } },
58
+ itemStyle: { ...style(i.Key, i.Color, index), decal: decal(index, fill), borderRadius: 3 }, emphasis: { itemStyle: highlightStyle(fill) } };
59
+ })
60
+ };
61
+ });
62
+ const selected = points.find(p => p.Key === o.SelectedKey);
63
+ const total = selected?.Value ?? c.Items.reduce((n, i) => n + (i.Value || 0), 0);
64
+ return { ...base, legend, series, media: outside ? [{ query: { maxWidth: 420 }, option: { series: series.map(s => ({ id: s.id, radius: s.radius.map(r => `${parseFloat(r) * .84}%`) })) } }] : [], graphic: kind === 'doughnut' && !nested && inner >= 35 ? [{ type: 'text', left: 'center', top: half ? '63%' : '48%', silent: true, style: { text: `${formatValue(total, o, true)}\n${selected?.Label || c.CenterLabel || 'Total'}`, textAlign: 'center', fill: t.text, font: '600 16px Inter,system-ui', lineHeight: 24, width: Math.max(24, Math.min(width, o.Height || 320) * inner / 100 - 12), overflow: 'truncate' } }] : [] };
65
+
66
+ }