paulo@81: <html> paulo@81: <head> paulo@81: <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> paulo@81: <title>Life Calendar</title> paulo@81: <style type="text/css"> paulo@81: body { paulo@81: padding: 0; paulo@81: margin: 0; paulo@81: position: relative; paulo@81: font-family: sans-serif; paulo@81: background-size: cover; paulo@81: } paulo@81: paulo@81: a, a:visited { paulo@81: color: #70BCF9; paulo@81: } paulo@81: paulo@81: table { paulo@81: width: 100%; paulo@81: height: 100%; paulo@81: } paulo@81: paulo@81: table, td { paulo@81: border: 1px solid; paulo@81: border-collapse: collapse; paulo@81: padding: 0; paulo@81: margin: 0; paulo@81: } paulo@81: paulo@81: div#welcome { paulo@81: display: none; paulo@81: paulo@81: width: 50vw; paulo@81: margin: 8vh auto 0; paulo@81: } paulo@81: paulo@81: div#welcome h1 { paulo@81: text-align: center; paulo@81: text-transform: uppercase; paulo@81: font-family: serif; paulo@81: font-size: 4em; paulo@81: } paulo@81: div#welcome form { paulo@81: border: 1px solid #999999; paulo@81: padding: 10px 20px; paulo@81: } paulo@81: paulo@81: div#welcome .settings { paulo@81: margin-left: 5px; paulo@81: padding: 5px; paulo@81: } paulo@81: paulo@81: div#welcome div#footer { paulo@81: color: #a6a6a6; paulo@81: padding-top: 50px; paulo@81: text-align: center; paulo@81: } paulo@81: paulo@81: #settingsForm span.unhappyMessage { paulo@81: font-size: 0.8em; paulo@81: padding-left: 10px; paulo@81: } paulo@81: paulo@81: div#tooltip { paulo@81: background-color: #5070D0; paulo@81: padding: 0.5em; paulo@81: } paulo@81: paulo@81: div#tooltip h1 { paulo@81: color: #DFDFDF; paulo@81: font-size: 1.25em; paulo@81: margin: 1px; paulo@81: } paulo@81: paulo@81: div#tooltip h2 { paulo@81: color: #AAAAAA; paulo@81: font-size: 0.75em; paulo@81: font-weight: normal; paulo@81: margin: 1px; paulo@81: } paulo@81: paulo@86: div#tooltip h3 { paulo@86: color: #333333; paulo@86: font-size: 0.8em; paulo@86: margin: 2px; paulo@86: } paulo@86: paulo@81: td { paulo@81: border-color: #FFFFFF; paulo@81: } paulo@81: paulo@81: table { paulo@81: border-color: #FFFFFF; paulo@81: } paulo@81: paulo@81: tr.previous td { paulo@81: background-color: #BBBBBB; paulo@81: } paulo@81: paulo@81: tr.current td.partial { paulo@81: background-color: #CCDDCC; paulo@81: } paulo@81: paulo@81: tr.current td.today { paulo@81: background-color: #DDDDCC; paulo@81: } paulo@81: paulo@81: tr.current td.future { paulo@81: background-color: #DDCCCC; paulo@81: } paulo@81: paulo@81: tr.future { paulo@81: background-color: #DDDDDD; paulo@81: } paulo@81: paulo@81: </style> paulo@81: <script src="jquery-2.1.3.min.js"></script> paulo@81: <script src="happy.js"></script> paulo@81: <script src="tooltip.js"></script> paulo@81: paulo@81: <script type="text/javascript"> paulo@81: var nWeeks = 52; paulo@81: var nMsPerDay = 3600 * 24 * 1000; paulo@81: var nMsPerWeek = 7 * nMsPerDay; paulo@81: var QSParams; paulo@81: paulo@81: var birthday; paulo@81: var birthdayString; paulo@81: var birthdayDate; paulo@81: paulo@81: var lifespan; paulo@81: paulo@81: var age; paulo@81: var ageYears; paulo@81: var remainder; paulo@81: paulo@81: var previousYears; paulo@81: var futureYears; paulo@81: var remainderWeeks; paulo@81: var restOfYearWeeks; paulo@81: paulo@86: var selecting = false; paulo@86: var start_wyc = null; paulo@86: var end_wyc = null; paulo@86: paulo@81: function validateBirthday(birthday) { paulo@81: var birthdayPattern = /^[0-9]{4}-[0-9]{2}-[0-9]{2}$/; paulo@81: return birthdayPattern.test( $("#birthday").val()); paulo@81: } paulo@81: paulo@81: function validateLifespan(lifespan) { paulo@81: var lifespanPattern = /^[0-9]{1,3}$/; paulo@81: return lifespanPattern.test( $("#lifespan").val()); paulo@81: } paulo@81: paulo@81: // Read a page's GET URL variables and return them as an associative array. paulo@81: // From http://jquery-howto.blogspot.ca/2009/09/get-url-parameters-values-with-jquery.html paulo@81: function getQSParams() paulo@81: { paulo@81: var vars = [], hash; paulo@81: var hashes = window.location.href.slice(window.location.href.indexOf('?') + 1).split('&'); paulo@81: for(var i = 0; i < hashes.length; i++) paulo@81: { paulo@81: hash = hashes[i].split('='); paulo@81: vars.push(hash[0]); paulo@81: vars[hash[0]] = hash[1]; paulo@81: } paulo@81: return vars; paulo@81: } paulo@81: paulo@81: function isLeapYear(year) { paulo@81: var d = new Date(year, 1, 28); paulo@81: d.setDate(d.getDate() + 1); paulo@81: return d.getMonth() == 1; paulo@81: } paulo@81: paulo@81: function getAge(date) { paulo@81: var d = new Date(date), now = new Date(); paulo@81: var years = now.getFullYear() - d.getFullYear(); paulo@81: d.setFullYear(d.getFullYear() + years); paulo@81: if (d > now) { paulo@81: years--; paulo@81: d.setFullYear(d.getFullYear() - 1); paulo@81: } paulo@81: var days = (now.getTime() - d.getTime()) / nMsPerDay; paulo@81: return years + days / (isLeapYear(now.getFullYear()) ? 366 : 365); paulo@81: } paulo@81: paulo@81: function getCalendar(d, week, year) { paulo@81: var c = new Date(d); paulo@81: c.setFullYear(d.getFullYear() + year); paulo@81: c.setTime(c.getTime() + (week * nMsPerWeek)); paulo@81: return c; paulo@81: } paulo@81: paulo@81: function getWeekYearCal(e, week) { paulo@81: var yearParent = e.parentNode; paulo@81: var calParent = yearParent.parentNode; paulo@81: var year; paulo@81: for (year = 0; year < calParent.childNodes.length; year++) { paulo@81: if (calParent.childNodes[year] === yearParent) { paulo@81: break; paulo@81: } paulo@81: } paulo@81: return { paulo@81: week: week, paulo@81: year: year, paulo@86: cal: getCalendar(birthdayDate, week, year), paulo@86: weeks: nWeeks*year + week, paulo@81: } paulo@81: } paulo@81: paulo@86: function inSpan(x, s, e) { paulo@86: return x >= s && x <= e; paulo@86: } paulo@86: paulo@81: function loop(x, f) { paulo@81: for (var i = 0; i < x; f(i++)); paulo@81: } paulo@81: paulo@81: function mapLoop(x, f) { paulo@81: var ret = []; paulo@81: for (var i = 0; i < x; i++) { paulo@81: ret.push(f(i)); paulo@81: } paulo@81: return ret; paulo@81: } paulo@81: paulo@86: function setStyle(e) { paulo@86: var styles = []; paulo@86: paulo@86: if (e["_fill_color"] != undefined) { paulo@86: styles.push("background-color: " + e._fill_color); paulo@86: } paulo@86: if (e["_border_color"] != undefined) { paulo@86: styles.push("border-color: " + e._border_color); paulo@86: } paulo@86: paulo@86: if (styles.length == 0) { paulo@86: e.setAttribute("style", undefined); paulo@86: } else { paulo@86: e.setAttribute("style", styles.join('; ')); paulo@86: } paulo@86: } paulo@86: paulo@86: function highlightDistance(e0, e1_wyc, e2_wyc) { paulo@86: var calParent = e0.parentNode.parentNode; paulo@86: var min_wyc = (e1_wyc.weeks <= e2_wyc.weeks) ? e1_wyc : e2_wyc; paulo@86: var max_wyc = (e2_wyc == min_wyc) ? e1_wyc : e2_wyc; paulo@86: for (var year = 0; year < lifespan; year++) { paulo@86: for (var week = 0; week < nWeeks; week++) { paulo@86: w = nWeeks*year + week; paulo@86: e = calParent.childNodes[year].childNodes[week] paulo@86: if (inSpan(w, min_wyc.weeks, max_wyc.weeks)) { paulo@86: e._border_color = "black"; paulo@86: } else { paulo@86: e._border_color = undefined; paulo@86: } paulo@86: setStyle(e); paulo@86: } paulo@86: } paulo@86: } paulo@86: paulo@81: function cycleColors(e) { paulo@81: var colors = [ paulo@81: undefined, paulo@81: "black", paulo@81: "red", paulo@81: "green", paulo@81: "blue", paulo@81: "white", paulo@81: ]; paulo@81: paulo@81: var i = 0; paulo@81: for (; i < colors.length, colors[i] != e._fill_color; i++); paulo@81: e._fill_color = colors[(i + 1) % colors.length]; paulo@86: setStyle(e); paulo@81: } paulo@81: paulo@81: function _createElement(tagName, className) { paulo@81: var e = document.createElement(tagName); paulo@81: if (className) { paulo@81: e.className = className; paulo@81: } paulo@81: return e; paulo@81: } paulo@81: paulo@81: function weekElem(className) { paulo@81: return _createElement("td", className); paulo@81: } paulo@81: paulo@81: function yearElem(className) { paulo@81: return _createElement("tr", className); paulo@81: } paulo@81: paulo@81: function yearWeekElems() { paulo@81: return mapLoop(nWeeks, function(i) { paulo@81: var e = weekElem(); paulo@81: addWeekMouseEvents(e, i); paulo@81: return e; paulo@81: }); paulo@81: } paulo@81: paulo@81: function addWeekMouseEvents(e, i) { paulo@81: e.addEventListener("mouseover", function(evt) { paulo@81: var wyc = getWeekYearCal(e, i); paulo@81: var calYear = wyc.cal.getFullYear(); paulo@81: var calMonth = wyc.cal.getMonth() + 1; paulo@81: var calDay = wyc.cal.getDate(); paulo@86: var title = calYear + '-' + calMonth + '-' + calDay; paulo@86: var text = "(Week: " + wyc.week + ", Year: " + wyc.year + ")"; paulo@86: var subtext = null; paulo@86: if (selecting) { paulo@86: end_wyc = wyc; paulo@86: highlightDistance(e, start_wyc, end_wyc); paulo@86: } paulo@86: if (start_wyc && end_wyc) { paulo@86: var min_wyc = (start_wyc.weeks <= end_wyc.weeks) ? start_wyc : end_wyc; paulo@86: var max_wyc = (end_wyc == min_wyc) ? start_wyc : end_wyc; paulo@86: if (inSpan(wyc.weeks, min_wyc.weeks, max_wyc.weeks)) { paulo@86: var sel_weeks = max_wyc.weeks - min_wyc.weeks; paulo@86: var sel_years = sel_weeks/nWeeks; paulo@86: subtext = "[Selected: weeks: " + sel_weeks + " years: " + sel_years.toFixed(2) + "]"; paulo@86: } paulo@86: } paulo@86: createTooltip(evt, title, text, subtext); paulo@81: }); paulo@81: e.addEventListener("click", function() { paulo@86: var wyc = getWeekYearCal(e, i); paulo@86: highlightDistance(e, wyc, wyc); paulo@86: if (start_wyc && end_wyc) { paulo@86: start_wyc = null; paulo@86: end_wyc = null; paulo@86: } else { paulo@86: cycleColors(e); paulo@86: } paulo@86: }); paulo@86: e.addEventListener("mousedown", function() { paulo@86: selecting = true; paulo@86: start_wyc = getWeekYearCal(e, i); paulo@86: }); paulo@86: e.addEventListener("mouseup", function() { paulo@86: selecting = false; paulo@81: }); paulo@81: } paulo@81: paulo@81: $( document ).ready(function() { paulo@81: $( "#settingsForm" ).isHappy({ paulo@81: fields: { paulo@81: '#birthday': { paulo@81: required: true, paulo@81: test: validateBirthday, paulo@81: message: 'Please enter your birthday (like YYYY-MM-DD).' paulo@81: }, paulo@81: '#lifespan': { paulo@81: required: true, paulo@81: test: validateLifespan, paulo@81: message: 'Please enter your expected lifespan (like 90)' paulo@81: } paulo@81: } paulo@81: }); paulo@81: paulo@81: QSParams = getQSParams(); paulo@81: paulo@81: if(!("birthday" in QSParams) | !("lifespan" in QSParams)) { paulo@81: // Show the form paulo@81: $("#welcome").css("display", "block"); paulo@81: paulo@81: // Give the first field focus paulo@81: $("#birthday").focus(); paulo@81: return; paulo@81: } paulo@81: paulo@81: $("body").append( '<table id="calendar"></table>' ); paulo@81: paulo@81: birthday = QSParams["birthday"]; paulo@81: birthdayString = birthday + "T00:00:00"; paulo@81: birthdayDate = new Date(birthdayString); paulo@81: paulo@81: lifespan = QSParams["lifespan"]; paulo@81: paulo@81: age = getAge(birthdayDate); paulo@81: ageYears = Math.floor(age); paulo@81: remainder = age - ageYears; paulo@81: paulo@81: previousYears = (ageYears).toFixed(0); paulo@81: paulo@81: if( lifespan > ageYears) { paulo@81: futureYears = lifespan - ageYears - 1; paulo@81: remainderWeeks = Math.floor(remainder * nWeeks); paulo@81: restOfYearWeeks = Math.ceil(nWeeks - remainderWeeks) - 1; paulo@81: } else { paulo@81: futureYears = 0; paulo@81: remainderWeeks = nWeeks; paulo@81: restOfYearWeeks = 0; paulo@81: } paulo@81: paulo@81: // Fill in lived years paulo@81: loop(previousYears, function() { paulo@81: var tr = yearElem("previous"); paulo@81: $(tr).append(yearWeekElems()); paulo@81: $("#calendar").append(tr); paulo@81: }) paulo@81: paulo@81: // Fill in the current birth-year (the number of weeks elapsed since the most recent birthday.) paulo@81: var current_tr = yearElem("current"); paulo@81: loop(remainderWeeks, function(i) { paulo@81: var e = weekElem("partial"); paulo@81: addWeekMouseEvents(e, i); paulo@81: $(current_tr).append(e); paulo@81: }) paulo@81: var e = weekElem("today"); paulo@81: addWeekMouseEvents(e, remainderWeeks); paulo@81: $(current_tr).append(e); paulo@81: loop(restOfYearWeeks, function(i) { paulo@81: var e = weekElem("future"); paulo@81: addWeekMouseEvents(e, remainderWeeks + 1 + i); paulo@81: $(current_tr).append(e); paulo@81: }) paulo@81: $("#calendar").append(current_tr); paulo@81: paulo@81: // Fill in future years paulo@81: loop(futureYears, function() { paulo@81: var tr = yearElem("future"); paulo@81: $(tr).append(yearWeekElems()); paulo@81: $("#calendar").append(tr); paulo@81: }) paulo@81: }); paulo@81: </script> paulo@81: </head> paulo@81: <body> paulo@81: <div id="welcome"> paulo@81: <h1>Life Calendar</h1> paulo@81: <p> paulo@81: A minimalist life calendar. Shows the number of weeks you've lived and the number of weeks you paulo@81: have left. paulo@81: </p> paulo@81: <form method="get" action="" id="settingsForm"> paulo@81: <p> paulo@81: <label for="birthday">Birthday: paulo@81: <input class="settings" id="birthday" name="birthday" placeholder="1985-01-01"/> paulo@81: </label> paulo@81: </p> paulo@81: <p> paulo@81: <label for="lifespan">Life expectancy: paulo@81: <input class="settings" id="lifespan" name="lifespan" placeholder="90"/> paulo@81: </label> paulo@81: </p> paulo@81: <p> paulo@81: <input type="submit" id="submit" value="Show calendar" /> paulo@81: </p> paulo@81: </form> paulo@81: <div id="footer"> paulo@81: <p> paulo@81: Based on <a href="http://waitbutwhy.com/2014/05/life-weeks.html">Your Life in Weeks</a> paulo@81: and <a href="http://count.life">count.life</a>. paulo@81: </p> paulo@81: </div> paulo@81: </div> paulo@81: </body> paulo@81: </html>