ggaction 0.0.16 → 0.0.18
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 +66 -0
- package/README.md +3 -2
- package/knowledge/action-card.schema.json +1 -1
- package/knowledge/action-cards.json +1 -1
- package/knowledge/intent-taxonomy.json +1 -1
- package/knowledge/mcp-resources.json +1 -1
- package/knowledge/task-resolver.js +1 -1
- package/package.json +10 -4
- package/src/BasicChartProgram.js +1 -1
- package/src/accessibility.js +3 -2
- package/src/actions/boxPlots/create.js +14 -4
- package/src/actions/boxPlots/edit.js +39 -33
- package/src/actions/boxPlots/materialize.js +7 -3
- package/src/actions/boxPlots/options.js +37 -11
- package/src/actions/canvas/actions.js +70 -95
- package/src/actions/canvas/fitting.js +16 -1
- package/src/actions/canvas/index.js +3 -0
- package/src/actions/canvas/plotBorder.js +28 -0
- package/src/actions/charts/area.js +2 -2
- package/src/actions/charts/bar.js +34 -7
- package/src/actions/charts/ecdf.js +4 -18
- package/src/actions/charts/endpoints.js +4 -18
- package/src/actions/charts/heatmap.js +4 -4
- package/src/actions/charts/index.js +2 -0
- package/src/actions/charts/interval-regression.js +2 -2
- package/src/actions/charts/line.js +1 -1
- package/src/actions/charts/pie.js +3 -47
- package/src/actions/charts/radial.js +34 -12
- package/src/actions/charts/scatter.js +16 -2
- package/src/actions/charts/shared.js +2 -2
- package/src/actions/charts/text.js +35 -0
- package/src/actions/composition/actions.js +6 -2
- package/src/actions/data/bin.js +1 -1
- package/src/actions/data/box.js +2 -1
- package/src/actions/data/create.js +18 -5
- package/src/actions/data/density.js +1 -1
- package/src/actions/data/derived.js +15 -1
- package/src/actions/data/edit.js +20 -7
- package/src/actions/data/filter.js +15 -4
- package/src/actions/data/index.js +3 -0
- package/src/actions/data/interval.js +2 -0
- package/src/actions/data/regression.js +6 -2
- package/src/actions/data/revise.js +37 -5
- package/src/actions/data/shared.js +33 -4
- package/src/actions/data/sort.js +21 -0
- package/src/actions/data/stack.js +3 -1
- package/src/actions/data/summary.js +1 -1
- package/src/actions/encodings/angle.js +4 -1
- package/src/actions/encodings/appearance.js +43 -31
- package/src/actions/encodings/barWidth.js +36 -11
- package/src/actions/encodings/channels.js +0 -1
- package/src/actions/encodings/color/continuous.js +5 -2
- package/src/actions/encodings/color/index.js +5 -2
- package/src/actions/encodings/color/policy.js +10 -3
- package/src/actions/encodings/density/resolve.js +1 -6
- package/src/actions/encodings/offset.js +4 -1
- package/src/actions/encodings/position/policies/arc.js +12 -10
- package/src/actions/encodings/position/policies/area.js +3 -3
- package/src/actions/encodings/position/policies/bar.js +15 -6
- package/src/actions/encodings/position/policies/line.js +17 -6
- package/src/actions/encodings/position/resolve.js +9 -3
- package/src/actions/encodings/ranged.js +23 -1
- package/src/actions/encodings/remove.js +3 -2
- package/src/actions/encodings/ruleAppearance.js +4 -1
- package/src/actions/encodings/seriesLayout.js +1 -0
- package/src/actions/encodings/stroke.js +4 -1
- package/src/actions/encodings/strokeDash.js +6 -1
- package/src/actions/encodings/text.js +12 -4
- package/src/actions/errorBands/create.js +2 -2
- package/src/actions/errorBands/edit.js +2 -1
- package/src/actions/facets/actions.js +158 -240
- package/src/actions/gradientPlots/resolve.js +2 -17
- package/src/actions/guides/applicability.js +1 -1
- package/src/actions/guides/axes/axis.js +16 -4
- package/src/actions/guides/axes/edit.js +4 -4
- package/src/actions/guides/axes/labels.js +120 -46
- package/src/actions/guides/axes/lines.js +6 -5
- package/src/actions/guides/axes/parallel/lifecycle.js +1 -1
- package/src/actions/guides/axes/parallel/policy.js +8 -7
- package/src/actions/guides/axes/remove.js +3 -2
- package/src/actions/guides/axes/tickGroups.js +5 -2
- package/src/actions/guides/axes/ticks.js +6 -3
- package/src/actions/guides/axes/titles.js +71 -26
- package/src/actions/guides/basic.js +2 -0
- package/src/actions/guides/facadeLegend.js +10 -2
- package/src/actions/guides/grids/grid.js +7 -1
- package/src/actions/guides/grids/resolve.js +59 -12
- package/src/actions/guides/legends/blocks.js +3 -1
- package/src/actions/guides/legends/categorical/actions.js +70 -17
- package/src/actions/guides/legends/categorical/components.js +12 -8
- package/src/actions/guides/legends/categorical/index.js +6 -0
- package/src/actions/guides/legends/categorical/layout.js +37 -8
- package/src/actions/guides/legends/categorical/options.js +31 -20
- package/src/actions/guides/legends/categorical/resolve.js +2 -2
- package/src/actions/guides/legends/categorical/symbols.js +18 -12
- package/src/actions/guides/legends/categorical/validation.js +1 -6
- package/src/actions/guides/legends/continuous/colorSymbols.js +1 -0
- package/src/actions/guides/legends/continuous/common.js +1 -447
- package/src/actions/guides/legends/continuous/gradient.js +47 -34
- package/src/actions/guides/legends/continuous/interval.js +4 -4
- package/src/actions/guides/legends/continuous/opacity.js +19 -12
- package/src/actions/guides/legends/edit.js +1 -604
- package/src/actions/guides/legends/focused.js +2 -2
- package/src/actions/guides/legends/group.js +1 -0
- package/src/actions/guides/legends/lane.js +1 -516
- package/src/actions/guides/legends/remove.js +3 -0
- package/src/actions/guides/legends/sampling.js +1 -1
- package/src/actions/guides/legends/size.js +51 -43
- package/src/actions/guides/legends/strokeWidth.js +11 -29
- package/src/actions/guides/legends/target.js +3 -3
- package/src/actions/guides/legends/transition.js +21 -7
- package/src/actions/guides/polar/axes/labels.js +4 -2
- package/src/actions/guides/polar/axes/titles.js +7 -5
- package/src/actions/guides/polar/resolve.js +6 -14
- package/src/actions/index.js +16 -16
- package/src/actions/marks/arc/actions.js +3 -10
- package/src/actions/marks/area/actions.js +8 -5
- package/src/actions/marks/area/materialize.js +8 -9
- package/src/actions/marks/bar/create.js +17 -4
- package/src/actions/marks/bar/edit.js +18 -7
- package/src/actions/marks/bar/materialize.js +9 -6
- package/src/actions/marks/line/actions.js +11 -4
- package/src/actions/marks/line/materialize.js +9 -5
- package/src/actions/marks/point/create.js +8 -1
- package/src/actions/marks/point/edit.js +13 -6
- package/src/actions/marks/point/materialize.js +4 -2
- package/src/actions/marks/rect/actions.js +26 -11
- package/src/actions/marks/remove.js +2 -0
- package/src/actions/marks/rule/actions.js +31 -12
- package/src/actions/marks/text/actions.js +42 -11
- package/src/actions/marks/text/layout.js +3 -0
- package/src/actions/marks/tick/actions.js +23 -8
- package/src/actions/primitives/semanticAction.js +37 -3
- package/src/actions/primitives/semanticValidation/dataset.js +13 -1
- package/src/actions/primitives/semanticValidation/layer.js +26 -2
- package/src/actions/primitives/semanticValidation/scale.js +6 -0
- package/src/actions/regression/create.js +26 -5
- package/src/actions/regression/edit.js +50 -15
- package/src/actions/regression/follow.js +22 -0
- package/src/actions/regression/resolve.js +2 -6
- package/src/actions/scales/channels.js +20 -15
- package/src/actions/scales/consumers/common.js +2 -1
- package/src/actions/scales/consumers/families.js +16 -5
- package/src/actions/scales/create.js +10 -4
- package/src/actions/scales/definitions.js +1 -425
- package/src/actions/scales/edit.js +3 -3
- package/src/actions/scales/editPolicy.js +9 -3
- package/src/actions/scales/materialize.js +1 -1
- package/src/actions/scales/parallel.js +1 -1
- package/src/actions/scales/patch.js +1 -1
- package/src/actions/scales/preview.js +49 -2
- package/src/actions/scales/quantitativeColor.js +3 -3
- package/src/actions/selection/actions.js +1 -13
- package/src/actions/theme/reconcile.js +83 -27
- package/src/actions/theme/state.js +12 -0
- package/src/actions/titles/actions.js +5 -18
- package/src/actions/titles/resolve.js +41 -19
- package/src/actions/violinPlots/edit.js +3 -20
- package/src/core/action.js +3 -2
- package/src/core/compositionState.js +4 -1
- package/src/core/font.js +9 -2
- package/src/core/optionRequirements.js +1 -0
- package/src/core/programState.js +5 -1
- package/src/core/resourceReferences.js +3 -0
- package/src/core/textMetricProfile.js +3 -2
- package/src/core/textMetrics.js +41 -4
- package/src/core/validation.js +26 -0
- package/src/core/vocabulary.js +4 -0
- package/src/grammar/aggregate.js +4 -18
- package/src/grammar/arcs.js +1 -386
- package/src/grammar/areaPaint.js +10 -0
- package/src/grammar/areaSeries.js +18 -10
- package/src/grammar/bars/geometry.js +30 -3
- package/src/grammar/bars/policy.js +12 -1
- package/src/grammar/bin.js +41 -22
- package/src/grammar/bin2d.js +1 -6
- package/src/grammar/boxPlot.js +56 -9
- package/src/grammar/categoryOrder.js +1 -6
- package/src/grammar/complete.js +15 -19
- package/src/grammar/computed.js +92 -25
- package/src/grammar/curveCommands.js +29 -16
- package/src/grammar/datasetSchema.js +382 -0
- package/src/grammar/density.js +53 -5
- package/src/grammar/displayLabels.js +28 -0
- package/src/grammar/ecdf.js +2 -9
- package/src/grammar/facets/dependencies.js +6 -2
- package/src/grammar/facets/guides.js +5 -0
- package/src/grammar/facets/index.js +2 -5
- package/src/grammar/filter.js +133 -39
- package/src/grammar/fold.js +2 -12
- package/src/grammar/gradientProfile.js +1 -6
- package/src/grammar/impute.js +21 -48
- package/src/grammar/interval.js +34 -10
- package/src/grammar/itemMissing.js +59 -0
- package/src/grammar/legendGradient.js +18 -0
- package/src/grammar/legendGroups.js +1 -0
- package/src/grammar/legendTitleStyle.js +22 -0
- package/src/grammar/lineSeries.js +32 -17
- package/src/grammar/markLabels.js +5 -2
- package/src/grammar/normalize.js +4 -54
- package/src/grammar/numeric.js +1 -290
- package/src/grammar/parallelCoordinates.js +1 -6
- package/src/grammar/parseTime.js +41 -0
- package/src/grammar/pathCommands.js +11 -4
- package/src/grammar/polar.js +1 -184
- package/src/grammar/positionCompatibility.js +3 -3
- package/src/grammar/regression/derive.js +63 -16
- package/src/grammar/regression/index.js +1 -0
- package/src/grammar/regression/models.js +116 -59
- package/src/grammar/regression/parameters.js +69 -6
- package/src/grammar/roundedRect.js +42 -54
- package/src/grammar/scales/appearance.js +1 -206
- package/src/grammar/scales/color.js +1 -382
- package/src/grammar/scales/colorConsumers.js +5 -2
- package/src/grammar/scales/definition.js +1 -190
- package/src/grammar/scales/discretized.js +1 -195
- package/src/grammar/scales/size.js +1 -456
- package/src/grammar/scales/temporal.js +2 -13
- package/src/grammar/scales/transformed.js +1 -383
- package/src/grammar/scales/types.js +1 -1
- package/src/grammar/scales/validation.js +1 -66
- package/src/grammar/schemas/concreteGraphic.js +11 -0
- package/src/grammar/schemas/graphic.js +3 -0
- package/src/grammar/schemas/semanticPath.js +5 -3
- package/src/grammar/sort.js +88 -0
- package/src/grammar/stack.js +2 -10
- package/src/grammar/statisticalReference.js +1 -6
- package/src/grammar/summary.js +120 -45
- package/src/grammar/text.js +19 -0
- package/src/grammar/timeUnit.js +16 -7
- package/src/grammar/transformKeys.js +47 -0
- package/src/grammar/transformTopology.js +1 -0
- package/src/grammar/transforms.js +25 -12
- package/src/grammar/valueFormat.js +17 -5
- package/src/grammar/weightedStatistics.js +1 -6
- package/src/grammar/window.js +4 -58
- package/src/inspection.js +495 -0
- package/src/inspectionDescriptors.js +3 -0
- package/src/layout/canvas.js +1 -110
- package/src/layout/facets.js +1 -244
- package/src/layout/labels.js +1 -0
- package/src/layout/legend.js +13 -0
- package/src/layout/legendGroup.js +1 -0
- package/src/layout/legendItems.js +59 -15
- package/src/layout/legendLane.js +1 -441
- package/src/layout/legendText.js +1 -0
- package/src/layout/title.js +35 -13
- package/src/materialization/bars/aggregate.js +11 -5
- package/src/materialization/bars/histogram.js +4 -1
- package/src/materialization/bars/ranged.js +19 -6
- package/src/materialization/bars/resolve.js +26 -3
- package/src/materialization/coordinateBounds.js +8 -0
- package/src/materialization/dependencies.js +4 -44
- package/src/materialization/facetGuides/layout.js +1 -0
- package/src/materialization/facetGuides/placement.js +19 -8
- package/src/materialization/facetGuides/preparation.js +15 -7
- package/src/materialization/facetHeaders.js +1 -347
- package/src/materialization/facets.js +1 -185
- package/src/materialization/graphicHierarchy.js +1 -1
- package/src/materialization/guides/layout.js +1 -57
- package/src/materialization/guides/resources.js +1 -123
- package/src/materialization/layout.js +53 -0
- package/src/materialization/legendGroups.js +1 -0
- package/src/materialization/legendText.js +1 -0
- package/src/materialization/legends.js +1 -1
- package/src/materialization/marks/capabilities.js +5 -6
- package/src/materialization/rect.js +1 -1
- package/src/materialization/revisionIdentity.js +19 -0
- package/src/materialization/scaleGuideDependencies.js +1 -1
- package/src/materialization/scales/map.js +3 -0
- package/src/materialization/scales/policies/arc.js +8 -7
- package/src/materialization/scales/policies/bar.js +19 -7
- package/src/materialization/scales/policies/series.js +2 -1
- package/src/materialization/scales/resolve.js +72 -7
- package/src/materialization/selection/items/arc.js +1 -1
- package/src/materialization/selection/items/bar.js +7 -6
- package/src/materialization/selection/items/path.js +7 -6
- package/src/materialization/selection/items/rect.js +2 -2
- package/src/materialization/text.js +17 -6
- package/src/materialization/textLines.js +15 -0
- package/src/persistence/codec.js +37 -6
- package/src/persistence/validation.js +47 -0
- package/src/persistence.js +94 -8
- package/src/renderers/canvas/circle.js +6 -3
- package/src/renderers/canvas/fill.js +7 -1
- package/src/renderers/canvas/index.js +2 -1
- package/src/renderers/canvas/line.js +1 -0
- package/src/renderers/canvas/path.js +1 -1
- package/src/renderers/canvas/rect.js +11 -8
- package/src/renderers/canvas/stroke.js +2 -1
- package/src/renderers/canvas/text.js +5 -3
- package/src/renderers/svg.js +4 -1
- package/src/selectors/layers.js +19 -0
- package/src/theme/defaults.js +8 -2
- package/src/version.js +1 -1
- package/types/basic.d.ts +1 -0
- package/types/index.d.ts +7 -0
- package/types/inspection.d.ts +17 -0
- package/types/program.d.ts +494 -112
package/src/grammar/filter.js
CHANGED
|
@@ -39,33 +39,62 @@ function validateRange(range) {
|
|
|
39
39
|
throw new TypeError("Filter range must be a plain object.");
|
|
40
40
|
}
|
|
41
41
|
const unknown = Object.keys(range).find(
|
|
42
|
-
key => !["min", "max", "inclusive"].includes(key)
|
|
42
|
+
key => !["min", "max", "inclusive", "minInclusive", "maxInclusive"].includes(key)
|
|
43
43
|
);
|
|
44
44
|
if (unknown !== undefined) {
|
|
45
45
|
throw new Error(`Unknown filter range property "${unknown}".`);
|
|
46
46
|
}
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
);
|
|
47
|
+
const hasMin = Object.hasOwn(range, "min");
|
|
48
|
+
const hasMax = Object.hasOwn(range, "max");
|
|
49
|
+
if (!hasMin && !hasMax) throw new TypeError("Filter range requires min or max.");
|
|
50
|
+
if ((hasMin && !isFiniteOrString(range.min)) || (hasMax && !isFiniteOrString(range.max))) {
|
|
51
|
+
throw new TypeError("Filter range endpoints must be finite numbers or strings.");
|
|
52
|
+
}
|
|
53
|
+
if (hasMin && hasMax && typeof range.min !== typeof range.max) {
|
|
54
|
+
throw new TypeError("Filter range endpoints must use one type.");
|
|
55
55
|
}
|
|
56
|
-
if (range.min > range.max) {
|
|
56
|
+
if (hasMin && hasMax && range.min > range.max) {
|
|
57
57
|
throw new RangeError("Filter range min must not exceed max.");
|
|
58
58
|
}
|
|
59
59
|
if (range.inclusive !== undefined && typeof range.inclusive !== "boolean") {
|
|
60
60
|
throw new TypeError("Filter range inclusive must be a boolean.");
|
|
61
61
|
}
|
|
62
|
+
if (range.inclusive !== undefined && (
|
|
63
|
+
range.minInclusive !== undefined || range.maxInclusive !== undefined
|
|
64
|
+
)) throw new Error("Filter range cannot combine inclusive with endpoint inclusivity.");
|
|
65
|
+
for (const [key, endpoint] of [["minInclusive", "min"], ["maxInclusive", "max"]]) {
|
|
66
|
+
if (range[key] !== undefined && typeof range[key] !== "boolean") {
|
|
67
|
+
throw new TypeError(`Filter range ${key} must be a boolean.`);
|
|
68
|
+
}
|
|
69
|
+
if (range[key] !== undefined && !Object.hasOwn(range, endpoint)) {
|
|
70
|
+
throw new Error(`Filter range ${key} requires ${endpoint}.`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function normalizeRange(range) {
|
|
76
|
+
validateRange(range);
|
|
77
|
+
const shared = range.inclusive;
|
|
78
|
+
return {
|
|
79
|
+
...(Object.hasOwn(range, "min") ? { min: range.min, minInclusive: shared ?? range.minInclusive ?? true } : {}),
|
|
80
|
+
...(Object.hasOwn(range, "max") ? { max: range.max, maxInclusive: shared ?? range.maxInclusive ?? true } : {})
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function normalizeSet(values, name) {
|
|
85
|
+
if (!Array.isArray(values) || values.length === 0 ||
|
|
86
|
+
Array.from({ length: values.length }, (_, index) => index).some(index => !Object.hasOwn(values, index)) ||
|
|
87
|
+
values.some(value => value !== null && typeof value !== "string" && typeof value !== "boolean" && !(typeof value === "number" && Number.isFinite(value)))) {
|
|
88
|
+
throw new TypeError(`Filter ${name} must be a non-empty dense array of scalar values.`);
|
|
89
|
+
}
|
|
90
|
+
return [...new Set(values)];
|
|
62
91
|
}
|
|
63
92
|
|
|
64
93
|
export function validateFilterTransform(transform) {
|
|
65
94
|
if (!isPlainObject(transform)) {
|
|
66
95
|
throw new TypeError("Filter transform must be a plain object.");
|
|
67
96
|
}
|
|
68
|
-
const supported = ["type", "field", "oneOf", "predicate", "range"];
|
|
97
|
+
const supported = ["type", "field", "oneOf", "noneOf", "predicate", "range", "nulls"];
|
|
69
98
|
const unknown = Object.keys(transform).find(key => !supported.includes(key));
|
|
70
99
|
if (unknown !== undefined) {
|
|
71
100
|
throw new Error(`Unknown filter transform property "${unknown}".`);
|
|
@@ -73,12 +102,15 @@ export function validateFilterTransform(transform) {
|
|
|
73
102
|
if (typeof transform.field !== "string" || transform.field.length === 0) {
|
|
74
103
|
throw new TypeError("Filter field must be a non-empty field string.");
|
|
75
104
|
}
|
|
76
|
-
|
|
105
|
+
if (transform.nulls !== undefined && !["include", "exclude"].includes(transform.nulls)) {
|
|
106
|
+
throw new Error('Filter nulls must be "include" or "exclude".');
|
|
107
|
+
}
|
|
108
|
+
const modes = ["oneOf", "noneOf", "predicate", "range"].filter(
|
|
77
109
|
key => Object.hasOwn(transform, key)
|
|
78
110
|
);
|
|
79
111
|
if (modes.length !== 1) {
|
|
80
112
|
throw new Error(
|
|
81
|
-
"Filter transform requires exactly one of oneOf, predicate, or range."
|
|
113
|
+
"Filter transform requires exactly one of oneOf, noneOf, predicate, or range."
|
|
82
114
|
);
|
|
83
115
|
}
|
|
84
116
|
if (modes[0] === "predicate") {
|
|
@@ -89,46 +121,101 @@ export function validateFilterTransform(transform) {
|
|
|
89
121
|
validateRange(transform.range);
|
|
90
122
|
return;
|
|
91
123
|
}
|
|
92
|
-
|
|
93
|
-
!Array.isArray(transform.oneOf) ||
|
|
94
|
-
transform.oneOf.length === 0 ||
|
|
95
|
-
transform.oneOf.some(value =>
|
|
96
|
-
value !== null &&
|
|
97
|
-
typeof value !== "string" &&
|
|
98
|
-
typeof value !== "boolean" &&
|
|
99
|
-
!(typeof value === "number" && Number.isFinite(value))
|
|
100
|
-
)
|
|
101
|
-
) {
|
|
102
|
-
throw new TypeError("Filter oneOf must be a non-empty array of scalar values.");
|
|
103
|
-
}
|
|
124
|
+
normalizeSet(transform[modes[0]], modes[0]);
|
|
104
125
|
}
|
|
105
126
|
|
|
106
127
|
export function normalizeFilterTransform({
|
|
107
128
|
field,
|
|
108
129
|
oneOf,
|
|
130
|
+
noneOf,
|
|
109
131
|
predicate,
|
|
110
|
-
range
|
|
132
|
+
range,
|
|
133
|
+
nulls
|
|
111
134
|
}) {
|
|
112
|
-
const modes = { oneOf, predicate, range };
|
|
135
|
+
const modes = { oneOf, noneOf, predicate, range };
|
|
113
136
|
const selected = Object.keys(modes).filter(key => modes[key] !== undefined);
|
|
114
137
|
const transform = {
|
|
115
138
|
type: "filter",
|
|
116
139
|
field,
|
|
117
|
-
...(selected[0] === "oneOf" ? { oneOf } : {}),
|
|
140
|
+
...(selected[0] === "oneOf" ? { oneOf: normalizeSet(oneOf, "oneOf") } : {}),
|
|
141
|
+
...(selected[0] === "noneOf" ? { noneOf: normalizeSet(noneOf, "noneOf") } : {}),
|
|
118
142
|
...(selected[0] === "predicate" ? { predicate } : {}),
|
|
119
143
|
...(selected[0] === "range"
|
|
120
|
-
? { range:
|
|
121
|
-
: {})
|
|
144
|
+
? { range: normalizeRange(range) }
|
|
145
|
+
: {}),
|
|
146
|
+
...(nulls === undefined ? {} : { nulls })
|
|
122
147
|
};
|
|
123
148
|
if (selected.length !== 1) {
|
|
124
149
|
throw new Error(
|
|
125
|
-
"filterData requires exactly one of oneOf, predicate, or range."
|
|
150
|
+
"filterData requires exactly one of oneOf, noneOf, predicate, or range."
|
|
126
151
|
);
|
|
127
152
|
}
|
|
128
153
|
validateFilterTransform(transform);
|
|
129
154
|
return cloneAndFreeze(transform);
|
|
130
155
|
}
|
|
131
156
|
|
|
157
|
+
export function normalizeFilterTransformEdit(transform, patch) {
|
|
158
|
+
const modes = ["oneOf", "noneOf", "predicate", "range"];
|
|
159
|
+
const previousMode = modes.find(key => Object.hasOwn(transform, key));
|
|
160
|
+
const requestedModes = modes.filter(key => Object.hasOwn(patch, key));
|
|
161
|
+
if (requestedModes.length > 1) {
|
|
162
|
+
throw new Error("Filter edit can replace only one filter mode.");
|
|
163
|
+
}
|
|
164
|
+
const fieldChanged = Object.hasOwn(patch, "field") && patch.field !== transform.field;
|
|
165
|
+
if (fieldChanged && requestedModes.length === 0) {
|
|
166
|
+
throw new Error("Changing a filter field requires a complete filter mode.");
|
|
167
|
+
}
|
|
168
|
+
let mode = previousMode;
|
|
169
|
+
let modeValue = transform[previousMode];
|
|
170
|
+
if (requestedModes.length === 1) {
|
|
171
|
+
mode = requestedModes[0];
|
|
172
|
+
if (mode === "range" && previousMode === "range" && !fieldChanged) {
|
|
173
|
+
if (!isPlainObject(patch.range)) throw new TypeError("Filter range patch must be a plain object.");
|
|
174
|
+
const legacy = normalizeRange(transform.range);
|
|
175
|
+
const next = { ...legacy };
|
|
176
|
+
const rangePatch = patch.range;
|
|
177
|
+
const unknown = Object.keys(rangePatch).find(key => !["min", "max", "inclusive", "minInclusive", "maxInclusive"].includes(key));
|
|
178
|
+
if (unknown !== undefined) throw new Error(`Unknown filter range property "${unknown}".`);
|
|
179
|
+
if (rangePatch.inclusive !== undefined && (rangePatch.minInclusive !== undefined || rangePatch.maxInclusive !== undefined)) {
|
|
180
|
+
throw new Error("Filter range cannot combine inclusive with endpoint inclusivity.");
|
|
181
|
+
}
|
|
182
|
+
for (const endpoint of ["min", "max"]) {
|
|
183
|
+
if (!Object.hasOwn(rangePatch, endpoint)) continue;
|
|
184
|
+
if (rangePatch[endpoint] === false) {
|
|
185
|
+
delete next[endpoint];
|
|
186
|
+
delete next[`${endpoint}Inclusive`];
|
|
187
|
+
} else {
|
|
188
|
+
next[endpoint] = rangePatch[endpoint];
|
|
189
|
+
next[`${endpoint}Inclusive`] = Object.hasOwn(legacy, endpoint)
|
|
190
|
+
? legacy[`${endpoint}Inclusive`]
|
|
191
|
+
: true;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
if (rangePatch.inclusive !== undefined) {
|
|
195
|
+
if (typeof rangePatch.inclusive !== "boolean") throw new TypeError("Filter range inclusive must be a boolean.");
|
|
196
|
+
if (Object.hasOwn(next, "min")) next.minInclusive = rangePatch.inclusive;
|
|
197
|
+
if (Object.hasOwn(next, "max")) next.maxInclusive = rangePatch.inclusive;
|
|
198
|
+
}
|
|
199
|
+
for (const endpoint of ["min", "max"]) {
|
|
200
|
+
const key = `${endpoint}Inclusive`;
|
|
201
|
+
if (rangePatch[key] !== undefined) {
|
|
202
|
+
if (!Object.hasOwn(next, endpoint)) throw new Error(`Filter range ${key} requires ${endpoint}.`);
|
|
203
|
+
next[key] = rangePatch[key];
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
modeValue = next;
|
|
207
|
+
} else {
|
|
208
|
+
modeValue = patch[mode];
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
const nulls = patch.nulls === false ? undefined : (patch.nulls ?? transform.nulls);
|
|
212
|
+
return normalizeFilterTransform({
|
|
213
|
+
field: patch.field ?? transform.field,
|
|
214
|
+
[mode]: modeValue,
|
|
215
|
+
...(nulls === undefined ? {} : { nulls })
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
|
|
132
219
|
function comparable(value, operand) {
|
|
133
220
|
return (
|
|
134
221
|
Number.isFinite(value) && Number.isFinite(operand)
|
|
@@ -141,6 +228,7 @@ function matches(value, transform) {
|
|
|
141
228
|
if (transform.oneOf !== undefined) {
|
|
142
229
|
return transform.oneOf.includes(value);
|
|
143
230
|
}
|
|
231
|
+
if (transform.noneOf !== undefined) return !transform.noneOf.includes(value);
|
|
144
232
|
if (transform.predicate !== undefined) {
|
|
145
233
|
const { op, value: operand } = transform.predicate;
|
|
146
234
|
if (op === "eq") return value === operand;
|
|
@@ -151,11 +239,12 @@ function matches(value, transform) {
|
|
|
151
239
|
if (op === "gt") return value > operand;
|
|
152
240
|
return value >= operand;
|
|
153
241
|
}
|
|
154
|
-
const { min, max, inclusive = true } = transform.range;
|
|
155
|
-
if (
|
|
156
|
-
return
|
|
157
|
-
|
|
158
|
-
|
|
242
|
+
const { min, max, minInclusive = transform.range.inclusive ?? true, maxInclusive = transform.range.inclusive ?? true } = transform.range;
|
|
243
|
+
if (min !== undefined && !comparable(value, min)) return false;
|
|
244
|
+
if (max !== undefined && !comparable(value, max)) return false;
|
|
245
|
+
if (min !== undefined && (value < min || (value === min && !minInclusive))) return false;
|
|
246
|
+
if (max !== undefined && (value > max || (value === max && !maxInclusive))) return false;
|
|
247
|
+
return true;
|
|
159
248
|
}
|
|
160
249
|
|
|
161
250
|
export function deriveFilteredRows(values, transform) {
|
|
@@ -163,7 +252,12 @@ export function deriveFilteredRows(values, transform) {
|
|
|
163
252
|
throw new TypeError("Filter source values must be an array.");
|
|
164
253
|
}
|
|
165
254
|
validateFilterTransform(transform);
|
|
166
|
-
return values.filter(row =>
|
|
167
|
-
isPlainObject(row)
|
|
168
|
-
|
|
255
|
+
return values.filter(row => {
|
|
256
|
+
if (!isPlainObject(row)) return false;
|
|
257
|
+
const value = row[transform.field];
|
|
258
|
+
if (value === null || value === undefined) {
|
|
259
|
+
if (transform.nulls !== undefined) return transform.nulls === "include";
|
|
260
|
+
}
|
|
261
|
+
return matches(value, transform);
|
|
262
|
+
});
|
|
169
263
|
}
|
package/src/grammar/fold.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { rejectUnknownProperties as rejectUnknownKeys } from "../core/validation.js";
|
|
2
|
+
import { requireStringValue as requireField } from "../core/validation.js";
|
|
1
3
|
import { cloneAndFreeze, isPlainObject } from "../core/immutable.js";
|
|
2
4
|
|
|
3
5
|
const TRANSFORM_KEYS = Object.freeze(["type", "fields", "as"]);
|
|
@@ -5,19 +7,7 @@ const AS_KEYS = Object.freeze(["key", "value"]);
|
|
|
5
7
|
const MAX_FIELDS = 64;
|
|
6
8
|
const MAX_OUTPUT_ROWS = 10_000;
|
|
7
9
|
|
|
8
|
-
function rejectUnknownKeys(value, supported, label) {
|
|
9
|
-
const unknown = Object.keys(value).find(key => !supported.includes(key));
|
|
10
|
-
if (unknown !== undefined) {
|
|
11
|
-
throw new Error(`Unknown ${label} property "${unknown}".`);
|
|
12
|
-
}
|
|
13
|
-
}
|
|
14
10
|
|
|
15
|
-
function requireField(value, label) {
|
|
16
|
-
if (typeof value !== "string" || value.length === 0) {
|
|
17
|
-
throw new TypeError(`${label} must be a non-empty string.`);
|
|
18
|
-
}
|
|
19
|
-
return value;
|
|
20
|
-
}
|
|
21
11
|
|
|
22
12
|
function normalizeAs(value) {
|
|
23
13
|
const requested = value ?? {};
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { requireStringValue as requireField } from "../core/validation.js";
|
|
1
2
|
import { cloneAndFreeze, isPlainObject } from "../core/immutable.js";
|
|
2
3
|
import {
|
|
3
4
|
deriveKernelDensity,
|
|
@@ -23,12 +24,6 @@ const TRANSFORM_KEYS = [
|
|
|
23
24
|
];
|
|
24
25
|
const OUTPUT_KEYS = Object.keys(GRADIENT_PROFILE_FIELDS);
|
|
25
26
|
|
|
26
|
-
function requireField(value, label) {
|
|
27
|
-
if (typeof value !== "string" || value.length === 0) {
|
|
28
|
-
throw new TypeError(`${label} must be a non-empty string.`);
|
|
29
|
-
}
|
|
30
|
-
return value;
|
|
31
|
-
}
|
|
32
27
|
|
|
33
28
|
function validatePair(value, label) {
|
|
34
29
|
if (
|
package/src/grammar/impute.js
CHANGED
|
@@ -1,26 +1,16 @@
|
|
|
1
|
+
import { normalizeSortBy, validateSortBy } from "./transformKeys.js";
|
|
2
|
+
import { rejectUnknownProperties as rejectUnknownKeys } from "../core/validation.js";
|
|
3
|
+
import { requireStringValue as requireField } from "../core/validation.js";
|
|
1
4
|
import { IMPUTE_REQUIRED_OPTIONS } from "../core/optionRequirements.js";
|
|
2
5
|
import { cloneAndFreeze, isPlainObject } from "../core/immutable.js";
|
|
3
|
-
import { interpolateNumber, inverseLerp } from "./numeric.js";
|
|
6
|
+
import { interpolateNumber, inverseLerp, stableFiniteMean } from "./numeric.js";
|
|
4
7
|
import { normalizeTemporalValue } from "./scales/fields.js";
|
|
5
8
|
|
|
6
9
|
const TRANSFORM_KEYS = Object.freeze([
|
|
7
10
|
"type", "fields", "groupBy", "sortBy", "method", "value", "edges", "maxGap"
|
|
8
11
|
]);
|
|
9
|
-
const SORT_KEYS = Object.freeze(["field", "order"]);
|
|
10
12
|
const METHODS = Object.freeze(Object.keys(IMPUTE_REQUIRED_OPTIONS));
|
|
11
13
|
|
|
12
|
-
function rejectUnknownKeys(value, supported, label) {
|
|
13
|
-
const unknown = Object.keys(value).find(key => !supported.includes(key));
|
|
14
|
-
if (unknown !== undefined) throw new Error(`Unknown ${label} property "${unknown}".`);
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
function requireField(value, label) {
|
|
18
|
-
if (typeof value !== "string" || value.length === 0) {
|
|
19
|
-
throw new TypeError(`${label} must be a non-empty string.`);
|
|
20
|
-
}
|
|
21
|
-
return value;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
14
|
function validateFieldList(value, label) {
|
|
25
15
|
if (!Array.isArray(value) || value.length === 0 || value.some(field =>
|
|
26
16
|
typeof field !== "string" || field.length === 0
|
|
@@ -35,33 +25,6 @@ function normalizeFieldList(value, { allowEmpty = false } = {}) {
|
|
|
35
25
|
return Array.isArray(value) ? [...value] : [value];
|
|
36
26
|
}
|
|
37
27
|
|
|
38
|
-
function normalizeSortBy(value) {
|
|
39
|
-
if (value === undefined) return [];
|
|
40
|
-
if (!Array.isArray(value)) return value;
|
|
41
|
-
return value.map(sort => isPlainObject(sort)
|
|
42
|
-
? { ...sort, order: sort.order ?? "ascending" }
|
|
43
|
-
: sort
|
|
44
|
-
);
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
function validateSortBy(sortBy) {
|
|
48
|
-
if (!Array.isArray(sortBy)) throw new TypeError("Impute sortBy must be an array.");
|
|
49
|
-
const fields = [];
|
|
50
|
-
sortBy.forEach((sort, index) => {
|
|
51
|
-
if (!isPlainObject(sort)) {
|
|
52
|
-
throw new TypeError(`Impute sortBy[${index}] must be a plain object.`);
|
|
53
|
-
}
|
|
54
|
-
rejectUnknownKeys(sort, SORT_KEYS, `impute sortBy[${index}]`);
|
|
55
|
-
fields.push(requireField(sort.field, `Impute sortBy[${index}].field`));
|
|
56
|
-
if (!["ascending", "descending"].includes(sort.order)) {
|
|
57
|
-
throw new Error(`Unsupported impute sort order "${sort.order}".`);
|
|
58
|
-
}
|
|
59
|
-
});
|
|
60
|
-
if (new Set(fields).size !== fields.length) {
|
|
61
|
-
throw new Error("Impute sortBy fields must be unique.");
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
|
|
65
28
|
function scalarType(value, label) {
|
|
66
29
|
if (value === null || value === undefined) return undefined;
|
|
67
30
|
if (typeof value === "string" || typeof value === "boolean") return typeof value;
|
|
@@ -94,7 +57,7 @@ export function validateImputeTransform(transform) {
|
|
|
94
57
|
if (new Set(transform.groupBy).size !== transform.groupBy.length) {
|
|
95
58
|
throw new Error("Impute groupBy fields must be unique.");
|
|
96
59
|
}
|
|
97
|
-
validateSortBy(transform.sortBy);
|
|
60
|
+
validateSortBy(transform.sortBy, "Impute");
|
|
98
61
|
if (!METHODS.includes(transform.method)) {
|
|
99
62
|
throw new Error(`Unsupported impute method "${transform.method}".`);
|
|
100
63
|
}
|
|
@@ -107,7 +70,7 @@ export function validateImputeTransform(transform) {
|
|
|
107
70
|
scalarType(transform.value, "Impute constant value");
|
|
108
71
|
} else {
|
|
109
72
|
if (hasValue) throw new Error(`Impute value is not available for ${transform.method}.`);
|
|
110
|
-
if (transform.sortBy.length === 0) {
|
|
73
|
+
if (transform.method !== "mean" && transform.sortBy.length === 0) {
|
|
111
74
|
throw new Error(`Impute ${transform.method} requires non-empty sortBy.`);
|
|
112
75
|
}
|
|
113
76
|
}
|
|
@@ -132,7 +95,7 @@ export function normalizeImputeTransform(args = {}) {
|
|
|
132
95
|
fields: normalizeFieldList(args.fields),
|
|
133
96
|
groupBy: normalizeFieldList(args.groupBy, { allowEmpty: true }),
|
|
134
97
|
sortBy: normalizeSortBy(args.sortBy),
|
|
135
|
-
method: args.method,
|
|
98
|
+
method: args.method ?? "mean",
|
|
136
99
|
edges: args.edges ?? "keep"
|
|
137
100
|
};
|
|
138
101
|
if (Object.hasOwn(args, "value")) transform.value = args.value;
|
|
@@ -212,9 +175,9 @@ function validateFieldTypes(entries, transform) {
|
|
|
212
175
|
if (types.size > 1) {
|
|
213
176
|
throw new TypeError(`Impute field "${field}" must contain one scalar type.`);
|
|
214
177
|
}
|
|
215
|
-
if (transform.method
|
|
178
|
+
if (["linear", "mean"].includes(transform.method) &&
|
|
216
179
|
(types.size > 0 && !types.has("number"))) {
|
|
217
|
-
throw new TypeError(`Impute
|
|
180
|
+
throw new TypeError(`Impute ${transform.method} field "${field}" must contain finite numbers.`);
|
|
218
181
|
}
|
|
219
182
|
}
|
|
220
183
|
}
|
|
@@ -278,10 +241,18 @@ function setField(row, field, value) {
|
|
|
278
241
|
});
|
|
279
242
|
}
|
|
280
243
|
|
|
281
|
-
function fillRun(partition, field, run, transform, positions) {
|
|
244
|
+
function fillRun(partition, field, run, transform, positions, mean) {
|
|
282
245
|
const [start, end] = run;
|
|
283
246
|
const length = end - start + 1;
|
|
284
247
|
if (transform.maxGap !== undefined && length > transform.maxGap) return;
|
|
248
|
+
if (transform.method === "mean") {
|
|
249
|
+
if (mean === undefined) {
|
|
250
|
+
if (transform.edges === "error") throw new RangeError(`Impute mean requires an observed value in field "${field}".`);
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
for (let index = start; index <= end; index += 1) setField(partition[index].row, field, mean);
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
285
256
|
if (transform.method === "constant") {
|
|
286
257
|
for (let index = start; index <= end; index += 1) {
|
|
287
258
|
setField(partition[index].row, field, transform.value);
|
|
@@ -332,8 +303,10 @@ export function deriveImputedRows(rows, transform) {
|
|
|
332
303
|
partition.sort((left, right) => compareEntries(left, right, transform.sortBy));
|
|
333
304
|
const positions = linearPositions(partition, transform);
|
|
334
305
|
for (const field of transform.fields) {
|
|
306
|
+
const observed = transform.method === "mean" ? partition.map(entry => entry.row[field]).filter(value => value !== null && value !== undefined) : [];
|
|
307
|
+
const mean = observed.length ? stableFiniteMean(observed, "Impute group mean") : undefined;
|
|
335
308
|
for (const run of missingRuns(partition, field)) {
|
|
336
|
-
fillRun(partition, field, run, transform, positions);
|
|
309
|
+
fillRun(partition, field, run, transform, positions, mean);
|
|
337
310
|
}
|
|
338
311
|
}
|
|
339
312
|
}
|
package/src/grammar/interval.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { requireStringValue as nonEmptyString } from "../core/validation.js";
|
|
2
|
+
import { isGroupValue } from "./transformKeys.js";
|
|
1
3
|
import { cloneAndFreeze, isPlainObject } from "../core/immutable.js";
|
|
2
4
|
import { aggregateScalarValues } from "./aggregate.js";
|
|
3
5
|
import { requireFiniteResult, stableDecimal } from "./numeric.js";
|
|
@@ -16,15 +18,11 @@ const TRANSFORM_KEYS = [
|
|
|
16
18
|
"extent",
|
|
17
19
|
"method",
|
|
18
20
|
"level",
|
|
21
|
+
"missing",
|
|
19
22
|
"as"
|
|
20
23
|
];
|
|
21
24
|
const OUTPUT_KEYS = ["center", "lower", "upper"];
|
|
22
25
|
|
|
23
|
-
function nonEmptyString(value, label) {
|
|
24
|
-
if (typeof value !== "string" || value.length === 0) {
|
|
25
|
-
throw new TypeError(`${label} must be a non-empty string.`);
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
26
|
|
|
29
27
|
function validateGrouping(groupBy) {
|
|
30
28
|
if (
|
|
@@ -82,6 +80,9 @@ export function validateIntervalTransform(transform) {
|
|
|
82
80
|
if (!EXTENT_VALUES.includes(transform.extent)) {
|
|
83
81
|
throw new Error(`Unsupported interval extent "${transform.extent}".`);
|
|
84
82
|
}
|
|
83
|
+
if (transform.missing !== undefined && !["error", "drop"].includes(transform.missing)) {
|
|
84
|
+
throw new Error('Interval missing must be "error" or "drop".');
|
|
85
|
+
}
|
|
85
86
|
if (
|
|
86
87
|
(transform.center === "median") !== (transform.extent === "iqr")
|
|
87
88
|
) {
|
|
@@ -162,10 +163,6 @@ function isMissing(value) {
|
|
|
162
163
|
(typeof value === "number" && Number.isNaN(value));
|
|
163
164
|
}
|
|
164
165
|
|
|
165
|
-
function isGroupValue(value) {
|
|
166
|
-
return typeof value === "string" || typeof value === "boolean" ||
|
|
167
|
-
(typeof value === "number" && Number.isFinite(value));
|
|
168
|
-
}
|
|
169
166
|
|
|
170
167
|
function deriveGroup(values, transform) {
|
|
171
168
|
if (transform.center === "median") {
|
|
@@ -201,15 +198,24 @@ export function deriveInterval(rows, transform) {
|
|
|
201
198
|
}
|
|
202
199
|
validateIntervalTransform(transform);
|
|
203
200
|
const groups = new Map();
|
|
201
|
+
let missingRows = 0;
|
|
204
202
|
for (const row of rows) {
|
|
205
203
|
if (!isPlainObject(row)) continue;
|
|
206
204
|
const value = row[transform.field];
|
|
207
205
|
if (isMissing(value) || !Number.isFinite(value)) {
|
|
206
|
+
const nullish = value === undefined || value === null;
|
|
207
|
+
if (transform.missing === "error" && nullish) {
|
|
208
|
+
throw new TypeError(`Interval field "${transform.field}" is missing.`);
|
|
209
|
+
}
|
|
210
|
+
if (transform.missing !== undefined && !nullish) {
|
|
211
|
+
throw new TypeError(`Interval field "${transform.field}" must contain finite numbers.`);
|
|
212
|
+
}
|
|
208
213
|
if (value !== undefined && value !== null && typeof value !== "number") {
|
|
209
214
|
throw new TypeError(
|
|
210
215
|
`Interval field "${transform.field}" must contain numeric or missing values.`
|
|
211
216
|
);
|
|
212
217
|
}
|
|
218
|
+
if (nullish) missingRows += 1;
|
|
213
219
|
continue;
|
|
214
220
|
}
|
|
215
221
|
const groupValues = transform.groupBy.map(field => row[field]);
|
|
@@ -240,7 +246,23 @@ export function deriveInterval(rows, transform) {
|
|
|
240
246
|
[transform.as.upper]: stableDecimal(result.upper)
|
|
241
247
|
});
|
|
242
248
|
}
|
|
243
|
-
return cloneAndFreeze(output);
|
|
249
|
+
if (transform.missing === undefined) return cloneAndFreeze(output);
|
|
250
|
+
return cloneAndFreeze({
|
|
251
|
+
values: output,
|
|
252
|
+
report: {
|
|
253
|
+
version: 1,
|
|
254
|
+
owner: { kind: "data", id: "pending" },
|
|
255
|
+
inputs: [],
|
|
256
|
+
units: [{
|
|
257
|
+
role: transform.as.center,
|
|
258
|
+
group: {},
|
|
259
|
+
inputRows: rows.length,
|
|
260
|
+
usedRows: rows.length - missingRows,
|
|
261
|
+
excludedRows: missingRows,
|
|
262
|
+
excludedByReason: missingRows === 0 ? {} : { "missing-value": missingRows }
|
|
263
|
+
}]
|
|
264
|
+
}
|
|
265
|
+
});
|
|
244
266
|
}
|
|
245
267
|
|
|
246
268
|
export function normalizeIntervalTransform({
|
|
@@ -250,6 +272,7 @@ export function normalizeIntervalTransform({
|
|
|
250
272
|
extent,
|
|
251
273
|
method,
|
|
252
274
|
level,
|
|
275
|
+
missing,
|
|
253
276
|
as
|
|
254
277
|
}) {
|
|
255
278
|
nonEmptyString(field, "Interval field");
|
|
@@ -260,6 +283,7 @@ export function normalizeIntervalTransform({
|
|
|
260
283
|
field,
|
|
261
284
|
groupBy: grouping,
|
|
262
285
|
...parameters,
|
|
286
|
+
...(missing === undefined ? {} : { missing }),
|
|
263
287
|
as
|
|
264
288
|
};
|
|
265
289
|
validateIntervalTransform(transform);
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { cloneAndFreeze } from "../core/immutable.js";
|
|
2
|
+
|
|
3
|
+
export const ITEM_MISSING_MARK_TYPES = Object.freeze([
|
|
4
|
+
"point", "bar", "rect", "tick", "rule", "text"
|
|
5
|
+
]);
|
|
6
|
+
|
|
7
|
+
export function validateItemMissing(value, label = "Mark missing") {
|
|
8
|
+
if (!["error", "skip"].includes(value)) {
|
|
9
|
+
throw new Error(`${label} must be "error" or "skip".`);
|
|
10
|
+
}
|
|
11
|
+
return value;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function collectFields(value, fields, seen) {
|
|
15
|
+
if (value === null || typeof value !== "object" || seen.has(value)) return;
|
|
16
|
+
seen.add(value);
|
|
17
|
+
if (typeof value.field === "string" && value.field.length > 0) {
|
|
18
|
+
fields.add(value.field);
|
|
19
|
+
}
|
|
20
|
+
for (const child of Object.values(value)) collectFields(child, fields, seen);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function itemMissingFields(layer) {
|
|
24
|
+
const fields = new Set();
|
|
25
|
+
collectFields(layer.encoding ?? {}, fields, new Set());
|
|
26
|
+
return [...fields];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function applyItemMissingPolicy(layer, dataset) {
|
|
30
|
+
const policy = layer.mark?.missing;
|
|
31
|
+
if (policy === undefined) return dataset;
|
|
32
|
+
if (!ITEM_MISSING_MARK_TYPES.includes(layer.mark?.type)) return dataset;
|
|
33
|
+
validateItemMissing(policy, `${layer.mark.type} missing`);
|
|
34
|
+
const fields = itemMissingFields(layer);
|
|
35
|
+
if (fields.length === 0) return dataset;
|
|
36
|
+
const eligible = [];
|
|
37
|
+
for (const [index, row] of dataset.values.entries()) {
|
|
38
|
+
const missing = fields.find(field => row[field] === null || row[field] === undefined);
|
|
39
|
+
if (missing === undefined) {
|
|
40
|
+
eligible.push(row);
|
|
41
|
+
} else if (policy === "error") {
|
|
42
|
+
throw new Error(
|
|
43
|
+
`${layer.mark.type} mark "${layer.id}" field "${missing}" is missing at row ${index}.`
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
if (eligible.length === dataset.values.length) return dataset;
|
|
48
|
+
return cloneAndFreeze({ ...dataset, values: eligible });
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function applyRequestedItemMissingPolicy(layer, dataset, field) {
|
|
52
|
+
if (typeof field !== "string" || field.length === 0) {
|
|
53
|
+
return applyItemMissingPolicy(layer, dataset);
|
|
54
|
+
}
|
|
55
|
+
return applyItemMissingPolicy({
|
|
56
|
+
...layer,
|
|
57
|
+
encoding: { ...layer.encoding, __requested: { field } }
|
|
58
|
+
}, dataset);
|
|
59
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { validateOptionObject, validatePositiveFinite, validateUnitInterval } from "../core/validation.js";
|
|
2
|
+
|
|
3
|
+
export const DEFAULT_LEGEND_GRADIENT = Object.freeze({ length: 120, thickness: 12 });
|
|
4
|
+
const OPTIONS = Object.freeze(["length", "thickness", "opacity"]);
|
|
5
|
+
|
|
6
|
+
export function validateLegendGradient(value, label = "Legend gradient") {
|
|
7
|
+
validateOptionObject(value, OPTIONS, label);
|
|
8
|
+
validatePositiveFinite(value.length, `${label} length`);
|
|
9
|
+
validatePositiveFinite(value.thickness, `${label} thickness`);
|
|
10
|
+
if (Object.hasOwn(value, "opacity")) validateUnitInterval(value.opacity, `${label} opacity`);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function normalizeLegendGradient(patch, previous = DEFAULT_LEGEND_GRADIENT) {
|
|
14
|
+
if (patch !== undefined) validateOptionObject(patch, OPTIONS, "Legend gradient");
|
|
15
|
+
const value = { ...previous, ...patch };
|
|
16
|
+
validateLegendGradient(value);
|
|
17
|
+
return value;
|
|
18
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{validateOptionObject as r,validateNonNegativeFinite as i}from"../core/validation.js";import{validateUserId as a}from"../core/identifiers.js";import{PLOT_LEGEND_POSITIONS as g,LEGEND_POSITIONS as d,LEGEND_BLOCK_LAYOUTS as f}from"../core/vocabulary.js";function s(n){r(n,void 0,"Legend groups");for(const[o,t]of Object.entries(n)){if(a(o,"Legend group target"),r(t,["placement","blockLayout","inferredOffset"],"Legend group"),!f.includes(t.blockLayout))throw new Error("Legend group blockLayout must be row, column or wrap.");if(t.inferredOffset!==void 0&&typeof t.inferredOffset!="boolean")throw new TypeError("Legend group inferredOffset must be boolean.");const e=t.placement;if(r(e,["at","position","align","offset"],"Legend group placement"),Object.hasOwn(e,"at")){if(Object.keys(e).length!==1)throw new Error("Legend group at cannot coexist with an edge anchor.");if(r(e.at,["x","y"],"Legend group at"),!Number.isFinite(e.at.x)||!Number.isFinite(e.at.y))throw new Error("Legend group at requires finite x and y.")}else{if(Object.keys(e).length!==3||!d.includes(e.position))throw new Error("Legend group requires a complete edge placement.");if(!["left","center","right"].includes(e.align))throw new Error("Unsupported legend group alignment.");if((g.includes(e.position)||["left","right"].includes(e.position))&&e.align!=="center")throw new Error("Side and in-plot legend groups require center alignment.");i(e.offset,"Legend group offset")}}return n}export{s as validateLegendGroupState};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { validateOptionObject } from "../core/validation.js";
|
|
2
|
+
|
|
3
|
+
export const LEGEND_TITLE_STYLE_KEYS = Object.freeze([
|
|
4
|
+
"color", "fontSize", "fontFamily", "fontWeight", "align"
|
|
5
|
+
]);
|
|
6
|
+
|
|
7
|
+
export function validateLegendTitleAlignment(style, titlePosition = "top") {
|
|
8
|
+
if (style?.align === undefined) {
|
|
9
|
+
if (style !== undefined && Object.hasOwn(style, "align")) throw new TypeError("Legend title align requires left, center or right.");
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
if (!["left", "center", "right"].includes(style.align)) throw new Error("Legend title align requires left, center or right.");
|
|
13
|
+
if (titlePosition === "left") throw new Error("Inline legend titles do not accept title alignment.");
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function mergeLegendTitleStyle(previous, patch, { edit = false } = {}) {
|
|
17
|
+
if (patch !== undefined) validateOptionObject(patch, LEGEND_TITLE_STYLE_KEYS, "Legend titleStyle");
|
|
18
|
+
const result = { ...previous, ...patch };
|
|
19
|
+
if (edit && patch?.align === "auto") delete result.align;
|
|
20
|
+
validateLegendTitleAlignment(result);
|
|
21
|
+
return result;
|
|
22
|
+
}
|