Giant namespace fix.

Reindent the whole render.js and put it inside a function to hide the
internal implementation details and make it safer to import the javascript
into your own project.

Not too useful yet, since the render() function still makes a lot of
assumptions about your html file, so you still probably just always need to
use render.html still.
diff --git a/edit.html b/edit.html
index 2d0f03d..9be9bda 100644
--- a/edit.html
+++ b/edit.html
@@ -60,7 +60,7 @@
   for (var parti in parts) {
     var part = parts[parti];
     if (part) {
-      var bits = trySplitOne(part, '=');
+      var bits = afterquery.trySplitOne(part, '=');
       if (bits) {
         out.push(niceEncode(bits[0]) + '=' + niceEncode(bits[1]));
       } else {
@@ -90,7 +90,7 @@
 lastloaded = null;
 var exampleurl = (window.location.protocol + '//' + window.location.host +
                   '/example1.json');
-args = parseArgs(window.location.search || ('?url=' + exampleurl));
+args = afterquery.parseArgs(window.location.search || ('?url=' + exampleurl));
 argsToForm(args);
 updateUrl();
 </script>
diff --git a/render.js b/render.js
index 6998e31..9945178 100644
--- a/render.js
+++ b/render.js
@@ -1,1119 +1,1121 @@
 "use strict";
 
-
-// Mostly for konqueror compatibility
-if (!window.console) {
-  var console = {};
-  console.debug = function() {};
-}
-
-
-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();
+var afterquery = (function() {
+  // Mostly for konqueror compatibility
+  var console = window.console;
+  if (!console) {
+    console = {};
+    console.debug = function() {};
   }
-}
 
 
-function parseArgs(query) {
-  var kvlist = query.substr(1).split('&');
-  var out = {};
-  var outlist = [];
-  for (var i in kvlist) {
-    var kv = kvlist[i].split('=');
-    var key = decodeURIComponent(kv.shift());
-    var value = decodeURIComponent(kv.join('='));
-    out[key] = value;
-    outlist.push([key, value]);
+  function err(s) {
+    $('#vizlog').append('\n' + s);
   }
-  console.debug('query args:', out);
-  console.debug('query arglist:', outlist);
-  return {
-    get: function(key) { return out[key]; },
-    all: outlist
-  };
-}
 
 
-function dataToGvizTable(grid, options) {
-  if (!options) options = {};
-  var headers = grid.headers, data = grid.data, types = grid.types;
-  var dheaders = [];
-  for (var i in headers) {
-    dheaders.push({
-      id: headers[i],
-      label: headers[i],
-      type: types[i]
+  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 = {};
+    var outlist = [];
+    for (var i in kvlist) {
+      var kv = kvlist[i].split('=');
+      var key = decodeURIComponent(kv.shift());
+      var value = decodeURIComponent(kv.join('='));
+      out[key] = value;
+      outlist.push([key, value]);
+    }
+    console.debug('query args:', out);
+    console.debug('query arglist:', outlist);
+    return {
+      get: function(key) { return out[key]; },
+      all: outlist
+    };
+  }
+
+
+  function dataToGvizTable(grid, options) {
+    if (!options) options = {};
+    var headers = grid.headers, data = grid.data, types = grid.types;
+    var dheaders = [];
+    for (var i in headers) {
+      dheaders.push({
+        id: headers[i],
+        label: headers[i],
+        type: types[i]
+      });
+    }
+    var ddata = [];
+    for (var rowi in data) {
+      var row = [];
+      for (var coli in data[rowi]) {
+        var col = { v: data[rowi][coli] };
+        if (options.show_only_lastseg && col.v && col.v.split) {
+          var lastseg = col.v.split('|').pop();
+          if (lastseg != col.v) {
+            col.f = lastseg;
+          }
+        }
+        row.push(col);
+      }
+      ddata.push({c: row});
+    }
+    return new google.visualization.DataTable({
+      cols: dheaders,
+      rows: ddata
     });
   }
-  var ddata = [];
-  for (var rowi in data) {
-    var row = [];
-    for (var coli in data[rowi]) {
-      var col = { v: data[rowi][coli] };
-      if (options.show_only_lastseg && col.v && col.v.split) {
-	var lastseg = col.v.split('|').pop();
-	if (lastseg != col.v) {
-	  col.f = lastseg;
-	}
-      }
-      row.push(col);
-    }
-    ddata.push({c: row});
-  }
-  return new google.visualization.DataTable({
-    cols: dheaders,
-    rows: ddata
-  });
-}
 
 
-var CANT_NUM = 1;
-var CANT_BOOL = 2;
-var CANT_DATE = 4;
-var CANT_DATETIME = 8;
+  var CANT_NUM = 1;
+  var CANT_BOOL = 2;
+  var CANT_DATE = 4;
+  var CANT_DATETIME = 8;
 
-var T_NUM = 'number';
-var T_DATE = 'date';
-var T_DATETIME = 'datetime';
-var T_BOOL = 'boolean';
-var T_STRING = 'string';
+  var T_NUM = 'number';
+  var T_DATE = 'date';
+  var T_DATETIME = 'datetime';
+  var T_BOOL = 'boolean';
+  var T_STRING = 'string';
 
 
-function guessTypes(data) {
-  console.debug('guessTypes');
-  var impossible = [];
-  for (var rowi in data) {
-    var row = data[rowi];
-    for (var coli in row) {
-      impossible[coli] += 0;
-      var cell = row[coli];
-      if (cell == '' || cell == null) continue;
-      var d = myParseDate(cell);
-      if (isNaN(d)) {
-	impossible[coli] |= CANT_DATE | CANT_DATETIME;
-      } else if (d.getHours() || d.getMinutes() || d.getSeconds()) {
-	impossible[coli] |= CANT_DATE; // has time, so isn't a pure date
-      }
-      var f = cell * 1;
-      if (isNaN(f)) impossible[coli] |= CANT_NUM;
-      if (!(cell == 0 || cell == 1 ||
-	    cell == 'true' || cell == 'false' ||
-	    cell == true || cell == false ||
-	    cell == 'True' || cell == 'False')) impossible[coli] |= CANT_BOOL;
-    }
-  }
-  console.debug('guessTypes impossibility list:', impossible);
-  var types = [];
-  for (var coli in impossible) {
-    var imp = impossible[coli];
-    if (!(imp & CANT_BOOL)) {
-      types[coli] = T_BOOL;
-    } else if (!(imp & CANT_DATE)) {
-      types[coli] = T_DATE;
-    } else if (!(imp & CANT_DATETIME)) {
-      types[coli] = T_DATETIME;
-    } else if (!(imp & CANT_NUM)) {
-      types[coli] = T_NUM;
-    } else {
-      types[coli] = T_STRING;
-    }
-  }
-  return types;
-}
-
-
-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+)(?:,(\\d+))?)?)?\\)$');
-function myParseDate(s) {
-  if (s == null) return s;
-  if (s && s.getDate) return 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, g[7] || 0);
-  }
-  return NaN;
-}
-
-
-function parseDates(data, types) {
-  for (var coli in types) {
-    var type = types[coli];
-    if (type === T_DATE || type === T_DATETIME) {
-      for (var rowi in data) {
-	data[rowi][coli] = myParseDate(data[rowi][coli]);
+  function guessTypes(data) {
+    console.debug('guessTypes');
+    var impossible = [];
+    for (var rowi in data) {
+      var row = data[rowi];
+      for (var coli in row) {
+        impossible[coli] += 0;
+        var cell = row[coli];
+        if (cell == '' || cell == null) continue;
+        var d = myParseDate(cell);
+        if (isNaN(d)) {
+          impossible[coli] |= CANT_DATE | CANT_DATETIME;
+        } else if (d.getHours() || d.getMinutes() || d.getSeconds()) {
+          impossible[coli] |= CANT_DATE; // has time, so isn't a pure date
+        }
+        var f = cell * 1;
+        if (isNaN(f)) impossible[coli] |= CANT_NUM;
+        if (!(cell == 0 || cell == 1 ||
+              cell == 'true' || cell == 'false' ||
+              cell == true || cell == false ||
+              cell == 'True' || cell == 'False')) impossible[coli] |= CANT_BOOL;
       }
     }
-  }
-}
-
-
-function colNameToColNum(grid, colname) {
-  var keycol = colname=='*' ? 0 : grid.headers.indexOf(colname);
-  if (keycol < 0) {
-    throw new Error('unknown column name "' + key + '"');
-  }
-  return keycol;
-}
-
-
-var FUNC_RE = /^(\w+)\((.*)\)$/;
-function keyToColNum(grid, key) {
-  var g = FUNC_RE.exec(key);
-  if (g) {
-    return colNameToColNum(grid, g[2]);
-  } else {
-    return colNameToColNum(grid, key);
-  }
-}
-
-
-function _groupByLoop(ingrid, keys, initval, addcols_func, putvalues_func) {
-  var outgrid = {headers: [], data: [], types: []};
-  var keycols = [];
-  for (var keyi in keys) {
-    var colnum = keyToColNum(ingrid, keys[keyi]);
-    keycols.push(colnum);
-    outgrid.headers.push(ingrid.headers[colnum]);
-    outgrid.types.push(ingrid.types[colnum]);
-  }
-  
-  addcols_func(outgrid);
-
-  var out = {};
-  for (var rowi in ingrid.data) {
-    var row = ingrid.data[rowi];
-    var key = [];
-    for (var kcoli in keycols) {
-      key.push(row[keycols[kcoli]]);
-    }
-    var orow = out[key];
-    if (!orow) {
-      orow = [];
-      for (var keyi in keys) {
-	orow[keyi] = row[keycols[keyi]];
-      }
-      for (var i = keys.length; i < outgrid.headers.length; i++) {
-	orow[i] = initval;
-      }
-      out[key] = orow;
-      // deliberately preserve sequencing as much as possible.  The first
-      // time we see a given key is when we add it to the outgrid.
-      outgrid.data.push(orow);
-    }
-    putvalues_func(outgrid, key, orow, row);
-  }
-  return outgrid;
-}
-
-
-var agg_types = {
-  count: T_NUM,
-  sum: T_NUM
-};
-
-
-var agg_funcs = {
-  first: function(l) {
-    return l[0];
-  },
-
-  last: function(l) {
-    return l.slice(l.length-1)[0];
-  },
-  
-  only: function(l) {
-    if (l.length == 1) {
-      return l[0];
-    } else if (l.length < 1) {
-      return null;
-    } else {
-      throw new Error('cell has more than one value: only(' + l + ')')
-    }
-  },
-  
-  min: function(l) {
-    var out = null;
-    for (var i in l) {
-      if (out == null || l[i] < out) {
-	out = l[i];
-      }
-    }
-    return out;
-  },
-  
-  max: function(l) {
-    var out = null;
-    for (var i in l) {
-      if (out == null || l[i] > out) {
-	out = l[i];
-      }
-    }
-    return out;
-  },
-  
-  cat: function(l) {
-    return l.join(' ');
-  },
-  
-  count: function(l) {
-    return l.length;
-  },
-  
-  count_distinct: function(l) {
-    var a = {};
-    for (var i in l) {
-      a[l[i]] = 1;
-    }
-    var acc = 0;
-    for (var i in a) {
-      acc += 1;
-    }
-    return acc;
-  },
-
-  sum: function(l) {
-    var acc;
-    if (l.length) acc = 0;
-    for (var i in l) {
-      acc += parseFloat(l[i]);
-    }
-    return acc;
-  }
-};
-agg_funcs.count.return_type = T_NUM;
-agg_funcs.sum.return_type = T_NUM;
-
-
-function groupBy(ingrid, keys, values) {
-  // add one value column for every column listed in values.
-  var valuecols = [];
-  var valuefuncs = [];
-  var addcols_func = function(outgrid) {
-    for (var valuei in values) {
-      var g = FUNC_RE.exec(values[valuei]);
-      var field, func;
-      if (g) {
-	func = agg_funcs[g[1]];
-	if (!func) {
-	  throw new Error('unknown aggregation function "' + g[1] + '"');
-	}
-	field = g[2];
+    console.debug('guessTypes impossibility list:', impossible);
+    var types = [];
+    for (var coli in impossible) {
+      var imp = impossible[coli];
+      if (!(imp & CANT_BOOL)) {
+        types[coli] = T_BOOL;
+      } else if (!(imp & CANT_DATE)) {
+        types[coli] = T_DATE;
+      } else if (!(imp & CANT_DATETIME)) {
+        types[coli] = T_DATETIME;
+      } else if (!(imp & CANT_NUM)) {
+        types[coli] = T_NUM;
       } else {
-	func = null;
-	field = values[valuei];
-      }
-      var colnum = keyToColNum(ingrid, field);
-      console.debug('v', values[valuei], func, field);
-      if (!func) {
-	if (ingrid.types[colnum] === T_NUM) {
-	  func = agg_funcs.sum;
-	} else {
-	  func = agg_funcs.count;
-	}
-      }
-      valuecols.push(colnum);
-      valuefuncs.push(func);
-      outgrid.headers.push(field=='*' ? '_count' : ingrid.headers[colnum]);
-      outgrid.types.push(func.return_type || ingrid.types[colnum]);
-    }
-  };
-  
-  // by default, we do a count(*) operation for non-numeric value
-  // columns, and sum(*) otherwise.
-  var putvalues_func = function(outgrid, key, orow, row) {
-    for (var valuei in values) {
-      var incoli = valuecols[valuei];
-      var outcoli = key.length + parseInt(valuei);
-      var cell = row[incoli];
-      if (!orow[outcoli]) orow[outcoli] = [];
-      if (cell != null) {
-	orow[outcoli].push(cell);
+        types[coli] = T_STRING;
       }
     }
-  };
+    return types;
+  }
 
-  var outgrid = _groupByLoop(ingrid, keys, 0,
-			     addcols_func, putvalues_func);
-  
-  for (var rowi in outgrid.data) {
-    var row = outgrid.data[rowi];
-    for (var valuei in values) {
-      var outcoli = keys.length + parseInt(valuei);
-      var func = valuefuncs[valuei];
-      row[outcoli] = func(row[outcoli]);
+
+  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+)(?:,(\\d+))?)?)?\\)$');
+  function myParseDate(s) {
+    if (s == null) return s;
+    if (s && s.getDate) return 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, g[7] || 0);
+    }
+    return NaN;
+  }
+
+
+  function parseDates(data, types) {
+    for (var coli in types) {
+      var type = types[coli];
+      if (type === T_DATE || type === T_DATETIME) {
+        for (var rowi in data) {
+          data[rowi][coli] = myParseDate(data[rowi][coli]);
+        }
+      }
     }
   }
-  
-  return outgrid;
-}
 
 
-function pivotBy(ingrid, rowkeys, colkeys, valkeys) {
-  // We generate a list of value columns based on all the unique combinations
-  // of (values in colkeys)*(column names in valkeys)
-  var valuecols = {};
-  var colkey_outcols = {};
-  var colkey_incols = [];
-  for (var coli in colkeys) {
-    colkey_incols.push(keyToColNum(ingrid, colkeys[coli]));
+  function colNameToColNum(grid, colname) {
+    var keycol = colname=='*' ? 0 : grid.headers.indexOf(colname);
+    if (keycol < 0) {
+      throw new Error('unknown column name "' + key + '"');
+    }
+    return keycol;
   }
-  var addcols_func = function(outgrid) {
+
+
+  var FUNC_RE = /^(\w+)\((.*)\)$/;
+  function keyToColNum(grid, key) {
+    var g = FUNC_RE.exec(key);
+    if (g) {
+      return colNameToColNum(grid, g[2]);
+    } else {
+      return colNameToColNum(grid, key);
+    }
+  }
+
+
+  function _groupByLoop(ingrid, keys, initval, addcols_func, putvalues_func) {
+    var outgrid = {headers: [], data: [], types: []};
+    var keycols = [];
+    for (var keyi in keys) {
+      var colnum = keyToColNum(ingrid, keys[keyi]);
+      keycols.push(colnum);
+      outgrid.headers.push(ingrid.headers[colnum]);
+      outgrid.types.push(ingrid.types[colnum]);
+    }
+
+    addcols_func(outgrid);
+
+    var out = {};
     for (var rowi in ingrid.data) {
       var row = ingrid.data[rowi];
+      var key = [];
+      for (var kcoli in keycols) {
+        key.push(row[keycols[kcoli]]);
+      }
+      var orow = out[key];
+      if (!orow) {
+        orow = [];
+        for (var keyi in keys) {
+          orow[keyi] = row[keycols[keyi]];
+        }
+        for (var i = keys.length; i < outgrid.headers.length; i++) {
+          orow[i] = initval;
+        }
+        out[key] = orow;
+        // deliberately preserve sequencing as much as possible.  The first
+        // time we see a given key is when we add it to the outgrid.
+        outgrid.data.push(orow);
+      }
+      putvalues_func(outgrid, key, orow, row);
+    }
+    return outgrid;
+  }
+
+
+  var agg_types = {
+    count: T_NUM,
+    sum: T_NUM
+  };
+
+
+  var agg_funcs = {
+    first: function(l) {
+      return l[0];
+    },
+
+    last: function(l) {
+      return l.slice(l.length-1)[0];
+    },
+
+    only: function(l) {
+      if (l.length == 1) {
+        return l[0];
+      } else if (l.length < 1) {
+        return null;
+      } else {
+        throw new Error('cell has more than one value: only(' + l + ')')
+      }
+    },
+
+    min: function(l) {
+      var out = null;
+      for (var i in l) {
+        if (out == null || l[i] < out) {
+          out = l[i];
+        }
+      }
+      return out;
+    },
+
+    max: function(l) {
+      var out = null;
+      for (var i in l) {
+        if (out == null || l[i] > out) {
+          out = l[i];
+        }
+      }
+      return out;
+    },
+
+    cat: function(l) {
+      return l.join(' ');
+    },
+
+    count: function(l) {
+      return l.length;
+    },
+
+    count_distinct: function(l) {
+      var a = {};
+      for (var i in l) {
+        a[l[i]] = 1;
+      }
+      var acc = 0;
+      for (var i in a) {
+        acc += 1;
+      }
+      return acc;
+    },
+
+    sum: function(l) {
+      var acc;
+      if (l.length) acc = 0;
+      for (var i in l) {
+        acc += parseFloat(l[i]);
+      }
+      return acc;
+    }
+  };
+  agg_funcs.count.return_type = T_NUM;
+  agg_funcs.sum.return_type = T_NUM;
+
+
+  function groupBy(ingrid, keys, values) {
+    // add one value column for every column listed in values.
+    var valuecols = [];
+    var valuefuncs = [];
+    var addcols_func = function(outgrid) {
+      for (var valuei in values) {
+        var g = FUNC_RE.exec(values[valuei]);
+        var field, func;
+        if (g) {
+          func = agg_funcs[g[1]];
+          if (!func) {
+            throw new Error('unknown aggregation function "' + g[1] + '"');
+          }
+          field = g[2];
+        } else {
+          func = null;
+          field = values[valuei];
+        }
+        var colnum = keyToColNum(ingrid, field);
+        console.debug('v', values[valuei], func, field);
+        if (!func) {
+          if (ingrid.types[colnum] === T_NUM) {
+            func = agg_funcs.sum;
+          } else {
+            func = agg_funcs.count;
+          }
+        }
+        valuecols.push(colnum);
+        valuefuncs.push(func);
+        outgrid.headers.push(field=='*' ? '_count' : ingrid.headers[colnum]);
+        outgrid.types.push(func.return_type || ingrid.types[colnum]);
+      }
+    };
+
+    // by default, we do a count(*) operation for non-numeric value
+    // columns, and sum(*) otherwise.
+    var putvalues_func = function(outgrid, key, orow, row) {
+      for (var valuei in values) {
+        var incoli = valuecols[valuei];
+        var outcoli = key.length + parseInt(valuei);
+        var cell = row[incoli];
+        if (!orow[outcoli]) orow[outcoli] = [];
+        if (cell != null) {
+          orow[outcoli].push(cell);
+        }
+      }
+    };
+
+    var outgrid = _groupByLoop(ingrid, keys, 0,
+                               addcols_func, putvalues_func);
+
+    for (var rowi in outgrid.data) {
+      var row = outgrid.data[rowi];
+      for (var valuei in values) {
+        var outcoli = keys.length + parseInt(valuei);
+        var func = valuefuncs[valuei];
+        row[outcoli] = func(row[outcoli]);
+      }
+    }
+
+    return outgrid;
+  }
+
+
+  function pivotBy(ingrid, rowkeys, colkeys, valkeys) {
+    // We generate a list of value columns based on all the unique combinations
+    // of (values in colkeys)*(column names in valkeys)
+    var valuecols = {};
+    var colkey_outcols = {};
+    var colkey_incols = [];
+    for (var coli in colkeys) {
+      colkey_incols.push(keyToColNum(ingrid, colkeys[coli]));
+    }
+    var addcols_func = function(outgrid) {
+      for (var rowi in ingrid.data) {
+        var row = ingrid.data[rowi];
+        var colkey = [];
+        for (var coli in colkey_incols) {
+          var colnum = colkey_incols[coli];
+          colkey.push(row[colnum]);
+        }
+        for (var coli in valkeys) {
+          var xcolkey = colkey.concat([valkeys[coli]]);
+          if (!(xcolkey in colkey_outcols)) {
+            // if there's only one valkey (the common case), don't include the
+            // name of the old value column in the new column names; it's
+            // just clutter.
+            var name = valkeys.length>1 ? xcolkey.join(' ') : colkey.join(' ');
+            var colnum = keyToColNum(ingrid, valkeys[coli]);
+            colkey_outcols[xcolkey] = outgrid.headers.length;
+            valuecols[xcolkey] = colnum;
+            outgrid.headers.push(name);
+            outgrid.types.push(ingrid.types[colnum]);
+          }
+        }
+      }
+      console.debug('pivot colkey_outcols', colkey_outcols);
+      console.debug('pivot valuecols:', valuecols);
+    };
+
+    // by the time pivotBy is called, we're guaranteed that there's only one
+    // row with a given (rowkeys+colkeys) key, so there is only one value
+    // for each value cell.  Thus we don't need to worry about count/sum here;
+    // we just assign the values directly as we see them.
+    var putvalues_func = function(outgrid, rowkey, orow, row) {
       var colkey = [];
       for (var coli in colkey_incols) {
-	var colnum = colkey_incols[coli];
-	colkey.push(row[colnum]);
+        var colnum = colkey_incols[coli];
+        colkey.push(row[colnum]);
       }
       for (var coli in valkeys) {
-	var xcolkey = colkey.concat([valkeys[coli]]);
-	if (!(xcolkey in colkey_outcols)) {
-	  // if there's only one valkey (the common case), don't include the
-	  // name of the old value column in the new column names; it's
-	  // just clutter.
-	  var name = valkeys.length>1 ? xcolkey.join(' ') : colkey.join(' ');
-	  var colnum = keyToColNum(ingrid, valkeys[coli]);
-	  colkey_outcols[xcolkey] = outgrid.headers.length;
-	  valuecols[xcolkey] = colnum;
-	  outgrid.headers.push(name);
-	  outgrid.types.push(ingrid.types[colnum]);
-	}
+        var xcolkey = colkey.concat([valkeys[coli]]);
+        var outcolnum = colkey_outcols[xcolkey];
+        var valuecol = valuecols[xcolkey];
+        orow[outcolnum] = row[valuecol];
       }
-    }
-    console.debug('pivot colkey_outcols', colkey_outcols);
-    console.debug('pivot valuecols:', valuecols);
-  };
-  
-  // by the time pivotBy is called, we're guaranteed that there's only one
-  // row with a given (rowkeys+colkeys) key, so there is only one value
-  // for each value cell.  Thus we don't need to worry about count/sum here;
-  // we just assign the values directly as we see them.
-  var putvalues_func = function(outgrid, rowkey, orow, row) {
-    var colkey = [];
-    for (var coli in colkey_incols) {
-      var colnum = colkey_incols[coli];
-      colkey.push(row[colnum]);
-    }
-    for (var coli in valkeys) { 
-      var xcolkey = colkey.concat([valkeys[coli]]);
-      var outcolnum = colkey_outcols[xcolkey];
-      var valuecol = valuecols[xcolkey];
-      orow[outcolnum] = row[valuecol];
-    }
-  };
+    };
 
-  return _groupByLoop(ingrid, rowkeys, undefined,
-		      addcols_func, putvalues_func);
-}
-
-
-function stringifiedCols(row, types) {
-  var out = []
-  for (var coli in types) {
-    if (types[coli] === T_DATE) {
-      out.push(row[coli].strftime('%Y-%m-%d') || '');
-    } else if (types[coli] === T_DATETIME) {
-      out.push(row[coli].strftime('%Y-%m-%d %H:%M:%S') || '');
-    } else {
-      out.push((row[coli] + '') || '(none)');
-    }
+    return _groupByLoop(ingrid, rowkeys, undefined,
+                        addcols_func, putvalues_func);
   }
-  return out;
-}
 
 
-var KEY_ALL = ['ALL'];
-function treeify(ingrid, nkeys) {
-  var outgrid = {
-      headers: ['_id', '_parent'].concat(ingrid.headers.slice(nkeys)),
-      types: [T_STRING, T_STRING].concat(ingrid.types.slice(nkeys)),
-      data: []
-  };
-
-  var seen = {};
-  var missing = {};
-  
-  var add = function(key, values) {
-    var pkey = key.slice(0, key.length - 1);
-    if (!pkey.length && key != KEY_ALL) pkey = KEY_ALL;
-    outgrid.data.push([key.join('|'), pkey.join('|')].concat(values));
-    if (pkey.length && !(pkey in seen)) {
-      missing[pkey] = pkey;
-    }
-    if (key in missing) {
-      delete missing[key];
-    }
-    seen[key] = 1;
-  }
-  
-  for (var rowi in ingrid.data) {
-    var row = ingrid.data[rowi];
-    var key = row.slice(0, nkeys);
-    add(stringifiedCols(row.slice(0, nkeys),
-			ingrid.types.slice(0, nkeys)),
-	row.slice(nkeys));
-  }
-  var done = 0;
-  for (var i = 0; i < ingrid.data.length * nkeys && !done; i++) {
-    for (var missi in missing) {
-      var miss = missing[missi];
-      add(miss, []);
-      done = 0;
-      break;
-    }
-  }
-  return outgrid;
-}
-
-
-function splitNoEmpty(s, splitter) {
-  if (!s) return [];
-  return s.split(splitter);
-}
-
-
-function keysOtherThan(grid, keys) {
-  var out = [];
-  var keynames = [];
-  for (var keyi in keys) {
-    // this converts func(x) notation to just 'x'
-    keynames.push(grid.headers[keyToColNum(grid, keys[keyi])]);
-  }
-  for (var coli in grid.headers) {
-    if (keynames.indexOf(grid.headers[coli]) < 0) {
-      out.push(grid.headers[coli]);
-    }
-  }
-  return out;
-}
-
-
-function doGroupBy(grid, argval) {
-  console.debug('groupBy:', argval);
-  var parts = argval.split(';', 2);
-  var keys = splitNoEmpty(parts[0], ',');
-  var values;
-  if (parts.length >= 2) {
-    // if there's a ';' separator, the names after it are the desired
-    // value columns (and that list may be empty).
-    var tmpvalues = splitNoEmpty(parts[1], ',');
-    values = [];
-    for (var tmpi in tmpvalues) {
-      var tmpval = tmpvalues[tmpi];
-      if (tmpval == '*') {
-	values = values.concat(keysOtherThan(grid, keys.concat(values)));
+  function stringifiedCols(row, types) {
+    var out = []
+    for (var coli in types) {
+      if (types[coli] === T_DATE) {
+        out.push(row[coli].strftime('%Y-%m-%d') || '');
+      } else if (types[coli] === T_DATETIME) {
+        out.push(row[coli].strftime('%Y-%m-%d %H:%M:%S') || '');
       } else {
-	values.push(tmpval);
+        out.push((row[coli] + '') || '(none)');
       }
     }
-  } else {
-    // if there is no ';' at all, the default is to just pull in all the
-    // remaining non-key columns as values.
-    values = keysOtherThan(grid, keys);
-  }
-  console.debug('grouping by', keys, values);
-  grid = groupBy(grid, keys, values);
-  console.debug('grid:', grid);
-  return grid;
-}
-
-
-function doTreeGroupBy(grid, argval) {
-  console.debug('treeGroupBy:', argval);
-  var parts = argval.split(';', 2);
-  var keys = splitNoEmpty(parts[0], ',');
-  var values;
-  if (parts.length >= 2) {
-    // if there's a ';' separator, the names after it are the desired
-    // value columns (and that list may be empty).
-    values = splitNoEmpty(parts[1], ',');
-  } else {
-    // if there is no ';' at all, the default is to just pull in all the
-    // remaining non-key columns as values.
-    values = keysOtherThan(grid, keys);
-  }
-  console.debug('treegrouping by', keys, values);
-  grid = groupBy(grid, keys, values);
-  grid = treeify(grid, keys.length);
-  console.debug('grid:', grid);
-  return grid;
-}
-
-
-function doPivotBy(grid, argval) {
-  console.debug('pivotBy:', argval);
-  
-  // the parts are rowkeys;colkeys;values
-  var parts = argval.split(';', 3);
-  var rowkeys = splitNoEmpty(parts[0], ',');
-  var colkeys = splitNoEmpty(parts[1], ',');
-  var values;
-  if (parts.length >= 3) {
-    // if there's a second ';' separator, the names after it are the desired
-    // value columns.
-    values = splitNoEmpty(parts[2], ',');
-  } else {
-    // if there is no second ';' at all, the default is to just pull
-    // in all the remaining non-key columns as values.
-    values = keysOtherThan(grid, rowkeys.concat(colkeys));
+    return out;
   }
 
-  // first group by the rowkeys+colkeys, so there is only one row for each
-  // unique rowkeys+colkeys combination.
-  grid = groupBy(grid, rowkeys.concat(colkeys), values);
-  console.debug('tmpgrid:', grid);
 
-  // now actually do the pivot.
-  grid = pivotBy(grid, rowkeys, colkeys, values);
+  var KEY_ALL = ['ALL'];
+  function treeify(ingrid, nkeys) {
+    var outgrid = {
+        headers: ['_id', '_parent'].concat(ingrid.headers.slice(nkeys)),
+        types: [T_STRING, T_STRING].concat(ingrid.types.slice(nkeys)),
+        data: []
+    };
 
-  return grid;
-}
+    var seen = {};
+    var missing = {};
+
+    var add = function(key, values) {
+      var pkey = key.slice(0, key.length - 1);
+      if (!pkey.length && key != KEY_ALL) pkey = KEY_ALL;
+      outgrid.data.push([key.join('|'), pkey.join('|')].concat(values));
+      if (pkey.length && !(pkey in seen)) {
+        missing[pkey] = pkey;
+      }
+      if (key in missing) {
+        delete missing[key];
+      }
+      seen[key] = 1;
+    }
+
+    for (var rowi in ingrid.data) {
+      var row = ingrid.data[rowi];
+      var key = row.slice(0, nkeys);
+      add(stringifiedCols(row.slice(0, nkeys),
+                          ingrid.types.slice(0, nkeys)),
+          row.slice(nkeys));
+    }
+    var done = 0;
+    for (var i = 0; i < ingrid.data.length * nkeys && !done; i++) {
+      for (var missi in missing) {
+        var miss = missing[missi];
+        add(miss, []);
+        done = 0;
+        break;
+      }
+    }
+    return outgrid;
+  }
 
 
-function filterBy(ingrid, key, op, values) {
-  var outgrid = {headers: ingrid.headers, data: [], types: ingrid.types};
-  var keycol = keyToColNum(ingrid, key);
-  var wantvals = [];
-  for (var valuei in values) {
-    if (ingrid.types[keycol] === T_NUM) {
-      wantvals.push(parseFloat(values[valuei]));
-    } else if (ingrid.types[keycol] === T_DATE ||
-	       ingrid.types[keycol] === T_DATETIME) {
-      wantvals.push(myParseDate(values[valuei]));
+  function splitNoEmpty(s, splitter) {
+    if (!s) return [];
+    return s.split(splitter);
+  }
+
+
+  function keysOtherThan(grid, keys) {
+    var out = [];
+    var keynames = [];
+    for (var keyi in keys) {
+      // this converts func(x) notation to just 'x'
+      keynames.push(grid.headers[keyToColNum(grid, keys[keyi])]);
+    }
+    for (var coli in grid.headers) {
+      if (keynames.indexOf(grid.headers[coli]) < 0) {
+        out.push(grid.headers[coli]);
+      }
+    }
+    return out;
+  }
+
+
+  function doGroupBy(grid, argval) {
+    console.debug('groupBy:', argval);
+    var parts = argval.split(';', 2);
+    var keys = splitNoEmpty(parts[0], ',');
+    var values;
+    if (parts.length >= 2) {
+      // if there's a ';' separator, the names after it are the desired
+      // value columns (and that list may be empty).
+      var tmpvalues = splitNoEmpty(parts[1], ',');
+      values = [];
+      for (var tmpi in tmpvalues) {
+        var tmpval = tmpvalues[tmpi];
+        if (tmpval == '*') {
+          values = values.concat(keysOtherThan(grid, keys.concat(values)));
+        } else {
+          values.push(tmpval);
+        }
+      }
     } else {
-      wantvals.push(values[valuei]);
+      // if there is no ';' at all, the default is to just pull in all the
+      // remaining non-key columns as values.
+      values = keysOtherThan(grid, keys);
     }
+    console.debug('grouping by', keys, values);
+    grid = groupBy(grid, keys, values);
+    console.debug('grid:', grid);
+    return grid;
   }
 
-  for (var rowi in ingrid.data) {
-    var row = ingrid.data[rowi];
-    var cell = row[keycol];
-    var found = 0;
-    for (var valuei in wantvals) {
-      if (op == '=' && cell == wantvals[valuei]) {
-	found = 1;
-      } else if (op == '==' && cell == wantvals[valuei]) {
-	found = 1;
-      } else if (op == '>=' && cell >= wantvals[valuei]) {
-	found = 1;
-      } else if (op == '<=' && cell <= wantvals[valuei]) {
-	found = 1;
-      } else if (op == '>' && cell > wantvals[valuei]) {
-	found = 1;
-      } else if (op == '<' && cell < wantvals[valuei]) {
-	found = 1;
-      } else if (op == '!=' && cell != wantvals[valuei]) {
-	found = 1;
-      } else if (op == '<>' && cell != wantvals[valuei]) {
-	found = 1;
+
+  function doTreeGroupBy(grid, argval) {
+    console.debug('treeGroupBy:', argval);
+    var parts = argval.split(';', 2);
+    var keys = splitNoEmpty(parts[0], ',');
+    var values;
+    if (parts.length >= 2) {
+      // if there's a ';' separator, the names after it are the desired
+      // value columns (and that list may be empty).
+      values = splitNoEmpty(parts[1], ',');
+    } else {
+      // if there is no ';' at all, the default is to just pull in all the
+      // remaining non-key columns as values.
+      values = keysOtherThan(grid, keys);
+    }
+    console.debug('treegrouping by', keys, values);
+    grid = groupBy(grid, keys, values);
+    grid = treeify(grid, keys.length);
+    console.debug('grid:', grid);
+    return grid;
+  }
+
+
+  function doPivotBy(grid, argval) {
+    console.debug('pivotBy:', argval);
+
+    // the parts are rowkeys;colkeys;values
+    var parts = argval.split(';', 3);
+    var rowkeys = splitNoEmpty(parts[0], ',');
+    var colkeys = splitNoEmpty(parts[1], ',');
+    var values;
+    if (parts.length >= 3) {
+      // if there's a second ';' separator, the names after it are the desired
+      // value columns.
+      values = splitNoEmpty(parts[2], ',');
+    } else {
+      // if there is no second ';' at all, the default is to just pull
+      // in all the remaining non-key columns as values.
+      values = keysOtherThan(grid, rowkeys.concat(colkeys));
+    }
+
+    // first group by the rowkeys+colkeys, so there is only one row for each
+    // unique rowkeys+colkeys combination.
+    grid = groupBy(grid, rowkeys.concat(colkeys), values);
+    console.debug('tmpgrid:', grid);
+
+    // now actually do the pivot.
+    grid = pivotBy(grid, rowkeys, colkeys, values);
+
+    return grid;
+  }
+
+
+  function filterBy(ingrid, key, op, values) {
+    var outgrid = {headers: ingrid.headers, data: [], types: ingrid.types};
+    var keycol = keyToColNum(ingrid, key);
+    var wantvals = [];
+    for (var valuei in values) {
+      if (ingrid.types[keycol] === T_NUM) {
+        wantvals.push(parseFloat(values[valuei]));
+      } else if (ingrid.types[keycol] === T_DATE ||
+                 ingrid.types[keycol] === T_DATETIME) {
+        wantvals.push(myParseDate(values[valuei]));
+      } else {
+        wantvals.push(values[valuei]);
       }
-      if (found) break;
     }
-    if (found) outgrid.data.push(row);
+
+    for (var rowi in ingrid.data) {
+      var row = ingrid.data[rowi];
+      var cell = row[keycol];
+      var found = 0;
+      for (var valuei in wantvals) {
+        if (op == '=' && cell == wantvals[valuei]) {
+          found = 1;
+        } else if (op == '==' && cell == wantvals[valuei]) {
+          found = 1;
+        } else if (op == '>=' && cell >= wantvals[valuei]) {
+          found = 1;
+        } else if (op == '<=' && cell <= wantvals[valuei]) {
+          found = 1;
+        } else if (op == '>' && cell > wantvals[valuei]) {
+          found = 1;
+        } else if (op == '<' && cell < wantvals[valuei]) {
+          found = 1;
+        } else if (op == '!=' && cell != wantvals[valuei]) {
+          found = 1;
+        } else if (op == '<>' && cell != wantvals[valuei]) {
+          found = 1;
+        }
+        if (found) break;
+      }
+      if (found) outgrid.data.push(row);
+    }
+    return outgrid;
   }
-  return outgrid;
-}
 
 
-function trySplitOne(argval, splitstr) {
-  var pos = argval.indexOf(splitstr);
-  if (pos >= 0) {
-    return [argval.substr(0, pos).trim(),
-	    argval.substr(pos + splitstr.length).trim()];
-  } else {
-    return;
-  }
-}
-
-
-function doFilterBy(grid, argval) {
-  console.debug('filterBy:', argval);
-  var ops = ['>=', '<=', '==', '!=', '<>', '>', '<', '='];
-  var parts;
-  for (var opi in ops) {
-    var op = ops[opi];
-    if ((parts = trySplitOne(argval, op))) {
-      grid = filterBy(grid, parts[0], op, parts[1].split(','));
-      console.debug('grid:', grid);
-      return grid;
+  function trySplitOne(argval, splitstr) {
+    var pos = argval.indexOf(splitstr);
+    if (pos >= 0) {
+      return [argval.substr(0, pos).trim(),
+              argval.substr(pos + splitstr.length).trim()];
+    } else {
+      return;
     }
   }
-  throw new Error('unknown filter operation in "' + argval + '"');
-  return grid;
-}
 
 
-function queryBy(ingrid, words) {
-  var outgrid = {headers: ingrid.headers, data: [], types: ingrid.types};
-  for (var rowi in ingrid.data) {
-    var row = ingrid.data[rowi];
-    var found = 0;
-    for (var wordi in words) {
+  function doFilterBy(grid, argval) {
+    console.debug('filterBy:', argval);
+    var ops = ['>=', '<=', '==', '!=', '<>', '>', '<', '='];
+    var parts;
+    for (var opi in ops) {
+      var op = ops[opi];
+      if ((parts = trySplitOne(argval, op))) {
+        grid = filterBy(grid, parts[0], op, parts[1].split(','));
+        console.debug('grid:', grid);
+        return grid;
+      }
+    }
+    throw new Error('unknown filter operation in "' + argval + '"');
+    return grid;
+  }
+
+
+  function queryBy(ingrid, words) {
+    var outgrid = {headers: ingrid.headers, data: [], types: ingrid.types};
+    for (var rowi in ingrid.data) {
+      var row = ingrid.data[rowi];
+      var found = 0;
+      for (var wordi in words) {
+        for (var coli in row) {
+          var cell = row[coli];
+          if (cell.indexOf && cell.indexOf(words[wordi]) >= 0) {
+            found = 1;
+            break;
+          }
+        }
+        if (found) break;
+      }
+      if (found) {
+        outgrid.data.push(row);
+      }
+    }
+    return outgrid;
+  }
+
+
+  function doQueryBy(grid, argval) {
+    console.debug('queryBy:', argval);
+    grid = queryBy(grid, argval.split(','));
+    console.debug('grid:', grid);
+    return grid;
+  }
+
+
+  function orderBy(grid, keys) {
+    var keycols = [];
+    for (var keyi in keys) {
+      var key = keys[keyi];
+      var invert = 1;
+      if (key[0] == '-') {
+        invert = -1;
+        key = key.substr(1);
+      }
+      keycols.push([keyToColNum(grid, key), invert]);
+    }
+    console.debug('sort keycols', keycols);
+    var comparator = function(a, b) {
+      for (var keyi in keycols) {
+        var keycol = keycols[keyi][0], invert = keycols[keyi][1];
+        var av = a[keycol], bv = b[keycol];
+        if (grid.types[keycol] === T_NUM) {
+          av = parseFloat(av);
+          bv = parseFloat(bv);
+        }
+        if (av < bv) {
+          return -1 * invert;
+        } else if (av > bv) {
+          return 1 * invert;
+        }
+      }
+      return 0;
+    }
+    var outdata = grid.data.concat();
+    outdata.sort(comparator);
+    return { headers: grid.headers, data: outdata, types: grid.types };
+  }
+
+
+  function doOrderBy(grid, argval) {
+    console.debug('orderBy:', argval);
+    grid = orderBy(grid, argval.split(','));
+    console.debug('grid:', grid);
+    return grid;
+  }
+
+
+  function extractRegexp(grid, colname, regexp) {
+    var r = RegExp(regexp);
+    var colnum = keyToColNum(grid, colname);
+    for (var rowi in grid.data) {
+      var row = grid.data[rowi];
+      var match = r.exec(row[colnum]);
+      if (match) {
+        row[colnum] = match.slice(1).join('');
+      } else {
+        row[colnum] = '';
+      }
+    }
+    return grid;
+  }
+
+
+  function doExtractRegexp(grid, argval) {
+    console.debug('extractRegexp:', argval);
+    var parts = trySplitOne(argval, '=');
+    var colname = parts[0], regexp = parts[1];
+    grid = extractRegexp(grid, colname, regexp);
+    console.debug('grid:', grid);
+    return grid;
+  }
+
+
+  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) {
+      var row = grid.data[rowi];
       for (var coli in row) {
-	var cell = row[coli];
-	if (cell.indexOf && cell.indexOf(words[wordi]) >= 0) {
-	  found = 1;
-	  break;
-	}
-      }
-      if (found) break;
-    }
-    if (found) {
-      outgrid.data.push(row);
-    }
-  }
-  return outgrid;
-}
-
-
-function doQueryBy(grid, argval) {
-  console.debug('queryBy:', argval);
-  grid = queryBy(grid, argval.split(','));
-  console.debug('grid:', grid);
-  return grid;
-}
-
-
-function orderBy(grid, keys) {
-  var keycols = [];
-  for (var keyi in keys) {
-    var key = keys[keyi];
-    var invert = 1;
-    if (key[0] == '-') {
-      invert = -1;
-      key = key.substr(1);
-    }
-    keycols.push([keyToColNum(grid, key), invert]);
-  }
-  console.debug('sort keycols', keycols);
-  var comparator = function(a, b) {
-    for (var keyi in keycols) {
-      var keycol = keycols[keyi][0], invert = keycols[keyi][1];
-      var av = a[keycol], bv = b[keycol];
-      if (grid.types[keycol] === T_NUM) {
-	av = parseFloat(av);
-	bv = parseFloat(bv);
-      }
-      if (av < bv) {
-	return -1 * invert;
-      } else if (av > bv) {
-	return 1 * invert;
+        if (grid.types[coli] === T_NUM && row[coli] == undefined) {
+          row[coli] = 0;
+        }
       }
     }
-    return 0;
+    return grid;
   }
-  var outdata = grid.data.concat();
-  outdata.sort(comparator);
-  return { headers: grid.headers, data: outdata, types: grid.types };
-}
 
 
-function doOrderBy(grid, argval) {
-  console.debug('orderBy:', argval);
-  grid = orderBy(grid, argval.split(','));
-  console.debug('grid:', grid);
-  return grid;
-}
+  function gridFromData(gotdata) {
+    var headers, data, types;
 
+    var err;
+    if (gotdata.errors && gotdata.errors.length) {
+      err = gotdata.errors[0];
+    } else if (gotdata.error) {
+      err = gotdata.error;
+    }
+    if (err) {
+      var msglist = [];
+      if (err.message) msglist.push(err.message);
+      if (err.detailed_message) msglist.push(err.detailed_message);
+      throw new Error('Data provider returned an error: ' + msglist.join(': '));
+    }
 
-function extractRegexp(grid, colname, regexp) {
-  var r = RegExp(regexp);
-  var colnum = keyToColNum(grid, colname);
-  for (var rowi in grid.data) {
-    var row = grid.data[rowi];
-    var match = r.exec(row[colnum]);
-    if (match) {
-      row[colnum] = match.slice(1).join('');
+    if (gotdata.table) {
+      // gviz format
+      headers = [];
+      for (var headeri in gotdata.table.cols) {
+        headers.push(gotdata.table.cols[headeri].label ||
+                     gotdata.table.cols[headeri].id);
+      }
+      data = [];
+      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];
+          var g;
+          if (!col) {
+            orow.push(null);
+          } else {
+            orow.push(col.v);
+          }
+        }
+        data.push(orow);
+      }
+    } else if (gotdata.data && gotdata.cols) {
+      // eqldata.com format
+      headers = [];
+      for (var coli in gotdata.cols) {
+        var col = gotdata.cols[coli];
+        headers.push(col.caption);
+      }
+      data = gotdata.data;
     } else {
-      row[colnum] = '';
+      // assume simple [[cols...]...] (two-dimensional array) format, where
+      // the first row is the headers.
+      headers = gotdata.shift();
+      data = gotdata;
     }
+    types = guessTypes(data);
+    parseDates(data, types);
+    return {headers: headers, data: data, types: types};
   }
-  return grid;
-}
 
 
-function doExtractRegexp(grid, argval) {
-  console.debug('extractRegexp:', argval);
-  var parts = trySplitOne(argval, '=');
-  var colname = parts[0], regexp = parts[1];
-  grid = extractRegexp(grid, colname, regexp);
-  console.debug('grid:', grid);
-  return grid;
-}
+  var _queue = [];
 
 
-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
+  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) {
+    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);
+      });
     };
-  } else {
-    return ingrid;
-  }
-}
 
-
-function fillNullsWithZero(grid) {
-  for (var rowi in grid.data) {
-    var row = grid.data[rowi];
-    for (var coli in row) {
-      if (grid.types[coli] === T_NUM && row[coli] == undefined) {
-	row[coli] = 0;
+    for (var argi in args.all) {
+      var argkey = args.all[argi][0], argval = args.all[argi][1];
+      if (argkey == 'group') {
+        transform(doGroupBy, argval);
+      } else if (argkey == 'treegroup') {
+        transform(doTreeGroupBy, argval);
+      } else if (argkey == 'pivot') {
+        transform(doPivotBy, argval);
+      } else if (argkey == 'filter') {
+        transform(doFilterBy, argval);
+      } else if (argkey == 'q') {
+        transform(doQueryBy, argval);
+      } else if (argkey == 'limit') {
+        transform(doLimit, argval);
+      } else if (argkey == 'order') {
+        transform(doOrderBy, argval);
+      } else if (argkey == 'extract_regexp') {
+        transform(doExtractRegexp, argval);
       }
     }
-  }
-  return grid;
-}
 
+    var chartops = args.get('chart'), trace = args.get('trace');
+    var t, datatable;
+    var options = {};
 
-function gridFromData(gotdata) {
-  var headers, data, types;
-
-  var err;
-  if (gotdata.errors && gotdata.errors.length) {
-    err = gotdata.errors[0];
-  } else if (gotdata.error) {
-    err = gotdata.error;
-  }
-  if (err) {
-    var msglist = [];
-    if (err.message) msglist.push(err.message);
-    if (err.detailed_message) msglist.push(err.detailed_message);
-    throw new Error('Data provider returned an error: ' + msglist.join(': '));
-  }
-
-  if (gotdata.table) {
-    // gviz format
-    headers = [];
-    for (var headeri in gotdata.table.cols) {
-      headers.push(gotdata.table.cols[headeri].label ||
-		   gotdata.table.cols[headeri].id);
-    }
-    data = [];
-    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];
-	var g;
-	if (!col) {
-	  orow.push(null);
-	} else {
-	  orow.push(col.v);
-	}
-      }
-      data.push(orow);
-    }
-  } else if (gotdata.data && gotdata.cols) {
-    // eqldata.com format
-    headers = [];
-    for (var coli in gotdata.cols) {
-      var col = gotdata.cols[coli];
-      headers.push(col.caption);
-    }
-    data = gotdata.data;
-  } else {
-    // assume simple [[cols...]...] (two-dimensional array) format, where
-    // the first row is the headers.
-    headers = gotdata.shift();
-    data = gotdata;
-  }
-  types = guessTypes(data);
-  parseDates(data, types);
-  return {headers: headers, data: data, types: types};
-}
-
-
-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) {
-  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') {
-      transform(doGroupBy, argval);
-    } else if (argkey == 'treegroup') {
-      transform(doTreeGroupBy, argval);
-    } else if (argkey == 'pivot') {
-      transform(doPivotBy, argval);
-    } else if (argkey == 'filter') {
-      transform(doFilterBy, argval);
-    } else if (argkey == 'q') {
-      transform(doQueryBy, argval);
-    } else if (argkey == 'limit') {
-      transform(doLimit, argval);
-    } else if (argkey == 'order') {
-      transform(doOrderBy, argval);
-    } else if (argkey == 'extract_regexp') {
-      transform(doExtractRegexp, argval);
-    }
-  }
-  
-  var chartops = args.get('chart'), trace = args.get('trace');
-  var t, datatable;
-  var options = {};
-  
-  enqueue('gentable', function() {
-    if (chartops) {
-      if (chartops == 'stacked' || chartops == 'stackedarea') {
-	// Some charts react badly to missing values, so fill them in.
-	grid = fillNullsWithZero(grid);
-      }
-      var el = document.getElementById('vizchart');
-      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 == 'spark') {
-	// sparkline chart: get rid of everything but the data series.
-	// Looks best when small.
-	options.hAxis = {};
-	options.hAxis.baselineColor = 'none';
-	options.hAxis.textPosition = 'none';
-	options.hAxis.gridlines = {};
-	options.hAxis.gridlines.color = 'none';
-	options.vAxis = {};
-	options.vAxis.baselineColor = 'none';
-	options.vAxis.textPosition = 'none';
-	options.vAxis.gridlines = {};
-	options.vAxis.gridlines.color = 'none';
-	options.theme = 'maximized';
-	options.legend = {};
-	options.legend.position = 'none';
-	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;
-	}
+    enqueue('gentable', function() {
+      if (chartops) {
+        if (chartops == 'stacked' || chartops == 'stackedarea') {
+          // Some charts react badly to missing values, so fill them in.
+          grid = fillNullsWithZero(grid);
+        }
+        var el = document.getElementById('vizchart');
+        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 == 'spark') {
+          // sparkline chart: get rid of everything but the data series.
+          // Looks best when small.
+          options.hAxis = {};
+          options.hAxis.baselineColor = 'none';
+          options.hAxis.textPosition = 'none';
+          options.hAxis.gridlines = {};
+          options.hAxis.gridlines.color = 'none';
+          options.vAxis = {};
+          options.vAxis.baselineColor = 'none';
+          options.vAxis.textPosition = 'none';
+          options.vAxis.gridlines = {};
+          options.vAxis.gridlines.color = 'none';
+          options.theme = 'maximized';
+          options.legend = {};
+          options.legend.position = 'none';
+          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 + '"');
+        }
+        $(el).height(window.innerHeight);
+        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);
       }
-      $(el).height(window.innerHeight);
-      datatable = dataToGvizTable(grid, { show_only_lastseg: true });
+
+      var wantwidth = trace ? window.innerWidth - 40 : window.innerWidth;
+      $(el).width(wantwidth);
+
+      var dateformat = new google.visualization.DateFormat({
+        pattern: 'yyyy-MM-dd'
+      });
+      var datetimeformat = new google.visualization.DateFormat({
+        pattern: 'yyyy-MM-dd HH:mm:ss'
+      });
+      for (var coli = 0; coli < grid.types.length; coli++) {
+        if (grid.types[coli] === T_DATE) {
+          dateformat.format(datatable, coli);
+        } else if (grid.types[coli] === T_DATETIME) {
+          datetimeformat.format(datatable, coli);
+        }
+      }
+    });
+
+    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();
     }
+  }
 
-    var wantwidth = trace ? window.innerWidth - 40 : window.innerWidth;
-    $(el).width(wantwidth);
 
-    var dateformat = new google.visualization.DateFormat({
-      pattern: 'yyyy-MM-dd'
-    });
-    var datetimeformat = new google.visualization.DateFormat({
-      pattern: 'yyyy-MM-dd HH:mm:ss'
-    });
-    for (var coli = 0; coli < grid.types.length; coli++) {
-      if (grid.types[coli] === T_DATE) {
-	dateformat.format(datatable, coli);
-      } else if (grid.types[coli] === T_DATETIME) {
-	datetimeformat.format(datatable, coli);
+  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 wrap(func) {
+    var pre_args = [].slice.call(arguments, 1);
+    var f = function() {
+      try {
+        return func.apply(null, pre_args.concat([].slice.call(arguments)));
+      } catch (e) {
+        $('#vizchart').hide();
+        $('#viztable').hide();
+        $('#vizstatus').css('position', 'relative');
+        $('.vizstep').show();
+        err(e);
+        err("<p><a href='/help'>here's the documentation</a>");
+        throw e;
       }
     }
-  });
-  
-  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();
+    return f;
+  }
+
+
+  function getUrlData(url, success_func, error_func) {
+    // some services expect callback=, some expect jsonp=, so supply both
+    var plus = 'callback=jsonp&jsonp=jsonp';
+    var nurl;
+    if (url.indexOf('?') >= 0) {
+      nurl = url + '&' + plus;
+    } else {
+      nurl = url + '?' + plus;
+    }
+
+    var iframe = document.createElement('iframe');
+    iframe.style.display = 'none';
+    document.body.insertBefore(iframe, 0);
+
+    // the default jsonp callback
+    iframe.contentWindow.jsonp = success_func;
+
+    // some services are hardcoded to use the gviz callback, so supply that too
+    iframe.contentWindow.google = {
+      visualization: {
+        Query: {
+          setResponse: success_func
+        }
       }
     };
-    runqueue(after_each);
-  } else {
-    runqueue();
+
+    iframe.contentWindow.onerror = function(message, xurl, lineno) {
+      error(null, message + ' url=' + xurl + ' line=' + lineno);
+    }
+
+    iframe.contentWindow.loaded = function() {
+      alert('loaded');
+    }
+
+    iframe.contentDocument.write(
+        '<script async onerror="loaded" onload="loaded" src="' + encodeURI(url) + '"></script>');
   }
-}
 
 
-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 wrap(func) {
-  var pre_args = [].slice.call(arguments, 1);
-  var f = function() {
-    try {
-      return func.apply(null, pre_args.concat([].slice.call(arguments)));
-    } catch (e) {
-      $('#vizchart').hide();
-      $('#viztable').hide();
-      $('#vizstatus').css('position', 'relative');
-      $('.vizstep').show();
-      err(e);
-      err("<p><a href='/help'>here's the documentation</a>");
-      throw e;
+  function _run(query) {
+    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>...');
+    getUrlData(url, wrap(gotData, args), wrap(gotError, url));
+    var editlink = args.get('editlink');
+    if (editlink == 0) {
+      $('#editmenu').hide();
     }
   }
-  return f;
-}
 
-
-function getUrlData(url, success_func, error_func) {
-  // some services expect callback=, some expect jsonp=, so supply both
-  var plus = 'callback=jsonp&jsonp=jsonp';
-  var nurl;
-  if (url.indexOf('?') >= 0) {
-    nurl = url + '&' + plus;
-  } else {
-    nurl = url + '?' + plus;
+  return {
+    parseArgs: parseArgs,
+    trySplitOne: trySplitOne,
+    render: wrap(_run)
   }
-
-  var iframe = document.createElement('iframe');
-  iframe.style.display = 'none';
-  document.body.insertBefore(iframe, 0);
-
-  // the default jsonp callback
-  iframe.contentWindow.jsonp = success_func;
-
-  // some services are hardcoded to use the gviz callback, so supply that too
-  iframe.contentWindow.google = {
-    visualization: {
-      Query: {
-	setResponse: success_func
-      }
-    }
-  };
-
-  iframe.contentWindow.onerror = function(message, xurl, lineno) {
-    error(null, message + ' url=' + xurl + ' line=' + lineno);
-  }
-
-  iframe.contentWindow.loaded = function() {
-    alert('loaded');
-  }
-
-  iframe.contentDocument.write(
-      '<script async onerror="loaded" onload="loaded" src="' + encodeURI(url) + '"></script>');
-}
-
-
-function _run(query) {
-  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>...');
-  getUrlData(url, wrap(gotData, args), wrap(gotError, url));
-  var editlink = args.get('editlink');
-  if (editlink == 0) {
-    $('#editmenu').hide();
-  }
-}
-
-
-var afterquery = {
-  render: wrap(_run)
-};
+})();