ggaction 0.0.2 → 0.0.3
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 -0
- package/README.md +12 -1
- package/package.json +6 -1
- 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/encodings/appearance.js +11 -0
- package/src/actions/encodings/barWidth.js +5 -3
- package/src/actions/encodings/color.js +120 -25
- package/src/actions/encodings/index.js +2 -0
- package/src/actions/encodings/offset.js +90 -72
- package/src/actions/encodings/position/apply.js +66 -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 +57 -8
- package/src/actions/encodings/position.js +44 -90
- package/src/actions/encodings/ranged.js +28 -6
- package/src/actions/encodings/shared.js +17 -21
- 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 +21 -18
- 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 +9 -4
- package/src/actions/guides/axes/titles.js +7 -5
- package/src/actions/guides/grids/grid.js +144 -15
- package/src/actions/guides/grids/resolve.js +5 -13
- 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 +3 -1
- 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 +5 -5
- package/src/actions/guides/legends/edit.js +2 -6
- 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 +64 -0
- package/src/actions/guides/legends/size.js +12 -14
- package/src/actions/guides/polar/axes/facade.js +219 -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/axes.js +55 -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/index.js +4 -0
- 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/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 +25 -3
- package/src/actions/regression/components.js +1 -1
- package/src/actions/regression/create.js +18 -1
- 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.js +15 -6
- package/src/actions/scales/definitions.js +13 -4
- package/src/actions/scales/edit.js +42 -19
- package/src/actions/scales/materialize.js +57 -408
- 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 +200 -2
- package/src/core/action.js +10 -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/bars/aggregate.js +2 -2
- 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 +20 -1
- package/src/grammar/facets/dependencies.js +185 -0
- package/src/grammar/facets/guides.js +257 -0
- package/src/grammar/facets/scales.js +254 -0
- package/src/grammar/facets.js +122 -0
- package/src/grammar/lineSeries.js +111 -3
- 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/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/text.js +71 -0
- package/src/grammar/ticks.js +50 -13
- 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 +5 -2
- package/src/materialization/bars/grouped.js +75 -37
- package/src/materialization/composition.js +152 -0
- package/src/materialization/compositionSnapshot.js +114 -0
- package/src/materialization/dependencies.js +71 -16
- package/src/materialization/encodings.js +14 -28
- package/src/materialization/facetGuides.js +426 -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.js +220 -14
- 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/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 +24 -16
- package/src/materialization/text.js +156 -0
- package/src/renderers/canvas/index.js +68 -22
- package/types/extension.d.ts +1 -0
- package/types/index.d.ts +56 -1
- package/types/program.d.ts +664 -27
- package/src/actions/marks/line.js +0 -248
- package/src/materialization/selection/items.js +0 -503
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
import { cloneAndFreeze, isPlainObject } from "../core/immutable.js";
|
|
2
|
+
|
|
3
|
+
const DIRECTIONS = Object.freeze(["horizontal", "vertical"]);
|
|
4
|
+
const ALIGNMENTS = Object.freeze(["start", "center", "end"]);
|
|
5
|
+
const SIZE_MODES = Object.freeze(["auto", "explicit"]);
|
|
6
|
+
const PADDING_KEYS = Object.freeze(["top", "right", "bottom", "left"]);
|
|
7
|
+
|
|
8
|
+
export const DEFAULT_COMPOSITION_LAYOUT = cloneAndFreeze({
|
|
9
|
+
gap: 16,
|
|
10
|
+
align: "center",
|
|
11
|
+
padding: { top: 0, right: 0, bottom: 0, left: 0 }
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
export function validateCompositionSpacing(value, label) {
|
|
15
|
+
if (!Number.isFinite(value) || value < 0) {
|
|
16
|
+
throw new RangeError(`${label} must be a non-negative finite number.`);
|
|
17
|
+
}
|
|
18
|
+
return value;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function validatePaddingBase(base) {
|
|
22
|
+
if (!isPlainObject(base)) {
|
|
23
|
+
throw new TypeError("Composition padding base must be a plain object.");
|
|
24
|
+
}
|
|
25
|
+
for (const key of PADDING_KEYS) {
|
|
26
|
+
validateCompositionSpacing(base[key], `Composition padding.${key}`);
|
|
27
|
+
}
|
|
28
|
+
return base;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function normalizeCompositionPadding(
|
|
32
|
+
padding,
|
|
33
|
+
base = DEFAULT_COMPOSITION_LAYOUT.padding
|
|
34
|
+
) {
|
|
35
|
+
validatePaddingBase(base);
|
|
36
|
+
if (Number.isFinite(padding)) {
|
|
37
|
+
validateCompositionSpacing(padding, "Composition padding");
|
|
38
|
+
return cloneAndFreeze({
|
|
39
|
+
top: padding,
|
|
40
|
+
right: padding,
|
|
41
|
+
bottom: padding,
|
|
42
|
+
left: padding
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
if (!isPlainObject(padding)) {
|
|
46
|
+
throw new TypeError("Composition padding must be a number or a plain object.");
|
|
47
|
+
}
|
|
48
|
+
const keys = Object.keys(padding);
|
|
49
|
+
if (keys.length === 0) {
|
|
50
|
+
throw new TypeError("Composition padding object must contain at least one side.");
|
|
51
|
+
}
|
|
52
|
+
for (const key of keys) {
|
|
53
|
+
if (!PADDING_KEYS.includes(key)) {
|
|
54
|
+
throw new Error(`Unknown composition padding option "${key}".`);
|
|
55
|
+
}
|
|
56
|
+
validateCompositionSpacing(padding[key], `Composition padding.${key}`);
|
|
57
|
+
}
|
|
58
|
+
return cloneAndFreeze({ ...base, ...padding });
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function normalizeDirection(direction) {
|
|
62
|
+
if (!DIRECTIONS.includes(direction)) {
|
|
63
|
+
throw new Error(
|
|
64
|
+
`Unknown composition direction "${direction}"; expected horizontal or vertical.`
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
return direction;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function normalizeCompositionAlign(align) {
|
|
71
|
+
if (!ALIGNMENTS.includes(align)) {
|
|
72
|
+
throw new Error(
|
|
73
|
+
`Unknown composition align "${align}"; expected start, center, or end.`
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
return align;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function normalizeCompositionChildren(children) {
|
|
80
|
+
if (!Array.isArray(children) || children.length === 0) {
|
|
81
|
+
throw new TypeError("Composition layout requires a non-empty children array.");
|
|
82
|
+
}
|
|
83
|
+
const ids = new Set();
|
|
84
|
+
return children.map((child, index) => {
|
|
85
|
+
if (!isPlainObject(child)) {
|
|
86
|
+
throw new TypeError(`Composition child ${index} must be a plain object.`);
|
|
87
|
+
}
|
|
88
|
+
const unknown = Object.keys(child).find(
|
|
89
|
+
key => !["id", "width", "height", "widthMode", "heightMode"].includes(key)
|
|
90
|
+
);
|
|
91
|
+
if (unknown !== undefined) {
|
|
92
|
+
throw new Error(`Unknown composition child property "${unknown}".`);
|
|
93
|
+
}
|
|
94
|
+
if (typeof child.id !== "string" || child.id.length === 0) {
|
|
95
|
+
throw new TypeError(`Composition child ${index} requires a non-empty id.`);
|
|
96
|
+
}
|
|
97
|
+
if (ids.has(child.id)) {
|
|
98
|
+
throw new Error(`Duplicate composition child id "${child.id}".`);
|
|
99
|
+
}
|
|
100
|
+
ids.add(child.id);
|
|
101
|
+
for (const dimension of ["width", "height"]) {
|
|
102
|
+
if (!Number.isFinite(child[dimension]) || child[dimension] <= 0) {
|
|
103
|
+
throw new RangeError(
|
|
104
|
+
`Composition child "${child.id}" ${dimension} must be a positive finite number.`
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
const mode = child[`${dimension}Mode`] ?? "explicit";
|
|
108
|
+
if (!SIZE_MODES.includes(mode)) {
|
|
109
|
+
throw new Error(
|
|
110
|
+
`Unknown composition child ${dimension} mode "${mode}"; expected auto or explicit.`
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return cloneAndFreeze({
|
|
115
|
+
id: child.id,
|
|
116
|
+
width: child.width,
|
|
117
|
+
height: child.height,
|
|
118
|
+
widthMode: child.widthMode ?? "explicit",
|
|
119
|
+
heightMode: child.heightMode ?? "explicit"
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function normalizeAutoCrossSize(children, horizontal) {
|
|
125
|
+
const dimension = horizontal ? "height" : "width";
|
|
126
|
+
const mode = `${dimension}Mode`;
|
|
127
|
+
const sharedSize = Math.max(...children.map(child => child[dimension]));
|
|
128
|
+
return children.map(child => cloneAndFreeze({
|
|
129
|
+
...child,
|
|
130
|
+
[dimension]: child[mode] === "auto" ? sharedSize : child[dimension]
|
|
131
|
+
}));
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function placementSize(child) {
|
|
135
|
+
return {
|
|
136
|
+
id: child.id,
|
|
137
|
+
width: child.width,
|
|
138
|
+
height: child.height
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function crossOffset(remaining, align) {
|
|
143
|
+
if (align === "start") return 0;
|
|
144
|
+
if (align === "end") return remaining;
|
|
145
|
+
return remaining / 2;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export function resolveCompositionLayout({
|
|
149
|
+
direction,
|
|
150
|
+
children,
|
|
151
|
+
gap = DEFAULT_COMPOSITION_LAYOUT.gap,
|
|
152
|
+
align = DEFAULT_COMPOSITION_LAYOUT.align,
|
|
153
|
+
padding = DEFAULT_COMPOSITION_LAYOUT.padding
|
|
154
|
+
} = {}) {
|
|
155
|
+
const resolvedDirection = normalizeDirection(direction);
|
|
156
|
+
const resolvedChildren = normalizeCompositionChildren(children);
|
|
157
|
+
const resolvedGap = validateCompositionSpacing(gap, "Composition gap");
|
|
158
|
+
const resolvedAlign = normalizeCompositionAlign(align);
|
|
159
|
+
const resolvedPadding = normalizeCompositionPadding(padding);
|
|
160
|
+
const horizontal = resolvedDirection === "horizontal";
|
|
161
|
+
const sizedChildren = normalizeAutoCrossSize(resolvedChildren, horizontal);
|
|
162
|
+
const contentWidth = horizontal
|
|
163
|
+
? sizedChildren.reduce((sum, child) => sum + child.width, 0) +
|
|
164
|
+
resolvedGap * (sizedChildren.length - 1)
|
|
165
|
+
: Math.max(...sizedChildren.map(child => child.width));
|
|
166
|
+
const contentHeight = horizontal
|
|
167
|
+
? Math.max(...sizedChildren.map(child => child.height))
|
|
168
|
+
: sizedChildren.reduce((sum, child) => sum + child.height, 0) +
|
|
169
|
+
resolvedGap * (sizedChildren.length - 1);
|
|
170
|
+
let cursor = horizontal ? resolvedPadding.left : resolvedPadding.top;
|
|
171
|
+
const placements = sizedChildren.map(child => {
|
|
172
|
+
const placement = horizontal
|
|
173
|
+
? {
|
|
174
|
+
...placementSize(child),
|
|
175
|
+
x: cursor,
|
|
176
|
+
y: resolvedPadding.top + crossOffset(
|
|
177
|
+
contentHeight - child.height,
|
|
178
|
+
resolvedAlign
|
|
179
|
+
)
|
|
180
|
+
}
|
|
181
|
+
: {
|
|
182
|
+
...placementSize(child),
|
|
183
|
+
x: resolvedPadding.left + crossOffset(
|
|
184
|
+
contentWidth - child.width,
|
|
185
|
+
resolvedAlign
|
|
186
|
+
),
|
|
187
|
+
y: cursor
|
|
188
|
+
};
|
|
189
|
+
cursor += (horizontal ? child.width : child.height) + resolvedGap;
|
|
190
|
+
return placement;
|
|
191
|
+
});
|
|
192
|
+
return cloneAndFreeze({
|
|
193
|
+
direction: resolvedDirection,
|
|
194
|
+
gap: resolvedGap,
|
|
195
|
+
align: resolvedAlign,
|
|
196
|
+
padding: resolvedPadding,
|
|
197
|
+
width: resolvedPadding.left + contentWidth + resolvedPadding.right,
|
|
198
|
+
height: resolvedPadding.top + contentHeight + resolvedPadding.bottom,
|
|
199
|
+
children: placements
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
export function resolveCompositionSnapshotPlacement({
|
|
204
|
+
direction,
|
|
205
|
+
align,
|
|
206
|
+
placement,
|
|
207
|
+
width,
|
|
208
|
+
height
|
|
209
|
+
} = {}) {
|
|
210
|
+
const resolvedDirection = normalizeDirection(direction);
|
|
211
|
+
const resolvedAlign = normalizeCompositionAlign(align);
|
|
212
|
+
validatePlacedBounds(placement, "Composition snapshot placement");
|
|
213
|
+
if (!Number.isFinite(width) || width <= 0) {
|
|
214
|
+
throw new RangeError("Composition snapshot width must be a positive finite number.");
|
|
215
|
+
}
|
|
216
|
+
if (!Number.isFinite(height) || height <= 0) {
|
|
217
|
+
throw new RangeError("Composition snapshot height must be a positive finite number.");
|
|
218
|
+
}
|
|
219
|
+
if (width > placement.width || height > placement.height) {
|
|
220
|
+
throw new RangeError("Composition snapshot must fit its resolved placement.");
|
|
221
|
+
}
|
|
222
|
+
const horizontal = resolvedDirection === "horizontal";
|
|
223
|
+
return cloneAndFreeze({
|
|
224
|
+
x: placement.x + (horizontal
|
|
225
|
+
? 0
|
|
226
|
+
: crossOffset(placement.width - width, resolvedAlign)),
|
|
227
|
+
y: placement.y + (horizontal
|
|
228
|
+
? crossOffset(placement.height - height, resolvedAlign)
|
|
229
|
+
: 0),
|
|
230
|
+
width,
|
|
231
|
+
height
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function validatePlacedBounds(value, label) {
|
|
236
|
+
if (!isPlainObject(value)) {
|
|
237
|
+
throw new TypeError(`${label} must be a plain object.`);
|
|
238
|
+
}
|
|
239
|
+
for (const property of ["x", "y", "width", "height"]) {
|
|
240
|
+
if (!Number.isFinite(value[property])) {
|
|
241
|
+
throw new TypeError(`${label}.${property} must be finite.`);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
if (value.width <= 0 || value.height <= 0) {
|
|
245
|
+
throw new RangeError(`${label} width and height must be positive.`);
|
|
246
|
+
}
|
|
247
|
+
return value;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
export function resolvePlacedPlotBounds({ placements, plots } = {}) {
|
|
251
|
+
if (!Array.isArray(placements) || placements.length === 0) {
|
|
252
|
+
throw new TypeError("Placed plot bounds require non-empty placements.");
|
|
253
|
+
}
|
|
254
|
+
if (!Array.isArray(plots) || plots.length !== placements.length) {
|
|
255
|
+
throw new TypeError("Placed plot bounds require one local plot per placement.");
|
|
256
|
+
}
|
|
257
|
+
const plotById = new Map();
|
|
258
|
+
for (const [index, plot] of plots.entries()) {
|
|
259
|
+
if (!isPlainObject(plot) || typeof plot.id !== "string" || plot.id.length === 0) {
|
|
260
|
+
throw new TypeError(`Placed plot ${index} requires a non-empty id.`);
|
|
261
|
+
}
|
|
262
|
+
if (plotById.has(plot.id)) {
|
|
263
|
+
throw new Error(`Duplicate placed plot id "${plot.id}".`);
|
|
264
|
+
}
|
|
265
|
+
plotById.set(plot.id, validatePlacedBounds(plot, `Placed plot "${plot.id}"`));
|
|
266
|
+
}
|
|
267
|
+
const translated = placements.map((placement, index) => {
|
|
268
|
+
if (
|
|
269
|
+
!isPlainObject(placement) ||
|
|
270
|
+
typeof placement.id !== "string" ||
|
|
271
|
+
placement.id.length === 0
|
|
272
|
+
) {
|
|
273
|
+
throw new TypeError(`Plot placement ${index} requires a non-empty id.`);
|
|
274
|
+
}
|
|
275
|
+
validatePlacedBounds(placement, `Plot placement "${placement.id}"`);
|
|
276
|
+
const plot = plotById.get(placement.id);
|
|
277
|
+
if (plot === undefined) {
|
|
278
|
+
throw new Error(`Missing local plot bounds for "${placement.id}".`);
|
|
279
|
+
}
|
|
280
|
+
if (
|
|
281
|
+
plot.x < 0 || plot.y < 0 ||
|
|
282
|
+
plot.x + plot.width > placement.width ||
|
|
283
|
+
plot.y + plot.height > placement.height
|
|
284
|
+
) {
|
|
285
|
+
throw new RangeError(
|
|
286
|
+
`Local plot bounds for "${placement.id}" must fit its placement.`
|
|
287
|
+
);
|
|
288
|
+
}
|
|
289
|
+
plotById.delete(placement.id);
|
|
290
|
+
return {
|
|
291
|
+
left: placement.x + plot.x,
|
|
292
|
+
top: placement.y + plot.y,
|
|
293
|
+
right: placement.x + plot.x + plot.width,
|
|
294
|
+
bottom: placement.y + plot.y + plot.height
|
|
295
|
+
};
|
|
296
|
+
});
|
|
297
|
+
if (plotById.size > 0) {
|
|
298
|
+
throw new Error(`Unknown placed plot id "${plotById.keys().next().value}".`);
|
|
299
|
+
}
|
|
300
|
+
const left = Math.min(...translated.map(bounds => bounds.left));
|
|
301
|
+
const top = Math.min(...translated.map(bounds => bounds.top));
|
|
302
|
+
const right = Math.max(...translated.map(bounds => bounds.right));
|
|
303
|
+
const bottom = Math.max(...translated.map(bounds => bounds.bottom));
|
|
304
|
+
return cloneAndFreeze({
|
|
305
|
+
x: left,
|
|
306
|
+
y: top,
|
|
307
|
+
width: right - left,
|
|
308
|
+
height: bottom - top
|
|
309
|
+
});
|
|
310
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { cloneAndFreeze } from "../core/immutable.js";
|
|
2
|
+
import {
|
|
3
|
+
DEFAULT_COMPOSITION_LAYOUT,
|
|
4
|
+
normalizeCompositionAlign,
|
|
5
|
+
normalizeCompositionChildren,
|
|
6
|
+
normalizeCompositionPadding,
|
|
7
|
+
validateCompositionSpacing
|
|
8
|
+
} from "./composition.js";
|
|
9
|
+
|
|
10
|
+
export const DEFAULT_FACET_LEGEND_GAP = 18;
|
|
11
|
+
export const DEFAULT_FACET_LEGEND_WIDTH = 132;
|
|
12
|
+
|
|
13
|
+
function resolveColumns(columns, count) {
|
|
14
|
+
const value = columns ?? count;
|
|
15
|
+
if (!Number.isInteger(value) || value <= 0) {
|
|
16
|
+
throw new RangeError("Facet columns must be a positive integer.");
|
|
17
|
+
}
|
|
18
|
+
return Math.min(value, count);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function alignedOffset(remaining, align) {
|
|
22
|
+
if (align === "start") return 0;
|
|
23
|
+
if (align === "end") return remaining;
|
|
24
|
+
return remaining / 2;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function resolveFacetLayout({
|
|
28
|
+
children,
|
|
29
|
+
columns,
|
|
30
|
+
gap = DEFAULT_COMPOSITION_LAYOUT.gap,
|
|
31
|
+
align = DEFAULT_COMPOSITION_LAYOUT.align,
|
|
32
|
+
padding = DEFAULT_COMPOSITION_LAYOUT.padding,
|
|
33
|
+
titleHeight = 0,
|
|
34
|
+
sharedLegend = false,
|
|
35
|
+
sharedLegendGap = DEFAULT_FACET_LEGEND_GAP,
|
|
36
|
+
sharedLegendWidth = DEFAULT_FACET_LEGEND_WIDTH
|
|
37
|
+
} = {}) {
|
|
38
|
+
const values = children?.map(child => child?.value);
|
|
39
|
+
const resolvedChildren = normalizeCompositionChildren(children?.map(
|
|
40
|
+
({ value: _value, ...child }) => child
|
|
41
|
+
));
|
|
42
|
+
const resolvedColumns = resolveColumns(columns, resolvedChildren.length);
|
|
43
|
+
const resolvedGap = validateCompositionSpacing(gap, "Facet gap");
|
|
44
|
+
const resolvedAlign = normalizeCompositionAlign(align);
|
|
45
|
+
const resolvedPadding = normalizeCompositionPadding(padding);
|
|
46
|
+
const resolvedTitleHeight = validateCompositionSpacing(
|
|
47
|
+
titleHeight,
|
|
48
|
+
"Facet title height"
|
|
49
|
+
);
|
|
50
|
+
if (typeof sharedLegend !== "boolean") {
|
|
51
|
+
throw new TypeError("Facet sharedLegend must be a boolean.");
|
|
52
|
+
}
|
|
53
|
+
const legendGap = validateCompositionSpacing(
|
|
54
|
+
sharedLegendGap,
|
|
55
|
+
"Facet shared legend gap"
|
|
56
|
+
);
|
|
57
|
+
const legendWidth = validateCompositionSpacing(
|
|
58
|
+
sharedLegendWidth,
|
|
59
|
+
"Facet shared legend width"
|
|
60
|
+
);
|
|
61
|
+
const rowCount = Math.ceil(resolvedChildren.length / resolvedColumns);
|
|
62
|
+
const columnWidths = Array.from({ length: resolvedColumns }, (_, column) =>
|
|
63
|
+
Math.max(...resolvedChildren
|
|
64
|
+
.filter((_, index) => index % resolvedColumns === column)
|
|
65
|
+
.map(child => child.width))
|
|
66
|
+
);
|
|
67
|
+
const rowHeights = Array.from({ length: rowCount }, (_, row) =>
|
|
68
|
+
Math.max(...resolvedChildren
|
|
69
|
+
.slice(row * resolvedColumns, (row + 1) * resolvedColumns)
|
|
70
|
+
.map(child => child.height))
|
|
71
|
+
);
|
|
72
|
+
const columnStarts = columnWidths.map((_, column) =>
|
|
73
|
+
resolvedPadding.left + columnWidths
|
|
74
|
+
.slice(0, column)
|
|
75
|
+
.reduce((sum, width) => sum + width, 0) + resolvedGap * column
|
|
76
|
+
);
|
|
77
|
+
const rowStarts = rowHeights.map((_, row) =>
|
|
78
|
+
resolvedTitleHeight + resolvedPadding.top + rowHeights
|
|
79
|
+
.slice(0, row)
|
|
80
|
+
.reduce((sum, height) => sum + height, 0) + resolvedGap * row
|
|
81
|
+
);
|
|
82
|
+
const placements = resolvedChildren.map((child, index) => {
|
|
83
|
+
const column = index % resolvedColumns;
|
|
84
|
+
const row = Math.floor(index / resolvedColumns);
|
|
85
|
+
return {
|
|
86
|
+
id: child.id,
|
|
87
|
+
value: values[index],
|
|
88
|
+
column,
|
|
89
|
+
row,
|
|
90
|
+
x: columnStarts[column] + alignedOffset(
|
|
91
|
+
columnWidths[column] - child.width,
|
|
92
|
+
resolvedAlign
|
|
93
|
+
),
|
|
94
|
+
y: rowStarts[row] + alignedOffset(
|
|
95
|
+
rowHeights[row] - child.height,
|
|
96
|
+
resolvedAlign
|
|
97
|
+
),
|
|
98
|
+
width: child.width,
|
|
99
|
+
height: child.height
|
|
100
|
+
};
|
|
101
|
+
});
|
|
102
|
+
const gridWidth = resolvedPadding.left +
|
|
103
|
+
columnWidths.reduce((sum, width) => sum + width, 0) +
|
|
104
|
+
resolvedGap * Math.max(0, resolvedColumns - 1) +
|
|
105
|
+
resolvedPadding.right;
|
|
106
|
+
const gridHeight = resolvedTitleHeight + resolvedPadding.top +
|
|
107
|
+
rowHeights.reduce((sum, height) => sum + height, 0) +
|
|
108
|
+
resolvedGap * Math.max(0, rowCount - 1) +
|
|
109
|
+
resolvedPadding.bottom;
|
|
110
|
+
return cloneAndFreeze({
|
|
111
|
+
columns: resolvedColumns,
|
|
112
|
+
rows: rowCount,
|
|
113
|
+
gap: resolvedGap,
|
|
114
|
+
align: resolvedAlign,
|
|
115
|
+
padding: resolvedPadding,
|
|
116
|
+
titleHeight: resolvedTitleHeight,
|
|
117
|
+
gridWidth,
|
|
118
|
+
width: gridWidth + (sharedLegend
|
|
119
|
+
? legendGap + legendWidth
|
|
120
|
+
: 0),
|
|
121
|
+
height: gridHeight,
|
|
122
|
+
children: placements,
|
|
123
|
+
...(sharedLegend ? {
|
|
124
|
+
legend: {
|
|
125
|
+
x: gridWidth + legendGap,
|
|
126
|
+
y: resolvedTitleHeight + resolvedPadding.top + 30,
|
|
127
|
+
width: legendWidth
|
|
128
|
+
}
|
|
129
|
+
} : {})
|
|
130
|
+
});
|
|
131
|
+
}
|
package/src/layout/title.js
CHANGED
|
@@ -55,6 +55,12 @@ export function alignedTitleAnchor(start, length, blockLength, align) {
|
|
|
55
55
|
return start + length - blockLength / 2;
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
+
export function alignedTextAnchor(start, length, align) {
|
|
59
|
+
if (align === "left") return start;
|
|
60
|
+
if (align === "center") return start + length / 2;
|
|
61
|
+
return start + length;
|
|
62
|
+
}
|
|
63
|
+
|
|
58
64
|
function axisAlignedTextBounds({ x, y, text, style, align, rotation }) {
|
|
59
65
|
const width = measureTextWidth(text, style);
|
|
60
66
|
const left = align === "left" ? 0 : align === "center" ? -width / 2 : -width;
|
|
@@ -4,7 +4,10 @@ import {
|
|
|
4
4
|
resolveBarChannels,
|
|
5
5
|
resolveBarColorLayout
|
|
6
6
|
} from "../../grammar/bars/policy.js";
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
DEFAULT_SERIES_BASELINE,
|
|
9
|
+
layoutSeriesPartition
|
|
10
|
+
} from "../../grammar/seriesLayout.js";
|
|
8
11
|
import {
|
|
9
12
|
isDiscretePositionScaleType,
|
|
10
13
|
mapContinuousScaleValues,
|
|
@@ -66,7 +69,7 @@ export function deriveAggregateRectangles(required, resolved, widthConfig) {
|
|
|
66
69
|
widthConfig,
|
|
67
70
|
Math.abs(categoryScale.bandwidth ?? categoryScale.step)
|
|
68
71
|
);
|
|
69
|
-
const baseline =
|
|
72
|
+
const baseline = DEFAULT_SERIES_BASELINE;
|
|
70
73
|
const continuousColor = colorEncoding?.fieldType === "quantitative";
|
|
71
74
|
if (continuousColor) {
|
|
72
75
|
const mappedColors = mapSequentialColors(
|
|
@@ -1,18 +1,32 @@
|
|
|
1
1
|
import { deriveBarAggregates } from "../../grammar/bars/aggregate.js";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
mapContinuousScaleValues,
|
|
4
|
+
mapOrdinalPositionValues
|
|
5
|
+
} from "../../grammar/scales.js";
|
|
3
6
|
import { sameOrderedValues } from "../../core/validation.js";
|
|
4
7
|
import {
|
|
5
8
|
DEFAULT_BAR_STROKE,
|
|
6
9
|
DEFAULT_BAR_STROKE_WIDTH
|
|
7
10
|
} from "./resolve.js";
|
|
8
11
|
import { resolveBarWidth } from "../../grammar/bars/geometry.js";
|
|
12
|
+
import { DEFAULT_SERIES_BASELINE } from "../../grammar/seriesLayout.js";
|
|
13
|
+
import {
|
|
14
|
+
BAR_ORIENTATIONS,
|
|
15
|
+
resolveBarChannels,
|
|
16
|
+
resolveBarOffsetChannel
|
|
17
|
+
} from "../../grammar/bars/policy.js";
|
|
9
18
|
|
|
10
19
|
export function deriveGroupedRectangles(required, resolved, widthConfig) {
|
|
11
20
|
const { dataset, layer } = required;
|
|
12
21
|
const xScale = resolved.resolvedScales[required.xEncoding.scale];
|
|
13
22
|
const yScale = resolved.resolvedScales[required.yEncoding.scale];
|
|
23
|
+
const channels = resolveBarChannels(layer);
|
|
24
|
+
const vertical = channels.orientation === BAR_ORIENTATIONS.vertical;
|
|
25
|
+
const categoryScale = vertical ? xScale : yScale;
|
|
26
|
+
const measureScale = vertical ? yScale : xScale;
|
|
27
|
+
const offsetChannel = resolveBarOffsetChannel(layer);
|
|
14
28
|
const colorEncoding = layer.encoding?.color;
|
|
15
|
-
const offsetEncoding = layer.encoding?.
|
|
29
|
+
const offsetEncoding = layer.encoding?.[offsetChannel];
|
|
16
30
|
const colorScale = resolved.resolvedScales[colorEncoding?.scale];
|
|
17
31
|
const offsetScale = resolved.resolvedScales[offsetEncoding?.scale];
|
|
18
32
|
|
|
@@ -23,16 +37,18 @@ export function deriveGroupedRectangles(required, resolved, widthConfig) {
|
|
|
23
37
|
offsetScale === undefined
|
|
24
38
|
) {
|
|
25
39
|
throw new Error(
|
|
26
|
-
`Grouped bar mark "${layer.id}" requires matching color and
|
|
40
|
+
`Grouped bar mark "${layer.id}" requires matching color and ${offsetChannel} scales.`
|
|
27
41
|
);
|
|
28
42
|
}
|
|
29
43
|
if (!sameOrderedValues(colorScale.domain, offsetScale.domain)) {
|
|
30
44
|
throw new Error(
|
|
31
|
-
`Grouped bar mark "${layer.id}" requires matching color and
|
|
45
|
+
`Grouped bar mark "${layer.id}" requires matching color and ${offsetChannel} domains.`
|
|
32
46
|
);
|
|
33
47
|
}
|
|
34
48
|
|
|
35
|
-
const
|
|
49
|
+
const categoryIndex = new Map(
|
|
50
|
+
categoryScale.domain.map((value, index) => [value, index])
|
|
51
|
+
);
|
|
36
52
|
const offsetIndex = new Map(
|
|
37
53
|
offsetScale.domain.map((value, index) => [value, index])
|
|
38
54
|
);
|
|
@@ -43,18 +59,21 @@ export function deriveGroupedRectangles(required, resolved, widthConfig) {
|
|
|
43
59
|
])
|
|
44
60
|
);
|
|
45
61
|
const offsetMidpoint = (offsetScale.range[0] + offsetScale.range[1]) / 2;
|
|
46
|
-
const
|
|
62
|
+
const categoryDirection = Math.sign(
|
|
63
|
+
categoryScale.step ?? categoryScale.range[1] - categoryScale.range[0]
|
|
64
|
+
) || 1;
|
|
47
65
|
const offsetDirection = Math.sign(offsetScale.step) || 1;
|
|
48
66
|
const width = resolveBarWidth(widthConfig, offsetScale.bandwidth);
|
|
49
67
|
const baseline = mapContinuousScaleValues(
|
|
50
|
-
[
|
|
51
|
-
|
|
68
|
+
[DEFAULT_SERIES_BASELINE],
|
|
69
|
+
measureScale
|
|
52
70
|
)[0];
|
|
53
71
|
const cells = [...deriveBarAggregates(dataset.values, layer).values].sort(
|
|
54
72
|
(left, right) =>
|
|
55
|
-
(
|
|
56
|
-
? left.
|
|
57
|
-
:
|
|
73
|
+
(categoryScale.type === "time"
|
|
74
|
+
? left[channels.category] - right[channels.category]
|
|
75
|
+
: categoryIndex.get(left[channels.category]) -
|
|
76
|
+
categoryIndex.get(right[channels.category])) ||
|
|
58
77
|
offsetIndex.get(left.color) - offsetIndex.get(right.color)
|
|
59
78
|
);
|
|
60
79
|
const existing = resolved.graphicSpec.objects[layer.id].items;
|
|
@@ -62,41 +81,60 @@ export function deriveGroupedRectangles(required, resolved, widthConfig) {
|
|
|
62
81
|
const appearance = config.barAppearance ?? {};
|
|
63
82
|
|
|
64
83
|
return cells.map((cell, index) => {
|
|
65
|
-
const
|
|
84
|
+
const categoryValue = cell[channels.category];
|
|
85
|
+
const category = categoryIndex.get(categoryValue);
|
|
66
86
|
const offset = offsetIndex.get(cell.color);
|
|
67
|
-
if ((
|
|
87
|
+
if ((categoryScale.type !== "time" && category === undefined) || offset === undefined) {
|
|
68
88
|
throw new Error("Grouped bar value is outside a resolved ordinal domain.");
|
|
69
89
|
}
|
|
70
90
|
|
|
71
|
-
const temporal = xScale.type === "time";
|
|
72
|
-
const categoryStart = temporal
|
|
73
|
-
? mapContinuousScaleValues(
|
|
74
|
-
[cell.x],
|
|
75
|
-
xScale
|
|
76
|
-
)[0] - xScale.bandwidth / 2
|
|
77
|
-
: (xScale.start ?? xScale.range[0]) + category * xScale.step;
|
|
78
91
|
const offsetCenter = offsetScale.start + offset * offsetScale.step +
|
|
79
92
|
offsetDirection * offsetScale.bandwidth / 2;
|
|
80
|
-
const
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
? categoryStart + offsetCenter
|
|
84
|
-
: categoryStart + xScale.step / 2 +
|
|
85
|
-
direction * (offsetCenter - offsetMidpoint);
|
|
86
|
-
const x = temporal || (xScale.step > 0 && offsetScale.step > 0)
|
|
87
|
-
? categoryStart + offsetScale.start + offset * offsetScale.step +
|
|
88
|
-
(offsetScale.bandwidth - width) / 2
|
|
89
|
-
: center - width / 2;
|
|
90
|
-
const valueY = mapContinuousScaleValues(
|
|
91
|
-
[cell.y],
|
|
92
|
-
yScale
|
|
93
|
+
const valuePosition = mapContinuousScaleValues(
|
|
94
|
+
[cell[channels.measure]],
|
|
95
|
+
measureScale
|
|
93
96
|
)[0];
|
|
97
|
+
let geometry;
|
|
98
|
+
if (vertical) {
|
|
99
|
+
// Preserve the established vertical arithmetic order exactly. Primitive
|
|
100
|
+
// baselines intentionally compare concrete floating-point coordinates.
|
|
101
|
+
const temporal = xScale.type === "time";
|
|
102
|
+
const categoryStart = temporal
|
|
103
|
+
? mapContinuousScaleValues([cell.x], xScale)[0] - xScale.bandwidth / 2
|
|
104
|
+
: (xScale.start ?? xScale.range[0]) + category * xScale.step;
|
|
105
|
+
const center = temporal
|
|
106
|
+
? categoryStart + offsetCenter
|
|
107
|
+
: xScale.step > 0 && offsetScale.step > 0
|
|
108
|
+
? categoryStart + offsetCenter
|
|
109
|
+
: categoryStart + xScale.step / 2 +
|
|
110
|
+
categoryDirection * (offsetCenter - offsetMidpoint);
|
|
111
|
+
const x = temporal || (xScale.step > 0 && offsetScale.step > 0)
|
|
112
|
+
? categoryStart + offsetScale.start + offset * offsetScale.step +
|
|
113
|
+
(offsetScale.bandwidth - width) / 2
|
|
114
|
+
: center - width / 2;
|
|
115
|
+
geometry = {
|
|
116
|
+
x,
|
|
117
|
+
y: Math.min(valuePosition, baseline),
|
|
118
|
+
width,
|
|
119
|
+
height: Math.abs(baseline - valuePosition)
|
|
120
|
+
};
|
|
121
|
+
} else {
|
|
122
|
+
const categoryCenter = mapOrdinalPositionValues(
|
|
123
|
+
[categoryValue],
|
|
124
|
+
categoryScale
|
|
125
|
+
)[0];
|
|
126
|
+
const center = categoryCenter + categoryDirection *
|
|
127
|
+
(offsetCenter - offsetMidpoint);
|
|
128
|
+
geometry = {
|
|
129
|
+
x: Math.min(valuePosition, baseline),
|
|
130
|
+
y: center - width / 2,
|
|
131
|
+
width: Math.abs(baseline - valuePosition),
|
|
132
|
+
height: width
|
|
133
|
+
};
|
|
134
|
+
}
|
|
94
135
|
|
|
95
136
|
return {
|
|
96
|
-
|
|
97
|
-
y: Math.min(valueY, baseline),
|
|
98
|
-
width,
|
|
99
|
-
height: Math.abs(baseline - valueY),
|
|
137
|
+
...geometry,
|
|
100
138
|
fill: colors.get(cell.color),
|
|
101
139
|
stroke: appearance.stroke === false
|
|
102
140
|
? "transparent"
|