`;
// Render chart with user's data
renderChart(age, sex, userHRV, improvedHRV, equivalentAge);
});
function renderChart(userAge, userSex, userHRV, improvedHRV, equivalentAge) {
const canvas = document.getElementById('hrv-canvas-' + widgetId);
const tooltip = document.getElementById('hrv-tooltip-' + widgetId);
const ctx = canvas.getContext('2d');
// Set canvas size
const container = canvas.parentElement;
canvas.width = container.offsetWidth;
canvas.height = container.offsetHeight;
const width = canvas.width;
const height = canvas.height;
const padding = { top: 40, right: 40, bottom: 60, left: 60 };
const chartWidth = width - padding.left - padding.right;
const chartHeight = height - padding.top - padding.bottom;
// Scales
const xScale = (age) => padding.left + ((age - 20) / (65 - 20)) * chartWidth;
const yScale = (hrv) => padding.top + ((110 - hrv) / 110) * chartHeight;
// Clear canvas
ctx.clearRect(0, 0, width, height);
// Draw grid
ctx.strokeStyle = '#e0e0e0';
ctx.lineWidth = 1;
for (let age = 20; age <= 65; age += 5) {
ctx.beginPath();
ctx.moveTo(xScale(age), padding.top);
ctx.lineTo(xScale(age), height - padding.bottom);
ctx.stroke();
}
for (let hrv = 0; hrv <= 110; hrv += 20) {
ctx.beginPath();
ctx.moveTo(padding.left, yScale(hrv));
ctx.lineTo(width - padding.right, yScale(hrv));
ctx.stroke();
}
// Draw axes
ctx.strokeStyle = '#02427A';
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(padding.left, padding.top);
ctx.lineTo(padding.left, height - padding.bottom);
ctx.lineTo(width - padding.right, height - padding.bottom);
ctx.stroke();
// X-axis labels
ctx.fillStyle = '#64748b';
ctx.font = '12px Arial';
ctx.textAlign = 'center';
for (let age = 20; age <= 65; age += 5) {
ctx.fillText(age, xScale(age), height - padding.bottom + 20);
}
ctx.fillText('Age (years)', width / 2, height - 10);
// Y-axis labels
ctx.textAlign = 'right';
for (let hrv = 0; hrv <= 110; hrv += 20) {
ctx.fillText(hrv, padding.left - 10, yScale(hrv) + 4);
}
ctx.save();
ctx.translate(15, height / 2);
ctx.rotate(-Math.PI / 2);
ctx.textAlign = 'center';
ctx.fillText('HRV (ms)', 0, 0);
ctx.restore();
// Draw error bars
baseData.forEach(point => {
// Male error bars
ctx.strokeStyle = maleLineColor;
ctx.lineWidth = 2;
ctx.globalAlpha = 0.3;
const maleTop = point.male + point.maleError[1];
const maleBottom = point.male - point.maleError[0];
ctx.beginPath();
ctx.moveTo(xScale(point.age), yScale(maleTop));
ctx.lineTo(xScale(point.age), yScale(maleBottom));
ctx.stroke();
// Female error bars
ctx.strokeStyle = femaleLineColor;
const femaleTop = point.female + point.femaleError[1];
const femaleBottom = point.female - point.femaleError[0];
ctx.beginPath();
ctx.moveTo(xScale(point.age), yScale(femaleTop));
ctx.lineTo(xScale(point.age), yScale(femaleBottom));
ctx.stroke();
ctx.globalAlpha = 1;
});
// Draw male line
ctx.strokeStyle = maleLineColor;
ctx.lineWidth = 3;
ctx.beginPath();
baseData.forEach((point, i) => {
if (i === 0) {
ctx.moveTo(xScale(point.age), yScale(point.male));
} else {
ctx.lineTo(xScale(point.age), yScale(point.male));
}
});
ctx.stroke();
// Draw male points
baseData.forEach(point => {
ctx.fillStyle = maleLineColor;
ctx.beginPath();
ctx.arc(xScale(point.age), yScale(point.male), 5, 0, Math.PI * 2);
ctx.fill();
});
// Draw female line
ctx.strokeStyle = femaleLineColor;
ctx.lineWidth = 3;
ctx.beginPath();
baseData.forEach((point, i) => {
if (i === 0) {
ctx.moveTo(xScale(point.age), yScale(point.female));
} else {
ctx.lineTo(xScale(point.age), yScale(point.female));
}
});
ctx.stroke();
// Draw female points
baseData.forEach(point => {
ctx.fillStyle = femaleLineColor;
ctx.beginPath();
ctx.arc(xScale(point.age), yScale(point.female), 5, 0, Math.PI * 2);
ctx.fill();
});
// Draw legend
ctx.font = 'bold 14px Arial';
const legendY = padding.top - 15;
ctx.strokeStyle = maleLineColor;
ctx.lineWidth = 3;
ctx.beginPath();
ctx.moveTo(width - 160, legendY);
ctx.lineTo(width - 140, legendY);
ctx.stroke();
ctx.fillStyle = '#02427A';
ctx.textAlign = 'left';
ctx.fillText('Male', width - 135, legendY + 4);
ctx.strokeStyle = femaleLineColor;
ctx.beginPath();
ctx.moveTo(width - 80, legendY);
ctx.lineTo(width - 60, legendY);
ctx.stroke();
ctx.fillText('Female', width - 55, legendY + 4);
// Draw user points if provided
if (userAge && userHRV) {
const userColor = userSex === 'male' ? maleLineColor : femaleLineColor;
// Current point
ctx.fillStyle = userMarkerColor;
ctx.strokeStyle = '#02427A';
ctx.lineWidth = 3;
ctx.beginPath();
ctx.arc(xScale(userAge), yScale(userHRV), 10, 0, Math.PI * 2);
ctx.fill();
ctx.stroke();
ctx.fillStyle = '#02427A';
ctx.font = 'bold 12px Arial';
ctx.textAlign = 'center';
ctx.fillText('YOU', xScale(userAge), yScale(userHRV) - 15);
// Improved point
if (improvedHRV && equivalentAge) {
ctx.fillStyle = vnsMarkerColor;
ctx.strokeStyle = '#02427A';
ctx.beginPath();
ctx.arc(xScale(equivalentAge), yScale(improvedHRV), 10, 0, Math.PI * 2);
ctx.fill();
ctx.stroke();
ctx.fillStyle = '#02427A';
ctx.fillText('+VNS', xScale(equivalentAge), yScale(improvedHRV) - 15);
}
}
// Add hover interaction
canvas.onmousemove = (e) => {
const rect = canvas.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
let hoveredPoint = null;
baseData.forEach(point => {
const px = xScale(point.age);
const pyMale = yScale(point.male);
const pyFemale = yScale(point.female);
if (Math.abs(x - px) < 10 && Math.abs(y - pyMale) < 10) {
hoveredPoint = { ...point, gender: 'male' };
} else if (Math.abs(x - px) < 10 && Math.abs(y - pyFemale) < 10) {
hoveredPoint = { ...point, gender: 'female' };
}
});
if (hoveredPoint) {
const isMale = hoveredPoint.gender === 'male';
const hrv = isMale ? hoveredPoint.male : hoveredPoint.female;
const error = isMale ? hoveredPoint.maleError : hoveredPoint.femaleError;
tooltip.style.display = 'block';
tooltip.style.left = (e.clientX - rect.left + 10) + 'px';
tooltip.style.top = (e.clientY - rect.top - 60) + 'px';
tooltip.innerHTML = `
Age: ${hoveredPoint.age} years
${isMale ? 'Male' : 'Female'} Average: ${hrv} ms
Range: ${hrv - error[0]} - ${hrv + error[1]} ms
`;
} else {
tooltip.style.display = 'none';
}
};
canvas.onmouseleave = () => {
tooltip.style.display = 'none';
};
}
// Initial chart render
renderChart();
})();
Range: ${hrv - error[0]} - ${hrv + error[1]} ms