more options in storybook
This commit is contained in:
@@ -4,6 +4,7 @@ import { storiesOf } from '@storybook/react';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
import { withCenteredStory } from '../../utils/storybook/withCenteredStory';
|
||||
import { TableReducePicker } from './TableReducePicker';
|
||||
import { text, boolean } from '@storybook/addon-knobs';
|
||||
|
||||
interface State {
|
||||
reducers: string[];
|
||||
@@ -13,17 +14,33 @@ export class WrapperWithState extends PureComponent<any, State> {
|
||||
constructor(props: any) {
|
||||
super(props);
|
||||
this.state = {
|
||||
reducers: [], // nothing?
|
||||
reducers: this.toReducersArray(props.initialReducers),
|
||||
};
|
||||
}
|
||||
|
||||
toReducersArray = (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) });
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { placeholder, defaultReducer, allowMultiple } = this.props;
|
||||
const { reducers } = this.state;
|
||||
|
||||
return (
|
||||
<TableReducePicker
|
||||
{...this.props}
|
||||
reducers={this.state.reducers}
|
||||
placeholder={placeholder}
|
||||
defaultReducer={defaultReducer}
|
||||
allowMultiple={allowMultiple}
|
||||
reducers={reducers}
|
||||
onChange={(reducers: string[]) => {
|
||||
action('Reduce')(reducers);
|
||||
action('Picked:')(reducers);
|
||||
this.setState({ reducers });
|
||||
}}
|
||||
/>
|
||||
@@ -33,10 +50,19 @@ export class WrapperWithState extends PureComponent<any, State> {
|
||||
|
||||
const story = storiesOf('UI/TableReducePicker', module);
|
||||
story.addDecorator(withCenteredStory);
|
||||
story.add('default', () => {
|
||||
story.add('picker', () => {
|
||||
const placeholder = text('Placeholder Text', '');
|
||||
const defaultReducer = text('Default Reducer', '');
|
||||
const allowMultiple = boolean('Allow Multiple', false);
|
||||
const initialReducers = text('Initial Reducers', '');
|
||||
return (
|
||||
<div>
|
||||
<WrapperWithState />
|
||||
<WrapperWithState
|
||||
placeholder={placeholder}
|
||||
defaultReducer={defaultReducer}
|
||||
allowMultiple={allowMultiple}
|
||||
initialReducers={initialReducers}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -8,14 +8,38 @@ import { getTableReducers } from '../../utils/tableReducer';
|
||||
import { SelectOptionItem } from '../Select/Select';
|
||||
|
||||
interface Props {
|
||||
placeholder?: string;
|
||||
onChange: (reducers: string[]) => void;
|
||||
reducers: string[];
|
||||
width?: number;
|
||||
allowMultiple?: boolean;
|
||||
defaultReducer?: string;
|
||||
}
|
||||
|
||||
export class TableReducePicker extends PureComponent<Props> {
|
||||
static defaultProps = {
|
||||
width: 12,
|
||||
allowMultiple: false,
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
console.log('MOUNT!', this);
|
||||
this.checkInput();
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps: Props) {
|
||||
console.log('UPDATE', this);
|
||||
this.checkInput();
|
||||
}
|
||||
|
||||
checkInput = () => {
|
||||
const { reducers, allowMultiple, defaultReducer, onChange } = this.props;
|
||||
if (!allowMultiple && reducers.length > 1) {
|
||||
onChange(reducers.slice(0, 1));
|
||||
}
|
||||
if (defaultReducer && reducers.length < 1) {
|
||||
onChange([defaultReducer]);
|
||||
}
|
||||
};
|
||||
|
||||
onSelectionChange = (item: SelectOptionItem) => {
|
||||
@@ -28,28 +52,18 @@ export class TableReducePicker extends PureComponent<Props> {
|
||||
};
|
||||
|
||||
render() {
|
||||
const { width, reducers } = this.props;
|
||||
const { width, reducers, allowMultiple, defaultReducer, placeholder } = this.props;
|
||||
const current = getTableReducers(reducers);
|
||||
|
||||
const allReducers = getTableReducers();
|
||||
|
||||
// Need to transform the data structure to work well with Select
|
||||
const reducerOptions = allReducers.map(info => {
|
||||
return {
|
||||
label: info.name,
|
||||
value: info.key,
|
||||
description: info.description,
|
||||
};
|
||||
});
|
||||
|
||||
const current = reducerOptions.filter(options => reducers.includes(options.value));
|
||||
return (
|
||||
<Select
|
||||
width={width}
|
||||
value={current}
|
||||
isMulti={true}
|
||||
isClearable={!defaultReducer}
|
||||
isMulti={allowMultiple}
|
||||
isSearchable={true}
|
||||
options={reducerOptions}
|
||||
placeholder="Choose"
|
||||
options={getTableReducers()}
|
||||
placeholder={placeholder}
|
||||
onChange={this.onSelectionChange}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -3,22 +3,44 @@ import isNumber from 'lodash/isNumber';
|
||||
|
||||
import { TableData, NullValueMode } from '../types/index';
|
||||
|
||||
/** Reduce each column in a table to a single value */
|
||||
type TableReducer = (data: TableData, columnIndexes: number[], ignoreNulls: boolean, nullAsZero: boolean) => any[];
|
||||
export enum TableReducerID {
|
||||
sum = 'sum',
|
||||
max = 'max',
|
||||
min = 'min',
|
||||
logmin = 'logmin',
|
||||
mean = 'mean',
|
||||
last = 'last',
|
||||
first = 'first',
|
||||
count = 'count',
|
||||
range = 'range',
|
||||
diff = 'diff',
|
||||
|
||||
/** Information about the reducing(stats) functions */
|
||||
allIsZero = 'allIsZero',
|
||||
allIsNull = 'allIsNull',
|
||||
}
|
||||
|
||||
/** Information about the reducing(stats) functions */
|
||||
export interface TableReducerInfo {
|
||||
key: string;
|
||||
name: string;
|
||||
value: string; // The ID - value maps directly to select component
|
||||
label: string; // The name - label
|
||||
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;
|
||||
alias?: string; // optional secondary key. 'avg' vs 'mean'
|
||||
}
|
||||
|
||||
/** Get a list of the known reducing functions */
|
||||
export function getTableReducers(): TableReducerInfo[] {
|
||||
return reducers;
|
||||
export function getTableReducers(ids?: string[]): TableReducerInfo[] {
|
||||
if (!hasBuiltIndex) {
|
||||
getById(TableReducerID.sum); // Force the registry to load
|
||||
}
|
||||
if (ids === null || ids === undefined) {
|
||||
return listOfReducers;
|
||||
}
|
||||
return ids.map(id => getById(id));
|
||||
}
|
||||
|
||||
export interface TableReducerOptions {
|
||||
@@ -34,39 +56,22 @@ export function reduceTableData(data: TableData, options: TableReducerOptions):
|
||||
const ignoreNulls = options.nullValueMode === NullValueMode.Ignore;
|
||||
const nullAsZero = options.nullValueMode === NullValueMode.AsZero;
|
||||
|
||||
const queue = getTableReducers(options.stats);
|
||||
|
||||
// Return early for empty tables
|
||||
// This lets the concrete implementations assume at least one row
|
||||
if (!data.rows || data.rows.length < 1) {
|
||||
const val = nullAsZero ? 0 : null;
|
||||
const rows = [indexes.map(v => val)];
|
||||
return options.stats.map(stat => {
|
||||
return queue.map(stat => {
|
||||
return {
|
||||
columns,
|
||||
rows,
|
||||
rows: [indexes.map(v => stat.emptyInputResult)],
|
||||
type: 'table',
|
||||
columnMap: {},
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
if (registry == null) {
|
||||
registry = new Map<string, TableReducerInfo>();
|
||||
reducers.forEach(calc => {
|
||||
registry!.set(calc.key, calc);
|
||||
if (calc.alias) {
|
||||
registry!.set(calc.alias, calc);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const queue = options.stats.map(key => {
|
||||
const c = registry!.get(key);
|
||||
if (!c) {
|
||||
throw new Error('Unknown stats calculator: ' + key);
|
||||
}
|
||||
return c;
|
||||
});
|
||||
|
||||
// Avoid the standard calculator if possible
|
||||
// Avoid calculating all the standard stats if possible
|
||||
if (queue.length === 1 && queue[0].reducer) {
|
||||
return [
|
||||
{
|
||||
@@ -82,7 +87,7 @@ export function reduceTableData(data: TableData, options: TableReducerOptions):
|
||||
const standard = standardStatsReducer(data, indexes, ignoreNulls, nullAsZero);
|
||||
return queue.map(calc => {
|
||||
const values = calc.standard
|
||||
? standard.map((s: any) => s[calc.key])
|
||||
? standard.map((s: any) => s[calc.value])
|
||||
: calc.reducer!(data, indexes, ignoreNulls, nullAsZero);
|
||||
return {
|
||||
columns,
|
||||
@@ -99,6 +104,70 @@ 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;
|
||||
}
|
||||
const listOfReducers: TableReducerInfo[] = [];
|
||||
const index: TableReducerIndex = {};
|
||||
let hasBuiltIndex = false;
|
||||
|
||||
function getById(id: string): TableReducerInfo {
|
||||
if (!hasBuiltIndex) {
|
||||
[
|
||||
{
|
||||
value: TableReducerID.last,
|
||||
label: 'Last',
|
||||
description: 'Last Value (current)',
|
||||
standard: true,
|
||||
alias: 'current',
|
||||
reducer: getLastRow,
|
||||
},
|
||||
{ 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: TableReducerID.sum,
|
||||
label: 'Total',
|
||||
description: 'The sum of all values',
|
||||
standard: true,
|
||||
alias: 'total',
|
||||
},
|
||||
{ value: TableReducerID.count, label: 'Count', description: 'Value Count', standard: true },
|
||||
{
|
||||
value: TableReducerID.range,
|
||||
label: 'Range',
|
||||
description: 'Difference between minimum and maximum values',
|
||||
standard: true,
|
||||
},
|
||||
{
|
||||
value: TableReducerID.diff,
|
||||
label: 'Difference',
|
||||
description: 'Difference between first and last values',
|
||||
standard: true,
|
||||
},
|
||||
].forEach(calc => {
|
||||
const { value, alias } = calc;
|
||||
if (index.hasOwnProperty(value)) {
|
||||
console.warn('Duplicate Reducer', value, calc, index);
|
||||
}
|
||||
index[value] = calc;
|
||||
if (alias) {
|
||||
if (index.hasOwnProperty(alias)) {
|
||||
console.warn('Duplicate Reducer (alias)', alias, calc, index);
|
||||
}
|
||||
index[alias] = calc;
|
||||
}
|
||||
listOfReducers.push(calc);
|
||||
});
|
||||
hasBuiltIndex = true;
|
||||
}
|
||||
return index[id];
|
||||
}
|
||||
|
||||
/**
|
||||
* This will return an array of valid indexes and throw an error if invalid request
|
||||
*/
|
||||
@@ -132,27 +201,6 @@ interface StandardStats {
|
||||
allIsNull: boolean;
|
||||
}
|
||||
|
||||
const reducers: TableReducerInfo[] = [
|
||||
{ key: 'sum', alias: 'total', name: 'Total', description: 'The sum of all values', standard: true },
|
||||
{ key: 'min', name: 'Min', description: 'Minimum Value', standard: true },
|
||||
{ key: 'max', name: 'Max', description: 'Maximum Value', standard: true },
|
||||
{ key: 'mean', name: 'Mean', description: 'Average Value', standard: true, alias: 'avg' },
|
||||
{ key: 'first', name: 'First', description: 'First Value', standard: true, reducer: getFirstRow },
|
||||
{
|
||||
key: 'last',
|
||||
name: 'Last',
|
||||
description: 'Last Value (current)',
|
||||
standard: true,
|
||||
alias: 'current',
|
||||
reducer: getLastRow,
|
||||
},
|
||||
{ key: 'count', name: 'Count', description: 'Value Count', standard: true },
|
||||
{ key: 'range', name: 'Range', description: 'Difference between minimum and maximum values', standard: true },
|
||||
{ key: 'diff', name: 'Difference', description: 'Difference between first and last values', standard: true },
|
||||
];
|
||||
|
||||
let registry: Map<string, TableReducerInfo> | null = null;
|
||||
|
||||
function standardStatsReducer(
|
||||
data: TableData,
|
||||
columnIndexes: number[],
|
||||
|
||||
Reference in New Issue
Block a user