@genome-spy/core 0.43.3 → 0.45.0
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/dist/bundle/index.es.js +5231 -4324
- package/dist/bundle/index.js +197 -85
- package/dist/schema.json +723 -104
- package/dist/src/data/collector.d.ts.map +1 -1
- package/dist/src/data/collector.js +4 -2
- package/dist/src/data/flowOptimizer.test.js +12 -3
- package/dist/src/data/sources/dataUtils.d.ts.map +1 -1
- package/dist/src/data/sources/dataUtils.js +3 -1
- package/dist/src/data/sources/lazy/axisTickSource.d.ts +1 -1
- package/dist/src/data/sources/lazy/axisTickSource.d.ts.map +1 -1
- package/dist/src/data/sources/lazy/axisTickSource.js +2 -2
- package/dist/src/data/sources/lazy/bigBedSource.d.ts +1 -1
- package/dist/src/data/sources/lazy/bigBedSource.d.ts.map +1 -1
- package/dist/src/data/sources/lazy/bigBedSource.js +52 -20
- package/dist/src/data/sources/lazy/bigWigSource.d.ts +6 -1
- package/dist/src/data/sources/lazy/bigWigSource.d.ts.map +1 -1
- package/dist/src/data/sources/lazy/bigWigSource.js +33 -9
- package/dist/src/data/sources/lazy/singleAxisLazySource.d.ts +1 -1
- package/dist/src/data/sources/lazy/singleAxisLazySource.d.ts.map +1 -1
- package/dist/src/data/sources/lazy/singleAxisLazySource.js +1 -3
- package/dist/src/data/sources/lazy/singleAxisWindowedSource.d.ts +13 -14
- package/dist/src/data/sources/lazy/singleAxisWindowedSource.d.ts.map +1 -1
- package/dist/src/data/sources/lazy/singleAxisWindowedSource.js +70 -48
- package/dist/src/data/sources/sequenceSource.d.ts.map +1 -1
- package/dist/src/data/sources/sequenceSource.js +14 -5
- package/dist/src/data/sources/sequenceSource.test.js +23 -5
- package/dist/src/data/sources/urlSource.d.ts.map +1 -1
- package/dist/src/data/sources/urlSource.js +15 -2
- package/dist/src/data/transforms/aggregate.d.ts.map +1 -1
- package/dist/src/data/transforms/aggregate.js +5 -2
- package/dist/src/data/transforms/filterScoredLabels.js +1 -1
- package/dist/src/encoder/encoder.d.ts +2 -4
- package/dist/src/encoder/encoder.d.ts.map +1 -1
- package/dist/src/encoder/encoder.js +20 -10
- package/dist/src/encoder/encoder.test.js +3 -0
- package/dist/src/genomeSpy.d.ts +8 -5
- package/dist/src/genomeSpy.d.ts.map +1 -1
- package/dist/src/genomeSpy.js +121 -42
- package/dist/src/gl/glslScaleGenerator.d.ts +23 -3
- package/dist/src/gl/glslScaleGenerator.d.ts.map +1 -1
- package/dist/src/gl/glslScaleGenerator.js +137 -42
- package/dist/src/gl/webGLHelper.d.ts.map +1 -1
- package/dist/src/gl/webGLHelper.js +5 -7
- package/dist/src/index.d.ts +1 -1
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +1 -1
- package/dist/src/marks/link.common.glsl.js +2 -0
- package/dist/src/marks/link.d.ts.map +1 -1
- package/dist/src/marks/link.js +19 -9
- package/dist/src/marks/link.vertex.glsl.js +1 -1
- package/dist/src/marks/mark.d.ts +25 -20
- package/dist/src/marks/mark.d.ts.map +1 -1
- package/dist/src/marks/mark.js +234 -129
- package/dist/src/marks/point.common.glsl.js +1 -1
- package/dist/src/marks/point.d.ts +1 -4
- package/dist/src/marks/point.d.ts.map +1 -1
- package/dist/src/marks/point.js +31 -23
- package/dist/src/marks/point.vertex.glsl.js +1 -1
- package/dist/src/marks/rect.common.glsl.js +2 -0
- package/dist/src/marks/rect.d.ts.map +1 -1
- package/dist/src/marks/rect.js +12 -12
- package/dist/src/marks/rect.vertex.glsl.js +1 -1
- package/dist/src/marks/rule.common.glsl.js +1 -1
- package/dist/src/marks/rule.js +2 -2
- package/dist/src/marks/text.common.glsl.js +1 -1
- package/dist/src/marks/text.d.ts.map +1 -1
- package/dist/src/marks/text.js +17 -9
- package/dist/src/spec/channel.d.ts +4 -3
- package/dist/src/spec/data.d.ts +11 -10
- package/dist/src/spec/mark.d.ts +28 -46
- package/dist/src/spec/parameter.d.ts +127 -0
- package/dist/src/spec/root.d.ts +1 -0
- package/dist/src/spec/scale.d.ts +2 -1
- package/dist/src/spec/title.d.ts +5 -4
- package/dist/src/spec/view.d.ts +20 -5
- package/dist/src/styles/genome-spy.css.d.ts +1 -1
- package/dist/src/styles/genome-spy.css.d.ts.map +1 -1
- package/dist/src/styles/genome-spy.css.js +52 -5
- package/dist/src/styles/genome-spy.scss +63 -10
- package/dist/src/styles/update.sh +6 -0
- package/dist/src/tooltip/dataTooltipHandler.js +1 -1
- package/dist/src/tooltip/refseqGeneTooltipHandler.js +1 -1
- package/dist/src/tooltip/tooltipHandler.d.ts +1 -1
- package/dist/src/tooltip/tooltipHandler.d.ts.map +1 -1
- package/dist/src/tooltip/tooltipHandler.ts +1 -1
- package/dist/src/types/embedApi.d.ts +6 -0
- package/dist/src/types/scaleResolutionApi.d.ts +7 -3
- package/dist/src/types/viewContext.d.ts +2 -3
- package/dist/src/utils/debounce.d.ts +2 -2
- package/dist/src/utils/debounce.d.ts.map +1 -1
- package/dist/src/utils/debounce.js +5 -2
- package/dist/src/utils/expression.d.ts +2 -2
- package/dist/src/utils/expression.d.ts.map +1 -1
- package/dist/src/utils/expression.js +3 -3
- package/dist/src/utils/formatObject.d.ts +2 -2
- package/dist/src/utils/formatObject.d.ts.map +1 -1
- package/dist/src/utils/formatObject.js +2 -2
- package/dist/src/utils/inputBinding.d.ts +5 -0
- package/dist/src/utils/inputBinding.d.ts.map +1 -0
- package/dist/src/utils/inputBinding.js +115 -0
- package/dist/src/utils/ui/tooltip.js +1 -1
- package/dist/src/view/axisView.js +3 -3
- package/dist/src/view/paramMediator.d.ts +108 -0
- package/dist/src/view/paramMediator.d.ts.map +1 -0
- package/dist/src/view/paramMediator.js +337 -0
- package/dist/src/view/paramMediator.test.js +211 -0
- package/dist/src/view/scaleResolution.d.ts +8 -18
- package/dist/src/view/scaleResolution.d.ts.map +1 -1
- package/dist/src/view/scaleResolution.js +225 -126
- package/dist/src/view/scaleResolution.test.js +7 -7
- package/dist/src/view/unitView.d.ts.map +1 -1
- package/dist/src/view/unitView.js +10 -3
- package/dist/src/view/view.d.ts +4 -1
- package/dist/src/view/view.d.ts.map +1 -1
- package/dist/src/view/view.js +21 -7
- package/dist/src/view/viewFactory.d.ts.map +1 -1
- package/dist/src/view/viewFactory.js +45 -0
- package/dist/src/view/viewUtils.d.ts +5 -1
- package/dist/src/view/viewUtils.d.ts.map +1 -1
- package/dist/src/view/viewUtils.js +9 -4
- package/package.json +16 -17
- package/dist/src/paramBroker.d.ts +0 -30
- package/dist/src/paramBroker.d.ts.map +0 -1
- package/dist/src/paramBroker.js +0 -102
|
@@ -459,7 +459,7 @@ export function createGenomeAxis(axisProps, type) {
|
|
|
459
459
|
* @return {import("../spec/view.js").UnitSpec}
|
|
460
460
|
*/
|
|
461
461
|
const createChromosomeLabels = () => {
|
|
462
|
-
/** @type {Partial<import("../spec/mark.js").
|
|
462
|
+
/** @type {Partial<import("../spec/mark.js").MarkProps>} */
|
|
463
463
|
let chromLabelMarkProps;
|
|
464
464
|
switch (ap.orient) {
|
|
465
465
|
case "top":
|
|
@@ -594,7 +594,7 @@ export function createGenomeAxis(axisProps, type) {
|
|
|
594
594
|
if (axisProps.chromLabels) {
|
|
595
595
|
chromLayerSpec.layer.push(createChromosomeLabels());
|
|
596
596
|
|
|
597
|
-
/** @type {import("../spec/mark.js").
|
|
597
|
+
/** @type {import("../spec/mark.js").MarkProps} */
|
|
598
598
|
let labelMarkSpec;
|
|
599
599
|
|
|
600
600
|
// TODO: Simplify the following mess
|
|
@@ -608,7 +608,7 @@ export function createGenomeAxis(axisProps, type) {
|
|
|
608
608
|
/** @type {import("../spec/view.js").UnitSpec} */ view
|
|
609
609
|
) => {
|
|
610
610
|
labelMarkSpec =
|
|
611
|
-
/** @type {import("../spec/mark.js").
|
|
611
|
+
/** @type {import("../spec/mark.js").MarkProps} */ (
|
|
612
612
|
view.mark
|
|
613
613
|
);
|
|
614
614
|
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @param {any} x
|
|
3
|
+
* @returns {x is import("../spec/parameter.js").ExprRef}
|
|
4
|
+
*/
|
|
5
|
+
export function isExprRef(x: any): x is import("../spec/parameter.js").ExprRef;
|
|
6
|
+
/**
|
|
7
|
+
* Removes ExprRef from the type and checks that the value is not an ExprRef.
|
|
8
|
+
* This is designed to be used with `activateExprRefProps`.
|
|
9
|
+
*
|
|
10
|
+
* @param {T | import("../spec/parameter.js").ExprRef} x
|
|
11
|
+
* @template T
|
|
12
|
+
* @returns {T}
|
|
13
|
+
*/
|
|
14
|
+
export function withoutExprRef<T>(x: T | import("../spec/parameter.js").ExprRef): T;
|
|
15
|
+
/**
|
|
16
|
+
* Takes a record of properties that may have ExprRefs as values. Converts the
|
|
17
|
+
* ExprRefs to getters and setups a listener that is called when any of the
|
|
18
|
+
* expressions (upstream parameters) change.
|
|
19
|
+
*
|
|
20
|
+
* @param {ParamMediator} paramMediator
|
|
21
|
+
* @param {T} props The properties object
|
|
22
|
+
* @param {(props: (keyof T)[]) => void} [listener] Listener to be called when any of the expressions change
|
|
23
|
+
* @returns T
|
|
24
|
+
* @template {Record<string, any | import("../spec/parameter.js").ExprRef>} T
|
|
25
|
+
*/
|
|
26
|
+
export function activateExprRefProps<T extends Record<string, any>>(paramMediator: ParamMediator, props: T, listener?: (props: (keyof T)[]) => void): T;
|
|
27
|
+
/**
|
|
28
|
+
* A class that manages parameters and expressions.
|
|
29
|
+
* Supports nesting and scoped parameters.
|
|
30
|
+
*
|
|
31
|
+
* @typedef {import("../utils/expression.js").ExpressionFunction & { addListener: (listener: () => void) => void, invalidate: () => void, identifier: () => string}} ExprRefFunction
|
|
32
|
+
*/
|
|
33
|
+
export default class ParamMediator {
|
|
34
|
+
/**
|
|
35
|
+
* @param {() => ParamMediator} [parentFinder]
|
|
36
|
+
* An optional function that returns the parent mediator.
|
|
37
|
+
* N.B. The function must always return the same mediator for the same parent,
|
|
38
|
+
* i.e., the changing the structure of the hierarchy is NOT supported.
|
|
39
|
+
*/
|
|
40
|
+
constructor(parentFinder?: () => ParamMediator);
|
|
41
|
+
/**
|
|
42
|
+
* @type {Map<string, Set<() => void>>}
|
|
43
|
+
* @protected
|
|
44
|
+
*/
|
|
45
|
+
protected paramListeners: Map<string, Set<() => void>>;
|
|
46
|
+
/**
|
|
47
|
+
* @param {VariableParameter} param
|
|
48
|
+
* @returns {ParameterSetter}
|
|
49
|
+
*/
|
|
50
|
+
registerParam(param: import("../spec/parameter.js").VariableParameter): (value: any) => void;
|
|
51
|
+
/**
|
|
52
|
+
*
|
|
53
|
+
* @param {string} paramName
|
|
54
|
+
* @param {T} initialValue
|
|
55
|
+
* @returns {(value: T) => void}
|
|
56
|
+
* @template T
|
|
57
|
+
*/
|
|
58
|
+
allocateSetter<T>(paramName: string, initialValue: T): (value: T) => void;
|
|
59
|
+
/**
|
|
60
|
+
* Gets an existing setter for a parameter. Throws if the setter is not found.
|
|
61
|
+
* @param {string} paramName
|
|
62
|
+
*/
|
|
63
|
+
getSetter(paramName: string): (value: any) => void;
|
|
64
|
+
/**
|
|
65
|
+
* Get the value of a parameter from this mediator.
|
|
66
|
+
* @param {string} paramName
|
|
67
|
+
*/
|
|
68
|
+
getValue(paramName: string): any;
|
|
69
|
+
/**
|
|
70
|
+
* Get the value of a parameter from this mediator or the ancestors.
|
|
71
|
+
* @param {string} paramName
|
|
72
|
+
*/
|
|
73
|
+
findValue(paramName: string): any;
|
|
74
|
+
/**
|
|
75
|
+
* Returns configs for all parameters that have been registered using `registerParam`.
|
|
76
|
+
*/
|
|
77
|
+
get paramConfigs(): ReadonlyMap<string, import("../spec/parameter.js").VariableParameter>;
|
|
78
|
+
/**
|
|
79
|
+
*
|
|
80
|
+
* @param {string} paramName
|
|
81
|
+
* @returns {ParamMediator}
|
|
82
|
+
* @protected
|
|
83
|
+
*/
|
|
84
|
+
protected findMediatorForParam(paramName: string): ParamMediator;
|
|
85
|
+
/**
|
|
86
|
+
* Parse expr and return a function that returns the value of the parameter.
|
|
87
|
+
*
|
|
88
|
+
* @param {string} expr
|
|
89
|
+
*/
|
|
90
|
+
createExpression(expr: string): ExprRefFunction;
|
|
91
|
+
/**
|
|
92
|
+
* A convenience method for evaluating an expression.
|
|
93
|
+
*
|
|
94
|
+
* @param {string} expr
|
|
95
|
+
*/
|
|
96
|
+
evaluateAndGet(expr: string): any;
|
|
97
|
+
#private;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* A class that manages parameters and expressions.
|
|
101
|
+
* Supports nesting and scoped parameters.
|
|
102
|
+
*/
|
|
103
|
+
export type ExprRefFunction = ((datum?: object) => any) & import("../utils/expression.js").ExpressionProps & {
|
|
104
|
+
addListener: (listener: () => void) => void;
|
|
105
|
+
invalidate: () => void;
|
|
106
|
+
identifier: () => string;
|
|
107
|
+
};
|
|
108
|
+
//# sourceMappingURL=paramMediator.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"paramMediator.d.ts","sourceRoot":"","sources":["../../../src/view/paramMediator.js"],"names":[],"mappings":"AAsQA;;;GAGG;AACH,6BAHW,GAAG,+CAKb;AAED;;;;;;;GAOG;AACH,oFASC;AAED;;;;;;;;;;GAUG;AACH,mFANW,aAAa,+CAEW,IAAI,KAwCtC;AA7UD;;;;;GAKG;AACH;IA2BI;;;;;OAKG;IACH,2BALW,MAAM,aAAa,EAU7B;IA7BD;;;OAGG;IACH,0BAHU,IAAI,MAAM,EAAE,IAAI,MAAM,IAAI,CAAC,CAAC,CAGvB;IA2Bf;;;OAGG;IACH,gFAzCqB,GAAG,KAAK,IAAI,CAkEhC;IAED;;;;;;OAMG;IACH,6BALW,MAAM,kCAEU,IAAI,CA8B9B;IAED;;;OAGG;IACH,qBAFW,MAAM,WA9Fc,GAAG,KAAK,IAAI,CAsG1C;IAED;;;OAGG;IACH,oBAFW,MAAM,OAIhB;IAED;;;OAGG;IACH,qBAFW,MAAM,OAKhB;IAED;;OAEG;IACH,0FAIC;IAED;;;;;OAKG;IACH,0CAJW,MAAM,GACJ,aAAa,CASzB;IAID;;;;OAIG;IACH,uBAFW,MAAM,mBA4EhB;IAED;;;;OAIG;IACH,qBAFW,MAAM,OAKhB;;CACJ;;;;;;4BA7P4F,MAAM,IAAI,KAAK,IAAI;gBAAc,MAAM,IAAI;gBAAc,MAAM,MAAM"}
|
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
import { isString } from "vega-util";
|
|
2
|
+
import createFunction from "../utils/expression.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* A class that manages parameters and expressions.
|
|
6
|
+
* Supports nesting and scoped parameters.
|
|
7
|
+
*
|
|
8
|
+
* @typedef {import("../utils/expression.js").ExpressionFunction & { addListener: (listener: () => void) => void, invalidate: () => void, identifier: () => string}} ExprRefFunction
|
|
9
|
+
*/
|
|
10
|
+
export default class ParamMediator {
|
|
11
|
+
/**
|
|
12
|
+
* @typedef {import("../spec/parameter.js").VariableParameter} VariableParameter
|
|
13
|
+
* @typedef {(value: any) => void} ParameterSetter
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/** @type {Map<string, any>} */
|
|
17
|
+
#paramValues;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* @type {Map<string, Set<() => void>>}
|
|
21
|
+
* @protected
|
|
22
|
+
*/
|
|
23
|
+
paramListeners;
|
|
24
|
+
|
|
25
|
+
/** @type {Map<string, (value: any) => void>} */
|
|
26
|
+
#allocatedSetters = new Map();
|
|
27
|
+
|
|
28
|
+
/** @type {Map<string, ExprRefFunction>} */
|
|
29
|
+
#expressions = new Map();
|
|
30
|
+
|
|
31
|
+
/** @type {Map<string, VariableParameter>} */
|
|
32
|
+
#paramConfigs = new Map();
|
|
33
|
+
|
|
34
|
+
/** @type {() => ParamMediator} */
|
|
35
|
+
#parentFinder;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* @param {() => ParamMediator} [parentFinder]
|
|
39
|
+
* An optional function that returns the parent mediator.
|
|
40
|
+
* N.B. The function must always return the same mediator for the same parent,
|
|
41
|
+
* i.e., the changing the structure of the hierarchy is NOT supported.
|
|
42
|
+
*/
|
|
43
|
+
constructor(parentFinder) {
|
|
44
|
+
this.#parentFinder = parentFinder ?? (() => undefined);
|
|
45
|
+
|
|
46
|
+
this.#paramValues = new Map();
|
|
47
|
+
this.paramListeners = new Map();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* @param {VariableParameter} param
|
|
52
|
+
* @returns {ParameterSetter}
|
|
53
|
+
*/
|
|
54
|
+
registerParam(param) {
|
|
55
|
+
if ("value" in param && "expr" in param) {
|
|
56
|
+
throw new Error(
|
|
57
|
+
"Parameter must not have both value and expr: " + param.name
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** @type {ParameterSetter} */
|
|
62
|
+
let setter;
|
|
63
|
+
|
|
64
|
+
if ("value" in param) {
|
|
65
|
+
setter = this.allocateSetter(param.name, param.value);
|
|
66
|
+
} else if ("expr" in param) {
|
|
67
|
+
const expr = this.createExpression(param.expr);
|
|
68
|
+
// TODO: getSetter(param) should return a setter that throws if
|
|
69
|
+
// modifying the value is attempted.
|
|
70
|
+
const realSetter = this.allocateSetter(param.name, expr(null));
|
|
71
|
+
expr.addListener(() => realSetter(expr(null)));
|
|
72
|
+
// NOP
|
|
73
|
+
setter = (_) => undefined;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
this.#paramConfigs.set(param.name, param);
|
|
77
|
+
|
|
78
|
+
return setter;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
*
|
|
83
|
+
* @param {string} paramName
|
|
84
|
+
* @param {T} initialValue
|
|
85
|
+
* @returns {(value: T) => void}
|
|
86
|
+
* @template T
|
|
87
|
+
*/
|
|
88
|
+
allocateSetter(paramName, initialValue) {
|
|
89
|
+
if (this.#allocatedSetters.has(paramName)) {
|
|
90
|
+
throw new Error(
|
|
91
|
+
"Setter already allocated for parameter: " + paramName
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** @type {(value: any) => void} */
|
|
96
|
+
const setter = (value) => {
|
|
97
|
+
const previous = this.#paramValues.get(paramName);
|
|
98
|
+
if (value !== previous) {
|
|
99
|
+
this.#paramValues.set(paramName, value);
|
|
100
|
+
|
|
101
|
+
const listeners = this.paramListeners.get(paramName);
|
|
102
|
+
if (listeners) {
|
|
103
|
+
for (const listener of listeners) {
|
|
104
|
+
listener();
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
setter(initialValue);
|
|
111
|
+
|
|
112
|
+
this.#allocatedSetters.set(paramName, setter);
|
|
113
|
+
|
|
114
|
+
return setter;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Gets an existing setter for a parameter. Throws if the setter is not found.
|
|
119
|
+
* @param {string} paramName
|
|
120
|
+
*/
|
|
121
|
+
getSetter(paramName) {
|
|
122
|
+
const setter = this.#allocatedSetters.get(paramName);
|
|
123
|
+
if (!setter) {
|
|
124
|
+
throw new Error("Setter not found for parameter: " + paramName);
|
|
125
|
+
}
|
|
126
|
+
return setter;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Get the value of a parameter from this mediator.
|
|
131
|
+
* @param {string} paramName
|
|
132
|
+
*/
|
|
133
|
+
getValue(paramName) {
|
|
134
|
+
return this.#paramValues.get(paramName);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Get the value of a parameter from this mediator or the ancestors.
|
|
139
|
+
* @param {string} paramName
|
|
140
|
+
*/
|
|
141
|
+
findValue(paramName) {
|
|
142
|
+
const mediator = this.findMediatorForParam(paramName);
|
|
143
|
+
return mediator?.getValue(paramName);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Returns configs for all parameters that have been registered using `registerParam`.
|
|
148
|
+
*/
|
|
149
|
+
get paramConfigs() {
|
|
150
|
+
return /** @type {ReadonlyMap<string, VariableParameter>} */ (
|
|
151
|
+
this.#paramConfigs
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
*
|
|
157
|
+
* @param {string} paramName
|
|
158
|
+
* @returns {ParamMediator}
|
|
159
|
+
* @protected
|
|
160
|
+
*/
|
|
161
|
+
findMediatorForParam(paramName) {
|
|
162
|
+
if (this.#paramValues.has(paramName)) {
|
|
163
|
+
return this;
|
|
164
|
+
} else {
|
|
165
|
+
return this.#parentFinder()?.findMediatorForParam(paramName);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// TODO: deallocateSetter
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Parse expr and return a function that returns the value of the parameter.
|
|
173
|
+
*
|
|
174
|
+
* @param {string} expr
|
|
175
|
+
*/
|
|
176
|
+
createExpression(expr) {
|
|
177
|
+
if (this.#expressions.has(expr)) {
|
|
178
|
+
return this.#expressions.get(expr);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const globalObject = {};
|
|
182
|
+
|
|
183
|
+
/** @type {ExprRefFunction} */
|
|
184
|
+
const fn = /** @type {any} */ (createFunction(expr, globalObject));
|
|
185
|
+
|
|
186
|
+
/** @type {Map<string, ParamMediator>} */
|
|
187
|
+
const mediatorsForParams = new Map();
|
|
188
|
+
|
|
189
|
+
for (const param of fn.globals) {
|
|
190
|
+
const mediator = this.findMediatorForParam(param);
|
|
191
|
+
if (!mediator) {
|
|
192
|
+
throw new Error(
|
|
193
|
+
`Unknown variable "${param}" in expression: ${expr}`
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
mediatorsForParams.set(param, mediator);
|
|
198
|
+
|
|
199
|
+
Object.defineProperty(globalObject, param, {
|
|
200
|
+
enumerable: true,
|
|
201
|
+
get() {
|
|
202
|
+
return mediator.getValue(param);
|
|
203
|
+
},
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
// TODO: There should be a way to "materialize" the global object when
|
|
207
|
+
// it is used in expressions in transformation batches, i.e., when the same
|
|
208
|
+
// expression is applied to multiple data objects. In that case, the global
|
|
209
|
+
// object remains constant and the Map lookups cause unnecessary overhead.
|
|
210
|
+
|
|
211
|
+
// Keep track of them so that they can be detached later
|
|
212
|
+
const myListeners = new Set();
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
*
|
|
216
|
+
* @param {() => void} listener
|
|
217
|
+
*/
|
|
218
|
+
fn.addListener = (listener) => {
|
|
219
|
+
for (const [param, mediator] of mediatorsForParams) {
|
|
220
|
+
const listeners =
|
|
221
|
+
mediator.paramListeners.get(param) ?? new Set();
|
|
222
|
+
mediator.paramListeners.set(param, listeners);
|
|
223
|
+
|
|
224
|
+
listeners.add(listener);
|
|
225
|
+
myListeners.add(listener);
|
|
226
|
+
}
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Detach listeners. This must be called if the expression is no longer used.
|
|
231
|
+
* TODO: What if the expression is used in multiple places?
|
|
232
|
+
*/
|
|
233
|
+
fn.invalidate = () => {
|
|
234
|
+
for (const [param, mediator] of mediatorsForParams) {
|
|
235
|
+
for (const listener of myListeners) {
|
|
236
|
+
mediator.paramListeners.get(param)?.delete(listener);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
// TODO: This should contain unique identifier for each parameter.
|
|
242
|
+
// As the same parameter name may be used in different branches of the
|
|
243
|
+
// hierarchy, they should be distinguished by a unique identifier, e.g.,
|
|
244
|
+
// a serial number of something similar.
|
|
245
|
+
fn.identifier = () => fn.code;
|
|
246
|
+
|
|
247
|
+
this.#expressions.set(expr, fn);
|
|
248
|
+
|
|
249
|
+
return fn;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* A convenience method for evaluating an expression.
|
|
254
|
+
*
|
|
255
|
+
* @param {string} expr
|
|
256
|
+
*/
|
|
257
|
+
evaluateAndGet(expr) {
|
|
258
|
+
const fn = this.createExpression(expr);
|
|
259
|
+
return fn();
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* @param {any} x
|
|
265
|
+
* @returns {x is import("../spec/parameter.js").ExprRef}
|
|
266
|
+
*/
|
|
267
|
+
export function isExprRef(x) {
|
|
268
|
+
return typeof x == "object" && x != null && "expr" in x && isString(x.expr);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Removes ExprRef from the type and checks that the value is not an ExprRef.
|
|
273
|
+
* This is designed to be used with `activateExprRefProps`.
|
|
274
|
+
*
|
|
275
|
+
* @param {T | import("../spec/parameter.js").ExprRef} x
|
|
276
|
+
* @template T
|
|
277
|
+
* @returns {T}
|
|
278
|
+
*/
|
|
279
|
+
export function withoutExprRef(x) {
|
|
280
|
+
if (isExprRef(x)) {
|
|
281
|
+
throw new Error(
|
|
282
|
+
`ExprRef ${JSON.stringify(
|
|
283
|
+
x
|
|
284
|
+
)} not allowed here. Expected a scalar value.`
|
|
285
|
+
);
|
|
286
|
+
}
|
|
287
|
+
return /** @type {T} */ (x);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Takes a record of properties that may have ExprRefs as values. Converts the
|
|
292
|
+
* ExprRefs to getters and setups a listener that is called when any of the
|
|
293
|
+
* expressions (upstream parameters) change.
|
|
294
|
+
*
|
|
295
|
+
* @param {ParamMediator} paramMediator
|
|
296
|
+
* @param {T} props The properties object
|
|
297
|
+
* @param {(props: (keyof T)[]) => void} [listener] Listener to be called when any of the expressions change
|
|
298
|
+
* @returns T
|
|
299
|
+
* @template {Record<string, any | import("../spec/parameter.js").ExprRef>} T
|
|
300
|
+
*/
|
|
301
|
+
export function activateExprRefProps(paramMediator, props, listener) {
|
|
302
|
+
/** @type {Record<string, any | import("../spec/parameter.js").ExprRef>} */
|
|
303
|
+
const activatedProps = { ...props };
|
|
304
|
+
|
|
305
|
+
/** @type {(keyof T)[]} */
|
|
306
|
+
const alteredProps = [];
|
|
307
|
+
|
|
308
|
+
const batchPropertyChange = (/** @type {keyof T} */ prop) => {
|
|
309
|
+
alteredProps.push(prop);
|
|
310
|
+
if (alteredProps.length === 1) {
|
|
311
|
+
queueMicrotask(() => {
|
|
312
|
+
listener(alteredProps.slice());
|
|
313
|
+
alteredProps.length = 0;
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
};
|
|
317
|
+
|
|
318
|
+
for (const [key, value] of Object.entries(props)) {
|
|
319
|
+
if (isExprRef(value)) {
|
|
320
|
+
const fn = paramMediator.createExpression(value.expr);
|
|
321
|
+
if (listener) {
|
|
322
|
+
fn.addListener(() => batchPropertyChange(key));
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
Object.defineProperty(activatedProps, key, {
|
|
326
|
+
enumerable: true,
|
|
327
|
+
get() {
|
|
328
|
+
return fn();
|
|
329
|
+
},
|
|
330
|
+
});
|
|
331
|
+
} else {
|
|
332
|
+
activatedProps[key] = value;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
return /** @type {T} */ (activatedProps);
|
|
337
|
+
}
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import { describe, expect, test } from "vitest";
|
|
2
|
+
import ParamMediator, { activateExprRefProps } from "./paramMediator.js";
|
|
3
|
+
|
|
4
|
+
describe("Single-level ParamMediator", () => {
|
|
5
|
+
test("Trivial case", () => {
|
|
6
|
+
const pm = new ParamMediator();
|
|
7
|
+
pm.registerParam({ name: "foo", value: 42 });
|
|
8
|
+
expect(pm.getValue("foo")).toBe(42);
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
test("Setter", () => {
|
|
12
|
+
const pm = new ParamMediator();
|
|
13
|
+
const setter = pm.allocateSetter("foo", 42);
|
|
14
|
+
expect(pm.getValue("foo")).toBe(42);
|
|
15
|
+
|
|
16
|
+
setter(43);
|
|
17
|
+
expect(pm.getValue("foo")).toBe(43);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
test("Expressions have access to parameters", () => {
|
|
21
|
+
const pm = new ParamMediator();
|
|
22
|
+
pm.registerParam({ name: "foo", value: 42 });
|
|
23
|
+
const expr = pm.createExpression("foo + 1");
|
|
24
|
+
expect(expr()).toBe(43);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test("Throws on an unknown parameter", () => {
|
|
28
|
+
const pm = new ParamMediator();
|
|
29
|
+
expect(() => pm.createExpression("foo")).toThrow();
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test("Listener on an expression gets called (only) when a parameter changes", () => {
|
|
33
|
+
const pm = new ParamMediator();
|
|
34
|
+
const setter = pm.allocateSetter("foo", 42);
|
|
35
|
+
const expr = pm.createExpression("foo + 1");
|
|
36
|
+
|
|
37
|
+
let result;
|
|
38
|
+
let calls = 0;
|
|
39
|
+
|
|
40
|
+
expr.addListener(() => {
|
|
41
|
+
result = expr();
|
|
42
|
+
calls++;
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
setter(50);
|
|
46
|
+
expect(result).toBe(51);
|
|
47
|
+
expect(calls).toBe(1);
|
|
48
|
+
|
|
49
|
+
setter(60);
|
|
50
|
+
expect(result).toBe(61);
|
|
51
|
+
expect(calls).toBe(2);
|
|
52
|
+
|
|
53
|
+
setter(60);
|
|
54
|
+
expect(result).toBe(61);
|
|
55
|
+
expect(calls).toBe(2);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test("Expression invalidation", () => {
|
|
59
|
+
const pm = new ParamMediator();
|
|
60
|
+
const setter = pm.allocateSetter("foo", 42);
|
|
61
|
+
const expr = pm.createExpression("foo + 1");
|
|
62
|
+
|
|
63
|
+
let result = expr();
|
|
64
|
+
expect(result).toBe(43);
|
|
65
|
+
|
|
66
|
+
expr.addListener(() => (result = expr()));
|
|
67
|
+
|
|
68
|
+
setter(50);
|
|
69
|
+
expect(result).toBe(51);
|
|
70
|
+
|
|
71
|
+
expr.invalidate();
|
|
72
|
+
// Listeners should be invalidated now: the result must remain the same.
|
|
73
|
+
setter(60);
|
|
74
|
+
expect(result).toBe(51);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test("Expression parameter handles dependencies", () => {
|
|
78
|
+
const pm = new ParamMediator();
|
|
79
|
+
const setter = pm.registerParam({ name: "foo", value: 42 });
|
|
80
|
+
pm.registerParam({ name: "bar", expr: "foo + 1" });
|
|
81
|
+
pm.registerParam({ name: "baz", expr: "bar + 2" });
|
|
82
|
+
|
|
83
|
+
const expr = pm.createExpression("baz");
|
|
84
|
+
|
|
85
|
+
let result = expr();
|
|
86
|
+
expect(result).toBe(45);
|
|
87
|
+
|
|
88
|
+
expr.addListener(() => (result = expr()));
|
|
89
|
+
|
|
90
|
+
setter(52);
|
|
91
|
+
expect(result).toBe(55);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test("Throws if both value and expr are provided", () => {
|
|
95
|
+
const pm = new ParamMediator();
|
|
96
|
+
expect(() =>
|
|
97
|
+
pm.registerParam({ name: "foo", value: 42, expr: "bar" })
|
|
98
|
+
).toThrow();
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
describe("Nested ParamMediators", () => {
|
|
103
|
+
test("Value in parent", () => {
|
|
104
|
+
const parent = new ParamMediator();
|
|
105
|
+
const child = new ParamMediator(() => parent);
|
|
106
|
+
|
|
107
|
+
parent.registerParam({ name: "foo", value: 42 });
|
|
108
|
+
expect(parent.findValue("foo")).toBe(42);
|
|
109
|
+
expect(child.findValue("foo")).toBe(42);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test("Value in child", () => {
|
|
113
|
+
const parent = new ParamMediator();
|
|
114
|
+
const child = new ParamMediator(() => parent);
|
|
115
|
+
|
|
116
|
+
child.registerParam({ name: "foo", value: 42 });
|
|
117
|
+
expect(parent.findValue("foo")).toBeUndefined();
|
|
118
|
+
expect(child.findValue("foo")).toBe(42);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test("Child overrides parent", () => {
|
|
122
|
+
const parent = new ParamMediator();
|
|
123
|
+
const child = new ParamMediator(() => parent);
|
|
124
|
+
|
|
125
|
+
parent.registerParam({ name: "foo", value: 1 });
|
|
126
|
+
child.registerParam({ name: "foo", value: 2 });
|
|
127
|
+
|
|
128
|
+
expect(parent.findValue("foo")).toBe(1);
|
|
129
|
+
expect(child.findValue("foo")).toBe(2);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
test("Expression", () => {
|
|
133
|
+
const parent = new ParamMediator();
|
|
134
|
+
const child = new ParamMediator(() => parent);
|
|
135
|
+
|
|
136
|
+
parent.registerParam({ name: "foo", value: 1 });
|
|
137
|
+
child.registerParam({ name: "bar", value: 2 });
|
|
138
|
+
|
|
139
|
+
const expr = child.createExpression("foo + bar");
|
|
140
|
+
expect(expr()).toBe(3);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
test("Listener on an expression", () => {
|
|
144
|
+
const parent = new ParamMediator();
|
|
145
|
+
const child = new ParamMediator(() => parent);
|
|
146
|
+
|
|
147
|
+
const parentSetter = parent.allocateSetter("foo", 1);
|
|
148
|
+
const childSetter = parent.allocateSetter("bar", 2);
|
|
149
|
+
|
|
150
|
+
const expr = child.createExpression("foo + bar");
|
|
151
|
+
|
|
152
|
+
let result = expr();
|
|
153
|
+
expr.addListener(() => (result = expr()));
|
|
154
|
+
|
|
155
|
+
expect(result).toBe(3);
|
|
156
|
+
|
|
157
|
+
parentSetter(10);
|
|
158
|
+
expect(result).toBe(12);
|
|
159
|
+
|
|
160
|
+
childSetter(20);
|
|
161
|
+
expect(result).toBe(30);
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
test("activateExprRefProps", async () => {
|
|
166
|
+
const pm = new ParamMediator();
|
|
167
|
+
|
|
168
|
+
const fooSetter = pm.registerParam({ name: "foo", value: 7 });
|
|
169
|
+
const barSetter = pm.registerParam({ name: "bar", value: 11 });
|
|
170
|
+
|
|
171
|
+
/** @type {Record<string, any | import("../spec/parameter.js").ExprRef} */
|
|
172
|
+
const props = {
|
|
173
|
+
a: 42,
|
|
174
|
+
b: { expr: "foo" },
|
|
175
|
+
c: { expr: "bar" },
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
/** @type {string[]} */
|
|
179
|
+
let altered = [];
|
|
180
|
+
|
|
181
|
+
const activatedProps = activateExprRefProps(pm, props, (props) => {
|
|
182
|
+
altered = props;
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
expect(activatedProps).toEqual({
|
|
186
|
+
a: 42,
|
|
187
|
+
b: 7,
|
|
188
|
+
c: 11,
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
fooSetter(8);
|
|
192
|
+
|
|
193
|
+
// Let the scheduled microtask call the listener
|
|
194
|
+
await Promise.resolve();
|
|
195
|
+
|
|
196
|
+
expect(altered).toEqual(["b"]);
|
|
197
|
+
|
|
198
|
+
fooSetter(1);
|
|
199
|
+
barSetter(2);
|
|
200
|
+
|
|
201
|
+
// Let the scheduled microtask call the listener
|
|
202
|
+
await Promise.resolve();
|
|
203
|
+
|
|
204
|
+
expect(altered).toEqual(["b", "c"]);
|
|
205
|
+
|
|
206
|
+
expect(activatedProps).toEqual({
|
|
207
|
+
a: 42,
|
|
208
|
+
b: 1,
|
|
209
|
+
c: 2,
|
|
210
|
+
});
|
|
211
|
+
});
|