Better progress reporting, and add a trace=1 option.

When tracing is enabled (which is the default on the edit screen) we print a
series of grids, one for each transformation step.  That can help debug
what's going wrong with your query.
diff --git a/edit.html b/edit.html
index d49ec95..25c1ea1 100644
--- a/edit.html
+++ b/edit.html
@@ -81,7 +81,7 @@
   }
   tmo = setTimeout(function() {
     if (lastloaded != url) {
-      $('iframe#result').attr('src', url + '&editlink=0');
+      $('iframe#result').attr('src', url + '&editlink=0&trace=1');
       lastloaded = url;
     }
   }, 1000);
diff --git a/render.html b/render.html
index 978739c..6f63823 100644
--- a/render.html
+++ b/render.html
@@ -9,9 +9,48 @@
     position: absolute;
     right: 0;
     z-index: 5;
+    background: #eee;
+  }
+  div#editmenu a {
+    text-decoration: none;
+    padding: 2px;
+  }
+  div#editmenu a:hover {
+    background: #aaa;
+  }
+  #vizchart {
+    overflow: hidden;
+  }
+  #vizlog {
+    white-space: pre;
+  }
+  #vizstatus {
+    position: absolute;
+    z-index: 4;
+    width: 100%;
+    text-align: center;
+    padding-top: 1em;
+  }
+  #statussub {
+    padding-left: 1em;
+    color: #aaa;
+  }
+  .vizstep {
+    margin-top: 2em;
+    display: none;
+  }
+  .vizstep .text {
+    font-family: mono;
+    font-weight: bold;
+  }
+  .vizstep .grid {
+    margin-left: 3em;
+    max-height: 10em;
+    width: 50%;
+    overflow: auto;
   }
   </style>
-  <script src="//ajax.googleapis.com/ajax/libs/jquery/1.8.3/jquery.min.js"></script>
+  <script src="//ajax.googleapis.com/ajax/libs/jquery/1.8.3/jquery.js"></script>
   <script src="dygraph-combined.js"></script>
   <script src="https://www.google.com/jsapi"></script>
   <script>
@@ -25,8 +64,11 @@
 <body>
 
 <div id='editmenu'><a id='editlink' href='' target='_top'>edit</a></div>
+<div id='vizstatus'><div id='statustext'></div><div id='statussub'></div></div>
+<div id='vizraw'></div>
 <div id='vizchart'></div>
 <div id='viztable'></div>
+<div id='vizlog'></div>
 
 <script>
 $('#editlink').attr('href', '/edit' + window.location.search);
diff --git a/render.js b/render.js
index 1530c51..39a13a4 100644
--- a/render.js
+++ b/render.js
@@ -1,5 +1,23 @@
 "use strict";
 
+
+function err(s) {
+  $('#vizlog').append('\n' + s);
+}
+
+
+function showstatus(s, s2) {
+  $('#statustext').html(s);
+  $('#statussub').text(s2 || '');
+  if (s || s2) {
+    console.debug('status message:', s, s2);
+    $('#vizstatus').show();
+  } else {
+    $('#vizstatus').hide();
+  }
+}
+
+
 function parseArgs(query) {
   var kvlist = query.substr(1).split('&');
   var out = {};
@@ -66,6 +84,7 @@
 
 
 function guessTypes(data) {
+  console.debug('guessTypes');
   var impossible = [];
   for (var rowi in data) {
     var row = data[rowi];
@@ -109,8 +128,7 @@
 var DATE_RE1 = RegExp('^(\\d{4})[-/](\\d{1,2})(?:[-/](\\d{1,2})(?:[T\\s](\\d{1,2}):(\\d\\d)(?::(\\d\\d))?)?)?$');
 var DATE_RE2 = RegExp('^Date\\((\\d+),(\\d+),(\\d+)(?:,(\\d+),(\\d+)(?:,(\\d+))?)?\\)$');
 function myParseDate(s) {
-  var g = DATE_RE1.exec(s);
-  if (!g) g = DATE_RE2.exec(s);
+  var g = DATE_RE1.exec(s) || DATE_RE2.exec(s);
   if (g) {
     return new Date(g[1], g[2]-1, g[3] || 1,
 		    g[4] || 0, g[5] || 0, g[6] || 0);
@@ -669,6 +687,20 @@
 }
 
 
+function doLimit(ingrid, limit) {
+  limit = parseInt(limit)
+  if (ingrid.data.length > limit) {
+    return {
+        headers: ingrid.headers,
+        data: ingrid.data.slice(0, limit),
+        types: ingrid.types
+    };
+  } else {
+    return ingrid;
+  }
+}
+
+
 function fillNullsWithZero(grid) {
   for (var rowi in grid.data) {
     row = grid.data[rowi];
@@ -726,86 +758,178 @@
 }
 
 
+var _queue = [];
+
+
+function enqueue() {
+  _queue.push([].slice.apply(arguments));
+}
+
+
+function runqueue(after_each) {
+  var step = function(i) {
+    if (i < _queue.length) {
+      var el = _queue[i]
+      var text = el[0], func = el[1], args = el.slice(2);
+      showstatus('Running step ' + (+i+1) + ' of ' + _queue.length + '...',
+		 text);
+      setTimeout(function() {
+	var start = Date.now();
+	wrap(func).apply(null, args);
+	var end = Date.now();
+	if (after_each) {
+	  after_each(i + 1, _queue.length, text, end-start);
+	}
+	step(i + 1);
+      }, 0);
+    } else {
+      showstatus('');
+    }
+  }
+  step(0);
+}
+
+
 function gotData(args, gotdata) {
-  console.debug('gotdata:', gotdata);
-  var grid = gridFromData(gotdata);
-  console.debug('grid:',  grid);
+  var grid;
+  enqueue('parse', function() {
+    console.debug('gotdata:', gotdata);
+    grid = gridFromData(gotdata);
+    console.debug('grid:',  grid);
+  });
+  
+  var argi;
+  var transform = function(f, arg) {
+    enqueue(args.all[argi][0] + '=' + args.all[argi][1], function() {
+      grid = f(grid, arg);
+    });
+  };
   
   for (var argi in args.all) {
     var argkey = args.all[argi][0], argval = args.all[argi][1];
     if (argkey == 'group') {
-      grid = doGroupBy(grid, argval);
+      transform(doGroupBy, argval);
     } else if (argkey == 'treegroup') {
-      grid = doTreeGroupBy(grid, argval);
+      transform(doTreeGroupBy, argval);
     } else if (argkey == 'pivot') {
-      grid = doPivotBy(grid, argval);
+      transform(doPivotBy, argval);
     } else if (argkey == 'filter') {
-      grid = doFilterBy(grid, argval);
+      transform(doFilterBy, argval);
     } else if (argkey == 'q') {
-      grid = doQueryBy(grid, argval);
+      transform(doQueryBy, argval);
     } else if (argkey == 'limit') {
-      if (grid.data.length > argval) {
-	grid.data.length = argval;
-      }
+      transform(doLimit, argval);
     } else if (argkey == 'order') {
-      grid = doOrderBy(grid, argval);
+      transform(doOrderBy, argval);
     } else if (argkey == 'extract_regexp') {
-      grid = doExtractRegexp(grid, argval);
+      transform(doExtractRegexp, argval);
     }
   }
-  var chartops = args.get('chart');
+  
+  var chartops = args.get('chart'), trace = args.get('trace');
   var t, datatable;
-  if (chartops) {
-    //TODO(apenwarr): something needed this, but I no longer remember what.
-    //  At least line and dygraph charts are seemingly fine without it.
-    //grid = fillNullsWithZero(grid);
-    var el = document.getElementById('vizchart');
-    $(el).height(window.innerHeight).width(window.innerWidth);
-    var options = {};
-    if (args.get('title')) {
-      options.title = args.get('title');
-    }
-    if (chartops == 'stackedarea' || chartops == 'stacked') {
-      t = new google.visualization.AreaChart(el);
-      options.isStacked = true;
-    } else if (chartops == 'column') {
-      t = new google.visualization.ColumnChart(el);
-    } else if (chartops == 'bar') {
-      t = new google.visualization.BarChart(el);
-    } else if (chartops == 'line') {
-      t = new google.visualization.LineChart(el);
-    } else if (chartops == 'pie') {
-      t = new google.visualization.PieChart(el);
-    } else if (chartops == 'tree') {
-      options.maxDepth = 3;
-      options.maxPostDepth = 1;
-      options.showScale = 1;
-      t = new google.visualization.TreeMap(el);
-    } else if (chartops == 'candle' || chartops == 'candlestick') {
-      t = new google.visualization.CandlestickChart(el);
-    } else if (chartops == 'timeline') {
-      t = new google.visualization.AnnotatedTimeLine(el);
-    } else if (chartops == 'dygraph' || chartops == 'dygraph+errors') {
-      t = new Dygraph.GVizChart(el);
-      options.showRoller = true;
-      if (chartops == 'dygraph+errors') {
-	options.errorBars = true;
+  var options = {};
+  
+  enqueue('gentable', function() {
+    if (chartops) {
+      //TODO(apenwarr): something needed this, but I no longer remember what.
+      //  At least line and dygraph charts are seemingly fine without it.
+      //grid = fillNullsWithZero(grid);
+      var el = document.getElementById('vizchart');
+      $(el).height(window.innerHeight)
+	  .width(trace ? window.innerWidth - 40 : window.innerWidth);
+      if (args.get('title')) {
+	options.title = args.get('title');
       }
+      if (chartops == 'stackedarea' || chartops == 'stacked') {
+	t = new google.visualization.AreaChart(el);
+	options.isStacked = true;
+      } else if (chartops == 'column') {
+	t = new google.visualization.ColumnChart(el);
+      } else if (chartops == 'bar') {
+	t = new google.visualization.BarChart(el);
+      } else if (chartops == 'line') {
+	t = new google.visualization.LineChart(el);
+      } else if (chartops == 'pie') {
+	t = new google.visualization.PieChart(el);
+      } else if (chartops == 'tree') {
+	options.maxDepth = 3;
+	options.maxPostDepth = 1;
+	options.showScale = 1;
+	t = new google.visualization.TreeMap(el);
+      } else if (chartops == 'candle' || chartops == 'candlestick') {
+	t = new google.visualization.CandlestickChart(el);
+      } else if (chartops == 'timeline') {
+	t = new google.visualization.AnnotatedTimeLine(el);
+      } else if (chartops == 'dygraph' || chartops == 'dygraph+errors') {
+	t = new Dygraph.GVizChart(el);
+	options.showRoller = true;
+	if (chartops == 'dygraph+errors') {
+	  options.errorBars = true;
+	}
+      } else {
+	throw new Error('unknown chart type "' + chartops + '"');
+      }
+      datatable = dataToGvizTable(grid, { show_only_lastseg: true });
     } else {
-      throw new Error('unknown chart type "' + chartops + '"');
+      var el = document.getElementById('viztable');
+      t = new google.visualization.Table(el);
+      datatable = dataToGvizTable(grid);
     }
-    datatable = dataToGvizTable(grid, { show_only_lastseg: true });
+  });
+  
+  enqueue(chartops ? 'chart=' + chartops : 'view', function() {
+    t.draw(datatable, options);
+  });
+  
+  
+  if (trace) {
+    var prevdata;
+    var after_each = function(stepi, nsteps, text, msec_time) {
+      $('#vizlog').append('<div class="vizstep" id="step' + stepi + '">' +
+			  '  <div class="text"></div>' +
+			  '  <div class="grid"></div>' +
+			  '</div>');
+      $('#step' + stepi + ' .text').text('Step ' + stepi + 
+					 ' (' + msec_time + 'ms):  ' +
+					 text);
+      var viewel = $('#step' + stepi + ' .grid');
+      if (prevdata != grid.data) {
+	var t = new google.visualization.Table(viewel[0]);
+	var datatable = dataToGvizTable({
+	  headers: grid.headers,
+	  data: grid.data.slice(0, 1000),
+	  types: grid.types
+	});
+	t.draw(datatable);
+	prevdata = grid.data;
+      } else {
+	viewel.text('(unchanged)');
+      }
+      if (stepi == nsteps) {
+	$('.vizstep').show();
+      }
+    };
+    runqueue(after_each);
   } else {
-    var el = document.getElementById('viztable');
-    t = new google.visualization.Table(el);
-    datatable = dataToGvizTable(grid);
+    runqueue();
   }
-  t.draw(datatable, options);
 }
 
 
-function gotError(args, jqxhr, status) {
-  throw new Error('error getting url "' + args.get('url') + '": ' + status)
+function gotError(url, jqxhr, status) {
+  showstatus('');
+  $('#vizraw').html('<a href="' + encodeURI(url) + '">' +
+		    encodeURI(url) +
+		    '</a>');
+  throw new Error('error getting url "' + url + '": ' +
+		  status + ': ' +
+		  'visit the data page and ensure it\'s valid jsonp.');
+}
+
+
+function gotComplete() {
+  console.debug('complete', arguments);
 }
 
 
@@ -815,8 +939,8 @@
     try {
       return func.apply(null, pre_args.concat([].slice.call(arguments)));
     } catch (e) {
-      document.write(e);
-      document.write("<p><a href='/help'>here's the documentation</a>");
+      err(e);
+      err("<p><a href='/help'>here's the documentation</a>");
       throw e;
     }
   }
@@ -828,14 +952,17 @@
   var args = parseArgs(query);
   var url = args.get('url');
   if (!url) throw new Error("Missing url= in query parameter");
+  showstatus('Loading <a href="' + encodeURI(url) + '">data</a>...');
   var data = $.ajax({
     url: url,
     dataType: 'jsonp',
     jsonpCallback: 'jsonp',
     cache: true,
     success: wrap(gotData, args),
-    error: wrap(gotError, args)
+    error: wrap(gotError, url),
+    complete: wrap(gotComplete)
   });
+  $('body').append('<script>console.debug("query done");</script>');
   var editlink = args.get('editlink');
   if (editlink == 0) {
     $('#editmenu').hide();