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
|
@@ -0,0 +1,800 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// Auto-scale inference
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
// Given resolved aesthetics + raw data + a channel role, return a scale that
|
|
5
|
+
// the geom can call as `scale(value) -> pixel/visual`. Domain is computed
|
|
6
|
+
// from the data (or from explicit override). Type is picked from data type.
|
|
7
|
+
|
|
8
|
+
import type { Color } from "insomni";
|
|
9
|
+
import {
|
|
10
|
+
bandScale,
|
|
11
|
+
linearScale,
|
|
12
|
+
logScale,
|
|
13
|
+
numericTicks,
|
|
14
|
+
sqrtScale,
|
|
15
|
+
timeScale,
|
|
16
|
+
type BandScale,
|
|
17
|
+
type ContinuousScale,
|
|
18
|
+
type NumericDomain,
|
|
19
|
+
type NumericRange,
|
|
20
|
+
type TimeScale,
|
|
21
|
+
} from "../scales.ts";
|
|
22
|
+
import { colorScale, type CategoricalPalette, type ContinuousPalette } from "../colors.ts";
|
|
23
|
+
import { quantile } from "../stats/index.ts";
|
|
24
|
+
import { POINT_SHAPE_PALETTE, type PointBorderStyle, type PointShapeKind } from "../marks.ts";
|
|
25
|
+
import { inferDataType, materialize, type ChannelDataType, type ResolvedAes } from "./aes.ts";
|
|
26
|
+
import type { Theme } from "./theme.ts";
|
|
27
|
+
|
|
28
|
+
// Re-exported for convenience: `ScaleBundle` now lives in `./geoms/types.ts`
|
|
29
|
+
// but historically was importable from this module (and many tests still do).
|
|
30
|
+
export type { ScaleBundle } from "./geoms/types.ts";
|
|
31
|
+
|
|
32
|
+
export type Channel =
|
|
33
|
+
| "x"
|
|
34
|
+
| "y"
|
|
35
|
+
| "color"
|
|
36
|
+
| "size"
|
|
37
|
+
| "shape"
|
|
38
|
+
| "alpha"
|
|
39
|
+
| "borderStyle"
|
|
40
|
+
| "overlayGlyph";
|
|
41
|
+
|
|
42
|
+
export type PositionScaleType = "linear" | "log" | "sqrt" | "time" | "band";
|
|
43
|
+
export type ColorScaleType = "categorical" | "continuous" | "diverging";
|
|
44
|
+
|
|
45
|
+
interface BasePositionScaleOptions {
|
|
46
|
+
range?: NumericRange;
|
|
47
|
+
nice?: boolean;
|
|
48
|
+
padding?: number;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* `"nice"` is sugar for the existing `nice: true` flag — set as the domain
|
|
53
|
+
* shortcut so consumers don't need a second toggle when the only reason to
|
|
54
|
+
* touch `domain` was to pad it to round values. The data extent still
|
|
55
|
+
* derives the underlying numbers; `"nice"` just toggles the rounding.
|
|
56
|
+
*/
|
|
57
|
+
export type NumericDomainShortcut = "nice";
|
|
58
|
+
|
|
59
|
+
export interface NumericPositionScaleOptions extends BasePositionScaleOptions {
|
|
60
|
+
type?: "linear" | "log" | "sqrt";
|
|
61
|
+
domain?: readonly [number, number] | NumericDomainShortcut;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface TimePositionScaleOptions extends BasePositionScaleOptions {
|
|
65
|
+
type?: "time";
|
|
66
|
+
domain?: readonly [Date, Date];
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export interface BandPositionScaleOptions extends BasePositionScaleOptions {
|
|
70
|
+
type?: "band";
|
|
71
|
+
domain?: readonly string[];
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export type PositionScaleOptions =
|
|
75
|
+
| NumericPositionScaleOptions
|
|
76
|
+
| TimePositionScaleOptions
|
|
77
|
+
| BandPositionScaleOptions;
|
|
78
|
+
|
|
79
|
+
export interface CategoricalColorScaleOptions<T> {
|
|
80
|
+
type?: "categorical";
|
|
81
|
+
domain?: readonly T[];
|
|
82
|
+
palette?: CategoricalPalette;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Color-scale domain shortcuts.
|
|
87
|
+
*
|
|
88
|
+
* - `"nice"`: alias for the existing `nice: true` flag on continuous color
|
|
89
|
+
* scales — pad the data extent to round values.
|
|
90
|
+
* - `"quantile"`: bucket the data into N quantile bins (default 5). The
|
|
91
|
+
* returned scale's `type` becomes `"categorical"` with bucket-label
|
|
92
|
+
* domain entries (`"Q1".."QN"`), and the palette is sampled at N evenly
|
|
93
|
+
* spaced stops along the chart's continuous palette so the color
|
|
94
|
+
* gradient reads as ordinal magnitude rather than nominal category.
|
|
95
|
+
*/
|
|
96
|
+
export type ColorDomainShortcut = "nice" | "quantile";
|
|
97
|
+
|
|
98
|
+
export interface ContinuousColorScaleOptions {
|
|
99
|
+
type: "continuous" | "diverging";
|
|
100
|
+
domain?: readonly [number, number] | ColorDomainShortcut;
|
|
101
|
+
palette?: ContinuousPalette;
|
|
102
|
+
/**
|
|
103
|
+
* Extend the inferred domain outward to "nice" round values (e.g. data
|
|
104
|
+
* extent `[-2.8, 1.9]` → `[-3, 2]`). Same algorithm position scales use
|
|
105
|
+
* via `nice: true`. Ignored when `domain` is set explicitly to a tuple.
|
|
106
|
+
* Equivalent to `domain: "nice"` — keep one form per chart for clarity.
|
|
107
|
+
*/
|
|
108
|
+
nice?: boolean;
|
|
109
|
+
/**
|
|
110
|
+
* Number of quantile buckets to use when `domain: "quantile"`. Default 5
|
|
111
|
+
* (quintiles). Ignored otherwise.
|
|
112
|
+
*/
|
|
113
|
+
quantiles?: number;
|
|
114
|
+
/**
|
|
115
|
+
* Override the color space used to interpolate the palette stops.
|
|
116
|
+
* Falls back to `theme.paletteBlendSpace` (default `"oklch"`).
|
|
117
|
+
*/
|
|
118
|
+
blendSpace?: import("insomni").BlendSpace;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export type ColorScaleOptions<T> = CategoricalColorScaleOptions<T> | ContinuousColorScaleOptions;
|
|
122
|
+
|
|
123
|
+
export interface SizeScaleOptions {
|
|
124
|
+
type?: "linear" | "sqrt";
|
|
125
|
+
domain?: readonly [number, number];
|
|
126
|
+
range?: readonly [number, number];
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export interface AlphaScaleOptions {
|
|
130
|
+
domain?: readonly [number, number];
|
|
131
|
+
range?: readonly [number, number];
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export interface ShapeScaleOptions {
|
|
135
|
+
/** Explicit category order (default: order of first appearance). */
|
|
136
|
+
domain?: readonly unknown[];
|
|
137
|
+
/** Override the shape palette. Default: `POINT_SHAPE_PALETTE` from `marks`. */
|
|
138
|
+
palette?: readonly PointShapeKind[];
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export interface BorderStyleScaleOptions {
|
|
142
|
+
/** Explicit category order (default: order of first appearance). */
|
|
143
|
+
domain?: readonly unknown[];
|
|
144
|
+
/** Override the border-style palette. Default: `DEFAULT_BORDER_STYLE_PALETTE`. */
|
|
145
|
+
palette?: readonly PointBorderStyle[];
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export interface OverlayGlyphScaleOptions {
|
|
149
|
+
/** Explicit category order (default: order of first appearance). */
|
|
150
|
+
domain?: readonly unknown[];
|
|
151
|
+
/**
|
|
152
|
+
* Override the overlay palette. Default: a small subset of
|
|
153
|
+
* `POINT_SHAPE_PALETTE` excluding `circle` so the overlay is visually
|
|
154
|
+
* distinct from the typical base shape. Use `null` entries to suppress an
|
|
155
|
+
* overlay for that domain value.
|
|
156
|
+
*/
|
|
157
|
+
palette?: readonly (PointShapeKind | null)[];
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export type ScaleOptions =
|
|
161
|
+
| PositionScaleOptions
|
|
162
|
+
| ColorScaleOptions<unknown>
|
|
163
|
+
| SizeScaleOptions
|
|
164
|
+
| AlphaScaleOptions
|
|
165
|
+
| ShapeScaleOptions
|
|
166
|
+
| BorderStyleScaleOptions
|
|
167
|
+
| OverlayGlyphScaleOptions;
|
|
168
|
+
|
|
169
|
+
/** A scale callable: any value → number (or color, for color channels). */
|
|
170
|
+
export type ScaleFn<In, Out> = (value: In) => Out;
|
|
171
|
+
|
|
172
|
+
export interface PositionScale {
|
|
173
|
+
readonly kind: "position";
|
|
174
|
+
readonly type: PositionScaleType;
|
|
175
|
+
readonly dataType: ChannelDataType;
|
|
176
|
+
/** Pixel-space transform within the plot frame. */
|
|
177
|
+
readonly fn: ScaleFn<unknown, number>;
|
|
178
|
+
/** Underlying scale for axis builders. */
|
|
179
|
+
readonly axisScale: ContinuousScale | TimeScale | BandScale<string>;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export interface ColorScale {
|
|
183
|
+
readonly kind: "color";
|
|
184
|
+
readonly dataType: ChannelDataType;
|
|
185
|
+
readonly type: ColorScaleType;
|
|
186
|
+
readonly fn: ScaleFn<unknown, Color>;
|
|
187
|
+
readonly domain: readonly unknown[];
|
|
188
|
+
/** Original palette — present for continuous/diverging scales (used by the color-bar legend). */
|
|
189
|
+
readonly palette?: ContinuousPalette | CategoricalPalette;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export interface SizeScale {
|
|
193
|
+
readonly kind: "size";
|
|
194
|
+
readonly fn: ScaleFn<unknown, number>;
|
|
195
|
+
readonly domain: readonly [number, number];
|
|
196
|
+
readonly range: readonly [number, number];
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export interface AlphaScale {
|
|
200
|
+
readonly kind: "alpha";
|
|
201
|
+
readonly fn: ScaleFn<unknown, number>;
|
|
202
|
+
readonly domain: readonly [number, number];
|
|
203
|
+
readonly range: readonly [number, number];
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export interface ShapeScale {
|
|
207
|
+
readonly kind: "shape";
|
|
208
|
+
readonly fn: ScaleFn<unknown, PointShapeKind>;
|
|
209
|
+
readonly domain: readonly unknown[];
|
|
210
|
+
readonly palette: readonly PointShapeKind[];
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
export interface BorderStyleScale {
|
|
214
|
+
readonly kind: "borderStyle";
|
|
215
|
+
readonly fn: ScaleFn<unknown, PointBorderStyle>;
|
|
216
|
+
readonly domain: readonly unknown[];
|
|
217
|
+
readonly palette: readonly PointBorderStyle[];
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
export interface OverlayGlyphScale {
|
|
221
|
+
readonly kind: "overlayGlyph";
|
|
222
|
+
readonly fn: ScaleFn<unknown, PointShapeKind | null>;
|
|
223
|
+
readonly domain: readonly unknown[];
|
|
224
|
+
readonly palette: readonly (PointShapeKind | null)[];
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
export type ResolvedScale =
|
|
228
|
+
| PositionScale
|
|
229
|
+
| ColorScale
|
|
230
|
+
| SizeScale
|
|
231
|
+
| AlphaScale
|
|
232
|
+
| ShapeScale
|
|
233
|
+
| BorderStyleScale
|
|
234
|
+
| OverlayGlyphScale;
|
|
235
|
+
|
|
236
|
+
// ---------------------------------------------------------------------------
|
|
237
|
+
// Domain inference
|
|
238
|
+
// ---------------------------------------------------------------------------
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Numeric extent over an arbitrary value array. Non-numeric values are skipped.
|
|
242
|
+
* Falls back to `[0, 1]` for empty / non-numeric input. Expands a degenerate
|
|
243
|
+
* single-point domain by ±1 so downstream scales don't divide by zero.
|
|
244
|
+
*/
|
|
245
|
+
export function numericExtent(values: readonly unknown[]): [number, number] {
|
|
246
|
+
let lo = Infinity;
|
|
247
|
+
let hi = -Infinity;
|
|
248
|
+
for (const v of values) {
|
|
249
|
+
if (typeof v !== "number" || !Number.isFinite(v)) continue;
|
|
250
|
+
if (v < lo) lo = v;
|
|
251
|
+
if (v > hi) hi = v;
|
|
252
|
+
}
|
|
253
|
+
if (!Number.isFinite(lo) || !Number.isFinite(hi)) return [0, 1];
|
|
254
|
+
if (lo === hi) return [lo - 1, hi + 1];
|
|
255
|
+
return [lo, hi];
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function dateExtent(values: readonly unknown[]): [Date, Date] {
|
|
259
|
+
let lo = Infinity;
|
|
260
|
+
let hi = -Infinity;
|
|
261
|
+
for (const v of values) {
|
|
262
|
+
if (!(v instanceof Date)) continue;
|
|
263
|
+
const t = v.getTime();
|
|
264
|
+
if (!Number.isFinite(t)) continue;
|
|
265
|
+
if (t < lo) lo = t;
|
|
266
|
+
if (t > hi) hi = t;
|
|
267
|
+
}
|
|
268
|
+
if (!Number.isFinite(lo) || !Number.isFinite(hi)) {
|
|
269
|
+
const now = Date.now();
|
|
270
|
+
return [new Date(now), new Date(now + 1)];
|
|
271
|
+
}
|
|
272
|
+
if (lo === hi) return [new Date(lo - 1), new Date(hi + 1)];
|
|
273
|
+
return [new Date(lo), new Date(hi)];
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Categorical domain builder. Null / undefined are dropped per the policy in
|
|
277
|
+
// `aes.ts` ("Null / undefined policy for categorical channels") — they do not
|
|
278
|
+
// become a `"null"` category, never throw. Geoms grouping by the same channel
|
|
279
|
+
// should call `dropNullCategoricalIndices` so rendered groups stay aligned
|
|
280
|
+
// with this domain.
|
|
281
|
+
function uniqueStrings(values: readonly unknown[]): string[] {
|
|
282
|
+
const seen = new Set<string>();
|
|
283
|
+
const out: string[] = [];
|
|
284
|
+
for (const v of values) {
|
|
285
|
+
if (v === null || v === undefined) continue;
|
|
286
|
+
// oxlint-disable-next-line no-base-to-string -- categorical data can be any scalar; String() is intentional for non-object values after null/undefined filter
|
|
287
|
+
const s = String(v);
|
|
288
|
+
if (seen.has(s)) continue;
|
|
289
|
+
seen.add(s);
|
|
290
|
+
out.push(s);
|
|
291
|
+
}
|
|
292
|
+
return out;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function niceNumeric(domain: [number, number]): [number, number] {
|
|
296
|
+
const [lo, hi] = domain;
|
|
297
|
+
if (lo === hi) return [lo - 1, hi + 1];
|
|
298
|
+
// Snap to a "nice" tick step, then *only* extend outward when the data
|
|
299
|
+
// overshoots the nearest inner tick by more than half a step. So
|
|
300
|
+
// [-2.7, 1.9] (step 1) → [-3, 2], but [-2.2, 2.2] stays put — and the
|
|
301
|
+
// half-step boundary itself (e.g. ±2.5) keeps the original extent.
|
|
302
|
+
const min = Math.min(lo, hi);
|
|
303
|
+
const max = Math.max(lo, hi);
|
|
304
|
+
const ticks = numericTicks(min, max, 5);
|
|
305
|
+
if (ticks.length < 2) return [lo, hi];
|
|
306
|
+
const step = Math.abs(ticks[1]! - ticks[0]!);
|
|
307
|
+
if (!Number.isFinite(step) || step === 0) return [lo, hi];
|
|
308
|
+
const eps = step * 1e-9;
|
|
309
|
+
const innerLow = Math.ceil((min - eps) / step) * step; // first tick ≥ min
|
|
310
|
+
const innerHigh = Math.floor((max + eps) / step) * step; // last tick ≤ max
|
|
311
|
+
const half = step / 2;
|
|
312
|
+
const niceMin = innerLow - min > half + eps ? innerLow - step : min;
|
|
313
|
+
const niceMax = max - innerHigh > half + eps ? innerHigh + step : max;
|
|
314
|
+
const round = (v: number): number => Number.parseFloat(v.toPrecision(12));
|
|
315
|
+
return lo <= hi ? [round(niceMin), round(niceMax)] : [round(niceMax), round(niceMin)];
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// ---------------------------------------------------------------------------
|
|
319
|
+
// Position scales (x / y)
|
|
320
|
+
// ---------------------------------------------------------------------------
|
|
321
|
+
|
|
322
|
+
// Pan-zoom reads the user's scale config to seed initial bounds and clamp
|
|
323
|
+
// behavior. Lives here (not in mount.ts) because the parsing rules are
|
|
324
|
+
// scale-shape concerns, not mount-time wiring.
|
|
325
|
+
export function readNumericDomain(
|
|
326
|
+
scale: PositionScaleOptions | undefined,
|
|
327
|
+
axis: "x" | "y",
|
|
328
|
+
): readonly [number, number] {
|
|
329
|
+
const dom = scale?.domain;
|
|
330
|
+
if (!dom || dom.length !== 2) {
|
|
331
|
+
throw new Error(
|
|
332
|
+
`panZoom: ${axis} scale must have an explicit numeric domain (set .scale("${axis}", { type: "linear", domain: [a, b] })).`,
|
|
333
|
+
);
|
|
334
|
+
}
|
|
335
|
+
const [a, b] = dom as readonly [unknown, unknown];
|
|
336
|
+
const an = a instanceof Date ? a.getTime() : (a as number);
|
|
337
|
+
const bn = b instanceof Date ? b.getTime() : (b as number);
|
|
338
|
+
if (
|
|
339
|
+
typeof an !== "number" ||
|
|
340
|
+
typeof bn !== "number" ||
|
|
341
|
+
!Number.isFinite(an) ||
|
|
342
|
+
!Number.isFinite(bn)
|
|
343
|
+
) {
|
|
344
|
+
throw new Error(
|
|
345
|
+
`panZoom: ${axis} domain must be a finite [number, number] (got ${JSON.stringify(dom)}).`,
|
|
346
|
+
);
|
|
347
|
+
}
|
|
348
|
+
return [an, bn];
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
export function readContinuousType(
|
|
352
|
+
scale: PositionScaleOptions | undefined,
|
|
353
|
+
): "linear" | "log" | "sqrt" {
|
|
354
|
+
const t = scale?.type;
|
|
355
|
+
if (t === "log" || t === "sqrt") return t;
|
|
356
|
+
if (t === "band" || t === "time") {
|
|
357
|
+
throw new Error(
|
|
358
|
+
`panZoom: only linear / log / sqrt scales are supported (got "${t}"). Use a continuous numeric domain.`,
|
|
359
|
+
);
|
|
360
|
+
}
|
|
361
|
+
return "linear";
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
export function buildPositionScale<T>(
|
|
365
|
+
aes: ResolvedAes<T, unknown>,
|
|
366
|
+
data: readonly T[],
|
|
367
|
+
range: NumericRange,
|
|
368
|
+
options: PositionScaleOptions = {},
|
|
369
|
+
): PositionScale {
|
|
370
|
+
const values = materialize(aes, data);
|
|
371
|
+
const dataType = inferDataType(values);
|
|
372
|
+
const explicitType = options.type;
|
|
373
|
+
const type: PositionScaleType = explicitType ?? defaultPositionType(dataType);
|
|
374
|
+
|
|
375
|
+
// When `type` is omitted the caller's intent is encoded in `options.domain`'s
|
|
376
|
+
// shape (string[] → band, [Date, Date] → time, [number, number] → numeric).
|
|
377
|
+
// We've already inferred `type` from the data; trust the explicit domain
|
|
378
|
+
// when it's present rather than re-narrowing through `options.type`.
|
|
379
|
+
if (type === "band") {
|
|
380
|
+
const domain = (options.domain as readonly string[] | undefined) ?? uniqueStrings(values);
|
|
381
|
+
const scale = bandScale<string>(domain, range, { padding: options.padding ?? 0.1 });
|
|
382
|
+
const half = scale.bandwidth() / 2;
|
|
383
|
+
return {
|
|
384
|
+
kind: "position",
|
|
385
|
+
type: "band",
|
|
386
|
+
dataType,
|
|
387
|
+
axisScale: scale,
|
|
388
|
+
// Centered within band so points/lines anchor mid-cell.
|
|
389
|
+
fn: (v: unknown) => scale(String(v)) + half,
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
if (type === "time" || dataType === "date") {
|
|
394
|
+
const domain = (options.domain as readonly [Date, Date] | undefined) ?? dateExtent(values);
|
|
395
|
+
const scale = timeScale(domain, range);
|
|
396
|
+
return {
|
|
397
|
+
kind: "position",
|
|
398
|
+
type: "time",
|
|
399
|
+
dataType,
|
|
400
|
+
axisScale: scale,
|
|
401
|
+
fn: (v: unknown) => scale(v as Date),
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// `domain: "nice"` is sugar for the existing `nice: true` flag — apply
|
|
406
|
+
// the same niceNumeric pass as if the consumer had set both. Tuple
|
|
407
|
+
// domains still win over the data extent; the `"nice"` string is a
|
|
408
|
+
// marker that means "use the data extent, then nice it."
|
|
409
|
+
const rawDomain = options.domain;
|
|
410
|
+
const isNiceShortcut = rawDomain === "nice";
|
|
411
|
+
let domain =
|
|
412
|
+
rawDomain && !isNiceShortcut ? (rawDomain as readonly [number, number]) : numericExtent(values);
|
|
413
|
+
if (options.nice || isNiceShortcut) domain = niceNumeric([domain[0], domain[1]]);
|
|
414
|
+
|
|
415
|
+
let resolvedType: "linear" | "log" | "sqrt" = "linear";
|
|
416
|
+
let factory: (domain: NumericDomain, range: NumericRange) => ContinuousScale;
|
|
417
|
+
if (type === "log") {
|
|
418
|
+
// Log scales require strictly positive (or strictly negative) domains.
|
|
419
|
+
// When the inferred domain straddles or touches zero we can't return a
|
|
420
|
+
// valid log scale, so surface the constraint at construction rather
|
|
421
|
+
// than letting `logScale` throw a less-actionable error downstream.
|
|
422
|
+
if (domain[0] === 0 || domain[1] === 0 || Math.sign(domain[0]) !== Math.sign(domain[1])) {
|
|
423
|
+
throw new Error(
|
|
424
|
+
`Log scale requires a domain with non-zero, same-sign endpoints (got [${domain[0]}, ${domain[1]}]). ` +
|
|
425
|
+
`Set an explicit positive domain via .scale(channel, { type: "log", domain: [lo, hi] }).`,
|
|
426
|
+
);
|
|
427
|
+
}
|
|
428
|
+
factory = logScale;
|
|
429
|
+
resolvedType = "log";
|
|
430
|
+
} else if (type === "sqrt") {
|
|
431
|
+
factory = sqrtScale;
|
|
432
|
+
resolvedType = "sqrt";
|
|
433
|
+
} else {
|
|
434
|
+
factory = linearScale;
|
|
435
|
+
}
|
|
436
|
+
const scale = factory(domain, range);
|
|
437
|
+
return {
|
|
438
|
+
kind: "position",
|
|
439
|
+
type: resolvedType,
|
|
440
|
+
dataType,
|
|
441
|
+
axisScale: scale,
|
|
442
|
+
fn: (v: unknown) => scale(v as number),
|
|
443
|
+
};
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
function defaultPositionType(dataType: ChannelDataType): PositionScaleType {
|
|
447
|
+
if (dataType === "date") return "time";
|
|
448
|
+
if (dataType === "string" || dataType === "boolean") return "band";
|
|
449
|
+
return "linear";
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// ---------------------------------------------------------------------------
|
|
453
|
+
// Color scale
|
|
454
|
+
// ---------------------------------------------------------------------------
|
|
455
|
+
|
|
456
|
+
export function buildColorScale<T>(
|
|
457
|
+
aes: ResolvedAes<T, unknown>,
|
|
458
|
+
data: readonly T[],
|
|
459
|
+
theme: Theme,
|
|
460
|
+
options: ColorScaleOptions<unknown> = {},
|
|
461
|
+
): ColorScale {
|
|
462
|
+
const values = materialize(aes, data);
|
|
463
|
+
const dataType = inferDataType(values);
|
|
464
|
+
const type: ColorScaleType =
|
|
465
|
+
options.type ?? (dataType === "number" ? "continuous" : "categorical");
|
|
466
|
+
|
|
467
|
+
if (type === "categorical") {
|
|
468
|
+
return buildCategoricalColorScale(uniqueStrings(values), theme, options, dataType);
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
const contOpts =
|
|
472
|
+
options.type === "continuous" || options.type === "diverging"
|
|
473
|
+
? (options as ContinuousColorScaleOptions)
|
|
474
|
+
: undefined;
|
|
475
|
+
let palette: ContinuousPalette =
|
|
476
|
+
contOpts?.palette ??
|
|
477
|
+
(type === "diverging" ? theme.palettes.diverging : theme.palettes.continuous);
|
|
478
|
+
const space = contOpts?.blendSpace ?? theme.paletteBlendSpace;
|
|
479
|
+
if (palette.blendSpace !== space) palette = palette.withBlendSpace(space);
|
|
480
|
+
|
|
481
|
+
// Quantile shortcut: collapse the continuous scale to a categorical scale
|
|
482
|
+
// whose buckets are quantile bins of the data. Palette is sampled at N
|
|
483
|
+
// evenly spaced stops along the continuous palette so the gradient still
|
|
484
|
+
// reads as ordinal magnitude (low → high) rather than nominal category.
|
|
485
|
+
if (contOpts?.domain === "quantile") {
|
|
486
|
+
return buildQuantileColorScale(values, palette, contOpts, dataType);
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
const isNiceShortcut = contOpts?.domain === "nice";
|
|
490
|
+
// `domain === "quantile"` already returned above, so by here the only
|
|
491
|
+
// shortcut left to exclude is `"nice"`; what remains is an explicit tuple.
|
|
492
|
+
const explicitDomain =
|
|
493
|
+
contOpts?.domain && contOpts.domain !== "nice"
|
|
494
|
+
? (contOpts.domain as readonly [number, number])
|
|
495
|
+
: undefined;
|
|
496
|
+
let domain: readonly [number, number] = explicitDomain ?? numericExtent(values);
|
|
497
|
+
if ((contOpts?.nice || isNiceShortcut) && !explicitDomain) {
|
|
498
|
+
domain = niceNumeric([domain[0], domain[1]]);
|
|
499
|
+
}
|
|
500
|
+
const fn = colorScale(palette, domain);
|
|
501
|
+
return {
|
|
502
|
+
kind: "color",
|
|
503
|
+
type,
|
|
504
|
+
dataType,
|
|
505
|
+
domain,
|
|
506
|
+
fn: (v: unknown) => fn(v as number),
|
|
507
|
+
palette,
|
|
508
|
+
};
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
/**
|
|
512
|
+
* Build a quantile-bucketed color scale. Data values are sorted, N-1
|
|
513
|
+
* breakpoints are computed at evenly spaced quantiles (default N=5), and
|
|
514
|
+
* each input value is mapped to its bucket index via a binary-search-free
|
|
515
|
+
* sweep (breakpoints are monotonic so a linear scan stays cheap). The
|
|
516
|
+
* returned scale is shaped as `type: "categorical"` so the rest of the
|
|
517
|
+
* pipeline (legend, color-bar) treats it as discrete; bucket labels
|
|
518
|
+
* (`"Q1".."QN"`) live in `domain`.
|
|
519
|
+
*/
|
|
520
|
+
function buildQuantileColorScale(
|
|
521
|
+
values: readonly unknown[],
|
|
522
|
+
palette: ContinuousPalette,
|
|
523
|
+
contOpts: ContinuousColorScaleOptions,
|
|
524
|
+
dataType: ChannelDataType,
|
|
525
|
+
): ColorScale {
|
|
526
|
+
const requested = Math.max(1, Math.floor(contOpts.quantiles ?? 5));
|
|
527
|
+
const numeric: number[] = [];
|
|
528
|
+
for (const v of values) {
|
|
529
|
+
if (typeof v === "number" && Number.isFinite(v)) numeric.push(v);
|
|
530
|
+
}
|
|
531
|
+
numeric.sort((a, b) => a - b);
|
|
532
|
+
|
|
533
|
+
// Internal breakpoints at p = 1/N, 2/N, …, (N-1)/N. Empty array when N=1.
|
|
534
|
+
// Low-cardinality data can make adjacent quantiles tie (e.g. many values at
|
|
535
|
+
// the same level), producing duplicate breakpoints. Duplicate or extremal
|
|
536
|
+
// breakpoints create unreachable buckets: a span between two equal
|
|
537
|
+
// breakpoints can never be entered, and a breakpoint sitting on the data
|
|
538
|
+
// min/max leaves the bucket beyond it empty — both skew the palette. Keep
|
|
539
|
+
// only the *strictly interior* distinct breakpoints (min < bp < max) so D
|
|
540
|
+
// such breakpoints yield exactly D+1 reachable buckets and palette/labels
|
|
541
|
+
// match that count.
|
|
542
|
+
const lo = numeric.length > 0 ? numeric[0]! : 0;
|
|
543
|
+
const hi = numeric.length > 0 ? numeric[numeric.length - 1]! : 0;
|
|
544
|
+
const rawBreakpoints: number[] = [];
|
|
545
|
+
if (numeric.length > 0) {
|
|
546
|
+
for (let i = 1; i < requested; i++) {
|
|
547
|
+
rawBreakpoints.push(quantile(numeric, i / requested));
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
const breakpoints: number[] = [];
|
|
551
|
+
for (const bp of rawBreakpoints) {
|
|
552
|
+
if (bp <= lo || bp >= hi) continue; // drop extremal → empty boundary buckets
|
|
553
|
+
if (breakpoints.length === 0 || breakpoints[breakpoints.length - 1]! !== bp) {
|
|
554
|
+
breakpoints.push(bp);
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
// Reachable buckets = interior breakpoints + 1.
|
|
559
|
+
const buckets = breakpoints.length + 1;
|
|
560
|
+
|
|
561
|
+
// Sample N palette stops at bucket centers (0.5/N, 1.5/N, …, (N-0.5)/N)
|
|
562
|
+
// — keeps the first/last colors a little inside the palette's extremes,
|
|
563
|
+
// which tends to read better than hitting the very ends.
|
|
564
|
+
const bucketColors: Color[] = Array.from({ length: buckets });
|
|
565
|
+
for (let i = 0; i < buckets; i++) {
|
|
566
|
+
const t = buckets === 1 ? 0.5 : (i + 0.5) / buckets;
|
|
567
|
+
bucketColors[i] = palette(t);
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
const domain: readonly string[] = Array.from({ length: buckets }, (_, i) => `Q${i + 1}`);
|
|
571
|
+
|
|
572
|
+
return {
|
|
573
|
+
kind: "color",
|
|
574
|
+
type: "categorical",
|
|
575
|
+
dataType,
|
|
576
|
+
domain,
|
|
577
|
+
fn: (value: unknown) => {
|
|
578
|
+
const num = typeof value === "number" ? value : Number(value);
|
|
579
|
+
if (!Number.isFinite(num)) return bucketColors[0]!;
|
|
580
|
+
// Linear scan over interior, monotonic breakpoints. Buckets are
|
|
581
|
+
// left-closed: a value exactly equal to a breakpoint falls into the lower
|
|
582
|
+
// bucket (`>`), so the data minimum always lands in Q1 and ties resolve to
|
|
583
|
+
// a single well-defined bucket.
|
|
584
|
+
let bucket = 0;
|
|
585
|
+
for (let i = 0; i < breakpoints.length; i++) {
|
|
586
|
+
if (num > breakpoints[i]!) bucket = i + 1;
|
|
587
|
+
else break;
|
|
588
|
+
}
|
|
589
|
+
return bucketColors[bucket]!;
|
|
590
|
+
},
|
|
591
|
+
};
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
/**
|
|
595
|
+
* Build a categorical color scale from an explicit key list. Shared between
|
|
596
|
+
* `buildColorScale` (data-derived domain) and the chart pipeline's implicit
|
|
597
|
+
* series scale (keys = column names). User-supplied `options.palette` /
|
|
598
|
+
* `options.domain` win over the inferred values.
|
|
599
|
+
*/
|
|
600
|
+
export function buildCategoricalColorScale(
|
|
601
|
+
keys: readonly unknown[],
|
|
602
|
+
theme: Theme,
|
|
603
|
+
options: ColorScaleOptions<unknown> = {},
|
|
604
|
+
dataType: ChannelDataType = "string",
|
|
605
|
+
): ColorScale {
|
|
606
|
+
const catOpts =
|
|
607
|
+
options.type === "continuous" || options.type === "diverging"
|
|
608
|
+
? undefined
|
|
609
|
+
: (options as CategoricalColorScaleOptions<unknown>);
|
|
610
|
+
const palette: CategoricalPalette = catOpts?.palette ?? theme.palettes.categorical;
|
|
611
|
+
const domain: readonly unknown[] = catOpts?.domain ?? keys;
|
|
612
|
+
const fn = colorScale(palette, domain);
|
|
613
|
+
return {
|
|
614
|
+
kind: "color",
|
|
615
|
+
type: "categorical",
|
|
616
|
+
dataType,
|
|
617
|
+
domain,
|
|
618
|
+
fn: (v: unknown) => fn(v),
|
|
619
|
+
palette,
|
|
620
|
+
};
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
// ---------------------------------------------------------------------------
|
|
624
|
+
// Size scale
|
|
625
|
+
// ---------------------------------------------------------------------------
|
|
626
|
+
|
|
627
|
+
/**
|
|
628
|
+
* Default range pair for size/alpha when neither the user nor the theme
|
|
629
|
+
* supplies one. The `buildSizeScale` / `buildAlphaScale` callers pass a theme
|
|
630
|
+
* override via the chart pipeline; this constant exists so direct callers
|
|
631
|
+
* (tests, ad-hoc usage) get a sensible default.
|
|
632
|
+
*/
|
|
633
|
+
const defaultSizeRange: readonly [number, number] = [2, 14];
|
|
634
|
+
const defaultAlphaRange: readonly [number, number] = [0.2, 1];
|
|
635
|
+
|
|
636
|
+
export function buildSizeScale<T>(
|
|
637
|
+
aes: ResolvedAes<T, number>,
|
|
638
|
+
data: readonly T[],
|
|
639
|
+
options: SizeScaleOptions = {},
|
|
640
|
+
): SizeScale {
|
|
641
|
+
const values = materialize(aes, data);
|
|
642
|
+
const domain = options.domain ?? numericExtent(values);
|
|
643
|
+
const range = options.range ?? defaultSizeRange;
|
|
644
|
+
const type = options.type ?? "sqrt";
|
|
645
|
+
const factory = type === "sqrt" ? sqrtScale : linearScale;
|
|
646
|
+
const scale = factory(domain, range);
|
|
647
|
+
return {
|
|
648
|
+
kind: "size",
|
|
649
|
+
fn: (v: unknown) => scale(v as number),
|
|
650
|
+
domain,
|
|
651
|
+
range,
|
|
652
|
+
};
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
// ---------------------------------------------------------------------------
|
|
656
|
+
// Alpha scale
|
|
657
|
+
// ---------------------------------------------------------------------------
|
|
658
|
+
|
|
659
|
+
export function buildAlphaScale<T>(
|
|
660
|
+
aes: ResolvedAes<T, number>,
|
|
661
|
+
data: readonly T[],
|
|
662
|
+
options: AlphaScaleOptions = {},
|
|
663
|
+
): AlphaScale {
|
|
664
|
+
const values = materialize(aes, data);
|
|
665
|
+
const domain = options.domain ?? numericExtent(values);
|
|
666
|
+
const range = options.range ?? defaultAlphaRange;
|
|
667
|
+
const scale = linearScale(domain, range);
|
|
668
|
+
return {
|
|
669
|
+
kind: "alpha",
|
|
670
|
+
fn: (v: unknown) => scale(v as number),
|
|
671
|
+
domain,
|
|
672
|
+
range,
|
|
673
|
+
};
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
// ---------------------------------------------------------------------------
|
|
677
|
+
// Shape scale
|
|
678
|
+
// ---------------------------------------------------------------------------
|
|
679
|
+
|
|
680
|
+
function uniqueValues(values: readonly unknown[]): unknown[] {
|
|
681
|
+
const seen = new Set<unknown>();
|
|
682
|
+
const out: unknown[] = [];
|
|
683
|
+
for (const v of values) {
|
|
684
|
+
if (v === null || v === undefined) continue;
|
|
685
|
+
// oxlint-disable-next-line no-base-to-string -- String() only reached for non-object scalars (number, boolean, string) after the typeof guard
|
|
686
|
+
const key = typeof v === "object" ? v : String(v);
|
|
687
|
+
if (seen.has(key)) continue;
|
|
688
|
+
seen.add(key);
|
|
689
|
+
out.push(v);
|
|
690
|
+
}
|
|
691
|
+
return out;
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
export function buildShapeScale<T>(
|
|
695
|
+
aes: ResolvedAes<T, unknown>,
|
|
696
|
+
data: readonly T[],
|
|
697
|
+
options: ShapeScaleOptions = {},
|
|
698
|
+
): ShapeScale {
|
|
699
|
+
const values = materialize(aes, data);
|
|
700
|
+
const domain = options.domain ?? uniqueValues(values);
|
|
701
|
+
const palette = options.palette ?? POINT_SHAPE_PALETTE;
|
|
702
|
+
const lookup = new Map<unknown, PointShapeKind>();
|
|
703
|
+
for (let i = 0; i < domain.length; i++) {
|
|
704
|
+
// oxlint-disable-next-line no-base-to-string -- String() only reached for non-object scalars after the typeof guard
|
|
705
|
+
const key = typeof domain[i] === "object" ? domain[i] : String(domain[i]);
|
|
706
|
+
lookup.set(key, palette[i % palette.length]!);
|
|
707
|
+
}
|
|
708
|
+
const fallback = palette[0] ?? "circle";
|
|
709
|
+
return {
|
|
710
|
+
kind: "shape",
|
|
711
|
+
domain,
|
|
712
|
+
palette,
|
|
713
|
+
fn: (v: unknown) => {
|
|
714
|
+
// oxlint-disable-next-line no-base-to-string -- String() only reached for non-object scalars after the typeof guard
|
|
715
|
+
const key = typeof v === "object" ? v : String(v);
|
|
716
|
+
return lookup.get(key) ?? fallback;
|
|
717
|
+
},
|
|
718
|
+
};
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
// ---------------------------------------------------------------------------
|
|
722
|
+
// Border-style scale
|
|
723
|
+
// ---------------------------------------------------------------------------
|
|
724
|
+
|
|
725
|
+
export const DEFAULT_BORDER_STYLE_PALETTE: readonly PointBorderStyle[] = [
|
|
726
|
+
"solid",
|
|
727
|
+
"open",
|
|
728
|
+
"dashed",
|
|
729
|
+
"dotted",
|
|
730
|
+
];
|
|
731
|
+
|
|
732
|
+
export function buildBorderStyleScale<T>(
|
|
733
|
+
aes: ResolvedAes<T, unknown>,
|
|
734
|
+
data: readonly T[],
|
|
735
|
+
options: BorderStyleScaleOptions = {},
|
|
736
|
+
): BorderStyleScale {
|
|
737
|
+
const values = materialize(aes, data);
|
|
738
|
+
const domain = options.domain ?? uniqueValues(values);
|
|
739
|
+
const palette = options.palette ?? DEFAULT_BORDER_STYLE_PALETTE;
|
|
740
|
+
const lookup = new Map<unknown, PointBorderStyle>();
|
|
741
|
+
for (let i = 0; i < domain.length; i++) {
|
|
742
|
+
// oxlint-disable-next-line no-base-to-string -- String() only reached for non-object scalars after the typeof guard
|
|
743
|
+
const key = typeof domain[i] === "object" ? domain[i] : String(domain[i]);
|
|
744
|
+
lookup.set(key, palette[i % palette.length]!);
|
|
745
|
+
}
|
|
746
|
+
const fallback = palette[0] ?? "solid";
|
|
747
|
+
return {
|
|
748
|
+
kind: "borderStyle",
|
|
749
|
+
domain,
|
|
750
|
+
palette,
|
|
751
|
+
fn: (v: unknown) => {
|
|
752
|
+
// oxlint-disable-next-line no-base-to-string -- String() only reached for non-object scalars after the typeof guard
|
|
753
|
+
const key = typeof v === "object" ? v : String(v);
|
|
754
|
+
return lookup.get(key) ?? fallback;
|
|
755
|
+
},
|
|
756
|
+
};
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
// ---------------------------------------------------------------------------
|
|
760
|
+
// Overlay-glyph scale
|
|
761
|
+
// ---------------------------------------------------------------------------
|
|
762
|
+
|
|
763
|
+
/**
|
|
764
|
+
* Default overlay palette. First slot is `null` so the most-common category
|
|
765
|
+
* gets no overlay — overlays are typically a minority styling for flagged /
|
|
766
|
+
* exceptional rows. The remaining slots are visually distinct glyphs.
|
|
767
|
+
*/
|
|
768
|
+
export const DEFAULT_OVERLAY_GLYPH_PALETTE: readonly (PointShapeKind | null)[] = [
|
|
769
|
+
null,
|
|
770
|
+
"plus",
|
|
771
|
+
"cross",
|
|
772
|
+
"star",
|
|
773
|
+
"diamond",
|
|
774
|
+
];
|
|
775
|
+
|
|
776
|
+
export function buildOverlayGlyphScale<T>(
|
|
777
|
+
aes: ResolvedAes<T, unknown>,
|
|
778
|
+
data: readonly T[],
|
|
779
|
+
options: OverlayGlyphScaleOptions = {},
|
|
780
|
+
): OverlayGlyphScale {
|
|
781
|
+
const values = materialize(aes, data);
|
|
782
|
+
const domain = options.domain ?? uniqueValues(values);
|
|
783
|
+
const palette = options.palette ?? DEFAULT_OVERLAY_GLYPH_PALETTE;
|
|
784
|
+
const lookup = new Map<unknown, PointShapeKind | null>();
|
|
785
|
+
for (let i = 0; i < domain.length; i++) {
|
|
786
|
+
// oxlint-disable-next-line no-base-to-string -- String() only reached for non-object scalars after the typeof guard
|
|
787
|
+
const key = typeof domain[i] === "object" ? domain[i] : String(domain[i]);
|
|
788
|
+
lookup.set(key, palette[i % palette.length] ?? null);
|
|
789
|
+
}
|
|
790
|
+
return {
|
|
791
|
+
kind: "overlayGlyph",
|
|
792
|
+
domain,
|
|
793
|
+
palette,
|
|
794
|
+
fn: (v: unknown) => {
|
|
795
|
+
// oxlint-disable-next-line no-base-to-string -- String() only reached for non-object scalars after the typeof guard
|
|
796
|
+
const key = typeof v === "object" ? v : String(v);
|
|
797
|
+
return lookup.has(key) ? (lookup.get(key) ?? null) : null;
|
|
798
|
+
},
|
|
799
|
+
};
|
|
800
|
+
}
|