@m2c2kit/assessment-color-shapes 0.8.33 → 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/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.
@@ -106,9 +176,11 @@ class DataCalc {
106
176
  */
107
177
  pull(variable) {
108
178
  if (this._observations.length === 0) {
109
- console.warn(
110
- `DataCalc.pull(): No observations available to pull variable "${variable}" from. Returning null.`
111
- );
179
+ if (this._warnings) {
180
+ console.warn(
181
+ `DataCalc.pull(): No observations available to pull variable "${variable}" from. Returning null.`
182
+ );
183
+ }
112
184
  return null;
113
185
  }
114
186
  this.verifyObservationsContainVariable(variable);
@@ -168,7 +240,7 @@ class DataCalc {
168
240
  this._observations.filter(
169
241
  predicate
170
242
  ),
171
- { groups: this._groups }
243
+ { groups: this._groups, warnings: this._warnings }
172
244
  );
173
245
  }
174
246
  /**
@@ -177,7 +249,8 @@ class DataCalc {
177
249
  * @remarks This is used with the `summarize()` method to calculate summaries
178
250
  * by group.
179
251
  *
180
- * @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).
181
254
  * @returns A new `DataCalc` object with the observations grouped by one or
182
255
  * more variables
183
256
  *
@@ -198,6 +271,16 @@ class DataCalc {
198
271
  groupBy(...groups) {
199
272
  groups.forEach((group) => {
200
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
+ }
201
284
  });
202
285
  return new DataCalc(this._observations, { groups });
203
286
  }
@@ -251,7 +334,10 @@ class DataCalc {
251
334
  }
252
335
  return newObservation;
253
336
  });
254
- return new DataCalc(newObservations, { groups: this._groups });
337
+ return new DataCalc(newObservations, {
338
+ groups: this._groups,
339
+ warnings: this._warnings
340
+ });
255
341
  }
256
342
  /**
257
343
  * Calculates summaries of the data.
@@ -288,6 +374,31 @@ class DataCalc {
288
374
  * );
289
375
  * // [ { filteredTotalC: 10 } ]
290
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
+ * ```
291
402
  */
292
403
  summarize(summarizations) {
293
404
  if (this._groups.length === 0) {
@@ -300,20 +411,103 @@ class DataCalc {
300
411
  summarizeOperation.parameters,
301
412
  summarizeOperation.options
302
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
+ }
303
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
+ }
304
497
  obs[newVariable] = value;
305
498
  }
306
499
  }
307
- return new DataCalc([obs], { groups: this._groups });
500
+ return new DataCalc([obs], {
501
+ groups: this._groups,
502
+ warnings: this._warnings
503
+ });
308
504
  }
309
505
  return this.summarizeByGroups(summarizations);
310
506
  }
311
507
  summarizeByGroups(summarizations) {
312
508
  const groupMap = /* @__PURE__ */ new Map();
313
509
  this._observations.forEach((obs) => {
314
- const groupKey = this._groups.map(
315
- (g) => typeof obs[g] === "object" ? JSON.stringify(obs[g]) : obs[g]
316
- ).join("|");
510
+ const groupKey = this._groups.map((g) => String(obs[g])).join("|");
317
511
  if (!groupMap.has(groupKey)) {
318
512
  groupMap.set(groupKey, []);
319
513
  }
@@ -331,21 +525,17 @@ class DataCalc {
331
525
  const summaryObj = {};
332
526
  this._groups.forEach((group, i) => {
333
527
  const valueStr = groupValues[i];
334
- const originalType = typeof firstObs[group];
335
- if (originalType === "number") {
336
- summaryObj[group] = Number(valueStr);
337
- } else if (originalType === "boolean") {
338
- summaryObj[group] = valueStr === "true";
339
- } else if (valueStr.startsWith("{") || valueStr.startsWith("[")) {
340
- try {
341
- summaryObj[group] = JSON.parse(valueStr);
342
- } catch {
343
- throw new M2Error(
344
- `Failed to parse group value ${valueStr} as JSON for group ${group}`
345
- );
346
- }
528
+ if (firstObs[group] === null) {
529
+ summaryObj[group] = null;
347
530
  } else {
348
- summaryObj[group] = valueStr;
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
+ }
349
539
  }
350
540
  });
351
541
  const groupDataCalc = new DataCalc(groupObs);
@@ -357,13 +547,98 @@ class DataCalc {
357
547
  summarizeOperation.parameters,
358
548
  summarizeOperation.options
359
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
+ }
360
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
+ }
361
633
  summaryObj[newVariable] = value;
362
634
  }
363
635
  }
364
636
  summarizedObservations.push(summaryObj);
365
637
  });
366
- return new DataCalc(summarizedObservations, { groups: this._groups });
638
+ return new DataCalc(summarizedObservations, {
639
+ groups: this._groups,
640
+ warnings: this._warnings
641
+ });
367
642
  }
368
643
  /**
369
644
  * Selects specific variables to keep in the dataset.
@@ -416,7 +691,10 @@ class DataCalc {
416
691
  }
417
692
  return newObservation;
418
693
  });
419
- return new DataCalc(newObservations, { groups: this._groups });
694
+ return new DataCalc(newObservations, {
695
+ groups: this._groups,
696
+ warnings: this._warnings
697
+ });
420
698
  }
421
699
  /**
422
700
  * Arranges (sorts) the observations based on one or more variables.
@@ -468,7 +746,10 @@ class DataCalc {
468
746
  }
469
747
  return 0;
470
748
  });
471
- return new DataCalc(sortedObservations, { groups: this._groups });
749
+ return new DataCalc(sortedObservations, {
750
+ groups: this._groups,
751
+ warnings: this._warnings
752
+ });
472
753
  }
473
754
  /**
474
755
  * Keeps only unique/distinct observations.
@@ -497,11 +778,19 @@ class DataCalc {
497
778
  seen.add(key);
498
779
  return true;
499
780
  });
500
- return new DataCalc(uniqueObs, { groups: this._groups });
781
+ return new DataCalc(uniqueObs, {
782
+ groups: this._groups,
783
+ warnings: this._warnings
784
+ });
501
785
  }
502
786
  /**
503
787
  * Renames variables in the observations.
504
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
+ *
505
794
  * @param renames - Object mapping new variable names to old variable names
506
795
  * @returns A new DataCalc object with renamed variables
507
796
  *
@@ -523,21 +812,37 @@ class DataCalc {
523
812
  Object.values(renames).forEach((oldName) => {
524
813
  this.verifyObservationsContainVariable(oldName);
525
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
+ }
526
827
  const newObservations = this._observations.map((observation) => {
527
828
  const newObservation = {};
829
+ const newNames = new Set(Object.keys(renames));
528
830
  for (const [key, value] of Object.entries(observation)) {
529
831
  const newKey = Object.entries(renames).find(
530
832
  ([, old]) => old === key
531
833
  )?.[0];
532
834
  if (newKey) {
533
835
  newObservation[newKey] = value;
534
- } else if (!Object.values(renames).includes(key)) {
836
+ } else if (!newNames.has(key)) {
535
837
  newObservation[key] = value;
536
838
  }
537
839
  }
538
840
  return newObservation;
539
841
  });
540
- return new DataCalc(newObservations, { groups: this._groups });
842
+ return new DataCalc(newObservations, {
843
+ groups: this._groups,
844
+ warnings: this._warnings
845
+ });
541
846
  }
542
847
  /**
543
848
  * Performs an inner join with another DataCalc object.
@@ -877,7 +1182,10 @@ class DataCalc {
877
1182
  }
878
1183
  let sliced;
879
1184
  if (start >= this._observations.length) {
880
- return new DataCalc([], { groups: this._groups });
1185
+ return new DataCalc([], {
1186
+ groups: this._groups,
1187
+ warnings: this._warnings
1188
+ });
881
1189
  }
882
1190
  if (end === void 0) {
883
1191
  const index = start < 0 ? this._observations.length + start : start;
@@ -885,7 +1193,10 @@ class DataCalc {
885
1193
  } else {
886
1194
  sliced = this._observations.slice(start, end);
887
1195
  }
888
- return new DataCalc(sliced, { groups: this._groups });
1196
+ return new DataCalc(sliced, {
1197
+ groups: this._groups,
1198
+ warnings: this._warnings
1199
+ });
889
1200
  }
890
1201
  /**
891
1202
  * Combines observations from two DataCalc objects by rows.
@@ -1047,6 +1358,26 @@ class DataCalc {
1047
1358
  * @returns a deep copy of the object
1048
1359
  */
1049
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
+ }
1050
1381
  if (source === null || typeof source !== "object") {
1051
1382
  return source;
1052
1383
  }
@@ -1065,8 +1396,9 @@ class DataCalc {
1065
1396
  key
1066
1397
  );
1067
1398
  if (descriptor) {
1399
+ const { get, set, ...descWithoutAccessors } = descriptor;
1068
1400
  Object.defineProperty(copy, key, {
1069
- ...descriptor,
1401
+ ...descWithoutAccessors,
1070
1402
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
1071
1403
  value: this.deepCopy(source[key], map)
1072
1404
  });
@@ -1086,7 +1418,613 @@ class DataCalc {
1086
1418
  return keys.some((key) => obs[key] === null || obs[key] === void 0);
1087
1419
  }
1088
1420
  }
1089
- console.log("\u26AA @m2c2kit/data-calc version 0.8.6 (c86b5047)");
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)");
1090
2028
 
1091
2029
  class ColorShapes extends Game {
1092
2030
  constructor() {
@@ -1199,6 +2137,14 @@ class ColorShapes extends Game {
1199
2137
  type: "boolean",
1200
2138
  default: false,
1201
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."
1202
2148
  }
1203
2149
  };
1204
2150
  const colorShapesTrialSchema = {
@@ -1331,6 +2277,14 @@ class ColorShapes extends Game {
1331
2277
  format: "date-time",
1332
2278
  description: "ISO 8601 timestamp at the end of the last trial. Null if no trials were completed."
1333
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
+ },
1334
2288
  n_trials: {
1335
2289
  type: "integer",
1336
2290
  description: "Number of trials completed."
@@ -1350,6 +2304,158 @@ class ColorShapes extends Game {
1350
2304
  participant_score: {
1351
2305
  type: ["number", "null"],
1352
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."
1353
2459
  }
1354
2460
  };
1355
2461
  const translation = {
@@ -1416,8 +2522,8 @@ class ColorShapes extends Game {
1416
2522
  */
1417
2523
  id: "color-shapes",
1418
2524
  publishUuid: "394cb010-2ccf-4a87-9d23-cda7fb07a960",
1419
- version: "0.8.33 (c86b5047)",
1420
- moduleMetadata: { "name": "@m2c2kit/assessment-color-shapes", "version": "0.8.33", "dependencies": { "@m2c2kit/addons": "0.3.34", "@m2c2kit/core": "0.3.35" } },
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" } },
1421
2527
  translation,
1422
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.",
1423
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.`,
@@ -1495,6 +2601,12 @@ class ColorShapes extends Game {
1495
2601
  game.trialComplete();
1496
2602
  if (game.getParameter("scoring")) {
1497
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],
1498
2610
  numberOfTrials: game.getParameter("number_of_trials")
1499
2611
  });
1500
2612
  game.addScoringData(scores);
@@ -1917,6 +3029,12 @@ class ColorShapes extends Game {
1917
3029
  } else {
1918
3030
  if (game.getParameter("scoring")) {
1919
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],
1920
3038
  numberOfTrials: game.getParameter("number_of_trials")
1921
3039
  });
1922
3040
  game.addScoringData(scores);
@@ -1960,18 +3078,198 @@ class ColorShapes extends Game {
1960
3078
  }
1961
3079
  calculateScores(data, extras) {
1962
3080
  const dc = new DataCalc(data);
1963
- const scores = dc.summarize({
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({
1964
3103
  activity_begin_iso8601_timestamp: this.beginIso8601Timestamp,
1965
- first_trial_begin_iso8601_timestamp: dc.arrange("trial_begin_iso8601_timestamp").slice(0).pull("trial_begin_iso8601_timestamp"),
1966
- last_trial_end_iso8601_timestamp: dc.arrange("-trial_end_iso8601_timestamp").slice(0).pull("trial_end_iso8601_timestamp"),
1967
- n_trials: dc.length,
1968
- flag_trials_match_expected: dc.length === extras.numberOfTrials ? 1 : 0,
1969
- n_trials_correct: dc.filter((obs) => obs.user_response_correct === true).length,
1970
- n_trials_incorrect: dc.filter(
1971
- (obs) => obs.user_response_correct === false
1972
- ).length
1973
- }).mutate({
1974
- participant_score: (obs) => obs.n_trials > 0 ? obs.n_trials_correct / obs.n_trials * 100 : null
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
+ }
1975
3273
  });
1976
3274
  return scores.observations;
1977
3275
  }