Merge remote-tracking branch 'upstream/master'
diff --git a/render.js b/render.js
index 27c37bd..28dc211 100644
--- a/render.js
+++ b/render.js
@@ -283,6 +283,28 @@
   }
 
 
+  function zpad(n) {
+    var s = '' + n;
+    if (s.length < 2) s = '0' + s;
+    return s;
+  }
+
+
+  function dateToStr(d) {
+    return (d.getFullYear() + '-' +
+            zpad(d.getMonth() + 1) + '-' +
+            zpad(d.getDate()));
+  }
+
+
+  function dateTimeToStr(d) {
+    return (dateToStr(d) + ' ' +
+            zpad(d.getHours()) + ':' +
+            zpad(d.getMinutes()) + ':' +
+            zpad(d.getSeconds()));
+  }
+
+
   function parseDates(data, types) {
     for (var coli in types) {
       var type = types[coli];
@@ -440,6 +462,37 @@
       return agg_funcs.sum(l) / agg_funcs.count_nz(l);
     },
 
+    // also works for non-numeric values, as long as they're sortable
+    median: function(l) {
+      var comparator = function(a, b) {
+        a = a || '0'; // ensure consistent ordering given NaN and undefined
+        b = b || '0';
+        if (a < b) {
+          return -1;
+        } else if (a > b) {
+          return 1;
+        } else {
+          return 0;
+        }
+      }
+      if (l.length > 0) {
+        l.sort(comparator);
+        return l[parseInt(l.length/2)];
+      } else {
+        return null;
+      }
+    },
+
+    stddev: function(l) {
+      var avg = agg_funcs.avg(l);
+      var sumsq = 0.0;
+      for (var i in l) {
+        var d = parseFloat(l[i]) - avg;
+        if (d) sumsq += d * d;
+      }
+      return Math.sqrt(sumsq);
+    },
+
     color: function(l) {
       for (var i in l) {
         var v = l[i];
@@ -455,6 +508,7 @@
   agg_funcs.count_distinct.return_type = T_NUM;
   agg_funcs.sum.return_type = T_NUM;
   agg_funcs.avg.return_type = T_NUM;
+  agg_funcs.stddev.return_type = T_NUM;
   agg_funcs.cat.return_type = T_STRING;
   agg_funcs.color.return_type = T_NUM;
 
@@ -538,7 +592,7 @@
         var colkey = [];
         for (var coli in colkey_incols) {
           var colnum = colkey_incols[coli];
-          colkey.push(row[colnum]);
+          colkey.push(stringifiedCol(row[colnum], ingrid.types[colnum]));
         }
         for (var coli in valkeys) {
           var xcolkey = colkey.concat([valkeys[coli]]);
@@ -548,7 +602,7 @@
             // just clutter.
             var name = valkeys.length > 1 ?
                 xcolkey.join(' ') : colkey.join(' ');
-            var colnum = keyToColNum(ingrid, valkeys[coli]);
+            var colnum = rowkeys.length + colkeys.length + parseInt(coli);
             colkey_outcols[xcolkey] = outgrid.headers.length;
             valuecols[xcolkey] = colnum;
             outgrid.headers.push(name);
@@ -568,7 +622,7 @@
       var colkey = [];
       for (var coli in colkey_incols) {
         var colnum = colkey_incols[coli];
-        colkey.push(row[colnum]);
+        colkey.push(stringifiedCol(row[colnum], ingrid.types[colnum]));
       }
       for (var coli in valkeys) {
         var xcolkey = colkey.concat([valkeys[coli]]);
@@ -583,16 +637,21 @@
   }
 
 
+  function stringifiedCol(value, typ) {
+    if (typ === T_DATE) {
+      return dateToStr(value) || '';
+    } else if (typ === T_DATETIME) {
+      return dateTimeToStr(value) || '';
+    } else {
+      return (value + '') || '(none)';
+    }
+  }
+
+
   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)');
-      }
+      out.push(stringifiedCol(row[coli], types[coli]));
     }
     return out;
   }
@@ -1412,7 +1471,7 @@
           $(el).height(window.innerHeight);
           options.height = window.innerHeight;
           t.draw(datatable, options);
-        }
+        };
         doRender();
         $(window).resize(function() {
           clearTimeout(resizeTimer);
@@ -1675,6 +1734,7 @@
       trySplitOne: trySplitOne,
       dataToGvizTable: dataToGvizTable,
       guessTypes: guessTypes,
+      myParseDate: myParseDate,
       groupBy: groupBy,
       pivotBy: pivotBy,
       stringifiedCols: stringifiedCols,
@@ -1691,6 +1751,11 @@
       runqueue: runqueue,
       gridFromData: gridFromData
     },
+    T_NUM: T_NUM,
+    T_DATE: T_DATE,
+    T_DATETIME: T_DATETIME,
+    T_BOOL: T_BOOL,
+    T_STRING: T_STRING,
     parseArgs: parseArgs,
     exec: exec,
     render: wrap(render)
diff --git a/t/trender.js b/t/trender.js
index ba09458..e65ff81 100644
--- a/t/trender.js
+++ b/t/trender.js
@@ -239,3 +239,78 @@
     WVPASSEQ(grid.data, [[1, 1, 2]]);
   });
 });
+
+wvtest('pivot', function() {
+  var rawdata = [
+    ['a', 'b', 'c'],
+    ['fred', 9, '2013/01/02'],
+    ['bob', 7, '2013/01/01'],
+    ['fred', 11, '2013/02/03']
+  ];
+  var mpd = afterquery.internal.myParseDate;
+  var dlist = [mpd('2013/01/02'), mpd('2013/01/01'), mpd('2013/02/03')];
+  afterquery.exec('group=a,b;only(c),count(c),sum(c),min(c),max(c),' +
+                  'avg(c),median(c),stddev(c)', rawdata, function(grid) {
+    WVPASSEQ(grid.headers, ['a', 'b', 'c', 'c', 'c', 'c', 'c', 'c', 'c', 'c']);
+    WVPASSEQ(grid.data, [
+      ['fred', 9, dlist[0], 1, 0, dlist[0], dlist[0], 0, dlist[0], 0],
+      ['bob', 7, dlist[1], 1, 0, dlist[1], dlist[1], 0, dlist[1], 0],
+      ['fred', 11, dlist[2], 1, 0, dlist[2], dlist[2], 0, dlist[2], 0]
+    ]);
+  });
+  afterquery.exec('group=;count(b),sum(b),min(b),max(b),' +
+                  'avg(b),median(b),stddev(b)', rawdata, function(grid) {
+    WVPASSEQ(grid.headers, ['b', 'b', 'b', 'b', 'b', 'b', 'b']);
+    WVPASSEQ(grid.data, [[3, 27, 7, 11, 27.0/3.0, 9, Math.sqrt(8)]]);
+  });
+  afterquery.exec('pivot=a;b;only(c)', rawdata, function(grid) {
+    WVPASSEQ(grid.headers, ['a', 9, 7, 11]);
+    WVPASSEQ(grid.types, [
+      afterquery.T_STRING,
+      afterquery.T_DATE,
+      afterquery.T_DATE,
+      afterquery.T_DATE
+    ]);
+    WVPASSEQ(grid.data, [
+      ['fred', dlist[0], null, dlist[2]],
+      ['bob', null, dlist[1], null]
+    ]);
+  });
+  afterquery.exec('pivot=a;b;c', rawdata, function(grid) {
+    WVPASSEQ(grid.headers, ['a', 9, 7, 11]);
+    WVPASSEQ(grid.types, [
+      afterquery.T_STRING,
+      afterquery.T_NUM,
+      afterquery.T_NUM,
+      afterquery.T_NUM
+    ]);
+    WVPASSEQ(grid.data, [
+      ['fred', 1, null, 1],
+      ['bob', null, 1, null]
+    ]);
+  });
+  afterquery.exec('pivot=a;b;only(c),count(c)', rawdata, function(grid) {
+    WVPASSEQ(grid.headers, [
+      'a',
+      '9 only(c)', '9 count(c)',
+      '7 only(c)', '7 count(c)',
+      '11 only(c)', '11 count(c)'
+    ]);
+    WVPASSEQ(grid.data, [
+      ['fred', dlist[0], 1, null, null, dlist[2], 1],
+      ['bob', null, null, dlist[1], 1, null, null]
+    ]);
+  });
+  afterquery.exec('pivot=a;b,c;count(*)', rawdata, function(grid) {
+    WVPASSEQ(grid.headers, [
+      'a',
+      '9 2013-01-02',
+      '7 2013-01-01',
+      '11 2013-02-03'
+    ]);
+    WVPASSEQ(grid.data, [
+      ['fred', 1, null, 1],
+      ['bob', null, 1, null]
+    ]);
+  });
+});