Add edit support.
diff --git a/app.yaml b/app.yaml
index c6d3e07..0063637 100644
--- a/app.yaml
+++ b/app.yaml
@@ -8,6 +8,9 @@
 - url: /
   static_files: render.html
   upload: render.html
+- url: /edit
+  static_files: edit.html
+  upload: edit.html
 - url: /help
   static_files: help.html
   upload: help.html
diff --git a/edit.html b/edit.html
new file mode 100644
index 0000000..c58fd97
--- /dev/null
+++ b/edit.html
@@ -0,0 +1,103 @@
+<!doctype html>
+<html>
+<head>
+  <style>
+  iframe {
+    width: 95%;
+    height: 450px;
+    margin-left: auto;
+    margin-right: auto;
+    margin-bottom: 2em;
+  }
+  input, textarea {
+    display: block;
+    width: 95%;
+  }
+  textarea {
+    height: 8em;
+  }
+  </style>
+  <script src="//ajax.googleapis.com/ajax/libs/jquery/1.8.3/jquery.min.js"></script>
+  <script src="render.js"></script>
+</head>
+
+<body>
+
+<form>
+<input id='url' oninput='updateUrl()'>
+<textarea id='editor' wrap='off' oninput='updateUrl();'></textarea>
+</form>
+
+<div id='viewurl'><a href=''>foo</a></div>
+
+<iframe id='result'></iframe>
+
+<script>
+function argsToForm(args) {
+  var t = '';
+  for (var argi in args.all) {
+    var arg = args.all[argi];
+    var key = arg[0], value = arg[1];
+    if (key && key != 'url') {
+      t += key + '=' + value + '\n';
+    }
+  }
+  $('#editor').text(t);
+}
+
+
+function niceEncode(s) {
+  // we could use encodeURIComponent() here, but it's too fancy.  Let's
+  // *only* encode & characters so the resulting URL looks as nice as
+  // possible.
+  return s.replace('&', '%26');
+}
+
+
+function formToUrl() {
+  var dataurl = $('#url').attr('value');
+  var urlbase = window.location.protocol + '//' + window.location.host;
+  var out = ['url=' + niceEncode(dataurl)];
+  var parts = $('#editor').attr('value').trim().split('\n');
+  for (var parti in parts) {
+    var part = parts[parti];
+    if (part) {
+      var bits = trySplitOne(part, '=');
+      if (bits) {
+        out.push(niceEncode(bits[0]) + '=' + niceEncode(bits[1]));
+      } else {
+        out.push(niceEncode(part));
+      }
+    }
+  }
+  return urlbase + '?' + out.join('&');
+}
+
+
+function updateUrl() {
+  var url = formToUrl();
+  console.debug('url is', url);
+  $('#viewurl a').attr('href', url).text(url);
+  if (tmo) {
+    clearTimeout(tmo);
+  }
+  tmo = setTimeout(function() {
+    if (lastloaded != url) {
+      $('iframe#result').attr('src', url + '&editlink=0');
+      lastloaded = url;
+    }
+  }, 1000);
+}
+
+tmo = null;
+lastloaded = null;
+args = parseArgs(window.location.search);
+var exampleurl = (window.location.protocol + '//' + window.location.host +
+                  '/example1.json');
+$('#url').attr('value', args.get('url') || exampleurl);
+argsToForm(args);
+updateUrl();
+</script>
+
+</body>
+</html>
diff --git a/render.html b/render.html
index 04e8d64..73dcd89 100644
--- a/render.html
+++ b/render.html
@@ -1,6 +1,16 @@
 <!doctype html>
 <html>
 <head>
+  <style>
+  body {
+    margin: 0px;
+  }
+  div#editmenu {
+    position: absolute;
+    right: 0;
+    z-index: 5;
+  }
+  </style>
   <script src="//ajax.googleapis.com/ajax/libs/jquery/1.8.3/jquery.min.js"></script>
   <script type="text/javascript" src="https://www.google.com/jsapi"></script>
   <script>
@@ -13,10 +23,12 @@
 
 <body>
 
+<div id='editmenu'><a id='editlink' href='' target='_top'>edit</a></div>
 <div id='vizchart'></div>
 <div id='viztable'></div>
 
 <script>
+$('#editlink').attr('href', '/edit' + window.location.search);
 afterquery.render(window.location.search);
 </script>
 
diff --git a/render.js b/render.js
index 1332974..5abc885 100644
--- a/render.js
+++ b/render.js
@@ -377,7 +377,7 @@
     for (var wordi in words) {
       for (var coli in row) {
 	var cell = row[coli];
-	if (cell.indexOf(words[wordi]) >= 0) {
+	if (cell.indexOf && cell.indexOf(words[wordi]) >= 0) {
 	  found = 1;
 	  break;
 	}
@@ -491,12 +491,24 @@
 		   gotdata.table.cols[headeri].id);
     }
     data = [];
+    var re = /^Date\((\d+),(\d+),(\d+),(\d+),(\d+),(\d+)\)$/;
     for (var rowi in gotdata.table.rows) {
       var row = gotdata.table.rows[rowi];
       var orow = [];
       for (var coli in row.c) {
 	var col = row.c[coli];
-	orow.push(col.v);
+	var g;
+	if (!col) {
+	  orow.push(null);
+	} else if ((g = re.exec(col.v))) {
+	  //TODO(apenwarr): deal with date vs. time columns properly throughout
+	  var d = new Date(g[1],g[2],g[3],g[4],g[5],g[6]);
+	  orow.push((d.getYear() + 1900) + '-' +
+		    (d.getMonth() + 1) + '-' +
+		    d.getDate());
+	} else {
+	  orow.push(col.v);
+	}
       }
       data.push(orow);
     }
@@ -549,7 +561,7 @@
     grid = fillNullsWithZero(grid);
     var datatable = dataToGvizTable(grid);
     var el = document.getElementById('vizchart');
-    var options = {height:400};
+    var options = { height: 400 };
     if (args.get('title')) {
       options.title = args.get('title');
     }
@@ -564,6 +576,8 @@
       t = new google.visualization.BarChart(el);
     } else if (chartops == 'pie') {
       t = new google.visualization.PieChart(el);
+    } else if (chartops == 'candle' || chartops == 'candlestick') {
+      t = new google.visualization.CandlestickChart(el);
     } else {
       // default to a line chart if unrecognized type
       t = new google.visualization.LineChart(el);
@@ -606,6 +620,10 @@
     success: function(data, status) { return wrap(gotData, data, status); },
     error: function(data, status) { return wrap(gotError, data, status); }
   });
+  var editlink = args.get('editlink');
+  if (editlink == 0) {
+    $('#editmenu').hide();
+  }
 }