import { ICanvasGraphServiceSettings , IPerfMinMax , IGraphDrawableArea , IPerfMousePanningPosition , IPerfIndexBounds , IPerfTooltip , IPerfTextMeasureCache , IPerfLayoutSize , IPerfTicker , TimestampUnit , ITooltipPreprocessedInformation , IPerfTooltipHoverPosition , IVisibleRangeChangedObservableProps , } from "./graphSupportingTypes" ; import { IPerfDatasets , IPerfMetadata } from "babylonjs/Misc/interfaces/iPerfViewer" ; import { Scalar } from "babylonjs/Maths/math.scalar" ; import { PerformanceViewerCollector } from "babylonjs/Misc/PerformanceViewer/performanceViewerCollector" ; import { Observable } from "babylonjs/Misc/observable" ; const defaultColor = "#000" ; const axisColor = "#c0c4c8" ; const futureBoxColor = "#dfe9ed" ; const dividerColor = "#0a3066" ; const playheadColor = "#b9dbef" ; const positionIndicatorColor = "#4d5960" ; const tooltipBackgroundColor = "#566268" ; const tooltipForegroundColor = "#fbfbfb" ; const topOfGraphY = 0 ; const defaultAlpha = 1 ; const tooltipBackgroundAlpha = 0.8 ; const backgroundLineAlpha = 0.2 ; const maxDistanceForHover = 10 ; const tooltipHorizontalPadding = 10 ; const spaceBetweenTextAndBox = 5 ; const tooltipPaddingFromBottom = 20 ; const triangleHeight = 10 ; const triangleWidth = 20 ; const trianglePaddingFromAxisLine = 3 ; const tickerHorizontalPadding = 10 ; const dataPadding = 2 ; const playheadSize = 8 ; const dividerSize = 2 ; const axisLineLength = 10 ; const axisPadding = 10 ; const scaleFactor = 0.8 ; const stopDrawingPlayheadThreshold = 0.95 ; const returnToLiveThreshold = 0.998 ; const graphAddonFont = "12px Arial" ; const alphabet = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" ; const maximumDatasetsAllowed = 64 ; const msInSecond = 1000 ; const msInMinute = msInSecond * 60 ; const msInHour = msInMinute * 60 ; const tooltipDebounceTime = 32 ; const drawThrottleTime = 15 ; const maxXDistancePercBetweenLinePoints = 0.1 ; const noDataRectangleColor = "#aaaaaa" ; param callback param time function debounce ( callback , time ) { let timerId ; return function ( ... args ) { clearTimeout ( timerId ) ; timerId = setTimeout ( ( ) => callback ( ... args ) , time ) ; } ; } param callback param time function throttle ( callback , time ) { let lastCalledTime = 0 ; return function ( ... args ) { const now = Date . now ( ) ; if ( now - lastCalledTime < time ) { return ; } lastCalledTime = now ; callback ( ... args ) ; } ; } export class CanvasGraphService { private _ctx : CanvasRenderingContext2D | null ; private _width : number ; private _height : number ; private _sizeOfWindow : number = 300 ; private _ticks : number [ ] ; private _panPosition : IPerfMousePanningPosition | null ; private _position : number | null ; private _datasetBounds : IPerfIndexBounds ; private _globalTimeMinMax : IPerfMinMax ; private _hoverPosition : IPerfTooltipHoverPosition | null ; private _drawableArea : IGraphDrawableArea ; private _axisHeight : number ; private _tooltipItems : IPerfTooltip [ ] ; private _tooltipTextCache : IPerfTextMeasureCache ; private _tickerTextCache : IPerfTextMeasureCache ; private _tickerItems : IPerfTicker [ ] ; private _preprocessedTooltipInfo : ITooltipPreprocessedInformation ; private _numberOfTickers : number ; private _onVisibleRangeChangedObservable ? : Observable < IVisibleRangeChangedObservableProps > ; private readonly _addonFontLineHeight : number ; private readonly _defaultLineHeight : number ; public readonly datasets : IPerfDatasets ; public metadata : Map < string , IPerfMetadata > ; param canvas param settings constructor ( canvas , settings ) { this . _ctx = canvas . getContext && canvas . getContext ( "2d" ) ; this . _width = canvas . width ; this . _height = canvas . height ; this . _ticks = [ ] ; this . _panPosition = null ; this . _hoverPosition = null ; this . _position = null ; this . _datasetBounds = { start : 0 , end : 0 } ; this . _globalTimeMinMax = { min : Infinity , max : 0 } ; this . _drawableArea = { top : 0 , left : 0 , right : 0 , bottom : 0 } ; this . _tooltipTextCache = { text : "" , width : 0 } ; this . _tickerTextCache = { text : "" , width : 0 } ; this . _tooltipItems = [ ] ; this . _tickerItems = [ ] ; this . _preprocessedTooltipInfo = { focusedId : "" , longestText : "" , numberOfTooltipItems : 0 , xForActualTimestamp : 0 } ; this . _numberOfTickers = 0 ; this . _onVisibleRangeChangedObservable = settings . onVisibleRangeChangedObservable ; for ( let i = 0 ; i < maximumDatasetsAllowed ; i ++ ) { this . _tooltipItems . push ( { text : "" , color : "" } ) ; this . _tickerItems . push ( { text : "" , id : "" , max : 0 , min : 0 } ) ; } if ( ! this . _ctx ) { throw Error ( "No canvas context accessible" ) ; } const defaultMetrics = this . _ctx . measureText ( alphabet ) ; this . _defaultLineHeight = defaultMetrics . actualBoundingBoxAscent + defaultMetrics . actualBoundingBoxDescent ; this . _axisHeight = axisLineLength + axisPadding + this . _defaultLineHeight + axisPadding ; this . _ctx . save ( ) ; this . _ctx . font = graphAddonFont ; const fontMetrics = this . _ctx . measureText ( alphabet ) ; this . _addonFontLineHeight = fontMetrics . actualBoundingBoxAscent + fontMetrics . actualBoundingBoxDescent ; this . _ctx . restore ( ) ; this . datasets = settings . datasets ; this . metadata = new Map < string , IPerfMetadata > ( ) ; this . _attachEventListeners ( canvas ) ; } /**
     * This method lets the service know it should get ready to update what it is displaying.
     */ public update = throttle ( ( ) => this . _draw ( ) , drawThrottleTime ) ; param size public resize ( size ) { const { _ctx : ctx } = this ; const { width , height } = size ; if ( ! ctx || ! ctx . canvas ) { return ; } this . _width = width ; this . _height = height ; ctx . canvas . width = width ; ctx . canvas . height = height ; this . update ( ) ; } /**
     * Force resets the position in the data, effectively returning to the most current data.
     */ public resetDataPosition ( ) { this . _position = null ; } private _prevPointById : Map < string , [ number , number ] > = new Map < string , [ number , number ] > ( ) ; private _prevValueById : Map < string , number > = new Map < string , number > ( ) ; /**
     * This method draws the data and sets up the appropriate scales.
     */ private _draw ( ) { const { _ctx : ctx } = this ; if ( ! ctx ) { return ; } const numSlices = this . _getNumberOfSlices ( ) ; if ( numSlices === 0 ) { return ; } this . clear ( ) ; this . _globalTimeMinMax . min = Infinity ; this . _globalTimeMinMax . max = 0 ; const pos = this . _position ?? numSlices - 1 ; let start = pos - Math . ceil ( this . _sizeOfWindow * scaleFactor ) ; let startOverflow = 0 ; if ( start < 0 ) { startOverflow = 0 - start ; start = 0 ; } let end = Math . ceil ( pos + this . _sizeOfWindow * ( 1 - scaleFactor ) + startOverflow ) ; if ( end > numSlices ) { const endOverflow = end - numSlices ; end = numSlices ; start = Math . max ( start - endOverflow , 0 ) ; } this . _datasetBounds . start = start ; this . _datasetBounds . end = end ; this . _globalTimeMinMax . min = this . datasets . data . at ( this . datasets . startingIndices . at ( this . _datasetBounds . start ) ) ; this . _globalTimeMinMax . max = this . datasets . data . at ( this . datasets . startingIndices . at ( this . _datasetBounds . end - 1 ) ) ; const bufferMaximum = Math . ceil ( ( this . _globalTimeMinMax . max - this . _globalTimeMinMax . min ) / scaleFactor + this . _globalTimeMinMax . min ) ; this . _datasetBounds . end = this . _getClosestPointToTimestamp ( bufferMaximum ) + 1 ; this . _globalTimeMinMax . max = Math . max ( this . datasets . data . at ( this . datasets . startingIndices . at ( this . _datasetBounds . end - 1 ) ) , this . _globalTimeMinMax . max ) ; const updatedScaleFactor = Scalar . Clamp ( ( this . _globalTimeMinMax . max - this . _globalTimeMinMax . min ) / ( bufferMaximum - this . _globalTimeMinMax . min ) , scaleFactor , 1 ) ; this . _globalTimeMinMax . max = bufferMaximum ; this . _drawableArea . top = 0 ; this . _drawableArea . left = 0 ; this . _drawableArea . bottom = this . _height ; this . _drawableArea . right = this . _width ; this . _drawTickers ( this . _drawableArea , this . _datasetBounds ) ; this . _drawTimeAxis ( this . _globalTimeMinMax , this . _drawableArea ) ; this . _drawPlayheadRegion ( this . _drawableArea , updatedScaleFactor ) ; this . _drawableArea . top += dataPadding ; this . _drawableArea . bottom -= dataPadding ; this . _preprocessTooltip ( this . _hoverPosition , this . _drawableArea ) ; const { left , right , bottom , top } = this . _drawableArea ; this . datasets . ids . forEach ( ( id , idOffset ) => { let valueMinMax ; let prevPoint = this . _prevPointById . get ( id ) ; let prevValue = this . _prevValueById . get ( id ) ; for ( let i = 0 ; i < this . _numberOfTickers ; i ++ ) { if ( this . _tickerItems [ i ] . id === id ) { valueMinMax = { ... this . _tickerItems [ i ] } ; const delta = valueMinMax . max - valueMinMax . min ; valueMinMax . min -= 0.01 * delta ; valueMinMax . max += 0.01 * delta ; } } if ( ! valueMinMax ) { return ; } ctx . beginPath ( ) ; ctx . strokeStyle = this . metadata . get ( id ) ?. color ?? defaultColor ; if ( this . _preprocessedTooltipInfo . focusedId === id ) { ctx . globalAlpha = defaultAlpha ; } else if ( this . _preprocessedTooltipInfo . focusedId !== "" ) { ctx . globalAlpha = backgroundLineAlpha ; } const smoothingFactor = 0.2 ; for ( let pointIndex = this . _datasetBounds . start ; pointIndex < this . _datasetBounds . end ; pointIndex ++ ) { const numPoints = this . datasets . data . at ( this . datasets . startingIndices . at ( pointIndex ) + PerformanceViewerCollector . NumberOfPointsOffset ) ; if ( idOffset >= numPoints ) { continue ; } const valueIndex = this . datasets . startingIndices . at ( pointIndex ) + PerformanceViewerCollector . SliceDataOffset + idOffset ; const timestamp = this . datasets . data . at ( this . datasets . startingIndices . at ( pointIndex ) ) ; const value = this . datasets . data . at ( valueIndex ) ; if ( prevValue === undefined ) { prevValue = value ; this . _prevValueById . set ( id , prevValue ) ; } const smoothedValue = smoothingFactor * value + ( 1 - smoothingFactor ) * prevValue ; const drawableTime = this . _getPixelForNumber ( timestamp , this . _globalTimeMinMax , left , right - left , false ) ; const drawableValue = this . _getPixelForNumber ( smoothedValue , valueMinMax , top , bottom - top , true ) ; if ( prevPoint === undefined ) { prevPoint = [ drawableTime , drawableValue ] ; this . _prevPointById . set ( id , prevPoint ) ; } const xDifference = drawableTime - prevPoint [ 0 ] ; const skipLine = xDifference > maxXDistancePercBetweenLinePoints * ( right - left ) ; if ( skipLine ) { ctx . fillStyle = noDataRectangleColor ; ctx . fillRect ( prevPoint [ 0 ] , top , xDifference , bottom - top ) ; } else { if ( prevPoint [ 0 ] < drawableTime ) { ctx . moveTo ( prevPoint [ 0 ] , prevPoint [ 1 ] ) ; ctx . lineTo ( drawableTime , drawableValue ) ; } } prevPoint [ 0 ] = drawableTime ; prevPoint [ 1 ] = drawableValue ; this . _prevValueById . set ( id , smoothedValue ) ; } ctx . stroke ( ) ; } ) ; ctx . globalAlpha = defaultAlpha ; this . _drawTooltip ( this . _hoverPosition , this . _drawableArea ) ; } private _drawTickers ( drawableArea , bounds ) { const { _ctx : ctx } = this ; if ( ! ctx ) { return ; } let longestText = "" ; this . _numberOfTickers = 0 ; const valueMap = new Map < string , IPerfMinMax > ( ) ; this . datasets . ids . forEach ( ( id , idOffset ) => { if ( ! ! this . metadata . get ( id ) ?. hidden ) { return ; } const valueMinMax = this . _getMinMax ( bounds , idOffset ) ; const latestValue = this . datasets . data . at ( this . datasets . startingIndices . at ( bounds . end - 1 ) + PerformanceViewerCollector . SliceDataOffset + idOffset ) ; const text = `${ id }: ${ latestValue . toFixed ( 2 ) } (max: ${ valueMinMax . max . toFixed ( 2 ) }, min: ${ valueMinMax . min . toFixed ( 2 ) })` ; valueMap . set ( id , { min : valueMinMax . min , max : valueMinMax . max , current : latestValue , } ) ; if ( text . length > longestText . length ) { longestText = text ; } this . _tickerItems [ this . _numberOfTickers ] . id = id ; this . _tickerItems [ this . _numberOfTickers ] . max = valueMinMax . max ; this . _tickerItems [ this . _numberOfTickers ] . min = valueMinMax . min ; this . _tickerItems [ this . _numberOfTickers ] . text = text ; this . _numberOfTickers ++ ; } ) ; this . _onVisibleRangeChangedObservable ?. notifyObservers ( { valueMap } ) ; ctx . save ( ) ; ctx . font = graphAddonFont ; ctx . textBaseline = "middle" ; ctx . textAlign = "left" ; let width ; if ( this . _tickerTextCache . text . length === longestText . length ) { width = this . _tickerTextCache . width ; } else { width = ctx . measureText ( longestText ) . width + 2 * tickerHorizontalPadding ; this . _tickerTextCache . text = longestText ; this . _tickerTextCache . width = width ; } ctx . restore ( ) ; } param targetTime returns private _getClosestPointToTimestamp ( targetTime ) { let low = 0 ; let high = this . _getNumberOfSlices ( ) - 1 ; let closestIndex = 0 ; while ( low <= high ) { const middle = Math . trunc ( ( low + high ) / 2 ) ; const middleTimestamp = this . datasets . data . at ( this . datasets . startingIndices . at ( middle ) ) ; if ( Math . abs ( middleTimestamp - targetTime ) < Math . abs ( this . datasets . data . at ( this . datasets . startingIndices . at ( closestIndex ) ) - targetTime ) ) { closestIndex = middle ; } if ( middleTimestamp < targetTime ) { low = middle + 1 ; } else if ( middleTimestamp > targetTime ) { high = middle - 1 ; } else { break ; } } return closestIndex ; } returns private _getNumberOfSlices ( ) { return this . datasets . startingIndices . itemLength ; } param timeMinMax param drawableArea private _drawTimeAxis ( timeMinMax , drawableArea ) { const { _ctx : ctx } = this ; if ( ! ctx ) { return ; } const spaceAvailable = drawableArea . right - drawableArea . left ; this . _generateTicks ( timeMinMax , spaceAvailable ) ; drawableArea . bottom -= this . _axisHeight ; ctx . save ( ) ; ctx . fillStyle = axisColor ; ctx . fillRect ( drawableArea . left , drawableArea . bottom , spaceAvailable , this . _axisHeight ) ; ctx . beginPath ( ) ; ctx . strokeStyle = defaultColor ; ctx . moveTo ( drawableArea . left , drawableArea . bottom ) ; ctx . lineTo ( drawableArea . right , drawableArea . bottom ) ; ctx . fillStyle = defaultColor ; ctx . textAlign = "center" ; ctx . textBaseline = "middle" ; const timestampUnit = this . _getTimestampUnit ( this . _ticks [ this . _ticks . length - 1 ] ) ; this . _ticks . forEach ( ( tick ) => { let position = this . _getPixelForNumber ( tick , timeMinMax , drawableArea . left , spaceAvailable , false ) ; if ( position > spaceAvailable ) { position = spaceAvailable ; } ctx . moveTo ( position , drawableArea . bottom ) ; ctx . lineTo ( position , drawableArea . bottom + 10 ) ; ctx . fillText ( this . _parseTimestamp ( tick , timestampUnit ) , position , drawableArea . bottom + 20 ) ; } ) ; ctx . stroke ( ) ; ctx . restore ( ) ; } param timestamp returns private _getTimestampUnit ( timestamp ) { if ( timestamp / msInHour > 1 ) { return TimestampUnit . Hours ; } else if ( timestamp / msInMinute > 1 ) { return TimestampUnit . Minutes ; } else if ( timestamp / msInSecond > 1 ) { return TimestampUnit . Seconds ; } else { return TimestampUnit . Milliseconds ; } } param timestamp param intervalUnit returns private _parseTimestamp ( timestamp , intervalUnit ) { let parsedTimestamp = "" ; if ( intervalUnit >= TimestampUnit . Hours ) { const numHours = Math . floor ( timestamp / msInHour ) ; timestamp -= numHours * msInHour ; parsedTimestamp += `${ numHours . toString ( ) . padStart ( intervalUnit > TimestampUnit . Hours ? 2 : 1 , "0" ) }:` ; } if ( intervalUnit >= TimestampUnit . Minutes ) { const numMinutes = Math . floor ( timestamp / msInMinute ) ; timestamp -= numMinutes * msInMinute ; parsedTimestamp += `${ numMinutes . toString ( ) . padStart ( intervalUnit > TimestampUnit . Minutes ? 2 : 1 , "0" ) }:` ; } const numSeconds = Math . floor ( timestamp / msInSecond ) ; timestamp -= numSeconds * msInSecond ; parsedTimestamp += numSeconds . toString ( ) . padStart ( intervalUnit > TimestampUnit . Seconds ? 2 : 1 , "0" ) ; if ( timestamp > 0 ) { if ( parsedTimestamp . length > 0 ) { parsedTimestamp += "." ; } parsedTimestamp += Math . round ( timestamp ) . toString ( ) . padStart ( 3 , "0" ) ; } return parsedTimestamp ; } param minMax param spaceAvailable private _generateTicks ( minMax , spaceAvailable ) { const { min , max } = minMax ; const minTickSpacing = 40 ; this . _ticks . length = 0 ; const maxTickCount = Math . ceil ( spaceAvailable / minTickSpacing ) ; const range = this . _niceNumber ( max - min , false ) ; const spacing = this . _niceNumber ( range / ( maxTickCount - 1 ) , true ) ; const niceMin = Math . floor ( min / spacing ) * spacing ; const niceMax = Math . floor ( max / spacing ) * spacing ; for ( let i = niceMin ; i <= niceMax + 0.5 * spacing ; i += spacing ) { this . _ticks . push ( i ) ; } } param num param shouldRound returns private _niceNumber ( num , shouldRound ) { const exp = Math . floor ( Math . log10 ( num ) ) ; const fraction = num / Math . pow ( 10 , exp ) ; let niceFraction ; if ( shouldRound ) { if ( fraction < 1.5 ) { niceFraction = 1 ; } else if ( fraction < 3 ) { niceFraction = 2 ; } else if ( fraction < 7 ) { niceFraction = 5 ; } else { niceFraction = 10 ; } } else { if ( fraction <= 1 ) { niceFraction = 1 ; } else if ( fraction <= 2 ) { niceFraction = 2 ; } else if ( fraction <= 5 ) { niceFraction = 5 ; } else { niceFraction = 10 ; } } return niceFraction * Math . pow ( 10 , exp ) ; } param items returns private _getMinMax ( bounds , offset ) { let min = Infinity , max = 0 ; for ( let i = bounds . start ; i < bounds . end ; i ++ ) { const numPoints = this . datasets . data . at ( this . datasets . startingIndices . at ( i ) + PerformanceViewerCollector . NumberOfPointsOffset ) ; if ( offset >= numPoints ) { continue ; } const itemIndex = this . datasets . startingIndices . at ( i ) + PerformanceViewerCollector . SliceDataOffset + offset ; const item = this . datasets . data . at ( itemIndex ) ; if ( item < min ) { min = item ; } if ( item > max ) { max = item ; } } return { min , max , } ; } param num param minMax param startingPixel param spaceAvailable param shouldFlipValue returns private _getPixelForNumber ( num , minMax , startingPixel , spaceAvailable , shouldFlipValue ) { const { min , max } = minMax ; let normalizedValue = Math . abs ( max - min ) > 0.001 ? ( num - min ) / ( max - min ) : 0.5 ; if ( shouldFlipValue ) { normalizedValue = 1 - normalizedValue ; } return startingPixel + normalizedValue * spaceAvailable ; } param canvas private _attachEventListeners ( canvas ) { canvas . addEventListener ( "wheel" , this . _handleZoom ) ; canvas . addEventListener ( "mousemove" , this . _handleDataHover ) ; canvas . addEventListener ( "mousedown" , this . _handlePanStart ) ; canvas . addEventListener ( "mouseleave" , this . _handleStopHover ) ; canvas . ownerDocument . addEventListener ( "mouseup" , this . _handlePanStop ) ; } param canvas private _removeEventListeners ( canvas ) { canvas . removeEventListener ( "wheel" , this . _handleZoom ) ; canvas . removeEventListener ( "mousemove" , this . _handleDataHover ) ; canvas . removeEventListener ( "mousedown" , this . _handlePanStart ) ; canvas . removeEventListener ( "mouseleave" , this . _handleStopHover ) ; canvas . ownerDocument . removeEventListener ( "mouseup" , this . _handlePanStop ) ; } param event private _handleDataHover = ( event ) => { if ( this . _panPosition ) { return ; } this . _hoverPosition = { xPos : event . clientX , yPos : event . clientY } ; this . _debouncedTooltip ( this . _hoverPosition , this . _drawableArea ) ; } ; /**
     * Debounced processing and drawing of tooltip.
     */ private _debouncedTooltip = debounce ( ( pos , drawableArea ) => { this . _preprocessTooltip ( pos , drawableArea ) ; this . _drawTooltip ( pos , drawableArea ) ; } , tooltipDebounceTime ) ; /**
     * Handles what to do when we stop hovering over the canvas.
     */ private _handleStopHover = ( ) => { this . _hoverPosition = null ; } ; param x1 param y1 param x2 param y2 param x0 param y0 returns private _getDistanceFromLine ( x1 , y1 , x2 , y2 , x0 , y0 ) { if ( x1 === x2 && y1 === y2 ) { return Math . sqrt ( Math . pow ( x1 - x0 , 2 ) + Math . pow ( y1 - y0 , 2 ) ) ; } let topX = 0 ; let topY = 0 ; let bottomX = 0 ; let bottomY = 0 ; if ( y1 >= y2 ) { topX = x1 ; topY = y1 ; bottomX = x2 ; bottomY = y2 ; } else { topX = x2 ; topY = y2 ; bottomX = x1 ; bottomY = y1 ; } if ( y0 < bottomY ) { return Math . sqrt ( Math . pow ( bottomX - x0 , 2 ) + Math . pow ( bottomY - y0 , 2 ) ) ; } if ( y0 > topY ) { return Math . sqrt ( Math . pow ( topX - x0 , 2 ) + Math . pow ( topY - y0 , 2 ) ) ; } const numerator = Math . abs ( ( x2 - x1 ) * ( y1 - y0 ) - ( x1 - x0 ) * ( y2 - y1 ) ) ; const denominator = Math . sqrt ( Math . pow ( x2 - x1 , 2 ) + Math . pow ( y2 - y1 , 2 ) ) ; return numerator / denominator ; } param pos param drawableArea private _preprocessTooltip ( pos , drawableArea ) { const { _ctx : ctx } = this ; if ( pos === null || ! ctx || ! ctx . canvas || this . _getNumberOfSlices ( ) === 0 ) { return ; } const { left , top } = ctx . canvas . getBoundingClientRect ( ) ; const adjustedYPos = pos . yPos - top ; let adjustedXPos = pos . xPos - left ; if ( adjustedXPos > drawableArea . right ) { adjustedXPos = drawableArea . right ; } const inferredTimestamp = this . _getNumberFromPixel ( adjustedXPos , this . _globalTimeMinMax , drawableArea . left , drawableArea . right , false ) ; let longestText = "" ; let numberOfTooltipItems = 0 ; const closestIndex = this . _getClosestPointToTimestamp ( inferredTimestamp ) ; let actualTimestamp = 0 ; let closestLineId = "" ; let closestLineValueMinMax = { min : 0 , max : 0 } ; let closestLineDistance = Number . POSITIVE_INFINITY ; this . datasets . ids . forEach ( ( id , idOffset ) => { if ( ! ! this . metadata . get ( id ) ?. hidden ) { return ; } const numPoints = this . datasets . data . at ( this . datasets . startingIndices . at ( closestIndex ) + PerformanceViewerCollector . NumberOfPointsOffset ) ; if ( idOffset >= numPoints ) { return ; } const valueAtClosestPointIndex = this . datasets . startingIndices . at ( closestIndex ) + PerformanceViewerCollector . SliceDataOffset + idOffset ; const valueAtClosestPoint = this . datasets . data . at ( valueAtClosestPointIndex ) ; let valueMinMax ; for ( let i = 0 ; i < this . _numberOfTickers ; i ++ ) { if ( this . _tickerItems [ i ] . id === id ) { valueMinMax = this . _tickerItems [ i ] ; } } if ( ! valueMinMax ) { return ; } actualTimestamp = this . datasets . data . at ( this . datasets . startingIndices . at ( closestIndex ) ) ; const valueAtClosestPointYPos = this . _getPixelForNumber ( valueAtClosestPoint , valueMinMax , drawableArea . top , drawableArea . bottom - drawableArea . top , true ) ; const xForActualTimestamp = this . _getPixelForNumber ( actualTimestamp , this . _globalTimeMinMax , drawableArea . left , drawableArea . right - drawableArea . left , false ) ; const text = `${ id }: ${ valueAtClosestPoint . toFixed ( 2 ) }` ; if ( text . length > longestText . length ) { longestText = text ; } this . _tooltipItems [ numberOfTooltipItems ] . text = text ; this . _tooltipItems [ numberOfTooltipItems ] . color = this . metadata . get ( id ) ?. color ?? defaultColor ; numberOfTooltipItems ++ ; if ( ! this . _position ) { return ; } let distance = this . _getDistanceFromLine ( xForActualTimestamp , valueAtClosestPointYPos , xForActualTimestamp , valueAtClosestPointYPos , pos . xPos - left , adjustedYPos ) ; if ( closestIndex + 1 < this . datasets . data . itemLength && this . datasets . data . at ( this . datasets . startingIndices . at ( closestIndex + 1 ) + PerformanceViewerCollector . NumberOfPointsOffset ) > idOffset ) { const secondPointTimestamp = this . datasets . data . at ( this . datasets . startingIndices . at ( closestIndex + 1 ) ) ; const secondPointX = this . _getPixelForNumber ( secondPointTimestamp , this . _globalTimeMinMax , drawableArea . left , drawableArea . right - drawableArea . left , false ) ; const secondPointValue = this . datasets . data . at ( this . datasets . startingIndices . at ( closestIndex + 1 ) + PerformanceViewerCollector . SliceDataOffset + idOffset ) ; const secondPointY = this . _getPixelForNumber ( secondPointValue , valueMinMax , drawableArea . top , drawableArea . bottom - drawableArea . top , true ) ; distance = Math . min ( this . _getDistanceFromLine ( xForActualTimestamp , valueAtClosestPointYPos , secondPointX , secondPointY , pos . xPos - left , adjustedYPos ) , distance ) ; } if ( closestIndex - 1 >= 0 && this . datasets . data . at ( this . datasets . startingIndices . at ( closestIndex + 1 ) + PerformanceViewerCollector . NumberOfPointsOffset ) > idOffset ) { const secondPointTimestamp = this . datasets . data . at ( this . datasets . startingIndices . at ( closestIndex - 1 ) ) ; const secondPointX = this . _getPixelForNumber ( secondPointTimestamp , this . _globalTimeMinMax , drawableArea . left , drawableArea . right - drawableArea . left , false ) ; const secondPointValue = this . datasets . data . at ( this . datasets . startingIndices . at ( closestIndex - 1 ) + PerformanceViewerCollector . SliceDataOffset + idOffset ) ; const secondPointY = this . _getPixelForNumber ( secondPointValue , valueMinMax , drawableArea . top , drawableArea . bottom - drawableArea . top , true ) ; distance = Math . min ( this . _getDistanceFromLine ( xForActualTimestamp , valueAtClosestPointYPos , secondPointX , secondPointY , pos . xPos - left , adjustedYPos ) , distance ) ; } if ( distance < closestLineDistance ) { closestLineId = id ; closestLineDistance = distance ; closestLineValueMinMax = valueMinMax ; } } ) ; const xForActualTimestamp = this . _getPixelForNumber ( actualTimestamp , this . _globalTimeMinMax , drawableArea . left , drawableArea . right - drawableArea . left , false ) ; this . _preprocessedTooltipInfo . xForActualTimestamp = xForActualTimestamp ; if ( closestLineDistance <= maxDistanceForHover && this . _position ) { this . _preprocessedTooltipInfo . focusedId = closestLineId ; const inferredValue = this . _getNumberFromPixel ( adjustedYPos , closestLineValueMinMax , drawableArea . top , drawableArea . bottom , true ) ; const closestLineText = `${ closestLineId }: ${ inferredValue . toFixed ( 2 ) }` ; this . _preprocessedTooltipInfo . longestText = closestLineText ; this . _preprocessedTooltipInfo . numberOfTooltipItems = 1 ; this . _tooltipItems [ 0 ] . text = closestLineText ; this . _tooltipItems [ 0 ] . color = this . metadata . get ( closestLineId ) ?. color ?? defaultColor ; } else { this . _preprocessedTooltipInfo . focusedId = "" ; this . _preprocessedTooltipInfo . longestText = longestText ; this . _preprocessedTooltipInfo . numberOfTooltipItems = numberOfTooltipItems ; } } param pos param drawableArea private _drawTooltip ( pos , drawableArea ) { const { _ctx : ctx } = this ; if ( pos === null || ! ctx || ! ctx . canvas || this . _getNumberOfSlices ( ) === 0 ) { return ; } const { left , top } = ctx . canvas . getBoundingClientRect ( ) ; const { numberOfTooltipItems , xForActualTimestamp , longestText } = this . _preprocessedTooltipInfo ; ctx . save ( ) ; ctx . fillStyle = positionIndicatorColor ; let yTriangle = drawableArea . bottom + trianglePaddingFromAxisLine ; ctx . beginPath ( ) ; ctx . moveTo ( xForActualTimestamp , yTriangle ) ; ctx . lineTo ( xForActualTimestamp + triangleWidth / 2 , yTriangle + triangleHeight ) ; ctx . lineTo ( xForActualTimestamp - triangleWidth / 2 , yTriangle + triangleHeight ) ; ctx . closePath ( ) ; ctx . fill ( ) ; ctx . strokeStyle = positionIndicatorColor ; ctx . beginPath ( ) ; if ( this . _preprocessedTooltipInfo . focusedId === "" ) { ctx . moveTo ( xForActualTimestamp , drawableArea . bottom ) ; ctx . lineTo ( xForActualTimestamp , topOfGraphY ) ; } else { const lineY = pos . yPos - top ; ctx . moveTo ( drawableArea . left , lineY ) ; ctx . lineTo ( drawableArea . right , lineY ) ; } ctx . stroke ( ) ; ctx . font = graphAddonFont ; ctx . textBaseline = "middle" ; ctx . textAlign = "left" ; const boxLength = this . _addonFontLineHeight ; const textHeight = this . _addonFontLineHeight + Math . floor ( tooltipHorizontalPadding / 2 ) ; let width ; if ( longestText === this . _tooltipTextCache . text ) { width = this . _tooltipTextCache . width ; } else { width = ctx . measureText ( longestText ) . width + boxLength + 2 * tooltipHorizontalPadding + spaceBetweenTextAndBox ; this . _tooltipTextCache . text = longestText ; this . _tooltipTextCache . width = width ; } const tooltipHeight = textHeight * ( numberOfTooltipItems + 1 ) ; let x = pos . xPos - left ; let y = drawableArea . bottom - tooltipPaddingFromBottom - tooltipHeight ; if ( x + width > this . _width ) { x -= width ; } ctx . globalAlpha = tooltipBackgroundAlpha ; ctx . fillStyle = tooltipBackgroundColor ; ctx . fillRect ( x , y , width , tooltipHeight ) ; ctx . globalAlpha = defaultAlpha ; x += tooltipHorizontalPadding ; y += textHeight ; for ( let i = 0 ; i < numberOfTooltipItems ; i ++ ) { const tooltipItem = this . _tooltipItems [ i ] ; ctx . fillStyle = tooltipItem . color ; ctx . fillRect ( x , y - Math . floor ( boxLength / 2 ) , boxLength , boxLength ) ; ctx . fillStyle = tooltipForegroundColor ; ctx . fillText ( tooltipItem . text , x + boxLength + spaceBetweenTextAndBox , y ) ; y += textHeight ; } ctx . restore ( ) ; } param pixel param minMax param startingPixel param endingPixel param shouldFlipValue returns private _getNumberFromPixel ( pixel , minMax , startingPixel , endingPixel , shouldFlip ) { let normalizedPixelPosition = ( pixel - startingPixel ) / ( endingPixel - startingPixel ) ; if ( shouldFlip ) { normalizedPixelPosition = 1 - normalizedPixelPosition ; } return minMax . min + normalizedPixelPosition * ( minMax . max - minMax . min ) ; } param event private _handleZoom = ( event ) => { event . preventDefault ( ) ; if ( ! event . deltaY ) { return ; } const amount = ( ( event . deltaY * - 0.01 ) | 0 ) * 100 ; const minZoom = 60 ; const maxZoom = this . _getNumberOfSlices ( ) ; if ( this . _shouldBecomeRealtime ( ) ) { this . _position = null ; } this . _sizeOfWindow = Scalar . Clamp ( this . _sizeOfWindow - amount , minZoom , maxZoom ) ; } ; param event private _handlePanStart = ( event ) => { const { _ctx : ctx } = this ; if ( ! ctx || ! ctx . canvas ) { return ; } const canvas = ctx . canvas ; this . _panPosition = { xPos : event . clientX , delta : 0 , } ; this . _hoverPosition = null ; canvas . addEventListener ( "mousemove" , this . _handlePan ) ; } ; param event private _handlePan = ( event ) => { if ( ! this . _panPosition || this . _getNumberOfSlices ( ) === 0 ) { return ; } const pixelDelta = this . _panPosition . delta + event . clientX - this . _panPosition . xPos ; const pixelsPerItem = ( this . _drawableArea . right - this . _drawableArea . left ) / this . _sizeOfWindow ; const itemsDelta = ( pixelDelta / pixelsPerItem ) | 0 ; const pos = this . _position ?? this . _getNumberOfSlices ( ) - 1 ; this . _position = Scalar . Clamp ( pos - itemsDelta , Math . floor ( this . _sizeOfWindow * scaleFactor ) , this . _getNumberOfSlices ( ) - Math . floor ( this . _sizeOfWindow * ( 1 - scaleFactor ) ) ) ; if ( itemsDelta === 0 ) { this . _panPosition . delta += pixelDelta ; } else { this . _panPosition . delta = 0 ; } this . _panPosition . xPos = event . clientX ; this . _prevPointById . clear ( ) ; this . _prevValueById . clear ( ) ; } ; param event private _handlePanStop = ( ) => { const { _ctx : ctx } = this ; if ( ! ctx || ! ctx . canvas ) { return ; } if ( this . _shouldBecomeRealtime ( ) ) { this . _position = null ; } const canvas = ctx . canvas ; canvas . removeEventListener ( "mousemove" , this . _handlePan ) ; this . _panPosition = null ; } ; returns private _shouldBecomeRealtime ( ) { if ( this . _getNumberOfSlices ( ) === 0 ) { return false ; } const pos = this . _position ; const latestSlicePos = this . _getNumberOfSlices ( ) - 1 ; if ( pos === null ) { return false ; } const overflow = Math . max ( 0 - ( pos - Math . ceil ( this . _sizeOfWindow * scaleFactor ) ) , 0 ) ; const rightmostPos = Math . min ( overflow + pos + Math . ceil ( this . _sizeOfWindow * ( 1 - scaleFactor ) ) , latestSlicePos ) ; return ( this . datasets . data . at ( this . datasets . startingIndices . at ( rightmostPos ) ) / this . datasets . data . at ( this . datasets . startingIndices . at ( latestSlicePos ) ) > returnToLiveThreshold ) ; } param drawableArea param scaleFactor private _drawPlayheadRegion ( drawableArea , scaleFactor ) { const { _ctx : ctx } = this ; if ( ! ctx || scaleFactor >= stopDrawingPlayheadThreshold ) { return ; } const dividerXPos = Math . ceil ( drawableArea . right * scaleFactor ) ; const playheadPos = dividerXPos - playheadSize ; const futureBoxPos = dividerXPos + dividerSize ; const rectangleHeight = drawableArea . bottom - drawableArea . top - 1 ; ctx . save ( ) ; ctx . fillStyle = futureBoxColor ; ctx . fillRect ( futureBoxPos , drawableArea . top , drawableArea . right - futureBoxPos , rectangleHeight ) ; ctx . fillStyle = dividerColor ; ctx . fillRect ( dividerXPos , drawableArea . top , dividerSize , rectangleHeight ) ; ctx . fillStyle = playheadColor ; ctx . fillRect ( playheadPos , drawableArea . top , playheadSize , rectangleHeight ) ; ctx . restore ( ) ; } /**
     *  Method to do cleanup when the object is done being used.
     *
     */ public destroy ( ) { if ( ! this . _ctx || ! this . _ctx . canvas ) { return ; } this . _removeEventListeners ( this . _ctx . canvas ) ; this . _ctx = null ; } /**
     * This method clears the canvas
     */ public clear ( ) { const { _ctx : ctx , _width , _height } = this ; if ( ! ctx ) { return ; } ctx . save ( ) ; ctx . resetTransform ( ) ; ctx . clearRect ( 0 , 0 , _width , _height ) ; ctx . restore ( ) ; } }