ggaction 0.0.18 → 0.0.19
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 +50 -0
- package/README.md +6 -2
- package/knowledge/action-cards.json +1 -1
- package/knowledge/intent-taxonomy.json +1 -1
- package/knowledge/mcp-resources.json +1 -1
- package/package.json +1 -1
- package/src/actions/canvas/fitting.js +6 -2
- package/src/actions/charts/heatmap.js +4 -3
- package/src/actions/data/derived.js +11 -1
- package/src/actions/data/edit.js +2 -0
- package/src/actions/data/index.js +4 -0
- package/src/actions/data/regression.js +24 -16
- package/src/actions/data/regressionStatistics.js +73 -0
- package/src/actions/data/shared.js +5 -2
- package/src/actions/distributions/revision.js +24 -18
- package/src/actions/encodings/remove.js +11 -6
- package/src/actions/encodings/shared.js +26 -24
- package/src/actions/facets/actions.js +18 -7
- package/src/actions/facets/guides.js +2 -1
- package/src/actions/guides/axes/axes.js +30 -11
- package/src/actions/guides/axes/axis.js +58 -8
- package/src/actions/guides/axes/components.js +13 -13
- package/src/actions/guides/axes/edit.js +36 -24
- package/src/actions/guides/axes/labels.js +37 -27
- package/src/actions/guides/axes/lines.js +22 -20
- package/src/actions/guides/axes/remove.js +10 -6
- package/src/actions/guides/axes/tickGroups.js +8 -7
- package/src/actions/guides/axes/ticks.js +31 -22
- package/src/actions/guides/axes/titles.js +53 -60
- package/src/actions/guides/facadeAxes.js +33 -24
- package/src/actions/guides/facadeLegend.js +3 -3
- package/src/actions/guides/grids/grid.js +1 -1
- package/src/actions/guides/grids/resolve.js +48 -51
- package/src/actions/guides/legends/blocks.js +43 -19
- package/src/actions/guides/legends/categorical/actions.js +30 -7
- package/src/actions/guides/legends/categorical/components.js +3 -10
- package/src/actions/guides/legends/categorical/index.js +2 -0
- package/src/actions/guides/legends/categorical/layout.js +9 -13
- package/src/actions/guides/legends/categorical/options.js +8 -2
- package/src/actions/guides/legends/categorical/recipes.js +7 -3
- package/src/actions/guides/legends/categorical/resolve.js +9 -2
- package/src/actions/guides/legends/categorical/symbols.js +22 -8
- package/src/actions/guides/legends/colorSize.js +110 -0
- package/src/actions/guides/legends/combine.js +37 -0
- package/src/actions/guides/legends/continuous/colorSymbols.js +1 -1
- package/src/actions/guides/legends/continuous/common.js +1 -1
- package/src/actions/guides/legends/continuous/gradient.js +27 -13
- package/src/actions/guides/legends/continuous/opacity.js +16 -11
- package/src/actions/guides/legends/creation.js +2 -0
- package/src/actions/guides/legends/edit.js +1 -1
- package/src/actions/guides/legends/focused.js +6 -5
- package/src/actions/guides/legends/lane.js +1 -1
- package/src/actions/guides/legends/lifecycle.js +2 -1
- package/src/actions/guides/legends/remove.js +6 -3
- package/src/actions/guides/legends/size.js +16 -6
- package/src/actions/guides/legends/strokeWidth.js +2 -0
- package/src/actions/guides/legends/target.js +2 -79
- package/src/actions/guides/legends/transition.js +24 -5
- package/src/actions/guides/tickValues.js +8 -5
- package/src/actions/marks/bar/edit.js +23 -12
- package/src/actions/marks/bar/materialize.js +6 -1
- package/src/actions/marks/point/materialize.js +5 -1
- package/src/actions/marks/remove.js +10 -10
- package/src/actions/primitives/createGraphics.js +3 -2
- package/src/actions/primitives/editGraphics.js +15 -11
- package/src/actions/primitives/graphicProperties.js +1 -6
- package/src/actions/primitives/semanticAction.js +27 -1
- package/src/actions/primitives/semanticValidation/guide.js +11 -1
- package/src/actions/primitives/semanticValidation/layer.js +4 -0
- package/src/actions/regression/edit.js +15 -3
- package/src/actions/scales/channels.js +2 -2
- package/src/actions/scales/create.js +3 -3
- package/src/actions/scales/definitions.js +1 -1
- package/src/actions/scales/edit.js +1 -1
- package/src/actions/scales/editPolicy.js +3 -0
- package/src/actions/scales/patch.js +1 -1
- package/src/actions/scales/quantitativeColor.js +8 -3
- package/src/actions/theme/reconcile.js +50 -23
- package/src/core/compositionState.js +2 -2
- package/src/core/graphicProperties.js +1 -0
- package/src/core/programState.js +4 -0
- package/src/core/resourceReferences.js +2 -2
- package/src/core/validation.js +9 -0
- package/src/core/vocabulary.js +3 -0
- package/src/grammar/axisTextPlacement.js +1 -0
- package/src/grammar/colorSizeLegend.js +39 -0
- package/src/grammar/computed.js +23 -1
- package/src/grammar/datasetSchema.js +10 -2
- package/src/grammar/displayLabels.js +8 -0
- package/src/grammar/facets/guides.js +54 -31
- package/src/grammar/facets/legendIntent.js +65 -0
- package/src/grammar/guideDash.js +1 -0
- package/src/grammar/legendBlocks.js +1 -0
- package/src/grammar/legendBorder.js +9 -0
- package/src/grammar/legendClip.js +19 -0
- package/src/grammar/legendCombinations.js +45 -0
- package/src/grammar/legendGradient.js +9 -0
- package/src/grammar/legendGroups.js +1 -1
- package/src/grammar/legendOpacity.js +6 -0
- package/src/grammar/pointShapes.js +16 -2
- package/src/grammar/regression/derive.js +9 -1
- package/src/grammar/regression/models.js +60 -0
- package/src/grammar/regression/parameters.js +9 -1
- package/src/grammar/regression/statistics.js +64 -0
- package/src/grammar/scales/color.js +1 -1
- package/src/grammar/scales/colorConsumers.js +1 -1
- package/src/grammar/scales/definition.js +1 -1
- package/src/grammar/scales/types.js +1 -1
- package/src/grammar/schemas/concreteGraphic.js +3 -0
- package/src/grammar/schemas/graphic.js +3 -0
- package/src/grammar/schemas/graphicBounds.js +31 -7
- package/src/grammar/schemas/semanticPath.js +17 -2
- package/src/grammar/summary.js +1 -1
- package/src/grammar/timeUnit.js +6 -4
- package/src/grammar/transformTopology.js +1 -0
- package/src/grammar/transforms.js +11 -1
- package/src/inspectionDescriptors.js +1 -1
- package/src/layout/guideCollisions.js +9 -1
- package/src/layout/legendGroup.js +1 -1
- package/src/materialization/compositionSnapshot.js +2 -7
- package/src/materialization/coordinateBounds.js +17 -4
- package/src/materialization/facetGuides/edit.js +30 -0
- package/src/materialization/facetGuides/layout.js +1 -1
- package/src/materialization/facetGuides/owner.js +125 -0
- package/src/materialization/facetGuides/ownerLayout.js +71 -0
- package/src/materialization/facetGuides/placement.js +52 -6
- package/src/materialization/facetGuides/preparation.js +7 -0
- package/src/materialization/facets.js +1 -1
- package/src/materialization/graphicHierarchy.js +5 -1
- package/src/materialization/graphicTranslation.js +73 -0
- package/src/materialization/guides/layout.js +1 -1
- package/src/materialization/guides/resources.js +1 -1
- package/src/materialization/legendBorder.js +28 -0
- package/src/materialization/legendClip.js +76 -0
- package/src/materialization/legendGroups.js +1 -1
- package/src/materialization/legends.js +1 -1
- package/src/materialization/scaleGuideDependencies.js +13 -14
- package/src/materialization/scales/map.js +3 -0
- package/src/materialization/scales/resolve.js +2 -0
- package/src/materialization/typography.js +10 -2
- package/src/persistence/validation.js +174 -1
- package/src/persistence.js +1 -1
- package/src/renderers/canvas/index.js +20 -10
- package/src/renderers/canvas/isolated.js +166 -0
- package/src/renderers/canvas/limits.js +3 -0
- package/src/renderers/canvas/rect.js +1 -1
- package/src/renderers/pdf.js +2 -2
- package/src/renderers/png.js +2 -2
- package/src/renderers/svg.js +10 -3
- package/src/selectors/axes.js +1 -0
- package/src/selectors/graphicCapacity.js +38 -0
- package/src/version.js +1 -1
- package/types/basic.d.ts +2 -0
- package/types/index.d.ts +5 -0
- package/types/program.d.ts +206 -61
|
@@ -35,9 +35,11 @@ export function requireDerivedDataset(program, id, type) {
|
|
|
35
35
|
return { id: validatedId, dataset, source, transform: dataset.transform[0] };
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
-
export function derivedMaterializer(op, description, type, derive, resolve) {
|
|
38
|
+
export function derivedMaterializer(op, description, type, derive, resolve, { before, after } = {}) {
|
|
39
39
|
return action({ op, description }, function (args = {}) {
|
|
40
40
|
validateKeys(args, MATERIALIZE_OPTIONS, op);
|
|
41
|
+
const existing = before?.(this, args);
|
|
42
|
+
if (existing !== undefined) return existing;
|
|
41
43
|
const { id, source, transform } = requireDerivedDataset(
|
|
42
44
|
this,
|
|
43
45
|
args.id,
|
|
@@ -51,6 +53,7 @@ export function derivedMaterializer(op, description, type, derive, resolve) {
|
|
|
51
53
|
const values = Array.isArray(result) ? result : result.values;
|
|
52
54
|
const schema = deriveTransformSchema(source.schema, transform, values, {
|
|
53
55
|
sourceId: source.id,
|
|
56
|
+
sourceTransform: source.transform?.[0],
|
|
54
57
|
ownerId: id
|
|
55
58
|
});
|
|
56
59
|
validateRowsAgainstSchema(values, schema, `Derived dataset "${id}"`);
|
|
@@ -75,7 +78,7 @@ export function derivedMaterializer(op, description, type, derive, resolve) {
|
|
|
75
78
|
report
|
|
76
79
|
);
|
|
77
80
|
}
|
|
78
|
-
return next;
|
|
81
|
+
return after === undefined ? next : after(next, { id, source, transform, result });
|
|
79
82
|
});
|
|
80
83
|
}
|
|
81
84
|
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { cartesianAxisOwners, axisTargetArgs } from "../../selectors/axes.js";
|
|
1
2
|
import { DEFAULT_TICK_COUNT } from "../guides/tickValues.js";
|
|
2
3
|
import { findLayer } from "../../selectors/layers.js";
|
|
3
4
|
import { findSemanticScale } from "../../selectors/scales.js";
|
|
@@ -296,39 +297,44 @@ export function clearCartesianPositions(program, id) {
|
|
|
296
297
|
return next;
|
|
297
298
|
}
|
|
298
299
|
|
|
299
|
-
function rebindAxis(program, channel, {
|
|
300
|
+
function rebindAxis(program, channel, options) {
|
|
301
|
+
return cartesianAxisOwners(program).filter(owner => owner.channel === channel)
|
|
302
|
+
.reduce((next, owner) => rebindAxisOwner(next, channel, owner.id, options), program);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function rebindAxisOwner(program, channel, axisId, {
|
|
300
306
|
previousScale,
|
|
301
307
|
nextScale,
|
|
302
308
|
previousTitle,
|
|
303
309
|
nextTitle
|
|
304
310
|
}) {
|
|
305
|
-
const semantic = program.semanticSpec.guides.axis?.[
|
|
311
|
+
const semantic = program.semanticSpec.guides.axis?.[axisId];
|
|
306
312
|
if (semantic?.scale !== previousScale) return program;
|
|
307
313
|
let next = program.editSemantic({
|
|
308
|
-
property: `guide.axis.${
|
|
314
|
+
property: `guide.axis.${axisId}.scale`,
|
|
309
315
|
value: nextScale
|
|
310
316
|
});
|
|
311
317
|
if (semantic.title === previousTitle) {
|
|
312
318
|
next = next.editSemantic({
|
|
313
|
-
property: `guide.axis.${
|
|
319
|
+
property: `guide.axis.${axisId}.title`,
|
|
314
320
|
value: nextTitle
|
|
315
321
|
});
|
|
316
322
|
}
|
|
317
323
|
const scale = next.resolvedScales[nextScale];
|
|
318
324
|
const ticks = normalizeTickConfig(
|
|
319
|
-
next.guideConfigs.axis?.[
|
|
325
|
+
next.guideConfigs.axis?.[axisId]?.ticks,
|
|
320
326
|
scale
|
|
321
327
|
);
|
|
322
328
|
if (ticks !== undefined) {
|
|
323
|
-
next = next._withGuideConfig(
|
|
329
|
+
next = next._withGuideConfig(axisId, "ticks", {
|
|
324
330
|
...ticks,
|
|
325
331
|
scale: nextScale
|
|
326
332
|
});
|
|
327
333
|
}
|
|
328
|
-
const labels = next.guideConfigs.axis?.[
|
|
334
|
+
const labels = next.guideConfigs.axis?.[axisId]?.labels;
|
|
329
335
|
if (labels !== undefined) {
|
|
330
|
-
const tickConfig = next.guideConfigs.axis[
|
|
331
|
-
next = next._withGuideConfig(
|
|
336
|
+
const tickConfig = next.guideConfigs.axis[axisId].ticks;
|
|
337
|
+
next = next._withGuideConfig(axisId, "labels", {
|
|
332
338
|
...labels,
|
|
333
339
|
scale: nextScale,
|
|
334
340
|
mode: tickConfig.mode,
|
|
@@ -338,30 +344,30 @@ function rebindAxis(program, channel, {
|
|
|
338
344
|
: { count: tickConfig.count })
|
|
339
345
|
});
|
|
340
346
|
if (tickConfig.mode === "values") {
|
|
341
|
-
const config = next.guideConfigs.axis[
|
|
347
|
+
const config = next.guideConfigs.axis[axisId].labels;
|
|
342
348
|
if (Object.hasOwn(config, "count")) {
|
|
343
349
|
const { count: _count, ...withoutCount } = config;
|
|
344
|
-
next = next._withGuideConfig(
|
|
350
|
+
next = next._withGuideConfig(axisId, "labels", withoutCount);
|
|
345
351
|
}
|
|
346
352
|
} else {
|
|
347
|
-
const config = next.guideConfigs.axis[
|
|
353
|
+
const config = next.guideConfigs.axis[axisId].labels;
|
|
348
354
|
if (Object.hasOwn(config, "values")) {
|
|
349
355
|
const { values: _values, ...withoutValues } = config;
|
|
350
|
-
next = next._withGuideConfig(
|
|
356
|
+
next = next._withGuideConfig(axisId, "labels", withoutValues);
|
|
351
357
|
}
|
|
352
358
|
}
|
|
353
359
|
}
|
|
354
|
-
const title = next.guideConfigs.axis?.[
|
|
360
|
+
const title = next.guideConfigs.axis?.[axisId]?.title;
|
|
355
361
|
if (title !== undefined) {
|
|
356
|
-
next = next._withGuideConfig(
|
|
362
|
+
next = next._withGuideConfig(axisId, "title", {
|
|
357
363
|
...title,
|
|
358
364
|
scale: nextScale
|
|
359
365
|
});
|
|
360
366
|
}
|
|
361
367
|
const methods = axisMethods(channel);
|
|
362
|
-
if (ticks !== undefined) next = next[methods.ticks]();
|
|
363
|
-
if (labels !== undefined) next = next[methods.labels]();
|
|
364
|
-
if (title !== undefined) next = next[methods.title]();
|
|
368
|
+
if (ticks !== undefined) next = next[methods.ticks](axisId === channel ? undefined : axisTargetArgs(axisId, channel));
|
|
369
|
+
if (labels !== undefined) next = next[methods.labels](axisId === channel ? undefined : axisTargetArgs(axisId, channel));
|
|
370
|
+
if (title !== undefined) next = next[methods.title](axisId === channel ? undefined : axisTargetArgs(axisId, channel));
|
|
365
371
|
return next;
|
|
366
372
|
}
|
|
367
373
|
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { cartesianAxisOwners, axisTargetArgs } from "../../selectors/axes.js";
|
|
1
2
|
import { closedAction } from "../../core/action.js";
|
|
2
3
|
import { validateUserId } from "../../core/identifiers.js";
|
|
3
4
|
|
|
@@ -41,9 +42,9 @@ const REMOVE_AXIS = Object.freeze({
|
|
|
41
42
|
radius: "removeRadialAxis"
|
|
42
43
|
});
|
|
43
44
|
const SPECIALIZED_LEGEND_KIND = Object.freeze({
|
|
44
|
-
color: Object.freeze(["gradient", "interval"]),
|
|
45
|
+
color: Object.freeze(["gradient", "interval", "colorSize"]),
|
|
45
46
|
stroke: Object.freeze(["strokeGradient", "strokeInterval"]),
|
|
46
|
-
size: Object.freeze(["size"]),
|
|
47
|
+
size: Object.freeze(["size", "colorSize"]),
|
|
47
48
|
opacity: Object.freeze(["opacity"]),
|
|
48
49
|
strokeWidth: Object.freeze(["strokeWidth"])
|
|
49
50
|
});
|
|
@@ -144,10 +145,14 @@ function cleanupPositionGuides(program, removed) {
|
|
|
144
145
|
) {
|
|
145
146
|
continue;
|
|
146
147
|
}
|
|
147
|
-
const
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
148
|
+
const owners = ["x", "y"].includes(definition.guideChannel)
|
|
149
|
+
? cartesianAxisOwners(next).filter(owner => owner.channel === definition.guideChannel)
|
|
150
|
+
: [{ id: definition.guideChannel, channel: definition.guideChannel,
|
|
151
|
+
semantic: next.semanticSpec.guides.axis?.[definition.guideChannel] }];
|
|
152
|
+
for (const owner of owners) {
|
|
153
|
+
const axis = owner.semantic;
|
|
154
|
+
if (axis?.scale === scale) next = next[REMOVE_AXIS[definition.guideChannel]]({
|
|
155
|
+
...axisTargetArgs(owner.id, owner.channel), scale,
|
|
151
156
|
...(axis.coordinate === undefined ? {} : { coordinate: axis.coordinate })
|
|
152
157
|
});
|
|
153
158
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { cartesianAxisOwners } from "../../selectors/axes.js";
|
|
1
2
|
import { isSourceOwnedText } from "../../grammar/text.js";
|
|
2
3
|
import { validateUserId } from "../../core/identifiers.js";
|
|
3
4
|
import { findDataset } from "../../selectors/datasets.js";
|
|
@@ -80,7 +81,7 @@ export function applyEncodingScale(
|
|
|
80
81
|
for (const property of [
|
|
81
82
|
"domain", "range", "nice", "zero", "clamp", "reverse",
|
|
82
83
|
"base", "exponent", "constant", "paddingInner", "paddingOuter",
|
|
83
|
-
"padding", "align", "unknown", "interpolate", "radialMapping"
|
|
84
|
+
"padding", "align", "unknown", "interpolate", "whitePoint", "gamma", "radialMapping"
|
|
84
85
|
]) {
|
|
85
86
|
if (Object.hasOwn(options, property)) patch[property] = definition[property];
|
|
86
87
|
}
|
|
@@ -102,26 +103,26 @@ export function rebindPositionGuides(
|
|
|
102
103
|
if (definition?.guideChannel === undefined) return program;
|
|
103
104
|
if (previousScale === undefined || previousScale === nextScale) return program;
|
|
104
105
|
|
|
105
|
-
const
|
|
106
|
+
const owners = ["x", "y"].includes(definition.guideChannel)
|
|
107
|
+
? cartesianAxisOwners(program).filter(owner => owner.channel === definition.guideChannel)
|
|
108
|
+
: [{ id: definition.guideChannel, semantic: program.semanticSpec.guides.axis?.[definition.guideChannel] }];
|
|
106
109
|
const direction = definition.gridDirection;
|
|
107
110
|
const grid = program.semanticSpec.guides.grid?.[direction];
|
|
108
|
-
const ownsAxis = axis?.scale === previousScale;
|
|
109
|
-
const refreshesReboundAxis = axis?.scale === nextScale;
|
|
110
111
|
const ownsGrid = grid?.scale === previousScale;
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
const remaining = program.semanticSpec.layers.some(layer =>
|
|
114
|
-
|
|
115
|
-
)
|
|
116
|
-
|
|
117
|
-
throw new Error(
|
|
118
|
-
`Cannot rebind ${channel} guides from shared scale "${previousScale}" while it has other consumers.`
|
|
119
|
-
);
|
|
112
|
+
const affected = owners.filter(owner => [previousScale, nextScale].includes(owner.semantic?.scale));
|
|
113
|
+
if (!affected.length && !ownsGrid) return program;
|
|
114
|
+
const remaining = program.semanticSpec.layers.some(layer => layer.id !== target &&
|
|
115
|
+
!isSourceOwnedText(layer) && layer.encoding?.[channel]?.scale === previousScale);
|
|
116
|
+
if ((affected.some(owner => owner.semantic?.scale === previousScale) || ownsGrid) && remaining) {
|
|
117
|
+
throw new Error(`Cannot rebind ${channel} guides from shared scale "${previousScale}" while it has other consumers.`);
|
|
120
118
|
}
|
|
121
|
-
|
|
122
119
|
let next = program;
|
|
120
|
+
for (const owner of affected) {
|
|
121
|
+
const axisId = owner.id;
|
|
122
|
+
const ownsAxis = owner.semantic?.scale === previousScale;
|
|
123
|
+
const refreshesReboundAxis = owner.semantic?.scale === nextScale;
|
|
123
124
|
if (ownsAxis || refreshesReboundAxis) {
|
|
124
|
-
const previousAxisConfig = next.guideConfigs.axis?.[
|
|
125
|
+
const previousAxisConfig = next.guideConfigs.axis?.[axisId];
|
|
125
126
|
const previousTicks = previousAxisConfig?.ticks;
|
|
126
127
|
const previousLabels = previousAxisConfig?.labels;
|
|
127
128
|
const labelsFollowTicks = previousTicks !== undefined &&
|
|
@@ -135,13 +136,13 @@ export function rebindPositionGuides(
|
|
|
135
136
|
));
|
|
136
137
|
if (ownsAxis) {
|
|
137
138
|
next = next.editSemantic({
|
|
138
|
-
property: `guide.axis.${
|
|
139
|
+
property: `guide.axis.${axisId}.scale`,
|
|
139
140
|
value: nextScale
|
|
140
141
|
});
|
|
141
142
|
for (const component of ["line", "ticks", "labels", "title"]) {
|
|
142
|
-
const config = next.guideConfigs.axis?.[
|
|
143
|
+
const config = next.guideConfigs.axis?.[axisId]?.[component];
|
|
143
144
|
if (config?.scale === previousScale) {
|
|
144
|
-
next = next._withGuideConfig(
|
|
145
|
+
next = next._withGuideConfig(axisId, component, {
|
|
145
146
|
...config,
|
|
146
147
|
scale: nextScale
|
|
147
148
|
});
|
|
@@ -151,7 +152,7 @@ export function rebindPositionGuides(
|
|
|
151
152
|
const resolved = next.resolvedScales[nextScale] ??
|
|
152
153
|
findSemanticScale(next, nextScale);
|
|
153
154
|
const discrete = ["ordinal", "band", "point"].includes(resolved?.type);
|
|
154
|
-
let ticks = next.guideConfigs.axis?.[
|
|
155
|
+
let ticks = next.guideConfigs.axis?.[axisId]?.ticks;
|
|
155
156
|
if (ticks !== undefined && discrete && ticks.mode !== "values") {
|
|
156
157
|
ticks = {
|
|
157
158
|
...ticks,
|
|
@@ -160,7 +161,7 @@ export function rebindPositionGuides(
|
|
|
160
161
|
inferredValues: true
|
|
161
162
|
};
|
|
162
163
|
delete ticks.count;
|
|
163
|
-
next = next._withGuideConfig(
|
|
164
|
+
next = next._withGuideConfig(axisId, "ticks", ticks);
|
|
164
165
|
} else if (
|
|
165
166
|
ticks !== undefined &&
|
|
166
167
|
!discrete &&
|
|
@@ -174,18 +175,18 @@ export function rebindPositionGuides(
|
|
|
174
175
|
inferredValues: true
|
|
175
176
|
};
|
|
176
177
|
delete ticks.values;
|
|
177
|
-
next = next._withGuideConfig(
|
|
178
|
+
next = next._withGuideConfig(axisId, "ticks", ticks);
|
|
178
179
|
} else if (
|
|
179
180
|
ticks !== undefined &&
|
|
180
181
|
discrete &&
|
|
181
182
|
ticks.inferredValues === true
|
|
182
183
|
) {
|
|
183
184
|
ticks = { ...ticks, values: resolved.domain };
|
|
184
|
-
next = next._withGuideConfig(
|
|
185
|
+
next = next._withGuideConfig(axisId, "ticks", ticks);
|
|
185
186
|
}
|
|
186
187
|
if (previousLabels !== undefined && labelsFollowTicks && ticks !== undefined) {
|
|
187
188
|
const labels = {
|
|
188
|
-
...next.guideConfigs.axis[
|
|
189
|
+
...next.guideConfigs.axis[axisId].labels,
|
|
189
190
|
mode: ticks.mode,
|
|
190
191
|
inferredValues: ticks.inferredValues === true,
|
|
191
192
|
...(ticks.mode === "values"
|
|
@@ -195,12 +196,13 @@ export function rebindPositionGuides(
|
|
|
195
196
|
if (ticks.mode === "values") delete labels.count;
|
|
196
197
|
else delete labels.values;
|
|
197
198
|
next = next._withGuideConfig(
|
|
198
|
-
|
|
199
|
+
axisId,
|
|
199
200
|
"labels",
|
|
200
201
|
labels
|
|
201
202
|
);
|
|
202
203
|
}
|
|
203
204
|
}
|
|
205
|
+
}
|
|
204
206
|
if (ownsGrid) {
|
|
205
207
|
next = next
|
|
206
208
|
.editSemantic({
|
|
@@ -60,7 +60,7 @@ const HEADER_STYLE_OPTIONS = Object.freeze([
|
|
|
60
60
|
"fontSize", "fontFamily", "fontWeight", "color", "offset", "align"
|
|
61
61
|
]);
|
|
62
62
|
|
|
63
|
-
function normalizeGuides(guides) {
|
|
63
|
+
function normalizeGuides(guides, allowStored = false) {
|
|
64
64
|
if (guides === undefined) return { axes: "each", legend: false };
|
|
65
65
|
validateOptionObject(guides, GUIDE_OPTIONS, "facet.guides");
|
|
66
66
|
const axes = guides.axes ?? "each";
|
|
@@ -68,8 +68,11 @@ function normalizeGuides(guides) {
|
|
|
68
68
|
throw new Error('facet guides.axes must be "each" or "outer".');
|
|
69
69
|
}
|
|
70
70
|
const legend = guides.legend ?? false;
|
|
71
|
-
if (legend !== false && legend !== "shared") {
|
|
72
|
-
throw new Error('facet guides.legend must be false
|
|
71
|
+
if (legend !== false && legend !== "shared" && !isPlainObject(legend)) {
|
|
72
|
+
throw new Error('facet guides.legend must be false, "shared", or legend options.');
|
|
73
|
+
}
|
|
74
|
+
if (!allowStored && isPlainObject(legend) && Object.hasOwn(legend, "type")) {
|
|
75
|
+
throw new Error("Facet legend options do not accept internal type tags.");
|
|
73
76
|
}
|
|
74
77
|
return { axes, legend };
|
|
75
78
|
}
|
|
@@ -617,7 +620,8 @@ export const editFacetGuides = /* @__PURE__ */ action(
|
|
|
617
620
|
emptyMessage: "editFacetGuides requires at least one guide policy."
|
|
618
621
|
});
|
|
619
622
|
const current = this.compositionSpec.facet;
|
|
620
|
-
|
|
623
|
+
if (Object.hasOwn(args, "legend")) normalizeGuides({ legend: args.legend });
|
|
624
|
+
const guides = normalizeGuides({ ...current.guides, ...args }, true);
|
|
621
625
|
const family = resolveFacetFamily(this.semanticSpec).family;
|
|
622
626
|
if (family !== "cartesian" && guides.axes === "outer") {
|
|
623
627
|
throw new Error("Polar and Parallel facets do not support outer axes.");
|
|
@@ -688,6 +692,8 @@ export const editFacetSource = /* @__PURE__ */ closedAction(
|
|
|
688
692
|
const current = this.compositionSpec;
|
|
689
693
|
const facetConfig = this.materializationConfigs.facets?.[current.id];
|
|
690
694
|
const base = adoptUnitState(args.program, this);
|
|
695
|
+
const authored = isPlainObject(current.facet.guides.legend);
|
|
696
|
+
const guides = authored ? { ...current.facet.guides, legend: false } : current.facet.guides;
|
|
691
697
|
let revised;
|
|
692
698
|
const scales = usedFacetScalePolicies(base, current.facet.scales);
|
|
693
699
|
if (current.facet.grid !== undefined) {
|
|
@@ -702,7 +708,7 @@ export const editFacetSource = /* @__PURE__ */ closedAction(
|
|
|
702
708
|
align: current.align,
|
|
703
709
|
padding: current.padding,
|
|
704
710
|
scales,
|
|
705
|
-
guides
|
|
711
|
+
guides
|
|
706
712
|
});
|
|
707
713
|
} else if (current.facet.repeat !== undefined) {
|
|
708
714
|
revised = base.repeatCharts({
|
|
@@ -714,7 +720,7 @@ export const editFacetSource = /* @__PURE__ */ closedAction(
|
|
|
714
720
|
align: current.align,
|
|
715
721
|
padding: current.padding,
|
|
716
722
|
scales,
|
|
717
|
-
guides
|
|
723
|
+
guides
|
|
718
724
|
});
|
|
719
725
|
} else {
|
|
720
726
|
revised = base.facet({
|
|
@@ -728,9 +734,14 @@ export const editFacetSource = /* @__PURE__ */ closedAction(
|
|
|
728
734
|
align: current.align,
|
|
729
735
|
padding: current.padding,
|
|
730
736
|
scales,
|
|
731
|
-
guides
|
|
737
|
+
guides
|
|
732
738
|
});
|
|
733
739
|
}
|
|
740
|
+
if (authored) {
|
|
741
|
+
const spec = revised.compositionSpec;
|
|
742
|
+
revised = revised._withCompositionState({ children: revised.children,
|
|
743
|
+
compositionSpec: { ...spec, facet: { ...spec.facet, guides: current.facet.guides } } });
|
|
744
|
+
}
|
|
734
745
|
if (facetConfig !== undefined) {
|
|
735
746
|
revised = revised
|
|
736
747
|
._withMaterializationConfig(["facets", current.id], facetConfig)
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { synchronizeFacetColorSizeMeaning } from "../primitives/semanticAction.js";
|
|
1
2
|
import { closedAction } from "../../core/action.js";
|
|
2
3
|
|
|
3
4
|
import { applyFacetGuideComposition } from
|
|
@@ -10,6 +11,6 @@ export const composeFacetGuides = /* @__PURE__ */ closedAction(
|
|
|
10
11
|
scope: "composition"
|
|
11
12
|
}, ["layout", "plot"],
|
|
12
13
|
function ({ layout, plot } = {}) {
|
|
13
|
-
return applyFacetGuideComposition(this, { layout, plot });
|
|
14
|
+
return applyFacetGuideComposition(this, { layout, plot, synchronizeColorSizeMeaning: synchronizeFacetColorSizeMeaning });
|
|
14
15
|
}
|
|
15
16
|
);
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { withGuideLayoutTransaction } from "../../../materialization/guides/layout.js";
|
|
1
2
|
import { isSourceOwnedText } from "../../../grammar/text.js";
|
|
2
3
|
import { action } from "../../../core/action.js";
|
|
3
4
|
import { isPlainObject } from "../../../core/immutable.js";
|
|
@@ -15,6 +16,13 @@ const COORDINATE_API_TYPES = new Set([
|
|
|
15
16
|
]);
|
|
16
17
|
|
|
17
18
|
function validateAxisOption(value, channel) {
|
|
19
|
+
if (["x", "y"].includes(channel) && Array.isArray(value)) {
|
|
20
|
+
if (value.length === 0 || value.some(entry => !isPlainObject(entry) || !Object.hasOwn(entry, "id"))) {
|
|
21
|
+
throw new TypeError(`createAxes ${channel} arrays require nonempty named axis descriptors.`);
|
|
22
|
+
}
|
|
23
|
+
for (const entry of value) validateUserId(entry.id, "Axis id");
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
18
26
|
if (value !== undefined && value !== false && !isPlainObject(value)) {
|
|
19
27
|
throw new TypeError(`createAxes ${channel} must be false or a plain object.`);
|
|
20
28
|
}
|
|
@@ -287,23 +295,34 @@ const createAxes = /* @__PURE__ */ action(
|
|
|
287
295
|
coordinateDescriptor,
|
|
288
296
|
cartesianLayers
|
|
289
297
|
);
|
|
290
|
-
const
|
|
291
|
-
|
|
298
|
+
const resolve = (channel, value) => Array.isArray(value)
|
|
299
|
+
? value.map(entry => resolveAxisArgs(coordinate.layers, channel, entry))
|
|
300
|
+
: resolveAxisArgs(coordinate.layers, channel, value);
|
|
301
|
+
const x = resolve("x", args.x);
|
|
302
|
+
const y = resolve("y", args.y);
|
|
292
303
|
|
|
293
304
|
if (x === undefined && y === undefined) {
|
|
294
305
|
throw new Error("createAxes requires at least one selected axis.");
|
|
295
306
|
}
|
|
296
307
|
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
308
|
+
const apply = source => {
|
|
309
|
+
let program = source;
|
|
310
|
+
for (const [channel, option] of [["x", x], ["y", y]]) {
|
|
311
|
+
if (option === undefined) continue;
|
|
312
|
+
for (const entry of Array.isArray(option) ? option : [option]) {
|
|
313
|
+
program = program[`create${channel.toUpperCase()}Axis`]({ ...entry, coordinate: coordinate.id });
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
return program;
|
|
317
|
+
};
|
|
318
|
+
if (Array.isArray(x) || Array.isArray(y)) {
|
|
319
|
+
const ids = [...(Array.isArray(x) ? x : x === undefined ? [] : [x]),
|
|
320
|
+
...(Array.isArray(y) ? y : y === undefined ? [] : [y])].map((entry, index) => entry.id ?? (index === 0 ? "x" : "y"));
|
|
321
|
+
if (new Set(ids).size !== ids.length) throw new Error("createAxes requires unique axis ids across its descriptors.");
|
|
322
|
+
withGuideLayoutTransaction(this, apply);
|
|
323
|
+
return withGuideLayoutTransaction(this, apply);
|
|
304
324
|
}
|
|
305
|
-
|
|
306
|
-
return program;
|
|
325
|
+
return apply(this);
|
|
307
326
|
}
|
|
308
327
|
);
|
|
309
328
|
|
|
@@ -1,21 +1,27 @@
|
|
|
1
|
+
import { AXIS_RESOURCE_CHANNELS } from "../../../core/vocabulary.js";
|
|
2
|
+
import { cartesianAxisOwners, axisTargetArgs, assertCartesianAxisSideAvailable } from "../../../selectors/axes.js";
|
|
3
|
+
import { axisGraphicIds } from "../../../materialization/guides/resources.js";
|
|
4
|
+
import { defaultAxisPosition, validateAxisPosition } from "./policy.js";
|
|
1
5
|
import { isSourceOwnedText } from "../../../grammar/text.js";
|
|
2
6
|
import { withGuideLayoutValidation } from "../../../materialization/guides/layout.js";
|
|
3
7
|
import { action } from "../../../core/action.js";
|
|
4
8
|
import { isPlainObject } from "../../../core/immutable.js";
|
|
5
9
|
import { validateUserId } from "../../../core/identifiers.js";
|
|
6
|
-
import { validateKeys } from "../../../core/validation.js";
|
|
10
|
+
import { validateKeys, validateNonNegativeFinite } from "../../../core/validation.js";
|
|
7
11
|
import { hasCoordinate } from "../../../selectors/index.js";
|
|
8
12
|
import { validateEnabledAxisComponents } from "./components.js";
|
|
9
13
|
import { requireSemanticScale } from "../../../selectors/scales.js";
|
|
10
14
|
import { requireCoordinate } from "../../../selectors/coordinates.js";
|
|
11
15
|
|
|
12
16
|
const TOP_OPTIONS = Object.freeze([
|
|
17
|
+
"id",
|
|
13
18
|
"scale",
|
|
14
19
|
"coordinate",
|
|
15
20
|
"position",
|
|
16
21
|
"line",
|
|
17
22
|
"ticksAndLabels",
|
|
18
|
-
"title"
|
|
23
|
+
"title",
|
|
24
|
+
"offset"
|
|
19
25
|
]);
|
|
20
26
|
const LINE_OPTIONS = Object.freeze(["color", "lineWidth"]);
|
|
21
27
|
const TICK_GROUP_OPTIONS = Object.freeze([
|
|
@@ -32,7 +38,8 @@ const TITLE_OPTIONS = Object.freeze([
|
|
|
32
38
|
"color",
|
|
33
39
|
"fontSize",
|
|
34
40
|
"fontFamily",
|
|
35
|
-
"fontWeight"
|
|
41
|
+
"fontWeight",
|
|
42
|
+
"alongOffset", "align", "baseline"
|
|
36
43
|
]);
|
|
37
44
|
|
|
38
45
|
function validateNested(value, supported, label) {
|
|
@@ -45,6 +52,7 @@ function validateNested(value, supported, label) {
|
|
|
45
52
|
|
|
46
53
|
export function validateAxisArgs(args, operation) {
|
|
47
54
|
validateKeys(args, TOP_OPTIONS, operation);
|
|
55
|
+
if (Object.hasOwn(args, "offset")) validateNonNegativeFinite(args.offset, "Axis frame offset");
|
|
48
56
|
|
|
49
57
|
validateEnabledAxisComponents(args, operation);
|
|
50
58
|
if (Object.hasOwn(args, "line") && args.line !== false) {
|
|
@@ -84,10 +92,52 @@ function makeCreateAxis(channel) {
|
|
|
84
92
|
},
|
|
85
93
|
withGuideLayoutValidation(function (args = {}) {
|
|
86
94
|
validateAxisArgs(args, operation.create);
|
|
87
|
-
const
|
|
95
|
+
const axisId = validateUserId(args.id ?? channel, "Axis id");
|
|
96
|
+
if (AXIS_RESOURCE_CHANNELS.includes(axisId) && axisId !== channel) {
|
|
97
|
+
throw new Error(`Axis id "${axisId}" is reserved for another axis channel.`);
|
|
98
|
+
}
|
|
99
|
+
const named = axisId !== channel;
|
|
100
|
+
let source = this;
|
|
101
|
+
if (named) {
|
|
102
|
+
if (cartesianAxisOwners(this).some(owner => owner.id === axisId) ||
|
|
103
|
+
axisGraphicIds(channel, axisId).some(id => this.graphicSpec.objects[id] !== undefined)) {
|
|
104
|
+
throw new Error(`Axis "${axisId}" requires a missing resource and missing concrete components.`);
|
|
105
|
+
}
|
|
106
|
+
const layers = this.semanticSpec.layers.filter(layer => !isSourceOwnedText(layer) &&
|
|
107
|
+
layer.encoding?.[channel]?.scale !== undefined &&
|
|
108
|
+
(args.coordinate === undefined || layer.coordinate === args.coordinate) &&
|
|
109
|
+
(args.scale === undefined || layer.encoding[channel].scale === args.scale));
|
|
110
|
+
const scales = [...new Set(layers.map(layer => layer.encoding[channel].scale))];
|
|
111
|
+
if (args.scale === undefined && scales.length !== 1) throw new Error("Named axes require an explicit scale when the channel binding is ambiguous.");
|
|
112
|
+
const scale = args.scale ?? scales[0];
|
|
113
|
+
const coordinates = [...new Set(layers.map(layer => layer.coordinate))];
|
|
114
|
+
if (args.coordinate === undefined && coordinates.length !== 1) throw new Error("Named axes require an explicit coordinate when the binding is ambiguous.");
|
|
115
|
+
const coordinate = args.coordinate ?? coordinates[0];
|
|
116
|
+
if (requireCoordinate(this, coordinate).type !== "cartesian") throw new Error("Named Cartesian axes require a Cartesian coordinate.");
|
|
117
|
+
const position = validateAxisPosition(channel, args.position ?? defaultAxisPosition(channel));
|
|
118
|
+
assertCartesianAxisSideAvailable(this, { id: axisId, channel, coordinate, position });
|
|
119
|
+
args = { ...args, scale, coordinate, position };
|
|
120
|
+
source = source.editSemantic({ property: `guide.axis.${axisId}.channel`, value: channel })
|
|
121
|
+
.editSemantic({ property: `guide.axis.${axisId}.position`, value: position })
|
|
122
|
+
.editSemantic({ property: `guide.axis.${axisId}.scale`, value: scale })
|
|
123
|
+
.editSemantic({ property: `guide.axis.${axisId}.coordinate`, value: coordinate });
|
|
124
|
+
} else if (cartesianAxisOwners(this).some(owner => owner.named)) {
|
|
125
|
+
const scale = args.scale ?? channel;
|
|
126
|
+
const coordinates = [...new Set(this.semanticSpec.layers.filter(layer =>
|
|
127
|
+
layer.encoding?.[channel]?.scale === scale).map(layer => layer.coordinate))];
|
|
128
|
+
const coordinate = args.coordinate ?? (coordinates.length === 1 ? coordinates[0] : undefined);
|
|
129
|
+
assertCartesianAxisSideAvailable(this, { id: axisId, channel, coordinate,
|
|
130
|
+
position: validateAxisPosition(channel, args.position ?? defaultAxisPosition(channel)) });
|
|
131
|
+
if (!cartesianAxisOwners(this).some(owner => owner.id === channel)) {
|
|
132
|
+
source = source.editSemantic({ property: `guide.axis.${axisId}.scale`, value: scale });
|
|
133
|
+
if (coordinate !== undefined) source = source.editSemantic({ property: `guide.axis.${axisId}.coordinate`, value: coordinate });
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
const shared = axisTargetArgs(axisId, channel);
|
|
88
137
|
if (Object.hasOwn(args, "scale")) shared.scale = args.scale;
|
|
89
138
|
if (Object.hasOwn(args, "position")) shared.position = args.position;
|
|
90
|
-
let next =
|
|
139
|
+
let next = Object.hasOwn(args, "offset")
|
|
140
|
+
? source._withGuideConfig(axisId, "frame", { offset: args.offset }) : source;
|
|
91
141
|
|
|
92
142
|
if (Object.hasOwn(args, "coordinate")) {
|
|
93
143
|
const coordinate = validateUserId(args.coordinate, "Coordinate id");
|
|
@@ -116,11 +166,11 @@ function makeCreateAxis(channel) {
|
|
|
116
166
|
}
|
|
117
167
|
|
|
118
168
|
next = next.editSemantic({
|
|
119
|
-
property: `guide.axis.${
|
|
169
|
+
property: `guide.axis.${axisId}.coordinate`,
|
|
120
170
|
value: coordinate
|
|
121
171
|
});
|
|
122
|
-
if (standalone) next = next._withGuideConfig(
|
|
123
|
-
.editSemantic({ property: `guide.axis.${
|
|
172
|
+
if (standalone) next = next._withGuideConfig(axisId, "binding", { standalone: true })
|
|
173
|
+
.editSemantic({ property: `guide.axis.${axisId}.scale`, value: scale })
|
|
124
174
|
.rematerializeScale({ id: scale, guides: false, marks: false });
|
|
125
175
|
}
|
|
126
176
|
|
|
@@ -2,8 +2,8 @@ import { axisGraphicIds } from "../../../materialization/guides/resources.js";
|
|
|
2
2
|
|
|
3
3
|
export const AXIS_COMPONENTS = Object.freeze(["line", "ticks", "labels", "title"]);
|
|
4
4
|
|
|
5
|
-
export function axisComponentId(channel, component) {
|
|
6
|
-
return axisGraphicIds(channel)[AXIS_COMPONENTS.indexOf(component)];
|
|
5
|
+
export function axisComponentId(channel, component, axisId = channel) {
|
|
6
|
+
return axisGraphicIds(channel, axisId)[AXIS_COMPONENTS.indexOf(component)];
|
|
7
7
|
}
|
|
8
8
|
|
|
9
9
|
export function axisComponentDisabled(args, component) {
|
|
@@ -17,27 +17,27 @@ export function validateEnabledAxisComponents(args, operation) {
|
|
|
17
17
|
}
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
export function hasAxisComponent(program, channel, component) {
|
|
21
|
-
return program.graphicSpec.objects[axisComponentId(channel, component)] !== undefined ||
|
|
22
|
-
program.guideConfigs.axis?.[
|
|
20
|
+
export function hasAxisComponent(program, channel, component, axisId = channel) {
|
|
21
|
+
return program.graphicSpec.objects[axisComponentId(channel, component, axisId)] !== undefined ||
|
|
22
|
+
program.guideConfigs.axis?.[axisId]?.[component] !== undefined ||
|
|
23
23
|
(component === "title" &&
|
|
24
|
-
program.semanticSpec.guides.axis?.[
|
|
24
|
+
program.semanticSpec.guides.axis?.[axisId]?.title !== undefined);
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
-
export function assertRemovableAxisComponent(program, channel, component, operation) {
|
|
28
|
-
if (!hasAxisComponent(program, channel, component)) {
|
|
27
|
+
export function assertRemovableAxisComponent(program, channel, component, operation, axisId = channel) {
|
|
28
|
+
if (!hasAxisComponent(program, channel, component, axisId)) {
|
|
29
29
|
throw new Error(`${operation}.${component} requires an existing ${channel}-axis ${component}.`);
|
|
30
30
|
}
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
-
export function removeAxisComponent(program, channel, component) {
|
|
34
|
-
const id = axisComponentId(channel, component);
|
|
33
|
+
export function removeAxisComponent(program, channel, component, axisId = channel) {
|
|
34
|
+
const id = axisComponentId(channel, component, axisId);
|
|
35
35
|
let next = program;
|
|
36
|
-
if (component === "title" && next.semanticSpec.guides.axis?.[
|
|
37
|
-
next = next.editSemantic({ property: `guide.axis.${
|
|
36
|
+
if (component === "title" && next.semanticSpec.guides.axis?.[axisId]?.title !== undefined) {
|
|
37
|
+
next = next.editSemantic({ property: `guide.axis.${axisId}.title`, remove: true });
|
|
38
38
|
}
|
|
39
39
|
if (next.graphicSpec.objects[id] !== undefined) {
|
|
40
40
|
next = next.editGraphics({ target: id, remove: true });
|
|
41
41
|
}
|
|
42
|
-
return next._withoutMaterializationConfig(["guides", "axis",
|
|
42
|
+
return next._withoutMaterializationConfig(["guides", "axis", axisId, component]);
|
|
43
43
|
}
|