more options in storybook

This commit is contained in:
ryan
2019-03-12 14:26:07 -07:00
parent 9016c18088
commit 0446fb9171
3 changed files with 163 additions and 75 deletions
@@ -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}
/>
);
+101 -53
View File
@@ -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[],