@decidables/detectable-elements 0.0.3

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.
Files changed (37) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/LICENSE.md +1112 -0
  3. package/README.md +1218 -0
  4. package/lib/detectableElements.esm.js +18385 -0
  5. package/lib/detectableElements.esm.js.map +1 -0
  6. package/lib/detectableElements.esm.min.js +13 -0
  7. package/lib/detectableElements.esm.min.js.map +1 -0
  8. package/lib/detectableElements.umd.js +18413 -0
  9. package/lib/detectableElements.umd.js.map +1 -0
  10. package/lib/detectableElements.umd.min.js +13 -0
  11. package/lib/detectableElements.umd.min.js.map +1 -0
  12. package/package.json +58 -0
  13. package/src/components/detectable-control.js +272 -0
  14. package/src/components/detectable-response.js +414 -0
  15. package/src/components/detectable-table.js +602 -0
  16. package/src/components/index.js +7 -0
  17. package/src/components/rdk-task.js +586 -0
  18. package/src/components/roc-space.js +1220 -0
  19. package/src/components/sdt-model.js +1835 -0
  20. package/src/detectable-element.js +121 -0
  21. package/src/equations/dc2far.js +182 -0
  22. package/src/equations/dc2hr.js +191 -0
  23. package/src/equations/facr2far.js +120 -0
  24. package/src/equations/hm2hr.js +121 -0
  25. package/src/equations/hmfacr2acc.js +161 -0
  26. package/src/equations/hrfar2c.js +179 -0
  27. package/src/equations/hrfar2d.js +162 -0
  28. package/src/equations/index.js +8 -0
  29. package/src/equations/sdt-equation.js +141 -0
  30. package/src/examples/double-interactive.js +171 -0
  31. package/src/examples/human.js +184 -0
  32. package/src/examples/index.js +6 -0
  33. package/src/examples/interactive.js +131 -0
  34. package/src/examples/model.js +203 -0
  35. package/src/examples/sdt-example.js +76 -0
  36. package/src/examples/unequal.js +43 -0
  37. package/src/index.js +6 -0
@@ -0,0 +1,1835 @@
1
+
2
+ import {html, css} from 'lit';
3
+ import * as d3 from 'd3';
4
+ import jStat from 'jstat';
5
+
6
+ import SDTMath from '@decidables/detectable-math';
7
+
8
+ import DetectableElement from '../detectable-element';
9
+
10
+ /*
11
+ SDTModel element
12
+ <sdt-model>
13
+
14
+ Attributes:
15
+ d'; C;
16
+ FAR; HR; zFAR; zHR;
17
+
18
+ draggable: d'; C;
19
+ highlight: H; M; CR; FA;
20
+
21
+ Styles:
22
+ ??
23
+ */
24
+ export default class SDTModel extends DetectableElement {
25
+ static get properties() {
26
+ return {
27
+ color: {
28
+ attribute: 'color',
29
+ type: String,
30
+ reflect: true,
31
+ },
32
+ distributions: {
33
+ attribute: 'distributions',
34
+ type: Boolean,
35
+ reflect: true,
36
+ },
37
+ threshold: {
38
+ attribute: 'threshold',
39
+ type: Boolean,
40
+ reflect: true,
41
+ },
42
+ unequal: {
43
+ attribute: 'unequal',
44
+ type: Boolean,
45
+ reflect: true,
46
+ },
47
+ sensitivity: {
48
+ attribute: 'sensitivity',
49
+ type: Boolean,
50
+ reflect: true,
51
+ },
52
+ bias: {
53
+ attribute: 'bias',
54
+ type: Boolean,
55
+ reflect: true,
56
+ },
57
+ variance: {
58
+ attribute: 'variance',
59
+ type: Boolean,
60
+ reflect: true,
61
+ },
62
+ histogram: {
63
+ attribute: 'histogram',
64
+ type: Boolean,
65
+ reflect: true,
66
+ },
67
+ d: {
68
+ attribute: 'd',
69
+ type: Number,
70
+ reflect: true,
71
+ },
72
+ c: {
73
+ attribute: 'c',
74
+ type: Number,
75
+ reflect: true,
76
+ },
77
+ s: {
78
+ attribute: 's',
79
+ type: Number,
80
+ reflect: true,
81
+ },
82
+
83
+ far: {
84
+ attribute: false,
85
+ type: Number,
86
+ reflect: false,
87
+ },
88
+ hr: {
89
+ attribute: false,
90
+ type: Number,
91
+ reflect: false,
92
+ },
93
+ binWidth: {
94
+ attribute: false,
95
+ type: Number,
96
+ reflect: false,
97
+ },
98
+ trials: {
99
+ attribute: false,
100
+ type: Array,
101
+ reflect: false,
102
+ },
103
+
104
+ width: {
105
+ attribute: false,
106
+ type: Number,
107
+ reflect: false,
108
+ },
109
+ height: {
110
+ attribute: false,
111
+ type: Number,
112
+ reflect: false,
113
+ },
114
+ rem: {
115
+ attribute: false,
116
+ type: Number,
117
+ reflect: false,
118
+ },
119
+ };
120
+ }
121
+
122
+
123
+ constructor() {
124
+ super();
125
+
126
+ // Attributes
127
+ this.colors = ['outcome', 'response', 'stimulus', 'none']; // Allowable values of 'color'
128
+ this.color = 'outcome'; // How to color distributions and trials
129
+ this.distributions = false; // Show distributions?
130
+ this.threshold = false; // Show threshold?
131
+ this.unequal = false; // Allow unequal variance?
132
+ this.sensitivity = false; // Show d'?
133
+ this.bias = false; // Show c?
134
+ this.variance = false; // Show variance?
135
+ this.histogram = false; // Show histogram?
136
+
137
+ this.d = 1; // Sensitivity
138
+ this.c = 0; // Bias
139
+ this.s = 1; // Variance
140
+
141
+ // Properties
142
+ this.binWidth = 0.25; // Histogram bin width in units of evidence
143
+ this.signals = ['present', 'absent']; // Allowable values of trial.signal
144
+ this.responses = ['present', 'absent']; // Allowable values of trial.response
145
+ this.trials = []; // Array of simulated trials
146
+
147
+ this.width = NaN; // Width of component in pixels
148
+ this.height = NaN; // Height of component in pixels
149
+ this.rem = NaN; // Pixels per rem for component
150
+
151
+ // Private
152
+ this.muN = NaN; // Mean of noise distribution
153
+ this.muS = NaN; // Mean of signal distribution
154
+ this.l = NaN; // lambda (threshold location)
155
+ this.hS = NaN; // Height of signal distribution
156
+
157
+ this.binRange = [-3.0, 3.0]; // Range of histogram
158
+ this.h = 0; // Hits
159
+ this.m = 0; // Misses
160
+ this.fa = 0; // False alarms
161
+ this.cr = 0; // Correct rejections
162
+
163
+ this.firstUpdate = true; // Are we waiting for the first update?
164
+ this.drag = false; // Are we currently dragging?
165
+
166
+ this.alignState();
167
+ }
168
+
169
+ reset() {
170
+ this.trials = [];
171
+ this.h = 0;
172
+ this.m = 0;
173
+ this.fa = 0;
174
+ this.cr = 0;
175
+ }
176
+
177
+ trial(trialNumber, signal, duration, wait, iti) {
178
+ const trial = {};
179
+ trial.new = true;
180
+ trial.paused = false;
181
+ trial.trial = trialNumber;
182
+ trial.signal = signal;
183
+ trial.duration = duration;
184
+ trial.wait = wait;
185
+ trial.iti = iti;
186
+ trial.evidence = jStat.normal.sample(0, 1);
187
+
188
+ this.alignTrial(trial);
189
+
190
+ this.trials.push(trial);
191
+
192
+ this.requestUpdate();
193
+ }
194
+
195
+ alignTrial(trial) {
196
+ if (trial.signal === 'present') {
197
+ trial.trueEvidence = trial.evidence * this.s + this.muS;
198
+ trial.response = (trial.trueEvidence > this.l) ? 'present' : 'absent';
199
+ trial.outcome = (trial.response === 'present') ? 'h' : 'm';
200
+ } else { // trial.signal == 'absent'
201
+ trial.trueEvidence = trial.evidence + this.muN;
202
+ trial.response = (trial.trueEvidence > this.l) ? 'present' : 'absent';
203
+ trial.outcome = (trial.response === 'present') ? 'fa' : 'cr';
204
+ }
205
+ if (!trial.new) this[trial.outcome] += 1;
206
+ return trial;
207
+ }
208
+
209
+ alignState() {
210
+ this.far = SDTMath.dC2Far(this.d, this.c, this.s);
211
+ this.hr = SDTMath.dC2Hr(this.d, this.c, this.s);
212
+
213
+ this.muN = SDTMath.d2MuN(this.d, this.s);
214
+ this.muS = SDTMath.d2MuS(this.d, this.s);
215
+ this.l = SDTMath.c2L(this.c, this.s);
216
+ this.hS = SDTMath.s2H(this.s);
217
+
218
+ this.h = 0;
219
+ this.m = 0;
220
+ this.fa = 0;
221
+ this.cr = 0;
222
+ for (let i = 0; i < this.trials.length; i += 1) {
223
+ this.alignTrial(this.trials[i]);
224
+ }
225
+ }
226
+
227
+ static get styles() {
228
+ return [
229
+ super.styles,
230
+ css`
231
+ :host {
232
+ display: inline-block;
233
+
234
+ width: 27rem;
235
+ height: 15rem;
236
+ }
237
+
238
+ .main {
239
+ width: 100%;
240
+ height: 100%;
241
+ }
242
+
243
+ text {
244
+ /* stylelint-disable property-no-vendor-prefix */
245
+ -webkit-user-select: none;
246
+ -moz-user-select: none;
247
+ -ms-user-select: none;
248
+ user-select: none;
249
+ }
250
+
251
+ .tick {
252
+ font-size: 0.75rem;
253
+ }
254
+
255
+ .axis-x path,
256
+ .axis-x line,
257
+ .axis-y path,
258
+ .axis-y line,
259
+ .axis-y2 path,
260
+ .axis-y2 line {
261
+ stroke: var(---color-element-border);
262
+ }
263
+
264
+ .noise.interactive,
265
+ .signal.interactive,
266
+ .threshold.interactive {
267
+ cursor: ew-resize;
268
+
269
+ filter: url("#shadow-2");
270
+ outline: none;
271
+ }
272
+
273
+ .signal.unequal {
274
+ cursor: ns-resize;
275
+
276
+ filter: url("#shadow-2");
277
+ outline: none;
278
+ }
279
+
280
+ .signal.interactive.unequal {
281
+ cursor: move;
282
+ }
283
+
284
+ .noise.interactive:hover,
285
+ .signal.interactive:hover,
286
+ .signal.unequal:hover,
287
+ .threshold.interactive:hover {
288
+ filter: url("#shadow-4");
289
+
290
+ /* HACK: This gets Safari to correctly apply the filter! */
291
+ transform: translateX(0);
292
+ }
293
+
294
+ .noise.interactive:active,
295
+ .signal.interactive:active,
296
+ .signal.unequal:active,
297
+ .threshold.interactive:active {
298
+ filter: url("#shadow-8");
299
+
300
+ /* HACK: This gets Safari to correctly apply the filter! */
301
+ transform: translateY(0);
302
+ }
303
+
304
+ :host(.keyboard) .noise.interactive:focus,
305
+ :host(.keyboard) .signal.interactive:focus,
306
+ :host(.keyboard) .signal.unequal:focus,
307
+ :host(.keyboard) .threshold.interactive:focus {
308
+ filter: url("#shadow-8");
309
+
310
+ /* HACK: This gets Safari to correctly apply the filter! */
311
+ transform: translateZ(0);
312
+ }
313
+
314
+ .underlayer .background {
315
+ fill: var(---color-element-background);
316
+ stroke: none;
317
+ }
318
+
319
+ .overlayer .background {
320
+ fill: none;
321
+ stroke: var(---color-element-border);
322
+ stroke-width: 1;
323
+ shape-rendering: crispEdges;
324
+ }
325
+
326
+ .title-x,
327
+ .title-y,
328
+ .title-y2 {
329
+ font-weight: 600;
330
+
331
+ fill: currentColor;
332
+ }
333
+
334
+ .curve-cr,
335
+ .curve-fa,
336
+ .curve-m,
337
+ .curve-h {
338
+ fill-opacity: 0.5;
339
+ stroke: none;
340
+
341
+ transition: fill var(---transition-duration) ease;
342
+ }
343
+
344
+ .curve-cr {
345
+ fill: var(---color-cr);
346
+ }
347
+
348
+ .curve-fa {
349
+ fill: var(---color-fa);
350
+ }
351
+
352
+ .curve-m {
353
+ fill: var(---color-m);
354
+ }
355
+
356
+ .curve-h {
357
+ fill: var(---color-h);
358
+ }
359
+
360
+ :host([color="accuracy"]) .curve-h,
361
+ :host([color="accuracy"]) .curve-cr {
362
+ fill: var(---color-correct);
363
+ }
364
+
365
+ :host([color="accuracy"]) .curve-m,
366
+ :host([color="accuracy"]) .curve-fa {
367
+ fill: var(---color-error);
368
+ }
369
+
370
+ :host([color="stimulus"]) .curve-cr,
371
+ :host([color="stimulus"]) .curve-fa {
372
+ fill: var(---color-far);
373
+ }
374
+
375
+ :host([color="stimulus"]) .curve-m,
376
+ :host([color="stimulus"]) .curve-h {
377
+ fill: var(---color-hr);
378
+ }
379
+
380
+ :host([color="response"]) .curve-cr,
381
+ :host([color="response"]) .curve-m {
382
+ fill: var(---color-absent);
383
+ }
384
+
385
+ :host([color="response"]) .curve-fa,
386
+ :host([color="response"]) .curve-h {
387
+ fill: var(---color-present);
388
+ }
389
+
390
+ :host([color="none"]) .curve-cr,
391
+ :host([color="none"]) .curve-fa,
392
+ :host([color="none"]) .curve-m,
393
+ :host([color="none"]) .curve-h {
394
+ fill: var(---color-element-enabled);
395
+ }
396
+
397
+ .curve-noise,
398
+ .curve-signal {
399
+ fill: none;
400
+ stroke: var(---color-element-emphasis);
401
+ stroke-width: 2;
402
+ }
403
+
404
+ .measure-d,
405
+ .measure-c,
406
+ .measure-s {
407
+ pointer-events: none;
408
+ }
409
+
410
+ .threshold .line {
411
+ stroke: var(---color-element-emphasis);
412
+ stroke-width: 2;
413
+ }
414
+
415
+ .threshold .handle {
416
+ fill: var(---color-element-emphasis);
417
+
418
+ /* r: 6; HACK: Firefox does not support CSS SVG Geometry Properties */
419
+ }
420
+
421
+ /* Make a larger target for touch users */
422
+ @media (pointer: coarse) {
423
+ .threshold.interactive .handle {
424
+ stroke: #000000;
425
+ stroke-opacity: 0;
426
+ stroke-width: 12px;
427
+ }
428
+ }
429
+
430
+ .measure-d .line,
431
+ .measure-d .cap-left,
432
+ .measure-d .cap-right {
433
+ stroke: var(---color-d);
434
+ stroke-width: 2;
435
+ shape-rendering: crispEdges;
436
+ }
437
+
438
+ .measure-d .label {
439
+ font-size: 0.75rem;
440
+
441
+ text-anchor: start;
442
+ fill: currentColor;
443
+ }
444
+
445
+ .measure-c .line,
446
+ .measure-c .cap-zero {
447
+ stroke: var(---color-c);
448
+ stroke-width: 2;
449
+ shape-rendering: crispEdges;
450
+ }
451
+
452
+ .measure-c .label {
453
+ font-size: 0.75rem;
454
+
455
+ fill: currentColor;
456
+ }
457
+
458
+ .measure-s .line,
459
+ .measure-s .cap-left,
460
+ .measure-s .cap-right {
461
+ stroke: var(---color-s);
462
+ stroke-width: 2;
463
+ shape-rendering: crispEdges;
464
+ }
465
+
466
+ .measure-s .label {
467
+ font-size: 0.75rem;
468
+
469
+ text-anchor: middle;
470
+ fill: currentColor;
471
+ }
472
+ `,
473
+ ];
474
+ }
475
+
476
+ render() { // eslint-disable-line class-methods-use-this
477
+ return html`
478
+ ${DetectableElement.svgFilters}
479
+ `;
480
+ }
481
+
482
+ sendEvent() {
483
+ this.dispatchEvent(new CustomEvent('sdt-model-change', {
484
+ detail: {
485
+ d: this.d,
486
+ c: this.c,
487
+ s: this.s,
488
+ far: this.far,
489
+ hr: this.hr,
490
+ h: this.h,
491
+ m: this.m,
492
+ fa: this.fa,
493
+ cr: this.cr,
494
+ },
495
+ bubbles: true,
496
+ }));
497
+ }
498
+
499
+ getDimensions() {
500
+ this.width = parseFloat(this.getComputedStyleValue('width'), 10);
501
+ this.height = parseFloat(this.getComputedStyleValue('height'), 10);
502
+ this.rem = parseFloat(getComputedStyle(document.documentElement).getPropertyValue('font-size'), 10);
503
+ // console.log(`sdt-model: width = ${this.width}, height = ${this.height}, rem = ${this.rem}`);
504
+ }
505
+
506
+ connectedCallback() {
507
+ super.connectedCallback();
508
+ window.addEventListener('resize', this.getDimensions.bind(this));
509
+ }
510
+
511
+ disconnectedCallback() {
512
+ window.removeEventListener('resize', this.getDimensions.bind(this));
513
+ super.disconnectedCallback();
514
+ }
515
+
516
+ firstUpdated(changedProperties) {
517
+ super.firstUpdated(changedProperties);
518
+
519
+ // Get the width and height after initial render/update has occurred
520
+ // HACK Edge: Edge doesn't have width/height until after a 0ms timeout
521
+ window.setTimeout(this.getDimensions.bind(this), 0);
522
+ }
523
+
524
+ update(changedProperties) {
525
+ super.update(changedProperties);
526
+
527
+ this.alignState();
528
+
529
+ // Bail out if we can't get the width/height
530
+ if (Number.isNaN(this.width) || Number.isNaN(this.height) || Number.isNaN(this.rem)) {
531
+ return;
532
+ }
533
+
534
+ const hostWidth = this.width;
535
+ const hostHeight = this.height;
536
+ const hostAspectRatio = hostWidth / hostHeight;
537
+
538
+ const elementAspectRatio = 1.8;
539
+ let elementWidth;
540
+ let elementHeight;
541
+
542
+ if (hostAspectRatio > elementAspectRatio) {
543
+ elementHeight = hostHeight;
544
+ elementWidth = elementHeight * elementAspectRatio;
545
+ } else {
546
+ elementWidth = hostWidth;
547
+ elementHeight = elementWidth / elementAspectRatio;
548
+ }
549
+
550
+ const margin = {
551
+ top: 2 * this.rem,
552
+ bottom: 3 * this.rem,
553
+ left: 3 * this.rem,
554
+ right: ((this.histogram && this.distributions) ? 3 : 0.75) * this.rem,
555
+ };
556
+ const height = elementHeight - (margin.top + margin.bottom);
557
+ const width = elementWidth - (margin.left + margin.right);
558
+
559
+ const transitionDuration = parseInt(this.getComputedStyleValue('---transition-duration'), 10);
560
+
561
+ // X Scale
562
+ const xScale = d3.scaleLinear()
563
+ .domain([-3, 3]) // Evidence // FIX - no hardcoding
564
+ .range([0, width]);
565
+
566
+ // Y Scale
567
+ const yScale = d3.scaleLinear()
568
+ .domain([0.5, 0]) // Probability // FIX - no hardcoding
569
+ .range([0, height]);
570
+
571
+ // 2nd Y Scale
572
+ const strokeWidth = 3; // FIX - no hardcoding
573
+ const binWidth = xScale(this.binWidth) - xScale(0);
574
+ const y2Scale = d3.scaleLinear()
575
+ .domain([height / binWidth, 0]) // Number of Stimuli
576
+ .range([0, height]);
577
+
578
+ // Threshold Drag behavior
579
+ const dragThreshold = d3.drag()
580
+ .subject(() => {
581
+ return {x: xScale(this.l), y: 0};
582
+ })
583
+ .on('start', (event) => {
584
+ const element = event.currentTarget;
585
+ d3.select(element).classed('dragging', true);
586
+ })
587
+ .on('drag', (event) => {
588
+ this.drag = true;
589
+ let l = xScale.invert(event.x);
590
+ // Clamp lambda to stay visible
591
+ l = (l < xScale.domain()[0])
592
+ ? xScale.domain()[0]
593
+ : (l > xScale.domain()[1])
594
+ ? xScale.domain()[1]
595
+ : l;
596
+ this.c = SDTMath.l2C(l, this.s);
597
+ this.alignState();
598
+ this.sendEvent();
599
+ })
600
+ .on('end', (event) => {
601
+ const element = event.currentTarget;
602
+ d3.select(element).classed('dragging', false);
603
+ });
604
+
605
+ // Noise Curve Drag behavior
606
+ const dragNoise = d3.drag()
607
+ .subject(() => {
608
+ return {x: xScale(this.muN), y: 0};
609
+ })
610
+ .on('start', (event) => {
611
+ const element = event.currentTarget;
612
+ d3.select(element).classed('dragging', true);
613
+ })
614
+ .on('drag', (event) => {
615
+ this.drag = true;
616
+ let muN = xScale.invert(event.x);
617
+ // Clamp Noise Curve to stay visible
618
+ muN = (muN < xScale.domain()[0])
619
+ ? xScale.domain()[0]
620
+ : (muN > xScale.domain()[1])
621
+ ? xScale.domain()[1]
622
+ : muN;
623
+ this.d = SDTMath.muN2D(muN, this.s);
624
+ this.alignState();
625
+ this.sendEvent();
626
+ })
627
+ .on('end', (event) => {
628
+ const element = event.currentTarget;
629
+ d3.select(element).classed('dragging', false);
630
+ });
631
+
632
+ // Signal+Noise Curve Drag behavior
633
+ const dragSignal = d3.drag()
634
+ .subject(() => {
635
+ return {x: xScale(this.muS), y: yScale(this.hS)};
636
+ })
637
+ .on('start', (event, datum) => {
638
+ const element = event.currentTarget;
639
+ d3.select(element).classed('dragging', true);
640
+ datum.startX = event.x;
641
+ datum.startY = event.y;
642
+ datum.startHS = this.hS;
643
+ datum.startMuS = this.muS;
644
+ })
645
+ .on('drag', (event, datum) => {
646
+ this.drag = true;
647
+ let muS = this.muS; // eslint-disable-line prefer-destructuring
648
+ if (this.interactive) {
649
+ muS = xScale.invert(event.x);
650
+ // Clamp Signal Curve to stay visible
651
+ muS = (muS < xScale.domain()[0])
652
+ ? xScale.domain()[0]
653
+ : (muS > xScale.domain()[1])
654
+ ? xScale.domain()[1]
655
+ : muS;
656
+ }
657
+ let hS = this.hS; // eslint-disable-line prefer-destructuring
658
+ if (this.unequal) {
659
+ hS = yScale.invert(event.y);
660
+ // Clamp Signal Curve to stay visible
661
+ hS = (hS < 0.01)
662
+ ? 0.01
663
+ : (hS > yScale.domain()[0])
664
+ ? yScale.domain()[0]
665
+ : hS;
666
+ }
667
+ if (this.interactive && this.unequal) {
668
+ // Use shift key as modifier for single dimension
669
+ if (event.sourceEvent.shiftKey) {
670
+ if (Math.abs(event.x - datum.startX) > Math.abs(event.y - datum.startY)) {
671
+ hS = datum.startHS;
672
+ } else {
673
+ muS = datum.startMuS;
674
+ }
675
+ }
676
+ }
677
+ if (this.unequal) {
678
+ this.s = SDTMath.h2S(hS);
679
+ this.c = SDTMath.l2C(this.l, this.s);
680
+ }
681
+ this.d = SDTMath.muS2D(muS, this.s);
682
+ this.alignState();
683
+ this.sendEvent();
684
+ })
685
+ .on('end', (event) => {
686
+ const element = event.currentTarget;
687
+ d3.select(element).classed('dragging', false);
688
+ });
689
+
690
+ // Line for Evidence/Probability Space
691
+ const line = d3.line()
692
+ .x((datum) => { return xScale(datum.e); })
693
+ .y((datum) => { return yScale(datum.p); });
694
+
695
+ // Svg
696
+ // DATA-JOIN
697
+ const svgUpdate = d3.select(this.renderRoot).selectAll('.main')
698
+ .data([{
699
+ width: this.width,
700
+ height: this.height,
701
+ rem: this.rem,
702
+ }]);
703
+ // ENTER
704
+ const svgEnter = svgUpdate.enter().append('svg')
705
+ .classed('main', true);
706
+ // MERGE
707
+ const svgMerge = svgEnter.merge(svgUpdate)
708
+ .attr('viewBox', `0 0 ${elementWidth} ${elementHeight}`);
709
+
710
+ // Plot
711
+ // ENTER
712
+ const plotEnter = svgEnter.append('g')
713
+ .classed('plot', true);
714
+ // MERGE
715
+ const plotMerge = svgMerge.select('.plot')
716
+ .attr('transform', `translate(${margin.left}, ${margin.top})`);
717
+
718
+ // Underlayer
719
+ // ENTER
720
+ const underlayerEnter = plotEnter.append('g')
721
+ .classed('underlayer', true);
722
+ // MERGE
723
+ const underlayerMerge = plotMerge.select('.underlayer');
724
+
725
+ // Background
726
+ // ENTER
727
+ underlayerEnter.append('rect')
728
+ .classed('background', true);
729
+ // MERGE
730
+ underlayerMerge.select('.background')
731
+ .attr('height', height)
732
+ .attr('width', width);
733
+
734
+ // X Axis
735
+ // ENTER
736
+ underlayerEnter.append('g')
737
+ .classed('axis-x', true);
738
+ // MERGE
739
+ const axisXMerge = underlayerMerge.select('.axis-x')
740
+ .attr('transform', `translate(0, ${height})`)
741
+ .call(d3.axisBottom(xScale))
742
+ .attr('font-size', null)
743
+ .attr('font-family', null);
744
+ axisXMerge.selectAll('line, path')
745
+ .attr('stroke', null);
746
+
747
+ // X Axis Title
748
+ // ENTER
749
+ underlayerEnter.append('text')
750
+ .classed('title-x', true)
751
+ .attr('text-anchor', 'middle')
752
+ .text('Evidence');
753
+ // MERGE
754
+ underlayerMerge.select('.title-x')
755
+ .attr('transform', `translate(${width / 2}, ${height + (2.25 * this.rem)})`);
756
+
757
+ // Y Axis
758
+ // DATA-JOIN
759
+ const axisYUpdate = underlayerMerge.selectAll('.axis-y')
760
+ .data(this.distributions ? [{}] : []);
761
+ // ENTER
762
+ const axisYEnter = axisYUpdate.enter().append('g')
763
+ .classed('axis-y', true);
764
+ // MERGE
765
+ const axisYMerge = axisYEnter.merge(axisYUpdate)
766
+ .call(d3.axisLeft(yScale).ticks(5))
767
+ .attr('font-size', null)
768
+ .attr('font-family', null);
769
+ axisYMerge.selectAll('line, path')
770
+ .attr('stroke', null);
771
+ // EXIT
772
+ axisYUpdate.exit().remove();
773
+
774
+ // Y Axis Title
775
+ // DATA-JOIN
776
+ const titleYUpdate = underlayerMerge.selectAll('.title-y')
777
+ .data(this.distributions ? [{}] : []);
778
+ // ENTER
779
+ const titleYEnter = titleYUpdate.enter().append('text')
780
+ .classed('title-y', true)
781
+ .attr('text-anchor', 'middle')
782
+ .text('Probability');
783
+ // MERGE
784
+ titleYEnter.merge(titleYUpdate)
785
+ .attr('transform', `translate(${-2 * this.rem}, ${height / 2})rotate(-90)`);
786
+ // EXIT
787
+ titleYUpdate.exit().remove();
788
+
789
+ // 2nd Y Axis
790
+ // DATA-JOIN
791
+ const axisY2Update = underlayerMerge.selectAll('.axis-y2')
792
+ .data(this.histogram ? [{}] : []);
793
+ // ENTER
794
+ const axisY2Enter = axisY2Update.enter().append('g')
795
+ .classed('axis-y2', true);
796
+ // MERGE
797
+ const axisY2Merge = axisY2Enter.merge(axisY2Update)
798
+ .attr('transform', this.distributions ? `translate(${width}, 0)` : '')
799
+ .call(this.distributions ? d3.axisRight(y2Scale).ticks(10) : d3.axisLeft(y2Scale).ticks(10))
800
+ .attr('font-size', null)
801
+ .attr('font-family', null);
802
+ axisY2Merge.selectAll('line, path')
803
+ .attr('stroke', null);
804
+ // EXIT
805
+ axisY2Update.exit().remove();
806
+
807
+ // 2nd Y Axis Title
808
+ // DATA-JOIN
809
+ const titleY2Update = underlayerMerge.selectAll('.title-y2')
810
+ .data(this.histogram ? [{}] : []);
811
+ // ENTER
812
+ const titleY2Enter = titleY2Update.enter().append('text')
813
+ .classed('title-y2', true)
814
+ .attr('text-anchor', 'middle')
815
+ .text('Count');
816
+ // MERGE
817
+ titleY2Enter.merge(titleY2Update)
818
+ .attr('transform', this.distributions
819
+ ? `translate(${width + (1.5 * this.rem)}, ${height / 2})rotate(90)`
820
+ : `translate(${-1.5 * this.rem}, ${height / 2})rotate(-90)`);
821
+ // EXIT
822
+ titleY2Update.exit().remove();
823
+
824
+ // Plot Content
825
+ plotEnter.append('g')
826
+ .classed('content', true);
827
+ // MERGE
828
+ const contentMerge = plotMerge.select('.content');
829
+
830
+ // Noise & Signal + Noise Distributions
831
+ // DATA-JOIN
832
+ const signalNoiseUpdate = contentMerge.selectAll('.signal-noise')
833
+ .data(this.distributions ? [{}] : []);
834
+ // ENTER
835
+ const signalNoiseEnter = signalNoiseUpdate.enter().append('g')
836
+ .classed('signal-noise', true);
837
+ // MERGE
838
+ const signalNoiseMerge = signalNoiseEnter.merge(signalNoiseUpdate);
839
+ // EXIT
840
+ signalNoiseUpdate.exit().remove();
841
+
842
+ // Noise Distribution
843
+ // ENTER
844
+ const noiseEnter = signalNoiseEnter.append('g')
845
+ .classed('noise', true);
846
+ // MERGE
847
+ const noiseMerge = signalNoiseMerge.selectAll('.noise')
848
+ .attr('tabindex', this.interactive ? 0 : null)
849
+ .classed('interactive', this.interactive)
850
+ .on('keydown', this.interactive
851
+ ? (event) => {
852
+ if (['ArrowRight', 'ArrowLeft'].includes(event.key)) {
853
+ let muN = this.muN; // eslint-disable-line prefer-destructuring
854
+ switch (event.key) {
855
+ case 'ArrowRight':
856
+ muN += event.shiftKey ? 0.01 : 0.1;
857
+ break;
858
+ case 'ArrowLeft':
859
+ muN -= event.shiftKey ? 0.01 : 0.1;
860
+ break;
861
+ default:
862
+ }
863
+ // Clamp C to visible extent
864
+ muN = (muN < xScale.domain()[0])
865
+ ? xScale.domain()[0]
866
+ : (muN > xScale.domain()[1])
867
+ ? xScale.domain()[1]
868
+ : muN;
869
+ if (muN !== this.muN) {
870
+ this.d = SDTMath.muN2D(muN, this.s);
871
+ this.alignState();
872
+ this.sendEvent();
873
+ }
874
+ event.preventDefault();
875
+ }
876
+ }
877
+ : null);
878
+ if (
879
+ this.firstUpdate
880
+ || changedProperties.has('interactive')
881
+ ) {
882
+ if (this.interactive) {
883
+ noiseMerge.call(dragNoise);
884
+ } else {
885
+ noiseMerge.on('.drag', null);
886
+ }
887
+ }
888
+
889
+ // CR Curve
890
+ // ENTER
891
+ noiseEnter.append('path')
892
+ .classed('curve-cr', true);
893
+ // MERGE
894
+ noiseMerge.select('.curve-cr').transition()
895
+ .duration(this.drag ? 0 : transitionDuration)
896
+ .ease(d3.easeCubicOut)
897
+ .attrTween('d', (datum, index, elements) => {
898
+ const element = elements[index];
899
+ const interpolateD = d3.interpolate(
900
+ (element.d !== undefined) ? element.d : this.d,
901
+ this.d,
902
+ );
903
+ const interpolateC = d3.interpolate(
904
+ (element.c !== undefined) ? element.c : this.c,
905
+ this.c,
906
+ );
907
+ const interpolateS = d3.interpolate(
908
+ (element.s !== undefined) ? element.s : this.s,
909
+ this.s,
910
+ );
911
+ return (time) => {
912
+ element.d = interpolateD(time);
913
+ element.c = interpolateC(time);
914
+ element.s = interpolateS(time);
915
+ const correctRejections = d3.range(
916
+ xScale.domain()[0],
917
+ SDTMath.c2L(element.c, element.s),
918
+ 0.05,
919
+ ).map((e) => {
920
+ return {
921
+ e: e,
922
+ p: jStat.normal.pdf(e, SDTMath.d2MuN(element.d, element.s), 1),
923
+ };
924
+ });
925
+ correctRejections.push({
926
+ e: SDTMath.c2L(element.c, element.s),
927
+ p: jStat.normal.pdf(
928
+ SDTMath.c2L(element.c, element.s),
929
+ SDTMath.d2MuN(element.d, element.s),
930
+ 1,
931
+ ),
932
+ });
933
+ correctRejections.push({
934
+ e: SDTMath.c2L(element.c, element.s),
935
+ p: 0,
936
+ });
937
+ correctRejections.push({
938
+ e: xScale.domain()[0],
939
+ p: 0,
940
+ });
941
+ return line(correctRejections);
942
+ };
943
+ });
944
+
945
+ // FA Curve
946
+ // ENTER
947
+ noiseEnter.append('path')
948
+ .classed('curve-fa', true);
949
+ // MERGE
950
+ noiseMerge.select('.curve-fa').transition()
951
+ .duration(this.drag ? 0 : transitionDuration)
952
+ .ease(d3.easeCubicOut)
953
+ .attrTween('d', (datum, index, elements) => {
954
+ const element = elements[index];
955
+ const interpolateD = d3.interpolate(
956
+ (element.d !== undefined) ? element.d : this.d,
957
+ this.d,
958
+ );
959
+ const interpolateC = d3.interpolate(
960
+ (element.c !== undefined) ? element.c : this.c,
961
+ this.c,
962
+ );
963
+ const interpolateS = d3.interpolate(
964
+ (element.s !== undefined) ? element.s : this.s,
965
+ this.s,
966
+ );
967
+ return (time) => {
968
+ element.d = interpolateD(time);
969
+ element.c = interpolateC(time);
970
+ element.s = interpolateS(time);
971
+ const falseAlarms = d3.range(
972
+ SDTMath.c2L(element.c, element.s),
973
+ xScale.domain()[1],
974
+ 0.05,
975
+ ).map((e) => {
976
+ return {
977
+ e: e,
978
+ p: jStat.normal.pdf(e, SDTMath.d2MuN(element.d, element.s), 1),
979
+ };
980
+ });
981
+ falseAlarms.push({
982
+ e: xScale.domain()[1],
983
+ p: jStat.normal.pdf(xScale.domain()[1], SDTMath.d2MuN(element.d, element.s), 1),
984
+ });
985
+ falseAlarms.push({
986
+ e: xScale.domain()[1],
987
+ p: 0,
988
+ });
989
+ falseAlarms.push({
990
+ e: SDTMath.c2L(element.c, element.s),
991
+ p: 0,
992
+ });
993
+ return line(falseAlarms);
994
+ };
995
+ });
996
+
997
+ // Noise Curve
998
+ // ENTER
999
+ noiseEnter.append('path')
1000
+ .classed('curve-noise', true);
1001
+ // MERGE
1002
+ noiseMerge.select('.curve-noise').transition()
1003
+ .duration(this.drag ? 0 : transitionDuration)
1004
+ .ease(d3.easeCubicOut)
1005
+ .attrTween('d', (datum, index, elements) => {
1006
+ const element = elements[index];
1007
+ const interpolateD = d3.interpolate(
1008
+ (element.d !== undefined) ? element.d : this.d,
1009
+ this.d,
1010
+ );
1011
+ const interpolateS = d3.interpolate(
1012
+ (element.s !== undefined) ? element.s : this.s,
1013
+ this.s,
1014
+ );
1015
+ return (time) => {
1016
+ element.d = interpolateD(time);
1017
+ element.s = interpolateS(time);
1018
+ const noise = d3.range(
1019
+ xScale.domain()[0],
1020
+ xScale.domain()[1],
1021
+ 0.05,
1022
+ ).map((e) => {
1023
+ return {
1024
+ e: e,
1025
+ p: jStat.normal.pdf(e, SDTMath.d2MuN(element.d, element.s), 1),
1026
+ };
1027
+ });
1028
+ noise.push({
1029
+ e: xScale.domain()[1],
1030
+ p: jStat.normal.pdf(xScale.domain()[1], SDTMath.d2MuN(element.d, element.s), 1),
1031
+ });
1032
+ return line(noise);
1033
+ };
1034
+ });
1035
+
1036
+ // Signal + Noise Distribution
1037
+ // ENTER
1038
+ const signalEnter = signalNoiseEnter.append('g')
1039
+ .classed('signal', true);
1040
+ // MERGE
1041
+ const signalMerge = signalNoiseMerge.selectAll('.signal')
1042
+ .attr('tabindex', (this.interactive || this.unequal) ? 0 : null)
1043
+ .classed('interactive', this.interactive)
1044
+ .classed('unequal', this.unequal)
1045
+ .on('keydown.sensitivity', this.interactive
1046
+ ? (event) => {
1047
+ if (['ArrowRight', 'ArrowLeft'].includes(event.key)) {
1048
+ let muS = this.muS; // eslint-disable-line prefer-destructuring
1049
+ switch (event.key) {
1050
+ case 'ArrowRight':
1051
+ muS += event.shiftKey ? 0.01 : 0.1;
1052
+ break;
1053
+ case 'ArrowLeft':
1054
+ muS -= event.shiftKey ? 0.01 : 0.1;
1055
+ break;
1056
+ default:
1057
+ }
1058
+ // Clamp C to visible extent
1059
+ muS = (muS < xScale.domain()[0])
1060
+ ? xScale.domain()[0]
1061
+ : (muS > xScale.domain()[1])
1062
+ ? xScale.domain()[1]
1063
+ : muS;
1064
+ if (muS !== this.muS) {
1065
+ this.d = SDTMath.muS2D(muS, this.s);
1066
+ this.alignState();
1067
+ this.sendEvent();
1068
+ }
1069
+ event.preventDefault();
1070
+ }
1071
+ }
1072
+ : null)
1073
+ .on('keydown.variance', this.unequal
1074
+ ? (event) => {
1075
+ if (['ArrowUp', 'ArrowDown'].includes(event.key)) {
1076
+ let hS = this.hS; // eslint-disable-line prefer-destructuring
1077
+ switch (event.key) {
1078
+ case 'ArrowUp':
1079
+ hS += event.shiftKey ? 0.002 : 0.02;
1080
+ break;
1081
+ case 'ArrowDown':
1082
+ hS -= event.shiftKey ? 0.002 : 0.02;
1083
+ break;
1084
+ default:
1085
+ }
1086
+ // Clamp s so distribution stays visible
1087
+ hS = (hS < 0.01)
1088
+ ? 0.01
1089
+ : (hS > yScale.domain()[0])
1090
+ ? yScale.domain()[0]
1091
+ : hS;
1092
+ if (hS !== this.hS) {
1093
+ this.s = SDTMath.h2S(hS);
1094
+ this.d = SDTMath.muN2D(this.muN, this.s);
1095
+ this.c = SDTMath.l2C(this.l, this.s);
1096
+ this.alignState();
1097
+ this.sendEvent();
1098
+ }
1099
+ event.preventDefault();
1100
+ }
1101
+ }
1102
+ : null);
1103
+ if (
1104
+ this.firstUpdate
1105
+ || changedProperties.has('interactive')
1106
+ || changedProperties.has('unequal')
1107
+ ) {
1108
+ if (this.interactive || this.unequal) {
1109
+ signalMerge.call(dragSignal);
1110
+ } else {
1111
+ signalMerge.on('.drag', null);
1112
+ }
1113
+ }
1114
+
1115
+ // M Curve
1116
+ // ENTER
1117
+ signalEnter.append('path')
1118
+ .classed('curve-m', true);
1119
+ // MERGE
1120
+ signalMerge.select('.curve-m').transition()
1121
+ .duration(this.drag ? 0 : transitionDuration)
1122
+ .ease(d3.easeCubicOut)
1123
+ .attrTween('d', (datum, index, elements) => {
1124
+ const element = elements[index];
1125
+ const interpolateD = d3.interpolate(
1126
+ (element.d !== undefined) ? element.d : this.d,
1127
+ this.d,
1128
+ );
1129
+ const interpolateC = d3.interpolate(
1130
+ (element.c !== undefined) ? element.c : this.c,
1131
+ this.c,
1132
+ );
1133
+ const interpolateS = d3.interpolate(
1134
+ (element.s !== undefined) ? element.s : this.s,
1135
+ this.s,
1136
+ );
1137
+ return (time) => {
1138
+ element.d = interpolateD(time);
1139
+ element.c = interpolateC(time);
1140
+ element.s = interpolateS(time);
1141
+ const misses = d3.range(
1142
+ xScale.domain()[0],
1143
+ SDTMath.c2L(element.c, element.s),
1144
+ 0.05,
1145
+ ).map((e) => {
1146
+ return {
1147
+ e: e,
1148
+ p: jStat.normal.pdf(e, SDTMath.d2MuS(element.d, element.s), element.s),
1149
+ };
1150
+ });
1151
+ misses.push({
1152
+ e: SDTMath.c2L(element.c, element.s),
1153
+ p: jStat.normal.pdf(
1154
+ SDTMath.c2L(element.c, element.s),
1155
+ SDTMath.d2MuS(element.d, element.s),
1156
+ element.s,
1157
+ ),
1158
+ });
1159
+ misses.push({
1160
+ e: SDTMath.c2L(element.c, element.s),
1161
+ p: 0,
1162
+ });
1163
+ misses.push({
1164
+ e: xScale.domain()[0],
1165
+ p: 0,
1166
+ });
1167
+ return line(misses);
1168
+ };
1169
+ });
1170
+
1171
+ // H Curve
1172
+ // ENTER
1173
+ signalEnter.append('path')
1174
+ .classed('curve-h', true);
1175
+ // MERGE
1176
+ signalMerge.select('.curve-h').transition()
1177
+ .duration(this.drag ? 0 : transitionDuration)
1178
+ .ease(d3.easeCubicOut)
1179
+ .attrTween('d', (datum, index, elements) => {
1180
+ const element = elements[index];
1181
+ const interpolateD = d3.interpolate(
1182
+ (element.d !== undefined) ? element.d : this.d,
1183
+ this.d,
1184
+ );
1185
+ const interpolateC = d3.interpolate(
1186
+ (element.c !== undefined) ? element.c : this.c,
1187
+ this.c,
1188
+ );
1189
+ const interpolateS = d3.interpolate(
1190
+ (element.s !== undefined) ? element.s : this.s,
1191
+ this.s,
1192
+ );
1193
+ return (time) => {
1194
+ element.d = interpolateD(time);
1195
+ element.c = interpolateC(time);
1196
+ element.s = interpolateS(time);
1197
+ const hits = d3.range(
1198
+ SDTMath.c2L(element.c, element.s),
1199
+ xScale.domain()[1],
1200
+ 0.05,
1201
+ ).map((e) => {
1202
+ return {
1203
+ e: e,
1204
+ p: jStat.normal.pdf(e, SDTMath.d2MuS(element.d, element.s), element.s),
1205
+ };
1206
+ });
1207
+ hits.push({
1208
+ e: xScale.domain()[1],
1209
+ p: jStat.normal.pdf(
1210
+ xScale.domain()[1],
1211
+ SDTMath.d2MuS(element.d, element.s),
1212
+ element.s,
1213
+ ),
1214
+ });
1215
+ hits.push({
1216
+ e: xScale.domain()[1],
1217
+ p: 0,
1218
+ });
1219
+ hits.push({
1220
+ e: SDTMath.c2L(element.c, element.s),
1221
+ p: 0,
1222
+ });
1223
+ return line(hits);
1224
+ };
1225
+ });
1226
+
1227
+ // Signal Curve
1228
+ // ENTER
1229
+ signalEnter.append('path')
1230
+ .classed('curve-signal', true);
1231
+ // MERGE
1232
+ signalMerge.select('.curve-signal').transition()
1233
+ .duration(this.drag ? 0 : transitionDuration)
1234
+ .ease(d3.easeCubicOut)
1235
+ .attrTween('d', (datum, index, elements) => {
1236
+ const element = elements[index];
1237
+ const interpolateD = d3.interpolate(
1238
+ (element.d !== undefined) ? element.d : this.d,
1239
+ this.d,
1240
+ );
1241
+ const interpolateS = d3.interpolate(
1242
+ (element.s !== undefined) ? element.s : this.s,
1243
+ this.s,
1244
+ );
1245
+ return (time) => {
1246
+ element.d = interpolateD(time);
1247
+ element.s = interpolateS(time);
1248
+ const signal = d3.range(
1249
+ xScale.domain()[0],
1250
+ xScale.domain()[1],
1251
+ 0.05,
1252
+ ).map((e) => {
1253
+ return {
1254
+ e: e,
1255
+ p: jStat.normal.pdf(e, SDTMath.d2MuS(element.d, element.s), element.s),
1256
+ };
1257
+ });
1258
+ signal.push({
1259
+ e: xScale.domain()[1],
1260
+ p: jStat.normal.pdf(
1261
+ xScale.domain()[1],
1262
+ SDTMath.d2MuS(element.d, element.s),
1263
+ element.s,
1264
+ ),
1265
+ });
1266
+ return line(signal);
1267
+ };
1268
+ });
1269
+
1270
+ // d' Measure
1271
+ // DATA-JOIN
1272
+ const dUpdate = contentMerge.selectAll('.measure-d')
1273
+ .data(this.sensitivity ? [{}] : []);
1274
+ // ENTER
1275
+ const dEnter = dUpdate.enter().append('g')
1276
+ .classed('measure-d', true);
1277
+ dEnter.append('line')
1278
+ .classed('line', true);
1279
+ dEnter.append('line')
1280
+ .classed('cap-left', true);
1281
+ dEnter.append('line')
1282
+ .classed('cap-right', true);
1283
+ const dLabel = dEnter.append('text')
1284
+ .classed('label', true);
1285
+ dLabel.append('tspan')
1286
+ .classed('d math-var', true)
1287
+ .text('d′');
1288
+ dLabel.append('tspan')
1289
+ .classed('equals', true)
1290
+ .text(' = ');
1291
+ dLabel.append('tspan')
1292
+ .classed('value', true);
1293
+ // MERGE
1294
+ const dMerge = dEnter.merge(dUpdate);
1295
+ dMerge.select('.line').transition()
1296
+ .duration(this.drag ? 0 : transitionDuration)
1297
+ .ease(d3.easeCubicOut)
1298
+ .attr('x1', xScale(this.muN))
1299
+ .attr('y1', yScale(0.43)) // FIX - no hardcoding
1300
+ .attr('x2', xScale(this.muS))
1301
+ .attr('y2', yScale(0.43)); // FIX - no hardcoding
1302
+ dMerge.select('.cap-left').transition()
1303
+ .duration(this.drag ? 0 : transitionDuration)
1304
+ .ease(d3.easeCubicOut)
1305
+ .attr('x1', xScale(this.muN))
1306
+ .attr('y1', yScale(0.43) + 5) // FIX - no hardcoding
1307
+ .attr('x2', xScale(this.muN))
1308
+ .attr('y2', yScale(0.43) - 5); // FIX - no hardcoding
1309
+ dMerge.select('.cap-right').transition()
1310
+ .duration(this.drag ? 0 : transitionDuration)
1311
+ .ease(d3.easeCubicOut)
1312
+ .attr('x1', xScale(this.muS))
1313
+ .attr('y1', yScale(0.43) + 5) // FIX - no hardcoding
1314
+ .attr('x2', xScale(this.muS))
1315
+ .attr('y2', yScale(0.43) - 5); // FIX - no hardcoding
1316
+ const dLabelTransition = dMerge.select('.label').transition()
1317
+ .duration(this.drag ? 0 : transitionDuration)
1318
+ .ease(d3.easeCubicOut)
1319
+ .attr('x', xScale((this.muN > this.muS) ? this.muN : this.muS) + 5)
1320
+ .attr('y', yScale(0.43) + 3); // FIX - no hardcoding
1321
+ dLabelTransition.select('.value')
1322
+ .tween('text', (datum, index, elements) => {
1323
+ const element = elements[index];
1324
+ const interpolateD = d3.interpolate(
1325
+ (element.d !== undefined) ? element.d : this.d,
1326
+ this.d,
1327
+ );
1328
+ return (time) => {
1329
+ element.d = interpolateD(time);
1330
+ d3.select(element).text(+(element.d).toFixed(3));
1331
+ };
1332
+ });
1333
+ // EXIT
1334
+ dUpdate.exit().remove();
1335
+
1336
+ // c Measure
1337
+ // DATA-JOIN
1338
+ const cUpdate = contentMerge.selectAll('.measure-c')
1339
+ .data(this.bias ? [{}] : []);
1340
+ // ENTER
1341
+ const cEnter = cUpdate.enter().append('g')
1342
+ .classed('measure-c', true);
1343
+ cEnter.append('line')
1344
+ .classed('line', true);
1345
+ cEnter.append('line')
1346
+ .classed('cap-zero', true);
1347
+ const cLabel = cEnter.append('text')
1348
+ .classed('label', true);
1349
+ cLabel.append('tspan')
1350
+ .classed('c math-var', true)
1351
+ .text('c');
1352
+ cLabel.append('tspan')
1353
+ .classed('equals', true)
1354
+ .text(' = ');
1355
+ cLabel.append('tspan')
1356
+ .classed('value', true);
1357
+ // MERGE
1358
+ const cMerge = cEnter.merge(cUpdate);
1359
+ cMerge.select('.line').transition()
1360
+ .duration(this.drag ? 0 : transitionDuration)
1361
+ .ease(d3.easeCubicOut)
1362
+ .attr('x1', xScale(this.l))
1363
+ .attr('y1', yScale(0.47)) // FIX - no hardcoding
1364
+ .attr('x2', xScale(0))
1365
+ .attr('y2', yScale(0.47)); // FIX - no hardcoding
1366
+ cMerge.select('.cap-zero').transition()
1367
+ .duration(this.drag ? 0 : transitionDuration)
1368
+ .ease(d3.easeCubicOut)
1369
+ .attr('x1', xScale(0))
1370
+ .attr('y1', yScale(0.47) + 5) // FIX - no hardcoding
1371
+ .attr('x2', xScale(0))
1372
+ .attr('y2', yScale(0.47) - 5); // FIX - no hardcoding
1373
+ const cLabelTransition = cMerge.select('.label').transition()
1374
+ .duration(this.drag ? 0 : transitionDuration)
1375
+ .ease(d3.easeCubicOut)
1376
+ .attr('x', xScale(0) + ((this.l < 0) ? 5 : -5))
1377
+ .attr('y', yScale(0.47) + 3) // FIX - no hardcoding
1378
+ .attr('text-anchor', (this.c < 0) ? 'start' : 'end');
1379
+ cLabelTransition.select('.value')
1380
+ .tween('text', (datum, index, elements) => {
1381
+ const element = elements[index];
1382
+ const interpolateC = d3.interpolate(
1383
+ (element.c !== undefined) ? element.c : this.c,
1384
+ this.c,
1385
+ );
1386
+ return (time) => {
1387
+ element.c = interpolateC(time);
1388
+ d3.select(element).text(+(element.c).toFixed(3));
1389
+ };
1390
+ });
1391
+ // EXIT
1392
+ cUpdate.exit().remove();
1393
+
1394
+ // s Measure
1395
+ // DATA-JOIN
1396
+ const sUpdate = contentMerge.selectAll('.measure-s')
1397
+ .data(this.variance ? [{}] : []);
1398
+ // ENTER
1399
+ const sEnter = sUpdate.enter().append('g')
1400
+ .classed('measure-s', true);
1401
+ sEnter.append('line')
1402
+ .classed('line', true);
1403
+ sEnter.append('line')
1404
+ .classed('cap-left', true);
1405
+ sEnter.append('line')
1406
+ .classed('cap-right', true);
1407
+ const sLabel = sEnter.append('text')
1408
+ .classed('label', true);
1409
+ sLabel.append('tspan')
1410
+ .classed('s math-var', true)
1411
+ .text('σ');
1412
+ sLabel.append('tspan')
1413
+ .classed('equals', true)
1414
+ .text(' = ');
1415
+ sLabel.append('tspan')
1416
+ .classed('value', true);
1417
+ // MERGE
1418
+ const sMerge = sEnter.merge(sUpdate);
1419
+ sMerge.select('.line').transition()
1420
+ .duration(this.drag ? 0 : transitionDuration)
1421
+ .ease(d3.easeCubicOut)
1422
+ .attr('x1', xScale(this.muS - this.s))
1423
+ .attr('y1', yScale(jStat.normal.pdf(this.s, 0, this.s)) + (10 / this.s)) // FIX - no hardcoding
1424
+ .attr('x2', xScale(this.muS + this.s))
1425
+ .attr('y2', yScale(jStat.normal.pdf(this.s, 0, this.s)) + (10 / this.s)); // FIX - no hardcoding
1426
+ sMerge.select('.cap-left').transition()
1427
+ .duration(this.drag ? 0 : transitionDuration)
1428
+ .ease(d3.easeCubicOut)
1429
+ .attr('x1', xScale(this.muS - this.s))
1430
+ .attr('y1', yScale(jStat.normal.pdf(this.s, 0, this.s)) + (10 / this.s) + 5) // FIX - no hardcoding
1431
+ .attr('x2', xScale(this.muS - this.s))
1432
+ .attr('y2', yScale(jStat.normal.pdf(this.s, 0, this.s)) + (10 / this.s) - 5); // FIX - no hardcoding
1433
+ sMerge.select('.cap-right').transition()
1434
+ .duration(this.drag ? 0 : transitionDuration)
1435
+ .ease(d3.easeCubicOut)
1436
+ .attr('x1', xScale(this.muS + this.s))
1437
+ .attr('y1', yScale(jStat.normal.pdf(this.s, 0, this.s)) + (10 / this.s) + 5) // FIX - no hardcoding
1438
+ .attr('x2', xScale(this.muS + this.s))
1439
+ .attr('y2', yScale(jStat.normal.pdf(this.s, 0, this.s)) + (10 / this.s) - 5); // FIX - no hardcoding
1440
+ const sLabelTransition = sMerge.select('.label').transition()
1441
+ .duration(this.drag ? 0 : transitionDuration)
1442
+ .ease(d3.easeCubicOut)
1443
+ .attr('x', xScale(this.muS))
1444
+ .attr('y', yScale(jStat.normal.pdf(this.s, 0, this.s)) + (10 / this.s) - 3); // FIX - no hardcoding
1445
+ sLabelTransition.select('.value')
1446
+ .tween('text', (datum, index, elements) => {
1447
+ const element = elements[index];
1448
+ const interpolateS = d3.interpolate(
1449
+ (element.s !== undefined) ? element.s : this.s,
1450
+ this.s,
1451
+ );
1452
+ return (time) => {
1453
+ element.s = interpolateS(time);
1454
+ d3.select(element).text(+(element.s).toFixed(3));
1455
+ };
1456
+ });
1457
+ // EXIT
1458
+ sUpdate.exit().remove();
1459
+
1460
+ // Threshold Line
1461
+ // DATA-JOIN
1462
+ const thresholdUpdate = contentMerge.selectAll('.threshold')
1463
+ .data(this.threshold ? [{}] : []);
1464
+ // ENTER
1465
+ const thresholdEnter = thresholdUpdate.enter().append('g')
1466
+ .classed('threshold', true);
1467
+ thresholdEnter.append('line')
1468
+ .classed('line', true);
1469
+ thresholdEnter.append('circle')
1470
+ .classed('handle', true)
1471
+ .attr('r', 6); /* HACK: Firefox does not support CSS SVG Geometry Properties */
1472
+ // MERGE
1473
+ const thresholdMerge = thresholdEnter.merge(thresholdUpdate)
1474
+ .attr('tabindex', this.interactive ? 0 : null)
1475
+ .classed('interactive', this.interactive);
1476
+ if (
1477
+ this.firstUpdate
1478
+ || changedProperties.has('interactive')
1479
+ ) {
1480
+ if (this.interactive) {
1481
+ thresholdMerge
1482
+ .call(dragThreshold)
1483
+ .on('keydown', (event) => {
1484
+ if (['ArrowRight', 'ArrowLeft'].includes(event.key)) {
1485
+ let l = this.l; // eslint-disable-line prefer-destructuring
1486
+ switch (event.key) {
1487
+ case 'ArrowRight':
1488
+ l += event.shiftKey ? 0.01 : 0.1;
1489
+ break;
1490
+ case 'ArrowLeft':
1491
+ l -= event.shiftKey ? 0.01 : 0.1;
1492
+ break;
1493
+ default:
1494
+ }
1495
+ // Clamp C to visible extent
1496
+ l = (l < xScale.domain()[0])
1497
+ ? xScale.domain()[0]
1498
+ : (l > xScale.domain()[1])
1499
+ ? xScale.domain()[1]
1500
+ : l;
1501
+ if (l !== this.l) {
1502
+ this.c = SDTMath.l2C(l, this.s);
1503
+ this.alignState();
1504
+ this.sendEvent();
1505
+ }
1506
+ event.preventDefault();
1507
+ }
1508
+ });
1509
+ } else {
1510
+ thresholdMerge
1511
+ .on('drag', null)
1512
+ .on('keydown', null);
1513
+ }
1514
+ }
1515
+ thresholdMerge.select('.line').transition()
1516
+ .duration(this.drag ? 0 : transitionDuration)
1517
+ .ease(d3.easeCubicOut)
1518
+ .attr('x1', xScale(this.l))
1519
+ .attr('y1', yScale(0))
1520
+ .attr('x2', xScale(this.l))
1521
+ .attr('y2', yScale(0.54));
1522
+ thresholdMerge.select('.handle').transition()
1523
+ .duration(this.drag ? 0 : transitionDuration)
1524
+ .ease(d3.easeCubicOut)
1525
+ .attr('cx', xScale(this.l))
1526
+ .attr('cy', yScale(0.54));
1527
+ // EXIT
1528
+ thresholdUpdate.exit().remove();
1529
+
1530
+ // Histogram
1531
+ // DATA-JOIN
1532
+ const histogramUpdate = contentMerge.selectAll('.histogram')
1533
+ .data(this.histogram ? [{}] : []);
1534
+ // ENTER
1535
+ const histogramEnter = histogramUpdate.enter().append('g')
1536
+ .classed('histogram', true);
1537
+ // MERGE
1538
+ const histogramMerge = histogramEnter.merge(histogramUpdate);
1539
+ // EXIT
1540
+ histogramUpdate.exit().remove();
1541
+
1542
+ // Trials
1543
+ if (this.histogram) {
1544
+ const histogram = d3.histogram()
1545
+ .value((datum) => { return datum.trueEvidence; })
1546
+ .domain(xScale.domain())
1547
+ .thresholds(d3.range(this.binRange[0], this.binRange[1], this.binWidth));
1548
+ const hist = histogram(this.trials);
1549
+ let binCountLeft = -1;
1550
+ let binCountRight = -1;
1551
+ for (let i = 0; i < hist.length; i += 1) {
1552
+ for (let j = 0; j < hist[i].length; j += 1) {
1553
+ hist[i][j].binValue = hist[i].x0;
1554
+ hist[i][j].binCount = j;
1555
+ if (i === 0) binCountLeft = j;
1556
+ if (i === hist.length - 1) binCountRight = j;
1557
+ }
1558
+ }
1559
+ // Put out-of-range values in extreme left/right bins
1560
+ for (let i = 0; i < this.trials.length; i += 1) {
1561
+ if (this.trials[i].trueEvidence < this.binRange[0]) {
1562
+ binCountLeft += 1;
1563
+ this.trials[i].binCount = binCountLeft;
1564
+ this.trials[i].binValue = hist[0].x0;
1565
+ }
1566
+ if (this.trials[i].trueEvidence > this.binRange[1]) {
1567
+ binCountRight += 1;
1568
+ this.trials[i].binCount = binCountRight;
1569
+ this.trials[i].binValue = hist[hist.length - 1].x0;
1570
+ }
1571
+ }
1572
+ // DATA-JOIN
1573
+ const trialUpdate = histogramMerge.selectAll('.trial')
1574
+ .data(this.trials, (datum) => { return datum.trial; });
1575
+ // ENTER
1576
+ const trialEnter = trialUpdate.enter().append('rect')
1577
+ .attr('stroke-width', strokeWidth)
1578
+ .attr('data-new-trial-ease-time', 0) // use 'data-trial-enter'
1579
+ .attr('stroke', this.getComputedStyleValue('---color-acc'))
1580
+ .attr('fill', this.getComputedStyleValue('---color-acc-light'));
1581
+ // MERGE
1582
+ const trialMerge = trialEnter.merge(trialUpdate)
1583
+ .attr('class', (datum) => { return `trial ${datum.outcome}`; })
1584
+ .attr('width', binWidth - strokeWidth)
1585
+ .attr('height', binWidth - strokeWidth);
1586
+ // MERGE - Active New Trials
1587
+ const trialMergeNewActive = trialMerge.filter((datum) => {
1588
+ return (datum.new && !datum.paused);
1589
+ });
1590
+ if (!trialMergeNewActive.empty()) {
1591
+ const easeTime = trialMergeNewActive.attr('data-new-trial-ease-time');
1592
+ const scaleIn = (time) => {
1593
+ return d3.scaleLinear().domain([0, 1]).range([easeTime, 1])(time);
1594
+ };
1595
+ const scaleOutGenerator = (easeFunction) => {
1596
+ return (time) => {
1597
+ return d3.scaleLinear()
1598
+ .domain([easeFunction(easeTime), 1]).range([0, 1])(easeFunction(time));
1599
+ };
1600
+ };
1601
+ trialMergeNewActive.transition('new')
1602
+ .duration((datum) => {
1603
+ return Math.floor((datum.duration * 0.75 + datum.wait * 0.25) * (1 - easeTime));
1604
+ })
1605
+ .ease(scaleIn)
1606
+ .attr('data-new-trial-ease-time', 1)
1607
+ .attrTween('stroke', (datum, index, elements) => {
1608
+ const element = elements[index];
1609
+ const interpolator = d3.interpolateRgb(
1610
+ element.getAttribute('stroke'),
1611
+ (this.color === 'stimulus')
1612
+ ? (datum.signal === 'present')
1613
+ ? this.getComputedStyleValue('---color-hr')
1614
+ : this.getComputedStyleValue('---color-far')
1615
+ : (this.color === 'response')
1616
+ ? this.getComputedStyleValue(`---color-${datum.response}`)
1617
+ : (this.color === 'outcome')
1618
+ ? this.getComputedStyleValue(`---color-${datum.outcome}`)
1619
+ : this.getComputedStyleValue('---color-acc'),
1620
+ );
1621
+ return (time) => { return interpolator(scaleOutGenerator(d3.easeCubicIn)(time)); };
1622
+ })
1623
+ .attrTween('fill', (datum, index, elements) => {
1624
+ const element = elements[index];
1625
+ const interpolator = d3.interpolateRgb(
1626
+ element.getAttribute('fill'),
1627
+ (this.color === 'stimulus')
1628
+ ? (datum.signal === 'present')
1629
+ ? this.getComputedStyleValue('---color-hr-light')
1630
+ : this.getComputedStyleValue('---color-far-light')
1631
+ : (this.color === 'response')
1632
+ ? this.getComputedStyleValue(`---color-${datum.response}-light`)
1633
+ : (this.color === 'outcome')
1634
+ ? this.getComputedStyleValue(`---color-${datum.outcome}-light`)
1635
+ : this.getComputedStyleValue('---color-acc-light'),
1636
+ );
1637
+ return (time) => { return interpolator(scaleOutGenerator(d3.easeCubicIn)(time)); };
1638
+ })
1639
+ .attrTween('x', (datum, index, elements) => {
1640
+ const element = elements[index];
1641
+ const interpolator = d3.interpolate(
1642
+ element.getAttribute('x'),
1643
+ xScale(datum.binValue) + (strokeWidth / 2),
1644
+ );
1645
+ return (time) => { return interpolator(scaleOutGenerator(d3.easeCubicOut)(time)); };
1646
+ })
1647
+ .attrTween('y', (datum, index, elements) => {
1648
+ const element = elements[index];
1649
+ const interpolator = d3.interpolate(
1650
+ element.getAttribute('y'),
1651
+ yScale(0) + (strokeWidth / 2) - ((datum.binCount + 1) * binWidth),
1652
+ );
1653
+ return (time) => { return interpolator(scaleOutGenerator(d3.easeCubicIn)(time)); };
1654
+ })
1655
+ .on('end', (datum, index, elements) => {
1656
+ const element = elements[index];
1657
+ element.removeAttribute('data-new-trial-ease-time');
1658
+ datum.new = false;
1659
+ this.alignTrial(datum);
1660
+ this.dispatchEvent(new CustomEvent('detectable-response', {
1661
+ detail: {
1662
+ stimulus: datum.signal,
1663
+ response: datum.response,
1664
+ outcome: datum.outcome,
1665
+ h: this.h,
1666
+ m: this.m,
1667
+ fa: this.fa,
1668
+ cr: this.cr,
1669
+ nr: 0,
1670
+ },
1671
+ bubbles: true,
1672
+ }));
1673
+ });
1674
+ }
1675
+ // MERGE - Paused New Trials
1676
+ const trialMergeNewPaused = trialMerge.filter((datum) => {
1677
+ return (datum.new && datum.paused);
1678
+ });
1679
+ if (!trialMergeNewPaused.empty()) {
1680
+ const easeTime = trialMergeNewPaused.attr('data-new-trial-ease-time');
1681
+ trialMergeNewPaused.transition()
1682
+ .duration(transitionDuration)
1683
+ .ease(d3.easeCubicOut)
1684
+ .attr('x', (datum) => {
1685
+ const interpolator = d3.interpolate(
1686
+ 0,
1687
+ xScale(datum.binValue) + (strokeWidth / 2),
1688
+ );
1689
+ return interpolator(d3.easeCubicOut(easeTime));
1690
+ })
1691
+ .attr('y', (datum) => {
1692
+ const interpolator = d3.interpolate(
1693
+ 0,
1694
+ yScale(0) + (strokeWidth / 2) - ((datum.binCount + 1) * binWidth),
1695
+ );
1696
+ return interpolator(d3.easeCubicIn(easeTime));
1697
+ })
1698
+ .attr('stroke', (datum) => {
1699
+ const interpolator = d3.interpolateRgb(
1700
+ this.getComputedStyleValue('---color-acc'),
1701
+ (this.color === 'stimulus')
1702
+ ? (datum.signal === 'present')
1703
+ ? this.getComputedStyleValue('---color-hr')
1704
+ : this.getComputedStyleValue('---color-far')
1705
+ : (this.color === 'response')
1706
+ ? this.getComputedStyleValue(`---color-${datum.response}`)
1707
+ : (this.color === 'outcome')
1708
+ ? this.getComputedStyleValue(`---color-${datum.outcome}`)
1709
+ : this.getComputedStyleValue('---color-acc'),
1710
+ );
1711
+ return interpolator(d3.easeCubicIn(easeTime));
1712
+ })
1713
+ .attr('fill', (datum) => {
1714
+ const interpolator = d3.interpolateRgb(
1715
+ this.getComputedStyleValue('---color-acc-light'),
1716
+ (this.color === 'stimulus')
1717
+ ? (datum.signal === 'present')
1718
+ ? this.getComputedStyleValue('---color-hr-light')
1719
+ : this.getComputedStyleValue('---color-far-light')
1720
+ : (this.color === 'response')
1721
+ ? this.getComputedStyleValue(`---color-${datum.response}-light`)
1722
+ : (this.color === 'outcome')
1723
+ ? this.getComputedStyleValue(`---color-${datum.outcome}-light`)
1724
+ : this.getComputedStyleValue('---color-acc-light'),
1725
+ );
1726
+ return interpolator(d3.easeCubicIn(easeTime));
1727
+ });
1728
+ }
1729
+ // MERGE - Old Trials
1730
+ trialMerge.filter((datum) => { return !datum.new; }).transition()
1731
+ .duration(transitionDuration)
1732
+ .ease(d3.easeCubicOut)
1733
+ .attr('x', (datum) => {
1734
+ return xScale(datum.binValue) + (strokeWidth / 2);
1735
+ })
1736
+ .attr('y', (datum) => {
1737
+ return yScale(0) + (strokeWidth / 2) - ((datum.binCount + 1) * binWidth);
1738
+ })
1739
+ .attr('stroke', (datum) => {
1740
+ return (this.color === 'stimulus')
1741
+ ? (datum.signal === 'present')
1742
+ ? this.getComputedStyleValue('---color-hr')
1743
+ : this.getComputedStyleValue('---color-far')
1744
+ : (this.color === 'response')
1745
+ ? this.getComputedStyleValue(`---color-${datum.response}`)
1746
+ : (this.color === 'outcome')
1747
+ ? this.getComputedStyleValue(`---color-${datum.outcome}`)
1748
+ : this.getComputedStyleValue('---color-acc');
1749
+ })
1750
+ .attr('fill', (datum) => {
1751
+ return (this.color === 'stimulus')
1752
+ ? (datum.signal === 'present')
1753
+ ? this.getComputedStyleValue('---color-hr-light')
1754
+ : this.getComputedStyleValue('---color-far-light')
1755
+ : (this.color === 'response')
1756
+ ? this.getComputedStyleValue(`---color-${datum.response}-light`)
1757
+ : (this.color === 'outcome')
1758
+ ? this.getComputedStyleValue(`---color-${datum.outcome}-light`)
1759
+ : this.getComputedStyleValue('---color-acc-light');
1760
+ });
1761
+ // EXIT
1762
+ trialUpdate.exit().transition()
1763
+ .duration(transitionDuration)
1764
+ .ease(d3.easeLinear)
1765
+ .attrTween('stroke', (datum, index, elements) => {
1766
+ const element = elements[index];
1767
+ const interpolator = d3.interpolateRgb(
1768
+ element.getAttribute('stroke'),
1769
+ this.getComputedStyleValue('---color-acc'),
1770
+ );
1771
+ return (time) => { return interpolator(d3.easeCubicIn(time)); };
1772
+ })
1773
+ .attrTween('fill', (datum, index, elements) => {
1774
+ const element = elements[index];
1775
+ const interpolator = d3.interpolateRgb(
1776
+ element.getAttribute('fill'),
1777
+ this.getComputedStyleValue('---color-acc-light'),
1778
+ );
1779
+ return (time) => { return interpolator(d3.easeCubicIn(time)); };
1780
+ })
1781
+ .attrTween('x', (datum, index, elements) => {
1782
+ const element = elements[index];
1783
+ const interpolator = d3.interpolate(element.getAttribute('x'), 0);
1784
+ return (time) => { return interpolator(d3.easeCubicIn(time)); };
1785
+ })
1786
+ .attrTween('y', (datum, index, elements) => {
1787
+ const element = elements[index];
1788
+ const interpolator = d3.interpolate(element.getAttribute('y'), 0);
1789
+ return (time) => { return interpolator(d3.easeCubicOut(time)); };
1790
+ })
1791
+ .remove();
1792
+ }
1793
+
1794
+ // Overlayer
1795
+ // ENTER
1796
+ const overlayerEnter = plotEnter.append('g')
1797
+ .classed('overlayer', true);
1798
+ // MERGE
1799
+ const overlayerMerge = plotMerge.select('.overlayer');
1800
+
1801
+ // Background
1802
+ // ENTER
1803
+ overlayerEnter.append('rect')
1804
+ .classed('background', true);
1805
+ // MERGE
1806
+ overlayerMerge.select('.background')
1807
+ .attr('height', height)
1808
+ .attr('width', width);
1809
+
1810
+ this.drag = false;
1811
+ this.firstUpdate = false;
1812
+ }
1813
+
1814
+ // Called to pause trial animations!
1815
+ pauseTrial() {
1816
+ const trialNew = d3.select(this.renderRoot).select('.trial[data-new-trial-ease-time]');
1817
+ trialNew.interrupt('new');
1818
+ trialNew.datum((datum) => {
1819
+ datum.paused = true;
1820
+ return datum;
1821
+ });
1822
+ }
1823
+
1824
+ // Called to resume trial animations!
1825
+ resumeTrial() {
1826
+ const trialNew = d3.select(this.renderRoot).select('.trial[data-new-trial-ease-time]');
1827
+ trialNew.datum((datum) => {
1828
+ datum.paused = false;
1829
+ return datum;
1830
+ });
1831
+ this.requestUpdate();
1832
+ }
1833
+ }
1834
+
1835
+ customElements.define('sdt-model', SDTModel);