@decidables/discountable-elements
Version:
discountable-elements: Web Components for visualizing Hyperbolic Temporal Discounting
1,216 lines (1,118 loc) • 33.9 kB
JavaScript
import {html, css} from 'lit';
import * as d3 from 'd3';
import HTDMath from '@decidables/discountable-math';
import {DecidablesMixinResizeable} from '@decidables/decidables-elements';
import DiscountableElement from '../discountable-element';
/*
HTDCurves element
<htd-curves>
Attributes:
interactive: true/false
a: numeric (-infinity, infinity)
d: numeric [0, infinity)
k: numeric [0, infinity)
label: string
Styles:
??
*/
export default class HTDCurves extends DecidablesMixinResizeable(DiscountableElement) {
static get properties() {
return {
a: {
attribute: 'amount',
type: Number,
reflect: true,
},
d: {
attribute: 'delay',
type: Number,
reflect: true,
},
label: {
attribute: 'label',
type: String,
reflect: true,
},
k: {
attribute: 'k',
type: Number,
reflect: true,
},
v: {
attribute: false,
type: Number,
reflect: false,
},
};
}
constructor() {
super();
this.firstUpdate = true;
this.drag = false;
this.scale = {
value: {
min: 0,
max: 80,
step: 1,
round: Math.round,
},
time: {
min: 0,
max: 100,
step: 1,
round: Math.round,
},
};
this.a = null;
this.d = null;
this.label = '';
this.k = HTDMath.k.DEFAULT;
this.options = [
{
name: 'default',
a: this.a,
d: this.d,
label: this.label,
},
];
this.as = null;
this.ds = null;
this.al = null;
this.dl = null;
this.trialCount = null;
this.response = null;
this.alignState();
}
alignState() {
// Default options
this.options[0].a = this.a;
this.options[0].d = this.d;
this.options[0].label = this.label;
// Update values
this.options.forEach((option) => {
option.v = HTDMath.adk2v(option.a, option.d, this.k);
});
this.v = this.options[0].v;
}
trial(as, ds, al, dl, trial, response) {
// Remove the old trial
if (this.trialCount) this.removeOption(`${this.trialCount}-s`);
if (this.trialCount) this.removeOption(`${this.trialCount}-l`);
this.as = as;
this.ds = ds;
this.al = al;
this.dl = dl;
this.trialCount = trial;
this.response = response;
// Add the new trial
this.setOption(this.as, this.ds, `${this.trialCount}-s`, 's', true);
this.setOption(this.al, this.dl, `${this.trialCount}-l`, 'l', true);
}
// Called to pause trial animations!
pauseTrial() {
const lineNew = d3.select(this.renderRoot).selectAll('.lines[data-animating-ease-time-1]');
lineNew.interrupt('new-1');
lineNew.interrupt('new-2');
lineNew.datum((datum) => {
datum.paused = true;
return datum;
});
}
// Called to resume trial animations!
resumeTrial() {
const lineNew = d3.select(this.renderRoot).selectAll('.lines[data-animating-ease-time-1]');
lineNew.datum((datum) => {
datum.paused = false;
return datum;
});
this.requestUpdate();
}
clearOptions() {
this.options.splice(1);
this.requestUpdate();
}
removeOption(name) {
this.options = this.options.filter((option) => {
return (option.name !== name);
});
this.requestUpdate();
}
getOption(name = 'default') {
return this.options.find((option) => {
return (option.name === name);
});
}
setOption(a, d, name = 'default', label = '', trial = false) {
if (name === 'default') {
this.a = a;
this.d = d;
this.label = label;
}
const myOption = this.options.find((option) => {
return (option.name === name);
});
if (myOption === undefined) {
this.options.push({
name: name,
a: a,
d: d,
label: label,
trial: trial,
new: trial,
});
} else {
myOption.a = a;
myOption.d = d;
myOption.label = label;
}
this.requestUpdate();
}
static get styles() {
return [
super.styles,
css`
:host {
display: inline-block;
width: 27rem;
height: 15rem;
}
.main {
width: 100%;
height: 100%;
}
text {
/* stylelint-disable property-no-vendor-prefix */
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
.background {
fill: var(---color-element-background);
stroke: var(---color-element-border);
stroke-width: 1;
shape-rendering: crispEdges;
}
.title-x,
.title-y {
font-weight: 600;
fill: currentColor;
}
.tick {
font-size: 0.75rem;
}
.axis-x path,
.axis-x line,
.axis-y path,
.axis-y line {
stroke: var(---color-element-border);
/* shape-rendering: crispEdges; */
}
.curve {
fill: none;
stroke: var(---color-element-emphasis);
stroke-width: 2;
}
.curve.interactive {
cursor: nwse-resize;
filter: url("#shadow-2");
outline: none;
}
.curve.interactive:hover {
filter: url("#shadow-4");
}
.curve.interactive:active {
filter: url("#shadow-8");
}
:host(.keyboard) .curve.interactive:focus {
filter: url("#shadow-8");
}
.bar {
fill: none;
stroke: var(---color-element-emphasis);
stroke-width: 2;
}
.bar.interactive {
cursor: ew-resize;
filter: url("#shadow-2");
outline: none;
}
.bar.interactive:hover {
filter: url("#shadow-4");
}
.bar.interactive:active {
filter: url("#shadow-8");
}
:host(.keyboard) .bar.interactive:focus {
filter: url("#shadow-8");
}
.point .mark {
fill: var(---color-element-emphasis);
r: 6px;
}
.point .label {
font-size: 0.75rem;
dominant-baseline: middle;
text-anchor: middle;
fill: var(---color-text-inverse);
}
.point.interactive {
cursor: ns-resize;
filter: url("#shadow-2");
outline: none;
/* HACK: This gets Safari to correctly apply the filter! */
/* https://github.com/emilbjorklund/svg-weirdness/issues/27 */
stroke: #000000;
stroke-opacity: 0;
stroke-width: 0;
}
.point.interactive:hover {
filter: url("#shadow-4");
/* HACK: This gets Safari to correctly apply the filter! */
stroke: #ff0000;
}
.point.interactive:active {
filter: url("#shadow-8");
/* HACK: This gets Safari to correctly apply the filter! */
stroke: #00ff00;
}
:host(.keyboard) .point.interactive:focus {
filter: url("#shadow-8");
/* HACK: This gets Safari to correctly apply the filter! */
stroke: #0000ff;
}
/* Make larger targets for touch users */
.interactive .touch {
stroke: #000000;
stroke-opacity: 0;
}
(pointer: coarse) {
.interactive .touch {
stroke-linecap: round;
stroke-width: 12;
}
}
`,
];
}
render() { /* eslint-disable-line class-methods-use-this */
return html``;
}
willUpdate() {
this.alignState();
}
update(changedProperties) {
super.update(changedProperties);
// Bail out if we can't get the width/height
if (Number.isNaN(this.width) || Number.isNaN(this.height) || Number.isNaN(this.rem)) {
return;
}
const hostWidth = this.width;
const hostHeight = this.height;
const hostAspectRatio = hostWidth / hostHeight;
const elementAspectRatio = 1.8;
let elementWidth;
let elementHeight;
if (hostAspectRatio > elementAspectRatio) {
elementHeight = hostHeight;
elementWidth = elementHeight * elementAspectRatio;
} else {
elementWidth = hostWidth;
elementHeight = elementWidth / elementAspectRatio;
}
const margin = {
top: 2 * this.rem,
bottom: 3 * this.rem,
left: 3 * this.rem,
right: 2 * this.rem,
};
const height = elementHeight - (margin.top + margin.bottom);
const width = elementWidth - (margin.left + margin.right);
const transitionDuration = parseInt(this.getComputedStyleValue('---transition-duration'), 10);
// X Scale
const xScale = d3.scaleLinear()
.domain([this.scale.time.min, this.scale.time.max])
.range([0, width]);
// Y Scale
const yScale = d3.scaleLinear()
.domain([this.scale.value.min, this.scale.value.max])
.range([height, 0]);
// Line for time/value space
const line = d3.line()
.x((datum) => { return xScale(datum.d); })
.y((datum) => { return yScale(datum.v); });
// Svg
// DATA-JOIN
const svgUpdate = d3.select(this.renderRoot).selectAll('.main')
.data([{
width: this.width,
height: this.height,
rem: this.rem,
}]);
// ENTER
const svgEnter = svgUpdate.enter().append('svg')
.classed('main', true);
svgEnter.html(DiscountableElement.svgDefs);
// MERGE
const svgMerge = svgEnter.merge(svgUpdate)
.attr('viewBox', `0 0 ${elementWidth} ${elementHeight}`);
// Plot
// ENTER
const plotEnter = svgEnter.append('g')
.classed('plot', true);
// MERGE
const plotMerge = svgMerge.select('.plot')
.attr('transform', `translate(${margin.left}, ${margin.top})`);
// Clippath
// ENTER
plotEnter.append('clipPath')
.attr('id', 'clip-htd-curves')
.append('rect');
// MERGE
plotMerge.select('clipPath rect')
.attr('height', height + 1)
.attr('width', width + 1);
// Underlayer
// ENTER
const underlayerEnter = plotEnter.append('g')
.classed('underlayer', true);
// MERGE
const underlayerMerge = plotMerge.select('.underlayer');
// Background
// ENTER
underlayerEnter.append('rect')
.classed('background', true);
// MERGE
underlayerMerge.select('.background')
.attr('height', height)
.attr('width', width);
// X Axis
// ENTER
underlayerEnter.append('g')
.classed('axis-x', true);
// MERGE
const scaleXMerge = underlayerMerge.select('.axis-x')
.attr('transform', `translate(0, ${yScale(0)})`);
const scaleXTransition = scaleXMerge.transition()
.duration(transitionDuration * 2) // Extra long transition!
.ease(d3.easeCubicOut)
.call(d3.axisBottom(xScale))
.attr('font-size', null)
.attr('font-family', null);
scaleXTransition.selectAll('line, path')
.attr('stroke', null);
// X Axis Title
// ENTER
const titleXEnter = underlayerEnter.append('text')
.classed('title-x', true)
.attr('text-anchor', 'middle');
titleXEnter.append('tspan')
.classed('name', true)
.text('Delay (');
titleXEnter.append('tspan')
.classed('math-var d', true)
.text('D');
titleXEnter.append('tspan')
.classed('name', true)
.text(')');
// MERGE
underlayerMerge.select('.title-x')
.attr('transform', `translate(${(width / 2)}, ${(height + (2.25 * this.rem))})`);
// Y Axis
// ENTER
underlayerEnter.append('g')
.classed('axis-y', true);
// MERGE
const scaleYTransition = underlayerMerge.select('.axis-y').transition()
.duration(transitionDuration * 2) // Extra long transition!
.ease(d3.easeCubicOut)
.call(d3.axisLeft(yScale))
.attr('font-size', null)
.attr('font-family', null);
scaleYTransition.selectAll('line, path')
.attr('stroke', null);
// Y Axis Title
// ENTER
const titleYEnter = underlayerEnter.append('text')
.classed('title-y', true)
.attr('text-anchor', 'middle');
titleYEnter.append('tspan')
.classed('name', true)
.text('Value (');
titleYEnter.append('tspan')
.classed('math-var v', true)
.text('V');
titleYEnter.append('tspan')
.classed('name', true)
.text(')');
// MERGE
underlayerMerge.select('.title-y')
.attr('transform', `translate(${-2 * this.rem}, ${(height / 2)})rotate(-90)`);
// Content
// ENTER
plotEnter.append('g')
.classed('content', true);
// MERGE
const contentMerge = plotMerge.select('.content');
// Options
// DATA-JOIN
const optionUpdate = contentMerge.selectAll('.option')
.data(
this.options.filter((option) => { return ((option.a !== null) && (option.d !== null)); }),
(datum) => { return datum.name; },
);
// ENTER
const optionEnter = optionUpdate.enter().append('g')
.classed('option', true);
// Curve
const curveEnter = optionEnter.append('g')
.classed('curve', true)
.attr('clip-path', 'url(#clip-htd-curves)');
curveEnter.append('path')
.classed('path', true)
.attr('d', (datum) => {
const curve = d3.range(xScale(datum.d), xScale(0), -1).map((range) => {
return {
d: xScale.invert(range),
v: HTDMath.adk2v(
datum.a,
datum.d - xScale.invert(range),
this.k,
),
};
});
return line(curve);
})
.attr('stroke-dasharray', (datum, index, nodes) => {
if (datum.trial) {
const length = nodes[index].getTotalLength();
return `0,${length}`;
}
return 'none';
});
curveEnter.append('path')
.classed('path touch', true)
.attr('d', (datum) => {
const curve = d3.range(xScale(datum.d), xScale(0), -1).map((range) => {
return {
d: xScale.invert(range),
v: HTDMath.adk2v(
datum.a,
datum.d - xScale.invert(range),
this.k,
),
};
});
return line(curve);
})
.attr('stroke-dasharray', (datum, index, nodes) => {
if (datum.trial) {
const length = nodes[index].getTotalLength();
return `0,${length}`;
}
return 'none';
});
// Bar
const barEnter = optionEnter.append('g')
.classed('bar', true);
barEnter.append('line')
.classed('line', true)
.attr('x1', (datum) => { return xScale(datum.d); })
.attr('x2', (datum) => { return xScale(datum.d); })
.attr('y1', yScale(0))
.attr('y2', (datum) => { return yScale(datum.a); })
.attr('stroke-dasharray', (datum, index, nodes) => {
if (datum.trial) {
const length = nodes[index].getTotalLength();
return `0,${length}`;
}
return 'none';
});
barEnter.append('line')
.classed('line touch', true)
.attr('x1', (datum) => { return xScale(datum.d); })
.attr('x2', (datum) => { return xScale(datum.d); })
.attr('y1', yScale(0))
.attr('y2', (datum) => { return yScale(datum.a); })
.attr('stroke-dasharray', (datum, index, nodes) => {
if (datum.trial) {
const length = nodes[index].getTotalLength();
return `0,${length}`;
}
return 'none';
});
// Point
const pointEnter = optionEnter.append('g')
.classed('point', true)
.attr('transform', (datum) => {
return `translate(${xScale(datum.d)}, ${yScale(datum.a)})`;
})
.attr('opacity', (datum) => {
if (datum.trial) {
return 0;
}
return 1;
});
pointEnter.append('circle')
.classed('mark touch', true);
pointEnter.append('text')
.classed('label', true);
// MERGE
const optionMerge = optionEnter.merge(optionUpdate);
// Interactive options
// Curve
optionMerge
.filter((datum, index, nodes) => {
return (this.interactive && !d3.select(nodes[index]).select('.curve').classed('interactive'));
})
.select('.curve')
.classed('interactive', true)
.attr('tabindex', 0)
// Drag interaction
.call(d3.drag()
.subject((event) => {
return {
x: event.x,
y: event.y,
};
})
.on('start', (event) => {
const element = event.currentTarget;
d3.select(element).classed('dragging', true);
})
.on('drag', (event, datum) => {
this.drag = true;
const dragD = datum.d - xScale.invert(event.x);
const d = (dragD < 0)
? 0
: (dragD > datum.d)
? datum.d
: dragD;
const dragV = yScale.invert(event.y);
const v = (dragV <= 0)
? 0.001
: (dragV > datum.a)
? datum.a
: dragV;
const k = HTDMath.adv2k(datum.a, d, v);
this.k = (k < HTDMath.k.MIN)
? HTDMath.k.MIN
: (k > HTDMath.k.MAX)
? HTDMath.k.MAX
: k;
this.alignState();
this.requestUpdate();
this.dispatchEvent(new CustomEvent('htd-curves-change', {
detail: {
name: datum.name,
a: datum.a,
d: datum.d,
k: this.k,
label: datum.label,
},
bubbles: true,
}));
})
.on('end', (event) => {
const element = event.currentTarget;
d3.select(element).classed('dragging', false);
}))
// Keyboard interaction
.on('keydown', (event, datum) => {
if (['ArrowUp', 'ArrowDown', 'ArrowRight', 'ArrowLeft'].includes(event.key)) {
let {k} = this;
switch (event.key) {
case 'ArrowUp':
case 'ArrowLeft':
k *= event.shiftKey ? 0.95 : 0.85;
break;
case 'ArrowDown':
case 'ArrowRight':
k *= event.shiftKey ? (1 / 0.95) : (1 / 0.85);
break;
default:
// no-op
}
k = (k < HTDMath.k.MIN)
? HTDMath.k.MIN
: (k > HTDMath.k.MAX)
? HTDMath.k.MAX
: k;
if (k !== this.k) {
this.k = k;
this.alignState();
this.requestUpdate();
this.dispatchEvent(new CustomEvent('htd-curves-change', {
detail: {
name: datum.name,
a: datum.a,
d: datum.d,
k: this.k,
label: datum.label,
},
bubbles: true,
}));
}
event.preventDefault();
}
});
// Bar
optionMerge
.filter((datum, index, nodes) => {
return (this.interactive && !datum.trial && !d3.select(nodes[index]).select('.bar').classed('interactive'));
})
.select('.bar')
.classed('interactive', true)
.attr('tabindex', 0)
// Drag interaction
.call(d3.drag()
.subject((event, datum) => {
return {
x: xScale(datum.d),
y: yScale(datum.a),
};
})
.on('start', (event) => {
const element = event.currentTarget;
d3.select(element).classed('dragging', true);
})
.on('drag', (event, datum) => {
this.drag = true;
const d = xScale.invert(event.x);
datum.d = (d < this.scale.time.min)
? this.scale.time.min
: (d > this.scale.time.max)
? this.scale.time.max
: this.scale.time.round(d);
if (datum.name === 'default') {
this.d = datum.d;
}
this.alignState();
this.requestUpdate();
this.dispatchEvent(new CustomEvent('htd-curves-change', {
detail: {
name: datum.name,
a: datum.a,
d: datum.d,
k: this.k,
label: datum.label,
},
bubbles: true,
}));
})
.on('end', (event) => {
const element = event.currentTarget;
d3.select(element).classed('dragging', false);
}))
// Keyboard interaction
.on('keydown', (event, datum) => {
if (['ArrowLeft', 'ArrowRight'].includes(event.key)) {
let keyD = datum.d;
switch (event.key) {
case 'ArrowRight':
keyD += event.shiftKey ? 1 : 5;
break;
case 'ArrowLeft':
keyD -= event.shiftKey ? 1 : 5;
break;
default:
// no-op
}
keyD = (keyD < this.scale.time.min)
? this.scale.time.min
: ((keyD > this.scale.time.max)
? this.scale.time.max
: keyD);
if (keyD !== datum.d) {
datum.d = keyD;
if (datum.name === 'default') {
this.d = datum.d;
}
this.alignState();
this.requestUpdate();
this.dispatchEvent(new CustomEvent('htd-curves-change', {
detail: {
name: datum.name,
a: datum.a,
d: datum.d,
k: this.k,
label: datum.label,
},
bubbles: true,
}));
}
event.preventDefault();
}
});
// Point
optionMerge
.filter((datum, index, nodes) => {
return (this.interactive && !datum.trial && !d3.select(nodes[index]).select('.point').classed('interactive'));
})
.select('.point')
.classed('interactive', true)
.attr('tabindex', 0)
// Drag interaction
.call(d3.drag()
.subject((event, datum) => {
return {
x: xScale(datum.d),
y: yScale(datum.a),
};
})
.on('start', (event) => {
const element = event.currentTarget;
d3.select(element).classed('dragging', true);
})
.on('drag', (event, datum) => {
this.drag = true;
const a = yScale.invert(event.y);
datum.a = (a < this.scale.value.min)
? this.scale.value.min
: (a > this.scale.value.max)
? this.scale.value.max
: this.scale.value.round(a);
if (datum.name === 'default') {
this.a = datum.a;
}
this.alignState();
this.requestUpdate();
this.dispatchEvent(new CustomEvent('htd-curves-change', {
detail: {
name: datum.name,
a: datum.a,
d: datum.d,
k: this.k,
label: datum.label,
},
bubbles: true,
}));
})
.on('end', (event) => {
const element = event.currentTarget;
d3.select(element).classed('dragging', false);
}))
// Keyboard interaction
.on('keydown', (event, datum) => {
if (['ArrowUp', 'ArrowDown'].includes(event.key)) {
let keyA = datum.a;
switch (event.key) {
case 'ArrowUp':
keyA += event.shiftKey ? 1 : 5;
break;
case 'ArrowDown':
keyA -= event.shiftKey ? 1 : 5;
break;
default:
// no-op
}
keyA = (keyA < this.scale.value.min)
? this.scale.value.min
: ((keyA > this.scale.value.max)
? this.scale.value.max
: keyA);
if (keyA !== datum.a) {
datum.a = keyA;
if (datum.name === 'default') {
this.a = datum.a;
}
this.alignState();
this.requestUpdate();
this.dispatchEvent(new CustomEvent('htd-curves-change', {
detail: {
name: datum.name,
a: datum.a,
d: datum.d,
k: this.k,
label: datum.label,
},
bubbles: true,
}));
}
event.preventDefault();
}
});
// Non-interactive options
// Curve
optionMerge
.filter((datum, index, nodes) => {
return (!this.interactive && d3.select(nodes[index]).select('.curve').classed('interactive'));
})
.select('.curve')
.classed('interactive', false)
.attr('tabindex', null)
.on('drag', null)
.on('keydown', null);
// Bar
optionMerge
.filter((datum, index, nodes) => {
return ((!this.interactive || datum.trial) && d3.select(nodes[index]).select('.bar').classed('interactive'));
})
.select('.bar')
.classed('interactive', false)
.attr('tabindex', null)
.on('drag', null)
.on('keydown', null);
// Point
optionMerge
.filter((datum, index, nodes) => {
return ((!this.interactive || datum.trial) && d3.select(nodes[index]).select('.point').classed('interactive'));
})
.select('.point')
.classed('interactive', false)
.attr('tabindex', null)
.on('drag', null)
.on('keydown', null);
// Trial Animation
// Curve
optionMerge
.filter((datum) => {
return (datum.new);
})
.select('.curve .path').transition()
.duration(transitionDuration)
.delay(transitionDuration + transitionDuration / 10)
.ease(d3.easeLinear)
.attrTween('stroke-dasharray', (datum, index, nodes) => {
const length = nodes[index].getTotalLength();
return d3.interpolate(`0,${length}`, `${length},${0}`);
})
.on('end', (datum) => {
datum.new = false;
this.dispatchEvent(new CustomEvent('discountable-response', {
detail: {
trial: this.trialCount,
as: this.as,
ds: this.ds,
al: this.al,
dl: this.dl,
response: this.response,
},
bubbles: true,
}));
});
optionMerge
.filter((datum) => {
return (datum.new);
})
.select('.curve .path.touch').transition()
.duration(transitionDuration)
.delay(transitionDuration + transitionDuration / 10)
.ease(d3.easeLinear)
.attrTween('stroke-dasharray', (datum, index, nodes) => {
const length = nodes[index].getTotalLength();
return d3.interpolate(`0,${length}`, `${length},${0}`);
});
// Bar
optionMerge
.filter((datum) => {
return (datum.new);
})
.select('.bar .line').transition()
.duration(transitionDuration)
.ease(d3.easeLinear)
.attrTween('stroke-dasharray', (datum, index, nodes) => {
const length = nodes[index].getTotalLength();
return d3.interpolate(`0,${length}`, `${length},${length}`);
});
optionMerge
.filter((datum) => {
return (datum.new);
})
.select('.bar .line.touch').transition()
.duration(transitionDuration)
.ease(d3.easeLinear)
.attrTween('stroke-dasharray', (datum, index, nodes) => {
const length = nodes[index].getTotalLength();
return d3.interpolate(`0,${length}`, `${length},${length}`);
});
// Point
optionMerge
.filter((datum) => {
return (datum.new);
})
.select('.point').transition()
.duration(transitionDuration / 10)
.delay(transitionDuration)
.ease(d3.easeLinear)
.attrTween('opacity', () => { return d3.interpolate(0, 1); });
// All options
optionUpdate.select('.curve .path').transition()
.duration(this.drag
? 0
: (this.firstUpdate
? (transitionDuration * 2)
: transitionDuration))
.ease(d3.easeCubicOut)
.attrTween('d', (datum, index, elements) => {
const element = elements[index];
const interpolateA = d3.interpolate(
(element.a !== undefined) ? element.a : datum.a,
datum.a,
);
const interpolateD = d3.interpolate(
(element.d !== undefined) ? element.d : datum.d,
datum.d,
);
return (time) => {
element.a = interpolateA(time);
element.d = interpolateD(time);
const curve = d3.range(xScale(element.d), xScale(0), -1).map((range) => {
return {
d: xScale.invert(range),
v: HTDMath.adk2v(
element.a,
element.d - xScale.invert(range),
this.k,
),
};
});
return line(curve);
};
});
optionUpdate.select('.curve .path.touch').transition()
.duration(this.drag
? 0
: (this.firstUpdate
? (transitionDuration * 2)
: transitionDuration))
.ease(d3.easeCubicOut)
.attrTween('d', (datum, index, elements) => {
const element = elements[index];
const interpolateA = d3.interpolate(
(element.a !== undefined) ? element.a : datum.a,
datum.a,
);
const interpolateD = d3.interpolate(
(element.d !== undefined) ? element.d : datum.d,
datum.d,
);
return (time) => {
element.a = interpolateA(time);
element.d = interpolateD(time);
const curve = d3.range(xScale(element.d), xScale(0), -1).map((range) => {
return {
d: xScale.invert(range),
v: HTDMath.adk2v(
element.a,
element.d - xScale.invert(range),
this.k,
),
};
});
return line(curve);
};
});
optionUpdate.select('.bar .line').transition()
.duration(this.drag
? 0
: (this.firstUpdate
? (transitionDuration * 2)
: transitionDuration))
.ease(d3.easeCubicOut)
.attrTween('x1', (datum, index, elements) => {
const element = elements[index];
const interpolateD = d3.interpolate(
(element.d !== undefined) ? element.d : datum.d,
datum.d,
);
return (time) => {
element.d = interpolateD(time);
return `${xScale(element.d)}`;
};
})
.attrTween('x2', (datum, index, elements) => {
const element = elements[index];
const interpolateD = d3.interpolate(
(element.d !== undefined) ? element.d : datum.d,
datum.d,
);
return (time) => {
element.d = interpolateD(time);
return `${xScale(element.d)}`;
};
})
.attrTween('y2', (datum, index, elements) => {
const element = elements[index];
const interpolateA = d3.interpolate(
(element.a !== undefined) ? element.a : datum.a,
datum.a,
);
return (time) => {
element.a = interpolateA(time);
return `${yScale(element.a)}`;
};
});
optionUpdate.select('.bar .line.touch').transition()
.duration(this.drag
? 0
: (this.firstUpdate
? (transitionDuration * 2)
: transitionDuration))
.ease(d3.easeCubicOut)
.attrTween('x1', (datum, index, elements) => {
const element = elements[index];
const interpolateD = d3.interpolate(
(element.d !== undefined) ? element.d : datum.d,
datum.d,
);
return (time) => {
element.d = interpolateD(time);
return `${xScale(element.d)}`;
};
})
.attrTween('x2', (datum, index, elements) => {
const element = elements[index];
const interpolateD = d3.interpolate(
(element.d !== undefined) ? element.d : datum.d,
datum.d,
);
return (time) => {
element.d = interpolateD(time);
return `${xScale(element.d)}`;
};
})
.attrTween('y2', (datum, index, elements) => {
const element = elements[index];
const interpolateA = d3.interpolate(
(element.a !== undefined) ? element.a : datum.a,
datum.a,
);
return (time) => {
element.a = interpolateA(time);
return `${yScale(element.a)}`;
};
});
optionUpdate.select('.point').transition()
.duration(this.drag
? 0
: (this.firstUpdate
? (transitionDuration * 2)
: transitionDuration))
.ease(d3.easeCubicOut)
.attrTween('transform', (datum, index, elements) => {
const element = elements[index];
const interpolateD = d3.interpolate(
(element.d !== undefined) ? element.d : datum.d,
datum.d,
);
const interpolateA = d3.interpolate(
(element.a !== undefined) ? element.a : datum.a,
datum.a,
);
return (time) => {
element.d = interpolateD(time);
element.a = interpolateA(time);
return `translate(${xScale(element.d)}, ${yScale(element.a)})`;
};
});
optionMerge.select('.point .label')
.text((datum) => { return datum.label; });
// EXIT
// NOTE: Could add a transition here
optionUpdate.exit().remove();
this.drag = false;
this.firstUpdate = false;
}
}
customElements.define('htd-curves', HTDCurves);