ggaction 0.0.15 → 0.0.17
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 +88 -0
- package/README.md +23 -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-packet.schema.json +1 -1
- package/knowledge/task-resolver.js +1 -1
- package/package.json +40 -7
- package/src/BasicChartProgram.js +3 -1
- package/src/ChartProgram.js +2 -0
- package/src/accessibility.js +157 -0
- package/src/actions/boxPlots/components.js +6 -8
- package/src/actions/boxPlots/create.js +4 -5
- package/src/actions/boxPlots/edit.js +3 -4
- package/src/actions/boxPlots/materialize.js +1 -1
- package/src/actions/canvas/actions.js +3 -3
- package/src/actions/canvas/fitting.js +1 -1
- package/src/actions/categoryOrder/index.js +6 -8
- package/src/actions/charts/area.js +1 -1
- package/src/actions/charts/bar.js +1 -1
- package/src/actions/charts/beeswarm.js +1 -1
- package/src/actions/charts/density.js +1 -1
- package/src/actions/charts/ecdf.js +2 -2
- package/src/actions/charts/endpoints.js +4 -4
- package/src/actions/charts/heatmap.js +1 -1
- package/src/actions/charts/histogram.js +1 -1
- package/src/actions/charts/horizon.js +1 -1
- package/src/actions/charts/interval-regression.js +2 -2
- package/src/actions/charts/line.js +1 -1
- package/src/actions/charts/parallel.js +1 -1
- package/src/actions/charts/pie.js +1 -1
- package/src/actions/charts/polar.js +2 -2
- package/src/actions/charts/radar.js +1 -1
- package/src/actions/charts/radial.js +2 -2
- package/src/actions/charts/raincloud.js +2 -2
- package/src/actions/charts/rug-strip.js +2 -2
- package/src/actions/charts/scatter.js +1 -1
- package/src/actions/composition/actions.js +20 -24
- package/src/actions/coordinates/actions.js +1 -1
- package/src/actions/coordinates/edit.js +1 -1
- package/src/actions/data/basicBin2d.js +4 -5
- package/src/actions/data/bin.js +3 -3
- package/src/actions/data/bin2d.js +6 -8
- package/src/actions/data/bin2dMaterialize.js +4 -5
- package/src/actions/data/box.js +4 -4
- package/src/actions/data/complete.js +2 -2
- package/src/actions/data/computed.js +2 -2
- package/src/actions/data/create.js +23 -17
- package/src/actions/data/density.js +4 -4
- package/src/actions/data/derived.js +25 -15
- package/src/actions/data/ecdf.js +2 -2
- package/src/actions/data/edit.js +54 -29
- package/src/actions/data/filter.js +29 -24
- package/src/actions/data/fold.js +2 -2
- package/src/actions/data/gradientProfile.js +2 -2
- package/src/actions/data/horizon.js +2 -2
- package/src/actions/data/impute.js +2 -2
- package/src/actions/data/index.js +58 -54
- package/src/actions/data/interval.js +4 -2
- package/src/actions/data/intervalEdit.js +26 -18
- package/src/actions/data/normalize.js +2 -2
- package/src/actions/data/regression.js +9 -7
- package/src/actions/data/revise.js +80 -0
- package/src/actions/data/revisionBindings.js +13 -33
- package/src/actions/data/shared.js +30 -2
- package/src/actions/data/sort.js +21 -0
- package/src/actions/data/stack.js +2 -2
- package/src/actions/data/summary.js +3 -3
- package/src/actions/data/timeUnit.js +2 -2
- package/src/actions/data/window.js +2 -2
- package/src/actions/encodings/angle.js +5 -2
- package/src/actions/encodings/appearance.js +10 -6
- package/src/actions/encodings/barWidth.js +1 -1
- package/src/actions/encodings/channels.js +2 -2
- package/src/actions/encodings/color/continuous.js +4 -1
- package/src/actions/encodings/color/index.js +5 -2
- package/src/actions/encodings/density.js +2 -2
- package/src/actions/encodings/histogram.js +1 -1
- package/src/actions/encodings/horizon.js +6 -8
- package/src/actions/encodings/offset.js +6 -3
- package/src/actions/encodings/parallel.js +4 -5
- package/src/actions/encodings/pathOrder.js +6 -11
- package/src/actions/encodings/position/index.js +4 -4
- package/src/actions/encodings/position/resolve.js +6 -1
- package/src/actions/encodings/ranged.js +11 -6
- package/src/actions/encodings/remove.js +4 -5
- package/src/actions/encodings/ruleAppearance.js +5 -2
- package/src/actions/encodings/seriesLayout.js +1 -1
- package/src/actions/encodings/stroke.js +5 -2
- package/src/actions/encodings/strokeDash.js +8 -3
- package/src/actions/encodings/text.js +7 -2
- package/src/actions/errorBands/create.js +11 -17
- package/src/actions/errorBands/edit.js +13 -131
- package/src/actions/errorBars/create.js +8 -13
- package/src/actions/errorBars/edit.js +10 -121
- package/src/actions/facets/actions.js +30 -16
- package/src/actions/facets/derive.js +8 -2
- package/src/actions/facets/guides.js +4 -5
- package/src/actions/facets/replay.js +4 -5
- package/src/actions/gradientPlots/components.js +5 -6
- package/src/actions/gradientPlots/create.js +4 -5
- package/src/actions/gradientPlots/edit.js +4 -5
- package/src/actions/gradientPlots/materialize.js +2 -2
- package/src/actions/gradientPlots/rebind.js +4 -5
- package/src/actions/guides/axes/axes.js +23 -4
- package/src/actions/guides/axes/axis.js +2 -2
- package/src/actions/guides/axes/edit.js +2 -2
- package/src/actions/guides/axes/labels.js +9 -9
- package/src/actions/guides/axes/lines.js +9 -7
- package/src/actions/guides/axes/parallel/lifecycle.js +13 -13
- package/src/actions/guides/axes/parallel.js +5 -5
- package/src/actions/guides/axes/remove.js +4 -4
- package/src/actions/guides/axes/tickGroups.js +4 -4
- package/src/actions/guides/axes/ticks.js +13 -4
- package/src/actions/guides/axes/titles.js +10 -10
- package/src/actions/guides/grids/grid.js +18 -29
- package/src/actions/guides/guides.js +1 -1
- package/src/actions/guides/legends/blocks.js +1 -1
- package/src/actions/guides/legends/categorical/actions.js +4 -4
- package/src/actions/guides/legends/categorical/components.js +38 -33
- package/src/actions/guides/legends/categorical/layout.js +1 -1
- package/src/actions/guides/legends/categorical/symbols.js +27 -30
- package/src/actions/guides/legends/continuous/common.js +46 -8
- package/src/actions/guides/legends/continuous/gradient.js +89 -155
- package/src/actions/guides/legends/continuous/interval.js +13 -13
- package/src/actions/guides/legends/continuous/opacity.js +30 -79
- package/src/actions/guides/legends/continuous/stroke.js +22 -322
- package/src/actions/guides/legends/edit.js +1 -1
- package/src/actions/guides/legends/focused.js +5 -5
- package/src/actions/guides/legends/lane.js +6 -6
- package/src/actions/guides/legends/remove.js +4 -5
- package/src/actions/guides/legends/size.js +21 -82
- package/src/actions/guides/legends/strokeWidth.js +17 -77
- package/src/actions/guides/polar/axes/facade.js +4 -4
- package/src/actions/guides/polar/axes/labels.js +5 -5
- package/src/actions/guides/polar/axes/lines.js +9 -15
- package/src/actions/guides/polar/axes/ticks.js +9 -15
- package/src/actions/guides/polar/axes/titles.js +5 -5
- package/src/actions/guides/polar/grids.js +11 -13
- package/src/actions/index.js +2 -0
- package/src/actions/intervals/revision.js +126 -0
- package/src/actions/marks/arc/actions.js +3 -3
- package/src/actions/marks/area/actions.js +4 -4
- package/src/actions/marks/bar/create.js +9 -2
- package/src/actions/marks/bar/edit.js +13 -6
- package/src/actions/marks/bar/materialize.js +12 -38
- package/src/actions/marks/line/actions.js +3 -3
- package/src/actions/marks/point/create.js +9 -2
- package/src/actions/marks/point/edit.js +13 -6
- package/src/actions/marks/point/jitter.js +6 -8
- package/src/actions/marks/point/materialize.js +20 -26
- package/src/actions/marks/point/packing.js +6 -8
- package/src/actions/marks/rect/actions.js +26 -11
- package/src/actions/marks/references.js +8 -10
- package/src/actions/marks/remove.js +5 -49
- package/src/actions/marks/rule/actions.js +28 -12
- package/src/actions/marks/text/actions.js +30 -11
- package/src/actions/marks/text/layout.js +12 -20
- package/src/actions/marks/text/removal.js +1 -1
- package/src/actions/marks/text/selection.js +4 -5
- package/src/actions/marks/tick/actions.js +26 -11
- package/src/actions/primitives/createGraphics.js +5 -4
- package/src/actions/primitives/editGraphics.js +5 -4
- package/src/actions/primitives/graphicProperties.js +6 -0
- package/src/actions/primitives/semantic.js +10 -3
- package/src/actions/primitives/semanticAction.js +44 -7
- package/src/actions/primitives/semanticValidation/dataset.js +14 -4
- package/src/actions/primitives/semanticValidation/layer.js +22 -2
- package/src/actions/primitives/semanticValidation/scale.js +6 -0
- package/src/actions/regression/components.js +10 -14
- package/src/actions/regression/create.js +28 -8
- package/src/actions/regression/edit.js +50 -16
- package/src/actions/regression/follow.js +22 -0
- package/src/actions/resources/remove.js +12 -11
- package/src/actions/scales/channels.js +11 -11
- package/src/actions/scales/consumers/common.js +2 -1
- package/src/actions/scales/create.js +3 -2
- package/src/actions/scales/definitions.js +1 -1
- package/src/actions/scales/edit.js +2 -2
- package/src/actions/scales/materialize.js +1 -1
- package/src/actions/scales/offset.js +2 -2
- package/src/actions/scales/parallel.js +2 -2
- package/src/actions/scales/patch.js +1 -1
- package/src/actions/scales/preview.js +28 -1
- package/src/actions/scales/quantitativeColor.js +4 -5
- package/src/actions/selection/actions.js +25 -36
- package/src/actions/textMetrics/index.js +56 -0
- package/src/actions/theme/actions.js +2 -2
- package/src/actions/theme/reconcile.js +67 -121
- package/src/actions/titles/actions.js +8 -8
- package/src/actions/titles/resolve.js +3 -3
- package/src/actions/violinPlots/create.js +1 -1
- package/src/actions/violinPlots/edit.js +1 -1
- package/src/core/action.js +109 -36
- package/src/core/diagnostics.js +16 -0
- package/src/core/extensionRegistry.js +27 -0
- package/src/core/font.js +18 -0
- package/src/core/identifiers.js +5 -4
- package/src/core/immutable.js +10 -3
- package/src/core/optionRequirements.js +8 -0
- package/src/core/programState.js +6 -2
- package/src/core/resourceReferences.js +15 -6
- package/src/core/textMetricProfile.js +28 -0
- package/src/core/textMetrics.js +24 -3
- package/src/core/validation.js +22 -10
- package/src/diagnostics.js +1 -0
- package/src/grammar/bin.js +39 -10
- package/src/grammar/datasetSchema.js +386 -0
- package/src/grammar/density.js +53 -5
- package/src/grammar/filter.js +133 -39
- package/src/grammar/impute.js +3 -2
- package/src/grammar/interval.js +32 -1
- package/src/grammar/itemMissing.js +59 -0
- package/src/grammar/regression/derive.js +57 -16
- package/src/grammar/regression/index.js +1 -0
- package/src/grammar/regression/models.js +51 -22
- package/src/grammar/regression/parameters.js +56 -4
- package/src/grammar/scales/definition.js +7 -0
- package/src/grammar/scales/types.js +8 -0
- package/src/grammar/schemas/graphicBounds.js +11 -11
- package/src/grammar/schemas/semanticPath.js +3 -2
- package/src/grammar/sort.js +88 -0
- package/src/grammar/summary.js +117 -19
- package/src/grammar/transformTopology.js +1 -0
- package/src/grammar/transforms.js +21 -11
- package/src/inspection.js +495 -0
- package/src/inspectionDescriptors.js +3 -0
- package/src/layout/labels.js +11 -10
- package/src/layout/legend.js +4 -4
- package/src/layout/legendItems.js +6 -6
- package/src/layout/text.js +10 -9
- package/src/layout/title.js +9 -9
- package/src/materialization/bars/resolve.js +4 -2
- package/src/materialization/facetGuides/placement.js +1 -1
- package/src/materialization/facetGuides/preparation.js +1 -1
- package/src/materialization/facetHeaders.js +11 -10
- package/src/materialization/guides/layout.js +3 -3
- package/src/materialization/revisionIdentity.js +19 -0
- package/src/materialization/scaleGuideDependencies.js +10 -0
- package/src/materialization/scales/resolve.js +67 -4
- package/src/materialization/selection/items/common.js +1 -1
- package/src/materialization/text.js +6 -4
- package/src/materialization/typography.js +31 -0
- package/src/mcp/cli.js +5 -3
- package/src/persistence/codec.js +65 -0
- package/src/persistence/validation.js +185 -0
- package/src/persistence.js +147 -0
- package/src/renderers/canvas/index.js +5 -4
- package/src/renderers/nativeBackend.js +10 -0
- package/src/renderers/options.js +13 -0
- package/src/renderers/pdf.js +25 -54
- package/src/renderers/png.js +22 -17
- package/src/renderers/svg.js +10 -14
- package/src/renderers/text.js +1 -11
- package/src/selectors/coordinates.js +3 -1
- package/src/selectors/datasets.js +8 -6
- package/src/selectors/layers.js +6 -4
- package/src/selectors/markOwners.js +46 -0
- package/src/selectors/scales.js +6 -4
- package/src/theme/defaults.js +2 -1
- package/src/version.js +2 -0
- package/types/accessibility.d.ts +37 -0
- package/types/basic.d.ts +4 -2
- package/types/diagnostics.d.ts +15 -0
- package/types/index.d.ts +5 -0
- package/types/inspection.d.ts +17 -0
- package/types/pdf.d.ts +9 -0
- package/types/persistence.d.ts +7 -0
- package/types/png.d.ts +9 -0
- package/types/program.d.ts +181 -28
package/src/core/identifiers.js
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
|
+
import { annotateError } from "./diagnostics.js";
|
|
1
2
|
export const USER_ID_SOURCE = "[A-Za-z0-9_-]+";
|
|
2
3
|
|
|
3
4
|
const USER_ID_PATTERN = new RegExp(`^${USER_ID_SOURCE}$`);
|
|
4
5
|
|
|
5
6
|
export function validateUserId(id, label = "ID") {
|
|
6
7
|
if (typeof id !== "string" || !USER_ID_PATTERN.test(id)) {
|
|
7
|
-
throw new TypeError(
|
|
8
|
+
throw annotateError(new TypeError(
|
|
8
9
|
`${label} must contain only letters, numbers, _ or - and must not be empty.`
|
|
9
|
-
);
|
|
10
|
+
), { code: "invalid-value", optionPath: label });
|
|
10
11
|
}
|
|
11
12
|
|
|
12
13
|
return id;
|
|
@@ -20,9 +21,9 @@ export function resolveOptionalUserId(id, {
|
|
|
20
21
|
}) {
|
|
21
22
|
if (id !== undefined) return validateUserId(id, label);
|
|
22
23
|
if (ambiguous) {
|
|
23
|
-
throw new Error(
|
|
24
|
+
throw annotateError(new Error(
|
|
24
25
|
`${operation} requires an explicit ${label.toLowerCase()} because its default is ambiguous.`
|
|
25
|
-
);
|
|
26
|
+
), { code: "ambiguous-resource", operation, optionPath: label });
|
|
26
27
|
}
|
|
27
28
|
return validateUserId(defaultId, label);
|
|
28
29
|
}
|
package/src/core/immutable.js
CHANGED
|
@@ -22,11 +22,18 @@ export function isOwned(value) {
|
|
|
22
22
|
return value !== null && typeof value === "object" && ownedValues.has(value);
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
export function cloneAndFreeze(value, ancestors = new WeakSet()) {
|
|
25
|
+
export function cloneAndFreeze(value, ancestors = new WeakSet(), path = "state") {
|
|
26
|
+
if (typeof value === "function") {
|
|
27
|
+
throw new TypeError(`Cannot store a function at ${path} in a ChartProgram.`);
|
|
28
|
+
}
|
|
26
29
|
if (value === null || typeof value !== "object") {
|
|
27
30
|
return value;
|
|
28
31
|
}
|
|
29
32
|
|
|
33
|
+
// Only library-owned values are recursively immutable; Object.freeze alone
|
|
34
|
+
// does not prove ownership of a caller's descendants.
|
|
35
|
+
if (isOwned(value)) return value;
|
|
36
|
+
|
|
30
37
|
if (ancestors.has(value)) {
|
|
31
38
|
throw new TypeError("Cannot store cyclic values in a ChartProgram.");
|
|
32
39
|
}
|
|
@@ -36,12 +43,12 @@ export function cloneAndFreeze(value, ancestors = new WeakSet()) {
|
|
|
36
43
|
let clone;
|
|
37
44
|
|
|
38
45
|
if (Array.isArray(value)) {
|
|
39
|
-
clone = value.map(item => cloneAndFreeze(item, ancestors));
|
|
46
|
+
clone = value.map((item, index) => cloneAndFreeze(item, ancestors, `${path}[${index}]`));
|
|
40
47
|
} else if (isPlainObject(value)) {
|
|
41
48
|
clone = Object.fromEntries(
|
|
42
49
|
Object.entries(value).map(([key, item]) => [
|
|
43
50
|
key,
|
|
44
|
-
cloneAndFreeze(item, ancestors)
|
|
51
|
+
cloneAndFreeze(item, ancestors, `${path}.${key}`)
|
|
45
52
|
])
|
|
46
53
|
);
|
|
47
54
|
} else {
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// The method's value-producing contract is shared by runtime validation and
|
|
2
|
+
// authoring knowledge; editing can inherit these values from stored state.
|
|
3
|
+
export const IMPUTE_REQUIRED_OPTIONS = Object.freeze({
|
|
4
|
+
constant: Object.freeze(["value"]),
|
|
5
|
+
forward: Object.freeze(["sortBy"]),
|
|
6
|
+
backward: Object.freeze(["sortBy"]),
|
|
7
|
+
linear: Object.freeze(["sortBy"])
|
|
8
|
+
});
|
package/src/core/programState.js
CHANGED
|
@@ -23,7 +23,7 @@ export function ownChildPrograms(children, ProgramClass) {
|
|
|
23
23
|
if (!(program instanceof ProgramClass)) {
|
|
24
24
|
throw new TypeError(`ChartProgram child "${id}" must be a ChartProgram.`);
|
|
25
25
|
}
|
|
26
|
-
owned
|
|
26
|
+
Object.defineProperty(owned, id, { value: program, enumerable: true });
|
|
27
27
|
}
|
|
28
28
|
return freezeOwned(owned);
|
|
29
29
|
}
|
|
@@ -34,12 +34,16 @@ export function ownChildPrograms(children, ProgramClass) {
|
|
|
34
34
|
// domain's dependency validation and before the primitive deletion.
|
|
35
35
|
export function releaseNamedResourceOwnership(program, { kind, id, dataOwner }) {
|
|
36
36
|
if (kind !== "data") return program;
|
|
37
|
-
|
|
37
|
+
let materializationConfigs = dataOwner === undefined
|
|
38
38
|
? program.materializationConfigs
|
|
39
39
|
: removeMaterializationConfig(
|
|
40
40
|
program.materializationConfigs,
|
|
41
41
|
["data", dataOwner.family, dataOwner.owner]
|
|
42
42
|
).value;
|
|
43
|
+
materializationConfigs = removeMaterializationConfig(
|
|
44
|
+
materializationConfigs,
|
|
45
|
+
["calculations", "datasets", dataOwner?.current ?? id]
|
|
46
|
+
).value;
|
|
43
47
|
const context = program.context.currentData === id
|
|
44
48
|
? freezeOwned({ ...program.context, currentData: undefined })
|
|
45
49
|
: program.context;
|
|
@@ -148,14 +148,19 @@ export function collectMarkConfigReferences(program) {
|
|
|
148
148
|
"coordinate", value, "markConfig", ownerId, path
|
|
149
149
|
));
|
|
150
150
|
}
|
|
151
|
-
|
|
151
|
+
const reservedScale = root === "regression" && key === "colorScale" && config.regression.groupBy === undefined;
|
|
152
|
+
if (!reservedScale && (key === "scale" || key.endsWith("Scale")) && typeof value === "string") {
|
|
152
153
|
append(references, reference("scale", value, "markConfig", ownerId, path));
|
|
153
154
|
} else if (key === "scale" && isPlainObject(value)) {
|
|
154
155
|
append(references, reference(
|
|
155
156
|
"scale", value.id, "markConfig", ownerId, [...path, "id"]
|
|
156
157
|
));
|
|
157
158
|
}
|
|
158
|
-
|
|
159
|
+
const inactiveMark = (root === "boxPlot" && key === "outlierId" && !Object.hasOwn(program.markConfigs, value)) ||
|
|
160
|
+
(root === "errorBar" && ["lowerCapId", "upperCapId"].includes(key) && config.errorBar.caps === false) ||
|
|
161
|
+
(root === "errorBand" && ["lowerBoundaryId", "upperBoundaryId"].includes(key) &&
|
|
162
|
+
!Object.hasOwn(program.markConfigs, value));
|
|
163
|
+
if (MARK_ID_KEYS.has(key) && !inactiveMark) {
|
|
159
164
|
append(references, reference("mark", value, "markConfig", ownerId, path));
|
|
160
165
|
}
|
|
161
166
|
if (
|
|
@@ -316,7 +321,13 @@ export function collectResourceReferences(program, { kind, id }) {
|
|
|
316
321
|
throw new Error(`Unknown resource kind "${kind}".`);
|
|
317
322
|
}
|
|
318
323
|
const target = validateUserId(id, "Resource id");
|
|
319
|
-
const combined =
|
|
324
|
+
const combined = collectAllResourceReferences(program).filter(value => value.kind === kind && value.id === target);
|
|
325
|
+
const unique = new Map(combined.map(value => [referenceKey(value), value]));
|
|
326
|
+
return freezeOwned([...unique.values()].sort(compareReferences));
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
export function collectAllResourceReferences(program) {
|
|
330
|
+
return [
|
|
320
331
|
...collectSemanticDataReferences(program),
|
|
321
332
|
...collectSemanticScaleReferences(program),
|
|
322
333
|
...collectSemanticCoordinateReferences(program),
|
|
@@ -326,7 +337,5 @@ export function collectResourceReferences(program, { kind, id }) {
|
|
|
326
337
|
...collectDataOwnerReferences(program),
|
|
327
338
|
...collectCompositionReferences(program),
|
|
328
339
|
...collectContextReferences(program)
|
|
329
|
-
]
|
|
330
|
-
const unique = new Map(combined.map(value => [referenceKey(value), value]));
|
|
331
|
-
return freezeOwned([...unique.values()].sort(compareReferences));
|
|
340
|
+
];
|
|
332
341
|
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { cloneAndFreeze } from "./immutable.js";
|
|
2
|
+
import { validateUserId } from "./identifiers.js";
|
|
3
|
+
import { validateOptionObject, validatePositiveFinite, validateNonNegativeFinite, validateNonEmptyString } from "./validation.js";
|
|
4
|
+
|
|
5
|
+
import { textMetricKey } from "./font.js";
|
|
6
|
+
|
|
7
|
+
export function normalizeTextMetricProfile(profile) {
|
|
8
|
+
validateOptionObject(profile, ["schemaVersion", "id", "measurements"], "Text metrics profile");
|
|
9
|
+
if (profile.schemaVersion !== 1) throw new Error("Text metrics require schemaVersion 1.");
|
|
10
|
+
validateUserId(profile.id, "Text metrics profile id");
|
|
11
|
+
if (!Array.isArray(profile.measurements)) throw new TypeError("Text metrics measurements must be an array.");
|
|
12
|
+
const seen = new Set();
|
|
13
|
+
for (const measurement of profile.measurements) {
|
|
14
|
+
validateOptionObject(measurement, ["text", "fontFamily", "fontSize", "fontWeight", "width"], "Text measurement");
|
|
15
|
+
if (typeof measurement.text !== "string") throw new TypeError("Text measurement text must be a string.");
|
|
16
|
+
validateNonEmptyString(measurement.fontFamily, "Text measurement fontFamily");
|
|
17
|
+
validatePositiveFinite(measurement.fontSize, "Text measurement fontSize");
|
|
18
|
+
validateNonNegativeFinite(measurement.width, "Text measurement width");
|
|
19
|
+
if (!Number.isInteger(measurement.fontWeight) || measurement.fontWeight < 100 ||
|
|
20
|
+
measurement.fontWeight > 900 || measurement.fontWeight % 100 !== 0) {
|
|
21
|
+
throw new RangeError("Text measurement fontWeight must be 100–900 in steps of 100.");
|
|
22
|
+
}
|
|
23
|
+
const key = textMetricKey(measurement);
|
|
24
|
+
if (seen.has(key)) throw new Error("Duplicate text measurement combination.");
|
|
25
|
+
seen.add(key);
|
|
26
|
+
}
|
|
27
|
+
return cloneAndFreeze(profile);
|
|
28
|
+
}
|
package/src/core/textMetrics.js
CHANGED
|
@@ -1,3 +1,21 @@
|
|
|
1
|
+
import { DEFAULT_FONT_FAMILY, textMetricFontWeight, textMetricKey } from "./font.js";
|
|
2
|
+
|
|
3
|
+
import { isOwned } from "./immutable.js";
|
|
4
|
+
|
|
5
|
+
const measurementIndexes = new WeakMap();
|
|
6
|
+
|
|
7
|
+
function measuredWidth(profile, text, style) {
|
|
8
|
+
if (profile === undefined) return undefined;
|
|
9
|
+
let index = measurementIndexes.get(profile);
|
|
10
|
+
if (index === undefined) {
|
|
11
|
+
index = new Map(profile.measurements.map(measurement =>
|
|
12
|
+
[textMetricKey(measurement), measurement.width]));
|
|
13
|
+
if (isOwned(profile)) measurementIndexes.set(profile, index);
|
|
14
|
+
}
|
|
15
|
+
return index.get(textMetricKey({ text, fontFamily: style.fontFamily ?? DEFAULT_FONT_FAMILY,
|
|
16
|
+
fontSize: style.fontSize, fontWeight: textMetricFontWeight(style.fontWeight) }));
|
|
17
|
+
}
|
|
18
|
+
|
|
1
19
|
function codePointWidth(codePoint) {
|
|
2
20
|
if (/\s/u.test(codePoint)) return 0.28;
|
|
3
21
|
if (/[iIl.,:;!'|]/u.test(codePoint)) return 0.27;
|
|
@@ -25,7 +43,8 @@ export function textBoundsFitCanvas(bounds, canvas) {
|
|
|
25
43
|
|
|
26
44
|
export function measureTextWidth(
|
|
27
45
|
text,
|
|
28
|
-
{ fontSize, fontFamily, fontWeight } = {}
|
|
46
|
+
{ fontSize, fontFamily, fontWeight } = {},
|
|
47
|
+
profile
|
|
29
48
|
) {
|
|
30
49
|
if (typeof text !== "string") {
|
|
31
50
|
throw new TypeError("Text measurement requires a string.");
|
|
@@ -33,6 +52,8 @@ export function measureTextWidth(
|
|
|
33
52
|
if (!Number.isFinite(fontSize) || fontSize <= 0) {
|
|
34
53
|
throw new RangeError("Text measurement requires a positive fontSize.");
|
|
35
54
|
}
|
|
55
|
+
const measured = measuredWidth(profile, text, { fontSize, fontFamily, fontWeight });
|
|
56
|
+
if (measured !== undefined) return measured;
|
|
36
57
|
let joined = false;
|
|
37
58
|
let width = 0;
|
|
38
59
|
for (const codePoint of text) {
|
|
@@ -76,11 +97,11 @@ export function resolveTextBounds({
|
|
|
76
97
|
textAlign = "left",
|
|
77
98
|
textBaseline = "alphabetic",
|
|
78
99
|
rotation = 0
|
|
79
|
-
} = {}) {
|
|
100
|
+
} = {}, profile) {
|
|
80
101
|
if (![x, y, rotation].every(Number.isFinite)) {
|
|
81
102
|
throw new TypeError("Text bounds require finite x, y, and rotation values.");
|
|
82
103
|
}
|
|
83
|
-
const width = measureTextWidth(text, { fontSize, fontFamily, fontWeight });
|
|
104
|
+
const width = measureTextWidth(text, { fontSize, fontFamily, fontWeight }, profile);
|
|
84
105
|
const [left, right] = textAlign === "center"
|
|
85
106
|
? [-width / 2, width / 2]
|
|
86
107
|
: ["right", "end"].includes(textAlign) ? [-width, 0] : [0, width];
|
package/src/core/validation.js
CHANGED
|
@@ -1,15 +1,27 @@
|
|
|
1
|
+
import { annotateError } from "./diagnostics.js";
|
|
1
2
|
import { isPlainObject } from "./immutable.js";
|
|
2
3
|
|
|
3
4
|
export const MAX_GENERATED_ITEMS = 10_000;
|
|
4
5
|
export const MAX_WORK_ITEMS = 10_000_000;
|
|
5
6
|
|
|
7
|
+
export function validateDataRows(values, label = "Dataset") {
|
|
8
|
+
if (!Array.isArray(values)) {
|
|
9
|
+
throw annotateError(new TypeError(`${label} requires values to be an array.`), { code: "invalid-value", optionPath: "values" });
|
|
10
|
+
}
|
|
11
|
+
for (let index = 0; index < values.length; index += 1) {
|
|
12
|
+
if (!Object.hasOwn(values, index) || !isPlainObject(values[index])) {
|
|
13
|
+
throw annotateError(new TypeError(`${label} requires every row to be a plain object; invalid row at index ${index}.`), { code: "invalid-value", optionPath: `values[${index}]` });
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
6
18
|
export function validateGeneratedItemLimit(
|
|
7
19
|
value,
|
|
8
20
|
label,
|
|
9
21
|
maximum = MAX_GENERATED_ITEMS
|
|
10
22
|
) {
|
|
11
23
|
if (value > maximum) {
|
|
12
|
-
throw new RangeError(`${label} must not exceed ${maximum}.`);
|
|
24
|
+
throw annotateError(new RangeError(`${label} must not exceed ${maximum}.`), { code: "resource-limit", optionPath: label, limit: maximum, actual: value });
|
|
13
25
|
}
|
|
14
26
|
return value;
|
|
15
27
|
}
|
|
@@ -21,7 +33,7 @@ export function validateWorkLimit(value, label) {
|
|
|
21
33
|
export function validateKeys(value, supported, label) {
|
|
22
34
|
for (const key of Object.keys(value)) {
|
|
23
35
|
if (!supported.includes(key)) {
|
|
24
|
-
throw new Error(`Unknown ${label} option "${key}".`);
|
|
36
|
+
throw annotateError(new Error(`Unknown ${label} option "${key}".`), { code: "invalid-option", optionPath: key });
|
|
25
37
|
}
|
|
26
38
|
}
|
|
27
39
|
}
|
|
@@ -33,18 +45,18 @@ export function validateOptionObject(value, supported, label, {
|
|
|
33
45
|
emptyError = TypeError
|
|
34
46
|
} = {}) {
|
|
35
47
|
if (!isPlainObject(value)) {
|
|
36
|
-
throw new TypeError(plainObjectMessage);
|
|
48
|
+
throw annotateError(new TypeError(plainObjectMessage), { code: "invalid-option" });
|
|
37
49
|
}
|
|
38
50
|
if (supported !== undefined) validateKeys(value, supported, label);
|
|
39
51
|
if (!allowEmpty && Object.keys(value).length === 0) {
|
|
40
|
-
throw new emptyError(emptyMessage);
|
|
52
|
+
throw annotateError(new emptyError(emptyMessage), { code: "invalid-option" });
|
|
41
53
|
}
|
|
42
54
|
return value;
|
|
43
55
|
}
|
|
44
56
|
|
|
45
57
|
export function noOptions(args, operation) {
|
|
46
58
|
if (!isPlainObject(args) || Object.keys(args).length > 0) {
|
|
47
|
-
throw new Error(`${operation} does not accept options.`);
|
|
59
|
+
throw annotateError(new Error(`${operation} does not accept options.`), { code: "invalid-option", operation });
|
|
48
60
|
}
|
|
49
61
|
}
|
|
50
62
|
|
|
@@ -59,30 +71,30 @@ export function sameOrderedValues(left, right) {
|
|
|
59
71
|
|
|
60
72
|
export function validateNonEmptyString(value, label) {
|
|
61
73
|
if (typeof value !== "string" || value.length === 0) {
|
|
62
|
-
throw new TypeError(`${label} must be a non-empty string.`);
|
|
74
|
+
throw annotateError(new TypeError(`${label} must be a non-empty string.`), { code: "invalid-value", optionPath: label });
|
|
63
75
|
}
|
|
64
76
|
return value;
|
|
65
77
|
}
|
|
66
78
|
|
|
67
79
|
export function validateUnitInterval(value, label) {
|
|
68
80
|
if (!Number.isFinite(value) || value < 0 || value > 1) {
|
|
69
|
-
throw new RangeError(
|
|
81
|
+
throw annotateError(new RangeError(
|
|
70
82
|
`${label} must be between 0 and 1 (values from 0 to 1).`
|
|
71
|
-
);
|
|
83
|
+
), { code: "invalid-value", optionPath: label });
|
|
72
84
|
}
|
|
73
85
|
return value;
|
|
74
86
|
}
|
|
75
87
|
|
|
76
88
|
export function validatePositiveFinite(value, label) {
|
|
77
89
|
if (!Number.isFinite(value) || value <= 0) {
|
|
78
|
-
throw new RangeError(`${label} must be a positive finite number.`);
|
|
90
|
+
throw annotateError(new RangeError(`${label} must be a positive finite number.`), { code: "invalid-value", optionPath: label });
|
|
79
91
|
}
|
|
80
92
|
return value;
|
|
81
93
|
}
|
|
82
94
|
|
|
83
95
|
export function validateNonNegativeFinite(value, label) {
|
|
84
96
|
if (!Number.isFinite(value) || value < 0) {
|
|
85
|
-
throw new RangeError(`${label} must be a non-negative finite number.`);
|
|
97
|
+
throw annotateError(new RangeError(`${label} must be a non-negative finite number.`), { code: "invalid-value", optionPath: label });
|
|
86
98
|
}
|
|
87
99
|
return value;
|
|
88
100
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { getErrorDetails } from "./core/diagnostics.js";
|
package/src/grammar/bin.js
CHANGED
|
@@ -15,7 +15,7 @@ import {
|
|
|
15
15
|
|
|
16
16
|
const TRANSFORM_KEYS = Object.freeze([
|
|
17
17
|
"type", "field", "bin", "extent", "nice", "zero", "includeEmpty",
|
|
18
|
-
"members", "as", "weight", "resolved"
|
|
18
|
+
"members", "as", "weight", "missing", "resolved"
|
|
19
19
|
]);
|
|
20
20
|
const AS_KEYS = Object.freeze(["lower", "upper", "count", "members"]);
|
|
21
21
|
const RESOLVED_KEYS = Object.freeze(["domain", "step", "boundaries"]);
|
|
@@ -59,7 +59,8 @@ export function normalizeBinTransform({
|
|
|
59
59
|
includeEmpty,
|
|
60
60
|
members,
|
|
61
61
|
as,
|
|
62
|
-
weight
|
|
62
|
+
weight,
|
|
63
|
+
missing
|
|
63
64
|
} = {}) {
|
|
64
65
|
const selected = [maxBins, step, boundaries].filter(value => value !== undefined);
|
|
65
66
|
if (selected.length > 1) {
|
|
@@ -83,7 +84,8 @@ export function normalizeBinTransform({
|
|
|
83
84
|
as: normalizeAs(field, as, resolvedMembers),
|
|
84
85
|
...(weight === undefined
|
|
85
86
|
? {}
|
|
86
|
-
: { weight: normalizeStatisticalWeight(weight, "Bin weight") })
|
|
87
|
+
: { weight: normalizeStatisticalWeight(weight, "Bin weight") }),
|
|
88
|
+
...(missing === undefined ? {} : { missing })
|
|
87
89
|
};
|
|
88
90
|
validateBinTransform(transform);
|
|
89
91
|
return cloneAndFreeze(transform);
|
|
@@ -101,6 +103,9 @@ export function validateBinTransform(transform) {
|
|
|
101
103
|
if (transform.weight !== undefined) {
|
|
102
104
|
normalizeStatisticalWeight(transform.weight, "Bin weight");
|
|
103
105
|
}
|
|
106
|
+
if (transform.missing !== undefined && !["error", "drop"].includes(transform.missing)) {
|
|
107
|
+
throw new Error('Bin missing must be "error" or "drop".');
|
|
108
|
+
}
|
|
104
109
|
normalizeHistogramBin(transform.bin);
|
|
105
110
|
if (
|
|
106
111
|
transform.extent !== "auto" &&
|
|
@@ -142,22 +147,31 @@ export function validateBinTransform(transform) {
|
|
|
142
147
|
return transform;
|
|
143
148
|
}
|
|
144
149
|
|
|
145
|
-
function readValues(rows, field) {
|
|
146
|
-
|
|
150
|
+
function readValues(rows, field, missing) {
|
|
151
|
+
const eligible = [];
|
|
152
|
+
let excluded = 0;
|
|
153
|
+
rows.forEach((row, index) => {
|
|
147
154
|
const value = row[field];
|
|
155
|
+
if (value === null || value === undefined) {
|
|
156
|
+
if (missing === "drop") { excluded += 1; return; }
|
|
157
|
+
throw new TypeError(`Bin field "${field}" must contain a finite number at row ${index}.`);
|
|
158
|
+
}
|
|
148
159
|
if (!Number.isFinite(value)) {
|
|
149
160
|
throw new TypeError(`Bin field "${field}" must contain a finite number at row ${index}.`);
|
|
150
161
|
}
|
|
151
|
-
|
|
162
|
+
eligible.push({ row, index });
|
|
152
163
|
});
|
|
164
|
+
return { eligible, excluded };
|
|
153
165
|
}
|
|
154
166
|
|
|
155
167
|
export function deriveBinRows(rows, transform) {
|
|
156
168
|
validateBinTransform(transform);
|
|
157
|
-
const
|
|
169
|
+
const selected = readValues(rows, transform.field, transform.missing);
|
|
170
|
+
const eligibleRows = selected.eligible.map(entry => entry.row);
|
|
171
|
+
const values = eligibleRows.map(row => row[transform.field]);
|
|
158
172
|
const weights = transform.weight === undefined
|
|
159
173
|
? undefined
|
|
160
|
-
: readStatisticalWeights(
|
|
174
|
+
: readStatisticalWeights(eligibleRows, transform.weight, "Bin");
|
|
161
175
|
if (weights !== undefined) {
|
|
162
176
|
validateWeightedNumericFields(weights.entries, [transform.field], "Bin");
|
|
163
177
|
}
|
|
@@ -188,7 +202,7 @@ export function deriveBinRows(rows, transform) {
|
|
|
188
202
|
});
|
|
189
203
|
const members = resolved.boundaries.slice(0, -1).map(() => []);
|
|
190
204
|
const entries = weightSummary === undefined
|
|
191
|
-
?
|
|
205
|
+
? selected.eligible
|
|
192
206
|
: weightSummary.positive;
|
|
193
207
|
entries.forEach(entry => {
|
|
194
208
|
const value = entry.row[transform.field];
|
|
@@ -217,7 +231,22 @@ export function deriveBinRows(rows, transform) {
|
|
|
217
231
|
domain: resolved.domain,
|
|
218
232
|
...(resolved.step === undefined ? {} : { step: resolved.step }),
|
|
219
233
|
boundaries: resolved.boundaries
|
|
220
|
-
}
|
|
234
|
+
},
|
|
235
|
+
...(transform.missing === undefined ? {} : {
|
|
236
|
+
report: {
|
|
237
|
+
version: 1,
|
|
238
|
+
owner: { kind: "data", id: "pending" },
|
|
239
|
+
inputs: [],
|
|
240
|
+
units: [{
|
|
241
|
+
role: transform.as.count,
|
|
242
|
+
group: {},
|
|
243
|
+
inputRows: rows.length,
|
|
244
|
+
usedRows: eligibleRows.length,
|
|
245
|
+
excludedRows: selected.excluded,
|
|
246
|
+
excludedByReason: selected.excluded === 0 ? {} : { "missing-value": selected.excluded }
|
|
247
|
+
}]
|
|
248
|
+
}
|
|
249
|
+
})
|
|
221
250
|
};
|
|
222
251
|
}
|
|
223
252
|
|