ggaction 0.0.17 → 0.0.19
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 +96 -0
- package/README.md +6 -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 +3 -3
- 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 +20 -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 +8 -7
- 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/box.js +2 -1
- package/src/actions/data/derived.js +11 -1
- package/src/actions/data/edit.js +3 -1
- package/src/actions/data/index.js +4 -0
- package/src/actions/data/regression.js +25 -16
- package/src/actions/data/regressionStatistics.js +73 -0
- package/src/actions/data/shared.js +8 -4
- package/src/actions/data/stack.js +3 -1
- package/src/actions/distributions/revision.js +24 -18
- package/src/actions/encodings/appearance.js +37 -29
- package/src/actions/encodings/barWidth.js +36 -11
- package/src/actions/encodings/channels.js +0 -1
- package/src/actions/encodings/color/continuous.js +1 -1
- package/src/actions/encodings/color/index.js +1 -1
- package/src/actions/encodings/color/policy.js +10 -3
- package/src/actions/encodings/density/resolve.js +1 -6
- 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 +3 -2
- package/src/actions/encodings/ranged.js +17 -0
- package/src/actions/encodings/remove.js +14 -8
- package/src/actions/encodings/seriesLayout.js +1 -0
- package/src/actions/encodings/shared.js +26 -24
- package/src/actions/encodings/text.js +6 -3
- package/src/actions/errorBands/create.js +2 -2
- package/src/actions/errorBands/edit.js +2 -1
- package/src/actions/facets/actions.js +176 -247
- package/src/actions/facets/guides.js +2 -1
- package/src/actions/gradientPlots/resolve.js +2 -17
- package/src/actions/guides/applicability.js +1 -1
- package/src/actions/guides/axes/axes.js +30 -11
- package/src/actions/guides/axes/axis.js +72 -10
- package/src/actions/guides/axes/components.js +13 -13
- package/src/actions/guides/axes/edit.js +36 -24
- package/src/actions/guides/axes/labels.js +155 -71
- package/src/actions/guides/axes/lines.js +27 -24
- 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 +13 -8
- package/src/actions/guides/axes/tickGroups.js +11 -7
- package/src/actions/guides/axes/ticks.js +36 -24
- package/src/actions/guides/axes/titles.js +107 -69
- package/src/actions/guides/basic.js +2 -0
- package/src/actions/guides/facadeAxes.js +33 -24
- package/src/actions/guides/facadeLegend.js +12 -4
- package/src/actions/guides/grids/grid.js +8 -2
- package/src/actions/guides/grids/resolve.js +102 -58
- package/src/actions/guides/legends/blocks.js +46 -20
- package/src/actions/guides/legends/categorical/actions.js +95 -19
- package/src/actions/guides/legends/categorical/components.js +15 -18
- package/src/actions/guides/legends/categorical/index.js +8 -0
- package/src/actions/guides/legends/categorical/layout.js +45 -20
- package/src/actions/guides/legends/categorical/options.js +39 -22
- package/src/actions/guides/legends/categorical/recipes.js +7 -3
- package/src/actions/guides/legends/categorical/resolve.js +11 -4
- package/src/actions/guides/legends/categorical/symbols.js +38 -18
- package/src/actions/guides/legends/categorical/validation.js +1 -6
- package/src/actions/guides/legends/colorSize.js +110 -0
- package/src/actions/guides/legends/combine.js +37 -0
- 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 +73 -46
- package/src/actions/guides/legends/continuous/interval.js +4 -4
- package/src/actions/guides/legends/continuous/opacity.js +33 -21
- package/src/actions/guides/legends/creation.js +2 -0
- package/src/actions/guides/legends/edit.js +1 -604
- package/src/actions/guides/legends/focused.js +8 -7
- package/src/actions/guides/legends/group.js +1 -0
- package/src/actions/guides/legends/lane.js +1 -516
- package/src/actions/guides/legends/lifecycle.js +2 -1
- package/src/actions/guides/legends/remove.js +9 -3
- package/src/actions/guides/legends/sampling.js +1 -1
- package/src/actions/guides/legends/size.js +65 -47
- package/src/actions/guides/legends/strokeWidth.js +13 -29
- package/src/actions/guides/legends/target.js +2 -79
- package/src/actions/guides/legends/transition.js +44 -11
- 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/guides/tickValues.js +8 -5
- 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 +10 -4
- package/src/actions/marks/bar/edit.js +29 -14
- package/src/actions/marks/bar/materialize.js +15 -7
- package/src/actions/marks/line/actions.js +11 -4
- package/src/actions/marks/line/materialize.js +9 -5
- package/src/actions/marks/point/edit.js +1 -1
- package/src/actions/marks/point/materialize.js +5 -1
- package/src/actions/marks/rect/actions.js +3 -3
- package/src/actions/marks/remove.js +12 -10
- package/src/actions/marks/rule/actions.js +7 -4
- package/src/actions/marks/text/actions.js +17 -5
- package/src/actions/marks/text/layout.js +3 -0
- package/src/actions/primitives/createGraphics.js +3 -2
- package/src/actions/primitives/editGraphics.js +15 -11
- package/src/actions/primitives/graphicProperties.js +1 -6
- package/src/actions/primitives/semanticAction.js +27 -1
- package/src/actions/primitives/semanticValidation/guide.js +11 -1
- package/src/actions/primitives/semanticValidation/layer.js +8 -0
- package/src/actions/regression/create.js +1 -1
- package/src/actions/regression/edit.js +18 -6
- package/src/actions/regression/resolve.js +2 -6
- package/src/actions/scales/channels.js +9 -4
- package/src/actions/scales/consumers/families.js +16 -5
- package/src/actions/scales/create.js +10 -5
- package/src/actions/scales/definitions.js +1 -425
- package/src/actions/scales/edit.js +3 -3
- package/src/actions/scales/editPolicy.js +12 -3
- package/src/actions/scales/materialize.js +1 -1
- package/src/actions/scales/patch.js +1 -1
- package/src/actions/scales/preview.js +21 -1
- package/src/actions/scales/quantitativeColor.js +8 -3
- package/src/actions/selection/actions.js +1 -13
- package/src/actions/theme/reconcile.js +126 -45
- 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/compositionState.js +6 -3
- package/src/core/font.js +9 -2
- package/src/core/graphicProperties.js +1 -0
- package/src/core/optionRequirements.js +1 -0
- package/src/core/programState.js +4 -0
- package/src/core/resourceReferences.js +5 -2
- package/src/core/textMetricProfile.js +3 -2
- package/src/core/textMetrics.js +41 -4
- package/src/core/validation.js +35 -0
- package/src/core/vocabulary.js +7 -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/axisTextPlacement.js +1 -0
- package/src/grammar/bars/geometry.js +30 -3
- package/src/grammar/bars/policy.js +12 -1
- package/src/grammar/bin.js +2 -12
- package/src/grammar/bin2d.js +1 -6
- package/src/grammar/boxPlot.js +56 -9
- package/src/grammar/categoryOrder.js +1 -6
- package/src/grammar/colorSizeLegend.js +39 -0
- package/src/grammar/complete.js +15 -19
- package/src/grammar/computed.js +115 -26
- package/src/grammar/curveCommands.js +29 -16
- package/src/grammar/datasetSchema.js +13 -9
- package/src/grammar/displayLabels.js +36 -0
- package/src/grammar/ecdf.js +2 -9
- package/src/grammar/facets/dependencies.js +6 -2
- package/src/grammar/facets/guides.js +58 -30
- package/src/grammar/facets/index.js +2 -5
- package/src/grammar/facets/legendIntent.js +65 -0
- package/src/grammar/fold.js +2 -12
- package/src/grammar/gradientProfile.js +1 -6
- package/src/grammar/guideDash.js +1 -0
- package/src/grammar/impute.js +21 -48
- package/src/grammar/interval.js +2 -9
- package/src/grammar/legendBlocks.js +1 -0
- package/src/grammar/legendBorder.js +9 -0
- package/src/grammar/legendClip.js +19 -0
- package/src/grammar/legendCombinations.js +45 -0
- package/src/grammar/legendGradient.js +27 -0
- package/src/grammar/legendGroups.js +1 -0
- package/src/grammar/legendOpacity.js +6 -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/pointShapes.js +16 -2
- package/src/grammar/polar.js +1 -184
- package/src/grammar/positionCompatibility.js +3 -3
- package/src/grammar/regression/derive.js +16 -2
- package/src/grammar/regression/models.js +142 -54
- package/src/grammar/regression/parameters.js +23 -4
- package/src/grammar/regression/statistics.js +64 -0
- 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 -197
- 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 +14 -0
- package/src/grammar/schemas/graphic.js +6 -0
- package/src/grammar/schemas/graphicBounds.js +31 -7
- package/src/grammar/schemas/semanticPath.js +19 -3
- package/src/grammar/stack.js +2 -10
- package/src/grammar/statisticalReference.js +1 -6
- package/src/grammar/summary.js +4 -27
- package/src/grammar/text.js +19 -0
- package/src/grammar/timeUnit.js +22 -11
- package/src/grammar/transformKeys.js +47 -0
- package/src/grammar/transformTopology.js +1 -0
- package/src/grammar/transforms.js +15 -2
- package/src/grammar/valueFormat.js +17 -5
- package/src/grammar/weightedStatistics.js +1 -6
- package/src/grammar/window.js +4 -58
- package/src/inspectionDescriptors.js +1 -1
- package/src/layout/canvas.js +1 -110
- package/src/layout/facets.js +1 -244
- package/src/layout/guideCollisions.js +9 -1
- 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 +22 -1
- package/src/materialization/compositionSnapshot.js +2 -7
- package/src/materialization/coordinateBounds.js +21 -0
- package/src/materialization/dependencies.js +4 -44
- package/src/materialization/facetGuides/edit.js +30 -0
- package/src/materialization/facetGuides/layout.js +1 -0
- package/src/materialization/facetGuides/owner.js +125 -0
- package/src/materialization/facetGuides/ownerLayout.js +71 -0
- package/src/materialization/facetGuides/placement.js +71 -14
- package/src/materialization/facetGuides/preparation.js +22 -7
- package/src/materialization/facetHeaders.js +1 -347
- package/src/materialization/facets.js +1 -185
- package/src/materialization/graphicHierarchy.js +5 -1
- package/src/materialization/graphicTranslation.js +73 -0
- 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/legendBorder.js +28 -0
- package/src/materialization/legendClip.js +76 -0
- package/src/materialization/legendGroups.js +1 -0
- package/src/materialization/legendText.js +1 -0
- package/src/materialization/legends.js +2 -2
- package/src/materialization/marks/capabilities.js +5 -6
- package/src/materialization/rect.js +1 -1
- package/src/materialization/scaleGuideDependencies.js +14 -15
- package/src/materialization/scales/map.js +6 -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 +7 -3
- 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 +13 -4
- package/src/materialization/textLines.js +15 -0
- package/src/materialization/typography.js +10 -2
- package/src/persistence/codec.js +37 -6
- package/src/persistence/validation.js +220 -0
- package/src/persistence.js +19 -6
- package/src/renderers/canvas/circle.js +6 -3
- package/src/renderers/canvas/fill.js +7 -1
- package/src/renderers/canvas/index.js +22 -11
- package/src/renderers/canvas/isolated.js +166 -0
- package/src/renderers/canvas/limits.js +3 -0
- 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/pdf.js +2 -2
- package/src/renderers/png.js +2 -2
- package/src/renderers/svg.js +14 -4
- package/src/selectors/axes.js +1 -0
- package/src/selectors/graphicCapacity.js +38 -0
- 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 +3 -0
- package/types/index.d.ts +12 -0
- package/types/program.d.ts +536 -129
package/src/grammar/boxPlot.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { cloneAndFreeze, isPlainObject } from "../core/immutable.js";
|
|
2
|
-
import {
|
|
2
|
+
import { quantileSorted, stableDecimal } from "./numeric.js";
|
|
3
3
|
|
|
4
4
|
export const BOX_FIELDS = Object.freeze({
|
|
5
5
|
q1: "__boxPlot_q1", median: "__boxPlot_median", q3: "__boxPlot_q3",
|
|
@@ -9,7 +9,7 @@ export const BOX_FIELDS = Object.freeze({
|
|
|
9
9
|
});
|
|
10
10
|
|
|
11
11
|
const TRANSFORM_KEYS = [
|
|
12
|
-
"type", "category", "field", "method", "whisker", "factor", "as"
|
|
12
|
+
"type", "category", "field", "method", "whisker", "factor", "as", "summary"
|
|
13
13
|
];
|
|
14
14
|
const OUTPUT_KEYS = Object.keys(BOX_FIELDS);
|
|
15
15
|
|
|
@@ -18,6 +18,36 @@ function field(value, label) {
|
|
|
18
18
|
return value;
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
+
const SUMMARY_KEYS = Object.freeze(["min", "q1", "median", "q3", "max"]);
|
|
22
|
+
|
|
23
|
+
export function normalizeBoxSummary(value) {
|
|
24
|
+
if (value === undefined) return undefined;
|
|
25
|
+
if (!isPlainObject(value) || Object.keys(value).length !== SUMMARY_KEYS.length ||
|
|
26
|
+
!SUMMARY_KEYS.every(key => Object.hasOwn(value, key))) {
|
|
27
|
+
throw new TypeError("Box summary requires min, q1, median, q3 and max field names.");
|
|
28
|
+
}
|
|
29
|
+
return cloneAndFreeze(Object.fromEntries(SUMMARY_KEYS.map(key => [key, field(value[key], `Box summary.${key}`)])));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function derivePrecomputedBoxData(rows, transform) {
|
|
33
|
+
const { category, summary, as } = transform;
|
|
34
|
+
const summaries = [];
|
|
35
|
+
for (const row of rows) {
|
|
36
|
+
const key = row[category];
|
|
37
|
+
if (key === undefined || key === null || key === "") continue;
|
|
38
|
+
const values = SUMMARY_KEYS.map(role => row[summary[role]]);
|
|
39
|
+
if (!values.every(Number.isFinite)) throw new TypeError("Precomputed box summaries require finite values for all five fields.");
|
|
40
|
+
if (values.some((value, index) => index > 0 && value < values[index - 1])) {
|
|
41
|
+
throw new RangeError("Box summary must satisfy min <= q1 <= median <= q3 <= max.");
|
|
42
|
+
}
|
|
43
|
+
const [min, q1, median, q3, max] = values;
|
|
44
|
+
summaries.push({ [category]: key, [as.q1]: q1, [as.median]: median, [as.q3]: q3,
|
|
45
|
+
[as.lowerWhisker]: min, [as.upperWhisker]: max });
|
|
46
|
+
}
|
|
47
|
+
if (summaries.length === 0) throw new Error("Box transform requires at least one valid row.");
|
|
48
|
+
return cloneAndFreeze({ summaries, outliers: [] });
|
|
49
|
+
}
|
|
50
|
+
|
|
21
51
|
function validateOutputs(as, occupied) {
|
|
22
52
|
if (!isPlainObject(as)) throw new TypeError("Box as must be a plain object.");
|
|
23
53
|
const keys = Object.keys(as);
|
|
@@ -40,6 +70,18 @@ export function validateBoxTransform(value) {
|
|
|
40
70
|
}
|
|
41
71
|
field(value.category, "Box category field");
|
|
42
72
|
field(value.field, "Box measure field");
|
|
73
|
+
if (value.method === "precomputed") {
|
|
74
|
+
const summary = normalizeBoxSummary(value.summary);
|
|
75
|
+
if (summary === undefined || value.type !== "boxSummary" || value.field !== summary.median) {
|
|
76
|
+
throw new Error("Precomputed box summaries require a summary mapping and its median measure field.");
|
|
77
|
+
}
|
|
78
|
+
if (value.whisker !== undefined || value.factor !== undefined) {
|
|
79
|
+
throw new Error("Precomputed box summaries do not compute whiskers or factors.");
|
|
80
|
+
}
|
|
81
|
+
validateOutputs(value.as, new Set([value.category, ...Object.values(summary)]));
|
|
82
|
+
return value;
|
|
83
|
+
}
|
|
84
|
+
if (value.summary !== undefined) throw new Error("Box summary fields require the precomputed method.");
|
|
43
85
|
if (value.method !== "linear") throw new Error(`Unsupported box quantile method "${value.method}".`);
|
|
44
86
|
const whisker = value.whisker ?? "tukey";
|
|
45
87
|
if (!["tukey", "minmax"].includes(whisker)) {
|
|
@@ -56,6 +98,16 @@ export function validateBoxTransform(value) {
|
|
|
56
98
|
}
|
|
57
99
|
|
|
58
100
|
export function normalizeBoxTransform(options = {}) {
|
|
101
|
+
if (options.summary !== undefined) {
|
|
102
|
+
const summary = normalizeBoxSummary(options.summary);
|
|
103
|
+
const normalized = { type: options.type ?? "boxSummary", category: options.category,
|
|
104
|
+
field: options.field ?? summary.median, method: "precomputed", summary, as: options.as ?? BOX_FIELDS };
|
|
105
|
+
if (options.factor !== undefined || (options.whisker !== undefined && options.whisker !== "minmax")) {
|
|
106
|
+
throw new Error("Precomputed box summaries require minmax whiskers without a factor.");
|
|
107
|
+
}
|
|
108
|
+
validateBoxTransform(normalized);
|
|
109
|
+
return cloneAndFreeze(normalized);
|
|
110
|
+
}
|
|
59
111
|
const {
|
|
60
112
|
type = "boxSummary",
|
|
61
113
|
category,
|
|
@@ -79,16 +131,11 @@ export function normalizeBoxTransform(options = {}) {
|
|
|
79
131
|
return cloneAndFreeze(normalized);
|
|
80
132
|
}
|
|
81
133
|
|
|
82
|
-
function quantile(sorted, p) {
|
|
83
|
-
const position = (sorted.length - 1) * p;
|
|
84
|
-
const lower = Math.floor(position);
|
|
85
|
-
const upper = Math.ceil(position);
|
|
86
|
-
return interpolateNumber(sorted[lower], sorted[upper], position - lower);
|
|
87
|
-
}
|
|
88
134
|
|
|
89
135
|
export function deriveBoxData(rows, transform) {
|
|
90
136
|
if (!Array.isArray(rows)) throw new TypeError("Box rows must be an array.");
|
|
91
137
|
validateBoxTransform(transform);
|
|
138
|
+
if (transform.method === "precomputed") return derivePrecomputedBoxData(rows, transform);
|
|
92
139
|
const { category, field: measure, factor, as } = transform;
|
|
93
140
|
const whisker = transform.whisker ?? "tukey";
|
|
94
141
|
const groups = [];
|
|
@@ -111,7 +158,7 @@ export function deriveBoxData(rows, transform) {
|
|
|
111
158
|
for (const group of groups) {
|
|
112
159
|
const sorted = group.rows.map(row => row[measure]).sort((a, b) => a - b);
|
|
113
160
|
const [q1, median, q3] = [0.25, 0.5, 0.75].map(probability =>
|
|
114
|
-
stableDecimal(
|
|
161
|
+
stableDecimal(quantileSorted(sorted, probability))
|
|
115
162
|
);
|
|
116
163
|
const spread = q3 - q1;
|
|
117
164
|
const lowerFence = whisker === "minmax"
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { requireStringValue as nonEmptyField } from "../core/validation.js";
|
|
1
2
|
import { cloneAndFreeze, isPlainObject } from "../core/immutable.js";
|
|
2
3
|
import { isNominalValue, readNominalField } from "./scales/fields.js";
|
|
3
4
|
import { aggregateScalarValues } from "./aggregate.js";
|
|
@@ -7,12 +8,6 @@ export const CATEGORY_ORDER_CHANNELS = Object.freeze(["x", "y", "theta"]);
|
|
|
7
8
|
const DIRECTIONS = ["ascending", "descending"];
|
|
8
9
|
const AGGREGATES = ["sum", "mean", "min", "max"];
|
|
9
10
|
|
|
10
|
-
function nonEmptyField(value, label) {
|
|
11
|
-
if (typeof value !== "string" || value.length === 0) {
|
|
12
|
-
throw new TypeError(`${label} must be a non-empty string.`);
|
|
13
|
-
}
|
|
14
|
-
return value;
|
|
15
|
-
}
|
|
16
11
|
|
|
17
12
|
const sameValue = Object.is;
|
|
18
13
|
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import {sameOrderedValues,validateOptionObject,validateUnitInterval,validateNonNegativeFinite,validateNonEmptyString} from '../core/validation.js';
|
|
2
|
+
import {isContinuousColorScaleType} from './scales/types.js';
|
|
3
|
+
import {isContinuousSizeScaleType} from './scales/size.js';
|
|
4
|
+
|
|
5
|
+
export function isColorSizeChannels(channels) {
|
|
6
|
+
return Array.isArray(channels)&&channels.length===2&&channels.includes('color')&&channels.includes('size');
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function resolveColorSizeMapping({layer,scales,pointConfig,coordinate}) {
|
|
10
|
+
const color=layer?.encoding?.color,size=layer?.encoding?.size;
|
|
11
|
+
const colorScale=scales[color?.scale],sizeScale=scales[size?.scale];
|
|
12
|
+
if(layer?.mark?.type!=='point'||coordinate?.type!=='cartesian'||
|
|
13
|
+
pointConfig?.shape!=='circle'||layer.encoding.shape!==undefined||
|
|
14
|
+
color?.fieldType!=='quantitative'||size?.fieldType!=='quantitative'||
|
|
15
|
+
typeof color.field!=='string'||color.field!==size.field||
|
|
16
|
+
color.aggregate!==size.aggregate||color.data!==size.data||
|
|
17
|
+
!isContinuousColorScaleType(colorScale?.type)||!isContinuousSizeScaleType(sizeScale?.type)) {
|
|
18
|
+
throw new Error('Color-size combination requires one Cartesian circle point owner with the same quantitative field and grain.');
|
|
19
|
+
}
|
|
20
|
+
const ends=scale=>[scale.domain?.[0],scale.domain?.at(-1)];
|
|
21
|
+
const domain=ends(colorScale),sizeDomain=ends(sizeScale);
|
|
22
|
+
if(!domain.every(Number.isFinite)||!sizeDomain.every(Number.isFinite)||
|
|
23
|
+
!sameOrderedValues(domain,sizeDomain)||domain[0]===domain[1]) {
|
|
24
|
+
throw new Error('Color-size combination requires identical finite numeric domain endpoints.');
|
|
25
|
+
}
|
|
26
|
+
return {target:layer.id,field:color.field,fieldType:'quantitative',colorScale:color.scale,sizeScale:size.scale,domain};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function normalizeColorSizeSymbol(value,previous) {
|
|
30
|
+
if(value===undefined)return {...(previous??{type:'point',strokeWidth:0,opacity:1})};
|
|
31
|
+
if(value==='auto')return {type:'point',strokeWidth:0,opacity:1};
|
|
32
|
+
validateOptionObject(value,['type','stroke','strokeWidth','opacity'],'Color-size symbol');
|
|
33
|
+
if(value.type!==undefined&&value.type!=='point')throw new Error('Color-size symbols require type point.');
|
|
34
|
+
const symbol={type:'point',strokeWidth:0,opacity:1,...previous,...value};
|
|
35
|
+
if(symbol.stroke!==undefined)validateNonEmptyString(symbol.stroke,'Color-size symbol stroke');
|
|
36
|
+
validateNonNegativeFinite(symbol.strokeWidth,'Color-size symbol strokeWidth');
|
|
37
|
+
validateUnitInterval(symbol.opacity,'Color-size symbol opacity');
|
|
38
|
+
return symbol;
|
|
39
|
+
}
|
package/src/grammar/complete.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([
|
|
@@ -6,17 +8,7 @@ const TRANSFORM_KEYS = Object.freeze([
|
|
|
6
8
|
const SEQUENCE_KEYS = Object.freeze(["start", "end", "step"]);
|
|
7
9
|
const MAX_OUTPUT_ROWS = 10_000;
|
|
8
10
|
|
|
9
|
-
function rejectUnknownKeys(value, supported, label) {
|
|
10
|
-
const unknown = Object.keys(value).find(key => !supported.includes(key));
|
|
11
|
-
if (unknown !== undefined) throw new Error(`Unknown ${label} property "${unknown}".`);
|
|
12
|
-
}
|
|
13
11
|
|
|
14
|
-
function requireField(value, label) {
|
|
15
|
-
if (typeof value !== "string" || value.length === 0) {
|
|
16
|
-
throw new TypeError(`${label} must be a non-empty string.`);
|
|
17
|
-
}
|
|
18
|
-
return value;
|
|
19
|
-
}
|
|
20
12
|
|
|
21
13
|
function scalarType(value, label) {
|
|
22
14
|
if (value === null) return "null";
|
|
@@ -258,20 +250,24 @@ export function deriveCompleteRows(rows, transform) {
|
|
|
258
250
|
if (!allowed.has(identity)) {
|
|
259
251
|
throw new Error(`Complete observed key is outside the requested domain at row ${entry.index}.`);
|
|
260
252
|
}
|
|
261
|
-
if (lookup.has(identity))
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
253
|
+
if (!lookup.has(identity)) lookup.set(identity, []);
|
|
254
|
+
lookup.get(identity).push(entry);
|
|
255
|
+
}
|
|
256
|
+
const groupSize = group.values.length + domain.length - lookup.size;
|
|
257
|
+
if (output.length + groupSize > MAX_OUTPUT_ROWS) {
|
|
258
|
+
throw new RangeError(`Complete output cannot exceed ${MAX_OUTPUT_ROWS} rows.`);
|
|
265
259
|
}
|
|
266
260
|
for (const keyValue of domain) {
|
|
267
261
|
const identity = scalarKey(keyValue, "Complete domain value");
|
|
268
|
-
const
|
|
269
|
-
if (
|
|
262
|
+
const entries = lookup.get(identity);
|
|
263
|
+
if (entries === undefined) {
|
|
270
264
|
output.push(syntheticRow(fields, group, keyValue, transform));
|
|
271
265
|
} else {
|
|
272
|
-
const
|
|
273
|
-
|
|
274
|
-
|
|
266
|
+
for (const entry of entries) {
|
|
267
|
+
const row = { ...entry.row };
|
|
268
|
+
if (transform.members !== undefined) define(row, transform.members, [entry.index]);
|
|
269
|
+
output.push(row);
|
|
270
|
+
}
|
|
275
271
|
}
|
|
276
272
|
}
|
|
277
273
|
}
|
package/src/grammar/computed.js
CHANGED
|
@@ -1,29 +1,22 @@
|
|
|
1
|
+
import {parseTimeValue,validateParseTimeFormat} from "./parseTime.js";
|
|
2
|
+
import { calendarParts, validateTimeZone } from "./timeUnit.js";
|
|
3
|
+
import { rejectUnknownProperties as rejectUnknownKeys } from "../core/validation.js";
|
|
4
|
+
import { requireStringValue as requireField } from "../core/validation.js";
|
|
1
5
|
import { cloneAndFreeze, isPlainObject } from "../core/immutable.js";
|
|
2
6
|
|
|
3
7
|
const TRANSFORM_KEYS = Object.freeze(["type", "as", "expression"]);
|
|
4
8
|
const ARITHMETIC_BINARY = new Set(["add", "subtract", "multiply", "divide"]);
|
|
5
9
|
const COMPARISONS = new Set(["eq", "neq", "lt", "lte", "gt", "gte"]);
|
|
6
|
-
const NUMERIC_UNARY = new Set(["negate", "absolute", "log", "sqrt"]);
|
|
10
|
+
const NUMERIC_UNARY = new Set(["negate", "absolute", "log", "sqrt", "floor"]);
|
|
7
11
|
const BOOLEAN_UNARY = new Set(["not", "isNull"]);
|
|
8
12
|
const LOGICAL = new Set(["and", "or"]);
|
|
9
13
|
const VARIADIC = new Set(["coalesce", "concat"]);
|
|
14
|
+
const RESERVED_PATH_KEYS = new Set(["__proto__", "prototype", "constructor"]);
|
|
10
15
|
const MAX_DEPTH = 16;
|
|
11
16
|
const MAX_NODES = 128;
|
|
12
17
|
const MAX_WORK = 10_000_000;
|
|
13
18
|
|
|
14
|
-
function rejectUnknownKeys(value, supported, label) {
|
|
15
|
-
const unknown = Object.keys(value).find(key => !supported.includes(key));
|
|
16
|
-
if (unknown !== undefined) {
|
|
17
|
-
throw new Error(`Unknown ${label} property "${unknown}".`);
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
19
|
|
|
21
|
-
function requireField(value, label) {
|
|
22
|
-
if (typeof value !== "string" || value.length === 0) {
|
|
23
|
-
throw new TypeError(`${label} must be a non-empty string.`);
|
|
24
|
-
}
|
|
25
|
-
return value;
|
|
26
|
-
}
|
|
27
20
|
|
|
28
21
|
function validateConstant(value) {
|
|
29
22
|
if (value === null || typeof value === "string" || typeof value === "boolean") return;
|
|
@@ -50,7 +43,7 @@ function validateOperands(expression, minimum) {
|
|
|
50
43
|
}
|
|
51
44
|
}
|
|
52
45
|
|
|
53
|
-
function inspectExpression(expression, depth, state) {
|
|
46
|
+
function inspectExpression(expression, depth, state, optionalPathRoot = false) {
|
|
54
47
|
if (!isPlainObject(expression)) {
|
|
55
48
|
throw new TypeError("Computed expression nodes must be plain objects.");
|
|
56
49
|
}
|
|
@@ -72,7 +65,9 @@ function inspectExpression(expression, depth, state) {
|
|
|
72
65
|
}
|
|
73
66
|
if (kinds[0] === "field") {
|
|
74
67
|
rejectUnknownKeys(expression, ["field"], "computed field expression");
|
|
75
|
-
|
|
68
|
+
const field = requireField(expression.field, "Computed expression field");
|
|
69
|
+
state.fields.add(field);
|
|
70
|
+
if (!optionalPathRoot) state.requiredFields.add(field);
|
|
76
71
|
return;
|
|
77
72
|
}
|
|
78
73
|
if (kinds[0] === "constant") {
|
|
@@ -88,7 +83,47 @@ function inspectExpression(expression, depth, state) {
|
|
|
88
83
|
inspectExpression(expression.right, depth + 1, state);
|
|
89
84
|
return;
|
|
90
85
|
}
|
|
91
|
-
if (
|
|
86
|
+
if (expression.op === "getPath") {
|
|
87
|
+
rejectUnknownKeys(expression, ["op", "operand", "path"], "computed path expression");
|
|
88
|
+
if (!isPlainObject(expression.operand) ||
|
|
89
|
+
Object.keys(expression.operand).length !== 1 ||
|
|
90
|
+
!Object.hasOwn(expression.operand, "field")) {
|
|
91
|
+
throw new TypeError("Computed getPath operand must be a literal field reference.");
|
|
92
|
+
}
|
|
93
|
+
if (!Array.isArray(expression.path) || expression.path.length < 1 ||
|
|
94
|
+
expression.path.length > 8 || !Array.from(expression.path).every(key =>
|
|
95
|
+
typeof key === "string" && key.length > 0 && !RESERVED_PATH_KEYS.has(key))) {
|
|
96
|
+
throw new TypeError("Computed getPath path must contain 1 to 8 nonempty, nonreserved string keys.");
|
|
97
|
+
}
|
|
98
|
+
inspectExpression(expression.operand, depth + 1, state, true);
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
if (expression.op === "replaceLiteral") {
|
|
102
|
+
rejectUnknownKeys(expression, ["op", "operand", "search", "replacement"], "computed literal replacement");
|
|
103
|
+
requireNode(expression, "operand", expression.op);
|
|
104
|
+
if (typeof expression.search !== "string" || expression.search.length === 0 ||
|
|
105
|
+
typeof expression.replacement !== "string") {
|
|
106
|
+
throw new TypeError("Computed replaceLiteral requires a nonempty string search and string replacement.");
|
|
107
|
+
}
|
|
108
|
+
inspectExpression(expression.operand, depth + 1, state);
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
if (expression.op === "datePart") {
|
|
112
|
+
rejectUnknownKeys(expression, ["op", "part", "operand", "timeZone"], "computed calendar expression");
|
|
113
|
+
requireNode(expression, "operand", expression.op);
|
|
114
|
+
if (expression.part !== "year") throw new Error("Computed datePart supports part year only.");
|
|
115
|
+
if (Object.hasOwn(expression, "timeZone")) validateTimeZone(expression.timeZone);
|
|
116
|
+
inspectExpression(expression.operand, depth + 1, state);
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
if (expression.op === "parseTime") {
|
|
120
|
+
rejectUnknownKeys(expression, ["op", "operand", "format"], "computed time expression");
|
|
121
|
+
requireNode(expression, "operand", expression.op);
|
|
122
|
+
validateParseTimeFormat(expression.format);
|
|
123
|
+
inspectExpression(expression.operand, depth + 1, state);
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
if (NUMERIC_UNARY.has(expression.op) || BOOLEAN_UNARY.has(expression.op) || expression.op === "toNumber") {
|
|
92
127
|
rejectUnknownKeys(expression, ["op", "operand"], "computed unary expression");
|
|
93
128
|
requireNode(expression, "operand", expression.op);
|
|
94
129
|
inspectExpression(expression.operand, depth + 1, state);
|
|
@@ -131,7 +166,7 @@ function inspectComputedTransform(transform) {
|
|
|
131
166
|
throw new Error(`Unsupported computed transform "${transform.type}".`);
|
|
132
167
|
}
|
|
133
168
|
requireField(transform.as, "Computed output field");
|
|
134
|
-
const state = { nodes: 0, fields: new Set(), ancestors: new WeakSet() };
|
|
169
|
+
const state = { nodes: 0, fields: new Set(), requiredFields: new Set(), ancestors: new WeakSet() };
|
|
135
170
|
inspectExpression(transform.expression, 1, state);
|
|
136
171
|
return state;
|
|
137
172
|
}
|
|
@@ -233,6 +268,27 @@ function compare(left, right, operation, rowIndex) {
|
|
|
233
268
|
: order >= 0;
|
|
234
269
|
}
|
|
235
270
|
|
|
271
|
+
function ownDataProperty(record, key, rowIndex) {
|
|
272
|
+
const descriptor = Object.getOwnPropertyDescriptor(record, key);
|
|
273
|
+
if (descriptor === undefined) return undefined;
|
|
274
|
+
if (!Object.hasOwn(descriptor, "value")) {
|
|
275
|
+
throw new TypeError(`Computed getPath cannot read an accessor at row ${rowIndex}.`);
|
|
276
|
+
}
|
|
277
|
+
return descriptor.value;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function nestedScalar(expression, row, rowIndex) {
|
|
281
|
+
let value = ownDataProperty(row, expression.operand.field, rowIndex);
|
|
282
|
+
for (const key of expression.path) {
|
|
283
|
+
if (value === undefined || value === null) return null;
|
|
284
|
+
if (!isPlainObject(value)) {
|
|
285
|
+
throw new TypeError(`Computed getPath requires plain records at row ${rowIndex}.`);
|
|
286
|
+
}
|
|
287
|
+
value = ownDataProperty(value, key, rowIndex);
|
|
288
|
+
}
|
|
289
|
+
return scalar(value, "Computed getPath result", rowIndex);
|
|
290
|
+
}
|
|
291
|
+
|
|
236
292
|
function evaluate(expression, row, rowIndex) {
|
|
237
293
|
if (Object.hasOwn(expression, "field")) {
|
|
238
294
|
return scalar(
|
|
@@ -243,6 +299,16 @@ function evaluate(expression, row, rowIndex) {
|
|
|
243
299
|
}
|
|
244
300
|
if (Object.hasOwn(expression, "constant")) return expression.constant;
|
|
245
301
|
const operation = expression.op;
|
|
302
|
+
if (operation === "getPath") return nestedScalar(expression, row, rowIndex);
|
|
303
|
+
if (operation === "replaceLiteral") {
|
|
304
|
+
const value = evaluate(expression.operand, row, rowIndex);
|
|
305
|
+
if (typeof value !== "string") {
|
|
306
|
+
throw new TypeError(`Computed replaceLiteral requires a string at row ${rowIndex}.`);
|
|
307
|
+
}
|
|
308
|
+
const index = value.indexOf(expression.search);
|
|
309
|
+
return index < 0 ? value
|
|
310
|
+
: value.slice(0, index) + expression.replacement + value.slice(index + expression.search.length);
|
|
311
|
+
}
|
|
246
312
|
if (operation === "if") {
|
|
247
313
|
return boolean(evaluate(expression.condition, row, rowIndex), operation, rowIndex)
|
|
248
314
|
? evaluate(expression.then, row, rowIndex)
|
|
@@ -282,6 +348,29 @@ function evaluate(expression, row, rowIndex) {
|
|
|
282
348
|
? value === null
|
|
283
349
|
: !boolean(value, operation, rowIndex);
|
|
284
350
|
}
|
|
351
|
+
if (operation === "datePart") {
|
|
352
|
+
const value = evaluate(expression.operand, row, rowIndex);
|
|
353
|
+
if (value === null) return null;
|
|
354
|
+
number(value, operation, rowIndex);
|
|
355
|
+
if (Math.abs(value) > 8_640_000_000_000_000) {
|
|
356
|
+
throw new RangeError(`Computed datePart timestamp is outside the supported Date range at row ${rowIndex}.`);
|
|
357
|
+
}
|
|
358
|
+
const timestamp = new Date(value).getTime();
|
|
359
|
+
return (expression.timeZone ?? "UTC") === "UTC"
|
|
360
|
+
? new Date(timestamp).getUTCFullYear()
|
|
361
|
+
: calendarParts(timestamp, expression.timeZone).year;
|
|
362
|
+
}
|
|
363
|
+
if (operation === "parseTime") {
|
|
364
|
+
return parseTimeValue(evaluate(expression.operand, row, rowIndex), expression.format);
|
|
365
|
+
}
|
|
366
|
+
if (operation === "toNumber") {
|
|
367
|
+
const value = evaluate(expression.operand, row, rowIndex);
|
|
368
|
+
if (typeof value === "number") return number(value, operation, rowIndex);
|
|
369
|
+
if (typeof value !== "string" || !/^[+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?$/.test(value.trim())) {
|
|
370
|
+
throw new TypeError(`Computed toNumber requires a finite number or decimal string at row ${rowIndex}.`);
|
|
371
|
+
}
|
|
372
|
+
return number(Number(value.trim()), operation, rowIndex);
|
|
373
|
+
}
|
|
285
374
|
if (NUMERIC_UNARY.has(operation)) {
|
|
286
375
|
const value = number(evaluate(expression.operand, row, rowIndex), operation, rowIndex);
|
|
287
376
|
if (operation === "log" && value <= 0) {
|
|
@@ -292,6 +381,7 @@ function evaluate(expression, row, rowIndex) {
|
|
|
292
381
|
}
|
|
293
382
|
const result = operation === "negate" ? -value
|
|
294
383
|
: operation === "absolute" ? Math.abs(value)
|
|
384
|
+
: operation === "floor" ? Math.floor(value)
|
|
295
385
|
: operation === "log" ? Math.log(value)
|
|
296
386
|
: Math.sqrt(value);
|
|
297
387
|
if (!Number.isFinite(result)) {
|
|
@@ -318,7 +408,7 @@ function evaluate(expression, row, rowIndex) {
|
|
|
318
408
|
}
|
|
319
409
|
|
|
320
410
|
export function deriveComputedRows(rows, transform) {
|
|
321
|
-
const { nodes, fields } = inspectComputedTransform(transform);
|
|
411
|
+
const { nodes, fields, requiredFields } = inspectComputedTransform(transform);
|
|
322
412
|
if (rows.length * nodes > MAX_WORK) {
|
|
323
413
|
throw new RangeError(`Computed work cannot exceed ${MAX_WORK} row-nodes.`);
|
|
324
414
|
}
|
|
@@ -326,15 +416,14 @@ export function deriveComputedRows(rows, transform) {
|
|
|
326
416
|
throw new Error(`Computed output field "${transform.as}" already exists.`);
|
|
327
417
|
}
|
|
328
418
|
if (rows.length > 0) {
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
);
|
|
419
|
+
for (const field of fields) {
|
|
420
|
+
const knownOptional = !requiredFields.has(field) && rows.some(row => Object.hasOwn(row, field));
|
|
421
|
+
rows.forEach((row, index) => {
|
|
422
|
+
if (!knownOptional && !Object.hasOwn(row, field)) {
|
|
423
|
+
throw new Error(`Computed source does not contain field "${field}" at row ${index}.`);
|
|
335
424
|
}
|
|
336
|
-
}
|
|
337
|
-
}
|
|
425
|
+
});
|
|
426
|
+
}
|
|
338
427
|
}
|
|
339
428
|
let outputType;
|
|
340
429
|
return rows.map((row, index) => {
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { freezeOwned, isPlainObject } from "../core/immutable.js";
|
|
2
|
-
import {
|
|
3
|
-
import { buildLinearPathCommands } from "./pathCommands.js";
|
|
2
|
+
import { buildLinearPathCommands, validatePathCommandCount } from "./pathCommands.js";
|
|
4
3
|
|
|
5
4
|
export const CURVE_INTERPOLATIONS = Object.freeze([
|
|
6
5
|
"linear",
|
|
@@ -42,6 +41,15 @@ function validatePoints(points) {
|
|
|
42
41
|
return points;
|
|
43
42
|
}
|
|
44
43
|
|
|
44
|
+
export function validateCardinalTension(tension, curve) {
|
|
45
|
+
if (tension === undefined) return 0;
|
|
46
|
+
if (curve !== "cardinal") throw new Error("Line tension requires cardinal interpolation.");
|
|
47
|
+
if (!Number.isFinite(tension) || tension < 0 || tension > 1) {
|
|
48
|
+
throw new RangeError("Cardinal tension must be between 0 and 1.");
|
|
49
|
+
}
|
|
50
|
+
return tension;
|
|
51
|
+
}
|
|
52
|
+
|
|
45
53
|
export function validateCurveInterpolation(curve) {
|
|
46
54
|
if (!CURVE_INTERPOLATIONS.includes(curve)) {
|
|
47
55
|
throw new Error(`Unsupported curve interpolation "${curve}".`);
|
|
@@ -134,7 +142,7 @@ function buildBasisCommands(points) {
|
|
|
134
142
|
return ownCommands(commands);
|
|
135
143
|
}
|
|
136
144
|
|
|
137
|
-
function buildCardinalCommands(points) {
|
|
145
|
+
function buildCardinalCommands(points, tension = 0) {
|
|
138
146
|
const commands = [{ op: "M", x: points[0].x, y: points[0].y }];
|
|
139
147
|
for (let index = 0; index < points.length - 1; index += 1) {
|
|
140
148
|
const before = points[index - 1] ?? points[index];
|
|
@@ -143,10 +151,10 @@ function buildCardinalCommands(points) {
|
|
|
143
151
|
const after = points[index + 2] ?? next;
|
|
144
152
|
commands.push({
|
|
145
153
|
op: "C",
|
|
146
|
-
x1: current.x + (next.x - before.x) / 6,
|
|
147
|
-
y1: current.y + (next.y - before.y) / 6,
|
|
148
|
-
x2: next.x - (after.x - current.x) / 6,
|
|
149
|
-
y2: next.y - (after.y - current.y) / 6,
|
|
154
|
+
x1: current.x + (next.x - before.x) / 6 * (1 - tension),
|
|
155
|
+
y1: current.y + (next.y - before.y) / 6 * (1 - tension),
|
|
156
|
+
x2: next.x - (after.x - current.x) / 6 * (1 - tension),
|
|
157
|
+
y2: next.y - (after.y - current.y) / 6 * (1 - tension),
|
|
150
158
|
x: next.x,
|
|
151
159
|
y: next.y
|
|
152
160
|
});
|
|
@@ -265,7 +273,7 @@ function buildNaturalCommands(points) {
|
|
|
265
273
|
return ownCommands(commands);
|
|
266
274
|
}
|
|
267
275
|
|
|
268
|
-
function buildValidatedCurvePathCommands(points, curve) {
|
|
276
|
+
function buildValidatedCurvePathCommands(points, curve, tension) {
|
|
269
277
|
if (curve === "linear" ||
|
|
270
278
|
(SMOOTH_CURVES.has(curve) &&
|
|
271
279
|
curve !== "monotone" &&
|
|
@@ -277,7 +285,7 @@ function buildValidatedCurvePathCommands(points, curve) {
|
|
|
277
285
|
}
|
|
278
286
|
if (curve === "basis") return buildBasisCommands(points);
|
|
279
287
|
if (curve === "cardinal") {
|
|
280
|
-
return buildCardinalCommands(points);
|
|
288
|
+
return buildCardinalCommands(points, tension);
|
|
281
289
|
}
|
|
282
290
|
if (curve === "monotone") {
|
|
283
291
|
return buildMonotoneCommands(points);
|
|
@@ -301,17 +309,19 @@ function curveCommandCount(pointCount, curve) {
|
|
|
301
309
|
: pointCount;
|
|
302
310
|
}
|
|
303
311
|
|
|
304
|
-
export function buildCurvePathCommands(points, curve = "linear") {
|
|
312
|
+
export function buildCurvePathCommands(points, curve = "linear", tension) {
|
|
313
|
+
validateCardinalTension(tension, curve);
|
|
305
314
|
const validatedPoints = validatePoints(points);
|
|
306
315
|
const validatedCurve = validateCurveInterpolation(curve);
|
|
307
|
-
|
|
316
|
+
validatePathCommandCount(
|
|
308
317
|
curveCommandCount(validatedPoints.length, validatedCurve),
|
|
309
318
|
"Curve path command count"
|
|
310
319
|
);
|
|
311
320
|
const direct = buildValidatedCurvePathCommands(
|
|
312
321
|
validatedPoints,
|
|
313
|
-
validatedCurve
|
|
322
|
+
validatedCurve, tension
|
|
314
323
|
);
|
|
324
|
+
validatePathCommandCount(direct.length, "Curve path command count");
|
|
315
325
|
if (finiteCommands(direct)) return direct;
|
|
316
326
|
|
|
317
327
|
let xScale = 0;
|
|
@@ -327,7 +337,7 @@ export function buildCurvePathCommands(points, curve = "linear") {
|
|
|
327
337
|
x: point.x / xScale,
|
|
328
338
|
y: point.y / yScale
|
|
329
339
|
})),
|
|
330
|
-
validatedCurve
|
|
340
|
+
validatedCurve, tension
|
|
331
341
|
);
|
|
332
342
|
const restored = normalized.map(command => {
|
|
333
343
|
const copy = {};
|
|
@@ -341,6 +351,7 @@ export function buildCurvePathCommands(points, curve = "linear") {
|
|
|
341
351
|
if (!finiteCommands(restored)) {
|
|
342
352
|
throw new RangeError("Curve interpolation exceeds the finite numeric range.");
|
|
343
353
|
}
|
|
354
|
+
validatePathCommandCount(restored.length, "Curve path command count");
|
|
344
355
|
return ownCommands(restored);
|
|
345
356
|
}
|
|
346
357
|
|
|
@@ -403,7 +414,7 @@ export function buildAreaCurvePathCommands(
|
|
|
403
414
|
const validatedCurve = validateCurveInterpolation(curve);
|
|
404
415
|
const validatedLower = validatePoints(lowerPoints);
|
|
405
416
|
const validatedUpper = validatePoints(upperPoints);
|
|
406
|
-
|
|
417
|
+
validatePathCommandCount(
|
|
407
418
|
curveCommandCount(validatedLower.length, validatedCurve) +
|
|
408
419
|
curveCommandCount(validatedUpper.length, validatedCurve) + 1,
|
|
409
420
|
"Area path command count"
|
|
@@ -416,10 +427,12 @@ export function buildAreaCurvePathCommands(
|
|
|
416
427
|
const upper = reverseOpenPathCommands(
|
|
417
428
|
buildOrientedCurvePathCommands(validatedUpper, validatedCurve, independentAxis)
|
|
418
429
|
);
|
|
419
|
-
|
|
430
|
+
const commands = [
|
|
420
431
|
...lower,
|
|
421
432
|
{ op: "L", x: upper[0].x, y: upper[0].y },
|
|
422
433
|
...upper.slice(1),
|
|
423
434
|
{ op: "Z" }
|
|
424
|
-
]
|
|
435
|
+
];
|
|
436
|
+
validatePathCommandCount(commands.length, "Area path command count");
|
|
437
|
+
return ownCommands(commands);
|
|
425
438
|
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import { requireStringValue as requireName } from "../core/validation.js";
|
|
1
2
|
import { annotateError } from "../core/diagnostics.js";
|
|
2
3
|
import { cloneAndFreeze, isPlainObject } from "../core/immutable.js";
|
|
4
|
+
import { requireRegressionStatisticsSource, REGRESSION_STATISTIC_FIELDS } from "./regression/statistics.js";
|
|
3
5
|
|
|
4
6
|
const STORAGE_TYPES = new Set([
|
|
5
7
|
"number", "string", "boolean", "array", "object", "unknown", "mixed"
|
|
@@ -19,12 +21,6 @@ function rejectUnknownKeys(value, keys, label) {
|
|
|
19
21
|
if (unknown !== undefined) throw new Error(`Unknown ${label} property "${unknown}".`);
|
|
20
22
|
}
|
|
21
23
|
|
|
22
|
-
function requireName(value, label) {
|
|
23
|
-
if (typeof value !== "string" || value.length === 0) {
|
|
24
|
-
throw new TypeError(`${label} must be a non-empty string.`);
|
|
25
|
-
}
|
|
26
|
-
return value;
|
|
27
|
-
}
|
|
28
24
|
|
|
29
25
|
function valueStorageType(value) {
|
|
30
26
|
if (typeof value === "number") return "number";
|
|
@@ -61,7 +57,8 @@ function normalizeField(value, index, { declared = false } = {}) {
|
|
|
61
57
|
declared ? SOURCE_FIELD_KEYS : FIELD_KEYS,
|
|
62
58
|
`dataset schema field ${index}`
|
|
63
59
|
);
|
|
64
|
-
|
|
60
|
+
// Stored row keys are lossless; action field references still require non-empty names.
|
|
61
|
+
const name = value.name === "" ? "" : requireName(value.name, `Dataset schema field ${index} name`);
|
|
65
62
|
const allowed = declared ? DECLARED_STORAGE_TYPES : STORAGE_TYPES;
|
|
66
63
|
if (!allowed.has(value.storageType)) {
|
|
67
64
|
throw new Error(`Unsupported dataset schema storageType "${value.storageType}".`);
|
|
@@ -271,7 +268,7 @@ function generatedField(name, storageType, transform, role, inputs = []) {
|
|
|
271
268
|
};
|
|
272
269
|
}
|
|
273
270
|
|
|
274
|
-
export function deriveTransformSchema(inputSchema, transform, values, { sourceId, ownerId } = {}) {
|
|
271
|
+
export function deriveTransformSchema(inputSchema, transform, values, { sourceId, ownerId, sourceTransform } = {}) {
|
|
275
272
|
const input = validateDatasetSchema(inputSchema);
|
|
276
273
|
const definition = { ...transform, __sourceId: sourceId, __ownerId: ownerId };
|
|
277
274
|
const retain = ["filter", "markFilter", "sort", "computed", "timeUnit", "window", "normalize", "stack", "impute", "complete"];
|
|
@@ -304,6 +301,12 @@ export function deriveTransformSchema(inputSchema, transform, values, { sourceId
|
|
|
304
301
|
generatedField(transform.as.count, "number", definition, "count", [transform.x, transform.y])
|
|
305
302
|
];
|
|
306
303
|
if (transform.as.members !== undefined) push(generatedField(transform.as.members, "array", definition, "members"));
|
|
304
|
+
} else if (transform.type === "regressionStatistics") {
|
|
305
|
+
const fit = requireRegressionStatisticsSource({ transform: sourceTransform === undefined ? [] : [sourceTransform] });
|
|
306
|
+
fields = [
|
|
307
|
+
...(fit.groupBy === undefined ? [] : [inheritedField(input, fit.groupBy)]),
|
|
308
|
+
...REGRESSION_STATISTIC_FIELDS.map(name => generatedField(name, "number", definition, name))
|
|
309
|
+
].filter(Boolean);
|
|
307
310
|
} else if (transform.type === "regression") {
|
|
308
311
|
fields = [inheritedField(input, transform.x), inheritedField(input, transform.y)].filter(Boolean);
|
|
309
312
|
if (transform.groupBy !== undefined) fields.unshift(inheritedField(input, transform.groupBy));
|
|
@@ -371,7 +374,8 @@ export function deriveTransformSchema(inputSchema, transform, values, { sourceId
|
|
|
371
374
|
if (planned.lineage === undefined && !["complete", "impute"].includes(transform.type)) {
|
|
372
375
|
return planned;
|
|
373
376
|
}
|
|
374
|
-
return { ...planned, storageType:
|
|
377
|
+
return { ...planned, storageType: transform.type === "regressionStatistics" && planned.storageType === "number"
|
|
378
|
+
? "number" : field.storageType, nullable: field.nullable, optional: field.optional };
|
|
375
379
|
});
|
|
376
380
|
for (const field of inferred.fields) {
|
|
377
381
|
if (!fields.some(planned => planned.name === field.name)) fields.push(field);
|