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/grammar/ticks.js
CHANGED
|
@@ -125,13 +125,7 @@ function pad(value) {
|
|
|
125
125
|
return String(value).padStart(2, "0");
|
|
126
126
|
}
|
|
127
127
|
|
|
128
|
-
|
|
129
|
-
if (!Number.isFinite(value)) {
|
|
130
|
-
throw new TypeError("Time tick value must be a finite timestamp.");
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
const [low, high] = validateTimeDomain(domain);
|
|
134
|
-
const span = high - low;
|
|
128
|
+
function formatTimePrecision(value, precision) {
|
|
135
129
|
const date = new Date(value);
|
|
136
130
|
const year = String(date.getUTCFullYear()).padStart(4, "0");
|
|
137
131
|
const month = pad(date.getUTCMonth() + 1);
|
|
@@ -139,11 +133,54 @@ export function formatTimeTick(value, domain) {
|
|
|
139
133
|
const hour = pad(date.getUTCHours());
|
|
140
134
|
const minute = pad(date.getUTCMinutes());
|
|
141
135
|
const second = pad(date.getUTCSeconds());
|
|
136
|
+
const millisecond = String(date.getUTCMilliseconds()).padStart(3, "0");
|
|
137
|
+
|
|
138
|
+
if (precision === 0) return year;
|
|
139
|
+
if (precision === 1) return `${year}-${month}`;
|
|
140
|
+
if (precision === 2) return `${year}-${month}-${day}`;
|
|
141
|
+
if (precision === 3) return `${year}-${month}-${day} ${hour}:${minute}`;
|
|
142
|
+
if (precision === 4) return `${hour}:${minute}`;
|
|
143
|
+
if (precision === 5) return `${hour}:${minute}:${second}`;
|
|
144
|
+
return `${hour}:${minute}:${second}.${millisecond}`;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function timePrecision(domain) {
|
|
148
|
+
const [low, high] = validateTimeDomain(domain);
|
|
149
|
+
const span = high - low;
|
|
150
|
+
|
|
151
|
+
if (span >= 2 * YEAR) return 0;
|
|
152
|
+
if (span >= 60 * DAY) return 1;
|
|
153
|
+
if (span >= 2 * DAY) return 2;
|
|
154
|
+
if (span >= 2 * HOUR) return 3;
|
|
155
|
+
if (span >= 2 * MINUTE) return 4;
|
|
156
|
+
return 5;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export function formatTimeTick(value, domain) {
|
|
160
|
+
if (!Number.isFinite(value)) {
|
|
161
|
+
throw new TypeError("Time tick value must be a finite timestamp.");
|
|
162
|
+
}
|
|
163
|
+
return formatTimePrecision(value, timePrecision(domain));
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function distinguishesTicks(values, labels) {
|
|
167
|
+
const timestamps = new Map();
|
|
168
|
+
for (let index = 0; index < values.length; index += 1) {
|
|
169
|
+
const previous = timestamps.get(labels[index]);
|
|
170
|
+
if (previous !== undefined && previous !== values[index]) return false;
|
|
171
|
+
timestamps.set(labels[index], values[index]);
|
|
172
|
+
}
|
|
173
|
+
return true;
|
|
174
|
+
}
|
|
142
175
|
|
|
143
|
-
|
|
144
|
-
if (
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
176
|
+
export function formatTimeTicks(values, domain) {
|
|
177
|
+
if (!Array.isArray(values) || !values.every(Number.isFinite)) {
|
|
178
|
+
throw new TypeError("Time tick values must be finite timestamps.");
|
|
179
|
+
}
|
|
180
|
+
const initial = timePrecision(domain);
|
|
181
|
+
for (let precision = initial; precision <= 6; precision += 1) {
|
|
182
|
+
const labels = values.map(value => formatTimePrecision(value, precision));
|
|
183
|
+
if (distinguishesTicks(values, labels)) return cloneAndFreeze(labels);
|
|
184
|
+
}
|
|
185
|
+
return cloneAndFreeze(values.map(value => String(value)));
|
|
149
186
|
}
|
|
@@ -4,7 +4,7 @@ import { validateDensityTransform } from "./density.js";
|
|
|
4
4
|
import { validateFilterTransform } from "./filter.js";
|
|
5
5
|
import { validateIntervalTransform } from "./interval.js";
|
|
6
6
|
import { validateMarkFilterTransform } from "./markFilter.js";
|
|
7
|
-
import { validateRegressionTransform } from "./regression.js";
|
|
7
|
+
import { validateRegressionTransform } from "./regression/index.js";
|
|
8
8
|
|
|
9
9
|
const TRANSFORM_VALIDATORS = Object.freeze({
|
|
10
10
|
boxOutlier: validateBoxTransform,
|
package/src/index.js
CHANGED
|
@@ -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,13 +4,16 @@ 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,
|
|
11
14
|
mapOrdinalPositionValues,
|
|
12
15
|
mapSequentialColors
|
|
13
|
-
} from "../../grammar/scales.js";
|
|
16
|
+
} from "../../grammar/scales/index.js";
|
|
14
17
|
import {
|
|
15
18
|
DEFAULT_BAR_FILL,
|
|
16
19
|
DEFAULT_BAR_STROKE,
|
|
@@ -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(
|