react-mapfilter
Version:
These components are designed for viewing data in Mapeo. They share a common interface:
351 lines (313 loc) • 9.97 kB
JavaScript
import "core-js/modules/es.array.iterator";
import "core-js/modules/web.dom-collections.iterator";
import _mapInstanceProperty from "@babel/runtime-corejs3/core-js-stable/instance/map";
import _indexOfInstanceProperty from "@babel/runtime-corejs3/core-js-stable/instance/index-of";
import _filterInstanceProperty from "@babel/runtime-corejs3/core-js-stable/instance/filter";
import _valuesInstanceProperty from "@babel/runtime-corejs3/core-js-stable/instance/values";
import _JSON$stringify from "@babel/runtime-corejs3/core-js-stable/json/stringify";
import _forEachInstanceProperty from "@babel/runtime-corejs3/core-js-stable/instance/for-each";
import _Map from "@babel/runtime-corejs3/core-js-stable/map";
// @flow
import cloneDeep from 'clone-deep';
import { flatObjectEntries } from '../../utils/flat_object_entries';
import { guessValueType } from './value_types';
import * as valueTypes from '../../constants/value_types';
import { leftPad } from '../../utils/helpers';
/*:: import type {
JSONObject,
Statistics,
FieldStatistic,
StringStatistic,
NumberStatistic,
DateStatistic
} from '../../types'*/
function defaultStats()
/*: FieldStatistic*/
{
return {
[valueTypes.STRING]: {
count: 0,
values: new _Map()
},
[valueTypes.NUMBER]: {
count: 0,
values: new _Map()
},
[valueTypes.DATE]: {
count: 0,
values: new _Map()
},
[valueTypes.DATETIME]: {
count: 0,
values: new _Map()
},
[valueTypes.BOOLEAN]: {
count: 0,
values: new _Map([[true, 0], [false, 0]])
},
[valueTypes.URL]: 0,
[valueTypes.IMAGE_URL]: 0,
[valueTypes.AUDIO_URL]: 0,
[valueTypes.VIDEO_URL]: 0,
[valueTypes.NULL]: 0,
[valueTypes.UNDEFINED]: 0,
[valueTypes.LOCATION]: 0
};
}
export default function createMemoizedStats() {
let stats
/*: Statistics*/
= {};
let dataMemo = [];
return function getStats(data
/*: Array<JSONObject>*/
)
/*: Statistics*/
{
if (data === dataMemo) return stats;
const {
added,
removed
} = diffArrays(dataMemo, data);
if (!added.length && !removed.length) return cloneDeep(stats);
if (removed.length) {
// Anything removed, calculate all the stats
// TODO: more efficient stats on removal
stats = {};
_forEachInstanceProperty(data).call(data, item => addItemStats(item, stats));
} else {
// Only added items -> only need to process new items
_forEachInstanceProperty(added).call(added, item => addItemStats(item, stats));
}
dataMemo = data;
return cloneDeep(stats);
};
}
function addItemStats(item
/*: JSONObject*/
= {}, stats
/*: Statistics*/
) {
var _context;
_forEachInstanceProperty(_context = flatObjectEntries(item)).call(_context, function ([path, value]) {
const key = _JSON$stringify(path);
if (!stats[key]) stats[key] = defaultStats();
addFieldStats(value, stats[key]);
});
}
function addFieldStats(value
/*: any*/
, fieldStats
/*: FieldStatistic*/
) {
const type = guessValueType(value);
if (typeof fieldStats[type] === 'number') {
fieldStats[type] += 1;
return;
}
switch (type) {
case valueTypes.ARRAY:
let arrayStats = fieldStats[valueTypes.ARRAY];
if (arrayStats === undefined) {
arrayStats = fieldStats[valueTypes.ARRAY] = {
count: 0,
lengthMin: +Infinity,
lengthMax: -Infinity,
valueStats: defaultStats()
};
}
addArrayStats(value, arrayStats);
return;
case valueTypes.STRING:
addStringStats(value, fieldStats[valueTypes.STRING]);
return;
case valueTypes.NUMBER:
addNumberStats(value, fieldStats[valueTypes.NUMBER]);
return;
case valueTypes.DATE:
addDateStats(value, fieldStats[valueTypes.DATE]);
return;
case valueTypes.DATETIME:
addDateTimeStats(value, fieldStats[valueTypes.DATETIME]);
return;
case valueTypes.BOOLEAN:
fieldStats[valueTypes.BOOLEAN].count += 1;
_valuesInstanceProperty(fieldStats[valueTypes.BOOLEAN]).set(value, _valuesInstanceProperty(fieldStats[valueTypes.BOOLEAN]).get(value) + 1);
return;
default:
// $FlowFixMe - flow doesn't realise that type is not values above
fieldStats[type].count += 1;
}
}
function addArrayStats(value
/*: []*/
, stats
/*: $NonMaybeType<$ElementType<FieldStatistic, 'array'>>*/
) {
if (value.length > stats.lengthMax) stats.lengthMax = value.length;
if (value.length < stats.lengthMin) stats.lengthMin = value.length;
stats.count += 1;
_forEachInstanceProperty(value).call(value, function (item) {
addFieldStats(item, stats.valueStats);
});
}
function addNumberStats(value
/*: number*/
, stats
/*: NumberStatistic*/
) {
stats.count += 1;
const {
min,
max,
variance,
mean
} = statReduce(stats, value, stats.count - 1);
stats.min = min;
stats.max = max;
stats.variance = variance;
stats.mean = mean;
if (_valuesInstanceProperty(stats).has(value)) _valuesInstanceProperty(stats).set(value, _valuesInstanceProperty(stats).get(value) + 1);else _valuesInstanceProperty(stats).set(value, 1);
}
function addDateTimeStats(value
/*: string*/
, stats
/*: DateStatistic*/
) {
const dateAsNumber = +Date.parse(value);
stats.count += 1;
const {
mean
} = statReduce({
mean: stats.mean !== undefined ? +Date.parse(stats.mean) : undefined
}, dateAsNumber, stats.count - 1);
stats.min = stats.min === undefined ? value : value < stats.min ? value : stats.min;
stats.max = stats.max === undefined ? value : value > stats.max ? value : stats.max;
stats.mean = mean !== undefined ? new Date(mean).toISOString() : undefined;
if (_valuesInstanceProperty(stats).has(value)) _valuesInstanceProperty(stats).set(value, _valuesInstanceProperty(stats).get(value) + 1);else _valuesInstanceProperty(stats).set(value, 1);
}
/** This requires slightly special treatment because date does not include time */
function addDateStats(value
/*: string*/
, stats
/*: DateStatistic*/
) {
const dateAsNumber = dateToNumber(value);
stats.count += 1;
const {
mean
} = statReduce({
mean: stats.mean !== undefined ? dateToNumber(stats.mean) : undefined
}, dateAsNumber, stats.count - 1);
stats.min = stats.min === undefined ? value : value < stats.min ? value : stats.min;
stats.max = stats.max === undefined ? value : value > stats.max ? value : stats.max;
stats.mean = mean !== undefined ? numberToDate(mean) : undefined;
if (_valuesInstanceProperty(stats).has(value)) _valuesInstanceProperty(stats).set(value, _valuesInstanceProperty(stats).get(value) + 1);else _valuesInstanceProperty(stats).set(value, 1);
}
function addStringStats(value
/*: string*/
, stats
/*: StringStatistic*/
) {
stats.count += 1;
const lengthStats = statReduce({
min: stats.lengthMin,
max: stats.lengthMax,
variance: stats.lengthVariance,
mean: stats.lengthMean
}, value.length, stats.count - 1);
const wordStats = statReduce({
min: stats.wordsMin,
max: stats.wordsMax,
variance: stats.wordsVariance,
mean: stats.wordsMean
}, value.split(' ').length, stats.count - 1);
stats.lengthMin = lengthStats.min;
stats.lengthMax = lengthStats.max;
stats.lengthVariance = lengthStats.variance;
stats.lengthMean = lengthStats.mean;
stats.wordsMin = wordStats.min;
stats.wordsMax = wordStats.max;
stats.wordsVariance = wordStats.variance;
stats.wordsMean = wordStats.mean;
if (_valuesInstanceProperty(stats).has(value)) _valuesInstanceProperty(stats).set(value, _valuesInstanceProperty(stats).get(value) + 1);else _valuesInstanceProperty(stats).set(value, 1);
}
/**
* Compare two arrays of objects and return items in the second but not in the
* first (added) and items in the first but not in the second (removed).
* Compares using strict equality.
*/
export function diffArrays(oldArray
/*: Array<Object>*/
, newArray
/*: Array<Object>*/
)
/*: { added: Array<Object>, removed: Array<Object> }*/
{
const added = _filterInstanceProperty(newArray).call(newArray, v => _indexOfInstanceProperty(oldArray).call(oldArray, v) === -1);
const removed = _filterInstanceProperty(oldArray).call(oldArray, v => _indexOfInstanceProperty(newArray).call(newArray, v) === -1);
return {
added,
removed
};
}
/*:: type MathStat = {
mean?: number,
variance?: number,
min?: number,
max?: number
}*/
/**
* Reducer that computes running mean, variance, min and max
* Adapted from http://www.johndcook.com/blog/standard_deviation/
* @param {Object} p The previous value for the analysis
* @param {Number} x New value to be included in analysis
* @param {Number} i zero-based index of the current element being processed
* @return {Object} New analysis including `x`
*/
export function statReduce({
mean = NaN,
variance = NaN,
min = +Infinity,
max = -Infinity
}
/*: MathStat*/
, x
/*: number*/
, i
/*: number*/
)
/*: $ObjMap<MathStat, <V>(V) => $NonMaybeType<V>>*/
{
mean = isNaN(mean) ? 0 : mean;
x = x instanceof Date ? +x : x;
const newMean = mean + (x - mean) / (i + 1);
return {
mean: newMean,
min: x < min ? x : min,
max: x > max ? x : max,
variance: i < 1 ? 0 : (variance * i + (x - mean) * (x - newMean)) / (i + 1)
};
}
/** Convert date in the format YYYY-MM-DD to a number */
function dateToNumber(value
/*: string*/
)
/*: number*/
{
var _context2;
const [year, month, day] = _mapInstanceProperty(_context2 = value.split('-')).call(_context2, Number); // Add 12 hours -> middle of day
return new Date(year, month - 1, day).getTime() + 12 * 60 * 60 * 1000;
}
function numberToDate(value
/*: number*/
)
/*: string*/
{
const date = new Date(value);
const YYYY = date.getFullYear();
const MM = leftPad(date.getMonth() + 1 + '', 2, '0');
const DD = leftPad(date.getDate() + '', 2, '0');
return `${YYYY}-${MM}-${DD}`;
}
//# sourceMappingURL=statistics.js.map