ggaction 0.0.1
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 +27 -0
- package/LICENSE +21 -0
- package/README.md +108 -0
- package/package.json +84 -0
- package/src/AGENTS.md +119 -0
- package/src/ChartProgram.js +10 -0
- package/src/actions/boxPlots/components.js +88 -0
- package/src/actions/boxPlots/create.js +84 -0
- package/src/actions/boxPlots/index.js +10 -0
- package/src/actions/boxPlots/materialize.js +183 -0
- package/src/actions/boxPlots/options.js +147 -0
- package/src/actions/boxPlots/resolve.js +38 -0
- package/src/actions/canvas/actions.js +144 -0
- package/src/actions/canvas/index.js +6 -0
- package/src/actions/coordinates/actions.js +85 -0
- package/src/actions/coordinates/index.js +5 -0
- package/src/actions/data/box.js +40 -0
- package/src/actions/data/create.js +33 -0
- package/src/actions/data/density.js +62 -0
- package/src/actions/data/derived.js +52 -0
- package/src/actions/data/filter.js +168 -0
- package/src/actions/data/index.js +32 -0
- package/src/actions/data/interval.js +69 -0
- package/src/actions/data/regression.js +71 -0
- package/src/actions/data/shared.js +25 -0
- package/src/actions/encodings/appearance.js +243 -0
- package/src/actions/encodings/barWidth.js +60 -0
- package/src/actions/encodings/color.js +329 -0
- package/src/actions/encodings/density.js +225 -0
- package/src/actions/encodings/histogram.js +50 -0
- package/src/actions/encodings/index.js +23 -0
- package/src/actions/encodings/offset.js +104 -0
- package/src/actions/encodings/position/policies/area.js +24 -0
- package/src/actions/encodings/position/policies/bar.js +110 -0
- package/src/actions/encodings/position/policies/common.js +17 -0
- package/src/actions/encodings/position/policies/index.js +25 -0
- package/src/actions/encodings/position/policies/line.js +74 -0
- package/src/actions/encodings/position/policies/point.js +19 -0
- package/src/actions/encodings/position/policies/rule.js +17 -0
- package/src/actions/encodings/position/resolve.js +170 -0
- package/src/actions/encodings/position.js +176 -0
- package/src/actions/encodings/ranged.js +276 -0
- package/src/actions/encodings/ruleAppearance.js +47 -0
- package/src/actions/encodings/shared.js +164 -0
- package/src/actions/encodings/strokeDash.js +145 -0
- package/src/actions/errorBands/components.js +81 -0
- package/src/actions/errorBands/create.js +159 -0
- package/src/actions/errorBands/index.js +7 -0
- package/src/actions/errorBands/resolve.js +28 -0
- package/src/actions/errorBars/components.js +62 -0
- package/src/actions/errorBars/create.js +144 -0
- package/src/actions/errorBars/index.js +7 -0
- package/src/actions/errorBars/resolve.js +25 -0
- package/src/actions/guides/axes/axes.js +204 -0
- package/src/actions/guides/axes/axis.js +131 -0
- package/src/actions/guides/axes/index.js +17 -0
- package/src/actions/guides/axes/labels.js +231 -0
- package/src/actions/guides/axes/lines.js +149 -0
- package/src/actions/guides/axes/policy.js +155 -0
- package/src/actions/guides/axes/tickGroups.js +149 -0
- package/src/actions/guides/axes/ticks.js +179 -0
- package/src/actions/guides/axes/titles.js +264 -0
- package/src/actions/guides/grids/grid.js +228 -0
- package/src/actions/guides/grids/index.js +1 -0
- package/src/actions/guides/grids/resolve.js +258 -0
- package/src/actions/guides/guides.js +138 -0
- package/src/actions/guides/index.js +11 -0
- package/src/actions/guides/legends/categorical/actions.js +329 -0
- package/src/actions/guides/legends/categorical/components.js +140 -0
- package/src/actions/guides/legends/categorical/index.js +49 -0
- package/src/actions/guides/legends/categorical/layout.js +445 -0
- package/src/actions/guides/legends/categorical/options.js +188 -0
- package/src/actions/guides/legends/categorical/recipes.js +120 -0
- package/src/actions/guides/legends/categorical/resolve.js +131 -0
- package/src/actions/guides/legends/categorical/symbols.js +280 -0
- package/src/actions/guides/legends/categorical/validation.js +27 -0
- package/src/actions/guides/legends/continuous/common.js +273 -0
- package/src/actions/guides/legends/continuous/gradient.js +363 -0
- package/src/actions/guides/legends/continuous/index.js +25 -0
- package/src/actions/guides/legends/continuous/interval.js +234 -0
- package/src/actions/guides/legends/continuous/opacity.js +382 -0
- package/src/actions/guides/legends/edit.js +314 -0
- package/src/actions/guides/legends/index.js +9 -0
- package/src/actions/guides/legends/size.js +220 -0
- package/src/actions/guides/tickValues.js +84 -0
- package/src/actions/index.js +31 -0
- package/src/actions/intervals/resolve.js +352 -0
- package/src/actions/marks/area.js +369 -0
- package/src/actions/marks/bar/create.js +46 -0
- package/src/actions/marks/bar/edit.js +90 -0
- package/src/actions/marks/bar/index.js +9 -0
- package/src/actions/marks/bar/materialize.js +130 -0
- package/src/actions/marks/index.js +13 -0
- package/src/actions/marks/line.js +248 -0
- package/src/actions/marks/point.js +420 -0
- package/src/actions/marks/rule.js +287 -0
- package/src/actions/marks/shared.js +112 -0
- package/src/actions/primitives/createGraphics.js +163 -0
- package/src/actions/primitives/editGraphics.js +208 -0
- package/src/actions/primitives/index.js +9 -0
- package/src/actions/primitives/semantic.js +168 -0
- package/src/actions/primitives/semanticValue.js +182 -0
- package/src/actions/regression/components.js +216 -0
- package/src/actions/regression/create.js +102 -0
- package/src/actions/regression/index.js +23 -0
- package/src/actions/regression/resolve.js +57 -0
- package/src/actions/scales/consumers.js +283 -0
- package/src/actions/scales/create.js +148 -0
- package/src/actions/scales/definitions.js +319 -0
- package/src/actions/scales/edit.js +290 -0
- package/src/actions/scales/index.js +11 -0
- package/src/actions/scales/materialize.js +466 -0
- package/src/actions/scales/quantitativeColor.js +45 -0
- package/src/actions/selection/actions.js +384 -0
- package/src/actions/selection/index.js +23 -0
- package/src/actions/titles/actions.js +235 -0
- package/src/actions/titles/index.js +19 -0
- package/src/actions/titles/resolve.js +321 -0
- package/src/core/ChartProgram.js +316 -0
- package/src/core/action.js +120 -0
- package/src/core/identifiers.js +28 -0
- package/src/core/immutable.js +94 -0
- package/src/core/specs.js +29 -0
- package/src/core/textMetrics.js +23 -0
- package/src/core/validation.js +54 -0
- package/src/core/vocabulary.js +58 -0
- package/src/extension.js +2 -0
- package/src/grammar/aggregate.js +259 -0
- package/src/grammar/areaSeries.js +217 -0
- package/src/grammar/bars/aggregate.js +98 -0
- package/src/grammar/bars/geometry.js +54 -0
- package/src/grammar/bars/policy.js +90 -0
- package/src/grammar/boxPlot.js +140 -0
- package/src/grammar/coordinates.js +25 -0
- package/src/grammar/curveCommands.js +356 -0
- package/src/grammar/density.js +296 -0
- package/src/grammar/filter.js +169 -0
- package/src/grammar/histogram.js +332 -0
- package/src/grammar/interval.js +343 -0
- package/src/grammar/lineSeries.js +197 -0
- package/src/grammar/markFilter.js +46 -0
- package/src/grammar/markSelection.js +326 -0
- package/src/grammar/palettes.js +208 -0
- package/src/grammar/pathCommands.js +86 -0
- package/src/grammar/pointShapes.js +164 -0
- package/src/grammar/positionCompatibility.js +32 -0
- package/src/grammar/regression.js +578 -0
- package/src/grammar/ruleAppearance.js +12 -0
- package/src/grammar/rules.js +90 -0
- package/src/grammar/scales/appearance.js +185 -0
- package/src/grammar/scales/color.js +349 -0
- package/src/grammar/scales/continuous.js +111 -0
- package/src/grammar/scales/definition.js +153 -0
- package/src/grammar/scales/discretized.js +159 -0
- package/src/grammar/scales/fields.js +133 -0
- package/src/grammar/scales/index.js +12 -0
- package/src/grammar/scales/mapping.js +27 -0
- package/src/grammar/scales/ordinal.js +221 -0
- package/src/grammar/scales/policies.js +50 -0
- package/src/grammar/scales/temporal.js +53 -0
- package/src/grammar/scales/transformed.js +236 -0
- package/src/grammar/scales/types.js +102 -0
- package/src/grammar/scales/validation.js +66 -0
- package/src/grammar/scales.js +1 -0
- package/src/grammar/schemas/concreteGraphic.js +73 -0
- package/src/grammar/schemas/graphic.js +131 -0
- package/src/grammar/schemas/graphicBounds.js +146 -0
- package/src/grammar/schemas/graphicTree.js +133 -0
- package/src/grammar/schemas/mark.js +4 -0
- package/src/grammar/schemas/semanticPath.js +174 -0
- package/src/grammar/seriesLayout.js +76 -0
- package/src/grammar/ticks.js +149 -0
- package/src/grammar/transforms.js +37 -0
- package/src/index.js +2 -0
- package/src/layout/canvas.js +114 -0
- package/src/layout/grid.js +17 -0
- package/src/layout/legend.js +51 -0
- package/src/layout/text.js +62 -0
- package/src/layout/title.js +107 -0
- package/src/materialization/bars/aggregate.js +184 -0
- package/src/materialization/bars/grouped.js +117 -0
- package/src/materialization/bars/histogram.js +163 -0
- package/src/materialization/bars/ranged.js +53 -0
- package/src/materialization/bars/resolve.js +56 -0
- package/src/materialization/dataProvenance.js +15 -0
- package/src/materialization/density.js +29 -0
- package/src/materialization/dependencies.js +134 -0
- package/src/materialization/encodings.js +50 -0
- package/src/materialization/graphicHierarchy.js +104 -0
- package/src/materialization/legends.js +20 -0
- package/src/materialization/marks.js +110 -0
- package/src/materialization/planner.js +38 -0
- package/src/materialization/selection/filter.js +40 -0
- package/src/materialization/selection/items.js +503 -0
- package/src/materialization/selection/path.js +37 -0
- package/src/materialization/selection/point.js +118 -0
- package/src/materialization/selection/policies/area.js +10 -0
- package/src/materialization/selection/policies/bar.js +10 -0
- package/src/materialization/selection/policies/index.js +40 -0
- package/src/materialization/selection/policies/line.js +10 -0
- package/src/materialization/selection/policies/point.js +10 -0
- package/src/materialization/selection/policies/rule.js +10 -0
- package/src/materialization/selection/state.js +67 -0
- package/src/materialization/selection/styles.js +183 -0
- package/src/renderers/canvas/circle.js +62 -0
- package/src/renderers/canvas/index.js +132 -0
- package/src/renderers/canvas/line.js +54 -0
- package/src/renderers/canvas/path.js +98 -0
- package/src/renderers/canvas/rect.js +57 -0
- package/src/renderers/canvas/text.js +96 -0
- package/src/renderers/canvas/validation.js +23 -0
- package/src/renderers/png.js +29 -0
- package/src/selectors/coordinates.js +13 -0
- package/src/selectors/datasets.js +13 -0
- package/src/selectors/index.js +4 -0
- package/src/selectors/layers.js +32 -0
- package/src/selectors/scales.js +21 -0
- package/src/theme/defaults.js +13 -0
- package/types/extension.d.ts +14 -0
- package/types/index.d.ts +81 -0
- package/types/png.d.ts +14 -0
- package/types/program.d.ts +1096 -0
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { cloneAndFreeze, freezeOwned, isPlainObject } from "./immutable.js";
|
|
2
|
+
|
|
3
|
+
function summarizeObject(value, ancestors = new WeakSet()) {
|
|
4
|
+
if (ancestors.has(value)) {
|
|
5
|
+
throw new TypeError("Action arguments must not contain circular references.");
|
|
6
|
+
}
|
|
7
|
+
ancestors.add(value);
|
|
8
|
+
const summary = {};
|
|
9
|
+
|
|
10
|
+
for (const [key, item] of Object.entries(value)) {
|
|
11
|
+
if (Array.isArray(item)) {
|
|
12
|
+
summary[`${key}Count`] = item.length;
|
|
13
|
+
} else if (isPlainObject(item)) {
|
|
14
|
+
summary[key] = summarizeObject(item, ancestors);
|
|
15
|
+
} else {
|
|
16
|
+
summary[key] = item;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
ancestors.delete(value);
|
|
21
|
+
return summary;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function summarizeArgs(args) {
|
|
25
|
+
if (!isPlainObject(args)) {
|
|
26
|
+
throw new TypeError("Action arguments must be a plain object.");
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return cloneAndFreeze(summarizeObject(args));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function countActionNodes(node) {
|
|
33
|
+
return node.children.reduce(
|
|
34
|
+
(count, child) => count + 1 + countActionNodes(child),
|
|
35
|
+
0
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function createActionNode({ id, op, description, args }) {
|
|
40
|
+
return freezeOwned({
|
|
41
|
+
id,
|
|
42
|
+
op,
|
|
43
|
+
description,
|
|
44
|
+
args,
|
|
45
|
+
children: freezeOwned([])
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function nodeAtPath(root, path) {
|
|
50
|
+
let node = root;
|
|
51
|
+
for (const index of path) {
|
|
52
|
+
if (!Number.isInteger(index) || index < 0 || index >= node.children.length) {
|
|
53
|
+
throw new Error(`Unknown parent action path "${path.join(".")}".`);
|
|
54
|
+
}
|
|
55
|
+
node = node.children[index];
|
|
56
|
+
}
|
|
57
|
+
return node;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function appendActionNodeAtPath(root, parentPath, actionNode) {
|
|
61
|
+
if (!Array.isArray(parentPath)) {
|
|
62
|
+
throw new TypeError("Parent action path must be an array.");
|
|
63
|
+
}
|
|
64
|
+
const parent = nodeAtPath(root, parentPath);
|
|
65
|
+
const path = [...parentPath, parent.children.length];
|
|
66
|
+
|
|
67
|
+
function append(node, depth) {
|
|
68
|
+
if (depth === parentPath.length) {
|
|
69
|
+
return freezeOwned({
|
|
70
|
+
...node,
|
|
71
|
+
children: freezeOwned([...node.children, actionNode])
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
const index = parentPath[depth];
|
|
75
|
+
const children = [...node.children];
|
|
76
|
+
children[index] = append(children[index], depth + 1);
|
|
77
|
+
return freezeOwned({ ...node, children: freezeOwned(children) });
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return { root: append(root, 0), path };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function action(metadata, implementation) {
|
|
84
|
+
if (!isPlainObject(metadata)) {
|
|
85
|
+
throw new TypeError("Action metadata must be a plain object.");
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (typeof metadata.op !== "string" || metadata.op.length === 0) {
|
|
89
|
+
throw new TypeError("Action metadata requires a non-empty op.");
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (
|
|
93
|
+
typeof metadata.description !== "string" ||
|
|
94
|
+
metadata.description.length === 0
|
|
95
|
+
) {
|
|
96
|
+
throw new TypeError("Action metadata requires a non-empty description.");
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (typeof implementation !== "function") {
|
|
100
|
+
throw new TypeError("Action implementation must be a function.");
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return function wrappedAction(args = {}) {
|
|
104
|
+
if (!isPlainObject(args)) {
|
|
105
|
+
throw new TypeError("Action arguments must be a plain object.");
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const entered = this._enterAction({
|
|
109
|
+
...metadata,
|
|
110
|
+
args: summarizeArgs(args)
|
|
111
|
+
});
|
|
112
|
+
const result = implementation.call(entered, args);
|
|
113
|
+
|
|
114
|
+
if (!(result instanceof this.constructor)) {
|
|
115
|
+
throw new TypeError(`${metadata.op} must return a ChartProgram.`);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return result._exitAction();
|
|
119
|
+
};
|
|
120
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export const USER_ID_SOURCE = "[A-Za-z0-9_-]+";
|
|
2
|
+
|
|
3
|
+
const USER_ID_PATTERN = new RegExp(`^${USER_ID_SOURCE}$`);
|
|
4
|
+
|
|
5
|
+
export function validateUserId(id, label = "ID") {
|
|
6
|
+
if (typeof id !== "string" || !USER_ID_PATTERN.test(id)) {
|
|
7
|
+
throw new TypeError(
|
|
8
|
+
`${label} must contain only letters, numbers, _ or - and must not be empty.`
|
|
9
|
+
);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
return id;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function resolveOptionalUserId(id, {
|
|
16
|
+
defaultId,
|
|
17
|
+
label = "ID",
|
|
18
|
+
operation,
|
|
19
|
+
ambiguous = false
|
|
20
|
+
}) {
|
|
21
|
+
if (id !== undefined) return validateUserId(id, label);
|
|
22
|
+
if (ambiguous) {
|
|
23
|
+
throw new Error(
|
|
24
|
+
`${operation} requires an explicit ${label.toLowerCase()} because its default is ambiguous.`
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
return validateUserId(defaultId, label);
|
|
28
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
const ownedValues = new WeakSet();
|
|
2
|
+
|
|
3
|
+
export function isPlainObject(value) {
|
|
4
|
+
if (value === null || typeof value !== "object") {
|
|
5
|
+
return false;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const prototype = Object.getPrototypeOf(value);
|
|
9
|
+
return prototype === Object.prototype || prototype === null;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function freezeOwned(value) {
|
|
13
|
+
if (value !== null && typeof value === "object") {
|
|
14
|
+
Object.freeze(value);
|
|
15
|
+
ownedValues.add(value);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return value;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function isOwned(value) {
|
|
22
|
+
return value !== null && typeof value === "object" && ownedValues.has(value);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function cloneAndFreeze(value, ancestors = new WeakSet()) {
|
|
26
|
+
if (value === null || typeof value !== "object") {
|
|
27
|
+
return value;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (ancestors.has(value)) {
|
|
31
|
+
throw new TypeError("Cannot store cyclic values in a ChartProgram.");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
ancestors.add(value);
|
|
35
|
+
|
|
36
|
+
let clone;
|
|
37
|
+
|
|
38
|
+
if (Array.isArray(value)) {
|
|
39
|
+
clone = value.map(item => cloneAndFreeze(item, ancestors));
|
|
40
|
+
} else if (isPlainObject(value)) {
|
|
41
|
+
clone = Object.fromEntries(
|
|
42
|
+
Object.entries(value).map(([key, item]) => [
|
|
43
|
+
key,
|
|
44
|
+
cloneAndFreeze(item, ancestors)
|
|
45
|
+
])
|
|
46
|
+
);
|
|
47
|
+
} else {
|
|
48
|
+
throw new TypeError("ChartProgram state only supports plain objects and arrays.");
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
ancestors.delete(value);
|
|
52
|
+
return freezeOwned(clone);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function removeOwnedPath(source, path) {
|
|
56
|
+
if (!isPlainObject(source)) {
|
|
57
|
+
throw new TypeError("Structural removal requires a plain object source.");
|
|
58
|
+
}
|
|
59
|
+
if (
|
|
60
|
+
!Array.isArray(path) ||
|
|
61
|
+
path.length === 0 ||
|
|
62
|
+
!path.every(key => typeof key === "string" && key.length > 0)
|
|
63
|
+
) {
|
|
64
|
+
throw new TypeError("Structural removal path must contain names.");
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const [key, ...rest] = path;
|
|
68
|
+
if (!Object.hasOwn(source, key)) {
|
|
69
|
+
return { value: source, removed: false };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (rest.length === 0) {
|
|
73
|
+
const { [key]: removedValue, ...remaining } = source;
|
|
74
|
+
void removedValue;
|
|
75
|
+
return { value: freezeOwned(remaining), removed: true };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const child = source[key];
|
|
79
|
+
if (!isPlainObject(child)) {
|
|
80
|
+
return { value: source, removed: false };
|
|
81
|
+
}
|
|
82
|
+
const removed = removeOwnedPath(child, rest);
|
|
83
|
+
if (!removed.removed) return { value: source, removed: false };
|
|
84
|
+
|
|
85
|
+
if (Object.keys(removed.value).length === 0) {
|
|
86
|
+
const { [key]: emptyChild, ...remaining } = source;
|
|
87
|
+
void emptyChild;
|
|
88
|
+
return { value: freezeOwned(remaining), removed: true };
|
|
89
|
+
}
|
|
90
|
+
return {
|
|
91
|
+
value: freezeOwned({ ...source, [key]: removed.value }),
|
|
92
|
+
removed: true
|
|
93
|
+
};
|
|
94
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { cloneAndFreeze } from "./immutable.js";
|
|
2
|
+
|
|
3
|
+
export function createEmptySemanticSpec() {
|
|
4
|
+
return cloneAndFreeze({
|
|
5
|
+
datasets: [],
|
|
6
|
+
layers: [],
|
|
7
|
+
scales: [],
|
|
8
|
+
coordinates: [],
|
|
9
|
+
guides: {},
|
|
10
|
+
title: {}
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function createEmptyGraphicSpec() {
|
|
15
|
+
return cloneAndFreeze({
|
|
16
|
+
objects: {},
|
|
17
|
+
order: []
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function createTraceRoot() {
|
|
22
|
+
return cloneAndFreeze({
|
|
23
|
+
id: "program",
|
|
24
|
+
op: "program",
|
|
25
|
+
description: "Program action trace root.",
|
|
26
|
+
args: {},
|
|
27
|
+
children: []
|
|
28
|
+
});
|
|
29
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
function codePointWidth(codePoint) {
|
|
2
|
+
if (/\s/u.test(codePoint)) return 0.28;
|
|
3
|
+
if (/[iIl1.,:;!'|]/u.test(codePoint)) return 0.27;
|
|
4
|
+
if (/[mwMW@#%&]/u.test(codePoint)) return 0.82;
|
|
5
|
+
if (/[A-Z]/u.test(codePoint)) return 0.61;
|
|
6
|
+
if (/[0-9]/u.test(codePoint)) return 0.53;
|
|
7
|
+
if (/[-_]/u.test(codePoint)) return 0.34;
|
|
8
|
+
if (codePoint.codePointAt(0) > 0x7f) return 1;
|
|
9
|
+
return 0.47;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function measureTextWidth(text, { fontSize } = {}) {
|
|
13
|
+
if (typeof text !== "string") {
|
|
14
|
+
throw new TypeError("Text measurement requires a string.");
|
|
15
|
+
}
|
|
16
|
+
if (!Number.isFinite(fontSize) || fontSize <= 0) {
|
|
17
|
+
throw new RangeError("Text measurement requires a positive fontSize.");
|
|
18
|
+
}
|
|
19
|
+
return [...text].reduce(
|
|
20
|
+
(width, codePoint) => width + codePointWidth(codePoint) * fontSize,
|
|
21
|
+
0
|
|
22
|
+
);
|
|
23
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { isPlainObject } from "./immutable.js";
|
|
2
|
+
|
|
3
|
+
export function validateKeys(value, supported, label) {
|
|
4
|
+
for (const key of Object.keys(value)) {
|
|
5
|
+
if (!supported.includes(key)) {
|
|
6
|
+
throw new Error(`Unknown ${label} option "${key}".`);
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function noOptions(args, operation) {
|
|
12
|
+
if (!isPlainObject(args) || Object.keys(args).length > 0) {
|
|
13
|
+
throw new Error(`${operation} does not accept options.`);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function sameOrderedValues(left, right) {
|
|
18
|
+
return (
|
|
19
|
+
Array.isArray(left) &&
|
|
20
|
+
Array.isArray(right) &&
|
|
21
|
+
left.length === right.length &&
|
|
22
|
+
left.every((value, index) => value === right[index])
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function validateNonEmptyString(value, label) {
|
|
27
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
28
|
+
throw new TypeError(`${label} must be a non-empty string.`);
|
|
29
|
+
}
|
|
30
|
+
return value;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function validateUnitInterval(value, label) {
|
|
34
|
+
if (!Number.isFinite(value) || value < 0 || value > 1) {
|
|
35
|
+
throw new RangeError(
|
|
36
|
+
`${label} must be between 0 and 1 (values from 0 to 1).`
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
return value;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function validatePositiveFinite(value, label) {
|
|
43
|
+
if (!Number.isFinite(value) || value <= 0) {
|
|
44
|
+
throw new RangeError(`${label} must be a positive finite number.`);
|
|
45
|
+
}
|
|
46
|
+
return value;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function validateNonNegativeFinite(value, label) {
|
|
50
|
+
if (!Number.isFinite(value) || value < 0) {
|
|
51
|
+
throw new RangeError(`${label} must be a non-negative finite number.`);
|
|
52
|
+
}
|
|
53
|
+
return value;
|
|
54
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
export const MARK_TYPES = Object.freeze([
|
|
2
|
+
"point",
|
|
3
|
+
"line",
|
|
4
|
+
"bar",
|
|
5
|
+
"area",
|
|
6
|
+
"rule"
|
|
7
|
+
]);
|
|
8
|
+
|
|
9
|
+
export const ENCODING_CHANNELS = Object.freeze([
|
|
10
|
+
"x",
|
|
11
|
+
"y",
|
|
12
|
+
"x2",
|
|
13
|
+
"y2",
|
|
14
|
+
"xOffset",
|
|
15
|
+
"theta",
|
|
16
|
+
"radius",
|
|
17
|
+
"color",
|
|
18
|
+
"strokeDash",
|
|
19
|
+
"size",
|
|
20
|
+
"shape",
|
|
21
|
+
"group",
|
|
22
|
+
"opacity"
|
|
23
|
+
]);
|
|
24
|
+
|
|
25
|
+
export const SCALED_ENCODING_CHANNELS = Object.freeze(
|
|
26
|
+
ENCODING_CHANNELS.filter(channel => channel !== "group")
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
export const POSITION_CHANNELS = Object.freeze(["x", "y"]);
|
|
30
|
+
|
|
31
|
+
export const COLOR_LAYOUTS = Object.freeze([
|
|
32
|
+
"stack",
|
|
33
|
+
"fill",
|
|
34
|
+
"group",
|
|
35
|
+
"overlay",
|
|
36
|
+
"diverging"
|
|
37
|
+
]);
|
|
38
|
+
|
|
39
|
+
export const STACK_MODES = Object.freeze(["zero", "normalize"]);
|
|
40
|
+
|
|
41
|
+
export const CATEGORICAL_LEGEND_CHANNELS = Object.freeze([
|
|
42
|
+
"color",
|
|
43
|
+
"strokeDash",
|
|
44
|
+
"shape"
|
|
45
|
+
]);
|
|
46
|
+
|
|
47
|
+
export const LEGEND_CONFIG_KINDS = Object.freeze([
|
|
48
|
+
"series",
|
|
49
|
+
"color",
|
|
50
|
+
"size",
|
|
51
|
+
"gradient",
|
|
52
|
+
"interval",
|
|
53
|
+
"opacity"
|
|
54
|
+
]);
|
|
55
|
+
|
|
56
|
+
export function includesVocabulary(vocabulary, value) {
|
|
57
|
+
return vocabulary.includes(value);
|
|
58
|
+
}
|
package/src/extension.js
ADDED
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
import { isPlainObject } from "../core/immutable.js";
|
|
2
|
+
|
|
3
|
+
export const SCALAR_AGGREGATE_OPERATIONS = Object.freeze([
|
|
4
|
+
"count", "sum", "mean", "median", "min", "max",
|
|
5
|
+
"distinct", "valid", "missing",
|
|
6
|
+
"variance", "varianceP", "stdev", "stdevP", "stderr",
|
|
7
|
+
"q1", "q3", "ciLower", "ciUpper"
|
|
8
|
+
]);
|
|
9
|
+
|
|
10
|
+
const SCALAR_OPERATIONS = new Set(SCALAR_AGGREGATE_OPERATIONS);
|
|
11
|
+
const NOMINAL_OPERATIONS = new Set(["count", "distinct", "valid", "missing"]);
|
|
12
|
+
const PARAMETERIZED_OPERATIONS = new Set(["quantile", "first", "last"]);
|
|
13
|
+
|
|
14
|
+
function nonEmptyString(value, label) {
|
|
15
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
16
|
+
throw new TypeError(`${label} must be a non-empty string.`);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function isScalarAggregate(value) {
|
|
21
|
+
return typeof value === "string" && SCALAR_OPERATIONS.has(value);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function isParameterizedAggregate(value) {
|
|
25
|
+
return isPlainObject(value) && PARAMETERIZED_OPERATIONS.has(value.op);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function isAggregate(value) {
|
|
29
|
+
return isScalarAggregate(value) || isParameterizedAggregate(value);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function validateAggregate(value) {
|
|
33
|
+
if (typeof value === "string") {
|
|
34
|
+
if (!SCALAR_OPERATIONS.has(value)) {
|
|
35
|
+
throw new Error(`Unsupported aggregate "${value}".`);
|
|
36
|
+
}
|
|
37
|
+
return value;
|
|
38
|
+
}
|
|
39
|
+
if (!isPlainObject(value)) {
|
|
40
|
+
throw new TypeError("Aggregate must be a supported operation or parameter object.");
|
|
41
|
+
}
|
|
42
|
+
if (value.op === "quantile") {
|
|
43
|
+
const unknown = Object.keys(value).find(
|
|
44
|
+
key => !["op", "probability"].includes(key)
|
|
45
|
+
);
|
|
46
|
+
if (unknown !== undefined) {
|
|
47
|
+
throw new Error(`Unknown quantile aggregate property "${unknown}".`);
|
|
48
|
+
}
|
|
49
|
+
if (
|
|
50
|
+
!Number.isFinite(value.probability) ||
|
|
51
|
+
value.probability < 0 ||
|
|
52
|
+
value.probability > 1
|
|
53
|
+
) {
|
|
54
|
+
throw new RangeError("Quantile probability must be between 0 and 1.");
|
|
55
|
+
}
|
|
56
|
+
return { op: "quantile", probability: value.probability };
|
|
57
|
+
}
|
|
58
|
+
if (value.op === "first" || value.op === "last") {
|
|
59
|
+
const unknown = Object.keys(value).find(
|
|
60
|
+
key => !["op", "orderBy", "order"].includes(key)
|
|
61
|
+
);
|
|
62
|
+
if (unknown !== undefined) {
|
|
63
|
+
throw new Error(`Unknown ordered aggregate property "${unknown}".`);
|
|
64
|
+
}
|
|
65
|
+
nonEmptyString(value.orderBy, "Ordered aggregate orderBy");
|
|
66
|
+
if (
|
|
67
|
+
value.order !== undefined &&
|
|
68
|
+
!["ascending", "descending"].includes(value.order)
|
|
69
|
+
) {
|
|
70
|
+
throw new Error(`Unsupported ordered aggregate order "${value.order}".`);
|
|
71
|
+
}
|
|
72
|
+
return {
|
|
73
|
+
op: value.op,
|
|
74
|
+
orderBy: value.orderBy,
|
|
75
|
+
order: value.order ?? "ascending"
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
throw new Error(`Unsupported aggregate "${value.op}".`);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function validateAggregateFieldType(operation, fieldType) {
|
|
82
|
+
const aggregate = validateAggregate(operation);
|
|
83
|
+
if (isScalarAggregate(aggregate)) {
|
|
84
|
+
return validateScalarAggregateFieldType(aggregate, fieldType);
|
|
85
|
+
}
|
|
86
|
+
if (fieldType === "quantitative") return fieldType;
|
|
87
|
+
throw new Error(
|
|
88
|
+
`Aggregate "${aggregate.op}" does not support field type "${fieldType}".`
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function validateScalarAggregateFieldType(operation, fieldType) {
|
|
93
|
+
validateAggregate(operation);
|
|
94
|
+
if (!isScalarAggregate(operation)) {
|
|
95
|
+
throw new Error("Scalar aggregate calculation requires a scalar operation.");
|
|
96
|
+
}
|
|
97
|
+
if (fieldType === "quantitative") return fieldType;
|
|
98
|
+
if (fieldType === "nominal" && NOMINAL_OPERATIONS.has(operation)) {
|
|
99
|
+
return fieldType;
|
|
100
|
+
}
|
|
101
|
+
throw new Error(
|
|
102
|
+
`Aggregate "${operation}" does not support field type "${fieldType}".`
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function isMissing(value) {
|
|
107
|
+
return value === null || value === undefined ||
|
|
108
|
+
(typeof value === "number" && Number.isNaN(value));
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function isNominal(value) {
|
|
112
|
+
return typeof value === "string" || typeof value === "boolean" ||
|
|
113
|
+
(typeof value === "number" && Number.isFinite(value));
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function validateAggregateFieldValues(rows, field, fieldType) {
|
|
117
|
+
if (!Array.isArray(rows)) {
|
|
118
|
+
throw new TypeError("Aggregate rows must be an array.");
|
|
119
|
+
}
|
|
120
|
+
nonEmptyString(field, "Aggregate field");
|
|
121
|
+
for (const row of rows) {
|
|
122
|
+
const value = row[field];
|
|
123
|
+
if (isMissing(value)) continue;
|
|
124
|
+
if (fieldType === "quantitative" && typeof value !== "number") {
|
|
125
|
+
throw new TypeError(`Aggregate field "${field}" must contain numeric or missing values.`);
|
|
126
|
+
}
|
|
127
|
+
if (fieldType === "nominal" && !isNominal(value)) {
|
|
128
|
+
throw new TypeError(`Aggregate field "${field}" must contain nominal or missing values.`);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function finiteValues(values) {
|
|
134
|
+
return values.filter(value => typeof value === "number" && Number.isFinite(value));
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function quantile(values, probability) {
|
|
138
|
+
const ordered = [...values].sort((left, right) => left - right);
|
|
139
|
+
const position = (ordered.length - 1) * probability;
|
|
140
|
+
const lower = Math.floor(position);
|
|
141
|
+
const upper = Math.ceil(position);
|
|
142
|
+
return ordered[lower] +
|
|
143
|
+
(ordered[upper] - ordered[lower]) * (position - lower);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function comparableOrderKey(value) {
|
|
147
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
148
|
+
return { type: "number", value };
|
|
149
|
+
}
|
|
150
|
+
if (typeof value === "string") return { type: "string", value };
|
|
151
|
+
if (typeof value === "boolean") return { type: "boolean", value: Number(value) };
|
|
152
|
+
return undefined;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function compareOrderKeys(left, right) {
|
|
156
|
+
if (left.value < right.value) return -1;
|
|
157
|
+
if (left.value > right.value) return 1;
|
|
158
|
+
return 0;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function aggregateOrderedRows(rows, field, aggregate) {
|
|
162
|
+
const candidates = rows.flatMap((row, sourceIndex) => {
|
|
163
|
+
const key = comparableOrderKey(row[aggregate.orderBy]);
|
|
164
|
+
const value = row[field];
|
|
165
|
+
return key === undefined || !Number.isFinite(value)
|
|
166
|
+
? []
|
|
167
|
+
: [{ key, value, sourceIndex }];
|
|
168
|
+
});
|
|
169
|
+
if (candidates.length === 0) return undefined;
|
|
170
|
+
const keyTypes = new Set(candidates.map(candidate => candidate.key.type));
|
|
171
|
+
if (keyTypes.size !== 1) return undefined;
|
|
172
|
+
const direction = aggregate.order === "descending" ? -1 : 1;
|
|
173
|
+
candidates.sort((left, right) =>
|
|
174
|
+
direction * compareOrderKeys(left.key, right.key) ||
|
|
175
|
+
left.sourceIndex - right.sourceIndex
|
|
176
|
+
);
|
|
177
|
+
return aggregate.op === "first"
|
|
178
|
+
? candidates[0].value
|
|
179
|
+
: candidates.at(-1).value;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function moments(values) {
|
|
183
|
+
const mean = values.reduce((sum, value) => sum + value, 0) / values.length;
|
|
184
|
+
const squared = values.reduce(
|
|
185
|
+
(sum, value) => sum + (value - mean) ** 2,
|
|
186
|
+
0
|
|
187
|
+
);
|
|
188
|
+
return { mean, squared };
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export function aggregateScalarValues(values, operation) {
|
|
192
|
+
if (!Array.isArray(values)) {
|
|
193
|
+
throw new TypeError("Aggregate values must be an array.");
|
|
194
|
+
}
|
|
195
|
+
validateAggregate(operation);
|
|
196
|
+
if (!isScalarAggregate(operation)) {
|
|
197
|
+
throw new Error("Scalar aggregate calculation requires a scalar operation.");
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (operation === "count") return values.length;
|
|
201
|
+
const valid = values.filter(value => !isMissing(value));
|
|
202
|
+
if (operation === "valid") return valid.length;
|
|
203
|
+
if (operation === "missing") return values.length - valid.length;
|
|
204
|
+
if (operation === "distinct") return new Set(valid).size;
|
|
205
|
+
|
|
206
|
+
const finite = finiteValues(values);
|
|
207
|
+
if (finite.length === 0) return undefined;
|
|
208
|
+
if (operation === "sum") {
|
|
209
|
+
return finite.reduce((sum, value) => sum + value, 0);
|
|
210
|
+
}
|
|
211
|
+
if (operation === "min") return Math.min(...finite);
|
|
212
|
+
if (operation === "max") return Math.max(...finite);
|
|
213
|
+
if (operation === "median") return quantile(finite, 0.5);
|
|
214
|
+
if (operation === "q1") return quantile(finite, 0.25);
|
|
215
|
+
if (operation === "q3") return quantile(finite, 0.75);
|
|
216
|
+
|
|
217
|
+
const { mean, squared } = moments(finite);
|
|
218
|
+
if (operation === "mean") return mean;
|
|
219
|
+
if (operation === "varianceP") return squared / finite.length;
|
|
220
|
+
if (operation === "stdevP") return Math.sqrt(squared / finite.length);
|
|
221
|
+
if (finite.length < 2) return undefined;
|
|
222
|
+
|
|
223
|
+
const variance = squared / (finite.length - 1);
|
|
224
|
+
if (operation === "variance") return variance;
|
|
225
|
+
if (operation === "stdev") return Math.sqrt(variance);
|
|
226
|
+
const stderr = Math.sqrt(variance) / Math.sqrt(finite.length);
|
|
227
|
+
if (operation === "stderr") return stderr;
|
|
228
|
+
if (operation === "ciLower") return mean - 1.96 * stderr;
|
|
229
|
+
if (operation === "ciUpper") return mean + 1.96 * stderr;
|
|
230
|
+
throw new Error(`Unsupported scalar aggregate "${operation}".`);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
export function aggregateRows(rows, field, operation) {
|
|
234
|
+
if (!Array.isArray(rows)) {
|
|
235
|
+
throw new TypeError("Aggregate rows must be an array.");
|
|
236
|
+
}
|
|
237
|
+
nonEmptyString(field, "Aggregate field");
|
|
238
|
+
const aggregate = validateAggregate(operation);
|
|
239
|
+
if (isScalarAggregate(aggregate)) {
|
|
240
|
+
return aggregateScalarValues(rows.map(row => row[field]), aggregate);
|
|
241
|
+
}
|
|
242
|
+
if (aggregate.op === "quantile") {
|
|
243
|
+
const values = finiteValues(rows.map(row => row[field]));
|
|
244
|
+
return values.length === 0
|
|
245
|
+
? undefined
|
|
246
|
+
: quantile(values, aggregate.probability);
|
|
247
|
+
}
|
|
248
|
+
return aggregateOrderedRows(rows, field, aggregate);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
export function formatAggregateTitle(operation, field) {
|
|
252
|
+
const aggregate = validateAggregate(operation);
|
|
253
|
+
nonEmptyString(field, "Aggregate field");
|
|
254
|
+
if (isScalarAggregate(aggregate)) return `${aggregate}(${field})`;
|
|
255
|
+
if (aggregate.op === "quantile") {
|
|
256
|
+
return `quantile(${field}, ${aggregate.probability})`;
|
|
257
|
+
}
|
|
258
|
+
return `${aggregate.op}(${field}, ${aggregate.orderBy} ${aggregate.order})`;
|
|
259
|
+
}
|