ggaction 0.0.11 → 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 +80 -0
- package/README.md +3 -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 +33 -27
- package/src/actions/encodings/parallel.js +2 -8
- package/src/actions/encodings/position/apply.js +40 -4
- package/src/actions/encodings/position/index.js +43 -5
- package/src/actions/encodings/position/policies/arc.js +37 -2
- 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 +79 -24
- 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 +153 -11
- package/src/actions/errorBars/edit.js +365 -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 +138 -14
- 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 +60 -4
- 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/core/vocabulary.js +2 -2
- package/src/grammar/aggregate.js +66 -23
- package/src/grammar/arcs.js +213 -65
- 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/positionCompatibility.js +1 -1
- 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 +9 -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/ordinal.js +23 -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 +86 -23
- 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/facetGuides/legacyCategorical.js +7 -1
- package/src/materialization/facetGuides/placement.js +16 -1
- package/src/materialization/facetGuides/preparation.js +26 -1
- package/src/materialization/facets.js +11 -2
- 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 +30 -7
- package/src/materialization/marks/grouping.js +52 -0
- package/src/materialization/marks/index.js +63 -25
- package/src/materialization/marks/policies.js +2 -0
- package/src/materialization/rect.js +33 -33
- package/src/materialization/rowEncoding.js +70 -14
- 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/bar.js +0 -43
- package/src/materialization/scales/policies/binnedPosition.js +7 -4
- package/src/materialization/scales/policies/offset.js +54 -0
- package/src/materialization/scales/policies/series.js +2 -1
- package/src/materialization/scales/resolve.js +48 -18
- 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 +1327 -124
- package/src/materialization/bars/grouped.js +0 -144
package/src/grammar/arcs.js
CHANGED
|
@@ -17,12 +17,30 @@ function requireArcLayer(layer) {
|
|
|
17
17
|
throw new Error("Arc derivation requires a semantic arc mark.");
|
|
18
18
|
}
|
|
19
19
|
const theta = layer.encoding?.theta;
|
|
20
|
-
if (
|
|
21
|
-
|
|
20
|
+
if (
|
|
21
|
+
theta === undefined ||
|
|
22
|
+
!["quantitative", "nominal", "ordinal"].includes(theta.fieldType)
|
|
23
|
+
) {
|
|
24
|
+
throw new Error(
|
|
25
|
+
`Arc mark "${layer.id}" requires quantitative or categorical theta.`
|
|
26
|
+
);
|
|
22
27
|
}
|
|
23
28
|
return theta;
|
|
24
29
|
}
|
|
25
30
|
|
|
31
|
+
function requireContinuousThetaScale(scale, label) {
|
|
32
|
+
if (
|
|
33
|
+
scale?.type !== "linear" ||
|
|
34
|
+
!Array.isArray(scale.range) ||
|
|
35
|
+
scale.range.length !== 2 ||
|
|
36
|
+
!scale.range.every(Number.isFinite) ||
|
|
37
|
+
scale.range[0] === scale.range[1]
|
|
38
|
+
) {
|
|
39
|
+
throw new Error(`${label} requires a resolved linear scale.`);
|
|
40
|
+
}
|
|
41
|
+
return scale;
|
|
42
|
+
}
|
|
43
|
+
|
|
26
44
|
function requireBandScale(scale, label) {
|
|
27
45
|
if (
|
|
28
46
|
scale?.type !== "band" ||
|
|
@@ -45,24 +63,90 @@ function colorValues(rows, encoding) {
|
|
|
45
63
|
: readNominalField(rows, encoding.field);
|
|
46
64
|
}
|
|
47
65
|
|
|
48
|
-
|
|
49
|
-
if (!Array.isArray(rows)) throw new TypeError(
|
|
66
|
+
function readNonNegativeArcThetaValues(rows, field, layerId, role) {
|
|
67
|
+
if (!Array.isArray(rows)) throw new TypeError(`Arc theta ${role}s require rows.`);
|
|
50
68
|
if (typeof field !== "string" || field.length === 0) {
|
|
51
|
-
throw new TypeError(
|
|
69
|
+
throw new TypeError(`Arc theta ${role} must be a non-empty string.`);
|
|
52
70
|
}
|
|
53
|
-
const
|
|
71
|
+
const values = rows.map((row, index) => {
|
|
54
72
|
const value = row?.[field];
|
|
55
73
|
if (!Number.isFinite(value) || value < 0) {
|
|
56
74
|
throw new TypeError(
|
|
57
|
-
`Arc theta
|
|
75
|
+
`Arc theta ${role} field "${field}" must contain non-negative finite numbers at row ${index}.`
|
|
58
76
|
);
|
|
59
77
|
}
|
|
60
78
|
return value;
|
|
61
79
|
});
|
|
62
|
-
if (
|
|
63
|
-
throw new Error(`Arc mark "${layerId}" theta
|
|
80
|
+
if (values.every(value => value === 0)) {
|
|
81
|
+
throw new Error(`Arc mark "${layerId}" theta ${role}s must have a positive total.`);
|
|
64
82
|
}
|
|
65
|
-
return cloneAndFreeze(
|
|
83
|
+
return cloneAndFreeze(values);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function readArcThetaWeights(rows, field, layerId = "arc") {
|
|
87
|
+
return readNonNegativeArcThetaValues(rows, field, layerId, "weight");
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function readArcThetaValues(rows, field, layerId = "arc") {
|
|
91
|
+
return readNonNegativeArcThetaValues(rows, field, layerId, "value");
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function resolveProportionalRanges(values, range, labelAt) {
|
|
95
|
+
const total = values.reduce((sum, value) => sum + value, 0);
|
|
96
|
+
const normalizedTotal = Number.isFinite(total)
|
|
97
|
+
? undefined
|
|
98
|
+
: normalizedFiniteSum(values, "Arc theta values");
|
|
99
|
+
if (total === 0 || normalizedTotal?.total <= 0) {
|
|
100
|
+
throw new Error("Arc theta values must have a positive total.");
|
|
101
|
+
}
|
|
102
|
+
const span = range[1] - range[0];
|
|
103
|
+
let cursor = range[0];
|
|
104
|
+
let cumulative = 0;
|
|
105
|
+
let cumulativeCorrection = 0;
|
|
106
|
+
return values.map((value, index) => {
|
|
107
|
+
const normalizedValue = normalizedTotal === undefined
|
|
108
|
+
? value
|
|
109
|
+
: value / normalizedTotal.scale;
|
|
110
|
+
const next = cumulative + normalizedValue;
|
|
111
|
+
if (normalizedTotal !== undefined) {
|
|
112
|
+
cumulativeCorrection += Math.abs(cumulative) >= Math.abs(normalizedValue)
|
|
113
|
+
? cumulative - next + normalizedValue
|
|
114
|
+
: normalizedValue - next + cumulative;
|
|
115
|
+
}
|
|
116
|
+
cumulative = next;
|
|
117
|
+
const directEnd = cursor + value / total * span;
|
|
118
|
+
const endTheta = index === values.length - 1
|
|
119
|
+
? range[1]
|
|
120
|
+
: normalizedTotal === undefined && Number.isFinite(directEnd)
|
|
121
|
+
? directEnd
|
|
122
|
+
: interpolateNumber(
|
|
123
|
+
range[0],
|
|
124
|
+
range[1],
|
|
125
|
+
(cumulative + cumulativeCorrection) /
|
|
126
|
+
(normalizedTotal?.total ?? total)
|
|
127
|
+
);
|
|
128
|
+
if (!Number.isFinite(endTheta) || endTheta === cursor) {
|
|
129
|
+
throw new RangeError(
|
|
130
|
+
`Arc theta ${labelAt(index)} cannot resolve a distinct finite sector.`
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
const result = { startTheta: cursor, endTheta };
|
|
134
|
+
cursor = endTheta;
|
|
135
|
+
return result;
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function resolveProportionalRadii(layer, frame, innerRadiusRatio) {
|
|
140
|
+
if (!Number.isFinite(frame?.availableRadius) || frame.availableRadius < 0) {
|
|
141
|
+
throw new RangeError(`Arc mark "${layer.id}" requires a finite available radius.`);
|
|
142
|
+
}
|
|
143
|
+
return {
|
|
144
|
+
innerRadius: requireFiniteResult(
|
|
145
|
+
frame.availableRadius * innerRadiusRatio,
|
|
146
|
+
`Arc mark "${layer.id}" inner radius`
|
|
147
|
+
),
|
|
148
|
+
outerRadius: frame.availableRadius
|
|
149
|
+
};
|
|
66
150
|
}
|
|
67
151
|
|
|
68
152
|
function proportionalSectors(rows, layer, thetaScale, frame, innerRadiusRatio) {
|
|
@@ -87,14 +171,7 @@ function proportionalSectors(rows, layer, thetaScale, frame, innerRadiusRatio) {
|
|
|
87
171
|
weight: weights[index]
|
|
88
172
|
});
|
|
89
173
|
}
|
|
90
|
-
|
|
91
|
-
throw new RangeError(`Arc mark "${layer.id}" requires a finite available radius.`);
|
|
92
|
-
}
|
|
93
|
-
const innerRadius = requireFiniteResult(
|
|
94
|
-
frame.availableRadius * innerRadiusRatio,
|
|
95
|
-
`Arc mark "${layer.id}" inner radius`
|
|
96
|
-
);
|
|
97
|
-
let cursor = thetaScale.range[0];
|
|
174
|
+
const radii = resolveProportionalRadii(layer, frame, innerRadiusRatio);
|
|
98
175
|
const sectors = [];
|
|
99
176
|
const positiveValues = thetaScale.domain.filter(value =>
|
|
100
177
|
groups.get(value).some(item => item.weight > 0)
|
|
@@ -103,48 +180,15 @@ function proportionalSectors(rows, layer, thetaScale, frame, innerRadiusRatio) {
|
|
|
103
180
|
groups.get(value).map(item => item.weight),
|
|
104
181
|
`Arc theta group "${String(value)}" aggregate`
|
|
105
182
|
));
|
|
106
|
-
const
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
`Arc mark "${layer.id}" theta aggregates`
|
|
112
|
-
);
|
|
113
|
-
if (total === 0 || normalizedTotal?.total <= 0) {
|
|
114
|
-
throw new Error(`Arc mark "${layer.id}" has no positive theta weight.`);
|
|
115
|
-
}
|
|
116
|
-
const span = thetaScale.range[1] - thetaScale.range[0];
|
|
117
|
-
let cumulative = 0;
|
|
118
|
-
let cumulativeCorrection = 0;
|
|
183
|
+
const ranges = resolveProportionalRanges(
|
|
184
|
+
aggregateValues,
|
|
185
|
+
thetaScale.range,
|
|
186
|
+
index => `group "${String(positiveValues[index])}"`
|
|
187
|
+
);
|
|
119
188
|
for (const [positiveIndex, value] of positiveValues.entries()) {
|
|
120
189
|
const group = groups.get(value);
|
|
121
190
|
const aggregateValue = aggregateValues[positiveIndex];
|
|
122
|
-
const
|
|
123
|
-
? aggregateValue
|
|
124
|
-
: aggregateValue / normalizedTotal.scale;
|
|
125
|
-
const next = cumulative + normalizedValue;
|
|
126
|
-
if (normalizedTotal !== undefined) {
|
|
127
|
-
cumulativeCorrection += Math.abs(cumulative) >= Math.abs(normalizedValue)
|
|
128
|
-
? cumulative - next + normalizedValue
|
|
129
|
-
: normalizedValue - next + cumulative;
|
|
130
|
-
}
|
|
131
|
-
cumulative = next;
|
|
132
|
-
const directEnd = cursor + aggregateValue / total * span;
|
|
133
|
-
const endTheta = positiveIndex === positiveValues.length - 1
|
|
134
|
-
? thetaScale.range[1]
|
|
135
|
-
: normalizedTotal === undefined && Number.isFinite(directEnd)
|
|
136
|
-
? directEnd
|
|
137
|
-
: interpolateNumber(
|
|
138
|
-
thetaScale.range[0],
|
|
139
|
-
thetaScale.range[1],
|
|
140
|
-
(cumulative + cumulativeCorrection) /
|
|
141
|
-
(normalizedTotal?.total ?? total)
|
|
142
|
-
);
|
|
143
|
-
if (!Number.isFinite(endTheta) || endTheta === cursor) {
|
|
144
|
-
throw new RangeError(
|
|
145
|
-
`Arc theta group "${String(value)}" cannot resolve a distinct finite sector.`
|
|
146
|
-
);
|
|
147
|
-
}
|
|
191
|
+
const { startTheta, endTheta } = ranges[positiveIndex];
|
|
148
192
|
const distinctColors = [...new Set(group.map(item => item.color))];
|
|
149
193
|
if (distinctColors.length > 1) {
|
|
150
194
|
throw new Error(
|
|
@@ -157,17 +201,101 @@ function proportionalSectors(rows, layer, thetaScale, frame, innerRadiusRatio) {
|
|
|
157
201
|
count: group.length,
|
|
158
202
|
aggregateValue,
|
|
159
203
|
color: distinctColors[0],
|
|
160
|
-
startTheta
|
|
204
|
+
startTheta,
|
|
161
205
|
endTheta,
|
|
162
|
-
|
|
163
|
-
outerRadius: frame.availableRadius,
|
|
206
|
+
...radii,
|
|
164
207
|
sourceIndices: group.map(item => item.sourceIndex)
|
|
165
208
|
});
|
|
166
|
-
cursor = endTheta;
|
|
167
209
|
}
|
|
168
210
|
return sectors;
|
|
169
211
|
}
|
|
170
212
|
|
|
213
|
+
function quantitativeSectors(rows, layer, thetaScale, frame, innerRadiusRatio) {
|
|
214
|
+
const theta = layer.encoding.theta;
|
|
215
|
+
if (layer.encoding?.radius !== undefined) {
|
|
216
|
+
throw new Error(
|
|
217
|
+
`Arc mark "${layer.id}" cannot combine quantitative theta with radius.`
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
const values = readArcThetaValues(rows, theta.field, layer.id);
|
|
221
|
+
const colors = colorValues(rows, layer.encoding?.color);
|
|
222
|
+
const positiveIndices = values
|
|
223
|
+
.map((value, index) => value > 0 ? index : undefined)
|
|
224
|
+
.filter(index => index !== undefined);
|
|
225
|
+
const positiveValues = positiveIndices.map(index => values[index]);
|
|
226
|
+
const ranges = resolveProportionalRanges(
|
|
227
|
+
positiveValues,
|
|
228
|
+
thetaScale.range,
|
|
229
|
+
index => `row ${positiveIndices[index]}`
|
|
230
|
+
);
|
|
231
|
+
const radii = resolveProportionalRadii(layer, frame, innerRadiusRatio);
|
|
232
|
+
return positiveIndices.map((sourceIndex, index) => ({
|
|
233
|
+
key: `${String(values[sourceIndex])}:${sourceIndex}`,
|
|
234
|
+
theta: values[sourceIndex],
|
|
235
|
+
count: 1,
|
|
236
|
+
aggregateValue: values[sourceIndex],
|
|
237
|
+
color: colors[sourceIndex],
|
|
238
|
+
...ranges[index],
|
|
239
|
+
...radii,
|
|
240
|
+
sourceIndices: [sourceIndex]
|
|
241
|
+
}));
|
|
242
|
+
}
|
|
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
|
+
|
|
171
299
|
function radialSectors(rows, layer, thetaScale, radiusScale) {
|
|
172
300
|
const theta = layer.encoding.theta;
|
|
173
301
|
const radius = layer.encoding?.radius;
|
|
@@ -223,7 +351,6 @@ export function deriveArcSectors(rows, layer, {
|
|
|
223
351
|
} = {}) {
|
|
224
352
|
if (!Array.isArray(rows)) throw new TypeError("Arc derivation requires rows.");
|
|
225
353
|
const theta = requireArcLayer(layer);
|
|
226
|
-
requireBandScale(thetaScale, `Arc mark "${layer.id}" theta`);
|
|
227
354
|
if (
|
|
228
355
|
!Number.isFinite(innerRadiusRatio) ||
|
|
229
356
|
innerRadiusRatio < 0 ||
|
|
@@ -231,8 +358,29 @@ export function deriveArcSectors(rows, layer, {
|
|
|
231
358
|
) {
|
|
232
359
|
throw new RangeError("Arc innerRadius must be from 0 (inclusive) to 1 (exclusive).");
|
|
233
360
|
}
|
|
234
|
-
const sectors =
|
|
235
|
-
?
|
|
236
|
-
:
|
|
361
|
+
const sectors = radiusScale?.radialMapping !== undefined
|
|
362
|
+
? measuredRadialSectors(rows, layer, requireBandScale(thetaScale, `Arc mark "${layer.id}" theta`), radiusScale)
|
|
363
|
+
: theta.fieldType === "quantitative"
|
|
364
|
+
? quantitativeSectors(
|
|
365
|
+
rows,
|
|
366
|
+
layer,
|
|
367
|
+
requireContinuousThetaScale(thetaScale, `Arc mark "${layer.id}" theta`),
|
|
368
|
+
frame,
|
|
369
|
+
innerRadiusRatio
|
|
370
|
+
)
|
|
371
|
+
: ["count", "sum"].includes(theta.aggregate)
|
|
372
|
+
? proportionalSectors(
|
|
373
|
+
rows,
|
|
374
|
+
layer,
|
|
375
|
+
requireBandScale(thetaScale, `Arc mark "${layer.id}" theta`),
|
|
376
|
+
frame,
|
|
377
|
+
innerRadiusRatio
|
|
378
|
+
)
|
|
379
|
+
: radialSectors(
|
|
380
|
+
rows,
|
|
381
|
+
layer,
|
|
382
|
+
requireBandScale(thetaScale, `Arc mark "${layer.id}" theta`),
|
|
383
|
+
radiusScale
|
|
384
|
+
);
|
|
237
385
|
return cloneAndFreeze({ sectors });
|
|
238
386
|
}
|
|
@@ -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
|
+
}
|
|
@@ -1,15 +1,9 @@
|
|
|
1
|
+
import { readSeriesIdentity } from "../pathSeries.js";
|
|
1
2
|
import { cloneAndFreeze } from "../../core/immutable.js";
|
|
2
3
|
import { readNominalField, readTemporalField } from "../scales/index.js";
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
validateAggregateFieldValues,
|
|
7
|
-
} from "../aggregate.js";
|
|
8
|
-
import {
|
|
9
|
-
BAR_GRAINS,
|
|
10
|
-
resolveBarChannels,
|
|
11
|
-
resolveBarGrain
|
|
12
|
-
} from "./policy.js";
|
|
4
|
+
import { aggregateRows, validateAggregateFieldType, validateAggregateFieldValues } from "../aggregate.js";
|
|
5
|
+
import { BAR_GRAINS, resolveBarChannels, resolveBarGrain, resolveBarColorLayout } from "./policy.js";
|
|
6
|
+
import { layoutSeriesPartition } from "../seriesLayout.js";
|
|
13
7
|
|
|
14
8
|
function requireAggregateBarEncoding(layer) {
|
|
15
9
|
if (layer?.mark?.type !== "bar") {
|
|
@@ -33,7 +27,7 @@ export function deriveBarAggregates(rows, layer) {
|
|
|
33
27
|
const { channels, category, measure } = requireAggregateBarEncoding(layer);
|
|
34
28
|
validateAggregateFieldValues(rows, measure.field, measure.fieldType);
|
|
35
29
|
const categoryValues = category.fieldType === "temporal"
|
|
36
|
-
? readTemporalField(rows, category.field)
|
|
30
|
+
? readTemporalField(rows, category.field, category.temporalUnit)
|
|
37
31
|
: readNominalField(rows, category.field);
|
|
38
32
|
const color = layer.encoding?.color;
|
|
39
33
|
let colorValues;
|
|
@@ -52,17 +46,23 @@ export function deriveBarAggregates(rows, layer) {
|
|
|
52
46
|
);
|
|
53
47
|
}
|
|
54
48
|
}
|
|
49
|
+
const identity = layer.encoding?.group === undefined ? undefined : readSeriesIdentity(rows, layer);
|
|
55
50
|
const groups = new Map();
|
|
56
51
|
|
|
57
52
|
for (let index = 0; index < rows.length; index += 1) {
|
|
58
53
|
const categoryValue = categoryValues[index];
|
|
59
54
|
const colorValue = colorValues?.[index];
|
|
60
|
-
const
|
|
55
|
+
const seriesValue = identity?.values[index];
|
|
56
|
+
const key = JSON.stringify([categoryValue, identity === undefined ? colorValue : seriesValue]);
|
|
61
57
|
const group = groups.get(key) ?? {
|
|
62
58
|
category: categoryValue,
|
|
59
|
+
...(identity === undefined ? {} : { series: seriesValue }),
|
|
63
60
|
...(colorValues === undefined ? {} : { color: colorValue }),
|
|
64
61
|
rows: []
|
|
65
62
|
};
|
|
63
|
+
if (identity !== undefined && colorValues !== undefined && group.color !== colorValue) {
|
|
64
|
+
throw new Error("Bar color requires one categorical value within each aggregate cell.");
|
|
65
|
+
}
|
|
66
66
|
group.rows.push(rows[index]);
|
|
67
67
|
groups.set(key, group);
|
|
68
68
|
}
|
|
@@ -82,6 +82,7 @@ export function deriveBarAggregates(rows, layer) {
|
|
|
82
82
|
x: channels.category === "x" ? group.category : value,
|
|
83
83
|
y: channels.category === "y" ? group.category : value,
|
|
84
84
|
...(color === undefined ? {} : { color: aggregateColor }),
|
|
85
|
+
...(identity === undefined ? {} : { series: group.series }),
|
|
85
86
|
count: group.rows.length
|
|
86
87
|
}];
|
|
87
88
|
});
|
|
@@ -96,3 +97,23 @@ export function deriveBarAggregates(rows, layer) {
|
|
|
96
97
|
values
|
|
97
98
|
});
|
|
98
99
|
}
|
|
100
|
+
|
|
101
|
+
export function deriveSeriesBarCells(rows, layer, categoryDomain, seriesDomain) {
|
|
102
|
+
const channels = resolveBarChannels(layer);
|
|
103
|
+
const values = deriveBarAggregates(rows, layer).values;
|
|
104
|
+
const identity = readSeriesIdentity(rows, layer);
|
|
105
|
+
const categories = categoryDomain ?? [...new Set(values.map(cell => cell[channels.category]))].sort((a, b) => a - b);
|
|
106
|
+
const seriesKey = cell => layer.encoding.group !== undefined ? cell.series : layer.encoding.color?.fieldType === "quantitative" ? undefined : cell.color;
|
|
107
|
+
const series = seriesDomain ?? (layer.encoding.group !== undefined ? identity.domain : [...new Set(values.map(seriesKey))]);
|
|
108
|
+
const cells = new Map(values.map(cell => [JSON.stringify([cell[channels.category], seriesKey(cell)]), cell]));
|
|
109
|
+
const layout = resolveBarColorLayout(layer);
|
|
110
|
+
return categories.flatMap(category => {
|
|
111
|
+
const partition = series.map(key => cells.get(JSON.stringify([category, key])));
|
|
112
|
+
const segments = layout === "group"
|
|
113
|
+
? partition.flatMap((cell, index) => cell === undefined ? [] : [{ index, start: 0, end: cell[channels.measure] }])
|
|
114
|
+
: layoutSeriesPartition(partition.map(cell => cell?.[channels.measure] ?? 0), layout);
|
|
115
|
+
return segments.flatMap(segment => partition[segment.index] === undefined ? [] : [{
|
|
116
|
+
cell: partition[segment.index], start: segment.start, end: segment.end, seriesIndex: segment.index
|
|
117
|
+
}]);
|
|
118
|
+
});
|
|
119
|
+
}
|