ggaction 0.0.2 → 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 +55 -2
- package/README.md +95 -72
- package/package.json +15 -5
- package/src/actions/boxPlots/components.js +3 -3
- package/src/actions/boxPlots/edit.js +277 -0
- package/src/actions/boxPlots/index.js +2 -0
- package/src/actions/boxPlots/materialize.js +12 -1
- package/src/actions/boxPlots/options.js +25 -14
- package/src/actions/canvas/actions.js +21 -17
- package/src/actions/composition/actions.js +247 -0
- package/src/actions/composition/index.js +1 -0
- package/src/actions/coordinates/actions.js +2 -5
- package/src/actions/data/density.js +7 -1
- package/src/actions/data/filter.js +3 -1
- package/src/actions/data/regression.js +1 -1
- package/src/actions/encodings/appearance.js +12 -1
- package/src/actions/encodings/barWidth.js +5 -3
- 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 +4 -2
- package/src/actions/encodings/offset.js +91 -73
- package/src/actions/encodings/position/apply.js +66 -0
- package/src/actions/encodings/position/index.js +130 -0
- package/src/actions/encodings/position/policies/arc.js +26 -0
- package/src/actions/encodings/position/policies/bar.js +9 -5
- package/src/actions/encodings/position/policies/index.js +5 -1
- package/src/actions/encodings/position/policies/line.js +20 -3
- package/src/actions/encodings/position/resolve.js +58 -9
- package/src/actions/encodings/ranged.js +30 -8
- package/src/actions/encodings/shared.js +17 -21
- package/src/actions/encodings/strokeDash.js +1 -1
- package/src/actions/encodings/text.js +73 -0
- package/src/actions/errorBands/create.js +37 -29
- package/src/actions/errorBands/edit.js +206 -0
- package/src/actions/errorBands/index.js +9 -0
- package/src/actions/errorBands/options.js +39 -0
- package/src/actions/errorBars/create.js +50 -43
- package/src/actions/errorBars/edit.js +130 -0
- package/src/actions/errorBars/index.js +3 -0
- package/src/actions/errorBars/options.js +52 -0
- package/src/actions/facets/actions.js +162 -0
- package/src/actions/facets/derive.js +200 -0
- package/src/actions/facets/guides.js +16 -0
- package/src/actions/facets/index.js +1 -0
- package/src/actions/facets/replay.js +69 -0
- package/src/actions/guides/applicability.js +113 -0
- package/src/actions/guides/axes/axes.js +70 -16
- package/src/actions/guides/axes/edit.js +126 -0
- package/src/actions/guides/axes/index.js +12 -0
- package/src/actions/guides/axes/labels.js +22 -19
- package/src/actions/guides/axes/remove.js +60 -0
- package/src/actions/guides/axes/tickGroups.js +7 -5
- package/src/actions/guides/axes/ticks.js +10 -5
- package/src/actions/guides/axes/titles.js +8 -6
- package/src/actions/guides/grids/grid.js +144 -15
- package/src/actions/guides/grids/resolve.js +6 -14
- package/src/actions/guides/guides.js +17 -75
- package/src/actions/guides/index.js +6 -0
- package/src/actions/guides/legends/categorical/actions.js +48 -9
- package/src/actions/guides/legends/categorical/components.js +16 -16
- package/src/actions/guides/legends/categorical/index.js +14 -14
- package/src/actions/guides/legends/categorical/layout.js +7 -3
- package/src/actions/guides/legends/categorical/options.js +2 -5
- package/src/actions/guides/legends/categorical/resolve.js +2 -2
- package/src/actions/guides/legends/categorical/symbols.js +14 -14
- package/src/actions/guides/legends/continuous/common.js +7 -7
- package/src/actions/guides/legends/continuous/interval.js +6 -6
- package/src/actions/guides/legends/continuous/opacity.js +1 -1
- package/src/actions/guides/legends/edit.js +4 -26
- package/src/actions/guides/legends/focused.js +63 -0
- package/src/actions/guides/legends/index.js +4 -0
- package/src/actions/guides/legends/remove.js +46 -0
- package/src/actions/guides/legends/size.js +13 -15
- package/src/actions/guides/legends/target.js +25 -0
- package/src/actions/guides/polar/axes/facade.js +219 -0
- package/src/actions/guides/polar/axes/index.js +55 -0
- package/src/actions/guides/polar/axes/labels.js +188 -0
- package/src/actions/guides/polar/axes/lines.js +113 -0
- package/src/actions/guides/polar/axes/shared.js +112 -0
- package/src/actions/guides/polar/axes/ticks.js +164 -0
- package/src/actions/guides/polar/axes/titles.js +165 -0
- package/src/actions/guides/polar/grids.js +252 -0
- package/src/actions/guides/polar/index.js +2 -0
- package/src/actions/guides/polar/resolve.js +269 -0
- package/src/actions/guides/tickValues.js +1 -1
- package/src/actions/index.js +4 -0
- package/src/actions/intervals/resolve.js +1 -1
- package/src/actions/marks/arc/actions.js +239 -0
- package/src/actions/marks/arc/index.js +1 -0
- package/src/actions/marks/{area.js → area/actions.js} +51 -38
- package/src/actions/marks/area/index.js +4 -0
- package/src/actions/marks/bar/create.js +26 -5
- package/src/actions/marks/bar/edit.js +1 -1
- package/src/actions/marks/bar/materialize.js +25 -31
- package/src/actions/marks/index.js +12 -4
- package/src/actions/marks/lifecycle.js +27 -0
- package/src/actions/marks/line/actions.js +373 -0
- package/src/actions/marks/line/index.js +1 -0
- package/src/actions/marks/{point.js → point/actions.js} +67 -108
- package/src/actions/marks/point/index.js +1 -0
- package/src/actions/marks/rect/actions.js +165 -0
- package/src/actions/marks/rect/index.js +1 -0
- package/src/actions/marks/remove.js +214 -0
- package/src/actions/marks/{rule.js → rule/actions.js} +34 -28
- package/src/actions/marks/rule/index.js +1 -0
- package/src/actions/marks/shared.js +156 -25
- package/src/actions/marks/text/actions.js +192 -0
- package/src/actions/marks/text/index.js +1 -0
- package/src/actions/primitives/createGraphics.js +13 -6
- package/src/actions/primitives/editGraphics.js +10 -3
- package/src/actions/primitives/semantic.js +26 -3
- package/src/actions/primitives/semanticValue.js +26 -4
- package/src/actions/regression/components.js +2 -2
- package/src/actions/regression/create.js +19 -2
- package/src/actions/regression/edit.js +247 -0
- package/src/actions/regression/index.js +3 -0
- package/src/actions/regression/resolve.js +3 -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} +14 -127
- package/src/actions/scales/create.js +1 -1
- package/src/actions/scales/definitions.js +15 -6
- package/src/actions/scales/edit.js +45 -22
- package/src/actions/scales/materialize.js +58 -409
- package/src/actions/selection/actions.js +38 -24
- package/src/actions/selection/index.js +2 -0
- package/src/actions/titles/actions.js +63 -2
- package/src/actions/titles/index.js +2 -0
- package/src/actions/titles/resolve.js +12 -17
- package/src/composition.js +10 -0
- package/src/core/ChartProgram.js +49 -65
- package/src/core/action.js +10 -0
- package/src/core/compositionState.js +169 -0
- package/src/core/materializationState.js +65 -0
- package/src/core/programState.js +28 -0
- package/src/core/validation.js +16 -0
- package/src/core/vocabulary.js +154 -4
- package/src/grammar/arcs.js +154 -0
- package/src/grammar/areaSeries.js +1 -1
- package/src/grammar/bars/aggregate.js +3 -3
- package/src/grammar/bars/geometry.js +3 -3
- package/src/grammar/bars/policy.js +7 -1
- package/src/grammar/coordinates.js +3 -7
- package/src/grammar/density.js +21 -2
- package/src/grammar/facets/dependencies.js +185 -0
- package/src/grammar/facets/guides.js +257 -0
- package/src/grammar/facets/index.js +122 -0
- package/src/grammar/facets/scales.js +254 -0
- package/src/grammar/interval.js +2 -96
- package/src/grammar/lineSeries.js +112 -4
- package/src/grammar/palettes.js +5 -7
- package/src/grammar/polar.js +102 -0
- package/src/grammar/polarGuides.js +243 -0
- package/src/grammar/polarLineCommands.js +43 -0
- package/src/grammar/polarPaths.js +161 -0
- package/src/grammar/positionCompatibility.js +18 -2
- package/src/grammar/rects.js +26 -0
- 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/scales/continuous.js +6 -1
- package/src/grammar/scales/fields.js +7 -0
- package/src/grammar/scales/ordinal.js +5 -2
- package/src/grammar/scales/policies.js +1 -1
- package/src/grammar/schemas/graphic.js +1 -1
- package/src/grammar/schemas/graphicBounds.js +64 -9
- package/src/grammar/schemas/graphicTree.js +44 -10
- package/src/grammar/schemas/semanticPath.js +31 -9
- package/src/grammar/seriesLayout.js +13 -2
- package/src/grammar/statistics/studentT.js +98 -0
- package/src/grammar/text.js +71 -0
- package/src/grammar/ticks.js +50 -13
- package/src/grammar/transforms.js +1 -1
- package/src/index.js +1 -0
- package/src/layout/composition.js +310 -0
- package/src/layout/facets.js +131 -0
- package/src/layout/title.js +6 -0
- package/src/materialization/bars/aggregate.js +6 -3
- package/src/materialization/bars/grouped.js +75 -37
- package/src/materialization/bars/histogram.js +1 -1
- package/src/materialization/bars/ranged.js +1 -1
- package/src/materialization/composition.js +152 -0
- package/src/materialization/compositionSnapshot.js +114 -0
- package/src/materialization/density.js +1 -1
- package/src/materialization/dependencies.js +47 -73
- package/src/materialization/encodings.js +14 -28
- 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 +214 -0
- package/src/materialization/graphicHierarchy.js +15 -5
- package/src/materialization/guides/resources.js +46 -0
- 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/planner.js +105 -12
- package/src/materialization/rect.js +138 -0
- package/src/materialization/rectConfig.js +38 -0
- package/src/materialization/rowEncoding.js +33 -0
- package/src/materialization/scaleGuideDependencies.js +94 -0
- package/src/materialization/scales/map.js +40 -0
- package/src/materialization/scales/policies/arc.js +37 -0
- package/src/materialization/scales/policies/bar.js +132 -0
- package/src/materialization/scales/policies/series.js +76 -0
- package/src/materialization/scales/resolve.js +326 -0
- package/src/materialization/selection/items/arc.js +29 -0
- package/src/materialization/selection/items/bar.js +255 -0
- package/src/materialization/selection/items/common.js +131 -0
- package/src/materialization/selection/items/index.js +6 -0
- package/src/materialization/selection/items/path.js +59 -0
- package/src/materialization/selection/items/point.js +55 -0
- package/src/materialization/selection/items/rect.js +23 -0
- package/src/materialization/selection/items/rule.js +25 -0
- package/src/materialization/selection/policies/arc.js +10 -0
- package/src/materialization/selection/policies/area.js +1 -1
- package/src/materialization/selection/policies/bar.js +1 -1
- package/src/materialization/selection/policies/index.js +5 -1
- package/src/materialization/selection/policies/line.js +1 -1
- package/src/materialization/selection/policies/point.js +1 -1
- package/src/materialization/selection/policies/rect.js +10 -0
- package/src/materialization/selection/policies/rule.js +1 -1
- package/src/materialization/selection/styles.js +25 -17
- package/src/materialization/text.js +156 -0
- package/src/renderers/canvas/index.js +68 -22
- package/src/renderers/canvas/text.js +14 -1
- package/types/extension.d.ts +5 -1
- package/types/index.d.ts +56 -1
- package/types/program.d.ts +664 -27
- package/src/actions/encodings/color.js +0 -329
- package/src/actions/encodings/position.js +0 -176
- package/src/actions/marks/line.js +0 -248
- package/src/grammar/regression.js +0 -578
- package/src/grammar/scales.js +0 -1
- package/src/materialization/marks.js +0 -110
- package/src/materialization/selection/items.js +0 -503
package/src/core/validation.js
CHANGED
|
@@ -8,6 +8,22 @@ export function validateKeys(value, supported, label) {
|
|
|
8
8
|
}
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
+
export function validateOptionObject(value, supported, label, {
|
|
12
|
+
allowEmpty = true,
|
|
13
|
+
plainObjectMessage = `${label} options must be a plain object.`,
|
|
14
|
+
emptyMessage = `${label} requires at least one option.`,
|
|
15
|
+
emptyError = TypeError
|
|
16
|
+
} = {}) {
|
|
17
|
+
if (!isPlainObject(value)) {
|
|
18
|
+
throw new TypeError(plainObjectMessage);
|
|
19
|
+
}
|
|
20
|
+
if (supported !== undefined) validateKeys(value, supported, label);
|
|
21
|
+
if (!allowEmpty && Object.keys(value).length === 0) {
|
|
22
|
+
throw new emptyError(emptyMessage);
|
|
23
|
+
}
|
|
24
|
+
return value;
|
|
25
|
+
}
|
|
26
|
+
|
|
11
27
|
export function noOptions(args, operation) {
|
|
12
28
|
if (!isPlainObject(args) || Object.keys(args).length > 0) {
|
|
13
29
|
throw new Error(`${operation} does not accept options.`);
|
package/src/core/vocabulary.js
CHANGED
|
@@ -3,15 +3,30 @@ export const MARK_TYPES = Object.freeze([
|
|
|
3
3
|
"line",
|
|
4
4
|
"bar",
|
|
5
5
|
"area",
|
|
6
|
-
"
|
|
6
|
+
"arc",
|
|
7
|
+
"rule",
|
|
8
|
+
"text",
|
|
9
|
+
"rect"
|
|
7
10
|
]);
|
|
8
11
|
|
|
12
|
+
export const MARK_GRAPHIC_TYPES = Object.freeze({
|
|
13
|
+
point: Object.freeze(["circle", "rect", "path", "collection"]),
|
|
14
|
+
line: Object.freeze(["path"]),
|
|
15
|
+
bar: Object.freeze(["rect"]),
|
|
16
|
+
area: Object.freeze(["path"]),
|
|
17
|
+
arc: Object.freeze(["path"]),
|
|
18
|
+
rule: Object.freeze(["line"]),
|
|
19
|
+
text: Object.freeze(["text"]),
|
|
20
|
+
rect: Object.freeze(["rect"])
|
|
21
|
+
});
|
|
22
|
+
|
|
9
23
|
export const ENCODING_CHANNELS = Object.freeze([
|
|
10
24
|
"x",
|
|
11
25
|
"y",
|
|
12
26
|
"x2",
|
|
13
27
|
"y2",
|
|
14
28
|
"xOffset",
|
|
29
|
+
"yOffset",
|
|
15
30
|
"theta",
|
|
16
31
|
"radius",
|
|
17
32
|
"color",
|
|
@@ -19,14 +34,149 @@ export const ENCODING_CHANNELS = Object.freeze([
|
|
|
19
34
|
"size",
|
|
20
35
|
"shape",
|
|
21
36
|
"group",
|
|
22
|
-
"opacity"
|
|
37
|
+
"opacity",
|
|
38
|
+
"text"
|
|
39
|
+
]);
|
|
40
|
+
|
|
41
|
+
const CARTESIAN_MARK_TYPES = Object.freeze([
|
|
42
|
+
"point", "line", "bar", "area", "rule", "text", "rect"
|
|
23
43
|
]);
|
|
44
|
+
const POLAR_MARK_TYPES = Object.freeze(["point", "line", "arc"]);
|
|
45
|
+
|
|
46
|
+
export const POSITION_CHANNEL_DEFINITIONS = Object.freeze({
|
|
47
|
+
x: Object.freeze({
|
|
48
|
+
family: "cartesian",
|
|
49
|
+
role: "primary",
|
|
50
|
+
scaleChannel: "x",
|
|
51
|
+
coordinate: Object.freeze({ id: "main", type: "cartesian" }),
|
|
52
|
+
guideChannel: "x",
|
|
53
|
+
gridDirection: "vertical",
|
|
54
|
+
markTypes: CARTESIAN_MARK_TYPES
|
|
55
|
+
}),
|
|
56
|
+
y: Object.freeze({
|
|
57
|
+
family: "cartesian",
|
|
58
|
+
role: "primary",
|
|
59
|
+
scaleChannel: "y",
|
|
60
|
+
coordinate: Object.freeze({ id: "main", type: "cartesian" }),
|
|
61
|
+
guideChannel: "y",
|
|
62
|
+
gridDirection: "horizontal",
|
|
63
|
+
markTypes: CARTESIAN_MARK_TYPES
|
|
64
|
+
}),
|
|
65
|
+
x2: Object.freeze({
|
|
66
|
+
family: "cartesian",
|
|
67
|
+
role: "secondary",
|
|
68
|
+
scaleChannel: "x",
|
|
69
|
+
coordinate: Object.freeze({ id: "main", type: "cartesian" }),
|
|
70
|
+
markTypes: Object.freeze(["area", "rule", "rect"])
|
|
71
|
+
}),
|
|
72
|
+
y2: Object.freeze({
|
|
73
|
+
family: "cartesian",
|
|
74
|
+
role: "secondary",
|
|
75
|
+
scaleChannel: "y",
|
|
76
|
+
coordinate: Object.freeze({ id: "main", type: "cartesian" }),
|
|
77
|
+
markTypes: Object.freeze(["area", "rule", "rect"])
|
|
78
|
+
}),
|
|
79
|
+
xOffset: Object.freeze({
|
|
80
|
+
family: "cartesian",
|
|
81
|
+
role: "offset",
|
|
82
|
+
scaleChannel: "xOffset",
|
|
83
|
+
coordinate: Object.freeze({ id: "main", type: "cartesian" }),
|
|
84
|
+
markTypes: Object.freeze(["bar"])
|
|
85
|
+
}),
|
|
86
|
+
yOffset: Object.freeze({
|
|
87
|
+
family: "cartesian",
|
|
88
|
+
role: "offset",
|
|
89
|
+
scaleChannel: "yOffset",
|
|
90
|
+
coordinate: Object.freeze({ id: "main", type: "cartesian" }),
|
|
91
|
+
markTypes: Object.freeze(["bar"])
|
|
92
|
+
}),
|
|
93
|
+
theta: Object.freeze({
|
|
94
|
+
family: "polar",
|
|
95
|
+
role: "primary",
|
|
96
|
+
scaleChannel: "theta",
|
|
97
|
+
coordinate: Object.freeze({ id: "polar", type: "polar" }),
|
|
98
|
+
guideChannel: "theta",
|
|
99
|
+
gridDirection: "theta",
|
|
100
|
+
markTypes: POLAR_MARK_TYPES
|
|
101
|
+
}),
|
|
102
|
+
radius: Object.freeze({
|
|
103
|
+
family: "polar",
|
|
104
|
+
role: "primary",
|
|
105
|
+
scaleChannel: "radius",
|
|
106
|
+
coordinate: Object.freeze({ id: "polar", type: "polar" }),
|
|
107
|
+
guideChannel: "radius",
|
|
108
|
+
gridDirection: "radial",
|
|
109
|
+
markTypes: POLAR_MARK_TYPES
|
|
110
|
+
})
|
|
111
|
+
});
|
|
24
112
|
|
|
25
113
|
export const SCALED_ENCODING_CHANNELS = Object.freeze(
|
|
26
|
-
ENCODING_CHANNELS.filter(channel =>
|
|
114
|
+
ENCODING_CHANNELS.filter(channel => !["group", "text"].includes(channel))
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
export const POSITION_ENCODING_CHANNELS = Object.freeze(
|
|
118
|
+
Object.keys(POSITION_CHANNEL_DEFINITIONS)
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
export const OFFSET_POSITION_CHANNELS = Object.freeze(
|
|
122
|
+
POSITION_ENCODING_CHANNELS.filter(channel =>
|
|
123
|
+
POSITION_CHANNEL_DEFINITIONS[channel].role === "offset"
|
|
124
|
+
)
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
export const CARTESIAN_POSITION_CHANNELS = Object.freeze(
|
|
128
|
+
POSITION_ENCODING_CHANNELS.filter(channel =>
|
|
129
|
+
POSITION_CHANNEL_DEFINITIONS[channel].family === "cartesian" &&
|
|
130
|
+
POSITION_CHANNEL_DEFINITIONS[channel].role === "primary"
|
|
131
|
+
)
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
export const POLAR_POSITION_CHANNELS = Object.freeze(
|
|
135
|
+
POSITION_ENCODING_CHANNELS.filter(channel =>
|
|
136
|
+
POSITION_CHANNEL_DEFINITIONS[channel].family === "polar" &&
|
|
137
|
+
POSITION_CHANNEL_DEFINITIONS[channel].role === "primary"
|
|
138
|
+
)
|
|
27
139
|
);
|
|
28
140
|
|
|
29
|
-
export const POSITION_CHANNELS = Object.freeze([
|
|
141
|
+
export const POSITION_CHANNELS = Object.freeze([
|
|
142
|
+
...CARTESIAN_POSITION_CHANNELS,
|
|
143
|
+
...POLAR_POSITION_CHANNELS
|
|
144
|
+
]);
|
|
145
|
+
|
|
146
|
+
export const FACET_SCALE_CHANNELS = Object.freeze([
|
|
147
|
+
"x",
|
|
148
|
+
"y",
|
|
149
|
+
"xOffset",
|
|
150
|
+
"yOffset",
|
|
151
|
+
"color",
|
|
152
|
+
"size",
|
|
153
|
+
"shape",
|
|
154
|
+
"opacity",
|
|
155
|
+
"strokeDash"
|
|
156
|
+
]);
|
|
157
|
+
|
|
158
|
+
export const FACET_SCALE_RESOLUTIONS = Object.freeze([
|
|
159
|
+
"shared",
|
|
160
|
+
"independent"
|
|
161
|
+
]);
|
|
162
|
+
|
|
163
|
+
export function getPositionChannelDefinition(channel) {
|
|
164
|
+
return POSITION_CHANNEL_DEFINITIONS[channel];
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export function normalizePositionScaleChannel(channel) {
|
|
168
|
+
return POSITION_CHANNEL_DEFINITIONS[channel]?.scaleChannel ?? channel;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export function positionChannelsForFamily(family) {
|
|
172
|
+
return POSITION_CHANNELS.filter(channel =>
|
|
173
|
+
POSITION_CHANNEL_DEFINITIONS[channel].family === family
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export function getMarkGraphicTypes(markType) {
|
|
178
|
+
return MARK_GRAPHIC_TYPES[markType];
|
|
179
|
+
}
|
|
30
180
|
|
|
31
181
|
export const COLOR_LAYOUTS = Object.freeze([
|
|
32
182
|
"stack",
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { cloneAndFreeze } from "../core/immutable.js";
|
|
2
|
+
import {
|
|
3
|
+
mapContinuousScaleValues,
|
|
4
|
+
mapOrdinalPositionValues,
|
|
5
|
+
readNominalField,
|
|
6
|
+
readQuantitativeField
|
|
7
|
+
} from "./scales/index.js";
|
|
8
|
+
|
|
9
|
+
function requireArcLayer(layer) {
|
|
10
|
+
if (layer?.mark?.type !== "arc") {
|
|
11
|
+
throw new Error("Arc derivation requires a semantic arc mark.");
|
|
12
|
+
}
|
|
13
|
+
const theta = layer.encoding?.theta;
|
|
14
|
+
if (theta === undefined || !["nominal", "ordinal"].includes(theta.fieldType)) {
|
|
15
|
+
throw new Error(`Arc mark "${layer.id}" requires categorical theta.`);
|
|
16
|
+
}
|
|
17
|
+
return theta;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function requireBandScale(scale, label) {
|
|
21
|
+
if (
|
|
22
|
+
scale?.type !== "band" ||
|
|
23
|
+
!Array.isArray(scale.domain) ||
|
|
24
|
+
!Array.isArray(scale.range) ||
|
|
25
|
+
!Number.isFinite(scale.bandwidth) ||
|
|
26
|
+
scale.bandwidth <= 0
|
|
27
|
+
) {
|
|
28
|
+
throw new Error(`${label} requires a resolved band scale.`);
|
|
29
|
+
}
|
|
30
|
+
return scale;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function colorValues(rows, encoding) {
|
|
34
|
+
return encoding === undefined
|
|
35
|
+
? rows.map(() => undefined)
|
|
36
|
+
: readNominalField(rows, encoding.field);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function countSectors(rows, layer, thetaScale, frame, innerRadiusRatio) {
|
|
40
|
+
const theta = layer.encoding.theta;
|
|
41
|
+
if (theta.aggregate !== "count") {
|
|
42
|
+
throw new Error(`Arc mark "${layer.id}" requires count or radial layout.`);
|
|
43
|
+
}
|
|
44
|
+
const values = readNominalField(rows, theta.field);
|
|
45
|
+
const colors = colorValues(rows, layer.encoding?.color);
|
|
46
|
+
const groups = new Map(thetaScale.domain.map(value => [value, []]));
|
|
47
|
+
for (let index = 0; index < rows.length; index += 1) {
|
|
48
|
+
const group = groups.get(values[index]);
|
|
49
|
+
if (group === undefined) {
|
|
50
|
+
throw new Error(`Arc theta value "${values[index]}" is outside the scale domain.`);
|
|
51
|
+
}
|
|
52
|
+
group.push({ row: rows[index], color: colors[index], sourceIndex: index });
|
|
53
|
+
}
|
|
54
|
+
const total = [...groups.values()].reduce((sum, group) => sum + group.length, 0);
|
|
55
|
+
if (total === 0) throw new Error(`Arc mark "${layer.id}" has no rows to count.`);
|
|
56
|
+
const start = thetaScale.range[0];
|
|
57
|
+
const span = thetaScale.range[1] - thetaScale.range[0];
|
|
58
|
+
const innerRadius = frame.availableRadius * innerRadiusRatio;
|
|
59
|
+
let cursor = start;
|
|
60
|
+
const sectors = [];
|
|
61
|
+
for (const value of thetaScale.domain) {
|
|
62
|
+
const group = groups.get(value);
|
|
63
|
+
if (group.length === 0) continue;
|
|
64
|
+
const endTheta = cursor + group.length / total * span;
|
|
65
|
+
const distinctColors = [...new Set(group.map(item => item.color))];
|
|
66
|
+
if (distinctColors.length > 1) {
|
|
67
|
+
throw new Error(
|
|
68
|
+
`Count arc theta group "${value}" must resolve to one color value.`
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
sectors.push({
|
|
72
|
+
key: value,
|
|
73
|
+
theta: value,
|
|
74
|
+
count: group.length,
|
|
75
|
+
color: distinctColors[0],
|
|
76
|
+
startTheta: cursor,
|
|
77
|
+
endTheta,
|
|
78
|
+
innerRadius,
|
|
79
|
+
outerRadius: frame.availableRadius,
|
|
80
|
+
sourceIndices: group.map(item => item.sourceIndex)
|
|
81
|
+
});
|
|
82
|
+
cursor = endTheta;
|
|
83
|
+
}
|
|
84
|
+
return sectors;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function radialSectors(rows, layer, thetaScale, radiusScale) {
|
|
88
|
+
const theta = layer.encoding.theta;
|
|
89
|
+
const radius = layer.encoding?.radius;
|
|
90
|
+
if (radius?.fieldType !== "quantitative") {
|
|
91
|
+
throw new Error(`Arc mark "${layer.id}" requires quantitative radius.`);
|
|
92
|
+
}
|
|
93
|
+
const thetaValues = readNominalField(rows, theta.field);
|
|
94
|
+
const radiusValues = readQuantitativeField(rows, radius.field);
|
|
95
|
+
const colors = colorValues(rows, layer.encoding?.color);
|
|
96
|
+
const centers = mapOrdinalPositionValues(thetaValues, thetaScale);
|
|
97
|
+
const outerRadii = mapContinuousScaleValues(radiusValues, radiusScale);
|
|
98
|
+
const innerRadius = Math.min(...radiusScale.range);
|
|
99
|
+
const direction = Math.sign(thetaScale.step) || 1;
|
|
100
|
+
const halfBand = direction * thetaScale.bandwidth / 2;
|
|
101
|
+
const grouped = new Map(thetaScale.domain.map(value => [value, []]));
|
|
102
|
+
for (let index = 0; index < rows.length; index += 1) {
|
|
103
|
+
if (outerRadii[index] <= innerRadius) continue;
|
|
104
|
+
const group = grouped.get(thetaValues[index]);
|
|
105
|
+
if (group === undefined) {
|
|
106
|
+
throw new Error(
|
|
107
|
+
`Arc theta value "${thetaValues[index]}" is outside the scale domain.`
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
group.push({
|
|
111
|
+
key: `${String(thetaValues[index])}:${index}`,
|
|
112
|
+
theta: thetaValues[index],
|
|
113
|
+
radius: radiusValues[index],
|
|
114
|
+
color: colors[index],
|
|
115
|
+
startTheta: centers[index] - halfBand,
|
|
116
|
+
endTheta: centers[index] + halfBand,
|
|
117
|
+
innerRadius,
|
|
118
|
+
outerRadius: outerRadii[index],
|
|
119
|
+
sourceIndices: [index]
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
const sectors = [];
|
|
123
|
+
for (const value of thetaScale.domain) {
|
|
124
|
+
const group = grouped.get(value);
|
|
125
|
+
group.sort((left, right) =>
|
|
126
|
+
right.outerRadius - left.outerRadius ||
|
|
127
|
+
left.sourceIndices[0] - right.sourceIndices[0]
|
|
128
|
+
);
|
|
129
|
+
sectors.push(...group);
|
|
130
|
+
}
|
|
131
|
+
return sectors;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function deriveArcSectors(rows, layer, {
|
|
135
|
+
thetaScale,
|
|
136
|
+
radiusScale,
|
|
137
|
+
frame,
|
|
138
|
+
innerRadiusRatio = 0
|
|
139
|
+
} = {}) {
|
|
140
|
+
if (!Array.isArray(rows)) throw new TypeError("Arc derivation requires rows.");
|
|
141
|
+
const theta = requireArcLayer(layer);
|
|
142
|
+
requireBandScale(thetaScale, `Arc mark "${layer.id}" theta`);
|
|
143
|
+
if (
|
|
144
|
+
!Number.isFinite(innerRadiusRatio) ||
|
|
145
|
+
innerRadiusRatio < 0 ||
|
|
146
|
+
innerRadiusRatio >= 1
|
|
147
|
+
) {
|
|
148
|
+
throw new RangeError("Arc innerRadius must be from 0 (inclusive) to 1 (exclusive).");
|
|
149
|
+
}
|
|
150
|
+
const sectors = theta.aggregate === "count"
|
|
151
|
+
? countSectors(rows, layer, thetaScale, frame, innerRadiusRatio)
|
|
152
|
+
: radialSectors(rows, layer, thetaScale, radiusScale);
|
|
153
|
+
return cloneAndFreeze({ sectors });
|
|
154
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { cloneAndFreeze } from "../../core/immutable.js";
|
|
2
|
-
import { readNominalField, readTemporalField } from "../scales.js";
|
|
2
|
+
import { readNominalField, readTemporalField } from "../scales/index.js";
|
|
3
3
|
import {
|
|
4
4
|
aggregateRows,
|
|
5
5
|
validateAggregateFieldType,
|
|
@@ -40,7 +40,7 @@ export function deriveBarAggregates(rows, layer) {
|
|
|
40
40
|
let colorAggregate;
|
|
41
41
|
|
|
42
42
|
if (color !== undefined) {
|
|
43
|
-
if (color.fieldType
|
|
43
|
+
if (["nominal", "ordinal"].includes(color.fieldType)) {
|
|
44
44
|
colorValues = readNominalField(rows, color.field);
|
|
45
45
|
} else if (color.fieldType === "quantitative") {
|
|
46
46
|
colorAggregate = color.aggregate;
|
|
@@ -48,7 +48,7 @@ export function deriveBarAggregates(rows, layer) {
|
|
|
48
48
|
validateAggregateFieldValues(rows, color.field, color.fieldType);
|
|
49
49
|
} else {
|
|
50
50
|
throw new Error(
|
|
51
|
-
`Bar color encoding on mark "${layer.id}" must be
|
|
51
|
+
`Bar color encoding on mark "${layer.id}" must be categorical or aggregate quantitative.`
|
|
52
52
|
);
|
|
53
53
|
}
|
|
54
54
|
}
|
|
@@ -27,19 +27,19 @@ export function normalizeBarWidth(options = {}, existing) {
|
|
|
27
27
|
return cloneAndFreeze({ pixels: options.pixels });
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
-
export function normalizeOffsetPadding(options = {}, existing) {
|
|
30
|
+
export function normalizeOffsetPadding(options = {}, existing, channel = "xOffset") {
|
|
31
31
|
const paddingInner = options.paddingInner ??
|
|
32
32
|
existing?.paddingInner ?? DEFAULT_OFFSET_PADDING.paddingInner;
|
|
33
33
|
const paddingOuter = options.paddingOuter ??
|
|
34
34
|
existing?.paddingOuter ?? DEFAULT_OFFSET_PADDING.paddingOuter;
|
|
35
35
|
if (!Number.isFinite(paddingInner) || paddingInner < 0 || paddingInner >= 1) {
|
|
36
36
|
throw new RangeError(
|
|
37
|
-
|
|
37
|
+
`${channel} paddingInner must be from 0 (inclusive) to 1 (exclusive).`
|
|
38
38
|
);
|
|
39
39
|
}
|
|
40
40
|
if (!Number.isFinite(paddingOuter) || paddingOuter < 0) {
|
|
41
41
|
throw new RangeError(
|
|
42
|
-
|
|
42
|
+
`${channel} paddingOuter must be a non-negative finite number.`
|
|
43
43
|
);
|
|
44
44
|
}
|
|
45
45
|
return cloneAndFreeze({ paddingInner, paddingOuter });
|
|
@@ -46,6 +46,11 @@ export function resolveBarChannels(layer) {
|
|
|
46
46
|
: { orientation, category: "y", measure: "x" };
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
+
export function resolveBarOffsetChannel(layer) {
|
|
50
|
+
const channels = resolveBarChannels(layer);
|
|
51
|
+
return channels === undefined ? undefined : `${channels.category}Offset`;
|
|
52
|
+
}
|
|
53
|
+
|
|
49
54
|
export function resolveBarGrain(layer) {
|
|
50
55
|
if (layer?.mark?.type !== "bar") return undefined;
|
|
51
56
|
const x = layer.encoding?.x;
|
|
@@ -84,7 +89,8 @@ export function resolveBarColorLayout(layer) {
|
|
|
84
89
|
}
|
|
85
90
|
const channels = resolveBarChannels(layer);
|
|
86
91
|
if (layer?.encoding?.[channels?.measure]?.stack === "normalize") return "fill";
|
|
87
|
-
|
|
92
|
+
const offsetChannel = resolveBarOffsetChannel(layer);
|
|
93
|
+
if (layer?.encoding?.[offsetChannel] !== undefined) return "group";
|
|
88
94
|
if (layer?.encoding?.[channels?.measure]?.stack === null) return "overlay";
|
|
89
95
|
return "stack";
|
|
90
96
|
}
|
|
@@ -1,10 +1,6 @@
|
|
|
1
|
+
import { getPositionChannelDefinition } from "../core/vocabulary.js";
|
|
2
|
+
|
|
1
3
|
const COORDINATE_TYPES = new Set(["cartesian", "polar"]);
|
|
2
|
-
const POSITION_COORDINATES = Object.freeze({
|
|
3
|
-
x: Object.freeze({ id: "main", type: "cartesian" }),
|
|
4
|
-
y: Object.freeze({ id: "main", type: "cartesian" }),
|
|
5
|
-
theta: Object.freeze({ id: "polar", type: "polar" }),
|
|
6
|
-
radius: Object.freeze({ id: "polar", type: "polar" })
|
|
7
|
-
});
|
|
8
4
|
|
|
9
5
|
export function validateCoordinateType(value) {
|
|
10
6
|
if (!COORDINATE_TYPES.has(value)) {
|
|
@@ -15,7 +11,7 @@ export function validateCoordinateType(value) {
|
|
|
15
11
|
}
|
|
16
12
|
|
|
17
13
|
export function getPositionCoordinateDefaults(channel) {
|
|
18
|
-
const coordinate =
|
|
14
|
+
const coordinate = getPositionChannelDefinition(channel)?.coordinate;
|
|
19
15
|
|
|
20
16
|
if (coordinate === undefined) {
|
|
21
17
|
throw new Error(`Unknown positional channel "${channel}".`);
|
package/src/grammar/density.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { cloneAndFreeze } from "../core/immutable.js";
|
|
2
|
-
import { isNominalValue } from "./scales.js";
|
|
2
|
+
import { isNominalValue } from "./scales/index.js";
|
|
3
3
|
|
|
4
4
|
const SQRT_TWO_PI = Math.sqrt(2 * Math.PI);
|
|
5
5
|
|
|
@@ -35,7 +35,7 @@ function requireField(value, label) {
|
|
|
35
35
|
export function validateDensityTransform(transform) {
|
|
36
36
|
const supported = [
|
|
37
37
|
"type", "field", "groupBy", "bandwidth", "extent", "steps", "as",
|
|
38
|
-
"resolve", "kernel", "normalization"
|
|
38
|
+
"resolve", "kernel", "normalization", "resolved"
|
|
39
39
|
];
|
|
40
40
|
const unknown = Object.keys(transform).find(key => !supported.includes(key));
|
|
41
41
|
if (unknown !== undefined) {
|
|
@@ -92,6 +92,25 @@ export function validateDensityTransform(transform) {
|
|
|
92
92
|
if (transform.resolve !== "shared") {
|
|
93
93
|
throw new Error(`Unsupported density resolve "${transform.resolve}".`);
|
|
94
94
|
}
|
|
95
|
+
if (transform.resolved !== undefined) {
|
|
96
|
+
const resolved = transform.resolved;
|
|
97
|
+
if (
|
|
98
|
+
resolved === null ||
|
|
99
|
+
typeof resolved !== "object" ||
|
|
100
|
+
Array.isArray(resolved) ||
|
|
101
|
+
Object.keys(resolved).some(key => !["bandwidth", "extent"].includes(key)) ||
|
|
102
|
+
!Number.isFinite(resolved.bandwidth) ||
|
|
103
|
+
resolved.bandwidth <= 0 ||
|
|
104
|
+
!Array.isArray(resolved.extent) ||
|
|
105
|
+
resolved.extent.length !== 2 ||
|
|
106
|
+
!resolved.extent.every(Number.isFinite) ||
|
|
107
|
+
resolved.extent[0] >= resolved.extent[1]
|
|
108
|
+
) {
|
|
109
|
+
throw new TypeError(
|
|
110
|
+
"Density resolved provenance requires a positive bandwidth and ascending finite extent."
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
95
114
|
return transform;
|
|
96
115
|
}
|
|
97
116
|
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import { cloneAndFreeze, isPlainObject } from "../../core/immutable.js";
|
|
2
|
+
import { validateUserId } from "../../core/identifiers.js";
|
|
3
|
+
|
|
4
|
+
const ROW_PRESERVING_TRANSFORMS = new Set(["filter"]);
|
|
5
|
+
const STATISTICAL_TRANSFORMS = new Set([
|
|
6
|
+
"regression",
|
|
7
|
+
"density",
|
|
8
|
+
"interval",
|
|
9
|
+
"boxSummary",
|
|
10
|
+
"boxOutlier"
|
|
11
|
+
]);
|
|
12
|
+
|
|
13
|
+
function requireCollection(value, label) {
|
|
14
|
+
if (!Array.isArray(value) || value.length === 0) {
|
|
15
|
+
throw new Error(`Facet dependency planning requires at least one ${label}.`);
|
|
16
|
+
}
|
|
17
|
+
return value;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function indexById(values, label) {
|
|
21
|
+
const indexed = new Map();
|
|
22
|
+
values.forEach((value, index) => {
|
|
23
|
+
if (!isPlainObject(value)) {
|
|
24
|
+
throw new TypeError(`Facet ${label} at index ${index} must be a plain object.`);
|
|
25
|
+
}
|
|
26
|
+
const id = validateUserId(value.id, `Facet ${label} id`);
|
|
27
|
+
if (indexed.has(id)) {
|
|
28
|
+
throw new Error(`Facet dependency graph contains duplicate ${label} id "${id}".`);
|
|
29
|
+
}
|
|
30
|
+
indexed.set(id, { value, index });
|
|
31
|
+
});
|
|
32
|
+
return indexed;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function classifyDataset(dataset) {
|
|
36
|
+
const transforms = dataset.transform;
|
|
37
|
+
if (transforms === undefined || transforms.length === 0) {
|
|
38
|
+
if (dataset.source !== undefined) {
|
|
39
|
+
throw new Error(
|
|
40
|
+
`Derived dataset "${dataset.id}" requires one supported transform.`
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
if (!Array.isArray(dataset.values)) {
|
|
44
|
+
throw new TypeError(`Source dataset "${dataset.id}" requires array values.`);
|
|
45
|
+
}
|
|
46
|
+
return "source";
|
|
47
|
+
}
|
|
48
|
+
if (!Array.isArray(transforms) || transforms.length !== 1 || !isPlainObject(transforms[0])) {
|
|
49
|
+
throw new Error(
|
|
50
|
+
`Facet replay currently requires dataset "${dataset.id}" to contain exactly one transform.`
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
if (typeof dataset.source !== "string" || dataset.source.length === 0) {
|
|
54
|
+
throw new Error(`Transformed dataset "${dataset.id}" requires a source dataset.`);
|
|
55
|
+
}
|
|
56
|
+
const type = transforms[0].type;
|
|
57
|
+
if (ROW_PRESERVING_TRANSFORMS.has(type)) return "rowPreserving";
|
|
58
|
+
if (STATISTICAL_TRANSFORMS.has(type)) return "statistical";
|
|
59
|
+
throw new Error(
|
|
60
|
+
`Facet replay does not support dataset transform "${type ?? "unknown"}" on "${dataset.id}".`
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function tracePath(id, datasets, classifications, visiting = new Set()) {
|
|
65
|
+
const indexed = datasets.get(id);
|
|
66
|
+
if (indexed === undefined) {
|
|
67
|
+
throw new Error(`Facet dependency graph references missing dataset "${id}".`);
|
|
68
|
+
}
|
|
69
|
+
if (visiting.has(id)) {
|
|
70
|
+
throw new Error(`Facet dependency graph contains a cycle at dataset "${id}".`);
|
|
71
|
+
}
|
|
72
|
+
const dataset = indexed.value;
|
|
73
|
+
const kind = classifications.get(id);
|
|
74
|
+
if (kind === "source") return [id];
|
|
75
|
+
const nextVisiting = new Set(visiting).add(id);
|
|
76
|
+
return [
|
|
77
|
+
...tracePath(dataset.source, datasets, classifications, nextVisiting),
|
|
78
|
+
id
|
|
79
|
+
];
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function eligiblePrefix(path, classifications) {
|
|
83
|
+
const statisticalIndex = path.findIndex(id => classifications.get(id) === "statistical");
|
|
84
|
+
return statisticalIndex === -1 ? path : path.slice(0, statisticalIndex);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function resolveAnchor(paths, datasets, classifications, requested) {
|
|
88
|
+
const eligible = paths.map(path => eligiblePrefix(path, classifications));
|
|
89
|
+
const common = eligible[0].filter(id =>
|
|
90
|
+
eligible.every(path => path.includes(id))
|
|
91
|
+
);
|
|
92
|
+
if (common.length === 0) {
|
|
93
|
+
const roots = [...new Set(paths.map(path => path[0]))];
|
|
94
|
+
throw new Error(
|
|
95
|
+
`Facet layers do not share one partition anchor; roots are ${roots.map(id => `"${id}"`).join(", ")}.`
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
if (requested !== undefined) {
|
|
99
|
+
const id = validateUserId(requested, "Facet partition dataset id");
|
|
100
|
+
if (!datasets.has(id)) {
|
|
101
|
+
throw new Error(`Facet partition dataset "${id}" does not exist.`);
|
|
102
|
+
}
|
|
103
|
+
if (!common.includes(id)) {
|
|
104
|
+
throw new Error(
|
|
105
|
+
`Facet partition dataset "${id}" is not a common row-preserving ancestor of every layer.`
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
return id;
|
|
109
|
+
}
|
|
110
|
+
return common.at(-1);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function requireFacetField(dataset, field) {
|
|
114
|
+
if (!Array.isArray(dataset.values)) {
|
|
115
|
+
throw new TypeError(
|
|
116
|
+
`Facet partition dataset "${dataset.id}" requires materialized array values.`
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
for (const [index, row] of dataset.values.entries()) {
|
|
120
|
+
if (!isPlainObject(row) || !Object.hasOwn(row, field)) {
|
|
121
|
+
throw new Error(
|
|
122
|
+
`Facet field "${field}" is missing from row ${index} of partition dataset "${dataset.id}".`
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function planFacetDependencies(semanticSpec, options = {}) {
|
|
129
|
+
if (!isPlainObject(semanticSpec)) {
|
|
130
|
+
throw new TypeError("planFacetDependencies requires a semantic spec.");
|
|
131
|
+
}
|
|
132
|
+
if (!isPlainObject(options)) {
|
|
133
|
+
throw new TypeError("Facet dependency options must be a plain object.");
|
|
134
|
+
}
|
|
135
|
+
if (typeof options.field !== "string" || options.field.length === 0) {
|
|
136
|
+
throw new TypeError("Facet field must be a non-empty string.");
|
|
137
|
+
}
|
|
138
|
+
const field = options.field;
|
|
139
|
+
const datasetValues = requireCollection(semanticSpec.datasets, "dataset");
|
|
140
|
+
const layerValues = requireCollection(semanticSpec.layers, "visible layer");
|
|
141
|
+
const datasets = indexById(datasetValues, "dataset");
|
|
142
|
+
const layers = indexById(layerValues, "layer");
|
|
143
|
+
const classifications = new Map(
|
|
144
|
+
[...datasets].map(([id, { value }]) => [id, classifyDataset(value)])
|
|
145
|
+
);
|
|
146
|
+
const layerPaths = [...layers].map(([layerId, { value: layer }]) => {
|
|
147
|
+
const data = validateUserId(layer.data, `Facet layer "${layerId}" dataset id`);
|
|
148
|
+
return { layerId, data, path: tracePath(data, datasets, classifications) };
|
|
149
|
+
});
|
|
150
|
+
const anchor = resolveAnchor(
|
|
151
|
+
layerPaths.map(entry => entry.path),
|
|
152
|
+
datasets,
|
|
153
|
+
classifications,
|
|
154
|
+
options.data
|
|
155
|
+
);
|
|
156
|
+
requireFacetField(datasets.get(anchor).value, field);
|
|
157
|
+
|
|
158
|
+
const replayIds = new Set();
|
|
159
|
+
for (const { path } of layerPaths) {
|
|
160
|
+
const anchorIndex = path.indexOf(anchor);
|
|
161
|
+
for (const id of path.slice(anchorIndex + 1)) replayIds.add(id);
|
|
162
|
+
}
|
|
163
|
+
const replay = [...replayIds]
|
|
164
|
+
.map(id => {
|
|
165
|
+
const { value: dataset, index } = datasets.get(id);
|
|
166
|
+
const depth = layerPaths.find(entry => entry.path.includes(id)).path.indexOf(id);
|
|
167
|
+
return {
|
|
168
|
+
id,
|
|
169
|
+
source: dataset.source,
|
|
170
|
+
kind: classifications.get(id),
|
|
171
|
+
transform: dataset.transform[0],
|
|
172
|
+
depth,
|
|
173
|
+
index
|
|
174
|
+
};
|
|
175
|
+
})
|
|
176
|
+
.sort((left, right) => left.depth - right.depth || left.index - right.index)
|
|
177
|
+
.map(({ depth, index, ...entry }) => entry);
|
|
178
|
+
|
|
179
|
+
return cloneAndFreeze({
|
|
180
|
+
field,
|
|
181
|
+
anchor,
|
|
182
|
+
replay,
|
|
183
|
+
layers: layerPaths.map(({ layerId, data }) => ({ id: layerId, data }))
|
|
184
|
+
});
|
|
185
|
+
}
|