ggaction 0.0.3 → 0.0.4
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 +28 -3
- package/README.md +95 -83
- package/package.json +11 -6
- package/src/actions/data/regression.js +1 -1
- package/src/actions/encodings/appearance.js +1 -1
- package/src/actions/encodings/color/continuous.js +137 -0
- package/src/actions/encodings/color/index.js +151 -0
- package/src/actions/encodings/color/layout.js +91 -0
- package/src/actions/encodings/color/policy.js +88 -0
- package/src/actions/encodings/index.js +2 -2
- package/src/actions/encodings/offset.js +1 -1
- package/src/actions/encodings/{position.js → position/index.js} +6 -6
- package/src/actions/encodings/position/resolve.js +1 -1
- package/src/actions/encodings/ranged.js +2 -2
- package/src/actions/encodings/strokeDash.js +1 -1
- package/src/actions/encodings/text.js +1 -1
- package/src/actions/errorBands/options.js +1 -1
- package/src/actions/errorBars/options.js +1 -1
- package/src/actions/facets/actions.js +1 -1
- package/src/actions/facets/derive.js +2 -2
- package/src/actions/facets/guides.js +1 -1
- package/src/actions/guides/axes/labels.js +1 -1
- package/src/actions/guides/axes/ticks.js +1 -1
- package/src/actions/guides/axes/titles.js +1 -1
- package/src/actions/guides/grids/resolve.js +1 -1
- package/src/actions/guides/legends/categorical/layout.js +4 -2
- package/src/actions/guides/legends/continuous/interval.js +1 -1
- package/src/actions/guides/legends/continuous/opacity.js +1 -1
- package/src/actions/guides/legends/edit.js +2 -20
- package/src/actions/guides/legends/remove.js +2 -20
- package/src/actions/guides/legends/size.js +1 -1
- package/src/actions/guides/legends/target.js +25 -0
- package/src/actions/guides/polar/{axes.js → axes/index.js} +5 -5
- package/src/actions/guides/polar/index.js +1 -1
- package/src/actions/guides/polar/resolve.js +1 -1
- package/src/actions/guides/tickValues.js +1 -1
- package/src/actions/intervals/resolve.js +1 -1
- package/src/actions/marks/arc/actions.js +2 -2
- package/src/actions/marks/area/actions.js +2 -2
- package/src/actions/marks/bar/edit.js +1 -1
- package/src/actions/marks/line/actions.js +3 -3
- package/src/actions/marks/rect/actions.js +1 -1
- package/src/actions/marks/rule/actions.js +1 -1
- package/src/actions/marks/shared.js +1 -1
- package/src/actions/marks/text/actions.js +1 -1
- package/src/actions/primitives/semanticValue.js +1 -1
- package/src/actions/regression/components.js +1 -1
- package/src/actions/regression/create.js +1 -1
- package/src/actions/regression/edit.js +1 -1
- package/src/actions/scales/consumers/common.js +91 -0
- package/src/actions/scales/consumers/families.js +47 -0
- package/src/actions/scales/consumers/index.js +41 -0
- package/src/actions/scales/{consumers.js → consumers/seriesLayout.js} +10 -132
- package/src/actions/scales/create.js +1 -1
- package/src/actions/scales/definitions.js +2 -2
- package/src/actions/scales/edit.js +3 -3
- package/src/actions/scales/materialize.js +2 -2
- package/src/core/ChartProgram.js +22 -236
- package/src/core/compositionState.js +169 -0
- package/src/core/materializationState.js +65 -0
- package/src/core/programState.js +28 -0
- package/src/grammar/arcs.js +1 -1
- package/src/grammar/areaSeries.js +1 -1
- package/src/grammar/bars/aggregate.js +1 -1
- package/src/grammar/density.js +1 -1
- package/src/grammar/{facets.js → facets/index.js} +5 -5
- package/src/grammar/interval.js +2 -96
- package/src/grammar/lineSeries.js +1 -1
- package/src/grammar/palettes.js +5 -7
- package/src/grammar/polarLineCommands.js +1 -1
- package/src/grammar/regression/derive.js +97 -0
- package/src/grammar/regression/index.js +22 -0
- package/src/grammar/regression/models.js +282 -0
- package/src/grammar/regression/parameters.js +106 -0
- package/src/grammar/rules.js +1 -1
- package/src/grammar/scales/color.js +4 -2
- package/src/grammar/statistics/studentT.js +98 -0
- package/src/grammar/transforms.js +1 -1
- package/src/materialization/bars/aggregate.js +1 -1
- package/src/materialization/bars/grouped.js +1 -1
- package/src/materialization/bars/histogram.js +1 -1
- package/src/materialization/bars/ranged.js +1 -1
- package/src/materialization/density.js +1 -1
- package/src/materialization/dependencies.js +6 -87
- package/src/materialization/encodings.js +2 -2
- package/src/materialization/facetGuides/index.js +2 -0
- package/src/materialization/facetGuides/legacyCategorical.js +125 -0
- package/src/materialization/facetGuides/placement.js +126 -0
- package/src/materialization/facetGuides/preparation.js +188 -0
- package/src/materialization/facets.js +1 -1
- package/src/materialization/marks/capabilities.js +141 -0
- package/src/materialization/marks/index.js +105 -0
- package/src/materialization/marks/policies.js +87 -0
- package/src/materialization/rect.js +1 -1
- package/src/materialization/rowEncoding.js +1 -1
- package/src/materialization/scaleGuideDependencies.js +94 -0
- package/src/materialization/scales/map.js +1 -1
- package/src/materialization/scales/policies/series.js +1 -1
- package/src/materialization/scales/resolve.js +1 -1
- package/src/materialization/selection/items/bar.js +1 -1
- package/src/materialization/selection/styles.js +1 -1
- package/src/renderers/canvas/text.js +14 -1
- package/types/extension.d.ts +4 -1
- package/src/actions/encodings/color.js +0 -424
- package/src/grammar/regression.js +0 -578
- package/src/grammar/scales.js +0 -1
- package/src/materialization/facetGuides.js +0 -426
- package/src/materialization/marks.js +0 -316
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import {
|
|
2
|
+
unionConcreteGraphicBounds
|
|
3
|
+
} from "../../grammar/schemas/graphicBounds.js";
|
|
4
|
+
import {
|
|
5
|
+
resolveSharedFacetLegends
|
|
6
|
+
} from "../../grammar/facets/guides.js";
|
|
7
|
+
import { findLayer } from "../../selectors/layers.js";
|
|
8
|
+
import { findSemanticScale } from "../../selectors/scales.js";
|
|
9
|
+
import { allLegendGraphicIds } from "../guides/resources.js";
|
|
10
|
+
import { DEFAULT_COLORS } from "../../theme/defaults.js";
|
|
11
|
+
import { resolveLegacyCategoricalLegend } from "./legacyCategorical.js";
|
|
12
|
+
|
|
13
|
+
const LEGEND_SOURCE_CANVAS = Object.freeze({
|
|
14
|
+
width: 640,
|
|
15
|
+
height: 400,
|
|
16
|
+
margin: Object.freeze({ top: 40, right: 170, bottom: 60, left: 70 })
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
export function legendKinds(program) {
|
|
20
|
+
return Object.keys(program.guideConfigs.legend ?? {});
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function legendCandidates(program) {
|
|
24
|
+
const layers = program.semanticSpec.layers;
|
|
25
|
+
const color = layers.filter(layer => {
|
|
26
|
+
const encoding = layer.encoding?.color;
|
|
27
|
+
return encoding?.scale !== undefined &&
|
|
28
|
+
findSemanticScale(program, encoding.scale) !== undefined;
|
|
29
|
+
});
|
|
30
|
+
if (color.length === 1) {
|
|
31
|
+
const layer = color[0];
|
|
32
|
+
return {
|
|
33
|
+
target: layer.id,
|
|
34
|
+
channels: ["color", "shape", "strokeDash"].filter(
|
|
35
|
+
channel => layer.encoding?.[channel]?.scale !== undefined
|
|
36
|
+
)
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
if (color.length > 1) {
|
|
40
|
+
throw new Error(
|
|
41
|
+
"Shared facet legend requires one unambiguous color legend target."
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
for (const channel of ["shape", "strokeDash", "size", "opacity"]) {
|
|
45
|
+
const candidates = layers.filter(
|
|
46
|
+
layer => layer.encoding?.[channel]?.scale !== undefined
|
|
47
|
+
);
|
|
48
|
+
if (candidates.length === 1) {
|
|
49
|
+
return { target: candidates[0].id, channels: [channel] };
|
|
50
|
+
}
|
|
51
|
+
if (candidates.length > 1) {
|
|
52
|
+
throw new Error(
|
|
53
|
+
`Shared facet legend requires one unambiguous ${channel} legend target.`
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
throw new Error("Shared facet legend requires one eligible encoded scale.");
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function compactNumber(value) {
|
|
61
|
+
if (Math.abs(value) >= 1_000_000_000) {
|
|
62
|
+
return `${+(value / 1_000_000_000).toFixed(1)}B`;
|
|
63
|
+
}
|
|
64
|
+
if (Math.abs(value) >= 1_000_000) {
|
|
65
|
+
return `${Math.round(value / 1_000_000)}M`;
|
|
66
|
+
}
|
|
67
|
+
if (Math.abs(value) >= 1_000) {
|
|
68
|
+
return `${Math.round(value / 1_000)}K`;
|
|
69
|
+
}
|
|
70
|
+
return String(+value.toPrecision(3));
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function continuousValues(domain, count) {
|
|
74
|
+
return Array.from(
|
|
75
|
+
{ length: count },
|
|
76
|
+
(_, index) => domain[0] + index / (count - 1) * (domain[1] - domain[0])
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function prepareAutoLegendSource(child) {
|
|
81
|
+
const request = legendCandidates(child);
|
|
82
|
+
const canvas = child.graphicSpec.objects.canvas;
|
|
83
|
+
let source = child
|
|
84
|
+
._withCanvasConfig({
|
|
85
|
+
margin: LEGEND_SOURCE_CANVAS.margin,
|
|
86
|
+
size: { width: "explicit", height: "explicit" }
|
|
87
|
+
})
|
|
88
|
+
.editGraphics({
|
|
89
|
+
target: "canvas",
|
|
90
|
+
property: "width",
|
|
91
|
+
value: LEGEND_SOURCE_CANVAS.width
|
|
92
|
+
})
|
|
93
|
+
.editGraphics({
|
|
94
|
+
target: "canvas",
|
|
95
|
+
property: "height",
|
|
96
|
+
value: LEGEND_SOURCE_CANVAS.height
|
|
97
|
+
});
|
|
98
|
+
if (canvas?.properties.background !== undefined) {
|
|
99
|
+
source = source.editGraphics({
|
|
100
|
+
target: "canvas",
|
|
101
|
+
property: "background",
|
|
102
|
+
value: canvas.properties.background
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
const scale = findSemanticScale(
|
|
106
|
+
source,
|
|
107
|
+
findLayer(source, request.target)?.encoding?.color?.scale
|
|
108
|
+
);
|
|
109
|
+
const gradient = scale?.type === "sequential";
|
|
110
|
+
source = source.createLegend({
|
|
111
|
+
target: request.target,
|
|
112
|
+
channels: request.channels,
|
|
113
|
+
...(gradient ? {
|
|
114
|
+
count: 5,
|
|
115
|
+
gradient: { length: 180, thickness: 12 },
|
|
116
|
+
labels: { offset: 9, fontSize: 10 },
|
|
117
|
+
titleStyle: { color: DEFAULT_COLORS.strongText, fontSize: 11 }
|
|
118
|
+
} : {})
|
|
119
|
+
});
|
|
120
|
+
if (gradient) {
|
|
121
|
+
const resolved = source.resolvedScales[scale.id];
|
|
122
|
+
source = source
|
|
123
|
+
.editGraphics({
|
|
124
|
+
target: "colorGradientLabels",
|
|
125
|
+
property: "text",
|
|
126
|
+
value: continuousValues(resolved.domain, 5).map(compactNumber)
|
|
127
|
+
})
|
|
128
|
+
.editGraphics({
|
|
129
|
+
target: "colorGradientTitle",
|
|
130
|
+
property: "y",
|
|
131
|
+
value: source.graphicSpec.objects.colorGradientTitle.properties.y + 8
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
return source;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function compatibleLegendChildren(program, source, inferred) {
|
|
138
|
+
return program.compositionSpec.children.map(id => {
|
|
139
|
+
const child = program.children[id];
|
|
140
|
+
return {
|
|
141
|
+
id,
|
|
142
|
+
guideConfigs: inferred ? source.guideConfigs : child.guideConfigs,
|
|
143
|
+
resolvedScales: child.resolvedScales
|
|
144
|
+
};
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export function prepareSharedFacetLegend(program) {
|
|
149
|
+
if (program.compositionSpec.facet.guides.legend !== "shared") {
|
|
150
|
+
return undefined;
|
|
151
|
+
}
|
|
152
|
+
const firstId = program.compositionSpec.children[0];
|
|
153
|
+
const first = program.children[firstId];
|
|
154
|
+
const inferred = legendKinds(first).length === 0;
|
|
155
|
+
if (inferred) {
|
|
156
|
+
const legacy = resolveLegacyCategoricalLegend(program);
|
|
157
|
+
if (legacy !== undefined) return legacy;
|
|
158
|
+
}
|
|
159
|
+
const source = inferred ? prepareAutoLegendSource(first) : first;
|
|
160
|
+
const kinds = legendKinds(source);
|
|
161
|
+
const compatibility = resolveSharedFacetLegends(
|
|
162
|
+
compatibleLegendChildren(program, source, inferred)
|
|
163
|
+
);
|
|
164
|
+
const roots = allLegendGraphicIds(kinds).filter(
|
|
165
|
+
id => source.graphicSpec.objects[id] !== undefined
|
|
166
|
+
);
|
|
167
|
+
if (roots.length === 0) {
|
|
168
|
+
throw new Error("Shared facet legend has no concrete guide graphics.");
|
|
169
|
+
}
|
|
170
|
+
const bounds = unionConcreteGraphicBounds(source.graphicSpec, roots);
|
|
171
|
+
if (bounds === undefined) {
|
|
172
|
+
throw new Error("Shared facet legend has no measurable concrete bounds.");
|
|
173
|
+
}
|
|
174
|
+
const gradient = kinds.includes("gradient");
|
|
175
|
+
return {
|
|
176
|
+
mode: "promoted",
|
|
177
|
+
source,
|
|
178
|
+
inferred,
|
|
179
|
+
kinds,
|
|
180
|
+
roots,
|
|
181
|
+
bounds,
|
|
182
|
+
compatibility,
|
|
183
|
+
reservation: {
|
|
184
|
+
gap: gradient ? 24 : 18,
|
|
185
|
+
width: gradient ? 112 : Math.max(132, Math.ceil(bounds.right - bounds.left))
|
|
186
|
+
}
|
|
187
|
+
};
|
|
188
|
+
}
|
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
} from "../layout/title.js";
|
|
7
7
|
import { resolveFacetLayout } from "../layout/facets.js";
|
|
8
8
|
import { namespaceGraphicSnapshot } from "./compositionSnapshot.js";
|
|
9
|
-
import { prepareSharedFacetLegend } from "./facetGuides.js";
|
|
9
|
+
import { prepareSharedFacetLegend } from "./facetGuides/index.js";
|
|
10
10
|
import {
|
|
11
11
|
attachSnapshotObject,
|
|
12
12
|
clearCompositionChildren,
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { findDataset } from "../../selectors/datasets.js";
|
|
2
|
+
import { findLayer } from "../../selectors/layers.js";
|
|
3
|
+
import { isAggregate } from "../../grammar/aggregate.js";
|
|
4
|
+
import {
|
|
5
|
+
BAR_GRAINS,
|
|
6
|
+
resolveBarColorLayout,
|
|
7
|
+
resolveBarOffsetChannel,
|
|
8
|
+
resolveBarGrain
|
|
9
|
+
} from "../../grammar/bars/policy.js";
|
|
10
|
+
import { resolveRuleMode } from "../../grammar/rules.js";
|
|
11
|
+
import { findUpstreamTransform } from "../dataProvenance.js";
|
|
12
|
+
import { resolveRectMode } from "../../grammar/rects.js";
|
|
13
|
+
|
|
14
|
+
function hasCartesianPositionScales(layer) {
|
|
15
|
+
return (
|
|
16
|
+
layer.encoding?.x?.scale !== undefined &&
|
|
17
|
+
layer.encoding?.y?.scale !== undefined
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function hasPolarPositionScales(layer) {
|
|
22
|
+
return (
|
|
23
|
+
layer.encoding?.theta?.scale !== undefined &&
|
|
24
|
+
layer.encoding?.radius?.scale !== undefined
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function canMaterializePoint(_program, layer) {
|
|
29
|
+
return layer.mark?.type === "point" && (
|
|
30
|
+
hasCartesianPositionScales(layer) || hasPolarPositionScales(layer)
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function canMaterializeLine(program, layer) {
|
|
35
|
+
const dataset = findDataset(program, layer.data);
|
|
36
|
+
const interval = dataset?.transform?.some(item => item.type === "interval");
|
|
37
|
+
if (hasPolarPositionScales(layer)) {
|
|
38
|
+
return (
|
|
39
|
+
layer.mark?.type === "line" &&
|
|
40
|
+
["quantitative", "temporal", "ordinal", "nominal"].includes(
|
|
41
|
+
layer.encoding.theta.fieldType
|
|
42
|
+
) &&
|
|
43
|
+
layer.encoding.radius.fieldType === "quantitative"
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
return (
|
|
47
|
+
layer.mark?.type === "line" &&
|
|
48
|
+
hasCartesianPositionScales(layer) &&
|
|
49
|
+
(isAggregate(layer.encoding.y.aggregate) ||
|
|
50
|
+
(layer.encoding.x.fieldType === "quantitative" &&
|
|
51
|
+
layer.encoding.y.fieldType === "quantitative") ||
|
|
52
|
+
((interval || layer.encoding.y.aggregate === undefined) &&
|
|
53
|
+
["quantitative", "temporal"].includes(layer.encoding.x.fieldType) &&
|
|
54
|
+
["quantitative", "temporal"].includes(layer.encoding.y.fieldType) &&
|
|
55
|
+
layer.encoding.x.fieldType !== layer.encoding.y.fieldType))
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function canMaterializeArea(program, layer) {
|
|
60
|
+
if (layer.mark?.type !== "area" || !hasCartesianPositionScales(layer)) {
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
const dataset = findDataset(program, layer.data);
|
|
64
|
+
const densityTransform = findUpstreamTransform(program, dataset, "density");
|
|
65
|
+
const completeDensity =
|
|
66
|
+
densityTransform !== undefined &&
|
|
67
|
+
(densityTransform.groupBy === undefined ||
|
|
68
|
+
layer.encoding?.group?.field === densityTransform.groupBy);
|
|
69
|
+
return (
|
|
70
|
+
completeDensity ||
|
|
71
|
+
layer.encoding?.y2?.scale === layer.encoding.y.scale ||
|
|
72
|
+
layer.encoding?.x2?.scale === layer.encoding.x.scale
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function canMaterializeArc(_program, layer) {
|
|
77
|
+
if (
|
|
78
|
+
layer.mark?.type !== "arc" ||
|
|
79
|
+
layer.encoding?.theta?.scale === undefined ||
|
|
80
|
+
!["nominal", "ordinal"].includes(layer.encoding.theta.fieldType)
|
|
81
|
+
) {
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
if (layer.encoding.theta.aggregate === "count") {
|
|
85
|
+
return layer.encoding?.radius === undefined;
|
|
86
|
+
}
|
|
87
|
+
return (
|
|
88
|
+
layer.encoding.theta.aggregate === undefined &&
|
|
89
|
+
layer.encoding?.radius?.scale !== undefined &&
|
|
90
|
+
layer.encoding.radius.fieldType === "quantitative"
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function canMaterializeBar(program, layer) {
|
|
95
|
+
if (layer.mark?.type !== "bar" || !hasCartesianPositionScales(layer)) {
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
const grain = resolveBarGrain(layer);
|
|
99
|
+
if (![BAR_GRAINS.histogram, BAR_GRAINS.aggregate, BAR_GRAINS.ranged].includes(grain)) {
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
if (
|
|
103
|
+
grain === BAR_GRAINS.aggregate &&
|
|
104
|
+
resolveBarColorLayout(layer) === "group"
|
|
105
|
+
) {
|
|
106
|
+
const offsetChannel = resolveBarOffsetChannel(layer);
|
|
107
|
+
return (
|
|
108
|
+
layer.encoding?.color?.field !== undefined &&
|
|
109
|
+
layer.encoding?.[offsetChannel]?.field === layer.encoding.color.field &&
|
|
110
|
+
layer.encoding[offsetChannel].scale !== undefined
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
return true;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function canMaterializeRule(program, layer) {
|
|
117
|
+
const fixedSpan = program.markConfigs[layer.id]?.fixedSpan;
|
|
118
|
+
const boxSpanOwner = program.markConfigs[layer.id]?.boxSpanOwner;
|
|
119
|
+
return layer.mark?.type === "rule" && (
|
|
120
|
+
resolveRuleMode(layer) !== undefined ||
|
|
121
|
+
(boxSpanOwner !== undefined && hasCartesianPositionScales(layer)) ||
|
|
122
|
+
(fixedSpan !== undefined && hasCartesianPositionScales(layer))
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function canMaterializeText(program, layer) {
|
|
127
|
+
if (layer.mark?.type !== "text" || layer.encoding?.text === undefined) {
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
130
|
+
if (layer.source !== undefined) {
|
|
131
|
+
const source = findLayer(program, layer.source);
|
|
132
|
+
return source !== undefined &&
|
|
133
|
+
["point", "bar", "rule", "rect"].includes(source.mark?.type) &&
|
|
134
|
+
Array.isArray(program.graphicSpec.objects[source.id]?.items);
|
|
135
|
+
}
|
|
136
|
+
return hasCartesianPositionScales(layer);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export function canMaterializeRect(_program, layer) {
|
|
140
|
+
return resolveRectMode(layer) !== undefined;
|
|
141
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { POSITION_CHANNELS } from "../../core/vocabulary.js";
|
|
2
|
+
import { getMarkMaterializationPolicy } from "./policies.js";
|
|
3
|
+
|
|
4
|
+
export {
|
|
5
|
+
canMaterializeArc,
|
|
6
|
+
canMaterializeArea,
|
|
7
|
+
canMaterializeBar,
|
|
8
|
+
canMaterializeLine,
|
|
9
|
+
canMaterializePoint,
|
|
10
|
+
canMaterializeRect,
|
|
11
|
+
canMaterializeRule,
|
|
12
|
+
canMaterializeText
|
|
13
|
+
} from "./capabilities.js";
|
|
14
|
+
|
|
15
|
+
export function getMarkRematerializationStep(layer) {
|
|
16
|
+
const policy = getMarkMaterializationPolicy(layer);
|
|
17
|
+
return policy === undefined
|
|
18
|
+
? undefined
|
|
19
|
+
: { op: policy.op, args: { id: layer.id } };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function getMarkMaterializationStep(program, layer) {
|
|
23
|
+
const policy = getMarkMaterializationPolicy(layer);
|
|
24
|
+
if (policy === undefined || !policy.canMaterialize(program, layer)) {
|
|
25
|
+
return undefined;
|
|
26
|
+
}
|
|
27
|
+
return getMarkRematerializationStep(layer);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function getSourceDependentMarkSteps(program, sourceId) {
|
|
31
|
+
return program.semanticSpec.layers.flatMap(layer =>
|
|
32
|
+
layer.source === sourceId &&
|
|
33
|
+
getMarkMaterializationPolicy(layer)?.sourceDependent === true
|
|
34
|
+
? [getMarkMaterializationStep(program, layer)].filter(
|
|
35
|
+
step => step !== undefined
|
|
36
|
+
)
|
|
37
|
+
: []
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function getPositionEncodingMaterializationSteps(program, layer, scaleId) {
|
|
42
|
+
const policy = getMarkMaterializationPolicy(layer);
|
|
43
|
+
if (policy === undefined) return [];
|
|
44
|
+
const scale = { op: "rematerializeScale", args: { id: scaleId } };
|
|
45
|
+
const complete = policy.canMaterialize(program, layer);
|
|
46
|
+
const mark = getMarkRematerializationStep(layer);
|
|
47
|
+
if (!complete) {
|
|
48
|
+
if (policy.positionEncoding.incomplete === "scale") return [scale];
|
|
49
|
+
return policy.positionEncoding.scaleFirst ? [scale, mark] : [mark];
|
|
50
|
+
}
|
|
51
|
+
return policy.positionEncoding.scaleFirst ? [scale, mark] : [mark];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function getScaleConsumerMaterializationMode(layer, channel) {
|
|
55
|
+
const policy = getMarkMaterializationPolicy(layer);
|
|
56
|
+
if (policy === undefined) return "direct";
|
|
57
|
+
if (POSITION_CHANNELS.includes(channel)) {
|
|
58
|
+
return policy.scaleApplication.position ?? policy.scaleApplication.default;
|
|
59
|
+
}
|
|
60
|
+
if (policy.scaleApplication.deferredChannels?.includes(channel)) {
|
|
61
|
+
return "defer";
|
|
62
|
+
}
|
|
63
|
+
return policy.scaleApplication.default;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function canDeferScaleConsumerApplication(layer) {
|
|
67
|
+
return getMarkMaterializationPolicy(layer)?.scaleApplication.deferWithMark === true;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function getExistingMarkRematerializationStep(program, layer) {
|
|
71
|
+
const policy = getMarkMaterializationPolicy(layer);
|
|
72
|
+
if (
|
|
73
|
+
policy?.rematerializeIncompleteExisting !== true ||
|
|
74
|
+
program.graphicSpec.objects[layer.id] === undefined
|
|
75
|
+
) {
|
|
76
|
+
return undefined;
|
|
77
|
+
}
|
|
78
|
+
return getMarkRematerializationStep(layer);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function getEncodingMaterializationStages(program, layer, channel, scale) {
|
|
82
|
+
const policy = getMarkMaterializationPolicy(layer);
|
|
83
|
+
if (policy === undefined) return { scales: [], marks: [] };
|
|
84
|
+
const scales = policy.encoding?.scaleFirst === true && scale !== undefined
|
|
85
|
+
? [{
|
|
86
|
+
op: "rematerializeScale",
|
|
87
|
+
args: { id: scale, guides: false, marks: false }
|
|
88
|
+
}]
|
|
89
|
+
: [];
|
|
90
|
+
const shared = policy.encoding?.sharedChannels?.includes(channel) === true &&
|
|
91
|
+
scale !== undefined;
|
|
92
|
+
const candidates = shared
|
|
93
|
+
? program.semanticSpec.layers.filter(candidate =>
|
|
94
|
+
candidate.mark?.type === layer.mark?.type &&
|
|
95
|
+
candidate.encoding?.[channel]?.scale === scale
|
|
96
|
+
)
|
|
97
|
+
: [layer];
|
|
98
|
+
const marks = candidates.flatMap(candidate => {
|
|
99
|
+
const step = policy.encoding?.completeOnly === true
|
|
100
|
+
? getMarkMaterializationStep(program, candidate)
|
|
101
|
+
: getMarkRematerializationStep(candidate);
|
|
102
|
+
return step === undefined ? [] : [step];
|
|
103
|
+
});
|
|
104
|
+
return { scales, marks };
|
|
105
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import {
|
|
2
|
+
canMaterializeArc,
|
|
3
|
+
canMaterializeArea,
|
|
4
|
+
canMaterializeBar,
|
|
5
|
+
canMaterializeLine,
|
|
6
|
+
canMaterializePoint,
|
|
7
|
+
canMaterializeRect,
|
|
8
|
+
canMaterializeRule,
|
|
9
|
+
canMaterializeText
|
|
10
|
+
} from "./capabilities.js";
|
|
11
|
+
|
|
12
|
+
const MARK_MATERIALIZATION_POLICIES = Object.freeze({
|
|
13
|
+
point: Object.freeze({
|
|
14
|
+
canMaterialize: canMaterializePoint,
|
|
15
|
+
op: "rematerializePointMark",
|
|
16
|
+
positionEncoding: Object.freeze({ incomplete: "mark", scaleFirst: true }),
|
|
17
|
+
encoding: Object.freeze({ scaleFirst: true }),
|
|
18
|
+
scaleApplication: Object.freeze({
|
|
19
|
+
deferWithMark: true,
|
|
20
|
+
position: "rematerialize",
|
|
21
|
+
deferredChannels: Object.freeze(["size", "shape"]),
|
|
22
|
+
default: "direct"
|
|
23
|
+
}),
|
|
24
|
+
rematerializeIncompleteExisting: true
|
|
25
|
+
}),
|
|
26
|
+
line: Object.freeze({
|
|
27
|
+
canMaterialize: canMaterializeLine,
|
|
28
|
+
op: "rematerializeLineMark",
|
|
29
|
+
positionEncoding: Object.freeze({ incomplete: "scale", scaleFirst: false }),
|
|
30
|
+
scaleApplication: Object.freeze({ default: "defer" })
|
|
31
|
+
}),
|
|
32
|
+
area: Object.freeze({
|
|
33
|
+
canMaterialize: canMaterializeArea,
|
|
34
|
+
op: "rematerializeAreaMark",
|
|
35
|
+
positionEncoding: Object.freeze({ incomplete: "scale", scaleFirst: true }),
|
|
36
|
+
encoding: Object.freeze({ sharedChannels: Object.freeze(["color"]) }),
|
|
37
|
+
scaleApplication: Object.freeze({ default: "defer" })
|
|
38
|
+
}),
|
|
39
|
+
arc: Object.freeze({
|
|
40
|
+
canMaterialize: canMaterializeArc,
|
|
41
|
+
op: "rematerializeArcMark",
|
|
42
|
+
positionEncoding: Object.freeze({ incomplete: "scale", scaleFirst: false }),
|
|
43
|
+
encoding: Object.freeze({ completeOnly: true }),
|
|
44
|
+
scaleApplication: Object.freeze({ default: "defer" })
|
|
45
|
+
}),
|
|
46
|
+
bar: Object.freeze({
|
|
47
|
+
canMaterialize: canMaterializeBar,
|
|
48
|
+
op: "rematerializeBarMark",
|
|
49
|
+
positionEncoding: Object.freeze({ incomplete: "scale", scaleFirst: false }),
|
|
50
|
+
scaleApplication: Object.freeze({ default: "defer" })
|
|
51
|
+
}),
|
|
52
|
+
rule: Object.freeze({
|
|
53
|
+
canMaterialize: canMaterializeRule,
|
|
54
|
+
op: "rematerializeRuleMark",
|
|
55
|
+
positionEncoding: Object.freeze({ incomplete: "mark", scaleFirst: false }),
|
|
56
|
+
scaleApplication: Object.freeze({ default: "defer" })
|
|
57
|
+
}),
|
|
58
|
+
text: Object.freeze({
|
|
59
|
+
canMaterialize: canMaterializeText,
|
|
60
|
+
op: "rematerializeTextMark",
|
|
61
|
+
positionEncoding: Object.freeze({ incomplete: "mark", scaleFirst: true }),
|
|
62
|
+
encoding: Object.freeze({ scaleFirst: true }),
|
|
63
|
+
scaleApplication: Object.freeze({
|
|
64
|
+
deferWithMark: true,
|
|
65
|
+
position: "rematerialize",
|
|
66
|
+
default: "defer"
|
|
67
|
+
}),
|
|
68
|
+
rematerializeIncompleteExisting: true,
|
|
69
|
+
sourceDependent: true
|
|
70
|
+
}),
|
|
71
|
+
rect: Object.freeze({
|
|
72
|
+
canMaterialize: canMaterializeRect,
|
|
73
|
+
op: "rematerializeRectMark",
|
|
74
|
+
positionEncoding: Object.freeze({ incomplete: "scale", scaleFirst: true }),
|
|
75
|
+
encoding: Object.freeze({ scaleFirst: true }),
|
|
76
|
+
scaleApplication: Object.freeze({
|
|
77
|
+
deferWithMark: true,
|
|
78
|
+
position: "rematerialize",
|
|
79
|
+
default: "defer"
|
|
80
|
+
}),
|
|
81
|
+
rematerializeIncompleteExisting: true
|
|
82
|
+
})
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
export function getMarkMaterializationPolicy(layer) {
|
|
86
|
+
return MARK_MATERIALIZATION_POLICIES[layer.mark?.type];
|
|
87
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
mapOrdinalPositionValues,
|
|
3
3
|
readScaleField
|
|
4
|
-
} from "../grammar/scales.js";
|
|
4
|
+
} from "../grammar/scales/index.js";
|
|
5
5
|
import { RECT_MODES, resolveRectMode } from "../grammar/rects.js";
|
|
6
6
|
import { DEFAULT_RECT_MARK } from "./rectConfig.js";
|
|
7
7
|
import { mapScaleConsumerValues } from "./scales/map.js";
|
|
@@ -5,7 +5,7 @@ import {
|
|
|
5
5
|
readQuantitativeField,
|
|
6
6
|
readScaleField,
|
|
7
7
|
readTemporalField
|
|
8
|
-
} from "../grammar/scales.js";
|
|
8
|
+
} from "../grammar/scales/index.js";
|
|
9
9
|
import { mapScaleConsumerValues } from "./scales/map.js";
|
|
10
10
|
|
|
11
11
|
export function resolveRowEncodingValues(program, layer, dataset, channel) {
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { POSITION_ENCODING_CHANNELS } from "../core/vocabulary.js";
|
|
2
|
+
import { materializedLegendUsesScale } from "./legends.js";
|
|
3
|
+
import { buildMaterializationPlan } from "./planner.js";
|
|
4
|
+
|
|
5
|
+
const CARTESIAN_AXES = Object.freeze([
|
|
6
|
+
Object.freeze({ channel: "x", prefix: "X", lineObject: "xAxisLine" }),
|
|
7
|
+
Object.freeze({ channel: "y", prefix: "Y", lineObject: "yAxisLine" })
|
|
8
|
+
]);
|
|
9
|
+
const POLAR_AXES = Object.freeze([
|
|
10
|
+
Object.freeze({ channel: "theta", prefix: "Theta" }),
|
|
11
|
+
Object.freeze({ channel: "radius", prefix: "Radial" })
|
|
12
|
+
]);
|
|
13
|
+
const GRIDS = Object.freeze([
|
|
14
|
+
Object.freeze({ role: "horizontal", op: "rematerializeHorizontalGrid" }),
|
|
15
|
+
Object.freeze({ role: "vertical", op: "rematerializeVerticalGrid" }),
|
|
16
|
+
Object.freeze({ role: "theta", op: "rematerializeThetaGrid" }),
|
|
17
|
+
Object.freeze({ role: "radial", op: "rematerializeRadialGrid" })
|
|
18
|
+
]);
|
|
19
|
+
|
|
20
|
+
function usesPositionalScale(program, id) {
|
|
21
|
+
return program.semanticSpec.layers.some(layer =>
|
|
22
|
+
POSITION_ENCODING_CHANNELS.some(
|
|
23
|
+
channel => layer.encoding?.[channel]?.scale === id
|
|
24
|
+
)
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function usesRadialScale(program, id) {
|
|
29
|
+
return program.semanticSpec.layers.some(layer =>
|
|
30
|
+
layer.encoding?.radius?.scale === id
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function semanticGuideUsesScale(program, id) {
|
|
35
|
+
return [
|
|
36
|
+
...CARTESIAN_AXES.map(({ channel }) =>
|
|
37
|
+
program.semanticSpec.guides.axis?.[channel]
|
|
38
|
+
),
|
|
39
|
+
...POLAR_AXES.map(({ channel }) =>
|
|
40
|
+
program.semanticSpec.guides.axis?.[channel]
|
|
41
|
+
),
|
|
42
|
+
...GRIDS.map(({ role }) => program.semanticSpec.guides.grid?.[role])
|
|
43
|
+
].some(guide => guide?.scale === id);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function needsCanvasScaleRematerialization(program, scale) {
|
|
47
|
+
return (
|
|
48
|
+
(scale.range === "auto" ||
|
|
49
|
+
usesRadialScale(program, scale.id) ||
|
|
50
|
+
semanticGuideUsesScale(program, scale.id)) &&
|
|
51
|
+
program.resolvedScales[scale.id] !== undefined &&
|
|
52
|
+
usesPositionalScale(program, scale.id)
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function componentEditOperation(prefix, component) {
|
|
57
|
+
const suffix = component[0].toUpperCase() + component.slice(1);
|
|
58
|
+
return `edit${prefix}Axis${suffix}`;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function planScaleGuideRematerialization(program, id) {
|
|
62
|
+
const guides = [];
|
|
63
|
+
const objects = program.graphicSpec?.objects ?? {};
|
|
64
|
+
const guideConfigs = program.guideConfigs ?? {};
|
|
65
|
+
for (const axis of CARTESIAN_AXES) {
|
|
66
|
+
if (
|
|
67
|
+
objects[axis.lineObject] &&
|
|
68
|
+
program.semanticSpec.guides.axis?.[axis.channel]?.scale === id
|
|
69
|
+
) {
|
|
70
|
+
guides.push({ op: componentEditOperation(axis.prefix, "line") });
|
|
71
|
+
}
|
|
72
|
+
for (const component of ["ticks", "labels", "title"]) {
|
|
73
|
+
if (guideConfigs.axis?.[axis.channel]?.[component]?.scale === id) {
|
|
74
|
+
guides.push({ op: componentEditOperation(axis.prefix, component) });
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
for (const axis of POLAR_AXES) {
|
|
79
|
+
for (const component of ["line", "ticks", "labels", "title"]) {
|
|
80
|
+
if (guideConfigs.axis?.[axis.channel]?.[component]?.scale === id) {
|
|
81
|
+
guides.push({ op: componentEditOperation(axis.prefix, component) });
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
for (const grid of GRIDS) {
|
|
86
|
+
if (guideConfigs.grid?.[grid.role]?.scale === id) {
|
|
87
|
+
guides.push({ op: grid.op });
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
if (materializedLegendUsesScale(program, id)) {
|
|
91
|
+
guides.push({ op: "rematerializeLegend" });
|
|
92
|
+
}
|
|
93
|
+
return buildMaterializationPlan({ guides });
|
|
94
|
+
}
|
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
mapDiscretizedColors,
|
|
7
7
|
mapOrdinalValues,
|
|
8
8
|
mapSequentialColors
|
|
9
|
-
} from "../../grammar/scales.js";
|
|
9
|
+
} from "../../grammar/scales/index.js";
|
|
10
10
|
|
|
11
11
|
export function mapScaleConsumerValues(values, resolvedScale, channel) {
|
|
12
12
|
if (channel === "color" && isDiscretizedColorScaleType(resolvedScale.type)) {
|
|
@@ -24,7 +24,7 @@ import {
|
|
|
24
24
|
validateOrdinalScaleType,
|
|
25
25
|
validateScaleTypeForRole,
|
|
26
26
|
validateTimeScaleType
|
|
27
|
-
} from "../../grammar/scales.js";
|
|
27
|
+
} from "../../grammar/scales/index.js";
|
|
28
28
|
import { resolveArcAutoPositionRange } from "./policies/arc.js";
|
|
29
29
|
import {
|
|
30
30
|
resolveBinnedBarDomain,
|
|
@@ -10,7 +10,7 @@ import { layoutSeriesPartition } from "../../../grammar/seriesLayout.js";
|
|
|
10
10
|
import {
|
|
11
11
|
readNominalField,
|
|
12
12
|
readTemporalField
|
|
13
|
-
} from "../../../grammar/scales.js";
|
|
13
|
+
} from "../../../grammar/scales/index.js";
|
|
14
14
|
import { findSemanticScale } from "../../../selectors/scales.js";
|
|
15
15
|
import { deriveHistogramSegments } from "../../bars/histogram.js";
|
|
16
16
|
import {
|