UNPKG

virool-pivot

Version:

A web-based exploratory visualization UI for Druid.io

297 lines (244 loc) 11.3 kB
import { TimeBucketAction, NumberBucketAction, ActionJS, Action, ActionValue, TimeRange, Duration, PlywoodRange } from 'plywood'; import { day, hour, minute } from 'chronoshift'; import { hasOwnProperty, findFirstBiggerIndex, findExactIndex, findMaxValueIndex, findMinValueIndex, toSignificantDigits, getNumberOfWholeDigits, findBiggerClosestToIdeal } from '../../../common/utils/general/general'; const MENU_LENGTH = 5; export type Granularity = TimeBucketAction | NumberBucketAction; export type GranularityJS = string | number | ActionJS; export type BucketUnit = Duration | number; export type ContinuousDimensionKind = 'time' | 'number'; export interface Checker { checkPoint: number; returnValue: GranularityJS; } function makeCheckpoint(checkPoint: number, returnValue: GranularityJS): Checker { return { checkPoint, returnValue }; } function makeNumberBuckets(centerAround: number, count: number, coarse?: boolean): Granularity[] { var granularities: Granularity[] = []; var logTen = Math.log(centerAround) / Math.LN10; var digits = getNumberOfWholeDigits(centerAround); while (granularities.length <= count) { if (!coarse) { var halfStep = toSignificantDigits(5 * Math.pow(10, logTen - 1), digits); granularities.push(granularityFromJS(halfStep)); } if (granularities.length >= count) break; var wholeStep = toSignificantDigits(Math.pow(10, logTen), digits); granularities.push(granularityFromJS(wholeStep)); logTen++; } return granularities; } function makeNumberBucketsSimple() { var granularities: Granularity[] = []; for (var i = 3; i > -2; i--) { granularities.push(granularityFromJS(Math.pow(10, i))); } return granularities; } function days(count: number) { return count * day.canonicalLength; } function hours(count: number) { return count * hour.canonicalLength; } function minutes(count: number) { return count * minute.canonicalLength; } export interface Helper { dimensionKind: ContinuousDimensionKind; minGranularity: Granularity; defaultGranularity: Granularity; defaultGranularities: Granularity[]; supportedGranularities: Granularity[]; checkers: Checker[]; coarseCheckers?: Checker[]; coarseGranularities?: Granularity[]; } export class TimeHelper { static dimensionKind: ContinuousDimensionKind = 'time'; static minGranularity = granularityFromJS('PT1M'); static defaultGranularity = granularityFromJS('P1D'); static supportedGranularities = function(bucketedBy: Granularity) { return [ 'PT1S', 'PT1M', 'PT5M', 'PT15M', 'PT1H', 'PT6H', 'PT8H', 'PT12H', 'P1D', 'P1W', 'P1M', 'P3M', 'P6M', 'P1Y', 'P2Y' ].map(granularityFromJS); }; static checkers = [ makeCheckpoint(days(95), 'P1W'), makeCheckpoint(days(8), 'P1D'), makeCheckpoint(hours(8), 'PT1H'), makeCheckpoint(hours(3), 'PT5M')]; static coarseCheckers = [ makeCheckpoint(days(95), 'P1M'), makeCheckpoint(days(20), 'P1W'), makeCheckpoint(days(6), 'P1D'), makeCheckpoint(days(2), 'PT12H'), makeCheckpoint(hours(23), 'PT6H'), makeCheckpoint(hours(3), 'PT1H'), makeCheckpoint(minutes(30), 'PT5M') ]; static defaultGranularities = TimeHelper.checkers.map((c) => { return granularityFromJS(c.returnValue); }).concat(TimeHelper.minGranularity); static coarseGranularities = TimeHelper.coarseCheckers.map((c) => { return granularityFromJS(c.returnValue); }).concat(TimeHelper.minGranularity); } export class NumberHelper { static dimensionKind: ContinuousDimensionKind = 'number'; static minGranularity = granularityFromJS(1); static defaultGranularity = granularityFromJS(10); static checkers = makeNumberBucketsSimple().map((v: NumberBucketAction) => makeCheckpoint(v.size, v)); static defaultGranularities = NumberHelper.checkers.map((c: any) => { return granularityFromJS(c.checkPoint); }).reverse(); static coarseGranularities: Granularity[] = null; static coarseCheckers: Checker[] = null; static supportedGranularities = (bucketedBy: Granularity) => { return makeNumberBuckets(getBucketSize(bucketedBy), 10); }; } function getHelperForKind(kind: ContinuousDimensionKind) { if (kind === 'time') return TimeHelper; return NumberHelper; } function getHelperForRange(input: PlywoodRange) { if (input instanceof TimeRange) return TimeHelper; return NumberHelper; } function getBucketSize(input: Granularity): number { if (input instanceof TimeBucketAction) return input.duration.getCanonicalLength(); if (input instanceof NumberBucketAction) return input.size; throw new Error(`unrecognized granularity: ${input} must be of type TimeBucketAction or NumberBucketAction`); } function getBucketUnit(input: Granularity): BucketUnit { if (input instanceof TimeBucketAction) return input.duration; if (input instanceof NumberBucketAction) return input.size; throw new Error(`unrecognized granularity: ${input} must be of type TimeBucketAction or NumberBucketAction`); } function bucketUnitToGranularity(input: BucketUnit): Granularity { if (input instanceof Duration) { return new TimeBucketAction({ duration: input }); } else if (!isNaN(input)) { return new NumberBucketAction({ size: input, offset: 0 }); } throw new Error(`unrecognized bucket unit: ${input} must be of type number or Duration`); } function startValue(input: PlywoodRange): number { return input instanceof TimeRange ? input.start.valueOf() : input.start as number; } function endValue(input: PlywoodRange): number { return input instanceof TimeRange ? input.end.valueOf() : input.end as number; } function findBestMatch(array: Granularity[], target: Granularity) { var exactMatch = findExactIndex(array, target, getBucketSize); if (exactMatch !== -1) { return array[exactMatch]; } var minBiggerIdx = findFirstBiggerIndex(array, target, getBucketSize); if (minBiggerIdx !== -1) { return array[minBiggerIdx]; } return array[findMaxValueIndex(array, getBucketSize)]; } function generateGranularitySet(allGranularities: Granularity[], bucketedBy: Granularity) { var start = findFirstBiggerIndex(allGranularities, bucketedBy, getBucketSize); var returnGranularities = allGranularities.slice(start, start + MENU_LENGTH); // makes sure the bucket is part of the list if (findExactIndex(returnGranularities, bucketedBy, getBucketSize) === -1) { returnGranularities = [bucketedBy].concat(returnGranularities.slice(0, returnGranularities.length - 1)); } return returnGranularities; } export function granularityFromJS(input: GranularityJS): Granularity { if (typeof input === 'number') return NumberBucketAction.fromJS({ size: input }); if (typeof input === 'string') return TimeBucketAction.fromJS({ duration: input }); if (typeof input === "object") { if (!hasOwnProperty(input, 'action')) { throw new Error(`could not recognize object as action`); } return (Action.fromJS(input as GranularityJS) as Granularity); } throw new Error(`input should be of type number, string, or action`); } export function granularityToString(input: Granularity): string { if (input instanceof TimeBucketAction) { return input.duration.toString(); } else if (input instanceof NumberBucketAction) { return input.size.toString(); } throw new Error(`unrecognized granularity: ${input} must be of type TimeBucketAction or NumberBucketAction`); } export function granularityEquals(g1: Granularity, g2: Granularity) { if (!Boolean(g1) === Boolean(g2)) return false; if (g1 === g2 ) return true; return (g1 as Action).equals(g2 as Action); } export function granularityToJS(input: Granularity): GranularityJS { var js = input.toJS(); if (js.action === 'timeBucket') { if (Object.keys(js).length === 2) return js.duration; } if (js.action === 'numberBucket') { if (Object.keys(js).length === 2) return js.size; } return js; } export function updateBucketSize(existing: Granularity, newInput: Granularity): Granularity { if (newInput instanceof TimeBucketAction) { return new TimeBucketAction({ duration: (newInput as TimeBucketAction).duration, timezone: (existing as TimeBucketAction).timezone }); } else if (newInput instanceof NumberBucketAction) { var value: ActionValue = { size: (newInput as NumberBucketAction).size }; if ((existing as NumberBucketAction).offset) value.offset = (existing as NumberBucketAction).offset; return new NumberBucketAction(value); } throw new Error(`unrecognized granularity: ${newInput} must be of type TimeBucket or NumberBucket`); } export function getGranularities(kind: ContinuousDimensionKind, bucketedBy?: Granularity, coarse?: boolean): Granularity[] { var helper = getHelperForKind(kind); var coarseGranularities = helper.coarseGranularities; if (!bucketedBy) return coarse && coarseGranularities ? coarseGranularities : helper.defaultGranularities; // make list that makes most sense with bucket var allGranularities = helper.supportedGranularities(bucketedBy); return generateGranularitySet(allGranularities, bucketedBy); } export function getDefaultGranularityForKind(kind: ContinuousDimensionKind, bucketedBy?: Granularity, customGranularities?: Granularity[]): Granularity { if (bucketedBy) return bucketedBy; if (customGranularities)return customGranularities[2]; return getHelperForKind(kind).defaultGranularity; } export function getBestGranularityForRange(inputRange: PlywoodRange, bigChecker: boolean, bucketedBy?: Granularity, customGranularities?: Granularity[]): Granularity { return bucketUnitToGranularity(getBestBucketUnitForRange(inputRange, bigChecker, bucketedBy, customGranularities)); } export function getBestBucketUnitForRange(inputRange: PlywoodRange, bigChecker: boolean, bucketedBy?: Granularity, customGranularities?: Granularity[]): BucketUnit { var rangeLength = Math.abs(endValue(inputRange) - startValue(inputRange)); var helper = getHelperForRange(inputRange); var bucketLength = bucketedBy ? getBucketSize(bucketedBy) : 0; var checkPoints = bigChecker && helper.coarseCheckers ? helper.coarseCheckers : helper.checkers; for (var i = 0; i < checkPoints.length; i++) { var checkPoint = checkPoints[i].checkPoint; var returnVal = granularityFromJS(checkPoints[i].returnValue); if (rangeLength > checkPoint || bucketLength > checkPoint) { if (bucketedBy) { var granArray = customGranularities || getGranularities(helper.dimensionKind, bucketedBy); var closest = findBiggerClosestToIdeal(granArray, bucketedBy, returnVal, getBucketSize); // this could happen if bucketedBy were very big or if custom granularities are smaller than maker action if (closest === null) return getBucketUnit(helper.defaultGranularity); return getBucketUnit(closest); } else { if (!customGranularities) return getBucketUnit(returnVal); return getBucketUnit(findBestMatch(customGranularities, returnVal)); } } } var minBucket = customGranularities ? customGranularities[findMinValueIndex(customGranularities, getBucketSize)] : helper.minGranularity; var granularity = bucketLength > getBucketSize(minBucket) ? bucketedBy : minBucket; return getBucketUnit(granularity); }