ggaction 0.0.12 → 0.0.13
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 +49 -0
- package/README.md +2 -2
- package/knowledge/action-card.schema.json +1 -1
- package/knowledge/action-cards.json +1 -1
- package/knowledge/action-cards.schema.json +1 -1
- package/knowledge/extension-authoring.md +2 -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 +4 -2
- package/src/actions/basic.js +2 -0
- package/src/actions/boxPlots/materialize.js +2 -1
- package/src/actions/canvas/actions.js +3 -1
- package/src/actions/canvas/fitting.js +211 -0
- package/src/actions/canvas/index.js +2 -0
- package/src/actions/categoryOrder/index.js +3 -2
- package/src/actions/charts/area.js +47 -0
- package/src/actions/charts/bar.js +11 -5
- package/src/actions/charts/beeswarm.js +70 -0
- package/src/actions/charts/density.js +44 -0
- package/src/actions/charts/ecdf.js +183 -0
- package/src/actions/charts/endpoints.js +451 -0
- package/src/actions/charts/heatmap.js +2 -2
- package/src/actions/charts/histogram.js +1 -1
- package/src/actions/charts/horizon.js +71 -0
- package/src/actions/charts/index.js +37 -0
- package/src/actions/charts/interval-regression.js +169 -0
- package/src/actions/charts/line.js +5 -5
- package/src/actions/charts/parallel.js +1 -1
- package/src/actions/charts/pie.js +44 -0
- package/src/actions/charts/polar.js +123 -0
- package/src/actions/charts/radar.js +299 -0
- package/src/actions/charts/radial.js +42 -0
- package/src/actions/charts/raincloud.js +422 -0
- package/src/actions/charts/rug-strip.js +279 -0
- package/src/actions/charts/scatter.js +9 -3
- package/src/actions/charts/shared.js +69 -10
- package/src/actions/composition/actions.js +118 -1
- package/src/actions/data/bin.js +29 -0
- package/src/actions/data/computed.js +24 -0
- package/src/actions/data/derived.js +66 -0
- package/src/actions/data/ecdf.js +38 -0
- package/src/actions/data/filter.js +212 -28
- package/src/actions/data/fold.js +24 -0
- package/src/actions/data/index.js +24 -0
- package/src/actions/data/interval.js +2 -0
- package/src/actions/data/intervalEdit.js +86 -4
- package/src/actions/data/regression.js +5 -2
- package/src/actions/data/stack.js +26 -0
- package/src/actions/data/summary.js +26 -0
- package/src/actions/data/timeUnit.js +1 -1
- package/src/actions/encodings/appearance.js +32 -13
- package/src/actions/encodings/barWidth.js +8 -0
- package/src/actions/encodings/basic.js +2 -0
- package/src/actions/encodings/color/continuous.js +19 -59
- package/src/actions/encodings/color/index.js +28 -22
- package/src/actions/encodings/color/layout.js +3 -97
- package/src/actions/encodings/color/policy.js +5 -8
- package/src/actions/encodings/density/resolve.js +37 -5
- package/src/actions/encodings/density.js +120 -33
- package/src/actions/encodings/horizon/resolve.js +8 -4
- package/src/actions/encodings/horizon.js +18 -8
- package/src/actions/encodings/index.js +2 -0
- package/src/actions/encodings/offset.js +10 -16
- package/src/actions/encodings/parallel.js +2 -8
- package/src/actions/encodings/position/apply.js +10 -4
- package/src/actions/encodings/position/index.js +43 -5
- package/src/actions/encodings/position/policies/arc.js +14 -0
- package/src/actions/encodings/position/policies/area.js +4 -42
- package/src/actions/encodings/position/policies/bar.js +48 -13
- package/src/actions/encodings/position/resolve.js +67 -22
- package/src/actions/encodings/ranged.js +123 -97
- package/src/actions/encodings/remove.js +37 -32
- package/src/actions/encodings/ruleAppearance.js +23 -9
- package/src/actions/encodings/seriesLayout.js +104 -0
- package/src/actions/encodings/shared.js +25 -23
- package/src/actions/encodings/strokeDash.js +9 -6
- package/src/actions/encodings/temporal.js +7 -0
- package/src/actions/encodings/text.js +30 -15
- package/src/actions/errorBands/create.js +18 -5
- package/src/actions/errorBands/edit.js +341 -21
- package/src/actions/errorBars/create.js +21 -10
- package/src/actions/errorBars/edit.js +355 -19
- package/src/actions/facets/actions.js +410 -12
- package/src/actions/facets/derive.js +57 -8
- package/src/actions/gradientPlots/components.js +33 -11
- package/src/actions/gradientPlots/materialize.js +5 -7
- package/src/actions/guides/applicability.js +20 -17
- package/src/actions/guides/axes/axes.js +4 -3
- package/src/actions/guides/axes/axis.js +18 -18
- package/src/actions/guides/axes/components.js +43 -0
- package/src/actions/guides/axes/edit.js +16 -51
- package/src/actions/guides/axes/labels.js +129 -19
- package/src/actions/guides/axes/lines.js +7 -5
- package/src/actions/guides/axes/parallel/lifecycle.js +117 -0
- package/src/actions/guides/axes/parallel/policy.js +137 -0
- package/src/actions/guides/axes/parallel/resolve.js +44 -23
- package/src/actions/guides/axes/parallel.js +93 -217
- package/src/actions/guides/axes/policy.js +21 -67
- package/src/actions/guides/axes/tickGroups.js +14 -8
- package/src/actions/guides/axes/ticks.js +5 -4
- package/src/actions/guides/axes/titles.js +21 -8
- package/src/actions/guides/basic.js +2 -0
- package/src/actions/guides/facade.js +106 -0
- package/src/actions/guides/facadeAxes.js +200 -0
- package/src/actions/guides/facadeLegend.js +63 -0
- package/src/actions/guides/grids/grid.js +37 -33
- package/src/actions/guides/grids/resolve.js +10 -2
- package/src/actions/guides/guides.js +35 -28
- package/src/actions/guides/legends/categorical/actions.js +193 -249
- package/src/actions/guides/legends/categorical/components.js +4 -5
- package/src/actions/guides/legends/categorical/layout.js +36 -349
- package/src/actions/guides/legends/categorical/options.js +30 -14
- package/src/actions/guides/legends/categorical/recipes.js +14 -0
- package/src/actions/guides/legends/categorical/resolve.js +21 -10
- package/src/actions/guides/legends/categorical/symbols.js +2 -3
- package/src/actions/guides/legends/continuous/common.js +69 -14
- package/src/actions/guides/legends/continuous/gradient.js +111 -83
- package/src/actions/guides/legends/continuous/index.js +6 -2
- package/src/actions/guides/legends/continuous/interval.js +71 -92
- package/src/actions/guides/legends/continuous/opacity.js +105 -84
- package/src/actions/guides/legends/creation.js +27 -0
- package/src/actions/guides/legends/edit.js +198 -66
- package/src/actions/guides/legends/focused.js +2 -2
- package/src/actions/guides/legends/lane.js +98 -86
- package/src/actions/guides/legends/lifecycle.js +132 -0
- package/src/actions/guides/legends/remove.js +31 -76
- package/src/actions/guides/legends/size.js +149 -105
- package/src/actions/guides/legends/strokeWidth.js +106 -64
- package/src/actions/guides/legends/target.js +12 -0
- package/src/actions/guides/legends/transition.js +43 -0
- package/src/actions/guides/polar/axes/facade.js +77 -62
- package/src/actions/guides/polar/axes/labels.js +5 -4
- package/src/actions/guides/polar/axes/lines.js +2 -1
- package/src/actions/guides/polar/axes/shared.js +6 -0
- package/src/actions/guides/polar/axes/ticks.js +5 -4
- package/src/actions/guides/polar/axes/titles.js +5 -4
- package/src/actions/guides/polar/grids.js +7 -7
- package/src/actions/guides/reuse.js +44 -0
- package/src/actions/guides/tickValues.js +19 -21
- package/src/actions/index.js +2 -0
- package/src/actions/intervals/resolve.js +35 -15
- package/src/actions/marks/arc/actions.js +1 -1
- package/src/actions/marks/area/actions.js +18 -4
- package/src/actions/marks/area/materialize.js +5 -2
- package/src/actions/marks/bar/materialize.js +11 -7
- package/src/actions/marks/index.js +5 -0
- package/src/actions/marks/line/actions.js +43 -108
- package/src/actions/marks/line/materialize.js +34 -52
- package/src/actions/marks/point/edit.js +11 -15
- package/src/actions/marks/point/index.js +1 -0
- package/src/actions/marks/point/jitter.js +3 -0
- package/src/actions/marks/point/materialize.js +106 -2
- package/src/actions/marks/point/packing.js +68 -0
- package/src/actions/marks/references.js +87 -0
- package/src/actions/marks/remove.js +17 -4
- package/src/actions/marks/rule/actions.js +34 -3
- package/src/actions/marks/rule/appearance.js +26 -0
- package/src/actions/marks/shared.js +12 -12
- package/src/actions/marks/text/actions.js +224 -6
- package/src/actions/marks/text/layout.js +2 -2
- package/src/actions/primitives/createGraphics.js +3 -1
- package/src/actions/primitives/semantic.js +1 -1
- package/src/actions/primitives/semanticAction.js +39 -2
- package/src/actions/primitives/semanticValidation/guide.js +10 -0
- package/src/actions/primitives/semanticValidation/layer.js +18 -1
- package/src/actions/primitives/semanticValidation/scale.js +10 -1
- package/src/actions/regression/create.js +1 -1
- package/src/actions/regression/edit.js +19 -6
- package/src/actions/regression/resolve.js +1 -1
- package/src/actions/scales/consumers/common.js +20 -53
- package/src/actions/scales/consumers/families.js +27 -14
- package/src/actions/scales/consumers/index.js +23 -2
- package/src/actions/scales/consumers/seriesLayout.js +33 -2
- package/src/actions/scales/create.js +4 -4
- package/src/actions/scales/definitions.js +19 -3
- package/src/actions/scales/edit.js +46 -34
- package/src/actions/scales/editPolicy.js +21 -31
- package/src/actions/scales/materialize.js +10 -46
- package/src/actions/scales/patch.js +1 -1
- package/src/actions/scales/preview.js +39 -0
- package/src/actions/scales/quantitativeColor.js +9 -5
- package/src/actions/theme/actions.js +43 -0
- package/src/actions/theme/index.js +9 -0
- package/src/actions/theme/reconcile.js +882 -0
- package/src/actions/titles/actions.js +11 -10
- package/src/actions/titles/resolve.js +1 -40
- package/src/actions/violinPlots/create.js +86 -42
- package/src/actions/violinPlots/edit.js +191 -0
- package/src/actions/violinPlots/index.js +2 -0
- package/src/core/ChartProgram.js +7 -0
- package/src/core/action.js +22 -2
- package/src/core/compositionState.js +136 -8
- package/src/grammar/aggregate.js +66 -23
- package/src/grammar/arcs.js +58 -1
- package/src/grammar/areaEndpoints.js +46 -0
- package/src/grammar/areaSeries.js +87 -41
- package/src/grammar/bars/aggregate.js +33 -12
- package/src/grammar/bars/policy.js +9 -7
- package/src/grammar/bin.js +181 -0
- package/src/grammar/categoryOrder.js +34 -0
- package/src/grammar/computed.js +150 -0
- package/src/grammar/ecdf.js +198 -0
- package/src/grammar/facets/index.js +117 -6
- package/src/grammar/fold.js +123 -0
- package/src/grammar/horizon.js +6 -4
- package/src/grammar/interval.js +37 -20
- package/src/grammar/lineSeries.js +20 -63
- package/src/grammar/markFilter.js +22 -4
- package/src/grammar/markLabels.js +93 -0
- package/src/grammar/parallelAxisTitles.js +16 -0
- package/src/grammar/pathSeries.js +91 -0
- package/src/grammar/pointPacking.js +183 -0
- package/src/grammar/positionDatum.js +12 -0
- package/src/grammar/rects.js +9 -1
- package/src/grammar/regression/derive.js +4 -0
- package/src/grammar/regression/index.js +7 -2
- package/src/grammar/regression/models.js +22 -7
- package/src/grammar/regression/parameters.js +40 -13
- package/src/grammar/rotation.js +29 -0
- package/src/grammar/rules.js +7 -21
- package/src/grammar/scales/color.js +23 -1
- package/src/grammar/scales/colorConsumers.js +38 -0
- package/src/grammar/scales/definition.js +15 -1
- package/src/grammar/scales/discretized.js +11 -5
- package/src/grammar/scales/fields.js +38 -8
- package/src/grammar/scales/mapping.js +2 -0
- package/src/grammar/scales/radial.js +55 -0
- package/src/grammar/scales/types.js +1 -0
- package/src/grammar/schemas/semanticPath.js +17 -3
- package/src/grammar/stack.js +170 -0
- package/src/grammar/statistics/confidenceInterval.js +101 -0
- package/src/grammar/summary.js +183 -0
- package/src/grammar/text.js +22 -16
- package/src/grammar/timeUnit.js +21 -11
- package/src/grammar/transformTopology.js +6 -0
- package/src/grammar/transforms.js +36 -0
- package/src/grammar/valueFormat.js +112 -0
- package/src/grammar/window.js +13 -7
- package/src/layout/facets.js +41 -7
- package/src/layout/guideCollisions.js +12 -0
- package/src/layout/legend.js +2 -10
- package/src/layout/legendItems.js +102 -0
- package/src/layout/legendLane.js +100 -11
- package/src/materialization/bars/aggregate.js +38 -152
- package/src/materialization/bars/histogram.js +24 -0
- package/src/materialization/categorySlotOffset.js +40 -0
- package/src/materialization/compositionSnapshot.js +1 -1
- package/src/materialization/dependencies.js +57 -16
- package/src/materialization/encodings.js +1 -1
- package/src/materialization/facets.js +8 -1
- package/src/materialization/guides/layout.js +57 -0
- package/src/materialization/guides/resources.js +2 -2
- package/src/materialization/legends.js +10 -1
- package/src/materialization/marks/capabilities.js +20 -5
- package/src/materialization/marks/grouping.js +52 -0
- package/src/materialization/marks/index.js +58 -23
- package/src/materialization/marks/policies.js +2 -0
- package/src/materialization/rect.js +33 -33
- package/src/materialization/rowEncoding.js +13 -13
- package/src/materialization/scaleGuideDependencies.js +8 -5
- package/src/materialization/scales/map.js +1 -0
- package/src/materialization/scales/policies/arc.js +42 -0
- package/src/materialization/scales/policies/binnedPosition.js +7 -4
- package/src/materialization/scales/policies/series.js +2 -1
- package/src/materialization/scales/resolve.js +46 -14
- package/src/materialization/selection/compatibility.js +11 -0
- package/src/materialization/selection/items/bar.js +12 -68
- package/src/materialization/selection/items/common.js +8 -2
- package/src/materialization/selection/items/path.js +3 -13
- package/src/materialization/selection/items/rect.js +7 -4
- package/src/materialization/text.js +64 -5
- package/src/mcp/adapter.js +2 -2
- package/src/selectors/datasets.js +13 -0
- package/src/selectors/layers.js +3 -2
- package/src/selectors/scales.js +6 -0
- package/src/theme/defaults.js +39 -0
- package/types/basic.d.ts +11 -2
- package/types/index.d.ts +126 -0
- package/types/program.d.ts +1320 -135
- package/src/materialization/bars/grouped.js +0 -144
|
@@ -11,24 +11,23 @@ function validateFacetSpec(compositionSpec) {
|
|
|
11
11
|
}
|
|
12
12
|
if (
|
|
13
13
|
!Number.isInteger(compositionSpec.columns) ||
|
|
14
|
-
compositionSpec.columns <= 0
|
|
15
|
-
compositionSpec.columns > compositionSpec.children.length
|
|
14
|
+
compositionSpec.columns <= 0
|
|
16
15
|
) {
|
|
17
16
|
throw new RangeError(
|
|
18
|
-
"Facet compositionSpec.columns must be a positive integer
|
|
17
|
+
"Facet compositionSpec.columns must be a positive integer."
|
|
19
18
|
);
|
|
20
19
|
}
|
|
21
20
|
if (!isPlainObject(compositionSpec.facet)) {
|
|
22
21
|
throw new TypeError("Facet compositionSpec.facet must be a plain object.");
|
|
23
22
|
}
|
|
24
|
-
const facetKeys = ["data", "field", "values", "scales", "guides"];
|
|
23
|
+
const facetKeys = ["data", "field", "values", "grid", "repeat", "scales", "guides"];
|
|
25
24
|
const unknownFacet = Object.keys(compositionSpec.facet).find(
|
|
26
25
|
key => !facetKeys.includes(key)
|
|
27
26
|
);
|
|
28
27
|
if (unknownFacet !== undefined) {
|
|
29
28
|
throw new Error(`Unknown compositionSpec.facet property "${unknownFacet}".`);
|
|
30
29
|
}
|
|
31
|
-
for (const property of ["data"
|
|
30
|
+
for (const property of ["data"]) {
|
|
32
31
|
if (
|
|
33
32
|
typeof compositionSpec.facet[property] !== "string" ||
|
|
34
33
|
compositionSpec.facet[property].length === 0
|
|
@@ -36,6 +35,129 @@ function validateFacetSpec(compositionSpec) {
|
|
|
36
35
|
throw new TypeError(`compositionSpec.facet.${property} must be a non-empty string.`);
|
|
37
36
|
}
|
|
38
37
|
}
|
|
38
|
+
const grid = compositionSpec.facet.grid;
|
|
39
|
+
const repeat = compositionSpec.facet.repeat;
|
|
40
|
+
if (grid !== undefined && repeat !== undefined) {
|
|
41
|
+
throw new Error("Facet compositionSpec cannot combine grid and repeat recipes.");
|
|
42
|
+
}
|
|
43
|
+
if (grid === undefined && repeat === undefined) {
|
|
44
|
+
if (compositionSpec.columns > compositionSpec.children.length) {
|
|
45
|
+
throw new RangeError(
|
|
46
|
+
"Facet compositionSpec.columns must be no larger than its children."
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
if (
|
|
50
|
+
typeof compositionSpec.facet.field !== "string" ||
|
|
51
|
+
compositionSpec.facet.field.length === 0
|
|
52
|
+
) {
|
|
53
|
+
throw new TypeError("compositionSpec.facet.field must be a non-empty string.");
|
|
54
|
+
}
|
|
55
|
+
} else if (grid !== undefined) {
|
|
56
|
+
if (compositionSpec.columns !== grid.columns?.values?.length) {
|
|
57
|
+
throw new RangeError(
|
|
58
|
+
"Grid facet compositionSpec.columns must match its column domain."
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
if (compositionSpec.facet.field !== undefined) {
|
|
62
|
+
throw new Error("Grid facet compositionSpec does not use facet.field.");
|
|
63
|
+
}
|
|
64
|
+
if (!isPlainObject(grid)) {
|
|
65
|
+
throw new TypeError("compositionSpec.facet.grid must be a plain object.");
|
|
66
|
+
}
|
|
67
|
+
const gridKeys = ["rows", "columns", "combinations", "cells"];
|
|
68
|
+
const unknownGrid = Object.keys(grid).find(key => !gridKeys.includes(key));
|
|
69
|
+
if (unknownGrid !== undefined) {
|
|
70
|
+
throw new Error(`Unknown compositionSpec.facet.grid property "${unknownGrid}".`);
|
|
71
|
+
}
|
|
72
|
+
for (const role of ["rows", "columns"]) {
|
|
73
|
+
const value = grid[role];
|
|
74
|
+
if (!isPlainObject(value) || Object.keys(value).some(
|
|
75
|
+
key => !["field", "values"].includes(key)
|
|
76
|
+
)) {
|
|
77
|
+
throw new TypeError(
|
|
78
|
+
`compositionSpec.facet.grid.${role} requires field and values.`
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
if (typeof value.field !== "string" || value.field.length === 0) {
|
|
82
|
+
throw new TypeError(
|
|
83
|
+
`compositionSpec.facet.grid.${role}.field must be a non-empty string.`
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
if (
|
|
87
|
+
!Array.isArray(value.values) || value.values.length === 0 ||
|
|
88
|
+
value.values.some(item => !(
|
|
89
|
+
typeof item === "string" || typeof item === "boolean" ||
|
|
90
|
+
(typeof item === "number" && Number.isFinite(item))
|
|
91
|
+
)) || new Set(value.values).size !== value.values.length
|
|
92
|
+
) {
|
|
93
|
+
throw new TypeError(
|
|
94
|
+
`compositionSpec.facet.grid.${role}.values must contain unique scalars.`
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
if (grid.rows.field === grid.columns.field) {
|
|
99
|
+
throw new Error("compositionSpec.facet.grid fields must be different.");
|
|
100
|
+
}
|
|
101
|
+
if (!["observed", "full"].includes(grid.combinations)) {
|
|
102
|
+
throw new Error(
|
|
103
|
+
'compositionSpec.facet.grid.combinations must be "observed" or "full".'
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
if (!Array.isArray(grid.cells) || grid.cells.length !== compositionSpec.children.length) {
|
|
107
|
+
throw new TypeError(
|
|
108
|
+
"compositionSpec.facet.grid.cells must contain one descriptor per child."
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
const cellKeys = ["id", "row", "column", "rowValue", "columnValue", "empty"];
|
|
112
|
+
const coordinates = new Set();
|
|
113
|
+
for (const cell of grid.cells) {
|
|
114
|
+
if (!isPlainObject(cell) || Object.keys(cell).some(key => !cellKeys.includes(key))) {
|
|
115
|
+
throw new TypeError("compositionSpec.facet.grid cells must be canonical objects.");
|
|
116
|
+
}
|
|
117
|
+
if (!compositionSpec.children.includes(cell.id)) {
|
|
118
|
+
throw new Error(`Unknown grid facet child "${cell.id}".`);
|
|
119
|
+
}
|
|
120
|
+
if (!Number.isInteger(cell.row) || cell.row < 0 || cell.row >= grid.rows.values.length ||
|
|
121
|
+
!Number.isInteger(cell.column) || cell.column < 0 || cell.column >= grid.columns.values.length) {
|
|
122
|
+
throw new RangeError("Grid facet cell coordinates must be inside their domains.");
|
|
123
|
+
}
|
|
124
|
+
if (cell.rowValue !== grid.rows.values[cell.row] ||
|
|
125
|
+
cell.columnValue !== grid.columns.values[cell.column]) {
|
|
126
|
+
throw new Error("Grid facet cell values must match their row and column coordinates.");
|
|
127
|
+
}
|
|
128
|
+
if (typeof cell.empty !== "boolean") {
|
|
129
|
+
throw new TypeError("Grid facet cell empty must be a boolean.");
|
|
130
|
+
}
|
|
131
|
+
const key = `${cell.row}:${cell.column}`;
|
|
132
|
+
if (coordinates.has(key)) {
|
|
133
|
+
throw new Error("Grid facet cells must occupy unique coordinates.");
|
|
134
|
+
}
|
|
135
|
+
coordinates.add(key);
|
|
136
|
+
}
|
|
137
|
+
} else {
|
|
138
|
+
if (compositionSpec.facet.field !== undefined) {
|
|
139
|
+
throw new Error("Repeat compositionSpec does not use facet.field.");
|
|
140
|
+
}
|
|
141
|
+
if (compositionSpec.columns > compositionSpec.children.length) {
|
|
142
|
+
throw new RangeError(
|
|
143
|
+
"Repeat compositionSpec.columns must be no larger than its children."
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
if (!isPlainObject(repeat) || Object.keys(repeat).some(
|
|
147
|
+
key => !["target", "channel", "fields"].includes(key)
|
|
148
|
+
)) {
|
|
149
|
+
throw new TypeError(
|
|
150
|
+
"compositionSpec.facet.repeat requires target, channel, and fields."
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
if (typeof repeat.target !== "string" || repeat.target.length === 0 ||
|
|
154
|
+
!["x", "y"].includes(repeat.channel) ||
|
|
155
|
+
!Array.isArray(repeat.fields) || repeat.fields.length === 0 ||
|
|
156
|
+
repeat.fields.some(field => typeof field !== "string" || field.length === 0) ||
|
|
157
|
+
new Set(repeat.fields).size !== repeat.fields.length) {
|
|
158
|
+
throw new TypeError("compositionSpec.facet.repeat is not canonical.");
|
|
159
|
+
}
|
|
160
|
+
}
|
|
39
161
|
const values = compositionSpec.facet.values;
|
|
40
162
|
if (
|
|
41
163
|
!Array.isArray(values) ||
|
|
@@ -45,12 +167,18 @@ function validateFacetSpec(compositionSpec) {
|
|
|
45
167
|
typeof value === "boolean" ||
|
|
46
168
|
(typeof value === "number" && Number.isFinite(value))
|
|
47
169
|
)) ||
|
|
48
|
-
new Set(values).size !== values.length
|
|
170
|
+
(grid === undefined && new Set(values).size !== values.length)
|
|
49
171
|
) {
|
|
50
172
|
throw new TypeError(
|
|
51
173
|
"compositionSpec.facet.values must contain one unique scalar per child."
|
|
52
174
|
);
|
|
53
175
|
}
|
|
176
|
+
if (repeat !== undefined && (
|
|
177
|
+
values.length !== repeat.fields.length ||
|
|
178
|
+
values.some((value, index) => value !== repeat.fields[index])
|
|
179
|
+
)) {
|
|
180
|
+
throw new Error("compositionSpec.facet.values must match repeat fields.");
|
|
181
|
+
}
|
|
54
182
|
const scales = compositionSpec.facet.scales;
|
|
55
183
|
if (
|
|
56
184
|
!isPlainObject(scales) ||
|
|
@@ -80,13 +208,13 @@ function validateFacetSpec(compositionSpec) {
|
|
|
80
208
|
function validateCompositionChildren(compositionSpec, childIds, facet) {
|
|
81
209
|
if (
|
|
82
210
|
!Array.isArray(compositionSpec.children) ||
|
|
83
|
-
compositionSpec.children.length <
|
|
211
|
+
compositionSpec.children.length < 1 ||
|
|
84
212
|
!compositionSpec.children.every(id => typeof id === "string" && id.length > 0)
|
|
85
213
|
) {
|
|
86
214
|
throw new TypeError(
|
|
87
215
|
facet
|
|
88
216
|
? "Facet compositionSpec.children requires at least one child ID."
|
|
89
|
-
: "compositionSpec.children requires at least
|
|
217
|
+
: "compositionSpec.children requires at least one child ID."
|
|
90
218
|
);
|
|
91
219
|
}
|
|
92
220
|
if (new Set(compositionSpec.children).size !== compositionSpec.children.length) {
|
package/src/grammar/aggregate.js
CHANGED
|
@@ -7,6 +7,10 @@ import {
|
|
|
7
7
|
stableFiniteMean,
|
|
8
8
|
stableFiniteSum
|
|
9
9
|
} from "./numeric.js";
|
|
10
|
+
import {
|
|
11
|
+
confidenceCriticalValue,
|
|
12
|
+
normalizeConfidenceInterval
|
|
13
|
+
} from "./statistics/confidenceInterval.js";
|
|
10
14
|
|
|
11
15
|
export const SCALAR_AGGREGATE_OPERATIONS = Object.freeze([
|
|
12
16
|
"count", "sum", "mean", "median", "min", "max",
|
|
@@ -17,7 +21,9 @@ export const SCALAR_AGGREGATE_OPERATIONS = Object.freeze([
|
|
|
17
21
|
|
|
18
22
|
const SCALAR_OPERATIONS = new Set(SCALAR_AGGREGATE_OPERATIONS);
|
|
19
23
|
const NOMINAL_OPERATIONS = ["count", "distinct", "valid", "missing"];
|
|
20
|
-
const PARAMETERIZED_OPERATIONS = [
|
|
24
|
+
const PARAMETERIZED_OPERATIONS = [
|
|
25
|
+
"quantile", "first", "last", "ciLower", "ciUpper"
|
|
26
|
+
];
|
|
21
27
|
|
|
22
28
|
function nonEmptyString(value, label) {
|
|
23
29
|
if (typeof value !== "string" || value.length === 0) {
|
|
@@ -83,6 +89,21 @@ export function validateAggregate(value) {
|
|
|
83
89
|
order: value.order ?? "ascending"
|
|
84
90
|
};
|
|
85
91
|
}
|
|
92
|
+
if (value.op === "ciLower" || value.op === "ciUpper") {
|
|
93
|
+
const unknown = Object.keys(value).find(
|
|
94
|
+
key => !["op", "method", "level"].includes(key)
|
|
95
|
+
);
|
|
96
|
+
if (unknown !== undefined) {
|
|
97
|
+
throw new Error(`Unknown confidence aggregate property "${unknown}".`);
|
|
98
|
+
}
|
|
99
|
+
return {
|
|
100
|
+
op: value.op,
|
|
101
|
+
...normalizeConfidenceInterval(value, {
|
|
102
|
+
defaultMethod: "normal",
|
|
103
|
+
label: "Aggregate confidence interval"
|
|
104
|
+
})
|
|
105
|
+
};
|
|
106
|
+
}
|
|
86
107
|
throw new Error(`Unsupported aggregate "${value.op}".`);
|
|
87
108
|
}
|
|
88
109
|
|
|
@@ -194,39 +215,44 @@ export function aggregateScalarValues(values, operation) {
|
|
|
194
215
|
if (!Array.isArray(values)) {
|
|
195
216
|
throw new TypeError("Aggregate values must be an array.");
|
|
196
217
|
}
|
|
197
|
-
validateAggregate(operation);
|
|
198
|
-
|
|
218
|
+
const aggregate = validateAggregate(operation);
|
|
219
|
+
const scalarOperation = isScalarAggregate(aggregate)
|
|
220
|
+
? aggregate
|
|
221
|
+
: ["ciLower", "ciUpper"].includes(aggregate.op)
|
|
222
|
+
? aggregate.op
|
|
223
|
+
: undefined;
|
|
224
|
+
if (scalarOperation === undefined) {
|
|
199
225
|
throw new Error("Scalar aggregate calculation requires a scalar operation.");
|
|
200
226
|
}
|
|
201
227
|
|
|
202
|
-
if (
|
|
228
|
+
if (scalarOperation === "count") return values.length;
|
|
203
229
|
const valid = values.filter(value => !isMissing(value));
|
|
204
|
-
if (
|
|
205
|
-
if (
|
|
206
|
-
if (
|
|
230
|
+
if (scalarOperation === "valid") return valid.length;
|
|
231
|
+
if (scalarOperation === "missing") return values.length - valid.length;
|
|
232
|
+
if (scalarOperation === "distinct") return new Set(valid).size;
|
|
207
233
|
|
|
208
234
|
const finite = finiteValues(values);
|
|
209
235
|
if (finite.length === 0) return undefined;
|
|
210
|
-
if (
|
|
236
|
+
if (scalarOperation === "sum") {
|
|
211
237
|
return stableFiniteSum(finite, "Aggregate sum");
|
|
212
238
|
}
|
|
213
|
-
if (
|
|
214
|
-
if (
|
|
215
|
-
if (
|
|
216
|
-
if (
|
|
217
|
-
if (
|
|
239
|
+
if (scalarOperation === "min") return numericExtent(finite)[0];
|
|
240
|
+
if (scalarOperation === "max") return numericExtent(finite)[1];
|
|
241
|
+
if (scalarOperation === "median") return quantile(finite, 0.5);
|
|
242
|
+
if (scalarOperation === "q1") return quantile(finite, 0.25);
|
|
243
|
+
if (scalarOperation === "q3") return quantile(finite, 0.75);
|
|
218
244
|
|
|
219
245
|
const mean = stableFiniteMean(finite, "Aggregate mean");
|
|
220
|
-
if (
|
|
221
|
-
if (["variance", "varianceP", "stdev", "stdevP"].includes(
|
|
222
|
-
const population =
|
|
246
|
+
if (scalarOperation === "mean") return mean;
|
|
247
|
+
if (["variance", "varianceP", "stdev", "stdevP"].includes(scalarOperation)) {
|
|
248
|
+
const population = scalarOperation.endsWith("P");
|
|
223
249
|
if (!population && finite.length < 2) return undefined;
|
|
224
250
|
const kind = population ? "population" : "sample";
|
|
225
251
|
const deviation = stableFiniteDeviation(finite, {
|
|
226
252
|
sample: !population,
|
|
227
253
|
label: `Aggregate ${kind} deviation`
|
|
228
254
|
});
|
|
229
|
-
if (
|
|
255
|
+
if (scalarOperation.startsWith("stdev")) return deviation.deviation;
|
|
230
256
|
return deviation.squared === undefined
|
|
231
257
|
? requireFiniteResult(
|
|
232
258
|
deviation.deviation ** 2,
|
|
@@ -240,14 +266,25 @@ export function aggregateScalarValues(values, operation) {
|
|
|
240
266
|
divisor: Math.sqrt(finite.length),
|
|
241
267
|
label: "Aggregate standard error"
|
|
242
268
|
}).deviation;
|
|
243
|
-
if (
|
|
244
|
-
|
|
245
|
-
|
|
269
|
+
if (scalarOperation === "stderr") return stderr;
|
|
270
|
+
const confidence = isScalarAggregate(aggregate)
|
|
271
|
+
? normalizeConfidenceInterval({}, {
|
|
272
|
+
defaultMethod: "normal",
|
|
273
|
+
label: "Aggregate confidence interval"
|
|
274
|
+
})
|
|
275
|
+
: aggregate;
|
|
276
|
+
const critical = confidenceCriticalValue({
|
|
277
|
+
method: confidence.method,
|
|
278
|
+
level: confidence.level,
|
|
279
|
+
degreesOfFreedom: finite.length - 1
|
|
280
|
+
});
|
|
281
|
+
if (scalarOperation === "ciLower") {
|
|
282
|
+
return requireFiniteResult(mean - critical * stderr, "Aggregate lower CI");
|
|
246
283
|
}
|
|
247
|
-
if (
|
|
248
|
-
return requireFiniteResult(mean +
|
|
284
|
+
if (scalarOperation === "ciUpper") {
|
|
285
|
+
return requireFiniteResult(mean + critical * stderr, "Aggregate upper CI");
|
|
249
286
|
}
|
|
250
|
-
throw new Error(`Unsupported scalar aggregate "${
|
|
287
|
+
throw new Error(`Unsupported scalar aggregate "${scalarOperation}".`);
|
|
251
288
|
}
|
|
252
289
|
|
|
253
290
|
export function aggregateRows(rows, field, operation) {
|
|
@@ -265,6 +302,9 @@ export function aggregateRows(rows, field, operation) {
|
|
|
265
302
|
? undefined
|
|
266
303
|
: quantile(values, aggregate.probability);
|
|
267
304
|
}
|
|
305
|
+
if (aggregate.op === "ciLower" || aggregate.op === "ciUpper") {
|
|
306
|
+
return aggregateScalarValues(rows.map(row => row[field]), aggregate);
|
|
307
|
+
}
|
|
268
308
|
return aggregateOrderedRows(rows, field, aggregate);
|
|
269
309
|
}
|
|
270
310
|
|
|
@@ -275,5 +315,8 @@ export function formatAggregateTitle(operation, field) {
|
|
|
275
315
|
if (aggregate.op === "quantile") {
|
|
276
316
|
return `quantile(${field}, ${aggregate.probability})`;
|
|
277
317
|
}
|
|
318
|
+
if (aggregate.op === "ciLower" || aggregate.op === "ciUpper") {
|
|
319
|
+
return `${aggregate.op}(${field}, ${aggregate.method}, ${aggregate.level})`;
|
|
320
|
+
}
|
|
278
321
|
return `${aggregate.op}(${field}, ${aggregate.orderBy} ${aggregate.order})`;
|
|
279
322
|
}
|
package/src/grammar/arcs.js
CHANGED
|
@@ -241,6 +241,61 @@ function quantitativeSectors(rows, layer, thetaScale, frame, innerRadiusRatio) {
|
|
|
241
241
|
}));
|
|
242
242
|
}
|
|
243
243
|
|
|
244
|
+
export function deriveMeasuredArcValues(rows, layer) {
|
|
245
|
+
const theta = requireArcLayer(layer);
|
|
246
|
+
const radius = layer.encoding?.radius;
|
|
247
|
+
if (!["nominal", "ordinal"].includes(theta.fieldType) || theta.aggregate !== undefined ||
|
|
248
|
+
!["count", "sum"].includes(radius?.aggregate) || radius.fieldType !== "quantitative") {
|
|
249
|
+
throw new Error("Measured Arc requires equal-angle categorical theta and count or sum radius.");
|
|
250
|
+
}
|
|
251
|
+
if (radius.aggregate === "count" && Object.hasOwn(radius, "field")) {
|
|
252
|
+
throw new Error("Measured count radius does not accept a field.");
|
|
253
|
+
}
|
|
254
|
+
const categories = readNominalField(rows, theta.field);
|
|
255
|
+
const measures = radius.aggregate === "count" ? rows.map(() => 1)
|
|
256
|
+
: readQuantitativeField(rows, radius.field);
|
|
257
|
+
if (measures.some(value => value < 0)) {
|
|
258
|
+
throw new RangeError("Measured Arc radius inputs must be non-negative.");
|
|
259
|
+
}
|
|
260
|
+
const colors = colorValues(rows, layer.encoding?.color);
|
|
261
|
+
const groups = new Map();
|
|
262
|
+
for (const [index, category] of categories.entries()) {
|
|
263
|
+
const group = groups.get(category) ?? { key: category, sourceIndices: [], values: [], colors: [] };
|
|
264
|
+
group.sourceIndices.push(index);
|
|
265
|
+
group.values.push(measures[index]);
|
|
266
|
+
group.colors.push(colors[index]);
|
|
267
|
+
groups.set(category, group);
|
|
268
|
+
}
|
|
269
|
+
const result = [...groups.values()].map(group => {
|
|
270
|
+
const distinctColors = [...new Set(group.colors)];
|
|
271
|
+
if (distinctColors.length > 1) {
|
|
272
|
+
throw new Error(`Measured Arc category "${group.key}" must resolve to one color value.`);
|
|
273
|
+
}
|
|
274
|
+
return { key: group.key, theta: group.key, count: group.sourceIndices.length,
|
|
275
|
+
radius: stableFiniteSum(group.values, "Measured Arc radius aggregate"),
|
|
276
|
+
color: distinctColors[0], sourceIndices: group.sourceIndices };
|
|
277
|
+
});
|
|
278
|
+
if (!result.some(group => group.radius > 0)) {
|
|
279
|
+
throw new Error("Measured Arc radius requires at least one positive category aggregate.");
|
|
280
|
+
}
|
|
281
|
+
return cloneAndFreeze(result);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function measuredRadialSectors(rows, layer, thetaScale, radiusScale) {
|
|
285
|
+
const items = deriveMeasuredArcValues(rows, layer);
|
|
286
|
+
const centers = mapOrdinalPositionValues(items.map(item => item.theta), thetaScale);
|
|
287
|
+
const radii = mapContinuousScaleValues(items.map(item => item.radius), radiusScale);
|
|
288
|
+
const halfBand = (Math.sign(thetaScale.step) || 1) * thetaScale.bandwidth / 2;
|
|
289
|
+
const byCategory = new Map(items.map((item, index) => [item.theta, {
|
|
290
|
+
...item, startTheta: centers[index] - halfBand, endTheta: centers[index] + halfBand,
|
|
291
|
+
innerRadius: radiusScale.range[0], outerRadius: radii[index]
|
|
292
|
+
}]));
|
|
293
|
+
return thetaScale.domain.flatMap(category => {
|
|
294
|
+
const item = byCategory.get(category);
|
|
295
|
+
return item === undefined || item.radius === 0 ? [] : [item];
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
|
|
244
299
|
function radialSectors(rows, layer, thetaScale, radiusScale) {
|
|
245
300
|
const theta = layer.encoding.theta;
|
|
246
301
|
const radius = layer.encoding?.radius;
|
|
@@ -303,7 +358,9 @@ export function deriveArcSectors(rows, layer, {
|
|
|
303
358
|
) {
|
|
304
359
|
throw new RangeError("Arc innerRadius must be from 0 (inclusive) to 1 (exclusive).");
|
|
305
360
|
}
|
|
306
|
-
const sectors =
|
|
361
|
+
const sectors = radiusScale?.radialMapping !== undefined
|
|
362
|
+
? measuredRadialSectors(rows, layer, requireBandScale(thetaScale, `Arc mark "${layer.id}" theta`), radiusScale)
|
|
363
|
+
: theta.fieldType === "quantitative"
|
|
307
364
|
? quantitativeSectors(
|
|
308
365
|
rows,
|
|
309
366
|
layer,
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { validateKeys, validateNonEmptyString } from "../core/validation.js";
|
|
2
|
+
import { isPlainObject } from "../core/immutable.js";
|
|
3
|
+
import { readQuantitativeField } from "./scales/fields.js";
|
|
4
|
+
|
|
5
|
+
export function validateAreaMissing(value = "error") {
|
|
6
|
+
if (!["error", "break"].includes(value)) {
|
|
7
|
+
throw new Error('Area missing must be "error" or "break".');
|
|
8
|
+
}
|
|
9
|
+
return value;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function normalizeAreaBound(bound) {
|
|
13
|
+
if (typeof bound === "string") return { field: validateNonEmptyString(bound, "Area endpoint field") };
|
|
14
|
+
if (!isPlainObject(bound)) throw new TypeError("Area bound must be a field or datum object.");
|
|
15
|
+
validateKeys(bound, ["datum"], "Area bound");
|
|
16
|
+
if (!Number.isFinite(bound.datum)) throw new TypeError("Area datum must be finite.");
|
|
17
|
+
return { datum: bound.datum };
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function readAreaEndpoint(rows, encoding, missing = "error") {
|
|
21
|
+
if (Object.hasOwn(encoding, "datum")) {
|
|
22
|
+
if (Object.hasOwn(encoding, "field") || !Number.isFinite(encoding.datum) || encoding.fieldType !== "quantitative") {
|
|
23
|
+
throw new TypeError("Area endpoint requires exactly one finite quantitative datum or field.");
|
|
24
|
+
}
|
|
25
|
+
return rows.map(() => encoding.datum);
|
|
26
|
+
}
|
|
27
|
+
if (missing !== "break") return readQuantitativeField(rows, encoding.field);
|
|
28
|
+
validateNonEmptyString(encoding.field, "Area endpoint field");
|
|
29
|
+
if (rows.length > 0 && !rows.some(row => Object.hasOwn(row, encoding.field))) {
|
|
30
|
+
throw new Error(`Area endpoint field "${encoding.field}" does not exist.`);
|
|
31
|
+
}
|
|
32
|
+
return rows.map((row, index) => {
|
|
33
|
+
const value = row[encoding.field];
|
|
34
|
+
if (value == null) return null;
|
|
35
|
+
if (!Number.isFinite(value)) {
|
|
36
|
+
throw new TypeError(`Field "${encoding.field}" must contain a finite number at row ${index}.`);
|
|
37
|
+
}
|
|
38
|
+
return value;
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function validateAreaEndpointPair(primary, secondary) {
|
|
43
|
+
if (primary?.datum !== undefined && secondary?.datum !== undefined) {
|
|
44
|
+
throw new Error("Area range requires at least one field endpoint.");
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -1,15 +1,11 @@
|
|
|
1
1
|
import { cloneAndFreeze } from "../core/immutable.js";
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
readQuantitativeField,
|
|
5
|
-
readTemporalField
|
|
6
|
-
} from "./scales/index.js";
|
|
7
|
-
import {
|
|
8
|
-
layoutSeriesPartition,
|
|
9
|
-
validateColorLayout
|
|
10
|
-
} from "./seriesLayout.js";
|
|
2
|
+
import { readNominalField, readQuantitativeField, readTemporalField } from "./scales/index.js";
|
|
3
|
+
import { layoutSeriesPartition, validateColorLayout } from "./seriesLayout.js";
|
|
11
4
|
import { stableOrderPathValues } from "./pathOrder.js";
|
|
12
5
|
import { deriveCategoricalDensitySeries } from "./categoricalDensity.js";
|
|
6
|
+
import { validatePathSeriesAppearance } from "./pathSeries.js";
|
|
7
|
+
import { readAreaEndpoint, validateAreaEndpointPair } from "./areaEndpoints.js";
|
|
8
|
+
import { validateGeneratedItemLimit } from "../core/validation.js";
|
|
13
9
|
|
|
14
10
|
function indexSegments(segments) {
|
|
15
11
|
const indexed = [];
|
|
@@ -21,7 +17,7 @@ export function deriveAreaSeries(rows, layer) {
|
|
|
21
17
|
if (layer?.mark?.type !== "area") {
|
|
22
18
|
throw new Error("Area series derivation requires a semantic area mark.");
|
|
23
19
|
}
|
|
24
|
-
const { x, y, x2, y2
|
|
20
|
+
const { x, y, x2, y2 } = layer.encoding ?? {};
|
|
25
21
|
const vertical =
|
|
26
22
|
["quantitative", "temporal"].includes(x?.fieldType) &&
|
|
27
23
|
y?.fieldType === "quantitative" &&
|
|
@@ -37,26 +33,19 @@ export function deriveAreaSeries(rows, layer) {
|
|
|
37
33
|
`Area mark "${layer.id}" requires exactly one quantitative x/x2 or y/y2 range and one quantitative or temporal independent position.`
|
|
38
34
|
);
|
|
39
35
|
}
|
|
40
|
-
|
|
41
|
-
throw new Error(`Area group encoding on mark "${layer.id}" must be nominal.`);
|
|
42
|
-
}
|
|
36
|
+
const grouping = validatePathSeriesAppearance(rows, layer);
|
|
43
37
|
const orientation = vertical ? "vertical" : "horizontal";
|
|
44
38
|
const independent = vertical ? x : y;
|
|
45
39
|
const independentValues = independent.fieldType === "temporal"
|
|
46
|
-
? readTemporalField(rows, independent.field)
|
|
40
|
+
? readTemporalField(rows, independent.field, independent.temporalUnit)
|
|
47
41
|
: readQuantitativeField(rows, independent.field);
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
const
|
|
55
|
-
? rows.map(() => undefined)
|
|
56
|
-
: readNominalField(rows, group.field);
|
|
57
|
-
const colorValues = color === undefined
|
|
58
|
-
? rows.map(() => undefined)
|
|
59
|
-
: readNominalField(rows, color.field);
|
|
42
|
+
if (Object.hasOwn(independent, "datum")) throw new Error("Area independent position requires a field.");
|
|
43
|
+
const primary = vertical ? y : x;
|
|
44
|
+
const secondary = vertical ? y2 : x2;
|
|
45
|
+
validateAreaEndpointPair(primary, secondary);
|
|
46
|
+
const missing = layer.mark.missing ?? "error";
|
|
47
|
+
const lower = readAreaEndpoint(rows, primary, missing);
|
|
48
|
+
const upper = readAreaEndpoint(rows, secondary, missing);
|
|
60
49
|
const pathOrder = layer.encoding?.pathOrder;
|
|
61
50
|
const orderValues = pathOrder === undefined
|
|
62
51
|
? undefined
|
|
@@ -64,32 +53,24 @@ export function deriveAreaSeries(rows, layer) {
|
|
|
64
53
|
const groups = new Map();
|
|
65
54
|
|
|
66
55
|
for (let index = 0; index < rows.length; index += 1) {
|
|
67
|
-
const
|
|
56
|
+
const tuple = grouping.map(field => rows[index][field]);
|
|
57
|
+
const key = JSON.stringify(tuple);
|
|
68
58
|
const series = groups.get(key) ?? {
|
|
69
|
-
key:
|
|
70
|
-
...(group === undefined ? {} : { [group.field]: key }),
|
|
71
|
-
...(color === undefined ? {} : { [color.field]: colorValues[index] })
|
|
72
|
-
},
|
|
59
|
+
key: Object.fromEntries(grouping.map((field, i) => [field, tuple[i]])),
|
|
73
60
|
values: []
|
|
74
61
|
};
|
|
75
|
-
if (
|
|
76
|
-
color !== undefined &&
|
|
77
|
-
!Object.is(series.key[color.field], colorValues[index])
|
|
78
|
-
) {
|
|
79
|
-
throw new Error(
|
|
80
|
-
`Area series "${String(key)}" must have one color value.`
|
|
81
|
-
);
|
|
82
|
-
}
|
|
83
62
|
series.values.push(vertical
|
|
84
63
|
? {
|
|
85
64
|
x: independentValues[index],
|
|
86
65
|
y: lower[index],
|
|
87
66
|
y2: upper[index],
|
|
67
|
+
...(missing === "break" ? { sourceIndex: index } : {}),
|
|
88
68
|
...(orderValues === undefined ? {} : { pathOrder: orderValues[index] })
|
|
89
69
|
}
|
|
90
70
|
: {
|
|
91
71
|
x: lower[index],
|
|
92
72
|
x2: upper[index],
|
|
73
|
+
...(missing === "break" ? { sourceIndex: index } : {}),
|
|
93
74
|
y: independentValues[index],
|
|
94
75
|
...(orderValues === undefined ? {} : { pathOrder: orderValues[index] })
|
|
95
76
|
});
|
|
@@ -98,7 +79,8 @@ export function deriveAreaSeries(rows, layer) {
|
|
|
98
79
|
if (groups.size === 0) {
|
|
99
80
|
throw new Error(`Area mark "${layer.id}" has no values.`);
|
|
100
81
|
}
|
|
101
|
-
|
|
82
|
+
layoutRawAreaGroups(groups, { layer, vertical, primary, secondary });
|
|
83
|
+
const series = [...groups.values()].flatMap(item => {
|
|
102
84
|
const key = vertical ? "x" : "y";
|
|
103
85
|
const values = pathOrder === undefined
|
|
104
86
|
? item.values.sort((left, right) => left[key] - right[key])
|
|
@@ -107,6 +89,22 @@ export function deriveAreaSeries(rows, layer) {
|
|
|
107
89
|
item.values.map(value => value.pathOrder),
|
|
108
90
|
pathOrder.order
|
|
109
91
|
);
|
|
92
|
+
if (missing === "break") {
|
|
93
|
+
const segments = [];
|
|
94
|
+
let segment = [];
|
|
95
|
+
const emit = () => {
|
|
96
|
+
if (segment.length >= 2) segments.push({ key: item.key,
|
|
97
|
+
sourceIndices: segment.map(value => value.sourceIndex),
|
|
98
|
+
values: segment.map(({ sourceIndex, ...value }) => value) });
|
|
99
|
+
segment = [];
|
|
100
|
+
};
|
|
101
|
+
for (const value of values) {
|
|
102
|
+
if ((vertical ? value.y == null || value.y2 == null : value.x == null || value.x2 == null)) emit();
|
|
103
|
+
else segment.push(value);
|
|
104
|
+
}
|
|
105
|
+
emit();
|
|
106
|
+
return segments;
|
|
107
|
+
}
|
|
110
108
|
if (values.length < 2) {
|
|
111
109
|
throw new Error(
|
|
112
110
|
`Area series on mark "${layer.id}" requires at least two points.`
|
|
@@ -114,6 +112,7 @@ export function deriveAreaSeries(rows, layer) {
|
|
|
114
112
|
}
|
|
115
113
|
return { key: item.key, values };
|
|
116
114
|
});
|
|
115
|
+
if (series.length === 0) throw new Error(`Area mark "${layer.id}" has no valid segment with at least two points.`);
|
|
117
116
|
return cloneAndFreeze({
|
|
118
117
|
orientation,
|
|
119
118
|
xValues: series.flatMap(item => item.values.flatMap(value =>
|
|
@@ -151,7 +150,7 @@ export function deriveCenteredAreaSeries(rows, layer) {
|
|
|
151
150
|
);
|
|
152
151
|
}
|
|
153
152
|
const xValues = x.fieldType === "temporal"
|
|
154
|
-
? readTemporalField(rows, x.field)
|
|
153
|
+
? readTemporalField(rows, x.field, x.temporalUnit)
|
|
155
154
|
: readQuantitativeField(rows, x.field);
|
|
156
155
|
const yValues = readQuantitativeField(rows, y.field);
|
|
157
156
|
const groups = readNominalField(rows, group.field);
|
|
@@ -359,3 +358,50 @@ export function layoutDensityAreaSeries(derived, layout = "overlay") {
|
|
|
359
358
|
}))
|
|
360
359
|
});
|
|
361
360
|
}
|
|
361
|
+
|
|
362
|
+
export function layoutRawAreaGroups(groups, { layer, vertical, primary, secondary }) {
|
|
363
|
+
const mode = layer.layout?.mode ?? "overlay";
|
|
364
|
+
validateColorLayout(mode);
|
|
365
|
+
if (mode === "overlay") return;
|
|
366
|
+
if (mode === "group" || (mode === "center" && !vertical)) {
|
|
367
|
+
throw new Error(`Area layout "${mode}" is incompatible with this orientation.`);
|
|
368
|
+
}
|
|
369
|
+
const primaryDatum = Object.hasOwn(primary, "datum");
|
|
370
|
+
const secondaryDatum = Object.hasOwn(secondary, "datum");
|
|
371
|
+
if (primaryDatum === secondaryDatum || (primaryDatum ? primary.datum : secondary.datum) !== 0) {
|
|
372
|
+
throw new Error("Stacked area requires one value field and one zero datum endpoint.");
|
|
373
|
+
}
|
|
374
|
+
const position = vertical ? "x" : "y";
|
|
375
|
+
const lower = vertical ? "y" : "x", upper = `${lower}2`;
|
|
376
|
+
const measure = primaryDatum ? upper : lower;
|
|
377
|
+
const series = [...groups.values()];
|
|
378
|
+
const indices = series.map(item => {
|
|
379
|
+
const index = new Map();
|
|
380
|
+
for (const value of item.values) {
|
|
381
|
+
if (index.has(value[position])) throw new Error("Area layout requires unique group/position rows.");
|
|
382
|
+
index.set(value[position], value);
|
|
383
|
+
}
|
|
384
|
+
return index;
|
|
385
|
+
});
|
|
386
|
+
const positions = [...new Set(series.flatMap(item => item.values.map(value => value[position])))].sort((a, b) => a - b);
|
|
387
|
+
validateGeneratedItemLimit(positions.length * series.length, "Area layout cell count");
|
|
388
|
+
if (indices.some(index => index.size !== positions.length || positions.some(value => !index.has(value)))) {
|
|
389
|
+
throw new Error("Area layout requires an aligned row for every group and position.");
|
|
390
|
+
}
|
|
391
|
+
for (const position of positions) {
|
|
392
|
+
const points = indices.map(index => index.get(position));
|
|
393
|
+
if (points.some(point => point[measure] == null)) {
|
|
394
|
+
for (const point of points) { point[lower] = null; point[upper] = null; }
|
|
395
|
+
continue;
|
|
396
|
+
}
|
|
397
|
+
const segments = layoutSeriesPartition(points.map(point => point[measure]), mode);
|
|
398
|
+
const byIndex = new Map(segments.map(segment => [segment.index, segment]));
|
|
399
|
+
let endpoint = mode === "center" ? segments[0]?.start ?? 0 : 0;
|
|
400
|
+
points.forEach((point, index) => {
|
|
401
|
+
const segment = byIndex.get(index);
|
|
402
|
+
point[lower] = segment?.start ?? (mode === "diverging" ? 0 : endpoint);
|
|
403
|
+
point[upper] = segment?.end ?? (mode === "diverging" ? 0 : endpoint);
|
|
404
|
+
if (segment !== undefined && mode !== "diverging") endpoint = segment.end;
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
}
|