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
|
@@ -1,161 +1,47 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
resolveBarChannels,
|
|
5
|
-
resolveBarColorLayout
|
|
6
|
-
} from "../../grammar/bars/policy.js";
|
|
7
|
-
import {
|
|
8
|
-
DEFAULT_SERIES_BASELINE,
|
|
9
|
-
layoutSeriesPartition
|
|
10
|
-
} from "../../grammar/seriesLayout.js";
|
|
11
|
-
import {
|
|
12
|
-
isDiscretePositionScaleType,
|
|
13
|
-
mapContinuousScaleValues,
|
|
14
|
-
mapOrdinalPositionValues
|
|
15
|
-
} from "../../grammar/scales/index.js";
|
|
1
|
+
import { deriveSeriesBarCells } from "../../grammar/bars/aggregate.js";
|
|
2
|
+
import { resolveBarChannels, resolveBarColorLayout, resolveBarOffsetChannel } from "../../grammar/bars/policy.js";
|
|
3
|
+
import { mapContinuousScaleValues, mapOrdinalPositionValues } from "../../grammar/scales/index.js";
|
|
16
4
|
import { mapScaleConsumerValues } from "../scales/map.js";
|
|
17
|
-
import {
|
|
18
|
-
DEFAULT_BAR_FILL,
|
|
19
|
-
resolveBarAppearance
|
|
20
|
-
} from "./resolve.js";
|
|
21
|
-
import { deriveGroupedRectangles } from "./grouped.js";
|
|
5
|
+
import { DEFAULT_BAR_FILL, resolveBarAppearance } from "./resolve.js";
|
|
22
6
|
import { resolveBarWidth } from "../../grammar/bars/geometry.js";
|
|
7
|
+
import { finiteMidpoint } from "../../grammar/numeric.js";
|
|
23
8
|
|
|
24
9
|
export function deriveAggregateRectangles(required, resolved, widthConfig) {
|
|
25
10
|
const { dataset, layer } = required;
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
const
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
const
|
|
37
|
-
const colorEncoding = layer.encoding?.color;
|
|
38
|
-
const colorScale = resolved.resolvedScales[colorEncoding?.scale];
|
|
39
|
-
if (colorEncoding !== undefined && colorScale === undefined) {
|
|
40
|
-
throw new Error(
|
|
41
|
-
`Bar mark "${layer.id}" requires a resolved color scale.`
|
|
42
|
-
);
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
const seriesDomain = colorScale?.domain ?? [undefined];
|
|
46
|
-
const seriesIndex = new Map(
|
|
47
|
-
seriesDomain.map((value, index) => [value, index])
|
|
48
|
-
);
|
|
49
|
-
const colors = new Map(seriesDomain.map((value, index) => [
|
|
50
|
-
value,
|
|
51
|
-
colorScale?.range[index % colorScale.range.length] ?? DEFAULT_BAR_FILL
|
|
52
|
-
]));
|
|
53
|
-
const cells = deriveBarAggregates(dataset.values, layer).values;
|
|
54
|
-
const discreteCategory = categoryScale.type === "ordinal" ||
|
|
55
|
-
isDiscretePositionScaleType(categoryScale.type);
|
|
56
|
-
const categoryDomain = discreteCategory
|
|
57
|
-
? categoryScale.domain
|
|
58
|
-
: [...new Set(cells.map(cell => cell[channels.category]))]
|
|
59
|
-
.sort((left, right) => left - right);
|
|
60
|
-
const categoryIndex = new Map(
|
|
61
|
-
categoryDomain.map((value, index) => [value, index])
|
|
62
|
-
);
|
|
63
|
-
const cellMap = new Map(cells.map(cell => [
|
|
64
|
-
JSON.stringify([cell[channels.category], cell.color]),
|
|
65
|
-
cell
|
|
66
|
-
]));
|
|
67
|
-
const thickness = resolveBarWidth(
|
|
68
|
-
widthConfig,
|
|
69
|
-
Math.abs(categoryScale.bandwidth ?? categoryScale.step)
|
|
70
|
-
);
|
|
71
|
-
const baseline = DEFAULT_SERIES_BASELINE;
|
|
72
|
-
const continuousColor = colorEncoding?.fieldType === "quantitative";
|
|
73
|
-
if (continuousColor) {
|
|
74
|
-
const mappedColors = mapScaleConsumerValues(
|
|
75
|
-
cells.map(cell => cell.color), colorScale, "color"
|
|
76
|
-
);
|
|
77
|
-
const cellByCategory = new Map(
|
|
78
|
-
cells.map((cell, index) => [cell[channels.category], { cell, index }])
|
|
79
|
-
);
|
|
80
|
-
const existing = resolved.graphicSpec.objects[layer.id].items;
|
|
81
|
-
const config = resolved.markConfigs[layer.id] ?? {};
|
|
82
|
-
const appearance = config.barAppearance ?? {};
|
|
83
|
-
return categoryDomain.flatMap(categoryValue => {
|
|
84
|
-
const entry = cellByCategory.get(categoryValue);
|
|
85
|
-
if (entry === undefined) return [];
|
|
86
|
-
const { cell, index } = entry;
|
|
87
|
-
const categoryCenter = discreteCategory
|
|
88
|
-
? mapOrdinalPositionValues([categoryValue], categoryScale)[0]
|
|
89
|
-
: mapContinuousScaleValues(
|
|
90
|
-
[categoryValue],
|
|
91
|
-
categoryScale
|
|
92
|
-
)[0];
|
|
93
|
-
const [segment] = layoutSeriesPartition(
|
|
94
|
-
[cell[channels.measure]],
|
|
95
|
-
layout,
|
|
96
|
-
{ baseline }
|
|
97
|
-
);
|
|
98
|
-
if (segment === undefined) return [];
|
|
99
|
-
const [start, end] = mapContinuousScaleValues(
|
|
100
|
-
[segment.start, segment.end],
|
|
101
|
-
measureScale
|
|
102
|
-
);
|
|
103
|
-
return [{
|
|
104
|
-
x: vertical ? categoryCenter - thickness / 2 : Math.min(start, end),
|
|
105
|
-
y: vertical ? Math.min(start, end) : categoryCenter - thickness / 2,
|
|
106
|
-
width: vertical ? thickness : Math.abs(start - end),
|
|
107
|
-
height: vertical ? Math.abs(start - end) : thickness,
|
|
108
|
-
fill: mappedColors[index],
|
|
109
|
-
...resolveBarAppearance(config, existing[index]?.properties)
|
|
110
|
-
}];
|
|
111
|
-
});
|
|
112
|
-
}
|
|
113
|
-
const segments = [];
|
|
114
|
-
|
|
115
|
-
for (const category of categoryDomain) {
|
|
116
|
-
const partition = seriesDomain.map(color =>
|
|
117
|
-
cellMap.get(JSON.stringify([category, color]))?.[channels.measure] ?? 0
|
|
118
|
-
);
|
|
119
|
-
for (const segment of layoutSeriesPartition(partition, layout, { baseline })) {
|
|
120
|
-
const color = seriesDomain[segment.index];
|
|
121
|
-
const cell = cellMap.get(JSON.stringify([category, color]));
|
|
122
|
-
if (cell === undefined) continue;
|
|
123
|
-
segments.push({ category, color, ...segment });
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
const existing = resolved.graphicSpec.objects[layer.id].items;
|
|
11
|
+
const channels = resolveBarChannels(layer), vertical = channels.orientation === "vertical";
|
|
12
|
+
const categoryScale = resolved.resolvedScales[layer.encoding[channels.category].scale];
|
|
13
|
+
const measureScale = resolved.resolvedScales[layer.encoding[channels.measure].scale];
|
|
14
|
+
const color = layer.encoding.color, colorScale = resolved.resolvedScales[color?.scale];
|
|
15
|
+
const grouped = resolveBarColorLayout(layer) === "group";
|
|
16
|
+
const offsetScale = resolved.resolvedScales[layer.encoding[resolveBarOffsetChannel(layer)]?.scale];
|
|
17
|
+
if (grouped && offsetScale === undefined) throw new Error("Grouped bar requires a resolved offset scale.");
|
|
18
|
+
const temporal = categoryScale.type === "time";
|
|
19
|
+
const segments = deriveSeriesBarCells(dataset.values, layer, temporal ? undefined : categoryScale.domain,
|
|
20
|
+
grouped ? offsetScale.domain : layer.encoding.group === undefined && color?.fieldType !== "quantitative" ? colorScale?.domain : undefined);
|
|
21
|
+
const fills = color === undefined ? undefined : mapScaleConsumerValues(segments.map(segment => segment.cell.color), colorScale, "color");
|
|
128
22
|
const config = resolved.markConfigs[layer.id] ?? {};
|
|
129
|
-
const
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
23
|
+
const existing = resolved.graphicSpec.objects[layer.id].items;
|
|
24
|
+
const width = resolveBarWidth(widthConfig, grouped ? offsetScale.bandwidth : Math.abs(categoryScale.bandwidth ?? categoryScale.step));
|
|
25
|
+
const indices = new Map(categoryScale.domain.map((value, index) => [value, index]));
|
|
26
|
+
return segments.map(({ cell, start, end, seriesIndex }, index) => {
|
|
27
|
+
const categoryValue = cell[channels.category];
|
|
28
|
+
const categoryCenter = (temporal ? mapContinuousScaleValues : mapOrdinalPositionValues)([categoryValue], categoryScale)[0];
|
|
29
|
+
let position = !grouped && layer.encoding.group !== undefined && layer.encoding.group.inferredFrom === undefined && !temporal && categoryScale.step > 0
|
|
30
|
+
? (categoryScale.start ?? categoryScale.range[0]) + indices.get(categoryValue) * categoryScale.step + categoryScale.bandwidth / 2 - width / 2
|
|
31
|
+
: categoryCenter - width / 2;
|
|
32
|
+
if (grouped) {
|
|
33
|
+
const direction = Math.sign(categoryScale.step ?? categoryScale.range[1] - categoryScale.range[0]) || 1;
|
|
34
|
+
const offsetCenter = offsetScale.start + seriesIndex * offsetScale.step + (Math.sign(offsetScale.step) || 1) * offsetScale.bandwidth / 2;
|
|
35
|
+
if (vertical && direction > 0 && offsetScale.step > 0) {
|
|
36
|
+
const categoryStart = temporal ? categoryCenter - categoryScale.bandwidth / 2
|
|
37
|
+
: (categoryScale.start ?? categoryScale.range[0]) + indices.get(categoryValue) * categoryScale.step;
|
|
38
|
+
position = categoryStart + offsetScale.start + seriesIndex * offsetScale.step + (offsetScale.bandwidth - width) / 2;
|
|
39
|
+
} else position = categoryCenter + direction * (offsetCenter - finiteMidpoint(...offsetScale.range)) - width / 2;
|
|
135
40
|
}
|
|
136
|
-
const
|
|
137
|
-
|
|
138
|
-
:
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
)[0];
|
|
142
|
-
const [start, end] = mapContinuousScaleValues(
|
|
143
|
-
[segment.start, segment.end],
|
|
144
|
-
measureScale
|
|
145
|
-
);
|
|
146
|
-
return {
|
|
147
|
-
x: vertical
|
|
148
|
-
? categoryCenter - thickness / 2
|
|
149
|
-
: Math.min(start, end),
|
|
150
|
-
y: vertical
|
|
151
|
-
? Math.min(start, end)
|
|
152
|
-
: categoryCenter - thickness / 2,
|
|
153
|
-
width: vertical ? thickness : Math.abs(start - end),
|
|
154
|
-
height: vertical ? Math.abs(start - end) : thickness,
|
|
155
|
-
fill: segment.color === undefined
|
|
156
|
-
? appearance.fill ?? config.fill ?? colors.get(segment.color)
|
|
157
|
-
: colors.get(segment.color),
|
|
158
|
-
...resolveBarAppearance(config, existing[index]?.properties)
|
|
159
|
-
};
|
|
41
|
+
const [a, b] = mapContinuousScaleValues([start, end], measureScale);
|
|
42
|
+
return { x: vertical ? position : Math.min(a, b), y: vertical ? Math.min(a, b) : position,
|
|
43
|
+
width: vertical ? width : Math.abs(a - b), height: vertical ? Math.abs(a - b) : width,
|
|
44
|
+
fill: fills?.[index] ?? config.barAppearance?.fill ?? config.fill ?? DEFAULT_BAR_FILL,
|
|
45
|
+
...resolveBarAppearance(config, existing[index]?.properties) };
|
|
160
46
|
});
|
|
161
47
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { readSeriesIdentity } from "../../grammar/pathSeries.js";
|
|
1
2
|
import {
|
|
2
3
|
countHistogramBins,
|
|
3
4
|
findHistogramBinIndex,
|
|
@@ -34,6 +35,29 @@ export function deriveHistogramSegments({
|
|
|
34
35
|
const colorEncoding = layer.encoding?.color;
|
|
35
36
|
const layout = resolveBarColorLayout(layer);
|
|
36
37
|
|
|
38
|
+
if (layer.encoding?.group !== undefined) {
|
|
39
|
+
const identity = readSeriesIdentity(dataset.values, layer);
|
|
40
|
+
const cellRows = bins.boundaries.slice(0, -1).map(() => identity.domain.map(() => []));
|
|
41
|
+
const index = new Map(identity.domain.map((value, i) => [value, i]));
|
|
42
|
+
xValues.forEach((value, i) => { const bin = findHistogramBinIndex(value, bins.boundaries);
|
|
43
|
+
if (bin !== -1) cellRows[bin][index.get(identity.values[i])].push(dataset.values[i]); });
|
|
44
|
+
return cellRows.flatMap((cells, bin) => layoutSeriesPartition(cells.map(rows => rows.length), layout).map(segment => {
|
|
45
|
+
const members = cells[segment.index];
|
|
46
|
+
let color, colorValue;
|
|
47
|
+
if (colorEncoding !== undefined) {
|
|
48
|
+
const values = readNominalField(members, colorEncoding.field);
|
|
49
|
+
if (new Set(values).size !== 1) throw new Error("Histogram color requires one value within each bin/series cell.");
|
|
50
|
+
colorValue = values[0];
|
|
51
|
+
const scale = resolvedScales[colorEncoding.scale];
|
|
52
|
+
color = mapOrdinalValues([colorValue], scale.domain, scale.range)[0];
|
|
53
|
+
}
|
|
54
|
+
return { bin, start: bins.boundaries[bin], end: bins.boundaries[bin + 1],
|
|
55
|
+
category: segment.index, categoryCount: identity.domain.length,
|
|
56
|
+
stackStart: segment.start, stackEnd: segment.end, members,
|
|
57
|
+
...(color === undefined ? {} : { color, colorValue }) };
|
|
58
|
+
}));
|
|
59
|
+
}
|
|
60
|
+
|
|
37
61
|
if (colorEncoding?.scale === undefined) {
|
|
38
62
|
return countHistogramBins(xValues, bins.boundaries).flatMap((count, bin) =>
|
|
39
63
|
layoutSeriesPartition([count], layout).map(segment => ({
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
export function resolveCategorySlotOffset(program, layer) {
|
|
2
|
+
const policy = program.markConfigs[layer.id]?.categorySlotOffset;
|
|
3
|
+
if (policy === undefined) return undefined;
|
|
4
|
+
if (!["x", "y"].includes(policy.channel)) {
|
|
5
|
+
throw new Error(`Category slot offset for "${layer.id}" requires channel x or y.`);
|
|
6
|
+
}
|
|
7
|
+
if (!Number.isFinite(policy.band) || policy.band < -0.5 || policy.band > 0.5) {
|
|
8
|
+
throw new RangeError(`Category slot offset for "${layer.id}" must be within [-0.5, 0.5] band.`);
|
|
9
|
+
}
|
|
10
|
+
const encoding = layer.encoding?.[policy.channel];
|
|
11
|
+
if (!["nominal", "ordinal"].includes(encoding?.fieldType)) {
|
|
12
|
+
throw new Error(`Category slot offset for "${layer.id}" requires a categorical position.`);
|
|
13
|
+
}
|
|
14
|
+
const scale = program.resolvedScales[encoding.scale];
|
|
15
|
+
if (scale?.type !== "band" || !Number.isFinite(scale.bandwidth) || scale.bandwidth <= 0) {
|
|
16
|
+
throw new Error(`Category slot offset for "${layer.id}" requires a positive band scale.`);
|
|
17
|
+
}
|
|
18
|
+
return { channel: policy.channel, pixels: scale.bandwidth * policy.band };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function offsetCategoryPositions(program, layer, positions) {
|
|
22
|
+
const offset = resolveCategorySlotOffset(program, layer);
|
|
23
|
+
if (offset === undefined || offset.pixels === 0) return positions;
|
|
24
|
+
const shift = values => values === undefined
|
|
25
|
+
? undefined
|
|
26
|
+
: values.map(value => Number.isFinite(value) ? value + offset.pixels : value);
|
|
27
|
+
return {
|
|
28
|
+
...positions,
|
|
29
|
+
[offset.channel]: shift(positions[offset.channel])
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function offsetCategoryRectangles(program, layer, rectangles) {
|
|
34
|
+
const offset = resolveCategorySlotOffset(program, layer);
|
|
35
|
+
if (offset === undefined || offset.pixels === 0) return rectangles;
|
|
36
|
+
return rectangles.map(rectangle => ({
|
|
37
|
+
...rectangle,
|
|
38
|
+
[offset.channel]: rectangle[offset.channel] + offset.pixels
|
|
39
|
+
}));
|
|
40
|
+
}
|
|
@@ -32,7 +32,7 @@ export function namespaceGraphicId(namespace, id) {
|
|
|
32
32
|
const encode = value => Array.from(value, character =>
|
|
33
33
|
character.codePointAt(0).toString(16).padStart(6, "0")
|
|
34
34
|
).join("");
|
|
35
|
-
return `g${encode(namespace)}_${
|
|
35
|
+
return `g${encode(namespace)}_${id}`;
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
function rewriteItem(item, namespace) {
|
|
@@ -1,12 +1,16 @@
|
|
|
1
|
+
import { withGuideLayoutTransaction } from "./guides/layout.js";
|
|
1
2
|
import {
|
|
2
3
|
canDeferScaleConsumerApplication,
|
|
4
|
+
getLayerScaleIds,
|
|
3
5
|
getExistingMarkRematerializationStep,
|
|
4
6
|
getMarkMaterializationStep,
|
|
5
|
-
|
|
7
|
+
getScaleConsumerMarkSteps,
|
|
8
|
+
getSourceDependentMarkSteps,
|
|
9
|
+
reusePlannedMarkScales
|
|
6
10
|
} from "./marks/index.js";
|
|
7
|
-
import { requireLayer } from "../selectors/layers.js";
|
|
11
|
+
import { findLayer, requireLayer } from "../selectors/layers.js";
|
|
8
12
|
import {
|
|
9
|
-
applyMaterializationPlan,
|
|
13
|
+
applyMaterializationPlan as executeMaterializationPlan,
|
|
10
14
|
buildMaterializationPlan
|
|
11
15
|
} from "./planner.js";
|
|
12
16
|
import { hasMaterializedLegend } from "./legends.js";
|
|
@@ -16,13 +20,6 @@ import {
|
|
|
16
20
|
} from "./scaleGuideDependencies.js";
|
|
17
21
|
import { planLayoutRematerialization } from "./layout.js";
|
|
18
22
|
|
|
19
|
-
function layerScaleIds(layer) {
|
|
20
|
-
return [
|
|
21
|
-
...Object.values(layer.encoding ?? {}).map(encoding => encoding?.scale),
|
|
22
|
-
...(layer.encoding?.parallel?.dimensions ?? []).map(dimension => dimension.scale)
|
|
23
|
-
].filter(scale => scale !== undefined);
|
|
24
|
-
}
|
|
25
|
-
|
|
26
23
|
export function planCanvasRematerialization(program) {
|
|
27
24
|
const marks = [];
|
|
28
25
|
for (const layer of program.semanticSpec.layers) {
|
|
@@ -38,7 +35,7 @@ export function planCanvasRematerialization(program) {
|
|
|
38
35
|
if (needsCanvasScaleRematerialization(program, scale)) {
|
|
39
36
|
const deferredConsumers = program.semanticSpec.layers.filter(layer =>
|
|
40
37
|
canDeferScaleConsumerApplication(layer) &&
|
|
41
|
-
|
|
38
|
+
getLayerScaleIds(layer).includes(scale.id)
|
|
42
39
|
);
|
|
43
40
|
const canDeferMarks = deferredConsumers.length === 0 ||
|
|
44
41
|
deferredConsumers.every(layer => deferredMarkIds.has(layer.id));
|
|
@@ -66,7 +63,7 @@ export function planCanvasRematerialization(program) {
|
|
|
66
63
|
|
|
67
64
|
export function planLayerDataRematerialization(program, id) {
|
|
68
65
|
const layer = requireLayer(program, id);
|
|
69
|
-
const scaleIds = [...new Set(
|
|
66
|
+
const scaleIds = [...new Set(getLayerScaleIds(layer))];
|
|
70
67
|
const markStep = getMarkMaterializationStep(program, layer);
|
|
71
68
|
const scales = scaleIds.map(scale => ({
|
|
72
69
|
op: "rematerializeScale",
|
|
@@ -76,15 +73,19 @@ export function planLayerDataRematerialization(program, id) {
|
|
|
76
73
|
...(markStep === undefined ? {} : { marks: false })
|
|
77
74
|
}
|
|
78
75
|
}));
|
|
79
|
-
const
|
|
76
|
+
const directMarks = [
|
|
80
77
|
...(markStep === undefined ? [] : [markStep]),
|
|
81
|
-
...
|
|
78
|
+
...getScaleConsumerMarkSteps(program, scaleIds)
|
|
79
|
+
];
|
|
80
|
+
const marks = [
|
|
81
|
+
...directMarks,
|
|
82
|
+
...directMarks.flatMap(step => getSourceDependentMarkSteps(program, step.args.id))
|
|
82
83
|
];
|
|
83
84
|
if (scales.length > 0 || marks.length > 0) {
|
|
84
85
|
const guides = scaleIds.flatMap(scale =>
|
|
85
86
|
planScaleGuideRematerialization(program, scale)
|
|
86
87
|
);
|
|
87
|
-
return buildMaterializationPlan({ scales, marks, guides });
|
|
88
|
+
return buildMaterializationPlan({ scales, marks: reusePlannedMarkScales(program, marks, scaleIds), guides });
|
|
88
89
|
}
|
|
89
90
|
const existingStep = getExistingMarkRematerializationStep(program, layer);
|
|
90
91
|
return buildMaterializationPlan({
|
|
@@ -99,5 +100,45 @@ export function applyLayerDataRematerialization(program, id) {
|
|
|
99
100
|
);
|
|
100
101
|
}
|
|
101
102
|
|
|
102
|
-
export
|
|
103
|
+
export function applyLayerEmptyDataView(program, id) {
|
|
104
|
+
requireLayer(program, id);
|
|
105
|
+
const targets = [
|
|
106
|
+
id,
|
|
107
|
+
...getSourceDependentMarkSteps(program, id).map(step => step.args.id)
|
|
108
|
+
];
|
|
109
|
+
let next = program;
|
|
110
|
+
for (const target of new Set(targets)) {
|
|
111
|
+
const graphic = next.graphicSpec.objects[target];
|
|
112
|
+
if (graphic === undefined) continue;
|
|
113
|
+
next = graphic.type === "collection"
|
|
114
|
+
? next.editGraphics({ target, property: "items", value: [] })
|
|
115
|
+
: next.editGraphics({ target, property: "length", value: 0 });
|
|
116
|
+
}
|
|
117
|
+
return next;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function applyDetachedScaleRematerialization(program, previousLayers) {
|
|
121
|
+
const retained = new Set(program.semanticSpec.layers.flatMap(getLayerScaleIds));
|
|
122
|
+
const scaleIds = [...new Set(previousLayers.flatMap(layer => {
|
|
123
|
+
const current = findLayer(program, layer.id);
|
|
124
|
+
const currentIds = current === undefined ? [] : getLayerScaleIds(current);
|
|
125
|
+
return getLayerScaleIds(layer).filter(id => !currentIds.includes(id) && retained.has(id));
|
|
126
|
+
}))];
|
|
127
|
+
if (scaleIds.length === 0) return program;
|
|
128
|
+
const directMarks = getScaleConsumerMarkSteps(program, scaleIds);
|
|
129
|
+
return applyMaterializationPlan(program, buildMaterializationPlan({
|
|
130
|
+
scales: scaleIds.map(id => ({
|
|
131
|
+
op: "rematerializeScale", args: { id, guides: false, marks: false }
|
|
132
|
+
})),
|
|
133
|
+
marks: [
|
|
134
|
+
...directMarks,
|
|
135
|
+
...directMarks.flatMap(step => getSourceDependentMarkSteps(program, step.args.id))
|
|
136
|
+
],
|
|
137
|
+
guides: scaleIds.flatMap(id => planScaleGuideRematerialization(program, id))
|
|
138
|
+
}));
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export function applyMaterializationPlan(program, plan) {
|
|
142
|
+
return withGuideLayoutTransaction(program, next => executeMaterializationPlan(next, plan));
|
|
143
|
+
}
|
|
103
144
|
export { planScaleGuideRematerialization };
|
|
@@ -25,7 +25,7 @@ export function planEncodingRematerialization(program, {
|
|
|
25
25
|
);
|
|
26
26
|
const marks = [
|
|
27
27
|
...directMarks,
|
|
28
|
-
...getSourceDependentMarkSteps(program,
|
|
28
|
+
...directMarks.flatMap(step => getSourceDependentMarkSteps(program, step.args.id))
|
|
29
29
|
];
|
|
30
30
|
|
|
31
31
|
const guides = [];
|
|
@@ -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,
|
|
@@ -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
|
}
|
|
@@ -145,9 +145,9 @@ export function canMaterializeBar(program, layer) {
|
|
|
145
145
|
) {
|
|
146
146
|
const offsetChannel = resolveBarOffsetChannel(layer);
|
|
147
147
|
return (
|
|
148
|
-
layer.encoding?.color?.field !== undefined &&
|
|
149
|
-
layer.encoding?.[offsetChannel]?.field === layer.encoding.color.field &&
|
|
150
|
-
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
|
|
151
151
|
);
|
|
152
152
|
}
|
|
153
153
|
return true;
|
|
@@ -163,14 +163,29 @@ export function canMaterializeRule(program, layer) {
|
|
|
163
163
|
);
|
|
164
164
|
}
|
|
165
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
|
+
|
|
166
181
|
export function canMaterializeText(program, layer) {
|
|
167
182
|
if (layer.mark?.type !== "text" || layer.encoding?.text === undefined) {
|
|
168
183
|
return false;
|
|
169
184
|
}
|
|
170
185
|
if (layer.source !== undefined) {
|
|
171
186
|
const source = findLayer(program, layer.source);
|
|
172
|
-
return source
|
|
173
|
-
[
|
|
187
|
+
return isTextSource(source) &&
|
|
188
|
+
TEXT_SOURCE_CAPABILITIES[source.mark.type](program, source) &&
|
|
174
189
|
Array.isArray(program.graphicSpec.objects[source.id]?.items);
|
|
175
190
|
}
|
|
176
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
|
+
}
|