import { isString , isObject as isObjectLodash , isPlainObject , sortBy } from 'lodash' ; import moment , { Moment } from 'moment' ; import { parseInterval } from '../../../utils' ; import { TimeRangeBounds } from '../../../../../query' ; import { calcAutoIntervalLessThan , calcAutoIntervalNear } from './calc_auto_interval' ; import { convertDurationToNormalizedOpenSearchInterval , convertIntervalToOpenSearchInterval , OpenSearchInterval , } from './calc_opensearch_interval' ; import { autoInterval } from '../../_interval_options' ; interface TimeBucketsInterval extends moment . Duration { description : string ; opensearchValue : OpenSearchInterval [ 'value' ] ; opensearchUnit : OpenSearchInterval [ 'unit' ] ; expression : OpenSearchInterval [ 'expression' ] ; preScaled ? : TimeBucketsInterval ; scale ? : number ; scaled ? : boolean ; } function isObject ( o ) { return isObjectLodash ( o ) ; } function isValidMoment ( m ) { return m && 'isValid' in m && m . isValid ( ) ; } export interface TimeBucketsConfig extends Record < string , any > { 'histogram:maxBars' : number ; 'histogram:barTarget' : number ; dateFormat : string ; 'dateFormat:scaled' : string [ ] [ ] ; } param { state } object param { [ type ] } display export class TimeBuckets { private _timeBucketConfig : TimeBucketsConfig ; private _lb : TimeRangeBounds [ 'min' ] ; private _ub : TimeRangeBounds [ 'max' ] ; private _originalInterval : string | null = null ; private _i ? : moment . Duration | 'auto' ; [ key ] : any ; constructor ( timeBucketConfig ) { this . _timeBucketConfig = timeBucketConfig ; } return { moment . duration | undefined } private getDuration ( ) { if ( this . _ub === undefined || this . _lb === undefined || ! this . hasBounds ( ) ) { return ; } const difference = this . _ub . valueOf ( ) - this . _lb . valueOf ( ) ; return moment . duration ( difference , 'ms' ) ; } param { object } input returns { undefined } setBounds ( input ? ) { if ( ! input ) return this . clearBounds ( ) ; let bounds ; if ( isPlainObject ( input ) && ! Array . isArray ( input ) ) { bounds = [ input . min , input . max ] ; } else { bounds = Array . isArray ( input ) ? input : [ ] ; } const moments = sortBy ( bounds , Number ) as Moment [ ] ; const valid = moments . length === 2 && moments . every ( isValidMoment ) ; if ( ! valid ) { this . clearBounds ( ) ; throw new Error ( 'invalid bounds set: ' + input ) ; } this . _lb = moments . shift ( ) as any ; this . _ub = moments . pop ( ) as any ; const duration = this . getDuration ( ) ; if ( ! duration || duration . asSeconds ( ) < 0 ) { throw new TypeError ( 'Intervals must be positive' ) ; } } return { undefined } clearBounds ( ) { this . _lb = this . _ub = undefined ; } return { Boolean } hasBounds ( ) { return isValidMoment ( this . _ub ) && isValidMoment ( this . _lb ) ; } return { object | undefined } getBounds ( ) { if ( ! this . hasBounds ( ) ) return ; return { min : this . _lb , max : this . _ub , } ; } param { object | string | moment . duration } input setInterval ( input ) { let interval = input ; if ( isObject ( input ) && ! moment . isDuration ( input ) ) { interval = input . val ; } if ( ! interval || interval === autoInterval ) { this . _i = autoInterval ; return ; } if ( isString ( interval ) ) { input = interval ; this . _originalInterval = input ; interval = parseInterval ( interval ) ; if ( interval === null || + interval === 0 ) { interval = null ; } } if ( ! moment . isDuration ( interval ) ) { throw new TypeError ( '"' + input + '" is not a valid interval.' ) ; } this . _i = interval ; } /**
     * Get the interval for the buckets. If the
     * number of buckets created by the interval set
     * is larger than config:histogram:maxBars then the
     * interval will be scaled up. If the number of buckets
     * created is less than one, the interval is scaled back.
     *
     * The interval object returned is a moment.duration
     * object that has been decorated with the following
     * properties.
     *
     * interval.description: a text description of the interval.
     *   designed to be used list "field per {{ desc }}".
     *     - "minute"
     *     - "10 days"
     *     - "3 years"
     *
     * interval.expression: the OpenSearch expression that creates this
     *   interval. If the interval does not properly form an OpenSearch
     *   expression it will be forced into one.
     *
     * interval.scaled: the interval was adjusted to
     *   accommodate the maxBars setting.
     *
     * interval.scale: the number that y-values should be
     *   multiplied by
     */ getInterval ( useNormalizedOpenSearchInterval = true ) { const duration = this . getDuration ( ) ; const readInterval = ( ) => { const interval = this . _i ; if ( moment . isDuration ( interval ) ) return interval ; return calcAutoIntervalNear ( this . _timeBucketConfig [ 'histogram:barTarget' ] , Number ( duration ) ) ; } ; const parsedInterval = readInterval ( ) ; const maybeScaleInterval = ( interval ) => { if ( ! this . hasBounds ( ) || ! duration ) { return interval ; } const maxLength = this . _timeBucketConfig [ 'histogram:maxBars' ] ; const approxLen = Number ( duration ) / Number ( interval ) ; let scaled ; if ( approxLen > maxLength ) { scaled = calcAutoIntervalLessThan ( maxLength , Number ( duration ) ) ; } else { return interval ; } if ( + scaled === + interval ) return interval ; interval = decorateInterval ( interval ) ; return Object . assign ( scaled , { preScaled : interval , scale : Number ( interval ) / Number ( scaled ) , scaled : true , } ) ; } ; const decorateInterval = ( interval ) : TimeBucketsInterval => { const opensearchInterval = useNormalizedOpenSearchInterval ? convertDurationToNormalizedOpenSearchInterval ( interval ) : convertIntervalToOpenSearchInterval ( String ( this . _originalInterval ) ) ; const prettyUnits = moment . normalizeUnits ( opensearchInterval . unit ) ; return Object . assign ( interval , { description : opensearchInterval . value === 1 ? prettyUnits : opensearchInterval . value + ' ' + prettyUnits + 's' , opensearchValue : opensearchInterval . value , opensearchUnit : opensearchInterval . unit , expression : opensearchInterval . expression , } ) ; } ; if ( useNormalizedOpenSearchInterval ) { return decorateInterval ( maybeScaleInterval ( parsedInterval ) ) ; } else { return decorateInterval ( parsedInterval ) ; } } return { string } getScaledDateFormat ( ) { const interval = this . getInterval ( ) ; const rules = this . _timeBucketConfig [ 'dateFormat:scaled' ] ; for ( let i = rules . length - 1 ; i >= 0 ; i -- ) { const rule = rules [ i ] ; if ( ! rule [ 0 ] || ( interval && interval >= moment . duration ( rule [ 0 ] ) ) ) { return rule [ 1 ] ; } } return this . _timeBucketConfig . dateFormat ; } }