Created
April 4, 2025 18:12
-
-
Save pedramamini/ffff26a145252eca8ecfb20160946d96 to your computer and use it in GitHub Desktop.
Obsidian dashboard for Oura stats
This file contains hidden or 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
```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