Last active
November 11, 2015 10:49
-
-
Save martinVelicky/d19f76572243c774557f to your computer and use it in GitHub Desktop.
HeartBeat using D3.js
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> | |
<html> | |
<head> | |
<title>HeartBeat</title> | |
<script src="//d3js.org/d3.v3.min.js"></script> | |
<style> | |
#svg-wrapper { | |
width: 500px; | |
height: 160px; | |
margin: 2em auto; | |
} | |
svg path { | |
fill: none; | |
stroke: #000; | |
stroke-width: 1.5px; | |
} | |
svg .axis { | |
font-size: 12px; | |
} | |
svg .axis path { | |
display: none; | |
} | |
</style> | |
<script type="text/javascript"> | |
window.onload = function() { | |
var svg = null; | |
var circle = null; | |
var circleTransition = null; | |
var latestBeat = null; | |
var insideBeat = false; | |
var data = []; | |
var SECONDS_SAMPLE = 5; | |
var BEAT_TIME = 400; | |
var TICK_FREQUENCY = SECONDS_SAMPLE * 1000 / BEAT_TIME; | |
var BEAT_VALUES = [0, 0, 3, -4, 10, -7, 3, 0, 0]; | |
var CIRCLE_FULL_RADIUS = 40; | |
var MAX_LATENCY = 5000; | |
var colorScale = d3.scale.linear() | |
.domain([BEAT_TIME, (MAX_LATENCY - BEAT_TIME) / 2, MAX_LATENCY]) | |
.range(["#6D9521", "#D77900", "#CD3333"]); | |
var radiusScale = d3.scale.linear() | |
.range([5, CIRCLE_FULL_RADIUS]) | |
.domain([MAX_LATENCY, BEAT_TIME]); | |
function beat() { | |
if (insideBeat) return; | |
insideBeat = true; | |
var now = new Date(); | |
var nowTime = now.getTime(); | |
if (data.length > 0 && data[data.length - 1].date > now) { | |
data.splice(data.length - 1, 1); | |
} | |
data.push({ | |
date: now, | |
value: 0 | |
}); | |
var step = BEAT_TIME / BEAT_VALUES.length - 2; | |
for (var i = 1; i < BEAT_VALUES.length; i++) { | |
data.push({ | |
date: new Date(nowTime + i * step), | |
value: BEAT_VALUES[i] | |
}); | |
} | |
latestBeat = now; | |
circleTransition = circle.transition() | |
.duration(BEAT_TIME) | |
.attr("r", CIRCLE_FULL_RADIUS) | |
.attr("fill", "#6D9521"); | |
setTimeout(function() { | |
insideBeat = false; | |
}, BEAT_TIME); | |
} | |
var svgWrapper = document.getElementById("svg-wrapper"); | |
var margin = {left: 10, top: 10, right: CIRCLE_FULL_RADIUS * 3, bottom: 10}, | |
width = svgWrapper.offsetWidth - margin.left - margin.right, | |
height = svgWrapper.offsetHeight - margin.top - margin.bottom; | |
// create SVG | |
svg = d3.select('#svg-wrapper').append("svg") | |
.attr("width", width + margin.left + margin.right) | |
.attr("height", height + margin.bottom + margin.top) | |
.append("g") | |
.attr("transform", "translate(" + margin.left + "," + margin.top + ")"); | |
circle = svg | |
.append("circle") | |
.attr("fill", "#6D9521") | |
.attr("cx", width + margin.right / 2) | |
.attr("cy", height / 2) | |
.attr("r", CIRCLE_FULL_RADIUS); | |
// init scales | |
var now = new Date(), | |
fromDate = new Date(now.getTime() - SECONDS_SAMPLE * 1000); | |
// create initial set of data | |
data.push({ | |
date: now, | |
value: 0 | |
}); | |
var x = d3.time.scale() | |
.domain([fromDate, new Date(now.getTime())]) | |
.range([0, width]), | |
y = d3.scale.linear() | |
.domain([-10, 10]) | |
.range([height, 0]); | |
var line = d3.svg.line() | |
.interpolate("basis") | |
.x(function(d) { | |
return x(d.date); | |
}) | |
.y(function(d) { | |
return y(d.value); | |
}); | |
var xAxis = d3.svg.axis() | |
.scale(x) | |
.orient("bottom") | |
.ticks(d3.time.seconds, 1) | |
.tickFormat(function(d) { | |
var seconds = d.getSeconds() === 0 ? "00" : d.getSeconds(); | |
return seconds % 10 === 0 ? d.getMinutes() + ":" + seconds : ":" + seconds; | |
}); | |
// add clipPath | |
svg.append("defs").append("clipPath") | |
.attr("id", "clip") | |
.append("rect") | |
.attr("width", width) | |
.attr("height", height); | |
var axis = d3.select("svg").append("g") | |
.attr("class", "axis") | |
.attr("transform", "translate(0," + height + ")") | |
.call(xAxis); | |
var path = svg.append("g") | |
.attr("clip-path", "url(#clip)") | |
.append("path") | |
.attr("class", "line"); | |
svg.select(".line") | |
.attr("d", line(data)); | |
var transition = d3.select("path").transition() | |
.duration(100) | |
.ease("linear"); | |
(function tick() { | |
transition = transition.each(function() { | |
// update the domains | |
now = new Date(); | |
fromDate = new Date(now.getTime() - SECONDS_SAMPLE * 1000); | |
x.domain([fromDate, new Date(now.getTime() - 100)]); | |
var translateTo = x(new Date(fromDate.getTime()) - 100); | |
// redraw the line | |
svg.select(".line") | |
.attr("d", line(data)) | |
.attr("transform", null) | |
.transition() | |
.attr("transform", "translate(" + translateTo + ")"); | |
// slide the x-axis left | |
axis.call(xAxis); | |
}).transition().each("start", tick); | |
})(); | |
setInterval(function() { | |
now = new Date(); | |
fromDate = new Date(now.getTime() - SECONDS_SAMPLE * 1000); | |
for (var i = 0; i < data.length; i++) { | |
if (data[i].date < fromDate) { | |
data.shift(); | |
} else { | |
break; | |
} | |
} | |
if (insideBeat) return; | |
data.push({ | |
date: now, | |
value: 0 | |
}); | |
if (circleTransition != null) { | |
var diff = now.getTime() - latestBeat.getTime(); | |
if (diff < MAX_LATENCY) { | |
circleTransition = circle.transition() | |
.duration(TICK_FREQUENCY) | |
.attr("r", radiusScale(diff)) | |
.attr("fill", colorScale(diff)); | |
} | |
} | |
}, TICK_FREQUENCY); | |
setInterval(function() { | |
beat(); | |
}, 2000); | |
beat(); | |
}; | |
</script> | |
<body> | |
<div id="svg-wrapper"></div> | |
</body> | |
</html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Demo at http://bl.ocks.org/velickym/d19f76572243c774557f