(function(){ "use strict"; /* ---------------- date helpers ---------------- */ function toDate(y,m,d){ return new Date(y,m,d); } function fromInput(str){ if(!str) return null; var p = str.split("-"); return new Date(+p[0], +p[1]-1, +p[2]); } function toInputVal(d){ return d.getFullYear()+"-"+String(d.getMonth()+1).padStart(2,"0")+"-"+String(d.getDate()).padStart(2,"0"); } function addDays(d,n){ var r=new Date(d); r.setDate(r.getDate()+n); return r; } function daysBetween(a,b){ // b - a, in whole days, ignoring time var A = new Date(a.getFullYear(),a.getMonth(),a.getDate()); var B = new Date(b.getFullYear(),b.getMonth(),b.getDate()); return Math.round((B-A)/86400000); } function sameDay(a,b){ return a.getFullYear()===b.getFullYear() && a.getMonth()===b.getMonth() && a.getDate()===b.getDate(); } function mod(n,m){ return ((n % m) + m) % m; } var MONTH_NAMES = ["January","February","March","April","May","June","July","August","September","October","November","December"]; var today = new Date(); today.setHours(0,0,0,0); function loadLS(key, fallback){ try{ var v = localStorage.getItem(key); return v===null ? fallback : v; }catch(e){ return fallback; } } function saveLS(key, val){ try{ localStorage.setItem(key,val); }catch(e){} } /* ---------------- mode switching ---------------- */ var modeBtns = document.querySelectorAll(".mode-btn"); var panels = { period: document.getElementById("mode-period"), ovulation: document.getElementById("mode-ovulation"), pregnancy: document.getElementById("mode-pregnancy") }; modeBtns.forEach(function(btn){ btn.addEventListener("click", function(){ modeBtns.forEach(function(b){ b.classList.remove("active"); b.setAttribute("aria-selected","false"); }); btn.classList.add("active"); btn.setAttribute("aria-selected","true"); Object.keys(panels).forEach(function(k){ panels[k].classList.remove("active"); }); panels[btn.dataset.mode].classList.add("active"); saveLS("ca-mode", btn.dataset.mode); }); }); var savedMode = loadLS("ca-mode", null); if(savedMode && panels[savedMode]){ modeBtns.forEach(function(b){ b.classList.toggle("active", b.dataset.mode===savedMode); b.setAttribute("aria-selected", b.dataset.mode===savedMode ? "true":"false"); }); Object.keys(panels).forEach(function(k){ panels[k].classList.toggle("active", k===savedMode); }); } /* ================================================================ PHASE + OVULATION REFERENCE DATA ================================================================ */ var PHASES = { menstrual: { title:"Menstrual", color:"var(--period)", soft:"var(--period-soft)", window:"Cycle day 1 through the end of bleeding", desc:"The uterine lining sheds because no pregnancy occurred last cycle. Estrogen and progesterone are both at their lowest point of the month.", hormone:"Estrogen & progesterone: low", energy:"Energy: lowest", symptoms:"Cramping, fatigue, lower back ache, mood dips", tip:"Iron-rich food, gentle movement, and heat on the lower abdomen ease cramping better than rest alone." }, follicular: { title:"Follicular", color:"var(--ovulation)", soft:"var(--ovulation-soft)", window:"Cycle day 1 through ovulation (overlaps menstrual)", desc:"The pituitary releases FSH, prompting several ovarian follicles to mature. Estrogen climbs steadily as one dominant follicle takes the lead.", hormone:"Estrogen: rising", energy:"Energy: climbing", symptoms:"Clearer skin, sharper focus, rising motivation", tip:"This is the physiologically best window for starting new training blocks or demanding projects." }, ovulatory: { title:"Ovulatory", color:"var(--ovulation-strong)", soft:"var(--ovulation-soft)", window:"Roughly one day, mid-cycle", desc:"A surge in luteinizing hormone (LH) triggers the dominant follicle to release its egg. This is the single most fertile day of the cycle.", hormone:"LH: sharp surge", energy:"Energy & libido: peak", symptoms:"Egg-white cervical mucus, one-sided pelvic twinge (mittelschmerz)", tip:"Ovulation predictor kits detect the LH surge roughly 24–36 hours before the egg is released." }, luteal: { title:"Luteal", color:"var(--pregnancy)", soft:"var(--pregnancy-soft)", window:"Ovulation through the day before your next period", desc:"The ruptured follicle becomes the corpus luteum and secretes progesterone to prepare the uterine lining. If no implantation occurs, hormone levels fall and the cycle restarts.", hormone:"Progesterone: rises then falls", energy:"Energy: gradually declining", symptoms:"Bloating, breast tenderness, irritability, food cravings (PMS)", tip:"Basal body temperature stays elevated ~0.3–0.5°C through this phase — a useful way to confirm ovulation already happened." } }; // offset relative to ovulation day (0 = ovulation) var OVULATION_DAYS = { "-5": {fert:"Low", fertClass:"fert-low", bbt:"Baseline (pre-shift)", mucus:"Dry to sticky", note:"Sperm can survive up to 5 days, so conception is technically possible starting here."}, "-4": {fert:"Low–moderate", fertClass:"fert-low", bbt:"Baseline", mucus:"Sticky, cloudy", note:"Fertility begins climbing as estrogen rises toward its pre-ovulation peak."}, "-3": {fert:"Moderate", fertClass:"fert-mid", bbt:"Baseline", mucus:"Creamy, increasing in volume", note:"Cervical mucus is becoming more hospitable to sperm."}, "-2": {fert:"High", fertClass:"fert-high", bbt:"Baseline", mucus:"Thin, stretchy, egg-white-like", note:"One of the two most fertile days of the entire cycle."}, "-1": {fert:"Peak", fertClass:"fert-peak", bbt:"Baseline (often the day of the LH surge)", mucus:"Clear, slippery, maximum stretch", note:"Statistically the single highest-probability day for conception."}, "0": {fert:"Peak", fertClass:"fert-peak", bbt:"Still baseline — the rise hasn't happened yet", mucus:"Egg-white, abundant", note:"The egg is released and is viable for roughly 12–24 hours. Some feel a one-sided twinge (mittelschmerz)."}, "1": {fert:"Dropping fast", fertClass:"fert-mid", bbt:"Beginning to rise ~0.3–0.5°C", mucus:"Becoming tacky again", note:"The egg's fertile window is closing; BBT starts its post-ovulation climb."}, "2": {fert:"Very low", fertClass:"fert-low", bbt:"Elevated, holding", mucus:"Dry or minimal", note:"Progesterone from the corpus luteum keeps temperature elevated through the luteal phase."}, "3": {fert:"Essentially none", fertClass:"fert-low", bbt:"Elevated, holding", mucus:"Dry", note:"If fertilization happened, the embryo is traveling toward the uterus."}, "4": {fert:"Essentially none", fertClass:"fert-low", bbt:"Elevated, holding", mucus:"Dry", note:"Too late in the cycle for a new ovulation; luteal phase symptoms may start appearing."}, "5": {fert:"Essentially none", fertClass:"fert-low", bbt:"Elevated, holding", mucus:"Dry", note:"Implantation, if it happens this cycle, typically occurs around days 6–10 after ovulation."} }; var FERT_COLOR = {"fert-low":"var(--ink-soft)","fert-mid":"var(--fertile)","fert-high":"var(--fertile-strong)","fert-peak":"var(--ovulation-strong)"}; function phaseForDayInCycle(dic, periodLen, ovulationDic){ if(dic < periodLen) return "menstrual"; if(dic < ovulationDic) return "follicular"; if(dic === ovulationDic) return "ovulatory"; return "luteal"; } function isFertile(dic, ovulationDic){ return dic >= ovulationDic - 5 && dic <= ovulationDic; } /* ================================================================ PREGNANCY WEEK DATA (1–40) ================================================================ */ var WEEKS = [ {c:"a poppy seed (not yet fertilized)", m:"n/a", dev:"Pregnancy is dated from the first day of this period, before conception happens — your body is shedding last cycle's lining and preparing a fresh egg.", body:"This week looks and feels like an ordinary period.", tip:"If you're trying to conceive, this is a good week to start a prenatal vitamin with folic acid."}, {c:"an egg cell", m:"~0.1 mm", dev:"Rising estrogen prompts an egg to mature; ovulation typically occurs near the end of this week, roughly two weeks before a missed period.", body:"Cervical mucus becomes clear and stretchy around ovulation — a natural fertility sign.", tip:"This is the fertile window if you're trying to conceive — see the Ovulation tab for day-by-day detail."}, {c:"a grain of sand", m:"~0.1–0.2 mm", dev:"If sperm meets egg this week, fertilization occurs in the fallopian tube and the resulting cell begins dividing as it travels toward the uterus.", body:"No symptoms are detectable yet — even a sensitive test won't turn positive for another week or two.", tip:"Implantation, if it happens, usually occurs 6–10 days after ovulation."}, {c:"a poppy seed", m:"~0.1 mm", dev:"The fertilized egg implants in the uterine lining and begins producing hCG, the hormone home pregnancy tests detect.", body:"Most people notice no symptoms yet; a sensitive test may already turn positive near the end of this week.", tip:"A missed period is usually the first sign — testing a few days after that gives the most reliable result."}, {c:"a sesame seed", m:"~2 mm", dev:"The neural tube — the earliest structure of the brain and spinal cord — begins to form, along with the first blood vessels.", body:"A missed period is usually the first clear sign; fatigue and tender breasts may begin.", tip:"Avoid alcohol and confirm any medications with a clinician — this window is critical for early development."}, {c:"a lentil", m:"~4 mm", dev:"A primitive heart tube starts beating, and the beginnings of the face, arms, and legs appear as small buds.", body:"Morning sickness and smell sensitivity often begin here, though timing varies widely.", tip:"Small, frequent, bland meals tend to sit better than three large ones if nausea has started."}, {c:"a blueberry", m:"~1.3 cm", dev:"The brain is dividing into distinct sections, and the arm and leg buds are elongating into recognizable limbs.", body:"Bloating and mild cramping are common as the uterus makes room to grow.", tip:"This is when most people schedule their first prenatal appointment — dating scans are most accurate now."}, {c:"a raspberry", m:"~1.6 cm", dev:"Fingers and toes are forming, still webbed; the heart now has four distinct chambers.", body:"Fatigue often peaks around this point as progesterone rises sharply.", tip:"Rest when you can — first-trimester exhaustion is real and temporary, not a sign anything is wrong."}, {c:"a grape", m:"~2.3 cm · 4 g", dev:"The embryo is officially called a fetus starting around now; essential organs are all present in early form and the tail has disappeared.", body:"Nausea, food aversions, and a heightened sense of smell are often at their strongest.", tip:"Ginger tea, vitamin B6, and staying hydrated are commonly recommended first-line options for nausea."}, {c:"a kumquat", m:"~3 cm · 7 g", dev:"Vital organs — kidneys, liver, brain — are functioning in a basic form, and tiny fingernail beds are forming.", body:"A visible baby bump is still unlikely, though clothes may feel tighter.", tip:"Ask your provider about first-trimester screening options available in this window."}, {c:"a fig", m:"~4.1 cm · 10 g", dev:"The fetus can now bend its arms and legs, and tooth buds are forming beneath the gums.", body:"Round ligament twinges may start as the uterus stretches.", tip:"Gentle stretching and changing positions slowly can ease early ligament discomfort."}, {c:"a lime", m:"~5.4 cm · 14 g", dev:"Reflexes are developing — fingers can curl, and toes can wiggle, though it can't be felt yet.", body:"The end of the first trimester is near, and miscarriage risk drops meaningfully from here.", tip:"Many people choose to share their pregnancy news around now, though the choice is entirely personal."}, {c:"a plum", m:"~6.7 cm · 23 g", dev:"Vocal cords are forming, and unique fingerprints are being etched onto tiny fingertips.", body:"Some early nausea may start easing as the placenta takes over hormone production.", tip:"The nuchal translucency scan, if chosen, is typically done between now and week 13."}, {c:"a passion fruit", m:"~10 cm · 45 g", dev:"Baby can open and close fists and may even suck a thumb; genitals are differentiating, though not yet visible on ultrasound.", body:"For many, first-trimester nausea and fatigue begin to lift.", tip:"Second-trimester energy often supports resuming or starting light-to-moderate exercise — check in with your provider first."}, {c:"a peach", m:"~11.6 cm · 58 g", dev:"Vocal cords are complete and the intestines, which developed in the umbilical cord, move into the abdomen.", body:"Welcome to the second trimester — often called the most comfortable stretch of pregnancy.", tip:"An anatomy scan is usually scheduled in the coming weeks; ask your clinic when it's booked."}, {c:"a lemon", m:"~13 cm · 70 g", dev:"Fine, downy hair called lanugo begins covering the body, and the kidneys are producing urine into the amniotic fluid.", body:"A small bump may finally become visible; appetite often improves noticeably.", tip:"Iron needs rise sharply now — leafy greens, legumes, and lean meat help meet increased demand."}, {c:"an apple", m:"~14.2 cm · 95 g", dev:"The skeleton continues hardening and baby can sense light through closed eyelids.", body:"Skin changes like a faint linea nigra or darkened areolas may appear as pigment increases.", tip:"Daily SPF helps limit pregnancy-related skin pigmentation (melasma) from sun exposure."}, {c:"an avocado", m:"~16 cm · 135 g", dev:"The placenta is now fully formed and functioning as baby's life-support system.", body:"First flutters of movement ('quickening') may be felt any time from now through week 20.", tip:"Movement timing varies a lot — first pregnancies and anterior placentas often mean feeling it later."}, {c:"a pomegranate", m:"~17.7 cm · 160 g", dev:"The skeleton is converting from cartilage to true bone, and fat stores are just beginning to accumulate.", body:"The uterus is now roughly at the level of the belly button.", tip:"A pregnancy support pillow can meaningfully improve sleep as side-sleeping becomes more comfortable."}, {c:"a bell pepper", m:"~19 cm · 190 g", dev:"Ears have reached their final position and baby may begin startling at loud external sounds.", body:"Quickening is commonly felt by now — often described as bubbles or light taps.", tip:"Try narrating your day out loud — hearing is developing and familiar voices become recognizable over time."}, {c:"a mango", m:"~20.3 cm · 240 g", dev:"A waxy coating called vernix caseosa begins protecting the skin, and taste buds are forming.", body:"Backaches can start as posture shifts to balance the growing bump.", tip:"A supportive, flatter shoe and a pillow between the knees at night both help low back strain."}, {c:"a banana", m:"~25.6 cm · 300 g", dev:"The detailed anatomy ultrasound is often done around now if it hasn't happened already, and sex can typically be identified on it.", body:"Movements are stronger and more frequent; the uterus is now level with the belly button.", tip:"If you want to know the sex and haven't had the anatomy scan yet, ask your clinic to schedule it soon — it's usually done weeks 18–22."}, {c:"a carrot", m:"~27 cm · 360 g", dev:"Baby is swallowing small amounts of amniotic fluid, giving the digestive system practice.", body:"Stretch marks may start appearing on the belly, hips, or chest as skin stretches.", tip:"Moisturizing won't prevent stretch marks, but it can ease the itchiness that comes with stretching skin."}, {c:"a papaya", m:"~28.9 cm · 430 g", dev:"Eyebrows and eyelids are fully formed, and the sense of touch is developing rapidly.", body:"Braxton Hicks — brief, painless practice contractions — may be felt occasionally from here on.", tip:"Braxton Hicks usually ease with a change of position or a glass of water; true labor contractions don't."}, {c:"an ear of corn", m:"~32.2 cm · 600 g", dev:"The lungs begin producing surfactant, a substance essential for breathing air after birth — a key viability milestone.", body:"The linea nigra (a dark vertical line on the belly) may become more visible.", tip:"This week is often cited as the threshold of viability with intensive neonatal care, should birth happen very early."}, {c:"a cauliflower head", m:"~33.7 cm · 660 g", dev:"Baby can now respond to your voice, and hand grip strength is developing.", body:"The glucose screening test for gestational diabetes is typically scheduled around now.", tip:"If asked to fast beforehand, follow your clinic's instructions closely for an accurate glucose result."}, {c:"a head of lettuce", m:"~35.6 cm · 760 g", dev:"Eyes begin to open after being fused shut since early development, and hair may be visibly growing.", body:"Round ligament pain and rib discomfort can increase as space gets tighter.", tip:"A maternity support band can ease rib and ligament pressure during long periods of standing or walking."}, {c:"a bunch of scallions", m:"~36.6 cm · 875 g", dev:"Brain tissue is developing rapidly, and baby is developing regular hiccups you may be able to feel.", body:"Third trimester begins soon — appointments typically become more frequent from here.", tip:"Hiccup-like twitches are completely normal and are the diaphragm practicing its rhythm."}, {c:"an eggplant", m:"~37.6 cm · 1.1 kg", dev:"Eyes can blink now, and baby may cycle through active REM sleep.", body:"Welcome to the third trimester — the home stretch begins.", tip:"This is a common time to start planning a hospital bag and reviewing birth preferences."}, {c:"a butternut squash", m:"~38.6 cm · 1.2 kg", dev:"Bones are fully formed but still soft and flexible, ready to move through the birth canal.", body:"Frequent urination often returns as the head engages lower in the pelvis.", tip:"Kick counts become a useful daily check-in — note baby's typical pattern of movement."}, {c:"a large cabbage", m:"~39.9 cm · 1.3 kg", dev:"The brain is developing at its fastest rate yet, adding folds and surface area.", body:"Sleep can become harder to find as the bump limits comfortable positions.", tip:"A full-body pregnancy pillow supports the belly, back, and knees simultaneously."}, {c:"a coconut", m:"~41.1 cm · 1.5 kg", dev:"All five senses are now functioning, and baby is gaining roughly half a pound a week.", body:"Colostrum, the first form of breast milk, may begin to leak slightly.", tip:"A few nursing pads in your bag can help if colostrum leaking becomes noticeable."}, {c:"a jicama", m:"~42.4 cm · 1.7 kg", dev:"Baby is practicing breathing movements against a closed airway, strengthening the lungs.", body:"Swelling and mild carpal tunnel tingling in the hands can appear from fluid retention.", tip:"Report sudden or severe swelling to your provider promptly — it can occasionally signal preeclampsia."}, {c:"a pineapple", m:"~43.7 cm · 1.9 kg", dev:"Bones are hardening overall, though the skull stays deliberately soft and flexible for delivery.", body:"Pelvic pressure increases as baby continues descending.", tip:"Prenatal massage or a warm bath can ease the deep pelvic ache many feel this week."}, {c:"a cantaloupe", m:"~45 cm · 2.1 kg", dev:"The central nervous system is maturing quickly, and baby's grip is now quite strong.", body:"Braxton Hicks contractions often become more noticeable and frequent.", tip:"Time any contractions that feel different — regular, intensifying, closer together warrants a call to your provider."}, {c:"a honeydew melon", m:"~46.2 cm · 2.4 kg", dev:"The kidneys are fully developed, and the liver is processing waste products on its own.", body:"You may feel baby 'drop' lower as it settles into position for birth.", tip:"Lightening (the baby dropping) can ease heartburn but often increases bathroom trips."}, {c:"a head of romaine", m:"~47.4 cm · 2.6 kg", dev:"Most physical development is complete; the focus now is steady weight gain.", body:"You've just reached 'early term' — babies born from here on tend to do very well.", tip:"Pack your hospital bag if you haven't yet — early-term babies can arrive with little warning."}, {c:"a leek", m:"~49.8 cm · 3.1 kg", dev:"Baby continues laying down fat for temperature regulation after birth, and the brain is still rapidly developing.", body:"Nesting instinct and a burst of energy are common right before labor.", tip:"Balance nesting energy with rest — labor's timing is unpredictable."}, {c:"a mini watermelon", m:"~50.7 cm · 3.3 kg", dev:"Full term. The lungs are completing final maturation in preparation for that first breath.", body:"Increased pelvic pressure and more frequent, intense Braxton Hicks are common.", tip:"Watch for your labor signs: regular contractions, water breaking, or a bloody show."}, {c:"a small pumpkin", m:"~51.2 cm · 3.5 kg", dev:"Baby is fully developed and simply continuing to grow, ready to meet you.", body:"If labor hasn't started, your provider will discuss monitoring or induction options.", tip:"Most babies arrive within two weeks of their due date in either direction — both are entirely normal."} ]; /* ================================================================ SVG helpers ================================================================ */ function polarToXY(cx,cy,r,angleDeg){ var rad = (angleDeg-90) * Math.PI/180; return [cx + r*Math.cos(rad), cy + r*Math.sin(rad)]; } function arcPath(cx,cy,r,startDeg,endDeg){ if(endDeg <= startDeg) endDeg = startDeg + 0.01; var s = polarToXY(cx,cy,r,startDeg), e = polarToXY(cx,cy,r,endDeg); var large = (endDeg-startDeg) > 180 ? 1 : 0; return "M "+s[0].toFixed(2)+" "+s[1].toFixed(2)+" A "+r+" "+r+" 0 "+large+" 1 "+e[0].toFixed(2)+" "+e[1].toFixed(2); } /* ================================================================ SHARED STATE: period start / cycle length / period length ================================================================ */ var exampleStart = addDays(today, -10); var startInput = document.getElementById("p-start"); var cycleLenInput = document.getElementById("p-cyclelen"); var periodLenInput = document.getElementById("p-periodlen"); var savedStart = loadLS("ca-start", null); var savedCycleLen = loadLS("ca-cyclelen", null); var savedPeriodLen = loadLS("ca-periodlen", null); var usingExample = !savedStart; startInput.value = savedStart || toInputVal(exampleStart); cycleLenInput.value = savedCycleLen || 28; periodLenInput.value = savedPeriodLen || 5; function getCycleState(){ return { start: fromInput(startInput.value) || exampleStart, cycleLen: Math.max(21, Math.min(35, parseInt(cycleLenInput.value,10) || 28)), periodLen: Math.max(2, Math.min(10, parseInt(periodLenInput.value,10) || 5)) }; } var pCalMonth = new Date(today.getFullYear(), today.getMonth(), 1); var oCalMonth = new Date(today.getFullYear(), today.getMonth(), 1); var pSelectedDate = new Date(today); var oSelectedDate = new Date(today); [startInput,cycleLenInput,periodLenInput].forEach(function(inp){ inp.addEventListener("input", function(){ saveLS("ca-start", startInput.value); saveLS("ca-cyclelen", cycleLenInput.value); saveLS("ca-periodlen", periodLenInput.value); usingExample = false; document.getElementById("p-example-note").style.display = "none"; document.getElementById("pr-example-note").style.display = "none"; renderAll(); }); }); if(!usingExample) document.getElementById("p-example-note").style.display = "none"; /* ================================================================ CALENDAR RENDERING (shared builder) ================================================================ */ function buildCalendar(gridEl, weekdayHeaderIgnored, monthDate, dayClassifier, onSelect, selectedDate){ gridEl.innerHTML = ""; var year = monthDate.getFullYear(), month = monthDate.getMonth(); var firstDow = new Date(year,month,1).getDay(); var daysInMonth = new Date(year,month+1,0).getDate(); var totalCells = Math.ceil((firstDow+daysInMonth)/7)*7; for(var i=0;i daysInMonth){ cell.classList.add("pad"); gridEl.appendChild(cell); continue; } var d = new Date(year,month,dayNum); var info = dayClassifier(d); cell.innerHTML = ""+dayNum+""; if(info.period) cell.classList.add("is-period"); if(info.fertile) cell.classList.add("is-fertile"); if(info.ovulation) cell.classList.add("is-ovulation"); if(sameDay(d,today)) cell.classList.add("today"); if(selectedDate && sameDay(d,selectedDate)) cell.classList.add("selected"); cell.setAttribute("aria-label", d.toDateString()); cell.addEventListener("click", function(dd){ return function(){ onSelect(dd); }; }(d)); gridEl.appendChild(cell); } } function classifyForCycle(d, state){ var dic = mod(daysBetween(state.start, d), state.cycleLen); var ovDic = mod(state.cycleLen - 14, state.cycleLen); return { period: dic < state.periodLen, fertile: isFertile(dic, ovDic), ovulation: dic === ovDic, dic: dic, ovDic: ovDic }; } /* ---------------- PERIOD tab render ---------------- */ function renderPeriodCalendar(){ var state = getCycleState(); document.getElementById("p-cal-title").textContent = MONTH_NAMES[pCalMonth.getMonth()] + " " + pCalMonth.getFullYear(); buildCalendar(document.getElementById("p-daygrid"), null, pCalMonth, function(d){ return classifyForCycle(d, state); }, function(d){ pSelectedDate = d; renderPeriodDetail(); renderPeriodCalendar(); }, pSelectedDate ); } function renderPeriodDetail(){ var state = getCycleState(); var info = classifyForCycle(pSelectedDate, state); var phaseKey = phaseForDayInCycle(info.dic, state.periodLen, info.ovDic); var phase = PHASES[phaseKey]; var el = document.getElementById("p-detail"); el.innerHTML = ''+pSelectedDate.toDateString()+''+ ''+phase.title+' phase'+(info.fertile?' · fertile window':'')+''+ '
Cycle day '+(info.dic+1)+'
'+ '

'+phase.desc+'

'+ '
'+ '
Hormones
'+phase.hormone+'
'+ '
Typical symptoms
'+phase.symptoms+'
'+ '
'+ '

Care note — '+phase.tip+'

'; } function renderPeriodStats(){ var state = getCycleState(); var dic = mod(daysBetween(state.start, today), state.cycleLen); var ovDic = mod(state.cycleLen - 14, state.cycleLen); var daysToNextPeriod = mod(state.cycleLen - dic, state.cycleLen); if(daysToNextPeriod === 0 && dic !== 0) daysToNextPeriod = state.cycleLen; var daysToOvulation = mod(ovDic - dic, state.cycleLen); var phaseKey = phaseForDayInCycle(dic, state.periodLen, ovDic); document.getElementById("period-stats").innerHTML = statTile(dic+1, "Cycle day today") + statTile(PHASES[phaseKey].title, "Current phase") + statTile(dic===0 ? "Today" : (daysToNextPeriod+"d"), "Until next period") + statTile(dic===ovDic ? "Today" : (daysToOvulation+"d"), "Until ovulation"); // hero ring var cx=100, cy=100, r=88; var perStart=0, perEnd = state.periodLen/state.cycleLen*360; var follEnd = ovDic/state.cycleLen*360; var fertStart = Math.max(0,(ovDic-5))/state.cycleLen*360; var lutStart = (ovDic+1)/state.cycleLen*360; document.getElementById("phase-arc-menstrual").setAttribute("d", arcPath(cx,cy,r,perStart,perEnd)); document.getElementById("phase-arc-follicular").setAttribute("d", arcPath(cx,cy,r,perEnd,fertStart)); document.getElementById("phase-arc-fertile").setAttribute("d", arcPath(cx,cy,r,fertStart,follEnd+ (360/state.cycleLen))); document.getElementById("phase-arc-luteal").setAttribute("d", arcPath(cx,cy,r,lutStart,360)); var todayAngle = dic/state.cycleLen*360; var tp = polarToXY(cx,cy,r,todayAngle); document.getElementById("today-marker").setAttribute("cx", tp[0]); document.getElementById("today-marker").setAttribute("cy", tp[1]); document.getElementById("cycle-day-big").textContent = dic+1; // hand the same numbers to the optional 3D ring (ring3d.js) window.cycleRingData = {perEnd:perEnd, fertStart:fertStart, fertEnd:follEnd+(360/state.cycleLen), lutStart:lutStart, todayAngle:todayAngle}; window.dispatchEvent(new CustomEvent("cycle:ring", {detail: window.cycleRingData})); } function statTile(num,label){ return '
'+num+'
'+label+'
'; } document.getElementById("p-prev").addEventListener("click", function(){ pCalMonth.setMonth(pCalMonth.getMonth()-1); renderPeriodCalendar(); }); document.getElementById("p-next").addEventListener("click", function(){ pCalMonth.setMonth(pCalMonth.getMonth()+1); renderPeriodCalendar(); }); function renderPhaseRefGrid(){ var order = ["menstrual","follicular","ovulatory","luteal"]; document.getElementById("phase-ref-grid").innerHTML = order.map(function(key,i){ var p = PHASES[key]; return '
0'+(i+1)+'

'+p.title+'

'+ '

'+p.desc+'

'+ '
'+p.window+'
'; }).join(""); } /* ---------------- OVULATION tab render ---------------- */ var ovSelectedOffset = 0; function renderOvTimeline(){ var wrap = document.getElementById("ov-timeline"); wrap.innerHTML = ""; for(var off=-5; off<=5; off++){ var d = OVULATION_DAYS[String(off)]; var btn = document.createElement("button"); btn.className = "tl-btn" + (off===ovSelectedOffset ? " active":""); btn.innerHTML = '
'+(off===0?"OVUL.":(off>0?"+":"")+off)+'
'+ '
'; btn.addEventListener("click", function(o){ return function(){ ovSelectedOffset=o; renderOvTimeline(); renderOvDetailFromOffset(); }; }(off)); wrap.appendChild(btn); } } function renderOvDetailFromOffset(){ var d = OVULATION_DAYS[String(ovSelectedOffset)]; var el = document.getElementById("o-detail"); var label = ovSelectedOffset===0 ? "Ovulation day" : (ovSelectedOffset>0 ? ovSelectedOffset+" day"+(ovSelectedOffset>1?"s":"")+" after ovulation" : Math.abs(ovSelectedOffset)+" day"+(ovSelectedOffset<-1?"s":"")+" before ovulation"); el.innerHTML = ''+label+''+ 'Fertility: '+d.fert+''+ '
'+(ovSelectedOffset===0?"Egg release":(ovSelectedOffset<0?"Building fertility":"Fertility fading"))+'
'+ '
'+ '
Basal body temp
'+d.bbt+'
'+ '
Cervical mucus
'+d.mucus+'
'+ '
'+ '

'+d.note+'

'; } function renderOvulationCalendar(){ var state = getCycleState(); document.getElementById("o-cal-title").textContent = MONTH_NAMES[oCalMonth.getMonth()] + " " + oCalMonth.getFullYear(); buildCalendar(document.getElementById("o-daygrid"), null, oCalMonth, function(d){ return classifyForCycle(d, state); }, function(d){ oSelectedDate = d; var info = classifyForCycle(d, state); var offset = info.dic - info.ovDic; if(offset < -5) offset = -5; if(offset > 5) offset = 5; ovSelectedOffset = offset; renderOvTimeline(); renderOvDetailFromOffset(); renderOvulationCalendar(); }, oSelectedDate ); } function renderOvulationStats(){ var state = getCycleState(); var dic = mod(daysBetween(state.start, today), state.cycleLen); var ovDic = mod(state.cycleLen - 14, state.cycleLen); var daysToOvulation = mod(ovDic - dic, state.cycleLen); var fertileStartDic = mod(ovDic-5, state.cycleLen); var inFertile = isFertile(dic, ovDic); var daysToFertile = inFertile ? 0 : mod(fertileStartDic - dic, state.cycleLen); document.getElementById("ovulation-stats").innerHTML = statTile(inFertile ? "Yes" : "No", "In fertile window") + statTile(dic===ovDic ? "Today" : daysToOvulation+"d", "Until ovulation") + statTile(inFertile ? "Now" : daysToFertile+"d", "Fertile window") + statTile("6 days", "Window length"); } document.getElementById("o-prev").addEventListener("click", function(){ oCalMonth.setMonth(oCalMonth.getMonth()-1); renderOvulationCalendar(); }); document.getElementById("o-next").addEventListener("click", function(){ oCalMonth.setMonth(oCalMonth.getMonth()+1); renderOvulationCalendar(); }); /* ================================================================ PREGNANCY tab ================================================================ */ var lmpInput = document.getElementById("pr-lmp"); var exampleLMP = addDays(today, -20*7 - 3); var savedLMP = loadLS("ca-lmp", null); lmpInput.value = savedLMP || toInputVal(exampleLMP); if(savedLMP) document.getElementById("pr-example-note").style.display = "none"; var slider = document.getElementById("week-slider"); var prCalMonth = new Date(today.getFullYear(), today.getMonth(), 1); lmpInput.addEventListener("input", function(){ saveLS("ca-lmp", lmpInput.value); document.getElementById("pr-example-note").style.display = "none"; document.getElementById("p-example-note").style.display = "none"; syncSliderToLMP(); renderPregnancy(); }); function getLMP(){ return fromInput(lmpInput.value) || exampleLMP; } function currentGestWeek(){ var lmp = getLMP(); var w = Math.floor(daysBetween(lmp,today)/7) + 1; return Math.max(1, Math.min(40, w)); } function syncSliderToLMP(){ slider.value = currentGestWeek(); } syncSliderToLMP(); function renderPregnancyStats(){ var lmp = getLMP(); var due = addDays(lmp, 280); var week = currentGestWeek(); var daysLeft = Math.max(0, daysBetween(today, due)); document.getElementById("pregnancy-stats").innerHTML = statTile(week, "Weeks along") + statTile(Math.floor((week-1)/13)+1 <= 3 ? ["First","Second","Third"][Math.min(2,Math.floor((week-1)/13))] : "Third", "Trimester") + statTile(daysLeft, "Days to due date") + statTile(due.toLocaleDateString(undefined,{month:"short",day:"numeric"}), "Estimated due date"); var pct = (week-1)/39; var pathLen = 230; var offset = pathLen - pct*pathLen; var path = document.getElementById("pregnancy-progress-path"); path.setAttribute("stroke-dashoffset", offset.toFixed(1)); try{ var totalLen = path.getTotalLength ? path.getTotalLength() : pathLen; var pt = path.getPointAtLength ? path.getPointAtLength(pct*totalLen) : null; if(pt) { document.getElementById("pregnancy-progress-dot").setAttribute("cx",pt.x); document.getElementById("pregnancy-progress-dot").setAttribute("cy",pt.y); } }catch(e){} } function renderWeekPanel(week){ var w = WEEKS[week-1]; document.getElementById("wk-badge").textContent = "WEEK " + week; document.getElementById("wk-compare").textContent = "size of " + w.c; document.getElementById("wk-metric").textContent = w.m === "n/a" ? "not yet fertilized" : "≈ " + w.m; document.getElementById("wk-dev").textContent = w.dev; document.getElementById("wk-body").textContent = w.body; document.getElementById("wk-tip").textContent = w.tip; var tri = week<=13?1:(week<=27?2:3); var triColor = "var(--pregnancy)"; document.getElementById("wk-blob").innerHTML = ''; document.querySelectorAll(".film-item").forEach(function(f){ f.classList.toggle("active", +f.dataset.week===week); }); var active = document.querySelector('.film-item[data-week="'+week+'"]'); if(active) active.scrollIntoView({inline:"center", block:"nearest", behavior:"smooth"}); } slider.addEventListener("input", function(){ renderWeekPanel(+slider.value); }); function buildFilmstrip(){ var wrap = document.getElementById("week-filmstrip"); wrap.innerHTML = ""; for(var w=1; w<=40; w++){ var item = document.createElement("button"); item.className = "film-item"; item.dataset.week = w; item.innerHTML = '
WK
'+w+'
'; item.addEventListener("click", function(ww){ return function(){ slider.value=ww; renderWeekPanel(ww); }; }(w)); wrap.appendChild(item); } } function renderPregnancyCalendar(){ var lmp = getLMP(); document.getElementById("pr-cal-title").textContent = MONTH_NAMES[prCalMonth.getMonth()] + " " + prCalMonth.getFullYear(); var grid = document.getElementById("pr-daygrid"); grid.innerHTML = ""; var year = prCalMonth.getFullYear(), month = prCalMonth.getMonth(); var firstDow = new Date(year,month,1).getDay(); var daysInMonth = new Date(year,month+1,0).getDate(); var totalCells = Math.ceil((firstDow+daysInMonth)/7)*7; for(var i=0;idaysInMonth){ cell.classList.add("pad"); grid.appendChild(cell); continue; } var d = new Date(year,month,dayNum); var wk = Math.floor(daysBetween(lmp,d)/7)+1; var inRange = wk>=1 && wk<=42; cell.innerHTML = ""+dayNum+"" + (inRange ? 'W'+wk+'' : ""); if(inRange && wk<=40) cell.classList.add("is-fertile"); if(sameDay(d,today)) cell.classList.add("today"); cell.addEventListener("click", function(ww){ return function(){ if(ww>=1&&ww<=40){ slider.value=ww; renderWeekPanel(ww); } }; }(wk)); grid.appendChild(cell); } } document.getElementById("pr-prev").addEventListener("click", function(){ prCalMonth.setMonth(prCalMonth.getMonth()-1); renderPregnancyCalendar(); }); document.getElementById("pr-next").addEventListener("click", function(){ prCalMonth.setMonth(prCalMonth.getMonth()+1); renderPregnancyCalendar(); }); function renderPregnancy(){ renderPregnancyStats(); renderWeekPanel(currentGestWeek()); renderPregnancyCalendar(); } /* ================================================================ INIT ================================================================ */ function renderAll(){ renderPeriodStats(); renderPeriodCalendar(); renderPeriodDetail(); renderOvulationStats(); renderOvTimeline(); renderOvDetailFromOffset(); renderOvulationCalendar(); renderPregnancy(); } renderPhaseRefGrid(); buildFilmstrip(); slider.value = currentGestWeek(); renderAll(); })();