insomni-plot 0.1.0-alpha.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/LICENSE.md +674 -0
- package/README.md +81 -0
- package/dist/core.d.mts +340 -0
- package/dist/core.mjs +1047 -0
- package/dist/index.d.mts +3426 -0
- package/dist/index.mjs +12762 -0
- package/dist/interactions-DEFL_F4E.mjs +5395 -0
- package/dist/range-presets-CzECsu3V.d.mts +1523 -0
- package/package.json +34 -0
- package/src/annotations.d.ts +121 -0
- package/src/annotations.ts +438 -0
- package/src/axis.d.ts +184 -0
- package/src/axis.test.ts +131 -0
- package/src/axis.ts +765 -0
- package/src/colorbar.d.ts +69 -0
- package/src/colorbar.ts +294 -0
- package/src/colors.d.ts +57 -0
- package/src/colors.test.ts +28 -0
- package/src/colors.ts +486 -0
- package/src/core.ts +299 -0
- package/src/format.d.ts +54 -0
- package/src/format.ts +138 -0
- package/src/grammar/accessibility.d.ts +147 -0
- package/src/grammar/accessibility.test.ts +199 -0
- package/src/grammar/accessibility.ts +443 -0
- package/src/grammar/aes.d.ts +35 -0
- package/src/grammar/aes.test.ts +75 -0
- package/src/grammar/aes.ts +120 -0
- package/src/grammar/annotations.d.ts +86 -0
- package/src/grammar/annotations.test.ts +68 -0
- package/src/grammar/annotations.ts +336 -0
- package/src/grammar/attach-brush.d.ts +44 -0
- package/src/grammar/attach-brush.test.ts +214 -0
- package/src/grammar/attach-brush.ts +111 -0
- package/src/grammar/attach-presets.d.ts +33 -0
- package/src/grammar/attach-presets.test.ts +106 -0
- package/src/grammar/attach-presets.ts +215 -0
- package/src/grammar/chart.d.ts +952 -0
- package/src/grammar/chart.test.ts +118 -0
- package/src/grammar/chart.ts +1172 -0
- package/src/grammar/color-utils.d.ts +29 -0
- package/src/grammar/color-utils.test.ts +53 -0
- package/src/grammar/color-utils.ts +66 -0
- package/src/grammar/constants.d.ts +45 -0
- package/src/grammar/constants.ts +61 -0
- package/src/grammar/coord.d.ts +183 -0
- package/src/grammar/coord.test.ts +355 -0
- package/src/grammar/coord.ts +619 -0
- package/src/grammar/data/pivot.d.ts +57 -0
- package/src/grammar/data/pivot.ts +107 -0
- package/src/grammar/emphasis-driver.d.ts +69 -0
- package/src/grammar/emphasis-driver.test.ts +199 -0
- package/src/grammar/emphasis-driver.ts +205 -0
- package/src/grammar/equality.d.ts +3 -0
- package/src/grammar/equality.ts +40 -0
- package/src/grammar/facet.d.ts +63 -0
- package/src/grammar/facet.test.ts +60 -0
- package/src/grammar/facet.ts +175 -0
- package/src/grammar/geoms/_categorical.d.ts +94 -0
- package/src/grammar/geoms/_categorical.ts +0 -0
- package/src/grammar/geoms/_distribution.d.ts +52 -0
- package/src/grammar/geoms/_distribution.ts +125 -0
- package/src/grammar/geoms/_mark.d.ts +69 -0
- package/src/grammar/geoms/_mark.ts +136 -0
- package/src/grammar/geoms/_shape.d.ts +41 -0
- package/src/grammar/geoms/_shape.ts +74 -0
- package/src/grammar/geoms/aggregate.d.ts +95 -0
- package/src/grammar/geoms/aggregate.test.ts +554 -0
- package/src/grammar/geoms/aggregate.ts +840 -0
- package/src/grammar/geoms/area.d.ts +32 -0
- package/src/grammar/geoms/area.test.ts +165 -0
- package/src/grammar/geoms/area.ts +578 -0
- package/src/grammar/geoms/band.d.ts +27 -0
- package/src/grammar/geoms/band.test.ts +57 -0
- package/src/grammar/geoms/band.ts +126 -0
- package/src/grammar/geoms/bar.d.ts +56 -0
- package/src/grammar/geoms/bar.test.ts +367 -0
- package/src/grammar/geoms/bar.ts +1054 -0
- package/src/grammar/geoms/boxplot.d.ts +129 -0
- package/src/grammar/geoms/boxplot.test.ts +299 -0
- package/src/grammar/geoms/boxplot.ts +834 -0
- package/src/grammar/geoms/connected-scatter.d.ts +27 -0
- package/src/grammar/geoms/connected-scatter.test.ts +157 -0
- package/src/grammar/geoms/connected-scatter.ts +63 -0
- package/src/grammar/geoms/emphasis.d.ts +76 -0
- package/src/grammar/geoms/emphasis.test.ts +135 -0
- package/src/grammar/geoms/emphasis.ts +162 -0
- package/src/grammar/geoms/histogram.d.ts +75 -0
- package/src/grammar/geoms/histogram.test.ts +262 -0
- package/src/grammar/geoms/histogram.ts +740 -0
- package/src/grammar/geoms/index.d.ts +20 -0
- package/src/grammar/geoms/index.ts +77 -0
- package/src/grammar/geoms/interval.d.ts +31 -0
- package/src/grammar/geoms/interval.test.ts +154 -0
- package/src/grammar/geoms/interval.ts +342 -0
- package/src/grammar/geoms/line.d.ts +38 -0
- package/src/grammar/geoms/line.test.ts +247 -0
- package/src/grammar/geoms/line.ts +659 -0
- package/src/grammar/geoms/point.d.ts +57 -0
- package/src/grammar/geoms/point.test.ts +163 -0
- package/src/grammar/geoms/point.ts +545 -0
- package/src/grammar/geoms/polar.test.ts +216 -0
- package/src/grammar/geoms/ribbon.d.ts +21 -0
- package/src/grammar/geoms/ribbon.test.ts +170 -0
- package/src/grammar/geoms/ribbon.ts +87 -0
- package/src/grammar/geoms/ridgeline.d.ts +89 -0
- package/src/grammar/geoms/ridgeline.test.ts +247 -0
- package/src/grammar/geoms/ridgeline.ts +1164 -0
- package/src/grammar/geoms/rolling.d.ts +43 -0
- package/src/grammar/geoms/rolling.test.ts +217 -0
- package/src/grammar/geoms/rolling.ts +387 -0
- package/src/grammar/geoms/rug.d.ts +28 -0
- package/src/grammar/geoms/rug.test.ts +126 -0
- package/src/grammar/geoms/rug.ts +214 -0
- package/src/grammar/geoms/rule.d.ts +23 -0
- package/src/grammar/geoms/rule.test.ts +69 -0
- package/src/grammar/geoms/rule.ts +212 -0
- package/src/grammar/geoms/smooth.d.ts +54 -0
- package/src/grammar/geoms/smooth.test.ts +78 -0
- package/src/grammar/geoms/smooth.ts +337 -0
- package/src/grammar/geoms/text.d.ts +29 -0
- package/src/grammar/geoms/text.test.ts +64 -0
- package/src/grammar/geoms/text.ts +234 -0
- package/src/grammar/geoms/tile.d.ts +61 -0
- package/src/grammar/geoms/tile.test.ts +157 -0
- package/src/grammar/geoms/tile.ts +621 -0
- package/src/grammar/geoms/types.d.ts +319 -0
- package/src/grammar/geoms/types.ts +362 -0
- package/src/grammar/geoms/violin.d.ts +85 -0
- package/src/grammar/geoms/violin.test.ts +187 -0
- package/src/grammar/geoms/violin.ts +672 -0
- package/src/grammar/index.d.ts +22 -0
- package/src/grammar/index.ts +269 -0
- package/src/grammar/interactions/_disposable.d.ts +5 -0
- package/src/grammar/interactions/_disposable.ts +23 -0
- package/src/grammar/interactions/_z.d.ts +4 -0
- package/src/grammar/interactions/_z.ts +16 -0
- package/src/grammar/interactions/brush-selection.test.ts +262 -0
- package/src/grammar/interactions/brush.d.ts +63 -0
- package/src/grammar/interactions/brush.test.ts +483 -0
- package/src/grammar/interactions/brush.ts +452 -0
- package/src/grammar/interactions/crosshair.d.ts +19 -0
- package/src/grammar/interactions/crosshair.test.ts +127 -0
- package/src/grammar/interactions/crosshair.ts +76 -0
- package/src/grammar/interactions/hit-layer.d.ts +64 -0
- package/src/grammar/interactions/hit-layer.ts +246 -0
- package/src/grammar/interactions/legend.d.ts +19 -0
- package/src/grammar/interactions/legend.ts +101 -0
- package/src/grammar/interactions/menu.d.ts +93 -0
- package/src/grammar/interactions/menu.test.ts +373 -0
- package/src/grammar/interactions/menu.ts +342 -0
- package/src/grammar/interactions/selection.d.ts +25 -0
- package/src/grammar/interactions/selection.test.ts +289 -0
- package/src/grammar/interactions/selection.ts +142 -0
- package/src/grammar/interactions/series-readout.d.ts +91 -0
- package/src/grammar/interactions/series-readout.test.ts +668 -0
- package/src/grammar/interactions/series-readout.ts +422 -0
- package/src/grammar/interactions/series-snap.d.ts +70 -0
- package/src/grammar/interactions/series-snap.test.ts +214 -0
- package/src/grammar/interactions/series-snap.ts +218 -0
- package/src/grammar/interactions/tooltip-axis.test.ts +176 -0
- package/src/grammar/interactions/tooltip-touch.browser.test.ts +49 -0
- package/src/grammar/interactions/tooltip-touch.test.ts +161 -0
- package/src/grammar/interactions/tooltip.d.ts +140 -0
- package/src/grammar/interactions/tooltip.test.ts +406 -0
- package/src/grammar/interactions/tooltip.ts +622 -0
- package/src/grammar/interactions/transitions.d.ts +34 -0
- package/src/grammar/interactions/transitions.test.ts +172 -0
- package/src/grammar/interactions/transitions.ts +160 -0
- package/src/grammar/layout.d.ts +68 -0
- package/src/grammar/layout.ts +186 -0
- package/src/grammar/legend-merge.test.ts +332 -0
- package/src/grammar/mount.d.ts +78 -0
- package/src/grammar/mount.test.ts +479 -0
- package/src/grammar/mount.ts +2112 -0
- package/src/grammar/palettes.d.ts +54 -0
- package/src/grammar/palettes.test.ts +80 -0
- package/src/grammar/palettes.ts +167 -0
- package/src/grammar/pan-zoom.test.ts +398 -0
- package/src/grammar/phylo.d.ts +65 -0
- package/src/grammar/phylo.test.ts +59 -0
- package/src/grammar/phylo.ts +112 -0
- package/src/grammar/pipeline.auto-ticks.test.ts +40 -0
- package/src/grammar/pipeline.d.ts +158 -0
- package/src/grammar/pipeline.test.ts +463 -0
- package/src/grammar/pipeline.ts +1233 -0
- package/src/grammar/profiling.d.ts +8 -0
- package/src/grammar/profiling.ts +24 -0
- package/src/grammar/scales.d.ts +188 -0
- package/src/grammar/scales.test.ts +181 -0
- package/src/grammar/scales.ts +800 -0
- package/src/grammar/svg.d.ts +3 -0
- package/src/grammar/svg.ts +39 -0
- package/src/grammar/theme.d.ts +261 -0
- package/src/grammar/theme.test.ts +105 -0
- package/src/grammar/theme.ts +490 -0
- package/src/heatmap/cpu.ts +109 -0
- package/src/heatmap/gpu.ts +565 -0
- package/src/heatmap/types.ts +177 -0
- package/src/heatmap.browser.test.ts +308 -0
- package/src/heatmap.test.ts +320 -0
- package/src/heatmap.ts +123 -0
- package/src/index.d.ts +1 -0
- package/src/index.ts +8 -0
- package/src/interactions.d.ts +48 -0
- package/src/interactions.test.ts +226 -0
- package/src/interactions.ts +394 -0
- package/src/layout/box.d.ts +48 -0
- package/src/layout/box.test.ts +107 -0
- package/src/layout/box.ts +143 -0
- package/src/legend.d.ts +115 -0
- package/src/legend.ts +422 -0
- package/src/marks/curve.d.ts +43 -0
- package/src/marks/curve.ts +244 -0
- package/src/marks/stack.d.ts +53 -0
- package/src/marks/stack.ts +184 -0
- package/src/marks.d.ts +273 -0
- package/src/marks.test.ts +541 -0
- package/src/marks.ts +1292 -0
- package/src/navigator.test.ts +174 -0
- package/src/navigator.ts +393 -0
- package/src/range-presets.d.ts +113 -0
- package/src/range-presets.test.ts +345 -0
- package/src/range-presets.ts +349 -0
- package/src/scales.d.ts +98 -0
- package/src/scales.test.ts +103 -0
- package/src/scales.ts +695 -0
- package/src/stats/index.d.ts +200 -0
- package/src/stats/index.test.ts +349 -0
- package/src/stats/index.ts +740 -0
- package/src/stats/regression.d.ts +38 -0
- package/src/stats/regression.test.ts +56 -0
- package/src/stats/regression.ts +396 -0
- package/src/stats/rolling-window.d.ts +55 -0
- package/src/stats/rolling-window.test.ts +237 -0
- package/src/stats/rolling-window.ts +256 -0
- package/src/test-setup.ts +19 -0
- package/src/viewport/axis-state.d.ts +72 -0
- package/src/viewport/axis-state.ts +476 -0
- package/src/viewport.d.ts +170 -0
- package/src/viewport.test.ts +363 -0
- package/src/viewport.ts +510 -0
package/src/scales.ts
ADDED
|
@@ -0,0 +1,695 @@
|
|
|
1
|
+
export type NumericRange = readonly [number, number];
|
|
2
|
+
export type NumericDomain = readonly [number, number];
|
|
3
|
+
export type DateDomain = readonly [Date, Date];
|
|
4
|
+
export type TickFormatter<T> = (value: T) => string;
|
|
5
|
+
|
|
6
|
+
export interface ContinuousScale {
|
|
7
|
+
(value: number): number;
|
|
8
|
+
readonly domain: NumericDomain;
|
|
9
|
+
readonly range: NumericRange;
|
|
10
|
+
invert(value: number): number;
|
|
11
|
+
ticks(count?: number): readonly number[];
|
|
12
|
+
tickFormat(count?: number): TickFormatter<number>;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface TimeScale {
|
|
16
|
+
(value: Date): number;
|
|
17
|
+
readonly domain: DateDomain;
|
|
18
|
+
readonly range: NumericRange;
|
|
19
|
+
invert(value: number): Date;
|
|
20
|
+
ticks(count?: number): readonly Date[];
|
|
21
|
+
tickFormat(count?: number): TickFormatter<Date>;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface BandScale<Domain> {
|
|
25
|
+
(value: Domain): number;
|
|
26
|
+
readonly domain: readonly Domain[];
|
|
27
|
+
readonly range: NumericRange;
|
|
28
|
+
bandwidth(): number;
|
|
29
|
+
step(): number;
|
|
30
|
+
ticks(): readonly Domain[];
|
|
31
|
+
tickFormat(): TickFormatter<Domain>;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export type AxisScale<Value> = ContinuousScale | TimeScale | BandScale<Value>;
|
|
35
|
+
|
|
36
|
+
export interface BandScaleOptions {
|
|
37
|
+
padding?: number;
|
|
38
|
+
paddingInner?: number;
|
|
39
|
+
paddingOuter?: number;
|
|
40
|
+
align?: number;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const DAY_MS = 24 * 60 * 60 * 1000;
|
|
44
|
+
const WEEK_MS = 7 * DAY_MS;
|
|
45
|
+
const MONTH_NAMES = [
|
|
46
|
+
"Jan",
|
|
47
|
+
"Feb",
|
|
48
|
+
"Mar",
|
|
49
|
+
"Apr",
|
|
50
|
+
"May",
|
|
51
|
+
"Jun",
|
|
52
|
+
"Jul",
|
|
53
|
+
"Aug",
|
|
54
|
+
"Sep",
|
|
55
|
+
"Oct",
|
|
56
|
+
"Nov",
|
|
57
|
+
"Dec",
|
|
58
|
+
];
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Time interval units accepted by {@link timeTicks} / {@link timeTickFormat}
|
|
62
|
+
* and by axis `ticks: { interval, step }` specs. `quarter` is sugar for
|
|
63
|
+
* `month` with step `3 × step`.
|
|
64
|
+
*/
|
|
65
|
+
export type TimeIntervalUnit =
|
|
66
|
+
| "millisecond"
|
|
67
|
+
| "second"
|
|
68
|
+
| "minute"
|
|
69
|
+
| "hour"
|
|
70
|
+
| "day"
|
|
71
|
+
| "week"
|
|
72
|
+
| "month"
|
|
73
|
+
| "quarter"
|
|
74
|
+
| "year";
|
|
75
|
+
|
|
76
|
+
type InternalTimeUnit = Exclude<TimeIntervalUnit, "quarter">;
|
|
77
|
+
|
|
78
|
+
interface TimeIntervalDef {
|
|
79
|
+
readonly unit: InternalTimeUnit;
|
|
80
|
+
readonly step: number;
|
|
81
|
+
readonly approxMs: number;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const APPROX_MS_BY_UNIT: Record<InternalTimeUnit, number> = {
|
|
85
|
+
millisecond: 1,
|
|
86
|
+
second: 1000,
|
|
87
|
+
minute: 60_000,
|
|
88
|
+
hour: 3_600_000,
|
|
89
|
+
day: DAY_MS,
|
|
90
|
+
week: WEEK_MS,
|
|
91
|
+
month: 30 * DAY_MS,
|
|
92
|
+
year: 365 * DAY_MS,
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
function resolveInterval(unit: TimeIntervalUnit, step = 1): TimeIntervalDef {
|
|
96
|
+
const cleanStep = Math.max(1, Math.floor(step));
|
|
97
|
+
if (unit === "quarter") {
|
|
98
|
+
return {
|
|
99
|
+
unit: "month",
|
|
100
|
+
step: 3 * cleanStep,
|
|
101
|
+
approxMs: 3 * cleanStep * APPROX_MS_BY_UNIT.month,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
return { unit, step: cleanStep, approxMs: cleanStep * APPROX_MS_BY_UNIT[unit] };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const TIME_INTERVALS: readonly TimeIntervalDef[] = [
|
|
108
|
+
{ unit: "millisecond", step: 1, approxMs: 1 },
|
|
109
|
+
{ unit: "millisecond", step: 5, approxMs: 5 },
|
|
110
|
+
{ unit: "millisecond", step: 10, approxMs: 10 },
|
|
111
|
+
{ unit: "millisecond", step: 50, approxMs: 50 },
|
|
112
|
+
{ unit: "millisecond", step: 100, approxMs: 100 },
|
|
113
|
+
{ unit: "millisecond", step: 250, approxMs: 250 },
|
|
114
|
+
{ unit: "millisecond", step: 500, approxMs: 500 },
|
|
115
|
+
{ unit: "second", step: 1, approxMs: 1000 },
|
|
116
|
+
{ unit: "second", step: 5, approxMs: 5000 },
|
|
117
|
+
{ unit: "second", step: 15, approxMs: 15_000 },
|
|
118
|
+
{ unit: "second", step: 30, approxMs: 30_000 },
|
|
119
|
+
{ unit: "minute", step: 1, approxMs: 60_000 },
|
|
120
|
+
{ unit: "minute", step: 5, approxMs: 300_000 },
|
|
121
|
+
{ unit: "minute", step: 15, approxMs: 900_000 },
|
|
122
|
+
{ unit: "minute", step: 30, approxMs: 1_800_000 },
|
|
123
|
+
{ unit: "hour", step: 1, approxMs: 3_600_000 },
|
|
124
|
+
{ unit: "hour", step: 3, approxMs: 10_800_000 },
|
|
125
|
+
{ unit: "hour", step: 6, approxMs: 21_600_000 },
|
|
126
|
+
{ unit: "hour", step: 12, approxMs: 43_200_000 },
|
|
127
|
+
{ unit: "day", step: 1, approxMs: DAY_MS },
|
|
128
|
+
{ unit: "day", step: 2, approxMs: 2 * DAY_MS },
|
|
129
|
+
{ unit: "week", step: 1, approxMs: WEEK_MS },
|
|
130
|
+
{ unit: "month", step: 1, approxMs: 30 * DAY_MS },
|
|
131
|
+
{ unit: "month", step: 3, approxMs: 91 * DAY_MS },
|
|
132
|
+
{ unit: "month", step: 6, approxMs: 182 * DAY_MS },
|
|
133
|
+
{ unit: "year", step: 1, approxMs: 365 * DAY_MS },
|
|
134
|
+
{ unit: "year", step: 2, approxMs: 2 * 365 * DAY_MS },
|
|
135
|
+
{ unit: "year", step: 5, approxMs: 5 * 365 * DAY_MS },
|
|
136
|
+
{ unit: "year", step: 10, approxMs: 10 * 365 * DAY_MS },
|
|
137
|
+
{ unit: "year", step: 25, approxMs: 25 * 365 * DAY_MS },
|
|
138
|
+
{ unit: "year", step: 50, approxMs: 50 * 365 * DAY_MS },
|
|
139
|
+
{ unit: "year", step: 100, approxMs: 100 * 365 * DAY_MS },
|
|
140
|
+
];
|
|
141
|
+
|
|
142
|
+
function validateNumberPair(name: string, pair: readonly [number, number]): NumericDomain {
|
|
143
|
+
if (!Number.isFinite(pair[0]) || !Number.isFinite(pair[1])) {
|
|
144
|
+
throw new Error(`${name} values must be finite numbers.`);
|
|
145
|
+
}
|
|
146
|
+
return [pair[0], pair[1]];
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function validateDatePair(domain: readonly [Date, Date]): DateDomain {
|
|
150
|
+
const start = domain[0].getTime();
|
|
151
|
+
const stop = domain[1].getTime();
|
|
152
|
+
if (!Number.isFinite(start) || !Number.isFinite(stop)) {
|
|
153
|
+
throw new Error("timeScale domain values must be valid Date instances.");
|
|
154
|
+
}
|
|
155
|
+
return [new Date(start), new Date(stop)];
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function normalize(value: number, start: number, stop: number): number {
|
|
159
|
+
if (!Number.isFinite(value)) return Number.NaN;
|
|
160
|
+
if (stop === start) return 0.5;
|
|
161
|
+
return (value - start) / (stop - start);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function interpolate(start: number, stop: number, t: number): number {
|
|
165
|
+
return start + (stop - start) * t;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export function clamp(value: number, min: number, max: number): number {
|
|
169
|
+
return Math.min(max, Math.max(min, value));
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function roundNumber(value: number, digits = 12): number {
|
|
173
|
+
return Number.parseFloat(value.toPrecision(digits));
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function stripTrailingZeros(value: string): string {
|
|
177
|
+
return value
|
|
178
|
+
.replace(/(\.\d*?[1-9])0+$/u, "$1")
|
|
179
|
+
.replace(/\.0+$/u, "")
|
|
180
|
+
.replace(/\.e/u, "e");
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function formatNumber(value: number, fractionDigits: number): string {
|
|
184
|
+
const normalized = Object.is(value, -0) ? 0 : value;
|
|
185
|
+
const abs = Math.abs(normalized);
|
|
186
|
+
if (abs >= 1e7 || (abs > 0 && abs < 1e-4)) {
|
|
187
|
+
return stripTrailingZeros(normalized.toExponential(2));
|
|
188
|
+
}
|
|
189
|
+
return stripTrailingZeros(normalized.toFixed(fractionDigits));
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function tickStep(start: number, stop: number, count: number): number {
|
|
193
|
+
const steps = Math.max(count, 1);
|
|
194
|
+
const span = Math.abs(stop - start);
|
|
195
|
+
if (span === 0) return 0;
|
|
196
|
+
|
|
197
|
+
const step0 = span / steps;
|
|
198
|
+
const power = Math.floor(Math.log10(step0));
|
|
199
|
+
const power10 = 10 ** power;
|
|
200
|
+
const error = step0 / power10;
|
|
201
|
+
|
|
202
|
+
let factor = 1;
|
|
203
|
+
if (error >= Math.sqrt(50)) {
|
|
204
|
+
factor = 10;
|
|
205
|
+
} else if (error >= Math.sqrt(10)) {
|
|
206
|
+
factor = 5;
|
|
207
|
+
} else if (error >= Math.sqrt(2)) {
|
|
208
|
+
factor = 2;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return factor * power10;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
export function numericTicks(start: number, stop: number, count = 5): readonly number[] {
|
|
215
|
+
if (count <= 0) return [];
|
|
216
|
+
if (start === stop) return [start];
|
|
217
|
+
|
|
218
|
+
const reverse = stop < start;
|
|
219
|
+
const min = reverse ? stop : start;
|
|
220
|
+
const max = reverse ? start : stop;
|
|
221
|
+
const step = tickStep(min, max, count);
|
|
222
|
+
if (!Number.isFinite(step) || step === 0) return [start];
|
|
223
|
+
|
|
224
|
+
const epsilon = step * 1e-9;
|
|
225
|
+
const first = Math.ceil((min - epsilon) / step);
|
|
226
|
+
const last = Math.floor((max + epsilon) / step);
|
|
227
|
+
const ticks: number[] = [];
|
|
228
|
+
|
|
229
|
+
for (let index = first; index <= last; index++) {
|
|
230
|
+
ticks.push(roundNumber(index * step));
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
return reverse ? ticks.reverse() : ticks;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function inferFractionDigits(step: number): number {
|
|
237
|
+
if (!Number.isFinite(step) || step === 0) return 0;
|
|
238
|
+
return Math.max(0, Math.ceil(-Math.log10(Math.abs(step))));
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function inferTickStep(values: readonly number[]): number {
|
|
242
|
+
if (values.length < 2) return 0;
|
|
243
|
+
let step = Number.POSITIVE_INFINITY;
|
|
244
|
+
for (let index = 1; index < values.length; index++) {
|
|
245
|
+
const delta = Math.abs(values[index] - values[index - 1]);
|
|
246
|
+
if (delta > 0) {
|
|
247
|
+
step = Math.min(step, delta);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
return Number.isFinite(step) ? step : 0;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function numericTickFormatter(values: readonly number[]): TickFormatter<number> {
|
|
254
|
+
const digits = inferFractionDigits(inferTickStep(values));
|
|
255
|
+
return (value: number) => formatNumber(value, digits);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/** Sign-preserving sqrt: f(-x) = -sqrt(x). Used by `sqrtScale` and viewport. */
|
|
259
|
+
export function signedSqrt(value: number): number {
|
|
260
|
+
return Math.sign(value) * Math.sqrt(Math.abs(value));
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/** Inverse of `signedSqrt`. */
|
|
264
|
+
export function signedSquare(value: number): number {
|
|
265
|
+
return Math.sign(value) * value * value;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function createContinuousScale(
|
|
269
|
+
domain: NumericDomain,
|
|
270
|
+
range: NumericRange,
|
|
271
|
+
transform: (value: number) => number,
|
|
272
|
+
untransform: (value: number) => number,
|
|
273
|
+
ticks: (count?: number) => readonly number[],
|
|
274
|
+
): ContinuousScale {
|
|
275
|
+
const [d0, d1] = domain;
|
|
276
|
+
const [r0, r1] = range;
|
|
277
|
+
const td0 = transform(d0);
|
|
278
|
+
const td1 = transform(d1);
|
|
279
|
+
|
|
280
|
+
const scale = ((value: number) => {
|
|
281
|
+
const t = normalize(transform(value), td0, td1);
|
|
282
|
+
return interpolate(r0, r1, t);
|
|
283
|
+
}) as ContinuousScale;
|
|
284
|
+
|
|
285
|
+
Object.defineProperties(scale, {
|
|
286
|
+
domain: { value: [d0, d1] as const, enumerable: true },
|
|
287
|
+
range: { value: [r0, r1] as const, enumerable: true },
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
scale.invert = (value: number) => {
|
|
291
|
+
const t = normalize(value, r0, r1);
|
|
292
|
+
return untransform(interpolate(td0, td1, t));
|
|
293
|
+
};
|
|
294
|
+
scale.ticks = (count = 5) => ticks(count);
|
|
295
|
+
scale.tickFormat = (count = 5) => numericTickFormatter(scale.ticks(count));
|
|
296
|
+
|
|
297
|
+
return scale;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
export function linearScale(domain: NumericDomain, range: NumericRange): ContinuousScale {
|
|
301
|
+
return createContinuousScale(
|
|
302
|
+
validateNumberPair("linearScale domain", domain),
|
|
303
|
+
validateNumberPair("linearScale range", range),
|
|
304
|
+
(value) => value,
|
|
305
|
+
(value) => value,
|
|
306
|
+
(count) => numericTicks(domain[0], domain[1], count),
|
|
307
|
+
);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
export function sqrtScale(domain: NumericDomain, range: NumericRange): ContinuousScale {
|
|
311
|
+
const validatedDomain = validateNumberPair("sqrtScale domain", domain);
|
|
312
|
+
return createContinuousScale(
|
|
313
|
+
validatedDomain,
|
|
314
|
+
validateNumberPair("sqrtScale range", range),
|
|
315
|
+
signedSqrt,
|
|
316
|
+
signedSquare,
|
|
317
|
+
(count) => numericTicks(validatedDomain[0], validatedDomain[1], count),
|
|
318
|
+
);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/** Sign-preserving log. Used by `logScale` and viewport transforms. */
|
|
322
|
+
export function logTransform(value: number): number {
|
|
323
|
+
return Math.sign(value) * Math.log(Math.abs(value));
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/** Inverse of `logTransform`. */
|
|
327
|
+
export function logUntransform(value: number): number {
|
|
328
|
+
return Math.sign(value) * Math.exp(Math.abs(value));
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function logTicks(start: number, stop: number, count = 5): readonly number[] {
|
|
332
|
+
if (count <= 0) return [];
|
|
333
|
+
if (start === stop) return [start];
|
|
334
|
+
|
|
335
|
+
const reverse = stop < start;
|
|
336
|
+
const sign = start < 0 ? -1 : 1;
|
|
337
|
+
const min = Math.min(Math.abs(start), Math.abs(stop));
|
|
338
|
+
const max = Math.max(Math.abs(start), Math.abs(stop));
|
|
339
|
+
const useSubdivisions = Math.abs(Math.log10(max) - Math.log10(min)) <= count;
|
|
340
|
+
const multipliers = useSubdivisions ? [1, 2, 5] : [1];
|
|
341
|
+
const minExp = Math.floor(Math.log10(min));
|
|
342
|
+
const maxExp = Math.ceil(Math.log10(max));
|
|
343
|
+
const ticks: number[] = [];
|
|
344
|
+
|
|
345
|
+
for (let exponent = minExp; exponent <= maxExp; exponent++) {
|
|
346
|
+
const base = 10 ** exponent;
|
|
347
|
+
for (const multiplier of multipliers) {
|
|
348
|
+
const value = sign * multiplier * base;
|
|
349
|
+
const magnitude = Math.abs(value);
|
|
350
|
+
if (magnitude >= min * 0.999999999 && magnitude <= max * 1.000000001) {
|
|
351
|
+
ticks.push(value);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
if (ticks.length === 0) {
|
|
357
|
+
return numericTicks(start, stop, count);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
const deduped = ticks
|
|
361
|
+
.sort((a, b) => a - b)
|
|
362
|
+
.filter((value, index, values) => index === 0 || Math.abs(value - values[index - 1]) > 1e-9);
|
|
363
|
+
|
|
364
|
+
return reverse ? deduped.reverse() : deduped;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
export function logScale(domain: NumericDomain, range: NumericRange): ContinuousScale {
|
|
368
|
+
const validatedDomain = validateNumberPair("logScale domain", domain);
|
|
369
|
+
const [d0, d1] = validatedDomain;
|
|
370
|
+
if (d0 === 0 || d1 === 0) {
|
|
371
|
+
throw new Error("logScale domain values must be non-zero.");
|
|
372
|
+
}
|
|
373
|
+
if (Math.sign(d0) !== Math.sign(d1)) {
|
|
374
|
+
throw new Error("logScale domain values must share the same sign.");
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
return createContinuousScale(
|
|
378
|
+
validatedDomain,
|
|
379
|
+
validateNumberPair("logScale range", range),
|
|
380
|
+
logTransform,
|
|
381
|
+
logUntransform,
|
|
382
|
+
(count) => logTicks(validatedDomain[0], validatedDomain[1], count),
|
|
383
|
+
);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function chooseTimeInterval(start: Date, stop: Date, count = 5): TimeIntervalDef {
|
|
387
|
+
const target = Math.abs(stop.getTime() - start.getTime()) / Math.max(count, 1);
|
|
388
|
+
for (const interval of TIME_INTERVALS) {
|
|
389
|
+
if (target <= interval.approxMs) return interval;
|
|
390
|
+
}
|
|
391
|
+
return TIME_INTERVALS[TIME_INTERVALS.length - 1]!;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
function floorUtc(date: Date, interval: TimeIntervalDef): Date {
|
|
395
|
+
const value = new Date(date.getTime());
|
|
396
|
+
switch (interval.unit) {
|
|
397
|
+
case "millisecond":
|
|
398
|
+
value.setTime(Math.floor(value.getTime() / interval.step) * interval.step);
|
|
399
|
+
return value;
|
|
400
|
+
case "second":
|
|
401
|
+
value.setUTCMilliseconds(0);
|
|
402
|
+
value.setUTCSeconds(Math.floor(value.getUTCSeconds() / interval.step) * interval.step);
|
|
403
|
+
return value;
|
|
404
|
+
case "minute":
|
|
405
|
+
value.setUTCMilliseconds(0);
|
|
406
|
+
value.setUTCSeconds(0);
|
|
407
|
+
value.setUTCMinutes(Math.floor(value.getUTCMinutes() / interval.step) * interval.step);
|
|
408
|
+
return value;
|
|
409
|
+
case "hour":
|
|
410
|
+
value.setUTCMilliseconds(0);
|
|
411
|
+
value.setUTCSeconds(0);
|
|
412
|
+
value.setUTCMinutes(0);
|
|
413
|
+
value.setUTCHours(Math.floor(value.getUTCHours() / interval.step) * interval.step);
|
|
414
|
+
return value;
|
|
415
|
+
case "day":
|
|
416
|
+
value.setUTCHours(0, 0, 0, 0);
|
|
417
|
+
value.setTime(
|
|
418
|
+
Math.floor(value.getTime() / (interval.step * DAY_MS)) * interval.step * DAY_MS,
|
|
419
|
+
);
|
|
420
|
+
return value;
|
|
421
|
+
case "week":
|
|
422
|
+
value.setUTCHours(0, 0, 0, 0);
|
|
423
|
+
value.setUTCDate(value.getUTCDate() - ((value.getUTCDay() + 6) % 7));
|
|
424
|
+
value.setTime(
|
|
425
|
+
Math.floor(value.getTime() / (interval.step * WEEK_MS)) * interval.step * WEEK_MS,
|
|
426
|
+
);
|
|
427
|
+
return value;
|
|
428
|
+
case "month":
|
|
429
|
+
value.setUTCHours(0, 0, 0, 0);
|
|
430
|
+
value.setUTCDate(1);
|
|
431
|
+
value.setUTCMonth(Math.floor(value.getUTCMonth() / interval.step) * interval.step);
|
|
432
|
+
return value;
|
|
433
|
+
case "year":
|
|
434
|
+
value.setUTCHours(0, 0, 0, 0);
|
|
435
|
+
value.setUTCDate(1);
|
|
436
|
+
value.setUTCMonth(0);
|
|
437
|
+
value.setUTCFullYear(Math.floor(value.getUTCFullYear() / interval.step) * interval.step);
|
|
438
|
+
return value;
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
function offsetUtc(date: Date, interval: TimeIntervalDef): Date {
|
|
443
|
+
const value = new Date(date.getTime());
|
|
444
|
+
switch (interval.unit) {
|
|
445
|
+
case "millisecond":
|
|
446
|
+
value.setTime(value.getTime() + interval.step);
|
|
447
|
+
return value;
|
|
448
|
+
case "second":
|
|
449
|
+
value.setUTCSeconds(value.getUTCSeconds() + interval.step);
|
|
450
|
+
return value;
|
|
451
|
+
case "minute":
|
|
452
|
+
value.setUTCMinutes(value.getUTCMinutes() + interval.step);
|
|
453
|
+
return value;
|
|
454
|
+
case "hour":
|
|
455
|
+
value.setUTCHours(value.getUTCHours() + interval.step);
|
|
456
|
+
return value;
|
|
457
|
+
case "day":
|
|
458
|
+
value.setUTCDate(value.getUTCDate() + interval.step);
|
|
459
|
+
return value;
|
|
460
|
+
case "week":
|
|
461
|
+
value.setUTCDate(value.getUTCDate() + interval.step * 7);
|
|
462
|
+
return value;
|
|
463
|
+
case "month":
|
|
464
|
+
value.setUTCMonth(value.getUTCMonth() + interval.step);
|
|
465
|
+
return value;
|
|
466
|
+
case "year":
|
|
467
|
+
value.setUTCFullYear(value.getUTCFullYear() + interval.step);
|
|
468
|
+
return value;
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
function formatUtcDate(date: Date, interval: TimeIntervalDef): string {
|
|
473
|
+
const month = MONTH_NAMES[date.getUTCMonth()] ?? "";
|
|
474
|
+
const day = date.getUTCDate();
|
|
475
|
+
const year = date.getUTCFullYear();
|
|
476
|
+
const hours = String(date.getUTCHours()).padStart(2, "0");
|
|
477
|
+
const minutes = String(date.getUTCMinutes()).padStart(2, "0");
|
|
478
|
+
const seconds = String(date.getUTCSeconds()).padStart(2, "0");
|
|
479
|
+
const millis = String(date.getUTCMilliseconds()).padStart(3, "0");
|
|
480
|
+
|
|
481
|
+
switch (interval.unit) {
|
|
482
|
+
case "year":
|
|
483
|
+
return String(year);
|
|
484
|
+
case "month":
|
|
485
|
+
return `${month} ${year}`;
|
|
486
|
+
case "week":
|
|
487
|
+
case "day":
|
|
488
|
+
return `${month} ${day}`;
|
|
489
|
+
case "hour":
|
|
490
|
+
return `${hours}:00`;
|
|
491
|
+
case "minute":
|
|
492
|
+
return `${hours}:${minutes}`;
|
|
493
|
+
case "second":
|
|
494
|
+
return `${hours}:${minutes}:${seconds}`;
|
|
495
|
+
case "millisecond":
|
|
496
|
+
return `${hours}:${minutes}:${seconds}.${millis}`;
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
export function timeScale(domain: DateDomain, range: NumericRange): TimeScale {
|
|
501
|
+
const validatedDomain = validateDatePair(domain);
|
|
502
|
+
const [start, stop] = validatedDomain;
|
|
503
|
+
const [r0, r1] = validateNumberPair("timeScale range", range);
|
|
504
|
+
const startMs = start.getTime();
|
|
505
|
+
const stopMs = stop.getTime();
|
|
506
|
+
|
|
507
|
+
const scale = ((value: Date) => {
|
|
508
|
+
const t = normalize(value.getTime(), startMs, stopMs);
|
|
509
|
+
return interpolate(r0, r1, t);
|
|
510
|
+
}) as TimeScale;
|
|
511
|
+
|
|
512
|
+
Object.defineProperties(scale, {
|
|
513
|
+
domain: { value: [new Date(startMs), new Date(stopMs)] as const, enumerable: true },
|
|
514
|
+
range: { value: [r0, r1] as const, enumerable: true },
|
|
515
|
+
});
|
|
516
|
+
|
|
517
|
+
scale.invert = (value: number) => {
|
|
518
|
+
const t = normalize(value, r0, r1);
|
|
519
|
+
return new Date(interpolate(startMs, stopMs, t));
|
|
520
|
+
};
|
|
521
|
+
|
|
522
|
+
scale.ticks = (count = 5) => {
|
|
523
|
+
if (count <= 0) return [];
|
|
524
|
+
if (startMs === stopMs) return [new Date(startMs)];
|
|
525
|
+
|
|
526
|
+
const reverse = stopMs < startMs;
|
|
527
|
+
const lo = reverse ? new Date(stopMs) : new Date(startMs);
|
|
528
|
+
const hi = reverse ? new Date(startMs) : new Date(stopMs);
|
|
529
|
+
const interval = chooseTimeInterval(lo, hi, count);
|
|
530
|
+
return generateTimeTicks(lo, hi, interval, reverse);
|
|
531
|
+
};
|
|
532
|
+
|
|
533
|
+
scale.tickFormat = (count = 5) => {
|
|
534
|
+
const interval = chooseTimeInterval(start, stop, count);
|
|
535
|
+
return (value: Date) => formatUtcDate(value, interval);
|
|
536
|
+
};
|
|
537
|
+
|
|
538
|
+
return scale;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
function generateTimeTicks(
|
|
542
|
+
lo: Date,
|
|
543
|
+
hi: Date,
|
|
544
|
+
interval: TimeIntervalDef,
|
|
545
|
+
reverse: boolean,
|
|
546
|
+
): readonly Date[] {
|
|
547
|
+
let cursor = floorUtc(lo, interval);
|
|
548
|
+
if (cursor.getTime() < lo.getTime()) {
|
|
549
|
+
cursor = offsetUtc(cursor, interval);
|
|
550
|
+
}
|
|
551
|
+
const ticks: Date[] = [];
|
|
552
|
+
while (cursor.getTime() <= hi.getTime() && ticks.length < 10_000) {
|
|
553
|
+
ticks.push(new Date(cursor.getTime()));
|
|
554
|
+
cursor = offsetUtc(cursor, interval);
|
|
555
|
+
}
|
|
556
|
+
return reverse ? ticks.reverse() : ticks;
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
/**
|
|
560
|
+
* Generate UTC tick dates at an explicit interval (e.g. every month, every
|
|
561
|
+
* 3 weeks, every quarter). Use this when the default `timeScale.ticks(count)`
|
|
562
|
+
* heuristic picks the wrong granularity for a known cadence. `step` defaults
|
|
563
|
+
* to `1`. `quarter` is sugar for `month, step: 3`.
|
|
564
|
+
*/
|
|
565
|
+
export function timeTicks(domain: DateDomain, unit: TimeIntervalUnit, step = 1): readonly Date[] {
|
|
566
|
+
const validated = validateDatePair(domain);
|
|
567
|
+
const [start, stop] = validated;
|
|
568
|
+
const startMs = start.getTime();
|
|
569
|
+
const stopMs = stop.getTime();
|
|
570
|
+
if (startMs === stopMs) return [new Date(startMs)];
|
|
571
|
+
const reverse = stopMs < startMs;
|
|
572
|
+
const lo = reverse ? stop : start;
|
|
573
|
+
const hi = reverse ? start : stop;
|
|
574
|
+
return generateTimeTicks(lo, hi, resolveInterval(unit, step), reverse);
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
/**
|
|
578
|
+
* Tick formatter paired with {@link timeTicks}. The label shape is picked
|
|
579
|
+
* from the unit: years render `"2026"`, months `"Mar 2026"`, days `"Mar 14"`,
|
|
580
|
+
* etc. — matching the formatter used by {@link timeScale}.
|
|
581
|
+
*/
|
|
582
|
+
export function timeTickFormat(unit: TimeIntervalUnit, step = 1): TickFormatter<Date> {
|
|
583
|
+
const interval = resolveInterval(unit, step);
|
|
584
|
+
return (value: Date) => formatUtcDate(value, interval);
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
export function bandScale<Domain>(
|
|
588
|
+
domain: readonly Domain[],
|
|
589
|
+
range: NumericRange,
|
|
590
|
+
options: BandScaleOptions = {},
|
|
591
|
+
): BandScale<Domain> {
|
|
592
|
+
const validatedRange = validateNumberPair("bandScale range", range);
|
|
593
|
+
const [r0, r1] = validatedRange;
|
|
594
|
+
const reverse = r1 < r0;
|
|
595
|
+
const start = reverse ? r1 : r0;
|
|
596
|
+
const stop = reverse ? r0 : r1;
|
|
597
|
+
const paddingInner = clamp(options.paddingInner ?? options.padding ?? 0, 0, 1);
|
|
598
|
+
const paddingOuter = Math.max(options.paddingOuter ?? options.padding ?? 0, 0);
|
|
599
|
+
const align = clamp(options.align ?? 0.5, 0, 1);
|
|
600
|
+
const count = domain.length;
|
|
601
|
+
const denominator = Math.max(1, count - paddingInner + paddingOuter * 2);
|
|
602
|
+
const stepValue = count === 0 ? 0 : (stop - start) / denominator;
|
|
603
|
+
const bandwidthValue = stepValue * (1 - paddingInner);
|
|
604
|
+
const offset = (stop - start - stepValue * (count - paddingInner)) * align;
|
|
605
|
+
const first = start + offset + stepValue * paddingOuter;
|
|
606
|
+
const index = new Map<Domain, number>();
|
|
607
|
+
|
|
608
|
+
for (let position = 0; position < domain.length; position++) {
|
|
609
|
+
const value = domain[position]!;
|
|
610
|
+
if (index.has(value)) {
|
|
611
|
+
throw new Error("bandScale domain values must be unique.");
|
|
612
|
+
}
|
|
613
|
+
index.set(value, position);
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
const scale = ((value: Domain) => {
|
|
617
|
+
const position = index.get(value);
|
|
618
|
+
if (position === undefined) return Number.NaN;
|
|
619
|
+
|
|
620
|
+
const x = first + stepValue * position;
|
|
621
|
+
if (!reverse) return x;
|
|
622
|
+
return r0 - (x - start) - bandwidthValue;
|
|
623
|
+
}) as BandScale<Domain>;
|
|
624
|
+
|
|
625
|
+
Object.defineProperties(scale, {
|
|
626
|
+
domain: { value: [...domain], enumerable: true },
|
|
627
|
+
range: { value: [r0, r1] as const, enumerable: true },
|
|
628
|
+
});
|
|
629
|
+
|
|
630
|
+
scale.bandwidth = () => bandwidthValue;
|
|
631
|
+
scale.step = () => stepValue;
|
|
632
|
+
scale.ticks = () => [...domain];
|
|
633
|
+
scale.tickFormat = () => (value: Domain) => String(value);
|
|
634
|
+
|
|
635
|
+
return scale;
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
// ---------------------------------------------------------------------------
|
|
639
|
+
// groupedBandScale — nested band layout for grouped bars
|
|
640
|
+
// ---------------------------------------------------------------------------
|
|
641
|
+
|
|
642
|
+
export interface GroupedBandScale<Outer, Inner> {
|
|
643
|
+
/** Position (start) of the inner band within `outer`. */
|
|
644
|
+
(outer: Outer, inner: Inner): number;
|
|
645
|
+
/** Inner bandwidth — the rendered width of one inner bar. */
|
|
646
|
+
bandwidth(): number;
|
|
647
|
+
/** Inner step — distance between adjacent inner band starts. */
|
|
648
|
+
step(): number;
|
|
649
|
+
/** Outer band scale this is nested inside. */
|
|
650
|
+
readonly outer: BandScale<Outer>;
|
|
651
|
+
/** Ordered keys of the inner band scale. */
|
|
652
|
+
readonly innerKeys: readonly Inner[];
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
export interface GroupedBandScaleOptions {
|
|
656
|
+
padding?: number;
|
|
657
|
+
paddingInner?: number;
|
|
658
|
+
paddingOuter?: number;
|
|
659
|
+
align?: number;
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
/**
|
|
663
|
+
* Build a nested band scale: each band of the `outer` scale is subdivided
|
|
664
|
+
* into one inner band per key. Returns a function `(outerKey, innerKey) →
|
|
665
|
+
* position` plus inner `bandwidth()` and `step()`.
|
|
666
|
+
*
|
|
667
|
+
* Use this for grouped bars / dot plots where each category has multiple
|
|
668
|
+
* series side by side. Outer scale handles category placement and
|
|
669
|
+
* spacing; inner band controls intra-group layout.
|
|
670
|
+
*/
|
|
671
|
+
export function groupedBandScale<Outer, Inner>(
|
|
672
|
+
outer: BandScale<Outer>,
|
|
673
|
+
innerKeys: readonly Inner[],
|
|
674
|
+
options: GroupedBandScaleOptions = {},
|
|
675
|
+
): GroupedBandScale<Outer, Inner> {
|
|
676
|
+
const inner = bandScale(innerKeys, [0, outer.bandwidth()], options);
|
|
677
|
+
|
|
678
|
+
const grouped = ((outerValue: Outer, innerValue: Inner) => {
|
|
679
|
+
const outerStart = outer(outerValue);
|
|
680
|
+
if (Number.isNaN(outerStart)) return Number.NaN;
|
|
681
|
+
const innerPos = inner(innerValue);
|
|
682
|
+
if (Number.isNaN(innerPos)) return Number.NaN;
|
|
683
|
+
return outerStart + innerPos;
|
|
684
|
+
}) as GroupedBandScale<Outer, Inner>;
|
|
685
|
+
|
|
686
|
+
Object.defineProperties(grouped, {
|
|
687
|
+
outer: { value: outer, enumerable: true },
|
|
688
|
+
innerKeys: { value: [...innerKeys], enumerable: true },
|
|
689
|
+
});
|
|
690
|
+
|
|
691
|
+
grouped.bandwidth = () => inner.bandwidth();
|
|
692
|
+
grouped.step = () => inner.step();
|
|
693
|
+
|
|
694
|
+
return grouped;
|
|
695
|
+
}
|