ggaction 0.0.23 → 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 +65 -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 +6 -4
- 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/basicBin2d.js +1 -1
- package/src/actions/data/bin2d.js +2 -2
- 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 +4 -2
- 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/components.js +5 -0
- package/src/actions/guides/axes/edit.js +1 -188
- package/src/actions/guides/axes/labels.js +2 -537
- 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/tickGroups.js +16 -1
- package/src/actions/guides/axes/ticks.js +33 -24
- package/src/actions/guides/axes/titles.js +1 -389
- 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 +23 -10
- package/src/core/action.js +41 -25
- package/src/core/compositionScales.js +12 -0
- package/src/core/compositionState.js +20 -3
- package/src/core/facetGap.js +44 -0
- 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/axisTextPlacement.js +1 -1
- package/src/grammar/bin2d.js +41 -4
- 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 +32 -10
- package/src/layout/compositionLegend.js +91 -0
- package/src/layout/facetContentFlow.js +15 -7
- package/src/layout/facetHeaderTitles.js +1 -1
- package/src/layout/facets.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/layout/title.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/planner.js +19 -4
- 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 +116 -25
- 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 +4 -2
- package/types/index.d.ts +14 -0
- package/types/inspection.d.ts +34 -0
- package/types/program.d.ts +572 -372
|
@@ -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";
|
|
@@ -24,10 +25,10 @@ import { findCanvasGraphic } from
|
|
|
24
25
|
|
|
25
26
|
const OPTIONS = Object.freeze([
|
|
26
27
|
"text", "subtitle", "position", "align", "offset", "alongOffset", "gap", "titleStyle",
|
|
27
|
-
"subtitleStyle", "maxWidth", "wrap", "lineHeight"
|
|
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`);
|
|
@@ -101,6 +103,9 @@ function normalizeConfig(args, defaults, { hasSubtitle }) {
|
|
|
101
103
|
const maxWidth = args.maxWidth ?? defaults.maxWidth;
|
|
102
104
|
const wrap = args.wrap ?? defaults.wrap ?? (maxWidth === undefined ? undefined : "word");
|
|
103
105
|
const lineHeight = args.lineHeight ?? defaults.lineHeight;
|
|
106
|
+
const subtitleLineHeight = args.subtitleLineHeight === undefined
|
|
107
|
+
? defaults.subtitleLineHeight
|
|
108
|
+
: args.subtitleLineHeight;
|
|
104
109
|
if (!POSITIONS.includes(position)) {
|
|
105
110
|
throw new Error(`Unsupported title position "${position}".`);
|
|
106
111
|
}
|
|
@@ -128,6 +133,12 @@ function normalizeConfig(args, defaults, { hasSubtitle }) {
|
|
|
128
133
|
)) {
|
|
129
134
|
throw new RangeError("Chart title lineHeight must cover every visible fontSize.");
|
|
130
135
|
}
|
|
136
|
+
if (subtitleLineHeight !== undefined && (
|
|
137
|
+
!Number.isFinite(subtitleLineHeight) ||
|
|
138
|
+
subtitleLineHeight < subtitleStyle.fontSize
|
|
139
|
+
)) {
|
|
140
|
+
throw new RangeError("Chart subtitleLineHeight must cover subtitleStyle.fontSize.");
|
|
141
|
+
}
|
|
131
142
|
return {
|
|
132
143
|
position,
|
|
133
144
|
align,
|
|
@@ -136,6 +147,7 @@ function normalizeConfig(args, defaults, { hasSubtitle }) {
|
|
|
136
147
|
gap,
|
|
137
148
|
titleStyle,
|
|
138
149
|
subtitleStyle,
|
|
150
|
+
...(subtitleLineHeight === undefined ? {} : { subtitleLineHeight }),
|
|
139
151
|
...(maxWidth === undefined ? {} : { maxWidth, wrap, ...(lineHeight === undefined
|
|
140
152
|
? {}
|
|
141
153
|
: { lineHeight }) })
|
|
@@ -241,11 +253,12 @@ export function resolveTitleLayout(program, config) {
|
|
|
241
253
|
: config.position === "top"
|
|
242
254
|
? 16 + config.offset
|
|
243
255
|
: plot.y + plot.height + config.offset;
|
|
244
|
-
|
|
256
|
+
const left = x - (config.align === "right" ? block.width : config.align === "center" ? block.width / 2 : 0);
|
|
257
|
+
component = (lines, centers, style) => ({
|
|
245
258
|
lines,
|
|
246
|
-
x,
|
|
259
|
+
x: style.align === undefined ? x : alignedTextAnchor(left, block.width, style.align),
|
|
247
260
|
y: centers.map(value => blockTop + value),
|
|
248
|
-
textAlign: config.align,
|
|
261
|
+
textAlign: style.align ?? config.align,
|
|
249
262
|
rotation: 0,
|
|
250
263
|
explicitRotation: config.position !== "top" || lines.length > 1
|
|
251
264
|
});
|
|
@@ -257,19 +270,19 @@ export function resolveTitleLayout(program, config) {
|
|
|
257
270
|
? 16 + config.offset
|
|
258
271
|
: canvas.properties.width - 16 + config.offset;
|
|
259
272
|
const mapX = value => config.position === "left" ? edge + value : edge - value;
|
|
260
|
-
component = (lines, centers) => ({
|
|
273
|
+
component = (lines, centers, style) => ({
|
|
261
274
|
lines,
|
|
262
275
|
x: centers.map(mapX),
|
|
263
|
-
y,
|
|
264
|
-
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",
|
|
265
278
|
rotation,
|
|
266
279
|
explicitRotation: true
|
|
267
280
|
});
|
|
268
281
|
}
|
|
269
|
-
const title = component(block.titleLines, block.titleCenters);
|
|
282
|
+
const title = component(block.titleLines, block.titleCenters, config.titleStyle);
|
|
270
283
|
const subtitle = block.subtitleLines.length === 0
|
|
271
284
|
? undefined
|
|
272
|
-
: component(block.subtitleLines, block.subtitleCenters);
|
|
285
|
+
: component(block.subtitleLines, block.subtitleCenters, config.subtitleStyle);
|
|
273
286
|
const titleBounds = unionTitleBounds([
|
|
274
287
|
resolveTitleComponentBounds(title, config.titleStyle, program.materializationConfigs.textMetrics),
|
|
275
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,5 @@
|
|
|
1
|
+
import { normalizeCompositionShares } from "./compositionScales.js";
|
|
2
|
+
import { normalizeFacetGap } from "./facetGap.js";
|
|
1
3
|
import { normalizeFacetAxes } from "./facetAxisPolicy.js";
|
|
2
4
|
import { isOwned, isPlainObject } from "./immutable.js";
|
|
3
5
|
import { ownProgramState } from "./programState.js";
|
|
@@ -8,6 +10,11 @@ import {
|
|
|
8
10
|
DEFAULT_FACET_TOP_LANE
|
|
9
11
|
} from "./vocabulary.js";
|
|
10
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
|
+
|
|
11
18
|
// Primitive graphics/trace edits retain both immutable owners. Validate their
|
|
12
19
|
// relationship once, rather than scanning every facet cell on every primitive.
|
|
13
20
|
const validatedCompositions = new WeakMap();
|
|
@@ -242,10 +249,12 @@ function validateCompositionChildren(compositionSpec, childIds, facet) {
|
|
|
242
249
|
}
|
|
243
250
|
|
|
244
251
|
function validateLayout(compositionSpec) {
|
|
245
|
-
if (
|
|
252
|
+
if (compositionSpec.type === "facet") {
|
|
253
|
+
normalizeFacetGap(compositionSpec.gap, "compositionSpec.gap");
|
|
254
|
+
} else if (!Number.isFinite(compositionSpec.gap) || compositionSpec.gap < 0) {
|
|
246
255
|
throw new RangeError("compositionSpec.gap must be a non-negative finite number.");
|
|
247
256
|
}
|
|
248
|
-
if (!["start", "center", "end"].includes(compositionSpec.align)) {
|
|
257
|
+
if (!(compositionSpec.type === "facet" ? ["start", "center", "end"] : ["start", "center", "end", "plot"]).includes(compositionSpec.align)) {
|
|
249
258
|
throw new Error("compositionSpec.align must be start, center, or end.");
|
|
250
259
|
}
|
|
251
260
|
if (!isPlainObject(compositionSpec.padding)) {
|
|
@@ -281,7 +290,7 @@ export function ownCompositionSpec(compositionSpec, children) {
|
|
|
281
290
|
}
|
|
282
291
|
const allowed = [
|
|
283
292
|
"id", "type", "direction", "children", "columns", "gap", "align",
|
|
284
|
-
"padding", "facet", "spacing", "topLane"
|
|
293
|
+
"padding", "facet", "spacing", "topLane", "share", "legends"
|
|
285
294
|
];
|
|
286
295
|
const unknown = Object.keys(compositionSpec).find(key => !allowed.includes(key));
|
|
287
296
|
if (unknown !== undefined) {
|
|
@@ -314,6 +323,14 @@ export function ownCompositionSpec(compositionSpec, children) {
|
|
|
314
323
|
) {
|
|
315
324
|
throw new Error("Concat compositionSpec does not accept facet properties.");
|
|
316
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
|
+
}
|
|
317
334
|
validateLayout(compositionSpec);
|
|
318
335
|
const owned = ownProgramState(compositionSpec);
|
|
319
336
|
if (isOwned(children)) {
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { cloneAndFreeze, isPlainObject } from "./immutable.js";
|
|
2
|
+
import { annotateError } from "./diagnostics.js";
|
|
3
|
+
export function validateCompositionSpacing(value, label) {
|
|
4
|
+
if (!Number.isFinite(value) || value < 0) {
|
|
5
|
+
throw new RangeError(`${label} must be a non-negative finite number.`);
|
|
6
|
+
}
|
|
7
|
+
return value;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/** Keep scalar intent intact; a facet pair always declares both axes. */
|
|
11
|
+
export function normalizeFacetGap(value, label = "Facet gap") {
|
|
12
|
+
if (!isPlainObject(value)) return validateCompositionSpacing(value, label);
|
|
13
|
+
const keys = Reflect.ownKeys(value);
|
|
14
|
+
if (keys.length !== 2 || !keys.includes("x") || !keys.includes("y")) {
|
|
15
|
+
throw annotateError(new TypeError(`${label} must contain exactly x and y.`), {
|
|
16
|
+
code: "invalid-value", optionPath: "gap", actual: value
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
const result = {};
|
|
20
|
+
for (const axis of ["x", "y"]) {
|
|
21
|
+
try {
|
|
22
|
+
result[axis] = validateCompositionSpacing(value[axis], `${label}.${axis}`);
|
|
23
|
+
} catch (error) {
|
|
24
|
+
throw annotateError(error, {
|
|
25
|
+
code: "invalid-value", optionPath: `gap.${axis}`, actual: value[axis], limit: 0
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return cloneAndFreeze(result);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Accept only a previously normalized scalar or pair. */
|
|
33
|
+
export function facetGapAxis(gap, axis) {
|
|
34
|
+
return typeof gap === "number" ? gap : gap[axis];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Overflow remains axis-specific for a pair; scalar callers keep their old error. */
|
|
38
|
+
export function assertFacetGapFinite(gap, axis, values, message) {
|
|
39
|
+
if (values.every(Number.isFinite)) return;
|
|
40
|
+
const error = new RangeError(message);
|
|
41
|
+
throw typeof gap === "number" ? error : annotateError(error, {
|
|
42
|
+
code: "invalid-value", optionPath: `gap.${axis}`, actual: gap[axis]
|
|
43
|
+
});
|
|
44
|
+
}
|
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
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{resolveTextLines as
|
|
1
|
+
import{validateKeys as a,validateNonNegativeFinite as o}from"../core/validation.js";import{isPlainObject as s}from"../core/immutable.js";import{resolveTextLines as b}from"../core/textMetrics.js";function c(e,{title:t=!1}={}){if(Object.hasOwn(e,"align")&&!["left","center","right"].includes(e.align))throw new TypeError("Axis text align must be left, center, or right.");if(t){if(Object.hasOwn(e,"overlap")&&!["error","allow"].includes(e.overlap))throw new TypeError("Axis title overlap must be error or allow.");if(Object.hasOwn(e,"baseline")&&!["top","hanging","middle","alphabetic","ideographic","bottom"].includes(e.baseline))throw new TypeError("Axis title baseline must be a supported text baseline.");if(Object.hasOwn(e,"offset")&&s(e.offset)){if(a(e.offset,["from","gap"],"Axis title offset"),e.offset.from!=="labels"||!Object.hasOwn(e.offset,"gap"))throw new TypeError("Axis title offset requires from: labels and gap.");o(e.offset.gap,"Axis title label gap")}for(const i of["offset","alongOffset"])if(!(i==="offset"&&f(e.offset))&&Object.hasOwn(e,i)&&!Number.isFinite(e[i]))throw new TypeError(`Axis title ${i} must be a finite number.`)}}function f(e){return s(e)&&e.from==="labels"}function h(e){return e?.inferredOffset===!0||f(e?.offset)}function m(e){return(e?.lines??[{text:e?.text}]).some(t=>typeof t.text=="string"&&t.text.length>0)}function w(e,t){if(!/[\r\n]/u.test(e))return;const i=t.baseline??"middle",r=b(e,{fontSize:t.fontSize,align:t.align??"center",baseline:i,...i==="middle"?{blockAlign:"middle"}:{}});if(["bottom","ideographic"].includes(i)){const l=(r.length-1)*t.fontSize*1.2;return r.map(n=>({...n,y:n.y-l}))}return r.length>1?r:void 0}function u(e){return e?.overlap==="allow"}function g(e,{allowReset:t=!1}={}){e===void 0||t&&e==="auto"||o(e,"Label flushThreshold")}export{m as axisLabelHasText,u as axisTitleAllowsLabelOverlap,h as axisTitleFollowsLabels,w as axisTitleTextLines,f as isLabelRelativeTitleOffset,g as validateAxisLabelFlushThreshold,c as validateAxisTextPlacement};
|
package/src/grammar/bin2d.js
CHANGED
|
@@ -11,7 +11,7 @@ const MAX_CELLS = 1_000_000;
|
|
|
11
11
|
|
|
12
12
|
const TRANSFORM_KEYS = Object.freeze([
|
|
13
13
|
"type", "x", "y", "bins", "extent", "includeEmpty", "members", "as",
|
|
14
|
-
"resolved"
|
|
14
|
+
"resolved", "assignment"
|
|
15
15
|
]);
|
|
16
16
|
const AXIS_KEYS = Object.freeze(["x", "y"]);
|
|
17
17
|
const FIELD_KEYS = Object.freeze(["x0", "x1", "y0", "y1", "count", "members"]);
|
|
@@ -180,6 +180,11 @@ export function validateBin2DTransform(transform) {
|
|
|
180
180
|
}
|
|
181
181
|
requireField(transform.x, "2D bin x field");
|
|
182
182
|
requireField(transform.y, "2D bin y field");
|
|
183
|
+
if (transform.assignment !== undefined && transform.assignment !== "step") {
|
|
184
|
+
throw new TypeError(
|
|
185
|
+
'Stored 2D bin assignment must be "step" or omitted.'
|
|
186
|
+
);
|
|
187
|
+
}
|
|
183
188
|
validateBins(transform.bins);
|
|
184
189
|
validateRequestedExtent(transform.extent);
|
|
185
190
|
if (typeof transform.includeEmpty !== "boolean") {
|
|
@@ -201,10 +206,16 @@ export function normalizeBin2DTransform({
|
|
|
201
206
|
y,
|
|
202
207
|
bins,
|
|
203
208
|
extent,
|
|
209
|
+
assignment = "edges",
|
|
204
210
|
includeEmpty = false,
|
|
205
211
|
members = false,
|
|
206
212
|
as
|
|
207
213
|
} = {}) {
|
|
214
|
+
if (assignment !== "edges" && assignment !== "step") {
|
|
215
|
+
throw new TypeError(
|
|
216
|
+
'2D bin assignment must be "edges" or "step".'
|
|
217
|
+
);
|
|
218
|
+
}
|
|
208
219
|
const normalizedBins = normalizeBins(bins);
|
|
209
220
|
const transform = {
|
|
210
221
|
type: "bin2d",
|
|
@@ -214,7 +225,8 @@ export function normalizeBin2DTransform({
|
|
|
214
225
|
extent: normalizeRequestedExtent(extent),
|
|
215
226
|
includeEmpty,
|
|
216
227
|
members,
|
|
217
|
-
as: normalizeOutputFields(id, as, members)
|
|
228
|
+
as: normalizeOutputFields(id, as, members),
|
|
229
|
+
...(assignment === "step" ? { assignment } : {})
|
|
218
230
|
};
|
|
219
231
|
validateBin2DTransform(transform);
|
|
220
232
|
return cloneAndFreeze(transform);
|
|
@@ -266,6 +278,20 @@ function findBinIndex(value, edges) {
|
|
|
266
278
|
return low;
|
|
267
279
|
}
|
|
268
280
|
|
|
281
|
+
function findStepBinIndex(value, extent, count) {
|
|
282
|
+
if (count === 1 || value === extent[1]) return count - 1;
|
|
283
|
+
const [minimum, maximum] = extent;
|
|
284
|
+
const span = maximum - minimum;
|
|
285
|
+
const step = Number.isFinite(span)
|
|
286
|
+
? span / count
|
|
287
|
+
: maximum / count - minimum / count;
|
|
288
|
+
const distance = value - minimum;
|
|
289
|
+
const position = Number.isFinite(distance)
|
|
290
|
+
? distance / step
|
|
291
|
+
: value / step - minimum / step;
|
|
292
|
+
return Math.max(0, Math.min(count - 1, Math.floor(1e-14 + position)));
|
|
293
|
+
}
|
|
294
|
+
|
|
269
295
|
export function deriveBin2DRows(rows, transform) {
|
|
270
296
|
if (!Array.isArray(rows)) {
|
|
271
297
|
throw new TypeError("2D bin source rows must be an array.");
|
|
@@ -304,8 +330,12 @@ export function deriveBin2DRows(rows, transform) {
|
|
|
304
330
|
() => []
|
|
305
331
|
);
|
|
306
332
|
for (const item of eligible) {
|
|
307
|
-
const xIndex =
|
|
308
|
-
|
|
333
|
+
const xIndex = transform.assignment === "step"
|
|
334
|
+
? findStepBinIndex(item.row[transform.x], extent.x, transform.bins.x)
|
|
335
|
+
: findBinIndex(item.row[transform.x], edges.x);
|
|
336
|
+
const yIndex = transform.assignment === "step"
|
|
337
|
+
? findStepBinIndex(item.row[transform.y], extent.y, transform.bins.y)
|
|
338
|
+
: findBinIndex(item.row[transform.y], edges.y);
|
|
309
339
|
buckets[yIndex * transform.bins.x + xIndex].push(item.index);
|
|
310
340
|
}
|
|
311
341
|
const values = [];
|
|
@@ -340,3 +370,10 @@ export function requestedBin2DTransform(transform) {
|
|
|
340
370
|
const { resolved: _resolved, ...requested } = transform;
|
|
341
371
|
return cloneAndFreeze(requested);
|
|
342
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
|
+
}
|