Commit a0fa5698 by ryan

rename reducer to statsCalculator

parent 29695077
......@@ -3,45 +3,45 @@ import React, { PureComponent } from 'react';
import { storiesOf } from '@storybook/react';
import { action } from '@storybook/addon-actions';
import { withCenteredStory } from '../../utils/storybook/withCenteredStory';
import { TableReducePicker } from './TableReducePicker';
import { StatsPicker } from './StatsPicker';
import { text, boolean } from '@storybook/addon-knobs';
interface State {
reducers: string[];
stats: string[];
}
export class WrapperWithState extends PureComponent<any, State> {
constructor(props: any) {
super(props);
this.state = {
reducers: this.toReducersArray(props.initialReducers),
stats: this.toStatsArray(props.initialReducers),
};
}
toReducersArray = (txt: string): string[] => {
toStatsArray = (txt: string): string[] => {
return txt.split(',').map(v => v.trim());
};
componentDidUpdate(prevProps: any) {
const { initialReducers } = this.props;
if (initialReducers !== prevProps.initialReducers) {
this.setState({ reducers: this.toReducersArray(initialReducers) });
this.setState({ stats: this.toStatsArray(initialReducers) });
}
}
render() {
const { placeholder, defaultReducer, allowMultiple } = this.props;
const { reducers } = this.state;
const { placeholder, defaultStat, allowMultiple } = this.props;
const { stats } = this.state;
return (
<TableReducePicker
<StatsPicker
placeholder={placeholder}
defaultReducer={defaultReducer}
defaultStat={defaultStat}
allowMultiple={allowMultiple}
reducers={reducers}
onChange={(reducers: string[]) => {
action('Picked:')(reducers);
this.setState({ reducers });
stats={stats}
onChange={(stats: string[]) => {
action('Picked:')(stats);
this.setState({ stats });
}}
/>
);
......@@ -52,16 +52,16 @@ const story = storiesOf('UI/TableReducePicker', module);
story.addDecorator(withCenteredStory);
story.add('picker', () => {
const placeholder = text('Placeholder Text', '');
const defaultReducer = text('Default Reducer', '');
const defaultStat = text('Default Stat', '');
const allowMultiple = boolean('Allow Multiple', false);
const initialReducers = text('Initial Reducers', '');
const initialStats = text('Initial Stats', '');
return (
<div>
<WrapperWithState
placeholder={placeholder}
defaultReducer={defaultReducer}
defaultStat={defaultStat}
allowMultiple={allowMultiple}
initialReducers={initialReducers}
initialStats={initialStats}
/>
</div>
);
......
......@@ -4,19 +4,19 @@ import isArray from 'lodash/isArray';
import { Select } from '../index';
import { getTableReducers } from '../../utils/tableReducer';
import { getStatsCalculators } from '../../utils/statsCalculator';
import { SelectOptionItem } from '../Select/Select';
interface Props {
placeholder?: string;
onChange: (reducers: string[]) => void;
reducers: string[];
onChange: (stats: string[]) => void;
stats: string[];
width?: number;
allowMultiple?: boolean;
defaultReducer?: string;
defaultStat?: string;
}
export class TableReducePicker extends PureComponent<Props> {
export class StatsPicker extends PureComponent<Props> {
static defaultProps = {
width: 12,
allowMultiple: false,
......@@ -31,25 +31,25 @@ export class TableReducePicker extends PureComponent<Props> {
}
checkInput = () => {
const { reducers, allowMultiple, defaultReducer, onChange } = this.props;
const { stats, allowMultiple, defaultStat, onChange } = this.props;
// Check that the selected reducers are all real
const notFound: string[] = [];
const current = getTableReducers(reducers, notFound);
const current = getStatsCalculators(stats, notFound);
if (notFound.length > 0) {
console.warn('Unknown reducers', notFound, reducers);
console.warn('Unknown reducers', notFound, stats);
onChange(current.map(reducer => reducer.value));
}
// Make sure there is only one
if (!allowMultiple && reducers.length > 1) {
console.warn('Removing extra reducers', reducers);
onChange([reducers[0]]);
if (!allowMultiple && stats.length > 1) {
console.warn('Removing extra stat', stats);
onChange([stats[0]]);
}
// Set the reducer from callback
if (defaultReducer && reducers.length < 1) {
onChange([defaultReducer]);
if (defaultStat && stats.length < 1) {
onChange([defaultStat]);
}
};
......@@ -63,17 +63,17 @@ export class TableReducePicker extends PureComponent<Props> {
};
render() {
const { width, reducers, allowMultiple, defaultReducer, placeholder } = this.props;
const current = getTableReducers(reducers);
const { width, stats, allowMultiple, defaultStat, placeholder } = this.props;
const current = getStatsCalculators(stats);
return (
<Select
width={width}
value={current}
isClearable={!defaultReducer}
isClearable={!defaultStat}
isMulti={allowMultiple}
isSearchable={true}
options={getTableReducers()}
options={getStatsCalculators()}
placeholder={placeholder}
onChange={this.onSelectionChange}
/>
......
import { parseCSV } from './processTableData';
import { getStatsCalculators, StatID, calculateStats } from './statsCalculator';
describe('Stats Calculators', () => {
const basicTable = parseCSV('a,b,c\n10,20,30\n20,30,40');
it('should load all standard stats', () => {
const names = [
StatID.sum,
StatID.max,
StatID.min,
StatID.logmin,
StatID.mean,
StatID.last,
StatID.first,
StatID.count,
StatID.range,
StatID.diff,
StatID.step,
StatID.delta,
// StatID.allIsZero,
// StatID.allIsNull,
];
const notFound: string[] = [];
const stats = getStatsCalculators(names, notFound);
stats.forEach((stat, index) => {
expect(stat ? stat.value : '<missing>').toEqual(names[index]);
});
expect(notFound.length).toBe(0);
});
it('should fail to load unknown stats', () => {
const names = ['not a stat', StatID.max, StatID.min, 'also not a stat'];
const notFound: string[] = [];
const stats = getStatsCalculators(names, notFound);
expect(stats.length).toBe(2);
expect(notFound.length).toBe(2);
});
it('should calculate stats', () => {
const stats = calculateStats({
data: basicTable,
columnIndex: 0,
stats: ['first', 'last', 'mean'],
});
// First
expect(stats.first).toEqual(10);
// Last
expect(stats.last).toEqual(20);
// Mean
expect(stats.mean).toEqual(15);
});
it('should support a single stat also', () => {
const stats = calculateStats({
data: basicTable,
columnIndex: 0,
stats: ['first', 'last', 'mean'],
});
// First
expect(stats.first).toEqual(10);
// Last
expect(stats.last).toEqual(20);
// Mean
expect(stats.mean).toEqual(15);
});
});
......@@ -3,7 +3,7 @@ import isNumber from 'lodash/isNumber';
import { TableData, NullValueMode } from '../types/index';
export enum TableReducerID {
export enum StatID {
sum = 'sum',
max = 'max',
min = 'min',
......@@ -21,92 +21,92 @@ export enum TableReducerID {
allIsNull = 'allIsNull',
}
/** Information about the reducing(stats) functions */
export interface TableReducerInfo {
export interface ColumnStats {
[key: string]: any;
}
// Internal function
type StatCalculator = (data: TableData, columnIndex: number, ignoreNulls: boolean, nullAsZero: boolean) => ColumnStats;
export interface StatCalculatorInfo {
value: string; // The ID - value maps directly to select component
label: string; // The name - label
label: string; // The name - label for Select component
description: string;
alias?: string; // optional secondary key. 'avg' vs 'mean', 'total' vs 'sum'
// Internal details
emptyInputResult?: any; // typically null, but some things like 'count' & 'sum' should be zero
standard: boolean; // The most common stats can all be calculated in a single pass
reducer?: TableReducer;
calculator?: StatCalculator;
}
/**
* Get a list of the known reducing functions
* @param ids list of reducer names or null to get all of them
* @param notFound optional error object that will be filled with the names on unknown reducers
* @param ids list of stat names or null to get all of them
* @param notFound optional error object that will be filled with the names on unknown stats
*/
export function getTableReducers(ids?: string[], notFound?: string[]): TableReducerInfo[] {
export function getStatsCalculators(ids?: string[], notFound?: string[]): StatCalculatorInfo[] {
if (ids === null || ids === undefined) {
return listOfReducers;
return listOfStats;
}
return ids.reduce((list, id) => {
const reducer = getById(id);
if (reducer) {
list.push(reducer);
const stat = getById(id);
if (stat) {
list.push(stat);
} else if (notFound && id) {
notFound.push(id);
}
return list;
}, new Array<TableReducerInfo>());
}, new Array<StatCalculatorInfo>());
}
export interface TableReducerOptions {
columnIndexes?: number[];
nullValueMode?: NullValueMode;
export interface CalculateStatsOptions {
data: TableData;
columnIndex: number;
stats: string[]; // The stats to calculate
nullValueMode?: NullValueMode;
}
export function reduceTableData(data: TableData, options: TableReducerOptions): TableData[] {
const indexes = verifyColumns(data, options);
const columns = indexes.map(v => data.columns[v]);
/**
* @returns an object with a key for each selected stat
*/
export function calculateStats(options: CalculateStatsOptions): ColumnStats {
const { data, columnIndex, stats, nullValueMode } = options;
const ignoreNulls = options.nullValueMode === NullValueMode.Ignore;
const nullAsZero = options.nullValueMode === NullValueMode.AsZero;
if (!stats || stats.length < 1) {
return {};
}
const queue = getTableReducers(options.stats);
const queue = getStatsCalculators(stats);
// Return early for empty tables
// This lets the concrete implementations assume at least one row
if (!data.rows || data.rows.length < 1) {
return queue.map(stat => {
return {
columns,
rows: [indexes.map(v => stat.emptyInputResult)],
type: 'table',
columnMap: {},
};
const stats = {} as ColumnStats;
queue.forEach(stat => {
stats[stat.value] = stat.emptyInputResult !== null ? stat.emptyInputResult : null;
});
return stats;
}
const ignoreNulls = nullValueMode === NullValueMode.Ignore;
const nullAsZero = nullValueMode === NullValueMode.AsZero;
// Avoid calculating all the standard stats if possible
if (queue.length === 1 && queue[0].reducer) {
return [
{
columns,
rows: [queue[0].reducer(data, indexes, ignoreNulls, nullAsZero)],
type: 'table',
columnMap: {},
},
];
if (queue.length === 1 && queue[0].calculator) {
return [queue[0].calculator(data, columnIndex, ignoreNulls, nullAsZero)];
}
// For now everything can use the standard stats
const standard = standardStatsReducer(data, indexes, ignoreNulls, nullAsZero);
return queue.map(calc => {
const values = calc.standard
? standard.map((s: any) => s[calc.value])
: calc.reducer!(data, indexes, ignoreNulls, nullAsZero);
return {
columns,
rows: [values],
type: 'table',
columnMap: {},
let values = standardStatsStat(data, columnIndex, ignoreNulls, nullAsZero);
queue.forEach(calc => {
if (!values.hasOwnProperty(calc.value) && calc.calculator) {
values = {
...values,
...calc.calculator(data, columnIndex, ignoreNulls, nullAsZero),
};
}
});
return values;
}
// ------------------------------------------------------------------------------
......@@ -115,66 +115,64 @@ export function reduceTableData(data: TableData, options: TableReducerOptions):
//
// ------------------------------------------------------------------------------
type TableReducer = (data: TableData, columnIndexes: number[], ignoreNulls: boolean, nullAsZero: boolean) => any[];
// private registry of all reducers
interface TableReducerIndex {
[id: string]: TableReducerInfo;
// private registry of all stats
interface TableStatIndex {
[id: string]: StatCalculatorInfo;
}
const listOfReducers: TableReducerInfo[] = [];
const index: TableReducerIndex = {};
const listOfStats: StatCalculatorInfo[] = [];
const index: TableStatIndex = {};
let hasBuiltIndex = false;
function getById(id: string): TableReducerInfo | undefined {
function getById(id: string): StatCalculatorInfo | undefined {
if (!hasBuiltIndex) {
[
{
value: TableReducerID.last,
value: StatID.last,
label: 'Last',
description: 'Last Value (current)',
standard: true,
alias: 'current',
reducer: getLastRow,
stat: calculateLast,
},
{ value: TableReducerID.first, label: 'First', description: 'First Value', standard: true, reducer: getFirstRow },
{ value: TableReducerID.min, label: 'Min', description: 'Minimum Value', standard: true },
{ value: TableReducerID.max, label: 'Max', description: 'Maximum Value', standard: true },
{ value: TableReducerID.mean, label: 'Mean', description: 'Average Value', standard: true, alias: 'avg' },
{ value: StatID.first, label: 'First', description: 'First Value', standard: true, stat: calculateFirst },
{ value: StatID.min, label: 'Min', description: 'Minimum Value', standard: true },
{ value: StatID.max, label: 'Max', description: 'Maximum Value', standard: true },
{ value: StatID.mean, label: 'Mean', description: 'Average Value', standard: true, alias: 'avg' },
{
value: TableReducerID.sum,
value: StatID.sum,
label: 'Total',
description: 'The sum of all values',
emptyInputResult: 0,
standard: true,
alias: 'total',
},
{ value: TableReducerID.count, label: 'Count', description: 'Value Count', emptyInputResult: 0, standard: true },
{ value: StatID.count, label: 'Count', description: 'Value Count', emptyInputResult: 0, standard: true },
{
value: TableReducerID.range,
value: StatID.range,
label: 'Range',
description: 'Difference between minimum and maximum values',
standard: true,
},
{
value: TableReducerID.delta,
value: StatID.delta,
label: 'Delta',
description: 'Cumulative change in value', // HELP! not totally sure what this does
standard: true,
},
{
value: TableReducerID.step,
value: StatID.step,
label: 'Step',
description: 'Minimum interval between values',
standard: true,
},
{
value: TableReducerID.diff,
value: StatID.diff,
label: 'Difference',
description: 'Difference between first and last values',
standard: true,
},
{
value: TableReducerID.logmin,
value: StatID.logmin,
label: 'Min (above zero)',
description: 'Used for log min scale',
standard: true,
......@@ -182,64 +180,29 @@ function getById(id: string): TableReducerInfo | undefined {
].forEach(calc => {
const { value, alias } = calc;
if (index.hasOwnProperty(value)) {
console.warn('Duplicate Reducer', value, calc, index);
console.warn('Duplicate Stat', value, calc, index);
}
index[value] = calc;
if (alias) {
if (index.hasOwnProperty(alias)) {
console.warn('Duplicate Reducer (alias)', alias, calc, index);
console.warn('Duplicate Stat (alias)', alias, calc, index);
}
index[alias] = calc;
}
listOfReducers.push(calc);
listOfStats.push(calc);
});
hasBuiltIndex = true;
}
return index[id];
}
/**
* This will return an array of valid indexes and throw an error if invalid request
*/
function verifyColumns(data: TableData, options: TableReducerOptions): number[] {
const { columnIndexes } = options;
if (!columnIndexes) {
return data.columns.map((v, idx) => idx);
}
columnIndexes.forEach(v => {
if (v < 0 || v >= data.columns.length) {
throw new Error('Invalid column selection: ' + v);
}
});
return columnIndexes;
}
interface StandardStats {
sum: number | null; // total
max: number | null;
min: number | null;
logmin: number;
mean: number | null; // avg
last: any; // current
first: any;
count: number;
nonNullCount: number;
range: number | null;
diff: number | null;
delta: number | null;
step: number | null;
allIsZero: boolean;
allIsNull: boolean;
}
function standardStatsReducer(
function standardStatsStat(
data: TableData,
columnIndexes: number[],
columnIndex: number,
ignoreNulls: boolean,
nullAsZero: boolean
): StandardStats[] {
const column = columnIndexes.map(idx => {
return {
): ColumnStats {
const stats = {
sum: 0,
max: -Number.MAX_VALUE,
min: Number.MAX_VALUE,
......@@ -256,15 +219,12 @@ function standardStatsReducer(
delta: 0,
step: 0,
// Just used for calcutations -- not exposed as a reducer
// Just used for calcutations -- not exposed as a stat
previousDeltaUp: true,
};
});
} as ColumnStats;
for (let i = 0; i < data.rows.length; i++) {
for (let x = 0; x < column.length; x++) {
const stats = column[x];
let currentValue = data.rows[i][x];
let currentValue = data.rows[i][columnIndex];
if (currentValue === null) {
if (ignoreNulls) {
......@@ -331,10 +291,6 @@ function standardStatsReducer(
stats.last = currentValue;
}
}
}
for (let x = 0; x < column.length; x++) {
const stats = column[x] as StandardStats;
if (stats.max === -Number.MAX_VALUE) {
stats.max = null;
......@@ -357,17 +313,14 @@ function standardStatsReducer(
stats.diff = stats.last - stats.first;
}
}
}
return column;
return stats;
}
function getFirstRow(data: TableData, columnIndexes: number[], ignoreNulls: boolean, nullAsZero: boolean): any[] {
const row = data.rows[0];
return columnIndexes.map(idx => row[idx]);
function calculateFirst(data: TableData, columnIndex: number, ignoreNulls: boolean, nullAsZero: boolean): ColumnStats {
return { first: data.rows[0][columnIndex] };
}
function getLastRow(data: TableData, columnIndexes: number[], ignoreNulls: boolean, nullAsZero: boolean): any[] {
const row = data.rows[data.rows.length - 1];
return columnIndexes.map(idx => row[idx]);
function calculateLast(data: TableData, columnIndex: number, ignoreNulls: boolean, nullAsZero: boolean): ColumnStats {
return { last: data.rows[data.rows.length - 1][columnIndex] };
}
import { parseCSV } from './processTableData';
import { reduceTableData, getTableReducers, TableReducerID } from './tableReducer';
describe('Table Reducer', () => {
const basicTable = parseCSV('a,b,c\n10,20,30\n20,30,40');
it('should load all standard stats', () => {
const names = [
TableReducerID.sum,
TableReducerID.max,
TableReducerID.min,
TableReducerID.logmin,
TableReducerID.mean,
TableReducerID.last,
TableReducerID.first,
TableReducerID.count,
TableReducerID.range,
TableReducerID.diff,
TableReducerID.step,
TableReducerID.delta,
// TableReducerID.allIsZero,
// TableReducerID.allIsNull,
];
const notFound: string[] = [];
const reducers = getTableReducers(names, notFound);
reducers.forEach((reducer, index) => {
expect(reducer ? reducer.value : '<missing>').toEqual(names[index]);
});
expect(notFound.length).toBe(0);
});
it('should fail to load unknown reducers', () => {
const names = ['not a reducer', TableReducerID.max, TableReducerID.min, 'also not a reducer'];
const notFound: string[] = [];
const reducers = getTableReducers(names, notFound);
expect(reducers.length).toBe(2);
expect(notFound.length).toBe(2);
});
it('should calculate stats', () => {
const reduced = reduceTableData(basicTable, {
columnIndexes: [0, 1],
stats: ['first', 'last', 'mean'],
});
expect(reduced.length).toBe(3);
// First
expect(reduced[0].rows[0]).toEqual([10, 20]);
// Last
expect(reduced[1].rows[0]).toEqual([20, 30]);
// Mean
expect(reduced[2].rows[0]).toEqual([15, 25]);
});
it('should support a single stat also', () => {
// First
let reduced = reduceTableData(basicTable, {
columnIndexes: [0, 1],
stats: ['first'],
});
expect(reduced.length).toBe(1);
expect(reduced[0].rows[0]).toEqual([10, 20]);
// Last
reduced = reduceTableData(basicTable, {
columnIndexes: [0, 1],
stats: ['last'],
});
expect(reduced.length).toBe(1);
expect(reduced[0].rows[0]).toEqual([20, 30]);
// Mean
reduced = reduceTableData(basicTable, {
columnIndexes: [0, 1],
stats: ['mean'],
});
expect(reduced.length).toBe(1);
expect(reduced[0].rows[0]).toEqual([15, 25]);
});
});
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment