ggaction 0.0.2 → 0.0.4
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 +55 -2
- package/README.md +95 -72
- package/package.json +15 -5
- package/src/actions/boxPlots/components.js +3 -3
- package/src/actions/boxPlots/edit.js +277 -0
- package/src/actions/boxPlots/index.js +2 -0
- package/src/actions/boxPlots/materialize.js +12 -1
- package/src/actions/boxPlots/options.js +25 -14
- package/src/actions/canvas/actions.js +21 -17
- package/src/actions/composition/actions.js +247 -0
- package/src/actions/composition/index.js +1 -0
- package/src/actions/coordinates/actions.js +2 -5
- package/src/actions/data/density.js +7 -1
- package/src/actions/data/filter.js +3 -1
- package/src/actions/data/regression.js +1 -1
- package/src/actions/encodings/appearance.js +12 -1
- package/src/actions/encodings/barWidth.js +5 -3
- package/src/actions/encodings/color/continuous.js +137 -0
- package/src/actions/encodings/color/index.js +151 -0
- package/src/actions/encodings/color/layout.js +91 -0
- package/src/actions/encodings/color/policy.js +88 -0
- package/src/actions/encodings/index.js +4 -2
- package/src/actions/encodings/offset.js +91 -73
- package/src/actions/encodings/position/apply.js +66 -0
- package/src/actions/encodings/position/index.js +130 -0
- package/src/actions/encodings/position/policies/arc.js +26 -0
- package/src/actions/encodings/position/policies/bar.js +9 -5
- package/src/actions/encodings/position/policies/index.js +5 -1
- package/src/actions/encodings/position/policies/line.js +20 -3
- package/src/actions/encodings/position/resolve.js +58 -9
- package/src/actions/encodings/ranged.js +30 -8
- package/src/actions/encodings/shared.js +17 -21
- package/src/actions/encodings/strokeDash.js +1 -1
- package/src/actions/encodings/text.js +73 -0
- package/src/actions/errorBands/create.js +37 -29
- package/src/actions/errorBands/edit.js +206 -0
- package/src/actions/errorBands/index.js +9 -0
- package/src/actions/errorBands/options.js +39 -0
- package/src/actions/errorBars/create.js +50 -43
- package/src/actions/errorBars/edit.js +130 -0
- package/src/actions/errorBars/index.js +3 -0
- package/src/actions/errorBars/options.js +52 -0
- package/src/actions/facets/actions.js +162 -0
- package/src/actions/facets/derive.js +200 -0
- package/src/actions/facets/guides.js +16 -0
- package/src/actions/facets/index.js +1 -0
- package/src/actions/facets/replay.js +69 -0
- package/src/actions/guides/applicability.js +113 -0
- package/src/actions/guides/axes/axes.js +70 -16
- package/src/actions/guides/axes/edit.js +126 -0
- package/src/actions/guides/axes/index.js +12 -0
- package/src/actions/guides/axes/labels.js +22 -19
- package/src/actions/guides/axes/remove.js +60 -0
- package/src/actions/guides/axes/tickGroups.js +7 -5
- package/src/actions/guides/axes/ticks.js +10 -5
- package/src/actions/guides/axes/titles.js +8 -6
- package/src/actions/guides/grids/grid.js +144 -15
- package/src/actions/guides/grids/resolve.js +6 -14
- package/src/actions/guides/guides.js +17 -75
- package/src/actions/guides/index.js +6 -0
- package/src/actions/guides/legends/categorical/actions.js +48 -9
- package/src/actions/guides/legends/categorical/components.js +16 -16
- package/src/actions/guides/legends/categorical/index.js +14 -14
- package/src/actions/guides/legends/categorical/layout.js +7 -3
- package/src/actions/guides/legends/categorical/options.js +2 -5
- package/src/actions/guides/legends/categorical/resolve.js +2 -2
- package/src/actions/guides/legends/categorical/symbols.js +14 -14
- package/src/actions/guides/legends/continuous/common.js +7 -7
- package/src/actions/guides/legends/continuous/interval.js +6 -6
- package/src/actions/guides/legends/continuous/opacity.js +1 -1
- package/src/actions/guides/legends/edit.js +4 -26
- package/src/actions/guides/legends/focused.js +63 -0
- package/src/actions/guides/legends/index.js +4 -0
- package/src/actions/guides/legends/remove.js +46 -0
- package/src/actions/guides/legends/size.js +13 -15
- package/src/actions/guides/legends/target.js +25 -0
- package/src/actions/guides/polar/axes/facade.js +219 -0
- package/src/actions/guides/polar/axes/index.js +55 -0
- package/src/actions/guides/polar/axes/labels.js +188 -0
- package/src/actions/guides/polar/axes/lines.js +113 -0
- package/src/actions/guides/polar/axes/shared.js +112 -0
- package/src/actions/guides/polar/axes/ticks.js +164 -0
- package/src/actions/guides/polar/axes/titles.js +165 -0
- package/src/actions/guides/polar/grids.js +252 -0
- package/src/actions/guides/polar/index.js +2 -0
- package/src/actions/guides/polar/resolve.js +269 -0
- package/src/actions/guides/tickValues.js +1 -1
- package/src/actions/index.js +4 -0
- package/src/actions/intervals/resolve.js +1 -1
- package/src/actions/marks/arc/actions.js +239 -0
- package/src/actions/marks/arc/index.js +1 -0
- package/src/actions/marks/{area.js → area/actions.js} +51 -38
- package/src/actions/marks/area/index.js +4 -0
- package/src/actions/marks/bar/create.js +26 -5
- package/src/actions/marks/bar/edit.js +1 -1
- package/src/actions/marks/bar/materialize.js +25 -31
- package/src/actions/marks/index.js +12 -4
- package/src/actions/marks/lifecycle.js +27 -0
- package/src/actions/marks/line/actions.js +373 -0
- package/src/actions/marks/line/index.js +1 -0
- package/src/actions/marks/{point.js → point/actions.js} +67 -108
- package/src/actions/marks/point/index.js +1 -0
- package/src/actions/marks/rect/actions.js +165 -0
- package/src/actions/marks/rect/index.js +1 -0
- package/src/actions/marks/remove.js +214 -0
- package/src/actions/marks/{rule.js → rule/actions.js} +34 -28
- package/src/actions/marks/rule/index.js +1 -0
- package/src/actions/marks/shared.js +156 -25
- package/src/actions/marks/text/actions.js +192 -0
- package/src/actions/marks/text/index.js +1 -0
- package/src/actions/primitives/createGraphics.js +13 -6
- package/src/actions/primitives/editGraphics.js +10 -3
- package/src/actions/primitives/semantic.js +26 -3
- package/src/actions/primitives/semanticValue.js +26 -4
- package/src/actions/regression/components.js +2 -2
- package/src/actions/regression/create.js +19 -2
- package/src/actions/regression/edit.js +247 -0
- package/src/actions/regression/index.js +3 -0
- package/src/actions/regression/resolve.js +3 -1
- package/src/actions/scales/consumers/common.js +91 -0
- package/src/actions/scales/consumers/families.js +47 -0
- package/src/actions/scales/consumers/index.js +41 -0
- package/src/actions/scales/{consumers.js → consumers/seriesLayout.js} +14 -127
- package/src/actions/scales/create.js +1 -1
- package/src/actions/scales/definitions.js +15 -6
- package/src/actions/scales/edit.js +45 -22
- package/src/actions/scales/materialize.js +58 -409
- package/src/actions/selection/actions.js +38 -24
- package/src/actions/selection/index.js +2 -0
- package/src/actions/titles/actions.js +63 -2
- package/src/actions/titles/index.js +2 -0
- package/src/actions/titles/resolve.js +12 -17
- package/src/composition.js +10 -0
- package/src/core/ChartProgram.js +49 -65
- package/src/core/action.js +10 -0
- package/src/core/compositionState.js +169 -0
- package/src/core/materializationState.js +65 -0
- package/src/core/programState.js +28 -0
- package/src/core/validation.js +16 -0
- package/src/core/vocabulary.js +154 -4
- package/src/grammar/arcs.js +154 -0
- package/src/grammar/areaSeries.js +1 -1
- package/src/grammar/bars/aggregate.js +3 -3
- package/src/grammar/bars/geometry.js +3 -3
- package/src/grammar/bars/policy.js +7 -1
- package/src/grammar/coordinates.js +3 -7
- package/src/grammar/density.js +21 -2
- package/src/grammar/facets/dependencies.js +185 -0
- package/src/grammar/facets/guides.js +257 -0
- package/src/grammar/facets/index.js +122 -0
- package/src/grammar/facets/scales.js +254 -0
- package/src/grammar/interval.js +2 -96
- package/src/grammar/lineSeries.js +112 -4
- package/src/grammar/palettes.js +5 -7
- package/src/grammar/polar.js +102 -0
- package/src/grammar/polarGuides.js +243 -0
- package/src/grammar/polarLineCommands.js +43 -0
- package/src/grammar/polarPaths.js +161 -0
- package/src/grammar/positionCompatibility.js +18 -2
- package/src/grammar/rects.js +26 -0
- package/src/grammar/regression/derive.js +97 -0
- package/src/grammar/regression/index.js +22 -0
- package/src/grammar/regression/models.js +282 -0
- package/src/grammar/regression/parameters.js +106 -0
- package/src/grammar/rules.js +1 -1
- package/src/grammar/scales/color.js +4 -2
- package/src/grammar/scales/continuous.js +6 -1
- package/src/grammar/scales/fields.js +7 -0
- package/src/grammar/scales/ordinal.js +5 -2
- package/src/grammar/scales/policies.js +1 -1
- package/src/grammar/schemas/graphic.js +1 -1
- package/src/grammar/schemas/graphicBounds.js +64 -9
- package/src/grammar/schemas/graphicTree.js +44 -10
- package/src/grammar/schemas/semanticPath.js +31 -9
- package/src/grammar/seriesLayout.js +13 -2
- package/src/grammar/statistics/studentT.js +98 -0
- package/src/grammar/text.js +71 -0
- package/src/grammar/ticks.js +50 -13
- package/src/grammar/transforms.js +1 -1
- package/src/index.js +1 -0
- package/src/layout/composition.js +310 -0
- package/src/layout/facets.js +131 -0
- package/src/layout/title.js +6 -0
- package/src/materialization/bars/aggregate.js +6 -3
- package/src/materialization/bars/grouped.js +75 -37
- package/src/materialization/bars/histogram.js +1 -1
- package/src/materialization/bars/ranged.js +1 -1
- package/src/materialization/composition.js +152 -0
- package/src/materialization/compositionSnapshot.js +114 -0
- package/src/materialization/density.js +1 -1
- package/src/materialization/dependencies.js +47 -73
- package/src/materialization/encodings.js +14 -28
- package/src/materialization/facetGuides/index.js +2 -0
- package/src/materialization/facetGuides/legacyCategorical.js +125 -0
- package/src/materialization/facetGuides/placement.js +126 -0
- package/src/materialization/facetGuides/preparation.js +188 -0
- package/src/materialization/facets.js +214 -0
- package/src/materialization/graphicHierarchy.js +15 -5
- package/src/materialization/guides/resources.js +46 -0
- package/src/materialization/marks/capabilities.js +141 -0
- package/src/materialization/marks/index.js +105 -0
- package/src/materialization/marks/policies.js +87 -0
- package/src/materialization/planner.js +105 -12
- package/src/materialization/rect.js +138 -0
- package/src/materialization/rectConfig.js +38 -0
- package/src/materialization/rowEncoding.js +33 -0
- package/src/materialization/scaleGuideDependencies.js +94 -0
- package/src/materialization/scales/map.js +40 -0
- package/src/materialization/scales/policies/arc.js +37 -0
- package/src/materialization/scales/policies/bar.js +132 -0
- package/src/materialization/scales/policies/series.js +76 -0
- package/src/materialization/scales/resolve.js +326 -0
- package/src/materialization/selection/items/arc.js +29 -0
- package/src/materialization/selection/items/bar.js +255 -0
- package/src/materialization/selection/items/common.js +131 -0
- package/src/materialization/selection/items/index.js +6 -0
- package/src/materialization/selection/items/path.js +59 -0
- package/src/materialization/selection/items/point.js +55 -0
- package/src/materialization/selection/items/rect.js +23 -0
- package/src/materialization/selection/items/rule.js +25 -0
- package/src/materialization/selection/policies/arc.js +10 -0
- package/src/materialization/selection/policies/area.js +1 -1
- package/src/materialization/selection/policies/bar.js +1 -1
- package/src/materialization/selection/policies/index.js +5 -1
- package/src/materialization/selection/policies/line.js +1 -1
- package/src/materialization/selection/policies/point.js +1 -1
- package/src/materialization/selection/policies/rect.js +10 -0
- package/src/materialization/selection/policies/rule.js +1 -1
- package/src/materialization/selection/styles.js +25 -17
- package/src/materialization/text.js +156 -0
- package/src/renderers/canvas/index.js +68 -22
- package/src/renderers/canvas/text.js +14 -1
- package/types/extension.d.ts +5 -1
- package/types/index.d.ts +56 -1
- package/types/program.d.ts +664 -27
- package/src/actions/encodings/color.js +0 -329
- package/src/actions/encodings/position.js +0 -176
- package/src/actions/marks/line.js +0 -248
- package/src/grammar/regression.js +0 -578
- package/src/grammar/scales.js +0 -1
- package/src/materialization/marks.js +0 -110
- package/src/materialization/selection/items.js +0 -503
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { cloneAndFreeze } from "../../core/immutable.js";
|
|
2
|
+
|
|
3
|
+
export function requireRegressionField(field, label) {
|
|
4
|
+
if (typeof field !== "string" || field.length === 0) {
|
|
5
|
+
throw new TypeError(`${label} must be a non-empty string.`);
|
|
6
|
+
}
|
|
7
|
+
return field;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function normalizeRegressionParameters({
|
|
11
|
+
method = "linear",
|
|
12
|
+
degree,
|
|
13
|
+
span,
|
|
14
|
+
confidence,
|
|
15
|
+
interval
|
|
16
|
+
} = {}) {
|
|
17
|
+
if (!["linear", "polynomial", "loess"].includes(method)) {
|
|
18
|
+
throw new Error(`Unsupported regression method "${method}".`);
|
|
19
|
+
}
|
|
20
|
+
if (method === "loess") {
|
|
21
|
+
if (degree !== undefined) {
|
|
22
|
+
throw new Error("Regression degree requires the polynomial method.");
|
|
23
|
+
}
|
|
24
|
+
if (confidence !== undefined || interval !== undefined) {
|
|
25
|
+
throw new Error("LOESS regression does not support confidence intervals.");
|
|
26
|
+
}
|
|
27
|
+
const resolvedSpan = span ?? 0.75;
|
|
28
|
+
if (!Number.isFinite(resolvedSpan) || resolvedSpan <= 0 || resolvedSpan > 1) {
|
|
29
|
+
throw new RangeError(
|
|
30
|
+
"Regression LOESS span must be greater than zero and at most one."
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
return cloneAndFreeze({ method, span: resolvedSpan });
|
|
34
|
+
}
|
|
35
|
+
if (span !== undefined) {
|
|
36
|
+
throw new Error("Regression span requires the loess method.");
|
|
37
|
+
}
|
|
38
|
+
const resolvedConfidence = confidence ?? 0.95;
|
|
39
|
+
if (
|
|
40
|
+
!Number.isFinite(resolvedConfidence) ||
|
|
41
|
+
resolvedConfidence <= 0 ||
|
|
42
|
+
resolvedConfidence >= 1
|
|
43
|
+
) {
|
|
44
|
+
throw new RangeError("Regression confidence must be between 0 and 1.");
|
|
45
|
+
}
|
|
46
|
+
const resolvedInterval = interval ?? "mean";
|
|
47
|
+
if (!["mean", "prediction"].includes(resolvedInterval)) {
|
|
48
|
+
throw new Error(`Unsupported regression interval "${resolvedInterval}".`);
|
|
49
|
+
}
|
|
50
|
+
if (method === "polynomial") {
|
|
51
|
+
const resolvedDegree = degree ?? 2;
|
|
52
|
+
if (!Number.isInteger(resolvedDegree) || resolvedDegree < 1) {
|
|
53
|
+
throw new RangeError(
|
|
54
|
+
"Regression polynomial degree must be a positive integer."
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
return cloneAndFreeze({
|
|
58
|
+
method,
|
|
59
|
+
degree: resolvedDegree,
|
|
60
|
+
confidence: resolvedConfidence,
|
|
61
|
+
interval: resolvedInterval
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
if (degree !== undefined) {
|
|
65
|
+
throw new Error("Regression degree requires the polynomial method.");
|
|
66
|
+
}
|
|
67
|
+
return cloneAndFreeze({
|
|
68
|
+
method,
|
|
69
|
+
confidence: resolvedConfidence,
|
|
70
|
+
interval: resolvedInterval
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function validateRegressionTransform(transform) {
|
|
75
|
+
const supported = [
|
|
76
|
+
"type", "method", "x", "y", "groupBy", "confidence", "interval",
|
|
77
|
+
"degree", "span"
|
|
78
|
+
];
|
|
79
|
+
const unknown = Object.keys(transform).find(key => !supported.includes(key));
|
|
80
|
+
if (unknown !== undefined) {
|
|
81
|
+
throw new Error(`Unknown regression transform property "${unknown}".`);
|
|
82
|
+
}
|
|
83
|
+
if (transform.type !== "regression") {
|
|
84
|
+
throw new Error(`Unsupported regression transform "${transform.type}".`);
|
|
85
|
+
}
|
|
86
|
+
requireRegressionField(transform.x, "Regression x field");
|
|
87
|
+
requireRegressionField(transform.y, "Regression y field");
|
|
88
|
+
if (transform.groupBy !== undefined) {
|
|
89
|
+
requireRegressionField(transform.groupBy, "Regression groupBy field");
|
|
90
|
+
}
|
|
91
|
+
const normalized = normalizeRegressionParameters({
|
|
92
|
+
method: transform.method,
|
|
93
|
+
degree: transform.degree,
|
|
94
|
+
span: transform.span,
|
|
95
|
+
confidence: transform.confidence,
|
|
96
|
+
interval: transform.interval
|
|
97
|
+
});
|
|
98
|
+
if (normalized.method === "loess") return transform;
|
|
99
|
+
if (!Number.isFinite(transform.confidence)) {
|
|
100
|
+
throw new RangeError("Regression confidence must be between 0 and 1.");
|
|
101
|
+
}
|
|
102
|
+
if (transform.interval === undefined) {
|
|
103
|
+
throw new Error(`Unsupported regression interval "${transform.interval}".`);
|
|
104
|
+
}
|
|
105
|
+
return transform;
|
|
106
|
+
}
|
package/src/grammar/rules.js
CHANGED
|
@@ -277,8 +277,10 @@ export function validateSequentialColorRange(value) {
|
|
|
277
277
|
throw new TypeError("Sequential color range requires colors or a palette descriptor.");
|
|
278
278
|
}
|
|
279
279
|
const palette = normalizePalette(value.palette);
|
|
280
|
-
if (palette.count !== undefined) {
|
|
281
|
-
throw new
|
|
280
|
+
if (palette.count !== undefined && palette.count < 2) {
|
|
281
|
+
throw new RangeError(
|
|
282
|
+
"Sequential palette count must be an integer of at least 2."
|
|
283
|
+
);
|
|
282
284
|
}
|
|
283
285
|
return cloneAndFreeze({ palette });
|
|
284
286
|
}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { cloneAndFreeze, isPlainObject } from "../../core/immutable.js";
|
|
2
|
+
import { POLAR_POSITION_CHANNELS } from "../../core/vocabulary.js";
|
|
3
|
+
import { resolvePolarScaleRange } from "../polar.js";
|
|
2
4
|
import { niceTimeDomain } from "./temporal.js";
|
|
3
5
|
import {
|
|
4
6
|
validatePair,
|
|
@@ -66,8 +68,11 @@ export function resolveContinuousDomain({ domain, values, type, nice, zero }) {
|
|
|
66
68
|
}
|
|
67
69
|
|
|
68
70
|
export function resolveScaleRange(range, channel, bounds) {
|
|
69
|
-
const validated = validateScaleRange(range);
|
|
70
71
|
validatePositionChannel(channel);
|
|
72
|
+
if (POLAR_POSITION_CHANNELS.includes(channel)) {
|
|
73
|
+
return resolvePolarScaleRange(range, channel, bounds);
|
|
74
|
+
}
|
|
75
|
+
const validated = validateScaleRange(range);
|
|
71
76
|
if (validated !== "auto") return validated;
|
|
72
77
|
if (
|
|
73
78
|
!isPlainObject(bounds) ||
|
|
@@ -22,6 +22,13 @@ export function validateNominalFieldType(fieldType) {
|
|
|
22
22
|
return fieldType;
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
+
export function validateCategoricalFieldType(fieldType) {
|
|
26
|
+
if (!["nominal", "ordinal"].includes(fieldType)) {
|
|
27
|
+
throw new Error(`Unsupported categorical field type "${fieldType}".`);
|
|
28
|
+
}
|
|
29
|
+
return fieldType;
|
|
30
|
+
}
|
|
31
|
+
|
|
25
32
|
export function validateSemanticFieldType(fieldType) {
|
|
26
33
|
if (!["quantitative", "nominal", "ordinal", "temporal"].includes(fieldType)) {
|
|
27
34
|
throw new Error(`Unsupported semantic field type "${fieldType}".`);
|
|
@@ -151,11 +151,14 @@ export function resolveOrdinalOffsetScale({
|
|
|
151
151
|
range,
|
|
152
152
|
parentBandwidth,
|
|
153
153
|
paddingInner = 0,
|
|
154
|
-
paddingOuter = 0
|
|
154
|
+
paddingOuter = 0,
|
|
155
|
+
channel = "xOffset"
|
|
155
156
|
}) {
|
|
156
157
|
const resolvedDomain = resolveOrdinalDomain(domain, values);
|
|
157
158
|
if (!Number.isFinite(parentBandwidth) || parentBandwidth <= 0) {
|
|
158
|
-
throw new Error(
|
|
159
|
+
throw new Error(
|
|
160
|
+
`Automatic ${channel} range requires a positive ${channel[0]} bandwidth.`
|
|
161
|
+
);
|
|
159
162
|
}
|
|
160
163
|
const resolvedRange = range === "auto"
|
|
161
164
|
? validatePair([0, parentBandwidth], "Offset scale range")
|
|
@@ -3,7 +3,7 @@ import { POINT_SHAPES } from "../pointShapes.js";
|
|
|
3
3
|
import { normalizeStrokeDashPattern } from "./appearance.js";
|
|
4
4
|
|
|
5
5
|
export function validateScaleUnknown(channel, value) {
|
|
6
|
-
if (["x", "y", "xOffset"].includes(channel)) {
|
|
6
|
+
if (["x", "y", "xOffset", "yOffset"].includes(channel)) {
|
|
7
7
|
if (!Number.isFinite(value)) {
|
|
8
8
|
throw new TypeError(`Scale unknown for ${channel} must be a finite number.`);
|
|
9
9
|
}
|
|
@@ -11,7 +11,7 @@ const DRAWABLE_TYPES = new Set([
|
|
|
11
11
|
]);
|
|
12
12
|
const STRUCTURAL_TYPES = new Set(["canvas"]);
|
|
13
13
|
const GRAPHIC_PROPERTIES = Object.freeze({
|
|
14
|
-
canvas: new Set(["width", "height", "background"]),
|
|
14
|
+
canvas: new Set(["x", "y", "width", "height", "background"]),
|
|
15
15
|
collection: new Set([
|
|
16
16
|
"items",
|
|
17
17
|
"x",
|
|
@@ -11,6 +11,69 @@ function unionBounds(bounds) {
|
|
|
11
11
|
};
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
+
function cubicValue(start, control1, control2, end, t) {
|
|
15
|
+
const remaining = 1 - t;
|
|
16
|
+
return remaining ** 3 * start +
|
|
17
|
+
3 * remaining ** 2 * t * control1 +
|
|
18
|
+
3 * remaining * t ** 2 * control2 +
|
|
19
|
+
t ** 3 * end;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function cubicExtrema(start, control1, control2, end) {
|
|
23
|
+
const quadratic = 3 * (-start + 3 * control1 - 3 * control2 + end);
|
|
24
|
+
const linear = 2 * (3 * start - 6 * control1 + 3 * control2);
|
|
25
|
+
const constant = 3 * (control1 - start);
|
|
26
|
+
const epsilon = Number.EPSILON * 64 * Math.max(
|
|
27
|
+
1,
|
|
28
|
+
Math.abs(quadratic),
|
|
29
|
+
Math.abs(linear),
|
|
30
|
+
Math.abs(constant)
|
|
31
|
+
);
|
|
32
|
+
if (Math.abs(quadratic) <= epsilon) {
|
|
33
|
+
if (Math.abs(linear) <= epsilon) return [];
|
|
34
|
+
const root = -constant / linear;
|
|
35
|
+
return root > 0 && root < 1 ? [root] : [];
|
|
36
|
+
}
|
|
37
|
+
const discriminant = linear ** 2 - 4 * quadratic * constant;
|
|
38
|
+
if (discriminant < -epsilon) return [];
|
|
39
|
+
const root = Math.sqrt(Math.max(0, discriminant));
|
|
40
|
+
return [
|
|
41
|
+
(-linear - root) / (2 * quadratic),
|
|
42
|
+
(-linear + root) / (2 * quadratic)
|
|
43
|
+
].filter(value => value > 0 && value < 1);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function pathPoints(commands) {
|
|
47
|
+
const points = [];
|
|
48
|
+
let current;
|
|
49
|
+
for (const command of commands) {
|
|
50
|
+
if (command.op === "M" || command.op === "L") {
|
|
51
|
+
if ([command.x, command.y].every(Number.isFinite)) {
|
|
52
|
+
current = { x: command.x, y: command.y };
|
|
53
|
+
points.push(current);
|
|
54
|
+
}
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
if (command.op !== "C" || current === undefined) continue;
|
|
58
|
+
if (![command.x1, command.y1, command.x2, command.y2, command.x, command.y]
|
|
59
|
+
.every(Number.isFinite)) continue;
|
|
60
|
+
const roots = new Set([
|
|
61
|
+
0,
|
|
62
|
+
1,
|
|
63
|
+
...cubicExtrema(current.x, command.x1, command.x2, command.x),
|
|
64
|
+
...cubicExtrema(current.y, command.y1, command.y2, command.y)
|
|
65
|
+
]);
|
|
66
|
+
for (const t of roots) {
|
|
67
|
+
points.push({
|
|
68
|
+
x: cubicValue(current.x, command.x1, command.x2, command.x, t),
|
|
69
|
+
y: cubicValue(current.y, command.y1, command.y2, command.y, t)
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
current = { x: command.x, y: command.y };
|
|
73
|
+
}
|
|
74
|
+
return points;
|
|
75
|
+
}
|
|
76
|
+
|
|
14
77
|
function primitiveBounds(type, properties = {}) {
|
|
15
78
|
const strokeExtent = (properties.strokeWidth ?? 0) / 2;
|
|
16
79
|
if (type === "circle") {
|
|
@@ -86,15 +149,7 @@ function primitiveBounds(type, properties = {}) {
|
|
|
86
149
|
if (type === "path") {
|
|
87
150
|
const commands = properties.commands;
|
|
88
151
|
if (!Array.isArray(commands)) return undefined;
|
|
89
|
-
const points = commands
|
|
90
|
-
if (command.op === "M" || command.op === "L") return [{ x: command.x, y: command.y }];
|
|
91
|
-
if (command.op === "C") return [
|
|
92
|
-
{ x: command.x1, y: command.y1 },
|
|
93
|
-
{ x: command.x2, y: command.y2 },
|
|
94
|
-
{ x: command.x, y: command.y }
|
|
95
|
-
];
|
|
96
|
-
return [];
|
|
97
|
-
}).filter(point => Number.isFinite(point.x) && Number.isFinite(point.y));
|
|
152
|
+
const points = pathPoints(commands);
|
|
98
153
|
if (points.length === 0) return undefined;
|
|
99
154
|
return {
|
|
100
155
|
left: Math.min(...points.map(point => point.x)) - strokeExtent,
|
|
@@ -70,7 +70,7 @@ export function graphicSiblings(graphicSpec, target) {
|
|
|
70
70
|
return graphicSpec.order;
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
-
function visitNamed(graphicSpec, id,
|
|
73
|
+
function visitNamed(graphicSpec, id, visitors, path, visited) {
|
|
74
74
|
if (path.includes(id)) {
|
|
75
75
|
throw new Error(`Graphic attachment cycle includes "${id}".`);
|
|
76
76
|
}
|
|
@@ -82,29 +82,37 @@ function visitNamed(graphicSpec, id, visitor, path, visited) {
|
|
|
82
82
|
throw new Error(`Graphic "${id}" is attached more than once.`);
|
|
83
83
|
}
|
|
84
84
|
visited.add(id);
|
|
85
|
-
|
|
85
|
+
const entry = { id, object, kind: "object", depth: path.length };
|
|
86
|
+
visitors.enter?.(entry);
|
|
86
87
|
for (const item of object.items ?? []) {
|
|
87
|
-
|
|
88
|
+
visitors.item?.({
|
|
88
89
|
id: item.id,
|
|
89
90
|
object: item,
|
|
90
91
|
kind: "item",
|
|
91
92
|
ownerId: id,
|
|
93
|
+
owner: object,
|
|
92
94
|
depth: path.length + 1
|
|
93
95
|
});
|
|
94
96
|
}
|
|
95
97
|
for (const childId of object.children ?? []) {
|
|
96
|
-
visitNamed(graphicSpec, childId,
|
|
98
|
+
visitNamed(graphicSpec, childId, visitors, [...path, id], visited);
|
|
97
99
|
}
|
|
100
|
+
visitors.exit?.(entry);
|
|
98
101
|
}
|
|
99
102
|
|
|
100
|
-
export function
|
|
103
|
+
export function walkGraphicTreeEvents(graphicSpec, visitors) {
|
|
101
104
|
requireGraphicSpec(graphicSpec);
|
|
102
|
-
if (
|
|
103
|
-
throw new TypeError("
|
|
105
|
+
if (!isPlainObject(visitors)) {
|
|
106
|
+
throw new TypeError("walkGraphicTreeEvents requires visitor callbacks.");
|
|
107
|
+
}
|
|
108
|
+
for (const name of ["enter", "item", "exit"]) {
|
|
109
|
+
if (visitors[name] !== undefined && typeof visitors[name] !== "function") {
|
|
110
|
+
throw new TypeError(`walkGraphicTreeEvents ${name} must be a function.`);
|
|
111
|
+
}
|
|
104
112
|
}
|
|
105
113
|
const visited = new Set();
|
|
106
114
|
for (const id of graphicSpec.order) {
|
|
107
|
-
visitNamed(graphicSpec, id,
|
|
115
|
+
visitNamed(graphicSpec, id, visitors, [], visited);
|
|
108
116
|
}
|
|
109
117
|
const orphan = Object.keys(graphicSpec.objects).find(id => !visited.has(id));
|
|
110
118
|
if (orphan !== undefined) {
|
|
@@ -112,12 +120,38 @@ export function walkGraphicTree(graphicSpec, visitor) {
|
|
|
112
120
|
}
|
|
113
121
|
}
|
|
114
122
|
|
|
123
|
+
export function walkGraphicTree(graphicSpec, visitor) {
|
|
124
|
+
if (typeof visitor !== "function") {
|
|
125
|
+
throw new TypeError("walkGraphicTree requires a visitor function.");
|
|
126
|
+
}
|
|
127
|
+
walkGraphicTreeEvents(graphicSpec, {
|
|
128
|
+
enter: visitor,
|
|
129
|
+
item: visitor
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
|
|
115
133
|
export function walkGraphicDrawOrder(graphicSpec, visitor) {
|
|
116
|
-
|
|
117
|
-
|
|
134
|
+
walkGraphicTreeEvents(graphicSpec, { enter: visitor });
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export function findOrderedGraphicsByType(graphicSpec, type) {
|
|
138
|
+
requireGraphicSpec(graphicSpec);
|
|
139
|
+
return graphicSpec.order.flatMap(id => {
|
|
140
|
+
const object = graphicSpec.objects[id];
|
|
141
|
+
return object?.type === type ? [{ id, object }] : [];
|
|
118
142
|
});
|
|
119
143
|
}
|
|
120
144
|
|
|
145
|
+
export function requireSingleOrderedGraphicByType(graphicSpec, type) {
|
|
146
|
+
const matches = findOrderedGraphicsByType(graphicSpec, type);
|
|
147
|
+
if (matches.length !== 1) {
|
|
148
|
+
throw new Error(
|
|
149
|
+
`graphicSpec must contain exactly one ordered ${type}.`
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
return matches[0];
|
|
153
|
+
}
|
|
154
|
+
|
|
121
155
|
export function collectGraphicSubtreeIds(graphicSpec, target) {
|
|
122
156
|
const found = requireGraphic(graphicSpec, target);
|
|
123
157
|
if (found.kind === "item") {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { USER_ID_SOURCE } from "../../core/identifiers.js";
|
|
2
2
|
import {
|
|
3
|
+
CARTESIAN_POSITION_CHANNELS,
|
|
3
4
|
ENCODING_CHANNELS,
|
|
4
|
-
POSITION_CHANNELS,
|
|
5
5
|
SCALED_ENCODING_CHANNELS
|
|
6
6
|
} from "../../core/vocabulary.js";
|
|
7
7
|
|
|
@@ -20,7 +20,9 @@ const ENCODING_PATHS = Object.freeze([
|
|
|
20
20
|
"encoding.x.bin.boundaries",
|
|
21
21
|
"encoding.color.layout",
|
|
22
22
|
"encoding.color.aggregate",
|
|
23
|
-
|
|
23
|
+
"encoding.theta.aggregate",
|
|
24
|
+
"encoding.text.format",
|
|
25
|
+
...CARTESIAN_POSITION_CHANNELS.flatMap(channel => [
|
|
24
26
|
`encoding.${channel}.aggregate`,
|
|
25
27
|
`encoding.${channel}.stack`
|
|
26
28
|
])
|
|
@@ -38,6 +40,7 @@ const ENTITY_PATHS = Object.freeze({
|
|
|
38
40
|
),
|
|
39
41
|
properties: new Set([
|
|
40
42
|
"data",
|
|
43
|
+
"source",
|
|
41
44
|
"coordinate",
|
|
42
45
|
"transform",
|
|
43
46
|
"mark.type",
|
|
@@ -65,6 +68,12 @@ const GUIDE_PATHS = new Set([
|
|
|
65
68
|
"axis.y.scale",
|
|
66
69
|
"axis.y.coordinate",
|
|
67
70
|
"axis.y.title",
|
|
71
|
+
"axis.theta.scale",
|
|
72
|
+
"axis.theta.coordinate",
|
|
73
|
+
"axis.theta.title",
|
|
74
|
+
"axis.radius.scale",
|
|
75
|
+
"axis.radius.coordinate",
|
|
76
|
+
"axis.radius.title",
|
|
68
77
|
"legend.color.scale",
|
|
69
78
|
"legend.color.title",
|
|
70
79
|
"legend.size.scale",
|
|
@@ -77,10 +86,22 @@ const GUIDE_PATHS = new Set([
|
|
|
77
86
|
"grid.horizontal.scale",
|
|
78
87
|
"grid.horizontal.coordinate",
|
|
79
88
|
"grid.vertical.scale",
|
|
80
|
-
"grid.vertical.coordinate"
|
|
89
|
+
"grid.vertical.coordinate",
|
|
90
|
+
"grid.theta.scale",
|
|
91
|
+
"grid.theta.coordinate",
|
|
92
|
+
"grid.radial.scale",
|
|
93
|
+
"grid.radial.coordinate"
|
|
81
94
|
]);
|
|
82
95
|
|
|
83
96
|
const GUIDE_REMOVABLE_CONTAINERS = new Set([
|
|
97
|
+
"axis.x",
|
|
98
|
+
"axis.y",
|
|
99
|
+
"axis.theta",
|
|
100
|
+
"axis.radius",
|
|
101
|
+
"grid.horizontal",
|
|
102
|
+
"grid.vertical",
|
|
103
|
+
"grid.theta",
|
|
104
|
+
"grid.radial",
|
|
84
105
|
"legend.color",
|
|
85
106
|
"legend.size",
|
|
86
107
|
"legend.opacity",
|
|
@@ -99,14 +120,15 @@ export function parseSemanticPath(property, { allowContainer = false } = {}) {
|
|
|
99
120
|
}
|
|
100
121
|
|
|
101
122
|
if (allowContainer) {
|
|
102
|
-
const
|
|
103
|
-
new RegExp(`^dataset\\[(${USER_ID_SOURCE})\\]$`)
|
|
123
|
+
const entityMatch = property.match(
|
|
124
|
+
new RegExp(`^(dataset|layer)\\[(${USER_ID_SOURCE})\\]$`)
|
|
104
125
|
);
|
|
105
|
-
if (
|
|
126
|
+
if (entityMatch) {
|
|
127
|
+
const [, kind, id] = entityMatch;
|
|
106
128
|
return Object.freeze({
|
|
107
|
-
kind
|
|
108
|
-
id
|
|
109
|
-
collection:
|
|
129
|
+
kind,
|
|
130
|
+
id,
|
|
131
|
+
collection: ENTITY_PATHS[kind].collection,
|
|
110
132
|
path: Object.freeze([])
|
|
111
133
|
});
|
|
112
134
|
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { cloneAndFreeze } from "../core/immutable.js";
|
|
2
2
|
import { COLOR_LAYOUTS } from "../core/vocabulary.js";
|
|
3
3
|
|
|
4
|
+
export const DEFAULT_SERIES_BASELINE = 0;
|
|
5
|
+
|
|
4
6
|
export function validateColorLayout(layout) {
|
|
5
7
|
if (!COLOR_LAYOUTS.includes(layout)) {
|
|
6
8
|
throw new Error(`Unsupported color layout "${layout}".`);
|
|
@@ -14,7 +16,11 @@ function validateValues(values) {
|
|
|
14
16
|
}
|
|
15
17
|
}
|
|
16
18
|
|
|
17
|
-
export function layoutSeriesPartition(
|
|
19
|
+
export function layoutSeriesPartition(
|
|
20
|
+
values,
|
|
21
|
+
layout,
|
|
22
|
+
{ baseline = DEFAULT_SERIES_BASELINE } = {}
|
|
23
|
+
) {
|
|
18
24
|
validateValues(values);
|
|
19
25
|
validateColorLayout(layout);
|
|
20
26
|
if (!Number.isFinite(baseline)) {
|
|
@@ -66,7 +72,12 @@ export function resolveSeriesLayoutDomainValues(partitions, layout) {
|
|
|
66
72
|
for (const partition of partitions) validateValues(partition);
|
|
67
73
|
|
|
68
74
|
if (layout === "fill") return [0, 1];
|
|
69
|
-
if (layout === "group" || layout === "overlay")
|
|
75
|
+
if (layout === "group" || layout === "overlay") {
|
|
76
|
+
return partitions.flatMap(partition => [
|
|
77
|
+
DEFAULT_SERIES_BASELINE,
|
|
78
|
+
...partition
|
|
79
|
+
]);
|
|
80
|
+
}
|
|
70
81
|
return partitions.flatMap(partition =>
|
|
71
82
|
layoutSeriesPartition(partition, layout).flatMap(segment => [
|
|
72
83
|
segment.start,
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
const LANCZOS = Object.freeze([
|
|
2
|
+
676.5203681218851,
|
|
3
|
+
-1259.1392167224028,
|
|
4
|
+
771.3234287776531,
|
|
5
|
+
-176.6150291621406,
|
|
6
|
+
12.507343278686905,
|
|
7
|
+
-0.13857109526572012,
|
|
8
|
+
9.984369578019572e-6,
|
|
9
|
+
1.5056327351493116e-7
|
|
10
|
+
]);
|
|
11
|
+
|
|
12
|
+
function logGamma(value) {
|
|
13
|
+
if (value < 0.5) {
|
|
14
|
+
return Math.log(Math.PI) - Math.log(Math.sin(Math.PI * value)) -
|
|
15
|
+
logGamma(1 - value);
|
|
16
|
+
}
|
|
17
|
+
const shifted = value - 1;
|
|
18
|
+
let sum = 0.9999999999998099;
|
|
19
|
+
for (const [index, coefficient] of LANCZOS.entries()) {
|
|
20
|
+
sum += coefficient / (shifted + index + 1);
|
|
21
|
+
}
|
|
22
|
+
const base = shifted + LANCZOS.length - 0.5;
|
|
23
|
+
return 0.5 * Math.log(2 * Math.PI) +
|
|
24
|
+
(shifted + 0.5) * Math.log(base) - base + Math.log(sum);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function betaContinuedFraction(a, b, value) {
|
|
28
|
+
const maximumIterations = 200;
|
|
29
|
+
const epsilon = 3e-14;
|
|
30
|
+
const floor = 1e-300;
|
|
31
|
+
const sum = a + b;
|
|
32
|
+
let c = 1;
|
|
33
|
+
let d = 1 - sum * value / (a + 1);
|
|
34
|
+
if (Math.abs(d) < floor) d = floor;
|
|
35
|
+
d = 1 / d;
|
|
36
|
+
let result = d;
|
|
37
|
+
|
|
38
|
+
for (let iteration = 1; iteration <= maximumIterations; iteration += 1) {
|
|
39
|
+
const even = 2 * iteration;
|
|
40
|
+
let coefficient = iteration * (b - iteration) * value /
|
|
41
|
+
((a + even - 1) * (a + even));
|
|
42
|
+
d = 1 + coefficient * d;
|
|
43
|
+
if (Math.abs(d) < floor) d = floor;
|
|
44
|
+
c = 1 + coefficient / c;
|
|
45
|
+
if (Math.abs(c) < floor) c = floor;
|
|
46
|
+
d = 1 / d;
|
|
47
|
+
result *= d * c;
|
|
48
|
+
|
|
49
|
+
coefficient = -(a + iteration) * (sum + iteration) * value /
|
|
50
|
+
((a + even) * (a + even + 1));
|
|
51
|
+
d = 1 + coefficient * d;
|
|
52
|
+
if (Math.abs(d) < floor) d = floor;
|
|
53
|
+
c = 1 + coefficient / c;
|
|
54
|
+
if (Math.abs(c) < floor) c = floor;
|
|
55
|
+
d = 1 / d;
|
|
56
|
+
const delta = d * c;
|
|
57
|
+
result *= delta;
|
|
58
|
+
if (Math.abs(delta - 1) <= epsilon) return result;
|
|
59
|
+
}
|
|
60
|
+
throw new Error("Student-t calculation did not converge.");
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function regularizedIncompleteBeta(value, a, b) {
|
|
64
|
+
if (value === 0 || value === 1) return value;
|
|
65
|
+
const factor = Math.exp(
|
|
66
|
+
logGamma(a + b) - logGamma(a) - logGamma(b) +
|
|
67
|
+
a * Math.log(value) + b * Math.log1p(-value)
|
|
68
|
+
);
|
|
69
|
+
if (value < (a + 1) / (a + b + 2)) {
|
|
70
|
+
return factor * betaContinuedFraction(a, b, value) / a;
|
|
71
|
+
}
|
|
72
|
+
return 1 - factor * betaContinuedFraction(b, a, 1 - value) / b;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function studentTCdf(value, degreesOfFreedom) {
|
|
76
|
+
if (value === 0) return 0.5;
|
|
77
|
+
const ratio = degreesOfFreedom /
|
|
78
|
+
(degreesOfFreedom + value * value);
|
|
79
|
+
const tail = regularizedIncompleteBeta(
|
|
80
|
+
ratio,
|
|
81
|
+
degreesOfFreedom / 2,
|
|
82
|
+
0.5
|
|
83
|
+
) / 2;
|
|
84
|
+
return value > 0 ? 1 - tail : tail;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function studentTCriticalValue(level, degreesOfFreedom) {
|
|
88
|
+
const probability = (1 + level) / 2;
|
|
89
|
+
let low = 0;
|
|
90
|
+
let high = 1;
|
|
91
|
+
while (studentTCdf(high, degreesOfFreedom) < probability) high *= 2;
|
|
92
|
+
for (let iteration = 0; iteration < 100; iteration += 1) {
|
|
93
|
+
const midpoint = (low + high) / 2;
|
|
94
|
+
if (studentTCdf(midpoint, degreesOfFreedom) < probability) low = midpoint;
|
|
95
|
+
else high = midpoint;
|
|
96
|
+
}
|
|
97
|
+
return (low + high) / 2;
|
|
98
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { cloneAndFreeze } from "../core/immutable.js";
|
|
2
|
+
import { validateConcreteGraphicValue } from "./schemas/concreteGraphic.js";
|
|
3
|
+
|
|
4
|
+
export const DEFAULT_TEXT_MARK = cloneAndFreeze({
|
|
5
|
+
fill: "#334155",
|
|
6
|
+
opacity: 1,
|
|
7
|
+
fontSize: 12,
|
|
8
|
+
fontFamily: "sans-serif",
|
|
9
|
+
fontWeight: "normal",
|
|
10
|
+
align: "left",
|
|
11
|
+
baseline: "alphabetic",
|
|
12
|
+
rotation: 0,
|
|
13
|
+
dx: 0,
|
|
14
|
+
dy: 0
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
const FIXED_FORMAT = /^\.(\d{1,2})f$/;
|
|
18
|
+
|
|
19
|
+
export function validateTextFormat(format) {
|
|
20
|
+
if (format === undefined || format === "auto") return format ?? "auto";
|
|
21
|
+
if (typeof format !== "string" || !FIXED_FORMAT.test(format)) {
|
|
22
|
+
throw new Error(
|
|
23
|
+
'Text format must be "auto" or a fixed-decimal token such as ".1f".'
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
const decimals = Number(format.match(FIXED_FORMAT)[1]);
|
|
27
|
+
if (decimals > 12) {
|
|
28
|
+
throw new RangeError("Text fixed-decimal format supports at most 12 decimals.");
|
|
29
|
+
}
|
|
30
|
+
return format;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function formatTextValue(value, format = "auto") {
|
|
34
|
+
const resolved = validateTextFormat(format);
|
|
35
|
+
if (value === undefined || value === null) return undefined;
|
|
36
|
+
if (resolved === "auto") {
|
|
37
|
+
const text = String(value);
|
|
38
|
+
return text.length === 0 ? undefined : text;
|
|
39
|
+
}
|
|
40
|
+
if (!Number.isFinite(value)) {
|
|
41
|
+
throw new TypeError(`Text format "${resolved}" requires a finite number.`);
|
|
42
|
+
}
|
|
43
|
+
return value.toFixed(Number(resolved.match(FIXED_FORMAT)[1]));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function normalizeTextMarkConfig(options, base = DEFAULT_TEXT_MARK) {
|
|
47
|
+
const config = { ...base };
|
|
48
|
+
const mapping = {
|
|
49
|
+
fill: "fill",
|
|
50
|
+
opacity: "opacity",
|
|
51
|
+
fontSize: "fontSize",
|
|
52
|
+
fontFamily: "fontFamily",
|
|
53
|
+
fontWeight: "fontWeight",
|
|
54
|
+
align: "textAlign",
|
|
55
|
+
baseline: "textBaseline",
|
|
56
|
+
rotation: "rotation"
|
|
57
|
+
};
|
|
58
|
+
for (const [option, property] of Object.entries(mapping)) {
|
|
59
|
+
if (!Object.hasOwn(options, option)) continue;
|
|
60
|
+
validateConcreteGraphicValue("text", property, options[option]);
|
|
61
|
+
config[option] = options[option];
|
|
62
|
+
}
|
|
63
|
+
for (const property of ["dx", "dy"]) {
|
|
64
|
+
if (!Object.hasOwn(options, property)) continue;
|
|
65
|
+
if (!Number.isFinite(options[property])) {
|
|
66
|
+
throw new TypeError(`Text ${property} must be a finite number.`);
|
|
67
|
+
}
|
|
68
|
+
config[property] = options[property];
|
|
69
|
+
}
|
|
70
|
+
return cloneAndFreeze(config);
|
|
71
|
+
}
|