mirror of https://github.com/grafana/grafana
Merge branch 'table-reducer' of https://github.com/ryantxu/grafana into ryantxu-table-reducer
commit
e0ecbc4c68
@ -0,0 +1,79 @@ |
|||||||
|
import React, { PureComponent } from 'react'; |
||||||
|
|
||||||
|
import { storiesOf } from '@storybook/react'; |
||||||
|
import { action } from '@storybook/addon-actions'; |
||||||
|
import { withCenteredStory } from '../../utils/storybook/withCenteredStory'; |
||||||
|
import { StatsPicker } from './StatsPicker'; |
||||||
|
import { text, boolean } from '@storybook/addon-knobs'; |
||||||
|
|
||||||
|
const getKnobs = () => { |
||||||
|
return { |
||||||
|
placeholder: text('Placeholder Text', ''), |
||||||
|
defaultStat: text('Default Stat', ''), |
||||||
|
allowMultiple: boolean('Allow Multiple', false), |
||||||
|
initialStats: text('Initial Stats', ''), |
||||||
|
}; |
||||||
|
}; |
||||||
|
|
||||||
|
interface State { |
||||||
|
stats: string[]; |
||||||
|
} |
||||||
|
|
||||||
|
export class WrapperWithState extends PureComponent<any, State> { |
||||||
|
constructor(props: any) { |
||||||
|
super(props); |
||||||
|
this.state = { |
||||||
|
stats: this.toStatsArray(props.initialReducers), |
||||||
|
}; |
||||||
|
} |
||||||
|
|
||||||
|
toStatsArray = (txt: string): string[] => { |
||||||
|
if (!txt) { |
||||||
|
return []; |
||||||
|
} |
||||||
|
return txt.split(',').map(v => v.trim()); |
||||||
|
}; |
||||||
|
|
||||||
|
componentDidUpdate(prevProps: any) { |
||||||
|
const { initialReducers } = this.props; |
||||||
|
if (initialReducers !== prevProps.initialReducers) { |
||||||
|
console.log('Changing initial reducers'); |
||||||
|
this.setState({ stats: this.toStatsArray(initialReducers) }); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
render() { |
||||||
|
const { placeholder, defaultStat, allowMultiple } = this.props; |
||||||
|
const { stats } = this.state; |
||||||
|
|
||||||
|
return ( |
||||||
|
<StatsPicker |
||||||
|
placeholder={placeholder} |
||||||
|
defaultStat={defaultStat} |
||||||
|
allowMultiple={allowMultiple} |
||||||
|
stats={stats} |
||||||
|
onChange={(stats: string[]) => { |
||||||
|
action('Picked:')(stats); |
||||||
|
this.setState({ stats }); |
||||||
|
}} |
||||||
|
/> |
||||||
|
); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
const story = storiesOf('UI/StatsPicker', module); |
||||||
|
story.addDecorator(withCenteredStory); |
||||||
|
story.add('picker', () => { |
||||||
|
const { placeholder, defaultStat, allowMultiple, initialStats } = getKnobs(); |
||||||
|
|
||||||
|
return ( |
||||||
|
<div> |
||||||
|
<WrapperWithState |
||||||
|
placeholder={placeholder} |
||||||
|
defaultStat={defaultStat} |
||||||
|
allowMultiple={allowMultiple} |
||||||
|
initialStats={initialStats} |
||||||
|
/> |
||||||
|
</div> |
||||||
|
); |
||||||
|
}); |
||||||
@ -0,0 +1,98 @@ |
|||||||
|
import React, { PureComponent } from 'react'; |
||||||
|
|
||||||
|
import isArray from 'lodash/isArray'; |
||||||
|
import difference from 'lodash/difference'; |
||||||
|
|
||||||
|
import { Select } from '../index'; |
||||||
|
|
||||||
|
import { getStatsCalculators } from '../../utils/statsCalculator'; |
||||||
|
import { SelectOptionItem } from '../Select/Select'; |
||||||
|
|
||||||
|
interface Props { |
||||||
|
placeholder?: string; |
||||||
|
onChange: (stats: string[]) => void; |
||||||
|
stats: string[]; |
||||||
|
width?: number; |
||||||
|
allowMultiple?: boolean; |
||||||
|
defaultStat?: string; |
||||||
|
} |
||||||
|
|
||||||
|
export class StatsPicker extends PureComponent<Props> { |
||||||
|
static defaultProps = { |
||||||
|
width: 12, |
||||||
|
allowMultiple: false, |
||||||
|
}; |
||||||
|
|
||||||
|
componentDidMount() { |
||||||
|
this.checkInput(); |
||||||
|
} |
||||||
|
|
||||||
|
componentDidUpdate(prevProps: Props) { |
||||||
|
this.checkInput(); |
||||||
|
} |
||||||
|
|
||||||
|
checkInput = () => { |
||||||
|
const { stats, allowMultiple, defaultStat, onChange } = this.props; |
||||||
|
|
||||||
|
const current = getStatsCalculators(stats); |
||||||
|
if (current.length !== stats.length) { |
||||||
|
const found = current.map(v => v.id); |
||||||
|
const notFound = difference(stats, found); |
||||||
|
console.warn('Unknown stats', notFound, stats); |
||||||
|
onChange(current.map(stat => stat.id)); |
||||||
|
} |
||||||
|
|
||||||
|
// Make sure there is only one
|
||||||
|
if (!allowMultiple && stats.length > 1) { |
||||||
|
console.warn('Removing extra stat', stats); |
||||||
|
onChange([stats[0]]); |
||||||
|
} |
||||||
|
|
||||||
|
// Set the reducer from callback
|
||||||
|
if (defaultStat && stats.length < 1) { |
||||||
|
onChange([defaultStat]); |
||||||
|
} |
||||||
|
}; |
||||||
|
|
||||||
|
onSelectionChange = (item: SelectOptionItem) => { |
||||||
|
const { onChange } = this.props; |
||||||
|
if (isArray(item)) { |
||||||
|
onChange(item.map(v => v.value)); |
||||||
|
} else { |
||||||
|
onChange([item.value]); |
||||||
|
} |
||||||
|
}; |
||||||
|
|
||||||
|
render() { |
||||||
|
const { width, stats, allowMultiple, defaultStat, placeholder } = this.props; |
||||||
|
const options = getStatsCalculators().map(s => { |
||||||
|
return { |
||||||
|
value: s.id, |
||||||
|
label: s.name, |
||||||
|
description: s.description, |
||||||
|
}; |
||||||
|
}); |
||||||
|
|
||||||
|
const value: SelectOptionItem[] = []; |
||||||
|
stats.forEach(s => { |
||||||
|
const o = options.find(v => v.value === s); |
||||||
|
if (o) { |
||||||
|
value.push(o); |
||||||
|
} |
||||||
|
}); |
||||||
|
|
||||||
|
//getStatsCalculators(stats);
|
||||||
|
return ( |
||||||
|
<Select |
||||||
|
width={width} |
||||||
|
value={value} |
||||||
|
isClearable={!defaultStat} |
||||||
|
isMulti={allowMultiple} |
||||||
|
isSearchable={true} |
||||||
|
options={options} |
||||||
|
placeholder={placeholder} |
||||||
|
onChange={this.onSelectionChange} |
||||||
|
/> |
||||||
|
); |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,92 @@ |
|||||||
|
import { parseCSV } from './processTableData'; |
||||||
|
import { getStatsCalculators, StatID, calculateStats } from './statsCalculator'; |
||||||
|
|
||||||
|
import _ from 'lodash'; |
||||||
|
|
||||||
|
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 stats = getStatsCalculators(names); |
||||||
|
expect(stats.length).toBe(names.length); |
||||||
|
}); |
||||||
|
|
||||||
|
it('should fail to load unknown stats', () => { |
||||||
|
const names = ['not a stat', StatID.max, StatID.min, 'also not a stat']; |
||||||
|
const stats = getStatsCalculators(names); |
||||||
|
expect(stats.length).toBe(2); |
||||||
|
|
||||||
|
const found = stats.map(v => v.id); |
||||||
|
const notFound = _.difference(names, found); |
||||||
|
expect(notFound.length).toBe(2); |
||||||
|
|
||||||
|
expect(notFound[0]).toBe('not a stat'); |
||||||
|
}); |
||||||
|
|
||||||
|
it('should calculate basic stats', () => { |
||||||
|
const stats = calculateStats({ |
||||||
|
table: 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({ |
||||||
|
table: basicTable, |
||||||
|
columnIndex: 0, |
||||||
|
stats: ['first'], |
||||||
|
}); |
||||||
|
|
||||||
|
// Should do the simple version that just looks up value
|
||||||
|
expect(Object.keys(stats).length).toEqual(1); |
||||||
|
expect(stats.first).toEqual(10); |
||||||
|
}); |
||||||
|
|
||||||
|
it('should get non standard stats', () => { |
||||||
|
const stats = calculateStats({ |
||||||
|
table: basicTable, |
||||||
|
columnIndex: 0, |
||||||
|
stats: [StatID.distinctCount, StatID.changeCount], |
||||||
|
}); |
||||||
|
|
||||||
|
expect(stats.distinctCount).toEqual(2); |
||||||
|
expect(stats.changeCount).toEqual(1); |
||||||
|
}); |
||||||
|
|
||||||
|
it('should calculate step', () => { |
||||||
|
const stats = calculateStats({ |
||||||
|
table: { columns: [{ text: 'A' }], rows: [[100], [200], [300], [400]] }, |
||||||
|
columnIndex: 0, |
||||||
|
stats: [StatID.step, StatID.delta], |
||||||
|
}); |
||||||
|
|
||||||
|
expect(stats.step).toEqual(100); |
||||||
|
expect(stats.delta).toEqual(300); |
||||||
|
}); |
||||||
|
}); |
||||||
@ -0,0 +1,403 @@ |
|||||||
|
// Libraries
|
||||||
|
import isNumber from 'lodash/isNumber'; |
||||||
|
|
||||||
|
import { TableData, NullValueMode } from '../types/index'; |
||||||
|
|
||||||
|
export enum StatID { |
||||||
|
sum = 'sum', |
||||||
|
max = 'max', |
||||||
|
min = 'min', |
||||||
|
logmin = 'logmin', |
||||||
|
mean = 'mean', |
||||||
|
last = 'last', |
||||||
|
first = 'first', |
||||||
|
count = 'count', |
||||||
|
range = 'range', |
||||||
|
diff = 'diff', |
||||||
|
delta = 'delta', |
||||||
|
step = 'step', |
||||||
|
|
||||||
|
changeCount = 'changeCount', |
||||||
|
distinctCount = 'distinctCount', |
||||||
|
|
||||||
|
allIsZero = 'allIsZero', |
||||||
|
allIsNull = 'allIsNull', |
||||||
|
} |
||||||
|
|
||||||
|
export interface ColumnStats { |
||||||
|
[key: string]: any; |
||||||
|
} |
||||||
|
|
||||||
|
// Internal function
|
||||||
|
type StatCalculator = (table: TableData, columnIndex: number, ignoreNulls: boolean, nullAsZero: boolean) => ColumnStats; |
||||||
|
|
||||||
|
export interface StatCalculatorInfo { |
||||||
|
id: string; |
||||||
|
name: string; |
||||||
|
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
|
||||||
|
calculator?: StatCalculator; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* @param ids list of stat names or null to get all of them |
||||||
|
*/ |
||||||
|
export function getStatsCalculators(ids?: string[]): StatCalculatorInfo[] { |
||||||
|
if (ids === null || ids === undefined) { |
||||||
|
if (!hasBuiltIndex) { |
||||||
|
getById(StatID.mean); |
||||||
|
} |
||||||
|
return listOfStats; |
||||||
|
} |
||||||
|
return ids.reduce((list, id) => { |
||||||
|
const stat = getById(id); |
||||||
|
if (stat) { |
||||||
|
list.push(stat); |
||||||
|
} |
||||||
|
return list; |
||||||
|
}, new Array<StatCalculatorInfo>()); |
||||||
|
} |
||||||
|
|
||||||
|
export interface CalculateStatsOptions { |
||||||
|
table: TableData; |
||||||
|
columnIndex: number; |
||||||
|
stats: string[]; // The stats to calculate
|
||||||
|
nullValueMode?: NullValueMode; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* @returns an object with a key for each selected stat |
||||||
|
*/ |
||||||
|
export function calculateStats(options: CalculateStatsOptions): ColumnStats { |
||||||
|
const { table, columnIndex, stats, nullValueMode } = options; |
||||||
|
|
||||||
|
if (!stats || stats.length < 1) { |
||||||
|
return {}; |
||||||
|
} |
||||||
|
|
||||||
|
const queue = getStatsCalculators(stats); |
||||||
|
|
||||||
|
// Return early for empty tables
|
||||||
|
// This lets the concrete implementations assume at least one row
|
||||||
|
if (!table.rows || table.rows.length < 1) { |
||||||
|
const stats = {} as ColumnStats; |
||||||
|
for (const stat of queue) { |
||||||
|
stats[stat.id] = 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].calculator) { |
||||||
|
return queue[0].calculator(table, columnIndex, ignoreNulls, nullAsZero); |
||||||
|
} |
||||||
|
|
||||||
|
// For now everything can use the standard stats
|
||||||
|
let values = standardStatsStat(table, columnIndex, ignoreNulls, nullAsZero); |
||||||
|
for (const calc of queue) { |
||||||
|
if (!values.hasOwnProperty(calc.id) && calc.calculator) { |
||||||
|
values = { |
||||||
|
...values, |
||||||
|
...calc.calculator(table, columnIndex, ignoreNulls, nullAsZero), |
||||||
|
}; |
||||||
|
} |
||||||
|
} |
||||||
|
return values; |
||||||
|
} |
||||||
|
|
||||||
|
// ------------------------------------------------------------------------------
|
||||||
|
//
|
||||||
|
// No Exported symbols below here.
|
||||||
|
//
|
||||||
|
// ------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
// private registry of all stats
|
||||||
|
interface TableStatIndex { |
||||||
|
[id: string]: StatCalculatorInfo; |
||||||
|
} |
||||||
|
const listOfStats: StatCalculatorInfo[] = []; |
||||||
|
const index: TableStatIndex = {}; |
||||||
|
let hasBuiltIndex = false; |
||||||
|
|
||||||
|
function getById(id: string): StatCalculatorInfo | undefined { |
||||||
|
if (!hasBuiltIndex) { |
||||||
|
[ |
||||||
|
{ |
||||||
|
id: StatID.last, |
||||||
|
name: 'Last', |
||||||
|
description: 'Last Value (current)', |
||||||
|
standard: true, |
||||||
|
alias: 'current', |
||||||
|
calculator: calculateLast, |
||||||
|
}, |
||||||
|
{ id: StatID.first, name: 'First', description: 'First Value', standard: true, calculator: calculateFirst }, |
||||||
|
{ id: StatID.min, name: 'Min', description: 'Minimum Value', standard: true }, |
||||||
|
{ id: StatID.max, name: 'Max', description: 'Maximum Value', standard: true }, |
||||||
|
{ id: StatID.mean, name: 'Mean', description: 'Average Value', standard: true, alias: 'avg' }, |
||||||
|
{ |
||||||
|
id: StatID.sum, |
||||||
|
name: 'Total', |
||||||
|
description: 'The sum of all values', |
||||||
|
emptyInputResult: 0, |
||||||
|
standard: true, |
||||||
|
alias: 'total', |
||||||
|
}, |
||||||
|
{ |
||||||
|
id: StatID.count, |
||||||
|
name: 'Count', |
||||||
|
description: 'Number of values in response', |
||||||
|
emptyInputResult: 0, |
||||||
|
standard: true, |
||||||
|
}, |
||||||
|
{ |
||||||
|
id: StatID.range, |
||||||
|
name: 'Range', |
||||||
|
description: 'Difference between minimum and maximum values', |
||||||
|
standard: true, |
||||||
|
}, |
||||||
|
{ |
||||||
|
id: StatID.delta, |
||||||
|
name: 'Delta', |
||||||
|
description: 'Cumulative change in value (??? help not really sure ???)', |
||||||
|
standard: true, |
||||||
|
}, |
||||||
|
{ |
||||||
|
id: StatID.step, |
||||||
|
name: 'Step', |
||||||
|
description: 'Minimum interval between values', |
||||||
|
standard: true, |
||||||
|
}, |
||||||
|
{ |
||||||
|
id: StatID.diff, |
||||||
|
name: 'Difference', |
||||||
|
description: 'Difference between first and last values', |
||||||
|
standard: true, |
||||||
|
}, |
||||||
|
{ |
||||||
|
id: StatID.logmin, |
||||||
|
name: 'Min (above zero)', |
||||||
|
description: 'Used for log min scale', |
||||||
|
standard: true, |
||||||
|
}, |
||||||
|
{ |
||||||
|
id: StatID.changeCount, |
||||||
|
name: 'Change Count', |
||||||
|
description: 'Number of times the value changes', |
||||||
|
standard: false, |
||||||
|
calculator: calculateChangeCount, |
||||||
|
}, |
||||||
|
{ |
||||||
|
id: StatID.distinctCount, |
||||||
|
name: 'Distinct Count', |
||||||
|
description: 'Number of distinct values', |
||||||
|
standard: false, |
||||||
|
calculator: calculateDistinctCount, |
||||||
|
}, |
||||||
|
].forEach(info => { |
||||||
|
const { id, alias } = info; |
||||||
|
if (index.hasOwnProperty(id)) { |
||||||
|
console.warn('Duplicate Stat', id, info, index); |
||||||
|
} |
||||||
|
index[id] = info; |
||||||
|
if (alias) { |
||||||
|
if (index.hasOwnProperty(alias)) { |
||||||
|
console.warn('Duplicate Stat (alias)', alias, info, index); |
||||||
|
} |
||||||
|
index[alias] = info; |
||||||
|
} |
||||||
|
listOfStats.push(info); |
||||||
|
}); |
||||||
|
hasBuiltIndex = true; |
||||||
|
} |
||||||
|
return index[id]; |
||||||
|
} |
||||||
|
|
||||||
|
function standardStatsStat( |
||||||
|
data: TableData, |
||||||
|
columnIndex: number, |
||||||
|
ignoreNulls: boolean, |
||||||
|
nullAsZero: boolean |
||||||
|
): ColumnStats { |
||||||
|
const stats = { |
||||||
|
sum: 0, |
||||||
|
max: -Number.MAX_VALUE, |
||||||
|
min: Number.MAX_VALUE, |
||||||
|
logmin: Number.MAX_VALUE, |
||||||
|
mean: null, |
||||||
|
last: null, |
||||||
|
first: null, |
||||||
|
count: 0, |
||||||
|
nonNullCount: 0, |
||||||
|
allIsNull: true, |
||||||
|
allIsZero: false, |
||||||
|
range: null, |
||||||
|
diff: null, |
||||||
|
delta: 0, |
||||||
|
step: Number.MAX_VALUE, |
||||||
|
|
||||||
|
// Just used for calcutations -- not exposed as a stat
|
||||||
|
previousDeltaUp: true, |
||||||
|
} as ColumnStats; |
||||||
|
|
||||||
|
for (let i = 0; i < data.rows.length; i++) { |
||||||
|
let currentValue = data.rows[i][columnIndex]; |
||||||
|
|
||||||
|
if (currentValue === null) { |
||||||
|
if (ignoreNulls) { |
||||||
|
continue; |
||||||
|
} |
||||||
|
if (nullAsZero) { |
||||||
|
currentValue = 0; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
if (currentValue !== null) { |
||||||
|
const isFirst = stats.first === null; |
||||||
|
if (isFirst) { |
||||||
|
stats.first = currentValue; |
||||||
|
} |
||||||
|
|
||||||
|
if (isNumber(currentValue)) { |
||||||
|
stats.sum += currentValue; |
||||||
|
stats.allIsNull = false; |
||||||
|
stats.nonNullCount++; |
||||||
|
|
||||||
|
if (!isFirst) { |
||||||
|
const step = currentValue - stats.last!; |
||||||
|
if (stats.step > step) { |
||||||
|
stats.step = step; // the minimum interval
|
||||||
|
} |
||||||
|
|
||||||
|
if (stats.last! > currentValue) { |
||||||
|
// counter reset
|
||||||
|
stats.previousDeltaUp = false; |
||||||
|
if (i === data.rows.length - 1) { |
||||||
|
// reset on last
|
||||||
|
stats.delta += currentValue; |
||||||
|
} |
||||||
|
} else { |
||||||
|
if (stats.previousDeltaUp) { |
||||||
|
stats.delta += step; // normal increment
|
||||||
|
} else { |
||||||
|
stats.delta += currentValue; // account for counter reset
|
||||||
|
} |
||||||
|
stats.previousDeltaUp = true; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
if (currentValue > stats.max) { |
||||||
|
stats.max = currentValue; |
||||||
|
} |
||||||
|
|
||||||
|
if (currentValue < stats.min) { |
||||||
|
stats.min = currentValue; |
||||||
|
} |
||||||
|
|
||||||
|
if (currentValue < stats.logmin && currentValue > 0) { |
||||||
|
stats.logmin = currentValue; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
if (currentValue !== 0) { |
||||||
|
stats.allIsZero = false; |
||||||
|
} |
||||||
|
|
||||||
|
stats.last = currentValue; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
if (stats.max === -Number.MAX_VALUE) { |
||||||
|
stats.max = null; |
||||||
|
} |
||||||
|
|
||||||
|
if (stats.min === Number.MAX_VALUE) { |
||||||
|
stats.min = null; |
||||||
|
} |
||||||
|
|
||||||
|
if (stats.step === Number.MAX_VALUE) { |
||||||
|
stats.step = null; |
||||||
|
} |
||||||
|
|
||||||
|
if (stats.nonNullCount > 0) { |
||||||
|
stats.mean = stats.sum! / stats.nonNullCount; |
||||||
|
} |
||||||
|
|
||||||
|
if (stats.max !== null && stats.min !== null) { |
||||||
|
stats.range = stats.max - stats.min; |
||||||
|
} |
||||||
|
|
||||||
|
if (stats.first !== null && stats.last !== null) { |
||||||
|
if (isNumber(stats.first) && isNumber(stats.last)) { |
||||||
|
stats.diff = stats.last - stats.first; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return stats; |
||||||
|
} |
||||||
|
|
||||||
|
function calculateFirst(data: TableData, columnIndex: number, ignoreNulls: boolean, nullAsZero: boolean): ColumnStats { |
||||||
|
return { first: data.rows[0][columnIndex] }; |
||||||
|
} |
||||||
|
|
||||||
|
function calculateLast(data: TableData, columnIndex: number, ignoreNulls: boolean, nullAsZero: boolean): ColumnStats { |
||||||
|
return { last: data.rows[data.rows.length - 1][columnIndex] }; |
||||||
|
} |
||||||
|
|
||||||
|
function calculateChangeCount( |
||||||
|
data: TableData, |
||||||
|
columnIndex: number, |
||||||
|
ignoreNulls: boolean, |
||||||
|
nullAsZero: boolean |
||||||
|
): ColumnStats { |
||||||
|
let count = 0; |
||||||
|
let first = true; |
||||||
|
let last: any = null; |
||||||
|
for (let i = 0; i < data.rows.length; i++) { |
||||||
|
let currentValue = data.rows[i][columnIndex]; |
||||||
|
if (currentValue === null) { |
||||||
|
if (ignoreNulls) { |
||||||
|
continue; |
||||||
|
} |
||||||
|
if (nullAsZero) { |
||||||
|
currentValue = 0; |
||||||
|
} |
||||||
|
} |
||||||
|
if (!first && last !== currentValue) { |
||||||
|
count++; |
||||||
|
} |
||||||
|
first = false; |
||||||
|
last = currentValue; |
||||||
|
} |
||||||
|
|
||||||
|
return { changeCount: count }; |
||||||
|
} |
||||||
|
|
||||||
|
function calculateDistinctCount( |
||||||
|
data: TableData, |
||||||
|
columnIndex: number, |
||||||
|
ignoreNulls: boolean, |
||||||
|
nullAsZero: boolean |
||||||
|
): ColumnStats { |
||||||
|
const distinct = new Set<any>(); |
||||||
|
for (let i = 0; i < data.rows.length; i++) { |
||||||
|
let currentValue = data.rows[i][columnIndex]; |
||||||
|
if (currentValue === null) { |
||||||
|
if (ignoreNulls) { |
||||||
|
continue; |
||||||
|
} |
||||||
|
if (nullAsZero) { |
||||||
|
currentValue = 0; |
||||||
|
} |
||||||
|
} |
||||||
|
distinct.add(currentValue); |
||||||
|
} |
||||||
|
return { distinctCount: distinct.size }; |
||||||
|
} |
||||||
Loading…
Reference in new issue