@decidables/discountable-elements 0.5.1 → 0.6.1

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.
@@ -249,54 +249,91 @@ export default class HTDCurves extends DecidablesMixinResizeable(DiscountableEle
249
249
  /* shape-rendering: crispEdges; */
250
250
  }
251
251
 
252
- .curve {
253
- fill: none;
254
- stroke: var(---color-element-emphasis);
255
- stroke-width: 2;
256
- }
257
-
258
- .curve.interactive {
259
- cursor: nwse-resize;
260
-
252
+ .option .interactive {
261
253
  filter: url("#shadow-2");
262
254
  outline: none;
263
255
  }
264
256
 
265
- .curve.interactive:hover {
257
+ .option .interactive:hover {
266
258
  filter: url("#shadow-4");
267
259
  }
268
260
 
269
- .curve.interactive:active {
261
+ .option .interactive:active {
270
262
  filter: url("#shadow-8");
271
263
  }
272
264
 
273
- :host(.keyboard) .curve.interactive:focus {
265
+ :host(.keyboard) .option .interactive:focus-within {
274
266
  filter: url("#shadow-8");
275
267
  }
276
268
 
277
- .bar {
278
- fill: none;
279
- stroke: var(---color-element-emphasis);
280
- stroke-width: 2;
269
+ .option .body.interactive:has(~ .point:hover) {
270
+ filter: url("#shadow-4");
281
271
  }
282
272
 
283
- .bar.interactive {
284
- cursor: ew-resize;
285
-
286
- filter: url("#shadow-2");
273
+ .option .body.interactive:has(~ .point:active) {
274
+ filter: url("#shadow-8");
275
+ }
276
+
277
+ :host(.keyboard) .option .body.interactive:has(~ .point:focus-within) {
278
+ filter: url("#shadow-8");
279
+ }
280
+
281
+ .gradient.sooner stop {
282
+ stop-color: var(---color-sooner);
283
+ }
284
+
285
+ .gradient.later stop {
286
+ stop-color: var(---color-later);
287
+ }
288
+
289
+ .stop-0,
290
+ .stop-before {
291
+ stop-opacity: 0;
292
+ }
293
+
294
+ .stop-after,
295
+ .stop-100 {
296
+ stop-opacity: 1;
297
+ }
298
+
299
+ .fill {
300
+ fill: var(---color-element-enabled);
301
+ fill-opacity: 0.5;
302
+ stroke: none;
303
+ }
304
+
305
+ .interactive .fill {
306
+ cursor: move;
307
+
287
308
  outline: none;
288
309
  }
289
310
 
290
- .bar.interactive:hover {
291
- filter: url("#shadow-4");
311
+ .sooner .fill {
312
+ fill: var(---color-sooner);
292
313
  }
293
314
 
294
- .bar.interactive:active {
295
- filter: url("#shadow-8");
315
+ .later .fill {
316
+ fill: var(---color-later);
296
317
  }
297
318
 
298
- :host(.keyboard) .bar.interactive:focus {
299
- filter: url("#shadow-8");
319
+ .trial.sooner .fill {
320
+ fill: url("#sooner-gradient");
321
+ }
322
+
323
+ .trial.later .fill {
324
+ fill: url("#later-gradient");
325
+ }
326
+
327
+ .bar {
328
+ fill: none;
329
+ stroke: var(---color-element-emphasis);
330
+ stroke-width: 2;
331
+ }
332
+
333
+ .interactive .bar {
334
+ cursor: ew-resize;
335
+
336
+ outline: none;
300
337
  }
301
338
 
302
339
  .point .mark {
@@ -314,38 +351,22 @@ export default class HTDCurves extends DecidablesMixinResizeable(DiscountableEle
314
351
  fill: var(---color-text-inverse);
315
352
  }
316
353
 
317
- .point.interactive {
354
+ .point.interact {
318
355
  cursor: ns-resize;
319
356
 
320
- filter: url("#shadow-2");
321
357
  outline: none;
322
-
323
- /* HACK: This gets Safari to correctly apply the filter! */
324
- /* https://github.com/emilbjorklund/svg-weirdness/issues/27 */
325
- stroke: #000000;
326
- stroke-opacity: 0;
327
- stroke-width: 0;
328
- }
329
-
330
- .point.interactive:hover {
331
- filter: url("#shadow-4");
332
-
333
- /* HACK: This gets Safari to correctly apply the filter! */
334
- stroke: #ff0000;
335
358
  }
336
359
 
337
- .point.interactive:active {
338
- filter: url("#shadow-8");
339
-
340
- /* HACK: This gets Safari to correctly apply the filter! */
341
- stroke: #00ff00;
360
+ .curve {
361
+ fill: none;
362
+ stroke: var(---color-element-emphasis);
363
+ stroke-width: 2;
342
364
  }
343
365
 
344
- :host(.keyboard) .point.interactive:focus {
345
- filter: url("#shadow-8");
366
+ .curve.interactive {
367
+ cursor: nwse-resize;
346
368
 
347
- /* HACK: This gets Safari to correctly apply the filter! */
348
- stroke: #0000ff;
369
+ outline: none;
349
370
  }
350
371
 
351
372
  /* Make larger targets for touch users */
@@ -434,6 +455,38 @@ export default class HTDCurves extends DecidablesMixinResizeable(DiscountableEle
434
455
  const svgEnter = svgUpdate.enter().append('svg')
435
456
  .classed('main', true);
436
457
  svgEnter.html(DiscountableElement.svgDefs);
458
+ // Gradients for fill animations
459
+ const svgDefs = svgEnter.append('defs');
460
+ const soonerGradient = svgDefs.append('linearGradient')
461
+ .classed('gradient sooner', true)
462
+ .attr('id', 'sooner-gradient');
463
+ soonerGradient.append('stop')
464
+ .classed('stop-0', true)
465
+ .attr('offset', '0');
466
+ soonerGradient.append('stop')
467
+ .classed('stop-before animation', true)
468
+ .attr('offset', '1');
469
+ soonerGradient.append('stop')
470
+ .classed('stop-after animation', true)
471
+ .attr('offset', '1');
472
+ soonerGradient.append('stop')
473
+ .classed('stop-100', true)
474
+ .attr('offset', '1');
475
+ const laterGradient = svgDefs.append('linearGradient')
476
+ .classed('gradient later', true)
477
+ .attr('id', 'later-gradient');
478
+ laterGradient.append('stop')
479
+ .classed('stop-0', true)
480
+ .attr('offset', '0');
481
+ laterGradient.append('stop')
482
+ .classed('stop-before animation', true)
483
+ .attr('offset', '1');
484
+ laterGradient.append('stop')
485
+ .classed('stop-after animation', true)
486
+ .attr('offset', '1');
487
+ laterGradient.append('stop')
488
+ .classed('stop-100', true)
489
+ .attr('offset', '1');
437
490
  // MERGE
438
491
  const svgMerge = svgEnter.merge(svgUpdate)
439
492
  .attr('viewBox', `0 0 ${elementWidth} ${elementHeight}`);
@@ -554,13 +607,27 @@ export default class HTDCurves extends DecidablesMixinResizeable(DiscountableEle
554
607
  );
555
608
  // ENTER
556
609
  const optionEnter = optionUpdate.enter().append('g')
557
- .classed('option', true);
558
- // Curve
559
- const curveEnter = optionEnter.append('g')
560
- .classed('curve', true)
561
- .attr('clip-path', 'url(#clip-htd-curves)');
562
- curveEnter.append('path')
563
- .classed('path', true)
610
+ .attr('class', (datum) => {
611
+ const labelClass = datum.label === 's' ? 'sooner' : datum.label === 'l' ? 'later' : '';
612
+ const trialClass = datum.trial ? 'trial' : '';
613
+ return `option ${labelClass} ${trialClass}`;
614
+ });
615
+ // Body (Fill, Bar, Point)
616
+ const bodyEnter = optionEnter.append('g')
617
+ .classed('body', true);
618
+ // Fill
619
+ const fillEnter = bodyEnter.append('g')
620
+ .classed('fill', true)
621
+ .attr('clip-path', 'url(#clip-htd-curves)')
622
+ .each((datum) => {
623
+ if (datum.trial) {
624
+ svgMerge
625
+ .selectAll('.gradient .animation')
626
+ .attr('offset', 1);
627
+ }
628
+ });
629
+ fillEnter.append('path')
630
+ .classed('region', true)
564
631
  .attr('d', (datum) => {
565
632
  const curve = d3.range(xScale(datum.d), xScale(0), -1).map((range) => {
566
633
  return {
@@ -572,8 +639,20 @@ export default class HTDCurves extends DecidablesMixinResizeable(DiscountableEle
572
639
  ),
573
640
  };
574
641
  });
575
- return line(curve);
576
- })
642
+ return line([...curve, {d: 0, v: 0}, {d: datum.d, v: 0}]);
643
+ });
644
+ // Bar
645
+ const barEnter = bodyEnter.append('g')
646
+ .classed('bar', true);
647
+ barEnter.append('line')
648
+ .classed('line', true);
649
+ barEnter.append('line')
650
+ .classed('line touch', true);
651
+ barEnter.selectAll('.line')
652
+ .attr('x1', (datum) => { return xScale(datum.d); })
653
+ .attr('x2', (datum) => { return xScale(datum.d); })
654
+ .attr('y1', yScale(0))
655
+ .attr('y2', (datum) => { return yScale(datum.a); })
577
656
  .attr('stroke-dasharray', (datum, index, nodes) => {
578
657
  if (datum.trial) {
579
658
  const length = nodes[index].getTotalLength();
@@ -581,8 +660,20 @@ export default class HTDCurves extends DecidablesMixinResizeable(DiscountableEle
581
660
  }
582
661
  return 'none';
583
662
  });
663
+ // Point
664
+ const pointEnter = bodyEnter.append('g')
665
+ .classed('point', true);
666
+ pointEnter.append('circle')
667
+ .classed('mark touch', true);
668
+ // Curve
669
+ const curveEnter = optionEnter.append('g')
670
+ .classed('curve', true)
671
+ .attr('clip-path', 'url(#clip-htd-curves)');
672
+ curveEnter.append('path')
673
+ .classed('path', true);
584
674
  curveEnter.append('path')
585
- .classed('path touch', true)
675
+ .classed('path touch', true);
676
+ curveEnter.selectAll('.path')
586
677
  .attr('d', (datum) => {
587
678
  const curve = d3.range(xScale(datum.d), xScale(0), -1).map((range) => {
588
679
  return {
@@ -603,38 +694,14 @@ export default class HTDCurves extends DecidablesMixinResizeable(DiscountableEle
603
694
  }
604
695
  return 'none';
605
696
  });
606
- // Bar
607
- const barEnter = optionEnter.append('g')
608
- .classed('bar', true);
609
- barEnter.append('line')
610
- .classed('line', true)
611
- .attr('x1', (datum) => { return xScale(datum.d); })
612
- .attr('x2', (datum) => { return xScale(datum.d); })
613
- .attr('y1', yScale(0))
614
- .attr('y2', (datum) => { return yScale(datum.a); })
615
- .attr('stroke-dasharray', (datum, index, nodes) => {
616
- if (datum.trial) {
617
- const length = nodes[index].getTotalLength();
618
- return `0,${length}`;
619
- }
620
- return 'none';
621
- });
622
- barEnter.append('line')
623
- .classed('line touch', true)
624
- .attr('x1', (datum) => { return xScale(datum.d); })
625
- .attr('x2', (datum) => { return xScale(datum.d); })
626
- .attr('y1', yScale(0))
627
- .attr('y2', (datum) => { return yScale(datum.a); })
628
- .attr('stroke-dasharray', (datum, index, nodes) => {
629
- if (datum.trial) {
630
- const length = nodes[index].getTotalLength();
631
- return `0,${length}`;
632
- }
633
- return 'none';
634
- });
635
- // Point
636
- const pointEnter = optionEnter.append('g')
637
- .classed('point', true)
697
+ // Point (again)
698
+ const topPointEnter = optionEnter.append('g')
699
+ .classed('point top-point', true);
700
+ topPointEnter.append('circle')
701
+ .classed('mark touch', true);
702
+ topPointEnter.append('text')
703
+ .classed('label', true);
704
+ optionEnter.selectAll('.point')
638
705
  .attr('transform', (datum) => {
639
706
  return `translate(${xScale(datum.d)}, ${yScale(datum.a)})`;
640
707
  })
@@ -644,94 +711,57 @@ export default class HTDCurves extends DecidablesMixinResizeable(DiscountableEle
644
711
  }
645
712
  return 1;
646
713
  });
647
- pointEnter.append('circle')
648
- .classed('mark touch', true);
649
- pointEnter.append('text')
650
- .classed('label', true);
714
+
651
715
  // MERGE
652
716
  const optionMerge = optionEnter.merge(optionUpdate);
653
717
 
654
718
  // Interactive options
655
- // Curve
656
- optionMerge
719
+ // Body (Fill, Bar, Point)
720
+ const bodyMergeInteractive = optionMerge
657
721
  .filter((datum, index, nodes) => {
658
- return (this.interactive && !d3.select(nodes[index]).select('.curve').classed('interactive'));
722
+ return (this.interactive && !datum.trial && !d3.select(nodes[index]).select('.body').classed('interactive'));
659
723
  })
660
- .select('.curve')
661
- .classed('interactive', true)
724
+ .select('.body');
725
+ bodyMergeInteractive.classed('interactive', true)
662
726
  .attr('tabindex', 0)
663
- // Drag interaction
664
- .call(d3.drag()
665
- .subject((event) => {
666
- return {
667
- x: event.x,
668
- y: event.y,
669
- };
670
- })
671
- .on('start', (event) => {
672
- const element = event.currentTarget;
673
- d3.select(element).classed('dragging', true);
674
- })
675
- .on('drag', (event, datum) => {
676
- this.drag = true;
677
- const dragD = datum.d - xScale.invert(event.x);
678
- const d = (dragD < 0)
679
- ? 0
680
- : (dragD > datum.d)
681
- ? datum.d
682
- : dragD;
683
- const dragV = yScale.invert(event.y);
684
- const v = (dragV <= 0)
685
- ? 0.001
686
- : (dragV > datum.a)
687
- ? datum.a
688
- : dragV;
689
- const k = HTDMath.adv2k(datum.a, d, v);
690
- this.k = (k < HTDMath.k.MIN)
691
- ? HTDMath.k.MIN
692
- : (k > HTDMath.k.MAX)
693
- ? HTDMath.k.MAX
694
- : k;
695
- this.alignState();
696
- this.requestUpdate();
697
- this.dispatchEvent(new CustomEvent('htd-curves-change', {
698
- detail: {
699
- name: datum.name,
700
- a: datum.a,
701
- d: datum.d,
702
- k: this.k,
703
- label: datum.label,
704
- },
705
- bubbles: true,
706
- }));
707
- })
708
- .on('end', (event) => {
709
- const element = event.currentTarget;
710
- d3.select(element).classed('dragging', false);
711
- }))
712
727
  // Keyboard interaction
713
728
  .on('keydown', (event, datum) => {
714
- if (['ArrowUp', 'ArrowDown', 'ArrowRight', 'ArrowLeft'].includes(event.key)) {
715
- let {k} = this;
729
+ if (['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'].includes(event.key)) {
730
+ let keyA = datum.a;
731
+ let keyD = datum.d;
716
732
  switch (event.key) {
717
733
  case 'ArrowUp':
718
- case 'ArrowLeft':
719
- k *= event.shiftKey ? 0.95 : 0.85;
734
+ keyA += event.shiftKey ? 1 : 5;
720
735
  break;
721
736
  case 'ArrowDown':
737
+ keyA -= event.shiftKey ? 1 : 5;
738
+ break;
722
739
  case 'ArrowRight':
723
- k *= event.shiftKey ? (1 / 0.95) : (1 / 0.85);
740
+ keyD += event.shiftKey ? 1 : 5;
741
+ break;
742
+ case 'ArrowLeft':
743
+ keyD -= event.shiftKey ? 1 : 5;
724
744
  break;
725
745
  default:
726
746
  // no-op
727
747
  }
728
- k = (k < HTDMath.k.MIN)
729
- ? HTDMath.k.MIN
730
- : (k > HTDMath.k.MAX)
731
- ? HTDMath.k.MAX
732
- : k;
733
- if (k !== this.k) {
734
- this.k = k;
748
+ keyD = (keyD < this.scale.time.min)
749
+ ? this.scale.time.min
750
+ : ((keyD > this.scale.time.max)
751
+ ? this.scale.time.max
752
+ : keyD);
753
+ keyA = (keyA < this.scale.value.min)
754
+ ? this.scale.value.min
755
+ : ((keyA > this.scale.value.max)
756
+ ? this.scale.value.max
757
+ : keyA);
758
+ if ((keyD !== datum.d) || (keyA !== datum.a)) {
759
+ datum.d = keyD;
760
+ datum.a = keyA;
761
+ if (datum.name === 'default') {
762
+ this.d = datum.d;
763
+ this.a = datum.a;
764
+ }
735
765
  this.alignState();
736
766
  this.requestUpdate();
737
767
  this.dispatchEvent(new CustomEvent('htd-curves-change', {
@@ -748,14 +778,8 @@ export default class HTDCurves extends DecidablesMixinResizeable(DiscountableEle
748
778
  event.preventDefault();
749
779
  }
750
780
  });
751
- // Bar
752
- optionMerge
753
- .filter((datum, index, nodes) => {
754
- return (this.interactive && !datum.trial && !d3.select(nodes[index]).select('.bar').classed('interactive'));
755
- })
756
- .select('.bar')
757
- .classed('interactive', true)
758
- .attr('tabindex', 0)
781
+ // Fill
782
+ bodyMergeInteractive.select('.fill')
759
783
  // Drag interaction
760
784
  .call(d3.drag()
761
785
  .subject((event, datum) => {
@@ -771,13 +795,20 @@ export default class HTDCurves extends DecidablesMixinResizeable(DiscountableEle
771
795
  .on('drag', (event, datum) => {
772
796
  this.drag = true;
773
797
  const d = xScale.invert(event.x);
798
+ const a = yScale.invert(event.y);
774
799
  datum.d = (d < this.scale.time.min)
775
800
  ? this.scale.time.min
776
801
  : (d > this.scale.time.max)
777
802
  ? this.scale.time.max
778
803
  : this.scale.time.round(d);
804
+ datum.a = (a < this.scale.value.min)
805
+ ? this.scale.value.min
806
+ : (a > this.scale.value.max)
807
+ ? this.scale.value.max
808
+ : this.scale.value.round(a);
779
809
  if (datum.name === 'default') {
780
810
  this.d = datum.d;
811
+ this.a = datum.a;
781
812
  }
782
813
  this.alignState();
783
814
  this.requestUpdate();
@@ -795,55 +826,57 @@ export default class HTDCurves extends DecidablesMixinResizeable(DiscountableEle
795
826
  .on('end', (event) => {
796
827
  const element = event.currentTarget;
797
828
  d3.select(element).classed('dragging', false);
798
- }))
799
- // Keyboard interaction
800
- .on('keydown', (event, datum) => {
801
- if (['ArrowLeft', 'ArrowRight'].includes(event.key)) {
802
- let keyD = datum.d;
803
- switch (event.key) {
804
- case 'ArrowRight':
805
- keyD += event.shiftKey ? 1 : 5;
806
- break;
807
- case 'ArrowLeft':
808
- keyD -= event.shiftKey ? 1 : 5;
809
- break;
810
- default:
811
- // no-op
812
- }
813
- keyD = (keyD < this.scale.time.min)
814
- ? this.scale.time.min
815
- : ((keyD > this.scale.time.max)
816
- ? this.scale.time.max
817
- : keyD);
818
- if (keyD !== datum.d) {
819
- datum.d = keyD;
820
- if (datum.name === 'default') {
821
- this.d = datum.d;
822
- }
823
- this.alignState();
824
- this.requestUpdate();
825
- this.dispatchEvent(new CustomEvent('htd-curves-change', {
826
- detail: {
827
- name: datum.name,
828
- a: datum.a,
829
- d: datum.d,
830
- k: this.k,
831
- label: datum.label,
832
- },
833
- bubbles: true,
834
- }));
829
+ }));
830
+ // Bar
831
+ bodyMergeInteractive.select('.bar')
832
+ // Drag interaction
833
+ .call(d3.drag()
834
+ .subject((event, datum) => {
835
+ return {
836
+ x: xScale(datum.d),
837
+ y: yScale(datum.a),
838
+ };
839
+ })
840
+ .on('start', (event) => {
841
+ const element = event.currentTarget;
842
+ d3.select(element).classed('dragging', true);
843
+ })
844
+ .on('drag', (event, datum) => {
845
+ this.drag = true;
846
+ const d = xScale.invert(event.x);
847
+ datum.d = (d < this.scale.time.min)
848
+ ? this.scale.time.min
849
+ : (d > this.scale.time.max)
850
+ ? this.scale.time.max
851
+ : this.scale.time.round(d);
852
+ if (datum.name === 'default') {
853
+ this.d = datum.d;
835
854
  }
836
- event.preventDefault();
837
- }
838
- });
855
+ this.alignState();
856
+ this.requestUpdate();
857
+ this.dispatchEvent(new CustomEvent('htd-curves-change', {
858
+ detail: {
859
+ name: datum.name,
860
+ a: datum.a,
861
+ d: datum.d,
862
+ k: this.k,
863
+ label: datum.label,
864
+ },
865
+ bubbles: true,
866
+ }));
867
+ })
868
+ .on('end', (event) => {
869
+ const element = event.currentTarget;
870
+ d3.select(element).classed('dragging', false);
871
+ }));
839
872
  // Point
840
873
  optionMerge
841
874
  .filter((datum, index, nodes) => {
842
- return (this.interactive && !datum.trial && !d3.select(nodes[index]).select('.point').classed('interactive'));
875
+ return (this.interactive && !datum.trial && !d3.select(nodes[index]).select('.top-point').classed('interact'));
843
876
  })
844
- .select('.point')
845
- .classed('interactive', true)
846
- .attr('tabindex', 0)
877
+ .select('.top-point')
878
+ .classed('interact', true)
879
+ .attr('tabindex', -1)
847
880
  // Drag interaction
848
881
  .call(d3.drag()
849
882
  .subject((event, datum) => {
@@ -886,8 +919,9 @@ export default class HTDCurves extends DecidablesMixinResizeable(DiscountableEle
886
919
  }))
887
920
  // Keyboard interaction
888
921
  .on('keydown', (event, datum) => {
889
- if (['ArrowUp', 'ArrowDown'].includes(event.key)) {
922
+ if (['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'].includes(event.key)) {
890
923
  let keyA = datum.a;
924
+ let keyD = datum.d;
891
925
  switch (event.key) {
892
926
  case 'ArrowUp':
893
927
  keyA += event.shiftKey ? 1 : 5;
@@ -895,17 +929,30 @@ export default class HTDCurves extends DecidablesMixinResizeable(DiscountableEle
895
929
  case 'ArrowDown':
896
930
  keyA -= event.shiftKey ? 1 : 5;
897
931
  break;
932
+ case 'ArrowRight':
933
+ keyD += event.shiftKey ? 1 : 5;
934
+ break;
935
+ case 'ArrowLeft':
936
+ keyD -= event.shiftKey ? 1 : 5;
937
+ break;
898
938
  default:
899
939
  // no-op
900
940
  }
941
+ keyD = (keyD < this.scale.time.min)
942
+ ? this.scale.time.min
943
+ : ((keyD > this.scale.time.max)
944
+ ? this.scale.time.max
945
+ : keyD);
901
946
  keyA = (keyA < this.scale.value.min)
902
947
  ? this.scale.value.min
903
948
  : ((keyA > this.scale.value.max)
904
949
  ? this.scale.value.max
905
950
  : keyA);
906
- if (keyA !== datum.a) {
951
+ if ((keyD !== datum.d) || (keyA !== datum.a)) {
952
+ datum.d = keyD;
907
953
  datum.a = keyA;
908
954
  if (datum.name === 'default') {
955
+ this.d = datum.d;
909
956
  this.a = datum.a;
910
957
  }
911
958
  this.alignState();
@@ -924,115 +971,212 @@ export default class HTDCurves extends DecidablesMixinResizeable(DiscountableEle
924
971
  event.preventDefault();
925
972
  }
926
973
  });
927
-
928
- // Non-interactive options
929
974
  // Curve
930
975
  optionMerge
931
976
  .filter((datum, index, nodes) => {
932
- return (!this.interactive && d3.select(nodes[index]).select('.curve').classed('interactive'));
977
+ return (this.interactive && !d3.select(nodes[index]).select('.curve').classed('interactive'));
933
978
  })
934
979
  .select('.curve')
935
- .classed('interactive', false)
980
+ .classed('interactive', true)
981
+ .attr('tabindex', 0)
982
+ // Drag interaction
983
+ .call(d3.drag()
984
+ .subject((event) => {
985
+ return {
986
+ x: event.x,
987
+ y: event.y,
988
+ };
989
+ })
990
+ .on('start', (event) => {
991
+ const element = event.currentTarget;
992
+ d3.select(element).classed('dragging', true);
993
+ })
994
+ .on('drag', (event, datum) => {
995
+ this.drag = true;
996
+ const dragD = datum.d - xScale.invert(event.x);
997
+ const d = (dragD < 0)
998
+ ? 0
999
+ : (dragD > datum.d)
1000
+ ? datum.d
1001
+ : dragD;
1002
+ const dragV = yScale.invert(event.y);
1003
+ const v = (dragV <= 0)
1004
+ ? 0.001
1005
+ : (dragV > datum.a)
1006
+ ? datum.a
1007
+ : dragV;
1008
+ const k = HTDMath.adv2k(datum.a, d, v);
1009
+ this.k = (k < HTDMath.k.MIN)
1010
+ ? HTDMath.k.MIN
1011
+ : (k > HTDMath.k.MAX)
1012
+ ? HTDMath.k.MAX
1013
+ : k;
1014
+ this.alignState();
1015
+ this.requestUpdate();
1016
+ this.dispatchEvent(new CustomEvent('htd-curves-change', {
1017
+ detail: {
1018
+ name: datum.name,
1019
+ a: datum.a,
1020
+ d: datum.d,
1021
+ k: this.k,
1022
+ label: datum.label,
1023
+ },
1024
+ bubbles: true,
1025
+ }));
1026
+ })
1027
+ .on('end', (event) => {
1028
+ const element = event.currentTarget;
1029
+ d3.select(element).classed('dragging', false);
1030
+ }))
1031
+ // Keyboard interaction
1032
+ .on('keydown', (event, datum) => {
1033
+ if (['ArrowUp', 'ArrowDown', 'ArrowRight', 'ArrowLeft'].includes(event.key)) {
1034
+ let {k} = this;
1035
+ switch (event.key) {
1036
+ case 'ArrowUp':
1037
+ case 'ArrowLeft':
1038
+ k *= event.shiftKey ? 0.95 : 0.85;
1039
+ break;
1040
+ case 'ArrowDown':
1041
+ case 'ArrowRight':
1042
+ k *= event.shiftKey ? (1 / 0.95) : (1 / 0.85);
1043
+ break;
1044
+ default:
1045
+ // no-op
1046
+ }
1047
+ k = (k < HTDMath.k.MIN)
1048
+ ? HTDMath.k.MIN
1049
+ : (k > HTDMath.k.MAX)
1050
+ ? HTDMath.k.MAX
1051
+ : k;
1052
+ if (k !== this.k) {
1053
+ this.k = k;
1054
+ this.alignState();
1055
+ this.requestUpdate();
1056
+ this.dispatchEvent(new CustomEvent('htd-curves-change', {
1057
+ detail: {
1058
+ name: datum.name,
1059
+ a: datum.a,
1060
+ d: datum.d,
1061
+ k: this.k,
1062
+ label: datum.label,
1063
+ },
1064
+ bubbles: true,
1065
+ }));
1066
+ }
1067
+ event.preventDefault();
1068
+ }
1069
+ });
1070
+
1071
+ // Non-interactive options
1072
+ // Body (Fill, Bar, Point)
1073
+ const bodyMergeNoninteractive = optionMerge
1074
+ .filter((datum, index, nodes) => {
1075
+ return ((!this.interactive || datum.trial) && d3.select(nodes[index]).select('.body').classed('interactive'));
1076
+ });
1077
+ bodyMergeNoninteractive.classed('interactive', false);
1078
+ // Fill
1079
+ bodyMergeNoninteractive
1080
+ .select('.fill')
936
1081
  .attr('tabindex', null)
937
1082
  .on('drag', null)
938
1083
  .on('keydown', null);
939
1084
  // Bar
1085
+ bodyMergeNoninteractive
1086
+ .select('.bar')
1087
+ .on('drag', null)
1088
+ .on('keydown', null);
1089
+ // Point
940
1090
  optionMerge
941
1091
  .filter((datum, index, nodes) => {
942
- return ((!this.interactive || datum.trial) && d3.select(nodes[index]).select('.bar').classed('interactive'));
1092
+ return ((!this.interactive || datum.trial) && d3.select(nodes[index]).select('.top-point').classed('interact'));
943
1093
  })
944
- .select('.bar')
945
- .classed('interactive', false)
946
- .attr('tabindex', null)
1094
+ .select('.top-point')
1095
+ .classed('interact', false)
947
1096
  .on('drag', null)
948
1097
  .on('keydown', null);
949
- // Point
1098
+ // Curve
950
1099
  optionMerge
951
1100
  .filter((datum, index, nodes) => {
952
- return ((!this.interactive || datum.trial) && d3.select(nodes[index]).select('.point').classed('interactive'));
1101
+ return (!this.interactive && d3.select(nodes[index]).select('.curve').classed('interactive'));
953
1102
  })
954
- .select('.point')
1103
+ .select('.curve')
955
1104
  .classed('interactive', false)
956
1105
  .attr('tabindex', null)
957
1106
  .on('drag', null)
958
1107
  .on('keydown', null);
959
1108
 
960
1109
  // Trial Animation
961
- // Curve
1110
+ // Fill
962
1111
  optionMerge
963
1112
  .filter((datum) => {
964
1113
  return (datum.new);
965
1114
  })
966
- .select('.curve .path').transition()
967
- .duration(transitionDuration)
968
- .delay(transitionDuration + transitionDuration / 10)
969
- .ease(d3.easeLinear)
970
- .attrTween('stroke-dasharray', (datum, index, nodes) => {
971
- const length = nodes[index].getTotalLength();
972
- return d3.interpolate(`0,${length}`, `${length},${0}`);
973
- })
974
- .on('end', (datum) => {
975
- datum.new = false;
976
- this.dispatchEvent(new CustomEvent('discountable-response', {
977
- detail: {
978
- trial: this.trialCount,
979
- as: this.as,
980
- ds: this.ds,
981
- al: this.al,
982
- dl: this.dl,
983
- response: this.response,
984
- },
985
- bubbles: true,
986
- }));
1115
+ .each(() => {
1116
+ svgMerge
1117
+ .selectAll('.gradient .animation').transition()
1118
+ .duration(transitionDuration)
1119
+ .delay(transitionDuration + transitionDuration / 10)
1120
+ .ease(d3.easeLinear)
1121
+ .attrTween('offset', () => { return d3.interpolate(1, 0); });
987
1122
  });
1123
+ // Bar
988
1124
  optionMerge
989
1125
  .filter((datum) => {
990
1126
  return (datum.new);
991
1127
  })
992
- .select('.curve .path.touch').transition()
1128
+ .selectAll('.bar .line').transition()
993
1129
  .duration(transitionDuration)
994
- .delay(transitionDuration + transitionDuration / 10)
995
1130
  .ease(d3.easeLinear)
996
1131
  .attrTween('stroke-dasharray', (datum, index, nodes) => {
997
1132
  const length = nodes[index].getTotalLength();
998
- return d3.interpolate(`0,${length}`, `${length},${0}`);
1133
+ return d3.interpolate(`0,${length}`, `${length},${length}`);
999
1134
  });
1000
- // Bar
1135
+ // Point
1001
1136
  optionMerge
1002
1137
  .filter((datum) => {
1003
1138
  return (datum.new);
1004
1139
  })
1005
- .select('.bar .line').transition()
1006
- .duration(transitionDuration)
1140
+ .selectAll('.point').transition()
1141
+ .duration(transitionDuration / 10)
1142
+ .delay(transitionDuration)
1007
1143
  .ease(d3.easeLinear)
1008
- .attrTween('stroke-dasharray', (datum, index, nodes) => {
1009
- const length = nodes[index].getTotalLength();
1010
- return d3.interpolate(`0,${length}`, `${length},${length}`);
1011
- });
1144
+ .attrTween('opacity', () => { return d3.interpolate(0, 1); });
1145
+ // Curve
1012
1146
  optionMerge
1013
1147
  .filter((datum) => {
1014
1148
  return (datum.new);
1015
1149
  })
1016
- .select('.bar .line.touch').transition()
1150
+ .selectAll('.curve .path').transition()
1017
1151
  .duration(transitionDuration)
1152
+ .delay(transitionDuration + transitionDuration / 10)
1018
1153
  .ease(d3.easeLinear)
1019
1154
  .attrTween('stroke-dasharray', (datum, index, nodes) => {
1020
1155
  const length = nodes[index].getTotalLength();
1021
- return d3.interpolate(`0,${length}`, `${length},${length}`);
1022
- });
1023
- // Point
1024
- optionMerge
1025
- .filter((datum) => {
1026
- return (datum.new);
1156
+ return d3.interpolate(`0,${length}`, `${length},${0}`);
1027
1157
  })
1028
- .select('.point').transition()
1029
- .duration(transitionDuration / 10)
1030
- .delay(transitionDuration)
1031
- .ease(d3.easeLinear)
1032
- .attrTween('opacity', () => { return d3.interpolate(0, 1); });
1158
+ .on('end', (datum) => {
1159
+ datum.new = false;
1160
+ this.dispatchEvent(new CustomEvent('discountable-response', {
1161
+ detail: {
1162
+ trial: this.trialCount,
1163
+ as: this.as,
1164
+ ds: this.ds,
1165
+ al: this.al,
1166
+ dl: this.dl,
1167
+ response: this.response,
1168
+ },
1169
+ bubbles: true,
1170
+ }));
1171
+ });
1033
1172
 
1034
1173
  // All options
1035
- optionUpdate.select('.curve .path').transition()
1174
+ optionMerge.filter((datum) => { return datum.label === 's'; })
1175
+ .raise();
1176
+ optionMerge.filter((datum) => { return datum.label === 'l'; })
1177
+ .lower();
1178
+ // Fill
1179
+ optionUpdate.select('.fill .region').transition()
1036
1180
  .duration(this.drag
1037
1181
  ? 0
1038
1182
  : (this.firstUpdate
@@ -1062,43 +1206,11 @@ export default class HTDCurves extends DecidablesMixinResizeable(DiscountableEle
1062
1206
  ),
1063
1207
  };
1064
1208
  });
1065
- return line(curve);
1209
+ return line([...curve, {d: 0, v: 0}, {d: element.d, v: 0}]);
1066
1210
  };
1067
1211
  });
1068
- optionUpdate.select('.curve .path.touch').transition()
1069
- .duration(this.drag
1070
- ? 0
1071
- : (this.firstUpdate
1072
- ? (transitionDuration * 2)
1073
- : transitionDuration))
1074
- .ease(d3.easeCubicOut)
1075
- .attrTween('d', (datum, index, elements) => {
1076
- const element = elements[index];
1077
- const interpolateA = d3.interpolate(
1078
- (element.a !== undefined) ? element.a : datum.a,
1079
- datum.a,
1080
- );
1081
- const interpolateD = d3.interpolate(
1082
- (element.d !== undefined) ? element.d : datum.d,
1083
- datum.d,
1084
- );
1085
- return (time) => {
1086
- element.a = interpolateA(time);
1087
- element.d = interpolateD(time);
1088
- const curve = d3.range(xScale(element.d), xScale(0), -1).map((range) => {
1089
- return {
1090
- d: xScale.invert(range),
1091
- v: HTDMath.adk2v(
1092
- element.a,
1093
- element.d - xScale.invert(range),
1094
- this.k,
1095
- ),
1096
- };
1097
- });
1098
- return line(curve);
1099
- };
1100
- });
1101
- optionUpdate.select('.bar .line').transition()
1212
+ // Bar
1213
+ optionUpdate.selectAll('.bar .line').transition()
1102
1214
  .duration(this.drag
1103
1215
  ? 0
1104
1216
  : (this.firstUpdate
@@ -1138,71 +1250,66 @@ export default class HTDCurves extends DecidablesMixinResizeable(DiscountableEle
1138
1250
  return `${yScale(element.a)}`;
1139
1251
  };
1140
1252
  });
1141
- optionUpdate.select('.bar .line.touch').transition()
1253
+ // Point
1254
+ optionUpdate.selectAll('.point').transition()
1142
1255
  .duration(this.drag
1143
1256
  ? 0
1144
1257
  : (this.firstUpdate
1145
1258
  ? (transitionDuration * 2)
1146
1259
  : transitionDuration))
1147
1260
  .ease(d3.easeCubicOut)
1148
- .attrTween('x1', (datum, index, elements) => {
1149
- const element = elements[index];
1150
- const interpolateD = d3.interpolate(
1151
- (element.d !== undefined) ? element.d : datum.d,
1152
- datum.d,
1153
- );
1154
- return (time) => {
1155
- element.d = interpolateD(time);
1156
- return `${xScale(element.d)}`;
1157
- };
1158
- })
1159
- .attrTween('x2', (datum, index, elements) => {
1261
+ .attrTween('transform', (datum, index, elements) => {
1160
1262
  const element = elements[index];
1161
1263
  const interpolateD = d3.interpolate(
1162
1264
  (element.d !== undefined) ? element.d : datum.d,
1163
1265
  datum.d,
1164
1266
  );
1165
- return (time) => {
1166
- element.d = interpolateD(time);
1167
- return `${xScale(element.d)}`;
1168
- };
1169
- })
1170
- .attrTween('y2', (datum, index, elements) => {
1171
- const element = elements[index];
1172
1267
  const interpolateA = d3.interpolate(
1173
1268
  (element.a !== undefined) ? element.a : datum.a,
1174
1269
  datum.a,
1175
1270
  );
1176
1271
  return (time) => {
1272
+ element.d = interpolateD(time);
1177
1273
  element.a = interpolateA(time);
1178
- return `${yScale(element.a)}`;
1274
+ return `translate(${xScale(element.d)}, ${yScale(element.a)})`;
1179
1275
  };
1180
1276
  });
1181
- optionUpdate.select('.point').transition()
1277
+ optionMerge.select('.point .label')
1278
+ .text((datum) => { return datum.label; });
1279
+ // Curve
1280
+ optionUpdate.selectAll('.curve .path').transition()
1182
1281
  .duration(this.drag
1183
1282
  ? 0
1184
1283
  : (this.firstUpdate
1185
1284
  ? (transitionDuration * 2)
1186
1285
  : transitionDuration))
1187
1286
  .ease(d3.easeCubicOut)
1188
- .attrTween('transform', (datum, index, elements) => {
1287
+ .attrTween('d', (datum, index, elements) => {
1189
1288
  const element = elements[index];
1190
- const interpolateD = d3.interpolate(
1191
- (element.d !== undefined) ? element.d : datum.d,
1192
- datum.d,
1193
- );
1194
1289
  const interpolateA = d3.interpolate(
1195
1290
  (element.a !== undefined) ? element.a : datum.a,
1196
1291
  datum.a,
1197
1292
  );
1293
+ const interpolateD = d3.interpolate(
1294
+ (element.d !== undefined) ? element.d : datum.d,
1295
+ datum.d,
1296
+ );
1198
1297
  return (time) => {
1199
- element.d = interpolateD(time);
1200
1298
  element.a = interpolateA(time);
1201
- return `translate(${xScale(element.d)}, ${yScale(element.a)})`;
1299
+ element.d = interpolateD(time);
1300
+ const curve = d3.range(xScale(element.d), xScale(0), -1).map((range) => {
1301
+ return {
1302
+ d: xScale.invert(range),
1303
+ v: HTDMath.adk2v(
1304
+ element.a,
1305
+ element.d - xScale.invert(range),
1306
+ this.k,
1307
+ ),
1308
+ };
1309
+ });
1310
+ return line(curve);
1202
1311
  };
1203
1312
  });
1204
- optionMerge.select('.point .label')
1205
- .text((datum) => { return datum.label; });
1206
1313
  // EXIT
1207
1314
  // NOTE: Could add a transition here
1208
1315
  optionUpdate.exit().remove();