@blotch/dolan 22.2.2 → 22.2.4

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 (259) hide show
  1. package/dist/commands/add.d.ts +6 -2
  2. package/dist/commands/add.js +41 -11
  3. package/dist/commands/add.js.map +1 -1
  4. package/dist/commands/add.test.js +162 -9
  5. package/dist/commands/add.test.js.map +1 -1
  6. package/dist/commands/diff.d.ts +2 -1
  7. package/dist/commands/diff.js +38 -25
  8. package/dist/commands/diff.js.map +1 -1
  9. package/dist/commands/diff.test.js +72 -11
  10. package/dist/commands/diff.test.js.map +1 -1
  11. package/dist/commands/doctor.js +228 -34
  12. package/dist/commands/doctor.js.map +1 -1
  13. package/dist/commands/doctor.test.js +367 -10
  14. package/dist/commands/doctor.test.js.map +1 -1
  15. package/dist/commands/eject.js +7 -0
  16. package/dist/commands/eject.js.map +1 -1
  17. package/dist/commands/eject.test.js +39 -0
  18. package/dist/commands/eject.test.js.map +1 -1
  19. package/dist/commands/info.js +3 -2
  20. package/dist/commands/info.js.map +1 -1
  21. package/dist/commands/init.d.ts +37 -1
  22. package/dist/commands/init.js +169 -40
  23. package/dist/commands/init.js.map +1 -1
  24. package/dist/commands/init.test.js +392 -27
  25. package/dist/commands/init.test.js.map +1 -1
  26. package/dist/commands/install-packages.d.ts +6 -0
  27. package/dist/commands/install-packages.js +20 -0
  28. package/dist/commands/install-packages.js.map +1 -0
  29. package/dist/commands/install-packages.test.d.ts +1 -0
  30. package/dist/commands/install-packages.test.js +55 -0
  31. package/dist/commands/install-packages.test.js.map +1 -0
  32. package/dist/commands/list.js +21 -14
  33. package/dist/commands/list.js.map +1 -1
  34. package/dist/commands/list.test.js +62 -5
  35. package/dist/commands/list.test.js.map +1 -1
  36. package/dist/commands/outdated.js +11 -10
  37. package/dist/commands/outdated.js.map +1 -1
  38. package/dist/commands/outdated.test.js +20 -5
  39. package/dist/commands/outdated.test.js.map +1 -1
  40. package/dist/commands/remove.d.ts +4 -0
  41. package/dist/commands/remove.js +17 -4
  42. package/dist/commands/remove.js.map +1 -1
  43. package/dist/commands/remove.test.js +53 -2
  44. package/dist/commands/remove.test.js.map +1 -1
  45. package/dist/commands/theme.js +1 -1
  46. package/dist/commands/theme.js.map +1 -1
  47. package/dist/commands/update.d.ts +11 -0
  48. package/dist/commands/update.js +174 -58
  49. package/dist/commands/update.js.map +1 -1
  50. package/dist/commands/update.test.js +587 -33
  51. package/dist/commands/update.test.js.map +1 -1
  52. package/dist/index.js +7 -0
  53. package/dist/index.js.map +1 -1
  54. package/dist/lockfile.d.ts +7 -0
  55. package/dist/lockfile.js +36 -0
  56. package/dist/lockfile.js.map +1 -1
  57. package/dist/lockfile.test.d.ts +1 -0
  58. package/dist/lockfile.test.js +25 -0
  59. package/dist/lockfile.test.js.map +1 -0
  60. package/dist/registry.d.ts +14 -0
  61. package/dist/registry.js +78 -24
  62. package/dist/registry.js.map +1 -1
  63. package/dist/registry.test.js +119 -38
  64. package/dist/registry.test.js.map +1 -1
  65. package/dist/test-utils.d.ts +5 -0
  66. package/dist/test-utils.js +6 -1
  67. package/dist/test-utils.js.map +1 -1
  68. package/dist/testing/fire-lynx-event.d.ts +14 -0
  69. package/dist/testing/fire-lynx-event.js +19 -0
  70. package/dist/testing/fire-lynx-event.js.map +1 -0
  71. package/dist/testing/fire-lynx-event.test.d.ts +1 -0
  72. package/dist/testing/fire-lynx-event.test.js +44 -0
  73. package/dist/testing/fire-lynx-event.test.js.map +1 -0
  74. package/dist/testing/lynx-main-thread.d.ts +22 -0
  75. package/dist/testing/lynx-main-thread.js +11 -0
  76. package/dist/testing/lynx-main-thread.js.map +1 -0
  77. package/dist/testing/lynx-main-thread.test.d.ts +1 -0
  78. package/dist/testing/lynx-main-thread.test.js +24 -0
  79. package/dist/testing/lynx-main-thread.test.js.map +1 -0
  80. package/dist/testing/set-input-signal.d.ts +14 -0
  81. package/dist/testing/set-input-signal.js +21 -0
  82. package/dist/testing/set-input-signal.js.map +1 -0
  83. package/dist/testing/set-input-signal.test.d.ts +1 -0
  84. package/dist/testing/set-input-signal.test.js +20 -0
  85. package/dist/testing/set-input-signal.test.js.map +1 -0
  86. package/dist/ui/components/accordion/accordion.test.ts +158 -144
  87. package/dist/ui/components/accordion/accordion.ts +19 -14
  88. package/dist/ui/components/action-sheet/action-sheet.test.ts +283 -151
  89. package/dist/ui/components/action-sheet/action-sheet.ts +77 -43
  90. package/dist/ui/components/alert/alert.test.ts +67 -19
  91. package/dist/ui/components/alert/alert.ts +14 -16
  92. package/dist/ui/components/alert-dialog/alert-dialog.test.ts +208 -94
  93. package/dist/ui/components/alert-dialog/alert-dialog.ts +75 -38
  94. package/dist/ui/components/area-chart/area-chart.test.ts +134 -119
  95. package/dist/ui/components/area-chart/area-chart.ts +100 -315
  96. package/dist/ui/components/area-chart/index.ts +2 -3
  97. package/dist/ui/components/avatar/avatar.test.ts +89 -45
  98. package/dist/ui/components/badge/badge.test.ts +57 -23
  99. package/dist/ui/components/badge/badge.ts +12 -14
  100. package/dist/ui/components/bar-chart/bar-chart.test.ts +172 -45
  101. package/dist/ui/components/bar-chart/bar-chart.ts +140 -294
  102. package/dist/ui/components/bar-chart/index.ts +3 -3
  103. package/dist/ui/components/bottom-sheet/bottom-sheet.test.ts +386 -128
  104. package/dist/ui/components/bottom-sheet/bottom-sheet.ts +170 -53
  105. package/dist/ui/components/button/button.test.ts +611 -2
  106. package/dist/ui/components/button/button.ts +434 -49
  107. package/dist/ui/components/button/index.ts +2 -0
  108. package/dist/ui/components/button-group/button-group.test.ts +484 -2
  109. package/dist/ui/components/button-group/button-group.ts +380 -68
  110. package/dist/ui/components/button-group/index.ts +3 -0
  111. package/dist/ui/components/candlestick-chart/candlestick-chart.test.ts +183 -58
  112. package/dist/ui/components/candlestick-chart/candlestick-chart.ts +150 -323
  113. package/dist/ui/components/candlestick-chart/index.ts +3 -4
  114. package/dist/ui/components/card/card.test.ts +82 -62
  115. package/dist/ui/components/chart/chart-focus.test.ts +132 -0
  116. package/dist/ui/components/chart/chart-focus.ts +115 -0
  117. package/dist/ui/components/chart/chart-presets.test.ts +335 -0
  118. package/dist/ui/components/chart/chart-presets.ts +278 -0
  119. package/dist/ui/components/chart/chart-scene.test.ts +589 -0
  120. package/dist/ui/components/chart/chart-scene.ts +483 -0
  121. package/dist/ui/components/chart/chart-theme.test.ts +85 -0
  122. package/dist/ui/components/chart/chart-theme.ts +210 -0
  123. package/dist/ui/components/chart/chart-zoom.test.ts +159 -0
  124. package/dist/ui/components/chart/chart-zoom.ts +162 -0
  125. package/dist/ui/components/chart/chart.test.ts +1017 -0
  126. package/dist/ui/components/chart/chart.ts +934 -0
  127. package/dist/ui/components/chart/index.ts +24 -0
  128. package/dist/ui/components/checkbox/checkbox.test.ts +124 -42
  129. package/dist/ui/components/checkbox/checkbox.ts +23 -23
  130. package/dist/ui/components/collapsible/collapsible.test.ts +158 -81
  131. package/dist/ui/components/collapsible/collapsible.ts +19 -14
  132. package/dist/ui/components/context-menu/context-menu.test.ts +224 -0
  133. package/dist/ui/components/context-menu/context-menu.ts +110 -0
  134. package/dist/ui/components/context-menu/index.ts +1 -0
  135. package/dist/ui/components/dialog/dialog.test.ts +213 -108
  136. package/dist/ui/components/dialog/dialog.ts +72 -35
  137. package/dist/ui/components/dock/dock-scroll.test.ts +130 -0
  138. package/dist/ui/components/dock/dock-scroll.ts +133 -0
  139. package/dist/ui/components/dock/dock.test.ts +749 -0
  140. package/dist/ui/components/dock/dock.ts +960 -0
  141. package/dist/ui/components/dock/index.ts +2 -0
  142. package/dist/ui/components/empty-state/empty-state.test.ts +28 -39
  143. package/dist/ui/components/glass/concentric-radius.test.ts +13 -0
  144. package/dist/ui/components/glass/concentric-radius.ts +17 -0
  145. package/dist/ui/components/glass/glass-design.test.ts +78 -0
  146. package/dist/ui/components/glass/glass-design.ts +119 -0
  147. package/dist/ui/components/glass/glass.test.ts +338 -0
  148. package/dist/ui/components/glass/glass.ts +360 -0
  149. package/dist/ui/components/glass/index.ts +18 -0
  150. package/dist/ui/components/icon/icon-theme.test.ts +169 -0
  151. package/dist/ui/components/icon/icon-theme.ts +227 -0
  152. package/dist/ui/components/icon/icon.test.ts +85 -25
  153. package/dist/ui/components/icon/icon.ts +38 -21
  154. package/dist/ui/components/input/input.test.ts +178 -101
  155. package/dist/ui/components/input/input.ts +15 -12
  156. package/dist/ui/components/label/label.test.ts +11 -24
  157. package/dist/ui/components/line-chart/index.ts +2 -3
  158. package/dist/ui/components/line-chart/line-chart.test.ts +149 -56
  159. package/dist/ui/components/line-chart/line-chart.ts +83 -241
  160. package/dist/ui/components/list/list.test.ts +119 -21
  161. package/dist/ui/components/list/list.ts +21 -15
  162. package/dist/ui/components/menu/index.ts +25 -0
  163. package/dist/ui/components/menu/menu-placement.test.ts +153 -0
  164. package/dist/ui/components/menu/menu-placement.ts +159 -0
  165. package/dist/ui/components/menu/menu-popup.test.ts +632 -0
  166. package/dist/ui/components/menu/menu-popup.ts +544 -0
  167. package/dist/ui/components/menu/menu.test.ts +429 -0
  168. package/dist/ui/components/menu/menu.ts +415 -0
  169. package/dist/ui/components/nav-drawer/nav-drawer.test.ts +435 -191
  170. package/dist/ui/components/nav-drawer/nav-drawer.ts +127 -56
  171. package/dist/ui/components/pagination/pagination.test.ts +177 -65
  172. package/dist/ui/components/pagination/pagination.ts +55 -8
  173. package/dist/ui/components/progress/progress.test.ts +102 -46
  174. package/dist/ui/components/progress/progress.ts +52 -54
  175. package/dist/ui/components/radio-group/radio-group.test.ts +128 -83
  176. package/dist/ui/components/radio-group/radio-group.ts +24 -24
  177. package/dist/ui/components/scatter-chart/index.ts +2 -3
  178. package/dist/ui/components/scatter-chart/scatter-chart.test.ts +131 -26
  179. package/dist/ui/components/scatter-chart/scatter-chart.ts +104 -250
  180. package/dist/ui/components/scroll-area/scroll-area.test.ts +95 -24
  181. package/dist/ui/components/scroll-area/scroll-area.ts +8 -8
  182. package/dist/ui/components/select/select.test.ts +138 -78
  183. package/dist/ui/components/select/select.ts +17 -11
  184. package/dist/ui/components/separator/separator.test.ts +18 -17
  185. package/dist/ui/components/sheet/sheet.test.ts +64 -11
  186. package/dist/ui/components/sheet/sheet.ts +9 -1
  187. package/dist/ui/components/skeleton/skeleton.test.ts +24 -7
  188. package/dist/ui/components/skeleton/skeleton.ts +10 -16
  189. package/dist/ui/components/spinner/spinner.test.ts +67 -22
  190. package/dist/ui/components/spinner/spinner.ts +48 -41
  191. package/dist/ui/components/switch/switch.test.ts +116 -46
  192. package/dist/ui/components/switch/switch.ts +8 -13
  193. package/dist/ui/components/tab-bar/index.ts +1 -0
  194. package/dist/ui/components/tab-bar/tab-bar.test.ts +280 -0
  195. package/dist/ui/components/tab-bar/tab-bar.ts +230 -0
  196. package/dist/ui/components/tabs/tabs.test.ts +215 -157
  197. package/dist/ui/components/tabs/tabs.ts +110 -118
  198. package/dist/ui/components/textarea/textarea.test.ts +206 -100
  199. package/dist/ui/components/textarea/textarea.ts +14 -12
  200. package/dist/ui/components/toast/toast.test.ts +436 -315
  201. package/dist/ui/components/toast/toast.ts +62 -55
  202. package/dist/ui/components/toggle/toggle.test.ts +247 -140
  203. package/dist/ui/theme/chart-colors.test.ts +35 -0
  204. package/dist/ui/theme/dark.css +46 -2
  205. package/dist/ui/theme/default.css +63 -7
  206. package/dist/ui/theme/icon-colors.test.ts +35 -0
  207. package/dist/ui/theme/rgb-channels.test.ts +84 -0
  208. package/dist/ui/theme/tailwind-build.test.ts +114 -0
  209. package/dist/ui/theme/tailwind-css.test.ts +83 -0
  210. package/dist/ui/theme/tailwind-plugin.test.ts +42 -2
  211. package/dist/ui/theme/tailwind-plugin.ts +98 -25
  212. package/dist/ui/theme/tailwind.css +113 -0
  213. package/dist/ui/utils/animate.test.ts +13 -1
  214. package/dist/utils/analyze.d.ts +14 -0
  215. package/dist/utils/analyze.js +15 -0
  216. package/dist/utils/analyze.js.map +1 -1
  217. package/dist/utils/analyze.test.js +14 -1
  218. package/dist/utils/analyze.test.js.map +1 -1
  219. package/dist/utils/installed.d.ts +29 -0
  220. package/dist/utils/installed.js +43 -0
  221. package/dist/utils/installed.js.map +1 -0
  222. package/dist/utils/installed.test.d.ts +1 -0
  223. package/dist/utils/installed.test.js +57 -0
  224. package/dist/utils/installed.test.js.map +1 -0
  225. package/dist/utils/interactive.d.ts +9 -0
  226. package/dist/utils/interactive.js +10 -0
  227. package/dist/utils/interactive.js.map +1 -0
  228. package/dist/utils/interactive.test.d.ts +1 -0
  229. package/dist/utils/interactive.test.js +24 -0
  230. package/dist/utils/interactive.test.js.map +1 -0
  231. package/dist/utils/packages.d.ts +36 -0
  232. package/dist/utils/packages.js +70 -0
  233. package/dist/utils/packages.js.map +1 -0
  234. package/dist/utils/packages.test.d.ts +1 -0
  235. package/dist/utils/packages.test.js +89 -0
  236. package/dist/utils/packages.test.js.map +1 -0
  237. package/dist/utils/resolve-paths.d.ts +17 -1
  238. package/dist/utils/resolve-paths.js +21 -3
  239. package/dist/utils/resolve-paths.js.map +1 -1
  240. package/dist/utils/resolve-paths.test.js +38 -2
  241. package/dist/utils/resolve-paths.test.js.map +1 -1
  242. package/dist/utils/tailwind.d.ts +25 -0
  243. package/dist/utils/tailwind.js +51 -0
  244. package/dist/utils/tailwind.js.map +1 -0
  245. package/dist/utils/tailwind.test.d.ts +1 -0
  246. package/dist/utils/tailwind.test.js +71 -0
  247. package/dist/utils/tailwind.test.js.map +1 -0
  248. package/package.json +13 -1
  249. package/dist/ui/components/area-chart/area-chart-render.test.ts +0 -121
  250. package/dist/ui/components/bar-chart/bar-chart-render.test.ts +0 -174
  251. package/dist/ui/components/button/button-render.test.ts +0 -113
  252. package/dist/ui/components/button-group/button-group-render.test.ts +0 -146
  253. package/dist/ui/components/candlestick-chart/candlestick-chart-render.test.ts +0 -174
  254. package/dist/ui/components/cartesian-chart/cartesian-chart-render.test.ts +0 -188
  255. package/dist/ui/components/cartesian-chart/cartesian-chart.test.ts +0 -525
  256. package/dist/ui/components/cartesian-chart/cartesian-chart.ts +0 -1140
  257. package/dist/ui/components/cartesian-chart/index.ts +0 -18
  258. package/dist/ui/components/line-chart/line-chart-render.test.ts +0 -121
  259. package/dist/ui/components/scatter-chart/scatter-chart-render.test.ts +0 -157
@@ -0,0 +1,335 @@
1
+ import {
2
+ Component,
3
+ Injector,
4
+ type Type,
5
+ computed,
6
+ input,
7
+ output,
8
+ signal,
9
+ viewChild,
10
+ } from '@angular/core';
11
+ import {
12
+ fireEvent,
13
+ render,
14
+ waitForUpdate,
15
+ } from '@blotch/angular-lynx-testing-library';
16
+ import { defineChart, dot } from '@tanstack/charts/universal';
17
+ import { describe, expect, it } from 'vitest';
18
+ import { UiChart, tooltip } from './chart';
19
+ import {
20
+ type CartesianPresetOptions,
21
+ PRESET_CHART_BINDINGS,
22
+ UiCartesianPreset,
23
+ type XYDatum,
24
+ linearAxis,
25
+ minSpacing,
26
+ presetAxes,
27
+ } from './chart-presets';
28
+
29
+ /**
30
+ * The configured domain of an axis config's scale instance.
31
+ */
32
+ const domainOf = (axis: { scale: unknown }): number[] =>
33
+ (axis.scale as { domain: () => number[] }).domain();
34
+
35
+ describe('linearAxis', () => {
36
+ const base = { tickCount: 5, grid: true };
37
+
38
+ it('fits the data and rounds out to nice values', () => {
39
+ expect(domainOf(linearAxis({ ...base, values: [3, 47] }))).toEqual([0, 50]);
40
+ });
41
+
42
+ it('keeps fixed ends exact and rounds only the free one', () => {
43
+ expect(
44
+ domainOf(linearAxis({ ...base, values: [3, 47], min: 1, max: 2 })),
45
+ ).toEqual([1, 2]);
46
+ expect(domainOf(linearAxis({ ...base, values: [3, 47], min: 1 }))).toEqual([
47
+ 1, 50,
48
+ ]);
49
+ });
50
+
51
+ it('includes extra values, such as a baseline', () => {
52
+ expect(
53
+ domainOf(linearAxis({ ...base, values: [5, 10], include: [0] })),
54
+ ).toEqual([0, 10]);
55
+ });
56
+
57
+ it('gives flat and empty data a span around the value', () => {
58
+ expect(domainOf(linearAxis({ ...base, values: [10, 10] }))).toEqual([
59
+ 4, 16,
60
+ ]);
61
+ expect(domainOf(linearAxis({ ...base, values: [0] }))).toEqual([-1, 1]);
62
+ expect(domainOf(linearAxis({ ...base, values: [] }))).toEqual([0, 1]);
63
+ expect(domainOf(linearAxis({ ...base, values: [Number.NaN] }))).toEqual([
64
+ 0, 1,
65
+ ]);
66
+ });
67
+
68
+ it('orders reversed fixed ends', () => {
69
+ expect(
70
+ domainOf(linearAxis({ ...base, values: [], min: 10, max: 0 })),
71
+ ).toEqual([0, 10]);
72
+ });
73
+
74
+ it('widens by a margin, then by padding fractions', () => {
75
+ expect(
76
+ domainOf(linearAxis({ ...base, values: [], min: 0, max: 10, margin: 1 })),
77
+ ).toEqual([-1, 11]);
78
+ // 10% each side of a 0–8 domain: the plot spans 10 units, inset 1 each side.
79
+ const [lo, hi] = domainOf(
80
+ linearAxis({ ...base, values: [], min: 0, max: 8, padding: [0.1, 0.1] }),
81
+ );
82
+ expect(lo).toBeCloseTo(-1);
83
+ expect(hi).toBeCloseTo(9);
84
+ // Padding that leaves no plot is ignored.
85
+ expect(
86
+ domainOf(
87
+ linearAxis({
88
+ ...base,
89
+ values: [],
90
+ min: 0,
91
+ max: 8,
92
+ padding: [0.6, 0.6],
93
+ }),
94
+ ),
95
+ ).toEqual([0, 8]);
96
+ });
97
+
98
+ it('configures grid, label and ticks', () => {
99
+ const format = (value: number) => `${value}%`;
100
+ const axis = linearAxis({
101
+ ...base,
102
+ values: [0, 1],
103
+ label: 'Share',
104
+ format,
105
+ grid: false,
106
+ });
107
+ expect(axis.grid).toBe(false);
108
+ expect(axis.axis).toEqual({ label: 'Share', ticks: { count: 5, format } });
109
+ expect(
110
+ (
111
+ linearAxis({ ...base, values: [0, 1], label: '' }).axis as {
112
+ label?: string;
113
+ }
114
+ ).label,
115
+ ).toBeUndefined();
116
+ });
117
+ });
118
+
119
+ describe('minSpacing', () => {
120
+ it('finds the smallest gap between distinct values', () => {
121
+ expect(minSpacing([5, 1, 3, 3, 4])).toBe(1);
122
+ expect(minSpacing([0, 10])).toBe(10);
123
+ });
124
+
125
+ it('defaults to 1 without a gap', () => {
126
+ expect(minSpacing([])).toBe(1);
127
+ expect(minSpacing([2])).toBe(1);
128
+ expect(minSpacing([2, 2])).toBe(1);
129
+ });
130
+ });
131
+
132
+ describe('presetAxes', () => {
133
+ it('maps the shared options onto both axes', () => {
134
+ const axes = presetAxes(
135
+ {
136
+ tickCount: 3,
137
+ showGrid: false,
138
+ showXGrid: true,
139
+ xAxisLabel: 'X',
140
+ yAxisLabel: 'Y',
141
+ padding: { left: 0.1, right: 0.1, top: 0.1, bottom: 0.1 },
142
+ xMin: 0,
143
+ xMax: 8,
144
+ yMin: 0,
145
+ yMax: 8,
146
+ },
147
+ { x: [], y: [], xMargin: 0 },
148
+ );
149
+ expect(axes.x.grid).toBe(true);
150
+ expect(axes.y.grid).toBe(false);
151
+ expect(axes.x.axis).toMatchObject({ label: 'X', ticks: { count: 3 } });
152
+ expect(axes.y.axis).toMatchObject({ label: 'Y' });
153
+ expect(domainOf(axes.x)[0]).toBeCloseTo(-1);
154
+ expect(domainOf(axes.y)[1]).toBeCloseTo(9);
155
+ });
156
+
157
+ it('defaults to five ticks, a y grid and no padding', () => {
158
+ const axes = presetAxes({}, { x: [0, 10], y: [0, 10], yInclude: [-5] });
159
+ expect(axes.x.grid).toBe(false);
160
+ expect(axes.y.grid).toBe(true);
161
+ expect(domainOf(axes.x)).toEqual([0, 10]);
162
+ expect(domainOf(axes.y)).toEqual([-10, 10]);
163
+ });
164
+ });
165
+
166
+ /**
167
+ * The smallest preset: a dot per datum on the shared axes, built the way the
168
+ * library's presets (`<ui-line-chart>`, …) build theirs.
169
+ */
170
+ @Component({
171
+ selector: 'test-dot-chart',
172
+ imports: [UiChart],
173
+ template: `
174
+ <ui-chart
175
+ #chart
176
+ [definition]="definition()"
177
+ ${PRESET_CHART_BINDINGS}
178
+ (pointSelect)="pointTap.emit(datumOf($event))"
179
+ />
180
+ `,
181
+ })
182
+ class DotChart extends UiCartesianPreset<XYDatum> {
183
+ readonly data = input.required<readonly XYDatum[]>();
184
+ /** The tapped datum, or `null` for a tap on empty space. */
185
+ readonly pointTap = output<XYDatum | null>();
186
+
187
+ protected readonly definition = computed(() => {
188
+ const options = this.commonOptions();
189
+ const data = this.data();
190
+ return defineChart({
191
+ marks: [dot(data, { x: 'x', y: 'y' })],
192
+ scales: presetAxes(options, {
193
+ x: data.map((d) => d.x),
194
+ y: data.map((d) => d.y),
195
+ }),
196
+ tooltip: options.tooltip === false ? false : tooltip,
197
+ });
198
+ });
199
+ }
200
+
201
+ @Component({
202
+ imports: [DotChart],
203
+ template: `
204
+ <test-dot-chart
205
+ [data]="data"
206
+ [width]="300"
207
+ [height]="160"
208
+ label="Sales"
209
+ class="mt-2"
210
+ [xAxisLabel]="xAxisLabel()"
211
+ yAxisLabel="Units"
212
+ [zoomable]="true"
213
+ zoomAxes="x"
214
+ (pointTap)="tapped.push($event)"
215
+ />
216
+ `,
217
+ })
218
+ class PresetHost {
219
+ readonly data: readonly XYDatum[] = [
220
+ { x: 0, y: 100 },
221
+ { x: 5, y: 300 },
222
+ { x: 10, y: 500 },
223
+ ];
224
+ readonly xAxisLabel = signal('Day');
225
+ /** Every `pointTap`, in order. */
226
+ readonly tapped: (XYDatum | null)[] = [];
227
+ readonly preset = viewChild.required(DotChart);
228
+ }
229
+
230
+ /**
231
+ * The chart's own view, which takes every touch.
232
+ */
233
+ const chartRoot = (container: Element) =>
234
+ container.querySelector('[accessibility-label]')!;
235
+
236
+ /**
237
+ * Where the chart drew each dot, read from its marks layer.
238
+ */
239
+ const dotsDrawn = (container: Element) =>
240
+ [
241
+ ...container
242
+ .querySelectorAll('svg')[1]!
243
+ .getAttribute('content')!
244
+ .matchAll(/<circle cx="([\d.-]+)" cy="([\d.-]+)"/g),
245
+ ].map(([, x, y]) => ({ x: Number(x), y: Number(y) }));
246
+
247
+ /**
248
+ * Touches a spot and lifts the finger right there: a tap.
249
+ */
250
+ const tap = async (root: Element, at: { x: number; y: number }) => {
251
+ fireEvent.touchstart(root, { touches: [at] });
252
+ fireEvent.touchend(root, { touches: [at] });
253
+ await waitForUpdate();
254
+ };
255
+
256
+ describe('UiCartesianPreset', () => {
257
+ it('starts every preset from the same defaults', () => {
258
+ // Angular builds a preset through the base's factory, with the preset's
259
+ // own type, so no preset runs that factory's default: build the base alone.
260
+ const base = Injector.create({
261
+ providers: [
262
+ {
263
+ provide: UiCartesianPreset,
264
+ useClass: UiCartesianPreset as unknown as Type<
265
+ UiCartesianPreset<XYDatum>
266
+ >,
267
+ },
268
+ ],
269
+ }).get(UiCartesianPreset);
270
+ const options = (
271
+ base as unknown as { commonOptions(): CartesianPresetOptions }
272
+ ).commonOptions();
273
+ expect(options).toEqual({
274
+ tickCount: 5,
275
+ showGrid: true,
276
+ showXGrid: false,
277
+ xAxisLabel: '',
278
+ yAxisLabel: '',
279
+ padding: {},
280
+ xMin: undefined,
281
+ xMax: undefined,
282
+ yMin: undefined,
283
+ yMax: undefined,
284
+ xTickFormat: undefined,
285
+ yTickFormat: undefined,
286
+ tooltip: true,
287
+ });
288
+ expect(base.height()).toBe(200);
289
+ expect(base.label()).toBe('Chart');
290
+ expect(base.zoomable()).toBe(false);
291
+ expect(base.zoomAxes()).toBe('xy');
292
+ });
293
+
294
+ it('passes the shared inputs through to its chart', async () => {
295
+ const { container, componentRef, getByText } = await render(PresetHost);
296
+ const root = chartRoot(container);
297
+ expect(root.getAttribute('accessibility-label')).toBe('Sales');
298
+ expect(root.getAttribute('class')).toContain('mt-2');
299
+ expect(root.getAttribute('style')).toContain('height: 160px');
300
+ expect(getByText('Units')).toBeTruthy();
301
+ expect(getByText('Day')).toBeTruthy();
302
+
303
+ (componentRef.instance as PresetHost).xAxisLabel.set('Week');
304
+ await waitForUpdate();
305
+ expect(getByText('Week')).toBeTruthy();
306
+ });
307
+
308
+ it('hands a tap over as the datum, or null off the points', async () => {
309
+ const { container, componentRef } = await render(PresetHost);
310
+ const host = componentRef.instance as PresetHost;
311
+ const root = chartRoot(container);
312
+ const middle = dotsDrawn(container)[1]!;
313
+ await tap(root, { x: middle.x + 1, y: middle.y + 1 });
314
+ await tap(root, { x: -100, y: -100 });
315
+ expect(host.tapped).toEqual([host.data[1], null]);
316
+ });
317
+
318
+ it('zooms its chart in and out, and resets it', async () => {
319
+ const { componentRef, queryByText } = await render(PresetHost);
320
+ const preset = (componentRef.instance as PresetHost).preset();
321
+ // The x axis spans 0–10; zoomed in around the middle, its last tick goes.
322
+ expect(queryByText('10')).not.toBeNull();
323
+ preset.zoomIn();
324
+ await waitForUpdate();
325
+ expect(queryByText('10')).toBeNull();
326
+ preset.zoomOut();
327
+ await waitForUpdate();
328
+ expect(queryByText('10')).not.toBeNull();
329
+
330
+ preset.zoomIn();
331
+ preset.resetZoom();
332
+ await waitForUpdate();
333
+ expect(queryByText('10')).not.toBeNull();
334
+ });
335
+ });
@@ -0,0 +1,278 @@
1
+ import { Directive, input, viewChild } from '@angular/core';
2
+ import { scaleLinear } from '@tanstack/charts/scales/linear';
3
+ import type {
4
+ ChartPoint,
5
+ ChartPositionScaleOptions,
6
+ } from '@tanstack/charts/types';
7
+
8
+ import { UiChart } from './chart';
9
+ import type { ChartColorSchemeOption } from './chart-theme';
10
+ import type { ChartZoomAxes } from './chart-zoom';
11
+
12
+ /**
13
+ * Shared pieces of the chart presets (`<ui-line-chart>`, `<ui-bar-chart>`, …).
14
+ * Each preset is a thin component that turns its inputs into a TanStack
15
+ * definition for `<ui-chart>`; this file holds the axis handling and the inputs
16
+ * they all share, so every preset behaves the same way.
17
+ */
18
+
19
+ /** A point in x/y data space, the datum the line, area and scatter presets plot. */
20
+ export type XYDatum = {
21
+ readonly x: number;
22
+ readonly y: number;
23
+ };
24
+
25
+ /**
26
+ * Inner inset per side, as a **fraction** (`0`–`1`) of the plot's width or
27
+ * height, so the first and last marks don't sit on the frame. Fractions keep
28
+ * the inset proportional as the chart resizes. Omitted sides default to `0`.
29
+ */
30
+ export type ChartPadding = {
31
+ readonly top?: number;
32
+ readonly right?: number;
33
+ readonly bottom?: number;
34
+ readonly left?: number;
35
+ };
36
+
37
+ /** The axis, grid and tooltip options every cartesian preset takes. */
38
+ export type CartesianPresetOptions = {
39
+ readonly tickCount?: number;
40
+ readonly showGrid?: boolean;
41
+ readonly showXGrid?: boolean;
42
+ readonly xAxisLabel?: string;
43
+ readonly yAxisLabel?: string;
44
+ readonly padding?: ChartPadding;
45
+ readonly xMin?: number;
46
+ readonly xMax?: number;
47
+ readonly yMin?: number;
48
+ readonly yMax?: number;
49
+ readonly xTickFormat?: (value: number) => string;
50
+ readonly yTickFormat?: (value: number) => string;
51
+ /** `false` turns the tooltip off. */
52
+ readonly tooltip?: boolean;
53
+ };
54
+
55
+ /**
56
+ * The x and y axes for a preset's data, from the shared options: `x` and `y`
57
+ * are the values each axis fits, `xMargin` widens x by data units (half a bar),
58
+ * `yInclude` keeps values like a baseline in view.
59
+ */
60
+ export const presetAxes = (
61
+ options: CartesianPresetOptions,
62
+ data: {
63
+ readonly x: readonly number[];
64
+ readonly y: readonly number[];
65
+ readonly xMargin?: number;
66
+ readonly yInclude?: readonly number[];
67
+ },
68
+ ) => {
69
+ const { tickCount = 5, padding = {} } = options;
70
+ return {
71
+ x: linearAxis({
72
+ values: data.x,
73
+ min: options.xMin,
74
+ max: options.xMax,
75
+ margin: data.xMargin,
76
+ tickCount,
77
+ padding: [padding.left ?? 0, padding.right ?? 0],
78
+ format: options.xTickFormat,
79
+ label: options.xAxisLabel,
80
+ grid: options.showXGrid ?? false,
81
+ }),
82
+ y: linearAxis({
83
+ values: data.y,
84
+ include: data.yInclude,
85
+ min: options.yMin,
86
+ max: options.yMax,
87
+ tickCount,
88
+ padding: [padding.bottom ?? 0, padding.top ?? 0],
89
+ format: options.yTickFormat,
90
+ label: options.yAxisLabel,
91
+ grid: options.showGrid ?? true,
92
+ }),
93
+ };
94
+ };
95
+
96
+ export type LinearAxisOptions = {
97
+ /** Data values on this axis, to fit the domain to. */
98
+ readonly values: readonly number[];
99
+ /** Fixed ends; a missing end fits the data. */
100
+ readonly min?: number;
101
+ readonly max?: number;
102
+ /** Values that must stay in view, such as an area's baseline. */
103
+ readonly include?: readonly number[];
104
+ readonly tickCount: number;
105
+ /** Inset at the low and high end, as fractions of the plot (see {@link ChartPadding}). */
106
+ readonly padding?: readonly [number, number];
107
+ /** Extra data units added beyond each end, e.g. half a bar so the edge bars fit. */
108
+ readonly margin?: number;
109
+ readonly format?: (value: number) => string;
110
+ readonly label?: string;
111
+ readonly grid: boolean;
112
+ };
113
+
114
+ /**
115
+ * A linear axis config: the domain fits the data (and `include`), is rounded
116
+ * out to nice tick values unless both ends are fixed, then widened by `margin`
117
+ * and `padding`. Flat or empty data gets a span around the value so the marks
118
+ * sit mid-plot instead of collapsing onto one edge.
119
+ *
120
+ * The domain is computed here, not left to TanStack's inference, because
121
+ * padding has to apply after rounding, and a zoomable axis needs a configured
122
+ * scale to carry its viewport.
123
+ */
124
+ export const linearAxis = (
125
+ options: LinearAxisOptions,
126
+ ): ChartPositionScaleOptions<number> => {
127
+ const values = [...options.values, ...(options.include ?? [])].filter(
128
+ Number.isFinite,
129
+ );
130
+ let lo = options.min ?? (values.length ? Math.min(...values) : 0);
131
+ let hi = options.max ?? (values.length ? Math.max(...values) : 1);
132
+ if (lo === hi) {
133
+ const pad = lo === 0 ? 1 : Math.abs(lo) * 0.5;
134
+ lo -= pad;
135
+ hi += pad;
136
+ }
137
+ if (lo > hi) [lo, hi] = [hi, lo];
138
+ if (options.min === undefined || options.max === undefined) {
139
+ const nice = scaleLinear()
140
+ .domain([lo, hi])
141
+ .nice(options.tickCount)
142
+ .domain();
143
+ lo = options.min ?? nice[0];
144
+ hi = options.max ?? nice[1];
145
+ }
146
+ const margin = options.margin ?? 0;
147
+ lo -= margin;
148
+ hi += margin;
149
+ const [padLo, padHi] = options.padding ?? [0, 0];
150
+ const keep = 1 - padLo - padHi;
151
+ if ((padLo || padHi) && keep > 0) {
152
+ const span = (hi - lo) / keep;
153
+ lo -= padLo * span;
154
+ hi = lo + span;
155
+ }
156
+ return {
157
+ scale: scaleLinear().domain([lo, hi]),
158
+ grid: options.grid,
159
+ axis: {
160
+ label: options.label || undefined,
161
+ ticks: { count: options.tickCount, format: options.format },
162
+ },
163
+ };
164
+ };
165
+
166
+ /** Smallest gap between neighboring values, `1` for fewer than two. */
167
+ export const minSpacing = (values: readonly number[]): number => {
168
+ const sorted = [...new Set(values)].sort((a, b) => a - b);
169
+ let spacing = Infinity;
170
+ for (let i = 1; i < sorted.length; i++) {
171
+ spacing = Math.min(spacing, sorted[i]! - sorted[i - 1]!);
172
+ }
173
+ return Number.isFinite(spacing) && spacing > 0 ? spacing : 1;
174
+ };
175
+
176
+ /**
177
+ * `UiChart`, for `viewChild()`. A call rather than the class itself: for a
178
+ * class, Angular's test build keeps an arrow function that nothing calls,
179
+ * which an app's coverage report would show as uncovered.
180
+ */
181
+ const chartType = () => UiChart;
182
+
183
+ /**
184
+ * The inputs and zoom API every cartesian preset shares. A preset extends this
185
+ * and adds its own data and mark inputs, and renders `<ui-chart #chart>`
186
+ * with the shared bindings.
187
+ */
188
+ @Directive()
189
+ export abstract class UiCartesianPreset<TDatum> {
190
+ /** Fixed width in px. Omit to fill the container. */
191
+ readonly width = input<number>();
192
+ readonly height = input(200);
193
+ /** Target number of ticks per axis; the axis picks nice values near it. */
194
+ readonly tickCount = input(5);
195
+ /** Horizontal gridlines at the y ticks. */
196
+ readonly showGrid = input(true);
197
+ /** Vertical gridlines at the x ticks. */
198
+ readonly showXGrid = input(false);
199
+ readonly xAxisLabel = input('');
200
+ readonly yAxisLabel = input('');
201
+ readonly padding = input<ChartPadding>({});
202
+ readonly xMin = input<number>();
203
+ readonly xMax = input<number>();
204
+ readonly yMin = input<number>();
205
+ readonly yMax = input<number>();
206
+ readonly xTickFormat = input<(value: number) => string>();
207
+ readonly yTickFormat = input<(value: number) => string>();
208
+ /** Shows a tooltip for the touched point; a tap pins it. */
209
+ readonly tooltip = input(true);
210
+ /** What a screen reader announces for the chart. */
211
+ readonly label = input('Chart');
212
+ /** Light, dark or `'system'` colors; the chart theme's setting when unset. */
213
+ readonly colorScheme = input<Exclude<ChartColorSchemeOption, Function>>();
214
+ readonly zoomable = input(false);
215
+ readonly zoomAxes = input<ChartZoomAxes>('xy');
216
+ readonly showZoomControls = input(true);
217
+ readonly minZoom = input(1);
218
+ readonly maxZoom = input(8);
219
+ readonly zoomStep = input(1.4);
220
+ readonly userClass = input<string>('', { alias: 'class' });
221
+
222
+ protected readonly chart = viewChild.required(chartType());
223
+
224
+ /**
225
+ * The shared inputs, as the options every preset's definition takes.
226
+ */
227
+ protected commonOptions(): CartesianPresetOptions {
228
+ return {
229
+ tickCount: this.tickCount(),
230
+ showGrid: this.showGrid(),
231
+ showXGrid: this.showXGrid(),
232
+ xAxisLabel: this.xAxisLabel(),
233
+ yAxisLabel: this.yAxisLabel(),
234
+ padding: this.padding(),
235
+ xMin: this.xMin(),
236
+ xMax: this.xMax(),
237
+ yMin: this.yMin(),
238
+ yMax: this.yMax(),
239
+ xTickFormat: this.xTickFormat(),
240
+ yTickFormat: this.yTickFormat(),
241
+ tooltip: this.tooltip(),
242
+ };
243
+ }
244
+
245
+ /**
246
+ * The tapped datum, unwrapped from TanStack's point.
247
+ */
248
+ protected datumOf(point: ChartPoint | null): TDatum | null {
249
+ return point ? (point.datum as TDatum) : null;
250
+ }
251
+
252
+ zoomIn(): void {
253
+ this.chart().zoomIn();
254
+ }
255
+
256
+ zoomOut(): void {
257
+ this.chart().zoomOut();
258
+ }
259
+
260
+ resetZoom(): void {
261
+ this.chart().resetZoom();
262
+ }
263
+ }
264
+
265
+ /** The `<ui-chart>` bindings a preset passes straight through. */
266
+ export const PRESET_CHART_BINDINGS = `
267
+ [width]="width()"
268
+ [height]="height()"
269
+ [label]="label()"
270
+ [colorScheme]="colorScheme()"
271
+ [class]="userClass()"
272
+ [zoomable]="zoomable()"
273
+ [zoomAxes]="zoomAxes()"
274
+ [showZoomControls]="showZoomControls()"
275
+ [minZoom]="minZoom()"
276
+ [maxZoom]="maxZoom()"
277
+ [zoomStep]="zoomStep()"
278
+ `;