Hello those with menstrual cycles needing a privacy focused way of tracking! I didn’t see any on here so I’d figured I’d contribute my own .mds.
I use this to view my own cycle by logging in values in my daily notes (in DailyNotes/Files)! This is free for you all to use, enjoy!
p= period, when your cycle starts you tag p in that daily note as #start, and once it ends, you can tag that last day as #end. Each day you can add symptoms such as #light #heavy, #medium, #disabling. Read the Severity Weights in this file and feel free to add your own!
m= mittelschmerz, if I can tell when that is, I mark that as any value in Daily Notes (I just put an ‘x’), otherwise there is a manual number listed in this section:
// manual override: days after P start
const MANUAL_M_OFFSET = 16;
Change this as needed.
I used numerical values to gauge how severe each period was per tagged symptom.
Code for dataview:
// =======================================================
// CONFIG
// =======================================================
const FOLDER = "DailyNotes/Files";
const FIELD = "p";
const M_FIELD = "m";
// manual override: days after P start
const MANUAL_M_OFFSET = 16;
// severity weights
const severityScores = {
"#light": 1,
"#medium": 2,
"#heavy": 3,
"#spotting": 1,
"#pain1": 1,
"#pain2": 2,
"#pain3": 3,
"#disabling": 4,
"#missedWork": 5,
"#chills": 3,
"#dizzy": 3
};
// =======================================================
// LOAD & NORMALIZE ENTRIES
// =======================================================
let pages = dv.pages(`"${FOLDER}"`).where(p => (p[FIELD] || p[M_FIELD]) && p.date);
let entries = pages.array().map(p => {
let raw = p[FIELD];
let tagText = Array.isArray(raw) ? raw.join(" ") : String(raw || "");
let tags = tagText.split(/\s+/).filter(t => t.startsWith("#"));
let severity = tags.reduce((s, t) => s + (severityScores[t] || 0), 0);
let date = p.date.toJSDate ? p.date.toJSDate() : new Date(p.date);
return {
file: p.file,
date,
tags,
severity,
m: p[M_FIELD] || null
};
});
entries.sort((a, b) => a.date - b.date);
// =======================================================
// BUILD P CYCLES
// =======================================================
let starts = entries.filter(e => e.tags.includes("#start"));
let ends = entries.filter(e => e.tags.includes("#end"));
let cycles = [];
let remainingEnds = [...ends];
for (let s of starts) {
let idx = remainingEnds.findIndex(e => e.date >= s.date);
let endDate = idx !== -1
? remainingEnds[idx].date
: entries[entries.length - 1].date;
let cycleEntries = entries.filter(e => e.date >= s.date && e.date <= endDate);
cycles.push({
start: s.date,
end: endDate,
duration: Math.round((endDate - s.date) / 86400000),
severity: cycleEntries.reduce((a, e) => a + e.severity, 0),
disabling: cycleEntries.filter(e => e.tags.includes("#disabling")).length,
missedWork: cycleEntries.filter(e => e.tags.includes("#missedWork")).length
});
if (idx !== -1) remainingEnds.splice(idx, 1);
}
// =======================================================
// AVERAGES
// =======================================================
let avgDuration = cycles.length
? cycles.reduce((a, c) => a + c.duration, 0) / cycles.length
: null;
let gaps = [];
for (let i = 0; i < cycles.length - 1; i++) {
gaps.push((cycles[i + 1].start - cycles[i].end) / 86400000);
}
let avgGap = gaps.length
? gaps.reduce((a, b) => a + b, 0) / gaps.length
: null;
// =======================================================
// NEXT P PREDICTIONS
// =======================================================
let lastEnd = cycles.length ? cycles[cycles.length - 1].end : null;
let predictedStart = lastEnd && avgGap !== null
? new Date(lastEnd.getTime() + Math.round(avgGap) * 86400000)
: null;
let predictedEnd = predictedStart && avgDuration !== null
? new Date(predictedStart.getTime() + Math.round(avgDuration) * 86400000)
: null;
let lastStart = starts.length ? starts[starts.length - 1].date : null;
let predictedStart29 = lastStart
? new Date(lastStart.getTime() + 29 * 86400000)
: null;
// =======================================================
// M EVENTS
// =======================================================
let pStarts = cycles.map(c => c.start).sort((a, b) => a - b);
function prevStart(d) {
let r = null;
for (let s of pStarts) if (s <= d) r = s;
return r;
}
let mEvents = entries
.filter(e => e.m)
.map(e => {
let s = prevStart(e.date);
return {
date: e.date,
start: s ? s : null,
day: s ? Math.floor((e.date - s) / 86400000) + 1 : null,
value: e.m
};
});
// =======================================================
// AVERAGE DAY OF M
// =======================================================
let anchoredMEvents = mEvents.filter(e => e.day !== null);
let avgMDay = anchoredMEvents.length
? anchoredMEvents.reduce((sum, e) => sum + e.day, 0) / anchoredMEvents.length
: null;
// =======================================================
// M PREDICTIONS (NEXT + CURRENT)
// =======================================================
let nextMFromAverage = (predictedStart && avgMDay !== null)
? new Date(predictedStart.getTime() + Math.round(avgMDay - 1) * 86400000)
: null;
let nextMFromManual = predictedStart
? new Date(predictedStart.getTime() + MANUAL_M_OFFSET * 86400000)
: null;
let currentMFromAverage = (lastStart && avgMDay !== null)
? new Date(lastStart.getTime() + Math.round(avgMDay - 1) * 86400000)
: null;
let currentMFromManual = lastStart
? new Date(lastStart.getTime() + MANUAL_M_OFFSET * 86400000)
: null;
// =======================================================
// OUTPUT
// =======================================================
dv.header(3, "Cycle Summary & Prediction");
dv.list([
`Average cycle duration: **${avgDuration?.toFixed(1) ?? "N/A"} days**`,
`Average gap: **${avgGap?.toFixed(1) ?? "N/A"} days**`,
`Predicted next start (P): **${predictedStart?.toISOString().slice(0,10) ?? "N/A"}**`,
`Predicted next start (P +29): **${predictedStart29?.toISOString().slice(0,10) ?? "N/A"}**`,
`Predicted next end (P): **${predictedEnd?.toISOString().slice(0,10) ?? "N/A"}**`,
`Next predicted M (average): **${nextMFromAverage?.toISOString().slice(0,10) ?? "N/A"}**`,
`Next predicted M (+${MANUAL_M_OFFSET}): **${nextMFromManual?.toISOString().slice(0,10) ?? "N/A"}**`,
`Current cycle M (average): **${currentMFromAverage?.toISOString().slice(0,10) ?? "N/A"}**`,
`Current cycle M (+${MANUAL_M_OFFSET}): **${currentMFromManual?.toISOString().slice(0,10) ?? "N/A"}**`,
`Cycles observed: **${cycles.length}**`,
`Average day of m: **${avgMDay?.toFixed(1) ?? "N/A"}**`
]);
dv.header(4, "Cycle Breakdown");
dv.table(
["Start", "End", "Duration", "Severity", "Disabling", "MissedWork"],
cycles.map(c => [
c.start.toISOString().slice(0,10),
c.end.toISOString().slice(0,10),
c.duration,
c.severity,
c.disabling,
c.missedWork
])
);
dv.header(4, "M Events");
dv.table(
["Date", "Cycle Start", "Day", "Value"],
mEvents.map(e => [
e.date.toISOString().slice(0,10),
e.start ? e.start.toISOString().slice(0,10) : "N/A",
e.day ?? "N/A",
e.value
])
);
Code to add to Daily Notes:
---
tags:
- daily
date: <% tp.date.now("YYYY-MM-DD") %>
---
# <% tp.file.title %>
---
## PTracker
- m::
- p::
- psym::
---
