ソースを参照

added bedrss demo. focus: visjs, pain driven development, marketing/build in public, live streaming.

Harlan J. Iverson 1 年間 前
コミット
14ded1220d

+ 81 - 0
bedrss/README.md

@@ -0,0 +1,81 @@
+# Author Made Bed / BedRSS
+
+A demo app with VisJS to follow an RSS feed and plot the timestamps.
+
+Later days can add the TV off/on feeds to the same plot.
+
+Search Twitter for #bedrss; this project was about creating marketing /
+"building in public" (before I knew the #buildinpublic hash tag), 
+and demonstrating lazy development practices that anyone could follow along with
+by watching a series of streams.
+
+## Status
+
+This app was more about minimal development practices and the series 
+than technical learning.
+
+I may have called the approach 'Pain Driven Development' and done things in the 
+worst possible way, until they were too annoying then I did something about them--
+I'm sure I'm not the first to have invented the term or done things this way,
+but I took it to the extreme in this series of projects.
+
+Day 1 and 2 are missing because I did this on live streams and hadn't introduced
+the concept to the audience yet.
+
+It was among the first apps I laid out in this fashion, if not the beginning.
+
+The latest changes are in the root directory and I copied them to day-X
+before I started working on a subsequent day.
+
+## Usage
+
+Requires visjs and some RSS feeds.
+
+Please see the index.html files for details.
+
+Pardon the lack of details sofar here.
+
+## Change Log
+
+I believe I have more detailed notes and some screenshots that I Tweeted 
+and Twitch stream backs, can update this file when I've located them. 
+Please excuse that.
+
+We can make the changelog better by diffing the files.
+
+### 2023-05-21
+
+Added README for harlanji/dev-practice release.
+
+### Day 6 (2021-11-16 / 2021-12-10)
+
+Added steps and bank deposits to feed, which are CSV data instead of RSS
+
+### Day 5 (2021-11-16)
+
+From changes.txt:
+
+Added link and feedLink to each item and use them together as the unique ID
+of an item in the dataset. We use the update method on datasets instead
+of add to take into account the IDs, and we also check if the sunrise times
+already exist before adding.
+
+### Day 4 (2021-11-15)
+
+Added TV feed
+
+
+### Day 3 (2021-11-11)
+
+Perhaps added sunrise and a second author?
+
+
+## Dev Practice
+
+Focus:
+
+* Making an app from scratch on live stream in the laziest possible way
+* Creating a VisJS plot
+* Fetching RSS feed with plain JS
+* Fetching CSV data and addint it to the same plot
+* Basic JS controls for loading feeds onto plot

+ 295 - 0
bedrss/day-3/index.html

@@ -0,0 +1,295 @@
+<html>
+  <head>
+
+<script>
+
+function importRssFeed (feedUrl) {
+
+  var rssRequest = new XMLHttpRequest();
+
+
+  rssRequest.onreadystatechange = function (e) {
+    var r = rssRequest;
+    if (r.readyState == XMLHttpRequest.DONE) {
+      if (r.status >= 200 && r.status < 300) {
+        console.log("Got RSS (" + r.status + "): ");
+
+        var items = importRSSDoc(r.responseXML);
+        populateItems( items );
+        createPlot( items );
+      }
+    }
+  };
+
+  //rssRequest.open("GET", "bed-harlanji.xml");
+
+  rssRequest.open("GET", feedUrl);
+  rssRequest.send();
+
+}
+
+function importRSSDoc( rssDoc ) {
+  console.log("importRSSDoc:");
+  console.log( rssDoc );
+
+  //var author = rssDoc.querySelector("generator").textContent.trim();
+
+  var items = Array.from( rssDoc.querySelectorAll("item") );
+  items = items.map(function (i) {
+    var item = {
+      title: i.querySelector("title").textContent.trim(),
+      date: new Date( i.querySelector("pubDate").textContent.trim() ),
+      description: i.querySelector("description").textContent.trim()
+    };
+
+    var occurenceParts = splitString( item.description, ":", 2);
+
+
+    item.occurence = parseInt( occurenceParts[0] ) || 1;
+    item.occurenceNote = occurenceParts[1].trim();
+
+    if (item.occurenceNote == "") {
+      item.occurenceNote = "Morning routine";
+    }
+
+    return item;
+  });
+
+
+  items = items.sort(function (a, b) {
+    return b.date - a.date;
+  });
+
+  console.log("items:");
+  console.log(items);
+
+  return items;
+}
+
+
+function populateItems( items ) {
+  console.log("populateItems");
+  console.log( items );
+
+  var rows = items.map(function (item) {
+    var row = document.createElement( "tr" );
+    var date = document.createElement( "td" );
+    var occurence = document.createElement( "td" );
+    var note = document.createElement( "td" );
+
+    date.textContent = dateString( item.date );
+    occurence.textContent = item.occurence;
+    note.textContent = item.occurenceNote;
+
+    row.appendChild( date );
+    row.appendChild( occurence );
+    row.appendChild( note );
+
+    return row;
+  });
+
+  var tableBody = document.querySelector("#bed-makings tbody");
+  console.log("populate: ");
+  console.log(tableBody);
+  console.log(rows);
+
+  while (tableBody.lastChild) {
+    tableBody.removeChild(tableBody.lastChild);
+  }
+
+  rows.forEach(function (row) {
+    tableBody.appendChild(row);
+  });
+}
+
+function dateString (date) {
+//  return "d: " + date;
+
+  var str = (date.getYear() + 1900) + "-"
+              + new String(date.getMonth() + 1).padStart(2, "0") + "-"
+              + new String(date.getDate()).padStart(2, "0") + " "
+              + new String(date.getHours()).padStart(2, "0") + ":"
+              + new String(date.getMinutes()).padStart(2, "0") + ":"
+              + new String(date.getSeconds()).padStart(2, "0") + " ";
+
+  var tzOffset = date.getTimezoneOffset() / 60;
+
+  if (tzOffset >= 0) {
+    str += "+";
+  }
+
+  str += new String(tzOffset * 100).padStart(4, "0");
+           
+
+  return str;
+}
+
+
+function splitString(string, delimiter, n) {
+    var parts = string.split(delimiter);
+    return parts.slice(0, n - 1).concat([parts.slice(n - 1).join(delimiter)]);
+}
+
+</script>
+
+<script defer data-domain="cityapper.com" src="https://pa.cityapper.com/js/plausible.js"></script>
+
+  <script src="vis-timeline-graph2d.min.js"></script>
+  <link href="vis-timeline-graph2d.min.css" rel="stylesheet" type="text/css" />
+
+  <script src="suncalc.js"></script>
+
+  </head>
+  <body>
+
+<script>
+
+function importAuthorSubmitted( form ) {
+  console.log("submitted:");
+
+  console.log(form);
+}
+
+var authors = {
+  "HarlanJI": "https://harlanji.com/bed.xml",
+  "Marty": "https://harlanji.com/bed-marty.xml"
+}
+
+function populateAuthors () {
+  var authorsElem = document.querySelector("#author");
+
+  while(authorsElem.lastChild) {
+    authorsElem.removeChild(authorsElem.lastChild);
+  }
+
+  Object.entries(authors).forEach(function (e, i) {
+    var option = document.createElement("option");
+    option.value = e[1];
+    option.textContent = e[0];
+
+    if ( i == 0 ) {
+      option.selected = "selected";
+    }
+
+    authorsElem.appendChild(option);
+
+    console.log(e);
+  });
+
+  authorsElem.dispatchEvent(new Event('change'));
+}
+
+function startOfDay (date) {
+  var startDate = new Date(date);
+
+  startDate.setHours(0);
+  startDate.setMinutes(0);
+  startDate.setSeconds(0);
+  startDate.setMilliseconds(0);
+
+  return startDate;
+}
+
+
+function createPlot ( feedItems ) {
+
+  console.log("createPlot");
+
+
+  var container = document.getElementById('visualization');
+
+  while( container.lastChild ) {
+    container.removeChild( container.lastChild );
+  }
+
+
+  var items = feedItems.map(function (item) {
+     var startDate = startOfDay(item.date);
+
+     return {
+       x: startDate,
+       y: (item.date - startDate) / (1000 * 60),
+       group: 0
+     };
+  });
+
+  // feed items are in descending order
+  var firstDate = startOfDay( feedItems[ feedItems.length - 1 ].date );
+  var lastDate = startOfDay( feedItems[ 0 ].date );
+
+  var firstTimes = SunCalc.getTimes(firstDate, 44.986656, -93.258133)
+  var lastTimes = SunCalc.getTimes(lastDate, 44.986656, -93.258133)
+  
+  items.push({x: firstDate,
+             y: (firstTimes.sunrise - firstDate) / (1000 * 60),
+             group: 1});
+
+  items.push({x: lastDate,
+             y: (lastTimes.sunrise - lastDate) / (1000 * 60),
+             group: 1});
+
+  var dataset = new vis.DataSet(items);
+  var options = {
+      sort: false,
+      sampling:false,
+      style:'points',
+      dataAxis: {
+          left: {
+              range: {
+                  min: 0, max: (60 * 24)
+              }
+          }
+      },
+      drawPoints: {
+          enabled: true,
+          size: 6,
+          style: 'circle' // square, circle
+      },
+      defaultGroup: 'Scatterplot',
+      height: '400px',
+      width: '90%'
+  };
+  var graph2d = new vis.Graph2d(container, dataset, options);
+
+
+}
+
+
+</script>
+
+<h1>Author Made Bed</h1>
+
+<form id="import-author" action="#" onsubmit="console.log('s')">
+<p>
+Author:
+<select id="author" onchange="importRssFeed(this.options[this.selectedIndex].value)">
+</select>
+
+</p>
+</form>
+
+<div id="visualization"></div>
+
+<table id="bed-makings">
+  <thead>
+    <tr>
+      <th>Date Time</th>
+      <th>#</th>
+      <th>Note</th>
+    </tr>
+  </thead>
+  <tbody>
+  </tbody>
+</table>
+
+
+<script>
+
+//importRssFeed( document.querySelector("#author").value );
+
+populateAuthors();
+
+</script>
+
+  </body>
+</html>

+ 397 - 0
bedrss/day-4/index.html

@@ -0,0 +1,397 @@
+<html>
+  <head>
+
+<script>
+
+
+// Bed
+
+function importRssFeed (feedUrl) {
+
+  var rssRequest = new XMLHttpRequest();
+
+
+  rssRequest.onreadystatechange = function (e) {
+    var r = rssRequest;
+    if (r.readyState == XMLHttpRequest.DONE) {
+      if (r.status >= 200 && r.status < 300) {
+        console.log("Got RSS (" + r.status + "): ");
+
+
+        var items = importRSSDoc(r.responseXML);
+        populateItems( items );
+        createPlot( items );
+
+        importTvFeed( "https://harlanji.com/tv.xml" );
+
+      }
+    }
+  };
+
+  //rssRequest.open("GET", "bed-harlanji.xml");
+
+  rssRequest.open("GET", feedUrl);
+  rssRequest.send();
+
+}
+
+function importRSSDoc( rssDoc ) {
+  console.log("importRSSDoc:");
+  console.log( rssDoc );
+
+  //var author = rssDoc.querySelector("generator").textContent.trim();
+
+  var items = Array.from( rssDoc.querySelectorAll("item") );
+  items = items.map(function (i) {
+    var item = {
+      title: i.querySelector("title").textContent.trim(),
+      date: new Date( i.querySelector("pubDate").textContent.trim() ),
+      description: i.querySelector("description").textContent.trim()
+    };
+
+    var occurenceParts = splitString( item.description, ":", 2);
+
+
+    item.occurence = parseInt( occurenceParts[0] ) || 1;
+    item.occurenceNote = occurenceParts[1].trim();
+
+    if (item.occurenceNote == "") {
+      item.occurenceNote = "Morning routine";
+    }
+
+    return item;
+  });
+
+
+  items = items.sort(function (a, b) {
+    return b.date - a.date;
+  });
+
+  console.log("items:");
+  console.log(items);
+
+  return items;
+}
+
+
+function populateItems( items ) {
+  console.log("populateItems");
+  console.log( items );
+
+  var rows = items.map(function (item) {
+    var row = document.createElement( "tr" );
+    var date = document.createElement( "td" );
+    var occurence = document.createElement( "td" );
+    var note = document.createElement( "td" );
+
+    date.textContent = dateString( item.date );
+    occurence.textContent = item.occurence;
+    note.textContent = item.occurenceNote;
+
+    row.appendChild( date );
+    row.appendChild( occurence );
+    row.appendChild( note );
+
+    return row;
+  });
+
+  var tableBody = document.querySelector("#bed-makings tbody");
+  console.log("populate: ");
+  console.log(tableBody);
+  console.log(rows);
+
+  while (tableBody.lastChild) {
+    tableBody.removeChild(tableBody.lastChild);
+  }
+
+  rows.forEach(function (row) {
+    tableBody.appendChild(row);
+  });
+}
+
+var dataset;
+
+function createPlot ( feedItems ) {
+
+  console.log("createPlot");
+
+
+  var container = document.getElementById('visualization');
+
+  while( container.lastChild ) {
+    container.removeChild( container.lastChild );
+  }
+
+
+  var items = feedItems.map(function (item) {
+     var startDate = startOfDay(item.date);
+
+     return {
+       x: startDate,
+       y: (item.date - startDate) / (1000 * 60),
+       group: 0
+     };
+  });
+
+  // feed items are in descending order
+  var firstDate = startOfDay( feedItems[ feedItems.length - 1 ].date );
+  var lastDate = startOfDay( feedItems[ 0 ].date );
+
+  var firstTimes = SunCalc.getTimes(firstDate, 44.986656, -93.258133)
+  var lastTimes = SunCalc.getTimes(lastDate, 44.986656, -93.258133)
+  
+  items.push({x: firstDate,
+             y: (firstTimes.sunrise - firstDate) / (1000 * 60),
+             group: 1});
+
+  items.push({x: lastDate,
+             y: (lastTimes.sunrise - lastDate) / (1000 * 60),
+             group: 1});
+
+  dataset = new vis.DataSet(items);
+
+  var options = {
+      sort: false,
+      sampling:false,
+      style:'points',
+      dataAxis: {
+          left: {
+              range: {
+                  min: 0, max: (60 * 24)
+              }
+          }
+      },
+      drawPoints: {
+          enabled: true,
+          size: 6,
+          style: 'circle' // square, circle
+      },
+      defaultGroup: 'Scatterplot',
+      height: '400px',
+      width: '90%'
+  };
+
+
+  var graph2d = new vis.Graph2d(container, dataset, options);
+
+
+  graph2d.setWindow(firstDate, startOfDay(new Date()), {animation: false});
+
+}
+
+
+
+// TV
+
+
+
+function importTvFeed (feedUrl) {
+
+  var rssRequest = new XMLHttpRequest();
+
+
+  rssRequest.onreadystatechange = function (e) {
+    var r = rssRequest;
+    if (r.readyState == XMLHttpRequest.DONE) {
+      if (r.status >= 200 && r.status < 300) {
+        console.log("Got TV RSS (" + r.status + "): ");
+
+        var items = importTvDoc(r.responseXML);
+        addTvToPlot( items );
+      }
+    }
+  };
+
+  //rssRequest.open("GET", "bed-harlanji.xml");
+
+  rssRequest.open("GET", feedUrl);
+  rssRequest.send();
+
+}
+
+function importTvDoc( rssDoc ) {
+  console.log("importTvDoc:");
+  console.log( rssDoc );
+
+
+  var items = Array.from( rssDoc.querySelectorAll("item") );
+  items = items.map(function (i) {
+
+    var title = i.querySelector("title").textContent.trim();
+    var state = title.indexOf("on") > -1;
+
+    var item = {
+      title: title,
+      state: state,
+      date: new Date( i.querySelector("pubDate").textContent.trim() ),
+      description: i.querySelector("description").textContent.trim()
+    };
+
+    return item;
+  });
+
+  items = items.sort(function (a, b) {
+    return b.date - a.date;
+  });
+
+  console.log("tv items:");
+  console.log(items);
+
+  return items;
+}
+
+
+
+function dateString (date) {
+//  return "d: " + date;
+
+  var str = (date.getYear() + 1900) + "-"
+              + new String(date.getMonth() + 1).padStart(2, "0") + "-"
+              + new String(date.getDate()).padStart(2, "0") + " "
+              + new String(date.getHours()).padStart(2, "0") + ":"
+              + new String(date.getMinutes()).padStart(2, "0") + ":"
+              + new String(date.getSeconds()).padStart(2, "0") + " ";
+
+  var tzOffset = date.getTimezoneOffset() / 60;
+
+  if (tzOffset >= 0) {
+    str += "+";
+  }
+
+  str += new String(tzOffset * 100).padStart(4, "0");
+           
+
+  return str;
+}
+
+
+
+
+function addTvToPlot ( feedItems ) {
+
+  console.log("addTvToPlot");
+
+
+  var items = feedItems.map(function (item) {
+     var startDate = startOfDay(item.date);
+     var group = item.state ? 3 : 4;
+
+     return {
+       x: startDate,
+       y: (item.date - startDate) / (1000 * 60),
+       group: group
+     };
+  });
+
+  dataset.add( items );
+}
+
+
+
+function splitString(string, delimiter, n) {
+    var parts = string.split(delimiter);
+    return parts.slice(0, n - 1).concat([parts.slice(n - 1).join(delimiter)]);
+}
+
+</script>
+
+<script defer data-domain="cityapper.com" src="https://pa.cityapper.com/js/plausible.js"></script>
+
+  <script src="vis-timeline-graph2d.min.js"></script>
+  <link href="vis-timeline-graph2d.min.css" rel="stylesheet" type="text/css" />
+
+  <script src="suncalc.js"></script>
+
+  </head>
+  <body>
+
+<script>
+
+function importAuthorSubmitted( form ) {
+  console.log("submitted:");
+
+  console.log(form);
+}
+
+var authors = {
+  "HarlanJI": "https://harlanji.com/bed.xml",
+  "Marty": "https://harlanji.com/bed-marty.xml"
+}
+
+function populateAuthors () {
+  var authorsElem = document.querySelector("#author");
+
+  while(authorsElem.lastChild) {
+    authorsElem.removeChild(authorsElem.lastChild);
+  }
+
+  Object.entries(authors).forEach(function (e, i) {
+    var option = document.createElement("option");
+    option.value = e[1];
+    option.textContent = e[0];
+
+    if ( i == 0 ) {
+      option.selected = "selected";
+    }
+
+    authorsElem.appendChild(option);
+
+    console.log(e);
+  });
+
+  authorsElem.dispatchEvent(new Event('change'));
+}
+
+function startOfDay (date) {
+  var startDate = new Date(date);
+
+  startDate.setHours(0);
+  startDate.setMinutes(0);
+  startDate.setSeconds(0);
+  startDate.setMilliseconds(0);
+
+  return startDate;
+}
+
+
+
+</script>
+
+<h1>Author Made Bed</h1>
+
+
+
+<form id="import-author" action="#" onsubmit="console.log('s')">
+<p>
+Author:
+<select id="author" onchange="importRssFeed(this.options[this.selectedIndex].value)">
+</select>
+
+</p>
+</form>
+
+<div id="visualization"></div>
+
+<table id="bed-makings">
+  <thead>
+    <tr>
+      <th>Date Time</th>
+      <th>#</th>
+      <th>Note</th>
+    </tr>
+  </thead>
+  <tbody>
+  </tbody>
+</table>
+
+
+<script>
+
+//importRssFeed( document.querySelector("#author").value );
+
+populateAuthors();
+
+</script>
+
+  </body>
+</html>

+ 6 - 0
bedrss/day-5-done/changes.txt

@@ -0,0 +1,6 @@
+Added link and feedLink to each item and use them together as the unique ID
+of an item in the dataset. We use the update method on datasets instead
+of add to take into account the IDs, and we also check if the sunrise times
+already exist before adding.
+
+

+ 430 - 0
bedrss/day-5-done/index.html

@@ -0,0 +1,430 @@
+<html>
+  <head>
+
+<script>
+
+
+var dataset;
+
+function initDataset() {
+ dataset = new vis.DataSet({queue: true});
+
+}
+
+// Bed
+
+function importRssFeed (feedUrl) {
+
+  var rssRequest = new XMLHttpRequest();
+
+
+  rssRequest.onreadystatechange = function (e) {
+    var r = rssRequest;
+    if (r.readyState == XMLHttpRequest.DONE) {
+      if (r.status >= 200 && r.status < 300) {
+        console.log("Got RSS (" + r.status + "): ");
+
+
+        var items = importRSSDoc(r.responseXML);
+        populateItems( items );
+        createPlot( items );
+
+        importTvFeed( "https://harlanji.com/tv.xml" );
+
+      }
+    }
+  };
+
+  //rssRequest.open("GET", "bed-harlanji.xml");
+
+  rssRequest.open("GET", feedUrl);
+  rssRequest.send();
+
+}
+
+function importRSSDoc( rssDoc ) {
+  console.log("importRSSDoc:");
+  console.log( rssDoc );
+
+  var feedLink = rssDoc.querySelector("link").textContent.trim();
+
+  console.log("bed feed link = " + feedLink);
+
+
+  //var author = rssDoc.querySelector("generator").textContent.trim();
+
+  var items = Array.from( rssDoc.querySelectorAll("item") );
+  items = items.map(function (i) {
+    var item = {
+      title: i.querySelector("title").textContent.trim(),
+      date: new Date( i.querySelector("pubDate").textContent.trim() ),
+      description: i.querySelector("description").textContent.trim(),
+
+      link: i.querySelector("link").textContent.trim(),
+      feedLink: feedLink
+    };
+
+    var occurenceParts = splitString( item.description, ":", 2);
+
+
+    item.occurence = parseInt( occurenceParts[0] ) || 1;
+    item.occurenceNote = occurenceParts[1].trim();
+
+    if (item.occurenceNote == "") {
+      item.occurenceNote = "Morning routine";
+    }
+
+    return item;
+  });
+
+
+  items = items.sort(function (a, b) {
+    return b.date - a.date;
+  });
+
+  console.log("items:");
+  console.log(items);
+
+  return items;
+}
+
+
+function populateItems( items ) {
+  console.log("populateItems");
+  console.log( items );
+
+  var rows = items.map(function (item) {
+    var row = document.createElement( "tr" );
+    var date = document.createElement( "td" );
+    var occurence = document.createElement( "td" );
+    var note = document.createElement( "td" );
+
+    date.textContent = dateString( item.date );
+    occurence.textContent = item.occurence;
+    note.textContent = item.occurenceNote;
+
+    row.appendChild( date );
+    row.appendChild( occurence );
+    row.appendChild( note );
+
+    return row;
+  });
+
+  var tableBody = document.querySelector("#bed-makings tbody");
+  console.log("populate: ");
+  console.log(tableBody);
+  console.log(rows);
+
+  while (tableBody.lastChild) {
+    tableBody.removeChild(tableBody.lastChild);
+  }
+
+  rows.forEach(function (row) {
+    tableBody.appendChild(row);
+  });
+}
+
+
+function createPlot ( feedItems ) {
+
+  console.log("createPlot");
+
+
+  var container = document.getElementById('visualization');
+
+  while( container.lastChild ) {
+    container.removeChild( container.lastChild );
+  }
+
+  var group = feedItems[0].feedLink == "https://harlanji.com/bed.xml" ? 0 : 10;
+
+  var items = feedItems.map(function (item) {
+     var startDate = startOfDay(item.date);
+
+     return {
+       id: item.feedLink + item.link,
+       x: startDate,
+       y: (item.date - startDate) / (1000 * 60),
+       group: group
+     };
+  });
+
+  // feed items are in descending order
+  var firstDate = startOfDay( feedItems[ feedItems.length - 1 ].date );
+  var lastDate = startOfDay( feedItems[ 0 ].date );
+
+  var firstTimes = SunCalc.getTimes(firstDate, 44.986656, -93.258133)
+  var lastTimes = SunCalc.getTimes(lastDate, 44.986656, -93.258133)
+  
+  if (!dataset.get("firstSunrise")) {
+
+  items.push({id: 'firstSunrise',
+             x: firstDate,
+             y: (firstTimes.sunrise - firstDate) / (1000 * 60),
+             group: 1});
+
+  items.push({id: 'lastSunrise',
+             x: lastDate,
+             y: (lastTimes.sunrise - lastDate) / (1000 * 60),
+             group: 1});
+  }
+  dataset.update(items);
+  dataset.flush();
+
+  var options = {
+      sort: false,
+      sampling:false,
+      style:'points',
+      dataAxis: {
+          left: {
+              range: {
+                  min: 0, max: (60 * 24)
+              }
+          }
+      },
+      drawPoints: {
+          enabled: true,
+          size: 6,
+          style: 'circle' // square, circle
+      },
+      defaultGroup: 'Scatterplot',
+      height: '400px',
+      width: '90%'
+  };
+
+
+  var graph2d = new vis.Graph2d(container, dataset, options);
+
+  var tomorrow = new Date();
+  tomorrow.setDate(tomorrow.getDate() + 1);
+  tomorrow = startOfDay(tomorrow);
+
+  graph2d.setWindow(firstDate, tomorrow, {animation: false});
+
+}
+
+
+
+// TV
+
+
+
+function importTvFeed (feedUrl) {
+
+  var rssRequest = new XMLHttpRequest();
+
+  rssRequest.onreadystatechange = function (e) {
+    var r = rssRequest;
+    if (r.readyState == XMLHttpRequest.DONE) {
+      if (r.status >= 200 && r.status < 300) {
+        console.log("Got TV RSS (" + r.status + "): ");
+
+        var items = importTvDoc(r.responseXML);
+        addTvToPlot( items );
+      }
+    }
+  };
+
+  //rssRequest.open("GET", "bed-harlanji.xml");
+
+  rssRequest.open("GET", feedUrl);
+  rssRequest.send();
+
+}
+
+function importTvDoc( rssDoc ) {
+  console.log("importTvDoc:");
+  console.log( rssDoc );
+
+  var feedLink = rssDoc.querySelector("link").textContent.trim();
+ 
+  console.log("tv feed link = " + feedLink);
+
+  var items = Array.from( rssDoc.querySelectorAll("item") );
+  items = items.map(function (i) {
+
+    var title = i.querySelector("title").textContent.trim();
+    var state = title.indexOf("on") > -1;
+
+    var item = {
+      title: title,
+      state: state,
+      date: new Date( i.querySelector("pubDate").textContent.trim() ),
+      description: i.querySelector("description").textContent.trim(),
+
+      link: i.querySelector("link").textContent.trim(),
+      feedLink: feedLink
+    };
+
+    return item;
+  });
+
+  items = items.sort(function (a, b) {
+    return b.date - a.date;
+  });
+
+  console.log("tv items:");
+  console.log(items);
+
+  return items;
+}
+
+
+
+function dateString (date) {
+//  return "d: " + date;
+
+  var str = (date.getYear() + 1900) + "-"
+              + new String(date.getMonth() + 1).padStart(2, "0") + "-"
+              + new String(date.getDate()).padStart(2, "0") + " "
+              + new String(date.getHours()).padStart(2, "0") + ":"
+              + new String(date.getMinutes()).padStart(2, "0") + ":"
+              + new String(date.getSeconds()).padStart(2, "0") + " ";
+
+  var tzOffset = date.getTimezoneOffset() / 60;
+
+  if (tzOffset >= 0) {
+    str += "+";
+  }
+
+  str += new String(tzOffset * 100).padStart(4, "0");
+           
+
+  return str;
+}
+
+
+
+
+function addTvToPlot ( feedItems ) {
+
+  console.log("addTvToPlot");
+
+
+  var items = feedItems.map(function (item) {
+     var startDate = startOfDay(item.date);
+     var group = item.state ? 3 : 4;
+
+     return {
+       id: item.feedLink + item.link,
+       x: startDate,
+       y: (item.date - startDate) / (1000 * 60),
+       group: group
+     };
+  });
+
+  dataset.update( items );
+  dataset.flush();
+}
+
+
+
+function splitString(string, delimiter, n) {
+    var parts = string.split(delimiter);
+    return parts.slice(0, n - 1).concat([parts.slice(n - 1).join(delimiter)]);
+}
+
+</script>
+<!--
+<script defer data-domain="cityapper.com" src="https://pa.cityapper.com/js/plausible.js"></script>
+-->
+
+  <script src="vis-timeline-graph2d.min.js"></script>
+  <link href="vis-timeline-graph2d.min.css" rel="stylesheet" type="text/css" />
+
+  <script src="suncalc.js"></script>
+
+  </head>
+  <body>
+
+<script>
+
+
+
+function importAuthorSubmitted( form ) {
+  console.log("submitted:");
+
+  console.log(form);
+}
+
+var authors = {
+  "HarlanJI": "https://harlanji.com/bed.xml",
+  "Marty": "https://harlanji.com/bed-marty.xml"
+}
+
+function populateAuthors () {
+  var authorsElem = document.querySelector("#author");
+
+  while(authorsElem.lastChild) {
+    authorsElem.removeChild(authorsElem.lastChild);
+  }
+
+  Object.entries(authors).forEach(function (e, i) {
+    var option = document.createElement("option");
+    option.value = e[1];
+    option.textContent = e[0];
+
+    if ( i == 0 ) {
+      option.selected = "selected";
+    }
+
+    authorsElem.appendChild(option);
+
+    console.log(e);
+  });
+
+  authorsElem.dispatchEvent(new Event('change'));
+}
+
+function startOfDay (date) {
+  var startDate = new Date(date);
+
+  startDate.setHours(0);
+  startDate.setMinutes(0);
+  startDate.setSeconds(0);
+  startDate.setMilliseconds(0);
+
+  return startDate;
+}
+
+
+
+</script>
+
+<h1>Author Made Bed</h1>
+
+<form id="import-author" action="#" onsubmit="console.log('s')">
+<p>
+Author:
+<select id="author" onchange="importRssFeed(this.options[this.selectedIndex].value)">
+</select>
+
+</p>
+</form>
+
+<div id="visualization"></div>
+
+<table id="bed-makings">
+  <thead>
+    <tr>
+      <th>Date Time</th>
+      <th>#</th>
+      <th>Note</th>
+    </tr>
+  </thead>
+  <tbody>
+  </tbody>
+</table>
+
+
+<script>
+
+//importRssFeed( document.querySelector("#author").value );
+
+initDataset();
+populateAuthors();
+
+</script>
+
+  </body>
+</html>

+ 440 - 0
bedrss/day-5/index.html

@@ -0,0 +1,440 @@
+<html>
+  <head>
+
+<script>
+
+
+// Bed
+
+function importRssFeed (feedUrl) {
+
+  var rssRequest = new XMLHttpRequest();
+
+
+  rssRequest.onreadystatechange = function (e) {
+    var r = rssRequest;
+    if (r.readyState == XMLHttpRequest.DONE) {
+      if (r.status >= 200 && r.status < 300) {
+        console.log("Got RSS (" + r.status + "): ");
+
+
+        var items = importRSSDoc(r.responseXML);
+        populateItems( items );
+        createPlot( items );
+
+        importTvFeed( "https://harlanji.com/tv.xml" );
+
+      }
+    }
+  };
+
+  //rssRequest.open("GET", "bed-harlanji.xml");
+
+  rssRequest.open("GET", feedUrl);
+  rssRequest.send();
+
+}
+
+function importRSSDoc( rssDoc ) {
+  console.log("importRSSDoc:");
+  console.log( rssDoc );
+
+  var feedLink = rssDoc.querySelector("link").textContent.trim();
+
+  var items = Array.from( rssDoc.querySelectorAll("item") );
+  items = items.map(function (i) {
+    var item = {
+      title: i.querySelector("title").textContent.trim(),
+      date: new Date( i.querySelector("pubDate").textContent.trim() ),
+      description: i.querySelector("description").textContent.trim(),
+
+      link: i.querySelector("link").textContent.trim(),
+      feedLink: feedLink
+    };
+
+    var occurenceParts = splitString( item.description, ":", 2);
+
+
+    item.occurence = parseInt( occurenceParts[0] ) || 1;
+    item.occurenceNote = occurenceParts[1].trim();
+
+    if (item.occurenceNote == "") {
+      item.occurenceNote = "Morning routine";
+    }
+
+    return item;
+  });
+
+
+  items = items.sort(function (a, b) {
+    return b.date - a.date;
+  });
+
+  console.log("items:");
+  console.log(items);
+
+  return items;
+}
+
+
+function populateItems( items ) {
+  console.log("populateItems");
+  console.log( items );
+
+  var rows = items.map(function (item) {
+    var row = document.createElement( "tr" );
+    var date = document.createElement( "td" );
+    var occurence = document.createElement( "td" );
+    var note = document.createElement( "td" );
+
+    date.textContent = dateString( item.date );
+    occurence.textContent = item.occurence;
+    note.textContent = item.occurenceNote;
+
+    row.appendChild( date );
+    row.appendChild( occurence );
+    row.appendChild( note );
+
+    return row;
+  });
+
+  var tableBody = document.querySelector("#bed-makings tbody");
+  console.log("populate: ");
+  console.log(tableBody);
+  console.log(rows);
+
+  while (tableBody.lastChild) {
+    tableBody.removeChild(tableBody.lastChild);
+  }
+
+  rows.forEach(function (row) {
+    tableBody.appendChild(row);
+  });
+}
+
+var dataset;
+
+function initDataset () {
+  dataset = new vis.DataSet();
+}
+
+var groups = [];
+function groupFor (str) {
+  var idx = groups.indexOf(str);
+  if (idx == -1) {
+    idx = groups.length;
+    groups.push(str);
+  }
+
+  return idx;
+}
+
+function createPlot ( feedItems ) {
+
+  console.log("createPlot");
+
+
+
+  var container = document.getElementById('visualization');
+
+  while( container.lastChild ) {
+    container.removeChild( container.lastChild );
+  }
+
+  
+  if (feedItems.length == 0) {
+    return;
+  }
+
+  var items = feedItems.map(function (item) {
+     var startDate = startOfDay(item.date);
+
+     return {
+       x: startDate,
+       y: (item.date - startDate) / (1000 * 60),
+
+       group: groupFor(item.feedLink),
+       id: item.feedLink + item.link
+     };
+  });
+
+  // feed items are in descending order
+  var firstDate = startOfDay( feedItems[ feedItems.length - 1 ].date );
+  var lastDate = startOfDay( feedItems[ 0 ].date );
+
+  var currentFirstDate = dataset.get("firstDateSunrise");
+  if (!currentFirstDate || currentFirstDate.x > firstDate) {
+    var firstTimes = SunCalc.getTimes(firstDate, 44.986656, -93.258133)
+    items.push({id: "firstDateSunrise",
+                x: firstDate,
+                y: (firstTimes.sunrise - firstDate) / (1000 * 60),
+                group: groupFor("sunrise")});
+  }
+
+
+  var currentLastDate = dataset.get("lastDateSunrise");
+  if (!currentLastDate || currentLastDate.x < lastDate) {
+    var lastTimes = SunCalc.getTimes(lastDate, 44.986656, -93.258133)
+  
+
+    items.push({id: "lastDateSunrise",
+                x: lastDate,
+                y: (lastTimes.sunrise - lastDate) / (1000 * 60),
+                group: groupFor("sunrise")});
+  }
+
+  dataset.update(items);
+
+  var options = {
+      sort: false,
+      sampling:false,
+      style:'points',
+      dataAxis: {
+          left: {
+              range: {
+                  min: 0, max: (60 * 24)
+              }
+          }
+      },
+      drawPoints: {
+          enabled: true,
+          size: 6,
+          style: 'circle' // square, circle
+      },
+      defaultGroup: 'Scatterplot',
+      height: '400px',
+      width: '90%'
+  };
+
+  var graph2d = new vis.Graph2d(container, dataset, options);
+
+
+  // sunrise is the earliest and latest feed items
+  var windowStartDate = new Date(dataset.get("firstDateSunrise").x);
+  var windowEndDate = new Date(dataset.get("lastDateSunrise").x);
+
+  // day before first and after the latest day
+  windowStartDate.setDate( windowStartDate.getDate() - 1 );
+  windowEndDate.setDate( windowEndDate.getDate() + 1 );
+
+  graph2d.setWindow(windowStartDate, windowEndDate, {animation: false});
+
+}
+
+
+
+// TV
+
+
+
+function importTvFeed (feedUrl) {
+
+  var rssRequest = new XMLHttpRequest();
+
+
+  rssRequest.onreadystatechange = function (e) {
+    var r = rssRequest;
+    if (r.readyState == XMLHttpRequest.DONE) {
+      if (r.status >= 200 && r.status < 300) {
+        console.log("Got TV RSS (" + r.status + "): ");
+
+        var items = importTvDoc(r.responseXML);
+        addTvToPlot( items );
+      }
+    }
+  };
+
+  //rssRequest.open("GET", "bed-harlanji.xml");
+
+  rssRequest.open("GET", feedUrl);
+  rssRequest.send();
+
+}
+
+function importTvDoc( rssDoc ) {
+  console.log("importTvDoc:");
+  console.log( rssDoc );
+
+
+  var items = Array.from( rssDoc.querySelectorAll("item") );
+  items = items.map(function (i) {
+
+    var title = i.querySelector("title").textContent.trim();
+    var state = title.indexOf("on") > -1;
+
+    var item = {
+      title: title,
+      state: state,
+      date: new Date( i.querySelector("pubDate").textContent.trim() ),
+      description: i.querySelector("description").textContent.trim()
+    };
+
+    return item;
+  });
+
+  items = items.sort(function (a, b) {
+    return b.date - a.date;
+  });
+
+  console.log("tv items:");
+  console.log(items);
+
+  return items;
+}
+
+
+
+function dateString (date) {
+//  return "d: " + date;
+
+  var str = (date.getYear() + 1900) + "-"
+              + new String(date.getMonth() + 1).padStart(2, "0") + "-"
+              + new String(date.getDate()).padStart(2, "0") + " "
+              + new String(date.getHours()).padStart(2, "0") + ":"
+              + new String(date.getMinutes()).padStart(2, "0") + ":"
+              + new String(date.getSeconds()).padStart(2, "0") + " ";
+
+  var tzOffset = date.getTimezoneOffset() / 60;
+
+  if (tzOffset >= 0) {
+    str += "+";
+  }
+
+  str += new String(tzOffset * 100).padStart(4, "0");
+           
+
+  return str;
+}
+
+
+
+
+function addTvToPlot ( feedItems ) {
+
+  console.log("addTvToPlot");
+
+
+  var items = feedItems.map(function (item) {
+     var startDate = startOfDay(item.date);
+     var group = item.state ? 3 : 4;
+
+     return {
+       x: startDate,
+       y: (item.date - startDate) / (1000 * 60),
+       group: group
+     };
+  });
+
+  dataset.add( items );
+}
+
+
+
+function splitString(string, delimiter, n) {
+    var parts = string.split(delimiter);
+    return parts.slice(0, n - 1).concat([parts.slice(n - 1).join(delimiter)]);
+}
+
+</script>
+
+<script defer data-domain="cityapper.com" src="https://pa.cityapper.com/js/plausible.js"></script>
+
+  <script src="vis-timeline-graph2d.min.js"></script>
+  <link href="vis-timeline-graph2d.min.css" rel="stylesheet" type="text/css" />
+
+  <script src="suncalc.js"></script>
+
+  </head>
+  <body>
+
+<script>
+
+function importAuthorSubmitted( form ) {
+  console.log("submitted:");
+
+  console.log(form);
+}
+
+var authors = {
+  "HarlanJI": "https://harlanji.com/bed.xml",
+  "Marty": "https://harlanji.com/bed-marty.xml"
+}
+
+function populateAuthors () {
+  var authorsElem = document.querySelector("#author");
+
+  while(authorsElem.lastChild) {
+    authorsElem.removeChild(authorsElem.lastChild);
+  }
+
+  Object.entries(authors).forEach(function (e, i) {
+    var option = document.createElement("option");
+    option.value = e[1];
+    option.textContent = e[0];
+
+    if ( i == 0 ) {
+      option.selected = "selected";
+    }
+
+    authorsElem.appendChild(option);
+
+    console.log(e);
+  });
+
+  authorsElem.dispatchEvent(new Event('change'));
+}
+
+function startOfDay (date) {
+  var startDate = new Date(date);
+
+  startDate.setHours(0);
+  startDate.setMinutes(0);
+  startDate.setSeconds(0);
+  startDate.setMilliseconds(0);
+
+  return startDate;
+}
+
+
+
+</script>
+
+<h1>Author Made Bed</h1>
+
+
+
+<form id="import-author" action="#" onsubmit="console.log('s')">
+<p>
+Author:
+<select id="author" onchange="importRssFeed(this.options[this.selectedIndex].value)">
+</select>
+
+</p>
+</form>
+
+<div id="visualization"></div>
+
+<table id="bed-makings">
+  <thead>
+    <tr>
+      <th>Date Time</th>
+      <th>#</th>
+      <th>Note</th>
+    </tr>
+  </thead>
+  <tbody>
+  </tbody>
+</table>
+
+
+<script>
+
+//importRssFeed( document.querySelector("#author").value );
+
+populateAuthors();
+initDataset();
+
+</script>
+
+  </body>
+</html>

+ 567 - 0
bedrss/day-6/index-bank.html

@@ -0,0 +1,567 @@
+<html>
+  <head>
+
+<script>
+
+
+// Bed
+
+function importRssFeed (feedUrl) {
+
+  var rssRequest = new XMLHttpRequest();
+
+
+  rssRequest.onreadystatechange = function (e) {
+    var r = rssRequest;
+    if (r.readyState == XMLHttpRequest.DONE) {
+      if (r.status >= 200 && r.status < 300) {
+        console.log("Got RSS (" + r.status + "): ");
+
+
+        var items = importRSSDoc(r.responseXML);
+        populateItems( items );
+        createPlot( items );
+
+        importTvFeed( "https://harlanji.com/tv.xml" );
+
+      }
+    }
+  };
+
+  //rssRequest.open("GET", "bed-harlanji.xml");
+
+  rssRequest.open("GET", feedUrl);
+  rssRequest.send();
+
+}
+
+function importRSSDoc( rssDoc ) {
+  console.log("importRSSDoc:");
+  console.log( rssDoc );
+
+  var feedLink = rssDoc.querySelector("link").textContent.trim();
+
+  var items = Array.from( rssDoc.querySelectorAll("item") );
+  items = items.map(function (i) {
+    var item = {
+      title: i.querySelector("title").textContent.trim(),
+      date: new Date( i.querySelector("pubDate").textContent.trim() ),
+      description: i.querySelector("description").textContent.trim(),
+
+      link: i.querySelector("link").textContent.trim(),
+      feedLink: feedLink
+    };
+
+    var occurenceParts = splitString( item.description, ":", 2);
+
+
+    item.occurence = parseInt( occurenceParts[0] ) || 1;
+    item.occurenceNote = occurenceParts[1].trim();
+
+    if (item.occurenceNote == "") {
+      item.occurenceNote = "Morning routine";
+    }
+
+    return item;
+  });
+
+
+  items = items.sort(function (a, b) {
+    return b.date - a.date;
+  });
+
+  console.log("items:");
+  console.log(items);
+
+  return items;
+}
+
+
+function populateItems( items ) {
+  console.log("populateItems");
+  console.log( items );
+
+  var rows = items.map(function (item) {
+    var row = document.createElement( "tr" );
+    var date = document.createElement( "td" );
+    var occurence = document.createElement( "td" );
+    var note = document.createElement( "td" );
+
+    date.textContent = dateString( item.date );
+    occurence.textContent = item.occurence;
+    note.textContent = item.occurenceNote;
+
+    row.appendChild( date );
+    row.appendChild( occurence );
+    row.appendChild( note );
+
+    return row;
+  });
+
+  var tableBody = document.querySelector("#bed-makings tbody");
+  console.log("populate: ");
+  console.log(tableBody);
+  console.log(rows);
+
+  while (tableBody.lastChild) {
+    tableBody.removeChild(tableBody.lastChild);
+  }
+
+  rows.forEach(function (row) {
+    tableBody.appendChild(row);
+  });
+}
+
+var dataset;
+
+function initDataset () {
+  dataset = new vis.DataSet();
+}
+
+var groups = [];
+function groupFor (str) {
+  var idx = groups.indexOf(str);
+  if (idx == -1) {
+    idx = groups.length;
+    groups.push(str);
+  }
+
+  return idx;
+}
+
+function createPlot ( feedItems ) {
+
+  console.log("createPlot");
+
+
+
+  var container = document.getElementById('visualization');
+
+  while( container.lastChild ) {
+    container.removeChild( container.lastChild );
+  }
+
+  
+  if (feedItems.length == 0) {
+    return;
+  }
+
+  var items = feedItems.map(function (item) {
+     var startDate = startOfDay(item.date);
+
+     return {
+       x: startDate,
+       y: (item.date - startDate) / (1000 * 60),
+
+       group: groupFor(item.feedLink),
+       id: item.feedLink + item.link
+     };
+  });
+
+  // feed items are in descending order
+  var firstDate = startOfDay( feedItems[ feedItems.length - 1 ].date );
+  var lastDate = startOfDay( feedItems[ 0 ].date );
+
+  var currentFirstDate = dataset.get("firstDateSunrise");
+  if (!currentFirstDate || currentFirstDate.x > firstDate) {
+    var firstTimes = SunCalc.getTimes(firstDate, 44.986656, -93.258133)
+    items.push({id: "firstDateSunrise",
+                x: firstDate,
+                y: (firstTimes.sunrise - firstDate) / (1000 * 60),
+                group: groupFor("sunrise")});
+  }
+
+
+  var currentLastDate = dataset.get("lastDateSunrise");
+  if (!currentLastDate || currentLastDate.x < lastDate) {
+    var lastTimes = SunCalc.getTimes(lastDate, 44.986656, -93.258133)
+  
+
+    items.push({id: "lastDateSunrise",
+                x: lastDate,
+                y: (lastTimes.sunrise - lastDate) / (1000 * 60),
+                group: groupFor("sunrise")});
+  }
+
+  dataset.update(items);
+
+  var options = {
+      sort: false,
+      sampling:false,
+      style:'points',
+      dataAxis: {
+          left: {
+              range: {
+                  min: 0, max: (60 * 24)
+              }
+          }
+      },
+      drawPoints: {
+          enabled: true,
+          size: 6,
+          style: 'circle' // square, circle
+      },
+      defaultGroup: 'Scatterplot',
+      height: '400px',
+      width: '90%'
+  };
+
+  var graph2d = new vis.Graph2d(container, dataset, options);
+
+
+  // sunrise is the earliest and latest feed items
+  var windowStartDate = new Date(dataset.get("firstDateSunrise").x);
+  var windowEndDate = new Date(dataset.get("lastDateSunrise").x);
+
+  // day before first and after the latest day
+  windowStartDate.setDate( windowStartDate.getDate() - 1 );
+  windowEndDate.setDate( windowEndDate.getDate() + 1 );
+
+  graph2d.setWindow(windowStartDate, windowEndDate, {animation: false});
+
+}
+
+
+
+// TV
+
+
+
+function importTvFeed (feedUrl) {
+
+  var rssRequest = new XMLHttpRequest();
+
+
+  rssRequest.onreadystatechange = function (e) {
+    var r = rssRequest;
+    if (r.readyState == XMLHttpRequest.DONE) {
+      if (r.status >= 200 && r.status < 300) {
+        console.log("Got TV RSS (" + r.status + "): ");
+
+        var items = importTvDoc(r.responseXML);
+        addTvToPlot( items );
+      }
+    }
+  };
+
+  //rssRequest.open("GET", "bed-harlanji.xml");
+
+  rssRequest.open("GET", feedUrl);
+  rssRequest.send();
+
+}
+
+function importTvDoc( rssDoc ) {
+  console.log("importTvDoc:");
+  console.log( rssDoc );
+
+
+  var items = Array.from( rssDoc.querySelectorAll("item") );
+  items = items.map(function (i) {
+
+    var title = i.querySelector("title").textContent.trim();
+    var state = title.indexOf("on") > -1;
+
+    var item = {
+      title: title,
+      state: state,
+      date: new Date( i.querySelector("pubDate").textContent.trim() ),
+      description: i.querySelector("description").textContent.trim()
+    };
+
+    return item;
+  });
+
+  items = items.sort(function (a, b) {
+    return b.date - a.date;
+  });
+
+  console.log("tv items:");
+  console.log(items);
+
+  return items;
+}
+
+
+
+
+// Steps CSV
+
+function importStepsFeed (feedUrl) {
+
+  var rssRequest = new XMLHttpRequest();
+
+
+  rssRequest.onreadystatechange = function (e) {
+    var r = rssRequest;
+    if (r.readyState == XMLHttpRequest.DONE) {
+      if (r.status >= 200 && r.status < 300) {
+        var parseHeader = true;
+        console.log("Got Steps CSV (" + r.status + ", parseHeader=" + parseHeader + "): ");
+
+        var items = r.responseText.split("\n")
+          .map(function (line) { return line.trim(); })
+          .filter(function (line, i) { return !(parseHeader && i == 0) && line != ""; })
+          .map(function (line, i, arr) {
+            var parts = line.split(",");
+            var date = new Date(parts[0]),
+                steps = parseInt(parts[1]);
+
+            return {
+              date: date,
+              steps: steps
+            }            
+          });
+
+
+        addStepsToPlot( items );
+      }
+    }
+  };
+
+  //rssRequest.open("GET", "bed-harlanji.xml");
+
+  rssRequest.open("GET", feedUrl);
+  rssRequest.send();
+
+}
+
+// Bank CSV
+
+function importBnkFeed (feedUrl) {
+
+  var rssRequest = new XMLHttpRequest();
+
+
+  rssRequest.onreadystatechange = function (e) {
+    var r = rssRequest;
+    if (r.readyState == XMLHttpRequest.DONE) {
+      if (r.status >= 200 && r.status < 300) {
+        var parseHeader = true;
+        console.log("Got Bank CSV (" + r.status + ", parseHeader=" + parseHeader + "): ");
+
+        var items = r.responseText.split("\n")
+          .map(function (line) { return line.trim(); })
+          .filter(function (line, i) { return !(parseHeader && i == 0) && line != ""; })
+          .map(function (line, i, arr) {
+            var parts = line.split(",");
+            var date = new Date(parts[0]),
+                amt = parseInt(parts[1]),
+                from = parts[2];
+
+            return {
+              date: date,
+              amt: amt,
+              from: from
+            }            
+          })
+          .filter(function (item) { return item.from == 'mi';  });
+
+
+        addBankToPlot( items );
+      }
+    }
+  };
+
+  //rssRequest.open("GET", "bed-harlanji.xml");
+
+  rssRequest.open("GET", feedUrl);
+  rssRequest.send();
+
+}
+
+
+function dateString (date) {
+//  return "d: " + date;
+
+  var str = (date.getYear() + 1900) + "-"
+              + new String(date.getMonth() + 1).padStart(2, "0") + "-"
+              + new String(date.getDate()).padStart(2, "0") + " "
+              + new String(date.getHours()).padStart(2, "0") + ":"
+              + new String(date.getMinutes()).padStart(2, "0") + ":"
+              + new String(date.getSeconds()).padStart(2, "0") + " ";
+
+  var tzOffset = date.getTimezoneOffset() / 60;
+
+  if (tzOffset >= 0) {
+    str += "+";
+  }
+
+  str += new String(tzOffset * 100).padStart(4, "0");
+           
+
+  return str;
+}
+
+
+
+
+function addStepsToPlot ( feedItems ) {
+
+  console.log("addStepsToPlot");
+
+
+  var items = feedItems.map(function (item) {
+     var startDate = startOfDay(item.date);
+     var group = 99;
+
+     return {
+       x: startDate,
+       y: item.steps / 8, // fixme: normalize over 1440 seconds
+       group: group,
+       id: "steps-" + item.date
+     };
+  });
+
+  dataset.update( items );
+}
+
+function addBankToPlot ( feedItems ) {
+
+  console.log("addBankToPlot");
+
+
+  var items = feedItems.map(function (item) {
+     var startDate = startOfDay(item.date);
+     var group = 199;
+
+     return {
+       x: startDate,
+       y: item.amt <= 1440 ? item.amt : 1440, // fixme: normalize over 1440 seconds
+       group: group,
+       id: "bank-" + item.date
+     };
+  });
+
+  dataset.update( items );
+}
+
+
+function addTvToPlot ( feedItems ) {
+
+  console.log("addTvToPlot");
+
+
+  var items = feedItems.map(function (item) {
+     var startDate = startOfDay(item.date);
+     var group = item.state ? 3 : 4;
+
+     return {
+       x: startDate,
+       y: (item.date - startDate) / (1000 * 60),
+       group: group
+     };
+  });
+
+  dataset.add( items );
+}
+
+
+
+function splitString(string, delimiter, n) {
+    var parts = string.split(delimiter);
+    return parts.slice(0, n - 1).concat([parts.slice(n - 1).join(delimiter)]);
+}
+
+</script>
+
+
+  <script src="vis-timeline-graph2d.min.js"></script>
+  <link href="vis-timeline-graph2d.min.css" rel="stylesheet" type="text/css" />
+
+  <script src="suncalc.js"></script>
+
+  </head>
+  <body>
+
+<script>
+
+function importAuthorSubmitted( form ) {
+  console.log("submitted:");
+
+  console.log(form);
+}
+
+var authors = {
+  "HarlanJI": "https://harlanji.com/bed.xml",
+  "Marty": "https://harlanji.com/bed-marty.xml"
+}
+
+function populateAuthors () {
+  var authorsElem = document.querySelector("#author");
+
+  while(authorsElem.lastChild) {
+    authorsElem.removeChild(authorsElem.lastChild);
+  }
+
+  Object.entries(authors).forEach(function (e, i) {
+    var option = document.createElement("option");
+    option.value = e[1];
+    option.textContent = e[0];
+
+    if ( i == 0 ) {
+      option.selected = "selected";
+    }
+
+    authorsElem.appendChild(option);
+
+    console.log(e);
+  });
+
+  authorsElem.dispatchEvent(new Event('change'));
+}
+
+function startOfDay (date) {
+  var startDate = new Date(date);
+
+  startDate.setHours(0);
+  startDate.setMinutes(0);
+  startDate.setSeconds(0);
+  startDate.setMilliseconds(0);
+
+  return startDate;
+}
+
+
+
+</script>
+
+<h1>Author Made Bed</h1>
+
+
+
+<form id="import-author" action="#" onsubmit="console.log('s')">
+<p>
+Author:
+<select id="author" onchange="importRssFeed(this.options[this.selectedIndex].value)">
+</select>
+
+</p>
+</form>
+
+<div id="visualization"></div>
+
+<table id="bed-makings">
+  <thead>
+    <tr>
+      <th>Date Time</th>
+      <th>#</th>
+      <th>Note</th>
+    </tr>
+  </thead>
+  <tbody>
+  </tbody>
+</table>
+
+
+<script>
+
+//importRssFeed( document.querySelector("#author").value );
+
+populateAuthors();
+initDataset();
+
+</script>
+
+  </body>
+</html>

+ 684 - 0
bedrss/day-6/index-jobs.html

@@ -0,0 +1,684 @@
+<html>
+  <head>
+
+<script>
+
+function setupWindowSync () {
+  graph2d.on('rangechanged', function (r) {
+    if (timeline) {
+      timeline.setWindow(r);
+    }
+    if (barChart) {
+      barChart.setWindow(r);
+    }
+  });
+}
+
+
+
+// Bar Chart Underlay
+var barChart;
+function createBarChart () {
+
+    var container = document.getElementById('visualization3');
+    var items = [
+    {id: 8, content: 'job 6', y: 55, x: '2006-03-01', end: '2007-11-20', group: 1},
+    {id: 1, content: 'job 7', y: 65, x: '2011-05-20', end: '2012-12-20', group: 1},
+    {id: 2, content: 'job 8', y: 166.4, x: '2013-05-01', end: '2013-11-15', group: 1},
+    {id: 3, content: 'job 9', y: 135, x: '2014-01-03', end: '2014-03-15', group: 1},
+    {id: 4, content: 'job 10', y: 145, x: '2014-03-21', end: '2016-04-01', group: 1},
+    {id: 5, content: 'job 11', y: 195, x: '2016-06-01', end: '2016-08-15', group: 1},
+    {id: 6, content: 'job 12', y: 145, x: '2016-12-01', end: '2017-05-01', group: 1},
+    {id: 7, content: 'job 13', y: 155, x: '2017-08-01', end: '2017-11-30', group: 1},
+
+
+    {id: 100, content: 'credit 1', y: 160, x: '2006-01-01', end: '2017-02-28', group: 2},
+    {id: 101, content: 'credit 2', y: 120, x: '2017-02-28', end: '2018-11-30', group: 2},
+    {id: 102, content: 'credit 3', y: 90, x: '2018-11-30', end: '2019-11-30', group: 2},
+    {id: 104, content: 'credit 5', y: 50, x: '2019-11-30', end: '2020-11-30', group: 2},
+    {id: 105, content: 'credit 6', y: 30, x: '2020-11-30', end: '2021-11-30', group: 2}
+
+    ];
+
+    var dataset = new vis.DataSet(items);
+    var options = {
+        style:'bar',
+        drawPoints: false,
+        dataAxis: {
+              width: '88px',
+         //     visible: false,
+            icons:true
+        },
+        graphHeight: '400px',
+
+        width: '90%',
+        orientation:'none'
+    };
+    barChart = new vis.Graph2d(container, items, options);
+
+}
+
+// Timeline
+
+var timeline;
+
+function createTimeline () {
+  // DOM element where the Timeline will be attached
+  var container = document.getElementById('visualization2');
+
+  // Create a DataSet (allows two way data-binding)
+  var items = new vis.DataSet([
+    {id: 8, content: 'job 6', start: '2006-03-01', end: '2007-11-20', group: 1},
+    {id: 1, content: 'job 7', start: '2011-05-20', end: '2012-12-20', group: 1},
+    {id: 2, content: 'job 8', start: '2013-05-01', end: '2013-11-15', group: 1},
+    {id: 3, content: 'job 9', start: '2014-01-03', end: '2014-03-15', group: 1},
+    {id: 4, content: 'job 10', start: '2014-03-21', end: '2016-04-01', group: 1},
+    {id: 5, content: 'job 11', start: '2016-06-01', end: '2016-08-15', group: 1},
+    {id: 6, content: 'job 12', start: '2016-12-01', end: '2017-05-01', group: 1},
+    {id: 7, content: 'job 13', start: '2017-08-01', end: '2017-11-30', group: 1},
+
+    {id: 100, content: 'friend 1', start: '2005-01-01', end: '2017-09-01', group: 2},
+    {id: 101, content: 'friend 2', start: '2005-06-01', end: '2015-03-01', group: 2},
+    {id: 102, content: 'friend 3', start: '2005-01-01', end: '2017-09-01', group: 2},
+
+    {id: 201, content: 'school 3', start: '2008-01-15', end: '2011-05-01', group: 3},
+
+    {id: 1001, content: 'project 1', start: '2007-06-15', end: '2009-01-01', group: 4, editable: true},
+
+    {id: 2001, content: 'loc 1', start: '1985-01-24', end: '2011-05-01', group: 5},
+    {id: 2002, content: 'loc 2', start: '2011-05-15', end: '2012-12-01', group: 5},
+
+    {id: 6000, content: 'fulton', start: '2014-01-03', end: '2016-03-01', group: 6},
+    {id: 6001, content: '48th ave', start: '2016-03-01', end: '2018-06-01', group: 6},
+    {id: 6010, content: 'civic', start: '2018-06-01', end: '2021-08-01', group: 6},
+    {id: 6020, content: 'relative', start: '2021-08-01', end: '2021-12-01', group: 6}
+  ]);
+
+  var groups = new vis.DataSet([
+    {id: 1, content: 'Jobs', order: 5},
+    {id: 2, content: 'Friends', order: 6},
+    {id: 3, content: 'Schools', order: 4},
+    {id: 4, content: 'Projects', order: 3},
+    {id: 5, content: 'Locations', order: 1},
+    {id: 6, content: 'Housing', order: 2}
+  ]);
+
+  // Configuration for the Timeline
+  var options = {
+    width: '90%',
+    groupOrder: 'order'
+  };
+
+  // Create a Timeline
+  timeline = new vis.Timeline(container, items, groups, options);
+
+}
+
+// Tweet storm summaries
+
+function importTweetStormSummaries (feedUrl) {
+
+
+  fetch( feedUrl )
+    .then(function (res) {
+      return res.json();
+    })
+    .then(function (storms) {
+      var items = storms.map(function (s) {
+        var dt = new Date(s.datetime);
+        var dayStart = startOfDay(dt);
+        return {
+          id: "tweet-storm_" + dt,
+          x: dayStart,
+          y: (dt - dayStart) / (1000 * 60)
+        };
+      });
+
+      return items;
+    })
+    .then(function (items) {
+      dataset.update(items);
+    });
+}
+
+
+// Bed
+
+function importRssFeed (feedUrl) {
+
+  var rssRequest = new XMLHttpRequest();
+
+
+  rssRequest.onreadystatechange = function (e) {
+    var r = rssRequest;
+    if (r.readyState == XMLHttpRequest.DONE) {
+      if (r.status >= 200 && r.status < 300) {
+        console.log("Got RSS (" + r.status + "): ");
+
+
+        var items = importRSSDoc(r.responseXML);
+        populateItems( items );
+        createPlot( items );
+
+        importTvFeed( "https://harlanji.com/tv.xml" );
+
+      }
+    }
+  };
+
+  //rssRequest.open("GET", "bed-harlanji.xml");
+
+  rssRequest.open("GET", feedUrl);
+  rssRequest.send();
+
+}
+
+function importRSSDoc( rssDoc ) {
+  console.log("importRSSDoc:");
+  console.log( rssDoc );
+
+  var feedLink = rssDoc.querySelector("link").textContent.trim();
+
+  var items = Array.from( rssDoc.querySelectorAll("item") );
+  items = items.map(function (i) {
+    var item = {
+      title: i.querySelector("title").textContent.trim(),
+      date: new Date( i.querySelector("pubDate").textContent.trim() ),
+      description: i.querySelector("description").textContent.trim(),
+
+      link: i.querySelector("link").textContent.trim(),
+      feedLink: feedLink
+    };
+
+    var occurenceParts = splitString( item.description, ":", 2);
+
+
+    item.occurence = parseInt( occurenceParts[0] ) || 1;
+    item.occurenceNote = occurenceParts[1].trim();
+
+    if (item.occurenceNote == "") {
+      item.occurenceNote = "Morning routine";
+    }
+
+    return item;
+  });
+
+
+  items = items.sort(function (a, b) {
+    return b.date - a.date;
+  });
+
+  console.log("items:");
+  console.log(items);
+
+  return items;
+}
+
+
+function populateItems( items ) {
+  console.log("populateItems");
+  console.log( items );
+
+  var rows = items.map(function (item) {
+    var row = document.createElement( "tr" );
+    var date = document.createElement( "td" );
+    var occurence = document.createElement( "td" );
+    var note = document.createElement( "td" );
+
+    date.textContent = dateString( item.date );
+    occurence.textContent = item.occurence;
+    note.textContent = item.occurenceNote;
+
+    row.appendChild( date );
+    row.appendChild( occurence );
+    row.appendChild( note );
+
+    return row;
+  });
+
+  var tableBody = document.querySelector("#bed-makings tbody");
+  console.log("populate: ");
+  console.log(tableBody);
+  console.log(rows);
+
+  while (tableBody.lastChild) {
+    tableBody.removeChild(tableBody.lastChild);
+  }
+
+  rows.forEach(function (row) {
+    tableBody.appendChild(row);
+  });
+}
+
+var dataset;
+
+function initDataset () {
+  dataset = new vis.DataSet();
+}
+
+var groups = [];
+function groupFor (str) {
+  var idx = groups.indexOf(str);
+  if (idx == -1) {
+    idx = groups.length;
+    groups.push(str);
+  }
+
+  return idx;
+}
+
+var graph2d;
+
+function createPlot ( feedItems ) {
+
+  console.log("createPlot");
+
+
+
+  var container = document.getElementById('visualization');
+
+  while( container.lastChild ) {
+    container.removeChild( container.lastChild );
+  }
+
+  
+  if (feedItems.length == 0) {
+    return;
+  }
+
+
+  var items = feedItems.map(function (item) {
+     var startDate = startOfDay(item.date);
+
+     return {
+       x: startDate,
+       y: (item.date - startDate) / (1000 * 60),
+
+       group: groupFor(item.feedLink),
+       id: item.feedLink + item.link
+     };
+  });
+
+  // feed items are in descending order
+  var firstDate = startOfDay( feedItems[ feedItems.length - 1 ].date );
+  var lastDate = startOfDay( feedItems[ 0 ].date );
+
+  var currentFirstDate = dataset.get("firstDateSunrise");
+  if (!currentFirstDate || currentFirstDate.x > firstDate) {
+    var firstTimes = SunCalc.getTimes(firstDate, 44.986656, -93.258133)
+    items.push({id: "firstDateSunrise",
+                x: firstDate,
+                y: (firstTimes.sunrise - firstDate) / (1000 * 60),
+                group: groupFor("sunrise")});
+  }
+
+
+  var currentLastDate = dataset.get("lastDateSunrise");
+  if (!currentLastDate || currentLastDate.x < lastDate) {
+    var lastTimes = SunCalc.getTimes(lastDate, 44.986656, -93.258133)
+  
+
+    items.push({id: "lastDateSunrise",
+                x: lastDate,
+                y: (lastTimes.sunrise - lastDate) / (1000 * 60),
+                group: groupFor("sunrise")});
+  }
+
+  dataset.update(items);
+
+  var options = {
+      sort: false,
+      sampling:false,
+      style:'points',
+      dataAxis: {
+              width: '88px',
+
+          //visible: false,
+          left: {
+              range: {
+                  min: 0, max: (60 * 24)
+              }
+          }
+      },  
+      drawPoints: {
+          enabled: true,
+          size: 6,
+          style: 'circle' // square, circle
+      },
+      defaultGroup: 'Scatterplot',
+      graphHeight: '400px',
+      width: '90%'
+  };
+
+  graph2d = new vis.Graph2d(container, dataset, options);
+
+
+
+
+  // sunrise is the earliest and latest feed items
+  var windowStartDate = new Date(dataset.get("firstDateSunrise").x);
+  var windowEndDate = new Date(dataset.get("lastDateSunrise").x);
+
+  // day before first and after the latest day
+  windowStartDate.setDate( windowStartDate.getDate() - 1 );
+  windowEndDate.setDate( windowEndDate.getDate() + 1 );
+
+  graph2d.setWindow(windowStartDate, windowEndDate, {animation: false});
+
+}
+
+
+
+// TV
+
+
+
+function importTvFeed (feedUrl) {
+
+  var rssRequest = new XMLHttpRequest();
+
+
+  rssRequest.onreadystatechange = function (e) {
+    var r = rssRequest;
+    if (r.readyState == XMLHttpRequest.DONE) {
+      if (r.status >= 200 && r.status < 300) {
+        console.log("Got TV RSS (" + r.status + "): ");
+
+        var items = importTvDoc(r.responseXML);
+        addTvToPlot( items );
+      }
+    }
+  };
+
+  //rssRequest.open("GET", "bed-harlanji.xml");
+
+  rssRequest.open("GET", feedUrl);
+  rssRequest.send();
+
+}
+
+function importTvDoc( rssDoc ) {
+  console.log("importTvDoc:");
+  console.log( rssDoc );
+
+
+  var items = Array.from( rssDoc.querySelectorAll("item") );
+  items = items.map(function (i) {
+
+    var title = i.querySelector("title").textContent.trim();
+    var state = title.indexOf("on") > -1;
+
+    var item = {
+      title: title,
+      state: state,
+      date: new Date( i.querySelector("pubDate").textContent.trim() ),
+      description: i.querySelector("description").textContent.trim()
+    };
+
+    return item;
+  });
+
+  items = items.sort(function (a, b) {
+    return b.date - a.date;
+  });
+
+  console.log("tv items:");
+  console.log(items);
+
+  return items;
+}
+
+
+
+
+// Steps CSV
+
+function importStepsFeed (feedUrl) {
+
+  var rssRequest = new XMLHttpRequest();
+
+
+  rssRequest.onreadystatechange = function (e) {
+    var r = rssRequest;
+    if (r.readyState == XMLHttpRequest.DONE) {
+      if (r.status >= 200 && r.status < 300) {
+        var parseHeader = true;
+        console.log("Got Steps CSV (" + r.status + ", parseHeader=" + parseHeader + "): ");
+
+        var items = r.responseText.split("\n")
+          .map(function (line) { return line.trim(); })
+          .filter(function (line, i) { return !(parseHeader && i == 0) && line != ""; })
+          .map(function (line, i, arr) {
+            var parts = line.split(",");
+            var date = new Date(parts[0]),
+                steps = parseInt(parts[1]);
+
+            return {
+              date: date,
+              steps: steps
+            }            
+          });
+
+
+        addStepsToPlot( items );
+      }
+    }
+  };
+
+  //rssRequest.open("GET", "bed-harlanji.xml");
+
+  rssRequest.open("GET", feedUrl);
+  rssRequest.send();
+
+}
+
+
+function dateString (date) {
+//  return "d: " + date;
+
+  var str = (date.getYear() + 1900) + "-"
+              + new String(date.getMonth() + 1).padStart(2, "0") + "-"
+              + new String(date.getDate()).padStart(2, "0") + " "
+              + new String(date.getHours()).padStart(2, "0") + ":"
+              + new String(date.getMinutes()).padStart(2, "0") + ":"
+              + new String(date.getSeconds()).padStart(2, "0") + " ";
+
+  var tzOffset = date.getTimezoneOffset() / 60;
+
+  if (tzOffset >= 0) {
+    str += "+";
+  }
+
+  str += new String(tzOffset * 100).padStart(4, "0");
+           
+
+  return str;
+}
+
+
+
+
+function addStepsToPlot ( feedItems ) {
+
+  console.log("addStepsToPlot");
+
+
+  var items = feedItems.map(function (item) {
+     var startDate = startOfDay(item.date);
+     var group = groupFor("steps");
+
+     return {
+       x: startDate,
+       y: item.steps / 8, // fixme: normalize over 1440 seconds
+       group: group,
+       id: "steps-" + item.date
+     };
+  });
+
+  dataset.update( items );
+}
+
+
+function addTvToPlot ( feedItems ) {
+
+  console.log("addTvToPlot");
+
+
+  var items = feedItems.map(function (item) {
+     var startDate = startOfDay(item.date);
+     var group = item.state ? groupFor("tv-on") : groupFor("tv-off");
+
+     return {
+       x: startDate,
+       y: (item.date - startDate) / (1000 * 60),
+       group: group
+     };
+  });
+
+  dataset.add( items );
+}
+
+
+
+function splitString(string, delimiter, n) {
+    var parts = string.split(delimiter);
+    return parts.slice(0, n - 1).concat([parts.slice(n - 1).join(delimiter)]);
+}
+
+</script>
+<!--
+<script defer data-domain="cityapper.com" src="https://pa.cityapper.com/js/plausible.js"></script>
+-->
+  <script src="vis-timeline-graph2d.min.js"></script>
+  <link href="vis-timeline-graph2d.min.css" rel="stylesheet" type="text/css" />
+
+  <script src="suncalc.js"></script>
+
+  </head>
+  <body>
+
+<script>
+
+function importAuthorSubmitted( form ) {
+  console.log("submitted:");
+
+  console.log(form);
+}
+
+var authors = {
+  "HarlanJI": "https://harlanji.com/bed.xml",
+  "Marty": "https://harlanji.com/bed-marty.xml"
+}
+
+function populateAuthors () {
+  var authorsElem = document.querySelector("#author");
+
+  while(authorsElem.lastChild) {
+    authorsElem.removeChild(authorsElem.lastChild);
+  }
+
+  Object.entries(authors).forEach(function (e, i) {
+    var option = document.createElement("option");
+    option.value = e[1];
+    option.textContent = e[0];
+
+    if ( i == 0 ) {
+      option.selected = "selected";
+    }
+
+    authorsElem.appendChild(option);
+
+    console.log(e);
+  });
+
+  authorsElem.dispatchEvent(new Event('change'));
+}
+
+function startOfDay (date) {
+  var startDate = new Date(date);
+
+  startDate.setHours(0);
+  startDate.setMinutes(0);
+  startDate.setSeconds(0);
+  startDate.setMilliseconds(0);
+
+  return startDate;
+}
+
+
+
+</script>
+
+<h1>Author Made Bed</h1>
+
+
+
+<form id="import-author" action="#" onsubmit="console.log('s')">
+<p>
+Author:
+<select id="author" onchange="importRssFeed(this.options[this.selectedIndex].value)">
+</select>
+
+</p>
+</form>
+
+<div style="position: relative; height: 465px;">
+  <div style="position: absolute; top: 0; left: 0; width: 100%;">
+    <div id="visualization3" style=""></div>
+  </div>
+
+  <div style="position: absolute; top: 0; left: 0; width: 100%">
+    <div id="visualization"></div>
+  </div>
+</div>
+
+<div id="visualization2"></div>
+
+<table id="bed-makings">
+  <thead>
+    <tr>
+      <th>Date Time</th>
+      <th>#</th>
+      <th>Note</th>
+    </tr>
+  </thead>
+  <tbody>
+  </tbody>
+</table>
+
+<p>This data will sync both directions with dataset:</p>
+<textarea id="data" style="width:100%; height: 400px;">
+[
+{id: 1,
+ x: '2021-01-01',
+ end: '2021-12-31',
+ group: 2,
+ y: 750,
+ content: 'credit 1'
+ }
+]
+
+</textarea>
+<p style="color: green">
+Syntax is valid.
+</p>
+<p>
+<input name="autosync" type="checkbox"> Auto-save upon blur.
+</p>
+
+<p>
+<span><input name="format" type="radio" value="json" checked> JSON</span>
+<span><input name="format" type="radio" value="edn"> EDN</span>
+</p>
+
+<script>
+
+//importRssFeed( document.querySelector("#author").value );
+
+populateAuthors();
+initDataset();
+
+</script>
+
+  </body>
+</html>

+ 507 - 0
bedrss/day-6/index.html

@@ -0,0 +1,507 @@
+<html>
+  <head>
+
+<script>
+
+// Bed
+
+function importRssFeed (feedUrl) {
+
+  var rssRequest = new XMLHttpRequest();
+
+
+  rssRequest.onreadystatechange = function (e) {
+    var r = rssRequest;
+    if (r.readyState == XMLHttpRequest.DONE) {
+      if (r.status >= 200 && r.status < 300) {
+        console.log("Got RSS (" + r.status + "): ");
+
+
+        var items = importRSSDoc(r.responseXML);
+        populateItems( items );
+        createPlot( items );
+
+        importTvFeed( "https://harlanji.com/tv.xml" );
+
+      }
+    }
+  };
+
+  //rssRequest.open("GET", "bed-harlanji.xml");
+
+  rssRequest.open("GET", feedUrl);
+  rssRequest.send();
+
+}
+
+function importRSSDoc( rssDoc ) {
+  console.log("importRSSDoc:");
+  console.log( rssDoc );
+
+  var feedLink = rssDoc.querySelector("link").textContent.trim();
+
+  var items = Array.from( rssDoc.querySelectorAll("item") );
+  items = items.map(function (i) {
+    var item = {
+      title: i.querySelector("title").textContent.trim(),
+      date: new Date( i.querySelector("pubDate").textContent.trim() ),
+      description: i.querySelector("description").textContent.trim(),
+
+      link: i.querySelector("link").textContent.trim(),
+      feedLink: feedLink
+    };
+
+    var occurenceParts = splitString( item.description, ":", 2);
+
+
+    item.occurence = parseInt( occurenceParts[0] ) || 1;
+    item.occurenceNote = occurenceParts[1].trim();
+
+    if (item.occurenceNote == "") {
+      item.occurenceNote = "Morning routine";
+    }
+
+    return item;
+  });
+
+
+  items = items.sort(function (a, b) {
+    return b.date - a.date;
+  });
+
+  console.log("items:");
+  console.log(items);
+
+  return items;
+}
+
+
+function populateItems( items ) {
+  console.log("populateItems");
+  console.log( items );
+
+  var rows = items.map(function (item) {
+    var row = document.createElement( "tr" );
+    var date = document.createElement( "td" );
+    var occurence = document.createElement( "td" );
+    var note = document.createElement( "td" );
+
+    date.textContent = dateString( item.date );
+    occurence.textContent = item.occurence;
+    note.textContent = item.occurenceNote;
+
+    row.appendChild( date );
+    row.appendChild( occurence );
+    row.appendChild( note );
+
+    return row;
+  });
+
+  var tableBody = document.querySelector("#bed-makings tbody");
+  console.log("populate: ");
+  console.log(tableBody);
+  console.log(rows);
+
+  while (tableBody.lastChild) {
+    tableBody.removeChild(tableBody.lastChild);
+  }
+
+  rows.forEach(function (row) {
+    tableBody.appendChild(row);
+  });
+}
+
+var dataset;
+
+function initDataset () {
+  dataset = new vis.DataSet();
+}
+
+var groups = [];
+function groupFor (str) {
+  var idx = groups.indexOf(str);
+  if (idx == -1) {
+    idx = groups.length;
+    groups.push(str);
+  }
+
+  return idx;
+}
+
+var graph2d;
+
+function createPlot ( feedItems ) {
+
+  console.log("createPlot");
+
+
+
+  var container = document.getElementById('visualization');
+
+  while( container.lastChild ) {
+    container.removeChild( container.lastChild );
+  }
+
+  
+  if (feedItems.length == 0) {
+    return;
+  }
+
+  var items = feedItems.map(function (item) {
+     var startDate = startOfDay(item.date);
+
+     return {
+       x: startDate,
+       y: (item.date - startDate) / (1000 * 60),
+
+       group: groupFor(item.feedLink),
+       id: item.feedLink + item.link
+     };
+  });
+
+  // feed items are in descending order
+  var firstDate = startOfDay( feedItems[ feedItems.length - 1 ].date );
+  var lastDate = startOfDay( feedItems[ 0 ].date );
+
+  var currentFirstDate = dataset.get("firstDateSunrise");
+  if (!currentFirstDate || currentFirstDate.x > firstDate) {
+    var firstTimes = SunCalc.getTimes(firstDate, 44.986656, -93.258133)
+    items.push({id: "firstDateSunrise",
+                x: firstDate,
+                y: (firstTimes.sunrise - firstDate) / (1000 * 60),
+                group: groupFor("sunrise")});
+  }
+
+
+  var currentLastDate = dataset.get("lastDateSunrise");
+  if (!currentLastDate || currentLastDate.x < lastDate) {
+    var lastTimes = SunCalc.getTimes(lastDate, 44.986656, -93.258133)
+  
+
+    items.push({id: "lastDateSunrise",
+                x: lastDate,
+                y: (lastTimes.sunrise - lastDate) / (1000 * 60),
+                group: groupFor("sunrise")});
+  }
+
+  dataset.update(items);
+
+  var options = {
+      sort: false,
+      sampling:false,
+      style:'points',
+      dataAxis: {
+          visible: false,
+          left: {
+              range: {
+                  min: 0, max: (60 * 24)
+              }
+          }
+      },  
+      drawPoints: {
+          enabled: true,
+          size: 6,
+          style: 'circle' // square, circle
+      },
+      defaultGroup: 'Scatterplot',
+      height: '400px',
+      width: '90%'
+  };
+
+  graph2d = new vis.Graph2d(container, dataset, options);
+
+
+  // sunrise is the earliest and latest feed items
+  var windowStartDate = new Date(dataset.get("firstDateSunrise").x);
+  var windowEndDate = new Date(dataset.get("lastDateSunrise").x);
+
+  // day before first and after the latest day
+  windowStartDate.setDate( windowStartDate.getDate() - 1 );
+  windowEndDate.setDate( windowEndDate.getDate() + 1 );
+
+  graph2d.setWindow(windowStartDate, windowEndDate, {animation: false});
+
+}
+
+
+
+// TV
+
+
+
+function importTvFeed (feedUrl) {
+
+  var rssRequest = new XMLHttpRequest();
+
+
+  rssRequest.onreadystatechange = function (e) {
+    var r = rssRequest;
+    if (r.readyState == XMLHttpRequest.DONE) {
+      if (r.status >= 200 && r.status < 300) {
+        console.log("Got TV RSS (" + r.status + "): ");
+
+        var items = importTvDoc(r.responseXML);
+        addTvToPlot( items );
+      }
+    }
+  };
+
+  //rssRequest.open("GET", "bed-harlanji.xml");
+
+  rssRequest.open("GET", feedUrl);
+  rssRequest.send();
+
+}
+
+function importTvDoc( rssDoc ) {
+  console.log("importTvDoc:");
+  console.log( rssDoc );
+
+
+  var items = Array.from( rssDoc.querySelectorAll("item") );
+  items = items.map(function (i) {
+
+    var title = i.querySelector("title").textContent.trim();
+    var state = title.indexOf("on") > -1;
+
+    var item = {
+      title: title,
+      state: state,
+      date: new Date( i.querySelector("pubDate").textContent.trim() ),
+      description: i.querySelector("description").textContent.trim()
+    };
+
+    return item;
+  });
+
+  items = items.sort(function (a, b) {
+    return b.date - a.date;
+  });
+
+  console.log("tv items:");
+  console.log(items);
+
+  return items;
+}
+
+
+
+
+// Steps CSV
+
+function importStepsFeed (feedUrl) {
+
+  var rssRequest = new XMLHttpRequest();
+
+
+  rssRequest.onreadystatechange = function (e) {
+    var r = rssRequest;
+    if (r.readyState == XMLHttpRequest.DONE) {
+      if (r.status >= 200 && r.status < 300) {
+        var parseHeader = true;
+        console.log("Got Steps CSV (" + r.status + ", parseHeader=" + parseHeader + "): ");
+
+        var items = r.responseText.split("\n")
+          .map(function (line) { return line.trim(); })
+          .filter(function (line, i) { return !(parseHeader && i == 0) && line != ""; })
+          .map(function (line, i, arr) {
+            var parts = line.split(",");
+            var date = new Date(parts[0]),
+                steps = parseInt(parts[1]);
+
+            return {
+              date: date,
+              steps: steps
+            }            
+          });
+
+
+        addStepsToPlot( items );
+      }
+    }
+  };
+
+  //rssRequest.open("GET", "bed-harlanji.xml");
+
+  rssRequest.open("GET", feedUrl);
+  rssRequest.send();
+
+}
+
+
+function dateString (date) {
+//  return "d: " + date;
+
+  var str = (date.getYear() + 1900) + "-"
+              + new String(date.getMonth() + 1).padStart(2, "0") + "-"
+              + new String(date.getDate()).padStart(2, "0") + " "
+              + new String(date.getHours()).padStart(2, "0") + ":"
+              + new String(date.getMinutes()).padStart(2, "0") + ":"
+              + new String(date.getSeconds()).padStart(2, "0") + " ";
+
+  var tzOffset = date.getTimezoneOffset() / 60;
+
+  if (tzOffset >= 0) {
+    str += "+";
+  }
+
+  str += new String(tzOffset * 100).padStart(4, "0");
+           
+
+  return str;
+}
+
+
+
+
+function addStepsToPlot ( feedItems ) {
+
+  console.log("addStepsToPlot");
+
+
+  var items = feedItems.map(function (item) {
+     var startDate = startOfDay(item.date);
+     var group = 99;
+
+     return {
+       x: startDate,
+       y: item.steps / 8, // fixme: normalize over 1440 seconds
+       group: group,
+       id: "steps-" + item.date
+     };
+  });
+
+  dataset.update( items );
+}
+
+
+function addTvToPlot ( feedItems ) {
+
+  console.log("addTvToPlot");
+
+
+  var items = feedItems.map(function (item) {
+     var startDate = startOfDay(item.date);
+     var group = item.state ? 3 : 4;
+
+     return {
+       x: startDate,
+       y: (item.date - startDate) / (1000 * 60),
+       group: group
+     };
+  });
+
+  dataset.add( items );
+}
+
+
+
+function splitString(string, delimiter, n) {
+    var parts = string.split(delimiter);
+    return parts.slice(0, n - 1).concat([parts.slice(n - 1).join(delimiter)]);
+}
+
+</script>
+<!--
+<script defer data-domain="cityapper.com" src="https://pa.cityapper.com/js/plausible.js"></script>
+-->
+  <script src="vis-timeline-graph2d.min.js"></script>
+  <link href="vis-timeline-graph2d.min.css" rel="stylesheet" type="text/css" />
+
+  <script src="suncalc.js"></script>
+
+  </head>
+  <body>
+
+<script>
+
+function importAuthorSubmitted( form ) {
+  console.log("submitted:");
+
+  console.log(form);
+}
+
+var authors = {
+  "HarlanJI": "https://harlanji.com/bed.xml",
+  "Marty": "https://harlanji.com/bed-marty.xml"
+}
+
+function populateAuthors () {
+  var authorsElem = document.querySelector("#author");
+
+  while(authorsElem.lastChild) {
+    authorsElem.removeChild(authorsElem.lastChild);
+  }
+
+  Object.entries(authors).forEach(function (e, i) {
+    var option = document.createElement("option");
+    option.value = e[1];
+    option.textContent = e[0];
+
+    if ( i == 0 ) {
+      option.selected = "selected";
+    }
+
+    authorsElem.appendChild(option);
+
+    console.log(e);
+  });
+
+  authorsElem.dispatchEvent(new Event('change'));
+}
+
+function startOfDay (date) {
+  var startDate = new Date(date);
+
+  startDate.setHours(0);
+  startDate.setMinutes(0);
+  startDate.setSeconds(0);
+  startDate.setMilliseconds(0);
+
+  return startDate;
+}
+
+
+
+</script>
+
+<h1>Author Made Bed</h1>
+
+
+
+<form id="import-author" action="#" onsubmit="console.log('s')">
+<p>
+Author:
+<select id="author" onchange="importRssFeed(this.options[this.selectedIndex].value)">
+</select>
+
+</p>
+</form>
+
+<div id="visualization"></div>
+<div id="visualization2"></div>
+
+<table id="bed-makings">
+  <thead>
+    <tr>
+      <th>Date Time</th>
+      <th>#</th>
+      <th>Note</th>
+    </tr>
+  </thead>
+  <tbody>
+  </tbody>
+</table>
+
+
+<script>
+
+//importRssFeed( document.querySelector("#author").value );
+
+populateAuthors();
+initDataset();
+
+</script>
+
+  </body>
+</html>

+ 295 - 0
bedrss/index-3.html

@@ -0,0 +1,295 @@
+<html>
+  <head>
+
+<script>
+
+function importRssFeed (feedUrl) {
+
+  var rssRequest = new XMLHttpRequest();
+
+
+  rssRequest.onreadystatechange = function (e) {
+    var r = rssRequest;
+    if (r.readyState == XMLHttpRequest.DONE) {
+      if (r.status >= 200 && r.status < 300) {
+        console.log("Got RSS (" + r.status + "): ");
+
+        var items = importRSSDoc(r.responseXML);
+        populateItems( items );
+        createPlot( items );
+      }
+    }
+  };
+
+  //rssRequest.open("GET", "bed-harlanji.xml");
+
+  rssRequest.open("GET", feedUrl);
+  rssRequest.send();
+
+}
+
+function importRSSDoc( rssDoc ) {
+  console.log("importRSSDoc:");
+  console.log( rssDoc );
+
+  //var author = rssDoc.querySelector("generator").textContent.trim();
+
+  var items = Array.from( rssDoc.querySelectorAll("item") );
+  items = items.map(function (i) {
+    var item = {
+      title: i.querySelector("title").textContent.trim(),
+      date: new Date( i.querySelector("pubDate").textContent.trim() ),
+      description: i.querySelector("description").textContent.trim()
+    };
+
+    var occurenceParts = splitString( item.description, ":", 2);
+
+
+    item.occurence = parseInt( occurenceParts[0] ) || 1;
+    item.occurenceNote = occurenceParts[1].trim();
+
+    if (item.occurenceNote == "") {
+      item.occurenceNote = "Morning routine";
+    }
+
+    return item;
+  });
+
+
+  items = items.sort(function (a, b) {
+    return b.date - a.date;
+  });
+
+  console.log("items:");
+  console.log(items);
+
+  return items;
+}
+
+
+function populateItems( items ) {
+  console.log("populateItems");
+  console.log( items );
+
+  var rows = items.map(function (item) {
+    var row = document.createElement( "tr" );
+    var date = document.createElement( "td" );
+    var occurence = document.createElement( "td" );
+    var note = document.createElement( "td" );
+
+    date.textContent = dateString( item.date );
+    occurence.textContent = item.occurence;
+    note.textContent = item.occurenceNote;
+
+    row.appendChild( date );
+    row.appendChild( occurence );
+    row.appendChild( note );
+
+    return row;
+  });
+
+  var tableBody = document.querySelector("#bed-makings tbody");
+  console.log("populate: ");
+  console.log(tableBody);
+  console.log(rows);
+
+  while (tableBody.lastChild) {
+    tableBody.removeChild(tableBody.lastChild);
+  }
+
+  rows.forEach(function (row) {
+    tableBody.appendChild(row);
+  });
+}
+
+function dateString (date) {
+//  return "d: " + date;
+
+  var str = (date.getYear() + 1900) + "-"
+              + new String(date.getMonth() + 1).padStart(2, "0") + "-"
+              + new String(date.getDate()).padStart(2, "0") + " "
+              + new String(date.getHours()).padStart(2, "0") + ":"
+              + new String(date.getMinutes()).padStart(2, "0") + ":"
+              + new String(date.getSeconds()).padStart(2, "0") + " ";
+
+  var tzOffset = date.getTimezoneOffset() / 60;
+
+  if (tzOffset >= 0) {
+    str += "+";
+  }
+
+  str += new String(tzOffset * 100).padStart(4, "0");
+           
+
+  return str;
+}
+
+
+function splitString(string, delimiter, n) {
+    var parts = string.split(delimiter);
+    return parts.slice(0, n - 1).concat([parts.slice(n - 1).join(delimiter)]);
+}
+
+</script>
+
+<script defer data-domain="cityapper.com" src="https://pa.cityapper.com/js/plausible.js"></script>
+
+  <script src="vis-timeline-graph2d.min.js"></script>
+  <link href="vis-timeline-graph2d.min.css" rel="stylesheet" type="text/css" />
+
+  <script src="suncalc.js"></script>
+
+  </head>
+  <body>
+
+<script>
+
+function importAuthorSubmitted( form ) {
+  console.log("submitted:");
+
+  console.log(form);
+}
+
+var authors = {
+  "HarlanJI": "https://harlanji.com/bed.xml",
+  "Marty": "https://harlanji.com/bed-marty.xml"
+}
+
+function populateAuthors () {
+  var authorsElem = document.querySelector("#author");
+
+  while(authorsElem.lastChild) {
+    authorsElem.removeChild(authorsElem.lastChild);
+  }
+
+  Object.entries(authors).forEach(function (e, i) {
+    var option = document.createElement("option");
+    option.value = e[1];
+    option.textContent = e[0];
+
+    if ( i == 0 ) {
+      option.selected = "selected";
+    }
+
+    authorsElem.appendChild(option);
+
+    console.log(e);
+  });
+
+  authorsElem.dispatchEvent(new Event('change'));
+}
+
+function startOfDay (date) {
+  var startDate = new Date(date);
+
+  startDate.setHours(0);
+  startDate.setMinutes(0);
+  startDate.setSeconds(0);
+  startDate.setMilliseconds(0);
+
+  return startDate;
+}
+
+
+function createPlot ( feedItems ) {
+
+  console.log("createPlot");
+
+
+  var container = document.getElementById('visualization');
+
+  while( container.lastChild ) {
+    container.removeChild( container.lastChild );
+  }
+
+
+  var items = feedItems.map(function (item) {
+     var startDate = startOfDay(item.date);
+
+     return {
+       x: startDate,
+       y: (item.date - startDate) / (1000 * 60),
+       group: 0
+     };
+  });
+
+  // feed items are in descending order
+  var firstDate = startOfDay( feedItems[ feedItems.length - 1 ].date );
+  var lastDate = startOfDay( feedItems[ 0 ].date );
+
+  var firstTimes = SunCalc.getTimes(firstDate, 44.986656, -93.258133)
+  var lastTimes = SunCalc.getTimes(lastDate, 44.986656, -93.258133)
+  
+  items.push({x: firstDate,
+             y: (firstTimes.sunrise - firstDate) / (1000 * 60),
+             group: 1});
+
+  items.push({x: lastDate,
+             y: (lastTimes.sunrise - lastDate) / (1000 * 60),
+             group: 1});
+
+  var dataset = new vis.DataSet(items);
+  var options = {
+      sort: false,
+      sampling:false,
+      style:'points',
+      dataAxis: {
+          left: {
+              range: {
+                  min: 0, max: (60 * 24)
+              }
+          }
+      },
+      drawPoints: {
+          enabled: true,
+          size: 6,
+          style: 'circle' // square, circle
+      },
+      defaultGroup: 'Scatterplot',
+      height: '400px',
+      width: '90%'
+  };
+  var graph2d = new vis.Graph2d(container, dataset, options);
+
+
+}
+
+
+</script>
+
+<h1>Author Made Bed</h1>
+
+<form id="import-author" action="#" onsubmit="console.log('s')">
+<p>
+Author:
+<select id="author" onchange="importRssFeed(this.options[this.selectedIndex].value)">
+</select>
+
+</p>
+</form>
+
+<div id="visualization"></div>
+
+<table id="bed-makings">
+  <thead>
+    <tr>
+      <th>Date Time</th>
+      <th>#</th>
+      <th>Note</th>
+    </tr>
+  </thead>
+  <tbody>
+  </tbody>
+</table>
+
+
+<script>
+
+//importRssFeed( document.querySelector("#author").value );
+
+populateAuthors();
+
+</script>
+
+  </body>
+</html>

+ 397 - 0
bedrss/index-4.html

@@ -0,0 +1,397 @@
+<html>
+  <head>
+
+<script>
+
+
+// Bed
+
+function importRssFeed (feedUrl) {
+
+  var rssRequest = new XMLHttpRequest();
+
+
+  rssRequest.onreadystatechange = function (e) {
+    var r = rssRequest;
+    if (r.readyState == XMLHttpRequest.DONE) {
+      if (r.status >= 200 && r.status < 300) {
+        console.log("Got RSS (" + r.status + "): ");
+
+
+        var items = importRSSDoc(r.responseXML);
+        populateItems( items );
+        createPlot( items );
+
+        importTvFeed( "https://harlanji.com/tv.xml" );
+
+      }
+    }
+  };
+
+  //rssRequest.open("GET", "bed-harlanji.xml");
+
+  rssRequest.open("GET", feedUrl);
+  rssRequest.send();
+
+}
+
+function importRSSDoc( rssDoc ) {
+  console.log("importRSSDoc:");
+  console.log( rssDoc );
+
+  //var author = rssDoc.querySelector("generator").textContent.trim();
+
+  var items = Array.from( rssDoc.querySelectorAll("item") );
+  items = items.map(function (i) {
+    var item = {
+      title: i.querySelector("title").textContent.trim(),
+      date: new Date( i.querySelector("pubDate").textContent.trim() ),
+      description: i.querySelector("description").textContent.trim()
+    };
+
+    var occurenceParts = splitString( item.description, ":", 2);
+
+
+    item.occurence = parseInt( occurenceParts[0] ) || 1;
+    item.occurenceNote = occurenceParts[1].trim();
+
+    if (item.occurenceNote == "") {
+      item.occurenceNote = "Morning routine";
+    }
+
+    return item;
+  });
+
+
+  items = items.sort(function (a, b) {
+    return b.date - a.date;
+  });
+
+  console.log("items:");
+  console.log(items);
+
+  return items;
+}
+
+
+function populateItems( items ) {
+  console.log("populateItems");
+  console.log( items );
+
+  var rows = items.map(function (item) {
+    var row = document.createElement( "tr" );
+    var date = document.createElement( "td" );
+    var occurence = document.createElement( "td" );
+    var note = document.createElement( "td" );
+
+    date.textContent = dateString( item.date );
+    occurence.textContent = item.occurence;
+    note.textContent = item.occurenceNote;
+
+    row.appendChild( date );
+    row.appendChild( occurence );
+    row.appendChild( note );
+
+    return row;
+  });
+
+  var tableBody = document.querySelector("#bed-makings tbody");
+  console.log("populate: ");
+  console.log(tableBody);
+  console.log(rows);
+
+  while (tableBody.lastChild) {
+    tableBody.removeChild(tableBody.lastChild);
+  }
+
+  rows.forEach(function (row) {
+    tableBody.appendChild(row);
+  });
+}
+
+var dataset;
+
+function createPlot ( feedItems ) {
+
+  console.log("createPlot");
+
+
+  var container = document.getElementById('visualization');
+
+  while( container.lastChild ) {
+    container.removeChild( container.lastChild );
+  }
+
+
+  var items = feedItems.map(function (item) {
+     var startDate = startOfDay(item.date);
+
+     return {
+       x: startDate,
+       y: (item.date - startDate) / (1000 * 60),
+       group: 0
+     };
+  });
+
+  // feed items are in descending order
+  var firstDate = startOfDay( feedItems[ feedItems.length - 1 ].date );
+  var lastDate = startOfDay( feedItems[ 0 ].date );
+
+  var firstTimes = SunCalc.getTimes(firstDate, 44.986656, -93.258133)
+  var lastTimes = SunCalc.getTimes(lastDate, 44.986656, -93.258133)
+  
+  items.push({x: firstDate,
+             y: (firstTimes.sunrise - firstDate) / (1000 * 60),
+             group: 1});
+
+  items.push({x: lastDate,
+             y: (lastTimes.sunrise - lastDate) / (1000 * 60),
+             group: 1});
+
+  dataset = new vis.DataSet(items);
+
+  var options = {
+      sort: false,
+      sampling:false,
+      style:'points',
+      dataAxis: {
+          left: {
+              range: {
+                  min: 0, max: (60 * 24)
+              }
+          }
+      },
+      drawPoints: {
+          enabled: true,
+          size: 6,
+          style: 'circle' // square, circle
+      },
+      defaultGroup: 'Scatterplot',
+      height: '400px',
+      width: '90%'
+  };
+
+
+  var graph2d = new vis.Graph2d(container, dataset, options);
+
+
+  graph2d.setWindow(firstDate, startOfDay(new Date()), {animation: false});
+
+}
+
+
+
+// TV
+
+
+
+function importTvFeed (feedUrl) {
+
+  var rssRequest = new XMLHttpRequest();
+
+
+  rssRequest.onreadystatechange = function (e) {
+    var r = rssRequest;
+    if (r.readyState == XMLHttpRequest.DONE) {
+      if (r.status >= 200 && r.status < 300) {
+        console.log("Got TV RSS (" + r.status + "): ");
+
+        var items = importTvDoc(r.responseXML);
+        addTvToPlot( items );
+      }
+    }
+  };
+
+  //rssRequest.open("GET", "bed-harlanji.xml");
+
+  rssRequest.open("GET", feedUrl);
+  rssRequest.send();
+
+}
+
+function importTvDoc( rssDoc ) {
+  console.log("importTvDoc:");
+  console.log( rssDoc );
+
+
+  var items = Array.from( rssDoc.querySelectorAll("item") );
+  items = items.map(function (i) {
+
+    var title = i.querySelector("title").textContent.trim();
+    var state = title.indexOf("on") > -1;
+
+    var item = {
+      title: title,
+      state: state,
+      date: new Date( i.querySelector("pubDate").textContent.trim() ),
+      description: i.querySelector("description").textContent.trim()
+    };
+
+    return item;
+  });
+
+  items = items.sort(function (a, b) {
+    return b.date - a.date;
+  });
+
+  console.log("tv items:");
+  console.log(items);
+
+  return items;
+}
+
+
+
+function dateString (date) {
+//  return "d: " + date;
+
+  var str = (date.getYear() + 1900) + "-"
+              + new String(date.getMonth() + 1).padStart(2, "0") + "-"
+              + new String(date.getDate()).padStart(2, "0") + " "
+              + new String(date.getHours()).padStart(2, "0") + ":"
+              + new String(date.getMinutes()).padStart(2, "0") + ":"
+              + new String(date.getSeconds()).padStart(2, "0") + " ";
+
+  var tzOffset = date.getTimezoneOffset() / 60;
+
+  if (tzOffset >= 0) {
+    str += "+";
+  }
+
+  str += new String(tzOffset * 100).padStart(4, "0");
+           
+
+  return str;
+}
+
+
+
+
+function addTvToPlot ( feedItems ) {
+
+  console.log("addTvToPlot");
+
+
+  var items = feedItems.map(function (item) {
+     var startDate = startOfDay(item.date);
+     var group = item.state ? 3 : 4;
+
+     return {
+       x: startDate,
+       y: (item.date - startDate) / (1000 * 60),
+       group: group
+     };
+  });
+
+  dataset.add( items );
+}
+
+
+
+function splitString(string, delimiter, n) {
+    var parts = string.split(delimiter);
+    return parts.slice(0, n - 1).concat([parts.slice(n - 1).join(delimiter)]);
+}
+
+</script>
+
+<script defer data-domain="cityapper.com" src="https://pa.cityapper.com/js/plausible.js"></script>
+
+  <script src="vis-timeline-graph2d.min.js"></script>
+  <link href="vis-timeline-graph2d.min.css" rel="stylesheet" type="text/css" />
+
+  <script src="suncalc.js"></script>
+
+  </head>
+  <body>
+
+<script>
+
+function importAuthorSubmitted( form ) {
+  console.log("submitted:");
+
+  console.log(form);
+}
+
+var authors = {
+  "HarlanJI": "https://harlanji.com/bed.xml",
+  "Marty": "https://harlanji.com/bed-marty.xml"
+}
+
+function populateAuthors () {
+  var authorsElem = document.querySelector("#author");
+
+  while(authorsElem.lastChild) {
+    authorsElem.removeChild(authorsElem.lastChild);
+  }
+
+  Object.entries(authors).forEach(function (e, i) {
+    var option = document.createElement("option");
+    option.value = e[1];
+    option.textContent = e[0];
+
+    if ( i == 0 ) {
+      option.selected = "selected";
+    }
+
+    authorsElem.appendChild(option);
+
+    console.log(e);
+  });
+
+  authorsElem.dispatchEvent(new Event('change'));
+}
+
+function startOfDay (date) {
+  var startDate = new Date(date);
+
+  startDate.setHours(0);
+  startDate.setMinutes(0);
+  startDate.setSeconds(0);
+  startDate.setMilliseconds(0);
+
+  return startDate;
+}
+
+
+
+</script>
+
+<h1>Author Made Bed</h1>
+
+
+
+<form id="import-author" action="#" onsubmit="console.log('s')">
+<p>
+Author:
+<select id="author" onchange="importRssFeed(this.options[this.selectedIndex].value)">
+</select>
+
+</p>
+</form>
+
+<div id="visualization"></div>
+
+<table id="bed-makings">
+  <thead>
+    <tr>
+      <th>Date Time</th>
+      <th>#</th>
+      <th>Note</th>
+    </tr>
+  </thead>
+  <tbody>
+  </tbody>
+</table>
+
+
+<script>
+
+//importRssFeed( document.querySelector("#author").value );
+
+populateAuthors();
+
+</script>
+
+  </body>
+</html>

+ 393 - 0
bedrss/index-tv.html

@@ -0,0 +1,393 @@
+<html>
+  <head>
+
+<script>
+
+
+
+function importRssFeed (feedUrl) {
+
+  var rssRequest = new XMLHttpRequest();
+
+
+  rssRequest.onreadystatechange = function (e) {
+    var r = rssRequest;
+    if (r.readyState == XMLHttpRequest.DONE) {
+      if (r.status >= 200 && r.status < 300) {
+        console.log("Got RSS (" + r.status + "): ");
+
+        var items = importRSSDoc(r.responseXML);
+        populateItems( items );
+        createPlot( items );
+
+
+        importTvFeed( "https://harlanji.com/tv.xml" );
+      }
+    }
+  };
+
+  rssRequest.open("GET", feedUrl);
+  rssRequest.send();
+
+}
+
+function importRSSDoc( rssDoc ) {
+  console.log("importRSSDoc:");
+  console.log( rssDoc );
+
+  //var author = rssDoc.querySelector("generator").textContent.trim();
+
+  var items = Array.from( rssDoc.querySelectorAll("item") );
+  items = items.map(function (i) {
+    var item = {
+      title: i.querySelector("title").textContent.trim(),
+      date: new Date( i.querySelector("pubDate").textContent.trim() ),
+      description: i.querySelector("description").textContent.trim()
+    };
+
+    var occurenceParts = splitString( item.description, ":", 2);
+
+
+    item.occurence = parseInt( occurenceParts[0] ) || 1;
+    item.occurenceNote = occurenceParts[1].trim();
+
+    if (item.occurenceNote == "") {
+      item.occurenceNote = "Morning routine";
+    }
+
+    return item;
+  });
+
+
+  items = items.sort(function (a, b) {
+    return b.date - a.date;
+  });
+
+  console.log("items:");
+  console.log(items);
+
+  return items;
+}
+
+
+function populateItems( items ) {
+  console.log("populateItems");
+  console.log( items );
+
+  var rows = items.map(function (item) {
+    var row = document.createElement( "tr" );
+    var date = document.createElement( "td" );
+    var occurence = document.createElement( "td" );
+    var note = document.createElement( "td" );
+
+    date.textContent = dateString( item.date );
+    occurence.textContent = item.occurence;
+    note.textContent = item.occurenceNote;
+
+    row.appendChild( date );
+    row.appendChild( occurence );
+    row.appendChild( note );
+
+    return row;
+  });
+
+  var tableBody = document.querySelector("#bed-makings tbody");
+  console.log("populate: ");
+  console.log(tableBody);
+  console.log(rows);
+
+  while (tableBody.lastChild) {
+    tableBody.removeChild(tableBody.lastChild);
+  }
+
+  rows.forEach(function (row) {
+    tableBody.appendChild(row);
+  });
+}
+
+
+function importTvFeed (feedUrl) {
+
+  var chart = {};
+  var rssRequest = new XMLHttpRequest();
+
+  rssRequest.onreadystatechange = function (e) {
+    var r = rssRequest;
+    if (r.readyState == XMLHttpRequest.DONE) {
+      if (r.status >= 200 && r.status < 300) {
+        console.log("Got TV RSS (" + r.status + "): ");
+
+        var items = importTvDoc(r.responseXML);
+        addTvToPlot( items );
+      }
+    }
+  };
+
+  rssRequest.open("GET", feedUrl);
+  rssRequest.send();
+
+
+}
+
+
+function importTvDoc( rssDoc ) {
+  console.log("importTvDoc:");
+  console.log( rssDoc );
+
+  var items = Array.from( rssDoc.querySelectorAll("item") );
+  items = items.map(function (i) {
+    var title = i.querySelector("title").textContent.trim();
+    var state = title.indexOf("on") > -1;
+
+
+    var item = {
+      title: title,
+      state: state,
+      date: new Date( i.querySelector("pubDate").textContent.trim() ),
+      description: i.querySelector("description").textContent.trim()
+    };
+
+    return item;
+  });
+
+
+  items = items.sort(function (a, b) {
+    return b.date - a.date;
+  });
+
+  console.log("tv items:");
+  console.log(items);
+
+  return items;
+}
+
+
+
+function dateString (date) {
+//  return "d: " + date;
+
+  var str = (date.getYear() + 1900) + "-"
+              + new String(date.getMonth() + 1).padStart(2, "0") + "-"
+              + new String(date.getDate()).padStart(2, "0") + " "
+              + new String(date.getHours()).padStart(2, "0") + ":"
+              + new String(date.getMinutes()).padStart(2, "0") + ":"
+              + new String(date.getSeconds()).padStart(2, "0") + " ";
+
+  var tzOffset = date.getTimezoneOffset() / 60;
+
+  if (tzOffset >= 0) {
+    str += "+";
+  }
+
+  str += new String(tzOffset * 100).padStart(4, "0");
+           
+
+  return str;
+}
+
+
+function splitString(string, delimiter, n) {
+    var parts = string.split(delimiter);
+    return parts.slice(0, n - 1).concat([parts.slice(n - 1).join(delimiter)]);
+}
+
+</script>
+<!--
+<script defer data-domain="cityapper.com" src="https://pa.cityapper.com/js/plausible.js"></script>
+-->
+
+  <script src="vis-timeline-graph2d.min.js"></script>
+  <link href="vis-timeline-graph2d.min.css" rel="stylesheet" type="text/css" />
+
+  <script src="suncalc.js"></script>
+
+  </head>
+  <body>
+
+<script>
+
+function importAuthorSubmitted( form ) {
+  console.log("submitted:");
+
+  console.log(form);
+}
+
+var authors = {
+  "HarlanJI": "https://harlanji.com/bed.xml",
+  "Marty": "https://harlanji.com/bed-marty.xml",
+  "Dev": "bed-dev.xml"
+}
+
+function populateAuthors () {
+  var authorsElem = document.querySelector("#author");
+
+  while(authorsElem.lastChild) {
+    authorsElem.removeChild(authorsElem.lastChild);
+  }
+
+  Object.entries(authors).forEach(function (e, i) {
+    var option = document.createElement("option");
+    option.value = e[1];
+    option.textContent = e[0];
+
+    if ( e[0] == "Dev" ) {
+      option.selected = "selected";
+    }
+
+    authorsElem.appendChild(option);
+
+    console.log(e);
+  });
+
+  authorsElem.dispatchEvent(new Event('change'));
+}
+
+function startOfDay (date) {
+  var startDate = new Date(date);
+
+  startDate.setHours(0);
+  startDate.setMinutes(0);
+  startDate.setSeconds(0);
+  startDate.setMilliseconds(0);
+
+  return startDate;
+}
+
+var dataset;
+
+function createPlot ( feedItems ) {
+
+  console.log("createPlot");
+
+
+  var container = document.getElementById('visualization');
+
+  while( container.lastChild ) {
+    container.removeChild( container.lastChild );
+  }
+
+
+  var items = feedItems.map(function (item) {
+     var startDate = startOfDay(item.date);
+
+     return {
+       x: startDate,
+       y: (item.date - startDate) / (1000 * 60),
+       group: 0
+     };
+  });
+
+  // feed items are in descending order
+  var firstDate = startOfDay( feedItems[ feedItems.length - 1 ].date );
+  var lastDate = startOfDay( feedItems[ 0 ].date );
+
+  var firstTimes = SunCalc.getTimes(firstDate, 44.986656, -93.258133)
+  var lastTimes = SunCalc.getTimes(lastDate, 44.986656, -93.258133)
+  
+  items.push({x: firstDate,
+             y: (firstTimes.sunrise - firstDate) / (1000 * 60),
+             group: 1});
+
+  items.push({x: lastDate,
+             y: (lastTimes.sunrise - lastDate) / (1000 * 60),
+             group: 1});
+
+
+  items.push({x: firstDate,
+             y: (firstTimes.goldenHourEnd - firstDate) / (1000 * 60),
+             group: 2});
+
+  items.push({x: lastDate,
+             y: (lastTimes.goldenHourEnd - lastDate) / (1000 * 60),
+             group: 2});
+
+  dataset = new vis.DataSet(items);
+  var options = {
+      sort: false,
+      sampling:false,
+      style:'points',
+      dataAxis: {
+          left: {
+              range: {
+                  min: 0, max: (60 * 24)
+              }
+          }
+      },
+      drawPoints: {
+          enabled: true,
+          size: 6,
+          style: 'circle' // square, circle
+      },
+      defaultGroup: 'Scatterplot',
+      height: '400px',
+      width: '90%'
+  };
+  var graph2d = new vis.Graph2d(container, dataset, options);
+
+
+}
+
+
+function addTvToPlot ( feedItems ) {
+
+  console.log("addTvToPlot");
+
+
+  // create two lists
+  var offItems = [], onItems = [];
+  feedItems.forEach(function (item) {
+    var items = item.state ? onItems : offItems;
+    var startDate = startOfDay(item.date);
+    var group = item.state ? 4 : 3;
+
+    items.push({
+      x: startDate,
+      y: (item.date - startDate) / (1000 * 60),
+      group: group
+      
+    });
+  });
+
+  dataset.add( onItems );
+  dataset.add( offItems );
+}
+
+
+</script>
+
+<h1>Author Made Bed</h1>
+
+<form id="import-author" action="#" onsubmit="console.log('s')">
+<p>
+Author:
+<select id="author" onchange="importRssFeed(this.options[this.selectedIndex].value)">
+</select>
+
+</p>
+</form>
+
+<div id="visualization"></div>
+
+<table id="bed-makings">
+  <thead>
+    <tr>
+      <th>Date Time</th>
+      <th>#</th>
+      <th>Note</th>
+    </tr>
+  </thead>
+  <tbody>
+  </tbody>
+</table>
+
+
+<script>
+
+//importRssFeed( document.querySelector("#author").value );
+
+populateAuthors();
+
+</script>
+
+  </body>
+</html>

+ 440 - 0
bedrss/index.html

@@ -0,0 +1,440 @@
+<html>
+  <head>
+
+<script>
+
+
+// Bed
+
+function importRssFeed (feedUrl) {
+
+  var rssRequest = new XMLHttpRequest();
+
+
+  rssRequest.onreadystatechange = function (e) {
+    var r = rssRequest;
+    if (r.readyState == XMLHttpRequest.DONE) {
+      if (r.status >= 200 && r.status < 300) {
+        console.log("Got RSS (" + r.status + "): ");
+
+
+        var items = importRSSDoc(r.responseXML);
+        populateItems( items );
+        createPlot( items );
+
+        importTvFeed( "https://harlanji.com/tv.xml" );
+
+      }
+    }
+  };
+
+  //rssRequest.open("GET", "bed-harlanji.xml");
+
+  rssRequest.open("GET", feedUrl);
+  rssRequest.send();
+
+}
+
+function importRSSDoc( rssDoc ) {
+  console.log("importRSSDoc:");
+  console.log( rssDoc );
+
+  var feedLink = rssDoc.querySelector("link").textContent.trim();
+
+  var items = Array.from( rssDoc.querySelectorAll("item") );
+  items = items.map(function (i) {
+    var item = {
+      title: i.querySelector("title").textContent.trim(),
+      date: new Date( i.querySelector("pubDate").textContent.trim() ),
+      description: i.querySelector("description").textContent.trim(),
+
+      link: i.querySelector("link").textContent.trim(),
+      feedLink: feedLink
+    };
+
+    var occurenceParts = splitString( item.description, ":", 2);
+
+
+    item.occurence = parseInt( occurenceParts[0] ) || 1;
+    item.occurenceNote = occurenceParts[1].trim();
+
+    if (item.occurenceNote == "") {
+      item.occurenceNote = "Morning routine";
+    }
+
+    return item;
+  });
+
+
+  items = items.sort(function (a, b) {
+    return b.date - a.date;
+  });
+
+  console.log("items:");
+  console.log(items);
+
+  return items;
+}
+
+
+function populateItems( items ) {
+  console.log("populateItems");
+  console.log( items );
+
+  var rows = items.map(function (item) {
+    var row = document.createElement( "tr" );
+    var date = document.createElement( "td" );
+    var occurence = document.createElement( "td" );
+    var note = document.createElement( "td" );
+
+    date.textContent = dateString( item.date );
+    occurence.textContent = item.occurence;
+    note.textContent = item.occurenceNote;
+
+    row.appendChild( date );
+    row.appendChild( occurence );
+    row.appendChild( note );
+
+    return row;
+  });
+
+  var tableBody = document.querySelector("#bed-makings tbody");
+  console.log("populate: ");
+  console.log(tableBody);
+  console.log(rows);
+
+  while (tableBody.lastChild) {
+    tableBody.removeChild(tableBody.lastChild);
+  }
+
+  rows.forEach(function (row) {
+    tableBody.appendChild(row);
+  });
+}
+
+var dataset;
+
+function initDataset () {
+  dataset = new vis.DataSet();
+}
+
+var groups = [];
+function groupFor (str) {
+  var idx = groups.indexOf(str);
+  if (idx == -1) {
+    idx = groups.length;
+    groups.push(str);
+  }
+
+  return idx;
+}
+
+function createPlot ( feedItems ) {
+
+  console.log("createPlot");
+
+
+
+  var container = document.getElementById('visualization');
+
+  while( container.lastChild ) {
+    container.removeChild( container.lastChild );
+  }
+
+  
+  if (feedItems.length == 0) {
+    return;
+  }
+
+  var items = feedItems.map(function (item) {
+     var startDate = startOfDay(item.date);
+
+     return {
+       x: startDate,
+       y: (item.date - startDate) / (1000 * 60),
+
+       group: groupFor(item.feedLink),
+       id: item.feedLink + item.link
+     };
+  });
+
+  // feed items are in descending order
+  var firstDate = startOfDay( feedItems[ feedItems.length - 1 ].date );
+  var lastDate = startOfDay( feedItems[ 0 ].date );
+
+  var currentFirstDate = dataset.get("firstDateSunrise");
+  if (!currentFirstDate || currentFirstDate.x > firstDate) {
+    var firstTimes = SunCalc.getTimes(firstDate, 44.986656, -93.258133)
+    items.push({id: "firstDateSunrise",
+                x: firstDate,
+                y: (firstTimes.sunrise - firstDate) / (1000 * 60),
+                group: groupFor("sunrise")});
+  }
+
+
+  var currentLastDate = dataset.get("lastDateSunrise");
+  if (!currentLastDate || currentLastDate.x < lastDate) {
+    var lastTimes = SunCalc.getTimes(lastDate, 44.986656, -93.258133)
+  
+
+    items.push({id: "lastDateSunrise",
+                x: lastDate,
+                y: (lastTimes.sunrise - lastDate) / (1000 * 60),
+                group: groupFor("sunrise")});
+  }
+
+  dataset.update(items);
+
+  var options = {
+      sort: false,
+      sampling:false,
+      style:'points',
+      dataAxis: {
+          left: {
+              range: {
+                  min: 0, max: (60 * 24)
+              }
+          }
+      },
+      drawPoints: {
+          enabled: true,
+          size: 6,
+          style: 'circle' // square, circle
+      },
+      defaultGroup: 'Scatterplot',
+      height: '400px',
+      width: '90%'
+  };
+
+  var graph2d = new vis.Graph2d(container, dataset, options);
+
+
+  // sunrise is the earliest and latest feed items
+  var windowStartDate = new Date(dataset.get("firstDateSunrise").x);
+  var windowEndDate = new Date(dataset.get("lastDateSunrise").x);
+
+  // day before first and after the latest day
+  windowStartDate.setDate( windowStartDate.getDate() - 1 );
+  windowEndDate.setDate( windowEndDate.getDate() + 1 );
+
+  graph2d.setWindow(windowStartDate, windowEndDate, {animation: false});
+
+}
+
+
+
+// TV
+
+
+
+function importTvFeed (feedUrl) {
+
+  var rssRequest = new XMLHttpRequest();
+
+
+  rssRequest.onreadystatechange = function (e) {
+    var r = rssRequest;
+    if (r.readyState == XMLHttpRequest.DONE) {
+      if (r.status >= 200 && r.status < 300) {
+        console.log("Got TV RSS (" + r.status + "): ");
+
+        var items = importTvDoc(r.responseXML);
+        addTvToPlot( items );
+      }
+    }
+  };
+
+  //rssRequest.open("GET", "bed-harlanji.xml");
+
+  rssRequest.open("GET", feedUrl);
+  rssRequest.send();
+
+}
+
+function importTvDoc( rssDoc ) {
+  console.log("importTvDoc:");
+  console.log( rssDoc );
+
+
+  var items = Array.from( rssDoc.querySelectorAll("item") );
+  items = items.map(function (i) {
+
+    var title = i.querySelector("title").textContent.trim();
+    var state = title.indexOf("on") > -1;
+
+    var item = {
+      title: title,
+      state: state,
+      date: new Date( i.querySelector("pubDate").textContent.trim() ),
+      description: i.querySelector("description").textContent.trim()
+    };
+
+    return item;
+  });
+
+  items = items.sort(function (a, b) {
+    return b.date - a.date;
+  });
+
+  console.log("tv items:");
+  console.log(items);
+
+  return items;
+}
+
+
+
+function dateString (date) {
+//  return "d: " + date;
+
+  var str = (date.getYear() + 1900) + "-"
+              + new String(date.getMonth() + 1).padStart(2, "0") + "-"
+              + new String(date.getDate()).padStart(2, "0") + " "
+              + new String(date.getHours()).padStart(2, "0") + ":"
+              + new String(date.getMinutes()).padStart(2, "0") + ":"
+              + new String(date.getSeconds()).padStart(2, "0") + " ";
+
+  var tzOffset = date.getTimezoneOffset() / 60;
+
+  if (tzOffset >= 0) {
+    str += "+";
+  }
+
+  str += new String(tzOffset * 100).padStart(4, "0");
+           
+
+  return str;
+}
+
+
+
+
+function addTvToPlot ( feedItems ) {
+
+  console.log("addTvToPlot");
+
+
+  var items = feedItems.map(function (item) {
+     var startDate = startOfDay(item.date);
+     var group = item.state ? 3 : 4;
+
+     return {
+       x: startDate,
+       y: (item.date - startDate) / (1000 * 60),
+       group: group
+     };
+  });
+
+  dataset.add( items );
+}
+
+
+
+function splitString(string, delimiter, n) {
+    var parts = string.split(delimiter);
+    return parts.slice(0, n - 1).concat([parts.slice(n - 1).join(delimiter)]);
+}
+
+</script>
+
+<script defer data-domain="cityapper.com" src="https://pa.cityapper.com/js/plausible.js"></script>
+
+  <script src="vis-timeline-graph2d.min.js"></script>
+  <link href="vis-timeline-graph2d.min.css" rel="stylesheet" type="text/css" />
+
+  <script src="suncalc.js"></script>
+
+  </head>
+  <body>
+
+<script>
+
+function importAuthorSubmitted( form ) {
+  console.log("submitted:");
+
+  console.log(form);
+}
+
+var authors = {
+  "HarlanJI": "https://harlanji.com/bed.xml",
+  "Marty": "https://harlanji.com/bed-marty.xml"
+}
+
+function populateAuthors () {
+  var authorsElem = document.querySelector("#author");
+
+  while(authorsElem.lastChild) {
+    authorsElem.removeChild(authorsElem.lastChild);
+  }
+
+  Object.entries(authors).forEach(function (e, i) {
+    var option = document.createElement("option");
+    option.value = e[1];
+    option.textContent = e[0];
+
+    if ( i == 0 ) {
+      option.selected = "selected";
+    }
+
+    authorsElem.appendChild(option);
+
+    console.log(e);
+  });
+
+  authorsElem.dispatchEvent(new Event('change'));
+}
+
+function startOfDay (date) {
+  var startDate = new Date(date);
+
+  startDate.setHours(0);
+  startDate.setMinutes(0);
+  startDate.setSeconds(0);
+  startDate.setMilliseconds(0);
+
+  return startDate;
+}
+
+
+
+</script>
+
+<h1>Author Made Bed</h1>
+
+
+
+<form id="import-author" action="#" onsubmit="console.log('s')">
+<p>
+Author:
+<select id="author" onchange="importRssFeed(this.options[this.selectedIndex].value)">
+</select>
+
+</p>
+</form>
+
+<div id="visualization"></div>
+
+<table id="bed-makings">
+  <thead>
+    <tr>
+      <th>Date Time</th>
+      <th>#</th>
+      <th>Note</th>
+    </tr>
+  </thead>
+  <tbody>
+  </tbody>
+</table>
+
+
+<script>
+
+//importRssFeed( document.querySelector("#author").value );
+
+populateAuthors();
+initDataset();
+
+</script>
+
+  </body>
+</html>

+ 392 - 0
bedrss/possible-rounding-error/index-tv.html

@@ -0,0 +1,392 @@
+<html>
+  <head>
+
+<script>
+
+
+function importTvFeed (feedUrl) {
+
+  var chart = {};
+  var rssRequest = new XMLHttpRequest();
+
+  rssRequest.onreadystatechange = function (e) {
+    var r = rssRequest;
+    if (r.readyState == XMLHttpRequest.DONE) {
+      if (r.status >= 200 && r.status < 300) {
+        console.log("Got TV RSS (" + r.status + "): ");
+
+        var items = importTvDoc(r.responseXML);
+        addTvToPlot( items );
+      }
+    }
+  };
+
+  rssRequest.open("GET", feedUrl);
+  rssRequest.send();
+
+
+}
+
+function importRssFeed (feedUrl) {
+
+  var rssRequest = new XMLHttpRequest();
+
+
+  rssRequest.onreadystatechange = function (e) {
+    var r = rssRequest;
+    if (r.readyState == XMLHttpRequest.DONE) {
+      if (r.status >= 200 && r.status < 300) {
+        console.log("Got RSS (" + r.status + "): ");
+
+        var items = importRSSDoc(r.responseXML);
+        populateItems( items );
+        createPlot( items );
+
+
+        //importTvRss( "https://harlanji.com/tv.xml" );
+      }
+    }
+  };
+
+  rssRequest.open("GET", feedUrl);
+  rssRequest.send();
+
+}
+
+function importRSSDoc( rssDoc ) {
+  console.log("importRSSDoc:");
+  console.log( rssDoc );
+
+  //var author = rssDoc.querySelector("generator").textContent.trim();
+
+  var items = Array.from( rssDoc.querySelectorAll("item") );
+  items = items.map(function (i) {
+    var item = {
+      title: i.querySelector("title").textContent.trim(),
+      date: new Date( i.querySelector("pubDate").textContent.trim() ),
+      description: i.querySelector("description").textContent.trim()
+    };
+
+    var occurenceParts = splitString( item.description, ":", 2);
+
+
+    item.occurence = parseInt( occurenceParts[0] ) || 1;
+    item.occurenceNote = occurenceParts[1].trim();
+
+    if (item.occurenceNote == "") {
+      item.occurenceNote = "Morning routine";
+    }
+
+    return item;
+  });
+
+
+  items = items.sort(function (a, b) {
+    return b.date - a.date;
+  });
+
+  console.log("items:");
+  console.log(items);
+
+  return items;
+}
+
+
+function populateItems( items ) {
+  console.log("populateItems");
+  console.log( items );
+
+  var rows = items.map(function (item) {
+    var row = document.createElement( "tr" );
+    var date = document.createElement( "td" );
+    var occurence = document.createElement( "td" );
+    var note = document.createElement( "td" );
+
+    date.textContent = dateString( item.date );
+    occurence.textContent = item.occurence;
+    note.textContent = item.occurenceNote;
+
+    row.appendChild( date );
+    row.appendChild( occurence );
+    row.appendChild( note );
+
+    return row;
+  });
+
+  var tableBody = document.querySelector("#bed-makings tbody");
+  console.log("populate: ");
+  console.log(tableBody);
+  console.log(rows);
+
+  while (tableBody.lastChild) {
+    tableBody.removeChild(tableBody.lastChild);
+  }
+
+  rows.forEach(function (row) {
+    tableBody.appendChild(row);
+  });
+}
+
+
+
+function importTvDoc( rssDoc ) {
+  console.log("importTvDoc:");
+  console.log( rssDoc );
+
+  var items = Array.from( rssDoc.querySelectorAll("item") );
+  items = items.map(function (i) {
+    var title = i.querySelector("title").textContent.trim();
+    var state = title.indexOf("on") > -1;
+
+
+    var item = {
+      title: title,
+      state: state,
+      date: new Date( i.querySelector("pubDate").textContent.trim() ),
+      description: i.querySelector("description").textContent.trim()
+    };
+
+    return item;
+  });
+
+
+  items = items.sort(function (a, b) {
+    return b.date - a.date;
+  });
+
+  console.log("tv items:");
+  console.log(items);
+
+  return items;
+}
+
+
+
+function dateString (date) {
+//  return "d: " + date;
+
+  var str = (date.getYear() + 1900) + "-"
+              + new String(date.getMonth() + 1).padStart(2, "0") + "-"
+              + new String(date.getDate()).padStart(2, "0") + " "
+              + new String(date.getHours()).padStart(2, "0") + ":"
+              + new String(date.getMinutes()).padStart(2, "0") + ":"
+              + new String(date.getSeconds()).padStart(2, "0") + " ";
+
+  var tzOffset = date.getTimezoneOffset() / 60;
+
+  if (tzOffset >= 0) {
+    str += "+";
+  }
+
+  str += new String(tzOffset * 100).padStart(4, "0");
+           
+
+  return str;
+}
+
+
+function splitString(string, delimiter, n) {
+    var parts = string.split(delimiter);
+    return parts.slice(0, n - 1).concat([parts.slice(n - 1).join(delimiter)]);
+}
+
+</script>
+<!--
+<script defer data-domain="cityapper.com" src="https://pa.cityapper.com/js/plausible.js"></script>
+-->
+
+  <script src="vis-timeline-graph2d.min.js"></script>
+  <link href="vis-timeline-graph2d.min.css" rel="stylesheet" type="text/css" />
+
+  <script src="suncalc.js"></script>
+
+  </head>
+  <body>
+
+<script>
+
+function importAuthorSubmitted( form ) {
+  console.log("submitted:");
+
+  console.log(form);
+}
+
+var authors = {
+  "HarlanJI": "https://harlanji.com/bed.xml",
+  "Marty": "https://harlanji.com/bed-marty.xml",
+  "Dev": "bed-dev.xml"
+}
+
+function populateAuthors () {
+  var authorsElem = document.querySelector("#author");
+
+  while(authorsElem.lastChild) {
+    authorsElem.removeChild(authorsElem.lastChild);
+  }
+
+  Object.entries(authors).forEach(function (e, i) {
+    var option = document.createElement("option");
+    option.value = e[1];
+    option.textContent = e[0];
+
+    if ( e[0] == "Dev" ) {
+      option.selected = "selected";
+    }
+
+    authorsElem.appendChild(option);
+
+    console.log(e);
+  });
+
+  authorsElem.dispatchEvent(new Event('change'));
+}
+
+function startOfDay (date) {
+  var startDate = new Date(date);
+
+  startDate.setHours(0);
+  startDate.setMinutes(0);
+  startDate.setSeconds(0);
+  startDate.setMilliseconds(0);
+
+  return startDate;
+}
+
+var dataset;
+
+function createPlot ( feedItems ) {
+
+  console.log("createPlot");
+
+
+  var container = document.getElementById('visualization');
+
+  while( container.lastChild ) {
+    container.removeChild( container.lastChild );
+  }
+
+
+  var items = feedItems.map(function (item) {
+     var startDate = startOfDay(item.date);
+
+     return {
+       x: startDate,
+       y: (item.date - startDate) / (1000 * 60),
+       group: 0
+     };
+  });
+
+  // feed items are in descending order
+  var firstDate = startOfDay( feedItems[ feedItems.length - 1 ].date );
+  var lastDate = startOfDay( feedItems[ 0 ].date );
+
+  var firstTimes = SunCalc.getTimes(firstDate, 44.986656, -93.258133)
+  var lastTimes = SunCalc.getTimes(lastDate, 44.986656, -93.258133)
+  
+  items.push({x: firstDate,
+             y: (firstTimes.sunrise - firstDate) / (1000 * 60),
+             group: 1});
+
+  items.push({x: lastDate,
+             y: (lastTimes.sunrise - lastDate) / (1000 * 60),
+             group: 1});
+
+
+  items.push({x: firstDate,
+             y: (firstTimes.goldenHourEnd - firstDate) / (1000 * 60),
+             group: 2});
+
+  items.push({x: lastDate,
+             y: (lastTimes.goldenHourEnd - lastDate) / (1000 * 60),
+             group: 2});
+
+  dataset = new vis.DataSet(items);
+  var options = {
+      sort: false,
+      sampling:false,
+      style:'points',
+      dataAxis: {
+          left: {
+              range: {
+                  min: 0, max: (60 * 24)
+              }
+          }
+      },
+      drawPoints: {
+          enabled: true,
+          size: 6,
+          style: 'circle' // square, circle
+      },
+      defaultGroup: 'Scatterplot',
+      height: '400px',
+      width: '90%'
+  };
+  var graph2d = new vis.Graph2d(container, dataset, options);
+
+
+}
+
+
+function addTvToPlot ( feedItems ) {
+
+  console.log("addTvToPlot");
+
+
+  // create two lists
+  var offItems = [], onItems = [];
+  feedItems.forEach(function (item) {
+    var items = item.state ? onItems : offItems;
+    var startDate = startOfDay(item.date);
+    var group = item.state ? 4 : 3;
+
+    items.push({
+      x: startDate,
+      y: (item.date - startDate) / (1000 * 60),
+      group: group
+      
+    });
+  });
+
+  dataset.add( onItems );
+  dataset.add( offItems );
+}
+
+
+</script>
+
+<h1>Author Made Bed</h1>
+
+<form id="import-author" action="#" onsubmit="console.log('s')">
+<p>
+Author:
+<select id="author" onchange="importRssFeed(this.options[this.selectedIndex].value)">
+</select>
+
+</p>
+</form>
+
+<div id="visualization"></div>
+
+<table id="bed-makings">
+  <thead>
+    <tr>
+      <th>Date Time</th>
+      <th>#</th>
+      <th>Note</th>
+    </tr>
+  </thead>
+  <tbody>
+  </tbody>
+</table>
+
+
+<script>
+
+//importRssFeed( document.querySelector("#author").value );
+
+populateAuthors();
+
+</script>
+
+  </body>
+</html>