Skip to content

Instantly share code, notes, and snippets.

@aminnj
Last active May 6, 2024 03:50
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save aminnj/9f581e90bfec59a112896f3eb9042cf7 to your computer and use it in GitHub Desktop.
Save aminnj/9f581e90bfec59a112896f3eb9042cf7 to your computer and use it in GitHub Desktop.
Accelerometer-based heart beat detection
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Accelerometer Data</title>
<script src="https://d3js.org/d3.v7.min.js"></script>
<style>
body {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100vh;
margin: 0;
background-color: #f0f0f0;
}
.heart {
width: 200px;
height: 200px;
position: relative;
animation: none; /* Initially no animation */
}
.heart:before,
.heart:after {
content: '';
position: absolute;
top: 0;
width: 100px;
height: 160px;
background-color: red;
border-radius: 100px 100px 0 0;
}
.heart:before {
left: 100px;
transform: rotate(-45deg);
transform-origin: 0 100%;
}
.heart:after {
left: 0;
transform: rotate(45deg);
transform-origin: 100% 100%;
}
@keyframes heartbeat {
0%, 100% {
transform: scale(1);
filter: blur(0px) drop-shadow(0 0 0.1rem crimson);
}
10% {
transform: scale(0.95);
filter: blur(0.1px) drop-shadow(0 0 0.2rem crimson);
}
40% {
transform: scale(1.3);
filter: blur(0.3px) drop-shadow(0 0 0.4rem crimson);
}
80% {
transform: scale(0.95);
filter: blur(0.1px) drop-shadow(0 0 0.1rem crimson);
}
}
.animate-heartbeat {
animation: heartbeat .4s ease-out;
}
button {
margin-top: 20px;
padding: 10px 20px;
font-size: 32px;
cursor: pointer;
}
#chart-container {
width: 500px;
height: 200px;
border: 1px solid #ccc;
}
</style>
</head>
<body>
<div id="chart-container"></div>
<div id="accelerometerData">
<p>Acceleration X: <span id="accelerationX"></span></p>
<p>Acceleration Y: <span id="accelerationY"></span></p>
<p>Acceleration Z: <span id="accelerationZ"></span></p>
</div>
<div class="heart" id="heart"></div>
<button onclick="heartbeat()">Trigger Heartbeat</button>
<div>
<h3>Statistics</h3>
<div id="statistics"></div>
</div>
<div id="lastTenData">
<h3>Last 10 Accelerometer Measurements:</h3>
<ul id="lastTenList"></ul>
</div>
<button id="permissionButton">Request Accelerometer Permission</button>
<button id="saveDataButton">Save Data as CSV</button>
<script>
let permissionButton = document.getElementById('permissionButton');
let saveDataButton = document.getElementById('saveDataButton');
let accelerometerValues = [];
let measurementCount = 0;
let lastFiredDate = Date.now();
let plotValues = Array.from({ length: 500 }, () => 0.0 );;
// Create SVG element
const svg = d3.select("#chart-container")
.append("svg")
.attr("width", 500)
.attr("height", 200);
// Create scales
const scaleX = d3.scaleLinear()
.range([0, 500]);
const scaleY = d3.scaleLinear()
.range([200, 0]);
// Create line function
const line = d3.line()
.x((_, i) => i * (500 / plotValues.length))
.y(d => scaleY(d))
.curve(d3.curveMonotoneX);
// Add path for the line
const path = svg.append("path")
.attr("class", "line")
.style("fill", "none")
.style("stroke", "black")
.style("stroke-width", 2);
// Function to update chart
function updateChart() {
plotValues = plotValues.slice(-500);
vals = plotValues;
// Update scales domain
scaleX.domain([0, vals.length - 1]);
scaleY.domain([d3.min(vals), d3.max(vals)]);
// Update line path
path.attr("d", line(vals));
// Update points color
svg.selectAll("circle").remove();
svg.selectAll("circle")
.data(vals)
.enter().append("circle")
.attr("cx", (_, i) => i * (500 / vals.length))
.attr("cy", d => scaleY(d))
.attr("r", d => d > 2.25 ? 3 : 0)
.attr("fill", d => d > 2.25 ? "red" : "black");
}
permissionButton.addEventListener('click', async () => {
try {
await requestDeviceMotionPermission();
} catch (error) {
console.error('Error requesting permission:', error);
}
});
saveDataButton.addEventListener('click', () => {
downloadCSV();
});
async function requestDeviceMotionPermission() {
if (typeof DeviceMotionEvent.requestPermission === 'function') {
let permission = await DeviceMotionEvent.requestPermission();
if (permission === 'granted') {
startAccelerometer();
} else {
console.error('Permission denied');
}
} else {
console.error('DeviceMotionEvent.requestPermission is not supported');
}
}
function startAccelerometer() {
window.addEventListener("devicemotion", handleMotionEvent, true);
}
function handleMotionEvent(event) {
var acceleration = event.acceleration;
if (acceleration) {
let timestamp = performance.now();
let data = {
x: acceleration.x.toFixed(4),
y: acceleration.y.toFixed(4),
z: acceleration.z.toFixed(4),
t: timestamp,
t_utc: Date.now()
};
accelerometerValues.push(data);
measurementCount++;
if (measurementCount % 3 == 0) {
updateDisplay(acceleration);
updateChart();
displayLastTenValues();
displayStatistics();
}
}
}
function updateDisplay(acceleration) {
document.getElementById("accelerationX").innerText = acceleration.x.toFixed(2);
document.getElementById("accelerationY").innerText = acceleration.y.toFixed(2);
document.getElementById("accelerationZ").innerText = acceleration.z.toFixed(2);
}
function displayStatistics() {
let div = document.getElementById("statistics");
div.innerHTML = '';
const rvals = accelerometerValues.slice(-300).map((m) => Math.sqrt(m.x*m.x + m.y*m.y + m.z*m.z));
const rmean = rvals.reduce((acc,curr)=>acc+curr,0)/rvals.length;
const rstd = Math.sqrt(rvals.reduce((acc, val) => acc + (val - rmean) ** 2, 0) / rvals.length);
const zscore = (rvals[rvals.length-1] - rmean)/rstd;
plotValues.push(zscore);
if ((zscore > 2.25) && (Date.now() - lastFiredDate > 200)) {
div.innerHTML = `<b>${rmean.toFixed(4)} ${rstd.toFixed(4)} ==> PEAK!</b>`;
heartbeat();
lastFiredDate = Date.now()
} else {
div.innerHTML = `<b>${rmean.toFixed(4)} ${rstd.toFixed(4)}</b>`;
}
console.log("HERE");
}
function displayLastTenValues() {
let lastTenList = document.getElementById("lastTenList");
lastTenList.innerHTML = '';
const startIndex = Math.max(0, accelerometerValues.length - 10);
const lastTen = accelerometerValues.slice(startIndex);
lastTen.forEach((data, index) => {
let timeDiff = index > 0 ? (data.t - lastTen[index - 1].t).toFixed(2) : '-';
let listItem = document.createElement('li');
listItem.innerText = `#${startIndex + index + 1}: ${data.x}, ${data.y}, ${data.z}, t=${data.t.toFixed(2)}, tdelta: ${timeDiff}`;
lastTenList.appendChild(listItem);
});
}
function downloadCSV() {
const now = new Date();
const utcTimestamp = now.toISOString().replace(/[:T-]/g, '_').split('.')[0];
const filename = `accelerometer_data_${utcTimestamp}.csv`;
const csvContent = "data:text/csv;charset=utf-8," +
"x,y,z,t,t_utc\n" + // Header row
accelerometerValues.map(item => Object.values(item).join(',')).join('\n');
const encodedUri = encodeURI(csvContent);
const link = document.createElement("a");
link.setAttribute("href", encodedUri);
link.setAttribute("download", filename);
document.body.appendChild(link);
link.click();
}
function heartbeat() {
const heart = document.getElementById('heart');
heart.classList.remove('animate-heartbeat');
void heart.offsetWidth; // Trigger reflow to restart the animation
heart.classList.add('animate-heartbeat');
heart.addEventListener('animationend', () => {
heart.classList.remove('animate-heartbeat');
}, { once: true });
}
</script>
</body>
</html>
@aminnj
Copy link
Author

aminnj commented May 6, 2024

90% written by ChatGPT. All I did was tweak + offline analysis to figure out that zscore works remarkably well.

IMG_2204

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment