import { Color } from '../../../common/colors' ; import { SeriesIdentifier , SeriesKey } from '../../../common/series_id' ; import { ScaleType } from '../../../scales/constants' ; import { BinAgg , Direction , XScaleType } from '../../../specs' ; import { OrderBy } from '../../../specs/settings' ; import { ColorOverrides } from '../../../state/chart_state' ; import { Accessor , AccessorFn , getAccessorValue } from '../../../utils/accessor' ; import { Datum , isNil } from '../../../utils/common' ; import { GroupId } from '../../../utils/ids' ; import { Logger } from '../../../utils/logger' ; import { ColorConfig } from '../../../utils/themes/theme' ; import { groupSeriesByYGroup , isHistogramEnabled , isStackedSpec } from '../domains/y_domain' ; import { X_SCALE_DEFAULT } from '../scales/scale_defaults' ; import { SmallMultiplesGroupBy } from '../state/selectors/get_specs' ; import { applyFitFunctionToDataSeries } from './fit_function_utils' ; import { groupBy } from './group_data_series' ; import { BaseDatum , BasicSeriesSpec , SeriesNameConfigOptions , SeriesSpecs , SeriesType , StackMode } from './specs' ; import { datumXSortPredicate , formatStackedDataSeriesValues } from './stacked_series_utils' ; internal export const SERIES_DELIMITER = ' - ' ; public export interface FilledValues { /** the x value */ x ? : number | string ; /** the max y value */ y1 ? : number ; /** the minimum y value */ y0 ? : number ; } public export interface DataSeriesDatum < T = any > { /** the x value */ x : number | string ; /** the max y value */ y1 : number | null ; /** the minimum y value */ y0 : number | null ; /** initial y1 value, non stacked */ initialY1 : number | null ; /** initial y0 value, non stacked */ initialY0 : number | null ; /** the optional mark metric, used for lines and area series */ mark : number | null ; /** initial datum */ datum : T ; /** the list of filled values because missing or nulls */ filled ? : FilledValues ; } public export interface XYChartSeriesIdentifier < D extends BaseDatum = Datum > extends SeriesIdentifier { yAccessor : Accessor < D > ; splitAccessors : Map < string | number , string | number > ; smVerticalAccessorValue ? : string | number ; smHorizontalAccessorValue ? : string | number ; seriesKeys : ( string | number ) [ ] ; } internal export type DataSeries < D extends BaseDatum = Datum > = XYChartSeriesIdentifier < D > & { groupId : GroupId ; seriesType : SeriesType ; data : DataSeriesDatum [ ] ; isStacked : boolean ; stackMode : StackMode | undefined ; spec : Exclude < BasicSeriesSpec , 'data' > ; insertIndex : number ; isFiltered : boolean ; } ; internal export type DataSeriesCounts = { [ key in SeriesType ] : number ; } ; internal export function getSeriesIndex ( series , target ) { if ( ! series ) { return - 1 ; } return series . findIndex ( ( { key } ) => target . key === key ) ; } internal export function getAccessorFieldName < D extends BaseDatum > ( accessor , index ) { return typeof accessor === 'function' ? accessor . fieldName ?? `(index:${ index })` : accessor ; } internal export function splitSeriesDataByAccessors ( spec , xValueSums , isStacked = false , stackMode ? , groupBySpec ? ) { const { seriesType , id : specId , groupId , data , xAccessor , yAccessors , y0Accessors , markSizeAccessor , splitSeriesAccessors = [ ] , } = spec ; const dataSeries = new Map < SeriesKey , DataSeries > ( ) ; const xValues = [ ] ; const nonNumericValues = [ ] ; if ( isStacked && Boolean ( y0Accessors ?. length ) ) { Logger . warn ( `y0Accessors are not allowed with stackAccessors. y0Accessors will be ignored but available under initialY0.` ) ; } for ( let i = 0 ; i < data . length ; i ++ ) { const datum = data [ i ] ; const splitAccessors = getSplitAccessors ( datum , splitSeriesAccessors ) ; if ( splitSeriesAccessors . length > 0 && splitAccessors . size < 1 ) { continue ; } if ( typeof datum !== 'object' || datum === null ) { continue ; } const x = getAccessorValue ( datum , xAccessor ) ; if ( typeof x !== 'string' && typeof x !== 'number' ) { continue ; } xValues . push ( x ) ; let sum = xValueSums . get ( x ) ?? 0 ; const smH = groupBySpec ?. horizontal ?. by ?. ( spec , datum ) ; const smV = groupBySpec ?. vertical ?. by ?. ( spec , datum ) ; yAccessors . forEach ( ( accessor , index ) => { const cleanedDatum = extractYAndMarkFromDatum ( datum , accessor , nonNumericValues , isBandedSpec ( spec ) , y0Accessors && y0Accessors [ index ] , markSizeAccessor ) ; const accessorStr = getAccessorFieldName ( accessor , index ) ; const splitAccessorStrs = [ ... splitAccessors . values ( ) ] . map ( ( a , si ) => getAccessorFieldName ( a , si ) ) ; const seriesKeys = [ ... splitAccessorStrs , accessorStr ] ; const seriesIdentifier = { specId , seriesKeys , yAccessor : accessorStr , splitAccessors , ... ( ! isNil ( smV ) && { smVerticalAccessorValue : smV } ) , ... ( ! isNil ( smH ) && { smHorizontalAccessorValue : smH } ) , } ; const seriesKey = getSeriesKey ( seriesIdentifier , groupId ) ; sum += cleanedDatum . y1 ?? 0 ; const newDatum = { x , ... cleanedDatum , smH , smV } ; const series = dataSeries . get ( seriesKey ) ; if ( series ) { series . data . push ( newDatum ) ; } else { dataSeries . set ( seriesKey , { ... seriesIdentifier , groupId , seriesType , stackMode , isStacked , seriesKeys , key : seriesKey , data : [ newDatum ] , spec , insertIndex : 0 , isFiltered : false , } ) ; } xValueSums . set ( x , sum ) ; } ) ; } if ( nonNumericValues . length > 0 ) { Logger . warn ( `Found non-numeric y value${ nonNumericValues . length > 1 ? 's' : '' } in dataset for spec "${ specId }"` , `(${ nonNumericValues . map ( ( v ) => JSON . stringify ( v ) ) . join ( ', ' ) })` ) ; } return { dataSeries , xValues , } ; } internal export function getSeriesKey ( { specId , yAccessor , splitAccessors , smVerticalAccessorValue , smHorizontalAccessorValue , } , groupId ) { const joinedAccessors = [ ... splitAccessors . entries ( ) ] . sort ( ( [ a ] , [ b ] ) => ( a > b ? 1 : - 1 ) ) . map ( ( [ key , value ] ) => `${ key }-${ value }` ) . join ( '|' ) ; const smV = smVerticalAccessorValue ? `smV{${ smVerticalAccessorValue }}` : '' ; const smH = smHorizontalAccessorValue ? `smH{${ smHorizontalAccessorValue }}` : '' ; return `groupId{${ groupId }}spec{${ specId }}yAccessor{${ yAccessor }}splitAccessors{${ joinedAccessors }}${ smV }${ smH }` ; } internal function getSplitAccessors < D extends BaseDatum > ( datum , accessors = [ ] ) { const splitAccessors = new Map < string | number , string | number > ( ) ; if ( typeof datum === 'object' && datum !== null ) { accessors . forEach ( ( accessor , index ) => { const value = getAccessorValue ( datum , accessor ) ; if ( typeof value === 'string' || typeof value === 'number' ) { const accessorStr = getAccessorFieldName ( accessor , index ) ; splitAccessors . set ( accessorStr , value ) ; } } ) ; } return splitAccessors ; } internal export function extractYAndMarkFromDatum < D extends BaseDatum > ( datum , yAccessor , nonNumericValues , bandedSpec , y0Accessor ? , markSizeAccessor ? ) { const mark = markSizeAccessor === undefined ? null : finiteOrNull ( getAccessorValue ( datum , markSizeAccessor ) , nonNumericValues ) ; const y1Value = getAccessorValue ( datum , yAccessor ) ; const y1 = finiteOrNull ( y1Value , nonNumericValues ) ; const y0 = y0Accessor ? finiteOrNull ( getAccessorValue ( datum , y0Accessor ) , nonNumericValues ) : null ; return { y1 , datum , y0 : bandedSpec ? y0 : null , mark , initialY0 : y0 , initialY1 : y1 } ; } function finiteOrNull ( value , nonNumericValues ) { const candidateNumber = Number ( value ?? undefined ) ; const finite = Number . isFinite ( candidateNumber ) ; if ( ! finite ) nonNumericValues . push ( value ) ; return finite ? candidateNumber : null ; } /** Sorts data based on order of xValues */ const getSortedDataSeries = ( dataSeries , xValues , xScaleType ) : DataSeries [ ] => dataSeries . map ( ( { data , ... rest } ) => ( { ... rest , data : [ ... data ] . sort ( datumXSortPredicate ( xScaleType , [ ... xValues . values ( ) ] ) ) , } ) ) ; internal export function getFormattedDataSeries ( seriesSpecs , availableDataSeries , xValues , xScaleType ) { const histogramEnabled = isHistogramEnabled ( seriesSpecs ) ; const fittedDataSeries = applyFitFunctionToDataSeries ( getSortedDataSeries ( availableDataSeries , xValues , xScaleType ) , seriesSpecs , xScaleType ) ; const stackedDataSeries = fittedDataSeries . filter ( ( { spec } ) => isStackedSpec ( spec , histogramEnabled ) ) ; const stackedGroups = groupBy < DataSeries > ( stackedDataSeries , [ 'smHorizontalAccessorValue' , 'smVerticalAccessorValue' , 'groupId' ] , true ) ; const fittedAndStackedDataSeries = stackedGroups . reduce < DataSeries [ ] > ( ( acc , dataSeries ) => { const [ { stackMode , seriesType } ] = dataSeries ; const formatted = formatStackedDataSeriesValues ( dataSeries , xValues , seriesType , stackMode ) ; return [ ... acc , ... formatted ] ; } , [ ] ) ; const nonStackedDataSeries = fittedDataSeries . filter ( ( { spec } ) => ! isStackedSpec ( spec , histogramEnabled ) ) ; return [ ... fittedAndStackedDataSeries , ... nonStackedDataSeries ] ; } internal export function getDataSeriesFromSpecs ( seriesSpecs , deselectedDataSeries = [ ] , orderOrdinalBinsBy ? , groupBySpec ? ) { let globalDataSeries = [ ] ; const mutatedXValueSums = new Map < string | number , number > ( ) ; const globalXValues = new Set ( ) ; let isNumberArray = true ; let isOrdinalScale = false ; const specsByYGroup = groupSeriesByYGroup ( seriesSpecs ) ; for ( const spec of seriesSpecs ) { if ( spec . xScaleType === ScaleType . Ordinal ) { isOrdinalScale = true ; } const specGroup = specsByYGroup . get ( spec . groupId ) ; const isStacked = Boolean ( specGroup ?. stacked . find ( ( { id } ) => id === spec . id ) ) ; const { dataSeries , xValues } = splitSeriesDataByAccessors ( spec , mutatedXValueSums , isStacked , specGroup ?. stackMode , groupBySpec ) ; let filteredDataSeries = [ ... dataSeries . values ( ) ] ; if ( deselectedDataSeries . length > 0 ) { filteredDataSeries = filteredDataSeries . map ( ( series ) => ( { ... series , isFiltered : deselectedDataSeries . some ( ( { key : deselectedKey } ) => series . key === deselectedKey ) , } ) ) ; } globalDataSeries = [ ... globalDataSeries , ... filteredDataSeries ] ; for ( const xValue of xValues ) { if ( isNumberArray && typeof xValue !== 'number' ) { isNumberArray = false ; } globalXValues . add ( xValue ) ; } } const xValues = isOrdinalScale || ! isNumberArray ? getSortedOrdinalXValues ( globalXValues , mutatedXValueSums , orderOrdinalBinsBy ) : new Set ( [ ... globalXValues ] . sort ( ( a , b ) => { if ( typeof a === 'string' || typeof b === 'string' ) { return 0 ; } return a - b ; } ) ) ; const dataSeries = globalDataSeries . map ( ( d , i ) => ( { ... d , insertIndex : i , } ) ) ; const smallMultipleUniqueValues = dataSeries . reduce < { smVValues : Set < string | number > ; smHValues : Set < string | number > ; } > ( ( acc , curr ) => { if ( curr . isFiltered ) { return acc ; } if ( ! isNil ( curr . smHorizontalAccessorValue ) ) { acc . smHValues . add ( curr . smHorizontalAccessorValue ) ; } if ( ! isNil ( curr . smVerticalAccessorValue ) ) { acc . smVValues . add ( curr . smVerticalAccessorValue ) ; } return acc ; } , { smVValues : new Set ( ) , smHValues : new Set ( ) } ) ; return { dataSeries , xValues , ... smallMultipleUniqueValues , fallbackScale : ! isOrdinalScale && ! isNumberArray ? X_SCALE_DEFAULT . type : undefined , } ; } internal export function isBandedSpec ( spec ) { return Boolean ( spec . y0Accessors && spec . y0Accessors . length > 0 && ! isStackedSpec ( spec , false ) ) ; } function getSortedOrdinalXValues ( xValues , xValueSums , orderOrdinalBinsBy ? ) { if ( ! orderOrdinalBinsBy ) { return xValues ; } switch ( orderOrdinalBinsBy ?. binAgg ) { case BinAgg . None : return xValues ; case BinAgg . Sum : default : return new Set ( [ ... xValues ] . sort ( ( v1 , v2 ) => ( orderOrdinalBinsBy . direction === Direction . Ascending ? 1 : - 1 ) * ( ( xValueSums . get ( v1 ) ?? 0 ) - ( xValueSums . get ( v2 ) ?? 0 ) ) ) ) ; } } const BIG_NUMBER = Number . MAX_SAFE_INTEGER ; function getSeriesNameFromOptions ( options , { yAccessor , splitAccessors } , delimiter ) { if ( ! options . names ) { return null ; } return ( [ ... options . names ] . sort ( ( { sortIndex : a = BIG_NUMBER } , { sortIndex : b = BIG_NUMBER } ) => a - b ) . map ( ( { accessor , value , name } ) => { const accessorValue = splitAccessors . get ( accessor ) ?? null ; if ( accessorValue === value ) { return name ?? value ; } if ( yAccessor === accessor ) { return name ?? accessor ; } return null ; } ) . filter ( ( d ) => Boolean ( d ) || d === 0 ) . join ( delimiter ) || null ) ; } internal export function getSeriesName ( seriesIdentifier , hasSingleSeries , isTooltip , spec ? ) { const customLabel = typeof spec ?. name === 'function' ? spec . name ( seriesIdentifier , isTooltip ) : typeof spec ?. name === 'object' ? getSeriesNameFromOptions ( spec . name , seriesIdentifier , spec . name . delimiter ?? SERIES_DELIMITER ) : null ; if ( customLabel !== null ) { return customLabel . toString ( ) ; } const multipleYAccessors = spec && spec . yAccessors . length > 1 ; const nameKeys = multipleYAccessors ? seriesIdentifier . seriesKeys : seriesIdentifier . seriesKeys . slice ( 0 , - 1 ) ; const nonZeroLength = nameKeys . length > 0 ; return nonZeroLength && ( spec ?. splitSeriesAccessors || ! hasSingleSeries ) ? nameKeys . join ( typeof spec ?. name === 'object' ? spec . name . delimiter ?? SERIES_DELIMITER : SERIES_DELIMITER ) : spec === undefined ? '' : typeof spec . name === 'string' ? spec . name : spec . id ; } /**
 * Helper function to get highest override color.
 * From highest to lowest: `temporary`, `seriesSpec.color` then, unless `temporary` is set to `null`, `persisted`
 */ function getHighestOverride ( key , customColors , overrides ) { const tempColor = overrides . temporary [ key ] ; return tempColor || customColors . get ( key ) || ( tempColor === null ? undefined : overrides . persisted [ key ] ) ; } internal export function getSeriesColors ( dataSeries , chartColors , customColors , overrides ) { const seriesColorMap = new Map < SeriesKey , Color > ( ) ; let counter = 0 ; const sortedDataSeries = [ ... dataSeries ] . sort ( ( a , b ) => a . insertIndex - b . insertIndex ) ; groupBy ( sortedDataSeries , ( ds ) => { return [ ds . specId , ds . groupId , ds . yAccessor , ... ds . splitAccessors . values ( ) ] . join ( '__' ) ; } , true ) . forEach ( ( ds ) => { const dsKeys = { specId : ds [ 0 ] . specId , yAccessor : ds [ 0 ] . yAccessor , splitAccessors : ds [ 0 ] . splitAccessors , smVerticalAccessorValue : undefined , smHorizontalAccessorValue : undefined , } ; const seriesKey = getSeriesKey ( dsKeys , ds [ 0 ] . groupId ) ; const colorOverride = getHighestOverride ( seriesKey , customColors , overrides ) ; const color = colorOverride || chartColors . vizColors [ counter % chartColors . vizColors . length ] ; seriesColorMap . set ( seriesKey , color ) ; counter ++ ; } ) ; return seriesColorMap ; } internal export function getSeriesIdentifierFromDataSeries ( dataSeries ) { const { yAccessor , splitAccessors , smVerticalAccessorValue , smHorizontalAccessorValue , seriesKeys , specId , key , } = dataSeries ; return { yAccessor , splitAccessors , smVerticalAccessorValue , smHorizontalAccessorValue , seriesKeys , specId , key , } ; }