@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,1220 @@
1
+
2
+ import {html, css} from 'lit';
3
+ import * as d3 from 'd3';
4
+
5
+ import SDTMath from '@decidables/detectable-math';
6
+
7
+ import DetectableElement from '../detectable-element';
8
+
9
+ /*
10
+ ROCSpace element
11
+ <roc-space>
12
+
13
+ Attributes:
14
+ FAR; HR;
15
+ d'; C; zFAR; zHR
16
+
17
+ draggable: yes/no
18
+
19
+ scale: FAR/HR; zFAR/zHR; d'/C
20
+ grid: FAR/HR; zFAR/zHR; d'/C
21
+ isos: d'; C; FAR; HR
22
+
23
+ Styles:
24
+ ??
25
+ */
26
+ export default class ROCSpace extends DetectableElement {
27
+ static get properties() {
28
+ return {
29
+ contour: {
30
+ attribute: 'contour',
31
+ type: String,
32
+ reflect: true,
33
+ },
34
+ point: {
35
+ attribute: 'point',
36
+ type: String,
37
+ reflect: true,
38
+ },
39
+ isoD: {
40
+ attribute: 'iso-d',
41
+ type: String,
42
+ reflect: true,
43
+ },
44
+ isoC: {
45
+ attribute: 'iso-c',
46
+ type: String,
47
+ reflect: true,
48
+ },
49
+
50
+ zRoc: {
51
+ attribute: 'z-roc',
52
+ type: Boolean,
53
+ reflect: true,
54
+ },
55
+
56
+ far: {
57
+ attribute: 'far',
58
+ type: Number,
59
+ reflect: true,
60
+ },
61
+ hr: {
62
+ attribute: 'hr',
63
+ type: Number,
64
+ reflect: true,
65
+ },
66
+
67
+ d: {
68
+ attribute: false,
69
+ type: Number,
70
+ reflect: false,
71
+ },
72
+ c: {
73
+ attribute: false,
74
+ type: Number,
75
+ reflect: false,
76
+ },
77
+ s: {
78
+ attribute: false,
79
+ type: Number,
80
+ reflect: false,
81
+ },
82
+
83
+ width: {
84
+ attribute: false,
85
+ type: Number,
86
+ reflect: false,
87
+ },
88
+ height: {
89
+ attribute: false,
90
+ type: Number,
91
+ reflect: false,
92
+ },
93
+ rem: {
94
+ attribute: false,
95
+ type: Number,
96
+ reflect: false,
97
+ },
98
+ };
99
+ }
100
+
101
+ constructor() {
102
+ super();
103
+
104
+ this.firstUpdate = true;
105
+ this.drag = false;
106
+ this.sdt = false;
107
+
108
+ this.contours = ['sensitivity', 'bias', 'accuracy'];
109
+ this.contour = undefined;
110
+ this.points = ['all', 'first', 'rest', 'none'];
111
+ this.point = 'all';
112
+ this.isoDs = ['all', 'first', 'rest', 'none'];
113
+ this.isoD = 'first';
114
+ this.isoCs = ['all', 'first', 'rest', 'none'];
115
+ this.isoC = 'first';
116
+
117
+ this.zRoc = false;
118
+
119
+ this.far = 0.25;
120
+ this.hr = 0.75;
121
+
122
+ this.s = 1;
123
+
124
+ this.label = '';
125
+
126
+ this.locations = [
127
+ {
128
+ name: 'default',
129
+ far: this.far,
130
+ hr: this.hr,
131
+ s: this.s,
132
+ label: '',
133
+ },
134
+ ];
135
+
136
+ this.pointArray = [];
137
+ this.isoDArray = [];
138
+ this.isoCArray = [];
139
+
140
+ this.width = NaN;
141
+ this.height = NaN;
142
+ this.rem = NaN;
143
+
144
+ this.alignState();
145
+ }
146
+
147
+ alignState() {
148
+ this.locations[0].hr = this.hr;
149
+ this.locations[0].far = this.far;
150
+ this.locations[0].s = this.s;
151
+ this.locations[0].label = this.label;
152
+
153
+ this.d = SDTMath.hrFar2D(this.hr, this.far, this.s);
154
+ this.c = SDTMath.hrFar2C(this.hr, this.far, this.s);
155
+
156
+ this.pointArray = [];
157
+ this.isoDArray = [];
158
+ this.isoCArray = [];
159
+ this.locations.forEach((item, index) => {
160
+ item.d = SDTMath.hrFar2D(item.hr, item.far, item.s);
161
+ item.c = SDTMath.hrFar2C(item.hr, item.far, item.s);
162
+
163
+ if ((index === 0) && (this.point === 'first' || this.point === 'all')) {
164
+ this.pointArray.push(item);
165
+ } else if ((index > 0) && (this.point === 'rest' || this.point === 'all')) {
166
+ this.pointArray.push(item);
167
+ }
168
+
169
+ if ((index === 0) && (this.isoD === 'first' || this.isoD === 'all')) {
170
+ this.isoDArray.push(item);
171
+ } else if ((index > 0) && (this.isoD === 'rest' || this.isoD === 'all')) {
172
+ this.isoDArray.push(item);
173
+ }
174
+
175
+ if ((index === 0) && (this.isoC === 'first' || this.isoC === 'all')) {
176
+ this.isoCArray.push(item);
177
+ } else if ((index > 0) && (this.isoC === 'rest' || this.isoC === 'all')) {
178
+ this.isoCArray.push(item);
179
+ }
180
+ });
181
+ }
182
+
183
+ set(hr, far, name = 'default', label = '', s = 1) {
184
+ if (name === 'default') {
185
+ this.hr = hr;
186
+ this.far = far;
187
+ this.s = s;
188
+ this.label = label;
189
+ }
190
+ const location = this.locations.find((item) => {
191
+ return (item.name === name);
192
+ });
193
+ if (location === undefined) {
194
+ this.locations.push({
195
+ name: name,
196
+ far: far,
197
+ hr: hr,
198
+ s: s,
199
+ label: label,
200
+ });
201
+ } else {
202
+ location.hr = hr;
203
+ location.far = far;
204
+ location.s = s;
205
+ location.label = label;
206
+ }
207
+
208
+ this.requestUpdate();
209
+ }
210
+
211
+ setWithSDT(d, c, name = 'default', label = '', s = 1) {
212
+ if (name === 'default') {
213
+ this.hr = SDTMath.dC2Hr(d, c, s);
214
+ this.far = SDTMath.dC2Far(d, c, s);
215
+ this.s = s;
216
+ this.label = label;
217
+ }
218
+ const location = this.locations.find((item) => {
219
+ return (item.name === name);
220
+ });
221
+ if (location === undefined) {
222
+ this.locations.push({
223
+ name: name,
224
+ far: SDTMath.dC2Far(d, c, s),
225
+ hr: SDTMath.dC2Hr(d, c, s),
226
+ s: s,
227
+ label: label,
228
+ });
229
+ } else {
230
+ location.hr = SDTMath.dC2Hr(d, c, s);
231
+ location.far = SDTMath.dC2Far(d, c, s);
232
+ location.s = s;
233
+ location.label = label;
234
+ }
235
+
236
+ this.sdt = true;
237
+ this.requestUpdate();
238
+ }
239
+
240
+ static get styles() {
241
+ return [
242
+ super.styles,
243
+ css`
244
+ :host {
245
+ display: inline-block;
246
+
247
+ width: 20rem;
248
+ height: 20rem;
249
+ }
250
+
251
+ .main {
252
+ width: 100%;
253
+ height: 100%;
254
+ }
255
+
256
+ .plot-contour,
257
+ .legend-contour .contour {
258
+ stroke: var(---color-background);
259
+ stroke-width: 0.5;
260
+ }
261
+
262
+ text {
263
+ /* stylelint-disable property-no-vendor-prefix */
264
+ -webkit-user-select: none;
265
+ -moz-user-select: none;
266
+ -ms-user-select: none;
267
+ user-select: none;
268
+ }
269
+
270
+ .point.interactive {
271
+ cursor: move;
272
+
273
+ filter: url("#shadow-2");
274
+ outline: none;
275
+
276
+ /* HACK: This gets Safari to correctly apply the filter! */
277
+ /* https://github.com/emilbjorklund/svg-weirdness/issues/27 */
278
+ stroke: #000000;
279
+ stroke-opacity: 0;
280
+ stroke-width: 0;
281
+ }
282
+
283
+ /* Make a larger target for touch users */
284
+ @media (pointer: coarse) {
285
+ .point.interactive .circle {
286
+ stroke: #000000;
287
+ stroke-opacity: 0;
288
+ stroke-width: 12px;
289
+ }
290
+ }
291
+
292
+ .point.interactive:hover {
293
+ filter: url("#shadow-4");
294
+
295
+ /* HACK: This gets Safari to correctly apply the filter! */
296
+ stroke: #ff0000;
297
+ }
298
+
299
+ .point.interactive:active {
300
+ filter: url("#shadow-8");
301
+
302
+ /* HACK: This gets Safari to correctly apply the filter! */
303
+ stroke: #00ff00;
304
+ }
305
+
306
+ :host(.keyboard) .point.interactive:focus {
307
+ filter: url("#shadow-8");
308
+
309
+ /* HACK: This gets Safari to correctly apply the filter! */
310
+ stroke: #0000ff;
311
+ }
312
+
313
+ .background {
314
+ fill: var(---color-element-background);
315
+ stroke: var(---color-element-border);
316
+ stroke-width: 1;
317
+ shape-rendering: crispEdges;
318
+ }
319
+
320
+ .title-x,
321
+ .title-y,
322
+ .title-contour {
323
+ font-weight: 600;
324
+
325
+ fill: currentColor;
326
+ }
327
+
328
+ .tick {
329
+ font-size: 0.75rem;
330
+ }
331
+
332
+ .axis-x path,
333
+ .axis-x line,
334
+ .axis-y path,
335
+ .axis-y line {
336
+ stroke: var(---color-element-border);
337
+ }
338
+
339
+ .axis-contour .domain {
340
+ stroke: none;
341
+ }
342
+
343
+ .diagonal {
344
+ stroke: var(---color-element-border);
345
+ stroke-dasharray: 4;
346
+ stroke-width: 1;
347
+ }
348
+
349
+ .curve-iso-d {
350
+ fill: none;
351
+ stroke: var(---color-d);
352
+ stroke-width: 2;
353
+ }
354
+
355
+ .curve-iso-c {
356
+ fill: none;
357
+ stroke: var(---color-c);
358
+ stroke-width: 2;
359
+ }
360
+
361
+ .point .circle {
362
+ fill: var(---color-element-emphasis);
363
+
364
+ /* r: 6; HACK: Firefox does not support CSS SVG Geometry Properties */
365
+ }
366
+
367
+ .point .label {
368
+ font-size: 0.75rem;
369
+
370
+ dominant-baseline: middle;
371
+ text-anchor: middle;
372
+
373
+ fill: var(---color-text-inverse);
374
+ }
375
+ `,
376
+ ];
377
+ }
378
+
379
+ render() { // eslint-disable-line class-methods-use-this
380
+ return html`
381
+ ${DetectableElement.svgFilters}
382
+ `;
383
+ }
384
+
385
+ getDimensions() {
386
+ this.width = parseFloat(this.getComputedStyleValue('width'), 10);
387
+ this.height = parseFloat(this.getComputedStyleValue('height'), 10);
388
+ this.rem = parseFloat(getComputedStyle(document.documentElement).getPropertyValue('font-size'), 10);
389
+ // console.log(`roc-space: width = ${this.width}, height = ${this.height}, rem = ${this.rem}`);
390
+ }
391
+
392
+ connectedCallback() {
393
+ super.connectedCallback();
394
+ window.addEventListener('resize', this.getDimensions.bind(this));
395
+ }
396
+
397
+ disconnectedCallback() {
398
+ window.removeEventListener('resize', this.getDimensions.bind(this));
399
+ super.disconnectedCallback();
400
+ }
401
+
402
+ firstUpdated(changedProperties) {
403
+ super.firstUpdated(changedProperties);
404
+
405
+ // Get the width and height after initial render/update has occurred
406
+ // HACK Edge: Edge doesn't have width/height until after a 0ms timeout
407
+ window.setTimeout(this.getDimensions.bind(this), 0);
408
+ }
409
+
410
+ update(changedProperties) {
411
+ super.update(changedProperties);
412
+
413
+ this.alignState();
414
+
415
+ // Bail out if we can't get the width/height/rem
416
+ if (Number.isNaN(this.width) || Number.isNaN(this.height) || Number.isNaN(this.rem)) {
417
+ return;
418
+ }
419
+
420
+ const elementWidth = this.width;
421
+ const elementHeight = this.height;
422
+ const elementSize = Math.min(elementWidth, elementHeight);
423
+
424
+ const margin = {
425
+ top: 2 * this.rem,
426
+ bottom: 3 * this.rem,
427
+ left: 3 * this.rem,
428
+ right: 2 * this.rem,
429
+ };
430
+ const height = elementSize - (margin.top + margin.bottom);
431
+ const width = elementSize - (margin.left + margin.right);
432
+
433
+ const transitionDuration = parseInt(this.getComputedStyleValue('---transition-duration'), 10);
434
+
435
+ // X Scale
436
+ const xScale = d3.scaleLinear()
437
+ .domain(this.zRoc ? [-3, 3] : [0, 1]) // zFAR or FAR
438
+ .range([0, width]);
439
+ this.xScale = xScale;
440
+
441
+ // Y Scale
442
+ const yScale = d3.scaleLinear()
443
+ .domain(this.zRoc ? [3, -3] : [1, 0]) // zHR or HR
444
+ .range([0, height]);
445
+ this.yScale = yScale;
446
+
447
+ // Drag behavior
448
+ const drag = d3.drag()
449
+ .subject((event, datum) => {
450
+ return {
451
+ x: this.xScale(this.zRoc ? SDTMath.far2Zfar(datum.far) : datum.far),
452
+ y: this.yScale(this.zRoc ? SDTMath.hr2Zhr(datum.hr) : datum.hr),
453
+ };
454
+ })
455
+ .on('start', (event) => {
456
+ const element = event.currentTarget;
457
+ d3.select(element).classed('dragging', true);
458
+ })
459
+ .on('drag', (event, datum) => {
460
+ this.drag = true;
461
+ const far = this.zRoc
462
+ ? SDTMath.zfar2Far(this.xScale.invert(event.x))
463
+ : this.xScale.invert(event.x);
464
+ const hr = this.zRoc
465
+ ? SDTMath.zhr2Hr(this.yScale.invert(event.y))
466
+ : this.yScale.invert(event.y);
467
+ // Clamp FAR and HR to ROC Space
468
+ datum.far = (far < 0.001)
469
+ ? 0.001
470
+ : ((far > 0.999)
471
+ ? 0.999
472
+ : far);
473
+ datum.hr = (hr <= 0.001)
474
+ ? 0.001
475
+ : (hr >= 0.999)
476
+ ? 0.999
477
+ : hr;
478
+ // console.log(`roc-space.drag: far = ${datum.far}, hr = ${datum.hr}`);
479
+ if (datum.name === 'default') {
480
+ this.far = datum.far;
481
+ this.hr = datum.hr;
482
+ }
483
+ this.alignState();
484
+ this.requestUpdate();
485
+ this.dispatchEvent(new CustomEvent('roc-point-change', {
486
+ detail: {
487
+ name: datum.name,
488
+ far: datum.far,
489
+ hr: datum.hr,
490
+ d: datum.d,
491
+ c: datum.c,
492
+ s: datum.s,
493
+ label: datum.label,
494
+ },
495
+ bubbles: true,
496
+ }));
497
+ })
498
+ .on('end', (event) => {
499
+ const element = event.currentTarget;
500
+ d3.select(element).classed('dragging', false);
501
+ });
502
+
503
+ // Line for FAR/HR Space
504
+ const line = d3.line()
505
+ .x((datum) => { return xScale(this.zRoc ? SDTMath.far2Zfar(datum.far) : datum.far); })
506
+ .y((datum) => { return yScale(this.zRoc ? SDTMath.hr2Zhr(datum.hr) : datum.hr); });
507
+
508
+ // Svg
509
+ // DATA-JOIN
510
+ const svgUpdate = d3.select(this.renderRoot).selectAll('.main')
511
+ .data([{
512
+ width: this.width,
513
+ height: this.height,
514
+ rem: this.rem,
515
+ }]);
516
+ // ENTER
517
+ const svgEnter = svgUpdate.enter().append('svg')
518
+ .classed('main', true);
519
+ // MERGE
520
+ const svgMerge = svgEnter.merge(svgUpdate)
521
+ .attr('viewBox', `0 0 ${elementSize} ${elementSize}`);
522
+
523
+ // Plot
524
+ // ENTER
525
+ const plotEnter = svgEnter.append('g')
526
+ .classed('plot', true);
527
+ // MERGE
528
+ const plotMerge = svgMerge.select('.plot')
529
+ .attr('transform', `translate(${margin.left}, ${margin.top})`);
530
+
531
+ // Clippath
532
+ // ENTER
533
+ plotEnter.append('clipPath')
534
+ .attr('id', 'clip-roc-space')
535
+ .append('rect');
536
+ // MERGE
537
+ plotMerge.select('clipPath rect')
538
+ .attr('height', height + 1)
539
+ .attr('width', width + 1);
540
+
541
+ // Underlayer
542
+ // ENTER
543
+ const underlayerEnter = plotEnter.append('g')
544
+ .classed('underlayer', true);
545
+ // MERGE
546
+ const underlayerMerge = plotMerge.select('.underlayer');
547
+
548
+ // Background
549
+ // ENTER
550
+ underlayerEnter.append('rect')
551
+ .classed('background', true);
552
+ // MERGE
553
+ underlayerMerge.select('.background')
554
+ .attr('height', height)
555
+ .attr('width', width);
556
+
557
+ // Contour Plotting
558
+ // Handles: Bias, Sensitivity, & Accuracy
559
+ if (
560
+ this.firstUpdate
561
+ || changedProperties.has('contour')
562
+ || changedProperties.has('zRoc')
563
+ || changedProperties.has('width')
564
+ || changedProperties.has('height')
565
+ || changedProperties.has('rem')
566
+ || changedProperties.has('s')
567
+ ) {
568
+ if (this.contour !== undefined) {
569
+ // Contour Plot
570
+ const n = 100; // Resolution
571
+ const contourValues = [];
572
+ for (let j = 0.5, k = 0; j < n; j += 1) {
573
+ for (let i = 0.5; i < n; i += 1, k += 1) {
574
+ const hr = this.zRoc
575
+ ? SDTMath.zhr2Hr(((i / n) * 6) - 3)
576
+ : i / n;
577
+ const far = this.zRoc
578
+ ? SDTMath.zfar2Far(((1 - j / n) * 6) - 3)
579
+ : (1 - j / n);
580
+ contourValues[k] = (this.contour === 'bias')
581
+ ? SDTMath.hrFar2C(hr, far, this.s)
582
+ : (this.contour === 'sensitivity')
583
+ ? SDTMath.hrFar2D(hr, far, this.s)
584
+ : (this.contour === 'accuracy')
585
+ ? SDTMath.hrFar2Acc(hr, far)
586
+ : null;
587
+ }
588
+ }
589
+ const contourThresholds = (this.contour === 'bias')
590
+ ? d3.range(-3, 3, 0.25)
591
+ : (this.contour === 'sensitivity')
592
+ ? d3.range(-6, 6, 0.5)
593
+ : (this.contour === 'accuracy')
594
+ ? d3.range(0, 1, 0.05)
595
+ : null;
596
+ const contours = d3.contours()
597
+ .size([n, n])
598
+ .thresholds(contourThresholds);
599
+ const contourColorStart = this.getComputedStyleValue((this.contour === 'bias')
600
+ ? '---color-element-background'
601
+ : (this.contour === 'sensitivity')
602
+ ? '---color-d'
603
+ : (this.contour === 'accuracy')
604
+ ? '---color-acc-dark'
605
+ : null);
606
+ const contourColorEnd = this.getComputedStyleValue((this.contour === 'bias')
607
+ ? '---color-c'
608
+ : (this.contour === 'sensitivity')
609
+ ? '---color-element-background'
610
+ : (this.contour === 'accuracy')
611
+ ? '---color-element-background'
612
+ : null);
613
+ const contourColor = d3.scaleLinear()
614
+ .domain(d3.extent(contourThresholds))
615
+ .interpolate(() => { return d3.interpolateRgb(contourColorStart, contourColorEnd); });
616
+ // DATA-JOIN
617
+ const contourPlotUpdate = underlayerMerge.selectAll('.plot-contour')
618
+ .data([this.contour]);
619
+ // ENTER
620
+ const contourPlotEnter = contourPlotUpdate.enter().append('g')
621
+ .classed('plot-contour', true);
622
+ // MERGE
623
+ const contourPlotMerge = contourPlotEnter.merge(contourPlotUpdate);
624
+
625
+ // Contour Plot Contours
626
+ // DATA-JOIN
627
+ const contoursUpdate = contourPlotMerge.selectAll('.contour')
628
+ .data(contours(contourValues));
629
+ // ENTER
630
+ const contoursEnter = contoursUpdate.enter().append('path')
631
+ .classed('contour', true);
632
+ // MERGE
633
+ contoursEnter.merge(contoursUpdate).transition()
634
+ .duration(transitionDuration * 2) // Extra long transition!
635
+ .ease(d3.easeCubicOut)
636
+ .attr('d', d3.geoPath(d3.geoIdentity().scale(width / n))) // ????
637
+ .attr('fill', (datum) => { return contourColor(datum.value); });
638
+ // EXIT
639
+ contoursUpdate.exit().remove();
640
+
641
+ // Contour Title
642
+ // DATA-JOIN
643
+ const contourTitleUpdate = underlayerMerge.selectAll('.title-contour')
644
+ .data([this.contour]);
645
+ // ENTER
646
+ const contourTitleEnter = contourTitleUpdate.enter().append('text')
647
+ .classed('title-contour', true)
648
+ .attr('text-anchor', 'middle');
649
+ // MERGE
650
+ contourTitleEnter.merge(contourTitleUpdate)
651
+ .classed('math-var', (this.contour === 'bias') || (this.contour === 'sensitivity'))
652
+ .attr('transform', (this.contour === 'bias')
653
+ ? `translate(${(width + (1.25 * this.rem))}, ${this.rem})`
654
+ : (this.contour === 'sensitivity')
655
+ ? `translate(${(width + (1.25 * this.rem))}, ${this.rem})`
656
+ : (this.contour === 'accuracy')
657
+ ? `translate(${(width + (1.125 * this.rem))}, ${this.rem})`
658
+ : null)
659
+ .text((this.contour === 'bias')
660
+ ? 'c'
661
+ : (this.contour === 'sensitivity')
662
+ ? 'd′'
663
+ : (this.contour === 'accuracy')
664
+ ? 'Acc'
665
+ : null);
666
+
667
+ // Contour Legend
668
+ const l = 100;
669
+ const contourLegendValues = []; // new Array(4 * l);
670
+ for (let i = 0.5, k = 0; i < l; i += 1, k += 4) {
671
+ contourLegendValues[k] = (this.contour === 'bias')
672
+ ? -(((i / n) * 6) - 3)
673
+ : (this.contour === 'sensitivity')
674
+ ? ((i / n) * 12) - 6
675
+ : (this.contour === 'accuracy')
676
+ ? (i / n)
677
+ : null;
678
+ contourLegendValues[k + 1] = contourLegendValues[k];
679
+ contourLegendValues[k + 2] = contourLegendValues[k];
680
+ contourLegendValues[k + 3] = contourLegendValues[k];
681
+ }
682
+ const legendContours = d3.contours()
683
+ .size([4, l])
684
+ .thresholds(contourThresholds);
685
+ const legendScale = d3.scaleLinear()
686
+ .domain((this.contour === 'bias')
687
+ ? [3, -3]
688
+ : (this.contour === 'sensitivity')
689
+ ? [6, -6]
690
+ : (this.contour === 'accuracy')
691
+ ? [1, 0]
692
+ : null)
693
+ .range([0, (10 * this.rem)]);
694
+ // DATA-JOIN
695
+ const contourLegendUpdate = underlayerMerge.selectAll('.legend-contour')
696
+ .data([this.contour]);
697
+ // ENTER
698
+ const contourLegendEnter = contourLegendUpdate.enter().append('g')
699
+ .classed('legend-contour', true);
700
+ // MERGE
701
+ const contourLegendMerge = contourLegendEnter.merge(contourLegendUpdate)
702
+ .attr('transform', (this.contour === 'bias')
703
+ ? `translate(${(width + (1.25 * this.rem))}, ${(1.5 * this.rem)})`
704
+ : (this.contour === 'sensitivity')
705
+ ? `translate(${(width + (1.25 * this.rem))}, ${(1.5 * this.rem)})`
706
+ : (this.contour === 'accuracy')
707
+ ? `translate(${(width + (1.5 * this.rem))}, ${(1.5 * this.rem)})`
708
+ : null);
709
+ // EXIT
710
+ contourLegendUpdate.exit().remove();
711
+
712
+ // Contour Legend Axis
713
+ // ENTER
714
+ contourLegendEnter.append('g')
715
+ .classed('axis-contour', true);
716
+ // MERGE
717
+ contourLegendMerge.select('.axis-contour')
718
+ .call(d3.axisLeft(legendScale).ticks(7).tickSize(0))
719
+ .attr('font-size', null)
720
+ .attr('font-family', null);
721
+
722
+ // Contour Legend Contours
723
+ // DATA-JOIN
724
+ const legendContoursUpdate = contourLegendMerge.selectAll('.contour')
725
+ .data(legendContours(contourLegendValues));
726
+ // ENTER
727
+ const legendContoursEnter = legendContoursUpdate.enter().append('path')
728
+ .classed('contour', true);
729
+ // MERGE
730
+ legendContoursEnter.merge(legendContoursUpdate)
731
+ .attr('d', d3.geoPath(d3.geoIdentity().scale((10 * this.rem) / l))) // ????
732
+ .attr('fill', (datum) => { return contourColor(datum.value); });
733
+ // EXIT
734
+ legendContoursUpdate.exit().remove();
735
+ } else {
736
+ // Contour Plot
737
+ // DATA-JOIN
738
+ const contourPlotUpdate = underlayerMerge.selectAll('.plot-contour')
739
+ .data([]);
740
+ // EXIT
741
+ contourPlotUpdate.exit().remove();
742
+
743
+ // Contour Title
744
+ // DATA-JOIN
745
+ const contourTitleUpdate = underlayerMerge.selectAll('.title-contour')
746
+ .data([]);
747
+ // EXIT
748
+ contourTitleUpdate.exit().remove();
749
+
750
+ // Contour Legend
751
+ // DATA-JOIN
752
+ const contourLegendUpdate = underlayerMerge.selectAll('.legend-contour')
753
+ .data([]);
754
+ // EXIT
755
+ contourLegendUpdate.exit().remove();
756
+ }
757
+ }
758
+
759
+ // X Axis
760
+ // ENTER
761
+ underlayerEnter.append('g')
762
+ .classed('axis-x', true);
763
+ // MERGE
764
+ const axisXMerge = underlayerMerge.select('.axis-x')
765
+ .attr('transform', `translate(0, ${height})`);
766
+ const axisXTransition = axisXMerge.transition()
767
+ .duration(transitionDuration * 2) // Extra long transition!
768
+ .ease(d3.easeCubicOut)
769
+ .call(d3.axisBottom(xScale))
770
+ .attr('font-size', null)
771
+ .attr('font-family', null);
772
+ axisXTransition.selectAll('line, path')
773
+ .attr('stroke', null);
774
+
775
+ // X Axis Title
776
+ // ENTER
777
+ const titleXEnter = underlayerEnter.append('text')
778
+ .classed('title-x', true)
779
+ .attr('text-anchor', 'middle');
780
+ titleXEnter.append('tspan')
781
+ .classed('z math-var', true);
782
+ titleXEnter.append('tspan')
783
+ .classed('name', true);
784
+ // MERGE
785
+ const titleXMerge = underlayerMerge.select('.title-x')
786
+ .attr('transform', `translate(${(width / 2)}, ${(height + (2.25 * this.rem))})`);
787
+ titleXMerge.select('tspan.z')
788
+ .text(this.zRoc ? 'z' : '');
789
+ titleXMerge.select('tspan.name')
790
+ .text(this.zRoc ? '(False Alarm Rate)' : 'False Alarm Rate');
791
+
792
+ // Y Axis
793
+ // ENTER
794
+ underlayerEnter.append('g')
795
+ .classed('axis-y', true);
796
+ // MERGE
797
+ const axisYTransition = underlayerMerge.select('.axis-y').transition()
798
+ .duration(transitionDuration * 2) // Extra long transition!
799
+ .ease(d3.easeCubicOut)
800
+ .call(d3.axisLeft(yScale))
801
+ .attr('font-size', null)
802
+ .attr('font-family', null);
803
+ axisYTransition.selectAll('line, path')
804
+ .attr('stroke', null);
805
+
806
+ // Y Axis Title
807
+ // ENTER
808
+ const titleYEnter = underlayerEnter.append('text')
809
+ .classed('title-y', true)
810
+ .attr('text-anchor', 'middle');
811
+ titleYEnter.append('tspan')
812
+ .classed('z math-var', true);
813
+ titleYEnter.append('tspan')
814
+ .classed('name', true);
815
+ // MERGE
816
+ const titleYMerge = underlayerMerge.select('.title-y')
817
+ .attr('transform', `translate(${-2 * this.rem}, ${(height / 2)})rotate(-90)`);
818
+ titleYMerge.select('tspan.z')
819
+ .text(this.zRoc ? 'z' : '');
820
+ titleYMerge.select('tspan.name')
821
+ .text(this.zRoc ? '(Hit Rate)' : 'Hit Rate');
822
+
823
+ // No-Information Line
824
+ // ENTER
825
+ underlayerEnter.append('line')
826
+ .classed('diagonal', true);
827
+ // MERGE
828
+ underlayerMerge.select('.diagonal')
829
+ .attr('x1', this.zRoc ? xScale(-3) : xScale(0))
830
+ .attr('y1', this.zRoc ? yScale(-3) : yScale(0))
831
+ .attr('x2', this.zRoc ? xScale(3) : xScale(1))
832
+ .attr('y2', this.zRoc ? yScale(3) : yScale(1));
833
+
834
+ // Content
835
+ // ENTER
836
+ plotEnter.append('g')
837
+ .classed('content', true);
838
+ // MERGE
839
+ const contentMerge = plotMerge.select('.content');
840
+
841
+ // Iso-sensitivity Curve
842
+ // DATA-JOIN
843
+ const isoDUpdate = contentMerge.selectAll('.curve-iso-d')
844
+ .data(this.isoDArray, (datum) => { return datum.name; });
845
+ // ENTER
846
+ const isoDEnter = isoDUpdate.enter().append('path')
847
+ .classed('curve-iso-d', true)
848
+ .attr('clip-path', 'url(#clip-roc-space)');
849
+ // MERGE
850
+ const isoDMerge = isoDEnter.merge(isoDUpdate);
851
+ if (this.firstUpdate || changedProperties.has('zRoc')) {
852
+ isoDMerge.transition()
853
+ .duration(this.drag ? 0 : (transitionDuration * 2)) // Extra long transition!
854
+ .ease(d3.easeCubicOut)
855
+ .attr('d', (datum) => {
856
+ return line(d3.range(xScale.range()[0], xScale.range()[1] + 1, 1).map((x) => {
857
+ return {
858
+ far: (this.zRoc
859
+ ? SDTMath.zfar2Far(xScale.invert(x))
860
+ : xScale.invert(x)),
861
+ hr: (this.zRoc
862
+ ? SDTMath.dFar2Hr(datum.d, SDTMath.zfar2Far(xScale.invert(x)), datum.s)
863
+ : SDTMath.dFar2Hr(datum.d, xScale.invert(x), datum.s)),
864
+ };
865
+ }));
866
+ });
867
+ } else if (this.sdt) {
868
+ isoDMerge.transition()
869
+ .duration(this.drag ? 0 : transitionDuration)
870
+ .ease(d3.easeCubicOut)
871
+ .attrTween('d', (datum, index, elements) => {
872
+ const element = elements[index];
873
+ element.hr = undefined;
874
+ element.far = undefined;
875
+ const interpolateD = d3.interpolate(
876
+ (element.d !== undefined) ? element.d : datum.d,
877
+ datum.d,
878
+ );
879
+ const interpolateS = d3.interpolate(
880
+ (element.s !== undefined) ? element.s : datum.s,
881
+ datum.s,
882
+ );
883
+ return (time) => {
884
+ element.d = interpolateD(time);
885
+ element.s = interpolateS(time);
886
+ const isoD = d3.range(xScale.range()[0], xScale.range()[1] + 1, 1).map((x) => {
887
+ return {
888
+ far: (this.zRoc
889
+ ? SDTMath.zfar2Far(xScale.invert(x))
890
+ : xScale.invert(x)),
891
+ hr: (this.zRoc
892
+ ? SDTMath.dFar2Hr(element.d, SDTMath.zfar2Far(xScale.invert(x)), element.s)
893
+ : SDTMath.dFar2Hr(element.d, xScale.invert(x), element.s)),
894
+ };
895
+ });
896
+ return line(isoD);
897
+ };
898
+ });
899
+ } else {
900
+ isoDMerge.transition()
901
+ .duration(this.drag ? 0 : transitionDuration)
902
+ .ease(d3.easeCubicOut)
903
+ .attrTween('d', (datum, index, elements) => {
904
+ const element = elements[index];
905
+ element.d = undefined;
906
+ element.s = undefined;
907
+ const interpolateHr = d3.interpolate(
908
+ (element.hr !== undefined) ? element.hr : datum.hr,
909
+ datum.hr,
910
+ );
911
+ const interpolateFar = d3.interpolate(
912
+ (element.far !== undefined) ? element.far : datum.far,
913
+ datum.far,
914
+ );
915
+ return (time) => {
916
+ element.hr = interpolateHr(time);
917
+ element.far = interpolateFar(time);
918
+ const isoD = d3.range(xScale.range()[0], xScale.range()[1] + 1, 1).map((x) => {
919
+ return {
920
+ far: (this.zRoc
921
+ ? SDTMath.zfar2Far(xScale.invert(x))
922
+ : xScale.invert(x)),
923
+ hr: (this.zRoc
924
+ ? SDTMath.dFar2Hr(
925
+ SDTMath.hrFar2D(element.hr, element.far, datum.s),
926
+ SDTMath.zfar2Far(xScale.invert(x)),
927
+ datum.s,
928
+ )
929
+ : SDTMath.dFar2Hr(
930
+ SDTMath.hrFar2D(element.hr, element.far, datum.s),
931
+ xScale.invert(x),
932
+ datum.s,
933
+ )
934
+ ),
935
+ };
936
+ });
937
+ return line(isoD);
938
+ };
939
+ });
940
+ }
941
+ // EXIT
942
+ // NOTE: Could add a transition here
943
+ isoDUpdate.exit().remove();
944
+
945
+ // Iso-bias Curve
946
+ // DATA-JOIN
947
+ const isoCUpdate = contentMerge.selectAll('.curve-iso-c')
948
+ .data(this.isoCArray, (datum) => { return datum.name; });
949
+ // ENTER
950
+ const isoCEnter = isoCUpdate.enter().append('path')
951
+ .classed('curve-iso-c', true)
952
+ .attr('clip-path', 'url(#clip-roc-space)');
953
+ // MERGE
954
+ const isoCMerge = isoCEnter.merge(isoCUpdate);
955
+ if (this.firstUpdate || changedProperties.has('zRoc')) {
956
+ isoCMerge.transition()
957
+ .duration(this.drag ? 0 : (transitionDuration * 2)) // Extra long transition!
958
+ .ease(d3.easeCubicOut)
959
+ .attr('d', (datum) => {
960
+ return line(d3.range(xScale.range()[0], xScale.range()[1] + 1, 1).map((x) => {
961
+ return {
962
+ far: (this.zRoc
963
+ ? SDTMath.zfar2Far(xScale.invert(x))
964
+ : xScale.invert(x)),
965
+ hr: (this.zRoc
966
+ ? SDTMath.cFar2Hr(datum.c, SDTMath.zfar2Far(xScale.invert(x)), datum.s)
967
+ : SDTMath.cFar2Hr(datum.c, xScale.invert(x), datum.s)),
968
+ };
969
+ }));
970
+ });
971
+ } else if (this.sdt) {
972
+ isoCMerge.transition()
973
+ .duration(this.drag ? 0 : transitionDuration)
974
+ .ease(d3.easeCubicOut)
975
+ .attrTween('d', (datum, index, elements) => {
976
+ const element = elements[index];
977
+ element.hr = undefined;
978
+ element.far = undefined;
979
+ const interpolateC = d3.interpolate(
980
+ (element.c !== undefined) ? element.c : datum.c,
981
+ datum.c,
982
+ );
983
+ const interpolateS = d3.interpolate(
984
+ (element.s !== undefined) ? element.s : datum.s,
985
+ datum.s,
986
+ );
987
+ return (time) => {
988
+ element.c = interpolateC(time);
989
+ element.s = interpolateS(time);
990
+ const isoC = d3.range(xScale.range()[0], xScale.range()[1] + 1, 1).map((x) => {
991
+ return {
992
+ far: (this.zRoc
993
+ ? SDTMath.zfar2Far(xScale.invert(x))
994
+ : xScale.invert(x)),
995
+ hr: (this.zRoc
996
+ ? SDTMath.cFar2Hr(element.c, SDTMath.zfar2Far(xScale.invert(x)), element.s)
997
+ : SDTMath.cFar2Hr(element.c, xScale.invert(x), element.s)),
998
+ };
999
+ });
1000
+ return line(isoC);
1001
+ };
1002
+ });
1003
+ } else {
1004
+ isoCMerge.transition()
1005
+ .duration(this.drag ? 0 : transitionDuration)
1006
+ .ease(d3.easeCubicOut)
1007
+ .attrTween('d', (datum, index, elements) => {
1008
+ const element = elements[index];
1009
+ element.c = undefined;
1010
+ element.s = undefined;
1011
+ const interpolateHr = d3.interpolate(
1012
+ (element.hr !== undefined) ? element.hr : datum.hr,
1013
+ datum.hr,
1014
+ );
1015
+ const interpolateFar = d3.interpolate(
1016
+ (element.far !== undefined) ? element.far : datum.far,
1017
+ datum.far,
1018
+ );
1019
+ return (time) => {
1020
+ element.hr = interpolateHr(time);
1021
+ element.far = interpolateFar(time);
1022
+ const isoC = d3.range(xScale.range()[0], xScale.range()[1] + 1, 1).map((x) => {
1023
+ return {
1024
+ far: (this.zRoc
1025
+ ? SDTMath.zfar2Far(xScale.invert(x))
1026
+ : xScale.invert(x)),
1027
+ hr: (this.zRoc
1028
+ ? SDTMath.cFar2Hr(
1029
+ SDTMath.hrFar2C(element.hr, element.far, datum.s),
1030
+ SDTMath.zfar2Far(xScale.invert(x)),
1031
+ datum.s,
1032
+ )
1033
+ : SDTMath.cFar2Hr(
1034
+ SDTMath.hrFar2C(element.hr, element.far, datum.s),
1035
+ xScale.invert(x),
1036
+ datum.s,
1037
+ )
1038
+ ),
1039
+ };
1040
+ });
1041
+ return line(isoC);
1042
+ };
1043
+ });
1044
+ }
1045
+ // EXIT
1046
+ // NOTE: Could add a transition here
1047
+ isoCUpdate.exit().remove();
1048
+
1049
+ // Point
1050
+ // DATA-JOIN
1051
+ const pointUpdate = contentMerge.selectAll('.point')
1052
+ .data(this.pointArray, (datum) => { return datum.name; });
1053
+ // ENTER
1054
+ const pointEnter = pointUpdate.enter().append('g')
1055
+ .classed('point', true);
1056
+ pointEnter.append('circle')
1057
+ .classed('circle', true)
1058
+ .attr('r', 6); /* HACK: Firefox does not support CSS SVG Geometry Properties */
1059
+ pointEnter.append('text')
1060
+ .classed('label', true);
1061
+ // MERGE
1062
+ const pointMerge = pointEnter.merge(pointUpdate);
1063
+ pointMerge.select('text')
1064
+ .text((datum) => { return datum.label; });
1065
+ if (this.firstUpdate || changedProperties.has('interactive')) {
1066
+ if (this.interactive) {
1067
+ pointMerge
1068
+ .attr('tabindex', 0)
1069
+ .classed('interactive', true)
1070
+ .call(drag)
1071
+ .on('keydown', (event, datum) => {
1072
+ if (['ArrowUp', 'ArrowDown', 'ArrowRight', 'ArrowLeft'].includes(event.key)) {
1073
+ let hr = this.zRoc ? SDTMath.hr2Zhr(datum.hr) : datum.hr;
1074
+ let far = this.zRoc ? SDTMath.far2Zfar(datum.far) : datum.far;
1075
+ switch (event.key) {
1076
+ case 'ArrowUp':
1077
+ hr += this.zRoc
1078
+ ? (event.shiftKey ? 0.05 : 0.25)
1079
+ : (event.shiftKey ? 0.01 : 0.05);
1080
+ break;
1081
+ case 'ArrowDown':
1082
+ hr -= this.zRoc
1083
+ ? (event.shiftKey ? 0.05 : 0.25)
1084
+ : (event.shiftKey ? 0.01 : 0.05);
1085
+ break;
1086
+ case 'ArrowRight':
1087
+ far += this.zRoc
1088
+ ? (event.shiftKey ? 0.05 : 0.25)
1089
+ : (event.shiftKey ? 0.01 : 0.05);
1090
+ break;
1091
+ case 'ArrowLeft':
1092
+ far -= this.zRoc
1093
+ ? (event.shiftKey ? 0.05 : 0.25)
1094
+ : (event.shiftKey ? 0.01 : 0.05);
1095
+ break;
1096
+ default:
1097
+ // no-op
1098
+ }
1099
+ hr = this.zRoc ? SDTMath.zhr2Hr(hr) : hr;
1100
+ far = this.zRoc ? SDTMath.zfar2Far(far) : far;
1101
+ // Clamp FAR and HR to ROC Space
1102
+ hr = (hr < 0.001)
1103
+ ? 0.001
1104
+ : (hr > 0.999)
1105
+ ? 0.999
1106
+ : hr;
1107
+ far = (far < 0.001)
1108
+ ? 0.001
1109
+ : (far > 0.999)
1110
+ ? 0.999
1111
+ : far;
1112
+ if ((hr !== datum.hr) || (far !== datum.far)) {
1113
+ datum.hr = hr;
1114
+ datum.far = far;
1115
+ if (datum.name === 'default') {
1116
+ this.hr = datum.hr;
1117
+ this.far = datum.far;
1118
+ }
1119
+ this.alignState();
1120
+ this.requestUpdate();
1121
+ this.dispatchEvent(new CustomEvent('roc-point-change', {
1122
+ detail: {
1123
+ name: datum.name,
1124
+ far: datum.far,
1125
+ hr: datum.hr,
1126
+ d: datum.d,
1127
+ c: datum.c,
1128
+ s: datum.s,
1129
+ label: datum.label,
1130
+ },
1131
+ bubbles: true,
1132
+ }));
1133
+ }
1134
+ event.preventDefault();
1135
+ }
1136
+ });
1137
+ } else {
1138
+ pointMerge
1139
+ .attr('tabindex', null)
1140
+ .classed('interactive', false)
1141
+ .on('drag', null)
1142
+ .on('keydown', null);
1143
+ }
1144
+ }
1145
+ if (this.firstUpdate || changedProperties.has('zRoc')) {
1146
+ pointMerge.transition()
1147
+ .duration(this.drag ? 0 : (transitionDuration * 2)) // Extra long transition!
1148
+ .ease(d3.easeCubicOut)
1149
+ .attr('transform', (datum, index, elements) => {
1150
+ const element = elements[index];
1151
+ element.d = undefined;
1152
+ element.c = undefined;
1153
+ element.s = undefined;
1154
+ return `translate(
1155
+ ${xScale(this.zRoc ? SDTMath.far2Zfar(datum.far) : datum.far)},
1156
+ ${yScale(this.zRoc ? SDTMath.hr2Zhr(datum.hr) : datum.hr)}
1157
+ )`;
1158
+ });
1159
+ } else if (this.sdt) {
1160
+ pointMerge.transition()
1161
+ .duration(this.drag ? 0 : transitionDuration)
1162
+ .ease(d3.easeCubicOut)
1163
+ .attrTween('transform', (datum, index, elements) => {
1164
+ const element = elements[index];
1165
+ const interpolateD = d3.interpolate(
1166
+ (element.d !== undefined) ? element.d : datum.d,
1167
+ datum.d,
1168
+ );
1169
+ const interpolateC = d3.interpolate(
1170
+ (element.c !== undefined) ? element.c : datum.c,
1171
+ datum.c,
1172
+ );
1173
+ const interpolateS = d3.interpolate(
1174
+ (element.s !== undefined) ? element.s : datum.s,
1175
+ datum.s,
1176
+ );
1177
+ return (time) => {
1178
+ element.d = interpolateD(time);
1179
+ element.c = interpolateC(time);
1180
+ element.s = interpolateS(time);
1181
+ return `translate(
1182
+ ${xScale(
1183
+ this.zRoc
1184
+ ? SDTMath.far2Zfar(SDTMath.dC2Far(element.d, element.c, element.s))
1185
+ : SDTMath.dC2Far(element.d, element.c, element.s),
1186
+ )},
1187
+ ${yScale(
1188
+ this.zRoc
1189
+ ? SDTMath.hr2Zhr(SDTMath.dC2Hr(element.d, element.c, element.s))
1190
+ : SDTMath.dC2Hr(element.d, element.c, element.s),
1191
+ )}
1192
+ )`;
1193
+ };
1194
+ });
1195
+ } else {
1196
+ pointMerge.transition()
1197
+ .duration(this.drag ? 0 : transitionDuration)
1198
+ .ease(d3.easeCubicOut)
1199
+ .attr('transform', (datum, index, elements) => {
1200
+ const element = elements[index];
1201
+ element.d = undefined;
1202
+ element.c = undefined;
1203
+ element.s = undefined;
1204
+ return `translate(
1205
+ ${xScale(this.zRoc ? SDTMath.far2Zfar(datum.far) : datum.far)},
1206
+ ${yScale(this.zRoc ? SDTMath.hr2Zhr(datum.hr) : datum.hr)}
1207
+ )`;
1208
+ });
1209
+ }
1210
+ // EXIT
1211
+ // NOTE: Could add a transition here
1212
+ pointUpdate.exit().remove();
1213
+
1214
+ this.drag = false;
1215
+ this.sdt = false;
1216
+ this.firstUpdate = false;
1217
+ }
1218
+ }
1219
+
1220
+ customElements.define('roc-space', ROCSpace);