index-jobs.html 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684
  1. <html>
  2. <head>
  3. <script>
  4. function setupWindowSync () {
  5. graph2d.on('rangechanged', function (r) {
  6. if (timeline) {
  7. timeline.setWindow(r);
  8. }
  9. if (barChart) {
  10. barChart.setWindow(r);
  11. }
  12. });
  13. }
  14. // Bar Chart Underlay
  15. var barChart;
  16. function createBarChart () {
  17. var container = document.getElementById('visualization3');
  18. var items = [
  19. {id: 8, content: 'job 6', y: 55, x: '2006-03-01', end: '2007-11-20', group: 1},
  20. {id: 1, content: 'job 7', y: 65, x: '2011-05-20', end: '2012-12-20', group: 1},
  21. {id: 2, content: 'job 8', y: 166.4, x: '2013-05-01', end: '2013-11-15', group: 1},
  22. {id: 3, content: 'job 9', y: 135, x: '2014-01-03', end: '2014-03-15', group: 1},
  23. {id: 4, content: 'job 10', y: 145, x: '2014-03-21', end: '2016-04-01', group: 1},
  24. {id: 5, content: 'job 11', y: 195, x: '2016-06-01', end: '2016-08-15', group: 1},
  25. {id: 6, content: 'job 12', y: 145, x: '2016-12-01', end: '2017-05-01', group: 1},
  26. {id: 7, content: 'job 13', y: 155, x: '2017-08-01', end: '2017-11-30', group: 1},
  27. {id: 100, content: 'credit 1', y: 160, x: '2006-01-01', end: '2017-02-28', group: 2},
  28. {id: 101, content: 'credit 2', y: 120, x: '2017-02-28', end: '2018-11-30', group: 2},
  29. {id: 102, content: 'credit 3', y: 90, x: '2018-11-30', end: '2019-11-30', group: 2},
  30. {id: 104, content: 'credit 5', y: 50, x: '2019-11-30', end: '2020-11-30', group: 2},
  31. {id: 105, content: 'credit 6', y: 30, x: '2020-11-30', end: '2021-11-30', group: 2}
  32. ];
  33. var dataset = new vis.DataSet(items);
  34. var options = {
  35. style:'bar',
  36. drawPoints: false,
  37. dataAxis: {
  38. width: '88px',
  39. // visible: false,
  40. icons:true
  41. },
  42. graphHeight: '400px',
  43. width: '90%',
  44. orientation:'none'
  45. };
  46. barChart = new vis.Graph2d(container, items, options);
  47. }
  48. // Timeline
  49. var timeline;
  50. function createTimeline () {
  51. // DOM element where the Timeline will be attached
  52. var container = document.getElementById('visualization2');
  53. // Create a DataSet (allows two way data-binding)
  54. var items = new vis.DataSet([
  55. {id: 8, content: 'job 6', start: '2006-03-01', end: '2007-11-20', group: 1},
  56. {id: 1, content: 'job 7', start: '2011-05-20', end: '2012-12-20', group: 1},
  57. {id: 2, content: 'job 8', start: '2013-05-01', end: '2013-11-15', group: 1},
  58. {id: 3, content: 'job 9', start: '2014-01-03', end: '2014-03-15', group: 1},
  59. {id: 4, content: 'job 10', start: '2014-03-21', end: '2016-04-01', group: 1},
  60. {id: 5, content: 'job 11', start: '2016-06-01', end: '2016-08-15', group: 1},
  61. {id: 6, content: 'job 12', start: '2016-12-01', end: '2017-05-01', group: 1},
  62. {id: 7, content: 'job 13', start: '2017-08-01', end: '2017-11-30', group: 1},
  63. {id: 100, content: 'friend 1', start: '2005-01-01', end: '2017-09-01', group: 2},
  64. {id: 101, content: 'friend 2', start: '2005-06-01', end: '2015-03-01', group: 2},
  65. {id: 102, content: 'friend 3', start: '2005-01-01', end: '2017-09-01', group: 2},
  66. {id: 201, content: 'school 3', start: '2008-01-15', end: '2011-05-01', group: 3},
  67. {id: 1001, content: 'project 1', start: '2007-06-15', end: '2009-01-01', group: 4, editable: true},
  68. {id: 2001, content: 'loc 1', start: '1985-01-24', end: '2011-05-01', group: 5},
  69. {id: 2002, content: 'loc 2', start: '2011-05-15', end: '2012-12-01', group: 5},
  70. {id: 6000, content: 'fulton', start: '2014-01-03', end: '2016-03-01', group: 6},
  71. {id: 6001, content: '48th ave', start: '2016-03-01', end: '2018-06-01', group: 6},
  72. {id: 6010, content: 'civic', start: '2018-06-01', end: '2021-08-01', group: 6},
  73. {id: 6020, content: 'relative', start: '2021-08-01', end: '2021-12-01', group: 6}
  74. ]);
  75. var groups = new vis.DataSet([
  76. {id: 1, content: 'Jobs', order: 5},
  77. {id: 2, content: 'Friends', order: 6},
  78. {id: 3, content: 'Schools', order: 4},
  79. {id: 4, content: 'Projects', order: 3},
  80. {id: 5, content: 'Locations', order: 1},
  81. {id: 6, content: 'Housing', order: 2}
  82. ]);
  83. // Configuration for the Timeline
  84. var options = {
  85. width: '90%',
  86. groupOrder: 'order'
  87. };
  88. // Create a Timeline
  89. timeline = new vis.Timeline(container, items, groups, options);
  90. }
  91. // Tweet storm summaries
  92. function importTweetStormSummaries (feedUrl) {
  93. fetch( feedUrl )
  94. .then(function (res) {
  95. return res.json();
  96. })
  97. .then(function (storms) {
  98. var items = storms.map(function (s) {
  99. var dt = new Date(s.datetime);
  100. var dayStart = startOfDay(dt);
  101. return {
  102. id: "tweet-storm_" + dt,
  103. x: dayStart,
  104. y: (dt - dayStart) / (1000 * 60)
  105. };
  106. });
  107. return items;
  108. })
  109. .then(function (items) {
  110. dataset.update(items);
  111. });
  112. }
  113. // Bed
  114. function importRssFeed (feedUrl) {
  115. var rssRequest = new XMLHttpRequest();
  116. rssRequest.onreadystatechange = function (e) {
  117. var r = rssRequest;
  118. if (r.readyState == XMLHttpRequest.DONE) {
  119. if (r.status >= 200 && r.status < 300) {
  120. console.log("Got RSS (" + r.status + "): ");
  121. var items = importRSSDoc(r.responseXML);
  122. populateItems( items );
  123. createPlot( items );
  124. importTvFeed( "https://harlanji.com/tv.xml" );
  125. }
  126. }
  127. };
  128. //rssRequest.open("GET", "bed-harlanji.xml");
  129. rssRequest.open("GET", feedUrl);
  130. rssRequest.send();
  131. }
  132. function importRSSDoc( rssDoc ) {
  133. console.log("importRSSDoc:");
  134. console.log( rssDoc );
  135. var feedLink = rssDoc.querySelector("link").textContent.trim();
  136. var items = Array.from( rssDoc.querySelectorAll("item") );
  137. items = items.map(function (i) {
  138. var item = {
  139. title: i.querySelector("title").textContent.trim(),
  140. date: new Date( i.querySelector("pubDate").textContent.trim() ),
  141. description: i.querySelector("description").textContent.trim(),
  142. link: i.querySelector("link").textContent.trim(),
  143. feedLink: feedLink
  144. };
  145. var occurenceParts = splitString( item.description, ":", 2);
  146. item.occurence = parseInt( occurenceParts[0] ) || 1;
  147. item.occurenceNote = occurenceParts[1].trim();
  148. if (item.occurenceNote == "") {
  149. item.occurenceNote = "Morning routine";
  150. }
  151. return item;
  152. });
  153. items = items.sort(function (a, b) {
  154. return b.date - a.date;
  155. });
  156. console.log("items:");
  157. console.log(items);
  158. return items;
  159. }
  160. function populateItems( items ) {
  161. console.log("populateItems");
  162. console.log( items );
  163. var rows = items.map(function (item) {
  164. var row = document.createElement( "tr" );
  165. var date = document.createElement( "td" );
  166. var occurence = document.createElement( "td" );
  167. var note = document.createElement( "td" );
  168. date.textContent = dateString( item.date );
  169. occurence.textContent = item.occurence;
  170. note.textContent = item.occurenceNote;
  171. row.appendChild( date );
  172. row.appendChild( occurence );
  173. row.appendChild( note );
  174. return row;
  175. });
  176. var tableBody = document.querySelector("#bed-makings tbody");
  177. console.log("populate: ");
  178. console.log(tableBody);
  179. console.log(rows);
  180. while (tableBody.lastChild) {
  181. tableBody.removeChild(tableBody.lastChild);
  182. }
  183. rows.forEach(function (row) {
  184. tableBody.appendChild(row);
  185. });
  186. }
  187. var dataset;
  188. function initDataset () {
  189. dataset = new vis.DataSet();
  190. }
  191. var groups = [];
  192. function groupFor (str) {
  193. var idx = groups.indexOf(str);
  194. if (idx == -1) {
  195. idx = groups.length;
  196. groups.push(str);
  197. }
  198. return idx;
  199. }
  200. var graph2d;
  201. function createPlot ( feedItems ) {
  202. console.log("createPlot");
  203. var container = document.getElementById('visualization');
  204. while( container.lastChild ) {
  205. container.removeChild( container.lastChild );
  206. }
  207. if (feedItems.length == 0) {
  208. return;
  209. }
  210. var items = feedItems.map(function (item) {
  211. var startDate = startOfDay(item.date);
  212. return {
  213. x: startDate,
  214. y: (item.date - startDate) / (1000 * 60),
  215. group: groupFor(item.feedLink),
  216. id: item.feedLink + item.link
  217. };
  218. });
  219. // feed items are in descending order
  220. var firstDate = startOfDay( feedItems[ feedItems.length - 1 ].date );
  221. var lastDate = startOfDay( feedItems[ 0 ].date );
  222. var currentFirstDate = dataset.get("firstDateSunrise");
  223. if (!currentFirstDate || currentFirstDate.x > firstDate) {
  224. var firstTimes = SunCalc.getTimes(firstDate, 44.986656, -93.258133)
  225. items.push({id: "firstDateSunrise",
  226. x: firstDate,
  227. y: (firstTimes.sunrise - firstDate) / (1000 * 60),
  228. group: groupFor("sunrise")});
  229. }
  230. var currentLastDate = dataset.get("lastDateSunrise");
  231. if (!currentLastDate || currentLastDate.x < lastDate) {
  232. var lastTimes = SunCalc.getTimes(lastDate, 44.986656, -93.258133)
  233. items.push({id: "lastDateSunrise",
  234. x: lastDate,
  235. y: (lastTimes.sunrise - lastDate) / (1000 * 60),
  236. group: groupFor("sunrise")});
  237. }
  238. dataset.update(items);
  239. var options = {
  240. sort: false,
  241. sampling:false,
  242. style:'points',
  243. dataAxis: {
  244. width: '88px',
  245. //visible: false,
  246. left: {
  247. range: {
  248. min: 0, max: (60 * 24)
  249. }
  250. }
  251. },
  252. drawPoints: {
  253. enabled: true,
  254. size: 6,
  255. style: 'circle' // square, circle
  256. },
  257. defaultGroup: 'Scatterplot',
  258. graphHeight: '400px',
  259. width: '90%'
  260. };
  261. graph2d = new vis.Graph2d(container, dataset, options);
  262. // sunrise is the earliest and latest feed items
  263. var windowStartDate = new Date(dataset.get("firstDateSunrise").x);
  264. var windowEndDate = new Date(dataset.get("lastDateSunrise").x);
  265. // day before first and after the latest day
  266. windowStartDate.setDate( windowStartDate.getDate() - 1 );
  267. windowEndDate.setDate( windowEndDate.getDate() + 1 );
  268. graph2d.setWindow(windowStartDate, windowEndDate, {animation: false});
  269. }
  270. // TV
  271. function importTvFeed (feedUrl) {
  272. var rssRequest = new XMLHttpRequest();
  273. rssRequest.onreadystatechange = function (e) {
  274. var r = rssRequest;
  275. if (r.readyState == XMLHttpRequest.DONE) {
  276. if (r.status >= 200 && r.status < 300) {
  277. console.log("Got TV RSS (" + r.status + "): ");
  278. var items = importTvDoc(r.responseXML);
  279. addTvToPlot( items );
  280. }
  281. }
  282. };
  283. //rssRequest.open("GET", "bed-harlanji.xml");
  284. rssRequest.open("GET", feedUrl);
  285. rssRequest.send();
  286. }
  287. function importTvDoc( rssDoc ) {
  288. console.log("importTvDoc:");
  289. console.log( rssDoc );
  290. var items = Array.from( rssDoc.querySelectorAll("item") );
  291. items = items.map(function (i) {
  292. var title = i.querySelector("title").textContent.trim();
  293. var state = title.indexOf("on") > -1;
  294. var item = {
  295. title: title,
  296. state: state,
  297. date: new Date( i.querySelector("pubDate").textContent.trim() ),
  298. description: i.querySelector("description").textContent.trim()
  299. };
  300. return item;
  301. });
  302. items = items.sort(function (a, b) {
  303. return b.date - a.date;
  304. });
  305. console.log("tv items:");
  306. console.log(items);
  307. return items;
  308. }
  309. // Steps CSV
  310. function importStepsFeed (feedUrl) {
  311. var rssRequest = new XMLHttpRequest();
  312. rssRequest.onreadystatechange = function (e) {
  313. var r = rssRequest;
  314. if (r.readyState == XMLHttpRequest.DONE) {
  315. if (r.status >= 200 && r.status < 300) {
  316. var parseHeader = true;
  317. console.log("Got Steps CSV (" + r.status + ", parseHeader=" + parseHeader + "): ");
  318. var items = r.responseText.split("\n")
  319. .map(function (line) { return line.trim(); })
  320. .filter(function (line, i) { return !(parseHeader && i == 0) && line != ""; })
  321. .map(function (line, i, arr) {
  322. var parts = line.split(",");
  323. var date = new Date(parts[0]),
  324. steps = parseInt(parts[1]);
  325. return {
  326. date: date,
  327. steps: steps
  328. }
  329. });
  330. addStepsToPlot( items );
  331. }
  332. }
  333. };
  334. //rssRequest.open("GET", "bed-harlanji.xml");
  335. rssRequest.open("GET", feedUrl);
  336. rssRequest.send();
  337. }
  338. function dateString (date) {
  339. // return "d: " + date;
  340. var str = (date.getYear() + 1900) + "-"
  341. + new String(date.getMonth() + 1).padStart(2, "0") + "-"
  342. + new String(date.getDate()).padStart(2, "0") + " "
  343. + new String(date.getHours()).padStart(2, "0") + ":"
  344. + new String(date.getMinutes()).padStart(2, "0") + ":"
  345. + new String(date.getSeconds()).padStart(2, "0") + " ";
  346. var tzOffset = date.getTimezoneOffset() / 60;
  347. if (tzOffset >= 0) {
  348. str += "+";
  349. }
  350. str += new String(tzOffset * 100).padStart(4, "0");
  351. return str;
  352. }
  353. function addStepsToPlot ( feedItems ) {
  354. console.log("addStepsToPlot");
  355. var items = feedItems.map(function (item) {
  356. var startDate = startOfDay(item.date);
  357. var group = groupFor("steps");
  358. return {
  359. x: startDate,
  360. y: item.steps / 8, // fixme: normalize over 1440 seconds
  361. group: group,
  362. id: "steps-" + item.date
  363. };
  364. });
  365. dataset.update( items );
  366. }
  367. function addTvToPlot ( feedItems ) {
  368. console.log("addTvToPlot");
  369. var items = feedItems.map(function (item) {
  370. var startDate = startOfDay(item.date);
  371. var group = item.state ? groupFor("tv-on") : groupFor("tv-off");
  372. return {
  373. x: startDate,
  374. y: (item.date - startDate) / (1000 * 60),
  375. group: group
  376. };
  377. });
  378. dataset.add( items );
  379. }
  380. function splitString(string, delimiter, n) {
  381. var parts = string.split(delimiter);
  382. return parts.slice(0, n - 1).concat([parts.slice(n - 1).join(delimiter)]);
  383. }
  384. </script>
  385. <!--
  386. <script defer data-domain="cityapper.com" src="https://pa.cityapper.com/js/plausible.js"></script>
  387. -->
  388. <script src="vis-timeline-graph2d.min.js"></script>
  389. <link href="vis-timeline-graph2d.min.css" rel="stylesheet" type="text/css" />
  390. <script src="suncalc.js"></script>
  391. </head>
  392. <body>
  393. <script>
  394. function importAuthorSubmitted( form ) {
  395. console.log("submitted:");
  396. console.log(form);
  397. }
  398. var authors = {
  399. "HarlanJI": "https://harlanji.com/bed.xml",
  400. "Marty": "https://harlanji.com/bed-marty.xml"
  401. }
  402. function populateAuthors () {
  403. var authorsElem = document.querySelector("#author");
  404. while(authorsElem.lastChild) {
  405. authorsElem.removeChild(authorsElem.lastChild);
  406. }
  407. Object.entries(authors).forEach(function (e, i) {
  408. var option = document.createElement("option");
  409. option.value = e[1];
  410. option.textContent = e[0];
  411. if ( i == 0 ) {
  412. option.selected = "selected";
  413. }
  414. authorsElem.appendChild(option);
  415. console.log(e);
  416. });
  417. authorsElem.dispatchEvent(new Event('change'));
  418. }
  419. function startOfDay (date) {
  420. var startDate = new Date(date);
  421. startDate.setHours(0);
  422. startDate.setMinutes(0);
  423. startDate.setSeconds(0);
  424. startDate.setMilliseconds(0);
  425. return startDate;
  426. }
  427. </script>
  428. <h1>Author Made Bed</h1>
  429. <form id="import-author" action="#" onsubmit="console.log('s')">
  430. <p>
  431. Author:
  432. <select id="author" onchange="importRssFeed(this.options[this.selectedIndex].value)">
  433. </select>
  434. </p>
  435. </form>
  436. <div style="position: relative; height: 465px;">
  437. <div style="position: absolute; top: 0; left: 0; width: 100%;">
  438. <div id="visualization3" style=""></div>
  439. </div>
  440. <div style="position: absolute; top: 0; left: 0; width: 100%">
  441. <div id="visualization"></div>
  442. </div>
  443. </div>
  444. <div id="visualization2"></div>
  445. <table id="bed-makings">
  446. <thead>
  447. <tr>
  448. <th>Date Time</th>
  449. <th>#</th>
  450. <th>Note</th>
  451. </tr>
  452. </thead>
  453. <tbody>
  454. </tbody>
  455. </table>
  456. <p>This data will sync both directions with dataset:</p>
  457. <textarea id="data" style="width:100%; height: 400px;">
  458. [
  459. {id: 1,
  460. x: '2021-01-01',
  461. end: '2021-12-31',
  462. group: 2,
  463. y: 750,
  464. content: 'credit 1'
  465. }
  466. ]
  467. </textarea>
  468. <p style="color: green">
  469. Syntax is valid.
  470. </p>
  471. <p>
  472. <input name="autosync" type="checkbox"> Auto-save upon blur.
  473. </p>
  474. <p>
  475. <span><input name="format" type="radio" value="json" checked> JSON</span>
  476. <span><input name="format" type="radio" value="edn"> EDN</span>
  477. </p>
  478. <script>
  479. //importRssFeed( document.querySelector("#author").value );
  480. populateAuthors();
  481. initDataset();
  482. </script>
  483. </body>
  484. </html>