Skip to content

Instantly share code, notes, and snippets.

@pedramamini
Created April 4, 2025 18:12
Show Gist options
  • Save pedramamini/ffff26a145252eca8ecfb20160946d96 to your computer and use it in GitHub Desktop.
Save pedramamini/ffff26a145252eca8ecfb20160946d96 to your computer and use it in GitHub Desktop.
Obsidian dashboard for Oura stats
```dataviewjs
// Function to format number with commas
function formatNumber(num, decimals = 0) {
return num.toFixed(decimals).replace(/\B(?=(\d{3})+(?!\d))/g, ",");
}
// Function to format date from YYYY-MM-DD to M/D (no leading zeros)
function formatDate(dateString) {
const [year, month, day] = dateString.split('-');
return `${parseInt(month)}/${parseInt(day)}`;
}
// Function to calculate statistics
function calculateStats(values, dates, variable) {
let combinedData = [];
for (let i = 0; i < values.length; i++) {
if (typeof values[i] === 'number' && !isNaN(values[i])) {
combinedData.push({ value: values[i], date: dates[i] });
}
}
combinedData.sort((a, b) => new Date(b.date) - new Date(a.date));
if (variable === 'BodyRestingHeartRate') {
combinedData = combinedData.filter(item => item.value >= 25);
}
if (variable === 'BodyBodyTemperature') {
combinedData = combinedData.filter(item => item.value >= 85);
}
if (combinedData.length === 0) return { min: 'N/A', max: 'N/A', avg: 'N/A', last: 'N/A' };
let min = combinedData[0], max = combinedData[0];
let sum = 0;
for (let item of combinedData) {
if (item.value < min.value) min = item;
if (item.value > max.value) max = item;
sum += item.value;
}
const avg = sum / combinedData.length;
const last = combinedData[0];
const decimals = variable === 'BodyBodyTemperature' ? 1 : 0;
return {
min: `${formatNumber(min.value, decimals)} (${formatDate(min.date)})`,
max: `${formatNumber(max.value, decimals)} (${formatDate(max.date)})`,
avg: formatNumber(avg, decimals),
last: formatNumber(last.value, decimals)
};
}
// Function to get files based on time range
function getFiles(timeRange) {
const today = dv.date('today');
let startDate;
switch(timeRange) {
case '1w': startDate = today.minus({ weeks: 1 }); break;
case '2w': startDate = today.minus({ weeks: 2 }); break;
case '1m': startDate = today.minus({ months: 1 }); break;
case '6w': startDate = today.minus({ weeks: 6 }); break;
case '2m': startDate = today.minus({ months: 2 }); break;
case '90d': startDate = today.minus({ days: 90 }); break;
case 'all': startDate = dv.date('1900-01-01'); break; // A date far in the past
default: startDate = today.minus({ months: 1 }); // Default to 1 month
}
return dv.pages('"Body/Oura"')
.where(p => p.file.name.match(/^\d{4}-\d{2}-\d{2}$/))
.where(p => dv.date(p.file.name) >= startDate)
.sort(p => p.file.name, 'desc');
}
// Variables to analyze with their display names
const variables = [
{ key: 'ActivityScore', display: 'Activity Score' },
{ key: 'ActivitySteps', display: 'Steps' },
{ key: 'ActivityTotalCalories', display: 'Calories' },
{ key: 'BodyBodyTemperature', display: 'Body Temperature' },
{ key: 'BodyReadinessScore', display: 'Readiness Score' },
{ key: 'BodyRestingHeartRate', display: 'Resting HR' },
{ key: 'SleepREM', display: 'REM Time' },
{ key: 'SleepScore', display: 'Sleep Score' }
].sort((a, b) => a.display.localeCompare(b.display));
// Create dropdown options
const timeRanges = [
{ value: '1w', display: '1 Week' },
{ value: '2w', display: '2 Weeks' },
{ value: '1m', display: '1 Month' },
{ value: '6w', display: '6 Weeks' },
{ value: '2m', display: '2 Months' },
{ value: '90d', display: '90 Days' },
{ value: 'all', display: 'All Time' }
];
// Create dropdown and button HTML
const controlsHtml = `
<select id="timeRangeSelect" style="margin-bottom: 10px;">
${timeRanges.map(range => `<option value="${range.value}" ${range.value === '1m' ? 'selected' : ''}>${range.display}</option>`).join('')}
</select>
<button id="updateButton" style="margin-left: 10px;">Update</button>
<div id="tableContainer"></div>
`;
// Display controls
dv.paragraph(controlsHtml);
// Function to generate table data
function getTableData(timeRange) {
const files = getFiles(timeRange);
return variables.map(variable => {
const values = files.map(p => p[variable.key]);
const dates = files.map(p => p.file.name);
const stats = calculateStats(values, dates, variable.key);
return [
variable.display,
stats.min,
stats.max,
stats.avg,
stats.last
];
});
}
// Function to update the table
function updateTable() {
const select = dv.container.querySelector('#timeRangeSelect');
const timeRange = select.value;
const data = getTableData(timeRange);
const tableHtml = `
<table style="width:100%; border-collapse: collapse; margin-top: 10px;">
<tr>
<th style="border: 1px solid #ddd; padding: 8px; text-align: left;">Variable</th>
<th style="border: 1px solid #ddd; padding: 8px; text-align: left;">Min</th>
<th style="border: 1px solid #ddd; padding: 8px; text-align: left;">Max</th>
<th style="border: 1px solid #ddd; padding: 8px; text-align: left;">Average</th>
<th style="border: 1px solid #ddd; padding: 8px; text-align: left;">Last Value</th>
</tr>
${data.map(row => `
<tr>
${row.map(cell => `<td style="border: 1px solid #ddd; padding: 8px;">${cell}</td>`).join('')}
</tr>
`).join('')}
</table>
`;
const tableContainer = dv.container.querySelector('#tableContainer');
tableContainer.innerHTML = tableHtml;
}
// Initial table update
updateTable();
// Add click event listener to update button
const updateButton = dv.container.querySelector('#updateButton');
updateButton.addEventListener('click', updateTable);
```
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment