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
|
@@ -149,6 +149,27 @@ function compatibleLegendChildren(program, source, inferred) {
|
|
|
149
149
|
});
|
|
150
150
|
}
|
|
151
151
|
|
|
152
|
+
function promotedLegendPlacement(source, kinds) {
|
|
153
|
+
const positioned = kinds.map(kind => source.guideConfigs.legend[kind])
|
|
154
|
+
.filter(config => config.position !== undefined);
|
|
155
|
+
const positions = new Set(positioned.map(config => config.position));
|
|
156
|
+
if (positions.size > 1) {
|
|
157
|
+
throw new Error(
|
|
158
|
+
"Shared facet legends must use one common edge position."
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
const aligns = new Set(positioned.map(config => config.align ?? "center"));
|
|
162
|
+
if (aligns.size > 1) {
|
|
163
|
+
throw new Error(
|
|
164
|
+
"Shared facet legends must use one common alignment."
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
return {
|
|
168
|
+
position: positioned[0]?.position ?? "right",
|
|
169
|
+
align: positioned[0]?.align ?? "center"
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
152
173
|
export function prepareSharedFacetLegend(program) {
|
|
153
174
|
if (program.compositionSpec.facet.guides.legend !== "shared") {
|
|
154
175
|
return undefined;
|
|
@@ -176,6 +197,8 @@ export function prepareSharedFacetLegend(program) {
|
|
|
176
197
|
throw new Error("Shared facet legend has no measurable concrete bounds.");
|
|
177
198
|
}
|
|
178
199
|
const gradient = kinds.includes("gradient");
|
|
200
|
+
const placement = promotedLegendPlacement(source, kinds);
|
|
201
|
+
const width = Math.ceil(bounds.right - bounds.left);
|
|
179
202
|
return {
|
|
180
203
|
mode: "promoted",
|
|
181
204
|
source,
|
|
@@ -185,8 +208,10 @@ export function prepareSharedFacetLegend(program) {
|
|
|
185
208
|
bounds,
|
|
186
209
|
compatibility,
|
|
187
210
|
reservation: {
|
|
211
|
+
...placement,
|
|
188
212
|
gap: gradient ? 24 : 18,
|
|
189
|
-
width: gradient ? 112 :
|
|
213
|
+
width: Math.max(gradient ? 112 : 132, width),
|
|
214
|
+
height: Math.ceil(bounds.bottom - bounds.top)
|
|
190
215
|
}
|
|
191
216
|
};
|
|
192
217
|
}
|
|
@@ -162,10 +162,17 @@ export function resolveFacetProgramLayout(program, preparedLegend) {
|
|
|
162
162
|
}
|
|
163
163
|
const title = titleLayout(program);
|
|
164
164
|
const spec = program.compositionSpec;
|
|
165
|
+
const gridCells = new Map(
|
|
166
|
+
(spec.facet.grid?.cells ?? []).map(cell => [cell.id, cell])
|
|
167
|
+
);
|
|
165
168
|
const layout = resolveFacetLayout({
|
|
166
169
|
children: spec.children.map((id, index) => ({
|
|
167
170
|
...compositionChildDescriptor(id, program.children[id]),
|
|
168
|
-
value: spec.facet.values[index]
|
|
171
|
+
value: spec.facet.values[index],
|
|
172
|
+
...(gridCells.has(id) ? {
|
|
173
|
+
row: gridCells.get(id).row,
|
|
174
|
+
column: gridCells.get(id).column
|
|
175
|
+
} : {})
|
|
169
176
|
})),
|
|
170
177
|
columns: spec.columns,
|
|
171
178
|
gap: spec.gap,
|
|
@@ -175,7 +182,9 @@ export function resolveFacetProgramLayout(program, preparedLegend) {
|
|
|
175
182
|
sharedLegend: spec.facet.guides.legend === "shared",
|
|
176
183
|
...(preparedLegend === undefined ? {} : {
|
|
177
184
|
sharedLegendGap: preparedLegend.reservation.gap,
|
|
178
|
-
sharedLegendWidth: preparedLegend.reservation.width
|
|
185
|
+
sharedLegendWidth: preparedLegend.reservation.width,
|
|
186
|
+
sharedLegendHeight: preparedLegend.reservation.height,
|
|
187
|
+
sharedLegendPosition: preparedLegend.reservation.position
|
|
179
188
|
})
|
|
180
189
|
});
|
|
181
190
|
const plots = spec.children.map(id => ({
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { assertGuideCollisionBlocks } from "../../layout/guideCollisions.js";
|
|
2
|
+
import { unionConcreteGraphicBounds } from "../../grammar/schemas/graphicBounds.js";
|
|
3
|
+
import { axisGraphicIds, legendResourcePolicies } from "./resources.js";
|
|
4
|
+
|
|
5
|
+
// Only domain-owned guides participate. Extension primitives may deliberately
|
|
6
|
+
// overlap, and a combined categorical/size legend is one occupied group.
|
|
7
|
+
export function resolveGuideCollisionBlocks(graphicSpec, guideConfigs, titleConfig) {
|
|
8
|
+
const blocks = [];
|
|
9
|
+
const append = (id, kind, position, ids) => {
|
|
10
|
+
if (position === undefined) return;
|
|
11
|
+
const existing = ids.filter(id => graphicSpec.objects[id] !== undefined);
|
|
12
|
+
const bounds = unionConcreteGraphicBounds(graphicSpec, existing);
|
|
13
|
+
if (bounds !== undefined) blocks.push({ id, kind, position, bounds });
|
|
14
|
+
};
|
|
15
|
+
append("chart title", "title", titleConfig?.position, ["chartTitle", "chartSubtitle"]);
|
|
16
|
+
for (const channel of ["x", "y"]) {
|
|
17
|
+
for (const id of axisGraphicIds(channel)) {
|
|
18
|
+
const component = id.slice(`${channel}Axis`.length).toLowerCase();
|
|
19
|
+
append(`${channel}-axis ${component}`, "axis", guideConfigs.axis?.[channel]?.[component]?.position, [id]);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
const policies = legendResourcePolicies();
|
|
23
|
+
const configs = guideConfigs.legend ?? {};
|
|
24
|
+
const categorical = policies.find(policy => policy.family === "categorical" && configs[policy.kind] !== undefined);
|
|
25
|
+
const combined = categorical !== undefined && configs.size !== undefined &&
|
|
26
|
+
configs[categorical.kind].target === configs.size.target;
|
|
27
|
+
for (const policy of policies) {
|
|
28
|
+
const config = configs[policy.kind];
|
|
29
|
+
if (config === undefined || combined && policy.kind === "size") continue;
|
|
30
|
+
const ids = combined && policy === categorical
|
|
31
|
+
? [...policy.graphicIds, ...policies.find(item => item.kind === "size").graphicIds]
|
|
32
|
+
: policy.graphicIds;
|
|
33
|
+
append(`${policy.kind}${combined && policy === categorical ? "+size" : ""} legend`, "legend", config.position, ids);
|
|
34
|
+
}
|
|
35
|
+
return blocks;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
// Defer cross-guide checks while sibling materializers still carry old
|
|
40
|
+
// coordinates. The scope is transient and never survives a returned program.
|
|
41
|
+
export function withGuideLayoutTransaction(program, apply) {
|
|
42
|
+
if (program.context.deferGuideLayoutValidation === true) return apply(program);
|
|
43
|
+
const result = apply(program._withContext({ deferGuideLayoutValidation: true }));
|
|
44
|
+
const { deferGuideLayoutValidation: _deferred, ...context } = result.context;
|
|
45
|
+
if (Object.hasOwn(program.context, "deferGuideLayoutValidation")) {
|
|
46
|
+
context.deferGuideLayoutValidation = program.context.deferGuideLayoutValidation;
|
|
47
|
+
}
|
|
48
|
+
const next = result._clone({ context });
|
|
49
|
+
assertGuideCollisionBlocks(resolveGuideCollisionBlocks(next.graphicSpec, next.guideConfigs, next.titleConfig));
|
|
50
|
+
return next;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function withGuideLayoutValidation(implementation) {
|
|
54
|
+
return function (args) {
|
|
55
|
+
return withGuideLayoutTransaction(this, program => implementation.call(program, args));
|
|
56
|
+
};
|
|
57
|
+
}
|
|
@@ -27,7 +27,7 @@ const LEGEND_RESOURCE_POLICIES = Object.freeze({
|
|
|
27
27
|
family: "size",
|
|
28
28
|
rematerializeOp: "rematerializeSizeLegend",
|
|
29
29
|
graphicIds: Object.freeze([
|
|
30
|
-
"sizeLegendSymbols", "sizeLegendLabels", "sizeLegendTitle"
|
|
30
|
+
"sizeLegendBackground", "sizeLegendSymbols", "sizeLegendLabels", "sizeLegendTitle"
|
|
31
31
|
])
|
|
32
32
|
}),
|
|
33
33
|
gradient: Object.freeze({
|
|
@@ -62,7 +62,7 @@ const LEGEND_RESOURCE_POLICIES = Object.freeze({
|
|
|
62
62
|
family: "strokeWidth",
|
|
63
63
|
rematerializeOp: "rematerializeStrokeWidthLegend",
|
|
64
64
|
graphicIds: Object.freeze([
|
|
65
|
-
"strokeWidthLegendSymbols", "strokeWidthLegendLabels",
|
|
65
|
+
"strokeWidthLegendBackground", "strokeWidthLegendSymbols", "strokeWidthLegendLabels",
|
|
66
66
|
"strokeWidthLegendTitle"
|
|
67
67
|
])
|
|
68
68
|
})
|
|
@@ -1,5 +1,11 @@
|
|
|
1
|
+
import { findLayer } from "../selectors/layers.js";
|
|
1
2
|
import { LEGEND_CONFIG_KINDS } from "../core/vocabulary.js";
|
|
2
3
|
|
|
4
|
+
export function isOpacityLegendLayer(layer) {
|
|
5
|
+
return ["point", "line"].includes(layer.mark?.type) &&
|
|
6
|
+
layer.encoding?.opacity?.scale !== undefined;
|
|
7
|
+
}
|
|
8
|
+
|
|
3
9
|
export function hasMaterializedLegend(program) {
|
|
4
10
|
return LEGEND_CONFIG_KINDS.some(kind => {
|
|
5
11
|
const config = program.guideConfigs?.legend?.[kind];
|
|
@@ -15,6 +21,9 @@ export function materializedLegendUsesScale(program, id) {
|
|
|
15
21
|
return LEGEND_CONFIG_KINDS.some(kind => {
|
|
16
22
|
const config = program.guideConfigs?.legend?.[kind];
|
|
17
23
|
if (config === undefined) return false;
|
|
18
|
-
|
|
24
|
+
const order = program.semanticSpec.guides.legend?.[kind]?.order;
|
|
25
|
+
const linkedScale = order?.channel === undefined ? undefined
|
|
26
|
+
: findLayer(program, config.target)?.encoding?.[order.channel]?.scale;
|
|
27
|
+
return config.scale === id || config.scales?.includes(id) || linkedScale === id;
|
|
19
28
|
});
|
|
20
29
|
}
|
|
@@ -108,11 +108,19 @@ export function isIntentionallyEmptyArea(program, layer) {
|
|
|
108
108
|
export function canMaterializeArc(_program, layer) {
|
|
109
109
|
if (
|
|
110
110
|
layer.mark?.type !== "arc" ||
|
|
111
|
-
layer.encoding?.theta?.scale === undefined
|
|
112
|
-
!["nominal", "ordinal"].includes(layer.encoding.theta.fieldType)
|
|
111
|
+
layer.encoding?.theta?.scale === undefined
|
|
113
112
|
) {
|
|
114
113
|
return false;
|
|
115
114
|
}
|
|
115
|
+
if (layer.encoding.theta.fieldType === "quantitative") {
|
|
116
|
+
return (
|
|
117
|
+
layer.encoding.theta.aggregate === undefined &&
|
|
118
|
+
layer.encoding?.radius === undefined
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
if (!["nominal", "ordinal"].includes(layer.encoding.theta.fieldType)) {
|
|
122
|
+
return false;
|
|
123
|
+
}
|
|
116
124
|
if (["count", "sum"].includes(layer.encoding.theta.aggregate)) {
|
|
117
125
|
return layer.encoding?.radius === undefined;
|
|
118
126
|
}
|
|
@@ -137,9 +145,9 @@ export function canMaterializeBar(program, layer) {
|
|
|
137
145
|
) {
|
|
138
146
|
const offsetChannel = resolveBarOffsetChannel(layer);
|
|
139
147
|
return (
|
|
140
|
-
layer.encoding?.color?.field !== undefined &&
|
|
141
|
-
layer.encoding?.[offsetChannel]?.field === layer.encoding.color.field &&
|
|
142
|
-
layer.encoding[offsetChannel]
|
|
148
|
+
(layer.encoding?.group !== undefined || (layer.encoding?.color?.field !== undefined &&
|
|
149
|
+
layer.encoding?.[offsetChannel]?.field === layer.encoding.color.field)) &&
|
|
150
|
+
layer.encoding[offsetChannel]?.scale !== undefined
|
|
143
151
|
);
|
|
144
152
|
}
|
|
145
153
|
return true;
|
|
@@ -155,14 +163,29 @@ export function canMaterializeRule(program, layer) {
|
|
|
155
163
|
);
|
|
156
164
|
}
|
|
157
165
|
|
|
166
|
+
const TEXT_SOURCE_CAPABILITIES = Object.freeze({
|
|
167
|
+
point: canMaterializePoint,
|
|
168
|
+
line: canMaterializeLine,
|
|
169
|
+
bar: canMaterializeBar,
|
|
170
|
+
rule: canMaterializeRule,
|
|
171
|
+
rect: (program, layer) => canMaterializeRect(program, layer) ||
|
|
172
|
+
program.markConfigs[layer.id]?.gradientPlot?.materialized === true,
|
|
173
|
+
arc: canMaterializeArc
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
export function isTextSource(layer) {
|
|
177
|
+
return layer?.data !== undefined &&
|
|
178
|
+
Object.hasOwn(TEXT_SOURCE_CAPABILITIES, layer.mark?.type);
|
|
179
|
+
}
|
|
180
|
+
|
|
158
181
|
export function canMaterializeText(program, layer) {
|
|
159
182
|
if (layer.mark?.type !== "text" || layer.encoding?.text === undefined) {
|
|
160
183
|
return false;
|
|
161
184
|
}
|
|
162
185
|
if (layer.source !== undefined) {
|
|
163
186
|
const source = findLayer(program, layer.source);
|
|
164
|
-
return source
|
|
165
|
-
[
|
|
187
|
+
return isTextSource(source) &&
|
|
188
|
+
TEXT_SOURCE_CAPABILITIES[source.mark.type](program, source) &&
|
|
166
189
|
Array.isArray(program.graphicSpec.objects[source.id]?.items);
|
|
167
190
|
}
|
|
168
191
|
return hasCartesianPositionScales(layer);
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { findUpstreamTransform } from "../dataProvenance.js";
|
|
2
|
+
import { readPathSeriesFields, validatePathSeriesAppearance } from "../../grammar/pathSeries.js";
|
|
3
|
+
|
|
4
|
+
export function validatePathGroupAppearance(program, layer, dataset) {
|
|
5
|
+
if (layer.mark.type === "area" && findUpstreamTransform(program, dataset, "density")) {
|
|
6
|
+
// Density placement owns a category/split grain beyond the visible group field.
|
|
7
|
+
readPathSeriesFields(dataset.values, layer);
|
|
8
|
+
return;
|
|
9
|
+
}
|
|
10
|
+
validatePathSeriesAppearance(dataset.values, layer);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function assertPathGroupCompatible(program, layer, dataset, group) {
|
|
14
|
+
if (layer.encoding?.parallel !== undefined) {
|
|
15
|
+
throw new Error("Parallel paths have row identity and do not support encodeGroup.");
|
|
16
|
+
}
|
|
17
|
+
const field = group?.field;
|
|
18
|
+
for (const [type, editor] of [
|
|
19
|
+
["density", "editDensity"],
|
|
20
|
+
["horizon", "editHorizon"],
|
|
21
|
+
["regression", "editRegression"],
|
|
22
|
+
["interval", "editErrorBand"]
|
|
23
|
+
]) {
|
|
24
|
+
const transform = findUpstreamTransform(program, dataset, type);
|
|
25
|
+
if (transform === undefined) continue;
|
|
26
|
+
const fields = type === "horizon" ? [transform.as.group]
|
|
27
|
+
: type === "interval"
|
|
28
|
+
? transform.groupBy.filter(value =>
|
|
29
|
+
value !== layer.encoding?.x?.field && value !== layer.encoding?.y?.field
|
|
30
|
+
)
|
|
31
|
+
: [transform.groupBy];
|
|
32
|
+
if (group?.fields !== undefined || group === undefined ||
|
|
33
|
+
field === undefined || !fields.includes(field)) {
|
|
34
|
+
throw new Error(
|
|
35
|
+
`${type === "density" ? "Density area" : type + " path"} mark "${layer.id}" must group by ${fields.length === 1 && fields[0] !== undefined ? JSON.stringify(fields[0]) : "its owned field"}; use ${editor}.`
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
if (layer.layout?.mode !== undefined) {
|
|
40
|
+
if (group === undefined && !["overlay"].includes(layer.layout.mode)) throw new Error("Remove active series layout before removing its group.");
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
if (layer.mark?.type === "area" && (
|
|
44
|
+
layer.encoding?.y?.stack === "center" ||
|
|
45
|
+
![undefined, "overlay"].includes(layer.encoding?.color?.layout)
|
|
46
|
+
)) {
|
|
47
|
+
if (group?.fields !== undefined ||
|
|
48
|
+
(layer.encoding?.color !== undefined && field !== layer.encoding.color.field)) {
|
|
49
|
+
throw new Error("Area layout requires one group field matching its color field.");
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -1,5 +1,11 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { isSourceOwnedText } from "../../grammar/text.js";
|
|
2
|
+
import { findLayer } from "../../selectors/layers.js";
|
|
3
|
+
import {
|
|
4
|
+
POSITION_CHANNELS,
|
|
5
|
+
POSITION_ENCODING_CHANNELS
|
|
6
|
+
} from "../../core/vocabulary.js";
|
|
2
7
|
import { getMarkMaterializationPolicy } from "./policies.js";
|
|
8
|
+
import { buildMaterializationPlan } from "../planner.js";
|
|
3
9
|
|
|
4
10
|
export {
|
|
5
11
|
canMaterializeArc,
|
|
@@ -10,10 +16,29 @@ export {
|
|
|
10
16
|
canMaterializeRect,
|
|
11
17
|
canMaterializeRule,
|
|
12
18
|
canMaterializeText,
|
|
13
|
-
canMaterializeTick
|
|
19
|
+
canMaterializeTick,
|
|
20
|
+
isTextSource
|
|
14
21
|
} from "./capabilities.js";
|
|
15
22
|
|
|
16
|
-
export function
|
|
23
|
+
export function getLayerScaleIds(layer) {
|
|
24
|
+
if (isSourceOwnedText(layer)) return [];
|
|
25
|
+
return [
|
|
26
|
+
...Object.values(layer.encoding ?? {}).map(encoding => encoding?.scale),
|
|
27
|
+
...(layer.encoding?.parallel?.dimensions ?? []).map(dimension => dimension.scale)
|
|
28
|
+
].filter(scale => scale !== undefined);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function getScaleConsumerMarkSteps(program, scaleIds) {
|
|
32
|
+
return program.semanticSpec.layers
|
|
33
|
+
.filter(layer => getLayerScaleIds(layer).some(id => scaleIds.includes(id)))
|
|
34
|
+
.map(layer => getMarkMaterializationStep(program, layer))
|
|
35
|
+
.filter(step => step !== undefined);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function getMarkRematerializationStep(program, layer) {
|
|
39
|
+
if (program.markConfigs?.[layer.id]?.markFilter?.empty === true) {
|
|
40
|
+
return { op: "materializeEmptyMark", args: { id: layer.id } };
|
|
41
|
+
}
|
|
17
42
|
const policy = getMarkMaterializationPolicy(layer);
|
|
18
43
|
return policy === undefined
|
|
19
44
|
? undefined
|
|
@@ -25,14 +50,15 @@ export function getMarkMaterializationStep(program, layer) {
|
|
|
25
50
|
if (policy === undefined || !policy.canMaterialize(program, layer)) {
|
|
26
51
|
return undefined;
|
|
27
52
|
}
|
|
28
|
-
return getMarkRematerializationStep(layer);
|
|
53
|
+
return getMarkRematerializationStep(program, layer);
|
|
29
54
|
}
|
|
30
55
|
|
|
31
56
|
export function getSourceDependentMarkSteps(program, sourceId) {
|
|
32
|
-
return program.semanticSpec.layers.flatMap(layer =>
|
|
57
|
+
return (program.semanticSpec.layers ?? []).flatMap(layer =>
|
|
33
58
|
layer.source === sourceId &&
|
|
34
59
|
getMarkMaterializationPolicy(layer)?.sourceDependent === true
|
|
35
|
-
? [getMarkMaterializationStep(program, layer)
|
|
60
|
+
? [getMarkMaterializationStep(program, layer) ??
|
|
61
|
+
getExistingMarkRematerializationStep(program, layer)].filter(
|
|
36
62
|
step => step !== undefined
|
|
37
63
|
)
|
|
38
64
|
: []
|
|
@@ -43,39 +69,39 @@ export function getPositionEncodingMaterializationSteps(program, layer, scaleId)
|
|
|
43
69
|
const policy = getMarkMaterializationPolicy(layer);
|
|
44
70
|
if (policy === undefined) return [];
|
|
45
71
|
const complete = policy.canMaterialize(program, layer);
|
|
46
|
-
const mark = getMarkRematerializationStep(layer);
|
|
72
|
+
const mark = getMarkRematerializationStep(program, layer);
|
|
47
73
|
const sharedConsumerMarks = (program.semanticSpec.layers ?? [])
|
|
48
74
|
.filter(candidate =>
|
|
49
75
|
candidate.id !== layer.id &&
|
|
76
|
+
!isSourceOwnedText(candidate) &&
|
|
50
77
|
POSITION_CHANNELS.some(channel =>
|
|
51
78
|
candidate.encoding?.[channel]?.scale === scaleId
|
|
52
79
|
)
|
|
53
80
|
)
|
|
54
81
|
.map(candidate => getMarkMaterializationStep(program, candidate))
|
|
55
82
|
.filter(step => step !== undefined);
|
|
83
|
+
const dependentMarks = [mark, ...sharedConsumerMarks]
|
|
84
|
+
.flatMap(step => getSourceDependentMarkSteps(program, step.args.id));
|
|
56
85
|
const scale = {
|
|
57
86
|
op: "rematerializeScale",
|
|
58
87
|
args: sharedConsumerMarks.length === 0
|
|
59
88
|
? { id: scaleId }
|
|
60
89
|
: { id: scaleId, marks: false }
|
|
61
90
|
};
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
}
|
|
66
|
-
return policy.positionEncoding.scaleFirst
|
|
91
|
+
const direct = !complete && policy.positionEncoding.incomplete === "scale"
|
|
92
|
+
? [scale, ...sharedConsumerMarks]
|
|
93
|
+
: policy.positionEncoding.scaleFirst
|
|
67
94
|
? [scale, mark, ...sharedConsumerMarks]
|
|
68
95
|
: [mark, ...sharedConsumerMarks];
|
|
69
|
-
}
|
|
70
|
-
return policy.positionEncoding.scaleFirst
|
|
71
|
-
? [scale, mark, ...sharedConsumerMarks]
|
|
72
|
-
: [mark, ...sharedConsumerMarks];
|
|
96
|
+
return buildMaterializationPlan({ marks: [...direct, ...dependentMarks] });
|
|
73
97
|
}
|
|
74
98
|
|
|
75
99
|
export function getScaleConsumerMaterializationMode(layer, channel) {
|
|
76
100
|
const policy = getMarkMaterializationPolicy(layer);
|
|
77
101
|
if (policy === undefined) return "direct";
|
|
78
|
-
if (
|
|
102
|
+
if (POSITION_ENCODING_CHANNELS.includes(channel)) {
|
|
103
|
+
// Attached labels require their source's completed geometry, after scales.
|
|
104
|
+
if (layer.source !== undefined && policy.sourceDependent === true) return "defer";
|
|
79
105
|
return policy.scaleApplication.position ?? policy.scaleApplication.default;
|
|
80
106
|
}
|
|
81
107
|
if (policy.scaleApplication.deferredChannels?.includes(channel)) {
|
|
@@ -96,7 +122,7 @@ export function getExistingMarkRematerializationStep(program, layer) {
|
|
|
96
122
|
) {
|
|
97
123
|
return undefined;
|
|
98
124
|
}
|
|
99
|
-
return getMarkRematerializationStep(layer);
|
|
125
|
+
return getMarkRematerializationStep(program, layer);
|
|
100
126
|
}
|
|
101
127
|
|
|
102
128
|
export function getEncodingMaterializationStages(program, layer, channel, scale) {
|
|
@@ -108,22 +134,34 @@ export function getEncodingMaterializationStages(program, layer, channel, scale)
|
|
|
108
134
|
args: { id: scale, guides: false, marks: false }
|
|
109
135
|
}]
|
|
110
136
|
: [];
|
|
111
|
-
const shared =
|
|
112
|
-
|
|
137
|
+
const shared = scale !== undefined && (
|
|
138
|
+
scales.length > 0 || policy.encoding?.sharedChannels?.includes(channel) === true
|
|
139
|
+
);
|
|
113
140
|
const candidates = shared
|
|
114
141
|
? program.semanticSpec.layers.filter(candidate =>
|
|
115
|
-
candidate.
|
|
116
|
-
candidate.encoding?.[channel]?.scale === scale
|
|
142
|
+
candidate.id === layer.id || candidate.encoding?.[channel]?.scale === scale
|
|
117
143
|
)
|
|
118
144
|
: [layer];
|
|
119
145
|
const marks = candidates.flatMap(candidate => {
|
|
120
|
-
|
|
146
|
+
const candidatePolicy = getMarkMaterializationPolicy(candidate);
|
|
147
|
+
if (candidatePolicy?.encoding?.skipRematerialization?.(program, candidate) === true) {
|
|
121
148
|
return [];
|
|
122
149
|
}
|
|
123
|
-
const step =
|
|
150
|
+
const step = candidate.id !== layer.id || candidatePolicy?.encoding?.completeOnly === true
|
|
124
151
|
? getMarkMaterializationStep(program, candidate)
|
|
125
|
-
: getMarkRematerializationStep(candidate);
|
|
152
|
+
: getMarkRematerializationStep(program, candidate);
|
|
126
153
|
return step === undefined ? [] : [step];
|
|
127
154
|
});
|
|
128
155
|
return { scales, marks };
|
|
129
156
|
}
|
|
157
|
+
|
|
158
|
+
// Reuse a plan's scale stage only when it covers every scale consumed by the mark.
|
|
159
|
+
export function reusePlannedMarkScales(program, steps, scaleIds) {
|
|
160
|
+
const resolved = new Set(scaleIds);
|
|
161
|
+
return steps.map(step => {
|
|
162
|
+
const layer = findLayer(program, step.args?.id);
|
|
163
|
+
if (layer === undefined || getMarkMaterializationPolicy(layer)?.acceptsResolvedScales !== true ||
|
|
164
|
+
!getLayerScaleIds(layer).every(id => resolved.has(id))) return step;
|
|
165
|
+
return { ...step, args: { ...step.args, scales: false } };
|
|
166
|
+
});
|
|
167
|
+
}
|
|
@@ -40,7 +40,9 @@ const MARK_MATERIALIZATION_POLICIES = Object.freeze({
|
|
|
40
40
|
line: Object.freeze({
|
|
41
41
|
canMaterialize: canMaterializeLine,
|
|
42
42
|
op: "rematerializeLineMark",
|
|
43
|
+
acceptsResolvedScales: true,
|
|
43
44
|
positionEncoding: Object.freeze({ incomplete: "scale", scaleFirst: false }),
|
|
45
|
+
encoding: Object.freeze({ scaleFirst: true, completeOnly: true }),
|
|
44
46
|
scaleApplication: Object.freeze({ default: "defer" })
|
|
45
47
|
}),
|
|
46
48
|
area: Object.freeze({
|
|
@@ -2,28 +2,36 @@ import {
|
|
|
2
2
|
mapOrdinalPositionValues,
|
|
3
3
|
readScaleField
|
|
4
4
|
} from "../grammar/scales/index.js";
|
|
5
|
-
import {
|
|
5
|
+
import { normalizePositionDatum } from "../grammar/positionDatum.js";
|
|
6
|
+
import { resolveGraphicBounds } from "../layout/canvas.js";
|
|
7
|
+
import { RECT_MODES, resolveRectMode, rectUsesFields } from "../grammar/rects.js";
|
|
6
8
|
import { DEFAULT_RECT_MARK } from "./rectConfig.js";
|
|
7
9
|
import { mapScaleConsumerValues } from "./scales/map.js";
|
|
8
10
|
|
|
9
|
-
function optionalValues(rows, encoding) {
|
|
11
|
+
function optionalValues(rows, encoding, length, channel) {
|
|
12
|
+
if (Object.hasOwn(encoding, "datum")) {
|
|
13
|
+
const value = normalizePositionDatum(encoding.datum, encoding.fieldType, channel, encoding.temporalUnit, "Rect");
|
|
14
|
+
return Array.from({ length }, () => value);
|
|
15
|
+
}
|
|
10
16
|
return readScaleField(rows, encoding.field, encoding.fieldType, {
|
|
11
|
-
allowUnknown: true
|
|
17
|
+
allowUnknown: true, temporalUnit: encoding.temporalUnit
|
|
12
18
|
});
|
|
13
19
|
}
|
|
14
20
|
|
|
15
21
|
function requiredChannels(layer, mode) {
|
|
16
|
-
|
|
17
|
-
? ["
|
|
18
|
-
: ["x", "y",
|
|
22
|
+
const position = mode === RECT_MODES.xSpan ? ["x", "x2"]
|
|
23
|
+
: mode === RECT_MODES.ySpan ? ["y", "y2"]
|
|
24
|
+
: mode === RECT_MODES.ranged ? ["x", "x2", "y", "y2"] : ["x", "y"];
|
|
25
|
+
return [...position, ...(layer.encoding?.color ? ["color"] : [])];
|
|
19
26
|
}
|
|
20
27
|
|
|
21
28
|
export function resolveRectConsumerValues(layer, dataset, channel) {
|
|
22
|
-
const
|
|
29
|
+
const length = rectUsesFields(layer) ? dataset.values.length : 1;
|
|
30
|
+
const requested = optionalValues(dataset.values, layer.encoding[channel], length, channel);
|
|
23
31
|
const mode = resolveRectMode(layer);
|
|
24
32
|
if (mode === undefined) return requested;
|
|
25
33
|
const complete = requiredChannels(layer, mode).map(candidate =>
|
|
26
|
-
optionalValues(dataset.values, layer.encoding[candidate])
|
|
34
|
+
optionalValues(dataset.values, layer.encoding[candidate], length, candidate)
|
|
27
35
|
);
|
|
28
36
|
return requested.map((value, index) =>
|
|
29
37
|
complete.every(values => values[index] !== undefined) ? value : undefined
|
|
@@ -68,24 +76,22 @@ function appearance(config, fill) {
|
|
|
68
76
|
export function resolveRectRows(program, layer, dataset) {
|
|
69
77
|
const mode = resolveRectMode(layer);
|
|
70
78
|
if (mode === undefined) return [];
|
|
71
|
-
const
|
|
72
|
-
const
|
|
73
|
-
const
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
const y2 = mode === RECT_MODES.ranged
|
|
77
|
-
? mappedEncoding(program, layer, dataset, "y2")
|
|
78
|
-
: undefined;
|
|
79
|
+
const bounds = resolveGraphicBounds(program);
|
|
80
|
+
const x = layer.encoding.x === undefined ? undefined : mappedEncoding(program, layer, dataset, "x");
|
|
81
|
+
const y = layer.encoding.y === undefined ? undefined : mappedEncoding(program, layer, dataset, "y");
|
|
82
|
+
const x2 = layer.encoding.x2 === undefined ? undefined : mappedEncoding(program, layer, dataset, "x2");
|
|
83
|
+
const y2 = layer.encoding.y2 === undefined ? undefined : mappedEncoding(program, layer, dataset, "y2");
|
|
79
84
|
const color = layer.encoding?.color === undefined
|
|
80
85
|
? undefined
|
|
81
86
|
: mappedEncoding(program, layer, dataset, "color").values;
|
|
82
87
|
const config = program.markConfigs[layer.id] ?? DEFAULT_RECT_MARK;
|
|
83
88
|
|
|
84
|
-
|
|
89
|
+
const rows = rectUsesFields(layer) ? dataset.values : [{}];
|
|
90
|
+
return rows.flatMap((row, index) => {
|
|
85
91
|
const fill = color === undefined ? config.fill : color[index];
|
|
86
92
|
if (
|
|
87
|
-
!Number.isFinite(x.values[index]) ||
|
|
88
|
-
!Number.isFinite(y.values[index]) ||
|
|
93
|
+
(x !== undefined && !Number.isFinite(x.values[index])) ||
|
|
94
|
+
(y !== undefined && !Number.isFinite(y.values[index])) ||
|
|
89
95
|
typeof fill !== "string"
|
|
90
96
|
) return [];
|
|
91
97
|
|
|
@@ -103,28 +109,22 @@ export function resolveRectRows(program, layer, dataset) {
|
|
|
103
109
|
height
|
|
104
110
|
};
|
|
105
111
|
} else {
|
|
106
|
-
const
|
|
107
|
-
const
|
|
112
|
+
const startX = x?.values[index] ?? bounds.x;
|
|
113
|
+
const startY = y?.values[index] ?? bounds.y;
|
|
114
|
+
const endX = x2?.values[index] ?? bounds.x + bounds.width;
|
|
115
|
+
const endY = y2?.values[index] ?? bounds.y + bounds.height;
|
|
108
116
|
if (!Number.isFinite(endX) || !Number.isFinite(endY)) return [];
|
|
109
117
|
geometry = {
|
|
110
|
-
x: Math.min(
|
|
111
|
-
y: Math.min(
|
|
112
|
-
width: Math.abs(endX -
|
|
113
|
-
height: Math.abs(endY -
|
|
118
|
+
x: Math.min(startX, endX),
|
|
119
|
+
y: Math.min(startY, endY),
|
|
120
|
+
width: Math.abs(endX - startX),
|
|
121
|
+
height: Math.abs(endY - startY)
|
|
114
122
|
};
|
|
115
123
|
if (geometry.width <= 0 || geometry.height <= 0) return [];
|
|
116
124
|
}
|
|
117
125
|
return [{
|
|
118
126
|
row,
|
|
119
127
|
sourceIndex: index,
|
|
120
|
-
channels: Object.fromEntries(
|
|
121
|
-
["x", "y", "x2", "y2", "color"].flatMap(channel => {
|
|
122
|
-
const encoding = layer.encoding?.[channel];
|
|
123
|
-
return encoding?.field === undefined
|
|
124
|
-
? []
|
|
125
|
-
: [[channel, row[encoding.field]]];
|
|
126
|
-
})
|
|
127
|
-
),
|
|
128
128
|
properties: { ...geometry, ...appearance(config, fill) }
|
|
129
129
|
}];
|
|
130
130
|
});
|