| 'use strict'; |
| |
| var afterquery = (function() { |
| // To appease v8shell |
| var console, localStorage; |
| try { |
| console = window.console; |
| } |
| catch (ReferenceError) { |
| console = { |
| debug: print |
| }; |
| } |
| try { |
| localStorage = window.localStorage; |
| } catch (ReferenceError) { |
| localStorage = {} |
| } |
| |
| // For konqueror compatibility |
| if (!console) { |
| console = window.console; |
| } |
| if (!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(); |
| } |
| } |
| |
| |
| 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 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'; |
| |
| |
| function guessTypes(data) { |
| 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 = /^Date\(([\d,]+)\)$/; |
| function myParseDate(s) { |
| if (s == null) return s; |
| if (s && s.getDate) return s; |
| var g = DATE_RE2.exec(s); |
| if (g) g = (',' + g[1]).split(','); |
| if (!g || g.length > 8) g = DATE_RE1.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 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]; |
| } 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]); |
| } |
| 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 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))); |
| } else { |
| values.push(tmpval); |
| } |
| } |
| } 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)); |
| } |
| |
| // 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]); |
| } |
| } |
| |
| 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; |
| } |
| |
| |
| 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; |
| } |
| } |
| 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) { |
| if (grid.types[coli] === T_NUM && row[coli] == undefined) { |
| row[coli] = 0; |
| } |
| } |
| } |
| 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(': ')); |
| } |
| |
| 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; |
| } |
| } else { |
| throw new Error('unknown chart type "' + chartops + '"'); |
| } |
| $(el).height(window.innerHeight); |
| datatable = dataToGvizTable(grid, { show_only_lastseg: true }); |
| } else { |
| var el = document.getElementById('viztable'); |
| t = new google.visualization.Table(el); |
| datatable = dataToGvizTable(grid); |
| } |
| |
| 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 { |
| runqueue(); |
| } |
| } |
| |
| |
| 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; |
| } |
| }; |
| return f; |
| } |
| |
| |
| var URL_RE = RegExp("^((\\w+:)?(//[^/]*)?)"); |
| |
| |
| function urlMinusPath(url) { |
| var g = URL_RE.exec(url); |
| if (g && g[1]) { |
| return g[1]; |
| } else { |
| return url; |
| } |
| } |
| |
| |
| function getUrlData(url, success_func, error_func) { |
| // some services expect callback=, some expect jsonp=, so supply both |
| var plus = 'callback=jsonp&jsonp=jsonp'; |
| var hostpart = urlMinusPath(url); |
| var auth = localStorage[['auth', hostpart]]; |
| if (auth) { |
| plus += '&auth=' + encodeURIComponent(auth); |
| } |
| |
| 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 |
| } |
| } |
| }; |
| |
| iframe.contentWindow.onerror = function(message, xurl, lineno) { |
| error(null, message + ' url=' + xurl + ' line=' + lineno); |
| }; |
| |
| //TODO(apenwarr): change the domain/origin attribute of the iframe. |
| // That way the script won't be able to affect us, no matter how badly |
| // behaved it might be. That's important so they can't access our |
| // localStorage, set cookies, etc. We can use the new html5 postMessage |
| // feature to safely send json data from the iframe back to us. |
| // ...but for the moment we have to trust the data provider. |
| var script = document.createElement('script'); |
| script.async = 1; |
| script.src = nurl; |
| iframe.contentDocument.body.appendChild(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(); |
| } |
| } |
| |
| return { |
| internal: { |
| parseArgs: parseArgs, |
| trySplitOne: trySplitOne, |
| dataToGvizTable: dataToGvizTable, |
| guessTypes: guessTypes, |
| groupBy: groupBy, |
| pivotBy: pivotBy, |
| stringifiedCols: stringifiedCols, |
| treeify: treeify, |
| filterBy: filterBy, |
| queryBy: queryBy, |
| orderBy: orderBy, |
| extractRegexp: extractRegexp, |
| fillNullsWithZero: fillNullsWithZero, |
| urlMinusPath: urlMinusPath, |
| gridFromData: gridFromData |
| }, |
| render: wrap(_run) |
| }; |
| })(); |