ggaction 0.0.1
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 +27 -0
- package/LICENSE +21 -0
- package/README.md +108 -0
- package/package.json +84 -0
- package/src/AGENTS.md +119 -0
- package/src/ChartProgram.js +10 -0
- package/src/actions/boxPlots/components.js +88 -0
- package/src/actions/boxPlots/create.js +84 -0
- package/src/actions/boxPlots/index.js +10 -0
- package/src/actions/boxPlots/materialize.js +183 -0
- package/src/actions/boxPlots/options.js +147 -0
- package/src/actions/boxPlots/resolve.js +38 -0
- package/src/actions/canvas/actions.js +144 -0
- package/src/actions/canvas/index.js +6 -0
- package/src/actions/coordinates/actions.js +85 -0
- package/src/actions/coordinates/index.js +5 -0
- package/src/actions/data/box.js +40 -0
- package/src/actions/data/create.js +33 -0
- package/src/actions/data/density.js +62 -0
- package/src/actions/data/derived.js +52 -0
- package/src/actions/data/filter.js +168 -0
- package/src/actions/data/index.js +32 -0
- package/src/actions/data/interval.js +69 -0
- package/src/actions/data/regression.js +71 -0
- package/src/actions/data/shared.js +25 -0
- package/src/actions/encodings/appearance.js +243 -0
- package/src/actions/encodings/barWidth.js +60 -0
- package/src/actions/encodings/color.js +329 -0
- package/src/actions/encodings/density.js +225 -0
- package/src/actions/encodings/histogram.js +50 -0
- package/src/actions/encodings/index.js +23 -0
- package/src/actions/encodings/offset.js +104 -0
- package/src/actions/encodings/position/policies/area.js +24 -0
- package/src/actions/encodings/position/policies/bar.js +110 -0
- package/src/actions/encodings/position/policies/common.js +17 -0
- package/src/actions/encodings/position/policies/index.js +25 -0
- package/src/actions/encodings/position/policies/line.js +74 -0
- package/src/actions/encodings/position/policies/point.js +19 -0
- package/src/actions/encodings/position/policies/rule.js +17 -0
- package/src/actions/encodings/position/resolve.js +170 -0
- package/src/actions/encodings/position.js +176 -0
- package/src/actions/encodings/ranged.js +276 -0
- package/src/actions/encodings/ruleAppearance.js +47 -0
- package/src/actions/encodings/shared.js +164 -0
- package/src/actions/encodings/strokeDash.js +145 -0
- package/src/actions/errorBands/components.js +81 -0
- package/src/actions/errorBands/create.js +159 -0
- package/src/actions/errorBands/index.js +7 -0
- package/src/actions/errorBands/resolve.js +28 -0
- package/src/actions/errorBars/components.js +62 -0
- package/src/actions/errorBars/create.js +144 -0
- package/src/actions/errorBars/index.js +7 -0
- package/src/actions/errorBars/resolve.js +25 -0
- package/src/actions/guides/axes/axes.js +204 -0
- package/src/actions/guides/axes/axis.js +131 -0
- package/src/actions/guides/axes/index.js +17 -0
- package/src/actions/guides/axes/labels.js +231 -0
- package/src/actions/guides/axes/lines.js +149 -0
- package/src/actions/guides/axes/policy.js +155 -0
- package/src/actions/guides/axes/tickGroups.js +149 -0
- package/src/actions/guides/axes/ticks.js +179 -0
- package/src/actions/guides/axes/titles.js +264 -0
- package/src/actions/guides/grids/grid.js +228 -0
- package/src/actions/guides/grids/index.js +1 -0
- package/src/actions/guides/grids/resolve.js +258 -0
- package/src/actions/guides/guides.js +138 -0
- package/src/actions/guides/index.js +11 -0
- package/src/actions/guides/legends/categorical/actions.js +329 -0
- package/src/actions/guides/legends/categorical/components.js +140 -0
- package/src/actions/guides/legends/categorical/index.js +49 -0
- package/src/actions/guides/legends/categorical/layout.js +445 -0
- package/src/actions/guides/legends/categorical/options.js +188 -0
- package/src/actions/guides/legends/categorical/recipes.js +120 -0
- package/src/actions/guides/legends/categorical/resolve.js +131 -0
- package/src/actions/guides/legends/categorical/symbols.js +280 -0
- package/src/actions/guides/legends/categorical/validation.js +27 -0
- package/src/actions/guides/legends/continuous/common.js +273 -0
- package/src/actions/guides/legends/continuous/gradient.js +363 -0
- package/src/actions/guides/legends/continuous/index.js +25 -0
- package/src/actions/guides/legends/continuous/interval.js +234 -0
- package/src/actions/guides/legends/continuous/opacity.js +382 -0
- package/src/actions/guides/legends/edit.js +314 -0
- package/src/actions/guides/legends/index.js +9 -0
- package/src/actions/guides/legends/size.js +220 -0
- package/src/actions/guides/tickValues.js +84 -0
- package/src/actions/index.js +31 -0
- package/src/actions/intervals/resolve.js +352 -0
- package/src/actions/marks/area.js +369 -0
- package/src/actions/marks/bar/create.js +46 -0
- package/src/actions/marks/bar/edit.js +90 -0
- package/src/actions/marks/bar/index.js +9 -0
- package/src/actions/marks/bar/materialize.js +130 -0
- package/src/actions/marks/index.js +13 -0
- package/src/actions/marks/line.js +248 -0
- package/src/actions/marks/point.js +420 -0
- package/src/actions/marks/rule.js +287 -0
- package/src/actions/marks/shared.js +112 -0
- package/src/actions/primitives/createGraphics.js +163 -0
- package/src/actions/primitives/editGraphics.js +208 -0
- package/src/actions/primitives/index.js +9 -0
- package/src/actions/primitives/semantic.js +168 -0
- package/src/actions/primitives/semanticValue.js +182 -0
- package/src/actions/regression/components.js +216 -0
- package/src/actions/regression/create.js +102 -0
- package/src/actions/regression/index.js +23 -0
- package/src/actions/regression/resolve.js +57 -0
- package/src/actions/scales/consumers.js +283 -0
- package/src/actions/scales/create.js +148 -0
- package/src/actions/scales/definitions.js +319 -0
- package/src/actions/scales/edit.js +290 -0
- package/src/actions/scales/index.js +11 -0
- package/src/actions/scales/materialize.js +466 -0
- package/src/actions/scales/quantitativeColor.js +45 -0
- package/src/actions/selection/actions.js +384 -0
- package/src/actions/selection/index.js +23 -0
- package/src/actions/titles/actions.js +235 -0
- package/src/actions/titles/index.js +19 -0
- package/src/actions/titles/resolve.js +321 -0
- package/src/core/ChartProgram.js +316 -0
- package/src/core/action.js +120 -0
- package/src/core/identifiers.js +28 -0
- package/src/core/immutable.js +94 -0
- package/src/core/specs.js +29 -0
- package/src/core/textMetrics.js +23 -0
- package/src/core/validation.js +54 -0
- package/src/core/vocabulary.js +58 -0
- package/src/extension.js +2 -0
- package/src/grammar/aggregate.js +259 -0
- package/src/grammar/areaSeries.js +217 -0
- package/src/grammar/bars/aggregate.js +98 -0
- package/src/grammar/bars/geometry.js +54 -0
- package/src/grammar/bars/policy.js +90 -0
- package/src/grammar/boxPlot.js +140 -0
- package/src/grammar/coordinates.js +25 -0
- package/src/grammar/curveCommands.js +356 -0
- package/src/grammar/density.js +296 -0
- package/src/grammar/filter.js +169 -0
- package/src/grammar/histogram.js +332 -0
- package/src/grammar/interval.js +343 -0
- package/src/grammar/lineSeries.js +197 -0
- package/src/grammar/markFilter.js +46 -0
- package/src/grammar/markSelection.js +326 -0
- package/src/grammar/palettes.js +208 -0
- package/src/grammar/pathCommands.js +86 -0
- package/src/grammar/pointShapes.js +164 -0
- package/src/grammar/positionCompatibility.js +32 -0
- package/src/grammar/regression.js +578 -0
- package/src/grammar/ruleAppearance.js +12 -0
- package/src/grammar/rules.js +90 -0
- package/src/grammar/scales/appearance.js +185 -0
- package/src/grammar/scales/color.js +349 -0
- package/src/grammar/scales/continuous.js +111 -0
- package/src/grammar/scales/definition.js +153 -0
- package/src/grammar/scales/discretized.js +159 -0
- package/src/grammar/scales/fields.js +133 -0
- package/src/grammar/scales/index.js +12 -0
- package/src/grammar/scales/mapping.js +27 -0
- package/src/grammar/scales/ordinal.js +221 -0
- package/src/grammar/scales/policies.js +50 -0
- package/src/grammar/scales/temporal.js +53 -0
- package/src/grammar/scales/transformed.js +236 -0
- package/src/grammar/scales/types.js +102 -0
- package/src/grammar/scales/validation.js +66 -0
- package/src/grammar/scales.js +1 -0
- package/src/grammar/schemas/concreteGraphic.js +73 -0
- package/src/grammar/schemas/graphic.js +131 -0
- package/src/grammar/schemas/graphicBounds.js +146 -0
- package/src/grammar/schemas/graphicTree.js +133 -0
- package/src/grammar/schemas/mark.js +4 -0
- package/src/grammar/schemas/semanticPath.js +174 -0
- package/src/grammar/seriesLayout.js +76 -0
- package/src/grammar/ticks.js +149 -0
- package/src/grammar/transforms.js +37 -0
- package/src/index.js +2 -0
- package/src/layout/canvas.js +114 -0
- package/src/layout/grid.js +17 -0
- package/src/layout/legend.js +51 -0
- package/src/layout/text.js +62 -0
- package/src/layout/title.js +107 -0
- package/src/materialization/bars/aggregate.js +184 -0
- package/src/materialization/bars/grouped.js +117 -0
- package/src/materialization/bars/histogram.js +163 -0
- package/src/materialization/bars/ranged.js +53 -0
- package/src/materialization/bars/resolve.js +56 -0
- package/src/materialization/dataProvenance.js +15 -0
- package/src/materialization/density.js +29 -0
- package/src/materialization/dependencies.js +134 -0
- package/src/materialization/encodings.js +50 -0
- package/src/materialization/graphicHierarchy.js +104 -0
- package/src/materialization/legends.js +20 -0
- package/src/materialization/marks.js +110 -0
- package/src/materialization/planner.js +38 -0
- package/src/materialization/selection/filter.js +40 -0
- package/src/materialization/selection/items.js +503 -0
- package/src/materialization/selection/path.js +37 -0
- package/src/materialization/selection/point.js +118 -0
- package/src/materialization/selection/policies/area.js +10 -0
- package/src/materialization/selection/policies/bar.js +10 -0
- package/src/materialization/selection/policies/index.js +40 -0
- package/src/materialization/selection/policies/line.js +10 -0
- package/src/materialization/selection/policies/point.js +10 -0
- package/src/materialization/selection/policies/rule.js +10 -0
- package/src/materialization/selection/state.js +67 -0
- package/src/materialization/selection/styles.js +183 -0
- package/src/renderers/canvas/circle.js +62 -0
- package/src/renderers/canvas/index.js +132 -0
- package/src/renderers/canvas/line.js +54 -0
- package/src/renderers/canvas/path.js +98 -0
- package/src/renderers/canvas/rect.js +57 -0
- package/src/renderers/canvas/text.js +96 -0
- package/src/renderers/canvas/validation.js +23 -0
- package/src/renderers/png.js +29 -0
- package/src/selectors/coordinates.js +13 -0
- package/src/selectors/datasets.js +13 -0
- package/src/selectors/index.js +4 -0
- package/src/selectors/layers.js +32 -0
- package/src/selectors/scales.js +21 -0
- package/src/theme/defaults.js +13 -0
- package/types/extension.d.ts +14 -0
- package/types/index.d.ts +81 -0
- package/types/png.d.ts +14 -0
- package/types/program.d.ts +1096 -0
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import { action } from "../../../core/action.js";
|
|
2
|
+
import { validateUserId } from "../../../core/identifiers.js";
|
|
3
|
+
import {
|
|
4
|
+
validateNonEmptyString,
|
|
5
|
+
validateNonNegativeFinite
|
|
6
|
+
} from "../../../core/validation.js";
|
|
7
|
+
import { resolveGraphicBounds } from "../../../layout/canvas.js";
|
|
8
|
+
import {
|
|
9
|
+
isTransformedScaleType,
|
|
10
|
+
mapContinuousScaleValues,
|
|
11
|
+
mapOrdinalPositionValues
|
|
12
|
+
} from "../../../grammar/scales.js";
|
|
13
|
+
import {
|
|
14
|
+
DEFAULT_TICK_COUNT,
|
|
15
|
+
inferHistogramBoundaries,
|
|
16
|
+
valuesFromTickConfig
|
|
17
|
+
} from "../tickValues.js";
|
|
18
|
+
import { DEFAULT_COLORS } from "../../../theme/defaults.js";
|
|
19
|
+
import {
|
|
20
|
+
defaultAxisPosition,
|
|
21
|
+
resolveAxisTickGeometry,
|
|
22
|
+
validateAxisPosition
|
|
23
|
+
} from "./policy.js";
|
|
24
|
+
import { resolvePlotGraphicPlacement } from
|
|
25
|
+
"../../../materialization/graphicHierarchy.js";
|
|
26
|
+
|
|
27
|
+
const OPTIONS = Object.freeze(["scale", "position", "count", "values", "length", "color", "lineWidth"]);
|
|
28
|
+
const DEFAULTS = Object.freeze({
|
|
29
|
+
count: DEFAULT_TICK_COUNT,
|
|
30
|
+
length: 6,
|
|
31
|
+
color: DEFAULT_COLORS.mutedText,
|
|
32
|
+
lineWidth: 1
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
function validateOptions(args, operation, create) {
|
|
36
|
+
for (const key of Object.keys(args)) if (!OPTIONS.includes(key) || (!create && key === "scale")) throw new Error(`Unknown ${operation} option "${key}".`);
|
|
37
|
+
if (Object.hasOwn(args, "count") && Object.hasOwn(args, "values")) throw new Error(`${operation} cannot use count and values together.`);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function validateConfig(channel, config) {
|
|
41
|
+
validateAxisPosition(channel, config.position);
|
|
42
|
+
if (config.mode === "count" && (!Number.isInteger(config.count) || config.count <= 0)) throw new RangeError("Tick count must be a positive integer.");
|
|
43
|
+
if (
|
|
44
|
+
config.mode === "values" &&
|
|
45
|
+
(!Array.isArray(config.values) || !config.values.every(value =>
|
|
46
|
+
typeof value === "string" ||
|
|
47
|
+
typeof value === "boolean" ||
|
|
48
|
+
(typeof value === "number" && Number.isFinite(value))
|
|
49
|
+
))
|
|
50
|
+
) throw new TypeError("Tick values must be nominal values or finite numbers.");
|
|
51
|
+
validateNonNegativeFinite(config.length, "Tick length");
|
|
52
|
+
validateNonNegativeFinite(config.lineWidth, "Tick lineWidth");
|
|
53
|
+
validateNonEmptyString(config.color, "Tick color");
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function geometry(program, channel, config) {
|
|
57
|
+
const scale = program.resolvedScales[config.scale];
|
|
58
|
+
const bounds = resolveGraphicBounds(program);
|
|
59
|
+
const discrete = ["ordinal", "band", "point"].includes(scale?.type);
|
|
60
|
+
if ((
|
|
61
|
+
!["linear", "time", "ordinal", "band", "point"].includes(scale?.type) &&
|
|
62
|
+
!isTransformedScaleType(scale?.type)
|
|
63
|
+
) || !bounds) throw new Error("Axis ticks require a supported resolved scale and Canvas bounds.");
|
|
64
|
+
if (discrete && config.mode !== "values") throw new Error("Discrete axis ticks require explicit or inferred values, not count.");
|
|
65
|
+
const domain = scale.domain;
|
|
66
|
+
const values = valuesFromTickConfig(program, config);
|
|
67
|
+
if (discrete) {
|
|
68
|
+
const domainValues = new Set(domain);
|
|
69
|
+
if (!values.every(value => domainValues.has(value))) throw new RangeError("Tick values must be inside the scale domain.");
|
|
70
|
+
} else {
|
|
71
|
+
const low = Math.min(...domain), high = Math.max(...domain);
|
|
72
|
+
if (!values.every(value => value >= low && value <= high)) throw new RangeError("Tick values must be inside the scale domain.");
|
|
73
|
+
}
|
|
74
|
+
const positions = discrete
|
|
75
|
+
? mapOrdinalPositionValues(values, scale)
|
|
76
|
+
: mapContinuousScaleValues(values, scale);
|
|
77
|
+
const resolved = {
|
|
78
|
+
values,
|
|
79
|
+
...resolveAxisTickGeometry({
|
|
80
|
+
bounds,
|
|
81
|
+
channel,
|
|
82
|
+
position: config.position,
|
|
83
|
+
positions,
|
|
84
|
+
length: config.length
|
|
85
|
+
})
|
|
86
|
+
};
|
|
87
|
+
const canvas = program.graphicSpec.objects.canvas?.properties;
|
|
88
|
+
const coordinates = channel === "x"
|
|
89
|
+
? [...resolved.x1, ...resolved.x2, resolved.y1, resolved.y2]
|
|
90
|
+
: [resolved.x1, resolved.x2, ...resolved.y1, ...resolved.y2];
|
|
91
|
+
const newEdgeDoesNotFit =
|
|
92
|
+
(config.position === "top" && resolved.y2 < 0) ||
|
|
93
|
+
(config.position === "right" && resolved.x2 > canvas?.width);
|
|
94
|
+
if (
|
|
95
|
+
!canvas ||
|
|
96
|
+
coordinates.some(value => !Number.isFinite(value)) ||
|
|
97
|
+
newEdgeDoesNotFit
|
|
98
|
+
) {
|
|
99
|
+
throw new Error(`The ${channel}-axis ticks do not fit the Canvas margin.`);
|
|
100
|
+
}
|
|
101
|
+
return resolved;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function makeEdit(channel) {
|
|
105
|
+
const op = channel === "x" ? "editXAxisTicks" : "editYAxisTicks";
|
|
106
|
+
return action({ op, description: `Edit concrete ${channel}-axis ticks.` }, function (args = {}) {
|
|
107
|
+
validateOptions(args, op, false);
|
|
108
|
+
const id = `${channel}AxisTicks`;
|
|
109
|
+
if (this.graphicSpec.objects[id]?.type !== "line") throw new Error(`${op} requires existing axis ticks.`);
|
|
110
|
+
const previous = this.guideConfigs.axis?.[channel]?.ticks;
|
|
111
|
+
if (!previous) throw new Error(`${op} requires tick configuration.`);
|
|
112
|
+
const explicitMode = Object.hasOwn(args, "values") || Object.hasOwn(args, "count");
|
|
113
|
+
const inferredValues = !explicitMode && previous.inferredValues === true
|
|
114
|
+
? inferHistogramBoundaries(this, channel, previous.scale) ??
|
|
115
|
+
(["ordinal", "band", "point"].includes(
|
|
116
|
+
this.resolvedScales[previous.scale]?.type
|
|
117
|
+
)
|
|
118
|
+
? this.resolvedScales[previous.scale].domain
|
|
119
|
+
: undefined)
|
|
120
|
+
: undefined;
|
|
121
|
+
const mode = Object.hasOwn(args, "values") || inferredValues !== undefined
|
|
122
|
+
? "values"
|
|
123
|
+
: Object.hasOwn(args, "count") ? "count" : previous.mode;
|
|
124
|
+
const config = {
|
|
125
|
+
...previous,
|
|
126
|
+
...args,
|
|
127
|
+
...(inferredValues === undefined ? {} : { values: inferredValues }),
|
|
128
|
+
...(explicitMode ? { inferredValues: false } : {}),
|
|
129
|
+
mode
|
|
130
|
+
};
|
|
131
|
+
if (mode === "values") delete config.count; else delete config.values;
|
|
132
|
+
validateConfig(channel, config);
|
|
133
|
+
const resolved = geometry(this, channel, config);
|
|
134
|
+
let next = this._withGuideConfig(channel, config).editGraphics({ target: id, property: "length", value: resolved.values.length });
|
|
135
|
+
for (const property of ["x1", "y1", "x2", "y2"]) next = next.editGraphics({ target: id, property, value: resolved[property] });
|
|
136
|
+
return next.editGraphics({ target: id, property: "stroke", value: config.color }).editGraphics({ target: id, property: "strokeWidth", value: config.lineWidth });
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const editXAxisTicks = makeEdit("x"), editYAxisTicks = makeEdit("y");
|
|
141
|
+
|
|
142
|
+
function makeCreate(channel) {
|
|
143
|
+
const op = channel === "x" ? "createXAxisTicks" : "createYAxisTicks";
|
|
144
|
+
const edit = channel === "x" ? "editXAxisTicks" : "editYAxisTicks";
|
|
145
|
+
return action({ op, description: `Create concrete ${channel}-axis ticks.` }, function (args = {}) {
|
|
146
|
+
validateOptions(args, op, true);
|
|
147
|
+
const scale = validateUserId(args.scale ?? channel, "Scale id");
|
|
148
|
+
const existingGuide = this.semanticSpec.guides.axis?.[channel]?.scale;
|
|
149
|
+
if (existingGuide && existingGuide !== scale) throw new Error(`${op} conflicts with the existing axis scale.`);
|
|
150
|
+
const id = `${channel}AxisTicks`;
|
|
151
|
+
if (this.graphicSpec.objects[id]) throw new Error(`${op} requires missing axis ticks.`);
|
|
152
|
+
const resolvedScale = this.resolvedScales[scale];
|
|
153
|
+
const inferredValues =
|
|
154
|
+
Object.hasOwn(args, "count") || Object.hasOwn(args, "values")
|
|
155
|
+
? undefined
|
|
156
|
+
: ["ordinal", "band", "point"].includes(resolvedScale?.type)
|
|
157
|
+
? resolvedScale.domain
|
|
158
|
+
: inferHistogramBoundaries(this, channel, scale);
|
|
159
|
+
const options =
|
|
160
|
+
inferredValues === undefined ? args : { ...args, values: inferredValues };
|
|
161
|
+
const config = { scale, position: defaultAxisPosition(channel), length: DEFAULTS.length, color: DEFAULTS.color, lineWidth: DEFAULTS.lineWidth, ...options, inferredValues: inferredValues !== undefined, mode: Object.hasOwn(options, "values") ? "values" : "count" };
|
|
162
|
+
if (config.mode === "values") delete config.count; else config.count ??= DEFAULTS.count;
|
|
163
|
+
validateConfig(channel, config); geometry(this, channel, config);
|
|
164
|
+
return this.editSemantic({ property: `guide.axis.${channel}.scale`, value: scale })
|
|
165
|
+
.createGraphics({
|
|
166
|
+
id,
|
|
167
|
+
type: "line",
|
|
168
|
+
length: 0,
|
|
169
|
+
...resolvePlotGraphicPlacement(this)
|
|
170
|
+
})
|
|
171
|
+
._withGuideConfig(channel, config)[edit]();
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const createXAxisTicks = makeCreate("x"), createYAxisTicks = makeCreate("y");
|
|
176
|
+
export function registerAxisTickActions(Class) {
|
|
177
|
+
Class.prototype.editXAxisTicks = editXAxisTicks; Class.prototype.editYAxisTicks = editYAxisTicks;
|
|
178
|
+
Class.prototype.createXAxisTicks = createXAxisTicks; Class.prototype.createYAxisTicks = createYAxisTicks;
|
|
179
|
+
}
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
import { action } from "../../../core/action.js";
|
|
2
|
+
import { validateUserId } from "../../../core/identifiers.js";
|
|
3
|
+
import {
|
|
4
|
+
validateKeys,
|
|
5
|
+
validateNonEmptyString,
|
|
6
|
+
validateNonNegativeFinite,
|
|
7
|
+
validatePositiveFinite
|
|
8
|
+
} from "../../../core/validation.js";
|
|
9
|
+
import { resolveGraphicBounds } from "../../../layout/canvas.js";
|
|
10
|
+
import {
|
|
11
|
+
isTransformedScaleType,
|
|
12
|
+
mapContinuousScaleValues,
|
|
13
|
+
mapOrdinalPositionValues
|
|
14
|
+
} from "../../../grammar/scales.js";
|
|
15
|
+
import { DEFAULT_COLORS, DEFAULT_FONT_FAMILY } from
|
|
16
|
+
"../../../theme/defaults.js";
|
|
17
|
+
import { findDataset } from "../../../selectors/datasets.js";
|
|
18
|
+
import { formatAggregateTitle } from "../../../grammar/aggregate.js";
|
|
19
|
+
import {
|
|
20
|
+
defaultAxisPosition,
|
|
21
|
+
defaultAxisTitleRotation,
|
|
22
|
+
resolveAxisTitleGeometry,
|
|
23
|
+
validateAxisPosition
|
|
24
|
+
} from "./policy.js";
|
|
25
|
+
import { resolvePlotGraphicPlacement } from
|
|
26
|
+
"../../../materialization/graphicHierarchy.js";
|
|
27
|
+
|
|
28
|
+
const CREATE_OPTIONS = Object.freeze([
|
|
29
|
+
"text", "scale", "position", "at", "offset", "rotation", "color",
|
|
30
|
+
"fontSize", "fontFamily", "fontWeight"
|
|
31
|
+
]);
|
|
32
|
+
const EDIT_OPTIONS = CREATE_OPTIONS.filter(key => key !== "scale");
|
|
33
|
+
const DEFAULTS = Object.freeze({
|
|
34
|
+
color: DEFAULT_COLORS.text,
|
|
35
|
+
fontSize: 13,
|
|
36
|
+
fontFamily: DEFAULT_FONT_FAMILY,
|
|
37
|
+
fontWeight: 600
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
function validateText(text) {
|
|
41
|
+
return validateNonEmptyString(text, "Axis title text");
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function validateConfig(channel, config) {
|
|
45
|
+
validateAxisPosition(channel, config.position);
|
|
46
|
+
if (!["start", "center", "end"].includes(config.at) && !Number.isFinite(config.at)) throw new TypeError("Axis title at must be start, center, end, or a finite number.");
|
|
47
|
+
validateNonNegativeFinite(config.offset, "Axis title offset");
|
|
48
|
+
if (!Number.isFinite(config.rotation)) throw new TypeError("Axis title rotation must be finite radians.");
|
|
49
|
+
validateNonEmptyString(config.color, "Axis title color");
|
|
50
|
+
validatePositiveFinite(config.fontSize, "Axis title fontSize");
|
|
51
|
+
validateNonEmptyString(config.fontFamily, "Axis title fontFamily");
|
|
52
|
+
if (typeof config.fontWeight !== "string" && !Number.isFinite(config.fontWeight)) throw new TypeError("Axis title fontWeight must be a string or number.");
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function inferText(program, channel, scaleId) {
|
|
56
|
+
const titles = new Set();
|
|
57
|
+
const primaryTitles = new Set();
|
|
58
|
+
for (const layer of program.semanticSpec.layers) {
|
|
59
|
+
const encoding = layer.encoding?.[channel];
|
|
60
|
+
if (
|
|
61
|
+
encoding?.scale === scaleId &&
|
|
62
|
+
typeof encoding.field === "string" &&
|
|
63
|
+
encoding.field.length
|
|
64
|
+
) {
|
|
65
|
+
const dataset = findDataset(program, layer.data);
|
|
66
|
+
const density = dataset?.transform?.length === 1 &&
|
|
67
|
+
dataset.transform[0].type === "density"
|
|
68
|
+
? dataset.transform[0]
|
|
69
|
+
: undefined;
|
|
70
|
+
const densityTitle = density === undefined
|
|
71
|
+
? undefined
|
|
72
|
+
: encoding.field === density.as?.[0]
|
|
73
|
+
? density.field
|
|
74
|
+
: encoding.field === density.as?.[1]
|
|
75
|
+
? "Density"
|
|
76
|
+
: undefined;
|
|
77
|
+
const interval = dataset?.transform?.length === 1 &&
|
|
78
|
+
dataset.transform[0].type === "interval"
|
|
79
|
+
? dataset.transform[0]
|
|
80
|
+
: undefined;
|
|
81
|
+
const intervalTitle = interval !== undefined &&
|
|
82
|
+
Object.values(interval.as).includes(encoding.field)
|
|
83
|
+
? `${interval.center}(${interval.field})`
|
|
84
|
+
: undefined;
|
|
85
|
+
const box = dataset?.transform?.length === 1 &&
|
|
86
|
+
dataset.transform[0].type === "boxSummary"
|
|
87
|
+
? dataset.transform[0]
|
|
88
|
+
: undefined;
|
|
89
|
+
const boxTitle = box !== undefined &&
|
|
90
|
+
Object.values(box.as).includes(encoding.field)
|
|
91
|
+
? box.field
|
|
92
|
+
: undefined;
|
|
93
|
+
const title = encoding.title ?? densityTitle ?? intervalTitle ?? boxTitle ?? (encoding.aggregate === undefined
|
|
94
|
+
? encoding.field
|
|
95
|
+
: formatAggregateTitle(encoding.aggregate, encoding.field));
|
|
96
|
+
titles.add(title);
|
|
97
|
+
if (layer.mark?.type === "point") primaryTitles.add(title);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
if (primaryTitles.size === 1) return [...primaryTitles][0];
|
|
101
|
+
if (titles.size !== 1) throw new Error(`Axis title text cannot be inferred for scale "${scaleId}".`);
|
|
102
|
+
return [...titles][0];
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function resolveGeometry(program, channel, config) {
|
|
106
|
+
const scale = program.resolvedScales[config.scale];
|
|
107
|
+
const bounds = resolveGraphicBounds(program);
|
|
108
|
+
const discrete = ["ordinal", "band", "point"].includes(scale?.type);
|
|
109
|
+
if ((
|
|
110
|
+
!["linear", "time", "ordinal", "band", "point"].includes(scale?.type) &&
|
|
111
|
+
!isTransformedScaleType(scale?.type)
|
|
112
|
+
) || !bounds) throw new Error("Axis title requires a supported resolved scale and Canvas bounds.");
|
|
113
|
+
let along;
|
|
114
|
+
if (config.at === "start") along = scale.range[0];
|
|
115
|
+
else if (config.at === "center") along = (scale.range[0] + scale.range[1]) / 2;
|
|
116
|
+
else if (config.at === "end") along = scale.range[1];
|
|
117
|
+
else {
|
|
118
|
+
if (discrete) {
|
|
119
|
+
if (!scale.domain.includes(config.at)) throw new RangeError("Axis title at value must be inside the scale domain.");
|
|
120
|
+
along = mapOrdinalPositionValues([config.at], scale)[0];
|
|
121
|
+
} else {
|
|
122
|
+
const low = Math.min(...scale.domain), high = Math.max(...scale.domain);
|
|
123
|
+
if (config.at < low || config.at > high) throw new RangeError("Axis title at value must be inside the scale domain.");
|
|
124
|
+
along = mapContinuousScaleValues([config.at], scale)[0];
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
const geometry = resolveAxisTitleGeometry({
|
|
128
|
+
bounds,
|
|
129
|
+
channel,
|
|
130
|
+
position: config.position,
|
|
131
|
+
along,
|
|
132
|
+
offset: config.offset
|
|
133
|
+
});
|
|
134
|
+
const text = program.semanticSpec.guides.axis?.[channel]?.title ?? "";
|
|
135
|
+
const width = [...text].length * config.fontSize * 0.6;
|
|
136
|
+
const height = config.fontSize;
|
|
137
|
+
const cosine = Math.cos(config.rotation);
|
|
138
|
+
const sine = Math.sin(config.rotation);
|
|
139
|
+
const extentX = Math.abs(cosine) * width / 2 + Math.abs(sine) * height / 2;
|
|
140
|
+
const extentY = Math.abs(sine) * width / 2 + Math.abs(cosine) * height / 2;
|
|
141
|
+
const canvas = program.graphicSpec.objects.canvas?.properties;
|
|
142
|
+
const newEdgeFits = config.position === "top"
|
|
143
|
+
? geometry.y - extentY >= 0
|
|
144
|
+
: config.position === "right"
|
|
145
|
+
? geometry.x + extentX <= canvas?.width
|
|
146
|
+
: true;
|
|
147
|
+
if (!canvas || !newEdgeFits) {
|
|
148
|
+
throw new Error(`The ${channel}-axis title does not fit the Canvas margin.`);
|
|
149
|
+
}
|
|
150
|
+
return geometry;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function names(channel) {
|
|
154
|
+
const prefix = channel === "x" ? "X" : "Y";
|
|
155
|
+
return { create: `create${prefix}AxisTitle`, edit: `edit${prefix}AxisTitle`, graphic: `${channel}AxisTitle` };
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function makeEdit(channel) {
|
|
159
|
+
const operation = names(channel);
|
|
160
|
+
return action({ op: operation.edit, description: `Edit the ${channel}-axis title.` }, function (args = {}) {
|
|
161
|
+
validateKeys(args, EDIT_OPTIONS, operation.edit);
|
|
162
|
+
if (this.graphicSpec.objects[operation.graphic]?.type !== "text") throw new Error(`${operation.edit} requires an existing axis title.`);
|
|
163
|
+
const previous = this.guideConfigs.axis?.[channel]?.title;
|
|
164
|
+
if (!previous) throw new Error(`${operation.edit} requires title configuration.`);
|
|
165
|
+
const { text, ...appearance } = args;
|
|
166
|
+
const explicitText = Object.hasOwn(args, "text");
|
|
167
|
+
const explicitRotation = Object.hasOwn(args, "rotation");
|
|
168
|
+
const position = appearance.position ?? previous.position;
|
|
169
|
+
const inferredRotation = explicitRotation
|
|
170
|
+
? false
|
|
171
|
+
: previous.inferredRotation === true;
|
|
172
|
+
const config = {
|
|
173
|
+
...previous,
|
|
174
|
+
...appearance,
|
|
175
|
+
position,
|
|
176
|
+
rotation: inferredRotation
|
|
177
|
+
? defaultAxisTitleRotation(channel, position)
|
|
178
|
+
: appearance.rotation ?? previous.rotation,
|
|
179
|
+
inferredRotation,
|
|
180
|
+
...(explicitText ? { inferredText: false } : {})
|
|
181
|
+
};
|
|
182
|
+
validateConfig(channel, config);
|
|
183
|
+
let next = this;
|
|
184
|
+
const resolvedTitle = explicitText
|
|
185
|
+
? validateText(text)
|
|
186
|
+
: config.inferredText === true
|
|
187
|
+
? inferText(this, channel, config.scale)
|
|
188
|
+
: this.semanticSpec.guides.axis?.[channel]?.title;
|
|
189
|
+
if (resolvedTitle !== this.semanticSpec.guides.axis?.[channel]?.title) {
|
|
190
|
+
next = next.editSemantic({
|
|
191
|
+
property: `guide.axis.${channel}.title`,
|
|
192
|
+
value: validateText(resolvedTitle)
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
const resolvedText = validateText(next.semanticSpec.guides.axis?.[channel]?.title);
|
|
196
|
+
const geometry = resolveGeometry(next, channel, config);
|
|
197
|
+
next = next._withGuideConfig(channel, "title", config);
|
|
198
|
+
const properties = {
|
|
199
|
+
x: geometry.x, y: geometry.y, text: resolvedText, fill: config.color,
|
|
200
|
+
fontSize: config.fontSize, fontFamily: config.fontFamily,
|
|
201
|
+
fontWeight: config.fontWeight, textAlign: "center", textBaseline: "middle",
|
|
202
|
+
rotation: config.rotation
|
|
203
|
+
};
|
|
204
|
+
for (const [property, value] of Object.entries(properties)) next = next.editGraphics({ target: operation.graphic, property, value });
|
|
205
|
+
return next;
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const editXAxisTitle = makeEdit("x");
|
|
210
|
+
const editYAxisTitle = makeEdit("y");
|
|
211
|
+
|
|
212
|
+
function makeCreate(channel) {
|
|
213
|
+
const operation = names(channel);
|
|
214
|
+
return action({ op: operation.create, description: `Create the ${channel}-axis title.` }, function (args = {}) {
|
|
215
|
+
validateKeys(args, CREATE_OPTIONS, operation.create);
|
|
216
|
+
const scale = validateUserId(args.scale ?? channel, "Scale id");
|
|
217
|
+
const guideScale = this.semanticSpec.guides.axis?.[channel]?.scale;
|
|
218
|
+
if (guideScale && guideScale !== scale) throw new Error(`${operation.create} conflicts with the existing axis scale.`);
|
|
219
|
+
if (this.graphicSpec.objects[operation.graphic]) throw new Error(`${operation.create} requires a missing axis title.`);
|
|
220
|
+
const inferredText = !Object.hasOwn(args, "text");
|
|
221
|
+
const text = validateText(args.text ?? inferText(this, channel, scale));
|
|
222
|
+
const position = args.position ?? defaultAxisPosition(channel);
|
|
223
|
+
const inferredRotation = !Object.hasOwn(args, "rotation");
|
|
224
|
+
const config = {
|
|
225
|
+
scale,
|
|
226
|
+
inferredText,
|
|
227
|
+
position,
|
|
228
|
+
at: "center",
|
|
229
|
+
offset: channel === "x" ? 42 : 52,
|
|
230
|
+
rotation: inferredRotation
|
|
231
|
+
? defaultAxisTitleRotation(channel, position)
|
|
232
|
+
: args.rotation,
|
|
233
|
+
inferredRotation,
|
|
234
|
+
color: DEFAULTS.color,
|
|
235
|
+
fontSize: DEFAULTS.fontSize,
|
|
236
|
+
fontFamily: DEFAULTS.fontFamily,
|
|
237
|
+
fontWeight: DEFAULTS.fontWeight,
|
|
238
|
+
...Object.fromEntries(Object.entries(args).filter(
|
|
239
|
+
([key]) => !["text", "scale", "position", "rotation"].includes(key)
|
|
240
|
+
))
|
|
241
|
+
};
|
|
242
|
+
validateConfig(channel, config);
|
|
243
|
+
resolveGeometry(this, channel, config);
|
|
244
|
+
return this
|
|
245
|
+
.editSemantic({ property: `guide.axis.${channel}.scale`, value: scale })
|
|
246
|
+
.editSemantic({ property: `guide.axis.${channel}.title`, value: text })
|
|
247
|
+
.createGraphics({
|
|
248
|
+
id: operation.graphic,
|
|
249
|
+
type: "text",
|
|
250
|
+
...resolvePlotGraphicPlacement(this)
|
|
251
|
+
})
|
|
252
|
+
._withGuideConfig(channel, "title", config)[operation.edit]();
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const createXAxisTitle = makeCreate("x");
|
|
257
|
+
const createYAxisTitle = makeCreate("y");
|
|
258
|
+
|
|
259
|
+
export function registerAxisTitleActions(Class) {
|
|
260
|
+
Class.prototype.createXAxisTitle = createXAxisTitle;
|
|
261
|
+
Class.prototype.createYAxisTitle = createYAxisTitle;
|
|
262
|
+
Class.prototype.editXAxisTitle = editXAxisTitle;
|
|
263
|
+
Class.prototype.editYAxisTitle = editYAxisTitle;
|
|
264
|
+
}
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import { action } from "../../../core/action.js";
|
|
2
|
+
import { isPlainObject } from "../../../core/immutable.js";
|
|
3
|
+
import { validateKeys } from "../../../core/validation.js";
|
|
4
|
+
import {
|
|
5
|
+
gridNames,
|
|
6
|
+
editGridConfig,
|
|
7
|
+
refreshGridConfig,
|
|
8
|
+
resolveGridConfig,
|
|
9
|
+
resolveGridGeometry,
|
|
10
|
+
resolveGridResources,
|
|
11
|
+
validateGridCreateArgs,
|
|
12
|
+
validateGridEditArgs
|
|
13
|
+
} from "./resolve.js";
|
|
14
|
+
|
|
15
|
+
const AGGREGATE_OPTIONS = Object.freeze(["horizontal", "vertical"]);
|
|
16
|
+
|
|
17
|
+
function makeRematerialize(direction) {
|
|
18
|
+
const operation = gridNames(direction);
|
|
19
|
+
return action(
|
|
20
|
+
{
|
|
21
|
+
op: operation.rematerialize,
|
|
22
|
+
description: `Recompute concrete ${direction} grid lines.`
|
|
23
|
+
},
|
|
24
|
+
function (args = {}) {
|
|
25
|
+
if (!isPlainObject(args) || Object.keys(args).length !== 0) {
|
|
26
|
+
throw new TypeError(`${operation.rematerialize} does not accept options.`);
|
|
27
|
+
}
|
|
28
|
+
const storedConfig = this.guideConfigs.grid?.[direction];
|
|
29
|
+
const semantic = this.semanticSpec.guides.grid?.[direction];
|
|
30
|
+
if (
|
|
31
|
+
storedConfig === undefined ||
|
|
32
|
+
semantic?.scale !== storedConfig.scale ||
|
|
33
|
+
semantic.coordinate !== storedConfig.coordinate ||
|
|
34
|
+
this.graphicSpec.objects[operation.graphic]?.type !== "line"
|
|
35
|
+
) {
|
|
36
|
+
throw new Error(
|
|
37
|
+
`${operation.rematerialize} requires an existing ${direction} grid.`
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const config = refreshGridConfig(this, storedConfig);
|
|
42
|
+
const geometry = resolveGridGeometry(this, config);
|
|
43
|
+
let next = this._withGridConfig(direction, config).editGraphics({
|
|
44
|
+
target: operation.graphic,
|
|
45
|
+
property: "length",
|
|
46
|
+
value: geometry.values.length
|
|
47
|
+
});
|
|
48
|
+
for (const property of ["x1", "y1", "x2", "y2"]) {
|
|
49
|
+
next = next.editGraphics({
|
|
50
|
+
target: operation.graphic,
|
|
51
|
+
property,
|
|
52
|
+
value: geometry[property]
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
return next
|
|
56
|
+
.editGraphics({
|
|
57
|
+
target: operation.graphic,
|
|
58
|
+
property: "stroke",
|
|
59
|
+
value: config.color
|
|
60
|
+
})
|
|
61
|
+
.editGraphics({
|
|
62
|
+
target: operation.graphic,
|
|
63
|
+
property: "strokeWidth",
|
|
64
|
+
value: config.lineWidth
|
|
65
|
+
})
|
|
66
|
+
.editGraphics({
|
|
67
|
+
target: operation.graphic,
|
|
68
|
+
property: "strokeDash",
|
|
69
|
+
value: geometry.values.map(() => config.strokeDash)
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function makeCreate(direction) {
|
|
76
|
+
const operation = gridNames(direction);
|
|
77
|
+
return action(
|
|
78
|
+
{
|
|
79
|
+
op: operation.create,
|
|
80
|
+
description: `Create a semantic and concrete ${direction} grid.`
|
|
81
|
+
},
|
|
82
|
+
function (args = {}) {
|
|
83
|
+
validateGridCreateArgs(args, operation.create);
|
|
84
|
+
if (
|
|
85
|
+
this.semanticSpec.guides.grid?.[direction] !== undefined ||
|
|
86
|
+
this.graphicSpec.objects[operation.graphic] !== undefined
|
|
87
|
+
) {
|
|
88
|
+
throw new Error(`${operation.create} requires a missing grid.`);
|
|
89
|
+
}
|
|
90
|
+
const resources = resolveGridResources(this, direction, args);
|
|
91
|
+
const config = resolveGridConfig(this, direction, args, resources);
|
|
92
|
+
resolveGridGeometry(this, config);
|
|
93
|
+
|
|
94
|
+
return this
|
|
95
|
+
.editSemantic({
|
|
96
|
+
property: `guide.grid.${direction}.scale`,
|
|
97
|
+
value: resources.scale
|
|
98
|
+
})
|
|
99
|
+
.editSemantic({
|
|
100
|
+
property: `guide.grid.${direction}.coordinate`,
|
|
101
|
+
value: resources.coordinate
|
|
102
|
+
})
|
|
103
|
+
.createGraphics({
|
|
104
|
+
id: operation.graphic,
|
|
105
|
+
type: "line",
|
|
106
|
+
length: 0,
|
|
107
|
+
...(resources.parent === undefined
|
|
108
|
+
? {}
|
|
109
|
+
: { parent: resources.parent }),
|
|
110
|
+
before: resources.before
|
|
111
|
+
})
|
|
112
|
+
._withGridConfig(direction, config)[operation.rematerialize]();
|
|
113
|
+
}
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function makeEdit(direction) {
|
|
118
|
+
const operation = gridNames(direction);
|
|
119
|
+
const editOperation = `edit${direction === "horizontal" ? "Horizontal" : "Vertical"}Grid`;
|
|
120
|
+
return action(
|
|
121
|
+
{
|
|
122
|
+
op: editOperation,
|
|
123
|
+
description: `Edit the existing ${direction} grid.`
|
|
124
|
+
},
|
|
125
|
+
function (args = {}) {
|
|
126
|
+
validateGridEditArgs(args, editOperation);
|
|
127
|
+
const storedConfig = this.guideConfigs.grid?.[direction];
|
|
128
|
+
const semantic = this.semanticSpec.guides.grid?.[direction];
|
|
129
|
+
if (
|
|
130
|
+
storedConfig === undefined ||
|
|
131
|
+
semantic?.scale !== storedConfig.scale ||
|
|
132
|
+
semantic.coordinate !== storedConfig.coordinate ||
|
|
133
|
+
this.graphicSpec.objects[operation.graphic]?.type !== "line"
|
|
134
|
+
) {
|
|
135
|
+
throw new Error(`${editOperation} requires an existing ${direction} grid.`);
|
|
136
|
+
}
|
|
137
|
+
const config = editGridConfig(storedConfig, args);
|
|
138
|
+
return this
|
|
139
|
+
._withGridConfig(direction, config)
|
|
140
|
+
[operation.rematerialize]();
|
|
141
|
+
}
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function normalizeDirection(value, direction) {
|
|
146
|
+
if (value === false) return undefined;
|
|
147
|
+
if (value === true || value === undefined) return {};
|
|
148
|
+
if (!isPlainObject(value)) {
|
|
149
|
+
throw new TypeError(
|
|
150
|
+
`createGrid ${direction} must be a boolean or plain object.`
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
return value;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const rematerializeHorizontalGrid = makeRematerialize("horizontal");
|
|
157
|
+
const rematerializeVerticalGrid = makeRematerialize("vertical");
|
|
158
|
+
const createHorizontalGrid = makeCreate("horizontal");
|
|
159
|
+
const createVerticalGrid = makeCreate("vertical");
|
|
160
|
+
const editHorizontalGrid = makeEdit("horizontal");
|
|
161
|
+
const editVerticalGrid = makeEdit("vertical");
|
|
162
|
+
|
|
163
|
+
const createGrid = action(
|
|
164
|
+
{
|
|
165
|
+
op: "createGrid",
|
|
166
|
+
description: "Create selected Cartesian grid directions."
|
|
167
|
+
},
|
|
168
|
+
function (args = {}) {
|
|
169
|
+
if (!isPlainObject(args)) {
|
|
170
|
+
throw new TypeError("createGrid options must be a plain object.");
|
|
171
|
+
}
|
|
172
|
+
validateKeys(args, AGGREGATE_OPTIONS, "createGrid");
|
|
173
|
+
const horizontal = normalizeDirection(args.horizontal, "horizontal");
|
|
174
|
+
const vertical = args.vertical === undefined
|
|
175
|
+
? undefined
|
|
176
|
+
: normalizeDirection(args.vertical, "vertical");
|
|
177
|
+
if (horizontal === undefined && vertical === undefined) {
|
|
178
|
+
throw new Error("createGrid requires at least one selected direction.");
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
let next = this;
|
|
182
|
+
if (horizontal !== undefined) {
|
|
183
|
+
next = next.createHorizontalGrid(horizontal);
|
|
184
|
+
}
|
|
185
|
+
if (vertical !== undefined) {
|
|
186
|
+
next = next.createVerticalGrid(vertical);
|
|
187
|
+
}
|
|
188
|
+
return next;
|
|
189
|
+
}
|
|
190
|
+
);
|
|
191
|
+
|
|
192
|
+
const rematerializeGrid = action(
|
|
193
|
+
{
|
|
194
|
+
op: "rematerializeGrid",
|
|
195
|
+
description: "Recompute every existing grid direction."
|
|
196
|
+
},
|
|
197
|
+
function (args = {}) {
|
|
198
|
+
if (!isPlainObject(args) || Object.keys(args).length !== 0) {
|
|
199
|
+
throw new TypeError("rematerializeGrid does not accept options.");
|
|
200
|
+
}
|
|
201
|
+
let next = this;
|
|
202
|
+
let count = 0;
|
|
203
|
+
if (this.guideConfigs.grid?.horizontal !== undefined) {
|
|
204
|
+
next = next.rematerializeHorizontalGrid();
|
|
205
|
+
count += 1;
|
|
206
|
+
}
|
|
207
|
+
if (this.guideConfigs.grid?.vertical !== undefined) {
|
|
208
|
+
next = next.rematerializeVerticalGrid();
|
|
209
|
+
count += 1;
|
|
210
|
+
}
|
|
211
|
+
if (count === 0) {
|
|
212
|
+
throw new Error("rematerializeGrid requires an existing grid.");
|
|
213
|
+
}
|
|
214
|
+
return next;
|
|
215
|
+
}
|
|
216
|
+
);
|
|
217
|
+
|
|
218
|
+
export function registerGridActions(ProgramClass) {
|
|
219
|
+
ProgramClass.prototype.createGrid = createGrid;
|
|
220
|
+
ProgramClass.prototype.createHorizontalGrid = createHorizontalGrid;
|
|
221
|
+
ProgramClass.prototype.createVerticalGrid = createVerticalGrid;
|
|
222
|
+
ProgramClass.prototype.editHorizontalGrid = editHorizontalGrid;
|
|
223
|
+
ProgramClass.prototype.editVerticalGrid = editVerticalGrid;
|
|
224
|
+
ProgramClass.prototype.rematerializeGrid = rematerializeGrid;
|
|
225
|
+
ProgramClass.prototype.rematerializeHorizontalGrid =
|
|
226
|
+
rematerializeHorizontalGrid;
|
|
227
|
+
ProgramClass.prototype.rematerializeVerticalGrid = rematerializeVerticalGrid;
|
|
228
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { registerGridActions } from "./grid.js";
|