@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,428 @@
1
+
2
+ import {html, css} from 'lit';
3
+
4
+ import DDMMath from '@decidables/accumulable-math';
5
+ import '@decidables/decidables-elements/button';
6
+
7
+ import AccumulableElement from '../accumulable-element';
8
+
9
+ /*
10
+ AccumulableResponse element
11
+ <accumulable-response>
12
+
13
+ Attributes:
14
+
15
+ */
16
+ export default class AccumulableResponse extends AccumulableElement {
17
+ static get properties() {
18
+ return {
19
+ feedback: {
20
+ attribute: 'feedback',
21
+ type: Boolean,
22
+ reflect: true,
23
+ },
24
+ trial: {
25
+ attribute: 'trial',
26
+ type: Boolean,
27
+ reflect: true,
28
+ },
29
+ payoff: {
30
+ attribute: 'payoff',
31
+ type: String,
32
+ reflect: true,
33
+ },
34
+ correctPayoff: {
35
+ attribute: 'correct-payoff',
36
+ type: Number,
37
+ reflect: true,
38
+ },
39
+ errorPayoff: {
40
+ attribute: 'error-payoff',
41
+ type: Number,
42
+ reflect: true,
43
+ },
44
+ nrPayoff: {
45
+ attribute: 'no-response-payoff',
46
+ type: Number,
47
+ reflect: true,
48
+ },
49
+
50
+ state: {
51
+ attribute: false,
52
+ type: String,
53
+ reflect: false,
54
+ },
55
+ trialCount: {
56
+ attribute: false,
57
+ type: Number,
58
+ reflect: false,
59
+ },
60
+ trialTotal: {
61
+ attribute: false,
62
+ type: Number,
63
+ reflect: false,
64
+ },
65
+ };
66
+ }
67
+
68
+ constructor() {
69
+ super();
70
+
71
+ // Attributes
72
+ this.feedback = false; // Display feedback?
73
+ this.trial = false; // Show trial count?
74
+ this.payoffs = ['none', 'trial', 'total']; // Possible types of 'payoff' info
75
+ this.payoff = 'none'; // What payoff info to display
76
+
77
+ this.correctPayoff = 0; // Correct payoff
78
+ this.errorPayoff = 0; // Error payoff
79
+ this.nrPayoff = 0; // No Response payoff
80
+
81
+ // Properties
82
+ this.states = ['off', 'waiting', 'feedback']; // Possible states
83
+ this.state = 'off'; // Current state
84
+
85
+ this.trialCount = 0; // Current trial
86
+ this.trialTotal = 0; // Total trials
87
+
88
+ // Private
89
+ this.signals = ['left', 'right']; // Possible values of 'signal'
90
+ this.signal = undefined; // Signal for current trial
91
+ this.responses = ['left', 'right']; // Possible values of 'response'
92
+ this.response = undefined; // Response for current trial
93
+ this.outcomes = ['correct', 'error', 'nr']; // Possible values of 'outcome'
94
+ this.outcome = undefined; // Outcome for current trial
95
+ this.startTime = undefined; // Start time of current trial
96
+ this.rt = undefined; // RT for current trial
97
+
98
+ this.correctCount = 0; // Count of Correct Trials
99
+ this.errorCount = 0; // Count of Error Trials
100
+ this.nrCount = 0; // Count of No Response trials
101
+
102
+ this.trials = []; // Record of trials in block
103
+ this.alignState();
104
+ }
105
+
106
+ get trialPayoff() {
107
+ switch (this.outcome) {
108
+ case 'correct':
109
+ return this.correctPayoff;
110
+ case 'error':
111
+ return this.errorPayoff;
112
+ case 'nr':
113
+ return this.nrPayoff;
114
+ default:
115
+ return undefined;
116
+ }
117
+ }
118
+
119
+ get totalPayoff() {
120
+ return ((this.correctCount * this.correctPayoff)
121
+ + (this.errorCount * this.errorPayoff)
122
+ + (this.nrCount * this.nrPayoff));
123
+ }
124
+
125
+ alignState() {
126
+ const stats = DDMMath.trials2stats(this.trials);
127
+ Object.assign(this, stats);
128
+ }
129
+
130
+ start(signal, trial) {
131
+ this.startTime = Date.now();
132
+ this.trialCount = trial;
133
+ this.state = 'waiting';
134
+ this.signal = signal;
135
+ this.response = undefined;
136
+ this.outcome = undefined;
137
+ this.rt = undefined;
138
+ }
139
+
140
+ stop() {
141
+ this.state = 'feedback';
142
+ if (this.response === undefined) {
143
+ this.outcome = 'nr';
144
+ this.nrCount += 1;
145
+ this.rt = undefined;
146
+
147
+ this.trials.push({
148
+ trial: this.trialCount,
149
+ rt: this.rt,
150
+ signal: this.signal,
151
+ response: this.response,
152
+ outcome: this.outcome,
153
+ payoff: this.trialPayoff,
154
+ });
155
+ this.alignState();
156
+ }
157
+ }
158
+
159
+ left() {
160
+ this.responded('left');
161
+ }
162
+
163
+ right() {
164
+ this.responded('right');
165
+ }
166
+
167
+ responded(response) {
168
+ this.rt = Date.now() - this.startTime;
169
+ this.state = 'feedback';
170
+ this.response = response;
171
+ if (this.signal === this.response) {
172
+ this.outcome = 'correct';
173
+ this.correctCount += 1;
174
+ } else if (this.signal !== this.response) {
175
+ this.outcome = 'error';
176
+ this.errorCount += 1;
177
+ }
178
+ this.trials.push({
179
+ trial: this.trialCount,
180
+ rt: this.rt,
181
+ signal: this.signal,
182
+ response: this.response,
183
+ outcome: this.outcome,
184
+ payoff: this.trialPayoff,
185
+ });
186
+ this.alignState();
187
+
188
+ this.dispatchEvent(new CustomEvent('accumulable-response', {
189
+ detail: {
190
+ trial: this.trialCount,
191
+ rt: this.rt,
192
+ signal: this.signal,
193
+ response: this.response,
194
+ outcome: this.outcome,
195
+ payoff: this.trialPayoff,
196
+
197
+ correctCount: this.correctCount,
198
+ errorCount: this.errorCount,
199
+ nrCount: this.nrCount,
200
+ accuracy: this.accuracy,
201
+ meanRT: this.meanRT,
202
+ correctMeanRT: this.correctMeanRT,
203
+ errorMeanRT: this.errorMeanRT,
204
+ sdRT: this.sdRT,
205
+ correctSDRT: this.correctSDRT,
206
+ errorSDRT: this.errorSDRT,
207
+
208
+ totalPayoff: this.totalPayoff,
209
+ },
210
+ bubbles: true,
211
+ }));
212
+ }
213
+
214
+ reset() {
215
+ this.state = 'off';
216
+ this.trialCount = 0;
217
+ this.rt = undefined;
218
+ this.signal = undefined;
219
+ this.response = undefined;
220
+ this.outcome = undefined;
221
+ this.correctCount = 0;
222
+ this.errorCount = 0;
223
+ this.nrCount = 0;
224
+
225
+ this.trials = [];
226
+ this.alignState();
227
+ }
228
+
229
+ keydown(event) {
230
+ if (this.state === 'waiting') {
231
+ if (event.key === 'ArrowLeft') {
232
+ this.responded('left');
233
+ event.preventDefault();
234
+ } else if (event.key === 'ArrowRight') {
235
+ this.responded('right');
236
+ event.preventDefault();
237
+ }
238
+ }
239
+ }
240
+
241
+ connectedCallback() {
242
+ super.connectedCallback();
243
+
244
+ window.addEventListener('keydown', this.keydown.bind(this));
245
+ }
246
+
247
+ disconnectedCallback() {
248
+ window.removeEventListener('keydown', this.keydown.bind(this));
249
+
250
+ super.disconnectedCallback();
251
+ }
252
+
253
+ static get styles() {
254
+ return [
255
+ super.styles,
256
+ css`
257
+ :host {
258
+ display: inline-block;
259
+ }
260
+
261
+ /* Overall container */
262
+ .holder {
263
+ display: flex;
264
+
265
+ flex-direction: column;
266
+ }
267
+
268
+ /* Response buttons */
269
+ .responses {
270
+ display: flex;
271
+
272
+ flex-direction: row;
273
+
274
+ align-items: stretch;
275
+ justify-content: center;
276
+ }
277
+
278
+ .response {
279
+ flex: 1 0 50%;
280
+ }
281
+
282
+ .waiting[disabled] {
283
+ --decidables-button-background-color: var(---color-element-enabled);
284
+ }
285
+
286
+ .selected[disabled][name="left"] {
287
+ --decidables-button-background-color: var(---color-left);
288
+ }
289
+
290
+ .selected[disabled][name="right"] {
291
+ --decidables-button-background-color: var(---color-right);
292
+ }
293
+
294
+ /* Feedback messages */
295
+ .feedbacks {
296
+ display: flex;
297
+
298
+ flex-direction: column;
299
+
300
+ align-items: center;
301
+
302
+ margin: 0.25rem;
303
+ }
304
+
305
+ /* Trial feedback */
306
+ .trial {
307
+ text-align: center;
308
+ }
309
+
310
+ .trial .label {
311
+ font-weight: 600;
312
+ }
313
+
314
+ /* Outcome feedback */
315
+ .feedback {
316
+ display: flex;
317
+
318
+ flex-direction: column;
319
+
320
+ align-items: center;
321
+ justify-content: center;
322
+
323
+ width: 6rem;
324
+ height: 3.5rem;
325
+ padding: 0.375rem 0.75rem;
326
+ margin: 0.25rem;
327
+
328
+ text-align: center;
329
+
330
+ background-color: var(---color-element-background);
331
+ border: 1px solid var(---color-element-border);
332
+ }
333
+
334
+ :host([payoff="trial"]) .feedback,
335
+ :host([payoff="total"]) .feedback {
336
+ height: 5rem;
337
+ }
338
+
339
+ .feedback.correct {
340
+ background-color: var(---color-correct-light);
341
+ }
342
+
343
+ .feedback.error {
344
+ background-color: var(---color-error-light);
345
+ }
346
+
347
+ .feedback.nr {
348
+ background-color: var(---color-nr-light);
349
+ }
350
+
351
+ .feedback .outcome {
352
+ font-weight: 600;
353
+ line-height: 1.15;
354
+ }
355
+
356
+ /* Payoff feedback */
357
+ .total {
358
+ text-align: center;
359
+ }
360
+
361
+ .total .label {
362
+ font-weight: 600;
363
+ }
364
+ `,
365
+ ];
366
+ }
367
+
368
+ render() {
369
+ const payoffFormatter = new Intl.NumberFormat('en-US', {
370
+ style: 'currency',
371
+ currency: 'USD',
372
+ minimumFractionDigits: 0,
373
+ maximumFractionDigits: 0,
374
+ });
375
+ const payoffFormat = (number) => {
376
+ return payoffFormatter.formatToParts(number).map(({type, value}) => {
377
+ if (type === 'minusSign') {
378
+ return '−';
379
+ }
380
+ return value;
381
+ }).reduce((string, part) => { return string + part; });
382
+ };
383
+
384
+ return html`
385
+ <div class="holder">
386
+ <div class="responses">
387
+ <decidables-button name="left" class=${`response ${(this.state === 'feedback' && this.response === 'left') ? 'selected' : ((this.state === 'waiting') ? 'waiting' : '')}`} ?disabled=${this.state !== 'waiting' || this.interactive !== true} @click=${this.left.bind(this)}>Left</decidables-button>
388
+ <decidables-button name="right" class=${`response ${(this.state === 'feedback' && this.response === 'right') ? 'selected' : ((this.state === 'waiting') ? 'waiting' : '')}`} ?disabled=${this.state !== 'waiting' || this.interactive !== true} @click=${this.right.bind(this)}>Right</decidables-button>
389
+ </div>
390
+ ${(this.trial || this.feedback || this.payoff === 'total')
391
+ ? html`
392
+ <div class="feedbacks">
393
+ ${(this.trial)
394
+ ? html`
395
+ <div class="trial">
396
+ <span class="label">Trial: </span><span class="count">${this.trialCount}</span><span class="of"> of </span><span class="total">${this.trialTotal}</span>
397
+ </div>`
398
+ : html``}
399
+ ${(this.feedback)
400
+ ? html`
401
+ <div class=${`feedback ${((this.state === 'feedback') && this.feedback)
402
+ ? this.outcome
403
+ : ''}`}>
404
+ ${((this.state === 'feedback') && this.feedback)
405
+ ? (this.outcome === 'correct')
406
+ ? html`<span class="outcome">Correct</span>`
407
+ : (this.outcome === 'error')
408
+ ? html`<span class="outcome">Error</span>`
409
+ : html`<span class="outcome">No<br>Response</span>`
410
+ : ''}
411
+ ${((this.state === 'feedback') && (this.payoff === 'trial' || this.payoff === 'total'))
412
+ ? html`<span class="payoff">${payoffFormat(this.trialPayoff)}</span>`
413
+ : html``}
414
+ </div>`
415
+ : html``}
416
+ ${(this.payoff === 'total')
417
+ ? html`
418
+ <div class="total">
419
+ <span class="label">Total: </span><span class="value">${payoffFormat(this.totalPayoff)}</span>
420
+ </div>`
421
+ : html``}
422
+ </div>`
423
+ : html``}
424
+ </div>`;
425
+ }
426
+ }
427
+
428
+ customElements.define('accumulable-response', AccumulableResponse);