333 lines
9.3 KiB
JavaScript
Executable File
333 lines
9.3 KiB
JavaScript
Executable File
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;
|