@m2c2kit/assessment-color-shapes 0.8.32 → 0.8.34
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/__tests__/data.d.ts +3 -0
- package/dist/__tests__/data.d.ts.map +1 -0
- package/dist/__tests__/python-code.d.ts +2 -0
- package/dist/__tests__/python-code.d.ts.map +1 -0
- package/dist/__tests__/scoring.test.d.ts +2 -0
- package/dist/__tests__/scoring.test.d.ts.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1347 -48
- package/dist/index.js.map +1 -1
- package/dist/index.min.js +1 -1
- package/package.json +12 -10
- package/schemas.json +247 -1
package/dist/index.js
CHANGED
|
@@ -11,6 +11,72 @@ class M2Error extends Error {
|
|
|
11
11
|
}
|
|
12
12
|
}
|
|
13
13
|
}
|
|
14
|
+
const CHAIN_REGISTRY = /* @__PURE__ */ new Map();
|
|
15
|
+
let CHAIN_COUNTER = 0;
|
|
16
|
+
function getChainOps(id) {
|
|
17
|
+
return CHAIN_REGISTRY.get(id);
|
|
18
|
+
}
|
|
19
|
+
function clearChainOps(id) {
|
|
20
|
+
CHAIN_REGISTRY.delete(id);
|
|
21
|
+
}
|
|
22
|
+
function makeChainFn() {
|
|
23
|
+
const fn = ((dataCalc) => {
|
|
24
|
+
if (!dataCalc) return void 0;
|
|
25
|
+
let current = dataCalc;
|
|
26
|
+
for (const op of fn.ops) {
|
|
27
|
+
const method = current[op.name];
|
|
28
|
+
if (typeof method !== "function") {
|
|
29
|
+
throw new M2Error(
|
|
30
|
+
`chain: method ${op.name} does not exist on DataCalc`
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
const res = method.apply(current, op.args);
|
|
34
|
+
if (res instanceof DataCalc) {
|
|
35
|
+
current = res;
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
return res;
|
|
39
|
+
}
|
|
40
|
+
return void 0;
|
|
41
|
+
});
|
|
42
|
+
fn.ops = [];
|
|
43
|
+
const record = (name, args) => {
|
|
44
|
+
fn.ops.push({ name, args });
|
|
45
|
+
return fn;
|
|
46
|
+
};
|
|
47
|
+
fn.arrange = (...variables) => record("arrange", variables);
|
|
48
|
+
fn.slice = (start, end) => record("slice", [start, end]);
|
|
49
|
+
fn.pull = (variable) => record("pull", [variable]);
|
|
50
|
+
fn.filter = (predicate) => record("filter", [predicate]);
|
|
51
|
+
fn.mutate = (mutations) => record("mutate", [mutations]);
|
|
52
|
+
fn.select = (...variables) => record("select", variables);
|
|
53
|
+
fn.groupBy = (...groups) => record("groupBy", groups);
|
|
54
|
+
fn.ungroup = () => record("ungroup", []);
|
|
55
|
+
fn.rename = (renames) => record("rename", [renames]);
|
|
56
|
+
fn.distinct = () => record("distinct", []);
|
|
57
|
+
Object.defineProperty(fn, "length", {
|
|
58
|
+
configurable: true,
|
|
59
|
+
get: function() {
|
|
60
|
+
try {
|
|
61
|
+
const id = `c${++CHAIN_COUNTER}`;
|
|
62
|
+
CHAIN_REGISTRY.set(
|
|
63
|
+
id,
|
|
64
|
+
(fn.ops || []).map((o) => ({ ...o }))
|
|
65
|
+
);
|
|
66
|
+
return `__CHAIN_EXPR__[${id}]`;
|
|
67
|
+
} catch (err) {
|
|
68
|
+
return void 0;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
return fn;
|
|
73
|
+
}
|
|
74
|
+
function arrange(...variables) {
|
|
75
|
+
return makeChainFn().arrange(...variables);
|
|
76
|
+
}
|
|
77
|
+
function filter(predicate) {
|
|
78
|
+
return makeChainFn().filter(predicate);
|
|
79
|
+
}
|
|
14
80
|
class DataCalc {
|
|
15
81
|
/**
|
|
16
82
|
* A class for transformation and calculation of m2c2kit data.
|
|
@@ -34,6 +100,7 @@ class DataCalc {
|
|
|
34
100
|
*/
|
|
35
101
|
constructor(data, options) {
|
|
36
102
|
this._groups = new Array();
|
|
103
|
+
this._warnings = false;
|
|
37
104
|
if (!Array.isArray(data)) {
|
|
38
105
|
throw new M2Error(
|
|
39
106
|
"DataCalc constructor expects an array of observations as first argument"
|
|
@@ -63,6 +130,9 @@ class DataCalc {
|
|
|
63
130
|
if (options?.groups) {
|
|
64
131
|
this._groups = Array.from(options.groups);
|
|
65
132
|
}
|
|
133
|
+
if (options?.warnings) {
|
|
134
|
+
this._warnings = true;
|
|
135
|
+
}
|
|
66
136
|
}
|
|
67
137
|
/**
|
|
68
138
|
* Returns the groups in the data.
|
|
@@ -89,7 +159,8 @@ class DataCalc {
|
|
|
89
159
|
* Returns a single variable from the data.
|
|
90
160
|
*
|
|
91
161
|
* @remarks If the variable length is 1, the value is returned. If the
|
|
92
|
-
* variable has length > 1, an array of values is returned.
|
|
162
|
+
* variable has length > 1, an array of values is returned. If an empty
|
|
163
|
+
* dataset is provided, `null` is returned and a warning is logged.
|
|
93
164
|
*
|
|
94
165
|
* @param variable - Name of variable to pull from the data
|
|
95
166
|
* @returns the value of the variable
|
|
@@ -105,9 +176,11 @@ class DataCalc {
|
|
|
105
176
|
*/
|
|
106
177
|
pull(variable) {
|
|
107
178
|
if (this._observations.length === 0) {
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
179
|
+
if (this._warnings) {
|
|
180
|
+
console.warn(
|
|
181
|
+
`DataCalc.pull(): No observations available to pull variable "${variable}" from. Returning null.`
|
|
182
|
+
);
|
|
183
|
+
}
|
|
111
184
|
return null;
|
|
112
185
|
}
|
|
113
186
|
this.verifyObservationsContainVariable(variable);
|
|
@@ -167,7 +240,7 @@ class DataCalc {
|
|
|
167
240
|
this._observations.filter(
|
|
168
241
|
predicate
|
|
169
242
|
),
|
|
170
|
-
{ groups: this._groups }
|
|
243
|
+
{ groups: this._groups, warnings: this._warnings }
|
|
171
244
|
);
|
|
172
245
|
}
|
|
173
246
|
/**
|
|
@@ -176,7 +249,8 @@ class DataCalc {
|
|
|
176
249
|
* @remarks This is used with the `summarize()` method to calculate summaries
|
|
177
250
|
* by group.
|
|
178
251
|
*
|
|
179
|
-
* @param groups - variable names to group by
|
|
252
|
+
* @param groups - variable names to group by. Grouping variables must be
|
|
253
|
+
* primitive values (string, number, boolean).
|
|
180
254
|
* @returns A new `DataCalc` object with the observations grouped by one or
|
|
181
255
|
* more variables
|
|
182
256
|
*
|
|
@@ -197,6 +271,16 @@ class DataCalc {
|
|
|
197
271
|
groupBy(...groups) {
|
|
198
272
|
groups.forEach((group) => {
|
|
199
273
|
this.verifyObservationsContainVariable(group);
|
|
274
|
+
for (let i = 0; i < this._observations.length; i++) {
|
|
275
|
+
const val = this._observations[i][group];
|
|
276
|
+
if (val === null) continue;
|
|
277
|
+
const t = typeof val;
|
|
278
|
+
if (t !== "number" && t !== "string" && t !== "boolean") {
|
|
279
|
+
throw new M2Error(
|
|
280
|
+
`groupBy(): variable "${group}" contains non-primitive value at index ${i} (type=${t}). Only number, string, boolean, or null are allowed for grouping.`
|
|
281
|
+
);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
200
284
|
});
|
|
201
285
|
return new DataCalc(this._observations, { groups });
|
|
202
286
|
}
|
|
@@ -250,7 +334,10 @@ class DataCalc {
|
|
|
250
334
|
}
|
|
251
335
|
return newObservation;
|
|
252
336
|
});
|
|
253
|
-
return new DataCalc(newObservations, {
|
|
337
|
+
return new DataCalc(newObservations, {
|
|
338
|
+
groups: this._groups,
|
|
339
|
+
warnings: this._warnings
|
|
340
|
+
});
|
|
254
341
|
}
|
|
255
342
|
/**
|
|
256
343
|
* Calculates summaries of the data.
|
|
@@ -287,6 +374,31 @@ class DataCalc {
|
|
|
287
374
|
* );
|
|
288
375
|
* // [ { filteredTotalC: 10 } ]
|
|
289
376
|
* ```
|
|
377
|
+
*
|
|
378
|
+
* @remarks Within a `summarize()` call, regular arithmetic operations
|
|
379
|
+
* (+, -, *, /, etc.) must not be used directly on `DataCalc` objects
|
|
380
|
+
* or summary functions. This is not allowed:
|
|
381
|
+
*
|
|
382
|
+
* @example
|
|
383
|
+
* ```js
|
|
384
|
+
* dc.summarize({
|
|
385
|
+
* meanA: mean("a"),
|
|
386
|
+
* // The below will cause an error because it attempts to add a number
|
|
387
|
+
* // to a DataCalc object, which is not supported
|
|
388
|
+
* totalBPlus5: sum("b") + 5
|
|
389
|
+
* })
|
|
390
|
+
* ```
|
|
391
|
+
*
|
|
392
|
+
* Instead, use the `DataCalc` helper functions such as `add` to perform
|
|
393
|
+
* calculations within summary results. For example, to add 5 to the sum
|
|
394
|
+
* of "b", you can do:
|
|
395
|
+
* @example
|
|
396
|
+
* ```js
|
|
397
|
+
* dc.summarize({
|
|
398
|
+
* meanA: mean("a"),
|
|
399
|
+
* totalBPlus5: sum("b").add(5)
|
|
400
|
+
* })
|
|
401
|
+
* ```
|
|
290
402
|
*/
|
|
291
403
|
summarize(summarizations) {
|
|
292
404
|
if (this._groups.length === 0) {
|
|
@@ -299,20 +411,103 @@ class DataCalc {
|
|
|
299
411
|
summarizeOperation.parameters,
|
|
300
412
|
summarizeOperation.options
|
|
301
413
|
);
|
|
414
|
+
} else if (typeof value === "function") {
|
|
415
|
+
try {
|
|
416
|
+
const res = value(this);
|
|
417
|
+
if (typeof res === "function") {
|
|
418
|
+
throw new M2Error(
|
|
419
|
+
`summarize(): lazy callback for ${newVariable} returned a function; expected a value or array of values.`
|
|
420
|
+
);
|
|
421
|
+
}
|
|
422
|
+
if (res instanceof DataCalc) {
|
|
423
|
+
throw new M2Error(
|
|
424
|
+
`summarize(): lazy callback for ${newVariable} returned a DataCalc; expected a value or array of values.`
|
|
425
|
+
);
|
|
426
|
+
}
|
|
427
|
+
obs[newVariable] = res;
|
|
428
|
+
} catch (err) {
|
|
429
|
+
throw new M2Error(
|
|
430
|
+
`summarize(): lazy callback for ${newVariable} threw an error: ${err && err.message ? err.message : String(err)}`
|
|
431
|
+
);
|
|
432
|
+
}
|
|
302
433
|
} else {
|
|
434
|
+
if (typeof value === "string") {
|
|
435
|
+
const re = /__CHAIN_EXPR__\[(.*?)\]/g;
|
|
436
|
+
const matches = Array.from(value.matchAll(re));
|
|
437
|
+
if (matches.length > 0) {
|
|
438
|
+
let sum2 = 0;
|
|
439
|
+
for (const m of matches) {
|
|
440
|
+
const payload = m[1];
|
|
441
|
+
let ops = getChainOps(payload);
|
|
442
|
+
if (!ops) {
|
|
443
|
+
try {
|
|
444
|
+
ops = JSON.parse(decodeURIComponent(payload));
|
|
445
|
+
} catch (err) {
|
|
446
|
+
throw new M2Error(
|
|
447
|
+
`summarize(): failed to parse chain payload for ${newVariable}`
|
|
448
|
+
);
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
let current = this;
|
|
452
|
+
let evaluated = void 0;
|
|
453
|
+
if (!ops) {
|
|
454
|
+
throw new M2Error(
|
|
455
|
+
`summarize(): empty chain ops for ${newVariable}`
|
|
456
|
+
);
|
|
457
|
+
}
|
|
458
|
+
for (const op of ops) {
|
|
459
|
+
const method = current[op.name];
|
|
460
|
+
if (typeof method !== "function") {
|
|
461
|
+
throw new M2Error(
|
|
462
|
+
`summarize(): chain method ${op.name} does not exist on DataCalc`
|
|
463
|
+
);
|
|
464
|
+
}
|
|
465
|
+
const res = method.apply(current, op.args);
|
|
466
|
+
if (res instanceof DataCalc) {
|
|
467
|
+
current = res;
|
|
468
|
+
continue;
|
|
469
|
+
}
|
|
470
|
+
if (Array.isArray(res)) {
|
|
471
|
+
evaluated = res.length;
|
|
472
|
+
break;
|
|
473
|
+
}
|
|
474
|
+
if (typeof res === "boolean") {
|
|
475
|
+
evaluated = res ? 1 : 0;
|
|
476
|
+
break;
|
|
477
|
+
}
|
|
478
|
+
evaluated = typeof res === "number" ? res : Number(res);
|
|
479
|
+
break;
|
|
480
|
+
}
|
|
481
|
+
if (evaluated === void 0) {
|
|
482
|
+
evaluated = current instanceof DataCalc ? current.length : 0;
|
|
483
|
+
}
|
|
484
|
+
sum2 += evaluated;
|
|
485
|
+
}
|
|
486
|
+
let leftover = value;
|
|
487
|
+
for (const m of matches) leftover = leftover.replace(m[0], "");
|
|
488
|
+
const leftoverNum = Number(leftover);
|
|
489
|
+
if (!Number.isNaN(leftoverNum)) sum2 += leftoverNum;
|
|
490
|
+
obs[newVariable] = sum2;
|
|
491
|
+
for (const m of matches) {
|
|
492
|
+
clearChainOps(m[1]);
|
|
493
|
+
}
|
|
494
|
+
continue;
|
|
495
|
+
}
|
|
496
|
+
}
|
|
303
497
|
obs[newVariable] = value;
|
|
304
498
|
}
|
|
305
499
|
}
|
|
306
|
-
return new DataCalc([obs], {
|
|
500
|
+
return new DataCalc([obs], {
|
|
501
|
+
groups: this._groups,
|
|
502
|
+
warnings: this._warnings
|
|
503
|
+
});
|
|
307
504
|
}
|
|
308
505
|
return this.summarizeByGroups(summarizations);
|
|
309
506
|
}
|
|
310
507
|
summarizeByGroups(summarizations) {
|
|
311
508
|
const groupMap = /* @__PURE__ */ new Map();
|
|
312
509
|
this._observations.forEach((obs) => {
|
|
313
|
-
const groupKey = this._groups.map(
|
|
314
|
-
(g) => typeof obs[g] === "object" ? JSON.stringify(obs[g]) : obs[g]
|
|
315
|
-
).join("|");
|
|
510
|
+
const groupKey = this._groups.map((g) => String(obs[g])).join("|");
|
|
316
511
|
if (!groupMap.has(groupKey)) {
|
|
317
512
|
groupMap.set(groupKey, []);
|
|
318
513
|
}
|
|
@@ -330,21 +525,17 @@ class DataCalc {
|
|
|
330
525
|
const summaryObj = {};
|
|
331
526
|
this._groups.forEach((group, i) => {
|
|
332
527
|
const valueStr = groupValues[i];
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
summaryObj[group] = Number(valueStr);
|
|
336
|
-
} else if (originalType === "boolean") {
|
|
337
|
-
summaryObj[group] = valueStr === "true";
|
|
338
|
-
} else if (valueStr.startsWith("{") || valueStr.startsWith("[")) {
|
|
339
|
-
try {
|
|
340
|
-
summaryObj[group] = JSON.parse(valueStr);
|
|
341
|
-
} catch {
|
|
342
|
-
throw new M2Error(
|
|
343
|
-
`Failed to parse group value ${valueStr} as JSON for group ${group}`
|
|
344
|
-
);
|
|
345
|
-
}
|
|
528
|
+
if (firstObs[group] === null) {
|
|
529
|
+
summaryObj[group] = null;
|
|
346
530
|
} else {
|
|
347
|
-
|
|
531
|
+
const originalType = typeof firstObs[group];
|
|
532
|
+
if (originalType === "number") {
|
|
533
|
+
summaryObj[group] = Number(valueStr);
|
|
534
|
+
} else if (originalType === "boolean") {
|
|
535
|
+
summaryObj[group] = valueStr === "true";
|
|
536
|
+
} else {
|
|
537
|
+
summaryObj[group] = valueStr;
|
|
538
|
+
}
|
|
348
539
|
}
|
|
349
540
|
});
|
|
350
541
|
const groupDataCalc = new DataCalc(groupObs);
|
|
@@ -356,13 +547,98 @@ class DataCalc {
|
|
|
356
547
|
summarizeOperation.parameters,
|
|
357
548
|
summarizeOperation.options
|
|
358
549
|
);
|
|
550
|
+
} else if (typeof value === "function") {
|
|
551
|
+
try {
|
|
552
|
+
const res = value(groupDataCalc);
|
|
553
|
+
if (typeof res === "function") {
|
|
554
|
+
throw new M2Error(
|
|
555
|
+
`summarize(): lazy callback for ${newVariable} returned a function; expected a value or array of values.`
|
|
556
|
+
);
|
|
557
|
+
}
|
|
558
|
+
if (res instanceof DataCalc) {
|
|
559
|
+
throw new M2Error(
|
|
560
|
+
`summarize(): lazy callback for ${newVariable} returned a DataCalc; expected a value or array of values.`
|
|
561
|
+
);
|
|
562
|
+
}
|
|
563
|
+
summaryObj[newVariable] = res;
|
|
564
|
+
} catch (err) {
|
|
565
|
+
throw new M2Error(
|
|
566
|
+
`summarize(): lazy callback for ${newVariable} threw an error: ${err && err.message ? err.message : String(err)}`
|
|
567
|
+
);
|
|
568
|
+
}
|
|
359
569
|
} else {
|
|
570
|
+
if (typeof value === "string") {
|
|
571
|
+
const re = /__CHAIN_EXPR__\[(.*?)\]/g;
|
|
572
|
+
const matches = Array.from(value.matchAll(re));
|
|
573
|
+
if (matches.length > 0) {
|
|
574
|
+
let sum2 = 0;
|
|
575
|
+
for (const m of matches) {
|
|
576
|
+
const payload = m[1];
|
|
577
|
+
let ops = getChainOps(payload);
|
|
578
|
+
if (!ops) {
|
|
579
|
+
try {
|
|
580
|
+
ops = JSON.parse(decodeURIComponent(payload));
|
|
581
|
+
} catch (err) {
|
|
582
|
+
throw new M2Error(
|
|
583
|
+
`summarize(): failed to parse chain payload for ${newVariable}`
|
|
584
|
+
);
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
let current = groupDataCalc;
|
|
588
|
+
let evaluated = void 0;
|
|
589
|
+
if (!ops) {
|
|
590
|
+
throw new M2Error(
|
|
591
|
+
`summarize(): empty chain ops for ${newVariable}`
|
|
592
|
+
);
|
|
593
|
+
}
|
|
594
|
+
for (const op of ops) {
|
|
595
|
+
const method = current[op.name];
|
|
596
|
+
if (typeof method !== "function") {
|
|
597
|
+
throw new M2Error(
|
|
598
|
+
`summarize(): chain method ${op.name} does not exist on DataCalc`
|
|
599
|
+
);
|
|
600
|
+
}
|
|
601
|
+
const res = method.apply(current, op.args);
|
|
602
|
+
if (res instanceof DataCalc) {
|
|
603
|
+
current = res;
|
|
604
|
+
continue;
|
|
605
|
+
}
|
|
606
|
+
if (Array.isArray(res)) {
|
|
607
|
+
evaluated = res.length;
|
|
608
|
+
break;
|
|
609
|
+
}
|
|
610
|
+
if (typeof res === "boolean") {
|
|
611
|
+
evaluated = res ? 1 : 0;
|
|
612
|
+
break;
|
|
613
|
+
}
|
|
614
|
+
evaluated = typeof res === "number" ? res : Number(res);
|
|
615
|
+
break;
|
|
616
|
+
}
|
|
617
|
+
if (evaluated === void 0) {
|
|
618
|
+
evaluated = current instanceof DataCalc ? current.length : 0;
|
|
619
|
+
}
|
|
620
|
+
sum2 += evaluated;
|
|
621
|
+
}
|
|
622
|
+
let leftover = value;
|
|
623
|
+
for (const m of matches) leftover = leftover.replace(m[0], "");
|
|
624
|
+
const leftoverNum = Number(leftover);
|
|
625
|
+
if (!Number.isNaN(leftoverNum)) sum2 += leftoverNum;
|
|
626
|
+
summaryObj[newVariable] = sum2;
|
|
627
|
+
for (const m of matches) {
|
|
628
|
+
clearChainOps(m[1]);
|
|
629
|
+
}
|
|
630
|
+
continue;
|
|
631
|
+
}
|
|
632
|
+
}
|
|
360
633
|
summaryObj[newVariable] = value;
|
|
361
634
|
}
|
|
362
635
|
}
|
|
363
636
|
summarizedObservations.push(summaryObj);
|
|
364
637
|
});
|
|
365
|
-
return new DataCalc(summarizedObservations, {
|
|
638
|
+
return new DataCalc(summarizedObservations, {
|
|
639
|
+
groups: this._groups,
|
|
640
|
+
warnings: this._warnings
|
|
641
|
+
});
|
|
366
642
|
}
|
|
367
643
|
/**
|
|
368
644
|
* Selects specific variables to keep in the dataset.
|
|
@@ -415,7 +691,10 @@ class DataCalc {
|
|
|
415
691
|
}
|
|
416
692
|
return newObservation;
|
|
417
693
|
});
|
|
418
|
-
return new DataCalc(newObservations, {
|
|
694
|
+
return new DataCalc(newObservations, {
|
|
695
|
+
groups: this._groups,
|
|
696
|
+
warnings: this._warnings
|
|
697
|
+
});
|
|
419
698
|
}
|
|
420
699
|
/**
|
|
421
700
|
* Arranges (sorts) the observations based on one or more variables.
|
|
@@ -467,7 +746,10 @@ class DataCalc {
|
|
|
467
746
|
}
|
|
468
747
|
return 0;
|
|
469
748
|
});
|
|
470
|
-
return new DataCalc(sortedObservations, {
|
|
749
|
+
return new DataCalc(sortedObservations, {
|
|
750
|
+
groups: this._groups,
|
|
751
|
+
warnings: this._warnings
|
|
752
|
+
});
|
|
471
753
|
}
|
|
472
754
|
/**
|
|
473
755
|
* Keeps only unique/distinct observations.
|
|
@@ -496,11 +778,19 @@ class DataCalc {
|
|
|
496
778
|
seen.add(key);
|
|
497
779
|
return true;
|
|
498
780
|
});
|
|
499
|
-
return new DataCalc(uniqueObs, {
|
|
781
|
+
return new DataCalc(uniqueObs, {
|
|
782
|
+
groups: this._groups,
|
|
783
|
+
warnings: this._warnings
|
|
784
|
+
});
|
|
500
785
|
}
|
|
501
786
|
/**
|
|
502
787
|
* Renames variables in the observations.
|
|
503
788
|
*
|
|
789
|
+
* @remarks If a target name in `renames` already exists in the dataset (and is not
|
|
790
|
+
* simply the source name being renamed), the rename will overwrite the existing
|
|
791
|
+
* variable. When warnings are enabled, a warning will be logged to make users aware
|
|
792
|
+
* of the potential overwrite.
|
|
793
|
+
*
|
|
504
794
|
* @param renames - Object mapping new variable names to old variable names
|
|
505
795
|
* @returns A new DataCalc object with renamed variables
|
|
506
796
|
*
|
|
@@ -522,21 +812,37 @@ class DataCalc {
|
|
|
522
812
|
Object.values(renames).forEach((oldName) => {
|
|
523
813
|
this.verifyObservationsContainVariable(oldName);
|
|
524
814
|
});
|
|
815
|
+
if (this._observations.length > 0) {
|
|
816
|
+
const existingKeys = new Set(Object.keys(this._observations[0]));
|
|
817
|
+
const oldNames = new Set(Object.values(renames));
|
|
818
|
+
const collisions = Object.keys(renames).filter(
|
|
819
|
+
(n2) => existingKeys.has(n2) && !oldNames.has(n2)
|
|
820
|
+
);
|
|
821
|
+
if (collisions.length > 0 && this._warnings) {
|
|
822
|
+
console.warn(
|
|
823
|
+
`DataCalc.rename(): renaming will overwrite existing variables: ${collisions.join(", ")}`
|
|
824
|
+
);
|
|
825
|
+
}
|
|
826
|
+
}
|
|
525
827
|
const newObservations = this._observations.map((observation) => {
|
|
526
828
|
const newObservation = {};
|
|
829
|
+
const newNames = new Set(Object.keys(renames));
|
|
527
830
|
for (const [key, value] of Object.entries(observation)) {
|
|
528
831
|
const newKey = Object.entries(renames).find(
|
|
529
832
|
([, old]) => old === key
|
|
530
833
|
)?.[0];
|
|
531
834
|
if (newKey) {
|
|
532
835
|
newObservation[newKey] = value;
|
|
533
|
-
} else if (!
|
|
836
|
+
} else if (!newNames.has(key)) {
|
|
534
837
|
newObservation[key] = value;
|
|
535
838
|
}
|
|
536
839
|
}
|
|
537
840
|
return newObservation;
|
|
538
841
|
});
|
|
539
|
-
return new DataCalc(newObservations, {
|
|
842
|
+
return new DataCalc(newObservations, {
|
|
843
|
+
groups: this._groups,
|
|
844
|
+
warnings: this._warnings
|
|
845
|
+
});
|
|
540
846
|
}
|
|
541
847
|
/**
|
|
542
848
|
* Performs an inner join with another DataCalc object.
|
|
@@ -876,7 +1182,10 @@ class DataCalc {
|
|
|
876
1182
|
}
|
|
877
1183
|
let sliced;
|
|
878
1184
|
if (start >= this._observations.length) {
|
|
879
|
-
return new DataCalc([], {
|
|
1185
|
+
return new DataCalc([], {
|
|
1186
|
+
groups: this._groups,
|
|
1187
|
+
warnings: this._warnings
|
|
1188
|
+
});
|
|
880
1189
|
}
|
|
881
1190
|
if (end === void 0) {
|
|
882
1191
|
const index = start < 0 ? this._observations.length + start : start;
|
|
@@ -884,7 +1193,10 @@ class DataCalc {
|
|
|
884
1193
|
} else {
|
|
885
1194
|
sliced = this._observations.slice(start, end);
|
|
886
1195
|
}
|
|
887
|
-
return new DataCalc(sliced, {
|
|
1196
|
+
return new DataCalc(sliced, {
|
|
1197
|
+
groups: this._groups,
|
|
1198
|
+
warnings: this._warnings
|
|
1199
|
+
});
|
|
888
1200
|
}
|
|
889
1201
|
/**
|
|
890
1202
|
* Combines observations from two DataCalc objects by rows.
|
|
@@ -1046,6 +1358,26 @@ class DataCalc {
|
|
|
1046
1358
|
* @returns a deep copy of the object
|
|
1047
1359
|
*/
|
|
1048
1360
|
deepCopy(source, map = /* @__PURE__ */ new WeakMap()) {
|
|
1361
|
+
const sClone = globalThis.structuredClone;
|
|
1362
|
+
if (typeof sClone === "function") {
|
|
1363
|
+
try {
|
|
1364
|
+
const proto = source && typeof source === "object" ? Object.getPrototypeOf(source) : null;
|
|
1365
|
+
const isPlain = Array.isArray(source) || proto === Object.prototype || proto === null;
|
|
1366
|
+
if (isPlain) {
|
|
1367
|
+
const descriptors = Object.getOwnPropertyDescriptors(
|
|
1368
|
+
source
|
|
1369
|
+
);
|
|
1370
|
+
const hasAccessors = Object.values(descriptors).some(
|
|
1371
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1372
|
+
(d) => typeof d.get === "function" || typeof d.set === "function"
|
|
1373
|
+
);
|
|
1374
|
+
if (!hasAccessors) {
|
|
1375
|
+
return sClone(source);
|
|
1376
|
+
}
|
|
1377
|
+
}
|
|
1378
|
+
} catch {
|
|
1379
|
+
}
|
|
1380
|
+
}
|
|
1049
1381
|
if (source === null || typeof source !== "object") {
|
|
1050
1382
|
return source;
|
|
1051
1383
|
}
|
|
@@ -1064,8 +1396,9 @@ class DataCalc {
|
|
|
1064
1396
|
key
|
|
1065
1397
|
);
|
|
1066
1398
|
if (descriptor) {
|
|
1399
|
+
const { get, set, ...descWithoutAccessors } = descriptor;
|
|
1067
1400
|
Object.defineProperty(copy, key, {
|
|
1068
|
-
...
|
|
1401
|
+
...descWithoutAccessors,
|
|
1069
1402
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1070
1403
|
value: this.deepCopy(source[key], map)
|
|
1071
1404
|
});
|
|
@@ -1085,7 +1418,613 @@ class DataCalc {
|
|
|
1085
1418
|
return keys.some((key) => obs[key] === null || obs[key] === void 0);
|
|
1086
1419
|
}
|
|
1087
1420
|
}
|
|
1088
|
-
|
|
1421
|
+
const PRECEDENCE = {
|
|
1422
|
+
"+": 1,
|
|
1423
|
+
"-": 1,
|
|
1424
|
+
"*": 2,
|
|
1425
|
+
"/": 2,
|
|
1426
|
+
"^": 3
|
|
1427
|
+
};
|
|
1428
|
+
class SummarizeOperation {
|
|
1429
|
+
constructor(leafFn, parameters, options, tokens) {
|
|
1430
|
+
this.leafFn = leafFn;
|
|
1431
|
+
this.parameters = parameters;
|
|
1432
|
+
this.options = options;
|
|
1433
|
+
if (tokens && tokens.length > 0) {
|
|
1434
|
+
this.tokens = tokens.slice();
|
|
1435
|
+
} else if (leafFn) {
|
|
1436
|
+
this.tokens = [{ t: "operand", v: this }];
|
|
1437
|
+
} else {
|
|
1438
|
+
this.tokens = [];
|
|
1439
|
+
}
|
|
1440
|
+
this.summarizeFunction = (dc) => {
|
|
1441
|
+
return this.evaluateAsValue(dc);
|
|
1442
|
+
};
|
|
1443
|
+
}
|
|
1444
|
+
// Factory for creating a leaf SummarizeOperation (use in helpers)
|
|
1445
|
+
static leaf(leafFn, parameters, options) {
|
|
1446
|
+
return new SummarizeOperation(leafFn, parameters, options);
|
|
1447
|
+
}
|
|
1448
|
+
// clone with new token stream (immutable-ish)
|
|
1449
|
+
cloneWithTokens(newTokens) {
|
|
1450
|
+
return new SummarizeOperation(void 0, void 0, void 0, newTokens);
|
|
1451
|
+
}
|
|
1452
|
+
// append operator + operand (operand can be number or SummarizeOperation)
|
|
1453
|
+
appendOp(op, operand) {
|
|
1454
|
+
const newTokens = this.tokens.slice();
|
|
1455
|
+
newTokens.push({ t: "op", v: op });
|
|
1456
|
+
newTokens.push({ t: "operand", v: operand });
|
|
1457
|
+
return this.cloneWithTokens(newTokens);
|
|
1458
|
+
}
|
|
1459
|
+
/**
|
|
1460
|
+
* Append addition to this expression.
|
|
1461
|
+
*
|
|
1462
|
+
* @param x - A numeric literal or another `SummarizeOperation` to add to this expression
|
|
1463
|
+
* @returns A new `SummarizeOperation` representing `(this + x)`
|
|
1464
|
+
*
|
|
1465
|
+
* @example
|
|
1466
|
+
* ```js
|
|
1467
|
+
* const d = [
|
|
1468
|
+
* { a: 1, b: 2, c: 3 },
|
|
1469
|
+
* { a: 0, b: 8, c: 3 },
|
|
1470
|
+
* { a: 9, b: 4, c: 7 },
|
|
1471
|
+
* ];
|
|
1472
|
+
* const dc = new DataCalc(d);
|
|
1473
|
+
* console.log(
|
|
1474
|
+
* dc.summarize({
|
|
1475
|
+
* result: mean("a").add(10),
|
|
1476
|
+
* }).observations
|
|
1477
|
+
* );
|
|
1478
|
+
* // [ { result: 13.33333 } ]
|
|
1479
|
+
* ```
|
|
1480
|
+
*/
|
|
1481
|
+
add(x) {
|
|
1482
|
+
return this.appendOp("+", x);
|
|
1483
|
+
}
|
|
1484
|
+
/**
|
|
1485
|
+
* Append subtraction to this expression.
|
|
1486
|
+
*
|
|
1487
|
+
* @param x - A numeric literal or another `SummarizeOperation` to subtract from this expression
|
|
1488
|
+
* @returns A new `SummarizeOperation` representing `(this - x)`
|
|
1489
|
+
*
|
|
1490
|
+
* @example
|
|
1491
|
+
* ```js
|
|
1492
|
+
* const d = [
|
|
1493
|
+
* { a: 1, b: 2, c: 3 },
|
|
1494
|
+
* { a: 0, b: 8, c: 3 },
|
|
1495
|
+
* { a: 9, b: 4, c: 7 },
|
|
1496
|
+
* ];
|
|
1497
|
+
* const dc = new DataCalc(d);
|
|
1498
|
+
* console.log(
|
|
1499
|
+
* dc.summarize({
|
|
1500
|
+
* result: mean("a").sub(10),
|
|
1501
|
+
* }).observations
|
|
1502
|
+
* );
|
|
1503
|
+
* // [ { result: -6.6667 } ]
|
|
1504
|
+
* ```
|
|
1505
|
+
*/
|
|
1506
|
+
sub(x) {
|
|
1507
|
+
return this.appendOp("-", x);
|
|
1508
|
+
}
|
|
1509
|
+
/**
|
|
1510
|
+
* Append multiplication to this expression. Multiplication has higher
|
|
1511
|
+
* precedence than addition/subtraction.
|
|
1512
|
+
*
|
|
1513
|
+
* @param x - A numeric literal or another `SummarizeOperation` to multiply with this expression
|
|
1514
|
+
* @returns A new `SummarizeOperation` representing `(this * x)`
|
|
1515
|
+
*
|
|
1516
|
+
* @example
|
|
1517
|
+
* ```js
|
|
1518
|
+
* const d = [
|
|
1519
|
+
* { a: 1, b: 2, c: 3 },
|
|
1520
|
+
* { a: 0, b: 8, c: 3 },
|
|
1521
|
+
* { a: 9, b: 4, c: 7 },
|
|
1522
|
+
* ];
|
|
1523
|
+
* const dc = new DataCalc(d);
|
|
1524
|
+
* console.log(
|
|
1525
|
+
* dc.summarize({
|
|
1526
|
+
* result: mean("a").mul(10),
|
|
1527
|
+
* }).observations
|
|
1528
|
+
* );
|
|
1529
|
+
* // [ { result: 33.3333 } ]
|
|
1530
|
+
* ```
|
|
1531
|
+
*/
|
|
1532
|
+
mul(x) {
|
|
1533
|
+
return this.appendOp("*", x);
|
|
1534
|
+
}
|
|
1535
|
+
/**
|
|
1536
|
+
* Append division to this expression.
|
|
1537
|
+
*
|
|
1538
|
+
* @param x - A numeric literal or another `SummarizeOperation` to divide this expression by
|
|
1539
|
+
* @returns A new `SummarizeOperation` representing `(this / x)`
|
|
1540
|
+
*
|
|
1541
|
+
* @example
|
|
1542
|
+
* ```js
|
|
1543
|
+
* const d = [
|
|
1544
|
+
* { a: 1, b: 2, c: 3 },
|
|
1545
|
+
* { a: 0, b: 8, c: 3 },
|
|
1546
|
+
* { a: 9, b: 4, c: 7 },
|
|
1547
|
+
* ];
|
|
1548
|
+
* const dc = new DataCalc(d);
|
|
1549
|
+
* console.log(
|
|
1550
|
+
* dc.summarize({
|
|
1551
|
+
* result: mean("a").div(10),
|
|
1552
|
+
* }).observations
|
|
1553
|
+
* );
|
|
1554
|
+
* // [ { result: .3333 } ]
|
|
1555
|
+
* ```
|
|
1556
|
+
*/
|
|
1557
|
+
div(x) {
|
|
1558
|
+
return this.appendOp("/", x);
|
|
1559
|
+
}
|
|
1560
|
+
/**
|
|
1561
|
+
* Append exponentiation (power) to this expression.
|
|
1562
|
+
*
|
|
1563
|
+
* Note: exponentiation uses right-associative semantics (a ^ b ^ c -> a ^ (b ^ c)).
|
|
1564
|
+
*
|
|
1565
|
+
* @param x - A numeric literal or another `SummarizeOperation` used as the exponent
|
|
1566
|
+
* @returns A new `SummarizeOperation` representing `(this ^ x)`
|
|
1567
|
+
*
|
|
1568
|
+
* @example
|
|
1569
|
+
* ```js
|
|
1570
|
+
* const d = [
|
|
1571
|
+
* { a: 1, b: 2, c: 3 },
|
|
1572
|
+
* { a: 0, b: 8, c: 3 },
|
|
1573
|
+
* { a: 9, b: 4, c: 7 },
|
|
1574
|
+
* ];
|
|
1575
|
+
* const dc = new DataCalc(d);
|
|
1576
|
+
* console.log(
|
|
1577
|
+
* dc.summarize({
|
|
1578
|
+
* result: mean("a").pow(2),
|
|
1579
|
+
* }).observations
|
|
1580
|
+
* );
|
|
1581
|
+
* // [ { result: 11.1111 } ]
|
|
1582
|
+
* ```
|
|
1583
|
+
*/
|
|
1584
|
+
pow(x) {
|
|
1585
|
+
return this.appendOp("^", x);
|
|
1586
|
+
}
|
|
1587
|
+
// Evaluate an operand token to a number (returns NaN for non-numeric)
|
|
1588
|
+
evaluateOperandToNumber(opd, dc) {
|
|
1589
|
+
if (!this._usedChainIds) this._usedChainIds = /* @__PURE__ */ new Set();
|
|
1590
|
+
if (typeof opd === "number") return opd;
|
|
1591
|
+
if (typeof opd === "string") {
|
|
1592
|
+
const re = /^__CHAIN_EXPR__\[(.*?)\]$/;
|
|
1593
|
+
const m = re.exec(opd);
|
|
1594
|
+
if (m) {
|
|
1595
|
+
const payload = m[1];
|
|
1596
|
+
let ops = getChainOps(payload);
|
|
1597
|
+
if (ops) this._usedChainIds?.add(payload);
|
|
1598
|
+
if (!ops) {
|
|
1599
|
+
try {
|
|
1600
|
+
ops = JSON.parse(decodeURIComponent(payload));
|
|
1601
|
+
} catch {
|
|
1602
|
+
ops = void 0;
|
|
1603
|
+
}
|
|
1604
|
+
}
|
|
1605
|
+
if (!ops) return NaN;
|
|
1606
|
+
let current = dc;
|
|
1607
|
+
let evaluated = void 0;
|
|
1608
|
+
for (const op of ops) {
|
|
1609
|
+
const method = current[op.name];
|
|
1610
|
+
if (typeof method !== "function") return NaN;
|
|
1611
|
+
const res = method.apply(current, op.args);
|
|
1612
|
+
if (res instanceof DataCalc) {
|
|
1613
|
+
current = res;
|
|
1614
|
+
continue;
|
|
1615
|
+
}
|
|
1616
|
+
if (Array.isArray(res)) {
|
|
1617
|
+
evaluated = res.length;
|
|
1618
|
+
break;
|
|
1619
|
+
}
|
|
1620
|
+
if (typeof res === "boolean") {
|
|
1621
|
+
const coerce = this.options && typeof this.options.coerceBooleans === "boolean" ? this.options.coerceBooleans : true;
|
|
1622
|
+
if (coerce) {
|
|
1623
|
+
evaluated = res ? 1 : 0;
|
|
1624
|
+
break;
|
|
1625
|
+
} else {
|
|
1626
|
+
return NaN;
|
|
1627
|
+
}
|
|
1628
|
+
}
|
|
1629
|
+
evaluated = typeof res === "number" ? res : Number(res);
|
|
1630
|
+
break;
|
|
1631
|
+
}
|
|
1632
|
+
if (evaluated === void 0)
|
|
1633
|
+
evaluated = current instanceof DataCalc ? current.length : NaN;
|
|
1634
|
+
return typeof evaluated === "number" && !isNaN(evaluated) ? evaluated : NaN;
|
|
1635
|
+
}
|
|
1636
|
+
}
|
|
1637
|
+
const params = Array.isArray(opd.parameters) ? opd.parameters : opd.parameters === void 0 ? void 0 : [opd.parameters];
|
|
1638
|
+
const raw = opd.leafFn ? opd.leafFn(dc, params, opd.options) : opd.evaluateAsValue(dc);
|
|
1639
|
+
if (raw === null || raw === void 0) return NaN;
|
|
1640
|
+
const num = Number(raw);
|
|
1641
|
+
return typeof num === "number" && !isNaN(num) ? num : NaN;
|
|
1642
|
+
}
|
|
1643
|
+
// Evaluate a flat token stream (no nested handling required here; nested SummarizeOperation
|
|
1644
|
+
// operands will evaluate themselves recursively)
|
|
1645
|
+
evaluateFlatTokens(tokens, dc) {
|
|
1646
|
+
const operands = [];
|
|
1647
|
+
const ops = [];
|
|
1648
|
+
for (const tk of tokens) {
|
|
1649
|
+
if (tk.t === "operand") operands.push(tk.v);
|
|
1650
|
+
else ops.push(tk.v);
|
|
1651
|
+
}
|
|
1652
|
+
if (operands.length === 0) return NaN;
|
|
1653
|
+
while (ops.length > 0) {
|
|
1654
|
+
let bestIdx = 0;
|
|
1655
|
+
let bestPrec = PRECEDENCE[ops[0]] ?? 0;
|
|
1656
|
+
for (let i = 1; i < ops.length; i++) {
|
|
1657
|
+
const p = PRECEDENCE[ops[i]] ?? 0;
|
|
1658
|
+
if (p > bestPrec || p === bestPrec && ops[i] === "^") {
|
|
1659
|
+
bestPrec = p;
|
|
1660
|
+
bestIdx = i;
|
|
1661
|
+
}
|
|
1662
|
+
}
|
|
1663
|
+
const op = ops.splice(bestIdx, 1)[0];
|
|
1664
|
+
const left = operands.splice(bestIdx, 1)[0];
|
|
1665
|
+
const right = operands.splice(bestIdx, 1)[0];
|
|
1666
|
+
const a = this.evaluateOperandToNumber(left, dc);
|
|
1667
|
+
const b = this.evaluateOperandToNumber(right, dc);
|
|
1668
|
+
let res2 = NaN;
|
|
1669
|
+
if (op === "+") res2 = a + b;
|
|
1670
|
+
else if (op === "-") res2 = a - b;
|
|
1671
|
+
else if (op === "*") res2 = a * b;
|
|
1672
|
+
else if (op === "/") res2 = b === 0 ? NaN : a / b;
|
|
1673
|
+
else if (op === "^") res2 = Math.pow(a, b);
|
|
1674
|
+
operands.splice(bestIdx, 0, res2);
|
|
1675
|
+
}
|
|
1676
|
+
const final = operands[0];
|
|
1677
|
+
if (typeof final === "number") {
|
|
1678
|
+
if (this._usedChainIds) {
|
|
1679
|
+
for (const id of this._usedChainIds) clearChainOps(id);
|
|
1680
|
+
this._usedChainIds = void 0;
|
|
1681
|
+
}
|
|
1682
|
+
return final;
|
|
1683
|
+
}
|
|
1684
|
+
const res = this.evaluateOperandToNumber(final, dc);
|
|
1685
|
+
if (this._usedChainIds) {
|
|
1686
|
+
for (const id of this._usedChainIds) clearChainOps(id);
|
|
1687
|
+
this._usedChainIds = void 0;
|
|
1688
|
+
}
|
|
1689
|
+
return res;
|
|
1690
|
+
}
|
|
1691
|
+
/**
|
|
1692
|
+
* Instance helper: return a grouped version of this operation so it becomes
|
|
1693
|
+
* a single operand in outer expressions. Equivalent to parens(this).
|
|
1694
|
+
*/
|
|
1695
|
+
parens() {
|
|
1696
|
+
return parens(this);
|
|
1697
|
+
}
|
|
1698
|
+
// Top-level evaluation: handles the case where this instance is a leaf (leafFn present)
|
|
1699
|
+
// or an expression token stream.
|
|
1700
|
+
evaluateAsValue(dc) {
|
|
1701
|
+
if (this.leafFn) {
|
|
1702
|
+
const params = Array.isArray(this.parameters) ? this.parameters : this.parameters === void 0 ? void 0 : [this.parameters];
|
|
1703
|
+
const res = this.leafFn(dc, params, this.options);
|
|
1704
|
+
if (typeof res === "number" && Number.isNaN(res)) return null;
|
|
1705
|
+
return res;
|
|
1706
|
+
}
|
|
1707
|
+
if (!this.tokens || this.tokens.length === 0) return NaN;
|
|
1708
|
+
const val = this.evaluateFlatTokens(this.tokens, dc);
|
|
1709
|
+
if (typeof val === "number" && Number.isNaN(val)) return null;
|
|
1710
|
+
return val;
|
|
1711
|
+
}
|
|
1712
|
+
}
|
|
1713
|
+
function parens(op) {
|
|
1714
|
+
const tokens = [{ t: "operand", v: op }];
|
|
1715
|
+
return new SummarizeOperation(void 0, void 0, void 0, tokens);
|
|
1716
|
+
}
|
|
1717
|
+
const DEFAULT_SUMMARIZE_OPTIONS = {
|
|
1718
|
+
coerceBooleans: true,
|
|
1719
|
+
skipMissing: false
|
|
1720
|
+
};
|
|
1721
|
+
function applyDefaultOptions(options) {
|
|
1722
|
+
return { ...DEFAULT_SUMMARIZE_OPTIONS, ...options };
|
|
1723
|
+
}
|
|
1724
|
+
function resolveLazy(param, dataCalc, opName) {
|
|
1725
|
+
if (typeof param !== "function") {
|
|
1726
|
+
return param;
|
|
1727
|
+
}
|
|
1728
|
+
try {
|
|
1729
|
+
const res = param(dataCalc);
|
|
1730
|
+
if (typeof res === "function") {
|
|
1731
|
+
throw new M2Error(
|
|
1732
|
+
`${opName || "summarize()"}: lazy callback returned a function; expected a value or array of values.`
|
|
1733
|
+
);
|
|
1734
|
+
}
|
|
1735
|
+
if (res instanceof DataCalc) {
|
|
1736
|
+
throw new M2Error(
|
|
1737
|
+
`${opName || "summarize()"}: lazy callback returned a DataCalc instance; expected a value or array of values.`
|
|
1738
|
+
);
|
|
1739
|
+
}
|
|
1740
|
+
return res;
|
|
1741
|
+
} catch (err) {
|
|
1742
|
+
throw new M2Error(
|
|
1743
|
+
`${"summarize()"}: lazy callback threw an error: ${err && err.message ? err.message : String(err)}`
|
|
1744
|
+
);
|
|
1745
|
+
}
|
|
1746
|
+
}
|
|
1747
|
+
function processNumericValues(dataCalc, variable, options, collector, errorPrefix, initialState) {
|
|
1748
|
+
const mergedOptions = applyDefaultOptions(options);
|
|
1749
|
+
dataCalc.verifyObservationsContainVariable(variable);
|
|
1750
|
+
let count = 0;
|
|
1751
|
+
let state = initialState;
|
|
1752
|
+
let containsMissing = false;
|
|
1753
|
+
dataCalc.observations.forEach((o) => {
|
|
1754
|
+
if (dataCalc.isNonMissingNumeric(o[variable])) {
|
|
1755
|
+
state = collector(o[variable], state);
|
|
1756
|
+
count++;
|
|
1757
|
+
return;
|
|
1758
|
+
}
|
|
1759
|
+
if (typeof o[variable] === "boolean" && mergedOptions.coerceBooleans) {
|
|
1760
|
+
state = collector(o[variable] ? 1 : 0, state);
|
|
1761
|
+
count++;
|
|
1762
|
+
return;
|
|
1763
|
+
}
|
|
1764
|
+
if (dataCalc.isMissingNumeric(o[variable])) {
|
|
1765
|
+
containsMissing = true;
|
|
1766
|
+
return;
|
|
1767
|
+
}
|
|
1768
|
+
throw new M2Error(
|
|
1769
|
+
`${errorPrefix}: variable ${variable} has non-numeric value ${o[variable]} in this observation: ${JSON.stringify(o)}`
|
|
1770
|
+
);
|
|
1771
|
+
});
|
|
1772
|
+
return { state, count, containsMissing };
|
|
1773
|
+
}
|
|
1774
|
+
function processSingleValue(value, options, errorPrefix) {
|
|
1775
|
+
const mergedOptions = applyDefaultOptions(options);
|
|
1776
|
+
if (typeof value === "number" && !isNaN(value) && isFinite(value)) {
|
|
1777
|
+
return { value, isMissing: false };
|
|
1778
|
+
} else if (typeof value === "boolean" && mergedOptions.coerceBooleans) {
|
|
1779
|
+
return { value: value ? 1 : 0, isMissing: false };
|
|
1780
|
+
} else if (value === null || value === void 0 || typeof value === "number" && (isNaN(value) || !isFinite(value))) {
|
|
1781
|
+
return { value: 0, isMissing: true };
|
|
1782
|
+
} else {
|
|
1783
|
+
throw new M2Error(`${errorPrefix}: has non-numeric value ${value}`);
|
|
1784
|
+
}
|
|
1785
|
+
}
|
|
1786
|
+
const varianceInternal = (dataCalc, params, options) => {
|
|
1787
|
+
let variableOrValues = params ? params[0] : void 0;
|
|
1788
|
+
variableOrValues = resolveLazy(variableOrValues, dataCalc);
|
|
1789
|
+
const mergedOptions = applyDefaultOptions(options);
|
|
1790
|
+
if (typeof variableOrValues === "string") {
|
|
1791
|
+
if (!dataCalc.variableExists(variableOrValues)) {
|
|
1792
|
+
return null;
|
|
1793
|
+
}
|
|
1794
|
+
const variable = variableOrValues;
|
|
1795
|
+
const meanResult = processNumericValues(
|
|
1796
|
+
dataCalc,
|
|
1797
|
+
variable,
|
|
1798
|
+
options,
|
|
1799
|
+
(value, sum2) => sum2 + value,
|
|
1800
|
+
"variance()",
|
|
1801
|
+
0
|
|
1802
|
+
);
|
|
1803
|
+
if (meanResult.containsMissing && !mergedOptions.skipMissing) {
|
|
1804
|
+
return null;
|
|
1805
|
+
}
|
|
1806
|
+
if (meanResult.count <= 1) {
|
|
1807
|
+
return null;
|
|
1808
|
+
}
|
|
1809
|
+
const meanValue = meanResult.state / meanResult.count;
|
|
1810
|
+
const varianceResult = processNumericValues(
|
|
1811
|
+
dataCalc,
|
|
1812
|
+
variable,
|
|
1813
|
+
options,
|
|
1814
|
+
(value, sum2) => {
|
|
1815
|
+
const actualValue = typeof value === "boolean" && mergedOptions.coerceBooleans ? value ? 1 : 0 : value;
|
|
1816
|
+
return sum2 + Math.pow(actualValue - meanValue, 2);
|
|
1817
|
+
},
|
|
1818
|
+
"variance()",
|
|
1819
|
+
0
|
|
1820
|
+
);
|
|
1821
|
+
return varianceResult.state / (meanResult.count - 1);
|
|
1822
|
+
} else if (Array.isArray(variableOrValues)) {
|
|
1823
|
+
const validValues = [];
|
|
1824
|
+
let containsMissing = false;
|
|
1825
|
+
for (const value of variableOrValues) {
|
|
1826
|
+
if (typeof value === "number" && !isNaN(value) && isFinite(value)) {
|
|
1827
|
+
validValues.push(value);
|
|
1828
|
+
} else if (typeof value === "boolean" && mergedOptions.coerceBooleans) {
|
|
1829
|
+
validValues.push(value ? 1 : 0);
|
|
1830
|
+
} else if (value === null || value === void 0 || typeof value === "number" && (isNaN(value) || !isFinite(value))) {
|
|
1831
|
+
containsMissing = true;
|
|
1832
|
+
} else {
|
|
1833
|
+
throw new M2Error(`variance(): has non-numeric value ${value}`);
|
|
1834
|
+
}
|
|
1835
|
+
}
|
|
1836
|
+
if (containsMissing && !mergedOptions.skipMissing) {
|
|
1837
|
+
return null;
|
|
1838
|
+
}
|
|
1839
|
+
if (validValues.length <= 1) {
|
|
1840
|
+
return null;
|
|
1841
|
+
}
|
|
1842
|
+
const sum2 = validValues.reduce((acc, val) => acc + val, 0);
|
|
1843
|
+
const mean2 = sum2 / validValues.length;
|
|
1844
|
+
const sumSquaredDiffs = validValues.reduce(
|
|
1845
|
+
(acc, val) => acc + Math.pow(val - mean2, 2),
|
|
1846
|
+
0
|
|
1847
|
+
);
|
|
1848
|
+
return sumSquaredDiffs / (validValues.length - 1);
|
|
1849
|
+
} else {
|
|
1850
|
+
return null;
|
|
1851
|
+
}
|
|
1852
|
+
};
|
|
1853
|
+
const medianInternal = (dataCalc, params, options) => {
|
|
1854
|
+
let variableOrValues = params ? params[0] : void 0;
|
|
1855
|
+
variableOrValues = resolveLazy(variableOrValues, dataCalc);
|
|
1856
|
+
const mergedOptions = applyDefaultOptions(options);
|
|
1857
|
+
if (typeof variableOrValues === "string") {
|
|
1858
|
+
if (!dataCalc.variableExists(variableOrValues)) {
|
|
1859
|
+
return null;
|
|
1860
|
+
}
|
|
1861
|
+
const variable = variableOrValues;
|
|
1862
|
+
dataCalc.verifyObservationsContainVariable(variable);
|
|
1863
|
+
const values = [];
|
|
1864
|
+
let containsMissing = false;
|
|
1865
|
+
dataCalc.observations.forEach((o) => {
|
|
1866
|
+
if (dataCalc.isNonMissingNumeric(o[variable])) {
|
|
1867
|
+
values.push(o[variable]);
|
|
1868
|
+
} else if (typeof o[variable] === "boolean" && mergedOptions.coerceBooleans) {
|
|
1869
|
+
values.push(o[variable] ? 1 : 0);
|
|
1870
|
+
} else if (dataCalc.isMissingNumeric(o[variable])) {
|
|
1871
|
+
containsMissing = true;
|
|
1872
|
+
} else {
|
|
1873
|
+
throw new M2Error(
|
|
1874
|
+
`median(): variable ${variable} has non-numeric value ${o[variable]} in this observation: ${JSON.stringify(o)}`
|
|
1875
|
+
);
|
|
1876
|
+
}
|
|
1877
|
+
});
|
|
1878
|
+
if (containsMissing && !mergedOptions.skipMissing) {
|
|
1879
|
+
return null;
|
|
1880
|
+
}
|
|
1881
|
+
if (values.length === 0) {
|
|
1882
|
+
return null;
|
|
1883
|
+
}
|
|
1884
|
+
values.sort((a, b) => a - b);
|
|
1885
|
+
const mid = Math.floor(values.length / 2);
|
|
1886
|
+
if (values.length % 2 === 0) {
|
|
1887
|
+
return (values[mid - 1] + values[mid]) / 2;
|
|
1888
|
+
} else {
|
|
1889
|
+
return values[mid];
|
|
1890
|
+
}
|
|
1891
|
+
} else if (Array.isArray(variableOrValues)) {
|
|
1892
|
+
const values = [];
|
|
1893
|
+
let containsMissing = false;
|
|
1894
|
+
for (const value of variableOrValues) {
|
|
1895
|
+
if (typeof value === "number" && !isNaN(value) && isFinite(value)) {
|
|
1896
|
+
values.push(value);
|
|
1897
|
+
} else if (typeof value === "boolean" && mergedOptions.coerceBooleans) {
|
|
1898
|
+
values.push(value ? 1 : 0);
|
|
1899
|
+
} else if (value === null || value === void 0 || typeof value === "number" && (isNaN(value) || !isFinite(value))) {
|
|
1900
|
+
containsMissing = true;
|
|
1901
|
+
} else {
|
|
1902
|
+
throw new M2Error(`median(): has non-numeric value ${value}`);
|
|
1903
|
+
}
|
|
1904
|
+
}
|
|
1905
|
+
if (containsMissing && !mergedOptions.skipMissing) {
|
|
1906
|
+
return null;
|
|
1907
|
+
}
|
|
1908
|
+
if (values.length === 0) {
|
|
1909
|
+
return null;
|
|
1910
|
+
}
|
|
1911
|
+
values.sort((a, b) => a - b);
|
|
1912
|
+
const mid = Math.floor(values.length / 2);
|
|
1913
|
+
if (values.length % 2 === 0) {
|
|
1914
|
+
return (values[mid - 1] + values[mid]) / 2;
|
|
1915
|
+
} else {
|
|
1916
|
+
return values[mid];
|
|
1917
|
+
}
|
|
1918
|
+
} else {
|
|
1919
|
+
const result = processSingleValue(variableOrValues, options, "median()");
|
|
1920
|
+
if (result.isMissing && !mergedOptions.skipMissing) {
|
|
1921
|
+
return null;
|
|
1922
|
+
}
|
|
1923
|
+
return result.isMissing ? null : result.value;
|
|
1924
|
+
}
|
|
1925
|
+
};
|
|
1926
|
+
function median(variableOrValues, options) {
|
|
1927
|
+
return SummarizeOperation.leaf(medianInternal, [variableOrValues], options);
|
|
1928
|
+
}
|
|
1929
|
+
const sdInternal = (dataCalc, params, options) => {
|
|
1930
|
+
let variableOrValues = params ? params[0] : void 0;
|
|
1931
|
+
variableOrValues = resolveLazy(variableOrValues, dataCalc);
|
|
1932
|
+
if (typeof variableOrValues === "string") {
|
|
1933
|
+
if (!dataCalc.variableExists(variableOrValues)) {
|
|
1934
|
+
return null;
|
|
1935
|
+
}
|
|
1936
|
+
const varianceValue = varianceInternal(dataCalc, params, options);
|
|
1937
|
+
if (varianceValue === null) {
|
|
1938
|
+
return null;
|
|
1939
|
+
}
|
|
1940
|
+
return Math.sqrt(varianceValue);
|
|
1941
|
+
} else if (Array.isArray(variableOrValues)) {
|
|
1942
|
+
const newParams = params ? [...params] : [variableOrValues];
|
|
1943
|
+
const varianceValue = varianceInternal(dataCalc, newParams, options);
|
|
1944
|
+
if (varianceValue === null) {
|
|
1945
|
+
return null;
|
|
1946
|
+
}
|
|
1947
|
+
return Math.sqrt(varianceValue);
|
|
1948
|
+
} else {
|
|
1949
|
+
return null;
|
|
1950
|
+
}
|
|
1951
|
+
};
|
|
1952
|
+
function sd(variableOrValues, options) {
|
|
1953
|
+
return SummarizeOperation.leaf(sdInternal, [variableOrValues], options);
|
|
1954
|
+
}
|
|
1955
|
+
const scalarInternal = (_dataCalc, params, options) => {
|
|
1956
|
+
let v = params ? params[0] : void 0;
|
|
1957
|
+
if (typeof v === "function") v = v(_dataCalc);
|
|
1958
|
+
if (typeof v === "string") {
|
|
1959
|
+
const re = /^__CHAIN_EXPR__\[(.*?)\]$/;
|
|
1960
|
+
const m = re.exec(v);
|
|
1961
|
+
if (m) {
|
|
1962
|
+
const payload = m[1];
|
|
1963
|
+
let ops = getChainOps(payload);
|
|
1964
|
+
if (!ops) {
|
|
1965
|
+
try {
|
|
1966
|
+
ops = JSON.parse(decodeURIComponent(payload));
|
|
1967
|
+
} catch {
|
|
1968
|
+
ops = void 0;
|
|
1969
|
+
}
|
|
1970
|
+
}
|
|
1971
|
+
if (ops) {
|
|
1972
|
+
let current = _dataCalc;
|
|
1973
|
+
let evaluated = void 0;
|
|
1974
|
+
for (const op of ops) {
|
|
1975
|
+
const method = current[op.name];
|
|
1976
|
+
if (typeof method !== "function") {
|
|
1977
|
+
evaluated = NaN;
|
|
1978
|
+
break;
|
|
1979
|
+
}
|
|
1980
|
+
const res = method.apply(current, op.args);
|
|
1981
|
+
if (res instanceof DataCalc) {
|
|
1982
|
+
current = res;
|
|
1983
|
+
continue;
|
|
1984
|
+
}
|
|
1985
|
+
if (Array.isArray(res)) {
|
|
1986
|
+
evaluated = res.length;
|
|
1987
|
+
break;
|
|
1988
|
+
}
|
|
1989
|
+
if (typeof res === "boolean") {
|
|
1990
|
+
evaluated = res;
|
|
1991
|
+
break;
|
|
1992
|
+
}
|
|
1993
|
+
evaluated = typeof res === "number" ? res : Number(res);
|
|
1994
|
+
break;
|
|
1995
|
+
}
|
|
1996
|
+
if (evaluated === void 0)
|
|
1997
|
+
evaluated = current instanceof DataCalc ? current.length : NaN;
|
|
1998
|
+
if (!Number.isNaN(evaluated)) v = evaluated;
|
|
1999
|
+
else v = null;
|
|
2000
|
+
try {
|
|
2001
|
+
clearChainOps(payload);
|
|
2002
|
+
} catch {
|
|
2003
|
+
}
|
|
2004
|
+
}
|
|
2005
|
+
}
|
|
2006
|
+
}
|
|
2007
|
+
if (typeof v === "object" && v !== null && "summarizeFunction" in v) {
|
|
2008
|
+
try {
|
|
2009
|
+
const op = v;
|
|
2010
|
+
const paramsForOp = Array.isArray(op.parameters) ? op.parameters : op.parameters === void 0 ? void 0 : [op.parameters];
|
|
2011
|
+
const raw = op.summarizeFunction(
|
|
2012
|
+
_dataCalc,
|
|
2013
|
+
paramsForOp,
|
|
2014
|
+
op.options
|
|
2015
|
+
);
|
|
2016
|
+
v = raw;
|
|
2017
|
+
} catch {
|
|
2018
|
+
v = null;
|
|
2019
|
+
}
|
|
2020
|
+
}
|
|
2021
|
+
const result = processSingleValue(v, options, "scalar()");
|
|
2022
|
+
return result.isMissing ? null : result.value;
|
|
2023
|
+
};
|
|
2024
|
+
function scalar(value) {
|
|
2025
|
+
return SummarizeOperation.leaf(scalarInternal, [value], void 0);
|
|
2026
|
+
}
|
|
2027
|
+
console.log("\u26AA @m2c2kit/data-calc version 0.8.7 (38a5862e)");
|
|
1089
2028
|
|
|
1090
2029
|
class ColorShapes extends Game {
|
|
1091
2030
|
constructor() {
|
|
@@ -1198,6 +2137,14 @@ class ColorShapes extends Game {
|
|
|
1198
2137
|
type: "boolean",
|
|
1199
2138
|
default: false,
|
|
1200
2139
|
description: "Should scoring data be generated? Default is false."
|
|
2140
|
+
},
|
|
2141
|
+
scoring_filter_response_time_duration_ms: {
|
|
2142
|
+
type: "array",
|
|
2143
|
+
items: {
|
|
2144
|
+
type: "number"
|
|
2145
|
+
},
|
|
2146
|
+
default: [100, 1e4],
|
|
2147
|
+
description: "When scoring, values of response_time_duration_ms less than the lower bound or greater than the upper bound are discarded. This array contains two numbers, the lower and upper bounds."
|
|
1201
2148
|
}
|
|
1202
2149
|
};
|
|
1203
2150
|
const colorShapesTrialSchema = {
|
|
@@ -1330,6 +2277,14 @@ class ColorShapes extends Game {
|
|
|
1330
2277
|
format: "date-time",
|
|
1331
2278
|
description: "ISO 8601 timestamp at the end of the last trial. Null if no trials were completed."
|
|
1332
2279
|
},
|
|
2280
|
+
response_time_filter_lower_bound: {
|
|
2281
|
+
type: "number",
|
|
2282
|
+
description: "Response times less than this lower bound were discarded when calculating filtered response times."
|
|
2283
|
+
},
|
|
2284
|
+
response_time_filter_upper_bound: {
|
|
2285
|
+
type: "number",
|
|
2286
|
+
description: "Response times greater than this upper bound were discarded when calculating filtered response times."
|
|
2287
|
+
},
|
|
1333
2288
|
n_trials: {
|
|
1334
2289
|
type: "integer",
|
|
1335
2290
|
description: "Number of trials completed."
|
|
@@ -1349,6 +2304,158 @@ class ColorShapes extends Game {
|
|
|
1349
2304
|
participant_score: {
|
|
1350
2305
|
type: ["number", "null"],
|
|
1351
2306
|
description: "Participant-facing score, calculated as (number of correct trials / number of trials attempted) * 100. This is a simple metric to provide feedback to the participant. Null if no trials attempted."
|
|
2307
|
+
},
|
|
2308
|
+
flag_trials_lt_expected: {
|
|
2309
|
+
type: "number",
|
|
2310
|
+
description: "Is the number of completed trials fewer than expected? 1 = true, 0 = false."
|
|
2311
|
+
},
|
|
2312
|
+
flag_trials_gt_expected: {
|
|
2313
|
+
type: "number",
|
|
2314
|
+
description: "Is the number of completed trials greater than expected? 1 = true, 0 = false."
|
|
2315
|
+
},
|
|
2316
|
+
n_trials_HIT: {
|
|
2317
|
+
type: "number",
|
|
2318
|
+
description: "Number of HIT trials (correctly identified a 'different' trial)"
|
|
2319
|
+
},
|
|
2320
|
+
n_trials_MISS: {
|
|
2321
|
+
type: "number",
|
|
2322
|
+
description: "Number of MISS trials (failed to detect a 'different' trial, responded 'same')"
|
|
2323
|
+
},
|
|
2324
|
+
n_trials_FA: {
|
|
2325
|
+
type: "number",
|
|
2326
|
+
description: "Number of False Alarm trials (incorrectly responded 'different' on a 'same' trial)"
|
|
2327
|
+
},
|
|
2328
|
+
n_trials_CR: {
|
|
2329
|
+
type: "number",
|
|
2330
|
+
description: "Number of Correct Rejection trials (correctly responded 'same' on a 'same' trial)"
|
|
2331
|
+
},
|
|
2332
|
+
n_trials_type_same: {
|
|
2333
|
+
type: "number",
|
|
2334
|
+
description: "Number of trials where the true signal type was 'SAME' (presented and response shapes were identical)"
|
|
2335
|
+
},
|
|
2336
|
+
n_trials_type_different: {
|
|
2337
|
+
type: "number",
|
|
2338
|
+
description: "Number of trials where the true signal type was 'DIFFERENT' (response shapes differed from presented)"
|
|
2339
|
+
},
|
|
2340
|
+
HIT_rate: {
|
|
2341
|
+
type: ["number", "null"],
|
|
2342
|
+
description: "Proportion of 'different' trials correctly identified as different. Calculated as n_trials_HIT / n_trials_type_different"
|
|
2343
|
+
},
|
|
2344
|
+
MISS_rate: {
|
|
2345
|
+
type: ["number", "null"],
|
|
2346
|
+
description: "Proportion of 'different' trials incorrectly identified as same. Calculated as 1 - HIT_rate"
|
|
2347
|
+
},
|
|
2348
|
+
FA_rate: {
|
|
2349
|
+
type: ["number", "null"],
|
|
2350
|
+
description: "Proportion of 'same' trials incorrectly identified as different. Calculated as n_trials_FA / n_trials_type_same"
|
|
2351
|
+
},
|
|
2352
|
+
CR_rate: {
|
|
2353
|
+
type: ["number", "null"],
|
|
2354
|
+
description: "Proportion of 'same' trials correctly identified as same. Calculated as 1 - FA_rate"
|
|
2355
|
+
},
|
|
2356
|
+
n_trials_rt_invalid: {
|
|
2357
|
+
type: "number",
|
|
2358
|
+
description: "Number of trials with null or non-positive response times (excluded from RT calculations)"
|
|
2359
|
+
},
|
|
2360
|
+
median_rt_overall_valid: {
|
|
2361
|
+
type: ["number", "null"],
|
|
2362
|
+
description: "Median response time (ms) across all trials with valid (non-null, positive) response times. No outlier filtering applied"
|
|
2363
|
+
},
|
|
2364
|
+
sd_rt_overall_valid: {
|
|
2365
|
+
type: ["number", "null"],
|
|
2366
|
+
description: "Standard deviation of response time (ms) across all trials with valid response times. No outlier filtering applied"
|
|
2367
|
+
},
|
|
2368
|
+
median_rt_HIT_valid: {
|
|
2369
|
+
type: ["number", "null"],
|
|
2370
|
+
description: "Median response time (ms) for HIT trials with valid response times. No outlier filtering"
|
|
2371
|
+
},
|
|
2372
|
+
sd_rt_HIT_valid: {
|
|
2373
|
+
type: ["number", "null"],
|
|
2374
|
+
description: "Standard deviation of response time (ms) for HIT trials with valid response times. No outlier filtering"
|
|
2375
|
+
},
|
|
2376
|
+
median_rt_MISS_valid: {
|
|
2377
|
+
type: ["number", "null"],
|
|
2378
|
+
description: "Median response time (ms) for MISS trials with valid response times. No outlier filtering"
|
|
2379
|
+
},
|
|
2380
|
+
sd_rt_MISS_valid: {
|
|
2381
|
+
type: ["number", "null"],
|
|
2382
|
+
description: "Standard deviation of response time (ms) for MISS trials with valid response times. No outlier filtering"
|
|
2383
|
+
},
|
|
2384
|
+
median_rt_FA_valid: {
|
|
2385
|
+
type: ["number", "null"],
|
|
2386
|
+
description: "Median response time (ms) for False Alarm trials with valid response times. No outlier filtering"
|
|
2387
|
+
},
|
|
2388
|
+
sd_rt_FA_valid: {
|
|
2389
|
+
type: ["number", "null"],
|
|
2390
|
+
description: "Standard deviation of response time (ms) for False Alarm trials with valid response times. No outlier filtering"
|
|
2391
|
+
},
|
|
2392
|
+
median_rt_CR_valid: {
|
|
2393
|
+
type: ["number", "null"],
|
|
2394
|
+
description: "Median response time (ms) for Correct Rejection trials with valid response times. No outlier filtering"
|
|
2395
|
+
},
|
|
2396
|
+
sd_rt_CR_valid: {
|
|
2397
|
+
type: ["number", "null"],
|
|
2398
|
+
description: "Standard deviation of response time (ms) for Correct Rejection trials with valid response times. No outlier filtering"
|
|
2399
|
+
},
|
|
2400
|
+
median_rt_overall_valid_filtered: {
|
|
2401
|
+
type: ["number", "null"],
|
|
2402
|
+
description: "Median response time (ms) across all valid trials after removing outliers, as specified in response_time_filter_lower_bound and response_time_filter_upper_bound."
|
|
2403
|
+
},
|
|
2404
|
+
sd_rt_overall_valid_filtered: {
|
|
2405
|
+
type: ["number", "null"],
|
|
2406
|
+
description: "Standard deviation of response time (ms) across all valid trials after removing outliers, as specified in response_time_filter_lower_bound and response_time_filter_upper_bound."
|
|
2407
|
+
},
|
|
2408
|
+
n_outliers_rt_overall_valid: {
|
|
2409
|
+
type: "number",
|
|
2410
|
+
description: "Number of valid trials removed as RT outliers, as specified in response_time_filter_lower_bound and response_time_filter_upper_bound."
|
|
2411
|
+
},
|
|
2412
|
+
median_rt_HIT_valid_filtered: {
|
|
2413
|
+
type: ["number", "null"],
|
|
2414
|
+
description: "Median response time (ms) for HIT trials after removing outliers, as specified in response_time_filter_lower_bound and response_time_filter_upper_bound."
|
|
2415
|
+
},
|
|
2416
|
+
sd_rt_HIT_valid_filtered: {
|
|
2417
|
+
type: ["number", "null"],
|
|
2418
|
+
description: "Standard deviation of response time (ms) for HIT trials after removing outliers, as specified in response_time_filter_lower_bound and response_time_filter_upper_bound."
|
|
2419
|
+
},
|
|
2420
|
+
n_outliers_rt_HIT_valid: {
|
|
2421
|
+
type: "number",
|
|
2422
|
+
description: "Number of HIT trials removed as RT outliers, as specified in response_time_filter_lower_bound and response_time_filter_upper_bound."
|
|
2423
|
+
},
|
|
2424
|
+
median_rt_MISS_valid_filtered: {
|
|
2425
|
+
type: ["number", "null"],
|
|
2426
|
+
description: "Median response time (ms) for MISS trials after removing outliers, as specified in response_time_filter_lower_bound and response_time_filter_upper_bound."
|
|
2427
|
+
},
|
|
2428
|
+
sd_rt_MISS_valid_filtered: {
|
|
2429
|
+
type: ["number", "null"],
|
|
2430
|
+
description: "Standard deviation of response time (ms) for MISS trials after removing outliers, as specified in response_time_filter_lower_bound and response_time_filter_upper_bound."
|
|
2431
|
+
},
|
|
2432
|
+
n_outliers_rt_MISS_valid: {
|
|
2433
|
+
type: "number",
|
|
2434
|
+
description: "Number of MISS trials removed as RT outliers, as specified in response_time_filter_lower_bound and response_time_filter_upper_bound."
|
|
2435
|
+
},
|
|
2436
|
+
median_rt_FA_valid_filtered: {
|
|
2437
|
+
type: ["number", "null"],
|
|
2438
|
+
description: "Median response time (ms) for False Alarm trials after removing outliers, as specified in response_time_filter_lower_bound and response_time_filter_upper_bound."
|
|
2439
|
+
},
|
|
2440
|
+
sd_rt_FA_valid_filtered: {
|
|
2441
|
+
type: ["number", "null"],
|
|
2442
|
+
description: "Standard deviation of response time (ms) for False Alarm trials after removing outliers, as specified in response_time_filter_lower_bound and response_time_filter_upper_bound."
|
|
2443
|
+
},
|
|
2444
|
+
n_outliers_rt_FA_valid: {
|
|
2445
|
+
type: "number",
|
|
2446
|
+
description: "Number of False Alarm trials removed as RT outliers, as specified in response_time_filter_lower_bound and response_time_filter_upper_bound."
|
|
2447
|
+
},
|
|
2448
|
+
median_rt_CR_valid_filtered: {
|
|
2449
|
+
type: ["number", "null"],
|
|
2450
|
+
description: "Median response time (ms) for Correct Rejection trials after removing outliers, as specified in response_time_filter_lower_bound and response_time_filter_upper_bound."
|
|
2451
|
+
},
|
|
2452
|
+
sd_rt_CR_valid_filtered: {
|
|
2453
|
+
type: ["number", "null"],
|
|
2454
|
+
description: "Standard deviation of response time (ms) for Correct Rejection trials after removing outliers, as specified in response_time_filter_lower_bound and response_time_filter_upper_bound."
|
|
2455
|
+
},
|
|
2456
|
+
n_outliers_rt_CR_valid: {
|
|
2457
|
+
type: ["number", "null"],
|
|
2458
|
+
description: "Number of Correct Rejection trials removed as RT outliers, as specified in response_time_filter_lower_bound and response_time_filter_upper_bound."
|
|
1352
2459
|
}
|
|
1353
2460
|
};
|
|
1354
2461
|
const translation = {
|
|
@@ -1415,8 +2522,8 @@ class ColorShapes extends Game {
|
|
|
1415
2522
|
*/
|
|
1416
2523
|
id: "color-shapes",
|
|
1417
2524
|
publishUuid: "394cb010-2ccf-4a87-9d23-cda7fb07a960",
|
|
1418
|
-
version: "0.8.
|
|
1419
|
-
moduleMetadata: { "name": "@m2c2kit/assessment-color-shapes", "version": "0.8.
|
|
2525
|
+
version: "0.8.34 (38a5862e)",
|
|
2526
|
+
moduleMetadata: { "name": "@m2c2kit/assessment-color-shapes", "version": "0.8.34", "dependencies": { "@m2c2kit/addons": "0.3.35", "@m2c2kit/core": "0.3.36", "@m2c2kit/data-calc": "0.8.7" } },
|
|
1420
2527
|
translation,
|
|
1421
2528
|
shortDescription: "Color Shapes is a visual array change detection task, measuring intra-item feature binding, where participants determine if shapes change color across two sequential presentations of shape stimuli.",
|
|
1422
2529
|
longDescription: `Color Shapes is a change detection paradigm used to measure visual short-term memory binding (Parra et al., 2009). Participants are asked to memorize the shapes and colors of three different polygons for 3 seconds. The three polygons are then removed from the screen and re-displayed at different locations, either having the same or different colors. Participants are then asked to decide whether the combination of colors and shapes are the "Same" or "Different" between the study and test phases.`,
|
|
@@ -1494,6 +2601,12 @@ class ColorShapes extends Game {
|
|
|
1494
2601
|
game.trialComplete();
|
|
1495
2602
|
if (game.getParameter("scoring")) {
|
|
1496
2603
|
const scores = game.calculateScores([], {
|
|
2604
|
+
rtLowerBound: game.getParameter(
|
|
2605
|
+
"scoring_filter_response_time_duration_ms"
|
|
2606
|
+
)[0],
|
|
2607
|
+
rtUpperBound: game.getParameter(
|
|
2608
|
+
"scoring_filter_response_time_duration_ms"
|
|
2609
|
+
)[1],
|
|
1497
2610
|
numberOfTrials: game.getParameter("number_of_trials")
|
|
1498
2611
|
});
|
|
1499
2612
|
game.addScoringData(scores);
|
|
@@ -1916,6 +3029,12 @@ class ColorShapes extends Game {
|
|
|
1916
3029
|
} else {
|
|
1917
3030
|
if (game.getParameter("scoring")) {
|
|
1918
3031
|
const scores = game.calculateScores(game.data.trials, {
|
|
3032
|
+
rtLowerBound: game.getParameter(
|
|
3033
|
+
"scoring_filter_response_time_duration_ms"
|
|
3034
|
+
)[0],
|
|
3035
|
+
rtUpperBound: game.getParameter(
|
|
3036
|
+
"scoring_filter_response_time_duration_ms"
|
|
3037
|
+
)[1],
|
|
1919
3038
|
numberOfTrials: game.getParameter("number_of_trials")
|
|
1920
3039
|
});
|
|
1921
3040
|
game.addScoringData(scores);
|
|
@@ -1959,18 +3078,198 @@ class ColorShapes extends Game {
|
|
|
1959
3078
|
}
|
|
1960
3079
|
calculateScores(data, extras) {
|
|
1961
3080
|
const dc = new DataCalc(data);
|
|
1962
|
-
const
|
|
3081
|
+
const n_trials = dc.filter((o) => o.quit_button_pressed === false).length;
|
|
3082
|
+
const scores = dc.mutate({
|
|
3083
|
+
metric_accuracy: (obs) => {
|
|
3084
|
+
const ur = obs.user_response;
|
|
3085
|
+
const uc = obs.user_response_correct;
|
|
3086
|
+
if (ur === "same" && uc === true) return "CR";
|
|
3087
|
+
if (ur === "same" && uc === false) return "MISS";
|
|
3088
|
+
if (ur === "different" && uc === true) return "HIT";
|
|
3089
|
+
if (ur === "different" && uc === false) return "FA";
|
|
3090
|
+
return null;
|
|
3091
|
+
},
|
|
3092
|
+
metric_trial_type: (obs) => {
|
|
3093
|
+
const ur = obs.user_response;
|
|
3094
|
+
const uc = obs.user_response_correct;
|
|
3095
|
+
if (ur === "same" && uc === true) return "SAME";
|
|
3096
|
+
if (ur === "same" && uc === false) return "DIFFERENT";
|
|
3097
|
+
if (ur === "different" && uc === true) return "DIFFERENT";
|
|
3098
|
+
if (ur === "different" && uc === false) return "SAME";
|
|
3099
|
+
return null;
|
|
3100
|
+
},
|
|
3101
|
+
is_valid: (obs) => typeof obs.response_time_duration_ms === "number" && obs.response_time_duration_ms > 0
|
|
3102
|
+
}).summarize({
|
|
1963
3103
|
activity_begin_iso8601_timestamp: this.beginIso8601Timestamp,
|
|
1964
|
-
first_trial_begin_iso8601_timestamp:
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
3104
|
+
first_trial_begin_iso8601_timestamp: arrange(
|
|
3105
|
+
"trial_begin_iso8601_timestamp"
|
|
3106
|
+
).slice(0).pull("trial_begin_iso8601_timestamp"),
|
|
3107
|
+
last_trial_end_iso8601_timestamp: arrange(
|
|
3108
|
+
"-trial_end_iso8601_timestamp"
|
|
3109
|
+
).slice(0).pull("trial_end_iso8601_timestamp"),
|
|
3110
|
+
n_trials,
|
|
3111
|
+
flag_trials_match_expected: n_trials === extras.numberOfTrials ? 1 : 0,
|
|
3112
|
+
flag_trials_lt_expected: n_trials < extras.numberOfTrials ? 1 : 0,
|
|
3113
|
+
flag_trials_gt_expected: n_trials > extras.numberOfTrials ? 1 : 0,
|
|
3114
|
+
// counts by accuracy
|
|
3115
|
+
n_trials_HIT: filter((o) => o.metric_accuracy === "HIT").length,
|
|
3116
|
+
n_trials_MISS: filter((o) => o.metric_accuracy === "MISS").length,
|
|
3117
|
+
n_trials_FA: filter((o) => o.metric_accuracy === "FA").length,
|
|
3118
|
+
n_trials_CR: filter((o) => o.metric_accuracy === "CR").length,
|
|
3119
|
+
// trial types
|
|
3120
|
+
n_trials_type_same: filter((o) => o.metric_trial_type === "SAME").length,
|
|
3121
|
+
n_trials_type_different: filter(
|
|
3122
|
+
(o) => o.metric_trial_type === "DIFFERENT"
|
|
3123
|
+
).length,
|
|
3124
|
+
// Note: When the summarize operation is a custom function,
|
|
3125
|
+
// () => value, rather than a built in function like sum or mean,
|
|
3126
|
+
// we must provide the DataCalc object as an argument to the
|
|
3127
|
+
// function, and do the filtering and pulling of values on that
|
|
3128
|
+
// object within the function. Specifically below, we must do:
|
|
3129
|
+
// const denom = d.filter(...).length
|
|
3130
|
+
// NOT
|
|
3131
|
+
// const denom = filter(...).length
|
|
3132
|
+
// rates
|
|
3133
|
+
HIT_rate: (d) => {
|
|
3134
|
+
const denom = d.filter(
|
|
3135
|
+
(o) => o.metric_trial_type === "DIFFERENT"
|
|
3136
|
+
).length;
|
|
3137
|
+
return denom > 0 ? d.filter((o) => o.metric_accuracy === "HIT").length / denom : null;
|
|
3138
|
+
},
|
|
3139
|
+
MISS_rate: (d) => {
|
|
3140
|
+
const denom = d.filter(
|
|
3141
|
+
(o) => o.metric_trial_type === "DIFFERENT"
|
|
3142
|
+
).length;
|
|
3143
|
+
return denom > 0 ? 1 - d.filter((o) => o.metric_accuracy === "HIT").length / denom : null;
|
|
3144
|
+
},
|
|
3145
|
+
FA_rate: (d) => {
|
|
3146
|
+
const denom = d.filter((o) => o.metric_trial_type === "SAME").length;
|
|
3147
|
+
return denom > 0 ? d.filter((o) => o.metric_accuracy === "FA").length / denom : null;
|
|
3148
|
+
},
|
|
3149
|
+
CR_rate: (d) => {
|
|
3150
|
+
const denom = d.filter((o) => o.metric_trial_type === "SAME").length;
|
|
3151
|
+
return denom > 0 ? 1 - d.filter((o) => o.metric_accuracy === "FA").length / denom : null;
|
|
3152
|
+
},
|
|
3153
|
+
// response time summaries
|
|
3154
|
+
// overall valid RTs (>0)
|
|
3155
|
+
median_rt_overall_valid: median(
|
|
3156
|
+
filter((o) => o.is_valid).pull("response_time_duration_ms")
|
|
3157
|
+
),
|
|
3158
|
+
sd_rt_overall_valid: sd(
|
|
3159
|
+
filter((o) => o.is_valid).pull("response_time_duration_ms")
|
|
3160
|
+
),
|
|
3161
|
+
// filtered overall RTs
|
|
3162
|
+
median_rt_overall_valid_filtered: median(
|
|
3163
|
+
filter((o) => o.is_valid).filter(
|
|
3164
|
+
(o) => o.response_time_duration_ms >= extras.rtLowerBound && o.response_time_duration_ms <= extras.rtUpperBound
|
|
3165
|
+
).pull("response_time_duration_ms")
|
|
3166
|
+
),
|
|
3167
|
+
sd_rt_overall_valid_filtered: sd(
|
|
3168
|
+
filter((o) => o.is_valid).filter(
|
|
3169
|
+
(o) => o.response_time_duration_ms >= extras.rtLowerBound && o.response_time_duration_ms <= extras.rtUpperBound
|
|
3170
|
+
).pull("response_time_duration_ms")
|
|
3171
|
+
),
|
|
3172
|
+
// counts of invalid/outliers
|
|
3173
|
+
n_trials_rt_invalid: filter((o) => !o.is_valid).length,
|
|
3174
|
+
n_outliers_rt_overall_valid: filter((o) => o.is_valid).filter(
|
|
3175
|
+
(o) => o.response_time_duration_ms < extras.rtLowerBound || o.response_time_duration_ms > extras.rtUpperBound
|
|
3176
|
+
).length,
|
|
3177
|
+
response_time_filter_lower_bound: extras.rtLowerBound,
|
|
3178
|
+
response_time_filter_upper_bound: extras.rtUpperBound,
|
|
3179
|
+
// per-metric RT summaries
|
|
3180
|
+
median_rt_HIT_valid: median(
|
|
3181
|
+
filter((o) => o.is_valid).filter((o) => o.metric_accuracy === "HIT").pull("response_time_duration_ms")
|
|
3182
|
+
),
|
|
3183
|
+
sd_rt_HIT_valid: sd(
|
|
3184
|
+
filter((o) => o.is_valid).filter((o) => o.metric_accuracy === "HIT").pull("response_time_duration_ms")
|
|
3185
|
+
),
|
|
3186
|
+
median_rt_HIT_valid_filtered: median(
|
|
3187
|
+
filter((o) => o.is_valid).filter(
|
|
3188
|
+
(o) => o.metric_accuracy === "HIT" && o.response_time_duration_ms >= extras.rtLowerBound && o.response_time_duration_ms <= extras.rtUpperBound
|
|
3189
|
+
).pull("response_time_duration_ms")
|
|
3190
|
+
),
|
|
3191
|
+
sd_rt_HIT_valid_filtered: sd(
|
|
3192
|
+
filter((o) => o.is_valid).filter(
|
|
3193
|
+
(o) => o.metric_accuracy === "HIT" && o.response_time_duration_ms >= extras.rtLowerBound && o.response_time_duration_ms <= extras.rtUpperBound
|
|
3194
|
+
).pull("response_time_duration_ms")
|
|
3195
|
+
),
|
|
3196
|
+
n_outliers_rt_HIT_valid: filter((o) => o.is_valid).filter((o) => o.metric_accuracy === "HIT").filter(
|
|
3197
|
+
(o) => o.response_time_duration_ms < extras.rtLowerBound || o.response_time_duration_ms > extras.rtUpperBound
|
|
3198
|
+
).length,
|
|
3199
|
+
median_rt_MISS_valid: median(
|
|
3200
|
+
filter((o) => o.is_valid).filter((o) => o.metric_accuracy === "MISS").pull("response_time_duration_ms")
|
|
3201
|
+
),
|
|
3202
|
+
sd_rt_MISS_valid: sd(
|
|
3203
|
+
filter((o) => o.is_valid).filter((o) => o.metric_accuracy === "MISS").pull("response_time_duration_ms")
|
|
3204
|
+
),
|
|
3205
|
+
median_rt_MISS_valid_filtered: median(
|
|
3206
|
+
filter((o) => o.is_valid).filter(
|
|
3207
|
+
(o) => o.metric_accuracy === "MISS" && o.response_time_duration_ms >= extras.rtLowerBound && o.response_time_duration_ms <= extras.rtUpperBound
|
|
3208
|
+
).pull("response_time_duration_ms")
|
|
3209
|
+
),
|
|
3210
|
+
sd_rt_MISS_valid_filtered: sd(
|
|
3211
|
+
filter((o) => o.is_valid).filter(
|
|
3212
|
+
(o) => o.metric_accuracy === "MISS" && o.response_time_duration_ms >= extras.rtLowerBound && o.response_time_duration_ms <= extras.rtUpperBound
|
|
3213
|
+
).pull("response_time_duration_ms")
|
|
3214
|
+
),
|
|
3215
|
+
n_outliers_rt_MISS_valid: filter((o) => o.is_valid).filter((o) => o.metric_accuracy === "MISS").filter(
|
|
3216
|
+
(o) => o.response_time_duration_ms < extras.rtLowerBound || o.response_time_duration_ms > extras.rtUpperBound
|
|
3217
|
+
).length,
|
|
3218
|
+
median_rt_FA_valid: median(
|
|
3219
|
+
filter((o) => o.is_valid).filter((o) => o.metric_accuracy === "FA").pull("response_time_duration_ms")
|
|
3220
|
+
),
|
|
3221
|
+
sd_rt_FA_valid: sd(
|
|
3222
|
+
filter((o) => o.is_valid).filter((o) => o.metric_accuracy === "FA").pull("response_time_duration_ms")
|
|
3223
|
+
),
|
|
3224
|
+
median_rt_FA_valid_filtered: median(
|
|
3225
|
+
filter((o) => o.is_valid).filter(
|
|
3226
|
+
(o) => o.metric_accuracy === "FA" && o.response_time_duration_ms >= extras.rtLowerBound && o.response_time_duration_ms <= extras.rtUpperBound
|
|
3227
|
+
).pull("response_time_duration_ms")
|
|
3228
|
+
),
|
|
3229
|
+
sd_rt_FA_valid_filtered: sd(
|
|
3230
|
+
filter((o) => o.is_valid).filter(
|
|
3231
|
+
(o) => o.metric_accuracy === "FA" && o.response_time_duration_ms >= extras.rtLowerBound && o.response_time_duration_ms <= extras.rtUpperBound
|
|
3232
|
+
).pull("response_time_duration_ms")
|
|
3233
|
+
),
|
|
3234
|
+
n_outliers_rt_FA_valid: filter((o) => o.is_valid).filter((o) => o.metric_accuracy === "FA").filter(
|
|
3235
|
+
(o) => o.response_time_duration_ms < extras.rtLowerBound || o.response_time_duration_ms > extras.rtUpperBound
|
|
3236
|
+
).length,
|
|
3237
|
+
median_rt_CR_valid: median(
|
|
3238
|
+
filter((o) => o.is_valid).filter((o) => o.metric_accuracy === "CR").pull("response_time_duration_ms")
|
|
3239
|
+
),
|
|
3240
|
+
sd_rt_CR_valid: sd(
|
|
3241
|
+
filter((o) => o.is_valid).filter((o) => o.metric_accuracy === "CR").pull("response_time_duration_ms")
|
|
3242
|
+
),
|
|
3243
|
+
median_rt_CR_valid_filtered: median(
|
|
3244
|
+
filter((o) => o.is_valid).filter(
|
|
3245
|
+
(o) => o.metric_accuracy === "CR" && o.response_time_duration_ms >= extras.rtLowerBound && o.response_time_duration_ms <= extras.rtUpperBound
|
|
3246
|
+
).pull("response_time_duration_ms")
|
|
3247
|
+
),
|
|
3248
|
+
sd_rt_CR_valid_filtered: sd(
|
|
3249
|
+
filter((o) => o.is_valid).filter(
|
|
3250
|
+
(o) => o.metric_accuracy === "CR" && o.response_time_duration_ms >= extras.rtLowerBound && o.response_time_duration_ms <= extras.rtUpperBound
|
|
3251
|
+
).pull("response_time_duration_ms")
|
|
3252
|
+
),
|
|
3253
|
+
n_outliers_rt_CR_valid: filter((o) => o.is_valid).filter((o) => o.metric_accuracy === "CR").filter(
|
|
3254
|
+
(o) => o.response_time_duration_ms < extras.rtLowerBound || o.response_time_duration_ms > extras.rtUpperBound
|
|
3255
|
+
).length,
|
|
3256
|
+
n_trials_correct: filter(
|
|
3257
|
+
(o) => o.metric_accuracy === "HIT" || o.metric_accuracy === "CR"
|
|
3258
|
+
).length,
|
|
3259
|
+
n_trials_incorrect: filter(
|
|
3260
|
+
(o) => o.metric_accuracy === "MISS" || o.metric_accuracy === "FA"
|
|
3261
|
+
).length,
|
|
3262
|
+
participant_score: (d) => {
|
|
3263
|
+
const total = d.length;
|
|
3264
|
+
if (total === 0) return null;
|
|
3265
|
+
return d.summarize({
|
|
3266
|
+
correct: parens(
|
|
3267
|
+
scalar(d.filter((o) => o.metric_accuracy === "HIT").length).add(
|
|
3268
|
+
scalar(d.filter((o) => o.metric_accuracy === "CR").length)
|
|
3269
|
+
)
|
|
3270
|
+
).div(scalar(total)).mul(scalar(100))
|
|
3271
|
+
}).pull("correct");
|
|
3272
|
+
}
|
|
1974
3273
|
});
|
|
1975
3274
|
return scores.observations;
|
|
1976
3275
|
}
|