Open-ended predictions, faded past six weeks; upcoming periods list
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
bb19bc0aeb
commit
ce84bee359
6 changed files with 124 additions and 36 deletions
|
|
@ -158,10 +158,15 @@
|
|||
return h("span", { class: "flow", "aria-hidden": "true" }, [1, 2, 3].map(step => h("i", { class: step <= level ? "on" : null })));
|
||||
}
|
||||
|
||||
// Which calendar days carry which marker, from logs and the summary.
|
||||
function dayMarks(summary) {
|
||||
// Which calendar days carry which marker, from logs and predictions up to
|
||||
// `until` (the last day on screen; predictions have no horizon).
|
||||
function dayMarks(summary, until) {
|
||||
const marks = {
|
||||
period: new Set(), spotting: new Set(), symptoms: new Set(), predicted: new Set(), fertile: new Set(), ovulation: new Set(),
|
||||
// Predictions far enough ahead to be shown as less certain.
|
||||
faded: new Set(),
|
||||
// day -> the predicted period it belongs to.
|
||||
prediction: new Map(),
|
||||
// day -> 1..3: the logged strength, or the usual strength for that day
|
||||
// of the period when predicted.
|
||||
level: new Map()
|
||||
|
|
@ -173,18 +178,24 @@
|
|||
if (entry.flow === "spotting") marks.spotting.add(day);
|
||||
if (entry.symptoms.length) marks.symptoms.add(day);
|
||||
}
|
||||
const { current, predictions, fertile, today, periodLength, flowProfile } = summary;
|
||||
const predict = (from, to, start) => {
|
||||
const { current, today, periodLength, flowProfile } = summary;
|
||||
const { predictions, fertile } = Cycle.forecast(summary, until);
|
||||
const predict = (from, to, start, prediction) => {
|
||||
for (const day of range(from, to)) {
|
||||
if (marks.period.has(day)) continue;
|
||||
marks.predicted.add(day);
|
||||
if (prediction) marks.prediction.set(day, prediction);
|
||||
if (prediction && prediction.faded) marks.faded.add(day);
|
||||
if (flowProfile[day - start]) marks.level.set(day, flowProfile[day - start]);
|
||||
}
|
||||
};
|
||||
if (current && current.onPeriod) predict(today + 1, current.start + periodLength - 1, current.start);
|
||||
for (const prediction of predictions) predict(Math.max(prediction.start, today + 1), prediction.end, prediction.start);
|
||||
if (current && current.onPeriod) predict(today + 1, current.start + periodLength - 1, current.start, null);
|
||||
for (const prediction of predictions) predict(Math.max(prediction.start, today + 1), prediction.end, prediction.start, prediction);
|
||||
for (const window of fertile) {
|
||||
for (const day of range(window.start, window.end)) marks.fertile.add(day);
|
||||
for (const day of range(window.start, window.end)) {
|
||||
marks.fertile.add(day);
|
||||
if (window.faded) marks.faded.add(day);
|
||||
}
|
||||
marks.ovulation.add(window.ovulation);
|
||||
}
|
||||
return marks;
|
||||
|
|
@ -331,9 +342,9 @@
|
|||
function renderCalendar(summary) {
|
||||
const { year, month } = state.month;
|
||||
$("month-title").textContent = `${MONTHS[month]} ${year}`;
|
||||
const marks = dayMarks(summary);
|
||||
const first = Cycle.toDay(`${year}-${pad(month + 1)}-01`);
|
||||
const count = new Date(Date.UTC(year, month + 1, 0)).getUTCDate();
|
||||
const marks = dayMarks(summary, first + count - 1);
|
||||
const leading = (new Date(first * 86400000).getUTCDay() + 6) % 7;
|
||||
const cells = range(1, leading).map(() => h("span", { class: "day outside", "aria-hidden": "true" }));
|
||||
for (const day of range(first, first + count - 1)) {
|
||||
|
|
@ -343,6 +354,7 @@
|
|||
if (marks.fertile.has(day) && !marks.period.has(day)) kinds.push("fertile");
|
||||
if (day === summary.today) kinds.push("today");
|
||||
if (day > summary.today) kinds.push("future");
|
||||
if (marks.faded.has(day)) kinds.push("faded");
|
||||
const dots = [];
|
||||
if (marks.ovulation.has(day)) dots.push("ovulation");
|
||||
if (marks.spotting.has(day)) dots.push("spotting");
|
||||
|
|
@ -355,7 +367,8 @@
|
|||
marks.fertile.has(day) && "fertile window estimate",
|
||||
marks.ovulation.has(day) && "estimated ovulation",
|
||||
marks.spotting.has(day) && "spotting",
|
||||
marks.symptoms.has(day) && "symptoms logged"
|
||||
marks.symptoms.has(day) && "symptoms logged",
|
||||
marks.faded.has(day) && "less certain this far ahead"
|
||||
].filter(Boolean);
|
||||
cells.push(h("button", {
|
||||
type: "button",
|
||||
|
|
@ -387,15 +400,18 @@
|
|||
function renderSheet(summary) {
|
||||
const day = state.sheetDay;
|
||||
if (day === null) return;
|
||||
const marks = dayMarks(summary);
|
||||
const marks = dayMarks(summary, day);
|
||||
$("sheet-title").textContent = longDay(day);
|
||||
const notes = [];
|
||||
if (marks.predicted.has(day)) {
|
||||
const level = marks.level.get(day);
|
||||
notes.push(level ? `Predicted period day, usually ${Cycle.LEVEL_NAMES[level]} flow.` : "Predicted period day.");
|
||||
const prediction = marks.prediction.get(day);
|
||||
if (prediction) notes.push(`This period is expected to start around ${formatDay(prediction.start)}, likely between ${formatDay(prediction.earliest)} and ${formatDay(prediction.latest)}.`);
|
||||
}
|
||||
if (marks.ovulation.has(day)) notes.push("Estimated ovulation day.");
|
||||
else if (marks.fertile.has(day)) notes.push("Inside the estimated fertile window.");
|
||||
if (marks.faded.has(day)) notes.push("This far ahead the prediction is less certain.");
|
||||
const body = [];
|
||||
if (notes.length) body.push(h("p", { class: "prediction-note" }, notes.join(" ")));
|
||||
if (day > summary.today) {
|
||||
|
|
@ -423,6 +439,7 @@
|
|||
tile("Cycles tracked", sampleSize)
|
||||
);
|
||||
renderChart(cycles.filter(cycle => cycle.used).slice(-12), summary);
|
||||
renderUpcoming(summary);
|
||||
|
||||
const rows = [];
|
||||
if (current) {
|
||||
|
|
@ -443,6 +460,23 @@
|
|||
$("history").replaceChildren(...rows);
|
||||
}
|
||||
|
||||
// A year of predictions as a list, for planning travel and the like.
|
||||
function renderUpcoming(summary) {
|
||||
const { predictions } = Cycle.forecast(summary, summary.today + 365, 1);
|
||||
const thisYear = new Date(summary.today * 86400000).getUTCFullYear();
|
||||
// Built by hand: en-GB adds a comma after the weekday once a year is included.
|
||||
const dated = number => {
|
||||
const year = new Date(number * 86400000).getUTCFullYear();
|
||||
const day = formatDay(number, { weekday: "short", day: "numeric", month: "short" });
|
||||
return year === thisYear ? day : `${day} ${year}`;
|
||||
};
|
||||
const rows = predictions.map(prediction => h("li", { class: prediction.faded ? "faded" : null },
|
||||
h("span", { class: "when" }, dated(prediction.start)),
|
||||
h("span", { class: "likely" }, `likely ${formatDay(prediction.earliest)} – ${formatDay(prediction.latest)}`, prediction.faded && h("small", {}, "less certain"))
|
||||
));
|
||||
$("upcoming").replaceChildren(...(rows.length ? rows : [h("li", {}, "Log a period to see predictions.")]));
|
||||
}
|
||||
|
||||
function renderChart(data, summary) {
|
||||
const container = $("chart");
|
||||
if (data.length < 2) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue