@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.
- package/CHANGELOG.md +24 -0
- package/LICENSE +21 -0
- package/README.md +598 -2
- package/dist/cli.mjs +2293 -0
- package/dist/dev/cli/schema.d.ts +73 -0
- package/dist/dev/index.d.ts +6 -0
- package/dist/dev/index.js +5 -0
- package/dist/dev/locales/ar.d.ts +141 -0
- package/dist/dev/locales/ar.js +128 -0
- package/dist/dev/locales/ar.js.map +1 -0
- package/dist/dev/locales/en.d.ts +141 -0
- package/dist/dev/locales/en.js +128 -0
- package/dist/dev/locales/en.js.map +1 -0
- package/dist/dev/registry/utils/utils.d.ts +29 -0
- package/dist/dev/registry/utils/utils.js +27 -0
- package/dist/dev/registry/utils/utils.js.map +1 -0
- package/dist/dev/theme/theme-logic.d.ts +187 -0
- package/dist/dev/theme/theme-logic.js +184 -0
- package/dist/dev/theme/theme-logic.js.map +1 -0
- package/dist/dev/theme/theme-provider.d.ts +90 -0
- package/dist/dev/theme/theme-provider.js +150 -0
- package/dist/dev/theme/theme-provider.js.map +1 -0
- package/dist/prod/index.js +1 -0
- package/dist/prod/locales/ar.js +2 -0
- package/dist/prod/locales/ar.js.map +1 -0
- package/dist/prod/locales/en.js +2 -0
- package/dist/prod/locales/en.js.map +1 -0
- package/dist/prod/registry/utils/utils.js +2 -0
- package/dist/prod/registry/utils/utils.js.map +1 -0
- package/dist/prod/theme/theme-logic.js +2 -0
- package/dist/prod/theme/theme-logic.js.map +1 -0
- package/dist/prod/theme/theme-provider.js +2 -0
- package/dist/prod/theme/theme-provider.js.map +1 -0
- package/package.json +102 -4
- package/registry/accordion/accordion.tsx +210 -0
- package/registry/accordion/item.json +4 -0
- package/registry/alert/alert.tsx +200 -0
- package/registry/alert/item.json +4 -0
- package/registry/alert-dialog/alert-dialog.tsx +530 -0
- package/registry/alert-dialog/item.json +4 -0
- package/registry/aspect-ratio/aspect-ratio.tsx +71 -0
- package/registry/aspect-ratio/item.json +4 -0
- package/registry/attachment/attachment.tsx +459 -0
- package/registry/attachment/item.json +4 -0
- package/registry/avatar/avatar.tsx +385 -0
- package/registry/avatar/item.json +4 -0
- package/registry/badge/badge.tsx +108 -0
- package/registry/badge/item.json +4 -0
- package/registry/breadcrumb/breadcrumb.tsx +354 -0
- package/registry/breadcrumb/item.json +4 -0
- package/registry/bubble/bubble.tsx +260 -0
- package/registry/bubble/item.json +4 -0
- package/registry/button/button.tsx +125 -0
- package/registry/button/item.json +4 -0
- package/registry/button-group/button-group.tsx +189 -0
- package/registry/button-group/item.json +4 -0
- package/registry/calendar/calendar.tsx +348 -0
- package/registry/calendar/item.json +4 -0
- package/registry/card/card.tsx +311 -0
- package/registry/card/item.json +4 -0
- package/registry/carousel/carousel.tsx +851 -0
- package/registry/carousel/item.json +4 -0
- package/registry/chart/chart.tsx +1956 -0
- package/registry/chart/item.json +4 -0
- package/registry/chart-scale/chart-scale.ts +670 -0
- package/registry/chart-scale/item.json +5 -0
- package/registry/checkbox/checkbox.tsx +79 -0
- package/registry/checkbox/item.json +4 -0
- package/registry/code-block/code-block.tsx +470 -0
- package/registry/code-block/item.json +4 -0
- package/registry/collapsible/collapsible.tsx +117 -0
- package/registry/collapsible/item.json +4 -0
- package/registry/combobox/combobox.tsx +977 -0
- package/registry/combobox/item.json +4 -0
- package/registry/command/command.tsx +757 -0
- package/registry/command/item.json +4 -0
- package/registry/context-menu/context-menu.tsx +697 -0
- package/registry/context-menu/item.json +4 -0
- package/registry/data-table/data-table.tsx +437 -0
- package/registry/data-table/item.json +4 -0
- package/registry/date-picker/date-picker.tsx +349 -0
- package/registry/date-picker/item.json +4 -0
- package/registry/dialog/dialog.tsx +412 -0
- package/registry/dialog/item.json +4 -0
- package/registry/direction/direction.tsx +88 -0
- package/registry/direction/item.json +4 -0
- package/registry/drawer/drawer.tsx +787 -0
- package/registry/drawer/item.json +4 -0
- package/registry/dropdown-menu/dropdown-menu.tsx +692 -0
- package/registry/dropdown-menu/item.json +4 -0
- package/registry/empty/empty.tsx +240 -0
- package/registry/empty/item.json +4 -0
- package/registry/field/field.tsx +624 -0
- package/registry/field/item.json +4 -0
- package/registry/form/form.tsx +1112 -0
- package/registry/form/item.json +4 -0
- package/registry/hover-card/hover-card.tsx +144 -0
- package/registry/hover-card/item.json +4 -0
- package/registry/icons/icons.tsx +1691 -0
- package/registry/icons/item.json +4 -0
- package/registry/input/input.tsx +43 -0
- package/registry/input/item.json +4 -0
- package/registry/input-group/input-group.tsx +315 -0
- package/registry/input-group/item.json +4 -0
- package/registry/input-otp/input-otp.tsx +329 -0
- package/registry/input-otp/item.json +4 -0
- package/registry/item/item.json +4 -0
- package/registry/item/item.tsx +439 -0
- package/registry/kbd/item.json +4 -0
- package/registry/kbd/kbd.tsx +65 -0
- package/registry/label/item.json +4 -0
- package/registry/label/label.tsx +60 -0
- package/registry/marker/item.json +4 -0
- package/registry/marker/marker.tsx +175 -0
- package/registry/menubar/item.json +4 -0
- package/registry/menubar/menubar.tsx +694 -0
- package/registry/message/item.json +4 -0
- package/registry/message/message.tsx +280 -0
- package/registry/message-scroller/item.json +4 -0
- package/registry/message-scroller/message-scroller.tsx +262 -0
- package/registry/native-select/item.json +4 -0
- package/registry/native-select/native-select.tsx +113 -0
- package/registry/navigation-menu/item.json +4 -0
- package/registry/navigation-menu/navigation-menu.tsx +362 -0
- package/registry/pagination/item.json +4 -0
- package/registry/pagination/pagination.tsx +460 -0
- package/registry/password-input/item.json +5 -0
- package/registry/password-input/password-input.tsx +494 -0
- package/registry/popover/item.json +4 -0
- package/registry/popover/popover.tsx +284 -0
- package/registry/progress/item.json +4 -0
- package/registry/progress/progress.tsx +190 -0
- package/registry/questionnaire/item.json +4 -0
- package/registry/questionnaire/questionnaire.tsx +569 -0
- package/registry/radio-group/item.json +4 -0
- package/registry/radio-group/radio-group.tsx +101 -0
- package/registry/resizable/item.json +4 -0
- package/registry/resizable/resizable.tsx +455 -0
- package/registry/scroll-area/item.json +4 -0
- package/registry/scroll-area/scroll-area.tsx +149 -0
- package/registry/select/item.json +4 -0
- package/registry/select/select.tsx +571 -0
- package/registry/separator/item.json +4 -0
- package/registry/separator/separator.tsx +52 -0
- package/registry/sheet/item.json +4 -0
- package/registry/sheet/sheet.tsx +476 -0
- package/registry/sidebar/item.json +4 -0
- package/registry/sidebar/sidebar.tsx +1870 -0
- package/registry/skeleton/item.json +4 -0
- package/registry/skeleton/skeleton.tsx +47 -0
- package/registry/slider/item.json +4 -0
- package/registry/slider/slider.tsx +149 -0
- package/registry/spinner/item.json +4 -0
- package/registry/spinner/spinner.tsx +51 -0
- package/registry/switch/item.json +4 -0
- package/registry/switch/switch.tsx +93 -0
- package/registry/table/item.json +4 -0
- package/registry/table/table.tsx +373 -0
- package/registry/tabs/item.json +4 -0
- package/registry/tabs/tabs.tsx +209 -0
- package/registry/textarea/item.json +4 -0
- package/registry/textarea/textarea.tsx +51 -0
- package/registry/toast/item.json +4 -0
- package/registry/toast/toast.tsx +949 -0
- package/registry/toggle/item.json +4 -0
- package/registry/toggle/toggle.tsx +110 -0
- package/registry/toggle-group/item.json +4 -0
- package/registry/toggle-group/toggle-group.tsx +189 -0
- package/registry/tooltip/item.json +4 -0
- package/registry/tooltip/tooltip.tsx +169 -0
- package/registry/trace/item.json +4 -0
- package/registry/trace/trace.tsx +356 -0
- package/registry/ui-strings/item.json +5 -0
- package/registry/ui-strings/ui-strings.tsx +454 -0
- package/registry/use-highlighted-code/item.json +5 -0
- package/registry/use-highlighted-code/use-highlighted-code.tsx +145 -0
- package/registry/use-mobile/item.json +5 -0
- package/registry/use-mobile/use-mobile.ts +46 -0
- package/registry/utils/item.json +5 -0
- package/registry/utils/utils.ts +38 -0
- package/registry.json +3274 -0
- package/styles/animations.css +321 -0
- package/styles/theme-core.css +55 -0
- package/styles/theme.css +12 -0
- package/styles/themes/gray.css +69 -0
- package/styles/themes/neutral.css +69 -0
- package/styles/themes/slate.css +69 -0
- package/styles/themes/stone.css +69 -0
- package/styles/themes/zinc.css +69 -0
- package/styles/typeset.css +251 -0
|
@@ -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
|
+
}
|