@shadow-js/ui 0.0.0-stage → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (190) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/LICENSE +21 -0
  3. package/README.md +598 -2
  4. package/dist/cli.mjs +2293 -0
  5. package/dist/dev/cli/schema.d.ts +73 -0
  6. package/dist/dev/index.d.ts +6 -0
  7. package/dist/dev/index.js +5 -0
  8. package/dist/dev/locales/ar.d.ts +141 -0
  9. package/dist/dev/locales/ar.js +128 -0
  10. package/dist/dev/locales/ar.js.map +1 -0
  11. package/dist/dev/locales/en.d.ts +141 -0
  12. package/dist/dev/locales/en.js +128 -0
  13. package/dist/dev/locales/en.js.map +1 -0
  14. package/dist/dev/registry/utils/utils.d.ts +29 -0
  15. package/dist/dev/registry/utils/utils.js +27 -0
  16. package/dist/dev/registry/utils/utils.js.map +1 -0
  17. package/dist/dev/theme/theme-logic.d.ts +187 -0
  18. package/dist/dev/theme/theme-logic.js +184 -0
  19. package/dist/dev/theme/theme-logic.js.map +1 -0
  20. package/dist/dev/theme/theme-provider.d.ts +90 -0
  21. package/dist/dev/theme/theme-provider.js +150 -0
  22. package/dist/dev/theme/theme-provider.js.map +1 -0
  23. package/dist/prod/index.js +1 -0
  24. package/dist/prod/locales/ar.js +2 -0
  25. package/dist/prod/locales/ar.js.map +1 -0
  26. package/dist/prod/locales/en.js +2 -0
  27. package/dist/prod/locales/en.js.map +1 -0
  28. package/dist/prod/registry/utils/utils.js +2 -0
  29. package/dist/prod/registry/utils/utils.js.map +1 -0
  30. package/dist/prod/theme/theme-logic.js +2 -0
  31. package/dist/prod/theme/theme-logic.js.map +1 -0
  32. package/dist/prod/theme/theme-provider.js +2 -0
  33. package/dist/prod/theme/theme-provider.js.map +1 -0
  34. package/package.json +102 -4
  35. package/registry/accordion/accordion.tsx +210 -0
  36. package/registry/accordion/item.json +4 -0
  37. package/registry/alert/alert.tsx +200 -0
  38. package/registry/alert/item.json +4 -0
  39. package/registry/alert-dialog/alert-dialog.tsx +530 -0
  40. package/registry/alert-dialog/item.json +4 -0
  41. package/registry/aspect-ratio/aspect-ratio.tsx +71 -0
  42. package/registry/aspect-ratio/item.json +4 -0
  43. package/registry/attachment/attachment.tsx +459 -0
  44. package/registry/attachment/item.json +4 -0
  45. package/registry/avatar/avatar.tsx +385 -0
  46. package/registry/avatar/item.json +4 -0
  47. package/registry/badge/badge.tsx +108 -0
  48. package/registry/badge/item.json +4 -0
  49. package/registry/breadcrumb/breadcrumb.tsx +354 -0
  50. package/registry/breadcrumb/item.json +4 -0
  51. package/registry/bubble/bubble.tsx +260 -0
  52. package/registry/bubble/item.json +4 -0
  53. package/registry/button/button.tsx +125 -0
  54. package/registry/button/item.json +4 -0
  55. package/registry/button-group/button-group.tsx +189 -0
  56. package/registry/button-group/item.json +4 -0
  57. package/registry/calendar/calendar.tsx +348 -0
  58. package/registry/calendar/item.json +4 -0
  59. package/registry/card/card.tsx +311 -0
  60. package/registry/card/item.json +4 -0
  61. package/registry/carousel/carousel.tsx +851 -0
  62. package/registry/carousel/item.json +4 -0
  63. package/registry/chart/chart.tsx +1956 -0
  64. package/registry/chart/item.json +4 -0
  65. package/registry/chart-scale/chart-scale.ts +670 -0
  66. package/registry/chart-scale/item.json +5 -0
  67. package/registry/checkbox/checkbox.tsx +79 -0
  68. package/registry/checkbox/item.json +4 -0
  69. package/registry/code-block/code-block.tsx +470 -0
  70. package/registry/code-block/item.json +4 -0
  71. package/registry/collapsible/collapsible.tsx +117 -0
  72. package/registry/collapsible/item.json +4 -0
  73. package/registry/combobox/combobox.tsx +977 -0
  74. package/registry/combobox/item.json +4 -0
  75. package/registry/command/command.tsx +757 -0
  76. package/registry/command/item.json +4 -0
  77. package/registry/context-menu/context-menu.tsx +697 -0
  78. package/registry/context-menu/item.json +4 -0
  79. package/registry/data-table/data-table.tsx +437 -0
  80. package/registry/data-table/item.json +4 -0
  81. package/registry/date-picker/date-picker.tsx +349 -0
  82. package/registry/date-picker/item.json +4 -0
  83. package/registry/dialog/dialog.tsx +412 -0
  84. package/registry/dialog/item.json +4 -0
  85. package/registry/direction/direction.tsx +88 -0
  86. package/registry/direction/item.json +4 -0
  87. package/registry/drawer/drawer.tsx +787 -0
  88. package/registry/drawer/item.json +4 -0
  89. package/registry/dropdown-menu/dropdown-menu.tsx +692 -0
  90. package/registry/dropdown-menu/item.json +4 -0
  91. package/registry/empty/empty.tsx +240 -0
  92. package/registry/empty/item.json +4 -0
  93. package/registry/field/field.tsx +624 -0
  94. package/registry/field/item.json +4 -0
  95. package/registry/form/form.tsx +1112 -0
  96. package/registry/form/item.json +4 -0
  97. package/registry/hover-card/hover-card.tsx +144 -0
  98. package/registry/hover-card/item.json +4 -0
  99. package/registry/icons/icons.tsx +1691 -0
  100. package/registry/icons/item.json +4 -0
  101. package/registry/input/input.tsx +43 -0
  102. package/registry/input/item.json +4 -0
  103. package/registry/input-group/input-group.tsx +315 -0
  104. package/registry/input-group/item.json +4 -0
  105. package/registry/input-otp/input-otp.tsx +329 -0
  106. package/registry/input-otp/item.json +4 -0
  107. package/registry/item/item.json +4 -0
  108. package/registry/item/item.tsx +439 -0
  109. package/registry/kbd/item.json +4 -0
  110. package/registry/kbd/kbd.tsx +65 -0
  111. package/registry/label/item.json +4 -0
  112. package/registry/label/label.tsx +60 -0
  113. package/registry/marker/item.json +4 -0
  114. package/registry/marker/marker.tsx +175 -0
  115. package/registry/menubar/item.json +4 -0
  116. package/registry/menubar/menubar.tsx +694 -0
  117. package/registry/message/item.json +4 -0
  118. package/registry/message/message.tsx +280 -0
  119. package/registry/message-scroller/item.json +4 -0
  120. package/registry/message-scroller/message-scroller.tsx +262 -0
  121. package/registry/native-select/item.json +4 -0
  122. package/registry/native-select/native-select.tsx +113 -0
  123. package/registry/navigation-menu/item.json +4 -0
  124. package/registry/navigation-menu/navigation-menu.tsx +362 -0
  125. package/registry/pagination/item.json +4 -0
  126. package/registry/pagination/pagination.tsx +460 -0
  127. package/registry/password-input/item.json +5 -0
  128. package/registry/password-input/password-input.tsx +494 -0
  129. package/registry/popover/item.json +4 -0
  130. package/registry/popover/popover.tsx +284 -0
  131. package/registry/progress/item.json +4 -0
  132. package/registry/progress/progress.tsx +190 -0
  133. package/registry/questionnaire/item.json +4 -0
  134. package/registry/questionnaire/questionnaire.tsx +569 -0
  135. package/registry/radio-group/item.json +4 -0
  136. package/registry/radio-group/radio-group.tsx +101 -0
  137. package/registry/resizable/item.json +4 -0
  138. package/registry/resizable/resizable.tsx +455 -0
  139. package/registry/scroll-area/item.json +4 -0
  140. package/registry/scroll-area/scroll-area.tsx +149 -0
  141. package/registry/select/item.json +4 -0
  142. package/registry/select/select.tsx +571 -0
  143. package/registry/separator/item.json +4 -0
  144. package/registry/separator/separator.tsx +52 -0
  145. package/registry/sheet/item.json +4 -0
  146. package/registry/sheet/sheet.tsx +476 -0
  147. package/registry/sidebar/item.json +4 -0
  148. package/registry/sidebar/sidebar.tsx +1870 -0
  149. package/registry/skeleton/item.json +4 -0
  150. package/registry/skeleton/skeleton.tsx +47 -0
  151. package/registry/slider/item.json +4 -0
  152. package/registry/slider/slider.tsx +149 -0
  153. package/registry/spinner/item.json +4 -0
  154. package/registry/spinner/spinner.tsx +51 -0
  155. package/registry/switch/item.json +4 -0
  156. package/registry/switch/switch.tsx +93 -0
  157. package/registry/table/item.json +4 -0
  158. package/registry/table/table.tsx +373 -0
  159. package/registry/tabs/item.json +4 -0
  160. package/registry/tabs/tabs.tsx +209 -0
  161. package/registry/textarea/item.json +4 -0
  162. package/registry/textarea/textarea.tsx +51 -0
  163. package/registry/toast/item.json +4 -0
  164. package/registry/toast/toast.tsx +949 -0
  165. package/registry/toggle/item.json +4 -0
  166. package/registry/toggle/toggle.tsx +110 -0
  167. package/registry/toggle-group/item.json +4 -0
  168. package/registry/toggle-group/toggle-group.tsx +189 -0
  169. package/registry/tooltip/item.json +4 -0
  170. package/registry/tooltip/tooltip.tsx +169 -0
  171. package/registry/trace/item.json +4 -0
  172. package/registry/trace/trace.tsx +356 -0
  173. package/registry/ui-strings/item.json +5 -0
  174. package/registry/ui-strings/ui-strings.tsx +454 -0
  175. package/registry/use-highlighted-code/item.json +5 -0
  176. package/registry/use-highlighted-code/use-highlighted-code.tsx +145 -0
  177. package/registry/use-mobile/item.json +5 -0
  178. package/registry/use-mobile/use-mobile.ts +46 -0
  179. package/registry/utils/item.json +5 -0
  180. package/registry/utils/utils.ts +38 -0
  181. package/registry.json +3274 -0
  182. package/styles/animations.css +321 -0
  183. package/styles/theme-core.css +55 -0
  184. package/styles/theme.css +12 -0
  185. package/styles/themes/gray.css +69 -0
  186. package/styles/themes/neutral.css +69 -0
  187. package/styles/themes/slate.css +69 -0
  188. package/styles/themes/stone.css +69 -0
  189. package/styles/themes/zinc.css +69 -0
  190. package/styles/typeset.css +251 -0
@@ -0,0 +1,4 @@
1
+ {
2
+ "title": "Chart",
3
+ "description": "Themeable SVG line, area, bar, pie, donut and radial charts with axes, a keyboard-navigable tooltip and legend, and a table for screen readers."
4
+ }
@@ -0,0 +1,670 @@
1
+ /**
2
+ * A point `[x, y]` in the coordinates of an SVG: `x` grows to the right and `y` grows downward.
3
+ * @see {@link linePath}
4
+ * @since 1.0.0
5
+ */
6
+ export type ChartPoint = readonly [number, number];
7
+
8
+ /**
9
+ * How a line joins its points: `"linear"` draws straight segments, `"monotone"` draws a smooth curve
10
+ * that never rises or falls between two points more than the points themselves do, and `"step"`
11
+ * draws a horizontal run to the middle of each gap and a vertical jump there.
12
+ * @see {@link linePath}
13
+ * @since 1.0.0
14
+ */
15
+ export type ChartCurve = "linear" | "monotone" | "step";
16
+
17
+ /** A full turn, less the rounding a user-supplied angle can carry. @internal */
18
+ const FULL_TURN = Math.PI * 2 - 1e-9;
19
+
20
+ /** A coordinate with two decimals. @internal */
21
+ const fixed = (value: number): string => value.toFixed(2);
22
+
23
+ /** A radius as a short number: `40` stays `40`, `40.456` becomes `40.46`. @internal */
24
+ const short = (value: number): string => String(Number(value.toFixed(2)));
25
+
26
+ /**
27
+ * Maps a value from a domain to a range, linearly. It is the scale the charts use for their axes. A flat
28
+ * domain (`low` equal to `high`) maps to the middle of the range.
29
+ *
30
+ * @param domain The input `[low, high]`.
31
+ * @param range The output `[low, high]`; `low` may be above `high`, as for a y axis that points up.
32
+ * @param value The value to map.
33
+ * @returns The mapped value.
34
+ * @example
35
+ * import { linearScale } from "@/lib/chart-scale";
36
+ *
37
+ * export const middle = linearScale([0, 100], [200, 0], 50); // 100
38
+ * @see {@link invertLinear}
39
+ * @see {@link niceMax}
40
+ * @since 1.0.0
41
+ */
42
+ export function linearScale(
43
+ domain: readonly [number, number],
44
+ range: readonly [number, number],
45
+ value: number,
46
+ ): number {
47
+ const [low, high] = domain;
48
+ const [from, to] = range;
49
+ if (high === low) return (from + to) / 2;
50
+ return from + ((value - low) / (high - low)) * (to - from);
51
+ }
52
+
53
+ /**
54
+ * The inverse of {@link linearScale}: the value that maps to a position. A flat range gives the low end
55
+ * of the domain.
56
+ *
57
+ * @param domain The input `[low, high]` of the scale.
58
+ * @param range The output `[low, high]` of the scale.
59
+ * @param position A position in the range.
60
+ * @returns The value in the domain.
61
+ * @example
62
+ * import { invertLinear } from "@/lib/chart-scale";
63
+ *
64
+ * export const value = invertLinear([0, 100], [200, 0], 100); // 50
65
+ * @see {@link linearScale}
66
+ * @since 1.0.0
67
+ */
68
+ export function invertLinear(
69
+ domain: readonly [number, number],
70
+ range: readonly [number, number],
71
+ position: number,
72
+ ): number {
73
+ const [low, high] = domain;
74
+ const [from, to] = range;
75
+ if (to === from) return low;
76
+ return low + ((position - from) / (to - from)) * (high - low);
77
+ }
78
+
79
+ /**
80
+ * A round upper bound for a value, so an axis ends on a tidy number: 1, 2, 5 or 10 times a power of ten.
81
+ * A value that is not positive, or not finite, gives `1`.
82
+ *
83
+ * @param value The largest value the axis must show.
84
+ * @returns The bound, which is at least `value`.
85
+ * @example
86
+ * import { niceMax } from "@/lib/chart-scale";
87
+ *
88
+ * export const top = niceMax(87); // 100
89
+ * export const small = niceMax(0.4); // 0.5
90
+ * @see {@link niceTicks}
91
+ * @since 1.0.0
92
+ */
93
+ export function niceMax(value: number): number {
94
+ if (!(value > 0) || !Number.isFinite(value)) return 1;
95
+ const power = 10 ** Math.floor(Math.log10(value));
96
+ const fraction = value / power;
97
+ const step = fraction <= 1 ? 1 : fraction <= 2 ? 2 : fraction <= 5 ? 5 : 10;
98
+ return step * power;
99
+ }
100
+
101
+ /**
102
+ * Round, evenly spaced tick values that cover a range: the step is 1, 2 or 5 times a power of ten, the
103
+ * first tick is at or below `min` and the last is at or above `max`. About `count` ticks are made, never
104
+ * more than twice that plus two. A flat range (`min` equal to `max`, or closer than a millionth of the larger one) is shown from zero to the value,
105
+ * and a range that is not finite gives the ticks of `0` to `1`.
106
+ *
107
+ * @param min The lowest value to cover.
108
+ * @param max The highest value to cover.
109
+ * @param count About how many ticks to make (default `5`).
110
+ * @returns The ticks, in ascending order, at least two.
111
+ * @example
112
+ * import { niceTicks } from "@/lib/chart-scale";
113
+ *
114
+ * export const ticks = niceTicks(0, 87, 5); // [0, 20, 40, 60, 80, 100]
115
+ * @see {@link niceDomain}
116
+ * @since 1.0.0
117
+ */
118
+ export function niceTicks(min: number, max: number, count = 5): number[] {
119
+ let low = Math.min(min, max);
120
+ let high = Math.max(min, max);
121
+ const scale = Math.max(Math.abs(low), Math.abs(high));
122
+ if (!Number.isFinite(scale) || scale < 1e-290) {
123
+ low = 0;
124
+ high = 1;
125
+ } else if (high - low <= 1e-6 * scale) {
126
+ if (high > 0) low = 0;
127
+ else high = 0;
128
+ }
129
+ const wanted = Math.min(50, Math.max(1, Math.floor(count) || 5));
130
+ const raw = (high - low) / wanted;
131
+ const base = 10 ** Math.floor(Math.log10(raw));
132
+ const error = raw / base;
133
+ const factor =
134
+ error >= Math.sqrt(50) ? 10 : error >= Math.sqrt(10) ? 5 : error >= Math.SQRT2 ? 2 : 1;
135
+ const step = factor * base;
136
+ const first = Math.floor(low / step + 1e-9);
137
+ const last = Math.ceil(high / step - 1e-9);
138
+ const ticks: number[] = [];
139
+ for (let index = first; index <= last; index += 1) {
140
+ const value = Number((index * step).toPrecision(15));
141
+ ticks.push(value === 0 ? 0 : value);
142
+ }
143
+ return ticks;
144
+ }
145
+
146
+ /**
147
+ * The nice range of an axis: the first and last of {@link niceTicks}.
148
+ *
149
+ * @param min The lowest value to cover.
150
+ * @param max The highest value to cover.
151
+ * @param count About how many ticks the axis has (default `5`).
152
+ * @returns The domain `[low, high]`, which contains `[min, max]`.
153
+ * @example
154
+ * import { niceDomain } from "@/lib/chart-scale";
155
+ *
156
+ * export const domain = niceDomain(3, 87); // [0, 100]
157
+ * @see {@link niceTicks}
158
+ * @since 1.0.0
159
+ */
160
+ export function niceDomain(min: number, max: number, count = 5): [number, number] {
161
+ const ticks = niceTicks(min, max, count);
162
+ return [ticks[0] ?? 0, ticks[ticks.length - 1] ?? 1];
163
+ }
164
+
165
+ /**
166
+ * Evenly spaced positions from `from` to `to`, both ends included: where the points of a line sit. One
167
+ * point sits in the middle. `from` may be above `to`, which mirrors the row for a right-to-left chart.
168
+ *
169
+ * @param count How many positions.
170
+ * @param from The position of the first point.
171
+ * @param to The position of the last point.
172
+ * @returns The positions, in order.
173
+ * @example
174
+ * import { pointPositions } from "@/lib/chart-scale";
175
+ *
176
+ * export const xs = pointPositions(3, 0, 100); // [0, 50, 100]
177
+ * @see {@link bandPositions}
178
+ * @since 1.0.0
179
+ */
180
+ export function pointPositions(count: number, from: number, to: number): number[] {
181
+ if (count <= 0) return [];
182
+ if (count === 1) return [(from + to) / 2];
183
+ const gap = (to - from) / (count - 1);
184
+ return Array.from({ length: count }, (_, index) => from + index * gap);
185
+ }
186
+
187
+ /**
188
+ * The layout of bars: `count` equal bands between `from` and `to`, each with a centered bar that leaves a
189
+ * share of the band empty. `from` may be above `to`, which mirrors the row for a right-to-left chart.
190
+ *
191
+ * @param count How many bands.
192
+ * @param from The edge where the first band starts.
193
+ * @param to The edge where the last band ends.
194
+ * @param padding The share of each band left empty, from `0` to below `1` (default `0.2`).
195
+ * @returns The `centers` of the bands, the signed `step` between two centers, and the `bandwidth` (the
196
+ * width of a bar group).
197
+ * @example
198
+ * import { bandPositions } from "@/lib/chart-scale";
199
+ *
200
+ * export const bands = bandPositions(4, 0, 100); // centers 12.5, 37.5, 62.5, 87.5
201
+ * @see {@link pointPositions}
202
+ * @since 1.0.0
203
+ */
204
+ export function bandPositions(
205
+ count: number,
206
+ from: number,
207
+ to: number,
208
+ padding = 0.2,
209
+ ): { readonly centers: number[]; readonly step: number; readonly bandwidth: number } {
210
+ if (count <= 0) return { centers: [], step: 0, bandwidth: 0 };
211
+ const step = (to - from) / count;
212
+ const share = Math.min(1, Math.max(0, padding));
213
+ return {
214
+ centers: Array.from({ length: count }, (_, index) => from + (index + 0.5) * step),
215
+ step,
216
+ bandwidth: Math.abs(step) * (1 - share),
217
+ };
218
+ }
219
+
220
+ /**
221
+ * The index of the position nearest to `x`, which is how a pointer over a chart finds the data point under
222
+ * it. The positions must be in order, ascending or descending (the x positions of a chart are, mirrored or
223
+ * not). The first of two equally near positions wins. It takes time proportional to the logarithm of the
224
+ * number of positions.
225
+ *
226
+ * @param positions The x positions of the items, in order.
227
+ * @param x The x position to match.
228
+ * @returns The index of the nearest position, or `-1` when there are no positions or `x` is not finite.
229
+ * @example
230
+ * import { nearestIndex } from "@/lib/chart-scale";
231
+ *
232
+ * export const index = nearestIndex([0, 50, 100], 60); // 1
233
+ * @see {@link pointPositions}
234
+ * @since 1.0.0
235
+ */
236
+ export function nearestIndex(positions: readonly number[], x: number): number {
237
+ const count = positions.length;
238
+ if (count === 0 || !Number.isFinite(x)) return -1;
239
+ const first = positions[0] ?? 0;
240
+ const last = positions[count - 1] ?? 0;
241
+ const ascending = last >= first;
242
+ let low = 0;
243
+ let high = count - 1;
244
+ while (low < high) {
245
+ const middle = (low + high) >> 1;
246
+ const position = positions[middle] ?? 0;
247
+ if (ascending ? position < x : position > x) low = middle + 1;
248
+ else high = middle;
249
+ }
250
+ if (low === 0) return 0;
251
+ const before = Math.abs((positions[low - 1] ?? 0) - x);
252
+ const after = Math.abs((positions[low] ?? 0) - x);
253
+ let best = after < before ? low : low - 1;
254
+ // Of several items at one position the first wins.
255
+ while (best > 0 && positions[best - 1] === positions[best]) best -= 1;
256
+ return best;
257
+ }
258
+
259
+ /** The slopes a monotone curve leaves each point with, per unit of distance along the line. @internal */
260
+ function monotoneSlopes(points: readonly ChartPoint[]): { slopes: number[]; widths: number[] } {
261
+ const widths: number[] = [];
262
+ const secants: number[] = [];
263
+ for (let index = 0; index < points.length - 1; index += 1) {
264
+ const from = points[index] as ChartPoint;
265
+ const to = points[index + 1] as ChartPoint;
266
+ const width = Math.abs(to[0] - from[0]);
267
+ widths.push(width);
268
+ secants.push(width === 0 ? 0 : (to[1] - from[1]) / width);
269
+ }
270
+ const slopes: number[] = [secants[0] ?? 0];
271
+ for (let index = 1; index < points.length - 1; index += 1) {
272
+ const before = secants[index - 1] ?? 0;
273
+ const after = secants[index] ?? 0;
274
+ const left = widths[index - 1] ?? 0;
275
+ const right = widths[index] ?? 0;
276
+ const first = 2 * right + left;
277
+ const second = right + 2 * left;
278
+ slopes.push(
279
+ before * after <= 0 || first + second === 0
280
+ ? 0
281
+ : (first + second) / (first / before + second / after),
282
+ );
283
+ }
284
+ slopes.push(secants[secants.length - 1] ?? 0);
285
+ return { slopes, widths };
286
+ }
287
+
288
+ /**
289
+ * The SVG path `d` through points, joined by straight segments, steps or a smooth monotone curve. The
290
+ * coordinates are written with two decimals.
291
+ *
292
+ * A monotone curve (a weighted harmonic mean of the neighboring slopes, as in Fritsch and Butland) never
293
+ * bulges past a point: between two points it stays between their heights. The points may run in either
294
+ * direction along x.
295
+ *
296
+ * @param points The points `[x, y]`, in drawing order.
297
+ * @param curve How to join the points (default `"linear"`).
298
+ * @returns The path data, or an empty string for no points.
299
+ * @example
300
+ * import { linePath } from "@/lib/chart-scale";
301
+ *
302
+ * export const line = linePath([
303
+ * [0, 10],
304
+ * [50, 4],
305
+ * ]); // "M0.00 10.00 L50.00 4.00"
306
+ * @see {@link areaPath}
307
+ * @since 1.0.0
308
+ */
309
+ export function linePath(points: readonly ChartPoint[], curve: ChartCurve = "linear"): string {
310
+ const first = points[0];
311
+ if (first === undefined) return "";
312
+ const parts = [`M${fixed(first[0])} ${fixed(first[1])}`];
313
+ if (curve === "monotone" && points.length > 1) {
314
+ const { slopes, widths } = monotoneSlopes(points);
315
+ for (let index = 0; index < points.length - 1; index += 1) {
316
+ const from = points[index] as ChartPoint;
317
+ const to = points[index + 1] as ChartPoint;
318
+ const third = (to[0] - from[0]) / 3;
319
+ const reach = (widths[index] ?? 0) / 3;
320
+ const startY = from[1] + (slopes[index] ?? 0) * reach;
321
+ const endY = to[1] - (slopes[index + 1] ?? 0) * reach;
322
+ parts.push(
323
+ `C${fixed(from[0] + third)} ${fixed(startY)} ${fixed(to[0] - third)} ${fixed(endY)} ${fixed(to[0])} ${fixed(to[1])}`,
324
+ );
325
+ }
326
+ return parts.join(" ");
327
+ }
328
+ for (let index = 1; index < points.length; index += 1) {
329
+ const from = points[index - 1] as ChartPoint;
330
+ const to = points[index] as ChartPoint;
331
+ if (curve === "step") {
332
+ const middle = (from[0] + to[0]) / 2;
333
+ parts.push(
334
+ `L${fixed(middle)} ${fixed(from[1])}`,
335
+ `L${fixed(middle)} ${fixed(to[1])}`,
336
+ `L${fixed(to[0])} ${fixed(to[1])}`,
337
+ );
338
+ } else parts.push(`L${fixed(to[0])} ${fixed(to[1])}`);
339
+ }
340
+ return parts.join(" ");
341
+ }
342
+
343
+ /**
344
+ * The closed SVG path of the area between a line of points and a baseline or a second line: the top
345
+ * line, then down to the baseline at the last x, across to the first x, and closed. With a second line of
346
+ * points (the bottom edge of a stacked area, one point for each of the top ones) the path goes back along
347
+ * that line instead.
348
+ *
349
+ * @param points The points `[x, y]` of the top edge, in drawing order.
350
+ * @param baseline The y coordinate of the bottom edge, or the points of the bottom edge in the same
351
+ * order as the top ones.
352
+ * @param curve How to join the points of both edges (default `"linear"`).
353
+ * @returns The path data, or an empty string for no points.
354
+ * @example
355
+ * import { areaPath } from "@/lib/chart-scale";
356
+ *
357
+ * export const area = areaPath(
358
+ * [
359
+ * [0, 10],
360
+ * [50, 4],
361
+ * ],
362
+ * 100,
363
+ * );
364
+ * @see {@link linePath}
365
+ * @since 1.0.0
366
+ */
367
+ export function areaPath(
368
+ points: readonly ChartPoint[],
369
+ baseline: number | readonly ChartPoint[],
370
+ curve: ChartCurve = "linear",
371
+ ): string {
372
+ const first = points[0];
373
+ const last = points[points.length - 1];
374
+ if (first === undefined || last === undefined) return "";
375
+ const top = linePath(points, curve);
376
+ if (typeof baseline === "number") {
377
+ return `${top} L${fixed(last[0])} ${fixed(baseline)} L${fixed(first[0])} ${fixed(baseline)} Z`;
378
+ }
379
+ const back = linePath([...baseline].reverse(), curve);
380
+ return back === "" ? `${top} Z` : `${top} L${back.slice(1)} Z`;
381
+ }
382
+
383
+ /**
384
+ * A point on a circle, measured in radians clockwise from the top: angle `0` points up, `Math.PI / 2`
385
+ * points right. Use it to place labels around a pie.
386
+ *
387
+ * @param cx The x coordinate of the center.
388
+ * @param cy The y coordinate of the center.
389
+ * @param radius The distance from the center.
390
+ * @param angle The angle in radians.
391
+ * @returns The point `[x, y]`.
392
+ * @example
393
+ * import { polarPoint } from "@/lib/chart-scale";
394
+ *
395
+ * export const right = polarPoint(50, 50, 40, Math.PI / 2); // [90, 50] up to rounding
396
+ * @see {@link slicePath}
397
+ * @since 1.0.0
398
+ */
399
+ export function polarPoint(
400
+ cx: number,
401
+ cy: number,
402
+ radius: number,
403
+ angle: number,
404
+ ): [number, number] {
405
+ return [cx + radius * Math.sin(angle), cy - radius * Math.cos(angle)];
406
+ }
407
+
408
+ /**
409
+ * The closed SVG path of a pie slice: a wedge from the center to the arc between two angles. Angles are in
410
+ * radians, clockwise from the top, as in {@link polarPoint}. A slice of a full turn is a disc, and a slice
411
+ * with no sweep gives an empty string.
412
+ *
413
+ * @param cx The x coordinate of the center.
414
+ * @param cy The y coordinate of the center.
415
+ * @param radius The radius of the arc.
416
+ * @param start The angle the slice starts at, in radians.
417
+ * @param end The angle the slice ends at, in radians, after `start`.
418
+ * @returns The path data.
419
+ * @example
420
+ * import { slicePath } from "@/lib/chart-scale";
421
+ *
422
+ * export const quarter = slicePath(50, 50, 40, 0, Math.PI / 2);
423
+ * @see {@link arcPath}
424
+ * @since 1.0.0
425
+ */
426
+ export function slicePath(
427
+ cx: number,
428
+ cy: number,
429
+ radius: number,
430
+ start: number,
431
+ end: number,
432
+ ): string {
433
+ const sweep = end - start;
434
+ if (!(sweep > 0)) return "";
435
+ const r = short(radius);
436
+ const [x1, y1] = polarPoint(cx, cy, radius, start);
437
+ if (sweep >= FULL_TURN) {
438
+ const [x2, y2] = polarPoint(cx, cy, radius, start + Math.PI);
439
+ return `M${fixed(x1)} ${fixed(y1)} A${r} ${r} 0 1 1 ${fixed(x2)} ${fixed(y2)} A${r} ${r} 0 1 1 ${fixed(x1)} ${fixed(y1)} Z`;
440
+ }
441
+ const [x2, y2] = polarPoint(cx, cy, radius, end);
442
+ const large = sweep > Math.PI ? 1 : 0;
443
+ return `M${cx} ${cy} L${fixed(x1)} ${fixed(y1)} A${r} ${r} 0 ${large} 1 ${fixed(x2)} ${fixed(y2)} Z`;
444
+ }
445
+
446
+ /**
447
+ * The closed SVG path of a ring segment: the part of a ring between two radii and two angles. It is how a
448
+ * donut slice and a radial bar are drawn. Angles are in radians, clockwise from the top. With an inner
449
+ * radius of zero it is the wedge of {@link slicePath}; a segment of a full turn is a whole ring.
450
+ *
451
+ * @param cx The x coordinate of the center.
452
+ * @param cy The y coordinate of the center.
453
+ * @param inner The inner radius.
454
+ * @param outer The outer radius.
455
+ * @param start The angle the segment starts at, in radians.
456
+ * @param end The angle the segment ends at, in radians, after `start`.
457
+ * @returns The path data, or an empty string when the segment has no sweep or no thickness.
458
+ * @example
459
+ * import { arcPath } from "@/lib/chart-scale";
460
+ *
461
+ * export const ring = arcPath(50, 50, 20, 40, 0, Math.PI);
462
+ * @see {@link slicePath}
463
+ * @since 1.0.0
464
+ */
465
+ export function arcPath(
466
+ cx: number,
467
+ cy: number,
468
+ inner: number,
469
+ outer: number,
470
+ start: number,
471
+ end: number,
472
+ ): string {
473
+ if (inner <= 0) return slicePath(cx, cy, outer, start, end);
474
+ const sweep = end - start;
475
+ if (!(sweep > 0) || outer <= inner) return "";
476
+ const big = short(outer);
477
+ const small = short(inner);
478
+ const [ox1, oy1] = polarPoint(cx, cy, outer, start);
479
+ const [ix1, iy1] = polarPoint(cx, cy, inner, start);
480
+ if (sweep >= FULL_TURN) {
481
+ const [ox2, oy2] = polarPoint(cx, cy, outer, start + Math.PI);
482
+ const [ix2, iy2] = polarPoint(cx, cy, inner, start + Math.PI);
483
+ return [
484
+ `M${fixed(ox1)} ${fixed(oy1)} A${big} ${big} 0 1 1 ${fixed(ox2)} ${fixed(oy2)} A${big} ${big} 0 1 1 ${fixed(ox1)} ${fixed(oy1)} Z`,
485
+ `M${fixed(ix1)} ${fixed(iy1)} A${small} ${small} 0 1 0 ${fixed(ix2)} ${fixed(iy2)} A${small} ${small} 0 1 0 ${fixed(ix1)} ${fixed(iy1)} Z`,
486
+ ].join(" ");
487
+ }
488
+ const [ox2, oy2] = polarPoint(cx, cy, outer, end);
489
+ const [ix2, iy2] = polarPoint(cx, cy, inner, end);
490
+ const large = sweep > Math.PI ? 1 : 0;
491
+ return `M${fixed(ox1)} ${fixed(oy1)} A${big} ${big} 0 ${large} 1 ${fixed(ox2)} ${fixed(oy2)} L${fixed(ix2)} ${fixed(iy2)} A${small} ${small} 0 ${large} 0 ${fixed(ix1)} ${fixed(iy1)} Z`;
492
+ }
493
+
494
+ /**
495
+ * One slice of {@link pieAngles}: where it starts and ends, and its share of the total.
496
+ * @see {@link pieAngles}
497
+ * @since 1.0.0
498
+ */
499
+ export interface PieAngle {
500
+ /** The angle the slice starts at, in radians clockwise from the top. */
501
+ readonly start: number;
502
+ /** The angle the slice ends at, in radians. */
503
+ readonly end: number;
504
+ /** The slice's share of the total, from `0` to `1`. */
505
+ readonly fraction: number;
506
+ }
507
+
508
+ /**
509
+ * Options of {@link pieAngles}.
510
+ * @see {@link pieAngles}
511
+ * @since 1.0.0
512
+ */
513
+ export interface PieAngleOptions {
514
+ /**
515
+ * The angle the first slice starts at, in radians clockwise from the top.
516
+ * @default 0
517
+ */
518
+ readonly start?: number | undefined;
519
+ /**
520
+ * The angle the last slice ends at.
521
+ * @default start plus a full turn
522
+ */
523
+ readonly end?: number | undefined;
524
+ /**
525
+ * The gap left between two slices, in radians. A slice thinner than the gap shrinks to a line.
526
+ * @default 0
527
+ */
528
+ readonly pad?: number | undefined;
529
+ }
530
+
531
+ /**
532
+ * The angles of the slices of a pie: each value gets a share of the arc in proportion to its size. A value
533
+ * that is zero, negative or not finite gets no slice (an empty one at its place), and when every value is
534
+ * empty no slice has a share.
535
+ *
536
+ * @param values The value of each slice, in order.
537
+ * @param options The arc to fill and the gap between slices.
538
+ * @returns One {@link PieAngle} for each value, in the same order.
539
+ * @example
540
+ * import { pieAngles } from "@/lib/chart-scale";
541
+ *
542
+ * export const slices = pieAngles([1, 1, 2]); // fractions 0.25, 0.25, 0.5
543
+ * @see {@link arcPath}
544
+ * @since 1.0.0
545
+ */
546
+ export function pieAngles(values: readonly number[], options: PieAngleOptions = {}): PieAngle[] {
547
+ const start = options.start ?? 0;
548
+ const end = options.end ?? start + Math.PI * 2;
549
+ const gap = Math.max(0, options.pad ?? 0);
550
+ const weights = values.map((value) => (value > 0 && Number.isFinite(value) ? value : 0));
551
+ const total = weights.reduce((sum, weight) => sum + weight, 0);
552
+ let cursor = start;
553
+ return weights.map((weight) => {
554
+ if (weight === 0 || total === 0) return { start: cursor, end: cursor, fraction: 0 };
555
+ const fraction = weight / total;
556
+ const room = (end - start) * fraction;
557
+ const from = cursor;
558
+ cursor += room;
559
+ const inset = Math.min(gap / 2, Math.abs(room) / 2);
560
+ return { start: from + inset, end: cursor - inset, fraction };
561
+ });
562
+ }
563
+
564
+ /**
565
+ * Stacks series on top of each other. The series are columns of numbers, one number for each row. In
566
+ * every row the positive values pile up from zero in series order and the negative ones pile down from
567
+ * zero, so a stack never hides a value.
568
+ *
569
+ * @param columns The values of each series, one array for each series.
570
+ * @returns For each series, where each of its segments starts (`lower`) and ends (`upper`).
571
+ * @example
572
+ * import { stackColumns } from "@/lib/chart-scale";
573
+ *
574
+ * export const stacked = stackColumns([
575
+ * [1, 2],
576
+ * [3, 4],
577
+ * ]); // upper: [[1, 2], [4, 6]]
578
+ * @see {@link niceTicks}
579
+ * @since 1.0.0
580
+ */
581
+ export function stackColumns(columns: readonly (readonly number[])[]): {
582
+ readonly lower: number[][];
583
+ readonly upper: number[][];
584
+ } {
585
+ const rows = columns.reduce((most, column) => Math.max(most, column.length), 0);
586
+ const lower = columns.map(() => new Array<number>(rows).fill(0));
587
+ const upper = columns.map(() => new Array<number>(rows).fill(0));
588
+ for (let row = 0; row < rows; row += 1) {
589
+ let up = 0;
590
+ let down = 0;
591
+ columns.forEach((column, series) => {
592
+ const value = column[row] ?? 0;
593
+ const amount = Number.isFinite(value) ? value : 0;
594
+ const base = amount >= 0 ? up : down;
595
+ (lower[series] as number[])[row] = base;
596
+ (upper[series] as number[])[row] = base + amount;
597
+ if (amount >= 0) up += amount;
598
+ else down += amount;
599
+ });
600
+ }
601
+ return { lower, upper };
602
+ }
603
+
604
+ /**
605
+ * Chooses which rows of a long series to draw. With no more rows than `limit` every row is kept. With more,
606
+ * the rows are cut into runs and each run keeps its lowest and its highest row of every series (so a spike
607
+ * or a dip is never lost), plus the first and the last row. The result has at most `limit` indices, or
608
+ * `2 * columns.length + 2` when `limit` is smaller than that.
609
+ *
610
+ * @param columns The values of each series, one array for each series, all as long as the rows.
611
+ * @param limit The most rows to keep.
612
+ * @returns The indices of the rows to draw, in ascending order, without repeats.
613
+ * @example
614
+ * import { decimateIndices } from "@/lib/chart-scale";
615
+ *
616
+ * const values = Array.from({ length: 20_000 }, (_, index) => Math.sin(index / 40));
617
+ * export const kept = decimateIndices([values], 5000); // fewer than 5000 indices
618
+ * @see {@link linePath}
619
+ * @since 1.0.0
620
+ */
621
+ export function decimateIndices(columns: readonly (readonly number[])[], limit: number): number[] {
622
+ let length = 0;
623
+ for (const column of columns) if (column.length > length) length = column.length;
624
+ if (length === 0) return [];
625
+ const cap = Math.max(2, Number.isFinite(limit) ? Math.floor(limit) : 2);
626
+ if (length <= cap) return Array.from({ length }, (_, index) => index);
627
+ const series = columns.length;
628
+ const runs = Math.max(1, Math.floor((cap - 2) / (2 * Math.max(series, 1))));
629
+ const interior = length - 2;
630
+ const keep = new Set<number>([0, length - 1]);
631
+ for (let run = 0; run < runs; run += 1) {
632
+ const from = 1 + Math.floor((run * interior) / runs);
633
+ const to = 1 + Math.floor(((run + 1) * interior) / runs);
634
+ if (to <= from) continue;
635
+ if (series === 0) {
636
+ keep.add(from);
637
+ continue;
638
+ }
639
+ for (const column of columns) {
640
+ let lowest = -1;
641
+ let highest = -1;
642
+ for (let index = from; index < to; index += 1) {
643
+ const value = column[index];
644
+ if (value === undefined || Number.isNaN(value)) continue;
645
+ if (lowest < 0 || value < (column[lowest] as number)) lowest = index;
646
+ if (highest < 0 || value > (column[highest] as number)) highest = index;
647
+ }
648
+ if (lowest >= 0) keep.add(lowest);
649
+ if (highest >= 0) keep.add(highest);
650
+ }
651
+ }
652
+ return [...keep].sort((a, b) => a - b);
653
+ }
654
+
655
+ /**
656
+ * A series key made safe to use inside a CSS custom property name (`--color-<key>`): every character
657
+ * other than a letter, a digit, `-` and `_` becomes `-`. An empty key becomes `series`.
658
+ *
659
+ * @param key The key of a series or the name of a slice.
660
+ * @returns The safe key.
661
+ * @example
662
+ * import { cssKey } from "@/lib/chart-scale";
663
+ *
664
+ * export const name = `--color-${cssKey("Chrome browser")}`; // "--color-Chrome-browser"
665
+ * @since 1.0.0
666
+ */
667
+ export function cssKey(key: string): string {
668
+ const safe = key.replace(/[^A-Za-z0-9_-]/g, "-");
669
+ return safe === "" ? "series" : safe;
670
+ }
@@ -0,0 +1,5 @@
1
+ {
2
+ "title": "Chart scale",
3
+ "description": "Pure functions for charts: scales, nice ticks, line, area and arc paths, pie angles, stacking and decimation.",
4
+ "type": "lib"
5
+ }