import { EuiFlexGroup , EuiFlexItem , EuiIcon , EuiSpacer } from '@elastic/eui' ; import moment from 'moment-timezone' ; import { unitOfTime } from 'moment' ; import React , { Component } from 'react' ; import PropTypes from 'prop-types' ; import lightEuiTheme from '@elastic/eui/dist/eui_theme_light.json' ; import darkEuiTheme from '@elastic/eui/dist/eui_theme_dark.json' ; import { AnnotationDomainTypes , Axis , Chart , HistogramBarSeries , LineAnnotation , Position , ScaleType , Settings , RectAnnotation , TooltipValue , TooltipType , ElementClickListener , XYChartElementEvent , BrushEndListener , Theme , } from '@elastic/charts' ; import { i18n } from '@osd/i18n' ; import { IUiSettingsClient } from 'opensearch-dashboards/public' ; import { EuiChartThemeType } from '@elastic/eui/dist/eui_charts_theme' ; import { Subscription , combineLatest } from 'rxjs' ; import { getServices } from '../../../opensearch_dashboards_services' ; import { Chart as IChart } from '../helpers/point_series' ; export interface DiscoverHistogramProps { chartData : IChart ; timefilterUpdateHandler : ( ranges : { from : number ; to : number ; } ) => void ; } interface DiscoverHistogramState { chartsTheme : EuiChartThemeType [ 'theme' ] ; chartsBaseTheme : Theme ; } function findIntervalFromDuration ( dateValue , opensearchValue , opensearchUnit , timeZone ) { const date = moment . tz ( dateValue , timeZone ) ; const startOfDate = moment . tz ( date , timeZone ) . startOf ( opensearchUnit ) ; const endOfDate = moment . tz ( date , timeZone ) . startOf ( opensearchUnit ) . add ( opensearchValue , opensearchUnit ) ; return endOfDate . valueOf ( ) - startOfDate . valueOf ( ) ; } function getIntervalInMs ( value , opensearchValue , opensearchUnit , timeZone ) { switch ( opensearchUnit ) { case 's' : return 1000 * opensearchValue ; case 'ms' : return 1 * opensearchValue ; default : return findIntervalFromDuration ( value , opensearchValue , opensearchUnit , timeZone ) ; } } function getTimezone ( uiSettings ) { if ( uiSettings . isDefault ( 'dateFormat:tz' ) ) { const detectedTimezone = moment . tz . guess ( ) ; if ( detectedTimezone ) return detectedTimezone ; else return moment ( ) . format ( 'Z' ) ; } else { return uiSettings . get ( 'dateFormat:tz' , 'Browser' ) ; } } export function findMinInterval ( xValues , opensearchValue , opensearchUnit , timeZone ) { return xValues . reduce ( ( minInterval , currentXvalue , index ) => { let currentDiff = minInterval ; if ( index > 0 ) { currentDiff = Math . abs ( xValues [ index - 1 ] - currentXvalue ) ; } const singleUnitInterval = getIntervalInMs ( currentXvalue , opensearchValue , opensearchUnit as unitOfTime . Base , timeZone ) ; return Math . min ( minInterval , singleUnitInterval , currentDiff ) ; } , Number . MAX_SAFE_INTEGER ) ; } export class DiscoverHistogram extends Component < DiscoverHistogramProps , DiscoverHistogramState > { public static propTypes = { chartData : PropTypes . object , timefilterUpdateHandler : PropTypes . func , } ; private subscription ? : Subscription ; public state = { chartsTheme : getServices ( ) . theme . chartsDefaultTheme , chartsBaseTheme : getServices ( ) . theme . chartsDefaultBaseTheme , } ; componentDidMount ( ) { this . subscription = combineLatest ( getServices ( ) . theme . chartsTheme$ , getServices ( ) . theme . chartsBaseTheme$ ) . subscribe ( ( [ chartsTheme , chartsBaseTheme ] ) => this . setState ( { chartsTheme , chartsBaseTheme } ) ) ; } componentWillUnmount ( ) { if ( this . subscription ) { this . subscription . unsubscribe ( ) ; } } public onBrushEnd : BrushEndListener = ( { x } ) => { if ( ! x ) { return ; } const [ from , to ] = x ; this . props . timefilterUpdateHandler ( { from , to } ) ; } ; public onElementClick = ( xInterval ) : ElementClickListener => ( [ elementData ] ) => { const startRange = ( elementData as XYChartElementEvent ) [ 0 ] . x ; const range = { from : startRange , to : startRange + xInterval , } ; this . props . timefilterUpdateHandler ( range ) ; } ; public formatXValue = ( val ) => { const xAxisFormat = this . props . chartData . xAxisFormat . params ! . pattern ; return moment ( val ) . format ( xAxisFormat ) ; } ; public renderBarTooltip = ( xInterval , domainStart , domainEnd ) => ( headerData ) : JSX . Element | string => { const headerDataValue = headerData . value ; const formattedValue = this . formatXValue ( headerDataValue ) ; const partialDataText = i18n . translate ( 'discover.histogram.partialData.bucketTooltipText' , { defaultMessage : 'The selected time range does not include this entire bucket, it may contain partial data.' , } ) ; if ( headerDataValue < domainStart || headerDataValue + xInterval > domainEnd ) { return ( < React . Fragment > < EuiFlexGroup alignItems = "center" className = "dscHistogram__header--partial" responsive = { false } gutterSize = "xs" > < EuiFlexItem grow = { false } > < EuiIcon type = "iInCircle" / > < /EuiFlexItem> < EuiFlexItem > { partialDataText } < /EuiFlexItem> < /EuiFlexGroup> < EuiSpacer size = "xs" / > < p > { formattedValue } < /p> < /React.Fragment>); } return formattedValue ; } ; public render ( ) { const uiSettings = getServices ( ) . uiSettings ; const timeZone = getTimezone ( uiSettings ) ; const { chartData } = this . props ; const { chartsTheme , chartsBaseTheme } = this . state ; if ( ! chartData ) { return null ; } const data = chartData . values ; /**
         * Deprecation: [interval] on [date_histogram] is deprecated, use [fixed_interval] or [calendar_interval].
         * see https://github.com/elastic/kibana/issues/27410
         * TODO: Once the Discover query has been update, we should change the below to use the new field
         */ const { intervalOpenSearchValue , intervalOpenSearchUnit , interval } = chartData . ordered ; const xInterval = interval . asMilliseconds ( ) ; const xValues = chartData . xAxisOrderedValues ; const lastXValue = xValues [ xValues . length - 1 ] ; const domain = chartData . ordered ; const domainStart = domain . min . valueOf ( ) ; const domainEnd = domain . max . valueOf ( ) ; const domainMin = data [ 0 ] ?. x > domainStart ? domainStart : data [ 0 ] ?. x ; const domainMax = domainEnd - xInterval > lastXValue ? domainEnd - xInterval : lastXValue ; const xDomain = { min : domainMin , max : domainMax , minInterval : findMinInterval ( xValues , intervalOpenSearchValue , intervalOpenSearchUnit , timeZone ) , } ; const now = moment ( ) ; const isAnnotationAtEdge = moment ( domainEnd ) . add ( 60000 ) . isAfter ( now ) && now . isAfter ( domainEnd ) ; const lineAnnotationValue = isAnnotationAtEdge ? domainEnd : now ; const lineAnnotationData = [ { dataValue : lineAnnotationValue , } , ] ; const isDarkMode = uiSettings . get ( 'theme:darkMode' ) ; const lineAnnotationStyle = { line : { strokeWidth : 2 , stroke : isDarkMode ? darkEuiTheme . euiColorDanger : lightEuiTheme . euiColorDanger , opacity : 0.7 , } , } ; const rectAnnotations = [ ] ; if ( domainStart !== domainMin ) { rectAnnotations . push ( { coordinates : { x1 : domainStart , } , } ) ; } if ( domainEnd !== domainMax ) { rectAnnotations . push ( { coordinates : { x0 : domainEnd , } , } ) ; } const rectAnnotationStyle = { stroke : isDarkMode ? darkEuiTheme . euiColorLightShade : lightEuiTheme . euiColorDarkShade , strokeWidth : 0 , opacity : isDarkMode ? 0.6 : 0.2 , fill : isDarkMode ? darkEuiTheme . euiColorLightShade : lightEuiTheme . euiColorDarkShade , } ; const tooltipProps = { headerFormatter : this . renderBarTooltip ( xInterval , domainStart , domainEnd ) , type : TooltipType . VerticalCursor , } ; return ( < Chart size = "100%" > < Settings xDomain = { xDomain } onBrushEnd = { this . onBrushEnd } onElementClick = { this . onElementClick ( xInterval ) } tooltip = { tooltipProps } theme = { chartsTheme } baseTheme = { chartsBaseTheme } / > < Axis id = "discover-histogram-left-axis" position = { Position . Left } ticks = { 5 } title = { chartData . yAxisLabel } / > < Axis id = "discover-histogram-bottom-axis" position = { Position . Bottom } title = { chartData . xAxisLabel } tickFormat = { this . formatXValue } ticks = { 10 } / > < LineAnnotation id = "line-annotation" domainType = { AnnotationDomainTypes . XDomain } dataValues = { lineAnnotationData } hideTooltips = { true } style = { lineAnnotationStyle } / > < RectAnnotation dataValues = { rectAnnotations } id = "rect-annotation" zIndex = { 2 } style = { rectAnnotationStyle } hideTooltips = { true } / > < HistogramBarSeries id = "discover-histogram" minBarHeight = { 2 } xScaleType = { ScaleType . Time } yScaleType = { ScaleType . Linear } xAccessor = "x" yAccessors = { [ 'y' ] } data = { data } timeZone = { timeZone } name = { chartData . yAxisLabel } / > < /Chart>); } }