Initial commit
This commit is contained in:
+332
@@ -0,0 +1,332 @@
|
||||
var CDB = require('internal-carto.js');
|
||||
var _ = require('underscore');
|
||||
|
||||
function TableStats (opts) {
|
||||
if (opts.user && opts.api_key) {
|
||||
this.user = opts.username;
|
||||
this.api_key = opts.api_key;
|
||||
} else if (opts.configModel) {
|
||||
this.configModel = opts.configModel;
|
||||
this.userModel = opts.userModel;
|
||||
}
|
||||
this.tables = {};
|
||||
this.queue = {};
|
||||
}
|
||||
|
||||
TableStats.prototype = {
|
||||
|
||||
graphFor: function (tableName, column, callback) {
|
||||
this._getPgStats(tableName, function (stats) {
|
||||
var graph = new ColumnGraph(stats[column]);
|
||||
callback(graph);
|
||||
});
|
||||
},
|
||||
|
||||
_getPgStats: function (table, callback) {
|
||||
var userModel = this.userModel;
|
||||
var self = this;
|
||||
if (this.tables[table]) {
|
||||
if (!_.isEmpty(this.tables[table])) {
|
||||
callback(this.tables[table]);
|
||||
} else {
|
||||
this.queue[table].push(callback);
|
||||
}
|
||||
} else {
|
||||
this.tables[table] = {};
|
||||
this.queue[table] = [callback];
|
||||
|
||||
var schema = userModel.getSchemaName();
|
||||
var sql = new CDB.SQL({
|
||||
user: this.configModel.get('user_name'),
|
||||
sql_api_template: this.configModel.get('sql_api_template'),
|
||||
api_key: this.configModel.get('api_key')
|
||||
});
|
||||
|
||||
sql.execute(
|
||||
'with a as (select reltuples from pg_class where relname = \'' + table + '\'), b as (select * from pg_stats where tablename = \'' + table + '\' and schemaname = \'' + schema + '\') select * from a,b',
|
||||
null,
|
||||
{
|
||||
rows_per_page: 40,
|
||||
page: 0
|
||||
}
|
||||
).done(function (data) {
|
||||
data = data || {};
|
||||
var rows = data.rows.map(function (r) {
|
||||
var count = r.reltuples;
|
||||
var bounds = self._reformatToJSON(r.histogram_bounds);
|
||||
if (bounds) {
|
||||
var avgs = bounds.reduce(function (p, c, i) {
|
||||
if (i < bounds.length - 1) {
|
||||
p.push((bounds[i + 1] + c) / 2);
|
||||
}
|
||||
return p;
|
||||
}, []);
|
||||
var binsize = count / (bounds.length - 1);
|
||||
var sum = avgs.reduce(function (p, c) {
|
||||
return p + c * binsize;
|
||||
}, 0);
|
||||
var average = sum / count;
|
||||
var min = bounds[0];
|
||||
var max = _.last(bounds);
|
||||
}
|
||||
var mostCommon = self._reformatToJSON(r.most_common_vals);
|
||||
var trues;
|
||||
if (mostCommon) {
|
||||
var trueIndex = mostCommon.indexOf('t');
|
||||
var falseIndex = mostCommon.indexOf('f');
|
||||
if (trueIndex > -1 && falseIndex > -1) {
|
||||
trues = r.most_common_freqs[trueIndex];
|
||||
}
|
||||
}
|
||||
return {
|
||||
column: r.attname,
|
||||
histogram_bounds: bounds,
|
||||
freqs: r.most_common_freqs,
|
||||
mostcommon: mostCommon,
|
||||
nulls: r.null_frac,
|
||||
count: count,
|
||||
sum: sum,
|
||||
min: min,
|
||||
max: max,
|
||||
avg: average,
|
||||
trues: trues
|
||||
};
|
||||
});
|
||||
|
||||
var stats = rows.reduce(function (p, c) {
|
||||
var column = c.column;
|
||||
delete c.column;
|
||||
p[column] = c;
|
||||
return p;
|
||||
}, {});
|
||||
self.tables[table] = stats;
|
||||
self._processQueue(table);
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
_processQueue: function (table) {
|
||||
var self = this;
|
||||
this.queue[table].forEach(function (callback) {
|
||||
callback(self.tables[table]);
|
||||
});
|
||||
},
|
||||
|
||||
_reformatToJSON: function (sqlData) {
|
||||
if (!sqlData) return null;
|
||||
sqlData = sqlData.replace('{', '').replace('}', '').split(',');
|
||||
if (!sqlData.some(isNaN)) {
|
||||
sqlData = sqlData.map(function (n) {
|
||||
return parseInt(n, 10);
|
||||
});
|
||||
}
|
||||
return sqlData;
|
||||
}
|
||||
};
|
||||
|
||||
function ColumnGraph (stats, options) {
|
||||
options = options || {};
|
||||
this.stats = stats;
|
||||
this.normalize = typeof options.normalize === 'undefined' ? true : options.normalize;
|
||||
}
|
||||
|
||||
ColumnGraph.prototype = {
|
||||
getNullsPercentage: function () {
|
||||
return this.stats.nulls;
|
||||
},
|
||||
|
||||
getPercentageInTopCategories: function (topx) {
|
||||
return this.stats.freqs.slice(0, topx || 10).reduce(function (p, c) { return p + c; });
|
||||
},
|
||||
|
||||
getTrues: function () {
|
||||
return this.stats.trues;
|
||||
},
|
||||
|
||||
getHistogram: function (options) {
|
||||
return this._generateHistogram(options.width, options.height, options.color, options.bins);
|
||||
},
|
||||
|
||||
getCategory: function (options) {
|
||||
return this._generateCategory(options.width, options.height, options.color);
|
||||
},
|
||||
|
||||
getMin: function () {
|
||||
return this.stats.min;
|
||||
},
|
||||
|
||||
getMax: function () {
|
||||
return this.stats.max;
|
||||
},
|
||||
|
||||
getAverage: function () {
|
||||
return this.stats.avg;
|
||||
},
|
||||
|
||||
getCount: function () {
|
||||
return this.stats.count;
|
||||
},
|
||||
|
||||
getSum: function () {
|
||||
return this.stats.sum;
|
||||
},
|
||||
|
||||
_generateCategory: function (width, height, color) {
|
||||
if (!this.stats.freqs) return;
|
||||
var canvas = document.createElement('canvas');
|
||||
var context = canvas.getContext('2d');
|
||||
var proportion;
|
||||
if (_.isNumber(this.getTrues())) {
|
||||
proportion = this.getTrues();
|
||||
} else {
|
||||
proportion = this.getPercentageInTopCategories();
|
||||
}
|
||||
canvas.height = height || 10;
|
||||
canvas.width = width || 400;
|
||||
var spacing = 2;
|
||||
var begin = spacing;
|
||||
var end = canvas.width - spacing * 2;
|
||||
var greenPosition = (end - begin) * proportion + begin;
|
||||
context.lineCap = 'round';
|
||||
context.strokeStyle = '#EDEDED';
|
||||
context.lineWidth = 4;
|
||||
context.beginPath();
|
||||
context.moveTo(begin, canvas.height / 2);
|
||||
context.lineTo(end, canvas.height / 2);
|
||||
context.stroke();
|
||||
context.strokeStyle = color;
|
||||
context.beginPath();
|
||||
context.moveTo(begin, canvas.height / 2);
|
||||
context.lineTo(greenPosition, canvas.height / 2);
|
||||
context.stroke();
|
||||
return canvas;
|
||||
},
|
||||
|
||||
_generateHistogram: function (width, height, color, bins) {
|
||||
var histogram;
|
||||
|
||||
if (_.isEmpty(this.stats)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.stats.freqs && this.stats.freqs.length < 60) {
|
||||
histogram = this._getHistogramFromFreqs(bins);
|
||||
} else if (this.stats.histogram_bounds) {
|
||||
histogram = this._getHistogramFromBounds(bins);
|
||||
}
|
||||
|
||||
if (!histogram || _.isEmpty(histogram)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.normalize) {
|
||||
var max = _.max(histogram);
|
||||
histogram = histogram.map(function (bin) {
|
||||
return bin / max;
|
||||
});
|
||||
}
|
||||
|
||||
bins = histogram.length;
|
||||
|
||||
var canvas = document.createElement('canvas');
|
||||
var context = canvas.getContext('2d');
|
||||
canvas.height = height || 90;
|
||||
canvas.width = width || 400;
|
||||
|
||||
var barSeparation = 2;
|
||||
var barWidth = (canvas.width - barSeparation * (bins - 1)) / bins;
|
||||
context.strokeStyle = '#EDEDED';
|
||||
context.lineWidth = 1;
|
||||
|
||||
// Guides
|
||||
|
||||
context.beginPath();
|
||||
context.moveTo(0, 0);
|
||||
context.lineTo(canvas.width, 0);
|
||||
context.stroke();
|
||||
context.beginPath();
|
||||
context.moveTo(0, canvas.height / 2);
|
||||
context.lineTo(canvas.width, canvas.height / 2);
|
||||
context.stroke();
|
||||
|
||||
// Bars
|
||||
|
||||
histogram.forEach(function (bar, index) {
|
||||
var x = index * (barWidth + barSeparation);
|
||||
var barHeight = canvas.height * bar;
|
||||
context.beginPath();
|
||||
context.fillStyle = color;
|
||||
context.fillRect(x, canvas.height - barHeight, barWidth, barHeight);
|
||||
});
|
||||
|
||||
// Base line
|
||||
|
||||
context.strokeStyle = '#A1BBA7';
|
||||
context.lineWidth = 2;
|
||||
context.beginPath();
|
||||
context.moveTo(0, canvas.height);
|
||||
context.lineTo(canvas.width, canvas.height);
|
||||
context.stroke();
|
||||
|
||||
return canvas;
|
||||
},
|
||||
|
||||
_getHistogramFromBounds: function (bins) {
|
||||
var bounds = this.stats.histogram_bounds;
|
||||
bounds = bounds.sort(function (a, b) { return a - b; });
|
||||
var min = bounds[0];
|
||||
var max = bounds[bounds.length - 1];
|
||||
var width = (max - min) / bins;
|
||||
var histogram = new Array(bins + 1).join('0').split('').map(parseFloat);
|
||||
function scaleBin (value) {
|
||||
if (max === min) return min;
|
||||
return Math.floor((bins - 1) * (value - min) / (max - min));
|
||||
}
|
||||
|
||||
var boundProportion = 1 / (bounds.length - 1);
|
||||
for (var i = 0; i < bounds.length - 1; ++i) {
|
||||
var binMin = scaleBin(bounds[i]);
|
||||
var binMax = scaleBin(bounds[i + 1]);
|
||||
if (binMin === binMax) {
|
||||
histogram[binMin] += boundProportion;
|
||||
} else {
|
||||
var proportionForFirstBin = ((min + width * (binMin + 1)) - bounds[i]) / (bounds[i + 1] - bounds[i]);
|
||||
histogram[binMin] += proportionForFirstBin * boundProportion;
|
||||
var proportionForLastBin = (bounds[i + 1] - (min + width * (binMax))) / (bounds[i + 1] - bounds[i]);
|
||||
histogram[binMax] += proportionForLastBin * boundProportion;
|
||||
var remaining = boundProportion - proportionForFirstBin - proportionForLastBin;
|
||||
_.range(binMin + 1, binMax).forEach(function (binIndex, i, array) {
|
||||
histogram[binIndex] = remaining / array.length;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return histogram;
|
||||
},
|
||||
|
||||
_getHistogramFromFreqs: function (bins) {
|
||||
var values = this.stats.mostcommon;
|
||||
var freqs = this.stats.freqs;
|
||||
bins = Math.min(bins, values.length);
|
||||
var min = _.min(values);
|
||||
var max = _.max(values);
|
||||
|
||||
// If there is only one value, histogram should not be rendered
|
||||
if (min === max) {
|
||||
return [];
|
||||
}
|
||||
|
||||
var histogram = new Array(bins + 1).join('0').split('').map(parseFloat);
|
||||
function scale (value) {
|
||||
if (max === min) return min;
|
||||
return (bins - 1) * (value - min) / (max - min);
|
||||
}
|
||||
values.forEach(function (v, i) {
|
||||
histogram[Math.floor(scale(v))] += freqs[i];
|
||||
});
|
||||
return histogram;
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
module.exports = TableStats;
|
||||
Reference in New Issue
Block a user