An area chart with randomized data used to expirement with interpolations and easing functions.
Last active
September 18, 2015 19:10
-
-
Save matt-mcdaniel/f1cda1ba12f9904e65a0 to your computer and use it in GitHub Desktop.
Interpolation and Area Charts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<head> | |
<meta charset="utf-8"> | |
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.5/d3.min.js"></script> | |
<style> | |
body { margin:0;position:fixed;top:0;right:0;bottom:0;left:0; } | |
svg { width: 100%; height: 100%; } | |
</style> | |
<link href="styles.css" rel="stylesheet" type="text/css" /> | |
</head> | |
<body> | |
<div class="button-container"> | |
<button onclick="changeData()"> | |
Update Data | |
</button> | |
<p>Interpolator</p> | |
<button class="interpolate-button" onclick="changeInterpolation()"></button> | |
<div class="ease-container"> | |
<p>Easing Function</p> | |
<button class="ease-button" onclick="changeEasing()"></button> | |
<div class="radio"> | |
<input name="ease-type" class="ease-suffix" type="radio" value="in"> | |
<label>in</label> | |
<input name="ease-type" class="ease-suffix" type="radio" value="out"> | |
<label>out</label> | |
<input name="ease-type" class="ease-suffix" type="radio" value="in-out"> | |
<label>in-out</label> | |
</div> | |
</div> | |
<p>Duration</p> | |
<input type="range" class="duration" value="400" min="0" max="1200"/> | |
<label class="dur"></label> | |
</div> | |
<figure> | |
<svg></svg> | |
</figure> | |
<script> | |
var eases = ['linear', 'poly(2)', 'cubic', 'sin', 'exp', 'circle', 'elastic', 'back', 'bounce']; | |
var interps = ['linear', 'linear-closed', 'step', 'step-before', 'step-after', 'basis', 'basis-open', 'basis-closed', 'bundle', 'cardinal', 'cardinal-open', 'cardinal-closed', 'monotone']; | |
var ease = 'back', | |
easeCount = eases.indexOf(ease), | |
interp = 'cardinal', | |
interpCount = interps.indexOf(interp), | |
duration = 600; | |
// tooltip dimensions | |
var tt ={ w: 80, h: 50 }; | |
// generate array of random numbers | |
function generate(min, max) { | |
var arr = []; | |
for (var i = 0; i < 6; i++) { | |
var rand = Math.random() * (max-min) + min; | |
var rounded = Math.round(rand * 100)/100; | |
arr.push(rounded); | |
} | |
// add endpoints | |
var startPoint = arr[0] - 0.2; | |
var endPoint = arr[arr.length - 1] - 0.2; | |
arr.unshift(startPoint); | |
arr.push(endPoint); | |
return arr; | |
}; | |
var margin = { | |
top: 20, | |
right: 40, | |
bottom: 40, | |
left: 40 | |
} | |
var data = [ | |
{ | |
name: 'state', | |
bg: '#C2E3F9', | |
stroke: '#0362AD', | |
opacity: 0.5, | |
data: [2.2, 2.4, 3.9, 4.6, 2.19, 4.5, 3.45, 3.25] | |
}, | |
{ | |
name: 'national', | |
bg: '#F7CDCD', | |
stroke: '#D9534F', | |
opacity: 0.5, | |
data: [3.6, 3.8, 3.15, 3.4, 2.93, 4.3, 4.1, 3.9] | |
}, | |
{ | |
name: 'facility', | |
bg: '#F9E0B2', | |
stroke: '#F0A611', | |
opacity: 0.84, | |
data: [2.8, 3, 3.5, 4.2, 2.35, 4.8, 2.9, 2.7] | |
}, | |
] | |
// ** Updates ** // | |
// update data | |
function changeData() { | |
for (var obj in data) { | |
var updated = generate(2, 4.8); | |
data[obj]['data'] = updated; | |
} | |
update(); | |
} | |
// change easing | |
function changeEasing() { | |
d3.selectAll('.ease-suffix').property('checked', false); | |
easeCount = ++easeCount % eases.length; | |
ease = eases[easeCount]; | |
d3.select('.ease-button').html(ease); | |
update(); | |
} | |
// easing suffix | |
d3.selectAll('.ease-suffix').on('click', function() { | |
console.info(this.value); | |
ease = eases[easeCount] + '-' + this.value; | |
d3.select('.ease-button').html(ease); | |
update(); | |
console.info(ease); | |
}); | |
// change interpolation | |
function changeInterpolation() { | |
interpCount = ++interpCount % interps.length; | |
interp = interps[interpCount]; | |
d3.select('.interpolate-button').html(interp); | |
update(); | |
} | |
d3.select('.duration').on('input', function() { | |
duration = this.value; | |
d3.select('.dur').html(duration + 'ms'); | |
update(); | |
}) | |
// ** DOM Elements ** // | |
d3.select('.ease-button').html(ease); | |
d3.select('.interpolate-button').html(interp); | |
d3.select('.dur').html(duration + ' ms'); | |
var w = d3.select('figure').node().clientWidth - margin.left - margin.right; | |
var h = d3.select('figure').node().clientHeight - margin.top - margin.bottom; | |
var x = d3.scale.ordinal() | |
.domain([0,1,2,3,4,5,6, 7]) | |
.rangePoints([0, w], -1.5); | |
var y = d3.scale.linear() | |
.domain([0, 5]) | |
.range([h, 0]); | |
var xAxis = d3.svg.axis() | |
.scale(x) | |
.orient('bottom') | |
.tickPadding([20]) | |
.tickSize(0, 0) | |
.tickValues([1,2,3,4,5,6]); | |
var yAxis = d3.svg.axis() | |
.scale(y) | |
.orient('left') | |
.tickSize(-(w), 0) | |
.tickPadding([20]) | |
.tickFormat(d3.format('r')) | |
.tickValues([1, 2, 3, 4, 5]); | |
// Define the div for the tooltip | |
var tooltip = d3.select('figure').append('div') | |
.classed('tooltip', true) | |
.style({ | |
opacity: 0, | |
width: tt.w + 'px', | |
height: tt.h + 'px' | |
}); | |
// Tooltip Text | |
tooltip.append('div') | |
.classed('text', true); | |
// Triangle after tooltip | |
tooltip.append('div') | |
.classed('triangle', true) | |
.style({ | |
bottom: ( (tt.h/4) * -1 ) + 'px', | |
left: (tt.w / 2) - (tt.h/3) + 'px', | |
'border-top': (tt.h/3) + 'px solid white', | |
'border-right': (tt.h/3) + 'px solid transparent', | |
'border-left': (tt.h/3) + 'px solid transparent' | |
}); | |
var clip = d3.select('svg').append('defs').append('clipPath') | |
.attr('id', 'clip') | |
.append('rect') | |
.attr({ | |
id: 'clip-rect', | |
x: 0, | |
y: 0, | |
width: w, | |
height: h | |
}); | |
var svg = d3.select('svg') | |
.attr('width', w + margin.left + margin.right) | |
.attr('height', h + margin.top + margin.bottom); | |
// X Axis | |
svg.append('g') | |
.attr('class', 'x axis') | |
.attr('transform', 'translate(' + margin.left + ',' + parseInt(h + margin.top) + ')') | |
.call(xAxis) | |
// Y Axis | |
svg.append('g') | |
.attr('class', 'y axis') | |
.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')') | |
.call(yAxis); | |
var g = svg.append('g') | |
.attr('clip-path', 'url(#clip)') | |
.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')'); | |
var area = d3.svg.area() | |
.interpolate(interp) | |
.x(function(d, i) { return x(i); }) | |
.y0(h) | |
.y1(function(d) { return y(d); }); | |
var stacks = g.selectAll('.stacks') | |
.data(data) | |
.enter().append('g') | |
.classed('stacks', true); | |
stacks.append('path') | |
.attr({ | |
fill: function(d) { return d.bg; }, | |
stroke: function(d) { return d.stroke; }, | |
'stroke-width': 2, | |
opacity: function(d) { return d.opacity; } | |
}) | |
.attr('d', function(d) { return area(d.data) }); | |
var lines = stacks.selectAll('line') | |
.data(function(d) { return d.data; }) | |
.enter().append('line') | |
.attr({ | |
x1: function(d,i) { | |
return x(i) | |
}, | |
y1: h, | |
x2: function(d,i) { | |
return x(i) | |
}, | |
y2: function(d) { | |
return y(d) | |
} | |
}) | |
.each(function(d, i) { | |
var parent = this.parentNode.__data__; | |
d3.select(this) | |
.attr({ | |
stroke: parent.stroke, | |
'stroke-width': 2, | |
opacity: function(d, i) { | |
if (parent.name === 'facility') { | |
return 1; | |
} else { | |
return 0; | |
} | |
} | |
}); | |
}); | |
var circles = stacks.selectAll('circle') | |
.data(function(d) { return d.data; }) | |
.enter().append('circle') | |
.attr({ | |
r: 10, | |
'stroke-width': 2, | |
cx: function(d, i) { | |
return x(i); | |
}, | |
cy: function(d) { | |
return y(d); | |
}, | |
opacity: function(d, i) { | |
if (!i || i === data[0].data.length -1) { | |
return 0; | |
} else { | |
return 1; | |
} | |
} | |
}) | |
.each(function() { | |
var parent = this.parentNode.__data__; | |
this._parentAttrs = parent; | |
d3.select(this) | |
.attr({ | |
fill: parent.bg, | |
stroke: parent.stroke, | |
'stroke-width': 2 | |
}); | |
}); | |
// Tooltip on hover | |
circles | |
.on('mousemove', function() { | |
d3.select(this).transition().duration(50).attr('r', 13); | |
var parent = this._parentAttrs; | |
var text = d3.select(this).data()[0]; | |
tooltip.select('.text').html(text); | |
var locX = d3.event.pageX - (tt.w / 2); | |
var locY = d3.event.pageY - ( tt.h + (tt.h/1.3) ); | |
tooltip.style({ | |
opacity: 1, | |
left: locX + 'px', | |
top: locY + 'px', | |
'background-color': parent.stroke | |
}); | |
tooltip.select('.triangle').style({ | |
'border-top-color': parent.stroke | |
}); | |
}) | |
.on('mouseleave', function() { | |
d3.select(this).transition().duration(50).attr('r', 10); | |
tooltip.style({ | |
opacity: 0, | |
left: 0, | |
right: 0 | |
}); | |
}); | |
function update() { | |
area.interpolate(interp); | |
stacks = stacks.data(data); | |
stacks.select('path') | |
.transition() | |
.ease(ease) | |
.duration(duration) | |
.attr('d', function(d) { return area(d.data); }); | |
circles = stacks.selectAll('circle') | |
.data(function(d) { return d.data; }); | |
circles.transition() | |
.duration(duration) | |
.ease(ease) | |
.attr({ | |
cx: function(d, i) { return x(i); }, | |
cy: function(d) { return y(d); } | |
}); | |
lines = stacks.selectAll('line') | |
.data(function(d) { return d.data; }); | |
lines.transition() | |
.duration(duration) | |
.ease(ease) | |
.attr({ | |
x1: function(d,i) { | |
return x(i) | |
}, | |
y1: h, | |
x2: function(d,i) { | |
return x(i) | |
}, | |
y2: function(d) { | |
return y(d) | |
} | |
}) | |
} | |
</script> | |
</body> | |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
* { | |
box-sizing: border-box; | |
padding: 0; | |
margin: 0; | |
} | |
body { | |
font-family: sans-serif; | |
} | |
.button-container { | |
display: flex; | |
justify-content: center; | |
align-items: center; | |
} | |
p { | |
font-size: 12px; | |
color: #a0a0a0; | |
display: inline; | |
} | |
button { | |
position: relative; | |
padding: 1em; | |
margin: 2em 4em 2em 1em; | |
width: 9em; | |
} | |
.ease-container { | |
position: relative; | |
text-align: center; | |
} | |
.radio { | |
position: absolute; | |
right: 0; | |
bottom: 0.5em; | |
text-align: center; | |
margin-right: 3.5em; | |
bottom: 0; | |
font-size: 11px; | |
} | |
input[type='range'] { | |
margin: 2em; | |
width: 14em; | |
} | |
label { | |
margin-right: 0.5em; | |
color: #a0a0a0; | |
font-size: 80%; | |
} | |
figure { | |
margin: auto; | |
width: 900px; | |
height: 410px; | |
} | |
circle { | |
cursor: pointer; | |
} | |
.axis text { | |
fill: #a0a0a0; | |
font: 14px 'Arial'; | |
} | |
.axis path, | |
.axis line { | |
fill: none; | |
stroke: #d8d8d8; | |
stroke-width: 2; | |
shape-rendering: crispEdges; | |
} | |
.y line { | |
stroke: #d8d8d8; | |
stroke-width: 2; | |
stroke-opacity: 0.5; | |
} | |
.tooltip { | |
position: absolute; | |
text-align: center; | |
} | |
.text { | |
font-family: 'Oswald'; | |
font-weight: 400; | |
font-size: 24px; | |
margin-top: 0.25em; | |
color: white; | |
width: 100%; | |
height: 100%; | |
} | |
.triangle { | |
position: absolute; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment