rev |
line source |
paulo@81
|
1 <html>
|
paulo@81
|
2 <head>
|
paulo@81
|
3 <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
|
paulo@81
|
4 <title>Life Calendar</title>
|
paulo@81
|
5 <style type="text/css">
|
paulo@81
|
6 body {
|
paulo@81
|
7 padding: 0;
|
paulo@81
|
8 margin: 0;
|
paulo@81
|
9 position: relative;
|
paulo@81
|
10 font-family: sans-serif;
|
paulo@81
|
11 background-size: cover;
|
paulo@81
|
12 }
|
paulo@81
|
13
|
paulo@81
|
14 a, a:visited {
|
paulo@81
|
15 color: #70BCF9;
|
paulo@81
|
16 }
|
paulo@81
|
17
|
paulo@81
|
18 table {
|
paulo@81
|
19 width: 100%;
|
paulo@81
|
20 height: 100%;
|
paulo@81
|
21 }
|
paulo@81
|
22
|
paulo@81
|
23 table, td {
|
paulo@81
|
24 border: 1px solid;
|
paulo@81
|
25 border-collapse: collapse;
|
paulo@81
|
26 padding: 0;
|
paulo@81
|
27 margin: 0;
|
paulo@81
|
28 }
|
paulo@81
|
29
|
paulo@81
|
30 div#welcome {
|
paulo@81
|
31 display: none;
|
paulo@81
|
32
|
paulo@81
|
33 width: 50vw;
|
paulo@81
|
34 margin: 8vh auto 0;
|
paulo@81
|
35 }
|
paulo@81
|
36
|
paulo@81
|
37 div#welcome h1 {
|
paulo@81
|
38 text-align: center;
|
paulo@81
|
39 text-transform: uppercase;
|
paulo@81
|
40 font-family: serif;
|
paulo@81
|
41 font-size: 4em;
|
paulo@81
|
42 }
|
paulo@81
|
43 div#welcome form {
|
paulo@81
|
44 border: 1px solid #999999;
|
paulo@81
|
45 padding: 10px 20px;
|
paulo@81
|
46 }
|
paulo@81
|
47
|
paulo@81
|
48 div#welcome .settings {
|
paulo@81
|
49 margin-left: 5px;
|
paulo@81
|
50 padding: 5px;
|
paulo@81
|
51 }
|
paulo@81
|
52
|
paulo@81
|
53 div#welcome div#footer {
|
paulo@81
|
54 color: #a6a6a6;
|
paulo@81
|
55 padding-top: 50px;
|
paulo@81
|
56 text-align: center;
|
paulo@81
|
57 }
|
paulo@81
|
58
|
paulo@81
|
59 #settingsForm span.unhappyMessage {
|
paulo@81
|
60 font-size: 0.8em;
|
paulo@81
|
61 padding-left: 10px;
|
paulo@81
|
62 }
|
paulo@81
|
63
|
paulo@81
|
64 div#tooltip {
|
paulo@81
|
65 background-color: #5070D0;
|
paulo@81
|
66 padding: 0.5em;
|
paulo@81
|
67 }
|
paulo@81
|
68
|
paulo@81
|
69 div#tooltip h1 {
|
paulo@81
|
70 color: #DFDFDF;
|
paulo@81
|
71 font-size: 1.25em;
|
paulo@81
|
72 margin: 1px;
|
paulo@81
|
73 }
|
paulo@81
|
74
|
paulo@81
|
75 div#tooltip h2 {
|
paulo@81
|
76 color: #AAAAAA;
|
paulo@81
|
77 font-size: 0.75em;
|
paulo@81
|
78 font-weight: normal;
|
paulo@81
|
79 margin: 1px;
|
paulo@81
|
80 }
|
paulo@81
|
81
|
paulo@86
|
82 div#tooltip h3 {
|
paulo@86
|
83 color: #333333;
|
paulo@86
|
84 font-size: 0.8em;
|
paulo@86
|
85 margin: 2px;
|
paulo@86
|
86 }
|
paulo@86
|
87
|
paulo@81
|
88 td {
|
paulo@81
|
89 border-color: #FFFFFF;
|
paulo@81
|
90 }
|
paulo@81
|
91
|
paulo@81
|
92 table {
|
paulo@81
|
93 border-color: #FFFFFF;
|
paulo@81
|
94 }
|
paulo@81
|
95
|
paulo@81
|
96 tr.previous td {
|
paulo@81
|
97 background-color: #BBBBBB;
|
paulo@81
|
98 }
|
paulo@81
|
99
|
paulo@81
|
100 tr.current td.partial {
|
paulo@81
|
101 background-color: #CCDDCC;
|
paulo@81
|
102 }
|
paulo@81
|
103
|
paulo@81
|
104 tr.current td.today {
|
paulo@81
|
105 background-color: #DDDDCC;
|
paulo@81
|
106 }
|
paulo@81
|
107
|
paulo@81
|
108 tr.current td.future {
|
paulo@81
|
109 background-color: #DDCCCC;
|
paulo@81
|
110 }
|
paulo@81
|
111
|
paulo@81
|
112 tr.future {
|
paulo@81
|
113 background-color: #DDDDDD;
|
paulo@81
|
114 }
|
paulo@81
|
115
|
paulo@81
|
116 </style>
|
paulo@81
|
117 <script src="jquery-2.1.3.min.js"></script>
|
paulo@81
|
118 <script src="happy.js"></script>
|
paulo@81
|
119 <script src="tooltip.js"></script>
|
paulo@81
|
120
|
paulo@81
|
121 <script type="text/javascript">
|
paulo@81
|
122 var nWeeks = 52;
|
paulo@81
|
123 var nMsPerDay = 3600 * 24 * 1000;
|
paulo@81
|
124 var nMsPerWeek = 7 * nMsPerDay;
|
paulo@81
|
125 var QSParams;
|
paulo@81
|
126
|
paulo@81
|
127 var birthday;
|
paulo@81
|
128 var birthdayString;
|
paulo@81
|
129 var birthdayDate;
|
paulo@81
|
130
|
paulo@81
|
131 var lifespan;
|
paulo@81
|
132
|
paulo@81
|
133 var age;
|
paulo@81
|
134 var ageYears;
|
paulo@81
|
135 var remainder;
|
paulo@81
|
136
|
paulo@81
|
137 var previousYears;
|
paulo@81
|
138 var futureYears;
|
paulo@81
|
139 var remainderWeeks;
|
paulo@81
|
140 var restOfYearWeeks;
|
paulo@81
|
141
|
paulo@86
|
142 var selecting = false;
|
paulo@86
|
143 var start_wyc = null;
|
paulo@86
|
144 var end_wyc = null;
|
paulo@86
|
145
|
paulo@81
|
146 function validateBirthday(birthday) {
|
paulo@81
|
147 var birthdayPattern = /^[0-9]{4}-[0-9]{2}-[0-9]{2}$/;
|
paulo@81
|
148 return birthdayPattern.test( $("#birthday").val());
|
paulo@81
|
149 }
|
paulo@81
|
150
|
paulo@81
|
151 function validateLifespan(lifespan) {
|
paulo@81
|
152 var lifespanPattern = /^[0-9]{1,3}$/;
|
paulo@81
|
153 return lifespanPattern.test( $("#lifespan").val());
|
paulo@81
|
154 }
|
paulo@81
|
155
|
paulo@81
|
156 // Read a page's GET URL variables and return them as an associative array.
|
paulo@81
|
157 // From http://jquery-howto.blogspot.ca/2009/09/get-url-parameters-values-with-jquery.html
|
paulo@81
|
158 function getQSParams()
|
paulo@81
|
159 {
|
paulo@81
|
160 var vars = [], hash;
|
paulo@81
|
161 var hashes = window.location.href.slice(window.location.href.indexOf('?') + 1).split('&');
|
paulo@81
|
162 for(var i = 0; i < hashes.length; i++)
|
paulo@81
|
163 {
|
paulo@81
|
164 hash = hashes[i].split('=');
|
paulo@81
|
165 vars.push(hash[0]);
|
paulo@81
|
166 vars[hash[0]] = hash[1];
|
paulo@81
|
167 }
|
paulo@81
|
168 return vars;
|
paulo@81
|
169 }
|
paulo@81
|
170
|
paulo@81
|
171 function isLeapYear(year) {
|
paulo@81
|
172 var d = new Date(year, 1, 28);
|
paulo@81
|
173 d.setDate(d.getDate() + 1);
|
paulo@81
|
174 return d.getMonth() == 1;
|
paulo@81
|
175 }
|
paulo@81
|
176
|
paulo@81
|
177 function getAge(date) {
|
paulo@81
|
178 var d = new Date(date), now = new Date();
|
paulo@81
|
179 var years = now.getFullYear() - d.getFullYear();
|
paulo@81
|
180 d.setFullYear(d.getFullYear() + years);
|
paulo@81
|
181 if (d > now) {
|
paulo@81
|
182 years--;
|
paulo@81
|
183 d.setFullYear(d.getFullYear() - 1);
|
paulo@81
|
184 }
|
paulo@81
|
185 var days = (now.getTime() - d.getTime()) / nMsPerDay;
|
paulo@81
|
186 return years + days / (isLeapYear(now.getFullYear()) ? 366 : 365);
|
paulo@81
|
187 }
|
paulo@81
|
188
|
paulo@81
|
189 function getCalendar(d, week, year) {
|
paulo@81
|
190 var c = new Date(d);
|
paulo@81
|
191 c.setFullYear(d.getFullYear() + year);
|
paulo@81
|
192 c.setTime(c.getTime() + (week * nMsPerWeek));
|
paulo@81
|
193 return c;
|
paulo@81
|
194 }
|
paulo@81
|
195
|
paulo@81
|
196 function getWeekYearCal(e, week) {
|
paulo@81
|
197 var yearParent = e.parentNode;
|
paulo@81
|
198 var calParent = yearParent.parentNode;
|
paulo@81
|
199 var year;
|
paulo@81
|
200 for (year = 0; year < calParent.childNodes.length; year++) {
|
paulo@81
|
201 if (calParent.childNodes[year] === yearParent) {
|
paulo@81
|
202 break;
|
paulo@81
|
203 }
|
paulo@81
|
204 }
|
paulo@81
|
205 return {
|
paulo@81
|
206 week: week,
|
paulo@81
|
207 year: year,
|
paulo@86
|
208 cal: getCalendar(birthdayDate, week, year),
|
paulo@86
|
209 weeks: nWeeks*year + week,
|
paulo@81
|
210 }
|
paulo@81
|
211 }
|
paulo@81
|
212
|
paulo@86
|
213 function inSpan(x, s, e) {
|
paulo@86
|
214 return x >= s && x <= e;
|
paulo@86
|
215 }
|
paulo@86
|
216
|
paulo@81
|
217 function loop(x, f) {
|
paulo@81
|
218 for (var i = 0; i < x; f(i++));
|
paulo@81
|
219 }
|
paulo@81
|
220
|
paulo@81
|
221 function mapLoop(x, f) {
|
paulo@81
|
222 var ret = [];
|
paulo@81
|
223 for (var i = 0; i < x; i++) {
|
paulo@81
|
224 ret.push(f(i));
|
paulo@81
|
225 }
|
paulo@81
|
226 return ret;
|
paulo@81
|
227 }
|
paulo@81
|
228
|
paulo@86
|
229 function setStyle(e) {
|
paulo@86
|
230 var styles = [];
|
paulo@86
|
231
|
paulo@86
|
232 if (e["_fill_color"] != undefined) {
|
paulo@86
|
233 styles.push("background-color: " + e._fill_color);
|
paulo@86
|
234 }
|
paulo@86
|
235 if (e["_border_color"] != undefined) {
|
paulo@86
|
236 styles.push("border-color: " + e._border_color);
|
paulo@86
|
237 }
|
paulo@86
|
238
|
paulo@86
|
239 if (styles.length == 0) {
|
paulo@86
|
240 e.setAttribute("style", undefined);
|
paulo@86
|
241 } else {
|
paulo@86
|
242 e.setAttribute("style", styles.join('; '));
|
paulo@86
|
243 }
|
paulo@86
|
244 }
|
paulo@86
|
245
|
paulo@86
|
246 function highlightDistance(e0, e1_wyc, e2_wyc) {
|
paulo@86
|
247 var calParent = e0.parentNode.parentNode;
|
paulo@86
|
248 var min_wyc = (e1_wyc.weeks <= e2_wyc.weeks) ? e1_wyc : e2_wyc;
|
paulo@86
|
249 var max_wyc = (e2_wyc == min_wyc) ? e1_wyc : e2_wyc;
|
paulo@86
|
250 for (var year = 0; year < lifespan; year++) {
|
paulo@86
|
251 for (var week = 0; week < nWeeks; week++) {
|
paulo@86
|
252 w = nWeeks*year + week;
|
paulo@86
|
253 e = calParent.childNodes[year].childNodes[week]
|
paulo@86
|
254 if (inSpan(w, min_wyc.weeks, max_wyc.weeks)) {
|
paulo@86
|
255 e._border_color = "black";
|
paulo@86
|
256 } else {
|
paulo@86
|
257 e._border_color = undefined;
|
paulo@86
|
258 }
|
paulo@86
|
259 setStyle(e);
|
paulo@86
|
260 }
|
paulo@86
|
261 }
|
paulo@86
|
262 }
|
paulo@86
|
263
|
paulo@81
|
264 function cycleColors(e) {
|
paulo@81
|
265 var colors = [
|
paulo@81
|
266 undefined,
|
paulo@81
|
267 "black",
|
paulo@81
|
268 "red",
|
paulo@81
|
269 "green",
|
paulo@81
|
270 "blue",
|
paulo@81
|
271 "white",
|
paulo@81
|
272 ];
|
paulo@81
|
273
|
paulo@81
|
274 var i = 0;
|
paulo@81
|
275 for (; i < colors.length, colors[i] != e._fill_color; i++);
|
paulo@81
|
276 e._fill_color = colors[(i + 1) % colors.length];
|
paulo@86
|
277 setStyle(e);
|
paulo@81
|
278 }
|
paulo@81
|
279
|
paulo@81
|
280 function _createElement(tagName, className) {
|
paulo@81
|
281 var e = document.createElement(tagName);
|
paulo@81
|
282 if (className) {
|
paulo@81
|
283 e.className = className;
|
paulo@81
|
284 }
|
paulo@81
|
285 return e;
|
paulo@81
|
286 }
|
paulo@81
|
287
|
paulo@81
|
288 function weekElem(className) {
|
paulo@81
|
289 return _createElement("td", className);
|
paulo@81
|
290 }
|
paulo@81
|
291
|
paulo@81
|
292 function yearElem(className) {
|
paulo@81
|
293 return _createElement("tr", className);
|
paulo@81
|
294 }
|
paulo@81
|
295
|
paulo@81
|
296 function yearWeekElems() {
|
paulo@81
|
297 return mapLoop(nWeeks, function(i) {
|
paulo@81
|
298 var e = weekElem();
|
paulo@81
|
299 addWeekMouseEvents(e, i);
|
paulo@81
|
300 return e;
|
paulo@81
|
301 });
|
paulo@81
|
302 }
|
paulo@81
|
303
|
paulo@81
|
304 function addWeekMouseEvents(e, i) {
|
paulo@81
|
305 e.addEventListener("mouseover", function(evt) {
|
paulo@81
|
306 var wyc = getWeekYearCal(e, i);
|
paulo@81
|
307 var calYear = wyc.cal.getFullYear();
|
paulo@81
|
308 var calMonth = wyc.cal.getMonth() + 1;
|
paulo@81
|
309 var calDay = wyc.cal.getDate();
|
paulo@86
|
310 var title = calYear + '-' + calMonth + '-' + calDay;
|
paulo@86
|
311 var text = "(Week: " + wyc.week + ", Year: " + wyc.year + ")";
|
paulo@86
|
312 var subtext = null;
|
paulo@86
|
313 if (selecting) {
|
paulo@86
|
314 end_wyc = wyc;
|
paulo@86
|
315 highlightDistance(e, start_wyc, end_wyc);
|
paulo@86
|
316 }
|
paulo@86
|
317 if (start_wyc && end_wyc) {
|
paulo@86
|
318 var min_wyc = (start_wyc.weeks <= end_wyc.weeks) ? start_wyc : end_wyc;
|
paulo@86
|
319 var max_wyc = (end_wyc == min_wyc) ? start_wyc : end_wyc;
|
paulo@86
|
320 if (inSpan(wyc.weeks, min_wyc.weeks, max_wyc.weeks)) {
|
paulo@86
|
321 var sel_weeks = max_wyc.weeks - min_wyc.weeks;
|
paulo@86
|
322 var sel_years = sel_weeks/nWeeks;
|
paulo@86
|
323 subtext = "[Selected: weeks: " + sel_weeks + " years: " + sel_years.toFixed(2) + "]";
|
paulo@86
|
324 }
|
paulo@86
|
325 }
|
paulo@86
|
326 createTooltip(evt, title, text, subtext);
|
paulo@81
|
327 });
|
paulo@81
|
328 e.addEventListener("click", function() {
|
paulo@86
|
329 var wyc = getWeekYearCal(e, i);
|
paulo@86
|
330 highlightDistance(e, wyc, wyc);
|
paulo@86
|
331 if (start_wyc && end_wyc) {
|
paulo@86
|
332 start_wyc = null;
|
paulo@86
|
333 end_wyc = null;
|
paulo@86
|
334 } else {
|
paulo@86
|
335 cycleColors(e);
|
paulo@86
|
336 }
|
paulo@86
|
337 });
|
paulo@86
|
338 e.addEventListener("mousedown", function() {
|
paulo@86
|
339 selecting = true;
|
paulo@86
|
340 start_wyc = getWeekYearCal(e, i);
|
paulo@86
|
341 });
|
paulo@86
|
342 e.addEventListener("mouseup", function() {
|
paulo@86
|
343 selecting = false;
|
paulo@81
|
344 });
|
paulo@81
|
345 }
|
paulo@81
|
346
|
paulo@81
|
347 $( document ).ready(function() {
|
paulo@81
|
348 $( "#settingsForm" ).isHappy({
|
paulo@81
|
349 fields: {
|
paulo@81
|
350 '#birthday': {
|
paulo@81
|
351 required: true,
|
paulo@81
|
352 test: validateBirthday,
|
paulo@81
|
353 message: 'Please enter your birthday (like YYYY-MM-DD).'
|
paulo@81
|
354 },
|
paulo@81
|
355 '#lifespan': {
|
paulo@81
|
356 required: true,
|
paulo@81
|
357 test: validateLifespan,
|
paulo@81
|
358 message: 'Please enter your expected lifespan (like 90)'
|
paulo@81
|
359 }
|
paulo@81
|
360 }
|
paulo@81
|
361 });
|
paulo@81
|
362
|
paulo@81
|
363 QSParams = getQSParams();
|
paulo@81
|
364
|
paulo@81
|
365 if(!("birthday" in QSParams) | !("lifespan" in QSParams)) {
|
paulo@81
|
366 // Show the form
|
paulo@81
|
367 $("#welcome").css("display", "block");
|
paulo@81
|
368
|
paulo@81
|
369 // Give the first field focus
|
paulo@81
|
370 $("#birthday").focus();
|
paulo@81
|
371 return;
|
paulo@81
|
372 }
|
paulo@81
|
373
|
paulo@81
|
374 $("body").append( '<table id="calendar"></table>' );
|
paulo@81
|
375
|
paulo@81
|
376 birthday = QSParams["birthday"];
|
paulo@81
|
377 birthdayString = birthday + "T00:00:00";
|
paulo@81
|
378 birthdayDate = new Date(birthdayString);
|
paulo@81
|
379
|
paulo@81
|
380 lifespan = QSParams["lifespan"];
|
paulo@81
|
381
|
paulo@81
|
382 age = getAge(birthdayDate);
|
paulo@81
|
383 ageYears = Math.floor(age);
|
paulo@81
|
384 remainder = age - ageYears;
|
paulo@81
|
385
|
paulo@81
|
386 previousYears = (ageYears).toFixed(0);
|
paulo@81
|
387
|
paulo@81
|
388 if( lifespan > ageYears) {
|
paulo@81
|
389 futureYears = lifespan - ageYears - 1;
|
paulo@81
|
390 remainderWeeks = Math.floor(remainder * nWeeks);
|
paulo@81
|
391 restOfYearWeeks = Math.ceil(nWeeks - remainderWeeks) - 1;
|
paulo@81
|
392 } else {
|
paulo@81
|
393 futureYears = 0;
|
paulo@81
|
394 remainderWeeks = nWeeks;
|
paulo@81
|
395 restOfYearWeeks = 0;
|
paulo@81
|
396 }
|
paulo@81
|
397
|
paulo@81
|
398 // Fill in lived years
|
paulo@81
|
399 loop(previousYears, function() {
|
paulo@81
|
400 var tr = yearElem("previous");
|
paulo@81
|
401 $(tr).append(yearWeekElems());
|
paulo@81
|
402 $("#calendar").append(tr);
|
paulo@81
|
403 })
|
paulo@81
|
404
|
paulo@81
|
405 // Fill in the current birth-year (the number of weeks elapsed since the most recent birthday.)
|
paulo@81
|
406 var current_tr = yearElem("current");
|
paulo@81
|
407 loop(remainderWeeks, function(i) {
|
paulo@81
|
408 var e = weekElem("partial");
|
paulo@81
|
409 addWeekMouseEvents(e, i);
|
paulo@81
|
410 $(current_tr).append(e);
|
paulo@81
|
411 })
|
paulo@81
|
412 var e = weekElem("today");
|
paulo@81
|
413 addWeekMouseEvents(e, remainderWeeks);
|
paulo@81
|
414 $(current_tr).append(e);
|
paulo@81
|
415 loop(restOfYearWeeks, function(i) {
|
paulo@81
|
416 var e = weekElem("future");
|
paulo@81
|
417 addWeekMouseEvents(e, remainderWeeks + 1 + i);
|
paulo@81
|
418 $(current_tr).append(e);
|
paulo@81
|
419 })
|
paulo@81
|
420 $("#calendar").append(current_tr);
|
paulo@81
|
421
|
paulo@81
|
422 // Fill in future years
|
paulo@81
|
423 loop(futureYears, function() {
|
paulo@81
|
424 var tr = yearElem("future");
|
paulo@81
|
425 $(tr).append(yearWeekElems());
|
paulo@81
|
426 $("#calendar").append(tr);
|
paulo@81
|
427 })
|
paulo@81
|
428 });
|
paulo@81
|
429 </script>
|
paulo@81
|
430 </head>
|
paulo@81
|
431 <body>
|
paulo@81
|
432 <div id="welcome">
|
paulo@81
|
433 <h1>Life Calendar</h1>
|
paulo@81
|
434 <p>
|
paulo@81
|
435 A minimalist life calendar. Shows the number of weeks you've lived and the number of weeks you
|
paulo@81
|
436 have left.
|
paulo@81
|
437 </p>
|
paulo@81
|
438 <form method="get" action="" id="settingsForm">
|
paulo@81
|
439 <p>
|
paulo@81
|
440 <label for="birthday">Birthday:
|
paulo@81
|
441 <input class="settings" id="birthday" name="birthday" placeholder="1985-01-01"/>
|
paulo@81
|
442 </label>
|
paulo@81
|
443 </p>
|
paulo@81
|
444 <p>
|
paulo@81
|
445 <label for="lifespan">Life expectancy:
|
paulo@81
|
446 <input class="settings" id="lifespan" name="lifespan" placeholder="90"/>
|
paulo@81
|
447 </label>
|
paulo@81
|
448 </p>
|
paulo@81
|
449 <p>
|
paulo@81
|
450 <input type="submit" id="submit" value="Show calendar" />
|
paulo@81
|
451 </p>
|
paulo@81
|
452 </form>
|
paulo@81
|
453 <div id="footer">
|
paulo@81
|
454 <p>
|
paulo@81
|
455 Based on <a href="http://waitbutwhy.com/2014/05/life-weeks.html">Your Life in Weeks</a>
|
paulo@81
|
456 and <a href="http://count.life">count.life</a>.
|
paulo@81
|
457 </p>
|
paulo@81
|
458 </div>
|
paulo@81
|
459 </div>
|
paulo@81
|
460 </body>
|
paulo@81
|
461 </html>
|