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
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { cloneAndFreeze, isPlainObject } from "../core/immutable.js";
|
|
2
|
+
import { normalizeTemporalValue } from "./scales/fields.js";
|
|
3
|
+
|
|
4
|
+
const ORDERS = new Set(["ascending", "descending"]);
|
|
5
|
+
const NULLS = new Set(["first", "last"]);
|
|
6
|
+
const TEMPORAL_UNITS = new Set(["year", "timestamp"]);
|
|
7
|
+
|
|
8
|
+
export function normalizeSortTransform({ sortBy }) {
|
|
9
|
+
if (!Array.isArray(sortBy) || sortBy.length === 0 ||
|
|
10
|
+
Array.from({ length: sortBy.length }, (_, index) => index).some(index => !Object.hasOwn(sortBy, index))) {
|
|
11
|
+
throw new TypeError("Sorted data sortBy must be a non-empty dense array.");
|
|
12
|
+
}
|
|
13
|
+
const keys = sortBy.map((key, index) => {
|
|
14
|
+
if (!isPlainObject(key)) throw new TypeError(`Sort key ${index} must be a plain object.`);
|
|
15
|
+
const unknown = Object.keys(key).find(name => !["field", "order", "nulls", "temporalUnit"].includes(name));
|
|
16
|
+
if (unknown !== undefined) throw new Error(`Unknown sort key property "${unknown}".`);
|
|
17
|
+
if (typeof key.field !== "string" || key.field.length === 0) {
|
|
18
|
+
throw new TypeError(`Sort key ${index} field must be a non-empty string.`);
|
|
19
|
+
}
|
|
20
|
+
const order = key.order ?? "ascending";
|
|
21
|
+
const nulls = key.nulls ?? "last";
|
|
22
|
+
if (!ORDERS.has(order)) throw new Error(`Unsupported sort order "${order}".`);
|
|
23
|
+
if (!NULLS.has(nulls)) throw new Error(`Unsupported sort null placement "${nulls}".`);
|
|
24
|
+
if (key.temporalUnit !== undefined && !TEMPORAL_UNITS.has(key.temporalUnit)) {
|
|
25
|
+
throw new Error(`Unsupported sort temporalUnit "${key.temporalUnit}".`);
|
|
26
|
+
}
|
|
27
|
+
return { field: key.field, order, nulls, ...(key.temporalUnit === undefined ? {} : { temporalUnit: key.temporalUnit }) };
|
|
28
|
+
});
|
|
29
|
+
if (new Set(keys.map(key => key.field)).size !== keys.length) {
|
|
30
|
+
throw new Error("Sorted data sortBy fields must be unique.");
|
|
31
|
+
}
|
|
32
|
+
return cloneAndFreeze({ type: "sort", sortBy: keys });
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function validateSortTransform(transform) {
|
|
36
|
+
if (!isPlainObject(transform)) throw new TypeError("Sort transform must be a plain object.");
|
|
37
|
+
const unknown = Object.keys(transform).find(key => !["type", "sortBy"].includes(key));
|
|
38
|
+
if (unknown !== undefined) throw new Error(`Unknown sort transform property "${unknown}".`);
|
|
39
|
+
if (transform.type !== "sort") throw new Error('Sort transform type must be "sort".');
|
|
40
|
+
normalizeSortTransform(transform);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function compareValues(left, right) {
|
|
44
|
+
if (left === right) return 0;
|
|
45
|
+
return left < right ? -1 : 1;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function normalizedKey(value, key, rowIndex) {
|
|
49
|
+
if (value === null || value === undefined) return { missing: true };
|
|
50
|
+
if (key.temporalUnit !== undefined) {
|
|
51
|
+
return { missing: false, value: normalizeTemporalValue(value, key.field, rowIndex, key.temporalUnit) };
|
|
52
|
+
}
|
|
53
|
+
if (!["number", "string", "boolean"].includes(typeof value) ||
|
|
54
|
+
(typeof value === "number" && !Number.isFinite(value))) {
|
|
55
|
+
throw new TypeError(`Sort field "${key.field}" requires finite number, string, boolean, or missing values at row ${rowIndex}.`);
|
|
56
|
+
}
|
|
57
|
+
return { missing: false, value, type: typeof value };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function deriveSortedRows(values, transform) {
|
|
61
|
+
if (!Array.isArray(values)) throw new TypeError("Sort source values must be an array.");
|
|
62
|
+
validateSortTransform(transform);
|
|
63
|
+
const decorated = values.map((row, index) => ({
|
|
64
|
+
row,
|
|
65
|
+
index,
|
|
66
|
+
keys: transform.sortBy.map(key => normalizedKey(row[key.field], key, index))
|
|
67
|
+
}));
|
|
68
|
+
for (let keyIndex = 0; keyIndex < transform.sortBy.length; keyIndex += 1) {
|
|
69
|
+
const key = transform.sortBy[keyIndex];
|
|
70
|
+
const types = new Set(decorated.map(entry => entry.keys[keyIndex]).filter(value => !value.missing).map(value => value.type ?? "number"));
|
|
71
|
+
if (types.size > 1) throw new TypeError(`Sort field "${key.field}" contains mixed non-missing types.`);
|
|
72
|
+
}
|
|
73
|
+
decorated.sort((left, right) => {
|
|
74
|
+
for (let index = 0; index < transform.sortBy.length; index += 1) {
|
|
75
|
+
const key = transform.sortBy[index];
|
|
76
|
+
const a = left.keys[index];
|
|
77
|
+
const b = right.keys[index];
|
|
78
|
+
if (a.missing || b.missing) {
|
|
79
|
+
if (a.missing !== b.missing) return a.missing === (key.nulls === "first") ? -1 : 1;
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
const compared = compareValues(a.value, b.value);
|
|
83
|
+
if (compared !== 0) return key.order === "ascending" ? compared : -compared;
|
|
84
|
+
}
|
|
85
|
+
return left.index - right.index;
|
|
86
|
+
});
|
|
87
|
+
return decorated.map(entry => entry.row);
|
|
88
|
+
}
|
package/src/grammar/stack.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
|
import { layoutSeriesPartition } from "./seriesLayout.js";
|
|
3
5
|
|
|
@@ -8,17 +10,7 @@ const AS_KEYS = Object.freeze(["start", "end", "value", "share"]);
|
|
|
8
10
|
const MODES = new Set(["stack", "fill", "center", "diverging"]);
|
|
9
11
|
const MAX_OUTPUT_ROWS = 10_000;
|
|
10
12
|
|
|
11
|
-
function rejectUnknownKeys(value, supported, label) {
|
|
12
|
-
const unknown = Object.keys(value).find(key => !supported.includes(key));
|
|
13
|
-
if (unknown !== undefined) throw new Error(`Unknown ${label} property "${unknown}".`);
|
|
14
|
-
}
|
|
15
13
|
|
|
16
|
-
function requireField(value, label) {
|
|
17
|
-
if (typeof value !== "string" || value.length === 0) {
|
|
18
|
-
throw new TypeError(`${label} must be a non-empty string.`);
|
|
19
|
-
}
|
|
20
|
-
return value;
|
|
21
|
-
}
|
|
22
14
|
|
|
23
15
|
function normalizeAs(value, field) {
|
|
24
16
|
const requested = value ?? {};
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { rejectUnknownProperties as rejectUnknownKeys } from "../core/validation.js";
|
|
1
2
|
import { aggregateRows } from "./aggregate.js";
|
|
2
3
|
import { cloneAndFreeze, isPlainObject } from "../core/immutable.js";
|
|
3
4
|
|
|
@@ -6,12 +7,6 @@ const SIMPLE_STATISTICS = new Set(["mean", "median", "min", "max"]);
|
|
|
6
7
|
const POPULATIONS = new Set(["boundData", "visibleItems"]);
|
|
7
8
|
const TRANSFORM_KEYS = Object.freeze(["type", "target"]);
|
|
8
9
|
|
|
9
|
-
function rejectUnknownKeys(value, supported, label) {
|
|
10
|
-
const unknown = Object.keys(value).find(key => !supported.includes(key));
|
|
11
|
-
if (unknown !== undefined) {
|
|
12
|
-
throw new Error(`Unknown ${label} property "${unknown}".`);
|
|
13
|
-
}
|
|
14
|
-
}
|
|
15
10
|
|
|
16
11
|
export function normalizeReferenceStatistic(value, label = "Reference statistic") {
|
|
17
12
|
if (!isPlainObject(value)) {
|
package/src/grammar/summary.js
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { scalarKey } from "./transformKeys.js";
|
|
2
|
+
import { rejectUnknownProperties as rejectUnknownKeys } from "../core/validation.js";
|
|
3
|
+
import { requireStringValue as requireField } from "../core/validation.js";
|
|
1
4
|
import { cloneAndFreeze, isPlainObject } from "../core/immutable.js";
|
|
2
5
|
import {
|
|
3
6
|
aggregateRows,
|
|
@@ -16,26 +19,12 @@ import {
|
|
|
16
19
|
} from "./weightedStatistics.js";
|
|
17
20
|
|
|
18
21
|
const TRANSFORM_KEYS = Object.freeze([
|
|
19
|
-
"type", "groupBy", "aggregates", "members", "weight"
|
|
22
|
+
"type", "groupBy", "aggregates", "members", "weight", "missing", "empty"
|
|
20
23
|
]);
|
|
21
24
|
const AGGREGATE_KEYS = Object.freeze(["op", "field", "as"]);
|
|
22
25
|
const NOMINAL_OPERATIONS = new Set(["distinct", "valid", "missing"]);
|
|
23
26
|
const MAX_OUTPUT_ROWS = 10_000;
|
|
24
27
|
|
|
25
|
-
function rejectUnknownKeys(value, supported, label) {
|
|
26
|
-
const unknown = Object.keys(value).find(key => !supported.includes(key));
|
|
27
|
-
if (unknown !== undefined) {
|
|
28
|
-
throw new Error(`Unknown ${label} property "${unknown}".`);
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
function requireField(value, label) {
|
|
33
|
-
if (typeof value !== "string" || value.length === 0) {
|
|
34
|
-
throw new TypeError(`${label} must be a non-empty string.`);
|
|
35
|
-
}
|
|
36
|
-
return value;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
28
|
function normalizeGroupBy(value) {
|
|
40
29
|
if (value === undefined) return [];
|
|
41
30
|
return Array.isArray(value) ? [...value] : [value];
|
|
@@ -51,7 +40,7 @@ function normalizeAggregate(value) {
|
|
|
51
40
|
};
|
|
52
41
|
}
|
|
53
42
|
|
|
54
|
-
export function normalizeSummaryTransform({ groupBy, aggregates, members, weight } = {}) {
|
|
43
|
+
export function normalizeSummaryTransform({ groupBy, aggregates, members, weight, missing, empty } = {}) {
|
|
55
44
|
const transform = {
|
|
56
45
|
type: "summary",
|
|
57
46
|
groupBy: normalizeGroupBy(groupBy),
|
|
@@ -61,7 +50,9 @@ export function normalizeSummaryTransform({ groupBy, aggregates, members, weight
|
|
|
61
50
|
...(members === undefined ? {} : { members }),
|
|
62
51
|
...(weight === undefined
|
|
63
52
|
? {}
|
|
64
|
-
: { weight: normalizeStatisticalWeight(weight, "Summary weight") })
|
|
53
|
+
: { weight: normalizeStatisticalWeight(weight, "Summary weight") }),
|
|
54
|
+
...(missing === undefined ? {} : { missing }),
|
|
55
|
+
...(empty === undefined ? {} : { empty })
|
|
65
56
|
};
|
|
66
57
|
validateSummaryTransform(transform);
|
|
67
58
|
return cloneAndFreeze(transform);
|
|
@@ -120,21 +111,15 @@ export function validateSummaryTransform(transform) {
|
|
|
120
111
|
if (transform.weight !== undefined) {
|
|
121
112
|
normalizeStatisticalWeight(transform.weight, "Summary weight");
|
|
122
113
|
}
|
|
114
|
+
if (transform.missing !== undefined && !["error", "drop"].includes(transform.missing)) {
|
|
115
|
+
throw new Error('Summary missing must be "error" or "drop".');
|
|
116
|
+
}
|
|
117
|
+
if (transform.empty !== undefined && !["null", "identity"].includes(transform.empty)) {
|
|
118
|
+
throw new Error('Summary empty must be "null" or "identity".');
|
|
119
|
+
}
|
|
123
120
|
return transform;
|
|
124
121
|
}
|
|
125
122
|
|
|
126
|
-
function scalarKey(value, label) {
|
|
127
|
-
if (value === null) return "null";
|
|
128
|
-
if (typeof value === "string") return `string:${value.length}:${value}`;
|
|
129
|
-
if (typeof value === "boolean") return `boolean:${value}`;
|
|
130
|
-
if (typeof value === "number" && Number.isFinite(value)) {
|
|
131
|
-
return `number:${Object.is(value, -0) ? 0 : value}`;
|
|
132
|
-
}
|
|
133
|
-
throw new TypeError(
|
|
134
|
-
`${label} must contain null, strings, booleans, or finite numbers.`
|
|
135
|
-
);
|
|
136
|
-
}
|
|
137
|
-
|
|
138
123
|
function requireSourceFields(rows, transform) {
|
|
139
124
|
if (rows.length === 0) return;
|
|
140
125
|
const fields = new Set(rows.flatMap(row => Object.keys(row)));
|
|
@@ -169,12 +154,13 @@ function validateAggregateValues(rows, aggregate) {
|
|
|
169
154
|
export function deriveSummaryRows(rows, transform) {
|
|
170
155
|
validateSummaryTransform(transform);
|
|
171
156
|
requireSourceFields(rows, transform);
|
|
157
|
+
const explicitWeighted = transform.weight !== undefined && transform.missing !== undefined;
|
|
172
158
|
let weightEntries;
|
|
173
159
|
if (transform.weight === undefined) {
|
|
174
160
|
for (const aggregate of transform.aggregates) {
|
|
175
161
|
validateAggregateValues(rows, aggregate);
|
|
176
162
|
}
|
|
177
|
-
} else {
|
|
163
|
+
} else if (!explicitWeighted) {
|
|
178
164
|
weightEntries = readStatisticalWeights(rows, transform.weight, "Summary").entries;
|
|
179
165
|
validateWeightedNumericFields(
|
|
180
166
|
weightEntries,
|
|
@@ -183,6 +169,37 @@ export function deriveSummaryRows(rows, transform) {
|
|
|
183
169
|
))],
|
|
184
170
|
"Summary"
|
|
185
171
|
);
|
|
172
|
+
} else {
|
|
173
|
+
// Explicit missing policy is evaluated per measure. Validate every
|
|
174
|
+
// nonmissing value now, while leaving nullish value/weight pairs for the
|
|
175
|
+
// reason-priority accounting below.
|
|
176
|
+
readStatisticalWeights(
|
|
177
|
+
rows.filter(row => row[transform.weight.field] !== null && row[transform.weight.field] !== undefined),
|
|
178
|
+
transform.weight,
|
|
179
|
+
"Summary"
|
|
180
|
+
);
|
|
181
|
+
for (const aggregate of transform.aggregates) {
|
|
182
|
+
if (aggregate.field === undefined) continue;
|
|
183
|
+
validateAggregateValues(
|
|
184
|
+
rows.filter(row => row[aggregate.field] !== null && row[aggregate.field] !== undefined),
|
|
185
|
+
aggregate
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
if (transform.missing === "error") {
|
|
190
|
+
if (transform.weight !== undefined) {
|
|
191
|
+
const weightIndex = rows.findIndex(row =>
|
|
192
|
+
row[transform.weight.field] === null || row[transform.weight.field] === undefined
|
|
193
|
+
);
|
|
194
|
+
if (weightIndex !== -1) {
|
|
195
|
+
throw new TypeError(`Summary weight "${transform.weight.field}" is missing at row ${weightIndex}.`);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
for (const aggregate of transform.aggregates) {
|
|
199
|
+
if (aggregate.field === undefined) continue;
|
|
200
|
+
const index = rows.findIndex(row => row[aggregate.field] === null || row[aggregate.field] === undefined);
|
|
201
|
+
if (index !== -1) throw new TypeError(`Summary field "${aggregate.field}" is missing at row ${index}.`);
|
|
202
|
+
}
|
|
186
203
|
}
|
|
187
204
|
|
|
188
205
|
const groups = new Map();
|
|
@@ -212,34 +229,92 @@ export function deriveSummaryRows(rows, transform) {
|
|
|
212
229
|
throw new RangeError(`Summary output cannot exceed ${MAX_OUTPUT_ROWS} groups.`);
|
|
213
230
|
}
|
|
214
231
|
|
|
215
|
-
|
|
216
|
-
|
|
232
|
+
const units = [];
|
|
233
|
+
const values = [...groups.values()].map(group => {
|
|
234
|
+
const sharedWeightSummary = transform.weight === undefined || explicitWeighted
|
|
217
235
|
? undefined
|
|
218
236
|
: summarizeStatisticalWeights(
|
|
219
237
|
group.entries,
|
|
220
238
|
transform.weight.kind,
|
|
221
239
|
"Summary group"
|
|
222
240
|
);
|
|
241
|
+
const aggregated = Object.fromEntries(transform.aggregates.map(aggregate => {
|
|
242
|
+
let missingValue = 0;
|
|
243
|
+
let missingWeight = 0;
|
|
244
|
+
let zeroWeightRows = 0;
|
|
245
|
+
let measureWeightSummary = sharedWeightSummary;
|
|
246
|
+
if (explicitWeighted) {
|
|
247
|
+
const eligible = group.rows.filter(row => {
|
|
248
|
+
if (aggregate.field !== undefined &&
|
|
249
|
+
(row[aggregate.field] === null || row[aggregate.field] === undefined)) {
|
|
250
|
+
missingValue += 1;
|
|
251
|
+
return false;
|
|
252
|
+
}
|
|
253
|
+
if (row[transform.weight.field] === null || row[transform.weight.field] === undefined) {
|
|
254
|
+
missingWeight += 1;
|
|
255
|
+
return false;
|
|
256
|
+
}
|
|
257
|
+
return true;
|
|
258
|
+
});
|
|
259
|
+
const entries = readStatisticalWeights(eligible, transform.weight, "Summary").entries;
|
|
260
|
+
zeroWeightRows = entries.filter(entry => entry.weight === 0).length;
|
|
261
|
+
measureWeightSummary = summarizeStatisticalWeights(
|
|
262
|
+
entries,
|
|
263
|
+
transform.weight.kind,
|
|
264
|
+
"Summary group"
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
let value = measureWeightSummary === undefined
|
|
268
|
+
? aggregateRows(group.rows, aggregate.field ?? "__row", aggregate.op)
|
|
269
|
+
: calculateWeightedAggregate(
|
|
270
|
+
measureWeightSummary,
|
|
271
|
+
aggregate.field,
|
|
272
|
+
aggregate.op,
|
|
273
|
+
`Summary aggregate "${aggregate.as}"`
|
|
274
|
+
);
|
|
275
|
+
if (value === undefined && transform.empty !== undefined) {
|
|
276
|
+
const op = typeof aggregate.op === "string" ? aggregate.op : aggregate.op.op;
|
|
277
|
+
value = transform.empty === "identity" && op === "sum" ? 0 : null;
|
|
278
|
+
}
|
|
279
|
+
if (transform.missing !== undefined || transform.empty !== undefined) {
|
|
280
|
+
const missing = explicitWeighted
|
|
281
|
+
? missingValue + missingWeight
|
|
282
|
+
: aggregate.field === undefined ? 0 : group.rows.filter(row => row[aggregate.field] === null || row[aggregate.field] === undefined).length;
|
|
283
|
+
units.push({
|
|
284
|
+
role: aggregate.as,
|
|
285
|
+
group: group.values,
|
|
286
|
+
inputRows: group.rows.length,
|
|
287
|
+
usedRows: group.rows.length - missing,
|
|
288
|
+
excludedRows: missing,
|
|
289
|
+
excludedByReason: missing === 0 ? {} : {
|
|
290
|
+
...(missingValue === 0 && explicitWeighted ? {} : { "missing-value": explicitWeighted ? missingValue : missing }),
|
|
291
|
+
...(missingWeight === 0 ? {} : { "missing-weight": missingWeight })
|
|
292
|
+
},
|
|
293
|
+
...(explicitWeighted ? { zeroWeightRows } : {})
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
return [aggregate.as, value];
|
|
297
|
+
}));
|
|
223
298
|
return {
|
|
224
299
|
...group.values,
|
|
225
|
-
...
|
|
226
|
-
aggregate.as,
|
|
227
|
-
weightSummary === undefined
|
|
228
|
-
? aggregateRows(group.rows, aggregate.field ?? "__row", aggregate.op)
|
|
229
|
-
: calculateWeightedAggregate(
|
|
230
|
-
weightSummary,
|
|
231
|
-
aggregate.field,
|
|
232
|
-
aggregate.op,
|
|
233
|
-
`Summary aggregate "${aggregate.as}"`
|
|
234
|
-
)
|
|
235
|
-
])),
|
|
300
|
+
...aggregated,
|
|
236
301
|
...(transform.members === undefined
|
|
237
302
|
? {}
|
|
238
303
|
: {
|
|
239
|
-
[transform.members]:
|
|
304
|
+
[transform.members]: sharedWeightSummary === undefined
|
|
240
305
|
? group.rows
|
|
241
|
-
: weightedRows(
|
|
306
|
+
: weightedRows(sharedWeightSummary)
|
|
242
307
|
})
|
|
243
308
|
};
|
|
244
309
|
});
|
|
310
|
+
if (transform.missing === undefined && transform.empty === undefined) return values;
|
|
311
|
+
return {
|
|
312
|
+
values,
|
|
313
|
+
report: cloneAndFreeze({
|
|
314
|
+
version: 1,
|
|
315
|
+
owner: { kind: "data", id: "pending" },
|
|
316
|
+
inputs: [],
|
|
317
|
+
units
|
|
318
|
+
})
|
|
319
|
+
};
|
|
245
320
|
}
|
package/src/grammar/text.js
CHANGED
|
@@ -11,6 +11,11 @@ export function isSourceOwnedText(layer) {
|
|
|
11
11
|
return layer?.mark?.type === "text" && layer.source !== undefined;
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
+
export const TEXT_MARK_STYLE_OPTIONS = Object.freeze([
|
|
15
|
+
"fill", "opacity", "fontSize", "fontFamily", "fontWeight",
|
|
16
|
+
"align", "baseline", "rotation", "dx", "dy", "inheritColor", "lineHeight", "blockAlign"
|
|
17
|
+
]);
|
|
18
|
+
|
|
14
19
|
export const DEFAULT_TEXT_MARK = cloneAndFreeze({
|
|
15
20
|
fill: "#334155",
|
|
16
21
|
opacity: 1,
|
|
@@ -49,6 +54,18 @@ export function formatTextValue(value, format = "auto") {
|
|
|
49
54
|
|
|
50
55
|
export function normalizeTextMarkConfig(options, base = DEFAULT_TEXT_MARK) {
|
|
51
56
|
const config = { ...base };
|
|
57
|
+
if (Object.hasOwn(options, "lineHeight")) {
|
|
58
|
+
if (!Number.isFinite(options.lineHeight) || options.lineHeight <= 0) throw new RangeError("Text lineHeight must be positive and finite.");
|
|
59
|
+
config.lineHeight = options.lineHeight;
|
|
60
|
+
}
|
|
61
|
+
if (Object.hasOwn(options, "blockAlign")) {
|
|
62
|
+
if (!["first", "middle"].includes(options.blockAlign)) throw new Error("Text blockAlign must be first or middle.");
|
|
63
|
+
config.blockAlign = options.blockAlign;
|
|
64
|
+
}
|
|
65
|
+
if (Object.hasOwn(options, "inheritColor")) {
|
|
66
|
+
if (![false, "fill", "stroke"].includes(options.inheritColor)) throw new Error("Text inheritColor must be fill, stroke, or false.");
|
|
67
|
+
config.inheritColor = options.inheritColor;
|
|
68
|
+
}
|
|
52
69
|
const mapping = {
|
|
53
70
|
fill: "fill",
|
|
54
71
|
opacity: "opacity",
|
|
@@ -75,3 +92,5 @@ export function normalizeTextMarkConfig(options, base = DEFAULT_TEXT_MARK) {
|
|
|
75
92
|
}
|
|
76
93
|
return cloneAndFreeze(config);
|
|
77
94
|
}
|
|
95
|
+
|
|
96
|
+
export { resolveTextLines } from "../core/textMetrics.js";
|
package/src/grammar/timeUnit.js
CHANGED
|
@@ -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 { normalizeTemporalValue, validateTemporalUnit } from "./scales/fields.js";
|
|
3
4
|
|
|
@@ -75,12 +76,6 @@ function validateTimeZone(timeZone) {
|
|
|
75
76
|
return timeZone;
|
|
76
77
|
}
|
|
77
78
|
|
|
78
|
-
function requireField(value, label) {
|
|
79
|
-
if (typeof value !== "string" || value.length === 0) {
|
|
80
|
-
throw new TypeError(`${label} must be a non-empty string.`);
|
|
81
|
-
}
|
|
82
|
-
return value;
|
|
83
|
-
}
|
|
84
79
|
|
|
85
80
|
function rejectUnknownKeys(value) {
|
|
86
81
|
const unknown = Object.keys(value).find(key => !TRANSFORM_KEYS.includes(key));
|
|
@@ -127,7 +122,8 @@ export function validateTimeUnitTransform(transform) {
|
|
|
127
122
|
}
|
|
128
123
|
|
|
129
124
|
export function normalizeTimeUnitTransform(args = {}) {
|
|
130
|
-
const { field, unit,
|
|
125
|
+
const { field, unit, temporalUnit, timeZone } = args;
|
|
126
|
+
const as = args.as === undefined ? `${field}_${unit}` : args.as;
|
|
131
127
|
const transform = { type: "timeUnit", field, unit, as,
|
|
132
128
|
...(temporalUnit === undefined ? {} : { temporalUnit }),
|
|
133
129
|
...(timeZone === undefined ? {} : { timeZone }) };
|
|
@@ -337,3 +333,16 @@ export function deriveTimeUnitRows(rows, transform) {
|
|
|
337
333
|
});
|
|
338
334
|
return cloneAndFreeze(values);
|
|
339
335
|
}
|
|
336
|
+
|
|
337
|
+
export function offsetUtcTimeUnit(timestamp, unit) {
|
|
338
|
+
const date = new Date(timestamp);
|
|
339
|
+
if (unit === "year") return utcTimestamp(date.getUTCFullYear() + 1);
|
|
340
|
+
if (unit === "quarter" || unit === "month") {
|
|
341
|
+
return utcTimestamp(date.getUTCFullYear(), date.getUTCMonth() + (unit === "quarter" ? 3 : 1));
|
|
342
|
+
}
|
|
343
|
+
if (unit === "day") return timestamp + 24 * 60 * 60 * 1000;
|
|
344
|
+
if (unit === "hour") return timestamp + 60 * 60 * 1000;
|
|
345
|
+
if (unit === "minute") return timestamp + 60 * 1000;
|
|
346
|
+
return timestamp + 1000;
|
|
347
|
+
}
|
|
348
|
+
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { isPlainObject } from "../core/immutable.js";
|
|
2
|
+
import { requireStringValue, rejectUnknownProperties } from "../core/validation.js";
|
|
3
|
+
|
|
4
|
+
export function scalarKey(value, label) {
|
|
5
|
+
if (value === null) return "null";
|
|
6
|
+
if (typeof value === "string") return `string:${value.length}:${value}`;
|
|
7
|
+
if (typeof value === "boolean") return `boolean:${value}`;
|
|
8
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
9
|
+
return `number:${Object.is(value, -0) ? 0 : value}`;
|
|
10
|
+
}
|
|
11
|
+
throw new TypeError(
|
|
12
|
+
`${label} must contain null, strings, booleans, or finite numbers.`
|
|
13
|
+
);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function normalizeSortBy(value) {
|
|
17
|
+
if (value === undefined) return [];
|
|
18
|
+
if (!Array.isArray(value)) return value;
|
|
19
|
+
return value.map(sort => isPlainObject(sort)
|
|
20
|
+
? { ...sort, order: sort.order ?? "ascending" }
|
|
21
|
+
: sort
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function validateSortBy(sortBy, label) {
|
|
26
|
+
const kind = label.toLowerCase();
|
|
27
|
+
if (!Array.isArray(sortBy)) throw new TypeError(`${label} sortBy must be an array.`);
|
|
28
|
+
const fields = [];
|
|
29
|
+
sortBy.forEach((sort, index) => {
|
|
30
|
+
if (!isPlainObject(sort)) {
|
|
31
|
+
throw new TypeError(`${label} sortBy[${index}] must be a plain object.`);
|
|
32
|
+
}
|
|
33
|
+
rejectUnknownProperties(sort, ["field", "order"], `${kind} sortBy[${index}]`);
|
|
34
|
+
fields.push(requireStringValue(sort.field, `${label} sortBy[${index}].field`));
|
|
35
|
+
if (!["ascending", "descending"].includes(sort.order)) {
|
|
36
|
+
throw new Error(`Unsupported ${kind} sort order "${sort.order}".`);
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
if (new Set(fields).size !== fields.length) {
|
|
40
|
+
throw new Error(`${label} sortBy fields must be unique.`);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function isGroupValue(value) {
|
|
45
|
+
return typeof value === "string" || typeof value === "boolean" ||
|
|
46
|
+
(typeof value === "number" && Number.isFinite(value));
|
|
47
|
+
}
|
|
@@ -16,6 +16,7 @@ const TRANSFORM_TOPOLOGY = Object.freeze({
|
|
|
16
16
|
markFilter: Object.freeze({ provenanceTransparent: true }),
|
|
17
17
|
normalize: Object.freeze({ facetTopology: "statistical" }),
|
|
18
18
|
regression: Object.freeze({ facetTopology: "statistical" }),
|
|
19
|
+
sort: Object.freeze({ facetTopology: "statistical" }),
|
|
19
20
|
summary: Object.freeze({ facetTopology: "statistical" }),
|
|
20
21
|
stack: Object.freeze({ facetTopology: "statistical" }),
|
|
21
22
|
statisticalReference: Object.freeze({ facetTopology: "statistical" }),
|
|
@@ -23,6 +23,7 @@ import {
|
|
|
23
23
|
validateCompleteTransform
|
|
24
24
|
} from "./complete.js";
|
|
25
25
|
import {
|
|
26
|
+
normalizeFilterTransformEdit,
|
|
26
27
|
normalizeFilterTransform,
|
|
27
28
|
validateFilterTransform
|
|
28
29
|
} from "./filter.js";
|
|
@@ -62,6 +63,7 @@ import {
|
|
|
62
63
|
validateSummaryTransform
|
|
63
64
|
} from "./summary.js";
|
|
64
65
|
import { normalizeStackTransform, validateStackTransform } from "./stack.js";
|
|
66
|
+
import { normalizeSortTransform, validateSortTransform } from "./sort.js";
|
|
65
67
|
import { validateStatisticalReferenceTransform } from
|
|
66
68
|
"./statisticalReference.js";
|
|
67
69
|
import { findTransformTopology } from "./transformTopology.js";
|
|
@@ -102,15 +104,6 @@ function normalizeSimple(normalize) {
|
|
|
102
104
|
return (transform, patch) => normalize(mergedOptions(transform, patch));
|
|
103
105
|
}
|
|
104
106
|
|
|
105
|
-
function normalizeFilterEdit(transform, patch) {
|
|
106
|
-
const base = replaceExclusive(
|
|
107
|
-
requestedOptions(transform),
|
|
108
|
-
patch,
|
|
109
|
-
["oneOf", "predicate", "range"]
|
|
110
|
-
);
|
|
111
|
-
return normalizeFilterTransform({ ...base, ...patch });
|
|
112
|
-
}
|
|
113
|
-
|
|
114
107
|
function normalizeBinEdit(transform, patch) {
|
|
115
108
|
const prior = requestedOptions(transform);
|
|
116
109
|
const base = replaceExclusive(
|
|
@@ -202,17 +195,28 @@ function normalizeNormalizeEdit(transform, patch) {
|
|
|
202
195
|
|
|
203
196
|
function normalizeRegressionEdit(transform, patch) {
|
|
204
197
|
const base = { ...requestedOptions(transform) };
|
|
198
|
+
if (patch.predict === false) delete base.predict;
|
|
205
199
|
const method = patch.method ?? base.method;
|
|
206
200
|
if (method !== base.method) {
|
|
207
201
|
if (method !== "polynomial") delete base.degree;
|
|
208
|
-
if (method !== "loess")
|
|
202
|
+
if (method !== "loess") {
|
|
203
|
+
delete base.span;
|
|
204
|
+
delete base.robustIterations;
|
|
205
|
+
}
|
|
209
206
|
if (method === "loess" || base.method === "loess") {
|
|
210
207
|
for (const key of [
|
|
211
208
|
"confidenceMethod", "level", "confidence", "interval"
|
|
212
209
|
]) delete base[key];
|
|
213
210
|
}
|
|
214
211
|
}
|
|
215
|
-
|
|
212
|
+
if (patch.interval === false) {
|
|
213
|
+
for (const key of ["confidenceMethod", "level", "confidence"]) delete base[key];
|
|
214
|
+
}
|
|
215
|
+
return normalizeRegressionTransform({
|
|
216
|
+
...base,
|
|
217
|
+
...Object.fromEntries(Object.entries(patch).filter(([key, value]) => !(key === "predict" && value === false))),
|
|
218
|
+
method
|
|
219
|
+
});
|
|
216
220
|
}
|
|
217
221
|
|
|
218
222
|
function normalizeTimeUnitEdit(transform, patch) {
|
|
@@ -373,7 +377,7 @@ const TRANSFORM_POLICIES = Object.freeze({
|
|
|
373
377
|
materializeOp: "materializeFilteredData",
|
|
374
378
|
replayTransform: requestedTransform,
|
|
375
379
|
editable: true,
|
|
376
|
-
normalizeEdit:
|
|
380
|
+
normalizeEdit: normalizeFilterTransformEdit,
|
|
377
381
|
outputRoles: () => ({})
|
|
378
382
|
}),
|
|
379
383
|
fold: Object.freeze({
|
|
@@ -442,6 +446,15 @@ const TRANSFORM_POLICIES = Object.freeze({
|
|
|
442
446
|
normalizeEdit: normalizeRegressionEdit,
|
|
443
447
|
outputRoles: () => ({})
|
|
444
448
|
}),
|
|
449
|
+
sort: Object.freeze({
|
|
450
|
+
...findTransformTopology("sort"),
|
|
451
|
+
validate: validateSortTransform,
|
|
452
|
+
materializeOp: "materializeSortedData",
|
|
453
|
+
replayTransform: requestedTransform,
|
|
454
|
+
editable: true,
|
|
455
|
+
normalizeEdit: normalizeSimple(normalizeSortTransform),
|
|
456
|
+
outputRoles: () => ({})
|
|
457
|
+
}),
|
|
445
458
|
summary: Object.freeze({
|
|
446
459
|
...findTransformTopology("summary"),
|
|
447
460
|
validate: validateSummaryTransform,
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
const NUMERIC_FORMAT = /^\.(0?\d|1[0-2])(f|%|e)$/;
|
|
2
|
-
const
|
|
2
|
+
const GROUPED_NUMERIC_FORMAT = /^,(?:\.(0?\d|1[0-2])f)?$/;
|
|
3
|
+
const WEEKDAY_NAMES = Object.freeze(["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]);
|
|
4
|
+
const TIME_DIRECTIVES = new Set(["Y", "m", "d", "b", "a", "H", "M", "S", "L", "%"]);
|
|
3
5
|
const MONTH_NAMES = Object.freeze([
|
|
4
6
|
"Jan", "Feb", "Mar", "Apr", "May", "Jun",
|
|
5
7
|
"Jul", "Aug", "Sep", "Oct", "Nov", "Dec"
|
|
@@ -33,12 +35,12 @@ export function validateValueFormat(format, label = "Label format", {
|
|
|
33
35
|
if (allowDecimalsObject) return format;
|
|
34
36
|
}
|
|
35
37
|
if (typeof format === "string" && (
|
|
36
|
-
NUMERIC_FORMAT.test(format) || isUtcValueFormat(format)
|
|
38
|
+
NUMERIC_FORMAT.test(format) || GROUPED_NUMERIC_FORMAT.test(format) || isUtcValueFormat(format)
|
|
37
39
|
)) {
|
|
38
40
|
return format;
|
|
39
41
|
}
|
|
40
42
|
const numericCandidate = typeof format === "string"
|
|
41
|
-
? format.match(
|
|
43
|
+
? format.match(/^,?\.(\d+)(f|%|e)$/)
|
|
42
44
|
: undefined;
|
|
43
45
|
if (numericCandidate && Number(numericCandidate[1]) > 12) {
|
|
44
46
|
throw new RangeError(`${label} numeric format supports at most 12 decimals.`);
|
|
@@ -61,9 +63,14 @@ function formatUtcValue(value, format, label) {
|
|
|
61
63
|
Y: pad(date.getUTCFullYear(), 4),
|
|
62
64
|
m: pad(date.getUTCMonth() + 1),
|
|
63
65
|
d: pad(date.getUTCDate()),
|
|
64
|
-
b: MONTH_NAMES[date.getUTCMonth()]
|
|
66
|
+
b: MONTH_NAMES[date.getUTCMonth()],
|
|
67
|
+
a: WEEKDAY_NAMES[date.getUTCDay()],
|
|
68
|
+
H: pad(date.getUTCHours()),
|
|
69
|
+
M: pad(date.getUTCMinutes()),
|
|
70
|
+
S: pad(date.getUTCSeconds()),
|
|
71
|
+
L: pad(date.getUTCMilliseconds(), 3)
|
|
65
72
|
};
|
|
66
|
-
return format.replace(/%([
|
|
73
|
+
return format.replace(/%([YmdbaHMSL%])/g, (_, directive) =>
|
|
67
74
|
directive === "%" ? "%" : values[directive]
|
|
68
75
|
);
|
|
69
76
|
}
|
|
@@ -98,6 +105,11 @@ export function formatValue(value, {
|
|
|
98
105
|
throw new TypeError(`${label} "${typeof resolved === "string" ? resolved : "decimals"}" requires a finite number.`);
|
|
99
106
|
}
|
|
100
107
|
if (typeof resolved === "object") return value.toFixed(resolved.decimals);
|
|
108
|
+
const grouped = resolved.match(GROUPED_NUMERIC_FORMAT);
|
|
109
|
+
if (grouped) {
|
|
110
|
+
const precision = grouped[1] === undefined ? 0 : Number(grouped[1]);
|
|
111
|
+
return new Intl.NumberFormat("en-US", { useGrouping: true, minimumFractionDigits: precision, maximumFractionDigits: precision }).format(value);
|
|
112
|
+
}
|
|
101
113
|
const match = resolved.match(NUMERIC_FORMAT);
|
|
102
114
|
const precision = Number(match[1]);
|
|
103
115
|
if (match[2] === "f") return value.toFixed(precision);
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { requireStringValue as requireField } from "../core/validation.js";
|
|
1
2
|
import { isPlainObject } from "../core/immutable.js";
|
|
2
3
|
import {
|
|
3
4
|
interpolateNumber,
|
|
@@ -15,12 +16,6 @@ export const WEIGHTED_AGGREGATE_OPERATIONS = Object.freeze([
|
|
|
15
16
|
"stderr", "median", "q1", "q3", "quantile"
|
|
16
17
|
]);
|
|
17
18
|
|
|
18
|
-
function requireField(value, label) {
|
|
19
|
-
if (typeof value !== "string" || value.length === 0) {
|
|
20
|
-
throw new TypeError(`${label} must be a non-empty string.`);
|
|
21
|
-
}
|
|
22
|
-
return value;
|
|
23
|
-
}
|
|
24
19
|
|
|
25
20
|
export function normalizeStatisticalWeight(value, label = "Statistical weight") {
|
|
26
21
|
if (!isPlainObject(value)) {
|