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,208 +1 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { cloneAndFreeze, isPlainObject } from "../core/immutable.js";
|
|
3
|
-
import { validateUserId } from "../core/identifiers.js";
|
|
4
|
-
import { validateOptionObject } from "../core/validation.js";
|
|
5
|
-
import {
|
|
6
|
-
mapContinuousScaleValues,
|
|
7
|
-
mapOrdinalPositionValues,
|
|
8
|
-
validateSemanticFieldType
|
|
9
|
-
} from "./scales/index.js";
|
|
10
|
-
|
|
11
|
-
const DIMENSION_OPTIONS = Object.freeze([
|
|
12
|
-
"field", "fieldType", "title", "scale"
|
|
13
|
-
]);
|
|
14
|
-
const FIELD_TYPES = new Set(["quantitative", "ordinal"]);
|
|
15
|
-
const MISSING_POLICIES = new Set(["break", "drop-row", "error"]);
|
|
16
|
-
const DIMENSION_SCALE_OPTIONS = Object.freeze([
|
|
17
|
-
"type", "domain", "range", "nice", "zero", "clamp", "reverse",
|
|
18
|
-
"base", "exponent", "constant", "paddingInner", "paddingOuter",
|
|
19
|
-
"padding", "align", "unknown"
|
|
20
|
-
]);
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
export function validateParallelKeyField(value) {
|
|
24
|
-
return nonEmptyString(value, "Parallel key field");
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
function inferFieldType(rows, field) {
|
|
28
|
-
const values = rows
|
|
29
|
-
.map(row => row[field])
|
|
30
|
-
.filter(value => value !== undefined && value !== null);
|
|
31
|
-
if (values.length === 0) {
|
|
32
|
-
throw new Error(`Cannot infer Parallel field type for "${field}".`);
|
|
33
|
-
}
|
|
34
|
-
if (values.every(Number.isFinite)) return "quantitative";
|
|
35
|
-
if (values.every(value => typeof value === "string")) return "ordinal";
|
|
36
|
-
throw new Error(
|
|
37
|
-
`Parallel dimension "${field}" requires an explicit compatible fieldType.`
|
|
38
|
-
);
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
function validateScaleOptions(value, index) {
|
|
42
|
-
if (value === undefined) return {};
|
|
43
|
-
if (!isPlainObject(value)) {
|
|
44
|
-
throw new TypeError(`Parallel dimension ${index} scale must be a plain object.`);
|
|
45
|
-
}
|
|
46
|
-
validateOptionObject(
|
|
47
|
-
value,
|
|
48
|
-
DIMENSION_SCALE_OPTIONS,
|
|
49
|
-
`Parallel dimension ${index} scale`
|
|
50
|
-
);
|
|
51
|
-
return { ...value };
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
export function validateParallelMissingPolicy(value) {
|
|
55
|
-
if (!MISSING_POLICIES.has(value)) {
|
|
56
|
-
throw new Error(`Unsupported Parallel missing policy "${value}".`);
|
|
57
|
-
}
|
|
58
|
-
return value;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
export function validateParallelDimensions(dimensions, { normalized = false } = {}) {
|
|
62
|
-
if (!Array.isArray(dimensions) || dimensions.length < 2) {
|
|
63
|
-
throw new RangeError("Parallel coordinates require at least two dimensions.");
|
|
64
|
-
}
|
|
65
|
-
const fields = new Set();
|
|
66
|
-
for (const [index, dimension] of dimensions.entries()) {
|
|
67
|
-
if (!isPlainObject(dimension)) {
|
|
68
|
-
throw new TypeError(`Parallel dimension ${index} must be an object.`);
|
|
69
|
-
}
|
|
70
|
-
const supported = normalized
|
|
71
|
-
? ["field", "fieldType", "title", "scale"]
|
|
72
|
-
: DIMENSION_OPTIONS;
|
|
73
|
-
validateOptionObject(dimension, supported, `Parallel dimension ${index}`);
|
|
74
|
-
const field = nonEmptyString(
|
|
75
|
-
dimension.field,
|
|
76
|
-
`Parallel dimension ${index} field`
|
|
77
|
-
);
|
|
78
|
-
if (fields.has(field)) {
|
|
79
|
-
throw new Error("Parallel dimensions must use unique fields.");
|
|
80
|
-
}
|
|
81
|
-
fields.add(field);
|
|
82
|
-
validateSemanticFieldType(dimension.fieldType);
|
|
83
|
-
if (!FIELD_TYPES.has(dimension.fieldType)) {
|
|
84
|
-
throw new Error(`Unsupported Parallel field type "${dimension.fieldType}".`);
|
|
85
|
-
}
|
|
86
|
-
nonEmptyString(dimension.title, `Parallel dimension ${index} title`);
|
|
87
|
-
validateUserId(dimension.scale, `Parallel dimension ${index} scale id`);
|
|
88
|
-
}
|
|
89
|
-
return dimensions;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
export function normalizeParallelDimensions(rows, dimensions, target) {
|
|
93
|
-
if (!Array.isArray(dimensions) || dimensions.length < 2) {
|
|
94
|
-
throw new RangeError("Parallel coordinates require at least two dimensions.");
|
|
95
|
-
}
|
|
96
|
-
const normalized = dimensions.map((input, index) => {
|
|
97
|
-
const dimension = typeof input === "string" ? { field: input } : input;
|
|
98
|
-
if (!isPlainObject(dimension)) {
|
|
99
|
-
throw new TypeError(
|
|
100
|
-
`Parallel dimension ${index} must be a field string or plain object.`
|
|
101
|
-
);
|
|
102
|
-
}
|
|
103
|
-
validateOptionObject(
|
|
104
|
-
dimension,
|
|
105
|
-
DIMENSION_OPTIONS,
|
|
106
|
-
`Parallel dimension ${index}`
|
|
107
|
-
);
|
|
108
|
-
const field = nonEmptyString(
|
|
109
|
-
dimension.field,
|
|
110
|
-
`Parallel dimension ${index} field`
|
|
111
|
-
);
|
|
112
|
-
const fieldType = dimension.fieldType ?? inferFieldType(rows, field);
|
|
113
|
-
if (!FIELD_TYPES.has(fieldType)) {
|
|
114
|
-
throw new Error(`Unsupported Parallel field type "${fieldType}".`);
|
|
115
|
-
}
|
|
116
|
-
const scale = validateScaleOptions(dimension.scale, index);
|
|
117
|
-
return {
|
|
118
|
-
field,
|
|
119
|
-
fieldType,
|
|
120
|
-
title: dimension.title ?? field,
|
|
121
|
-
scale: `${target}-parallel-${index}`,
|
|
122
|
-
scaleOptions: scale
|
|
123
|
-
};
|
|
124
|
-
});
|
|
125
|
-
const fields = normalized.map(dimension => dimension.field);
|
|
126
|
-
if (new Set(fields).size !== fields.length) {
|
|
127
|
-
throw new Error("Parallel dimensions must use unique fields.");
|
|
128
|
-
}
|
|
129
|
-
return cloneAndFreeze(normalized);
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
function validDimensionValue(value, fieldType) {
|
|
133
|
-
return fieldType === "quantitative"
|
|
134
|
-
? Number.isFinite(value)
|
|
135
|
-
: typeof value === "string" || Number.isFinite(value);
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
function commandsFromVertices(vertices) {
|
|
139
|
-
const commands = [];
|
|
140
|
-
let fragment = [];
|
|
141
|
-
const flush = () => {
|
|
142
|
-
if (fragment.length >= 2) {
|
|
143
|
-
fragment.forEach((point, index) => commands.push({
|
|
144
|
-
op: index === 0 ? "M" : "L",
|
|
145
|
-
x: point.x,
|
|
146
|
-
y: point.y
|
|
147
|
-
}));
|
|
148
|
-
}
|
|
149
|
-
fragment = [];
|
|
150
|
-
};
|
|
151
|
-
for (const vertex of vertices) {
|
|
152
|
-
if (vertex === undefined) flush();
|
|
153
|
-
else fragment.push(vertex);
|
|
154
|
-
}
|
|
155
|
-
flush();
|
|
156
|
-
return commands;
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
export function validateParallelRows(rows, dimensions, { key, missing }) {
|
|
160
|
-
const seen = new Set();
|
|
161
|
-
rows.forEach((row, index) => {
|
|
162
|
-
if (key !== undefined) {
|
|
163
|
-
const value = row[key];
|
|
164
|
-
if (value === undefined || value === null || value === "") {
|
|
165
|
-
throw new Error(`Parallel key is missing at row ${index}.`);
|
|
166
|
-
}
|
|
167
|
-
const token = `${typeof value}:${String(value)}`;
|
|
168
|
-
if (seen.has(token)) throw new Error(`Duplicate Parallel key "${value}".`);
|
|
169
|
-
seen.add(token);
|
|
170
|
-
}
|
|
171
|
-
const incomplete = dimensions.some(dimension =>
|
|
172
|
-
!validDimensionValue(row[dimension.field], dimension.fieldType)
|
|
173
|
-
);
|
|
174
|
-
if (incomplete && missing === "error") {
|
|
175
|
-
throw new Error(`Parallel row ${index} has a missing dimension.`);
|
|
176
|
-
}
|
|
177
|
-
});
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
export function materializeParallelRows(rows, dimensions, resolvedScales, bounds, {
|
|
181
|
-
key,
|
|
182
|
-
missing
|
|
183
|
-
}) {
|
|
184
|
-
validateParallelRows(rows, dimensions, { key, missing });
|
|
185
|
-
const step = bounds.width / (dimensions.length - 1);
|
|
186
|
-
const items = [];
|
|
187
|
-
rows.forEach((row, sourceRowIndex) => {
|
|
188
|
-
const vertices = dimensions.map((dimension, index) => {
|
|
189
|
-
const value = row[dimension.field];
|
|
190
|
-
if (!validDimensionValue(value, dimension.fieldType)) return undefined;
|
|
191
|
-
const scale = resolvedScales[dimension.scale];
|
|
192
|
-
const mapped = dimension.fieldType === "quantitative"
|
|
193
|
-
? mapContinuousScaleValues([value], scale)[0]
|
|
194
|
-
: mapOrdinalPositionValues([value], scale)[0];
|
|
195
|
-
return Number.isFinite(mapped)
|
|
196
|
-
? { x: bounds.x + step * index, y: mapped, value, dimension: dimension.field }
|
|
197
|
-
: undefined;
|
|
198
|
-
});
|
|
199
|
-
const incomplete = vertices.some(vertex => vertex === undefined);
|
|
200
|
-
if (incomplete && missing === "drop-row") return;
|
|
201
|
-
items.push({
|
|
202
|
-
key: key === undefined ? `source:${sourceRowIndex}` : row[key],
|
|
203
|
-
sourceRowIndex,
|
|
204
|
-
commands: commandsFromVertices(vertices)
|
|
205
|
-
});
|
|
206
|
-
});
|
|
207
|
-
return cloneAndFreeze(items);
|
|
208
|
-
}
|
|
1
|
+
import{requireStringValue as P}from"../core/validation.js";import{cloneAndFreeze as N,isOwned as E,isPlainObject as $}from"../core/immutable.js";import{validateUserId as I}from"../core/identifiers.js";import{validateOptionObject as S}from"../core/validation.js";import{mapContinuousScaleValues as A,mapOrdinalPositionValues as D,validateSemanticFieldType as M}from"./scales/index.js";const q=Object.freeze(["field","fieldType","title","scale"]),F=new Set(["quantitative","ordinal"]),k=new Set(["break","drop-row","error"]),z=new WeakMap,x=Object.freeze(["id","type","domain","range","nice","zero","clamp","reverse","base","exponent","constant","paddingInner","paddingOuter","padding","align","unknown"]);function W(e){return P(e,"Parallel key field")}function V(e,t){const i=e.map(n=>n[t]).filter(n=>n!=null);if(i.length===0)throw new Error(`Cannot infer Parallel field type for "${t}".`);if(i.every(Number.isFinite))return"quantitative";if(i.every(n=>typeof n=="string"))return"ordinal";throw new Error(`Parallel dimension "${t}" requires an explicit compatible fieldType.`)}function _(e,t){if(e===void 0)return{};if(!$(e))throw new TypeError(`Parallel dimension ${t} scale must be a plain object.`);return S(e,x,`Parallel dimension ${t} scale`),{...e}}function Y(e){if(!k.has(e))throw new Error(`Unsupported Parallel missing policy "${e}".`);return e}function B(e,{normalized:t=!1}={}){if(!Array.isArray(e)||e.length<2)throw new RangeError("Parallel coordinates require at least two dimensions.");const i=new Set;for(const[n,r]of e.entries()){if(!$(r))throw new TypeError(`Parallel dimension ${n} must be an object.`);S(r,t?["field","fieldType","title","scale"]:q,`Parallel dimension ${n}`);const o=P(r.field,`Parallel dimension ${n} field`);if(i.has(o))throw new Error("Parallel dimensions must use unique fields.");if(i.add(o),M(r.fieldType),!F.has(r.fieldType))throw new Error(`Unsupported Parallel field type "${r.fieldType}".`);P(r.title,`Parallel dimension ${n} title`),I(r.scale,`Parallel dimension ${n} scale id`)}return e}function H(e,t,i){if(!Array.isArray(t)||t.length<2)throw new RangeError("Parallel coordinates require at least two dimensions.");const n=t.map((s,o)=>{const f=typeof s=="string"?{field:s}:s;if(!$(f))throw new TypeError(`Parallel dimension ${o} must be a field string or plain object.`);S(f,q,`Parallel dimension ${o}`);const l=P(f.field,`Parallel dimension ${o} field`),a=f.fieldType??V(e,l);if(!F.has(a))throw new Error(`Unsupported Parallel field type "${a}".`);const y=_(f.scale,o);return{field:l,fieldType:a,title:f.title??l,scale:y.id===void 0?`${i}-parallel-${o}`:I(y.id,"Parallel dimension scale id"),scaleOptions:y}}),r=n.map(s=>s.field);if(new Set(r).size!==r.length)throw new Error("Parallel dimensions must use unique fields.");return N(n)}function T(e,t){return t==="quantitative"?Number.isFinite(e):typeof e=="string"||Number.isFinite(e)}function C(e){const t=[];let i=[];const n=()=>{i.length>=2&&i.forEach((r,s)=>t.push({op:s===0?"M":"L",x:r.x,y:r.y})),i=[]};for(const r of e)r===void 0?n():i.push(r);return n(),t}function L(e,t,{key:i,missing:n}){const r=new Set;e.forEach((s,o)=>{if(i!==void 0){const l=s[i];if(l==null||l==="")throw new Error(`Parallel key is missing at row ${o}.`);const a=`${typeof l}:${String(l)}`;if(r.has(a))throw new Error(`Duplicate Parallel key "${l}".`);r.add(a)}if(t.some(l=>!T(s[l.field],l.fieldType))&&n==="error")throw new Error(`Parallel row ${o} has a missing dimension.`)})}function Q(e,t,i,n,{key:r,missing:s}){const o=t.map(c=>i[c.scale]),f=E(e)&&E(t)&&o.every(E),l=f?JSON.stringify({dimensions:t,scales:o,bounds:n,key:r,missing:s}):void 0;let a=f?z.get(e):void 0;if(a?.has(l)){const c=a.get(l);return a.delete(l),a.set(l,c),c}L(e,t,{key:r,missing:s});const y=n.width/(t.length-1),j=t.map((c,u)=>{const d=[],g=[];e.forEach((m,h)=>{const b=m[c.field];T(b,c.fieldType)&&(d.push(b),g.push(h))});const p=d.length===0?[]:c.fieldType==="quantitative"?A(d,o[u]):D(d,o[u]),w=new Array(e.length);return g.forEach((m,h)=>{w[m]=p[h]}),w}),O=[];e.forEach((c,u)=>{const d=t.map((p,w)=>{const m=c[p.field];if(!T(m,p.fieldType))return;const h=j[w][u];return Number.isFinite(h)?{x:n.x+y*w,y:h,value:m,dimension:p.field}:void 0});d.some(p=>p===void 0)&&s==="drop-row"||O.push({key:r===void 0?`source:${u}`:c[r],sourceRowIndex:u,commands:C(d)})});const v=N(O);return f&&(a||z.set(e,a=new Map),a.set(l,v),a.size>4&&a.delete(a.keys().next().value)),v}export{Q as materializeParallelRows,H as normalizeParallelDimensions,B as validateParallelDimensions,W as validateParallelKeyField,Y as validateParallelMissingPolicy,L as validateParallelRows};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { freezeOwned, isPlainObject } from "../core/immutable.js";
|
|
1
|
+
import { freezeOwned, isOwned, isPlainObject } from "../core/immutable.js";
|
|
2
2
|
import { validateGeneratedItemLimit } from "../core/validation.js";
|
|
3
3
|
|
|
4
4
|
export const MAX_PATH_COMMANDS = 30_000;
|
|
@@ -13,6 +13,7 @@ const COMMAND_KEYS = Object.freeze({
|
|
|
13
13
|
C: Object.freeze(["op", "x1", "y1", "x2", "y2", "x", "y"]),
|
|
14
14
|
Z: Object.freeze(["op"])
|
|
15
15
|
});
|
|
16
|
+
const validatedCommands = new WeakSet();
|
|
16
17
|
|
|
17
18
|
function validateCommand(command, index) {
|
|
18
19
|
if (!isPlainObject(command) || !Object.hasOwn(COMMAND_KEYS, command.op)) {
|
|
@@ -41,6 +42,7 @@ function validateCommand(command, index) {
|
|
|
41
42
|
}
|
|
42
43
|
|
|
43
44
|
export function validatePathCommands(commands) {
|
|
45
|
+
if (validatedCommands.has(commands)) return commands;
|
|
44
46
|
if (!Array.isArray(commands) || commands.length < 1) {
|
|
45
47
|
throw new TypeError("path.commands must contain at least one command.");
|
|
46
48
|
}
|
|
@@ -49,7 +51,10 @@ export function validatePathCommands(commands) {
|
|
|
49
51
|
if (commands[0].op !== "M") {
|
|
50
52
|
throw new Error("path.commands must start with M.");
|
|
51
53
|
}
|
|
52
|
-
if (commands.length === 1)
|
|
54
|
+
if (commands.length === 1) {
|
|
55
|
+
if (isOwned(commands)) validatedCommands.add(commands);
|
|
56
|
+
return commands;
|
|
57
|
+
}
|
|
53
58
|
if (commands.slice(1).some(command => command.op === "M")) {
|
|
54
59
|
throw new Error("path.commands supports one subpath and only one initial M.");
|
|
55
60
|
}
|
|
@@ -60,6 +65,7 @@ export function validatePathCommands(commands) {
|
|
|
60
65
|
if (closeIndex !== -1 && closeIndex !== commands.length - 1) {
|
|
61
66
|
throw new Error("path.commands Z must be the final command.");
|
|
62
67
|
}
|
|
68
|
+
if (isOwned(commands)) validatedCommands.add(commands);
|
|
63
69
|
return commands;
|
|
64
70
|
}
|
|
65
71
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{validateNonEmptyString as
|
|
1
|
+
import{validateNonEmptyString as y}from"../core/validation.js";import{readCategoricalLineColor as p,validateCategoricalColorConsumer as S}from"./scales/colorConsumers.js";import{readNominalField as f,readQuantitativeField as O,readScaleField as x}from"./scales/fields.js";function m(n){const e=Array.isArray(n)?[...n]:[n];for(const i of e)y(i,"Path group field");if(e.length===0||new Set(e).size!==e.length)throw new Error("Path group fields must be non-empty and unique.");return e}function E(n){const e=n.encoding?.group;if(e!==void 0){if(e.fieldType!=="nominal")throw new Error(`Path group on mark "${n.id}" must be nominal.`);if(Object.hasOwn(e,"field")&&Object.hasOwn(e,"fields"))throw new Error("Path group requires exactly one of field or fields.");return m(e.fields??e.field)}if(n.mark?.type!=="line")return[];const i=["nominal","ordinal"].includes(n.encoding?.color?.fieldType)?n.encoding.color.field:void 0,o=n.encoding?.strokeDash?.field;if(i!==void 0&&o!==void 0&&i!==o)throw new Error("Line color field must match strokeDash field unless encodeGroup defines the series.");return i===void 0&&o===void 0?[]:[i??o]}function h(n){return JSON.stringify(n.map(e=>e===void 0?{}:e))}function g(n,e){const i=n.encoding?.color,o=Array.isArray(e)?e.find(t=>t.id===i?.scale):e?.[i?.scale];return o!==void 0&&S(n,i,o)?o:void 0}function P(n,e,i){const o=E(e),t=g(e,i);for(const r of o)t&&e.encoding?.group===void 0&&r===e.encoding.color.field?p(n,r):f(n,r);return o}function k(n,e,i,o,t={},r){const l=o==="strokeWidth"||o==="opacity",d=o==="color"&&r!==void 0?p(n,i):o==="stroke"||o==="color"&&["quantitative","temporal"].includes(t.fieldType)?x(n,i,t.fieldType??"nominal",{temporalUnit:t.temporalUnit}):l?O(n,i):f(n,i),a=new Map;return n.forEach((u,v)=>{const c=h(e.map(w=>u[w])),s=d[v];if(o==="strokeWidth"&&s<0)throw new RangeError(`Line strokeWidth field "${i}" cannot contain negative values.`);if(a.has(c)&&a.get(c)!==s)throw new Error(`Path ${o} field "${i}" must have one value within each series.`);a.set(c,s)}),a}function F(n,e,i,o,t,r){if(e.length===0)return[];const l=Object.keys(e[0]?.key??{}),d=k(n,l,i,o,t,Object.hasOwn(r??{},"unknown")?r:void 0);return e.map(a=>d.get(h(Object.values(a.key))))}function j(n,e,i,o){if(!["line","area"].includes(e.mark?.type)||e.encoding?.parallel!==void 0)return;const t=o??P(n,e,i);if(o!==void 0)for(const d of t)f(n,d);const r=g(e,i),l=e.mark.type==="line"?["color","stroke","strokeDash","strokeWidth","opacity"]:["color","stroke","opacity"];for(const d of l){const a=e.encoding?.[d];if(a?.field===void 0)continue;if(!(["color","stroke"].includes(d)?d==="color"?e.mark.type==="area"?["nominal","ordinal","quantitative"]:["nominal","ordinal","quantitative","temporal"]:["nominal","ordinal","quantitative","temporal"]:d==="strokeDash"?["nominal","ordinal"]:["quantitative"]).includes(a.fieldType))throw new Error(`Path ${d} encoding has an unsupported field type.`);k(n,t,a.field,d,a,d==="color"?r:void 0)}return t}function A(n,e){const i=e.encoding?.group,o=i===void 0?[]:m(i.fields??i.field);for(const r of o)f(n,r);const t=n.map(r=>o.length===0?void 0:o.length===1?r[o[0]]:JSON.stringify(o.map(l=>r[l])));return{fields:o,values:t,domain:[...new Set(t)]}}export{F as derivePathSeriesFieldValues,m as normalizeGroupFields,E as pathGroupFields,h as pathSeriesKey,P as readPathSeriesFields,A as readSeriesIdentity,j as validatePathSeriesAppearance};
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
// One constructor capability registry for layering, adjacent inference and
|
|
2
|
+
// inspection. Native constructors remain responsible for statistical work.
|
|
3
|
+
const xy = ['createScatterPlot','createLinePlot','createBarPlot','createAreaPlot','createStripPlot','createBeeswarmPlot','createRegressionPlot','createTextPlot'];
|
|
4
|
+
const summaryXY = ['createBoxPlot','createViolinPlot','createGradientPlot','createIntervalPlot'];
|
|
5
|
+
export const PLOT_COMPOSITION_ROLES = Object.freeze(Object.fromEntries([
|
|
6
|
+
...xy.map(a => [a, 'xy']), ...summaryXY.map(a => [a, 'summaryXY']),
|
|
7
|
+
...['createDotPlot','createLollipopPlot','createDumbbellPlot'].map(a => [a, 'endpoint']),
|
|
8
|
+
['createRaincloudPlot','categoryValue'], ['createHistogram','histogram'], ['createDensityPlot','density'],
|
|
9
|
+
['createECDFPlot','ecdf'], ['createRugPlot','rug'], ['createHeatmap','heatmap'], ['createHorizonPlot','horizon'],
|
|
10
|
+
...['createPolarScatterPlot','createPolarLinePlot'].map(a => [a, 'polar']),
|
|
11
|
+
...['createPiePlot','createRosePlot','createRadialBarPlot'].map(a => [a, 'radial']),
|
|
12
|
+
['createRadarPlot','radar'], ['createParallelCoordinates','parallel']
|
|
13
|
+
]));
|
|
14
|
+
export const COMPOSITION_PLOT_ACTIONS = Object.freeze(Object.keys(PLOT_COMPOSITION_ROLES));
|
|
15
|
+
export const fieldEncoding = value => typeof value === 'string' ? { field:value } : value;
|
|
16
|
+
const categorical = c => c && ['nominal','ordinal'].includes(c.fieldType);
|
|
17
|
+
const encoding = (c, aggregate = true) => c?.field ? { field:c.field, fieldType:c.fieldType,
|
|
18
|
+
...(aggregate && c.measurement.operations.length === 1 && ['mean','median','sum','min','max','count'].includes(c.measurement.operations[0]) ? { aggregate:c.measurement.operations[0] } : {}),
|
|
19
|
+
...(c.bin ? { bin:c.bin } : {}), ...(c.temporalUnit ? { temporalUnit:c.temporalUnit } : {}) } : undefined;
|
|
20
|
+
const plain = c => c?.field ? { field:c.field, fieldType:c.fieldType } : undefined;
|
|
21
|
+
const merge = (base, supplied) => {
|
|
22
|
+
const value=typeof supplied==='string'?{field:supplied}:supplied;
|
|
23
|
+
// An explicit new field owns its type/grain. Source defaults only fill an
|
|
24
|
+
// omitted role or an options-only patch on that inferred field.
|
|
25
|
+
if (value?.field !== undefined && value.field !== base?.field || value?.lower !== undefined || value?.upper !== undefined) return value;
|
|
26
|
+
const merged={ ...base, ...value };
|
|
27
|
+
if (value?.center !== undefined || value?.extent !== undefined) {
|
|
28
|
+
// Native interval measurements are not ordinary field encodings.
|
|
29
|
+
for (const key of ['fieldType','temporalUnit','aggregate','bin']) delete merged[key];
|
|
30
|
+
}
|
|
31
|
+
return merged;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export function inferCompositionOptions(source, action, supplied = {}, inherit = {}) {
|
|
35
|
+
if (!inherit || typeof inherit !== 'object' || Array.isArray(inherit) ||
|
|
36
|
+
Object.keys(inherit).some(key => key !== 'color') ||
|
|
37
|
+
inherit.color !== undefined && typeof inherit.color !== 'boolean') {
|
|
38
|
+
throw new TypeError('Plot inheritance accepts color: true or false.');
|
|
39
|
+
}
|
|
40
|
+
const kind = PLOT_COMPOSITION_ROLES[action];
|
|
41
|
+
if (!kind) throw new Error(`Unknown plot constructor "${action}".`);
|
|
42
|
+
const c = source.channels;
|
|
43
|
+
const x = c.x ?? c.theta, y = c.y ?? c.radius;
|
|
44
|
+
const categoryAxis = categorical(x) ? 'x' : categorical(y) ? 'y' : undefined;
|
|
45
|
+
const category = categoryAxis === 'x' ? x : categoryAxis === 'y' ? y : undefined;
|
|
46
|
+
const measure = categoryAxis === 'x' ? y : categoryAxis === 'y' ? x : x;
|
|
47
|
+
let defaults = { data: source.data };
|
|
48
|
+
if (['xy','summaryXY','heatmap','horizon'].includes(kind)) {
|
|
49
|
+
defaults.x = kind === 'xy' && ['createLinePlot','createBarPlot'].includes(action) ? encoding(x) : plain(x);
|
|
50
|
+
defaults.y = kind === 'xy' && ['createLinePlot','createBarPlot'].includes(action) ? encoding(y) : plain(y);
|
|
51
|
+
if (kind === 'summaryXY') {
|
|
52
|
+
if (!categoryAxis && !(supplied.x !== undefined && supplied.y !== undefined)) throw new Error(`${action} requires one category axis and one quantitative measure.`);
|
|
53
|
+
if (categoryAxis && action === 'createIntervalPlot') defaults[categoryAxis === 'x' ? 'y' : 'x'] = { field:measure.field, center:'mean', extent:'ci', level:0.95 };
|
|
54
|
+
}
|
|
55
|
+
// Binning follows the new chart's roles, including explicitly supplied
|
|
56
|
+
// categorical/ordinal grids, rather than the source chart's old axes.
|
|
57
|
+
if (kind === 'heatmap' && !['x', 'y'].some(axis => categorical(
|
|
58
|
+
supplied[axis] === undefined ? defaults[axis] : merge(defaults[axis], supplied[axis])
|
|
59
|
+
))) defaults.bin = { bins:12 };
|
|
60
|
+
if (kind === 'horizon' && c.y?.measurement.parameters) Object.assign(defaults, c.y.measurement.parameters);
|
|
61
|
+
if (action === 'createTextPlot') defaults.text = { field:c.color?.field ?? y?.field ?? x?.field };
|
|
62
|
+
} else if (['endpoint','categoryValue'].includes(kind)) {
|
|
63
|
+
if (!categoryAxis && !(supplied.category !== undefined && (supplied.value !== undefined || supplied.end !== undefined))) throw new Error(`${action} requires one category axis and one quantitative measure.`);
|
|
64
|
+
if (categoryAxis) Object.assign(defaults, {category:plain(category), orientation: categoryAxis === 'x' ? 'vertical' : 'horizontal'});
|
|
65
|
+
if (action === 'createDumbbellPlot') { defaults.end=plain(measure); } else defaults.value=plain(measure);
|
|
66
|
+
if (kind === 'endpoint') defaults.summary=['mean','median','sum','min','max'].includes(measure?.measurement.operations.at(-1))?measure.measurement.operations.at(-1):'mean';
|
|
67
|
+
} else if (['histogram','density','ecdf'].includes(kind)) {
|
|
68
|
+
defaults.field = supplied.field ?? measure?.field;
|
|
69
|
+
if (kind === 'density' && c.x?.measurement.unit === 'density') defaults.densityChannel='x';
|
|
70
|
+
if (kind === 'histogram' && x?.bin) {
|
|
71
|
+
defaults.maxBins=x.bin.maxBins; defaults.binStep=x.bin.step; defaults.binBoundaries=x.bin.boundaries;
|
|
72
|
+
}
|
|
73
|
+
if (inherit.color !== false && kind === 'density' && c.color?.field && categorical(c.color)) defaults.groupBy=c.color.field;
|
|
74
|
+
} else if (kind === 'rug') {
|
|
75
|
+
const axis = supplied.x !== undefined ? 'x' : supplied.y !== undefined ? 'y'
|
|
76
|
+
: supplied.edge === 'left' || supplied.edge === 'right' || categorical(c.x) ? 'y' : 'x';
|
|
77
|
+
defaults[axis] = plain(c[axis]); defaults.edge = axis === 'x' ? 'bottom' : 'left';
|
|
78
|
+
} else if (kind === 'polar') {
|
|
79
|
+
defaults.theta=plain(c.theta); defaults.radius=plain(c.radius);
|
|
80
|
+
if (action === 'createPolarLinePlot' && source.groupBy) defaults.groupBy=source.groupBy;
|
|
81
|
+
} else if (kind === 'radial') {
|
|
82
|
+
defaults.category=plain(c.theta);
|
|
83
|
+
defaults.aggregate=c.radius?.measurement.operations.at(-1) ?? c.theta?.measurement.parameters?.aggregate ?? 'count';
|
|
84
|
+
if(defaults.aggregate==='sum') defaults.value=c.radius?.field ?? c.theta?.measurement.parameters?.weight;
|
|
85
|
+
} else if (kind === 'radar') {
|
|
86
|
+
if (c.theta?.fields) defaults.wide={fields:c.theta.fields};
|
|
87
|
+
else {defaults.category=plain(c.theta); defaults.value=plain(c.radius);}
|
|
88
|
+
if (source.groupBy) defaults.groupBy=source.groupBy;
|
|
89
|
+
} else if (kind === 'parallel') defaults.dimensions=source.dimensions?.map(plain);
|
|
90
|
+
if (inherit.color !== false && c.color?.field && !['heatmap', 'horizon', 'rug'].includes(kind)) {
|
|
91
|
+
const summaryCategoryOnly = ['endpoint','categoryValue','summaryXY'].includes(kind) && action !== 'createIntervalPlot';
|
|
92
|
+
const colorSurvives = !summaryCategoryOnly || c.color.field === category?.field ||
|
|
93
|
+
action === 'createViolinPlot' && supplied.split?.field === c.color.field;
|
|
94
|
+
if (colorSurvives) defaults.color=plain(c.color);
|
|
95
|
+
if (action === 'createIntervalPlot' && categorical(c.color) && c.color.field !== category?.field) defaults.groupBy=c.color.field;
|
|
96
|
+
}
|
|
97
|
+
if (action === 'createAreaPlot') {
|
|
98
|
+
// Explicit native X/Y roles follow the native default value channel. The
|
|
99
|
+
// source orientation only applies when the new roles are inferred.
|
|
100
|
+
const axis = supplied.valueChannel ?? (supplied.x !== undefined && supplied.y !== undefined ? 'y' : categoryAxis === 'y' ? 'x' : 'y'); defaults.valueChannel=axis;
|
|
101
|
+
if(defaults[axis]) defaults[axis]={field:defaults[axis].field};
|
|
102
|
+
}
|
|
103
|
+
if (['createLinePlot','createAreaPlot'].includes(action) && source.groupBy) defaults.groupBy=source.groupBy;
|
|
104
|
+
if (inherit.color !== false && c.color?.field &&
|
|
105
|
+
(kind==='ecdf' || action==='createAreaPlot' && !defaults.groupBy && categorical(c.color))) defaults.groupBy=[c.color.field];
|
|
106
|
+
const result={...defaults,...supplied};
|
|
107
|
+
for(const key of ['x','y','category','value','theta','radius','color','end']) {
|
|
108
|
+
if (supplied[key] !== undefined && defaults[key] !== undefined) result[key]=merge(defaults[key],supplied[key]);
|
|
109
|
+
}
|
|
110
|
+
return Object.fromEntries(Object.entries(result).filter(([,v])=>v!==undefined));
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Describe prospective roles without constructing/rendering a candidate chart.
|
|
114
|
+
export function constructorChannels(action, options, source) {
|
|
115
|
+
const kind=PLOT_COMPOSITION_ROLES[action], c=source.channels, result={};
|
|
116
|
+
const add=(channel,value,path,operation) => {
|
|
117
|
+
const e=fieldEncoding(value); if(!e?.field) return;
|
|
118
|
+
const reference=c[channel]?.field===e.field?c[channel]:Object.values(c).find(r=>r.field===e.field);
|
|
119
|
+
result[channel]={ field:e.field,fieldType:e.fieldType??reference?.fieldType??'quantitative',
|
|
120
|
+
operations:operation ? [operation] : e.aggregate ? [e.aggregate] : [],
|
|
121
|
+
...(e.temporalUnit ? {temporalUnit:e.temporalUnit}:{}), ...(e.bin ? {bin:e.bin}:{}), path };
|
|
122
|
+
};
|
|
123
|
+
if(['xy','summaryXY','heatmap','horizon'].includes(kind)) {
|
|
124
|
+
add('x',options.x,'x.scale'); add('y',options.y,'y.scale');
|
|
125
|
+
if(kind==='horizon' && result.y) result.y.operations=['folded'];
|
|
126
|
+
} else if(['endpoint','categoryValue'].includes(kind)) {
|
|
127
|
+
const axis=options.orientation==='horizontal'?'y':'x', measureAxis=axis==='x'?'y':'x';
|
|
128
|
+
add(axis,options.category,'category.scale');
|
|
129
|
+
add(measureAxis,options.value??options.end,options.value?'value.scale':'end.scale',kind==='endpoint'?options.summary:undefined);
|
|
130
|
+
} else if(kind==='histogram') {
|
|
131
|
+
add('x',options.field,'xScale'); add('y',options.weight??options.field,'yScale',options.weight?'sum':'count');
|
|
132
|
+
} else if(kind==='density') {
|
|
133
|
+
const density=options.densityChannel??'y', value=density==='y'?'x':'y';
|
|
134
|
+
add(value,options.field,'valueScale'); add(density,options.field,'densityScale','density');
|
|
135
|
+
} else if(kind==='ecdf') {
|
|
136
|
+
const value=options.orientation==='horizontal'?'y':'x';
|
|
137
|
+
add(value,options.field,'valueScale'); add(value==='x'?'y':'x',options.field,'probabilityScale','probability');
|
|
138
|
+
} else if(kind==='rug') {
|
|
139
|
+
add('x',options.x,'x.scale'); add('y',options.y,'y.scale');
|
|
140
|
+
} else if(kind==='polar') {
|
|
141
|
+
add('theta',options.theta,'theta.scale'); add('radius',options.radius,'radius.scale');
|
|
142
|
+
} else if(kind==='radial') {
|
|
143
|
+
add('theta',options.category,'category.scale');
|
|
144
|
+
if(action!=='createPiePlot') {
|
|
145
|
+
if(options.aggregate==='count') result.radius={fieldType:'quantitative',operations:['count'],path:'radiusScale'};
|
|
146
|
+
else add('radius',options.value,'radiusScale',options.aggregate);
|
|
147
|
+
} else if(result.theta) result.theta.operations=['partition'];
|
|
148
|
+
} else if(kind==='radar' && !options.wide) {
|
|
149
|
+
add('theta',options.category,'category.scale'); add('radius',options.value,'value.scale');
|
|
150
|
+
}
|
|
151
|
+
return result;
|
|
152
|
+
}
|
|
@@ -20,6 +20,24 @@ export const POINT_SHAPES = cloneAndFreeze([
|
|
|
20
20
|
"arrow"
|
|
21
21
|
]);
|
|
22
22
|
|
|
23
|
+
// Recover the anchor from concrete geometry, including jitter, packing and rotation.
|
|
24
|
+
// Path glyphs can be asymmetric, so their bounding-box center is not their anchor.
|
|
25
|
+
export function pointGraphicAnchor(graphic, shape = "circle", angle) {
|
|
26
|
+
const p = graphic?.properties ?? {};
|
|
27
|
+
if (graphic?.type === "circle") return { x: p.x, y: p.y };
|
|
28
|
+
if (graphic?.type === "rect") return { x: p.x + p.width / 2, y: p.y + p.height / 2 };
|
|
29
|
+
const template = createPointShapeGraphic({ shape, angle, x: 0, y: 0, area: 1, fill: "black" });
|
|
30
|
+
const points = p.commands?.filter(command => Number.isFinite(command.x) && Number.isFinite(command.y));
|
|
31
|
+
const origin = template.properties.commands?.filter(command => Number.isFinite(command.x) && Number.isFinite(command.y));
|
|
32
|
+
if (!points?.length || points.length !== origin?.length) throw new Error("Point glyph does not match its shape geometry.");
|
|
33
|
+
const bounds = values => ({ left: Math.min(...values.map(p => p.x)), right: Math.max(...values.map(p => p.x)),
|
|
34
|
+
top: Math.min(...values.map(p => p.y)), bottom: Math.max(...values.map(p => p.y)) });
|
|
35
|
+
const actual = bounds(points), unit = bounds(origin);
|
|
36
|
+
const factor = unit.right !== unit.left ? (actual.right - actual.left) / (unit.right - unit.left)
|
|
37
|
+
: (actual.bottom - actual.top) / (unit.bottom - unit.top);
|
|
38
|
+
return { x: actual.left - factor * unit.left, y: actual.top - factor * unit.top };
|
|
39
|
+
}
|
|
40
|
+
|
|
23
41
|
const SHAPE_SET = new Set(POINT_SHAPES);
|
|
24
42
|
|
|
25
43
|
export function validatePointShape(shape) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{cloneAndFreeze as
|
|
1
|
+
import{cloneAndFreeze as M,isPlainObject as D}from"../../core/immutable.js";import{MAX_GENERATED_ITEMS as T}from"../../core/validation.js";import{interpolateNumber as R,inverseLerp as w,numericExtent as q}from"../numeric.js";import{niceTicks as $}from"../ticks.js";import{niceLinearDomain as j}from"./continuous.js";import{SCALE_ROLES as L,validateScaleTypeForRole as _}from"./types.js";import{validatePair as I}from"./validation.js";const y=Object.freeze(["log","pow","sqrt","symlog"]);function P(t,e){if(!Number.isFinite(t)||t<=0)throw new RangeError(`${e} must be a positive finite number.`);return t}function p(t,e={}){if(!y.includes(t)&&t!=="linear")throw new Error(`Scale type "${t}" is not a quantitative transform.`);if(!D(e))throw new TypeError("Scale transform options must be a plain object.");const o=t==="log"?["base"]:t==="pow"?["exponent"]:t==="symlog"?["constant"]:[];for(const r of Object.keys(e))if(!o.includes(r))throw new Error(`Scale type "${t}" does not support ${r}.`);if(t==="log"){const r=P(e.base??10,"Log scale base");if(r===1)throw new RangeError("Log scale base must not equal 1.");return M({base:r})}return t==="pow"?M({exponent:P(e.exponent??1,"Power scale exponent")}):t==="sqrt"?M({exponent:.5}):t==="symlog"?M({constant:P(e.constant??1,"Symlog scale constant")}):M({})}function S(t,e,o,r){const n=[e,o,t].map(i=>Math.sign(i)*r(Math.abs(i)));return n[0]!==n[1]&&n.every(Number.isFinite)?w(n[2],n[0],n[1]):void 0}function z(t,e,o){const[r,n]=e,i=S(t,r,n,c=>c**o);if(i!==void 0)return i;const s=Math.max(Math.abs(r),Math.abs(n)),a=S(t,r,n,c=>c===0?0:Math.exp(o*(Math.log(c)-Math.log(s))));if(a!==void 0)return a;if(r!==0&&n!==0&&t!==0&&Math.sign(r)===Math.sign(n)){const c=Math.log(Math.abs(n))-Math.log(Math.abs(r)),m=Math.log(Math.abs(t))-Math.log(Math.abs(r));if(Math.abs(o*c)<1e-8)return m/c;const f=Math.expm1(o*m)/Math.expm1(o*c);if(Number.isFinite(f))return f}return w(t,r,n)}function x(t,e){if(t===0)return 0;const o=Math.abs(t)/e;if(Number.isFinite(o))return Math.sign(t)*Math.log1p(o);const r=Math.log(Math.abs(t))-Math.log(e);return Math.sign(t)*(Math.max(0,r)+Math.log1p(Math.exp(-Math.abs(r))))}function V(t,e,o,r){const[n,i]=e;if(o==="linear")return w(t,n,i);if(o==="log"){const m=Math.log10(r.base);return S(t,n,i,g=>Math.log10(g)/m)??S(t,n,i,Math.log)??w(t,n,i)}if(o==="pow"||o==="sqrt")return z(t,e,r.exponent);const s=x(n,r.constant),a=x(i,r.constant);if(s===a)return w(t,n,i);const c=w(x(t,r.constant),s,a);return Number.isFinite(c)?c:w(t,n,i)}function N(t,e,o={}){_(t,L.quantitativePosition);const r=I(e,"Transformed scale domain");if(r[0]===r[1])throw new RangeError("Transformed scale domain values must be distinct.");if(t==="log"&&(r.includes(0)||Math.sign(r[0])!==Math.sign(r[1])))throw new RangeError("Log scale domain must be strictly positive or strictly negative.");return p(t,o),r}function A(t,e,o){const r=Number.isFinite(t)?t**e:Number.NaN;if(Number.isFinite(r)&&r>0)return r;const n=e*o;return n>Math.log(Number.MAX_VALUE)?Number.POSITIVE_INFINITY:n<Math.log(Number.MIN_VALUE)?0:Math.exp(n)}function O(t,e){const o=Math.min(...t),r=Math.max(...t),n=r<0;return[e<1?-Math.log(e):Math.log(e),e<1?1/e:e,t[0]>t[1],n,n?Math.abs(r):o,n?Math.abs(o):r]}function C(t,e){const[o,r,n,i,s,a]=O(t,e);let c=A(r,Math.floor(Math.log(s)/o),o),m=A(r,Math.ceil(Math.log(a)/o),o);(!Number.isFinite(c)||c<=0||c>s)&&(c=s),(!Number.isFinite(m)||m<a)&&(m=a);const f=i?[-m,-c]:[c,m];return M(n?f.reverse():f)}function U(t,e,o){const r=p(t,o);if(e===0){if(t==="log")throw new RangeError("Log scale domain must be strictly positive or strictly negative.");return[-1,1]}const n=Math.abs(e);let i=n/2,s=n*2;if(t==="pow"||t==="sqrt")s=n*(2-2**-r.exponent)**(1/r.exponent);else if(t==="symlog"){const a=Math.abs(x(e,r.constant));if(a===0)return[-1,1];s=r.constant*Math.expm1(2*a-x(i,r.constant))}return i<n&&s>n&&Number.isFinite(s)||(i=n/2,s=Number.isFinite(n*2)?n*2:n),t==="log"&&i===0&&(i=n),e>0?[i,s]:[-s,i===0?0:-i]}function W({type:t,domain:e="auto",values:o,nice:r=!1,zero:n=!1,...i}){if(_(t,L.quantitativePosition),!Array.isArray(o)||!o.every(Number.isFinite))throw new TypeError("Transformed scale values must be finite numbers.");if(typeof r!="boolean"||typeof n!="boolean")throw new TypeError("Scale nice and zero must be booleans.");if(t==="log"&&n)throw new Error('Scale type "log" does not support zero.');if(e!=="auto")return N(t,e,i);if(o.length===0)throw new Error("Cannot infer an automatic scale domain from no values.");let a=q(o);return n&&(a=[Math.min(0,a[0]),Math.max(0,a[1])]),a[0]===a[1]&&(a=U(t,a[0],i)),N(t,a,i),r&&(a=t==="log"?C(a,p(t,i).base):j(a)),N(t,a,i)}function X(t,{reverse:e=!1}={}){const o=I(t,"Scale range");if(typeof e!="boolean")throw new TypeError("Scale reverse must be a boolean.");return e?M([...o].reverse()):o}function Z(t,e,o,{type:r="linear",clamp:n=!1,reverse:i=!1,...s}={}){if(!Array.isArray(t))throw new TypeError("Scale values must be an array.");if(typeof n!="boolean")throw new TypeError("Scale clamp must be a boolean.");const a=Object.hasOwn(s,"unknown"),{unknown:c,...m}=s,f=p(r,m),g=N(r,e,m),l=X(o,{reverse:i}),d=l[1]-l[0];return M(t.map(h=>{if(!(Number.isFinite(h)&&!(r==="log"&&(h===0||Math.sign(h)!==Math.sign(g[0]))))){if(a)return c;throw new TypeError(`Scale type "${r}" received an invalid value.`)}let u=V(h,g,r,f);n&&(u=Math.max(0,Math.min(1,u)));const b=l[0]+u*d;return Number.isFinite(d)&&Number.isFinite(b)&&(u!==1||Math.abs(b-l[1])<=Number.EPSILON*Math.abs(l[1]))?b:R(l[0],l[1],u)}))}function tt(t,e,o,r={}){const n=N(t,e,r);if(!Number.isInteger(o)||o<=0)throw new RangeError("Tick count must be a positive integer.");if(t!=="log")return $(n,o);const{base:i}=p(t,r),[s,a,c,m,f,g]=O(n,i),l=Math.ceil(Math.log(f)/s-1e-12),d=Math.floor(Math.log(g)/s+1e-12),h=d-l+1,E=h<=T?h:Math.min(o,T),u=[];for(let b=0;b<E;b+=1){const k=h<=T?l+b:Math.round(l+(d-l)*b/Math.max(1,E-1)),F=A(a,k,s),v=(m?-1:1)*F;Number.isFinite(v)&&F>=f&&F<=g&&(u.length===0||v!==u.at(-1))&&u.push(v)}return m&&u.reverse(),c&&u.reverse(),M(u.length===0?[...n]:u)}function rt(t,e,o={}){const r=N("symlog",t,o);if(!Number.isInteger(e)||e<2||e>T)throw new RangeError(`Sample count must be an integer from 2 to ${T}.`);const{constant:n}=p("symlog",o),i=r.map(m=>x(m,n)),s=Math.min(...r),a=Math.max(...r),c=Array.from({length:e},(m,f)=>{if(f===0)return r[0];if(f===e-1)return r[1];const g=f/(e-1);if(i[0]===i[1])return R(...r,g);const l=R(...i,g),d=Math.abs(l),h=n*Math.expm1(d),E=Math.sign(l)*(Number.isFinite(h)?h:Math.exp(Math.log(n)+d+Math.log1p(-Math.exp(-d))));return Math.max(s,Math.min(a,E))});if(s<0&&a>0&&e>2){const m=Math.round(w(0,...i)*(e-1));m>0&&m<e-1&&(c[m]=0)}return M(c.filter((m,f)=>f===0||m!==c[f-1]))}function nt(t,e){if(t!=="log")return String(e);const o=Math.abs(e),n=[[1e9,"B"],[1e6,"M"],[1e3,"K"]].find(([s])=>o>=s);if(n===void 0)return String(e);const i=e/n[0];return`${Number(i.toPrecision(3))}${n[1]}`}export{nt as formatTransformedTick,Z as mapTransformedValues,p as normalizeTransformParameters,X as resolveMappingRange,W as resolveTransformedDomain,rt as sampleSymlogValues,tt as transformedTicks,N as validateTransformedDomain};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { validateGeneratedItemLimit } from "../../core/validation.js";
|
|
2
2
|
import { validateFontStyle } from "../../core/font.js";
|
|
3
|
-
import { isPlainObject } from "../../core/immutable.js";
|
|
3
|
+
import { isOwned, isPlainObject } from "../../core/immutable.js";
|
|
4
4
|
import { validatePathCommands } from "../pathCommands.js";
|
|
5
5
|
import { validateFillPaint } from "../paint.js";
|
|
6
6
|
import {
|
|
@@ -25,6 +25,7 @@ const TEXT_ALIGNS = new Set(["left", "right", "center", "start", "end"]);
|
|
|
25
25
|
const TEXT_BASELINES = new Set([
|
|
26
26
|
"top", "hanging", "middle", "alphabetic", "ideographic", "bottom"
|
|
27
27
|
]);
|
|
28
|
+
const validatedProperties = new WeakMap();
|
|
28
29
|
|
|
29
30
|
export function validateConcreteGraphicValue(type, property, value) {
|
|
30
31
|
if (property === "lines") {
|
|
@@ -98,6 +99,8 @@ export function validateConcreteGraphicValue(type, property, value) {
|
|
|
98
99
|
}
|
|
99
100
|
|
|
100
101
|
export function validateConcreteGraphicProperties(type, properties) {
|
|
102
|
+
const validatedTypes = validatedProperties.get(properties);
|
|
103
|
+
if (validatedTypes?.has(type)) return properties;
|
|
101
104
|
if (!isPlainObject(properties)) {
|
|
102
105
|
throw new TypeError(`${type}.properties must be a plain object.`);
|
|
103
106
|
}
|
|
@@ -105,5 +108,9 @@ export function validateConcreteGraphicProperties(type, properties) {
|
|
|
105
108
|
validateGraphicProperty(type, property);
|
|
106
109
|
validateConcreteGraphicValue(type, property, value);
|
|
107
110
|
}
|
|
111
|
+
if (isOwned(properties)) {
|
|
112
|
+
if (validatedTypes) validatedTypes.add(type);
|
|
113
|
+
else validatedProperties.set(properties, new Set([type]));
|
|
114
|
+
}
|
|
108
115
|
return properties;
|
|
109
116
|
}
|
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
resolveTextBounds
|
|
4
4
|
} from "../../core/textMetrics.js";
|
|
5
5
|
import { interpolateNumber } from "../numeric.js";
|
|
6
|
+
import { isOwned } from "../../core/immutable.js";
|
|
6
7
|
import { graphicClipBounds, intersectClipBounds } from "./graphicClip.js";
|
|
7
8
|
import { resolveStrokeDetails } from "../strokeStyle.js";
|
|
8
9
|
import {
|
|
@@ -12,6 +13,7 @@ import {
|
|
|
12
13
|
} from "./graphicTree.js";
|
|
13
14
|
|
|
14
15
|
const EPSILON = Number.EPSILON * 64;
|
|
16
|
+
const pathBounds = new WeakMap();
|
|
15
17
|
|
|
16
18
|
function box(left, right, top, bottom, extent = 0) {
|
|
17
19
|
return {
|
|
@@ -111,6 +113,10 @@ function cubicExtrema(start, control1, control2, end) {
|
|
|
111
113
|
}
|
|
112
114
|
|
|
113
115
|
export function resolvePathCommandBounds(commands) {
|
|
116
|
+
if (pathBounds.has(commands)) {
|
|
117
|
+
const cached = pathBounds.get(commands);
|
|
118
|
+
return cached === undefined ? undefined : { ...cached };
|
|
119
|
+
}
|
|
114
120
|
let bounds;
|
|
115
121
|
let current;
|
|
116
122
|
for (const command of commands) {
|
|
@@ -145,7 +151,8 @@ export function resolvePathCommandBounds(commands) {
|
|
|
145
151
|
}
|
|
146
152
|
current = { x: command.x, y: command.y };
|
|
147
153
|
}
|
|
148
|
-
|
|
154
|
+
if (isOwned(commands)) pathBounds.set(commands, bounds);
|
|
155
|
+
return bounds === undefined ? undefined : { ...bounds };
|
|
149
156
|
}
|
|
150
157
|
|
|
151
158
|
function endpointDirection(start, command, end, reverse = false) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { isPlainObject } from "../../core/immutable.js";
|
|
1
|
+
import { isOwned, isPlainObject } from "../../core/immutable.js";
|
|
2
2
|
import { isGraphicContainerType } from "./graphic.js";
|
|
3
3
|
|
|
4
4
|
export function resolveGraphicChildOrder(graphicSpec, target, value) {
|
|
@@ -34,7 +34,7 @@ export function findGraphic(graphicSpec, target) {
|
|
|
34
34
|
return { id: target, object, kind: "object" };
|
|
35
35
|
}
|
|
36
36
|
for (const [ownerId, owner] of Object.entries(graphicSpec.objects)) {
|
|
37
|
-
const itemIndex = owner.items
|
|
37
|
+
const itemIndex = findItemIndex(owner.items, target);
|
|
38
38
|
if (itemIndex !== undefined && itemIndex !== -1) {
|
|
39
39
|
return {
|
|
40
40
|
id: target,
|
|
@@ -49,6 +49,22 @@ export function findGraphic(graphicSpec, target) {
|
|
|
49
49
|
return undefined;
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
+
// Collections are immutable across unrelated edits. Index their items once,
|
|
53
|
+
// instead of scanning every point/path again for each label or inspection.
|
|
54
|
+
// Caller-owned arrays remain uncached, including shallow-frozen inputs.
|
|
55
|
+
const itemIndexes = new WeakMap();
|
|
56
|
+
function findItemIndex(items, target) {
|
|
57
|
+
if (!items) return undefined;
|
|
58
|
+
if (!isOwned(items)) return items.findIndex(item => item.id === target);
|
|
59
|
+
let index = itemIndexes.get(items);
|
|
60
|
+
if (!index) {
|
|
61
|
+
index = new Map();
|
|
62
|
+
items.forEach((item, i) => { if (!index.has(item.id)) index.set(item.id, i); });
|
|
63
|
+
itemIndexes.set(items, index);
|
|
64
|
+
}
|
|
65
|
+
return index.get(target) ?? -1;
|
|
66
|
+
}
|
|
67
|
+
|
|
52
68
|
export function requireGraphic(graphicSpec, target) {
|
|
53
69
|
const found = findGraphic(graphicSpec, target);
|
|
54
70
|
if (found === undefined) {
|
|
@@ -68,6 +68,7 @@ const ENTITY_PATHS = Object.freeze({
|
|
|
68
68
|
"mark.orientation",
|
|
69
69
|
"mark.blend",
|
|
70
70
|
"derivedBindings.regression",
|
|
71
|
+
"derivedBindings.overlay",
|
|
71
72
|
"layout.mode",
|
|
72
73
|
...ENCODING_PATHS
|
|
73
74
|
])
|
|
@@ -106,6 +107,8 @@ const GUIDE_PATHS = new Set([
|
|
|
106
107
|
"legend.colorSize.colorScale",
|
|
107
108
|
"legend.colorSize.sizeScale",
|
|
108
109
|
"legend.colorSize.title",
|
|
110
|
+
"legend.colorSize.shapeScale",
|
|
111
|
+
"legend.colorSize.colorChannel",
|
|
109
112
|
"legend.color.scale",
|
|
110
113
|
"legend.color.title",
|
|
111
114
|
"legend.color.order",
|