@decidables/accumulable-elements 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,2095 @@
1
+
2
+ import {html, css} from 'lit';
3
+ import * as d3 from 'd3';
4
+
5
+ import DDMMath from '@decidables/accumulable-math';
6
+ import {DecidablesMixinResizeable} from '@decidables/decidables-elements';
7
+
8
+ import AccumulableElement from '../accumulable-element';
9
+
10
+ /*
11
+ DDMModel element
12
+ <ddm-model>
13
+
14
+ Attributes:
15
+ interactive: true/false
16
+
17
+ measures: boolean
18
+ means: boolean
19
+
20
+ seed: numeric
21
+ trials: numeric
22
+
23
+ a: numeric
24
+ z: numeric
25
+ v: numeric
26
+ t0: numeric
27
+
28
+ // s: numeric
29
+ // sz: numeric
30
+ // eta: numeric
31
+ // st: numeric
32
+
33
+ Styles:
34
+ ??
35
+ */
36
+ export default class DDMModel extends DecidablesMixinResizeable(AccumulableElement) {
37
+ static get properties() {
38
+ return {
39
+ measures: {
40
+ attribute: 'measures',
41
+ type: Boolean,
42
+ reflect: true,
43
+ },
44
+ means: {
45
+ attribute: 'means',
46
+ type: Boolean,
47
+ reflect: true,
48
+ },
49
+ sds: {
50
+ attribute: 'sds',
51
+ type: Boolean,
52
+ reflect: true,
53
+ },
54
+
55
+ human: {
56
+ attribute: 'human',
57
+ type: Boolean,
58
+ reflect: true,
59
+ },
60
+ trials: {
61
+ attribute: 'trials',
62
+ type: Number,
63
+ reflect: true,
64
+ },
65
+ seed: {
66
+ attribute: 'seed',
67
+ type: Number,
68
+ reflect: true,
69
+ },
70
+
71
+ a: {
72
+ attribute: 'boundary-separation',
73
+ type: Number,
74
+ reflect: true,
75
+ },
76
+ z: {
77
+ attribute: 'starting-point',
78
+ type: Number,
79
+ reflect: true,
80
+ },
81
+ v: {
82
+ attribute: 'drift-rate',
83
+ type: Number,
84
+ reflect: true,
85
+ },
86
+ t0: {
87
+ attribute: 'nondecision-time',
88
+ type: Number,
89
+ reflect: true,
90
+ },
91
+
92
+ // s: {
93
+ // attribute: false, // within-trial variability in drift rate
94
+ // type: Number,
95
+ // reflect: false,
96
+ // },
97
+ // sz: {
98
+ // attribute: false, // starting point range
99
+ // type: Number,
100
+ // reflect: false,
101
+ // },
102
+ // eta: {
103
+ // attribute: false, // standard deviation for across-trial variability in drift rate
104
+ // type: Number,
105
+ // reflect: false,
106
+ // },
107
+ // st: {
108
+ // attribute: false, // nondecision-time range
109
+ // type: Number,
110
+ // reflect: false,
111
+ // },
112
+ };
113
+ }
114
+
115
+ constructor() {
116
+ super();
117
+
118
+ this.firstUpdate = true;
119
+ this.drag = false;
120
+
121
+ this.scale = {
122
+ time: {
123
+ min: 0,
124
+ max: 1000,
125
+ step: 1,
126
+ round: Math.round,
127
+ },
128
+ evidence: {
129
+ min: -1,
130
+ max: 1,
131
+ step: 0.01,
132
+ round: Math.round,
133
+ },
134
+ density: {
135
+ min: 0,
136
+ max: 10,
137
+ step: 0.01,
138
+ round: Math.round,
139
+ },
140
+ };
141
+
142
+ this.measures = false;
143
+ this.means = false;
144
+ this.sds = false;
145
+
146
+ this.resample();
147
+ this.human = false;
148
+ this.trials = 10;
149
+
150
+ this.a = 1.2;
151
+ this.z = 0.35;
152
+ this.v = 1.5;
153
+ this.t0 = 150;
154
+
155
+ // this.s = null;
156
+ // this.sz = null;
157
+ // this.eta = null;
158
+ // this.st = null;
159
+
160
+ this.precision = 0.005;
161
+ this.random = null;
162
+
163
+ this.bounds = null;
164
+ this.startingPoint = null;
165
+
166
+ this.data = {};
167
+ this.model = {};
168
+ this.animate = false;
169
+ this.paused = false;
170
+
171
+ this.alignState();
172
+ }
173
+
174
+ clear() {
175
+ this.trials = 0;
176
+ this.data.trials = [];
177
+ }
178
+
179
+ trial(trial = {}) {
180
+ this.trials += 1;
181
+ if (this.human) {
182
+ this.data.trials.push(trial);
183
+ } else {
184
+ this.animate = true;
185
+ }
186
+ }
187
+
188
+ // Called to pause trial animations!
189
+ pauseTrial() {
190
+ const pathNew = d3.select(this.renderRoot).select('.path[data-new-trial-ease-time]');
191
+ pathNew.interrupt('new').select('.curve').interrupt('new');
192
+ this.paused = true;
193
+ }
194
+
195
+ // Called to resume trial animations!
196
+ resumeTrial() {
197
+ this.paused = false;
198
+ this.requestUpdate();
199
+ }
200
+
201
+ resample() {
202
+ this.seed = d3.randomUniform(0, 1)();
203
+ }
204
+
205
+ alignPath(seed, a, z, v, t0) {
206
+ const random = d3.randomNormal.source(d3.randomLcg(seed))(0, this.precision ** 0.5);
207
+ const bounds = {
208
+ lower: -a / 2,
209
+ upper: a / 2,
210
+ };
211
+ const startingPoint = a * z - a / 2;
212
+ const drift = v * this.precision;
213
+
214
+ const path = [];
215
+ path.push(
216
+ {t: t0, e: startingPoint},
217
+ );
218
+ while ((path.at(-1).e > bounds.lower) && (path.at(-1).e < bounds.upper)) {
219
+ path.push({
220
+ t: path.at(-1).t + (this.precision * 1000),
221
+ e: path.at(-1).e + drift + DDMMath.s * random(),
222
+ });
223
+ }
224
+ return path;
225
+ }
226
+
227
+ alignCorrectDistribution(a, z, v, t0) {
228
+ const proportionCorrect = DDMMath.azv2pC(a, z, v);
229
+
230
+ const dist = [
231
+ {t: 0, d: 0},
232
+ {t: this.t0, d: 0},
233
+ ];
234
+ for (
235
+ let i = this.scale.time.min;
236
+ i <= (this.scale.time.max - (t0));
237
+ i += this.scale.time.step) {
238
+ if (i > 0) {
239
+ dist.push({
240
+ t: t0 + i,
241
+ d: DDMMath.tazv2gC(i / 1000, a, z, v) / proportionCorrect,
242
+ });
243
+ }
244
+ }
245
+
246
+ return dist;
247
+ }
248
+
249
+ alignErrorDistribution(a, z, v, t0) {
250
+ const proportionError = DDMMath.azv2pE(a, z, v);
251
+
252
+ const dist = [
253
+ {t: 0, d: 0},
254
+ {t: this.t0, d: 0},
255
+ ];
256
+ for (
257
+ let i = this.scale.time.min;
258
+ i <= (this.scale.time.max - (t0));
259
+ i += this.scale.time.step) {
260
+ if (i > 0) {
261
+ dist.push({
262
+ t: t0 + i,
263
+ d: DDMMath.tazv2gE(i / 1000, a, z, v) / proportionError,
264
+ });
265
+ }
266
+ }
267
+
268
+ return dist;
269
+ }
270
+
271
+ alignState() {
272
+ this.random = d3.randomUniform.source(d3.randomLcg(this.seed))(0, 1);
273
+
274
+ this.bounds = {
275
+ lower: -this.a / 2,
276
+ upper: this.a / 2,
277
+ };
278
+ this.startingPoint = this.a * this.z - this.a / 2;
279
+
280
+ // Data Trials
281
+ if (this.human) {
282
+ this.trials = this.data.trials.length;
283
+ } else {
284
+ this.data.trials = Array.from({length: this.trials}, (element, index) => {
285
+ const seed = (this.random() / 1000) * 997; // HACK to avoid randomLcg repetition
286
+ const animate = this.animate && (index === (this.trials - 1));
287
+
288
+ // Sample Paths
289
+ const path = this.alignPath(seed, this.a, this.z, this.v, this.t0);
290
+ const outcome = (path.at(-1).e <= this.bounds.lower)
291
+ ? 'error'
292
+ : (path.at(-1).e >= this.bounds.upper)
293
+ ? 'correct'
294
+ : 'nr';
295
+ const rt = (outcome === 'error')
296
+ ? path.at(-2).t + (
297
+ ((this.bounds.lower - path.at(-2).e) / (path.at(-1).e - path.at(-2).e))
298
+ * (this.precision * 1000)
299
+ )
300
+ : (outcome === 'correct')
301
+ ? path.at(-2).t + (
302
+ ((this.bounds.upper - path.at(-2).e) / (path.at(-1).e - path.at(-2).e))
303
+ * (this.precision * 1000)
304
+ )
305
+ : null;
306
+
307
+ return {
308
+ index, seed, path, rt, outcome, animate,
309
+ };
310
+ });
311
+ }
312
+
313
+ // Data Summary Stats
314
+ const dataStats = DDMMath.trials2stats(
315
+ this.data.trials.filter((path) => { return !path.animate; }),
316
+ );
317
+ this.data = {...this.data, ...dataStats};
318
+
319
+ // Model Summary Stats
320
+ this.model.accuracy = DDMMath.azv2pC(this.a, this.z, this.v);
321
+
322
+ this.model.correctMeanRT = DDMMath.azvt02mC(this.a, this.z, this.v, this.t0);
323
+ this.model.errorMeanRT = DDMMath.azvt02mE(this.a, this.z, this.v, this.t0);
324
+
325
+ this.model.correctSDRT = DDMMath.azv2sdC(this.a, this.z, this.v);
326
+ this.model.errorSDRT = DDMMath.azv2sdE(this.a, this.z, this.v);
327
+
328
+ // Model Distributions
329
+ this.model.correctDist = this.alignCorrectDistribution(this.a, this.z, this.v, this.t0);
330
+ this.model.errorDist = this.alignErrorDistribution(this.a, this.z, this.v, this.t0);
331
+
332
+ this.dispatchEvent(new CustomEvent('ddm-model-output', {
333
+ detail: {
334
+ data: this.data,
335
+ model: this.model,
336
+ },
337
+ bubbles: true,
338
+ }));
339
+ }
340
+
341
+ static get styles() {
342
+ return [
343
+ super.styles,
344
+ css`
345
+ :host {
346
+ display: inline-block;
347
+
348
+ width: 27rem;
349
+ height: 18rem;
350
+ }
351
+
352
+ .main {
353
+ width: 100%;
354
+ height: 100%;
355
+ }
356
+
357
+ text {
358
+ /* stylelint-disable property-no-vendor-prefix */
359
+ -webkit-user-select: none;
360
+ -moz-user-select: none;
361
+ -ms-user-select: none;
362
+ user-select: none;
363
+ }
364
+
365
+ /*
366
+ UNDERLAYER
367
+ */
368
+ .background {
369
+ fill: var(---color-element-background);
370
+ stroke: none;
371
+ stroke-width: 1;
372
+ shape-rendering: crispEdges;
373
+ }
374
+
375
+ .title {
376
+ font-weight: 600;
377
+
378
+ fill: currentColor;
379
+ }
380
+
381
+ .axis path,
382
+ .axis line {
383
+ stroke: var(---color-element-border);
384
+ /* shape-rendering: crispEdges; */
385
+ }
386
+
387
+ .tick {
388
+ font-size: 0.75rem;
389
+ }
390
+
391
+ /*
392
+ CONTENT
393
+ */
394
+ .line {
395
+ fill: none;
396
+ stroke: var(---color-element-emphasis);
397
+ stroke-width: 2;
398
+ }
399
+
400
+ .curve {
401
+ stroke-width: 2;
402
+ }
403
+
404
+ .path .curve {
405
+ opacity: 0.5;
406
+
407
+ fill: none;
408
+
409
+ transition: opacity 0.5s;
410
+ }
411
+
412
+ .path.highlight .curve {
413
+ filter: url("#shadow-2");
414
+ opacity: 1;
415
+ }
416
+
417
+ .path.correct .curve {
418
+ /* stroke: var(---color-correct); */
419
+ }
420
+
421
+ .path.error .curve {
422
+ /* stroke: var(---color-error); */
423
+ }
424
+
425
+ .stop-0 {
426
+ stop-color: var(---color-correct);
427
+ }
428
+
429
+ .stop-100 {
430
+ stop-color: var(---color-error);
431
+ }
432
+
433
+ .path.animate .curve {
434
+ opacity: 1;
435
+
436
+ stroke: url("#path-animate");
437
+ }
438
+
439
+ .dist.correct .curve {
440
+ fill: var(---color-correct-light);
441
+ stroke: var(---color-correct);
442
+ }
443
+
444
+ .dist.error .curve {
445
+ fill: var(---color-error-light);
446
+ stroke: var(---color-error);
447
+ }
448
+
449
+ .rt .mark {
450
+ stroke-width: 1;
451
+ }
452
+
453
+ .accuracy.model .bar {
454
+ stroke: none;
455
+ }
456
+
457
+ .accuracy.model.correct .bar {
458
+ fill: var(---color-correct);
459
+ }
460
+
461
+ .accuracy.model.error .bar {
462
+ fill: var(---color-error);
463
+ }
464
+
465
+ .accuracy.data .mark {
466
+ stroke-width: 2;
467
+ }
468
+
469
+ .accuracy.data.correct .mark {
470
+ stroke: var(---color-correct-light);
471
+ }
472
+
473
+ .accuracy.data.error .mark {
474
+ stroke: var(---color-error-light);
475
+ }
476
+
477
+ /*
478
+ OVERLAYER
479
+ */
480
+ .interactive {
481
+ filter: url("#shadow-2");
482
+ outline: none;
483
+ }
484
+
485
+ .interactive:hover {
486
+ filter: url("#shadow-4");
487
+ }
488
+
489
+ .interactive:active {
490
+ filter: url("#shadow-8");
491
+ }
492
+
493
+ :host(.keyboard) .interactive:focus {
494
+ filter: url("#shadow-8");
495
+ }
496
+
497
+ .boundary {
498
+ fill: none;
499
+ stroke: var(---color-element-emphasis);
500
+ stroke-width: 2;
501
+ }
502
+
503
+ .boundary.interactive {
504
+ cursor: ns-resize;
505
+ }
506
+
507
+ .drift {
508
+ pointer-events: visible;
509
+
510
+ fill: none;
511
+ stroke: var(---color-element-emphasis);
512
+ stroke-dasharray: 8 4;
513
+ stroke-width: 2;
514
+ }
515
+
516
+ .drift.interactive {
517
+ cursor: ns-resize;
518
+ }
519
+
520
+ .drift .arrow {
521
+ stroke-dasharray: none;
522
+ }
523
+
524
+ .t0z.interactive {
525
+ cursor: move;
526
+ }
527
+
528
+ .t0z .point {
529
+ fill: var(---color-element-emphasis);
530
+
531
+ r: 6px;
532
+ }
533
+
534
+ .measure {
535
+ stroke-width: 2;
536
+ }
537
+
538
+ .measure .label {
539
+ font-size: 0.75rem;
540
+
541
+ fill: currentColor;
542
+ }
543
+
544
+ .measure.a .line {
545
+ stroke: var(---color-a);
546
+ }
547
+
548
+ .measure.a .label {
549
+ dominant-baseline: auto;
550
+ text-anchor: end;
551
+ }
552
+
553
+ .measure.z .line {
554
+ stroke: var(---color-z);
555
+ }
556
+
557
+ .measure.z .label {
558
+ dominant-baseline: hanging;
559
+ text-anchor: start;
560
+ }
561
+
562
+ .measure.v .line {
563
+ stroke: var(---color-v);
564
+ }
565
+
566
+ .measure.v .label {
567
+ dominant-baseline: auto;
568
+ text-anchor: start;
569
+ }
570
+
571
+ .measure.t0 .line {
572
+ stroke: var(---color-t0);
573
+ }
574
+
575
+ .measure.t0 .label {
576
+ dominant-baseline: auto;
577
+ text-anchor: middle;
578
+ }
579
+
580
+ .sd .indicator,
581
+ .mean .indicator {
582
+ stroke-width: 2;
583
+ }
584
+
585
+ .sd.model .indicator,
586
+ .mean.model .indicator {
587
+ stroke-dasharray: 2 2;
588
+ }
589
+
590
+ .sd.data .indicator,
591
+ .mean.data .indicator {
592
+ stroke-dasharray: 1 1;
593
+ }
594
+
595
+ .sd.correct .indicator,
596
+ .mean.correct .indicator {
597
+ stroke: var(---color-correct-dark);
598
+ }
599
+
600
+ .sd.error .indicator,
601
+ .mean.error .indicator {
602
+ stroke: var(---color-error-dark);
603
+ }
604
+
605
+ .rt-label rect {
606
+ filter: url("#shadow-2");
607
+
608
+ fill: var(--color-background);
609
+ rx: 4;
610
+ }
611
+
612
+ .rt-label text {
613
+ font-size: 0.75rem;
614
+
615
+ text-anchor: middle;
616
+ }
617
+
618
+ .rt-label.correct text {
619
+ dominant-baseline: auto;
620
+ }
621
+
622
+ .rt-label.error text {
623
+ dominant-baseline: hanging;
624
+ }
625
+ `,
626
+ ];
627
+ }
628
+
629
+ render() { /* eslint-disable-line class-methods-use-this */
630
+ return html``;
631
+ }
632
+
633
+ willUpdate() {
634
+ this.alignState();
635
+ }
636
+
637
+ update(changedProperties) {
638
+ super.update(changedProperties);
639
+
640
+ // Bail out if we can't get the width/height
641
+ if (Number.isNaN(this.width) || Number.isNaN(this.height) || Number.isNaN(this.rem)) {
642
+ return;
643
+ }
644
+
645
+ const hostWidth = this.width;
646
+ const hostHeight = this.height;
647
+ const hostAspectRatio = hostWidth / hostHeight;
648
+
649
+ const elementAspectRatio = 1.5;
650
+ let elementWidth;
651
+ let elementHeight;
652
+
653
+ if (hostAspectRatio > elementAspectRatio) {
654
+ elementHeight = hostHeight;
655
+ elementWidth = elementHeight * elementAspectRatio;
656
+ } else {
657
+ elementWidth = hostWidth;
658
+ elementHeight = elementWidth / elementAspectRatio;
659
+ }
660
+
661
+ const margin = {
662
+ top: 1 * this.rem,
663
+ bottom: 3 * this.rem,
664
+ left: 3.75 * this.rem,
665
+ right: 3.25 * this.rem,
666
+ };
667
+ const height = elementHeight - (margin.top + margin.bottom);
668
+ const width = elementWidth - (margin.left + margin.right);
669
+
670
+ const gapHeight = 0.75 * this.rem;
671
+ const evidenceHeight = height * 0.5;
672
+ const densityHeight = height * 0.25 - gapHeight;
673
+
674
+ const gapWidth = 0.75 * this.rem;
675
+ const timeWidth = width * 0.90;
676
+ const accuracyWidth = width * 0.10 - gapWidth;
677
+
678
+ const transitionDuration = parseInt(this.getComputedStyleValue('---transition-duration'), 10);
679
+
680
+ //
681
+ // SCALES
682
+ //
683
+
684
+ // Time Scale
685
+ const timeScale = d3.scaleLinear()
686
+ .domain([this.scale.time.min, this.scale.time.max])
687
+ .range([0, timeWidth]);
688
+
689
+ // Evidence Scale
690
+ const evidenceScale = d3.scaleLinear()
691
+ .domain([this.scale.evidence.min, this.scale.evidence.max])
692
+ .range([evidenceHeight, 0]);
693
+
694
+ // Correct Density Scale
695
+ const correctDensityScale = d3.scaleLinear()
696
+ .domain([this.scale.density.min, this.scale.density.max])
697
+ .range([densityHeight, 0]);
698
+
699
+ // Error Density Scale
700
+ const errorDensityScale = d3.scaleLinear()
701
+ .domain([this.scale.density.min, this.scale.density.max])
702
+ .range([0, densityHeight]);
703
+
704
+ // Accuracy Scale
705
+ const accuracyScale = d3.scaleLinear()
706
+ .domain([0, 1])
707
+ .range([0, height]);
708
+
709
+ //
710
+ // DRAG BEHAVIORS
711
+ //
712
+
713
+ // Nondecision Time/Starting Point Drag behavior
714
+ const dragT0z = d3.drag()
715
+ .subject((event, datum) => {
716
+ return {x: timeScale(datum.t0), y: evidenceScale(datum.startingPoint)};
717
+ })
718
+ .on('start', (event) => {
719
+ const element = event.currentTarget;
720
+ d3.select(element).classed('dragging', true);
721
+ })
722
+ .on('drag', (event) => {
723
+ this.drag = true;
724
+ const shift = event.sourceEvent.shiftKey
725
+ ? (Math.abs(event.x - event.subject.x) > Math.abs(event.y - event.subject.y))
726
+ ? 't0'
727
+ : 'z'
728
+ : false;
729
+ let t0 = timeScale.invert(event.x);
730
+ let z = (evidenceScale.invert(event.y) + (this.a / 2)) / this.a;
731
+ // Clamp t0
732
+ t0 = (shift === 'z')
733
+ ? timeScale.invert(event.subject.x)
734
+ : (t0 < 0)
735
+ ? 0
736
+ : (t0 > 500)
737
+ ? 500
738
+ : t0;
739
+ // Clamp z
740
+ z = (shift === 't0')
741
+ ? (evidenceScale.invert(event.subject.y) + (this.a / 2)) / this.a
742
+ : (z < 0.01)
743
+ ? 0.01
744
+ : (z > 0.99)
745
+ ? 0.99
746
+ : z;
747
+ this.t0 = t0;
748
+ this.z = z;
749
+ this.alignState();
750
+ this.dispatchEvent(new CustomEvent('ddm-model-t0', {
751
+ detail: {
752
+ t0: this.t0,
753
+ },
754
+ bubbles: true,
755
+ }));
756
+ this.dispatchEvent(new CustomEvent('ddm-model-z', {
757
+ detail: {
758
+ z: this.z,
759
+ },
760
+ bubbles: true,
761
+ }));
762
+ })
763
+ .on('end', (event) => {
764
+ const element = event.currentTarget;
765
+ d3.select(element).classed('dragging', false);
766
+ this.drag = false;
767
+ });
768
+
769
+ // Drift Rate Drag behavior
770
+ const dragDrift = d3.drag()
771
+ .on('start', (event) => {
772
+ const element = event.currentTarget;
773
+ d3.select(element).classed('dragging', true);
774
+ })
775
+ .on('drag', (event) => {
776
+ this.drag = true;
777
+ let v = ((evidenceScale.invert(event.y) - this.startingPoint)
778
+ / (timeScale.invert(event.x) - this.t0)) * 1000;
779
+ // Clamp drift rate
780
+ v = (v < 0.01)
781
+ ? 0.01
782
+ : (v > 5)
783
+ ? 5
784
+ : v;
785
+ this.v = v;
786
+ this.alignState();
787
+ this.dispatchEvent(new CustomEvent('ddm-model-v', {
788
+ detail: {
789
+ v: this.v,
790
+ },
791
+ bubbles: true,
792
+ }));
793
+ })
794
+ .on('end', (event) => {
795
+ const element = event.currentTarget;
796
+ d3.select(element).classed('dragging', false);
797
+ this.drag = false;
798
+ });
799
+
800
+ // Boundary Drag behavior
801
+ const dragBoundary = d3.drag()
802
+ .subject((event, datum) => {
803
+ return {x: 0, y: evidenceScale(datum.value)};
804
+ })
805
+ .on('start', (event) => {
806
+ const element = event.currentTarget;
807
+ d3.select(element).classed('dragging', true);
808
+ })
809
+ .on('drag', (event, datum) => {
810
+ this.drag = true;
811
+ let boundary = evidenceScale.invert(event.y);
812
+ // Clamp boundaries to visible evidence
813
+ boundary = (boundary < this.scale.evidence.min)
814
+ ? this.scale.evidence.min
815
+ : (boundary > this.scale.evidence.max)
816
+ ? this.scale.evidence.max
817
+ : (datum.bound === 'upper' && boundary < 0.005)
818
+ ? 0.005
819
+ : (datum.bound === 'lower' && boundary > -0.005)
820
+ ? -0.005
821
+ : boundary;
822
+ this.a = Math.abs(boundary * 2);
823
+ this.alignState();
824
+ this.dispatchEvent(new CustomEvent('ddm-model-a', {
825
+ detail: {
826
+ a: this.a,
827
+ },
828
+ bubbles: true,
829
+ }));
830
+ })
831
+ .on('end', (event) => {
832
+ const element = event.currentTarget;
833
+ d3.select(element).classed('dragging', false);
834
+ this.drag = false;
835
+ });
836
+
837
+ //
838
+ // LINES
839
+ //
840
+
841
+ // Line for time/evidence space
842
+ const evidenceLine = d3.line()
843
+ .x((datum) => { return timeScale(datum.t); })
844
+ .y((datum) => { return evidenceScale(datum.e); });
845
+
846
+ // Line for correct time/density space
847
+ const correctDensityLine = d3.line()
848
+ .x((datum) => { return timeScale(datum.t); })
849
+ .y((datum) => { return correctDensityScale(datum.d); });
850
+
851
+ // Line for error time/density space
852
+ const errorDensityLine = d3.line()
853
+ .x((datum) => { return timeScale(datum.t); })
854
+ .y((datum) => { return errorDensityScale(datum.d); });
855
+
856
+ //
857
+ // PLOTS
858
+ //
859
+
860
+ // Svg
861
+ // DATA-JOIN
862
+ const svgUpdate = d3.select(this.renderRoot).selectAll('.main')
863
+ .data([{
864
+ width: this.width,
865
+ height: this.height,
866
+ rem: this.rem,
867
+ }]);
868
+ // ENTER
869
+ const svgEnter = svgUpdate.enter().append('svg')
870
+ .classed('main', true)
871
+ .html(AccumulableElement.svgDefs);
872
+ const svgDefs = svgEnter.append('defs');
873
+ // Arrowhead marker for measures
874
+ svgDefs.append('marker')
875
+ .attr('id', 'measure-arrow')
876
+ .attr('orient', 'auto-start-reverse')
877
+ .attr('markerUnits', 'userSpaceOnUse')
878
+ .attr('viewBox', '-5 -5 10 10')
879
+ .attr('refX', '2')
880
+ .attr('refY', '0')
881
+ .attr('markerWidth', '10')
882
+ .attr('markerHeight', '10')
883
+ .append('path')
884
+ .attr('stroke', 'context-stroke')
885
+ .attr('fill', 'context-stroke')
886
+ .attr('d', 'M -3 -3 l 6 3 l -6 3 z');
887
+ // Flat markers for SDs
888
+ svgDefs.append('marker')
889
+ .attr('id', 'model-sd-cap')
890
+ .attr('orient', 'auto-start-reverse')
891
+ .attr('markerUnits', 'userSpaceOnUse')
892
+ .attr('viewBox', '-5 -5 10 10')
893
+ .attr('refX', '0')
894
+ .attr('refY', '0')
895
+ .attr('markerWidth', '10')
896
+ .attr('markerHeight', '10')
897
+ .append('path')
898
+ .attr('stroke', 'context-stroke')
899
+ .attr('fill', 'context-stroke')
900
+ .attr('stroke-width', '2')
901
+ .attr('d', 'M 0 -4 l 0 8');
902
+ svgDefs.append('marker')
903
+ .attr('id', 'data-sd-cap')
904
+ .attr('orient', 'auto-start-reverse')
905
+ .attr('markerUnits', 'userSpaceOnUse')
906
+ .attr('viewBox', '-5 -5 10 10')
907
+ .attr('refX', '0')
908
+ .attr('refY', '0')
909
+ .attr('markerWidth', '10')
910
+ .attr('markerHeight', '10')
911
+ .append('path')
912
+ .attr('stroke', 'context-stroke')
913
+ .attr('fill', 'context-stroke')
914
+ .attr('stroke-width', '2')
915
+ .attr('d', 'M 0 -3 l 0 6');
916
+ const gradient = svgDefs.append('linearGradient')
917
+ .attr('id', 'path-animate')
918
+ .attr('gradientUnits', 'userSpaceOnUse')
919
+ .attr('color-interpolation', 'linearRGB')
920
+ .attr('x1', '0')
921
+ .attr('x2', '0')
922
+ .attr('y1', evidenceScale(this.bounds.upper))
923
+ .attr('y2', evidenceScale(this.bounds.lower));
924
+ gradient.append('stop')
925
+ .classed('stop-0', true)
926
+ .attr('offset', '0%');
927
+ gradient.append('stop')
928
+ .classed('stop-100', true)
929
+ .attr('offset', '100%');
930
+ // MERGE
931
+ const svgMerge = svgEnter.merge(svgUpdate)
932
+ .attr('viewBox', `0 0 ${elementWidth} ${elementHeight}`);
933
+
934
+ // Plots
935
+ // DATA-JOIN
936
+ const densityPlotUpdate = svgMerge.selectAll('.plot.density')
937
+ .data([
938
+ {
939
+ outcome: 'correct',
940
+ data: {
941
+ meanRT: this.data.correctMeanRT,
942
+ sdRT: this.data.correctSDRT,
943
+ },
944
+ model: {
945
+ meanRT: this.model.correctMeanRT,
946
+ sdRT: this.model.correctSDRT,
947
+ dist: this.model.correctDist,
948
+ },
949
+ densityScale: correctDensityScale,
950
+ densityLine: correctDensityLine,
951
+ alignDistribution: this.alignCorrectDistribution.bind(this),
952
+ },
953
+ {
954
+ outcome: 'error',
955
+ data: {
956
+ meanRT: this.data.errorMeanRT,
957
+ sdRT: this.data.errorSDRT,
958
+ },
959
+ model: {
960
+ meanRT: this.model.errorMeanRT,
961
+ sdRT: this.model.errorSDRT,
962
+ dist: this.model.errorDist,
963
+ },
964
+ densityScale: errorDensityScale,
965
+ densityLine: errorDensityLine,
966
+ alignDistribution: this.alignErrorDistribution.bind(this),
967
+ },
968
+ ]);
969
+ // ENTER
970
+ const evidencePlotEnter = svgEnter.append('g')
971
+ .classed('plot evidence', true);
972
+ const densityPlotEnter = densityPlotUpdate.enter().append('g')
973
+ .attr('class', (datum) => { return `plot density ${datum.outcome}`; });
974
+ const accuracyPlotEnter = svgEnter.append('g')
975
+ .classed('plot accuracy', true);
976
+ // MERGE
977
+ const evidencePlotMerge = svgMerge.select('.plot.evidence')
978
+ .attr('transform', `translate(${margin.left}, ${margin.top + densityHeight + gapHeight})`);
979
+ const densityPlotMerge = densityPlotEnter.merge(densityPlotUpdate)
980
+ .attr('transform', (datum) => {
981
+ return `translate(${margin.left}, ${
982
+ (datum.outcome === 'correct')
983
+ ? margin.top
984
+ : margin.top + densityHeight + evidenceHeight + 2 * gapHeight
985
+ })`;
986
+ });
987
+ const accuracyPlotMerge = svgMerge.select('.plot.accuracy')
988
+ .attr('transform', `translate(${margin.left + timeWidth + gapWidth}, ${margin.top})`);
989
+
990
+ // Clippaths
991
+ // ENTER
992
+ evidencePlotEnter.append('clipPath')
993
+ .attr('id', 'clip-evidence')
994
+ .append('rect');
995
+ // MERGE
996
+ evidencePlotMerge.select('clipPath rect')
997
+ .attr('y', evidenceScale(this.bounds.upper))
998
+ .attr('height', evidenceScale(this.bounds.lower) - evidenceScale(this.bounds.upper) + 1)
999
+ .attr('width', timeWidth + 1);
1000
+
1001
+ //
1002
+ // LAYERS
1003
+ //
1004
+
1005
+ // Underlayers
1006
+ // ENTER
1007
+ const evidenceUnderlayerEnter = evidencePlotEnter.append('g')
1008
+ .classed('underlayer', true);
1009
+ const densityUnderlayerEnter = densityPlotEnter.append('g')
1010
+ .classed('underlayer', true);
1011
+ const accuracyUnderlayerEnter = accuracyPlotEnter.append('g')
1012
+ .classed('underlayer', true);
1013
+ // MERGE
1014
+ const evidenceUnderlayerMerge = evidencePlotMerge.select('.underlayer');
1015
+ const densityUnderlayerMerge = densityPlotMerge.select('.underlayer');
1016
+ const accuracyUnderlayerMerge = accuracyPlotMerge.select('.underlayer');
1017
+
1018
+ // Contents
1019
+ // ENTER
1020
+ evidencePlotEnter.append('g')
1021
+ .classed('content', true)
1022
+ .append('g').classed('paths', true);
1023
+ const densityContentEnter = densityPlotEnter.append('g')
1024
+ .classed('content', true);
1025
+ accuracyPlotEnter.append('g')
1026
+ .classed('content', true);
1027
+ // MERGE
1028
+ const evidenceContentMerge = evidencePlotMerge.select('.content');
1029
+ const densityContentMerge = densityPlotMerge.select('.content');
1030
+ const accuracyContentMerge = accuracyPlotMerge.select('.content');
1031
+
1032
+ // Overlayers
1033
+ // ENTER
1034
+ evidencePlotEnter.append('g')
1035
+ .classed('overlayer', true);
1036
+ densityPlotEnter.append('g')
1037
+ .classed('overlayer', true);
1038
+ accuracyPlotEnter.append('g')
1039
+ .classed('overlayer', true);
1040
+ // MERGE
1041
+ const evidenceOverlayerMerge = evidencePlotMerge.select('.overlayer');
1042
+ const densityOverlayerMerge = densityPlotMerge.select('.overlayer');
1043
+ // const accuracyOverlayerMerge = accuracyPlotMerge.select('.overlayer');
1044
+
1045
+ //
1046
+ // UNDERLAYERS
1047
+ //
1048
+
1049
+ // Backgrounds
1050
+ // ENTER
1051
+ evidenceUnderlayerEnter.append('rect')
1052
+ .classed('background', true);
1053
+ densityUnderlayerEnter.append('rect')
1054
+ .classed('background', true);
1055
+ // MERGE
1056
+ evidenceUnderlayerMerge.select('.background')
1057
+ .transition()
1058
+ .duration(this.drag ? 0 : transitionDuration)
1059
+ .ease(d3.easeCubicOut)
1060
+ .attr('y', evidenceScale(this.bounds.upper))
1061
+ .attr('height', evidenceScale(this.bounds.lower) - evidenceScale(this.bounds.upper))
1062
+ .attr('width', timeWidth);
1063
+ densityUnderlayerMerge.select('.background')
1064
+ .transition()
1065
+ .duration(transitionDuration)
1066
+ .ease(d3.easeCubicOut)
1067
+ .attr('height', densityHeight)
1068
+ .attr('width', timeWidth);
1069
+
1070
+ // X Axes (Time)
1071
+ // ENTER
1072
+ densityUnderlayerEnter
1073
+ .filter((datum) => { return datum.outcome === 'error'; })
1074
+ .append('g')
1075
+ .classed('axis time', true);
1076
+ // MERGE
1077
+ const timeScaleMerge = densityUnderlayerMerge
1078
+ .filter((datum) => { return datum.outcome === 'error'; })
1079
+ .select('.axis.time')
1080
+ .attr('transform', `translate(0, ${densityHeight + (0.25 * this.rem)})`);
1081
+ const timeScaleTransition = timeScaleMerge.transition()
1082
+ .duration(transitionDuration)
1083
+ .ease(d3.easeCubicOut)
1084
+ .call(d3.axisBottom(timeScale))
1085
+ .attr('font-size', null)
1086
+ .attr('font-family', null);
1087
+ timeScaleTransition.selectAll('line, path')
1088
+ .attr('stroke', null);
1089
+
1090
+ // X Axes Titles
1091
+ // ENTER
1092
+ const timeTitleEnter = densityUnderlayerEnter
1093
+ .filter((datum) => { return datum.outcome === 'error'; })
1094
+ .append('text')
1095
+ .classed('title time', true)
1096
+ .attr('text-anchor', 'middle');
1097
+ timeTitleEnter.append('tspan')
1098
+ .classed('name', true)
1099
+ .text('Time (ms)');
1100
+ // MERGE
1101
+ densityUnderlayerMerge
1102
+ .filter((datum) => { return datum.outcome === 'error'; })
1103
+ .select('.title.time')
1104
+ .transition()
1105
+ .duration(transitionDuration)
1106
+ .ease(d3.easeCubicOut)
1107
+ .attr(
1108
+ 'transform',
1109
+ `translate(${(timeWidth / 2)}, ${(densityHeight + (2.5 * this.rem))})`,
1110
+ );
1111
+
1112
+ // Y Axes (Evidence, Density, Accuracy)
1113
+ // ENTER
1114
+ evidenceUnderlayerEnter.append('g')
1115
+ .classed('axis evidence', true);
1116
+ densityUnderlayerEnter.append('g')
1117
+ .attr('class', (datum) => { return `axis density ${datum.outcome}`; });
1118
+ accuracyUnderlayerEnter.append('g')
1119
+ .classed('axis accuracy', true);
1120
+ // MERGE
1121
+ const evidenceScaleMerge = evidenceUnderlayerMerge.select('.axis.evidence')
1122
+ .attr('transform', `translate(${-0.25 * this.rem}, 0)`);
1123
+ const densityScaleMerge = densityUnderlayerMerge.select('.axis.density')
1124
+ .attr('transform', `translate(${-0.25 * this.rem}, 0)`);
1125
+ const accuracyScaleMerge = accuracyUnderlayerMerge.select('.axis.accuracy')
1126
+ .attr('transform', `translate(${accuracyWidth + (0.25 * this.rem)}, 0)`);
1127
+ const evidenceScaleTransition = evidenceScaleMerge.transition()
1128
+ .duration(transitionDuration)
1129
+ .ease(d3.easeCubicOut)
1130
+ .call(d3.axisLeft(evidenceScale))
1131
+ .attr('font-size', null)
1132
+ .attr('font-family', null);
1133
+ const densityScaleTransition = densityScaleMerge.transition()
1134
+ .duration(transitionDuration)
1135
+ .ease(d3.easeCubicOut)
1136
+ .each((datum, index, elements) => {
1137
+ d3.axisLeft(datum.densityScale).ticks(2)(d3.select(elements[index]));
1138
+ })
1139
+ .attr('font-size', null)
1140
+ .attr('font-family', null);
1141
+ const accuracyScaleTransition = accuracyScaleMerge.transition()
1142
+ .duration(transitionDuration)
1143
+ .ease(d3.easeCubicOut)
1144
+ .call(d3.axisRight(accuracyScale))
1145
+ .attr('font-size', null)
1146
+ .attr('font-family', null);
1147
+ evidenceScaleTransition.selectAll('line, path')
1148
+ .attr('stroke', null);
1149
+ densityScaleTransition.selectAll('line, path')
1150
+ .attr('stroke', null);
1151
+ accuracyScaleTransition.selectAll('line, path')
1152
+ .attr('stroke', null);
1153
+
1154
+ // Y Axes Titles (Evidence & Density)
1155
+ // ENTER
1156
+ const evidenceTitleEnter = evidenceUnderlayerEnter.append('text')
1157
+ .classed('title evidence', true)
1158
+ .attr('text-anchor', 'middle');
1159
+ const densityTitleEnter = densityUnderlayerEnter.append('text')
1160
+ .attr('class', (datum) => { return `title density ${datum.outcome}`; })
1161
+ .attr('text-anchor', 'middle');
1162
+ const accuracyTitleEnter = accuracyUnderlayerEnter.append('text')
1163
+ .classed('title accuracy', true)
1164
+ .attr('text-anchor', 'middle');
1165
+ evidenceTitleEnter.append('tspan')
1166
+ .classed('name', true)
1167
+ .text('Evidence');
1168
+ densityTitleEnter.append('tspan')
1169
+ .classed('name', true)
1170
+ .text('Density');
1171
+ accuracyTitleEnter.append('tspan')
1172
+ .classed('name', true)
1173
+ .text('Accuracy');
1174
+ // MERGE
1175
+ evidenceUnderlayerMerge.select('.title.evidence')
1176
+ .transition()
1177
+ .duration(transitionDuration)
1178
+ .ease(d3.easeCubicOut)
1179
+ .attr('transform', `translate(${-2.5 * this.rem}, ${(evidenceHeight / 2)})rotate(-90)`);
1180
+ densityUnderlayerMerge.select('.title.density')
1181
+ .transition()
1182
+ .duration(transitionDuration)
1183
+ .ease(d3.easeCubicOut)
1184
+ .attr('transform', `translate(${-2.5 * this.rem}, ${(densityHeight / 2)})rotate(-90)`);
1185
+ accuracyUnderlayerMerge.select('.title.accuracy')
1186
+ .transition()
1187
+ .duration(transitionDuration)
1188
+ .ease(d3.easeCubicOut)
1189
+ .attr('transform', `translate(${accuracyWidth + 2.25 * this.rem}, ${(height / 2)})rotate(90)`);
1190
+
1191
+ //
1192
+ // CONTENTS
1193
+ //
1194
+
1195
+ // Paths
1196
+ // DATA-JOIN
1197
+ const pathUpdate = evidenceContentMerge.select('.paths').selectAll('.path')
1198
+ .data(
1199
+ this.data.trials.filter((trial) => { return trial.path !== undefined; }),
1200
+ );
1201
+ // ENTER
1202
+ const rtLabel = d3.local();
1203
+ const pathEnter = pathUpdate.enter().append('g')
1204
+ .classed('path', true)
1205
+ .attr('data-new-trial-ease-time', 0)
1206
+ .on('pointerenter', (event, datum) => {
1207
+ if (!this.drag) {
1208
+ d3.select(event.currentTarget)
1209
+ .classed('highlight', true)
1210
+ .raise();
1211
+ const myRtLabel = evidenceOverlayerMerge.append('g')
1212
+ .classed(`rt-label ${datum.outcome}`, true);
1213
+ const rect = myRtLabel.append('rect');
1214
+ const text = myRtLabel.append('text')
1215
+ .text(`RT = ${datum.rt.toFixed()}`)
1216
+ .attr('x', timeScale(datum.rt))
1217
+ .attr('y', datum.outcome === 'correct'
1218
+ ? evidenceScale(this.bounds.upper) - this.rem * 0.25
1219
+ : evidenceScale(this.bounds.lower) + this.rem * 0.125);
1220
+ const bbox = text.node().getBBox();
1221
+ rect
1222
+ .attr('x', bbox.x - this.rem * 0.125)
1223
+ .attr('y', bbox.y + this.rem * 0.125)
1224
+ .attr('width', bbox.width + this.rem * 0.25)
1225
+ .attr('height', bbox.height - this.rem * 0.25);
1226
+ rtLabel.set(event.currentTarget, myRtLabel);
1227
+ }
1228
+ })
1229
+ .on('pointerout', (event, datum) => {
1230
+ if (!this.drag) {
1231
+ d3.select(event.currentTarget)
1232
+ .classed('highlight', false)
1233
+ .lower();
1234
+ event.currentTarget.parentNode.insertBefore(
1235
+ event.currentTarget,
1236
+ event.currentTarget.parentNode.children[datum.index],
1237
+ );
1238
+ rtLabel.get(event.currentTarget).remove();
1239
+ }
1240
+ });
1241
+ pathEnter.append('path')
1242
+ .classed('curve', true)
1243
+ .attr('clip-path', 'url(#clip-evidence)')
1244
+ .attr('pathLength', 1)
1245
+ .attr('stroke-dashoffset', 1);
1246
+ // MERGE
1247
+ const pathMerge = pathEnter.merge(pathUpdate)
1248
+ .attr('class', (datum) => {
1249
+ return `path ${datum.outcome}`;
1250
+ });
1251
+ pathMerge.select('.curve')
1252
+ .transition()
1253
+ .duration(this.drag ? 0 : transitionDuration)
1254
+ .ease(d3.easeCubicOut)
1255
+ .attr('stroke', (datum) => {
1256
+ return this.getComputedStyleValue(`---color-${datum.outcome}`);
1257
+ })
1258
+ .attrTween('d', (datum, index, elements) => {
1259
+ const element = elements[index];
1260
+ const interpolateA = d3.interpolate(
1261
+ (element.a !== undefined) ? element.a : this.a,
1262
+ this.a,
1263
+ );
1264
+ const interpolateZ = d3.interpolate(
1265
+ (element.z !== undefined) ? element.z : this.z,
1266
+ this.z,
1267
+ );
1268
+ const interpolateV = d3.interpolate(
1269
+ (element.v !== undefined) ? element.v : this.v,
1270
+ this.v,
1271
+ );
1272
+ const interpolateT0 = d3.interpolate(
1273
+ (element.t0 !== undefined) ? element.t0 : this.t0,
1274
+ this.t0,
1275
+ );
1276
+ return (time) => {
1277
+ element.a = interpolateA(time);
1278
+ element.z = interpolateZ(time);
1279
+ element.v = interpolateV(time);
1280
+ element.t0 = interpolateT0(time);
1281
+ const path = this.alignPath(datum.seed, element.a, element.z, element.v, element.t0);
1282
+ return evidenceLine(path);
1283
+ };
1284
+ });
1285
+ // MERGE - Active Animate Paths
1286
+ const pathMergeNewActive = pathMerge.filter((datum) => {
1287
+ return (datum.animate && !this.paused);
1288
+ });
1289
+ if (!pathMergeNewActive.empty()) {
1290
+ const easeTime = pathMergeNewActive.attr('data-new-trial-ease-time');
1291
+ const scaleIn = (time) => {
1292
+ return d3.scaleLinear().domain([0, 1]).range([easeTime, 1])(time);
1293
+ };
1294
+ const scaleOutGenerator = (easeFunction) => {
1295
+ return (time) => {
1296
+ return d3.scaleLinear()
1297
+ .domain([easeFunction(easeTime), 1]).range([0, 1])(easeFunction(time));
1298
+ };
1299
+ };
1300
+ pathMergeNewActive
1301
+ .classed('animate', true)
1302
+ .select('.curve')
1303
+ .attr('stroke-dasharray', 1);
1304
+ pathMergeNewActive
1305
+ .transition('new')
1306
+ .duration((datum) => {
1307
+ // scale the RT for viewing pleasure
1308
+ return Math.floor((datum.rt * 1.5) * (1 - easeTime));
1309
+ })
1310
+ .ease(scaleIn)
1311
+ .attr('data-new-trial-ease-time', 1)
1312
+ .select('.curve')
1313
+ .attrTween('stroke-dashoffset', (datum, index, elements) => {
1314
+ const element = elements[index];
1315
+ const interpolator = d3.interpolate(
1316
+ element.getAttribute('stroke-dashoffset'),
1317
+ 0,
1318
+ );
1319
+ return (time) => { return interpolator(scaleOutGenerator(d3.easeLinear)(time)); };
1320
+ })
1321
+ .on('end', (datum, index, elements) => {
1322
+ const element = elements[index];
1323
+ d3.select(element.parentElement)
1324
+ .classed('animate', false)
1325
+ .attr('data-new-trial-ease-time', null);
1326
+ datum.animate = false;
1327
+ this.animate = false;
1328
+ this.alignState();
1329
+ this.requestUpdate();
1330
+ this.dispatchEvent(new CustomEvent('accumulable-response', {
1331
+ detail: {
1332
+ outcome: datum.outcome,
1333
+ data: this.data,
1334
+ model: this.model,
1335
+ },
1336
+ bubbles: true,
1337
+ }));
1338
+ });
1339
+ }
1340
+ // MERGE - Paused Animate Paths
1341
+ const pathMergeNewPaused = pathMerge.filter((datum) => {
1342
+ return (datum.animate && this.paused);
1343
+ });
1344
+ if (!pathMergeNewPaused.empty()) {
1345
+ const easeTime = pathMergeNewPaused.attr('data-new-trial-ease-time');
1346
+ pathMergeNewPaused
1347
+ .classed('animate', true)
1348
+ .select('.curve')
1349
+ .attr('stroke-dasharray', 1)
1350
+ .attr('stroke-dashoffset', () => {
1351
+ const interpolator = d3.interpolate(1, 0);
1352
+ return interpolator(d3.easeLinear(easeTime));
1353
+ });
1354
+ }
1355
+ // MERGE - Non-Animate Paths
1356
+ pathMerge.filter((datum) => { return (!datum.animate); })
1357
+ .attr('data-new-trial-ease-time', null);
1358
+ // EXIT
1359
+ pathUpdate.exit().remove();
1360
+
1361
+ // Distributions
1362
+ // ENTER
1363
+ const distEnter = densityContentEnter.append('g')
1364
+ .attr('class', (datum) => { return `dist ${datum.outcome}`; });
1365
+ distEnter.append('path')
1366
+ .classed('curve', true);
1367
+ // MERGE
1368
+ densityContentMerge.select('.dist').select('.curve')
1369
+ .transition()
1370
+ .duration(this.drag ? 0 : transitionDuration)
1371
+ .ease(d3.easeCubicOut)
1372
+ .attrTween('d', (datum, index, elements) => {
1373
+ const element = elements[index];
1374
+ const interpolateA = d3.interpolate(
1375
+ (element.a !== undefined) ? element.a : this.a,
1376
+ this.a,
1377
+ );
1378
+ const interpolateZ = d3.interpolate(
1379
+ (element.z !== undefined) ? element.z : this.z,
1380
+ this.z,
1381
+ );
1382
+ const interpolateV = d3.interpolate(
1383
+ (element.v !== undefined) ? element.v : this.v,
1384
+ this.v,
1385
+ );
1386
+ const interpolateT0 = d3.interpolate(
1387
+ (element.t0 !== undefined) ? element.t0 : this.t0,
1388
+ this.t0,
1389
+ );
1390
+ return (time) => {
1391
+ element.a = interpolateA(time);
1392
+ element.z = interpolateZ(time);
1393
+ element.v = interpolateV(time);
1394
+ element.t0 = interpolateT0(time);
1395
+ const path = datum.alignDistribution(element.a, element.z, element.v, element.t0);
1396
+ return datum.densityLine(path);
1397
+ };
1398
+ });
1399
+
1400
+ // RTs
1401
+ // DATA-JOIN
1402
+ const rtUpdate = evidenceContentMerge.selectAll('.rt')
1403
+ .data(this.data.trials);
1404
+ // ENTER
1405
+ const rtEnter = rtUpdate.enter().append('g');
1406
+ rtEnter.append('line')
1407
+ .classed('mark', true)
1408
+ .attr('x1', (datum) => {
1409
+ return timeScale(datum.rt);
1410
+ })
1411
+ .attr('x2', (datum) => {
1412
+ return timeScale(datum.rt);
1413
+ })
1414
+ .attr('y1', (datum) => {
1415
+ return (datum.outcome === 'correct')
1416
+ ? evidenceScale(1) - 0.125 * this.rem
1417
+ : evidenceScale(-1) + 0.125 * this.rem;
1418
+ })
1419
+ .attr('y2', (datum) => {
1420
+ return (datum.outcome === 'correct')
1421
+ ? evidenceScale(1) - 0.675 * this.rem
1422
+ : evidenceScale(-1) + 0.675 * this.rem;
1423
+ });
1424
+ // MERGE
1425
+ const rtMerge = rtEnter.merge(rtUpdate)
1426
+ .attr('class', (datum) => { return `rt ${datum.outcome}`; });
1427
+ rtMerge.filter((datum) => { return (!datum.animate); }).select('.mark')
1428
+ .transition()
1429
+ .duration(this.drag ? 0 : transitionDuration)
1430
+ .ease(d3.easeCubicOut)
1431
+ .attr('stroke', (datum) => {
1432
+ return this.getComputedStyleValue(`---color-${datum.outcome}`);
1433
+ })
1434
+ .attr('x1', (datum) => {
1435
+ return timeScale(datum.rt);
1436
+ })
1437
+ .attr('x2', (datum) => {
1438
+ return timeScale(datum.rt);
1439
+ })
1440
+ .attr('y1', (datum) => {
1441
+ return (datum.outcome === 'correct')
1442
+ ? evidenceScale(1) - 0.125 * this.rem
1443
+ : evidenceScale(-1) + 0.125 * this.rem;
1444
+ })
1445
+ .attr('y2', (datum) => {
1446
+ return (datum.outcome === 'correct')
1447
+ ? evidenceScale(1) - 0.675 * this.rem
1448
+ : evidenceScale(-1) + 0.675 * this.rem;
1449
+ });
1450
+ // EXIT
1451
+ rtUpdate.exit().remove();
1452
+
1453
+ // Model Accuracy
1454
+ // DATA-JOIN
1455
+ const accuracyUpdate = accuracyContentMerge.selectAll('.accuracy.model')
1456
+ .data(
1457
+ [this.model.accuracy, 1 - this.model.accuracy],
1458
+ );
1459
+ // ENTER
1460
+ const accuracyEnter = accuracyUpdate.enter().append('g')
1461
+ .attr('class', (_, index) => {
1462
+ return `accuracy model ${(index === 0) ? 'correct' : 'error'}`;
1463
+ });
1464
+ accuracyEnter.append('rect')
1465
+ .classed('bar', true)
1466
+ .attr('x', 0);
1467
+ // MERGE
1468
+ accuracyEnter.merge(accuracyUpdate).select('rect')
1469
+ .transition()
1470
+ .duration(this.drag ? 0 : transitionDuration)
1471
+ .ease(d3.easeCubicOut)
1472
+ // ## Tween based on params?
1473
+ .attr('y', (datum, index) => {
1474
+ return (index === 0) ? accuracyScale(0) : accuracyScale(1 - datum);
1475
+ })
1476
+ .attr('width', accuracyWidth)
1477
+ .attr('height', (datum) => {
1478
+ return accuracyScale(datum);
1479
+ });
1480
+ // EXIT
1481
+ accuracyUpdate.exit().remove();
1482
+
1483
+ // Data Accuracy
1484
+ // DATA-JOIN
1485
+ const dataAccuracyUpdate = accuracyContentMerge.selectAll('.accuracy.data')
1486
+ .data(
1487
+ !Number.isNaN(this.data.accuracy) ? [this.data.accuracy] : [],
1488
+ );
1489
+ // ENTER
1490
+ const dataAccuracyEnter = dataAccuracyUpdate.enter().append('g')
1491
+ .classed('accuracy data', true);
1492
+ dataAccuracyEnter.append('line')
1493
+ .classed('mark', true);
1494
+ // MERGE
1495
+ const dataAccuracyMerge = dataAccuracyEnter.merge(dataAccuracyUpdate)
1496
+ .attr('class', (datum) => {
1497
+ return `accuracy data ${(datum < this.model.accuracy.correct) ? 'correct' : 'error'}`;
1498
+ });
1499
+ dataAccuracyMerge.select('.mark')
1500
+ .transition()
1501
+ .duration(this.drag ? 0 : transitionDuration)
1502
+ .ease(d3.easeCubicOut)
1503
+ // ## Tween based on params?
1504
+ .attr('x1', 0 + 0.25 * this.rem)
1505
+ .attr('x2', accuracyWidth - 0.25 * this.rem)
1506
+ .attr('y1', (datum) => {
1507
+ return accuracyScale(datum) - 1;
1508
+ })
1509
+ .attr('y2', (datum) => {
1510
+ return accuracyScale(datum) - 1;
1511
+ });
1512
+ // EXIT
1513
+ dataAccuracyUpdate.exit().remove();
1514
+
1515
+ //
1516
+ // OVERLAYERS
1517
+ //
1518
+
1519
+ // Boundaries
1520
+ // DATA-JOIN
1521
+ const boundaryUpdate = evidenceOverlayerMerge.selectAll('.boundary')
1522
+ .data([
1523
+ {bound: 'upper', value: this.bounds.upper},
1524
+ {bound: 'lower', value: this.bounds.lower},
1525
+ ]);
1526
+ // ENTER
1527
+ const boundaryEnter = boundaryUpdate.enter().append('g')
1528
+ .attr('class', (_, index) => {
1529
+ return `boundary ${(index === 0) ? 'correct' : 'error'}`;
1530
+ });
1531
+ boundaryEnter.append('line')
1532
+ .classed('line', true);
1533
+ // MERGE
1534
+ const boundaryMerge = boundaryEnter.merge(boundaryUpdate)
1535
+ .attr('tabindex', this.interactive ? 0 : null)
1536
+ .classed('interactive', this.interactive)
1537
+ .on('keydown', this.interactive
1538
+ ? (event, datum) => {
1539
+ if (['ArrowUp', 'ArrowDown'].includes(event.key)) {
1540
+ let a = this.a; /* eslint-disable-line prefer-destructuring */
1541
+ switch (event.key) {
1542
+ case 'ArrowUp':
1543
+ a += (datum.bound === 'upper')
1544
+ ? event.shiftKey ? 0.01 : 0.1
1545
+ : event.shiftKey ? -0.01 : -0.1;
1546
+ break;
1547
+ case 'ArrowDown':
1548
+ a += (datum.bound === 'upper')
1549
+ ? event.shiftKey ? -0.01 : -0.1
1550
+ : event.shiftKey ? 0.01 : 0.1;
1551
+ break;
1552
+ default:
1553
+ }
1554
+ // Clamp boundaries to visible evidence
1555
+ a = (a < 0.01)
1556
+ ? 0.01
1557
+ : (a > this.scale.evidence.max * 2)
1558
+ ? this.scale.evidence.max * 2
1559
+ : a;
1560
+ this.a = a;
1561
+ this.alignState();
1562
+ this.dispatchEvent(new CustomEvent('ddm-model-a', {
1563
+ detail: {
1564
+ a: this.a,
1565
+ },
1566
+ bubbles: true,
1567
+ }));
1568
+ event.preventDefault();
1569
+ }
1570
+ }
1571
+ : null);
1572
+ if (
1573
+ this.firstUpdate
1574
+ || changedProperties.has('interactive')
1575
+ ) {
1576
+ if (this.interactive) {
1577
+ boundaryMerge.call(dragBoundary);
1578
+ } else {
1579
+ boundaryMerge.on('.drag', null);
1580
+ }
1581
+ }
1582
+ boundaryMerge.select('.line')
1583
+ .transition()
1584
+ .duration(this.drag ? 0 : transitionDuration)
1585
+ .ease(d3.easeCubicOut)
1586
+ .attr('x1', timeScale(this.scale.time.min))
1587
+ .attr('x2', timeScale(this.scale.time.max))
1588
+ .attr('y1', (datum) => {
1589
+ return evidenceScale(datum.value);
1590
+ })
1591
+ .attr('y2', (datum) => {
1592
+ return evidenceScale(datum.value);
1593
+ });
1594
+ // EXIT
1595
+ boundaryUpdate.exit().remove();
1596
+
1597
+ // Drift Rate
1598
+ // DATA-JOIN
1599
+ const driftUpdate = evidenceOverlayerMerge.selectAll('.drift')
1600
+ .data([
1601
+ {v: this.v, t0: this.t0, startingPoint: this.startingPoint},
1602
+ ]);
1603
+ // ENTER
1604
+ const driftEnter = driftUpdate.enter().append('g')
1605
+ .classed('drift', true);
1606
+ driftEnter.append('line')
1607
+ .classed('line', true);
1608
+ driftEnter.append('path')
1609
+ .classed('arrow', true);
1610
+ // MERGE
1611
+ const driftMerge = driftEnter.merge(driftUpdate)
1612
+ .attr('tabindex', this.interactive ? 0 : null)
1613
+ .classed('interactive', this.interactive)
1614
+ .on('keydown', this.interactive
1615
+ ? (event) => {
1616
+ if (['ArrowUp', 'ArrowDown'].includes(event.key)) {
1617
+ let v = this.v; /* eslint-disable-line prefer-destructuring */
1618
+ switch (event.key) {
1619
+ case 'ArrowUp':
1620
+ v += event.shiftKey ? 0.01 : 0.1;
1621
+ break;
1622
+ case 'ArrowDown':
1623
+ v -= event.shiftKey ? 0.01 : 0.1;
1624
+ break;
1625
+ default:
1626
+ }
1627
+ // Clamp z
1628
+ v = (v < 0.01)
1629
+ ? 0.01
1630
+ : (v > 5)
1631
+ ? 5
1632
+ : v;
1633
+ this.v = v;
1634
+ this.alignState();
1635
+ this.dispatchEvent(new CustomEvent('ddm-model-v', {
1636
+ detail: {
1637
+ v: this.v,
1638
+ },
1639
+ bubbles: true,
1640
+ }));
1641
+ event.preventDefault();
1642
+ }
1643
+ }
1644
+ : null);
1645
+ if (
1646
+ this.firstUpdate
1647
+ || changedProperties.has('interactive')
1648
+ ) {
1649
+ if (this.interactive) {
1650
+ driftMerge.call(dragDrift);
1651
+ } else {
1652
+ driftMerge.on('.drag', null);
1653
+ }
1654
+ }
1655
+ const scaleRatio = (evidenceScale(0) - evidenceScale(1)) / (timeScale(1) - timeScale(0));
1656
+ driftMerge
1657
+ .transition()
1658
+ .duration(this.drag ? 0 : transitionDuration)
1659
+ .ease(d3.easeCubicOut)
1660
+ .attr('transform', (datum) => {
1661
+ return `translate(${timeScale(datum.t0)}, ${evidenceScale(datum.startingPoint)})
1662
+ rotate(${-Math.atan((datum.v / 1000) * scaleRatio) * (180 / Math.PI)})`;
1663
+ });
1664
+ driftMerge.select('.line')
1665
+ .attr('x2', timeScale(200));
1666
+ driftMerge.select('.arrow')
1667
+ .attr('d', `
1668
+ M ${timeScale(200) - this.rem * 0.5},${-this.rem * 0.5}
1669
+ l ${this.rem * 0.5},${this.rem * 0.5}
1670
+ l ${-this.rem * 0.5},${this.rem * 0.5}
1671
+ `);
1672
+ // EXIT
1673
+ driftUpdate.exit().remove();
1674
+
1675
+ // Nondecision Time/Starting Point
1676
+ // DATA-JOIN
1677
+ const t0zUpdate = evidenceOverlayerMerge.selectAll('.t0z')
1678
+ .data([
1679
+ {t0: this.t0, startingPoint: this.startingPoint},
1680
+ ]);
1681
+ // ENTER
1682
+ const t0zEnter = t0zUpdate.enter().append('g')
1683
+ .classed('t0z', true);
1684
+ t0zEnter.append('line')
1685
+ .classed('line', true);
1686
+ t0zEnter.append('circle')
1687
+ .classed('point', true);
1688
+ // MERGE
1689
+ const t0zMerge = t0zEnter.merge(t0zUpdate)
1690
+ .attr('tabindex', this.interactive ? 0 : null)
1691
+ .classed('interactive', this.interactive)
1692
+ .on('keydown', this.interactive
1693
+ ? (event) => {
1694
+ if (['ArrowUp', 'ArrowDown'].includes(event.key)) {
1695
+ let z = this.z; /* eslint-disable-line prefer-destructuring */
1696
+ switch (event.key) {
1697
+ case 'ArrowUp':
1698
+ z += event.shiftKey ? 0.01 : 0.1;
1699
+ break;
1700
+ case 'ArrowDown':
1701
+ z -= event.shiftKey ? 0.01 : 0.1;
1702
+ break;
1703
+ default:
1704
+ }
1705
+ // Clamp z
1706
+ z = (z < 0.01)
1707
+ ? 0.01
1708
+ : (z > 0.99)
1709
+ ? 0.99
1710
+ : z;
1711
+ this.z = z;
1712
+ this.alignState();
1713
+ this.dispatchEvent(new CustomEvent('ddm-model-z', {
1714
+ detail: {
1715
+ z: this.z,
1716
+ },
1717
+ bubbles: true,
1718
+ }));
1719
+ event.preventDefault();
1720
+ }
1721
+ if (['ArrowLeft', 'ArrowRight'].includes(event.key)) {
1722
+ let t0 = this.t0; /* eslint-disable-line prefer-destructuring */
1723
+ switch (event.key) {
1724
+ case 'ArrowRight':
1725
+ t0 += event.shiftKey ? 1 : 10;
1726
+ break;
1727
+ case 'ArrowLeft':
1728
+ t0 -= event.shiftKey ? 1 : 10;
1729
+ break;
1730
+ default:
1731
+ }
1732
+ // Clamp t0
1733
+ t0 = (t0 < 0)
1734
+ ? 0
1735
+ : (t0 > 500)
1736
+ ? 500
1737
+ : t0;
1738
+ this.t0 = t0;
1739
+ this.alignState();
1740
+ this.dispatchEvent(new CustomEvent('ddm-model-t0', {
1741
+ detail: {
1742
+ t0: this.t0,
1743
+ },
1744
+ bubbles: true,
1745
+ }));
1746
+ event.preventDefault();
1747
+ }
1748
+ }
1749
+ : null);
1750
+ if (
1751
+ this.firstUpdate
1752
+ || changedProperties.has('interactive')
1753
+ ) {
1754
+ if (this.interactive) {
1755
+ t0zMerge.call(dragT0z);
1756
+ } else {
1757
+ t0zMerge.on('.drag', null);
1758
+ }
1759
+ }
1760
+ t0zMerge.select('.line')
1761
+ .transition()
1762
+ .duration(this.drag ? 0 : transitionDuration)
1763
+ .ease(d3.easeCubicOut)
1764
+ .attr('x1', timeScale(0))
1765
+ .attr('x2', (datum) => { return timeScale(datum.t0); })
1766
+ .attr('y1', (datum) => { return evidenceScale(datum.startingPoint); })
1767
+ .attr('y2', (datum) => { return evidenceScale(datum.startingPoint); });
1768
+ t0zMerge.select('.point')
1769
+ .transition()
1770
+ .duration(this.drag ? 0 : transitionDuration)
1771
+ .ease(d3.easeCubicOut)
1772
+ .attr('cx', (datum) => { return timeScale(datum.t0); })
1773
+ .attr('cy', (datum) => { return evidenceScale(datum.startingPoint); });
1774
+ // EXIT
1775
+ t0zUpdate.exit().remove();
1776
+
1777
+ // a Measure
1778
+ // DATA-JOIN
1779
+ const aUpdate = evidenceOverlayerMerge.selectAll('.measure.a')
1780
+ .data(this.measures ? [this.a] : []);
1781
+ // ENTER
1782
+ const aEnter = aUpdate.enter().append('g')
1783
+ .classed('measure a', true);
1784
+ aEnter.append('line')
1785
+ .classed('line', true)
1786
+ .attr('marker-start', 'url(#measure-arrow)')
1787
+ .attr('marker-end', 'url(#measure-arrow)');
1788
+ const aLabel = aEnter.append('text')
1789
+ .classed('label', true);
1790
+ aLabel.append('tspan')
1791
+ .classed('a math-var', true)
1792
+ .text('a');
1793
+ aLabel.append('tspan')
1794
+ .classed('equals', true)
1795
+ .text(' = ');
1796
+ aLabel.append('tspan')
1797
+ .classed('value', true);
1798
+ // MERGE
1799
+ const aMerge = aEnter.merge(aUpdate);
1800
+ aMerge.select('.line')
1801
+ .transition()
1802
+ .duration(this.drag ? 0 : transitionDuration)
1803
+ .ease(d3.easeCubicOut)
1804
+ .attr('x1', timeScale(this.scale.time.max) - this.rem * 0.75)
1805
+ .attr('y1', evidenceScale(this.bounds.upper) + 2)
1806
+ .attr('x2', timeScale(this.scale.time.max) - this.rem * 0.75)
1807
+ .attr('y2', evidenceScale(this.bounds.lower) - 2);
1808
+ const aLabelMerge = aMerge.select('.label')
1809
+ .transition()
1810
+ .duration(this.drag ? 0 : transitionDuration)
1811
+ .ease(d3.easeCubicOut)
1812
+ .attr('x', timeScale(this.scale.time.max))
1813
+ .attr('y', evidenceScale(this.bounds.upper) - this.rem * 0.25);
1814
+ aLabelMerge.select('.value')
1815
+ .text(d3.format('.2f')(this.a));
1816
+ // EXIT
1817
+ aUpdate.exit().remove();
1818
+
1819
+ // z Measure
1820
+ // DATA-JOIN
1821
+ const zUpdate = evidenceOverlayerMerge.selectAll('.measure.z')
1822
+ .data(this.measures ? [this.z] : []);
1823
+ // ENTER
1824
+ const zEnter = zUpdate.enter().append('g')
1825
+ .classed('measure z', true);
1826
+ zEnter.append('line')
1827
+ .classed('line', true)
1828
+ .attr('marker-start', 'url(#measure-arrow)')
1829
+ .attr('marker-end', 'url(#measure-arrow)');
1830
+ const zLabel = zEnter.append('text')
1831
+ .classed('label', true);
1832
+ zLabel.append('tspan')
1833
+ .classed('z math-var', true)
1834
+ .text('z');
1835
+ zLabel.append('tspan')
1836
+ .classed('equals', true)
1837
+ .text(' = ');
1838
+ zLabel.append('tspan')
1839
+ .classed('value', true);
1840
+ // MERGE
1841
+ const zMerge = zEnter.merge(zUpdate);
1842
+ zMerge.select('.line')
1843
+ .transition()
1844
+ .duration(this.drag ? 0 : transitionDuration)
1845
+ .ease(d3.easeCubicOut)
1846
+ .attr('x1', timeScale(this.scale.time.min) + this.rem * 0.75)
1847
+ .attr('y1', evidenceScale(this.startingPoint) + 2)
1848
+ .attr('x2', timeScale(this.scale.time.min) + this.rem * 0.75)
1849
+ .attr('y2', evidenceScale(this.bounds.lower) - 2);
1850
+ const zLabelMerge = zMerge.select('.label')
1851
+ .transition()
1852
+ .duration(this.drag ? 0 : transitionDuration)
1853
+ .ease(d3.easeCubicOut)
1854
+ .attr('x', timeScale(this.scale.time.min))
1855
+ .attr('y', evidenceScale(this.bounds.lower) + this.rem * 0.125);
1856
+ zLabelMerge.select('.value')
1857
+ .text(d3.format('.0%')(this.z));
1858
+ // EXIT
1859
+ zUpdate.exit().remove();
1860
+
1861
+ // v Measure
1862
+ // DATA-JOIN
1863
+ const vUpdate = evidenceOverlayerMerge.selectAll('.measure.v')
1864
+ .data(this.measures ? [this.v] : []);
1865
+ // ENTER
1866
+ const vEnter = vUpdate.enter().append('g')
1867
+ .classed('measure v', true);
1868
+ vEnter.append('path')
1869
+ .classed('line', true)
1870
+ .attr('marker-start', 'url(#measure-arrow)')
1871
+ .attr('marker-end', 'url(#measure-arrow)');
1872
+ const vLabel = vEnter.append('text')
1873
+ .classed('label', true);
1874
+ vLabel.append('tspan')
1875
+ .classed('v math-var', true)
1876
+ .text('v');
1877
+ vLabel.append('tspan')
1878
+ .classed('equals', true)
1879
+ .text(' = ');
1880
+ vLabel.append('tspan')
1881
+ .classed('value', true);
1882
+ // MERGE
1883
+ const driftAngle = Math.atan((this.v / 1000) * scaleRatio);
1884
+ const driftHypotenuse = timeScale(200) - timeScale(0) + this.rem * 0.75;
1885
+ const driftX = Math.cos(driftAngle) * driftHypotenuse;
1886
+ const driftY = Math.sin(driftAngle) * driftHypotenuse;
1887
+ const vMerge = vEnter.merge(vUpdate);
1888
+ vMerge.select('.line')
1889
+ .transition()
1890
+ .duration(this.drag ? 0 : transitionDuration)
1891
+ .ease(d3.easeCubicOut)
1892
+ .attr('d', `
1893
+ M ${timeScale(this.t0 + 200) + this.rem * 0.75}, ${evidenceScale(this.startingPoint)}
1894
+ A ${timeScale(200) - timeScale(0)} ${timeScale(200) - timeScale(0)} 0 0 0 ${timeScale(this.t0) + driftX} ${evidenceScale(this.startingPoint) - driftY}
1895
+ `);
1896
+ const vLabelMerge = vMerge.select('.label')
1897
+ .transition()
1898
+ .duration(this.drag ? 0 : transitionDuration)
1899
+ .ease(d3.easeCubicOut)
1900
+ .attr('x', timeScale(this.t0 + 200) + this.rem * 0.5)
1901
+ .attr('y', evidenceScale(this.bounds.upper) - this.rem * 0.25);
1902
+ vLabelMerge.select('.value')
1903
+ .text(d3.format('.2f')(this.v));
1904
+ // EXIT
1905
+ vUpdate.exit().remove();
1906
+
1907
+ // t0 Measure
1908
+ // DATA-JOIN
1909
+ const t0Update = evidenceOverlayerMerge.selectAll('.measure.t0')
1910
+ .data(this.measures ? [this.t0] : []);
1911
+ // ENTER
1912
+ const t0Enter = t0Update.enter().append('g')
1913
+ .classed('measure t0', true);
1914
+ t0Enter.append('line')
1915
+ .classed('line', true)
1916
+ .attr('marker-start', 'url(#measure-arrow)')
1917
+ .attr('marker-end', 'url(#measure-arrow)');
1918
+ const t0Label = t0Enter.append('text')
1919
+ .classed('label', true);
1920
+ t0Label.append('tspan')
1921
+ .classed('t0 math-var', true)
1922
+ .text('t₀');
1923
+ t0Label.append('tspan')
1924
+ .classed('equals', true)
1925
+ .text(' = ');
1926
+ t0Label.append('tspan')
1927
+ .classed('value', true);
1928
+ // MERGE
1929
+ const t0Merge = t0Enter.merge(t0Update);
1930
+ t0Merge.select('.line')
1931
+ .transition()
1932
+ .duration(this.drag ? 0 : transitionDuration)
1933
+ .ease(d3.easeCubicOut)
1934
+ .attr('x1', timeScale(0) + 2)
1935
+ .attr('y1', evidenceScale(this.startingPoint) - this.rem * 0.75)
1936
+ .attr('x2', timeScale(this.t0) - 2)
1937
+ .attr('y2', evidenceScale(this.startingPoint) - this.rem * 0.75);
1938
+ const t0LabelMerge = t0Merge.select('.label')
1939
+ .transition()
1940
+ .duration(this.drag ? 0 : transitionDuration)
1941
+ .ease(d3.easeCubicOut)
1942
+ .attr('x', timeScale(this.t0) + this.rem * 0.25)
1943
+ .attr('y', evidenceScale(this.bounds.upper) - this.rem * 0.25);
1944
+ t0LabelMerge.select('.value')
1945
+ .text(d3.format('d')(this.t0));
1946
+ // EXIT
1947
+ t0Update.exit().remove();
1948
+
1949
+ // Means
1950
+ // DATA-JOIN
1951
+ const meanUpdate = densityOverlayerMerge.selectAll('.model.mean')
1952
+ .data((datum) => {
1953
+ return this.means ? [datum] : [];
1954
+ });
1955
+ // ENTER
1956
+ const meanEnter = meanUpdate.enter().append('g')
1957
+ .attr('class', (datum) => { return `model mean ${datum.outcome}`; });
1958
+ meanEnter.append('line')
1959
+ .classed('indicator', true);
1960
+ // MERGE
1961
+ const meanMerge = meanEnter.merge(meanUpdate);
1962
+ meanMerge.select('.indicator')
1963
+ .transition()
1964
+ .duration(this.drag ? 0 : transitionDuration)
1965
+ .ease(d3.easeCubicOut)
1966
+ .attr('x1', (datum) => { return timeScale(datum.model.meanRT); })
1967
+ .attr('x2', (datum) => { return timeScale(datum.model.meanRT); })
1968
+ .attr('y1', (datum) => { return datum.densityScale(this.scale.density.min); })
1969
+ .attr('y2', (datum) => { return datum.densityScale(this.scale.density.max); });
1970
+ // EXIT
1971
+ meanUpdate.exit().remove();
1972
+
1973
+ // Data Means
1974
+ // DATA-JOIN
1975
+ const dataMeanUpdate = densityOverlayerMerge.selectAll('.data.mean')
1976
+ .data((datum) => {
1977
+ return (this.means && !Number.isNaN(datum.data.meanRT)) ? [datum] : [];
1978
+ });
1979
+ // ENTER
1980
+ const dataMeanEnter = dataMeanUpdate.enter().append('g')
1981
+ .attr('class', (datum) => { return `data mean ${datum.outcome}`; });
1982
+ dataMeanEnter.append('line')
1983
+ .classed('indicator', true)
1984
+ .attr('y1', (datum) => {
1985
+ return datum.densityScale(0) + ((datum.outcome === 'correct') ? 0.125 : -0.125) * this.rem;
1986
+ })
1987
+ .attr('y2', (datum) => {
1988
+ return datum.densityScale(0) + ((datum.outcome === 'correct') ? 0.675 : -0.675) * this.rem;
1989
+ });
1990
+ // MERGE
1991
+ const dataMeanMerge = dataMeanEnter.merge(dataMeanUpdate);
1992
+ dataMeanMerge.select('.indicator')
1993
+ .transition()
1994
+ .duration(this.drag ? 0 : transitionDuration)
1995
+ .ease(d3.easeCubicOut)
1996
+ .attr('x1', (datum) => { return timeScale(datum.data.meanRT); })
1997
+ .attr('x2', (datum) => { return timeScale(datum.data.meanRT); })
1998
+ .attr('y1', (datum) => {
1999
+ return datum.densityScale(0) + ((datum.outcome === 'correct') ? 0.125 : -0.125) * this.rem;
2000
+ })
2001
+ .attr('y2', (datum) => {
2002
+ return datum.densityScale(0) + ((datum.outcome === 'correct') ? 0.675 : -0.675) * this.rem;
2003
+ });
2004
+ // EXIT
2005
+ dataMeanUpdate.exit().select('.indicator')
2006
+ .transition()
2007
+ .duration(this.drag ? 0 : transitionDuration)
2008
+ .ease(d3.easeCubicOut)
2009
+ .attr('x1', 0)
2010
+ .attr('x2', 0)
2011
+ .on('end', (datum, index, elements) => {
2012
+ d3.select(elements[index].parentElement).remove();
2013
+ });
2014
+
2015
+ // Standard Deviations
2016
+ // DATA-JOIN
2017
+ const sdUpdate = densityOverlayerMerge.selectAll('.model.sd')
2018
+ .data((datum) => {
2019
+ return this.sds ? [datum] : [];
2020
+ });
2021
+ // ENTER
2022
+ const sdEnter = sdUpdate.enter().append('g')
2023
+ .attr('class', (datum) => { return `model sd ${datum.outcome}`; });
2024
+ sdEnter.append('line')
2025
+ .classed('indicator', true)
2026
+ .attr('marker-start', 'url(#model-sd-cap)')
2027
+ .attr('marker-end', 'url(#model-sd-cap)');
2028
+ // MERGE
2029
+ const sdMerge = sdEnter.merge(sdUpdate);
2030
+ sdMerge.select('.indicator')
2031
+ .transition()
2032
+ .duration(this.drag ? 0 : transitionDuration)
2033
+ .ease(d3.easeCubicOut)
2034
+ .attr('x1', (datum) => { return timeScale(datum.model.meanRT - (datum.model.sdRT / 2)); })
2035
+ .attr('x2', (datum) => { return timeScale(datum.model.meanRT + (datum.model.sdRT / 2)); })
2036
+ .attr('y1', (datum) => { return datum.densityScale(5); })
2037
+ .attr('y2', (datum) => { return datum.densityScale(5); });
2038
+ // EXIT
2039
+ sdUpdate.exit().remove();
2040
+
2041
+ // Data Standard Deviation
2042
+ // DATA-JOIN
2043
+ const dataSDUpdate = densityOverlayerMerge.selectAll('.data.sd')
2044
+ .data((datum) => {
2045
+ return (this.sds && !Number.isNaN(datum.data.meanRT) && !Number.isNaN(datum.data.sdRT))
2046
+ ? [datum]
2047
+ : [];
2048
+ });
2049
+ // ENTER
2050
+ const dataSDEnter = dataSDUpdate.enter().append('g')
2051
+ .attr('class', (datum) => { return `data sd ${datum.outcome}`; });
2052
+ dataSDEnter.append('line')
2053
+ .classed('indicator', true)
2054
+ .attr('marker-start', 'url(#data-sd-cap)')
2055
+ .attr('marker-end', 'url(#data-sd-cap)')
2056
+ .attr('y1', (datum) => {
2057
+ return datum.densityScale(0) + ((datum.outcome === 'correct') ? 0.375 : -0.375) * this.rem;
2058
+ })
2059
+ .attr('y2', (datum) => {
2060
+ return datum.densityScale(0) + ((datum.outcome === 'correct') ? 0.375 : -0.375) * this.rem;
2061
+ });
2062
+ // MERGE
2063
+ const dataSDMerge = dataSDEnter.merge(dataSDUpdate);
2064
+ dataSDMerge.select('.indicator')
2065
+ .transition()
2066
+ .duration(this.drag ? 0 : transitionDuration)
2067
+ .ease(d3.easeCubicOut)
2068
+ .attr('x1', (datum) => {
2069
+ return timeScale(datum.data.meanRT - (datum.data.sdRT / 2));
2070
+ })
2071
+ .attr('x2', (datum) => {
2072
+ return timeScale(datum.data.meanRT + (datum.data.sdRT / 2));
2073
+ })
2074
+ .attr('y1', (datum) => {
2075
+ return datum.densityScale(0) + ((datum.outcome === 'correct') ? 0.375 : -0.375) * this.rem;
2076
+ })
2077
+ .attr('y2', (datum) => {
2078
+ return datum.densityScale(0) + ((datum.outcome === 'correct') ? 0.375 : -0.375) * this.rem;
2079
+ });
2080
+ // EXIT
2081
+ dataSDUpdate.exit().select('.indicator')
2082
+ .transition()
2083
+ .duration(this.drag ? 0 : transitionDuration)
2084
+ .ease(d3.easeCubicOut)
2085
+ .attr('x1', 0)
2086
+ .attr('x2', 0)
2087
+ .on('end', (datum, index, elements) => {
2088
+ d3.select(elements[index].parentElement).remove();
2089
+ });
2090
+
2091
+ this.firstUpdate = false;
2092
+ }
2093
+ }
2094
+
2095
+ customElements.define('ddm-model', DDMModel);