@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,602 @@
1
+
2
+ import {html, css} from 'lit';
3
+
4
+ import '@decidables/decidables-elements/spinner';
5
+ import DecidablesConverterSet from '@decidables/decidables-elements/converter-set';
6
+ import SDTMath from '@decidables/detectable-math';
7
+
8
+ import DetectableElement from '../detectable-element';
9
+
10
+ /*
11
+ DetectableTable element
12
+ <detectable-table>
13
+
14
+ Attributes:
15
+ Hit; Miss; FalseAlarm; CorrectRejection;
16
+ */
17
+ export default class DetectableTable extends DetectableElement {
18
+ static get properties() {
19
+ return {
20
+ numeric: {
21
+ attribute: 'numeric',
22
+ type: Boolean,
23
+ reflect: true,
24
+ },
25
+ summary: {
26
+ attribute: 'summary',
27
+ converter: DecidablesConverterSet,
28
+ reflect: true,
29
+ },
30
+ color: {
31
+ attribute: 'color',
32
+ type: String,
33
+ reflect: true,
34
+ },
35
+ h: {
36
+ attribute: 'hits',
37
+ type: Number,
38
+ reflect: true,
39
+ },
40
+ m: {
41
+ attribute: 'misses',
42
+ type: Number,
43
+ reflect: true,
44
+ },
45
+ fa: {
46
+ attribute: 'false-alarms',
47
+ type: Number,
48
+ reflect: true,
49
+ },
50
+ cr: {
51
+ attribute: 'correct-rejections',
52
+ type: Number,
53
+ reflect: true,
54
+ },
55
+
56
+ payoff: {
57
+ attribute: 'payoff',
58
+ type: Boolean,
59
+ reflect: true,
60
+ },
61
+ hPayoff: {
62
+ attribute: 'hit-payoff',
63
+ type: Number,
64
+ reflect: true,
65
+ },
66
+ mPayoff: {
67
+ attribute: 'miss-payoff',
68
+ type: Number,
69
+ reflect: true,
70
+ },
71
+ faPayoff: {
72
+ attribute: 'false-alarm-payoff',
73
+ type: Number,
74
+ reflect: true,
75
+ },
76
+ crPayoff: {
77
+ attribute: 'correct-rejection-payoff',
78
+ type: Number,
79
+ reflect: true,
80
+ },
81
+
82
+ far: {
83
+ attribute: false,
84
+ type: Number,
85
+ reflect: false,
86
+ },
87
+ hr: {
88
+ attribute: false,
89
+ type: Number,
90
+ reflect: false,
91
+ },
92
+ acc: {
93
+ attribute: false,
94
+ type: Number,
95
+ reflect: false,
96
+ },
97
+ // positive predictive value (https://en.wikipedia.org/wiki/Receiver_operating_characteristic)
98
+ ppv: {
99
+ attribute: false,
100
+ type: Number,
101
+ reflect: false,
102
+ },
103
+ // false omission rate (https://en.wikipedia.org/wiki/Receiver_operating_characteristic)
104
+ // Using "fomr" to avoid keyword "for"
105
+ fomr: {
106
+ attribute: false,
107
+ type: Number,
108
+ reflect: false,
109
+ },
110
+ };
111
+ }
112
+
113
+ constructor() {
114
+ super();
115
+
116
+ this.numeric = false;
117
+
118
+ this.summaries = ['stimulusRates', 'responseRates', 'accuracy'];
119
+ this.summary = new Set();
120
+
121
+ this.colors = ['none', 'accuracy', 'stimulus', 'response', 'outcome'];
122
+ this.color = 'outcome';
123
+
124
+ this.h = 40;
125
+ this.m = 60;
126
+ this.fa = 75;
127
+ this.cr = 25;
128
+ this.alignState();
129
+
130
+ this.payoff = false;
131
+ this.hPayoff = undefined; // Hit payoff
132
+ this.mPayoff = undefined; // Miss payoff
133
+ this.crPayoff = undefined; // Correct Rejection payoff
134
+ this.faPayoff = undefined; // False Alarm payoff
135
+ }
136
+
137
+ alignState() {
138
+ this.hr = SDTMath.hM2Hr(this.h, this.m);
139
+ this.far = SDTMath.faCr2Far(this.fa, this.cr);
140
+ this.acc = SDTMath.hMFaCr2Acc(this.h, this.m, this.fa, this.cr);
141
+ this.ppv = SDTMath.hFa2Ppv(this.h, this.fa);
142
+ this.fomr = SDTMath.mCr2Fomr(this.m, this.cr);
143
+ }
144
+
145
+ sendEvent() {
146
+ this.dispatchEvent(new CustomEvent('detectable-table-change', {
147
+ detail: {
148
+ h: this.h,
149
+ m: this.m,
150
+ hr: this.hr,
151
+ fa: this.fa,
152
+ cr: this.cr,
153
+ far: this.far,
154
+ acc: this.acc,
155
+ ppv: this.ppv,
156
+ fomr: this.fomr,
157
+ },
158
+ bubbles: true,
159
+ }));
160
+ }
161
+
162
+ hInput(e) {
163
+ this.h = parseInt(e.target.value, 10);
164
+ this.alignState();
165
+ this.sendEvent();
166
+ }
167
+
168
+ mInput(e) {
169
+ this.m = parseInt(e.target.value, 10);
170
+ this.alignState();
171
+ this.sendEvent();
172
+ }
173
+
174
+ faInput(e) {
175
+ this.fa = parseInt(e.target.value, 10);
176
+ this.alignState();
177
+ this.sendEvent();
178
+ }
179
+
180
+ crInput(e) {
181
+ this.cr = parseInt(e.target.value, 10);
182
+ this.alignState();
183
+ this.sendEvent();
184
+ }
185
+
186
+ hrInput(e) {
187
+ const newhr = parseFloat(e.target.value);
188
+ const present = this.h + this.m;
189
+ this.h = Math.round(newhr * present);
190
+ this.m = present - this.h;
191
+ this.alignState();
192
+ this.sendEvent();
193
+ }
194
+
195
+ farInput(e) {
196
+ const newfar = parseFloat(e.target.value);
197
+ const absent = this.fa + this.cr;
198
+ this.fa = Math.round(newfar * absent);
199
+ this.cr = absent - this.fa;
200
+ this.alignState();
201
+ this.sendEvent();
202
+ }
203
+
204
+ accInput(e) {
205
+ const newacc = parseFloat(e.target.value);
206
+ const present = this.h + this.m;
207
+ const absent = this.fa + this.cr;
208
+ const x = (this.hr + this.far - 1) / 2; // Rotate into ACC
209
+ let newhr = x + newacc;
210
+ let newfar = 1 + x - newacc;
211
+
212
+ if (newfar > 1) {
213
+ newfar = 1;
214
+ newhr = newfar + 2 * newacc - 1;
215
+ }
216
+ if (newfar < 0) {
217
+ newfar = 0;
218
+ newhr = newfar + 2 * newacc - 1;
219
+ }
220
+ if (newhr > 1) {
221
+ newhr = 1;
222
+ newfar = newhr - 2 * newacc + 1;
223
+ }
224
+ if (newhr < 0) {
225
+ newhr = 0;
226
+ newfar = newhr - 2 * newacc + 1;
227
+ }
228
+ this.h = Math.round(newhr * present);
229
+ this.m = present - this.h;
230
+ this.fa = Math.round(newfar * absent);
231
+ this.cr = absent - this.fa;
232
+
233
+ this.alignState();
234
+ this.sendEvent();
235
+ }
236
+
237
+ ppvInput(e) {
238
+ const newppv = parseFloat(e.target.value);
239
+ const present = this.h + this.fa;
240
+ this.h = Math.round(newppv * present);
241
+ this.fa = present - this.h;
242
+ this.alignState();
243
+ this.sendEvent();
244
+ }
245
+
246
+ fomrInput(e) {
247
+ const newfomr = parseFloat(e.target.value);
248
+ const present = this.m + this.cr;
249
+ this.m = Math.round(newfomr * present);
250
+ this.cr = present - this.m;
251
+ this.alignState();
252
+ this.sendEvent();
253
+ }
254
+
255
+ static get styles() {
256
+ return [
257
+ super.styles,
258
+ css`
259
+ :host {
260
+ display: inline-block;
261
+ }
262
+
263
+ /* Overall element */
264
+ table {
265
+ text-align: center;
266
+
267
+ border-collapse: collapse;
268
+
269
+ border: 0;
270
+ }
271
+
272
+ /* Headers */
273
+ .th-main {
274
+ padding: 0;
275
+
276
+ font-weight: bold;
277
+ }
278
+
279
+ .th-sub {
280
+ padding: 0 0.25rem;
281
+
282
+ font-weight: 600;
283
+ }
284
+
285
+ .th-left {
286
+ padding-left: 0;
287
+
288
+ text-align: right;
289
+ }
290
+
291
+ /* Cells */
292
+ .td {
293
+ width: 10rem;
294
+
295
+ padding: 0.25rem 0.25rem 0.375rem;
296
+
297
+ transition: all var(---transition-duration) ease;
298
+ }
299
+
300
+ .numeric .td {
301
+ width: 7rem;
302
+ }
303
+
304
+ /* Labels */
305
+ .payoff {
306
+ font-weight: 600;
307
+ line-height: 0.75rem;
308
+ }
309
+
310
+ /* User interaction <input> */
311
+ .td-data decidables-spinner {
312
+ --decidables-spinner-input-width: 3.5rem;
313
+ }
314
+
315
+ .td-summary decidables-spinner {
316
+ --decidables-spinner-input-width: 4.5rem;
317
+ }
318
+
319
+ /* Color schemes & Table emphasis */
320
+
321
+ /* (Default) Outcome color scheme */
322
+ .h {
323
+ background: var(---color-h-light);
324
+ border-top: 2px solid var(---color-element-emphasis);
325
+ border-left: 2px solid var(---color-element-emphasis);
326
+ }
327
+
328
+ .m {
329
+ background: var(---color-m-light);
330
+ border-top: 2px solid var(---color-element-emphasis);
331
+ border-right: 2px solid var(---color-element-emphasis);
332
+ }
333
+
334
+ .fa {
335
+ background: var(---color-fa-light);
336
+ border-bottom: 2px solid var(---color-element-emphasis);
337
+ border-left: 2px solid var(---color-element-emphasis);
338
+ }
339
+
340
+ .cr {
341
+ background: var(---color-cr-light);
342
+ border-right: 2px solid var(---color-element-emphasis);
343
+ border-bottom: 2px solid var(---color-element-emphasis);
344
+ }
345
+
346
+ .hr {
347
+ background: var(---color-hr-light);
348
+ }
349
+
350
+ .far {
351
+ background: var(---color-far-light);
352
+ }
353
+
354
+ .acc {
355
+ background: var(---color-acc-light);
356
+ }
357
+
358
+ .ppv {
359
+ background: var(---color-present-light);
360
+ }
361
+
362
+ .fomr {
363
+ background: var(---color-absent-light);
364
+ }
365
+
366
+ /* Accuracy color scheme */
367
+ :host([color="accuracy"]) .h,
368
+ :host([color="accuracy"]) .cr {
369
+ background: var(---color-correct-light);
370
+ }
371
+
372
+ :host([color="accuracy"]) .m,
373
+ :host([color="accuracy"]) .fa {
374
+ color: var(---color-text-inverse);
375
+
376
+ background: var(---color-error-light);
377
+ }
378
+
379
+ :host([color="accuracy"]) .hr,
380
+ :host([color="accuracy"]) .far,
381
+ :host([color="accuracy"]) .ppv,
382
+ :host([color="accuracy"]) .fomr {
383
+ background: var(---color-element-background);
384
+ }
385
+
386
+ /* Stimulus color scheme */
387
+ :host([color="stimulus"]) .cr,
388
+ :host([color="stimulus"]) .fa {
389
+ background: var(---color-far-light);
390
+ }
391
+
392
+ :host([color="stimulus"]) .m,
393
+ :host([color="stimulus"]) .h {
394
+ background: var(---color-hr-light);
395
+ }
396
+
397
+ :host([color="stimulus"]) .ppv,
398
+ :host([color="stimulus"]) .fomr,
399
+ :host([color="stimulus"]) .acc {
400
+ background: var(---color-element-background);
401
+ }
402
+
403
+ /* Response color scheme */
404
+ :host([color="response"]) .cr,
405
+ :host([color="response"]) .m {
406
+ background: var(---color-absent-light);
407
+ }
408
+
409
+ :host([color="response"]) .fa,
410
+ :host([color="response"]) .h {
411
+ background: var(---color-present-light);
412
+ }
413
+
414
+ :host([color="response"]) .hr,
415
+ :host([color="response"]) .far,
416
+ :host([color="response"]) .acc {
417
+ background: var(---color-element-background);
418
+ }
419
+
420
+ /* No color scheme */
421
+ :host([color="none"]) .cr,
422
+ :host([color="none"]) .fa,
423
+ :host([color="none"]) .m,
424
+ :host([color="none"]) .h,
425
+ :host([color="none"]) .hr,
426
+ :host([color="none"]) .far,
427
+ :host([color="none"]) .ppv,
428
+ :host([color="none"]) .fomr,
429
+ :host([color="none"]) .acc {
430
+ background: var(---color-element-background);
431
+ }
432
+ `,
433
+ ];
434
+ }
435
+
436
+ render() {
437
+ const payoffFormatter = new Intl.NumberFormat('en-US', {
438
+ style: 'currency',
439
+ currency: 'USD',
440
+ minimumFractionDigits: 0,
441
+ maximumFractionDigits: 0,
442
+ });
443
+
444
+ this.alignState();
445
+ let h;
446
+ let m;
447
+ let fa;
448
+ let cr;
449
+ let hr;
450
+ let far;
451
+ let acc;
452
+ let ppv;
453
+ let fomr;
454
+ if (this.numeric) {
455
+ h = html`
456
+ <decidables-spinner ?disabled=${!this.interactive} min="0" .value="${this.h}" @input=${this.hInput.bind(this)}>
457
+ <span>Hits</span>
458
+ ${this.payoff ? html`<span class="payoff">${payoffFormatter.format(this.hPayoff)}</span>` : html``}
459
+ </decidables-spinner>
460
+ `;
461
+ m = html`
462
+ <decidables-spinner ?disabled=${!this.interactive} min="0" .value="${this.m}" @input=${this.mInput.bind(this)}>
463
+ <span>Misses</span>
464
+ ${this.payoff ? html`<span class="payoff">${payoffFormatter.format(this.mPayoff)}</span>` : html``}
465
+ </decidables-spinner>
466
+ `;
467
+ fa = html`
468
+ <decidables-spinner ?disabled=${!this.interactive} min="0" .value="${this.fa}" @input=${this.faInput.bind(this)}>
469
+ <span>False Alarms</span>
470
+ ${this.payoff ? html`<span class="payoff">${payoffFormatter.format(this.faPayoff)}</span>` : html``}
471
+ </decidables-spinner>
472
+ `;
473
+ cr = html`
474
+ <decidables-spinner ?disabled=${!this.interactive} min="0" .value="${this.cr}" @input=${this.crInput.bind(this)}>
475
+ <span>Correct Rejections</span>
476
+ ${this.payoff ? html`<span class="payoff">${payoffFormatter.format(this.crPayoff)}</span>` : html``}
477
+ </decidables-spinner>
478
+ `;
479
+ hr = html`
480
+ <decidables-spinner ?disabled=${!this.interactive} min="0" max="1" step=".001" .value="${+this.hr.toFixed(3)}" @input=${this.hrInput.bind(this)}>
481
+ <span>Hit Rate</span>
482
+ </decidables-spinner>
483
+ `;
484
+ far = html`
485
+ <decidables-spinner ?disabled=${!this.interactive} min="0" max="1" step=".001" .value="${+this.far.toFixed(3)}" @input=${this.farInput.bind(this)}>
486
+ <span>False Alarm Rate</span>
487
+ </decidables-spinner>
488
+ `;
489
+ acc = html`
490
+ <decidables-spinner ?disabled=${!this.interactive} min="0" max="1" step=".001" .value="${+this.acc.toFixed(3)}" @input=${this.accInput.bind(this)}>
491
+ <span>Accuracy</span>
492
+ </decidables-spinner>
493
+ `;
494
+ ppv = html`
495
+ <decidables-spinner ?disabled=${!this.interactive} min="0" max="1" step=".001" .value="${+this.ppv.toFixed(3)}" @input=${this.ppvInput.bind(this)}>
496
+ <span>Positive Predictive Value</span>
497
+ </decidables-spinner>
498
+ `;
499
+ fomr = html`
500
+ <decidables-spinner ?disabled=${!this.interactive} min="0" max="1" step=".001" .value="${+this.fomr.toFixed(3)}" @input=${this.fomrInput.bind(this)}>
501
+ <span>False Omission Rate</span>
502
+ </decidables-spinner>
503
+ `;
504
+ } else {
505
+ h = html`<span>Hits</span>
506
+ ${this.payoff ? html`<span class="payoff">${payoffFormatter.format(this.hPayoff)}</span>` : html``}`;
507
+ m = html`<span>Misses</span>
508
+ ${this.payoff ? html`<span class="payoff">${payoffFormatter.format(this.mPayoff)}</span>` : html``}`;
509
+ fa = html`<span>False Alarms</span>
510
+ ${this.payoff ? html`<span class="payoff">${payoffFormatter.format(this.faPayoff)}</span>` : html``}`;
511
+ cr = html`<span>Correct Rejections</span>
512
+ ${this.payoff ? html`<span class="payoff">${payoffFormatter.format(this.crPayoff)}</span>` : html``}`;
513
+ hr = html`<span>Hit Rate</span>`;
514
+ far = html`<span>False Alarm Rate</span>`;
515
+ acc = html`<span>Accuracy</span>`;
516
+ ppv = html`<span>Positive Predictive Value</span>`;
517
+ fomr = html`<span>False Omission Rate</span>`;
518
+ }
519
+ return html`
520
+ <table class=${this.numeric ? 'numeric' : ''}>
521
+ <thead>
522
+ <tr>
523
+ <th colspan="2" rowspan="2"></th>
524
+ <th class="th th-main" colspan="2" scope="col">
525
+ Response
526
+ </th>
527
+ </tr>
528
+ <tr>
529
+ <th class="th th-sub" scope="col">
530
+ "Present"
531
+ </th>
532
+ <th class="th th-sub" scope="col">
533
+ "Absent"
534
+ </th>
535
+ </tr>
536
+ </thead>
537
+ <tbody>
538
+ <tr>
539
+ <th class="th th-main" rowspan="2" scope="row">
540
+ Signal
541
+ </th>
542
+ <th class="th th-sub th-left" scope="row">
543
+ Present
544
+ </th>
545
+ <td class="td td-data h">
546
+ ${h}
547
+ </td>
548
+ <td class="td td-data m">
549
+ ${m}
550
+ </td>
551
+ ${(this.summary.has('stimulusRates'))
552
+ ? html`
553
+ <td class="td td-summary hr">
554
+ ${hr}
555
+ </td>`
556
+ : html``}
557
+ </tr>
558
+ <tr>
559
+ <th class="th th-sub th-left" scope="row">
560
+ Absent
561
+ </th>
562
+ <td class="td td-data fa">
563
+ ${fa}
564
+ </td>
565
+ <td class="td td-data cr">
566
+ ${cr}
567
+ </td>
568
+ ${(this.summary.has('stimulusRates'))
569
+ ? html`
570
+ <td class="td td-summary far">
571
+ ${far}
572
+ </td>`
573
+ : html``}
574
+ </tr>
575
+ ${(this.summary.has('responseRates') || this.summary.has('accuracy'))
576
+ ? html`
577
+ <tr>
578
+ <td colspan="2"></td>
579
+ ${(this.summary.has('responseRates'))
580
+ ? html`
581
+ <td class="td td-summary ppv">
582
+ ${ppv}
583
+ </td>
584
+ <td class="td td-summary fomr">
585
+ ${fomr}
586
+ </td>`
587
+ : html`
588
+ <td colspan="2"></td>`}
589
+ ${(this.summary.has('accuracy'))
590
+ ? html`
591
+ <td class="td td-summary acc" rowspan="2">
592
+ ${acc}
593
+ </td>`
594
+ : html``}
595
+ </tr>`
596
+ : html``}
597
+ </tbody>
598
+ </table>`;
599
+ }
600
+ }
601
+
602
+ customElements.define('detectable-table', DetectableTable);
@@ -0,0 +1,7 @@
1
+
2
+ export {default as RDKTask} from './rdk-task';
3
+ export {default as ROCSpace} from './roc-space';
4
+ export {default as DetectableControl} from './detectable-control';
5
+ export {default as SDTModel} from './sdt-model';
6
+ export {default as DetectableResponse} from './detectable-response';
7
+ export {default as DetectableTable} from './detectable-table';