ggaction 0.0.24 → 0.0.26
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 +47 -0
- package/README.md +2 -2
- package/knowledge/action-cards.json +1 -1
- package/knowledge/intent-taxonomy.json +1 -1
- package/knowledge/mcp-resources.json +1 -1
- package/knowledge/task-resolver.js +1 -1
- package/package.json +1 -1
- package/src/accessibility.js +13 -0
- package/src/actions/canvas/actions.js +2 -1
- package/src/actions/canvas/fitting.js +68 -13
- package/src/actions/charts/beeswarm.js +2 -1
- package/src/actions/charts/ecdf.js +2 -1
- package/src/actions/charts/endpoints.js +25 -16
- package/src/actions/charts/heatmap.js +5 -3
- package/src/actions/charts/missing.js +17 -0
- package/src/actions/charts/radar.js +70 -33
- package/src/actions/charts/raincloud.js +18 -4
- package/src/actions/charts/rug-strip.js +2 -2
- package/src/actions/charts/shared.js +17 -0
- package/src/actions/composition/actions.js +24 -7
- package/src/actions/composition/adjacent.js +90 -0
- package/src/actions/composition/index.js +11 -1
- package/src/actions/composition/layer.js +42 -0
- package/src/actions/composition/layerPlan.js +176 -0
- package/src/actions/composition/targets.js +105 -0
- package/src/actions/data/derived.js +3 -0
- package/src/actions/data/filter.js +1 -1
- package/src/actions/data/raincloudFilter.js +3 -1
- package/src/actions/encodings/appearance.js +26 -7
- package/src/actions/encodings/channels.js +1 -0
- package/src/actions/encodings/color/continuous.js +37 -34
- package/src/actions/encodings/color/index.js +57 -43
- package/src/actions/encodings/color/policy.js +15 -0
- package/src/actions/encodings/density/resolve.js +3 -2
- package/src/actions/encodings/density.js +2 -2
- package/src/actions/encodings/offset.js +2 -0
- package/src/actions/encodings/pathOrder.js +2 -0
- package/src/actions/encodings/position/areaAlignment.js +42 -0
- package/src/actions/encodings/position/index.js +37 -3
- package/src/actions/encodings/position/policies/line.js +1 -1
- package/src/actions/encodings/ranged.js +2 -0
- package/src/actions/encodings/remove.js +13 -10
- package/src/actions/encodings/ruleAppearance.js +14 -2
- package/src/actions/facets/actions.js +1 -1
- package/src/actions/guides/axes/axes.js +2 -1
- package/src/actions/guides/axes/edit.js +1 -1
- package/src/actions/guides/axes/labels.js +2 -1
- package/src/actions/guides/axes/parallel/lifecycle.js +11 -4
- package/src/actions/guides/axes/parallel/policy.js +5 -2
- package/src/actions/guides/axes/parallel/resolve.js +3 -1
- package/src/actions/guides/axes/parallel.js +9 -4
- package/src/actions/guides/axes/titles.js +1 -1
- package/src/actions/guides/facadeLegend.js +3 -2
- package/src/actions/guides/grids/grid.js +3 -2
- package/src/actions/guides/grids/resolve.js +2 -1
- package/src/actions/guides/guides.js +3 -2
- package/src/actions/guides/legends/automatic.js +28 -0
- package/src/actions/guides/legends/blocks.js +15 -21
- package/src/actions/guides/legends/categorical/actions.js +1 -1
- package/src/actions/guides/legends/categorical/components.js +4 -3
- package/src/actions/guides/legends/categorical/layout.js +5 -2
- package/src/actions/guides/legends/categorical/options.js +9 -14
- package/src/actions/guides/legends/categorical/recipes.js +3 -0
- package/src/actions/guides/legends/categorical/resolve.js +7 -5
- package/src/actions/guides/legends/colorSize.js +51 -14
- package/src/actions/guides/legends/continuous/common.js +1 -1
- package/src/actions/guides/legends/continuous/gradient.js +1 -1
- package/src/actions/guides/legends/continuous/opacity.js +2 -0
- package/src/actions/guides/legends/edit.js +1 -1
- package/src/actions/guides/legends/focused.js +2 -2
- package/src/actions/guides/legends/group.js +1 -1
- package/src/actions/guides/legends/lane.js +1 -1
- package/src/actions/guides/legends/lifecycle.js +2 -2
- package/src/actions/guides/legends/remove.js +18 -4
- package/src/actions/guides/legends/size.js +1 -1
- package/src/actions/guides/legends/transition.js +15 -6
- package/src/actions/guides/polar/axes/facade.js +4 -2
- package/src/actions/guides/polar/axes/labels.js +6 -1
- package/src/actions/guides/polar/axes/shared.js +4 -4
- package/src/actions/guides/polar/axes/titles.js +13 -2
- package/src/actions/guides/polar/grids.js +6 -3
- package/src/actions/guides/tickValues.js +7 -1
- package/src/actions/index.js +2 -0
- package/src/actions/marks/area/actions.js +9 -2
- package/src/actions/marks/area/materialize.js +1 -1
- package/src/actions/marks/bar/materialize.js +4 -4
- package/src/actions/marks/lifecycle.js +14 -0
- package/src/actions/marks/line/actions.js +3 -0
- package/src/actions/marks/line/materialize.js +4 -2
- package/src/actions/marks/point/edit.js +2 -0
- package/src/actions/marks/point/jitter.js +2 -0
- package/src/actions/marks/point/materialize.js +5 -4
- package/src/actions/marks/point/packing.js +2 -0
- package/src/actions/marks/references.js +5 -3
- package/src/actions/marks/remove.js +16 -0
- package/src/actions/marks/shared.js +2 -1
- package/src/actions/marks/text/actions.js +5 -2
- package/src/actions/overlays/index.js +168 -0
- package/src/actions/overlays/snapshot.js +45 -0
- package/src/actions/primitives/editGraphics.js +21 -27
- package/src/actions/primitives/semanticAction.js +4 -5
- package/src/actions/primitives/semanticValidation/guide.js +1 -0
- package/src/actions/primitives/semanticValidation/layer.js +7 -0
- package/src/actions/scales/consumers/common.js +10 -0
- package/src/actions/scales/consumers/families.js +1 -1
- package/src/actions/scales/preview.js +1 -1
- package/src/actions/selection/actions.js +63 -11
- package/src/actions/titles/actions.js +3 -14
- package/src/actions/titles/resolve.js +12 -9
- package/src/core/action.js +41 -25
- package/src/core/compositionScales.js +12 -0
- package/src/core/compositionState.js +16 -2
- package/src/core/immutable.js +11 -6
- package/src/core/programState.js +16 -0
- package/src/core/resourceReferences.js +19 -3
- package/src/core/validation.js +7 -0
- package/src/core/vocabulary.js +8 -2
- package/src/grammar/actionTargets.js +24 -0
- package/src/grammar/bin2d.js +7 -0
- package/src/grammar/colorSizeLegend.js +56 -5
- package/src/grammar/displayLabels.js +10 -0
- package/src/grammar/facetHeaders.js +1 -1
- package/src/grammar/facets/dependencies.js +10 -3
- package/src/grammar/facets/guides.js +2 -1
- package/src/grammar/facets/legendIntent.js +7 -5
- package/src/grammar/legendBlocks.js +1 -1
- package/src/grammar/legendCombinations.js +1 -1
- package/src/grammar/legendGroups.js +1 -1
- package/src/grammar/legendItemsOffset.js +12 -0
- package/src/grammar/legendOverflow.js +25 -0
- package/src/grammar/lineSeries.js +1 -1
- package/src/grammar/markSelection.js +23 -0
- package/src/grammar/overlay.js +51 -0
- package/src/grammar/parallelCoordinates.js +1 -208
- package/src/grammar/pathCommands.js +8 -2
- package/src/grammar/pathSeries.js +1 -1
- package/src/grammar/plotComposition.js +152 -0
- package/src/grammar/pointShapes.js +18 -0
- package/src/grammar/scales/transformed.js +1 -1
- package/src/grammar/schemas/concreteGraphic.js +8 -1
- package/src/grammar/schemas/graphicBounds.js +8 -1
- package/src/grammar/schemas/graphicTree.js +18 -2
- package/src/grammar/schemas/semanticPath.js +3 -0
- package/src/grammar/strokeStyle.js +14 -8
- package/src/grammar/text.js +72 -0
- package/src/inspection.js +1 -1
- package/src/inspectionDescriptors.js +1 -1
- package/src/layout/composition.js +30 -4
- package/src/layout/compositionLegend.js +91 -0
- package/src/layout/facetHeaderTitles.js +1 -1
- package/src/layout/labels.js +55 -27
- package/src/layout/legend.js +3 -2
- package/src/layout/legendGroup.js +1 -1
- package/src/layout/legendItems.js +7 -6
- package/src/layout/legendLane.js +1 -1
- package/src/layout/legendText.js +1 -1
- package/src/materialization/adjacentSource.js +27 -0
- package/src/materialization/composition.js +165 -52
- package/src/materialization/compositionLegends.js +120 -0
- package/src/materialization/compositionScales.js +125 -0
- package/src/materialization/compositionTitle.js +96 -0
- package/src/materialization/facetGuides/edit.js +22 -2
- package/src/materialization/facetGuides/layout.js +1 -1
- package/src/materialization/facetGuides/owner.js +4 -3
- package/src/materialization/facetGuides/ownerLayout.js +12 -6
- package/src/materialization/facetGuides/placement.js +19 -22
- package/src/materialization/facetGuides/preparation.js +17 -5
- package/src/materialization/facetHeaders.js +5 -1
- package/src/materialization/facets.js +1 -1
- package/src/materialization/guides/layout.js +1 -1
- package/src/materialization/guides/resources.js +1 -1
- package/src/materialization/guides/visibility.js +105 -0
- package/src/materialization/legendClip.js +1 -1
- package/src/materialization/legendGroups.js +1 -1
- package/src/materialization/legendItemsOffset.js +68 -0
- package/src/materialization/legendSuppression.js +22 -0
- package/src/materialization/legends.js +3 -2
- package/src/materialization/marks/capabilities.js +4 -0
- package/src/materialization/marks/index.js +5 -1
- package/src/materialization/marks/pathOrder.js +1 -1
- package/src/materialization/marks/policies.js +2 -1
- package/src/materialization/overlayItems.js +154 -0
- package/src/materialization/scales/policies/binnedPosition.js +43 -5
- package/src/materialization/scales/policies/series.js +12 -4
- package/src/materialization/scales/resolve.js +2 -0
- package/src/materialization/selection/items/bar.js +21 -3
- package/src/materialization/selection/items/common.js +18 -5
- package/src/materialization/selection/items/overlay.js +15 -0
- package/src/materialization/selection/items/path.js +1 -0
- package/src/materialization/selection/items/point.js +2 -3
- package/src/materialization/selection/items/tick.js +3 -12
- package/src/materialization/selection/path.js +2 -2
- package/src/materialization/selection/policies/bar.js +2 -0
- package/src/materialization/selection/policies/index.js +35 -1
- package/src/materialization/selection/policies/point.js +2 -0
- package/src/materialization/selection/policies/text.js +17 -0
- package/src/materialization/selection/rows.js +103 -0
- package/src/materialization/selection/state.js +19 -3
- package/src/materialization/selection/styles.js +8 -0
- package/src/materialization/text.js +6 -489
- package/src/materialization/textGeometry.js +503 -0
- package/src/materialization/textLines.js +20 -0
- package/src/persistence/validation.js +97 -24
- package/src/renderers/canvas/index.js +12 -7
- package/src/renderers/canvas/text.js +5 -5
- package/src/renderers/nativeBackend.js +25 -0
- package/src/renderers/pdf.js +3 -3
- package/src/renderers/png.js +4 -3
- package/src/renderers/svgSerialization.js +1 -643
- package/src/scene.js +5 -3
- package/src/selectors/plotComposition.js +115 -0
- package/src/selectors/selectionTargets.js +19 -0
- package/src/version.js +1 -1
- package/types/basic.d.ts +2 -2
- package/types/index.d.ts +11 -0
- package/types/inspection.d.ts +34 -0
- package/types/program.d.ts +553 -365
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { validateTextAlignment } from "../../core/validation.js";
|
|
1
2
|
import { textBoundsFitCanvas } from "../../core/textMetrics.js";
|
|
2
3
|
import { resolveGuideCollisionBlocks } from "../../materialization/guides/resources.js";
|
|
3
4
|
import { canvasOverflowError } from "../../layout/canvas.js";
|
|
@@ -27,7 +28,7 @@ const OPTIONS = Object.freeze([
|
|
|
27
28
|
"subtitleStyle", "maxWidth", "wrap", "lineHeight", "subtitleLineHeight"
|
|
28
29
|
]);
|
|
29
30
|
const STYLE_OPTIONS = Object.freeze([
|
|
30
|
-
"color", "fontSize", "fontFamily", "fontWeight", "fontStyle"
|
|
31
|
+
"color", "fontSize", "fontFamily", "fontWeight", "fontStyle", "align"
|
|
31
32
|
]);
|
|
32
33
|
const POSITIONS = Object.freeze(["top", "bottom", "left", "right"]);
|
|
33
34
|
const ALIGNS = Object.freeze(["left", "center", "right"]);
|
|
@@ -69,6 +70,7 @@ function normalizeStyle(value, defaults, label) {
|
|
|
69
70
|
}
|
|
70
71
|
const style = { ...defaults, ...(value ?? {}) };
|
|
71
72
|
validateKeys(style, STYLE_OPTIONS, label);
|
|
73
|
+
validateTextAlignment(style, label);
|
|
72
74
|
validateTitleString(style.color, `${label} color`);
|
|
73
75
|
validateTitleString(style.fontFamily, `${label} fontFamily`);
|
|
74
76
|
validatePositiveFinite(style.fontSize, `${label} fontSize`);
|
|
@@ -251,11 +253,12 @@ export function resolveTitleLayout(program, config) {
|
|
|
251
253
|
: config.position === "top"
|
|
252
254
|
? 16 + config.offset
|
|
253
255
|
: plot.y + plot.height + config.offset;
|
|
254
|
-
|
|
256
|
+
const left = x - (config.align === "right" ? block.width : config.align === "center" ? block.width / 2 : 0);
|
|
257
|
+
component = (lines, centers, style) => ({
|
|
255
258
|
lines,
|
|
256
|
-
x,
|
|
259
|
+
x: style.align === undefined ? x : alignedTextAnchor(left, block.width, style.align),
|
|
257
260
|
y: centers.map(value => blockTop + value),
|
|
258
|
-
textAlign: config.align,
|
|
261
|
+
textAlign: style.align ?? config.align,
|
|
259
262
|
rotation: 0,
|
|
260
263
|
explicitRotation: config.position !== "top" || lines.length > 1
|
|
261
264
|
});
|
|
@@ -267,19 +270,19 @@ export function resolveTitleLayout(program, config) {
|
|
|
267
270
|
? 16 + config.offset
|
|
268
271
|
: canvas.properties.width - 16 + config.offset;
|
|
269
272
|
const mapX = value => config.position === "left" ? edge + value : edge - value;
|
|
270
|
-
component = (lines, centers) => ({
|
|
273
|
+
component = (lines, centers, style) => ({
|
|
271
274
|
lines,
|
|
272
275
|
x: centers.map(mapX),
|
|
273
|
-
y,
|
|
274
|
-
textAlign: "center",
|
|
276
|
+
y: y + Math.sin(rotation) * (style.align === "left" ? -block.width / 2 : style.align === "right" ? block.width / 2 : 0),
|
|
277
|
+
textAlign: style.align ?? "center",
|
|
275
278
|
rotation,
|
|
276
279
|
explicitRotation: true
|
|
277
280
|
});
|
|
278
281
|
}
|
|
279
|
-
const title = component(block.titleLines, block.titleCenters);
|
|
282
|
+
const title = component(block.titleLines, block.titleCenters, config.titleStyle);
|
|
280
283
|
const subtitle = block.subtitleLines.length === 0
|
|
281
284
|
? undefined
|
|
282
|
-
: component(block.subtitleLines, block.subtitleCenters);
|
|
285
|
+
: component(block.subtitleLines, block.subtitleCenters, config.subtitleStyle);
|
|
283
286
|
const titleBounds = unionTitleBounds([
|
|
284
287
|
resolveTitleComponentBounds(title, config.titleStyle, program.materializationConfigs.textMetrics),
|
|
285
288
|
...(subtitle === undefined
|
package/src/core/action.js
CHANGED
|
@@ -5,6 +5,21 @@ import { cloneAndFreeze, freezeOwned, isOwned, isPlainObject } from "./immutable
|
|
|
5
5
|
const metadataByWrappedAction = new WeakMap();
|
|
6
6
|
const implementationByWrappedAction = new WeakMap();
|
|
7
7
|
let actionCompletionHook;
|
|
8
|
+
const targetDispatchers = new WeakMap();
|
|
9
|
+
const UNSCOPED_PRIMITIVES = new Set(["createGraphics", "editGraphics", "editSemantic", "createData"]);
|
|
10
|
+
export function registerActionTargetDispatcher(ProgramClass, dispatcher) {
|
|
11
|
+
targetDispatchers.set(ProgramClass, dispatcher);
|
|
12
|
+
}
|
|
13
|
+
function targetDispatcher(ProgramClass) {
|
|
14
|
+
for (let owner = ProgramClass; owner; owner = Object.getPrototypeOf(owner)) {
|
|
15
|
+
if (targetDispatchers.has(owner)) return targetDispatchers.get(owner);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
export function supportsScopedActionTargets(ProgramClass, operation) {
|
|
21
|
+
return !UNSCOPED_PRIMITIVES.has(operation) && targetDispatcher(ProgramClass) !== undefined;
|
|
22
|
+
}
|
|
8
23
|
|
|
9
24
|
export function setActionCompletionHook(hook) {
|
|
10
25
|
if (hook !== undefined && typeof hook !== "function") {
|
|
@@ -89,29 +104,28 @@ function childList(node) {
|
|
|
89
104
|
return list;
|
|
90
105
|
}
|
|
91
106
|
|
|
92
|
-
function
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
Object.defineProperty(next, "children", {
|
|
101
|
-
enumerable: true,
|
|
102
|
-
get() {
|
|
103
|
-
if (children === undefined) {
|
|
104
|
-
const values = new Array(list.length);
|
|
105
|
-
let item = list.tail;
|
|
106
|
-
for (let index = list.length - 1; index >= 0; index -= 1) {
|
|
107
|
-
values[index] = item.value;
|
|
108
|
-
item = item.previous;
|
|
109
|
-
}
|
|
110
|
-
children = freezeOwned(values);
|
|
111
|
-
}
|
|
112
|
-
return children;
|
|
107
|
+
function readChildren() {
|
|
108
|
+
const list = childLists.get(this);
|
|
109
|
+
if (list.children === undefined) {
|
|
110
|
+
const values = new Array(list.length);
|
|
111
|
+
let item = list.tail;
|
|
112
|
+
for (let index = list.length - 1; index >= 0; index -= 1) {
|
|
113
|
+
values[index] = item.value;
|
|
114
|
+
item = item.previous;
|
|
113
115
|
}
|
|
114
|
-
|
|
116
|
+
list.children = freezeOwned(values);
|
|
117
|
+
}
|
|
118
|
+
return list.children;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function withChildList(node, list) {
|
|
122
|
+
// Keep one accessor identity across revisions. A new getter closure and
|
|
123
|
+
// nonconfigurable descriptors per append force dictionary-shaped objects;
|
|
124
|
+
// freezing those dominates long nested materialization traces in V8.
|
|
125
|
+
// Copy data properties without evaluating the lazy children array.
|
|
126
|
+
const next = Object.fromEntries(Object.keys(node)
|
|
127
|
+
.filter(key => key !== "children").map(key => [key, node[key]]));
|
|
128
|
+
Object.defineProperty(next, "children", { enumerable: true, get: readChildren });
|
|
115
129
|
childLists.set(next, list);
|
|
116
130
|
return freezeOwned(next);
|
|
117
131
|
}
|
|
@@ -202,15 +216,17 @@ export function action(metadata, implementation) {
|
|
|
202
216
|
throw new TypeError("Action arguments must be a plain object.");
|
|
203
217
|
}
|
|
204
218
|
|
|
205
|
-
|
|
206
|
-
|
|
219
|
+
const scoped = isPlainObject(args.target) || Array.isArray(args.target);
|
|
220
|
+
const dispatch = scoped && !UNSCOPED_PRIMITIVES.has(ownedMetadata.op) ? targetDispatcher(this.constructor) : undefined;
|
|
221
|
+
if (!dispatch && scope === "unit") this._assertUnitProgram(ownedMetadata.op);
|
|
222
|
+
if (!dispatch && scope === "composition") this._assertCompositionProgram(ownedMetadata.op);
|
|
207
223
|
|
|
208
224
|
const summarizedArgs = summarizeArgs(args);
|
|
209
225
|
const entered = this._enterAction({
|
|
210
226
|
...ownedMetadata,
|
|
211
227
|
args: summarizedArgs
|
|
212
228
|
});
|
|
213
|
-
let result = implementation.call(entered, args);
|
|
229
|
+
let result = dispatch ? dispatch(entered, wrappedAction, args) : implementation.call(entered, args);
|
|
214
230
|
|
|
215
231
|
if (!(result instanceof this.constructor)) {
|
|
216
232
|
throw new TypeError(`${ownedMetadata.op} must return a ChartProgram.`);
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { isPlainObject, cloneAndFreeze } from "./immutable.js";
|
|
2
|
+
|
|
3
|
+
export const COMPOSITION_SCALE_CHANNELS = Object.freeze(["x", "y", "color", "stroke", "size", "shape", "opacity", "strokeWidth", "strokeDash"]);
|
|
4
|
+
|
|
5
|
+
export function normalizeCompositionShares(value = {}) {
|
|
6
|
+
if (!isPlainObject(value)) throw new TypeError("Composition share requires a channel object.");
|
|
7
|
+
for (const [channel, shared] of Object.entries(value)) {
|
|
8
|
+
if (!COMPOSITION_SCALE_CHANNELS.includes(channel)) throw new Error(`Unknown composition share channel "${channel}".`);
|
|
9
|
+
if (typeof shared !== "boolean") throw new TypeError(`Composition share.${channel} must be boolean.`);
|
|
10
|
+
}
|
|
11
|
+
return cloneAndFreeze(value);
|
|
12
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { normalizeCompositionShares } from "./compositionScales.js";
|
|
1
2
|
import { normalizeFacetGap } from "./facetGap.js";
|
|
2
3
|
import { normalizeFacetAxes } from "./facetAxisPolicy.js";
|
|
3
4
|
import { isOwned, isPlainObject } from "./immutable.js";
|
|
@@ -9,6 +10,11 @@ import {
|
|
|
9
10
|
DEFAULT_FACET_TOP_LANE
|
|
10
11
|
} from "./vocabulary.js";
|
|
11
12
|
|
|
13
|
+
export function normalizeCompositionLegends(value) {
|
|
14
|
+
if (!["local", "collect"].includes(value)) throw new TypeError('Composition legends must be "local" or "collect".');
|
|
15
|
+
return value;
|
|
16
|
+
}
|
|
17
|
+
|
|
12
18
|
// Primitive graphics/trace edits retain both immutable owners. Validate their
|
|
13
19
|
// relationship once, rather than scanning every facet cell on every primitive.
|
|
14
20
|
const validatedCompositions = new WeakMap();
|
|
@@ -248,7 +254,7 @@ function validateLayout(compositionSpec) {
|
|
|
248
254
|
} else if (!Number.isFinite(compositionSpec.gap) || compositionSpec.gap < 0) {
|
|
249
255
|
throw new RangeError("compositionSpec.gap must be a non-negative finite number.");
|
|
250
256
|
}
|
|
251
|
-
if (!["start", "center", "end"].includes(compositionSpec.align)) {
|
|
257
|
+
if (!(compositionSpec.type === "facet" ? ["start", "center", "end"] : ["start", "center", "end", "plot"]).includes(compositionSpec.align)) {
|
|
252
258
|
throw new Error("compositionSpec.align must be start, center, or end.");
|
|
253
259
|
}
|
|
254
260
|
if (!isPlainObject(compositionSpec.padding)) {
|
|
@@ -284,7 +290,7 @@ export function ownCompositionSpec(compositionSpec, children) {
|
|
|
284
290
|
}
|
|
285
291
|
const allowed = [
|
|
286
292
|
"id", "type", "direction", "children", "columns", "gap", "align",
|
|
287
|
-
"padding", "facet", "spacing", "topLane"
|
|
293
|
+
"padding", "facet", "spacing", "topLane", "share", "legends"
|
|
288
294
|
];
|
|
289
295
|
const unknown = Object.keys(compositionSpec).find(key => !allowed.includes(key));
|
|
290
296
|
if (unknown !== undefined) {
|
|
@@ -317,6 +323,14 @@ export function ownCompositionSpec(compositionSpec, children) {
|
|
|
317
323
|
) {
|
|
318
324
|
throw new Error("Concat compositionSpec does not accept facet properties.");
|
|
319
325
|
}
|
|
326
|
+
if (compositionSpec.legends !== undefined) {
|
|
327
|
+
if (facet) throw new Error("Facet uses guides.legend, not composition legends.");
|
|
328
|
+
normalizeCompositionLegends(compositionSpec.legends);
|
|
329
|
+
}
|
|
330
|
+
if (compositionSpec.share !== undefined) {
|
|
331
|
+
if (facet) throw new Error("Facet uses scales, not composition share.");
|
|
332
|
+
normalizeCompositionShares(compositionSpec.share);
|
|
333
|
+
}
|
|
320
334
|
validateLayout(compositionSpec);
|
|
321
335
|
const owned = ownProgramState(compositionSpec);
|
|
322
336
|
if (isOwned(children)) {
|
package/src/core/immutable.js
CHANGED
|
@@ -46,12 +46,17 @@ export function cloneAndFreeze(value, ancestors = new WeakSet(), path = "state")
|
|
|
46
46
|
if (Array.isArray(value)) {
|
|
47
47
|
clone = value.map((item, index) => cloneAndFreeze(item, ancestors, `${path}[${index}]`));
|
|
48
48
|
} else if (isPlainObject(value)) {
|
|
49
|
-
clone =
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
49
|
+
clone = {};
|
|
50
|
+
// Snapshot entries before descending, as with Object.fromEntries, but avoid
|
|
51
|
+
// allocating a second entries array and a pair for every stored property.
|
|
52
|
+
for (const [key, item] of Object.entries(value)) {
|
|
53
|
+
const child = cloneAndFreeze(item, ancestors, `${path}.${key}`);
|
|
54
|
+
if (key === "__proto__") {
|
|
55
|
+
Object.defineProperty(clone, key, {
|
|
56
|
+
value: child, enumerable: true, writable: true, configurable: true
|
|
57
|
+
});
|
|
58
|
+
} else clone[key] = child;
|
|
59
|
+
}
|
|
55
60
|
} else {
|
|
56
61
|
throw new TypeError("ChartProgram state only supports plain objects and arrays.");
|
|
57
62
|
}
|
package/src/core/programState.js
CHANGED
|
@@ -48,9 +48,25 @@ export function releaseNamedResourceOwnership(program, { kind, id, dataOwner })
|
|
|
48
48
|
materializationConfigs,
|
|
49
49
|
["regressionFits", dataOwner?.current ?? id]
|
|
50
50
|
).value;
|
|
51
|
+
materializationConfigs = removeMaterializationConfig(
|
|
52
|
+
materializationConfigs,
|
|
53
|
+
["rowProvenance", dataOwner?.current ?? id]
|
|
54
|
+
).value;
|
|
51
55
|
const context = program.context.currentData === id
|
|
52
56
|
? freezeOwned({ ...program.context, currentData: undefined })
|
|
53
57
|
: program.context;
|
|
54
58
|
if (context === program.context && materializationConfigs === program.materializationConfigs) return program;
|
|
55
59
|
return program._clone({ context, materializationConfigs });
|
|
56
60
|
}
|
|
61
|
+
|
|
62
|
+
// Scoped planners temporarily annotate context. Restore only those keys after
|
|
63
|
+
// the action so genuine target/data context changes survive, and absent keys
|
|
64
|
+
// remain absent in persistence rather than becoming serialized undefined tags.
|
|
65
|
+
export function restoreProgramContextKeys(program, reference, keys) {
|
|
66
|
+
const context = { ...program.context };
|
|
67
|
+
for (const key of keys) {
|
|
68
|
+
if (Object.hasOwn(reference.context, key)) context[key] = reference.context[key];
|
|
69
|
+
else delete context[key];
|
|
70
|
+
}
|
|
71
|
+
return program._clone({ context: freezeOwned(context) });
|
|
72
|
+
}
|
|
@@ -13,7 +13,7 @@ const DATA_VALUE_KEYS = new Set([
|
|
|
13
13
|
const DATA_SOURCE_CONFIGS = new Set([
|
|
14
14
|
"boxPlot", "density", "ecdfPlot", "endpointPlot", "errorBand", "errorBar",
|
|
15
15
|
"gradientPlot", "horizon", "intervalPlot", "markFilter", "raincloudPlot",
|
|
16
|
-
"regression", "violinPlot"
|
|
16
|
+
"regression", "violinPlot", "radarPlot"
|
|
17
17
|
]);
|
|
18
18
|
const MARK_ID_KEYS = new Set([
|
|
19
19
|
"bandId", "bodyId", "centerId", "connectorId", "intervalId", "lineId",
|
|
@@ -98,7 +98,7 @@ export function collectSemanticScaleReferences(program) {
|
|
|
98
98
|
}
|
|
99
99
|
}
|
|
100
100
|
collectKnownKeys(program.semanticSpec.guides, [], (key, value, path) => {
|
|
101
|
-
if (["scale", "colorScale", "sizeScale"].includes(key)) {
|
|
101
|
+
if (["scale", "colorScale", "sizeScale", "shapeScale"].includes(key)) {
|
|
102
102
|
append(references, reference(
|
|
103
103
|
"scale", value, "semanticGuide", "guides", path
|
|
104
104
|
));
|
|
@@ -134,6 +134,10 @@ export function collectMarkConfigReferences(program) {
|
|
|
134
134
|
append(references, reference(
|
|
135
135
|
"mark", layer.source, "layer", layer.id, ["source"]
|
|
136
136
|
));
|
|
137
|
+
append(references, reference(
|
|
138
|
+
"mark", layer.derivedBindings?.overlay?.source, "layer", layer.id,
|
|
139
|
+
["derivedBindings", "overlay", "source"]
|
|
140
|
+
));
|
|
137
141
|
}
|
|
138
142
|
for (const [ownerId, config] of Object.entries(program.markConfigs)) {
|
|
139
143
|
collectKnownKeys(config, [], (key, value, path, parentPath) => {
|
|
@@ -204,13 +208,16 @@ export function collectGuideConfigReferences(program) {
|
|
|
204
208
|
for (const target of Object.keys(program.guideConfigs?.legendGroups ?? {})) {
|
|
205
209
|
append(references, reference("mark", target, "legend", target, ["legendGroups", target]));
|
|
206
210
|
}
|
|
211
|
+
for (const target of Object.keys(program.guideConfigs?.suppressedLegends ?? {})) {
|
|
212
|
+
append(references, reference("mark", target, "legend", target, ["suppressedLegends", target]));
|
|
213
|
+
}
|
|
207
214
|
for (const [family, configs] of Object.entries(program.guideConfigs ?? {})) {
|
|
208
215
|
collectKnownKeys(configs, [], (key, value, path, parentPath) => {
|
|
209
216
|
const ownerId = parentPath.length === 0 ? family : parentPath.join(".");
|
|
210
217
|
const relativePath = parentPath.length === 0
|
|
211
218
|
? path
|
|
212
219
|
: path.slice(parentPath.length);
|
|
213
|
-
if (["scale", "colorScale", "sizeScale"].includes(key)) {
|
|
220
|
+
if (["scale", "colorScale", "sizeScale", "shapeScale"].includes(key)) {
|
|
214
221
|
append(references, reference(
|
|
215
222
|
"scale", value, family, ownerId, relativePath
|
|
216
223
|
));
|
|
@@ -240,6 +247,12 @@ export function collectSelectionReferences(program) {
|
|
|
240
247
|
append(references, reference(
|
|
241
248
|
"mark", config.target, "selection", id, ["target"]
|
|
242
249
|
));
|
|
250
|
+
append(references, reference(
|
|
251
|
+
"selection", config.sourceSelection, "selection", id, ["sourceSelection"]
|
|
252
|
+
));
|
|
253
|
+
append(references, reference(
|
|
254
|
+
"data", config.selector?.rows?.data, "selection", id, ["selector", "rows", "data"]
|
|
255
|
+
));
|
|
243
256
|
}
|
|
244
257
|
for (const [id, config] of Object.entries(
|
|
245
258
|
program.materializationConfigs.highlights ?? {}
|
|
@@ -250,6 +263,9 @@ export function collectSelectionReferences(program) {
|
|
|
250
263
|
append(references, reference(
|
|
251
264
|
"selection", config.selection, "highlight", id, ["selection"]
|
|
252
265
|
));
|
|
266
|
+
append(references, reference(
|
|
267
|
+
"selection", config.groupSelection, "highlight", id, ["groupSelection"]
|
|
268
|
+
));
|
|
253
269
|
}
|
|
254
270
|
return freezeOwned(references);
|
|
255
271
|
}
|
package/src/core/validation.js
CHANGED
|
@@ -127,7 +127,14 @@ export function validateFontWeight(value, label, numberType = "number") {
|
|
|
127
127
|
return value;
|
|
128
128
|
}
|
|
129
129
|
|
|
130
|
+
export function validateTextAlignment(style, label) {
|
|
131
|
+
if (Object.hasOwn(style, "align") && !["left", "center", "right"].includes(style.align)) {
|
|
132
|
+
throw new TypeError(`${label} align requires left, center or right.`);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
130
136
|
export function validateTextStyle(config, label) {
|
|
137
|
+
validateTextAlignment(config, label);
|
|
131
138
|
validateNonEmptyString(config.color, `${label} color`);
|
|
132
139
|
validatePositiveFinite(config.fontSize, `${label} fontSize`);
|
|
133
140
|
validateNonEmptyString(config.fontFamily, `${label} fontFamily`);
|
package/src/core/vocabulary.js
CHANGED
|
@@ -253,7 +253,7 @@ const marks = (...types) => Object.freeze(types);
|
|
|
253
253
|
export const APPEARANCE_MARK_TYPES = Object.freeze({
|
|
254
254
|
encodeColor: marks('point', 'line', 'bar', 'area', 'arc', 'rect', 'text'),
|
|
255
255
|
encodeStroke: marks('point', 'line', 'area', 'bar', 'rect', 'arc', 'rule', 'tick'),
|
|
256
|
-
encodeOpacity: marks('point', 'rule', 'line', 'bar', 'rect'),
|
|
256
|
+
encodeOpacity: marks('point', 'rule', 'line', 'area', 'bar', 'rect'),
|
|
257
257
|
encodeStrokeWidth: marks('rule', 'line', 'rect'),
|
|
258
258
|
encodeStrokeDash: marks('line', 'rule'),
|
|
259
259
|
encodeSize: marks('point'),
|
|
@@ -261,6 +261,12 @@ export const APPEARANCE_MARK_TYPES = Object.freeze({
|
|
|
261
261
|
encodeAngle: marks('point', 'tick')
|
|
262
262
|
});
|
|
263
263
|
|
|
264
|
+
// Constant appearance can support more marks than field mapping. Inspection
|
|
265
|
+
// and execution share this distinction instead of advertising unusable fields.
|
|
266
|
+
export const FIXED_APPEARANCE_MARK_TYPES = Object.freeze({
|
|
267
|
+
encodeStrokeWidth: marks('rule', 'line', 'rect', 'point', 'bar', 'area', 'arc', 'tick')
|
|
268
|
+
});
|
|
269
|
+
|
|
264
270
|
// Generated default action methods: start
|
|
265
|
-
export const DEFAULT_ACTION_METHODS = /* @__PURE__ */ Object.freeze(["createCanvas","editCanvas","fitCanvas","applyTextMetrics","removeTextMetrics","applyTheme","removeTheme","createData","reviseData","removeData","removeScale","removeCoordinate","bindMarkData","filterData","createSummaryData","createBinData","createLookupData","createFlattenData","createFoldData","createComputedData","createNormalizedData","createSortedData","createCompleteData","createImputedData","createStackData","createDensityData","createRegressionData","createRegressionStatisticsData","createWindowData","createTimeUnitData","createBin2DData","editBin2DData","editDerivedData","editComputedData","editFilteredData","editLookupData","editFlattenData","editFoldData","editSummaryData","editBinData","editTimeUnitData","editWindowData","editDensityData","editStackData","editRegressionData","editRegressionStatisticsData","editIntervalData","editECDFData","editNormalizedData","editSortedData","editCompleteData","editImputedData","createIntervalData","createECDFData","createPointMark","createTickMark","createStatisticalReference","createReferenceLine","createReferenceBand","createMarkLabels","editMarkLabelSelection","editMarkLabelPlacement","removeMarkLabels","createAnnotation","createTextMark","editTextMark","layoutLabels","removeLabelLayout","editPointMark","editTickMark","jitterPoints","removeJitter","packPoints","removePointPacking","removeMark","createLineMark","editLineMark","createBarMark","editBarMark","createAreaMark","createRuleMark","editRuleMark","editAreaMark","encodeX","encodeY","encodeChannels","encodeX2","encodeColor","encodeStrokeDash","encodeStroke","encodeStrokeWidth","encodeSize","encodeShape","encodeAngle","encodeOpacity","encodeRadius","encodeTheta","encodeR","encodePointRadius","removePointRadius","encodeXOffset","encodeYOffset","encodeY2","encodeYRange","encodeXRange","encodeGroup","encodePathOrder","encodeParallelCoordinates","removePathOrder","orderCategories","removeCategoryOrder","removeEncoding","encodeText","encodeDx","encodeDy","encodeHistogram","encodeDensity","editDensity","encodeHorizon","editHorizon","encodeBarWidth","createRegression","editRegression","createErrorBar","editErrorBar","createErrorBand","editErrorBand","editErrorBandBoundary","createBoxPlot","editBoxPlot","createGradientPlot","editGradientPlot","createViolinPlot","editViolinPlot","createParallelAxes","createParallelAxis","editParallelAxis","removeParallelAxis","removeParallelAxes","createAxes","createXAxis","createYAxis","createXAxisLine","createYAxisLine","editXAxisLine","editYAxisLine","createXAxisTicks","createYAxisTicks","editXAxisTicks","editYAxisTicks","createXAxisLabels","createYAxisLabels","editXAxisLabels","editYAxisLabels","createXAxisTicksAndLabels","createYAxisTicksAndLabels","editXAxisTicksAndLabels","editYAxisTicksAndLabels","createXAxisTitle","createYAxisTitle","editXAxisTitle","editYAxisTitle","editAxes","editXAxis","editYAxis","removeXAxis","removeYAxis","createGrid","createHorizontalGrid","createVerticalGrid","editHorizontalGrid","editVerticalGrid","editGrid","removeGrid","createLegend","editLegend","editLegendBlock","editLegendLayout","editLegendLabels","editLegendTitle","editLegendSymbols","editLegendBorder","createGuides","removeLegend","createTitle","editTitle","removeTitle","createCoordinate","editCoordinate","createScale","editScale","editXScale","editYScale","editXOffsetScale","editYOffsetScale","editParallelScale","editThetaScale","editRScale","editColorScale","editStrokeScale","editSizeScale","editOpacityScale","editShapeScale","editStrokeWidthScale","editStrokeDashScale","createDerivedData","createRegressionBand","editRegressionBand","createRegressionLine","editRegressionLine","filterMarks","removeMarkFilter","selectMarks","editMarkSelection","removeMarkHighlight","removeMarkSelection","highlightMarks","editSemantic","createGraphics","editGraphics","createArcMark","editArcMark","createThetaAxis","createRadialAxis","editThetaAxis","editRadialAxis","removeThetaAxis","removeRadialAxis","createThetaAxisLine","editThetaAxisLine","createRadialAxisLine","editRadialAxisLine","createThetaAxisTicks","editThetaAxisTicks","createRadialAxisTicks","editRadialAxisTicks","createThetaAxisLabels","editThetaAxisLabels","createRadialAxisLabels","editRadialAxisLabels","createThetaAxisTitle","editThetaAxisTitle","createRadialAxisTitle","editRadialAxisTitle","createThetaGrid","createRadialGrid","editThetaGrid","editRadialGrid","editCompositionLayout","replaceCompositionChild","insertCompositionChild","removeCompositionChild","reorderCompositionChildren","facet","facetGrid","repeatCharts","editFacetSource","editFacetHeaders","editFacetScales","editFacetGuides","createScatterPlot","createTextPlot","createDotPlot","createLollipopPlot","createDumbbellPlot","editEndpointPlot","createECDFPlot","editECDFPlot","createIntervalPlot","createRegressionPlot","createLinePlot","createPolarScatterPlot","createPolarLinePlot","createRadarPlot","editRadarPlot","createRugPlot","createStripPlot","createBeeswarmPlot","createRaincloudPlot","editRaincloudPlot","createBarPlot","createHistogram","createHeatmap","createParallelCoordinates","createRectMark","editRectMark","createPiePlot","createRosePlot","createRadialBarPlot","createDensityPlot","createHorizonPlot","createAreaPlot","layoutSeries"]);
|
|
271
|
+
export const DEFAULT_ACTION_METHODS = /* @__PURE__ */ Object.freeze(["createCanvas","editCanvas","fitCanvas","applyTextMetrics","removeTextMetrics","applyTheme","removeTheme","createData","reviseData","removeData","removeScale","removeCoordinate","bindMarkData","filterData","createSummaryData","createBinData","createLookupData","createFlattenData","createFoldData","createComputedData","createNormalizedData","createSortedData","createCompleteData","createImputedData","createStackData","createDensityData","createRegressionData","createRegressionStatisticsData","createWindowData","createTimeUnitData","createBin2DData","editBin2DData","editDerivedData","editComputedData","editFilteredData","editLookupData","editFlattenData","editFoldData","editSummaryData","editBinData","editTimeUnitData","editWindowData","editDensityData","editStackData","editRegressionData","editRegressionStatisticsData","editIntervalData","editECDFData","editNormalizedData","editSortedData","editCompleteData","editImputedData","createIntervalData","createECDFData","createPointMark","createTickMark","createStatisticalReference","createReferenceLine","createReferenceBand","createMarkLabels","editMarkLabelSelection","editMarkLabelPlacement","removeMarkLabels","createAnnotation","createTextMark","editTextMark","layoutLabels","removeLabelLayout","editPointMark","editTickMark","jitterPoints","removeJitter","packPoints","removePointPacking","removeMark","createLineMark","editLineMark","createBarMark","editBarMark","createAreaMark","createRuleMark","editRuleMark","editAreaMark","encodeX","encodeY","encodeChannels","encodeX2","encodeColor","encodeStrokeDash","encodeStroke","encodeStrokeWidth","encodeSize","encodeShape","encodeAngle","encodeOpacity","encodeRadius","encodeTheta","encodeR","encodePointRadius","removePointRadius","encodeXOffset","encodeYOffset","encodeY2","encodeYRange","encodeXRange","encodeGroup","encodePathOrder","encodeParallelCoordinates","removePathOrder","orderCategories","removeCategoryOrder","removeEncoding","encodeText","encodeDx","encodeDy","encodeHistogram","encodeDensity","editDensity","encodeHorizon","editHorizon","encodeBarWidth","createRegression","editRegression","createErrorBar","editErrorBar","createErrorBand","editErrorBand","editErrorBandBoundary","createBoxPlot","editBoxPlot","createGradientPlot","editGradientPlot","createViolinPlot","editViolinPlot","createParallelAxes","createParallelAxis","editParallelAxis","removeParallelAxis","removeParallelAxes","createAxes","createXAxis","createYAxis","createXAxisLine","createYAxisLine","editXAxisLine","editYAxisLine","createXAxisTicks","createYAxisTicks","editXAxisTicks","editYAxisTicks","createXAxisLabels","createYAxisLabels","editXAxisLabels","editYAxisLabels","createXAxisTicksAndLabels","createYAxisTicksAndLabels","editXAxisTicksAndLabels","editYAxisTicksAndLabels","createXAxisTitle","createYAxisTitle","editXAxisTitle","editYAxisTitle","editAxes","editXAxis","editYAxis","removeXAxis","removeYAxis","createGrid","createHorizontalGrid","createVerticalGrid","editHorizontalGrid","editVerticalGrid","editGrid","removeGrid","createLegend","editLegend","editLegendBlock","editLegendLayout","editLegendLabels","editLegendTitle","editLegendSymbols","editLegendBorder","createGuides","removeLegend","createTitle","editTitle","removeTitle","createCoordinate","editCoordinate","createScale","editScale","editXScale","editYScale","editXOffsetScale","editYOffsetScale","editParallelScale","editThetaScale","editRScale","editColorScale","editStrokeScale","editSizeScale","editOpacityScale","editShapeScale","editStrokeWidthScale","editStrokeDashScale","createDerivedData","createRegressionBand","editRegressionBand","createRegressionLine","editRegressionLine","filterMarks","removeMarkFilter","selectMarks","editMarkSelection","removeMarkHighlight","removeMarkSelection","highlightMarks","editSemantic","createGraphics","editGraphics","createArcMark","editArcMark","createThetaAxis","createRadialAxis","editThetaAxis","editRadialAxis","removeThetaAxis","removeRadialAxis","createThetaAxisLine","editThetaAxisLine","createRadialAxisLine","editRadialAxisLine","createThetaAxisTicks","editThetaAxisTicks","createRadialAxisTicks","editRadialAxisTicks","createThetaAxisLabels","editThetaAxisLabels","createRadialAxisLabels","editRadialAxisLabels","createThetaAxisTitle","editThetaAxisTitle","createRadialAxisTitle","editRadialAxisTitle","createThetaGrid","createRadialGrid","editThetaGrid","editRadialGrid","editCompositionLayout","replaceCompositionChild","insertCompositionChild","removeCompositionChild","reorderCompositionChildren","facet","facetGrid","repeatCharts","editFacetSource","editFacetHeaders","editFacetScales","editFacetGuides","createScatterPlot","createTextPlot","createDotPlot","createLollipopPlot","createDumbbellPlot","editEndpointPlot","createECDFPlot","editECDFPlot","createIntervalPlot","createRegressionPlot","createLinePlot","createPolarScatterPlot","createPolarLinePlot","createRadarPlot","editRadarPlot","createRugPlot","createStripPlot","createBeeswarmPlot","createRaincloudPlot","editRaincloudPlot","createBarPlot","createHistogram","createHeatmap","createParallelCoordinates","createRectMark","editRectMark","createPiePlot","createRosePlot","createRadialBarPlot","createDensityPlot","createHorizonPlot","createAreaPlot","layoutSeries","createOverlay","editOverlay","createLayerPlot","createAdjacentPlot","editCompositionScales"]);
|
|
266
272
|
// Generated default action methods: end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { validateOptionObject } from "../core/validation.js";
|
|
2
|
+
import { validateUserId } from "../core/identifiers.js";
|
|
3
|
+
|
|
4
|
+
function normalizeTarget(target) {
|
|
5
|
+
if (typeof target === "string") return { childPath: [], id: validateUserId(target, "Action target") };
|
|
6
|
+
validateOptionObject(target, ["childPath", "id", "facetSource"], "Scoped action target");
|
|
7
|
+
if (!Array.isArray(target.childPath) || target.childPath.some(id => typeof id !== "string")) {
|
|
8
|
+
throw new TypeError("Scoped action target requires a childPath array.");
|
|
9
|
+
}
|
|
10
|
+
if (target.facetSource !== undefined && target.facetSource !== true) throw new TypeError("facetSource must be true when supplied.");
|
|
11
|
+
target.childPath.forEach(id => validateUserId(id, "Target child id"));
|
|
12
|
+
return { childPath: [...target.childPath], ...(target.facetSource ? { facetSource: true } : {}), ...(target.id === undefined ? {} : { id: validateUserId(target.id, "Action target id") }) };
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
export function normalizeActionTargets(value) {
|
|
17
|
+
const requested = Array.isArray(value) ? value : [value];
|
|
18
|
+
if (!requested.length) throw new Error("Action target list must not be empty.");
|
|
19
|
+
const targets = requested.map(normalizeTarget);
|
|
20
|
+
if (new Set(targets.map(target => JSON.stringify(target))).size !== targets.length) {
|
|
21
|
+
throw new Error("Action targets must be unique.");
|
|
22
|
+
}
|
|
23
|
+
return targets;
|
|
24
|
+
}
|
package/src/grammar/bin2d.js
CHANGED
|
@@ -370,3 +370,10 @@ export function requestedBin2DTransform(transform) {
|
|
|
370
370
|
const { resolved: _resolved, ...requested } = transform;
|
|
371
371
|
return cloneAndFreeze(requested);
|
|
372
372
|
}
|
|
373
|
+
|
|
374
|
+
// Preserve caller-authored aliases; only the canonical generated count field
|
|
375
|
+
// receives a human-readable title when guide creation is deferred.
|
|
376
|
+
export function inferBin2DFieldTitle(transform, id, field) {
|
|
377
|
+
return transform?.type === 'bin2d' && transform.as.count === field &&
|
|
378
|
+
normalizeOutputFields(id, undefined, false).count === field ? 'Count' : field;
|
|
379
|
+
}
|
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
import {sameOrderedValues,validateOptionObject,validateUnitInterval,validateNonNegativeFinite,validateNonEmptyString} from '../core/validation.js';
|
|
2
|
-
import {isContinuousColorScaleType} from './scales/types.js';
|
|
2
|
+
import {isContinuousColorScaleType,isDiscretizedColorScaleType} from './scales/types.js';
|
|
3
3
|
import {isContinuousSizeScaleType} from './scales/size.js';
|
|
4
4
|
|
|
5
|
-
export function isColorSizeChannels(channels) {
|
|
6
|
-
return Array.isArray(channels)&&
|
|
5
|
+
export function isColorSizeChannels(channels,combine='color-size') {
|
|
6
|
+
return Array.isArray(channels)&&(combine==='color-size-shape'
|
|
7
|
+
? channels.length===3&&channels.includes('shape')&&channels.includes('size')&&['color','stroke'].some(c=>channels.includes(c))
|
|
8
|
+
: channels.length===2&&channels.includes('color')&&channels.includes('size'));
|
|
7
9
|
}
|
|
8
10
|
|
|
9
|
-
export function resolveColorSizeMapping({layer,scales,pointConfig,coordinate}) {
|
|
11
|
+
export function resolveColorSizeMapping({layer,scales,pointConfig,coordinate,combine='color-size',channels}) {
|
|
12
|
+
if(combine==='color-size-shape')return resolveColorSizeShapeMapping({layer,scales,coordinate,channels});
|
|
10
13
|
const color=layer?.encoding?.color,size=layer?.encoding?.size;
|
|
11
14
|
const colorScale=scales[color?.scale],sizeScale=scales[size?.scale];
|
|
12
15
|
if(layer?.mark?.type!=='point'||coordinate?.type!=='cartesian'||
|
|
@@ -26,7 +29,20 @@ export function resolveColorSizeMapping({layer,scales,pointConfig,coordinate}) {
|
|
|
26
29
|
return {target:layer.id,field:color.field,fieldType:'quantitative',colorScale:color.scale,sizeScale:size.scale,domain};
|
|
27
30
|
}
|
|
28
31
|
|
|
29
|
-
export function normalizeColorSizeSymbol(value,previous) {
|
|
32
|
+
export function normalizeColorSizeSymbol(value,previous,mapping) {
|
|
33
|
+
if(mapping?.combine==='color-size-shape') {
|
|
34
|
+
if(value===undefined)return {...(previous??{type:'point'})};
|
|
35
|
+
if(value==='auto')return {type:'point'};
|
|
36
|
+
validateOptionObject(value,['type','fill','stroke','strokeWidth','opacity'],'Color-size-shape symbol');
|
|
37
|
+
if(value.type!==undefined&&value.type!=='point')throw new Error('Combined symbols require type point.');
|
|
38
|
+
const symbol={type:'point',...previous,...value};
|
|
39
|
+
const mapped=mapping.colorChannel==='stroke'?'stroke':'fill';
|
|
40
|
+
if(Object.hasOwn(symbol,mapped))throw new Error(`Combined symbols cannot override mapped ${mapped}.`);
|
|
41
|
+
for(const key of ['fill','stroke'])if(symbol[key]!==undefined)validateNonEmptyString(symbol[key],`Combined symbol ${key}`);
|
|
42
|
+
if(symbol.strokeWidth!==undefined)validateNonNegativeFinite(symbol.strokeWidth,'Combined symbol strokeWidth');
|
|
43
|
+
if(symbol.opacity!==undefined)validateUnitInterval(symbol.opacity,'Combined symbol opacity');
|
|
44
|
+
return symbol;
|
|
45
|
+
}
|
|
30
46
|
if(value===undefined)return {...(previous??{type:'point',strokeWidth:0,opacity:1})};
|
|
31
47
|
if(value==='auto')return {type:'point',strokeWidth:0,opacity:1};
|
|
32
48
|
validateOptionObject(value,['type','stroke','strokeWidth','opacity'],'Color-size symbol');
|
|
@@ -37,3 +53,38 @@ export function normalizeColorSizeSymbol(value,previous) {
|
|
|
37
53
|
validateUnitInterval(symbol.opacity,'Color-size symbol opacity');
|
|
38
54
|
return symbol;
|
|
39
55
|
}
|
|
56
|
+
|
|
57
|
+
function resolveColorSizeShapeMapping({layer,scales,coordinate,channels}) {
|
|
58
|
+
const hues=channels===undefined?['color','stroke'].filter(c=>layer?.encoding?.[c]?.scale!==undefined)
|
|
59
|
+
:channels.filter(c=>['color','stroke'].includes(c));
|
|
60
|
+
if(hues.length!==1)throw new Error('Color-size-shape requires one color or stroke channel; select channels explicitly.');
|
|
61
|
+
const colorChannel=hues[0],hue=layer?.encoding?.[colorChannel],size=layer?.encoding?.size,shape=layer?.encoding?.shape;
|
|
62
|
+
const colorScale=scales[hue?.scale],sizeScale=scales[size?.scale],shapeScale=scales[shape?.scale];
|
|
63
|
+
if(layer?.mark?.type!=='point'||coordinate?.type!=='cartesian'||
|
|
64
|
+
hue?.fieldType!=='quantitative'||size?.fieldType!=='quantitative'||
|
|
65
|
+
!['nominal','ordinal'].includes(shape?.fieldType)||typeof hue.field!=='string'||
|
|
66
|
+
[size,shape].some(e=>e.field!==hue.field||e.data!==hue.data||e.aggregate!==hue.aggregate)||
|
|
67
|
+
!isDiscretizedColorScaleType(colorScale?.type)||!isDiscretizedColorScaleType(sizeScale?.type)||shapeScale?.type!=='ordinal') {
|
|
68
|
+
throw new Error('Color-size-shape requires one Cartesian point owner with the same field and grain on interval hue/size and ordinal shape.');
|
|
69
|
+
}
|
|
70
|
+
if(!sameOrderedValues(colorScale.thresholds,sizeScale.thresholds))throw new Error('Combined interval scales require identical threshold boundaries.');
|
|
71
|
+
return {target:layer.id,field:hue.field,fieldType:'quantitative',colorScale:hue.scale,sizeScale:size.scale,
|
|
72
|
+
shapeScale:shape.scale,colorChannel,domain:[...colorScale.domain]};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function validateColorSizeShapeSamples(sampling,scale) {
|
|
76
|
+
const cuts=scale.thresholds;
|
|
77
|
+
if(sampling.mode!=='values'||sampling.values.length!==cuts.length+1||sampling.values.some((value,index)=>
|
|
78
|
+
!Number.isFinite(value)||index>0&&value<cuts[index-1]||index<cuts.length&&value>=cuts[index])) {
|
|
79
|
+
throw new Error('Color-size-shape requires one explicit finite value in each ordered threshold interval.');
|
|
80
|
+
}
|
|
81
|
+
return [...sampling.values];
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function colorSizeScaleIds(config) {
|
|
85
|
+
return [config.colorScale,config.sizeScale,...(config.shapeScale===undefined?[]:[config.shapeScale])];
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function colorSizeChannels(config) {
|
|
89
|
+
return [config.colorChannel??'color','size',...(config.shapeScale===undefined?[]:['shape'])];
|
|
90
|
+
}
|
|
@@ -91,3 +91,13 @@ export function resolveDisplayLabelColor(value, map, fallback) {
|
|
|
91
91
|
const entry = map?.find(entry => sameDisplayLabelValue(entry.value, value));
|
|
92
92
|
return entry?.color ?? fallback;
|
|
93
93
|
}
|
|
94
|
+
|
|
95
|
+
export function normalizeLegendDisplayLabelMap(descriptor, map, scaleType) {
|
|
96
|
+
if (["size", "colorSize"].includes(descriptor.kind) && descriptor.family === "sampled") {
|
|
97
|
+
return normalizeNumericDisplayLabelMap(map, "editLegendBlock labelMap");
|
|
98
|
+
}
|
|
99
|
+
if (descriptor.family !== "categorical" && !(descriptor.family === "discrete-size" && scaleType === "ordinal")) {
|
|
100
|
+
throw new Error("editLegendBlock labelMap requires a categorical or continuous size block.");
|
|
101
|
+
}
|
|
102
|
+
return normalizeDisplayLabelMap(map, "editLegendBlock labelMap");
|
|
103
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{validateOptionObject as o,validateNonEmptyString as r,validatePositiveFinite as a,validateNonNegativeFinite as c}from"../core/validation.js";import{resolveRotation as s}from"./rotation.js";const f=Object.freeze(["text","fontSize","fontFamily","fontWeight","color","offset","align","rotation","alongOffset"]);function d(e){if(o(e,f,"Facet header title",{allowEmpty:!1}),r(e.text,"Facet header title text"),/[\
|
|
1
|
+
import{validateOptionObject as o,validateNonEmptyString as r,validatePositiveFinite as a,validateNonNegativeFinite as c}from"../core/validation.js";import{resolveRotation as s}from"./rotation.js";const f=Object.freeze(["text","fontSize","fontFamily","fontWeight","color","offset","align","rotation","alongOffset"]);function d(e){if(o(e,f,"Facet header title",{allowEmpty:!1}),r(e.text,"Facet header title text"),/[\u2028\u2029]/u.test(e.text))throw new TypeError("Facet header title line breaks must use LF, CR or CRLF.");e.fontSize!==void 0&&a(e.fontSize,"Facet header title fontSize");for(const t of["fontFamily","color"])e[t]!==void 0&&r(e[t],`Facet header title ${t}`);if(e.fontWeight!==void 0&&!(typeof e.fontWeight=="string"&&e.fontWeight.length>0||Number.isFinite(e.fontWeight)))throw new TypeError("Facet header title fontWeight must be a non-empty string or finite number.");if(e.offset!==void 0&&c(e.offset,"Facet header title offset"),e.align!==void 0&&!["start","center","end"].includes(e.align))throw new TypeError("Facet header title align must be start, center, or end.");for(const t of["rotation","alongOffset"])if(Object.hasOwn(e,t)&&!Number.isFinite(e[t]))throw new TypeError(`Facet header title ${t} must contain finite normalized radians or coordinates.`)}function h(e,t){if(t===!1)return;o(t,f,"Facet header title",{allowEmpty:!1});for(const i of Object.keys(t))if(t[i]===void 0)throw new TypeError(`Facet header title ${i} must not be undefined.`);const n={...e,...t};return Object.hasOwn(t,"rotation")&&(n.rotation=s(t.rotation,"Facet header title rotation")),d(n),n}function w(e){for(const t of e.common===void 0?[e]:[e.common,e.row,e.column]){if(t!==void 0&&Object.hasOwn(t,"anchor")&&!["plot","content","view"].includes(t.anchor))throw new TypeError("Facet header anchor must be plot, content, or view.");if(t?.rotation!==void 0&&!Number.isFinite(t.rotation))throw new TypeError("Facet header rotation must contain finite normalized radians.");t?.title!==void 0&&d(t.title)}if(e.title!==void 0||e.common?.title!==void 0)throw new TypeError("Facet header title requires an explicit row or column role.");if(e.mode!=="roles"&&(e.row?.title!==void 0||e.column?.title!==void 0))throw new TypeError("Facet field titles require role header mode.")}export{h as normalizeFacetHeaderTitle,w as validateFacetHeaderRotations,d as validateFacetHeaderTitle};
|
|
@@ -115,9 +115,16 @@ export function resolveFacetFamily(semanticSpec) {
|
|
|
115
115
|
semanticSpec.layers.length === 0) {
|
|
116
116
|
throw new Error("facet requires at least one materializable layer.");
|
|
117
117
|
}
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
118
|
+
// Literal positioned notes decorate a chart rather than define its data
|
|
119
|
+
// coordinate family. Keep validating/materializing them, but do not confuse
|
|
120
|
+
// a plot-space Cartesian annotation with a second Cartesian chart in a polar
|
|
121
|
+
// or parallel facet. A text-only chart still supplies its own primary layer.
|
|
122
|
+
const annotations = semanticSpec.layers.filter(layer => layer.mark?.type === 'text' &&
|
|
123
|
+
['x', 'y', 'text'].every(channel => Object.hasOwn(layer.encoding?.[channel] ?? {}, 'datum')));
|
|
124
|
+
const dataLayers = semanticSpec.layers.filter(layer =>
|
|
125
|
+
!(layer.mark?.type === 'text' && layer.source !== undefined) && !annotations.includes(layer));
|
|
126
|
+
const primary = dataLayers.length ? dataLayers : annotations;
|
|
127
|
+
for (const note of annotations) classifyPrimaryLayer(semanticSpec, note);
|
|
121
128
|
if (primary.length === 0) {
|
|
122
129
|
throw new Error("facet requires at least one primary materializable layer.");
|
|
123
130
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import {colorSizeScaleIds} from "../colorSizeLegend.js";
|
|
1
2
|
import { cloneAndFreeze, isPlainObject } from "../../core/immutable.js";
|
|
2
3
|
|
|
3
4
|
|
|
@@ -167,7 +168,7 @@ function requireLegendChildren(children) {
|
|
|
167
168
|
}
|
|
168
169
|
|
|
169
170
|
function scaleIds(config, kind) {
|
|
170
|
-
const ids = kind === "colorSize" ?
|
|
171
|
+
const ids = kind === "colorSize" ? colorSizeScaleIds(config)
|
|
171
172
|
: config.scales ?? (config.scale === undefined ? [] : [config.scale]);
|
|
172
173
|
if (!Array.isArray(ids) || ids.length === 0 || !ids.every(id =>
|
|
173
174
|
typeof id === "string" && id.length > 0
|