ggaction 0.0.1
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 +27 -0
- package/LICENSE +21 -0
- package/README.md +108 -0
- package/package.json +84 -0
- package/src/AGENTS.md +119 -0
- package/src/ChartProgram.js +10 -0
- package/src/actions/boxPlots/components.js +88 -0
- package/src/actions/boxPlots/create.js +84 -0
- package/src/actions/boxPlots/index.js +10 -0
- package/src/actions/boxPlots/materialize.js +183 -0
- package/src/actions/boxPlots/options.js +147 -0
- package/src/actions/boxPlots/resolve.js +38 -0
- package/src/actions/canvas/actions.js +144 -0
- package/src/actions/canvas/index.js +6 -0
- package/src/actions/coordinates/actions.js +85 -0
- package/src/actions/coordinates/index.js +5 -0
- package/src/actions/data/box.js +40 -0
- package/src/actions/data/create.js +33 -0
- package/src/actions/data/density.js +62 -0
- package/src/actions/data/derived.js +52 -0
- package/src/actions/data/filter.js +168 -0
- package/src/actions/data/index.js +32 -0
- package/src/actions/data/interval.js +69 -0
- package/src/actions/data/regression.js +71 -0
- package/src/actions/data/shared.js +25 -0
- package/src/actions/encodings/appearance.js +243 -0
- package/src/actions/encodings/barWidth.js +60 -0
- package/src/actions/encodings/color.js +329 -0
- package/src/actions/encodings/density.js +225 -0
- package/src/actions/encodings/histogram.js +50 -0
- package/src/actions/encodings/index.js +23 -0
- package/src/actions/encodings/offset.js +104 -0
- package/src/actions/encodings/position/policies/area.js +24 -0
- package/src/actions/encodings/position/policies/bar.js +110 -0
- package/src/actions/encodings/position/policies/common.js +17 -0
- package/src/actions/encodings/position/policies/index.js +25 -0
- package/src/actions/encodings/position/policies/line.js +74 -0
- package/src/actions/encodings/position/policies/point.js +19 -0
- package/src/actions/encodings/position/policies/rule.js +17 -0
- package/src/actions/encodings/position/resolve.js +170 -0
- package/src/actions/encodings/position.js +176 -0
- package/src/actions/encodings/ranged.js +276 -0
- package/src/actions/encodings/ruleAppearance.js +47 -0
- package/src/actions/encodings/shared.js +164 -0
- package/src/actions/encodings/strokeDash.js +145 -0
- package/src/actions/errorBands/components.js +81 -0
- package/src/actions/errorBands/create.js +159 -0
- package/src/actions/errorBands/index.js +7 -0
- package/src/actions/errorBands/resolve.js +28 -0
- package/src/actions/errorBars/components.js +62 -0
- package/src/actions/errorBars/create.js +144 -0
- package/src/actions/errorBars/index.js +7 -0
- package/src/actions/errorBars/resolve.js +25 -0
- package/src/actions/guides/axes/axes.js +204 -0
- package/src/actions/guides/axes/axis.js +131 -0
- package/src/actions/guides/axes/index.js +17 -0
- package/src/actions/guides/axes/labels.js +231 -0
- package/src/actions/guides/axes/lines.js +149 -0
- package/src/actions/guides/axes/policy.js +155 -0
- package/src/actions/guides/axes/tickGroups.js +149 -0
- package/src/actions/guides/axes/ticks.js +179 -0
- package/src/actions/guides/axes/titles.js +264 -0
- package/src/actions/guides/grids/grid.js +228 -0
- package/src/actions/guides/grids/index.js +1 -0
- package/src/actions/guides/grids/resolve.js +258 -0
- package/src/actions/guides/guides.js +138 -0
- package/src/actions/guides/index.js +11 -0
- package/src/actions/guides/legends/categorical/actions.js +329 -0
- package/src/actions/guides/legends/categorical/components.js +140 -0
- package/src/actions/guides/legends/categorical/index.js +49 -0
- package/src/actions/guides/legends/categorical/layout.js +445 -0
- package/src/actions/guides/legends/categorical/options.js +188 -0
- package/src/actions/guides/legends/categorical/recipes.js +120 -0
- package/src/actions/guides/legends/categorical/resolve.js +131 -0
- package/src/actions/guides/legends/categorical/symbols.js +280 -0
- package/src/actions/guides/legends/categorical/validation.js +27 -0
- package/src/actions/guides/legends/continuous/common.js +273 -0
- package/src/actions/guides/legends/continuous/gradient.js +363 -0
- package/src/actions/guides/legends/continuous/index.js +25 -0
- package/src/actions/guides/legends/continuous/interval.js +234 -0
- package/src/actions/guides/legends/continuous/opacity.js +382 -0
- package/src/actions/guides/legends/edit.js +314 -0
- package/src/actions/guides/legends/index.js +9 -0
- package/src/actions/guides/legends/size.js +220 -0
- package/src/actions/guides/tickValues.js +84 -0
- package/src/actions/index.js +31 -0
- package/src/actions/intervals/resolve.js +352 -0
- package/src/actions/marks/area.js +369 -0
- package/src/actions/marks/bar/create.js +46 -0
- package/src/actions/marks/bar/edit.js +90 -0
- package/src/actions/marks/bar/index.js +9 -0
- package/src/actions/marks/bar/materialize.js +130 -0
- package/src/actions/marks/index.js +13 -0
- package/src/actions/marks/line.js +248 -0
- package/src/actions/marks/point.js +420 -0
- package/src/actions/marks/rule.js +287 -0
- package/src/actions/marks/shared.js +112 -0
- package/src/actions/primitives/createGraphics.js +163 -0
- package/src/actions/primitives/editGraphics.js +208 -0
- package/src/actions/primitives/index.js +9 -0
- package/src/actions/primitives/semantic.js +168 -0
- package/src/actions/primitives/semanticValue.js +182 -0
- package/src/actions/regression/components.js +216 -0
- package/src/actions/regression/create.js +102 -0
- package/src/actions/regression/index.js +23 -0
- package/src/actions/regression/resolve.js +57 -0
- package/src/actions/scales/consumers.js +283 -0
- package/src/actions/scales/create.js +148 -0
- package/src/actions/scales/definitions.js +319 -0
- package/src/actions/scales/edit.js +290 -0
- package/src/actions/scales/index.js +11 -0
- package/src/actions/scales/materialize.js +466 -0
- package/src/actions/scales/quantitativeColor.js +45 -0
- package/src/actions/selection/actions.js +384 -0
- package/src/actions/selection/index.js +23 -0
- package/src/actions/titles/actions.js +235 -0
- package/src/actions/titles/index.js +19 -0
- package/src/actions/titles/resolve.js +321 -0
- package/src/core/ChartProgram.js +316 -0
- package/src/core/action.js +120 -0
- package/src/core/identifiers.js +28 -0
- package/src/core/immutable.js +94 -0
- package/src/core/specs.js +29 -0
- package/src/core/textMetrics.js +23 -0
- package/src/core/validation.js +54 -0
- package/src/core/vocabulary.js +58 -0
- package/src/extension.js +2 -0
- package/src/grammar/aggregate.js +259 -0
- package/src/grammar/areaSeries.js +217 -0
- package/src/grammar/bars/aggregate.js +98 -0
- package/src/grammar/bars/geometry.js +54 -0
- package/src/grammar/bars/policy.js +90 -0
- package/src/grammar/boxPlot.js +140 -0
- package/src/grammar/coordinates.js +25 -0
- package/src/grammar/curveCommands.js +356 -0
- package/src/grammar/density.js +296 -0
- package/src/grammar/filter.js +169 -0
- package/src/grammar/histogram.js +332 -0
- package/src/grammar/interval.js +343 -0
- package/src/grammar/lineSeries.js +197 -0
- package/src/grammar/markFilter.js +46 -0
- package/src/grammar/markSelection.js +326 -0
- package/src/grammar/palettes.js +208 -0
- package/src/grammar/pathCommands.js +86 -0
- package/src/grammar/pointShapes.js +164 -0
- package/src/grammar/positionCompatibility.js +32 -0
- package/src/grammar/regression.js +578 -0
- package/src/grammar/ruleAppearance.js +12 -0
- package/src/grammar/rules.js +90 -0
- package/src/grammar/scales/appearance.js +185 -0
- package/src/grammar/scales/color.js +349 -0
- package/src/grammar/scales/continuous.js +111 -0
- package/src/grammar/scales/definition.js +153 -0
- package/src/grammar/scales/discretized.js +159 -0
- package/src/grammar/scales/fields.js +133 -0
- package/src/grammar/scales/index.js +12 -0
- package/src/grammar/scales/mapping.js +27 -0
- package/src/grammar/scales/ordinal.js +221 -0
- package/src/grammar/scales/policies.js +50 -0
- package/src/grammar/scales/temporal.js +53 -0
- package/src/grammar/scales/transformed.js +236 -0
- package/src/grammar/scales/types.js +102 -0
- package/src/grammar/scales/validation.js +66 -0
- package/src/grammar/scales.js +1 -0
- package/src/grammar/schemas/concreteGraphic.js +73 -0
- package/src/grammar/schemas/graphic.js +131 -0
- package/src/grammar/schemas/graphicBounds.js +146 -0
- package/src/grammar/schemas/graphicTree.js +133 -0
- package/src/grammar/schemas/mark.js +4 -0
- package/src/grammar/schemas/semanticPath.js +174 -0
- package/src/grammar/seriesLayout.js +76 -0
- package/src/grammar/ticks.js +149 -0
- package/src/grammar/transforms.js +37 -0
- package/src/index.js +2 -0
- package/src/layout/canvas.js +114 -0
- package/src/layout/grid.js +17 -0
- package/src/layout/legend.js +51 -0
- package/src/layout/text.js +62 -0
- package/src/layout/title.js +107 -0
- package/src/materialization/bars/aggregate.js +184 -0
- package/src/materialization/bars/grouped.js +117 -0
- package/src/materialization/bars/histogram.js +163 -0
- package/src/materialization/bars/ranged.js +53 -0
- package/src/materialization/bars/resolve.js +56 -0
- package/src/materialization/dataProvenance.js +15 -0
- package/src/materialization/density.js +29 -0
- package/src/materialization/dependencies.js +134 -0
- package/src/materialization/encodings.js +50 -0
- package/src/materialization/graphicHierarchy.js +104 -0
- package/src/materialization/legends.js +20 -0
- package/src/materialization/marks.js +110 -0
- package/src/materialization/planner.js +38 -0
- package/src/materialization/selection/filter.js +40 -0
- package/src/materialization/selection/items.js +503 -0
- package/src/materialization/selection/path.js +37 -0
- package/src/materialization/selection/point.js +118 -0
- package/src/materialization/selection/policies/area.js +10 -0
- package/src/materialization/selection/policies/bar.js +10 -0
- package/src/materialization/selection/policies/index.js +40 -0
- package/src/materialization/selection/policies/line.js +10 -0
- package/src/materialization/selection/policies/point.js +10 -0
- package/src/materialization/selection/policies/rule.js +10 -0
- package/src/materialization/selection/state.js +67 -0
- package/src/materialization/selection/styles.js +183 -0
- package/src/renderers/canvas/circle.js +62 -0
- package/src/renderers/canvas/index.js +132 -0
- package/src/renderers/canvas/line.js +54 -0
- package/src/renderers/canvas/path.js +98 -0
- package/src/renderers/canvas/rect.js +57 -0
- package/src/renderers/canvas/text.js +96 -0
- package/src/renderers/canvas/validation.js +23 -0
- package/src/renderers/png.js +29 -0
- package/src/selectors/coordinates.js +13 -0
- package/src/selectors/datasets.js +13 -0
- package/src/selectors/index.js +4 -0
- package/src/selectors/layers.js +32 -0
- package/src/selectors/scales.js +21 -0
- package/src/theme/defaults.js +13 -0
- package/types/extension.d.ts +14 -0
- package/types/index.d.ts +81 -0
- package/types/png.d.ts +14 -0
- package/types/program.d.ts +1096 -0
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { validateContinuousColorInterpolation } from "./color.js";
|
|
2
|
+
import { isTransformedScaleType } from "./mapping.js";
|
|
3
|
+
import {
|
|
4
|
+
normalizeTransformParameters,
|
|
5
|
+
validateTransformedDomain
|
|
6
|
+
} from "./transformed.js";
|
|
7
|
+
import {
|
|
8
|
+
isContinuousColorScaleType,
|
|
9
|
+
validateScalePropertyForType
|
|
10
|
+
} from "./types.js";
|
|
11
|
+
|
|
12
|
+
const BOOLEAN_PROPERTIES = Object.freeze([
|
|
13
|
+
"nice", "zero", "clamp", "reverse"
|
|
14
|
+
]);
|
|
15
|
+
const TYPE_PARAMETERS = Object.freeze(["base", "exponent", "constant"]);
|
|
16
|
+
|
|
17
|
+
function retainedValue(previous, patch, defaults, property, typeChanged) {
|
|
18
|
+
if (Object.hasOwn(patch, property)) return patch[property];
|
|
19
|
+
if (!typeChanged && Object.hasOwn(previous, property)) {
|
|
20
|
+
return previous[property];
|
|
21
|
+
}
|
|
22
|
+
return defaults[property];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function validateBandParameters(definition, previous, patch, defaults, typeChanged) {
|
|
26
|
+
const paddingInner = retainedValue(
|
|
27
|
+
previous, patch, defaults, "paddingInner", typeChanged
|
|
28
|
+
) ?? 0;
|
|
29
|
+
const paddingOuter = retainedValue(
|
|
30
|
+
previous, patch, defaults, "paddingOuter", typeChanged
|
|
31
|
+
) ?? 0;
|
|
32
|
+
const align = retainedValue(previous, patch, defaults, "align", typeChanged) ?? 0.5;
|
|
33
|
+
if (!Number.isFinite(paddingInner) || paddingInner < 0 || paddingInner >= 1) {
|
|
34
|
+
throw new RangeError(
|
|
35
|
+
"Scale paddingInner must be from 0 (inclusive) to 1 (exclusive)."
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
if (!Number.isFinite(paddingOuter) || paddingOuter < 0) {
|
|
39
|
+
throw new RangeError("Scale paddingOuter must be non-negative and finite.");
|
|
40
|
+
}
|
|
41
|
+
if (!Number.isFinite(align) || align < 0 || align > 1) {
|
|
42
|
+
throw new RangeError("Scale align must be between 0 and 1.");
|
|
43
|
+
}
|
|
44
|
+
Object.assign(definition, { paddingInner, paddingOuter, align });
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function validatePointParameters(definition, previous, patch, defaults, typeChanged) {
|
|
48
|
+
const padding = retainedValue(
|
|
49
|
+
previous, patch, defaults, "padding", typeChanged
|
|
50
|
+
) ?? 0.5;
|
|
51
|
+
const align = retainedValue(previous, patch, defaults, "align", typeChanged) ?? 0.5;
|
|
52
|
+
if (!Number.isFinite(padding) || padding < 0) {
|
|
53
|
+
throw new RangeError("Scale padding must be non-negative and finite.");
|
|
54
|
+
}
|
|
55
|
+
if (!Number.isFinite(align) || align < 0 || align > 1) {
|
|
56
|
+
throw new RangeError("Scale align must be between 0 and 1.");
|
|
57
|
+
}
|
|
58
|
+
Object.assign(definition, { padding, align });
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function normalizeScaleDefinition({
|
|
62
|
+
type,
|
|
63
|
+
previous = {},
|
|
64
|
+
patch = {},
|
|
65
|
+
defaults = {},
|
|
66
|
+
retainCoreOnTypeChange = false,
|
|
67
|
+
retainCompatibleOnTypeChange = false,
|
|
68
|
+
validateDomain,
|
|
69
|
+
validateRange
|
|
70
|
+
}) {
|
|
71
|
+
const typeChanged = previous.type !== undefined && previous.type !== type;
|
|
72
|
+
const rawDomain = Object.hasOwn(patch, "domain")
|
|
73
|
+
? patch.domain
|
|
74
|
+
: (!typeChanged || retainCoreOnTypeChange) && Object.hasOwn(previous, "domain")
|
|
75
|
+
? previous.domain
|
|
76
|
+
: defaults.domain ?? "auto";
|
|
77
|
+
const rawRange = Object.hasOwn(patch, "range")
|
|
78
|
+
? patch.range
|
|
79
|
+
: (!typeChanged || retainCoreOnTypeChange) && Object.hasOwn(previous, "range")
|
|
80
|
+
? previous.range
|
|
81
|
+
: defaults.range ?? "auto";
|
|
82
|
+
const definition = {
|
|
83
|
+
type,
|
|
84
|
+
domain: validateDomain(type, rawDomain),
|
|
85
|
+
range: validateRange(type, rawRange)
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
for (const property of BOOLEAN_PROPERTIES) {
|
|
89
|
+
let value = retainedValue(previous, patch, defaults, property, typeChanged);
|
|
90
|
+
if (
|
|
91
|
+
value === undefined &&
|
|
92
|
+
typeChanged &&
|
|
93
|
+
retainCompatibleOnTypeChange &&
|
|
94
|
+
Object.hasOwn(previous, property)
|
|
95
|
+
) {
|
|
96
|
+
try {
|
|
97
|
+
validateScalePropertyForType(type, property);
|
|
98
|
+
value = previous[property];
|
|
99
|
+
} catch {
|
|
100
|
+
value = undefined;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
if (value === undefined) continue;
|
|
104
|
+
if (typeof value !== "boolean") {
|
|
105
|
+
throw new TypeError(`Scale ${property} must be a boolean.`);
|
|
106
|
+
}
|
|
107
|
+
validateScalePropertyForType(type, property);
|
|
108
|
+
definition[property] = value;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const requested = Object.fromEntries(TYPE_PARAMETERS.flatMap(property => {
|
|
112
|
+
const value = retainedValue(previous, patch, defaults, property, typeChanged);
|
|
113
|
+
return value === undefined ? [] : [[property, value]];
|
|
114
|
+
}));
|
|
115
|
+
if (isTransformedScaleType(type)) {
|
|
116
|
+
const parameters = normalizeTransformParameters(type, requested);
|
|
117
|
+
if (type === "log") definition.base = parameters.base;
|
|
118
|
+
if (type === "pow") definition.exponent = parameters.exponent;
|
|
119
|
+
if (type === "symlog") definition.constant = parameters.constant;
|
|
120
|
+
if (definition.domain !== "auto") {
|
|
121
|
+
validateTransformedDomain(type, definition.domain, parameters);
|
|
122
|
+
}
|
|
123
|
+
} else {
|
|
124
|
+
for (const property of Object.keys(requested)) {
|
|
125
|
+
validateScalePropertyForType(type, property);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const interpolate = retainedValue(
|
|
130
|
+
previous, patch, defaults, "interpolate", typeChanged
|
|
131
|
+
);
|
|
132
|
+
if (isContinuousColorScaleType(type)) {
|
|
133
|
+
definition.interpolate = validateContinuousColorInterpolation(
|
|
134
|
+
interpolate ?? "rgb"
|
|
135
|
+
);
|
|
136
|
+
} else if (interpolate !== undefined) {
|
|
137
|
+
throw new Error(`Scale type "${type}" does not support interpolate.`);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (type === "band") {
|
|
141
|
+
validateBandParameters(definition, previous, patch, defaults, typeChanged);
|
|
142
|
+
} else if (type === "point") {
|
|
143
|
+
validatePointParameters(definition, previous, patch, defaults, typeChanged);
|
|
144
|
+
} else {
|
|
145
|
+
for (const property of ["paddingInner", "paddingOuter", "padding", "align"]) {
|
|
146
|
+
if (Object.hasOwn(patch, property)) {
|
|
147
|
+
validateScalePropertyForType(type, property);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return definition;
|
|
153
|
+
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { cloneAndFreeze } from "../../core/immutable.js";
|
|
2
|
+
import { resolveColorRange, validateColorRange } from "./appearance.js";
|
|
3
|
+
import { SCALE_ROLES, validateScaleTypeForRole } from "./types.js";
|
|
4
|
+
|
|
5
|
+
export const DISCRETIZED_COLOR_SCALE_TYPES = cloneAndFreeze([
|
|
6
|
+
"quantize",
|
|
7
|
+
"quantile",
|
|
8
|
+
"threshold"
|
|
9
|
+
]);
|
|
10
|
+
|
|
11
|
+
function finiteValues(values, label) {
|
|
12
|
+
if (!Array.isArray(values) || values.length === 0 || !values.every(Number.isFinite)) {
|
|
13
|
+
throw new TypeError(`${label} must contain finite numbers.`);
|
|
14
|
+
}
|
|
15
|
+
return values;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function ascending(values, label) {
|
|
19
|
+
finiteValues(values, label);
|
|
20
|
+
if (values.some((value, index) => index > 0 && value <= values[index - 1])) {
|
|
21
|
+
throw new RangeError(`${label} must be strictly increasing.`);
|
|
22
|
+
}
|
|
23
|
+
return values;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function nondecreasing(values, label) {
|
|
27
|
+
finiteValues(values, label);
|
|
28
|
+
if (values.some((value, index) => index > 0 && value < values[index - 1])) {
|
|
29
|
+
throw new RangeError(`${label} must be nondecreasing.`);
|
|
30
|
+
}
|
|
31
|
+
return values;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function quantile(sorted, probability) {
|
|
35
|
+
const position = (sorted.length - 1) * probability;
|
|
36
|
+
const lower = Math.floor(position);
|
|
37
|
+
const upper = Math.ceil(position);
|
|
38
|
+
return sorted[lower] + (sorted[upper] - sorted[lower]) * (position - lower);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function validateDiscretizedColorDomain(type, domain) {
|
|
42
|
+
validateScaleTypeForRole(type, SCALE_ROLES.discretizedColor);
|
|
43
|
+
if (domain === "auto") {
|
|
44
|
+
if (type === "threshold") {
|
|
45
|
+
throw new Error("Threshold color scale requires an explicit domain.");
|
|
46
|
+
}
|
|
47
|
+
return domain;
|
|
48
|
+
}
|
|
49
|
+
if (!Array.isArray(domain)) {
|
|
50
|
+
throw new TypeError("Discretized color domain must be an array or auto.");
|
|
51
|
+
}
|
|
52
|
+
if (type === "quantize") {
|
|
53
|
+
if (domain.length !== 2 || !domain.every(Number.isFinite) || domain[0] >= domain[1]) {
|
|
54
|
+
throw new RangeError("Quantize color domain must be an increasing pair.");
|
|
55
|
+
}
|
|
56
|
+
} else if (type === "quantile") {
|
|
57
|
+
finiteValues(domain, "Quantile color domain");
|
|
58
|
+
} else {
|
|
59
|
+
ascending(domain, "Threshold color domain");
|
|
60
|
+
}
|
|
61
|
+
return cloneAndFreeze(domain);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function validateDiscretizedColorRange(range) {
|
|
65
|
+
const validated = validateColorRange(range);
|
|
66
|
+
if (validated === "auto") return validated;
|
|
67
|
+
if (Array.isArray(validated) && validated.length < 2) {
|
|
68
|
+
throw new RangeError("Discretized color range requires at least two colors.");
|
|
69
|
+
}
|
|
70
|
+
return validated;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function resolveDiscretizedColorScale({ type, domain, range, values }) {
|
|
74
|
+
validateScaleTypeForRole(type, SCALE_ROLES.discretizedColor);
|
|
75
|
+
finiteValues(values, "Discretized color values");
|
|
76
|
+
const requestedDomain = validateDiscretizedColorDomain(type, domain);
|
|
77
|
+
const sample = [...values].sort((left, right) => left - right);
|
|
78
|
+
const validatedRange = validateDiscretizedColorRange(range);
|
|
79
|
+
const colorCount = type === "threshold" && requestedDomain !== "auto"
|
|
80
|
+
? requestedDomain.length + 1
|
|
81
|
+
: Array.isArray(range) ? range.length : 5;
|
|
82
|
+
const colors = resolveColorRange(
|
|
83
|
+
validatedRange === "auto" ? { palette: "viridis" } : validatedRange,
|
|
84
|
+
colorCount
|
|
85
|
+
);
|
|
86
|
+
if (colors.length < 2) {
|
|
87
|
+
throw new RangeError("Discretized color range requires at least two colors.");
|
|
88
|
+
}
|
|
89
|
+
let resolvedDomain;
|
|
90
|
+
let thresholds;
|
|
91
|
+
if (type === "quantize") {
|
|
92
|
+
resolvedDomain = requestedDomain === "auto"
|
|
93
|
+
? [sample[0], sample.at(-1)]
|
|
94
|
+
: [...requestedDomain];
|
|
95
|
+
if (resolvedDomain[0] === resolvedDomain[1]) {
|
|
96
|
+
throw new RangeError("Quantize color scale requires a non-zero domain span.");
|
|
97
|
+
}
|
|
98
|
+
thresholds = Array.from(
|
|
99
|
+
{ length: colors.length - 1 },
|
|
100
|
+
(_, index) => resolvedDomain[0] + (index + 1) / colors.length *
|
|
101
|
+
(resolvedDomain[1] - resolvedDomain[0])
|
|
102
|
+
);
|
|
103
|
+
} else if (type === "quantile") {
|
|
104
|
+
const source = requestedDomain === "auto"
|
|
105
|
+
? sample
|
|
106
|
+
: [...requestedDomain].sort((left, right) => left - right);
|
|
107
|
+
resolvedDomain = source;
|
|
108
|
+
thresholds = Array.from(
|
|
109
|
+
{ length: colors.length - 1 },
|
|
110
|
+
(_, index) => quantile(source, (index + 1) / colors.length)
|
|
111
|
+
);
|
|
112
|
+
} else {
|
|
113
|
+
resolvedDomain = [...requestedDomain];
|
|
114
|
+
thresholds = [...requestedDomain];
|
|
115
|
+
if (colors.length !== thresholds.length + 1) {
|
|
116
|
+
throw new RangeError(
|
|
117
|
+
"Threshold color range must contain exactly one more color than its domain."
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return cloneAndFreeze({
|
|
122
|
+
type,
|
|
123
|
+
domain: resolvedDomain,
|
|
124
|
+
thresholds,
|
|
125
|
+
range: colors
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export function discretizedColorIndex(value, thresholds) {
|
|
130
|
+
if (!Number.isFinite(value)) {
|
|
131
|
+
throw new TypeError("Discretized color values must be finite numbers.");
|
|
132
|
+
}
|
|
133
|
+
let index = 0;
|
|
134
|
+
while (index < thresholds.length && value >= thresholds[index]) index += 1;
|
|
135
|
+
return index;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function mapDiscretizedColors(values, scale) {
|
|
139
|
+
const hasUnknown = Object.hasOwn(scale, "unknown");
|
|
140
|
+
return cloneAndFreeze(values.map(value => {
|
|
141
|
+
if (!Number.isFinite(value) && hasUnknown) return scale.unknown;
|
|
142
|
+
return scale.range[discretizedColorIndex(value, scale.thresholds)];
|
|
143
|
+
}));
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function formatBoundary(value) {
|
|
147
|
+
return Number.isInteger(value) ? String(value) : value.toFixed(1);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export function formatDiscretizedIntervals(thresholds) {
|
|
151
|
+
nondecreasing(thresholds, "Discretized color thresholds");
|
|
152
|
+
return cloneAndFreeze([
|
|
153
|
+
`< ${formatBoundary(thresholds[0])}`,
|
|
154
|
+
...thresholds.slice(0, -1).map((value, index) =>
|
|
155
|
+
`${formatBoundary(value)}–${formatBoundary(thresholds[index + 1])}`
|
|
156
|
+
),
|
|
157
|
+
`≥ ${formatBoundary(thresholds.at(-1))}`
|
|
158
|
+
]);
|
|
159
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { cloneAndFreeze } from "../../core/immutable.js";
|
|
2
|
+
|
|
3
|
+
export function isNominalValue(value) {
|
|
4
|
+
return (
|
|
5
|
+
typeof value === "string" ||
|
|
6
|
+
typeof value === "boolean" ||
|
|
7
|
+
(typeof value === "number" && Number.isFinite(value))
|
|
8
|
+
);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function validateFieldType(fieldType) {
|
|
12
|
+
if (!["quantitative", "ordinal", "temporal"].includes(fieldType)) {
|
|
13
|
+
throw new Error(`Unsupported field type "${fieldType}".`);
|
|
14
|
+
}
|
|
15
|
+
return fieldType;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function validateNominalFieldType(fieldType) {
|
|
19
|
+
if (fieldType !== "nominal") {
|
|
20
|
+
throw new Error(`Unsupported color field type "${fieldType}".`);
|
|
21
|
+
}
|
|
22
|
+
return fieldType;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function validateSemanticFieldType(fieldType) {
|
|
26
|
+
if (!["quantitative", "nominal", "ordinal", "temporal"].includes(fieldType)) {
|
|
27
|
+
throw new Error(`Unsupported semantic field type "${fieldType}".`);
|
|
28
|
+
}
|
|
29
|
+
return fieldType;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function validateField(field) {
|
|
33
|
+
if (typeof field !== "string" || field.length === 0) {
|
|
34
|
+
throw new TypeError("Encoding field must be a non-empty string.");
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function readQuantitativeField(rows, field) {
|
|
39
|
+
validateField(field);
|
|
40
|
+
return cloneAndFreeze(rows.map((row, index) => {
|
|
41
|
+
if (!Object.hasOwn(row, field) || !Number.isFinite(row[field])) {
|
|
42
|
+
throw new TypeError(
|
|
43
|
+
`Field "${field}" must contain a finite number at row ${index}.`
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
return row[field];
|
|
47
|
+
}));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function utcDate(year, month = 1, day = 1) {
|
|
51
|
+
const timestamp = Date.UTC(year, month - 1, day);
|
|
52
|
+
const date = new Date(timestamp);
|
|
53
|
+
return date.getUTCFullYear() === year &&
|
|
54
|
+
date.getUTCMonth() === month - 1 &&
|
|
55
|
+
date.getUTCDate() === day
|
|
56
|
+
? timestamp
|
|
57
|
+
: undefined;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function normalizeTemporalValue(value, field = "value", index = 0) {
|
|
61
|
+
let timestamp;
|
|
62
|
+
if (typeof value === "number") {
|
|
63
|
+
timestamp = Number.isInteger(value) && value >= 1000 && value <= 9999
|
|
64
|
+
? utcDate(value)
|
|
65
|
+
: value;
|
|
66
|
+
} else if (typeof value === "string") {
|
|
67
|
+
const year = /^(\d{4})$/.exec(value);
|
|
68
|
+
const date = /^(\d{4})[-/](\d{2})[-/](\d{2})$/.exec(value);
|
|
69
|
+
timestamp = year !== null
|
|
70
|
+
? utcDate(Number(year[1]))
|
|
71
|
+
: date !== null
|
|
72
|
+
? utcDate(Number(date[1]), Number(date[2]), Number(date[3]))
|
|
73
|
+
: Date.parse(value);
|
|
74
|
+
}
|
|
75
|
+
if (!Number.isFinite(timestamp)) {
|
|
76
|
+
throw new TypeError(
|
|
77
|
+
`Field "${field}" must contain a temporal string or finite timestamp ` +
|
|
78
|
+
`(including a valid date or four-digit year) at row ${index}.`
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
return timestamp;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function readTemporalField(rows, field) {
|
|
85
|
+
validateField(field);
|
|
86
|
+
return cloneAndFreeze(rows.map((row, index) => {
|
|
87
|
+
if (!Object.hasOwn(row, field)) {
|
|
88
|
+
throw new TypeError(
|
|
89
|
+
`Field "${field}" must contain a temporal string or finite timestamp at row ${index}.`
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
return normalizeTemporalValue(row[field], field, index);
|
|
93
|
+
}));
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function readNominalField(rows, field) {
|
|
97
|
+
validateField(field);
|
|
98
|
+
return cloneAndFreeze(rows.map((row, index) => {
|
|
99
|
+
if (!Object.hasOwn(row, field) || !isNominalValue(row[field])) {
|
|
100
|
+
throw new TypeError(
|
|
101
|
+
`Field "${field}" must contain a nominal value at row ${index}.`
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
return row[field];
|
|
105
|
+
}));
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function readScaleField(rows, field, fieldType, { allowUnknown = false } = {}) {
|
|
109
|
+
validateSemanticFieldType(fieldType);
|
|
110
|
+
if (!allowUnknown) {
|
|
111
|
+
if (fieldType === "temporal") return readTemporalField(rows, field);
|
|
112
|
+
if (["nominal", "ordinal"].includes(fieldType)) {
|
|
113
|
+
return readNominalField(rows, field);
|
|
114
|
+
}
|
|
115
|
+
return readQuantitativeField(rows, field);
|
|
116
|
+
}
|
|
117
|
+
validateField(field);
|
|
118
|
+
return cloneAndFreeze(rows.map((row, index) => {
|
|
119
|
+
if (!Object.hasOwn(row, field)) return undefined;
|
|
120
|
+
const value = row[field];
|
|
121
|
+
if (fieldType === "quantitative") {
|
|
122
|
+
return Number.isFinite(value) ? value : undefined;
|
|
123
|
+
}
|
|
124
|
+
if (["nominal", "ordinal"].includes(fieldType)) {
|
|
125
|
+
return isNominalValue(value) ? value : undefined;
|
|
126
|
+
}
|
|
127
|
+
try {
|
|
128
|
+
return normalizeTemporalValue(value, field, index);
|
|
129
|
+
} catch {
|
|
130
|
+
return undefined;
|
|
131
|
+
}
|
|
132
|
+
}));
|
|
133
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export * from "./appearance.js";
|
|
2
|
+
export * from "./color.js";
|
|
3
|
+
export * from "./continuous.js";
|
|
4
|
+
export * from "./discretized.js";
|
|
5
|
+
export * from "./definition.js";
|
|
6
|
+
export * from "./fields.js";
|
|
7
|
+
export * from "./mapping.js";
|
|
8
|
+
export * from "./ordinal.js";
|
|
9
|
+
export * from "./policies.js";
|
|
10
|
+
export * from "./transformed.js";
|
|
11
|
+
export * from "./types.js";
|
|
12
|
+
export * from "./validation.js";
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { mapLinearValues } from "./continuous.js";
|
|
2
|
+
import { mapTransformedValues } from "./transformed.js";
|
|
3
|
+
import { SCALE_ROLES, validateScaleTypeForRole } from "./types.js";
|
|
4
|
+
|
|
5
|
+
const TRANSFORMED_TYPES = new Set(["log", "pow", "sqrt", "symlog"]);
|
|
6
|
+
|
|
7
|
+
export function isTransformedScaleType(type) {
|
|
8
|
+
return TRANSFORMED_TYPES.has(type);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function mapContinuousScaleValues(values, scale) {
|
|
12
|
+
if (isTransformedScaleType(scale?.type)) {
|
|
13
|
+
validateScaleTypeForRole(scale.type, SCALE_ROLES.quantitativePosition);
|
|
14
|
+
return mapTransformedValues(values, scale.domain, scale.range, {
|
|
15
|
+
type: scale.type,
|
|
16
|
+
clamp: scale.clamp ?? false,
|
|
17
|
+
...(Object.hasOwn(scale, "unknown") ? { unknown: scale.unknown } : {}),
|
|
18
|
+
...(scale.type === "log" ? { base: scale.base } : {}),
|
|
19
|
+
...(scale.type === "pow" ? { exponent: scale.exponent } : {}),
|
|
20
|
+
...(scale.type === "symlog" ? { constant: scale.constant } : {})
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
return mapLinearValues(values, scale.domain, scale.range, {
|
|
24
|
+
clamp: scale.clamp ?? false,
|
|
25
|
+
...(Object.hasOwn(scale, "unknown") ? { unknown: scale.unknown } : {})
|
|
26
|
+
});
|
|
27
|
+
}
|